@luxalgo/vela 0.8.0 → 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 (49) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +16 -8
  3. package/dist/{DataProvider-BrlxntPN.d.cts → DataProvider-COGDs9Xg.d.cts} +1 -1
  4. package/dist/{DataProvider-DwFIoV4r.d.ts → DataProvider-DpOWZD8F.d.ts} +1 -1
  5. package/dist/{chunk-KA77UTRF.js → chunk-27UTYE7T.js} +25 -2
  6. package/dist/{chunk-EZ5FWVLA.js → chunk-DPAA3SRT.js} +94 -13
  7. package/dist/{chunk-BZQM2XO7.js → chunk-HKNO24GD.js} +13 -1
  8. package/dist/{chunk-2IAETM2I.js → chunk-NLDTVFOI.js} +79 -6
  9. package/dist/{chunk-E3GU6AUI.js → chunk-R2JMNIHY.js} +68 -23
  10. package/dist/{chunk-QBPUT5SU.js → chunk-T3DZIR5Y.js} +211 -54
  11. package/dist/{contributions-tjNExo1o.d.ts → contributions-Cgm9RJYP.d.cts} +38 -11
  12. package/dist/{contributions-CtlptxR8.d.cts → contributions-bWgwyiVi.d.ts} +38 -11
  13. package/dist/index.cjs +317 -62
  14. package/dist/index.d.cts +8 -8
  15. package/dist/index.d.ts +8 -8
  16. package/dist/index.js +4 -4
  17. package/dist/{options-BnEPUuVV.d.ts → options-NAUHiQEC.d.cts} +3 -3
  18. package/dist/{options-BnEPUuVV.d.cts → options-NAUHiQEC.d.ts} +3 -3
  19. package/dist/{plugin-BWxJRKzS.d.cts → plugin-CjjzZX2F.d.cts} +4 -4
  20. package/dist/{plugin-e2WUEtiI.d.ts → plugin-CqxWoDKS.d.ts} +4 -4
  21. package/dist/plugin.cjs +20 -6
  22. package/dist/plugin.d.cts +5 -5
  23. package/dist/plugin.d.ts +5 -5
  24. package/dist/plugin.js +2 -2
  25. package/dist/providers/binance.d.cts +2 -2
  26. package/dist/providers/binance.d.ts +2 -2
  27. package/dist/providers/coinbase.d.cts +2 -2
  28. package/dist/providers/coinbase.d.ts +2 -2
  29. package/dist/providers/hyperliquid.d.cts +2 -2
  30. package/dist/providers/hyperliquid.d.ts +2 -2
  31. package/dist/{side-panel-HF0IAzwf.d.cts → side-panel-wMNfgyZb.d.cts} +13 -0
  32. package/dist/{side-panel-HF0IAzwf.d.ts → side-panel-wMNfgyZb.d.ts} +13 -0
  33. package/dist/{statusline-model-zKXtGIjj.d.ts → statusline-model-Dih6FGWP.d.ts} +3 -3
  34. package/dist/{statusline-model-DFY7tuHj.d.cts → statusline-model-Dpfpq54A.d.cts} +3 -3
  35. package/dist/ui.cjs +115 -5
  36. package/dist/ui.d.cts +28 -2
  37. package/dist/ui.d.ts +28 -2
  38. package/dist/ui.js +3 -3
  39. package/dist/vela.global.js +317 -62
  40. package/dist/vela.global.min.js +61 -68
  41. package/dist/widget.cjs +476 -85
  42. package/dist/widget.d.cts +20 -10
  43. package/dist/widget.d.ts +20 -10
  44. package/dist/widget.js +7 -7
  45. package/dist/workspace.cjs +476 -85
  46. package/dist/workspace.d.cts +6 -6
  47. package/dist/workspace.d.ts +6 -6
  48. package/dist/workspace.js +6 -6
  49. package/package.json +3 -3
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
  }
@@ -3617,14 +3690,49 @@ function buildColorPicker(color, theme, onChange, options = {}) {
3617
3690
  const knob = document.createElement("div");
3618
3691
  knob.style.cssText = "position:absolute;top:50%;width:15px;height:15px;border-radius:50%;background:var(--vela-selected-bg);box-shadow:0 1px 3px rgba(0,0,0,0.55);transform:translate(-50%,-50%);pointer-events:none;";
3619
3692
  track.appendChild(knob);
3620
- const pctBox = document.createElement("div");
3621
- pctBox.style.cssText = `min-width:42px;text-align:center;font:var(--vela-font-size-md) ${theme.fontFamily};color:var(--vela-fg);border:1px solid var(--vela-border);border-radius:5px;padding:3px 4px;`;
3693
+ const pctBox = document.createElement("input");
3694
+ pctBox.type = "text";
3695
+ pctBox.inputMode = "numeric";
3696
+ pctBox.title = "Opacity (%)";
3697
+ pctBox.style.cssText = `width:46px;box-sizing:border-box;text-align:center;font:var(--vela-font-size-md) ${theme.fontFamily};color:var(--vela-fg);background:transparent;border:1px solid var(--vela-border);border-radius:5px;padding:3px 4px;outline:none;`;
3698
+ pctBox.addEventListener("focus", () => {
3699
+ pctBox.style.borderColor = "var(--vela-border-strong)";
3700
+ pctBox.select();
3701
+ });
3622
3702
  opRow.append(track, pctBox);
3623
3703
  const paintOpacity = () => {
3624
3704
  track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
3625
3705
  knob.style.left = `${curAlpha * 100}%`;
3626
- pctBox.textContent = `${Math.round(curAlpha * 100)}%`;
3706
+ pctBox.value = `${Math.round(curAlpha * 100)}%`;
3707
+ };
3708
+ const commitTyped = () => {
3709
+ pctBox.style.borderColor = "var(--vela-border)";
3710
+ const n = parseFloat(pctBox.value.replace("%", ""));
3711
+ if (Number.isFinite(n)) {
3712
+ const next = Math.max(0, Math.min(100, n)) / 100;
3713
+ if (Math.round(next * 100) !== Math.round(curAlpha * 100)) {
3714
+ curAlpha = next;
3715
+ paintOpacity();
3716
+ emit();
3717
+ return;
3718
+ }
3719
+ }
3720
+ paintOpacity();
3627
3721
  };
3722
+ pctBox.addEventListener("change", commitTyped);
3723
+ pctBox.addEventListener("blur", () => {
3724
+ pctBox.style.borderColor = "var(--vela-border)";
3725
+ paintOpacity();
3726
+ });
3727
+ pctBox.addEventListener("keydown", (e) => {
3728
+ if (e.key === "Escape") return;
3729
+ e.stopPropagation();
3730
+ if (e.key === "Enter") {
3731
+ e.preventDefault();
3732
+ commitTyped();
3733
+ pctBox.blur();
3734
+ }
3735
+ });
3628
3736
  let dragging = false;
3629
3737
  const onDrag = (clientX) => {
3630
3738
  const r = track.getBoundingClientRect();
@@ -4963,6 +5071,7 @@ var LayoutPicker = class {
4963
5071
  const panel = doc.createElement("div");
4964
5072
  panel.className = "vela-lp";
4965
5073
  this.layer.appendChild(panel);
5074
+ this.panel = panel;
4966
5075
  const cols = doc.createElement("div");
4967
5076
  cols.className = "vela-lp-cols";
4968
5077
  panel.appendChild(cols);
@@ -5044,10 +5153,12 @@ var LayoutPicker = class {
5044
5153
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
5045
5154
  this.doc.addEventListener("keydown", this.onDocKeydown, true);
5046
5155
  this.opts.onOpenChange?.(true);
5156
+ if (this.isOpen) announceSurface(this.panel, true, "popover", this.opts.trigger);
5047
5157
  }
5048
5158
  close() {
5049
5159
  if (!this.isOpen) return;
5050
5160
  this.isOpen = false;
5161
+ announceSurface(this.panel, false, "popover", this.opts.trigger);
5051
5162
  this.layer.style.display = "none";
5052
5163
  this.opts.trigger.setAttribute("aria-expanded", "false");
5053
5164
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
@@ -5288,10 +5399,12 @@ function legendActions() {
5288
5399
  }
5289
5400
  function legendActionsProviderFor(chart, context) {
5290
5401
  return (indicatorId) => {
5291
- 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();
5292
5405
  if (!handle) return [];
5293
- const info = { id: handle.id, title: handle.title, ...handle.source !== void 0 ? { source: handle.source } : {} };
5294
- return legendActions().filter((d) => !d.when || d.when(info)).map((d) => ({ id: d.id, icon: d.icon, tooltip: d.tooltip, run: () => d.run(context(), info) }));
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)) }));
5295
5408
  };
