@luxalgo/vela 0.8.2 → 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 (48) hide show
  1. package/dist/{DataProvider-DpOWZD8F.d.ts → DataProvider-oH78tYXL.d.ts} +1 -1
  2. package/dist/{DataProvider-COGDs9Xg.d.cts → DataProvider-wbtJ3-Sa.d.cts} +1 -1
  3. package/dist/{chunk-R2JMNIHY.js → chunk-2IPXYM4B.js} +258 -169
  4. package/dist/{chunk-NLDTVFOI.js → chunk-G7B7ZCBF.js} +129 -10
  5. package/dist/{chunk-T3DZIR5Y.js → chunk-HMHA733L.js} +306 -168
  6. package/dist/{chunk-DPAA3SRT.js → chunk-KWI3YDBW.js} +254 -9
  7. package/dist/{chunk-27UTYE7T.js → chunk-TO3GIK7Z.js} +49 -7
  8. package/dist/{chunk-HKNO24GD.js → chunk-VHGACEHO.js} +50 -1
  9. package/dist/{contributions-Cgm9RJYP.d.cts → contributions-BhOBRenr.d.cts} +44 -4
  10. package/dist/{contributions-bWgwyiVi.d.ts → contributions-HGqnNb_P.d.ts} +44 -4
  11. package/dist/index.cjs +497 -79
  12. package/dist/index.d.cts +34 -14
  13. package/dist/index.d.ts +34 -14
  14. package/dist/index.js +5 -5
  15. package/dist/{options-NAUHiQEC.d.ts → options-lMUwmRwh.d.cts} +66 -1
  16. package/dist/{options-NAUHiQEC.d.cts → options-lMUwmRwh.d.ts} +66 -1
  17. package/dist/{plugin-CqxWoDKS.d.ts → plugin-Cr2Pj8q8.d.ts} +4 -4
  18. package/dist/{plugin-CjjzZX2F.d.cts → plugin-Do5J5xuF.d.cts} +4 -4
  19. package/dist/plugin.cjs +18 -0
  20. package/dist/plugin.d.cts +5 -5
  21. package/dist/plugin.d.ts +5 -5
  22. package/dist/plugin.js +3 -3
  23. package/dist/providers/binance.d.cts +2 -2
  24. package/dist/providers/binance.d.ts +2 -2
  25. package/dist/providers/coinbase.d.cts +2 -2
  26. package/dist/providers/coinbase.d.ts +2 -2
  27. package/dist/providers/hyperliquid.d.cts +2 -2
  28. package/dist/providers/hyperliquid.d.ts +2 -2
  29. package/dist/{side-panel-wMNfgyZb.d.cts → side-panel-_2UK1gSm.d.cts} +45 -3
  30. package/dist/{side-panel-wMNfgyZb.d.ts → side-panel-_2UK1gSm.d.ts} +45 -3
  31. package/dist/{statusline-model-Dpfpq54A.d.cts → statusline-model-B-2xGlB_.d.cts} +3 -3
  32. package/dist/{statusline-model-Dih6FGWP.d.ts → statusline-model-CY1wzxHg.d.ts} +3 -3
  33. package/dist/ui.cjs +224 -14
  34. package/dist/ui.d.cts +119 -87
  35. package/dist/ui.d.ts +119 -87
  36. package/dist/ui.js +3 -3
  37. package/dist/vela.global.js +497 -79
  38. package/dist/vela.global.min.js +86 -52
  39. package/dist/widget.cjs +850 -167
  40. package/dist/widget.d.cts +24 -10
  41. package/dist/widget.d.ts +24 -10
  42. package/dist/widget.js +7 -7
  43. package/dist/workspace.cjs +849 -167
  44. package/dist/workspace.d.cts +47 -20
  45. package/dist/workspace.d.ts +47 -20
  46. package/dist/workspace.js +6 -6
  47. package/package.json +1 -1
  48. /package/dist/{chunk-JU4CWEBF.js → chunk-W5YPYZ2P.js} +0 -0
@@ -1565,6 +1565,55 @@ var Tooltip = class {
1565
1565
  this.trigger.removeAttribute("data-scope");
1566
1566
  }
