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