@surdeddd/bottom-sheet 0.13.0 → 0.20.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 (66) hide show
  1. package/README.md +46 -0
  2. package/dist/{BottomSheetCore-D16lsa6g.d.ts → BottomSheetCore-Cgn8b_6A.d.ts} +36 -2
  3. package/dist/{BottomSheetCore-CsIjyq7C.d.cts → BottomSheetCore-VPJZcKcg.d.cts} +36 -2
  4. package/dist/{BottomSheetEngine-BYjAVrmd.d.cts → BottomSheetEngine-B8C2CwF9.d.cts} +1 -1
  5. package/dist/{BottomSheetEngine-YuCNugZn.d.ts → BottomSheetEngine-DwYR_ygJ.d.ts} +1 -1
  6. package/dist/core.cjs +328 -33
  7. package/dist/core.d.cts +2 -2
  8. package/dist/core.d.ts +2 -2
  9. package/dist/core.js +328 -33
  10. package/dist/debug.d.cts +1 -1
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/element.cjs +404 -108
  13. package/dist/element.d.cts +2 -2
  14. package/dist/element.d.ts +2 -2
  15. package/dist/element.iife.global.js +2 -2
  16. package/dist/element.js +404 -108
  17. package/dist/features.cjs +6 -50
  18. package/dist/features.d.cts +4 -4
  19. package/dist/features.d.ts +4 -4
  20. package/dist/features.js +6 -50
  21. package/dist/index-CnfbOKx0.d.cts +9 -0
  22. package/dist/index-DcMkhXAy.d.ts +9 -0
  23. package/dist/index.cjs +335 -829
  24. package/dist/index.d.cts +44 -47
  25. package/dist/index.d.ts +44 -47
  26. package/dist/index.js +336 -816
  27. package/dist/integrations/formik.d.cts +1 -1
  28. package/dist/integrations/formik.d.ts +1 -1
  29. package/dist/integrations/react-hook-form.d.cts +1 -1
  30. package/dist/integrations/react-hook-form.d.ts +1 -1
  31. package/dist/overlay.cjs +5 -5
  32. package/dist/overlay.js +5 -5
  33. package/dist/preact.d.cts +2 -2
  34. package/dist/preact.d.ts +2 -2
  35. package/dist/qwik-lib/index.qwik.cjs +323 -83
  36. package/dist/qwik-lib/index.qwik.mjs +323 -83
  37. package/dist/qwik.cjs +342 -83
  38. package/dist/qwik.d.cts +4 -2
  39. package/dist/qwik.d.ts +4 -2
  40. package/dist/qwik.js +342 -83
  41. package/dist/react.cjs +347 -85
  42. package/dist/react.d.cts +3 -4
  43. package/dist/react.d.ts +3 -4
  44. package/dist/react.js +347 -85
  45. package/dist/solid.cjs +348 -83
  46. package/dist/solid.d.cts +37 -1
  47. package/dist/solid.d.ts +37 -1
  48. package/dist/solid.js +348 -83
  49. package/dist/solid.source.jsx +356 -83
  50. package/dist/styles.css +24 -4
  51. package/dist/svelte-core.cjs +334 -83
  52. package/dist/svelte-core.js +334 -83
  53. package/dist/svelte-src/BottomSheet.svelte +14 -0
  54. package/dist/svelte.cjs +344 -83
  55. package/dist/svelte.d.cts +2 -0
  56. package/dist/svelte.d.ts +2 -0
  57. package/dist/svelte.js +344 -83
  58. package/dist/vue.cjs +350 -83
  59. package/dist/vue.js +350 -83
  60. package/dist/webgl.cjs +673 -0
  61. package/dist/webgl.d.cts +17 -0
  62. package/dist/webgl.d.ts +17 -0
  63. package/dist/webgl.js +671 -0
  64. package/package.json +64 -40
  65. package/dist/index-DdYLOIIT.d.ts +0 -15
  66. package/dist/index-dfrzw-lI.d.cts +0 -15
@@ -300,6 +300,11 @@ class SnapResolver {
300
300
  this.raw = raw;
301
301
  this.recompute();
302
302
  }
303
+ setMode(mode) {
304
+ if (mode === this.mode) return;
305
+ this.mode = mode;
306
+ this.recompute();
307
+ }
303
308
  setAllowedIds(ids) {
304
309
  this.allowedIds = ids;
305
310
  this.rangeCache = null;
@@ -902,6 +907,11 @@ class ScrimController {
902
907
  interactive
903
908
  });
904
909
  }
910
+ setSheetMode(mode) {
911
+ if (this.isDestroyed() || mode === this.mode) return;
912
+ this.mode = mode;
913
+ this.invalidateOpacityCache();
914
+ }
905
915
  setScrimMode(mode, applySize) {
906
916
  if (this.isDestroyed()) return;
907
917
  if (mode === this.scrimMode) return;
@@ -1832,30 +1842,47 @@ class SampleRing {
1832
1842
  this.size = 0;
1833
1843
  }
1834
1844
  }
