@luxalgo/vela 0.6.7 → 0.6.9
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/dist/{DataProvider-BsQM2WNH.d.cts → DataProvider-8Z95Q-RJ.d.cts} +1 -1
- package/dist/{DataProvider-BSLlBpB9.d.ts → DataProvider-DExJrfut.d.ts} +1 -1
- package/dist/{chunk-2R7BANEM.js → chunk-62SVGONC.js} +2 -2
- package/dist/{chunk-6WFKTAT4.js → chunk-6WDDVMBJ.js} +548 -147
- package/dist/{chunk-L3I2CCYO.js → chunk-EQCHJZOT.js} +49 -2
- package/dist/{chunk-OICPLNU7.js → chunk-FCVIG7JG.js} +188 -2
- package/dist/{chunk-WVP4XV5A.js → chunk-STHSKXOR.js} +407 -159
- package/dist/{chunk-PN5KWFZ4.js → chunk-WFSZBX3R.js} +2 -2
- package/dist/{contributions-Z12BrWCy.d.ts → contributions-C1U2Krwg.d.cts} +112 -7
- package/dist/{contributions-Hv4_cOJo.d.cts → contributions-D7PVZO2i.d.ts} +112 -7
- package/dist/index.cjs +755 -152
- package/dist/index.d.cts +12 -7
- package/dist/index.d.ts +12 -7
- package/dist/index.js +4 -4
- package/dist/{options-BaVTMXaO.d.ts → options-FM0peknS.d.cts} +84 -7
- package/dist/{options-BaVTMXaO.d.cts → options-FM0peknS.d.ts} +84 -7
- package/dist/{plugin-9koBjc5H.d.ts → plugin-7bkF32Rk.d.ts} +3 -3
- package/dist/{plugin-BXhDHTYn.d.cts → plugin-DfVqBz9p.d.cts} +3 -3
- package/dist/plugin.cjs +18 -2
- 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/{bottombar-Br4rzx_R.d.ts → statusline-DOPiT6I6.d.cts} +91 -6
- package/dist/{bottombar-Div5zibZ.d.cts → statusline-zdF4eZLr.d.ts} +91 -6
- package/dist/ui.cjs +201 -4
- package/dist/ui.d.cts +89 -2
- package/dist/ui.d.ts +89 -2
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +755 -152
- package/dist/vela.global.min.js +93 -44
- package/dist/widget.cjs +1144 -271
- package/dist/widget.d.cts +12 -85
- package/dist/widget.d.ts +12 -85
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +1144 -271
- package/dist/workspace.d.cts +65 -11
- package/dist/workspace.d.ts +65 -11
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel, Vela, TypedEventBus, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-
|
|
2
|
-
import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, widgetAttachments, getDrawingType } from './chunk-
|
|
3
|
-
import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-
|
|
4
|
-
import { Menu, Dialog, applyPlotOverlayTokens, ensureUIHost } from './chunk-
|
|
5
|
-
import { registerIcon, svg16, injectStyles, iconEl, iconMarkup,
|
|
1
|
+
import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel, LEGEND_AT_TOP_ATTR, Vela, TypedEventBus, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-6WDDVMBJ.js';
|
|
2
|
+
import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, legendCalloutsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, widgetAttachments, getDrawingType } from './chunk-EQCHJZOT.js';
|
|
3
|
+
import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-62SVGONC.js';
|
|
4
|
+
import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-FCVIG7JG.js';
|
|
5
|
+
import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, SESSION_OFF, SESSION_POST, SESSION_PRE, categoricalColor } from './chunk-WFSZBX3R.js';
|
|
6
6
|
import { baseOf } from './chunk-W4EJWLEO.js';
|
|
7
7
|
|
|
8
8
|
// src/widget/timeframe.ts
|
|
@@ -599,6 +599,11 @@ var Topbar = class {
|
|
|
599
599
|
/** Overrides that LEFT their native slot (default side + a declared `order`) — they
|
|
600
600
|
* render through the flow cluster like ordinary actions. */
|
|
601
601
|
this.flowingOverrides = /* @__PURE__ */ new Set();
|
|
602
|
+
/** Tooltips of the CURRENT icon-only action buttons — rebuilt with every
|
|
603
|
+
* renderActions pass (contributed buttons are replaceChildren'd away). */
|
|
604
|
+
this.actionTooltips = [];
|
|
605
|
+
/** `iconOnly` misuse warned once per action id (renderActions re-runs freely). */
|
|
606
|
+
this.warnedIconless = /* @__PURE__ */ new Set();
|
|
602
607
|
this.onHairlineSync = () => {
|
|
603
608
|
if (this.hairlineRaf) return;
|
|
604
609
|
const win = this.el.ownerDocument.defaultView;
|
|
@@ -649,7 +654,7 @@ var Topbar = class {
|
|
|
649
654
|
this.leftActionsHost.className = "vela-widget-actions-left";
|
|
650
655
|
this.panelsHost = doc.createElement("span");
|
|
651
656
|
this.panelsHost.className = "vela-widget-panels";
|
|
652
|
-
const tool = (cls,
|
|
657
|
+
const tool = (cls, icon, tip, onClick) => this.toolButton(cls, icon, tip, onClick, this.tooltips);
|
|
653
658
|
if (vis("undo-redo")) {
|
|
654
659
|
this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
|
|
655
660
|
this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
|
|
@@ -847,8 +852,8 @@ var Topbar = class {
|
|
|
847
852
|
}
|
|
848
853
|
renderStyleButton(doc) {
|
|
849
854
|
this.styleButton.replaceChildren();
|
|
850
|
-
const
|
|
851
|
-
if (
|
|
855
|
+
const icon = priceStyleIcon(this.priceStyle);
|
|
856
|
+
if (icon) this.styleButton.appendChild(iconEl(icon, doc));
|
|
852
857
|
else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle)));
|
|
853
858
|
this.styleButton.setAttribute("aria-label", `Chart style \u2014 ${priceStyleLabel(this.priceStyle)}`);
|
|
854
859
|
}
|
|
@@ -861,6 +866,8 @@ var Topbar = class {
|
|
|
861
866
|
this.actionsHost.replaceChildren();
|
|
862
867
|
this.leftActionsHost.replaceChildren();
|
|
863
868
|
for (const pin of this.pinned.values()) pin.host.replaceChildren();
|
|
869
|
+
for (const t of this.actionTooltips) t.destroy();
|
|
870
|
+
this.actionTooltips = [];
|
|
864
871
|
const doc = this.actionsHost.ownerDocument;
|
|
865
872
|
const flowLeft = this.comp.left.includes("actions");
|
|
866
873
|
const flowRight = this.comp.right.includes("actions");
|
|
@@ -870,10 +877,20 @@ var Topbar = class {
|
|
|
870
877
|
if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
|
|
871
878
|
const left = pin ? pin.left : action.align === "left";
|
|
872
879
|
if (!pin && !(left ? flowLeft : flowRight)) continue;
|
|
880
|
+
const iconOnly = action.iconOnly === true && !!action.icon;
|
|
881
|
+
if (action.iconOnly === true && !action.icon && !this.warnedIconless.has(action.id)) {
|
|
882
|
+
this.warnedIconless.add(action.id);
|
|
883
|
+
console.warn(`[vela] widget action "${action.id}": iconOnly needs an \`icon\` \u2014 rendering the label instead.`);
|
|
884
|
+
}
|
|
873
885
|
const b = doc.createElement("button");
|
|
874
|
-
b.className = left ? "vela-widget-action-left" : "vela-widget-action";
|
|
886
|
+
b.className = left ? "vela-widget-action-left" : iconOnly ? "vela-widget-tool" : "vela-widget-action";
|
|
875
887
|
if (action.icon) b.appendChild(iconEl(action.icon, doc));
|
|
876
|
-
|
|
888
|
+
if (iconOnly) {
|
|
889
|
+
b.setAttribute("aria-label", action.label);
|
|
890
|
+
this.actionTooltips.push(new Tooltip(b, { content: action.label, triggerId: `vela-action-${action.id}`, host: this.host }));
|
|
891
|
+
} else {
|
|
892
|
+
b.appendChild(doc.createTextNode(action.label));
|
|
893
|
+
}
|
|
877
894
|
b.addEventListener("click", () => {
|
|
878
895
|
const c = this.opts.getContext?.();
|
|
879
896
|
if (c) action.run(c);
|
|
@@ -924,15 +941,15 @@ var Topbar = class {
|
|
|
924
941
|
this.tfMenu.destroy();
|
|
925
942
|
this.styleMenu.destroy();
|
|
926
943
|
this.layoutPicker?.destroy();
|
|
927
|
-
for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
|
|
944
|
+
for (const t of [...this.tooltips, ...this.panelTooltips, ...this.actionTooltips]) t.destroy();
|
|
928
945
|
this.el.remove();
|
|
929
946
|
}
|
|
930
947
|
/** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
|
|
931
|
-
toolButton(cls,
|
|
948
|
+
toolButton(cls, icon, tip, onClick, sink) {
|
|
932
949
|
const doc = this.el.ownerDocument;
|
|
933
950
|
const b = doc.createElement("button");
|
|
934
951
|
b.className = `vela-widget-tool ${cls}`;
|
|
935
|
-
b.appendChild(iconEl(
|
|
952
|
+
b.appendChild(iconEl(icon, doc));
|
|
936
953
|
b.setAttribute("aria-label", tip);
|
|
937
954
|
sink.push(new Tooltip(b, { content: tip, triggerId: `vela-tool-${cls}`, host: this.host }));
|
|
938
955
|
if (onClick) b.addEventListener("click", onClick);
|
|
@@ -1612,13 +1629,13 @@ var MenuBuilder = class {
|
|
|
1612
1629
|
this.actions = /* @__PURE__ */ new Map();
|
|
1613
1630
|
this.seq = 0;
|
|
1614
1631
|
}
|
|
1615
|
-
entry(label,
|
|
1632
|
+
entry(label, icon, run, separatorBefore = false) {
|
|
1616
1633
|
const id = this.next();
|
|
1617
1634
|
this.actions.set(id, run);
|
|
1618
|
-
return { id, label, icon
|
|
1635
|
+
return { id, label, icon, separatorBefore };
|
|
1619
1636
|
}
|
|
1620
|
-
submenu(label,
|
|
1621
|
-
return { id: this.next(), label, icon
|
|
1637
|
+
submenu(label, icon, submenu, separatorBefore = false) {
|
|
1638
|
+
return { id: this.next(), label, icon, submenu, separatorBefore };
|
|
1622
1639
|
}
|
|
1623
1640
|
next() {
|
|
1624
1641
|
this.seq += 1;
|
|
@@ -1855,8 +1872,8 @@ var ObjectTree = class extends SidePanel {
|
|
|
1855
1872
|
const acts = doc.createElement("span");
|
|
1856
1873
|
acts.className = "vela-ot-acts";
|
|
1857
1874
|
head.appendChild(acts);
|
|
1858
|
-
const op = (
|
|
1859
|
-
acts.appendChild(this.btn({ icon
|
|
1875
|
+
const op = (icon, title, run) => {
|
|
1876
|
+
acts.appendChild(this.btn({ icon, title, run }));
|
|
1860
1877
|
};
|
|
1861
1878
|
if (pane.kind !== "price") {
|
|
1862
1879
|
if (pane.order > 1) op("arrow-up", "Move pane up", () => chart.panes.move(pane.id, "up"));
|
|
@@ -1871,8 +1888,8 @@ var ObjectTree = class extends SidePanel {
|
|
|
1871
1888
|
const { chart } = pass;
|
|
1872
1889
|
if (row.kind === "price") {
|
|
1873
1890
|
const base = this.symbolName.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || this.symbolName;
|
|
1874
|
-
const
|
|
1875
|
-
const el2 = this.row(
|
|
1891
|
+
const icon = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
|
|
1892
|
+
const el2 = this.row(icon, row.label, row.visible, [
|
|
1876
1893
|
{
|
|
1877
1894
|
icon: row.visible ? "eye" : "eye-off",
|
|
1878
1895
|
title: row.visible ? "Hide" : "Show",
|
|
@@ -1991,16 +2008,16 @@ var ObjectTree = class extends SidePanel {
|
|
|
1991
2008
|
const doc = this.el.ownerDocument;
|
|
1992
2009
|
const { chart } = pass;
|
|
1993
2010
|
const markup = drawingMeta(d.type).icon;
|
|
1994
|
-
let
|
|
2011
|
+
let icon;
|
|
1995
2012
|
if (markup) {
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2013
|
+
icon = doc.createElement("span");
|
|
2014
|
+
icon.className = "vela-icon";
|
|
2015
|
+
icon.setAttribute("aria-hidden", "true");
|
|
2016
|
+
icon.innerHTML = markup;
|
|
2000
2017
|
} else {
|
|
2001
|
-
|
|
2018
|
+
icon = iconEl("pen", doc);
|
|
2002
2019
|
}
|
|
2003
|
-
const el = this.row(
|
|
2020
|
+
const el = this.row(icon, drawingLabel(d), d.visible, [
|
|
2004
2021
|
{
|
|
2005
2022
|
icon: d.locked ? "lock" : "unlock",
|
|
2006
2023
|
title: d.locked ? "Unlock" : "Lock",
|
|
@@ -2066,7 +2083,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
2066
2083
|
}
|
|
2067
2084
|
}
|
|
2068
2085
|
/** The shared row shell: icon, name, optional tag, then the action buttons. */
|
|
2069
|
-
row(
|
|
2086
|
+
row(icon, label, visible, actions, tag) {
|
|
2070
2087
|
const doc = this.el.ownerDocument;
|
|
2071
2088
|
const el = doc.createElement("div");
|
|
2072
2089
|
el.className = "vela-ot-row";
|
|
@@ -2075,7 +2092,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
2075
2092
|
name.className = "vela-ot-name";
|
|
2076
2093
|
name.textContent = label;
|
|
2077
2094
|
name.title = label;
|
|
2078
|
-
el.append(
|
|
2095
|
+
el.append(icon, name);
|
|
2079
2096
|
if (tag) {
|
|
2080
2097
|
const t = doc.createElement("span");
|
|
2081
2098
|
t.className = "vela-ot-tag";
|
|
@@ -3667,19 +3684,8 @@ function indicatorLedger(i) {
|
|
|
3667
3684
|
};
|
|
3668
3685
|
}
|
|
3669
3686
|
|
|
3670
|
-
// src/workspace/sync.ts
|
|
3671
|
-
function syncTargets(originId, setting, cellIds) {
|
|
3672
|
-
if (setting == null || setting === false) return [];
|
|
3673
|
-
if (setting === true) return cellIds.filter((id) => id !== originId);
|
|
3674
|
-
const group = setting[originId];
|
|
3675
|
-
if (group == null) return [];
|
|
3676
|
-
return cellIds.filter((id) => id !== originId && setting[id] === group);
|
|
3677
|
-
}
|
|
3678
|
-
function rangesWithin(a, b, epsMs) {
|
|
3679
|
-
return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
|
|
3680
|
-
}
|
|
3681
|
-
|
|
3682
3687
|
// src/state/document.ts
|
|
3688
|
+
var SYNC_KINDS = ["viewport", "symbol", "timeframe", "crosshair", "drawings", "style"];
|
|
3683
3689
|
function prefixedSymbol(cell) {
|
|
3684
3690
|
if (!cell?.symbol) return void 0;
|
|
3685
3691
|
if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
|
|
@@ -3767,7 +3773,7 @@ function sanitizeSync(raw) {
|
|
|
3767
3773
|
if (raw == null || typeof raw !== "object") return null;
|
|
3768
3774
|
const s = raw;
|
|
3769
3775
|
const out = {};
|
|
3770
|
-
for (const kind of
|
|
3776
|
+
for (const kind of SYNC_KINDS) {
|
|
3771
3777
|
const v = s[kind];
|
|
3772
3778
|
if (v === true) out[kind] = true;
|
|
3773
3779
|
else if (v != null && typeof v === "object") {
|
|
@@ -3810,6 +3816,28 @@ function sanitizeTrackSizes(raw) {
|
|
|
3810
3816
|
return Object.keys(out).length > 0 ? out : null;
|
|
3811
3817
|
}
|
|
3812
3818
|
|
|
3819
|
+
// src/workspace/sync.ts
|
|
3820
|
+
function syncTargets(originId, setting, cellIds) {
|
|
3821
|
+
if (setting == null || setting === false) return [];
|
|
3822
|
+
if (setting === true) return cellIds.filter((id) => id !== originId);
|
|
3823
|
+
const group = setting[originId];
|
|
3824
|
+
if (group == null) return [];
|
|
3825
|
+
return cellIds.filter((id) => id !== originId && setting[id] === group);
|
|
3826
|
+
}
|
|
3827
|
+
function rangesWithin(a, b, epsMs) {
|
|
3828
|
+
return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
|
|
3829
|
+
}
|
|
3830
|
+
var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
|
|
3831
|
+
function styleConfigSlice(config) {
|
|
3832
|
+
if (config == null || typeof config !== "object") return null;
|
|
3833
|
+
const doc = config;
|
|
3834
|
+
const out = {};
|
|
3835
|
+
for (const key of STYLE_SYNC_CONFIG_KEYS) {
|
|
3836
|
+
if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
|
|
3837
|
+
}
|
|
3838
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3813
3841
|
// src/workspace/persist.ts
|
|
3814
3842
|
var memoryStore = /* @__PURE__ */ new Map();
|
|
3815
3843
|
function memoryStorageAdapter() {
|
|
@@ -3912,45 +3940,24 @@ var CSS10 = `
|
|
|
3912
3940
|
}
|
|
3913
3941
|
.vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
|
|
3914
3942
|
.vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
|
|
3915
|
-
/* Market status badge \u2014 icon-only 16px circle, label on hover
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
place-items: center;
|
|
3919
|
-
width: 16px;
|
|
3920
|
-
height: 16px;
|
|
3921
|
-
border-radius: 50%;
|
|
3922
|
-
flex: none;
|
|
3923
|
-
align-self: center;
|
|
3924
|
-
line-height: 0;
|
|
3925
|
-
cursor: default;
|
|
3926
|
-
}
|
|
3927
|
-
.vela-statusline .vela-sl-market svg {
|
|
3928
|
-
width: 12px;
|
|
3929
|
-
height: 12px;
|
|
3930
|
-
display: block;
|
|
3931
|
-
}
|
|
3932
|
-
/* Open wears the theme's up color; the other sessions are meaning constants from the
|
|
3933
|
-
* palette (amber pre, sky post, gray closed/holiday). Same ink at 20% for the circle. */
|
|
3934
|
-
.vela-statusline .vela-sl-market[data-status='open'] {
|
|
3935
|
-
background: color-mix(in srgb, var(--vela-up) 20%, transparent);
|
|
3936
|
-
color: var(--vela-up);
|
|
3937
|
-
}
|
|
3938
|
-
.vela-statusline .vela-sl-market[data-status='pre'] { background: color-mix(in srgb, ${SESSION_PRE} 20%, transparent); color: ${SESSION_PRE}; }
|
|
3939
|
-
.vela-statusline .vela-sl-market[data-status='post'] { background: color-mix(in srgb, ${SESSION_POST} 20%, transparent); color: ${SESSION_POST}; }
|
|
3940
|
-
.vela-statusline .vela-sl-market[data-status='closed'],
|
|
3941
|
-
.vela-statusline .vela-sl-market[data-status='holiday'] { background: color-mix(in srgb, ${SESSION_OFF} 20%, transparent); color: ${SESSION_OFF}; }
|
|
3943
|
+
/* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
|
|
3944
|
+
* via the kit tooltip); the session tint is applied per status in setMarketStatus. */
|
|
3945
|
+
.vela-statusline .vela-sl-market { align-self: center; }
|
|
3942
3946
|
.vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
|
|
3943
3947
|
.vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
|
|
3944
3948
|
/* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
|
|
3945
3949
|
* these are the pre-ink fallbacks only. */
|
|
3946
3950
|
.vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
|
|
3947
3951
|
.vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
|
|
3948
|
-
/* Stack the
|
|
3949
|
-
* The renderer
|
|
3950
|
-
*
|
|
3951
|
-
*
|
|
3952
|
-
*
|
|
3953
|
-
|
|
3952
|
+
/* Stack the TOP pane's legend below the status line (lower study panes stay put).
|
|
3953
|
+
* The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
|
|
3954
|
+
* normally, or a maximized study pane filling the plot \u2014 so the legend never merges
|
|
3955
|
+
* with the status line whichever pane owns the top. The renderer sets the legend's
|
|
3956
|
+
* inline top \u2014 shift with a transform, don't fight it. Scoped to hosts that actually
|
|
3957
|
+
* CARRY a status line (the marker class set by the Statusline constructor) \u2014 the
|
|
3958
|
+
* stylesheet is document-global, so a bare attribute selector here would shift every
|
|
3959
|
+
* chart on the page, including statusline-less ones. */
|
|
3960
|
+
.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
3954
3961
|
/* Mobile: two-line chip \u2014 logo / symbol / meta / market status on one aligned row, the
|
|
3955
3962
|
* bar change on the next. Full O/H/L/C stays hidden (too dense on a phone-width plot).
|
|
3956
3963
|
* GRID, not a wrapping flexbox: an absolutely positioned wrapping flex container sizes
|
|
@@ -3986,7 +3993,7 @@ var CSS10 = `
|
|
|
3986
3993
|
font-size: var(--vela-font-size-sm);
|
|
3987
3994
|
line-height: 1.2;
|
|
3988
3995
|
}
|
|
3989
|
-
[data-layout='mobile'] .vela-has-statusline [
|
|
3996
|
+
[data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
3990
3997
|
/* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
|
|
3991
3998
|
* don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
|
|
3992
3999
|
* overflow:hidden only guards the transient between a resize and the next measure.
|
|
@@ -4003,7 +4010,7 @@ var CSS10 = `
|
|
|
4003
4010
|
padding-left: 0;
|
|
4004
4011
|
}
|
|
4005
4012
|
/* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
|
|
4006
|
-
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [
|
|
4013
|
+
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
4007
4014
|
`;
|
|
4008
4015
|
function baseOfTicker(ticker) {
|
|
4009
4016
|
return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
|
|
@@ -4015,6 +4022,13 @@ var MARKET_LABELS = {
|
|
|
4015
4022
|
closed: "Market Closed",
|
|
4016
4023
|
holiday: "Market Holiday"
|
|
4017
4024
|
};
|
|
4025
|
+
var MARKET_INKS = {
|
|
4026
|
+
open: "var(--vela-up)",
|
|
4027
|
+
pre: SESSION_PRE,
|
|
4028
|
+
post: SESSION_POST,
|
|
4029
|
+
closed: SESSION_OFF,
|
|
4030
|
+
holiday: SESSION_OFF
|
|
4031
|
+
};
|
|
4018
4032
|
function statuslineInkOf(renderer, priceStyle) {
|
|
4019
4033
|
const cfg = renderer.getConfig();
|
|
4020
4034
|
const c = (v) => typeof v === "string" ? v : null;
|
|
@@ -4073,8 +4087,15 @@ var Statusline = class {
|
|
|
4073
4087
|
this.symbolEl.textContent = ticker;
|
|
4074
4088
|
this.metaEl = doc.createElement("span");
|
|
4075
4089
|
this.metaEl.className = "vela-sl-meta";
|
|
4076
|
-
this.
|
|
4077
|
-
|
|
4090
|
+
this.marketBubble = new CalloutBubble({
|
|
4091
|
+
icon: "market-open",
|
|
4092
|
+
background: `color-mix(in srgb, ${MARKET_INKS.open} 20%, transparent)`,
|
|
4093
|
+
color: MARKET_INKS.open,
|
|
4094
|
+
label: MARKET_LABELS.open,
|
|
4095
|
+
host
|
|
4096
|
+
});
|
|
4097
|
+
this.marketEl = this.marketBubble.el;
|
|
4098
|
+
this.marketEl.classList.add("vela-sl-market");
|
|
4078
4099
|
this.ohlcEl = doc.createElement("span");
|
|
4079
4100
|
this.ohlcEl.className = "vela-sl-ohlc";
|
|
4080
4101
|
this.changeEl = doc.createElement("span");
|
|
@@ -4162,8 +4183,13 @@ var Statusline = class {
|
|
|
4162
4183
|
* hover label. Callers with no session model leave the constructor's 'open'. */
|
|
4163
4184
|
setMarketStatus(status) {
|
|
4164
4185
|
this.marketEl.dataset.status = status;
|
|
4165
|
-
|
|
4166
|
-
this.
|
|
4186
|
+
const ink = MARKET_INKS[status];
|
|
4187
|
+
this.marketBubble.set({
|
|
4188
|
+
icon: `market-${status}`,
|
|
4189
|
+
background: `color-mix(in srgb, ${ink} 20%, transparent)`,
|
|
4190
|
+
color: ink,
|
|
4191
|
+
label: MARKET_LABELS[status]
|
|
4192
|
+
});
|
|
4167
4193
|
this.marketTip.setContent(MARKET_LABELS[status]);
|
|
4168
4194
|
}
|
|
4169
4195
|
setPartVisible(part, visible) {
|
|
@@ -4196,6 +4222,7 @@ var Statusline = class {
|
|
|
4196
4222
|
this.fitRO?.disconnect();
|
|
4197
4223
|
this.fitRO = null;
|
|
4198
4224
|
this.marketTip.destroy();
|
|
4225
|
+
this.marketBubble.destroy();
|
|
4199
4226
|
this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
|
|
4200
4227
|
this.el.remove();
|
|
4201
4228
|
}
|
|
@@ -4645,61 +4672,148 @@ var MarketStatusTracker = class {
|
|
|
4645
4672
|
};
|
|
4646
4673
|
|
|
4647
4674
|
// src/widget/session-shading.ts
|
|
4648
|
-
|
|
4675
|
+
var DAY_MS = 864e5;
|
|
4676
|
+
var MINUTE_MS = 6e4;
|
|
4677
|
+
function parseWindow(text) {
|
|
4678
|
+
if (typeof text !== "string") return null;
|
|
4679
|
+
const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
|
|
4680
|
+
if (!m) return null;
|
|
4681
|
+
const start = Number(m[1]) * 60 + Number(m[2]);
|
|
4682
|
+
const end = Number(m[3]) * 60 + Number(m[4]);
|
|
4683
|
+
if (start >= end || end > 1440) return null;
|
|
4684
|
+
return { start, end };
|
|
4685
|
+
}
|
|
4686
|
+
function parseSessionSpec(si) {
|
|
4687
|
+
const session = si?.["session"];
|
|
4688
|
+
if (typeof session !== "string" || session === "" || session === "24x7") return null;
|
|
4689
|
+
const regular = parseWindow(session);
|
|
4690
|
+
if (!regular) return null;
|
|
4691
|
+
const tz = si?.["timezone"];
|
|
4692
|
+
const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
|
|
4693
|
+
const ext = parseWindow(si?.["session_extended"]);
|
|
4694
|
+
const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
|
|
4695
|
+
return { regular, extended, timezone };
|
|
4696
|
+
}
|
|
4697
|
+
var dtfCache = /* @__PURE__ */ new Map();
|
|
4698
|
+
function civilFormatter(tz) {
|
|
4699
|
+
const cached = dtfCache.get(tz);
|
|
4700
|
+
if (cached !== void 0) return cached;
|
|
4701
|
+
let dtf = null;
|
|
4702
|
+
try {
|
|
4703
|
+
dtf = new Intl.DateTimeFormat("en-US", {
|
|
4704
|
+
timeZone: tz,
|
|
4705
|
+
hourCycle: "h23",
|
|
4706
|
+
weekday: "short",
|
|
4707
|
+
year: "numeric",
|
|
4708
|
+
month: "2-digit",
|
|
4709
|
+
day: "2-digit",
|
|
4710
|
+
hour: "2-digit",
|
|
4711
|
+
minute: "2-digit"
|
|
4712
|
+
});
|
|
4713
|
+
} catch {
|
|
4714
|
+
dtf = null;
|
|
4715
|
+
}
|
|
4716
|
+
dtfCache.set(tz, dtf);
|
|
4717
|
+
return dtf;
|
|
4718
|
+
}
|
|
4719
|
+
function civilParts(dtf, ms) {
|
|
4720
|
+
const parts = dtf.formatToParts(ms);
|
|
4721
|
+
const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
|
|
4722
|
+
return {
|
|
4723
|
+
weekday: get("weekday"),
|
|
4724
|
+
year: Number(get("year")),
|
|
4725
|
+
month: Number(get("month")),
|
|
4726
|
+
day: Number(get("day")),
|
|
4727
|
+
hour: Number(get("hour")) % 24,
|
|
4728
|
+
minute: Number(get("minute"))
|
|
4729
|
+
};
|
|
4730
|
+
}
|
|
4731
|
+
function expandSessionZones(spec, from, to) {
|
|
4649
4732
|
const pre = [];
|
|
4650
4733
|
const post = [];
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4734
|
+
const dtf = civilFormatter(spec.timezone);
|
|
4735
|
+
if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
|
|
4736
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4737
|
+
for (let cursor = from - DAY_MS; cursor < to + DAY_MS; cursor += DAY_MS) {
|
|
4738
|
+
const civil = civilParts(dtf, cursor);
|
|
4739
|
+
const key = civil.year * 1e4 + civil.month * 100 + civil.day;
|
|
4740
|
+
if (!Number.isFinite(key) || seen.has(key)) continue;
|
|
4741
|
+
seen.add(key);
|
|
4742
|
+
if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
|
|
4743
|
+
const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
|
|
4744
|
+
const noonGuess = naive + 720 * MINUTE_MS;
|
|
4745
|
+
const atNoon = civilParts(dtf, noonGuess);
|
|
4746
|
+
const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
|
|
4747
|
+
const at = (minutes) => naive + minutes * MINUTE_MS - offset;
|
|
4748
|
+
if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
|
|
4749
|
+
if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
|
|
4659
4750
|
}
|
|
4660
4751
|
return { pre, post };
|
|
4661
4752
|
}
|
|
4662
|
-
var
|
|
4663
|
-
var MIN_LOOKBACK_MS = 3 * 864e5;
|
|
4664
|
-
var MAX_LOOKBACK_MS = 120 * 864e5;
|
|
4753
|
+
var COVER_PAD_MIN_MS = 2 * DAY_MS;
|
|
4665
4754
|
var SessionShadingTracker = class {
|
|
4666
4755
|
constructor(onZones) {
|
|
4667
4756
|
this.onZones = onZones;
|
|
4668
|
-
/** Invalidates
|
|
4757
|
+
/** Invalidates the one async step (metadata resolution) — bumped by track()/stop(). */
|
|
4669
4758
|
this.epoch = 0;
|
|
4670
|
-
|
|
4671
|
-
|
|
4759
|
+
this.spec = null;
|
|
4760
|
+
this.ready = false;
|
|
4761
|
+
this.session = "regular";
|
|
4762
|
+
/** Whether one bar is shorter than a civil day — only then do pre/post bars exist. */
|
|
4763
|
+
this.intraday = false;
|
|
4764
|
+
this.covered = null;
|
|
4765
|
+
/** The newest range seen — viewport moves during metadata resolution (a load's fit
|
|
4766
|
+
* animation) must not be lost, so the resolution always expands the LATEST range. */
|
|
4767
|
+
this.lastRange = null;
|
|
4768
|
+
}
|
|
4769
|
+
/** (Re)bind to a chart's data surface + market and expand once metadata lands. */
|
|
4672
4770
|
track(data, symbol, opts) {
|
|
4673
4771
|
const my = ++this.epoch;
|
|
4674
|
-
|
|
4772
|
+
this.ready = false;
|
|
4773
|
+
this.spec = null;
|
|
4774
|
+
this.covered = null;
|
|
4775
|
+
this.session = opts.session;
|
|
4776
|
+
const tfMs = timeframeMs(opts.timeframe);
|
|
4777
|
+
this.intraday = Number.isFinite(tfMs) && tfMs < DAY_MS;
|
|
4778
|
+
this.lastRange = opts.range;
|
|
4779
|
+
void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
|
|
4780
|
+
if (my !== this.epoch) return;
|
|
4781
|
+
this.ready = true;
|
|
4782
|
+
this.spec = parseSessionSpec(si);
|
|
4783
|
+
this.emit(this.lastRange ?? opts.range, true);
|
|
4784
|
+
});
|
|
4785
|
+
}
|
|
4786
|
+
/** Follow a pan/zoom synchronously: bands are epoch-anchored, so only a range that
|
|
4787
|
+
* leaves the last expansion's coverage needs a recompute — no fetch, no debounce. */
|
|
4788
|
+
updateRange(range) {
|
|
4789
|
+
this.lastRange = range;
|
|
4790
|
+
if (!this.ready) return;
|
|
4791
|
+
this.emit(range, false);
|
|
4675
4792
|
}
|
|
4676
4793
|
stop() {
|
|
4677
4794
|
this.epoch += 1;
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
this.onZones(null);
|
|
4795
|
+
this.ready = false;
|
|
4796
|
+
this.spec = null;
|
|
4797
|
+
this.covered = null;
|
|
4798
|
+
this.lastRange = null;
|
|
4799
|
+
}
|
|
4800
|
+
emit(range, force) {
|
|
4801
|
+
if (!this.spec) {
|
|
4802
|
+
if (force) this.onZones(null);
|
|
4687
4803
|
return;
|
|
4688
4804
|
}
|
|
4689
|
-
if (
|
|
4690
|
-
this.onZones({ pre: [], post: [] });
|
|
4805
|
+
if (this.session !== "extended" || !this.intraday) {
|
|
4806
|
+
if (force) this.onZones({ pre: [], post: [] });
|
|
4691
4807
|
return;
|
|
4692
4808
|
}
|
|
4693
|
-
const
|
|
4694
|
-
const
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
if (!regular || !extended) return;
|
|
4702
|
-
this.onZones(deriveSessionZones(regular, extended));
|
|
4809
|
+
const from = Math.min(range.from, range.to);
|
|
4810
|
+
const to = Math.max(range.from, range.to);
|
|
4811
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) return;
|
|
4812
|
+
if (!force && this.covered && from >= this.covered.from && to <= this.covered.to) return;
|
|
4813
|
+
const pad = Math.max(to - from, COVER_PAD_MIN_MS);
|
|
4814
|
+
const covered = { from: from - pad, to: to + pad };
|
|
4815
|
+
this.covered = covered;
|
|
4816
|
+
this.onZones(expandSessionZones(this.spec, covered.from, covered.to));
|
|
4703
4817
|
}
|
|
4704
4818
|
};
|
|
4705
4819
|
|
|
@@ -4891,6 +5005,7 @@ var ChartCell = class {
|
|
|
4891
5005
|
this.inner.renderer.set("attribution", false);
|
|
4892
5006
|
this.inner.renderer.set("dialogHost", deps.dialogHost);
|
|
4893
5007
|
this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => deps.context()));
|
|
5008
|
+
this.inner.renderer.setLegendCallouts(legendCalloutsProviderFor(this.inner, () => deps.context()));
|
|
4894
5009
|
if (this.inner.renderer.supports("historyChords")) this.inner.renderer.set("historyChords", false);
|
|
4895
5010
|
this.history.onChart(this.inner);
|
|
4896
5011
|
this.inner.renderer.onConfigChanged(() => {
|
|
@@ -4924,7 +5039,11 @@ var ChartCell = class {
|
|
|
4924
5039
|
this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
|
|
4925
5040
|
});
|
|
4926
5041
|
this.inner.on("load:start", () => this.watermark?.setLoading(true));
|
|
4927
|
-
this.inner.on("load:end", () =>
|
|
5042
|
+
this.inner.on("load:end", () => {
|
|
5043
|
+
this.watermark?.setLoading(false);
|
|
5044
|
+
this.refreshSessionShading();
|
|
5045
|
+
});
|
|
5046
|
+
this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
|
|
4928
5047
|
const tz = deps.timezone();
|
|
4929
5048
|
if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
|
|
4930
5049
|
this.indicatorTitlesOn = seed.indicatorTitles ?? true;
|
|
@@ -5003,20 +5122,30 @@ var ChartCell = class {
|
|
|
5003
5122
|
this.refreshNativeCatalog();
|
|
5004
5123
|
this.pushSettingsSections();
|
|
5005
5124
|
this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
|
|
5006
|
-
this.
|
|
5007
|
-
this.state.provider = parseSymbol(symbol2).provider ?? void 0;
|
|
5008
|
-
this.state.timeframe = timeframe;
|
|
5009
|
-
this.state.session = normalizeSession(this.inner?.market.session);
|
|
5010
|
-
this.watermark?.update(symbol2, timeframe);
|
|
5011
|
-
this.statusline?.setSymbol(symbol2);
|
|
5012
|
-
this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol2) ?? this.state.provider ?? "");
|
|
5013
|
-
if (this.inner) this.statusline?.onChart(this.inner);
|
|
5125
|
+
this.projectMarket(symbol2, timeframe);
|
|
5014
5126
|
this.refreshNativeCatalog();
|
|
5015
5127
|
this.refreshSessionAvailable();
|
|
5016
5128
|
if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
|
|
5017
5129
|
this.deps.onMarketChanged(this.id);
|
|
5018
5130
|
});
|
|
5019
5131
|
}
|
|
5132
|
+
/**
|
|
5133
|
+
* Project a market identity into the cell state and its display overlays (watermark,
|
|
5134
|
+
* statusline), WITHOUT the data-dependent bookkeeping. Runs twice per user pick: once
|
|
5135
|
+
* optimistically from the cell setters — the labels reflect the pick immediately, not
|
|
5136
|
+
* after the bars load — and again from `market:changed` (the committed pass, and the
|
|
5137
|
+
* only pass for host `chart.setMarket` calls). Idempotent, so the double run converges.
|
|
5138
|
+
*/
|
|
5139
|
+
projectMarket(symbol, timeframe) {
|
|
5140
|
+
this.state.symbol = symbol;
|
|
5141
|
+
this.state.provider = parseSymbol(symbol).provider ?? void 0;
|
|
5142
|
+
this.state.timeframe = timeframe;
|
|
5143
|
+
this.state.session = normalizeSession(this.inner?.market.session);
|
|
5144
|
+
this.watermark?.update(symbol, timeframe);
|
|
5145
|
+
this.statusline?.setSymbol(symbol);
|
|
5146
|
+
this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol) ?? this.state.provider ?? "");
|
|
5147
|
+
if (this.inner) this.statusline?.onChart(this.inner);
|
|
5148
|
+
}
|
|
5020
5149
|
/**
|
|
5021
5150
|
* Does this cell's market HAVE sessions (RTH/ETH meaningful)? Derived from the
|
|
5022
5151
|
* symbol's own metadata (`syminfo.session !== '24x7'`), asynchronously — the
|
|
@@ -5051,14 +5180,18 @@ var ChartCell = class {
|
|
|
5051
5180
|
this.refreshSessionShading();
|
|
5052
5181
|
});
|
|
5053
5182
|
}
|
|
5054
|
-
/** (Re)derive the pre/post-market shading bands for this cell's market. The
|
|
5055
|
-
*
|
|
5183
|
+
/** (Re)derive the pre/post-market shading bands for this cell's market. The bands
|
|
5184
|
+
* expand locally from the symbol's session vocabulary, so they paint as soon as
|
|
5185
|
+
* metadata is known and follow any pan depth without provider round trips. */
|
|
5056
5186
|
refreshSessionShading() {
|
|
5057
5187
|
const chart = this.inner;
|
|
5058
5188
|
const symbol = this.state.symbol;
|
|
5059
5189
|
if (!chart || !symbol) return;
|
|
5060
|
-
const
|
|
5061
|
-
this.
|
|
5190
|
+
const now = Date.now();
|
|
5191
|
+
const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
|
|
5192
|
+
const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
|
|
5193
|
+
const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
|
|
5194
|
+
this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
|
|
5062
5195
|
}
|
|
5063
5196
|
/** The session-shade colors live in the renderer CONFIG (persisted with it, edited
|
|
5064
5197
|
* live by the dialog swatch) — the cell only proxies them into its settings rows. */
|
|
@@ -5154,10 +5287,10 @@ var ChartCell = class {
|
|
|
5154
5287
|
id: "status-line",
|
|
5155
5288
|
rows: [
|
|
5156
5289
|
{ kind: "heading", label: "Status line", id: "parts" },
|
|
5157
|
-
{ kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) =>
|
|
5158
|
-
{ kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) =>
|
|
5159
|
-
{ kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) =>
|
|
5160
|
-
{ kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) =>
|
|
5290
|
+
{ kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
|
|
5291
|
+
{ kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
|
|
5292
|
+
{ kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
|
|
5293
|
+
{ kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
|
|
5161
5294
|
{ kind: "heading", label: "Indicators", id: "indicators" },
|
|
5162
5295
|
{
|
|
5163
5296
|
kind: "toggle",
|
|
@@ -5192,12 +5325,40 @@ var ChartCell = class {
|
|
|
5192
5325
|
this.indicatorTitlesOn = visible;
|
|
5193
5326
|
this.inner?.renderer.set("indicatorTitles", visible);
|
|
5194
5327
|
this.deps.onStateDirty();
|
|
5328
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
5195
5329
|
}
|
|
5196
5330
|
/** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
|
|
5197
5331
|
setIndicatorValuesVisible(visible) {
|
|
5198
5332
|
this.indicatorValuesOn = visible;
|
|
5199
5333
|
this.inner?.renderer.set("indicatorValues", visible);
|
|
5200
5334
|
this.deps.onStateDirty();
|
|
5335
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
5336
|
+
}
|
|
5337
|
+
/** Show/hide one status-line segment (the settings dialog's Status line tab). */
|
|
5338
|
+
setStatuslinePart(part, visible) {
|
|
5339
|
+
this.statusline?.setPartVisible(part, visible);
|
|
5340
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
5341
|
+
}
|
|
5342
|
+
/** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
|
|
5343
|
+
statusPrefs() {
|
|
5344
|
+
const sl = this.statusline;
|
|
5345
|
+
return {
|
|
5346
|
+
parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
|
|
5347
|
+
indicatorTitles: this.indicatorTitlesOn,
|
|
5348
|
+
indicatorValues: this.indicatorValuesOn
|
|
5349
|
+
};
|
|
5350
|
+
}
|
|
5351
|
+
/** Converge this cell's Status line tab prefs to `prefs` — the follower half of
|
|
5352
|
+
* the workspace's style link. Idempotent: matching values change nothing, so a
|
|
5353
|
+
* propagated echo dies on its own. */
|
|
5354
|
+
applyStatusPrefs(prefs) {
|
|
5355
|
+
if (prefs.parts && this.statusline) {
|
|
5356
|
+
for (const part of Object.keys(prefs.parts)) {
|
|
5357
|
+
if (this.statusline.partVisible(part) !== prefs.parts[part]) this.statusline.setPartVisible(part, prefs.parts[part]);
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5360
|
+
if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
|
|
5361
|
+
if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
|
|
5201
5362
|
}
|
|
5202
5363
|
/** The LIVE chart of this cell — never cache it across a layout change (the cell's
|
|
5203
5364
|
* identity is what endures; the chart dies with the cell). */
|
|
@@ -5219,17 +5380,23 @@ var ChartCell = class {
|
|
|
5219
5380
|
get indicatorCount() {
|
|
5220
5381
|
return this.instances.length + this.nativeCatalog.filter((n) => n.present).length;
|
|
5221
5382
|
}
|
|
5222
|
-
/** Switch this cell's market in place (the chart instance survives).
|
|
5383
|
+
/** Switch this cell's market in place (the chart instance survives). The projection
|
|
5384
|
+
* is OPTIMISTIC — labels and chrome show the pick before the bars load; it follows
|
|
5385
|
+
* the setMarket call so the statusline reads the already-blanked chart. */
|
|
5223
5386
|
setSymbol(symbol) {
|
|
5224
5387
|
if (!this.inner || symbol === this.symbol) return;
|
|
5225
5388
|
this.unresolvedToasted = null;
|
|
5226
5389
|
void this.inner.setMarket({ symbol });
|
|
5390
|
+
this.projectMarket(symbol, this.timeframe);
|
|
5391
|
+
this.deps.onMarketChanged(this.id);
|
|
5227
5392
|
}
|
|
5228
5393
|
setTimeframe(timeframe) {
|
|
5229
5394
|
if (!this.inner || timeframe === this.timeframe) return;
|
|
5230
5395
|
this.activeRangeId = null;
|
|
5231
5396
|
this.rangeBars = 0;
|
|
5232
5397
|
void this.inner.setMarket({ timeframe, bars: this.state.bars });
|
|
5398
|
+
this.projectMarket(this.symbol, timeframe);
|
|
5399
|
+
this.deps.onMarketChanged(this.id);
|
|
5233
5400
|
}
|
|
5234
5401
|
/** Applied live (renderer feature) — no reload. */
|
|
5235
5402
|
setPriceStyle(style) {
|
|
@@ -6092,11 +6259,11 @@ var MobileBar = class {
|
|
|
6092
6259
|
injectStyles(STYLE_ID13, CSS13, doc);
|
|
6093
6260
|
this.el = doc.createElement("div");
|
|
6094
6261
|
this.el.className = "vela-mobilebar";
|
|
6095
|
-
const item = (cls, label, onClick,
|
|
6262
|
+
const item = (cls, label, onClick, icon) => {
|
|
6096
6263
|
const b = doc.createElement("button");
|
|
6097
6264
|
b.className = `vela-mb-item ${cls}`;
|
|
6098
6265
|
b.setAttribute("aria-label", label);
|
|
6099
|
-
if (
|
|
6266
|
+
if (icon) b.appendChild(iconEl(icon, doc));
|
|
6100
6267
|
b.addEventListener("click", onClick);
|
|
6101
6268
|
return b;
|
|
6102
6269
|
};
|
|
@@ -6642,11 +6809,11 @@ var MoreDrawer = class {
|
|
|
6642
6809
|
renderMain(doc) {
|
|
6643
6810
|
const actions = doc.createElement("div");
|
|
6644
6811
|
actions.className = "vela-md-actions";
|
|
6645
|
-
const action = (
|
|
6812
|
+
const action = (icon, label, enabled, run) => {
|
|
6646
6813
|
const b = doc.createElement("button");
|
|
6647
6814
|
b.className = "vela-md-action";
|
|
6648
6815
|
b.disabled = !enabled;
|
|
6649
|
-
b.appendChild(iconEl(
|
|
6816
|
+
b.appendChild(iconEl(icon, doc));
|
|
6650
6817
|
b.appendChild(doc.createTextNode(label));
|
|
6651
6818
|
b.addEventListener("click", () => {
|
|
6652
6819
|
run();
|
|
@@ -7071,11 +7238,11 @@ var DrawingPill = class {
|
|
|
7071
7238
|
this.el.hidden = true;
|
|
7072
7239
|
this.toolGlyph = doc.createElement("span");
|
|
7073
7240
|
this.toolGlyph.className = "vela-drawpill-tool";
|
|
7074
|
-
const btn = (
|
|
7241
|
+
const btn = (icon, label, onClick) => {
|
|
7075
7242
|
const b = doc.createElement("button");
|
|
7076
7243
|
b.className = "vela-drawpill-btn";
|
|
7077
7244
|
b.setAttribute("aria-label", label);
|
|
7078
|
-
b.appendChild(iconEl(
|
|
7245
|
+
b.appendChild(iconEl(icon, doc));
|
|
7079
7246
|
b.addEventListener("click", onClick);
|
|
7080
7247
|
return b;
|
|
7081
7248
|
};
|
|
@@ -7139,16 +7306,21 @@ var DrawingPill = class {
|
|
|
7139
7306
|
|
|
7140
7307
|
// src/workspace/context.ts
|
|
7141
7308
|
function buildContext(host) {
|
|
7142
|
-
const active = host.active();
|
|
7143
7309
|
return {
|
|
7144
7310
|
get chart() {
|
|
7145
7311
|
const cell = host.active();
|
|
7146
7312
|
if (!cell) throw new Error("VelaWorkspace has no active cell yet");
|
|
7147
7313
|
return cell.chart;
|
|
7148
7314
|
},
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7315
|
+
get symbol() {
|
|
7316
|
+
return host.active()?.symbol ?? "";
|
|
7317
|
+
},
|
|
7318
|
+
get timeframe() {
|
|
7319
|
+
return host.active()?.timeframe ?? "60";
|
|
7320
|
+
},
|
|
7321
|
+
get priceStyle() {
|
|
7322
|
+
return host.active()?.priceStyle ?? "candles";
|
|
7323
|
+
},
|
|
7152
7324
|
setSymbol: (symbol) => host.active()?.setSymbol(symbol),
|
|
7153
7325
|
setTimeframe: (tf) => host.active()?.setTimeframe(tf),
|
|
7154
7326
|
setPriceStyle: (style) => host.active()?.setPriceStyle(style),
|
|
@@ -7159,8 +7331,12 @@ function buildContext(host) {
|
|
|
7159
7331
|
addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
|
|
7160
7332
|
addNativeIndicator: (type) => host.active()?.addNative(type),
|
|
7161
7333
|
stateChanged: () => host.stateDirty(),
|
|
7162
|
-
|
|
7163
|
-
|
|
7334
|
+
get cells() {
|
|
7335
|
+
return host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe }));
|
|
7336
|
+
},
|
|
7337
|
+
get activeCellId() {
|
|
7338
|
+
return host.active()?.id ?? "";
|
|
7339
|
+
},
|
|
7164
7340
|
setActiveCell: (id) => host.setActiveCell(id)
|
|
7165
7341
|
};
|
|
7166
7342
|
}
|
|
@@ -7254,6 +7430,10 @@ var VelaWorkspace = class {
|
|
|
7254
7430
|
/** Same guard for the drawings link: the propagated mutations' own `drawing:*`
|
|
7255
7431
|
* events fire synchronously inside the propagation loop and must not fan out again. */
|
|
7256
7432
|
this.drawingSyncBusy = false;
|
|
7433
|
+
/** Same guard for the style link: a follower's `applyConfig` re-fires its
|
|
7434
|
+
* `onConfigChanged` in the same tick, and a state restore applies per-cell
|
|
7435
|
+
* configs that legitimately differ — neither must propagate. */
|
|
7436
|
+
this.styleSyncBusy = false;
|
|
7257
7437
|
/** LINKED drawings (the drawings sync): one map per synced set (cellId → that
|
|
7258
7438
|
* cell's drawing id), reachable from every member under its `cellId\0drawingId`
|
|
7259
7439
|
* key — any member finds its peers to push edits/removals onto. Survives a
|
|
@@ -7314,9 +7494,7 @@ var VelaWorkspace = class {
|
|
|
7314
7494
|
if (boot?.favorites) this.favs = [...boot.favorites];
|
|
7315
7495
|
if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
|
|
7316
7496
|
const sync = boot?.sync ?? opts.sync;
|
|
7317
|
-
for (const kind of
|
|
7318
|
-
this.applySyncSetting(kind, sync?.[kind]);
|
|
7319
|
-
}
|
|
7497
|
+
for (const kind of SYNC_KINDS) this.applySyncSetting(kind, sync?.[kind]);
|
|
7320
7498
|
this.monoLayout = opts.layout === false;
|
|
7321
7499
|
const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
|
|
7322
7500
|
this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
|
|
@@ -7400,7 +7578,8 @@ var VelaWorkspace = class {
|
|
|
7400
7578
|
syncs: () => [
|
|
7401
7579
|
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
7402
7580
|
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
7403
|
-
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
7581
|
+
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
|
|
7582
|
+
{ id: "style", label: "Style", checked: this.syncOpts.style === true }
|
|
7404
7583
|
],
|
|
7405
7584
|
onToggleSync: (id) => {
|
|
7406
7585
|
const kind = id;
|
|
@@ -7619,7 +7798,10 @@ var VelaWorkspace = class {
|
|
|
7619
7798
|
this.topbar.renderActions();
|
|
7620
7799
|
this.mobileBar?.renderActions();
|
|
7621
7800
|
this.dock.refresh();
|
|
7622
|
-
for (const cell of this.cells())
|
|
7801
|
+
for (const cell of this.cells()) {
|
|
7802
|
+
cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
|
|
7803
|
+
cell.chart.renderer.setLegendCallouts(legendCalloutsProviderFor(cell.chart, () => this.context()));
|
|
7804
|
+
}
|
|
7623
7805
|
}
|
|
7624
7806
|
// ── state surface (the SDK's read/restore of the whole grid's config + content) ──
|
|
7625
7807
|
/**
|
|
@@ -7684,7 +7866,7 @@ var VelaWorkspace = class {
|
|
|
7684
7866
|
this.topbar.setTimeframeFavorites(this.tfFavs);
|
|
7685
7867
|
}
|
|
7686
7868
|
this.dock.applyState(st.panels);
|
|
7687
|
-
for (const kind of
|
|
7869
|
+
for (const kind of SYNC_KINDS) this.applySyncSetting(kind, st.sync?.[kind]);
|
|
7688
7870
|
this.trackSizes.clear();
|
|
7689
7871
|
if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
|
|
7690
7872
|
const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
|
|
@@ -7695,9 +7877,14 @@ var VelaWorkspace = class {
|
|
|
7695
7877
|
for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
|
|
7696
7878
|
}
|
|
7697
7879
|
this.drawingLinks.clear();
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
this.
|
|
7880
|
+
this.styleSyncBusy = true;
|
|
7881
|
+
try {
|
|
7882
|
+
for (const [i] of this.def.cells.entries()) {
|
|
7883
|
+
const { id, ...cs } = st.charts[i];
|
|
7884
|
+
this.cellsById.get(id)?.rehydrate(cs);
|
|
7885
|
+
}
|
|
7886
|
+
} finally {
|
|
7887
|
+
this.styleSyncBusy = false;
|
|
7701
7888
|
}
|
|
7702
7889
|
if (st.timezone) this.setTimezone(st.timezone);
|
|
7703
7890
|
this.pool.clear();
|
|
@@ -7797,6 +7984,7 @@ var VelaWorkspace = class {
|
|
|
7797
7984
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
7798
7985
|
this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
|
|
7799
7986
|
const keep = new Set(this.order.slice(0, next.cells.length));
|
|
7987
|
+
const preexisting = new Set(this.cellsById.keys());
|
|
7800
7988
|
for (const [id, cell] of [...this.cellsById]) {
|
|
7801
7989
|
if (!keep.has(id) || rebuildAll) {
|
|
7802
7990
|
this.poolSet(id, cell.dehydrate());
|
|
@@ -7809,6 +7997,7 @@ var VelaWorkspace = class {
|
|
|
7809
7997
|
this.cellBackend = nextBackend;
|
|
7810
7998
|
this.applyGrid();
|
|
7811
7999
|
this.buildCells();
|
|
8000
|
+
this.alignNewCellStyles(preexisting);
|
|
7812
8001
|
this.syncCellPresentation();
|
|
7813
8002
|
this.topbar.setLayout(next.id);
|
|
7814
8003
|
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
@@ -8012,6 +8201,7 @@ var VelaWorkspace = class {
|
|
|
8012
8201
|
onMarketChanged: (id2) => this.onCellMarketChanged(id2),
|
|
8013
8202
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
8014
8203
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
8204
|
+
onStatusPrefsChanged: (id2) => this.propagateStylePrefs(id2),
|
|
8015
8205
|
onStateDirty: () => this.markStateDirty(),
|
|
8016
8206
|
manifestSettled: () => this.manifestSettled,
|
|
8017
8207
|
toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
|
|
@@ -8087,6 +8277,7 @@ var VelaWorkspace = class {
|
|
|
8087
8277
|
this.drawToolbar?.setEraserActive(mode === "eraser");
|
|
8088
8278
|
});
|
|
8089
8279
|
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
8280
|
+
chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
|
|
8090
8281
|
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
8091
8282
|
chart.renderer.onAxisLongPress((e) => {
|
|
8092
8283
|
if (this.layoutCtl.current !== "mobile") return;
|
|
@@ -8119,11 +8310,67 @@ var VelaWorkspace = class {
|
|
|
8119
8310
|
if (kind === "viewport") {
|
|
8120
8311
|
const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
|
|
8121
8312
|
if (range) this.propagateViewport(this.activeId, range);
|
|
8313
|
+
} else if (kind === "style") {
|
|
8314
|
+
this.propagateStylePrefs(this.activeId);
|
|
8122
8315
|
} else {
|
|
8123
8316
|
this.propagateMarket(this.activeId);
|
|
8124
8317
|
}
|
|
8125
8318
|
}
|
|
8126
8319
|
}
|
|
8320
|
+
/**
|
|
8321
|
+
* Align cells minted by a layout change to their style group: with the link on, a
|
|
8322
|
+
* NEW cell (fresh slot or one returning from the pool, which missed edits while
|
|
8323
|
+
* dormant) inherits the presentation of a pre-existing group peer — the active
|
|
8324
|
+
* cell when it is one — instead of sitting on its own state beside a styled
|
|
8325
|
+
* group. Propagation runs FROM the peer, so a newborn's defaults never overwrite
|
|
8326
|
+
* the group, and the equality short-circuits keep converged peers untouched.
|
|
8327
|
+
*/
|
|
8328
|
+
alignNewCellStyles(preexisting) {
|
|
8329
|
+
const setting = this.syncOpts.style;
|
|
8330
|
+
if (!setting) return;
|
|
8331
|
+
const ids = [...this.cellsById.keys()];
|
|
8332
|
+
const propagated = /* @__PURE__ */ new Set();
|
|
8333
|
+
for (const id of ids) {
|
|
8334
|
+
if (preexisting.has(id)) continue;
|
|
8335
|
+
const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
|
|
8336
|
+
if (peers.length === 0) continue;
|
|
8337
|
+
const source = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
|
|
8338
|
+
if (propagated.has(source)) continue;
|
|
8339
|
+
propagated.add(source);
|
|
8340
|
+
this.propagateStylePrefs(source);
|
|
8341
|
+
}
|
|
8342
|
+
}
|
|
8343
|
+
/**
|
|
8344
|
+
* Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
|
|
8345
|
+
* its renderer config plus its Status line tab prefs — onto its same-group
|
|
8346
|
+
* followers (the style link). Loop-safe two ways: the busy guard eats the
|
|
8347
|
+
* followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
|
|
8348
|
+
* in the same tick), and the equality short-circuits leave already-converged
|
|
8349
|
+
* followers untouched, so nothing re-emits once the group agrees.
|
|
8350
|
+
*/
|
|
8351
|
+
propagateStylePrefs(originId) {
|
|
8352
|
+
if (this.styleSyncBusy || this.destroyed) return;
|
|
8353
|
+
const targets = syncTargets(originId, this.syncOpts.style, [...this.cellsById.keys()]);
|
|
8354
|
+
if (targets.length === 0) return;
|
|
8355
|
+
const origin = this.cellsById.get(originId);
|
|
8356
|
+
if (!origin) return;
|
|
8357
|
+
const slice = styleConfigSlice(origin.chart.renderer.getConfig());
|
|
8358
|
+
const sliceJson = slice ? JSON.stringify(slice) : null;
|
|
8359
|
+
const prefs = origin.statusPrefs();
|
|
8360
|
+
this.styleSyncBusy = true;
|
|
8361
|
+
try {
|
|
8362
|
+
for (const id of targets) {
|
|
8363
|
+
const cell = this.cellsById.get(id);
|
|
8364
|
+
if (!cell) continue;
|
|
8365
|
+
if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
|
|
8366
|
+
cell.chart.renderer.applyConfig(slice);
|
|
8367
|
+
}
|
|
8368
|
+
cell.applyStatusPrefs(prefs);
|
|
8369
|
+
}
|
|
8370
|
+
} finally {
|
|
8371
|
+
this.styleSyncBusy = false;
|
|
8372
|
+
}
|
|
8373
|
+
}
|
|
8127
8374
|
/**
|
|
8128
8375
|
* Mirror an origin cell's pointer time onto its same-group followers as GHOST
|
|
8129
8376
|
* crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
|
|
@@ -8431,7 +8678,8 @@ var VelaWorkspace = class {
|
|
|
8431
8678
|
syncs: () => [
|
|
8432
8679
|
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
8433
8680
|
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
8434
|
-
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
8681
|
+
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
|
|
8682
|
+
{ id: "style", label: "Style", checked: this.syncOpts.style === true }
|
|
8435
8683
|
],
|
|
8436
8684
|
onToggleSync: (id) => {
|
|
8437
8685
|
const kind = id;
|