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