@luxalgo/vela 0.6.14 → 0.6.16

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 (51) hide show
  1. package/NOTICE +28 -28
  2. package/README.md +9 -2
  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-KG4YT3TI.js → chunk-6DPAYEGO.js} +8 -1
  6. package/dist/chunk-FE6V2YAQ.js +1 -0
  7. package/dist/{chunk-N2Q5EEFH.js → chunk-GIJYO4LO.js} +2596 -242
  8. package/dist/{chunk-F4M24ANM.js → chunk-I77F2SVE.js} +80 -4
  9. package/dist/{chunk-MHY7MVXH.js → chunk-MGU3USTP.js} +29 -16
  10. package/dist/{chunk-G77Y7LK2.js → chunk-Q3TFJNJW.js} +2 -2
  11. package/dist/{chunk-64TF7JAJ.js → chunk-SEOO6YG7.js} +408 -184
  12. package/dist/{contributions-lPojhTxI.d.cts → contributions-BEkxwA5F.d.cts} +40 -13
  13. package/dist/{contributions-37nni40G.d.ts → contributions-CoAX7_SF.d.ts} +40 -13
  14. package/dist/index.cjs +2755 -380
  15. package/dist/index.d.cts +20 -12
  16. package/dist/index.d.ts +20 -12
  17. package/dist/index.js +5 -5
  18. package/dist/{options-CYS5Wmlx.d.ts → options-bdSAtp7y.d.cts} +5 -0
  19. package/dist/{options-CYS5Wmlx.d.cts → options-bdSAtp7y.d.ts} +5 -0
  20. package/dist/{plugin-DHyaoMjW.d.ts → plugin-CNUgbRNq.d.ts} +11 -8
  21. package/dist/{plugin-Cwikpz1m.d.cts → plugin-Dxyq35Zf.d.cts} +11 -8
  22. package/dist/plugin.cjs +8 -0
  23. package/dist/plugin.d.cts +5 -5
  24. package/dist/plugin.d.ts +5 -5
  25. package/dist/plugin.js +3 -3
  26. package/dist/providers/binance.d.cts +2 -2
  27. package/dist/providers/binance.d.ts +2 -2
  28. package/dist/providers/coinbase.d.cts +2 -2
  29. package/dist/providers/coinbase.d.ts +2 -2
  30. package/dist/providers/hyperliquid.d.cts +2 -2
  31. package/dist/providers/hyperliquid.d.ts +2 -2
  32. package/dist/{side-panel-CT9ZwIGz.d.cts → side-panel-HF0IAzwf.d.cts} +17 -0
  33. package/dist/{side-panel-CT9ZwIGz.d.ts → side-panel-HF0IAzwf.d.ts} +17 -0
  34. package/dist/{statusline-model-D-q_OOx9.d.ts → statusline-model-CbPza-Wc.d.cts} +6 -4
  35. package/dist/{statusline-model-CiJm-riV.d.cts → statusline-model-D7cWkw8A.d.ts} +6 -4
  36. package/dist/ui.cjs +34 -15
  37. package/dist/ui.d.cts +1 -1
  38. package/dist/ui.d.ts +1 -1
  39. package/dist/ui.js +3 -3
  40. package/dist/vela.global.js +2675 -300
  41. package/dist/vela.global.min.js +58 -52
  42. package/dist/widget.cjs +3296 -622
  43. package/dist/widget.d.cts +15 -11
  44. package/dist/widget.d.ts +15 -11
  45. package/dist/widget.js +7 -7
  46. package/dist/workspace.cjs +3296 -622
  47. package/dist/workspace.d.cts +49 -19
  48. package/dist/workspace.d.ts +49 -19
  49. package/dist/workspace.js +6 -6
  50. package/package.json +1 -1
  51. package/dist/chunk-H26BEHF4.js +0 -7
@@ -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-N2Q5EEFH.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-GIJYO4LO.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-I77F2SVE.js';
3
+ import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-Q3TFJNJW.js';
4
+ import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-MGU3USTP.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,12 @@ function filterSymbols(list, query, limit = 100, top = TOP_TICKERS) {
2986
2999
  }
2987
3000
  var STYLE_ID6 = "vela-widget-symbolpicker";
2988
3001
  var CSS6 = `
3002
+ /* The dialog body is flush (a flex column): search + market tabs are a fixed head
3003
+ and the result list is the ONLY scroller, in both layouts. A sticky head over a
3004
+ scrolling padded body is not an option \u2014 browsers pin sticky boxes to the
3005
+ scroller's content box, so the head lands one padding below the top and covers
3006
+ the first row. */
3007
+ .vela-sp-head { flex: none; padding: var(--vela-space-4) var(--vela-space-4) 0; }
2989
3008
  .vela-sp-searchrow {
2990
3009
  display: flex;
2991
3010
  align-items: center;
@@ -3005,15 +3024,18 @@ var CSS6 = `
3005
3024
  border: none;
3006
3025
  font-size: 14px;
3007
3026
  outline: none;
3027
+ text-transform: uppercase;
3008
3028
  }
3009
- .vela-sp-tabs { display: flex; gap: 14px; margin: 12px 2px 6px; border-bottom: 1px solid var(--vela-border); padding-bottom: 8px; }
3029
+ .vela-sp-input::placeholder { text-transform: none; }
3030
+ .vela-sp-tabs { display: flex; gap: 14px; margin: 12px 2px 0; border-bottom: 1px solid var(--vela-border); padding-bottom: 8px; }
3010
3031
  /* Mobile (fullscreen dialog): the asset-class strip scrolls sideways instead of
3011
- overflowing the body, and the result list stops capping itself \u2014 the body owns
3012
- the scrolling in the fullscreen presentation. */
3032
+ overflowing the body, and the result list stops capping itself \u2014 it fills the
3033
+ rest of the screen. The bottom inset (+ safe area) is the kit's: every mobile
3034
+ dialog body carries it, flush or not, so the list adds none of its own. */
3013
3035
  [data-layout='mobile'] .vela-sp-tabs { overflow-x: auto; scrollbar-width: none; gap: 6px; }
