@abhivarde/svelte-drawer 0.0.15 → 0.0.16

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.
package/README.md CHANGED
@@ -7,18 +7,19 @@ A drawer component for Svelte 5, inspired by [Vaul](https://github.com/emilkowal
7
7
 
8
8
  ## Features
9
9
 
10
- - ✅ Smooth animations and **gesture-driven dragging** (mouse & touch)
10
+ - ✅ Smooth animations with **gesture-driven dragging** (mouse & touch)
11
11
  - ✅ Mobile-optimized drag handling with **scroll prevention**
12
- - ✅ Multiple directions (bottom, top, left, right)
13
- - ✅ Prebuilt variants (default, sheet, dialog, minimal, sidebar)
12
+ - ✅ Support for multiple directions (**bottom, top, left, right**)
13
+ - ✅ Prebuilt variants (**default, sheet, dialog, minimal, sidebar**)
14
14
  - ✅ **Drag handle component** with auto-adaptive orientation
15
- - ✅ Nested drawers support
16
- - ✅ Scrollable content
17
- - ✅ Keyboard shortcuts (Escape to close, Tab navigation)
18
- - ✅ Focus management (auto-focus, focus trap, focus restoration)
15
+ - ✅ **Snap points** for iOS-like multi-height drawers
16
+ - ✅ Nested drawer support
17
+ - ✅ Scrollable content areas
18
+ - ✅ Keyboard shortcuts (**Escape to close**, Tab navigation)
19
+ - ✅ Focus management (**auto-focus, focus trap, focus restoration**)
19
20
  - ✅ Fully accessible with keyboard navigation
20
- - ✅ TypeScript support
21
- - ✅ Customizable styling with Tailwind CSS
21
+ - ✅ Full **TypeScript** support
22
+ - ✅ Customizable styling with **Tailwind CSS**
22
23
 
23
24
  ## Installation
24
25
 
@@ -192,6 +193,44 @@ npm install @abhivarde/svelte-drawer
192
193
  </Drawer>
193
194
  ```
194
195
 
196
+ ### Snap Points
197
+
198
+ Snap points allow the drawer to rest at predefined heights, creating an iOS-like sheet experience.
199
+
200
+ ```svelte
201
+ <script>
202
+ import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
203
+
204
+ let open = $state(false);
205
+ let activeSnapPoint = $state(undefined);
206
+ </script>
207
+
208
+ <Drawer
209
+ bind:open
210
+ snapPoints={[0.25, 0.5, 0.9]}
211
+ bind:activeSnapPoint
212
+ onSnapPointChange={(point) => console.log('Snapped to:', point)}
213
+ >
214
+ <DrawerOverlay class="fixed inset-0 bg-black/40" />
215
+ <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
216
+ <DrawerHandle class="mb-8" />
217
+ <h2>Drawer with Snap Points</h2>
218
+ <p>Drag to see snapping behavior at 25%, 50%, and 90%</p>
219
+
220
+ <!-- Programmatically change snap point -->
221
+ <button onclick={() => activeSnapPoint = 0.5}>Jump to 50%</button>
222
+ </DrawerContent>
223
+ </Drawer>
224
+ ```
225
+
226
+ **How it works:**
227
+
228
+ - Snap point values range from 0 to 1 (e.g., `0.5` = 50% of screen height)
229
+ - The drawer automatically snaps to the nearest point when released
230
+ - Dragging beyond the lowest snap point dismisses the drawer
231
+ - Use `bind:activeSnapPoint` to programmatically control the current position
232
+ - Use `onSnapPointChange` callback to react to snap changes
233
+
195
234
  ## Variants
196
235
 
197
236
  Available variants for `DrawerVariants` component:
@@ -220,6 +259,9 @@ Main wrapper component that manages drawer state and animations.
220
259
  - `onOpenChange` (function, optional) - Callback when open state changes
221
260
  - `direction` ('bottom' | 'top' | 'left' | 'right', default: 'bottom') - Direction from which drawer slides
222
261
  - `closeOnEscape` (boolean, optional, default: true) - Whether Escape key closes the drawer
262
+ - `snapPoints` (number[], optional) - Array of snap positions between 0-1, where 1 is fully open (e.g., `[0.25, 0.5, 0.9]`)
263
+ - `activeSnapPoint` (number, bindable, optional) - Current active snap point value
264
+ - `onSnapPointChange` (function, optional) - Callback fired when the drawer snaps to a different point
223
265
 
224
266
  ### DrawerOverlay
225
267
 
@@ -8,6 +8,9 @@
8
8
  onOpenChange = undefined,
9
9
  direction = "bottom",
10
10
  closeOnEscape = true,
11
+ snapPoints = undefined,
12
+ activeSnapPoint = $bindable(undefined),
13
+ onSnapPointChange = undefined,
11
14
  children,
12
15
  } = $props();
13
16
 
@@ -33,6 +36,14 @@
33
36
 
34
37
  overlayOpacity.set(1);
35
38
  drawerPosition.set(0);
39
+
40
+ if (
41
+ snapPoints &&
42
+ snapPoints.length > 0 &&
43
+ activeSnapPoint === undefined
44
+ ) {
45
+ activeSnapPoint = snapPoints[snapPoints.length - 1];
46
+ }
36
47
  } else if (visible) {
37
48
  overlayOpacity.set(0, { duration: 120 });
38
49
  drawerPosition.set(100, { duration: 180 });
@@ -86,6 +97,16 @@
86
97
  get direction() {
87
98
  return direction;
88
99
  },
100
+ get snapPoints() {
101
+ return snapPoints;
102
+ },
103
+ get activeSnapPoint() {
104
+ return activeSnapPoint;
105
+ },
106
+ setActiveSnapPoint(point: number) {
107
+ activeSnapPoint = point;
108
+ onSnapPointChange?.(point);
109
+ },
89
110
  closeDrawer,
90
111
  });
91
112
  </script>
@@ -3,7 +3,10 @@ declare const Drawer: import("svelte").Component<{
3
3
  onOpenChange?: any;
4
4
  direction?: string;
5
5
  closeOnEscape?: boolean;
6
+ snapPoints?: any;
7
+ activeSnapPoint?: any;
8
+ onSnapPointChange?: any;
6
9
  children: any;
7
- }, {}, "open">;
10
+ }, {}, "open" | "activeSnapPoint">;
8
11
  type Drawer = ReturnType<typeof Drawer>;
9
12
  export default Drawer;
@@ -8,6 +8,9 @@
8
8
  drawerPosition: { current: number; set: (v: number, opts?: any) => void };
9
9
  direction: "bottom" | "top" | "left" | "right";
10
10
  closeDrawer: () => void;
11
+ snapPoints?: number[];
12
+ activeSnapPoint?: number;
13
+ setActiveSnapPoint?: (point: number) => void;
11
14
  };
12
15
 
13
16
  let {
@@ -24,8 +27,53 @@
24
27
  let startDragPos = 0;
25
28
  let dragging = false;
26
29
 
30
+ function snapPointToPosition(snapPoint: number): number {
31
+ // snapPoint is 0-1 where 1 = fully open (0% position)
32
+ // Convert: 1 -> 0%, 0.5 -> 50%, 0 -> 100%
33
+ return (1 - snapPoint) * 100;
34
+ }
35
+
36
+ function findNearestSnapPoint(currentPos: number): number {
37
+ if (!drawer.snapPoints || drawer.snapPoints.length === 0) {
38
+ return currentPos;
39
+ }
40
+
41
+ const currentSnapValue = 1 - currentPos / 100;
42
+ let nearest = drawer.snapPoints[0];
43
+ let minDiff = Math.abs(currentSnapValue - nearest);
44
+
45
+ for (const snapPoint of drawer.snapPoints) {
46
+ const diff = Math.abs(currentSnapValue - snapPoint);
47
+ if (diff < minDiff) {
48
+ minDiff = diff;
49
+ nearest = snapPoint;
50
+ }
51
+ }
52
+
53
+ return nearest;
54
+ }
55
+
27
56
  function getTransform(): string {
28
57
  const pos = drawer.drawerPosition.current;
58
+
59
+ if (
60
+ drawer.snapPoints &&
61
+ drawer.activeSnapPoint !== undefined &&
62
+ !dragging
63
+ ) {
64
+ const snapPos = snapPointToPosition(drawer.activeSnapPoint);
65
+ switch (drawer.direction) {
66
+ case "bottom":
67
+ return `translateY(${snapPos}%)`;
68
+ case "top":
69
+ return `translateY(-${snapPos}%)`;
70
+ case "left":
71
+ return `translateX(-${snapPos}%)`;
72
+ case "right":
73
+ return `translateX(${snapPos}%)`;
74
+ }
75
+ }
76
+
29
77
  switch (drawer.direction) {
30
78
  case "bottom":
31
79
  return `translateY(${pos}%)`;
@@ -40,10 +88,10 @@
40
88
 
41
89
  function onPointerDown(e: PointerEvent | TouchEvent) {
42
90
  const target = e.target as HTMLElement;
43
-
91
+
44
92
  if (
45
- target.closest('button, a, input, textarea, select') &&
46
- !target.closest('[data-drawer-drag]')
93
+ target.closest("button, a, input, textarea, select") &&
94
+ !target.closest("[data-drawer-drag]")
47
95
  ) {
48
96
  return;
49
97
  }
@@ -101,15 +149,30 @@
101
149
 
102
150
  function onPointerUp() {
103
151
  if (!dragging) return;
104
-
152
+
105
153
  dragging = false;
106
154
 
107
155
  const pos = drawer.drawerPosition.current;
108
156
 
109
- if (pos > 30) {
110
- drawer.closeDrawer();
157
+ if (drawer.snapPoints && drawer.snapPoints.length > 0) {
158
+ const nearestSnapPoint = findNearestSnapPoint(pos);
159
+ const snapPos = snapPointToPosition(nearestSnapPoint);
160
+
161
+ const lowestSnapPoint = Math.min(...drawer.snapPoints);
162
+ const lowestSnapPos = snapPointToPosition(lowestSnapPoint);
163
+
164
+ if (pos > lowestSnapPos + 30) {
165
+ drawer.closeDrawer();
166
+ } else {
167
+ drawer.drawerPosition.set(snapPos);
168
+ drawer.setActiveSnapPoint?.(nearestSnapPoint);
169
+ }
111
170
  } else {
112
- drawer.drawerPosition.set(0);
171
+ if (pos > 30) {
172
+ drawer.closeDrawer();
173
+ } else {
174
+ drawer.drawerPosition.set(0);
175
+ }
113
176
  }
114
177
 
115
178
  window.removeEventListener("pointermove", onPointerMove);
@@ -180,4 +243,4 @@
180
243
  >
181
244
  {@render children()}
182
245
  </div>
183
- {/if}
246
+ {/if}
package/dist/types.d.ts CHANGED
@@ -3,6 +3,9 @@ export interface DrawerProps {
3
3
  onOpenChange?: (open: boolean) => void;
4
4
  direction?: "bottom" | "top" | "left" | "right";
5
5
  closeOnEscape?: boolean;
6
+ snapPoints?: number[];
7
+ activeSnapPoint?: number;
8
+ onSnapPointChange?: (snapPoint: number) => void;
6
9
  }
7
10
  export interface DrawerContentProps {
8
11
  class?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abhivarde/svelte-drawer",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "A drawer component for Svelte 5, inspired by Vaul",
5
5
  "author": "Abhi Varde",
6
6
  "license": "MIT",