@luxalgo/vela 0.8.1 → 0.8.2

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/README.md +1 -1
  2. package/dist/{DataProvider-BrlxntPN.d.cts → DataProvider-COGDs9Xg.d.cts} +1 -1
  3. package/dist/{DataProvider-DwFIoV4r.d.ts → DataProvider-DpOWZD8F.d.ts} +1 -1
  4. package/dist/{chunk-LMBYEGUR.js → chunk-27UTYE7T.js} +25 -2
  5. package/dist/{chunk-EZ5FWVLA.js → chunk-DPAA3SRT.js} +94 -13
  6. package/dist/{chunk-BZQM2XO7.js → chunk-HKNO24GD.js} +13 -1
  7. package/dist/{chunk-NELQJCGK.js → chunk-NLDTVFOI.js} +41 -3
  8. package/dist/{chunk-DBDXKZUF.js → chunk-R2JMNIHY.js} +53 -13
  9. package/dist/{chunk-ATGTCWHJ.js → chunk-T3DZIR5Y.js} +171 -32
  10. package/dist/{contributions-tjNExo1o.d.ts → contributions-Cgm9RJYP.d.cts} +38 -11
  11. package/dist/{contributions-CtlptxR8.d.cts → contributions-bWgwyiVi.d.ts} +38 -11
  12. package/dist/index.cjs +239 -37
  13. package/dist/index.d.cts +8 -8
  14. package/dist/index.d.ts +8 -8
  15. package/dist/index.js +4 -4
  16. package/dist/{options-BnEPUuVV.d.ts → options-NAUHiQEC.d.cts} +3 -3
  17. package/dist/{options-BnEPUuVV.d.cts → options-NAUHiQEC.d.ts} +3 -3
  18. package/dist/{plugin-BWxJRKzS.d.cts → plugin-CjjzZX2F.d.cts} +4 -4
  19. package/dist/{plugin-e2WUEtiI.d.ts → plugin-CqxWoDKS.d.ts} +4 -4
  20. package/dist/plugin.cjs +20 -6
  21. package/dist/plugin.d.cts +5 -5
  22. package/dist/plugin.d.ts +5 -5
  23. package/dist/plugin.js +2 -2
  24. package/dist/providers/binance.d.cts +2 -2
  25. package/dist/providers/binance.d.ts +2 -2
  26. package/dist/providers/coinbase.d.cts +2 -2
  27. package/dist/providers/coinbase.d.ts +2 -2
  28. package/dist/providers/hyperliquid.d.cts +2 -2
  29. package/dist/providers/hyperliquid.d.ts +2 -2
  30. package/dist/{side-panel-HF0IAzwf.d.cts → side-panel-wMNfgyZb.d.cts} +13 -0
  31. package/dist/{side-panel-HF0IAzwf.d.ts → side-panel-wMNfgyZb.d.ts} +13 -0
  32. package/dist/{statusline-model-zKXtGIjj.d.ts → statusline-model-Dih6FGWP.d.ts} +3 -3
  33. package/dist/{statusline-model-DFY7tuHj.d.cts → statusline-model-Dpfpq54A.d.cts} +3 -3
  34. package/dist/ui.cjs +77 -2
  35. package/dist/ui.d.cts +28 -2
  36. package/dist/ui.d.ts +28 -2
  37. package/dist/ui.js +3 -3
  38. package/dist/vela.global.js +239 -37
  39. package/dist/vela.global.min.js +52 -52
  40. package/dist/widget.cjs +383 -50
  41. package/dist/widget.d.cts +20 -10
  42. package/dist/widget.d.ts +20 -10
  43. package/dist/widget.js +7 -7
  44. package/dist/workspace.cjs +383 -50
  45. package/dist/workspace.d.cts +5 -5
  46. package/dist/workspace.d.ts +5 -5
  47. package/dist/workspace.js +6 -6
  48. package/package.json +2 -2