1567
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
+ }
1568
1617
  function menuController(opts) {
1569
1618
  return {
1570
1619
  machine: menu__namespace.machine,
@@ -1646,6 +1695,35 @@ var MENU_CSS = `
1646
1695
  color: var(--vela-fg-bright);
1647
1696
  }
1648
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
+ }
1649
1727
  /* Switch rows (boolean settings in a dropdown): a right-aligned toggle pill \u2014 the
1650
1728
  same control language as the settings dialog's toggles. */
1651
1729
  .vela-menu-switch {
@@ -1712,11 +1790,14 @@ var Surface = class _Surface {
1712
1790
  this.pinnedAnchor = null;
1713
1791
  /** The open state last announced — see {@link announce}. */
1714
1792
  this.shown = false;
1793
+ /** The list's exit animation after a close; a reopen cancels it. */
1794
+ this.exit = null;
1715
1795
  this.doc = doc;
1716
1796
  this.host = opts.host;
1717
1797
  this.onSelect = opts.onSelect;
1718
1798
  this.onFavorite = opts.onFavorite;
1719
1799
  this.checkmarks = opts.checkmarks === true;
1800
+ this.iconBadges = opts.iconBadges === true;
1720
1801
  this.positioner = doc.createElement("div");
1721
1802
  this.positioner.className = "vela-ui-layer";
1722
1803
  this.list = doc.createElement("ul");
@@ -1747,14 +1828,30 @@ var Surface = class _Surface {
1747
1828
  if (opts.triggerId && trigger) trigger.id = opts.triggerId;
1748
1829
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
1749
1830
  const api = this.ctrl.connect(service);
1750
- if (this.shown && !api.open) this.announce(false);
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
+ }
1751
1837
  if (trigger) vanilla.spreadProps(trigger, api.getTriggerProps(), this.mid);
1752
1838
  vanilla.spreadProps(this.positioner, api.getPositionerProps(), this.mid);
1753
1839
  vanilla.spreadProps(this.list, api.getContentProps(), this.mid);
1754
1840
  this.project(api);
1841
+ if (closing) this.holdExit();
1755
1842
  if (api.open && !this.shown) this.announce(true);
1756
1843
  });
1757
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;
1853
+ });
1854
+ }
1758
1855
  get api() {
1759
1856
  return this.ctrl.connect(this.handle.service);
1760
1857
  }
@@ -1778,6 +1875,7 @@ var Surface = class _Surface {
1778
1875
  destroy() {
1779
1876
  for (const sub2 of this.subs.values()) sub2.destroy();
1780
1877
  this.subs.clear();
1878
+ this.exit?.finish();
1781
1879
  this.handle.stop();
1782
1880
  if (this.shown) this.announce(false);
1783
1881
  this.positioner.remove();
@@ -1804,6 +1902,10 @@ var Surface = class _Surface {
1804
1902
  const doc = this.doc;
1805
1903
  this.list.replaceChildren();
1806
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;
1807
1909
  for (const item of this.items) {
1808
1910
  if (item.separatorBefore) {
1809
1911
  const sep = doc.createElement("li");
@@ -1829,7 +1931,16 @@ var Surface = class _Surface {
1829
1931
  } else if (!branch && !item.toggle && item.checked) {
1830
1932
  li.dataset.checked = "1";
1831
1933
  }
1832
- 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
+ }
1833
1944
  const label = doc.createElement("span");
1834
1945
  label.className = "vela-menu-label";
1835
1946
  label.textContent = item.label;
@@ -1877,6 +1988,7 @@ var Surface = class _Surface {
1877
1988
  onSelect: this.onSelect,
1878
1989
  onFavorite: this.onFavorite,
1879
1990
  checkmarks: this.checkmarks,
1991
+ iconBadges: this.iconBadges,
1880
1992
  id: `${this.mid}--${item.id}`
1881
1993
  });
1882
1994
  sub2.setItems(item.submenu ?? []);
@@ -1902,7 +2014,8 @@ var Menu = class {
1902
2014
  id: opts.id,
1903
2015
  minWidth: opts.minWidth,
1904
2016
  onFavorite: opts.onFavorite,
1905
- checkmarks: opts.checkmarks
2017
+ checkmarks: opts.checkmarks,
2018
+ iconBadges: opts.iconBadges
1906
2019
  });
1907
2020
  this.root.setItems(opts.items);
1908
2021
  }
@@ -2006,6 +2119,7 @@ var POPOVER_CSS = `
2006
2119
 
2007
2120
  // src/ui/components/popover/view.ts
2008
2121
  var open = null;
2122
+ var leaving = /* @__PURE__ */ new Set();
2009
2123
  function closeOpenPopovers() {
2010
2124
  open?.hide();
2011
2125
  }
@@ -2033,6 +2147,8 @@ var Popover = class {
2033
2147
  this.shown = false;
2034
2148
  /** The pending removal of a fading-out shell; a show() that reuses the shell cancels it. */
2035
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;
2036
2152
  const doc = opts.trigger.ownerDocument;
2037
2153
  injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
2038
2154
  this.trigger = opts.trigger;
@@ -2069,6 +2185,10 @@ var Popover = class {
2069
2185
  clearTimeout(this.leaveTimer);
2070
2186
  this.leaveTimer = null;
2071
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();
2072
2192
  ensureUIHost(this.el, this.inheritsTokens ? void 0 : this.theme);
2073
2193
  if (this.fadeMs > 0) {
2074
2194
  this.el.style.transition = `opacity ${this.fadeMs}ms ease`;
@@ -2125,7 +2245,12 @@ var Popover = class {
2125
2245
  this.el.remove();
2126
2246
  }, this.fadeMs);
2127
2247
  } else {
2128
- 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);
2129
2254
  }
2130
2255
  if (open === this) open = null;
2131
2256
  this.ctrl.onClose?.();
@@ -2137,6 +2262,7 @@ var Popover = class {
2137
2262
  }
2138
2263
  destroy() {
2139
2264
  this.hide();
2265
+ this.exit?.finish();
2140
2266
  }
2141
2267
  reposition() {
2142
2268
  if (this.shown) this.place();
@@ -2334,6 +2460,9 @@ var Dialog = class {
2334
2460
  constructor(opts = {}) {
2335
2461
  this.opener = null;
2336
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;
2337
2466
  const doc = (opts.host ?? document.body).ownerDocument;
2338
2467
  injectStyles(DIALOG_STYLE_ID, DIALOG_CSS, doc);
2339
2468
  const host = opts.host ?? doc.body;
@@ -2430,19 +2559,57 @@ var Dialog = class {
2430
2559
  const mid = String(this.ctrl.props.id);
2431
2560
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
2432
2561
  const api = this.ctrl.connect(service);
2433
- if (this.wasOpen && !api.open) this.announce(false);
2562
+ const closing = this.wasOpen && !api.open;
2563
+ if (closing) this.announce(false);
2434
2564
  vanilla.spreadProps(this.backdrop, api.getBackdropProps(), mid);
2435
2565
  vanilla.spreadProps(this.positioner, api.getPositionerProps(), mid);
2436
2566
  vanilla.spreadProps(this.panel, api.getContentProps(), mid);
2437
2567
  vanilla.spreadProps(title, api.getTitleProps(), mid);
2438
2568
  vanilla.spreadProps(close, api.getCloseTriggerProps(), mid);
2439
- this.backdrop.style.display = api.open ? "" : "none";
2440
- this.positioner.style.display = api.open ? "" : "none";
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
+ }
2441
2578
  if (api.open && !this.wasOpen) this.announce(true);
2442
2579
  if (this.wasOpen && !api.open) this.restoreOpener();
2443
2580
  this.wasOpen = api.open;
2444
2581
  });
2445
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
+ }
2446
2613
  /** Closing hands focus back to whatever opened it (Escape, the ✕, hide(), or a
2447
2614
  * teardown that outruns the machine's close notification). */
2448
2615
  restoreOpener() {
@@ -2471,15 +2638,16 @@ var Dialog = class {
2471
2638
  return el?.closest(".vela-popover, .vela-menu") != null;
2472
2639
  }
2473
2640
  destroy() {
2641
+ if (this.wasOpen && !this.open) this.handle.flush();
2474
2642
  if (this.open) this.hide();
2475
2643
  this.handle.stop();
2644
+ this.destroyed = true;
2476
2645
  if (this.wasOpen) {
2477
2646
  this.wasOpen = false;
2478
2647
  this.announce(false);
2479
2648
  }
2480
2649
  this.restoreOpener();
2481
- this.backdrop.remove();
2482
- this.positioner.remove();
2650
+ if (!this.exit) this.detach();
2483
2651
  }
2484
2652
  };
2485
2653
  function drawerController(opts = {}) {
@@ -2608,6 +2776,9 @@ var Drawer = class {
2608
2776
  constructor(opts = {}) {
2609
2777
  this.opener = null;
2610
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;
2611
2782
  const doc = (opts.host ?? document.body).ownerDocument;
2612
2783
  injectStyles(DRAWER_STYLE_ID, DRAWER_CSS, doc);
2613
2784
  const host = opts.host ?? doc.body;
@@ -2635,13 +2806,21 @@ var Drawer = class {
2635
2806
  const mid = String(this.ctrl.props.id);
2636
2807
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
2637
2808
  const api = this.ctrl.connect(service);
2638
- if (this.wasOpen && !api.open) this.announce(false);
2809
+ const closing = this.wasOpen && !api.open;
2810
+ if (closing) this.announce(false);
2639
2811
  vanilla.spreadProps(this.backdrop, api.getBackdropProps(), mid);
2640
2812
  vanilla.spreadProps(this.positioner, api.getPositionerProps(), mid);
2641
2813
  vanilla.spreadProps(this.panel, api.getContentProps(), mid);
2642
2814
  vanilla.spreadProps(this.titleEl, api.getTitleProps(), mid);
2643
- this.backdrop.style.display = api.open ? "" : "none";
2644
- 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
+ }
2645
2824
  if (api.open && !this.wasOpen) this.announce(true);
2646
2825
  if (this.wasOpen && !api.open) this.restoreOpener();
2647
2826
  this.wasOpen = api.open;
@@ -2753,6 +2932,36 @@ var Drawer = class {
2753
2932
  setTitle(title) {
2754
2933
  this.titleEl.textContent = title;
2755
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
+ }
2756
2965
  /** Closing hands focus back to whatever opened it (Escape, the ✕, hide(), or a
2757
2966
  * teardown that outruns the machine's close notification). */
2758
2967
  restoreOpener() {
@@ -2775,14 +2984,15 @@ var Drawer = class {
2775
2984
  this.ctrl.connect(this.handle.service).setOpen(false);
2776
2985
  }
2777
2986
  destroy() {
2987
+ if (this.wasOpen && !this.open) this.handle.flush();
2778
2988
  this.handle.stop();
2989
+ this.destroyed = true;
2779
2990
  if (this.wasOpen) {
2780
2991
  this.wasOpen = false;
2781
2992
  this.announce(false);
2782
2993
  }
2783
2994
  this.restoreOpener();
2784
- this.backdrop.remove();
2785
- this.positioner.remove();
2995
+ if (!this.exit) this.detach();
2786
2996
  }
2787
2997
  };
2788
2998
 
@@ -4918,11 +5128,11 @@ var DatePicker = class {
4918
5128
  };
4919
5129
 
4920
5130
  // src/widget/layout-picker.ts
4921
- var STYLE_ID = "vela-widget-layout-picker-v14";
5131
+ var STYLE_ID = "vela-widget-layout-picker-v15";
4922
5132
  var CSS2 = `
4923
5133
  .vela-lp-layer { position: absolute; z-index: var(--vela-z-menu); }
4924
5134
  .vela-lp {
4925
- background: var(--vela-surface-elev);
5135
+ background: var(--vela-surface);
4926
5136
  color: var(--vela-fg);
4927
5137
  border: 1px solid var(--vela-border-strong);
4928
5138
  border-radius: 8px;
@@ -5051,6 +5261,8 @@ var LayoutPicker = class {
5051
5261
  constructor(opts) {
5052
5262
  this.squares = [];
5053
5263
  this.isOpen = false;
5264
+ /** The card's exit animation after a close; a reopen cancels it. */
5265
+ this.exit = null;
5054
5266
  /** Hover preview (1-based rows/cols), null = show current shape. */
5055
5267
  this.hover = null;
5056
5268
  this.onDocPointerDown = (e) => {
@@ -5145,6 +5357,8 @@ var LayoutPicker = class {
5145
5357
  open() {
5146
5358
  if (this.isOpen) return;
5147
5359
  this.isOpen = true;
5360
+ this.exit?.cancel();
5361
+ this.exit = null;
5148
5362
  this.hover = null;
5149
5363
  this.refresh();
5150
5364
  this.layer.style.display = "";
@@ -5159,7 +5373,10 @@ var LayoutPicker = class {
5159
5373
  if (!this.isOpen) return;
5160
5374
  this.isOpen = false;
5161
5375
  announceSurface(this.panel, false, "popover", this.opts.trigger);
5162
- this.layer.style.display = "none";
5376
+ this.exit = holdForExit(this.panel, () => {
5377
+ this.exit = null;
5378
+ if (!this.isOpen) this.layer.style.display = "none";
5379
+ });
5163
5380
  this.opts.trigger.setAttribute("aria-expanded", "false");
5164
5381
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
5165
5382
  this.doc.removeEventListener("keydown", this.onDocKeydown, true);
@@ -5174,6 +5391,7 @@ var LayoutPicker = class {
5174
5391
  }
5175
5392
  destroy() {
5176
5393
  this.close();
5394
+ this.exit?.finish();
5177
5395
  this.infoTip.destroy();
5178
5396
  this.layer.remove();
5179
5397
  }
@@ -5240,7 +5458,7 @@ var LayoutPicker = class {
5240
5458
  const hostRect = this.opts.host.getBoundingClientRect();
5241
5459
  const trigRect = this.opts.trigger.getBoundingClientRect();
5242
5460
  let left = trigRect.left - hostRect.left;
5243
- const top = trigRect.bottom - hostRect.top + 4;
5461
+ const top = trigRect.bottom - hostRect.top + 8;
5244
5462
  const width = this.layer.offsetWidth;
5245
5463
  if (left + width > hostRect.width - 8) left = Math.max(8, hostRect.width - 8 - width);
5246
5464
  this.layer.style.left = `${left}px`;
@@ -6217,7 +6435,8 @@ var Topbar = class {
6217
6435
  triggerId: "vela-topbar-style",
6218
6436
  host,
6219
6437
  items: this.styleItems(),
6220
- onSelect: (id) => opts.onPriceStyle(id)
6438
+ onSelect: (id) => opts.onPriceStyle(id),
6439
+ iconBadges: true
6221
6440
  });
6222
6441
  }
6223
6442
  setSymbol(symbol) {
@@ -6283,8 +6502,14 @@ var Topbar = class {
6283
6502
  renderLayoutButton(doc) {
6284
6503
  if (!this.layoutButton) return;
6285
6504
  this.layoutButton.replaceChildren();
6286
- if (iconMarkup("layout")) this.layoutButton.appendChild(iconEl("layout", doc));
6287
- 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
+ }
6288
6513
  this.layoutButton.setAttribute("aria-label", `Layout \u2014 ${this.layoutId ?? ""}`);
6289
6514
  }
6290
6515
  renderStyleButton(doc) {
@@ -12959,6 +13184,11 @@ var SidePanel = class {
12959
13184
  this.maximizedOn = false;
12960
13185
  /** Set while the close is announced, so a listener's own close does not re-enter. */
12961
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;
12962
13192
  const doc = host.ownerDocument;
12963
13193
  injectStyles(STYLE_ID4, CSS5, doc);
12964
13194
  this.minWidth = opts.resizable ? opts.minWidth ?? DEFAULT_PANEL_MIN_WIDTH : 1;
@@ -13007,20 +13237,44 @@ var SidePanel = class {
13007
13237
  host.appendChild(this.el);
13008
13238
  }
13009
13239
  get open() {
13010
- return !this.el.hidden;
13240
+ return this.isOpen;
13011
13241
  }
13012
- /** Open/close the panel — a bare call flips it. */
13013
- toggle(open2 = this.el.hidden) {
13014
- if (open2 === !this.el.hidden || !open2 && this.closing) return;
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;
13015
13254
  if (!open2) {
13016
13255
  this.closing = true;
13017
13256
  announceSurface(this.el, false, "panel");
13018
13257
  this.closing = false;
13019
13258
  }
13020
- this.el.hidden = !open2;
13021
- if (!open2) this.setMaximized(false);
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
+ }
13022
13276
  this.onOpenChange?.(open2);
13023
- if (open2 && !this.el.hidden) announceSurface(this.el, true, "panel");
13277
+ if (open2 && this.isOpen) announceSurface(this.el, true, "panel");
13024
13278
  }
13025
13279
  /** Whether the panel covers every chart right now. */
13026
13280
  get maximized() {
@@ -13094,6 +13348,7 @@ var SidePanel = class {
13094
13348
  }
13095
13349
  destroy() {
13096
13350
  if (this.open) announceSurface(this.el, false, "panel");
13351
+ this.exit?.finish();
13097
13352
  this.el.remove();
13098
13353
  }
13099
13354
  /** The drag handle on the panel's inner (left) edge — the panel is docked right, so dragging
@@ -13398,8 +13653,8 @@ var ObjectTree = class extends SidePanel {
13398
13653
  set groups(next) {
13399
13654
  if (this.chart) this.groupsPerChart.set(this.chart, next);
13400
13655
  }
13401
- toggle(open2 = this.el.hidden) {
13402
- super.toggle(open2);
13656
+ toggle(open2 = !this.open, instant = false) {
13657
+ super.toggle(open2, instant);
13403
13658
  if (open2) this.refresh();
13404
13659
  }
13405
13660
  setSymbol(symbol) {
@@ -14433,8 +14688,8 @@ var DataWindow = class extends SidePanel {
14433
14688
  this.unsubs = [];
14434
14689
  injectStyles(STYLE_ID6, CSS7, host.ownerDocument);
14435
14690
  }
14436
- toggle(open2 = this.el.hidden) {
14437
- super.toggle(open2);
14691
+ toggle(open2 = !this.open, instant = false) {
14692
+ super.toggle(open2, instant);
14438
14693
  if (open2) this.refresh();
14439
14694
  }
14440
14695
  /** (Re)bind to a chart instance — called after every widget rebuild. */
@@ -14604,7 +14859,7 @@ var PanelDock = class {
14604
14859
  }
14605
14860
  this.pinned = new Set(state.pinned ?? []);
14606
14861
  for (const entry of this.entries) entry.panel.setOverlay(!this.pinned.has(entry.id));
14607
- 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);
14608
14863
  this.pendingOpen = state.open && !this.entries.some((e) => e.id === state.open) ? state.open : null;
14609
14864
  }
14610
14865
  /** Drop the contributed panels (the shell destroys its own). */
@@ -14620,7 +14875,7 @@ var PanelDock = class {
14620
14875
  if (this.pinned.has(entry.id)) entry.panel.setOverlay(false);
14621
14876
  entry.panel.onOpenChange = (open2) => {
14622
14877
  if (open2) {
14623
- 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);
14624
14879
  this.pendingOpen = null;
14625
14880
  }
14626
14881
  this.deps.chrome.setPanelActive(entry.id, open2);
@@ -16296,7 +16551,11 @@ var MoreDrawer = class {
16296
16551
  row(doc, label, opts) {
16297
16552
  const el = doc.createElement("div");
16298
16553
  el.className = "vela-md-row";
16299
- 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
+ }
16300
16559
  const text = doc.createElement("span");
16301
16560
  text.className = "vela-md-row-label";
16302
16561
  text.textContent = label;
@@ -16338,7 +16597,7 @@ var MoreDrawer = class {
16338
16597
  if (this.opts.layout) {
16339
16598
  const shape = this.opts.layout.shape();
16340
16599
  const value = shape ? `${shape.cols} \xD7 ${shape.rows}` : this.opts.layout.presets().find((p) => p.checked)?.label;
16341
- 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") }));
16342
16601
  }
16343
16602
  for (const act of this.opts.primaryActions?.() ?? []) list.appendChild(this.actionRow(doc, act));
16344
16603
  for (const panel of this.opts.panels()) {
@@ -16516,8 +16775,8 @@ var SETTINGS_SECTION = {
16516
16775
  "price-axis": "Scales and lines",
16517
16776
  "time-axis": "Scales and lines"
16518
16777
  };
16519
- function settingsItem(zone, label) {
16520
- 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 };
16521
16780
  }
16522
16781
  function settingsSectionOf(id) {
16523
16782
  return id.slice("settings:".length) || void 0;
@@ -16597,12 +16856,39 @@ function timeAxisItems(timezone) {
16597
16856
  }
16598
16857
  function bodyItems(counts) {
16599
16858
  return [
16600
- { id: "reset-view", label: "Reset chart view" },
16601
- { id: "remove-drawings", label: "Remove drawings", disabled: counts.drawings === 0, separatorBefore: true },
16602
- { id: "remove-indicators", label: "Remove indicators", disabled: counts.indicators === 0 },
16603
- 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")
16604
16863
  ];
16605
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
+ }
16606
16892
 
16607
16893
  // src/widget/price-scale-drawer.ts
16608
16894
  var STYLE_ID17 = "vela-widget-price-scale-drawer";
@@ -17877,6 +18163,7 @@ function chromeHint(text, opts) {
17877
18163
  // src/renderers/native/drawings/DrawingToolbar.ts
17878
18164
  var TOOLBAR_WIDTH = 44;
17879
18165
  var TOOLBAR_COLLAPSED_WIDTH = 16;
18166
+ var FLYOUT_GAP = 3;
17880
18167
  var DrawingToolbar = class {
17881
18168
  constructor(host, theme, onArm, onMagnet = () => {
17882
18169
  }, onMeasure = () => {
@@ -17906,6 +18193,9 @@ var DrawingToolbar = class {
17906
18193
  // group id (or MAGNET_ID) whose flyout is open
17907
18194
  this.flyoutCell = null;
17908
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();
17909
18199
  this.groupCells = /* @__PURE__ */ new Map();
17910
18200
  // the composite cell (hover/active bg + flyout anchor)
17911
18201
  this.groupIcons = /* @__PURE__ */ new Map();
@@ -18014,6 +18304,7 @@ var DrawingToolbar = class {
18014
18304
  }
18015
18305
  destroy() {
18016
18306
  this.closeFlyout();
18307
+ for (const { exit } of [...this.exitingFlyouts.values()]) exit.finish();
18017
18308
  for (const dispose of this.tipDisposers.splice(0)) dispose();
18018
18309
  this.root.remove();
18019
18310
  }
@@ -18241,16 +18532,22 @@ var DrawingToolbar = class {
18241
18532
  * outside-dismiss. Callers fill it with items. */
18242
18533
  beginFlyout(ownerId, cell) {
18243
18534
  const t = this.theme;
18244
- 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");
18245
18542
  fly.className = "vela-dtb-flyout";
18246
- 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;`;
18247
18544
  applyChromeTokens(fly, t);
18248
18545
  this.host.appendChild(fly);
18249
18546
  const r = cell.getBoundingClientRect();
18250
18547
  const rootR = this.root.getBoundingClientRect();
18251
18548
  const hostR = this.host.getBoundingClientRect();
18252
18549
  const top = r.top - hostR.top;
18253
- fly.style.left = `${rootR.right - hostR.left - 1}px`;
18550
+ fly.style.left = `${rootR.right - hostR.left + FLYOUT_GAP}px`;
18254
18551
  fly.style.top = `${top}px`;
18255
18552
  fly.style.maxHeight = `${Math.max(120, hostR.height - top - 8)}px`;
18256
18553
  this.flyout = fly;
@@ -18266,6 +18563,7 @@ var DrawingToolbar = class {
18266
18563
  if (!cell) return;
18267
18564
  const fly = this.beginFlyout(group.id, cell);
18268
18565
  const sections = group.sections ?? [{ label: "", tools: group.tools }];
18566
+ const badge = sections.some((s) => s.tools.some((tool) => !!tool.icon));
18269
18567
  for (let si = 0; si < sections.length; si++) {
18270
18568
  const section = sections[si];
18271
18569
  if (section.label) {
@@ -18276,6 +18574,7 @@ var DrawingToolbar = class {
18276
18574
  fly.appendChild(
18277
18575
  this.makeFlyoutItem({
18278
18576
  icon: tool.icon,
18577
+ badge,
18279
18578
  label: tool.label,
18280
18579
  selected: tool.type === this.active,
18281
18580
  shortcut: this.shortcuts.get(tool.type),
@@ -18326,21 +18625,24 @@ var DrawingToolbar = class {
18326
18625
  div.style.cssText = `height:1px;margin:4px 8px;background:${this.borderColor};`;
18327
18626
  return div;
18328
18627
  }
18329
- /** A flyout row, left to right: optional leading icon, label, a check when it's the selected
18330
- * entry, an optional shortcut hint, and — at the far right — the favorite star (tool rows).
18331
- * 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. */
18332
18631
  makeFlyoutItem(opts) {
18333
18632
  const t = this.theme;
18334
18633
  const item = document.createElement("button");
18335
18634
  item.type = "button";
18336
18635
  item.className = "vela-dtb-item";
18337
18636
  item.setAttribute("aria-label", opts.label);
18338
- 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;`;
18339
- if (opts.icon) {
18340
- const icon2 = document.createElement("span");
18341
- icon2.style.cssText = "width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex:none;";
18342
- icon2.innerHTML = sizedIcon(opts.icon);
18343
- 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);
18344
18646
  }
18345
18647
  const label = document.createElement("span");
18346
18648
  label.textContent = opts.label;
@@ -18385,7 +18687,12 @@ var DrawingToolbar = class {
18385
18687
  if (fly) {
18386
18688
  this.flyout = null;
18387
18689
  announceSurface(fly, false, "menu", this.flyoutCell);
18388
- fly.remove();
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 });
18389
18696
  this.flyoutCell?.classList.remove("vela-open");
18390
18697
  this.flyoutCell = null;
18391
18698
  this.flyoutOwnerId = null;
@@ -18469,6 +18776,11 @@ function ensureStyles() {
18469
18776
  .vela-dtb[data-collapsed='1'] .vela-dtb-collapse .vela-dtb-hit{width:14px;}
18470
18777
  .vela-dtb-item{background:transparent;border:none;transition:background var(--vela-dur-fast) ease;}
18471
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);}
18472
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;}
18473
18785
  .vela-dtb-star svg{width:16px;height:16px;}
18474
18786
  .vela-dtb-item:hover .vela-dtb-star{opacity:.55;}
@@ -18836,15 +19148,48 @@ function syncTargets(originId, setting, cellIds) {
18836
19148
  function rangesWithin(a, b, epsMs) {
18837
19149
  return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
18838
19150
  }
18839
- var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
18840
- function styleConfigSlice(config) {
18841
- if (config == null || typeof config !== "object") return null;
18842
- const doc = config;
18843
- 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 = {};
18844
19171
  for (const key of STYLE_SYNC_CONFIG_KEYS) {
18845
- 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;
18846
19174
  }
18847
- 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;
18848
19193
  }
18849
19194
 
18850
19195
  // src/workspace/persist.ts
@@ -19533,6 +19878,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19533
19878
  this.activeEngineStyle = null;
19534
19879
  /** The chart's current price style (tracked from the renderer's change events + initial read). */
19535
19880
  this.priceStyle = "candles";
19881
+ this.priceStyleWillChangeUnsub = null;
19536
19882
  this.unresolvedUnsub = null;
19537
19883
  /** Latest chart visible range (left/right bar times), fed to viewport-dependent scripts. */
19538
19884
  this.visibleRange = null;
@@ -19631,6 +19977,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19631
19977
  this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
19632
19978
  this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
19633
19979
  this.renderer.onPriceStyleChange?.((style) => this.syncPriceStyle(style));
19980
+ this.priceStyleWillChangeUnsub = this.renderer.onPriceStyleWillChange?.((from, to) => this.events.emit("priceStyle:change", { from, to })) ?? null;
19634
19981
  const initialStyle = this.renderer.readFeature("priceStyle");
19635
19982
  if (typeof initialStyle === "string") this.priceStyle = initialStyle;
19636
19983
  this.barTransform = barTransformFor(initialStyle);
@@ -21161,6 +21508,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
21161
21508
  if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
21162
21509
  this.viewportUnsub?.();
21163
21510
  this.paneActionUnsub?.();
21511
+ this.priceStyleWillChangeUnsub?.();
21512
+ this.priceStyleWillChangeUnsub = null;
21164
21513
  for (const record of this.registry.all()) {
21165
21514
  this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
21166
21515
  record.session?.stop();
@@ -23366,7 +23715,10 @@ var LEGEND_ICON_PX2 = 16;
23366
23715
  var LEGEND_CTL_PX = 18;
23367
23716
  var LEGEND_ROW_PAD_Y = 2;
23368
23717
  var LEGEND_ROW_PAD_X = 6;
23718
+ var LEGEND_LEFT_PX = 10;
23719
+ var LEGEND_SCALE_GAP_PX = 6;
23369
23720
  var LEGEND_TITLE_VALUES_GAP_PX = 6;
23721
+ var LEGEND_VALUES_GAP_PX = 5;
23370
23722
  var LEGEND_TITLE_STATUS_GAP_PX = 8;
23371
23723
  var LEGEND_ACTION_GAP_PX = 6;
23372
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;`;
@@ -23652,14 +24004,15 @@ var InputsUI = class {
23652
24004
  row.valuesEl.replaceChildren();
23653
24005
  return;
23654
24006
  }
23655
- row.valuesEl.style.display = "inline-flex";
24007
+ row.valuesEl.style.display = "block";
23656
24008
  row.valuesEl.replaceChildren();
23657
- for (const v of row.plotValues) {
24009
+ row.plotValues.forEach((v, i) => {
23658
24010
  const span = document.createElement("span");
23659
24011
  span.textContent = v.value;
23660
24012
  span.style.color = v.color;
24013
+ if (i > 0) span.style.marginLeft = `${LEGEND_VALUES_GAP_PX}px`;
23661
24014
  row.valuesEl.appendChild(span);
23662
- }
24015
+ });
23663
24016
  }
23664
24017
  // ── legend move/merge (menu + drag) ─────────────────────────────────────
23665
24018
  /** Open the "Move to" menu for a row, anchored under its move button. */
@@ -23760,7 +24113,7 @@ var InputsUI = class {
23760
24113
  if (!lg) {
23761
24114
  lg = document.createElement("div");
23762
24115
  lg.dataset.velaPane = paneId;
23763
- 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;`;
23764
24117
  applyChromeTokens(lg, this.theme);
23765
24118
  this.positionLegend(lg, paneId);
23766
24119
  this.container.appendChild(lg);
@@ -23772,6 +24125,7 @@ var InputsUI = class {
23772
24125
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
23773
24126
  lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
23774
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)`;
23775
24129
  const collapsed = this.paneCollapse.has(paneId);
23776
24130
  const masterId = this.paneCollapse.get(paneId) ?? null;
23777
24131
  for (const row of this.rows.values()) {
@@ -23863,7 +24217,7 @@ var InputsUI = class {
23863
24217
  }
23864
24218
  ensureDialogStyles();
23865
24219
  const el = document.createElement("div");
23866
- 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;`;
23867
24221
  el.addEventListener("mouseleave", () => {
23868
24222
  if (this.selectedId !== id) this.setRowHighlighted(id, false);
23869
24223
  });
@@ -23891,7 +24245,7 @@ var InputsUI = class {
23891
24245
  const statusEl = document.createElement("span");
23892
24246
  statusEl.style.cssText = "display:none;box-sizing:border-box;flex:none;";
23893
24247
  const titleWrap = document.createElement("span");
23894
- titleWrap.style.cssText = "white-space:nowrap;";
24248
+ titleWrap.style.cssText = "white-space:nowrap;min-width:0;overflow:hidden;text-overflow:ellipsis;";
23895
24249
  titleWrap.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
23896
24250
  const titleEl = document.createElement("span");
23897
24251
  titleEl.textContent = title;
@@ -23909,7 +24263,7 @@ var InputsUI = class {
23909
24263
  el.appendChild(calloutsEl);
23910
24264
  el.appendChild(statusEl);
23911
24265
  const valuesEl = document.createElement("span");
23912
- 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;`;
23913
24267
  el.appendChild(valuesEl);
23914
24268
  const controlsEl = document.createElement("span");
23915
24269
  controlsEl.style.cssText = `display:none;align-items:center;gap:${LEGEND_ACTION_GAP_PX}px;flex:none;margin-left:${LEGEND_ACTION_GAP_PX}px;`;
@@ -26153,6 +26507,11 @@ var InputController = class {
26153
26507
  /** When true (the default), the separators between stacked panes are draggable to
26154
26508
  * resize the panes above/below. When false, a press there is a normal data-area pan. */
26155
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;
26156
26515
  this.el = null;
26157
26516
  this.dragging = false;
26158
26517
  this.moved = false;
@@ -26216,18 +26575,18 @@ var InputController = class {
26216
26575
  this.onModifier = (e) => {
26217
26576
  if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
26218
26577
  if (Number.isNaN(this.cursorX)) return;
26219
- if (this.dragging && this.region !== "drawing") return;
26578
+ if (this.dragging ? this.region !== "drawing" : !this.drawings) return;
26220
26579
  this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
26221
26580
  };
26222
26581
  this.onDown = (e) => {
26223
26582
  if (e.button === 1) {
26224
26583
  const { x: x2, y: y2 } = this.local(e);
26225
- this.middleDeleted = this.deps.drawingsDeleteAt?.(x2, y2) ?? false;
26584
+ this.middleDeleted = this.drawings && (this.deps.drawingsDeleteAt?.(x2, y2) ?? false);
26226
26585
  if (this.middleDeleted) e.preventDefault();
26227
26586
  return;
26228
26587
  }
26229
26588
  if (e.button === 2) {
26230
- this.rightCancelled = this.deps.drawingsCancelPlacement?.() ?? false;
26589
+ this.rightCancelled = this.drawings && (this.deps.drawingsCancelPlacement?.() ?? false);
26231
26590
  if (this.rightCancelled) e.preventDefault();
26232
26591
  return;
26233
26592
  }
@@ -26249,20 +26608,20 @@ var InputController = class {
26249
26608
  this.startY = y;
26250
26609
  this.prevPressDrawing = this.lastPressDrawing;
26251
26610
  this.lastPressDrawing = false;
26252
- if (this.deps.drawingsClaim?.(x, y)) {
26611
+ if (this.drawings && this.deps.drawingsClaim?.(x, y)) {
26253
26612
  this.region = "drawing";
26254
26613
  this.lastPressDrawing = true;
26255
26614
  this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
26256
26615
  this.capture(e.pointerId);
26257
26616
  return;
26258
26617
  }
26259
- 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))) {
26260
26619
  this.region = "drawing";
26261
26620
  this.lastPressDrawing = true;
26262
26621
  this.capture(e.pointerId);
26263
26622
  return;
26264
26623
  }
26265
- 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)) {
26266
26625
  this.region = "drawing";
26267
26626
  this.lastPressDrawing = true;
26268
26627
  this.capture(e.pointerId);
@@ -26339,7 +26698,7 @@ var InputController = class {
26339
26698
  const drawCursor = this.deps.drawingsCursor?.(x, y);
26340
26699
  const r = this.regionAt(x, y);
26341
26700
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
26342
- 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);
26343
26702
  }
26344
26703
  if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
26345
26704
  };
@@ -26581,7 +26940,7 @@ var InputController = class {
26581
26940
  }
26582
26941
  /** Shared double-click / double-tap routing, by the region under the point. */
26583
26942
  doubleActivate(x, y) {
26584
- if (this.deps.drawingsDblClick?.(x, y)) return;
26943
+ if (this.drawings && this.deps.drawingsDblClick?.(x, y)) return;
26585
26944
  const region = this.regionAt(x, y);
26586
26945
  if (region === "price") this.deps.resetPriceScale(x, y);
26587
26946
  else if (region === "separator") this.deps.resetPaneSize(y);
@@ -28595,10 +28954,13 @@ function priceTicks(min, max, target = 6) {
28595
28954
  const norm = raw / mag;
28596
28955
  const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
28597
28956
  const decimals = Math.max(0, -Math.floor(Math.log10(step)) + 1);
28598
- const out = [];
28599
28957
  const start = Math.ceil(min / step) * step;
28600
- for (let v = start; v <= max + step * 1e-6; v += step) {
28601
- 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)));
28602
28964
  }
28603
28965
  return out;
28604
28966
  }
@@ -28796,6 +29158,100 @@ function calendarTicks(zFrom, zTo, months, offsetMs) {
28796
29158
  return out;
28797
29159
  }
28798
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
+
28799
29255
  // src/renderers/native/chrome/countdown.ts
28800
29256
  function countdownText(barOpen, barMs, now2) {
28801
29257
  if (!(barMs > 0)) return null;
@@ -29148,8 +29604,11 @@ function standaloneSvg(markup, ink, px) {
29148
29604
  }
29149
29605
 
29150
29606
  // src/renderers/native/chrome/ChromeRenderer.ts
29607
+ var MINOR_LABEL_ALPHA = 0.5;
29151
29608
  var ChromeRenderer = class {
29152
- constructor() {
29609
+ /** `axisTicks` is shared with the backdrop layer, so price labels and gridlines agree. */
29610
+ constructor(axisTicks = new PriceAxisTickSource()) {
29611
+ this.axisTicks = axisTicks;
29153
29612
  this.canvas = null;
29154
29613
  this.ctx = null;
29155
29614
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
@@ -29254,7 +29713,8 @@ var ChromeRenderer = class {
29254
29713
  this.drawTimeAxis(ctx, scene, coords, theme, dataW, dataH, fullH);
29255
29714
  this.drawMarkLane(ctx, scene, coords, theme, dataW, dataH);
29256
29715
  }
29257
- /** 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. */
29258
29718
  drawMarkLane(ctx, scene, coords, theme, dataW, dataH) {
29259
29719
  if (!scene.marks.visible || scene.timelineMarks.length === 0) {
29260
29720
  this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
@@ -29272,6 +29732,10 @@ var ChromeRenderer = class {
29272
29732
  expanded: scene.marksExpandedStack
29273
29733
  });
29274
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();
29275
29739
  paintMarkLane(ctx, this.markLayout, {
29276
29740
  axisY: dataH,
29277
29741
  background: theme.background,
@@ -29285,6 +29749,7 @@ var ChromeRenderer = class {
29285
29749
  flashKey: scene.marksFlash && scene.marksFlash.until > nowMs ? scene.marksFlash.key : null,
29286
29750
  nowMs
29287
29751
  });
29752
+ ctx.restore();
29288
29753
  }
29289
29754
  barTimes(scene) {
29290
29755
  if (this.barTimesSrc !== scene.bars || this.barTimesCache.length !== scene.bars.length) {
@@ -29341,13 +29806,19 @@ var ChromeRenderer = class {
29341
29806
  if (!scene.showAxisLabels) return;
29342
29807
  ctx.fillStyle = this.axisTextColor;
29343
29808
  ctx.textAlign = "left";
29809
+ const font = `${scene.style.fontSize}px ${theme.fontFamily}`;
29810
+ const majorFont = `600 ${font}`;
29811
+ const alpha = ctx.globalAlpha;
29344
29812
  for (const pane of panes) {
29345
29813
  if (pane.collapsed) continue;
29346
- const pct = percentScaleFor(scene, pane);
29347
- 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)) {
29348
29815
  const y = coords.priceToY(t.price, pane.scale, pane.bounds);
29349
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;
29350
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;
29351
29822
  }
29352
29823
  if (pane.axisBands) {
29353
29824
  for (const b of pane.axisBands) {
@@ -29589,15 +30060,23 @@ var CrosshairRenderer = class {
29589
30060
  constructor() {
29590
30061
  this.canvas = null;
29591
30062
  this.ctx = null;
30063
+ this.shade = null;
29592
30064
  }
29593
30065
  mount(canvas) {
29594
30066
  this.canvas = canvas;
29595
30067
  this.ctx = canvas.getContext("2d");
29596
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
+ }
29597
30075
  /** Clear the cursor canvas and (re)draw the crosshair lines + axis chips. The optional
29598
30076
  * `separatorHoverY` highlights the draggable pane separator under the cursor;
29599
30077
  * `external` is a SYNCED ghost crosshair (another chart's pointer, pixel-resolved). */
29600
30078
  render(scene, coords, theme, separatorHoverY = null, external = null) {
30079
+ this.shade = null;
29601
30080
  const ctx = this.ctx;
29602
30081
  const canvas = this.canvas;
29603
30082
  if (!ctx || !canvas) return;
@@ -29620,12 +30099,7 @@ var CrosshairRenderer = class {
29620
30099
  const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
29621
30100
  if (vertical && ov?.shadeRight) {
29622
30101
  const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
29623
- if (from < dataW) {
29624
- ctx.fillStyle = ov.shadeRight.color;
29625
- ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
29626
- ctx.fillRect(from, 0, dataW - from, dataH);
29627
- ctx.globalAlpha = 1;
29628
- }
30102
+ if (from < dataW) this.paintShade(ctx, ov.shadeRight, from, dataW, dataH);
29629
30103
  }
29630
30104
  ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
29631
30105
  ctx.lineWidth = ov?.width ?? cs.width;
@@ -29661,6 +30135,7 @@ var CrosshairRenderer = class {
29661
30135
  destroy() {
29662
30136
  this.canvas = null;
29663
30137
  this.ctx = null;
30138
+ this.shade = null;
29664
30139
  }
29665
30140
  /** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
29666
30141
  * CONTAINING the foreign time (+ horizontal line when a comparable price came
@@ -29680,12 +30155,7 @@ var CrosshairRenderer = class {
29680
30155
  if (ov?.vertical === false) return;
29681
30156
  if (ov?.shadeRight) {
29682
30157
  const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
29683
- if (from < dataW) {
29684
- ctx.fillStyle = ov.shadeRight.color;
29685
- ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
29686
- ctx.fillRect(from, 0, dataW - from, dataH);
29687
- ctx.globalAlpha = 1;
29688
- }
30158
+ if (from < dataW) this.paintShade(ctx, ov.shadeRight, from, dataW, dataH);
29689
30159
  }
29690
30160
  if (ext.line === false || x < 0 || x > dataW) return;
29691
30161
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
@@ -29721,6 +30191,15 @@ var CrosshairRenderer = class {
29721
30191
  this.chip(ctx, x, dataH + 1, formatTimeStamp(ext.time, scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
29722
30192
  ctx.globalAlpha = 1;
29723
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
+ }
29724
30203
  /** A soft band + a brighter crisp center line over the hovered separator, so it reads as
29725
30204
  * a draggable handle (the cursor is already `row-resize`). Spans the full width (data +
29726
30205
  * scale gutter) to match the separator itself. Theme-derived (textColor). */
@@ -33126,15 +33605,16 @@ var DrawingSettingsDialog = class {
33126
33605
  }
33127
33606
  open(drawing, actions, kind) {
33128
33607
  this.close();
33129
- const snapshot = clonePlain(drawing.serialize());
33608
+ const live = actions.resolve() ?? drawing;
33609
+ const snapshot = clonePlain(live.serialize());
33130
33610
  const grid = fieldGrid({ variant: "inputs" });
33131
33611
  grid.style.padding = "16px 20px";
33132
33612
  grid.style.overflowY = "auto";
33133
33613
  grid.style.overflowX = "hidden";
33134
33614
  grid.style.flex = "1 1 auto";
33135
- if (kind === "position" && drawing instanceof PositionTool) this.buildPosition(grid, drawing, actions);
33136
- else if (kind === "frvp" && drawing instanceof FixedRangeVolumeProfile) this.buildFrvp(grid, drawing, actions);
33137
- 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);
33138
33618
  else return;
33139
33619
  const ui = new Dialog({
33140
33620
  host: this.host,
@@ -33781,7 +34261,7 @@ var DrawingSettingsPopup = class {
33781
34261
  this.reposition();
33782
34262
  return;
33783
34263
  }
33784
- const text = drawing.text;
34264
+ const text = (actions.resolve() ?? drawing).text;
33785
34265
  const panel = document.createElement("div");
33786
34266
  panel.style.cssText = `padding:6px;border-top:1px solid var(--vela-border);display:flex;flex-direction:column;gap:4px;`;
33787
34267
  const ta = new TextArea({
@@ -34952,6 +35432,12 @@ var UserDrawingController = class {
34952
35432
  this.render();
34953
35433
  }
34954
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
+ }
34955
35441
  pointerUp(x, y, snap = "off") {
34956
35442
  if (this.eraserMode) {
34957
35443
  this.erasing = false;
@@ -35942,9 +36428,10 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
35942
36428
  consider(drawings.max);
35943
36429
  }
35944
36430
  if (min === Infinity || max === -Infinity) return { min: 0, max: 1 };
35945
- if (min === max) {
35946
- const pad = Math.abs(min) * 0.1 || 1;
35947
- 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 };
35948
36435
  }
35949
36436
  const content = Math.max(0.1, 1 - (margins.top + margins.bottom) / 100);
35950
36437
  const above = margins.top / 100 / content;
@@ -36448,7 +36935,9 @@ function bandEdgeSlot(logical) {
36448
36935
  return Math.ceil(logical) - 0.5;
36449
36936
  }
36450
36937
  var BackdropRenderer = class {
36451
- constructor() {
36938
+ /** `axisTicks` is shared with the chrome layer, so gridlines and price labels agree. */
36939
+ constructor(axisTicks = new PriceAxisTickSource()) {
36940
+ this.axisTicks = axisTicks;
36452
36941
  this.canvas = null;
36453
36942
  this.ctx = null;
36454
36943
  }
@@ -36525,9 +37014,8 @@ var BackdropRenderer = class {
36525
37014
  if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
36526
37015
  ctx.globalAlpha = gridAlpha;
36527
37016
  ctx.strokeStyle = horzColor;
36528
- const pct = percentScaleFor(scene, pane);
36529
37017
  ctx.beginPath();
36530
- 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)) {
36531
37019
  const y = Math.round(coords.priceToY(t.price, pane.scale, pane.bounds)) + 0.5;
36532
37020
  if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) continue;
36533
37021
  ctx.moveTo(0, y);
@@ -37014,8 +37502,16 @@ var NativeRenderer = class {
37014
37502
  this.toolbarGutter = 0;
37015
37503
  // px reserved on the left for the docked drawings toolbar (0 when hidden)
37016
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 = "";
37017
37511
  this.userDrawings = null;
37018
- 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);
37019
37515
  this.volumeRenderer = new VolumeRenderer();
37020
37516
  /** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
37021
37517
  /**
@@ -37048,7 +37544,7 @@ var NativeRenderer = class {
37048
37544
  this.backendMode = "auto";
37049
37545
  this.glowAmount = 0;
37050
37546
  // WebGL2 neon-glow intensity (canvas2d ignores it)
37051
- this.chrome = new ChromeRenderer();
37547
+ this.chrome = new ChromeRenderer(this.priceAxisTicks);
37052
37548
  /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
37053
37549
  this.indicatorSlices = new IndicatorDrawingSlices();
37054
37550
  /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
@@ -37165,6 +37661,8 @@ var NativeRenderer = class {
37165
37661
  // pane just below it
37166
37662
  this.resizeSplitStart = null;
37167
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;
37168
37666
  this.hoverSeparatorY = null;
37169
37667
  // pixel y of the separator under the cursor (drives its hover highlight)
37170
37668
  // ── pane management (merge / reorder / collapse / maximize) ──
@@ -37220,8 +37718,9 @@ var NativeRenderer = class {
37220
37718
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
37221
37719
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
37222
37720
  this.priceStyleCbs = /* @__PURE__ */ new Set();
37721
+ this.priceStyleWillChangeCbs = /* @__PURE__ */ new Set();
37223
37722
  this.name = "native";
37224
- 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"];
37225
37724
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
37226
37725
  * so moving onto the button doesn't count as leaving). */
37227
37726
  this.onScrollProximityMove = (e) => {
@@ -37265,6 +37764,7 @@ var NativeRenderer = class {
37265
37764
  this.scene.priceStyle = opts.priceStyle;
37266
37765
  this.scene.basePainting = basePaintingOf(opts.priceStyle);
37267
37766
  this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
37767
+ if (typeof opts.priceAxisTicks === "function") this.priceAxisTicks.setHook(opts.priceAxisTicks);
37268
37768
  }
37269
37769
  this.theme = this.deriveTheme(DARK_THEME);
37270
37770
  }
@@ -37403,6 +37903,15 @@ var NativeRenderer = class {
37403
37903
  case "crosshairOverride":
37404
37904
  this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
37405
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;
37406
37915
  case "keyboard":
37407
37916
  this.setKeyboardEnabled(Boolean(value));
37408
37917
  return;
@@ -37518,6 +38027,10 @@ var NativeRenderer = class {
37518
38027
  return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
37519
38028
  case "crosshairOverride":
37520
38029
  return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
38030
+ case "drawingsInteractive":
38031
+ return this.drawingsInteractive;
38032
+ case "priceAxisTicks":
38033
+ return this.priceAxisTicks.hook;
37521
38034
  case "keyboard":
37522
38035
  return this.keyboardEnabled;
37523
38036
  case "historyChords":
@@ -38191,6 +38704,12 @@ var NativeRenderer = class {
38191
38704
  Object.assign(this.drawingsCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
38192
38705
  this.cursorCanvas = document.createElement("canvas");
38193
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 = "";
38194
38713
  this.overlayRoot = document.createElement("div");
38195
38714
  Object.assign(this.overlayRoot.style, { position: "absolute", inset: "0", pointerEvents: "none" });
38196
38715
  this.plot = document.createElement("div");
@@ -38198,7 +38717,7 @@ var NativeRenderer = class {
38198
38717
  this.extLayers = rendererLayers().map((def) => ({ def, instance: def.create(), canvas: this.createLayerCanvas(), channel: def.id, owner: null }));
38199
38718
  const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
38200
38719
  const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
38201
- 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);
38202
38721
  this.layerOrderSig = "";
38203
38722
  this.wrapper.appendChild(this.plot);
38204
38723
  this.factoryConfig = this.getConfig();
@@ -38256,13 +38775,14 @@ var NativeRenderer = class {
38256
38775
  drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
38257
38776
  drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
38258
38777
  drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
38259
- 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),
38260
38779
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
38261
38780
  drawingsClearTransient: () => this.userDrawings?.clearTransient()
38262
38781
  });
38263
38782
  this.input.rightEdgeZoom = this.zoomAnchorMode === "right";
38264
38783
  this.input.axisDrag = this.axisDragEnabled;
38265
38784
  this.input.paneResize = this.paneResizeEnabled;
38785
+ this.input.drawings = this.drawingsInteractive;
38266
38786
  this.input.attach(this.dataCanvas);
38267
38787
  if (this.settingsEnabled) this.setSettingsEnabled(true);
38268
38788
  this.syncCountdownTimer();
@@ -38571,6 +39091,8 @@ var NativeRenderer = class {
38571
39091
  this.backend.destroy();
38572
39092
  this.chrome.destroy();
38573
39093
  this.crosshairLayer.destroy();
39094
+ this.shadeRightEl?.remove();
39095
+ this.shadeRightEl = null;
38574
39096
  this.attributionEl?.remove();
38575
39097
  this.attributionEl = null;
38576
39098
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
@@ -38917,14 +39439,21 @@ var NativeRenderer = class {
38917
39439
  this.priceStyleCbs.add(cb);
38918
39440
  return () => this.priceStyleCbs.delete(cb);
38919
39441
  }
39442
+ onPriceStyleWillChange(cb) {
39443
+ this.priceStyleWillChangeCbs.add(cb);
39444
+ return () => this.priceStyleWillChangeCbs.delete(cb);
39445
+ }
38920
39446
  /**
38921
39447
  * THE single write path for the base price style at runtime (feature set / settings dialog /
38922
- * config template — the constructor seeds the field directly, pre-listeners). Updates the
38923
- * scene, eases any reveal layer toward the new style's target, and notifies the core —
38924
- * 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).
38925
39452
  */
38926
39453
  setPriceStyle(style) {
38927
39454
  if (style === this.scene.priceStyle) return;
39455
+ const from = this.scene.priceStyle;
39456
+ for (const cb of this.priceStyleWillChangeCbs) cb(from, style);
38928
39457
  this.scene.priceStyle = style;
38929
39458
  this.scene.basePainting = basePaintingOf(style);
38930
39459
  this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
@@ -39154,7 +39683,7 @@ var NativeRenderer = class {
39154
39683
  if (this.easeLiveBar(dtMs)) active = true;
39155
39684
  this.skeletonClockMs += dtMs;
39156
39685
  this.paintData();
39157
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
39686
+ this.renderCrosshairLayer();
39158
39687
  this.updateLegendValues();
39159
39688
  this.emitViewportChange();
39160
39689
  return active;
@@ -39729,10 +40258,31 @@ var NativeRenderer = class {
39729
40258
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
39730
40259
  this.trackMarkPopover();
39731
40260
  }
39732
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
40261
+ this.renderCrosshairLayer();
39733
40262
  if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
39734
40263
  this.updateLegendValues();
39735
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
+ }
39736
40286
  /** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
39737
40287
  repaintCursorLayers() {
39738
40288
  const pane = this.scene.panes.get(PRICE_PANE_ID2);
@@ -40526,7 +41076,7 @@ var NativeRenderer = class {
40526
41076
  if (this.animator.active) {
40527
41077
  this.computeScales();
40528
41078
  this.paintData();
40529
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
41079
+ this.renderCrosshairLayer();
40530
41080
  } else {
40531
41081
  this.scheduler.flushNow(4 /* Full */);
40532
41082
  }
@@ -44541,7 +45091,8 @@ var Vela = class {
44541
45091
  glow: options.glow ?? 0,
44542
45092
  upColor: options.upColor ?? BULLISH,
44543
45093
  downColor: options.downColor ?? BEARISH,
44544
- priceStyle: options.priceStyle ?? "candles"
45094
+ priceStyle: options.priceStyle ?? "candles",
45095
+ priceAxisTicks: options.priceAxis?.ticks ?? null
44545
45096
  };
44546
45097
  const RendererClass = options.renderer ?? NativeRenderer;
44547
45098
  if (typeof RendererClass !== "function") {
@@ -46059,16 +46610,25 @@ var CellControls = class {
46059
46610
  // src/widget/context-menu.ts
46060
46611
  var PRICE_AXIS_W = 60;
46061
46612
  var TIME_AXIS_H3 = 26;
46613
+ var OFF_PLOT = { price: null, time: null, paneKind: null };
46062
46614
  var ChartContextMenu = class {
46063
46615
  constructor(host, cbs) {
46064
46616
  this.cbs = cbs;
46065
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;
46066
46625
  this.lastZone = "body";
46067
46626
  /** The pane whose scale the open price-axis menu targets (null ⇒ the main scale). */
46068
46627
  this.lastPane = null;
46069
46628
  this.onContextMenu = (e) => {
46070
46629
  e.preventDefault();
46071
46630
  if (!this.chart) return;
46631
+ this.pointer = this.crosshair;
46072
46632
  this.lastZone = this.zoneOf(e);
46073
46633
  this.lastPane = this.lastZone === "price-axis" ? this.paneAt(e) : null;
46074
46634
  this.menu.setItems(this.itemsFor(this.lastZone));
@@ -46082,15 +46642,23 @@ var ChartContextMenu = class {
46082
46642
  // Pointer-anchored action menu: checked state reads as a leading ✓, not a
46083
46643
  // washed row (which would read as hover in a menu with no trigger button).
46084
46644
  checkmarks: true,
46645
+ iconBadges: true,
46085
46646
  onSelect: (id) => this.run(id)
46086
46647
  });
46087
46648
  host.addEventListener("contextmenu", this.onContextMenu);
46088
46649
  }
46089
46650
  /** (Re)bind to a chart instance — called after every widget rebuild. */
46090
46651
  onChart(chart) {
46652
+ this.offCrosshair?.();
46091
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
+ });
46092
46658
  }
46093
46659
  destroy() {
46660
+ this.offCrosshair?.();
46661
+ this.offCrosshair = null;
46094
46662
  this.host.removeEventListener("contextmenu", this.onContextMenu);
46095
46663
  this.menu.destroy();
46096
46664
  }
@@ -46109,49 +46677,52 @@ var ChartContextMenu = class {
46109
46677
  flag(feature) {
46110
46678
  return Boolean(this.chart?.renderer.get(feature));
46111
46679
  }
46112
- 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() {
46113
46683
  const ctx = this.cbs.getContext?.();
46114
- return widgetActions(`context:${zone}`, ctx).map((a, i) => ({
46115
- id: `action:${a.id}`,
46116
- label: a.label,
46117
- icon: a.icon,
46118
- 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
46119
46694
  }));
46120
46695
  }
46121
46696
  itemsFor(zone) {
46697
+ return composeMenu(zone, this.builtinItems(zone), this.contributed(zone));
46698
+ }
46699
+ builtinItems(zone) {
46122
46700
  if (zone === "price-axis") {
46123
46701
  const pane = this.lastPane;
46124
- return [
46125
- ...priceAxisItems({
46126
- auto: this.chart?.renderer.get("autoScale") !== false,
46127
- invert: pane ? pane.invert : this.flag("invertScale"),
46128
- choice: scaleChoiceOf(pane ?? { mode: String(this.chart?.renderer.get("scaleMode") ?? "price"), log: this.flag("logScale") }),
46129
- axisLabels: this.flag("axisLabels"),
46130
- priceLabel: this.flag("priceLabel"),
46131
- countdown: this.flag("countdown"),
46132
- priceLine: this.flag("currentPriceLine")
46133
- }),
46134
- ...this.contributed(zone)
46135
- ];
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
+ });
46136
46711
  }
46137
46712
  if (zone === "time-axis") {
46138
- const tz = this.cbs.timezone?.() ?? String(this.chart?.renderer.get("timezone") ?? "Etc/UTC");
46139
- return [...timeAxisItems(tz), ...this.contributed("time-axis")];
46713
+ return timeAxisItems(this.cbs.timezone?.() ?? String(this.chart?.renderer.get("timezone") ?? "Etc/UTC"));
46140
46714
  }
46141
46715
  const chart = this.chart;
46142
- return [
46143
- ...bodyItems({
46144
- drawings: chart?.drawings.supported ? chart.drawings.all().length : 0,
46145
- indicators: chart?.indicators().length ?? 0
46146
- }),
46147
- ...this.contributed("body")
46148
- ];
46716
+ return bodyItems({
46717
+ drawings: chart?.drawings.supported ? chart.drawings.all().length : 0,
46718
+ indicators: chart?.indicators().length ?? 0
46719
+ });
46149
46720
  }
46150
46721
  run(id) {
46151
46722
  const chart = this.chart;
46152
46723
  if (!chart) return;
46153
46724
  if (id.startsWith("action:")) {
46154
- const ctx = this.cbs.getContext?.();
46725
+ const ctx = this.context();
46155
46726
  if (ctx) widgetActions(`context:${this.lastZone}`, ctx).find((a) => a.id === id.slice("action:".length))?.run(ctx);
46156
46727
  return;
46157
46728
  }
@@ -46275,8 +46846,8 @@ function seedDefaults(opts) {
46275
46846
  };
46276
46847
  }
46277
46848
  function cellChartDefaults(opts) {
46278
- const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings } = opts;
46279
- 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 };
46280
46851
  }
46281
46852
  function cellDrawings(opt) {
46282
46853
  if (opt === false) return false;
@@ -46846,12 +47417,14 @@ var ChartCell = class {
46846
47417
  this.watermarkOn = visible;
46847
47418
  this.watermark?.setVisible(visible);
46848
47419
  this.deps.onStateDirty();
47420
+ this.deps.onStatusPrefsChanged(this.id);
46849
47421
  }
46850
47422
  /** Show/hide the "Replay" line under this cell's watermark while it replays (persisted per cell). */
46851
47423
  setReplayWatermarkVisible(visible) {
46852
47424
  this.replayWatermarkOn = visible;
46853
47425
  this.watermark?.setReplayVisible(visible);
46854
47426
  this.deps.onStateDirty();
47427
+ this.deps.onStatusPrefsChanged(this.id);
46855
47428
  }
46856
47429
  /** Show/hide this cell's indicator titles — the in-chart legend rows (persisted per cell). */
46857
47430
  setIndicatorTitlesVisible(visible) {
@@ -46872,18 +47445,20 @@ var ChartCell = class {
46872
47445
  this.statusline?.setPartVisible(part, visible);
46873
47446
  this.deps.onStatusPrefsChanged(this.id);
46874
47447
  }
46875
- /** 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}). */
46876
47449
  statusPrefs() {
46877
47450
  const sl = this.statusline;
46878
47451
  return {
46879
47452
  parts: sl ? { logo: sl.partVisible("logo"), name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
46880
47453
  indicatorTitles: this.indicatorTitlesOn,
46881
- indicatorValues: this.indicatorValuesOn
47454
+ indicatorValues: this.indicatorValuesOn,
47455
+ watermark: this.watermarkOn,
47456
+ replayWatermark: this.replayWatermarkOn
46882
47457
  };
46883
47458
  }
46884
- /** Converge this cell's Status line tab prefs to `prefs` — the follower half of
46885
- * the workspace's style link. Idempotent: matching values change nothing, so a
46886
- * 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. */
46887
47462
  applyStatusPrefs(prefs) {
46888
47463
  if (prefs.parts && this.statusline) {
46889
47464
  for (const part of Object.keys(prefs.parts)) {
@@ -46892,6 +47467,8 @@ var ChartCell = class {
46892
47467
  }
46893
47468
  if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
46894
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);
46895
47472
  }
46896
47473
  /** The LIVE chart of this cell — never cache it across a layout change (the cell's
46897
47474
  * identity is what endures; the chart dies with the cell). */
@@ -47802,6 +48379,55 @@ function occupancyGrid(def) {
47802
48379
  const cols = def.cols.length;
47803
48380
  return def.rows.map((_, r) => def.cols.map((_2, c) => def.cells[r * cols + c]?.id ?? `\xB7${r}x${c}`));
47804
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
+ }
47805
48431
  function gridStyles(def, trackSizes) {
47806
48432
  const cols = trackSizes?.cols?.length === def.cols.length ? trackSizes.cols : def.cols;
47807
48433
  const rows = trackSizes?.rows?.length === def.rows.length ? trackSizes.rows : def.rows;
@@ -48333,7 +48959,8 @@ var VelaWorkspace = class {
48333
48959
  onToggleSync: (id) => {
48334
48960
  const kind = id;
48335
48961
  this.sync.set(kind, this.syncOpts[kind] ? false : true);
48336
- }
48962
+ },
48963
+ glyph: () => layoutGlyph(this.def)
48337
48964
  },
48338
48965
  getContext: () => this.context()
48339
48966
  });
@@ -48832,6 +49459,7 @@ var VelaWorkspace = class {
48832
49459
  this.applyGrid();
48833
49460
  this.buildCells();
48834
49461
  this.alignNewCellStyles(preexisting);
49462
+ this.alignNewCellDrawings(preexisting);
48835
49463
  this.syncCellPresentation();
48836
49464
  this.refreshCellControls();
48837
49465
  this.topbar.setLayout(next.id);
@@ -49248,6 +49876,7 @@ var VelaWorkspace = class {
49248
49876
  const chart = cell.chart;
49249
49877
  chart.on("indicator:error", ({ error }) => this.toastHost.show(`[${cell.id}] ${error.message}`, "error", 5e3));
49250
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 }));
49251
49880
  chart.on("alert", (alert) => {
49252
49881
  const source = [parseSymbol(cell.symbol).ticker || cell.symbol, timeframeLabel(cell.timeframe), alert.indicator].filter(Boolean).join(" ");
49253
49882
  this.alerts.unshift({ cellId: cell.id, source, title: alert.title ?? "Alert", message: alert.message, time: alert.time });
@@ -49365,12 +49994,66 @@ var VelaWorkspace = class {
49365
49994
  }
49366
49995
  }
49367
49996
  /**
49368
- * Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
49369
- * its renderer config plus its Status line tab prefs — onto its same-group
49370
- * followers (the style link). Loop-safe two ways: the busy guard eats the
49371
- * followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
49372
- * in the same tick), and the equality short-circuits leave already-converged
49373
- * 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.
49374
50057
  */
49375
50058
  propagateStylePrefs(originId) {
49376
50059
  if (this.styleSyncBusy || this.destroyed) return;
@@ -49378,17 +50061,15 @@ var VelaWorkspace = class {
49378
50061
  if (targets.length === 0) return;
49379
50062
  const origin = this.cellsById.get(originId);
49380
50063
  if (!origin) return;
49381
- const slice = styleConfigSlice(origin.chart.renderer.getConfig());
49382
- const sliceJson = slice ? JSON.stringify(slice) : null;
50064
+ const config = origin.chart.renderer.getConfig();
49383
50065
  const prefs = origin.statusPrefs();
49384
50066
  this.styleSyncBusy = true;
49385
50067
  try {
49386
50068
  for (const id of targets) {
49387
50069
  const cell = this.cellsById.get(id);
49388
50070
  if (!cell) continue;
49389
- if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
49390
- cell.chart.renderer.applyConfig(slice);
49391
- }
50071
+ const patch = styleConfigPatch(config, cell.chart.renderer.getConfig());
50072
+ if (patch) cell.chart.renderer.applyConfig(patch);
49392
50073
  cell.applyStatusPrefs(prefs);
49393
50074
  }
49394
50075
  } finally {
@@ -49709,7 +50390,8 @@ var VelaWorkspace = class {
49709
50390
  onToggleSync: (id) => {
49710
50391
  const kind = id;
49711
50392
  this.sync.set(kind, this.syncOpts[kind] ? false : true);
49712
- }
50393
+ },
50394
+ glyph: () => layoutGlyph(this.def)
49713
50395
  },
49714
50396
  onOpenChange: (open2) => this.trackDialog(open2)
49715
50397
  }));