@surdeddd/bottom-sheet 0.1.0 → 0.3.0

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 (57) hide show
  1. package/README.md +57 -21
  2. package/dist/BottomSheetEngine-CAymrH-P.d.cts +285 -0
  3. package/dist/BottomSheetEngine-DcqcuudN.d.ts +285 -0
  4. package/dist/element.cjs +775 -537
  5. package/dist/element.d.cts +9 -57
  6. package/dist/element.d.ts +9 -57
  7. package/dist/element.iife.global.js +2 -2
  8. package/dist/element.js +775 -537
  9. package/dist/index.cjs +760 -600
  10. package/dist/index.d.cts +18 -85
  11. package/dist/index.d.ts +18 -85
  12. package/dist/index.js +758 -601
  13. package/dist/integrations/formik.d.cts +2 -8
  14. package/dist/integrations/formik.d.ts +2 -8
  15. package/dist/integrations/react-hook-form.d.cts +2 -11
  16. package/dist/integrations/react-hook-form.d.ts +2 -11
  17. package/dist/overlay.cjs +168 -122
  18. package/dist/overlay.d.cts +14 -137
  19. package/dist/overlay.d.ts +14 -137
  20. package/dist/overlay.js +168 -122
  21. package/dist/preact.d.cts +2 -2
  22. package/dist/preact.d.ts +2 -2
  23. package/dist/preact.js +0 -1
  24. package/dist/qwik.cjs +667 -519
  25. package/dist/qwik.d.cts +3 -24
  26. package/dist/qwik.d.ts +3 -24
  27. package/dist/qwik.js +668 -520
  28. package/dist/react.cjs +873 -648
  29. package/dist/react.d.cts +34 -159
  30. package/dist/react.d.ts +34 -159
  31. package/dist/react.js +873 -648
  32. package/dist/solid.cjs +703 -560
  33. package/dist/solid.d.cts +264 -32
  34. package/dist/solid.d.ts +264 -32
  35. package/dist/solid.js +704 -561
  36. package/dist/solid.source.jsx +3215 -0
  37. package/dist/styles.css +37 -112
  38. package/dist/svelte-core.cjs +3149 -0
  39. package/dist/svelte-core.js +3147 -0
  40. package/dist/svelte-src/BottomSheet.svelte +210 -0
  41. package/dist/svelte-src/index.js +2 -0
  42. package/dist/svelte.cjs +718 -547
  43. package/dist/svelte.d.cts +100 -0
  44. package/dist/svelte.d.ts +15 -4
  45. package/dist/svelte.js +719 -548
  46. package/dist/themes/ios.css +0 -3
  47. package/dist/themes/material.css +0 -3
  48. package/dist/themes/vercel.css +0 -3
  49. package/dist/{transform-Brkf-wzo.d.cts → transform-jVfAikE9.d.cts} +0 -1
  50. package/dist/{transform-Brkf-wzo.d.ts → transform-jVfAikE9.d.ts} +0 -1
  51. package/dist/vue.cjs +677 -526
  52. package/dist/vue.d.cts +78 -0
  53. package/dist/vue.d.ts +12 -2
  54. package/dist/vue.js +678 -527
  55. package/package.json +105 -45
  56. package/dist/BottomSheetEngine-CvSVbo6c.d.cts +0 -704
  57. package/dist/BottomSheetEngine-DbO8rW_3.d.ts +0 -704
package/dist/overlay.cjs CHANGED
@@ -22,6 +22,18 @@ var SheetStack = class {
22
22
  this.entries.splice(idx, 1);
23
23
  this.recompute();
24
24
  }
25
+ promote(id) {
26
+ const idx = this.entries.findIndex((e) => e.id === id);
27
+ if (idx === -1) return;
28
+ if (idx !== this.entries.length - 1) {
29
+ const [entry] = this.entries.splice(idx, 1);
30
+ this.entries.push(entry);
31
+ }
32
+ this.recompute();
33
+ }
34
+ update() {
35
+ this.recompute();
36
+ }
25
37
  size() {
26
38
  return this.entries.length;
27
39
  }
@@ -29,9 +41,24 @@ var SheetStack = class {
29
41
  this.entries = [];
30
42
  }
