@luxalgo/vela 0.6.14 → 0.6.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +28 -28
- package/README.md +9 -2
- package/dist/{DataProvider-DhzstpQb.d.ts → DataProvider-XLdJB86x.d.ts} +1 -1
- package/dist/{DataProvider-DlMtrwqM.d.cts → DataProvider-xH2VBcFT.d.cts} +1 -1
- package/dist/{chunk-KG4YT3TI.js → chunk-6DPAYEGO.js} +8 -1
- package/dist/chunk-FE6V2YAQ.js +1 -0
- package/dist/{chunk-N2Q5EEFH.js → chunk-GIJYO4LO.js} +2596 -242
- package/dist/{chunk-F4M24ANM.js → chunk-I77F2SVE.js} +80 -4
- package/dist/{chunk-MHY7MVXH.js → chunk-MGU3USTP.js} +29 -16
- package/dist/{chunk-G77Y7LK2.js → chunk-Q3TFJNJW.js} +2 -2
- package/dist/{chunk-64TF7JAJ.js → chunk-SEOO6YG7.js} +408 -184
- package/dist/{contributions-lPojhTxI.d.cts → contributions-BEkxwA5F.d.cts} +40 -13
- package/dist/{contributions-37nni40G.d.ts → contributions-CoAX7_SF.d.ts} +40 -13
- package/dist/index.cjs +2755 -380
- package/dist/index.d.cts +20 -12
- package/dist/index.d.ts +20 -12
- package/dist/index.js +5 -5
- package/dist/{options-CYS5Wmlx.d.ts → options-bdSAtp7y.d.cts} +5 -0
- package/dist/{options-CYS5Wmlx.d.cts → options-bdSAtp7y.d.ts} +5 -0
- package/dist/{plugin-DHyaoMjW.d.ts → plugin-CNUgbRNq.d.ts} +11 -8
- package/dist/{plugin-Cwikpz1m.d.cts → plugin-Dxyq35Zf.d.cts} +11 -8
- package/dist/plugin.cjs +8 -0
- package/dist/plugin.d.cts +5 -5
- package/dist/plugin.d.ts +5 -5
- package/dist/plugin.js +3 -3
- 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/{side-panel-CT9ZwIGz.d.cts → side-panel-HF0IAzwf.d.cts} +17 -0
- package/dist/{side-panel-CT9ZwIGz.d.ts → side-panel-HF0IAzwf.d.ts} +17 -0
- package/dist/{statusline-model-D-q_OOx9.d.ts → statusline-model-CbPza-Wc.d.cts} +6 -4
- package/dist/{statusline-model-CiJm-riV.d.cts → statusline-model-D7cWkw8A.d.ts} +6 -4
- package/dist/ui.cjs +34 -15
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +2675 -300
- package/dist/vela.global.min.js +58 -52
- package/dist/widget.cjs +3296 -622
- package/dist/widget.d.cts +15 -11
- package/dist/widget.d.ts +15 -11
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +3296 -622
- package/dist/workspace.d.cts +49 -19
- package/dist/workspace.d.ts +49 -19
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
- package/dist/chunk-H26BEHF4.js +0 -7
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel, isGroupRow, defaultMemberOf, groupKeyOf, groupMembers, LEGEND_AT_TOP_ATTR, 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, legendCalloutsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, widgetAttachments, getDrawingType, inputDeltas } from './chunk-
|
|
3
|
-
import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-
|
|
4
|
-
import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-
|
|
5
|
-
import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, iconAt, SESSION_OFF, SESSION_POST, SESSION_PRE, icon, categoricalColor } from './chunk-
|
|
1
|
+
import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel, isGroupRow, defaultMemberOf, groupKeyOf, groupMembers, LEGEND_AT_TOP_ATTR, Vela, TypedEventBus, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-GIJYO4LO.js';
|
|
2
|
+
import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, legendCalloutsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, widgetAttachments, getDrawingType, inputDeltas } from './chunk-I77F2SVE.js';
|
|
3
|
+
import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-Q3TFJNJW.js';
|
|
4
|
+
import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-MGU3USTP.js';
|
|
5
|
+
import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, iconAt, SESSION_OFF, SESSION_POST, SESSION_PRE, icon, categoricalColor } from './chunk-6DPAYEGO.js';
|
|
6
6
|
import { baseOf } from './chunk-W4EJWLEO.js';
|
|
7
7
|
|
|
8
8
|
// src/widget/timeframe.ts
|
|
@@ -2784,6 +2784,8 @@ var PanelDock = class {
|
|
|
2784
2784
|
this.entries = [];
|
|
2785
2785
|
/** Widths the USER settled, by panel id — the only ones worth persisting. */
|
|
2786
2786
|
this.widths = /* @__PURE__ */ new Map();
|
|
2787
|
+
/** Floating panels the USER pinned as columns, by id — remembered for late registrations too. */
|
|
2788
|
+
this.pinned = /* @__PURE__ */ new Set();
|
|
2787
2789
|
/** A restored `open` naming a panel that has not registered yet: honored when it docks. */
|
|
2788
2790
|
this.pendingOpen = null;
|
|
2789
2791
|
this.chart = null;
|
|
@@ -2806,7 +2808,8 @@ var PanelDock = class {
|
|
|
2806
2808
|
width: desc.width,
|
|
2807
2809
|
resizable: desc.resizable,
|
|
2808
2810
|
minWidth: desc.minWidth,
|
|
2809
|
-
maxWidth: desc.maxWidth
|
|
2811
|
+
maxWidth: desc.maxWidth,
|
|
2812
|
+
overlay: desc.overlay
|
|
2810
2813
|
});
|
|
2811
2814
|
const entry = {
|
|
2812
2815
|
id: desc.id,
|
|
@@ -2854,7 +2857,8 @@ var PanelDock = class {
|
|
|
2854
2857
|
const open = this.openId;
|
|
2855
2858
|
if (open) out.open = open;
|
|
2856
2859
|
if (this.widths.size > 0) out.widths = Object.fromEntries(this.widths);
|
|
2857
|
-
|
|
2860
|
+
if (this.pinned.size > 0) out.pinned = [...this.pinned];
|
|
2861
|
+
return out.open || out.widths || out.pinned ? out : null;
|
|
2858
2862
|
}
|
|
2859
2863
|
/**
|
|
2860
2864
|
* Restore a persisted dock state. Widths apply to any panel present, and are remembered for
|
|
@@ -2862,7 +2866,8 @@ var PanelDock = class {
|
|
|
2862
2866
|
* a document that predates the dock has no `panels` field at all, so the shell never calls
|
|
2863
2867
|
* this and the default (everything closed) stands. An `open` naming a panel that has not
|
|
2864
2868
|
* registered yet is held until it docks (a plugin loaded after the restore), unless the user
|
|
2865
|
-
* opens something in the meantime.
|
|
2869
|
+
* opens something in the meantime. `pinned` is the whole list of floating panels the user
|
|
2870
|
+
* docked — its absence means none is, so every floatable panel floats again.
|
|
2866
2871
|
*/
|
|
2867
2872
|
applyState(state) {
|
|
2868
2873
|
if (!state) return;
|
|
@@ -2872,6 +2877,8 @@ var PanelDock = class {
|
|
|
2872
2877
|
this.entries.find((e) => e.id === id)?.panel.setWidth(px);
|
|
2873
2878
|
}
|
|
2874
2879
|
}
|
|
2880
|
+
this.pinned = new Set(state.pinned ?? []);
|
|
2881
|
+
for (const entry of this.entries) entry.panel.setOverlay(!this.pinned.has(entry.id));
|
|
2875
2882
|
for (const entry of this.entries) entry.panel.toggle(entry.id === state.open);
|
|
2876
2883
|
this.pendingOpen = state.open && !this.entries.some((e) => e.id === state.open) ? state.open : null;
|
|
2877
2884
|
}
|
|
@@ -2885,6 +2892,7 @@ var PanelDock = class {
|
|
|
2885
2892
|
this.entries.sort((a, b) => a.order - b.order);
|
|
2886
2893
|
const stored = this.widths.get(entry.id);
|
|
2887
2894
|
if (stored !== void 0) entry.panel.setWidth(stored);
|
|
2895
|
+
if (this.pinned.has(entry.id)) entry.panel.setOverlay(false);
|
|
2888
2896
|
entry.panel.onOpenChange = (open) => {
|
|
2889
2897
|
if (open) {
|
|
2890
2898
|
for (const other of this.entries) if (other !== entry) other.panel.toggle(false);
|
|
@@ -2898,6 +2906,11 @@ var PanelDock = class {
|
|
|
2898
2906
|
this.widths.set(entry.id, px);
|
|
2899
2907
|
this.deps.changed?.();
|
|
2900
2908
|
};
|
|
2909
|
+
entry.panel.onPlacementChange = (overlay) => {
|
|
2910
|
+
if (overlay) this.pinned.delete(entry.id);
|
|
2911
|
+
else this.pinned.add(entry.id);
|
|
2912
|
+
this.deps.changed?.();
|
|
2913
|
+
};
|
|
2901
2914
|
if (this.pendingOpen === entry.id) entry.panel.toggle(true);
|
|
2902
2915
|
if (!entry.contributed) this.publish();
|
|
2903
2916
|
}
|
|
@@ -2986,6 +2999,12 @@ function filterSymbols(list, query, limit = 100, top = TOP_TICKERS) {
|
|
|
2986
2999
|
}
|
|
2987
3000
|
var STYLE_ID6 = "vela-widget-symbolpicker";
|
|
2988
3001
|
var CSS6 = `
|
|
3002
|
+
/* The dialog body is flush (a flex column): search + market tabs are a fixed head
|
|
3003
|
+
and the result list is the ONLY scroller, in both layouts. A sticky head over a
|
|
3004
|
+
scrolling padded body is not an option \u2014 browsers pin sticky boxes to the
|
|
3005
|
+
scroller's content box, so the head lands one padding below the top and covers
|
|
3006
|
+
the first row. */
|
|
3007
|
+
.vela-sp-head { flex: none; padding: var(--vela-space-4) var(--vela-space-4) 0; }
|
|
2989
3008
|
.vela-sp-searchrow {
|
|
2990
3009
|
display: flex;
|
|
2991
3010
|
align-items: center;
|
|
@@ -3005,15 +3024,18 @@ var CSS6 = `
|
|
|
3005
3024
|
border: none;
|
|
3006
3025
|
font-size: 14px;
|
|
3007
3026
|
outline: none;
|
|
3027
|
+
text-transform: uppercase;
|
|
3008
3028
|
}
|
|
3009
|
-
.vela-sp-
|
|
3029
|
+
.vela-sp-input::placeholder { text-transform: none; }
|
|
3030
|
+
.vela-sp-tabs { display: flex; gap: 14px; margin: 12px 2px 0; border-bottom: 1px solid var(--vela-border); padding-bottom: 8px; }
|
|
3010
3031
|
/* Mobile (fullscreen dialog): the asset-class strip scrolls sideways instead of
|
|
3011
|
-
overflowing the body, and the result list stops capping itself \u2014
|
|
3012
|
-
the
|
|
3032
|
+
overflowing the body, and the result list stops capping itself \u2014 it fills the
|
|
3033
|
+
rest of the screen. The bottom inset (+ safe area) is the kit's: every mobile
|
|
3034
|
+
dialog body carries it, flush or not, so the list adds none of its own. */
|
|
3013
3035
|
[data-layout='mobile'] .vela-sp-tabs { overflow-x: auto; scrollbar-width: none; gap: 6px; }
|
|
3014
3036
|
[data-layout='mobile'] .vela-sp-tabs::-webkit-scrollbar { display: none; }
|
|
3015
3037
|
[data-layout='mobile'] .vela-sp-tab { flex: none; padding: 7px 10px; }
|
|
3016
|
-
[data-layout='mobile'] .vela-sp-list { max-height: none; }
|
|
3038
|
+
[data-layout='mobile'] .vela-sp-list { flex: 1 1 auto; max-height: none; margin-bottom: 0; }
|
|
3017
3039
|
.vela-sp-tab {
|
|
3018
3040
|
all: unset;
|
|
3019
3041
|
padding: 3px 10px;
|
|
@@ -3025,7 +3047,12 @@ var CSS6 = `
|
|
|
3025
3047
|
}
|
|
3026
3048
|
.vela-sp-tab:hover { color: var(--vela-fg); }
|
|
3027
3049
|
.vela-sp-tab[data-active] { background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
|
|
3028
|
-
.vela-sp-list {
|
|
3050
|
+
.vela-sp-list {
|
|
3051
|
+
min-height: 0;
|
|
3052
|
+
margin: var(--vela-space-2) var(--vela-space-4) var(--vela-space-4);
|
|
3053
|
+
max-height: 46vh;
|
|
3054
|
+
overflow: auto;
|
|
3055
|
+
}
|
|
3029
3056
|
.vela-sp-list::-webkit-scrollbar { width: 8px; }
|
|
3030
3057
|
.vela-sp-list::-webkit-scrollbar-thumb {
|
|
3031
3058
|
background: var(--vela-scroll);
|
|
@@ -3055,7 +3082,7 @@ var CSS6 = `
|
|
|
3055
3082
|
font-weight: 700;
|
|
3056
3083
|
}
|
|
3057
3084
|
.vela-sp-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
|
|
3058
|
-
.vela-sp-ticker { font-weight: 700; color: var(--vela-fg-bright); font-size: 14px; }
|
|
3085
|
+
.vela-sp-ticker { font-weight: 700; color: var(--vela-fg-bright); font-size: 14px; text-transform: uppercase; }
|
|
3059
3086
|
.vela-sp-desc { color: var(--vela-fg-muted); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3060
3087
|
.vela-sp-badge {
|
|
3061
3088
|
flex: none;
|
|
@@ -3118,6 +3145,8 @@ var SymbolPicker = class {
|
|
|
3118
3145
|
searchRow.append(iconEl("search", doc), this.input);
|
|
3119
3146
|
this.tabs = doc.createElement("div");
|
|
3120
3147
|
this.tabs.className = "vela-sp-tabs";
|
|
3148
|
+
const head = doc.createElement("div");
|
|
3149
|
+
head.className = "vela-sp-head";
|
|
3121
3150
|
for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
|
|
3122
3151
|
const b = doc.createElement("button");
|
|
3123
3152
|
b.className = "vela-sp-tab";
|
|
@@ -3131,6 +3160,7 @@ var SymbolPicker = class {
|
|
|
3131
3160
|
});
|
|
3132
3161
|
this.tabs.appendChild(b);
|
|
3133
3162
|
}
|
|
3163
|
+
head.append(searchRow, this.tabs);
|
|
3134
3164
|
this.list = doc.createElement("div");
|
|
3135
3165
|
this.list.className = "vela-sp-list";
|
|
3136
3166
|
this.list.addEventListener("scroll", () => {
|
|
@@ -3143,21 +3173,33 @@ var SymbolPicker = class {
|
|
|
3143
3173
|
title: "Symbol Search",
|
|
3144
3174
|
host: opts.host,
|
|
3145
3175
|
closeOnInteractOutside: true,
|
|
3146
|
-
|
|
3176
|
+
flush: true,
|
|
3177
|
+
content: (body) => body.append(head, this.list),
|
|
3147
3178
|
onOpenChange: (open) => {
|
|
3148
3179
|
if (open) {
|
|
3149
|
-
this.input.value = this.seed;
|
|
3180
|
+
this.input.value = this.seed.toUpperCase();
|
|
3150
3181
|
this.seed = "";
|
|
3151
3182
|
this.refresh();
|
|
3152
3183
|
setTimeout(() => {
|
|
3153
3184
|
this.input.focus();
|
|
3154
3185
|
this.input.setSelectionRange(this.input.value.length, this.input.value.length);
|
|
3155
3186
|
}, 0);
|
|
3187
|
+
} else {
|
|
3188
|
+
this.input.blur();
|
|
3156
3189
|
}
|
|
3157
3190
|
opts.onOpenChange?.(open);
|
|
3158
3191
|
}
|
|
3159
3192
|
});
|
|
3160
|
-
this.input.addEventListener("input", () =>
|
|
3193
|
+
this.input.addEventListener("input", () => {
|
|
3194
|
+
const next = this.input.value.toUpperCase();
|
|
3195
|
+
if (this.input.value !== next) {
|
|
3196
|
+
const start = this.input.selectionStart;
|
|
3197
|
+
const end = this.input.selectionEnd;
|
|
3198
|
+
this.input.value = next;
|
|
3199
|
+
if (start != null && end != null) this.input.setSelectionRange(start, end);
|
|
3200
|
+
}
|
|
3201
|
+
this.refresh();
|
|
3202
|
+
});
|
|
3161
3203
|
this.input.addEventListener("keydown", (e) => {
|
|
3162
3204
|
if (e.key === "ArrowDown") this.moveHighlight(1);
|
|
3163
3205
|
else if (e.key === "ArrowUp") this.moveHighlight(-1);
|
|
@@ -3889,7 +3931,11 @@ function sanitizePanels(raw) {
|
|
|
3889
3931
|
}
|
|
3890
3932
|
if (Object.keys(widths).length > 0) out.widths = widths;
|
|
3891
3933
|
}
|
|
3892
|
-
|
|
3934
|
+
if (Array.isArray(p.pinned)) {
|
|
3935
|
+
const pinned = p.pinned.filter((id) => typeof id === "string" && id !== "");
|
|
3936
|
+
if (pinned.length > 0) out.pinned = [...new Set(pinned)];
|
|
3937
|
+
}
|
|
3938
|
+
return out.open || out.widths || out.pinned ? out : null;
|
|
3893
3939
|
}
|
|
3894
3940
|
function sanitizeTrackSizes(raw) {
|
|
3895
3941
|
if (raw == null || typeof raw !== "object") return null;
|
|
@@ -4151,6 +4197,7 @@ var MARKET_LABELS = {
|
|
|
4151
4197
|
open: "Market Open",
|
|
4152
4198
|
pre: "Pre-Market",
|
|
4153
4199
|
post: "Post-Market",
|
|
4200
|
+
extended: "Extended Hours",
|
|
4154
4201
|
closed: "Market Closed",
|
|
4155
4202
|
holiday: "Market Holiday"
|
|
4156
4203
|
};
|
|
@@ -4158,6 +4205,7 @@ var MARKET_INKS = {
|
|
|
4158
4205
|
open: "var(--vela-up)",
|
|
4159
4206
|
pre: SESSION_PRE,
|
|
4160
4207
|
post: SESSION_POST,
|
|
4208
|
+
extended: SESSION_POST,
|
|
4161
4209
|
closed: SESSION_OFF,
|
|
4162
4210
|
holiday: SESSION_OFF
|
|
4163
4211
|
};
|
|
@@ -4781,118 +4829,17 @@ var ChartContextMenu = class {
|
|
|
4781
4829
|
// src/core/options.ts
|
|
4782
4830
|
var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
|
|
4783
4831
|
|
|
4784
|
-
// src/widget/market-status.ts
|
|
4785
|
-
function civilDate(ms, tz) {
|
|
4786
|
-
try {
|
|
4787
|
-
return new Intl.DateTimeFormat("en-CA", { timeZone: tz }).format(ms);
|
|
4788
|
-
} catch {
|
|
4789
|
-
return "";
|
|
4790
|
-
}
|
|
4791
|
-
}
|
|
4792
|
-
function isWeekday(ms, tz) {
|
|
4793
|
-
try {
|
|
4794
|
-
const wd = new Intl.DateTimeFormat("en-US", { timeZone: tz, weekday: "short" }).format(ms);
|
|
4795
|
-
return wd !== "Sat" && wd !== "Sun";
|
|
4796
|
-
} catch {
|
|
4797
|
-
return false;
|
|
4798
|
-
}
|
|
4799
|
-
}
|
|
4800
|
-
function deriveMarketStatus(now, w, tz) {
|
|
4801
|
-
const within = (ws) => ws.find(([s, e]) => now >= s && now < e);
|
|
4802
|
-
if (within(w.regular)) return "open";
|
|
4803
|
-
const ext = within(w.extended);
|
|
4804
|
-
if (ext) return w.regular.some(([s]) => s >= now && s < ext[1]) ? "pre" : "post";
|
|
4805
|
-
if (isWeekday(now, tz)) {
|
|
4806
|
-
const today = civilDate(now, tz);
|
|
4807
|
-
if (today !== "" && !w.extended.some(([s]) => civilDate(s, tz) === today)) return "holiday";
|
|
4808
|
-
}
|
|
4809
|
-
return "closed";
|
|
4810
|
-
}
|
|
4811
|
-
function nextStatusBoundary(now, w) {
|
|
4812
|
-
let next = null;
|
|
4813
|
-
for (const ws of [w.regular, w.extended]) {
|
|
4814
|
-
for (const [s, e] of ws) {
|
|
4815
|
-
for (const t of [s, e]) {
|
|
4816
|
-
if (t > now && (next == null || t < next)) next = t;
|
|
4817
|
-
}
|
|
4818
|
-
}
|
|
4819
|
-
}
|
|
4820
|
-
return next;
|
|
4821
|
-
}
|
|
4822
|
-
var FETCH_BACK_MS = 4 * 864e5;
|
|
4823
|
-
var FETCH_AHEAD_MS = 10 * 864e5;
|
|
4824
|
-
var MIN_TIMER_MS = 15e3;
|
|
4825
|
-
var MAX_TIMER_MS = 36e5;
|
|
4826
|
-
var RETRY_MS = 6e4;
|
|
4827
|
-
var MarketStatusTracker = class {
|
|
4828
|
-
constructor(onStatus) {
|
|
4829
|
-
this.onStatus = onStatus;
|
|
4830
|
-
/** Invalidates detached async work — bumped by every track()/stop(). */
|
|
4831
|
-
this.epoch = 0;
|
|
4832
|
-
this.timer = null;
|
|
4833
|
-
}
|
|
4834
|
-
/** (Re)bind to a chart's data surface + symbol and start evaluating. */
|
|
4835
|
-
track(data, symbol) {
|
|
4836
|
-
const my = ++this.epoch;
|
|
4837
|
-
this.clearTimer();
|
|
4838
|
-
void this.evaluate(my, data, symbol);
|
|
4839
|
-
}
|
|
4840
|
-
stop() {
|
|
4841
|
-
this.epoch += 1;
|
|
4842
|
-
this.clearTimer();
|
|
4843
|
-
}
|
|
4844
|
-
async evaluate(my, data, symbol) {
|
|
4845
|
-
const resolved = data.resolve(symbol);
|
|
4846
|
-
const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
|
|
4847
|
-
const si = await data.symbolInfo(symbol).catch(() => void 0);
|
|
4848
|
-
if (my !== this.epoch) return;
|
|
4849
|
-
const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
|
|
4850
|
-
if (!provider?.getCalendar || !hasSessions || !resolved) {
|
|
4851
|
-
this.onStatus("open");
|
|
4852
|
-
return;
|
|
4853
|
-
}
|
|
4854
|
-
const tz = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : "Etc/UTC";
|
|
4855
|
-
const now = Date.now();
|
|
4856
|
-
const range = { from: now - FETCH_BACK_MS, to: now + FETCH_AHEAD_MS };
|
|
4857
|
-
const [regular, extended] = await Promise.all([
|
|
4858
|
-
provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
|
|
4859
|
-
provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
|
|
4860
|
-
]);
|
|
4861
|
-
if (my !== this.epoch) return;
|
|
4862
|
-
if (!regular || !extended) {
|
|
4863
|
-
this.arm(my, data, symbol, RETRY_MS);
|
|
4864
|
-
return;
|
|
4865
|
-
}
|
|
4866
|
-
const w = { regular, extended };
|
|
4867
|
-
this.onStatus(deriveMarketStatus(now, w, tz));
|
|
4868
|
-
const boundary = nextStatusBoundary(now, w);
|
|
4869
|
-
const delay = Math.min(boundary != null ? boundary - now : MAX_TIMER_MS, MAX_TIMER_MS);
|
|
4870
|
-
this.arm(my, data, symbol, Math.max(delay, MIN_TIMER_MS));
|
|
4871
|
-
}
|
|
4872
|
-
arm(my, data, symbol, delay) {
|
|
4873
|
-
this.clearTimer();
|
|
4874
|
-
this.timer = setTimeout(() => {
|
|
4875
|
-
this.timer = null;
|
|
4876
|
-
if (my !== this.epoch) return;
|
|
4877
|
-
void this.evaluate(my, data, symbol);
|
|
4878
|
-
}, delay);
|
|
4879
|
-
}
|
|
4880
|
-
clearTimer() {
|
|
4881
|
-
if (this.timer != null) clearTimeout(this.timer);
|
|
4882
|
-
this.timer = null;
|
|
4883
|
-
}
|
|
4884
|
-
};
|
|
4885
|
-
|
|
4886
4832
|
// src/widget/session-shading.ts
|
|
4887
4833
|
var DAY_MS = 864e5;
|
|
4888
4834
|
var MINUTE_MS = 6e4;
|
|
4889
|
-
function parseWindow(text) {
|
|
4835
|
+
function parseWindow(text, allowOvernight = false) {
|
|
4890
4836
|
if (typeof text !== "string") return null;
|
|
4891
4837
|
const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
|
|
4892
4838
|
if (!m) return null;
|
|
4893
4839
|
const start = Number(m[1]) * 60 + Number(m[2]);
|
|
4894
4840
|
const end = Number(m[3]) * 60 + Number(m[4]);
|
|
4895
|
-
if (
|
|
4841
|
+
if (end > 1440 || start >= 1440) return null;
|
|
4842
|
+
if (start >= end && !allowOvernight) return null;
|
|
4896
4843
|
return { start, end };
|
|
4897
4844
|
}
|
|
4898
4845
|
function parseSessionSpec(si) {
|
|
@@ -4902,9 +4849,13 @@ function parseSessionSpec(si) {
|
|
|
4902
4849
|
if (!regular) return null;
|
|
4903
4850
|
const tz = si?.["timezone"];
|
|
4904
4851
|
const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
|
|
4905
|
-
const ext = parseWindow(si?.["session_extended"]);
|
|
4852
|
+
const ext = parseWindow(si?.["session_extended"], true);
|
|
4853
|
+
if (ext && ext.start >= ext.end) {
|
|
4854
|
+
if (ext.start >= regular.end && ext.end >= regular.end) return { regular, extended: ext, overnight: true, timezone };
|
|
4855
|
+
return { regular, extended: { start: 0, end: 1440 }, overnight: false, timezone };
|
|
4856
|
+
}
|
|
4906
4857
|
const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
|
|
4907
|
-
return { regular, extended, timezone };
|
|
4858
|
+
return { regular, extended, overnight: false, timezone };
|
|
4908
4859
|
}
|
|
4909
4860
|
var dtfCache = /* @__PURE__ */ new Map();
|
|
4910
4861
|
function civilFormatter(tz) {
|
|
@@ -4940,27 +4891,48 @@ function civilParts(dtf, ms) {
|
|
|
4940
4891
|
minute: Number(get("minute"))
|
|
4941
4892
|
};
|
|
4942
4893
|
}
|
|
4894
|
+
function mergeAbutting(bands) {
|
|
4895
|
+
const sorted = [...bands].sort((a, b) => a[0] - b[0]);
|
|
4896
|
+
const out = [];
|
|
4897
|
+
for (const [s, e] of sorted) {
|
|
4898
|
+
if (e <= s) continue;
|
|
4899
|
+
const last = out[out.length - 1];
|
|
4900
|
+
if (last && s <= last[1]) last[1] = Math.max(last[1], e);
|
|
4901
|
+
else out.push([s, e]);
|
|
4902
|
+
}
|
|
4903
|
+
return out;
|
|
4904
|
+
}
|
|
4943
4905
|
function expandSessionZones(spec, from, to) {
|
|
4944
4906
|
const pre = [];
|
|
4945
4907
|
const post = [];
|
|
4908
|
+
const extended = [];
|
|
4946
4909
|
const dtf = civilFormatter(spec.timezone);
|
|
4947
|
-
if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
|
|
4910
|
+
if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post, extended };
|
|
4948
4911
|
const seen = /* @__PURE__ */ new Set();
|
|
4949
4912
|
for (let cursor = from - DAY_MS; cursor < to + DAY_MS; cursor += DAY_MS) {
|
|
4950
4913
|
const civil = civilParts(dtf, cursor);
|
|
4951
4914
|
const key = civil.year * 1e4 + civil.month * 100 + civil.day;
|
|
4952
4915
|
if (!Number.isFinite(key) || seen.has(key)) continue;
|
|
4953
4916
|
seen.add(key);
|
|
4954
|
-
|
|
4917
|
+
const sunday = civil.weekday === "Sun";
|
|
4918
|
+
if (civil.weekday === "Sat" || sunday && !spec.overnight) continue;
|
|
4955
4919
|
const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
|
|
4956
4920
|
const noonGuess = naive + 720 * MINUTE_MS;
|
|
4957
4921
|
const atNoon = civilParts(dtf, noonGuess);
|
|
4958
4922
|
const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
|
|
4959
4923
|
const at = (minutes) => naive + minutes * MINUTE_MS - offset;
|
|
4924
|
+
if (spec.overnight) {
|
|
4925
|
+
if (!sunday) {
|
|
4926
|
+
extended.push([at(0), at(spec.regular.start)]);
|
|
4927
|
+
if (spec.regular.end < spec.extended.end) extended.push([at(spec.regular.end), at(spec.extended.end)]);
|
|
4928
|
+
}
|
|
4929
|
+
if (civil.weekday !== "Fri") extended.push([at(spec.extended.start), at(1440)]);
|
|
4930
|
+
continue;
|
|
4931
|
+
}
|
|
4960
4932
|
if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
|
|
4961
4933
|
if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
|
|
4962
4934
|
}
|
|
4963
|
-
return { pre, post };
|
|
4935
|
+
return { pre, post, extended: mergeAbutting(extended) };
|
|
4964
4936
|
}
|
|
4965
4937
|
var COVER_PAD_MIN_MS = 2 * DAY_MS;
|
|
4966
4938
|
var SessionShadingTracker = class {
|
|
@@ -5015,7 +4987,7 @@ var SessionShadingTracker = class {
|
|
|
5015
4987
|
return;
|
|
5016
4988
|
}
|
|
5017
4989
|
if (this.session !== "extended" || !this.intraday) {
|
|
5018
|
-
if (force) this.onZones({ pre: [], post: [] });
|
|
4990
|
+
if (force) this.onZones({ pre: [], post: [], extended: [] });
|
|
5019
4991
|
return;
|
|
5020
4992
|
}
|
|
5021
4993
|
const from = Math.min(range.from, range.to);
|
|
@@ -5029,6 +5001,112 @@ var SessionShadingTracker = class {
|
|
|
5029
5001
|
}
|
|
5030
5002
|
};
|
|
5031
5003
|
|
|
5004
|
+
// src/widget/market-status.ts
|
|
5005
|
+
function civilDate(ms, tz) {
|
|
5006
|
+
try {
|
|
5007
|
+
return new Intl.DateTimeFormat("en-CA", { timeZone: tz }).format(ms);
|
|
5008
|
+
} catch {
|
|
5009
|
+
return "";
|
|
5010
|
+
}
|
|
5011
|
+
}
|
|
5012
|
+
function isWeekday(ms, tz) {
|
|
5013
|
+
try {
|
|
5014
|
+
const wd = new Intl.DateTimeFormat("en-US", { timeZone: tz, weekday: "short" }).format(ms);
|
|
5015
|
+
return wd !== "Sat" && wd !== "Sun";
|
|
5016
|
+
} catch {
|
|
5017
|
+
return false;
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
function deriveMarketStatus(now, w, tz, overnight = false) {
|
|
5021
|
+
const within = (ws) => ws.find(([s, e]) => now >= s && now < e);
|
|
5022
|
+
if (within(w.regular)) return "open";
|
|
5023
|
+
const ext = within(w.extended);
|
|
5024
|
+
if (ext) {
|
|
5025
|
+
if (overnight) return "extended";
|
|
5026
|
+
return w.regular.some(([s]) => s >= now && s < ext[1]) ? "pre" : "post";
|
|
5027
|
+
}
|
|
5028
|
+
if (isWeekday(now, tz)) {
|
|
5029
|
+
const today = civilDate(now, tz);
|
|
5030
|
+
if (today !== "" && !w.extended.some(([s]) => civilDate(s, tz) === today)) return "holiday";
|
|
5031
|
+
}
|
|
5032
|
+
return "closed";
|
|
5033
|
+
}
|
|
5034
|
+
function nextStatusBoundary(now, w) {
|
|
5035
|
+
let next = null;
|
|
5036
|
+
for (const ws of [w.regular, w.extended]) {
|
|
5037
|
+
for (const [s, e] of ws) {
|
|
5038
|
+
for (const t of [s, e]) {
|
|
5039
|
+
if (t > now && (next == null || t < next)) next = t;
|
|
5040
|
+
}
|
|
5041
|
+
}
|
|
5042
|
+
}
|
|
5043
|
+
return next;
|
|
5044
|
+
}
|
|
5045
|
+
var FETCH_BACK_MS = 4 * 864e5;
|
|
5046
|
+
var FETCH_AHEAD_MS = 10 * 864e5;
|
|
5047
|
+
var MIN_TIMER_MS = 15e3;
|
|
5048
|
+
var MAX_TIMER_MS = 36e5;
|
|
5049
|
+
var RETRY_MS = 6e4;
|
|
5050
|
+
var MarketStatusTracker = class {
|
|
5051
|
+
constructor(onStatus) {
|
|
5052
|
+
this.onStatus = onStatus;
|
|
5053
|
+
/** Invalidates detached async work — bumped by every track()/stop(). */
|
|
5054
|
+
this.epoch = 0;
|
|
5055
|
+
this.timer = null;
|
|
5056
|
+
}
|
|
5057
|
+
/** (Re)bind to a chart's data surface + symbol and start evaluating. */
|
|
5058
|
+
track(data, symbol) {
|
|
5059
|
+
const my = ++this.epoch;
|
|
5060
|
+
this.clearTimer();
|
|
5061
|
+
void this.evaluate(my, data, symbol);
|
|
5062
|
+
}
|
|
5063
|
+
stop() {
|
|
5064
|
+
this.epoch += 1;
|
|
5065
|
+
this.clearTimer();
|
|
5066
|
+
}
|
|
5067
|
+
async evaluate(my, data, symbol) {
|
|
5068
|
+
const resolved = data.resolve(symbol);
|
|
5069
|
+
const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
|
|
5070
|
+
const si = await data.symbolInfo(symbol).catch(() => void 0);
|
|
5071
|
+
if (my !== this.epoch) return;
|
|
5072
|
+
const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
|
|
5073
|
+
if (!provider?.getCalendar || !hasSessions || !resolved) {
|
|
5074
|
+
this.onStatus("open");
|
|
5075
|
+
return;
|
|
5076
|
+
}
|
|
5077
|
+
const tz = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : "Etc/UTC";
|
|
5078
|
+
const now = Date.now();
|
|
5079
|
+
const range = { from: now - FETCH_BACK_MS, to: now + FETCH_AHEAD_MS };
|
|
5080
|
+
const [regular, extended] = await Promise.all([
|
|
5081
|
+
provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
|
|
5082
|
+
provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
|
|
5083
|
+
]);
|
|
5084
|
+
if (my !== this.epoch) return;
|
|
5085
|
+
if (!regular || !extended) {
|
|
5086
|
+
this.arm(my, data, symbol, RETRY_MS);
|
|
5087
|
+
return;
|
|
5088
|
+
}
|
|
5089
|
+
const w = { regular, extended };
|
|
5090
|
+
const overnight = parseSessionSpec(si)?.overnight === true;
|
|
5091
|
+
this.onStatus(deriveMarketStatus(now, w, tz, overnight));
|
|
5092
|
+
const boundary = nextStatusBoundary(now, w);
|
|
5093
|
+
const delay = Math.min(boundary != null ? boundary - now : MAX_TIMER_MS, MAX_TIMER_MS);
|
|
5094
|
+
this.arm(my, data, symbol, Math.max(delay, MIN_TIMER_MS));
|
|
5095
|
+
}
|
|
5096
|
+
arm(my, data, symbol, delay) {
|
|
5097
|
+
this.clearTimer();
|
|
5098
|
+
this.timer = setTimeout(() => {
|
|
5099
|
+
this.timer = null;
|
|
5100
|
+
if (my !== this.epoch) return;
|
|
5101
|
+
void this.evaluate(my, data, symbol);
|
|
5102
|
+
}, delay);
|
|
5103
|
+
}
|
|
5104
|
+
clearTimer() {
|
|
5105
|
+
if (this.timer != null) clearTimeout(this.timer);
|
|
5106
|
+
this.timer = null;
|
|
5107
|
+
}
|
|
5108
|
+
};
|
|
5109
|
+
|
|
5032
5110
|
// src/widget/glide.ts
|
|
5033
5111
|
var MIN_SPAN_MS = 6e4;
|
|
5034
5112
|
var FOLLOW = 0.25;
|
|
@@ -5394,7 +5472,10 @@ var ChartCell = class {
|
|
|
5394
5472
|
this.activeRangeId = null;
|
|
5395
5473
|
/** Latched verdict of {@link sessionAvailable} (async metadata, sticky per symbol). */
|
|
5396
5474
|
this.sessionAvailableFlag = false;
|
|
5397
|
-
/**
|
|
5475
|
+
/** Latched: the symbol's extended tape wraps midnight (an overnight roll market) —
|
|
5476
|
+
* one extended-hours shading phase instead of the pre/post split. */
|
|
5477
|
+
this.sessionOvernightFlag = false;
|
|
5478
|
+
/** Keeps the session shading on the symbol's real calendar (see {@link SessionShadingTracker}). */
|
|
5398
5479
|
this.sessionShading = new SessionShadingTracker((zones) => this.inner?.renderer.set("sessionZones", zones));
|
|
5399
5480
|
this.manifest = [];
|
|
5400
5481
|
/** A restored ledger's manifest entry NAMES, waiting for the manifest to resolve
|
|
@@ -5403,8 +5484,8 @@ var ChartCell = class {
|
|
|
5403
5484
|
/** The volume auto-add rides the cell's first candles (`load:end`); until then the
|
|
5404
5485
|
* registry can't show it and the dehydrated ledger reports the INTENT instead. */
|
|
5405
5486
|
this.volumeMayBePending = true;
|
|
5406
|
-
/** Sync mirror of the chart's
|
|
5407
|
-
*
|
|
5487
|
+
/** Sync mirror of the chart's native instances (id + type) — the removal handler
|
|
5488
|
+
* looks the removed id up here to learn which type an undo must re-add. */
|
|
5408
5489
|
this.presentNatives = [];
|
|
5409
5490
|
this.rangeBars = 0;
|
|
5410
5491
|
this.pendingRange = null;
|
|
@@ -5576,15 +5657,18 @@ var ChartCell = class {
|
|
|
5576
5657
|
redo: () => this.dropInstance(snapshot)
|
|
5577
5658
|
});
|
|
5578
5659
|
} else {
|
|
5579
|
-
const
|
|
5580
|
-
|
|
5660
|
+
const gone = this.presentNatives.find((n) => n.id === id2);
|
|
5661
|
+
if (gone) {
|
|
5662
|
+
const { type } = gone;
|
|
5663
|
+
let revived = null;
|
|
5581
5664
|
this.history.push({
|
|
5582
5665
|
undo: () => {
|
|
5583
|
-
this.inner?.addNativeIndicator(type);
|
|
5666
|
+
revived = this.inner?.addNativeIndicator(type) ?? null;
|
|
5584
5667
|
this.refreshNativeCatalog();
|
|
5585
5668
|
},
|
|
5586
5669
|
redo: () => {
|
|
5587
|
-
|
|
5670
|
+
revived?.remove();
|
|
5671
|
+
revived = null;
|
|
5588
5672
|
this.refreshNativeCatalog();
|
|
5589
5673
|
}
|
|
5590
5674
|
});
|
|
@@ -5647,8 +5731,10 @@ var ChartCell = class {
|
|
|
5647
5731
|
void chart.data.symbolInfo(symbol).then((si) => {
|
|
5648
5732
|
if (this.inner !== chart) return;
|
|
5649
5733
|
const available = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
|
|
5650
|
-
|
|
5734
|
+
const overnight = parseSessionSpec(si)?.overnight === true;
|
|
5735
|
+
if (available !== this.sessionAvailableFlag || overnight !== this.sessionOvernightFlag) {
|
|
5651
5736
|
this.sessionAvailableFlag = available;
|
|
5737
|
+
this.sessionOvernightFlag = overnight;
|
|
5652
5738
|
this.deps.onMarketChanged(this.id);
|
|
5653
5739
|
this.pushSettingsSections();
|
|
5654
5740
|
}
|
|
@@ -5682,16 +5768,41 @@ var ChartCell = class {
|
|
|
5682
5768
|
/**
|
|
5683
5769
|
* (Re)contribute this cell's settings-dialog sections: status line parts, the
|
|
5684
5770
|
* per-cell fetch depth, the watermark toggle, and — only while the cell's symbol
|
|
5685
|
-
* HAS sessions — the Trading session group (RTH/ETH switch + the
|
|
5686
|
-
*
|
|
5687
|
-
*
|
|
5688
|
-
* a
|
|
5771
|
+
* HAS sessions — the Trading session group (RTH/ETH switch + the session shading
|
|
5772
|
+
* colors: pre/post-market on day-split tapes, one extended-hours swatch on
|
|
5773
|
+
* overnight roll markets) inside the Symbol tab. Bars/watermark/titles are
|
|
5774
|
+
* persistable cell state; a depth-only reload is silent, so mark dirty here.
|
|
5775
|
+
* Re-run whenever a gate changes (the dialog reads the sections on open).
|
|
5689
5776
|
*/
|
|
5690
5777
|
pushSettingsSections() {
|
|
5691
5778
|
const chart = this.inner;
|
|
5692
5779
|
if (!chart) return;
|
|
5693
5780
|
const rth = "Regular hours (RTH)";
|
|
5694
5781
|
const eth = "Extended hours (ETH)";
|
|
5782
|
+
const shadeRows = this.sessionOvernightFlag ? [
|
|
5783
|
+
{
|
|
5784
|
+
kind: "color",
|
|
5785
|
+
label: "Extended hours",
|
|
5786
|
+
id: "extended-color",
|
|
5787
|
+
get: () => this.sessionShadeColor("extendedColor"),
|
|
5788
|
+
set: (v) => this.setSessionShadeColor("extendedColor", v)
|
|
5789
|
+
}
|
|
5790
|
+
] : [
|
|
5791
|
+
{
|
|
5792
|
+
kind: "color",
|
|
5793
|
+
label: "Pre-market",
|
|
5794
|
+
id: "premarket-color",
|
|
5795
|
+
get: () => this.sessionShadeColor("premarketColor"),
|
|
5796
|
+
set: (v) => this.setSessionShadeColor("premarketColor", v)
|
|
5797
|
+
},
|
|
5798
|
+
{
|
|
5799
|
+
kind: "color",
|
|
5800
|
+
label: "Post-market",
|
|
5801
|
+
id: "postmarket-color",
|
|
5802
|
+
get: () => this.sessionShadeColor("postmarketColor"),
|
|
5803
|
+
set: (v) => this.setSessionShadeColor("postmarketColor", v)
|
|
5804
|
+
}
|
|
5805
|
+
];
|
|
5695
5806
|
const sessionSection = {
|
|
5696
5807
|
title: "Trading session",
|
|
5697
5808
|
id: "trading-session",
|
|
@@ -5705,20 +5816,7 @@ var ChartCell = class {
|
|
|
5705
5816
|
get: () => this.session === "extended" ? eth : rth,
|
|
5706
5817
|
set: (v) => this.setSession(v === eth ? "extended" : "regular")
|
|
5707
5818
|
},
|
|
5708
|
-
|
|
5709
|
-
kind: "color",
|
|
5710
|
-
label: "Pre-market",
|
|
5711
|
-
id: "premarket-color",
|
|
5712
|
-
get: () => this.sessionShadeColor("premarketColor"),
|
|
5713
|
-
set: (v) => this.setSessionShadeColor("premarketColor", v)
|
|
5714
|
-
},
|
|
5715
|
-
{
|
|
5716
|
-
kind: "color",
|
|
5717
|
-
label: "Post-market",
|
|
5718
|
-
id: "postmarket-color",
|
|
5719
|
-
get: () => this.sessionShadeColor("postmarketColor"),
|
|
5720
|
-
set: (v) => this.setSessionShadeColor("postmarketColor", v)
|
|
5721
|
-
}
|
|
5819
|
+
...shadeRows
|
|
5722
5820
|
]
|
|
5723
5821
|
};
|
|
5724
5822
|
const advanced = {
|
|
@@ -5875,9 +5973,9 @@ var ChartCell = class {
|
|
|
5875
5973
|
const live = this.inner?.renderer.get("priceStyle");
|
|
5876
5974
|
return typeof live === "string" ? live : this.state.priceStyle ?? "candles";
|
|
5877
5975
|
}
|
|
5878
|
-
/** Manifest instances +
|
|
5976
|
+
/** Manifest instances + native instances — the topbar indicator count. */
|
|
5879
5977
|
get indicatorCount() {
|
|
5880
|
-
return this.instances.length + this.
|
|
5978
|
+
return this.instances.length + (this.inner ? this.nativeHandles().length : this.presentNatives.length);
|
|
5881
5979
|
}
|
|
5882
5980
|
/** Switch this cell's market in place (the chart instance survives). The projection
|
|
5883
5981
|
* is OPTIMISTIC — labels and chrome show the pick before the bars load; it follows
|
|
@@ -5962,6 +6060,10 @@ var ChartCell = class {
|
|
|
5962
6060
|
this.deps.activate(this.id);
|
|
5963
6061
|
this.inner?.renderer.focus();
|
|
5964
6062
|
}
|
|
6063
|
+
/** Raster of this cell's chart (same pixels as the PNG download), or null. */
|
|
6064
|
+
screenshotCanvas() {
|
|
6065
|
+
return this.inner?.renderer.screenshotCanvas() ?? null;
|
|
6066
|
+
}
|
|
5965
6067
|
/** Download this cell's chart as a PNG (named after its market). */
|
|
5966
6068
|
downloadScreenshot() {
|
|
5967
6069
|
const url = this.inner?.renderer.screenshot();
|
|
@@ -6005,13 +6107,15 @@ var ChartCell = class {
|
|
|
6005
6107
|
if (!chart) return;
|
|
6006
6108
|
this.volumeIntent = led.natives.includes("volume");
|
|
6007
6109
|
this.history.silently(() => {
|
|
6008
|
-
const
|
|
6009
|
-
for (const type of led.natives)
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
if (
|
|
6110
|
+
const owed = /* @__PURE__ */ new Map();
|
|
6111
|
+
for (const type of led.natives) owed.set(type, (owed.get(type) ?? 0) + 1);
|
|
6112
|
+
for (const h of this.nativeHandles()) {
|
|
6113
|
+
const type = h.nativeType;
|
|
6114
|
+
const n = owed.get(type) ?? 0;
|
|
6115
|
+
if (n > 0) owed.set(type, n - 1);
|
|
6116
|
+
else h.remove();
|
|
6014
6117
|
}
|
|
6118
|
+
for (const [type, n] of owed) for (let i = 0; i < n; i++) chart.addNativeIndicator(type);
|
|
6015
6119
|
for (const it of [...this.instances]) this.dropInstance(it);
|
|
6016
6120
|
if (this.manifest.length > 0) {
|
|
6017
6121
|
for (const item of led.manifest) {
|
|
@@ -6029,34 +6133,42 @@ var ChartCell = class {
|
|
|
6029
6133
|
this.syncPresentNatives();
|
|
6030
6134
|
this.refreshNativeCatalog();
|
|
6031
6135
|
}
|
|
6032
|
-
/** The picker
|
|
6136
|
+
/** The supported natives in picker order: A→Z by title — registration order follows
|
|
6137
|
+
* the catalog's families, which is meaningless to the reader. This is the library
|
|
6138
|
+
* index space the picker hands back, so `libraryRows` and `addFromLibrary` MUST both
|
|
6139
|
+
* read it — indexing the unsorted catalog on add would land on a different study. */
|
|
6140
|
+
supportedNatives() {
|
|
6141
|
+
return this.nativeCatalog.filter((n) => n.supported).sort((a, b) => a.title.localeCompare(b.title, "en", { sensitivity: "base" }));
|
|
6142
|
+
}
|
|
6143
|
+
/** The picker's library rows: supported natives first (see {@link supportedNatives}),
|
|
6144
|
+
* then the manifest in the host's own order. */
|
|
6033
6145
|
libraryRows() {
|
|
6034
6146
|
return [
|
|
6035
|
-
...this.
|
|
6147
|
+
...this.supportedNatives().map((n) => ({ name: n.title, category: "Vela", native: true, nativeType: n.type, beta: n.beta })),
|
|
6036
6148
|
...this.manifest.map((e) => ({ name: e.name, language: e.language, category: e.category }))
|
|
6037
6149
|
];
|
|
6038
6150
|
}
|
|
6039
|
-
/** The picker's on-chart rows:
|
|
6151
|
+
/** The picker's on-chart rows: native instances first, then live script instances. */
|
|
6040
6152
|
onChartRows() {
|
|
6041
6153
|
return [
|
|
6042
|
-
...this.
|
|
6154
|
+
...this.nativeHandles().map((h) => ({ name: h.title, native: true, nativeType: h.nativeType })),
|
|
6043
6155
|
...this.instances.map((it) => ({ name: it.entry.name, language: it.entry.language }))
|
|
6044
6156
|
];
|
|
6045
6157
|
}
|
|
6046
6158
|
/** Add by picker LIBRARY index (natives precede the manifest — mirrors libraryRows). */
|
|
6047
6159
|
addFromLibrary(index) {
|
|
6048
|
-
const natives = this.
|
|
6160
|
+
const natives = this.supportedNatives();
|
|
6049
6161
|
if (index < natives.length) this.addNative(natives[index].type);
|
|
6050
6162
|
else {
|
|
6051
6163
|
const entry = this.manifest[index - natives.length];
|
|
6052
6164
|
if (entry) this.addManifestInstance(entry);
|
|
6053
6165
|
}
|
|
6054
6166
|
}
|
|
6055
|
-
/** Remove by picker ON-CHART index (
|
|
6167
|
+
/** Remove by picker ON-CHART index (native instances precede script instances — mirrors onChartRows). */
|
|
6056
6168
|
removeFromChart(index) {
|
|
6057
|
-
const
|
|
6058
|
-
if (index <
|
|
6059
|
-
else this.removeInstance(index -
|
|
6169
|
+
const natives = this.nativeHandles();
|
|
6170
|
+
if (index < natives.length) this.removeNative(natives[index]);
|
|
6171
|
+
else this.removeInstance(index - natives.length);
|
|
6060
6172
|
}
|
|
6061
6173
|
/**
|
|
6062
6174
|
* Add a script indicator through the PUBLIC seam (`ctx.addIndicator`) — same undo/
|
|
@@ -6116,30 +6228,42 @@ var ChartCell = class {
|
|
|
6116
6228
|
it.handle = null;
|
|
6117
6229
|
this.deps.onIndicatorsChanged(this.id);
|
|
6118
6230
|
}
|
|
6119
|
-
/** Add a native indicator
|
|
6231
|
+
/** Add a native indicator. A multi-instance type gets a fresh instance every time; a
|
|
6232
|
+
* single-instance type already on the chart hands back its existing one — nothing
|
|
6233
|
+
* changed, so nothing enters the undo timeline. */
|
|
6120
6234
|
addNative(type) {
|
|
6121
|
-
this.inner
|
|
6235
|
+
const chart = this.inner;
|
|
6236
|
+
if (!chart) return;
|
|
6237
|
+
const before = new Set(chart.indicators().map((h) => h.id));
|
|
6238
|
+
let added = chart.addNativeIndicator(type);
|
|
6122
6239
|
this.syncPresentNatives();
|
|
6123
6240
|
this.refreshNativeCatalog();
|
|
6241
|
+
if (before.has(added.id)) return;
|
|
6124
6242
|
this.history.push({
|
|
6125
6243
|
undo: () => {
|
|
6126
|
-
|
|
6244
|
+
added?.remove();
|
|
6245
|
+
added = null;
|
|
6127
6246
|
this.refreshNativeCatalog();
|
|
6128
6247
|
},
|
|
6129
6248
|
redo: () => {
|
|
6130
|
-
this.inner?.addNativeIndicator(type);
|
|
6249
|
+
added = this.inner?.addNativeIndicator(type) ?? null;
|
|
6131
6250
|
this.refreshNativeCatalog();
|
|
6132
6251
|
}
|
|
6133
6252
|
});
|
|
6134
6253
|
}
|
|
6135
|
-
removeNative(
|
|
6136
|
-
|
|
6254
|
+
removeNative(handle) {
|
|
6255
|
+
handle.remove();
|
|
6137
6256
|
this.refreshNativeCatalog();
|
|
6138
6257
|
}
|
|
6139
|
-
/**
|
|
6140
|
-
*
|
|
6258
|
+
/** The chart's native instances, insertion order — the picker's on-chart rows and
|
|
6259
|
+
* the removal index space (script instances follow them). */
|
|
6260
|
+
nativeHandles() {
|
|
6261
|
+
return this.inner?.indicators().filter((h) => h.nativeType !== void 0) ?? [];
|
|
6262
|
+
}
|
|
6263
|
+
/** Sync mirror of the chart's native instances — the removal handler looks the
|
|
6264
|
+
* removed id up here (the registry has already forgotten it) to record its type. */
|
|
6141
6265
|
syncPresentNatives() {
|
|
6142
|
-
this.presentNatives = this.
|
|
6266
|
+
this.presentNatives = this.nativeHandles().map((h) => ({ id: h.id, type: h.nativeType }));
|
|
6143
6267
|
}
|
|
6144
6268
|
/** Refresh the native catalog (supported/present flags) for this cell's market. */
|
|
6145
6269
|
refreshNativeCatalog() {
|
|
@@ -7819,6 +7943,54 @@ function buildContext(host) {
|
|
|
7819
7943
|
};
|
|
7820
7944
|
}
|
|
7821
7945
|
|
|
7946
|
+
// src/workspace/screenshot.ts
|
|
7947
|
+
function destRect(tile, dpr) {
|
|
7948
|
+
const dx = Math.round(tile.x * dpr);
|
|
7949
|
+
const dy = Math.round(tile.y * dpr);
|
|
7950
|
+
return {
|
|
7951
|
+
dx,
|
|
7952
|
+
dy,
|
|
7953
|
+
dw: Math.round((tile.x + tile.width) * dpr) - dx,
|
|
7954
|
+
dh: Math.round((tile.y + tile.height) * dpr) - dy
|
|
7955
|
+
};
|
|
7956
|
+
}
|
|
7957
|
+
function tilesFromCellRects(grid, cells) {
|
|
7958
|
+
if (grid.width <= 0 || grid.height <= 0) return [];
|
|
7959
|
+
const tiles = [];
|
|
7960
|
+
for (const c of cells) {
|
|
7961
|
+
if (c.hidden || c.width <= 0 || c.height <= 0) continue;
|
|
7962
|
+
tiles.push({ x: c.left - grid.left, y: c.top - grid.top, width: c.width, height: c.height });
|
|
7963
|
+
}
|
|
7964
|
+
return tiles;
|
|
7965
|
+
}
|
|
7966
|
+
function compositeLayoutScreenshot(doc, frame, tiles) {
|
|
7967
|
+
if (frame.width <= 0 || frame.height <= 0 || tiles.length === 0) return null;
|
|
7968
|
+
const dpr = frame.dpr > 0 ? frame.dpr : 1;
|
|
7969
|
+
const out = doc.createElement("canvas");
|
|
7970
|
+
out.width = Math.max(1, Math.round(frame.width * dpr));
|
|
7971
|
+
out.height = Math.max(1, Math.round(frame.height * dpr));
|
|
7972
|
+
const ctx = out.getContext("2d");
|
|
7973
|
+
if (!ctx) return null;
|
|
7974
|
+
ctx.fillStyle = frame.gapColor;
|
|
7975
|
+
ctx.fillRect(0, 0, out.width, out.height);
|
|
7976
|
+
let painted = 0;
|
|
7977
|
+
for (const tile of tiles) {
|
|
7978
|
+
if (tile.width <= 0 || tile.height <= 0) continue;
|
|
7979
|
+
const { dx, dy, dw, dh } = destRect(tile, dpr);
|
|
7980
|
+
if (dw <= 0 || dh <= 0) continue;
|
|
7981
|
+
ctx.drawImage(tile.source, dx, dy, dw, dh);
|
|
7982
|
+
painted += 1;
|
|
7983
|
+
}
|
|
7984
|
+
if (painted === 0) return null;
|
|
7985
|
+
return out.toDataURL("image/png");
|
|
7986
|
+
}
|
|
7987
|
+
function triggerPngDownload(doc, url, filename) {
|
|
7988
|
+
const a = doc.createElement("a");
|
|
7989
|
+
a.href = url;
|
|
7990
|
+
a.download = filename;
|
|
7991
|
+
a.click();
|
|
7992
|
+
}
|
|
7993
|
+
|
|
7822
7994
|
// src/workspace/VelaWorkspace.ts
|
|
7823
7995
|
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "30", "60", "240", "D", "W", "M"];
|
|
7824
7996
|
var GAP_PX = 2;
|
|
@@ -8049,7 +8221,7 @@ var VelaWorkspace = class {
|
|
|
8049
8221
|
...picker ? { onIndicatorsClick: () => picker.open() } : {},
|
|
8050
8222
|
onUndoClick: () => this.active.history.undo(),
|
|
8051
8223
|
onRedoClick: () => this.active.history.redo(),
|
|
8052
|
-
onScreenshotClick: () => this.
|
|
8224
|
+
onScreenshotClick: () => this.downloadScreenshot(),
|
|
8053
8225
|
onAlertsClick: (anchor) => this.openAlertsMenu(anchor),
|
|
8054
8226
|
timeframe: "60",
|
|
8055
8227
|
timeframes: opts.timeframes ?? DEFAULT_TIMEFRAMES,
|
|
@@ -8108,6 +8280,7 @@ var VelaWorkspace = class {
|
|
|
8108
8280
|
this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
|
|
8109
8281
|
this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
|
|
8110
8282
|
this.dock.refresh();
|
|
8283
|
+
if (boot?.panels) this.dock.applyState(boot.panels);
|
|
8111
8284
|
this.root.appendChild(main);
|
|
8112
8285
|
this.toastHost = new Toast(this.gridEl);
|
|
8113
8286
|
const attribution = rendererDefaults().attribution;
|
|
@@ -8119,6 +8292,7 @@ var VelaWorkspace = class {
|
|
|
8119
8292
|
bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H3}px) + 10px)`,
|
|
8120
8293
|
zIndex: "11"
|
|
8121
8294
|
});
|
|
8295
|
+
mark.dataset.velaScreenshot = "1";
|
|
8122
8296
|
this.gridEl.appendChild(mark);
|
|
8123
8297
|
this.attributionMark = mark;
|
|
8124
8298
|
}
|
|
@@ -8255,6 +8429,56 @@ var VelaWorkspace = class {
|
|
|
8255
8429
|
get chart() {
|
|
8256
8430
|
return this.active.chart;
|
|
8257
8431
|
}
|
|
8432
|
+
/**
|
|
8433
|
+
* PNG data URL of the visible layout: every live cell in its grid slot, or the
|
|
8434
|
+
* maximized cell alone. Same pixels the screenshot button downloads. A single
|
|
8435
|
+
* visible cell returns that cell's own chart export (the one-chart case).
|
|
8436
|
+
*/
|
|
8437
|
+
screenshot() {
|
|
8438
|
+
const cells = this.shotCells();
|
|
8439
|
+
if (cells.length === 0) return null;
|
|
8440
|
+
if (cells.length === 1) return cells[0].chart.renderer.screenshot();
|
|
8441
|
+
return this.compositeLayoutShot(cells);
|
|
8442
|
+
}
|
|
8443
|
+
/** Download {@link screenshot} as a PNG. A multi-cell layout is named
|
|
8444
|
+
* `vela-layout.png`; one visible cell keeps `${symbol}-${timeframe}.png`. */
|
|
8445
|
+
downloadScreenshot() {
|
|
8446
|
+
const cells = this.shotCells();
|
|
8447
|
+
if (cells.length <= 1) {
|
|
8448
|
+
(cells[0] ?? this.active).downloadScreenshot();
|
|
8449
|
+
return;
|
|
8450
|
+
}
|
|
8451
|
+
const url = this.compositeLayoutShot(cells);
|
|
8452
|
+
if (!url) return;
|
|
8453
|
+
triggerPngDownload(this.root.ownerDocument, url, "vela-layout.png");
|
|
8454
|
+
}
|
|
8455
|
+
/** Live cells the screenshot should include — maximized siblings are `hidden`. */
|
|
8456
|
+
shotCells() {
|
|
8457
|
+
return this.cells().filter((c) => c.host.style.visibility !== "hidden");
|
|
8458
|
+
}
|
|
8459
|
+
/** Place every cell's raster onto one canvas the size of the grid. */
|
|
8460
|
+
compositeLayoutShot(cells) {
|
|
8461
|
+
const gridEl = this.gridEl;
|
|
8462
|
+
const grid = gridEl.getBoundingClientRect();
|
|
8463
|
+
const win = gridEl.ownerDocument.defaultView;
|
|
8464
|
+
const dpr = win?.devicePixelRatio ?? 1;
|
|
8465
|
+
const gapColor = win ? win.getComputedStyle(gridEl).backgroundColor : "";
|
|
8466
|
+
const tiles = [];
|
|
8467
|
+
for (const cell of cells) {
|
|
8468
|
+
const source = cell.screenshotCanvas();
|
|
8469
|
+
if (!source) continue;
|
|
8470
|
+
const r = cell.host.getBoundingClientRect();
|
|
8471
|
+
const place = tilesFromCellRects(grid, [{ left: r.left, top: r.top, width: r.width, height: r.height }])[0];
|
|
8472
|
+
if (!place) continue;
|
|
8473
|
+
tiles.push({ source, ...place });
|
|
8474
|
+
}
|
|
8475
|
+
return compositeLayoutScreenshot(gridEl.ownerDocument, {
|
|
8476
|
+
width: grid.width,
|
|
8477
|
+
height: grid.height,
|
|
8478
|
+
dpr,
|
|
8479
|
+
gapColor: gapColor || "#000000"
|
|
8480
|
+
}, tiles);
|
|
8481
|
+
}
|
|
8258
8482
|
setActiveCell(id) {
|
|
8259
8483
|
if (id === this.activeId || this.destroyed) return;
|
|
8260
8484
|
const prev = this.activeId;
|
|
@@ -9301,7 +9525,7 @@ var VelaWorkspace = class {
|
|
|
9301
9525
|
this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
|
|
9302
9526
|
host: this.root,
|
|
9303
9527
|
...has("undo-redo") ? { onUndo: () => this.active.history.undo(), onRedo: () => this.active.history.redo() } : {},
|
|
9304
|
-
...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.
|
|
9528
|
+
...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.downloadScreenshot() } : {},
|
|
9305
9529
|
canUndo: () => this.active.history.canUndo,
|
|
9306
9530
|
canRedo: () => this.active.history.canRedo,
|
|
9307
9531
|
priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
|
|
@@ -9402,9 +9626,9 @@ var VelaWorkspace = class {
|
|
|
9402
9626
|
this.keymap.register({
|
|
9403
9627
|
id: "chart.screenshot",
|
|
9404
9628
|
keys: "mod+alt+s",
|
|
9405
|
-
label: ov ? ov.label : "Download a
|
|
9629
|
+
label: ov ? ov.label : "Download a screenshot of the layout",
|
|
9406
9630
|
category: "Chart",
|
|
9407
|
-
run: ov ? () => this.runOverride(ov) : () => this.
|
|
9631
|
+
run: ov ? () => this.runOverride(ov) : () => this.downloadScreenshot()
|
|
9408
9632
|
});
|
|
9409
9633
|
}
|
|
9410
9634
|
this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
|