@luxalgo/vela 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist/{DataProvider-DwFIoV4r.d.ts → DataProvider-oH78tYXL.d.ts} +1 -1
  3. package/dist/{DataProvider-BrlxntPN.d.cts → DataProvider-wbtJ3-Sa.d.cts} +1 -1
  4. package/dist/{chunk-DBDXKZUF.js → chunk-2IPXYM4B.js} +306 -177
  5. package/dist/{chunk-NELQJCGK.js → chunk-G7B7ZCBF.js} +167 -10
  6. package/dist/{chunk-ATGTCWHJ.js → chunk-HMHA733L.js} +473 -196
  7. package/dist/{chunk-EZ5FWVLA.js → chunk-KWI3YDBW.js} +343 -17
  8. package/dist/{chunk-LMBYEGUR.js → chunk-TO3GIK7Z.js} +71 -6
  9. package/dist/{chunk-BZQM2XO7.js → chunk-VHGACEHO.js} +62 -1
  10. package/dist/{contributions-tjNExo1o.d.ts → contributions-BhOBRenr.d.cts} +79 -12
  11. package/dist/{contributions-CtlptxR8.d.cts → contributions-HGqnNb_P.d.ts} +79 -12
  12. package/dist/index.cjs +733 -113
  13. package/dist/index.d.cts +34 -14
  14. package/dist/index.d.ts +34 -14
  15. package/dist/index.js +5 -5
  16. package/dist/{options-BnEPUuVV.d.ts → options-lMUwmRwh.d.cts} +69 -4
  17. package/dist/{options-BnEPUuVV.d.cts → options-lMUwmRwh.d.ts} +69 -4
  18. package/dist/{plugin-e2WUEtiI.d.ts → plugin-Cr2Pj8q8.d.ts} +4 -4
  19. package/dist/{plugin-BWxJRKzS.d.cts → plugin-Do5J5xuF.d.cts} +4 -4
  20. package/dist/plugin.cjs +38 -6
  21. package/dist/plugin.d.cts +5 -5
  22. package/dist/plugin.d.ts +5 -5
  23. package/dist/plugin.js +3 -3
  24. package/dist/providers/binance.d.cts +2 -2
  25. package/dist/providers/binance.d.ts +2 -2
  26. package/dist/providers/coinbase.d.cts +2 -2
  27. package/dist/providers/coinbase.d.ts +2 -2
  28. package/dist/providers/hyperliquid.d.cts +2 -2
  29. package/dist/providers/hyperliquid.d.ts +2 -2
  30. package/dist/{side-panel-HF0IAzwf.d.cts → side-panel-_2UK1gSm.d.cts} +58 -3
  31. package/dist/{side-panel-HF0IAzwf.d.ts → side-panel-_2UK1gSm.d.ts} +58 -3
  32. package/dist/{statusline-model-DFY7tuHj.d.cts → statusline-model-B-2xGlB_.d.cts} +3 -3
  33. package/dist/{statusline-model-zKXtGIjj.d.ts → statusline-model-CY1wzxHg.d.ts} +3 -3
  34. package/dist/ui.cjs +298 -13
  35. package/dist/ui.d.cts +132 -74
  36. package/dist/ui.d.ts +132 -74
  37. package/dist/ui.js +3 -3
  38. package/dist/vela.global.js +734 -114
  39. package/dist/vela.global.min.js +86 -52
  40. package/dist/widget.cjs +1227 -211
  41. package/dist/widget.d.cts +36 -12
  42. package/dist/widget.d.ts +36 -12
  43. package/dist/widget.js +7 -7
  44. package/dist/workspace.cjs +1226 -211
  45. package/dist/workspace.d.cts +47 -20
  46. package/dist/workspace.d.ts +47 -20
  47. package/dist/workspace.js +6 -6
  48. package/package.json +2 -2
  49. /package/dist/{chunk-JU4CWEBF.js → chunk-W5YPYZ2P.js} +0 -0
