@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/widget.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"/>'));
|
|
@@ -5022,6 +5035,7 @@ var BASELINE_FILL_ALPHA_FAR = 0.05;
|
|
|
5022
5035
|
var BASELINE_LEVEL_DEFAULT = 50;
|
|
5023
5036
|
var PREMARKET_SHADE = withAlpha(WARNING, 0.08);
|
|
5024
5037
|
var POSTMARKET_SHADE = withAlpha(ACCENT, 0.08);
|
|
5038
|
+
var EXTENDED_SHADE = POSTMARKET_SHADE;
|
|
5025
5039
|
function defaultChartStyle() {
|
|
5026
5040
|
return {
|
|
5027
5041
|
chartTypes: {},
|
|
@@ -5030,7 +5044,7 @@ function defaultChartStyle() {
|
|
|
5030
5044
|
gridHorz: { visible: true, color: null },
|
|
5031
5045
|
borderColor: null,
|
|
5032
5046
|
separatorColor: null,
|
|
5033
|
-
crosshair: { color: CROSSHAIR, width: 1, style: "dashed", opacity: 0.4, labelBackground:
|
|
5047
|
+
crosshair: { color: CROSSHAIR, width: 1, style: "dashed", opacity: 0.4, labelBackground: CHIP_PLATE },
|
|
5034
5048
|
candle: {
|
|
5035
5049
|
bodyVisible: true,
|
|
5036
5050
|
borderVisible: false,
|
|
@@ -5053,7 +5067,7 @@ function defaultChartStyle() {
|
|
|
5053
5067
|
width: 2,
|
|
5054
5068
|
baselineLevel: BASELINE_LEVEL_DEFAULT
|
|
5055
5069
|
},
|
|
5056
|
-
sessions: { premarketColor: PREMARKET_SHADE, postmarketColor: POSTMARKET_SHADE }
|
|
5070
|
+
sessions: { premarketColor: PREMARKET_SHADE, postmarketColor: POSTMARKET_SHADE, extendedColor: EXTENDED_SHADE }
|
|
5057
5071
|
};
|
|
5058
5072
|
}
|
|
5059
5073
|
var LINE_STYLES = ["solid", "dashed", "dotted"];
|
|
@@ -5282,7 +5296,8 @@ function mergeConfig(base, patch) {
|
|
|
5282
5296
|
},
|
|
5283
5297
|
sessions: {
|
|
5284
5298
|
premarketColor: isColor(sessions.premarketColor) ? sessions.premarketColor : base.sessions.premarketColor,
|
|
5285
|
-
postmarketColor: isColor(sessions.postmarketColor) ? sessions.postmarketColor : base.sessions.postmarketColor
|
|
5299
|
+
postmarketColor: isColor(sessions.postmarketColor) ? sessions.postmarketColor : base.sessions.postmarketColor,
|
|
5300
|
+
extendedColor: isColor(sessions.extendedColor) ? sessions.extendedColor : base.sessions.extendedColor
|
|
5286
5301
|
},
|
|
5287
5302
|
stacking: {
|
|
5288
5303
|
candles: isNum(stacking.candles) ? stacking.candles : base.stacking.candles,
|
|
@@ -12278,6 +12293,22 @@ var CSS5 = `
|
|
|
12278
12293
|
}
|
|
12279
12294
|
.vela-panel-close .vela-icon { width: 16px; height: 16px; }
|
|
12280
12295
|
.vela-panel-close:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
|
|
12296
|
+
/* The pin: same footprint as the close button it sits beside; filled + bright once pinned. */
|
|
12297
|
+
.vela-panel-pin {
|
|
12298
|
+
all: unset;
|
|
12299
|
+
cursor: pointer;
|
|
12300
|
+
width: 26px;
|
|
12301
|
+
height: 26px;
|
|
12302
|
+
flex: none;
|
|
12303
|
+
display: inline-flex;
|
|
12304
|
+
align-items: center;
|
|
12305
|
+
justify-content: center;
|
|
12306
|
+
border-radius: 4px;
|
|
12307
|
+
color: var(--vela-fg-muted);
|
|
12308
|
+
}
|
|
12309
|
+
.vela-panel-pin .vela-icon { width: 15px; height: 15px; }
|
|
12310
|
+
.vela-panel-pin:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
|
|
12311
|
+
.vela-panel-pin[data-on='1'] { color: var(--vela-fg-bright); }
|
|
12281
12312
|
.vela-panel-body { flex: 1; overflow: auto; padding: 8px; }
|
|
12282
12313
|
.vela-panel-body::-webkit-scrollbar { width: 8px; }
|
|
12283
12314
|
.vela-panel-body::-webkit-scrollbar-thumb { background: var(--vela-scroll); border-radius: 4px; border: 2px solid transparent; background-clip: padding-box; }
|
|
@@ -12302,8 +12333,23 @@ var CSS5 = `
|
|
|
12302
12333
|
width: 1px;
|
|
12303
12334
|
background: transparent;
|
|
12304
12335
|
}
|
|
12336
|
+
/* The same hover ink as the workspace grid splitters and the canvas pane separators, so
|
|
12337
|
+
every draggable seam in the shell reads as one family. */
|
|
12305
12338
|
.vela-panel-resizer:hover::after,
|
|
12306
|
-
.vela-panel-resizer[data-dragging]::after { background: var(--vela-
|
|
12339
|
+
.vela-panel-resizer[data-dragging]::after { background: var(--vela-separator-hover-line); }
|
|
12340
|
+
/* An OVERLAY panel floats over the chart's right edge instead of taking a column: the
|
|
12341
|
+
chart keeps its width and layout (the dock host is position:relative \u2014 the anchoring the
|
|
12342
|
+
mobile rule below relies on too). Same z-order as that mobile overlay, above the chart's
|
|
12343
|
+
own DOM overlays; the shadow separates it from the plot it covers. */
|
|
12344
|
+
.vela-panel[data-overlay] {
|
|
12345
|
+
position: absolute;
|
|
12346
|
+
top: 0;
|
|
12347
|
+
right: 0;
|
|
12348
|
+
bottom: 0;
|
|
12349
|
+
max-width: 100%;
|
|
12350
|
+
z-index: 25;
|
|
12351
|
+
box-shadow: var(--vela-shadow);
|
|
12352
|
+
}
|
|
12307
12353
|
/* Mobile: a 280px column would crush a phone-width chart \u2014 the panel overlays the
|
|
12308
12354
|
chart area instead (its flex parent is position:relative), full-bleed, closed by
|
|
12309
12355
|
the same header \u2715. Width dragging is a pointer affordance; off on mobile. */
|
|
@@ -12334,6 +12380,10 @@ var SidePanel = class {
|
|
|
12334
12380
|
/** Notified when the USER settles a new width (drag release, or double-click reset) — never on
|
|
12335
12381
|
* a programmatic {@link setWidth}, so restoring a persisted width raises no change. */
|
|
12336
12382
|
this.onWidthChange = null;
|
|
12383
|
+
/** Notified when the USER pins or unpins a floating panel (`overlay` is the NEW placement) —
|
|
12384
|
+
* never on a programmatic {@link setOverlay}, for the same reason as widths. */
|
|
12385
|
+
this.onPlacementChange = null;
|
|
12386
|
+
this.pin = null;
|
|
12337
12387
|
const doc = host.ownerDocument;
|
|
12338
12388
|
injectStyles(STYLE_ID4, CSS5, doc);
|
|
12339
12389
|
this.minWidth = opts.resizable ? opts.minWidth ?? DEFAULT_PANEL_MIN_WIDTH : 1;
|
|
@@ -12343,6 +12393,9 @@ var SidePanel = class {
|
|
|
12343
12393
|
this.el = doc.createElement("div");
|
|
12344
12394
|
this.el.className = `vela-panel ${modifier}`;
|
|
12345
12395
|
this.el.hidden = true;
|
|
12396
|
+
this.floatable = opts.overlay === true;
|
|
12397
|
+
this.overlayOn = this.floatable;
|
|
12398
|
+
if (this.overlayOn) this.el.dataset.overlay = "1";
|
|
12346
12399
|
this.el.style.setProperty("--vela-panel-w", `${this.widthPx}px`);
|
|
12347
12400
|
const header = doc.createElement("div");
|
|
12348
12401
|
header.className = "vela-panel-header";
|
|
@@ -12356,7 +12409,16 @@ var SidePanel = class {
|
|
|
12356
12409
|
close.appendChild(iconEl("close", doc));
|
|
12357
12410
|
close.title = "Close";
|
|
12358
12411
|
close.addEventListener("click", () => this.toggle(false));
|
|
12359
|
-
|
|
12412
|
+
if (this.floatable) {
|
|
12413
|
+
this.pin = doc.createElement("button");
|
|
12414
|
+
this.pin.className = "vela-panel-pin";
|
|
12415
|
+
this.pin.addEventListener("click", () => {
|
|
12416
|
+
this.setOverlay(!this.overlayOn);
|
|
12417
|
+
this.onPlacementChange?.(this.overlayOn);
|
|
12418
|
+
});
|
|
12419
|
+
this.refreshPin();
|
|
12420
|
+
}
|
|
12421
|
+
header.append(this.heading, this.slot, ...this.pin ? [this.pin] : [], close);
|
|
12360
12422
|
this.body = doc.createElement("div");
|
|
12361
12423
|
this.body.className = "vela-panel-body";
|
|
12362
12424
|
this.el.append(header, this.body);
|
|
@@ -12398,6 +12460,29 @@ var SidePanel = class {
|
|
|
12398
12460
|
this.widthPx = next;
|
|
12399
12461
|
this.el.style.setProperty("--vela-panel-w", `${next}px`);
|
|
12400
12462
|
}
|
|
12463
|
+
/** Whether the panel floats over the chart right now (false for every docked panel). */
|
|
12464
|
+
get overlay() {
|
|
12465
|
+
return this.overlayOn;
|
|
12466
|
+
}
|
|
12467
|
+
/** Float or dock the panel. Only a panel declared `overlay` can float — on any other this is
|
|
12468
|
+
* a no-op. Silent — {@link onPlacementChange} reports the user's pin clicks only. */
|
|
12469
|
+
setOverlay(overlay) {
|
|
12470
|
+
if (!this.floatable || overlay === this.overlayOn) return;
|
|
12471
|
+
this.overlayOn = overlay;
|
|
12472
|
+
if (overlay) this.el.dataset.overlay = "1";
|
|
12473
|
+
else delete this.el.dataset.overlay;
|
|
12474
|
+
this.refreshPin();
|
|
12475
|
+
}
|
|
12476
|
+
refreshPin() {
|
|
12477
|
+
if (!this.pin) return;
|
|
12478
|
+
const pinned = !this.overlayOn;
|
|
12479
|
+
const doc = this.pin.ownerDocument;
|
|
12480
|
+
this.pin.replaceChildren(iconEl(pinned ? "pin-filled" : "pin", doc));
|
|
12481
|
+
this.pin.dataset.on = pinned ? "1" : "0";
|
|
12482
|
+
this.pin.title = pinned ? "Unpin \u2014 float over the chart" : "Pin beside the chart";
|
|
12483
|
+
this.pin.setAttribute("aria-label", this.pin.title);
|
|
12484
|
+
this.pin.setAttribute("aria-pressed", pinned ? "true" : "false");
|
|
12485
|
+
}
|
|
12401
12486
|
destroy() {
|
|
12402
12487
|
this.el.remove();
|
|
12403
12488
|
}
|
|
@@ -13798,6 +13883,8 @@ var PanelDock = class {
|
|
|
13798
13883
|
this.entries = [];
|
|
13799
13884
|
/** Widths the USER settled, by panel id — the only ones worth persisting. */
|
|
13800
13885
|
this.widths = /* @__PURE__ */ new Map();
|
|
13886
|
+
/** Floating panels the USER pinned as columns, by id — remembered for late registrations too. */
|
|
13887
|
+
this.pinned = /* @__PURE__ */ new Set();
|
|
13801
13888
|
/** A restored `open` naming a panel that has not registered yet: honored when it docks. */
|
|
13802
13889
|
this.pendingOpen = null;
|
|
13803
13890
|
this.chart = null;
|
|
@@ -13820,7 +13907,8 @@ var PanelDock = class {
|
|
|
13820
13907
|
width: desc.width,
|
|
13821
13908
|
resizable: desc.resizable,
|
|
13822
13909
|
minWidth: desc.minWidth,
|
|
13823
|
-
maxWidth: desc.maxWidth
|
|
13910
|
+
maxWidth: desc.maxWidth,
|
|
13911
|
+
overlay: desc.overlay
|
|
13824
13912
|
});
|
|
13825
13913
|
const entry = {
|
|
13826
13914
|
id: desc.id,
|
|
@@ -13868,7 +13956,8 @@ var PanelDock = class {
|
|
|
13868
13956
|
const open2 = this.openId;
|
|
13869
13957
|
if (open2) out.open = open2;
|
|
13870
13958
|
if (this.widths.size > 0) out.widths = Object.fromEntries(this.widths);
|
|
13871
|
-
|
|
13959
|
+
if (this.pinned.size > 0) out.pinned = [...this.pinned];
|
|
13960
|
+
return out.open || out.widths || out.pinned ? out : null;
|
|
13872
13961
|
}
|
|
13873
13962
|
/**
|
|
13874
13963
|
* Restore a persisted dock state. Widths apply to any panel present, and are remembered for
|
|
@@ -13876,7 +13965,8 @@ var PanelDock = class {
|
|
|
13876
13965
|
* a document that predates the dock has no `panels` field at all, so the shell never calls
|
|
13877
13966
|
* this and the default (everything closed) stands. An `open` naming a panel that has not
|
|
13878
13967
|
* registered yet is held until it docks (a plugin loaded after the restore), unless the user
|
|
13879
|
-
* opens something in the meantime.
|
|
13968
|
+
* opens something in the meantime. `pinned` is the whole list of floating panels the user
|
|
13969
|
+
* docked — its absence means none is, so every floatable panel floats again.
|
|
13880
13970
|
*/
|
|
13881
13971
|
applyState(state) {
|
|
13882
13972
|
if (!state) return;
|
|
@@ -13886,6 +13976,8 @@ var PanelDock = class {
|
|
|
13886
13976
|
this.entries.find((e) => e.id === id)?.panel.setWidth(px);
|
|
13887
13977
|
}
|
|
13888
13978
|
}
|
|
13979
|
+
this.pinned = new Set(state.pinned ?? []);
|
|
13980
|
+
for (const entry of this.entries) entry.panel.setOverlay(!this.pinned.has(entry.id));
|
|
13889
13981
|
for (const entry of this.entries) entry.panel.toggle(entry.id === state.open);
|
|
13890
13982
|
this.pendingOpen = state.open && !this.entries.some((e) => e.id === state.open) ? state.open : null;
|
|
13891
13983
|
}
|
|
@@ -13899,6 +13991,7 @@ var PanelDock = class {
|
|
|
13899
13991
|
this.entries.sort((a, b) => a.order - b.order);
|
|
13900
13992
|
const stored = this.widths.get(entry.id);
|
|
13901
13993
|
if (stored !== void 0) entry.panel.setWidth(stored);
|
|
13994
|
+
if (this.pinned.has(entry.id)) entry.panel.setOverlay(false);
|
|
13902
13995
|
entry.panel.onOpenChange = (open2) => {
|
|
13903
13996
|
if (open2) {
|
|
13904
13997
|
for (const other of this.entries) if (other !== entry) other.panel.toggle(false);
|
|
@@ -13912,6 +14005,11 @@ var PanelDock = class {
|
|
|
13912
14005
|
this.widths.set(entry.id, px);
|
|
13913
14006
|
this.deps.changed?.();
|
|
13914
14007
|
};
|
|
14008
|
+
entry.panel.onPlacementChange = (overlay) => {
|
|
14009
|
+
if (overlay) this.pinned.delete(entry.id);
|
|
14010
|
+
else this.pinned.add(entry.id);
|
|
14011
|
+
this.deps.changed?.();
|
|
14012
|
+
};
|
|
13915
14013
|
if (this.pendingOpen === entry.id) entry.panel.toggle(true);
|
|
13916
14014
|
if (!entry.contributed) this.publish();
|
|
13917
14015
|
}
|
|
@@ -14000,6 +14098,17 @@ function filterSymbols(list, query, limit = 100, top = TOP_TICKERS) {
|
|
|
14000
14098
|
}
|
|
14001
14099
|
var STYLE_ID7 = "vela-widget-symbolpicker";
|
|
14002
14100
|
var CSS8 = `
|
|
14101
|
+
/* Search + market tabs stay pinned while the result list scrolls underneath
|
|
14102
|
+
(mobile fullscreen: the dialog body is the scroller). Negative margin eats
|
|
14103
|
+
the body's padding so scrolled rows cannot peek around the island. */
|
|
14104
|
+
.vela-sp-sticky {
|
|
14105
|
+
position: sticky;
|
|
14106
|
+
top: 0;
|
|
14107
|
+
z-index: 1;
|
|
14108
|
+
background: var(--vela-surface);
|
|
14109
|
+
margin: calc(-1 * var(--vela-space-4)) calc(-1 * var(--vela-space-4)) 0;
|
|
14110
|
+
padding: var(--vela-space-4) var(--vela-space-4) 0;
|
|
14111
|
+
}
|
|
14003
14112
|
.vela-sp-searchrow {
|
|
14004
14113
|
display: flex;
|
|
14005
14114
|
align-items: center;
|
|
@@ -14019,8 +14128,10 @@ var CSS8 = `
|
|
|
14019
14128
|
border: none;
|
|
14020
14129
|
font-size: 14px;
|
|
14021
14130
|
outline: none;
|
|
14131
|
+
text-transform: uppercase;
|
|
14022
14132
|
}
|
|
14023
|
-
.vela-sp-
|
|
14133
|
+
.vela-sp-input::placeholder { text-transform: none; }
|
|
14134
|
+
.vela-sp-tabs { display: flex; gap: 14px; margin: 12px 2px 0; border-bottom: 1px solid var(--vela-border); padding-bottom: 8px; }
|
|
14024
14135
|
/* Mobile (fullscreen dialog): the asset-class strip scrolls sideways instead of
|
|
14025
14136
|
overflowing the body, and the result list stops capping itself \u2014 the body owns
|
|
14026
14137
|
the scrolling in the fullscreen presentation. */
|
|
@@ -14069,7 +14180,7 @@ var CSS8 = `
|
|
|
14069
14180
|
font-weight: 700;
|
|
14070
14181
|
}
|
|
14071
14182
|
.vela-sp-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
|
|
14072
|
-
.vela-sp-ticker { font-weight: 700; color: var(--vela-fg-bright); font-size: 14px; }
|
|
14183
|
+
.vela-sp-ticker { font-weight: 700; color: var(--vela-fg-bright); font-size: 14px; text-transform: uppercase; }
|
|
14073
14184
|
.vela-sp-desc { color: var(--vela-fg-muted); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
14074
14185
|
.vela-sp-badge {
|
|
14075
14186
|
flex: none;
|
|
@@ -14132,6 +14243,8 @@ var SymbolPicker = class {
|
|
|
14132
14243
|
searchRow.append(iconEl("search", doc), this.input);
|
|
14133
14244
|
this.tabs = doc.createElement("div");
|
|
14134
14245
|
this.tabs.className = "vela-sp-tabs";
|
|
14246
|
+
const sticky = doc.createElement("div");
|
|
14247
|
+
sticky.className = "vela-sp-sticky";
|
|
14135
14248
|
for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
|
|
14136
14249
|
const b = doc.createElement("button");
|
|
14137
14250
|
b.className = "vela-sp-tab";
|
|
@@ -14145,6 +14258,7 @@ var SymbolPicker = class {
|
|
|
14145
14258
|
});
|
|
14146
14259
|
this.tabs.appendChild(b);
|
|
14147
14260
|
}
|
|
14261
|
+
sticky.append(searchRow, this.tabs);
|
|
14148
14262
|
this.list = doc.createElement("div");
|
|
14149
14263
|
this.list.className = "vela-sp-list";
|
|
14150
14264
|
this.list.addEventListener("scroll", () => {
|
|
@@ -14157,21 +14271,32 @@ var SymbolPicker = class {
|
|
|
14157
14271
|
title: "Symbol Search",
|
|
14158
14272
|
host: opts.host,
|
|
14159
14273
|
closeOnInteractOutside: true,
|
|
14160
|
-
content: (body) => body.append(
|
|
14274
|
+
content: (body) => body.append(sticky, this.list),
|
|
14161
14275
|
onOpenChange: (open2) => {
|
|
14162
14276
|
if (open2) {
|
|
14163
|
-
this.input.value = this.seed;
|
|
14277
|
+
this.input.value = this.seed.toUpperCase();
|
|
14164
14278
|
this.seed = "";
|
|
14165
14279
|
this.refresh();
|
|
14166
14280
|
setTimeout(() => {
|
|
14167
14281
|
this.input.focus();
|
|
14168
14282
|
this.input.setSelectionRange(this.input.value.length, this.input.value.length);
|
|
14169
14283
|
}, 0);
|
|
14284
|
+
} else {
|
|
14285
|
+
this.input.blur();
|
|
14170
14286
|
}
|
|
14171
14287
|
opts.onOpenChange?.(open2);
|
|
14172
14288
|
}
|
|
14173
14289
|
});
|
|
14174
|
-
this.input.addEventListener("input", () =>
|
|
14290
|
+
this.input.addEventListener("input", () => {
|
|
14291
|
+
const next = this.input.value.toUpperCase();
|
|
14292
|
+
if (this.input.value !== next) {
|
|
14293
|
+
const start = this.input.selectionStart;
|
|
14294
|
+
const end = this.input.selectionEnd;
|
|
14295
|
+
this.input.value = next;
|
|
14296
|
+
if (start != null && end != null) this.input.setSelectionRange(start, end);
|
|
14297
|
+
}
|
|
14298
|
+
this.refresh();
|
|
14299
|
+
});
|
|
14175
14300
|
this.input.addEventListener("keydown", (e) => {
|
|
14176
14301
|
if (e.key === "ArrowDown") this.moveHighlight(1);
|
|
14177
14302
|
else if (e.key === "ArrowUp") this.moveHighlight(-1);
|
|
@@ -17806,6 +17931,7 @@ function createCustomMark(doc, html, background) {
|
|
|
17806
17931
|
});
|
|
17807
17932
|
el.style.color = attributionMarkColor(background);
|
|
17808
17933
|
el.innerHTML = html;
|
|
17934
|
+
el.dataset.velaScreenshot = "1";
|
|
17809
17935
|
return el;
|
|
17810
17936
|
}
|
|
17811
17937
|
function createAttributionMark(doc, background) {
|
|
@@ -17824,6 +17950,7 @@ function createAttributionMark(doc, background) {
|
|
|
17824
17950
|
cursor: "pointer"
|
|
17825
17951
|
});
|
|
17826
17952
|
applyAttributionMarkTheme(a, background);
|
|
17953
|
+
a.dataset.velaScreenshot = "1";
|
|
17827
17954
|
const symbol = doc.createElement("span");
|
|
17828
17955
|
symbol.className = "vela-attr-symbol";
|
|
17829
17956
|
symbol.setAttribute("aria-hidden", "true");
|
|
@@ -17992,7 +18119,11 @@ function sanitizePanels(raw) {
|
|
|
17992
18119
|
}
|
|
17993
18120
|
if (Object.keys(widths).length > 0) out.widths = widths;
|
|
17994
18121
|
}
|
|
17995
|
-
|
|
18122
|
+
if (Array.isArray(p.pinned)) {
|
|
18123
|
+
const pinned = p.pinned.filter((id) => typeof id === "string" && id !== "");
|
|
18124
|
+
if (pinned.length > 0) out.pinned = [...new Set(pinned)];
|
|
18125
|
+
}
|
|
18126
|
+
return out.open || out.widths || out.pinned ? out : null;
|
|
17996
18127
|
}
|
|
17997
18128
|
function sanitizeTrackSizes(raw) {
|
|
17998
18129
|
if (raw == null || typeof raw !== "object") return null;
|
|
@@ -18461,6 +18592,7 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
|
|
|
18461
18592
|
var PREVIEW_BARS = 300;
|
|
18462
18593
|
var SINGLE_LOAD_BARS = 5e3;
|
|
18463
18594
|
var CHUNK_BARS = 1e4;
|
|
18595
|
+
var FIRST_PAINT_BARS = 20;
|
|
18464
18596
|
var GAP_FACTOR = 1.5;
|
|
18465
18597
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
18466
18598
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
@@ -18736,6 +18868,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18736
18868
|
let painted = false;
|
|
18737
18869
|
const paint = (bars, final) => {
|
|
18738
18870
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
18871
|
+
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
18739
18872
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
18740
18873
|
if (!painted && bars.length > 0) {
|
|
18741
18874
|
painted = true;
|
|
@@ -20153,6 +20286,15 @@ var RendererControl = class {
|
|
|
20153
20286
|
console.warn(`[vela] renderer "${this.renderer.name}" does not support screenshot().`);
|
|
20154
20287
|
return null;
|
|
20155
20288
|
}
|
|
20289
|
+
/**
|
|
20290
|
+
* Raster of the current chart onto a canvas (same pixels as {@link screenshot}),
|
|
20291
|
+
* or null if the renderer has no canvas export. Silent — a host compositing
|
|
20292
|
+
* several charts skips a renderer that cannot contribute.
|
|
20293
|
+
*/
|
|
20294
|
+
screenshotCanvas() {
|
|
20295
|
+
if (typeof this.renderer.screenshotCanvas === "function") return this.renderer.screenshotCanvas();
|
|
20296
|
+
return null;
|
|
20297
|
+
}
|
|
20156
20298
|
/**
|
|
20157
20299
|
* The active renderer's full cosmetic config as a serializable, versioned JSON
|
|
20158
20300
|
* document — persist it (templates, saved settings) and feed it back to
|
|
@@ -22721,6 +22863,66 @@ function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
|
|
|
22721
22863
|
center: (wickLeftDev + wickDev / 2) / dpr
|
|
22722
22864
|
};
|
|
22723
22865
|
}
|
|
22866
|
+
function aggregateCandleColumns(bars, i0, i1, xOf, yOf) {
|
|
22867
|
+
const out = [];
|
|
22868
|
+
let col = NaN;
|
|
22869
|
+
let intervals = [];
|
|
22870
|
+
const flush = () => {
|
|
22871
|
+
if (intervals.length === 0) return;
|
|
22872
|
+
const merged = [intervals[0]];
|
|
22873
|
+
for (let k = 1; k < intervals.length; k += 1) {
|
|
22874
|
+
const prev2 = merged[merged.length - 1];
|
|
22875
|
+
const next = intervals[k];
|
|
22876
|
+
if (Math.abs(yOf(prev2.hi) - yOf(next.lo)) < 1) {
|
|
22877
|
+
prev2.hi = next.hi;
|
|
22878
|
+
if (next.headIdx < prev2.headIdx) {
|
|
22879
|
+
prev2.headIdx = next.headIdx;
|
|
22880
|
+
prev2.headTime = next.headTime;
|
|
22881
|
+
prev2.open = next.open;
|
|
22882
|
+
}
|
|
22883
|
+
if (next.lastIdx > prev2.lastIdx) {
|
|
22884
|
+
prev2.lastIdx = next.lastIdx;
|
|
22885
|
+
prev2.close = next.close;
|
|
22886
|
+
}
|
|
22887
|
+
} else {
|
|
22888
|
+
merged.push(next);
|
|
22889
|
+
}
|
|
22890
|
+
}
|
|
22891
|
+
for (const iv of merged) out.push({ x: col, hi: iv.hi, lo: iv.lo, headTime: iv.headTime, open: iv.open, close: iv.close });
|
|
22892
|
+
intervals = [];
|
|
22893
|
+
};
|
|
22894
|
+
for (let i = i0; i <= i1; i += 1) {
|
|
22895
|
+
const b = bars[i];
|
|
22896
|
+
if (!b || b.high <= b.low) continue;
|
|
22897
|
+
const x = Math.round(xOf(i));
|
|
22898
|
+
if (x !== col) {
|
|
22899
|
+
flush();
|
|
22900
|
+
col = x;
|
|
22901
|
+
}
|
|
22902
|
+
let lo = b.low;
|
|
22903
|
+
let hi = b.high;
|
|
22904
|
+
let headIdx = i;
|
|
22905
|
+
let headTime = b.time;
|
|
22906
|
+
let open2 = b.open;
|
|
22907
|
+
for (let k = intervals.length - 1; k >= 0; k -= 1) {
|
|
22908
|
+
const iv = intervals[k];
|
|
22909
|
+
if (iv.lo > hi || iv.hi < lo) continue;
|
|
22910
|
+
if (iv.lo < lo) lo = iv.lo;
|
|
22911
|
+
if (iv.hi > hi) hi = iv.hi;
|
|
22912
|
+
if (iv.headIdx < headIdx) {
|
|
22913
|
+
headIdx = iv.headIdx;
|
|
22914
|
+
headTime = iv.headTime;
|
|
22915
|
+
open2 = iv.open;
|
|
22916
|
+
}
|
|
22917
|
+
intervals.splice(k, 1);
|
|
22918
|
+
}
|
|
22919
|
+
let insertAt = 0;
|
|
22920
|
+
while (insertAt < intervals.length && intervals[insertAt].lo < lo) insertAt += 1;
|
|
22921
|
+
intervals.splice(insertAt, 0, { lo, hi, headIdx, headTime, open: open2, lastIdx: i, close: b.close });
|
|
22922
|
+
}
|
|
22923
|
+
flush();
|
|
22924
|
+
return out;
|
|
22925
|
+
}
|
|
22724
22926
|
|
|
22725
22927
|
// src/renderers/native/backend/gl/Batch.ts
|
|
22726
22928
|
var FLOATS_PER_VERT = 8;
|
|
@@ -23693,44 +23895,19 @@ var WebGL2Backend = class {
|
|
|
23693
23895
|
}
|
|
23694
23896
|
}
|
|
23695
23897
|
/**
|
|
23696
|
-
* Sub-pixel LOD (mirrors Canvas2dBackend.drawCandlesAggregated):
|
|
23697
|
-
*
|
|
23698
|
-
*
|
|
23898
|
+
* Sub-pixel LOD (mirrors Canvas2dBackend.drawCandlesAggregated): bars sharing a
|
|
23899
|
+
* rounded pixel column collapse into high-low sticks — one per contiguous
|
|
23900
|
+
* coverage run (see {@link aggregateCandleColumns}), so a price gap inside the
|
|
23901
|
+
* column stays a void. Draw cost stays bounded by screen width, not bar count;
|
|
23902
|
+
* stick color follows its own first-open→last-close.
|
|
23699
23903
|
*/
|
|
23700
23904
|
emitCandlesAggregated(b, bars, i0, i1, coords, pane, up, down, barColors) {
|
|
23701
|
-
|
|
23702
|
-
|
|
23703
|
-
|
|
23704
|
-
|
|
23705
|
-
|
|
23706
|
-
let headTime = 0;
|
|
23707
|
-
let active = false;
|
|
23708
|
-
const flush = () => {
|
|
23709
|
-
if (!active) return;
|
|
23710
|
-
const c = parseColor(barColors.get(headTime) ?? (closeV >= openV ? up : down));
|
|
23711
|
-
const hY = coords.priceToY(hi, pane.scale, pane.bounds);
|
|
23712
|
-
const lY = coords.priceToY(lo, pane.scale, pane.bounds);
|
|
23713
|
-
b.rect(px - 0.5, hY, 1, lY - hY, c);
|
|
23714
|
-
};
|
|
23715
|
-
for (let i = i0; i <= i1; i += 1) {
|
|
23716
|
-
const bar = bars[i];
|
|
23717
|
-
if (!bar || bar.high <= bar.low) continue;
|
|
23718
|
-
const x = Math.round(coords.logicalToX(i));
|
|
23719
|
-
if (!active || x !== px) {
|
|
23720
|
-
flush();
|
|
23721
|
-
px = x;
|
|
23722
|
-
lo = bar.low;
|
|
23723
|
-
hi = bar.high;
|
|
23724
|
-
openV = bar.open;
|
|
23725
|
-
headTime = bar.time;
|
|
23726
|
-
active = true;
|
|
23727
|
-
} else {
|
|
23728
|
-
if (bar.low < lo) lo = bar.low;
|
|
23729
|
-
if (bar.high > hi) hi = bar.high;
|
|
23730
|
-
}
|
|
23731
|
-
closeV = bar.close;
|
|
23905
|
+
const yOf = (price) => coords.priceToY(price, pane.scale, pane.bounds);
|
|
23906
|
+
for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf)) {
|
|
23907
|
+
const c = parseColor(barColors.get(s.headTime) ?? (s.close >= s.open ? up : down));
|
|
23908
|
+
const hY = yOf(s.hi);
|
|
23909
|
+
b.rect(s.x - 0.5, hY, 1, yOf(s.lo) - hY, c);
|
|
23732
23910
|
}
|
|
23733
|
-
flush();
|
|
23734
23911
|
}
|
|
23735
23912
|
emitSeries(b, spec, pane, coords, i0, i1, theme, off = 0) {
|
|
23736
23913
|
if (spec.kind === "candle" || spec.kind === "bar") {
|
|
@@ -25028,13 +25205,15 @@ var SceneGraph = class {
|
|
|
25028
25205
|
if (!this.orderedCache) this.orderedCache = [...this.panes.values()].sort((a, b) => a.order - b.order);
|
|
25029
25206
|
return this.orderedCache;
|
|
25030
25207
|
}
|
|
25031
|
-
/** The session zones resolved into colored bands (pre/post-market
|
|
25032
|
-
* config's session colors) — consumed by the same painting path as
|
|
25208
|
+
/** The session zones resolved into colored bands (pre/post-market or extended-hours
|
|
25209
|
+
* washes from the config's session colors) — consumed by the same painting path as
|
|
25210
|
+
* {@link highlights}. */
|
|
25033
25211
|
sessionHighlightBands() {
|
|
25034
25212
|
if (!this.sessionZones) return [];
|
|
25035
25213
|
const out = [];
|
|
25036
25214
|
for (const [from, to] of this.sessionZones.pre) out.push({ from, to, color: this.style.sessions.premarketColor });
|
|
25037
25215
|
for (const [from, to] of this.sessionZones.post) out.push({ from, to, color: this.style.sessions.postmarketColor });
|
|
25216
|
+
for (const [from, to] of this.sessionZones.extended) out.push({ from, to, color: this.style.sessions.extendedColor });
|
|
25038
25217
|
return out;
|
|
25039
25218
|
}
|
|
25040
25219
|
indicatorsForPane(paneId) {
|
|
@@ -25573,50 +25752,23 @@ var Canvas2dBackend = class {
|
|
|
25573
25752
|
}
|
|
25574
25753
|
}
|
|
25575
25754
|
/**
|
|
25576
|
-
* Sub-pixel LOD:
|
|
25577
|
-
*
|
|
25578
|
-
*
|
|
25579
|
-
*
|
|
25755
|
+
* Sub-pixel LOD: bars sharing a rounded pixel column collapse into high-low
|
|
25756
|
+
* sticks (one per contiguous coverage run — see {@link aggregateCandleColumns}),
|
|
25757
|
+
* so draw cost is bounded by screen width (not bar count) when zoomed far out
|
|
25758
|
+
* and a price gap inside the column stays a void. Each stick's color follows
|
|
25759
|
+
* its first-open→last-close direction (a barcolor() on the head bar still wins).
|
|
25580
25760
|
*/
|
|
25581
25761
|
drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, up, down, barColors) {
|
|
25582
|
-
|
|
25583
|
-
|
|
25584
|
-
|
|
25585
|
-
|
|
25586
|
-
|
|
25587
|
-
let headTime = 0;
|
|
25588
|
-
let active = false;
|
|
25589
|
-
const flush = () => {
|
|
25590
|
-
if (!active) return;
|
|
25591
|
-
const color = barColors.get(headTime) ?? (closeV >= openV ? up : down);
|
|
25592
|
-
const hY = coords.priceToY(hi, pane.scale, pane.bounds);
|
|
25593
|
-
const lY = coords.priceToY(lo, pane.scale, pane.bounds);
|
|
25594
|
-
ctx.strokeStyle = color;
|
|
25595
|
-
ctx.lineWidth = 1;
|
|
25762
|
+
const yOf = (price) => coords.priceToY(price, pane.scale, pane.bounds);
|
|
25763
|
+
ctx.lineWidth = 1;
|
|
25764
|
+
for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf)) {
|
|
25765
|
+
const x = s.x + 0.5;
|
|
25766
|
+
ctx.strokeStyle = barColors.get(s.headTime) ?? (s.close >= s.open ? up : down);
|
|
25596
25767
|
ctx.beginPath();
|
|
25597
|
-
ctx.moveTo(
|
|
25598
|
-
ctx.lineTo(
|
|
25768
|
+
ctx.moveTo(x, yOf(s.hi));
|
|
25769
|
+
ctx.lineTo(x, yOf(s.lo));
|
|
25599
25770
|
ctx.stroke();
|
|
25600
|
-
};
|
|
25601
|
-
for (let i = i0; i <= i1; i += 1) {
|
|
25602
|
-
const b = bars[i];
|
|
25603
|
-
if (!b || b.high <= b.low) continue;
|
|
25604
|
-
const x = Math.round(coords.logicalToX(i)) + 0.5;
|
|
25605
|
-
if (!active || x !== px) {
|
|
25606
|
-
flush();
|
|
25607
|
-
px = x;
|
|
25608
|
-
lo = b.low;
|
|
25609
|
-
hi = b.high;
|
|
25610
|
-
openV = b.open;
|
|
25611
|
-
headTime = b.time;
|
|
25612
|
-
active = true;
|
|
25613
|
-
} else {
|
|
25614
|
-
if (b.low < lo) lo = b.low;
|
|
25615
|
-
if (b.high > hi) hi = b.high;
|
|
25616
|
-
}
|
|
25617
|
-
closeV = b.close;
|
|
25618
25771
|
}
|
|
25619
|
-
flush();
|
|
25620
25772
|
}
|
|
25621
25773
|
drawSeries(ctx, spec, pane, coords, i0, i1, theme, off = 0) {
|
|
25622
25774
|
if (spec.kind === "candle" || spec.kind === "bar") {
|
|
@@ -26640,6 +26792,32 @@ var DrawingSceneRenderer = class {
|
|
|
26640
26792
|
}
|
|
26641
26793
|
};
|
|
26642
26794
|
|
|
26795
|
+
// src/renderers/native/chrome/tz.ts
|
|
26796
|
+
function tzOffsetMs(ms, timeZone) {
|
|
26797
|
+
if (!timeZone || timeZone === "UTC") return 0;
|
|
26798
|
+
try {
|
|
26799
|
+
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
26800
|
+
timeZone,
|
|
26801
|
+
hourCycle: "h23",
|
|
26802
|
+
year: "numeric",
|
|
26803
|
+
month: "2-digit",
|
|
26804
|
+
day: "2-digit",
|
|
26805
|
+
hour: "2-digit",
|
|
26806
|
+
minute: "2-digit",
|
|
26807
|
+
second: "2-digit"
|
|
26808
|
+
});
|
|
26809
|
+
const parts = dtf.formatToParts(new Date(ms));
|
|
26810
|
+
const get = (t) => Number(parts.find((p) => p.type === t)?.value);
|
|
26811
|
+
const asUTC = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
|
|
26812
|
+
return asUTC - ms;
|
|
26813
|
+
} catch {
|
|
26814
|
+
return 0;
|
|
26815
|
+
}
|
|
26816
|
+
}
|
|
26817
|
+
function zonedDate(ms, timeZone) {
|
|
26818
|
+
return new Date(ms + tzOffsetMs(ms, timeZone));
|
|
26819
|
+
}
|
|
26820
|
+
|
|
26643
26821
|
// src/renderers/native/chrome/ticks.ts
|
|
26644
26822
|
function priceTicks(min, max, target = 6) {
|
|
26645
26823
|
if (!(max > min) || !Number.isFinite(min) || !Number.isFinite(max)) return [];
|
|
@@ -26787,7 +26965,14 @@ var STEP_LADDER = [
|
|
|
26787
26965
|
YEAR
|
|
26788
26966
|
];
|
|
26789
26967
|
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
26968
|
+
var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
26790
26969
|
var pad2 = (n) => n < 10 ? `0${n}` : String(n);
|
|
26970
|
+
function formatTimeStamp(ms, timeZone, barIntervalMs) {
|
|
26971
|
+
const d = zonedDate(ms, timeZone);
|
|
26972
|
+
const date = `${WEEKDAYS[d.getUTCDay()]} ${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]} '${pad2(d.getUTCFullYear() % 100)}`;
|
|
26973
|
+
if (barIntervalMs >= DAY) return date;
|
|
26974
|
+
return `${date} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}`;
|
|
26975
|
+
}
|
|
26791
26976
|
function pickStep(targetMs) {
|
|
26792
26977
|
for (const step of STEP_LADDER) if (step >= targetMs) return step;
|
|
26793
26978
|
return STEP_LADDER[STEP_LADDER.length - 1];
|
|
@@ -26829,32 +27014,6 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
26829
27014
|
return out;
|
|
26830
27015
|
}
|
|
26831
27016
|
|
|
26832
|
-
// src/renderers/native/chrome/tz.ts
|
|
26833
|
-
function tzOffsetMs(ms, timeZone) {
|
|
26834
|
-
if (!timeZone || timeZone === "UTC") return 0;
|
|
26835
|
-
try {
|
|
26836
|
-
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
26837
|
-
timeZone,
|
|
26838
|
-
hourCycle: "h23",
|
|
26839
|
-
year: "numeric",
|
|
26840
|
-
month: "2-digit",
|
|
26841
|
-
day: "2-digit",
|
|
26842
|
-
hour: "2-digit",
|
|
26843
|
-
minute: "2-digit",
|
|
26844
|
-
second: "2-digit"
|
|
26845
|
-
});
|
|
26846
|
-
const parts = dtf.formatToParts(new Date(ms));
|
|
26847
|
-
const get = (t) => Number(parts.find((p) => p.type === t)?.value);
|
|
26848
|
-
const asUTC = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour") % 24, get("minute"), get("second"));
|
|
26849
|
-
return asUTC - ms;
|
|
26850
|
-
} catch {
|
|
26851
|
-
return 0;
|
|
26852
|
-
}
|
|
26853
|
-
}
|
|
26854
|
-
function zonedDate(ms, timeZone) {
|
|
26855
|
-
return new Date(ms + tzOffsetMs(ms, timeZone));
|
|
26856
|
-
}
|
|
26857
|
-
|
|
26858
27017
|
// src/renderers/native/chrome/ChromeRenderer.ts
|
|
26859
27018
|
var ChromeRenderer = class {
|
|
26860
27019
|
constructor() {
|
|
@@ -27301,7 +27460,7 @@ var CrosshairRenderer = class {
|
|
|
27301
27460
|
const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
|
|
27302
27461
|
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);
|
|
27303
27462
|
}
|
|
27304
|
-
this.chip(ctx, x, dataH + 1,
|
|
27463
|
+
this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
27305
27464
|
}
|
|
27306
27465
|
destroy() {
|
|
27307
27466
|
this.canvas = null;
|
|
@@ -27349,7 +27508,7 @@ var CrosshairRenderer = class {
|
|
|
27349
27508
|
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);
|
|
27350
27509
|
}
|
|
27351
27510
|
}
|
|
27352
|
-
this.chip(ctx, x, dataH + 1,
|
|
27511
|
+
this.chip(ctx, x, dataH + 1, formatTimeStamp(ext.time, scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
27353
27512
|
ctx.globalAlpha = 1;
|
|
27354
27513
|
}
|
|
27355
27514
|
/** A soft band + a brighter crisp center line over the hovered separator, so it reads as
|
|
@@ -27382,11 +27541,6 @@ function setDash3(ctx, style) {
|
|
|
27382
27541
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
27383
27542
|
else ctx.setLineDash([]);
|
|
27384
27543
|
}
|
|
27385
|
-
function formatStamp(ms, timeZone) {
|
|
27386
|
-
const d = zonedDate(ms, timeZone);
|
|
27387
|
-
const pad = (n) => n < 10 ? `0${n}` : String(n);
|
|
27388
|
-
return `${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
|
|
27389
|
-
}
|
|
27390
27544
|
|
|
27391
27545
|
// src/renderers/native/chrome/settings-visibility.ts
|
|
27392
27546
|
function settingsIdSlug(label) {
|
|
@@ -33153,6 +33307,9 @@ function clipHighlightRect(x1, x2, left, right) {
|
|
|
33153
33307
|
const end = Math.min(right, x2);
|
|
33154
33308
|
return end > x ? { x, width: end - x } : null;
|
|
33155
33309
|
}
|
|
33310
|
+
function bandEdgeSlot(logical) {
|
|
33311
|
+
return Math.ceil(logical) - 0.5;
|
|
33312
|
+
}
|
|
33156
33313
|
var BackdropRenderer = class {
|
|
33157
33314
|
constructor() {
|
|
33158
33315
|
this.canvas = null;
|
|
@@ -33183,21 +33340,22 @@ var BackdropRenderer = class {
|
|
|
33183
33340
|
this.drawGrid(ctx, scene, coords, theme, coords.width, gridAlpha);
|
|
33184
33341
|
}
|
|
33185
33342
|
/** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
|
|
33186
|
-
* Session-zone washes
|
|
33343
|
+
* Session-zone washes paint first (edges snapped to bar-slot boundaries so a band
|
|
33344
|
+
* never cuts a candle in two), host highlights on top (continuous time mapping —
|
|
33345
|
+
* hosts may paint sub-bar spans). */
|
|
33187
33346
|
drawHighlights(ctx, scene, coords) {
|
|
33188
33347
|
const sessions = scene.sessionHighlightBands();
|
|
33189
33348
|
if (sessions.length > 0) {
|
|
33190
33349
|
const left = Math.max(0, coords.logicalToX(-0.5));
|
|
33191
33350
|
const right = Math.min(coords.width, coords.logicalToX(coords.barCount - 0.5));
|
|
33192
|
-
this.drawHighlightSet(ctx, sessions, coords, left, right);
|
|
33351
|
+
this.drawHighlightSet(ctx, sessions, coords, left, right, true);
|
|
33193
33352
|
}
|
|
33194
|
-
this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width);
|
|
33353
|
+
this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width, false);
|
|
33195
33354
|
}
|
|
33196
|
-
drawHighlightSet(ctx, bands, coords, left, right) {
|
|
33355
|
+
drawHighlightSet(ctx, bands, coords, left, right, snapToSlots) {
|
|
33356
|
+
const edgeX = (ms) => snapToSlots ? coords.logicalToX(bandEdgeSlot(coords.timeToLogical(ms))) : coords.timeToX(ms);
|
|
33197
33357
|
for (const band of bands) {
|
|
33198
|
-
const
|
|
33199
|
-
const x2 = coords.timeToX(band.to);
|
|
33200
|
-
const rect = clipHighlightRect(x1, x2, left, right);
|
|
33358
|
+
const rect = clipHighlightRect(edgeX(band.from), edgeX(band.to), left, right);
|
|
33201
33359
|
if (!rect) continue;
|
|
33202
33360
|
ctx.fillStyle = band.color;
|
|
33203
33361
|
ctx.fillRect(rect.x, 0, rect.width, coords.height);
|
|
@@ -34344,7 +34502,7 @@ var NativeRenderer = class {
|
|
|
34344
34502
|
};
|
|
34345
34503
|
})(),
|
|
34346
34504
|
series: { style: this.scene.priceStyle, baseline: this.scene.baselineValue, spacing: this.coords.spacingScale },
|
|
34347
|
-
sessions: { premarketColor: s.sessions.premarketColor, postmarketColor: s.sessions.postmarketColor }
|
|
34505
|
+
sessions: { premarketColor: s.sessions.premarketColor, postmarketColor: s.sessions.postmarketColor, extendedColor: s.sessions.extendedColor }
|
|
34348
34506
|
};
|
|
34349
34507
|
}
|
|
34350
34508
|
/**
|
|
@@ -34426,7 +34584,7 @@ var NativeRenderer = class {
|
|
|
34426
34584
|
width: next.baseline.width,
|
|
34427
34585
|
baselineLevel: next.baseline.baselineLevel
|
|
34428
34586
|
};
|
|
34429
|
-
s.sessions = { premarketColor: next.sessions.premarketColor, postmarketColor: next.sessions.postmarketColor };
|
|
34587
|
+
s.sessions = { premarketColor: next.sessions.premarketColor, postmarketColor: next.sessions.postmarketColor, extendedColor: next.sessions.extendedColor };
|
|
34430
34588
|
this.setPriceStyle(next.series.style);
|
|
34431
34589
|
this.scene.candleOverride = candleOverrideFor(this.scene.priceStyle, s.chartTypes);
|
|
34432
34590
|
this.scene.baselineValue = next.series.baseline;
|
|
@@ -34648,9 +34806,10 @@ var NativeRenderer = class {
|
|
|
34648
34806
|
* opts in with a `data-vela-screenshot` attribute on the mount container's
|
|
34649
34807
|
* subtree (the widget marks its status line, and its symbol watermark with
|
|
34650
34808
|
* `"under"` — drawn beneath the canvases, where it sits on screen). Only the
|
|
34651
|
-
* crosshair (L2) is intentionally excluded.
|
|
34809
|
+
* crosshair (L2) is intentionally excluded. {@link screenshot} is this canvas
|
|
34810
|
+
* as a PNG data URL.
|
|
34652
34811
|
*/
|
|
34653
|
-
|
|
34812
|
+
screenshotCanvas() {
|
|
34654
34813
|
if (!this.dataCanvas) return null;
|
|
34655
34814
|
this.computeScales();
|
|
34656
34815
|
this.paintData();
|
|
@@ -34678,7 +34837,10 @@ var NativeRenderer = class {
|
|
|
34678
34837
|
if (el.getAttribute("data-vela-screenshot") !== "under") rasterizeOverlay(ctx, el, frame);
|
|
34679
34838
|
}
|
|
34680
34839
|
}
|
|
34681
|
-
return out
|
|
34840
|
+
return out;
|
|
34841
|
+
}
|
|
34842
|
+
screenshot() {
|
|
34843
|
+
return this.screenshotCanvas()?.toDataURL("image/png") ?? null;
|
|
34682
34844
|
}
|
|
34683
34845
|
/**
|
|
34684
34846
|
* The candles-first-appear reveal — animates the **candles themselves** (no extra
|
|
@@ -36073,7 +36235,7 @@ var NativeRenderer = class {
|
|
|
36073
36235
|
const pricePane = this.dataWindowPricePane();
|
|
36074
36236
|
const ohlc = this.dataWindowOHLC(bar, pricePane);
|
|
36075
36237
|
const parts = [];
|
|
36076
|
-
if (bar) parts.push(
|
|
36238
|
+
if (bar) parts.push(formatStamp(bar.time, this.scene.timezone));
|
|
36077
36239
|
if (ohlc) parts.push(`Open ${ohlc.o}, High ${ohlc.h}, Low ${ohlc.l}, Close ${ohlc.c}`);
|
|
36078
36240
|
for (const group of this.dataWindowGroups(idx, pricePane)) {
|
|
36079
36241
|
for (const row of group.rows) parts.push(`${row.label} ${row.value}`);
|
|
@@ -36960,7 +37122,7 @@ function formatStampParts(ms, timeZone) {
|
|
|
36960
37122
|
time: `${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`
|
|
36961
37123
|
};
|
|
36962
37124
|
}
|
|
36963
|
-
function
|
|
37125
|
+
function formatStamp(ms, timeZone) {
|
|
36964
37126
|
const { date, time } = formatStampParts(ms, timeZone);
|
|
36965
37127
|
return `${date} ${time}`;
|
|
36966
37128
|
}
|
|
@@ -36991,7 +37153,7 @@ function sanitizeSessionZones(value) {
|
|
|
36991
37153
|
}
|
|
36992
37154
|
return out.sort((a, b) => a[0] - b[0]);
|
|
36993
37155
|
};
|
|
36994
|
-
return { pre: windows(v.pre), post: windows(v.post) };
|
|
37156
|
+
return { pre: windows(v.pre), post: windows(v.post), extended: windows(v.extended) };
|
|
36995
37157
|
}
|
|
36996
37158
|
function clamp012(v) {
|
|
36997
37159
|
return Math.max(0, Math.min(1, v));
|
|
@@ -37680,6 +37842,7 @@ var MARKET_LABELS = {
|
|
|
37680
37842
|
open: "Market Open",
|
|
37681
37843
|
pre: "Pre-Market",
|
|
37682
37844
|
post: "Post-Market",
|
|
37845
|
+
extended: "Extended Hours",
|
|
37683
37846
|
closed: "Market Closed",
|
|
37684
37847
|
holiday: "Market Holiday"
|
|
37685
37848
|
};
|
|
@@ -37687,6 +37850,7 @@ var MARKET_INKS = {
|
|
|
37687
37850
|
open: "var(--vela-up)",
|
|
37688
37851
|
pre: SESSION_PRE,
|
|
37689
37852
|
post: SESSION_POST,
|
|
37853
|
+
extended: SESSION_POST,
|
|
37690
37854
|
closed: SESSION_OFF,
|
|
37691
37855
|
holiday: SESSION_OFF
|
|
37692
37856
|
};
|
|
@@ -38000,118 +38164,17 @@ var Statusline = class {
|
|
|
38000
38164
|
}
|
|
38001
38165
|
};
|
|
38002
38166
|
|
|
38003
|
-
// src/widget/market-status.ts
|
|
38004
|
-
function civilDate(ms, tz) {
|
|
38005
|
-
try {
|
|
38006
|
-
return new Intl.DateTimeFormat("en-CA", { timeZone: tz }).format(ms);
|
|
38007
|
-
} catch {
|
|
38008
|
-
return "";
|
|
38009
|
-
}
|
|
38010
|
-
}
|
|
38011
|
-
function isWeekday(ms, tz) {
|
|
38012
|
-
try {
|
|
38013
|
-
const wd = new Intl.DateTimeFormat("en-US", { timeZone: tz, weekday: "short" }).format(ms);
|
|
38014
|
-
return wd !== "Sat" && wd !== "Sun";
|
|
38015
|
-
} catch {
|
|
38016
|
-
return false;
|
|
38017
|
-
}
|
|
38018
|
-
}
|
|
38019
|
-
function deriveMarketStatus(now, w, tz) {
|
|
38020
|
-
const within = (ws) => ws.find(([s, e]) => now >= s && now < e);
|
|
38021
|
-
if (within(w.regular)) return "open";
|
|
38022
|
-
const ext = within(w.extended);
|
|
38023
|
-
if (ext) return w.regular.some(([s]) => s >= now && s < ext[1]) ? "pre" : "post";
|
|
38024
|
-
if (isWeekday(now, tz)) {
|
|
38025
|
-
const today = civilDate(now, tz);
|
|
38026
|
-
if (today !== "" && !w.extended.some(([s]) => civilDate(s, tz) === today)) return "holiday";
|
|
38027
|
-
}
|
|
38028
|
-
return "closed";
|
|
38029
|
-
}
|
|
38030
|
-
function nextStatusBoundary(now, w) {
|
|
38031
|
-
let next = null;
|
|
38032
|
-
for (const ws of [w.regular, w.extended]) {
|
|
38033
|
-
for (const [s, e] of ws) {
|
|
38034
|
-
for (const t of [s, e]) {
|
|
38035
|
-
if (t > now && (next == null || t < next)) next = t;
|
|
38036
|
-
}
|
|
38037
|
-
}
|
|
38038
|
-
}
|
|
38039
|
-
return next;
|
|
38040
|
-
}
|
|
38041
|
-
var FETCH_BACK_MS = 4 * 864e5;
|
|
38042
|
-
var FETCH_AHEAD_MS = 10 * 864e5;
|
|
38043
|
-
var MIN_TIMER_MS = 15e3;
|
|
38044
|
-
var MAX_TIMER_MS = 36e5;
|
|
38045
|
-
var RETRY_MS2 = 6e4;
|
|
38046
|
-
var MarketStatusTracker = class {
|
|
38047
|
-
constructor(onStatus) {
|
|
38048
|
-
this.onStatus = onStatus;
|
|
38049
|
-
/** Invalidates detached async work — bumped by every track()/stop(). */
|
|
38050
|
-
this.epoch = 0;
|
|
38051
|
-
this.timer = null;
|
|
38052
|
-
}
|
|
38053
|
-
/** (Re)bind to a chart's data surface + symbol and start evaluating. */
|
|
38054
|
-
track(data, symbol) {
|
|
38055
|
-
const my = ++this.epoch;
|
|
38056
|
-
this.clearTimer();
|
|
38057
|
-
void this.evaluate(my, data, symbol);
|
|
38058
|
-
}
|
|
38059
|
-
stop() {
|
|
38060
|
-
this.epoch += 1;
|
|
38061
|
-
this.clearTimer();
|
|
38062
|
-
}
|
|
38063
|
-
async evaluate(my, data, symbol) {
|
|
38064
|
-
const resolved = data.resolve(symbol);
|
|
38065
|
-
const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
|
|
38066
|
-
const si = await data.symbolInfo(symbol).catch(() => void 0);
|
|
38067
|
-
if (my !== this.epoch) return;
|
|
38068
|
-
const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
|
|
38069
|
-
if (!provider?.getCalendar || !hasSessions || !resolved) {
|
|
38070
|
-
this.onStatus("open");
|
|
38071
|
-
return;
|
|
38072
|
-
}
|
|
38073
|
-
const tz = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : "Etc/UTC";
|
|
38074
|
-
const now = Date.now();
|
|
38075
|
-
const range = { from: now - FETCH_BACK_MS, to: now + FETCH_AHEAD_MS };
|
|
38076
|
-
const [regular, extended] = await Promise.all([
|
|
38077
|
-
provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
|
|
38078
|
-
provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
|
|
38079
|
-
]);
|
|
38080
|
-
if (my !== this.epoch) return;
|
|
38081
|
-
if (!regular || !extended) {
|
|
38082
|
-
this.arm(my, data, symbol, RETRY_MS2);
|
|
38083
|
-
return;
|
|
38084
|
-
}
|
|
38085
|
-
const w = { regular, extended };
|
|
38086
|
-
this.onStatus(deriveMarketStatus(now, w, tz));
|
|
38087
|
-
const boundary = nextStatusBoundary(now, w);
|
|
38088
|
-
const delay = Math.min(boundary != null ? boundary - now : MAX_TIMER_MS, MAX_TIMER_MS);
|
|
38089
|
-
this.arm(my, data, symbol, Math.max(delay, MIN_TIMER_MS));
|
|
38090
|
-
}
|
|
38091
|
-
arm(my, data, symbol, delay) {
|
|
38092
|
-
this.clearTimer();
|
|
38093
|
-
this.timer = setTimeout(() => {
|
|
38094
|
-
this.timer = null;
|
|
38095
|
-
if (my !== this.epoch) return;
|
|
38096
|
-
void this.evaluate(my, data, symbol);
|
|
38097
|
-
}, delay);
|
|
38098
|
-
}
|
|
38099
|
-
clearTimer() {
|
|
38100
|
-
if (this.timer != null) clearTimeout(this.timer);
|
|
38101
|
-
this.timer = null;
|
|
38102
|
-
}
|
|
38103
|
-
};
|
|
38104
|
-
|
|
38105
38167
|
// src/widget/session-shading.ts
|
|
38106
38168
|
var DAY_MS2 = 864e5;
|
|
38107
38169
|
var MINUTE_MS = 6e4;
|
|
38108
|
-
function parseWindow(text) {
|
|
38170
|
+
function parseWindow(text, allowOvernight = false) {
|
|
38109
38171
|
if (typeof text !== "string") return null;
|
|
38110
38172
|
const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
|
|
38111
38173
|
if (!m) return null;
|
|
38112
38174
|
const start = Number(m[1]) * 60 + Number(m[2]);
|
|
38113
38175
|
const end = Number(m[3]) * 60 + Number(m[4]);
|
|
38114
|
-
if (
|
|
38176
|
+
if (end > 1440 || start >= 1440) return null;
|
|
38177
|
+
if (start >= end && !allowOvernight) return null;
|
|
38115
38178
|
return { start, end };
|
|
38116
38179
|
}
|
|
38117
38180
|
function parseSessionSpec(si) {
|
|
@@ -38121,9 +38184,13 @@ function parseSessionSpec(si) {
|
|
|
38121
38184
|
if (!regular) return null;
|
|
38122
38185
|
const tz = si?.["timezone"];
|
|
38123
38186
|
const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
|
|
38124
|
-
const ext = parseWindow(si?.["session_extended"]);
|
|
38187
|
+
const ext = parseWindow(si?.["session_extended"], true);
|
|
38188
|
+
if (ext && ext.start >= ext.end) {
|
|
38189
|
+
if (ext.start >= regular.end && ext.end >= regular.end) return { regular, extended: ext, overnight: true, timezone };
|
|
38190
|
+
return { regular, extended: { start: 0, end: 1440 }, overnight: false, timezone };
|
|
38191
|
+
}
|
|
38125
38192
|
const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
|
|
38126
|
-
return { regular, extended, timezone };
|
|
38193
|
+
return { regular, extended, overnight: false, timezone };
|
|
38127
38194
|
}
|
|
38128
38195
|
var dtfCache = /* @__PURE__ */ new Map();
|
|
38129
38196
|
function civilFormatter(tz) {
|
|
@@ -38159,27 +38226,48 @@ function civilParts(dtf, ms) {
|
|
|
38159
38226
|
minute: Number(get("minute"))
|
|
38160
38227
|
};
|
|
38161
38228
|
}
|
|
38229
|
+
function mergeAbutting(bands) {
|
|
38230
|
+
const sorted = [...bands].sort((a, b) => a[0] - b[0]);
|
|
38231
|
+
const out = [];
|
|
38232
|
+
for (const [s, e] of sorted) {
|
|
38233
|
+
if (e <= s) continue;
|
|
38234
|
+
const last = out[out.length - 1];
|
|
38235
|
+
if (last && s <= last[1]) last[1] = Math.max(last[1], e);
|
|
38236
|
+
else out.push([s, e]);
|
|
38237
|
+
}
|
|
38238
|
+
return out;
|
|
38239
|
+
}
|
|
38162
38240
|
function expandSessionZones(spec, from, to) {
|
|
38163
38241
|
const pre = [];
|
|
38164
38242
|
const post = [];
|
|
38243
|
+
const extended = [];
|
|
38165
38244
|
const dtf = civilFormatter(spec.timezone);
|
|
38166
|
-
if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
|
|
38245
|
+
if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post, extended };
|
|
38167
38246
|
const seen = /* @__PURE__ */ new Set();
|
|
38168
38247
|
for (let cursor = from - DAY_MS2; cursor < to + DAY_MS2; cursor += DAY_MS2) {
|
|
38169
38248
|
const civil = civilParts(dtf, cursor);
|
|
38170
38249
|
const key = civil.year * 1e4 + civil.month * 100 + civil.day;
|
|
38171
38250
|
if (!Number.isFinite(key) || seen.has(key)) continue;
|
|
38172
38251
|
seen.add(key);
|
|
38173
|
-
|
|
38252
|
+
const sunday = civil.weekday === "Sun";
|
|
38253
|
+
if (civil.weekday === "Sat" || sunday && !spec.overnight) continue;
|
|
38174
38254
|
const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
|
|
38175
38255
|
const noonGuess = naive + 720 * MINUTE_MS;
|
|
38176
38256
|
const atNoon = civilParts(dtf, noonGuess);
|
|
38177
38257
|
const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
|
|
38178
38258
|
const at = (minutes) => naive + minutes * MINUTE_MS - offset;
|
|
38259
|
+
if (spec.overnight) {
|
|
38260
|
+
if (!sunday) {
|
|
38261
|
+
extended.push([at(0), at(spec.regular.start)]);
|
|
38262
|
+
if (spec.regular.end < spec.extended.end) extended.push([at(spec.regular.end), at(spec.extended.end)]);
|
|
38263
|
+
}
|
|
38264
|
+
if (civil.weekday !== "Fri") extended.push([at(spec.extended.start), at(1440)]);
|
|
38265
|
+
continue;
|
|
38266
|
+
}
|
|
38179
38267
|
if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
|
|
38180
38268
|
if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
|
|
38181
38269
|
}
|
|
38182
|
-
return { pre, post };
|
|
38270
|
+
return { pre, post, extended: mergeAbutting(extended) };
|
|
38183
38271
|
}
|
|
38184
38272
|
var COVER_PAD_MIN_MS = 2 * DAY_MS2;
|
|
38185
38273
|
var SessionShadingTracker = class {
|
|
@@ -38234,7 +38322,7 @@ var SessionShadingTracker = class {
|
|
|
38234
38322
|
return;
|
|
38235
38323
|
}
|
|
38236
38324
|
if (this.session !== "extended" || !this.intraday) {
|
|
38237
|
-
if (force) this.onZones({ pre: [], post: [] });
|
|
38325
|
+
if (force) this.onZones({ pre: [], post: [], extended: [] });
|
|
38238
38326
|
return;
|
|
38239
38327
|
}
|
|
38240
38328
|
const from = Math.min(range.from, range.to);
|
|
@@ -38248,6 +38336,112 @@ var SessionShadingTracker = class {
|
|
|
38248
38336
|
}
|
|
38249
38337
|
};
|
|
38250
38338
|
|
|
38339
|
+
// src/widget/market-status.ts
|
|
38340
|
+
function civilDate(ms, tz) {
|
|
38341
|
+
try {
|
|
38342
|
+
return new Intl.DateTimeFormat("en-CA", { timeZone: tz }).format(ms);
|
|
38343
|
+
} catch {
|
|
38344
|
+
return "";
|
|
38345
|
+
}
|
|
38346
|
+
}
|
|
38347
|
+
function isWeekday(ms, tz) {
|
|
38348
|
+
try {
|
|
38349
|
+
const wd = new Intl.DateTimeFormat("en-US", { timeZone: tz, weekday: "short" }).format(ms);
|
|
38350
|
+
return wd !== "Sat" && wd !== "Sun";
|
|
38351
|
+
} catch {
|
|
38352
|
+
return false;
|
|
38353
|
+
}
|
|
38354
|
+
}
|
|
38355
|
+
function deriveMarketStatus(now, w, tz, overnight = false) {
|
|
38356
|
+
const within = (ws) => ws.find(([s, e]) => now >= s && now < e);
|
|
38357
|
+
if (within(w.regular)) return "open";
|
|
38358
|
+
const ext = within(w.extended);
|
|
38359
|
+
if (ext) {
|
|
38360
|
+
if (overnight) return "extended";
|
|
38361
|
+
return w.regular.some(([s]) => s >= now && s < ext[1]) ? "pre" : "post";
|
|
38362
|
+
}
|
|
38363
|
+
if (isWeekday(now, tz)) {
|
|
38364
|
+
const today = civilDate(now, tz);
|
|
38365
|
+
if (today !== "" && !w.extended.some(([s]) => civilDate(s, tz) === today)) return "holiday";
|
|
38366
|
+
}
|
|
38367
|
+
return "closed";
|
|
38368
|
+
}
|
|
38369
|
+
function nextStatusBoundary(now, w) {
|
|
38370
|
+
let next = null;
|
|
38371
|
+
for (const ws of [w.regular, w.extended]) {
|
|
38372
|
+
for (const [s, e] of ws) {
|
|
38373
|
+
for (const t of [s, e]) {
|
|
38374
|
+
if (t > now && (next == null || t < next)) next = t;
|
|
38375
|
+
}
|
|
38376
|
+
}
|
|
38377
|
+
}
|
|
38378
|
+
return next;
|
|
38379
|
+
}
|
|
38380
|
+
var FETCH_BACK_MS = 4 * 864e5;
|
|
38381
|
+
var FETCH_AHEAD_MS = 10 * 864e5;
|
|
38382
|
+
var MIN_TIMER_MS = 15e3;
|
|
38383
|
+
var MAX_TIMER_MS = 36e5;
|
|
38384
|
+
var RETRY_MS2 = 6e4;
|
|
38385
|
+
var MarketStatusTracker = class {
|
|
38386
|
+
constructor(onStatus) {
|
|
38387
|
+
this.onStatus = onStatus;
|
|
38388
|
+
/** Invalidates detached async work — bumped by every track()/stop(). */
|
|
38389
|
+
this.epoch = 0;
|
|
38390
|
+
this.timer = null;
|
|
38391
|
+
}
|
|
38392
|
+
/** (Re)bind to a chart's data surface + symbol and start evaluating. */
|
|
38393
|
+
track(data, symbol) {
|
|
38394
|
+
const my = ++this.epoch;
|
|
38395
|
+
this.clearTimer();
|
|
38396
|
+
void this.evaluate(my, data, symbol);
|
|
38397
|
+
}
|
|
38398
|
+
stop() {
|
|
38399
|
+
this.epoch += 1;
|
|
38400
|
+
this.clearTimer();
|
|
38401
|
+
}
|
|
38402
|
+
async evaluate(my, data, symbol) {
|
|
38403
|
+
const resolved = data.resolve(symbol);
|
|
38404
|
+
const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
|
|
38405
|
+
const si = await data.symbolInfo(symbol).catch(() => void 0);
|
|
38406
|
+
if (my !== this.epoch) return;
|
|
38407
|
+
const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
|
|
38408
|
+
if (!provider?.getCalendar || !hasSessions || !resolved) {
|
|
38409
|
+
this.onStatus("open");
|
|
38410
|
+
return;
|
|
38411
|
+
}
|
|
38412
|
+
const tz = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : "Etc/UTC";
|
|
38413
|
+
const now = Date.now();
|
|
38414
|
+
const range = { from: now - FETCH_BACK_MS, to: now + FETCH_AHEAD_MS };
|
|
38415
|
+
const [regular, extended] = await Promise.all([
|
|
38416
|
+
provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
|
|
38417
|
+
provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
|
|
38418
|
+
]);
|
|
38419
|
+
if (my !== this.epoch) return;
|
|
38420
|
+
if (!regular || !extended) {
|
|
38421
|
+
this.arm(my, data, symbol, RETRY_MS2);
|
|
38422
|
+
return;
|
|
38423
|
+
}
|
|
38424
|
+
const w = { regular, extended };
|
|
38425
|
+
const overnight = parseSessionSpec(si)?.overnight === true;
|
|
38426
|
+
this.onStatus(deriveMarketStatus(now, w, tz, overnight));
|
|
38427
|
+
const boundary = nextStatusBoundary(now, w);
|
|
38428
|
+
const delay = Math.min(boundary != null ? boundary - now : MAX_TIMER_MS, MAX_TIMER_MS);
|
|
38429
|
+
this.arm(my, data, symbol, Math.max(delay, MIN_TIMER_MS));
|
|
38430
|
+
}
|
|
38431
|
+
arm(my, data, symbol, delay) {
|
|
38432
|
+
this.clearTimer();
|
|
38433
|
+
this.timer = setTimeout(() => {
|
|
38434
|
+
this.timer = null;
|
|
38435
|
+
if (my !== this.epoch) return;
|
|
38436
|
+
void this.evaluate(my, data, symbol);
|
|
38437
|
+
}, delay);
|
|
38438
|
+
}
|
|
38439
|
+
clearTimer() {
|
|
38440
|
+
if (this.timer != null) clearTimeout(this.timer);
|
|
38441
|
+
this.timer = null;
|
|
38442
|
+
}
|
|
38443
|
+
};
|
|
38444
|
+
|
|
38251
38445
|
// src/widget/watermark.ts
|
|
38252
38446
|
var MAX_FONT_PX = 36;
|
|
38253
38447
|
var MIN_FONT_PX = 12;
|
|
@@ -38761,7 +38955,10 @@ var ChartCell = class {
|
|
|
38761
38955
|
this.activeRangeId = null;
|
|
38762
38956
|
/** Latched verdict of {@link sessionAvailable} (async metadata, sticky per symbol). */
|
|
38763
38957
|
this.sessionAvailableFlag = false;
|
|
38764
|
-
/**
|
|
38958
|
+
/** Latched: the symbol's extended tape wraps midnight (an overnight roll market) —
|
|
38959
|
+
* one extended-hours shading phase instead of the pre/post split. */
|
|
38960
|
+
this.sessionOvernightFlag = false;
|
|
38961
|
+
/** Keeps the session shading on the symbol's real calendar (see {@link SessionShadingTracker}). */
|
|
38765
38962
|
this.sessionShading = new SessionShadingTracker((zones) => this.inner?.renderer.set("sessionZones", zones));
|
|
38766
38963
|
this.manifest = [];
|
|
38767
38964
|
/** A restored ledger's manifest entry NAMES, waiting for the manifest to resolve
|
|
@@ -39014,8 +39211,10 @@ var ChartCell = class {
|
|
|
39014
39211
|
void chart.data.symbolInfo(symbol).then((si) => {
|
|
39015
39212
|
if (this.inner !== chart) return;
|
|
39016
39213
|
const available = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
|
|
39017
|
-
|
|
39214
|
+
const overnight = parseSessionSpec(si)?.overnight === true;
|
|
39215
|
+
if (available !== this.sessionAvailableFlag || overnight !== this.sessionOvernightFlag) {
|
|
39018
39216
|
this.sessionAvailableFlag = available;
|
|
39217
|
+
this.sessionOvernightFlag = overnight;
|
|
39019
39218
|
this.deps.onMarketChanged(this.id);
|
|
39020
39219
|
this.pushSettingsSections();
|
|
39021
39220
|
}
|
|
@@ -39049,16 +39248,41 @@ var ChartCell = class {
|
|
|
39049
39248
|
/**
|
|
39050
39249
|
* (Re)contribute this cell's settings-dialog sections: status line parts, the
|
|
39051
39250
|
* per-cell fetch depth, the watermark toggle, and — only while the cell's symbol
|
|
39052
|
-
* HAS sessions — the Trading session group (RTH/ETH switch + the
|
|
39053
|
-
*
|
|
39054
|
-
*
|
|
39055
|
-
* a
|
|
39251
|
+
* HAS sessions — the Trading session group (RTH/ETH switch + the session shading
|
|
39252
|
+
* colors: pre/post-market on day-split tapes, one extended-hours swatch on
|
|
39253
|
+
* overnight roll markets) inside the Symbol tab. Bars/watermark/titles are
|
|
39254
|
+
* persistable cell state; a depth-only reload is silent, so mark dirty here.
|
|
39255
|
+
* Re-run whenever a gate changes (the dialog reads the sections on open).
|
|
39056
39256
|
*/
|
|
39057
39257
|
pushSettingsSections() {
|
|
39058
39258
|
const chart = this.inner;
|
|
39059
39259
|
if (!chart) return;
|
|
39060
39260
|
const rth = "Regular hours (RTH)";
|
|
39061
39261
|
const eth = "Extended hours (ETH)";
|
|
39262
|
+
const shadeRows = this.sessionOvernightFlag ? [
|
|
39263
|
+
{
|
|
39264
|
+
kind: "color",
|
|
39265
|
+
label: "Extended hours",
|
|
39266
|
+
id: "extended-color",
|
|
39267
|
+
get: () => this.sessionShadeColor("extendedColor"),
|
|
39268
|
+
set: (v) => this.setSessionShadeColor("extendedColor", v)
|
|
39269
|
+
}
|
|
39270
|
+
] : [
|
|
39271
|
+
{
|
|
39272
|
+
kind: "color",
|
|
39273
|
+
label: "Pre-market",
|
|
39274
|
+
id: "premarket-color",
|
|
39275
|
+
get: () => this.sessionShadeColor("premarketColor"),
|
|
39276
|
+
set: (v) => this.setSessionShadeColor("premarketColor", v)
|
|
39277
|
+
},
|
|
39278
|
+
{
|
|
39279
|
+
kind: "color",
|
|
39280
|
+
label: "Post-market",
|
|
39281
|
+
id: "postmarket-color",
|
|
39282
|
+
get: () => this.sessionShadeColor("postmarketColor"),
|
|
39283
|
+
set: (v) => this.setSessionShadeColor("postmarketColor", v)
|
|
39284
|
+
}
|
|
39285
|
+
];
|
|
39062
39286
|
const sessionSection = {
|
|
39063
39287
|
title: "Trading session",
|
|
39064
39288
|
id: "trading-session",
|
|
@@ -39072,20 +39296,7 @@ var ChartCell = class {
|
|
|
39072
39296
|
get: () => this.session === "extended" ? eth : rth,
|
|
39073
39297
|
set: (v) => this.setSession(v === eth ? "extended" : "regular")
|
|
39074
39298
|
},
|
|
39075
|
-
|
|
39076
|
-
kind: "color",
|
|
39077
|
-
label: "Pre-market",
|
|
39078
|
-
id: "premarket-color",
|
|
39079
|
-
get: () => this.sessionShadeColor("premarketColor"),
|
|
39080
|
-
set: (v) => this.setSessionShadeColor("premarketColor", v)
|
|
39081
|
-
},
|
|
39082
|
-
{
|
|
39083
|
-
kind: "color",
|
|
39084
|
-
label: "Post-market",
|
|
39085
|
-
id: "postmarket-color",
|
|
39086
|
-
get: () => this.sessionShadeColor("postmarketColor"),
|
|
39087
|
-
set: (v) => this.setSessionShadeColor("postmarketColor", v)
|
|
39088
|
-
}
|
|
39299
|
+
...shadeRows
|
|
39089
39300
|
]
|
|
39090
39301
|
};
|
|
39091
39302
|
const advanced = {
|
|
@@ -39329,6 +39540,10 @@ var ChartCell = class {
|
|
|
39329
39540
|
this.deps.activate(this.id);
|
|
39330
39541
|
this.inner?.renderer.focus();
|
|
39331
39542
|
}
|
|
39543
|
+
/** Raster of this cell's chart (same pixels as the PNG download), or null. */
|
|
39544
|
+
screenshotCanvas() {
|
|
39545
|
+
return this.inner?.renderer.screenshotCanvas() ?? null;
|
|
39546
|
+
}
|
|
39332
39547
|
/** Download this cell's chart as a PNG (named after its market). */
|
|
39333
39548
|
downloadScreenshot() {
|
|
39334
39549
|
const url = this.inner?.renderer.screenshot();
|
|
@@ -39941,6 +40156,54 @@ var SplitterLayer = class {
|
|
|
39941
40156
|
}
|
|
39942
40157
|
};
|
|
39943
40158
|
|
|
40159
|
+
// src/workspace/screenshot.ts
|
|
40160
|
+
function destRect(tile, dpr) {
|
|
40161
|
+
const dx = Math.round(tile.x * dpr);
|
|
40162
|
+
const dy = Math.round(tile.y * dpr);
|
|
40163
|
+
return {
|
|
40164
|
+
dx,
|
|
40165
|
+
dy,
|
|
40166
|
+
dw: Math.round((tile.x + tile.width) * dpr) - dx,
|
|
40167
|
+
dh: Math.round((tile.y + tile.height) * dpr) - dy
|
|
40168
|
+
};
|
|
40169
|
+
}
|
|
40170
|
+
function tilesFromCellRects(grid, cells) {
|
|
40171
|
+
if (grid.width <= 0 || grid.height <= 0) return [];
|
|
40172
|
+
const tiles = [];
|
|
40173
|
+
for (const c of cells) {
|
|
40174
|
+
if (c.hidden || c.width <= 0 || c.height <= 0) continue;
|
|
40175
|
+
tiles.push({ x: c.left - grid.left, y: c.top - grid.top, width: c.width, height: c.height });
|
|
40176
|
+
}
|
|
40177
|
+
return tiles;
|
|
40178
|
+
}
|
|
40179
|
+
function compositeLayoutScreenshot(doc, frame, tiles) {
|
|
40180
|
+
if (frame.width <= 0 || frame.height <= 0 || tiles.length === 0) return null;
|
|
40181
|
+
const dpr = frame.dpr > 0 ? frame.dpr : 1;
|
|
40182
|
+
const out = doc.createElement("canvas");
|
|
40183
|
+
out.width = Math.max(1, Math.round(frame.width * dpr));
|
|
40184
|
+
out.height = Math.max(1, Math.round(frame.height * dpr));
|
|
40185
|
+
const ctx = out.getContext("2d");
|
|
40186
|
+
if (!ctx) return null;
|
|
40187
|
+
ctx.fillStyle = frame.gapColor;
|
|
40188
|
+
ctx.fillRect(0, 0, out.width, out.height);
|
|
40189
|
+
let painted = 0;
|
|
40190
|
+
for (const tile of tiles) {
|
|
40191
|
+
if (tile.width <= 0 || tile.height <= 0) continue;
|
|
40192
|
+
const { dx, dy, dw, dh } = destRect(tile, dpr);
|
|
40193
|
+
if (dw <= 0 || dh <= 0) continue;
|
|
40194
|
+
ctx.drawImage(tile.source, dx, dy, dw, dh);
|
|
40195
|
+
painted += 1;
|
|
40196
|
+
}
|
|
40197
|
+
if (painted === 0) return null;
|
|
40198
|
+
return out.toDataURL("image/png");
|
|
40199
|
+
}
|
|
40200
|
+
function triggerPngDownload(doc, url, filename) {
|
|
40201
|
+
const a = doc.createElement("a");
|
|
40202
|
+
a.href = url;
|
|
40203
|
+
a.download = filename;
|
|
40204
|
+
a.click();
|
|
40205
|
+
}
|
|
40206
|
+
|
|
39944
40207
|
// src/workspace/VelaWorkspace.ts
|
|
39945
40208
|
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "30", "60", "240", "D", "W", "M"];
|
|
39946
40209
|
var GAP_PX = 2;
|
|
@@ -40171,7 +40434,7 @@ var VelaWorkspace = class {
|
|
|
40171
40434
|
...picker ? { onIndicatorsClick: () => picker.open() } : {},
|
|
40172
40435
|
onUndoClick: () => this.active.history.undo(),
|
|
40173
40436
|
onRedoClick: () => this.active.history.redo(),
|
|
40174
|
-
onScreenshotClick: () => this.
|
|
40437
|
+
onScreenshotClick: () => this.downloadScreenshot(),
|
|
40175
40438
|
onAlertsClick: (anchor) => this.openAlertsMenu(anchor),
|
|
40176
40439
|
timeframe: "60",
|
|
40177
40440
|
timeframes: opts.timeframes ?? DEFAULT_TIMEFRAMES,
|
|
@@ -40230,6 +40493,7 @@ var VelaWorkspace = class {
|
|
|
40230
40493
|
this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
|
|
40231
40494
|
this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
|
|
40232
40495
|
this.dock.refresh();
|
|
40496
|
+
if (boot?.panels) this.dock.applyState(boot.panels);
|
|
40233
40497
|
this.root.appendChild(main);
|
|
40234
40498
|
this.toastHost = new Toast(this.gridEl);
|
|
40235
40499
|
const attribution = rendererDefaults().attribution;
|
|
@@ -40241,6 +40505,7 @@ var VelaWorkspace = class {
|
|
|
40241
40505
|
bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H4}px) + 10px)`,
|
|
40242
40506
|
zIndex: "11"
|
|
40243
40507
|
});
|
|
40508
|
+
mark.dataset.velaScreenshot = "1";
|
|
40244
40509
|
this.gridEl.appendChild(mark);
|
|
40245
40510
|
this.attributionMark = mark;
|
|
40246
40511
|
}
|
|
@@ -40377,6 +40642,56 @@ var VelaWorkspace = class {
|
|
|
40377
40642
|
get chart() {
|
|
40378
40643
|
return this.active.chart;
|
|
40379
40644
|
}
|
|
40645
|
+
/**
|
|
40646
|
+
* PNG data URL of the visible layout: every live cell in its grid slot, or the
|
|
40647
|
+
* maximized cell alone. Same pixels the screenshot button downloads. A single
|
|
40648
|
+
* visible cell returns that cell's own chart export (the one-chart case).
|
|
40649
|
+
*/
|
|
40650
|
+
screenshot() {
|
|
40651
|
+
const cells = this.shotCells();
|
|
40652
|
+
if (cells.length === 0) return null;
|
|
40653
|
+
if (cells.length === 1) return cells[0].chart.renderer.screenshot();
|
|
40654
|
+
return this.compositeLayoutShot(cells);
|
|
40655
|
+
}
|
|
40656
|
+
/** Download {@link screenshot} as a PNG. A multi-cell layout is named
|
|
40657
|
+
* `vela-layout.png`; one visible cell keeps `${symbol}-${timeframe}.png`. */
|
|
40658
|
+
downloadScreenshot() {
|
|
40659
|
+
const cells = this.shotCells();
|
|
40660
|
+
if (cells.length <= 1) {
|
|
40661
|
+
(cells[0] ?? this.active).downloadScreenshot();
|
|
40662
|
+
return;
|
|
40663
|
+
}
|
|
40664
|
+
const url = this.compositeLayoutShot(cells);
|
|
40665
|
+
if (!url) return;
|
|
40666
|
+
triggerPngDownload(this.root.ownerDocument, url, "vela-layout.png");
|
|
40667
|
+
}
|
|
40668
|
+
/** Live cells the screenshot should include — maximized siblings are `hidden`. */
|
|
40669
|
+
shotCells() {
|
|
40670
|
+
return this.cells().filter((c) => c.host.style.visibility !== "hidden");
|
|
40671
|
+
}
|
|
40672
|
+
/** Place every cell's raster onto one canvas the size of the grid. */
|
|
40673
|
+
compositeLayoutShot(cells) {
|
|
40674
|
+
const gridEl = this.gridEl;
|
|
40675
|
+
const grid = gridEl.getBoundingClientRect();
|
|
40676
|
+
const win = gridEl.ownerDocument.defaultView;
|
|
40677
|
+
const dpr = win?.devicePixelRatio ?? 1;
|
|
40678
|
+
const gapColor = win ? win.getComputedStyle(gridEl).backgroundColor : "";
|
|
40679
|
+
const tiles = [];
|
|
40680
|
+
for (const cell of cells) {
|
|
40681
|
+
const source = cell.screenshotCanvas();
|
|
40682
|
+
if (!source) continue;
|
|
40683
|
+
const r = cell.host.getBoundingClientRect();
|
|
40684
|
+
const place = tilesFromCellRects(grid, [{ left: r.left, top: r.top, width: r.width, height: r.height }])[0];
|
|
40685
|
+
if (!place) continue;
|
|
40686
|
+
tiles.push({ source, ...place });
|
|
40687
|
+
}
|
|
40688
|
+
return compositeLayoutScreenshot(gridEl.ownerDocument, {
|
|
40689
|
+
width: grid.width,
|
|
40690
|
+
height: grid.height,
|
|
40691
|
+
dpr,
|
|
40692
|
+
gapColor: gapColor || "#000000"
|
|
40693
|
+
}, tiles);
|
|
40694
|
+
}
|
|
40380
40695
|
setActiveCell(id) {
|
|
40381
40696
|
if (id === this.activeId || this.destroyed) return;
|
|
40382
40697
|
const prev2 = this.activeId;
|
|
@@ -41423,7 +41738,7 @@ var VelaWorkspace = class {
|
|
|
41423
41738
|
this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
|
|
41424
41739
|
host: this.root,
|
|
41425
41740
|
...has("undo-redo") ? { onUndo: () => this.active.history.undo(), onRedo: () => this.active.history.redo() } : {},
|
|
41426
|
-
...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.
|
|
41741
|
+
...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.downloadScreenshot() } : {},
|
|
41427
41742
|
canUndo: () => this.active.history.canUndo,
|
|
41428
41743
|
canRedo: () => this.active.history.canRedo,
|
|
41429
41744
|
priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
|
|
@@ -41524,9 +41839,9 @@ var VelaWorkspace = class {
|
|
|
41524
41839
|
this.keymap.register({
|
|
41525
41840
|
id: "chart.screenshot",
|
|
41526
41841
|
keys: "mod+alt+s",
|
|
41527
|
-
label: ov ? ov.label : "Download a
|
|
41842
|
+
label: ov ? ov.label : "Download a screenshot of the layout",
|
|
41528
41843
|
category: "Chart",
|
|
41529
|
-
run: ov ? () => this.runOverride(ov) : () => this.
|
|
41844
|
+
run: ov ? () => this.runOverride(ov) : () => this.downloadScreenshot()
|
|
41530
41845
|
});
|
|
41531
41846
|
}
|
|
41532
41847
|
this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
|