@luxalgo/vela 0.6.8 → 0.6.10

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 (45) hide show
  1. package/dist/{DataProvider-BSLlBpB9.d.ts → DataProvider-CWmp31dA.d.ts} +49 -1
  2. package/dist/{DataProvider-BsQM2WNH.d.cts → DataProvider-l_eLMly_.d.cts} +49 -1
  3. package/dist/{chunk-L3I2CCYO.js → chunk-4KZVZ7QQ.js} +49 -2
  4. package/dist/{chunk-SQETIBFU.js → chunk-FFOP37FQ.js} +996 -377
  5. package/dist/{chunk-2R7BANEM.js → chunk-G77Y7LK2.js} +2 -2
  6. package/dist/{chunk-PN5KWFZ4.js → chunk-KG4YT3TI.js} +5 -3
  7. package/dist/{chunk-OICPLNU7.js → chunk-MHY7MVXH.js} +192 -2
  8. package/dist/{chunk-N7LGMKCE.js → chunk-NMTQXNT4.js} +637 -164
  9. package/dist/{contributions-DLLdV9jD.d.ts → contributions-BCz6Dr6a.d.ts} +99 -7
  10. package/dist/{contributions-Vw-Hm58R.d.cts → contributions-D9vTzm5p.d.cts} +99 -7
  11. package/dist/index.cjs +1210 -383
  12. package/dist/index.d.cts +37 -11
  13. package/dist/index.d.ts +37 -11
  14. package/dist/index.js +4 -4
  15. package/dist/{options-BaVTMXaO.d.ts → options-DSqHsQyN.d.cts} +84 -7
  16. package/dist/{options-BaVTMXaO.d.cts → options-DSqHsQyN.d.ts} +84 -7
  17. package/dist/{plugin-BnJgjLAy.d.cts → plugin-Bt8hLR8Z.d.cts} +3 -3
  18. package/dist/{plugin-CC7rBrOY.d.ts → plugin-CH7pfMrE.d.ts} +3 -3
  19. package/dist/plugin.cjs +21 -3
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +2 -2
  23. package/dist/providers/binance.d.cts +2 -2
  24. package/dist/providers/binance.d.ts +2 -2
  25. package/dist/providers/coinbase.d.cts +2 -2
  26. package/dist/providers/coinbase.d.ts +2 -2
  27. package/dist/providers/hyperliquid.d.cts +2 -2
  28. package/dist/providers/hyperliquid.d.ts +2 -2
  29. package/dist/{statusline-CGkB2EOo.d.ts → statusline-5K7y4Ya2.d.ts} +5 -3
  30. package/dist/{statusline-DqzBqy42.d.cts → statusline-TYLQmoeh.d.cts} +5 -3
  31. package/dist/ui.cjs +208 -5
  32. package/dist/ui.d.cts +91 -2
  33. package/dist/ui.d.ts +91 -2
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +1210 -383
  36. package/dist/vela.global.min.js +99 -48
  37. package/dist/widget.cjs +1771 -449
  38. package/dist/widget.d.cts +17 -6
  39. package/dist/widget.d.ts +17 -6
  40. package/dist/widget.js +7 -7
  41. package/dist/workspace.cjs +1771 -449
  42. package/dist/workspace.d.cts +100 -6
  43. package/dist/workspace.d.ts +100 -6
  44. package/dist/workspace.js +6 -6
  45. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
- import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel, Vela, TypedEventBus, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-SQETIBFU.js';
2
- import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, widgetAttachments, getDrawingType } from './chunk-L3I2CCYO.js';
3
- import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-2R7BANEM.js';
4
- import { Menu, Dialog, applyPlotOverlayTokens, ensureUIHost } from './chunk-OICPLNU7.js';
5
- import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, SESSION_PRE, SESSION_POST, SESSION_OFF, icon, categoricalColor } from './chunk-PN5KWFZ4.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-FFOP37FQ.js';
2
+ import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, legendCalloutsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, widgetAttachments, getDrawingType } from './chunk-4KZVZ7QQ.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, SESSION_OFF, SESSION_POST, SESSION_PRE, icon, categoricalColor } from './chunk-KG4YT3TI.js';
6
6
  import { baseOf } from './chunk-W4EJWLEO.js';
7
7
 
8
8
  // src/widget/timeframe.ts
@@ -2931,6 +2931,17 @@ function dedupeSymbols(list) {
2931
2931
  }
2932
2932
  return out;
2933
2933
  }
2934
+ function foldGroups(list) {
2935
+ const groupsAbove = /* @__PURE__ */ new Set();
2936
+ const out = [];
2937
+ for (const s of list) {
2938
+ if (isGroupRow(s)) {
2939
+ groupsAbove.add(groupKeyOf(s));
2940
+ out.push(s);
2941
+ } else if (s.group == null || !groupsAbove.has(groupKeyOf(s))) out.push(s);
2942
+ }
2943
+ return out;
2944
+ }
2934
2945
  var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
