@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/widget.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 = `
|
|
@@ -4967,6 +5261,9 @@ function pinnedTopbarActionIds(comp) {
|
|
|
4967
5261
|
}
|
|
4968
5262
|
|
|
4969
5263
|
// src/widget/contributions.ts
|
|
5264
|
+
function mobilePlacement(action) {
|
|
5265
|
+
return action.mobile ?? (action.align === "left" ? "bar" : "menu");
|
|
5266
|
+
}
|
|
4970
5267
|
var registry3 = /* @__PURE__ */ new Map();
|
|
4971
5268
|
var attachments = /* @__PURE__ */ new Map();
|
|
4972
5269
|
var panels = /* @__PURE__ */ new Map();
|
|
@@ -5204,6 +5501,22 @@ function priceStyleIds() {
|
|
|
5204
5501
|
for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
|
|
5205
5502
|
return out;
|
|
5206
5503
|
}
|
|
5504
|
+
function sanitizeCrosshairOverride(value) {
|
|
5505
|
+
if (!value || typeof value !== "object") return null;
|
|
5506
|
+
const v = value;
|
|
5507
|
+
const out = {};
|
|
5508
|
+
if (isBool(v.vertical)) out.vertical = v.vertical;
|
|
5509
|
+
if (isBool(v.horizontal)) out.horizontal = v.horizontal;
|
|
5510
|
+
if (isColor(v.color)) out.color = v.color;
|
|
5511
|
+
if (isNum(v.width)) out.width = Math.max(0.5, Math.min(8, v.width));
|
|
5512
|
+
if (isLineStyle(v.style)) out.style = v.style;
|
|
5513
|
+
if (isNum(v.opacity)) out.opacity = clampOpacity(v.opacity);
|
|
5514
|
+
const shade = v.shadeRight;
|
|
5515
|
+
if (shade && typeof shade === "object" && isColor(shade.color)) {
|
|
5516
|
+
out.shadeRight = isNum(shade.opacity) ? { color: shade.color, opacity: clampOpacity(shade.opacity) } : { color: shade.color };
|
|
5517
|
+
}
|
|
5518
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
5519
|
+
}
|
|
5207
5520
|
function isColor(v) {
|
|
5208
5521
|
return typeof v === "string" && v.trim().length > 0;
|
|
5209
5522
|
}
|
|
@@ -6091,8 +6404,8 @@ var Topbar = class {
|
|
|
6091
6404
|
// src/core/util/wall-clock.ts
|
|
6092
6405
|
var BOUNDARY_SLACK_MS = 5;
|
|
6093
6406
|
var SecondClock = class _SecondClock {
|
|
6094
|
-
constructor(
|
|
6095
|
-
this.now =
|
|
6407
|
+
constructor(now2 = () => Date.now()) {
|
|
6408
|
+
this.now = now2;
|
|
6096
6409
|
this.subs = /* @__PURE__ */ new Set();
|
|
6097
6410
|
this.timer = null;
|
|
6098
6411
|
}
|
|
@@ -6108,15 +6421,15 @@ var SecondClock = class _SecondClock {
|
|
|
6108
6421
|
};
|
|
6109
6422
|
}
|
|
6110
6423
|
/** Milliseconds from `now` to just past the next second boundary. */
|
|
6111
|
-
static delayToNextSecond(
|
|
6112
|
-
const intoSecond = (
|
|
6424
|
+
static delayToNextSecond(now2) {
|
|
6425
|
+
const intoSecond = (now2 % 1e3 + 1e3) % 1e3;
|
|
6113
6426
|
return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
|
|
6114
6427
|
}
|
|
6115
6428
|
arm() {
|
|
6116
6429
|
this.timer = setTimeout(() => {
|
|
6117
6430
|
this.timer = null;
|
|
6118
|
-
const
|
|
6119
|
-
for (const cb of this.subs) cb(
|
|
6431
|
+
const now2 = this.now();
|
|
6432
|
+
for (const cb of this.subs) cb(now2);
|
|
6120
6433
|
if (this.subs.size > 0) this.arm();
|
|
6121
6434
|
}, _SecondClock.delayToNextSecond(this.now()));
|
|
6122
6435
|
}
|
|
@@ -14933,7 +15246,12 @@ var TimeframeQuick = class {
|
|
|
14933
15246
|
content: (body) => body.append(this.input, this.hint),
|
|
14934
15247
|
onOpenChange: (open2) => opts.onOpenChange?.(open2)
|
|
14935
15248
|
});
|
|
14936
|
-
this.input.addEventListener("input", () =>
|
|
15249
|
+
this.input.addEventListener("input", () => {
|
|
15250
|
+
const { selectionStart, selectionEnd } = this.input;
|
|
15251
|
+
this.input.value = this.input.value.toUpperCase();
|
|
15252
|
+
this.input.setSelectionRange(selectionStart, selectionEnd);
|
|
15253
|
+
this.renderHint();
|
|
15254
|
+
});
|
|
14937
15255
|
this.input.addEventListener("keydown", (e) => {
|
|
14938
15256
|
if (e.key !== "Enter") return;
|
|
14939
15257
|
const parsed = parseTimeframe(this.input.value);
|
|
@@ -14945,7 +15263,7 @@ var TimeframeQuick = class {
|
|
|
14945
15263
|
}
|
|
14946
15264
|
open(seed = "") {
|
|
14947
15265
|
this.dialog.show();
|
|
14948
|
-
this.input.value = seed;
|
|
15266
|
+
this.input.value = seed.toUpperCase();
|
|
14949
15267
|
this.renderHint();
|
|
14950
15268
|
setTimeout(() => {
|
|
14951
15269
|
this.input.focus();
|
|
@@ -14963,7 +15281,7 @@ var TimeframeQuick = class {
|
|
|
14963
15281
|
this.open(text);
|
|
14964
15282
|
return;
|
|
14965
15283
|
}
|
|
14966
|
-
this.input.value += text;
|
|
15284
|
+
this.input.value += text.toUpperCase();
|
|
14967
15285
|
this.renderHint();
|
|
14968
15286
|
}
|
|
14969
15287
|
close() {
|
|
@@ -14976,7 +15294,7 @@ var TimeframeQuick = class {
|
|
|
14976
15294
|
const raw = this.input.value.trim();
|
|
14977
15295
|
const parsed = parseTimeframe(raw);
|
|
14978
15296
|
if (!raw) {
|
|
14979
|
-
this.hint.textContent = "e.g. 15,
|
|
15297
|
+
this.hint.textContent = "e.g. 15, 4H, D, 3M";
|
|
14980
15298
|
delete this.hint.dataset.invalid;
|
|
14981
15299
|
} else if (parsed.valid) {
|
|
14982
15300
|
this.hint.textContent = parsed.label ?? "";
|
|
@@ -15079,7 +15397,7 @@ var ShortcutsHelp = class {
|
|
|
15079
15397
|
addRow("Delete the drawing under the cursor", ["Middle-click"]);
|
|
15080
15398
|
const s = doc.createElement("div");
|
|
15081
15399
|
s.className = "vela-sh-static";
|
|
15082
|
-
s.textContent = "Typing a letter opens the symbol search; typing a digit opens the timeframe entry.";
|
|
15400
|
+
s.textContent = "Typing a letter or 0 opens the symbol search; typing a digit from 1 to 9 opens the timeframe entry.";
|
|
15083
15401
|
this.list.appendChild(s);
|
|
15084
15402
|
}
|
|
15085
15403
|
};
|
|
@@ -15370,16 +15688,16 @@ var MobileBar = class {
|
|
|
15370
15688
|
host.appendChild(this.el);
|
|
15371
15689
|
this.renderActions();
|
|
15372
15690
|
}
|
|
15373
|
-
/** Re-project the
|
|
15374
|
-
* indicators slot (call after
|
|
15375
|
-
*
|
|
15691
|
+
/** Re-project the contributed actions placed on the bar (left-aligned by default —
|
|
15692
|
+
* see `mobilePlacement`) as icon-only stops in the indicators slot (call after
|
|
15693
|
+
* registrations change). The rest are three-dots drawer rows. */
|
|
15376
15694
|
renderActions() {
|
|
15377
15695
|
const ctx = this.opts.getContext?.();
|
|
15378
15696
|
if (!ctx) return;
|
|
15379
15697
|
const doc = this.el.ownerDocument;
|
|
15380
15698
|
this.actionsHost.replaceChildren();
|
|
15381
15699
|
const builtin = new Set(TOPBAR_BUILTIN_IDS);
|
|
15382
|
-
for (const action of widgetActions("topbar", ctx).filter((a) => a
|
|
15700
|
+
for (const action of widgetActions("topbar", ctx).filter((a) => mobilePlacement(a) === "bar" && !builtin.has(a.id))) {
|
|
15383
15701
|
const b = doc.createElement("button");
|
|
15384
15702
|
b.className = "vela-mb-item";
|
|
15385
15703
|
b.setAttribute("aria-label", action.label);
|
|
@@ -15935,6 +16253,7 @@ var MoreDrawer = class {
|
|
|
15935
16253
|
const value = shape ? `${shape.cols} \xD7 ${shape.rows}` : this.opts.layout.presets().find((p) => p.checked)?.label;
|
|
15936
16254
|
list.appendChild(this.row(doc, "Layout", { icon: "layout", value, chevron: true, onClick: () => this.show("layout") }));
|
|
15937
16255
|
}
|
|
16256
|
+
for (const act of this.opts.primaryActions?.() ?? []) list.appendChild(this.actionRow(doc, act));
|
|
15938
16257
|
for (const panel of this.opts.panels()) {
|
|
15939
16258
|
list.appendChild(
|
|
15940
16259
|
this.row(doc, panel.title, {
|
|
@@ -15950,19 +16269,19 @@ var MoreDrawer = class {
|
|
|
15950
16269
|
const alertCount = this.opts.alerts().length;
|
|
15951
16270
|
list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
|
|
15952
16271
|
}
|
|
15953
|
-
for (const act of this.opts.actions())
|
|
15954
|
-
list.appendChild(
|
|
15955
|
-
this.row(doc, act.label, {
|
|
15956
|
-
icon: act.icon,
|
|
15957
|
-
onClick: () => {
|
|
15958
|
-
act.run();
|
|
15959
|
-
this.drawer.hide();
|
|
15960
|
-
}
|
|
15961
|
-
})
|
|
15962
|
-
);
|
|
15963
|
-
}
|
|
16272
|
+
for (const act of this.opts.actions()) list.appendChild(this.actionRow(doc, act));
|
|
15964
16273
|
this.drawer.body.appendChild(list);
|
|
15965
16274
|
}
|
|
16275
|
+
/** A contributed action's row: runs it and closes the drawer. */
|
|
16276
|
+
actionRow(doc, act) {
|
|
16277
|
+
return this.row(doc, act.label, {
|
|
16278
|
+
icon: act.icon,
|
|
16279
|
+
onClick: () => {
|
|
16280
|
+
act.run();
|
|
16281
|
+
this.drawer.hide();
|
|
16282
|
+
}
|
|
16283
|
+
});
|
|
16284
|
+
}
|
|
15966
16285
|
renderStyle(doc) {
|
|
15967
16286
|
const list = doc.createElement("div");
|
|
15968
16287
|
list.className = "vela-md-list";
|
|
@@ -18100,23 +18419,21 @@ function arrowHitHtml(svg) {
|
|
|
18100
18419
|
return `<span class="vela-dtb-hit vela-dtb-hit--arrow">${iconSpan(svg, 11)}</span>`;
|
|
18101
18420
|
}
|
|
18102
18421
|
|
|
18103
|
-
// src/renderers/native/chrome/
|
|
18104
|
-
var
|
|
18105
|
-
var
|
|
18422
|
+
// src/renderers/native/chrome/vela-logos.ts
|
|
18423
|
+
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>';
|
|
18424
|
+
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>';
|
|
18106
18425
|
|
|
18107
18426
|
// src/renderers/native/chrome/AttributionMark.ts
|
|
18108
|
-
var ATTRIBUTION_URL = "https://
|
|
18427
|
+
var ATTRIBUTION_URL = "https://velacharts.dev/?utm_source=vela&utm_medium=attribution";
|
|
18109
18428
|
var STYLE_ID20 = "vela-attribution-styles";
|
|
18110
18429
|
var CSS20 = `
|
|
18111
18430
|
.vela-attribution {
|
|
18112
18431
|
text-decoration: none;
|
|
18113
18432
|
display: flex;
|
|
18114
18433
|
align-items: center;
|
|
18115
|
-
/*
|
|
18116
|
-
|
|
18117
|
-
|
|
18118
|
-
reads wrong because the symbol's ink hangs low and the wordmark descends. */
|
|
18119
|
-
gap: 4px;
|
|
18434
|
+
/* No gutter: the V is the wordmark's first letter, and the letters' viewBox starts
|
|
18435
|
+
where the V's ends, so the kit's own letter spacing comes with them. */
|
|
18436
|
+
gap: 0;
|
|
18120
18437
|
}
|
|
18121
18438
|
.vela-attribution .vela-attr-symbol {
|
|
18122
18439
|
flex: none;
|
|
@@ -18124,7 +18441,7 @@ var CSS20 = `
|
|
|
18124
18441
|
line-height: 0;
|
|
18125
18442
|
}
|
|
18126
18443
|
.vela-attribution .vela-attr-symbol svg {
|
|
18127
|
-
height:
|
|
18444
|
+
height: 24px;
|
|
18128
18445
|
width: auto;
|
|
18129
18446
|
display: block;
|
|
18130
18447
|
filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
|
|
@@ -18135,15 +18452,13 @@ var CSS20 = `
|
|
|
18135
18452
|
opacity: 0;
|
|
18136
18453
|
display: flex;
|
|
18137
18454
|
align-items: center;
|
|
18138
|
-
/*
|
|
18139
|
-
position: relative;
|
|
18140
|
-
top: 3px;
|
|
18455
|
+
/* Same height and baseline as the V (one viewBox height), so no vertical offset. */
|
|
18141
18456
|
transform: translateX(-8px);
|
|
18142
18457
|
transition: max-width 0.3s ease, opacity 0.25s ease, transform 0.3s ease;
|
|
18143
18458
|
flex: none;
|
|
18144
18459
|
}
|
|
18145
18460
|
.vela-attribution .vela-attr-wordmark svg {
|
|
18146
|
-
height:
|
|
18461
|
+
height: 24px;
|
|
18147
18462
|
width: auto;
|
|
18148
18463
|
display: block;
|
|
18149
18464
|
filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
|
|
@@ -18158,14 +18473,11 @@ var CSS20 = `
|
|
|
18158
18473
|
(data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
|
|
18159
18474
|
container, so only the shell attribute reaches it. */
|
|
18160
18475
|
[data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
|
|
18161
|
-
[data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height:
|
|
18476
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 20px; }
|
|
18162
18477
|
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
|
|
18163
|
-
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
|
|
18164
|
-
top: 2px;
|
|
18165
|
-
transform: translateX(-6px);
|
|
18166
|
-
}
|
|
18478
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark { transform: translateX(-6px); }
|
|
18167
18479
|
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
|
|
18168
|
-
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height:
|
|
18480
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 20px; }
|
|
18169
18481
|
[data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
|
|
18170
18482
|
[data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
|
|
18171
18483
|
`;
|
|
@@ -18222,10 +18534,10 @@ function createAttributionMark(doc, background) {
|
|
|
18222
18534
|
const symbol = doc.createElement("span");
|
|
18223
18535
|
symbol.className = "vela-attr-symbol";
|
|
18224
18536
|
symbol.setAttribute("aria-hidden", "true");
|
|
18225
|
-
symbol.innerHTML =
|
|
18537
|
+
symbol.innerHTML = VELA_MARK_SVG;
|
|
18226
18538
|
const wordmark = doc.createElement("span");
|
|
18227
18539
|
wordmark.className = "vela-attr-wordmark";
|
|
18228
|
-
wordmark.innerHTML =
|
|
18540
|
+
wordmark.innerHTML = VELA_LETTERS_SVG;
|
|
18229
18541
|
a.append(symbol, wordmark);
|
|
18230
18542
|
return a;
|
|
18231
18543
|
}
|
|
@@ -18336,6 +18648,7 @@ function sanitizeCell(raw) {
|
|
|
18336
18648
|
if (typeof c.bars === "number" && Number.isFinite(c.bars) && c.bars > 0) out.bars = c.bars;
|
|
18337
18649
|
if (c.session === "regular" || c.session === "extended") out.session = c.session;
|
|
18338
18650
|
if (typeof c.watermark === "boolean") out.watermark = c.watermark;
|
|
18651
|
+
if (typeof c.replayWatermark === "boolean") out.replayWatermark = c.replayWatermark;
|
|
18339
18652
|
if (typeof c.indicatorTitles === "boolean") out.indicatorTitles = c.indicatorTitles;
|
|
18340
18653
|
if (typeof c.indicatorValues === "boolean") out.indicatorValues = c.indicatorValues;
|
|
18341
18654
|
if (c.rendererConfig != null && typeof c.rendererConfig === "object") out.rendererConfig = c.rendererConfig;
|
|
@@ -19074,6 +19387,7 @@ var CHUNK_BARS = 1e4;
|
|
|
19074
19387
|
var FIRST_PAINT_BARS = 20;
|
|
19075
19388
|
var GAP_FACTOR = 1.5;
|
|
19076
19389
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
19390
|
+
var MIN_TICK_UPDATE_MS = 16;
|
|
19077
19391
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
19078
19392
|
constructor(container, renderer, feed, engines, config, dataControl) {
|
|
19079
19393
|
this.renderer = renderer;
|
|
@@ -19126,6 +19440,32 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19126
19440
|
this.lastHealAt = 0;
|
|
19127
19441
|
/** The `visibilitychange` catch-up listener (removed on destroy). */
|
|
19128
19442
|
this.onVisible = null;
|
|
19443
|
+
// ── bar replay: `rawBars` holds the history up to the cursor, the rest waits here ──
|
|
19444
|
+
/** Bars still hidden right of the replay cursor, oldest first; null ≡ replay off. */
|
|
19445
|
+
this.replayQueue = null;
|
|
19446
|
+
this.replayTimer = null;
|
|
19447
|
+
this.replayPlaying = false;
|
|
19448
|
+
this.replayIntervalMs = 1e3;
|
|
19449
|
+
/** The depth (`market.bars`) before a replay deepened the history — restored when it ends. */
|
|
19450
|
+
this.replayDepth = null;
|
|
19451
|
+
/** A start is loading older history before it can cut; `seek` = a replay was running
|
|
19452
|
+
* (and left silently to deepen), so abandoning the load must announce its end. */
|
|
19453
|
+
this.replayLoading = null;
|
|
19454
|
+
/** A replay carried over a timeframe/session switch, between the old series' silent end
|
|
19455
|
+
* and the cut of the new one: still ACTIVE for the outside (`replayState`), resumed
|
|
19456
|
+
* at `revealedEnd` once the new bars land, unless stopped (`cancelled`) meanwhile. */
|
|
19457
|
+
this.replayCarrying = null;
|
|
19458
|
+
/** Intrabar prices for the bars replay reveals — null ≡ whole bars. */
|
|
19459
|
+
this.replayTickSource = null;
|
|
19460
|
+
/** The bar being revealed tick by tick: its stored form (what it settles on), its ticks,
|
|
19461
|
+
* the next tick to apply, the candle built so far, the volume each tick adds when the
|
|
19462
|
+
* ticks carry none, and its pacing (ticks per update, ms between updates — one play
|
|
19463
|
+
* interval per tick — and the clock time tick 0 is due at, so late timers catch up). */
|
|
19464
|
+
this.replayForming = null;
|
|
19465
|
+
/** Tick requests by bar time, shared by prefetch and playback; `ticks` once they landed. */
|
|
19466
|
+
this.replayTickFetches = /* @__PURE__ */ new Map();
|
|
19467
|
+
/** Aborts the tick requests in flight — renewed whenever the replay moves on. */
|
|
19468
|
+
this.replayTickAbort = null;
|
|
19129
19469
|
/** What the last applied bar did: refine the open candle, or open a new one (which
|
|
19130
19470
|
* settles the previous). Read by the `script:run` cause attribution. */
|
|
19131
19471
|
this.barCause = "tick";
|
|
@@ -19349,6 +19689,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19349
19689
|
const paint = (bars, final) => {
|
|
19350
19690
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
19351
19691
|
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
19692
|
+
if (painted && this.replayQueue) {
|
|
19693
|
+
this.mergeHistoryIntoReplay(bars);
|
|
19694
|
+
return;
|
|
19695
|
+
}
|
|
19352
19696
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
19353
19697
|
if (!painted && bars.length > 0) {
|
|
19354
19698
|
painted = true;
|
|
@@ -19491,6 +19835,36 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19491
19835
|
// exactly like a timeframe change; the cache keys the sessions apart.
|
|
19492
19836
|
next.session !== void 0 && next.session !== m.session || next.data !== void 0;
|
|
19493
19837
|
const depthChanged = next.bars !== void 0 && next.bars !== m.bars;
|
|
19838
|
+
const sameClock = identityChanged && next.data === void 0 && (next.symbol === void 0 || next.symbol === m.symbol) && (next.timeframe !== void 0 || next.session !== void 0);
|
|
19839
|
+
const pending = identityChanged ? this.replayCarrying : null;
|
|
19840
|
+
if (pending) {
|
|
19841
|
+
this.replayCarrying = null;
|
|
19842
|
+
if (!sameClock) {
|
|
19843
|
+
pending.cancelled = true;
|
|
19844
|
+
this.events.emit("replay:end", { reason: "market" });
|
|
19845
|
+
}
|
|
19846
|
+
}
|
|
19847
|
+
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;
|
|
19848
|
+
if (pending) pending.cancelled = true;
|
|
19849
|
+
if (replayCarry) this.replayCarrying = replayCarry;
|
|
19850
|
+
if (replayCarry && this.replayQueue) {
|
|
19851
|
+
this.endReplay(null, { restore: false, resume: false });
|
|
19852
|
+
const depth = this.replayDepth;
|
|
19853
|
+
this.replayDepth = null;
|
|
19854
|
+
if (depth && next.bars === void 0) {
|
|
19855
|
+
if (depth.bars === void 0) delete m.bars;
|
|
19856
|
+
else m.bars = depth.bars;
|
|
19857
|
+
}
|
|
19858
|
+
}
|
|
19859
|
+
if (this.replayQueue && (identityChanged || depthChanged)) this.endReplay("market", { restore: !identityChanged, resume: false });
|
|
19860
|
+
if (this.replayLoading && (identityChanged || depthChanged)) {
|
|
19861
|
+
const depth = this.replayDepth;
|
|
19862
|
+
this.abandonReplayLoad("market");
|
|
19863
|
+
if (depth && next.bars === void 0) {
|
|
19864
|
+
if (depth.bars === void 0) delete m.bars;
|
|
19865
|
+
else m.bars = depth.bars;
|
|
19866
|
+
}
|
|
19867
|
+
}
|
|
19494
19868
|
const depthOnly = depthChanged && !identityChanged && this.rawBars.length > 0 && !m.data?.length && ((next.bars ?? 0) <= this.rawBars.length || typeof this.feed.loadRange === "function");
|
|
19495
19869
|
if (!identityChanged && !depthChanged) {
|
|
19496
19870
|
if (typeof next.visibleRange === "string") this.setVisibleRangePreset(next.visibleRange);
|
|
@@ -19548,15 +19922,20 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19548
19922
|
const from = bars[Math.max(0, idx - (carriedCount - 1))].time;
|
|
19549
19923
|
this.renderer.setVisibleRange({ from, to: Math.max(carried.right, bars[idx].time + 1) });
|
|
19550
19924
|
}
|
|
19551
|
-
|
|
19925
|
+
const carry = replayCarry && !replayCarry.cancelled ? replayCarry : null;
|
|
19926
|
+
const carryFrom = carry ? carry.revealedEnd - this.barIntervalMs() : 0;
|
|
19927
|
+
const carryNow = carry != null && this.rawBars.length > 0 && carryFrom >= this.rawBars[0].time;
|
|
19928
|
+
const replayBack = carryNow && this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
|
|
19929
|
+
if (!depthOnly && !replayBack) {
|
|
19552
19930
|
this.restartNativeIndicators();
|
|
19553
19931
|
this.restartChartTypeEngine();
|
|
19554
19932
|
this.reexecuteIndicators();
|
|
19555
19933
|
}
|
|
19556
|
-
this.startLive();
|
|
19934
|
+
if (!replayBack) this.startLive();
|
|
19557
19935
|
if (identityChanged) {
|
|
19558
19936
|
this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev2 });
|
|
19559
19937
|
}
|
|
19938
|
+
if (carry && !carryNow) this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
|
|
19560
19939
|
}
|
|
19561
19940
|
/**
|
|
19562
19941
|
* Restart every native indicator over the new market: a `NativeIndicatorContext`
|
|
@@ -19680,6 +20059,397 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19680
20059
|
historyComplete() {
|
|
19681
20060
|
return this.historyCompletePromise;
|
|
19682
20061
|
}
|
|
20062
|
+
// ── bar replay ──────────────────────────────────────────────
|
|
20063
|
+
/**
|
|
20064
|
+
* Enter replay — or seek, when already replaying — keeping every bar opened at or before
|
|
20065
|
+
* `opts.from` and queueing the rest. A `from` inside the loaded bars cuts at once, even
|
|
20066
|
+
* while older history is still streaming in: what lands later joins the tape's head
|
|
20067
|
+
* (see {@link mergeHistoryIntoReplay}) and leaves the cut alone. An older `from` waits
|
|
20068
|
+
* for that load, then deepens if it still has to. Consumers restart over the cut exactly
|
|
20069
|
+
* as on a market switch — their models describe bars that are now hidden — and the
|
|
20070
|
+
* active chart-type engine is rebuilt without live data.
|
|
20071
|
+
*/
|
|
20072
|
+
async replayStart(opts) {
|
|
20073
|
+
if (this.rawBars.length === 0) {
|
|
20074
|
+
console.warn("[vela] chart.replay.start() ignored \u2014 the chart has no bars yet");
|
|
20075
|
+
return;
|
|
20076
|
+
}
|
|
20077
|
+
let gen = this.generation;
|
|
20078
|
+
if (opts.from < this.rawBars[0].time) {
|
|
20079
|
+
await this.historyCompletePromise;
|
|
20080
|
+
if (this.generation !== gen || this.rawBars.length === 0) return;
|
|
20081
|
+
}
|
|
20082
|
+
const oldest = this.rawBars[0].time;
|
|
20083
|
+
if (opts.from < oldest && this.canHeal() && typeof this.feed.loadRange === "function") {
|
|
20084
|
+
const held = this.rawBars.length + (this.replayQueue?.length ?? 0);
|
|
20085
|
+
const bars = held + Math.ceil((oldest - opts.from) / this.barIntervalMs()) + 1;
|
|
20086
|
+
const seek = this.replayQueue !== null;
|
|
20087
|
+
if (seek) this.endReplay(null, { restore: true, resume: false });
|
|
20088
|
+
this.replayDepth ?? (this.replayDepth = { bars: this.config.market.bars });
|
|
20089
|
+
const deepened = this.setMarket({ bars });
|
|
20090
|
+
this.replayLoading = { seek };
|
|
20091
|
+
await deepened;
|
|
20092
|
+
gen = this.generation;
|
|
20093
|
+
await this.historyCompletePromise;
|
|
20094
|
+
if (!this.replayLoading || this.generation !== gen || this.rawBars.length === 0) return;
|
|
20095
|
+
this.replayLoading = null;
|
|
20096
|
+
}
|
|
20097
|
+
this.settleFormingBar();
|
|
20098
|
+
this.dropTickFetches();
|
|
20099
|
+
const tape = this.replayQueue ? [...this.rawBars, ...this.replayQueue] : this.rawBars;
|
|
20100
|
+
let cut = 0;
|
|
20101
|
+
while (cut < tape.length && tape[cut].time <= opts.from) cut += 1;
|
|
20102
|
+
cut = Math.max(1, cut);
|
|
20103
|
+
if (cut >= tape.length) {
|
|
20104
|
+
console.warn("[vela] chart.replay.start() ignored \u2014 no bar opens after the requested time");
|
|
20105
|
+
return;
|
|
20106
|
+
}
|
|
20107
|
+
const wasPlaying = this.replayPlaying;
|
|
20108
|
+
this.clearReplayTimer();
|
|
20109
|
+
if (!this.replayQueue) this.stopLive();
|
|
20110
|
+
this.replayQueue = tape.slice(cut);
|
|
20111
|
+
this.setBarSeries(tape.slice(0, cut));
|
|
20112
|
+
this.restartBarConsumers({ blank: true });
|
|
20113
|
+
if (wasPlaying) this.events.emit("replay:pause", void 0);
|
|
20114
|
+
this.events.emit("replay:start", { cursorTime: tape[cut - 1].time, remaining: this.replayQueue.length });
|
|
20115
|
+
this.prefetchTicks();
|
|
20116
|
+
}
|
|
20117
|
+
/** Reveal the next queued bar through the live-bar path — or complete the bar forming
|
|
20118
|
+
* tick by tick; the last one ends the replay. */
|
|
20119
|
+
replayStep() {
|
|
20120
|
+
if (this.replayForming) {
|
|
20121
|
+
this.finishFormingBar();
|
|
20122
|
+
return true;
|
|
20123
|
+
}
|
|
20124
|
+
const queue = this.replayQueue;
|
|
20125
|
+
const bar = queue?.shift();
|
|
20126
|
+
if (!queue || !bar) return false;
|
|
20127
|
+
this.replayTickFetches.delete(bar.time);
|
|
20128
|
+
this.applyBar(bar);
|
|
20129
|
+
this.completeReveal(bar);
|
|
20130
|
+
this.prefetchTicks();
|
|
20131
|
+
return true;
|
|
20132
|
+
}
|
|
20133
|
+
/**
|
|
20134
|
+
* Follow up a replay carried onto a new series of the same symbol (another timeframe or
|
|
20135
|
+
* session). The cut asked for keeps the bars that CLOSED by the end of what was revealed —
|
|
20136
|
+
* every finer bar of the last revealed one, never a coarser bar still open at that point
|
|
20137
|
+
* (no look-ahead). Once it settles: playback resumes if it was on; if the replay did not
|
|
20138
|
+
* come back (nothing left after that point, or a later switch took over), it ends as a
|
|
20139
|
+
* market switch would. Returns whether the replay is back already — a start with no
|
|
20140
|
+
* history to wait for cuts synchronously (an async function runs up to its first await).
|
|
20141
|
+
*/
|
|
20142
|
+
resumeCarriedReplay(start, carry) {
|
|
20143
|
+
if (this.replayQueue && this.replayCarrying === carry) this.replayCarrying = null;
|
|
20144
|
+
void start.then(() => {
|
|
20145
|
+
if (this.replayCarrying === carry) this.replayCarrying = null;
|
|
20146
|
+
if (carry.cancelled) return;
|
|
20147
|
+
if (!this.replayQueue) this.events.emit("replay:end", { reason: "market" });
|
|
20148
|
+
else if (carry.playing) this.replayPlay();
|
|
20149
|
+
});
|
|
20150
|
+
return this.replayQueue !== null;
|
|
20151
|
+
}
|
|
20152
|
+
replayStepUpdate() {
|
|
20153
|
+
if (!this.replayQueue) return false;
|
|
20154
|
+
if (this.replayForming) {
|
|
20155
|
+
this.applyReplayTicks(1);
|
|
20156
|
+
return true;
|
|
20157
|
+
}
|
|
20158
|
+
const bar = this.replayQueue[0];
|
|
20159
|
+
if (!bar) return false;
|
|
20160
|
+
const entry = this.replayTickSource ? this.requestTicks(bar) : null;
|
|
20161
|
+
if (entry && !entry.ticks) {
|
|
20162
|
+
const epoch = this.replayTickAbort;
|
|
20163
|
+
void entry.done.then(() => {
|
|
20164
|
+
if (!this.replayPlaying && !this.replayForming && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayStepUpdate();
|
|
20165
|
+
});
|
|
20166
|
+
return true;
|
|
20167
|
+
}
|
|
20168
|
+
if (!entry?.ticks?.length) return this.replayStep();
|
|
20169
|
+
this.openFormingBar(entry.ticks, 1);
|
|
20170
|
+
return true;
|
|
20171
|
+
}
|
|
20172
|
+
replaySetTicks(source) {
|
|
20173
|
+
this.finishFormingBar();
|
|
20174
|
+
this.dropTickFetches();
|
|
20175
|
+
this.replayTickSource = source;
|
|
20176
|
+
this.prefetchTicks();
|
|
20177
|
+
}
|
|
20178
|
+
replayPlay(intervalMs2) {
|
|
20179
|
+
if (!this.replayQueue) return;
|
|
20180
|
+
if (intervalMs2 !== void 0) {
|
|
20181
|
+
if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
|
|
20182
|
+
console.warn(`[vela] chart.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
|
|
20183
|
+
return;
|
|
20184
|
+
}
|
|
20185
|
+
this.replayIntervalMs = intervalMs2;
|
|
20186
|
+
}
|
|
20187
|
+
this.clearReplayTimer();
|
|
20188
|
+
this.replayPlaying = true;
|
|
20189
|
+
if (this.replayForming) this.paceFormingBar(this.replayForming);
|
|
20190
|
+
this.scheduleReplayStep(this.replayForming?.delayMs);
|
|
20191
|
+
this.events.emit("replay:play", { intervalMs: this.replayIntervalMs });
|
|
20192
|
+
}
|
|
20193
|
+
replayPause() {
|
|
20194
|
+
if (!this.replayPlaying) return;
|
|
20195
|
+
this.clearReplayTimer();
|
|
20196
|
+
this.events.emit("replay:pause", void 0);
|
|
20197
|
+
}
|
|
20198
|
+
replayStop() {
|
|
20199
|
+
const carrying = this.replayCarrying;
|
|
20200
|
+
if (carrying) {
|
|
20201
|
+
carrying.cancelled = true;
|
|
20202
|
+
this.replayCarrying = null;
|
|
20203
|
+
this.events.emit("replay:end", { reason: "stopped" });
|
|
20204
|
+
}
|
|
20205
|
+
if (this.replayLoading) {
|
|
20206
|
+
const depth = this.replayDepth;
|
|
20207
|
+
this.abandonReplayLoad("stopped");
|
|
20208
|
+
void this.setMarket({ bars: depth?.bars ?? 500 });
|
|
20209
|
+
return;
|
|
20210
|
+
}
|
|
20211
|
+
this.endReplay("stopped", { restore: true, resume: true });
|
|
20212
|
+
}
|
|
20213
|
+
abandonReplayLoad(reason) {
|
|
20214
|
+
const loading = this.replayLoading;
|
|
20215
|
+
if (!loading) return;
|
|
20216
|
+
this.replayLoading = null;
|
|
20217
|
+
this.replayDepth = null;
|
|
20218
|
+
if (loading.seek) this.events.emit("replay:end", { reason });
|
|
20219
|
+
}
|
|
20220
|
+
replayState() {
|
|
20221
|
+
const active = this.replayQueue !== null;
|
|
20222
|
+
return {
|
|
20223
|
+
active: active || this.replayCarrying !== null,
|
|
20224
|
+
// a replay carried over a switch stays on while the new bars load
|
|
20225
|
+
playing: this.replayCarrying?.playing ?? this.replayPlaying,
|
|
20226
|
+
cursorTime: active ? this.rawBars[this.rawBars.length - 1]?.time ?? null : null,
|
|
20227
|
+
remaining: this.replayQueue?.length ?? 0,
|
|
20228
|
+
nextTime: this.replayForming?.bar.time ?? this.replayQueue?.[0]?.time ?? null,
|
|
20229
|
+
intervalMs: this.replayIntervalMs
|
|
20230
|
+
};
|
|
20231
|
+
}
|
|
20232
|
+
replayBounds() {
|
|
20233
|
+
const first = this.rawBars[0];
|
|
20234
|
+
const last = this.replayQueue?.[this.replayQueue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
|
|
20235
|
+
return first && last ? { first: first.time, last: last.time } : null;
|
|
20236
|
+
}
|
|
20237
|
+
scheduleReplayStep(delayMs = this.replayIntervalMs) {
|
|
20238
|
+
this.replayTimer = setTimeout(() => {
|
|
20239
|
+
this.replayTimer = null;
|
|
20240
|
+
this.replayAdvance();
|
|
20241
|
+
}, delayMs);
|
|
20242
|
+
}
|
|
20243
|
+
/** One beat of timed playback: the forming bar's next ticks, else the next bar — whole,
|
|
20244
|
+
* or opened from its ticks (waiting for them when they have not landed yet). */
|
|
20245
|
+
replayAdvance() {
|
|
20246
|
+
if (!this.replayPlaying || !this.replayQueue) return;
|
|
20247
|
+
const forming = this.replayForming;
|
|
20248
|
+
if (forming) {
|
|
20249
|
+
const due = Math.floor((now() - forming.origin) / this.replayIntervalMs) + 1;
|
|
20250
|
+
this.applyReplayTicks(Math.max(forming.batch, due - forming.next));
|
|
20251
|
+
if (this.replayPlaying) this.scheduleReplayStep(forming.delayMs);
|
|
20252
|
+
return;
|
|
20253
|
+
}
|
|
20254
|
+
const bar = this.replayQueue[0];
|
|
20255
|
+
if (!bar) return;
|
|
20256
|
+
const entry = this.replayTickSource ? this.requestTicks(bar) : null;
|
|
20257
|
+
if (entry && !entry.ticks) {
|
|
20258
|
+
const epoch = this.replayTickAbort;
|
|
20259
|
+
void entry.done.then(() => {
|
|
20260
|
+
if (this.replayPlaying && this.replayTimer == null && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayAdvance();
|
|
20261
|
+
});
|
|
20262
|
+
return;
|
|
20263
|
+
}
|
|
20264
|
+
if (!entry?.ticks?.length) {
|
|
20265
|
+
if (this.replayStep() && this.replayPlaying) this.scheduleReplayStep();
|
|
20266
|
+
return;
|
|
20267
|
+
}
|
|
20268
|
+
this.openFormingBar(entry.ticks);
|
|
20269
|
+
if (this.replayPlaying) this.scheduleReplayStep(this.replayForming?.delayMs);
|
|
20270
|
+
}
|
|
20271
|
+
/** Take the next queued bar off the tape and open it at its first tick (the first
|
|
20272
|
+
* `count` ticks — default: one timed update's worth). */
|
|
20273
|
+
openFormingBar(ticks, count) {
|
|
20274
|
+
const bar = this.replayQueue.shift();
|
|
20275
|
+
const volumeEach = ticks.every((t) => t.volume === void 0) ? (bar.volume ?? 0) / ticks.length : null;
|
|
20276
|
+
const forming = { bar, ticks, next: 0, candle: null, volumeEach, batch: 1, delayMs: this.replayIntervalMs, origin: 0 };
|
|
20277
|
+
this.paceFormingBar(forming);
|
|
20278
|
+
this.replayForming = forming;
|
|
20279
|
+
this.applyReplayTicks(count ?? forming.batch);
|
|
20280
|
+
this.prefetchTicks();
|
|
20281
|
+
}
|
|
20282
|
+
/** One tick per interval, batched when the interval is shorter than a frame; the clock
|
|
20283
|
+
* is anchored on the ticks already applied (a resume or a pace change carries on from
|
|
20284
|
+
* there, never jumps). */
|
|
20285
|
+
paceFormingBar(f) {
|
|
20286
|
+
f.batch = Math.max(1, Math.ceil(MIN_TICK_UPDATE_MS / this.replayIntervalMs));
|
|
20287
|
+
f.delayMs = this.replayIntervalMs * f.batch;
|
|
20288
|
+
f.origin = now() - Math.max(0, f.next - 1) * this.replayIntervalMs;
|
|
20289
|
+
}
|
|
20290
|
+
/** Fold the next `count` ticks into the forming candle; the last one settles the bar. */
|
|
20291
|
+
applyReplayTicks(count) {
|
|
20292
|
+
const f = this.replayForming;
|
|
20293
|
+
if (!f) return;
|
|
20294
|
+
let c = f.candle;
|
|
20295
|
+
const stop = Math.min(f.ticks.length, f.next + count);
|
|
20296
|
+
for (; f.next < stop; f.next += 1) {
|
|
20297
|
+
const t = f.ticks[f.next];
|
|
20298
|
+
const v = f.volumeEach ?? t.volume ?? 0;
|
|
20299
|
+
const hi = Math.max(t.high ?? t.price, t.price);
|
|
20300
|
+
const lo = Math.min(t.low ?? t.price, t.price);
|
|
20301
|
+
if (c) {
|
|
20302
|
+
c = { ...c, high: Math.max(c.high, hi), low: Math.min(c.low, lo), close: t.price, volume: (c.volume ?? 0) + v };
|
|
20303
|
+
} else {
|
|
20304
|
+
const open2 = t.open ?? t.price;
|
|
20305
|
+
c = { time: f.bar.time, open: open2, high: Math.max(open2, hi), low: Math.min(open2, lo), close: t.price, volume: v };
|
|
20306
|
+
}
|
|
20307
|
+
}
|
|
20308
|
+
f.candle = c;
|
|
20309
|
+
if (f.next >= f.ticks.length) {
|
|
20310
|
+
this.finishFormingBar();
|
|
20311
|
+
return;
|
|
20312
|
+
}
|
|
20313
|
+
this.applyBar(c);
|
|
20314
|
+
this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.next - 1, count: f.ticks.length });
|
|
20315
|
+
}
|
|
20316
|
+
/** Settle the forming bar on its stored values and announce it revealed. */
|
|
20317
|
+
finishFormingBar() {
|
|
20318
|
+
const f = this.replayForming;
|
|
20319
|
+
if (!f) return;
|
|
20320
|
+
this.replayForming = null;
|
|
20321
|
+
this.replayTickFetches.delete(f.bar.time);
|
|
20322
|
+
this.applyBar(f.bar);
|
|
20323
|
+
this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.ticks.length - 1, count: f.ticks.length });
|
|
20324
|
+
this.completeReveal(f.bar);
|
|
20325
|
+
}
|
|
20326
|
+
/** Put the forming bar back in its stored form, silently — a seek or an end rebuilds
|
|
20327
|
+
* from the tape, which must hold the stored bar. */
|
|
20328
|
+
settleFormingBar() {
|
|
20329
|
+
const f = this.replayForming;
|
|
20330
|
+
if (!f) return;
|
|
20331
|
+
this.replayForming = null;
|
|
20332
|
+
this.applyBar(f.bar);
|
|
20333
|
+
}
|
|
20334
|
+
/**
|
|
20335
|
+
* A history answer landing mid-replay (a progressive snapshot — the whole answer so far):
|
|
20336
|
+
* the tape keeps its cut and its hidden queue; bars older than the tape join its head,
|
|
20337
|
+
* bars newer than it join the queue's end.
|
|
20338
|
+
*/
|
|
20339
|
+
mergeHistoryIntoReplay(bars) {
|
|
20340
|
+
const queue = this.replayQueue;
|
|
20341
|
+
const head = this.rawBars[0];
|
|
20342
|
+
if (!queue || !head) return;
|
|
20343
|
+
const tail = queue[queue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
|
|
20344
|
+
let older = 0;
|
|
20345
|
+
while (older < bars.length && bars[older].time < head.time) older += 1;
|
|
20346
|
+
let newer = bars.length;
|
|
20347
|
+
while (newer > older && bars[newer - 1].time > tail.time) newer -= 1;
|
|
20348
|
+
if (newer < bars.length) queue.push(...bars.slice(newer));
|
|
20349
|
+
if (older > 0) {
|
|
20350
|
+
this.setBarSeries([...bars.slice(0, older), ...this.rawBars], { preserveView: true });
|
|
20351
|
+
this.notifySessionsBars("backfill");
|
|
20352
|
+
}
|
|
20353
|
+
}
|
|
20354
|
+
/** A bar is fully on screen: announce it, and end the replay if it was the last. */
|
|
20355
|
+
completeReveal(bar) {
|
|
20356
|
+
const queue = this.replayQueue;
|
|
20357
|
+
if (!queue) return;
|
|
20358
|
+
this.events.emit("replay:step", { cursorTime: bar.time, remaining: queue.length });
|
|
20359
|
+
if (queue.length === 0) this.endReplay("finished", { restore: true, resume: true });
|
|
20360
|
+
}
|
|
20361
|
+
/** Ask for the ticks of the next hidden bar ahead of time. */
|
|
20362
|
+
prefetchTicks() {
|
|
20363
|
+
const next = this.replayQueue?.[0];
|
|
20364
|
+
if (next && this.replayTickSource) this.requestTicks(next);
|
|
20365
|
+
}
|
|
20366
|
+
/** The (shared) tick request of a queued bar — the source runs once per bar. */
|
|
20367
|
+
requestTicks(bar) {
|
|
20368
|
+
const known = this.replayTickFetches.get(bar.time);
|
|
20369
|
+
if (known) return known;
|
|
20370
|
+
const source = this.replayTickSource;
|
|
20371
|
+
this.replayTickAbort ?? (this.replayTickAbort = new AbortController());
|
|
20372
|
+
const signal = this.replayTickAbort.signal;
|
|
20373
|
+
const queue = this.replayQueue ?? [];
|
|
20374
|
+
const end = queue[queue.indexOf(bar) + 1]?.time ?? bar.time + this.barIntervalMs();
|
|
20375
|
+
const entry = { ticks: null, done: Promise.resolve() };
|
|
20376
|
+
const land = (ticks) => {
|
|
20377
|
+
entry.ticks = usableTicks(ticks);
|
|
20378
|
+
};
|
|
20379
|
+
const fail = (e) => {
|
|
20380
|
+
if (!signal.aborted) console.warn(`[vela] replay tick source failed \u2014 revealing the bar whole (${e instanceof Error ? e.message : String(e)})`);
|
|
20381
|
+
land([]);
|
|
20382
|
+
};
|
|
20383
|
+
try {
|
|
20384
|
+
const out = source(bar, { end, signal });
|
|
20385
|
+
if (Array.isArray(out)) land(out);
|
|
20386
|
+
else entry.done = Promise.resolve(out).then(land, fail);
|
|
20387
|
+
} catch (e) {
|
|
20388
|
+
fail(e);
|
|
20389
|
+
}
|
|
20390
|
+
this.replayTickFetches.set(bar.time, entry);
|
|
20391
|
+
return entry;
|
|
20392
|
+
}
|
|
20393
|
+
/** Abort the tick requests in flight and forget the landed ones. */
|
|
20394
|
+
dropTickFetches() {
|
|
20395
|
+
this.replayTickAbort?.abort();
|
|
20396
|
+
this.replayTickAbort = null;
|
|
20397
|
+
this.replayTickFetches.clear();
|
|
20398
|
+
}
|
|
20399
|
+
clearReplayTimer() {
|
|
20400
|
+
if (this.replayTimer != null) clearTimeout(this.replayTimer);
|
|
20401
|
+
this.replayTimer = null;
|
|
20402
|
+
this.replayPlaying = false;
|
|
20403
|
+
}
|
|
20404
|
+
/**
|
|
20405
|
+
* Leave replay. `restore` puts the queued bars back and restarts the consumers over the
|
|
20406
|
+
* full history (a market switch skips it: its load replaces everything anyway); `resume`
|
|
20407
|
+
* restarts live updates and heals the bars that closed while replaying. A null reason
|
|
20408
|
+
* leaves silently — a seek that re-enters right after.
|
|
20409
|
+
*/
|
|
20410
|
+
endReplay(reason, opts) {
|
|
20411
|
+
const queue = this.replayQueue;
|
|
20412
|
+
if (!queue) return;
|
|
20413
|
+
this.clearReplayTimer();
|
|
20414
|
+
if (opts.restore) this.settleFormingBar();
|
|
20415
|
+
else this.replayForming = null;
|
|
20416
|
+
this.dropTickFetches();
|
|
20417
|
+
this.replayQueue = null;
|
|
20418
|
+
const depth = reason ? this.replayDepth : null;
|
|
20419
|
+
if (reason) this.replayDepth = null;
|
|
20420
|
+
if (depth) {
|
|
20421
|
+
if (depth.bars === void 0) delete this.config.market.bars;
|
|
20422
|
+
else this.config.market.bars = depth.bars;
|
|
20423
|
+
}
|
|
20424
|
+
if (opts.restore) {
|
|
20425
|
+
const keep = depth ? depth.bars ?? 500 : Infinity;
|
|
20426
|
+
if (queue.length > 0 || this.rawBars.length > keep) {
|
|
20427
|
+
const full = [...this.rawBars, ...queue];
|
|
20428
|
+
this.setBarSeries(full.length > keep ? full.slice(full.length - keep) : full);
|
|
20429
|
+
this.restartBarConsumers({ blank: false });
|
|
20430
|
+
} else if (this.config.live) {
|
|
20431
|
+
this.restartNativeIndicators();
|
|
20432
|
+
this.restartChartTypeEngine();
|
|
20433
|
+
}
|
|
20434
|
+
}
|
|
20435
|
+
if (opts.resume && this.config.live) {
|
|
20436
|
+
this.startLive();
|
|
20437
|
+
const last = this.rawBars[this.rawBars.length - 1];
|
|
20438
|
+
if (last && this.canHeal()) void this.healGap(last.time);
|
|
20439
|
+
}
|
|
20440
|
+
if (reason) this.events.emit("replay:end", { reason });
|
|
20441
|
+
}
|
|
20442
|
+
/**
|
|
20443
|
+
* The bars were replaced by a cut or a restore of the SAME market: every consumer re-runs.
|
|
20444
|
+
* A cut also blanks the mounted models first — they still paint the hidden bars' values,
|
|
20445
|
+
* which must not show until the re-runs land.
|
|
20446
|
+
*/
|
|
20447
|
+
restartBarConsumers(opts) {
|
|
20448
|
+
if (opts.blank) this.blankIndicatorVisuals();
|
|
20449
|
+
this.restartNativeIndicators();
|
|
20450
|
+
this.restartChartTypeEngine();
|
|
20451
|
+
this.reexecuteIndicators();
|
|
20452
|
+
}
|
|
19683
20453
|
/** The primary symbol as the data layer resolves it, or null if no symbol is set. */
|
|
19684
20454
|
qualifiedSymbol() {
|
|
19685
20455
|
const market = this.config.market;
|
|
@@ -19751,7 +20521,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19751
20521
|
engine.start({
|
|
19752
20522
|
symbol: this.config.market.symbol ?? "TEST",
|
|
19753
20523
|
timeframe: this.config.market.timeframe ?? "60",
|
|
19754
|
-
live: this.config.live ?? false,
|
|
20524
|
+
live: (this.config.live ?? false) && this.replayQueue === null,
|
|
19755
20525
|
session: this.config.market.session,
|
|
19756
20526
|
bars: () => this.bars,
|
|
19757
20527
|
data: this.dataControl,
|
|
@@ -19877,6 +20647,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19877
20647
|
record.session.notifyBars(reason);
|
|
19878
20648
|
} else if (record.native) record.native.instance.onBars();
|
|
19879
20649
|
}
|
|
20650
|
+
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
|
|
19880
20651
|
}
|
|
19881
20652
|
/** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
|
|
19882
20653
|
canHeal() {
|
|
@@ -19899,10 +20670,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19899
20670
|
try {
|
|
19900
20671
|
const bars = await this.feed.loadRange(this.config.market, { from: fromMs });
|
|
19901
20672
|
if (this.generation !== gen) return;
|
|
20673
|
+
if (this.replayQueue) return;
|
|
19902
20674
|
for (const b of bars) this.applyBar(b, false);
|
|
19903
20675
|
} catch {
|
|
19904
20676
|
} finally {
|
|
19905
|
-
if (this.generation === gen) {
|
|
20677
|
+
if (this.generation === gen && !this.replayQueue) {
|
|
19906
20678
|
this.healing = false;
|
|
19907
20679
|
const pending = this.healBuffer;
|
|
19908
20680
|
this.healBuffer = [];
|
|
@@ -20207,6 +20979,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20207
20979
|
destroy() {
|
|
20208
20980
|
this.bumpGeneration();
|
|
20209
20981
|
this.resolveHistoryComplete();
|
|
20982
|
+
this.clearReplayTimer();
|
|
20983
|
+
this.replayQueue = null;
|
|
20984
|
+
this.replayForming = null;
|
|
20985
|
+
this.replayCarrying = null;
|
|
20986
|
+
this.dropTickFetches();
|
|
20987
|
+
this.replayDepth = null;
|
|
20988
|
+
this.replayLoading = null;
|
|
20210
20989
|
this.stopLive();
|
|
20211
20990
|
if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
|
|
20212
20991
|
this.viewportUnsub?.();
|
|
@@ -20305,6 +21084,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20305
21084
|
this.emitScriptRun(id, cause, first);
|
|
20306
21085
|
},
|
|
20307
21086
|
onAlert: (a) => {
|
|
21087
|
+
if (this.replayQueue || this.replayCarrying) return;
|
|
20308
21088
|
const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
|
|
20309
21089
|
this.events.emit("alert", { ...a, indicator });
|
|
20310
21090
|
handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
|
|
@@ -20331,7 +21111,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20331
21111
|
id,
|
|
20332
21112
|
symbol: this.config.market.symbol ?? "TEST",
|
|
20333
21113
|
timeframe: this.config.market.timeframe ?? "60",
|
|
20334
|
-
live: this.config.live,
|
|
21114
|
+
live: this.config.live && this.replayQueue === null,
|
|
20335
21115
|
session: this.config.market.session,
|
|
20336
21116
|
bars: () => this.bars,
|
|
20337
21117
|
data: this.dataControl,
|
|
@@ -20734,17 +21514,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20734
21514
|
}
|
|
20735
21515
|
emitContextChanged(id) {
|
|
20736
21516
|
if (!this.registry.get(id)?.session?.getContext) return;
|
|
20737
|
-
const
|
|
20738
|
-
if (
|
|
20739
|
-
this.contextEmitAt.set(id,
|
|
21517
|
+
const now2 = Date.now();
|
|
21518
|
+
if (now2 - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
|
|
21519
|
+
this.contextEmitAt.set(id, now2);
|
|
20740
21520
|
this.events.emit("context:changed", { id });
|
|
20741
21521
|
}
|
|
20742
21522
|
emitScriptRun(id, cause, first) {
|
|
20743
21523
|
const record = this.registry.get(id);
|
|
20744
21524
|
if (!record || record.native || !this.events.hasListeners("script:run")) return;
|
|
20745
|
-
const
|
|
20746
|
-
if (cause === "tick" &&
|
|
20747
|
-
this.runEmitAt.set(id,
|
|
21525
|
+
const now2 = Date.now();
|
|
21526
|
+
if (cause === "tick" && now2 - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
|
|
21527
|
+
this.runEmitAt.set(id, now2);
|
|
20748
21528
|
void Promise.resolve().then(() => this.buildScriptRun(record, cause, first)).then((run) => {
|
|
20749
21529
|
if (run && this.registry.get(id)) this.events.emit("script:run", run);
|
|
20750
21530
|
});
|
|
@@ -20797,6 +21577,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20797
21577
|
handle?.emit("error", { error });
|
|
20798
21578
|
}
|
|
20799
21579
|
};
|
|
21580
|
+
function now() {
|
|
21581
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
21582
|
+
}
|
|
21583
|
+
function usableTicks(value) {
|
|
21584
|
+
if (!Array.isArray(value)) return [];
|
|
21585
|
+
const optional = (n) => n === void 0 || Number.isFinite(n);
|
|
21586
|
+
return value.filter(
|
|
21587
|
+
(t) => t != null && Number.isFinite(t.price) && optional(t.volume) && optional(t.high) && optional(t.low) && optional(t.open)
|
|
21588
|
+
);
|
|
21589
|
+
}
|
|
20800
21590
|
function yieldToPaint() {
|
|
20801
21591
|
if (typeof requestAnimationFrame === "function") {
|
|
20802
21592
|
return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
@@ -20997,6 +21787,12 @@ var RendererControl = class {
|
|
|
20997
21787
|
onCrosshairMove(cb) {
|
|
20998
21788
|
return this.renderer.onCrosshairMove(cb);
|
|
20999
21789
|
}
|
|
21790
|
+
/** A click — or a touch tap — on the plot, never the end of a pan: the open time of
|
|
21791
|
+
* the bar under it (`null` off the bars). On touch, where a tap moves no crosshair,
|
|
21792
|
+
* this is how an interaction learns which bar was chosen. */
|
|
21793
|
+
onClick(cb) {
|
|
21794
|
+
return this.renderer.onClick(cb);
|
|
21795
|
+
}
|
|
21000
21796
|
/** Touch long-press on a price or time axis strip — silent no-op without the seam. */
|
|
21001
21797
|
onAxisLongPress(cb) {
|
|
21002
21798
|
return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
|
|
@@ -21495,6 +22291,71 @@ var MarksControl = class {
|
|
|
21495
22291
|
}
|
|
21496
22292
|
};
|
|
21497
22293
|
|
|
22294
|
+
// src/core/ReplayControl.ts
|
|
22295
|
+
var ReplayControl = class {
|
|
22296
|
+
constructor(ctrl) {
|
|
22297
|
+
this.ctrl = ctrl;
|
|
22298
|
+
}
|
|
22299
|
+
/**
|
|
22300
|
+
* Enter replay (or seek, when already replaying) at `opts.from`. Resolves once the
|
|
22301
|
+
* chart shows the rewound history — after any in-flight history load has finished.
|
|
22302
|
+
* Starts paused; call {@link play} or {@link step}.
|
|
22303
|
+
*/
|
|
22304
|
+
start(opts) {
|
|
22305
|
+
return this.ctrl.replayStart(opts);
|
|
22306
|
+
}
|
|
22307
|
+
/** Reveal the next bar — or, mid-way through a bar played tick by tick, complete it.
|
|
22308
|
+
* Returns false when replay is off or nothing is left. */
|
|
22309
|
+
step() {
|
|
22310
|
+
return this.ctrl.replayStep();
|
|
22311
|
+
}
|
|
22312
|
+
/** Reveal the next UPDATE: with {@link setTicks}, the next tick of the forming bar — or
|
|
22313
|
+
* the next bar opened at its first tick (once its ticks are in, if still loading);
|
|
22314
|
+
* otherwise the next whole bar, like {@link step}. Returns false when nothing is left. */
|
|
22315
|
+
stepUpdate() {
|
|
22316
|
+
return this.ctrl.replayStepUpdate();
|
|
22317
|
+
}
|
|
22318
|
+
/** Reveal one bar (one tick, with {@link setTicks}) every `intervalMs` (default: the last
|
|
22319
|
+
* interval used, else 1000). Calling it again while playing changes the pace. */
|
|
22320
|
+
play(intervalMs2) {
|
|
22321
|
+
this.ctrl.replayPlay(intervalMs2);
|
|
22322
|
+
return this;
|
|
22323
|
+
}
|
|
22324
|
+
pause() {
|
|
22325
|
+
this.ctrl.replayPause();
|
|
22326
|
+
return this;
|
|
22327
|
+
}
|
|
22328
|
+
/** Leave replay: the full history comes back and live updates resume. Also cancels a
|
|
22329
|
+
* `start()` still loading older history (the chart returns to its previous depth). */
|
|
22330
|
+
stop() {
|
|
22331
|
+
this.ctrl.replayStop();
|
|
22332
|
+
return this;
|
|
22333
|
+
}
|
|
22334
|
+
get state() {
|
|
22335
|
+
return this.ctrl.replayState();
|
|
22336
|
+
}
|
|
22337
|
+
/** The loaded history a replay can walk — hidden bars included while replaying. Null
|
|
22338
|
+
* before any bar loaded. A start older than `first` deepens the history first. */
|
|
22339
|
+
get bounds() {
|
|
22340
|
+
return this.ctrl.replayBounds();
|
|
22341
|
+
}
|
|
22342
|
+
/**
|
|
22343
|
+
* Play each revealed bar as a series of intrabar updates instead of whole: while
|
|
22344
|
+
* playing, the forming candle opens at the first tick, then every tick moves its close
|
|
22345
|
+
* and stretches its high and low, exactly like a live tick — indicators, chart types
|
|
22346
|
+
* and the `bar` event follow — and the last one settles it on the stored bar. The play
|
|
22347
|
+
* interval is then the time between two TICKS: a bar lasts as many intervals as it
|
|
22348
|
+
* has ticks (below ~16 ms, ticks are batched per frame). The next bar's ticks are
|
|
22349
|
+
* requested while the current one plays; playback waits for them. `null` goes back to
|
|
22350
|
+
* whole bars. Takes effect from the next bar; a bar forming when the source changes
|
|
22351
|
+
* completes at once.
|
|
22352
|
+
*/
|
|
22353
|
+
setTicks(source) {
|
|
22354
|
+
this.ctrl.replaySetTicks(source);
|
|
22355
|
+
return this;
|
|
22356
|
+
}
|
|
22357
|
+
};
|
|
22358
|
+
|
|
21498
22359
|
// src/core/model/inputs.ts
|
|
21499
22360
|
function inputVisible(when, values) {
|
|
21500
22361
|
if (!when) return true;
|
|
@@ -22104,10 +22965,16 @@ var IndicatorInputsDialog = class {
|
|
|
22104
22965
|
this.calendarPop = null;
|
|
22105
22966
|
this.calendarAnchor = null;
|
|
22106
22967
|
},
|
|
22107
|
-
content: (el) =>
|
|
22108
|
-
|
|
22109
|
-
|
|
22110
|
-
|
|
22968
|
+
content: (el) => {
|
|
22969
|
+
const picker = new DatePicker({
|
|
22970
|
+
value: current,
|
|
22971
|
+
onPick: (iso) => {
|
|
22972
|
+
pop.hide();
|
|
22973
|
+
onPick(iso);
|
|
22974
|
+
}
|
|
22975
|
+
});
|
|
22976
|
+
el.append(picker.el);
|
|
22977
|
+
}
|
|
22111
22978
|
});
|
|
22112
22979
|
this.calendarPop = pop;
|
|
22113
22980
|
this.calendarAnchor = anchor;
|
|
@@ -22232,28 +23099,6 @@ function ensureDialogStyles() {
|
|
|
22232
23099
|
.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;}
|
|
22233
23100
|
.vela-ind-combo-chevron:hover{opacity:0.9;}
|
|
22234
23101
|
.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;}
|
|
22235
|
-
.vela-ind-cal [hidden]{display:none !important;}
|
|
22236
|
-
.vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
|
|
22237
|
-
.vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
|
|
22238
|
-
.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;}
|
|
22239
|
-
.vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
|
|
22240
|
-
.vela-ind-cal-switch:disabled{cursor:default;}
|
|
22241
|
-
.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;}
|
|
22242
|
-
.vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
22243
|
-
.vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
|
|
22244
|
-
.vela-ind-cal-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
|
|
22245
|
-
.vela-ind-cal-week span{line-height:20px;}
|
|
22246
|
-
.vela-ind-cal-blank{width:28px;height:28px;}
|
|
22247
|
-
.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;}
|
|
22248
|
-
.vela-ind-cal-day:hover{background:var(--vela-hover);}
|
|
22249
|
-
.vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
22250
|
-
.vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
22251
|
-
.vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
|
|
22252
|
-
.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;}
|
|
22253
|
-
.vela-ind-cal-cell:hover{background:var(--vela-hover);}
|
|
22254
|
-
.vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
22255
|
-
.vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
22256
|
-
.vela-ind-cal-cell[data-outside]{opacity:0.45;}
|
|
22257
23102
|
${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
22258
23103
|
.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;}
|
|
22259
23104
|
.vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
|
|
@@ -22287,174 +23132,6 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
|
|
|
22287
23132
|
const v = `${hh}:${mm}`;
|
|
22288
23133
|
return { value: v, label: v };
|
|
22289
23134
|
});
|
|
22290
|
-
var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
22291
|
-
var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
22292
|
-
var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
22293
|
-
function normalizeDateInput(raw) {
|
|
22294
|
-
const t = raw.trim();
|
|
22295
|
-
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
|
|
22296
|
-
if (!m) return null;
|
|
22297
|
-
const y = Number(m[1]);
|
|
22298
|
-
const mo = Number(m[2]);
|
|
22299
|
-
const d = Number(m[3]);
|
|
22300
|
-
const dt = new Date(y, mo - 1, d);
|
|
22301
|
-
if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
|
|
22302
|
-
return isoDate(dt);
|
|
22303
|
-
}
|
|
22304
|
-
function parseIsoDate(raw) {
|
|
22305
|
-
const iso = normalizeDateInput(raw);
|
|
22306
|
-
if (!iso) return null;
|
|
22307
|
-
const [y, mo, d] = iso.split("-").map(Number);
|
|
22308
|
-
return new Date(y, mo - 1, d);
|
|
22309
|
-
}
|
|
22310
|
-
function isoDate(d) {
|
|
22311
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
22312
|
-
}
|
|
22313
|
-
function fillCalendar(el, current, onPick) {
|
|
22314
|
-
const today = /* @__PURE__ */ new Date();
|
|
22315
|
-
const selected = parseIsoDate(current);
|
|
22316
|
-
let year = selected?.getFullYear() ?? today.getFullYear();
|
|
22317
|
-
let month = selected?.getMonth() ?? today.getMonth();
|
|
22318
|
-
let mode = "date";
|
|
22319
|
-
const prev2 = document.createElement("button");
|
|
22320
|
-
prev2.type = "button";
|
|
22321
|
-
prev2.className = "vela-ind-cal-nav";
|
|
22322
|
-
prev2.innerHTML = iconAt("chevron-left", 14);
|
|
22323
|
-
const next = document.createElement("button");
|
|
22324
|
-
next.type = "button";
|
|
22325
|
-
next.className = "vela-ind-cal-nav";
|
|
22326
|
-
next.innerHTML = iconAt("chevron-right", 14);
|
|
22327
|
-
const monthBtn = document.createElement("button");
|
|
22328
|
-
monthBtn.type = "button";
|
|
22329
|
-
monthBtn.className = "vela-ind-cal-switch";
|
|
22330
|
-
monthBtn.setAttribute("aria-label", "Choose month");
|
|
22331
|
-
const yearBtn = document.createElement("button");
|
|
22332
|
-
yearBtn.type = "button";
|
|
22333
|
-
yearBtn.className = "vela-ind-cal-switch";
|
|
22334
|
-
yearBtn.setAttribute("aria-label", "Choose year");
|
|
22335
|
-
const title = document.createElement("div");
|
|
22336
|
-
title.className = "vela-ind-cal-title";
|
|
22337
|
-
title.append(monthBtn, yearBtn);
|
|
22338
|
-
const head = document.createElement("div");
|
|
22339
|
-
head.className = "vela-ind-cal-head";
|
|
22340
|
-
head.append(prev2, title, next);
|
|
22341
|
-
const week = document.createElement("div");
|
|
22342
|
-
week.className = "vela-ind-cal-week";
|
|
22343
|
-
for (const d of WEEKDAY_LABELS) {
|
|
22344
|
-
const cell = document.createElement("span");
|
|
22345
|
-
cell.textContent = d;
|
|
22346
|
-
week.appendChild(cell);
|
|
22347
|
-
}
|
|
22348
|
-
const grid = document.createElement("div");
|
|
22349
|
-
const paint = () => {
|
|
22350
|
-
const decade = Math.floor(year / 10) * 10;
|
|
22351
|
-
monthBtn.hidden = mode !== "date";
|
|
22352
|
-
monthBtn.textContent = MONTH_LABELS[month] ?? "";
|
|
22353
|
-
yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
|
|
22354
|
-
yearBtn.disabled = mode === "year";
|
|
22355
|
-
week.hidden = mode !== "date";
|
|
22356
|
-
prev2.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
|
|
22357
|
-
next.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
|
|
22358
|
-
grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
|
|
22359
|
-
grid.replaceChildren();
|
|
22360
|
-
if (mode === "date") {
|
|
22361
|
-
const startPad = new Date(year, month, 1).getDay();
|
|
22362
|
-
const days = new Date(year, month + 1, 0).getDate();
|
|
22363
|
-
const todayIso = isoDate(today);
|
|
22364
|
-
const selectedIso = selected ? isoDate(selected) : "";
|
|
22365
|
-
for (let i = 0; i < startPad; i++) {
|
|
22366
|
-
const blank = document.createElement("span");
|
|
22367
|
-
blank.className = "vela-ind-cal-blank";
|
|
22368
|
-
grid.appendChild(blank);
|
|
22369
|
-
}
|
|
22370
|
-
for (let day = 1; day <= days; day++) {
|
|
22371
|
-
const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
22372
|
-
const b = document.createElement("button");
|
|
22373
|
-
b.type = "button";
|
|
22374
|
-
b.className = "vela-ind-cal-day";
|
|
22375
|
-
b.textContent = String(day);
|
|
22376
|
-
if (iso === selectedIso) b.dataset.checked = "1";
|
|
22377
|
-
if (iso === todayIso) b.dataset.today = "1";
|
|
22378
|
-
b.addEventListener("click", (e) => {
|
|
22379
|
-
e.stopPropagation();
|
|
22380
|
-
onPick(iso);
|
|
22381
|
-
});
|
|
22382
|
-
grid.appendChild(b);
|
|
22383
|
-
}
|
|
22384
|
-
return;
|
|
22385
|
-
}
|
|
22386
|
-
if (mode === "month") {
|
|
22387
|
-
for (let m = 0; m < 12; m++) {
|
|
22388
|
-
const b = document.createElement("button");
|
|
22389
|
-
b.type = "button";
|
|
22390
|
-
b.className = "vela-ind-cal-cell";
|
|
22391
|
-
b.textContent = MONTH_SHORT[m] ?? "";
|
|
22392
|
-
if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
|
|
22393
|
-
if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
|
|
22394
|
-
b.addEventListener("click", (e) => {
|
|
22395
|
-
e.stopPropagation();
|
|
22396
|
-
month = m;
|
|
22397
|
-
mode = "date";
|
|
22398
|
-
paint();
|
|
22399
|
-
});
|
|
22400
|
-
grid.appendChild(b);
|
|
22401
|
-
}
|
|
22402
|
-
return;
|
|
22403
|
-
}
|
|
22404
|
-
for (let y = decade - 1; y <= decade + 10; y++) {
|
|
22405
|
-
const b = document.createElement("button");
|
|
22406
|
-
b.type = "button";
|
|
22407
|
-
b.className = "vela-ind-cal-cell";
|
|
22408
|
-
b.textContent = String(y);
|
|
22409
|
-
if (y < decade || y > decade + 9) b.dataset.outside = "1";
|
|
22410
|
-
if (selected?.getFullYear() === y) b.dataset.checked = "1";
|
|
22411
|
-
if (today.getFullYear() === y) b.dataset.today = "1";
|
|
22412
|
-
b.addEventListener("click", (e) => {
|
|
22413
|
-
e.stopPropagation();
|
|
22414
|
-
year = y;
|
|
22415
|
-
mode = "month";
|
|
22416
|
-
paint();
|
|
22417
|
-
});
|
|
22418
|
-
grid.appendChild(b);
|
|
22419
|
-
}
|
|
22420
|
-
};
|
|
22421
|
-
const step = (dir) => {
|
|
22422
|
-
if (mode === "date") {
|
|
22423
|
-
month += dir;
|
|
22424
|
-
if (month < 0) {
|
|
22425
|
-
month = 11;
|
|
22426
|
-
year -= 1;
|
|
22427
|
-
}
|
|
22428
|
-
if (month > 11) {
|
|
22429
|
-
month = 0;
|
|
22430
|
-
year += 1;
|
|
22431
|
-
}
|
|
22432
|
-
} else {
|
|
22433
|
-
year += mode === "month" ? dir : dir * 10;
|
|
22434
|
-
}
|
|
22435
|
-
paint();
|
|
22436
|
-
};
|
|
22437
|
-
prev2.addEventListener("click", (e) => {
|
|
22438
|
-
e.stopPropagation();
|
|
22439
|
-
step(-1);
|
|
22440
|
-
});
|
|
22441
|
-
next.addEventListener("click", (e) => {
|
|
22442
|
-
e.stopPropagation();
|
|
22443
|
-
step(1);
|
|
22444
|
-
});
|
|
22445
|
-
monthBtn.addEventListener("click", (e) => {
|
|
22446
|
-
e.stopPropagation();
|
|
22447
|
-
mode = "month";
|
|
22448
|
-
paint();
|
|
22449
|
-
});
|
|
22450
|
-
yearBtn.addEventListener("click", (e) => {
|
|
22451
|
-
e.stopPropagation();
|
|
22452
|
-
mode = "year";
|
|
22453
|
-
paint();
|
|
22454
|
-
});
|
|
22455
|
-
paint();
|
|
22456
|
-
el.append(head, week, grid);
|
|
22457
|
-
}
|
|
22458
23135
|
function normalizeTimeInput(raw) {
|
|
22459
23136
|
const t = raw.trim();
|
|
22460
23137
|
const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
|
|
@@ -25158,7 +25835,7 @@ var Scheduler = class {
|
|
|
25158
25835
|
|
|
25159
25836
|
// src/renderers/native/core/Animator.ts
|
|
25160
25837
|
var Animator = class {
|
|
25161
|
-
constructor(tick, raf, cancel,
|
|
25838
|
+
constructor(tick, raf, cancel, now2) {
|
|
25162
25839
|
this.tick = tick;
|
|
25163
25840
|
this.handle = null;
|
|
25164
25841
|
this.last = 0;
|
|
@@ -25172,7 +25849,7 @@ var Animator = class {
|
|
|
25172
25849
|
};
|
|
25173
25850
|
this.raf = raf ?? ((cb) => requestAnimationFrame(cb));
|
|
25174
25851
|
this.cancel = cancel ?? ((h) => cancelAnimationFrame(h));
|
|
25175
|
-
this.now =
|
|
25852
|
+
this.now = now2 ?? (() => performance.now());
|
|
25176
25853
|
}
|
|
25177
25854
|
get active() {
|
|
25178
25855
|
return this.handle !== null;
|
|
@@ -25304,6 +25981,12 @@ var InputController = class {
|
|
|
25304
25981
|
this.lastTapT = 0;
|
|
25305
25982
|
this.lastTapX = 0;
|
|
25306
25983
|
this.lastTapY = 0;
|
|
25984
|
+
// Whether the last two primary presses were claimed by the drawings layer. A double-click
|
|
25985
|
+
// is the last press plus the one before it; a tool-driven click pair must never fall through
|
|
25986
|
+
// to the pane maximize toggle, even when the tool disarmed itself between the two clicks
|
|
25987
|
+
// (a finished placement leaves plain cursor mode for the second click).
|
|
25988
|
+
this.lastPressDrawing = false;
|
|
25989
|
+
this.prevPressDrawing = false;
|
|
25307
25990
|
this.onTouchContextMenu = (e) => {
|
|
25308
25991
|
if (this.touches.size === 0) return;
|
|
25309
25992
|
if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
|
|
@@ -25358,19 +26041,24 @@ var InputController = class {
|
|
|
25358
26041
|
this.moved = false;
|
|
25359
26042
|
this.startX = x;
|
|
25360
26043
|
this.startY = y;
|
|
26044
|
+
this.prevPressDrawing = this.lastPressDrawing;
|
|
26045
|
+
this.lastPressDrawing = false;
|
|
25361
26046
|
if (this.deps.drawingsClaim?.(x, y)) {
|
|
25362
26047
|
this.region = "drawing";
|
|
26048
|
+
this.lastPressDrawing = true;
|
|
25363
26049
|
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
25364
26050
|
this.capture(e.pointerId);
|
|
25365
26051
|
return;
|
|
25366
26052
|
}
|
|
25367
26053
|
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
|
|
25368
26054
|
this.region = "drawing";
|
|
26055
|
+
this.lastPressDrawing = true;
|
|
25369
26056
|
this.capture(e.pointerId);
|
|
25370
26057
|
return;
|
|
25371
26058
|
}
|
|
25372
26059
|
if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
|
|
25373
26060
|
this.region = "drawing";
|
|
26061
|
+
this.lastPressDrawing = true;
|
|
25374
26062
|
this.capture(e.pointerId);
|
|
25375
26063
|
return;
|
|
25376
26064
|
}
|
|
@@ -25692,6 +26380,7 @@ var InputController = class {
|
|
|
25692
26380
|
if (region === "price") this.deps.resetPriceScale(x, y);
|
|
25693
26381
|
else if (region === "separator") this.deps.resetPaneSize(y);
|
|
25694
26382
|
else if (region === "time") this.deps.resetView();
|
|
26383
|
+
else if (this.lastPressDrawing || this.prevPressDrawing) return;
|
|
25695
26384
|
else this.deps.dataDblClick(x, y);
|
|
25696
26385
|
}
|
|
25697
26386
|
};
|
|
@@ -25971,6 +26660,8 @@ var SceneGraph = class {
|
|
|
25971
26660
|
/** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
|
|
25972
26661
|
this.vpvrLayer = null;
|
|
25973
26662
|
this.crosshair = null;
|
|
26663
|
+
/** Runtime override of the crosshair's lines and style (the `crosshairOverride` feature). Ephemeral. */
|
|
26664
|
+
this.crosshairOverride = null;
|
|
25974
26665
|
/** How the base price series is drawn on the price pane (candles by default). */
|
|
25975
26666
|
this.priceStyle = "candles";
|
|
25976
26667
|
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
@@ -27855,6 +28546,7 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
27855
28546
|
const step = pickStep(span / Math.max(1, target));
|
|
27856
28547
|
const zFrom = fromMs + offsetMs;
|
|
27857
28548
|
const zTo = toMs + offsetMs;
|
|
28549
|
+
if (step >= MONTH) return calendarTicks(zFrom, zTo, step >= YEAR ? 12 : step >= 3 * MONTH ? 3 : 1, offsetMs);
|
|
27858
28550
|
const first = Math.ceil(zFrom / step) * step;
|
|
27859
28551
|
const out = [];
|
|
27860
28552
|
for (let zt = first; zt <= zTo; zt += step) {
|
|
@@ -27885,11 +28577,23 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
27885
28577
|
}
|
|
27886
28578
|
return out;
|
|
27887
28579
|
}
|
|
28580
|
+
function calendarTicks(zFrom, zTo, months, offsetMs) {
|
|
28581
|
+
const d0 = new Date(zFrom);
|
|
28582
|
+
let idx = d0.getUTCFullYear() * 12 + d0.getUTCMonth();
|
|
28583
|
+
if (Date.UTC(Math.floor(idx / 12), idx % 12, 1) < zFrom) idx += 1;
|
|
28584
|
+
idx = Math.ceil(idx / months) * months;
|
|
28585
|
+
const out = [];
|
|
28586
|
+
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)) {
|
|
28587
|
+
const label = months === 12 ? String(Math.floor(idx / 12)) : MONTHS[idx % 12];
|
|
28588
|
+
out.push({ time: zt - offsetMs, label, major: true });
|
|
28589
|
+
}
|
|
28590
|
+
return out;
|
|
28591
|
+
}
|
|
27888
28592
|
|
|
27889
28593
|
// src/renderers/native/chrome/countdown.ts
|
|
27890
|
-
function countdownText(barOpen, barMs,
|
|
28594
|
+
function countdownText(barOpen, barMs, now2) {
|
|
27891
28595
|
if (!(barMs > 0)) return null;
|
|
27892
|
-
const remaining = barOpen + barMs -
|
|
28596
|
+
const remaining = barOpen + barMs - now2;
|
|
27893
28597
|
if (remaining <= 0) return null;
|
|
27894
28598
|
return formatCountdown(remaining);
|
|
27895
28599
|
}
|
|
@@ -28701,19 +29405,35 @@ var CrosshairRenderer = class {
|
|
|
28701
29405
|
const dataH = coords.height;
|
|
28702
29406
|
if (!ch || ch.x < 0 || ch.x > dataW || ch.y < 0 || ch.y > dataH) return;
|
|
28703
29407
|
const cs = scene.style.crosshair;
|
|
29408
|
+
const ov = scene.crosshairOverride;
|
|
29409
|
+
const vertical = ov?.vertical !== false;
|
|
29410
|
+
const horizontal = ov?.horizontal !== false;
|
|
28704
29411
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
28705
29412
|
ctx.textBaseline = "middle";
|
|
28706
29413
|
const logical = Math.round(coords.xToLogical(ch.x));
|
|
28707
|
-
const x =
|
|
28708
|
-
|
|
28709
|
-
|
|
28710
|
-
|
|
28711
|
-
|
|
29414
|
+
const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
|
|
29415
|
+
if (vertical && ov?.shadeRight) {
|
|
29416
|
+
const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
|
|
29417
|
+
if (from < dataW) {
|
|
29418
|
+
ctx.fillStyle = ov.shadeRight.color;
|
|
29419
|
+
ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
|
|
29420
|
+
ctx.fillRect(from, 0, dataW - from, dataH);
|
|
29421
|
+
ctx.globalAlpha = 1;
|
|
29422
|
+
}
|
|
29423
|
+
}
|
|
29424
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
29425
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
29426
|
+
ctx.globalAlpha = ov?.opacity ?? cs.opacity;
|
|
29427
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
28712
29428
|
ctx.beginPath();
|
|
28713
|
-
|
|
28714
|
-
|
|
28715
|
-
|
|
28716
|
-
|
|
29429
|
+
if (vertical) {
|
|
29430
|
+
ctx.moveTo(x, 0);
|
|
29431
|
+
ctx.lineTo(x, dataH);
|
|
29432
|
+
}
|
|
29433
|
+
if (horizontal) {
|
|
29434
|
+
ctx.moveTo(0, Math.round(ch.y) + 0.5);
|
|
29435
|
+
ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
|
|
29436
|
+
}
|
|
28717
29437
|
ctx.stroke();
|
|
28718
29438
|
ctx.setLineDash([]);
|
|
28719
29439
|
ctx.lineWidth = 1;
|
|
@@ -28726,11 +29446,11 @@ var CrosshairRenderer = class {
|
|
|
28726
29446
|
}
|
|
28727
29447
|
}
|
|
28728
29448
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
28729
|
-
if (pane && pane.axisFormat !== "none") {
|
|
29449
|
+
if (horizontal && pane && pane.axisFormat !== "none") {
|
|
28730
29450
|
const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
|
|
28731
29451
|
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);
|
|
28732
29452
|
}
|
|
28733
|
-
this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
29453
|
+
if (vertical) this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
28734
29454
|
}
|
|
28735
29455
|
destroy() {
|
|
28736
29456
|
this.canvas = null;
|
|
@@ -28741,23 +29461,37 @@ var CrosshairRenderer = class {
|
|
|
28741
29461
|
* along), with that bar's time chip in this chart's own timezone and — when the
|
|
28742
29462
|
* level resolved — the price chip on the right axis. The snap happened upstream
|
|
28743
29463
|
* (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
|
|
28744
|
-
* render slightly dimmed so the ghost still reads as foreign.
|
|
29464
|
+
* render slightly dimmed so the ghost still reads as foreign. A `crosshairOverride`
|
|
29465
|
+
* applies to the ghost as well — its line style, a dropped horizontal line, and the
|
|
29466
|
+
* `shadeRight` veil from the ghost bar's right edge — so a pick that spans several
|
|
29467
|
+
* charts reads the same on every one of them. */
|
|
28745
29468
|
drawExternal(ctx, ext, scene, coords, theme) {
|
|
28746
29469
|
const cs = scene.style.crosshair;
|
|
29470
|
+
const ov = scene.crosshairOverride;
|
|
28747
29471
|
const dataW = coords.width;
|
|
28748
29472
|
const dataH = coords.height;
|
|
28749
|
-
const x =
|
|
28750
|
-
if (
|
|
29473
|
+
const x = crisp(ext.x, ov?.width ?? cs.width);
|
|
29474
|
+
if (ov?.vertical === false) return;
|
|
29475
|
+
if (ov?.shadeRight) {
|
|
29476
|
+
const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
|
|
29477
|
+
if (from < dataW) {
|
|
29478
|
+
ctx.fillStyle = ov.shadeRight.color;
|
|
29479
|
+
ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
|
|
29480
|
+
ctx.fillRect(from, 0, dataW - from, dataH);
|
|
29481
|
+
ctx.globalAlpha = 1;
|
|
29482
|
+
}
|
|
29483
|
+
}
|
|
29484
|
+
if (ext.line === false || x < 0 || x > dataW) return;
|
|
28751
29485
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
28752
29486
|
ctx.textBaseline = "middle";
|
|
28753
|
-
ctx.strokeStyle = cs.color ?? theme.textColor;
|
|
28754
|
-
ctx.lineWidth = cs.width;
|
|
28755
|
-
ctx.globalAlpha = cs.opacity * 0.55;
|
|
28756
|
-
setDash3(ctx, cs.style);
|
|
29487
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
29488
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
29489
|
+
ctx.globalAlpha = (ov?.opacity ?? cs.opacity) * 0.55;
|
|
29490
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
28757
29491
|
ctx.beginPath();
|
|
28758
29492
|
ctx.moveTo(x, 0);
|
|
28759
29493
|
ctx.lineTo(x, dataH);
|
|
28760
|
-
if (ext.y != null) {
|
|
29494
|
+
if (ext.y != null && ov?.horizontal !== false) {
|
|
28761
29495
|
ctx.moveTo(0, Math.round(ext.y) + 0.5);
|
|
28762
29496
|
ctx.lineTo(dataW, Math.round(ext.y) + 0.5);
|
|
28763
29497
|
}
|
|
@@ -28766,7 +29500,7 @@ var CrosshairRenderer = class {
|
|
|
28766
29500
|
ctx.lineWidth = 1;
|
|
28767
29501
|
ctx.globalAlpha = 0.8;
|
|
28768
29502
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
28769
|
-
if (ext.y != null && ext.price != null) {
|
|
29503
|
+
if (ext.y != null && ext.price != null && ov?.horizontal !== false) {
|
|
28770
29504
|
let pane;
|
|
28771
29505
|
for (const p of scene.panes.values()) {
|
|
28772
29506
|
if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
|
|
@@ -28806,6 +29540,9 @@ var CrosshairRenderer = class {
|
|
|
28806
29540
|
ctx.textAlign = "start";
|
|
28807
29541
|
}
|
|
28808
29542
|
};
|
|
29543
|
+
function crisp(px, width) {
|
|
29544
|
+
return Math.round(px) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
|
|
29545
|
+
}
|
|
28809
29546
|
function setDash3(ctx, style) {
|
|
28810
29547
|
if (style === "dashed") ctx.setLineDash([6, 4]);
|
|
28811
29548
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
@@ -36244,7 +36981,7 @@ var NativeRenderer = class {
|
|
|
36244
36981
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
36245
36982
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
36246
36983
|
this.name = "native";
|
|
36247
|
-
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"];
|
|
36984
|
+
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"];
|
|
36248
36985
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
36249
36986
|
* so moving onto the button doesn't count as leaving). */
|
|
36250
36987
|
this.onScrollProximityMove = (e) => {
|
|
@@ -36423,6 +37160,9 @@ var NativeRenderer = class {
|
|
|
36423
37160
|
this.scene.marks = mergeMarksState(this.scene.marks, value);
|
|
36424
37161
|
this.markPopover?.close();
|
|
36425
37162
|
break;
|
|
37163
|
+
case "crosshairOverride":
|
|
37164
|
+
this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
|
|
37165
|
+
break;
|
|
36426
37166
|
case "keyboard":
|
|
36427
37167
|
this.setKeyboardEnabled(Boolean(value));
|
|
36428
37168
|
return;
|
|
@@ -36536,6 +37276,8 @@ var NativeRenderer = class {
|
|
|
36536
37276
|
return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
|
|
36537
37277
|
case "marks":
|
|
36538
37278
|
return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
|
|
37279
|
+
case "crosshairOverride":
|
|
37280
|
+
return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
|
|
36539
37281
|
case "keyboard":
|
|
36540
37282
|
return this.keyboardEnabled;
|
|
36541
37283
|
case "historyChords":
|
|
@@ -37114,8 +37856,8 @@ var NativeRenderer = class {
|
|
|
37114
37856
|
for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
|
|
37115
37857
|
this.modelAlpha = 0;
|
|
37116
37858
|
const start = performance.now();
|
|
37117
|
-
const step = (
|
|
37118
|
-
const p = Math.min(1, (
|
|
37859
|
+
const step = (now2) => {
|
|
37860
|
+
const p = Math.min(1, (now2 - start) / duration);
|
|
37119
37861
|
this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
|
|
37120
37862
|
this.paintData();
|
|
37121
37863
|
if (p < 1) {
|
|
@@ -37131,8 +37873,8 @@ var NativeRenderer = class {
|
|
|
37131
37873
|
fadeInModels() {
|
|
37132
37874
|
const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
|
|
37133
37875
|
const start = performance.now();
|
|
37134
|
-
const step = (
|
|
37135
|
-
this.modelAlpha = Math.min(1, (
|
|
37876
|
+
const step = (now2) => {
|
|
37877
|
+
this.modelAlpha = Math.min(1, (now2 - start) / fade);
|
|
37136
37878
|
this.paintData();
|
|
37137
37879
|
if (this.modelAlpha < 1) {
|
|
37138
37880
|
this.introRaf = requestAnimationFrame(step);
|
|
@@ -37655,10 +38397,13 @@ var NativeRenderer = class {
|
|
|
37655
38397
|
this.animator.start();
|
|
37656
38398
|
}
|
|
37657
38399
|
} else if (!last || bar.time > last.time) {
|
|
38400
|
+
const vp = this.coords.getViewport();
|
|
38401
|
+
const holdView = n > 0 && vp.rightOffset < 0 && this.scrollTargetRO === null;
|
|
37658
38402
|
this.bars.push(bar);
|
|
37659
38403
|
this.syncLiveEase(bar);
|
|
37660
38404
|
if (this.coords.barInterval > 0) this.coords.appendBar(bar.time);
|
|
37661
38405
|
else this.coords.setBars(this.bars.map((b) => b.time));
|
|
38406
|
+
if (holdView) this.coords.setViewport({ ...vp, rightOffset: vp.rightOffset - 1 });
|
|
37662
38407
|
} else {
|
|
37663
38408
|
return;
|
|
37664
38409
|
}
|
|
@@ -38340,10 +39085,10 @@ var NativeRenderer = class {
|
|
|
38340
39085
|
if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
|
|
38341
39086
|
const tick = () => {
|
|
38342
39087
|
this.markPulseRaf = null;
|
|
38343
|
-
const
|
|
38344
|
-
if (this.scene.marksFlash && this.scene.marksFlash.until <=
|
|
39088
|
+
const now2 = frameNow();
|
|
39089
|
+
if (this.scene.marksFlash && this.scene.marksFlash.until <= now2) this.scene.marksFlash = null;
|
|
38345
39090
|
this.scheduler?.invalidate(2 /* Chrome */);
|
|
38346
|
-
const pulsing = this.scene.marksHoverKey !== null &&
|
|
39091
|
+
const pulsing = this.scene.marksHoverKey !== null && now2 - this.scene.marksHoverSince < MARK_PULSE_MS;
|
|
38347
39092
|
if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
|
|
38348
39093
|
};
|
|
38349
39094
|
this.markPulseRaf = requestAnimationFrame(tick);
|
|
@@ -38904,9 +39649,10 @@ var NativeRenderer = class {
|
|
|
38904
39649
|
const ext = this.externalCross;
|
|
38905
39650
|
if (!ext || this.coords.barCount === 0) return null;
|
|
38906
39651
|
const logical = Math.floor(this.coords.timeToLogical(ext.time));
|
|
38907
|
-
if (logical <
|
|
38908
|
-
const x = this.coords.logicalToX(logical);
|
|
38909
|
-
if (!Number.isFinite(x)
|
|
39652
|
+
if (!(logical < this.coords.barCount)) return null;
|
|
39653
|
+
const x = this.coords.logicalToX(Math.max(logical, -1));
|
|
39654
|
+
if (!Number.isFinite(x)) return null;
|
|
39655
|
+
const line = logical >= 0 && x >= 0 && x <= this.coords.width;
|
|
38910
39656
|
let y = null;
|
|
38911
39657
|
if (ext.price != null) {
|
|
38912
39658
|
const pricePane = this.scene.orderedPanes().find((p) => p.kind === "price");
|
|
@@ -38915,7 +39661,7 @@ var NativeRenderer = class {
|
|
|
38915
39661
|
if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
|
|
38916
39662
|
}
|
|
38917
39663
|
}
|
|
38918
|
-
return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
|
|
39664
|
+
return { x, y, time: this.coords.logicalToTime(Math.max(logical, 0)), price: y != null ? ext.price : null, line };
|
|
38919
39665
|
}
|
|
38920
39666
|
/** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
|
|
38921
39667
|
setSnapMode(mode) {
|
|
@@ -43591,6 +44337,7 @@ var Vela = class {
|
|
|
43591
44337
|
this.panesControl = new PanesControl(this.orchestrator);
|
|
43592
44338
|
this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
|
|
43593
44339
|
this.marksControl = new MarksControl(this.orchestrator.marks);
|
|
44340
|
+
this.replayControl = new ReplayControl(this.orchestrator);
|
|
43594
44341
|
}
|
|
43595
44342
|
/**
|
|
43596
44343
|
* Register a scripting engine so `addIndicator({ language })` can run that
|
|
@@ -43830,6 +44577,16 @@ var Vela = class {
|
|
|
43830
44577
|
get marks() {
|
|
43831
44578
|
return this.marksControl;
|
|
43832
44579
|
}
|
|
44580
|
+
/**
|
|
44581
|
+
* The chart's bar-replay control surface: rewind to a past bar and reveal the
|
|
44582
|
+
* following ones by hand or on a timer, from the history already loaded —
|
|
44583
|
+
* `await chart.replay.start({ from })`, `chart.replay.play(2000)`, `chart.replay.step()`,
|
|
44584
|
+
* `chart.replay.stop()`. Revealed bars behave like live bars for indicators; live
|
|
44585
|
+
* updates pause meanwhile. Follow it with the `replay:*` events.
|
|
44586
|
+
*/
|
|
44587
|
+
get replay() {
|
|
44588
|
+
return this.replayControl;
|
|
44589
|
+
}
|
|
43833
44590
|
on(event, handler) {
|
|
43834
44591
|
return this.orchestrator.events.on(event, handler);
|
|
43835
44592
|
}
|
|
@@ -44025,9 +44782,15 @@ var CSS21 = `
|
|
|
44025
44782
|
}
|
|
44026
44783
|
.vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
|
|
44027
44784
|
.vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
|
|
44785
|
+
/* The meta opens with "\xB7 " \u2014 sit its dot one space-width after the ticker, as far as the
|
|
44786
|
+
* venue sits after it, not a full row gap away. */
|
|
44787
|
+
.vela-statusline .vela-sl-symbol + .vela-sl-meta { margin-left: calc(var(--vela-space-1) - var(--vela-space-2)); }
|
|
44028
44788
|
/* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
|
|
44029
|
-
* via the kit tooltip); the session tint is applied per status in setMarketStatus.
|
|
44030
|
-
|
|
44789
|
+
* via the kit tooltip); the session tint is applied per status in setMarketStatus. While
|
|
44790
|
+
* the chart replays past bars it wears the replay badge instead (the inverse chip). */
|
|
44791
|
+
.vela-statusline .vela-sl-market { align-self: center; display: inline-flex; }
|
|
44792
|
+
.vela-statusline .vela-sl-market > [hidden] { display: none !important; }
|
|
44793
|
+
.vela-statusline .vela-sl-replay-badge, .vela-statusline .vela-sl-replay-badge svg { display: block; width: 16px; height: 16px; }
|
|
44031
44794
|
.vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
|
|
44032
44795
|
.vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
|
|
44033
44796
|
/* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
|
|
@@ -44080,6 +44843,8 @@ var MARKET_LABELS = {
|
|
|
44080
44843
|
closed: "Market Closed",
|
|
44081
44844
|
holiday: "Market Holiday"
|
|
44082
44845
|
};
|
|
44846
|
+
var REPLAY_LABEL = "Replay Mode";
|
|
44847
|
+
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>';
|
|
44083
44848
|
var MARKET_INKS = {
|
|
44084
44849
|
open: "var(--vela-up)",
|
|
44085
44850
|
pre: SESSION_PRE,
|
|
@@ -44118,6 +44883,9 @@ var Statusline = class {
|
|
|
44118
44883
|
this.host = host;
|
|
44119
44884
|
this.iconFor = iconFor;
|
|
44120
44885
|
this.parts = { logo: true, name: true, market: true, ohlc: true, change: true };
|
|
44886
|
+
this.marketStatus = "open";
|
|
44887
|
+
/** The chart replays past bars — the badge shows the replay mode (see {@link setReplaying}). */
|
|
44888
|
+
this.replaying = false;
|
|
44121
44889
|
/** The right-click action menu — present once a host wires it via {@link attachMenu}. */
|
|
44122
44890
|
this.menu = null;
|
|
44123
44891
|
this.menuHooks = null;
|
|
@@ -44169,8 +44937,13 @@ var Statusline = class {
|
|
|
44169
44937
|
label: MARKET_LABELS.open,
|
|
44170
44938
|
host
|
|
44171
44939
|
});
|
|
44172
|
-
this.
|
|
44173
|
-
this.
|
|
44940
|
+
this.replayBadge = doc.createElement("span");
|
|
44941
|
+
this.replayBadge.className = "vela-sl-replay-badge";
|
|
44942
|
+
this.replayBadge.innerHTML = REPLAY_BADGE_SVG;
|
|
44943
|
+
this.replayBadge.hidden = true;
|
|
44944
|
+
this.marketEl = doc.createElement("span");
|
|
44945
|
+
this.marketEl.className = "vela-sl-market";
|
|
44946
|
+
this.marketEl.append(this.marketBubble.el, this.replayBadge);
|
|
44174
44947
|
this.ohlcEl = doc.createElement("span");
|
|
44175
44948
|
this.ohlcEl.className = "vela-sl-ohlc";
|
|
44176
44949
|
this.changeEl = doc.createElement("span");
|
|
@@ -44292,6 +45065,25 @@ var Statusline = class {
|
|
|
44292
45065
|
/** Dress the market badge for a session state: its icon, tinted circle, and the
|
|
44293
45066
|
* hover label. Callers with no session model leave the constructor's 'open'. */
|
|
44294
45067
|
setMarketStatus(status) {
|
|
45068
|
+
this.marketStatus = status;
|
|
45069
|
+
this.dressBadge();
|
|
45070
|
+
}
|
|
45071
|
+
/** The chart entered or left a bar replay: the badge shows the replay mode meanwhile
|
|
45072
|
+
* (the session state of a past bar says nothing about the market now). */
|
|
45073
|
+
setReplaying(replaying) {
|
|
45074
|
+
if (replaying === this.replaying) return;
|
|
45075
|
+
this.replaying = replaying;
|
|
45076
|
+
this.dressBadge();
|
|
45077
|
+
}
|
|
45078
|
+
dressBadge() {
|
|
45079
|
+
this.marketBubble.el.hidden = this.replaying;
|
|
45080
|
+
this.replayBadge.hidden = !this.replaying;
|
|
45081
|
+
if (this.replaying) {
|
|
45082
|
+
this.marketEl.dataset.status = "replay";
|
|
45083
|
+
this.marketTip.setContent(REPLAY_LABEL);
|
|
45084
|
+
return;
|
|
45085
|
+
}
|
|
45086
|
+
const status = this.marketStatus;
|
|
44295
45087
|
this.marketEl.dataset.status = status;
|
|
44296
45088
|
const ink = MARKET_INKS[status];
|
|
44297
45089
|
this.marketBubble.set({
|
|
@@ -44364,11 +45156,23 @@ var Statusline = class {
|
|
|
44364
45156
|
this.lastBar = b;
|
|
44365
45157
|
if (!this.hoverBar) this.render();
|
|
44366
45158
|
}),
|
|
45159
|
+
// A replay cut or restore replaces the newest bar without a `bar` event.
|
|
45160
|
+
chart.on("replay:start", () => {
|
|
45161
|
+
this.lastBar = null;
|
|
45162
|
+
this.setReplaying(true);
|
|
45163
|
+
this.render();
|
|
45164
|
+
}),
|
|
45165
|
+
chart.on("replay:end", () => {
|
|
45166
|
+
this.lastBar = null;
|
|
45167
|
+
this.setReplaying(false);
|
|
45168
|
+
this.render();
|
|
45169
|
+
}),
|
|
44367
45170
|
chart.renderer.onCrosshairMove((e) => {
|
|
44368
45171
|
this.hoverBar = e.ohlc;
|
|
44369
45172
|
this.render();
|
|
44370
45173
|
})
|
|
44371
45174
|
);
|
|
45175
|
+
this.setReplaying(chart.replay.state.active);
|
|
44372
45176
|
this.render();
|
|
44373
45177
|
}
|
|
44374
45178
|
destroy() {
|
|
@@ -44609,26 +45413,26 @@ function isWeekday(ms, tz) {
|
|
|
44609
45413
|
return false;
|
|
44610
45414
|
}
|
|
44611
45415
|
}
|
|
44612
|
-
function deriveMarketStatus(
|
|
44613
|
-
const within = (ws) => ws.find(([s, e]) =>
|
|
45416
|
+
function deriveMarketStatus(now2, w, tz, overnight = false) {
|
|
45417
|
+
const within = (ws) => ws.find(([s, e]) => now2 >= s && now2 < e);
|
|
44614
45418
|
if (within(w.regular)) return "open";
|
|
44615
45419
|
const ext = within(w.extended);
|
|
44616
45420
|
if (ext) {
|
|
44617
45421
|
if (overnight) return "extended";
|
|
44618
|
-
return w.regular.some(([s]) => s >=
|
|
45422
|
+
return w.regular.some(([s]) => s >= now2 && s < ext[1]) ? "pre" : "post";
|
|
44619
45423
|
}
|
|
44620
|
-
if (isWeekday(
|
|
44621
|
-
const today = civilDate(
|
|
45424
|
+
if (isWeekday(now2, tz)) {
|
|
45425
|
+
const today = civilDate(now2, tz);
|
|
44622
45426
|
if (today !== "" && !w.extended.some(([s]) => civilDate(s, tz) === today)) return "holiday";
|
|
44623
45427
|
}
|
|
44624
45428
|
return "closed";
|
|
44625
45429
|
}
|
|
44626
|
-
function nextStatusBoundary(
|
|
45430
|
+
function nextStatusBoundary(now2, w) {
|
|
44627
45431
|
let next = null;
|
|
44628
45432
|
for (const ws of [w.regular, w.extended]) {
|
|
44629
45433
|
for (const [s, e] of ws) {
|
|
44630
45434
|
for (const t of [s, e]) {
|
|
44631
|
-
if (t >
|
|
45435
|
+
if (t > now2 && (next == null || t < next)) next = t;
|
|
44632
45436
|
}
|
|
44633
45437
|
}
|
|
44634
45438
|
}
|
|
@@ -44667,8 +45471,8 @@ var MarketStatusTracker = class {
|
|
|
44667
45471
|
return;
|
|
44668
45472
|
}
|
|
44669
45473
|
const tz = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : "Etc/UTC";
|
|
44670
|
-
const
|
|
44671
|
-
const range = { from:
|
|
45474
|
+
const now2 = Date.now();
|
|
45475
|
+
const range = { from: now2 - FETCH_BACK_MS, to: now2 + FETCH_AHEAD_MS };
|
|
44672
45476
|
const [regular, extended] = await Promise.all([
|
|
44673
45477
|
provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
|
|
44674
45478
|
provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
|
|
@@ -44680,9 +45484,9 @@ var MarketStatusTracker = class {
|
|
|
44680
45484
|
}
|
|
44681
45485
|
const w = { regular, extended };
|
|
44682
45486
|
const overnight = parseSessionSpec(si)?.overnight === true;
|
|
44683
|
-
this.onStatus(deriveMarketStatus(
|
|
44684
|
-
const boundary = nextStatusBoundary(
|
|
44685
|
-
const delay = Math.min(boundary != null ? boundary -
|
|
45487
|
+
this.onStatus(deriveMarketStatus(now2, w, tz, overnight));
|
|
45488
|
+
const boundary = nextStatusBoundary(now2, w);
|
|
45489
|
+
const delay = Math.min(boundary != null ? boundary - now2 : MAX_TIMER_MS, MAX_TIMER_MS);
|
|
44686
45490
|
this.arm(my, data, symbol, Math.max(delay, MIN_TIMER_MS));
|
|
44687
45491
|
}
|
|
44688
45492
|
arm(my, data, symbol, delay) {
|
|
@@ -44715,6 +45519,7 @@ var CSS22 = `
|
|
|
44715
45519
|
left: var(--vela-toolbar-gutter, 0px);
|
|
44716
45520
|
right: var(--vela-scale-gutter, 0px);
|
|
44717
45521
|
display: flex;
|
|
45522
|
+
flex-direction: column;
|
|
44718
45523
|
align-items: center;
|
|
44719
45524
|
justify-content: center;
|
|
44720
45525
|
overflow: hidden;
|
|
@@ -44728,6 +45533,14 @@ var CSS22 = `
|
|
|
44728
45533
|
user-select: none;
|
|
44729
45534
|
white-space: nowrap;
|
|
44730
45535
|
}
|
|
45536
|
+
.vela-watermark-replay {
|
|
45537
|
+
display: inline-flex;
|
|
45538
|
+
align-items: center;
|
|
45539
|
+
gap: 0.25em;
|
|
45540
|
+
margin-top: 0.1em;
|
|
45541
|
+
font-size: 0.8em;
|
|
45542
|
+
}
|
|
45543
|
+
.vela-watermark [hidden] { display: none !important; }
|
|
44731
45544
|
`;
|
|
44732
45545
|
function watermarkFontPx(availPx, textPxAtMax) {
|
|
44733
45546
|
if (textPxAtMax <= 0) return MAX_FONT_PX;
|
|
@@ -44738,6 +45551,10 @@ var Watermark = class {
|
|
|
44738
45551
|
this.resizeObserver = null;
|
|
44739
45552
|
/** The host's visibility preference (the persisted watermark toggle). */
|
|
44740
45553
|
this.shown = true;
|
|
45554
|
+
/** The host's preference for the replay line (its own persisted toggle). */
|
|
45555
|
+
this.replayShown = true;
|
|
45556
|
+
/** The chart is replaying past bars. */
|
|
45557
|
+
this.replaying = false;
|
|
44741
45558
|
/** A bar load is in flight with nothing painted — the loading affordance owns the
|
|
44742
45559
|
* canvas, so the mark stays out of its way. Starts true: the FIRST `load:start`
|
|
44743
45560
|
* fires during chart construction, before any subscriber can see it. */
|
|
@@ -44747,7 +45564,10 @@ var Watermark = class {
|
|
|
44747
45564
|
this.el.className = "vela-watermark";
|
|
44748
45565
|
this.el.dataset.velaScreenshot = "under";
|
|
44749
45566
|
this.text = host.ownerDocument.createElement("span");
|
|
44750
|
-
this.
|
|
45567
|
+
this.replayLine = host.ownerDocument.createElement("span");
|
|
45568
|
+
this.replayLine.className = "vela-watermark-replay";
|
|
45569
|
+
this.replayLine.append(iconEl("replay", host.ownerDocument), host.ownerDocument.createTextNode("Replay"));
|
|
45570
|
+
this.el.append(this.text, this.replayLine);
|
|
44751
45571
|
host.appendChild(this.el);
|
|
44752
45572
|
if (typeof ResizeObserver !== "undefined") {
|
|
44753
45573
|
this.resizeObserver = new ResizeObserver(() => this.fit());
|
|
@@ -44760,13 +45580,26 @@ var Watermark = class {
|
|
|
44760
45580
|
this.shown = visible;
|
|
44761
45581
|
this.sync();
|
|
44762
45582
|
}
|
|
45583
|
+
/** Show/hide the replay line (the host's toggle); it only ever shows while replaying. */
|
|
45584
|
+
setReplayVisible(visible) {
|
|
45585
|
+
this.replayShown = visible;
|
|
45586
|
+
this.sync();
|
|
45587
|
+
}
|
|
45588
|
+
/** The chart entered or left a bar replay. */
|
|
45589
|
+
setReplaying(replaying) {
|
|
45590
|
+
this.replaying = replaying;
|
|
45591
|
+
this.sync();
|
|
45592
|
+
}
|
|
44763
45593
|
/** Loading and the watermark never share the canvas — hidden while a load is up. */
|
|
44764
45594
|
setLoading(loading) {
|
|
44765
45595
|
this.loading = loading;
|
|
44766
45596
|
this.sync();
|
|
44767
45597
|
}
|
|
44768
45598
|
sync() {
|
|
44769
|
-
|
|
45599
|
+
const replay = this.replayShown && this.replaying;
|
|
45600
|
+
this.text.hidden = !this.shown;
|
|
45601
|
+
this.replayLine.hidden = !replay;
|
|
45602
|
+
this.el.style.display = (this.shown || replay) && !this.loading ? "" : "none";
|
|
44770
45603
|
}
|
|
44771
45604
|
update(symbol, timeframe) {
|
|
44772
45605
|
this.text.textContent = symbol ? `${parseSymbol(symbol).ticker} \xB7 ${timeframeLabel(timeframe)}` : "";
|
|
@@ -45355,8 +46188,12 @@ var ChartCell = class {
|
|
|
45355
46188
|
this.indicatorValuesOn = seed.indicatorValues ?? true;
|
|
45356
46189
|
if (!this.indicatorValuesOn) this.inner.renderer.set("indicatorValues", false);
|
|
45357
46190
|
this.watermarkOn = seed.watermark ?? deps.watermark;
|
|
46191
|
+
this.replayWatermarkOn = seed.replayWatermark ?? true;
|
|
45358
46192
|
this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
|
|
45359
46193
|
if (!this.watermarkOn) this.watermark?.setVisible(false);
|
|
46194
|
+
if (!this.replayWatermarkOn) this.watermark?.setReplayVisible(false);
|
|
46195
|
+
this.inner.on("replay:start", () => this.watermark?.setReplaying(true));
|
|
46196
|
+
this.inner.on("replay:end", () => this.watermark?.setReplaying(false));
|
|
45360
46197
|
this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
|
|
45361
46198
|
this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
|
|
45362
46199
|
this.statusline?.onChart(this.inner);
|
|
@@ -45551,10 +46388,10 @@ var ChartCell = class {
|
|
|
45551
46388
|
const chart = this.inner;
|
|
45552
46389
|
const symbol = this.state.symbol;
|
|
45553
46390
|
if (!chart || !symbol) return;
|
|
45554
|
-
const
|
|
46391
|
+
const now2 = Date.now();
|
|
45555
46392
|
const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
|
|
45556
46393
|
const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
|
|
45557
|
-
const range = chart.getVisibleRange() ?? { from:
|
|
46394
|
+
const range = chart.getVisibleRange() ?? { from: now2 - fallbackSpan, to: now2 };
|
|
45558
46395
|
this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
|
|
45559
46396
|
}
|
|
45560
46397
|
/** The session-shade colors live in the renderer CONFIG (persisted with it, edited
|
|
@@ -45670,6 +46507,13 @@ var ChartCell = class {
|
|
|
45670
46507
|
id: "visible",
|
|
45671
46508
|
get: () => this.watermarkOn,
|
|
45672
46509
|
set: (v) => this.setWatermarkVisible(v)
|
|
46510
|
+
},
|
|
46511
|
+
{
|
|
46512
|
+
kind: "toggle",
|
|
46513
|
+
label: "Replay watermark",
|
|
46514
|
+
id: "replay",
|
|
46515
|
+
get: () => this.replayWatermarkOn,
|
|
46516
|
+
set: (v) => this.setReplayWatermarkVisible(v)
|
|
45673
46517
|
}
|
|
45674
46518
|
]
|
|
45675
46519
|
};
|
|
@@ -45738,6 +46582,12 @@ var ChartCell = class {
|
|
|
45738
46582
|
this.watermark?.setVisible(visible);
|
|
45739
46583
|
this.deps.onStateDirty();
|
|
45740
46584
|
}
|
|
46585
|
+
/** Show/hide the "Replay" line under this cell's watermark while it replays (persisted per cell). */
|
|
46586
|
+
setReplayWatermarkVisible(visible) {
|
|
46587
|
+
this.replayWatermarkOn = visible;
|
|
46588
|
+
this.watermark?.setReplayVisible(visible);
|
|
46589
|
+
this.deps.onStateDirty();
|
|
46590
|
+
}
|
|
45741
46591
|
/** Show/hide this cell's indicator titles — the in-chart legend rows (persisted per cell). */
|
|
45742
46592
|
setIndicatorTitlesVisible(visible) {
|
|
45743
46593
|
this.indicatorTitlesOn = visible;
|
|
@@ -45978,7 +46828,7 @@ var ChartCell = class {
|
|
|
45978
46828
|
* then the manifest in the host's own order. */
|
|
45979
46829
|
libraryRows() {
|
|
45980
46830
|
return [
|
|
45981
|
-
...this.supportedNatives().map((n) => ({ name: n.title, category: "
|
|
46831
|
+
...this.supportedNatives().map((n) => ({ name: n.title, category: "Built-in", native: true, nativeType: n.type, beta: n.beta })),
|
|
45982
46832
|
...this.manifest.map((e) => ({ name: e.name, language: e.language, category: e.category }))
|
|
45983
46833
|
];
|
|
45984
46834
|
}
|
|
@@ -46211,6 +47061,7 @@ var ChartCell = class {
|
|
|
46211
47061
|
if (!this.inner || this.destroyed) return;
|
|
46212
47062
|
if (cs.priceStyle && cs.priceStyle !== this.priceStyle) this.setPriceStyle(cs.priceStyle);
|
|
46213
47063
|
if (cs.watermark !== void 0 && cs.watermark !== this.watermarkOn) this.setWatermarkVisible(cs.watermark);
|
|
47064
|
+
if (cs.replayWatermark !== void 0 && cs.replayWatermark !== this.replayWatermarkOn) this.setReplayWatermarkVisible(cs.replayWatermark);
|
|
46214
47065
|
if (cs.indicatorTitles !== void 0 && cs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(cs.indicatorTitles);
|
|
46215
47066
|
if (cs.indicatorValues !== void 0 && cs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(cs.indicatorValues);
|
|
46216
47067
|
if (cs.rendererConfig != null) this.inner.renderer.applyConfig(cs.rendererConfig);
|
|
@@ -46245,6 +47096,7 @@ var ChartCell = class {
|
|
|
46245
47096
|
...live ? { symbol: live.symbol, provider: live.provider, timeframe: live.timeframe } : {},
|
|
46246
47097
|
priceStyle: this.priceStyle,
|
|
46247
47098
|
watermark: this.watermarkOn,
|
|
47099
|
+
replayWatermark: this.replayWatermarkOn,
|
|
46248
47100
|
indicatorTitles: this.indicatorTitlesOn,
|
|
46249
47101
|
indicatorValues: this.indicatorValuesOn,
|
|
46250
47102
|
rendererConfig: this.inner?.renderer.getConfig() ?? void 0,
|
|
@@ -46309,6 +47161,7 @@ function buildContext(host) {
|
|
|
46309
47161
|
setPriceStyle: (style) => host.active()?.setPriceStyle(style),
|
|
46310
47162
|
openSymbolSearch: (query) => host.openSymbolSearch(query),
|
|
46311
47163
|
togglePanel: (id, open2) => host.togglePanel(id, open2),
|
|
47164
|
+
dockStrip: (el) => host.dockStrip(el),
|
|
46312
47165
|
host: host.root,
|
|
46313
47166
|
toast: (message, kind) => host.toast(message, kind),
|
|
46314
47167
|
addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
|
|
@@ -46320,10 +47173,303 @@ function buildContext(host) {
|
|
|
46320
47173
|
get activeCellId() {
|
|
46321
47174
|
return host.active()?.id ?? "";
|
|
46322
47175
|
},
|
|
46323
|
-
setActiveCell: (id) => host.setActiveCell(id)
|
|
47176
|
+
setActiveCell: (id) => host.setActiveCell(id),
|
|
47177
|
+
replay: host.replay
|
|
46324
47178
|
};
|
|
46325
47179
|
}
|
|
46326
47180
|
|
|
47181
|
+
// src/workspace/WorkspaceReplay.ts
|
|
47182
|
+
var MAX_BARS_PER_STEP = 1e5;
|
|
47183
|
+
var WorkspaceReplay = class {
|
|
47184
|
+
constructor(host) {
|
|
47185
|
+
this.host = host;
|
|
47186
|
+
this.bus = new TypedEventBus();
|
|
47187
|
+
/** The running session: the shared replay time (every chart shows the bars closed by it). */
|
|
47188
|
+
this.session = null;
|
|
47189
|
+
this.playing = false;
|
|
47190
|
+
this.intervalMs = 1e3;
|
|
47191
|
+
this.timer = null;
|
|
47192
|
+
/** Cells whose replay a symbol switch ended — restarted at the shared time once their bars land. */
|
|
47193
|
+
this.rejoining = /* @__PURE__ */ new Set();
|
|
47194
|
+
this.cellSubs = /* @__PURE__ */ new Map();
|
|
47195
|
+
/** Inside a group step: chart events are the controller's own doing. */
|
|
47196
|
+
this.stepping = false;
|
|
47197
|
+
/** Inside `finish`: the charts' `replay:end` events are the controller's own doing. */
|
|
47198
|
+
this.stopping = false;
|
|
47199
|
+
/** Driving a chart's own timer: its play/pause events are not the user's. */
|
|
47200
|
+
this.muted = 0;
|
|
47201
|
+
/** A chart finished during a group step — the session ends right after it. */
|
|
47202
|
+
this.finishedDuringStep = false;
|
|
47203
|
+
for (const cell of host.cells()) this.watch(cell);
|
|
47204
|
+
this.offHost = host.onCells((e) => {
|
|
47205
|
+
if (e.kind === "created") this.onCellCreated(e.id);
|
|
47206
|
+
else if (e.kind === "destroyed") this.onCellDestroyed(e.id);
|
|
47207
|
+
});
|
|
47208
|
+
}
|
|
47209
|
+
/**
|
|
47210
|
+
* Enter replay on every cell (or seek, when already replaying) — paused. Resolves once
|
|
47211
|
+
* every chart shows its rewound history (a chart whose history does not reach that far
|
|
47212
|
+
* loads older bars first). A chart with nothing to replay after the shared time stays
|
|
47213
|
+
* as it is.
|
|
47214
|
+
*/
|
|
47215
|
+
async start(opts) {
|
|
47216
|
+
const cells = this.host.cells();
|
|
47217
|
+
const ref = cells.find((c) => c.id === (opts.cell ?? this.host.activeId())) ?? cells[0];
|
|
47218
|
+
if (!ref) return;
|
|
47219
|
+
if (this.playing) this.pause();
|
|
47220
|
+
this.rejoining.clear();
|
|
47221
|
+
await ref.chart.replay.start({ from: opts.from });
|
|
47222
|
+
const cursor = ref.chart.replay.state.cursorTime;
|
|
47223
|
+
if (cursor == null) return;
|
|
47224
|
+
const session = { clock: barClose(cursor, ref.chart.market.timeframe) };
|
|
47225
|
+
this.session = session;
|
|
47226
|
+
await Promise.all(this.host.cells().filter((c) => c.id !== ref.id).map((c) => this.startCell(c, session.clock)));
|
|
47227
|
+
if (this.session !== session) return;
|
|
47228
|
+
this.syncPlayback();
|
|
47229
|
+
const s = this.activeChart()?.replay.state;
|
|
47230
|
+
this.bus.emit("replay:start", { cursorTime: s?.cursorTime ?? cursor, remaining: s?.remaining ?? 0 });
|
|
47231
|
+
}
|
|
47232
|
+
/** Reveal the next bar(s): the clock moves to the next bar close on any chart. Returns false when nothing is left. */
|
|
47233
|
+
step() {
|
|
47234
|
+
const solo = this.solo();
|
|
47235
|
+
return solo ? solo.replay.step() : this.stepGroup();
|
|
47236
|
+
}
|
|
47237
|
+
/** Reveal the next update — in a single-chart layout, the chart's `stepUpdate()` (tick replay); otherwise {@link step}. */
|
|
47238
|
+
stepUpdate() {
|
|
47239
|
+
const solo = this.solo();
|
|
47240
|
+
return solo ? solo.replay.stepUpdate() : this.stepGroup();
|
|
47241
|
+
}
|
|
47242
|
+
/** Advance one update every `intervalMs` (default: the last pace, initially 1000). Calling it again while playing changes the pace. */
|
|
47243
|
+
play(intervalMs2) {
|
|
47244
|
+
if (!this.session) return this;
|
|
47245
|
+
if (intervalMs2 !== void 0) {
|
|
47246
|
+
if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
|
|
47247
|
+
console.warn(`[vela] workspace.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
|
|
47248
|
+
return this;
|
|
47249
|
+
}
|
|
47250
|
+
this.intervalMs = intervalMs2;
|
|
47251
|
+
}
|
|
47252
|
+
this.playing = true;
|
|
47253
|
+
this.clearTimer();
|
|
47254
|
+
this.syncPlayback();
|
|
47255
|
+
this.bus.emit("replay:play", { intervalMs: this.intervalMs });
|
|
47256
|
+
return this;
|
|
47257
|
+
}
|
|
47258
|
+
pause() {
|
|
47259
|
+
if (!this.playing) return this;
|
|
47260
|
+
this.playing = false;
|
|
47261
|
+
this.syncPlayback();
|
|
47262
|
+
this.bus.emit("replay:pause", void 0);
|
|
47263
|
+
return this;
|
|
47264
|
+
}
|
|
47265
|
+
/** Leave replay on every chart: full history back, live updates resumed. */
|
|
47266
|
+
stop() {
|
|
47267
|
+
this.finish("stopped");
|
|
47268
|
+
return this;
|
|
47269
|
+
}
|
|
47270
|
+
/** `active`/`playing`/`intervalMs` for the whole workspace; the cursor fields read the ACTIVE cell's chart. */
|
|
47271
|
+
get state() {
|
|
47272
|
+
const s = this.session ? this.activeChart()?.replay.state : void 0;
|
|
47273
|
+
return {
|
|
47274
|
+
active: this.session !== null,
|
|
47275
|
+
playing: this.playing,
|
|
47276
|
+
cursorTime: s?.cursorTime ?? null,
|
|
47277
|
+
remaining: s?.remaining ?? 0,
|
|
47278
|
+
nextTime: s?.nextTime ?? null,
|
|
47279
|
+
intervalMs: this.intervalMs
|
|
47280
|
+
};
|
|
47281
|
+
}
|
|
47282
|
+
/** The active cell's replayable history (`chart.replay.bounds`). */
|
|
47283
|
+
get bounds() {
|
|
47284
|
+
return this.activeChart()?.replay.bounds ?? null;
|
|
47285
|
+
}
|
|
47286
|
+
on(event, handler) {
|
|
47287
|
+
return this.bus.on(event, handler);
|
|
47288
|
+
}
|
|
47289
|
+
destroy() {
|
|
47290
|
+
this.session = null;
|
|
47291
|
+
this.playing = false;
|
|
47292
|
+
this.clearTimer();
|
|
47293
|
+
this.offHost();
|
|
47294
|
+
for (const off of this.cellSubs.values()) off();
|
|
47295
|
+
this.cellSubs.clear();
|
|
47296
|
+
this.bus.clear();
|
|
47297
|
+
}
|
|
47298
|
+
// ── internals ──
|
|
47299
|
+
/** The one chart of a single-chart layout — it replays on its own, tick replay included. */
|
|
47300
|
+
solo() {
|
|
47301
|
+
const cells = this.host.cells();
|
|
47302
|
+
return cells.length === 1 ? cells[0].chart : null;
|
|
47303
|
+
}
|
|
47304
|
+
activeChart() {
|
|
47305
|
+
const cells = this.host.cells();
|
|
47306
|
+
return (cells.find((c) => c.id === this.host.activeId()) ?? cells[0])?.chart ?? null;
|
|
47307
|
+
}
|
|
47308
|
+
/** Rewind one chart to the shared time: every bar that closed by `clock`, none after. */
|
|
47309
|
+
async startCell(cell, clock) {
|
|
47310
|
+
const replay = cell.chart.replay;
|
|
47311
|
+
const from = lastOpenClosedBy(clock, cell.chart.market.timeframe);
|
|
47312
|
+
const bounds = replay.bounds;
|
|
47313
|
+
if (!bounds || bounds.last <= from) return;
|
|
47314
|
+
await replay.start({ from });
|
|
47315
|
+
}
|
|
47316
|
+
/** Move the clock to the next bar close on any chart; each chart reveals what closed by then. */
|
|
47317
|
+
stepGroup() {
|
|
47318
|
+
const session = this.session;
|
|
47319
|
+
if (!session) return false;
|
|
47320
|
+
const cells = this.host.cells();
|
|
47321
|
+
let next = Infinity;
|
|
47322
|
+
for (const c of cells) {
|
|
47323
|
+
const t = c.chart.replay.state.nextTime;
|
|
47324
|
+
if (t != null) next = Math.min(next, barClose(t, c.chart.market.timeframe));
|
|
47325
|
+
}
|
|
47326
|
+
if (!Number.isFinite(next)) return false;
|
|
47327
|
+
this.stepping = true;
|
|
47328
|
+
try {
|
|
47329
|
+
for (const c of cells) {
|
|
47330
|
+
const tf = c.chart.market.timeframe;
|
|
47331
|
+
for (let n = 0; n < MAX_BARS_PER_STEP; n += 1) {
|
|
47332
|
+
const t = c.chart.replay.state.nextTime;
|
|
47333
|
+
if (t == null || barClose(t, tf) > next) break;
|
|
47334
|
+
if (!c.chart.replay.step()) break;
|
|
47335
|
+
}
|
|
47336
|
+
}
|
|
47337
|
+
} finally {
|
|
47338
|
+
this.stepping = false;
|
|
47339
|
+
}
|
|
47340
|
+
session.clock = next;
|
|
47341
|
+
if (this.finishedDuringStep) {
|
|
47342
|
+
this.finishedDuringStep = false;
|
|
47343
|
+
this.finish("finished");
|
|
47344
|
+
return true;
|
|
47345
|
+
}
|
|
47346
|
+
const s = this.activeChart()?.replay.state;
|
|
47347
|
+
if (s?.cursorTime != null) this.bus.emit("replay:step", { cursorTime: s.cursorTime, remaining: s.remaining });
|
|
47348
|
+
return true;
|
|
47349
|
+
}
|
|
47350
|
+
/** Put the timers in the state `playing` asks for: a single chart runs its own (it paces
|
|
47351
|
+
* ticks), several run on the controller's clock. */
|
|
47352
|
+
syncPlayback() {
|
|
47353
|
+
const solo = this.solo();
|
|
47354
|
+
const run = this.playing && this.session !== null;
|
|
47355
|
+
this.muted += 1;
|
|
47356
|
+
try {
|
|
47357
|
+
for (const c of this.host.cells()) {
|
|
47358
|
+
if (c.chart === solo && run) {
|
|
47359
|
+
if (!c.chart.replay.state.playing || c.chart.replay.state.intervalMs !== this.intervalMs) c.chart.replay.play(this.intervalMs);
|
|
47360
|
+
} else if (c.chart.replay.state.playing) {
|
|
47361
|
+
c.chart.replay.pause();
|
|
47362
|
+
}
|
|
47363
|
+
}
|
|
47364
|
+
} finally {
|
|
47365
|
+
this.muted -= 1;
|
|
47366
|
+
}
|
|
47367
|
+
if (!run || solo) this.clearTimer();
|
|
47368
|
+
else if (this.timer == null) this.schedule();
|
|
47369
|
+
}
|
|
47370
|
+
schedule() {
|
|
47371
|
+
this.timer = setTimeout(() => {
|
|
47372
|
+
this.timer = null;
|
|
47373
|
+
if (!this.playing || !this.session || this.solo()) return;
|
|
47374
|
+
if (this.stepGroup() || this.rejoining.size > 0) {
|
|
47375
|
+
if (this.playing && this.session && this.timer == null) this.schedule();
|
|
47376
|
+
} else {
|
|
47377
|
+
this.pause();
|
|
47378
|
+
}
|
|
47379
|
+
}, this.intervalMs);
|
|
47380
|
+
}
|
|
47381
|
+
clearTimer() {
|
|
47382
|
+
if (this.timer != null) clearTimeout(this.timer);
|
|
47383
|
+
this.timer = null;
|
|
47384
|
+
}
|
|
47385
|
+
/** End the session on every chart (idempotent). */
|
|
47386
|
+
finish(reason) {
|
|
47387
|
+
const had = this.session !== null;
|
|
47388
|
+
this.session = null;
|
|
47389
|
+
this.playing = false;
|
|
47390
|
+
this.clearTimer();
|
|
47391
|
+
this.rejoining.clear();
|
|
47392
|
+
this.stopping = true;
|
|
47393
|
+
try {
|
|
47394
|
+
for (const c of this.host.cells()) c.chart.replay.stop();
|
|
47395
|
+
} finally {
|
|
47396
|
+
this.stopping = false;
|
|
47397
|
+
}
|
|
47398
|
+
if (had) this.bus.emit("replay:end", { reason });
|
|
47399
|
+
}
|
|
47400
|
+
watch(cell) {
|
|
47401
|
+
const { id, chart } = cell;
|
|
47402
|
+
const isActive = () => (this.host.activeId() ?? this.host.cells()[0]?.id) === id;
|
|
47403
|
+
const offs = [
|
|
47404
|
+
chart.on("replay:end", ({ reason }) => {
|
|
47405
|
+
if (!this.session || this.stopping) return;
|
|
47406
|
+
if (reason === "market") this.rejoining.add(id);
|
|
47407
|
+
else if (this.stepping) this.finishedDuringStep || (this.finishedDuringStep = reason === "finished");
|
|
47408
|
+
else this.finish(reason);
|
|
47409
|
+
}),
|
|
47410
|
+
chart.on("market:changed", () => {
|
|
47411
|
+
if (!this.rejoining.delete(id) || !this.session) return;
|
|
47412
|
+
void this.startCell(cell, this.session.clock).then(() => this.syncPlayback());
|
|
47413
|
+
}),
|
|
47414
|
+
chart.on("replay:step", (e) => {
|
|
47415
|
+
if (this.session && !this.stepping && isActive()) this.bus.emit("replay:step", e);
|
|
47416
|
+
}),
|
|
47417
|
+
chart.on("replay:tick", (e) => {
|
|
47418
|
+
if (this.session && isActive()) this.bus.emit("replay:tick", e);
|
|
47419
|
+
}),
|
|
47420
|
+
// A single chart runs its own timer: a pause it takes on its own (a seek) is the session's.
|
|
47421
|
+
chart.on("replay:pause", () => {
|
|
47422
|
+
if (!this.session || this.muted > 0 || !this.playing || chart !== this.solo()) return;
|
|
47423
|
+
this.playing = false;
|
|
47424
|
+
this.bus.emit("replay:pause", void 0);
|
|
47425
|
+
})
|
|
47426
|
+
];
|
|
47427
|
+
this.cellSubs.set(id, () => {
|
|
47428
|
+
for (const off of offs) off();
|
|
47429
|
+
});
|
|
47430
|
+
}
|
|
47431
|
+
onCellCreated(id) {
|
|
47432
|
+
const cell = this.host.cells().find((c) => c.id === id);
|
|
47433
|
+
if (!cell) return;
|
|
47434
|
+
this.watch(cell);
|
|
47435
|
+
const session = this.session;
|
|
47436
|
+
if (!session) return;
|
|
47437
|
+
this.rejoining.add(id);
|
|
47438
|
+
void cell.chart.ready().then(async () => {
|
|
47439
|
+
if (this.session !== session || !this.rejoining.delete(id)) return;
|
|
47440
|
+
await this.startCell(cell, session.clock);
|
|
47441
|
+
this.syncPlayback();
|
|
47442
|
+
});
|
|
47443
|
+
this.syncPlayback();
|
|
47444
|
+
}
|
|
47445
|
+
onCellDestroyed(id) {
|
|
47446
|
+
this.cellSubs.get(id)?.();
|
|
47447
|
+
this.cellSubs.delete(id);
|
|
47448
|
+
this.rejoining.delete(id);
|
|
47449
|
+
this.syncPlayback();
|
|
47450
|
+
}
|
|
47451
|
+
};
|
|
47452
|
+
function barClose(open2, timeframe) {
|
|
47453
|
+
const months = monthSpan(timeframe);
|
|
47454
|
+
if (months > 0) {
|
|
47455
|
+
const d = new Date(open2);
|
|
47456
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months, d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes());
|
|
47457
|
+
}
|
|
47458
|
+
return open2 + timeframeToMs(timeframe ?? "60");
|
|
47459
|
+
}
|
|
47460
|
+
function lastOpenClosedBy(time, timeframe) {
|
|
47461
|
+
const months = monthSpan(timeframe);
|
|
47462
|
+
if (months > 0) {
|
|
47463
|
+
const d = new Date(time);
|
|
47464
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() - months, d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes());
|
|
47465
|
+
}
|
|
47466
|
+
return time - timeframeToMs(timeframe ?? "60");
|
|
47467
|
+
}
|
|
47468
|
+
function monthSpan(timeframe) {
|
|
47469
|
+
const m = /^(\d*)M$/.exec((timeframe ?? "").trim());
|
|
47470
|
+
return m ? Number(m[1] || 1) : 0;
|
|
47471
|
+
}
|
|
47472
|
+
|
|
46327
47473
|
// src/workspace/layouts.ts
|
|
46328
47474
|
var registry5 = /* @__PURE__ */ new Map();
|
|
46329
47475
|
function registerLayout(def) {
|
|
@@ -46622,6 +47768,8 @@ var CSS24 = `
|
|
|
46622
47768
|
.vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
|
|
46623
47769
|
.vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
|
|
46624
47770
|
.vela-ws-toolbar { position: relative; flex: none; }
|
|
47771
|
+
.vela-ws-strips { position: relative; flex: none; display: flex; flex-direction: column; min-width: 0; }
|
|
47772
|
+
.vela-ws-strips:empty { display: none; }
|
|
46625
47773
|
.vela-ws-grid { position: relative; flex: 1 1 auto; min-width: 0; display: grid; gap: ${GAP_PX}px; background: var(--vela-border-soft); }
|
|
46626
47774
|
.vela-cell { background: var(--vela-bg); position: relative; }
|
|
46627
47775
|
/* Active-cell highlight: an overlay ring ABOVE the chart's own canvas stack (a plain
|
|
@@ -46680,11 +47828,11 @@ function resolveTyping(key, open2) {
|
|
|
46680
47828
|
if (open2.dialogs > 0) {
|
|
46681
47829
|
if (key.length !== 1) return null;
|
|
46682
47830
|
if (open2.symbolSearch) return { target: "symbol", text: key.toUpperCase() };
|
|
46683
|
-
if (open2.timeframeEntry) return { target: "timeframe", text: key };
|
|
47831
|
+
if (open2.timeframeEntry) return { target: "timeframe", text: key.toUpperCase() };
|
|
46684
47832
|
return null;
|
|
46685
47833
|
}
|
|
46686
|
-
if (/^[a-zA-
|
|
46687
|
-
if (/^[
|
|
47834
|
+
if (/^[a-zA-Z0]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
|
|
47835
|
+
if (/^[1-9]$/.test(key)) return { target: "timeframe", text: key };
|
|
46688
47836
|
return null;
|
|
46689
47837
|
}
|
|
46690
47838
|
var VelaWorkspace = class {
|
|
@@ -46793,6 +47941,20 @@ var VelaWorkspace = class {
|
|
|
46793
47941
|
const hostEl = typeof container === "string" ? document.querySelector(container) : container;
|
|
46794
47942
|
if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
|
|
46795
47943
|
this.opts = opts;
|
|
47944
|
+
this.replay = new WorkspaceReplay({
|
|
47945
|
+
cells: () => this.cells(),
|
|
47946
|
+
activeId: () => this.activeId,
|
|
47947
|
+
onCells: (handler) => {
|
|
47948
|
+
const offs = [
|
|
47949
|
+
this.events.on("cell:created", ({ id }) => handler({ kind: "created", id })),
|
|
47950
|
+
this.events.on("cell:destroyed", ({ id }) => handler({ kind: "destroyed", id })),
|
|
47951
|
+
this.events.on("cell:active", ({ id }) => handler({ kind: "active", id }))
|
|
47952
|
+
];
|
|
47953
|
+
return () => {
|
|
47954
|
+
for (const off of offs) off();
|
|
47955
|
+
};
|
|
47956
|
+
}
|
|
47957
|
+
});
|
|
46796
47958
|
this.persistKey = opts.persist === void 0 || opts.persist === false ? null : opts.persist === true ? "vela-workspace" : opts.persist;
|
|
46797
47959
|
this.storage = opts.storage ?? localStorageAdapter();
|
|
46798
47960
|
let boot = null;
|
|
@@ -46930,6 +48092,9 @@ var VelaWorkspace = class {
|
|
|
46930
48092
|
this.dock.refresh();
|
|
46931
48093
|
if (boot?.panels) this.dock.applyState(boot.panels);
|
|
46932
48094
|
this.root.appendChild(main);
|
|
48095
|
+
this.stripsEl = doc.createElement("div");
|
|
48096
|
+
this.stripsEl.className = "vela-ws-strips";
|
|
48097
|
+
this.root.appendChild(this.stripsEl);
|
|
46933
48098
|
this.toastHost = new Toast(this.gridEl);
|
|
46934
48099
|
const attribution = rendererDefaults().attribution;
|
|
46935
48100
|
if (attribution !== false) {
|
|
@@ -47164,9 +48329,16 @@ var VelaWorkspace = class {
|
|
|
47164
48329
|
setActiveCell: (id) => this.setActiveCell(id),
|
|
47165
48330
|
openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
|
|
47166
48331
|
togglePanel: (id, open2) => this.dock.toggle(id, open2),
|
|
48332
|
+
dockStrip: (el) => {
|
|
48333
|
+
this.stripsEl.appendChild(el);
|
|
48334
|
+
return () => {
|
|
48335
|
+
if (el.parentElement === this.stripsEl) el.remove();
|
|
48336
|
+
};
|
|
48337
|
+
},
|
|
47167
48338
|
root: this.root,
|
|
47168
48339
|
toast: (message, kind) => this.toastHost.show(message, kind),
|
|
47169
|
-
stateDirty: () => this.markStateDirty()
|
|
48340
|
+
stateDirty: () => this.markStateDirty(),
|
|
48341
|
+
replay: this.replay
|
|
47170
48342
|
});
|
|
47171
48343
|
}
|
|
47172
48344
|
/** Re-project contributed topbar actions + side panels, and mount late-registered attachments. */
|
|
@@ -47474,6 +48646,7 @@ var VelaWorkspace = class {
|
|
|
47474
48646
|
if (this.destroyed) return;
|
|
47475
48647
|
this.flushPendingState();
|
|
47476
48648
|
this.destroyed = true;
|
|
48649
|
+
this.replay.destroy();
|
|
47477
48650
|
if (this.persistKey !== null && typeof window !== "undefined") window.removeEventListener("beforeunload", this.onUnload);
|
|
47478
48651
|
this.resizeObserver?.disconnect();
|
|
47479
48652
|
this.splitters.destroy();
|
|
@@ -48240,14 +49413,13 @@ var VelaWorkspace = class {
|
|
|
48240
49413
|
panels: () => has("panels") ? [...this.dock.list()] : [],
|
|
48241
49414
|
onTogglePanel: (id) => this.dock.toggle(id),
|
|
48242
49415
|
...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
|
|
48243
|
-
//
|
|
48244
|
-
//
|
|
48245
|
-
//
|
|
48246
|
-
//
|
|
48247
|
-
|
|
48248
|
-
|
|
48249
|
-
|
|
48250
|
-
},
|
|
49416
|
+
// Actions placed on the bottom bar (left-aligned by default) have their stop
|
|
49417
|
+
// there — only the menu-placed ones land in the drawer, or they would appear
|
|
49418
|
+
// twice: left (primary) ones with the primary rows, right ones at the end.
|
|
49419
|
+
// Built-in-id actions are slot OVERRIDES: they reach the drawer through the
|
|
49420
|
+
// slot's own routed button (screenshot) or stop (indicators), never as a row.
|
|
49421
|
+
primaryActions: () => this.drawerActions("left"),
|
|
49422
|
+
actions: () => this.drawerActions("right"),
|
|
48251
49423
|
// The desktop layout dropdown's whole surface — the grid canvas, the
|
|
48252
49424
|
// non-canvas presets and the sync switches — relocated into the kebab
|
|
48253
49425
|
// drawer (the topbar is hidden on mobile). Same reads as the topbar block;
|
|
@@ -48272,6 +49444,11 @@ var VelaWorkspace = class {
|
|
|
48272
49444
|
}));
|
|
48273
49445
|
this.moreDrawer.open();
|
|
48274
49446
|
}
|
|
49447
|
+
/** The contributed topbar actions the mobile menu lists, from one cluster. */
|
|
49448
|
+
drawerActions(cluster) {
|
|
49449
|
+
const builtin = new Set(TOPBAR_BUILTIN_IDS);
|
|
49450
|
+
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()) }));
|
|
49451
|
+
}
|
|
48275
49452
|
openTimezoneDrawer() {
|
|
48276
49453
|
this.timezoneDrawer ?? (this.timezoneDrawer = new TimezoneDrawer({
|
|
48277
49454
|
host: this.root,
|
|
@@ -48393,7 +49570,7 @@ var VelaWorkspace = class {
|
|
|
48393
49570
|
}
|
|
48394
49571
|
});
|
|
48395
49572
|
}
|
|
48396
|
-
/** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
|
|
49573
|
+
/** Bare-typing router: letters and `0` → symbol search (seeded), digits 1–9 → timeframe entry. */
|
|
48397
49574
|
routeTyping(ev) {
|
|
48398
49575
|
if (this.destroyed) return;
|
|
48399
49576
|
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|