@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
@@ -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);
@@ -5253,10 +5364,12 @@ function legendActions() {
5253
5364
  }
5254
5365
  function legendActionsProviderFor(chart, context) {
5255
5366
  return (indicatorId) => {
5256
- const handle = chart.indicators().find((h) => h.id === indicatorId);
5367
+ const find = () => chart.indicators().find((h) => h.id === indicatorId);
5368
+ const infoOf = (h) => ({ id: h.id, title: h.title, ...h.source !== void 0 ? { source: h.source } : {} });
5369
+ const handle = find();
5257
5370
  if (!handle) return [];
5258
- const info = { id: handle.id, title: handle.title, ...handle.source !== void 0 ? { source: handle.source } : {} };
5259
- 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) }));
5371
+ const info = infoOf(handle);
5372
+ 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)) }));
5260
5373
  };
5261
5374
  }
5262
5375
  var calloutRegistry = /* @__PURE__ */ new Map();
@@ -9454,6 +9567,10 @@ var FibRatios = class extends Drawing {
9454
9567
 
9455
9568
  // src/core/drawings/types/FibLevels.ts
9456
9569
  var FibLevels = class extends FibRatios {
9570
+ /** Price of a level for the two anchor prices; subclasses override to change which anchor ratio 0 sits on. */
9571
+ levelPrice(ratio, p1, p2) {
9572
+ return p1 + ratio * (p2 - p1);
9573
+ }
9457
9574
  /** Per-level pixel line + price for the ENABLED levels, spanning the anchors' time range. */
9458
9575
  levelLines(proj) {
9459
9576
  const a = this.anchors[0];
@@ -9463,11 +9580,10 @@ var FibLevels = class extends FibRatios {
9463
9580
  const xb = proj.xOf(b.time);
9464
9581
  const x1 = Math.min(xa, xb);
9465
9582
  const x2 = Math.max(xa, xb);
9466
- const delta = b.price - a.price;
9467
9583
  const out = [];
9468
9584
  for (const lv of this.levels) {
9469
9585
  if (!lv.enabled) continue;
9470
- const price = a.price + lv.ratio * delta;
9586
+ const price = this.levelPrice(lv.ratio, a.price, b.price);
9471
9587
  const y = proj.yOf(price, this.paneId);
9472
9588
  if (y == null) continue;
9473
9589
  out.push({ ratio: lv.ratio, color: lv.color, label: lv.label, price, x1, x2, y });
@@ -9507,8 +9623,7 @@ var FibLevels = class extends FibRatios {
9507
9623
  const a = this.anchors[0];
9508
9624
  const b = this.anchors[1];
9509
9625
  if (!a || !b) return null;
9510
- const delta = b.price - a.price;
9511
- const prices = this.levels.filter((l) => l.enabled).map((l) => a.price + l.ratio * delta);
9626
+ const prices = this.levels.filter((l) => l.enabled).map((l) => this.levelPrice(l.ratio, a.price, b.price));
9512
9627
  if (prices.length === 0) return null;
9513
9628
  return { min: Math.min(...prices), max: Math.max(...prices) };
9514
9629
  }
@@ -9594,13 +9709,24 @@ function cycleLevels(count, enabledCount = count) {
9594
9709
  // src/core/drawings/types/FibRetracement.ts
9595
9710
  var LEVELS = fibLevels([0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]);
9596
9711
  var FibRetracement = class extends FibLevels {
9597
- constructor() {
9598
- super(...arguments);
9712
+ constructor(init) {
9713
+ super(init);
9599
9714
  this.type = "fibretracement";
9715
+ if (this.reverse === void 0) this.reverse = false;
9600
9716
  }
9601
9717
  defaultLevels() {
9602
9718
  return LEVELS;
9603
9719
  }
9720
+ levelPrice(ratio, p1, p2) {
9721
+ return this.reverse ? p1 + ratio * (p2 - p1) : p2 + ratio * (p1 - p2);
9722
+ }
9723
+ writeProps() {
9724
+ return { ...super.writeProps(), reverse: this.reverse };
9725
+ }
9726
+ readProps(props) {
9727
+ super.readProps(props);
9728
+ this.reverse = typeof props.reverse === "boolean" ? props.reverse : true;
9729
+ }
9604
9730
  };
9605
9731
 
9606
9732
  // src/core/drawings/types/FibExtension.ts
@@ -12727,6 +12853,32 @@ var CSS5 = `
12727
12853
  .vela-panel-pin .vela-icon { width: 15px; height: 15px; }
12728
12854
  .vela-panel-pin:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
12729
12855
  .vela-panel-pin[data-on='1'] { color: var(--vela-fg-bright); }
12856
+ .vela-panel-max {
12857
+ all: unset;
12858
+ cursor: pointer;
12859
+ width: 26px;
12860
+ height: 26px;
12861
+ flex: none;
12862
+ display: inline-flex;
12863
+ align-items: center;
12864
+ justify-content: center;
12865
+ border-radius: 4px;
12866
+ color: var(--vela-fg-muted);
12867
+ }
12868
+ .vela-panel-max .vela-icon { width: 15px; height: 15px; }
12869
+ .vela-panel-max:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
12870
+ /* MAXIMIZED: the panel covers the whole dock host \u2014 every chart of the shell \u2014 whatever
12871
+ its placement; the width handle and the pin have nothing to do meanwhile. */
12872
+ .vela-panel[data-maximized] {
12873
+ position: absolute;
12874
+ inset: 0;
12875
+ width: auto;
12876
+ max-width: none;
12877
+ z-index: 26;
12878
+ border-left: none;
12879
+ }
12880
+ .vela-panel[data-maximized] .vela-panel-resizer,
12881
+ .vela-panel[data-maximized] .vela-panel-pin { display: none; }
12730
12882
  .vela-panel-body { flex: 1; overflow: auto; padding: 8px; }
12731
12883
  .vela-panel-body::-webkit-scrollbar { width: 8px; }
12732
12884
  .vela-panel-body::-webkit-scrollbar-thumb { background: var(--vela-scroll); border-radius: 4px; border: 2px solid transparent; background-clip: padding-box; }
@@ -12779,6 +12931,7 @@ var CSS5 = `
12779
12931
  border-left: none;
12780
12932
  }
12781
12933
  [data-layout='mobile'] .vela-panel-resizer { display: none; }
12934
+ [data-layout='mobile'] .vela-panel-max { display: none; }
12782
12935
  `;
12783
12936
  function clampPanelWidth(px, min = DEFAULT_PANEL_MIN_WIDTH, max = DEFAULT_PANEL_MAX_WIDTH) {
12784
12937
  const lo = Number.isFinite(min) && min > 0 ? min : DEFAULT_PANEL_MIN_WIDTH;
@@ -12802,6 +12955,10 @@ var SidePanel = class {
12802
12955
  * never on a programmatic {@link setOverlay}, for the same reason as widths. */
12803
12956
  this.onPlacementChange = null;
12804
12957
  this.pin = null;
12958
+ this.maxButton = null;
12959
+ this.maximizedOn = false;
12960
+ /** Set while the close is announced, so a listener's own close does not re-enter. */
12961
+ this.closing = false;
12805
12962
  const doc = host.ownerDocument;
12806
12963
  injectStyles(STYLE_ID4, CSS5, doc);
12807
12964
  this.minWidth = opts.resizable ? opts.minWidth ?? DEFAULT_PANEL_MIN_WIDTH : 1;
@@ -12836,7 +12993,13 @@ var SidePanel = class {
12836
12993
  });
12837
12994
  this.refreshPin();
12838
12995
  }
12839
- header.append(this.heading, this.slot, ...this.pin ? [this.pin] : [], close);
12996
+ if (opts.maximizable) {
12997
+ this.maxButton = doc.createElement("button");
12998
+ this.maxButton.className = "vela-panel-max";
12999
+ this.maxButton.addEventListener("click", () => this.setMaximized(!this.maximizedOn));
13000
+ this.refreshMaxButton();
13001
+ }
13002
+ header.append(this.heading, this.slot, ...this.pin ? [this.pin] : [], ...this.maxButton ? [this.maxButton] : [], close);
12840
13003
  this.body = doc.createElement("div");
12841
13004
  this.body.className = "vela-panel-body";
12842
13005
  this.el.append(header, this.body);
@@ -12848,9 +13011,37 @@ var SidePanel = class {
12848
13011
  }
12849
13012
  /** Open/close the panel — a bare call flips it. */
12850
13013
  toggle(open2 = this.el.hidden) {
12851
- if (open2 === !this.el.hidden) return;
13014
+ if (open2 === !this.el.hidden || !open2 && this.closing) return;
13015
+ if (!open2) {
13016
+ this.closing = true;
13017
+ announceSurface(this.el, false, "panel");
13018
+ this.closing = false;
13019
+ }
12852
13020
  this.el.hidden = !open2;
13021
+ if (!open2) this.setMaximized(false);
12853
13022
  this.onOpenChange?.(open2);
13023
+ if (open2 && !this.el.hidden) announceSurface(this.el, true, "panel");
13024
+ }
13025
+ /** Whether the panel covers every chart right now. */
13026
+ get maximized() {
13027
+ return this.maximizedOn;
13028
+ }
13029
+ /** Maximize the panel over the shell's charts, or restore it. A no-op on a panel not
13030
+ * declared `maximizable`. */
13031
+ setMaximized(maximized) {
13032
+ if (!this.maxButton || maximized === this.maximizedOn) return;
13033
+ this.maximizedOn = maximized;
13034
+ if (maximized) this.el.dataset.maximized = "1";
13035
+ else delete this.el.dataset.maximized;
13036
+ this.refreshMaxButton();
13037
+ }
13038
+ refreshMaxButton() {
13039
+ if (!this.maxButton) return;
13040
+ const doc = this.maxButton.ownerDocument;
13041
+ this.maxButton.replaceChildren(iconEl(this.maximizedOn ? "restore" : "maximize", doc));
13042
+ this.maxButton.title = this.maximizedOn ? "Restore" : "Maximize";
13043
+ this.maxButton.setAttribute("aria-label", this.maxButton.title);
13044
+ this.maxButton.setAttribute("aria-pressed", this.maximizedOn ? "true" : "false");
12854
13045
  }
12855
13046
  /** The scrolling body, for a panel filled from OUTSIDE the class — a contributed panel's
12856
13047
  * `mount` receives exactly this element. Subclasses use the protected `body`. */
@@ -12902,6 +13093,7 @@ var SidePanel = class {
12902
13093
  this.pin.setAttribute("aria-pressed", pinned ? "true" : "false");
12903
13094
  }
12904
13095
  destroy() {
13096
+ if (this.open) announceSurface(this.el, false, "panel");
12905
13097
  this.el.remove();
12906
13098
  }
12907
13099
  /** The drag handle on the panel's inner (left) edge — the panel is docked right, so dragging
@@ -14318,19 +14510,22 @@ var PanelDock = class {
14318
14510
  /**
14319
14511
  * (Re)build the CONTRIBUTED panels from the registry — call once after the built-ins, and
14320
14512
  * again on `refreshActions()` so a late registration appears. Contributed panels that are
14321
- * gone from the registry are dropped; the ones still there are rebuilt, so a replaced
14322
- * descriptor takes effect.
14513
+ * gone from the registry are dropped and a REPLACED descriptor is rebuilt; a panel whose
14514
+ * descriptor is unchanged stays mounted as it is, so its content and state survive.
14323
14515
  */
14324
14516
  refresh() {
14325
14517
  const openBefore = this.openId;
14326
- for (const entry of [...this.entries]) if (entry.contributed) this.drop(entry);
14327
- for (const desc of sidePanels()) {
14518
+ const registered = sidePanels();
14519
+ for (const entry of [...this.entries]) if (entry.contributed && !registered.includes(entry.desc)) this.drop(entry);
14520
+ for (const desc of registered) {
14521
+ if (this.entries.some((e) => e.desc === desc)) continue;
14328
14522
  const panel = new SidePanel(this.host, desc.title, `vela-panel-${desc.id}`, {
14329
14523
  width: desc.width,
14330
14524
  resizable: desc.resizable,
14331
14525
  minWidth: desc.minWidth,
14332
14526
  maxWidth: desc.maxWidth,
14333
- overlay: desc.overlay
14527
+ overlay: desc.overlay,
14528
+ maximizable: desc.maximizable
14334
14529
  });
14335
14530
  const entry = {
14336
14531
  id: desc.id,
@@ -14338,10 +14533,19 @@ var PanelDock = class {
14338
14533
  icon: desc.icon,
14339
14534
  order: desc.order ?? DEFAULT_PANEL_ORDER,
14340
14535
  panel,
14341
- contributed: true
14536
+ contributed: true,
14537
+ desc
14342
14538
  };
14343
14539
  try {
14344
- entry.handle = desc.mount(this.deps.context(), panel.content, { slot: panel.headerSlot, setTitle: (t) => panel.setTitle(t) }) ?? void 0;
14540
+ const header = {
14541
+ slot: panel.headerSlot,
14542
+ setTitle: (t) => panel.setTitle(t),
14543
+ get maximized() {
14544
+ return panel.maximized;
14545
+ },
14546
+ setMaximized: (on) => panel.setMaximized(on)
14547
+ };
14548
+ entry.handle = desc.mount(this.deps.context(), panel.content, header) ?? void 0;
14345
14549
  if (this.chart) entry.handle?.onChart?.(this.chart);
14346
14550
  } catch (err) {
14347
14551
  console.warn(`[vela] side panel "${desc.id}" failed to mount`, err);
@@ -15129,7 +15333,12 @@ var TimeframeQuick = class {
15129
15333
  content: (body) => body.append(this.input, this.hint),
15130
15334
  onOpenChange: (open2) => opts.onOpenChange?.(open2)
15131
15335
  });
15132
- this.input.addEventListener("input", () => this.renderHint());
15336
+ this.input.addEventListener("input", () => {
15337
+ const { selectionStart, selectionEnd } = this.input;
15338
+ this.input.value = this.input.value.toUpperCase();
15339
+ this.input.setSelectionRange(selectionStart, selectionEnd);
15340
+ this.renderHint();
15341
+ });
15133
15342
  this.input.addEventListener("keydown", (e) => {
15134
15343
  if (e.key !== "Enter") return;
15135
15344
  const parsed = parseTimeframe(this.input.value);
@@ -15141,7 +15350,7 @@ var TimeframeQuick = class {
15141
15350
  }
15142
15351
  open(seed = "") {
15143
15352
  this.dialog.show();
15144
- this.input.value = seed;
15353
+ this.input.value = seed.toUpperCase();
15145
15354
  this.renderHint();
15146
15355
  setTimeout(() => {
15147
15356
  this.input.focus();
@@ -15159,7 +15368,7 @@ var TimeframeQuick = class {
15159
15368
  this.open(text);
15160
15369
  return;
15161
15370
  }
15162
- this.input.value += text;
15371
+ this.input.value += text.toUpperCase();
15163
15372
  this.renderHint();
15164
15373
  }
15165
15374
  close() {
@@ -15172,7 +15381,7 @@ var TimeframeQuick = class {
15172
15381
  const raw = this.input.value.trim();
15173
15382
  const parsed = parseTimeframe(raw);
15174
15383
  if (!raw) {
15175
- this.hint.textContent = "e.g. 15, 4h, D, 3M";
15384
+ this.hint.textContent = "e.g. 15, 4H, D, 3M";
15176
15385
  delete this.hint.dataset.invalid;
15177
15386
  } else if (parsed.valid) {
15178
15387
  this.hint.textContent = parsed.label ?? "";
@@ -15275,7 +15484,7 @@ var ShortcutsHelp = class {
15275
15484
  addRow("Delete the drawing under the cursor", ["Middle-click"]);
15276
15485
  const s = doc.createElement("div");
15277
15486
  s.className = "vela-sh-static";
15278
- s.textContent = "Typing a letter opens the symbol search; typing a digit opens the timeframe entry.";
15487
+ s.textContent = "Typing a letter or 0 opens the symbol search; typing a digit from 1 to 9 opens the timeframe entry.";
15279
15488
  this.list.appendChild(s);
15280
15489
  }
15281
15490
  };
@@ -18079,6 +18288,7 @@ var DrawingToolbar = class {
18079
18288
  );
18080
18289
  }
18081
18290
  }
18291
+ announceSurface(fly, true, "menu", cell);
18082
18292
  }
18083
18293
  openMagnetFlyout() {
18084
18294
  const cell = this.magnetCell;
@@ -18102,6 +18312,7 @@ var DrawingToolbar = class {
18102
18312
  })
18103
18313
  );
18104
18314
  }
18315
+ announceSurface(fly, true, "menu", cell);
18105
18316
  }
18106
18317
  flyoutHeader(text) {
18107
18318
  const t = this.theme;
@@ -18170,9 +18381,11 @@ var DrawingToolbar = class {
18170
18381
  }
18171
18382
  closeFlyout() {
18172
18383
  this.starEls.clear();
18173
- if (this.flyout) {
18174
- this.flyout.remove();
18384
+ const fly = this.flyout;
18385
+ if (fly) {
18175
18386
  this.flyout = null;
18387
+ announceSurface(fly, false, "menu", this.flyoutCell);
18388
+ fly.remove();
18176
18389
  this.flyoutCell?.classList.remove("vela-open");
18177
18390
  this.flyoutCell = null;
18178
18391
  this.flyoutOwnerId = null;
@@ -18297,23 +18510,21 @@ function arrowHitHtml(svg) {
18297
18510
  return `<span class="vela-dtb-hit vela-dtb-hit--arrow">${iconSpan(svg, 11)}</span>`;
18298
18511
  }
18299
18512
 
18300
- // src/renderers/native/chrome/luxalgo-logos.ts
18301
- 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>';
18302
- 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>';
18513
+ // src/renderers/native/chrome/vela-logos.ts
18514
+ 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>';
18515
+ 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>';
18303
18516
 
18304
18517
  // src/renderers/native/chrome/AttributionMark.ts
18305
- var ATTRIBUTION_URL = "https://luxalgo.com/vela";
18518
+ var ATTRIBUTION_URL = "https://velacharts.dev/?utm_source=vela&utm_medium=attribution";
18306
18519
  var STYLE_ID20 = "vela-attribution-styles";
18307
18520
  var CSS20 = `
18308
18521
  .vela-attribution {
18309
18522
  text-decoration: none;
18310
18523
  display: flex;
18311
18524
  align-items: center;
18312
- /* Gutter, wordmark size and offset below are the brand lockup's own ratios,
18313
- measured off the official horizontal logo: gutter 0.157, wordmark height
18314
- 0.966, and wordmark 0.109 LOWER than the symbol \u2014 box-centering the two
18315
- reads wrong because the symbol's ink hangs low and the wordmark descends. */
18316
- gap: 4px;
18525
+ /* No gutter: the V is the wordmark's first letter, and the letters' viewBox starts
18526
+ where the V's ends, so the kit's own letter spacing comes with them. */
18527
+ gap: 0;
18317
18528
  }
18318
18529
  .vela-attribution .vela-attr-symbol {
18319
18530
  flex: none;
@@ -18321,7 +18532,7 @@ var CSS20 = `
18321
18532
  line-height: 0;
18322
18533
  }
18323
18534
  .vela-attribution .vela-attr-symbol svg {
18324
- height: 28px;
18535
+ height: 24px;
18325
18536
  width: auto;
18326
18537
  display: block;
18327
18538
  filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
@@ -18332,15 +18543,13 @@ var CSS20 = `
18332
18543
  opacity: 0;
18333
18544
  display: flex;
18334
18545
  align-items: center;
18335
- /* Offset here, not on the image: the clip box must not crop the descender. */
18336
- position: relative;
18337
- top: 3px;
18546
+ /* Same height and baseline as the V (one viewBox height), so no vertical offset. */
18338
18547
  transform: translateX(-8px);
18339
18548
  transition: max-width 0.3s ease, opacity 0.25s ease, transform 0.3s ease;
18340
18549
  flex: none;
18341
18550
  }
18342
18551
  .vela-attribution .vela-attr-wordmark svg {
18343
- height: 27px;
18552
+ height: 24px;
18344
18553
  width: auto;
18345
18554
  display: block;
18346
18555
  filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
@@ -18355,14 +18564,11 @@ var CSS20 = `
18355
18564
  (data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
18356
18565
  container, so only the shell attribute reaches it. */
18357
18566
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
18358
- [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 22px; }
18567
+ [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 20px; }
18359
18568
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
18360
- [data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
18361
- top: 2px;
18362
- transform: translateX(-6px);
18363
- }
18569
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark { transform: translateX(-6px); }
18364
18570
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
18365
- [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 21px; }
18571
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 20px; }
18366
18572
  [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
18367
18573
  [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
18368
18574
  `;
@@ -18419,10 +18625,10 @@ function createAttributionMark(doc, background) {
18419
18625
  const symbol = doc.createElement("span");
18420
18626
  symbol.className = "vela-attr-symbol";
18421
18627
  symbol.setAttribute("aria-hidden", "true");
18422
- symbol.innerHTML = LUXALGO_SYMBOL_SVG;
18628
+ symbol.innerHTML = VELA_MARK_SVG;
18423
18629
  const wordmark = doc.createElement("span");
18424
18630
  wordmark.className = "vela-attr-wordmark";
18425
- wordmark.innerHTML = LUXALGO_WORDMARK_SVG;
18631
+ wordmark.innerHTML = VELA_LETTERS_SVG;
18426
18632
  a.append(symbol, wordmark);
18427
18633
  return a;
18428
18634
  }
@@ -18848,7 +19054,7 @@ var IndicatorHandleImpl = class {
18848
19054
  this.controller.applyProps(this.id, values);
18849
19055
  }
18850
19056
  updateCode(source) {
18851
- this.controller.updateCode(this.id, source);
19057
+ return this.controller.updateCode(this.id, source);
18852
19058
  }
18853
19059
  setVisible(visible) {
18854
19060
  this.controller.setVisible(this.id, visible);
@@ -20711,20 +20917,26 @@ var EngineOrchestrator = class _EngineOrchestrator {
20711
20917
  /**
20712
20918
  * IndicatorController: replace a SCRIPT indicator's source in place (see
20713
20919
  * {@link IndicatorHandle.updateCode}). Prepare-first: the running session keeps
20714
- * computing until the new source has compiled, so a broken edit costs the user
20715
- * nothing but an `error` event. Natives have no script — warn and leave them alone.
20920
+ * computing until the new source has compiled; then the new code runs on TRIAL until
20921
+ * its first model lands, and a failure before that puts the previous code back.
20922
+ * Natives have no script — warn and leave them alone.
20716
20923
  */
20717
20924
  updateCode(id, source) {
20718
20925
  const record = this.registry.get(id);
20719
20926
  const handle = this.handles.get(id);
20720
- if (!record || !handle) return;
20927
+ if (!record || !handle) return Promise.resolve({ ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator is no longer on the chart.`) });
20721
20928
  if (record.native) {
20722
20929
  console.warn(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`);
20723
- return;
20930
+ return Promise.resolve({ ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`) });
20724
20931
  }
20725
- if (source === record.source && record.pendingSource === void 0) return;
20726
- record.pendingSource = source;
20727
- void this.swapSource(id, source, handle);
20932
+ const busy = record.pendingSource !== void 0 || record.codeTrial !== void 0;
20933
+ if (!busy && source === record.source) return Promise.resolve({ ok: true, error: null });
20934
+ return new Promise((resolve) => {
20935
+ (record.codeWaiters ?? (record.codeWaiters = [])).push(resolve);
20936
+ if (record.pendingSource === source || record.pendingSource === void 0 && record.codeTrial && record.source === source) return;
20937
+ record.pendingSource = source;
20938
+ void this.swapSource(id, source, handle);
20939
+ });
20728
20940
  }
20729
20941
  async swapSource(id, source, handle) {
20730
20942
  const engine = this.registry.get(id)?.engine ?? this.engineFor(this.registry.get(id)?.options?.language);
@@ -20735,26 +20947,41 @@ var EngineOrchestrator = class _EngineOrchestrator {
20735
20947
  const record2 = this.registry.get(id);
20736
20948
  if (record2?.pendingSource === source) {
20737
20949
  record2.pendingSource = void 0;
20738
- this.fail(id, handle, err);
20950
+ this.rejectCode(id, record2, handle, err);
20739
20951
  }
20740
20952
  return;
20741
20953
  }
20742
20954
  const record = this.registry.get(id);
20743
20955
  if (!record || record.pendingSource !== source) return;
20744
20956
  record.pendingSource = void 0;
20957
+ const previous = record.codeTrial?.previous ?? {
20958
+ source: record.source,
20959
+ engine: record.engine,
20960
+ prepared: record.prepared,
20961
+ inputValues: record.inputValues,
20962
+ propValues: record.propValues,
20963
+ inputs: [...handle.inputs],
20964
+ props: [...handle.props]
20965
+ };
20966
+ record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues, record.prepared?.inputs);
20967
+ record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues, record.prepared?.props);
20745
20968
  record.source = source;
20746
20969
  record.engine = engine;
20747
20970
  record.prepared = prepared;
20748
20971
  handle.setSource(source);
20749
- record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues);
20750
- record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues);
20751
20972
  handle.setSchema(prepared.inputs);
20752
20973
  handle.setPropsSchema(prepared.props ?? []);
20753
20974
  record.session?.stop();
20754
20975
  record.session = void 0;
20755
20976
  record.pendingStructural = true;
20756
20977
  record.pendingCause = "code";
20757
- if (record.hidden) return;
20978
+ if (record.hidden) {
20979
+ record.codeTrial = void 0;
20980
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
20981
+ this.settleCode(record, { ok: true, error: null });
20982
+ return;
20983
+ }
20984
+ record.codeTrial = { previous };
20758
20985
  if (record.renderHandle) {
20759
20986
  this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
20760
20987
  this.setLoading(record, true);
@@ -20762,6 +20989,46 @@ var EngineOrchestrator = class _EngineOrchestrator {
20762
20989
  this.mountLoadingPlaceholder(id, record);
20763
20990
  }
20764
20991
  this.executeIndicator(id, handle);
20992
+ if (this.bars.length === 0 && record.codeTrial) {
20993
+ record.codeTrial = void 0;
20994
+ this.settleCode(record, { ok: true, error: null });
20995
+ }
20996
+ }
20997
+ /** Resolve every `updateCode` caller waiting on this record's chain of edits. */
20998
+ settleCode(record, result) {
20999
+ const waiters = record.codeWaiters;
21000
+ record.codeWaiters = void 0;
21001
+ if (waiters) for (const resolve of waiters) resolve(result);
21002
+ }
21003
+ /**
21004
+ * An update failed — at prepare, or on trial before its first model. A trial puts the
21005
+ * previous code back (source, engine, prepared script, values, schema) and restarts it;
21006
+ * its visuals were never dropped, so the chart keeps painting throughout. Then the
21007
+ * failure reports the usual way and every waiting caller learns it.
21008
+ */
21009
+ rejectCode(id, record, handle, err) {
21010
+ const trial = record.codeTrial;
21011
+ record.codeTrial = void 0;
21012
+ this.fail(id, handle, err);
21013
+ if (trial) {
21014
+ const p = trial.previous;
21015
+ record.session?.stop();
21016
+ record.session = void 0;
21017
+ record.source = p.source;
21018
+ record.engine = p.engine;
21019
+ record.prepared = p.prepared;
21020
+ record.inputValues = p.inputValues;
21021
+ record.propValues = p.propValues;
21022
+ handle.setSource(p.source);
21023
+ handle.setSchema(p.inputs);
21024
+ handle.setPropsSchema(p.props);
21025
+ record.pendingStructural = true;
21026
+ record.pendingCause = void 0;
21027
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
21028
+ if (!record.hidden) this.executeIndicator(id, handle);
21029
+ }
21030
+ const error = err instanceof Error ? err : new Error(String(err));
21031
+ this.settleCode(record, { ok: false, error });
20765
21032
  }
20766
21033
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
20767
21034
  /** Live handles of every indicator on the chart (script + native), insertion order. */
@@ -20776,6 +21043,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20776
21043
  removeIndicator(id) {
20777
21044
  const record = this.registry.remove(id);
20778
21045
  if (record?.native?.type === "volume") this.volumeOptedOut = true;
21046
+ if (record) this.settleCode(record, { ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator was removed.`) });
20779
21047
  record?.session?.stop();
20780
21048
  if (record?.native?.started) record.native.instance.stop();
20781
21049
  this.handles.delete(id);
@@ -20803,6 +21071,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
20803
21071
  if (!visible) {
20804
21072
  record.session?.stop();
20805
21073
  record.session = void 0;
21074
+ if (record.codeTrial) {
21075
+ record.codeTrial = void 0;
21076
+ this.settleCode(record, { ok: true, error: null });
21077
+ }
20806
21078
  if (record.native?.started) record.native.instance.suspend();
20807
21079
  if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
20808
21080
  else if (record.native) this.mountHiddenNativeRow(id, record);
@@ -20890,6 +21162,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20890
21162
  this.viewportUnsub?.();
20891
21163
  this.paneActionUnsub?.();
20892
21164
  for (const record of this.registry.all()) {
21165
+ this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
20893
21166
  record.session?.stop();
20894
21167
  if (record.native?.started) record.native.instance.stop();
20895
21168
  }
@@ -20958,7 +21231,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
20958
21231
  const record = this.registry.get(id);
20959
21232
  if (!record || !record.engine || !record.prepared || record.hidden) return;
20960
21233
  const mode = this.config.live && !record.prepared.reactsToViewport && record.engine.capabilities.streaming ? "live" : "static";
20961
- record.session = record.engine.execute(
21234
+ const prepared = record.prepared;
21235
+ const session = record.engine.execute(
20962
21236
  {
20963
21237
  prepared: record.prepared,
20964
21238
  market: this.market(),
@@ -20979,6 +21253,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
20979
21253
  const cause = record.pendingCause ?? "history";
20980
21254
  if (!this.applyModel(id, model)) return;
20981
21255
  record.pendingCause = void 0;
21256
+ if (record.codeTrial) {
21257
+ record.codeTrial = void 0;
21258
+ this.settleCode(record, { ok: true, error: null });
21259
+ }
20982
21260
  this.emitContextChanged(id);
20983
21261
  this.emitScriptRun(id, cause, first);
20984
21262
  },
@@ -20989,9 +21267,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
20989
21267
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
20990
21268
  },
20991
21269
  onWarning: (w) => this.events.emit("warning", w),
20992
- onError: (err) => this.fail(id, handle, err)
21270
+ onError: (err) => {
21271
+ if (record.codeTrial) this.rejectCode(id, record, handle, err);
21272
+ else this.fail(id, handle, err);
21273
+ }
20993
21274
  }
20994
21275
  );
21276
+ if (record.prepared !== prepared) {
21277
+ session.stop();
21278
+ return;
21279
+ }
21280
+ record.session = session;
20995
21281
  this.emitContextChanged(id);
20996
21282
  }
20997
21283
  /**
@@ -21492,7 +21778,7 @@ function yieldToPaint() {
21492
21778
  }
21493
21779
  return new Promise((resolve) => setTimeout(resolve, 0));
21494
21780
  }
21495
- function valuesOnSchema(schema, previous) {
21781
+ function valuesOnSchema(schema, previous, oldSchema = []) {
21496
21782
  const out = {};
21497
21783
  const declared = /* @__PURE__ */ new Set();
21498
21784
  for (const s of schema) {
@@ -21500,9 +21786,22 @@ function valuesOnSchema(schema, previous) {
21500
21786
  declared.add(s.key);
21501
21787
  declared.add(s.title);
21502
21788
  }
21503
- for (const [k, v] of Object.entries(previous)) if (declared.has(k)) out[k] = v;
21789
+ const oldDefaults = /* @__PURE__ */ new Map();
21790
+ for (const s of oldSchema) {
21791
+ oldDefaults.set(s.key, s.defval);
21792
+ if (!oldDefaults.has(s.title)) oldDefaults.set(s.title, s.defval);
21793
+ }
21794
+ for (const [k, v] of Object.entries(previous)) {
21795
+ if (!declared.has(k)) continue;
21796
+ if (oldDefaults.has(k) && sameInputValue(oldDefaults.get(k), v)) continue;
21797
+ out[k] = v;
21798
+ }
21504
21799
  return out;
21505
21800
  }
21801
+ function sameInputValue(a, b) {
21802
+ if (a === b) return true;
21803
+ return typeof a === "object" && a !== null && typeof b === "object" && b !== null && JSON.stringify(a) === JSON.stringify(b);
21804
+ }
21506
21805
  function blankedModel(model) {
21507
21806
  return {
21508
21807
  ...model,
@@ -24432,15 +24731,23 @@ var Batch = class {
24432
24731
  this.seg(x + w, y + h, x, y + h, lineWidth, c);
24433
24732
  this.seg(x, y + h, x, y, lineWidth, c);
24434
24733
  }
24435
- /** Filled circle (triangle fan). */
24734
+ /**
24735
+ * Filled circle (triangle fan), feathered like a line: the rim is padded by {@link AA_PAD} and
24736
+ * edgeDist runs from 0 at the center to the padded radius at the rim, with edgeHalf = r. The
24737
+ * distance grows linearly along each spoke, so the shader's line feather gives a round soft edge.
24738
+ */
24436
24739
  circle(cx, cy, r, c, segments = 16) {
24437
- let px = cx + r;
24740
+ const ext = r + AA_PAD;
24741
+ let px = cx + ext;
24438
24742
  let py = cy;
24743
+ this.ensure(segments * 3);
24439
24744
  for (let i = 1; i <= segments; i += 1) {
24440
24745
  const a = i / segments * Math.PI * 2;
24441
- const nx = cx + Math.cos(a) * r;
24442
- const ny = cy + Math.sin(a) * r;
24443
- this.tri(cx, cy, c, px, py, c, nx, ny, c);
24746
+ const nx = cx + Math.cos(a) * ext;
24747
+ const ny = cy + Math.sin(a) * ext;
24748
+ this.v(cx, cy, c, 0, r);
24749
+ this.v(px, py, c, ext, r);
24750
+ this.v(nx, ny, c, ext, r);
24444
24751
  px = nx;
24445
24752
  py = ny;
24446
24753
  }
@@ -24621,7 +24928,7 @@ var WebGL2Backend = class {
24621
24928
  }
24622
24929
  mount(canvas) {
24623
24930
  this.canvas = canvas;
24624
- this.gl = canvas.getContext("webgl2", { alpha: true, antialias: true, depth: false, stencil: false, premultipliedAlpha: true });
24931
+ this.gl = canvas.getContext("webgl2", { alpha: true, antialias: false, depth: false, stencil: false, premultipliedAlpha: true });
24625
24932
  if (!this.gl) return;
24626
24933
  if (!this.initGL()) {
24627
24934
  this.gl = null;
@@ -25880,6 +26187,12 @@ var InputController = class {
25880
26187
  this.lastTapT = 0;
25881
26188
  this.lastTapX = 0;
25882
26189
  this.lastTapY = 0;
26190
+ // Whether the last two primary presses were claimed by the drawings layer. A double-click
26191
+ // is the last press plus the one before it; a tool-driven click pair must never fall through
26192
+ // to the pane maximize toggle, even when the tool disarmed itself between the two clicks
26193
+ // (a finished placement leaves plain cursor mode for the second click).
26194
+ this.lastPressDrawing = false;
26195
+ this.prevPressDrawing = false;
25883
26196
  this.onTouchContextMenu = (e) => {
25884
26197
  if (this.touches.size === 0) return;
25885
26198
  if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
@@ -25934,19 +26247,24 @@ var InputController = class {
25934
26247
  this.moved = false;
25935
26248
  this.startX = x;
25936
26249
  this.startY = y;
26250
+ this.prevPressDrawing = this.lastPressDrawing;
26251
+ this.lastPressDrawing = false;
25937
26252
  if (this.deps.drawingsClaim?.(x, y)) {
25938
26253
  this.region = "drawing";
26254
+ this.lastPressDrawing = true;
25939
26255
  this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
25940
26256
  this.capture(e.pointerId);
25941
26257
  return;
25942
26258
  }
25943
26259
  if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
25944
26260
  this.region = "drawing";
26261
+ this.lastPressDrawing = true;
25945
26262
  this.capture(e.pointerId);
25946
26263
  return;
25947
26264
  }
25948
26265
  if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
25949
26266
  this.region = "drawing";
26267
+ this.lastPressDrawing = true;
25950
26268
  this.capture(e.pointerId);
25951
26269
  return;
25952
26270
  }
@@ -26062,7 +26380,7 @@ var InputController = class {
26062
26380
  this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
26063
26381
  } else if (tapRelease && this.region === "data") {
26064
26382
  this.deps.onClick(x, y);
26065
- } else if (this.dragging && this.region === "data") {
26383
+ } else if (this.dragging && this.region === "data" && wasTouch) {
26066
26384
  const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
26067
26385
  if (!stale && Math.abs(this.vx) > FLING_MIN_SPEED) {
26068
26386
  const pitch = this.deps.getCoords().pxPerBar();
@@ -26268,6 +26586,7 @@ var InputController = class {
26268
26586
  if (region === "price") this.deps.resetPriceScale(x, y);
26269
26587
  else if (region === "separator") this.deps.resetPaneSize(y);
26270
26588
  else if (region === "time") this.deps.resetView();
26589
+ else if (this.lastPressDrawing || this.prevPressDrawing) return;
26271
26590
  else this.deps.dataDblClick(x, y);
26272
26591
  }
26273
26592
  };
@@ -28433,6 +28752,7 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
28433
28752
  const step = pickStep(span / Math.max(1, target));
28434
28753
  const zFrom = fromMs + offsetMs;
28435
28754
  const zTo = toMs + offsetMs;
28755
+ if (step >= MONTH) return calendarTicks(zFrom, zTo, step >= YEAR ? 12 : step >= 3 * MONTH ? 3 : 1, offsetMs);
28436
28756
  const first = Math.ceil(zFrom / step) * step;
28437
28757
  const out = [];
28438
28758
  for (let zt = first; zt <= zTo; zt += step) {
@@ -28463,6 +28783,18 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
28463
28783
  }
28464
28784
  return out;
28465
28785
  }
28786
+ function calendarTicks(zFrom, zTo, months, offsetMs) {
28787
+ const d0 = new Date(zFrom);
28788
+ let idx = d0.getUTCFullYear() * 12 + d0.getUTCMonth();
28789
+ if (Date.UTC(Math.floor(idx / 12), idx % 12, 1) < zFrom) idx += 1;
28790
+ idx = Math.ceil(idx / months) * months;
28791
+ const out = [];
28792
+ 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)) {
28793
+ const label = months === 12 ? String(Math.floor(idx / 12)) : MONTHS[idx % 12];
28794
+ out.push({ time: zt - offsetMs, label, major: true });
28795
+ }
28796
+ return out;
28797
+ }
28466
28798
 
28467
28799
  // src/renderers/native/chrome/countdown.ts
28468
28800
  function countdownText(barOpen, barMs, now2) {
@@ -32414,6 +32746,12 @@ var DrawingInteraction = class {
32414
32746
  activeDragId() {
32415
32747
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
32416
32748
  }
32749
+ /** The store drawings a live drag is moving — the pressed one and its riders — or none
32750
+ * before the drag passes the slop. A Ctrl-drag moves copies, so its sources stay put. */
32751
+ movingIds() {
32752
+ if (this.state.kind !== "pressed" || !this.state.moved || this.state.clones) return /* @__PURE__ */ new Set();
32753
+ return /* @__PURE__ */ new Set([this.state.id, ...this.state.riders.map((r) => r.id)]);
32754
+ }
32417
32755
  /** The drawing under a press that has not been released yet (drag or click undecided), or null. */
32418
32756
  pressedId() {
32419
32757
  return this.state.kind === "pressed" ? this.state.id : null;
@@ -33072,6 +33410,17 @@ var DrawingSettingsDialog = class {
33072
33410
  }
33073
33411
  }));
33074
33412
  }
33413
+ if (drawing instanceof FibRetracement) {
33414
+ const fib = drawing;
33415
+ grid.appendChild(fieldRow({
33416
+ label: "Reverse",
33417
+ bool: true,
33418
+ toggle: {
33419
+ checked: fib.reverse,
33420
+ onChange: (v) => actions.patch({ reverse: v })
33421
+ }
33422
+ }));
33423
+ }
33075
33424
  levels.forEach((lv, i) => {
33076
33425
  const row = document.createElement("div");
33077
33426
  row.className = "vela-field-span";
@@ -34225,6 +34574,8 @@ var UserDrawingController = class {
34225
34574
  this.drawings = [];
34226
34575
  /** Ids painted on an interleave layer this frame — the top canvas paints only their handles. */
34227
34576
  this.sliced = /* @__PURE__ */ new Set();
34577
+ /** Interleaved drawings mid-drag: painted on the top canvas, left out of their slice. */
34578
+ this.lifted = /* @__PURE__ */ new Set();
34228
34579
  /** Cached slice canvases, keyed `paneId|beforeZ`, reused across frames to avoid churn. */
34229
34580
  this.sliceCache = /* @__PURE__ */ new Map();
34230
34581
  /** Series boundaries per pane as of the last `prepareSlices` — lets a repaint between data
@@ -34312,7 +34663,7 @@ var UserDrawingController = class {
34312
34663
  selectedIds: () => this.selectedIds,
34313
34664
  emit: (i) => this.emit(i),
34314
34665
  changed: () => {
34315
- this.invalidateSlices();
34666
+ this.syncLift();
34316
34667
  this.render();
34317
34668
  },
34318
34669
  openSettings: (id, x, y) => this.openSettingsById(id, x, y),
@@ -34938,6 +35289,20 @@ var UserDrawingController = class {
34938
35289
  /** A drawing that paints inside the series stack changed (content, not hover): its pixels
34939
35290
  * live in the backend composite, so this layer alone can't show the change — ask for a
34940
35291
  * data frame, which re-runs `prepareSlices` before the backend composites. */
35292
+ /** A drawing being dragged leaves its interleave slice for the top canvas, so each pointer
35293
+ * move repaints one 2D layer instead of re-rasterizing and re-uploading a plot-sized slice
35294
+ * texture through the data frame. The slices rebuild once as the drag starts and once as it
35295
+ * ends (release, cancel), when the lifted set changes. */
35296
+ syncLift() {
35297
+ const moving = this.interaction.movingIds();
35298
+ let same = moving.size === this.lifted.size;
35299
+ if (same) {
35300
+ for (const id of moving) if (!this.lifted.has(id)) same = false;
35301
+ }
35302
+ if (same) return;
35303
+ this.lifted = new Set(moving);
35304
+ this.invalidateSlices();
35305
+ }
34941
35306
  invalidateSlices() {
34942
35307
  if (this.sliced.size > 0 || this.drawings.some((d) => this.isInterleaved(d))) this.deps.requestDataPaint();
34943
35308
  }
@@ -34956,7 +35321,7 @@ var UserDrawingController = class {
34956
35321
  const theme = this.deps.theme();
34957
35322
  const buckets = /* @__PURE__ */ new Map();
34958
35323
  for (const d of this.drawings) {
34959
- if (!d.visible) continue;
35324
+ if (!d.visible || this.lifted.has(d.id)) continue;
34960
35325
  const beforeZ = sliceKeyFor(d.zIndex, this.lastBounds.get(d.paneId) ?? []);
34961
35326
  if (beforeZ === null) continue;
34962
35327
  const key = `${d.paneId}|${beforeZ}`;
@@ -35008,8 +35373,9 @@ var UserDrawingController = class {
35008
35373
  mutedLabel: edited instanceof TextLabel ? edited.id : null
35009
35374
  };
35010
35375
  this.painter.seriesLook = this.deps.seriesLook();
35011
- this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
35012
- this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
35376
+ const onTop = (d) => !this.isInterleaved(d) || this.lifted.has(d.id);
35377
+ this.painter.paintAll(ctx, this.drawings.filter(onTop), proj, this.deps.theme(), targets);
35378
+ this.painter.paintHighlights(ctx, this.drawings.filter((d) => !onTop(d)), proj, handleIdsFor(targets));
35013
35379
  const clones = this.interaction.dragClones();
35014
35380
  if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
35015
35381
  this.layoutTextEditor();
@@ -44665,6 +45031,9 @@ var CSS21 = `
44665
45031
  .vela-statusline .vela-sl-market { align-self: center; display: inline-flex; }
44666
45032
  .vela-statusline .vela-sl-market > [hidden] { display: none !important; }
44667
45033
  .vela-statusline .vela-sl-replay-badge, .vela-statusline .vela-sl-replay-badge svg { display: block; width: 16px; height: 16px; }
45034
+ /* Tabular figures: every digit has one advance, so a readout whose text changes only in
45035
+ * its digits keeps its width \u2014 render() skips the ladder for those (see widthKey). */
45036
+ .vela-statusline .vela-sl-values { font-variant-numeric: tabular-nums; }
44668
45037
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
44669
45038
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
44670
45039
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
@@ -44782,6 +45151,10 @@ var Statusline = class {
44782
45151
  this.fitRO = null;
44783
45152
  /** The ladder rung currently applied — see {@link fit}. */
44784
45153
  this.layout = { stacked: false, level: "full" };
45154
+ /** The readout {@link widthKey} the applied rung was fitted to. */
45155
+ this.fitKey = null;
45156
+ /** Web fonts landing after the last fit change every width without a resize. */
45157
+ this.onFontsLoaded = () => this.fit();
44785
45158
  this.onContextMenu = (e) => {
44786
45159
  if (!this.menu || !this.menuHooks) return;
44787
45160
  e.preventDefault();
@@ -44845,6 +45218,7 @@ var Statusline = class {
44845
45218
  this.fitRO = new ResizeObserver(() => this.fit());
44846
45219
  this.fitRO.observe(host);
44847
45220
  }
45221
+ doc.fonts?.addEventListener("loadingdone", this.onFontsLoaded);
44848
45222
  this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
44849
45223
  this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
44850
45224
  this.setMarketStatus("open");
@@ -44897,6 +45271,7 @@ var Statusline = class {
44897
45271
  * (detached host, node tests) nothing overflows, so the widest rung stays.
44898
45272
  */
44899
45273
  fit() {
45274
+ this.fitKey = this.widthKey();
44900
45275
  this.syncParts();
44901
45276
  const seg = segmentVisibility(this.parts, this.chartHidden);
44902
45277
  const overflows = () => this.el.scrollWidth > this.el.clientWidth;
@@ -45053,6 +45428,7 @@ var Statusline = class {
45053
45428
  this.detach();
45054
45429
  this.fitRO?.disconnect();
45055
45430
  this.fitRO = null;
45431
+ this.el.ownerDocument.fonts?.removeEventListener("loadingdone", this.onFontsLoaded);
45056
45432
  this.el.removeEventListener("contextmenu", this.onContextMenu);
45057
45433
  this.menu?.destroy();
45058
45434
  this.menu = null;
@@ -45068,7 +45444,22 @@ var Statusline = class {
45068
45444
  }
45069
45445
  render() {
45070
45446
  if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
45071
- this.fit();
45447
+ if (this.widthKey() === this.fitKey) this.renderValues();
45448
+ else this.fit();
45449
+ }
45450
+ /** Everything the readout's width depends on, at every rung: its shape, and each
45451
+ * value's sign, digit count and decimals (the digits themselves are tabular — see
45452
+ * the CSS; the grouping commas follow from the digit count). `toFixed` carries all
45453
+ * three at a fraction of the locale formatter's cost — this runs per pointer move.
45454
+ * The parts, the host's size and the fonts re-fit through their own paths. */
45455
+ widthKey() {
45456
+ const bar = this.hoverBar ?? this.lastBar;
45457
+ if (!bar) return this.readout;
45458
+ const dp = decimalsFor(bar.close);
45459
+ const diff = bar.close - bar.open;
45460
+ const text = [bar.open, bar.high, bar.low, bar.close, diff].map((v) => v.toFixed(dp));
45461
+ text.push((diff / bar.open * 100).toFixed(2));
45462
+ return `${this.readout} ${text.join(" ")}`.replace(/\d/g, "0");
45072
45463
  }
45073
45464
  /** Write the value readout for the current bar at the current ladder level. */
45074
45465
  renderValues() {
@@ -46702,7 +47093,7 @@ var ChartCell = class {
46702
47093
  * then the manifest in the host's own order. */
46703
47094
  libraryRows() {
46704
47095
  return [
46705
- ...this.supportedNatives().map((n) => ({ name: n.title, category: "Vela", native: true, nativeType: n.type, beta: n.beta })),
47096
+ ...this.supportedNatives().map((n) => ({ name: n.title, category: "Built-in", native: true, nativeType: n.type, beta: n.beta })),
46706
47097
  ...this.manifest.map((e) => ({ name: e.name, language: e.language, category: e.category }))
46707
47098
  ];
46708
47099
  }
@@ -47708,11 +48099,11 @@ function resolveTyping(key, open2) {
47708
48099
  if (open2.dialogs > 0) {
47709
48100
  if (key.length !== 1) return null;
47710
48101
  if (open2.symbolSearch) return { target: "symbol", text: key.toUpperCase() };
47711
- if (open2.timeframeEntry) return { target: "timeframe", text: key };
48102
+ if (open2.timeframeEntry) return { target: "timeframe", text: key.toUpperCase() };
47712
48103
  return null;
47713
48104
  }
47714
- if (/^[a-zA-Z]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
47715
- if (/^[0-9]$/.test(key)) return { target: "timeframe", text: key };
48105
+ if (/^[a-zA-Z0]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
48106
+ if (/^[1-9]$/.test(key)) return { target: "timeframe", text: key };
47716
48107
  return null;
47717
48108
  }
47718
48109
  var VelaWorkspace = class {
@@ -49450,7 +49841,7 @@ var VelaWorkspace = class {
49450
49841
  }
49451
49842
  });
49452
49843
  }
49453
- /** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
49844
+ /** Bare-typing router: letters and `0` → symbol search (seeded), digits 1–9 → timeframe entry. */
49454
49845
  routeTyping(ev) {
49455
49846
  if (this.destroyed) return;
49456
49847
  if (ev.ctrlKey || ev.metaKey || ev.altKey) return;