@luxalgo/vela 0.6.9 → 0.6.11

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 (46) hide show
  1. package/dist/{DataProvider-8Z95Q-RJ.d.cts → DataProvider-BBf-jc6W.d.ts} +49 -1
  2. package/dist/{DataProvider-DExJrfut.d.ts → DataProvider-p0TEyhlX.d.cts} +49 -1
  3. package/dist/{chunk-6WDDVMBJ.js → chunk-73PEA4MU.js} +707 -353
  4. package/dist/{chunk-62SVGONC.js → chunk-G77Y7LK2.js} +2 -2
  5. package/dist/{chunk-STHSKXOR.js → chunk-IO3NYSQV.js} +650 -157
  6. package/dist/{chunk-WFSZBX3R.js → chunk-KG4YT3TI.js} +3 -1
  7. package/dist/{chunk-FCVIG7JG.js → chunk-MHY7MVXH.js} +5 -1
  8. package/dist/{chunk-EQCHJZOT.js → chunk-MTLJKZDZ.js} +10 -2
  9. package/dist/{contributions-D7PVZO2i.d.ts → contributions-Bbe2R-mQ.d.ts} +20 -6
  10. package/dist/{contributions-C1U2Krwg.d.cts → contributions-CO01zWve.d.cts} +20 -6
  11. package/dist/index.cjs +719 -350
  12. package/dist/index.d.cts +37 -10
  13. package/dist/index.d.ts +37 -10
  14. package/dist/index.js +5 -5
  15. package/dist/{options-FM0peknS.d.ts → options-yp7sA96q.d.cts} +14 -7
  16. package/dist/{options-FM0peknS.d.cts → options-yp7sA96q.d.ts} +14 -7
  17. package/dist/{plugin-DfVqBz9p.d.cts → plugin-CkkH8QnX.d.cts} +3 -3
  18. package/dist/{plugin-7bkF32Rk.d.ts → plugin-DwxjM3Ni.d.ts} +3 -3
  19. package/dist/plugin.cjs +12 -1
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +3 -3
  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-DOPiT6I6.d.cts → statusline-CHPDuKNp.d.ts} +13 -5
  30. package/dist/{statusline-zdF4eZLr.d.ts → statusline-DTHZUFqK.d.cts} +13 -5
  31. package/dist/ui.cjs +7 -1
  32. package/dist/ui.d.cts +3 -1
  33. package/dist/ui.d.ts +3 -1
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +719 -350
  36. package/dist/vela.global.min.js +51 -49
  37. package/dist/widget.cjs +1249 -388
  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 +1249 -388
  42. package/dist/workspace.d.cts +103 -10
  43. package/dist/workspace.d.ts +103 -10
  44. package/dist/workspace.js +6 -6
  45. package/package.json +1 -1
  46. /package/dist/{chunk-AOGZBKUE.js → chunk-H26BEHF4.js} +0 -0
@@ -1,8 +1,8 @@
1
- import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel, LEGEND_AT_TOP_ATTR, Vela, TypedEventBus, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-6WDDVMBJ.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-EQCHJZOT.js';
3
- import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-62SVGONC.js';
4
- import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-FCVIG7JG.js';
5
- import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, SESSION_OFF, SESSION_POST, SESSION_PRE, categoricalColor } from './chunk-WFSZBX3R.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-73PEA4MU.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-MTLJKZDZ.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
@@ -654,7 +654,7 @@ var Topbar = class {
654
654
  this.leftActionsHost.className = "vela-widget-actions-left";
655
655
  this.panelsHost = doc.createElement("span");
656
656
  this.panelsHost.className = "vela-widget-panels";
657
- const tool = (cls, icon, tip, onClick) => this.toolButton(cls, icon, tip, onClick, this.tooltips);
657
+ const tool = (cls, icon2, tip, onClick) => this.toolButton(cls, icon2, tip, onClick, this.tooltips);
658
658
  if (vis("undo-redo")) {
659
659
  this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
660
660
  this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
@@ -852,8 +852,8 @@ var Topbar = class {
852
852
  }
853
853
  renderStyleButton(doc) {
854
854
  this.styleButton.replaceChildren();
855
- const icon = priceStyleIcon(this.priceStyle);
856
- if (icon) this.styleButton.appendChild(iconEl(icon, doc));
855
+ const icon2 = priceStyleIcon(this.priceStyle);
856
+ if (icon2) this.styleButton.appendChild(iconEl(icon2, doc));
857
857
  else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle)));
858
858
  this.styleButton.setAttribute("aria-label", `Chart style \u2014 ${priceStyleLabel(this.priceStyle)}`);
859
859
  }
@@ -945,11 +945,11 @@ var Topbar = class {
945
945
  this.el.remove();
946
946
  }
947
947
  /** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
948
- toolButton(cls, icon, tip, onClick, sink) {
948
+ toolButton(cls, icon2, tip, onClick, sink) {
949
949
  const doc = this.el.ownerDocument;
950
950
  const b = doc.createElement("button");
951
951
  b.className = `vela-widget-tool ${cls}`;
952
- b.appendChild(iconEl(icon, doc));
952
+ b.appendChild(iconEl(icon2, doc));
953
953
  b.setAttribute("aria-label", tip);
954
954
  sink.push(new Tooltip(b, { content: tip, triggerId: `vela-tool-${cls}`, host: this.host }));
955
955
  if (onClick) b.addEventListener("click", onClick);
@@ -1629,13 +1629,13 @@ var MenuBuilder = class {
1629
1629
  this.actions = /* @__PURE__ */ new Map();
1630
1630
  this.seq = 0;
1631
1631
  }
