@luxalgo/vela 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist/{DataProvider-DwFIoV4r.d.ts → DataProvider-oH78tYXL.d.ts} +1 -1
  3. package/dist/{DataProvider-BrlxntPN.d.cts → DataProvider-wbtJ3-Sa.d.cts} +1 -1
  4. package/dist/{chunk-DBDXKZUF.js → chunk-2IPXYM4B.js} +306 -177
  5. package/dist/{chunk-NELQJCGK.js → chunk-G7B7ZCBF.js} +167 -10
  6. package/dist/{chunk-ATGTCWHJ.js → chunk-HMHA733L.js} +473 -196
  7. package/dist/{chunk-EZ5FWVLA.js → chunk-KWI3YDBW.js} +343 -17
  8. package/dist/{chunk-LMBYEGUR.js → chunk-TO3GIK7Z.js} +71 -6
  9. package/dist/{chunk-BZQM2XO7.js → chunk-VHGACEHO.js} +62 -1
  10. package/dist/{contributions-tjNExo1o.d.ts → contributions-BhOBRenr.d.cts} +79 -12
  11. package/dist/{contributions-CtlptxR8.d.cts → contributions-HGqnNb_P.d.ts} +79 -12
  12. package/dist/index.cjs +733 -113
  13. package/dist/index.d.cts +34 -14
  14. package/dist/index.d.ts +34 -14
  15. package/dist/index.js +5 -5
  16. package/dist/{options-BnEPUuVV.d.ts → options-lMUwmRwh.d.cts} +69 -4
  17. package/dist/{options-BnEPUuVV.d.cts → options-lMUwmRwh.d.ts} +69 -4
  18. package/dist/{plugin-e2WUEtiI.d.ts → plugin-Cr2Pj8q8.d.ts} +4 -4
  19. package/dist/{plugin-BWxJRKzS.d.cts → plugin-Do5J5xuF.d.cts} +4 -4
  20. package/dist/plugin.cjs +38 -6
  21. package/dist/plugin.d.cts +5 -5
  22. package/dist/plugin.d.ts +5 -5
  23. package/dist/plugin.js +3 -3
  24. package/dist/providers/binance.d.cts +2 -2
  25. package/dist/providers/binance.d.ts +2 -2
  26. package/dist/providers/coinbase.d.cts +2 -2
  27. package/dist/providers/coinbase.d.ts +2 -2
  28. package/dist/providers/hyperliquid.d.cts +2 -2
  29. package/dist/providers/hyperliquid.d.ts +2 -2
  30. package/dist/{side-panel-HF0IAzwf.d.cts → side-panel-_2UK1gSm.d.cts} +58 -3
  31. package/dist/{side-panel-HF0IAzwf.d.ts → side-panel-_2UK1gSm.d.ts} +58 -3
  32. package/dist/{statusline-model-DFY7tuHj.d.cts → statusline-model-B-2xGlB_.d.cts} +3 -3
  33. package/dist/{statusline-model-zKXtGIjj.d.ts → statusline-model-CY1wzxHg.d.ts} +3 -3
  34. package/dist/ui.cjs +298 -13
  35. package/dist/ui.d.cts +132 -74
  36. package/dist/ui.d.ts +132 -74
  37. package/dist/ui.js +3 -3
  38. package/dist/vela.global.js +734 -114
  39. package/dist/vela.global.min.js +86 -52
  40. package/dist/widget.cjs +1227 -211
  41. package/dist/widget.d.cts +36 -12
  42. package/dist/widget.d.ts +36 -12
  43. package/dist/widget.js +7 -7
  44. package/dist/workspace.cjs +1226 -211
  45. package/dist/workspace.d.cts +47 -20
  46. package/dist/workspace.d.ts +47 -20
  47. package/dist/workspace.js +6 -6
  48. package/package.json +2 -2
  49. /package/dist/{chunk-JU4CWEBF.js → chunk-W5YPYZ2P.js} +0 -0
