@luxalgo/vela 0.6.6 → 0.6.8
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-JJq7M2it.d.ts → DataProvider-BSLlBpB9.d.ts} +10 -0
- package/dist/{DataProvider-DDUYw2qP.d.cts → DataProvider-BsQM2WNH.d.cts} +10 -0
- package/dist/{chunk-OEUGUXL7.js → chunk-L3I2CCYO.js} +49 -1
- package/dist/{chunk-TMXK2SJR.js → chunk-N7LGMKCE.js} +523 -142
- package/dist/{chunk-JQFO2WMU.js → chunk-SQETIBFU.js} +83 -9
- package/dist/chunk-W4EJWLEO.js +12 -0
- package/dist/{contributions-BqCIsbaP.d.ts → contributions-DLLdV9jD.d.ts} +59 -3
- package/dist/{contributions-DNMqrAuw.d.cts → contributions-Vw-Hm58R.d.cts} +59 -3
- package/dist/index.cjs +110 -8
- package/dist/index.d.cts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +2 -2
- package/dist/{plugin-CN8U2__Q.d.cts → plugin-BnJgjLAy.d.cts} +2 -2
- package/dist/{plugin-WnvNNJOp.d.ts → plugin-CC7rBrOY.d.ts} +2 -2
- package/dist/plugin.cjs +28 -0
- package/dist/plugin.d.cts +3 -3
- package/dist/plugin.d.ts +3 -3
- package/dist/plugin.js +1 -1
- package/dist/providers/binance.cjs +16 -0
- package/dist/providers/binance.d.cts +4 -1
- package/dist/providers/binance.d.ts +4 -1
- package/dist/providers/binance.js +7 -0
- package/dist/providers/coinbase.cjs +16 -0
- package/dist/providers/coinbase.d.cts +4 -1
- package/dist/providers/coinbase.d.ts +4 -1
- package/dist/providers/coinbase.js +7 -0
- package/dist/providers/hyperliquid.cjs +16 -0
- package/dist/providers/hyperliquid.d.cts +4 -1
- package/dist/providers/hyperliquid.d.ts +4 -1
- package/dist/providers/hyperliquid.js +7 -0
- package/dist/{bottombar-COnL2Sk4.d.ts → statusline-CGkB2EOo.d.ts} +137 -8
- package/dist/{bottombar-DeDXbF_K.d.cts → statusline-DqzBqy42.d.cts} +137 -8
- package/dist/vela.global.js +136 -8
- package/dist/vela.global.min.js +42 -42
- package/dist/widget.cjs +667 -148
- package/dist/widget.d.cts +50 -81
- package/dist/widget.d.ts +50 -81
- package/dist/widget.js +5 -4
- package/dist/workspace.cjs +613 -150
- package/dist/workspace.d.cts +62 -9
- package/dist/workspace.d.ts +62 -9
- package/dist/workspace.js +4 -3
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
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, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, resolveEngines, legendActionsProviderFor, statePersistenceHandlers, rendererDefaults, widgetAttachments, getDrawingType } from './chunk-
|
|
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-SQETIBFU.js';
|
|
2
|
+
import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, widgetAttachments, getDrawingType } from './chunk-L3I2CCYO.js';
|
|
3
3
|
import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-2R7BANEM.js';
|
|
4
4
|
import { Menu, Dialog, applyPlotOverlayTokens, ensureUIHost } from './chunk-OICPLNU7.js';
|
|
5
5
|
import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, SESSION_PRE, SESSION_POST, SESSION_OFF, icon, categoricalColor } from './chunk-PN5KWFZ4.js';
|
|
6
|
+
import { baseOf } from './chunk-W4EJWLEO.js';
|
|
6
7
|
|
|
7
8
|
// src/widget/timeframe.ts
|
|
8
9
|
var UNIT_MS = {
|
|
@@ -526,9 +527,14 @@ var CSS2 = `
|
|
|
526
527
|
align-items: center;
|
|
527
528
|
justify-content: center;
|
|
528
529
|
}
|
|
529
|
-
|
|
530
|
+
/* The right side of the bar \u2014 whatever the composition puts there rides this one
|
|
531
|
+
auto-margin push (the flow-actions host used to carry it; composition can omit it). */
|
|
532
|
+
.vela-topbar-right { margin-left: auto; display: inline-flex; align-items: center; gap: var(--vela-space-1); }
|
|
533
|
+
.vela-widget-actions { display: inline-flex; gap: var(--vela-space-1); }
|
|
530
534
|
/* Left-aligned contributed actions \u2014 the primary-chrome cluster after the dropdowns. */
|
|
531
535
|
.vela-widget-actions-left { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
|
|
536
|
+
/* One PINNED contributed action's slot (a composition entry naming the action's id). */
|
|
537
|
+
.vela-widget-action-pin { display: inline-flex; align-items: center; }
|
|
532
538
|
/* The side-panel toggles, one per docked panel \u2014 a group so the dock can rebuild them
|
|
533
539
|
without disturbing the tools around it. */
|
|
534
540
|
.vela-widget-panels { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
|
|
@@ -587,6 +593,17 @@ var Topbar = class {
|
|
|
587
593
|
this.tooltips = [];
|
|
588
594
|
this.panelBtns = /* @__PURE__ */ new Map();
|
|
589
595
|
this.panelTooltips = [];
|
|
596
|
+
/** Pinned contributed-action slots, by action id (composition entries that name one,
|
|
597
|
+
* plus built-in slots taken over by an override). */
|
|
598
|
+
this.pinned = /* @__PURE__ */ new Map();
|
|
599
|
+
/** Overrides that LEFT their native slot (default side + a declared `order`) — they
|
|
600
|
+
* render through the flow cluster like ordinary actions. */
|
|
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();
|
|
590
607
|
this.onHairlineSync = () => {
|
|
591
608
|
if (this.hairlineRaf) return;
|
|
592
609
|
const win = this.el.ownerDocument.defaultView;
|
|
@@ -601,6 +618,8 @@ var Topbar = class {
|
|
|
601
618
|
this.host = host;
|
|
602
619
|
this.timeframe = opts.timeframe;
|
|
603
620
|
this.priceStyle = opts.priceStyle;
|
|
621
|
+
this.comp = resolveTopbarComposition(opts.composition);
|
|
622
|
+
const vis = (id) => topbarHas(this.comp, id);
|
|
604
623
|
const doc = host.ownerDocument;
|
|
605
624
|
injectStyles(STYLE_ID2, CSS2, doc);
|
|
606
625
|
this.el = doc.createElement("div");
|
|
@@ -623,7 +642,7 @@ var Topbar = class {
|
|
|
623
642
|
this.styleButton.className = "vela-widget-style";
|
|
624
643
|
this.renderStyleButton(doc);
|
|
625
644
|
let indicatorsBtn = null;
|
|
626
|
-
if (opts.onIndicatorsClick) {
|
|
645
|
+
if (opts.onIndicatorsClick && !topbarActionOverride("indicators")) {
|
|
627
646
|
indicatorsBtn = doc.createElement("button");
|
|
628
647
|
indicatorsBtn.className = "vela-widget-indicators";
|
|
629
648
|
indicatorsBtn.append(iconEl("indicators", doc), doc.createTextNode("Indicators"));
|
|
@@ -636,18 +655,23 @@ var Topbar = class {
|
|
|
636
655
|
this.panelsHost = doc.createElement("span");
|
|
637
656
|
this.panelsHost.className = "vela-widget-panels";
|
|
638
657
|
const tool = (cls, icon2, tip, onClick) => this.toolButton(cls, icon2, tip, onClick, this.tooltips);
|
|
639
|
-
|
|
640
|
-
|
|
658
|
+
if (vis("undo-redo")) {
|
|
659
|
+
this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
|
|
660
|
+
this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
|
|
661
|
+
} else {
|
|
662
|
+
this.undoBtn = doc.createElement("button");
|
|
663
|
+
this.redoBtn = doc.createElement("button");
|
|
664
|
+
}
|
|
641
665
|
this.setHistoryState(false, false);
|
|
642
|
-
const screenshotBtn = tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick);
|
|
643
|
-
this.alertsBtn = tool("vela-widget-alerts", "bell", "Alerts");
|
|
666
|
+
const screenshotBtn = vis("screenshot") && !topbarActionOverride("screenshot") ? tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick) : null;
|
|
667
|
+
this.alertsBtn = vis("alerts") ? tool("vela-widget-alerts", "bell", "Alerts") : doc.createElement("button");
|
|
644
668
|
this.alertsBtn.style.position = "relative";
|
|
645
669
|
if (opts.onAlertsClick) this.alertsBtn.addEventListener("click", () => opts.onAlertsClick(this.alertsBtn));
|
|
646
670
|
this.alertsBadge = doc.createElement("span");
|
|
647
671
|
this.alertsBadge.className = "vela-alerts-badge";
|
|
648
672
|
this.alertsBadge.style.display = "none";
|
|
649
673
|
this.alertsBtn.appendChild(this.alertsBadge);
|
|
650
|
-
if (opts.layout) {
|
|
674
|
+
if (opts.layout && vis("layout")) {
|
|
651
675
|
this.layoutId = opts.layout.current;
|
|
652
676
|
this.layoutButton = doc.createElement("button");
|
|
653
677
|
this.layoutButton.className = "vela-widget-style";
|
|
@@ -658,12 +682,65 @@ var Topbar = class {
|
|
|
658
682
|
d.className = "vela-sep";
|
|
659
683
|
return d;
|
|
660
684
|
};
|
|
661
|
-
const leading = [this.symbolEl, sep(), tfGroup, sep(), this.styleButton, sep()];
|
|
662
|
-
if (this.layoutButton) leading.push(this.layoutButton, sep());
|
|
663
|
-
if (indicatorsBtn) leading.push(indicatorsBtn, sep());
|
|
664
685
|
this.leftActionsSep = sep();
|
|
665
686
|
this.leftActionsSep.hidden = true;
|
|
666
|
-
|
|
687
|
+
const primaries = /* @__PURE__ */ new Set(["symbol", "timeframes", "style", "layout", "indicators"]);
|
|
688
|
+
const pinSlot = (id, left) => {
|
|
689
|
+
const slot = doc.createElement("span");
|
|
690
|
+
slot.className = "vela-widget-action-pin";
|
|
691
|
+
this.pinned.set(id, { host: slot, left });
|
|
692
|
+
return [slot];
|
|
693
|
+
};
|
|
694
|
+
const overridden = (id, left) => {
|
|
695
|
+
const ov = topbarActionOverride(id);
|
|
696
|
+
if (!ov) return null;
|
|
697
|
+
const sideDeclared = left ? opts.composition?.left != null : opts.composition?.right != null;
|
|
698
|
+
if (!sideDeclared && ov.order !== void 0) {
|
|
699
|
+
this.flowingOverrides.add(id);
|
|
700
|
+
return [];
|
|
701
|
+
}
|
|
702
|
+
return pinSlot(id, left);
|
|
703
|
+
};
|
|
704
|
+
const elementsFor = (id, left) => {
|
|
705
|
+
switch (id) {
|
|
706
|
+
case "symbol":
|
|
707
|
+
return [this.symbolEl];
|
|
708
|
+
case "timeframes":
|
|
709
|
+
return [tfGroup];
|
|
710
|
+
case "style":
|
|
711
|
+
return [this.styleButton];
|
|
712
|
+
case "layout":
|
|
713
|
+
return this.layoutButton ? [this.layoutButton] : [];
|
|
714
|
+
case "indicators":
|
|
715
|
+
return overridden(id, left) ?? (indicatorsBtn ? [indicatorsBtn] : []);
|
|
716
|
+
case "actions":
|
|
717
|
+
return left ? [this.leftActionsHost, this.leftActionsSep] : [this.actionsHost];
|
|
718
|
+
case "undo-redo":
|
|
719
|
+
return [this.undoBtn, this.redoBtn];
|
|
720
|
+
case "alerts":
|
|
721
|
+
return [this.alertsBtn];
|
|
722
|
+
case "panels":
|
|
723
|
+
return [this.panelsHost];
|
|
724
|
+
case "screenshot":
|
|
725
|
+
return overridden(id, left) ?? (screenshotBtn ? [screenshotBtn] : []);
|
|
726
|
+
default:
|
|
727
|
+
return pinSlot(id, left);
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
const sideEls = (list, left) => {
|
|
731
|
+
const out = [];
|
|
732
|
+
for (const [i, id] of list.entries()) {
|
|
733
|
+
const els = elementsFor(id, left);
|
|
734
|
+
if (els.length === 0) continue;
|
|
735
|
+
out.push(...els);
|
|
736
|
+
if (primaries.has(id) && i < list.length - 1) out.push(sep());
|
|
737
|
+
}
|
|
738
|
+
return out;
|
|
739
|
+
};
|
|
740
|
+
const right = doc.createElement("span");
|
|
741
|
+
right.className = "vela-topbar-right";
|
|
742
|
+
right.append(...sideEls(this.comp.right, false));
|
|
743
|
+
this.el.append(...sideEls(this.comp.left, true), right);
|
|
667
744
|
host.appendChild(this.el);
|
|
668
745
|
this.renderTfChips();
|
|
669
746
|
this.onHairlineSync();
|
|
@@ -780,23 +857,45 @@ var Topbar = class {
|
|
|
780
857
|
else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle)));
|
|
781
858
|
this.styleButton.setAttribute("aria-label", `Chart style \u2014 ${priceStyleLabel(this.priceStyle)}`);
|
|
782
859
|
}
|
|
783
|
-
/** Re-project the contributed topbar actions (call after registrations change).
|
|
860
|
+
/** Re-project the contributed topbar actions (call after registrations change).
|
|
861
|
+
* An action PINNED by the composition renders into its named slot (list position
|
|
862
|
+
* wins over `align`/`order`); the rest flow into the side's `actions` slot — or
|
|
863
|
+
* not at all when an explicit list omits it (the list is the side's contract). */
|
|
784
864
|
renderActions() {
|
|
785
865
|
const ctx = this.opts.getContext?.();
|
|
786
866
|
this.actionsHost.replaceChildren();
|
|
787
867
|
this.leftActionsHost.replaceChildren();
|
|
868
|
+
for (const pin of this.pinned.values()) pin.host.replaceChildren();
|
|
869
|
+
for (const t of this.actionTooltips) t.destroy();
|
|
870
|
+
this.actionTooltips = [];
|
|
788
871
|
const doc = this.actionsHost.ownerDocument;
|
|
872
|
+
const flowLeft = this.comp.left.includes("actions");
|
|
873
|
+
const flowRight = this.comp.right.includes("actions");
|
|
874
|
+
const builtin = new Set(TOPBAR_BUILTIN_IDS);
|
|
789
875
|
for (const action of widgetActions("topbar", ctx)) {
|
|
790
|
-
const
|
|
876
|
+
const pin = this.pinned.get(action.id);
|
|
877
|
+
if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
|
|
878
|
+
const left = pin ? pin.left : action.align === "left";
|
|
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
|
+
}
|
|
791
885
|
const b = doc.createElement("button");
|
|
792
|
-
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";
|
|
793
887
|
if (action.icon) b.appendChild(iconEl(action.icon, doc));
|
|
794
|
-
|
|
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
|
+
}
|
|
795
894
|
b.addEventListener("click", () => {
|
|
796
895
|
const c = this.opts.getContext?.();
|
|
797
896
|
if (c) action.run(c);
|
|
798
897
|
});
|
|
799
|
-
(left ? this.leftActionsHost : this.actionsHost).appendChild(b);
|
|
898
|
+
(pin ? pin.host : left ? this.leftActionsHost : this.actionsHost).appendChild(b);
|
|
800
899
|
}
|
|
801
900
|
this.leftActionsSep.hidden = this.leftActionsHost.childElementCount === 0;
|
|
802
901
|
this.onHairlineSync();
|
|
@@ -842,7 +941,7 @@ var Topbar = class {
|
|
|
842
941
|
this.tfMenu.destroy();
|
|
843
942
|
this.styleMenu.destroy();
|
|
844
943
|
this.layoutPicker?.destroy();
|
|
845
|
-
for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
|
|
944
|
+
for (const t of [...this.tooltips, ...this.panelTooltips, ...this.actionTooltips]) t.destroy();
|
|
846
945
|
this.el.remove();
|
|
847
946
|
}
|
|
848
947
|
/** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
|
|
@@ -1113,36 +1212,27 @@ var iconFailed = /* @__PURE__ */ new Set();
|
|
|
1113
1212
|
function initialsOf(name) {
|
|
1114
1213
|
return (name || "?").replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
|
|
1115
1214
|
}
|
|
1116
|
-
function
|
|
1117
|
-
return `https://crypto-icons.ledger.com/${encodeURIComponent(base.toUpperCase())}.png`;
|
|
1118
|
-
}
|
|
1119
|
-
function baseOf(d) {
|
|
1120
|
-
const fromDesc = d.description?.split("/")[0]?.trim();
|
|
1121
|
-
if (fromDesc) return fromDesc.replace(/\s+Perpetual$/i, "");
|
|
1122
|
-
return d.ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || d.ticker;
|
|
1123
|
-
}
|
|
1124
|
-
function tickerIconEl(doc, base, name, className) {
|
|
1215
|
+
function tickerIconEl(doc, base, name, className, iconUrl) {
|
|
1125
1216
|
const wrap = doc.createElement("span");
|
|
1126
1217
|
wrap.className = className;
|
|
1127
|
-
const key = base.toUpperCase();
|
|
1128
1218
|
const fallback = () => {
|
|
1129
1219
|
wrap.replaceChildren();
|
|
1130
1220
|
wrap.style.background = categoricalColor(name);
|
|
1131
1221
|
wrap.textContent = initialsOf(base || name);
|
|
1132
1222
|
};
|
|
1133
|
-
if (!
|
|
1223
|
+
if (!iconUrl || iconFailed.has(iconUrl)) {
|
|
1134
1224
|
fallback();
|
|
1135
1225
|
return wrap;
|
|
1136
1226
|
}
|
|
1137
1227
|
const img = doc.createElement("img");
|
|
1138
1228
|
img.alt = "";
|
|
1139
1229
|
img.crossOrigin = "anonymous";
|
|
1140
|
-
img.src =
|
|
1230
|
+
img.src = iconUrl;
|
|
1141
1231
|
img.style.cssText = "width:100%;height:100%;border-radius:50%;display:block;object-fit:cover;";
|
|
1142
1232
|
img.addEventListener(
|
|
1143
1233
|
"error",
|
|
1144
1234
|
() => {
|
|
1145
|
-
iconFailed.add(
|
|
1235
|
+
iconFailed.add(iconUrl);
|
|
1146
1236
|
fallback();
|
|
1147
1237
|
},
|
|
1148
1238
|
{ once: true }
|
|
@@ -1553,11 +1643,14 @@ var MenuBuilder = class {
|
|
|
1553
1643
|
}
|
|
1554
1644
|
};
|
|
1555
1645
|
var ObjectTree = class extends SidePanel {
|
|
1556
|
-
constructor(host) {
|
|
1646
|
+
constructor(host, iconFor) {
|
|
1557
1647
|
super(host, "Object tree", "vela-ot");
|
|
1648
|
+
this.iconFor = iconFor;
|
|
1558
1649
|
this.chart = null;
|
|
1559
1650
|
this.selectedDrawing = null;
|
|
1560
1651
|
this.symbolName = "";
|
|
1652
|
+
/** The raw (possibly venue-prefixed) symbol — what icon resolution routes on. */
|
|
1653
|
+
this.symbolRaw = "";
|
|
1561
1654
|
/** Drawing bundles — a view-side grouping, held for the panel's lifetime and never persisted.
|
|
1562
1655
|
* Kept per chart because a workspace points this one panel at whichever chart is active, and
|
|
1563
1656
|
* each chart's bundles have to survive the switch. */
|
|
@@ -1594,6 +1687,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
1594
1687
|
if (open) this.refresh();
|
|
1595
1688
|
}
|
|
1596
1689
|
setSymbol(symbol) {
|
|
1690
|
+
this.symbolRaw = symbol;
|
|
1597
1691
|
this.symbolName = parseSymbol(symbol).ticker;
|
|
1598
1692
|
}
|
|
1599
1693
|
/** (Re)bind to a chart instance — called after every widget rebuild. */
|
|
@@ -1794,7 +1888,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
1794
1888
|
const { chart } = pass;
|
|
1795
1889
|
if (row.kind === "price") {
|
|
1796
1890
|
const base = this.symbolName.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || this.symbolName;
|
|
1797
|
-
const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar");
|
|
1891
|
+
const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
|
|
1798
1892
|
const el2 = this.row(icon2, row.label, row.visible, [
|
|
1799
1893
|
{
|
|
1800
1894
|
icon: row.visible ? "eye" : "eye-off",
|
|
@@ -2826,6 +2920,17 @@ var PanelDock = class {
|
|
|
2826
2920
|
};
|
|
2827
2921
|
|
|
2828
2922
|
// src/widget/symbol-picker.ts
|
|
2923
|
+
function dedupeSymbols(list) {
|
|
2924
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2925
|
+
const out = [];
|
|
2926
|
+
for (const s of list) {
|
|
2927
|
+
const key = `${(s.prefix ?? s.provider ?? "").toLowerCase()}:${s.ticker.toUpperCase()}`;
|
|
2928
|
+
if (seen.has(key)) continue;
|
|
2929
|
+
seen.add(key);
|
|
2930
|
+
out.push(s);
|
|
2931
|
+
}
|
|
2932
|
+
return out;
|
|
2933
|
+
}
|
|
2829
2934
|
var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
|
|
2830
2935
|
function parseQuery(raw, venues) {
|
|
2831
2936
|
const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
|
|
@@ -2840,17 +2945,18 @@ function onlyOne(matches) {
|
|
|
2840
2945
|
return matches.length === 1 ? matches[0] : null;
|
|
2841
2946
|
}
|
|
2842
2947
|
var venueOf = (s) => s.prefix ?? s.provider;
|
|
2843
|
-
function filterSymbols(list, query, limit = 100) {
|
|
2948
|
+
function filterSymbols(list, query, limit = 100, top = TOP_TICKERS) {
|
|
2844
2949
|
const venues = [...new Set(list.flatMap((s) => [venueOf(s)?.toLowerCase(), s.provider?.toLowerCase()]).filter((p) => !!p))];
|
|
2845
2950
|
const { scope, term } = parseQuery(query, venues);
|
|
2846
2951
|
const pool = scope ? list.filter((s) => venueOf(s)?.toLowerCase() === scope || s.provider?.toLowerCase() === scope) : list;
|
|
2847
2952
|
const q = term.toUpperCase();
|
|
2848
2953
|
if (!q) {
|
|
2849
2954
|
if (scope) return [...pool].sort((a, b) => a.ticker.localeCompare(b.ticker)).slice(0, limit);
|
|
2955
|
+
if (top === false) return pool.slice(0, limit);
|
|
2850
2956
|
const byTicker = new Map(pool.map((s) => [s.ticker.toUpperCase(), s]));
|
|
2851
|
-
const
|
|
2852
|
-
const rest = pool.filter((s) => !
|
|
2853
|
-
return [...
|
|
2957
|
+
const pinned = top.map((t) => byTicker.get(t)).filter((s) => s !== void 0);
|
|
2958
|
+
const rest = pool.filter((s) => !top.includes(s.ticker.toUpperCase()));
|
|
2959
|
+
return [...pinned, ...rest].slice(0, limit);
|
|
2854
2960
|
}
|
|
2855
2961
|
const qLower = term.toLowerCase();
|
|
2856
2962
|
const prefix = [];
|
|
@@ -2960,12 +3066,16 @@ var CSS6 = `
|
|
|
2960
3066
|
var PAGE = 100;
|
|
2961
3067
|
var SymbolPicker = class {
|
|
2962
3068
|
constructor(opts) {
|
|
3069
|
+
this.opts = opts;
|
|
2963
3070
|
this.source = () => [];
|
|
2964
3071
|
this.rows = [];
|
|
2965
3072
|
this.highlighted = 0;
|
|
2966
3073
|
this.seed = "";
|
|
2967
3074
|
this.activeTab = "All";
|
|
2968
3075
|
this.visible = PAGE;
|
|
3076
|
+
/** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
|
|
3077
|
+
this.ranked = null;
|
|
3078
|
+
this.ranking = false;
|
|
2969
3079
|
const doc = (opts.host ?? document.body).ownerDocument;
|
|
2970
3080
|
injectStyles(STYLE_ID6, CSS6, doc);
|
|
2971
3081
|
this.input = doc.createElement("input");
|
|
@@ -3063,10 +3173,34 @@ var SymbolPicker = class {
|
|
|
3063
3173
|
} else delete el.dataset.highlighted;
|
|
3064
3174
|
});
|
|
3065
3175
|
}
|
|
3176
|
+
/** The picker's pool: the source, shaped by the registered symbol ranking. Cached —
|
|
3177
|
+
* the hook runs when the pool CHANGES (an index lands or refreshes), never per
|
|
3178
|
+
* keystroke; an async hook resolves onto the next repaint (stale-while-revalidate
|
|
3179
|
+
* in between, the raw pool before the first resolve). */
|
|
3180
|
+
pool() {
|
|
3181
|
+
const raw = this.source();
|
|
3182
|
+
const hook = symbolRanking();
|
|
3183
|
+
if (!hook) return raw;
|
|
3184
|
+
const key = `${raw.length}:${raw[0]?.ticker ?? ""}:${raw[raw.length - 1]?.ticker ?? ""}`;
|
|
3185
|
+
if (this.ranked?.key !== key && !this.ranking) {
|
|
3186
|
+
this.ranking = true;
|
|
3187
|
+
Promise.resolve([...raw]).then((copy) => hook(copy)).then((out) => {
|
|
3188
|
+
this.ranked = { key, result: dedupeSymbols(out) };
|
|
3189
|
+
}).catch((err) => {
|
|
3190
|
+
console.warn("[vela] symbol ranking failed \u2014 pool order kept:", err);
|
|
3191
|
+
this.ranked = { key, result: [...raw] };
|
|
3192
|
+
}).finally(() => {
|
|
3193
|
+
this.ranking = false;
|
|
3194
|
+
this.refresh();
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3197
|
+
return this.ranked?.result ?? raw;
|
|
3198
|
+
}
|
|
3066
3199
|
computeRows() {
|
|
3067
3200
|
const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
|
|
3068
|
-
const
|
|
3069
|
-
|
|
3201
|
+
const all = this.pool();
|
|
3202
|
+
const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
|
|
3203
|
+
return filterSymbols(pool, this.input.value, this.visible, symbolRanking() ? false : TOP_TICKERS);
|
|
3070
3204
|
}
|
|
3071
3205
|
refresh() {
|
|
3072
3206
|
const doc = this.list.ownerDocument;
|
|
@@ -3097,7 +3231,7 @@ var SymbolPicker = class {
|
|
|
3097
3231
|
row.dataset.ticker = s.ticker;
|
|
3098
3232
|
const venue = s.prefix ?? s.provider;
|
|
3099
3233
|
if (venue) row.dataset.venue = venue;
|
|
3100
|
-
const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
|
|
3234
|
+
const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar", this.opts.iconFor?.(s));
|
|
3101
3235
|
const main = doc.createElement("span");
|
|
3102
3236
|
main.className = "vela-sp-main";
|
|
3103
3237
|
const t = doc.createElement("span");
|
|
@@ -3550,19 +3684,8 @@ function indicatorLedger(i) {
|
|
|
3550
3684
|
};
|
|
3551
3685
|
}
|
|
3552
3686
|
|
|
3553
|
-
// src/workspace/sync.ts
|
|
3554
|
-
function syncTargets(originId, setting, cellIds) {
|
|
3555
|
-
if (setting == null || setting === false) return [];
|
|
3556
|
-
if (setting === true) return cellIds.filter((id) => id !== originId);
|
|
3557
|
-
const group = setting[originId];
|
|
3558
|
-
if (group == null) return [];
|
|
3559
|
-
return cellIds.filter((id) => id !== originId && setting[id] === group);
|
|
3560
|
-
}
|
|
3561
|
-
function rangesWithin(a, b, epsMs) {
|
|
3562
|
-
return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
|
|
3563
|
-
}
|
|
3564
|
-
|
|
3565
3687
|
// src/state/document.ts
|
|
3688
|
+
var SYNC_KINDS = ["viewport", "symbol", "timeframe", "crosshair", "drawings", "style"];
|
|
3566
3689
|
function prefixedSymbol(cell) {
|
|
3567
3690
|
if (!cell?.symbol) return void 0;
|
|
3568
3691
|
if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
|
|
@@ -3650,7 +3773,7 @@ function sanitizeSync(raw) {
|
|
|
3650
3773
|
if (raw == null || typeof raw !== "object") return null;
|
|
3651
3774
|
const s = raw;
|
|
3652
3775
|
const out = {};
|
|
3653
|
-
for (const kind of
|
|
3776
|
+
for (const kind of SYNC_KINDS) {
|
|
3654
3777
|
const v = s[kind];
|
|
3655
3778
|
if (v === true) out[kind] = true;
|
|
3656
3779
|
else if (v != null && typeof v === "object") {
|
|
@@ -3693,6 +3816,28 @@ function sanitizeTrackSizes(raw) {
|
|
|
3693
3816
|
return Object.keys(out).length > 0 ? out : null;
|
|
3694
3817
|
}
|
|
3695
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
|
+
|
|
3696
3841
|
// src/workspace/persist.ts
|
|
3697
3842
|
var memoryStore = /* @__PURE__ */ new Map();
|
|
3698
3843
|
function memoryStorageAdapter() {
|
|
@@ -3924,8 +4069,9 @@ function statuslineInkOf(renderer, priceStyle) {
|
|
|
3924
4069
|
}
|
|
3925
4070
|
}
|
|
3926
4071
|
var Statusline = class {
|
|
3927
|
-
constructor(host, symbol) {
|
|
4072
|
+
constructor(host, symbol, iconFor) {
|
|
3928
4073
|
this.host = host;
|
|
4074
|
+
this.iconFor = iconFor;
|
|
3929
4075
|
this.parts = { name: true, market: true, ohlc: true, change: true };
|
|
3930
4076
|
this.lastBar = null;
|
|
3931
4077
|
this.hoverBar = null;
|
|
@@ -3949,7 +4095,7 @@ var Statusline = class {
|
|
|
3949
4095
|
this.el.className = "vela-statusline";
|
|
3950
4096
|
this.el.dataset.velaScreenshot = "1";
|
|
3951
4097
|
const ticker = parseSymbol(symbol).ticker;
|
|
3952
|
-
this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar");
|
|
4098
|
+
this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
|
|
3953
4099
|
this.symbolEl = doc.createElement("span");
|
|
3954
4100
|
this.symbolEl.className = "vela-sl-symbol";
|
|
3955
4101
|
this.symbolEl.textContent = ticker;
|
|
@@ -3970,7 +4116,7 @@ var Statusline = class {
|
|
|
3970
4116
|
setSymbol(symbol) {
|
|
3971
4117
|
const ticker = parseSymbol(symbol).ticker;
|
|
3972
4118
|
this.symbolEl.textContent = ticker;
|
|
3973
|
-
const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar");
|
|
4119
|
+
const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
|
|
3974
4120
|
this.avatarEl.replaceWith(fresh);
|
|
3975
4121
|
this.avatarEl = fresh;
|
|
3976
4122
|
this.fit();
|
|
@@ -4527,61 +4673,144 @@ var MarketStatusTracker = class {
|
|
|
4527
4673
|
};
|
|
4528
4674
|
|
|
4529
4675
|
// src/widget/session-shading.ts
|
|
4530
|
-
|
|
4676
|
+
var DAY_MS = 864e5;
|
|
4677
|
+
var MINUTE_MS = 6e4;
|
|
4678
|
+
function parseWindow(text) {
|
|
4679
|
+
if (typeof text !== "string") return null;
|
|
4680
|
+
const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
|
|
4681
|
+
if (!m) return null;
|
|
4682
|
+
const start = Number(m[1]) * 60 + Number(m[2]);
|
|
4683
|
+
const end = Number(m[3]) * 60 + Number(m[4]);
|
|
4684
|
+
if (start >= end || end > 1440) return null;
|
|
4685
|
+
return { start, end };
|
|
4686
|
+
}
|
|
4687
|
+
function parseSessionSpec(si) {
|
|
4688
|
+
const session = si?.["session"];
|
|
4689
|
+
if (typeof session !== "string" || session === "" || session === "24x7") return null;
|
|
4690
|
+
const regular = parseWindow(session);
|
|
4691
|
+
if (!regular) return null;
|
|
4692
|
+
const tz = si?.["timezone"];
|
|
4693
|
+
const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
|
|
4694
|
+
const ext = parseWindow(si?.["session_extended"]);
|
|
4695
|
+
const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
|
|
4696
|
+
return { regular, extended, timezone };
|
|
4697
|
+
}
|
|
4698
|
+
var dtfCache = /* @__PURE__ */ new Map();
|
|
4699
|
+
function civilFormatter(tz) {
|
|
4700
|
+
const cached = dtfCache.get(tz);
|
|
4701
|
+
if (cached !== void 0) return cached;
|
|
4702
|
+
let dtf = null;
|
|
4703
|
+
try {
|
|
4704
|
+
dtf = new Intl.DateTimeFormat("en-US", {
|
|
4705
|
+
timeZone: tz,
|
|
4706
|
+
hourCycle: "h23",
|
|
4707
|
+
weekday: "short",
|
|
4708
|
+
year: "numeric",
|
|
4709
|
+
month: "2-digit",
|
|
4710
|
+
day: "2-digit",
|
|
4711
|
+
hour: "2-digit",
|
|
4712
|
+
minute: "2-digit"
|
|
4713
|
+
});
|
|
4714
|
+
} catch {
|
|
4715
|
+
dtf = null;
|
|
4716
|
+
}
|
|
4717
|
+
dtfCache.set(tz, dtf);
|
|
4718
|
+
return dtf;
|
|
4719
|
+
}
|
|
4720
|
+
function civilParts(dtf, ms) {
|
|
4721
|
+
const parts = dtf.formatToParts(ms);
|
|
4722
|
+
const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
|
|
4723
|
+
return {
|
|
4724
|
+
weekday: get("weekday"),
|
|
4725
|
+
year: Number(get("year")),
|
|
4726
|
+
month: Number(get("month")),
|
|
4727
|
+
day: Number(get("day")),
|
|
4728
|
+
hour: Number(get("hour")) % 24,
|
|
4729
|
+
minute: Number(get("minute"))
|
|
4730
|
+
};
|
|
4731
|
+
}
|
|
4732
|
+
function expandSessionZones(spec, from, to) {
|
|
4531
4733
|
const pre = [];
|
|
4532
4734
|
const post = [];
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4735
|
+
const dtf = civilFormatter(spec.timezone);
|
|
4736
|
+
if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
|
|
4737
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4738
|
+
for (let cursor = from - DAY_MS; cursor < to + DAY_MS; cursor += DAY_MS) {
|
|
4739
|
+
const civil = civilParts(dtf, cursor);
|
|
4740
|
+
const key = civil.year * 1e4 + civil.month * 100 + civil.day;
|
|
4741
|
+
if (!Number.isFinite(key) || seen.has(key)) continue;
|
|
4742
|
+
seen.add(key);
|
|
4743
|
+
if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
|
|
4744
|
+
const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
|
|
4745
|
+
const noonGuess = naive + 720 * MINUTE_MS;
|
|
4746
|
+
const atNoon = civilParts(dtf, noonGuess);
|
|
4747
|
+
const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
|
|
4748
|
+
const at = (minutes) => naive + minutes * MINUTE_MS - offset;
|
|
4749
|
+
if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
|
|
4750
|
+
if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
|
|
4541
4751
|
}
|
|
4542
4752
|
return { pre, post };
|
|
4543
4753
|
}
|
|
4544
|
-
var
|
|
4545
|
-
var MIN_LOOKBACK_MS = 3 * 864e5;
|
|
4546
|
-
var MAX_LOOKBACK_MS = 120 * 864e5;
|
|
4754
|
+
var COVER_PAD_MIN_MS = 2 * DAY_MS;
|
|
4547
4755
|
var SessionShadingTracker = class {
|
|
4548
4756
|
constructor(onZones) {
|
|
4549
4757
|
this.onZones = onZones;
|
|
4550
|
-
/** Invalidates
|
|
4758
|
+
/** Invalidates the one async step (metadata resolution) — bumped by track()/stop(). */
|
|
4551
4759
|
this.epoch = 0;
|
|
4552
|
-
|
|
4553
|
-
|
|
4760
|
+
this.spec = null;
|
|
4761
|
+
this.ready = false;
|
|
4762
|
+
this.session = "regular";
|
|
4763
|
+
this.covered = null;
|
|
4764
|
+
/** The newest range seen — viewport moves during metadata resolution (a load's fit
|
|
4765
|
+
* animation) must not be lost, so the resolution always expands the LATEST range. */
|
|
4766
|
+
this.lastRange = null;
|
|
4767
|
+
}
|
|
4768
|
+
/** (Re)bind to a chart's data surface + market and expand once metadata lands. */
|
|
4554
4769
|
track(data, symbol, opts) {
|
|
4555
4770
|
const my = ++this.epoch;
|
|
4556
|
-
|
|
4771
|
+
this.ready = false;
|
|
4772
|
+
this.spec = null;
|
|
4773
|
+
this.covered = null;
|
|
4774
|
+
this.session = opts.session;
|
|
4775
|
+
this.lastRange = opts.range;
|
|
4776
|
+
void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
|
|
4777
|
+
if (my !== this.epoch) return;
|
|
4778
|
+
this.ready = true;
|
|
4779
|
+
this.spec = parseSessionSpec(si);
|
|
4780
|
+
this.emit(this.lastRange ?? opts.range, true);
|
|
4781
|
+
});
|
|
4782
|
+
}
|
|
4783
|
+
/** Follow a pan/zoom synchronously: bands are epoch-anchored, so only a range that
|
|
4784
|
+
* leaves the last expansion's coverage needs a recompute — no fetch, no debounce. */
|
|
4785
|
+
updateRange(range) {
|
|
4786
|
+
this.lastRange = range;
|
|
4787
|
+
if (!this.ready) return;
|
|
4788
|
+
this.emit(range, false);
|
|
4557
4789
|
}
|
|
4558
4790
|
stop() {
|
|
4559
4791
|
this.epoch += 1;
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
this.onZones(null);
|
|
4792
|
+
this.ready = false;
|
|
4793
|
+
this.spec = null;
|
|
4794
|
+
this.covered = null;
|
|
4795
|
+
this.lastRange = null;
|
|
4796
|
+
}
|
|
4797
|
+
emit(range, force) {
|
|
4798
|
+
if (!this.spec) {
|
|
4799
|
+
if (force) this.onZones(null);
|
|
4569
4800
|
return;
|
|
4570
4801
|
}
|
|
4571
|
-
if (
|
|
4572
|
-
this.onZones({ pre: [], post: [] });
|
|
4802
|
+
if (this.session !== "extended") {
|
|
4803
|
+
if (force) this.onZones({ pre: [], post: [] });
|
|
4573
4804
|
return;
|
|
4574
4805
|
}
|
|
4575
|
-
const
|
|
4576
|
-
const
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
if (!regular || !extended) return;
|
|
4584
|
-
this.onZones(deriveSessionZones(regular, extended));
|
|
4806
|
+
const from = Math.min(range.from, range.to);
|
|
4807
|
+
const to = Math.max(range.from, range.to);
|
|
4808
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) return;
|
|
4809
|
+
if (!force && this.covered && from >= this.covered.from && to <= this.covered.to) return;
|
|
4810
|
+
const pad = Math.max(to - from, COVER_PAD_MIN_MS);
|
|
4811
|
+
const covered = { from: from - pad, to: to + pad };
|
|
4812
|
+
this.covered = covered;
|
|
4813
|
+
this.onZones(expandSessionZones(this.spec, covered.from, covered.to));
|
|
4585
4814
|
}
|
|
4586
4815
|
};
|
|
4587
4816
|
|
|
@@ -4806,7 +5035,11 @@ var ChartCell = class {
|
|
|
4806
5035
|
this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
|
|
4807
5036
|
});
|
|
4808
5037
|
this.inner.on("load:start", () => this.watermark?.setLoading(true));
|
|
4809
|
-
this.inner.on("load:end", () =>
|
|
5038
|
+
this.inner.on("load:end", () => {
|
|
5039
|
+
this.watermark?.setLoading(false);
|
|
5040
|
+
this.refreshSessionShading();
|
|
5041
|
+
});
|
|
5042
|
+
this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
|
|
4810
5043
|
const tz = deps.timezone();
|
|
4811
5044
|
if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
|
|
4812
5045
|
this.indicatorTitlesOn = seed.indicatorTitles ?? true;
|
|
@@ -4816,12 +5049,15 @@ var ChartCell = class {
|
|
|
4816
5049
|
this.watermarkOn = seed.watermark ?? deps.watermark;
|
|
4817
5050
|
this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
|
|
4818
5051
|
if (!this.watermarkOn) this.watermark?.setVisible(false);
|
|
4819
|
-
this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "") : null;
|
|
5052
|
+
this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
|
|
4820
5053
|
this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
|
|
4821
5054
|
this.statusline?.onChart(this.inner);
|
|
4822
5055
|
this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
|
|
4823
5056
|
void this.inner.data.ready().then(() => {
|
|
4824
|
-
if (this.inner && this.state.symbol)
|
|
5057
|
+
if (this.inner && this.state.symbol) {
|
|
5058
|
+
this.statusline?.setSymbol(this.state.symbol);
|
|
5059
|
+
this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
|
|
5060
|
+
}
|
|
4825
5061
|
this.refreshSessionAvailable();
|
|
4826
5062
|
if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
|
|
4827
5063
|
});
|
|
@@ -4930,14 +5166,18 @@ var ChartCell = class {
|
|
|
4930
5166
|
this.refreshSessionShading();
|
|
4931
5167
|
});
|
|
4932
5168
|
}
|
|
4933
|
-
/** (Re)derive the pre/post-market shading bands for this cell's market. The
|
|
4934
|
-
*
|
|
5169
|
+
/** (Re)derive the pre/post-market shading bands for this cell's market. The bands
|
|
5170
|
+
* expand locally from the symbol's session vocabulary, so they paint as soon as
|
|
5171
|
+
* metadata is known and follow any pan depth without provider round trips. */
|
|
4935
5172
|
refreshSessionShading() {
|
|
4936
5173
|
const chart = this.inner;
|
|
4937
5174
|
const symbol = this.state.symbol;
|
|
4938
5175
|
if (!chart || !symbol) return;
|
|
4939
|
-
const
|
|
4940
|
-
this.
|
|
5176
|
+
const now = Date.now();
|
|
5177
|
+
const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
|
|
5178
|
+
const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
|
|
5179
|
+
const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
|
|
5180
|
+
this.sessionShading.track(chart.data, symbol, { session: this.session, range });
|
|
4941
5181
|
}
|
|
4942
5182
|
/** The session-shade colors live in the renderer CONFIG (persisted with it, edited
|
|
4943
5183
|
* live by the dialog swatch) — the cell only proxies them into its settings rows. */
|
|
@@ -5033,10 +5273,10 @@ var ChartCell = class {
|
|
|
5033
5273
|
id: "status-line",
|
|
5034
5274
|
rows: [
|
|
5035
5275
|
{ kind: "heading", label: "Status line", id: "parts" },
|
|
5036
|
-
{ kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) =>
|
|
5037
|
-
{ kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) =>
|
|
5038
|
-
{ kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) =>
|
|
5039
|
-
{ kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) =>
|
|
5276
|
+
{ kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
|
|
5277
|
+
{ kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
|
|
5278
|
+
{ kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
|
|
5279
|
+
{ kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
|
|
5040
5280
|
{ kind: "heading", label: "Indicators", id: "indicators" },
|
|
5041
5281
|
{
|
|
5042
5282
|
kind: "toggle",
|
|
@@ -5071,12 +5311,40 @@ var ChartCell = class {
|
|
|
5071
5311
|
this.indicatorTitlesOn = visible;
|
|
5072
5312
|
this.inner?.renderer.set("indicatorTitles", visible);
|
|
5073
5313
|
this.deps.onStateDirty();
|
|
5314
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
5074
5315
|
}
|
|
5075
5316
|
/** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
|
|
5076
5317
|
setIndicatorValuesVisible(visible) {
|
|
5077
5318
|
this.indicatorValuesOn = visible;
|
|
5078
5319
|
this.inner?.renderer.set("indicatorValues", visible);
|
|
5079
5320
|
this.deps.onStateDirty();
|
|
5321
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
5322
|
+
}
|
|
5323
|
+
/** Show/hide one status-line segment (the settings dialog's Status line tab). */
|
|
5324
|
+
setStatuslinePart(part, visible) {
|
|
5325
|
+
this.statusline?.setPartVisible(part, visible);
|
|
5326
|
+
this.deps.onStatusPrefsChanged(this.id);
|
|
5327
|
+
}
|
|
5328
|
+
/** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
|
|
5329
|
+
statusPrefs() {
|
|
5330
|
+
const sl = this.statusline;
|
|
5331
|
+
return {
|
|
5332
|
+
parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
|
|
5333
|
+
indicatorTitles: this.indicatorTitlesOn,
|
|
5334
|
+
indicatorValues: this.indicatorValuesOn
|
|
5335
|
+
};
|
|
5336
|
+
}
|
|
5337
|
+
/** Converge this cell's Status line tab prefs to `prefs` — the follower half of
|
|
5338
|
+
* the workspace's style link. Idempotent: matching values change nothing, so a
|
|
5339
|
+
* propagated echo dies on its own. */
|
|
5340
|
+
applyStatusPrefs(prefs) {
|
|
5341
|
+
if (prefs.parts && this.statusline) {
|
|
5342
|
+
for (const part of Object.keys(prefs.parts)) {
|
|
5343
|
+
if (this.statusline.partVisible(part) !== prefs.parts[part]) this.statusline.setPartVisible(part, prefs.parts[part]);
|
|
5344
|
+
}
|
|
5345
|
+
}
|
|
5346
|
+
if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
|
|
5347
|
+
if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
|
|
5080
5348
|
}
|
|
5081
5349
|
/** The LIVE chart of this cell — never cache it across a layout change (the cell's
|
|
5082
5350
|
* identity is what endures; the chart dies with the cell). */
|
|
@@ -6003,7 +6271,8 @@ var MobileBar = class {
|
|
|
6003
6271
|
if (!ctx) return;
|
|
6004
6272
|
const doc = this.el.ownerDocument;
|
|
6005
6273
|
this.actionsHost.replaceChildren();
|
|
6006
|
-
|
|
6274
|
+
const builtin = new Set(TOPBAR_BUILTIN_IDS);
|
|
6275
|
+
for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left" && !builtin.has(a.id))) {
|
|
6007
6276
|
const b = doc.createElement("button");
|
|
6008
6277
|
b.className = "vela-mb-item";
|
|
6009
6278
|
b.setAttribute("aria-label", action.label);
|
|
@@ -6532,10 +6801,10 @@ var MoreDrawer = class {
|
|
|
6532
6801
|
});
|
|
6533
6802
|
actions.appendChild(b);
|
|
6534
6803
|
};
|
|
6535
|
-
action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
|
|
6536
|
-
action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
|
|
6537
|
-
action("camera", "Screenshot", true, this.opts.onScreenshot);
|
|
6538
|
-
this.drawer.body.appendChild(actions);
|
|
6804
|
+
if (this.opts.onUndo) action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
|
|
6805
|
+
if (this.opts.onRedo) action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
|
|
6806
|
+
if (this.opts.onScreenshot) action("camera", "Screenshot", true, this.opts.onScreenshot);
|
|
6807
|
+
if (actions.childElementCount > 0) this.drawer.body.appendChild(actions);
|
|
6539
6808
|
const list = doc.createElement("div");
|
|
6540
6809
|
list.className = "vela-md-list";
|
|
6541
6810
|
const current = this.opts.priceStyles().find((s) => s.id === this.opts.priceStyle());
|
|
@@ -6556,8 +6825,10 @@ var MoreDrawer = class {
|
|
|
6556
6825
|
})
|
|
6557
6826
|
);
|
|
6558
6827
|
}
|
|
6559
|
-
|
|
6560
|
-
|
|
6828
|
+
if (this.opts.alerts) {
|
|
6829
|
+
const alertCount = this.opts.alerts().length;
|
|
6830
|
+
list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
|
|
6831
|
+
}
|
|
6561
6832
|
for (const act of this.opts.actions()) {
|
|
6562
6833
|
list.appendChild(
|
|
6563
6834
|
this.row(doc, act.label, {
|
|
@@ -6638,7 +6909,7 @@ var MoreDrawer = class {
|
|
|
6638
6909
|
this.drawer.body.appendChild(list);
|
|
6639
6910
|
}
|
|
6640
6911
|
renderAlerts(doc) {
|
|
6641
|
-
const alerts = this.opts.alerts();
|
|
6912
|
+
const alerts = this.opts.alerts?.() ?? [];
|
|
6642
6913
|
if (alerts.length === 0) {
|
|
6643
6914
|
const empty = doc.createElement("div");
|
|
6644
6915
|
empty.className = "vela-md-empty";
|
|
@@ -7015,16 +7286,21 @@ var DrawingPill = class {
|
|
|
7015
7286
|
|
|
7016
7287
|
// src/workspace/context.ts
|
|
7017
7288
|
function buildContext(host) {
|
|
7018
|
-
const active = host.active();
|
|
7019
7289
|
return {
|
|
7020
7290
|
get chart() {
|
|
7021
7291
|
const cell = host.active();
|
|
7022
7292
|
if (!cell) throw new Error("VelaWorkspace has no active cell yet");
|
|
7023
7293
|
return cell.chart;
|
|
7024
7294
|
},
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7295
|
+
get symbol() {
|
|
7296
|
+
return host.active()?.symbol ?? "";
|
|
7297
|
+
},
|
|
7298
|
+
get timeframe() {
|
|
7299
|
+
return host.active()?.timeframe ?? "60";
|
|
7300
|
+
},
|
|
7301
|
+
get priceStyle() {
|
|
7302
|
+
return host.active()?.priceStyle ?? "candles";
|
|
7303
|
+
},
|
|
7028
7304
|
setSymbol: (symbol) => host.active()?.setSymbol(symbol),
|
|
7029
7305
|
setTimeframe: (tf) => host.active()?.setTimeframe(tf),
|
|
7030
7306
|
setPriceStyle: (style) => host.active()?.setPriceStyle(style),
|
|
@@ -7035,8 +7311,12 @@ function buildContext(host) {
|
|
|
7035
7311
|
addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
|
|
7036
7312
|
addNativeIndicator: (type) => host.active()?.addNative(type),
|
|
7037
7313
|
stateChanged: () => host.stateDirty(),
|
|
7038
|
-
|
|
7039
|
-
|
|
7314
|
+
get cells() {
|
|
7315
|
+
return host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe }));
|
|
7316
|
+
},
|
|
7317
|
+
get activeCellId() {
|
|
7318
|
+
return host.active()?.id ?? "";
|
|
7319
|
+
},
|
|
7040
7320
|
setActiveCell: (id) => host.setActiveCell(id)
|
|
7041
7321
|
};
|
|
7042
7322
|
}
|
|
@@ -7130,6 +7410,10 @@ var VelaWorkspace = class {
|
|
|
7130
7410
|
/** Same guard for the drawings link: the propagated mutations' own `drawing:*`
|
|
7131
7411
|
* events fire synchronously inside the propagation loop and must not fan out again. */
|
|
7132
7412
|
this.drawingSyncBusy = false;
|
|
7413
|
+
/** Same guard for the style link: a follower's `applyConfig` re-fires its
|
|
7414
|
+
* `onConfigChanged` in the same tick, and a state restore applies per-cell
|
|
7415
|
+
* configs that legitimately differ — neither must propagate. */
|
|
7416
|
+
this.styleSyncBusy = false;
|
|
7133
7417
|
/** LINKED drawings (the drawings sync): one map per synced set (cellId → that
|
|
7134
7418
|
* cell's drawing id), reachable from every member under its `cellId\0drawingId`
|
|
7135
7419
|
* key — any member finds its peers to push edits/removals onto. Survives a
|
|
@@ -7190,13 +7474,14 @@ var VelaWorkspace = class {
|
|
|
7190
7474
|
if (boot?.favorites) this.favs = [...boot.favorites];
|
|
7191
7475
|
if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
|
|
7192
7476
|
const sync = boot?.sync ?? opts.sync;
|
|
7193
|
-
for (const kind of
|
|
7194
|
-
this.applySyncSetting(kind, sync?.[kind]);
|
|
7195
|
-
}
|
|
7477
|
+
for (const kind of SYNC_KINDS) this.applySyncSetting(kind, sync?.[kind]);
|
|
7196
7478
|
this.monoLayout = opts.layout === false;
|
|
7197
7479
|
const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
|
|
7198
7480
|
this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
|
|
7199
7481
|
this.alertCap = Math.max(1, opts.alertCap ?? ALERT_CAP);
|
|
7482
|
+
this.topbarComp = resolveTopbarComposition(opts.topbar);
|
|
7483
|
+
this.indicatorsOverride = topbarActionOverride("indicators");
|
|
7484
|
+
this.screenshotOverride = topbarActionOverride("screenshot");
|
|
7200
7485
|
if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
|
|
7201
7486
|
if (boot?.ext) this.extState = { ...boot.ext };
|
|
7202
7487
|
if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
|
|
@@ -7213,6 +7498,8 @@ var VelaWorkspace = class {
|
|
|
7213
7498
|
});
|
|
7214
7499
|
this.symbolPicker = new SymbolPicker({
|
|
7215
7500
|
host: this.root,
|
|
7501
|
+
// Row icons come from each descriptor's OWNING provider (resolveSymbolIcon).
|
|
7502
|
+
iconFor: (d) => this.feed.symbolIconOf(d),
|
|
7216
7503
|
onSelect: (ticker) => this.active.setSymbol(ticker),
|
|
7217
7504
|
onOpenChange: (open) => {
|
|
7218
7505
|
if (open) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
|
|
@@ -7220,7 +7507,7 @@ var VelaWorkspace = class {
|
|
|
7220
7507
|
}
|
|
7221
7508
|
});
|
|
7222
7509
|
this.symbolPicker.setSource(() => this.feed.symbols());
|
|
7223
|
-
this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
|
|
7510
|
+
this.indicatorPicker = opts.indicatorPicker !== false && !this.indicatorsOverride && topbarHas(this.topbarComp, "indicators") ? new IndicatorPicker({
|
|
7224
7511
|
host: this.root,
|
|
7225
7512
|
library: () => this.active.libraryRows(),
|
|
7226
7513
|
onChart: () => this.active.onChartRows(),
|
|
@@ -7239,6 +7526,9 @@ var VelaWorkspace = class {
|
|
|
7239
7526
|
});
|
|
7240
7527
|
this.topbar = new Topbar(this.root, {
|
|
7241
7528
|
symbol: "",
|
|
7529
|
+
// RAW option, not the resolved lists — the bar distinguishes a host-declared
|
|
7530
|
+
// side (list is law) from a default one (an override's `order` may flow).
|
|
7531
|
+
composition: opts.topbar,
|
|
7242
7532
|
onSymbolClick: () => this.symbolPicker.open(),
|
|
7243
7533
|
...picker ? { onIndicatorsClick: () => picker.open() } : {},
|
|
7244
7534
|
onUndoClick: () => this.active.history.undo(),
|
|
@@ -7268,7 +7558,8 @@ var VelaWorkspace = class {
|
|
|
7268
7558
|
syncs: () => [
|
|
7269
7559
|
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
7270
7560
|
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
7271
|
-
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
7561
|
+
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
|
|
7562
|
+
{ id: "style", label: "Style", checked: this.syncOpts.style === true }
|
|
7272
7563
|
],
|
|
7273
7564
|
onToggleSync: (id) => {
|
|
7274
7565
|
const kind = id;
|
|
@@ -7296,7 +7587,7 @@ var VelaWorkspace = class {
|
|
|
7296
7587
|
context: () => this.context(),
|
|
7297
7588
|
changed: () => this.markStateDirty()
|
|
7298
7589
|
});
|
|
7299
|
-
this.objectTree = new ObjectTree(main);
|
|
7590
|
+
this.objectTree = new ObjectTree(main, (sym) => this.feed.symbolIcon(sym));
|
|
7300
7591
|
this.dataWindow = new DataWindow(main);
|
|
7301
7592
|
this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
|
|
7302
7593
|
this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
|
|
@@ -7368,7 +7659,9 @@ var VelaWorkspace = class {
|
|
|
7368
7659
|
timeframe: "60",
|
|
7369
7660
|
onSymbolClick: () => this.symbolPicker.open(),
|
|
7370
7661
|
onTimeframeClick: () => this.openTimeframeDrawer(),
|
|
7371
|
-
|
|
7662
|
+
// Same visibility truth as the desktop bar: composition-hidden
|
|
7663
|
+
// indicators lose their mobile stop too; an override takes it over.
|
|
7664
|
+
...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
|
|
7372
7665
|
getContext: () => this.context(),
|
|
7373
7666
|
...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
|
|
7374
7667
|
onMoreClick: () => this.openMoreDrawer(),
|
|
@@ -7550,7 +7843,7 @@ var VelaWorkspace = class {
|
|
|
7550
7843
|
this.topbar.setTimeframeFavorites(this.tfFavs);
|
|
7551
7844
|
}
|
|
7552
7845
|
this.dock.applyState(st.panels);
|
|
7553
|
-
for (const kind of
|
|
7846
|
+
for (const kind of SYNC_KINDS) this.applySyncSetting(kind, st.sync?.[kind]);
|
|
7554
7847
|
this.trackSizes.clear();
|
|
7555
7848
|
if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
|
|
7556
7849
|
const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
|
|
@@ -7561,9 +7854,14 @@ var VelaWorkspace = class {
|
|
|
7561
7854
|
for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
|
|
7562
7855
|
}
|
|
7563
7856
|
this.drawingLinks.clear();
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
this.
|
|
7857
|
+
this.styleSyncBusy = true;
|
|
7858
|
+
try {
|
|
7859
|
+
for (const [i] of this.def.cells.entries()) {
|
|
7860
|
+
const { id, ...cs } = st.charts[i];
|
|
7861
|
+
this.cellsById.get(id)?.rehydrate(cs);
|
|
7862
|
+
}
|
|
7863
|
+
} finally {
|
|
7864
|
+
this.styleSyncBusy = false;
|
|
7567
7865
|
}
|
|
7568
7866
|
if (st.timezone) this.setTimezone(st.timezone);
|
|
7569
7867
|
this.pool.clear();
|
|
@@ -7663,6 +7961,7 @@ var VelaWorkspace = class {
|
|
|
7663
7961
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
7664
7962
|
this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
|
|
7665
7963
|
const keep = new Set(this.order.slice(0, next.cells.length));
|
|
7964
|
+
const preexisting = new Set(this.cellsById.keys());
|
|
7666
7965
|
for (const [id, cell] of [...this.cellsById]) {
|
|
7667
7966
|
if (!keep.has(id) || rebuildAll) {
|
|
7668
7967
|
this.poolSet(id, cell.dehydrate());
|
|
@@ -7675,6 +7974,7 @@ var VelaWorkspace = class {
|
|
|
7675
7974
|
this.cellBackend = nextBackend;
|
|
7676
7975
|
this.applyGrid();
|
|
7677
7976
|
this.buildCells();
|
|
7977
|
+
this.alignNewCellStyles(preexisting);
|
|
7678
7978
|
this.syncCellPresentation();
|
|
7679
7979
|
this.topbar.setLayout(next.id);
|
|
7680
7980
|
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
@@ -7878,6 +8178,7 @@ var VelaWorkspace = class {
|
|
|
7878
8178
|
onMarketChanged: (id2) => this.onCellMarketChanged(id2),
|
|
7879
8179
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
7880
8180
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
8181
|
+
onStatusPrefsChanged: (id2) => this.propagateStylePrefs(id2),
|
|
7881
8182
|
onStateDirty: () => this.markStateDirty(),
|
|
7882
8183
|
manifestSettled: () => this.manifestSettled,
|
|
7883
8184
|
toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
|
|
@@ -7953,6 +8254,7 @@ var VelaWorkspace = class {
|
|
|
7953
8254
|
this.drawToolbar?.setEraserActive(mode === "eraser");
|
|
7954
8255
|
});
|
|
7955
8256
|
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
8257
|
+
chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
|
|
7956
8258
|
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
7957
8259
|
chart.renderer.onAxisLongPress((e) => {
|
|
7958
8260
|
if (this.layoutCtl.current !== "mobile") return;
|
|
@@ -7985,11 +8287,67 @@ var VelaWorkspace = class {
|
|
|
7985
8287
|
if (kind === "viewport") {
|
|
7986
8288
|
const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
|
|
7987
8289
|
if (range) this.propagateViewport(this.activeId, range);
|
|
8290
|
+
} else if (kind === "style") {
|
|
8291
|
+
this.propagateStylePrefs(this.activeId);
|
|
7988
8292
|
} else {
|
|
7989
8293
|
this.propagateMarket(this.activeId);
|
|
7990
8294
|
}
|
|
7991
8295
|
}
|
|
7992
8296
|
}
|
|
8297
|
+
/**
|
|
8298
|
+
* Align cells minted by a layout change to their style group: with the link on, a
|
|
8299
|
+
* NEW cell (fresh slot or one returning from the pool, which missed edits while
|
|
8300
|
+
* dormant) inherits the presentation of a pre-existing group peer — the active
|
|
8301
|
+
* cell when it is one — instead of sitting on its own state beside a styled
|
|
8302
|
+
* group. Propagation runs FROM the peer, so a newborn's defaults never overwrite
|
|
8303
|
+
* the group, and the equality short-circuits keep converged peers untouched.
|
|
8304
|
+
*/
|
|
8305
|
+
alignNewCellStyles(preexisting) {
|
|
8306
|
+
const setting = this.syncOpts.style;
|
|
8307
|
+
if (!setting) return;
|
|
8308
|
+
const ids = [...this.cellsById.keys()];
|
|
8309
|
+
const propagated = /* @__PURE__ */ new Set();
|
|
8310
|
+
for (const id of ids) {
|
|
8311
|
+
if (preexisting.has(id)) continue;
|
|
8312
|
+
const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
|
|
8313
|
+
if (peers.length === 0) continue;
|
|
8314
|
+
const source = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
|
|
8315
|
+
if (propagated.has(source)) continue;
|
|
8316
|
+
propagated.add(source);
|
|
8317
|
+
this.propagateStylePrefs(source);
|
|
8318
|
+
}
|
|
8319
|
+
}
|
|
8320
|
+
/**
|
|
8321
|
+
* Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
|
|
8322
|
+
* its renderer config plus its Status line tab prefs — onto its same-group
|
|
8323
|
+
* followers (the style link). Loop-safe two ways: the busy guard eats the
|
|
8324
|
+
* followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
|
|
8325
|
+
* in the same tick), and the equality short-circuits leave already-converged
|
|
8326
|
+
* followers untouched, so nothing re-emits once the group agrees.
|
|
8327
|
+
*/
|
|
8328
|
+
propagateStylePrefs(originId) {
|
|
8329
|
+
if (this.styleSyncBusy || this.destroyed) return;
|
|
8330
|
+
const targets = syncTargets(originId, this.syncOpts.style, [...this.cellsById.keys()]);
|
|
8331
|
+
if (targets.length === 0) return;
|
|
8332
|
+
const origin = this.cellsById.get(originId);
|
|
8333
|
+
if (!origin) return;
|
|
8334
|
+
const slice = styleConfigSlice(origin.chart.renderer.getConfig());
|
|
8335
|
+
const sliceJson = slice ? JSON.stringify(slice) : null;
|
|
8336
|
+
const prefs = origin.statusPrefs();
|
|
8337
|
+
this.styleSyncBusy = true;
|
|
8338
|
+
try {
|
|
8339
|
+
for (const id of targets) {
|
|
8340
|
+
const cell = this.cellsById.get(id);
|
|
8341
|
+
if (!cell) continue;
|
|
8342
|
+
if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
|
|
8343
|
+
cell.chart.renderer.applyConfig(slice);
|
|
8344
|
+
}
|
|
8345
|
+
cell.applyStatusPrefs(prefs);
|
|
8346
|
+
}
|
|
8347
|
+
} finally {
|
|
8348
|
+
this.styleSyncBusy = false;
|
|
8349
|
+
}
|
|
8350
|
+
}
|
|
7993
8351
|
/**
|
|
7994
8352
|
* Mirror an origin cell's pointer time onto its same-group followers as GHOST
|
|
7995
8353
|
* crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
|
|
@@ -8264,27 +8622,32 @@ var VelaWorkspace = class {
|
|
|
8264
8622
|
this.drawingsDrawer.open();
|
|
8265
8623
|
}
|
|
8266
8624
|
openMoreDrawer() {
|
|
8625
|
+
const has = (id) => topbarHas(this.topbarComp, id);
|
|
8267
8626
|
this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
|
|
8268
8627
|
host: this.root,
|
|
8269
|
-
onUndo: () => this.active.history.undo(),
|
|
8270
|
-
|
|
8271
|
-
onScreenshot: () => this.active.downloadScreenshot(),
|
|
8628
|
+
...has("undo-redo") ? { onUndo: () => this.active.history.undo(), onRedo: () => this.active.history.redo() } : {},
|
|
8629
|
+
...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.active.downloadScreenshot() } : {},
|
|
8272
8630
|
canUndo: () => this.active.history.canUndo,
|
|
8273
8631
|
canRedo: () => this.active.history.canRedo,
|
|
8274
8632
|
priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
|
|
8275
8633
|
priceStyle: () => this.active.priceStyle,
|
|
8276
8634
|
onPriceStyle: (id) => this.active.setPriceStyle(id),
|
|
8277
|
-
panels: () => [...this.dock.list()],
|
|
8635
|
+
panels: () => has("panels") ? [...this.dock.list()] : [],
|
|
8278
8636
|
onTogglePanel: (id) => this.dock.toggle(id),
|
|
8279
|
-
alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })),
|
|
8637
|
+
...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
|
|
8280
8638
|
// Left-aligned actions have their own bottom-bar stop — only the rest
|
|
8281
|
-
// lands in the drawer, or every left action would appear twice.
|
|
8282
|
-
actions
|
|
8639
|
+
// lands in the drawer, or every left action would appear twice. Built-in-id
|
|
8640
|
+
// actions are slot OVERRIDES: they reach the drawer through the slot's own
|
|
8641
|
+
// routed button (screenshot) or stop (indicators), never as an extra row.
|
|
8642
|
+
actions: () => {
|
|
8643
|
+
const builtin = new Set(TOPBAR_BUILTIN_IDS);
|
|
8644
|
+
return widgetActions("topbar", this.context()).filter((a) => a.align !== "left" && !builtin.has(a.id)).map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) }));
|
|
8645
|
+
},
|
|
8283
8646
|
// The desktop layout dropdown's whole surface — the grid canvas, the
|
|
8284
8647
|
// non-canvas presets and the sync switches — relocated into the kebab
|
|
8285
8648
|
// drawer (the topbar is hidden on mobile). Same reads as the topbar block;
|
|
8286
|
-
// single-chart mode
|
|
8287
|
-
layout: this.monoLayout ? void 0 : {
|
|
8649
|
+
// single-chart mode and a composition without 'layout' omit it here too.
|
|
8650
|
+
layout: this.monoLayout || !has("layout") ? void 0 : {
|
|
8288
8651
|
shape: () => layoutShape(this.def),
|
|
8289
8652
|
presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
|
|
8290
8653
|
onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
|
|
@@ -8292,7 +8655,8 @@ var VelaWorkspace = class {
|
|
|
8292
8655
|
syncs: () => [
|
|
8293
8656
|
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
8294
8657
|
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
8295
|
-
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
8658
|
+
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
|
|
8659
|
+
{ id: "style", label: "Style", checked: this.syncOpts.style === true }
|
|
8296
8660
|
],
|
|
8297
8661
|
onToggleSync: (id) => {
|
|
8298
8662
|
const kind = id;
|
|
@@ -8351,9 +8715,23 @@ var VelaWorkspace = class {
|
|
|
8351
8715
|
}
|
|
8352
8716
|
}
|
|
8353
8717
|
}
|
|
8718
|
+
/** Invoke a slot override the way its button would: fresh context, `when` respected. */
|
|
8719
|
+
runOverride(action) {
|
|
8720
|
+
const ctx = this.context();
|
|
8721
|
+
if (!action.when || action.when(ctx)) action.run(ctx);
|
|
8722
|
+
}
|
|
8354
8723
|
/** The default shortcut set — every binding acts on the ACTIVE cell. */
|
|
8355
8724
|
registerDefaultKeys() {
|
|
8356
|
-
this.
|
|
8725
|
+
if (topbarHas(this.topbarComp, "screenshot")) {
|
|
8726
|
+
const ov = this.screenshotOverride;
|
|
8727
|
+
this.keymap.register({
|
|
8728
|
+
id: "chart.screenshot",
|
|
8729
|
+
keys: "mod+alt+s",
|
|
8730
|
+
label: ov ? ov.label : "Download a chart screenshot",
|
|
8731
|
+
category: "Chart",
|
|
8732
|
+
run: ov ? () => this.runOverride(ov) : () => this.active.downloadScreenshot()
|
|
8733
|
+
});
|
|
8734
|
+
}
|
|
8357
8735
|
this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
|
|
8358
8736
|
this.keymap.register({ id: "chart.toggle-log", keys: "alt+l", label: "Toggle logarithmic scale", category: "Chart", run: () => this.active.chart.renderer.set("logScale", !this.active.chart.renderer.get("logScale")) });
|
|
8359
8737
|
this.keymap.register({
|
|
@@ -8393,7 +8771,10 @@ var VelaWorkspace = class {
|
|
|
8393
8771
|
this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
|
|
8394
8772
|
this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.active.chart.panBy(-PAN_FAST) });
|
|
8395
8773
|
this.keymap.register({ id: "view.pan-right", keys: "mod+arrowright", label: "Pan toward now", category: "Chart", run: () => this.active.chart.panBy(PAN_FAST) });
|
|
8396
|
-
|
|
8774
|
+
const indOv = this.indicatorsOverride;
|
|
8775
|
+
if (indOv && topbarHas(this.topbarComp, "indicators")) {
|
|
8776
|
+
this.keymap.register({ id: "indicators.open", keys: "/", label: indOv.label, category: "Indicators", run: () => this.runOverride(indOv) });
|
|
8777
|
+
} else if (this.indicatorPicker) {
|
|
8397
8778
|
this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => this.indicatorPicker?.open() });
|
|
8398
8779
|
}
|
|
8399
8780
|
this.keymap.register({
|