@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
package/dist/workspace.cjs
CHANGED
|
@@ -46,6 +46,7 @@ var SERIES_LINE = "#3b82f6";
|
|
|
46
46
|
var CROSSHAIR = "#9aa0ad";
|
|
47
47
|
var SLATE_DEEP = "#1e293b";
|
|
48
48
|
var SLATE = "#475569";
|
|
49
|
+
var CHIP_PLATE = "#595959";
|
|
49
50
|
var TRADE_LONG = ACCENT;
|
|
50
51
|
var TRADE_SHORT = BEARISH;
|
|
51
52
|
var TRADE_EXIT = "#d500f9";
|
|
@@ -1086,6 +1087,12 @@ ${STATIC_DECLS}
|
|
|
1086
1087
|
.vela-ui *, .vela-ui-layer * { box-sizing: border-box; }
|
|
1087
1088
|
/* Text ENTRY is the one exception: selection is part of editing. */
|
|
1088
1089
|
.vela-ui :is(input, textarea), .vela-ui-layer :is(input, textarea) { user-select: text; -webkit-user-select: text; }
|
|
1090
|
+
/* iOS Safari zooms the page when a focused field is under 16px, and often
|
|
1091
|
+
keeps that zoom after the field blurs or a dialog closes. The shell's
|
|
1092
|
+
mobile size class is the honest gate \u2014 not a media query \u2014 so an embedded
|
|
1093
|
+
chart on a wide desktop still gets the rule when it is in the phone layout.
|
|
1094
|
+
Scoped to the kit root so a host page's own [data-layout] is never touched. */
|
|
1095
|
+
.vela-ui[data-layout='mobile'] :is(input, textarea, select) { font-size: 16px; }
|
|
1089
1096
|
.vela-icon { display: inline-flex; align-items: center; flex: none; }
|
|
1090
1097
|
.vela-icon svg { display: block; }
|
|
1091
1098
|
`;
|
|
@@ -1181,6 +1188,10 @@ registerIcon(
|
|
|
1181
1188
|
"market-post",
|
|
1182
1189
|
svg24('<path d="M12 10V2"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 14-4 4-4-4"/><path d="M16 6a4 4 0 0 0-8 0"/>')
|
|
1183
1190
|
);
|
|
1191
|
+
registerIcon(
|
|
1192
|
+
"market-extended",
|
|
1193
|
+
svg24('<path d="M22 22H2"/><path d="M12 3a5 5 0 0 0 7.5 7.5A7.5 7.5 0 1 1 12 3Z"/>')
|
|
1194
|
+
);
|
|
1184
1195
|
registerIcon("market-closed", svg24('<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>'));
|
|
1185
1196
|
registerIcon(
|
|
1186
1197
|
"market-holiday",
|
|
@@ -1194,6 +1205,8 @@ registerIcon("chevrons-right", S('<path d="m4 3.5 4.5 4.5L4 12.5"/><path d="m8.5
|
|
|
1194
1205
|
registerIcon("chevrons-left", S('<path d="M12 3.5 7.5 8l4.5 4.5"/><path d="M7.5 3.5 3 8l4.5 4.5"/>'));
|
|
1195
1206
|
registerIcon("check", S('<path d="m2.8 8.4 3.4 3.4 7-7.6"/>'));
|
|
1196
1207
|
registerIcon("close", S('<path d="m3.8 3.8 8.4 8.4M12.2 3.8l-8.4 8.4"/>'));
|
|
1208
|
+
registerIcon("pin", S('<path d="M5.6 2.5h4.8M6.4 2.5v3.6L4.4 8.4v.9h7.2v-.9L9.6 6.1V2.5M8 9.3v4.2"/>'));
|
|
1209
|
+
registerIcon("pin-filled", S('<path d="M5.6 2.5h4.8M6.4 2.5v3.6L4.4 8.4v.9h7.2v-.9L9.6 6.1V2.5M8 9.3v4.2"/><path d="M6.4 2.5h3.2v3.6l2 2.3v.9H4.4v-.9l2-2.3z" fill="currentColor" stroke="none"/>'));
|
|
1197
1210
|
registerIcon("eye", S('<path d="M1.5 8s2.5-5.4 6.5-5.4S14.5 8 14.5 8 12 13.4 8 13.4 1.5 8 1.5 8z"/><circle cx="8" cy="8" r="1.8"/>'));
|
|
1198
1211
|
registerIcon("eye-off", S('<path d="M1.5 8s2.5-5.4 6.5-5.4S14.5 8 14.5 8 12 13.4 8 13.4 1.5 8 1.5 8z" opacity="0.45"/><path d="m3 13 10-10"/>'));
|
|
1199
1212
|
registerIcon("lock", S('<rect x="3.2" y="7" width="9.6" height="6.6" rx="1.2"/><path d="M5.6 7V5.2a2.4 2.4 0 0 1 4.8 0V7"/>'));
|
|
@@ -4968,6 +4981,7 @@ var BASELINE_FILL_ALPHA_FAR = 0.05;
|
|
|
4968
4981
|
var BASELINE_LEVEL_DEFAULT = 50;
|
|
4969
4982
|
var PREMARKET_SHADE = withAlpha(WARNING, 0.08);
|
|
4970
4983
|
var POSTMARKET_SHADE = withAlpha(ACCENT, 0.08);
|
|
4984
|
+
var EXTENDED_SHADE = POSTMARKET_SHADE;
|
|
4971
4985
|
function defaultChartStyle() {
|
|
4972
4986
|
return {
|
|
4973
4987
|
chartTypes: {},
|
|
@@ -4976,7 +4990,7 @@ function defaultChartStyle() {
|
|
|
4976
4990
|
gridHorz: { visible: true, color: null },
|
|
4977
4991
|
borderColor: null,
|
|
4978
4992
|
separatorColor: null,
|
|
4979
|
-
crosshair: { color: CROSSHAIR, width: 1, style: "dashed", opacity: 0.4, labelBackground:
|
|
4993
|
+
crosshair: { color: CROSSHAIR, width: 1, style: "dashed", opacity: 0.4, labelBackground: CHIP_PLATE },
|
|
4980
4994
|
candle: {
|
|
4981
4995
|
bodyVisible: true,
|
|
4982
4996
|
borderVisible: false,
|
|
@@ -4999,7 +5013,7 @@ function defaultChartStyle() {
|
|
|
4999
5013
|
width: 2,
|
|
5000
5014
|
baselineLevel: BASELINE_LEVEL_DEFAULT
|
|
5001
5015
|
},
|
|
5002
|
-
sessions: { premarketColor: PREMARKET_SHADE, postmarketColor: POSTMARKET_SHADE }
|
|
5016
|
+
sessions: { premarketColor: PREMARKET_SHADE, postmarketColor: POSTMARKET_SHADE, extendedColor: EXTENDED_SHADE }
|
|
5003
5017
|
};
|
|
5004
5018
|
}
|
|
5005
5019
|
var LINE_STYLES = ["solid", "dashed", "dotted"];
|
|
@@ -5228,7 +5242,8 @@ function mergeConfig(base, patch) {
|
|
|
5228
5242
|
},
|
|
5229
5243
|
sessions: {
|
|
5230
5244
|
premarketColor: isColor(sessions.premarketColor) ? sessions.premarketColor : base.sessions.premarketColor,
|
|
5231
|
-
postmarketColor: isColor(sessions.postmarketColor) ? sessions.postmarketColor : base.sessions.postmarketColor
|
|
5245
|
+
postmarketColor: isColor(sessions.postmarketColor) ? sessions.postmarketColor : base.sessions.postmarketColor,
|
|
5246
|
+
extendedColor: isColor(sessions.extendedColor) ? sessions.extendedColor : base.sessions.extendedColor
|
|
5232
5247
|
},
|
|
5233
5248
|
stacking: {
|
|
5234
5249
|
candles: isNum(stacking.candles) ? stacking.candles : base.stacking.candles,
|
|
@@ -12224,6 +12239,22 @@ var CSS5 = `
|
|
|
12224
12239
|
}
|
|
12225
12240
|
.vela-panel-close .vela-icon { width: 16px; height: 16px; }
|
|
12226
12241
|
.vela-panel-close:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
|
|
12242
|
+
/* The pin: same footprint as the close button it sits beside; filled + bright once pinned. */
|
|
12243
|
+
.vela-panel-pin {
|
|
12244
|
+
all: unset;
|
|
12245
|
+
cursor: pointer;
|
|
12246
|
+
width: 26px;
|
|
12247
|
+
height: 26px;
|
|
12248
|
+
flex: none;
|
|
12249
|
+
display: inline-flex;
|
|
12250
|
+
align-items: center;
|
|
12251
|
+
justify-content: center;
|
|
12252
|
+
border-radius: 4px;
|
|
12253
|
+
color: var(--vela-fg-muted);
|
|
12254
|
+
}
|
|
12255
|
+
.vela-panel-pin .vela-icon { width: 15px; height: 15px; }
|
|
12256
|
+
.vela-panel-pin:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
|
|
12257
|
+
.vela-panel-pin[data-on='1'] { color: var(--vela-fg-bright); }
|
|
12227
12258
|
.vela-panel-body { flex: 1; overflow: auto; padding: 8px; }
|
|
12228
12259
|
.vela-panel-body::-webkit-scrollbar { width: 8px; }
|
|
12229
12260
|
.vela-panel-body::-webkit-scrollbar-thumb { background: var(--vela-scroll); border-radius: 4px; border: 2px solid transparent; background-clip: padding-box; }
|
|
@@ -12248,8 +12279,23 @@ var CSS5 = `
|
|
|
12248
12279
|
width: 1px;
|
|
12249
12280
|
background: transparent;
|
|
12250
12281
|
}
|
|
12282
|
+
/* The same hover ink as the workspace grid splitters and the canvas pane separators, so
|
|
12283
|
+
every draggable seam in the shell reads as one family. */
|
|
12251
12284
|
.vela-panel-resizer:hover::after,
|
|
12252
|
-
.vela-panel-resizer[data-dragging]::after { background: var(--vela-
|
|
12285
|
+
.vela-panel-resizer[data-dragging]::after { background: var(--vela-separator-hover-line); }
|
|
12286
|
+
/* An OVERLAY panel floats over the chart's right edge instead of taking a column: the
|
|
12287
|
+
chart keeps its width and layout (the dock host is position:relative \u2014 the anchoring the
|
|
12288
|
+
mobile rule below relies on too). Same z-order as that mobile overlay, above the chart's
|
|
12289
|
+
own DOM overlays; the shadow separates it from the plot it covers. */
|
|
12290
|
+
.vela-panel[data-overlay] {
|
|
12291
|
+
position: absolute;
|
|
12292
|
+
top: 0;
|
|
12293
|
+
right: 0;
|
|
12294
|
+
bottom: 0;
|
|
12295
|
+
max-width: 100%;
|
|
12296
|
+
z-index: 25;
|
|
12297
|
+
box-shadow: var(--vela-shadow);
|
|
12298
|
+
}
|
|
12253
12299
|
/* Mobile: a 280px column would crush a phone-width chart \u2014 the panel overlays the
|
|
12254
12300
|
chart area instead (its flex parent is position:relative), full-bleed, closed by
|
|
12255
12301
|
the same header \u2715. Width dragging is a pointer affordance; off on mobile. */
|
|
@@ -12280,6 +12326,10 @@ var SidePanel = class {
|
|
|
12280
12326
|
/** Notified when the USER settles a new width (drag release, or double-click reset) — never on
|
|
12281
12327
|
* a programmatic {@link setWidth}, so restoring a persisted width raises no change. */
|
|
12282
12328
|
this.onWidthChange = null;
|
|
12329
|
+
/** Notified when the USER pins or unpins a floating panel (`overlay` is the NEW placement) —
|
|
12330
|
+
* never on a programmatic {@link setOverlay}, for the same reason as widths. */
|
|
12331
|
+
this.onPlacementChange = null;
|
|
12332
|
+
this.pin = null;
|
|
12283
12333
|
const doc = host.ownerDocument;
|
|
12284
12334
|
injectStyles(STYLE_ID4, CSS5, doc);
|
|
12285
12335
|
this.minWidth = opts.resizable ? opts.minWidth ?? DEFAULT_PANEL_MIN_WIDTH : 1;
|
|
@@ -12289,6 +12339,9 @@ var SidePanel = class {
|
|
|
12289
12339
|
this.el = doc.createElement("div");
|
|
12290
12340
|
this.el.className = `vela-panel ${modifier}`;
|
|
12291
12341
|
this.el.hidden = true;
|
|
12342
|
+
this.floatable = opts.overlay === true;
|
|
12343
|
+
this.overlayOn = this.floatable;
|
|
12344
|
+
if (this.overlayOn) this.el.dataset.overlay = "1";
|
|
12292
12345
|
this.el.style.setProperty("--vela-panel-w", `${this.widthPx}px`);
|
|
12293
12346
|
const header = doc.createElement("div");
|
|
12294
12347
|
header.className = "vela-panel-header";
|
|
@@ -12302,7 +12355,16 @@ var SidePanel = class {
|
|
|
12302
12355
|
close.appendChild(iconEl("close", doc));
|
|
12303
12356
|
close.title = "Close";
|
|
12304
12357
|
close.addEventListener("click", () => this.toggle(false));
|
|
12305
|
-
|
|
12358
|
+
if (this.floatable) {
|
|
12359
|
+
this.pin = doc.createElement("button");
|
|
12360
|
+
this.pin.className = "vela-panel-pin";
|
|
12361
|
+
this.pin.addEventListener("click", () => {
|
|
12362
|
+
this.setOverlay(!this.overlayOn);
|
|
12363
|
+
this.onPlacementChange?.(this.overlayOn);
|
|
12364
|
+
});
|
|
12365
|
+
this.refreshPin();
|
|
12366
|
+
}
|
|
12367
|
+
header.append(this.heading, this.slot, ...this.pin ? [this.pin] : [], close);
|
|
12306
12368
|
this.body = doc.createElement("div");
|
|
12307
12369
|
this.body.className = "vela-panel-body";
|
|
12308
12370
|
this.el.append(header, this.body);
|
|
@@ -12344,6 +12406,29 @@ var SidePanel = class {
|
|
|
12344
12406
|
this.widthPx = next;
|
|
12345
12407
|
this.el.style.setProperty("--vela-panel-w", `${next}px`);
|
|
12346
12408
|
}
|
|
12409
|
+
/** Whether the panel floats over the chart right now (false for every docked panel). */
|
|
12410
|
+
get overlay() {
|
|
12411
|
+
return this.overlayOn;
|
|
12412
|
+
}
|
|
12413
|
+
/** Float or dock the panel. Only a panel declared `overlay` can float — on any other this is
|
|
12414
|
+
* a no-op. Silent — {@link onPlacementChange} reports the user's pin clicks only. */
|
|
12415
|
+
setOverlay(overlay) {
|
|
12416
|
+
if (!this.floatable || overlay === this.overlayOn) return;
|
|
12417
|
+
this.overlayOn = overlay;
|
|
12418
|
+
if (overlay) this.el.dataset.overlay = "1";
|
|
12419
|
+
else delete this.el.dataset.overlay;
|
|
12420
|
+
this.refreshPin();
|
|
12421
|
+
}
|
|
12422
|
+
refreshPin() {
|
|
12423
|
+
if (!this.pin) return;
|
|
12424
|
+
const pinned = !this.overlayOn;
|
|
12425
|
+
const doc = this.pin.ownerDocument;
|
|
12426
|
+
this.pin.replaceChildren(iconEl(pinned ? "pin-filled" : "pin", doc));
|
|
12427
|
+
this.pin.dataset.on = pinned ? "1" : "0";
|
|
12428
|
+
this.pin.title = pinned ? "Unpin \u2014 float over the chart" : "Pin beside the chart";
|
|
12429
|
+
this.pin.setAttribute("aria-label", this.pin.title);
|
|
12430
|
+
this.pin.setAttribute("aria-pressed", pinned ? "true" : "false");
|
|
12431
|
+
}
|
|
12347
12432
|
destroy() {
|
|
12348
12433
|
this.el.remove();
|
|
12349
12434
|
}
|
|
@@ -13744,6 +13829,8 @@ var PanelDock = class {
|
|
|
13744
13829
|
this.entries = [];
|
|
13745
13830
|
/** Widths the USER settled, by panel id — the only ones worth persisting. */
|
|
13746
13831
|
this.widths = /* @__PURE__ */ new Map();
|
|
13832
|
+
/** Floating panels the USER pinned as columns, by id — remembered for late registrations too. */
|
|
13833
|
+
this.pinned = /* @__PURE__ */ new Set();
|
|
13747
13834
|
/** A restored `open` naming a panel that has not registered yet: honored when it docks. */
|
|
13748
13835
|
this.pendingOpen = null;
|
|
13749
13836
|
this.chart = null;
|
|
@@ -13766,7 +13853,8 @@ var PanelDock = class {
|
|
|
13766
13853
|
width: desc.width,
|
|
13767
13854
|
resizable: desc.resizable,
|
|
13768
13855
|
minWidth: desc.minWidth,
|
|
13769
|
-
maxWidth: desc.maxWidth
|
|
13856
|
+
maxWidth: desc.maxWidth,
|
|
13857
|
+
overlay: desc.overlay
|
|
13770
13858
|
});
|
|
13771
13859
|
const entry = {
|
|
13772
13860
|
id: desc.id,
|
|
@@ -13814,7 +13902,8 @@ var PanelDock = class {
|
|
|
13814
13902
|
const open2 = this.openId;
|
|
13815
13903
|
if (open2) out.open = open2;
|
|
13816
13904
|
if (this.widths.size > 0) out.widths = Object.fromEntries(this.widths);
|
|
13817
|
-
|
|
13905
|
+
if (this.pinned.size > 0) out.pinned = [...this.pinned];
|
|
13906
|
+
return out.open || out.widths || out.pinned ? out : null;
|
|
13818
13907
|
}
|
|
13819
13908
|
/**
|
|
13820
13909
|
* Restore a persisted dock state. Widths apply to any panel present, and are remembered for
|
|
@@ -13822,7 +13911,8 @@ var PanelDock = class {
|
|
|
13822
13911
|
* a document that predates the dock has no `panels` field at all, so the shell never calls
|
|
13823
13912
|
* this and the default (everything closed) stands. An `open` naming a panel that has not
|
|
13824
13913
|
* registered yet is held until it docks (a plugin loaded after the restore), unless the user
|
|
13825
|
-
* opens something in the meantime.
|
|
13914
|
+
* opens something in the meantime. `pinned` is the whole list of floating panels the user
|
|
13915
|
+
* docked — its absence means none is, so every floatable panel floats again.
|
|
13826
13916
|
*/
|
|
13827
13917
|
applyState(state) {
|
|
13828
13918
|
if (!state) return;
|
|
@@ -13832,6 +13922,8 @@ var PanelDock = class {
|
|
|
13832
13922
|
this.entries.find((e) => e.id === id)?.panel.setWidth(px);
|
|
13833
13923
|
}
|
|
13834
13924
|
}
|
|
13925
|
+
this.pinned = new Set(state.pinned ?? []);
|
|
13926
|
+
for (const entry of this.entries) entry.panel.setOverlay(!this.pinned.has(entry.id));
|
|
13835
13927
|
for (const entry of this.entries) entry.panel.toggle(entry.id === state.open);
|
|
13836
13928
|
this.pendingOpen = state.open && !this.entries.some((e) => e.id === state.open) ? state.open : null;
|
|
13837
13929
|
}
|
|
@@ -13845,6 +13937,7 @@ var PanelDock = class {
|
|
|
13845
13937
|
this.entries.sort((a, b) => a.order - b.order);
|
|
13846
13938
|
const stored = this.widths.get(entry.id);
|
|
13847
13939
|
if (stored !== void 0) entry.panel.setWidth(stored);
|
|
13940
|
+
if (this.pinned.has(entry.id)) entry.panel.setOverlay(false);
|
|
13848
13941
|
entry.panel.onOpenChange = (open2) => {
|
|
13849
13942
|
if (open2) {
|
|
13850
13943
|
for (const other of this.entries) if (other !== entry) other.panel.toggle(false);
|
|
@@ -13858,6 +13951,11 @@ var PanelDock = class {
|
|
|
13858
13951
|
this.widths.set(entry.id, px);
|
|
13859
13952
|
this.deps.changed?.();
|
|
13860
13953
|
};
|
|
13954
|
+
entry.panel.onPlacementChange = (overlay) => {
|
|
13955
|
+
if (overlay) this.pinned.delete(entry.id);
|
|
13956
|
+
else this.pinned.add(entry.id);
|
|
13957
|
+
this.deps.changed?.();
|
|
13958
|
+
};
|
|
13861
13959
|
if (this.pendingOpen === entry.id) entry.panel.toggle(true);
|
|
13862
13960
|
if (!entry.contributed) this.publish();
|
|
13863
13961
|
}
|
|
@@ -13933,6 +14031,17 @@ function filterSymbols(list, query, limit = 100, top = TOP_TICKERS) {
|
|
|
13933
14031
|
}
|
|
13934
14032
|
var STYLE_ID7 = "vela-widget-symbolpicker";
|
|
13935
14033
|
var CSS8 = `
|
|
14034
|
+
/* Search + market tabs stay pinned while the result list scrolls underneath
|
|
14035
|
+
(mobile fullscreen: the dialog body is the scroller). Negative margin eats
|
|
14036
|
+
the body's padding so scrolled rows cannot peek around the island. */
|
|
14037
|
+
.vela-sp-sticky {
|
|
14038
|
+
position: sticky;
|
|
14039
|
+
top: 0;
|
|
14040
|
+
z-index: 1;
|
|
14041
|
+
background: var(--vela-surface);
|
|
14042
|
+
margin: calc(-1 * var(--vela-space-4)) calc(-1 * var(--vela-space-4)) 0;
|
|
14043
|
+
padding: var(--vela-space-4) var(--vela-space-4) 0;
|
|
14044
|
+
}
|
|
13936
14045
|
.vela-sp-searchrow {
|
|
13937
14046
|
display: flex;
|
|
13938
14047
|
align-items: center;
|
|
@@ -13952,8 +14061,10 @@ var CSS8 = `
|
|
|
13952
14061
|
border: none;
|
|
13953
14062
|
font-size: 14px;
|
|
13954
14063
|
outline: none;
|
|
14064
|
+
text-transform: uppercase;
|
|
13955
14065
|
}
|
|
13956
|
-
.vela-sp-
|
|
14066
|
+
.vela-sp-input::placeholder { text-transform: none; }
|
|
14067
|
+
.vela-sp-tabs { display: flex; gap: 14px; margin: 12px 2px 0; border-bottom: 1px solid var(--vela-border); padding-bottom: 8px; }
|
|
13957
14068
|
/* Mobile (fullscreen dialog): the asset-class strip scrolls sideways instead of
|
|
13958
14069
|
overflowing the body, and the result list stops capping itself \u2014 the body owns
|
|
13959
14070
|
the scrolling in the fullscreen presentation. */
|
|
@@ -14002,7 +14113,7 @@ var CSS8 = `
|
|
|
14002
14113
|
font-weight: 700;
|
|
14003
14114
|
}
|
|
14004
14115
|
.vela-sp-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
|
|
14005
|
-
.vela-sp-ticker { font-weight: 700; color: var(--vela-fg-bright); font-size: 14px; }
|
|
14116
|
+
.vela-sp-ticker { font-weight: 700; color: var(--vela-fg-bright); font-size: 14px; text-transform: uppercase; }
|
|
14006
14117
|
.vela-sp-desc { color: var(--vela-fg-muted); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
14007
14118
|
.vela-sp-badge {
|
|
14008
14119
|
flex: none;
|
|
@@ -14065,6 +14176,8 @@ var SymbolPicker = class {
|
|
|
14065
14176
|
searchRow.append(iconEl("search", doc), this.input);
|
|
14066
14177
|
this.tabs = doc.createElement("div");
|
|
14067
14178
|
this.tabs.className = "vela-sp-tabs";
|
|
14179
|
+
const sticky = doc.createElement("div");
|
|
14180
|
+
sticky.className = "vela-sp-sticky";
|
|
14068
14181
|
for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
|
|
14069
14182
|
const b = doc.createElement("button");
|
|
14070
14183
|
b.className = "vela-sp-tab";
|
|
@@ -14078,6 +14191,7 @@ var SymbolPicker = class {
|
|
|
14078
14191
|
});
|
|
14079
14192
|
this.tabs.appendChild(b);
|
|
14080
14193
|
}
|
|
14194
|
+
sticky.append(searchRow, this.tabs);
|
|
14081
14195
|
this.list = doc.createElement("div");
|
|
14082
14196
|
this.list.className = "vela-sp-list";
|
|
14083
14197
|
this.list.addEventListener("scroll", () => {
|
|
@@ -14090,21 +14204,32 @@ var SymbolPicker = class {
|
|
|
14090
14204
|
title: "Symbol Search",
|
|
14091
14205
|
host: opts.host,
|
|
14092
14206
|
closeOnInteractOutside: true,
|
|
14093
|
-
content: (body) => body.append(
|
|
14207
|
+
content: (body) => body.append(sticky, this.list),
|
|
14094
14208
|
onOpenChange: (open2) => {
|
|
14095
14209
|
if (open2) {
|
|
14096
|
-
this.input.value = this.seed;
|
|
14210
|
+
this.input.value = this.seed.toUpperCase();
|
|
14097
14211
|
this.seed = "";
|
|
14098
14212
|
this.refresh();
|
|
14099
14213
|
setTimeout(() => {
|
|
14100
14214
|
this.input.focus();
|
|
14101
14215
|
this.input.setSelectionRange(this.input.value.length, this.input.value.length);
|
|
14102
14216
|
}, 0);
|
|
14217
|
+
} else {
|
|
14218
|
+
this.input.blur();
|
|
14103
14219
|
}
|
|
14104
14220
|
opts.onOpenChange?.(open2);
|
|
14105
14221
|
}
|
|
14106
14222
|
});
|
|
14107
|
-
this.input.addEventListener("input", () =>
|
|
14223
|
+
this.input.addEventListener("input", () => {
|
|
14224
|
+
const next = this.input.value.toUpperCase();
|
|
14225
|
+
if (this.input.value !== next) {
|
|
14226
|
+
const start = this.input.selectionStart;
|
|
14227
|
+
const end = this.input.selectionEnd;
|
|
14228
|
+
this.input.value = next;
|
|
14229
|
+
if (start != null && end != null) this.input.setSelectionRange(start, end);
|
|
14230
|
+
}
|
|
14231
|
+
this.refresh();
|
|
14232
|
+
});
|
|
14108
14233
|
this.input.addEventListener("keydown", (e) => {
|
|
14109
14234
|
if (e.key === "ArrowDown") this.moveHighlight(1);
|
|
14110
14235
|
else if (e.key === "ArrowUp") this.moveHighlight(-1);
|
|
@@ -17724,6 +17849,7 @@ function createCustomMark(doc, html, background) {
|
|
|
17724
17849
|
});
|
|
17725
17850
|
el.style.color = attributionMarkColor(background);
|
|
17726
17851
|
el.innerHTML = html;
|
|
17852
|
+
el.dataset.velaScreenshot = "1";
|
|
17727
17853
|
return el;
|
|
17728
17854
|
}
|
|
17729
17855
|
function createAttributionMark(doc, background) {
|
|
@@ -17742,6 +17868,7 @@ function createAttributionMark(doc, background) {
|
|
|
17742
17868
|
cursor: "pointer"
|
|
17743
17869
|
});
|
|
17744
17870
|
applyAttributionMarkTheme(a, background);
|
|
17871
|
+
a.dataset.velaScreenshot = "1";
|
|
17745
17872
|
const symbol = doc.createElement("span");
|
|
17746
17873
|
symbol.className = "vela-attr-symbol";
|
|
17747
17874
|
symbol.setAttribute("aria-hidden", "true");
|
|
@@ -17910,7 +18037,11 @@ function sanitizePanels(raw) {
|
|
|
17910
18037
|
}
|
|
17911
18038
|
if (Object.keys(widths).length > 0) out.widths = widths;
|
|
17912
18039
|
}
|
|
17913
|
-
|
|
18040
|
+
if (Array.isArray(p.pinned)) {
|
|
18041
|
+
const pinned = p.pinned.filter((id) => typeof id === "string" && id !== "");
|
|
18042
|
+
if (pinned.length > 0) out.pinned = [...new Set(pinned)];
|
|
18043
|
+
}
|
|
18044
|
+
return out.open || out.widths || out.pinned ? out : null;
|
|
17914
18045
|
}
|
|
17915
18046
|
function sanitizeTrackSizes(raw) {
|
|
17916
18047
|
if (raw == null || typeof raw !== "object") return null;
|
|
@@ -18393,6 +18524,7 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
|
|
|
18393
18524
|
var PREVIEW_BARS = 300;
|
|
18394
18525
|
var SINGLE_LOAD_BARS = 5e3;
|
|
18395
18526
|
var CHUNK_BARS = 1e4;
|
|
18527
|
+
var FIRST_PAINT_BARS = 20;
|
|
18396
18528
|
var GAP_FACTOR = 1.5;
|
|
18397
18529
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
18398
18530
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
@@ -18668,6 +18800,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18668
18800
|
let painted = false;
|
|
18669
18801
|
const paint = (bars, final) => {
|
|
18670
18802
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
18803
|
+
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
18671
18804
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
18672
18805
|
if (!painted && bars.length > 0) {
|
|
18673
18806
|
painted = true;
|
|
@@ -20085,6 +20218,15 @@ var RendererControl = class {
|
|
|
20085
20218
|
console.warn(`[vela] renderer "${this.renderer.name}" does not support screenshot().`);
|
|
20086
20219
|
return null;
|
|
20087
20220
|
}
|
|
20221
|
+
/**
|
|
20222
|
+
* Raster of the current chart onto a canvas (same pixels as {@link screenshot}),
|
|
20223
|
+
* or null if the renderer has no canvas export. Silent — a host compositing
|
|
20224
|
+
* several charts skips a renderer that cannot contribute.
|
|
20225
|
+
*/
|
|
20226
|
+
screenshotCanvas() {
|
|
20227
|
+
if (typeof this.renderer.screenshotCanvas === "function") return this.renderer.screenshotCanvas();
|
|
20228
|
+
return null;
|
|
20229
|
+
}
|
|
20088
20230
|
/**
|
|
20089
20231
|
* The active renderer's full cosmetic config as a serializable, versioned JSON
|
|
20090
20232
|
* document — persist it (templates, saved settings) and feed it back to
|
|
@@ -22653,6 +22795,66 @@ function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
|
|
|
22653
22795
|
center: (wickLeftDev + wickDev / 2) / dpr
|
|
22654
22796
|
};
|
|
22655
22797
|
}
|
|
22798
|
+
function aggregateCandleColumns(bars, i0, i1, xOf, yOf) {
|
|
22799
|
+
const out = [];
|
|
22800
|
+
let col = NaN;
|
|
22801
|
+
let intervals = [];
|
|
22802
|
+
const flush = () => {
|
|
22803
|
+
if (intervals.length === 0) return;
|
|
22804
|
+
const merged = [intervals[0]];
|
|
22805
|
+
for (let k = 1; k < intervals.length; k += 1) {
|
|
22806
|
+
const prev2 = merged[merged.length - 1];
|
|
22807
|
+
const next = intervals[k];
|
|
22808
|
+
if (Math.abs(yOf(prev2.hi) - yOf(next.lo)) < 1) {
|
|
22809
|
+
prev2.hi = next.hi;
|
|
22810
|
+
if (next.headIdx < prev2.headIdx) {
|
|
22811
|
+
prev2.headIdx = next.headIdx;
|
|
22812
|
+
prev2.headTime = next.headTime;
|
|
22813
|
+
prev2.open = next.open;
|
|
22814
|
+
}
|
|
22815
|
+
if (next.lastIdx > prev2.lastIdx) {
|
|
22816
|
+
prev2.lastIdx = next.lastIdx;
|
|
22817
|
+
prev2.close = next.close;
|
|
22818
|
+
}
|
|
22819
|
+
} else {
|
|
22820
|
+
merged.push(next);
|
|
22821
|
+
}
|
|
22822
|
+
}
|
|
22823
|
+
for (const iv of merged) out.push({ x: col, hi: iv.hi, lo: iv.lo, headTime: iv.headTime, open: iv.open, close: iv.close });
|
|
22824
|
+
intervals = [];
|
|
22825
|
+
};
|
|
22826
|
+
for (let i = i0; i <= i1; i += 1) {
|
|
22827
|
+
const b = bars[i];
|
|
22828
|
+
if (!b || b.high <= b.low) continue;
|
|
22829
|
+
const x = Math.round(xOf(i));
|
|
22830
|
+
if (x !== col) {
|
|
22831
|
+
flush();
|
|
22832
|
+
col = x;
|
|
22833
|
+
}
|
|
22834
|
+
let lo = b.low;
|
|
22835
|
+
let hi = b.high;
|
|
22836
|
+
let headIdx = i;
|
|
22837
|
+
let headTime = b.time;
|
|
22838
|
+
let open2 = b.open;
|
|
22839
|
+
for (let k = intervals.length - 1; k >= 0; k -= 1) {
|
|
22840
|
+
const iv = intervals[k];
|
|
22841
|
+
if (iv.lo > hi || iv.hi < lo) continue;
|
|
22842
|
+
if (iv.lo < lo) lo = iv.lo;
|
|
22843
|
+
if (iv.hi > hi) hi = iv.hi;
|
|
22844
|
+
if (iv.headIdx < headIdx) {
|
|
22845
|
+
headIdx = iv.headIdx;
|
|
22846
|
+
headTime = iv.headTime;
|
|
22847
|
+
open2 = iv.open;
|
|
22848
|
+
}
|
|
22849
|
+
intervals.splice(k, 1);
|
|
22850
|
+
}
|
|
22851
|
+
let insertAt = 0;
|
|
22852
|
+
while (insertAt < intervals.length && intervals[insertAt].lo < lo) insertAt += 1;
|
|
22853
|
+
intervals.splice(insertAt, 0, { lo, hi, headIdx, headTime, open: open2, lastIdx: i, close: b.close });
|
|
22854
|
+
}
|
|
22855
|
+
flush();
|
|
22856
|
+
return out;
|
|
22857
|
+
}
|
|
22656
22858
|
|
|
22657
22859
|
// src/renderers/native/backend/gl/Batch.ts
|
|
22658
22860
|
var FLOATS_PER_VERT = 8;
|
|
@@ -23625,44 +23827,19 @@ var WebGL2Backend = class {
|
|
|
23625
23827
|
}
|
|
23626
23828
|
}
|
|
23627
23829
|
/**
|
|
23628
|
-
* Sub-pixel LOD (mirrors Canvas2dBackend.drawCandlesAggregated):
|
|
23629
|
-
*
|
|
23630
|
-
*
|
|
23830
|
+
* Sub-pixel LOD (mirrors Canvas2dBackend.drawCandlesAggregated): bars sharing a
|
|
23831
|
+
* rounded pixel column collapse into high-low sticks — one per contiguous
|
|
23832
|
+
* coverage run (see {@link aggregateCandleColumns}), so a price gap inside the
|
|
23833
|
+
* column stays a void. Draw cost stays bounded by screen width, not bar count;
|
|
23834
|
+
* stick color follows its own first-open→last-close.
|
|
23631
23835
|
*/
|
|
23632
23836
|
emitCandlesAggregated(b, bars, i0, i1, coords, pane, up, down, barColors) {
|
|
23633
|
-
|
|
23634
|
-
|
|
23635
|
-
|
|
23636
|
-
|
|
23637
|
-
|
|
23638
|
-
let headTime = 0;
|
|
23639
|
-
let active = false;
|
|
23640
|
-
const flush = () => {
|
|
23641
|
-
if (!active) return;
|
|
23642
|
-
const c = parseColor(barColors.get(headTime) ?? (closeV >= openV ? up : down));
|
|
23643
|
-
const hY = coords.priceToY(hi, pane.scale, pane.bounds);
|
|
23644
|
-
const lY = coords.priceToY(lo, pane.scale, pane.bounds);
|
|
23645
|
-
b.rect(px - 0.5, hY, 1, lY - hY, c);
|
|
23646
|
-
};
|
|
23647
|
-
for (let i = i0; i <= i1; i += 1) {
|
|
23648
|
-
const bar = bars[i];
|
|
23649
|
-
if (!bar || bar.high <= bar.low) continue;
|
|
23650
|
-
const x = Math.round(coords.logicalToX(i));
|
|
23651
|
-
if (!active || x !== px) {
|
|
23652
|
-
flush();
|
|
23653
|
-
px = x;
|
|
23654
|
-
lo = bar.low;
|
|
23655
|
-
hi = bar.high;
|
|
23656
|
-
openV = bar.open;
|
|
23657
|
-
headTime = bar.time;
|
|
23658
|
-
active = true;
|
|
23659
|
-
} else {
|
|
23660
|
-
if (bar.low < lo) lo = bar.low;
|
|
23661
|
-
if (bar.high > hi) hi = bar.high;
|
|
23662
|
-
}
|
|
23663
|
-
closeV = bar.close;
|
|
23837
|
+
const yOf = (price) => coords.priceToY(price, pane.scale, pane.bounds);
|
|
23838
|
+
for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf)) {
|
|
23839
|
+
const c = parseColor(barColors.get(s.headTime) ?? (s.close >= s.open ? up : down));
|
|
23840
|
+
const hY = yOf(s.hi);
|
|
23841
|
+
b.rect(s.x - 0.5, hY, 1, yOf(s.lo) - hY, c);
|
|
23664
23842
|
}
|
|
23665
|
-
flush();
|
|
23666
23843
|
}
|
|
23667
23844
|
emitSeries(b, spec, pane, coords, i0, i1, theme, off = 0) {
|
|
23668
23845
|
if (spec.kind === "candle" || spec.kind === "bar") {
|
|
@@ -24960,13 +25137,15 @@ var SceneGraph = class {
|
|
|
24960
25137
|
if (!this.orderedCache) this.orderedCache = [...this.panes.values()].sort((a, b) => a.order - b.order);
|
|
24961
25138
|
return this.orderedCache;
|
|
24962
25139
|
}
|
|
24963
|
-
/** The session zones resolved into colored bands (pre/post-market
|
|
24964
|
-
* config's session colors) — consumed by the same painting path as
|
|
25140
|
+
/** The session zones resolved into colored bands (pre/post-market or extended-hours
|
|
25141
|
+
* washes from the config's session colors) — consumed by the same painting path as
|
|
25142
|
+
* {@link highlights}. */
|
|
24965
25143
|
sessionHighlightBands() {
|
|
24966
25144
|
if (!this.sessionZones) return [];
|
|
24967
25145
|
const out = [];
|
|
24968
25146
|
for (const [from, to] of this.sessionZones.pre) out.push({ from, to, color: this.style.sessions.premarketColor });
|
|
24969
25147
|
for (const [from, to] of this.sessionZones.post) out.push({ from, to, color: this.style.sessions.postmarketColor });
|
|
25148
|
+
for (const [from, to] of this.sessionZones.extended) out.push({ from, to, color: this.style.sessions.extendedColor });
|
|
24970
25149
|
return out;
|
|
24971
25150
|
}
|
|
24972
25151
|
indicatorsForPane(paneId) {
|
|
@@ -25505,50 +25684,23 @@ var Canvas2dBackend = class {
|
|
|
25505
25684
|
}
|
|
25506
25685
|
}
|
|
25507
25686
|
/**
|
|
25508
|
-
* Sub-pixel LOD:
|
|
25509
|
-
*
|
|
25510
|
-
*
|
|
25511
|
-
*
|
|
25687
|
+
* Sub-pixel LOD: bars sharing a rounded pixel column collapse into high-low
|
|
25688
|
+
* sticks (one per contiguous coverage run — see {@link aggregateCandleColumns}),
|
|
25689
|
+
* so draw cost is bounded by screen width (not bar count) when zoomed far out
|
|
25690
|
+
* and a price gap inside the column stays a void. Each stick's color follows
|
|
25691
|
+
* its first-open→last-close direction (a barcolor() on the head bar still wins).
|
|
25512
25692
|
*/
|
|
25513
25693
|
drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, up, down, barColors) {
|
|
25514
|
-
|
|
25515
|
-
|
|
25516
|
-
|
|
25517
|
-
|
|
25518
|
-
|
|
25519
|
-
let headTime = 0;
|
|
25520
|
-
let active = false;
|
|
25521
|
-
const flush = () => {
|
|
25522
|
-
if (!active) return;
|
|
25523
|
-
const color = barColors.get(headTime) ?? (closeV >= openV ? up : down);
|
|
25524
|
-
const hY = coords.priceToY(hi, pane.scale, pane.bounds);
|
|
25525
|
-
const lY = coords.priceToY(lo, pane.scale, pane.bounds);
|
|
25526
|
-
ctx.strokeStyle = color;
|
|
25527
|
-
ctx.lineWidth = 1;
|
|
25694
|
+
const yOf = (price) => coords.priceToY(price, pane.scale, pane.bounds);
|
|
25695
|
+
ctx.lineWidth = 1;
|
|
25696
|
+
for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf)) {
|
|
25697
|
+
const x = s.x + 0.5;
|
|
25698
|
+
ctx.strokeStyle = barColors.get(s.headTime) ?? (s.close >= s.open ? up : down);
|
|
25528
25699
|
ctx.beginPath();
|
|
25529
|
-
ctx.moveTo(
|
|
25530
|
-
ctx.lineTo(
|
|
25700
|
+
ctx.moveTo(x, yOf(s.hi));
|
|
25701
|
+
ctx.lineTo(x, yOf(s.lo));
|
|
25531
25702
|
ctx.stroke();
|
|
25532
|
-
};
|
|
25533
|
-
for (let i = i0; i <= i1; i += 1) {
|
|
25534
|
-
const b = bars[i];
|
|
25535
|
-
if (!b || b.high <= b.low) continue;
|
|
25536
|
-
const x = Math.round(coords.logicalToX(i)) + 0.5;
|
|
25537
|
-
if (!active || x !== px) {
|
|
25538
|
-
flush();
|
|
25539
|
-
px = x;
|
|
25540
|
-
lo = b.low;
|
|
25541
|
-
hi = b.high;
|
|
25542
|
-
openV = b.open;
|
|
25543
|
-
headTime = b.time;
|
|
25544
|
-
active = true;
|
|
25545
|
-
} else {
|
|
25546
|
-
if (b.low < lo) lo = b.low;
|
|
25547
|
-
if (b.high > hi) hi = b.high;
|
|
25548
|
-
}
|
|
25549
|
-
closeV = b.close;
|
|
25550
25703
|
}
|
|
25551
|
-
flush();
|
|
25552
25704
|
}
|
|
25553
25705
|
drawSeries(ctx, spec, pane, coords, i0, i1, theme, off = 0) {
|
|
25554
25706
|
if (spec.kind === "candle" || spec.kind === "bar") {
|
|
@@ -26572,6 +26724,32 @@ var DrawingSceneRenderer = class {
|
|
|
26572
26724
|
}
|
|
26573
26725
|
};
|
|
26574
26726
|
|
|
26727
|
+
// src/renderers/native/chrome/tz.ts
|
|
26728
|
+
function tzOffsetMs(ms, timeZone) {
|
|
26729
|
+
if (!timeZone || timeZone === "UTC") return 0;
|
|
26730
|
+
try {
|
|
26731
|
+
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
26732
|
+
timeZone,
|
|
26733
|
+
hourCycle: "h23",
|
|
26734
|
+
year: "numeric",
|
|
26735
|
+
month: "2-digit",
|
|
26736
|
+
day: "2-digit",
|
|
26737
|
+
hour: "2-digit",
|
|
26738
|
+
minute: "2-digit",
|
|
26739
|
+
second: "2-digit"
|
|
26740
|
+
});
|
|
26741
|
+
const parts = dtf.formatToParts(new Date(ms));
|
|
26742
|
+
const get = (t) => Number(parts.find((p) => p.type === t)?.value);
|
|
26743
|
+
const asUTC = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
|
|
26744
|
+
return asUTC - ms;
|
|
26745
|
+
} catch {
|
|
26746
|
+
return 0;
|
|
26747
|
+
}
|
|
26748
|
+
}
|
|
26749
|
+
function zonedDate(ms, timeZone) {
|
|
26750
|
+
return new Date(ms + tzOffsetMs(ms, timeZone));
|
|
26751
|
+
}
|
|
26752
|
+
|
|
26575
26753
|
// src/renderers/native/chrome/ticks.ts
|
|
26576
26754
|
function priceTicks(min, max, target = 6) {
|
|
26577
26755
|
if (!(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
|
|
@@ -26719,7 +26897,14 @@ var STEP_LADDER = [
|
|
|
26719
26897
|
YEAR
|
|
26720
26898
|
];
|
|
26721
26899
|
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
26900
|
+
var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
26722
26901
|
var pad2 = (n) => n < 10 ? `0${n}` : String(n);
|
|
26902
|
+
function formatTimeStamp(ms, timeZone, barIntervalMs) {
|
|
26903
|
+
const d = zonedDate(ms, timeZone);
|
|
26904
|
+
const date = `${WEEKDAYS[d.getUTCDay()]} ${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]} '${pad2(d.getUTCFullYear() % 100)}`;
|
|
26905
|
+
if (barIntervalMs >= DAY) return date;
|
|
26906
|
+
return `${date} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}`;
|
|
26907
|
+
}
|
|
26723
26908
|
function pickStep(targetMs) {
|
|
26724
26909
|
for (const step of STEP_LADDER) if (step >= targetMs) return step;
|
|
26725
26910
|
return STEP_LADDER[STEP_LADDER.length - 1];
|
|
@@ -26761,32 +26946,6 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
26761
26946
|
return out;
|
|
26762
26947
|
}
|
|
26763
26948
|
|
|
26764
|
-
// src/renderers/native/chrome/tz.ts
|
|
26765
|
-
function tzOffsetMs(ms, timeZone) {
|
|
26766
|
-
if (!timeZone || timeZone === "UTC") return 0;
|
|
26767
|
-
try {
|
|
26768
|
-
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
26769
|
-
timeZone,
|
|
26770
|
-
hourCycle: "h23",
|
|
26771
|
-
year: "numeric",
|
|
26772
|
-
month: "2-digit",
|
|
26773
|
-
day: "2-digit",
|
|
26774
|
-
hour: "2-digit",
|
|
26775
|
-
minute: "2-digit",
|
|
26776
|
-
second: "2-digit"
|
|
26777
|
-
});
|
|
26778
|
-
const parts = dtf.formatToParts(new Date(ms));
|
|
26779
|
-
const get = (t) => Number(parts.find((p) => p.type === t)?.value);
|
|
26780
|
-
const asUTC = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
|
|
26781
|
-
return asUTC - ms;
|
|
26782
|
-
} catch {
|
|
26783
|
-
return 0;
|
|
26784
|
-
}
|
|
26785
|
-
}
|
|
26786
|
-
function zonedDate(ms, timeZone) {
|
|
26787
|
-
return new Date(ms + tzOffsetMs(ms, timeZone));
|
|
26788
|
-
}
|
|
26789
|
-
|
|
26790
26949
|
// src/renderers/native/chrome/ChromeRenderer.ts
|
|
26791
26950
|
var ChromeRenderer = class {
|
|
26792
26951
|
constructor() {
|
|
@@ -27233,7 +27392,7 @@ var CrosshairRenderer = class {
|
|
|
27233
27392
|
const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
|
|
27234
27393
|
this.chip(ctx, dataW + 1, ch.y, formatAxisValue(pane.scale, pane.bounds.height, price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
|
|
27235
27394
|
}
|
|
27236
|
-
this.chip(ctx, x, dataH + 1,
|
|
27395
|
+
this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
27237
27396
|
}
|
|
27238
27397
|
destroy() {
|
|
27239
27398
|
this.canvas = null;
|
|
@@ -27281,7 +27440,7 @@ var CrosshairRenderer = class {
|
|
|
27281
27440
|
this.chip(ctx, dataW + 1, ext.y, formatAxisValue(pane.scale, pane.bounds.height, ext.price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
|
|
27282
27441
|
}
|
|
27283
27442
|
}
|
|
27284
|
-
this.chip(ctx, x, dataH + 1,
|
|
27443
|
+
this.chip(ctx, x, dataH + 1, formatTimeStamp(ext.time, scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
27285
27444
|
ctx.globalAlpha = 1;
|
|
27286
27445
|
}
|
|
27287
27446
|
/** A soft band + a brighter crisp center line over the hovered separator, so it reads as
|
|
@@ -27314,11 +27473,6 @@ function setDash3(ctx, style) {
|
|
|
27314
27473
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
27315
27474
|
else ctx.setLineDash([]);
|
|
27316
27475
|
}
|
|
27317
|
-
function formatStamp(ms, timeZone) {
|
|
27318
|
-
const d = zonedDate(ms, timeZone);
|
|
27319
|
-
const pad = (n) => n < 10 ? `0${n}` : String(n);
|
|
27320
|
-
return `${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
|
|
27321
|
-
}
|
|
27322
27476
|
|
|
27323
27477
|
// src/renderers/native/chrome/settings-visibility.ts
|
|
27324
27478
|
function settingsIdSlug(label) {
|
|
@@ -33085,6 +33239,9 @@ function clipHighlightRect(x1, x2, left, right) {
|
|
|
33085
33239
|
const end = Math.min(right, x2);
|
|
33086
33240
|
return end > x ? { x, width: end - x } : null;
|
|
33087
33241
|
}
|
|
33242
|
+
function bandEdgeSlot(logical) {
|
|
33243
|
+
return Math.ceil(logical) - 0.5;
|
|
33244
|
+
}
|
|
33088
33245
|
var BackdropRenderer = class {
|
|
33089
33246
|
constructor() {
|
|
33090
33247
|
this.canvas = null;
|
|
@@ -33115,21 +33272,22 @@ var BackdropRenderer = class {
|
|
|
33115
33272
|
this.drawGrid(ctx, scene, coords, theme, coords.width, gridAlpha);
|
|
33116
33273
|
}
|
|
33117
33274
|
/** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
|
|
33118
|
-
* Session-zone washes
|
|
33275
|
+
* Session-zone washes paint first (edges snapped to bar-slot boundaries so a band
|
|
33276
|
+
* never cuts a candle in two), host highlights on top (continuous time mapping —
|
|
33277
|
+
* hosts may paint sub-bar spans). */
|
|
33119
33278
|
drawHighlights(ctx, scene, coords) {
|
|
33120
33279
|
const sessions = scene.sessionHighlightBands();
|
|
33121
33280
|
if (sessions.length > 0) {
|
|
33122
33281
|
const left = Math.max(0, coords.logicalToX(-0.5));
|
|
33123
33282
|
const right = Math.min(coords.width, coords.logicalToX(coords.barCount - 0.5));
|
|
33124
|
-
this.drawHighlightSet(ctx, sessions, coords, left, right);
|
|
33283
|
+
this.drawHighlightSet(ctx, sessions, coords, left, right, true);
|
|
33125
33284
|
}
|
|
33126
|
-
this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width);
|
|
33285
|
+
this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width, false);
|
|
33127
33286
|
}
|
|
33128
|
-
drawHighlightSet(ctx, bands, coords, left, right) {
|
|
33287
|
+
drawHighlightSet(ctx, bands, coords, left, right, snapToSlots) {
|
|
33288
|
+
const edgeX = (ms) => snapToSlots ? coords.logicalToX(bandEdgeSlot(coords.timeToLogical(ms))) : coords.timeToX(ms);
|
|
33129
33289
|
for (const band of bands) {
|
|
33130
|
-
const
|
|
33131
|
-
const x2 = coords.timeToX(band.to);
|
|
33132
|
-
const rect = clipHighlightRect(x1, x2, left, right);
|
|
33290
|
+
const rect = clipHighlightRect(edgeX(band.from), edgeX(band.to), left, right);
|
|
33133
33291
|
if (!rect) continue;
|
|
33134
33292
|
ctx.fillStyle = band.color;
|
|
33135
33293
|
ctx.fillRect(rect.x, 0, rect.width, coords.height);
|
|
@@ -34276,7 +34434,7 @@ var NativeRenderer = class {
|
|
|
34276
34434
|
};
|
|
34277
34435
|
})(),
|
|
34278
34436
|
series: { style: this.scene.priceStyle, baseline: this.scene.baselineValue, spacing: this.coords.spacingScale },
|
|
34279
|
-
sessions: { premarketColor: s.sessions.premarketColor, postmarketColor: s.sessions.postmarketColor }
|
|
34437
|
+
sessions: { premarketColor: s.sessions.premarketColor, postmarketColor: s.sessions.postmarketColor, extendedColor: s.sessions.extendedColor }
|
|
34280
34438
|
};
|
|
34281
34439
|
}
|
|
34282
34440
|
/**
|
|
@@ -34358,7 +34516,7 @@ var NativeRenderer = class {
|
|
|
34358
34516
|
width: next.baseline.width,
|
|
34359
34517
|
baselineLevel: next.baseline.baselineLevel
|
|
34360
34518
|
};
|
|
34361
|
-
s.sessions = { premarketColor: next.sessions.premarketColor, postmarketColor: next.sessions.postmarketColor };
|
|
34519
|
+
s.sessions = { premarketColor: next.sessions.premarketColor, postmarketColor: next.sessions.postmarketColor, extendedColor: next.sessions.extendedColor };
|
|
34362
34520
|
this.setPriceStyle(next.series.style);
|
|
34363
34521
|
this.scene.candleOverride = candleOverrideFor(this.scene.priceStyle, s.chartTypes);
|
|
34364
34522
|
this.scene.baselineValue = next.series.baseline;
|
|
@@ -34580,9 +34738,10 @@ var NativeRenderer = class {
|
|
|
34580
34738
|
* opts in with a `data-vela-screenshot` attribute on the mount container's
|
|
34581
34739
|
* subtree (the widget marks its status line, and its symbol watermark with
|
|
34582
34740
|
* `"under"` — drawn beneath the canvases, where it sits on screen). Only the
|
|
34583
|
-
* crosshair (L2) is intentionally excluded.
|
|
34741
|
+
* crosshair (L2) is intentionally excluded. {@link screenshot} is this canvas
|
|
34742
|
+
* as a PNG data URL.
|
|
34584
34743
|
*/
|
|
34585
|
-
|
|
34744
|
+
screenshotCanvas() {
|
|
34586
34745
|
if (!this.dataCanvas) return null;
|
|
34587
34746
|
this.computeScales();
|
|
34588
34747
|
this.paintData();
|
|
@@ -34610,7 +34769,10 @@ var NativeRenderer = class {
|
|
|
34610
34769
|
if (el.getAttribute("data-vela-screenshot") !== "under") rasterizeOverlay(ctx, el, frame);
|
|
34611
34770
|
}
|
|
34612
34771
|
}
|
|
34613
|
-
return out
|
|
34772
|
+
return out;
|
|
34773
|
+
}
|
|
34774
|
+
screenshot() {
|
|
34775
|
+
return this.screenshotCanvas()?.toDataURL("image/png") ?? null;
|
|
34614
34776
|
}
|
|
34615
34777
|
/**
|
|
34616
34778
|
* The candles-first-appear reveal — animates the **candles themselves** (no extra
|
|
@@ -36005,7 +36167,7 @@ var NativeRenderer = class {
|
|
|
36005
36167
|
const pricePane = this.dataWindowPricePane();
|
|
36006
36168
|
const ohlc = this.dataWindowOHLC(bar, pricePane);
|
|
36007
36169
|
const parts = [];
|
|
36008
|
-
if (bar) parts.push(
|
|
36170
|
+
if (bar) parts.push(formatStamp(bar.time, this.scene.timezone));
|
|
36009
36171
|
if (ohlc) parts.push(`Open ${ohlc.o}, High ${ohlc.h}, Low ${ohlc.l}, Close ${ohlc.c}`);
|
|
36010
36172
|
for (const group of this.dataWindowGroups(idx, pricePane)) {
|
|
36011
36173
|
for (const row of group.rows) parts.push(`${row.label} ${row.value}`);
|
|
@@ -36892,7 +37054,7 @@ function formatStampParts(ms, timeZone) {
|
|
|
36892
37054
|
time: `${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`
|
|
36893
37055
|
};
|
|
36894
37056
|
}
|
|
36895
|
-
function
|
|
37057
|
+
function formatStamp(ms, timeZone) {
|
|
36896
37058
|
const { date, time } = formatStampParts(ms, timeZone);
|
|
36897
37059
|
return `${date} ${time}`;
|
|
36898
37060
|
}
|
|
@@ -36923,7 +37085,7 @@ function sanitizeSessionZones(value) {
|
|
|
36923
37085
|
}
|
|
36924
37086
|
return out.sort((a, b) => a[0] - b[0]);
|
|
36925
37087
|
};
|
|
36926
|
-
return { pre: windows(v.pre), post: windows(v.post) };
|
|
37088
|
+
return { pre: windows(v.pre), post: windows(v.post), extended: windows(v.extended) };
|
|
36927
37089
|
}
|
|
36928
37090
|
function clamp012(v) {
|
|
36929
37091
|
return Math.max(0, Math.min(1, v));
|
|
@@ -37612,6 +37774,7 @@ var MARKET_LABELS = {
|
|
|
37612
37774
|
open: "Market Open",
|
|
37613
37775
|
pre: "Pre-Market",
|
|
37614
37776
|
post: "Post-Market",
|
|
37777
|
+
extended: "Extended Hours",
|
|
37615
37778
|
closed: "Market Closed",
|
|
37616
37779
|
holiday: "Market Holiday"
|
|
37617
37780
|
};
|
|
@@ -37619,6 +37782,7 @@ var MARKET_INKS = {
|
|
|
37619
37782
|
open: "var(--vela-up)",
|
|
37620
37783
|
pre: SESSION_PRE,
|
|
37621
37784
|
post: SESSION_POST,
|
|
37785
|
+
extended: SESSION_POST,
|
|
37622
37786
|
closed: SESSION_OFF,
|
|
37623
37787
|
holiday: SESSION_OFF
|
|
37624
37788
|
};
|
|
@@ -37932,118 +38096,17 @@ var Statusline = class {
|
|
|
37932
38096
|
}
|
|
37933
38097
|
};
|
|
37934
38098
|
|
|
37935
|
-
// src/widget/market-status.ts
|
|
37936
|
-
function civilDate(ms, tz) {
|
|
37937
|
-
try {
|
|
37938
|
-
return new Intl.DateTimeFormat("en-CA", { timeZone: tz }).format(ms);
|
|
37939
|
-
} catch {
|
|
37940
|
-
return "";
|
|
37941
|
-
}
|
|
37942
|
-
}
|
|
37943
|
-
function isWeekday(ms, tz) {
|
|
37944
|
-
try {
|
|
37945
|
-
const wd = new Intl.DateTimeFormat("en-US", { timeZone: tz, weekday: "short" }).format(ms);
|
|
37946
|
-
return wd !== "Sat" && wd !== "Sun";
|
|
37947
|
-
} catch {
|
|
37948
|
-
return false;
|
|
37949
|
-
}
|
|
37950
|
-
}
|
|
37951
|
-
function deriveMarketStatus(now, w, tz) {
|
|
37952
|
-
const within = (ws) => ws.find(([s, e]) => now >= s && now < e);
|
|
37953
|
-
if (within(w.regular)) return "open";
|
|
37954
|
-
const ext = within(w.extended);
|
|
37955
|
-
if (ext) return w.regular.some(([s]) => s >= now && s < ext[1]) ? "pre" : "post";
|
|
37956
|
-
if (isWeekday(now, tz)) {
|
|
37957
|
-
const today = civilDate(now, tz);
|
|
37958
|
-
if (today !== "" && !w.extended.some(([s]) => civilDate(s, tz) === today)) return "holiday";
|
|
37959
|
-
}
|
|
37960
|
-
return "closed";
|
|
37961
|
-
}
|
|
37962
|
-
function nextStatusBoundary(now, w) {
|
|
37963
|
-
let next = null;
|
|
37964
|
-
for (const ws of [w.regular, w.extended]) {
|
|
37965
|
-
for (const [s, e] of ws) {
|
|
37966
|
-
for (const t of [s, e]) {
|
|
37967
|
-
if (t > now && (next == null || t < next)) next = t;
|
|
37968
|
-
}
|
|
37969
|
-
}
|
|
37970
|
-
}
|
|
37971
|
-
return next;
|
|
37972
|
-
}
|
|
37973
|
-
var FETCH_BACK_MS = 4 * 864e5;
|
|
37974
|
-
var FETCH_AHEAD_MS = 10 * 864e5;
|
|
37975
|
-
var MIN_TIMER_MS = 15e3;
|
|
37976
|
-
var MAX_TIMER_MS = 36e5;
|
|
37977
|
-
var RETRY_MS2 = 6e4;
|
|
37978
|
-
var MarketStatusTracker = class {
|
|
37979
|
-
constructor(onStatus) {
|
|
37980
|
-
this.onStatus = onStatus;
|
|
37981
|
-
/** Invalidates detached async work — bumped by every track()/stop(). */
|
|
37982
|
-
this.epoch = 0;
|
|
37983
|
-
this.timer = null;
|
|
37984
|
-
}
|
|
37985
|
-
/** (Re)bind to a chart's data surface + symbol and start evaluating. */
|
|
37986
|
-
track(data, symbol) {
|
|
37987
|
-
const my = ++this.epoch;
|
|
37988
|
-
this.clearTimer();
|
|
37989
|
-
void this.evaluate(my, data, symbol);
|
|
37990
|
-
}
|
|
37991
|
-
stop() {
|
|
37992
|
-
this.epoch += 1;
|
|
37993
|
-
this.clearTimer();
|
|
37994
|
-
}
|
|
37995
|
-
async evaluate(my, data, symbol) {
|
|
37996
|
-
const resolved = data.resolve(symbol);
|
|
37997
|
-
const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
|
|
37998
|
-
const si = await data.symbolInfo(symbol).catch(() => void 0);
|
|
37999
|
-
if (my !== this.epoch) return;
|
|
38000
|
-
const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
|
|
38001
|
-
if (!provider?.getCalendar || !hasSessions || !resolved) {
|
|
38002
|
-
this.onStatus("open");
|
|
38003
|
-
return;
|
|
38004
|
-
}
|
|
38005
|
-
const tz = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : "Etc/UTC";
|
|
38006
|
-
const now = Date.now();
|
|
38007
|
-
const range = { from: now - FETCH_BACK_MS, to: now + FETCH_AHEAD_MS };
|
|
38008
|
-
const [regular, extended] = await Promise.all([
|
|
38009
|
-
provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
|
|
38010
|
-
provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
|
|
38011
|
-
]);
|
|
38012
|
-
if (my !== this.epoch) return;
|
|
38013
|
-
if (!regular || !extended) {
|
|
38014
|
-
this.arm(my, data, symbol, RETRY_MS2);
|
|
38015
|
-
return;
|
|
38016
|
-
}
|
|
38017
|
-
const w = { regular, extended };
|
|
38018
|
-
this.onStatus(deriveMarketStatus(now, w, tz));
|
|
38019
|
-
const boundary = nextStatusBoundary(now, w);
|
|
38020
|
-
const delay = Math.min(boundary != null ? boundary - now : MAX_TIMER_MS, MAX_TIMER_MS);
|
|
38021
|
-
this.arm(my, data, symbol, Math.max(delay, MIN_TIMER_MS));
|
|
38022
|
-
}
|
|
38023
|
-
arm(my, data, symbol, delay) {
|
|
38024
|
-
this.clearTimer();
|
|
38025
|
-
this.timer = setTimeout(() => {
|
|
38026
|
-
this.timer = null;
|
|
38027
|
-
if (my !== this.epoch) return;
|
|
38028
|
-
void this.evaluate(my, data, symbol);
|
|
38029
|
-
}, delay);
|
|
38030
|
-
}
|
|
38031
|
-
clearTimer() {
|
|
38032
|
-
if (this.timer != null) clearTimeout(this.timer);
|
|
38033
|
-
this.timer = null;
|
|
38034
|
-
}
|
|
38035
|
-
};
|
|
38036
|
-
|
|
38037
38099
|
// src/widget/session-shading.ts
|
|
38038
38100
|
var DAY_MS2 = 864e5;
|
|
38039
38101
|
var MINUTE_MS = 6e4;
|
|
38040
|
-
function parseWindow(text) {
|
|
38102
|
+
function parseWindow(text, allowOvernight = false) {
|
|
38041
38103
|
if (typeof text !== "string") return null;
|
|
38042
38104
|
const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
|
|
38043
38105
|
if (!m) return null;
|
|
38044
38106
|
const start = Number(m[1]) * 60 + Number(m[2]);
|
|
38045
38107
|
const end = Number(m[3]) * 60 + Number(m[4]);
|
|
38046
|
-
if (
|
|
38108
|
+
if (end > 1440 || start >= 1440) return null;
|
|
38109
|
+
if (start >= end && !allowOvernight) return null;
|
|
38047
38110
|
return { start, end };
|
|
38048
38111
|
}
|
|
38049
38112
|
function parseSessionSpec(si) {
|
|
@@ -38053,9 +38116,13 @@ function parseSessionSpec(si) {
|
|
|
38053
38116
|
if (!regular) return null;
|
|
38054
38117
|
const tz = si?.["timezone"];
|
|
38055
38118
|
const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
|
|
38056
|
-
const ext = parseWindow(si?.["session_extended"]);
|
|
38119
|
+
const ext = parseWindow(si?.["session_extended"], true);
|
|
38120
|
+
if (ext && ext.start >= ext.end) {
|
|
38121
|
+
if (ext.start >= regular.end && ext.end >= regular.end) return { regular, extended: ext, overnight: true, timezone };
|
|
38122
|
+
return { regular, extended: { start: 0, end: 1440 }, overnight: false, timezone };
|
|
38123
|
+
}
|
|
38057
38124
|
const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
|
|
38058
|
-
return { regular, extended, timezone };
|
|
38125
|
+
return { regular, extended, overnight: false, timezone };
|
|
38059
38126
|
}
|
|
38060
38127
|
var dtfCache = /* @__PURE__ */ new Map();
|
|
38061
38128
|
function civilFormatter(tz) {
|
|
@@ -38091,27 +38158,48 @@ function civilParts(dtf, ms) {
|
|
|
38091
38158
|
minute: Number(get("minute"))
|
|
38092
38159
|
};
|
|
38093
38160
|
}
|
|
38161
|
+
function mergeAbutting(bands) {
|
|
38162
|
+
const sorted = [...bands].sort((a, b) => a[0] - b[0]);
|
|
38163
|
+
const out = [];
|
|
38164
|
+
for (const [s, e] of sorted) {
|
|
38165
|
+
if (e <= s) continue;
|
|
38166
|
+
const last = out[out.length - 1];
|
|
38167
|
+
if (last && s <= last[1]) last[1] = Math.max(last[1], e);
|
|
38168
|
+
else out.push([s, e]);
|
|
38169
|
+
}
|
|
38170
|
+
return out;
|
|
38171
|
+
}
|
|
38094
38172
|
function expandSessionZones(spec, from, to) {
|
|
38095
38173
|
const pre = [];
|
|
38096
38174
|
const post = [];
|
|
38175
|
+
const extended = [];
|
|
38097
38176
|
const dtf = civilFormatter(spec.timezone);
|
|
38098
|
-
if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
|
|
38177
|
+
if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post, extended };
|
|
38099
38178
|
const seen = /* @__PURE__ */ new Set();
|
|
38100
38179
|
for (let cursor = from - DAY_MS2; cursor < to + DAY_MS2; cursor += DAY_MS2) {
|
|
38101
38180
|
const civil = civilParts(dtf, cursor);
|
|
38102
38181
|
const key = civil.year * 1e4 + civil.month * 100 + civil.day;
|
|
38103
38182
|
if (!Number.isFinite(key) || seen.has(key)) continue;
|
|
38104
38183
|
seen.add(key);
|
|
38105
|
-
|
|
38184
|
+
const sunday = civil.weekday === "Sun";
|
|
38185
|
+
if (civil.weekday === "Sat" || sunday && !spec.overnight) continue;
|
|
38106
38186
|
const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
|
|
38107
38187
|
const noonGuess = naive + 720 * MINUTE_MS;
|
|
38108
38188
|
const atNoon = civilParts(dtf, noonGuess);
|
|
38109
38189
|
const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
|
|
38110
38190
|
const at = (minutes) => naive + minutes * MINUTE_MS - offset;
|
|
38191
|
+
if (spec.overnight) {
|
|
38192
|
+
if (!sunday) {
|
|
38193
|
+
extended.push([at(0), at(spec.regular.start)]);
|
|
38194
|
+
if (spec.regular.end < spec.extended.end) extended.push([at(spec.regular.end), at(spec.extended.end)]);
|
|
38195
|
+
}
|
|
38196
|
+
if (civil.weekday !== "Fri") extended.push([at(spec.extended.start), at(1440)]);
|
|
38197
|
+
continue;
|
|
38198
|
+
}
|
|
38111
38199
|
if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
|
|
38112
38200
|
if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
|
|
38113
38201
|
}
|
|
38114
|
-
return { pre, post };
|
|
38202
|
+
return { pre, post, extended: mergeAbutting(extended) };
|
|
38115
38203
|
}
|
|
38116
38204
|
var COVER_PAD_MIN_MS = 2 * DAY_MS2;
|
|
38117
38205
|
var SessionShadingTracker = class {
|
|
@@ -38166,7 +38254,7 @@ var SessionShadingTracker = class {
|
|
|
38166
38254
|
return;
|
|
38167
38255
|
}
|
|
38168
38256
|
if (this.session !== "extended" || !this.intraday) {
|
|
38169
|
-
if (force) this.onZones({ pre: [], post: [] });
|
|
38257
|
+
if (force) this.onZones({ pre: [], post: [], extended: [] });
|
|
38170
38258
|
return;
|
|
38171
38259
|
}
|
|
38172
38260
|
const from = Math.min(range.from, range.to);
|
|
@@ -38180,6 +38268,112 @@ var SessionShadingTracker = class {
|
|
|
38180
38268
|
}
|
|
38181
38269
|
};
|
|
38182
38270
|
|
|
38271
|
+
// src/widget/market-status.ts
|
|
38272
|
+
function civilDate(ms, tz) {
|
|
38273
|
+
try {
|
|
38274
|
+
return new Intl.DateTimeFormat("en-CA", { timeZone: tz }).format(ms);
|
|
38275
|
+
} catch {
|
|
38276
|
+
return "";
|
|
38277
|
+
}
|
|
38278
|
+
}
|
|
38279
|
+
function isWeekday(ms, tz) {
|
|
38280
|
+
try {
|
|
38281
|
+
const wd = new Intl.DateTimeFormat("en-US", { timeZone: tz, weekday: "short" }).format(ms);
|
|
38282
|
+
return wd !== "Sat" && wd !== "Sun";
|
|
38283
|
+
} catch {
|
|
38284
|
+
return false;
|
|
38285
|
+
}
|
|
38286
|
+
}
|
|
38287
|
+
function deriveMarketStatus(now, w, tz, overnight = false) {
|
|
38288
|
+
const within = (ws) => ws.find(([s, e]) => now >= s && now < e);
|
|
38289
|
+
if (within(w.regular)) return "open";
|
|
38290
|
+
const ext = within(w.extended);
|
|
38291
|
+
if (ext) {
|
|
38292
|
+
if (overnight) return "extended";
|
|
38293
|
+
return w.regular.some(([s]) => s >= now && s < ext[1]) ? "pre" : "post";
|
|
38294
|
+
}
|
|
38295
|
+
if (isWeekday(now, tz)) {
|
|
38296
|
+
const today = civilDate(now, tz);
|
|
38297
|
+
if (today !== "" && !w.extended.some(([s]) => civilDate(s, tz) === today)) return "holiday";
|
|
38298
|
+
}
|
|
38299
|
+
return "closed";
|
|
38300
|
+
}
|
|
38301
|
+
function nextStatusBoundary(now, w) {
|
|
38302
|
+
let next = null;
|
|
38303
|
+
for (const ws of [w.regular, w.extended]) {
|
|
38304
|
+
for (const [s, e] of ws) {
|
|
38305
|
+
for (const t of [s, e]) {
|
|
38306
|
+
if (t > now && (next == null || t < next)) next = t;
|
|
38307
|
+
}
|
|
38308
|
+
}
|
|
38309
|
+
}
|
|
38310
|
+
return next;
|
|
38311
|
+
}
|
|
38312
|
+
var FETCH_BACK_MS = 4 * 864e5;
|
|
38313
|
+
var FETCH_AHEAD_MS = 10 * 864e5;
|
|
38314
|
+
var MIN_TIMER_MS = 15e3;
|
|
38315
|
+
var MAX_TIMER_MS = 36e5;
|
|
38316
|
+
var RETRY_MS2 = 6e4;
|
|
38317
|
+
var MarketStatusTracker = class {
|
|
38318
|
+
constructor(onStatus) {
|
|
38319
|
+
this.onStatus = onStatus;
|
|
38320
|
+
/** Invalidates detached async work — bumped by every track()/stop(). */
|
|
38321
|
+
this.epoch = 0;
|
|
38322
|
+
this.timer = null;
|
|
38323
|
+
}
|
|
38324
|
+
/** (Re)bind to a chart's data surface + symbol and start evaluating. */
|
|
38325
|
+
track(data, symbol) {
|
|
38326
|
+
const my = ++this.epoch;
|
|
38327
|
+
this.clearTimer();
|
|
38328
|
+
void this.evaluate(my, data, symbol);
|
|
38329
|
+
}
|
|
38330
|
+
stop() {
|
|
38331
|
+
this.epoch += 1;
|
|
38332
|
+
this.clearTimer();
|
|
38333
|
+
}
|
|
38334
|
+
async evaluate(my, data, symbol) {
|
|
38335
|
+
const resolved = data.resolve(symbol);
|
|
38336
|
+
const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
|
|
38337
|
+
const si = await data.symbolInfo(symbol).catch(() => void 0);
|
|
38338
|
+
if (my !== this.epoch) return;
|
|
38339
|
+
const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
|
|
38340
|
+
if (!provider?.getCalendar || !hasSessions || !resolved) {
|
|
38341
|
+
this.onStatus("open");
|
|
38342
|
+
return;
|
|
38343
|
+
}
|
|
38344
|
+
const tz = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : "Etc/UTC";
|
|
38345
|
+
const now = Date.now();
|
|
38346
|
+
const range = { from: now - FETCH_BACK_MS, to: now + FETCH_AHEAD_MS };
|
|
38347
|
+
const [regular, extended] = await Promise.all([
|
|
38348
|
+
provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
|
|
38349
|
+
provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
|
|
38350
|
+
]);
|
|
38351
|
+
if (my !== this.epoch) return;
|
|
38352
|
+
if (!regular || !extended) {
|
|
38353
|
+
this.arm(my, data, symbol, RETRY_MS2);
|
|
38354
|
+
return;
|
|
38355
|
+
}
|
|
38356
|
+
const w = { regular, extended };
|
|
38357
|
+
const overnight = parseSessionSpec(si)?.overnight === true;
|
|
38358
|
+
this.onStatus(deriveMarketStatus(now, w, tz, overnight));
|
|
38359
|
+
const boundary = nextStatusBoundary(now, w);
|
|
38360
|
+
const delay = Math.min(boundary != null ? boundary - now : MAX_TIMER_MS, MAX_TIMER_MS);
|
|
38361
|
+
this.arm(my, data, symbol, Math.max(delay, MIN_TIMER_MS));
|
|
38362
|
+
}
|
|
38363
|
+
arm(my, data, symbol, delay) {
|
|
38364
|
+
this.clearTimer();
|
|
38365
|
+
this.timer = setTimeout(() => {
|
|
38366
|
+
this.timer = null;
|
|
38367
|
+
if (my !== this.epoch) return;
|
|
38368
|
+
void this.evaluate(my, data, symbol);
|
|
38369
|
+
}, delay);
|
|
38370
|
+
}
|
|
38371
|
+
clearTimer() {
|
|
38372
|
+
if (this.timer != null) clearTimeout(this.timer);
|
|
38373
|
+
this.timer = null;
|
|
38374
|
+
}
|
|
38375
|
+
};
|
|
38376
|
+
|
|
38183
38377
|
// src/widget/watermark.ts
|
|
38184
38378
|
var MAX_FONT_PX = 36;
|
|
38185
38379
|
var MIN_FONT_PX = 12;
|
|
@@ -38693,7 +38887,10 @@ var ChartCell = class {
|
|
|
38693
38887
|
this.activeRangeId = null;
|
|
38694
38888
|
/** Latched verdict of {@link sessionAvailable} (async metadata, sticky per symbol). */
|
|
38695
38889
|
this.sessionAvailableFlag = false;
|
|
38696
|
-
/**
|
|
38890
|
+
/** Latched: the symbol's extended tape wraps midnight (an overnight roll market) —
|
|
38891
|
+
* one extended-hours shading phase instead of the pre/post split. */
|
|
38892
|
+
this.sessionOvernightFlag = false;
|
|
38893
|
+
/** Keeps the session shading on the symbol's real calendar (see {@link SessionShadingTracker}). */
|
|
38697
38894
|
this.sessionShading = new SessionShadingTracker((zones) => this.inner?.renderer.set("sessionZones", zones));
|
|
38698
38895
|
this.manifest = [];
|
|
38699
38896
|
/** A restored ledger's manifest entry NAMES, waiting for the manifest to resolve
|
|
@@ -38946,8 +39143,10 @@ var ChartCell = class {
|
|
|
38946
39143
|
void chart.data.symbolInfo(symbol).then((si) => {
|
|
38947
39144
|
if (this.inner !== chart) return;
|
|
38948
39145
|
const available = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
|
|
38949
|
-
|
|
39146
|
+
const overnight = parseSessionSpec(si)?.overnight === true;
|
|
39147
|
+
if (available !== this.sessionAvailableFlag || overnight !== this.sessionOvernightFlag) {
|
|
38950
39148
|
this.sessionAvailableFlag = available;
|
|
39149
|
+
this.sessionOvernightFlag = overnight;
|
|
38951
39150
|
this.deps.onMarketChanged(this.id);
|
|
38952
39151
|
this.pushSettingsSections();
|
|
38953
39152
|
}
|
|
@@ -38981,16 +39180,41 @@ var ChartCell = class {
|
|
|
38981
39180
|
/**
|
|
38982
39181
|
* (Re)contribute this cell's settings-dialog sections: status line parts, the
|
|
38983
39182
|
* per-cell fetch depth, the watermark toggle, and — only while the cell's symbol
|
|
38984
|
-
* HAS sessions — the Trading session group (RTH/ETH switch + the
|
|
38985
|
-
*
|
|
38986
|
-
*
|
|
38987
|
-
* a
|
|
39183
|
+
* HAS sessions — the Trading session group (RTH/ETH switch + the session shading
|
|
39184
|
+
* colors: pre/post-market on day-split tapes, one extended-hours swatch on
|
|
39185
|
+
* overnight roll markets) inside the Symbol tab. Bars/watermark/titles are
|
|
39186
|
+
* persistable cell state; a depth-only reload is silent, so mark dirty here.
|
|
39187
|
+
* Re-run whenever a gate changes (the dialog reads the sections on open).
|
|
38988
39188
|
*/
|
|
38989
39189
|
pushSettingsSections() {
|
|
38990
39190
|
const chart = this.inner;
|
|
38991
39191
|
if (!chart) return;
|
|
38992
39192
|
const rth = "Regular hours (RTH)";
|
|
38993
39193
|
const eth = "Extended hours (ETH)";
|
|
39194
|
+
const shadeRows = this.sessionOvernightFlag ? [
|
|
39195
|
+
{
|
|
39196
|
+
kind: "color",
|
|
39197
|
+
label: "Extended hours",
|
|
39198
|
+
id: "extended-color",
|
|
39199
|
+
get: () => this.sessionShadeColor("extendedColor"),
|
|
39200
|
+
set: (v) => this.setSessionShadeColor("extendedColor", v)
|
|
39201
|
+
}
|
|
39202
|
+
] : [
|
|
39203
|
+
{
|
|
39204
|
+
kind: "color",
|
|
39205
|
+
label: "Pre-market",
|
|
39206
|
+
id: "premarket-color",
|
|
39207
|
+
get: () => this.sessionShadeColor("premarketColor"),
|
|
39208
|
+
set: (v) => this.setSessionShadeColor("premarketColor", v)
|
|
39209
|
+
},
|
|
39210
|
+
{
|
|
39211
|
+
kind: "color",
|
|
39212
|
+
label: "Post-market",
|
|
39213
|
+
id: "postmarket-color",
|
|
39214
|
+
get: () => this.sessionShadeColor("postmarketColor"),
|
|
39215
|
+
set: (v) => this.setSessionShadeColor("postmarketColor", v)
|
|
39216
|
+
}
|
|
39217
|
+
];
|
|
38994
39218
|
const sessionSection = {
|
|
38995
39219
|
title: "Trading session",
|
|
38996
39220
|
id: "trading-session",
|
|
@@ -39004,20 +39228,7 @@ var ChartCell = class {
|
|
|
39004
39228
|
get: () => this.session === "extended" ? eth : rth,
|
|
39005
39229
|
set: (v) => this.setSession(v === eth ? "extended" : "regular")
|
|
39006
39230
|
},
|
|
39007
|
-
|
|
39008
|
-
kind: "color",
|
|
39009
|
-
label: "Pre-market",
|
|
39010
|
-
id: "premarket-color",
|
|
39011
|
-
get: () => this.sessionShadeColor("premarketColor"),
|
|
39012
|
-
set: (v) => this.setSessionShadeColor("premarketColor", v)
|
|
39013
|
-
},
|
|
39014
|
-
{
|
|
39015
|
-
kind: "color",
|
|
39016
|
-
label: "Post-market",
|
|
39017
|
-
id: "postmarket-color",
|
|
39018
|
-
get: () => this.sessionShadeColor("postmarketColor"),
|
|
39019
|
-
set: (v) => this.setSessionShadeColor("postmarketColor", v)
|
|
39020
|
-
}
|
|
39231
|
+
...shadeRows
|
|
39021
39232
|
]
|
|
39022
39233
|
};
|
|
39023
39234
|
const advanced = {
|
|
@@ -39261,6 +39472,10 @@ var ChartCell = class {
|
|
|
39261
39472
|
this.deps.activate(this.id);
|
|
39262
39473
|
this.inner?.renderer.focus();
|
|
39263
39474
|
}
|
|
39475
|
+
/** Raster of this cell's chart (same pixels as the PNG download), or null. */
|
|
39476
|
+
screenshotCanvas() {
|
|
39477
|
+
return this.inner?.renderer.screenshotCanvas() ?? null;
|
|
39478
|
+
}
|
|
39264
39479
|
/** Download this cell's chart as a PNG (named after its market). */
|
|
39265
39480
|
downloadScreenshot() {
|
|
39266
39481
|
const url = this.inner?.renderer.screenshot();
|
|
@@ -39879,6 +40094,54 @@ var SplitterLayer = class {
|
|
|
39879
40094
|
}
|
|
39880
40095
|
};
|
|
39881
40096
|
|
|
40097
|
+
// src/workspace/screenshot.ts
|
|
40098
|
+
function destRect(tile, dpr) {
|
|
40099
|
+
const dx = Math.round(tile.x * dpr);
|
|
40100
|
+
const dy = Math.round(tile.y * dpr);
|
|
40101
|
+
return {
|
|
40102
|
+
dx,
|
|
40103
|
+
dy,
|
|
40104
|
+
dw: Math.round((tile.x + tile.width) * dpr) - dx,
|
|
40105
|
+
dh: Math.round((tile.y + tile.height) * dpr) - dy
|
|
40106
|
+
};
|
|
40107
|
+
}
|
|
40108
|
+
function tilesFromCellRects(grid, cells) {
|
|
40109
|
+
if (grid.width <= 0 || grid.height <= 0) return [];
|
|
40110
|
+
const tiles = [];
|
|
40111
|
+
for (const c of cells) {
|
|
40112
|
+
if (c.hidden || c.width <= 0 || c.height <= 0) continue;
|
|
40113
|
+
tiles.push({ x: c.left - grid.left, y: c.top - grid.top, width: c.width, height: c.height });
|
|
40114
|
+
}
|
|
40115
|
+
return tiles;
|
|
40116
|
+
}
|
|
40117
|
+
function compositeLayoutScreenshot(doc, frame, tiles) {
|
|
40118
|
+
if (frame.width <= 0 || frame.height <= 0 || tiles.length === 0) return null;
|
|
40119
|
+
const dpr = frame.dpr > 0 ? frame.dpr : 1;
|
|
40120
|
+
const out = doc.createElement("canvas");
|
|
40121
|
+
out.width = Math.max(1, Math.round(frame.width * dpr));
|
|
40122
|
+
out.height = Math.max(1, Math.round(frame.height * dpr));
|
|
40123
|
+
const ctx = out.getContext("2d");
|
|
40124
|
+
if (!ctx) return null;
|
|
40125
|
+
ctx.fillStyle = frame.gapColor;
|
|
40126
|
+
ctx.fillRect(0, 0, out.width, out.height);
|
|
40127
|
+
let painted = 0;
|
|
40128
|
+
for (const tile of tiles) {
|
|
40129
|
+
if (tile.width <= 0 || tile.height <= 0) continue;
|
|
40130
|
+
const { dx, dy, dw, dh } = destRect(tile, dpr);
|
|
40131
|
+
if (dw <= 0 || dh <= 0) continue;
|
|
40132
|
+
ctx.drawImage(tile.source, dx, dy, dw, dh);
|
|
40133
|
+
painted += 1;
|
|
40134
|
+
}
|
|
40135
|
+
if (painted === 0) return null;
|
|
40136
|
+
return out.toDataURL("image/png");
|
|
40137
|
+
}
|
|
40138
|
+
function triggerPngDownload(doc, url, filename) {
|
|
40139
|
+
const a = doc.createElement("a");
|
|
40140
|
+
a.href = url;
|
|
40141
|
+
a.download = filename;
|
|
40142
|
+
a.click();
|
|
40143
|
+
}
|
|
40144
|
+
|
|
39882
40145
|
// src/workspace/VelaWorkspace.ts
|
|
39883
40146
|
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "30", "60", "240", "D", "W", "M"];
|
|
39884
40147
|
var GAP_PX = 2;
|
|
@@ -40109,7 +40372,7 @@ var VelaWorkspace = class {
|
|
|
40109
40372
|
...picker ? { onIndicatorsClick: () => picker.open() } : {},
|
|
40110
40373
|
onUndoClick: () => this.active.history.undo(),
|
|
40111
40374
|
onRedoClick: () => this.active.history.redo(),
|
|
40112
|
-
onScreenshotClick: () => this.
|
|
40375
|
+
onScreenshotClick: () => this.downloadScreenshot(),
|
|
40113
40376
|
onAlertsClick: (anchor) => this.openAlertsMenu(anchor),
|
|
40114
40377
|
timeframe: "60",
|
|
40115
40378
|
timeframes: opts.timeframes ?? DEFAULT_TIMEFRAMES,
|
|
@@ -40168,6 +40431,7 @@ var VelaWorkspace = class {
|
|
|
40168
40431
|
this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
|
|
40169
40432
|
this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
|
|
40170
40433
|
this.dock.refresh();
|
|
40434
|
+
if (boot?.panels) this.dock.applyState(boot.panels);
|
|
40171
40435
|
this.root.appendChild(main);
|
|
40172
40436
|
this.toastHost = new Toast(this.gridEl);
|
|
40173
40437
|
const attribution = rendererDefaults().attribution;
|
|
@@ -40179,6 +40443,7 @@ var VelaWorkspace = class {
|
|
|
40179
40443
|
bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H4}px) + 10px)`,
|
|
40180
40444
|
zIndex: "11"
|
|
40181
40445
|
});
|
|
40446
|
+
mark.dataset.velaScreenshot = "1";
|
|
40182
40447
|
this.gridEl.appendChild(mark);
|
|
40183
40448
|
this.attributionMark = mark;
|
|
40184
40449
|
}
|
|
@@ -40315,6 +40580,56 @@ var VelaWorkspace = class {
|
|
|
40315
40580
|
get chart() {
|
|
40316
40581
|
return this.active.chart;
|
|
40317
40582
|
}
|
|
40583
|
+
/**
|
|
40584
|
+
* PNG data URL of the visible layout: every live cell in its grid slot, or the
|
|
40585
|
+
* maximized cell alone. Same pixels the screenshot button downloads. A single
|
|
40586
|
+
* visible cell returns that cell's own chart export (the one-chart case).
|
|
40587
|
+
*/
|
|
40588
|
+
screenshot() {
|
|
40589
|
+
const cells = this.shotCells();
|
|
40590
|
+
if (cells.length === 0) return null;
|
|
40591
|
+
if (cells.length === 1) return cells[0].chart.renderer.screenshot();
|
|
40592
|
+
return this.compositeLayoutShot(cells);
|
|
40593
|
+
}
|
|
40594
|
+
/** Download {@link screenshot} as a PNG. A multi-cell layout is named
|
|
40595
|
+
* `vela-layout.png`; one visible cell keeps `${symbol}-${timeframe}.png`. */
|
|
40596
|
+
downloadScreenshot() {
|
|
40597
|
+
const cells = this.shotCells();
|
|
40598
|
+
if (cells.length <= 1) {
|
|
40599
|
+
(cells[0] ?? this.active).downloadScreenshot();
|
|
40600
|
+
return;
|
|
40601
|
+
}
|
|
40602
|
+
const url = this.compositeLayoutShot(cells);
|
|
40603
|
+
if (!url) return;
|
|
40604
|
+
triggerPngDownload(this.root.ownerDocument, url, "vela-layout.png");
|
|
40605
|
+
}
|
|
40606
|
+
/** Live cells the screenshot should include — maximized siblings are `hidden`. */
|
|
40607
|
+
shotCells() {
|
|
40608
|
+
return this.cells().filter((c) => c.host.style.visibility !== "hidden");
|
|
40609
|
+
}
|
|
40610
|
+
/** Place every cell's raster onto one canvas the size of the grid. */
|
|
40611
|
+
compositeLayoutShot(cells) {
|
|
40612
|
+
const gridEl = this.gridEl;
|
|
40613
|
+
const grid = gridEl.getBoundingClientRect();
|
|
40614
|
+
const win = gridEl.ownerDocument.defaultView;
|
|
40615
|
+
const dpr = win?.devicePixelRatio ?? 1;
|
|
40616
|
+
const gapColor = win ? win.getComputedStyle(gridEl).backgroundColor : "";
|
|
40617
|
+
const tiles = [];
|
|
40618
|
+
for (const cell of cells) {
|
|
40619
|
+
const source = cell.screenshotCanvas();
|
|
40620
|
+
if (!source) continue;
|
|
40621
|
+
const r = cell.host.getBoundingClientRect();
|
|
40622
|
+
const place = tilesFromCellRects(grid, [{ left: r.left, top: r.top, width: r.width, height: r.height }])[0];
|
|
40623
|
+
if (!place) continue;
|
|
40624
|
+
tiles.push({ source, ...place });
|
|
40625
|
+
}
|
|
40626
|
+
return compositeLayoutScreenshot(gridEl.ownerDocument, {
|
|
40627
|
+
width: grid.width,
|
|
40628
|
+
height: grid.height,
|
|
40629
|
+
dpr,
|
|
40630
|
+
gapColor: gapColor || "#000000"
|
|
40631
|
+
}, tiles);
|
|
40632
|
+
}
|
|
40318
40633
|
setActiveCell(id) {
|
|
40319
40634
|
if (id === this.activeId || this.destroyed) return;
|
|
40320
40635
|
const prev2 = this.activeId;
|
|
@@ -41361,7 +41676,7 @@ var VelaWorkspace = class {
|
|
|
41361
41676
|
this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
|
|
41362
41677
|
host: this.root,
|
|
41363
41678
|
...has("undo-redo") ? { onUndo: () => this.active.history.undo(), onRedo: () => this.active.history.redo() } : {},
|
|
41364
|
-
...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.
|
|
41679
|
+
...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.downloadScreenshot() } : {},
|
|
41365
41680
|
canUndo: () => this.active.history.canUndo,
|
|
41366
41681
|
canRedo: () => this.active.history.canRedo,
|
|
41367
41682
|
priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
|
|
@@ -41462,9 +41777,9 @@ var VelaWorkspace = class {
|
|
|
41462
41777
|
this.keymap.register({
|
|
41463
41778
|
id: "chart.screenshot",
|
|
41464
41779
|
keys: "mod+alt+s",
|
|
41465
|
-
label: ov ? ov.label : "Download a
|
|
41780
|
+
label: ov ? ov.label : "Download a screenshot of the layout",
|
|
41466
41781
|
category: "Chart",
|
|
41467
|
-
run: ov ? () => this.runOverride(ov) : () => this.
|
|
41782
|
+
run: ov ? () => this.runOverride(ov) : () => this.downloadScreenshot()
|
|
41468
41783
|
});
|
|
41469
41784
|
}
|
|
41470
41785
|
this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
|