@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
@@ -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`;
@@ -5288,10 +5582,12 @@ function legendActions() {
5288
5582
  }
5289
5583
  function legendActionsProviderFor(chart, context) {
5290
5584
  return (indicatorId) => {
5291
- const handle = chart.indicators().find((h) => h.id === indicatorId);
5585
+ const find = () => chart.indicators().find((h) => h.id === indicatorId);
5586
+ const infoOf = (h) => ({ id: h.id, title: h.title, ...h.source !== void 0 ? { source: h.source } : {} });
5587
+ const handle = find();
5292
5588
  if (!handle) return [];
5293
- const info = { id: handle.id, title: handle.title, ...handle.source !== void 0 ? { source: handle.source } : {} };
5294
- 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) }));
5589
+ const info = infoOf(handle);
5590
+ 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)) }));
5295
5591
  };
5296
5592
  }
5297
5593
  var calloutRegistry = /* @__PURE__ */ new Map();
@@ -6139,7 +6435,8 @@ var Topbar = class {
6139
6435
  triggerId: "vela-topbar-style",
6140
6436
  host,
6141
6437
  items: this.styleItems(),
6142
- onSelect: (id) => opts.onPriceStyle(id)
6438
+ onSelect: (id) => opts.onPriceStyle(id),
6439
+ iconBadges: true
6143
6440
  });
6144
6441
  }
6145
6442
  setSymbol(symbol) {
@@ -6205,8 +6502,14 @@ var Topbar = class {
6205
6502
  renderLayoutButton(doc) {
6206
6503
  if (!this.layoutButton) return;
6207
6504
  this.layoutButton.replaceChildren();
6208
- if (iconMarkup("layout")) this.layoutButton.appendChild(iconEl("layout", doc));
6209
- else this.layoutButton.appendChild(doc.createTextNode(this.layoutId ?? ""));
6505
+ const glyph = this.opts.layout?.glyph?.();
6506
+ if (glyph || iconMarkup("layout")) {
6507
+ const icon2 = iconEl("layout", doc);
6508
+ if (glyph) icon2.innerHTML = glyph;
6509
+ this.layoutButton.appendChild(icon2);
6510
+ } else {
6511
+ this.layoutButton.appendChild(doc.createTextNode(this.layoutId ?? ""));
6512
+ }
6210
6513
  this.layoutButton.setAttribute("aria-label", `Layout \u2014 ${this.layoutId ?? ""}`);
6211
6514
  }
6212
6515
  renderStyleButton(doc) {
@@ -9489,6 +9792,10 @@ var FibRatios = class extends Drawing {
9489
9792
 
9490
9793
  // src/core/drawings/types/FibLevels.ts
9491
9794
  var FibLevels = class extends FibRatios {
9795
+ /** Price of a level for the two anchor prices; subclasses override to change which anchor ratio 0 sits on. */
9796
+ levelPrice(ratio, p1, p2) {
9797
+ return p1 + ratio * (p2 - p1);
9798
+ }
9492
9799
  /** Per-level pixel line + price for the ENABLED levels, spanning the anchors' time range. */
9493
9800
  levelLines(proj) {
9494
9801
  const a = this.anchors[0];
@@ -9498,11 +9805,10 @@ var FibLevels = class extends FibRatios {
9498
9805
  const xb = proj.xOf(b.time);
9499
9806
  const x1 = Math.min(xa, xb);
9500
9807
  const x2 = Math.max(xa, xb);
9501
- const delta = b.price - a.price;
9502
9808
  const out = [];
9503
9809
  for (const lv of this.levels) {
9504
9810
  if (!lv.enabled) continue;
9505
- const price = a.price + lv.ratio * delta;
9811
+ const price = this.levelPrice(lv.ratio, a.price, b.price);
9506
9812
  const y = proj.yOf(price, this.paneId);
9507
9813
  if (y == null) continue;
9508
9814
  out.push({ ratio: lv.ratio, color: lv.color, label: lv.label, price, x1, x2, y });
@@ -9542,8 +9848,7 @@ var FibLevels = class extends FibRatios {
9542
9848
  const a = this.anchors[0];
9543
9849
  const b = this.anchors[1];
9544
9850
  if (!a || !b) return null;
9545
- const delta = b.price - a.price;
9546
- const prices = this.levels.filter((l) => l.enabled).map((l) => a.price + l.ratio * delta);
9851
+ const prices = this.levels.filter((l) => l.enabled).map((l) => this.levelPrice(l.ratio, a.price, b.price));
9547
9852
  if (prices.length === 0) return null;
9548
9853
  return { min: Math.min(...prices), max: Math.max(...prices) };
9549
9854
  }
@@ -9629,13 +9934,24 @@ function cycleLevels(count, enabledCount = count) {
9629
9934
  // src/core/drawings/types/FibRetracement.ts
9630
9935
  var LEVELS = fibLevels([0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]);
9631
9936
  var FibRetracement = class extends FibLevels {
9632
- constructor() {
9633
- super(...arguments);
9937
+ constructor(init) {
9938
+ super(init);
9634
9939
  this.type = "fibretracement";
9940
+ if (this.reverse === void 0) this.reverse = false;
9635
9941
  }
9636
9942
  defaultLevels() {
9637
9943
  return LEVELS;
9638
9944
  }
9945
+ levelPrice(ratio, p1, p2) {
9946
+ return this.reverse ? p1 + ratio * (p2 - p1) : p2 + ratio * (p1 - p2);
9947
+ }
9948
+ writeProps() {
9949
+ return { ...super.writeProps(), reverse: this.reverse };
9950
+ }
9951
+ readProps(props) {
9952
+ super.readProps(props);
9953
+ this.reverse = typeof props.reverse === "boolean" ? props.reverse : true;
9954
+ }
9639
9955
  };
9640
9956
 
9641
9957
  // src/core/drawings/types/FibExtension.ts
@@ -12762,6 +13078,32 @@ var CSS5 = `
12762
13078
  .vela-panel-pin .vela-icon { width: 15px; height: 15px; }
12763
13079
  .vela-panel-pin:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
12764
13080
  .vela-panel-pin[data-on='1'] { color: var(--vela-fg-bright); }
13081
+ .vela-panel-max {
13082
+ all: unset;
13083
+ cursor: pointer;
13084
+ width: 26px;
13085
+ height: 26px;
13086
+ flex: none;
13087
+ display: inline-flex;
13088
+ align-items: center;
13089
+ justify-content: center;
13090
+ border-radius: 4px;
13091
+ color: var(--vela-fg-muted);
13092
+ }
13093
+ .vela-panel-max .vela-icon { width: 15px; height: 15px; }
13094
+ .vela-panel-max:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
13095
+ /* MAXIMIZED: the panel covers the whole dock host \u2014 every chart of the shell \u2014 whatever
13096
+ its placement; the width handle and the pin have nothing to do meanwhile. */
13097
+ .vela-panel[data-maximized] {
13098
+ position: absolute;
13099
+ inset: 0;
13100
+ width: auto;
13101
+ max-width: none;
13102
+ z-index: 26;
13103
+ border-left: none;
13104
+ }
13105
+ .vela-panel[data-maximized] .vela-panel-resizer,
13106
+ .vela-panel[data-maximized] .vela-panel-pin { display: none; }
12765
13107
  .vela-panel-body { flex: 1; overflow: auto; padding: 8px; }
12766
13108
  .vela-panel-body::-webkit-scrollbar { width: 8px; }
12767
13109
  .vela-panel-body::-webkit-scrollbar-thumb { background: var(--vela-scroll); border-radius: 4px; border: 2px solid transparent; background-clip: padding-box; }
@@ -12814,6 +13156,7 @@ var CSS5 = `
12814
13156
  border-left: none;
12815
13157
  }
12816
13158
  [data-layout='mobile'] .vela-panel-resizer { display: none; }
13159
+ [data-layout='mobile'] .vela-panel-max { display: none; }
12817
13160
  `;
12818
13161
  function clampPanelWidth(px, min = DEFAULT_PANEL_MIN_WIDTH, max = DEFAULT_PANEL_MAX_WIDTH) {
12819
13162
  const lo = Number.isFinite(min) && min > 0 ? min : DEFAULT_PANEL_MIN_WIDTH;
@@ -12837,6 +13180,15 @@ var SidePanel = class {
12837
13180
  * never on a programmatic {@link setOverlay}, for the same reason as widths. */
12838
13181
  this.onPlacementChange = null;
12839
13182
  this.pin = null;
13183
+ this.maxButton = null;
13184
+ this.maximizedOn = false;
13185
+ /** Set while the close is announced, so a listener's own close does not re-enter. */
13186
+ this.closing = false;
13187
+ /** Open as far as the dock and the host are concerned — a closed panel can still be on
13188
+ * screen while its exit animation runs. */
13189
+ this.isOpen = false;
13190
+ /** The exit animation of a close; a reopen cancels it. */
13191
+ this.exit = null;
12840
13192
  const doc = host.ownerDocument;
12841
13193
  injectStyles(STYLE_ID4, CSS5, doc);
12842
13194
  this.minWidth = opts.resizable ? opts.minWidth ?? DEFAULT_PANEL_MIN_WIDTH : 1;
@@ -12871,7 +13223,13 @@ var SidePanel = class {
12871
13223
  });
12872
13224
  this.refreshPin();
12873
13225
  }
12874
- header.append(this.heading, this.slot, ...this.pin ? [this.pin] : [], close);
13226
+ if (opts.maximizable) {
13227
+ this.maxButton = doc.createElement("button");
13228
+ this.maxButton.className = "vela-panel-max";
13229
+ this.maxButton.addEventListener("click", () => this.setMaximized(!this.maximizedOn));
13230
+ this.refreshMaxButton();
13231
+ }
13232
+ header.append(this.heading, this.slot, ...this.pin ? [this.pin] : [], ...this.maxButton ? [this.maxButton] : [], close);
12875
13233
  this.body = doc.createElement("div");
12876
13234
  this.body.className = "vela-panel-body";
12877
13235
  this.el.append(header, this.body);
@@ -12879,13 +13237,65 @@ var SidePanel = class {
12879
13237
  host.appendChild(this.el);
12880
13238
  }
12881
13239
  get open() {
12882
- return !this.el.hidden;
13240
+ return this.isOpen;
12883
13241
  }