1835
- const installGestures = (handle, mode, callbacks) => {
1845
+ const installGestures = (handle, mode, callbacks, options = {}) => {
1836
1846
  let activePointerId = null;
1837
1847
  let activePointerType = "touch";
1838
1848
  let startCoord = 0;
1849
+ let startCross = 0;
1839
1850
  let lastCoord = 0;
1851
+ let pendingStart = false;
1840
1852
  const VELOCITY_WINDOW_MS = (kind) => kind === "mouse" ? 160 : 120;
1841
1853
  const MAX_SAMPLES = 32;
1842
1854
  const samples = new SampleRing(MAX_SAMPLES);
1843
1855
  const axis = isAxisVertical(mode) ? "Y" : "X";
1844
1856
  const sign = mode === "bottom" || mode === "right" ? -1 : 1;
1845
1857
  const coordOf = (e) => axis === "Y" ? e.clientY : e.clientX;
1858
+ const crossOf = (e) => axis === "Y" ? e.clientX : e.clientY;
1859
+ const capture = (pointerId) => {
1860
+ try {
1861
+ handle.setPointerCapture(pointerId);
1862
+ } catch {
1863
+ }
1864
+ };
1865
+ const releaseTracking = () => {
1866
+ activePointerId = null;
1867
+ pendingStart = false;
1868
+ samples.reset();
1869
+ };
1846
1870
  const onPointerDown = (e) => {
1847
1871
  if (activePointerId !== null) return;
1848
1872
  if (e.button !== void 0 && e.button !== 0) return;
1873
+ if (options.shouldStart && !options.shouldStart(e)) return;
1849
1874
  activePointerId = e.pointerId;
1850
1875
  activePointerType = e.pointerType || "touch";
1851
1876
  startCoord = coordOf(e);
1877
+ startCross = crossOf(e);
1852
1878
  lastCoord = startCoord;
1853
1879
  samples.reset();
1854
1880
  samples.push(e.timeStamp, startCoord);
1855
- try {
1856
- handle.setPointerCapture(e.pointerId);
1857
- } catch {
1881
+ if (options.deferStart) {
1882
+ pendingStart = true;
1883
+ return;
1858
1884
  }
1885
+ capture(e.pointerId);
1859
1886
  callbacks.onStart(startCoord, activePointerType);
1860
1887
  };
1861
1888
  const onPointerMove = (e) => {
@@ -1863,6 +1890,22 @@ const installGestures = (handle, mode, callbacks) => {
1863
1890
  const coord = coordOf(e);
1864
1891
  const rawDelta = coord - startCoord;
1865
1892
  const delta = rawDelta * sign;
1893
+ if (pendingStart) {
1894
+ const verdict = options.deferStart(e, delta, crossOf(e) - startCross);
1895
+ if (verdict === false) {
1896
+ releaseTracking();
1897
+ return;
1898
+ }
1899
+ if (verdict !== true) return;
1900
+ pendingStart = false;
1901
+ startCoord = coord;
1902
+ lastCoord = coord;
1903
+ samples.reset();
1904
+ samples.push(e.timeStamp, coord);
1905
+ capture(e.pointerId);
1906
+ callbacks.onStart(startCoord, activePointerType);
1907
+ return;
1908
+ }
1866
1909
  lastCoord = coord;
1867
1910
  samples.push(e.timeStamp, coord);
1868
1911
  const cutoff = e.timeStamp - VELOCITY_WINDOW_MS(activePointerType);
@@ -1871,6 +1914,10 @@ const installGestures = (handle, mode, callbacks) => {
1871
1914
  };
1872
1915
  const finishGesture = (e) => {
1873
1916
  if (e.pointerId !== activePointerId) return;
1917
+ if (pendingStart) {
1918
+ releaseTracking();
1919
+ return;
1920
+ }
1874
1921
  const rawDelta = lastCoord - startCoord;
1875
1922
  const delta = rawDelta * sign;
1876
1923
  let velocity = 0;
@@ -1893,22 +1940,26 @@ const installGestures = (handle, mode, callbacks) => {
1893
1940
  };
1894
1941
  const onPointerCancel = (e) => {
1895
1942
  if (e.pointerId !== activePointerId) return;
1896
- activePointerId = null;
1897
- samples.reset();
1943
+ const wasPending = pendingStart;
1944
+ releaseTracking();
1945
+ if (wasPending) return;
1898
1946
  callbacks.onCancel?.();
1899
1947
  };
1900
1948
  handle.addEventListener("pointerdown", onPointerDown);
1901
1949
  handle.addEventListener("pointermove", onPointerMove);
1902
1950
  handle.addEventListener("pointerup", finishGesture);
1903
1951
  handle.addEventListener("pointercancel", onPointerCancel);
1952
+ const managesTouchAction = options.manageTouchAction !== false;
1904
1953
  const prevTouchAction = handle.style.touchAction;
1905
- handle.style.touchAction = isAxisVertical(mode) ? "pan-x" : "pan-y";
1954
+ if (managesTouchAction) {
1955
+ handle.style.touchAction = isAxisVertical(mode) ? "pan-x" : "pan-y";
1956
+ }
1906
1957
  return () => {
1907
1958
  handle.removeEventListener("pointerdown", onPointerDown);
1908
1959
  handle.removeEventListener("pointermove", onPointerMove);
1909
1960
  handle.removeEventListener("pointerup", finishGesture);
1910
1961
  handle.removeEventListener("pointercancel", onPointerCancel);
1911
- handle.style.touchAction = prevTouchAction;
1962
+ if (managesTouchAction) handle.style.touchAction = prevTouchAction;
1912
1963
  if (activePointerId !== null && handle.hasPointerCapture(activePointerId)) {
1913
1964
  handle.releasePointerCapture(activePointerId);
1914
1965
  }
@@ -2026,7 +2077,7 @@ class GestureController {
2026
2077
  const restoreTo = this.dragStartSize;
2027
2078
  void this.deps.animateTo(restoreTo, 0);
2028
2079
  }
2029
- });
2080
+ }, this.deps.gestureOptions);
2030
2081
  return this.detach;
2031
2082
  }
2032
2083
  forceClearDragState() {
@@ -2037,6 +2088,68 @@ class GestureController {
2037
2088
  this.deps.element.style.willChange = "";
2038
2089
  }
2039
2090
  }
2091
+ const DRAG_ZONE_SELECTOR = "[data-bs-drag]";
2092
+ const NO_DRAG_SELECTOR = "[data-bs-no-drag]";
2093
+ const asElement = (target) => {
2094
+ const candidate = target;
2095
+ return candidate && typeof candidate.closest === "function" ? candidate : null;
2096
+ };
2097
+ function isDragAllowedFrom(target, mode) {
2098
+ const el = asElement(target);
2099
+ if (!el) return mode !== "zones";
2100
+ if (el.closest(NO_DRAG_SELECTOR)) return false;
2101
+ if (mode === "zones") return el.closest(DRAG_ZONE_SELECTOR) !== null;
2102
+ return true;
2103
+ }
2104
+ const CONTENT_DRAG_SLOP = 6;
2105
+ function decideContentGesture(input) {
2106
+ const slop = input.slop ?? CONTENT_DRAG_SLOP;
2107
+ const cross = input.crossDelta ?? 0;
2108
+ const sharesScrollAxis = input.sharesScrollAxis ?? true;
2109
+ if (!sharesScrollAxis) {
2110
+ if (Math.abs(input.delta) < slop && Math.abs(cross) < slop) return "pending";
2111
+ if (Math.abs(input.delta) <= Math.abs(cross)) return "scroll";
2112
+ return "drag";
2113
+ }
2114
+ if (Math.abs(input.delta) < slop) return "pending";
2115
+ if (input.scrollTop > 0) return "scroll";
2116
+ if (input.delta > 0 && input.atMaxSnap) return "scroll";
2117
+ return "drag";
2118
+ }
2119
+ const isLogicalMode = (mode) => mode === "start" || mode === "end";
2120
+ const readDirection = (el) => {
2121
+ if (!el) return "ltr";
2122
+ if (typeof getComputedStyle === "function") {
2123
+ try {
2124
+ if (getComputedStyle(el).direction === "rtl") return "rtl";
2125
+ } catch {
2126
+ }
2127
+ }
2128
+ try {
2129
+ const marked = el.closest?.("[dir]");
2130
+ if (marked) {
2131
+ return marked.getAttribute("dir")?.toLowerCase() === "rtl" ? "rtl" : "ltr";
2132
+ }
2133
+ } catch {
2134
+ }
2135
+ return "ltr";
2136
+ };
2137
+ const resolveMode = (mode, el) => {
2138
+ if (!isLogicalMode(mode)) return mode;
2139
+ const rtl = readDirection(el) === "rtl";
2140
+ if (mode === "start") return rtl ? "right" : "left";
2141
+ return rtl ? "left" : "right";
2142
+ };
2143
+ function installTouchScrollGuard(surface, isDragging) {
2144
+ const onTouchMove = (e) => {
2145
+ if (!isDragging()) return;
2146
+ if (e.cancelable) e.preventDefault();
2147
+ };
2148
+ surface.addEventListener("touchmove", onTouchMove, {
2149
+ passive: false
2150
+ });
2151
+ return () => surface.removeEventListener("touchmove", onTouchMove);
2152
+ }
2040
2153
  class AriaSliderWriter {
2041
2154
  constructor(handle, mode) {
2042
2155
  __publicField(this, "handle");
@@ -2427,9 +2540,11 @@ function resolveEngineOptions(opts, extras) {
2427
2540
  }
2428
2541
  }
2429
2542
  return {
2430
- mode: opts.mode ?? "bottom",
2543
+ mode: resolveMode(opts.mode ?? "bottom", opts.element),
2431
2544
  flickVelocity: opts.flickVelocity ?? DEFAULT_FLICK_VELOCITY,
2432
2545
  dragThreshold: opts.dragThreshold ?? DEFAULT_DRAG_THRESHOLD,
2546
+ dragFrom: opts.dragFrom ?? (opts.handle ? "handle" : "sheet"),
2547
+ dragFromContent: opts.dragFromContent ?? true,
2433
2548
  rubberBandEnabled: opts.rubberBand ?? true,
2434
2549
  closeOnBack: opts.closeOnBack ?? false,
2435
2550
  persistent: opts.persistent ?? false,
@@ -2511,6 +2626,8 @@ class BottomSheetCore {
2511
2626
  __publicField(this, "mode");
2512
2627
  __publicField(this, "flickVelocity");
2513
2628
  __publicField(this, "dragThreshold");
2629
+ __publicField(this, "dragFromMode");
2630
+ __publicField(this, "dragFromContentDefault");
2514
2631
  __publicField(this, "rubberBandEnabled");
2515
2632
  __publicField(this, "scrim");
2516
2633
  __publicField(this, "aria");
@@ -2523,13 +2640,18 @@ class BottomSheetCore {
2523
2640
  __publicField(this, "disableDragFlag");
2524
2641
  __publicField(this, "closeOnRouteChange");
2525
2642
  __publicField(this, "maxHeight");
2643
+ __publicField(this, "maxHeightDeps");
2526
2644
  __publicField(this, "snapPointsRaw");
2527
2645
  __publicField(this, "snaps");
2528
2646
  __publicField(this, "activeId");
2529
2647
  __publicField(this, "size", 0);
2530
2648
  __publicField(this, "gesture");
2649
+ __publicField(this, "contentGesture");
2650
+ __publicField(this, "detachSheetGesture", null);
2651
+ __publicField(this, "detachSliderKeyboard", null);
2531
2652
  __publicField(this, "rootEl", null);
2532
2653
  __publicField(this, "destroyed", false);
2654
+ __publicField(this, "restClosed", false);
2533
2655
  __publicField(this, "currentViewTransition", null);
2534
2656
  __publicField(this, "currentAbort", new AbortController());
2535
2657
  __publicField(this, "sizeWriteSentinel", new WriteSentinel());
@@ -2579,10 +2701,13 @@ class BottomSheetCore {
2579
2701
  this.screenComponent = opts.scrim ?? opts.screenComponent;
2580
2702
  this.scrimParent = this.screenComponent?.parentElement ?? null;
2581
2703
  this.mode = resolved.mode;
2704
+ this.element.dataset.mode = this.mode;
2582
2705
  this.transformTemplate = buildTransformTemplate(this.mode);
2583
2706
  this.aria = new AriaSliderWriter(this.handle, this.mode);
2584
2707
  this.flickVelocity = resolved.flickVelocity;
2585
2708
  this.dragThreshold = resolved.dragThreshold;
2709
+ this.dragFromMode = resolved.dragFrom;
2710
+ this.dragFromContentDefault = resolved.dragFromContent;
2586
2711
  this.rubberBandEnabled = resolved.rubberBandEnabled;
2587
2712
  this.scrim = new ScrimController({
2588
2713
  mode: this.mode,
@@ -2605,7 +2730,7 @@ class BottomSheetCore {
2605
2730
  getRootEl: () => this.rootEl,
2606
2731
  applySize: (size) => this.applySize(size),
2607
2732
  getSize: () => this.size,
2608
- isDragging: () => this.gesture?.isDragging ?? false,
2733
+ isDragging: () => this.isDraggingAny(),
2609
2734
  applyAux: (size) => this.applySize(size, true),
2610
2735
  getTransformFor: (size) => this.transformTemplate(this.snaps.getMaxAxisSize() - size)
2611
2736
  }, resolved.animation);
@@ -2634,7 +2759,7 @@ class BottomSheetCore {
2634
2759
  auditVhUsage(this.snapPointsRaw);
2635
2760
  this.auditDuplicateSnapIds(this.snapPointsRaw);
2636
2761
  this.rootEl = this.element.closest(".bs-root");
2637
- this.maxHeight = createMaxHeightController({
2762
+ this.maxHeightDeps = {
2638
2763
  element: this.element,
2639
2764
  mode: this.mode,
2640
2765
  getMaxAxisSize: () => this.snaps.getMaxAxisSize(),
@@ -2642,7 +2767,8 @@ class BottomSheetCore {
2642
2767
  this.snaps.setMaxAxisSize(size);
2643
2768
  },
2644
2769
  recompute: () => this.recompute()
2645
- });
2770
+ };
2771
+ this.maxHeight = createMaxHeightController(this.maxHeightDeps);
2646
2772
  this.fitDeps = {
2647
2773
  element: this.element,
2648
2774
  scrollContainer: this.scrollContainer,
@@ -2655,6 +2781,7 @@ class BottomSheetCore {
2655
2781
  const initial = this.snaps.findById(this.activeId);
2656
2782
  if (initial) this.size = initial.size;
2657
2783
  this.applySize(this.size);
2784
+ this.setRestClosed(this.size === 0);
2658
2785
  this.updateAriaSlider();
2659
2786
  this.scrollCache = createScrollCache({
2660
2787
  scrollContainer: this.scrollContainer,
@@ -2682,7 +2809,7 @@ class BottomSheetCore {
2682
2809
  scrollContainer: this.scrollContainer,
2683
2810
  hasFitSnap: () => this.snapPointsRaw.some((p) => p.size === "fit" || p.size === "content"),
2684
2811
  isDestroyed: () => this.destroyed,
2685
- isDragging: () => this.gesture?.isDragging ?? false,
2812
+ isDragging: () => this.isDraggingAny(),
2686
2813
  recompute: () => this.recompute()
2687
2814
  }));
2688
2815
  if (resolved.radius !== void 0) this.setRadius(resolved.radius);
@@ -2707,7 +2834,7 @@ class BottomSheetCore {
2707
2834
  autoCollapseAfter
2708
2835
  },
2709
2836
  isDestroyed: () => this.destroyed,
2710
- isDragging: () => this.gesture?.isDragging ?? false,
2837
+ isDragging: () => this.isDraggingAny(),
2711
2838
  isAnimating: () => this.animation.isAnimating,
2712
2839
  isTopSheet: () => this.isTopSheet,
2713
2840
  isVerticalAxis: () => this.mode === "bottom" || this.mode === "top",
@@ -2737,6 +2864,7 @@ class BottomSheetCore {
2737
2864
  void this.snapTo(id);
2738
2865
  },
2739
2866
  close: (reason) => this.close(reason),
2867
+ attachDragSurface: (surface, kind) => this.attachDragSurface(surface, kind),
2740
2868
  on: (event, fn) => this.on(event, fn),
2741
2869
  addTeardown: (fn) => this.teardowns.add(fn)
2742
2870
  };
@@ -2761,7 +2889,7 @@ class BottomSheetCore {
2761
2889
  return {
2762
2890
  size: this.size,
2763
2891
  activeId: this.activeId,
2764
- isDragging: this.gesture?.isDragging ?? false,
2892
+ isDragging: this.isDraggingAny(),
2765
2893
  isAnimating: this.animation.isAnimating,
2766
2894
  progress: this.computeProgress(this.size)
2767
2895
  };
@@ -2883,9 +3011,16 @@ class BottomSheetCore {
2883
3011
  progress: this.computeProgress(this.size)
2884
3012
  });
2885
3013
  if (withHaptic) this.haptic();
3014
+ this.setRestClosed(targetSize === 0);
2886
3015
  if (wasClosed && targetSize > 0) this.emitOpenSequence(id);
2887
3016
  if (targetSize === 0) this.emitCloseSequence();
2888
3017
  }
3018
+ setRestClosed(closed) {
3019
+ if (closed === this.restClosed) return;
3020
+ this.restClosed = closed;
3021
+ if (closed) this.element.setAttribute("data-bs-rest", "closed");
3022
+ else this.element.removeAttribute("data-bs-rest");
3023
+ }
2889
3024
  emitOpenSequence(id) {
2890
3025
  this.emit("open", {
2891
3026
  id
@@ -2949,6 +3084,51 @@ class BottomSheetCore {
2949
3084
  if (this.destroyed) return;
2950
3085
  this.disableDragFlag = value;
2951
3086
  }
3087
+ setDragFromContent(value) {
3088
+ if (this.destroyed) return;
3089
+ this.dragFromContentDefault = value;
3090
+ }
3091
+ setDragFrom(mode) {
3092
+ if (this.destroyed || mode === this.dragFromMode) return;
3093
+ this.gesture?.forceClearDragState();
3094
+ this.detachSheetGesture?.();
3095
+ this.dragFromMode = mode;
3096
+ this.mountSheetGesture();
3097
+ }
3098
+ getDragFrom() {
3099
+ return this.dragFromMode;
3100
+ }
3101
+ setMode(mode) {
3102
+ if (this.destroyed) return;
3103
+ const next = resolveMode(mode, this.element);
3104
+ if (next === this.mode) return;
3105
+ const wasVertical = this.mode === "bottom" || this.mode === "top";
3106
+ const nowVertical = next === "bottom" || next === "top";
3107
+ this.gesture?.forceClearDragState();
3108
+ this.detachSheetGesture?.();
3109
+ this.animation.cancel();
3110
+ this.element.style.removeProperty(layoutAxis(this.mode));
3111
+ this.mode = next;
3112
+ this.element.dataset.mode = next;
3113
+ this.transformTemplate = buildTransformTemplate(next);
3114
+ this.maxHeightDeps.mode = next;
3115
+ this.fitDeps.mode = next;
3116
+ this.snaps.setMode(next);
3117
+ this.scrim.setSheetMode(next);
3118
+ if (wasVertical !== nowVertical) {
3119
+ this.aria = new AriaSliderWriter(this.handle, next);
3120
+ }
3121
+ this.detachSliderKeyboard?.();
3122
+ this.mountSliderKeyboard();
3123
+ this.mountSheetGesture();
3124
+ this.recompute();
3125
+ const active = this.snaps.findById(this.activeId);
3126
+ if (active) this.applySize(active.size);
3127
+ this.updateAriaSlider();
3128
+ }
3129
+ getMode() {
3130
+ return this.mode;
3131
+ }
2952
3132
  isTop() {
2953
3133
  return this.isTopSheet;
2954
3134
  }
@@ -3095,12 +3275,24 @@ class BottomSheetCore {
3095
3275
  auditVhUsage(points);
3096
3276
  this.auditDuplicateSnapIds(points);
3097
3277
  this.snaps.setRaw(points);
3098
- if (allowed) this.snaps.setAllowedIds(allowed);
3278
+ if (allowed) {
3279
+ this.snaps.setAllowedIds(allowed);
3280
+ } else {
3281
+ const present = this.snaps.getResolvedSnaps().map((s) => s.id);
3282
+ const kept = this.snaps.getAllowedIds().filter((id) => present.includes(id));
3283
+ this.snaps.setAllowedIds(kept.length ? kept : present);
3284
+ }
3099
3285
  this.scrollCache.clear();
3100
3286
  this.scrim.invalidateOpacityCache();
3101
3287
  const current = this.snaps.findById(this.activeId);
3102
3288
  if (current) {
3103
3289
  this.applySize(current.size);
3290
+ } else {
3291
+ const fallback = this.snaps.getResolvedSnaps().find((s) => this.snaps.getAllowedIds().includes(s.id)) ?? this.snaps.getResolvedSnaps()[0];
3292
+ if (fallback) {
3293
+ this.activeId = fallback.id;
3294
+ this.applySize(fallback.size);
3295
+ }
3104
3296
  }
3105
3297
  this.updateAriaSlider();
3106
3298
  }
@@ -3110,7 +3302,7 @@ class BottomSheetCore {
3110
3302
  this.snaps.recompute();
3111
3303
  this.maxHeight.clampTo();
3112
3304
  this.scrim.invalidateOpacityCache();
3113
- if (this.gesture?.isDragging) return;
3305
+ if (this.isDraggingAny()) return;
3114
3306
  this.healActiveSnap();
3115
3307
  }
3116
3308
  healActiveSnap() {
@@ -3132,6 +3324,8 @@ class BottomSheetCore {
3132
3324
  this.bus.clear();
3133
3325
  this.animation.cancel();
3134
3326
  this.gesture?.forceClearDragState();
3327
+ this.contentGesture?.forceClearDragState();
3328
+ this.setRestClosed(false);
3135
3329
  this.teardowns.drain();
3136
3330
  this.scrim.destroy();
3137
3331
  this.animation.destroy();
@@ -3242,11 +3436,12 @@ class BottomSheetCore {
3242
3436
  const scale = Math.max(1 - depth * 0.04, 0.86);
3243
3437
  this.element.style.scale = scale === 1 ? "" : String(scale);
3244
3438
  }
3245
- attach() {
3246
- this.gesture = new GestureController({
3247
- handle: this.handle,
3439
+ buildGestureDeps(surface, gestureOptions) {
3440
+ return {
3441
+ handle: surface,
3248
3442
  element: this.element,
3249
3443
  mode: this.mode,
3444
+ gestureOptions,
3250
3445
  getRoot: () => this.rootEl,
3251
3446
  getDragContext: () => {
3252
3447
  const buf = this.dragContextBuf;
@@ -3265,8 +3460,83 @@ class BottomSheetCore {
3265
3460
  settleAfterDrag: (delta, velocity, kind) => this.settleAfterDrag(delta, velocity, kind),
3266
3461
  emit: (event, payload) => this.emit(event, payload),
3267
3462
  listenerCount: (event) => this.bus.listenerCount(event)
3463
+ };
3464
+ }
3465
+ isContentDragAllowed() {
3466
+ if (this.disableDragFlag) return false;
3467
+ const point = this.snapPointsRaw.find((p) => p.id === this.activeId);
3468
+ return point?.dragFromContent ?? this.dragFromContentDefault;
3469
+ }
3470
+ decideContentDrag(surface, e, delta, crossDelta) {
3471
+ if (this.destroyed) return false;
3472
+ if (e.pointerType === "mouse") return false;
3473
+ if (this.gesture?.isDragging) return false;
3474
+ if (!this.isContentDragAllowed()) return false;
3475
+ if (!isDragAllowedFrom(e.target, "sheet")) return false;
3476
+ const decision = decideContentGesture({
3477
+ delta,
3478
+ crossDelta,
3479
+ sharesScrollAxis: this.mode === "bottom" || this.mode === "top",
3480
+ scrollTop: surface.scrollTop,
3481
+ atMaxSnap: this.size >= this.getAllowedRange().max - 0.5
3482
+ });
3483
+ if (decision === "pending") return null;
3484
+ return decision === "drag";
3485
+ }
3486
+ attachDragSurface(surface, kind) {
3487
+ if (this.destroyed || kind !== "content") return;
3488
+ if (this.contentGesture) return;
3489
+ const controller = new GestureController(this.buildGestureDeps(surface, {
3490
+ manageTouchAction: false,
3491
+ deferStart: (e, delta, crossDelta) => this.decideContentDrag(surface, e, delta, crossDelta)
3492
+ }));
3493
+ this.contentGesture = controller;
3494
+ const detachGestures = controller.install();
3495
+ const detachGuard = installTouchScrollGuard(surface, () => controller.isDragging);
3496
+ return () => {
3497
+ detachGuard();
3498
+ detachGestures();
3499
+ if (this.contentGesture === controller) this.contentGesture = void 0;
3500
+ };
3501
+ }
3502
+ sheetDragSurface() {
3503
+ return this.dragFromMode === "handle" ? this.handle : this.element;
3504
+ }
3505
+ canStartSheetDrag(e) {
3506
+ if (this.contentGesture?.isDragging) return false;
3507
+ const target = e.target;
3508
+ const scoped = typeof target?.closest === "function";
3509
+ if (scoped && target.closest(NO_DRAG_SELECTOR)) return false;
3510
+ if (this.handle !== this.element && scoped && this.handle.contains(target)) {
3511
+ return true;
3512
+ }
3513
+ if (!isDragAllowedFrom(e.target, this.dragFromMode)) return false;
3514
+ if (this.dragFromMode === "handle") return true;
3515
+ if (this.scrollContainer && scoped && this.scrollContainer.contains(target) && !target.closest(DRAG_ZONE_SELECTOR)) {
3516
+ return false;
3517
+ }
3518
+ return true;
3519
+ }
3520
+ mountSheetGesture() {
3521
+ const surface = this.sheetDragSurface();
3522
+ const controller = new GestureController(this.buildGestureDeps(surface, {
3523
+ shouldStart: (e) => this.canStartSheetDrag(e),
3524
+ manageTouchAction: this.dragFromMode === "handle"
3525
+ }));
3526
+ this.gesture = controller;
3527
+ const detachGestures = controller.install();
3528
+ const detachGuard = this.dragFromMode === "handle" ? null : installTouchScrollGuard(surface, () => controller.isDragging);
3529
+ this.detachSheetGesture = () => {
3530
+ detachGuard?.();
3531
+ detachGestures();
3532
+ };
3533
+ }
3534
+ attach() {
3535
+ this.mountSheetGesture();
3536
+ this.teardowns.add(() => {
3537
+ this.detachSheetGesture?.();
3538
+ this.detachSheetGesture = null;
3268
3539
  });
3269
- this.teardowns.add(this.gesture.install());
3270
3540
  try {
3271
3541
  this.attachFeatures();
3272
3542
  } catch (err) {
@@ -3303,7 +3573,7 @@ class BottomSheetCore {
3303
3573
  element: this.element,
3304
3574
  getMode: () => this.mode,
3305
3575
  isDestroyed: () => this.destroyed,
3306
- isDragging: () => this.gesture?.isDragging ?? false,
3576
+ isDragging: () => this.isDraggingAny(),
3307
3577
  resolveActiveSnap: () => this.snaps.findById(this.activeId),
3308
3578
  getMaxAxisSize: () => this.snaps.getMaxAxisSize(),
3309
3579
  getSize: () => this.size,
@@ -3336,7 +3606,11 @@ class BottomSheetCore {
3336
3606
  this.teardowns.add(() => document.removeEventListener("keydown", onKey));
3337
3607
  }
3338
3608
  }
3339
- this.teardowns.add(installSliderKeyboard({
3609
+ this.mountSliderKeyboard();
3610
+ this.scrim.attach();
3611
+ }
3612
+ mountSliderKeyboard() {
3613
+ const detach = installSliderKeyboard({
3340
3614
  handle: this.handle,
3341
3615
  mode: this.mode,
3342
3616
  isDestroyed: () => this.destroyed,
@@ -3345,8 +3619,12 @@ class BottomSheetCore {
3345
3619
  snapTo: (id) => {
3346
3620
  void this.snapTo(id);
3347
3621
  }
3348
- }));
3349
- this.scrim.attach();
3622
+ });
3623
+ this.detachSliderKeyboard = detach;
3624
+ this.teardowns.add(() => {
3625
+ this.detachSliderKeyboard?.();
3626
+ this.detachSliderKeyboard = null;
3627
+ });
3350
3628
  }
3351
3629
  settleAfterDrag(delta, velocity, kind = "touch") {
3352
3630
  const signal = this.newCycle();
@@ -3393,8 +3671,11 @@ class BottomSheetCore {
3393
3671
  this.completeSnap(target.id, target.size, previousSize, previousSnapSize === 0, true);
3394
3672
  });
3395
3673
  }
3674
+ isDraggingAny() {
3675
+ return (this.gesture?.isDragging ?? false) || (this.contentGesture?.isDragging ?? false);
3676
+ }
3396
3677
  clearWillChangeIfIdle() {
3397
- if (this.animation.isAnimating || this.gesture?.isDragging) return;
3678
+ if (this.animation.isAnimating || this.isDraggingAny()) return;
3398
3679
  this.element.style.willChange = "";
3399
3680
  }
3400
3681
  haptic() {
@@ -3407,8 +3688,9 @@ class BottomSheetCore {
3407
3688
  }
3408
3689
  applySize(size, skipTransform = false) {
3409
3690
  const cap = this.snaps.getMaxAxisSize();
3410
- const clamped = cap > 0 && size > cap && !this.gesture?.isDragging && !this.allowOvershoot ? cap : size;
3691
+ const clamped = cap > 0 && size > cap && !this.isDraggingAny() && !this.allowOvershoot ? cap : size;
3411
3692
  this.size = clamped;
3693
+ if (clamped > 0) this.setRestClosed(false);
3412
3694
  const offset = cap - clamped;
3413
3695
  const style = this.element.style;
3414
3696
  if (!skipTransform) style.transform = this.transformTemplate(offset);
@@ -3780,55 +4062,9 @@ function installAutoCollapse(deps) {
3780
4062
  unsubscribe();
3781
4063
  };
3782
4064
  }
3783
- const CONTENT_PULL_THRESHOLD = 60;
3784
- function installContentSwipe(deps) {
3785
- const { container } = deps;
3786
- let startY = 0;
3787
- let startScrollTop = 0;
3788
- let moved = false;
3789
- const onTouchStart = (e) => {
3790
- const t = e.touches[0];
3791
- if (!t) return;
3792
- startY = t.clientY;
3793
- startScrollTop = container.scrollTop;
3794
- moved = false;
3795
- };
3796
- const onTouchMove = (e) => {
3797
- const t = e.touches[0];
3798
- if (!t) return;
3799
- if (Math.abs(t.clientY - startY) > CONTENT_PULL_THRESHOLD) moved = true;
3800
- };
3801
- const onTouchEnd = (e) => {
3802
- if (!moved) return;
3803
- if (deps.isDragging() || deps.isAnimating()) return;
3804
- if (container.scrollTop !== startScrollTop) return;
3805
- const t = e.changedTouches[0];
3806
- if (!t) return;
3807
- const deltaY = t.clientY - startY;
3808
- const atTop = container.scrollTop <= 0;
3809
- const allowed = deps.getAllowedIds();
3810
- const idx = allowed.indexOf(deps.getActiveId());
3811
- if (idx === -1) return;
3812
- if (atTop && deltaY > CONTENT_PULL_THRESHOLD && idx > 0) {
3813
- deps.snapTo(allowed[idx - 1]);
3814
- } else if (deltaY < -CONTENT_PULL_THRESHOLD && idx < allowed.length - 1) {
3815
- deps.snapTo(allowed[idx + 1]);
3816
- }
3817
- };
3818
- container.addEventListener("touchstart", onTouchStart, {
3819
- passive: true
3820
- });
3821
- container.addEventListener("touchmove", onTouchMove, {
3822
- passive: true
3823
- });
3824
- container.addEventListener("touchend", onTouchEnd, {
3825
- passive: true
3826
- });
3827
- return () => {
3828
- container.removeEventListener("touchstart", onTouchStart);
3829
- container.removeEventListener("touchmove", onTouchMove);
3830
- container.removeEventListener("touchend", onTouchEnd);
3831
- };
4065
+ function installContentDrag(deps) {
4066
+ const detach = deps.attachDragSurface(deps.container, "content");
4067
+ return () => detach?.();
3832
4068
  }
3833
4069
  const SAFETY_PAD = 8;
3834
4070
  function installVisualViewport(deps) {
@@ -3930,13 +4166,9 @@ function contentSwipeFeature() {
3930
4166
  stage: "attach",
3931
4167
  install: (ctx) => {
3932
4168
  if (!ctx.scrollContainer) return;
3933
- return installContentSwipe({
4169
+ return installContentDrag({
3934
4170
  container: ctx.scrollContainer,
3935
- isDragging: ctx.isDragging,
3936
- isAnimating: ctx.isAnimating,
3937
- getAllowedIds: ctx.allowedIdsBySize,
3938
- getActiveId: ctx.getActiveId,
3939
- snapTo: ctx.snapTo
4171
+ attachDragSurface: ctx.attachDragSurface
3940
4172
  });
3941
4173
  }
3942
4174
  };
@@ -4067,6 +4299,8 @@ const BottomSheet = component$((props) => {
4067
4299
  persistent: props.persistent,
4068
4300
  disableClose: props.disableClose,
4069
4301
  disableDrag: props.disableDrag,
4302
+ dragFrom: props.dragFrom,
4303
+ dragFromContent: props.dragFromContent,
4070
4304
  closeOnRouteChange: props.closeOnRouteChange,
4071
4305
  stackEffect: props.stackEffect,
4072
4306
  returnFocusTo: props.returnFocusTo,
@@ -4174,11 +4408,17 @@ const BottomSheet = component$((props) => {
4174
4408
  const persistent = track(() => props.persistent);
4175
4409
  const disableClose = track(() => props.disableClose);
4176
4410
  const disableDrag = track(() => props.disableDrag);
4411
+ const dragFromContent = track(() => props.dragFromContent);
4412
+ const dragFrom = track(() => props.dragFrom);
4177
4413
  const engine = engineStore.engine;
4178
4414
  if (!engine) return;
4179
4415
  if (persistent !== void 0) engine.setPersistent(persistent);
4180
4416
  if (disableClose !== void 0) engine.setDisableClose(disableClose);
4181
4417
  if (disableDrag !== void 0) engine.setDisableDrag(disableDrag);
4418
+ if (dragFromContent !== void 0) {
4419
+ engine.setDragFromContent(dragFromContent);
4420
+ }
4421
+ if (dragFrom !== void 0) engine.setDragFrom(dragFrom);
4182
4422
  });
4183
4423
  const showBackdrop = props.backdrop !== false;
4184
4424
  const mode = props.mode ?? "bottom";