package/dist/widget.cjs CHANGED
@@ -1256,6 +1256,7 @@ registerIcon("expand", S('<rect x="2.2" y="2.2" width="11.6" height="11.6" rx="1
1256
1256
  registerIcon("plus", S('<path d="M8 2.8v10.4M2.8 8h10.4"/>'));
1257
1257
  registerIcon("minus", S('<path d="M2.8 8h10.4"/>'));
1258
1258
  registerIcon("maximize", S('<path d="M2.5 6V3a.5.5 0 0 1 .5-.5h3M10 2.5h3a.5.5 0 0 1 .5.5v3M13.5 10v3a.5.5 0 0 1-.5.5h-3M6 13.5H3a.5.5 0 0 1-.5-.5v-3"/>'));
1259
+ registerIcon("restore", S('<path d="M6 2.5v3a.5.5 0 0 1-.5.5h-3M13.5 6h-3a.5.5 0 0 1-.5-.5v-3M10 13.5v-3a.5.5 0 0 1 .5-.5h3M2.5 10h3a.5.5 0 0 1 .5.5v3"/>'));
1259
1260
  registerIcon("restore", S('<path d="M6.2 2.5v3.7H2.5M9.8 13.5V9.8h3.7M13.5 6.2H9.8V2.5M2.5 9.8h3.7v3.7"/>'));
1260
1261
  registerIcon("star", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l-3.5 1.85.65-3.9-2.8-2.75 3.9-.55z"/>'));
1261
1262
  registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l-3.5 1.85.65-3.9-2.8-2.75 3.9-.55z"/>', 'fill="currentColor"'));
@@ -1337,6 +1338,17 @@ function nextUid(prefix) {
1337
1338
  return `${prefix}-${++uid}`;
1338
1339
  }
1339
1340
 
1341
+ // src/ui/surface-events.ts
1342
+ var SURFACE_OPEN_EVENT = "vela:surface-open";
1343
+ var SURFACE_CLOSE_EVENT = "vela:surface-close";
1344
+ function announceSurface(el, open2, kind, trigger = null) {
1345
+ const View = el.ownerDocument.defaultView;
1346
+ if (!View) return;
1347
+ el.dispatchEvent(
1348
+ new View.CustomEvent(open2 ? SURFACE_OPEN_EVENT : SURFACE_CLOSE_EVENT, { bubbles: true, composed: true, detail: { kind, trigger } })
1349
+ );
1350
+ }
1351
+
1340
1352
  // src/ui/keymap.ts
1341
1353
  var KEY_ALIASES = {
1342
1354
  esc: "escape",
@@ -1553,6 +1565,55 @@ var Tooltip = class {
1553
1565
  this.trigger.removeAttribute("data-scope");
1554
1566
  }
1555
1567
  };
1568
+
1569
+ // src/ui/surface-exit.ts
1570
+ var CLOSING_ATTR = "data-closing";
1571
+ var SURFACE_EXIT_CAP_MS = 1e3;
1572
+ function animationsOf(els) {
1573
+ return els.flatMap((el) => el.getAnimations({ subtree: true }));
1574
+ }
1575
+ function ends(a) {
1576
+ return Number.isFinite(a.effect?.getComputedTiming().endTime ?? Number.POSITIVE_INFINITY);
1577
+ }
1578
+ function holdForExit(els, done, opts = {}) {
1579
+ const list = Array.isArray(els) ? els : [els];
1580
+ if (list.length === 0 || !list.every((el) => typeof el.getAnimations === "function")) {
1581
+ done();
1582
+ return null;
1583
+ }
1584
+ const before = new Set(animationsOf(list));
1585
+ for (const el of list) el.setAttribute(CLOSING_ATTR, "");
1586
+ const exit = animationsOf(list).filter((a) => !before.has(a) && ends(a));
1587
+ if (exit.length === 0) {
1588
+ for (const el of list) el.removeAttribute(CLOSING_ATTR);
1589
+ done();
1590
+ return null;
1591
+ }
1592
+ const frozen = [...list, ...opts.inert ?? []];
1593
+ const wasInert = frozen.map((el) => el.inert);
1594
+ for (const el of frozen) el.inert = true;
1595
+ let settled = false;
1596
+ const settle = (complete) => {
1597
+ if (settled) return;
1598
+ settled = true;
1599
+ clearTimeout(cap);
1600
+ for (const el of list) el.removeAttribute(CLOSING_ATTR);
1601
+ frozen.forEach((el, i) => {
1602
+ el.inert = wasInert[i];
1603
+ });
1604
+ if (complete) done();
1605
+ };
1606
+ const cap = setTimeout(() => settle(true), SURFACE_EXIT_CAP_MS);
1607
+ let pending = exit.length;
1608
+ const one = () => {
1609
+ if (--pending === 0) settle(true);
1610
+ };
1611
+ for (const a of exit) a.finished.then(one, one);
1612
+ return {
1613
+ cancel: () => settle(false),
1614
+ finish: () => settle(true)
1615
+ };
1616
+ }
1556
1617
  function menuController(opts) {
1557
1618
  return {
1558
1619
  machine: menu__namespace.machine,
@@ -1634,6 +1695,35 @@ var MENU_CSS = `
1634
1695
  color: var(--vela-fg-bright);
1635
1696
  }
1636
1697
  .vela-menu-item[data-checkmark] { color: var(--vela-fg-bright); }
1698
+ /* Badge mode (iconBadges): the row icon sits in a rounded tile and the row grows to fit
1699
+ it. An icon-less row of the same level keeps an invisible slot, so labels align. */
1700
+ .vela-menu[data-badges] > .vela-menu-item { padding: 5px 10px 5px 6px; }
1701
+ .vela-menu-item .vela-menu-badge {
1702
+ width: 24px;
1703
+ height: 24px;
1704
+ padding: 3px;
1705
+ flex: none;
1706
+ box-sizing: border-box;
1707
+ display: inline-flex;
1708
+ align-items: center;
1709
+ justify-content: center;
1710
+ border-radius: var(--vela-radius-md);
1711
+ background: var(--vela-hover);
1712
+ border: 1px solid var(--vela-border);
1713
+ color: var(--vela-fg-muted);
1714
+ transition: background var(--vela-dur-fast) var(--vela-ease), border-color var(--vela-dur-fast) var(--vela-ease), color var(--vela-dur-fast) var(--vela-ease);
1715
+ }
1716
+ .vela-menu-item .vela-menu-badge:empty { background: transparent; border-color: transparent; }
1717
+ .vela-menu-item .vela-menu-badge .vela-icon { color: inherit; }
1718
+ .vela-menu-item[data-highlighted] .vela-menu-badge:not(:empty) { color: var(--vela-fg-bright); }
1719
+ /* The selected tile inverts: a bright fill under the surface-colored glyph. After the
1720
+ hover rule, so a selected row keeps its tile while highlighted. */
1721
+ .vela-menu-item[data-checked] .vela-menu-badge:not(:empty),
1722
+ .vela-menu-item[data-checkmark] .vela-menu-badge:not(:empty) {
1723
+ background: color-mix(in srgb, var(--vela-fg-bright) 83%, var(--vela-surface));
1724
+ border-color: color-mix(in srgb, var(--vela-fg-bright) 40%, transparent);
1725
+ color: var(--vela-surface);
1726
+ }
1637
1727
  /* Switch rows (boolean settings in a dropdown): a right-aligned toggle pill \u2014 the
1638
1728
  same control language as the settings dialog's toggles. */
1639
1729
  .vela-menu-switch {
@@ -1698,11 +1788,16 @@ var Surface = class _Surface {
1698
1788
  /** Trigger rect captured when this level opened — the list stays put if the
1699
1789
  * trigger then moves (favorite chips shifting the caret). */
1700
1790
  this.pinnedAnchor = null;
1791
+ /** The open state last announced — see {@link announce}. */
1792
+ this.shown = false;
1793
+ /** The list's exit animation after a close; a reopen cancels it. */
1794
+ this.exit = null;
1701
1795
  this.doc = doc;
1702
1796
  this.host = opts.host;
1703
1797
  this.onSelect = opts.onSelect;
1704
1798
  this.onFavorite = opts.onFavorite;
1705
1799
  this.checkmarks = opts.checkmarks === true;
1800
+ this.iconBadges = opts.iconBadges === true;
1706
1801
  this.positioner = doc.createElement("div");
1707
1802
  this.positioner.className = "vela-ui-layer";
1708
1803
  this.list = doc.createElement("ul");
@@ -1711,6 +1806,7 @@ var Surface = class _Surface {
1711
1806
  this.positioner.appendChild(this.list);
1712
1807
  this.host.appendChild(this.positioner);
1713
1808
  const trigger = opts.trigger;
1809
+ this.opener = trigger ?? opts.opener ?? null;
1714
1810
  this.ctrl = menuController({
1715
1811
  items: [],
1716
1812
  id: opts.id,
@@ -1732,15 +1828,37 @@ var Surface = class _Surface {
1732
1828
  if (opts.triggerId && trigger) trigger.id = opts.triggerId;
1733
1829
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
1734
1830
  const api = this.ctrl.connect(service);
1831
+ const closing = this.shown && !api.open;
1832
+ if (closing) this.announce(false);
1833
+ if (api.open && this.exit) {
1834
+ this.exit.cancel();
1835
+ this.exit = null;
1836
+ }
1735
1837
  if (trigger) vanilla.spreadProps(trigger, api.getTriggerProps(), this.mid);
1736
1838
  vanilla.spreadProps(this.positioner, api.getPositionerProps(), this.mid);
1737
1839
  vanilla.spreadProps(this.list, api.getContentProps(), this.mid);
1738
1840
  this.project(api);
1841
+ if (closing) this.holdExit();
1842
+ if (api.open && !this.shown) this.announce(true);
1843
+ });
1844
+ }
1845
+ /** Keep the just-closed list up through its exit animation. `spreadProps` re-applies
1846
+ * `hidden` only when the machine's value for it changes, so the list stays shown until
1847
+ * the exit ends — or until a reopen, which projects `hidden: false` itself. */
1848
+ holdExit() {
1849
+ this.list.hidden = false;
1850
+ this.exit = holdForExit(this.list, () => {
1851
+ this.exit = null;
1852
+ if (!this.shown) this.list.hidden = true;
1739
1853
  });
1740
1854
  }
1741
1855
  get api() {
1742
1856
  return this.ctrl.connect(this.handle.service);
1743
1857
  }
1858
+ announce(open2) {
1859
+ this.shown = open2;
1860
+ announceSurface(this.list, open2, "menu", this.opener);
1861
+ }
1744
1862
  setItems(items) {
1745
1863
  this.items = items;
1746
1864
  for (const sub2 of this.subs.values()) sub2.destroy();
@@ -1757,7 +1875,9 @@ var Surface = class _Surface {
1757
1875
  destroy() {
1758
1876
  for (const sub2 of this.subs.values()) sub2.destroy();
1759
1877
  this.subs.clear();
1878
+ this.exit?.finish();
1760
1879
  this.handle.stop();
1880
+ if (this.shown) this.announce(false);
1761
1881
  this.positioner.remove();
1762
1882
  }
1763
1883
  byId(id) {
@@ -1782,6 +1902,10 @@ var Surface = class _Surface {
1782
1902
  const doc = this.doc;
1783
1903
  this.list.replaceChildren();
1784
1904
  const markable = this.checkmarks && this.items.some((i) => !(i.submenu && i.submenu.length > 0) && !i.toggle && i.checked !== void 0);
1905
+ const iconic = this.items.some((i) => !!i.icon);
1906
+ const badged = this.iconBadges && iconic;
1907
+ if (badged) this.list.dataset.badges = "1";
1908
+ else delete this.list.dataset.badges;
1785
1909
  for (const item of this.items) {
1786
1910
  if (item.separatorBefore) {
1787
1911
  const sep = doc.createElement("li");
@@ -1807,7 +1931,16 @@ var Surface = class _Surface {
1807
1931
  } else if (!branch && !item.toggle && item.checked) {
1808
1932
  li.dataset.checked = "1";
1809
1933
  }
1810
- if (item.icon) li.appendChild(iconEl(item.icon, doc));
1934
+ if (badged) {
1935
+ const badge = doc.createElement("span");
1936
+ badge.className = "vela-menu-badge";
1937
+ if (item.icon) badge.appendChild(iconEl(item.icon, doc));
1938
+ li.appendChild(badge);
1939
+ } else if (item.icon) {
1940
+ li.appendChild(iconEl(item.icon, doc));
1941
+ } else if (iconic) {
1942
+ li.appendChild(iconEl("", doc));
1943
+ }
1811
1944
  const label = doc.createElement("span");
1812
1945
  label.className = "vela-menu-label";
1813
1946
  label.textContent = item.label;
@@ -1851,9 +1984,11 @@ var Surface = class _Surface {
1851
1984
  const sub2 = new _Surface(doc, {
1852
1985
  host: this.host,
1853
1986
  placement: "right-start",
1987
+ opener: li,
1854
1988
  onSelect: this.onSelect,
1855
1989
  onFavorite: this.onFavorite,
1856
1990
  checkmarks: this.checkmarks,
1991
+ iconBadges: this.iconBadges,
1857
1992
  id: `${this.mid}--${item.id}`
1858
1993
  });
1859
1994
  sub2.setItems(item.submenu ?? []);
@@ -1879,7 +2014,8 @@ var Menu = class {
1879
2014
  id: opts.id,
1880
2015
  minWidth: opts.minWidth,
1881
2016
  onFavorite: opts.onFavorite,
1882
- checkmarks: opts.checkmarks
2017
+ checkmarks: opts.checkmarks,
2018
+ iconBadges: opts.iconBadges
1883
2019
  });
1884
2020
  this.root.setItems(opts.items);
1885
2021
  }
@@ -1983,6 +2119,7 @@ var POPOVER_CSS = `
1983
2119
 
1984
2120
  // src/ui/components/popover/view.ts
1985
2121
  var open = null;
2122
+ var leaving = /* @__PURE__ */ new Set();
1986
2123
  function closeOpenPopovers() {
1987
2124
  open?.hide();
1988
2125
  }
@@ -2010,6 +2147,8 @@ var Popover = class {
2010
2147
  this.shown = false;
2011
2148
  /** The pending removal of a fading-out shell; a show() that reuses the shell cancels it. */
2012
2149
  this.leaveTimer = null;
2150
+ /** The shell's exit animation before it leaves the DOM; a show() that reuses the shell cancels it. */
2151
+ this.exit = null;
2013
2152
  const doc = opts.trigger.ownerDocument;
2014
2153
  injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
2015
2154
  this.trigger = opts.trigger;
@@ -2046,6 +2185,10 @@ var Popover = class {
2046
2185
  clearTimeout(this.leaveTimer);
2047
2186
  this.leaveTimer = null;
2048
2187
  }
2188
+ this.exit?.cancel();
2189
+ this.exit = null;
2190
+ leaving.delete(this);
2191
+ for (const p of [...leaving]) if (p.trigger === this.trigger) p.exit?.finish();
2049
2192
  ensureUIHost(this.el, this.inheritsTokens ? void 0 : this.theme);
2050
2193
  if (this.fadeMs > 0) {
2051
2194
  this.el.style.transition = `opacity ${this.fadeMs}ms ease`;
@@ -2070,16 +2213,21 @@ var Popover = class {
2070
2213
  this.hide();
2071
2214
  };
2072
2215
  const onReflow = () => this.place();
2073
- setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
2216
+ setTimeout(() => {
2217
+ if (this.onOutside === onOutside) document.addEventListener("pointerdown", onOutside, true);
2218
+ }, 0);
2074
2219
  document.addEventListener("keydown", onKey, true);
2075
2220
  window.addEventListener("resize", onReflow, true);
2076
2221
  document.addEventListener("scroll", onReflow, true);
2077
2222
  this.onOutside = onOutside;
2078
2223
  this.onKey = onKey;
2079
2224
  this.onReflow = onReflow;
2225
+ announceSurface(this.el, true, "popover", this.trigger);
2080
2226
  }
2081
2227
  hide() {
2082
2228
  if (!this.shown) return;
2229
+ this.shown = false;
2230
+ announceSurface(this.el, false, "popover", this.trigger);
2083
2231
  if (this.onOutside) document.removeEventListener("pointerdown", this.onOutside, true);
2084
2232
  if (this.onKey) document.removeEventListener("keydown", this.onKey, true);
2085
2233
  if (this.onReflow) {
@@ -2097,9 +2245,13 @@ var Popover = class {
2097
2245
  this.el.remove();
2098
2246
  }, this.fadeMs);
2099
2247
  } else {
2100
- this.el.remove();
2248
+ this.exit = holdForExit(this.el, () => {
2249
+ this.exit = null;
2250
+ leaving.delete(this);
2251
+ this.el.remove();
2252
+ });
2253
+ if (this.exit) leaving.add(this);
2101
2254
  }
2102
- this.shown = false;
2103
2255
  if (open === this) open = null;
2104
2256
  this.ctrl.onClose?.();
2105
2257
  }
@@ -2110,6 +2262,7 @@ var Popover = class {
2110
2262
  }
2111
2263
  destroy() {
2112
2264
  this.hide();
2265
+ this.exit?.finish();
2113
2266
  }
2114
2267
  reposition() {
2115
2268
  if (this.shown) this.place();
@@ -2305,6 +2458,11 @@ ${overlayScrollbarCss(".vela-dialog *")}
2305
2458
  `;
2306
2459
  var Dialog = class {
2307
2460
  constructor(opts = {}) {
2461
+ this.opener = null;
2462
+ this.wasOpen = false;
2463
+ /** The panel and scrim's exit animation after a close; a reopen cancels it. */
2464
+ this.exit = null;
2465
+ this.destroyed = false;
2308
2466
  const doc = (opts.host ?? document.body).ownerDocument;
2309
2467
  injectStyles(DIALOG_STYLE_ID, DIALOG_CSS, doc);
2310
2468
  const host = opts.host ?? doc.body;
@@ -2401,19 +2559,73 @@ var Dialog = class {
2401
2559
  const mid = String(this.ctrl.props.id);
2402
2560
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
2403
2561
  const api = this.ctrl.connect(service);
2562
+ const closing = this.wasOpen && !api.open;
2563
+ if (closing) this.announce(false);
2404
2564
  vanilla.spreadProps(this.backdrop, api.getBackdropProps(), mid);
2405
2565
  vanilla.spreadProps(this.positioner, api.getPositionerProps(), mid);
2406
2566
  vanilla.spreadProps(this.panel, api.getContentProps(), mid);
2407
2567
  vanilla.spreadProps(title, api.getTitleProps(), mid);
2408
2568
  vanilla.spreadProps(close, api.getCloseTriggerProps(), mid);
2409
- this.backdrop.style.display = api.open ? "" : "none";
2410
- this.positioner.style.display = api.open ? "" : "none";
2411
- });
2569
+ if (api.open) {
2570
+ this.exit?.cancel();
2571
+ this.exit = null;
2572
+ this.setShown(true);
2573
+ } else if (closing) {
2574
+ this.holdExit();
2575
+ } else if (!this.exit) {
2576
+ this.setShown(false);
2577
+ }
2578
+ if (api.open && !this.wasOpen) this.announce(true);
2579
+ if (this.wasOpen && !api.open) this.restoreOpener();
2580
+ this.wasOpen = api.open;
2581
+ });
2582
+ }
2583
+ setShown(shown) {
2584
+ this.backdrop.style.display = shown ? "" : "none";
2585
+ this.positioner.style.display = shown ? "" : "none";
2586
+ }
2587
+ /** Keep the just-closed dialog up through its exit animation, then hide it — or
2588
+ * remove it, when it was destroyed meanwhile. `spreadProps` re-applies the close
2589
+ * props' `hidden` only when its value changes, so lifting it keeps the panel and
2590
+ * scrim up until the exit ends. The positioner spans the viewport: inert, it lets
2591
+ * clicks through to the page while the panel animates out. */
2592
+ holdExit() {
2593
+ const held = [this.panel, this.backdrop];
2594
+ for (const el of held) el.hidden = false;
2595
+ this.exit = holdForExit(
2596
+ held,
2597
+ () => {
2598
+ this.exit = null;
2599
+ if (this.destroyed) {
2600
+ this.detach();
2601
+ } else if (!this.open) {
2602
+ for (const el of held) el.hidden = true;
2603
+ this.setShown(false);
2604
+ }
2605
+ },
2606
+ { inert: [this.positioner] }
2607
+ );
2608
+ }
2609
+ detach() {
2610
+ this.backdrop.remove();
2611
+ this.positioner.remove();
2612
+ }
2613
+ /** Closing hands focus back to whatever opened it (Escape, the ✕, hide(), or a
2614
+ * teardown that outruns the machine's close notification). */
2615
+ restoreOpener() {
2616
+ const opener = this.opener;
2617
+ this.opener = null;
2618
+ if (opener?.isConnected) opener.focus({ preventScroll: true });
2619
+ }
2620
+ announce(open2) {
2621
+ const opener = this.opener === this.panel.ownerDocument.body ? null : this.opener;
2622
+ announceSurface(this.panel, open2, "dialog", opener);
2412
2623
  }
2413
2624
  get open() {
2414
2625
  return this.ctrl.connect(this.handle.service).open;
2415
2626
  }
2416
2627
  show() {
2628
+ this.opener = this.panel.ownerDocument.activeElement;
2417
2629
  this.ctrl.connect(this.handle.service).setOpen(true);
2418
2630
  }
2419
2631
  hide() {
@@ -2426,10 +2638,16 @@ var Dialog = class {
2426
2638
  return el?.closest(".vela-popover, .vela-menu") != null;
2427
2639
  }
2428
2640
  destroy() {
2641
+ if (this.wasOpen && !this.open) this.handle.flush();
2429
2642
  if (this.open) this.hide();
2430
2643
  this.handle.stop();
2431
- this.backdrop.remove();
2432
- this.positioner.remove();
2644
+ this.destroyed = true;
2645
+ if (this.wasOpen) {
2646
+ this.wasOpen = false;
2647
+ this.announce(false);
2648
+ }
2649
+ this.restoreOpener();
2650
+ if (!this.exit) this.detach();
2433
2651
  }
2434
2652
  };
2435
2653
  function drawerController(opts = {}) {
@@ -2556,6 +2774,11 @@ function swipeDirection(dx, dy) {
2556
2774
  }
2557
2775
  var Drawer = class {
2558
2776
  constructor(opts = {}) {
2777
+ this.opener = null;
2778
+ this.wasOpen = false;
2779
+ /** The sheet and scrim's exit animation after a close; a reopen cancels it. */
2780
+ this.exit = null;
2781
+ this.destroyed = false;
2559
2782
  const doc = (opts.host ?? document.body).ownerDocument;
2560
2783
  injectStyles(DRAWER_STYLE_ID, DRAWER_CSS, doc);
2561
2784
  const host = opts.host ?? doc.body;
@@ -2583,12 +2806,24 @@ var Drawer = class {
2583
2806
  const mid = String(this.ctrl.props.id);
2584
2807
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
2585
2808
  const api = this.ctrl.connect(service);
2809
+ const closing = this.wasOpen && !api.open;
2810
+ if (closing) this.announce(false);
2586
2811
  vanilla.spreadProps(this.backdrop, api.getBackdropProps(), mid);
2587
2812
  vanilla.spreadProps(this.positioner, api.getPositionerProps(), mid);
2588
2813
  vanilla.spreadProps(this.panel, api.getContentProps(), mid);
2589
2814
  vanilla.spreadProps(this.titleEl, api.getTitleProps(), mid);
2590
- this.backdrop.style.display = api.open ? "" : "none";
2591
- this.positioner.style.display = api.open ? "" : "none";
2815
+ if (api.open) {
2816
+ this.exit?.cancel();
2817
+ this.exit = null;
2818
+ this.setShown(true);
2819
+ } else if (closing) {
2820
+ this.holdExit();
2821
+ } else if (!this.exit) {
2822
+ this.setShown(false);
2823
+ }
2824
+ if (api.open && !this.wasOpen) this.announce(true);
2825
+ if (this.wasOpen && !api.open) this.restoreOpener();
2826
+ this.wasOpen = api.open;
2592
2827
  });
2593
2828
  }
2594
2829
  /** Any element between `from` and the panel that has already been scrolled down —
@@ -2697,19 +2932,67 @@ var Drawer = class {
2697
2932
  setTitle(title) {
2698
2933
  this.titleEl.textContent = title;
2699
2934
  }
2935
+ setShown(shown) {
2936
+ this.backdrop.style.display = shown ? "" : "none";
2937
+ this.positioner.style.display = shown ? "" : "none";
2938
+ }
2939
+ /** Keep the just-closed sheet up through its exit animation, then hide it — or
2940
+ * remove it, when it was destroyed meanwhile. `spreadProps` re-applies the close
2941
+ * props' `hidden` only when its value changes, so lifting it keeps the sheet and
2942
+ * scrim up until the exit ends. The positioner spans the host: inert, it lets
2943
+ * clicks through to the page while the sheet animates out. */
2944
+ holdExit() {
2945
+ const held = [this.panel, this.backdrop];
2946
+ for (const el of held) el.hidden = false;
2947
+ this.exit = holdForExit(
2948
+ held,
2949
+ () => {
2950
+ this.exit = null;
2951
+ if (this.destroyed) {
2952
+ this.detach();
2953
+ } else if (!this.open) {
2954
+ for (const el of held) el.hidden = true;
2955
+ this.setShown(false);
2956
+ }
2957
+ },
2958
+ { inert: [this.positioner] }
2959
+ );
2960
+ }
2961
+ detach() {
2962
+ this.backdrop.remove();
2963
+ this.positioner.remove();
2964
+ }
2965
+ /** Closing hands focus back to whatever opened it (Escape, the ✕, hide(), or a
2966
+ * teardown that outruns the machine's close notification). */
2967
+ restoreOpener() {
2968
+ const opener = this.opener;
2969
+ this.opener = null;
2970
+ if (opener?.isConnected) opener.focus({ preventScroll: true });
2971
+ }
2972
+ announce(open2) {
2973
+ const opener = this.opener === this.panel.ownerDocument.body ? null : this.opener;
2974
+ announceSurface(this.panel, open2, "drawer", opener);
2975
+ }
2700
2976
  get open() {
2701
2977
  return this.ctrl.connect(this.handle.service).open;
2702
2978
  }
2703
2979
  show() {
2980
+ this.opener = this.panel.ownerDocument.activeElement;
2704
2981
  this.ctrl.connect(this.handle.service).setOpen(true);
2705
2982
  }
2706
2983
  hide() {
2707
2984
  this.ctrl.connect(this.handle.service).setOpen(false);
2708
2985
  }
2709
2986
  destroy() {
2987
+ if (this.wasOpen && !this.open) this.handle.flush();
2710
2988
  this.handle.stop();
2711
- this.backdrop.remove();
2712
- this.positioner.remove();
2989
+ this.destroyed = true;
2990
+ if (this.wasOpen) {
2991
+ this.wasOpen = false;
2992
+ this.announce(false);
2993
+ }
2994
+ this.restoreOpener();
2995
+ if (!this.exit) this.detach();
2713
2996
  }
2714
2997
  };
2715
2998
 
@@ -4845,11 +5128,11 @@ var DatePicker = class {
4845
5128
  };
4846
5129
 
4847
5130
  // src/widget/layout-picker.ts
4848
- var STYLE_ID = "vela-widget-layout-picker-v14";
5131
+ var STYLE_ID = "vela-widget-layout-picker-v15";
4849
5132
  var CSS2 = `
4850
5133
  .vela-lp-layer { position: absolute; z-index: var(--vela-z-menu); }
4851
5134
  .vela-lp {
4852
- background: var(--vela-surface-elev);
5135
+ background: var(--vela-surface);
4853
5136
  color: var(--vela-fg);
4854
5137
  border: 1px solid var(--vela-border-strong);
4855
5138
  border-radius: 8px;
@@ -4978,6 +5261,8 @@ var LayoutPicker = class {
4978
5261
  constructor(opts) {
4979
5262
  this.squares = [];
4980
5263
  this.isOpen = false;
5264
+ /** The card's exit animation after a close; a reopen cancels it. */
5265
+ this.exit = null;
4981
5266
  /** Hover preview (1-based rows/cols), null = show current shape. */
4982
5267
  this.hover = null;
4983
5268
  this.onDocPointerDown = (e) => {
@@ -4998,6 +5283,7 @@ var LayoutPicker = class {
4998
5283
  const panel = doc.createElement("div");
4999
5284
  panel.className = "vela-lp";
5000
5285
  this.layer.appendChild(panel);
5286
+ this.panel = panel;
5001
5287
  const cols = doc.createElement("div");
5002
5288
  cols.className = "vela-lp-cols";
5003
5289
  panel.appendChild(cols);
@@ -5071,6 +5357,8 @@ var LayoutPicker = class {
5071
5357
  open() {
5072
5358
  if (this.isOpen) return;
5073
5359
  this.isOpen = true;
5360
+ this.exit?.cancel();
5361
+ this.exit = null;
5074
5362
  this.hover = null;
5075
5363
  this.refresh();
5076
5364
  this.layer.style.display = "";
@@ -5079,11 +5367,16 @@ var LayoutPicker = class {
5079
5367
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
5080
5368
  this.doc.addEventListener("keydown", this.onDocKeydown, true);
5081
5369
  this.opts.onOpenChange?.(true);
5370
+ if (this.isOpen) announceSurface(this.panel, true, "popover", this.opts.trigger);
5082
5371
  }
5083
5372
  close() {
5084
5373
  if (!this.isOpen) return;
5085
5374
  this.isOpen = false;
5086
- this.layer.style.display = "none";
5375
+ announceSurface(this.panel, false, "popover", this.opts.trigger);
5376
+ this.exit = holdForExit(this.panel, () => {
5377
+ this.exit = null;
5378
+ if (!this.isOpen) this.layer.style.display = "none";
5379
+ });
5087
5380
  this.opts.trigger.setAttribute("aria-expanded", "false");
5088
5381
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
5089
5382
  this.doc.removeEventListener("keydown", this.onDocKeydown, true);
@@ -5098,6 +5391,7 @@ var LayoutPicker = class {
5098
5391
  }
5099
5392
  destroy() {
5100
5393
  this.close();
5394
+ this.exit?.finish();
5101
5395
  this.infoTip.destroy();
5102
5396
  this.layer.remove();
5103
5397
  }
@@ -5164,7 +5458,7 @@ var LayoutPicker = class {
5164
5458
  const hostRect = this.opts.host.getBoundingClientRect();
5165
5459
  const trigRect = this.opts.trigger.getBoundingClientRect();
5166
5460
  let left = trigRect.left - hostRect.left;
5167
- const top = trigRect.bottom - hostRect.top + 4;
5461
+ const top = trigRect.bottom - hostRect.top + 8;
5168
5462
  const width = this.layer.offsetWidth;
5169
5463
  if (left + width > hostRect.width - 8) left = Math.max(8, hostRect.width - 8 - width);
5170
5464
  this.layer.style.left = `${left}px`;
@@ -5323,10 +5617,12 @@ function legendActions() {
5323
5617
  }
5324
5618
  function legendActionsProviderFor(chart, context) {
5325
5619
  return (indicatorId) => {
5326
- const handle = chart.indicators().find((h) => h.id === indicatorId);
5620
+ const find = () => chart.indicators().find((h) => h.id === indicatorId);
5621
+ const infoOf = (h) => ({ id: h.id, title: h.title, ...h.source !== void 0 ? { source: h.source } : {} });
5622
+ const handle = find();
5327
5623
  if (!handle) return [];
5328
- const info = { id: handle.id, title: handle.title, ...handle.source !== void 0 ? { source: handle.source } : {} };
5329
- return legendActions().filter((d) => !d.when || d.when(info)).map((d) => ({ id: d.id, icon: d.icon, tooltip: d.tooltip, run: () => d.run(context(), info) }));
5624
+ const info = infoOf(handle);
5625
+ return legendActions().filter((d) => !d.when || d.when(info)).map((d) => ({ id: d.id, icon: d.icon, tooltip: d.tooltip, run: () => d.run(context(), infoOf(find() ?? handle)) }));
5330
5626
  };
5331
5627
  }
5332
5628
  var calloutRegistry = /* @__PURE__ */ new Map();
@@ -6193,7 +6489,8 @@ var Topbar = class {
6193
6489
  triggerId: "vela-topbar-style",
6194
6490
  host,
6195
6491
  items: this.styleItems(),
6196
- onSelect: (id) => opts.onPriceStyle(id)
6492
+ onSelect: (id) => opts.onPriceStyle(id),
6493
+ iconBadges: true
6197
6494
  });
6198
6495
  }
6199
6496
  setSymbol(symbol) {
@@ -6259,8 +6556,14 @@ var Topbar = class {
6259
6556
  renderLayoutButton(doc) {
6260
6557
  if (!this.layoutButton) return;
6261
6558
  this.layoutButton.replaceChildren();
6262
- if (iconMarkup("layout")) this.layoutButton.appendChild(iconEl("layout", doc));
6263
- else this.layoutButton.appendChild(doc.createTextNode(this.layoutId ?? ""));
6559
+ const glyph = this.opts.layout?.glyph?.();
6560
+ if (glyph || iconMarkup("layout")) {
6561
+ const icon2 = iconEl("layout", doc);
6562
+ if (glyph) icon2.innerHTML = glyph;
6563
+ this.layoutButton.appendChild(icon2);
6564
+ } else {
6565
+ this.layoutButton.appendChild(doc.createTextNode(this.layoutId ?? ""));
6566
+ }
6264
6567
  this.layoutButton.setAttribute("aria-label", `Layout \u2014 ${this.layoutId ?? ""}`);
6265
6568
  }
6266
6569
  renderStyleButton(doc) {
@@ -9543,6 +9846,10 @@ var FibRatios = class extends Drawing {
9543
9846
 
9544
9847
  // src/core/drawings/types/FibLevels.ts
9545
9848
  var FibLevels = class extends FibRatios {
9849
+ /** Price of a level for the two anchor prices; subclasses override to change which anchor ratio 0 sits on. */
9850
+ levelPrice(ratio, p1, p2) {
9851
+ return p1 + ratio * (p2 - p1);
9852
+ }
9546
9853
  /** Per-level pixel line + price for the ENABLED levels, spanning the anchors' time range. */
9547
9854
  levelLines(proj) {
9548
9855
  const a = this.anchors[0];
@@ -9552,11 +9859,10 @@ var FibLevels = class extends FibRatios {
9552
9859
  const xb = proj.xOf(b.time);
9553
9860
  const x1 = Math.min(xa, xb);
9554
9861
  const x2 = Math.max(xa, xb);
9555
- const delta = b.price - a.price;
9556
9862
  const out = [];
9557
9863
  for (const lv of this.levels) {
9558
9864
  if (!lv.enabled) continue;
9559
- const price = a.price + lv.ratio * delta;
9865
+ const price = this.levelPrice(lv.ratio, a.price, b.price);
9560
9866
  const y = proj.yOf(price, this.paneId);
9561
9867
  if (y == null) continue;
9562
9868
  out.push({ ratio: lv.ratio, color: lv.color, label: lv.label, price, x1, x2, y });
@@ -9596,8 +9902,7 @@ var FibLevels = class extends FibRatios {
9596
9902
  const a = this.anchors[0];
9597
9903
  const b = this.anchors[1];
9598
9904
  if (!a || !b) return null;
9599
- const delta = b.price - a.price;
9600
- const prices = this.levels.filter((l) => l.enabled).map((l) => a.price + l.ratio * delta);
9905
+ const prices = this.levels.filter((l) => l.enabled).map((l) => this.levelPrice(l.ratio, a.price, b.price));
9601
9906
  if (prices.length === 0) return null;
9602
9907
  return { min: Math.min(...prices), max: Math.max(...prices) };
9603
9908
  }
@@ -9683,13 +9988,24 @@ function cycleLevels(count, enabledCount = count) {
9683
9988
  // src/core/drawings/types/FibRetracement.ts
9684
9989
  var LEVELS = fibLevels([0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]);
9685
9990
  var FibRetracement = class extends FibLevels {
9686
- constructor() {
9687
- super(...arguments);
9991
+ constructor(init) {
9992
+ super(init);
9688
9993
  this.type = "fibretracement";
9994
+ if (this.reverse === void 0) this.reverse = false;
9689
9995
  }
9690
9996
  defaultLevels() {
9691
9997
  return LEVELS;
9692
9998
  }
9999
+ levelPrice(ratio, p1, p2) {
10000
+ return this.reverse ? p1 + ratio * (p2 - p1) : p2 + ratio * (p1 - p2);
10001
+ }
10002
+ writeProps() {
10003
+ return { ...super.writeProps(), reverse: this.reverse };
10004
+ }
10005
+ readProps(props) {
10006
+ super.readProps(props);
10007
+ this.reverse = typeof props.reverse === "boolean" ? props.reverse : true;
10008
+ }
9693
10009
  };
9694
10010
 
9695
10011
  // src/core/drawings/types/FibExtension.ts
@@ -12816,6 +13132,32 @@ var CSS5 = `
12816
13132
  .vela-panel-pin .vela-icon { width: 15px; height: 15px; }
12817
13133
  .vela-panel-pin:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
12818
13134
  .vela-panel-pin[data-on='1'] { color: var(--vela-fg-bright); }
13135
+ .vela-panel-max {
13136
+ all: unset;
13137
+ cursor: pointer;
13138
+ width: 26px;
13139
+ height: 26px;
13140
+ flex: none;
13141
+ display: inline-flex;
13142
+ align-items: center;
13143
+ justify-content: center;
13144
+ border-radius: 4px;
13145
+ color: var(--vela-fg-muted);
13146
+ }
13147
+ .vela-panel-max .vela-icon { width: 15px; height: 15px; }
13148
+ .vela-panel-max:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
13149
+ /* MAXIMIZED: the panel covers the whole dock host \u2014 every chart of the shell \u2014 whatever
13150
+ its placement; the width handle and the pin have nothing to do meanwhile. */
13151
+ .vela-panel[data-maximized] {
13152
+ position: absolute;
13153
+ inset: 0;
13154
+ width: auto;
13155
+ max-width: none;
13156
+ z-index: 26;
13157
+ border-left: none;
13158
+ }
13159
+ .vela-panel[data-maximized] .vela-panel-resizer,
13160
+ .vela-panel[data-maximized] .vela-panel-pin { display: none; }
12819
13161
  .vela-panel-body { flex: 1; overflow: auto; padding: 8px; }
12820
13162
  .vela-panel-body::-webkit-scrollbar { width: 8px; }
12821
13163
  .vela-panel-body::-webkit-scrollbar-thumb { background: var(--vela-scroll); border-radius: 4px; border: 2px solid transparent; background-clip: padding-box; }
@@ -12868,6 +13210,7 @@ var CSS5 = `
12868
13210
  border-left: none;
12869
13211
  }
12870
13212
  [data-layout='mobile'] .vela-panel-resizer { display: none; }
13213
+ [data-layout='mobile'] .vela-panel-max { display: none; }
12871
13214
  `;
12872
13215
  function clampPanelWidth(px, min = DEFAULT_PANEL_MIN_WIDTH, max = DEFAULT_PANEL_MAX_WIDTH) {
12873
13216
  const lo = Number.isFinite(min) && min > 0 ? min : DEFAULT_PANEL_MIN_WIDTH;
@@ -12891,6 +13234,15 @@ var SidePanel = class {
12891
13234
  * never on a programmatic {@link setOverlay}, for the same reason as widths. */
12892
13235
  this.onPlacementChange = null;
12893
13236
  this.pin = null;
13237
+ this.maxButton = null;
13238
+ this.maximizedOn = false;
13239
+ /** Set while the close is announced, so a listener's own close does not re-enter. */
13240
+ this.closing = false;
13241
+ /** Open as far as the dock and the host are concerned — a closed panel can still be on
13242
+ * screen while its exit animation runs. */
13243
+ this.isOpen = false;
13244
+ /** The exit animation of a close; a reopen cancels it. */
13245
+ this.exit = null;
12894
13246
  const doc = host.ownerDocument;
12895
13247
  injectStyles(STYLE_ID4, CSS5, doc);
12896
13248
  this.minWidth = opts.resizable ? opts.minWidth ?? DEFAULT_PANEL_MIN_WIDTH : 1;
@@ -12925,7 +13277,13 @@ var SidePanel = class {
12925
13277
  });
12926
13278
  this.refreshPin();
12927
13279
  }
12928
- header.append(this.heading, this.slot, ...this.pin ? [this.pin] : [], close);
13280
+ if (opts.maximizable) {
13281
+ this.maxButton = doc.createElement("button");
13282
+ this.maxButton.className = "vela-panel-max";
13283
+ this.maxButton.addEventListener("click", () => this.setMaximized(!this.maximizedOn));
13284
+ this.refreshMaxButton();
13285
+ }
13286
+ header.append(this.heading, this.slot, ...this.pin ? [this.pin] : [], ...this.maxButton ? [this.maxButton] : [], close);
12929
13287
  this.body = doc.createElement("div");
12930
13288
  this.body.className = "vela-panel-body";
12931
13289
  this.el.append(header, this.body);
@@ -12933,13 +13291,65 @@ var SidePanel = class {
12933
13291
  host.appendChild(this.el);
12934
13292
  }
12935
13293
  get open() {
12936
- return !this.el.hidden;
13294
+ return this.isOpen;
12937
13295
  }
12938
- /** Open/close the panel — a bare call flips it. */
12939
- toggle(open2 = this.el.hidden) {
12940
- if (open2 === !this.el.hidden) return;
12941
- this.el.hidden = !open2;
13296
+ /**
13297
+ * Open/close the panel — a bare call flips it. The panel reports closed at once, but a
13298
+ * close keeps it on screen, marked `data-closing`, while a host exit animation runs.
13299
+ * `instant` skips that, and also cuts short the exit of a panel already closing: a panel
13300
+ * handing the dock to another leaves at once.
13301
+ */
13302
+ toggle(open2 = !this.isOpen, instant = false) {
13303
+ if (open2 === this.isOpen) {
13304
+ if (!open2 && instant) this.exit?.finish();
13305
+ return;
13306
+ }
13307
+ if (!open2 && this.closing) return;
13308
+ if (!open2) {
13309
+ this.closing = true;
13310
+ announceSurface(this.el, false, "panel");
13311
+ this.closing = false;
13312
+ }
13313
+ this.isOpen = open2;
13314
+ if (open2) {
13315
+ if (this.exit) {
13316
+ this.exit.cancel();
13317
+ this.exit = null;
13318
+ this.setMaximized(false);
13319
+ }
13320
+ this.el.hidden = false;
13321
+ } else {
13322
+ const hide = () => {
13323
+ this.exit = null;
13324
+ this.el.hidden = true;
13325
+ this.setMaximized(false);
13326
+ };
13327
+ if (instant) hide();
13328
+ else this.exit = holdForExit(this.el, hide);
13329
+ }
12942
13330
  this.onOpenChange?.(open2);
13331
+ if (open2 && this.isOpen) announceSurface(this.el, true, "panel");
13332
+ }
13333
+ /** Whether the panel covers every chart right now. */
13334
+ get maximized() {
13335
+ return this.maximizedOn;
13336
+ }
13337
+ /** Maximize the panel over the shell's charts, or restore it. A no-op on a panel not
13338
+ * declared `maximizable`. */
13339
+ setMaximized(maximized) {
13340
+ if (!this.maxButton || maximized === this.maximizedOn) return;
13341
+ this.maximizedOn = maximized;
13342
+ if (maximized) this.el.dataset.maximized = "1";
13343
+ else delete this.el.dataset.maximized;
13344
+ this.refreshMaxButton();
13345
+ }
13346
+ refreshMaxButton() {
13347
+ if (!this.maxButton) return;
13348
+ const doc = this.maxButton.ownerDocument;
13349
+ this.maxButton.replaceChildren(iconEl(this.maximizedOn ? "restore" : "maximize", doc));
13350
+ this.maxButton.title = this.maximizedOn ? "Restore" : "Maximize";
13351
+ this.maxButton.setAttribute("aria-label", this.maxButton.title);
13352
+ this.maxButton.setAttribute("aria-pressed", this.maximizedOn ? "true" : "false");
12943
13353
  }
12944
13354
  /** The scrolling body, for a panel filled from OUTSIDE the class — a contributed panel's
12945
13355
  * `mount` receives exactly this element. Subclasses use the protected `body`. */
@@ -12991,6 +13401,8 @@ var SidePanel = class {
12991
13401
  this.pin.setAttribute("aria-pressed", pinned ? "true" : "false");
12992
13402
  }
12993
13403
  destroy() {
13404
+ if (this.open) announceSurface(this.el, false, "panel");
13405
+ this.exit?.finish();
12994
13406
  this.el.remove();
12995
13407
  }
12996
13408
  /** The drag handle on the panel's inner (left) edge — the panel is docked right, so dragging
@@ -13295,8 +13707,8 @@ var ObjectTree = class extends SidePanel {
13295
13707
  set groups(next) {
13296
13708
  if (this.chart) this.groupsPerChart.set(this.chart, next);
13297
13709
  }
13298
- toggle(open2 = this.el.hidden) {
13299
- super.toggle(open2);
13710
+ toggle(open2 = !this.open, instant = false) {
13711
+ super.toggle(open2, instant);
13300
13712
  if (open2) this.refresh();
13301
13713
  }
13302
13714
  setSymbol(symbol) {
@@ -14330,8 +14742,8 @@ var DataWindow = class extends SidePanel {
14330
14742
  this.unsubs = [];
14331
14743
  injectStyles(STYLE_ID6, CSS7, host.ownerDocument);
14332
14744
  }
14333
- toggle(open2 = this.el.hidden) {
14334
- super.toggle(open2);
14745
+ toggle(open2 = !this.open, instant = false) {
14746
+ super.toggle(open2, instant);
14335
14747
  if (open2) this.refresh();
14336
14748
  }
14337
14749
  /** (Re)bind to a chart instance — called after every widget rebuild. */
@@ -14407,19 +14819,22 @@ var PanelDock = class {
14407
14819
  /**
14408
14820
  * (Re)build the CONTRIBUTED panels from the registry — call once after the built-ins, and
14409
14821
  * again on `refreshActions()` so a late registration appears. Contributed panels that are
14410
- * gone from the registry are dropped; the ones still there are rebuilt, so a replaced
14411
- * descriptor takes effect.
14822
+ * gone from the registry are dropped and a REPLACED descriptor is rebuilt; a panel whose
14823
+ * descriptor is unchanged stays mounted as it is, so its content and state survive.
14412
14824
  */
14413
14825
  refresh() {
14414
14826
  const openBefore = this.openId;
14415
- for (const entry of [...this.entries]) if (entry.contributed) this.drop(entry);
14416
- for (const desc of sidePanels()) {
14827
+ const registered = sidePanels();
14828
+ for (const entry of [...this.entries]) if (entry.contributed && !registered.includes(entry.desc)) this.drop(entry);
14829
+ for (const desc of registered) {
14830
+ if (this.entries.some((e) => e.desc === desc)) continue;
14417
14831
  const panel = new SidePanel(this.host, desc.title, `vela-panel-${desc.id}`, {
14418
14832
  width: desc.width,
14419
14833
  resizable: desc.resizable,
14420
14834
  minWidth: desc.minWidth,
14421
14835
  maxWidth: desc.maxWidth,
14422
- overlay: desc.overlay
14836
+ overlay: desc.overlay,
14837
+ maximizable: desc.maximizable
14423
14838
  });
14424
14839
  const entry = {
14425
14840
  id: desc.id,
@@ -14427,10 +14842,19 @@ var PanelDock = class {
14427
14842
  icon: desc.icon,
14428
14843
  order: desc.order ?? DEFAULT_PANEL_ORDER,
14429
14844
  panel,
14430
- contributed: true
14845
+ contributed: true,
14846
+ desc
14431
14847
  };
14432
14848
  try {
14433
- entry.handle = desc.mount(this.deps.context(), panel.content, { slot: panel.headerSlot, setTitle: (t) => panel.setTitle(t) }) ?? void 0;
14849
+ const header = {
14850
+ slot: panel.headerSlot,
14851
+ setTitle: (t) => panel.setTitle(t),
14852
+ get maximized() {
14853
+ return panel.maximized;
14854
+ },
14855
+ setMaximized: (on) => panel.setMaximized(on)
14856
+ };
14857
+ entry.handle = desc.mount(this.deps.context(), panel.content, header) ?? void 0;
14434
14858
  if (this.chart) entry.handle?.onChart?.(this.chart);
14435
14859
  } catch (err) {
14436
14860
  console.warn(`[vela] side panel "${desc.id}" failed to mount`, err);
@@ -14489,7 +14913,7 @@ var PanelDock = class {
14489
14913
  }
14490
14914
  this.pinned = new Set(state.pinned ?? []);
14491
14915
  for (const entry of this.entries) entry.panel.setOverlay(!this.pinned.has(entry.id));
14492
- for (const entry of this.entries) entry.panel.toggle(entry.id === state.open);
14916
+ for (const entry of this.entries) entry.panel.toggle(entry.id === state.open, true);
14493
14917
  this.pendingOpen = state.open && !this.entries.some((e) => e.id === state.open) ? state.open : null;
14494
14918
  }
14495
14919
  /** Drop the contributed panels (the shell destroys its own). */
@@ -14505,7 +14929,7 @@ var PanelDock = class {
14505
14929
  if (this.pinned.has(entry.id)) entry.panel.setOverlay(false);
14506
14930
  entry.panel.onOpenChange = (open2) => {
14507
14931
  if (open2) {
14508
- for (const other of this.entries) if (other !== entry) other.panel.toggle(false);
14932
+ for (const other of this.entries) if (other !== entry) other.panel.toggle(false, true);
14509
14933
  this.pendingOpen = null;
14510
14934
  }
14511
14935
  this.deps.chrome.setPanelActive(entry.id, open2);
@@ -16209,7 +16633,11 @@ var MoreDrawer = class {
16209
16633
  row(doc, label, opts) {
16210
16634
  const el = doc.createElement("div");
16211
16635
  el.className = "vela-md-row";
16212
- if (opts.icon) el.appendChild(iconEl(opts.icon, doc));
16636
+ if (opts.icon) {
16637
+ const icon2 = iconEl(opts.icon, doc);
16638
+ if (opts.glyph) icon2.innerHTML = opts.glyph;
16639
+ el.appendChild(icon2);
16640
+ }
16213
16641
  const text = doc.createElement("span");
16214
16642
  text.className = "vela-md-row-label";
16215
16643
  text.textContent = label;
@@ -16251,7 +16679,7 @@ var MoreDrawer = class {
16251
16679
  if (this.opts.layout) {
16252
16680
  const shape = this.opts.layout.shape();
16253
16681
  const value = shape ? `${shape.cols} \xD7 ${shape.rows}` : this.opts.layout.presets().find((p) => p.checked)?.label;
16254
- list.appendChild(this.row(doc, "Layout", { icon: "layout", value, chevron: true, onClick: () => this.show("layout") }));
16682
+ list.appendChild(this.row(doc, "Layout", { icon: "layout", glyph: this.opts.layout.glyph?.(), value, chevron: true, onClick: () => this.show("layout") }));
16255
16683
  }
16256
16684
  for (const act of this.opts.primaryActions?.() ?? []) list.appendChild(this.actionRow(doc, act));
16257
16685
  for (const panel of this.opts.panels()) {
@@ -16429,8 +16857,8 @@ var SETTINGS_SECTION = {
16429
16857
  "price-axis": "Scales and lines",
16430
16858
  "time-axis": "Scales and lines"
16431
16859
  };
16432
- function settingsItem(zone, label) {
16433
- return { id: `settings:${SETTINGS_SECTION[zone]}`, label, separatorBefore: true };
16860
+ function settingsItem(zone, label, icon2) {
16861
+ return { id: `settings:${SETTINGS_SECTION[zone]}`, label, ...icon2 ? { icon: icon2 } : {}, separatorBefore: true };
16434
16862
  }
16435
16863
  function settingsSectionOf(id) {
16436
16864
  return id.slice("settings:".length) || void 0;
@@ -16510,12 +16938,39 @@ function timeAxisItems(timezone) {
16510
16938
  }
16511
16939
  function bodyItems(counts) {
16512
16940
  return [
16513
- { id: "reset-view", label: "Reset chart view" },
16514
- { id: "remove-drawings", label: "Remove drawings", disabled: counts.drawings === 0, separatorBefore: true },
16515
- { id: "remove-indicators", label: "Remove indicators", disabled: counts.indicators === 0 },
16516
- settingsItem("body", "Settings\u2026")
16941
+ { id: "reset-view", label: "Reset chart view", icon: "reset" },
16942
+ { id: "remove-drawings", label: "Remove drawings", icon: "eraser", disabled: counts.drawings === 0, separatorBefore: true },
16943
+ { id: "remove-indicators", label: "Remove indicators", icon: "indicators", disabled: counts.indicators === 0 },
16944
+ settingsItem("body", "Settings\u2026", "gear")
16517
16945
  ];
16518
16946
  }
16947
+ var CONTEXT_MENU_BUILTIN_ORDER = {
16948
+ body: { "reset-view": -30, "remove-drawings": -20, "remove-indicators": -10, settings: 1e3 },
16949
+ "price-axis": {
16950
+ auto: -80,
16951
+ invert: -70,
16952
+ "scale:regular": -60,
16953
+ "scale:percent": -50,
16954
+ "scale:indexed": -40,
16955
+ "scale:log": -30,
16956
+ labels: -20,
16957
+ levels: -10,
16958
+ settings: 1e3
16959
+ },
16960
+ "time-axis": { timezone: -10, settings: 1e3 }
16961
+ };
16962
+ function composeMenu(zone, builtin, contributed) {
16963
+ const ranks = CONTEXT_MENU_BUILTIN_ORDER[zone];
16964
+ const rows = [];
16965
+ let group = 0;
16966
+ for (const item of builtin) {
16967
+ if (item.separatorBefore) group += 1;
16968
+ rows.push({ item, rank: ranks[item.id.startsWith("settings") ? "settings" : item.id] ?? 0, group });
16969
+ }
16970
+ for (const { item, order } of contributed) rows.push({ item, rank: order ?? 0, group: -1 });
16971
+ rows.sort((a, b) => a.rank - b.rank);
16972
+ return rows.map(({ item, group: g }, i) => ({ ...item, separatorBefore: i > 0 && rows[i - 1].group !== g }));
16973
+ }
16519
16974
 
16520
16975
  // src/widget/price-scale-drawer.ts
16521
16976
  var STYLE_ID17 = "vela-widget-price-scale-drawer";
@@ -17790,6 +18245,7 @@ function chromeHint(text, opts) {
17790
18245
  // src/renderers/native/drawings/DrawingToolbar.ts
17791
18246
  var TOOLBAR_WIDTH = 44;
17792
18247
  var TOOLBAR_COLLAPSED_WIDTH = 16;
18248
+ var FLYOUT_GAP = 3;
17793
18249
  var DrawingToolbar = class {
17794
18250
  constructor(host, theme, onArm, onMagnet = () => {
17795
18251
  }, onMeasure = () => {
@@ -17819,6 +18275,9 @@ var DrawingToolbar = class {
17819
18275
  // group id (or MAGNET_ID) whose flyout is open
17820
18276
  this.flyoutCell = null;
17821
18277
  // the cell the open flyout is anchored to
18278
+ /** Closed flyouts still playing their exit animation, by owner id — reopening the same
18279
+ * owner takes its element back instead of stacking a second one on it. */
18280
+ this.exitingFlyouts = /* @__PURE__ */ new Map();
17822
18281
  this.groupCells = /* @__PURE__ */ new Map();
17823
18282
  // the composite cell (hover/active bg + flyout anchor)
17824
18283
  this.groupIcons = /* @__PURE__ */ new Map();
@@ -17927,6 +18386,7 @@ var DrawingToolbar = class {
17927
18386
  }
17928
18387
  destroy() {
17929
18388
  this.closeFlyout();
18389
+ for (const { exit } of [...this.exitingFlyouts.values()]) exit.finish();
17930
18390
  for (const dispose of this.tipDisposers.splice(0)) dispose();
17931
18391
  this.root.remove();
17932
18392
  }
@@ -18154,16 +18614,22 @@ var DrawingToolbar = class {
18154
18614
  * outside-dismiss. Callers fill it with items. */
18155
18615
  beginFlyout(ownerId, cell) {
18156
18616
  const t = this.theme;
18157
- const fly = document.createElement("div");
18617
+ const back = this.exitingFlyouts.get(ownerId);
18618
+ if (back) {
18619
+ this.exitingFlyouts.delete(ownerId);
18620
+ back.exit.cancel();
18621
+ back.fly.replaceChildren();
18622
+ }
18623
+ const fly = back?.fly ?? document.createElement("div");
18158
18624
  fly.className = "vela-dtb-flyout";
18159
- fly.style.cssText = `position:absolute;z-index:23;display:flex;flex-direction:column;gap:2px;padding:4px;border-radius:0 8px 8px 0;background:var(--vela-surface-elev);border:1px solid ${this.borderColor};border-left:none;box-shadow:var(--vela-shadow);pointer-events:auto;overflow-y:auto;overscroll-behavior:contain;`;
18625
+ fly.style.cssText = `position:absolute;z-index:23;display:flex;flex-direction:column;gap:2px;padding:4px;border-radius:var(--vela-radius-md);background:var(--vela-surface);border:1px solid ${this.borderOverride ?? "var(--vela-border-strong)"};box-shadow:var(--vela-shadow);pointer-events:auto;overflow-y:auto;overscroll-behavior:contain;`;
18160
18626
  applyChromeTokens(fly, t);
18161
18627
  this.host.appendChild(fly);
18162
18628
  const r = cell.getBoundingClientRect();
18163
18629
  const rootR = this.root.getBoundingClientRect();
18164
18630
  const hostR = this.host.getBoundingClientRect();
18165
18631
  const top = r.top - hostR.top;
18166
- fly.style.left = `${rootR.right - hostR.left - 1}px`;
18632
+ fly.style.left = `${rootR.right - hostR.left + FLYOUT_GAP}px`;
18167
18633
  fly.style.top = `${top}px`;
18168
18634
  fly.style.maxHeight = `${Math.max(120, hostR.height - top - 8)}px`;
18169
18635
  this.flyout = fly;
@@ -18179,6 +18645,7 @@ var DrawingToolbar = class {
18179
18645
  if (!cell) return;
18180
18646
  const fly = this.beginFlyout(group.id, cell);
18181
18647
  const sections = group.sections ?? [{ label: "", tools: group.tools }];
18648
+ const badge = sections.some((s) => s.tools.some((tool) => !!tool.icon));
18182
18649
  for (let si = 0; si < sections.length; si++) {
18183
18650
  const section = sections[si];
18184
18651
  if (section.label) {
@@ -18189,6 +18656,7 @@ var DrawingToolbar = class {
18189
18656
  fly.appendChild(
18190
18657
  this.makeFlyoutItem({
18191
18658
  icon: tool.icon,
18659
+ badge,
18192
18660
  label: tool.label,
18193
18661
  selected: tool.type === this.active,
18194
18662
  shortcut: this.shortcuts.get(tool.type),
@@ -18201,6 +18669,7 @@ var DrawingToolbar = class {
18201
18669
  );
18202
18670
  }
18203
18671
  }
18672
+ announceSurface(fly, true, "menu", cell);
18204
18673
  }
18205
18674
  openMagnetFlyout() {
18206
18675
  const cell = this.magnetCell;
@@ -18224,6 +18693,7 @@ var DrawingToolbar = class {
18224
18693
  })
18225
18694
  );
18226
18695
  }
18696
+ announceSurface(fly, true, "menu", cell);
18227
18697
  }
18228
18698
  flyoutHeader(text) {
18229
18699
  const t = this.theme;
@@ -18237,21 +18707,24 @@ var DrawingToolbar = class {
18237
18707
  div.style.cssText = `height:1px;margin:4px 8px;background:${this.borderColor};`;
18238
18708
  return div;
18239
18709
  }
18240
- /** A flyout row, left to right: optional leading icon, label, a check when it's the selected
18241
- * entry, an optional shortcut hint, and — at the far right — the favorite star (tool rows).
18242
- * Hover tint is CSS (`.vela-dtb-item:hover`), so navigating the menu stays smooth. */
18710
+ /** A flyout row, left to right: optional leading icon badge, label, a check when it's the
18711
+ * selected entry, an optional shortcut hint, and — at the far right — the favorite star (tool
18712
+ * rows). Hover tint is CSS (`.vela-dtb-item:hover`), so navigating the menu stays smooth. */
18243
18713
  makeFlyoutItem(opts) {
18244
18714
  const t = this.theme;
18245
18715
  const item = document.createElement("button");
18246
18716
  item.type = "button";
18247
18717
  item.className = "vela-dtb-item";
18248
18718
  item.setAttribute("aria-label", opts.label);
18249
- item.style.cssText = `display:flex;align-items:center;gap:8px;padding:5px 10px 5px 8px;cursor:pointer;color:${t.textColor};border-radius:var(--vela-radius-sm);font:13px ${t.fontFamily};white-space:nowrap;min-width:148px;`;
18250
- if (opts.icon) {
18251
- const icon2 = document.createElement("span");
18252
- icon2.style.cssText = "width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex:none;";
18253
- icon2.innerHTML = sizedIcon(opts.icon);
18254
- item.appendChild(icon2);
18719
+ const badged = opts.badge === true || !!opts.icon;
18720
+ const box = badged ? "gap:10px;padding:4px 10px 4px 6px;" : "gap:8px;padding:5px 10px 5px 8px;";
18721
+ item.style.cssText = `display:flex;align-items:center;${box}cursor:pointer;color:${t.textColor};border-radius:var(--vela-radius-sm);font:13px ${t.fontFamily};white-space:nowrap;min-width:148px;`;
18722
+ if (opts.selected) item.dataset.selected = "1";
18723
+ if (badged) {
18724
+ const badge = document.createElement("span");
18725
+ badge.className = "vela-dtb-badge";
18726
+ if (opts.icon) badge.innerHTML = opts.icon;
18727
+ item.appendChild(badge);
18255
18728
  }
18256
18729
  const label = document.createElement("span");
18257
18730
  label.textContent = opts.label;
@@ -18292,9 +18765,16 @@ var DrawingToolbar = class {
18292
18765
  }
18293
18766
  closeFlyout() {
18294
18767
  this.starEls.clear();
18295
- if (this.flyout) {
18296
- this.flyout.remove();
18768
+ const fly = this.flyout;
18769
+ if (fly) {
18297
18770
  this.flyout = null;
18771
+ announceSurface(fly, false, "menu", this.flyoutCell);
18772
+ const owner = this.flyoutOwnerId ?? "";
18773
+ const exit = holdForExit(fly, () => {
18774
+ if (this.exitingFlyouts.get(owner)?.fly === fly) this.exitingFlyouts.delete(owner);
18775
+ fly.remove();
18776
+ });
18777
+ if (exit) this.exitingFlyouts.set(owner, { fly, exit });
18298
18778
  this.flyoutCell?.classList.remove("vela-open");
18299
18779
  this.flyoutCell = null;
18300
18780
  this.flyoutOwnerId = null;
@@ -18378,6 +18858,11 @@ function ensureStyles() {
18378
18858
  .vela-dtb[data-collapsed='1'] .vela-dtb-collapse .vela-dtb-hit{width:14px;}
18379
18859
  .vela-dtb-item{background:transparent;border:none;transition:background var(--vela-dur-fast) ease;}
18380
18860
  .vela-dtb-item:hover{background:var(--vela-hover-strong);}
18861
+ .vela-dtb-badge{width:24px;height:24px;padding:3px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;flex:none;border-radius:var(--vela-radius-md);background:var(--vela-hover);border:1px solid var(--vela-border);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
18862
+ .vela-dtb-badge:empty{background:transparent;border-color:transparent;}
18863
+ .vela-dtb-badge svg{width:100%;height:100%;}
18864
+ .vela-dtb-item:hover .vela-dtb-badge:not(:empty){background:var(--vela-active);color:var(--vela-fg-bright);}
18865
+ .vela-dtb-item[data-selected='1'] .vela-dtb-badge:not(:empty){background:color-mix(in srgb,var(--vela-fg-bright) 83%,var(--vela-surface));border-color:color-mix(in srgb,var(--vela-fg-bright) 40%,transparent);color:var(--vela-surface);}
18381
18866
  .vela-dtb-star{width:26px;height:22px;margin:-3px -5px -3px 0;padding:3px 5px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;flex:none;opacity:0;color:inherit;border-radius:var(--vela-radius-sm);transition:opacity .1s ease,color .1s ease,background .1s ease;}
18382
18867
  .vela-dtb-star svg{width:16px;height:16px;}
18383
18868
  .vela-dtb-item:hover .vela-dtb-star{opacity:.55;}
@@ -18745,15 +19230,48 @@ function syncTargets(originId, setting, cellIds) {
18745
19230
  function rangesWithin(a, b, epsMs) {
18746
19231
  return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
18747
19232
  }
18748
- var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
18749
- function styleConfigSlice(config) {
18750
- if (config == null || typeof config !== "object") return null;
18751
- const doc = config;
18752
- const out = {};
19233
+ var STYLE_SYNC_CONFIG_KEYS = [
19234
+ "layout",
19235
+ "panes",
19236
+ "grid",
19237
+ "margins",
19238
+ "priceScale",
19239
+ "crosshair",
19240
+ "animations",
19241
+ "candles",
19242
+ "bars",
19243
+ "line",
19244
+ "area",
19245
+ "baseline",
19246
+ "sessions"
19247
+ ];
19248
+ function styleConfigPatch(origin, follower) {
19249
+ const from = asRecord(origin);
19250
+ const to = asRecord(follower);
19251
+ if (!from || !to) return null;
19252
+ const patch = {};
18753
19253
  for (const key of STYLE_SYNC_CONFIG_KEYS) {
18754
- if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
19254
+ const block = asRecord(from[key]);
19255
+ if (block && JSON.stringify(block) !== JSON.stringify(to[key])) patch[key] = block;
18755
19256
  }
18756
- return Object.keys(out).length > 0 ? out : null;
19257
+ const spacing = asRecord(from.series)?.spacing;
19258
+ if (typeof spacing === "number" && spacing !== asRecord(to.series)?.spacing) patch.series = { spacing };
19259
+ const fromTypes = asRecord(from.chartTypes) ?? {};
19260
+ const toTypes = asRecord(to.chartTypes) ?? {};
19261
+ const types = {};
19262
+ for (const typeId of /* @__PURE__ */ new Set([...Object.keys(fromTypes), ...Object.keys(toTypes)])) {
19263
+ const src = asRecord(fromTypes[typeId]) ?? {};
19264
+ const dst = asRecord(toTypes[typeId]) ?? {};
19265
+ for (const key of CANDLE_OVERRIDE_KEYS) {
19266
+ const value = src[key] ?? null;
19267
+ if (value !== (dst[key] ?? null)) (types[typeId] ?? (types[typeId] = {}))[key] = value;
19268
+ }
19269
+ }
19270
+ if (Object.keys(types).length > 0) patch.chartTypes = types;
19271
+ return Object.keys(patch).length > 0 ? patch : null;
19272
+ }
19273
+ function asRecord(v) {
19274
+ return v != null && typeof v === "object" ? v : null;
18757
19275
  }
18758
19276
 
18759
19277
  // src/widget/persist.ts
@@ -18949,7 +19467,7 @@ var IndicatorHandleImpl = class {
18949
19467
  this.controller.applyProps(this.id, values);
18950
19468
  }
18951
19469
  updateCode(source) {
18952
- this.controller.updateCode(this.id, source);
19470
+ return this.controller.updateCode(this.id, source);
18953
19471
  }
18954
19472
  setVisible(visible) {
18955
19473
  this.controller.setVisible(this.id, visible);
@@ -19428,6 +19946,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19428
19946
  this.activeEngineStyle = null;
19429
19947
  /** The chart's current price style (tracked from the renderer's change events + initial read). */
19430
19948
  this.priceStyle = "candles";
19949
+ this.priceStyleWillChangeUnsub = null;
19431
19950
  this.unresolvedUnsub = null;
19432
19951
  /** Latest chart visible range (left/right bar times), fed to viewport-dependent scripts. */
19433
19952
  this.visibleRange = null;
@@ -19526,6 +20045,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19526
20045
  this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
19527
20046
  this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
19528
20047
  this.renderer.onPriceStyleChange?.((style) => this.syncPriceStyle(style));
20048
+ this.priceStyleWillChangeUnsub = this.renderer.onPriceStyleWillChange?.((from, to) => this.events.emit("priceStyle:change", { from, to })) ?? null;
19529
20049
  const initialStyle = this.renderer.readFeature("priceStyle");
19530
20050
  if (typeof initialStyle === "string") this.priceStyle = initialStyle;
19531
20051
  this.barTransform = barTransformFor(initialStyle);
@@ -20812,20 +21332,26 @@ var EngineOrchestrator = class _EngineOrchestrator {
20812
21332
  /**
20813
21333
  * IndicatorController: replace a SCRIPT indicator's source in place (see
20814
21334
  * {@link IndicatorHandle.updateCode}). Prepare-first: the running session keeps
20815
- * computing until the new source has compiled, so a broken edit costs the user
20816
- * nothing but an `error` event. Natives have no script — warn and leave them alone.
21335
+ * computing until the new source has compiled; then the new code runs on TRIAL until
21336
+ * its first model lands, and a failure before that puts the previous code back.
21337
+ * Natives have no script — warn and leave them alone.
20817
21338
  */
20818
21339
  updateCode(id, source) {
20819
21340
  const record = this.registry.get(id);
20820
21341
  const handle = this.handles.get(id);
20821
- if (!record || !handle) return;
21342
+ if (!record || !handle) return Promise.resolve({ ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator is no longer on the chart.`) });
20822
21343
  if (record.native) {
20823
21344
  console.warn(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`);
20824
- return;
21345
+ return Promise.resolve({ ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`) });
20825
21346
  }
20826
- if (source === record.source && record.pendingSource === void 0) return;
20827
- record.pendingSource = source;
20828
- void this.swapSource(id, source, handle);
21347
+ const busy = record.pendingSource !== void 0 || record.codeTrial !== void 0;
21348
+ if (!busy && source === record.source) return Promise.resolve({ ok: true, error: null });
21349
+ return new Promise((resolve) => {
21350
+ (record.codeWaiters ?? (record.codeWaiters = [])).push(resolve);
21351
+ if (record.pendingSource === source || record.pendingSource === void 0 && record.codeTrial && record.source === source) return;
21352
+ record.pendingSource = source;
21353
+ void this.swapSource(id, source, handle);
21354
+ });
20829
21355
  }
20830
21356
  async swapSource(id, source, handle) {
20831
21357
  const engine = this.registry.get(id)?.engine ?? this.engineFor(this.registry.get(id)?.options?.language);
@@ -20836,26 +21362,41 @@ var EngineOrchestrator = class _EngineOrchestrator {
20836
21362
  const record2 = this.registry.get(id);
20837
21363
  if (record2?.pendingSource === source) {
20838
21364
  record2.pendingSource = void 0;
20839
- this.fail(id, handle, err);
21365
+ this.rejectCode(id, record2, handle, err);
20840
21366
  }
20841
21367
  return;
20842
21368
  }
20843
21369
  const record = this.registry.get(id);
20844
21370
  if (!record || record.pendingSource !== source) return;
20845
21371
  record.pendingSource = void 0;
21372
+ const previous = record.codeTrial?.previous ?? {
21373
+ source: record.source,
21374
+ engine: record.engine,
21375
+ prepared: record.prepared,
21376
+ inputValues: record.inputValues,
21377
+ propValues: record.propValues,
21378
+ inputs: [...handle.inputs],
21379
+ props: [...handle.props]
21380
+ };
21381
+ record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues, record.prepared?.inputs);
21382
+ record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues, record.prepared?.props);
20846
21383
  record.source = source;
20847
21384
  record.engine = engine;
20848
21385
  record.prepared = prepared;
20849
21386
  handle.setSource(source);
20850
- record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues);
20851
- record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues);
20852
21387
  handle.setSchema(prepared.inputs);
20853
21388
  handle.setPropsSchema(prepared.props ?? []);
20854
21389
  record.session?.stop();
20855
21390
  record.session = void 0;
20856
21391
  record.pendingStructural = true;
20857
21392
  record.pendingCause = "code";
20858
- if (record.hidden) return;
21393
+ if (record.hidden) {
21394
+ record.codeTrial = void 0;
21395
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
21396
+ this.settleCode(record, { ok: true, error: null });
21397
+ return;
21398
+ }
21399
+ record.codeTrial = { previous };
20859
21400
  if (record.renderHandle) {
20860
21401
  this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
20861
21402
  this.setLoading(record, true);
@@ -20863,6 +21404,46 @@ var EngineOrchestrator = class _EngineOrchestrator {
20863
21404
  this.mountLoadingPlaceholder(id, record);
20864
21405
  }
20865
21406
  this.executeIndicator(id, handle);
21407
+ if (this.bars.length === 0 && record.codeTrial) {
21408
+ record.codeTrial = void 0;
21409
+ this.settleCode(record, { ok: true, error: null });
21410
+ }
21411
+ }
21412
+ /** Resolve every `updateCode` caller waiting on this record's chain of edits. */
21413
+ settleCode(record, result) {
21414
+ const waiters = record.codeWaiters;
21415
+ record.codeWaiters = void 0;
21416
+ if (waiters) for (const resolve of waiters) resolve(result);
21417
+ }
21418
+ /**
21419
+ * An update failed — at prepare, or on trial before its first model. A trial puts the
21420
+ * previous code back (source, engine, prepared script, values, schema) and restarts it;
21421
+ * its visuals were never dropped, so the chart keeps painting throughout. Then the
21422
+ * failure reports the usual way and every waiting caller learns it.
21423
+ */
21424
+ rejectCode(id, record, handle, err) {
21425
+ const trial = record.codeTrial;
21426
+ record.codeTrial = void 0;
21427
+ this.fail(id, handle, err);
21428
+ if (trial) {
21429
+ const p = trial.previous;
21430
+ record.session?.stop();
21431
+ record.session = void 0;
21432
+ record.source = p.source;
21433
+ record.engine = p.engine;
21434
+ record.prepared = p.prepared;
21435
+ record.inputValues = p.inputValues;
21436
+ record.propValues = p.propValues;
21437
+ handle.setSource(p.source);
21438
+ handle.setSchema(p.inputs);
21439
+ handle.setPropsSchema(p.props);
21440
+ record.pendingStructural = true;
21441
+ record.pendingCause = void 0;
21442
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
21443
+ if (!record.hidden) this.executeIndicator(id, handle);
21444
+ }
21445
+ const error = err instanceof Error ? err : new Error(String(err));
21446
+ this.settleCode(record, { ok: false, error });
20866
21447
  }
20867
21448
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
20868
21449
  /** Live handles of every indicator on the chart (script + native), insertion order. */
@@ -20877,6 +21458,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20877
21458
  removeIndicator(id) {
20878
21459
  const record = this.registry.remove(id);
20879
21460
  if (record?.native?.type === "volume") this.volumeOptedOut = true;
21461
+ if (record) this.settleCode(record, { ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator was removed.`) });
20880
21462
  record?.session?.stop();
20881
21463
  if (record?.native?.started) record.native.instance.stop();
20882
21464
  this.handles.delete(id);
@@ -20904,6 +21486,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
20904
21486
  if (!visible) {
20905
21487
  record.session?.stop();
20906
21488
  record.session = void 0;
21489
+ if (record.codeTrial) {
21490
+ record.codeTrial = void 0;
21491
+ this.settleCode(record, { ok: true, error: null });
21492
+ }
20907
21493
  if (record.native?.started) record.native.instance.suspend();
20908
21494
  if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
20909
21495
  else if (record.native) this.mountHiddenNativeRow(id, record);
@@ -20990,7 +21576,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
20990
21576
  if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
20991
21577
  this.viewportUnsub?.();
20992
21578
  this.paneActionUnsub?.();
21579
+ this.priceStyleWillChangeUnsub?.();
21580
+ this.priceStyleWillChangeUnsub = null;
20993
21581
  for (const record of this.registry.all()) {
21582
+ this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
20994
21583
  record.session?.stop();
20995
21584
  if (record.native?.started) record.native.instance.stop();
20996
21585
  }
@@ -21059,7 +21648,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
21059
21648
  const record = this.registry.get(id);
21060
21649
  if (!record || !record.engine || !record.prepared || record.hidden) return;
21061
21650
  const mode = this.config.live && !record.prepared.reactsToViewport && record.engine.capabilities.streaming ? "live" : "static";
21062
- record.session = record.engine.execute(
21651
+ const prepared = record.prepared;
21652
+ const session = record.engine.execute(
21063
21653
  {
21064
21654
  prepared: record.prepared,
21065
21655
  market: this.market(),
@@ -21080,6 +21670,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
21080
21670
  const cause = record.pendingCause ?? "history";
21081
21671
  if (!this.applyModel(id, model)) return;
21082
21672
  record.pendingCause = void 0;
21673
+ if (record.codeTrial) {
21674
+ record.codeTrial = void 0;
21675
+ this.settleCode(record, { ok: true, error: null });
21676
+ }
21083
21677
  this.emitContextChanged(id);
21084
21678
  this.emitScriptRun(id, cause, first);
21085
21679
  },
@@ -21090,9 +21684,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
21090
21684
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
21091
21685
  },
21092
21686
  onWarning: (w) => this.events.emit("warning", w),
21093
- onError: (err) => this.fail(id, handle, err)
21687
+ onError: (err) => {
21688
+ if (record.codeTrial) this.rejectCode(id, record, handle, err);
21689
+ else this.fail(id, handle, err);
21690
+ }
21094
21691
  }
21095
21692
  );
21693
+ if (record.prepared !== prepared) {
21694
+ session.stop();
21695
+ return;
21696
+ }
21697
+ record.session = session;
21096
21698
  this.emitContextChanged(id);
21097
21699
  }
21098
21700
  /**
@@ -21593,7 +22195,7 @@ function yieldToPaint() {
21593
22195
  }
21594
22196
  return new Promise((resolve) => setTimeout(resolve, 0));
21595
22197
  }
21596
- function valuesOnSchema(schema, previous) {
22198
+ function valuesOnSchema(schema, previous, oldSchema = []) {
21597
22199
  const out = {};
21598
22200
  const declared = /* @__PURE__ */ new Set();
21599
22201
  for (const s of schema) {
@@ -21601,9 +22203,22 @@ function valuesOnSchema(schema, previous) {
21601
22203
  declared.add(s.key);
21602
22204
  declared.add(s.title);
21603
22205
  }
21604
- for (const [k, v] of Object.entries(previous)) if (declared.has(k)) out[k] = v;
22206
+ const oldDefaults = /* @__PURE__ */ new Map();
22207
+ for (const s of oldSchema) {
22208
+ oldDefaults.set(s.key, s.defval);
22209
+ if (!oldDefaults.has(s.title)) oldDefaults.set(s.title, s.defval);
22210
+ }
22211
+ for (const [k, v] of Object.entries(previous)) {
22212
+ if (!declared.has(k)) continue;
22213
+ if (oldDefaults.has(k) && sameInputValue(oldDefaults.get(k), v)) continue;
22214
+ out[k] = v;
22215
+ }
21605
22216
  return out;
21606
22217
  }
22218
+ function sameInputValue(a, b) {
22219
+ if (a === b) return true;
22220
+ return typeof a === "object" && a !== null && typeof b === "object" && b !== null && JSON.stringify(a) === JSON.stringify(b);
22221
+ }
21607
22222
  function blankedModel(model) {
21608
22223
  return {
21609
22224
  ...model,
@@ -23168,7 +23783,10 @@ var LEGEND_ICON_PX2 = 16;
23168
23783
  var LEGEND_CTL_PX = 18;
23169
23784
  var LEGEND_ROW_PAD_Y = 2;
23170
23785
  var LEGEND_ROW_PAD_X = 6;
23786
+ var LEGEND_LEFT_PX = 10;
23787
+ var LEGEND_SCALE_GAP_PX = 6;
23171
23788
  var LEGEND_TITLE_VALUES_GAP_PX = 6;
23789
+ var LEGEND_VALUES_GAP_PX = 5;
23172
23790
  var LEGEND_TITLE_STATUS_GAP_PX = 8;
23173
23791
  var LEGEND_ACTION_GAP_PX = 6;
23174
23792
  var LEGEND_CTL_CSS = `cursor:pointer;display:inline-flex;align-items:center;justify-content:center;border:none;line-height:0;padding:0;width:${LEGEND_CTL_PX}px;height:${LEGEND_CTL_PX}px;border-radius:3px;box-sizing:border-box;flex:none;`;
@@ -23454,14 +24072,15 @@ var InputsUI = class {
23454
24072
  row.valuesEl.replaceChildren();
23455
24073
  return;
23456
24074
  }
23457
- row.valuesEl.style.display = "inline-flex";
24075
+ row.valuesEl.style.display = "block";
23458
24076
  row.valuesEl.replaceChildren();
23459
- for (const v of row.plotValues) {
24077
+ row.plotValues.forEach((v, i) => {
23460
24078
  const span = document.createElement("span");
23461
24079
  span.textContent = v.value;
23462
24080
  span.style.color = v.color;
24081
+ if (i > 0) span.style.marginLeft = `${LEGEND_VALUES_GAP_PX}px`;
23463
24082
  row.valuesEl.appendChild(span);
23464
- }
24083
+ });
23465
24084
  }
23466
24085
  // ── legend move/merge (menu + drag) ─────────────────────────────────────
23467
24086
  /** Open the "Move to" menu for a row, anchored under its move button. */
@@ -23562,7 +24181,7 @@ var InputsUI = class {
23562
24181
  if (!lg) {
23563
24182
  lg = document.createElement("div");
23564
24183
  lg.dataset.velaPane = paneId;
23565
- lg.style.cssText = "position:absolute;left:10px;z-index:5;display:flex;flex-direction:column;align-items:flex-start;gap:0;pointer-events:none;font:12px -apple-system,Segoe UI,sans-serif;";
24184
+ lg.style.cssText = `position:absolute;left:${LEGEND_LEFT_PX}px;z-index:5;display:flex;flex-direction:column;align-items:flex-start;gap:0;pointer-events:none;font:12px -apple-system,Segoe UI,sans-serif;`;
23566
24185
  applyChromeTokens(lg, this.theme);
23567
24186
  this.positionLegend(lg, paneId);
23568
24187
  this.container.appendChild(lg);
@@ -23574,6 +24193,7 @@ var InputsUI = class {
23574
24193
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
23575
24194
  lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
23576
24195
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
24196
+ lg.style.maxWidth = `calc(100% - ${(bounds.rightAxis ?? 0) + LEGEND_LEFT_PX + LEGEND_SCALE_GAP_PX}px)`;
23577
24197
  const collapsed = this.paneCollapse.has(paneId);
23578
24198
  const masterId = this.paneCollapse.get(paneId) ?? null;
23579
24199
  for (const row of this.rows.values()) {
@@ -23665,7 +24285,7 @@ var InputsUI = class {
23665
24285
  }
23666
24286
  ensureDialogStyles();
23667
24287
  const el = document.createElement("div");
23668
- el.style.cssText = `pointer-events:auto;display:flex;align-items:center;background:${this.idleRowFill()};border-radius:4px;padding:${LEGEND_ROW_PAD_Y}px ${LEGEND_ROW_PAD_X}px;margin-left:-${LEGEND_ROW_PAD_X}px;min-height:${LEGEND_ROW_MIN_H}px;box-sizing:border-box;color:${this.theme.textColor};user-select:none;-webkit-user-select:none;`;
24288
+ el.style.cssText = `pointer-events:auto;display:flex;align-items:center;background:${this.idleRowFill()};border-radius:4px;padding:${LEGEND_ROW_PAD_Y}px ${LEGEND_ROW_PAD_X}px;margin-left:-${LEGEND_ROW_PAD_X}px;max-width:calc(100% + ${LEGEND_ROW_PAD_X}px);min-height:${LEGEND_ROW_MIN_H}px;box-sizing:border-box;color:${this.theme.textColor};user-select:none;-webkit-user-select:none;`;
23669
24289
  el.addEventListener("mouseleave", () => {
23670
24290
  if (this.selectedId !== id) this.setRowHighlighted(id, false);
23671
24291
  });
@@ -23693,7 +24313,7 @@ var InputsUI = class {
23693
24313
  const statusEl = document.createElement("span");
23694
24314
  statusEl.style.cssText = "display:none;box-sizing:border-box;flex:none;";
23695
24315
  const titleWrap = document.createElement("span");
23696
- titleWrap.style.cssText = "white-space:nowrap;";
24316
+ titleWrap.style.cssText = "white-space:nowrap;min-width:0;overflow:hidden;text-overflow:ellipsis;";
23697
24317
  titleWrap.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
23698
24318
  const titleEl = document.createElement("span");
23699
24319
  titleEl.textContent = title;
@@ -23711,7 +24331,7 @@ var InputsUI = class {
23711
24331
  el.appendChild(calloutsEl);
23712
24332
  el.appendChild(statusEl);
23713
24333
  const valuesEl = document.createElement("span");
23714
- valuesEl.style.cssText = `display:none;align-items:center;gap:5px;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
24334
+ valuesEl.style.cssText = `display:none;flex:1 1 0%;min-width:0;overflow:hidden;text-overflow:ellipsis;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
23715
24335
  el.appendChild(valuesEl);
23716
24336
  const controlsEl = document.createElement("span");
23717
24337
  controlsEl.style.cssText = `display:none;align-items:center;gap:${LEGEND_ACTION_GAP_PX}px;flex:none;margin-left:${LEGEND_ACTION_GAP_PX}px;`;
@@ -24533,15 +25153,23 @@ var Batch = class {
24533
25153
  this.seg(x + w, y + h, x, y + h, lineWidth, c);
24534
25154
  this.seg(x, y + h, x, y, lineWidth, c);
24535
25155
  }
24536
- /** Filled circle (triangle fan). */
25156
+ /**
25157
+ * Filled circle (triangle fan), feathered like a line: the rim is padded by {@link AA_PAD} and
25158
+ * edgeDist runs from 0 at the center to the padded radius at the rim, with edgeHalf = r. The
25159
+ * distance grows linearly along each spoke, so the shader's line feather gives a round soft edge.
25160
+ */
24537
25161
  circle(cx, cy, r, c, segments = 16) {
24538
- let px = cx + r;
25162
+ const ext = r + AA_PAD;
25163
+ let px = cx + ext;
24539
25164
  let py = cy;
25165
+ this.ensure(segments * 3);
24540
25166
  for (let i = 1; i <= segments; i += 1) {
24541
25167
  const a = i / segments * Math.PI * 2;
24542
- const nx = cx + Math.cos(a) * r;
24543
- const ny = cy + Math.sin(a) * r;
24544
- this.tri(cx, cy, c, px, py, c, nx, ny, c);
25168
+ const nx = cx + Math.cos(a) * ext;
25169
+ const ny = cy + Math.sin(a) * ext;
25170
+ this.v(cx, cy, c, 0, r);
25171
+ this.v(px, py, c, ext, r);
25172
+ this.v(nx, ny, c, ext, r);
24545
25173
  px = nx;
24546
25174
  py = ny;
24547
25175
  }
@@ -24722,7 +25350,7 @@ var WebGL2Backend = class {
24722
25350
  }
24723
25351
  mount(canvas) {
24724
25352
  this.canvas = canvas;
24725
- this.gl = canvas.getContext("webgl2", { alpha: true, antialias: true, depth: false, stencil: false, premultipliedAlpha: true });
25353
+ this.gl = canvas.getContext("webgl2", { alpha: true, antialias: false, depth: false, stencil: false, premultipliedAlpha: true });
24726
25354
  if (!this.gl) return;
24727
25355
  if (!this.initGL()) {
24728
25356
  this.gl = null;
@@ -25947,6 +26575,11 @@ var InputController = class {
25947
26575
  /** When true (the default), the separators between stacked panes are draggable to
25948
26576
  * resize the panes above/below. When false, a press there is a normal data-area pan. */
25949
26577
  this.paneResize = true;
26578
+ /** When true (the default), the user drawings take part in pointer input: they claim a
26579
+ * press over a drawing or with a tool armed, and see hover moves. When false, every press
26580
+ * is a plain pan/click and hover leaves them alone; a drawing gesture already under way
26581
+ * still finishes. */
26582
+ this.drawings = true;
25950
26583
  this.el = null;
25951
26584
  this.dragging = false;
25952
26585
  this.moved = false;
@@ -26010,18 +26643,18 @@ var InputController = class {
26010
26643
  this.onModifier = (e) => {
26011
26644
  if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
26012
26645
  if (Number.isNaN(this.cursorX)) return;
26013
- if (this.dragging && this.region !== "drawing") return;
26646
+ if (this.dragging ? this.region !== "drawing" : !this.drawings) return;
26014
26647
  this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
26015
26648
  };
26016
26649
  this.onDown = (e) => {
26017
26650
  if (e.button === 1) {
26018
26651
  const { x: x2, y: y2 } = this.local(e);
26019
- this.middleDeleted = this.deps.drawingsDeleteAt?.(x2, y2) ?? false;
26652
+ this.middleDeleted = this.drawings && (this.deps.drawingsDeleteAt?.(x2, y2) ?? false);
26020
26653
  if (this.middleDeleted) e.preventDefault();
26021
26654
  return;
26022
26655
  }
26023
26656
  if (e.button === 2) {
26024
- this.rightCancelled = this.deps.drawingsCancelPlacement?.() ?? false;
26657
+ this.rightCancelled = this.drawings && (this.deps.drawingsCancelPlacement?.() ?? false);
26025
26658
  if (this.rightCancelled) e.preventDefault();
26026
26659
  return;
26027
26660
  }
@@ -26043,20 +26676,20 @@ var InputController = class {
26043
26676
  this.startY = y;
26044
26677
  this.prevPressDrawing = this.lastPressDrawing;
26045
26678
  this.lastPressDrawing = false;
26046
- if (this.deps.drawingsClaim?.(x, y)) {
26679
+ if (this.drawings && this.deps.drawingsClaim?.(x, y)) {
26047
26680
  this.region = "drawing";
26048
26681
  this.lastPressDrawing = true;
26049
26682
  this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
26050
26683
  this.capture(e.pointerId);
26051
26684
  return;
26052
26685
  }
26053
- if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
26686
+ if (this.drawings && e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
26054
26687
  this.region = "drawing";
26055
26688
  this.lastPressDrawing = true;
26056
26689
  this.capture(e.pointerId);
26057
26690
  return;
26058
26691
  }
26059
- if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
26692
+ if (this.drawings && (e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
26060
26693
  this.region = "drawing";
26061
26694
  this.lastPressDrawing = true;
26062
26695
  this.capture(e.pointerId);
@@ -26133,7 +26766,7 @@ var InputController = class {
26133
26766
  const drawCursor = this.deps.drawingsCursor?.(x, y);
26134
26767
  const r = this.regionAt(x, y);
26135
26768
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
26136
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
26769
+ if (this.drawings) this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
26137
26770
  }
26138
26771
  if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
26139
26772
  };
@@ -26174,7 +26807,7 @@ var InputController = class {
26174
26807
  this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
26175
26808
  } else if (tapRelease && this.region === "data") {
26176
26809
  this.deps.onClick(x, y);
26177
- } else if (this.dragging && this.region === "data") {
26810
+ } else if (this.dragging && this.region === "data" && wasTouch) {
26178
26811
  const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
26179
26812
  if (!stale && Math.abs(this.vx) > FLING_MIN_SPEED) {
26180
26813
  const pitch = this.deps.getCoords().pxPerBar();
@@ -26375,7 +27008,7 @@ var InputController = class {
26375
27008
  }
26376
27009
  /** Shared double-click / double-tap routing, by the region under the point. */
26377
27010
  doubleActivate(x, y) {
26378
- if (this.deps.drawingsDblClick?.(x, y)) return;
27011
+ if (this.drawings && this.deps.drawingsDblClick?.(x, y)) return;
26379
27012
  const region = this.regionAt(x, y);
26380
27013
  if (region === "price") this.deps.resetPriceScale(x, y);
26381
27014
  else if (region === "separator") this.deps.resetPaneSize(y);
@@ -28389,10 +29022,13 @@ function priceTicks(min, max, target = 6) {
28389
29022
  const norm = raw / mag;
28390
29023
  const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
28391
29024
  const decimals = Math.max(0, -Math.floor(Math.log10(step)) + 1);
28392
- const out = [];
28393
29025
  const start = Math.ceil(min / step) * step;
28394
- for (let v = start; v <= max + step * 1e-6; v += step) {
28395
- out.push(Number(v.toFixed(decimals)));
29026
+ if (!(step > 0) || start + step === start) return [];
29027
+ const count = Math.floor((max - start) / step + 1e-6) + 1;
29028
+ if (!(count > 0) || count > 1e3) return [];
29029
+ const out = [];
29030
+ for (let i = 0; i < count; i += 1) {
29031
+ out.push(Number((start + i * step).toFixed(decimals)));
28396
29032
  }
28397
29033
  return out;
28398
29034
  }
@@ -28590,6 +29226,100 @@ function calendarTicks(zFrom, zTo, months, offsetMs) {
28590
29226
  return out;
28591
29227
  }
28592
29228
 
29229
+ // src/renderers/native/chrome/priceAxisTicks.ts
29230
+ var PriceAxisTickSource = class {
29231
+ constructor() {
29232
+ this.fn = null;
29233
+ this.memo = /* @__PURE__ */ new WeakMap();
29234
+ this.warned = false;
29235
+ }
29236
+ /** The host function, or null for the built-in ticks. */
29237
+ get hook() {
29238
+ return this.fn;
29239
+ }
29240
+ /** Set (or clear) the host function. Drops every memoized result, so setting the same
29241
+ * function again recomputes — how a host refreshes ticks that depend on its own state. */
29242
+ setHook(fn) {
29243
+ this.fn = fn;
29244
+ this.memo = /* @__PURE__ */ new WeakMap();
29245
+ }
29246
+ /** The pane's master-scale ticks for this frame. An unscaled pane (`axisFormat: 'none'`)
29247
+ * gets none, and the host is never asked for it. */
29248
+ ticksFor(scene, pane, coords) {
29249
+ const format = pane.axisFormat;
29250
+ if (format === "none") return [];
29251
+ const pct = percentScaleFor(scene, pane);
29252
+ const height = pane.bounds.height;
29253
+ const fn = this.fn;
29254
+ if (!fn || !(height > 0)) return paneAxisTicks(pane.scale, height, pct, scene.priceMintick, format);
29255
+ const s = pane.scale;
29256
+ const m = this.memo.get(pane);
29257
+ const fontSize = scene.style.fontSize;
29258
+ if (m && m.kind === pane.kind && m.min === s.min && m.max === s.max && m.log === !!s.log && m.invert === !!s.invert && m.height === height && m.baseline === pct?.baseline && m.indexed === !!pct?.indexed && m.mintick === scene.priceMintick && m.format === format && m.fontSize === fontSize) {
29259
+ return m.ticks;
29260
+ }
29261
+ const defaults2 = paneAxisTicks(s, height, pct, scene.priceMintick, format);
29262
+ const ticks = this.callHook(fn, scene, pane, coords, defaults2, pct?.baseline, pct ? pct.indexed ? "indexed" : "percent" : "price");
29263
+ this.memo.set(pane, {
29264
+ kind: pane.kind,
29265
+ min: s.min,
29266
+ max: s.max,
29267
+ log: !!s.log,
29268
+ invert: !!s.invert,
29269
+ height,
29270
+ baseline: pct?.baseline,
29271
+ indexed: !!pct?.indexed,
29272
+ mintick: scene.priceMintick,
29273
+ format,
29274
+ fontSize,
29275
+ ticks
29276
+ });
29277
+ return ticks;
29278
+ }
29279
+ callHook(fn, scene, pane, coords, defaults2, baseline, mode) {
29280
+ const scale = { min: pane.scale.min, max: pane.scale.max, log: !!pane.scale.log, invert: !!pane.scale.invert };
29281
+ const bounds = { top: 0, height: pane.bounds.height };
29282
+ const ctx = {
29283
+ pane: { id: pane.id, kind: pane.kind },
29284
+ min: Math.min(scale.min, scale.max),
29285
+ max: Math.max(scale.min, scale.max),
29286
+ log: !!scale.log,
29287
+ height: bounds.height,
29288
+ mode,
29289
+ baseline,
29290
+ mintick: scene.priceMintick,
29291
+ fontSize: scene.style.fontSize,
29292
+ priceToY: (price) => coords.priceToY(price, scale, bounds),
29293
+ defaults: defaults2
29294
+ };
29295
+ let out;
29296
+ try {
29297
+ out = fn(ctx);
29298
+ } catch (err) {
29299
+ this.warnOnce("threw", err);
29300
+ return defaults2;
29301
+ }
29302
+ if (out == null) return defaults2;
29303
+ if (!Array.isArray(out)) {
29304
+ this.warnOnce("returned a non-array", out);
29305
+ return defaults2;
29306
+ }
29307
+ const ticks = [];
29308
+ for (const t of out) {
29309
+ if (!t || typeof t !== "object") continue;
29310
+ const { price, label, major } = t;
29311
+ if (typeof price !== "number" || !Number.isFinite(price) || typeof label !== "string") continue;
29312
+ ticks.push(typeof major === "boolean" ? { price, label, major } : { price, label });
29313
+ }
29314
+ return ticks;
29315
+ }
29316
+ warnOnce(what, detail) {
29317
+ if (this.warned) return;
29318
+ this.warned = true;
29319
+ console.warn(`[vela] the priceAxisTicks function ${what} \u2014 the default ticks are used instead.`, detail);
29320
+ }
29321
+ };
29322
+
28593
29323
  // src/renderers/native/chrome/countdown.ts
28594
29324
  function countdownText(barOpen, barMs, now2) {
28595
29325
  if (!(barMs > 0)) return null;
@@ -28942,8 +29672,11 @@ function standaloneSvg(markup, ink, px) {
28942
29672
  }
28943
29673
 
28944
29674
  // src/renderers/native/chrome/ChromeRenderer.ts
29675
+ var MINOR_LABEL_ALPHA = 0.5;
28945
29676
  var ChromeRenderer = class {
28946
- constructor() {
29677
+ /** `axisTicks` is shared with the backdrop layer, so price labels and gridlines agree. */
29678
+ constructor(axisTicks = new PriceAxisTickSource()) {
29679
+ this.axisTicks = axisTicks;
28947
29680
  this.canvas = null;
28948
29681
  this.ctx = null;
28949
29682
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
@@ -29048,7 +29781,8 @@ var ChromeRenderer = class {
29048
29781
  this.drawTimeAxis(ctx, scene, coords, theme, dataW, dataH, fullH);
29049
29782
  this.drawMarkLane(ctx, scene, coords, theme, dataW, dataH);
29050
29783
  }
29051
- /** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge. */
29784
+ /** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge,
29785
+ * but clipped to the data area so a glyph on the edge bar never paints over the price scale. */
29052
29786
  drawMarkLane(ctx, scene, coords, theme, dataW, dataH) {
29053
29787
  if (!scene.marks.visible || scene.timelineMarks.length === 0) {
29054
29788
  this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
@@ -29066,6 +29800,10 @@ var ChromeRenderer = class {
29066
29800
  expanded: scene.marksExpandedStack
29067
29801
  });
29068
29802
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
29803
+ ctx.save();
29804
+ ctx.beginPath();
29805
+ ctx.rect(0, 0, dataW, dataH);
29806
+ ctx.clip();
29069
29807
  paintMarkLane(ctx, this.markLayout, {
29070
29808
  axisY: dataH,
29071
29809
  background: theme.background,
@@ -29079,6 +29817,7 @@ var ChromeRenderer = class {
29079
29817
  flashKey: scene.marksFlash && scene.marksFlash.until > nowMs ? scene.marksFlash.key : null,
29080
29818
  nowMs
29081
29819
  });
29820
+ ctx.restore();
29082
29821
  }
29083
29822
  barTimes(scene) {
29084
29823
  if (this.barTimesSrc !== scene.bars || this.barTimesCache.length !== scene.bars.length) {
@@ -29135,13 +29874,19 @@ var ChromeRenderer = class {
29135
29874
  if (!scene.showAxisLabels) return;
29136
29875
  ctx.fillStyle = this.axisTextColor;
29137
29876
  ctx.textAlign = "left";
29877
+ const font = `${scene.style.fontSize}px ${theme.fontFamily}`;
29878
+ const majorFont = `600 ${font}`;
29879
+ const alpha = ctx.globalAlpha;
29138
29880
  for (const pane of panes) {
29139
29881
  if (pane.collapsed) continue;
29140
- const pct = percentScaleFor(scene, pane);
29141
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct, scene.priceMintick, pane.axisFormat)) {
29882
+ for (const t of this.axisTicks.ticksFor(scene, pane, coords)) {
29142
29883
  const y = coords.priceToY(t.price, pane.scale, pane.bounds);
29143
29884
  if (y < pane.bounds.top + 6 || y > pane.bounds.top + pane.bounds.height - 4) continue;
29885
+ if (t.major === true) ctx.font = majorFont;
29886
+ else if (t.major === false) ctx.globalAlpha = alpha * MINOR_LABEL_ALPHA;
29144
29887
  ctx.fillText(t.label, dataW + 6, y);
29888
+ if (t.major === true) ctx.font = font;
29889
+ else if (t.major === false) ctx.globalAlpha = alpha;
29145
29890
  }
29146
29891
  if (pane.axisBands) {
29147
29892
  for (const b of pane.axisBands) {
@@ -29383,15 +30128,23 @@ var CrosshairRenderer = class {
29383
30128
  constructor() {
29384
30129
  this.canvas = null;
29385
30130
  this.ctx = null;
30131
+ this.shade = null;
29386
30132
  }
29387
30133
  mount(canvas) {
29388
30134
  this.canvas = canvas;
29389
30135
  this.ctx = canvas.getContext("2d");
29390
30136
  }
30137
+ /** The plot area the last frame veiled for `crosshairOverride.shadeRight` — from the
30138
+ * picked bar's right edge to the price scale, full plot height (the union when both the
30139
+ * local and a synced crosshair veil) — or null when no veil was painted. */
30140
+ get shadeRightArea() {
30141
+ return this.shade;
30142
+ }
29391
30143
  /** Clear the cursor canvas and (re)draw the crosshair lines + axis chips. The optional
29392
30144
  * `separatorHoverY` highlights the draggable pane separator under the cursor;
29393
30145
  * `external` is a SYNCED ghost crosshair (another chart's pointer, pixel-resolved). */
29394
30146
  render(scene, coords, theme, separatorHoverY = null, external = null) {
30147
+ this.shade = null;
29395
30148
  const ctx = this.ctx;
29396
30149
  const canvas = this.canvas;
29397
30150
  if (!ctx || !canvas) return;
@@ -29414,12 +30167,7 @@ var CrosshairRenderer = class {
29414
30167
  const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
29415
30168
  if (vertical && ov?.shadeRight) {
29416
30169
  const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
29417
- if (from < dataW) {
29418
- ctx.fillStyle = ov.shadeRight.color;
29419
- ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
29420
- ctx.fillRect(from, 0, dataW - from, dataH);
29421
- ctx.globalAlpha = 1;
29422
- }
30170
+ if (from < dataW) this.paintShade(ctx, ov.shadeRight, from, dataW, dataH);
29423
30171
  }
29424
30172
  ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
29425
30173
  ctx.lineWidth = ov?.width ?? cs.width;
@@ -29455,6 +30203,7 @@ var CrosshairRenderer = class {
29455
30203
  destroy() {
29456
30204
  this.canvas = null;
29457
30205
  this.ctx = null;
30206
+ this.shade = null;
29458
30207
  }
29459
30208
  /** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
29460
30209
  * CONTAINING the foreign time (+ horizontal line when a comparable price came
@@ -29474,12 +30223,7 @@ var CrosshairRenderer = class {
29474
30223
  if (ov?.vertical === false) return;
29475
30224
  if (ov?.shadeRight) {
29476
30225
  const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
29477
- if (from < dataW) {
29478
- ctx.fillStyle = ov.shadeRight.color;
29479
- ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
29480
- ctx.fillRect(from, 0, dataW - from, dataH);
29481
- ctx.globalAlpha = 1;
29482
- }
30226
+ if (from < dataW) this.paintShade(ctx, ov.shadeRight, from, dataW, dataH);
29483
30227
  }
29484
30228
  if (ext.line === false || x < 0 || x > dataW) return;
29485
30229
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
@@ -29515,6 +30259,15 @@ var CrosshairRenderer = class {
29515
30259
  this.chip(ctx, x, dataH + 1, formatTimeStamp(ext.time, scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
29516
30260
  ctx.globalAlpha = 1;
29517
30261
  }
30262
+ /** Paint the `shadeRight` veil from `from` to the price scale and record the veiled area. */
30263
+ paintShade(ctx, shade, from, dataW, dataH) {
30264
+ ctx.fillStyle = shade.color;
30265
+ ctx.globalAlpha = shade.opacity ?? 1;
30266
+ ctx.fillRect(from, 0, dataW - from, dataH);
30267
+ ctx.globalAlpha = 1;
30268
+ const x = this.shade ? Math.min(this.shade.x, from) : from;
30269
+ this.shade = { x, width: dataW - x, height: dataH };
30270
+ }
29518
30271
  /** A soft band + a brighter crisp center line over the hovered separator, so it reads as
29519
30272
  * a draggable handle (the cursor is already `row-resize`). Spans the full width (data +
29520
30273
  * scale gutter) to match the separator itself. Theme-derived (textColor). */
@@ -32540,6 +33293,12 @@ var DrawingInteraction = class {
32540
33293
  activeDragId() {
32541
33294
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
32542
33295
  }
33296
+ /** The store drawings a live drag is moving — the pressed one and its riders — or none
33297
+ * before the drag passes the slop. A Ctrl-drag moves copies, so its sources stay put. */
33298
+ movingIds() {
33299
+ if (this.state.kind !== "pressed" || !this.state.moved || this.state.clones) return /* @__PURE__ */ new Set();
33300
+ return /* @__PURE__ */ new Set([this.state.id, ...this.state.riders.map((r) => r.id)]);
33301
+ }
32543
33302
  /** The drawing under a press that has not been released yet (drag or click undecided), or null. */
32544
33303
  pressedId() {
32545
33304
  return this.state.kind === "pressed" ? this.state.id : null;
@@ -32914,15 +33673,16 @@ var DrawingSettingsDialog = class {
32914
33673
  }
32915
33674
  open(drawing, actions, kind) {
32916
33675
  this.close();
32917
- const snapshot = clonePlain(drawing.serialize());
33676
+ const live = actions.resolve() ?? drawing;
33677
+ const snapshot = clonePlain(live.serialize());
32918
33678
  const grid = fieldGrid({ variant: "inputs" });
32919
33679
  grid.style.padding = "16px 20px";
32920
33680
  grid.style.overflowY = "auto";
32921
33681
  grid.style.overflowX = "hidden";
32922
33682
  grid.style.flex = "1 1 auto";
32923
- if (kind === "position" && drawing instanceof PositionTool) this.buildPosition(grid, drawing, actions);
32924
- else if (kind === "frvp" && drawing instanceof FixedRangeVolumeProfile) this.buildFrvp(grid, drawing, actions);
32925
- else if (kind === "levels") this.buildLevels(grid, drawing, actions);
33683
+ if (kind === "position" && live instanceof PositionTool) this.buildPosition(grid, live, actions);
33684
+ else if (kind === "frvp" && live instanceof FixedRangeVolumeProfile) this.buildFrvp(grid, live, actions);
33685
+ else if (kind === "levels") this.buildLevels(grid, live, actions);
32926
33686
  else return;
32927
33687
  const ui = new Dialog({
32928
33688
  host: this.host,
@@ -33198,6 +33958,17 @@ var DrawingSettingsDialog = class {
33198
33958
  }
33199
33959
  }));
33200
33960
  }
33961
+ if (drawing instanceof FibRetracement) {
33962
+ const fib = drawing;
33963
+ grid.appendChild(fieldRow({
33964
+ label: "Reverse",
33965
+ bool: true,
33966
+ toggle: {
33967
+ checked: fib.reverse,
33968
+ onChange: (v) => actions.patch({ reverse: v })
33969
+ }
33970
+ }));
33971
+ }
33201
33972
  levels.forEach((lv, i) => {
33202
33973
  const row = document.createElement("div");
33203
33974
  row.className = "vela-field-span";
@@ -33558,7 +34329,7 @@ var DrawingSettingsPopup = class {
33558
34329
  this.reposition();
33559
34330
  return;
33560
34331
  }
33561
- const text = drawing.text;
34332
+ const text = (actions.resolve() ?? drawing).text;
33562
34333
  const panel = document.createElement("div");
33563
34334
  panel.style.cssText = `padding:6px;border-top:1px solid var(--vela-border);display:flex;flex-direction:column;gap:4px;`;
33564
34335
  const ta = new TextArea({
@@ -34351,6 +35122,8 @@ var UserDrawingController = class {
34351
35122
  this.drawings = [];
34352
35123
  /** Ids painted on an interleave layer this frame — the top canvas paints only their handles. */
34353
35124
  this.sliced = /* @__PURE__ */ new Set();
35125
+ /** Interleaved drawings mid-drag: painted on the top canvas, left out of their slice. */
35126
+ this.lifted = /* @__PURE__ */ new Set();
34354
35127
  /** Cached slice canvases, keyed `paneId|beforeZ`, reused across frames to avoid churn. */
34355
35128
  this.sliceCache = /* @__PURE__ */ new Map();
34356
35129
  /** Series boundaries per pane as of the last `prepareSlices` — lets a repaint between data
@@ -34438,7 +35211,7 @@ var UserDrawingController = class {
34438
35211
  selectedIds: () => this.selectedIds,
34439
35212
  emit: (i) => this.emit(i),
34440
35213
  changed: () => {
34441
- this.invalidateSlices();
35214
+ this.syncLift();
34442
35215
  this.render();
34443
35216
  },
34444
35217
  openSettings: (id, x, y) => this.openSettingsById(id, x, y),
@@ -34727,6 +35500,12 @@ var UserDrawingController = class {
34727
35500
  this.render();
34728
35501
  }
34729
35502
  }
35503
+ /** Drop the hover (its handles) — the pointer no longer reaches the drawings. */
35504
+ clearHover() {
35505
+ if (this.hoveredId == null) return;
35506
+ this.hoveredId = null;
35507
+ this.render();
35508
+ }
34730
35509
  pointerUp(x, y, snap = "off") {
34731
35510
  if (this.eraserMode) {
34732
35511
  this.erasing = false;
@@ -35064,6 +35843,20 @@ var UserDrawingController = class {
35064
35843
  /** A drawing that paints inside the series stack changed (content, not hover): its pixels
35065
35844
  * live in the backend composite, so this layer alone can't show the change — ask for a
35066
35845
  * data frame, which re-runs `prepareSlices` before the backend composites. */
35846
+ /** A drawing being dragged leaves its interleave slice for the top canvas, so each pointer
35847
+ * move repaints one 2D layer instead of re-rasterizing and re-uploading a plot-sized slice
35848
+ * texture through the data frame. The slices rebuild once as the drag starts and once as it
35849
+ * ends (release, cancel), when the lifted set changes. */
35850
+ syncLift() {
35851
+ const moving = this.interaction.movingIds();
35852
+ let same = moving.size === this.lifted.size;
35853
+ if (same) {
35854
+ for (const id of moving) if (!this.lifted.has(id)) same = false;
35855
+ }
35856
+ if (same) return;
35857
+ this.lifted = new Set(moving);
35858
+ this.invalidateSlices();
35859
+ }
35067
35860
  invalidateSlices() {
35068
35861
  if (this.sliced.size > 0 || this.drawings.some((d) => this.isInterleaved(d))) this.deps.requestDataPaint();
35069
35862
  }
@@ -35082,7 +35875,7 @@ var UserDrawingController = class {
35082
35875
  const theme = this.deps.theme();
35083
35876
  const buckets = /* @__PURE__ */ new Map();
35084
35877
  for (const d of this.drawings) {
35085
- if (!d.visible) continue;
35878
+ if (!d.visible || this.lifted.has(d.id)) continue;
35086
35879
  const beforeZ = sliceKeyFor(d.zIndex, this.lastBounds.get(d.paneId) ?? []);
35087
35880
  if (beforeZ === null) continue;
35088
35881
  const key = `${d.paneId}|${beforeZ}`;
@@ -35134,8 +35927,9 @@ var UserDrawingController = class {
35134
35927
  mutedLabel: edited instanceof TextLabel ? edited.id : null
35135
35928
  };
35136
35929
  this.painter.seriesLook = this.deps.seriesLook();
35137
- this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
35138
- this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
35930
+ const onTop = (d) => !this.isInterleaved(d) || this.lifted.has(d.id);
35931
+ this.painter.paintAll(ctx, this.drawings.filter(onTop), proj, this.deps.theme(), targets);
35932
+ this.painter.paintHighlights(ctx, this.drawings.filter((d) => !onTop(d)), proj, handleIdsFor(targets));
35139
35933
  const clones = this.interaction.dragClones();
35140
35934
  if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
35141
35935
  this.layoutTextEditor();
@@ -35702,9 +36496,10 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
35702
36496
  consider(drawings.max);
35703
36497
  }
35704
36498
  if (min === Infinity || max === -Infinity) return { min: 0, max: 1 };
35705
- if (min === max) {
35706
- const pad = Math.abs(min) * 0.1 || 1;
35707
- return { min: min - pad, max: max + pad, log: log && min - pad > 0 };
36499
+ if (max - min <= Math.max(Math.abs(min), Math.abs(max)) * 1e-12) {
36500
+ const mid = (min + max) / 2;
36501
+ const pad = Math.abs(mid) * 0.1 || 1;
36502
+ return { min: mid - pad, max: mid + pad, log: log && mid - pad > 0 };
35708
36503
  }
35709
36504
  const content = Math.max(0.1, 1 - (margins.top + margins.bottom) / 100);
35710
36505
  const above = margins.top / 100 / content;
@@ -36208,7 +37003,9 @@ function bandEdgeSlot(logical) {
36208
37003
  return Math.ceil(logical) - 0.5;
36209
37004
  }
36210
37005
  var BackdropRenderer = class {
36211
- constructor() {
37006
+ /** `axisTicks` is shared with the chrome layer, so gridlines and price labels agree. */
37007
+ constructor(axisTicks = new PriceAxisTickSource()) {
37008
+ this.axisTicks = axisTicks;
36212
37009
  this.canvas = null;
36213
37010
  this.ctx = null;
36214
37011
  }
@@ -36285,9 +37082,8 @@ var BackdropRenderer = class {
36285
37082
  if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
36286
37083
  ctx.globalAlpha = gridAlpha;
36287
37084
  ctx.strokeStyle = horzColor;
36288
- const pct = percentScaleFor(scene, pane);
36289
37085
  ctx.beginPath();
36290
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct, void 0, pane.axisFormat)) {
37086
+ for (const t of this.axisTicks.ticksFor(scene, pane, coords)) {
36291
37087
  const y = Math.round(coords.priceToY(t.price, pane.scale, pane.bounds)) + 0.5;
36292
37088
  if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) continue;
36293
37089
  ctx.moveTo(0, y);
@@ -36774,8 +37570,16 @@ var NativeRenderer = class {
36774
37570
  this.toolbarGutter = 0;
36775
37571
  // px reserved on the left for the docked drawings toolbar (0 when hidden)
36776
37572
  this.mountContainer = null;
37573
+ /** The host-styleable twin of the `shadeRight` veil (`.vela-shade-right`), directly under
37574
+ * the cursor canvas; hidden whenever the crosshair layer paints no veil. */
37575
+ this.shadeRightEl = null;
37576
+ /** The veil geometry last written to {@link shadeRightEl} ('' = hidden) — the DOM is
37577
+ * touched only when it changes, never on a plain pointer move. */
37578
+ this.shadeRightSig = "";
36777
37579
  this.userDrawings = null;
36778
- this.backdropRenderer = new BackdropRenderer();
37580
+ /** One price-axis tick source for the gridlines (backdrop) and the labels (chrome). */
37581
+ this.priceAxisTicks = new PriceAxisTickSource();
37582
+ this.backdropRenderer = new BackdropRenderer(this.priceAxisTicks);
36779
37583
  this.volumeRenderer = new VolumeRenderer();
36780
37584
  /** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
36781
37585
  /**
@@ -36808,7 +37612,7 @@ var NativeRenderer = class {
36808
37612
  this.backendMode = "auto";
36809
37613
  this.glowAmount = 0;
36810
37614
  // WebGL2 neon-glow intensity (canvas2d ignores it)
36811
- this.chrome = new ChromeRenderer();
37615
+ this.chrome = new ChromeRenderer(this.priceAxisTicks);
36812
37616
  /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
36813
37617
  this.indicatorSlices = new IndicatorDrawingSlices();
36814
37618
  /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
@@ -36925,6 +37729,8 @@ var NativeRenderer = class {
36925
37729
  // pane just below it
36926
37730
  this.resizeSplitStart = null;
36927
37731
  // the two panes' shared span when the drag began
37732
+ /** The user drawings take part in pointer input (the runtime `drawingsInteractive` feature). */
37733
+ this.drawingsInteractive = true;
36928
37734
  this.hoverSeparatorY = null;
36929
37735
  // pixel y of the separator under the cursor (drives its hover highlight)
36930
37736
  // ── pane management (merge / reorder / collapse / maximize) ──
@@ -36980,8 +37786,9 @@ var NativeRenderer = class {
36980
37786
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
36981
37787
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
36982
37788
  this.priceStyleCbs = /* @__PURE__ */ new Set();
37789
+ this.priceStyleWillChangeCbs = /* @__PURE__ */ new Set();
36983
37790
  this.name = "native";
36984
- this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues", "crosshairOverride"];
37791
+ this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues", "crosshairOverride", "drawingsInteractive", "priceAxisTicks"];
36985
37792
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
36986
37793
  * so moving onto the button doesn't count as leaving). */
36987
37794
  this.onScrollProximityMove = (e) => {
@@ -37025,6 +37832,7 @@ var NativeRenderer = class {
37025
37832
  this.scene.priceStyle = opts.priceStyle;
37026
37833
  this.scene.basePainting = basePaintingOf(opts.priceStyle);
37027
37834
  this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
37835
+ if (typeof opts.priceAxisTicks === "function") this.priceAxisTicks.setHook(opts.priceAxisTicks);
37028
37836
  }
37029
37837
  this.theme = this.deriveTheme(DARK_THEME);
37030
37838
  }
@@ -37163,6 +37971,15 @@ var NativeRenderer = class {
37163
37971
  case "crosshairOverride":
37164
37972
  this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
37165
37973
  break;
37974
+ case "drawingsInteractive":
37975
+ this.drawingsInteractive = value !== false;
37976
+ if (this.input) this.input.drawings = this.drawingsInteractive;
37977
+ if (!this.drawingsInteractive) this.userDrawings?.clearHover();
37978
+ return;
37979
+ // affects the next gesture only — clearHover repaints what it drops
37980
+ case "priceAxisTicks":
37981
+ this.priceAxisTicks.setHook(typeof value === "function" ? value : null);
37982
+ break;
37166
37983
  case "keyboard":
37167
37984
  this.setKeyboardEnabled(Boolean(value));
37168
37985
  return;
@@ -37278,6 +38095,10 @@ var NativeRenderer = class {
37278
38095
  return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
37279
38096
  case "crosshairOverride":
37280
38097
  return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
38098
+ case "drawingsInteractive":
38099
+ return this.drawingsInteractive;
38100
+ case "priceAxisTicks":
38101
+ return this.priceAxisTicks.hook;
37281
38102
  case "keyboard":
37282
38103
  return this.keyboardEnabled;
37283
38104
  case "historyChords":
@@ -37951,6 +38772,12 @@ var NativeRenderer = class {
37951
38772
  Object.assign(this.drawingsCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
37952
38773
  this.cursorCanvas = document.createElement("canvas");
37953
38774
  Object.assign(this.cursorCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
38775
+ this.shadeRightEl = document.createElement("div");
38776
+ this.shadeRightEl.className = "vela-shade-right";
38777
+ this.shadeRightEl.setAttribute("aria-hidden", "true");
38778
+ this.shadeRightEl.hidden = true;
38779
+ Object.assign(this.shadeRightEl.style, { position: "absolute", top: "0", left: "0", width: "0", height: "0", pointerEvents: "none", display: "none" });
38780
+ this.shadeRightSig = "";
37954
38781
  this.overlayRoot = document.createElement("div");
37955
38782
  Object.assign(this.overlayRoot.style, { position: "absolute", inset: "0", pointerEvents: "none" });
37956
38783
  this.plot = document.createElement("div");
@@ -37958,7 +38785,7 @@ var NativeRenderer = class {
37958
38785
  this.extLayers = rendererLayers().map((def) => ({ def, instance: def.create(), canvas: this.createLayerCanvas(), channel: def.id, owner: null }));
37959
38786
  const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
37960
38787
  const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
37961
- this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
38788
+ this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.shadeRightEl, this.cursorCanvas, this.overlayRoot);
37962
38789
  this.layerOrderSig = "";
37963
38790
  this.wrapper.appendChild(this.plot);
37964
38791
  this.factoryConfig = this.getConfig();
@@ -38016,13 +38843,14 @@ var NativeRenderer = class {
38016
38843
  drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
38017
38844
  drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
38018
38845
  drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
38019
- drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
38846
+ drawingsCursor: (x, y) => (this.drawingsInteractive ? this.userDrawings?.cursorAt(x, y) : null) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
38020
38847
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
38021
38848
  drawingsClearTransient: () => this.userDrawings?.clearTransient()
38022
38849
  });
38023
38850
  this.input.rightEdgeZoom = this.zoomAnchorMode === "right";
38024
38851
  this.input.axisDrag = this.axisDragEnabled;
38025
38852
  this.input.paneResize = this.paneResizeEnabled;
38853
+ this.input.drawings = this.drawingsInteractive;
38026
38854
  this.input.attach(this.dataCanvas);
38027
38855
  if (this.settingsEnabled) this.setSettingsEnabled(true);
38028
38856
  this.syncCountdownTimer();
@@ -38331,6 +39159,8 @@ var NativeRenderer = class {
38331
39159
  this.backend.destroy();
38332
39160
  this.chrome.destroy();
38333
39161
  this.crosshairLayer.destroy();
39162
+ this.shadeRightEl?.remove();
39163
+ this.shadeRightEl = null;
38334
39164
  this.attributionEl?.remove();
38335
39165
  this.attributionEl = null;
38336
39166
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
@@ -38677,14 +39507,21 @@ var NativeRenderer = class {
38677
39507
  this.priceStyleCbs.add(cb);
38678
39508
  return () => this.priceStyleCbs.delete(cb);
38679
39509
  }
39510
+ onPriceStyleWillChange(cb) {
39511
+ this.priceStyleWillChangeCbs.add(cb);
39512
+ return () => this.priceStyleWillChangeCbs.delete(cb);
39513
+ }
38680
39514
  /**
38681
39515
  * THE single write path for the base price style at runtime (feature set / settings dialog /
38682
- * config template — the constructor seeds the field directly, pre-listeners). Updates the
38683
- * scene, eases any reveal layer toward the new style's target, and notifies the core —
38684
- * which owns the DATA side of styles that need one (a chart type's SeriesDataEngine).
39516
+ * config template — the constructor seeds the field directly, pre-listeners). Announces the
39517
+ * switch while the scene still holds the old style (a listener may paint/capture that frame),
39518
+ * then updates the scene, eases any reveal layer toward the new style's target, and notifies
39519
+ * the core — which owns the DATA side of styles that need one (a chart type's SeriesDataEngine).
38685
39520
  */
38686
39521
  setPriceStyle(style) {
38687
39522
  if (style === this.scene.priceStyle) return;
39523
+ const from = this.scene.priceStyle;
39524
+ for (const cb of this.priceStyleWillChangeCbs) cb(from, style);
38688
39525
  this.scene.priceStyle = style;
38689
39526
  this.scene.basePainting = basePaintingOf(style);
38690
39527
  this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
@@ -38914,7 +39751,7 @@ var NativeRenderer = class {
38914
39751
  if (this.easeLiveBar(dtMs)) active = true;
38915
39752
  this.skeletonClockMs += dtMs;
38916
39753
  this.paintData();
38917
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
39754
+ this.renderCrosshairLayer();
38918
39755
  this.updateLegendValues();
38919
39756
  this.emitViewportChange();
38920
39757
  return active;
@@ -39489,10 +40326,31 @@ var NativeRenderer = class {
39489
40326
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
39490
40327
  this.trackMarkPopover();
39491
40328
  }
39492
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
40329
+ this.renderCrosshairLayer();
39493
40330
  if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
39494
40331
  this.updateLegendValues();
39495
40332
  }
40333
+ /** Repaint the L2 crosshair canvas and keep the `.vela-shade-right` element on the veil it painted. */
40334
+ renderCrosshairLayer() {
40335
+ this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
40336
+ this.syncShadeRight();
40337
+ }
40338
+ /** Position (or hide) the `.vela-shade-right` element over the veil of the last crosshair frame. */
40339
+ syncShadeRight() {
40340
+ const el = this.shadeRightEl;
40341
+ if (!el) return;
40342
+ const area = this.crosshairLayer.shadeRightArea;
40343
+ const sig = area ? `${area.x},${area.width},${area.height}` : "";
40344
+ if (sig === this.shadeRightSig) return;
40345
+ this.shadeRightSig = sig;
40346
+ if (!area) {
40347
+ el.hidden = true;
40348
+ el.style.display = "none";
40349
+ return;
40350
+ }
40351
+ Object.assign(el.style, { left: `${area.x}px`, width: `${area.width}px`, height: `${area.height}px`, display: "" });
40352
+ el.hidden = false;
40353
+ }
39496
40354
  /** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
39497
40355
  repaintCursorLayers() {
39498
40356
  const pane = this.scene.panes.get(PRICE_PANE_ID2);
@@ -40286,7 +41144,7 @@ var NativeRenderer = class {
40286
41144
  if (this.animator.active) {
40287
41145
  this.computeScales();
40288
41146
  this.paintData();
40289
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
41147
+ this.renderCrosshairLayer();
40290
41148
  } else {
40291
41149
  this.scheduler.flushNow(4 /* Full */);
40292
41150
  }
@@ -44301,7 +45159,8 @@ var Vela = class {
44301
45159
  glow: options.glow ?? 0,
44302
45160
  upColor: options.upColor ?? BULLISH,
44303
45161
  downColor: options.downColor ?? BEARISH,
44304
- priceStyle: options.priceStyle ?? "candles"
45162
+ priceStyle: options.priceStyle ?? "candles",
45163
+ priceAxisTicks: options.priceAxis?.ticks ?? null
44305
45164
  };
44306
45165
  const RendererClass = options.renderer ?? NativeRenderer;
44307
45166
  if (typeof RendererClass !== "function") {
@@ -44791,6 +45650,9 @@ var CSS21 = `
44791
45650
  .vela-statusline .vela-sl-market { align-self: center; display: inline-flex; }
44792
45651
  .vela-statusline .vela-sl-market > [hidden] { display: none !important; }
44793
45652
  .vela-statusline .vela-sl-replay-badge, .vela-statusline .vela-sl-replay-badge svg { display: block; width: 16px; height: 16px; }
45653
+ /* Tabular figures: every digit has one advance, so a readout whose text changes only in
45654
+ * its digits keeps its width \u2014 render() skips the ladder for those (see widthKey). */
45655
+ .vela-statusline .vela-sl-values { font-variant-numeric: tabular-nums; }
44794
45656
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
44795
45657
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
44796
45658
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
@@ -44908,6 +45770,10 @@ var Statusline = class {
44908
45770
  this.fitRO = null;
44909
45771
  /** The ladder rung currently applied — see {@link fit}. */
44910
45772
  this.layout = { stacked: false, level: "full" };
45773
+ /** The readout {@link widthKey} the applied rung was fitted to. */
45774
+ this.fitKey = null;
45775
+ /** Web fonts landing after the last fit change every width without a resize. */
45776
+ this.onFontsLoaded = () => this.fit();
44911
45777
  this.onContextMenu = (e) => {
44912
45778
  if (!this.menu || !this.menuHooks) return;
44913
45779
  e.preventDefault();
@@ -44971,6 +45837,7 @@ var Statusline = class {
44971
45837
  this.fitRO = new ResizeObserver(() => this.fit());
44972
45838
  this.fitRO.observe(host);
44973
45839
  }
45840
+ doc.fonts?.addEventListener("loadingdone", this.onFontsLoaded);
44974
45841
  this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
44975
45842
  this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
44976
45843
  this.setMarketStatus("open");
@@ -45023,6 +45890,7 @@ var Statusline = class {
45023
45890
  * (detached host, node tests) nothing overflows, so the widest rung stays.
45024
45891
  */
45025
45892
  fit() {
45893
+ this.fitKey = this.widthKey();
45026
45894
  this.syncParts();
45027
45895
  const seg = segmentVisibility(this.parts, this.chartHidden);
45028
45896
  const overflows = () => this.el.scrollWidth > this.el.clientWidth;
@@ -45179,6 +46047,7 @@ var Statusline = class {
45179
46047
  this.detach();
45180
46048
  this.fitRO?.disconnect();
45181
46049
  this.fitRO = null;
46050
+ this.el.ownerDocument.fonts?.removeEventListener("loadingdone", this.onFontsLoaded);
45182
46051
  this.el.removeEventListener("contextmenu", this.onContextMenu);
45183
46052
  this.menu?.destroy();
45184
46053
  this.menu = null;
@@ -45194,7 +46063,22 @@ var Statusline = class {
45194
46063
  }
45195
46064
  render() {
45196
46065
  if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
45197
- this.fit();
46066
+ if (this.widthKey() === this.fitKey) this.renderValues();
46067
+ else this.fit();
46068
+ }
46069
+ /** Everything the readout's width depends on, at every rung: its shape, and each
46070
+ * value's sign, digit count and decimals (the digits themselves are tabular — see
46071
+ * the CSS; the grouping commas follow from the digit count). `toFixed` carries all
46072
+ * three at a fraction of the locale formatter's cost — this runs per pointer move.
46073
+ * The parts, the host's size and the fonts re-fit through their own paths. */
46074
+ widthKey() {
46075
+ const bar = this.hoverBar ?? this.lastBar;
46076
+ if (!bar) return this.readout;
46077
+ const dp = decimalsFor(bar.close);
46078
+ const diff = bar.close - bar.open;
46079
+ const text = [bar.open, bar.high, bar.low, bar.close, diff].map((v) => v.toFixed(dp));
46080
+ text.push((diff / bar.open * 100).toFixed(2));
46081
+ return `${this.readout} ${text.join(" ")}`.replace(/\d/g, "0");
45198
46082
  }
45199
46083
  /** Write the value readout for the current bar at the current ladder level. */
45200
46084
  renderValues() {
@@ -45794,16 +46678,25 @@ var CellControls = class {
45794
46678
  // src/widget/context-menu.ts
45795
46679
  var PRICE_AXIS_W = 60;
45796
46680
  var TIME_AXIS_H3 = 26;
46681
+ var OFF_PLOT = { price: null, time: null, paneKind: null };
45797
46682
  var ChartContextMenu = class {
45798
46683
  constructor(host, cbs) {
45799
46684
  this.cbs = cbs;
45800
46685
  this.chart = null;
46686
+ this.offCrosshair = null;
46687
+ /** The bound chart's latest crosshair report — a right-click always follows a pointer
46688
+ * move to its spot, so this is where it landed. */
46689
+ this.crosshair = OFF_PLOT;
46690
+ /** {@link crosshair} as it stood when the menu opened: the pointer moves on through the
46691
+ * menu before a row is picked, and `run` must see the right-clicked spot. */
46692
+ this.pointer = OFF_PLOT;
45801
46693
  this.lastZone = "body";
45802
46694
  /** The pane whose scale the open price-axis menu targets (null ⇒ the main scale). */
45803
46695
  this.lastPane = null;
45804
46696
  this.onContextMenu = (e) => {
45805
46697
  e.preventDefault();
45806
46698
  if (!this.chart) return;
46699
+ this.pointer = this.crosshair;
45807
46700
  this.lastZone = this.zoneOf(e);
45808
46701
  this.lastPane = this.lastZone === "price-axis" ? this.paneAt(e) : null;
45809
46702
  this.menu.setItems(this.itemsFor(this.lastZone));
@@ -45817,15 +46710,23 @@ var ChartContextMenu = class {
45817
46710
  // Pointer-anchored action menu: checked state reads as a leading ✓, not a
45818
46711
  // washed row (which would read as hover in a menu with no trigger button).
45819
46712
  checkmarks: true,
46713
+ iconBadges: true,
45820
46714
  onSelect: (id) => this.run(id)
45821
46715
  });
45822
46716
  host.addEventListener("contextmenu", this.onContextMenu);
45823
46717
  }
45824
46718
  /** (Re)bind to a chart instance — called after every widget rebuild. */
45825
46719
  onChart(chart) {
46720
+ this.offCrosshair?.();
45826
46721
  this.chart = chart;
46722
+ this.crosshair = OFF_PLOT;
46723
+ this.offCrosshair = chart.renderer.onCrosshairMove((e) => {
46724
+ this.crosshair = { price: e.price, time: e.time, paneKind: e.paneKind ?? null };
46725
+ });
45827
46726
  }
45828
46727
  destroy() {
46728
+ this.offCrosshair?.();
46729
+ this.offCrosshair = null;
45829
46730
  this.host.removeEventListener("contextmenu", this.onContextMenu);
45830
46731
  this.menu.destroy();
45831
46732
  }
@@ -45844,49 +46745,52 @@ var ChartContextMenu = class {
45844
46745
  flag(feature) {
45845
46746
  return Boolean(this.chart?.renderer.get(feature));
45846
46747
  }
45847
- contributed(zone) {
46748
+ /** A fresh widget context carrying the pointer captured at open. Copied by property
46749
+ * descriptor: the context's getters must stay LIVE, and a spread would freeze them. */
46750
+ context() {
45848
46751
  const ctx = this.cbs.getContext?.();
45849
- return widgetActions(`context:${zone}`, ctx).map((a, i) => ({
45850
- id: `action:${a.id}`,
45851
- label: a.label,
45852
- icon: a.icon,
45853
- separatorBefore: i === 0
46752
+ if (!ctx) return void 0;
46753
+ return Object.create(Object.getPrototypeOf(ctx), {
46754
+ ...Object.getOwnPropertyDescriptors(ctx),
46755
+ pointer: { value: this.pointer, enumerable: true }
46756
+ });
46757
+ }
46758
+ contributed(zone) {
46759
+ return widgetActions(`context:${zone}`, this.context()).map((a) => ({
46760
+ item: { id: `action:${a.id}`, label: a.label, icon: a.icon },
46761
+ order: a.order
45854
46762
  }));
45855
46763
  }
45856
46764
  itemsFor(zone) {
46765
+ return composeMenu(zone, this.builtinItems(zone), this.contributed(zone));
46766
+ }
46767
+ builtinItems(zone) {
45857
46768
  if (zone === "price-axis") {
45858
46769
  const pane = this.lastPane;
45859
- return [
45860
- ...priceAxisItems({
45861
- auto: this.chart?.renderer.get("autoScale") !== false,
45862
- invert: pane ? pane.invert : this.flag("invertScale"),
45863
- choice: scaleChoiceOf(pane ?? { mode: String(this.chart?.renderer.get("scaleMode") ?? "price"), log: this.flag("logScale") }),
45864
- axisLabels: this.flag("axisLabels"),
45865
- priceLabel: this.flag("priceLabel"),
45866
- countdown: this.flag("countdown"),
45867
- priceLine: this.flag("currentPriceLine")
45868
- }),
45869
- ...this.contributed(zone)
45870
- ];
46770
+ return priceAxisItems({
46771
+ auto: this.chart?.renderer.get("autoScale") !== false,
46772
+ invert: pane ? pane.invert : this.flag("invertScale"),
46773
+ choice: scaleChoiceOf(pane ?? { mode: String(this.chart?.renderer.get("scaleMode") ?? "price"), log: this.flag("logScale") }),
46774
+ axisLabels: this.flag("axisLabels"),
46775
+ priceLabel: this.flag("priceLabel"),
46776
+ countdown: this.flag("countdown"),
46777
+ priceLine: this.flag("currentPriceLine")
46778
+ });
45871
46779
  }
45872
46780
  if (zone === "time-axis") {
45873
- const tz = this.cbs.timezone?.() ?? String(this.chart?.renderer.get("timezone") ?? "Etc/UTC");
45874
- return [...timeAxisItems(tz), ...this.contributed("time-axis")];
46781
+ return timeAxisItems(this.cbs.timezone?.() ?? String(this.chart?.renderer.get("timezone") ?? "Etc/UTC"));
45875
46782
  }
45876
46783
  const chart = this.chart;
45877
- return [
45878
- ...bodyItems({
45879
- drawings: chart?.drawings.supported ? chart.drawings.all().length : 0,
45880
- indicators: chart?.indicators().length ?? 0
45881
- }),
45882
- ...this.contributed("body")
45883
- ];
46784
+ return bodyItems({
46785
+ drawings: chart?.drawings.supported ? chart.drawings.all().length : 0,
46786
+ indicators: chart?.indicators().length ?? 0
46787
+ });
45884
46788
  }
45885
46789
  run(id) {
45886
46790
  const chart = this.chart;
45887
46791
  if (!chart) return;
45888
46792
  if (id.startsWith("action:")) {
45889
- const ctx = this.cbs.getContext?.();
46793
+ const ctx = this.context();
45890
46794
  if (ctx) widgetActions(`context:${this.lastZone}`, ctx).find((a) => a.id === id.slice("action:".length))?.run(ctx);
45891
46795
  return;
45892
46796
  }
@@ -46010,8 +46914,8 @@ function seedDefaults(opts) {
46010
46914
  };
46011
46915
  }
46012
46916
  function cellChartDefaults(opts) {
46013
- const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings } = opts;
46014
- return { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings };
46917
+ const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings, priceAxis } = opts;
46918
+ return { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings, priceAxis };
46015
46919
  }
46016
46920
  function cellDrawings(opt) {
46017
46921
  if (opt === false) return false;
@@ -46581,12 +47485,14 @@ var ChartCell = class {
46581
47485
  this.watermarkOn = visible;
46582
47486
  this.watermark?.setVisible(visible);
46583
47487
  this.deps.onStateDirty();
47488
+ this.deps.onStatusPrefsChanged(this.id);
46584
47489
  }
46585
47490
  /** Show/hide the "Replay" line under this cell's watermark while it replays (persisted per cell). */
46586
47491
  setReplayWatermarkVisible(visible) {
46587
47492
  this.replayWatermarkOn = visible;
46588
47493
  this.watermark?.setReplayVisible(visible);
46589
47494
  this.deps.onStateDirty();
47495
+ this.deps.onStatusPrefsChanged(this.id);
46590
47496
  }
46591
47497
  /** Show/hide this cell's indicator titles — the in-chart legend rows (persisted per cell). */
46592
47498
  setIndicatorTitlesVisible(visible) {
@@ -46607,18 +47513,20 @@ var ChartCell = class {
46607
47513
  this.statusline?.setPartVisible(part, visible);
46608
47514
  this.deps.onStatusPrefsChanged(this.id);
46609
47515
  }
46610
- /** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
47516
+ /** This cell's Status line tab and watermark prefs as one bundle (see {@link CellStatusPrefs}). */
46611
47517
  statusPrefs() {
46612
47518
  const sl = this.statusline;
46613
47519
  return {
46614
47520
  parts: sl ? { logo: sl.partVisible("logo"), name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
46615
47521
  indicatorTitles: this.indicatorTitlesOn,
46616
- indicatorValues: this.indicatorValuesOn
47522
+ indicatorValues: this.indicatorValuesOn,
47523
+ watermark: this.watermarkOn,
47524
+ replayWatermark: this.replayWatermarkOn
46617
47525
  };
46618
47526
  }
46619
- /** Converge this cell's Status line tab prefs to `prefs` — the follower half of
46620
- * the workspace's style link. Idempotent: matching values change nothing, so a
46621
- * propagated echo dies on its own. */
47527
+ /** Converge this cell's Status line tab and watermark prefs to `prefs` — the
47528
+ * follower half of the workspace's style link. Idempotent: matching values change
47529
+ * nothing, so a propagated echo dies on its own. */
46622
47530
  applyStatusPrefs(prefs) {
46623
47531
  if (prefs.parts && this.statusline) {
46624
47532
  for (const part of Object.keys(prefs.parts)) {
@@ -46627,6 +47535,8 @@ var ChartCell = class {
46627
47535
  }
46628
47536
  if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
46629
47537
  if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
47538
+ if (prefs.watermark !== this.watermarkOn) this.setWatermarkVisible(prefs.watermark);
47539
+ if (prefs.replayWatermark !== this.replayWatermarkOn) this.setReplayWatermarkVisible(prefs.replayWatermark);
46630
47540
  }
46631
47541
  /** The LIVE chart of this cell — never cache it across a layout change (the cell's
46632
47542
  * identity is what endures; the chart dies with the cell). */
@@ -47531,6 +48441,55 @@ function occupancyGrid(def) {
47531
48441
  const cols = def.cols.length;
47532
48442
  return def.rows.map((_, r) => def.cols.map((_2, c) => def.cells[r * cols + c]?.id ?? `\xB7${r}x${c}`));
47533
48443
  }
48444
+ var GLYPH_MIN = 1.5;
48445
+ var GLYPH_SPAN = 13;
48446
+ function glyphStops(weights, n) {
48447
+ const w = Array.from({ length: n }, (_, i) => {
48448
+ const v = weights[i];
48449
+ return v !== void 0 && Number.isFinite(v) && v > 0 ? v : 1;
48450
+ });
48451
+ const total = w.reduce((a, b) => a + b, 0);
48452
+ const stops = [GLYPH_MIN];
48453
+ let acc = 0;
48454
+ for (const v of w) {
48455
+ acc += v;
48456
+ stops.push(Math.round((GLYPH_MIN + GLYPH_SPAN * acc / total) * 100) / 100);
48457
+ }
48458
+ return stops;
48459
+ }
48460
+ function layoutGlyph(def) {
48461
+ const grid = occupancyGrid(def);
48462
+ const rows = grid.length;
48463
+ const cols = grid.reduce((n, row) => Math.max(n, row.length), 0);
48464
+ const xs = glyphStops(def.cols, cols);
48465
+ const ys = glyphStops(def.rows, rows);
48466
+ const at = (r, c) => grid[r]?.[c];
48467
+ const seams = [];
48468
+ for (let c = 1; c < cols; c += 1) {
48469
+ let start = -1;
48470
+ for (let r = 0; r <= rows; r += 1) {
48471
+ const split = r < rows && at(r, c - 1) !== at(r, c);
48472
+ if (split && start < 0) start = r;
48473
+ if (!split && start >= 0) {
48474
+ seams.push(`M${xs[c]} ${ys[start]}V${ys[r]}`);
48475
+ start = -1;
48476
+ }
48477
+ }
48478
+ }
48479
+ for (let r = 1; r < rows; r += 1) {
48480
+ let start = -1;
48481
+ for (let c = 0; c <= cols; c += 1) {
48482
+ const split = c < cols && at(r - 1, c) !== at(r, c);
48483
+ if (split && start < 0) start = c;
48484
+ if (!split && start >= 0) {
48485
+ seams.push(`M${xs[start]} ${ys[r]}H${xs[c]}`);
48486
+ start = -1;
48487
+ }
48488
+ }
48489
+ }
48490
+ const frame = `<rect x="${GLYPH_MIN}" y="${GLYPH_MIN}" width="${GLYPH_SPAN}" height="${GLYPH_SPAN}" rx="1.5"/>`;
48491
+ return svg16(seams.length > 0 ? `${frame}<path d="${seams.join("")}"/>` : frame);
48492
+ }
47534
48493
  function gridStyles(def, trackSizes) {
47535
48494
  const cols = trackSizes?.cols?.length === def.cols.length ? trackSizes.cols : def.cols;
47536
48495
  const rows = trackSizes?.rows?.length === def.rows.length ? trackSizes.rows : def.rows;
@@ -48062,7 +49021,8 @@ var VelaWorkspace = class {
48062
49021
  onToggleSync: (id) => {
48063
49022
  const kind = id;
48064
49023
  this.sync.set(kind, this.syncOpts[kind] ? false : true);
48065
- }
49024
+ },
49025
+ glyph: () => layoutGlyph(this.def)
48066
49026
  },
48067
49027
  getContext: () => this.context()
48068
49028
  });
@@ -48561,6 +49521,7 @@ var VelaWorkspace = class {
48561
49521
  this.applyGrid();
48562
49522
  this.buildCells();
48563
49523
  this.alignNewCellStyles(preexisting);
49524
+ this.alignNewCellDrawings(preexisting);
48564
49525
  this.syncCellPresentation();
48565
49526
  this.refreshCellControls();
48566
49527
  this.topbar.setLayout(next.id);
@@ -48977,6 +49938,7 @@ var VelaWorkspace = class {
48977
49938
  const chart = cell.chart;
48978
49939
  chart.on("indicator:error", ({ error }) => this.toastHost.show(`[${cell.id}] ${error.message}`, "error", 5e3));
48979
49940
  chart.on("script:run", (run) => this.events.emit("script:run", { ...run, cell: cell.id }));
49941
+ chart.on("priceStyle:change", ({ from, to }) => this.events.emit("cell:priceStyle", { id: cell.id, from, to }));
48980
49942
  chart.on("alert", (alert) => {
48981
49943
  const source = [parseSymbol(cell.symbol).ticker || cell.symbol, timeframeLabel(cell.timeframe), alert.indicator].filter(Boolean).join(" ");
48982
49944
  this.alerts.unshift({ cellId: cell.id, source, title: alert.title ?? "Alert", message: alert.message, time: alert.time });
@@ -49094,12 +50056,66 @@ var VelaWorkspace = class {
49094
50056
  }
49095
50057
  }
49096
50058
  /**
49097
- * Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
49098
- * its renderer config plus its Status line tab prefs — onto its same-group
49099
- * followers (the style link). Loop-safe two ways: the busy guard eats the
49100
- * followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
49101
- * in the same tick), and the equality short-circuits leave already-converged
49102
- * followers untouched, so nothing re-emits once the group agrees.
50059
+ * Bring cells minted by a layout change into their drawings group: with the link
50060
+ * on, a NEW cell (fresh slot, or one returning from the pool that missed edits
50061
+ * while dormant) receives the drawings of a pre-existing group peer — the active
50062
+ * cell when it is one. A drawing the arriving cell already holds a linked copy of
50063
+ * is refreshed in place; any other is copied and linked like a freshly synced
50064
+ * drawing, so later edits and removals follow both ways. The arrival is the cell's
50065
+ * starting state, not an edit: it stays out of the cell's undo timeline, and the
50066
+ * busy guard keeps the copies' own events from fanning back out.
50067
+ */
50068
+ alignNewCellDrawings(preexisting) {
50069
+ const setting = this.syncOpts.drawings;
50070
+ if (!setting) return;
50071
+ const ids = [...this.cellsById.keys()];
50072
+ for (const id of ids) {
50073
+ if (preexisting.has(id)) continue;
50074
+ const cell = this.cellsById.get(id);
50075
+ if (!cell?.chart.drawings.supported) continue;
50076
+ const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
50077
+ if (peers.length === 0) continue;
50078
+ const sourceId = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
50079
+ const source = this.cellsById.get(sourceId);
50080
+ if (!source) continue;
50081
+ const drawings = cell.chart.drawings;
50082
+ this.drawingSyncBusy = true;
50083
+ try {
50084
+ cell.history.silently(() => {
50085
+ const held = new Set(drawings.all().map((d) => d.id));
50086
+ for (const doc of source.chart.drawings.all()) {
50087
+ const group = this.drawingLinks.get(`${sourceId}\0${doc.id}`) ?? /* @__PURE__ */ new Map([[sourceId, doc.id]]);
50088
+ const peerId = group.get(id);
50089
+ if (peerId != null && held.has(peerId)) {
50090
+ drawings.update(peerId, { anchors: doc.anchors, style: doc.style, text: doc.text, props: doc.props });
50091
+ continue;
50092
+ }
50093
+ const copy = drawings.add(doc.type, {
50094
+ paneId: doc.paneId,
50095
+ anchors: doc.anchors,
50096
+ style: doc.style,
50097
+ text: doc.text,
50098
+ props: doc.props,
50099
+ zIndex: doc.zIndex
50100
+ });
50101
+ if (!copy) continue;
50102
+ if (peerId != null) this.drawingLinks.delete(`${id}\0${peerId}`);
50103
+ group.set(id, copy.id);
50104
+ for (const [cellId, dId] of group) this.drawingLinks.set(`${cellId}\0${dId}`, group);
50105
+ }
50106
+ });
50107
+ } finally {
50108
+ this.drawingSyncBusy = false;
50109
+ }
50110
+ }
50111
+ }
50112
+ /**
50113
+ * Mirror an origin cell's presentation — the style-link slice of its renderer
50114
+ * config ({@link styleConfigPatch}) plus its Status line and watermark prefs —
50115
+ * onto its same-group followers (the style link). Loop-safe two ways: the busy
50116
+ * guard eats the followers' SYNCHRONOUS echoes (their `applyConfig` re-fires
50117
+ * `onConfigChanged` in the same tick), and the equality short-circuits leave
50118
+ * already-converged followers untouched, so nothing re-emits once the group agrees.
49103
50119
  */
49104
50120
  propagateStylePrefs(originId) {
49105
50121
  if (this.styleSyncBusy || this.destroyed) return;
@@ -49107,17 +50123,15 @@ var VelaWorkspace = class {
49107
50123
  if (targets.length === 0) return;
49108
50124
  const origin = this.cellsById.get(originId);
49109
50125
  if (!origin) return;
49110
- const slice = styleConfigSlice(origin.chart.renderer.getConfig());
49111
- const sliceJson = slice ? JSON.stringify(slice) : null;
50126
+ const config = origin.chart.renderer.getConfig();
49112
50127
  const prefs = origin.statusPrefs();
49113
50128
  this.styleSyncBusy = true;
49114
50129
  try {
49115
50130
  for (const id of targets) {
49116
50131
  const cell = this.cellsById.get(id);
49117
50132
  if (!cell) continue;
49118
- if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
49119
- cell.chart.renderer.applyConfig(slice);
49120
- }
50133
+ const patch = styleConfigPatch(config, cell.chart.renderer.getConfig());
50134
+ if (patch) cell.chart.renderer.applyConfig(patch);
49121
50135
  cell.applyStatusPrefs(prefs);
49122
50136
  }
49123
50137
  } finally {
@@ -49438,7 +50452,8 @@ var VelaWorkspace = class {
49438
50452
  onToggleSync: (id) => {
49439
50453
  const kind = id;
49440
50454
  this.sync.set(kind, this.syncOpts[kind] ? false : true);
49441
- }
50455
+ },
50456
+ glyph: () => layoutGlyph(this.def)
49442
50457
  },
49443
50458
  onOpenChange: (open2) => this.trackDialog(open2)
49444
50459
  }));
@@ -49702,6 +50717,7 @@ var VelaWidget = class {
49702
50717
  };
49703
50718
 
49704
50719
  exports.Bottombar = Bottombar;
50720
+ exports.CONTEXT_MENU_BUILTIN_ORDER = CONTEXT_MENU_BUILTIN_ORDER;
49705
50721
  exports.ChartContextMenu = ChartContextMenu;
49706
50722
  exports.DEFAULT_PANEL_MAX_WIDTH = DEFAULT_PANEL_MAX_WIDTH;
49707
50723
  exports.DEFAULT_PANEL_MIN_WIDTH = DEFAULT_PANEL_MIN_WIDTH;