@luxalgo/vela 0.6.13 → 0.6.15
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 +1 -1
- 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-F4M24ANM.js → chunk-32A7KHFM.js} +73 -3
- package/dist/{chunk-KG4YT3TI.js → chunk-6DPAYEGO.js} +8 -1
- package/dist/{chunk-G52XAKZY.js → chunk-7C7NWEXH.js} +345 -148
- package/dist/{chunk-JKGA36ZM.js → chunk-J5QIR2Z3.js} +165 -130
- package/dist/{chunk-MHY7MVXH.js → chunk-MWDNPGU3.js} +7 -1
- package/dist/{chunk-G77Y7LK2.js → chunk-MWS4CUJL.js} +2 -2
- package/dist/{contributions-37nni40G.d.ts → contributions-h5BzEiV2.d.ts} +13 -2
- package/dist/{contributions-lPojhTxI.d.cts → contributions-x7iamlnd.d.cts} +13 -2
- package/dist/index.cjs +176 -127
- package/dist/index.d.cts +20 -12
- package/dist/index.d.ts +20 -12
- package/dist/index.js +4 -4
- 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-Cwikpz1m.d.cts → plugin-2sJcf5mk.d.cts} +11 -8
- package/dist/{plugin-DHyaoMjW.d.ts → plugin-BLaNPh55.d.ts} +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 +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{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--tH0VWCy.d.cts} +6 -4
- package/dist/{statusline-model-CiJm-riV.d.cts → statusline-model-CLpaO3Dl.d.ts} +6 -4
- package/dist/ui.cjs +12 -0
- 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 +176 -127
- package/dist/vela.global.min.js +52 -46
- package/dist/widget.cjs +587 -272
- package/dist/widget.d.cts +15 -11
- package/dist/widget.d.ts +15 -11
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +587 -272
- package/dist/workspace.d.cts +29 -10
- package/dist/workspace.d.ts +29 -10
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel, 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-J5QIR2Z3.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-32A7KHFM.js';
|
|
3
|
+
import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-MWS4CUJL.js';
|
|
4
|
+
import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-MWDNPGU3.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,17 @@ function filterSymbols(list, query, limit = 100, top = TOP_TICKERS) {
|
|
|
2986
2999
|
}
|
|
2987
3000
|
var STYLE_ID6 = "vela-widget-symbolpicker";
|
|
2988
3001
|
var CSS6 = `
|
|
3002
|
+
/* Search + market tabs stay pinned while the result list scrolls underneath
|
|
3003
|
+
(mobile fullscreen: the dialog body is the scroller). Negative margin eats
|
|
3004
|
+
the body's padding so scrolled rows cannot peek around the island. */
|
|
3005
|
+
.vela-sp-sticky {
|
|
3006
|
+
position: sticky;
|
|
3007
|
+
top: 0;
|
|
3008
|
+
z-index: 1;
|
|
3009
|
+
background: var(--vela-surface);
|
|
3010
|
+
margin: calc(-1 * var(--vela-space-4)) calc(-1 * var(--vela-space-4)) 0;
|
|
3011
|
+
padding: var(--vela-space-4) var(--vela-space-4) 0;
|
|
3012
|
+
}
|
|
2989
3013
|
.vela-sp-searchrow {
|
|
2990
3014
|
display: flex;
|
|
2991
3015
|
align-items: center;
|
|
@@ -3005,8 +3029,10 @@ var CSS6 = `
|
|
|
3005
3029
|
border: none;
|
|
3006
3030
|
font-size: 14px;
|
|
3007
3031
|
outline: none;
|
|
3032
|
+
text-transform: uppercase;
|
|
3008
3033
|
}
|
|
3009
|
-
.vela-sp-
|
|
3034
|
+
.vela-sp-input::placeholder { text-transform: none; }
|
|
3035
|
+
.vela-sp-tabs { display: flex; gap: 14px; margin: 12px 2px 0; border-bottom: 1px solid var(--vela-border); padding-bottom: 8px; }
|
|
3010
3036
|
/* Mobile (fullscreen dialog): the asset-class strip scrolls sideways instead of
|
|
3011
3037
|
overflowing the body, and the result list stops capping itself \u2014 the body owns
|
|
3012
3038
|
the scrolling in the fullscreen presentation. */
|
|
@@ -3055,7 +3081,7 @@ var CSS6 = `
|
|
|
3055
3081
|
font-weight: 700;
|
|
3056
3082
|
}
|
|
3057
3083
|
.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; }
|
|
3084
|
+
.vela-sp-ticker { font-weight: 700; color: var(--vela-fg-bright); font-size: 14px; text-transform: uppercase; }
|
|
3059
3085
|
.vela-sp-desc { color: var(--vela-fg-muted); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3060
3086
|
.vela-sp-badge {
|
|
3061
3087
|
flex: none;
|
|
@@ -3118,6 +3144,8 @@ var SymbolPicker = class {
|
|
|
3118
3144
|
searchRow.append(iconEl("search", doc), this.input);
|
|
3119
3145
|
this.tabs = doc.createElement("div");
|
|
3120
3146
|
this.tabs.className = "vela-sp-tabs";
|
|
3147
|
+
const sticky = doc.createElement("div");
|
|
3148
|
+
sticky.className = "vela-sp-sticky";
|
|
3121
3149
|
for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
|
|
3122
3150
|
const b = doc.createElement("button");
|
|
3123
3151
|
b.className = "vela-sp-tab";
|
|
@@ -3131,6 +3159,7 @@ var SymbolPicker = class {
|
|
|
3131
3159
|
});
|
|
3132
3160
|
this.tabs.appendChild(b);
|
|
3133
3161
|
}
|
|
3162
|
+
sticky.append(searchRow, this.tabs);
|
|
3134
3163
|
this.list = doc.createElement("div");
|
|
3135
3164
|
this.list.className = "vela-sp-list";
|
|
3136
3165
|
this.list.addEventListener("scroll", () => {
|
|
@@ -3143,21 +3172,32 @@ var SymbolPicker = class {
|
|
|
3143
3172
|
title: "Symbol Search",
|
|
3144
3173
|
host: opts.host,
|
|
3145
3174
|
closeOnInteractOutside: true,
|
|
3146
|
-
content: (body) => body.append(
|
|
3175
|
+
content: (body) => body.append(sticky, this.list),
|
|
3147
3176
|
onOpenChange: (open) => {
|
|
3148
3177
|
if (open) {
|
|
3149
|
-
this.input.value = this.seed;
|
|
3178
|
+
this.input.value = this.seed.toUpperCase();
|
|
3150
3179
|
this.seed = "";
|
|
3151
3180
|
this.refresh();
|
|
3152
3181
|
setTimeout(() => {
|
|
3153
3182
|
this.input.focus();
|
|
3154
3183
|
this.input.setSelectionRange(this.input.value.length, this.input.value.length);
|
|
3155
3184
|
}, 0);
|
|
3185
|
+
} else {
|
|
3186
|
+
this.input.blur();
|
|
3156
3187
|
}
|
|
3157
3188
|
opts.onOpenChange?.(open);
|
|
3158
3189
|
}
|
|
3159
3190
|
});
|
|
3160
|
-
this.input.addEventListener("input", () =>
|
|
3191
|
+
this.input.addEventListener("input", () => {
|
|
3192
|
+
const next = this.input.value.toUpperCase();
|
|
3193
|
+
if (this.input.value !== next) {
|
|
3194
|
+
const start = this.input.selectionStart;
|
|
3195
|
+
const end = this.input.selectionEnd;
|
|
3196
|
+
this.input.value = next;
|
|
3197
|
+
if (start != null && end != null) this.input.setSelectionRange(start, end);
|
|
3198
|
+
}
|
|
3199
|
+
this.refresh();
|
|
3200
|
+
});
|
|
3161
3201
|
this.input.addEventListener("keydown", (e) => {
|
|
3162
3202
|
if (e.key === "ArrowDown") this.moveHighlight(1);
|
|
3163
3203
|
else if (e.key === "ArrowUp") this.moveHighlight(-1);
|
|
@@ -3889,7 +3929,11 @@ function sanitizePanels(raw) {
|
|
|
3889
3929
|
}
|
|
3890
3930
|
if (Object.keys(widths).length > 0) out.widths = widths;
|
|
3891
3931
|
}
|
|
3892
|
-
|
|
3932
|
+
if (Array.isArray(p.pinned)) {
|
|
3933
|
+
const pinned = p.pinned.filter((id) => typeof id === "string" && id !== "");
|
|
3934
|
+
if (pinned.length > 0) out.pinned = [...new Set(pinned)];
|
|
3935
|
+
}
|
|
3936
|
+
return out.open || out.widths || out.pinned ? out : null;
|
|
3893
3937
|
}
|
|
3894
3938
|
function sanitizeTrackSizes(raw) {
|
|
3895
3939
|
if (raw == null || typeof raw !== "object") return null;
|
|
@@ -4151,6 +4195,7 @@ var MARKET_LABELS = {
|
|
|
4151
4195
|
open: "Market Open",
|
|
4152
4196
|
pre: "Pre-Market",
|
|
4153
4197
|
post: "Post-Market",
|
|
4198
|
+
extended: "Extended Hours",
|
|
4154
4199
|
closed: "Market Closed",
|
|
4155
4200
|
holiday: "Market Holiday"
|
|
4156
4201
|
};
|
|
@@ -4158,6 +4203,7 @@ var MARKET_INKS = {
|
|
|
4158
4203
|
open: "var(--vela-up)",
|
|
4159
4204
|
pre: SESSION_PRE,
|
|
4160
4205
|
post: SESSION_POST,
|
|
4206
|
+
extended: SESSION_POST,
|
|
4161
4207
|
closed: SESSION_OFF,
|
|
4162
4208
|
holiday: SESSION_OFF
|
|
4163
4209
|
};
|
|
@@ -4781,118 +4827,17 @@ var ChartContextMenu = class {
|
|
|
4781
4827
|
// src/core/options.ts
|
|
4782
4828
|
var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
|
|
4783
4829
|
|
|
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
4830
|
// src/widget/session-shading.ts
|
|
4887
4831
|
var DAY_MS = 864e5;
|
|
4888
4832
|
var MINUTE_MS = 6e4;
|
|
4889
|
-
function parseWindow(text) {
|
|
4833
|
+
function parseWindow(text, allowOvernight = false) {
|
|
4890
4834
|
if (typeof text !== "string") return null;
|
|
4891
4835
|
const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
|
|
4892
4836
|
if (!m) return null;
|
|
4893
4837
|
const start = Number(m[1]) * 60 + Number(m[2]);
|
|
4894
4838
|
const end = Number(m[3]) * 60 + Number(m[4]);
|
|
4895
|
-
if (
|
|
4839
|
+
if (end > 1440 || start >= 1440) return null;
|
|
4840
|
+
if (start >= end && !allowOvernight) return null;
|
|
4896
4841
|
return { start, end };
|
|
4897
4842
|
}
|
|
4898
4843
|
function parseSessionSpec(si) {
|
|
@@ -4902,9 +4847,13 @@ function parseSessionSpec(si) {
|
|
|
4902
4847
|
if (!regular) return null;
|
|
4903
4848
|
const tz = si?.["timezone"];
|
|
4904
4849
|
const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
|
|
4905
|
-
const ext = parseWindow(si?.["session_extended"]);
|
|
4850
|
+
const ext = parseWindow(si?.["session_extended"], true);
|
|
4851
|
+
if (ext && ext.start >= ext.end) {
|
|
4852
|
+
if (ext.start >= regular.end && ext.end >= regular.end) return { regular, extended: ext, overnight: true, timezone };
|
|
4853
|
+
return { regular, extended: { start: 0, end: 1440 }, overnight: false, timezone };
|
|
4854
|
+
}
|
|
4906
4855
|
const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
|
|
4907
|
-
return { regular, extended, timezone };
|
|
4856
|
+
return { regular, extended, overnight: false, timezone };
|
|
4908
4857
|
}
|
|
4909
4858
|
var dtfCache = /* @__PURE__ */ new Map();
|
|
4910
4859
|
function civilFormatter(tz) {
|
|
@@ -4940,27 +4889,48 @@ function civilParts(dtf, ms) {
|
|
|
4940
4889
|
minute: Number(get("minute"))
|
|
4941
4890
|
};
|
|
4942
4891
|
}
|
|
4892
|
+
function mergeAbutting(bands) {
|
|
4893
|
+
const sorted = [...bands].sort((a, b) => a[0] - b[0]);
|
|
4894
|
+
const out = [];
|
|
4895
|
+
for (const [s, e] of sorted) {
|
|
4896
|
+
if (e <= s) continue;
|
|
4897
|
+
const last = out[out.length - 1];
|
|
4898
|
+
if (last && s <= last[1]) last[1] = Math.max(last[1], e);
|
|
4899
|
+
else out.push([s, e]);
|
|
4900
|
+
}
|
|
4901
|
+
return out;
|
|
4902
|
+
}
|
|
4943
4903
|
function expandSessionZones(spec, from, to) {
|
|
4944
4904
|
const pre = [];
|
|
4945
4905
|
const post = [];
|
|
4906
|
+
const extended = [];
|
|
4946
4907
|
const dtf = civilFormatter(spec.timezone);
|
|
4947
|
-
if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
|
|
4908
|
+
if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post, extended };
|
|
4948
4909
|
const seen = /* @__PURE__ */ new Set();
|
|
4949
4910
|
for (let cursor = from - DAY_MS; cursor < to + DAY_MS; cursor += DAY_MS) {
|
|
4950
4911
|
const civil = civilParts(dtf, cursor);
|
|
4951
4912
|
const key = civil.year * 1e4 + civil.month * 100 + civil.day;
|
|
4952
4913
|
if (!Number.isFinite(key) || seen.has(key)) continue;
|
|
4953
4914
|
seen.add(key);
|
|
4954
|
-
|
|
4915
|
+
const sunday = civil.weekday === "Sun";
|
|
4916
|
+
if (civil.weekday === "Sat" || sunday && !spec.overnight) continue;
|
|
4955
4917
|
const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
|
|
4956
4918
|
const noonGuess = naive + 720 * MINUTE_MS;
|
|
4957
4919
|
const atNoon = civilParts(dtf, noonGuess);
|
|
4958
4920
|
const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
|
|
4959
4921
|
const at = (minutes) => naive + minutes * MINUTE_MS - offset;
|
|
4922
|
+
if (spec.overnight) {
|
|
4923
|
+
if (!sunday) {
|
|
4924
|
+
extended.push([at(0), at(spec.regular.start)]);
|
|
4925
|
+
if (spec.regular.end < spec.extended.end) extended.push([at(spec.regular.end), at(spec.extended.end)]);
|
|
4926
|
+
}
|
|
4927
|
+
if (civil.weekday !== "Fri") extended.push([at(spec.extended.start), at(1440)]);
|
|
4928
|
+
continue;
|
|
4929
|
+
}
|
|
4960
4930
|
if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
|
|
4961
4931
|
if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
|
|
4962
4932
|
}
|
|
4963
|
-
return { pre, post };
|
|
4933
|
+
return { pre, post, extended: mergeAbutting(extended) };
|
|
4964
4934
|
}
|
|
4965
4935
|
var COVER_PAD_MIN_MS = 2 * DAY_MS;
|
|
4966
4936
|
var SessionShadingTracker = class {
|
|
@@ -5015,7 +4985,7 @@ var SessionShadingTracker = class {
|
|
|
5015
4985
|
return;
|
|
5016
4986
|
}
|
|
5017
4987
|
if (this.session !== "extended" || !this.intraday) {
|
|
5018
|
-
if (force) this.onZones({ pre: [], post: [] });
|
|
4988
|
+
if (force) this.onZones({ pre: [], post: [], extended: [] });
|
|
5019
4989
|
return;
|
|
5020
4990
|
}
|
|
5021
4991
|
const from = Math.min(range.from, range.to);
|
|
@@ -5029,6 +4999,112 @@ var SessionShadingTracker = class {
|
|
|
5029
4999
|
}
|
|
5030
5000
|
};
|
|
5031
5001
|
|
|
5002
|
+
// src/widget/market-status.ts
|
|
5003
|
+
function civilDate(ms, tz) {
|
|
5004
|
+
try {
|
|
5005
|
+
return new Intl.DateTimeFormat("en-CA", { timeZone: tz }).format(ms);
|
|
5006
|
+
} catch {
|
|
5007
|
+
return "";
|
|
5008
|
+
}
|
|
5009
|
+
}
|
|
5010
|
+
function isWeekday(ms, tz) {
|
|
5011
|
+
try {
|
|
5012
|
+
const wd = new Intl.DateTimeFormat("en-US", { timeZone: tz, weekday: "short" }).format(ms);
|
|
5013
|
+
return wd !== "Sat" && wd !== "Sun";
|
|
5014
|
+
} catch {
|
|
5015
|
+
return false;
|
|
5016
|
+
}
|
|
5017
|
+
}
|
|
5018
|
+
function deriveMarketStatus(now, w, tz, overnight = false) {
|
|
5019
|
+
const within = (ws) => ws.find(([s, e]) => now >= s && now < e);
|
|
5020
|
+
if (within(w.regular)) return "open";
|
|
5021
|
+
const ext = within(w.extended);
|
|
5022
|
+
if (ext) {
|
|
5023
|
+
if (overnight) return "extended";
|
|
5024
|
+
return w.regular.some(([s]) => s >= now && s < ext[1]) ? "pre" : "post";
|
|
5025
|
+
}
|
|
5026
|
+
if (isWeekday(now, tz)) {
|
|
5027
|
+
const today = civilDate(now, tz);
|
|
5028
|
+
if (today !== "" && !w.extended.some(([s]) => civilDate(s, tz) === today)) return "holiday";
|
|
5029
|
+
}
|
|
5030
|
+
return "closed";
|
|
5031
|
+
}
|
|
5032
|
+
function nextStatusBoundary(now, w) {
|
|
5033
|
+
let next = null;
|
|
5034
|
+
for (const ws of [w.regular, w.extended]) {
|
|
5035
|
+
for (const [s, e] of ws) {
|
|
5036
|
+
for (const t of [s, e]) {
|
|
5037
|
+
if (t > now && (next == null || t < next)) next = t;
|
|
5038
|
+
}
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
5041
|
+
return next;
|
|
5042
|
+
}
|
|
5043
|
+
var FETCH_BACK_MS = 4 * 864e5;
|
|
5044
|
+
var FETCH_AHEAD_MS = 10 * 864e5;
|
|
5045
|
+
var MIN_TIMER_MS = 15e3;
|
|
5046
|
+
var MAX_TIMER_MS = 36e5;
|
|
5047
|
+
var RETRY_MS = 6e4;
|
|
5048
|
+
var MarketStatusTracker = class {
|
|
5049
|
+
constructor(onStatus) {
|
|
5050
|
+
this.onStatus = onStatus;
|
|
5051
|
+
/** Invalidates detached async work — bumped by every track()/stop(). */
|
|
5052
|
+
this.epoch = 0;
|
|
5053
|
+
this.timer = null;
|
|
5054
|
+
}
|
|
5055
|
+
/** (Re)bind to a chart's data surface + symbol and start evaluating. */
|
|
5056
|
+
track(data, symbol) {
|
|
5057
|
+
const my = ++this.epoch;
|
|
5058
|
+
this.clearTimer();
|
|
5059
|
+
void this.evaluate(my, data, symbol);
|
|
5060
|
+
}
|
|
5061
|
+
stop() {
|
|
5062
|
+
this.epoch += 1;
|
|
5063
|
+
this.clearTimer();
|
|
5064
|
+
}
|
|
5065
|
+
async evaluate(my, data, symbol) {
|
|
5066
|
+
const resolved = data.resolve(symbol);
|
|
5067
|
+
const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
|
|
5068
|
+
const si = await data.symbolInfo(symbol).catch(() => void 0);
|
|
5069
|
+
if (my !== this.epoch) return;
|
|
5070
|
+
const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
|
|
5071
|
+
if (!provider?.getCalendar || !hasSessions || !resolved) {
|
|
5072
|
+
this.onStatus("open");
|
|
5073
|
+
return;
|
|
5074
|
+
}
|
|
5075
|
+
const tz = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : "Etc/UTC";
|
|
5076
|
+
const now = Date.now();
|
|
5077
|
+
const range = { from: now - FETCH_BACK_MS, to: now + FETCH_AHEAD_MS };
|
|
5078
|
+
const [regular, extended] = await Promise.all([
|
|
5079
|
+
provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
|
|
5080
|
+
provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
|
|
5081
|
+
]);
|
|
5082
|
+
if (my !== this.epoch) return;
|
|
5083
|
+
if (!regular || !extended) {
|
|
5084
|
+
this.arm(my, data, symbol, RETRY_MS);
|
|
5085
|
+
return;
|
|
5086
|
+
}
|
|
5087
|
+
const w = { regular, extended };
|
|
5088
|
+
const overnight = parseSessionSpec(si)?.overnight === true;
|
|
5089
|
+
this.onStatus(deriveMarketStatus(now, w, tz, overnight));
|
|
5090
|
+
const boundary = nextStatusBoundary(now, w);
|
|
5091
|
+
const delay = Math.min(boundary != null ? boundary - now : MAX_TIMER_MS, MAX_TIMER_MS);
|
|
5092
|
+
this.arm(my, data, symbol, Math.max(delay, MIN_TIMER_MS));
|
|
5093
|
+
}
|
|
5094
|
+
arm(my, data, symbol, delay) {
|
|
5095
|
+
this.clearTimer();
|
|
5096
|
+
this.timer = setTimeout(() => {
|
|
5097
|
+
this.timer = null;
|
|
5098
|
+
if (my !== this.epoch) return;
|
|
5099
|
+
void this.evaluate(my, data, symbol);
|
|
5100
|
+
}, delay);
|
|
5101
|
+
}
|
|
5102
|
+
clearTimer() {
|
|
5103
|
+
if (this.timer != null) clearTimeout(this.timer);
|
|
5104
|
+
this.timer = null;
|
|
5105
|
+
}
|
|
5106
|
+
};
|
|
5107
|
+
|
|
5032
5108
|
// src/widget/glide.ts
|
|
5033
5109
|
var MIN_SPAN_MS = 6e4;
|
|
5034
5110
|
var FOLLOW = 0.25;
|
|
@@ -5394,7 +5470,10 @@ var ChartCell = class {
|
|
|
5394
5470
|
this.activeRangeId = null;
|
|
5395
5471
|
/** Latched verdict of {@link sessionAvailable} (async metadata, sticky per symbol). */
|
|
5396
5472
|
this.sessionAvailableFlag = false;
|
|
5397
|
-
/**
|
|
5473
|
+
/** Latched: the symbol's extended tape wraps midnight (an overnight roll market) —
|
|
5474
|
+
* one extended-hours shading phase instead of the pre/post split. */
|
|
5475
|
+
this.sessionOvernightFlag = false;
|
|
5476
|
+
/** Keeps the session shading on the symbol's real calendar (see {@link SessionShadingTracker}). */
|
|
5398
5477
|
this.sessionShading = new SessionShadingTracker((zones) => this.inner?.renderer.set("sessionZones", zones));
|
|
5399
5478
|
this.manifest = [];
|
|
5400
5479
|
/** A restored ledger's manifest entry NAMES, waiting for the manifest to resolve
|
|
@@ -5647,8 +5726,10 @@ var ChartCell = class {
|
|
|
5647
5726
|
void chart.data.symbolInfo(symbol).then((si) => {
|
|
5648
5727
|
if (this.inner !== chart) return;
|
|
5649
5728
|
const available = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
|
|
5650
|
-
|
|
5729
|
+
const overnight = parseSessionSpec(si)?.overnight === true;
|
|
5730
|
+
if (available !== this.sessionAvailableFlag || overnight !== this.sessionOvernightFlag) {
|
|
5651
5731
|
this.sessionAvailableFlag = available;
|
|
5732
|
+
this.sessionOvernightFlag = overnight;
|
|
5652
5733
|
this.deps.onMarketChanged(this.id);
|
|
5653
5734
|
this.pushSettingsSections();
|
|
5654
5735
|
}
|
|
@@ -5682,16 +5763,41 @@ var ChartCell = class {
|
|
|
5682
5763
|
/**
|
|
5683
5764
|
* (Re)contribute this cell's settings-dialog sections: status line parts, the
|
|
5684
5765
|
* 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
|
|
5766
|
+
* HAS sessions — the Trading session group (RTH/ETH switch + the session shading
|
|
5767
|
+
* colors: pre/post-market on day-split tapes, one extended-hours swatch on
|
|
5768
|
+
* overnight roll markets) inside the Symbol tab. Bars/watermark/titles are
|
|
5769
|
+
* persistable cell state; a depth-only reload is silent, so mark dirty here.
|
|
5770
|
+
* Re-run whenever a gate changes (the dialog reads the sections on open).
|
|
5689
5771
|
*/
|
|
5690
5772
|
pushSettingsSections() {
|
|
5691
5773
|
const chart = this.inner;
|
|
5692
5774
|
if (!chart) return;
|
|
5693
5775
|
const rth = "Regular hours (RTH)";
|
|
5694
5776
|
const eth = "Extended hours (ETH)";
|
|
5777
|
+
const shadeRows = this.sessionOvernightFlag ? [
|
|
5778
|
+
{
|
|
5779
|
+
kind: "color",
|
|
5780
|
+
label: "Extended hours",
|
|
5781
|
+
id: "extended-color",
|
|
5782
|
+
get: () => this.sessionShadeColor("extendedColor"),
|
|
5783
|
+
set: (v) => this.setSessionShadeColor("extendedColor", v)
|
|
5784
|
+
}
|
|
5785
|
+
] : [
|
|
5786
|
+
{
|
|
5787
|
+
kind: "color",
|
|
5788
|
+
label: "Pre-market",
|
|
5789
|
+
id: "premarket-color",
|
|
5790
|
+
get: () => this.sessionShadeColor("premarketColor"),
|
|
5791
|
+
set: (v) => this.setSessionShadeColor("premarketColor", v)
|
|
5792
|
+
},
|
|
5793
|
+
{
|
|
5794
|
+
kind: "color",
|
|
5795
|
+
label: "Post-market",
|
|
5796
|
+
id: "postmarket-color",
|
|
5797
|
+
get: () => this.sessionShadeColor("postmarketColor"),
|
|
5798
|
+
set: (v) => this.setSessionShadeColor("postmarketColor", v)
|
|
5799
|
+
}
|
|
5800
|
+
];
|
|
5695
5801
|
const sessionSection = {
|
|
5696
5802
|
title: "Trading session",
|
|
5697
5803
|
id: "trading-session",
|
|
@@ -5705,20 +5811,7 @@ var ChartCell = class {
|
|
|
5705
5811
|
get: () => this.session === "extended" ? eth : rth,
|
|
5706
5812
|
set: (v) => this.setSession(v === eth ? "extended" : "regular")
|
|
5707
5813
|
},
|
|
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
|
-
}
|
|
5814
|
+
...shadeRows
|
|
5722
5815
|
]
|
|
5723
5816
|
};
|
|
5724
5817
|
const advanced = {
|
|
@@ -5962,6 +6055,10 @@ var ChartCell = class {
|
|
|
5962
6055
|
this.deps.activate(this.id);
|
|
5963
6056
|
this.inner?.renderer.focus();
|
|
5964
6057
|
}
|
|
6058
|
+
/** Raster of this cell's chart (same pixels as the PNG download), or null. */
|
|
6059
|
+
screenshotCanvas() {
|
|
6060
|
+
return this.inner?.renderer.screenshotCanvas() ?? null;
|
|
6061
|
+
}
|
|
5965
6062
|
/** Download this cell's chart as a PNG (named after its market). */
|
|
5966
6063
|
downloadScreenshot() {
|
|
5967
6064
|
const url = this.inner?.renderer.screenshot();
|
|
@@ -7819,6 +7916,54 @@ function buildContext(host) {
|
|
|
7819
7916
|
};
|
|
7820
7917
|
}
|
|
7821
7918
|
|
|
7919
|
+
// src/workspace/screenshot.ts
|
|
7920
|
+
function destRect(tile, dpr) {
|
|
7921
|
+
const dx = Math.round(tile.x * dpr);
|
|
7922
|
+
const dy = Math.round(tile.y * dpr);
|
|
7923
|
+
return {
|
|
7924
|
+
dx,
|
|
7925
|
+
dy,
|
|
7926
|
+
dw: Math.round((tile.x + tile.width) * dpr) - dx,
|
|
7927
|
+
dh: Math.round((tile.y + tile.height) * dpr) - dy
|
|
7928
|
+
};
|
|
7929
|
+
}
|
|
7930
|
+
function tilesFromCellRects(grid, cells) {
|
|
7931
|
+
if (grid.width <= 0 || grid.height <= 0) return [];
|
|
7932
|
+
const tiles = [];
|
|
7933
|
+
for (const c of cells) {
|
|
7934
|
+
if (c.hidden || c.width <= 0 || c.height <= 0) continue;
|
|
7935
|
+
tiles.push({ x: c.left - grid.left, y: c.top - grid.top, width: c.width, height: c.height });
|
|
7936
|
+
}
|
|
7937
|
+
return tiles;
|
|
7938
|
+
}
|
|
7939
|
+
function compositeLayoutScreenshot(doc, frame, tiles) {
|
|
7940
|
+
if (frame.width <= 0 || frame.height <= 0 || tiles.length === 0) return null;
|
|
7941
|
+
const dpr = frame.dpr > 0 ? frame.dpr : 1;
|
|
7942
|
+
const out = doc.createElement("canvas");
|
|
7943
|
+
out.width = Math.max(1, Math.round(frame.width * dpr));
|
|
7944
|
+
out.height = Math.max(1, Math.round(frame.height * dpr));
|
|
7945
|
+
const ctx = out.getContext("2d");
|
|
7946
|
+
if (!ctx) return null;
|
|
7947
|
+
ctx.fillStyle = frame.gapColor;
|
|
7948
|
+
ctx.fillRect(0, 0, out.width, out.height);
|
|
7949
|
+
let painted = 0;
|
|
7950
|
+
for (const tile of tiles) {
|
|
7951
|
+
if (tile.width <= 0 || tile.height <= 0) continue;
|
|
7952
|
+
const { dx, dy, dw, dh } = destRect(tile, dpr);
|
|
7953
|
+
if (dw <= 0 || dh <= 0) continue;
|
|
7954
|
+
ctx.drawImage(tile.source, dx, dy, dw, dh);
|
|
7955
|
+
painted += 1;
|
|
7956
|
+
}
|
|
7957
|
+
if (painted === 0) return null;
|
|
7958
|
+
return out.toDataURL("image/png");
|
|
7959
|
+
}
|
|
7960
|
+
function triggerPngDownload(doc, url, filename) {
|
|
7961
|
+
const a = doc.createElement("a");
|
|
7962
|
+
a.href = url;
|
|
7963
|
+
a.download = filename;
|
|
7964
|
+
a.click();
|
|
7965
|
+
}
|
|
7966
|
+
|
|
7822
7967
|
// src/workspace/VelaWorkspace.ts
|
|
7823
7968
|
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "30", "60", "240", "D", "W", "M"];
|
|
7824
7969
|
var GAP_PX = 2;
|
|
@@ -8049,7 +8194,7 @@ var VelaWorkspace = class {
|
|
|
8049
8194
|
...picker ? { onIndicatorsClick: () => picker.open() } : {},
|
|
8050
8195
|
onUndoClick: () => this.active.history.undo(),
|
|
8051
8196
|
onRedoClick: () => this.active.history.redo(),
|
|
8052
|
-
onScreenshotClick: () => this.
|
|
8197
|
+
onScreenshotClick: () => this.downloadScreenshot(),
|
|
8053
8198
|
onAlertsClick: (anchor) => this.openAlertsMenu(anchor),
|
|
8054
8199
|
timeframe: "60",
|
|
8055
8200
|
timeframes: opts.timeframes ?? DEFAULT_TIMEFRAMES,
|
|
@@ -8108,6 +8253,7 @@ var VelaWorkspace = class {
|
|
|
8108
8253
|
this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
|
|
8109
8254
|
this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
|
|
8110
8255
|
this.dock.refresh();
|
|
8256
|
+
if (boot?.panels) this.dock.applyState(boot.panels);
|
|
8111
8257
|
this.root.appendChild(main);
|
|
8112
8258
|
this.toastHost = new Toast(this.gridEl);
|
|
8113
8259
|
const attribution = rendererDefaults().attribution;
|
|
@@ -8119,6 +8265,7 @@ var VelaWorkspace = class {
|
|
|
8119
8265
|
bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H3}px) + 10px)`,
|
|
8120
8266
|
zIndex: "11"
|
|
8121
8267
|
});
|
|
8268
|
+
mark.dataset.velaScreenshot = "1";
|
|
8122
8269
|
this.gridEl.appendChild(mark);
|
|
8123
8270
|
this.attributionMark = mark;
|
|
8124
8271
|
}
|
|
@@ -8255,6 +8402,56 @@ var VelaWorkspace = class {
|
|
|
8255
8402
|
get chart() {
|
|
8256
8403
|
return this.active.chart;
|
|
8257
8404
|
}
|
|
8405
|
+
/**
|
|
8406
|
+
* PNG data URL of the visible layout: every live cell in its grid slot, or the
|
|
8407
|
+
* maximized cell alone. Same pixels the screenshot button downloads. A single
|
|
8408
|
+
* visible cell returns that cell's own chart export (the one-chart case).
|
|
8409
|
+
*/
|
|
8410
|
+
screenshot() {
|
|
8411
|
+
const cells = this.shotCells();
|
|
8412
|
+
if (cells.length === 0) return null;
|
|
8413
|
+
if (cells.length === 1) return cells[0].chart.renderer.screenshot();
|
|
8414
|
+
return this.compositeLayoutShot(cells);
|
|
8415
|
+
}
|
|
8416
|
+
/** Download {@link screenshot} as a PNG. A multi-cell layout is named
|
|
8417
|
+
* `vela-layout.png`; one visible cell keeps `${symbol}-${timeframe}.png`. */
|
|
8418
|
+
downloadScreenshot() {
|
|
8419
|
+
const cells = this.shotCells();
|
|
8420
|
+
if (cells.length <= 1) {
|
|
8421
|
+
(cells[0] ?? this.active).downloadScreenshot();
|
|
8422
|
+
return;
|
|
8423
|
+
}
|
|
8424
|
+
const url = this.compositeLayoutShot(cells);
|
|
8425
|
+
if (!url) return;
|
|
8426
|
+
triggerPngDownload(this.root.ownerDocument, url, "vela-layout.png");
|
|
8427
|
+
}
|
|
8428
|
+
/** Live cells the screenshot should include — maximized siblings are `hidden`. */
|
|
8429
|
+
shotCells() {
|
|
8430
|
+
return this.cells().filter((c) => c.host.style.visibility !== "hidden");
|
|
8431
|
+
}
|
|
8432
|
+
/** Place every cell's raster onto one canvas the size of the grid. */
|
|
8433
|
+
compositeLayoutShot(cells) {
|
|
8434
|
+
const gridEl = this.gridEl;
|
|
8435
|
+
const grid = gridEl.getBoundingClientRect();
|
|
8436
|
+
const win = gridEl.ownerDocument.defaultView;
|
|
8437
|
+
const dpr = win?.devicePixelRatio ?? 1;
|
|
8438
|
+
const gapColor = win ? win.getComputedStyle(gridEl).backgroundColor : "";
|
|
8439
|
+
const tiles = [];
|
|
8440
|
+
for (const cell of cells) {
|
|
8441
|
+
const source = cell.screenshotCanvas();
|
|
8442
|
+
if (!source) continue;
|
|
8443
|
+
const r = cell.host.getBoundingClientRect();
|
|
8444
|
+
const place = tilesFromCellRects(grid, [{ left: r.left, top: r.top, width: r.width, height: r.height }])[0];
|
|
8445
|
+
if (!place) continue;
|
|
8446
|
+
tiles.push({ source, ...place });
|
|
8447
|
+
}
|
|
8448
|
+
return compositeLayoutScreenshot(gridEl.ownerDocument, {
|
|
8449
|
+
width: grid.width,
|
|
8450
|
+
height: grid.height,
|
|
8451
|
+
dpr,
|
|
8452
|
+
gapColor: gapColor || "#000000"
|
|
8453
|
+
}, tiles);
|
|
8454
|
+
}
|
|
8258
8455
|
setActiveCell(id) {
|
|
8259
8456
|
if (id === this.activeId || this.destroyed) return;
|
|
8260
8457
|
const prev = this.activeId;
|
|
@@ -9301,7 +9498,7 @@ var VelaWorkspace = class {
|
|
|
9301
9498
|
this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
|
|
9302
9499
|
host: this.root,
|
|
9303
9500
|
...has("undo-redo") ? { onUndo: () => this.active.history.undo(), onRedo: () => this.active.history.redo() } : {},
|
|
9304
|
-
...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.
|
|
9501
|
+
...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.downloadScreenshot() } : {},
|
|
9305
9502
|
canUndo: () => this.active.history.canUndo,
|
|
9306
9503
|
canRedo: () => this.active.history.canRedo,
|
|
9307
9504
|
priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
|
|
@@ -9402,9 +9599,9 @@ var VelaWorkspace = class {
|
|
|
9402
9599
|
this.keymap.register({
|
|
9403
9600
|
id: "chart.screenshot",
|
|
9404
9601
|
keys: "mod+alt+s",
|
|
9405
|
-
label: ov ? ov.label : "Download a
|
|
9602
|
+
label: ov ? ov.label : "Download a screenshot of the layout",
|
|
9406
9603
|
category: "Chart",
|
|
9407
|
-
run: ov ? () => this.runOverride(ov) : () => this.
|
|
9604
|
+
run: ov ? () => this.runOverride(ov) : () => this.downloadScreenshot()
|
|
9408
9605
|
});
|
|
9409
9606
|
}
|
|
9410
9607
|
this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
|