package/dist/ui.cjs CHANGED
@@ -335,6 +335,7 @@ registerIcon("expand", S('<rect x="2.2" y="2.2" width="11.6" height="11.6" rx="1
335
335
  registerIcon("plus", S('<path d="M8 2.8v10.4M2.8 8h10.4"/>'));
336
336
  registerIcon("minus", S('<path d="M2.8 8h10.4"/>'));
337
337
  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"/>'));
338
+ 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"/>'));
338
339
  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"/>'));
339
340
  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"/>'));
340
341
  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"'));
@@ -416,6 +417,17 @@ function nextUid(prefix) {
416
417
  return `${prefix}-${++uid}`;
417
418
  }
418
419
 
420
+ // src/ui/surface-events.ts
421
+ var SURFACE_OPEN_EVENT = "vela:surface-open";
422
+ var SURFACE_CLOSE_EVENT = "vela:surface-close";
423
+ function announceSurface(el, open2, kind, trigger = null) {
424
+ const View = el.ownerDocument.defaultView;
425
+ if (!View) return;
426
+ el.dispatchEvent(
427
+ new View.CustomEvent(open2 ? SURFACE_OPEN_EVENT : SURFACE_CLOSE_EVENT, { bubbles: true, composed: true, detail: { kind, trigger } })
428
+ );
429
+ }
430
+
419
431
  // src/ui/keymap.ts
420
432
  var KEY_ALIASES = {
421
433
  esc: "escape",
@@ -632,6 +644,55 @@ var Tooltip = class {
632
644
  this.trigger.removeAttribute("data-scope");
633
645
  }
634
646
  };
647
+
648
+ // src/ui/surface-exit.ts
649
+ var CLOSING_ATTR = "data-closing";
650
+ var SURFACE_EXIT_CAP_MS = 1e3;
651
+ function animationsOf(els) {
652
+ return els.flatMap((el) => el.getAnimations({ subtree: true }));
653
+ }
654
+ function ends(a) {
655
+ return Number.isFinite(a.effect?.getComputedTiming().endTime ?? Number.POSITIVE_INFINITY);
656
+ }
657
+ function holdForExit(els, done, opts = {}) {
658
+ const list = Array.isArray(els) ? els : [els];
659
+ if (list.length === 0 || !list.every((el) => typeof el.getAnimations === "function")) {
660
+ done();
661
+ return null;
662
+ }
663
+ const before = new Set(animationsOf(list));
664
+ for (const el of list) el.setAttribute(CLOSING_ATTR, "");
665
+ const exit = animationsOf(list).filter((a) => !before.has(a) && ends(a));
666
+ if (exit.length === 0) {
667
+ for (const el of list) el.removeAttribute(CLOSING_ATTR);
668
+ done();
669
+ return null;
670
+ }
671
+ const frozen = [...list, ...opts.inert ?? []];
672
+ const wasInert = frozen.map((el) => el.inert);
673
+ for (const el of frozen) el.inert = true;
674
+ let settled = false;
675
+ const settle = (complete) => {
676
+ if (settled) return;
677
+ settled = true;
678
+ clearTimeout(cap);
679
+ for (const el of list) el.removeAttribute(CLOSING_ATTR);
680
+ frozen.forEach((el, i) => {
681
+ el.inert = wasInert[i];
682
+ });
683
+ if (complete) done();
684
+ };
685
+ const cap = setTimeout(() => settle(true), SURFACE_EXIT_CAP_MS);
686
+ let pending = exit.length;
687
+ const one = () => {
688
+ if (--pending === 0) settle(true);
689
+ };
690
+ for (const a of exit) a.finished.then(one, one);
691
+ return {
692
+ cancel: () => settle(false),
693
+ finish: () => settle(true)
694
+ };
695
+ }
635
696
  function menuController(opts) {
636
697
  return {
637
698
  machine: menu__namespace.machine,
@@ -713,6 +774,35 @@ var MENU_CSS = `
713
774
  color: var(--vela-fg-bright);
714
775
  }
715
776
  .vela-menu-item[data-checkmark] { color: var(--vela-fg-bright); }
777
+ /* Badge mode (iconBadges): the row icon sits in a rounded tile and the row grows to fit
778
+ it. An icon-less row of the same level keeps an invisible slot, so labels align. */
779
+ .vela-menu[data-badges] > .vela-menu-item { padding: 5px 10px 5px 6px; }
780
+ .vela-menu-item .vela-menu-badge {
781
+ width: 24px;
782
+ height: 24px;
783
+ padding: 3px;
784
+ flex: none;
785
+ box-sizing: border-box;
786
+ display: inline-flex;
787
+ align-items: center;
788
+ justify-content: center;
789
+ border-radius: var(--vela-radius-md);
790
+ background: var(--vela-hover);
791
+ border: 1px solid var(--vela-border);
792
+ color: var(--vela-fg-muted);
793
+ transition: background var(--vela-dur-fast) var(--vela-ease), border-color var(--vela-dur-fast) var(--vela-ease), color var(--vela-dur-fast) var(--vela-ease);
794
+ }
795
+ .vela-menu-item .vela-menu-badge:empty { background: transparent; border-color: transparent; }
796
+ .vela-menu-item .vela-menu-badge .vela-icon { color: inherit; }
797
+ .vela-menu-item[data-highlighted] .vela-menu-badge:not(:empty) { color: var(--vela-fg-bright); }
798
+ /* The selected tile inverts: a bright fill under the surface-colored glyph. After the
799
+ hover rule, so a selected row keeps its tile while highlighted. */
800
+ .vela-menu-item[data-checked] .vela-menu-badge:not(:empty),
801
+ .vela-menu-item[data-checkmark] .vela-menu-badge:not(:empty) {
802
+ background: color-mix(in srgb, var(--vela-fg-bright) 83%, var(--vela-surface));
803
+ border-color: color-mix(in srgb, var(--vela-fg-bright) 40%, transparent);
804
+ color: var(--vela-surface);
805
+ }
716
806
  /* Switch rows (boolean settings in a dropdown): a right-aligned toggle pill \u2014 the
717
807
  same control language as the settings dialog's toggles. */
718
808
  .vela-menu-switch {
@@ -777,11 +867,16 @@ var Surface = class _Surface {
777
867
  /** Trigger rect captured when this level opened — the list stays put if the
778
868
  * trigger then moves (favorite chips shifting the caret). */
779
869
  this.pinnedAnchor = null;
870
+ /** The open state last announced — see {@link announce}. */
871
+ this.shown = false;
872
+ /** The list's exit animation after a close; a reopen cancels it. */
873
+ this.exit = null;
780
874
  this.doc = doc;
781
875
  this.host = opts.host;
782
876
  this.onSelect = opts.onSelect;
783
877
  this.onFavorite = opts.onFavorite;
784
878
  this.checkmarks = opts.checkmarks === true;
879
+ this.iconBadges = opts.iconBadges === true;
785
880
  this.positioner = doc.createElement("div");
786
881
  this.positioner.className = "vela-ui-layer";
787
882
  this.list = doc.createElement("ul");
@@ -790,6 +885,7 @@ var Surface = class _Surface {
790
885
  this.positioner.appendChild(this.list);
791
886
  this.host.appendChild(this.positioner);
792
887
  const trigger = opts.trigger;
888
+ this.opener = trigger ?? opts.opener ?? null;
793
889
  this.ctrl = menuController({
794
890
  items: [],
795
891
  id: opts.id,
@@ -811,15 +907,37 @@ var Surface = class _Surface {
811
907
  if (opts.triggerId && trigger) trigger.id = opts.triggerId;
812
908
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
813
909
  const api = this.ctrl.connect(service);
910
+ const closing = this.shown && !api.open;
911
+ if (closing) this.announce(false);
912
+ if (api.open && this.exit) {
913
+ this.exit.cancel();
914
+ this.exit = null;
915
+ }
814
916
  if (trigger) vanilla.spreadProps(trigger, api.getTriggerProps(), this.mid);
815
917
  vanilla.spreadProps(this.positioner, api.getPositionerProps(), this.mid);
816
918
  vanilla.spreadProps(this.list, api.getContentProps(), this.mid);
817
919
  this.project(api);
920
+ if (closing) this.holdExit();
921
+ if (api.open && !this.shown) this.announce(true);
922
+ });
923
+ }
924
+ /** Keep the just-closed list up through its exit animation. `spreadProps` re-applies
925
+ * `hidden` only when the machine's value for it changes, so the list stays shown until
926
+ * the exit ends — or until a reopen, which projects `hidden: false` itself. */
927
+ holdExit() {
928
+ this.list.hidden = false;
929
+ this.exit = holdForExit(this.list, () => {
930
+ this.exit = null;
931
+ if (!this.shown) this.list.hidden = true;
818
932
  });
819
933
  }
820
934
  get api() {
821
935
  return this.ctrl.connect(this.handle.service);
822
936
  }
937
+ announce(open2) {
938
+ this.shown = open2;
939
+ announceSurface(this.list, open2, "menu", this.opener);
940
+ }
823
941
  setItems(items) {
824
942
  this.items = items;
825
943
  for (const sub of this.subs.values()) sub.destroy();
@@ -836,7 +954,9 @@ var Surface = class _Surface {
836
954
  destroy() {
837
955
  for (const sub of this.subs.values()) sub.destroy();
838
956
  this.subs.clear();
957
+ this.exit?.finish();
839
958
  this.handle.stop();
959
+ if (this.shown) this.announce(false);
840
960
  this.positioner.remove();
841
961
  }
842
962
  byId(id) {
@@ -861,6 +981,10 @@ var Surface = class _Surface {
861
981
  const doc = this.doc;
862
982
  this.list.replaceChildren();
863
983
  const markable = this.checkmarks && this.items.some((i) => !(i.submenu && i.submenu.length > 0) && !i.toggle && i.checked !== void 0);
984
+ const iconic = this.items.some((i) => !!i.icon);
985
+ const badged = this.iconBadges && iconic;
986
+ if (badged) this.list.dataset.badges = "1";
987
+ else delete this.list.dataset.badges;
864
988
  for (const item of this.items) {
865
989
  if (item.separatorBefore) {
866
990
  const sep = doc.createElement("li");
@@ -886,7 +1010,16 @@ var Surface = class _Surface {
886
1010
  } else if (!branch && !item.toggle && item.checked) {
887
1011
  li.dataset.checked = "1";
888
1012
  }
889
- if (item.icon) li.appendChild(iconEl(item.icon, doc));
1013
+ if (badged) {
1014
+ const badge = doc.createElement("span");
1015
+ badge.className = "vela-menu-badge";
1016
+ if (item.icon) badge.appendChild(iconEl(item.icon, doc));
1017
+ li.appendChild(badge);
1018
+ } else if (item.icon) {
1019
+ li.appendChild(iconEl(item.icon, doc));
1020
+ } else if (iconic) {
1021
+ li.appendChild(iconEl("", doc));
1022
+ }
890
1023
  const label = doc.createElement("span");
891
1024
  label.className = "vela-menu-label";
892
1025
  label.textContent = item.label;
@@ -930,9 +1063,11 @@ var Surface = class _Surface {
930
1063
  const sub = new _Surface(doc, {
931
1064
  host: this.host,
932
1065
  placement: "right-start",
1066
+ opener: li,
933
1067
  onSelect: this.onSelect,
934
1068
  onFavorite: this.onFavorite,
935
1069
  checkmarks: this.checkmarks,
1070
+ iconBadges: this.iconBadges,
936
1071
  id: `${this.mid}--${item.id}`
937
1072
  });
938
1073
  sub.setItems(item.submenu ?? []);
@@ -958,7 +1093,8 @@ var Menu = class {
958
1093
  id: opts.id,
959
1094
  minWidth: opts.minWidth,
960
1095
  onFavorite: opts.onFavorite,
961
- checkmarks: opts.checkmarks
1096
+ checkmarks: opts.checkmarks,
1097
+ iconBadges: opts.iconBadges
962
1098
  });
963
1099
  this.root.setItems(opts.items);
964
1100
  }
@@ -1062,6 +1198,7 @@ var POPOVER_CSS = `
1062
1198
 
1063
1199
  // src/ui/components/popover/view.ts
1064
1200
  var open = null;
1201
+ var leaving = /* @__PURE__ */ new Set();
1065
1202
  function closeOpenPopovers() {
1066
1203
  open?.hide();
1067
1204
  }
@@ -1089,6 +1226,8 @@ var Popover = class {
1089
1226
  this.shown = false;
1090
1227
  /** The pending removal of a fading-out shell; a show() that reuses the shell cancels it. */
1091
1228
  this.leaveTimer = null;
1229
+ /** The shell's exit animation before it leaves the DOM; a show() that reuses the shell cancels it. */
1230
+ this.exit = null;
1092
1231
  const doc = opts.trigger.ownerDocument;
1093
1232
  injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
1094
1233
  this.trigger = opts.trigger;
@@ -1125,6 +1264,10 @@ var Popover = class {
1125
1264
  clearTimeout(this.leaveTimer);
1126
1265
  this.leaveTimer = null;
1127
1266
  }
1267
+ this.exit?.cancel();
1268
+ this.exit = null;
1269
+ leaving.delete(this);
1270
+ for (const p of [...leaving]) if (p.trigger === this.trigger) p.exit?.finish();
1128
1271
  ensureUIHost(this.el, this.inheritsTokens ? void 0 : this.theme);
1129
1272
  if (this.fadeMs > 0) {
1130
1273
  this.el.style.transition = `opacity ${this.fadeMs}ms ease`;
@@ -1149,16 +1292,21 @@ var Popover = class {
1149
1292
  this.hide();
1150
1293
  };
1151
1294
  const onReflow = () => this.place();
1152
- setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
1295
+ setTimeout(() => {
1296
+ if (this.onOutside === onOutside) document.addEventListener("pointerdown", onOutside, true);
1297
+ }, 0);
1153
1298
  document.addEventListener("keydown", onKey, true);
1154
1299
  window.addEventListener("resize", onReflow, true);
1155
1300
  document.addEventListener("scroll", onReflow, true);
1156
1301
  this.onOutside = onOutside;
1157
1302
  this.onKey = onKey;
1158
1303
  this.onReflow = onReflow;
1304
+ announceSurface(this.el, true, "popover", this.trigger);
1159
1305
  }
1160
1306
  hide() {
1161
1307
  if (!this.shown) return;
1308
+ this.shown = false;
1309
+ announceSurface(this.el, false, "popover", this.trigger);
1162
1310
  if (this.onOutside) document.removeEventListener("pointerdown", this.onOutside, true);
1163
1311
  if (this.onKey) document.removeEventListener("keydown", this.onKey, true);
1164
1312
  if (this.onReflow) {
@@ -1176,9 +1324,13 @@ var Popover = class {
1176
1324
  this.el.remove();
1177
1325
  }, this.fadeMs);
1178
1326
  } else {
1179
- this.el.remove();
1327
+ this.exit = holdForExit(this.el, () => {
1328
+ this.exit = null;
1329
+ leaving.delete(this);
1330
+ this.el.remove();
1331
+ });
1332
+ if (this.exit) leaving.add(this);
1180
1333
  }
1181
- this.shown = false;
1182
1334
  if (open === this) open = null;
1183
1335
  this.ctrl.onClose?.();
1184
1336
  }
@@ -1189,6 +1341,7 @@ var Popover = class {
1189
1341
  }
1190
1342
  destroy() {
1191
1343
  this.hide();
1344
+ this.exit?.finish();
1192
1345
  }
1193
1346
  reposition() {
1194
1347
  if (this.shown) this.place();
@@ -1384,6 +1537,11 @@ ${overlayScrollbarCss(".vela-dialog *")}
1384
1537
  `;
1385
1538
  var Dialog = class {
1386
1539
  constructor(opts = {}) {
1540
+ this.opener = null;
1541
+ this.wasOpen = false;
1542
+ /** The panel and scrim's exit animation after a close; a reopen cancels it. */
1543
+ this.exit = null;
1544
+ this.destroyed = false;
1387
1545
  const doc = (opts.host ?? document.body).ownerDocument;
1388
1546
  injectStyles(DIALOG_STYLE_ID, DIALOG_CSS, doc);
1389
1547
  const host = opts.host ?? doc.body;
@@ -1480,19 +1638,73 @@ var Dialog = class {
1480
1638
  const mid = String(this.ctrl.props.id);
1481
1639
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
1482
1640
  const api = this.ctrl.connect(service);
1641
+ const closing = this.wasOpen && !api.open;
1642
+ if (closing) this.announce(false);
1483
1643
  vanilla.spreadProps(this.backdrop, api.getBackdropProps(), mid);
1484
1644
  vanilla.spreadProps(this.positioner, api.getPositionerProps(), mid);
1485
1645
  vanilla.spreadProps(this.panel, api.getContentProps(), mid);
1486
1646
  vanilla.spreadProps(title, api.getTitleProps(), mid);
1487
1647
  vanilla.spreadProps(close, api.getCloseTriggerProps(), mid);
1488
- this.backdrop.style.display = api.open ? "" : "none";
1489
- this.positioner.style.display = api.open ? "" : "none";
1648
+ if (api.open) {
1649
+ this.exit?.cancel();
1650
+ this.exit = null;
1651
+ this.setShown(true);
1652
+ } else if (closing) {
1653
+ this.holdExit();
1654
+ } else if (!this.exit) {
1655
+ this.setShown(false);
1656
+ }
1657
+ if (api.open && !this.wasOpen) this.announce(true);
1658
+ if (this.wasOpen && !api.open) this.restoreOpener();
1659
+ this.wasOpen = api.open;
1490
1660
  });
1491
1661
  }
1662
+ setShown(shown) {
1663
+ this.backdrop.style.display = shown ? "" : "none";
1664
+ this.positioner.style.display = shown ? "" : "none";
1665
+ }
1666
+ /** Keep the just-closed dialog up through its exit animation, then hide it — or
1667
+ * remove it, when it was destroyed meanwhile. `spreadProps` re-applies the close
1668
+ * props' `hidden` only when its value changes, so lifting it keeps the panel and
1669
+ * scrim up until the exit ends. The positioner spans the viewport: inert, it lets
1670
+ * clicks through to the page while the panel animates out. */
1671
+ holdExit() {
1672
+ const held = [this.panel, this.backdrop];
1673
+ for (const el of held) el.hidden = false;
1674
+ this.exit = holdForExit(
1675
+ held,
1676
+ () => {
1677
+ this.exit = null;
1678
+ if (this.destroyed) {
1679
+ this.detach();
1680
+ } else if (!this.open) {
1681
+ for (const el of held) el.hidden = true;
1682
+ this.setShown(false);
1683
+ }
1684
+ },
1685
+ { inert: [this.positioner] }
1686
+ );
1687
+ }
1688
+ detach() {
1689
+ this.backdrop.remove();
1690
+ this.positioner.remove();
1691
+ }
1692
+ /** Closing hands focus back to whatever opened it (Escape, the ✕, hide(), or a
1693
+ * teardown that outruns the machine's close notification). */
1694
+ restoreOpener() {
1695
+ const opener = this.opener;
1696
+ this.opener = null;
1697
+ if (opener?.isConnected) opener.focus({ preventScroll: true });
1698
+ }
1699
+ announce(open2) {
1700
+ const opener = this.opener === this.panel.ownerDocument.body ? null : this.opener;
1701
+ announceSurface(this.panel, open2, "dialog", opener);
1702
+ }
1492
1703
  get open() {
1493
1704
  return this.ctrl.connect(this.handle.service).open;
1494
1705
  }
1495
1706
  show() {
1707
+ this.opener = this.panel.ownerDocument.activeElement;
1496
1708
  this.ctrl.connect(this.handle.service).setOpen(true);
1497
1709
  }
1498
1710
  hide() {
@@ -1505,10 +1717,16 @@ var Dialog = class {
1505
1717
  return el?.closest(".vela-popover, .vela-menu") != null;
1506
1718
  }
1507
1719
  destroy() {
1720
+ if (this.wasOpen && !this.open) this.handle.flush();
1508
1721
  if (this.open) this.hide();
1509
1722
  this.handle.stop();
1510
- this.backdrop.remove();
1511
- this.positioner.remove();
1723
+ this.destroyed = true;
1724
+ if (this.wasOpen) {
1725
+ this.wasOpen = false;
1726
+ this.announce(false);
1727
+ }
1728
+ this.restoreOpener();
1729
+ if (!this.exit) this.detach();
1512
1730
  }
1513
1731
  };
1514
1732
  function drawerController(opts = {}) {
@@ -1635,6 +1853,11 @@ function swipeDirection(dx, dy) {
1635
1853
  }
1636
1854
  var Drawer = class {
1637
1855
  constructor(opts = {}) {
1856
+ this.opener = null;
1857
+ this.wasOpen = false;
1858
+ /** The sheet and scrim's exit animation after a close; a reopen cancels it. */
1859
+ this.exit = null;
1860
+ this.destroyed = false;
1638
1861
  const doc = (opts.host ?? document.body).ownerDocument;
1639
1862
  injectStyles(DRAWER_STYLE_ID, DRAWER_CSS, doc);
1640
1863
  const host = opts.host ?? doc.body;
@@ -1662,12 +1885,24 @@ var Drawer = class {
1662
1885
  const mid = String(this.ctrl.props.id);
1663
1886
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
1664
1887
  const api = this.ctrl.connect(service);
1888
+ const closing = this.wasOpen && !api.open;
1889
+ if (closing) this.announce(false);
1665
1890
  vanilla.spreadProps(this.backdrop, api.getBackdropProps(), mid);
1666
1891
  vanilla.spreadProps(this.positioner, api.getPositionerProps(), mid);
1667
1892
  vanilla.spreadProps(this.panel, api.getContentProps(), mid);
1668
1893
  vanilla.spreadProps(this.titleEl, api.getTitleProps(), mid);
1669
- this.backdrop.style.display = api.open ? "" : "none";
1670
- this.positioner.style.display = api.open ? "" : "none";
1894
+ if (api.open) {
1895
+ this.exit?.cancel();
1896
+ this.exit = null;
1897
+ this.setShown(true);
1898
+ } else if (closing) {
1899
+ this.holdExit();
1900
+ } else if (!this.exit) {
1901
+ this.setShown(false);
1902
+ }
1903
+ if (api.open && !this.wasOpen) this.announce(true);
1904
+ if (this.wasOpen && !api.open) this.restoreOpener();
1905
+ this.wasOpen = api.open;
1671
1906
  });
1672
1907
  }
1673
1908
  /** Any element between `from` and the panel that has already been scrolled down —
@@ -1776,19 +2011,67 @@ var Drawer = class {
1776
2011
  setTitle(title) {
1777
2012
  this.titleEl.textContent = title;
1778
2013
  }
2014
+ setShown(shown) {
2015
+ this.backdrop.style.display = shown ? "" : "none";
2016
+ this.positioner.style.display = shown ? "" : "none";
2017
+ }
2018
+ /** Keep the just-closed sheet up through its exit animation, then hide it — or
2019
+ * remove it, when it was destroyed meanwhile. `spreadProps` re-applies the close
2020
+ * props' `hidden` only when its value changes, so lifting it keeps the sheet and
2021
+ * scrim up until the exit ends. The positioner spans the host: inert, it lets
2022
+ * clicks through to the page while the sheet animates out. */
2023
+ holdExit() {
2024
+ const held = [this.panel, this.backdrop];
2025
+ for (const el of held) el.hidden = false;
2026
+ this.exit = holdForExit(
2027
+ held,
2028
+ () => {
2029
+ this.exit = null;
2030
+ if (this.destroyed) {
2031
+ this.detach();
2032
+ } else if (!this.open) {
2033
+ for (const el of held) el.hidden = true;
2034
+ this.setShown(false);
2035
+ }
2036
+ },
2037
+ { inert: [this.positioner] }
2038
+ );
2039
+ }
2040
+ detach() {
2041
+ this.backdrop.remove();
2042
+ this.positioner.remove();
2043
+ }
2044
+ /** Closing hands focus back to whatever opened it (Escape, the ✕, hide(), or a
2045
+ * teardown that outruns the machine's close notification). */
2046
+ restoreOpener() {
2047
+ const opener = this.opener;
2048
+ this.opener = null;
2049
+ if (opener?.isConnected) opener.focus({ preventScroll: true });
2050
+ }
2051
+ announce(open2) {
2052
+ const opener = this.opener === this.panel.ownerDocument.body ? null : this.opener;
2053
+ announceSurface(this.panel, open2, "drawer", opener);
2054
+ }
1779
2055
  get open() {
1780
2056
  return this.ctrl.connect(this.handle.service).open;
1781
2057
  }
1782
2058
  show() {
2059
+ this.opener = this.panel.ownerDocument.activeElement;
1783
2060
  this.ctrl.connect(this.handle.service).setOpen(true);
1784
2061
  }
1785
2062
  hide() {
1786
2063
  this.ctrl.connect(this.handle.service).setOpen(false);
1787
2064
  }
1788
2065
  destroy() {
2066
+ if (this.wasOpen && !this.open) this.handle.flush();
1789
2067
  this.handle.stop();
1790
- this.backdrop.remove();
1791
- this.positioner.remove();
2068
+ this.destroyed = true;
2069
+ if (this.wasOpen) {
2070
+ this.wasOpen = false;
2071
+ this.announce(false);
2072
+ }
2073
+ this.restoreOpener();
2074
+ if (!this.exit) this.detach();
1792
2075
  }
1793
2076
  };
1794
2077
 
@@ -3971,6 +4254,8 @@ exports.KeymapManager = KeymapManager;
3971
4254
  exports.Menu = Menu;
3972
4255
  exports.NumberInput = NumberInput;
3973
4256
  exports.Popover = Popover;
4257
+ exports.SURFACE_CLOSE_EVENT = SURFACE_CLOSE_EVENT;
4258
+ exports.SURFACE_OPEN_EVENT = SURFACE_OPEN_EVENT;
3974
4259
  exports.Select = Select;
3975
4260
  exports.Switch = Switch;
3976
4261
  exports.TextArea = TextArea;