@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.
Files changed (49) hide show
  1. package/NOTICE +28 -28
  2. package/README.md +1 -1
  3. package/dist/{DataProvider-DhzstpQb.d.ts → DataProvider-XLdJB86x.d.ts} +1 -1
  4. package/dist/{DataProvider-DlMtrwqM.d.cts → DataProvider-xH2VBcFT.d.cts} +1 -1
  5. package/dist/{chunk-F4M24ANM.js → chunk-32A7KHFM.js} +73 -3
  6. package/dist/{chunk-KG4YT3TI.js → chunk-6DPAYEGO.js} +8 -1
  7. package/dist/{chunk-G52XAKZY.js → chunk-7C7NWEXH.js} +345 -148
  8. package/dist/{chunk-JKGA36ZM.js → chunk-J5QIR2Z3.js} +165 -130
  9. package/dist/{chunk-MHY7MVXH.js → chunk-MWDNPGU3.js} +7 -1
  10. package/dist/{chunk-G77Y7LK2.js → chunk-MWS4CUJL.js} +2 -2
  11. package/dist/{contributions-37nni40G.d.ts → contributions-h5BzEiV2.d.ts} +13 -2
  12. package/dist/{contributions-lPojhTxI.d.cts → contributions-x7iamlnd.d.cts} +13 -2
  13. package/dist/index.cjs +176 -127
  14. package/dist/index.d.cts +20 -12
  15. package/dist/index.d.ts +20 -12
  16. package/dist/index.js +4 -4
  17. package/dist/{options-CYS5Wmlx.d.ts → options-bdSAtp7y.d.cts} +5 -0
  18. package/dist/{options-CYS5Wmlx.d.cts → options-bdSAtp7y.d.ts} +5 -0
  19. package/dist/{plugin-Cwikpz1m.d.cts → plugin-2sJcf5mk.d.cts} +11 -8
  20. package/dist/{plugin-DHyaoMjW.d.ts → plugin-BLaNPh55.d.ts} +11 -8
  21. package/dist/plugin.cjs +8 -0
  22. package/dist/plugin.d.cts +5 -5
  23. package/dist/plugin.d.ts +5 -5
  24. package/dist/plugin.js +2 -2
  25. package/dist/providers/binance.d.cts +2 -2
  26. package/dist/providers/binance.d.ts +2 -2
  27. package/dist/providers/coinbase.d.cts +2 -2
  28. package/dist/providers/coinbase.d.ts +2 -2
  29. package/dist/providers/hyperliquid.d.cts +2 -2
  30. package/dist/providers/hyperliquid.d.ts +2 -2
  31. package/dist/{side-panel-CT9ZwIGz.d.cts → side-panel-HF0IAzwf.d.cts} +17 -0
  32. package/dist/{side-panel-CT9ZwIGz.d.ts → side-panel-HF0IAzwf.d.ts} +17 -0
  33. package/dist/{statusline-model-D-q_OOx9.d.ts → statusline-model--tH0VWCy.d.cts} +6 -4
  34. package/dist/{statusline-model-CiJm-riV.d.cts → statusline-model-CLpaO3Dl.d.ts} +6 -4
  35. package/dist/ui.cjs +12 -0
  36. package/dist/ui.d.cts +1 -1
  37. package/dist/ui.d.ts +1 -1
  38. package/dist/ui.js +3 -3
  39. package/dist/vela.global.js +176 -127
  40. package/dist/vela.global.min.js +52 -46
  41. package/dist/widget.cjs +587 -272
  42. package/dist/widget.d.cts +15 -11
  43. package/dist/widget.d.ts +15 -11
  44. package/dist/widget.js +7 -7
  45. package/dist/workspace.cjs +587 -272
  46. package/dist/workspace.d.cts +29 -10
  47. package/dist/workspace.d.ts +29 -10
  48. package/dist/workspace.js +6 -6
  49. 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: SLATE },
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-accent); }
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
- header.append(this.heading, this.slot, close);
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
- return out.open || out.widths ? out : null;
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-tabs { display: flex; gap: 14px; margin: 12px 2px 6px; border-bottom: 1px solid var(--vela-border); padding-bottom: 8px; }
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(searchRow, this.tabs, this.list),
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", () => this.refresh());
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
- return out.open || out.widths ? out : null;
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): bucket bars
23697
- * sharing a rounded pixel column into one high-low stick draw cost bounded by
23698
- * screen width, not bar count. Bucket color follows first-open→last-close.
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
- let px = NaN;
23702
- let lo = Infinity;
23703
- let hi = -Infinity;
23704
- let openV = 0;
23705
- let closeV = 0;
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 washes from the
25032
- * config's session colors) — consumed by the same painting path as {@link highlights}. */
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: bucket every bar sharing a rounded pixel column into ONE
25577
- * high-low stick, so draw cost is bounded by screen width (not bar count) when
25578
- * zoomed far out. The bucket's color follows its first-open→last-close
25579
- * direction (a barcolor() on the head bar still wins).
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
- let px = NaN;
25583
- let lo = Infinity;
25584
- let hi = -Infinity;
25585
- let openV = 0;
25586
- let closeV = 0;
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(px, hY);
25598
- ctx.lineTo(px, lY);
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, formatStamp(coords.logicalToTime(logical), scene.timezone), chipBg, "center", true, theme.background);
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, formatStamp(ext.time, scene.timezone), chipBg, "center", true, theme.background);
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 (pre/post-market) paint first, host highlights on top. */
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 x1 = coords.timeToX(band.from);
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
- screenshot() {
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.toDataURL("image/png");
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(formatStamp2(bar.time, this.scene.timezone));
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 formatStamp2(ms, timeZone) {
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 (start >= end || end > 1440) return null;
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
- if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
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
- /** Keeps the pre/post-market shading on the symbol's real calendar (see {@link SessionShadingTracker}). */
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
- if (available !== this.sessionAvailableFlag) {
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 pre/post-market
39053
- * shading colors) inside the Symbol tab. Bars/watermark/titles are persistable
39054
- * cell state; a depth-only reload is silent, so mark dirty here. Re-run whenever
39055
- * a gate changes (the dialog reads the sections on open).
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.active.downloadScreenshot(),
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.active.downloadScreenshot() } : {},
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 chart screenshot",
41842
+ label: ov ? ov.label : "Download a screenshot of the layout",
41528
41843
  category: "Chart",
41529
- run: ov ? () => this.runOverride(ov) : () => this.active.downloadScreenshot()
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") });