5296
5409
  }
5297
5410
  var calloutRegistry = /* @__PURE__ */ new Map();
@@ -9508,6 +9621,10 @@ var FibRatios = class extends Drawing {
9508
9621
 
9509
9622
  // src/core/drawings/types/FibLevels.ts
9510
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
+ }
9511
9628
  /** Per-level pixel line + price for the ENABLED levels, spanning the anchors' time range. */
9512
9629
  levelLines(proj) {
9513
9630
  const a = this.anchors[0];
@@ -9517,11 +9634,10 @@ var FibLevels = class extends FibRatios {
9517
9634
  const xb = proj.xOf(b.time);
9518
9635
  const x1 = Math.min(xa, xb);
9519
9636
  const x2 = Math.max(xa, xb);
9520
- const delta = b.price - a.price;
9521
9637
  const out = [];
9522
9638
  for (const lv of this.levels) {
9523
9639
  if (!lv.enabled) continue;
9524
- const price = a.price + lv.ratio * delta;
9640
+ const price = this.levelPrice(lv.ratio, a.price, b.price);
9525
9641
  const y = proj.yOf(price, this.paneId);
9526
9642
  if (y == null) continue;
9527
9643
  out.push({ ratio: lv.ratio, color: lv.color, label: lv.label, price, x1, x2, y });
@@ -9561,8 +9677,7 @@ var FibLevels = class extends FibRatios {
9561
9677
  const a = this.anchors[0];
9562
9678
  const b = this.anchors[1];
9563
9679
  if (!a || !b) return null;
9564
- const delta = b.price - a.price;
9565
- 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));
9566
9681
  if (prices.length === 0) return null;
9567
9682
  return { min: Math.min(...prices), max: Math.max(...prices) };
9568
9683
  }
@@ -9648,13 +9763,24 @@ function cycleLevels(count, enabledCount = count) {
9648
9763
  // src/core/drawings/types/FibRetracement.ts
9649
9764
  var LEVELS = fibLevels([0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]);
9650
9765
  var FibRetracement = class extends FibLevels {
9651
- constructor() {
9652
- super(...arguments);
9766
+ constructor(init) {
9767
+ super(init);
9653
9768
  this.type = "fibretracement";
9769
+ if (this.reverse === void 0) this.reverse = false;
9654
9770
  }
9655
9771
  defaultLevels() {
9656
9772
  return LEVELS;
9657
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
+ }
9658
9784
  };
9659
9785
 
9660
9786
  // src/core/drawings/types/FibExtension.ts
@@ -12781,6 +12907,32 @@ var CSS5 = `
12781
12907
  .vela-panel-pin .vela-icon { width: 15px; height: 15px; }
12782
12908
  .vela-panel-pin:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
12783
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; }
12784
12936
  .vela-panel-body { flex: 1; overflow: auto; padding: 8px; }
12785
12937
  .vela-panel-body::-webkit-scrollbar { width: 8px; }
12786
12938
  .vela-panel-body::-webkit-scrollbar-thumb { background: var(--vela-scroll); border-radius: 4px; border: 2px solid transparent; background-clip: padding-box; }
@@ -12833,6 +12985,7 @@ var CSS5 = `
12833
12985
  border-left: none;
12834
12986
  }
12835
12987
  [data-layout='mobile'] .vela-panel-resizer { display: none; }
12988
+ [data-layout='mobile'] .vela-panel-max { display: none; }
12836
12989
  `;
12837
12990
  function clampPanelWidth(px, min = DEFAULT_PANEL_MIN_WIDTH, max = DEFAULT_PANEL_MAX_WIDTH) {
12838
12991
  const lo = Number.isFinite(min) && min > 0 ? min : DEFAULT_PANEL_MIN_WIDTH;
@@ -12856,6 +13009,10 @@ var SidePanel = class {
12856
13009
  * never on a programmatic {@link setOverlay}, for the same reason as widths. */
12857
13010
  this.onPlacementChange = null;
12858
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;
12859
13016
  const doc = host.ownerDocument;
12860
13017
  injectStyles(STYLE_ID4, CSS5, doc);
12861
13018
  this.minWidth = opts.resizable ? opts.minWidth ?? DEFAULT_PANEL_MIN_WIDTH : 1;
@@ -12890,7 +13047,13 @@ var SidePanel = class {
12890
13047
  });
12891
13048
  this.refreshPin();
12892
13049
  }
12893
- 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);
12894
13057
  this.body = doc.createElement("div");
12895
13058
  this.body.className = "vela-panel-body";
12896
13059
  this.el.append(header, this.body);
@@ -12902,9 +13065,37 @@ var SidePanel = class {
12902
13065
  }
12903
13066
  /** Open/close the panel — a bare call flips it. */
12904
13067
  toggle(open2 = this.el.hidden) {
12905
- 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
+ }
12906
13074
  this.el.hidden = !open2;
13075
+ if (!open2) this.setMaximized(false);
12907
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");
12908
13099
  }
12909
13100
  /** The scrolling body, for a panel filled from OUTSIDE the class — a contributed panel's
12910
13101
  * `mount` receives exactly this element. Subclasses use the protected `body`. */
@@ -12956,6 +13147,7 @@ var SidePanel = class {
12956
13147
  this.pin.setAttribute("aria-pressed", pinned ? "true" : "false");
12957
13148
  }
12958
13149
  destroy() {
13150
+ if (this.open) announceSurface(this.el, false, "panel");
12959
13151
  this.el.remove();
12960
13152
  }
12961
13153
  /** The drag handle on the panel's inner (left) edge — the panel is docked right, so dragging
@@ -14372,19 +14564,22 @@ var PanelDock = class {
14372
14564
  /**
14373
14565
  * (Re)build the CONTRIBUTED panels from the registry — call once after the built-ins, and
14374
14566
  * again on `refreshActions()` so a late registration appears. Contributed panels that are
14375
- * gone from the registry are dropped; the ones still there are rebuilt, so a replaced
14376
- * 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.
14377
14569
  */
14378
14570
  refresh() {
14379
14571
  const openBefore = this.openId;
14380
- for (const entry of [...this.entries]) if (entry.contributed) this.drop(entry);
14381
- 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;
14382
14576
  const panel = new SidePanel(this.host, desc.title, `vela-panel-${desc.id}`, {
14383
14577
  width: desc.width,
14384
14578
  resizable: desc.resizable,
14385
14579
  minWidth: desc.minWidth,
14386
14580
  maxWidth: desc.maxWidth,
14387
- overlay: desc.overlay
14581
+ overlay: desc.overlay,
14582
+ maximizable: desc.maximizable
14388
14583
  });
14389
14584
  const entry = {
14390
14585
  id: desc.id,
@@ -14392,10 +14587,19 @@ var PanelDock = class {
14392
14587
  icon: desc.icon,
14393
14588
  order: desc.order ?? DEFAULT_PANEL_ORDER,
14394
14589
  panel,
14395
- contributed: true
14590
+ contributed: true,
14591
+ desc
14396
14592
  };
14397
14593
  try {
14398
- 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;
14399
14603
  if (this.chart) entry.handle?.onChart?.(this.chart);
14400
14604
  } catch (err) {
14401
14605
  console.warn(`[vela] side panel "${desc.id}" failed to mount`, err);
@@ -15211,7 +15415,12 @@ var TimeframeQuick = class {
15211
15415
  content: (body) => body.append(this.input, this.hint),
15212
15416
  onOpenChange: (open2) => opts.onOpenChange?.(open2)
15213
15417
  });
15214
- this.input.addEventListener("input", () => this.renderHint());
15418
+ this.input.addEventListener("input", () => {
15419
+ const { selectionStart, selectionEnd } = this.input;
15420
+ this.input.value = this.input.value.toUpperCase();
15421
+ this.input.setSelectionRange(selectionStart, selectionEnd);
15422
+ this.renderHint();
15423
+ });
15215
15424
  this.input.addEventListener("keydown", (e) => {
15216
15425
  if (e.key !== "Enter") return;
15217
15426
  const parsed = parseTimeframe(this.input.value);
@@ -15223,7 +15432,7 @@ var TimeframeQuick = class {
15223
15432
  }
15224
15433
  open(seed = "") {
15225
15434
  this.dialog.show();
15226
- this.input.value = seed;
15435
+ this.input.value = seed.toUpperCase();
15227
15436
  this.renderHint();
15228
15437
  setTimeout(() => {
15229
15438
  this.input.focus();
@@ -15241,7 +15450,7 @@ var TimeframeQuick = class {
15241
15450
  this.open(text);
15242
15451
  return;
15243
15452
  }
15244
- this.input.value += text;
15453
+ this.input.value += text.toUpperCase();
15245
15454
  this.renderHint();
15246
15455
  }
15247
15456
  close() {
@@ -15254,7 +15463,7 @@ var TimeframeQuick = class {
15254
15463
  const raw = this.input.value.trim();
15255
15464
  const parsed = parseTimeframe(raw);
15256
15465
  if (!raw) {
15257
- this.hint.textContent = "e.g. 15, 4h, D, 3M";
15466
+ this.hint.textContent = "e.g. 15, 4H, D, 3M";
15258
15467
  delete this.hint.dataset.invalid;
15259
15468
  } else if (parsed.valid) {
15260
15469
  this.hint.textContent = parsed.label ?? "";
@@ -15357,7 +15566,7 @@ var ShortcutsHelp = class {
15357
15566
  addRow("Delete the drawing under the cursor", ["Middle-click"]);
15358
15567
  const s = doc.createElement("div");
15359
15568
  s.className = "vela-sh-static";
15360
- s.textContent = "Typing a letter opens the symbol search; typing a digit opens the timeframe entry.";
15569
+ s.textContent = "Typing a letter or 0 opens the symbol search; typing a digit from 1 to 9 opens the timeframe entry.";
15361
15570
  this.list.appendChild(s);
15362
15571
  }
15363
15572
  };
@@ -18161,6 +18370,7 @@ var DrawingToolbar = class {
18161
18370
  );
18162
18371
  }
18163
18372
  }
18373
+ announceSurface(fly, true, "menu", cell);
18164
18374
  }
18165
18375
  openMagnetFlyout() {
18166
18376
  const cell = this.magnetCell;
@@ -18184,6 +18394,7 @@ var DrawingToolbar = class {
18184
18394
  })
18185
18395
  );
18186
18396
  }
18397
+ announceSurface(fly, true, "menu", cell);
18187
18398
  }
18188
18399
  flyoutHeader(text) {
18189
18400
  const t = this.theme;
@@ -18252,9 +18463,11 @@ var DrawingToolbar = class {
18252
18463
  }
18253
18464
  closeFlyout() {
18254
18465
  this.starEls.clear();
18255
- if (this.flyout) {
18256
- this.flyout.remove();
18466
+ const fly = this.flyout;
18467
+ if (fly) {
18257
18468
  this.flyout = null;
18469
+ announceSurface(fly, false, "menu", this.flyoutCell);
18470
+ fly.remove();
18258
18471
  this.flyoutCell?.classList.remove("vela-open");
18259
18472
  this.flyoutCell = null;
18260
18473
  this.flyoutOwnerId = null;
@@ -18379,23 +18592,21 @@ function arrowHitHtml(svg) {
18379
18592
  return `<span class="vela-dtb-hit vela-dtb-hit--arrow">${iconSpan(svg, 11)}</span>`;
18380
18593
  }
18381
18594
 
18382
- // src/renderers/native/chrome/luxalgo-logos.ts
18383
- var LUXALGO_SYMBOL_SVG = '<svg viewBox="0 0 45 40" aria-hidden="true"><g fill="currentColor"><path d="m40.25 38 4.58-7.998L28.802 2l-16.03 28 9.16-.001 6.87-12z"/><path d="M34.525 32.002 9.33 31.997 27.655 0h-9.158L.18 31.993 4.759 40h34.347z"/></g></svg>';
18384
- var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M 614 94.500 L 614 189 633.500 189 L 653 189 653 94.500 L 653 0 633.500 0 L 614 0 614 94.500 M 0 97.500 L 0 189 63.023 189 L 126.045 189 125.773 170.750 L 125.500 152.500 84.250 152.239 L 43 151.978 43 78.989 L 43 6 21.500 6 L 0 6 0 97.500 M 450.387 96.451 C 421.871 146.253, 398.306 186.994, 398.020 186.987 C 397.423 186.972, 362 124.724, 362 123.691 C 362 123.316, 370.100 108.818, 380 91.472 C 389.900 74.127, 398 59.500, 398 58.968 C 398 58.380, 391.007 58, 380.210 58 L 362.420 58 348.060 83.138 C 334.400 107.052, 333.637 108.160, 332.399 105.888 C 331.682 104.575, 325.287 93.375, 318.187 81 L 305.277 58.500 287.139 58.227 C 277.162 58.078, 269 58.186, 269 58.469 C 269 58.751, 277.154 73.274, 287.120 90.741 C 297.086 108.209, 305.261 122.983, 305.287 123.574 C 305.312 124.164, 297.318 138.789, 287.522 156.074 C 277.725 173.358, 269.524 187.838, 269.296 188.250 C 269.068 188.662, 276.977 189, 286.870 189 L 304.859 189 319.163 163.959 C 331.321 142.674, 333.647 139.165, 334.670 140.563 C 335.331 141.468, 341.864 152.737, 349.186 165.606 L 362.500 189.003 402.442 188.752 L 442.384 188.500 472.080 136.695 C 488.413 108.203, 501.939 84.729, 502.138 84.531 C 502.337 84.333, 516 107.757, 532.500 136.583 L 562.500 188.994 584.750 188.997 C 596.987 188.999, 607 188.854, 607 188.675 C 607 188.319, 503.014 6.686, 502.532 6.201 C 502.369 6.037, 478.904 46.649, 450.387 96.451 M 717.500 55.017 C 697.175 57.389, 680.641 68.752, 672.036 86.264 C 665.866 98.818, 663.840 108.024, 663.840 123.500 C 663.840 133.409, 664.328 138.355, 665.892 144.300 C 672.076 167.810, 689.191 184.757, 711.529 189.489 C 730.549 193.518, 747.264 189.540, 761.282 177.648 C 764.944 174.542, 768.178 172, 768.470 172 C 769.345 172, 769.030 185.018, 767.979 192.285 C 765.618 208.613, 755.148 218.672, 738.473 220.632 C 732.586 221.324, 723.136 219.599, 717.709 216.843 C 712.693 214.295, 706.636 207.946, 703.972 202.443 C 702.789 199.999, 701.749 198, 701.661 198.001 C 700.201 198.007, 665.596 205.071, 665.230 205.437 C 664.326 206.341, 669.156 218.313, 672.607 223.721 C 682.146 238.672, 700.008 248.257, 723.500 251.033 C 758.132 255.124, 788.930 241.875, 800.287 218 C 806.923 204.047, 807 203.001, 807 126.542 L 807 58 788.500 58 L 770 58 770 66.500 C 770 71.175, 769.641 75, 769.203 75 C 768.765 75, 766.627 73.142, 764.453 70.872 C 752.805 58.708, 735.614 52.903, 717.500 55.017 M 878.500 55.031 C 850.107 59.412, 830.755 75.064, 822.285 100.500 C 819.905 107.644, 819.614 110.051, 819.565 123 C 819.501 139.581, 820.786 145.825, 826.763 158 C 832.104 168.880, 842.842 179.424, 854.077 184.822 C 867.651 191.343, 873.132 192.462, 891.500 192.462 C 909.868 192.462, 915.349 191.343, 928.923 184.822 C 944.653 177.265, 956.667 162.306, 961.710 144 C 963.074 139.048, 963.481 134.047, 963.418 123 C 963.346 110.140, 963.038 107.595, 960.694 100.500 C 953.325 78.193, 936.695 62.854, 913.500 56.969 C 904.262 54.625, 887.240 53.682, 878.500 55.031 M 134.015 102.750 C 134.027 139.250, 134.310 148.689, 135.549 153.947 C 141.139 177.663, 159.815 193, 183.105 193 C 196.453 193, 210.163 186.171, 217.936 175.650 C 219.826 173.093, 221.738 171, 222.186 171 C 222.634 171, 223 175.050, 223 180 L 223 189 242.507 189 L 262.014 189 261.757 123.750 L 261.500 58.500 242.500 58.500 L 223.500 58.500 223 99.500 C 222.513 139.406, 222.441 140.610, 220.289 144.634 C 209.729 164.377, 183.014 164.880, 174.299 145.500 C 172.677 141.895, 172.469 137.382, 172.190 99.750 L 171.880 58 152.940 58 L 134 58 134.015 102.750 M 729.932 87.998 C 719.311 89.821, 710.155 97.164, 705.141 107.880 C 702.851 112.772, 702.510 114.798, 702.505 123.521 C 702.500 132.834, 702.732 134.014, 705.787 140.218 C 709.761 148.290, 715.989 154.097, 723.853 157.063 C 731.409 159.912, 744.359 159.466, 750.795 156.136 C 767.289 147.601, 774.103 127.713, 767.019 108.780 C 761.433 93.851, 746.051 85.232, 729.932 87.998 M 883.972 88.348 C 865.749 92.310, 854.873 109.966, 858.119 130.316 C 860.933 147.963, 872.346 158.830, 889.084 159.799 C 900.714 160.472, 906.867 158.234, 914.650 150.497 C 919.856 145.323, 921.123 143.340, 922.900 137.587 C 925.412 129.456, 925.655 118.570, 923.489 111.275 C 918.479 94.405, 901.478 84.541, 883.972 88.348"/></svg>';
18595
+ // src/renderers/native/chrome/vela-logos.ts
18596
+ var VELA_MARK_SVG = '<svg viewBox="0 -142 161.66 148" aria-hidden="true"><g fill="currentColor"><path d="M0 -140L32.91 -140L77.52 -62.73L61.06 -34.24Z"/><path d="M161.66 -140L128.75 -140L84.14 -62.73L100.59 -34.24Z"/><path d="M80.83 -52.04L95.85 -26.02L80.83 0L65.81 -26.02Z"/></g></svg>';
18597
+ var VELA_LETTERS_SVG = '<svg viewBox="161.66 -142 310.57 148" aria-hidden="true"><g fill="currentColor"><path d="M226.3 1.17Q200.81 1.17 186.12 -12.99Q171.42 -27.15 171.42 -51.17Q171.42 -75.2 185.87 -89.6Q200.33 -104 224.25 -104Q247.4 -104 261.7 -90.58Q276.01 -77.15 276.3 -54.59Q276.3 -49.9 275.42 -45.02H200.81V-43.55Q201.2 -32.62 207.75 -26.42Q214.29 -20.21 225.03 -20.21Q233.72 -20.21 239.34 -23.93Q244.95 -27.64 246.81 -34.77H274.64Q272.1 -19.14 259.16 -8.98Q246.22 1.17 226.3 1.17ZM201.5 -63.38H247.88Q246.42 -72.75 240.27 -77.88Q234.12 -83.01 224.45 -83.01Q215.17 -83.01 208.92 -77.73Q202.67 -72.46 201.5 -63.38ZM287.54 0V-140.04H315.76V0ZM429.34 -23.83H432.56V0H418.11Q407.76 0 403.36 -4.44Q398.97 -8.89 399.16 -16.8Q387.83 1.17 365.57 1.17Q348.48 1.17 337.88 -6.74Q327.29 -14.65 327.29 -29Q327.29 -45.41 339.45 -54.3Q351.6 -63.18 374.55 -63.18H394.96V-68.16Q394.96 -75.2 390.08 -79.3Q385.2 -83.4 376.51 -83.4Q368.3 -83.4 363.03 -79.98Q357.76 -76.56 356.88 -70.8H329.73Q331.19 -85.84 344.08 -94.92Q356.97 -104 377.29 -104Q399.06 -104 410.88 -94.43Q422.7 -84.86 422.7 -66.6V-30.18Q422.7 -26.37 424.26 -25.1Q425.82 -23.83 429.34 -23.83ZM394.96 -42.38V-44.43H374.36Q365.47 -44.43 360.59 -40.92Q355.7 -37.4 355.7 -30.96Q355.7 -25.59 359.81 -22.41Q363.91 -19.24 370.94 -19.24Q381.97 -19.24 388.37 -25.54Q394.77 -31.84 394.96 -42.38Z"/><path fill-opacity="0.55" d="M437.44 -124.28H441.96V-113.41H445.34V-124.28H449.87V-127.4H437.44ZM451.43 -113.41H454.7V-123.29L458.97 -113.41H461.62L465.99 -123.29V-113.41H469.21V-127.4H464.53L460.42 -117.68L456.16 -127.4H451.43Z"/></g></svg>';
18385
18598
 
18386
18599
  // src/renderers/native/chrome/AttributionMark.ts
18387
- var ATTRIBUTION_URL = "https://luxalgo.com/vela";
18600
+ var ATTRIBUTION_URL = "https://velacharts.dev/?utm_source=vela&utm_medium=attribution";
18388
18601
  var STYLE_ID20 = "vela-attribution-styles";
18389
18602
  var CSS20 = `
18390
18603
  .vela-attribution {
18391
18604
  text-decoration: none;
18392
18605
  display: flex;
18393
18606
  align-items: center;
18394
- /* Gutter, wordmark size and offset below are the brand lockup's own ratios,
18395
- measured off the official horizontal logo: gutter 0.157, wordmark height
18396
- 0.966, and wordmark 0.109 LOWER than the symbol \u2014 box-centering the two
18397
- reads wrong because the symbol's ink hangs low and the wordmark descends. */
18398
- gap: 4px;
18607
+ /* No gutter: the V is the wordmark's first letter, and the letters' viewBox starts
18608
+ where the V's ends, so the kit's own letter spacing comes with them. */
18609
+ gap: 0;
18399
18610
  }
18400
18611
  .vela-attribution .vela-attr-symbol {
18401
18612
  flex: none;
@@ -18403,7 +18614,7 @@ var CSS20 = `
18403
18614
  line-height: 0;
18404
18615
  }
18405
18616
  .vela-attribution .vela-attr-symbol svg {
18406
- height: 28px;
18617
+ height: 24px;
18407
18618
  width: auto;
18408
18619
  display: block;
18409
18620
  filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
@@ -18414,15 +18625,13 @@ var CSS20 = `
18414
18625
  opacity: 0;
18415
18626
  display: flex;
18416
18627
  align-items: center;
18417
- /* Offset here, not on the image: the clip box must not crop the descender. */
18418
- position: relative;
18419
- top: 3px;
18628
+ /* Same height and baseline as the V (one viewBox height), so no vertical offset. */
18420
18629
  transform: translateX(-8px);
18421
18630
  transition: max-width 0.3s ease, opacity 0.25s ease, transform 0.3s ease;
18422
18631
  flex: none;
18423
18632
  }
18424
18633
  .vela-attribution .vela-attr-wordmark svg {
18425
- height: 27px;
18634
+ height: 24px;
18426
18635
  width: auto;
18427
18636
  display: block;
18428
18637
  filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
@@ -18437,14 +18646,11 @@ var CSS20 = `
18437
18646
  (data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
18438
18647
  container, so only the shell attribute reaches it. */
18439
18648
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
18440
- [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 22px; }
18649
+ [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 20px; }
18441
18650
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
18442
- [data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
18443
- top: 2px;
18444
- transform: translateX(-6px);
18445
- }
18651
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark { transform: translateX(-6px); }
18446
18652
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
18447
- [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 21px; }
18653
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 20px; }
18448
18654
  [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
18449
18655
  [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
18450
18656
  `;
@@ -18501,10 +18707,10 @@ function createAttributionMark(doc, background) {
18501
18707
  const symbol = doc.createElement("span");
18502
18708
  symbol.className = "vela-attr-symbol";
18503
18709
  symbol.setAttribute("aria-hidden", "true");
18504
- symbol.innerHTML = LUXALGO_SYMBOL_SVG;
18710
+ symbol.innerHTML = VELA_MARK_SVG;
18505
18711
  const wordmark = doc.createElement("span");
18506
18712
  wordmark.className = "vela-attr-wordmark";
18507
- wordmark.innerHTML = LUXALGO_WORDMARK_SVG;
18713
+ wordmark.innerHTML = VELA_LETTERS_SVG;
18508
18714
  a.append(symbol, wordmark);
18509
18715
  return a;
18510
18716
  }
@@ -18916,7 +19122,7 @@ var IndicatorHandleImpl = class {
18916
19122
  this.controller.applyProps(this.id, values);
18917
19123
  }
18918
19124
  updateCode(source) {
18919
- this.controller.updateCode(this.id, source);
19125
+ return this.controller.updateCode(this.id, source);
18920
19126
  }
18921
19127
  setVisible(visible) {
18922
19128
  this.controller.setVisible(this.id, visible);
@@ -20779,20 +20985,26 @@ var EngineOrchestrator = class _EngineOrchestrator {
20779
20985
  /**
20780
20986
  * IndicatorController: replace a SCRIPT indicator's source in place (see
20781
20987
  * {@link IndicatorHandle.updateCode}). Prepare-first: the running session keeps
20782
- * computing until the new source has compiled, so a broken edit costs the user
20783
- * 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.
20784
20991
  */
20785
20992
  updateCode(id, source) {
20786
20993
  const record = this.registry.get(id);
20787
20994
  const handle = this.handles.get(id);
20788
- 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.`) });
20789
20996
  if (record.native) {
20790
20997
  console.warn(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`);
20791
- return;
20998
+ return Promise.resolve({ ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`) });
20792
20999
  }
20793
- if (source === record.source && record.pendingSource === void 0) return;
20794
- record.pendingSource = source;
20795
- 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
+ });
20796
21008
  }
20797
21009
  async swapSource(id, source, handle) {
20798
21010
  const engine = this.registry.get(id)?.engine ?? this.engineFor(this.registry.get(id)?.options?.language);
@@ -20803,26 +21015,41 @@ var EngineOrchestrator = class _EngineOrchestrator {
20803
21015
  const record2 = this.registry.get(id);
20804
21016
  if (record2?.pendingSource === source) {
20805
21017
  record2.pendingSource = void 0;
20806
- this.fail(id, handle, err);
21018
+ this.rejectCode(id, record2, handle, err);
20807
21019
  }
20808
21020
  return;
20809
21021
  }
20810
21022
  const record = this.registry.get(id);
20811
21023
  if (!record || record.pendingSource !== source) return;
20812
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);
20813
21036
  record.source = source;
20814
21037
  record.engine = engine;
20815
21038
  record.prepared = prepared;
20816
21039
  handle.setSource(source);
20817
- record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues);
20818
- record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues);
20819
21040
  handle.setSchema(prepared.inputs);
20820
21041
  handle.setPropsSchema(prepared.props ?? []);
20821
21042
  record.session?.stop();
20822
21043
  record.session = void 0;
20823
21044
  record.pendingStructural = true;
20824
21045
  record.pendingCause = "code";
20825
- 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 };
20826
21053
  if (record.renderHandle) {
20827
21054
  this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
20828
21055
  this.setLoading(record, true);
@@ -20830,6 +21057,46 @@ var EngineOrchestrator = class _EngineOrchestrator {
20830
21057
  this.mountLoadingPlaceholder(id, record);
20831
21058
  }
20832
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 });
20833
21100
  }
20834
21101
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
20835
21102
  /** Live handles of every indicator on the chart (script + native), insertion order. */
@@ -20844,6 +21111,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20844
21111
  removeIndicator(id) {
20845
21112
  const record = this.registry.remove(id);
20846
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.`) });
20847
21115
  record?.session?.stop();
20848
21116
  if (record?.native?.started) record.native.instance.stop();
20849
21117
  this.handles.delete(id);
@@ -20871,6 +21139,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
20871
21139
  if (!visible) {
20872
21140
  record.session?.stop();
20873
21141
  record.session = void 0;
21142
+ if (record.codeTrial) {
21143
+ record.codeTrial = void 0;
21144
+ this.settleCode(record, { ok: true, error: null });
21145
+ }
20874
21146
  if (record.native?.started) record.native.instance.suspend();
20875
21147
  if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
20876
21148
  else if (record.native) this.mountHiddenNativeRow(id, record);
@@ -20958,6 +21230,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20958
21230
  this.viewportUnsub?.();
20959
21231
  this.paneActionUnsub?.();
20960
21232
  for (const record of this.registry.all()) {
21233
+ this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
20961
21234
  record.session?.stop();
20962
21235
  if (record.native?.started) record.native.instance.stop();
20963
21236
  }
@@ -21026,7 +21299,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
21026
21299
  const record = this.registry.get(id);
21027
21300
  if (!record || !record.engine || !record.prepared || record.hidden) return;
21028
21301
  const mode = this.config.live && !record.prepared.reactsToViewport && record.engine.capabilities.streaming ? "live" : "static";
21029
- record.session = record.engine.execute(
21302
+ const prepared = record.prepared;
21303
+ const session = record.engine.execute(
21030
21304
  {
21031
21305
  prepared: record.prepared,
21032
21306
  market: this.market(),
@@ -21047,6 +21321,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
21047
21321
  const cause = record.pendingCause ?? "history";
21048
21322
  if (!this.applyModel(id, model)) return;
21049
21323
  record.pendingCause = void 0;
21324
+ if (record.codeTrial) {
21325
+ record.codeTrial = void 0;
21326
+ this.settleCode(record, { ok: true, error: null });
21327
+ }
21050
21328
  this.emitContextChanged(id);
21051
21329
  this.emitScriptRun(id, cause, first);
21052
21330
  },
@@ -21057,9 +21335,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
21057
21335
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
21058
21336
  },
21059
21337
  onWarning: (w) => this.events.emit("warning", w),
21060
- 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
+ }
21061
21342
  }
21062
21343
  );
21344
+ if (record.prepared !== prepared) {
21345
+ session.stop();
21346
+ return;
21347
+ }
21348
+ record.session = session;
21063
21349
  this.emitContextChanged(id);
21064
21350
  }
21065
21351
  /**
@@ -21560,7 +21846,7 @@ function yieldToPaint() {
21560
21846
  }
21561
21847
  return new Promise((resolve) => setTimeout(resolve, 0));
21562
21848
  }
21563
- function valuesOnSchema(schema, previous) {
21849
+ function valuesOnSchema(schema, previous, oldSchema = []) {
21564
21850
  const out = {};
21565
21851
  const declared = /* @__PURE__ */ new Set();
21566
21852
  for (const s of schema) {
@@ -21568,9 +21854,22 @@ function valuesOnSchema(schema, previous) {
21568
21854
  declared.add(s.key);
21569
21855
  declared.add(s.title);
21570
21856
  }
21571
- 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
+ }
21572
21867
  return out;
21573
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
+ }
21574
21873
  function blankedModel(model) {
21575
21874
  return {
21576
21875
  ...model,
@@ -24500,15 +24799,23 @@ var Batch = class {
24500
24799
  this.seg(x + w, y + h, x, y + h, lineWidth, c);
24501
24800
  this.seg(x, y + h, x, y, lineWidth, c);
24502
24801
  }
24503
- /** 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
+ */
24504
24807
  circle(cx, cy, r, c, segments = 16) {
24505
- let px = cx + r;
24808
+ const ext = r + AA_PAD;
24809
+ let px = cx + ext;
24506
24810
  let py = cy;
24811
+ this.ensure(segments * 3);
24507
24812
  for (let i = 1; i <= segments; i += 1) {
24508
24813
  const a = i / segments * Math.PI * 2;
24509
- const nx = cx + Math.cos(a) * r;
24510
- const ny = cy + Math.sin(a) * r;
24511
- 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);
24512
24819
  px = nx;
24513
24820
  py = ny;
24514
24821
  }
@@ -24689,7 +24996,7 @@ var WebGL2Backend = class {
24689
24996
  }
24690
24997
  mount(canvas) {
24691
24998
  this.canvas = canvas;
24692
- 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 });
24693
25000
  if (!this.gl) return;
24694
25001
  if (!this.initGL()) {
24695
25002
  this.gl = null;
@@ -25948,6 +26255,12 @@ var InputController = class {
25948
26255
  this.lastTapT = 0;
25949
26256
  this.lastTapX = 0;
25950
26257
  this.lastTapY = 0;
26258
+ // Whether the last two primary presses were claimed by the drawings layer. A double-click
26259
+ // is the last press plus the one before it; a tool-driven click pair must never fall through
26260
+ // to the pane maximize toggle, even when the tool disarmed itself between the two clicks
26261
+ // (a finished placement leaves plain cursor mode for the second click).
26262
+ this.lastPressDrawing = false;
26263
+ this.prevPressDrawing = false;
25951
26264
  this.onTouchContextMenu = (e) => {
25952
26265
  if (this.touches.size === 0) return;
25953
26266
  if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
@@ -26002,19 +26315,24 @@ var InputController = class {
26002
26315
  this.moved = false;
26003
26316
  this.startX = x;
26004
26317
  this.startY = y;
26318
+ this.prevPressDrawing = this.lastPressDrawing;
26319
+ this.lastPressDrawing = false;
26005
26320
  if (this.deps.drawingsClaim?.(x, y)) {
26006
26321
  this.region = "drawing";
26322
+ this.lastPressDrawing = true;
26007
26323
  this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
26008
26324
  this.capture(e.pointerId);
26009
26325
  return;
26010
26326
  }
26011
26327
  if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
26012
26328
  this.region = "drawing";
26329
+ this.lastPressDrawing = true;
26013
26330
  this.capture(e.pointerId);
26014
26331
  return;
26015
26332
  }
26016
26333
  if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
26017
26334
  this.region = "drawing";
26335
+ this.lastPressDrawing = true;
26018
26336
  this.capture(e.pointerId);
26019
26337
  return;
26020
26338
  }
@@ -26130,7 +26448,7 @@ var InputController = class {
26130
26448
  this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
26131
26449
  } else if (tapRelease && this.region === "data") {
26132
26450
  this.deps.onClick(x, y);
26133
- } else if (this.dragging && this.region === "data") {
26451
+ } else if (this.dragging && this.region === "data" && wasTouch) {
26134
26452
  const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
26135
26453
  if (!stale && Math.abs(this.vx) > FLING_MIN_SPEED) {
26136
26454
  const pitch = this.deps.getCoords().pxPerBar();
@@ -26336,6 +26654,7 @@ var InputController = class {
26336
26654
  if (region === "price") this.deps.resetPriceScale(x, y);
26337
26655
  else if (region === "separator") this.deps.resetPaneSize(y);
26338
26656
  else if (region === "time") this.deps.resetView();
26657
+ else if (this.lastPressDrawing || this.prevPressDrawing) return;
26339
26658
  else this.deps.dataDblClick(x, y);
26340
26659
  }
26341
26660
  };
@@ -28501,6 +28820,7 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
28501
28820
  const step = pickStep(span / Math.max(1, target));
28502
28821
  const zFrom = fromMs + offsetMs;
28503
28822
  const zTo = toMs + offsetMs;
28823
+ if (step >= MONTH) return calendarTicks(zFrom, zTo, step >= YEAR ? 12 : step >= 3 * MONTH ? 3 : 1, offsetMs);
28504
28824
  const first = Math.ceil(zFrom / step) * step;
28505
28825
  const out = [];
28506
28826
  for (let zt = first; zt <= zTo; zt += step) {
@@ -28531,6 +28851,18 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
28531
28851
  }
28532
28852
  return out;
28533
28853
  }
28854
+ function calendarTicks(zFrom, zTo, months, offsetMs) {
28855
+ const d0 = new Date(zFrom);
28856
+ let idx = d0.getUTCFullYear() * 12 + d0.getUTCMonth();
28857
+ if (Date.UTC(Math.floor(idx / 12), idx % 12, 1) < zFrom) idx += 1;
28858
+ idx = Math.ceil(idx / months) * months;
28859
+ const out = [];
28860
+ for (let zt = Date.UTC(Math.floor(idx / 12), idx % 12, 1); zt <= zTo; idx += months, zt = Date.UTC(Math.floor(idx / 12), idx % 12, 1)) {
28861
+ const label = months === 12 ? String(Math.floor(idx / 12)) : MONTHS[idx % 12];
28862
+ out.push({ time: zt - offsetMs, label, major: true });
28863
+ }
28864
+ return out;
28865
+ }
28534
28866
 
28535
28867
  // src/renderers/native/chrome/countdown.ts
28536
28868
  function countdownText(barOpen, barMs, now2) {
@@ -32482,6 +32814,12 @@ var DrawingInteraction = class {
32482
32814
  activeDragId() {
32483
32815
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
32484
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
+ }
32485
32823
  /** The drawing under a press that has not been released yet (drag or click undecided), or null. */
32486
32824
  pressedId() {
32487
32825
  return this.state.kind === "pressed" ? this.state.id : null;
@@ -33140,6 +33478,17 @@ var DrawingSettingsDialog = class {
33140
33478
  }
33141
33479
  }));
33142
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
+ }
33143
33492
  levels.forEach((lv, i) => {
33144
33493
  const row = document.createElement("div");
33145
33494
  row.className = "vela-field-span";
@@ -34293,6 +34642,8 @@ var UserDrawingController = class {
34293
34642
  this.drawings = [];
34294
34643
  /** Ids painted on an interleave layer this frame — the top canvas paints only their handles. */
34295
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();
34296
34647
  /** Cached slice canvases, keyed `paneId|beforeZ`, reused across frames to avoid churn. */
34297
34648
  this.sliceCache = /* @__PURE__ */ new Map();
34298
34649
  /** Series boundaries per pane as of the last `prepareSlices` — lets a repaint between data
@@ -34380,7 +34731,7 @@ var UserDrawingController = class {
34380
34731
  selectedIds: () => this.selectedIds,
34381
34732
  emit: (i) => this.emit(i),
34382
34733
  changed: () => {
34383
- this.invalidateSlices();
34734
+ this.syncLift();
34384
34735
  this.render();
34385
34736
  },
34386
34737
  openSettings: (id, x, y) => this.openSettingsById(id, x, y),
@@ -35006,6 +35357,20 @@ var UserDrawingController = class {
35006
35357
  /** A drawing that paints inside the series stack changed (content, not hover): its pixels
35007
35358
  * live in the backend composite, so this layer alone can't show the change — ask for a
35008
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
+ }
35009
35374
  invalidateSlices() {
35010
35375
  if (this.sliced.size > 0 || this.drawings.some((d) => this.isInterleaved(d))) this.deps.requestDataPaint();
35011
35376
  }
@@ -35024,7 +35389,7 @@ var UserDrawingController = class {
35024
35389
  const theme = this.deps.theme();
35025
35390
  const buckets = /* @__PURE__ */ new Map();
35026
35391
  for (const d of this.drawings) {
35027
- if (!d.visible) continue;
35392
+ if (!d.visible || this.lifted.has(d.id)) continue;
35028
35393
  const beforeZ = sliceKeyFor(d.zIndex, this.lastBounds.get(d.paneId) ?? []);
35029
35394
  if (beforeZ === null) continue;
35030
35395
  const key = `${d.paneId}|${beforeZ}`;
@@ -35076,8 +35441,9 @@ var UserDrawingController = class {
35076
35441
  mutedLabel: edited instanceof TextLabel ? edited.id : null
35077
35442
  };
35078
35443
  this.painter.seriesLook = this.deps.seriesLook();
35079
- this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
35080
- 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));
35081
35447
  const clones = this.interaction.dragClones();
35082
35448
  if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
35083
35449
  this.layoutTextEditor();
@@ -44733,6 +45099,9 @@ var CSS21 = `
44733
45099
  .vela-statusline .vela-sl-market { align-self: center; display: inline-flex; }
44734
45100
  .vela-statusline .vela-sl-market > [hidden] { display: none !important; }
44735
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; }
44736
45105
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
44737
45106
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
44738
45107
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
@@ -44850,6 +45219,10 @@ var Statusline = class {
44850
45219
  this.fitRO = null;
44851
45220
  /** The ladder rung currently applied — see {@link fit}. */
44852
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();
44853
45226
  this.onContextMenu = (e) => {
44854
45227
  if (!this.menu || !this.menuHooks) return;
44855
45228
  e.preventDefault();
@@ -44913,6 +45286,7 @@ var Statusline = class {
44913
45286
  this.fitRO = new ResizeObserver(() => this.fit());
44914
45287
  this.fitRO.observe(host);
44915
45288
  }
45289
+ doc.fonts?.addEventListener("loadingdone", this.onFontsLoaded);
44916
45290
  this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
44917
45291
  this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
44918
45292
  this.setMarketStatus("open");
@@ -44965,6 +45339,7 @@ var Statusline = class {
44965
45339
  * (detached host, node tests) nothing overflows, so the widest rung stays.
44966
45340
  */
44967
45341
  fit() {
45342
+ this.fitKey = this.widthKey();
44968
45343
  this.syncParts();
44969
45344
  const seg = segmentVisibility(this.parts, this.chartHidden);
44970
45345
  const overflows = () => this.el.scrollWidth > this.el.clientWidth;
@@ -45121,6 +45496,7 @@ var Statusline = class {
45121
45496
  this.detach();
45122
45497
  this.fitRO?.disconnect();
45123
45498
  this.fitRO = null;
45499
+ this.el.ownerDocument.fonts?.removeEventListener("loadingdone", this.onFontsLoaded);
45124
45500
  this.el.removeEventListener("contextmenu", this.onContextMenu);
45125
45501
  this.menu?.destroy();
45126
45502
  this.menu = null;
@@ -45136,7 +45512,22 @@ var Statusline = class {
45136
45512
  }
45137
45513
  render() {
45138
45514
  if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
45139
- 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");
45140
45531
  }
45141
45532
  /** Write the value readout for the current bar at the current ladder level. */
45142
45533
  renderValues() {
@@ -46770,7 +47161,7 @@ var ChartCell = class {
46770
47161
  * then the manifest in the host's own order. */
46771
47162
  libraryRows() {
46772
47163
  return [
46773
- ...this.supportedNatives().map((n) => ({ name: n.title, category: "Vela", native: true, nativeType: n.type, beta: n.beta })),
47164
+ ...this.supportedNatives().map((n) => ({ name: n.title, category: "Built-in", native: true, nativeType: n.type, beta: n.beta })),
46774
47165
  ...this.manifest.map((e) => ({ name: e.name, language: e.language, category: e.category }))
46775
47166
  ];
46776
47167
  }
@@ -47770,11 +48161,11 @@ function resolveTyping(key, open2) {
47770
48161
  if (open2.dialogs > 0) {
47771
48162
  if (key.length !== 1) return null;
47772
48163
  if (open2.symbolSearch) return { target: "symbol", text: key.toUpperCase() };
47773
- if (open2.timeframeEntry) return { target: "timeframe", text: key };
48164
+ if (open2.timeframeEntry) return { target: "timeframe", text: key.toUpperCase() };
47774
48165
  return null;
47775
48166
  }
47776
- if (/^[a-zA-Z]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
47777
- if (/^[0-9]$/.test(key)) return { target: "timeframe", text: key };
48167
+ if (/^[a-zA-Z0]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
48168
+ if (/^[1-9]$/.test(key)) return { target: "timeframe", text: key };
47778
48169
  return null;
47779
48170
  }
47780
48171
  var VelaWorkspace = class {
@@ -49512,7 +49903,7 @@ var VelaWorkspace = class {
49512
49903
  }
49513
49904
  });
49514
49905
  }
49515
- /** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
49906
+ /** Bare-typing router: letters and `0` → symbol search (seeded), digits 1–9 → timeframe entry. */
49516
49907
  routeTyping(ev) {
49517
49908
  if (this.destroyed) return;
49518
49909
  if (ev.ctrlKey || ev.metaKey || ev.altKey) return;