2935
2946
  function parseQuery(raw, venues) {
2936
2947
  const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
@@ -3062,6 +3073,21 @@ var CSS6 = `
3062
3073
  .vela-sp-badge[data-p='binance'] { color: #f0b90b; } /* palette-exempt: venue brand mark */
3063
3074
  .vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
3064
3075
  .vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
3076
+ /* Grouped listings (futures roots): the chevron unfolds members inline, indented. */
3077
+ .vela-sp-expander {
3078
+ all: unset;
3079
+ flex: none;
3080
+ display: inline-flex;
3081
+ align-items: center;
3082
+ justify-content: center;
3083
+ width: 22px;
3084
+ height: 22px;
3085
+ border-radius: 5px;
3086
+ cursor: pointer;
3087
+ color: var(--vela-fg-muted);
3088
+ }
3089
+ .vela-sp-expander:hover { background: var(--vela-surface-elev); color: var(--vela-fg); }
3090
+ .vela-sp-row[data-member] { padding-left: 34px; }
3065
3091
  `;
3066
3092
  var PAGE = 100;
3067
3093
  var SymbolPicker = class {
@@ -3073,6 +3099,11 @@ var SymbolPicker = class {
3073
3099
  this.seed = "";
3074
3100
  this.activeTab = "All";
3075
3101
  this.visible = PAGE;
3102
+ /** The last filter pass returned fewer raw rows than asked — the pool is drained
3103
+ * (checked BEFORE folding: folding shortens pages without meaning exhaustion). */
3104
+ this.exhausted = false;
3105
+ /** Group rows currently expanded (venue-scoped keys) — members shown inline. */
3106
+ this.expanded = /* @__PURE__ */ new Set();
3076
3107
  /** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
3077
3108
  this.ranked = null;
3078
3109
  this.ranking = false;
@@ -3087,7 +3118,7 @@ var SymbolPicker = class {
3087
3118
  searchRow.append(iconEl("search", doc), this.input);
3088
3119
  this.tabs = doc.createElement("div");
3089
3120
  this.tabs.className = "vela-sp-tabs";
3090
- for (const t of ["All", "Stocks", "ETFs", "Crypto", "Forex", "Commodities"]) {
3121
+ for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
3091
3122
  const b = doc.createElement("button");
3092
3123
  b.className = "vela-sp-tab";
3093
3124
  b.textContent = t;
@@ -3103,7 +3134,7 @@ var SymbolPicker = class {
3103
3134
  this.list = doc.createElement("div");
3104
3135
  this.list.className = "vela-sp-list";
3105
3136
  this.list.addEventListener("scroll", () => {
3106
- if (this.rows.length < this.visible) return;
3137
+ if (this.exhausted) return;
3107
3138
  if (this.list.scrollTop + this.list.clientHeight < this.list.scrollHeight - 200) return;
3108
3139
  this.visible += PAGE;
3109
3140
  this.grow();
@@ -3132,14 +3163,19 @@ var SymbolPicker = class {
3132
3163
  else if (e.key === "ArrowUp") this.moveHighlight(-1);
3133
3164
  else if (e.key === "Enter") {
3134
3165
  const pick = this.rows[this.highlighted];
3135
- if (pick) this.select(pick.ticker, pick.prefix ?? pick.provider, opts.onSelect);
3166
+ if (pick) this.pick(pick);
3136
3167
  return;
3137
3168
  } else return;
3138
3169
  e.preventDefault();
3139
3170
  });
3140
3171
  this.list.addEventListener("click", (e) => {
3141
- const row = e.target.closest(".vela-sp-row");
3142
- if (row?.dataset.ticker) this.select(row.dataset.ticker, row.dataset.venue, opts.onSelect);
3172
+ const target = e.target;
3173
+ const row = target.closest(".vela-sp-row");
3174
+ if (!row) return;
3175
+ const s = this.rows[Number(row.dataset.i)];
3176
+ if (!s) return;
3177
+ if (target.closest(".vela-sp-expander")) this.toggleExpand(s);
3178
+ else this.pick(s);
3143
3179
  });
3144
3180
  }
3145
3181
  /** Wire where symbols come from (re-called on every widget rebuild). */
@@ -3156,6 +3192,27 @@ var SymbolPicker = class {
3156
3192
  destroy() {
3157
3193
  this.dialog.destroy();
3158
3194
  }
3195
+ /** Route a row activation: a GROUP row loads its default member (the root itself is
3196
+ * listed, never loadable), any other row loads itself. */
3197
+ pick(s) {
3198
+ const target = isGroupRow(s) ? defaultMemberOf(this.pool(), s) ?? s : s;
3199
+ this.select(target.ticker, target.prefix ?? target.provider, this.opts.onSelect);
3200
+ }
3201
+ /** Expand/collapse a group row IN PLACE — same query, same page, same scroll; only
3202
+ * the member rows under the group appear or go. */
3203
+ toggleExpand(s) {
3204
+ const key = groupKeyOf(s);
3205
+ if (!this.expanded.delete(key)) this.expanded.add(key);
3206
+ const scrollTop = this.list.scrollTop;
3207
+ const focus = this.rows[this.highlighted];
3208
+ this.rows = this.computeRows();
3209
+ this.list.replaceChildren();
3210
+ this.rows.forEach((r, i) => this.list.appendChild(this.rowEl(r, i)));
3211
+ const at = focus ? this.rows.indexOf(focus) : -1;
3212
+ this.highlighted = at >= 0 ? at : Math.min(this.highlighted, Math.max(0, this.rows.length - 1));
3213
+ this.renderHighlight();
3214
+ this.list.scrollTop = scrollTop;
3215
+ }
3159
3216
  select(ticker, venue, onSelect) {
3160
3217
  this.close();
3161
3218
  onSelect(venue ? `${venue}:${ticker}` : ticker);
@@ -3197,10 +3254,26 @@ var SymbolPicker = class {
3197
3254
  return this.ranked?.result ?? raw;
3198
3255
  }
3199
3256
  computeRows() {
3200
- const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
3257
+ const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Futures: ["futures", "root"], Forex: ["forex"], Commodities: ["commodity"] };
3201
3258
  const all = this.pool();
3202
3259
  const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
3203
- return filterSymbols(pool, this.input.value, this.visible, symbolRanking() ? false : TOP_TICKERS);
3260
+ const filtered = filterSymbols(pool, this.input.value, this.visible, symbolRanking() ? false : TOP_TICKERS);
3261
+ this.exhausted = filtered.length < this.visible;
3262
+ const folded = foldGroups(filtered);
3263
+ if (!this.expanded.size) return folded;
3264
+ const keyOf = (s) => `${(s.prefix ?? s.provider ?? "").toLowerCase()}:${s.ticker.toUpperCase()}`;
3265
+ const present = new Set(folded.map(keyOf));
3266
+ const out = [];
3267
+ for (const s of folded) {
3268
+ out.push(s);
3269
+ if (!isGroupRow(s) || !this.expanded.has(groupKeyOf(s))) continue;
3270
+ for (const m of groupMembers(all, s)) {
3271
+ if (present.has(keyOf(m))) continue;
3272
+ present.add(keyOf(m));
3273
+ out.push(m);
3274
+ }
3275
+ }
3276
+ return out;
3204
3277
  }
3205
3278
  refresh() {
3206
3279
  const doc = this.list.ownerDocument;
@@ -3215,19 +3288,20 @@ var SymbolPicker = class {
3215
3288
  this.list.appendChild(empty);
3216
3289
  return;
3217
3290
  }
3218
- for (const s of this.rows) this.list.appendChild(this.rowEl(s));
3291
+ this.rows.forEach((s, i) => this.list.appendChild(this.rowEl(s, i)));
3219
3292
  this.renderHighlight();
3220
3293
  }
3221
3294
  /** Append the page the grown `visible` just uncovered — rows already on screen stay put. */
3222
3295
  grow() {
3223
3296
  const already = this.rows.length;
3224
3297
  this.rows = this.computeRows();
3225
- for (const s of this.rows.slice(already)) this.list.appendChild(this.rowEl(s));
3298
+ this.rows.slice(already).forEach((s, j) => this.list.appendChild(this.rowEl(s, already + j)));
3226
3299
  }
3227
- rowEl(s) {
3300
+ rowEl(s, i) {
3228
3301
  const doc = this.list.ownerDocument;
3229
3302
  const row = doc.createElement("div");
3230
3303
  row.className = "vela-sp-row";
3304
+ row.dataset.i = String(i);
3231
3305
  row.dataset.ticker = s.ticker;
3232
3306
  const venue = s.prefix ?? s.provider;
3233
3307
  if (venue) row.dataset.venue = venue;
@@ -3242,6 +3316,14 @@ var SymbolPicker = class {
3242
3316
  d.textContent = s.description ?? (s.type ?? "");
3243
3317
  main.append(t, d);
3244
3318
  row.append(av, main);
3319
+ if (isGroupRow(s)) {
3320
+ row.dataset.group = "1";
3321
+ const expander = doc.createElement("button");
3322
+ expander.className = "vela-sp-expander";
3323
+ expander.setAttribute("aria-label", "Show contracts");
3324
+ expander.appendChild(iconEl(this.expanded.has(groupKeyOf(s)) ? "chevron-down" : "chevron-right", doc));
3325
+ row.appendChild(expander);
3326
+ } else if (s.group != null && this.expanded.has(groupKeyOf(s))) row.dataset.member = "1";
3245
3327
  if (venue) {
3246
3328
  const badge = doc.createElement("span");
3247
3329
  badge.className = "vela-sp-badge";
@@ -3940,45 +4022,24 @@ var CSS10 = `
3940
4022
  }
3941
4023
  .vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
3942
4024
  .vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
3943
- /* Market status badge \u2014 icon-only 16px circle, label on hover (kit tooltip). */
3944
- .vela-statusline .vela-sl-market {
3945
- display: inline-grid;
3946
- place-items: center;
3947
- width: 16px;
3948
- height: 16px;
3949
- border-radius: 50%;
3950
- flex: none;
3951
- align-self: center;
3952
- line-height: 0;
3953
- cursor: default;
3954
- }
3955
- .vela-statusline .vela-sl-market svg {
3956
- width: 12px;
3957
- height: 12px;
3958
- display: block;
3959
- }
3960
- /* Open wears the theme's up color; the other sessions are meaning constants from the
3961
- * palette (amber pre, sky post, gray closed/holiday). Same ink at 20% for the circle. */
3962
- .vela-statusline .vela-sl-market[data-status='open'] {
3963
- background: color-mix(in srgb, var(--vela-up) 20%, transparent);
3964
- color: var(--vela-up);
3965
- }
3966
- .vela-statusline .vela-sl-market[data-status='pre'] { background: color-mix(in srgb, ${SESSION_PRE} 20%, transparent); color: ${SESSION_PRE}; }
3967
- .vela-statusline .vela-sl-market[data-status='post'] { background: color-mix(in srgb, ${SESSION_POST} 20%, transparent); color: ${SESSION_POST}; }
3968
- .vela-statusline .vela-sl-market[data-status='closed'],
3969
- .vela-statusline .vela-sl-market[data-status='holiday'] { background: color-mix(in srgb, ${SESSION_OFF} 20%, transparent); color: ${SESSION_OFF}; }
4025
+ /* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
4026
+ * via the kit tooltip); the session tint is applied per status in setMarketStatus. */
4027
+ .vela-statusline .vela-sl-market { align-self: center; }
3970
4028
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
3971
4029
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
3972
4030
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
3973
4031
  * these are the pre-ink fallbacks only. */
3974
4032
  .vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
3975
4033
  .vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
3976
- /* Stack the renderer's PRICE-pane legend below the status line (study panes stay put).
3977
- * The renderer sets the legend's inline top \u2014 shift with a transform, don't fight it.
3978
- * Scoped to hosts that actually CARRY a status line (the marker class set by the
3979
- * Statusline constructor) \u2014 the stylesheet is document-global, so a bare container
3980
- * class here would shift every chart on the page, including statusline-less ones. */
3981
- .vela-has-statusline [data-vela-pane='price'] { transform: translateY(26px); }
4034
+ /* Stack the TOP pane's legend below the status line (lower study panes stay put).
4035
+ * The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
4036
+ * normally, or a maximized study pane filling the plot \u2014 so the legend never merges
4037
+ * with the status line whichever pane owns the top. The renderer sets the legend's
4038
+ * inline top \u2014 shift with a transform, don't fight it. Scoped to hosts that actually
4039
+ * CARRY a status line (the marker class set by the Statusline constructor) \u2014 the
4040
+ * stylesheet is document-global, so a bare attribute selector here would shift every
4041
+ * chart on the page, including statusline-less ones. */
4042
+ .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
3982
4043
  /* Mobile: two-line chip \u2014 logo / symbol / meta / market status on one aligned row, the
3983
4044
  * bar change on the next. Full O/H/L/C stays hidden (too dense on a phone-width plot).
3984
4045
  * GRID, not a wrapping flexbox: an absolutely positioned wrapping flex container sizes
@@ -4014,7 +4075,7 @@ var CSS10 = `
4014
4075
  font-size: var(--vela-font-size-sm);
4015
4076
  line-height: 1.2;
4016
4077
  }
4017
- [data-layout='mobile'] .vela-has-statusline [data-vela-pane='price'] { transform: translateY(40px); }
4078
+ [data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
4018
4079
  /* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
4019
4080
  * don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
4020
4081
  * overflow:hidden only guards the transient between a resize and the next measure.
@@ -4031,7 +4092,7 @@ var CSS10 = `
4031
4092
  padding-left: 0;
4032
4093
  }
4033
4094
  /* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
4034
- [data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [data-vela-pane='price'] { transform: translateY(26px); }
4095
+ [data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
4035
4096
  `;
4036
4097
  function baseOfTicker(ticker) {
4037
4098
  return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
@@ -4043,6 +4104,13 @@ var MARKET_LABELS = {
4043
4104
  closed: "Market Closed",
4044
4105
  holiday: "Market Holiday"
4045
4106
  };
4107
+ var MARKET_INKS = {
4108
+ open: "var(--vela-up)",
4109
+ pre: SESSION_PRE,
4110
+ post: SESSION_POST,
4111
+ closed: SESSION_OFF,
4112
+ holiday: SESSION_OFF
4113
+ };
4046
4114
  function statuslineInkOf(renderer, priceStyle) {
4047
4115
  const cfg = renderer.getConfig();
4048
4116
  const c = (v) => typeof v === "string" ? v : null;
@@ -4101,8 +4169,15 @@ var Statusline = class {
4101
4169
  this.symbolEl.textContent = ticker;
4102
4170
  this.metaEl = doc.createElement("span");
4103
4171
  this.metaEl.className = "vela-sl-meta";
4104
- this.marketEl = doc.createElement("span");
4105
- this.marketEl.className = "vela-sl-market";
4172
+ this.marketBubble = new CalloutBubble({
4173
+ icon: "market-open",
4174
+ background: `color-mix(in srgb, ${MARKET_INKS.open} 20%, transparent)`,
4175
+ color: MARKET_INKS.open,
4176
+ label: MARKET_LABELS.open,
4177
+ host
4178
+ });
4179
+ this.marketEl = this.marketBubble.el;
4180
+ this.marketEl.classList.add("vela-sl-market");
4106
4181
  this.ohlcEl = doc.createElement("span");
4107
4182
  this.ohlcEl.className = "vela-sl-ohlc";
4108
4183
  this.changeEl = doc.createElement("span");
@@ -4190,8 +4265,13 @@ var Statusline = class {
4190
4265
  * hover label. Callers with no session model leave the constructor's 'open'. */
4191
4266
  setMarketStatus(status) {
4192
4267
  this.marketEl.dataset.status = status;
4193
- this.marketEl.innerHTML = icon(`market-${status}`);
4194
- this.marketEl.setAttribute("aria-label", MARKET_LABELS[status]);
4268
+ const ink = MARKET_INKS[status];
4269
+ this.marketBubble.set({
4270
+ icon: `market-${status}`,
4271
+ background: `color-mix(in srgb, ${ink} 20%, transparent)`,
4272
+ color: ink,
4273
+ label: MARKET_LABELS[status]
4274
+ });
4195
4275
  this.marketTip.setContent(MARKET_LABELS[status]);
4196
4276
  }
4197
4277
  setPartVisible(part, visible) {
@@ -4224,6 +4304,7 @@ var Statusline = class {
4224
4304
  this.fitRO?.disconnect();
4225
4305
  this.fitRO = null;
4226
4306
  this.marketTip.destroy();
4307
+ this.marketBubble.destroy();
4227
4308
  this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
4228
4309
  this.el.remove();
4229
4310
  }
@@ -4760,6 +4841,8 @@ var SessionShadingTracker = class {
4760
4841
  this.spec = null;
4761
4842
  this.ready = false;
4762
4843
  this.session = "regular";
4844
+ /** Whether one bar is shorter than a civil day — only then do pre/post bars exist. */
4845
+ this.intraday = false;
4763
4846
  this.covered = null;
4764
4847
  /** The newest range seen — viewport moves during metadata resolution (a load's fit
4765
4848
  * animation) must not be lost, so the resolution always expands the LATEST range. */
@@ -4772,6 +4855,8 @@ var SessionShadingTracker = class {
4772
4855
  this.spec = null;
4773
4856
  this.covered = null;
4774
4857
  this.session = opts.session;
4858
+ const tfMs = timeframeMs(opts.timeframe);
4859
+ this.intraday = Number.isFinite(tfMs) && tfMs < DAY_MS;
4775
4860
  this.lastRange = opts.range;
4776
4861
  void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
4777
4862
  if (my !== this.epoch) return;
@@ -4799,7 +4884,7 @@ var SessionShadingTracker = class {
4799
4884
  if (force) this.onZones(null);
4800
4885
  return;
4801
4886
  }
4802
- if (this.session !== "extended") {
4887
+ if (this.session !== "extended" || !this.intraday) {
4803
4888
  if (force) this.onZones({ pre: [], post: [] });
4804
4889
  return;
4805
4890
  }
@@ -4814,6 +4899,229 @@ var SessionShadingTracker = class {
4814
4899
  }
4815
4900
  };
4816
4901
 
4902
+ // src/widget/glide.ts
4903
+ var MIN_SPAN_MS = 6e4;
4904
+ var FOLLOW = 0.25;
4905
+ var ZOOM_IN = 0.8;
4906
+ var ZOOM_OUT = 1.25;
4907
+ var PAN_FAST = 0.2;
4908
+ function zoomTarget(base, factor) {
4909
+ const newSpan = Math.max(MIN_SPAN_MS, (base.to - base.from) * factor);
4910
+ return { from: base.to - newSpan, to: base.to };
4911
+ }
4912
+ function followStep(cur, target, follow = FOLLOW, epsFrac = 15e-4) {
4913
+ const eps = Math.max(MIN_SPAN_MS, (target.to - target.from) * epsFrac);
4914
+ const nf = cur.from + (target.from - cur.from) * follow;
4915
+ const nt = cur.to + (target.to - cur.to) * follow;
4916
+ const done = Math.abs(target.from - nf) <= eps && Math.abs(target.to - nt) <= eps;
4917
+ return { cur: done ? { ...target } : { from: nf, to: nt }, done };
4918
+ }
4919
+ var Glider = class {
4920
+ constructor(chart) {
4921
+ this.chart = chart;
4922
+ // The glide eases ITS OWN range, never the chart's read-back: getVisibleRange()
4923
+ // is data-bounded (whole bars, `to` capped at the newest bar) and the renderer
4924
+ // clamps what gets applied — chasing that read-back can never converge on a
4925
+ // clamped step, leaving the rAF loop re-applying a stale target forever (and
4926
+ // grinding the span down when panning past the newest bar). The internal range
4927
+ // converges geometrically, so the glide always terminates; the renderer is free
4928
+ // to clamp each applied frame.
4929
+ this.cur = null;
4930
+ this.target = null;
4931
+ this.raf = 0;
4932
+ }
4933
+ zoom(factor) {
4934
+ this.to((base) => zoomTarget(base, factor));
4935
+ }
4936
+ stop() {
4937
+ if (this.raf) cancelAnimationFrame(this.raf);
4938
+ this.raf = 0;
4939
+ this.cur = null;
4940
+ this.target = null;
4941
+ }
4942
+ to(make) {
4943
+ const chart = this.chart();
4944
+ const base = this.target ?? chart?.getVisibleRange();
4945
+ if (!chart || !base) return;
4946
+ this.cur ?? (this.cur = { ...base });
4947
+ this.target = make(base);
4948
+ if (!this.raf) this.tick();
4949
+ }
4950
+ tick() {
4951
+ this.raf = requestAnimationFrame(() => {
4952
+ const chart = this.chart();
4953
+ if (!chart || !this.cur || !this.target) {
4954
+ this.stop();
4955
+ return;
4956
+ }
4957
+ const { cur: next, done } = followStep(this.cur, this.target);
4958
+ this.cur = next;
4959
+ chart.setVisibleRange(next);
4960
+ if (done) this.stop();
4961
+ else this.tick();
4962
+ });
4963
+ }
4964
+ };
4965
+
4966
+ // src/widget/cell-controls.ts
4967
+ var CELL_CONTROLS_PROXIMITY_PX = 120;
4968
+ var TIME_AXIS_H2 = 22;
4969
+ var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
4970
+ var CLUSTER_H = 24;
4971
+ var CLUSTER_PILL = "rgba(0,0,0,0.65)";
4972
+ var STYLE_ID12 = "vela-cell-controls";
4973
+ var CSS12 = `
4974
+ .vela-cc-btn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;border-radius:var(--vela-radius-sm);background:transparent;line-height:0;font-size:12px;color:var(--vela-fg-muted);cursor:pointer;}
4975
+ .vela-cc-btn svg{display:block;}
4976
+ .vela-cc-btn:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
4977
+ .vela-cc-on,.vela-cc-on:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
4978
+ .vela-cc-grip{cursor:grab;touch-action:none;}
4979
+ .vela-cc-grip:active{cursor:grabbing;}
4980
+ `;
4981
+ function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX) {
4982
+ const cx = width / 2;
4983
+ const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H / 2;
4984
+ return Math.hypot(x - cx, y - cy) <= proximityPx;
4985
+ }
4986
+ var CellControls = class {
4987
+ constructor(host, deps) {
4988
+ this.host = host;
4989
+ this.deps = deps;
4990
+ this.near = false;
4991
+ /** A grip drag is underway — the proximity reveal must not hide the cluster
4992
+ * while captured pointer moves sweep across the whole grid. */
4993
+ this.dragging = false;
4994
+ /** Mobile: the proximity reveal is meaningless without a cursor — the mobile
4995
+ * bar's maximize stop replaces the cluster. */
4996
+ this.suspended = false;
4997
+ this.onHostMove = (e) => {
4998
+ if (this.suspended) return;
4999
+ if (this.dragging) return;
5000
+ const rect = this.host.getBoundingClientRect();
5001
+ this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height));
5002
+ };
5003
+ this.onHostLeave = () => {
5004
+ if (this.dragging) return;
5005
+ this.setNear(false);
5006
+ };
5007
+ injectStyles(STYLE_ID12, CSS12, host.ownerDocument);
5008
+ this.glider = new Glider(deps.chart);
5009
+ this.root = host.ownerDocument.createElement("div");
5010
+ Object.assign(this.root.style, {
5011
+ position: "absolute",
5012
+ left: "50%",
5013
+ bottom: `${CONTROLS_BOTTOM_PX}px`,
5014
+ transform: "translateX(-50%)",
5015
+ zIndex: "6",
5016
+ display: "none",
5017
+ // revealed by cursor proximity (onHostMove)
5018
+ gap: "2px",
5019
+ padding: "2px",
5020
+ borderRadius: "var(--vela-radius-md)",
5021
+ background: CLUSTER_PILL,
5022
+ pointerEvents: "auto"
5023
+ });
5024
+ this.host.addEventListener("pointermove", this.onHostMove);
5025
+ this.host.addEventListener("pointerleave", this.onHostLeave);
5026
+ this.host.appendChild(this.root);
5027
+ this.refresh();
5028
+ }
5029
+ /** Rebuild the buttons (the multi-cell gate or the maximized state changed). */
5030
+ refresh() {
5031
+ this.root.textContent = "";
5032
+ const multi = this.deps.multiCell();
5033
+ const maximized = multi && this.deps.isMaximized();
5034
+ if (multi && !maximized) this.root.appendChild(this.makeGrip());
5035
+ this.root.appendChild(this.button("minus", "Zoom out", () => this.glider.zoom(ZOOM_OUT)));
5036
+ this.root.appendChild(this.button("plus", "Zoom in", () => this.glider.zoom(ZOOM_IN)));
5037
+ if (multi) {
5038
+ this.root.appendChild(
5039
+ this.button(maximized ? "restore" : "maximize", maximized ? "Restore layout" : "Maximize chart", () => this.deps.toggleMaximize(), {
5040
+ // The maximized state reads as an inverse chip (white-on-dark, dark-on-light),
5041
+ // the same active-state affordance as a collapsed pane's expand button.
5042
+ selected: maximized
5043
+ })
5044
+ );
5045
+ }
5046
+ this.root.appendChild(
5047
+ this.button("reset", "Reset chart", () => {
5048
+ this.glider.stop();
5049
+ this.deps.reset();
5050
+ })
5051
+ );
5052
+ }
5053
+ button(iconId, title, onClick, opts = {}) {
5054
+ const b = this.host.ownerDocument.createElement("button");
5055
+ b.type = "button";
5056
+ b.title = title;
5057
+ b.setAttribute("aria-label", title);
5058
+ b.className = opts.selected === true ? "vela-cc-btn vela-cc-on" : "vela-cc-btn";
5059
+ b.innerHTML = icon(iconId);
5060
+ b.addEventListener("click", (e) => {
5061
+ e.stopPropagation();
5062
+ onClick();
5063
+ });
5064
+ return b;
5065
+ }
5066
+ /** The drag handle (2×3 dot grip): press and drag onto another cell to trade
5067
+ * slots with it. The preview highlight follows the pointer; releasing outside
5068
+ * any other cell cancels. */
5069
+ makeGrip() {
5070
+ const b = this.host.ownerDocument.createElement("button");
5071
+ b.type = "button";
5072
+ b.title = "Drag to move chart";
5073
+ b.setAttribute("aria-label", "Drag to move chart");
5074
+ b.className = "vela-cc-btn vela-cc-grip";
5075
+ b.innerHTML = icon("grip");
5076
+ b.addEventListener("pointerdown", (e) => this.onGripDown(b, e));
5077
+ return b;
5078
+ }
5079
+ onGripDown(btn, e) {
5080
+ if (e.button !== 0 && e.pointerType === "mouse") return;
5081
+ e.preventDefault();
5082
+ e.stopPropagation();
5083
+ try {
5084
+ btn.setPointerCapture(e.pointerId);
5085
+ } catch {
5086
+ }
5087
+ this.dragging = true;
5088
+ let target = null;
5089
+ const move = (ev) => {
5090
+ target = this.deps.dragTargetAt(ev.clientX, ev.clientY);
5091
+ this.deps.previewDrop(target);
5092
+ };
5093
+ const finish = (commit) => () => {
5094
+ this.dragging = false;
5095
+ this.deps.previewDrop(null);
5096
+ btn.removeEventListener("pointermove", move);
5097
+ btn.removeEventListener("pointerup", onUp);
5098
+ btn.removeEventListener("pointercancel", onCancel);
5099
+ if (commit && target != null) this.deps.dropOn(target);
5100
+ };
5101
+ const onUp = finish(true);
5102
+ const onCancel = finish(false);
5103
+ btn.addEventListener("pointermove", move);
5104
+ btn.addEventListener("pointerup", onUp);
5105
+ btn.addEventListener("pointercancel", onCancel);
5106
+ }
5107
+ /** Mobile flips the cluster off entirely (and hides it if currently revealed). */
5108
+ setSuspended(on) {
5109
+ this.suspended = on;
5110
+ if (on) this.setNear(false);
5111
+ }
5112
+ setNear(near) {
5113
+ if (near === this.near) return;
5114
+ this.near = near;
5115
+ this.root.style.display = near ? "flex" : "none";
5116
+ }
5117
+ destroy() {
5118
+ this.glider.stop();
5119
+ this.host.removeEventListener("pointermove", this.onHostMove);
5120
+ this.host.removeEventListener("pointerleave", this.onHostLeave);
5121
+ this.root.remove();
5122
+ }
5123
+ };
5124
+
4817
5125
  // src/widget/history.ts
4818
5126
  var WidgetHistory = class {
4819
5127
  /** `getChart` late-resolves the CURRENT chart: drawing steps recorded before a chart
@@ -5002,6 +5310,7 @@ var ChartCell = class {
5002
5310
  this.inner.renderer.set("attribution", false);
5003
5311
  this.inner.renderer.set("dialogHost", deps.dialogHost);
5004
5312
  this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => deps.context()));
5313
+ this.inner.renderer.setLegendCallouts(legendCalloutsProviderFor(this.inner, () => deps.context()));
5005
5314
  if (this.inner.renderer.supports("historyChords")) this.inner.renderer.set("historyChords", false);
5006
5315
  this.history.onChart(this.inner);
5007
5316
  this.inner.renderer.onConfigChanged(() => {
@@ -5062,11 +5371,18 @@ var ChartCell = class {
5062
5371
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
5063
5372
  });
5064
5373
  this.syncStatuslineColors();
5374
+ this.cellControls = new CellControls(this.host, {
5375
+ chart: () => this.inner,
5376
+ reset: () => this.resetView(),
5377
+ multiCell: () => deps.multiCell(),
5378
+ isMaximized: () => deps.isMaximized(id),
5379
+ toggleMaximize: () => deps.toggleMaximize(id),
5380
+ dragTargetAt: (x, y) => deps.cellDragTarget(id, x, y),
5381
+ previewDrop: (target) => deps.previewDropTarget(target),
5382
+ dropOn: (target) => deps.dropCell(id, target)
5383
+ });
5065
5384
  this.contextMenu = new ChartContextMenu(this.host, {
5066
- resetView: () => {
5067
- this.inner?.renderer.set("autoScale", true);
5068
- this.inner?.setVisibleRangePreset("ALL");
5069
- },
5385
+ resetView: () => this.resetView(),
5070
5386
  timezone: () => this.deps.timezone(),
5071
5387
  setTimezone: (zone) => this.deps.setTimezone(zone),
5072
5388
  // Right-clicking activates the cell first (capture-phase pointerdown), so the
@@ -5118,20 +5434,30 @@ var ChartCell = class {
5118
5434
  this.refreshNativeCatalog();
5119
5435
  this.pushSettingsSections();
5120
5436
  this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
5121
- this.state.symbol = symbol2;
5122
- this.state.provider = parseSymbol(symbol2).provider ?? void 0;
5123
- this.state.timeframe = timeframe;
5124
- this.state.session = normalizeSession(this.inner?.market.session);
5125
- this.watermark?.update(symbol2, timeframe);
5126
- this.statusline?.setSymbol(symbol2);
5127
- this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol2) ?? this.state.provider ?? "");
5128
- if (this.inner) this.statusline?.onChart(this.inner);
5437
+ this.projectMarket(symbol2, timeframe);
5129
5438
  this.refreshNativeCatalog();
5130
5439
  this.refreshSessionAvailable();
5131
5440
  if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
5132
5441
  this.deps.onMarketChanged(this.id);
5133
5442
  });
5134
5443
  }
5444
+ /**
5445
+ * Project a market identity into the cell state and its display overlays (watermark,
5446
+ * statusline), WITHOUT the data-dependent bookkeeping. Runs twice per user pick: once
5447
+ * optimistically from the cell setters — the labels reflect the pick immediately, not
5448
+ * after the bars load — and again from `market:changed` (the committed pass, and the
5449
+ * only pass for host `chart.setMarket` calls). Idempotent, so the double run converges.
5450
+ */
5451
+ projectMarket(symbol, timeframe) {
5452
+ this.state.symbol = symbol;
5453
+ this.state.provider = parseSymbol(symbol).provider ?? void 0;
5454
+ this.state.timeframe = timeframe;
5455
+ this.state.session = normalizeSession(this.inner?.market.session);
5456
+ this.watermark?.update(symbol, timeframe);
5457
+ this.statusline?.setSymbol(symbol);
5458
+ this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol) ?? this.state.provider ?? "");
5459
+ if (this.inner) this.statusline?.onChart(this.inner);
5460
+ }
5135
5461
  /**
5136
5462
  * Does this cell's market HAVE sessions (RTH/ETH meaningful)? Derived from the
5137
5463
  * symbol's own metadata (`syminfo.session !== '24x7'`), asynchronously — the
@@ -5177,7 +5503,7 @@ var ChartCell = class {
5177
5503
  const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
5178
5504
  const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
5179
5505
  const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
5180
- this.sessionShading.track(chart.data, symbol, { session: this.session, range });
5506
+ this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
5181
5507
  }
5182
5508
  /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
5183
5509
  * live by the dialog swatch) — the cell only proxies them into its settings rows. */
@@ -5366,17 +5692,23 @@ var ChartCell = class {
5366
5692
  get indicatorCount() {
5367
5693
  return this.instances.length + this.nativeCatalog.filter((n) => n.present).length;
5368
5694
  }
5369
- /** Switch this cell's market in place (the chart instance survives). */
5695
+ /** Switch this cell's market in place (the chart instance survives). The projection
5696
+ * is OPTIMISTIC — labels and chrome show the pick before the bars load; it follows
5697
+ * the setMarket call so the statusline reads the already-blanked chart. */
5370
5698
  setSymbol(symbol) {
5371
5699
  if (!this.inner || symbol === this.symbol) return;
5372
5700
  this.unresolvedToasted = null;
5373
5701
  void this.inner.setMarket({ symbol });
5702
+ this.projectMarket(symbol, this.timeframe);
5703
+ this.deps.onMarketChanged(this.id);
5374
5704
  }
5375
5705
  setTimeframe(timeframe) {
5376
5706
  if (!this.inner || timeframe === this.timeframe) return;
5377
5707
  this.activeRangeId = null;
5378
5708
  this.rangeBars = 0;
5379
5709
  void this.inner.setMarket({ timeframe, bars: this.state.bars });
5710
+ this.projectMarket(this.symbol, timeframe);
5711
+ this.deps.onMarketChanged(this.id);
5380
5712
  }
5381
5713
  /** Applied live (renderer feature) — no reload. */
5382
5714
  setPriceStyle(style) {
@@ -5424,6 +5756,20 @@ var ChartCell = class {
5424
5756
  this.inner.setVisibleRangePreset(preset.preset);
5425
5757
  }
5426
5758
  }
5759
+ /** Reset this cell's view: re-enable auto scale and frame the full history —
5760
+ * the same action the chart context menu offers. */
5761
+ resetView() {
5762
+ this.inner?.renderer.set("autoScale", true);
5763
+ this.inner?.setVisibleRangePreset("ALL");
5764
+ }
5765
+ /** Rebuild the view-controls cluster (the maximize gate or state changed). */
5766
+ refreshControls() {
5767
+ this.cellControls.refresh();
5768
+ }
5769
+ /** Mobile flips the per-cell cluster off (the shell's mobile bar replaces it). */
5770
+ setControlsSuspended(on) {
5771
+ this.cellControls.setSuspended(on);
5772
+ }
5427
5773
  /** Make this cell the active one and put keyboard focus on its chart surface. */
5428
5774
  focus() {
5429
5775
  this.deps.activate(this.id);
@@ -5737,6 +6083,7 @@ var ChartCell = class {
5737
6083
  destroy() {
5738
6084
  this.destroyed = true;
5739
6085
  this.offMarket();
6086
+ this.cellControls.destroy();
5740
6087
  this.contextMenu.destroy();
5741
6088
  this.history.destroy();
5742
6089
  this.marketStatus?.stop();
@@ -5995,8 +6342,8 @@ var SplitterLayer = class {
5995
6342
  };
5996
6343
 
5997
6344
  // src/widget/toast.ts
5998
- var STYLE_ID12 = "vela-widget-toast";
5999
- var CSS12 = `
6345
+ var STYLE_ID13 = "vela-widget-toast";
6346
+ var CSS13 = `
6000
6347
  .vela-toast {
6001
6348
  position: absolute;
6002
6349
  left: 50%;
@@ -6026,7 +6373,7 @@ var CSS12 = `
6026
6373
  var Toast = class {
6027
6374
  constructor(host) {
6028
6375
  this.timer = null;
6029
- injectStyles(STYLE_ID12, CSS12, host.ownerDocument);
6376
+ injectStyles(STYLE_ID13, CSS13, host.ownerDocument);
6030
6377
  this.el = host.ownerDocument.createElement("div");
6031
6378
  this.el.className = "vela-toast";
6032
6379
  host.appendChild(this.el);
@@ -6050,70 +6397,6 @@ var Toast = class {
6050
6397
  }
6051
6398
  };
6052
6399
 
6053
- // src/widget/glide.ts
6054
- var MIN_SPAN_MS = 6e4;
6055
- var FOLLOW = 0.25;
6056
- var ZOOM_IN = 0.8;
6057
- var ZOOM_OUT = 1.25;
6058
- var PAN_FAST = 0.2;
6059
- function zoomTarget(base, factor) {
6060
- const newSpan = Math.max(MIN_SPAN_MS, (base.to - base.from) * factor);
6061
- return { from: base.to - newSpan, to: base.to };
6062
- }
6063
- function followStep(cur, target, follow = FOLLOW, epsFrac = 15e-4) {
6064
- const eps = Math.max(MIN_SPAN_MS, (target.to - target.from) * epsFrac);
6065
- const nf = cur.from + (target.from - cur.from) * follow;
6066
- const nt = cur.to + (target.to - cur.to) * follow;
6067
- const done = Math.abs(target.from - nf) <= eps && Math.abs(target.to - nt) <= eps;
6068
- return { cur: done ? { ...target } : { from: nf, to: nt }, done };
6069
- }
6070
- var Glider = class {
6071
- constructor(chart) {
6072
- this.chart = chart;
6073
- // The glide eases ITS OWN range, never the chart's read-back: getVisibleRange()
6074
- // is data-bounded (whole bars, `to` capped at the newest bar) and the renderer
6075
- // clamps what gets applied — chasing that read-back can never converge on a
6076
- // clamped step, leaving the rAF loop re-applying a stale target forever (and
6077
- // grinding the span down when panning past the newest bar). The internal range
6078
- // converges geometrically, so the glide always terminates; the renderer is free
6079
- // to clamp each applied frame.
6080
- this.cur = null;
6081
- this.target = null;
6082
- this.raf = 0;
6083
- }
6084
- zoom(factor) {
6085
- this.to((base) => zoomTarget(base, factor));
6086
- }
6087
- stop() {
6088
- if (this.raf) cancelAnimationFrame(this.raf);
6089
- this.raf = 0;
6090
- this.cur = null;
6091
- this.target = null;
6092
- }
6093
- to(make) {
6094
- const chart = this.chart();
6095
- const base = this.target ?? chart?.getVisibleRange();
6096
- if (!chart || !base) return;
6097
- this.cur ?? (this.cur = { ...base });
6098
- this.target = make(base);
6099
- if (!this.raf) this.tick();
6100
- }
6101
- tick() {
6102
- this.raf = requestAnimationFrame(() => {
6103
- const chart = this.chart();
6104
- if (!chart || !this.cur || !this.target) {
6105
- this.stop();
6106
- return;
6107
- }
6108
- const { cur: next, done } = followStep(this.cur, this.target);
6109
- this.cur = next;
6110
- chart.setVisibleRange(next);
6111
- if (done) this.stop();
6112
- else this.tick();
6113
- });
6114
- }
6115
- };
6116
-
6117
6400
  // src/widget/tool-shortcuts.ts
6118
6401
  var TOOL_BINDINGS = [
6119
6402
  ["trendline", "drawings.trendline"],
@@ -6185,8 +6468,8 @@ var LayoutModeController = class {
6185
6468
  };
6186
6469
 
6187
6470
  // src/widget/mobile-bar.ts
6188
- var STYLE_ID13 = "vela-widget-mobilebar";
6189
- var CSS13 = `
6471
+ var STYLE_ID14 = "vela-widget-mobilebar";
6472
+ var CSS14 = `
6190
6473
  .vela-mobilebar {
6191
6474
  display: none;
6192
6475
  align-items: stretch;
@@ -6217,6 +6500,9 @@ var CSS13 = `
6217
6500
  }
6218
6501
  .vela-mb-item:active { background: var(--vela-hover); }
6219
6502
  .vela-mb-item .vela-icon { font-size: 18px; width: 18px; height: 18px; }
6503
+ /* A lit stop (the maximize toggle while something is isolated): the inverse
6504
+ "selected" chip \u2014 white on the dark theme, dark on the light one. */
6505
+ .vela-mb-item.vela-mb-on, .vela-mb-item.vela-mb-on:active { background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
6220
6506
  /* Left-aligned contributed actions get their own stops (the built-in indicators
6221
6507
  slot) \u2014 the wrapper is layout-transparent so each stop flexes like a sibling. */
6222
6508
  .vela-mb-actions { display: contents; }
@@ -6236,7 +6522,7 @@ var MobileBar = class {
6236
6522
  constructor(host, opts) {
6237
6523
  this.opts = opts;
6238
6524
  const doc = host.ownerDocument;
6239
- injectStyles(STYLE_ID13, CSS13, doc);
6525
+ injectStyles(STYLE_ID14, CSS14, doc);
6240
6526
  this.el = doc.createElement("div");
6241
6527
  this.el.className = "vela-mobilebar";
6242
6528
  const item = (cls, label, onClick, icon2) => {
@@ -6257,9 +6543,11 @@ var MobileBar = class {
6257
6543
  this.actionsHost.className = "vela-mb-actions";
6258
6544
  const onDrawings = opts.onDrawingsClick;
6259
6545
  const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
6546
+ const onMaximize = opts.onMaximizeClick;
6547
+ this.maxEl = onMaximize ? item("vela-mb-maximize", "Maximize chart", onMaximize, "maximize") : null;
6260
6548
  const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
6261
6549
  const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
6262
- this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], more, settings);
6550
+ this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], ...this.maxEl ? [this.maxEl] : [], more, settings);
6263
6551
  host.appendChild(this.el);
6264
6552
  this.renderActions();
6265
6553
  }
@@ -6291,14 +6579,22 @@ var MobileBar = class {
6291
6579
  setTimeframe(tf) {
6292
6580
  this.tfEl.textContent = timeframeLabel(tf);
6293
6581
  }
6582
+ /** Light the maximize stop while something is isolated (a chart over the grid,
6583
+ * or a maximized pane inside the active chart) — inverse chip + restore glyph. */
6584
+ setMaximizeActive(on) {
6585
+ if (!this.maxEl) return;
6586
+ this.maxEl.classList.toggle("vela-mb-on", on);
6587
+ this.maxEl.setAttribute("aria-label", on ? "Restore layout" : "Maximize chart");
6588
+ this.maxEl.replaceChildren(iconEl(on ? "restore" : "maximize", this.el.ownerDocument));
6589
+ }
6294
6590
  destroy() {
6295
6591
  this.el.remove();
6296
6592
  }
6297
6593
  };
6298
6594
 
6299
6595
  // src/widget/timeframe-drawer.ts
6300
- var STYLE_ID14 = "vela-widget-tf-drawer";
6301
- var CSS14 = `
6596
+ var STYLE_ID15 = "vela-widget-tf-drawer";
6597
+ var CSS15 = `
6302
6598
  .vela-tfd-heading {
6303
6599
  padding: 4px 2px 8px;
6304
6600
  font-size: 12px;
@@ -6354,7 +6650,7 @@ var TimeframeDrawer = class {
6354
6650
  this.rangeChips = /* @__PURE__ */ new Map();
6355
6651
  this.tfChips = /* @__PURE__ */ new Map();
6356
6652
  const doc = opts.host.ownerDocument;
6357
- injectStyles(STYLE_ID14, CSS14, doc);
6653
+ injectStyles(STYLE_ID15, CSS15, doc);
6358
6654
  this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
6359
6655
  const rangeHeading = doc.createElement("div");
6360
6656
  rangeHeading.className = "vela-tfd-heading";
@@ -6412,8 +6708,8 @@ var TimeframeDrawer = class {
6412
6708
  };
6413
6709
 
6414
6710
  // src/widget/drawings-drawer.ts
6415
- var STYLE_ID15 = "vela-widget-drawings-drawer";
6416
- var CSS15 = `
6711
+ var STYLE_ID16 = "vela-widget-drawings-drawer";
6712
+ var CSS16 = `
6417
6713
  /* Search + tabs stay pinned while the tool list scrolls underneath. */
6418
6714
  .vela-dd-sticky {
6419
6715
  position: sticky;
@@ -6515,7 +6811,7 @@ var DrawingsDrawer = class {
6515
6811
  this.definition = { groups: [] };
6516
6812
  this.activeGroup = "";
6517
6813
  const doc = opts.host.ownerDocument;
6518
- injectStyles(STYLE_ID15, CSS15, doc);
6814
+ injectStyles(STYLE_ID16, CSS16, doc);
6519
6815
  this.drawer = new Drawer({
6520
6816
  host: opts.host,
6521
6817
  title: "Drawings",
@@ -6657,8 +6953,8 @@ var DrawingsDrawer = class {
6657
6953
  };
6658
6954
 
6659
6955
  // src/widget/more-drawer.ts
6660
- var STYLE_ID16 = "vela-widget-more-drawer";
6661
- var CSS16 = `
6956
+ var STYLE_ID17 = "vela-widget-more-drawer";
6957
+ var CSS17 = `
6662
6958
  .vela-md-actions {
6663
6959
  display: flex;
6664
6960
  gap: 6px;
@@ -6733,7 +7029,7 @@ var MoreDrawer = class {
6733
7029
  constructor(opts) {
6734
7030
  this.opts = opts;
6735
7031
  this.view = "main";
6736
- injectStyles(STYLE_ID16, CSS16, opts.host.ownerDocument);
7032
+ injectStyles(STYLE_ID17, CSS17, opts.host.ownerDocument);
6737
7033
  this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
6738
7034
  }
6739
7035
  open() {
@@ -6927,8 +7223,8 @@ var MoreDrawer = class {
6927
7223
  };
6928
7224
 
6929
7225
  // src/widget/timezone-drawer.ts
6930
- var STYLE_ID17 = "vela-widget-timezone-drawer";
6931
- var CSS17 = `
7226
+ var STYLE_ID18 = "vela-widget-timezone-drawer";
7227
+ var CSS18 = `
6932
7228
  .vela-tzd-list { padding: 2px 0 4px; }
6933
7229
  .vela-tzd-row {
6934
7230
  display: flex;
@@ -6947,7 +7243,7 @@ var CSS17 = `
6947
7243
  var TimezoneDrawer = class {
6948
7244
  constructor(opts) {
6949
7245
  this.opts = opts;
6950
- injectStyles(STYLE_ID17, CSS17, opts.host.ownerDocument);
7246
+ injectStyles(STYLE_ID18, CSS18, opts.host.ownerDocument);
6951
7247
  this.drawer = new Drawer({ host: opts.host, title: "Time zone", onOpenChange: opts.onOpenChange });
6952
7248
  }
6953
7249
  open() {
@@ -6985,8 +7281,8 @@ var TimezoneDrawer = class {
6985
7281
  };
6986
7282
 
6987
7283
  // src/widget/price-scale-drawer.ts
6988
- var STYLE_ID18 = "vela-widget-price-scale-drawer";
6989
- var CSS18 = `
7284
+ var STYLE_ID19 = "vela-widget-price-scale-drawer";
7285
+ var CSS19 = `
6990
7286
  .vela-psd-list { padding: 2px 0 4px; }
6991
7287
  .vela-psd-row {
6992
7288
  display: flex;
@@ -7015,7 +7311,7 @@ var PriceScaleDrawer = class {
7015
7311
  constructor(opts) {
7016
7312
  this.opts = opts;
7017
7313
  this.pane = null;
7018
- injectStyles(STYLE_ID18, CSS18, opts.host.ownerDocument);
7314
+ injectStyles(STYLE_ID19, CSS19, opts.host.ownerDocument);
7019
7315
  this.drawer = new Drawer({ host: opts.host, title: "Price scale", onOpenChange: opts.onOpenChange });
7020
7316
  }
7021
7317
  open() {
@@ -7151,8 +7447,8 @@ var PriceScaleDrawer = class {
7151
7447
  };
7152
7448
 
7153
7449
  // src/widget/drawing-pill.ts
7154
- var STYLE_ID19 = "vela-widget-drawing-pill";
7155
- var CSS19 = `
7450
+ var STYLE_ID20 = "vela-widget-drawing-pill";
7451
+ var CSS20 = `
7156
7452
  .vela-drawpill { display: none; }
7157
7453
  [data-layout='mobile'] .vela-drawpill {
7158
7454
  position: absolute;
@@ -7212,7 +7508,7 @@ var DrawingPill = class {
7212
7508
  this.chart = null;
7213
7509
  this.chartSubs = [];
7214
7510
  const doc = host.ownerDocument;
7215
- injectStyles(STYLE_ID19, CSS19, doc);
7511
+ injectStyles(STYLE_ID20, CSS20, doc);
7216
7512
  this.el = doc.createElement("div");
7217
7513
  this.el.className = "vela-drawpill";
7218
7514
  this.el.hidden = true;
@@ -7325,10 +7621,10 @@ function buildContext(host) {
7325
7621
  var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
7326
7622
  var GAP_PX = 2;
7327
7623
  var POOL_CAP = 16;
7328
- var TIME_AXIS_H2 = 22;
7624
+ var TIME_AXIS_H3 = 22;
7329
7625
  var ALERT_CAP = 50;
7330
- var STYLE_ID20 = "vela-workspace";
7331
- var CSS20 = `
7626
+ var STYLE_ID21 = "vela-workspace";
7627
+ var CSS21 = `
7332
7628
  .vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
7333
7629
  .vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
7334
7630
  .vela-ws-toolbar { position: relative; flex: none; }
@@ -7355,6 +7651,21 @@ var CSS20 = `
7355
7651
  /* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
7356
7652
  drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
7357
7653
  [data-layout='mobile'] .vela-ws-toolbar { display: none; }
7654
+ /* A maximized cell owns the whole grid: the splitter strips have no seams to grab and
7655
+ the active ring would just outline the only visible chart \u2014 both are noise here. */
7656
+ .vela-ws-grid[data-maximized='1'] .vela-ws-splitter { display: none; }
7657
+ .vela-ws-grid[data-maximized='1'] .vela-cell[data-active='1']::after { display: none; }
7658
+ /* Drop-target preview while a cell's drag handle is held: a dashed ring + the same
7659
+ soft wash the splitter hover uses, over the chart, inert to the pointer. */
7660
+ .vela-cell[data-drop-target='1']::before {
7661
+ content: '';
7662
+ position: absolute;
7663
+ inset: 0;
7664
+ border: 2px dashed var(--vela-fg-bright);
7665
+ background: var(--vela-separator-hover-band);
7666
+ pointer-events: none;
7667
+ z-index: 11;
7668
+ }
7358
7669
  `;
7359
7670
  registerIcon("layout", svg16('<rect x="1.5" y="1.5" width="13" height="13" rx="1.5"/><path d="M8 1.5v13M1.5 8h13"/>'));
7360
7671
  function declaredOrder(cells) {
@@ -7384,6 +7695,9 @@ var VelaWorkspace = class {
7384
7695
  * slots beyond the list get auto identities. Grows, never reorders. */
7385
7696
  this.order = [];
7386
7697
  this.activeId = null;
7698
+ /** The cell maximized over the whole grid (null = normal grid). TRANSIENT view
7699
+ * state — never persisted; any structural change (layout, applyState) restores. */
7700
+ this.maximizedId = null;
7387
7701
  this.cellBackend = "auto";
7388
7702
  this.destroyed = false;
7389
7703
  this.shortcutsHelp = null;
@@ -7488,7 +7802,7 @@ var VelaWorkspace = class {
7488
7802
  this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
7489
7803
  const bootActive = boot?.activeCellId ?? null;
7490
7804
  const doc = hostEl.ownerDocument;
7491
- injectStyles(STYLE_ID20, CSS20, doc);
7805
+ injectStyles(STYLE_ID21, CSS21, doc);
7492
7806
  this.root = doc.createElement("div");
7493
7807
  this.root.className = "vela-workspace";
7494
7808
  ensureUIHost(this.root, resolveTheme(opts.theme));
@@ -7598,7 +7912,11 @@ var VelaWorkspace = class {
7598
7912
  if (attribution !== false) {
7599
7913
  const background = resolveTheme(opts.theme).background;
7600
7914
  const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background);
7601
- Object.assign(mark.style, { left: "12px", bottom: `${TIME_AXIS_H2 + 10}px`, zIndex: "11" });
7915
+ Object.assign(mark.style, {
7916
+ left: "calc(var(--vela-toolbar-gutter, 0px) + 12px)",
7917
+ bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H3}px) + 10px)`,
7918
+ zIndex: "11"
7919
+ });
7602
7920
  this.gridEl.appendChild(mark);
7603
7921
  this.attributionMark = mark;
7604
7922
  }
@@ -7664,6 +7982,9 @@ var VelaWorkspace = class {
7664
7982
  ...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
7665
7983
  getContext: () => this.context(),
7666
7984
  ...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
7985
+ // Multi-chart only: the stop that isolates the ACTIVE chart (the
7986
+ // per-cell hover cluster has no cursor to reveal it on mobile).
7987
+ ...this.monoLayout ? {} : { onMaximizeClick: () => this.toggleMobileMaximize() },
7667
7988
  onMoreClick: () => this.openMoreDrawer(),
7668
7989
  onSettingsClick: () => this.active.chart.renderer.openSettings()
7669
7990
  }) : null;
@@ -7778,7 +8099,10 @@ var VelaWorkspace = class {
7778
8099
  this.topbar.renderActions();
7779
8100
  this.mobileBar?.renderActions();
7780
8101
  this.dock.refresh();
7781
- for (const cell of this.cells()) cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
8102
+ for (const cell of this.cells()) {
8103
+ cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
8104
+ cell.chart.renderer.setLegendCallouts(legendCalloutsProviderFor(cell.chart, () => this.context()));
8105
+ }
7782
8106
  }
7783
8107
  // ── state surface (the SDK's read/restore of the whole grid's config + content) ──
7784
8108
  /**
@@ -7867,7 +8191,9 @@ var VelaWorkspace = class {
7867
8191
  this.pool.clear();
7868
8192
  for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
7869
8193
  this.order = st.charts.map((c) => c.id);
8194
+ this.clearMaximized();
7870
8195
  this.applyGrid();
8196
+ this.refreshCellControls();
7871
8197
  const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
7872
8198
  if (nextActive2 === this.activeId) this.projectActiveCell();
7873
8199
  else this.setActiveCell(nextActive2);
@@ -7893,6 +8219,7 @@ var VelaWorkspace = class {
7893
8219
  const def = this.monoLayout ? null : ensureLayout(st.layout);
7894
8220
  if (def) this.def = def;
7895
8221
  this.cellBackend = this.backendFor(this.def);
8222
+ this.clearMaximized();
7896
8223
  this.applyGrid();
7897
8224
  this.buildCells();
7898
8225
  this.syncCellPresentation();
@@ -7956,6 +8283,7 @@ var VelaWorkspace = class {
7956
8283
  setLayout(layout) {
7957
8284
  if (this.destroyed) return;
7958
8285
  if (this.monoLayout) return;
8286
+ this.clearMaximized();
7959
8287
  const next = this.resolveLayout(layout);
7960
8288
  const nextBackend = this.backendFor(next);
7961
8289
  const rebuildAll = nextBackend !== this.cellBackend;
@@ -7976,6 +8304,7 @@ var VelaWorkspace = class {
7976
8304
  this.buildCells();
7977
8305
  this.alignNewCellStyles(preexisting);
7978
8306
  this.syncCellPresentation();
8307
+ this.refreshCellControls();
7979
8308
  this.topbar.setLayout(next.id);
7980
8309
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
7981
8310
  if (nextActive === this.activeId) this.projectActiveCell();
@@ -7984,6 +8313,67 @@ var VelaWorkspace = class {
7984
8313
  this.events.emit("layout:changed", { layout: next.id });
7985
8314
  this.markStateDirty();
7986
8315
  }
8316
+ /** The identity of the cell maximized over the whole grid, or null. */
8317
+ get maximizedCell() {
8318
+ return this.maximizedId;
8319
+ }
8320
+ /**
8321
+ * Maximize one cell over the whole grid, or restore the layout with `null`. Pure
8322
+ * presentation: the other cells stay alive underneath — charts, subscriptions and
8323
+ * state untouched — so restoring is instant. The maximized cell becomes the active
8324
+ * one. Transient view state (also reachable from each cell's bottom-center view
8325
+ * cluster): switching layouts or applying a state document restores the grid.
8326
+ */
8327
+ maximizeCell(id) {
8328
+ if (this.destroyed) return;
8329
+ if (id != null && (!this.cellsById.has(id) || this.def.cells.length <= 1)) return;
8330
+ if (id === this.maximizedId) return;
8331
+ this.maximizedId = id;
8332
+ if (id) this.setActiveCell(id);
8333
+ this.applyGrid();
8334
+ this.refreshCellControls();
8335
+ this.syncMobileMaximize();
8336
+ this.events.emit("cell:maximized", { id });
8337
+ }
8338
+ /** The mobile bar's maximize stop: one press isolates the ACTIVE chart over the
8339
+ * grid; while something is already isolated — the chart, or a pane inside it
8340
+ * (mobile's double-tap) — the press restores that instead. Every branch re-syncs
8341
+ * the stop on its own (`maximizeCell` directly, `panes.maximize` via its
8342
+ * synchronous `pane:changed`). */
8343
+ toggleMobileMaximize() {
8344
+ const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
8345
+ if (!cell) return;
8346
+ if (this.maximizedId) this.maximizeCell(null);
8347
+ else if (cell.chart.panes.list().some((p) => p.maximized)) cell.chart.panes.maximize(null);
8348
+ else this.maximizeCell(cell.id);
8349
+ }
8350
+ /** Keep the mobile bar's maximize stop truthful: lit (inverse chip, restore
8351
+ * glyph) while the active chart covers the grid OR one of its panes is
8352
+ * maximized — the state a double-tap toggles is otherwise invisible on mobile. */
8353
+ syncMobileMaximize() {
8354
+ if (!this.mobileBar) return;
8355
+ const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
8356
+ const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
8357
+ this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
8358
+ }
8359
+ /**
8360
+ * Trade the SLOTS of two live cells — the grid arrangement changes, the cells
8361
+ * themselves (charts, indicators, drawings, the active flag) stay untouched.
8362
+ * What each cell's drag handle commits; also callable directly by hosts.
8363
+ */
8364
+ swapCells(a, b) {
8365
+ if (this.destroyed || a === b) return;
8366
+ const i = this.order.indexOf(a);
8367
+ const j = this.order.indexOf(b);
8368
+ if (i < 0 || j < 0 || !this.cellsById.has(a) || !this.cellsById.has(b)) return;
8369
+ [this.order[i], this.order[j]] = [this.order[j], this.order[i]];
8370
+ for (const [k] of this.def.cells.entries()) {
8371
+ const host = this.cellsById.get(this.order[k] ?? "")?.host;
8372
+ if (host) this.gridEl.appendChild(host);
8373
+ }
8374
+ this.applyGrid();
8375
+ this.markStateDirty();
8376
+ }
7987
8377
  resize() {
7988
8378
  this.splitters.layout();
7989
8379
  }
@@ -8053,6 +8443,7 @@ var VelaWorkspace = class {
8053
8443
  this.mobileBar?.renderActions();
8054
8444
  this.mobileBar?.setSymbol(cell.symbol);
8055
8445
  this.mobileBar?.setTimeframe(cell.timeframe);
8446
+ this.syncMobileMaximize();
8056
8447
  this.drawingPill?.onChart(cell.chart);
8057
8448
  const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
8058
8449
  this.historyUnsub?.();
@@ -8142,8 +8533,78 @@ var VelaWorkspace = class {
8142
8533
  const host = this.cellsById.get(this.order[i] ?? "")?.host;
8143
8534
  if (host) host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
8144
8535
  }
8536
+ this.applyMaximizePresentation();
8537
+ this.mountAttributionMark();
8145
8538
  this.splitters.layout();
8146
8539
  }
8540
+ /** Overlay the maximize presentation on the freshly applied grid: EVERY cell spans
8541
+ * the full track grid — the maximized one on top, the siblings invisible beneath
8542
+ * it (their charts stay alive — restoring is instant). The siblings must span too:
8543
+ * left in their slots they would auto-flow into implicit zero-height rows, whose
8544
+ * gaps steal height from the maximized cell and collapse their renderers to 0.
8545
+ * The splitter strips and the active ring hide via the `data-maximized` rules. */
8546
+ applyMaximizePresentation() {
8547
+ const maxId = this.maximizedId;
8548
+ if (maxId) this.gridEl.dataset.maximized = "1";
8549
+ else delete this.gridEl.dataset.maximized;
8550
+ for (const [id, cell] of this.cellsById) {
8551
+ const style = cell.host.style;
8552
+ if (maxId) style.gridArea = "1 / 1 / -1 / -1";
8553
+ style.zIndex = maxId && id === maxId ? "5" : "";
8554
+ style.visibility = maxId && id !== maxId ? "hidden" : "";
8555
+ }
8556
+ }
8557
+ /** Rebuild every cell's view cluster (the maximize gate or state changed). */
8558
+ refreshCellControls() {
8559
+ for (const cell of this.cellsById.values()) cell.refreshControls();
8560
+ }
8561
+ /** Drop the transient maximize on a structural change (layout switch, state
8562
+ * document) — WITH the event, so hosts tracking `cell:maximized` never drift
8563
+ * from `maximizedCell`. The caller's own grid re-apply paints the restore. */
8564
+ clearMaximized() {
8565
+ if (this.maximizedId == null) return;
8566
+ this.maximizedId = null;
8567
+ this.events.emit("cell:maximized", { id: null });
8568
+ }
8569
+ /** The live cell under a viewport point, excluding `excludeId` and any host a
8570
+ * maximize has hidden — the drag handle's hit-test. */
8571
+ cellAtPoint(x, y, excludeId) {
8572
+ for (const [id, cell] of this.cellsById) {
8573
+ if (id === excludeId || cell.host.style.visibility === "hidden") continue;
8574
+ const r = cell.host.getBoundingClientRect();
8575
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return id;
8576
+ }
8577
+ return null;
8578
+ }
8579
+ /** Mark one cell as the live drop target of a grip drag (null clears all) —
8580
+ * the `data-drop-target` stylesheet rule paints the dashed preview ring. */
8581
+ setDropTarget(id) {
8582
+ for (const [cid, cell] of this.cellsById) {
8583
+ if (cid === id) cell.host.dataset.dropTarget = "1";
8584
+ else delete cell.host.dataset.dropTarget;
8585
+ }
8586
+ }
8587
+ /** The cell whose bottom-left corner the grid's attribution mark floats in — the
8588
+ * maximized cell while one covers the grid, else the bottom-left slot's cell. */
8589
+ bottomLeftCell() {
8590
+ if (this.maximizedId) return this.cellsById.get(this.maximizedId);
8591
+ const grid = occupancyGrid(this.def);
8592
+ const slot = grid[grid.length - 1]?.[0];
8593
+ const idx = this.def.cells.findIndex((c) => (c.area ?? c.id) === slot);
8594
+ return this.cellsById.get(this.order[idx >= 0 ? idx : 0] ?? "");
8595
+ }
8596
+ /** Keep the shared attribution mark inside the BOTTOM-LEFT visible cell: its
8597
+ * offsets ride that cell's renderer-published `--vela-bottom-gutter` /
8598
+ * `--vela-toolbar-gutter`, so collapsed pane strips push the mark up without any
8599
+ * bookkeeping here. Re-run after anything that changes which host that is
8600
+ * (layout switch, maximize, cell rebuild); a destroyed host drops the mark from
8601
+ * the DOM, and this re-mount brings it back. */
8602
+ mountAttributionMark() {
8603
+ const mark = this.attributionMark;
8604
+ if (!mark) return;
8605
+ const host = this.bottomLeftCell()?.host ?? this.gridEl;
8606
+ if (mark.parentElement !== host) host.appendChild(mark);
8607
+ }
8147
8608
  /** Create the cells the current layout wants but don't exist yet (pool-first).
8148
8609
  * A slot's CELL IDENTITY is `order[i]` (declaration order — never the slot's own
8149
8610
  * positional id); slots past the declared list mint an auto identity once. */
@@ -8175,6 +8636,12 @@ var VelaWorkspace = class {
8175
8636
  setTimezone: (zone) => this.setTimezone(zone),
8176
8637
  context: () => this.context(),
8177
8638
  activate: (id2) => this.setActiveCell(id2),
8639
+ multiCell: () => !this.monoLayout && this.def.cells.length > 1,
8640
+ isMaximized: (id2) => this.maximizedId === id2,
8641
+ toggleMaximize: (id2) => this.maximizeCell(this.maximizedId === id2 ? null : id2),
8642
+ cellDragTarget: (id2, x, y) => this.cellAtPoint(x, y, id2),
8643
+ previewDropTarget: (target) => this.setDropTarget(target),
8644
+ dropCell: (id2, target) => this.swapCells(id2, target),
8178
8645
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
8179
8646
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
8180
8647
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
@@ -8188,6 +8655,7 @@ var VelaWorkspace = class {
8188
8655
  if (id === this.activeId) cell.host.dataset.active = "1";
8189
8656
  this.wireCell(cell);
8190
8657
  cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
8658
+ cell.setControlsSuspended(this.layoutCtl.current === "mobile");
8191
8659
  if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
8192
8660
  cell.setManifest(this.manifest, pooled?.indicators == null);
8193
8661
  cell.restorePersistedExt();
@@ -8197,6 +8665,7 @@ var VelaWorkspace = class {
8197
8665
  const host = this.cellsById.get(this.order[i] ?? "")?.host;
8198
8666
  if (host) this.gridEl.appendChild(host);
8199
8667
  }
8668
+ this.mountAttributionMark();
8200
8669
  }
8201
8670
  /** Per-cell chart subscriptions (trigger ② — the chart instance is stable for the
8202
8671
  * cell's whole life, so these live and die with the cell). */
@@ -8256,6 +8725,9 @@ var VelaWorkspace = class {
8256
8725
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
8257
8726
  chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
8258
8727
  chart.on("theme:changed", (t) => this.setTheme(t));
8728
+ chart.on("pane:changed", () => {
8729
+ if (cell.id === this.activeId) this.syncMobileMaximize();
8730
+ });
8259
8731
  chart.renderer.onAxisLongPress((e) => {
8260
8732
  if (this.layoutCtl.current !== "mobile") return;
8261
8733
  if (e.axis === "time") this.openTimezoneDrawer();
@@ -8576,6 +9048,7 @@ var VelaWorkspace = class {
8576
9048
  for (const cell of this.cellsById.values()) {
8577
9049
  cell.chart.renderer.closeDialogs();
8578
9050
  cell.chart.renderer.setLayoutMode(mode);
9051
+ cell.setControlsSuspended(mode === "mobile");
8579
9052
  }
8580
9053
  this.syncCellPresentation();
8581
9054
  }