package/dist/widget.cjs CHANGED
@@ -1256,6 +1256,7 @@ registerIcon("expand", S('<rect x="2.2" y="2.2" width="11.6" height="11.6" rx="1
1256
1256
  registerIcon("plus", S('<path d="M8 2.8v10.4M2.8 8h10.4"/>'));
1257
1257
  registerIcon("minus", S('<path d="M2.8 8h10.4"/>'));
1258
1258
  registerIcon("maximize", S('<path d="M2.5 6V3a.5.5 0 0 1 .5-.5h3M10 2.5h3a.5.5 0 0 1 .5.5v3M13.5 10v3a.5.5 0 0 1-.5.5h-3M6 13.5H3a.5.5 0 0 1-.5-.5v-3"/>'));
1259
+ registerIcon("restore", S('<path d="M6 2.5v3a.5.5 0 0 1-.5.5h-3M13.5 6h-3a.5.5 0 0 1-.5-.5v-3M10 13.5v-3a.5.5 0 0 1 .5-.5h3M2.5 10h3a.5.5 0 0 1 .5.5v3"/>'));
1259
1260
  registerIcon("restore", S('<path d="M6.2 2.5v3.7H2.5M9.8 13.5V9.8h3.7M13.5 6.2H9.8V2.5M2.5 9.8h3.7v3.7"/>'));
1260
1261
  registerIcon("star", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l-3.5 1.85.65-3.9-2.8-2.75 3.9-.55z"/>'));
1261
1262
  registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l-3.5 1.85.65-3.9-2.8-2.75 3.9-.55z"/>', 'fill="currentColor"'));
@@ -1337,6 +1338,17 @@ function nextUid(prefix) {
1337
1338
  return `${prefix}-${++uid}`;
1338
1339
  }
1339
1340
 
1341
+ // src/ui/surface-events.ts
1342
+ var SURFACE_OPEN_EVENT = "vela:surface-open";
1343
+ var SURFACE_CLOSE_EVENT = "vela:surface-close";
1344
+ function announceSurface(el, open2, kind, trigger = null) {
1345
+ const View = el.ownerDocument.defaultView;
1346
+ if (!View) return;
1347
+ el.dispatchEvent(
1348
+ new View.CustomEvent(open2 ? SURFACE_OPEN_EVENT : SURFACE_CLOSE_EVENT, { bubbles: true, composed: true, detail: { kind, trigger } })
1349
+ );
1350
+ }
1351
+
1340
1352
  // src/ui/keymap.ts
1341
1353
  var KEY_ALIASES = {
1342
1354
  esc: "escape",
@@ -1698,6 +1710,8 @@ var Surface = class _Surface {
1698
1710
  /** Trigger rect captured when this level opened — the list stays put if the
1699
1711
  * trigger then moves (favorite chips shifting the caret). */
1700
1712
  this.pinnedAnchor = null;
1713
+ /** The open state last announced — see {@link announce}. */
1714
+ this.shown = false;
1701
1715
  this.doc = doc;
1702
1716
  this.host = opts.host;
1703
1717
  this.onSelect = opts.onSelect;
@@ -1711,6 +1725,7 @@ var Surface = class _Surface {
1711
1725
  this.positioner.appendChild(this.list);
1712
1726
  this.host.appendChild(this.positioner);
1713
1727
  const trigger = opts.trigger;
1728
+ this.opener = trigger ?? opts.opener ?? null;
1714
1729
  this.ctrl = menuController({
1715
1730
  items: [],
1716
1731
  id: opts.id,
@@ -1732,15 +1747,21 @@ var Surface = class _Surface {
1732
1747
  if (opts.triggerId && trigger) trigger.id = opts.triggerId;
1733
1748
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
1734
1749
  const api = this.ctrl.connect(service);
1750
+ if (this.shown && !api.open) this.announce(false);
1735
1751
  if (trigger) vanilla.spreadProps(trigger, api.getTriggerProps(), this.mid);
1736
1752
  vanilla.spreadProps(this.positioner, api.getPositionerProps(), this.mid);
1737
1753
  vanilla.spreadProps(this.list, api.getContentProps(), this.mid);
1738
1754
  this.project(api);
1755
+ if (api.open && !this.shown) this.announce(true);
1739
1756
  });
1740
1757
  }
1741
1758
  get api() {
1742
1759
  return this.ctrl.connect(this.handle.service);
1743
1760
  }
1761
+ announce(open2) {
1762
+ this.shown = open2;
1763
+ announceSurface(this.list, open2, "menu", this.opener);
1764
+ }
1744
1765
  setItems(items) {
1745
1766
  this.items = items;
1746
1767
  for (const sub2 of this.subs.values()) sub2.destroy();
@@ -1758,6 +1779,7 @@ var Surface = class _Surface {
1758
1779
  for (const sub2 of this.subs.values()) sub2.destroy();
1759
1780
  this.subs.clear();
1760
1781
  this.handle.stop();
1782
+ if (this.shown) this.announce(false);
1761
1783
  this.positioner.remove();
1762
1784
  }
1763
1785
  byId(id) {
@@ -1851,6 +1873,7 @@ var Surface = class _Surface {
1851
1873
  const sub2 = new _Surface(doc, {
1852
1874
  host: this.host,
1853
1875
  placement: "right-start",
1876
+ opener: li,
1854
1877
  onSelect: this.onSelect,
1855
1878
  onFavorite: this.onFavorite,
1856
1879
  checkmarks: this.checkmarks,
@@ -2070,16 +2093,21 @@ var Popover = class {
2070
2093
  this.hide();
2071
2094
  };
2072
2095
  const onReflow = () => this.place();
2073
- setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
2096
+ setTimeout(() => {
2097
+ if (this.onOutside === onOutside) document.addEventListener("pointerdown", onOutside, true);
2098
+ }, 0);
2074
2099
  document.addEventListener("keydown", onKey, true);
2075
2100
  window.addEventListener("resize", onReflow, true);
2076
2101
  document.addEventListener("scroll", onReflow, true);
2077
2102
  this.onOutside = onOutside;
2078
2103
  this.onKey = onKey;
2079
2104
  this.onReflow = onReflow;
2105
+ announceSurface(this.el, true, "popover", this.trigger);
2080
2106
  }
2081
2107
  hide() {
2082
2108
  if (!this.shown) return;
2109
+ this.shown = false;
2110
+ announceSurface(this.el, false, "popover", this.trigger);
2083
2111
  if (this.onOutside) document.removeEventListener("pointerdown", this.onOutside, true);
2084
2112
  if (this.onKey) document.removeEventListener("keydown", this.onKey, true);
2085
2113
  if (this.onReflow) {
@@ -2099,7 +2127,6 @@ var Popover = class {
2099
2127
  } else {
2100
2128
  this.el.remove();
2101
2129
  }
2102
- this.shown = false;
2103
2130
  if (open === this) open = null;
2104
2131
  this.ctrl.onClose?.();
2105
2132
  }
@@ -2305,6 +2332,8 @@ ${overlayScrollbarCss(".vela-dialog *")}
2305
2332
  `;
2306
2333
  var Dialog = class {
2307
2334
  constructor(opts = {}) {
2335
+ this.opener = null;
2336
+ this.wasOpen = false;
2308
2337
  const doc = (opts.host ?? document.body).ownerDocument;
2309
2338
  injectStyles(DIALOG_STYLE_ID, DIALOG_CSS, doc);
2310
2339
  const host = opts.host ?? doc.body;
@@ -2401,6 +2430,7 @@ var Dialog = class {
2401
2430
  const mid = String(this.ctrl.props.id);
2402
2431
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
2403
2432
  const api = this.ctrl.connect(service);
2433
+ if (this.wasOpen && !api.open) this.announce(false);
2404
2434
  vanilla.spreadProps(this.backdrop, api.getBackdropProps(), mid);
2405
2435
  vanilla.spreadProps(this.positioner, api.getPositionerProps(), mid);
2406
2436
  vanilla.spreadProps(this.panel, api.getContentProps(), mid);
@@ -2408,12 +2438,27 @@ var Dialog = class {
2408
2438
  vanilla.spreadProps(close, api.getCloseTriggerProps(), mid);
2409
2439
  this.backdrop.style.display = api.open ? "" : "none";
2410
2440
  this.positioner.style.display = api.open ? "" : "none";
2441
+ if (api.open && !this.wasOpen) this.announce(true);
2442
+ if (this.wasOpen && !api.open) this.restoreOpener();
2443
+ this.wasOpen = api.open;
2411
2444
  });
2412
2445
  }
2446
+ /** Closing hands focus back to whatever opened it (Escape, the ✕, hide(), or a
2447
+ * teardown that outruns the machine's close notification). */
2448
+ restoreOpener() {
2449
+ const opener = this.opener;
2450
+ this.opener = null;
2451
+ if (opener?.isConnected) opener.focus({ preventScroll: true });
2452
+ }
2453
+ announce(open2) {
2454
+ const opener = this.opener === this.panel.ownerDocument.body ? null : this.opener;
2455
+ announceSurface(this.panel, open2, "dialog", opener);
2456
+ }
2413
2457
  get open() {
2414
2458
  return this.ctrl.connect(this.handle.service).open;
2415
2459
  }
2416
2460
  show() {
2461
+ this.opener = this.panel.ownerDocument.activeElement;
2417
2462
  this.ctrl.connect(this.handle.service).setOpen(true);
2418
2463
  }
2419
2464
  hide() {
@@ -2428,6 +2473,11 @@ var Dialog = class {
2428
2473
  destroy() {
2429
2474
  if (this.open) this.hide();
2430
2475
  this.handle.stop();
2476
+ if (this.wasOpen) {
2477
+ this.wasOpen = false;
2478
+ this.announce(false);
2479
+ }
2480
+ this.restoreOpener();
2431
2481
  this.backdrop.remove();
2432
2482
  this.positioner.remove();
2433
2483
  }
@@ -2556,6 +2606,8 @@ function swipeDirection(dx, dy) {
2556
2606
  }
2557
2607
  var Drawer = class {
2558
2608
  constructor(opts = {}) {
2609
+ this.opener = null;
2610
+ this.wasOpen = false;
2559
2611
  const doc = (opts.host ?? document.body).ownerDocument;
2560
2612
  injectStyles(DRAWER_STYLE_ID, DRAWER_CSS, doc);
2561
2613
  const host = opts.host ?? doc.body;
@@ -2583,12 +2635,16 @@ var Drawer = class {
2583
2635
  const mid = String(this.ctrl.props.id);
2584
2636
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
2585
2637
  const api = this.ctrl.connect(service);
2638
+ if (this.wasOpen && !api.open) this.announce(false);
2586
2639
  vanilla.spreadProps(this.backdrop, api.getBackdropProps(), mid);
2587
2640
  vanilla.spreadProps(this.positioner, api.getPositionerProps(), mid);
2588
2641
  vanilla.spreadProps(this.panel, api.getContentProps(), mid);
2589
2642
  vanilla.spreadProps(this.titleEl, api.getTitleProps(), mid);
2590
2643
  this.backdrop.style.display = api.open ? "" : "none";
2591
2644
  this.positioner.style.display = api.open ? "" : "none";
2645
+ if (api.open && !this.wasOpen) this.announce(true);
2646
+ if (this.wasOpen && !api.open) this.restoreOpener();
2647
+ this.wasOpen = api.open;
2592
2648
  });
2593
2649
  }
2594
2650
  /** Any element between `from` and the panel that has already been scrolled down —
@@ -2697,10 +2753,22 @@ var Drawer = class {
2697
2753
  setTitle(title) {
2698
2754
  this.titleEl.textContent = title;
2699
2755
  }
2756
+ /** Closing hands focus back to whatever opened it (Escape, the ✕, hide(), or a
2757
+ * teardown that outruns the machine's close notification). */
2758
+ restoreOpener() {
2759
+ const opener = this.opener;
2760
+ this.opener = null;
2761
+ if (opener?.isConnected) opener.focus({ preventScroll: true });
2762
+ }
2763
+ announce(open2) {
2764
+ const opener = this.opener === this.panel.ownerDocument.body ? null : this.opener;
2765
+ announceSurface(this.panel, open2, "drawer", opener);
2766
+ }
2700
2767
  get open() {
2701
2768
  return this.ctrl.connect(this.handle.service).open;
2702
2769
  }
2703
2770
  show() {
2771
+ this.opener = this.panel.ownerDocument.activeElement;
2704
2772
  this.ctrl.connect(this.handle.service).setOpen(true);
2705
2773
  }
2706
2774
  hide() {
@@ -2708,6 +2776,11 @@ var Drawer = class {
2708
2776
  }
2709
2777
  destroy() {
2710
2778
  this.handle.stop();
2779
+ if (this.wasOpen) {
2780
+ this.wasOpen = false;
2781
+ this.announce(false);
2782
+ }
2783
+ this.restoreOpener();
2711
2784
  this.backdrop.remove();
2712
2785
  this.positioner.remove();
2713
2786
  }
@@ -4998,6 +5071,7 @@ var LayoutPicker = class {
4998
5071
  const panel = doc.createElement("div");
4999
5072
  panel.className = "vela-lp";
5000
5073
  this.layer.appendChild(panel);
5074
+ this.panel = panel;
5001
5075
  const cols = doc.createElement("div");
5002
5076
  cols.className = "vela-lp-cols";
5003
5077
  panel.appendChild(cols);
@@ -5079,10 +5153,12 @@ var LayoutPicker = class {
5079
5153
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
5080
5154
  this.doc.addEventListener("keydown", this.onDocKeydown, true);
5081
5155
  this.opts.onOpenChange?.(true);
5156
+ if (this.isOpen) announceSurface(this.panel, true, "popover", this.opts.trigger);
5082
5157
  }
5083
5158
  close() {
5084
5159
  if (!this.isOpen) return;
5085
5160
  this.isOpen = false;
5161
+ announceSurface(this.panel, false, "popover", this.opts.trigger);
5086
5162
  this.layer.style.display = "none";
5087
5163
  this.opts.trigger.setAttribute("aria-expanded", "false");
5088
5164
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
@@ -5323,10 +5399,12 @@ function legendActions() {
5323
5399
  }
5324
5400
  function legendActionsProviderFor(chart, context) {
5325
5401
  return (indicatorId) => {
5326
- const handle = chart.indicators().find((h) => h.id === indicatorId);
5402
+ const find = () => chart.indicators().find((h) => h.id === indicatorId);
5403
+ const infoOf = (h) => ({ id: h.id, title: h.title, ...h.source !== void 0 ? { source: h.source } : {} });
5404
+ const handle = find();
5327
5405
  if (!handle) return [];
5328
- const info = { id: handle.id, title: handle.title, ...handle.source !== void 0 ? { source: handle.source } : {} };
5329
- return legendActions().filter((d) => !d.when || d.when(info)).map((d) => ({ id: d.id, icon: d.icon, tooltip: d.tooltip, run: () => d.run(context(), info) }));
5406
+ const info = infoOf(handle);
5407
+ return legendActions().filter((d) => !d.when || d.when(info)).map((d) => ({ id: d.id, icon: d.icon, tooltip: d.tooltip, run: () => d.run(context(), infoOf(find() ?? handle)) }));
5330
5408
  };
5331
5409
  }
5332
5410
  var calloutRegistry = /* @__PURE__ */ new Map();
@@ -9543,6 +9621,10 @@ var FibRatios = class extends Drawing {
9543
9621
 
9544
9622
  // src/core/drawings/types/FibLevels.ts
9545
9623
  var FibLevels = class extends FibRatios {
9624
+ /** Price of a level for the two anchor prices; subclasses override to change which anchor ratio 0 sits on. */
9625
+ levelPrice(ratio, p1, p2) {
9626
+ return p1 + ratio * (p2 - p1);
9627
+ }
9546
9628
  /** Per-level pixel line + price for the ENABLED levels, spanning the anchors' time range. */
9547
9629
  levelLines(proj) {
9548
9630
  const a = this.anchors[0];
@@ -9552,11 +9634,10 @@ var FibLevels = class extends FibRatios {
9552
9634
  const xb = proj.xOf(b.time);
9553
9635
  const x1 = Math.min(xa, xb);
9554
9636
  const x2 = Math.max(xa, xb);
9555
- const delta = b.price - a.price;
9556
9637
  const out = [];
9557
9638
  for (const lv of this.levels) {
9558
9639
  if (!lv.enabled) continue;
9559
- const price = a.price + lv.ratio * delta;
9640
+ const price = this.levelPrice(lv.ratio, a.price, b.price);
9560
9641
  const y = proj.yOf(price, this.paneId);
9561
9642
  if (y == null) continue;
9562
9643
  out.push({ ratio: lv.ratio, color: lv.color, label: lv.label, price, x1, x2, y });
@@ -9596,8 +9677,7 @@ var FibLevels = class extends FibRatios {
9596
9677
  const a = this.anchors[0];
9597
9678
  const b = this.anchors[1];
9598
9679
  if (!a || !b) return null;
9599
- const delta = b.price - a.price;
9600
- const prices = this.levels.filter((l) => l.enabled).map((l) => a.price + l.ratio * delta);
9680
+ const prices = this.levels.filter((l) => l.enabled).map((l) => this.levelPrice(l.ratio, a.price, b.price));
9601
9681
  if (prices.length === 0) return null;
9602
9682
  return { min: Math.min(...prices), max: Math.max(...prices) };
9603
9683
  }
@@ -9683,13 +9763,24 @@ function cycleLevels(count, enabledCount = count) {
9683
9763
  // src/core/drawings/types/FibRetracement.ts
9684
9764
  var LEVELS = fibLevels([0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]);
9685
9765
  var FibRetracement = class extends FibLevels {
9686
- constructor() {
9687
- super(...arguments);
9766
+ constructor(init) {
9767
+ super(init);
9688
9768
  this.type = "fibretracement";
9769
+ if (this.reverse === void 0) this.reverse = false;
9689
9770
  }
9690
9771
  defaultLevels() {
9691
9772
  return LEVELS;
9692
9773
  }
9774
+ levelPrice(ratio, p1, p2) {
9775
+ return this.reverse ? p1 + ratio * (p2 - p1) : p2 + ratio * (p1 - p2);
9776
+ }
9777
+ writeProps() {
9778
+ return { ...super.writeProps(), reverse: this.reverse };
9779
+ }
9780
+ readProps(props) {
9781
+ super.readProps(props);
9782
+ this.reverse = typeof props.reverse === "boolean" ? props.reverse : true;
9783
+ }
9693
9784
  };
9694
9785
 
9695
9786
  // src/core/drawings/types/FibExtension.ts
@@ -12816,6 +12907,32 @@ var CSS5 = `
12816
12907
  .vela-panel-pin .vela-icon { width: 15px; height: 15px; }
12817
12908
  .vela-panel-pin:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
12818
12909
  .vela-panel-pin[data-on='1'] { color: var(--vela-fg-bright); }
12910
+ .vela-panel-max {
12911
+ all: unset;
12912
+ cursor: pointer;
12913
+ width: 26px;
12914
+ height: 26px;
12915
+ flex: none;
12916
+ display: inline-flex;
12917
+ align-items: center;
12918
+ justify-content: center;
12919
+ border-radius: 4px;
12920
+ color: var(--vela-fg-muted);
12921
+ }
12922
+ .vela-panel-max .vela-icon { width: 15px; height: 15px; }
12923
+ .vela-panel-max:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
12924
+ /* MAXIMIZED: the panel covers the whole dock host \u2014 every chart of the shell \u2014 whatever
12925
+ its placement; the width handle and the pin have nothing to do meanwhile. */
12926
+ .vela-panel[data-maximized] {
12927
+ position: absolute;
12928
+ inset: 0;
12929
+ width: auto;
12930
+ max-width: none;
12931
+ z-index: 26;
12932
+ border-left: none;
12933
+ }
12934
+ .vela-panel[data-maximized] .vela-panel-resizer,
12935
+ .vela-panel[data-maximized] .vela-panel-pin { display: none; }
12819
12936
  .vela-panel-body { flex: 1; overflow: auto; padding: 8px; }
12820
12937
  .vela-panel-body::-webkit-scrollbar { width: 8px; }
12821
12938
  .vela-panel-body::-webkit-scrollbar-thumb { background: var(--vela-scroll); border-radius: 4px; border: 2px solid transparent; background-clip: padding-box; }
@@ -12868,6 +12985,7 @@ var CSS5 = `
12868
12985
  border-left: none;
12869
12986
  }
12870
12987
  [data-layout='mobile'] .vela-panel-resizer { display: none; }
12988
+ [data-layout='mobile'] .vela-panel-max { display: none; }
12871
12989
  `;
12872
12990
  function clampPanelWidth(px, min = DEFAULT_PANEL_MIN_WIDTH, max = DEFAULT_PANEL_MAX_WIDTH) {
12873
12991
  const lo = Number.isFinite(min) && min > 0 ? min : DEFAULT_PANEL_MIN_WIDTH;
@@ -12891,6 +13009,10 @@ var SidePanel = class {
12891
13009
  * never on a programmatic {@link setOverlay}, for the same reason as widths. */
12892
13010
  this.onPlacementChange = null;
12893
13011
  this.pin = null;
13012
+ this.maxButton = null;
13013
+ this.maximizedOn = false;
13014
+ /** Set while the close is announced, so a listener's own close does not re-enter. */
13015
+ this.closing = false;
12894
13016
  const doc = host.ownerDocument;
12895
13017
  injectStyles(STYLE_ID4, CSS5, doc);
12896
13018
  this.minWidth = opts.resizable ? opts.minWidth ?? DEFAULT_PANEL_MIN_WIDTH : 1;
@@ -12925,7 +13047,13 @@ var SidePanel = class {
12925
13047
  });
12926
13048
  this.refreshPin();
12927
13049
  }
12928
- header.append(this.heading, this.slot, ...this.pin ? [this.pin] : [], close);
13050
+ if (opts.maximizable) {
13051
+ this.maxButton = doc.createElement("button");
13052
+ this.maxButton.className = "vela-panel-max";
13053
+ this.maxButton.addEventListener("click", () => this.setMaximized(!this.maximizedOn));
13054
+ this.refreshMaxButton();
13055
+ }
13056
+ header.append(this.heading, this.slot, ...this.pin ? [this.pin] : [], ...this.maxButton ? [this.maxButton] : [], close);
12929
13057
  this.body = doc.createElement("div");
12930
13058
  this.body.className = "vela-panel-body";
12931
13059
  this.el.append(header, this.body);
@@ -12937,9 +13065,37 @@ var SidePanel = class {
12937
13065
  }
12938
13066
  /** Open/close the panel — a bare call flips it. */
12939
13067
  toggle(open2 = this.el.hidden) {
12940
- if (open2 === !this.el.hidden) return;
13068
+ if (open2 === !this.el.hidden || !open2 && this.closing) return;
13069
+ if (!open2) {
13070
+ this.closing = true;
13071
+ announceSurface(this.el, false, "panel");
13072
+ this.closing = false;
13073
+ }
12941
13074
  this.el.hidden = !open2;
13075
+ if (!open2) this.setMaximized(false);
12942
13076
  this.onOpenChange?.(open2);
13077
+ if (open2 && !this.el.hidden) announceSurface(this.el, true, "panel");
13078
+ }
13079
+ /** Whether the panel covers every chart right now. */
13080
+ get maximized() {
13081
+ return this.maximizedOn;
13082
+ }
13083
+ /** Maximize the panel over the shell's charts, or restore it. A no-op on a panel not
13084
+ * declared `maximizable`. */
13085
+ setMaximized(maximized) {
13086
+ if (!this.maxButton || maximized === this.maximizedOn) return;
13087
+ this.maximizedOn = maximized;
13088
+ if (maximized) this.el.dataset.maximized = "1";
13089
+ else delete this.el.dataset.maximized;
13090
+ this.refreshMaxButton();
13091
+ }
13092
+ refreshMaxButton() {
13093
+ if (!this.maxButton) return;
13094
+ const doc = this.maxButton.ownerDocument;
13095
+ this.maxButton.replaceChildren(iconEl(this.maximizedOn ? "restore" : "maximize", doc));
13096
+ this.maxButton.title = this.maximizedOn ? "Restore" : "Maximize";
13097
+ this.maxButton.setAttribute("aria-label", this.maxButton.title);
13098
+ this.maxButton.setAttribute("aria-pressed", this.maximizedOn ? "true" : "false");
12943
13099
  }
12944
13100
  /** The scrolling body, for a panel filled from OUTSIDE the class — a contributed panel's
12945
13101
  * `mount` receives exactly this element. Subclasses use the protected `body`. */
@@ -12991,6 +13147,7 @@ var SidePanel = class {
12991
13147
  this.pin.setAttribute("aria-pressed", pinned ? "true" : "false");
12992
13148
  }
12993
13149
  destroy() {
13150
+ if (this.open) announceSurface(this.el, false, "panel");
12994
13151
  this.el.remove();
12995
13152
  }
12996
13153
  /** The drag handle on the panel's inner (left) edge — the panel is docked right, so dragging
@@ -14407,19 +14564,22 @@ var PanelDock = class {
14407
14564
  /**
14408
14565
  * (Re)build the CONTRIBUTED panels from the registry — call once after the built-ins, and
14409
14566
  * again on `refreshActions()` so a late registration appears. Contributed panels that are
14410
- * gone from the registry are dropped; the ones still there are rebuilt, so a replaced
14411
- * descriptor takes effect.
14567
+ * gone from the registry are dropped and a REPLACED descriptor is rebuilt; a panel whose
14568
+ * descriptor is unchanged stays mounted as it is, so its content and state survive.
14412
14569
  */
14413
14570
  refresh() {
14414
14571
  const openBefore = this.openId;
14415
- for (const entry of [...this.entries]) if (entry.contributed) this.drop(entry);
14416
- for (const desc of sidePanels()) {
14572
+ const registered = sidePanels();
14573
+ for (const entry of [...this.entries]) if (entry.contributed && !registered.includes(entry.desc)) this.drop(entry);
14574
+ for (const desc of registered) {
14575
+ if (this.entries.some((e) => e.desc === desc)) continue;
14417
14576
  const panel = new SidePanel(this.host, desc.title, `vela-panel-${desc.id}`, {
14418
14577
  width: desc.width,
14419
14578
  resizable: desc.resizable,
14420
14579
  minWidth: desc.minWidth,
14421
14580
  maxWidth: desc.maxWidth,
14422
- overlay: desc.overlay
14581
+ overlay: desc.overlay,
14582
+ maximizable: desc.maximizable
14423
14583
  });
14424
14584
  const entry = {
14425
14585
  id: desc.id,
@@ -14427,10 +14587,19 @@ var PanelDock = class {
14427
14587
  icon: desc.icon,
14428
14588
  order: desc.order ?? DEFAULT_PANEL_ORDER,
14429
14589
  panel,
14430
- contributed: true
14590
+ contributed: true,
14591
+ desc
14431
14592
  };
14432
14593
  try {
14433
- entry.handle = desc.mount(this.deps.context(), panel.content, { slot: panel.headerSlot, setTitle: (t) => panel.setTitle(t) }) ?? void 0;
14594
+ const header = {
14595
+ slot: panel.headerSlot,
14596
+ setTitle: (t) => panel.setTitle(t),
14597
+ get maximized() {
14598
+ return panel.maximized;
14599
+ },
14600
+ setMaximized: (on) => panel.setMaximized(on)
14601
+ };
14602
+ entry.handle = desc.mount(this.deps.context(), panel.content, header) ?? void 0;
14434
14603
  if (this.chart) entry.handle?.onChart?.(this.chart);
14435
14604
  } catch (err) {
14436
14605
  console.warn(`[vela] side panel "${desc.id}" failed to mount`, err);
@@ -18201,6 +18370,7 @@ var DrawingToolbar = class {
18201
18370
  );
18202
18371
  }
18203
18372
  }
18373
+ announceSurface(fly, true, "menu", cell);
18204
18374
  }
18205
18375
  openMagnetFlyout() {
18206
18376
  const cell = this.magnetCell;
@@ -18224,6 +18394,7 @@ var DrawingToolbar = class {
18224
18394
  })
18225
18395
  );
18226
18396
  }
18397
+ announceSurface(fly, true, "menu", cell);
18227
18398
  }
18228
18399
  flyoutHeader(text) {
18229
18400
  const t = this.theme;
@@ -18292,9 +18463,11 @@ var DrawingToolbar = class {
18292
18463
  }
18293
18464
  closeFlyout() {
18294
18465
  this.starEls.clear();
18295
- if (this.flyout) {
18296
- this.flyout.remove();
18466
+ const fly = this.flyout;
18467
+ if (fly) {
18297
18468
  this.flyout = null;
18469
+ announceSurface(fly, false, "menu", this.flyoutCell);
18470
+ fly.remove();
18298
18471
  this.flyoutCell?.classList.remove("vela-open");
18299
18472
  this.flyoutCell = null;
18300
18473
  this.flyoutOwnerId = null;
@@ -18949,7 +19122,7 @@ var IndicatorHandleImpl = class {
18949
19122
  this.controller.applyProps(this.id, values);
18950
19123
  }
18951
19124
  updateCode(source) {
18952
- this.controller.updateCode(this.id, source);
19125
+ return this.controller.updateCode(this.id, source);
18953
19126
  }
18954
19127
  setVisible(visible) {
18955
19128
  this.controller.setVisible(this.id, visible);
@@ -20812,20 +20985,26 @@ var EngineOrchestrator = class _EngineOrchestrator {
20812
20985
  /**
20813
20986
  * IndicatorController: replace a SCRIPT indicator's source in place (see
20814
20987
  * {@link IndicatorHandle.updateCode}). Prepare-first: the running session keeps
20815
- * computing until the new source has compiled, so a broken edit costs the user
20816
- * nothing but an `error` event. Natives have no script — warn and leave them alone.
20988
+ * computing until the new source has compiled; then the new code runs on TRIAL until
20989
+ * its first model lands, and a failure before that puts the previous code back.
20990
+ * Natives have no script — warn and leave them alone.
20817
20991
  */
20818
20992
  updateCode(id, source) {
20819
20993
  const record = this.registry.get(id);
20820
20994
  const handle = this.handles.get(id);
20821
- if (!record || !handle) return;
20995
+ if (!record || !handle) return Promise.resolve({ ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator is no longer on the chart.`) });
20822
20996
  if (record.native) {
20823
20997
  console.warn(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`);
20824
- return;
20998
+ return Promise.resolve({ ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`) });
20825
20999
  }
20826
- if (source === record.source && record.pendingSource === void 0) return;
20827
- record.pendingSource = source;
20828
- void this.swapSource(id, source, handle);
21000
+ const busy = record.pendingSource !== void 0 || record.codeTrial !== void 0;
21001
+ if (!busy && source === record.source) return Promise.resolve({ ok: true, error: null });
21002
+ return new Promise((resolve) => {
21003
+ (record.codeWaiters ?? (record.codeWaiters = [])).push(resolve);
21004
+ if (record.pendingSource === source || record.pendingSource === void 0 && record.codeTrial && record.source === source) return;
21005
+ record.pendingSource = source;
21006
+ void this.swapSource(id, source, handle);
21007
+ });
20829
21008
  }
20830
21009
  async swapSource(id, source, handle) {
20831
21010
  const engine = this.registry.get(id)?.engine ?? this.engineFor(this.registry.get(id)?.options?.language);
@@ -20836,26 +21015,41 @@ var EngineOrchestrator = class _EngineOrchestrator {
20836
21015
  const record2 = this.registry.get(id);
20837
21016
  if (record2?.pendingSource === source) {
20838
21017
  record2.pendingSource = void 0;
20839
- this.fail(id, handle, err);
21018
+ this.rejectCode(id, record2, handle, err);
20840
21019
  }
20841
21020
  return;
20842
21021
  }
20843
21022
  const record = this.registry.get(id);
20844
21023
  if (!record || record.pendingSource !== source) return;
20845
21024
  record.pendingSource = void 0;
21025
+ const previous = record.codeTrial?.previous ?? {
21026
+ source: record.source,
21027
+ engine: record.engine,
21028
+ prepared: record.prepared,
21029
+ inputValues: record.inputValues,
21030
+ propValues: record.propValues,
21031
+ inputs: [...handle.inputs],
21032
+ props: [...handle.props]
21033
+ };
21034
+ record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues, record.prepared?.inputs);
21035
+ record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues, record.prepared?.props);
20846
21036
  record.source = source;
20847
21037
  record.engine = engine;
20848
21038
  record.prepared = prepared;
20849
21039
  handle.setSource(source);
20850
- record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues);
20851
- record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues);
20852
21040
  handle.setSchema(prepared.inputs);
20853
21041
  handle.setPropsSchema(prepared.props ?? []);
20854
21042
  record.session?.stop();
20855
21043
  record.session = void 0;
20856
21044
  record.pendingStructural = true;
20857
21045
  record.pendingCause = "code";
20858
- if (record.hidden) return;
21046
+ if (record.hidden) {
21047
+ record.codeTrial = void 0;
21048
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
21049
+ this.settleCode(record, { ok: true, error: null });
21050
+ return;
21051
+ }
21052
+ record.codeTrial = { previous };
20859
21053
  if (record.renderHandle) {
20860
21054
  this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
20861
21055
  this.setLoading(record, true);
@@ -20863,6 +21057,46 @@ var EngineOrchestrator = class _EngineOrchestrator {
20863
21057
  this.mountLoadingPlaceholder(id, record);
20864
21058
  }
20865
21059
  this.executeIndicator(id, handle);
21060
+ if (this.bars.length === 0 && record.codeTrial) {
21061
+ record.codeTrial = void 0;
21062
+ this.settleCode(record, { ok: true, error: null });
21063
+ }
21064
+ }
21065
+ /** Resolve every `updateCode` caller waiting on this record's chain of edits. */
21066
+ settleCode(record, result) {
21067
+ const waiters = record.codeWaiters;
21068
+ record.codeWaiters = void 0;
21069
+ if (waiters) for (const resolve of waiters) resolve(result);
21070
+ }
21071
+ /**
21072
+ * An update failed — at prepare, or on trial before its first model. A trial puts the
21073
+ * previous code back (source, engine, prepared script, values, schema) and restarts it;
21074
+ * its visuals were never dropped, so the chart keeps painting throughout. Then the
21075
+ * failure reports the usual way and every waiting caller learns it.
21076
+ */
21077
+ rejectCode(id, record, handle, err) {
21078
+ const trial = record.codeTrial;
21079
+ record.codeTrial = void 0;
21080
+ this.fail(id, handle, err);
21081
+ if (trial) {
21082
+ const p = trial.previous;
21083
+ record.session?.stop();
21084
+ record.session = void 0;
21085
+ record.source = p.source;
21086
+ record.engine = p.engine;
21087
+ record.prepared = p.prepared;
21088
+ record.inputValues = p.inputValues;
21089
+ record.propValues = p.propValues;
21090
+ handle.setSource(p.source);
21091
+ handle.setSchema(p.inputs);
21092
+ handle.setPropsSchema(p.props);
21093
+ record.pendingStructural = true;
21094
+ record.pendingCause = void 0;
21095
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
21096
+ if (!record.hidden) this.executeIndicator(id, handle);
21097
+ }
21098
+ const error = err instanceof Error ? err : new Error(String(err));
21099
+ this.settleCode(record, { ok: false, error });
20866
21100
  }
20867
21101
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
20868
21102
  /** Live handles of every indicator on the chart (script + native), insertion order. */
@@ -20877,6 +21111,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20877
21111
  removeIndicator(id) {
20878
21112
  const record = this.registry.remove(id);
20879
21113
  if (record?.native?.type === "volume") this.volumeOptedOut = true;
21114
+ if (record) this.settleCode(record, { ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator was removed.`) });
20880
21115
  record?.session?.stop();
20881
21116
  if (record?.native?.started) record.native.instance.stop();
20882
21117
  this.handles.delete(id);
@@ -20904,6 +21139,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
20904
21139
  if (!visible) {
20905
21140
  record.session?.stop();
20906
21141
  record.session = void 0;
21142
+ if (record.codeTrial) {
21143
+ record.codeTrial = void 0;
21144
+ this.settleCode(record, { ok: true, error: null });
21145
+ }
20907
21146
  if (record.native?.started) record.native.instance.suspend();
20908
21147
  if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
20909
21148
  else if (record.native) this.mountHiddenNativeRow(id, record);
@@ -20991,6 +21230,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20991
21230
  this.viewportUnsub?.();
20992
21231
  this.paneActionUnsub?.();
20993
21232
  for (const record of this.registry.all()) {
21233
+ this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
20994
21234
  record.session?.stop();
20995
21235
  if (record.native?.started) record.native.instance.stop();
20996
21236
  }
@@ -21059,7 +21299,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
21059
21299
  const record = this.registry.get(id);
21060
21300
  if (!record || !record.engine || !record.prepared || record.hidden) return;
21061
21301
  const mode = this.config.live && !record.prepared.reactsToViewport && record.engine.capabilities.streaming ? "live" : "static";
21062
- record.session = record.engine.execute(
21302
+ const prepared = record.prepared;
21303
+ const session = record.engine.execute(
21063
21304
  {
21064
21305
  prepared: record.prepared,
21065
21306
  market: this.market(),
@@ -21080,6 +21321,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
21080
21321
  const cause = record.pendingCause ?? "history";
21081
21322
  if (!this.applyModel(id, model)) return;
21082
21323
  record.pendingCause = void 0;
21324
+ if (record.codeTrial) {
21325
+ record.codeTrial = void 0;
21326
+ this.settleCode(record, { ok: true, error: null });
21327
+ }
21083
21328
  this.emitContextChanged(id);
21084
21329
  this.emitScriptRun(id, cause, first);
21085
21330
  },
@@ -21090,9 +21335,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
21090
21335
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
21091
21336
  },
21092
21337
  onWarning: (w) => this.events.emit("warning", w),
21093
- onError: (err) => this.fail(id, handle, err)
21338
+ onError: (err) => {
21339
+ if (record.codeTrial) this.rejectCode(id, record, handle, err);
21340
+ else this.fail(id, handle, err);
21341
+ }
21094
21342
  }
21095
21343
  );
21344
+ if (record.prepared !== prepared) {
21345
+ session.stop();
21346
+ return;
21347
+ }
21348
+ record.session = session;
21096
21349
  this.emitContextChanged(id);
21097
21350
  }
21098
21351
  /**
@@ -21593,7 +21846,7 @@ function yieldToPaint() {
21593
21846
  }
21594
21847
  return new Promise((resolve) => setTimeout(resolve, 0));
21595
21848
  }
21596
- function valuesOnSchema(schema, previous) {
21849
+ function valuesOnSchema(schema, previous, oldSchema = []) {
21597
21850
  const out = {};
21598
21851
  const declared = /* @__PURE__ */ new Set();
21599
21852
  for (const s of schema) {
@@ -21601,9 +21854,22 @@ function valuesOnSchema(schema, previous) {
21601
21854
  declared.add(s.key);
21602
21855
  declared.add(s.title);
21603
21856
  }
21604
- for (const [k, v] of Object.entries(previous)) if (declared.has(k)) out[k] = v;
21857
+ const oldDefaults = /* @__PURE__ */ new Map();
21858
+ for (const s of oldSchema) {
21859
+ oldDefaults.set(s.key, s.defval);
21860
+ if (!oldDefaults.has(s.title)) oldDefaults.set(s.title, s.defval);
21861
+ }
21862
+ for (const [k, v] of Object.entries(previous)) {
21863
+ if (!declared.has(k)) continue;
21864
+ if (oldDefaults.has(k) && sameInputValue(oldDefaults.get(k), v)) continue;
21865
+ out[k] = v;
21866
+ }
21605
21867
  return out;
21606
21868
  }
21869
+ function sameInputValue(a, b) {
21870
+ if (a === b) return true;
21871
+ return typeof a === "object" && a !== null && typeof b === "object" && b !== null && JSON.stringify(a) === JSON.stringify(b);
21872
+ }
21607
21873
  function blankedModel(model) {
21608
21874
  return {
21609
21875
  ...model,
@@ -24533,15 +24799,23 @@ var Batch = class {
24533
24799
  this.seg(x + w, y + h, x, y + h, lineWidth, c);
24534
24800
  this.seg(x, y + h, x, y, lineWidth, c);
24535
24801
  }
24536
- /** Filled circle (triangle fan). */
24802
+ /**
24803
+ * Filled circle (triangle fan), feathered like a line: the rim is padded by {@link AA_PAD} and
24804
+ * edgeDist runs from 0 at the center to the padded radius at the rim, with edgeHalf = r. The
24805
+ * distance grows linearly along each spoke, so the shader's line feather gives a round soft edge.
24806
+ */
24537
24807
  circle(cx, cy, r, c, segments = 16) {
24538
- let px = cx + r;
24808
+ const ext = r + AA_PAD;
24809
+ let px = cx + ext;
24539
24810
  let py = cy;
24811
+ this.ensure(segments * 3);
24540
24812
  for (let i = 1; i <= segments; i += 1) {
24541
24813
  const a = i / segments * Math.PI * 2;
24542
- const nx = cx + Math.cos(a) * r;
24543
- const ny = cy + Math.sin(a) * r;
24544
- this.tri(cx, cy, c, px, py, c, nx, ny, c);
24814
+ const nx = cx + Math.cos(a) * ext;
24815
+ const ny = cy + Math.sin(a) * ext;
24816
+ this.v(cx, cy, c, 0, r);
24817
+ this.v(px, py, c, ext, r);
24818
+ this.v(nx, ny, c, ext, r);
24545
24819
  px = nx;
24546
24820
  py = ny;
24547
24821
  }
@@ -24722,7 +24996,7 @@ var WebGL2Backend = class {
24722
24996
  }
24723
24997
  mount(canvas) {
24724
24998
  this.canvas = canvas;
24725
- this.gl = canvas.getContext("webgl2", { alpha: true, antialias: true, depth: false, stencil: false, premultipliedAlpha: true });
24999
+ this.gl = canvas.getContext("webgl2", { alpha: true, antialias: false, depth: false, stencil: false, premultipliedAlpha: true });
24726
25000
  if (!this.gl) return;
24727
25001
  if (!this.initGL()) {
24728
25002
  this.gl = null;
@@ -26174,7 +26448,7 @@ var InputController = class {
26174
26448
  this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
26175
26449
  } else if (tapRelease && this.region === "data") {
26176
26450
  this.deps.onClick(x, y);
26177
- } else if (this.dragging && this.region === "data") {
26451
+ } else if (this.dragging && this.region === "data" && wasTouch) {
26178
26452
  const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
26179
26453
  if (!stale && Math.abs(this.vx) > FLING_MIN_SPEED) {
26180
26454
  const pitch = this.deps.getCoords().pxPerBar();
@@ -32540,6 +32814,12 @@ var DrawingInteraction = class {
32540
32814
  activeDragId() {
32541
32815
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
32542
32816
  }
32817
+ /** The store drawings a live drag is moving — the pressed one and its riders — or none
32818
+ * before the drag passes the slop. A Ctrl-drag moves copies, so its sources stay put. */
32819
+ movingIds() {
32820
+ if (this.state.kind !== "pressed" || !this.state.moved || this.state.clones) return /* @__PURE__ */ new Set();
32821
+ return /* @__PURE__ */ new Set([this.state.id, ...this.state.riders.map((r) => r.id)]);
32822
+ }
32543
32823
  /** The drawing under a press that has not been released yet (drag or click undecided), or null. */
32544
32824
  pressedId() {
32545
32825
  return this.state.kind === "pressed" ? this.state.id : null;
@@ -33198,6 +33478,17 @@ var DrawingSettingsDialog = class {
33198
33478
  }
33199
33479
  }));
33200
33480
  }
33481
+ if (drawing instanceof FibRetracement) {
33482
+ const fib = drawing;
33483
+ grid.appendChild(fieldRow({
33484
+ label: "Reverse",
33485
+ bool: true,
33486
+ toggle: {
33487
+ checked: fib.reverse,
33488
+ onChange: (v) => actions.patch({ reverse: v })
33489
+ }
33490
+ }));
33491
+ }
33201
33492
  levels.forEach((lv, i) => {
33202
33493
  const row = document.createElement("div");
33203
33494
  row.className = "vela-field-span";
@@ -34351,6 +34642,8 @@ var UserDrawingController = class {
34351
34642
  this.drawings = [];
34352
34643
  /** Ids painted on an interleave layer this frame — the top canvas paints only their handles. */
34353
34644
  this.sliced = /* @__PURE__ */ new Set();
34645
+ /** Interleaved drawings mid-drag: painted on the top canvas, left out of their slice. */
34646
+ this.lifted = /* @__PURE__ */ new Set();
34354
34647
  /** Cached slice canvases, keyed `paneId|beforeZ`, reused across frames to avoid churn. */
34355
34648
  this.sliceCache = /* @__PURE__ */ new Map();
34356
34649
  /** Series boundaries per pane as of the last `prepareSlices` — lets a repaint between data
@@ -34438,7 +34731,7 @@ var UserDrawingController = class {
34438
34731
  selectedIds: () => this.selectedIds,
34439
34732
  emit: (i) => this.emit(i),
34440
34733
  changed: () => {
34441
- this.invalidateSlices();
34734
+ this.syncLift();
34442
34735
  this.render();
34443
34736
  },
34444
34737
  openSettings: (id, x, y) => this.openSettingsById(id, x, y),
@@ -35064,6 +35357,20 @@ var UserDrawingController = class {
35064
35357
  /** A drawing that paints inside the series stack changed (content, not hover): its pixels
35065
35358
  * live in the backend composite, so this layer alone can't show the change — ask for a
35066
35359
  * data frame, which re-runs `prepareSlices` before the backend composites. */
35360
+ /** A drawing being dragged leaves its interleave slice for the top canvas, so each pointer
35361
+ * move repaints one 2D layer instead of re-rasterizing and re-uploading a plot-sized slice
35362
+ * texture through the data frame. The slices rebuild once as the drag starts and once as it
35363
+ * ends (release, cancel), when the lifted set changes. */
35364
+ syncLift() {
35365
+ const moving = this.interaction.movingIds();
35366
+ let same = moving.size === this.lifted.size;
35367
+ if (same) {
35368
+ for (const id of moving) if (!this.lifted.has(id)) same = false;
35369
+ }
35370
+ if (same) return;
35371
+ this.lifted = new Set(moving);
35372
+ this.invalidateSlices();
35373
+ }
35067
35374
  invalidateSlices() {
35068
35375
  if (this.sliced.size > 0 || this.drawings.some((d) => this.isInterleaved(d))) this.deps.requestDataPaint();
35069
35376
  }
@@ -35082,7 +35389,7 @@ var UserDrawingController = class {
35082
35389
  const theme = this.deps.theme();
35083
35390
  const buckets = /* @__PURE__ */ new Map();
35084
35391
  for (const d of this.drawings) {
35085
- if (!d.visible) continue;
35392
+ if (!d.visible || this.lifted.has(d.id)) continue;
35086
35393
  const beforeZ = sliceKeyFor(d.zIndex, this.lastBounds.get(d.paneId) ?? []);
35087
35394
  if (beforeZ === null) continue;
35088
35395
  const key = `${d.paneId}|${beforeZ}`;
@@ -35134,8 +35441,9 @@ var UserDrawingController = class {
35134
35441
  mutedLabel: edited instanceof TextLabel ? edited.id : null
35135
35442
  };
35136
35443
  this.painter.seriesLook = this.deps.seriesLook();
35137
- this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
35138
- this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
35444
+ const onTop = (d) => !this.isInterleaved(d) || this.lifted.has(d.id);
35445
+ this.painter.paintAll(ctx, this.drawings.filter(onTop), proj, this.deps.theme(), targets);
35446
+ this.painter.paintHighlights(ctx, this.drawings.filter((d) => !onTop(d)), proj, handleIdsFor(targets));
35139
35447
  const clones = this.interaction.dragClones();
35140
35448
  if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
35141
35449
  this.layoutTextEditor();
@@ -44791,6 +45099,9 @@ var CSS21 = `
44791
45099
  .vela-statusline .vela-sl-market { align-self: center; display: inline-flex; }
44792
45100
  .vela-statusline .vela-sl-market > [hidden] { display: none !important; }
44793
45101
  .vela-statusline .vela-sl-replay-badge, .vela-statusline .vela-sl-replay-badge svg { display: block; width: 16px; height: 16px; }
45102
+ /* Tabular figures: every digit has one advance, so a readout whose text changes only in
45103
+ * its digits keeps its width \u2014 render() skips the ladder for those (see widthKey). */
45104
+ .vela-statusline .vela-sl-values { font-variant-numeric: tabular-nums; }
44794
45105
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
44795
45106
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
44796
45107
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
@@ -44908,6 +45219,10 @@ var Statusline = class {
44908
45219
  this.fitRO = null;
44909
45220
  /** The ladder rung currently applied — see {@link fit}. */
44910
45221
  this.layout = { stacked: false, level: "full" };
45222
+ /** The readout {@link widthKey} the applied rung was fitted to. */
45223
+ this.fitKey = null;
45224
+ /** Web fonts landing after the last fit change every width without a resize. */
45225
+ this.onFontsLoaded = () => this.fit();
44911
45226
  this.onContextMenu = (e) => {
44912
45227
  if (!this.menu || !this.menuHooks) return;
44913
45228
  e.preventDefault();
@@ -44971,6 +45286,7 @@ var Statusline = class {
44971
45286
  this.fitRO = new ResizeObserver(() => this.fit());
44972
45287
  this.fitRO.observe(host);
44973
45288
  }
45289
+ doc.fonts?.addEventListener("loadingdone", this.onFontsLoaded);
44974
45290
  this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
44975
45291
  this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
44976
45292
  this.setMarketStatus("open");
@@ -45023,6 +45339,7 @@ var Statusline = class {
45023
45339
  * (detached host, node tests) nothing overflows, so the widest rung stays.
45024
45340
  */
45025
45341
  fit() {
45342
+ this.fitKey = this.widthKey();
45026
45343
  this.syncParts();
45027
45344
  const seg = segmentVisibility(this.parts, this.chartHidden);
45028
45345
  const overflows = () => this.el.scrollWidth > this.el.clientWidth;
@@ -45179,6 +45496,7 @@ var Statusline = class {
45179
45496
  this.detach();
45180
45497
  this.fitRO?.disconnect();
45181
45498
  this.fitRO = null;
45499
+ this.el.ownerDocument.fonts?.removeEventListener("loadingdone", this.onFontsLoaded);
45182
45500
  this.el.removeEventListener("contextmenu", this.onContextMenu);
45183
45501
  this.menu?.destroy();
45184
45502
  this.menu = null;
@@ -45194,7 +45512,22 @@ var Statusline = class {
45194
45512
  }
45195
45513
  render() {
45196
45514
  if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
45197
- this.fit();
45515
+ if (this.widthKey() === this.fitKey) this.renderValues();
45516
+ else this.fit();
45517
+ }
45518
+ /** Everything the readout's width depends on, at every rung: its shape, and each
45519
+ * value's sign, digit count and decimals (the digits themselves are tabular — see
45520
+ * the CSS; the grouping commas follow from the digit count). `toFixed` carries all
45521
+ * three at a fraction of the locale formatter's cost — this runs per pointer move.
45522
+ * The parts, the host's size and the fonts re-fit through their own paths. */
45523
+ widthKey() {
45524
+ const bar = this.hoverBar ?? this.lastBar;
45525
+ if (!bar) return this.readout;
45526
+ const dp = decimalsFor(bar.close);
45527
+ const diff = bar.close - bar.open;
45528
+ const text = [bar.open, bar.high, bar.low, bar.close, diff].map((v) => v.toFixed(dp));
45529
+ text.push((diff / bar.open * 100).toFixed(2));
45530
+ return `${this.readout} ${text.join(" ")}`.replace(/\d/g, "0");
45198
45531
  }
45199
45532
  /** Write the value readout for the current bar at the current ladder level. */
45200
45533
  renderValues() {