31
43
  recompute() {
44
+ let top = null;
45
+ const open = [];
46
+ for (const entry of this.entries) {
47
+ if (!entry.isOpen || entry.isOpen()) {
48
+ top = entry;
49
+ open.push(entry);
50
+ }
51
+ }
52
+ if (!top && this.entries.length > 0) {
53
+ top = this.entries[this.entries.length - 1];
54
+ }
32
55
  this.entries.forEach((entry, i) => {
33
56
  entry.setZIndex(BASE_Z + i * STEP);
34
- entry.setIsTop(i === this.entries.length - 1);
57
+ entry.setIsTop(entry === top);
58
+ if (entry.setDepth) {
59
+ const openIdx = open.indexOf(entry);
60
+ entry.setDepth(openIdx === -1 ? 0 : open.length - 1 - openIdx);
61
+ }
35
62
  });
36
63
  }
37
64
  };
@@ -154,9 +181,13 @@ var isVisible = (el) => {
154
181
  var focusables = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter(
155
182
  (el) => !el.hasAttribute("inert") && isVisible(el) && !el.matches("[aria-hidden='true']")
156
183
  );
184
+ var trapStack = [];
157
185
  var installFocusTrap = (container, options = {}) => {
158
186
  if (typeof document === "undefined") return () => {
159
187
  };
188
+ const token = {};
189
+ trapStack.push(token);
190
+ const isActive = () => trapStack[trapStack.length - 1] === token;
160
191
  const previouslyFocused = document.activeElement;
161
192
  const focusInitial = () => {
162
193
  const initial = typeof options.initialFocus === "string" ? container.querySelector(options.initialFocus) : options.initialFocus ?? focusables(container)[0] ?? container;
@@ -164,6 +195,7 @@ var installFocusTrap = (container, options = {}) => {
164
195
  };
165
196
  focusInitial();
166
197
  const handleKey = (e) => {
198
+ if (!isActive()) return;
167
199
  if (e.key === "Escape" && options.onEscape) {
168
200
  options.onEscape();
169
201
  return;
@@ -186,6 +218,7 @@ var installFocusTrap = (container, options = {}) => {
186
218
  }
187
219
  };
188
220
  const handleFocusIn = (e) => {
221
+ if (!isActive()) return;
189
222
  const target = e.target;
190
223
  if (target && !container.contains(target)) {
191
224
  const list = focusables(container);
@@ -198,6 +231,8 @@ var installFocusTrap = (container, options = {}) => {
198
231
  return () => {
199
232
  if (released) return;
200
233
  released = true;
234
+ const idx = trapStack.indexOf(token);
235
+ if (idx !== -1) trapStack.splice(idx, 1);
201
236
  document.removeEventListener("keydown", handleKey, true);
202
237
  document.removeEventListener("focusin", handleFocusIn, true);
203
238
  if (previouslyFocused && document.contains(previouslyFocused)) {
@@ -253,11 +288,17 @@ function createInertSiblings(rootProvider) {
253
288
  return {
254
289
  apply() {
255
290
  if (typeof document === "undefined") return;
256
- const root = rootProvider();
257
- const parent = root.parentElement ?? document.body;
291
+ let top = rootProvider();
292
+ while (top.parentElement && top.parentElement !== document.body) {
293
+ top = top.parentElement;
294
+ }
295
+ const parent = top.parentElement ?? document.body;
258
296
  for (const child of Array.from(parent.children)) {
259
- if (child === root) continue;
297
+ if (child === top) continue;
260
298
  if (child.hasAttribute("inert")) continue;
299
+ if (child.classList.contains("bs-backdrop") || child.classList.contains("bs-screen") || child.classList.contains("bs-root")) {
300
+ continue;
301
+ }
261
302
  child.setAttribute("inert", "");
262
303
  tracked.push(child);
263
304
  }
@@ -273,7 +314,6 @@ function createInertSiblings(rootProvider) {
273
314
  var LifecycleController = class {
274
315
  constructor(deps, opts) {
275
316
  __publicField(this, "element");
276
- __publicField(this, "closeFn");
277
317
  __publicField(this, "focusTrapEnabled");
278
318
  __publicField(this, "initialFocus");
279
319
  __publicField(this, "closeOnEscape");
@@ -282,16 +322,11 @@ var LifecycleController = class {
282
322
  __publicField(this, "inertSiblings");
283
323
  __publicField(this, "shouldApplyInertSiblings");
284
324
  __publicField(this, "returnFocus");
285
- /** Toggle-state teardowns — fire on close, NOT in destroy-only stack. */
286
325
  __publicField(this, "releaseFocusTrap", null);
287
326
  __publicField(this, "releaseScrollLock", null);
288
- /** True between install() and release(). Guards release()/destroy() from
289
- * re-running side effects (notably `returnFocus.focus()`) on already-
290
- * released or never-installed instances. */
291
327
  __publicField(this, "installed", false);
292
328
  __publicField(this, "destroyed", false);
293
329
  this.element = deps.element;
294
- this.closeFn = deps.close;
295
330
  this.focusTrapEnabled = opts.focusTrap ?? false;
296
331
  this.initialFocus = opts.initialFocus;
297
332
  this.closeOnEscape = opts.closeOnEscape ?? true;
@@ -301,14 +336,9 @@ var LifecycleController = class {
301
336
  this.returnFocus = opts.returnFocus;
302
337
  this.inertSiblings = createInertSiblings(() => this.element);
303
338
  }
304
- /**
305
- * Run the open-time install steps. Idempotent: each step checks its
306
- * existing teardown ref so a re-open during a half-cancelled close
307
- * doesn't double-install. Sets `installed=true` BEFORE the first side
308
- * effect so a partial throw (detached / shadow-DOM target) leaves the
309
- * caller's `release()` rollback path armed to undo whatever did succeed.
310
- * No-op when destroyed.
311
- */
339
+ get isInstalled() {
340
+ return this.installed;
341
+ }
312
342
  install() {
313
343
  if (this.destroyed) return;
314
344
  this.installed = true;
@@ -317,30 +347,17 @@ var LifecycleController = class {
317
347
  }
318
348
  if (this.focusTrapEnabled && !this.releaseFocusTrap) {
319
349
  this.releaseFocusTrap = installFocusTrap(this.element, {
320
- initialFocus: this.initialFocus,
321
- onEscape: this.closeOnEscape ? () => void this.closeFn() : void 0
350
+ initialFocus: this.initialFocus
322
351
  });
323
352
  }
324
353
  if (this.inertSiblingsEnabled && this.shouldApplyInertSiblings()) {
325
354
  this.inertSiblings.apply();
326
355
  }
327
356
  }
328
- /**
329
- * Update the post-release focus target. Overlay re-binds this when the
330
- * consumer changes `returnFocus` after construction; the new value applies
331
- * on the next `release()`. No-op when destroyed.
332
- */
333
357
  setReturnFocus(target) {
334
358
  if (this.destroyed) return;
335
359
  this.returnFocus = target;
336
360
  }
337
- /**
338
- * Release all install-time teardowns. Short-circuits when not installed,
339
- * so `close() → destroy()` (each routing through here) only restores
340
- * focus once. The `installed` guard lets `inertSiblings.remove()` and
341
- * the focus-trap/scroll-lock teardown nullings stay simple — they were
342
- * already idempotent, but `returnFocus.focus()` was firing on every call.
343
- */
344
361
  release() {
345
362
  if (!this.installed) return;
346
363
  this.installed = false;
@@ -354,18 +371,18 @@ var LifecycleController = class {
354
371
  target?.focus?.();
355
372
  }
356
373
  }
357
- /**
358
- * Final teardown — releases any still-installed listeners and seals the
359
- * controller. After destroy(), install() is a no-op and setReturnFocus()
360
- * is rejected, so post-destroy mutations can't queue up a stale focus
361
- * restore.
362
- */
363
374
  destroy() {
364
375
  this.release();
365
376
  this.destroyed = true;
366
377
  }
367
378
  };
368
379
 
380
+ // src/core/animation/animation.ts
381
+ var prefersReducedMotion = () => {
382
+ if (typeof window === "undefined" || !window.matchMedia) return false;
383
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
384
+ };
385
+
369
386
  // src/core/overlay.ts
370
387
  var OVERLAY_PRESETS = Object.freeze({
371
388
  sheet: Object.freeze({
@@ -431,8 +448,6 @@ var OverlayEngine = class {
431
448
  __publicField(this, "exitEasing");
432
449
  __publicField(this, "backdropOpacity");
433
450
  __publicField(this, "backdropFilter");
434
- // Definite-assignment: written via assignSwipeFromConfig in the
435
- // constructor — TS can't follow the helper, mark the assertion explicit.
436
451
  __publicField(this, "swipeToCloseEnabled");
437
452
  __publicField(this, "swipeThreshold");
438
453
  __publicField(this, "swipeVelocityThreshold");
@@ -447,18 +462,16 @@ var OverlayEngine = class {
447
462
  __publicField(this, "closeOnOutsidePointerEnabled");
448
463
  __publicField(this, "detachOutsidePointer", null);
449
464
  __publicField(this, "detachSwipe", null);
465
+ __publicField(this, "respectReducedMotionEnabled");
466
+ __publicField(this, "isTop", true);
467
+ __publicField(this, "backPushed", false);
468
+ __publicField(this, "movedToTarget", false);
450
469
  __publicField(this, "onOpenCb");
451
470
  __publicField(this, "onCloseCb");
452
471
  __publicField(this, "isOpen_", false);
453
472
  __publicField(this, "isAnimating_", false);
454
473
  __publicField(this, "destroyed", false);
455
- // Tracks whether setOverlayChildren ever populated the panel — lets
456
- // clearOverlayChildren skip the DOM mutation when nothing was injected
457
- // (so the field also acts as a zero-allocation feature flag).
458
474
  __publicField(this, "hasInjectedChildren", false);
459
- // Bumped on every open/close. awaitTransition captures it and ignores
460
- // transitionend/timeout fired by older cycles — prevents cross-cycle
461
- // listener bleed during rapid toggling.
462
475
  __publicField(this, "cycleNonce", 0);
463
476
  __publicField(this, "bus", createEventBus());
464
477
  __publicField(this, "detachEscape", null);
@@ -489,6 +502,7 @@ var OverlayEngine = class {
489
502
  this.mountTarget = opts.mountTo;
490
503
  this.peekRaw = opts.peek;
491
504
  this.closeOnOutsidePointerEnabled = opts.closeOnOutsidePointer ?? false;
505
+ this.respectReducedMotionEnabled = opts.respectReducedMotion ?? true;
492
506
  this.onOpenCb = opts.onOpen;
493
507
  this.onCloseCb = opts.onClose;
494
508
  if (this.peekRaw !== void 0 && this.edge !== "bottom") {
@@ -511,9 +525,6 @@ var OverlayEngine = class {
511
525
  closeOnEscape: this.closeOnEscape,
512
526
  lockBodyScroll: this.bodyScrollLockEnabled,
513
527
  inertSiblings: this.inertSiblingsEnabled,
514
- // Overlay-specific guard: only paint body siblings inert when the
515
- // overlay element is actually a body descendant. Detached / shadow-
516
- // DOM / portal overlays would otherwise lock unrelated trees.
517
528
  shouldApplyInertSiblings: () => {
518
529
  const bodyChildren = Array.from(document.body.children);
519
530
  return bodyChildren.some(
@@ -539,15 +550,17 @@ var OverlayEngine = class {
539
550
  this.emit("before-open", void 0);
540
551
  this.isOpen_ = true;
541
552
  this.isAnimating_ = true;
553
+ sheetStack.promote(this.id);
542
554
  this.element.removeAttribute("hidden");
543
555
  this.element.setAttribute("data-state", "entering");
544
556
  if (this.focusTrapEnabled) {
545
557
  this.element.setAttribute("aria-modal", "true");
546
558
  }
547
- const transitionStr = `transform ${this.duration}ms ${this.enterEasing}, opacity ${this.duration}ms ease-out`;
559
+ const dur = this.effectiveDuration();
560
+ const transitionStr = `transform ${dur}ms ${this.enterEasing}, opacity ${dur}ms ease-out`;
548
561
  this.element.style.transition = transitionStr;
549
562
  if (this.backdrop) {
550
- this.backdrop.style.transition = `opacity ${this.duration}ms ease-out`;
563
+ this.backdrop.style.transition = `opacity ${dur}ms ease-out`;
551
564
  }
552
565
  const cycle = ++this.cycleNonce;
553
566
  return new Promise((resolve) => {
@@ -559,7 +572,7 @@ var OverlayEngine = class {
559
572
  this.element.style.opacity = "1";
560
573
  if (this.backdrop) {
561
574
  this.backdrop.style.opacity = String(this.backdropOpacity);
562
- this.backdrop.style.pointerEvents = "auto";
575
+ this.backdrop.style.pointerEvents = this.backdropOpacity > 0 ? "auto" : "none";
563
576
  if (this.backdropFilter) {
564
577
  this.backdrop.style.backdropFilter = this.backdropFilter;
565
578
  this.backdrop.style.webkitBackdropFilter = this.backdropFilter;
@@ -595,7 +608,10 @@ var OverlayEngine = class {
595
608
  });
596
609
  return resolve();
597
610
  }
598
- this.awaitTransition(cycle).then(() => {
611
+ this.awaitTransition(
612
+ cycle,
613
+ this.enterAnimation === "fade" ? "opacity" : "transform"
614
+ ).then(() => {
599
615
  if (this.destroyed || cycle !== this.cycleNonce) return resolve();
600
616
  this.isAnimating_ = false;
601
617
  this.element.setAttribute("data-state", "open");
@@ -612,11 +628,14 @@ var OverlayEngine = class {
612
628
  this.emit("before-close", void 0);
613
629
  this.isOpen_ = false;
614
630
  this.isAnimating_ = true;
631
+ sheetStack.update();
632
+ this.popBackMarker(reason);
615
633
  this.element.setAttribute("data-state", "exiting");
616
- const transitionStr = `transform ${this.duration}ms ${this.exitEasing}, opacity ${this.duration}ms ease-in`;
634
+ const dur = this.effectiveDuration();
635
+ const transitionStr = `transform ${dur}ms ${this.exitEasing}, opacity ${dur}ms ease-in`;
617
636
  this.element.style.transition = transitionStr;
618
637
  if (this.backdrop) {
619
- this.backdrop.style.transition = `opacity ${this.duration}ms ease-in`;
638
+ this.backdrop.style.transition = `opacity ${dur}ms ease-in`;
620
639
  }
621
640
  const cycle = ++this.cycleNonce;
622
641
  return new Promise((resolve) => {
@@ -627,7 +646,7 @@ var OverlayEngine = class {
627
646
  this.exitAnimation,
628
647
  this.peekPx
629
648
  );
630
- this.element.style.opacity = "0";
649
+ this.element.style.opacity = this.peekPx !== void 0 ? "1" : "0";
631
650
  if (this.backdrop) {
632
651
  this.backdrop.style.opacity = "0";
633
652
  this.backdrop.style.pointerEvents = "none";
@@ -637,7 +656,10 @@ var OverlayEngine = class {
637
656
  }
638
657
  }
639
658
  this.releaseInteractiveListeners();
640
- this.awaitTransition(cycle).then(() => {
659
+ this.awaitTransition(
660
+ cycle,
661
+ this.exitAnimation === "fade" ? "opacity" : "transform"
662
+ ).then(() => {
641
663
  if (this.destroyed || cycle !== this.cycleNonce) return resolve();
642
664
  this.isAnimating_ = false;
643
665
  if (this.peekPx === void 0) {
@@ -665,14 +687,12 @@ var OverlayEngine = class {
665
687
  this.releaseInteractiveListeners();
666
688
  this.releaseStackEntry?.();
667
689
  this.releaseStackEntry = null;
690
+ const hadOriginalParent = this.originalParent !== null;
668
691
  this.restoreMount();
692
+ if (this.movedToTarget && !hadOriginalParent && this.element.parentNode) {
693
+ this.element.parentNode.removeChild(this.element);
694
+ }
669
695
  }
670
- /**
671
- * Runtime visual setter — opacity is applied immediately when open so a
672
- * live-controls panel can preview the change without re-opening. When
673
- * closed we only stash the field; next open() picks it up so we don't
674
- * mutate hidden DOM (avoids paint thrash on detached panels).
675
- */
676
696
  setBackdropOpacity(opacity) {
677
697
  if (this.destroyed) return;
678
698
  this.backdropOpacity = opacity;
@@ -680,11 +700,6 @@ var OverlayEngine = class {
680
700
  this.backdrop.style.opacity = String(opacity);
681
701
  }
682
702
  }
683
- /**
684
- * Runtime visual setter for `backdrop-filter`. `undefined` clears the
685
- * filter (mirrors the close() behaviour). Defers to next open() when the
686
- * overlay is currently closed.
687
- */
688
703
  setBackdropFilter(filter) {
689
704
  if (this.destroyed) return;
690
705
  this.backdropFilter = filter;
@@ -698,12 +713,6 @@ var OverlayEngine = class {
698
713
  style.webkitBackdropFilter = "";
699
714
  }
700
715
  }
701
- /**
702
- * Toggle / reconfigure swipe-to-close at runtime. Re-resolves the flat
703
- * threshold fields, then installs or tears down the pointer listener so
704
- * the change applies to the CURRENT open cycle. Idempotent: safe to call
705
- * with the same config repeatedly.
706
- */
707
716
  setSwipeToClose(config) {
708
717
  if (this.destroyed) return;
709
718
  const wasEnabled = this.swipeToCloseEnabled;
@@ -716,7 +725,6 @@ var OverlayEngine = class {
716
725
  this.detachSwipe = null;
717
726
  }
718
727
  }
719
- /** Animation setters only stage the field — applied on next open/close. */
720
728
  setEnterAnimation(animation) {
721
729
  if (this.destroyed) return;
722
730
  this.enterAnimation = animation;
@@ -725,23 +733,15 @@ var OverlayEngine = class {
725
733
  if (this.destroyed) return;
726
734
  this.exitAnimation = animation;
727
735
  }
728
- /** Updates the field used at next close() — no immediate side effect. */
729
736
  setReturnFocus(target) {
730
737
  if (this.destroyed) return;
731
738
  this.lifecycle.setReturnFocus(target);
732
739
  }
733
- /**
734
- * Batch update — preset first, then individual overrides. Applied as a
735
- * single DOM-write phase when open so live-controls don't trigger
736
- * cascading reflows. Safe to call when closed (defers to next open).
737
- */
738
740
  setOverlay(opts) {
739
741
  if (this.destroyed) return;
740
742
  const preset = opts.preset ? OVERLAY_PRESETS[opts.preset] : void 0;
741
743
  const merged = preset ? {
742
744
  backdropOpacity: preset.backdropOpacity,
743
- // Preset's `undefined` filter means "no filter" — surface as null
744
- // so the merged step clears the field instead of inheriting old.
745
745
  backdropFilter: preset.backdropFilter === void 0 ? null : preset.backdropFilter,
746
746
  swipeToClose: preset.swipeToClose,
747
747
  enterAnimation: preset.enterAnimation,
@@ -792,6 +792,36 @@ var OverlayEngine = class {
792
792
  }
793
793
  }
794
794
  }
795
+ if (opts.duration !== void 0) this.duration = opts.duration;
796
+ if (opts.enterEasing !== void 0) this.enterEasing = opts.enterEasing;
797
+ if (opts.exitEasing !== void 0) this.exitEasing = opts.exitEasing;
798
+ if (opts.closeOnEscape !== void 0) {
799
+ this.closeOnEscape = opts.closeOnEscape;
800
+ if (!opts.closeOnEscape) {
801
+ this.detachEscape?.();
802
+ this.detachEscape = null;
803
+ } else if (this.isOpen_) {
804
+ this.installInteractiveListeners();
805
+ }
806
+ }
807
+ if (opts.closeOnBackdrop !== void 0) {
808
+ this.closeOnBackdropEnabled = opts.closeOnBackdrop;
809
+ if (!opts.closeOnBackdrop) {
810
+ this.detachBackdrop?.();
811
+ this.detachBackdrop = null;
812
+ } else if (this.isOpen_) {
813
+ this.installInteractiveListeners();
814
+ }
815
+ }
816
+ if (opts.closeOnOutsidePointer !== void 0) {
817
+ this.closeOnOutsidePointerEnabled = opts.closeOnOutsidePointer;
818
+ if (!opts.closeOnOutsidePointer) {
819
+ this.detachOutsidePointer?.();
820
+ this.detachOutsidePointer = null;
821
+ } else if (this.isOpen_) {
822
+ this.installInteractiveListeners();
823
+ }
824
+ }
795
825
  if (opts.children !== void 0) {
796
826
  if (opts.children === null) {
797
827
  this.clearOverlayChildren();
@@ -800,30 +830,17 @@ var OverlayEngine = class {
800
830
  }
801
831
  }
802
832
  }
803
- /**
804
- * Runtime children injection — replace the panel's contents without
805
- * unmounting the engine. `replaceChildren()` is a single atomic DOM op,
806
- * so callers can swap content rapidly without intermediate flicker.
807
- * Function form is resolved exactly ONCE per call (cheap lazy content).
808
- */
809
833
  setOverlayChildren(children) {
810
834
  if (this.destroyed) return;
811
835
  const node = typeof children === "function" ? children() : children;
812
836
  this.element.replaceChildren(node);
813
837
  this.hasInjectedChildren = true;
814
838
  }
815
- /**
816
- * Clear injected children. No-op when nothing was injected so consumers
817
- * who never used setOverlayChildren can call this defensively without
818
- * paying for a DOM mutation.
819
- */
820
839
  clearOverlayChildren() {
821
840
  if (this.destroyed || !this.hasInjectedChildren) return;
822
841
  this.element.replaceChildren();
823
842
  this.hasInjectedChildren = false;
824
843
  }
825
- // Shared swipe-config normalizer — used by constructor, setSwipeToClose,
826
- // and setOverlay so the union/object shape is decoded in exactly one place.
827
844
  assignSwipeFromConfig(swipe) {
828
845
  if (typeof swipe === "object" && swipe !== null) {
829
846
  this.swipeToCloseEnabled = swipe.enabled ?? true;
@@ -838,6 +855,22 @@ var OverlayEngine = class {
838
855
  emit(event, payload) {
839
856
  this.bus.emit(event, payload);
840
857
  }
858
+ effectiveDuration() {
859
+ return this.respectReducedMotionEnabled && prefersReducedMotion() ? 0 : this.duration;
860
+ }
861
+ popBackMarker(reason) {
862
+ if (!this.backPushed) return;
863
+ this.backPushed = false;
864
+ if (reason === "back") return;
865
+ const state = history.state;
866
+ if (state && state.__bsOverlay === this.id) {
867
+ try {
868
+ history.back();
869
+ } catch {
870
+ return;
871
+ }
872
+ }
873
+ }
841
874
  resolvePeekPx(raw) {
842
875
  if (typeof raw === "number") return Math.max(0, raw);
843
876
  try {
@@ -859,6 +892,7 @@ var OverlayEngine = class {
859
892
  this.originalParent = this.element.parentNode;
860
893
  this.originalNextSibling = this.element.nextSibling;
861
894
  target.appendChild(this.element);
895
+ this.movedToTarget = true;
862
896
  this.mounted = true;
863
897
  }
864
898
  restoreMount() {
@@ -892,7 +926,7 @@ var OverlayEngine = class {
892
926
  this.backdrop.style.willChange = "opacity";
893
927
  }
894
928
  }
895
- awaitTransition(cycle) {
929
+ awaitTransition(cycle, property = "transform") {
896
930
  return new Promise((resolve) => {
897
931
  let done = false;
898
932
  const finish = () => {
@@ -907,10 +941,10 @@ var OverlayEngine = class {
907
941
  finish();
908
942
  return;
909
943
  }
910
- if (e.propertyName === "transform") finish();
944
+ if (e.target === this.element && e.propertyName === property) finish();
911
945
  };
912
946
  this.element.addEventListener("transitionend", onEnd);
913
- const timer = window.setTimeout(finish, this.duration + 50);
947
+ const timer = window.setTimeout(finish, this.effectiveDuration() + 50);
914
948
  });
915
949
  }
916
950
  installInteractiveListeners() {
@@ -920,7 +954,10 @@ var OverlayEngine = class {
920
954
  }
921
955
  if (this.closeOnEscape && !this.detachEscape) {
922
956
  const handler = (e) => {
923
- if (e.key === "Escape" && this.isOpen_) void this.close("escape");
957
+ if (e.defaultPrevented) return;
958
+ if (e.key === "Escape" && this.isOpen_ && this.isTop) {
959
+ void this.close("escape");
960
+ }
924
961
  };
925
962
  document.addEventListener("keydown", handler);
926
963
  this.detachEscape = () => document.removeEventListener("keydown", handler);
@@ -933,10 +970,14 @@ var OverlayEngine = class {
933
970
  if (this.closeOnBackEnabled && typeof window !== "undefined" && !this.detachHardwareBack) {
934
971
  try {
935
972
  history.pushState({ __bsOverlay: this.id }, "");
973
+ this.backPushed = true;
936
974
  } catch {
937
975
  }
938
976
  const onPop = () => {
939
- if (this.isOpen_) void this.close("back");
977
+ if (this.isOpen_ && this.isTop) {
978
+ this.backPushed = false;
979
+ void this.close("back");
980
+ }
940
981
  };
941
982
  window.addEventListener("popstate", onPop);
942
983
  this.detachHardwareBack = () => window.removeEventListener("popstate", onPop);
@@ -967,55 +1008,57 @@ var OverlayEngine = class {
967
1008
  this.detachOutsidePointer = null;
968
1009
  this.lifecycle.release();
969
1010
  }
970
- /**
971
- * Light pointer-driven dismiss. Tracks finger along the edge axis, follows
972
- * with translate, dismisses if past `swipeThreshold` (fraction of panel
973
- * size) or release velocity exceeds `swipeVelocityThreshold` (px/ms).
974
- * Pure CSS-transform during drag — no rAF or physics required since the
975
- * panel only moves one direction.
976
- */
977
1011
  installSwipeToClose() {
978
1012
  const el = this.element;
979
1013
  const edge = this.edge;
980
1014
  const isVertical = edge === "bottom" || edge === "top";
981
1015
  const sign = edge === "bottom" || edge === "right" ? 1 : -1;
982
- const threshold = this.swipeThreshold;
983
- const velocityThreshold = this.swipeVelocityThreshold;
1016
+ const SLOP = 8;
984
1017
  let startX = 0;
985
1018
  let startY = 0;
986
1019
  let startT = 0;
1020
+ let tracking = false;
987
1021
  let dragging = false;
988
1022
  let pointerId = null;
1023
+ const previousTouchAction = el.style.touchAction;
1024
+ el.style.touchAction = isVertical ? "pan-x" : "pan-y";
989
1025
  const onDown = (e) => {
990
1026
  if (e.button !== void 0 && e.button !== 0) return;
991
1027
  pointerId = e.pointerId;
992
1028
  startX = e.clientX;
993
1029
  startY = e.clientY;
994
1030
  startT = performance.now();
995
- dragging = true;
996
- el.style.transition = "none";
997
- try {
998
- el.setPointerCapture(e.pointerId);
999
- } catch {
1000
- }
1031
+ tracking = true;
1032
+ dragging = false;
1001
1033
  };
1002
1034
  const onMove = (e) => {
1003
- if (!dragging || e.pointerId !== pointerId) return;
1035
+ if (!tracking || e.pointerId !== pointerId) return;
1004
1036
  const delta = isVertical ? e.clientY - startY : e.clientX - startX;
1037
+ if (!dragging) {
1038
+ if (Math.abs(delta) < SLOP) return;
1039
+ dragging = true;
1040
+ el.style.transition = "none";
1041
+ try {
1042
+ el.setPointerCapture(e.pointerId);
1043
+ } catch {
1044
+ }
1045
+ }
1005
1046
  const offset = Math.max(0, sign * delta);
1006
1047
  el.style.transform = isVertical ? `translate3d(0, ${sign * offset}px, 0)` : `translate3d(${sign * offset}px, 0, 0)`;
1007
1048
  };
1008
1049
  const finish = (e) => {
1009
- if (!dragging || e.pointerId !== pointerId) return;
1010
- dragging = false;
1050
+ if (!tracking || e.pointerId !== pointerId) return;
1051
+ tracking = false;
1011
1052
  pointerId = null;
1053
+ if (!dragging) return;
1054
+ dragging = false;
1012
1055
  const dt = performance.now() - startT || 1;
1013
1056
  const delta = isVertical ? e.clientY - startY : e.clientX - startX;
1014
1057
  const offset = Math.max(0, sign * delta);
1015
1058
  const velocity = sign * delta / dt;
1016
1059
  const size = isVertical ? el.offsetHeight : el.offsetWidth;
1017
- const past = offset > size * threshold || velocity > velocityThreshold;
1018
- el.style.transition = `transform ${this.duration}ms ${this.exitEasing}`;
1060
+ const past = offset > size * this.swipeThreshold || velocity > this.swipeVelocityThreshold;
1061
+ el.style.transition = `transform ${this.effectiveDuration()}ms ${this.exitEasing}`;
1019
1062
  if (past) {
1020
1063
  void this.close("swipe");
1021
1064
  } else {
@@ -1027,6 +1070,7 @@ var OverlayEngine = class {
1027
1070
  el.addEventListener("pointerup", finish);
1028
1071
  el.addEventListener("pointercancel", finish);
1029
1072
  return () => {
1073
+ el.style.touchAction = previousTouchAction;
1030
1074
  el.removeEventListener("pointerdown", onDown);
1031
1075
  el.removeEventListener("pointermove", onMove);
1032
1076
  el.removeEventListener("pointerup", finish);
@@ -1041,10 +1085,12 @@ var OverlayEngine = class {
1041
1085
  if (this.backdrop) this.backdrop.style.zIndex = String(z - 1);
1042
1086
  },
1043
1087
  setIsTop: (isTop) => {
1088
+ this.isTop = isTop;
1044
1089
  if (this.backdrop) {
1045
1090
  this.backdrop.style.display = isTop ? "" : "none";
1046
1091
  }
1047
- }
1092
+ },
1093
+ isOpen: () => this.isOpen_
1048
1094
  });
1049
1095
  }
1050
1096
  };