3014
3036
  [data-layout='mobile'] .vela-sp-tabs::-webkit-scrollbar { display: none; }
3015
3037
  [data-layout='mobile'] .vela-sp-tab { flex: none; padding: 7px 10px; }
3016
- [data-layout='mobile'] .vela-sp-list { max-height: none; }
3038
+ [data-layout='mobile'] .vela-sp-list { flex: 1 1 auto; max-height: none; margin-bottom: 0; }
3017
3039
  .vela-sp-tab {
3018
3040
  all: unset;
3019
3041
  padding: 3px 10px;
@@ -3025,7 +3047,12 @@ var CSS6 = `
3025
3047
  }
3026
3048
  .vela-sp-tab:hover { color: var(--vela-fg); }
3027
3049
  .vela-sp-tab[data-active] { background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
3028
- .vela-sp-list { margin-top: var(--vela-space-2); max-height: 46vh; overflow: auto; }
3050
+ .vela-sp-list {
3051
+ min-height: 0;
3052
+ margin: var(--vela-space-2) var(--vela-space-4) var(--vela-space-4);
3053
+ max-height: 46vh;
3054
+ overflow: auto;
3055
+ }
3029
3056
  .vela-sp-list::-webkit-scrollbar { width: 8px; }
3030
3057
  .vela-sp-list::-webkit-scrollbar-thumb {
3031
3058
  background: var(--vela-scroll);
@@ -3055,7 +3082,7 @@ var CSS6 = `
3055
3082
  font-weight: 700;
3056
3083
  }
3057
3084
  .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; }