12884
- /** Open/close the panel — a bare call flips it. */
12885
- toggle(open2 = this.el.hidden) {
12886
- if (open2 === !this.el.hidden) return;
12887
- this.el.hidden = !open2;
13242
+ /**
13243
+ * Open/close the panel — a bare call flips it. The panel reports closed at once, but a
13244
+ * close keeps it on screen, marked `data-closing`, while a host exit animation runs.
13245
+ * `instant` skips that, and also cuts short the exit of a panel already closing: a panel
13246
+ * handing the dock to another leaves at once.
13247
+ */
13248
+ toggle(open2 = !this.isOpen, instant = false) {
13249
+ if (open2 === this.isOpen) {
13250
+ if (!open2 && instant) this.exit?.finish();
13251
+ return;
13252
+ }
13253
+ if (!open2 && this.closing) return;
13254
+ if (!open2) {
13255
+ this.closing = true;
13256
+ announceSurface(this.el, false, "panel");
13257
+ this.closing = false;
13258
+ }
13259
+ this.isOpen = open2;
13260
+ if (open2) {
13261
+ if (this.exit) {
13262
+ this.exit.cancel();
13263
+ this.exit = null;
13264
+ this.setMaximized(false);
13265
+ }
13266
+ this.el.hidden = false;
13267
+ } else {
13268
+ const hide = () => {
13269
+ this.exit = null;
13270
+ this.el.hidden = true;
13271
+ this.setMaximized(false);
13272
+ };
13273
+ if (instant) hide();
13274
+ else this.exit = holdForExit(this.el, hide);
13275
+ }
12888
13276
  this.onOpenChange?.(open2);
13277
+ if (open2 && this.isOpen) announceSurface(this.el, true, "panel");
13278
+ }
13279
+ /** Whether the panel covers every chart right now. */
13280
+ get maximized() {
13281
+ return this.maximizedOn;
13282
+ }
13283
+ /** Maximize the panel over the shell's charts, or restore it. A no-op on a panel not
13284
+ * declared `maximizable`. */
13285
+ setMaximized(maximized) {
13286
+ if (!this.maxButton || maximized === this.maximizedOn) return;
13287
+ this.maximizedOn = maximized;
13288
+ if (maximized) this.el.dataset.maximized = "1";
13289
+ else delete this.el.dataset.maximized;
13290
+ this.refreshMaxButton();
13291
+ }
13292
+ refreshMaxButton() {
13293
+ if (!this.maxButton) return;
13294
+ const doc = this.maxButton.ownerDocument;
13295
+ this.maxButton.replaceChildren(iconEl(this.maximizedOn ? "restore" : "maximize", doc));
13296
+ this.maxButton.title = this.maximizedOn ? "Restore" : "Maximize";
13297
+ this.maxButton.setAttribute("aria-label", this.maxButton.title);
13298
+ this.maxButton.setAttribute("aria-pressed", this.maximizedOn ? "true" : "false");
12889
13299
  }
12890
13300
  /** The scrolling body, for a panel filled from OUTSIDE the class — a contributed panel's
12891
13301
  * `mount` receives exactly this element. Subclasses use the protected `body`. */
@@ -12937,6 +13347,8 @@ var SidePanel = class {
12937
13347
  this.pin.setAttribute("aria-pressed", pinned ? "true" : "false");
12938
13348
  }
12939
13349
  destroy() {
13350
+ if (this.open) announceSurface(this.el, false, "panel");
13351
+ this.exit?.finish();
12940
13352
  this.el.remove();
12941
13353
  }
12942
13354
  /** The drag handle on the panel's inner (left) edge — the panel is docked right, so dragging
@@ -13241,8 +13653,8 @@ var ObjectTree = class extends SidePanel {
13241
13653
  set groups(next) {
13242
13654
  if (this.chart) this.groupsPerChart.set(this.chart, next);
13243
13655
  }
13244
- toggle(open2 = this.el.hidden) {
13245
- super.toggle(open2);
13656
+ toggle(open2 = !this.open, instant = false) {
13657
+ super.toggle(open2, instant);
13246
13658
  if (open2) this.refresh();
13247
13659
  }
13248
13660
  setSymbol(symbol) {
@@ -14276,8 +14688,8 @@ var DataWindow = class extends SidePanel {
14276
14688
  this.unsubs = [];
14277
14689
  injectStyles(STYLE_ID6, CSS7, host.ownerDocument);
14278
14690
  }
14279
- toggle(open2 = this.el.hidden) {
14280
- super.toggle(open2);
14691
+ toggle(open2 = !this.open, instant = false) {
14692
+ super.toggle(open2, instant);
14281
14693
  if (open2) this.refresh();
14282
14694
  }
14283
14695
  /** (Re)bind to a chart instance — called after every widget rebuild. */
@@ -14353,19 +14765,22 @@ var PanelDock = class {
14353
14765
  /**
14354
14766
  * (Re)build the CONTRIBUTED panels from the registry — call once after the built-ins, and
14355
14767
  * again on `refreshActions()` so a late registration appears. Contributed panels that are
14356
- * gone from the registry are dropped; the ones still there are rebuilt, so a replaced
14357
- * descriptor takes effect.
14768
+ * gone from the registry are dropped and a REPLACED descriptor is rebuilt; a panel whose
14769
+ * descriptor is unchanged stays mounted as it is, so its content and state survive.
14358
14770
  */
14359
14771
  refresh() {
14360
14772
  const openBefore = this.openId;
14361
- for (const entry of [...this.entries]) if (entry.contributed) this.drop(entry);
14362
- for (const desc of sidePanels()) {
14773
+ const registered = sidePanels();
14774
+ for (const entry of [...this.entries]) if (entry.contributed && !registered.includes(entry.desc)) this.drop(entry);
14775
+ for (const desc of registered) {
14776
+ if (this.entries.some((e) => e.desc === desc)) continue;
14363
14777
  const panel = new SidePanel(this.host, desc.title, `vela-panel-${desc.id}`, {
14364
14778
  width: desc.width,
14365
14779
  resizable: desc.resizable,
14366
14780
  minWidth: desc.minWidth,
14367
14781
  maxWidth: desc.maxWidth,
14368
- overlay: desc.overlay
14782
+ overlay: desc.overlay,
14783
+ maximizable: desc.maximizable
14369
14784
  });
14370
14785
  const entry = {
14371
14786
  id: desc.id,
@@ -14373,10 +14788,19 @@ var PanelDock = class {
14373
14788
  icon: desc.icon,
14374
14789
  order: desc.order ?? DEFAULT_PANEL_ORDER,
14375
14790
  panel,
14376
- contributed: true
14791
+ contributed: true,
14792
+ desc
14377
14793
  };
14378
14794
  try {
14379
- entry.handle = desc.mount(this.deps.context(), panel.content, { slot: panel.headerSlot, setTitle: (t) => panel.setTitle(t) }) ?? void 0;
14795
+ const header = {
14796
+ slot: panel.headerSlot,
14797
+ setTitle: (t) => panel.setTitle(t),
14798
+ get maximized() {
14799
+ return panel.maximized;
14800
+ },
14801
+ setMaximized: (on) => panel.setMaximized(on)
14802
+ };
14803
+ entry.handle = desc.mount(this.deps.context(), panel.content, header) ?? void 0;
14380
14804
  if (this.chart) entry.handle?.onChart?.(this.chart);
14381
14805
  } catch (err) {
14382
14806
  console.warn(`[vela] side panel "${desc.id}" failed to mount`, err);
@@ -14435,7 +14859,7 @@ var PanelDock = class {
14435
14859
  }
14436
14860
  this.pinned = new Set(state.pinned ?? []);
14437
14861
  for (const entry of this.entries) entry.panel.setOverlay(!this.pinned.has(entry.id));
14438
- for (const entry of this.entries) entry.panel.toggle(entry.id === state.open);
14862
+ for (const entry of this.entries) entry.panel.toggle(entry.id === state.open, true);
14439
14863
  this.pendingOpen = state.open && !this.entries.some((e) => e.id === state.open) ? state.open : null;
14440
14864
  }
14441
14865
  /** Drop the contributed panels (the shell destroys its own). */
@@ -14451,7 +14875,7 @@ var PanelDock = class {
14451
14875
  if (this.pinned.has(entry.id)) entry.panel.setOverlay(false);
14452
14876
  entry.panel.onOpenChange = (open2) => {
14453
14877
  if (open2) {
14454
- for (const other of this.entries) if (other !== entry) other.panel.toggle(false);
14878
+ for (const other of this.entries) if (other !== entry) other.panel.toggle(false, true);
14455
14879
  this.pendingOpen = null;
14456
14880
  }
14457
14881
  this.deps.chrome.setPanelActive(entry.id, open2);
@@ -16127,7 +16551,11 @@ var MoreDrawer = class {
16127
16551
  row(doc, label, opts) {
16128
16552
  const el = doc.createElement("div");
16129
16553
  el.className = "vela-md-row";
16130
- if (opts.icon) el.appendChild(iconEl(opts.icon, doc));
16554
+ if (opts.icon) {
16555
+ const icon2 = iconEl(opts.icon, doc);
16556
+ if (opts.glyph) icon2.innerHTML = opts.glyph;
16557
+ el.appendChild(icon2);
16558
+ }
16131
16559
  const text = doc.createElement("span");
16132
16560
  text.className = "vela-md-row-label";
16133
16561
  text.textContent = label;
@@ -16169,7 +16597,7 @@ var MoreDrawer = class {
16169
16597
  if (this.opts.layout) {
16170
16598
  const shape = this.opts.layout.shape();
16171
16599
  const value = shape ? `${shape.cols} \xD7 ${shape.rows}` : this.opts.layout.presets().find((p) => p.checked)?.label;
16172
- list.appendChild(this.row(doc, "Layout", { icon: "layout", value, chevron: true, onClick: () => this.show("layout") }));
16600
+ list.appendChild(this.row(doc, "Layout", { icon: "layout", glyph: this.opts.layout.glyph?.(), value, chevron: true, onClick: () => this.show("layout") }));
16173
16601
  }
16174
16602
  for (const act of this.opts.primaryActions?.() ?? []) list.appendChild(this.actionRow(doc, act));
16175
16603
  for (const panel of this.opts.panels()) {
@@ -16347,8 +16775,8 @@ var SETTINGS_SECTION = {
16347
16775
  "price-axis": "Scales and lines",
16348
16776
  "time-axis": "Scales and lines"
16349
16777
  };
16350
- function settingsItem(zone, label) {
16351
- return { id: `settings:${SETTINGS_SECTION[zone]}`, label, separatorBefore: true };
16778
+ function settingsItem(zone, label, icon2) {
16779
+ return { id: `settings:${SETTINGS_SECTION[zone]}`, label, ...icon2 ? { icon: icon2 } : {}, separatorBefore: true };
16352
16780
  }
16353
16781
  function settingsSectionOf(id) {
16354
16782
  return id.slice("settings:".length) || void 0;
@@ -16428,12 +16856,39 @@ function timeAxisItems(timezone) {
16428
16856
  }
16429
16857
  function bodyItems(counts) {
16430
16858
  return [
16431
- { id: "reset-view", label: "Reset chart view" },
16432
- { id: "remove-drawings", label: "Remove drawings", disabled: counts.drawings === 0, separatorBefore: true },
16433
- { id: "remove-indicators", label: "Remove indicators", disabled: counts.indicators === 0 },
16434
- settingsItem("body", "Settings\u2026")
16859
+ { id: "reset-view", label: "Reset chart view", icon: "reset" },
16860
+ { id: "remove-drawings", label: "Remove drawings", icon: "eraser", disabled: counts.drawings === 0, separatorBefore: true },
16861
+ { id: "remove-indicators", label: "Remove indicators", icon: "indicators", disabled: counts.indicators === 0 },
16862
+ settingsItem("body", "Settings\u2026", "gear")
16435
16863
  ];
16436
16864
  }
16865
+ var CONTEXT_MENU_BUILTIN_ORDER = {
16866
+ body: { "reset-view": -30, "remove-drawings": -20, "remove-indicators": -10, settings: 1e3 },
16867
+ "price-axis": {
16868
+ auto: -80,
16869
+ invert: -70,
16870
+ "scale:regular": -60,
16871
+ "scale:percent": -50,
16872
+ "scale:indexed": -40,
16873
+ "scale:log": -30,
16874
+ labels: -20,
16875
+ levels: -10,
16876
+ settings: 1e3
16877
+ },
16878
+ "time-axis": { timezone: -10, settings: 1e3 }
16879
+ };
16880
+ function composeMenu(zone, builtin, contributed) {
16881
+ const ranks = CONTEXT_MENU_BUILTIN_ORDER[zone];
16882
+ const rows = [];
16883
+ let group = 0;
16884
+ for (const item of builtin) {
16885
+ if (item.separatorBefore) group += 1;
16886
+ rows.push({ item, rank: ranks[item.id.startsWith("settings") ? "settings" : item.id] ?? 0, group });
16887
+ }
16888
+ for (const { item, order } of contributed) rows.push({ item, rank: order ?? 0, group: -1 });
16889
+ rows.sort((a, b) => a.rank - b.rank);
16890
+ return rows.map(({ item, group: g }, i) => ({ ...item, separatorBefore: i > 0 && rows[i - 1].group !== g }));
16891
+ }
16437
16892
 
16438
16893
  // src/widget/price-scale-drawer.ts
16439
16894
  var STYLE_ID17 = "vela-widget-price-scale-drawer";
@@ -17708,6 +18163,7 @@ function chromeHint(text, opts) {
17708
18163
  // src/renderers/native/drawings/DrawingToolbar.ts
17709
18164
  var TOOLBAR_WIDTH = 44;
17710
18165
  var TOOLBAR_COLLAPSED_WIDTH = 16;
18166
+ var FLYOUT_GAP = 3;
17711
18167
  var DrawingToolbar = class {
17712
18168
  constructor(host, theme, onArm, onMagnet = () => {
17713
18169
  }, onMeasure = () => {
@@ -17737,6 +18193,9 @@ var DrawingToolbar = class {
17737
18193
  // group id (or MAGNET_ID) whose flyout is open
17738
18194
  this.flyoutCell = null;
17739
18195
  // the cell the open flyout is anchored to
18196
+ /** Closed flyouts still playing their exit animation, by owner id — reopening the same
18197
+ * owner takes its element back instead of stacking a second one on it. */
18198
+ this.exitingFlyouts = /* @__PURE__ */ new Map();
17740
18199
  this.groupCells = /* @__PURE__ */ new Map();
17741
18200
  // the composite cell (hover/active bg + flyout anchor)
17742
18201
  this.groupIcons = /* @__PURE__ */ new Map();
@@ -17845,6 +18304,7 @@ var DrawingToolbar = class {
17845
18304
  }
17846
18305
  destroy() {
17847
18306
  this.closeFlyout();
18307
+ for (const { exit } of [...this.exitingFlyouts.values()]) exit.finish();
17848
18308
  for (const dispose of this.tipDisposers.splice(0)) dispose();
17849
18309
  this.root.remove();
17850
18310
  }
@@ -18072,16 +18532,22 @@ var DrawingToolbar = class {
18072
18532
  * outside-dismiss. Callers fill it with items. */
18073
18533
  beginFlyout(ownerId, cell) {
18074
18534
  const t = this.theme;
18075
- const fly = document.createElement("div");
18535
+ const back = this.exitingFlyouts.get(ownerId);
18536
+ if (back) {
18537
+ this.exitingFlyouts.delete(ownerId);
18538
+ back.exit.cancel();
18539
+ back.fly.replaceChildren();
18540
+ }
18541
+ const fly = back?.fly ?? document.createElement("div");
18076
18542
  fly.className = "vela-dtb-flyout";
18077
- 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;`;
18543
+ 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;`;
18078
18544
  applyChromeTokens(fly, t);
18079
18545
  this.host.appendChild(fly);
18080
18546
  const r = cell.getBoundingClientRect();
18081
18547
  const rootR = this.root.getBoundingClientRect();
18082
18548
  const hostR = this.host.getBoundingClientRect();
18083
18549
  const top = r.top - hostR.top;
18084
- fly.style.left = `${rootR.right - hostR.left - 1}px`;
18550
+ fly.style.left = `${rootR.right - hostR.left + FLYOUT_GAP}px`;
18085
18551
  fly.style.top = `${top}px`;
18086
18552
  fly.style.maxHeight = `${Math.max(120, hostR.height - top - 8)}px`;
18087
18553
  this.flyout = fly;
@@ -18097,6 +18563,7 @@ var DrawingToolbar = class {
18097
18563
  if (!cell) return;
18098
18564
  const fly = this.beginFlyout(group.id, cell);
18099
18565
  const sections = group.sections ?? [{ label: "", tools: group.tools }];
18566
+ const badge = sections.some((s) => s.tools.some((tool) => !!tool.icon));
18100
18567
  for (let si = 0; si < sections.length; si++) {
18101
18568
  const section = sections[si];
18102
18569
  if (section.label) {
@@ -18107,6 +18574,7 @@ var DrawingToolbar = class {
18107
18574
  fly.appendChild(
18108
18575
  this.makeFlyoutItem({
18109
18576
  icon: tool.icon,
18577
+ badge,
18110
18578
  label: tool.label,
18111
18579
  selected: tool.type === this.active,
18112
18580
  shortcut: this.shortcuts.get(tool.type),
@@ -18119,6 +18587,7 @@ var DrawingToolbar = class {
18119
18587
  );
18120
18588
  }
18121
18589
  }
18590
+ announceSurface(fly, true, "menu", cell);
18122
18591
  }
18123
18592
  openMagnetFlyout() {
18124
18593
  const cell = this.magnetCell;
@@ -18142,6 +18611,7 @@ var DrawingToolbar = class {
18142
18611
  })
18143
18612
  );
18144
18613
  }
18614
+ announceSurface(fly, true, "menu", cell);
18145
18615
  }
18146
18616
  flyoutHeader(text) {
18147
18617
  const t = this.theme;
@@ -18155,21 +18625,24 @@ var DrawingToolbar = class {
18155
18625
  div.style.cssText = `height:1px;margin:4px 8px;background:${this.borderColor};`;
18156
18626
  return div;
18157
18627
  }
18158
- /** A flyout row, left to right: optional leading icon, label, a check when it's the selected
18159
- * entry, an optional shortcut hint, and — at the far right — the favorite star (tool rows).
18160
- * Hover tint is CSS (`.vela-dtb-item:hover`), so navigating the menu stays smooth. */
18628
+ /** A flyout row, left to right: optional leading icon badge, label, a check when it's the
18629
+ * selected entry, an optional shortcut hint, and — at the far right — the favorite star (tool
18630
+ * rows). Hover tint is CSS (`.vela-dtb-item:hover`), so navigating the menu stays smooth. */
18161
18631
  makeFlyoutItem(opts) {
18162
18632
  const t = this.theme;
18163
18633
  const item = document.createElement("button");
18164
18634
  item.type = "button";
18165
18635
  item.className = "vela-dtb-item";
18166
18636
  item.setAttribute("aria-label", opts.label);
18167
- 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;`;
18168
- if (opts.icon) {
18169
- const icon2 = document.createElement("span");
18170
- icon2.style.cssText = "width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex:none;";
18171
- icon2.innerHTML = sizedIcon(opts.icon);
18172
- item.appendChild(icon2);
18637
+ const badged = opts.badge === true || !!opts.icon;
18638
+ const box = badged ? "gap:10px;padding:4px 10px 4px 6px;" : "gap:8px;padding:5px 10px 5px 8px;";
18639
+ 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;`;
18640
+ if (opts.selected) item.dataset.selected = "1";
18641
+ if (badged) {
18642
+ const badge = document.createElement("span");
18643
+ badge.className = "vela-dtb-badge";
18644
+ if (opts.icon) badge.innerHTML = opts.icon;
18645
+ item.appendChild(badge);
18173
18646
  }
18174
18647
  const label = document.createElement("span");
18175
18648
  label.textContent = opts.label;
@@ -18210,9 +18683,16 @@ var DrawingToolbar = class {
18210
18683
  }
18211
18684
  closeFlyout() {
18212
18685
  this.starEls.clear();
18213
- if (this.flyout) {
18214
- this.flyout.remove();
18686
+ const fly = this.flyout;
18687
+ if (fly) {
18215
18688
  this.flyout = null;
18689
+ announceSurface(fly, false, "menu", this.flyoutCell);
18690
+ const owner = this.flyoutOwnerId ?? "";
18691
+ const exit = holdForExit(fly, () => {
18692
+ if (this.exitingFlyouts.get(owner)?.fly === fly) this.exitingFlyouts.delete(owner);
18693
+ fly.remove();
18694
+ });
18695
+ if (exit) this.exitingFlyouts.set(owner, { fly, exit });
18216
18696
  this.flyoutCell?.classList.remove("vela-open");
18217
18697
  this.flyoutCell = null;
18218
18698
  this.flyoutOwnerId = null;
@@ -18296,6 +18776,11 @@ function ensureStyles() {
18296
18776
  .vela-dtb[data-collapsed='1'] .vela-dtb-collapse .vela-dtb-hit{width:14px;}
18297
18777
  .vela-dtb-item{background:transparent;border:none;transition:background var(--vela-dur-fast) ease;}
18298
18778
  .vela-dtb-item:hover{background:var(--vela-hover-strong);}
18779
+ .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;}
18780
+ .vela-dtb-badge:empty{background:transparent;border-color:transparent;}
18781
+ .vela-dtb-badge svg{width:100%;height:100%;}
18782
+ .vela-dtb-item:hover .vela-dtb-badge:not(:empty){background:var(--vela-active);color:var(--vela-fg-bright);}
18783
+ .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);}
18299
18784
  .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;}
18300
18785
  .vela-dtb-star svg{width:16px;height:16px;}
18301
18786
  .vela-dtb-item:hover .vela-dtb-star{opacity:.55;}
@@ -18663,15 +19148,48 @@ function syncTargets(originId, setting, cellIds) {
18663
19148
  function rangesWithin(a, b, epsMs) {
18664
19149
  return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
18665
19150
  }
18666
- var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
18667
- function styleConfigSlice(config) {
18668
- if (config == null || typeof config !== "object") return null;
18669
- const doc = config;
18670
- const out = {};
19151
+ var STYLE_SYNC_CONFIG_KEYS = [
19152
+ "layout",
19153
+ "panes",
19154
+ "grid",
19155
+ "margins",
19156
+ "priceScale",
19157
+ "crosshair",
19158
+ "animations",
19159
+ "candles",
19160
+ "bars",
19161
+ "line",
19162
+ "area",
19163
+ "baseline",
19164
+ "sessions"
19165
+ ];
19166
+ function styleConfigPatch(origin, follower) {
19167
+ const from = asRecord(origin);
19168
+ const to = asRecord(follower);
19169
+ if (!from || !to) return null;
19170
+ const patch = {};
18671
19171
  for (const key of STYLE_SYNC_CONFIG_KEYS) {
18672
- if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
19172
+ const block = asRecord(from[key]);
19173
+ if (block && JSON.stringify(block) !== JSON.stringify(to[key])) patch[key] = block;
18673
19174
  }
18674
- return Object.keys(out).length > 0 ? out : null;
19175
+ const spacing = asRecord(from.series)?.spacing;
19176
+ if (typeof spacing === "number" && spacing !== asRecord(to.series)?.spacing) patch.series = { spacing };
19177
+ const fromTypes = asRecord(from.chartTypes) ?? {};
19178
+ const toTypes = asRecord(to.chartTypes) ?? {};
19179
+ const types = {};
19180
+ for (const typeId of /* @__PURE__ */ new Set([...Object.keys(fromTypes), ...Object.keys(toTypes)])) {
19181
+ const src = asRecord(fromTypes[typeId]) ?? {};
19182
+ const dst = asRecord(toTypes[typeId]) ?? {};
19183
+ for (const key of CANDLE_OVERRIDE_KEYS) {
19184
+ const value = src[key] ?? null;
19185
+ if (value !== (dst[key] ?? null)) (types[typeId] ?? (types[typeId] = {}))[key] = value;
19186
+ }
19187
+ }
19188
+ if (Object.keys(types).length > 0) patch.chartTypes = types;
19189
+ return Object.keys(patch).length > 0 ? patch : null;
19190
+ }
19191
+ function asRecord(v) {
19192
+ return v != null && typeof v === "object" ? v : null;
18675
19193
  }
18676
19194
 
18677
19195
  // src/workspace/persist.ts
@@ -18881,7 +19399,7 @@ var IndicatorHandleImpl = class {
18881
19399
  this.controller.applyProps(this.id, values);
18882
19400
  }
18883
19401
  updateCode(source) {
18884
- this.controller.updateCode(this.id, source);
19402
+ return this.controller.updateCode(this.id, source);
18885
19403
  }
18886
19404
  setVisible(visible) {
18887
19405
  this.controller.setVisible(this.id, visible);
@@ -19360,6 +19878,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19360
19878
  this.activeEngineStyle = null;
19361
19879
  /** The chart's current price style (tracked from the renderer's change events + initial read). */
19362
19880
  this.priceStyle = "candles";
19881
+ this.priceStyleWillChangeUnsub = null;
19363
19882
  this.unresolvedUnsub = null;
19364
19883
  /** Latest chart visible range (left/right bar times), fed to viewport-dependent scripts. */
19365
19884
  this.visibleRange = null;
@@ -19458,6 +19977,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19458
19977
  this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
19459
19978
  this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
19460
19979
  this.renderer.onPriceStyleChange?.((style) => this.syncPriceStyle(style));
19980
+ this.priceStyleWillChangeUnsub = this.renderer.onPriceStyleWillChange?.((from, to) => this.events.emit("priceStyle:change", { from, to })) ?? null;
19461
19981
  const initialStyle = this.renderer.readFeature("priceStyle");
19462
19982
  if (typeof initialStyle === "string") this.priceStyle = initialStyle;
19463
19983
  this.barTransform = barTransformFor(initialStyle);
@@ -20744,20 +21264,26 @@ var EngineOrchestrator = class _EngineOrchestrator {
20744
21264
  /**
20745
21265
  * IndicatorController: replace a SCRIPT indicator's source in place (see
20746
21266
  * {@link IndicatorHandle.updateCode}). Prepare-first: the running session keeps
20747
- * computing until the new source has compiled, so a broken edit costs the user
20748
- * nothing but an `error` event. Natives have no script — warn and leave them alone.
21267
+ * computing until the new source has compiled; then the new code runs on TRIAL until
21268
+ * its first model lands, and a failure before that puts the previous code back.
21269
+ * Natives have no script — warn and leave them alone.
20749
21270
  */
20750
21271
  updateCode(id, source) {
20751
21272
  const record = this.registry.get(id);
20752
21273
  const handle = this.handles.get(id);
20753
- if (!record || !handle) return;
21274
+ if (!record || !handle) return Promise.resolve({ ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator is no longer on the chart.`) });
20754
21275
  if (record.native) {
20755
21276
  console.warn(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`);
20756
- return;
21277
+ return Promise.resolve({ ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`) });
20757
21278
  }
20758
- if (source === record.source && record.pendingSource === void 0) return;
20759
- record.pendingSource = source;
20760
- void this.swapSource(id, source, handle);
21279
+ const busy = record.pendingSource !== void 0 || record.codeTrial !== void 0;
21280
+ if (!busy && source === record.source) return Promise.resolve({ ok: true, error: null });
21281
+ return new Promise((resolve) => {
21282
+ (record.codeWaiters ?? (record.codeWaiters = [])).push(resolve);
21283
+ if (record.pendingSource === source || record.pendingSource === void 0 && record.codeTrial && record.source === source) return;
21284
+ record.pendingSource = source;
21285
+ void this.swapSource(id, source, handle);
21286
+ });
20761
21287
  }
20762
21288
  async swapSource(id, source, handle) {
20763
21289
  const engine = this.registry.get(id)?.engine ?? this.engineFor(this.registry.get(id)?.options?.language);
@@ -20768,26 +21294,41 @@ var EngineOrchestrator = class _EngineOrchestrator {
20768
21294
  const record2 = this.registry.get(id);
20769
21295
  if (record2?.pendingSource === source) {
20770
21296
  record2.pendingSource = void 0;
20771
- this.fail(id, handle, err);
21297
+ this.rejectCode(id, record2, handle, err);
20772
21298
  }
20773
21299
  return;
20774
21300
  }
20775
21301
  const record = this.registry.get(id);
20776
21302
  if (!record || record.pendingSource !== source) return;
20777
21303
  record.pendingSource = void 0;
21304
+ const previous = record.codeTrial?.previous ?? {
21305
+ source: record.source,
21306
+ engine: record.engine,
21307
+ prepared: record.prepared,
21308
+ inputValues: record.inputValues,
21309
+ propValues: record.propValues,
21310
+ inputs: [...handle.inputs],
21311
+ props: [...handle.props]
21312
+ };
21313
+ record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues, record.prepared?.inputs);
21314
+ record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues, record.prepared?.props);
20778
21315
  record.source = source;
20779
21316
  record.engine = engine;
20780
21317
  record.prepared = prepared;
20781
21318
  handle.setSource(source);
20782
- record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues);
20783
- record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues);
20784
21319
  handle.setSchema(prepared.inputs);
20785
21320
  handle.setPropsSchema(prepared.props ?? []);
20786
21321
  record.session?.stop();
20787
21322
  record.session = void 0;
20788
21323
  record.pendingStructural = true;
20789
21324
  record.pendingCause = "code";
20790
- if (record.hidden) return;
21325
+ if (record.hidden) {
21326
+ record.codeTrial = void 0;
21327
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
21328
+ this.settleCode(record, { ok: true, error: null });
21329
+ return;
21330
+ }
21331
+ record.codeTrial = { previous };
20791
21332
  if (record.renderHandle) {
20792
21333
  this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
20793
21334
  this.setLoading(record, true);
@@ -20795,6 +21336,46 @@ var EngineOrchestrator = class _EngineOrchestrator {
20795
21336
  this.mountLoadingPlaceholder(id, record);
20796
21337
  }
20797
21338
  this.executeIndicator(id, handle);
21339
+ if (this.bars.length === 0 && record.codeTrial) {
21340
+ record.codeTrial = void 0;
21341
+ this.settleCode(record, { ok: true, error: null });
21342
+ }
21343
+ }
21344
+ /** Resolve every `updateCode` caller waiting on this record's chain of edits. */
21345
+ settleCode(record, result) {
21346
+ const waiters = record.codeWaiters;
21347
+ record.codeWaiters = void 0;
21348
+ if (waiters) for (const resolve of waiters) resolve(result);
21349
+ }
21350
+ /**
21351
+ * An update failed — at prepare, or on trial before its first model. A trial puts the
21352
+ * previous code back (source, engine, prepared script, values, schema) and restarts it;
21353
+ * its visuals were never dropped, so the chart keeps painting throughout. Then the
21354
+ * failure reports the usual way and every waiting caller learns it.
21355
+ */
21356
+ rejectCode(id, record, handle, err) {
21357
+ const trial = record.codeTrial;
21358
+ record.codeTrial = void 0;
21359
+ this.fail(id, handle, err);
21360
+ if (trial) {
21361
+ const p = trial.previous;
21362
+ record.session?.stop();
21363
+ record.session = void 0;
21364
+ record.source = p.source;
21365
+ record.engine = p.engine;
21366
+ record.prepared = p.prepared;
21367
+ record.inputValues = p.inputValues;
21368
+ record.propValues = p.propValues;
21369
+ handle.setSource(p.source);
21370
+ handle.setSchema(p.inputs);
21371
+ handle.setPropsSchema(p.props);
21372
+ record.pendingStructural = true;
21373
+ record.pendingCause = void 0;
21374
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
21375
+ if (!record.hidden) this.executeIndicator(id, handle);
21376
+ }
21377
+ const error = err instanceof Error ? err : new Error(String(err));
21378
+ this.settleCode(record, { ok: false, error });
20798
21379
  }
20799
21380
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
20800
21381
  /** Live handles of every indicator on the chart (script + native), insertion order. */
@@ -20809,6 +21390,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20809
21390
  removeIndicator(id) {
20810
21391
  const record = this.registry.remove(id);
20811
21392
  if (record?.native?.type === "volume") this.volumeOptedOut = true;
21393
+ if (record) this.settleCode(record, { ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator was removed.`) });
20812
21394
  record?.session?.stop();
20813
21395
  if (record?.native?.started) record.native.instance.stop();
20814
21396
  this.handles.delete(id);
@@ -20836,6 +21418,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
20836
21418
  if (!visible) {
20837
21419
  record.session?.stop();
20838
21420
  record.session = void 0;
21421
+ if (record.codeTrial) {
21422
+ record.codeTrial = void 0;
21423
+ this.settleCode(record, { ok: true, error: null });
21424
+ }
20839
21425
  if (record.native?.started) record.native.instance.suspend();
20840
21426
  if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
20841
21427
  else if (record.native) this.mountHiddenNativeRow(id, record);
@@ -20922,7 +21508,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
20922
21508
  if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
20923
21509
  this.viewportUnsub?.();
20924
21510
  this.paneActionUnsub?.();
21511
+ this.priceStyleWillChangeUnsub?.();
21512
+ this.priceStyleWillChangeUnsub = null;
20925
21513
  for (const record of this.registry.all()) {
21514
+ this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
20926
21515
  record.session?.stop();
20927
21516
  if (record.native?.started) record.native.instance.stop();
20928
21517
  }
@@ -20991,7 +21580,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
20991
21580
  const record = this.registry.get(id);
20992
21581
  if (!record || !record.engine || !record.prepared || record.hidden) return;
20993
21582
  const mode = this.config.live && !record.prepared.reactsToViewport && record.engine.capabilities.streaming ? "live" : "static";
20994
- record.session = record.engine.execute(
21583
+ const prepared = record.prepared;
21584
+ const session = record.engine.execute(
20995
21585
  {
20996
21586
  prepared: record.prepared,
20997
21587
  market: this.market(),
@@ -21012,6 +21602,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
21012
21602
  const cause = record.pendingCause ?? "history";
21013
21603
  if (!this.applyModel(id, model)) return;
21014
21604
  record.pendingCause = void 0;
21605
+ if (record.codeTrial) {
21606
+ record.codeTrial = void 0;
21607
+ this.settleCode(record, { ok: true, error: null });
21608
+ }
21015
21609
  this.emitContextChanged(id);
21016
21610
  this.emitScriptRun(id, cause, first);
21017
21611
  },
@@ -21022,9 +21616,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
21022
21616
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
21023
21617
  },
21024
21618
  onWarning: (w) => this.events.emit("warning", w),
21025
- onError: (err) => this.fail(id, handle, err)
21619
+ onError: (err) => {
21620
+ if (record.codeTrial) this.rejectCode(id, record, handle, err);
21621
+ else this.fail(id, handle, err);
21622
+ }
21026
21623
  }
21027
21624
  );
21625
+ if (record.prepared !== prepared) {
21626
+ session.stop();
21627
+ return;
21628
+ }
21629
+ record.session = session;
21028
21630
  this.emitContextChanged(id);
21029
21631
  }
21030
21632
  /**
@@ -21525,7 +22127,7 @@ function yieldToPaint() {
21525
22127
  }
21526
22128
  return new Promise((resolve) => setTimeout(resolve, 0));
21527
22129
  }
21528
- function valuesOnSchema(schema, previous) {
22130
+ function valuesOnSchema(schema, previous, oldSchema = []) {
21529
22131
  const out = {};
21530
22132
  const declared = /* @__PURE__ */ new Set();
21531
22133
  for (const s of schema) {
@@ -21533,9 +22135,22 @@ function valuesOnSchema(schema, previous) {
21533
22135
  declared.add(s.key);
21534
22136
  declared.add(s.title);
21535
22137
  }
21536
- for (const [k, v] of Object.entries(previous)) if (declared.has(k)) out[k] = v;
22138
+ const oldDefaults = /* @__PURE__ */ new Map();
22139
+ for (const s of oldSchema) {
22140
+ oldDefaults.set(s.key, s.defval);
22141
+ if (!oldDefaults.has(s.title)) oldDefaults.set(s.title, s.defval);
22142
+ }
22143
+ for (const [k, v] of Object.entries(previous)) {
22144
+ if (!declared.has(k)) continue;
22145
+ if (oldDefaults.has(k) && sameInputValue(oldDefaults.get(k), v)) continue;
22146
+ out[k] = v;
22147
+ }
21537
22148
  return out;
21538
22149
  }
22150
+ function sameInputValue(a, b) {
22151
+ if (a === b) return true;
22152
+ return typeof a === "object" && a !== null && typeof b === "object" && b !== null && JSON.stringify(a) === JSON.stringify(b);
22153
+ }
21539
22154
  function blankedModel(model) {
21540
22155
  return {
21541
22156
  ...model,
@@ -23100,7 +23715,10 @@ var LEGEND_ICON_PX2 = 16;
23100
23715
  var LEGEND_CTL_PX = 18;
23101
23716
  var LEGEND_ROW_PAD_Y = 2;
23102
23717
  var LEGEND_ROW_PAD_X = 6;
23718
+ var LEGEND_LEFT_PX = 10;
23719
+ var LEGEND_SCALE_GAP_PX = 6;
23103
23720
  var LEGEND_TITLE_VALUES_GAP_PX = 6;
23721
+ var LEGEND_VALUES_GAP_PX = 5;
23104
23722
  var LEGEND_TITLE_STATUS_GAP_PX = 8;
23105
23723
  var LEGEND_ACTION_GAP_PX = 6;
23106
23724
  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;`;
@@ -23386,14 +24004,15 @@ var InputsUI = class {
23386
24004
  row.valuesEl.replaceChildren();
23387
24005
  return;
23388
24006
  }
23389
- row.valuesEl.style.display = "inline-flex";
24007
+ row.valuesEl.style.display = "block";
23390
24008
  row.valuesEl.replaceChildren();
23391
- for (const v of row.plotValues) {
24009
+ row.plotValues.forEach((v, i) => {
23392
24010
  const span = document.createElement("span");
23393
24011
  span.textContent = v.value;
23394
24012
  span.style.color = v.color;
24013
+ if (i > 0) span.style.marginLeft = `${LEGEND_VALUES_GAP_PX}px`;
23395
24014
  row.valuesEl.appendChild(span);
23396
- }
24015
+ });
23397
24016
  }
23398
24017
  // ── legend move/merge (menu + drag) ─────────────────────────────────────
23399
24018
  /** Open the "Move to" menu for a row, anchored under its move button. */
@@ -23494,7 +24113,7 @@ var InputsUI = class {
23494
24113
  if (!lg) {
23495
24114
  lg = document.createElement("div");
23496
24115
  lg.dataset.velaPane = paneId;
23497
- 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;";
24116
+ 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;`;
23498
24117
  applyChromeTokens(lg, this.theme);
23499
24118
  this.positionLegend(lg, paneId);
23500
24119
  this.container.appendChild(lg);
@@ -23506,6 +24125,7 @@ var InputsUI = class {
23506
24125
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
23507
24126
  lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
23508
24127
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
24128
+ lg.style.maxWidth = `calc(100% - ${(bounds.rightAxis ?? 0) + LEGEND_LEFT_PX + LEGEND_SCALE_GAP_PX}px)`;
23509
24129
  const collapsed = this.paneCollapse.has(paneId);
23510
24130
  const masterId = this.paneCollapse.get(paneId) ?? null;
23511
24131
  for (const row of this.rows.values()) {
@@ -23597,7 +24217,7 @@ var InputsUI = class {
23597
24217
  }
23598
24218
  ensureDialogStyles();
23599
24219
  const el = document.createElement("div");
23600
- 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;`;
24220
+ 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;`;
23601
24221
  el.addEventListener("mouseleave", () => {
23602
24222
  if (this.selectedId !== id) this.setRowHighlighted(id, false);
23603
24223
  });
@@ -23625,7 +24245,7 @@ var InputsUI = class {
23625
24245
  const statusEl = document.createElement("span");
23626
24246
  statusEl.style.cssText = "display:none;box-sizing:border-box;flex:none;";
23627
24247
  const titleWrap = document.createElement("span");
23628
- titleWrap.style.cssText = "white-space:nowrap;";
24248
+ titleWrap.style.cssText = "white-space:nowrap;min-width:0;overflow:hidden;text-overflow:ellipsis;";
23629
24249
  titleWrap.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
23630
24250
  const titleEl = document.createElement("span");
23631
24251
  titleEl.textContent = title;
@@ -23643,7 +24263,7 @@ var InputsUI = class {
23643
24263
  el.appendChild(calloutsEl);
23644
24264
  el.appendChild(statusEl);
23645
24265
  const valuesEl = document.createElement("span");
23646
- 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;`;
24266
+ 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;`;
23647
24267
  el.appendChild(valuesEl);
23648
24268
  const controlsEl = document.createElement("span");
23649
24269
  controlsEl.style.cssText = `display:none;align-items:center;gap:${LEGEND_ACTION_GAP_PX}px;flex:none;margin-left:${LEGEND_ACTION_GAP_PX}px;`;
@@ -24465,15 +25085,23 @@ var Batch = class {
24465
25085
  this.seg(x + w, y + h, x, y + h, lineWidth, c);
24466
25086
  this.seg(x, y + h, x, y, lineWidth, c);
24467
25087
  }
24468
- /** Filled circle (triangle fan). */
25088
+ /**
25089
+ * Filled circle (triangle fan), feathered like a line: the rim is padded by {@link AA_PAD} and
25090
+ * edgeDist runs from 0 at the center to the padded radius at the rim, with edgeHalf = r. The
25091
+ * distance grows linearly along each spoke, so the shader's line feather gives a round soft edge.
25092
+ */
24469
25093
  circle(cx, cy, r, c, segments = 16) {
24470
- let px = cx + r;
25094
+ const ext = r + AA_PAD;
25095
+ let px = cx + ext;
24471
25096
  let py = cy;
25097
+ this.ensure(segments * 3);
24472
25098
  for (let i = 1; i <= segments; i += 1) {
24473
25099
  const a = i / segments * Math.PI * 2;
24474
- const nx = cx + Math.cos(a) * r;
24475
- const ny = cy + Math.sin(a) * r;
24476
- this.tri(cx, cy, c, px, py, c, nx, ny, c);
25100
+ const nx = cx + Math.cos(a) * ext;
25101
+ const ny = cy + Math.sin(a) * ext;
25102
+ this.v(cx, cy, c, 0, r);
25103
+ this.v(px, py, c, ext, r);
25104
+ this.v(nx, ny, c, ext, r);
24477
25105
  px = nx;
24478
25106
  py = ny;
24479
25107
  }
@@ -24654,7 +25282,7 @@ var WebGL2Backend = class {
24654
25282
  }
24655
25283
  mount(canvas) {
24656
25284
  this.canvas = canvas;
24657
- this.gl = canvas.getContext("webgl2", { alpha: true, antialias: true, depth: false, stencil: false, premultipliedAlpha: true });
25285
+ this.gl = canvas.getContext("webgl2", { alpha: true, antialias: false, depth: false, stencil: false, premultipliedAlpha: true });
24658
25286
  if (!this.gl) return;
24659
25287
  if (!this.initGL()) {
24660
25288
  this.gl = null;
@@ -25879,6 +26507,11 @@ var InputController = class {
25879
26507
  /** When true (the default), the separators between stacked panes are draggable to
25880
26508
  * resize the panes above/below. When false, a press there is a normal data-area pan. */
25881
26509
  this.paneResize = true;
26510
+ /** When true (the default), the user drawings take part in pointer input: they claim a
26511
+ * press over a drawing or with a tool armed, and see hover moves. When false, every press
26512
+ * is a plain pan/click and hover leaves them alone; a drawing gesture already under way
26513
+ * still finishes. */
26514
+ this.drawings = true;
25882
26515
  this.el = null;
25883
26516
  this.dragging = false;
25884
26517
  this.moved = false;
@@ -25942,18 +26575,18 @@ var InputController = class {
25942
26575
  this.onModifier = (e) => {
25943
26576
  if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
25944
26577
  if (Number.isNaN(this.cursorX)) return;
25945
- if (this.dragging && this.region !== "drawing") return;
26578
+ if (this.dragging ? this.region !== "drawing" : !this.drawings) return;
25946
26579
  this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
25947
26580
  };
25948
26581
  this.onDown = (e) => {
25949
26582
  if (e.button === 1) {
25950
26583
  const { x: x2, y: y2 } = this.local(e);
25951
- this.middleDeleted = this.deps.drawingsDeleteAt?.(x2, y2) ?? false;
26584
+ this.middleDeleted = this.drawings && (this.deps.drawingsDeleteAt?.(x2, y2) ?? false);
25952
26585
  if (this.middleDeleted) e.preventDefault();
25953
26586
  return;
25954
26587
  }
25955
26588
  if (e.button === 2) {
25956
- this.rightCancelled = this.deps.drawingsCancelPlacement?.() ?? false;
26589
+ this.rightCancelled = this.drawings && (this.deps.drawingsCancelPlacement?.() ?? false);
25957
26590
  if (this.rightCancelled) e.preventDefault();
25958
26591
  return;
25959
26592
  }
@@ -25975,20 +26608,20 @@ var InputController = class {
25975
26608
  this.startY = y;
25976
26609
  this.prevPressDrawing = this.lastPressDrawing;
25977
26610
  this.lastPressDrawing = false;
25978
- if (this.deps.drawingsClaim?.(x, y)) {
26611
+ if (this.drawings && this.deps.drawingsClaim?.(x, y)) {
25979
26612
  this.region = "drawing";
25980
26613
  this.lastPressDrawing = true;
25981
26614
  this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
25982
26615
  this.capture(e.pointerId);
25983
26616
  return;
25984
26617
  }
25985
- if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
26618
+ if (this.drawings && e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
25986
26619
  this.region = "drawing";
25987
26620
  this.lastPressDrawing = true;
25988
26621
  this.capture(e.pointerId);
25989
26622
  return;
25990
26623
  }
25991
- if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
26624
+ if (this.drawings && (e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
25992
26625
  this.region = "drawing";
25993
26626
  this.lastPressDrawing = true;
25994
26627
  this.capture(e.pointerId);
@@ -26065,7 +26698,7 @@ var InputController = class {
26065
26698
  const drawCursor = this.deps.drawingsCursor?.(x, y);
26066
26699
  const r = this.regionAt(x, y);
26067
26700
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
26068
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
26701
+ if (this.drawings) this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
26069
26702
  }
26070
26703
  if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
26071
26704
  };
@@ -26106,7 +26739,7 @@ var InputController = class {
26106
26739
  this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
26107
26740
  } else if (tapRelease && this.region === "data") {
26108
26741
  this.deps.onClick(x, y);
26109
- } else if (this.dragging && this.region === "data") {
26742
+ } else if (this.dragging && this.region === "data" && wasTouch) {
26110
26743
  const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
26111
26744
  if (!stale && Math.abs(this.vx) > FLING_MIN_SPEED) {
26112
26745
  const pitch = this.deps.getCoords().pxPerBar();
@@ -26307,7 +26940,7 @@ var InputController = class {
26307
26940
  }
26308
26941
  /** Shared double-click / double-tap routing, by the region under the point. */
26309
26942
  doubleActivate(x, y) {
26310
- if (this.deps.drawingsDblClick?.(x, y)) return;
26943
+ if (this.drawings && this.deps.drawingsDblClick?.(x, y)) return;
26311
26944
  const region = this.regionAt(x, y);
26312
26945
  if (region === "price") this.deps.resetPriceScale(x, y);
26313
26946
  else if (region === "separator") this.deps.resetPaneSize(y);
@@ -28321,10 +28954,13 @@ function priceTicks(min, max, target = 6) {
28321
28954
  const norm = raw / mag;
28322
28955
  const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
28323
28956
  const decimals = Math.max(0, -Math.floor(Math.log10(step)) + 1);
28324
- const out = [];
28325
28957
  const start = Math.ceil(min / step) * step;
28326
- for (let v = start; v <= max + step * 1e-6; v += step) {
28327
- out.push(Number(v.toFixed(decimals)));
28958
+ if (!(step > 0) || start + step === start) return [];
28959
+ const count = Math.floor((max - start) / step + 1e-6) + 1;
28960
+ if (!(count > 0) || count > 1e3) return [];
28961
+ const out = [];
28962
+ for (let i = 0; i < count; i += 1) {
28963
+ out.push(Number((start + i * step).toFixed(decimals)));
28328
28964
  }
28329
28965
  return out;
28330
28966
  }
@@ -28522,6 +29158,100 @@ function calendarTicks(zFrom, zTo, months, offsetMs) {
28522
29158
  return out;
28523
29159
  }
28524
29160
 
29161
+ // src/renderers/native/chrome/priceAxisTicks.ts
29162
+ var PriceAxisTickSource = class {
29163
+ constructor() {
29164
+ this.fn = null;
29165
+ this.memo = /* @__PURE__ */ new WeakMap();
29166
+ this.warned = false;
29167
+ }
29168
+ /** The host function, or null for the built-in ticks. */
29169
+ get hook() {
29170
+ return this.fn;
29171
+ }
29172
+ /** Set (or clear) the host function. Drops every memoized result, so setting the same
29173
+ * function again recomputes — how a host refreshes ticks that depend on its own state. */
29174
+ setHook(fn) {
29175
+ this.fn = fn;
29176
+ this.memo = /* @__PURE__ */ new WeakMap();
29177
+ }
29178
+ /** The pane's master-scale ticks for this frame. An unscaled pane (`axisFormat: 'none'`)
29179
+ * gets none, and the host is never asked for it. */
29180
+ ticksFor(scene, pane, coords) {
29181
+ const format = pane.axisFormat;
29182
+ if (format === "none") return [];
29183
+ const pct = percentScaleFor(scene, pane);
29184
+ const height = pane.bounds.height;
29185
+ const fn = this.fn;
29186
+ if (!fn || !(height > 0)) return paneAxisTicks(pane.scale, height, pct, scene.priceMintick, format);
29187
+ const s = pane.scale;
29188
+ const m = this.memo.get(pane);
29189
+ const fontSize = scene.style.fontSize;
29190
+ 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) {
29191
+ return m.ticks;
29192
+ }
29193
+ const defaults2 = paneAxisTicks(s, height, pct, scene.priceMintick, format);
29194
+ const ticks = this.callHook(fn, scene, pane, coords, defaults2, pct?.baseline, pct ? pct.indexed ? "indexed" : "percent" : "price");
29195
+ this.memo.set(pane, {
29196
+ kind: pane.kind,
29197
+ min: s.min,
29198
+ max: s.max,
29199
+ log: !!s.log,
29200
+ invert: !!s.invert,
29201
+ height,
29202
+ baseline: pct?.baseline,
29203
+ indexed: !!pct?.indexed,
29204
+ mintick: scene.priceMintick,
29205
+ format,
29206
+ fontSize,
29207
+ ticks
29208
+ });
29209
+ return ticks;
29210
+ }
29211
+ callHook(fn, scene, pane, coords, defaults2, baseline, mode) {
29212
+ const scale = { min: pane.scale.min, max: pane.scale.max, log: !!pane.scale.log, invert: !!pane.scale.invert };
29213
+ const bounds = { top: 0, height: pane.bounds.height };
29214
+ const ctx = {
29215
+ pane: { id: pane.id, kind: pane.kind },
29216
+ min: Math.min(scale.min, scale.max),
29217
+ max: Math.max(scale.min, scale.max),
29218
+ log: !!scale.log,
29219
+ height: bounds.height,
29220
+ mode,
29221
+ baseline,
29222
+ mintick: scene.priceMintick,
29223
+ fontSize: scene.style.fontSize,
29224
+ priceToY: (price) => coords.priceToY(price, scale, bounds),
29225
+ defaults: defaults2
29226
+ };
29227
+ let out;
29228
+ try {
29229
+ out = fn(ctx);
29230
+ } catch (err) {
29231
+ this.warnOnce("threw", err);
29232
+ return defaults2;
29233
+ }
29234
+ if (out == null) return defaults2;
29235
+ if (!Array.isArray(out)) {
29236
+ this.warnOnce("returned a non-array", out);
29237
+ return defaults2;
29238
+ }
29239
+ const ticks = [];
29240
+ for (const t of out) {
29241
+ if (!t || typeof t !== "object") continue;
29242
+ const { price, label, major } = t;
29243
+ if (typeof price !== "number" || !Number.isFinite(price) || typeof label !== "string") continue;
29244
+ ticks.push(typeof major === "boolean" ? { price, label, major } : { price, label });
29245
+ }
29246
+ return ticks;
29247
+ }
29248
+ warnOnce(what, detail) {
29249
+ if (this.warned) return;
29250
+ this.warned = true;
29251
+ console.warn(`[vela] the priceAxisTicks function ${what} \u2014 the default ticks are used instead.`, detail);
29252
+ }
29253
+ };
29254
+
28525
29255
  // src/renderers/native/chrome/countdown.ts
28526
29256
  function countdownText(barOpen, barMs, now2) {
28527
29257
  if (!(barMs > 0)) return null;
@@ -28874,8 +29604,11 @@ function standaloneSvg(markup, ink, px) {
28874
29604
  }
28875
29605
 
28876
29606
  // src/renderers/native/chrome/ChromeRenderer.ts
29607
+ var MINOR_LABEL_ALPHA = 0.5;
28877
29608
  var ChromeRenderer = class {
28878
- constructor() {
29609
+ /** `axisTicks` is shared with the backdrop layer, so price labels and gridlines agree. */
29610
+ constructor(axisTicks = new PriceAxisTickSource()) {
29611
+ this.axisTicks = axisTicks;
28879
29612
  this.canvas = null;
28880
29613
  this.ctx = null;
28881
29614
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
@@ -28980,7 +29713,8 @@ var ChromeRenderer = class {
28980
29713
  this.drawTimeAxis(ctx, scene, coords, theme, dataW, dataH, fullH);
28981
29714
  this.drawMarkLane(ctx, scene, coords, theme, dataW, dataH);
28982
29715
  }
28983
- /** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge. */
29716
+ /** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge,
29717
+ * but clipped to the data area so a glyph on the edge bar never paints over the price scale. */
28984
29718
  drawMarkLane(ctx, scene, coords, theme, dataW, dataH) {
28985
29719
  if (!scene.marks.visible || scene.timelineMarks.length === 0) {
28986
29720
  this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
@@ -28998,6 +29732,10 @@ var ChromeRenderer = class {
28998
29732
  expanded: scene.marksExpandedStack
28999
29733
  });
29000
29734
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
29735
+ ctx.save();
29736
+ ctx.beginPath();
29737
+ ctx.rect(0, 0, dataW, dataH);
29738
+ ctx.clip();
29001
29739
  paintMarkLane(ctx, this.markLayout, {
29002
29740
  axisY: dataH,
29003
29741
  background: theme.background,
@@ -29011,6 +29749,7 @@ var ChromeRenderer = class {
29011
29749
  flashKey: scene.marksFlash && scene.marksFlash.until > nowMs ? scene.marksFlash.key : null,
29012
29750
  nowMs
29013
29751
  });
29752
+ ctx.restore();
29014
29753
  }
29015
29754
  barTimes(scene) {
29016
29755
  if (this.barTimesSrc !== scene.bars || this.barTimesCache.length !== scene.bars.length) {
@@ -29067,13 +29806,19 @@ var ChromeRenderer = class {
29067
29806
  if (!scene.showAxisLabels) return;
29068
29807
  ctx.fillStyle = this.axisTextColor;
29069
29808
  ctx.textAlign = "left";
29809
+ const font = `${scene.style.fontSize}px ${theme.fontFamily}`;
29810
+ const majorFont = `600 ${font}`;
29811
+ const alpha = ctx.globalAlpha;
29070
29812
  for (const pane of panes) {
29071
29813
  if (pane.collapsed) continue;
29072
- const pct = percentScaleFor(scene, pane);
29073
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct, scene.priceMintick, pane.axisFormat)) {
29814
+ for (const t of this.axisTicks.ticksFor(scene, pane, coords)) {
29074
29815
  const y = coords.priceToY(t.price, pane.scale, pane.bounds);
29075
29816
  if (y < pane.bounds.top + 6 || y > pane.bounds.top + pane.bounds.height - 4) continue;
29817
+ if (t.major === true) ctx.font = majorFont;
29818
+ else if (t.major === false) ctx.globalAlpha = alpha * MINOR_LABEL_ALPHA;
29076
29819
  ctx.fillText(t.label, dataW + 6, y);
29820
+ if (t.major === true) ctx.font = font;
29821
+ else if (t.major === false) ctx.globalAlpha = alpha;
29077
29822
  }
29078
29823
  if (pane.axisBands) {
29079
29824
  for (const b of pane.axisBands) {
@@ -29315,15 +30060,23 @@ var CrosshairRenderer = class {
29315
30060
  constructor() {
29316
30061
  this.canvas = null;
29317
30062
  this.ctx = null;
30063
+ this.shade = null;
29318
30064
  }
29319
30065
  mount(canvas) {
29320
30066
  this.canvas = canvas;
29321
30067
  this.ctx = canvas.getContext("2d");
29322
30068
  }
30069
+ /** The plot area the last frame veiled for `crosshairOverride.shadeRight` — from the
30070
+ * picked bar's right edge to the price scale, full plot height (the union when both the
30071
+ * local and a synced crosshair veil) — or null when no veil was painted. */
30072
+ get shadeRightArea() {
30073
+ return this.shade;
30074
+ }
29323
30075
  /** Clear the cursor canvas and (re)draw the crosshair lines + axis chips. The optional
29324
30076
  * `separatorHoverY` highlights the draggable pane separator under the cursor;
29325
30077
  * `external` is a SYNCED ghost crosshair (another chart's pointer, pixel-resolved). */
29326
30078
  render(scene, coords, theme, separatorHoverY = null, external = null) {
30079
+ this.shade = null;
29327
30080
  const ctx = this.ctx;
29328
30081
  const canvas = this.canvas;
29329
30082
  if (!ctx || !canvas) return;
@@ -29346,12 +30099,7 @@ var CrosshairRenderer = class {
29346
30099
  const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
29347
30100
  if (vertical && ov?.shadeRight) {
29348
30101
  const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
29349
- if (from < dataW) {
29350
- ctx.fillStyle = ov.shadeRight.color;
29351
- ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
29352
- ctx.fillRect(from, 0, dataW - from, dataH);
29353
- ctx.globalAlpha = 1;
29354
- }
30102
+ if (from < dataW) this.paintShade(ctx, ov.shadeRight, from, dataW, dataH);
29355
30103
  }
29356
30104
  ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
29357
30105
  ctx.lineWidth = ov?.width ?? cs.width;
@@ -29387,6 +30135,7 @@ var CrosshairRenderer = class {
29387
30135
  destroy() {
29388
30136
  this.canvas = null;
29389
30137
  this.ctx = null;
30138
+ this.shade = null;
29390
30139
  }
29391
30140
  /** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
29392
30141
  * CONTAINING the foreign time (+ horizontal line when a comparable price came
@@ -29406,12 +30155,7 @@ var CrosshairRenderer = class {
29406
30155
  if (ov?.vertical === false) return;
29407
30156
  if (ov?.shadeRight) {
29408
30157
  const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
29409
- if (from < dataW) {
29410
- ctx.fillStyle = ov.shadeRight.color;
29411
- ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
29412
- ctx.fillRect(from, 0, dataW - from, dataH);
29413
- ctx.globalAlpha = 1;
29414
- }
30158
+ if (from < dataW) this.paintShade(ctx, ov.shadeRight, from, dataW, dataH);
29415
30159
  }
29416
30160
  if (ext.line === false || x < 0 || x > dataW) return;
29417
30161
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
@@ -29447,6 +30191,15 @@ var CrosshairRenderer = class {
29447
30191
  this.chip(ctx, x, dataH + 1, formatTimeStamp(ext.time, scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
29448
30192
  ctx.globalAlpha = 1;
29449
30193
  }
30194
+ /** Paint the `shadeRight` veil from `from` to the price scale and record the veiled area. */
30195
+ paintShade(ctx, shade, from, dataW, dataH) {
30196
+ ctx.fillStyle = shade.color;
30197
+ ctx.globalAlpha = shade.opacity ?? 1;
30198
+ ctx.fillRect(from, 0, dataW - from, dataH);
30199
+ ctx.globalAlpha = 1;
30200
+ const x = this.shade ? Math.min(this.shade.x, from) : from;
30201
+ this.shade = { x, width: dataW - x, height: dataH };
30202
+ }
29450
30203
  /** A soft band + a brighter crisp center line over the hovered separator, so it reads as
29451
30204
  * a draggable handle (the cursor is already `row-resize`). Spans the full width (data +
29452
30205
  * scale gutter) to match the separator itself. Theme-derived (textColor). */
@@ -32472,6 +33225,12 @@ var DrawingInteraction = class {
32472
33225
  activeDragId() {
32473
33226
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
32474
33227
  }
33228
+ /** The store drawings a live drag is moving — the pressed one and its riders — or none
33229
+ * before the drag passes the slop. A Ctrl-drag moves copies, so its sources stay put. */
33230
+ movingIds() {
33231
+ if (this.state.kind !== "pressed" || !this.state.moved || this.state.clones) return /* @__PURE__ */ new Set();
33232
+ return /* @__PURE__ */ new Set([this.state.id, ...this.state.riders.map((r) => r.id)]);
33233
+ }
32475
33234
  /** The drawing under a press that has not been released yet (drag or click undecided), or null. */
32476
33235
  pressedId() {
32477
33236
  return this.state.kind === "pressed" ? this.state.id : null;
@@ -32846,15 +33605,16 @@ var DrawingSettingsDialog = class {
32846
33605
  }
32847
33606
  open(drawing, actions, kind) {
32848
33607
  this.close();
32849
- const snapshot = clonePlain(drawing.serialize());
33608
+ const live = actions.resolve() ?? drawing;
33609
+ const snapshot = clonePlain(live.serialize());
32850
33610
  const grid = fieldGrid({ variant: "inputs" });
32851
33611
  grid.style.padding = "16px 20px";
32852
33612
  grid.style.overflowY = "auto";
32853
33613
  grid.style.overflowX = "hidden";
32854
33614
  grid.style.flex = "1 1 auto";
32855
- if (kind === "position" && drawing instanceof PositionTool) this.buildPosition(grid, drawing, actions);
32856
- else if (kind === "frvp" && drawing instanceof FixedRangeVolumeProfile) this.buildFrvp(grid, drawing, actions);
32857
- else if (kind === "levels") this.buildLevels(grid, drawing, actions);
33615
+ if (kind === "position" && live instanceof PositionTool) this.buildPosition(grid, live, actions);
33616
+ else if (kind === "frvp" && live instanceof FixedRangeVolumeProfile) this.buildFrvp(grid, live, actions);
33617
+ else if (kind === "levels") this.buildLevels(grid, live, actions);
32858
33618
  else return;
32859
33619
  const ui = new Dialog({
32860
33620
  host: this.host,
@@ -33130,6 +33890,17 @@ var DrawingSettingsDialog = class {
33130
33890
  }
33131
33891
  }));
33132
33892
  }
33893
+ if (drawing instanceof FibRetracement) {
33894
+ const fib = drawing;
33895
+ grid.appendChild(fieldRow({
33896
+ label: "Reverse",
33897
+ bool: true,
33898
+ toggle: {
33899
+ checked: fib.reverse,
33900
+ onChange: (v) => actions.patch({ reverse: v })
33901
+ }
33902
+ }));
33903
+ }
33133
33904
  levels.forEach((lv, i) => {
33134
33905
  const row = document.createElement("div");
33135
33906
  row.className = "vela-field-span";
@@ -33490,7 +34261,7 @@ var DrawingSettingsPopup = class {
33490
34261
  this.reposition();
33491
34262
  return;
33492
34263
  }
33493
- const text = drawing.text;
34264
+ const text = (actions.resolve() ?? drawing).text;
33494
34265
  const panel = document.createElement("div");
33495
34266
  panel.style.cssText = `padding:6px;border-top:1px solid var(--vela-border);display:flex;flex-direction:column;gap:4px;`;
33496
34267
  const ta = new TextArea({
@@ -34283,6 +35054,8 @@ var UserDrawingController = class {
34283
35054
  this.drawings = [];
34284
35055
  /** Ids painted on an interleave layer this frame — the top canvas paints only their handles. */
34285
35056
  this.sliced = /* @__PURE__ */ new Set();
35057
+ /** Interleaved drawings mid-drag: painted on the top canvas, left out of their slice. */
35058
+ this.lifted = /* @__PURE__ */ new Set();
34286
35059
  /** Cached slice canvases, keyed `paneId|beforeZ`, reused across frames to avoid churn. */
34287
35060
  this.sliceCache = /* @__PURE__ */ new Map();
34288
35061
  /** Series boundaries per pane as of the last `prepareSlices` — lets a repaint between data
@@ -34370,7 +35143,7 @@ var UserDrawingController = class {
34370
35143
  selectedIds: () => this.selectedIds,
34371
35144
  emit: (i) => this.emit(i),
34372
35145
  changed: () => {
34373
- this.invalidateSlices();
35146
+ this.syncLift();
34374
35147
  this.render();
34375
35148
  },
34376
35149
  openSettings: (id, x, y) => this.openSettingsById(id, x, y),
@@ -34659,6 +35432,12 @@ var UserDrawingController = class {
34659
35432
  this.render();
34660
35433
  }
34661
35434
  }
35435
+ /** Drop the hover (its handles) — the pointer no longer reaches the drawings. */
35436
+ clearHover() {
35437
+ if (this.hoveredId == null) return;
35438
+ this.hoveredId = null;
35439
+ this.render();
35440
+ }
34662
35441
  pointerUp(x, y, snap = "off") {
34663
35442
  if (this.eraserMode) {
34664
35443
  this.erasing = false;
@@ -34996,6 +35775,20 @@ var UserDrawingController = class {
34996
35775
  /** A drawing that paints inside the series stack changed (content, not hover): its pixels
34997
35776
  * live in the backend composite, so this layer alone can't show the change — ask for a
34998
35777
  * data frame, which re-runs `prepareSlices` before the backend composites. */
35778
+ /** A drawing being dragged leaves its interleave slice for the top canvas, so each pointer
35779
+ * move repaints one 2D layer instead of re-rasterizing and re-uploading a plot-sized slice
35780
+ * texture through the data frame. The slices rebuild once as the drag starts and once as it
35781
+ * ends (release, cancel), when the lifted set changes. */
35782
+ syncLift() {
35783
+ const moving = this.interaction.movingIds();
35784
+ let same = moving.size === this.lifted.size;
35785
+ if (same) {
35786
+ for (const id of moving) if (!this.lifted.has(id)) same = false;
35787
+ }
35788
+ if (same) return;
35789
+ this.lifted = new Set(moving);
35790
+ this.invalidateSlices();
35791
+ }
34999
35792
  invalidateSlices() {
35000
35793
  if (this.sliced.size > 0 || this.drawings.some((d) => this.isInterleaved(d))) this.deps.requestDataPaint();
35001
35794
  }
@@ -35014,7 +35807,7 @@ var UserDrawingController = class {
35014
35807
  const theme = this.deps.theme();
35015
35808
  const buckets = /* @__PURE__ */ new Map();
35016
35809
  for (const d of this.drawings) {
35017
- if (!d.visible) continue;
35810
+ if (!d.visible || this.lifted.has(d.id)) continue;
35018
35811
  const beforeZ = sliceKeyFor(d.zIndex, this.lastBounds.get(d.paneId) ?? []);
35019
35812
  if (beforeZ === null) continue;
35020
35813
  const key = `${d.paneId}|${beforeZ}`;
@@ -35066,8 +35859,9 @@ var UserDrawingController = class {
35066
35859
  mutedLabel: edited instanceof TextLabel ? edited.id : null
35067
35860
  };
35068
35861
  this.painter.seriesLook = this.deps.seriesLook();
35069
- this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
35070
- this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
35862
+ const onTop = (d) => !this.isInterleaved(d) || this.lifted.has(d.id);
35863
+ this.painter.paintAll(ctx, this.drawings.filter(onTop), proj, this.deps.theme(), targets);
35864
+ this.painter.paintHighlights(ctx, this.drawings.filter((d) => !onTop(d)), proj, handleIdsFor(targets));
35071
35865
  const clones = this.interaction.dragClones();
35072
35866
  if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
35073
35867
  this.layoutTextEditor();
@@ -35634,9 +36428,10 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
35634
36428
  consider(drawings.max);
35635
36429
  }
35636
36430
  if (min === Infinity || max === -Infinity) return { min: 0, max: 1 };
35637
- if (min === max) {
35638
- const pad = Math.abs(min) * 0.1 || 1;
35639
- return { min: min - pad, max: max + pad, log: log && min - pad > 0 };
36431
+ if (max - min <= Math.max(Math.abs(min), Math.abs(max)) * 1e-12) {
36432
+ const mid = (min + max) / 2;
36433
+ const pad = Math.abs(mid) * 0.1 || 1;
36434
+ return { min: mid - pad, max: mid + pad, log: log && mid - pad > 0 };
35640
36435
  }
35641
36436
  const content = Math.max(0.1, 1 - (margins.top + margins.bottom) / 100);
35642
36437
  const above = margins.top / 100 / content;
@@ -36140,7 +36935,9 @@ function bandEdgeSlot(logical) {
36140
36935
  return Math.ceil(logical) - 0.5;
36141
36936
  }
36142
36937
  var BackdropRenderer = class {
36143
- constructor() {
36938
+ /** `axisTicks` is shared with the chrome layer, so gridlines and price labels agree. */
36939
+ constructor(axisTicks = new PriceAxisTickSource()) {
36940
+ this.axisTicks = axisTicks;
36144
36941
  this.canvas = null;
36145
36942
  this.ctx = null;
36146
36943
  }
@@ -36217,9 +37014,8 @@ var BackdropRenderer = class {
36217
37014
  if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
36218
37015
  ctx.globalAlpha = gridAlpha;
36219
37016
  ctx.strokeStyle = horzColor;
36220
- const pct = percentScaleFor(scene, pane);
36221
37017
  ctx.beginPath();
36222
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct, void 0, pane.axisFormat)) {
37018
+ for (const t of this.axisTicks.ticksFor(scene, pane, coords)) {
36223
37019
  const y = Math.round(coords.priceToY(t.price, pane.scale, pane.bounds)) + 0.5;
36224
37020
  if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) continue;
36225
37021
  ctx.moveTo(0, y);
@@ -36706,8 +37502,16 @@ var NativeRenderer = class {
36706
37502
  this.toolbarGutter = 0;
36707
37503
  // px reserved on the left for the docked drawings toolbar (0 when hidden)
36708
37504
  this.mountContainer = null;
37505
+ /** The host-styleable twin of the `shadeRight` veil (`.vela-shade-right`), directly under
37506
+ * the cursor canvas; hidden whenever the crosshair layer paints no veil. */
37507
+ this.shadeRightEl = null;
37508
+ /** The veil geometry last written to {@link shadeRightEl} ('' = hidden) — the DOM is
37509
+ * touched only when it changes, never on a plain pointer move. */
37510
+ this.shadeRightSig = "";
36709
37511
  this.userDrawings = null;
36710
- this.backdropRenderer = new BackdropRenderer();
37512
+ /** One price-axis tick source for the gridlines (backdrop) and the labels (chrome). */
37513
+ this.priceAxisTicks = new PriceAxisTickSource();
37514
+ this.backdropRenderer = new BackdropRenderer(this.priceAxisTicks);
36711
37515
  this.volumeRenderer = new VolumeRenderer();
36712
37516
  /** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
36713
37517
  /**
@@ -36740,7 +37544,7 @@ var NativeRenderer = class {
36740
37544
  this.backendMode = "auto";
36741
37545
  this.glowAmount = 0;
36742
37546
  // WebGL2 neon-glow intensity (canvas2d ignores it)
36743
- this.chrome = new ChromeRenderer();
37547
+ this.chrome = new ChromeRenderer(this.priceAxisTicks);
36744
37548
  /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
36745
37549
  this.indicatorSlices = new IndicatorDrawingSlices();
36746
37550
  /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
@@ -36857,6 +37661,8 @@ var NativeRenderer = class {
36857
37661
  // pane just below it
36858
37662
  this.resizeSplitStart = null;
36859
37663
  // the two panes' shared span when the drag began
37664
+ /** The user drawings take part in pointer input (the runtime `drawingsInteractive` feature). */
37665
+ this.drawingsInteractive = true;
36860
37666
  this.hoverSeparatorY = null;
36861
37667
  // pixel y of the separator under the cursor (drives its hover highlight)
36862
37668
  // ── pane management (merge / reorder / collapse / maximize) ──
@@ -36912,8 +37718,9 @@ var NativeRenderer = class {
36912
37718
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
36913
37719
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
36914
37720
  this.priceStyleCbs = /* @__PURE__ */ new Set();
37721
+ this.priceStyleWillChangeCbs = /* @__PURE__ */ new Set();
36915
37722
  this.name = "native";
36916
- 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"];
37723
+ 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"];
36917
37724
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
36918
37725
  * so moving onto the button doesn't count as leaving). */
36919
37726
  this.onScrollProximityMove = (e) => {
@@ -36957,6 +37764,7 @@ var NativeRenderer = class {
36957
37764
  this.scene.priceStyle = opts.priceStyle;
36958
37765
  this.scene.basePainting = basePaintingOf(opts.priceStyle);
36959
37766
  this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
37767
+ if (typeof opts.priceAxisTicks === "function") this.priceAxisTicks.setHook(opts.priceAxisTicks);
36960
37768
  }
36961
37769
  this.theme = this.deriveTheme(DARK_THEME);
36962
37770
  }
@@ -37095,6 +37903,15 @@ var NativeRenderer = class {
37095
37903
  case "crosshairOverride":
37096
37904
  this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
37097
37905
  break;
37906
+ case "drawingsInteractive":
37907
+ this.drawingsInteractive = value !== false;
37908
+ if (this.input) this.input.drawings = this.drawingsInteractive;
37909
+ if (!this.drawingsInteractive) this.userDrawings?.clearHover();
37910
+ return;
37911
+ // affects the next gesture only — clearHover repaints what it drops
37912
+ case "priceAxisTicks":
37913
+ this.priceAxisTicks.setHook(typeof value === "function" ? value : null);
37914
+ break;
37098
37915
  case "keyboard":
37099
37916
  this.setKeyboardEnabled(Boolean(value));
37100
37917
  return;
@@ -37210,6 +38027,10 @@ var NativeRenderer = class {
37210
38027
  return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
37211
38028
  case "crosshairOverride":
37212
38029
  return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
38030
+ case "drawingsInteractive":
38031
+ return this.drawingsInteractive;
38032
+ case "priceAxisTicks":
38033
+ return this.priceAxisTicks.hook;
37213
38034
  case "keyboard":
37214
38035
  return this.keyboardEnabled;
37215
38036
  case "historyChords":
@@ -37883,6 +38704,12 @@ var NativeRenderer = class {
37883
38704
  Object.assign(this.drawingsCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
37884
38705
  this.cursorCanvas = document.createElement("canvas");
37885
38706
  Object.assign(this.cursorCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
38707
+ this.shadeRightEl = document.createElement("div");
38708
+ this.shadeRightEl.className = "vela-shade-right";
38709
+ this.shadeRightEl.setAttribute("aria-hidden", "true");
38710
+ this.shadeRightEl.hidden = true;
38711
+ Object.assign(this.shadeRightEl.style, { position: "absolute", top: "0", left: "0", width: "0", height: "0", pointerEvents: "none", display: "none" });
38712
+ this.shadeRightSig = "";
37886
38713
  this.overlayRoot = document.createElement("div");
37887
38714
  Object.assign(this.overlayRoot.style, { position: "absolute", inset: "0", pointerEvents: "none" });
37888
38715
  this.plot = document.createElement("div");
@@ -37890,7 +38717,7 @@ var NativeRenderer = class {
37890
38717
  this.extLayers = rendererLayers().map((def) => ({ def, instance: def.create(), canvas: this.createLayerCanvas(), channel: def.id, owner: null }));
37891
38718
  const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
37892
38719
  const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
37893
- this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
38720
+ this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.shadeRightEl, this.cursorCanvas, this.overlayRoot);
37894
38721
  this.layerOrderSig = "";
37895
38722
  this.wrapper.appendChild(this.plot);
37896
38723
  this.factoryConfig = this.getConfig();
@@ -37948,13 +38775,14 @@ var NativeRenderer = class {
37948
38775
  drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
37949
38776
  drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
37950
38777
  drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
37951
- drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
38778
+ drawingsCursor: (x, y) => (this.drawingsInteractive ? this.userDrawings?.cursorAt(x, y) : null) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
37952
38779
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
37953
38780
  drawingsClearTransient: () => this.userDrawings?.clearTransient()
37954
38781
  });
37955
38782
  this.input.rightEdgeZoom = this.zoomAnchorMode === "right";
37956
38783
  this.input.axisDrag = this.axisDragEnabled;
37957
38784
  this.input.paneResize = this.paneResizeEnabled;
38785
+ this.input.drawings = this.drawingsInteractive;
37958
38786
  this.input.attach(this.dataCanvas);
37959
38787
  if (this.settingsEnabled) this.setSettingsEnabled(true);
37960
38788
  this.syncCountdownTimer();
@@ -38263,6 +39091,8 @@ var NativeRenderer = class {
38263
39091
  this.backend.destroy();
38264
39092
  this.chrome.destroy();
38265
39093
  this.crosshairLayer.destroy();
39094
+ this.shadeRightEl?.remove();
39095
+ this.shadeRightEl = null;
38266
39096
  this.attributionEl?.remove();
38267
39097
  this.attributionEl = null;
38268
39098
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
@@ -38609,14 +39439,21 @@ var NativeRenderer = class {
38609
39439
  this.priceStyleCbs.add(cb);
38610
39440
  return () => this.priceStyleCbs.delete(cb);
38611
39441
  }
39442
+ onPriceStyleWillChange(cb) {
39443
+ this.priceStyleWillChangeCbs.add(cb);
39444
+ return () => this.priceStyleWillChangeCbs.delete(cb);
39445
+ }
38612
39446
  /**
38613
39447
  * THE single write path for the base price style at runtime (feature set / settings dialog /
38614
- * config template — the constructor seeds the field directly, pre-listeners). Updates the
38615
- * scene, eases any reveal layer toward the new style's target, and notifies the core —
38616
- * which owns the DATA side of styles that need one (a chart type's SeriesDataEngine).
39448
+ * config template — the constructor seeds the field directly, pre-listeners). Announces the
39449
+ * switch while the scene still holds the old style (a listener may paint/capture that frame),
39450
+ * then updates the scene, eases any reveal layer toward the new style's target, and notifies
39451
+ * the core — which owns the DATA side of styles that need one (a chart type's SeriesDataEngine).
38617
39452
  */
38618
39453
  setPriceStyle(style) {
38619
39454
  if (style === this.scene.priceStyle) return;
39455
+ const from = this.scene.priceStyle;
39456
+ for (const cb of this.priceStyleWillChangeCbs) cb(from, style);
38620
39457
  this.scene.priceStyle = style;
38621
39458
  this.scene.basePainting = basePaintingOf(style);
38622
39459
  this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
@@ -38846,7 +39683,7 @@ var NativeRenderer = class {
38846
39683
  if (this.easeLiveBar(dtMs)) active = true;
38847
39684
  this.skeletonClockMs += dtMs;
38848
39685
  this.paintData();
38849
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
39686
+ this.renderCrosshairLayer();
38850
39687
  this.updateLegendValues();
38851
39688
  this.emitViewportChange();
38852
39689
  return active;
@@ -39421,10 +40258,31 @@ var NativeRenderer = class {
39421
40258
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
39422
40259
  this.trackMarkPopover();
39423
40260
  }
39424
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
40261
+ this.renderCrosshairLayer();
39425
40262
  if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
39426
40263
  this.updateLegendValues();
39427
40264
  }
40265
+ /** Repaint the L2 crosshair canvas and keep the `.vela-shade-right` element on the veil it painted. */
40266
+ renderCrosshairLayer() {
40267
+ this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
40268
+ this.syncShadeRight();
40269
+ }
40270
+ /** Position (or hide) the `.vela-shade-right` element over the veil of the last crosshair frame. */
40271
+ syncShadeRight() {
40272
+ const el = this.shadeRightEl;
40273
+ if (!el) return;
40274
+ const area = this.crosshairLayer.shadeRightArea;
40275
+ const sig = area ? `${area.x},${area.width},${area.height}` : "";
40276
+ if (sig === this.shadeRightSig) return;
40277
+ this.shadeRightSig = sig;
40278
+ if (!area) {
40279
+ el.hidden = true;
40280
+ el.style.display = "none";
40281
+ return;
40282
+ }
40283
+ Object.assign(el.style, { left: `${area.x}px`, width: `${area.width}px`, height: `${area.height}px`, display: "" });
40284
+ el.hidden = false;
40285
+ }
39428
40286
  /** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
39429
40287
  repaintCursorLayers() {
39430
40288
  const pane = this.scene.panes.get(PRICE_PANE_ID2);
@@ -40218,7 +41076,7 @@ var NativeRenderer = class {
40218
41076
  if (this.animator.active) {
40219
41077
  this.computeScales();
40220
41078
  this.paintData();
40221
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
41079
+ this.renderCrosshairLayer();
40222
41080
  } else {
40223
41081
  this.scheduler.flushNow(4 /* Full */);
40224
41082
  }
@@ -44233,7 +45091,8 @@ var Vela = class {
44233
45091
  glow: options.glow ?? 0,
44234
45092
  upColor: options.upColor ?? BULLISH,
44235
45093
  downColor: options.downColor ?? BEARISH,
44236
- priceStyle: options.priceStyle ?? "candles"
45094
+ priceStyle: options.priceStyle ?? "candles",
45095
+ priceAxisTicks: options.priceAxis?.ticks ?? null
44237
45096
  };
44238
45097
  const RendererClass = options.renderer ?? NativeRenderer;
44239
45098
  if (typeof RendererClass !== "function") {
@@ -44723,6 +45582,9 @@ var CSS21 = `
44723
45582
  .vela-statusline .vela-sl-market { align-self: center; display: inline-flex; }
44724
45583
  .vela-statusline .vela-sl-market > [hidden] { display: none !important; }
44725
45584
  .vela-statusline .vela-sl-replay-badge, .vela-statusline .vela-sl-replay-badge svg { display: block; width: 16px; height: 16px; }
45585
+ /* Tabular figures: every digit has one advance, so a readout whose text changes only in
45586
+ * its digits keeps its width \u2014 render() skips the ladder for those (see widthKey). */
45587
+ .vela-statusline .vela-sl-values { font-variant-numeric: tabular-nums; }
44726
45588
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
44727
45589
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
44728
45590
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
@@ -44840,6 +45702,10 @@ var Statusline = class {
44840
45702
  this.fitRO = null;
44841
45703
  /** The ladder rung currently applied — see {@link fit}. */
44842
45704
  this.layout = { stacked: false, level: "full" };
45705
+ /** The readout {@link widthKey} the applied rung was fitted to. */
45706
+ this.fitKey = null;
45707
+ /** Web fonts landing after the last fit change every width without a resize. */
45708
+ this.onFontsLoaded = () => this.fit();
44843
45709
  this.onContextMenu = (e) => {
44844
45710
  if (!this.menu || !this.menuHooks) return;
44845
45711
  e.preventDefault();
@@ -44903,6 +45769,7 @@ var Statusline = class {
44903
45769
  this.fitRO = new ResizeObserver(() => this.fit());
44904
45770
  this.fitRO.observe(host);
44905
45771
  }
45772
+ doc.fonts?.addEventListener("loadingdone", this.onFontsLoaded);
44906
45773
  this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
44907
45774
  this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
44908
45775
  this.setMarketStatus("open");
@@ -44955,6 +45822,7 @@ var Statusline = class {
44955
45822
  * (detached host, node tests) nothing overflows, so the widest rung stays.
44956
45823
  */
44957
45824
  fit() {
45825
+ this.fitKey = this.widthKey();
44958
45826
  this.syncParts();
44959
45827
  const seg = segmentVisibility(this.parts, this.chartHidden);
44960
45828
  const overflows = () => this.el.scrollWidth > this.el.clientWidth;
@@ -45111,6 +45979,7 @@ var Statusline = class {
45111
45979
  this.detach();
45112
45980
  this.fitRO?.disconnect();
45113
45981
  this.fitRO = null;
45982
+ this.el.ownerDocument.fonts?.removeEventListener("loadingdone", this.onFontsLoaded);
45114
45983
  this.el.removeEventListener("contextmenu", this.onContextMenu);
45115
45984
  this.menu?.destroy();
45116
45985
  this.menu = null;
@@ -45126,7 +45995,22 @@ var Statusline = class {
45126
45995
  }
45127
45996
  render() {
45128
45997
  if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
45129
- this.fit();
45998
+ if (this.widthKey() === this.fitKey) this.renderValues();
45999
+ else this.fit();
46000
+ }
46001
+ /** Everything the readout's width depends on, at every rung: its shape, and each
46002
+ * value's sign, digit count and decimals (the digits themselves are tabular — see
46003
+ * the CSS; the grouping commas follow from the digit count). `toFixed` carries all
46004
+ * three at a fraction of the locale formatter's cost — this runs per pointer move.
46005
+ * The parts, the host's size and the fonts re-fit through their own paths. */
46006
+ widthKey() {
46007
+ const bar = this.hoverBar ?? this.lastBar;
46008
+ if (!bar) return this.readout;
46009
+ const dp = decimalsFor(bar.close);
46010
+ const diff = bar.close - bar.open;
46011
+ const text = [bar.open, bar.high, bar.low, bar.close, diff].map((v) => v.toFixed(dp));
46012
+ text.push((diff / bar.open * 100).toFixed(2));
46013
+ return `${this.readout} ${text.join(" ")}`.replace(/\d/g, "0");
45130
46014
  }
45131
46015
  /** Write the value readout for the current bar at the current ladder level. */
45132
46016
  renderValues() {
@@ -45726,16 +46610,25 @@ var CellControls = class {
45726
46610
  // src/widget/context-menu.ts
45727
46611
  var PRICE_AXIS_W = 60;
45728
46612
  var TIME_AXIS_H3 = 26;
46613
+ var OFF_PLOT = { price: null, time: null, paneKind: null };
45729
46614
  var ChartContextMenu = class {
45730
46615
  constructor(host, cbs) {
45731
46616
  this.cbs = cbs;
45732
46617
  this.chart = null;
46618
+ this.offCrosshair = null;
46619
+ /** The bound chart's latest crosshair report — a right-click always follows a pointer
46620
+ * move to its spot, so this is where it landed. */
46621
+ this.crosshair = OFF_PLOT;
46622
+ /** {@link crosshair} as it stood when the menu opened: the pointer moves on through the
46623
+ * menu before a row is picked, and `run` must see the right-clicked spot. */
46624
+ this.pointer = OFF_PLOT;
45733
46625
  this.lastZone = "body";
45734
46626
  /** The pane whose scale the open price-axis menu targets (null ⇒ the main scale). */
45735
46627
  this.lastPane = null;
45736
46628
  this.onContextMenu = (e) => {
45737
46629
  e.preventDefault();
45738
46630
  if (!this.chart) return;
46631
+ this.pointer = this.crosshair;
45739
46632
  this.lastZone = this.zoneOf(e);
45740
46633
  this.lastPane = this.lastZone === "price-axis" ? this.paneAt(e) : null;
45741
46634
  this.menu.setItems(this.itemsFor(this.lastZone));
@@ -45749,15 +46642,23 @@ var ChartContextMenu = class {
45749
46642
  // Pointer-anchored action menu: checked state reads as a leading ✓, not a
45750
46643
  // washed row (which would read as hover in a menu with no trigger button).
45751
46644
  checkmarks: true,
46645
+ iconBadges: true,
45752
46646
  onSelect: (id) => this.run(id)
45753
46647
  });
45754
46648
  host.addEventListener("contextmenu", this.onContextMenu);
45755
46649
  }
45756
46650
  /** (Re)bind to a chart instance — called after every widget rebuild. */
45757
46651
  onChart(chart) {
46652
+ this.offCrosshair?.();
45758
46653
  this.chart = chart;
46654
+ this.crosshair = OFF_PLOT;
46655
+ this.offCrosshair = chart.renderer.onCrosshairMove((e) => {
46656
+ this.crosshair = { price: e.price, time: e.time, paneKind: e.paneKind ?? null };
46657
+ });
45759
46658
  }
45760
46659
  destroy() {
46660
+ this.offCrosshair?.();
46661
+ this.offCrosshair = null;
45761
46662
  this.host.removeEventListener("contextmenu", this.onContextMenu);
45762
46663
  this.menu.destroy();
45763
46664
  }
@@ -45776,49 +46677,52 @@ var ChartContextMenu = class {
45776
46677
  flag(feature) {
45777
46678
  return Boolean(this.chart?.renderer.get(feature));
45778
46679
  }
45779
- contributed(zone) {
46680
+ /** A fresh widget context carrying the pointer captured at open. Copied by property
46681
+ * descriptor: the context's getters must stay LIVE, and a spread would freeze them. */
46682
+ context() {
45780
46683
  const ctx = this.cbs.getContext?.();
45781
- return widgetActions(`context:${zone}`, ctx).map((a, i) => ({
45782
- id: `action:${a.id}`,
45783
- label: a.label,
45784
- icon: a.icon,
45785
- separatorBefore: i === 0
46684
+ if (!ctx) return void 0;
46685
+ return Object.create(Object.getPrototypeOf(ctx), {
46686
+ ...Object.getOwnPropertyDescriptors(ctx),
46687
+ pointer: { value: this.pointer, enumerable: true }
46688
+ });
46689
+ }
46690
+ contributed(zone) {
46691
+ return widgetActions(`context:${zone}`, this.context()).map((a) => ({
46692
+ item: { id: `action:${a.id}`, label: a.label, icon: a.icon },
46693
+ order: a.order
45786
46694
  }));
45787
46695
  }
45788
46696
  itemsFor(zone) {
46697
+ return composeMenu(zone, this.builtinItems(zone), this.contributed(zone));
46698
+ }
46699
+ builtinItems(zone) {
45789
46700
  if (zone === "price-axis") {
45790
46701
  const pane = this.lastPane;
45791
- return [
45792
- ...priceAxisItems({
45793
- auto: this.chart?.renderer.get("autoScale") !== false,
45794
- invert: pane ? pane.invert : this.flag("invertScale"),
45795
- choice: scaleChoiceOf(pane ?? { mode: String(this.chart?.renderer.get("scaleMode") ?? "price"), log: this.flag("logScale") }),
45796
- axisLabels: this.flag("axisLabels"),
45797
- priceLabel: this.flag("priceLabel"),
45798
- countdown: this.flag("countdown"),
45799
- priceLine: this.flag("currentPriceLine")
45800
- }),
45801
- ...this.contributed(zone)
45802
- ];
46702
+ return priceAxisItems({
46703
+ auto: this.chart?.renderer.get("autoScale") !== false,
46704
+ invert: pane ? pane.invert : this.flag("invertScale"),
46705
+ choice: scaleChoiceOf(pane ?? { mode: String(this.chart?.renderer.get("scaleMode") ?? "price"), log: this.flag("logScale") }),
46706
+ axisLabels: this.flag("axisLabels"),
46707
+ priceLabel: this.flag("priceLabel"),
46708
+ countdown: this.flag("countdown"),
46709
+ priceLine: this.flag("currentPriceLine")
46710
+ });
45803
46711
  }
45804
46712
  if (zone === "time-axis") {
45805
- const tz = this.cbs.timezone?.() ?? String(this.chart?.renderer.get("timezone") ?? "Etc/UTC");
45806
- return [...timeAxisItems(tz), ...this.contributed("time-axis")];
46713
+ return timeAxisItems(this.cbs.timezone?.() ?? String(this.chart?.renderer.get("timezone") ?? "Etc/UTC"));
45807
46714
  }
45808
46715
  const chart = this.chart;
45809
- return [
45810
- ...bodyItems({
45811
- drawings: chart?.drawings.supported ? chart.drawings.all().length : 0,
45812
- indicators: chart?.indicators().length ?? 0
45813
- }),
45814
- ...this.contributed("body")
45815
- ];
46716
+ return bodyItems({
46717
+ drawings: chart?.drawings.supported ? chart.drawings.all().length : 0,
46718
+ indicators: chart?.indicators().length ?? 0
46719
+ });
45816
46720
  }
45817
46721
  run(id) {
45818
46722
  const chart = this.chart;
45819
46723
  if (!chart) return;
45820
46724
  if (id.startsWith("action:")) {
45821
- const ctx = this.cbs.getContext?.();
46725
+ const ctx = this.context();
45822
46726
  if (ctx) widgetActions(`context:${this.lastZone}`, ctx).find((a) => a.id === id.slice("action:".length))?.run(ctx);
45823
46727
  return;
45824
46728
  }
@@ -45942,8 +46846,8 @@ function seedDefaults(opts) {
45942
46846
  };
45943
46847
  }
45944
46848
  function cellChartDefaults(opts) {
45945
- const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings } = opts;
45946
- return { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings };
46849
+ const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings, priceAxis } = opts;
46850
+ return { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings, priceAxis };
45947
46851
  }
45948
46852
  function cellDrawings(opt) {
45949
46853
  if (opt === false) return false;
@@ -46513,12 +47417,14 @@ var ChartCell = class {
46513
47417
  this.watermarkOn = visible;
46514
47418
  this.watermark?.setVisible(visible);
46515
47419
  this.deps.onStateDirty();
47420
+ this.deps.onStatusPrefsChanged(this.id);
46516
47421
  }
46517
47422
  /** Show/hide the "Replay" line under this cell's watermark while it replays (persisted per cell). */
46518
47423
  setReplayWatermarkVisible(visible) {
46519
47424
  this.replayWatermarkOn = visible;
46520
47425
  this.watermark?.setReplayVisible(visible);
46521
47426
  this.deps.onStateDirty();
47427
+ this.deps.onStatusPrefsChanged(this.id);
46522
47428
  }
46523
47429
  /** Show/hide this cell's indicator titles — the in-chart legend rows (persisted per cell). */
46524
47430
  setIndicatorTitlesVisible(visible) {
@@ -46539,18 +47445,20 @@ var ChartCell = class {
46539
47445
  this.statusline?.setPartVisible(part, visible);
46540
47446
  this.deps.onStatusPrefsChanged(this.id);
46541
47447
  }
46542
- /** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
47448
+ /** This cell's Status line tab and watermark prefs as one bundle (see {@link CellStatusPrefs}). */
46543
47449
  statusPrefs() {
46544
47450
  const sl = this.statusline;
46545
47451
  return {
46546
47452
  parts: sl ? { logo: sl.partVisible("logo"), name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
46547
47453
  indicatorTitles: this.indicatorTitlesOn,
46548
- indicatorValues: this.indicatorValuesOn
47454
+ indicatorValues: this.indicatorValuesOn,
47455
+ watermark: this.watermarkOn,
47456
+ replayWatermark: this.replayWatermarkOn
46549
47457
  };
46550
47458
  }
46551
- /** Converge this cell's Status line tab prefs to `prefs` — the follower half of
46552
- * the workspace's style link. Idempotent: matching values change nothing, so a
46553
- * propagated echo dies on its own. */
47459
+ /** Converge this cell's Status line tab and watermark prefs to `prefs` — the
47460
+ * follower half of the workspace's style link. Idempotent: matching values change
47461
+ * nothing, so a propagated echo dies on its own. */
46554
47462
  applyStatusPrefs(prefs) {
46555
47463
  if (prefs.parts && this.statusline) {
46556
47464
  for (const part of Object.keys(prefs.parts)) {
@@ -46559,6 +47467,8 @@ var ChartCell = class {
46559
47467
  }
46560
47468
  if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
46561
47469
  if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
47470
+ if (prefs.watermark !== this.watermarkOn) this.setWatermarkVisible(prefs.watermark);
47471
+ if (prefs.replayWatermark !== this.replayWatermarkOn) this.setReplayWatermarkVisible(prefs.replayWatermark);
46562
47472
  }
46563
47473
  /** The LIVE chart of this cell — never cache it across a layout change (the cell's
46564
47474
  * identity is what endures; the chart dies with the cell). */
@@ -47469,6 +48379,55 @@ function occupancyGrid(def) {
47469
48379
  const cols = def.cols.length;
47470
48380
  return def.rows.map((_, r) => def.cols.map((_2, c) => def.cells[r * cols + c]?.id ?? `\xB7${r}x${c}`));
47471
48381
  }
48382
+ var GLYPH_MIN = 1.5;
48383
+ var GLYPH_SPAN = 13;
48384
+ function glyphStops(weights, n) {
48385
+ const w = Array.from({ length: n }, (_, i) => {
48386
+ const v = weights[i];
48387
+ return v !== void 0 && Number.isFinite(v) && v > 0 ? v : 1;
48388
+ });
48389
+ const total = w.reduce((a, b) => a + b, 0);
48390
+ const stops = [GLYPH_MIN];
48391
+ let acc = 0;
48392
+ for (const v of w) {
48393
+ acc += v;
48394
+ stops.push(Math.round((GLYPH_MIN + GLYPH_SPAN * acc / total) * 100) / 100);
48395
+ }
48396
+ return stops;
48397
+ }
48398
+ function layoutGlyph(def) {
48399
+ const grid = occupancyGrid(def);
48400
+ const rows = grid.length;
48401
+ const cols = grid.reduce((n, row) => Math.max(n, row.length), 0);
48402
+ const xs = glyphStops(def.cols, cols);
48403
+ const ys = glyphStops(def.rows, rows);
48404
+ const at = (r, c) => grid[r]?.[c];
48405
+ const seams = [];
48406
+ for (let c = 1; c < cols; c += 1) {
48407
+ let start = -1;
48408
+ for (let r = 0; r <= rows; r += 1) {
48409
+ const split = r < rows && at(r, c - 1) !== at(r, c);
48410
+ if (split && start < 0) start = r;
48411
+ if (!split && start >= 0) {
48412
+ seams.push(`M${xs[c]} ${ys[start]}V${ys[r]}`);
48413
+ start = -1;
48414
+ }
48415
+ }
48416
+ }
48417
+ for (let r = 1; r < rows; r += 1) {
48418
+ let start = -1;
48419
+ for (let c = 0; c <= cols; c += 1) {
48420
+ const split = c < cols && at(r - 1, c) !== at(r, c);
48421
+ if (split && start < 0) start = c;
48422
+ if (!split && start >= 0) {
48423
+ seams.push(`M${xs[start]} ${ys[r]}H${xs[c]}`);
48424
+ start = -1;
48425
+ }
48426
+ }
48427
+ }
48428
+ const frame = `<rect x="${GLYPH_MIN}" y="${GLYPH_MIN}" width="${GLYPH_SPAN}" height="${GLYPH_SPAN}" rx="1.5"/>`;
48429
+ return svg16(seams.length > 0 ? `${frame}<path d="${seams.join("")}"/>` : frame);
48430
+ }
47472
48431
  function gridStyles(def, trackSizes) {
47473
48432
  const cols = trackSizes?.cols?.length === def.cols.length ? trackSizes.cols : def.cols;
47474
48433
  const rows = trackSizes?.rows?.length === def.rows.length ? trackSizes.rows : def.rows;
@@ -48000,7 +48959,8 @@ var VelaWorkspace = class {
48000
48959
  onToggleSync: (id) => {
48001
48960
  const kind = id;
48002
48961
  this.sync.set(kind, this.syncOpts[kind] ? false : true);
48003
- }
48962
+ },
48963
+ glyph: () => layoutGlyph(this.def)
48004
48964
  },
48005
48965
  getContext: () => this.context()
48006
48966
  });
@@ -48499,6 +49459,7 @@ var VelaWorkspace = class {
48499
49459
  this.applyGrid();
48500
49460
  this.buildCells();
48501
49461
  this.alignNewCellStyles(preexisting);
49462
+ this.alignNewCellDrawings(preexisting);
48502
49463
  this.syncCellPresentation();
48503
49464
  this.refreshCellControls();
48504
49465
  this.topbar.setLayout(next.id);
@@ -48915,6 +49876,7 @@ var VelaWorkspace = class {
48915
49876
  const chart = cell.chart;
48916
49877
  chart.on("indicator:error", ({ error }) => this.toastHost.show(`[${cell.id}] ${error.message}`, "error", 5e3));
48917
49878
  chart.on("script:run", (run) => this.events.emit("script:run", { ...run, cell: cell.id }));
49879
+ chart.on("priceStyle:change", ({ from, to }) => this.events.emit("cell:priceStyle", { id: cell.id, from, to }));
48918
49880
  chart.on("alert", (alert) => {
48919
49881
  const source = [parseSymbol(cell.symbol).ticker || cell.symbol, timeframeLabel(cell.timeframe), alert.indicator].filter(Boolean).join(" ");
48920
49882
  this.alerts.unshift({ cellId: cell.id, source, title: alert.title ?? "Alert", message: alert.message, time: alert.time });
@@ -49032,12 +49994,66 @@ var VelaWorkspace = class {
49032
49994
  }
49033
49995
  }
49034
49996
  /**
49035
- * Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
49036
- * its renderer config plus its Status line tab prefs — onto its same-group
49037
- * followers (the style link). Loop-safe two ways: the busy guard eats the
49038
- * followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
49039
- * in the same tick), and the equality short-circuits leave already-converged
49040
- * followers untouched, so nothing re-emits once the group agrees.
49997
+ * Bring cells minted by a layout change into their drawings group: with the link
49998
+ * on, a NEW cell (fresh slot, or one returning from the pool that missed edits
49999
+ * while dormant) receives the drawings of a pre-existing group peer — the active
50000
+ * cell when it is one. A drawing the arriving cell already holds a linked copy of
50001
+ * is refreshed in place; any other is copied and linked like a freshly synced
50002
+ * drawing, so later edits and removals follow both ways. The arrival is the cell's
50003
+ * starting state, not an edit: it stays out of the cell's undo timeline, and the
50004
+ * busy guard keeps the copies' own events from fanning back out.
50005
+ */
50006
+ alignNewCellDrawings(preexisting) {
50007
+ const setting = this.syncOpts.drawings;
50008
+ if (!setting) return;
50009
+ const ids = [...this.cellsById.keys()];
50010
+ for (const id of ids) {
50011
+ if (preexisting.has(id)) continue;
50012
+ const cell = this.cellsById.get(id);
50013
+ if (!cell?.chart.drawings.supported) continue;
50014
+ const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
50015
+ if (peers.length === 0) continue;
50016
+ const sourceId = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
50017
+ const source = this.cellsById.get(sourceId);
50018
+ if (!source) continue;
50019
+ const drawings = cell.chart.drawings;
50020
+ this.drawingSyncBusy = true;
50021
+ try {
50022
+ cell.history.silently(() => {
50023
+ const held = new Set(drawings.all().map((d) => d.id));
50024
+ for (const doc of source.chart.drawings.all()) {
50025
+ const group = this.drawingLinks.get(`${sourceId}\0${doc.id}`) ?? /* @__PURE__ */ new Map([[sourceId, doc.id]]);
50026
+ const peerId = group.get(id);
50027
+ if (peerId != null && held.has(peerId)) {
50028
+ drawings.update(peerId, { anchors: doc.anchors, style: doc.style, text: doc.text, props: doc.props });
50029
+ continue;
50030
+ }
50031
+ const copy = drawings.add(doc.type, {
50032
+ paneId: doc.paneId,
50033
+ anchors: doc.anchors,
50034
+ style: doc.style,
50035
+ text: doc.text,
50036
+ props: doc.props,
50037
+ zIndex: doc.zIndex
50038
+ });
50039
+ if (!copy) continue;
50040
+ if (peerId != null) this.drawingLinks.delete(`${id}\0${peerId}`);
50041
+ group.set(id, copy.id);
50042
+ for (const [cellId, dId] of group) this.drawingLinks.set(`${cellId}\0${dId}`, group);
50043
+ }
50044
+ });
50045
+ } finally {
50046
+ this.drawingSyncBusy = false;
50047
+ }
50048
+ }
50049
+ }
50050
+ /**
50051
+ * Mirror an origin cell's presentation — the style-link slice of its renderer
50052
+ * config ({@link styleConfigPatch}) plus its Status line and watermark prefs —
50053
+ * onto its same-group followers (the style link). Loop-safe two ways: the busy
50054
+ * guard eats the followers' SYNCHRONOUS echoes (their `applyConfig` re-fires
50055
+ * `onConfigChanged` in the same tick), and the equality short-circuits leave
50056
+ * already-converged followers untouched, so nothing re-emits once the group agrees.
49041
50057
  */
49042
50058
  propagateStylePrefs(originId) {
49043
50059
  if (this.styleSyncBusy || this.destroyed) return;
@@ -49045,17 +50061,15 @@ var VelaWorkspace = class {
49045
50061
  if (targets.length === 0) return;
49046
50062
  const origin = this.cellsById.get(originId);
49047
50063
  if (!origin) return;
49048
- const slice = styleConfigSlice(origin.chart.renderer.getConfig());
49049
- const sliceJson = slice ? JSON.stringify(slice) : null;
50064
+ const config = origin.chart.renderer.getConfig();
49050
50065
  const prefs = origin.statusPrefs();
49051
50066
  this.styleSyncBusy = true;
49052
50067
  try {
49053
50068
  for (const id of targets) {
49054
50069
  const cell = this.cellsById.get(id);
49055
50070
  if (!cell) continue;
49056
- if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
49057
- cell.chart.renderer.applyConfig(slice);
49058
- }
50071
+ const patch = styleConfigPatch(config, cell.chart.renderer.getConfig());
50072
+ if (patch) cell.chart.renderer.applyConfig(patch);
49059
50073
  cell.applyStatusPrefs(prefs);
49060
50074
  }
49061
50075
  } finally {
@@ -49376,7 +50390,8 @@ var VelaWorkspace = class {
49376
50390
  onToggleSync: (id) => {
49377
50391
  const kind = id;
49378
50392
  this.sync.set(kind, this.syncOpts[kind] ? false : true);
49379
- }
50393
+ },
50394
+ glyph: () => layoutGlyph(this.def)
49380
50395
  },
49381
50396
  onOpenChange: (open2) => this.trackDialog(open2)
49382
50397
  }));