1632
- entry(label, icon, run, separatorBefore = false) {
1632
+ entry(label, icon2, run, separatorBefore = false) {
1633
1633
  const id = this.next();
1634
1634
  this.actions.set(id, run);
1635
- return { id, label, icon, separatorBefore };
1635
+ return { id, label, icon: icon2, separatorBefore };
1636
1636
  }
1637
- submenu(label, icon, submenu, separatorBefore = false) {
1638
- return { id: this.next(), label, icon, submenu, separatorBefore };
1637
+ submenu(label, icon2, submenu, separatorBefore = false) {
1638
+ return { id: this.next(), label, icon: icon2, submenu, separatorBefore };
1639
1639
  }
1640
1640
  next() {
1641
1641
  this.seq += 1;
@@ -1872,8 +1872,8 @@ var ObjectTree = class extends SidePanel {
1872
1872
  const acts = doc.createElement("span");
1873
1873
  acts.className = "vela-ot-acts";
1874
1874
  head.appendChild(acts);
1875
- const op = (icon, title, run) => {
1876
- acts.appendChild(this.btn({ icon, title, run }));
1875
+ const op = (icon2, title, run) => {
1876
+ acts.appendChild(this.btn({ icon: icon2, title, run }));
1877
1877
  };
1878
1878
  if (pane.kind !== "price") {
1879
1879
  if (pane.order > 1) op("arrow-up", "Move pane up", () => chart.panes.move(pane.id, "up"));
@@ -1888,8 +1888,8 @@ var ObjectTree = class extends SidePanel {
1888
1888
  const { chart } = pass;
1889
1889
  if (row.kind === "price") {
1890
1890
  const base = this.symbolName.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || this.symbolName;
1891
- const icon = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
1892
- const el2 = this.row(icon, row.label, row.visible, [
1891
+ const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
1892
+ const el2 = this.row(icon2, row.label, row.visible, [
1893
1893
  {
1894
1894
  icon: row.visible ? "eye" : "eye-off",
1895
1895
  title: row.visible ? "Hide" : "Show",
@@ -2008,16 +2008,16 @@ var ObjectTree = class extends SidePanel {
2008
2008
  const doc = this.el.ownerDocument;
2009
2009
  const { chart } = pass;
2010
2010
  const markup = drawingMeta(d.type).icon;
2011
- let icon;
2011
+ let icon2;
2012
2012
  if (markup) {
2013
- icon = doc.createElement("span");
2014
- icon.className = "vela-icon";
2015
- icon.setAttribute("aria-hidden", "true");
2016
- icon.innerHTML = markup;
2013
+ icon2 = doc.createElement("span");
2014
+ icon2.className = "vela-icon";
2015
+ icon2.setAttribute("aria-hidden", "true");
2016
+ icon2.innerHTML = markup;
2017
2017
  } else {
2018
- icon = iconEl("pen", doc);
2018
+ icon2 = iconEl("pen", doc);
2019
2019
  }
2020
- const el = this.row(icon, drawingLabel(d), d.visible, [
2020
+ const el = this.row(icon2, drawingLabel(d), d.visible, [
2021
2021
  {
2022
2022
  icon: d.locked ? "lock" : "unlock",
2023
2023
  title: d.locked ? "Unlock" : "Lock",
@@ -2083,7 +2083,7 @@ var ObjectTree = class extends SidePanel {
2083
2083
  }
2084
2084
  }
2085
2085
  /** The shared row shell: icon, name, optional tag, then the action buttons. */
2086
- row(icon, label, visible, actions, tag) {
2086
+ row(icon2, label, visible, actions, tag) {
2087
2087
  const doc = this.el.ownerDocument;
2088
2088
  const el = doc.createElement("div");
2089
2089
  el.className = "vela-ot-row";
@@ -2092,7 +2092,7 @@ var ObjectTree = class extends SidePanel {
2092
2092
  name.className = "vela-ot-name";
2093
2093
  name.textContent = label;
2094
2094
  name.title = label;
2095
- el.append(icon, name);
2095
+ el.append(icon2, name);
2096
2096
  if (tag) {
2097
2097
  const t = doc.createElement("span");
2098
2098
  t.className = "vela-ot-tag";
@@ -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";
@@ -3675,11 +3757,12 @@ async function resolveIndicators(config, fetchImpl = fetch) {
3675
3757
  }
3676
3758
  return out;
3677
3759
  }
3760
+ var ledgerEntryName = (e) => typeof e === "string" ? e : e.name;
3678
3761
  function indicatorLedger(i) {
3679
3762
  const natives = [...i.present];
3680
3763
  if (i.volumePending && !natives.includes("volume")) natives.push("volume");
3681
3764
  return {
3682
- manifest: i.manifestSettled ? [...i.instanceNames] : [...i.pendingManifest ?? i.instanceNames],
3765
+ manifest: i.manifestSettled ? [...i.instanceEntries] : [...i.pendingManifest ?? i.instanceEntries],
3683
3766
  natives
3684
3767
  };
3685
3768
  }
@@ -3761,7 +3844,17 @@ function sanitizeCell(raw) {
3761
3844
  if (c.drawings != null && typeof c.drawings === "object") out.drawings = c.drawings;
3762
3845
  const ind = c.indicators;
3763
3846
  if (ind != null && typeof ind === "object") {
3764
- const manifest = Array.isArray(ind.manifest) ? ind.manifest.filter((n) => typeof n === "string") : [];
3847
+ const manifest = Array.isArray(ind.manifest) ? ind.manifest.flatMap((n) => {
3848
+ if (typeof n === "string") return [n];
3849
+ if (n != null && typeof n === "object" && typeof n.name === "string") {
3850
+ const e = n;
3851
+ const bag = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
3852
+ const inputs = bag(e.inputs);
3853
+ const props = bag(e.props);
3854
+ return [inputs || props ? { name: e.name, ...inputs ? { inputs } : {}, ...props ? { props } : {} } : e.name];
3855
+ }
3856
+ return [];
3857
+ }) : [];
3765
3858
  const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
3766
3859
  out.indicators = { manifest, natives };
3767
3860
  }
@@ -4817,6 +4910,243 @@ var SessionShadingTracker = class {
4817
4910
  }
4818
4911
  };
4819
4912
 
4913
+ // src/widget/glide.ts
4914
+ var MIN_SPAN_MS = 6e4;
4915
+ var FOLLOW = 0.25;
4916
+ var ZOOM_IN = 0.8;
4917
+ var ZOOM_OUT = 1.25;
4918
+ var PAN_FAST = 0.2;
4919
+ function zoomTarget(base, factor) {
4920
+ const newSpan = Math.max(MIN_SPAN_MS, (base.to - base.from) * factor);
4921
+ return { from: base.to - newSpan, to: base.to };
4922
+ }
4923
+ function followStep(cur, target, follow = FOLLOW, epsFrac = 15e-4) {
4924
+ const eps = Math.max(MIN_SPAN_MS, (target.to - target.from) * epsFrac);
4925
+ const nf = cur.from + (target.from - cur.from) * follow;
4926
+ const nt = cur.to + (target.to - cur.to) * follow;
4927
+ const done = Math.abs(target.from - nf) <= eps && Math.abs(target.to - nt) <= eps;
4928
+ return { cur: done ? { ...target } : { from: nf, to: nt }, done };
4929
+ }
4930
+ var Glider = class {
4931
+ constructor(chart) {
4932
+ this.chart = chart;
4933
+ // The glide eases ITS OWN range, never the chart's read-back: getVisibleRange()
4934
+ // is data-bounded (whole bars, `to` capped at the newest bar) and the renderer
4935
+ // clamps what gets applied — chasing that read-back can never converge on a
4936
+ // clamped step, leaving the rAF loop re-applying a stale target forever (and
4937
+ // grinding the span down when panning past the newest bar). The internal range
4938
+ // converges geometrically, so the glide always terminates; the renderer is free
4939
+ // to clamp each applied frame.
4940
+ this.cur = null;
4941
+ this.target = null;
4942
+ this.raf = 0;
4943
+ }
4944
+ zoom(factor) {
4945
+ this.to((base) => zoomTarget(base, factor));
4946
+ }
4947
+ stop() {
4948
+ if (this.raf) cancelAnimationFrame(this.raf);
4949
+ this.raf = 0;
4950
+ this.cur = null;
4951
+ this.target = null;
4952
+ }
4953
+ to(make) {
4954
+ const chart = this.chart();
4955
+ const base = this.target ?? chart?.getVisibleRange();
4956
+ if (!chart || !base) return;
4957
+ this.cur ?? (this.cur = { ...base });
4958
+ this.target = make(base);
4959
+ if (!this.raf) this.tick();
4960
+ }
4961
+ tick() {
4962
+ this.raf = requestAnimationFrame(() => {
4963
+ const chart = this.chart();
4964
+ if (!chart || !this.cur || !this.target) {
4965
+ this.stop();
4966
+ return;
4967
+ }
4968
+ const { cur: next, done } = followStep(this.cur, this.target);
4969
+ this.cur = next;
4970
+ chart.setVisibleRange(next);
4971
+ if (done) this.stop();
4972
+ else this.tick();
4973
+ });
4974
+ }
4975
+ };
4976
+
4977
+ // src/widget/cell-controls.ts
4978
+ var CELL_CONTROLS_PROXIMITY_PX = 120;
4979
+ var TIME_AXIS_H2 = 22;
4980
+ var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
4981
+ var CLUSTER_H = 24;
4982
+ var CLUSTER_PILL = "rgba(0,0,0,0.65)";
4983
+ var CLUSTER_LEFT_CSS = "calc((100% + var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px)) / 2)";
4984
+ var STYLE_ID12 = "vela-cell-controls";
4985
+ var CSS12 = `
4986
+ .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;}
4987
+ .vela-cc-btn svg{display:block;}
4988
+ .vela-cc-btn:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
4989
+ .vela-cc-on,.vela-cc-on:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
4990
+ .vela-cc-grip{cursor:grab;touch-action:none;}
4991
+ .vela-cc-grip:active{cursor:grabbing;}
4992
+ `;
4993
+ function plotCenterX(width, toolbarGutter = 0, scaleGutter = 0) {
4994
+ return (width + toolbarGutter - scaleGutter) / 2;
4995
+ }
4996
+ function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX, gutters = {}) {
4997
+ const cx = plotCenterX(width, gutters.toolbar ?? 0, gutters.scale ?? 0);
4998
+ const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H / 2;
4999
+ return Math.hypot(x - cx, y - cy) <= proximityPx;
5000
+ }
5001
+ var CellControls = class {
5002
+ constructor(host, deps) {
5003
+ this.host = host;
5004
+ this.deps = deps;
5005
+ this.near = false;
5006
+ /** A grip drag is underway — the proximity reveal must not hide the cluster
5007
+ * while captured pointer moves sweep across the whole grid. */
5008
+ this.dragging = false;
5009
+ /** Mobile: the proximity reveal is meaningless without a cursor — the mobile
5010
+ * bar's maximize stop replaces the cluster. */
5011
+ this.suspended = false;
5012
+ this.onHostMove = (e) => {
5013
+ if (this.suspended) return;
5014
+ if (this.dragging) return;
5015
+ const rect = this.host.getBoundingClientRect();
5016
+ this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height, CELL_CONTROLS_PROXIMITY_PX, this.hostGutters()));
5017
+ };
5018
+ this.onHostLeave = () => {
5019
+ if (this.dragging) return;
5020
+ this.setNear(false);
5021
+ };
5022
+ injectStyles(STYLE_ID12, CSS12, host.ownerDocument);
5023
+ this.glider = new Glider(deps.chart);
5024
+ this.root = host.ownerDocument.createElement("div");
5025
+ Object.assign(this.root.style, {
5026
+ position: "absolute",
5027
+ left: CLUSTER_LEFT_CSS,
5028
+ bottom: `${CONTROLS_BOTTOM_PX}px`,
5029
+ transform: "translateX(-50%)",
5030
+ zIndex: "6",
5031
+ display: "none",
5032
+ // revealed by cursor proximity (onHostMove)
5033
+ gap: "2px",
5034
+ padding: "2px",
5035
+ borderRadius: "var(--vela-radius-md)",
5036
+ background: CLUSTER_PILL,
5037
+ pointerEvents: "auto"
5038
+ });
5039
+ this.host.addEventListener("pointermove", this.onHostMove);
5040
+ this.host.addEventListener("pointerleave", this.onHostLeave);
5041
+ this.host.appendChild(this.root);
5042
+ this.refresh();
5043
+ }
5044
+ /** Rebuild the buttons (the multi-cell gate or the maximized state changed). */
5045
+ refresh() {
5046
+ this.root.textContent = "";
5047
+ const multi = this.deps.multiCell();
5048
+ const maximized = multi && this.deps.isMaximized();
5049
+ if (multi && !maximized) this.root.appendChild(this.makeGrip());
5050
+ this.root.appendChild(this.button("minus", "Zoom out", () => this.glider.zoom(ZOOM_OUT)));
5051
+ this.root.appendChild(this.button("plus", "Zoom in", () => this.glider.zoom(ZOOM_IN)));
5052
+ if (multi) {
5053
+ this.root.appendChild(
5054
+ this.button(maximized ? "restore" : "maximize", maximized ? "Restore layout" : "Maximize chart", () => this.deps.toggleMaximize(), {
5055
+ // The maximized state reads as an inverse chip (white-on-dark, dark-on-light),
5056
+ // the same active-state affordance as a collapsed pane's expand button.
5057
+ selected: maximized
5058
+ })
5059
+ );
5060
+ }
5061
+ this.root.appendChild(
5062
+ this.button("reset", "Reset chart", () => {
5063
+ this.glider.stop();
5064
+ this.deps.reset();
5065
+ })
5066
+ );
5067
+ }
5068
+ button(iconId, title, onClick, opts = {}) {
5069
+ const b = this.host.ownerDocument.createElement("button");
5070
+ b.type = "button";
5071
+ b.title = title;
5072
+ b.setAttribute("aria-label", title);
5073
+ b.className = opts.selected === true ? "vela-cc-btn vela-cc-on" : "vela-cc-btn";
5074
+ b.innerHTML = icon(iconId);
5075
+ b.addEventListener("click", (e) => {
5076
+ e.stopPropagation();
5077
+ onClick();
5078
+ });
5079
+ return b;
5080
+ }
5081
+ /** The drag handle (2×3 dot grip): press and drag onto another cell to trade
5082
+ * slots with it. The preview highlight follows the pointer; releasing outside
5083
+ * any other cell cancels. */
5084
+ makeGrip() {
5085
+ const b = this.host.ownerDocument.createElement("button");
5086
+ b.type = "button";
5087
+ b.title = "Drag to move chart";
5088
+ b.setAttribute("aria-label", "Drag to move chart");
5089
+ b.className = "vela-cc-btn vela-cc-grip";
5090
+ b.innerHTML = icon("grip");
5091
+ b.addEventListener("pointerdown", (e) => this.onGripDown(b, e));
5092
+ return b;
5093
+ }
5094
+ onGripDown(btn, e) {
5095
+ if (e.button !== 0 && e.pointerType === "mouse") return;
5096
+ e.preventDefault();
5097
+ e.stopPropagation();
5098
+ try {
5099
+ btn.setPointerCapture(e.pointerId);
5100
+ } catch {
5101
+ }
5102
+ this.dragging = true;
5103
+ let target = null;
5104
+ const move = (ev) => {
5105
+ target = this.deps.dragTargetAt(ev.clientX, ev.clientY);
5106
+ this.deps.previewDrop(target);
5107
+ };
5108
+ const finish = (commit) => () => {
5109
+ this.dragging = false;
5110
+ this.deps.previewDrop(null);
5111
+ btn.removeEventListener("pointermove", move);
5112
+ btn.removeEventListener("pointerup", onUp);
5113
+ btn.removeEventListener("pointercancel", onCancel);
5114
+ if (commit && target != null) this.deps.dropOn(target);
5115
+ };
5116
+ const onUp = finish(true);
5117
+ const onCancel = finish(false);
5118
+ btn.addEventListener("pointermove", move);
5119
+ btn.addEventListener("pointerup", onUp);
5120
+ btn.addEventListener("pointercancel", onCancel);
5121
+ }
5122
+ /** Mobile flips the cluster off entirely (and hides it if currently revealed). */
5123
+ setSuspended(on) {
5124
+ this.suspended = on;
5125
+ if (on) this.setNear(false);
5126
+ }
5127
+ /** Live renderer gutters on the cell host (0 when unpublished — a test stub). */
5128
+ hostGutters() {
5129
+ const view = this.host.ownerDocument.defaultView;
5130
+ if (!view) return { toolbar: 0, scale: 0 };
5131
+ const cs = view.getComputedStyle(this.host);
5132
+ return {
5133
+ toolbar: Number.parseFloat(cs.getPropertyValue("--vela-toolbar-gutter")) || 0,
5134
+ scale: Number.parseFloat(cs.getPropertyValue("--vela-scale-gutter")) || 0
5135
+ };
5136
+ }
5137
+ setNear(near) {
5138
+ if (near === this.near) return;
5139
+ this.near = near;
5140
+ this.root.style.display = near ? "flex" : "none";
5141
+ }
5142
+ destroy() {
5143
+ this.glider.stop();
5144
+ this.host.removeEventListener("pointermove", this.onHostMove);
5145
+ this.host.removeEventListener("pointerleave", this.onHostLeave);
5146
+ this.root.remove();
5147
+ }
5148
+ };
5149
+
4820
5150
  // src/widget/history.ts
4821
5151
  var WidgetHistory = class {
4822
5152
  /** `getChart` late-resolves the CURRENT chart: drawing steps recorded before a chart
@@ -4918,6 +5248,12 @@ function cellDrawings(opt) {
4918
5248
  if (opt === true || opt == null) return { toolbar: false };
4919
5249
  return { ...opt, toolbar: false };
4920
5250
  }
5251
+ function instanceDeltas(handle) {
5252
+ if (!handle) return void 0;
5253
+ const inputs = inputDeltas(handle.inputs, handle.inputValues());
5254
+ const props = inputDeltas(handle.props, handle.propValues());
5255
+ return inputs || props ? { ...inputs ? { inputs } : {}, ...props ? { props } : {} } : void 0;
5256
+ }
4921
5257
  var ChartCell = class {
4922
5258
  constructor(id, gridHost, seed, deps) {
4923
5259
  this.id = id;
@@ -5066,11 +5402,18 @@ var ChartCell = class {
5066
5402
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
5067
5403
  });
5068
5404
  this.syncStatuslineColors();
5405
+ this.cellControls = new CellControls(this.host, {
5406
+ chart: () => this.inner,
5407
+ reset: () => this.resetView(),
5408
+ multiCell: () => deps.multiCell(),
5409
+ isMaximized: () => deps.isMaximized(id),
5410
+ toggleMaximize: () => deps.toggleMaximize(id),
5411
+ dragTargetAt: (x, y) => deps.cellDragTarget(id, x, y),
5412
+ previewDrop: (target) => deps.previewDropTarget(target),
5413
+ dropOn: (target) => deps.dropCell(id, target)
5414
+ });
5069
5415
  this.contextMenu = new ChartContextMenu(this.host, {
5070
- resetView: () => {
5071
- this.inner?.renderer.set("autoScale", true);
5072
- this.inner?.setVisibleRangePreset("ALL");
5073
- },
5416
+ resetView: () => this.resetView(),
5074
5417
  timezone: () => this.deps.timezone(),
5075
5418
  setTimezone: (zone) => this.deps.setTimezone(zone),
5076
5419
  // Right-clicking activates the cell first (capture-phase pointerdown), so the
@@ -5086,6 +5429,7 @@ var ChartCell = class {
5086
5429
  this.syncPresentNatives();
5087
5430
  this.refreshNativeCatalog();
5088
5431
  });
5432
+ this.inner.on("indicator:inputs", () => this.deps.onStateDirty());
5089
5433
  this.inner.on("indicator:removed", ({ id: id2 }) => {
5090
5434
  if (this.destroyed) return;
5091
5435
  const idx = this.instances.findIndex((it) => it.handle?.id === id2);
@@ -5094,7 +5438,7 @@ var ChartCell = class {
5094
5438
  this.instances.splice(idx, 1);
5095
5439
  this.history.push({
5096
5440
  undo: () => {
5097
- snapshot.handle = this.addToChart(snapshot.entry);
5441
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
5098
5442
  this.instances.push(snapshot);
5099
5443
  this.deps.onIndicatorsChanged(this.id);
5100
5444
  },
@@ -5444,6 +5788,20 @@ var ChartCell = class {
5444
5788
  this.inner.setVisibleRangePreset(preset.preset);
5445
5789
  }
5446
5790
  }
5791
+ /** Reset this cell's view: re-enable auto scale and frame the full history —
5792
+ * the same action the chart context menu offers. */
5793
+ resetView() {
5794
+ this.inner?.renderer.set("autoScale", true);
5795
+ this.inner?.setVisibleRangePreset("ALL");
5796
+ }
5797
+ /** Rebuild the view-controls cluster (the maximize gate or state changed). */
5798
+ refreshControls() {
5799
+ this.cellControls.refresh();
5800
+ }
5801
+ /** Mobile flips the per-cell cluster off (the shell's mobile bar replaces it). */
5802
+ setControlsSuspended(on) {
5803
+ this.cellControls.setSuspended(on);
5804
+ }
5447
5805
  /** Make this cell the active one and put keyboard focus on its chart surface. */
5448
5806
  focus() {
5449
5807
  this.deps.activate(this.id);
@@ -5469,9 +5827,9 @@ var ChartCell = class {
5469
5827
  this.manifest = list;
5470
5828
  if (this.pendingManifestNames) {
5471
5829
  if (list.length === 0) return;
5472
- for (const name of this.pendingManifestNames) {
5473
- const entry = list.find((e) => e.name === name);
5474
- if (entry) this.addManifestInstance(entry, { record: false });
5830
+ for (const led of this.pendingManifestNames) {
5831
+ const entry = list.find((e) => e.name === ledgerEntryName(led));
5832
+ if (entry) this.addManifestInstance(entry, { record: false, ...typeof led === "object" ? { inputs: led.inputs, props: led.props } : {} });
5475
5833
  }
5476
5834
  this.pendingManifestNames = null;
5477
5835
  return;
@@ -5501,9 +5859,9 @@ var ChartCell = class {
5501
5859
  }
5502
5860
  for (const it of [...this.instances]) this.dropInstance(it);
5503
5861
  if (this.manifest.length > 0) {
5504
- for (const name of led.manifest) {
5505
- const entry = this.manifest.find((e) => e.name === name);
5506
- if (entry) this.addManifestInstance(entry, { record: false });
5862
+ for (const item of led.manifest) {
5863
+ const entry = this.manifest.find((e) => e.name === ledgerEntryName(item));
5864
+ if (entry) this.addManifestInstance(entry, { record: false, ...typeof item === "object" ? { inputs: item.inputs, props: item.props } : {} });
5507
5865
  }
5508
5866
  this.pendingManifestNames = null;
5509
5867
  } else if (!this.deps.manifestSettled()) {
@@ -5552,12 +5910,13 @@ var ChartCell = class {
5552
5910
  * a persistence handler's `restore` runs silently, a user-driven call records.
5553
5911
  */
5554
5912
  addExternalIndicator(entry) {
5555
- this.addManifestInstance({ ...entry, enabled: true }, { external: true });
5913
+ this.addManifestInstance({ ...entry, enabled: true }, { external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {} });
5556
5914
  }
5557
5915
  /** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
5558
5916
  addManifestInstance(entry, opts = {}) {
5559
5917
  if (this.destroyed) return;
5560
- const it = { entry, handle: this.addToChart(entry), ...opts.external ? { external: true } : {} };
5918
+ const values = opts.inputs || opts.props ? { inputs: opts.inputs, props: opts.props } : void 0;
5919
+ const it = { entry, handle: this.addToChart(entry, values), ...opts.external ? { external: true } : {}, ...values ? { values } : {} };
5561
5920
  this.instances.push(it);
5562
5921
  this.deps.onIndicatorsChanged(this.id);
5563
5922
  if (opts.record === false) return;
@@ -5565,7 +5924,7 @@ var ChartCell = class {
5565
5924
  this.history.push({
5566
5925
  undo: () => this.dropInstance(snapshot),
5567
5926
  redo: () => {
5568
- snapshot.handle = this.addToChart(snapshot.entry);
5927
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
5569
5928
  this.instances.push(snapshot);
5570
5929
  this.deps.onIndicatorsChanged(this.id);
5571
5930
  }
@@ -5578,7 +5937,7 @@ var ChartCell = class {
5578
5937
  const snapshot = it;
5579
5938
  this.history.push({
5580
5939
  undo: () => {
5581
- snapshot.handle = this.addToChart(snapshot.entry);
5940
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
5582
5941
  this.instances.push(snapshot);
5583
5942
  this.deps.onIndicatorsChanged(this.id);
5584
5943
  },
@@ -5588,6 +5947,9 @@ var ChartCell = class {
5588
5947
  dropInstance(it) {
5589
5948
  const idx = this.instances.indexOf(it);
5590
5949
  if (idx >= 0) this.instances.splice(idx, 1);
5950
+ const captured = instanceDeltas(it.handle);
5951
+ if (captured) it.values = captured;
5952
+ else delete it.values;
5591
5953
  try {
5592
5954
  it.handle?.remove();
5593
5955
  } catch {
@@ -5630,9 +5992,13 @@ var ChartCell = class {
5630
5992
  this.deps.onIndicatorsChanged(this.id);
5631
5993
  });
5632
5994
  }
5633
- addToChart(entry) {
5995
+ addToChart(entry, values) {
5634
5996
  try {
5635
- return this.inner?.addIndicator(entry.script, entry.language !== void 0 ? { language: entry.language } : void 0) ?? null;
5997
+ return this.inner?.addIndicator(entry.script, {
5998
+ ...entry.language !== void 0 ? { language: entry.language } : {},
5999
+ ...values?.inputs ? { inputs: values.inputs } : {},
6000
+ ...values?.props ? { props: values.props } : {}
6001
+ }) ?? null;
5636
6002
  } catch (err) {
5637
6003
  console.warn(`[vela] indicator "${entry.name}" failed to add:`, err);
5638
6004
  return null;
@@ -5746,7 +6112,10 @@ var ChartCell = class {
5746
6112
  // manifest — their plugin persists them via the `ext` seam instead.
5747
6113
  indicators: indicatorLedger({
5748
6114
  present: this.inner ? this.inner.presentNativeIndicators() : [],
5749
- instanceNames: this.instances.filter((it) => !it.external).map((it) => it.entry.name),
6115
+ instanceEntries: this.instances.filter((it) => !it.external).map((it) => {
6116
+ const d = it.handle ? instanceDeltas(it.handle) : it.values;
6117
+ return d ? { name: it.entry.name, ...d } : it.entry.name;
6118
+ }),
5750
6119
  pendingManifest: this.pendingManifestNames,
5751
6120
  manifestSettled: this.deps.manifestSettled(),
5752
6121
  volumePending: this.volumeMayBePending && this.volumeIntent
@@ -5757,6 +6126,7 @@ var ChartCell = class {
5757
6126
  destroy() {
5758
6127
  this.destroyed = true;
5759
6128
  this.offMarket();
6129
+ this.cellControls.destroy();
5760
6130
  this.contextMenu.destroy();
5761
6131
  this.history.destroy();
5762
6132
  this.marketStatus?.stop();
@@ -6015,8 +6385,8 @@ var SplitterLayer = class {
6015
6385
  };
6016
6386
 
6017
6387
  // src/widget/toast.ts
6018
- var STYLE_ID12 = "vela-widget-toast";
6019
- var CSS12 = `
6388
+ var STYLE_ID13 = "vela-widget-toast";
6389
+ var CSS13 = `
6020
6390
  .vela-toast {
6021
6391
  position: absolute;
6022
6392
  left: 50%;
@@ -6046,7 +6416,7 @@ var CSS12 = `
6046
6416
  var Toast = class {
6047
6417
  constructor(host) {
6048
6418
  this.timer = null;
6049
- injectStyles(STYLE_ID12, CSS12, host.ownerDocument);
6419
+ injectStyles(STYLE_ID13, CSS13, host.ownerDocument);
6050
6420
  this.el = host.ownerDocument.createElement("div");
6051
6421
  this.el.className = "vela-toast";
6052
6422
  host.appendChild(this.el);
@@ -6070,70 +6440,6 @@ var Toast = class {
6070
6440
  }
6071
6441
  };
6072
6442
 
6073
- // src/widget/glide.ts
6074
- var MIN_SPAN_MS = 6e4;
6075
- var FOLLOW = 0.25;
6076
- var ZOOM_IN = 0.8;
6077
- var ZOOM_OUT = 1.25;
6078
- var PAN_FAST = 0.2;
6079
- function zoomTarget(base, factor) {
6080
- const newSpan = Math.max(MIN_SPAN_MS, (base.to - base.from) * factor);
6081
- return { from: base.to - newSpan, to: base.to };
6082
- }
6083
- function followStep(cur, target, follow = FOLLOW, epsFrac = 15e-4) {
6084
- const eps = Math.max(MIN_SPAN_MS, (target.to - target.from) * epsFrac);
6085
- const nf = cur.from + (target.from - cur.from) * follow;
6086
- const nt = cur.to + (target.to - cur.to) * follow;
6087
- const done = Math.abs(target.from - nf) <= eps && Math.abs(target.to - nt) <= eps;
6088
- return { cur: done ? { ...target } : { from: nf, to: nt }, done };
6089
- }
6090
- var Glider = class {
6091
- constructor(chart) {
6092
- this.chart = chart;
6093
- // The glide eases ITS OWN range, never the chart's read-back: getVisibleRange()
6094
- // is data-bounded (whole bars, `to` capped at the newest bar) and the renderer
6095
- // clamps what gets applied — chasing that read-back can never converge on a
6096
- // clamped step, leaving the rAF loop re-applying a stale target forever (and
6097
- // grinding the span down when panning past the newest bar). The internal range
6098
- // converges geometrically, so the glide always terminates; the renderer is free
6099
- // to clamp each applied frame.
6100
- this.cur = null;
6101
- this.target = null;
6102
- this.raf = 0;
6103
- }
6104
- zoom(factor) {
6105
- this.to((base) => zoomTarget(base, factor));
6106
- }
6107
- stop() {
6108
- if (this.raf) cancelAnimationFrame(this.raf);
6109
- this.raf = 0;
6110
- this.cur = null;
6111
- this.target = null;
6112
- }
6113
- to(make) {
6114
- const chart = this.chart();
6115
- const base = this.target ?? chart?.getVisibleRange();
6116
- if (!chart || !base) return;
6117
- this.cur ?? (this.cur = { ...base });
6118
- this.target = make(base);
6119
- if (!this.raf) this.tick();
6120
- }
6121
- tick() {
6122
- this.raf = requestAnimationFrame(() => {
6123
- const chart = this.chart();
6124
- if (!chart || !this.cur || !this.target) {
6125
- this.stop();
6126
- return;
6127
- }
6128
- const { cur: next, done } = followStep(this.cur, this.target);
6129
- this.cur = next;
6130
- chart.setVisibleRange(next);
6131
- if (done) this.stop();
6132
- else this.tick();
6133
- });
6134
- }
6135
- };
6136
-
6137
6443
  // src/widget/tool-shortcuts.ts
6138
6444
  var TOOL_BINDINGS = [
6139
6445
  ["trendline", "drawings.trendline"],
@@ -6205,8 +6511,8 @@ var LayoutModeController = class {
6205
6511
  };
6206
6512
 
6207
6513
  // src/widget/mobile-bar.ts
6208
- var STYLE_ID13 = "vela-widget-mobilebar";
6209
- var CSS13 = `
6514
+ var STYLE_ID14 = "vela-widget-mobilebar";
6515
+ var CSS14 = `
6210
6516
  .vela-mobilebar {
6211
6517
  display: none;
6212
6518
  align-items: stretch;
@@ -6237,6 +6543,9 @@ var CSS13 = `
6237
6543
  }
6238
6544
  .vela-mb-item:active { background: var(--vela-hover); }
6239
6545
  .vela-mb-item .vela-icon { font-size: 18px; width: 18px; height: 18px; }
6546
+ /* A lit stop (the maximize toggle while something is isolated): the inverse
6547
+ "selected" chip \u2014 white on the dark theme, dark on the light one. */
6548
+ .vela-mb-item.vela-mb-on, .vela-mb-item.vela-mb-on:active { background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
6240
6549
  /* Left-aligned contributed actions get their own stops (the built-in indicators
6241
6550
  slot) \u2014 the wrapper is layout-transparent so each stop flexes like a sibling. */
6242
6551
  .vela-mb-actions { display: contents; }
@@ -6256,14 +6565,14 @@ var MobileBar = class {
6256
6565
  constructor(host, opts) {
6257
6566
  this.opts = opts;
6258
6567
  const doc = host.ownerDocument;
6259
- injectStyles(STYLE_ID13, CSS13, doc);
6568
+ injectStyles(STYLE_ID14, CSS14, doc);
6260
6569
  this.el = doc.createElement("div");
6261
6570
  this.el.className = "vela-mobilebar";
6262
- const item = (cls, label, onClick, icon) => {
6571
+ const item = (cls, label, onClick, icon2) => {
6263
6572
  const b = doc.createElement("button");
6264
6573
  b.className = `vela-mb-item ${cls}`;
6265
6574
  b.setAttribute("aria-label", label);
6266
- if (icon) b.appendChild(iconEl(icon, doc));
6575
+ if (icon2) b.appendChild(iconEl(icon2, doc));
6267
6576
  b.addEventListener("click", onClick);
6268
6577
  return b;
6269
6578
  };
@@ -6277,9 +6586,11 @@ var MobileBar = class {
6277
6586
  this.actionsHost.className = "vela-mb-actions";
6278
6587
  const onDrawings = opts.onDrawingsClick;
6279
6588
  const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
6589
+ const onMaximize = opts.onMaximizeClick;
6590
+ this.maxEl = onMaximize ? item("vela-mb-maximize", "Maximize chart", onMaximize, "maximize") : null;
6280
6591
  const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
6281
6592
  const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
6282
- this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], more, settings);
6593
+ this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], ...this.maxEl ? [this.maxEl] : [], more, settings);
6283
6594
  host.appendChild(this.el);
6284
6595
  this.renderActions();
6285
6596
  }
@@ -6311,14 +6622,22 @@ var MobileBar = class {
6311
6622
  setTimeframe(tf) {
6312
6623
  this.tfEl.textContent = timeframeLabel(tf);
6313
6624
  }
6625
+ /** Light the maximize stop while something is isolated (a chart over the grid,
6626
+ * or a maximized pane inside the active chart) — inverse chip + restore glyph. */
6627
+ setMaximizeActive(on) {
6628
+ if (!this.maxEl) return;
6629
+ this.maxEl.classList.toggle("vela-mb-on", on);
6630
+ this.maxEl.setAttribute("aria-label", on ? "Restore layout" : "Maximize chart");
6631
+ this.maxEl.replaceChildren(iconEl(on ? "restore" : "maximize", this.el.ownerDocument));
6632
+ }
6314
6633
  destroy() {
6315
6634
  this.el.remove();
6316
6635
  }
6317
6636
  };
6318
6637
 
6319
6638
  // src/widget/timeframe-drawer.ts
6320
- var STYLE_ID14 = "vela-widget-tf-drawer";
6321
- var CSS14 = `
6639
+ var STYLE_ID15 = "vela-widget-tf-drawer";
6640
+ var CSS15 = `
6322
6641
  .vela-tfd-heading {
6323
6642
  padding: 4px 2px 8px;
6324
6643
  font-size: 12px;
@@ -6374,7 +6693,7 @@ var TimeframeDrawer = class {
6374
6693
  this.rangeChips = /* @__PURE__ */ new Map();
6375
6694
  this.tfChips = /* @__PURE__ */ new Map();
6376
6695
  const doc = opts.host.ownerDocument;
6377
- injectStyles(STYLE_ID14, CSS14, doc);
6696
+ injectStyles(STYLE_ID15, CSS15, doc);
6378
6697
  this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
6379
6698
  const rangeHeading = doc.createElement("div");
6380
6699
  rangeHeading.className = "vela-tfd-heading";
@@ -6432,8 +6751,8 @@ var TimeframeDrawer = class {
6432
6751
  };
6433
6752
 
6434
6753
  // src/widget/drawings-drawer.ts
6435
- var STYLE_ID15 = "vela-widget-drawings-drawer";
6436
- var CSS15 = `
6754
+ var STYLE_ID16 = "vela-widget-drawings-drawer";
6755
+ var CSS16 = `
6437
6756
  /* Search + tabs stay pinned while the tool list scrolls underneath. */
6438
6757
  .vela-dd-sticky {
6439
6758
  position: sticky;
@@ -6535,7 +6854,7 @@ var DrawingsDrawer = class {
6535
6854
  this.definition = { groups: [] };
6536
6855
  this.activeGroup = "";
6537
6856
  const doc = opts.host.ownerDocument;
6538
- injectStyles(STYLE_ID15, CSS15, doc);
6857
+ injectStyles(STYLE_ID16, CSS16, doc);
6539
6858
  this.drawer = new Drawer({
6540
6859
  host: opts.host,
6541
6860
  title: "Drawings",
@@ -6677,8 +6996,8 @@ var DrawingsDrawer = class {
6677
6996
  };
6678
6997
 
6679
6998
  // src/widget/more-drawer.ts
6680
- var STYLE_ID16 = "vela-widget-more-drawer";
6681
- var CSS16 = `
6999
+ var STYLE_ID17 = "vela-widget-more-drawer";
7000
+ var CSS17 = `
6682
7001
  .vela-md-actions {
6683
7002
  display: flex;
6684
7003
  gap: 6px;
@@ -6753,7 +7072,7 @@ var MoreDrawer = class {
6753
7072
  constructor(opts) {
6754
7073
  this.opts = opts;
6755
7074
  this.view = "main";
6756
- injectStyles(STYLE_ID16, CSS16, opts.host.ownerDocument);
7075
+ injectStyles(STYLE_ID17, CSS17, opts.host.ownerDocument);
6757
7076
  this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
6758
7077
  }
6759
7078
  open() {
@@ -6809,11 +7128,11 @@ var MoreDrawer = class {
6809
7128
  renderMain(doc) {
6810
7129
  const actions = doc.createElement("div");
6811
7130
  actions.className = "vela-md-actions";
6812
- const action = (icon, label, enabled, run) => {
7131
+ const action = (icon2, label, enabled, run) => {
6813
7132
  const b = doc.createElement("button");
6814
7133
  b.className = "vela-md-action";
6815
7134
  b.disabled = !enabled;
6816
- b.appendChild(iconEl(icon, doc));
7135
+ b.appendChild(iconEl(icon2, doc));
6817
7136
  b.appendChild(doc.createTextNode(label));
6818
7137
  b.addEventListener("click", () => {
6819
7138
  run();
@@ -6947,8 +7266,8 @@ var MoreDrawer = class {
6947
7266
  };
6948
7267
 
6949
7268
  // src/widget/timezone-drawer.ts
6950
- var STYLE_ID17 = "vela-widget-timezone-drawer";
6951
- var CSS17 = `
7269
+ var STYLE_ID18 = "vela-widget-timezone-drawer";
7270
+ var CSS18 = `
6952
7271
  .vela-tzd-list { padding: 2px 0 4px; }
6953
7272
  .vela-tzd-row {
6954
7273
  display: flex;
@@ -6967,7 +7286,7 @@ var CSS17 = `
6967
7286
  var TimezoneDrawer = class {
6968
7287
  constructor(opts) {
6969
7288
  this.opts = opts;
6970
- injectStyles(STYLE_ID17, CSS17, opts.host.ownerDocument);
7289
+ injectStyles(STYLE_ID18, CSS18, opts.host.ownerDocument);
6971
7290
  this.drawer = new Drawer({ host: opts.host, title: "Time zone", onOpenChange: opts.onOpenChange });
6972
7291
  }
6973
7292
  open() {
@@ -7005,8 +7324,8 @@ var TimezoneDrawer = class {
7005
7324
  };
7006
7325
 
7007
7326
  // src/widget/price-scale-drawer.ts
7008
- var STYLE_ID18 = "vela-widget-price-scale-drawer";
7009
- var CSS18 = `
7327
+ var STYLE_ID19 = "vela-widget-price-scale-drawer";
7328
+ var CSS19 = `
7010
7329
  .vela-psd-list { padding: 2px 0 4px; }
7011
7330
  .vela-psd-row {
7012
7331
  display: flex;
@@ -7035,7 +7354,7 @@ var PriceScaleDrawer = class {
7035
7354
  constructor(opts) {
7036
7355
  this.opts = opts;
7037
7356
  this.pane = null;
7038
- injectStyles(STYLE_ID18, CSS18, opts.host.ownerDocument);
7357
+ injectStyles(STYLE_ID19, CSS19, opts.host.ownerDocument);
7039
7358
  this.drawer = new Drawer({ host: opts.host, title: "Price scale", onOpenChange: opts.onOpenChange });
7040
7359
  }
7041
7360
  open() {
@@ -7171,8 +7490,8 @@ var PriceScaleDrawer = class {
7171
7490
  };
7172
7491
 
7173
7492
  // src/widget/drawing-pill.ts
7174
- var STYLE_ID19 = "vela-widget-drawing-pill";
7175
- var CSS19 = `
7493
+ var STYLE_ID20 = "vela-widget-drawing-pill";
7494
+ var CSS20 = `
7176
7495
  .vela-drawpill { display: none; }
7177
7496
  [data-layout='mobile'] .vela-drawpill {
7178
7497
  position: absolute;
@@ -7232,17 +7551,17 @@ var DrawingPill = class {
7232
7551
  this.chart = null;
7233
7552
  this.chartSubs = [];
7234
7553
  const doc = host.ownerDocument;
7235
- injectStyles(STYLE_ID19, CSS19, doc);
7554
+ injectStyles(STYLE_ID20, CSS20, doc);
7236
7555
  this.el = doc.createElement("div");
7237
7556
  this.el.className = "vela-drawpill";
7238
7557
  this.el.hidden = true;
7239
7558
  this.toolGlyph = doc.createElement("span");
7240
7559
  this.toolGlyph.className = "vela-drawpill-tool";
7241
- const btn = (icon, label, onClick) => {
7560
+ const btn = (icon2, label, onClick) => {
7242
7561
  const b = doc.createElement("button");
7243
7562
  b.className = "vela-drawpill-btn";
7244
7563
  b.setAttribute("aria-label", label);
7245
- b.appendChild(iconEl(icon, doc));
7564
+ b.appendChild(iconEl(icon2, doc));
7246
7565
  b.addEventListener("click", onClick);
7247
7566
  return b;
7248
7567
  };
@@ -7345,10 +7664,10 @@ function buildContext(host) {
7345
7664
  var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
7346
7665
  var GAP_PX = 2;
7347
7666
  var POOL_CAP = 16;
7348
- var TIME_AXIS_H2 = 22;
7667
+ var TIME_AXIS_H3 = 22;
7349
7668
  var ALERT_CAP = 50;
7350
- var STYLE_ID20 = "vela-workspace";
7351
- var CSS20 = `
7669
+ var STYLE_ID21 = "vela-workspace";
7670
+ var CSS21 = `
7352
7671
  .vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
7353
7672
  .vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
7354
7673
  .vela-ws-toolbar { position: relative; flex: none; }
@@ -7375,6 +7694,21 @@ var CSS20 = `
7375
7694
  /* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
7376
7695
  drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
7377
7696
  [data-layout='mobile'] .vela-ws-toolbar { display: none; }
7697
+ /* A maximized cell owns the whole grid: the splitter strips have no seams to grab and
7698
+ the active ring would just outline the only visible chart \u2014 both are noise here. */
7699
+ .vela-ws-grid[data-maximized='1'] .vela-ws-splitter { display: none; }
7700
+ .vela-ws-grid[data-maximized='1'] .vela-cell[data-active='1']::after { display: none; }
7701
+ /* Drop-target preview while a cell's drag handle is held: a dashed ring + the same
7702
+ soft wash the splitter hover uses, over the chart, inert to the pointer. */
7703
+ .vela-cell[data-drop-target='1']::before {
7704
+ content: '';
7705
+ position: absolute;
7706
+ inset: 0;
7707
+ border: 2px dashed var(--vela-fg-bright);
7708
+ background: var(--vela-separator-hover-band);
7709
+ pointer-events: none;
7710
+ z-index: 11;
7711
+ }
7378
7712
  `;
7379
7713
  registerIcon("layout", svg16('<rect x="1.5" y="1.5" width="13" height="13" rx="1.5"/><path d="M8 1.5v13M1.5 8h13"/>'));
7380
7714
  function declaredOrder(cells) {
@@ -7404,6 +7738,9 @@ var VelaWorkspace = class {
7404
7738
  * slots beyond the list get auto identities. Grows, never reorders. */
7405
7739
  this.order = [];
7406
7740
  this.activeId = null;
7741
+ /** The cell maximized over the whole grid (null = normal grid). TRANSIENT view
7742
+ * state — never persisted; any structural change (layout, applyState) restores. */
7743
+ this.maximizedId = null;
7407
7744
  this.cellBackend = "auto";
7408
7745
  this.destroyed = false;
7409
7746
  this.shortcutsHelp = null;
@@ -7508,7 +7845,7 @@ var VelaWorkspace = class {
7508
7845
  this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
7509
7846
  const bootActive = boot?.activeCellId ?? null;
7510
7847
  const doc = hostEl.ownerDocument;
7511
- injectStyles(STYLE_ID20, CSS20, doc);
7848
+ injectStyles(STYLE_ID21, CSS21, doc);
7512
7849
  this.root = doc.createElement("div");
7513
7850
  this.root.className = "vela-workspace";
7514
7851
  ensureUIHost(this.root, resolveTheme(opts.theme));
@@ -7618,7 +7955,11 @@ var VelaWorkspace = class {
7618
7955
  if (attribution !== false) {
7619
7956
  const background = resolveTheme(opts.theme).background;
7620
7957
  const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background);
7621
- Object.assign(mark.style, { left: "12px", bottom: `${TIME_AXIS_H2 + 10}px`, zIndex: "11" });
7958
+ Object.assign(mark.style, {
7959
+ left: "calc(var(--vela-toolbar-gutter, 0px) + 12px)",
7960
+ bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H3}px) + 10px)`,
7961
+ zIndex: "11"
7962
+ });
7622
7963
  this.gridEl.appendChild(mark);
7623
7964
  this.attributionMark = mark;
7624
7965
  }
@@ -7684,6 +8025,9 @@ var VelaWorkspace = class {
7684
8025
  ...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
7685
8026
  getContext: () => this.context(),
7686
8027
  ...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
8028
+ // Multi-chart only: the stop that isolates the ACTIVE chart (the
8029
+ // per-cell hover cluster has no cursor to reveal it on mobile).
8030
+ ...this.monoLayout ? {} : { onMaximizeClick: () => this.toggleMobileMaximize() },
7687
8031
  onMoreClick: () => this.openMoreDrawer(),
7688
8032
  onSettingsClick: () => this.active.chart.renderer.openSettings()
7689
8033
  }) : null;
@@ -7890,7 +8234,9 @@ var VelaWorkspace = class {
7890
8234
  this.pool.clear();
7891
8235
  for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
7892
8236
  this.order = st.charts.map((c) => c.id);
8237
+ this.clearMaximized();
7893
8238
  this.applyGrid();
8239
+ this.refreshCellControls();
7894
8240
  const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
7895
8241
  if (nextActive2 === this.activeId) this.projectActiveCell();
7896
8242
  else this.setActiveCell(nextActive2);
@@ -7916,6 +8262,7 @@ var VelaWorkspace = class {
7916
8262
  const def = this.monoLayout ? null : ensureLayout(st.layout);
7917
8263
  if (def) this.def = def;
7918
8264
  this.cellBackend = this.backendFor(this.def);
8265
+ this.clearMaximized();
7919
8266
  this.applyGrid();
7920
8267
  this.buildCells();
7921
8268
  this.syncCellPresentation();
@@ -7979,6 +8326,7 @@ var VelaWorkspace = class {
7979
8326
  setLayout(layout) {
7980
8327
  if (this.destroyed) return;
7981
8328
  if (this.monoLayout) return;
8329
+ this.clearMaximized();
7982
8330
  const next = this.resolveLayout(layout);
7983
8331
  const nextBackend = this.backendFor(next);
7984
8332
  const rebuildAll = nextBackend !== this.cellBackend;
@@ -7999,6 +8347,7 @@ var VelaWorkspace = class {
7999
8347
  this.buildCells();
8000
8348
  this.alignNewCellStyles(preexisting);
8001
8349
  this.syncCellPresentation();
8350
+ this.refreshCellControls();
8002
8351
  this.topbar.setLayout(next.id);
8003
8352
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
8004
8353
  if (nextActive === this.activeId) this.projectActiveCell();
@@ -8007,6 +8356,67 @@ var VelaWorkspace = class {
8007
8356
  this.events.emit("layout:changed", { layout: next.id });
8008
8357
  this.markStateDirty();
8009
8358
  }
8359
+ /** The identity of the cell maximized over the whole grid, or null. */
8360
+ get maximizedCell() {
8361
+ return this.maximizedId;
8362
+ }
8363
+ /**
8364
+ * Maximize one cell over the whole grid, or restore the layout with `null`. Pure
8365
+ * presentation: the other cells stay alive underneath — charts, subscriptions and
8366
+ * state untouched — so restoring is instant. The maximized cell becomes the active
8367
+ * one. Transient view state (also reachable from each cell's bottom-center view
8368
+ * cluster): switching layouts or applying a state document restores the grid.
8369
+ */
8370
+ maximizeCell(id) {
8371
+ if (this.destroyed) return;
8372
+ if (id != null && (!this.cellsById.has(id) || this.def.cells.length <= 1)) return;
8373
+ if (id === this.maximizedId) return;
8374
+ this.maximizedId = id;
8375
+ if (id) this.setActiveCell(id);
8376
+ this.applyGrid();
8377
+ this.refreshCellControls();
8378
+ this.syncMobileMaximize();
8379
+ this.events.emit("cell:maximized", { id });
8380
+ }
8381
+ /** The mobile bar's maximize stop: one press isolates the ACTIVE chart over the
8382
+ * grid; while something is already isolated — the chart, or a pane inside it
8383
+ * (mobile's double-tap) — the press restores that instead. Every branch re-syncs
8384
+ * the stop on its own (`maximizeCell` directly, `panes.maximize` via its
8385
+ * synchronous `pane:changed`). */
8386
+ toggleMobileMaximize() {
8387
+ const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
8388
+ if (!cell) return;
8389
+ if (this.maximizedId) this.maximizeCell(null);
8390
+ else if (cell.chart.panes.list().some((p) => p.maximized)) cell.chart.panes.maximize(null);
8391
+ else this.maximizeCell(cell.id);
8392
+ }
8393
+ /** Keep the mobile bar's maximize stop truthful: lit (inverse chip, restore
8394
+ * glyph) while the active chart covers the grid OR one of its panes is
8395
+ * maximized — the state a double-tap toggles is otherwise invisible on mobile. */
8396
+ syncMobileMaximize() {
8397
+ if (!this.mobileBar) return;
8398
+ const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
8399
+ const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
8400
+ this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
8401
+ }
8402
+ /**
8403
+ * Trade the SLOTS of two live cells — the grid arrangement changes, the cells
8404
+ * themselves (charts, indicators, drawings, the active flag) stay untouched.
8405
+ * What each cell's drag handle commits; also callable directly by hosts.
8406
+ */
8407
+ swapCells(a, b) {
8408
+ if (this.destroyed || a === b) return;
8409
+ const i = this.order.indexOf(a);
8410
+ const j = this.order.indexOf(b);
8411
+ if (i < 0 || j < 0 || !this.cellsById.has(a) || !this.cellsById.has(b)) return;
8412
+ [this.order[i], this.order[j]] = [this.order[j], this.order[i]];
8413
+ for (const [k] of this.def.cells.entries()) {
8414
+ const host = this.cellsById.get(this.order[k] ?? "")?.host;
8415
+ if (host) this.gridEl.appendChild(host);
8416
+ }
8417
+ this.applyGrid();
8418
+ this.markStateDirty();
8419
+ }
8010
8420
  resize() {
8011
8421
  this.splitters.layout();
8012
8422
  }
@@ -8076,6 +8486,7 @@ var VelaWorkspace = class {
8076
8486
  this.mobileBar?.renderActions();
8077
8487
  this.mobileBar?.setSymbol(cell.symbol);
8078
8488
  this.mobileBar?.setTimeframe(cell.timeframe);
8489
+ this.syncMobileMaximize();
8079
8490
  this.drawingPill?.onChart(cell.chart);
8080
8491
  const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
8081
8492
  this.historyUnsub?.();
@@ -8165,8 +8576,78 @@ var VelaWorkspace = class {
8165
8576
  const host = this.cellsById.get(this.order[i] ?? "")?.host;
8166
8577
  if (host) host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
8167
8578
  }
8579
+ this.applyMaximizePresentation();
8580
+ this.mountAttributionMark();
8168
8581
  this.splitters.layout();
8169
8582
  }
8583
+ /** Overlay the maximize presentation on the freshly applied grid: EVERY cell spans
8584
+ * the full track grid — the maximized one on top, the siblings invisible beneath
8585
+ * it (their charts stay alive — restoring is instant). The siblings must span too:
8586
+ * left in their slots they would auto-flow into implicit zero-height rows, whose
8587
+ * gaps steal height from the maximized cell and collapse their renderers to 0.
8588
+ * The splitter strips and the active ring hide via the `data-maximized` rules. */
8589
+ applyMaximizePresentation() {
8590
+ const maxId = this.maximizedId;
8591
+ if (maxId) this.gridEl.dataset.maximized = "1";
8592
+ else delete this.gridEl.dataset.maximized;
8593
+ for (const [id, cell] of this.cellsById) {
8594
+ const style = cell.host.style;
8595
+ if (maxId) style.gridArea = "1 / 1 / -1 / -1";
8596
+ style.zIndex = maxId && id === maxId ? "5" : "";
8597
+ style.visibility = maxId && id !== maxId ? "hidden" : "";
8598
+ }
8599
+ }
8600
+ /** Rebuild every cell's view cluster (the maximize gate or state changed). */
8601
+ refreshCellControls() {
8602
+ for (const cell of this.cellsById.values()) cell.refreshControls();
8603
+ }
8604
+ /** Drop the transient maximize on a structural change (layout switch, state
8605
+ * document) — WITH the event, so hosts tracking `cell:maximized` never drift
8606
+ * from `maximizedCell`. The caller's own grid re-apply paints the restore. */
8607
+ clearMaximized() {
8608
+ if (this.maximizedId == null) return;
8609
+ this.maximizedId = null;
8610
+ this.events.emit("cell:maximized", { id: null });
8611
+ }
8612
+ /** The live cell under a viewport point, excluding `excludeId` and any host a
8613
+ * maximize has hidden — the drag handle's hit-test. */
8614
+ cellAtPoint(x, y, excludeId) {
8615
+ for (const [id, cell] of this.cellsById) {
8616
+ if (id === excludeId || cell.host.style.visibility === "hidden") continue;
8617
+ const r = cell.host.getBoundingClientRect();
8618
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return id;
8619
+ }
8620
+ return null;
8621
+ }
8622
+ /** Mark one cell as the live drop target of a grip drag (null clears all) —
8623
+ * the `data-drop-target` stylesheet rule paints the dashed preview ring. */
8624
+ setDropTarget(id) {
8625
+ for (const [cid, cell] of this.cellsById) {
8626
+ if (cid === id) cell.host.dataset.dropTarget = "1";
8627
+ else delete cell.host.dataset.dropTarget;
8628
+ }
8629
+ }
8630
+ /** The cell whose bottom-left corner the grid's attribution mark floats in — the
8631
+ * maximized cell while one covers the grid, else the bottom-left slot's cell. */
8632
+ bottomLeftCell() {
8633
+ if (this.maximizedId) return this.cellsById.get(this.maximizedId);
8634
+ const grid = occupancyGrid(this.def);
8635
+ const slot = grid[grid.length - 1]?.[0];
8636
+ const idx = this.def.cells.findIndex((c) => (c.area ?? c.id) === slot);
8637
+ return this.cellsById.get(this.order[idx >= 0 ? idx : 0] ?? "");
8638
+ }
8639
+ /** Keep the shared attribution mark inside the BOTTOM-LEFT visible cell: its
8640
+ * offsets ride that cell's renderer-published `--vela-bottom-gutter` /
8641
+ * `--vela-toolbar-gutter`, so collapsed pane strips push the mark up without any
8642
+ * bookkeeping here. Re-run after anything that changes which host that is
8643
+ * (layout switch, maximize, cell rebuild); a destroyed host drops the mark from
8644
+ * the DOM, and this re-mount brings it back. */
8645
+ mountAttributionMark() {
8646
+ const mark = this.attributionMark;
8647
+ if (!mark) return;
8648
+ const host = this.bottomLeftCell()?.host ?? this.gridEl;
8649
+ if (mark.parentElement !== host) host.appendChild(mark);
8650
+ }
8170
8651
  /** Create the cells the current layout wants but don't exist yet (pool-first).
8171
8652
  * A slot's CELL IDENTITY is `order[i]` (declaration order — never the slot's own
8172
8653
  * positional id); slots past the declared list mint an auto identity once. */
@@ -8198,6 +8679,12 @@ var VelaWorkspace = class {
8198
8679
  setTimezone: (zone) => this.setTimezone(zone),
8199
8680
  context: () => this.context(),
8200
8681
  activate: (id2) => this.setActiveCell(id2),
8682
+ multiCell: () => !this.monoLayout && this.def.cells.length > 1,
8683
+ isMaximized: (id2) => this.maximizedId === id2,
8684
+ toggleMaximize: (id2) => this.maximizeCell(this.maximizedId === id2 ? null : id2),
8685
+ cellDragTarget: (id2, x, y) => this.cellAtPoint(x, y, id2),
8686
+ previewDropTarget: (target) => this.setDropTarget(target),
8687
+ dropCell: (id2, target) => this.swapCells(id2, target),
8201
8688
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
8202
8689
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
8203
8690
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
@@ -8211,6 +8698,7 @@ var VelaWorkspace = class {
8211
8698
  if (id === this.activeId) cell.host.dataset.active = "1";
8212
8699
  this.wireCell(cell);
8213
8700
  cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
8701
+ cell.setControlsSuspended(this.layoutCtl.current === "mobile");
8214
8702
  if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
8215
8703
  cell.setManifest(this.manifest, pooled?.indicators == null);
8216
8704
  cell.restorePersistedExt();
@@ -8220,6 +8708,7 @@ var VelaWorkspace = class {
8220
8708
  const host = this.cellsById.get(this.order[i] ?? "")?.host;
8221
8709
  if (host) this.gridEl.appendChild(host);
8222
8710
  }
8711
+ this.mountAttributionMark();
8223
8712
  }
8224
8713
  /** Per-cell chart subscriptions (trigger ② — the chart instance is stable for the
8225
8714
  * cell's whole life, so these live and die with the cell). */
@@ -8279,6 +8768,9 @@ var VelaWorkspace = class {
8279
8768
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
8280
8769
  chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
8281
8770
  chart.on("theme:changed", (t) => this.setTheme(t));
8771
+ chart.on("pane:changed", () => {
8772
+ if (cell.id === this.activeId) this.syncMobileMaximize();
8773
+ });
8282
8774
  chart.renderer.onAxisLongPress((e) => {
8283
8775
  if (this.layoutCtl.current !== "mobile") return;
8284
8776
  if (e.axis === "time") this.openTimezoneDrawer();
@@ -8599,6 +9091,7 @@ var VelaWorkspace = class {
8599
9091
  for (const cell of this.cellsById.values()) {
8600
9092
  cell.chart.renderer.closeDialogs();
8601
9093
  cell.chart.renderer.setLayoutMode(mode);
9094
+ cell.setControlsSuspended(mode === "mobile");
8602
9095
  }
8603
9096
  this.syncCellPresentation();
8604
9097
  }