3085
+ .vela-sp-ticker { font-weight: 700; color: var(--vela-fg-bright); font-size: 14px; text-transform: uppercase; }
3059
3086
  .vela-sp-desc { color: var(--vela-fg-muted); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3060
3087
  .vela-sp-badge {
3061
3088
  flex: none;
@@ -3118,6 +3145,8 @@ var SymbolPicker = class {
3118
3145
  searchRow.append(iconEl("search", doc), this.input);
3119
3146
  this.tabs = doc.createElement("div");
3120
3147
  this.tabs.className = "vela-sp-tabs";
3148
+ const head = doc.createElement("div");
3149
+ head.className = "vela-sp-head";
3121
3150
  for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
3122
3151
  const b = doc.createElement("button");
3123
3152
  b.className = "vela-sp-tab";
@@ -3131,6 +3160,7 @@ var SymbolPicker = class {
3131
3160
  });
3132
3161
  this.tabs.appendChild(b);
3133
3162
  }
3163
+ head.append(searchRow, this.tabs);
3134
3164
  this.list = doc.createElement("div");
3135
3165
  this.list.className = "vela-sp-list";
3136
3166
  this.list.addEventListener("scroll", () => {
@@ -3143,21 +3173,33 @@ var SymbolPicker = class {
3143
3173
  title: "Symbol Search",
3144
3174
  host: opts.host,
3145
3175
  closeOnInteractOutside: true,
3146
- content: (body) => body.append(searchRow, this.tabs, this.list),
3176
+ flush: true,
3177
+ content: (body) => body.append(head, this.list),
3147
3178
  onOpenChange: (open) => {
3148
3179
  if (open) {
3149
- this.input.value = this.seed;
3180
+ this.input.value = this.seed.toUpperCase();
3150
3181
  this.seed = "";
3151
3182
  this.refresh();
3152
3183
  setTimeout(() => {
3153
3184
  this.input.focus();
3154
3185
  this.input.setSelectionRange(this.input.value.length, this.input.value.length);
3155
3186
  }, 0);
3187
+ } else {
3188
+ this.input.blur();
3156
3189
  }
3157
3190
  opts.onOpenChange?.(open);
3158
3191
  }
3159
3192
  });
3160
- this.input.addEventListener("input", () => this.refresh());
3193
+ this.input.addEventListener("input", () => {
3194
+ const next = this.input.value.toUpperCase();
3195
+ if (this.input.value !== next) {
3196
+ const start = this.input.selectionStart;
3197
+ const end = this.input.selectionEnd;
3198
+ this.input.value = next;
3199
+ if (start != null && end != null) this.input.setSelectionRange(start, end);
3200
+ }
3201
+ this.refresh();
3202
+ });
3161
3203
  this.input.addEventListener("keydown", (e) => {
3162
3204
  if (e.key === "ArrowDown") this.moveHighlight(1);
3163
3205
  else if (e.key === "ArrowUp") this.moveHighlight(-1);
@@ -3889,7 +3931,11 @@ function sanitizePanels(raw) {
3889
3931
  }
3890
3932
  if (Object.keys(widths).length > 0) out.widths = widths;
3891
3933
  }
3892
- return out.open || out.widths ? out : null;
3934
+ if (Array.isArray(p.pinned)) {
3935
+ const pinned = p.pinned.filter((id) => typeof id === "string" && id !== "");
3936
+ if (pinned.length > 0) out.pinned = [...new Set(pinned)];
3937
+ }
3938
+ return out.open || out.widths || out.pinned ? out : null;
3893
3939
  }
3894
3940
  function sanitizeTrackSizes(raw) {
3895
3941
  if (raw == null || typeof raw !== "object") return null;
@@ -4151,6 +4197,7 @@ var MARKET_LABELS = {
4151
4197
  open: "Market Open",
4152
4198
  pre: "Pre-Market",
4153
4199
  post: "Post-Market",
4200
+ extended: "Extended Hours",
4154
4201
  closed: "Market Closed",
4155
4202
  holiday: "Market Holiday"
4156
4203
  };
@@ -4158,6 +4205,7 @@ var MARKET_INKS = {
4158
4205
  open: "var(--vela-up)",
4159
4206
  pre: SESSION_PRE,
4160
4207
  post: SESSION_POST,
4208
+ extended: SESSION_POST,
4161
4209
  closed: SESSION_OFF,
4162
4210
  holiday: SESSION_OFF
4163
4211
  };
@@ -4781,118 +4829,17 @@ var ChartContextMenu = class {
4781
4829
  // src/core/options.ts
4782
4830
  var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
4783
4831
 
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
4832
  // src/widget/session-shading.ts
4887
4833
  var DAY_MS = 864e5;
4888
4834
  var MINUTE_MS = 6e4;
4889
- function parseWindow(text) {
4835
+ function parseWindow(text, allowOvernight = false) {
4890
4836
  if (typeof text !== "string") return null;
4891
4837
  const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
4892
4838
  if (!m) return null;
4893
4839
  const start = Number(m[1]) * 60 + Number(m[2]);
4894
4840
  const end = Number(m[3]) * 60 + Number(m[4]);
4895
- if (start >= end || end > 1440) return null;
4841
+ if (end > 1440 || start >= 1440) return null;
4842
+ if (start >= end && !allowOvernight) return null;
4896
4843
  return { start, end };
4897
4844
  }
4898
4845
  function parseSessionSpec(si) {
@@ -4902,9 +4849,13 @@ function parseSessionSpec(si) {
4902
4849
  if (!regular) return null;
4903
4850
  const tz = si?.["timezone"];
4904
4851
  const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
4905
- const ext = parseWindow(si?.["session_extended"]);
4852
+ const ext = parseWindow(si?.["session_extended"], true);
4853
+ if (ext && ext.start >= ext.end) {
4854
+ if (ext.start >= regular.end && ext.end >= regular.end) return { regular, extended: ext, overnight: true, timezone };
4855
+ return { regular, extended: { start: 0, end: 1440 }, overnight: false, timezone };
4856
+ }
4906
4857
  const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
4907
- return { regular, extended, timezone };
4858
+ return { regular, extended, overnight: false, timezone };
4908
4859
  }
4909
4860
  var dtfCache = /* @__PURE__ */ new Map();
4910
4861
  function civilFormatter(tz) {
@@ -4940,27 +4891,48 @@ function civilParts(dtf, ms) {
4940
4891
  minute: Number(get("minute"))
4941
4892
  };
4942
4893
  }
4894
+ function mergeAbutting(bands) {
4895
+ const sorted = [...bands].sort((a, b) => a[0] - b[0]);
4896
+ const out = [];
4897
+ for (const [s, e] of sorted) {
4898
+ if (e <= s) continue;
4899
+ const last = out[out.length - 1];
4900
+ if (last && s <= last[1]) last[1] = Math.max(last[1], e);
4901
+ else out.push([s, e]);
4902
+ }
4903
+ return out;
4904
+ }
4943
4905
  function expandSessionZones(spec, from, to) {
4944
4906
  const pre = [];
4945
4907
  const post = [];
4908
+ const extended = [];
4946
4909
  const dtf = civilFormatter(spec.timezone);
4947
- if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
4910
+ if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post, extended };
4948
4911
  const seen = /* @__PURE__ */ new Set();
4949
4912
  for (let cursor = from - DAY_MS; cursor < to + DAY_MS; cursor += DAY_MS) {
4950
4913
  const civil = civilParts(dtf, cursor);
4951
4914
  const key = civil.year * 1e4 + civil.month * 100 + civil.day;
4952
4915
  if (!Number.isFinite(key) || seen.has(key)) continue;
4953
4916
  seen.add(key);
4954
- if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
4917
+ const sunday = civil.weekday === "Sun";
4918
+ if (civil.weekday === "Sat" || sunday && !spec.overnight) continue;
4955
4919
  const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
4956
4920
  const noonGuess = naive + 720 * MINUTE_MS;
4957
4921
  const atNoon = civilParts(dtf, noonGuess);
4958
4922
  const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
4959
4923
  const at = (minutes) => naive + minutes * MINUTE_MS - offset;
4924
+ if (spec.overnight) {
4925
+ if (!sunday) {
4926
+ extended.push([at(0), at(spec.regular.start)]);
4927
+ if (spec.regular.end < spec.extended.end) extended.push([at(spec.regular.end), at(spec.extended.end)]);
4928
+ }
4929
+ if (civil.weekday !== "Fri") extended.push([at(spec.extended.start), at(1440)]);
4930
+ continue;
4931
+ }
4960
4932
  if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
4961
4933
  if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
4962
4934
  }
4963
- return { pre, post };
4935
+ return { pre, post, extended: mergeAbutting(extended) };
4964
4936
  }
4965
4937
  var COVER_PAD_MIN_MS = 2 * DAY_MS;
4966
4938
  var SessionShadingTracker = class {
@@ -5015,7 +4987,7 @@ var SessionShadingTracker = class {
5015
4987
  return;
5016
4988
  }
5017
4989
  if (this.session !== "extended" || !this.intraday) {
5018
- if (force) this.onZones({ pre: [], post: [] });
4990
+ if (force) this.onZones({ pre: [], post: [], extended: [] });
5019
4991
  return;
5020
4992
  }
5021
4993
  const from = Math.min(range.from, range.to);
@@ -5029,6 +5001,112 @@ var SessionShadingTracker = class {
5029
5001
  }
5030
5002
  };
5031
5003
 
5004
+ // src/widget/market-status.ts
5005
+ function civilDate(ms, tz) {
5006
+ try {
5007
+ return new Intl.DateTimeFormat("en-CA", { timeZone: tz }).format(ms);
5008
+ } catch {
5009
+ return "";
5010
+ }
5011
+ }
5012
+ function isWeekday(ms, tz) {
5013
+ try {
5014
+ const wd = new Intl.DateTimeFormat("en-US", { timeZone: tz, weekday: "short" }).format(ms);
5015
+ return wd !== "Sat" && wd !== "Sun";
5016
+ } catch {
5017
+ return false;
5018
+ }
5019
+ }
5020
+ function deriveMarketStatus(now, w, tz, overnight = false) {
5021
+ const within = (ws) => ws.find(([s, e]) => now >= s && now < e);
5022
+ if (within(w.regular)) return "open";
5023
+ const ext = within(w.extended);
5024
+ if (ext) {
5025
+ if (overnight) return "extended";
5026
+ return w.regular.some(([s]) => s >= now && s < ext[1]) ? "pre" : "post";
5027
+ }
5028
+ if (isWeekday(now, tz)) {
5029
+ const today = civilDate(now, tz);
5030
+ if (today !== "" && !w.extended.some(([s]) => civilDate(s, tz) === today)) return "holiday";
5031
+ }
5032
+ return "closed";
5033
+ }
5034
+ function nextStatusBoundary(now, w) {
5035
+ let next = null;
5036
+ for (const ws of [w.regular, w.extended]) {
5037
+ for (const [s, e] of ws) {
5038
+ for (const t of [s, e]) {
5039
+ if (t > now && (next == null || t < next)) next = t;
5040
+ }
5041
+ }
5042
+ }
5043
+ return next;
5044
+ }
5045
+ var FETCH_BACK_MS = 4 * 864e5;
5046
+ var FETCH_AHEAD_MS = 10 * 864e5;
5047
+ var MIN_TIMER_MS = 15e3;
5048
+ var MAX_TIMER_MS = 36e5;
5049
+ var RETRY_MS = 6e4;
5050
+ var MarketStatusTracker = class {
5051
+ constructor(onStatus) {
5052
+ this.onStatus = onStatus;
5053
+ /** Invalidates detached async work — bumped by every track()/stop(). */
5054
+ this.epoch = 0;
5055
+ this.timer = null;
5056
+ }
5057
+ /** (Re)bind to a chart's data surface + symbol and start evaluating. */
5058
+ track(data, symbol) {
5059
+ const my = ++this.epoch;
5060
+ this.clearTimer();
5061
+ void this.evaluate(my, data, symbol);
5062
+ }
5063
+ stop() {
5064
+ this.epoch += 1;
5065
+ this.clearTimer();
5066
+ }
5067
+ async evaluate(my, data, symbol) {
5068
+ const resolved = data.resolve(symbol);
5069
+ const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
5070
+ const si = await data.symbolInfo(symbol).catch(() => void 0);
5071
+ if (my !== this.epoch) return;
5072
+ const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
5073
+ if (!provider?.getCalendar || !hasSessions || !resolved) {
5074
+ this.onStatus("open");
5075
+ return;
5076
+ }
5077
+ const tz = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : "Etc/UTC";
5078
+ const now = Date.now();
5079
+ const range = { from: now - FETCH_BACK_MS, to: now + FETCH_AHEAD_MS };
5080
+ const [regular, extended] = await Promise.all([
5081
+ provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
5082
+ provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
5083
+ ]);
5084
+ if (my !== this.epoch) return;
5085
+ if (!regular || !extended) {
5086
+ this.arm(my, data, symbol, RETRY_MS);
5087
+ return;
5088
+ }
5089
+ const w = { regular, extended };
5090
+ const overnight = parseSessionSpec(si)?.overnight === true;
5091
+ this.onStatus(deriveMarketStatus(now, w, tz, overnight));
5092
+ const boundary = nextStatusBoundary(now, w);
5093
+ const delay = Math.min(boundary != null ? boundary - now : MAX_TIMER_MS, MAX_TIMER_MS);
5094
+ this.arm(my, data, symbol, Math.max(delay, MIN_TIMER_MS));
5095
+ }
5096
+ arm(my, data, symbol, delay) {
5097
+ this.clearTimer();
5098
+ this.timer = setTimeout(() => {
5099
+ this.timer = null;
5100
+ if (my !== this.epoch) return;
5101
+ void this.evaluate(my, data, symbol);
5102
+ }, delay);
5103
+ }
5104
+ clearTimer() {
5105
+ if (this.timer != null) clearTimeout(this.timer);
5106
+ this.timer = null;
5107
+ }
5108
+ };
5109
+
5032
5110
  // src/widget/glide.ts
5033
5111
  var MIN_SPAN_MS = 6e4;
5034
5112
  var FOLLOW = 0.25;
@@ -5394,7 +5472,10 @@ var ChartCell = class {
5394
5472
  this.activeRangeId = null;
5395
5473
  /** Latched verdict of {@link sessionAvailable} (async metadata, sticky per symbol). */
5396
5474
  this.sessionAvailableFlag = false;
5397
- /** Keeps the pre/post-market shading on the symbol's real calendar (see {@link SessionShadingTracker}). */
5475
+ /** Latched: the symbol's extended tape wraps midnight (an overnight roll market) —
5476
+ * one extended-hours shading phase instead of the pre/post split. */
5477
+ this.sessionOvernightFlag = false;
5478
+ /** Keeps the session shading on the symbol's real calendar (see {@link SessionShadingTracker}). */
5398
5479
  this.sessionShading = new SessionShadingTracker((zones) => this.inner?.renderer.set("sessionZones", zones));
5399
5480
  this.manifest = [];
5400
5481
  /** A restored ledger's manifest entry NAMES, waiting for the manifest to resolve
@@ -5403,8 +5484,8 @@ var ChartCell = class {
5403
5484
  /** The volume auto-add rides the cell's first candles (`load:end`); until then the
5404
5485
  * registry can't show it and the dehydrated ledger reports the INTENT instead. */
5405
5486
  this.volumeMayBePending = true;
5406
- /** Sync mirror of the chart's present native types — the removal handler diffs
5407
- * against it to identify (and record) whichever type was just removed. */
5487
+ /** Sync mirror of the chart's native instances (id + type) — the removal handler
5488
+ * looks the removed id up here to learn which type an undo must re-add. */
5408
5489
  this.presentNatives = [];
5409
5490
  this.rangeBars = 0;
5410
5491
  this.pendingRange = null;
@@ -5576,15 +5657,18 @@ var ChartCell = class {
5576
5657
  redo: () => this.dropInstance(snapshot)
5577
5658
  });
5578
5659
  } else {
5579
- const now = this.inner?.presentNativeIndicators() ?? [];
5580
- for (const type of this.presentNatives.filter((t) => !now.includes(t))) {
5660
+ const gone = this.presentNatives.find((n) => n.id === id2);
5661
+ if (gone) {
5662
+ const { type } = gone;
5663
+ let revived = null;
5581
5664
  this.history.push({
5582
5665
  undo: () => {
5583
- this.inner?.addNativeIndicator(type);
5666
+ revived = this.inner?.addNativeIndicator(type) ?? null;
5584
5667
  this.refreshNativeCatalog();
5585
5668
  },
5586
5669
  redo: () => {
5587
- this.inner?.addNativeIndicator(type).remove();
5670
+ revived?.remove();
5671
+ revived = null;
5588
5672
  this.refreshNativeCatalog();
5589
5673
  }
5590
5674
  });
@@ -5647,8 +5731,10 @@ var ChartCell = class {
5647
5731
  void chart.data.symbolInfo(symbol).then((si) => {
5648
5732
  if (this.inner !== chart) return;
5649
5733
  const available = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
5650
- if (available !== this.sessionAvailableFlag) {
5734
+ const overnight = parseSessionSpec(si)?.overnight === true;
5735
+ if (available !== this.sessionAvailableFlag || overnight !== this.sessionOvernightFlag) {
5651
5736
  this.sessionAvailableFlag = available;
5737
+ this.sessionOvernightFlag = overnight;
5652
5738
  this.deps.onMarketChanged(this.id);
5653
5739
  this.pushSettingsSections();
5654
5740
  }
@@ -5682,16 +5768,41 @@ var ChartCell = class {
5682
5768
  /**
5683
5769
  * (Re)contribute this cell's settings-dialog sections: status line parts, the
5684
5770
  * 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).
5771
+ * HAS sessions — the Trading session group (RTH/ETH switch + the session shading
5772
+ * colors: pre/post-market on day-split tapes, one extended-hours swatch on
5773
+ * overnight roll markets) inside the Symbol tab. Bars/watermark/titles are
5774
+ * persistable cell state; a depth-only reload is silent, so mark dirty here.
5775
+ * Re-run whenever a gate changes (the dialog reads the sections on open).
5689
5776
  */
5690
5777
  pushSettingsSections() {
5691
5778
  const chart = this.inner;
5692
5779
  if (!chart) return;
5693
5780
  const rth = "Regular hours (RTH)";
5694
5781
  const eth = "Extended hours (ETH)";
5782
+ const shadeRows = this.sessionOvernightFlag ? [
5783
+ {
5784
+ kind: "color",
5785
+ label: "Extended hours",
5786
+ id: "extended-color",
5787
+ get: () => this.sessionShadeColor("extendedColor"),
5788
+ set: (v) => this.setSessionShadeColor("extendedColor", v)
5789
+ }
5790
+ ] : [
5791
+ {
5792
+ kind: "color",
5793
+ label: "Pre-market",
5794
+ id: "premarket-color",
5795
+ get: () => this.sessionShadeColor("premarketColor"),
5796
+ set: (v) => this.setSessionShadeColor("premarketColor", v)
5797
+ },
5798
+ {
5799
+ kind: "color",
5800
+ label: "Post-market",
5801
+ id: "postmarket-color",
5802
+ get: () => this.sessionShadeColor("postmarketColor"),
5803
+ set: (v) => this.setSessionShadeColor("postmarketColor", v)
5804
+ }
5805
+ ];
5695
5806
  const sessionSection = {
5696
5807
  title: "Trading session",
5697
5808
  id: "trading-session",
@@ -5705,20 +5816,7 @@ var ChartCell = class {
5705
5816
  get: () => this.session === "extended" ? eth : rth,
5706
5817
  set: (v) => this.setSession(v === eth ? "extended" : "regular")
5707
5818
  },
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
- }
5819
+ ...shadeRows
5722
5820
  ]
5723
5821
  };
5724
5822
  const advanced = {
@@ -5875,9 +5973,9 @@ var ChartCell = class {
5875
5973
  const live = this.inner?.renderer.get("priceStyle");
5876
5974
  return typeof live === "string" ? live : this.state.priceStyle ?? "candles";
5877
5975
  }
5878
- /** Manifest instances + present natives — the topbar indicator count. */
5976
+ /** Manifest instances + native instances — the topbar indicator count. */
5879
5977
  get indicatorCount() {
5880
- return this.instances.length + this.nativeCatalog.filter((n) => n.present).length;
5978
+ return this.instances.length + (this.inner ? this.nativeHandles().length : this.presentNatives.length);
5881
5979
  }
5882
5980
  /** Switch this cell's market in place (the chart instance survives). The projection
5883
5981
  * is OPTIMISTIC — labels and chrome show the pick before the bars load; it follows
@@ -5962,6 +6060,10 @@ var ChartCell = class {
5962
6060
  this.deps.activate(this.id);
5963
6061
  this.inner?.renderer.focus();
5964
6062
  }
6063
+ /** Raster of this cell's chart (same pixels as the PNG download), or null. */
6064
+ screenshotCanvas() {
6065
+ return this.inner?.renderer.screenshotCanvas() ?? null;
6066
+ }
5965
6067
  /** Download this cell's chart as a PNG (named after its market). */
5966
6068
  downloadScreenshot() {
5967
6069
  const url = this.inner?.renderer.screenshot();
@@ -6005,13 +6107,15 @@ var ChartCell = class {
6005
6107
  if (!chart) return;
6006
6108
  this.volumeIntent = led.natives.includes("volume");
6007
6109
  this.history.silently(() => {
6008
- const present = chart.presentNativeIndicators();
6009
- for (const type of led.natives) {
6010
- if (!present.includes(type)) chart.addNativeIndicator(type);
6011
- }
6012
- for (const type of present) {
6013
- if (!led.natives.includes(type)) chart.addNativeIndicator(type).remove();
6110
+ const owed = /* @__PURE__ */ new Map();
6111
+ for (const type of led.natives) owed.set(type, (owed.get(type) ?? 0) + 1);
6112
+ for (const h of this.nativeHandles()) {
6113
+ const type = h.nativeType;
6114
+ const n = owed.get(type) ?? 0;
6115
+ if (n > 0) owed.set(type, n - 1);
6116
+ else h.remove();
6014
6117
  }
6118
+ for (const [type, n] of owed) for (let i = 0; i < n; i++) chart.addNativeIndicator(type);
6015
6119
  for (const it of [...this.instances]) this.dropInstance(it);
6016
6120
  if (this.manifest.length > 0) {
6017
6121
  for (const item of led.manifest) {
@@ -6029,34 +6133,42 @@ var ChartCell = class {
6029
6133
  this.syncPresentNatives();
6030
6134
  this.refreshNativeCatalog();
6031
6135
  }
6032
- /** The picker's library rows: supported natives first, then the manifest. */
6136
+ /** The supported natives in picker order: A→Z by title — registration order follows
6137
+ * the catalog's families, which is meaningless to the reader. This is the library
6138
+ * index space the picker hands back, so `libraryRows` and `addFromLibrary` MUST both
6139
+ * read it — indexing the unsorted catalog on add would land on a different study. */
6140
+ supportedNatives() {
6141
+ return this.nativeCatalog.filter((n) => n.supported).sort((a, b) => a.title.localeCompare(b.title, "en", { sensitivity: "base" }));
6142
+ }
6143
+ /** The picker's library rows: supported natives first (see {@link supportedNatives}),
6144
+ * then the manifest in the host's own order. */
6033
6145
  libraryRows() {
6034
6146
  return [
6035
- ...this.nativeCatalog.filter((n) => n.supported).map((n) => ({ name: n.title, category: "Vela", native: true, nativeType: n.type, beta: n.beta })),
6147
+ ...this.supportedNatives().map((n) => ({ name: n.title, category: "Vela", native: true, nativeType: n.type, beta: n.beta })),
6036
6148
  ...this.manifest.map((e) => ({ name: e.name, language: e.language, category: e.category }))
6037
6149
  ];
6038
6150
  }
6039
- /** The picker's on-chart rows: present natives first, then live instances. */
6151
+ /** The picker's on-chart rows: native instances first, then live script instances. */
6040
6152
  onChartRows() {
6041
6153
  return [
6042
- ...this.nativeCatalog.filter((n) => n.present).map((n) => ({ name: n.title, native: true, nativeType: n.type })),
6154
+ ...this.nativeHandles().map((h) => ({ name: h.title, native: true, nativeType: h.nativeType })),
6043
6155
  ...this.instances.map((it) => ({ name: it.entry.name, language: it.entry.language }))
6044
6156
  ];
6045
6157
  }
6046
6158
  /** Add by picker LIBRARY index (natives precede the manifest — mirrors libraryRows). */
6047
6159
  addFromLibrary(index) {
6048
- const natives = this.nativeCatalog.filter((n) => n.supported);
6160
+ const natives = this.supportedNatives();
6049
6161
  if (index < natives.length) this.addNative(natives[index].type);
6050
6162
  else {
6051
6163
  const entry = this.manifest[index - natives.length];
6052
6164
  if (entry) this.addManifestInstance(entry);
6053
6165
  }
6054
6166
  }
6055
- /** Remove by picker ON-CHART index (present natives precede instances). */
6167
+ /** Remove by picker ON-CHART index (native instances precede script instances — mirrors onChartRows). */
6056
6168
  removeFromChart(index) {
6057
- const present = this.nativeCatalog.filter((n) => n.present);
6058
- if (index < present.length) this.removeNative(present[index].type);
6059
- else this.removeInstance(index - present.length);
6169
+ const natives = this.nativeHandles();
6170
+ if (index < natives.length) this.removeNative(natives[index]);
6171
+ else this.removeInstance(index - natives.length);
6060
6172
  }
6061
6173
  /**
6062
6174
  * Add a script indicator through the PUBLIC seam (`ctx.addIndicator`) — same undo/
@@ -6116,30 +6228,42 @@ var ChartCell = class {
6116
6228
  it.handle = null;
6117
6229
  this.deps.onIndicatorsChanged(this.id);
6118
6230
  }
6119
- /** Add a native indicator (single-instance per type — the core dedupes). */
6231
+ /** Add a native indicator. A multi-instance type gets a fresh instance every time; a
6232
+ * single-instance type already on the chart hands back its existing one — nothing
6233
+ * changed, so nothing enters the undo timeline. */
6120
6234
  addNative(type) {
6121
- this.inner?.addNativeIndicator(type);
6235
+ const chart = this.inner;
6236
+ if (!chart) return;
6237
+ const before = new Set(chart.indicators().map((h) => h.id));
6238
+ let added = chart.addNativeIndicator(type);
6122
6239
  this.syncPresentNatives();
6123
6240
  this.refreshNativeCatalog();
6241
+ if (before.has(added.id)) return;
6124
6242
  this.history.push({
6125
6243
  undo: () => {
6126
- this.inner?.addNativeIndicator(type).remove();
6244
+ added?.remove();
6245
+ added = null;
6127
6246
  this.refreshNativeCatalog();
6128
6247
  },
6129
6248
  redo: () => {
6130
- this.inner?.addNativeIndicator(type);
6249
+ added = this.inner?.addNativeIndicator(type) ?? null;
6131
6250
  this.refreshNativeCatalog();
6132
6251
  }
6133
6252
  });
6134
6253
  }
6135
- removeNative(type) {
6136
- this.inner?.addNativeIndicator(type).remove();
6254
+ removeNative(handle) {
6255
+ handle.remove();
6137
6256
  this.refreshNativeCatalog();
6138
6257
  }
6139
- /** Sync mirror of the chart's present native types — the removal handler diffs
6140
- * against it to identify (and record) whichever type was just removed. */
6258
+ /** The chart's native instances, insertion order — the picker's on-chart rows and
6259
+ * the removal index space (script instances follow them). */
6260
+ nativeHandles() {
6261
+ return this.inner?.indicators().filter((h) => h.nativeType !== void 0) ?? [];
6262
+ }
6263
+ /** Sync mirror of the chart's native instances — the removal handler looks the
6264
+ * removed id up here (the registry has already forgotten it) to record its type. */
6141
6265
  syncPresentNatives() {
6142
- this.presentNatives = this.inner?.presentNativeIndicators() ?? [];
6266
+ this.presentNatives = this.nativeHandles().map((h) => ({ id: h.id, type: h.nativeType }));
6143
6267
  }
6144
6268
  /** Refresh the native catalog (supported/present flags) for this cell's market. */
6145
6269
  refreshNativeCatalog() {
@@ -7819,6 +7943,54 @@ function buildContext(host) {
7819
7943
  };
7820
7944
  }
7821
7945
 
7946
+ // src/workspace/screenshot.ts
7947
+ function destRect(tile, dpr) {
7948
+ const dx = Math.round(tile.x * dpr);
7949
+ const dy = Math.round(tile.y * dpr);
7950
+ return {
7951
+ dx,
7952
+ dy,
7953
+ dw: Math.round((tile.x + tile.width) * dpr) - dx,
7954
+ dh: Math.round((tile.y + tile.height) * dpr) - dy
7955
+ };
7956
+ }
7957
+ function tilesFromCellRects(grid, cells) {
7958
+ if (grid.width <= 0 || grid.height <= 0) return [];
7959
+ const tiles = [];
7960
+ for (const c of cells) {
7961
+ if (c.hidden || c.width <= 0 || c.height <= 0) continue;
7962
+ tiles.push({ x: c.left - grid.left, y: c.top - grid.top, width: c.width, height: c.height });
7963
+ }
7964
+ return tiles;
7965
+ }
7966
+ function compositeLayoutScreenshot(doc, frame, tiles) {
7967
+ if (frame.width <= 0 || frame.height <= 0 || tiles.length === 0) return null;
7968
+ const dpr = frame.dpr > 0 ? frame.dpr : 1;
7969
+ const out = doc.createElement("canvas");
7970
+ out.width = Math.max(1, Math.round(frame.width * dpr));
7971
+ out.height = Math.max(1, Math.round(frame.height * dpr));
7972
+ const ctx = out.getContext("2d");
7973
+ if (!ctx) return null;
7974
+ ctx.fillStyle = frame.gapColor;
7975
+ ctx.fillRect(0, 0, out.width, out.height);
7976
+ let painted = 0;
7977
+ for (const tile of tiles) {
7978
+ if (tile.width <= 0 || tile.height <= 0) continue;
7979
+ const { dx, dy, dw, dh } = destRect(tile, dpr);
7980
+ if (dw <= 0 || dh <= 0) continue;
7981
+ ctx.drawImage(tile.source, dx, dy, dw, dh);
7982
+ painted += 1;
7983
+ }
7984
+ if (painted === 0) return null;
7985
+ return out.toDataURL("image/png");
7986
+ }
7987
+ function triggerPngDownload(doc, url, filename) {
7988
+ const a = doc.createElement("a");
7989
+ a.href = url;
7990
+ a.download = filename;
7991
+ a.click();
7992
+ }
7993
+
7822
7994
  // src/workspace/VelaWorkspace.ts
7823
7995
  var DEFAULT_TIMEFRAMES = ["1", "5", "15", "30", "60", "240", "D", "W", "M"];
7824
7996
  var GAP_PX = 2;
@@ -8049,7 +8221,7 @@ var VelaWorkspace = class {
8049
8221
  ...picker ? { onIndicatorsClick: () => picker.open() } : {},
8050
8222
  onUndoClick: () => this.active.history.undo(),
8051
8223
  onRedoClick: () => this.active.history.redo(),
8052
- onScreenshotClick: () => this.active.downloadScreenshot(),
8224
+ onScreenshotClick: () => this.downloadScreenshot(),
8053
8225
  onAlertsClick: (anchor) => this.openAlertsMenu(anchor),
8054
8226
  timeframe: "60",
8055
8227
  timeframes: opts.timeframes ?? DEFAULT_TIMEFRAMES,
@@ -8108,6 +8280,7 @@ var VelaWorkspace = class {
8108
8280
  this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
8109
8281
  this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
8110
8282
  this.dock.refresh();
8283
+ if (boot?.panels) this.dock.applyState(boot.panels);
8111
8284
  this.root.appendChild(main);
8112
8285
  this.toastHost = new Toast(this.gridEl);
8113
8286
  const attribution = rendererDefaults().attribution;
@@ -8119,6 +8292,7 @@ var VelaWorkspace = class {
8119
8292
  bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H3}px) + 10px)`,
8120
8293
  zIndex: "11"
8121
8294
  });
8295
+ mark.dataset.velaScreenshot = "1";
8122
8296
  this.gridEl.appendChild(mark);
8123
8297
  this.attributionMark = mark;
8124
8298
  }
@@ -8255,6 +8429,56 @@ var VelaWorkspace = class {
8255
8429
  get chart() {
8256
8430
  return this.active.chart;
8257
8431
  }
8432
+ /**
8433
+ * PNG data URL of the visible layout: every live cell in its grid slot, or the
8434
+ * maximized cell alone. Same pixels the screenshot button downloads. A single
8435
+ * visible cell returns that cell's own chart export (the one-chart case).
8436
+ */
8437
+ screenshot() {
8438
+ const cells = this.shotCells();
8439
+ if (cells.length === 0) return null;
8440
+ if (cells.length === 1) return cells[0].chart.renderer.screenshot();
8441
+ return this.compositeLayoutShot(cells);
8442
+ }
8443
+ /** Download {@link screenshot} as a PNG. A multi-cell layout is named
8444
+ * `vela-layout.png`; one visible cell keeps `${symbol}-${timeframe}.png`. */
8445
+ downloadScreenshot() {
8446
+ const cells = this.shotCells();
8447
+ if (cells.length <= 1) {
8448
+ (cells[0] ?? this.active).downloadScreenshot();
8449
+ return;
8450
+ }
8451
+ const url = this.compositeLayoutShot(cells);
8452
+ if (!url) return;
8453
+ triggerPngDownload(this.root.ownerDocument, url, "vela-layout.png");
8454
+ }
8455
+ /** Live cells the screenshot should include — maximized siblings are `hidden`. */
8456
+ shotCells() {
8457
+ return this.cells().filter((c) => c.host.style.visibility !== "hidden");
8458
+ }
8459
+ /** Place every cell's raster onto one canvas the size of the grid. */
8460
+ compositeLayoutShot(cells) {
8461
+ const gridEl = this.gridEl;
8462
+ const grid = gridEl.getBoundingClientRect();
8463
+ const win = gridEl.ownerDocument.defaultView;
8464
+ const dpr = win?.devicePixelRatio ?? 1;
8465
+ const gapColor = win ? win.getComputedStyle(gridEl).backgroundColor : "";
8466
+ const tiles = [];
8467
+ for (const cell of cells) {
8468
+ const source = cell.screenshotCanvas();
8469
+ if (!source) continue;
8470
+ const r = cell.host.getBoundingClientRect();
8471
+ const place = tilesFromCellRects(grid, [{ left: r.left, top: r.top, width: r.width, height: r.height }])[0];
8472
+ if (!place) continue;
8473
+ tiles.push({ source, ...place });
8474
+ }
8475
+ return compositeLayoutScreenshot(gridEl.ownerDocument, {
8476
+ width: grid.width,
8477
+ height: grid.height,
8478
+ dpr,
8479
+ gapColor: gapColor || "#000000"
8480
+ }, tiles);
8481
+ }
8258
8482
  setActiveCell(id) {
8259
8483
  if (id === this.activeId || this.destroyed) return;
8260
8484
  const prev = this.activeId;
@@ -9301,7 +9525,7 @@ var VelaWorkspace = class {
9301
9525
  this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
9302
9526
  host: this.root,
9303
9527
  ...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() } : {},
9528
+ ...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.downloadScreenshot() } : {},
9305
9529
  canUndo: () => this.active.history.canUndo,
9306
9530
  canRedo: () => this.active.history.canRedo,
9307
9531
  priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
@@ -9402,9 +9626,9 @@ var VelaWorkspace = class {
9402
9626
  this.keymap.register({
9403
9627
  id: "chart.screenshot",
9404
9628
  keys: "mod+alt+s",
9405
- label: ov ? ov.label : "Download a chart screenshot",
9629
+ label: ov ? ov.label : "Download a screenshot of the layout",
9406
9630
  category: "Chart",
9407
- run: ov ? () => this.runOverride(ov) : () => this.active.downloadScreenshot()
9631
+ run: ov ? () => this.runOverride(ov) : () => this.downloadScreenshot()
9408
9632
  });
9409
9633
  }
9410
9634
  this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });