css-drawer 0.1.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.
package/README.md ADDED
@@ -0,0 +1,470 @@
1
+ # CSS Drawer
2
+
3
+ A near drop-in replacement for [Vaul](https://vaul.emilkowal.ski) using native `<dialog>` and pure CSS animations.
4
+
5
+ **Zero JavaScript animations.** The only JS: `dialog.showModal()` and `dialog.close()`.
6
+
7
+ ## Why?
8
+
9
+ | Feature | Vaul | CSS Drawer |
10
+ |---------|------|------------|
11
+ | Bundle size | ~12KB | **1.4KB** JS + 8KB CSS (gzip: ~2.5KB total) |
12
+ | Animation engine | JavaScript | Pure CSS |
13
+ | Nesting | Manual setup | Automatic (CSS `:has()`) |
14
+ | Accessibility | Built-in | Automatic (native `<dialog>` + `inert`) |
15
+ | API | Controlled state | Native refs |
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install css-drawer
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Quick Start
26
+
27
+ ### React (Recommended)
28
+
29
+ ```tsx
30
+ import { useRef } from 'react'
31
+ import { Drawer } from 'css-drawer/react'
32
+
33
+ function App() {
34
+ const ref = useRef<HTMLDialogElement>(null)
35
+
36
+ return (
37
+ <>
38
+ <button onClick={() => ref.current?.showModal()}>
39
+ Open
40
+ </button>
41
+
42
+ <Drawer.Root>
43
+ <Drawer.Content ref={ref}>
44
+ <Drawer.Handle />
45
+ <div className="drawer-content">
46
+ <Drawer.Title>Title</Drawer.Title>
47
+ <Drawer.Description>Description</Drawer.Description>
48
+ <button onClick={() => ref.current?.close()}>Close</button>
49
+ </div>
50
+ </Drawer.Content>
51
+ </Drawer.Root>
52
+ </>
53
+ )
54
+ }
55
+ ```
56
+
57
+ ### Vanilla JS
58
+
59
+ ```ts
60
+ import { open, close } from 'css-drawer'
61
+ import 'css-drawer/styles'
62
+
63
+ document.querySelector('#open-btn').onclick = () => open('my-drawer')
64
+ ```
65
+
66
+ ```html
67
+ <button id="open-btn">Open</button>
68
+
69
+ <dialog class="drawer" id="my-drawer">
70
+ <div class="drawer-handle"></div>
71
+ <div className="drawer-content">
72
+ <h2>Title</h2>
73
+ <p>Description</p>
74
+ <button onclick="this.closest('dialog').close()">Close</button>
75
+ </div>
76
+ </dialog>
77
+ ```
78
+
79
+ ---
80
+
81
+ ## React API
82
+
83
+ ### Installation
84
+
85
+ ```tsx
86
+ import { Drawer } from 'css-drawer/react'
87
+ // Styles are auto-injected
88
+ ```
89
+
90
+ ### Drawer.Root
91
+
92
+ Provides context for direction. Wrap your drawer content.
93
+
94
+ ```tsx
95
+ <Drawer.Root direction="right">
96
+ <Drawer.Content ref={ref}>...</Drawer.Content>
97
+ </Drawer.Root>
98
+ ```
99
+
100
+ | Prop | Type | Default | Description |
101
+ |------|------|---------|-------------|
102
+ | `direction` | `'bottom' \| 'top' \| 'left' \| 'right'` | `'bottom'` | Direction the drawer opens from |
103
+ | `children` | `ReactNode` | - | Drawer content |
104
+
105
+ ### Drawer.Content
106
+
107
+ The dialog element. Pass a ref to control open/close.
108
+
109
+ ```tsx
110
+ const ref = useRef<HTMLDialogElement>(null)
111
+
112
+ // Open
113
+ ref.current?.showModal()
114
+
115
+ // Close
116
+ ref.current?.close()
117
+
118
+ <Drawer.Content ref={ref}>...</Drawer.Content>
119
+ ```
120
+
121
+ | Prop | Type | Default | Description |
122
+ |------|------|---------|-------------|
123
+ | `ref` | `Ref<HTMLDialogElement>` | - | Ref to control the dialog |
124
+ | `className` | `string` | - | Additional CSS classes |
125
+ | `...props` | `DialogHTMLAttributes` | - | All native dialog props |
126
+
127
+ ### Drawer.Handle
128
+
129
+ Visual drag handle indicator.
130
+
131
+ ```tsx
132
+ <Drawer.Handle />
133
+ ```
134
+
135
+ | Prop | Type | Default | Description |
136
+ |------|------|---------|-------------|
137
+ | `className` | `string` | - | Additional CSS classes |
138
+
139
+ ### Drawer.Title
140
+
141
+ Semantic heading for accessibility.
142
+
143
+ ```tsx
144
+ <Drawer.Title>Create Issue</Drawer.Title>
145
+ ```
146
+
147
+ | Prop | Type | Default | Description |
148
+ |------|------|---------|-------------|
149
+ | `...props` | `HTMLAttributes<HTMLHeadingElement>` | - | All native h2 props |
150
+
151
+ ### Drawer.Description
152
+
153
+ Semantic description for accessibility.
154
+
155
+ ```tsx
156
+ <Drawer.Description>Fill out the form below.</Drawer.Description>
157
+ ```
158
+
159
+ | Prop | Type | Default | Description |
160
+ |------|------|---------|-------------|
161
+ | `...props` | `HTMLAttributes<HTMLParagraphElement>` | - | All native p props |
162
+
163
+ ---
164
+
165
+ ## Vanilla JS API
166
+
167
+ ### Installation
168
+
169
+ ```ts
170
+ import { open, close, closeAll } from 'css-drawer'
171
+ import 'css-drawer/styles'
172
+ ```
173
+
174
+ ### open(drawer)
175
+
176
+ Opens a drawer by ID or element reference.
177
+
178
+ ```ts
179
+ open('my-drawer')
180
+ open(document.getElementById('my-drawer'))
181
+ ```
182
+
183
+ | Param | Type | Description |
184
+ |-------|------|-------------|
185
+ | `drawer` | `string \| HTMLDialogElement` | Drawer ID or element |
186
+
187
+ ### close(drawer)
188
+
189
+ Closes a drawer by ID or element reference.
190
+
191
+ ```ts
192
+ close('my-drawer')
193
+ ```
194
+
195
+ | Param | Type | Description |
196
+ |-------|------|-------------|
197
+ | `drawer` | `string \| HTMLDialogElement` | Drawer ID or element |
198
+
199
+ ### closeAll()
200
+
201
+ Closes all open drawers in reverse order (top to bottom).
202
+
203
+ ```ts
204
+ closeAll()
205
+ ```
206
+
207
+ ### isOpen(drawer)
208
+
209
+ Returns whether a drawer is open.
210
+
211
+ ```ts
212
+ if (isOpen('my-drawer')) {
213
+ // ...
214
+ }
215
+ ```
216
+
217
+ | Param | Type | Description |
218
+ |-------|------|-------------|
219
+ | `drawer` | `string \| HTMLDialogElement` | Drawer ID or element |
220
+
221
+ **Returns:** `boolean`
222
+
223
+ ### getOpen()
224
+
225
+ Returns all currently open drawers.
226
+
227
+ ```ts
228
+ const openDrawers = getOpen()
229
+ ```
230
+
231
+ **Returns:** `HTMLDialogElement[]`
232
+
233
+ ### getTop()
234
+
235
+ Returns the topmost open drawer.
236
+
237
+ ```ts
238
+ const topDrawer = getTop()
239
+ topDrawer?.close()
240
+ ```
241
+
242
+ **Returns:** `HTMLDialogElement | null`
243
+
244
+ ### create(options)
245
+
246
+ Creates a drawer element programmatically.
247
+
248
+ ```ts
249
+ const drawer = create({
250
+ id: 'my-drawer',
251
+ content: '<h2>Hello</h2>',
252
+ handle: true,
253
+ className: 'custom-class'
254
+ })
255
+
256
+ mount(drawer)
257
+ open(drawer)
258
+ ```
259
+
260
+ | Option | Type | Default | Description |
261
+ |--------|------|---------|-------------|
262
+ | `id` | `string` | - | Drawer ID |
263
+ | `content` | `string` | `''` | HTML content |
264
+ | `handle` | `boolean` | `true` | Include drag handle |
265
+ | `className` | `string` | `''` | Additional CSS classes |
266
+
267
+ **Returns:** `HTMLDialogElement`
268
+
269
+ ### mount(drawer)
270
+
271
+ Appends a drawer to the document body.
272
+
273
+ ```ts
274
+ const drawer = create({ id: 'my-drawer' })
275
+ mount(drawer)
276
+ ```
277
+
278
+ | Param | Type | Description |
279
+ |-------|------|-------------|
280
+ | `drawer` | `HTMLDialogElement` | Drawer element |
281
+
282
+ **Returns:** `HTMLDialogElement`
283
+
284
+ ### unmount(drawer)
285
+
286
+ Removes a drawer from the DOM.
287
+
288
+ ```ts
289
+ unmount('my-drawer')
290
+ ```
291
+
292
+ | Param | Type | Description |
293
+ |-------|------|-------------|
294
+ | `drawer` | `string \| HTMLDialogElement` | Drawer ID or element |
295
+
296
+ ### subscribe(drawer, handlers)
297
+
298
+ Subscribe to drawer events.
299
+
300
+ ```ts
301
+ const unsubscribe = subscribe('my-drawer', {
302
+ onOpen: () => console.log('Opened'),
303
+ onClose: () => console.log('Closed'),
304
+ onCancel: () => console.log('Cancelled (Escape/backdrop)')
305
+ })
306
+
307
+ // Later
308
+ unsubscribe()
309
+ ```
310
+
311
+ | Param | Type | Description |
312
+ |-------|------|-------------|
313
+ | `drawer` | `string \| HTMLDialogElement` | Drawer ID or element |
314
+ | `handlers.onOpen` | `() => void` | Called when drawer opens |
315
+ | `handlers.onClose` | `() => void` | Called when drawer closes |
316
+ | `handlers.onCancel` | `() => void` | Called on Escape or backdrop click |
317
+
318
+ **Returns:** `() => void` (cleanup function)
319
+
320
+ ---
321
+
322
+ ## Directions
323
+
324
+ ### React
325
+
326
+ ```tsx
327
+ <Drawer.Root direction="right">
328
+ <Drawer.Content ref={ref}>...</Drawer.Content>
329
+ </Drawer.Root>
330
+ ```
331
+
332
+ ### Vanilla
333
+
334
+ ```html
335
+ <dialog class="drawer" data-direction="right">...</dialog>
336
+ ```
337
+
338
+ ### Responsive Direction
339
+
340
+ ```tsx
341
+ const isMobile = useMediaQuery('(max-width: 768px)')
342
+
343
+ <Drawer.Root direction={isMobile ? 'bottom' : 'right'}>
344
+ ...
345
+ </Drawer.Root>
346
+ ```
347
+
348
+ | Direction | Description |
349
+ |-----------|-------------|
350
+ | `bottom` | Opens from bottom (default) |
351
+ | `top` | Opens from top |
352
+ | `left` | Opens from left |
353
+ | `right` | Opens from right |
354
+
355
+ ---
356
+
357
+ ## Auto-Nesting
358
+
359
+ Drawers automatically stack when opened. No configuration needed.
360
+
361
+ ```tsx
362
+ const drawer1 = useRef<HTMLDialogElement>(null)
363
+ const drawer2 = useRef<HTMLDialogElement>(null)
364
+
365
+ // Open drawer1
366
+ drawer1.current?.showModal()
367
+
368
+ // Open drawer2 on top
369
+ drawer2.current?.showModal()
370
+ // drawer1 automatically scales down and dims
371
+ ```
372
+
373
+ Works up to 5 levels. CSS `:has()` selectors handle the visual stacking.
374
+
375
+ ---
376
+
377
+ ## Accessibility
378
+
379
+ Accessibility is automatic:
380
+
381
+ - **Focus trapping**: Native `<dialog>` traps focus
382
+ - **Escape to close**: Native `<dialog>` behavior
383
+ - **Stacked drawers**: Underlying drawers get `inert` attribute automatically
384
+ - **Screen readers**: Only the top drawer is accessible
385
+
386
+ No setup required.
387
+
388
+ ---
389
+
390
+ ## CSS Customization
391
+
392
+ Override CSS custom properties:
393
+
394
+ ```css
395
+ :root {
396
+ --drawer-bg: #fff;
397
+ --drawer-radius: 24px;
398
+ --drawer-max-width: 500px;
399
+ --drawer-max-height: 96dvh;
400
+ --drawer-backdrop: hsl(0 0% 0% / 0.4);
401
+ --drawer-handle: hsl(0 0% 80%);
402
+ --drawer-duration: 0.5s;
403
+ --drawer-duration-close: 0.35s;
404
+ --drawer-ease: cubic-bezier(0.32, 0.72, 0, 1);
405
+ }
406
+ ```
407
+
408
+ | Variable | Default | Description |
409
+ |----------|---------|-------------|
410
+ | `--drawer-bg` | `#fff` | Background color |
411
+ | `--drawer-radius` | `24px` | Border radius |
412
+ | `--drawer-max-width` | `500px` | Maximum width |
413
+ | `--drawer-max-height` | `96dvh` | Maximum height |
414
+ | `--drawer-backdrop` | `hsl(0 0% 0% / 0.4)` | Backdrop color |
415
+ | `--drawer-handle` | `hsl(0 0% 80%)` | Handle color |
416
+ | `--drawer-duration` | `0.5s` | Open animation duration |
417
+ | `--drawer-duration-close` | `0.35s` | Close animation duration |
418
+ | `--drawer-ease` | `cubic-bezier(0.32, 0.72, 0, 1)` | Animation easing |
419
+
420
+ Dark mode is automatic via `prefers-color-scheme`.
421
+
422
+ ---
423
+
424
+ ## CSS Classes
425
+
426
+ | Class | Description |
427
+ |-------|-------------|
428
+ | `.drawer` | Required on the dialog element |
429
+ | `.drawer-handle` | Visual drag handle |
430
+ | `.drawer-content` | Scrollable content area |
431
+
432
+ ---
433
+
434
+ ## Browser Support
435
+
436
+ | Browser | Version |
437
+ |---------|---------|
438
+ | Chrome | 117+ |
439
+ | Safari | 17.5+ |
440
+ | Firefox | 129+ |
441
+
442
+ Uses `@starting-style`, `:has()`, `allow-discrete`, and `dvh` units.
443
+
444
+ ---
445
+
446
+ ## TypeScript
447
+
448
+ Full TypeScript support included.
449
+
450
+ ```tsx
451
+ import {
452
+ Drawer,
453
+ type DrawerRootProps,
454
+ type DrawerContentProps,
455
+ type DrawerDirection
456
+ } from 'css-drawer/react'
457
+
458
+ import {
459
+ open,
460
+ close,
461
+ type DrawerElement,
462
+ type DrawerRef
463
+ } from 'css-drawer'
464
+ ```
465
+
466
+ ---
467
+
468
+ ## License
469
+
470
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,4 @@
1
+ if(typeof window<`u`){let e=()=>{let e=Array.from(document.querySelectorAll(`dialog.drawer[open]`));e.forEach((t,n)=>{n===e.length-1?t.removeAttribute(`inert`):t.setAttribute(`inert`,``)})};new MutationObserver(t=>{for(let n of t)if(n.type===`attributes`&&n.attributeName===`open`&&n.target.classList.contains(`drawer`)){e();break}}).observe(document.body,{subtree:!0,attributes:!0,attributeFilter:[`open`]})}function e(e){return e?typeof e==`string`?document.getElementById(e):e:null}function t(t){e(t)?.showModal()}function n(t){e(t)?.close()}function r(){Array.from(document.querySelectorAll(`dialog.drawer[open]`)).reverse().forEach(e=>e.close())}function i(t){return e(t)?.open??!1}function a(){return Array.from(document.querySelectorAll(`dialog.drawer[open]`))}function o(){let e=a();return e[e.length-1]??null}function s(e={}){let{id:t,content:n=``,handle:r=!0,className:i=``}=e,a=document.createElement(`dialog`);return a.className=`drawer ${i}`.trim(),t&&(a.id=t),a.innerHTML=`
2
+ ${r?`<div class="drawer-handle"></div>`:``}
3
+ <div class="drawer-content">${n}</div>
4
+ `,a.addEventListener(`click`,e=>{e.target===a&&a.close()}),a}function c(e){return document.body.appendChild(e),e}function l(t){e(t)?.remove()}function u(){let e=e=>{let t=e.target;t.matches(`dialog.drawer`)&&t.close()};return document.addEventListener(`click`,e),()=>document.removeEventListener(`click`,e)}function d(t,n){let r=e(t);if(!r)return()=>{};let{onOpen:i,onClose:a,onCancel:o}=n,s=()=>a?.(),c=()=>o?.(),l=new MutationObserver(e=>{for(let t of e)t.attributeName===`open`&&r.open&&i?.()});return r.addEventListener(`close`,s),r.addEventListener(`cancel`,c),l.observe(r,{attributes:!0}),()=>{r.removeEventListener(`close`,s),r.removeEventListener(`cancel`,c),l.disconnect()}}function f(e){return{id:e,className:`drawer`,onClick:e=>{e.target===e.currentTarget&&e.currentTarget.close()}}}exports.close=n,exports.closeAll=r,exports.create=s,exports.getOpen=a,exports.getTop=o,exports.init=u,exports.isOpen=i,exports.mount=c,exports.open=t,exports.props=f,exports.subscribe=d,exports.unmount=l;
@@ -0,0 +1,83 @@
1
+ //#region src/index.d.ts
2
+ /**
3
+ * CSS Drawer - Headless drawer component
4
+ * Works with any framework: React, Vue, Svelte, vanilla JS
5
+ */
6
+ type DrawerElement = HTMLDialogElement;
7
+ type DrawerRef = string | DrawerElement | null | undefined;
8
+ interface CreateDrawerOptions {
9
+ /** Drawer ID */
10
+ id?: string;
11
+ /** HTML content for the drawer */
12
+ content?: string;
13
+ /** Include drag handle (default: true) */
14
+ handle?: boolean;
15
+ /** Additional CSS classes */
16
+ className?: string;
17
+ }
18
+ interface DrawerEventHandlers {
19
+ /** Called when drawer opens */
20
+ onOpen?: () => void;
21
+ /** Called when drawer closes */
22
+ onClose?: () => void;
23
+ /** Called when drawer is cancelled (backdrop click or Escape) */
24
+ onCancel?: () => void;
25
+ }
26
+ /**
27
+ * Open a drawer by ID or element reference
28
+ */
29
+ declare function open(drawer: DrawerRef): void;
30
+ /**
31
+ * Close a drawer by ID or element reference
32
+ */
33
+ declare function close(drawer: DrawerRef): void;
34
+ /**
35
+ * Close all open drawers (in reverse DOM order for proper animation)
36
+ */
37
+ declare function closeAll(): void;
38
+ /**
39
+ * Check if a drawer is open
40
+ */
41
+ declare function isOpen(drawer: DrawerRef): boolean;
42
+ /**
43
+ * Get all open drawers
44
+ */
45
+ declare function getOpen(): DrawerElement[];
46
+ /**
47
+ * Get the topmost open drawer
48
+ */
49
+ declare function getTop(): DrawerElement | null;
50
+ /**
51
+ * Create a drawer element programmatically
52
+ */
53
+ declare function create(options?: CreateDrawerOptions): DrawerElement;
54
+ /**
55
+ * Mount a drawer to the DOM (appends to body)
56
+ */
57
+ declare function mount(drawer: DrawerElement): DrawerElement;
58
+ /**
59
+ * Unmount a drawer from the DOM
60
+ */
61
+ declare function unmount(drawer: DrawerRef): void;
62
+ /**
63
+ * Initialize global backdrop-click-to-close behavior
64
+ * Alternative to adding onclick to each drawer
65
+ */
66
+ declare function init(): () => void;
67
+ /**
68
+ * Subscribe to drawer events
69
+ * @returns Cleanup function
70
+ */
71
+ declare function subscribe(drawer: DrawerRef, handlers: DrawerEventHandlers): () => void;
72
+ /**
73
+ * React-friendly hook helper - returns props to spread on dialog
74
+ * Usage: <dialog {...drawer.props('my-drawer')} />
75
+ */
76
+ declare function props(id: string): {
77
+ readonly id: string;
78
+ readonly className: "drawer";
79
+ readonly onClick: (e: MouseEvent) => void;
80
+ };
81
+ //#endregion
82
+ export { CreateDrawerOptions, DrawerElement, DrawerEventHandlers, DrawerRef, close, closeAll, create, getOpen, getTop, init, isOpen, mount, open, props, subscribe, unmount };
83
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;AAwCA;AAEA;AAEA;AAWiB,KAfL,aAAA,GAAgB,iBAeQ;AAuBpB,KApCJ,SAAA,GAoCiB,MAAA,GApCI,aAoCK,GAAA,IAAA,GAAA,SAAA;AAQtB,UA1CC,mBAAA,CA0CsB;EAQvB;EAQA,EAAA,CAAA,EAAA,MAAM;EAQN;EAOA,OAAA,CAAA,EAAM,MAAA;EAQN;EAuBA,MAAA,CAAA,EAAK,OAAA;EAQL;EASA,SAAI,CAAA,EAAA,MAAA;AAgBpB;AAoCgB,UAlKC,mBAAA,CAsKU;;;;;;;;;;;iBA/IX,IAAA,SAAa;;;;iBAQb,KAAA,SAAc;;;;iBAQd,QAAA,CAAA;;;;iBAQA,MAAA,SAAe;;;;iBAQf,OAAA,CAAA,GAAW;;;;iBAOX,MAAA,CAAA,GAAU;;;;iBAQV,MAAA,WAAgB,sBAA2B;;;;iBAuB3C,KAAA,SAAc,gBAAgB;;;;iBAQ9B,OAAA,SAAgB;;;;;iBAShB,IAAA,CAAA;;;;;iBAgBA,SAAA,SACN,qBACE;;;;;iBAkCI,KAAA;;;wBAIC"}
@@ -0,0 +1,83 @@
1
+ //#region src/index.d.ts
2
+ /**
3
+ * CSS Drawer - Headless drawer component
4
+ * Works with any framework: React, Vue, Svelte, vanilla JS
5
+ */
6
+ type DrawerElement = HTMLDialogElement;
7
+ type DrawerRef = string | DrawerElement | null | undefined;
8
+ interface CreateDrawerOptions {
9
+ /** Drawer ID */
10
+ id?: string;
11
+ /** HTML content for the drawer */
12
+ content?: string;
13
+ /** Include drag handle (default: true) */
14
+ handle?: boolean;
15
+ /** Additional CSS classes */
16
+ className?: string;
17
+ }
18
+ interface DrawerEventHandlers {
19
+ /** Called when drawer opens */
20
+ onOpen?: () => void;
21
+ /** Called when drawer closes */
22
+ onClose?: () => void;
23
+ /** Called when drawer is cancelled (backdrop click or Escape) */
24
+ onCancel?: () => void;
25
+ }
26
+ /**
27
+ * Open a drawer by ID or element reference
28
+ */
29
+ declare function open(drawer: DrawerRef): void;
30
+ /**
31
+ * Close a drawer by ID or element reference
32
+ */
33
+ declare function close(drawer: DrawerRef): void;
34
+ /**
35
+ * Close all open drawers (in reverse DOM order for proper animation)
36
+ */
37
+ declare function closeAll(): void;
38
+ /**
39
+ * Check if a drawer is open
40
+ */
41
+ declare function isOpen(drawer: DrawerRef): boolean;
42
+ /**
43
+ * Get all open drawers
44
+ */
45
+ declare function getOpen(): DrawerElement[];
46
+ /**
47
+ * Get the topmost open drawer
48
+ */
49
+ declare function getTop(): DrawerElement | null;
50
+ /**
51
+ * Create a drawer element programmatically
52
+ */
53
+ declare function create(options?: CreateDrawerOptions): DrawerElement;
54
+ /**
55
+ * Mount a drawer to the DOM (appends to body)
56
+ */
57
+ declare function mount(drawer: DrawerElement): DrawerElement;
58
+ /**
59
+ * Unmount a drawer from the DOM
60
+ */
61
+ declare function unmount(drawer: DrawerRef): void;
62
+ /**
63
+ * Initialize global backdrop-click-to-close behavior
64
+ * Alternative to adding onclick to each drawer
65
+ */
66
+ declare function init(): () => void;
67
+ /**
68
+ * Subscribe to drawer events
69
+ * @returns Cleanup function
70
+ */
71
+ declare function subscribe(drawer: DrawerRef, handlers: DrawerEventHandlers): () => void;
72
+ /**
73
+ * React-friendly hook helper - returns props to spread on dialog
74
+ * Usage: <dialog {...drawer.props('my-drawer')} />
75
+ */
76
+ declare function props(id: string): {
77
+ readonly id: string;
78
+ readonly className: "drawer";
79
+ readonly onClick: (e: MouseEvent) => void;
80
+ };
81
+ //#endregion
82
+ export { CreateDrawerOptions, DrawerElement, DrawerEventHandlers, DrawerRef, close, closeAll, create, getOpen, getTop, init, isOpen, mount, open, props, subscribe, unmount };
83
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;AAwCA;AAEA;AAEA;AAWiB,KAfL,aAAA,GAAgB,iBAeQ;AAuBpB,KApCJ,SAAA,GAoCiB,MAAA,GApCI,aAoCK,GAAA,IAAA,GAAA,SAAA;AAQtB,UA1CC,mBAAA,CA0CsB;EAQvB;EAQA,EAAA,CAAA,EAAA,MAAM;EAQN;EAOA,OAAA,CAAA,EAAM,MAAA;EAQN;EAuBA,MAAA,CAAA,EAAK,OAAA;EAQL;EASA,SAAI,CAAA,EAAA,MAAA;AAgBpB;AAoCgB,UAlKC,mBAAA,CAsKU;;;;;;;;;;;iBA/IX,IAAA,SAAa;;;;iBAQb,KAAA,SAAc;;;;iBAQd,QAAA,CAAA;;;;iBAQA,MAAA,SAAe;;;;iBAQf,OAAA,CAAA,GAAW;;;;iBAOX,MAAA,CAAA,GAAU;;;;iBAQV,MAAA,WAAgB,sBAA2B;;;;iBAuB3C,KAAA,SAAc,gBAAgB;;;;iBAQ9B,OAAA,SAAgB;;;;;iBAShB,IAAA,CAAA;;;;;iBAgBA,SAAA,SACN,qBACE;;;;;iBAkCI,KAAA;;;wBAIC"}
package/dist/index.mjs ADDED
@@ -0,0 +1,5 @@
1
+ if(typeof window<`u`){let e=()=>{let e=Array.from(document.querySelectorAll(`dialog.drawer[open]`));e.forEach((t,n)=>{n===e.length-1?t.removeAttribute(`inert`):t.setAttribute(`inert`,``)})};new MutationObserver(t=>{for(let n of t)if(n.type===`attributes`&&n.attributeName===`open`&&n.target.classList.contains(`drawer`)){e();break}}).observe(document.body,{subtree:!0,attributes:!0,attributeFilter:[`open`]})}function e(e){return e?typeof e==`string`?document.getElementById(e):e:null}function t(t){e(t)?.showModal()}function n(t){e(t)?.close()}function r(){Array.from(document.querySelectorAll(`dialog.drawer[open]`)).reverse().forEach(e=>e.close())}function i(t){return e(t)?.open??!1}function a(){return Array.from(document.querySelectorAll(`dialog.drawer[open]`))}function o(){let e=a();return e[e.length-1]??null}function s(e={}){let{id:t,content:n=``,handle:r=!0,className:i=``}=e,a=document.createElement(`dialog`);return a.className=`drawer ${i}`.trim(),t&&(a.id=t),a.innerHTML=`
2
+ ${r?`<div class="drawer-handle"></div>`:``}
3
+ <div class="drawer-content">${n}</div>
4
+ `,a.addEventListener(`click`,e=>{e.target===a&&a.close()}),a}function c(e){return document.body.appendChild(e),e}function l(t){e(t)?.remove()}function u(){let e=e=>{let t=e.target;t.matches(`dialog.drawer`)&&t.close()};return document.addEventListener(`click`,e),()=>document.removeEventListener(`click`,e)}function d(t,n){let r=e(t);if(!r)return()=>{};let{onOpen:i,onClose:a,onCancel:o}=n,s=()=>a?.(),c=()=>o?.(),l=new MutationObserver(e=>{for(let t of e)t.attributeName===`open`&&r.open&&i?.()});return r.addEventListener(`close`,s),r.addEventListener(`cancel`,c),l.observe(r,{attributes:!0}),()=>{r.removeEventListener(`close`,s),r.removeEventListener(`cancel`,c),l.disconnect()}}function f(e){return{id:e,className:`drawer`,onClick:e=>{e.target===e.currentTarget&&e.currentTarget.close()}}}export{n as close,r as closeAll,s as create,a as getOpen,o as getTop,u as init,i as isOpen,c as mount,t as open,f as props,d as subscribe,l as unmount};
5
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["open"],"sources":["../src/index.ts"],"sourcesContent":["/**\n * CSS Drawer - Headless drawer component\n * Works with any framework: React, Vue, Svelte, vanilla JS\n */\n\n/* ===== Auto-enable accessibility for stacked drawers ===== */\nif (typeof window !== 'undefined') {\n const updateInertState = () => {\n const openDrawers = Array.from(\n document.querySelectorAll<HTMLDialogElement>('dialog.drawer[open]')\n )\n openDrawers.forEach((drawer, index) => {\n if (index === openDrawers.length - 1) {\n drawer.removeAttribute('inert')\n } else {\n drawer.setAttribute('inert', '')\n }\n })\n }\n\n const observer = new MutationObserver((mutations) => {\n for (const mutation of mutations) {\n if (\n mutation.type === 'attributes' &&\n mutation.attributeName === 'open' &&\n (mutation.target as HTMLElement).classList.contains('drawer')\n ) {\n updateInertState()\n break\n }\n }\n })\n\n observer.observe(document.body, {\n subtree: true,\n attributes: true,\n attributeFilter: ['open'],\n })\n}\n\nexport type DrawerElement = HTMLDialogElement\n\nexport type DrawerRef = string | DrawerElement | null | undefined\n\nexport interface CreateDrawerOptions {\n /** Drawer ID */\n id?: string\n /** HTML content for the drawer */\n content?: string\n /** Include drag handle (default: true) */\n handle?: boolean\n /** Additional CSS classes */\n className?: string\n}\n\nexport interface DrawerEventHandlers {\n /** Called when drawer opens */\n onOpen?: () => void\n /** Called when drawer closes */\n onClose?: () => void\n /** Called when drawer is cancelled (backdrop click or Escape) */\n onCancel?: () => void\n}\n\n/**\n * Resolve a drawer reference to an element\n */\nfunction resolve(drawer: DrawerRef): DrawerElement | null {\n if (!drawer) return null\n if (typeof drawer === 'string') {\n return document.getElementById(drawer) as DrawerElement | null\n }\n return drawer\n}\n\n/**\n * Open a drawer by ID or element reference\n */\nexport function open(drawer: DrawerRef): void {\n const el = resolve(drawer)\n el?.showModal()\n}\n\n/**\n * Close a drawer by ID or element reference\n */\nexport function close(drawer: DrawerRef): void {\n const el = resolve(drawer)\n el?.close()\n}\n\n/**\n * Close all open drawers (in reverse DOM order for proper animation)\n */\nexport function closeAll(): void {\n const drawers = Array.from(document.querySelectorAll<DrawerElement>('dialog.drawer[open]'))\n drawers.reverse().forEach((d) => d.close())\n}\n\n/**\n * Check if a drawer is open\n */\nexport function isOpen(drawer: DrawerRef): boolean {\n const el = resolve(drawer)\n return el?.open ?? false\n}\n\n/**\n * Get all open drawers\n */\nexport function getOpen(): DrawerElement[] {\n return Array.from(document.querySelectorAll<DrawerElement>('dialog.drawer[open]'))\n}\n\n/**\n * Get the topmost open drawer\n */\nexport function getTop(): DrawerElement | null {\n const open = getOpen()\n return open[open.length - 1] ?? null\n}\n\n/**\n * Create a drawer element programmatically\n */\nexport function create(options: CreateDrawerOptions = {}): DrawerElement {\n const { id, content = '', handle = true, className = '' } = options\n\n const dialog = document.createElement('dialog') as DrawerElement\n dialog.className = `drawer ${className}`.trim()\n if (id) dialog.id = id\n\n dialog.innerHTML = `\n ${handle ? '<div class=\"drawer-handle\"></div>' : ''}\n <div class=\"drawer-content\">${content}</div>\n `\n\n // Backdrop click to close\n dialog.addEventListener('click', (e) => {\n if (e.target === dialog) dialog.close()\n })\n\n return dialog\n}\n\n/**\n * Mount a drawer to the DOM (appends to body)\n */\nexport function mount(drawer: DrawerElement): DrawerElement {\n document.body.appendChild(drawer)\n return drawer\n}\n\n/**\n * Unmount a drawer from the DOM\n */\nexport function unmount(drawer: DrawerRef): void {\n const el = resolve(drawer)\n el?.remove()\n}\n\n/**\n * Initialize global backdrop-click-to-close behavior\n * Alternative to adding onclick to each drawer\n */\nexport function init(): () => void {\n const handler = (e: MouseEvent) => {\n const target = e.target as HTMLElement\n if (target.matches('dialog.drawer')) {\n ;(target as DrawerElement).close()\n }\n }\n\n document.addEventListener('click', handler)\n return () => document.removeEventListener('click', handler)\n}\n\n/**\n * Subscribe to drawer events\n * @returns Cleanup function\n */\nexport function subscribe(\n drawer: DrawerRef,\n handlers: DrawerEventHandlers\n): () => void {\n const el = resolve(drawer)\n if (!el) return () => {}\n\n const { onOpen, onClose, onCancel } = handlers\n\n const handleClose = () => onClose?.()\n const handleCancel = () => onCancel?.()\n\n // Use MutationObserver to detect open attribute\n const observer = new MutationObserver((mutations) => {\n for (const mutation of mutations) {\n if (mutation.attributeName === 'open' && el.open) {\n onOpen?.()\n }\n }\n })\n\n el.addEventListener('close', handleClose)\n el.addEventListener('cancel', handleCancel)\n observer.observe(el, { attributes: true })\n\n return () => {\n el.removeEventListener('close', handleClose)\n el.removeEventListener('cancel', handleCancel)\n observer.disconnect()\n }\n}\n\n/**\n * React-friendly hook helper - returns props to spread on dialog\n * Usage: <dialog {...drawer.props('my-drawer')} />\n */\nexport function props(id: string) {\n return {\n id,\n className: 'drawer',\n onClick: (e: MouseEvent) => {\n if (e.target === e.currentTarget) {\n ;(e.currentTarget as DrawerElement).close()\n }\n },\n } as const\n}\n\n"],"mappings":"AAMA,GAAI,OAAO,OAAW,IAAa,CACjC,IAAM,MAAyB,CAC7B,IAAM,EAAc,MAAM,KACxB,SAAS,iBAAoC,sBAAsB,CACpE,CACD,EAAY,SAAS,EAAQ,IAAU,CACjC,IAAU,EAAY,OAAS,EACjC,EAAO,gBAAgB,QAAQ,CAE/B,EAAO,aAAa,QAAS,GAAG,EAElC,EAGa,IAAI,iBAAkB,GAAc,CACnD,IAAK,IAAM,KAAY,EACrB,GACE,EAAS,OAAS,cAClB,EAAS,gBAAkB,QAC1B,EAAS,OAAuB,UAAU,SAAS,SAAS,CAC7D,CACA,GAAkB,CAClB,QAGJ,CAEO,QAAQ,SAAS,KAAM,CAC9B,QAAS,GACT,WAAY,GACZ,gBAAiB,CAAC,OAAO,CAC1B,CAAC,CA8BJ,SAAS,EAAQ,EAAyC,CAKxD,OAJK,EACD,OAAO,GAAW,SACb,SAAS,eAAe,EAAO,CAEjC,EAJa,KAUtB,SAAgB,EAAK,EAAyB,CACjC,EAAQ,EAAO,EACtB,WAAW,CAMjB,SAAgB,EAAM,EAAyB,CAClC,EAAQ,EAAO,EACtB,OAAO,CAMb,SAAgB,GAAiB,CACf,MAAM,KAAK,SAAS,iBAAgC,sBAAsB,CAAC,CACnF,SAAS,CAAC,QAAS,GAAM,EAAE,OAAO,CAAC,CAM7C,SAAgB,EAAO,EAA4B,CAEjD,OADW,EAAQ,EAAO,EACf,MAAQ,GAMrB,SAAgB,GAA2B,CACzC,OAAO,MAAM,KAAK,SAAS,iBAAgC,sBAAsB,CAAC,CAMpF,SAAgB,GAA+B,CAC7C,IAAMA,EAAO,GAAS,CACtB,OAAOA,EAAKA,EAAK,OAAS,IAAM,KAMlC,SAAgB,EAAO,EAA+B,EAAE,CAAiB,CACvE,GAAM,CAAE,KAAI,UAAU,GAAI,SAAS,GAAM,YAAY,IAAO,EAEtD,EAAS,SAAS,cAAc,SAAS,CAc/C,MAbA,GAAO,UAAY,UAAU,IAAY,MAAM,CAC3C,IAAI,EAAO,GAAK,GAEpB,EAAO,UAAY;MACf,EAAS,oCAAsC,GAAG;kCACtB,EAAQ;IAIxC,EAAO,iBAAiB,QAAU,GAAM,CAClC,EAAE,SAAW,GAAQ,EAAO,OAAO,EACvC,CAEK,EAMT,SAAgB,EAAM,EAAsC,CAE1D,OADA,SAAS,KAAK,YAAY,EAAO,CAC1B,EAMT,SAAgB,EAAQ,EAAyB,CACpC,EAAQ,EAAO,EACtB,QAAQ,CAOd,SAAgB,GAAmB,CACjC,IAAM,EAAW,GAAkB,CACjC,IAAM,EAAS,EAAE,OACb,EAAO,QAAQ,gBAAgB,EAC/B,EAAyB,OAAO,EAKtC,OADA,SAAS,iBAAiB,QAAS,EAAQ,KAC9B,SAAS,oBAAoB,QAAS,EAAQ,CAO7D,SAAgB,EACd,EACA,EACY,CACZ,IAAM,EAAK,EAAQ,EAAO,CAC1B,GAAI,CAAC,EAAI,UAAa,GAEtB,GAAM,CAAE,SAAQ,UAAS,YAAa,EAEhC,MAAoB,KAAW,CAC/B,MAAqB,KAAY,CAGjC,EAAW,IAAI,iBAAkB,GAAc,CACnD,IAAK,IAAM,KAAY,EACjB,EAAS,gBAAkB,QAAU,EAAG,MAC1C,KAAU,EAGd,CAMF,OAJA,EAAG,iBAAiB,QAAS,EAAY,CACzC,EAAG,iBAAiB,SAAU,EAAa,CAC3C,EAAS,QAAQ,EAAI,CAAE,WAAY,GAAM,CAAC,KAE7B,CACX,EAAG,oBAAoB,QAAS,EAAY,CAC5C,EAAG,oBAAoB,SAAU,EAAa,CAC9C,EAAS,YAAY,EAQzB,SAAgB,EAAM,EAAY,CAChC,MAAO,CACL,KACA,UAAW,SACX,QAAU,GAAkB,CACtB,EAAE,SAAW,EAAE,eACf,EAAE,cAAgC,OAAO,EAGhD"}
package/dist/react.cjs ADDED
@@ -0,0 +1,2 @@
1
+ require('./react.css');
2
+ let e=require(`react`),t=require(`react/jsx-runtime`);if(typeof window<`u`){let e=()=>{let e=Array.from(document.querySelectorAll(`dialog.drawer[open]`));e.forEach((t,n)=>{n===e.length-1?t.removeAttribute(`inert`):t.setAttribute(`inert`,``)})};new MutationObserver(t=>{for(let n of t)if(n.type===`attributes`&&n.attributeName===`open`&&n.target.classList.contains(`drawer`)){e();break}}).observe(document.body,{subtree:!0,attributes:!0,attributeFilter:[`open`]})}const n=(0,e.createContext)({direction:void 0});function r(){return(0,e.useContext)(n)}function i({children:e,direction:r}){return(0,t.jsx)(n.Provider,{value:{direction:r},children:e})}const a=(0,e.forwardRef)(({children:e,className:n,...i},a)=>{let{direction:o}=r();return(0,t.jsx)(`dialog`,{ref:a,className:`drawer ${n??``}`.trim(),"data-direction":o,onClick:e=>{i.onClick?.(e),e.target===e.currentTarget&&e.currentTarget.close()},...i,children:e})});a.displayName=`Drawer.Content`;const o=(0,e.forwardRef)(({className:e,...n},r)=>(0,t.jsx)(`div`,{ref:r,className:`drawer-handle ${e??``}`.trim(),"aria-hidden":`true`,...n}));o.displayName=`Drawer.Handle`;const s=(0,e.forwardRef)((e,n)=>(0,t.jsx)(`h2`,{ref:n,...e}));s.displayName=`Drawer.Title`;const c=(0,e.forwardRef)((e,n)=>(0,t.jsx)(`p`,{ref:n,...e}));c.displayName=`Drawer.Description`;const l={Root:i,Content:a,Handle:o,Title:s,Description:c};exports.Drawer=l;
package/dist/react.css ADDED
@@ -0,0 +1,290 @@
1
+ /* CSS Drawer - Vaul-quality drawer with auto-nesting and directions */
2
+
3
+ :root {
4
+ --drawer-bg: #fff;
5
+ --drawer-radius: 24px;
6
+ --drawer-max-width: 500px;
7
+ --drawer-max-height: 96dvh;
8
+ --drawer-backdrop: hsl(0 0% 0% / 0.4);
9
+ --drawer-handle: hsl(0 0% 80%);
10
+ --drawer-shadow-bottom: 0 -10px 60px hsl(0 0% 0% / 0.12), 0 -4px 20px hsl(0 0% 0% / 0.08);
11
+ --drawer-shadow-top: 0 10px 60px hsl(0 0% 0% / 0.12), 0 4px 20px hsl(0 0% 0% / 0.08);
12
+ --drawer-shadow-right: -10px 0 60px hsl(0 0% 0% / 0.12), -4px 0 20px hsl(0 0% 0% / 0.08);
13
+ --drawer-shadow-left: 10px 0 60px hsl(0 0% 0% / 0.12), 4px 0 20px hsl(0 0% 0% / 0.08);
14
+ --drawer-duration: 0.5s;
15
+ --drawer-duration-close: 0.35s;
16
+ --drawer-ease: cubic-bezier(0.32, 0.72, 0, 1);
17
+ }
18
+
19
+ @media (prefers-color-scheme: dark) {
20
+ :root {
21
+ --drawer-bg: hsl(0 0% 12%);
22
+ --drawer-handle: hsl(0 0% 35%);
23
+ --drawer-shadow-bottom: 0 -10px 60px hsl(0 0% 0% / 0.4), 0 -4px 20px hsl(0 0% 0% / 0.3);
24
+ --drawer-shadow-top: 0 10px 60px hsl(0 0% 0% / 0.4), 0 4px 20px hsl(0 0% 0% / 0.3);
25
+ --drawer-shadow-right: -10px 0 60px hsl(0 0% 0% / 0.4), -4px 0 20px hsl(0 0% 0% / 0.3);
26
+ --drawer-shadow-left: 10px 0 60px hsl(0 0% 0% / 0.4), 4px 0 20px hsl(0 0% 0% / 0.3);
27
+ }
28
+ }
29
+
30
+ /* Background scale effect */
31
+ body {
32
+ transition: scale var(--drawer-duration) var(--drawer-ease), border-radius var(--drawer-duration) var(--drawer-ease);
33
+ transform-origin: center top;
34
+ }
35
+
36
+ body:has(.drawer[open]) {
37
+ overflow: hidden;
38
+ scale: 0.94;
39
+ border-radius: var(--drawer-radius);
40
+ }
41
+
42
+ /* Base drawer */
43
+ .drawer {
44
+ border: none;
45
+ padding: 0;
46
+ margin: 0;
47
+ max-width: 100%;
48
+ max-height: 100%;
49
+ position: fixed;
50
+ background: var(--drawer-bg);
51
+ overflow: hidden;
52
+ opacity: 0;
53
+ transition:
54
+ display var(--drawer-duration-close) allow-discrete,
55
+ overlay var(--drawer-duration-close) allow-discrete,
56
+ translate var(--drawer-duration-close) var(--drawer-ease),
57
+ scale var(--drawer-duration-close) var(--drawer-ease),
58
+ filter var(--drawer-duration-close) ease,
59
+ opacity var(--drawer-duration-close) ease;
60
+
61
+ /* Default: bottom */
62
+ --_translate-closed: 0 100%;
63
+ inset: auto 0 0 0;
64
+ margin-inline: auto;
65
+ width: 100%;
66
+ max-width: var(--drawer-max-width);
67
+ height: auto;
68
+ max-height: var(--drawer-max-height);
69
+ border-radius: var(--drawer-radius) var(--drawer-radius) 0 0;
70
+ box-shadow: var(--drawer-shadow-bottom);
71
+ translate: var(--_translate-closed);
72
+ }
73
+
74
+ .drawer::backdrop {
75
+ background: var(--drawer-backdrop);
76
+ opacity: 0;
77
+ backdrop-filter: blur(4px);
78
+ -webkit-backdrop-filter: blur(4px);
79
+ transition:
80
+ display var(--drawer-duration-close) allow-discrete,
81
+ overlay var(--drawer-duration-close) allow-discrete,
82
+ opacity var(--drawer-duration-close) ease;
83
+ }
84
+
85
+ .drawer[open] {
86
+ opacity: 1;
87
+ translate: 0 0;
88
+ transition:
89
+ display var(--drawer-duration) allow-discrete,
90
+ overlay var(--drawer-duration) allow-discrete,
91
+ translate var(--drawer-duration) var(--drawer-ease),
92
+ scale var(--drawer-duration) var(--drawer-ease),
93
+ filter var(--drawer-duration) ease,
94
+ opacity 0.15s ease;
95
+ }
96
+
97
+ .drawer[open]::backdrop {
98
+ opacity: 1;
99
+ transition: display var(--drawer-duration) allow-discrete, overlay var(--drawer-duration) allow-discrete, opacity 0.2s ease;
100
+ }
101
+
102
+ @starting-style {
103
+ .drawer[open] { opacity: 0; translate: var(--_translate-closed); }
104
+ .drawer[open]::backdrop { opacity: 0; }
105
+ }
106
+
107
+ /* ===== DIRECTIONS ===== */
108
+
109
+ /* Right */
110
+ .drawer[data-direction="right"] {
111
+ --_translate-closed: 100% 0;
112
+ inset: 0 0 0 auto;
113
+ margin: 0;
114
+ width: 100%;
115
+ max-width: var(--drawer-max-width);
116
+ height: 100dvh;
117
+ max-height: 100dvh;
118
+ border-radius: var(--drawer-radius) 0 0 var(--drawer-radius);
119
+ box-shadow: var(--drawer-shadow-right);
120
+ }
121
+
122
+ /* Left */
123
+ .drawer[data-direction="left"] {
124
+ --_translate-closed: -100% 0;
125
+ inset: 0 auto 0 0;
126
+ margin: 0;
127
+ width: 100%;
128
+ max-width: var(--drawer-max-width);
129
+ height: 100dvh;
130
+ max-height: 100dvh;
131
+ border-radius: 0 var(--drawer-radius) var(--drawer-radius) 0;
132
+ box-shadow: var(--drawer-shadow-left);
133
+ }
134
+
135
+ /* Top */
136
+ .drawer[data-direction="top"] {
137
+ --_translate-closed: 0 -100%;
138
+ inset: 0 0 auto 0;
139
+ margin-inline: auto;
140
+ width: 100%;
141
+ max-width: var(--drawer-max-width);
142
+ height: auto;
143
+ max-height: var(--drawer-max-height);
144
+ border-radius: 0 0 var(--drawer-radius) var(--drawer-radius);
145
+ box-shadow: var(--drawer-shadow-top);
146
+ }
147
+
148
+ /* ===== AUTO-NESTING (up to 5 levels) ===== */
149
+ /* Uses sibling combinators to count open drawers */
150
+
151
+ /* 1+ open drawers after */
152
+ .drawer[open]:has(~ .drawer[open]) {
153
+ scale: 0.94;
154
+ translate: 0 -20px;
155
+ border-radius: var(--drawer-radius);
156
+ filter: brightness(0.92);
157
+ pointer-events: none;
158
+ }
159
+
160
+ /* 2+ open drawers after */
161
+ .drawer[open]:has(~ .drawer[open] ~ .drawer[open]) {
162
+ scale: 0.88;
163
+ translate: 0 -40px;
164
+ filter: brightness(0.84);
165
+ }
166
+
167
+ /* 3+ open drawers after */
168
+ .drawer[open]:has(~ .drawer[open] ~ .drawer[open] ~ .drawer[open]) {
169
+ scale: 0.82;
170
+ translate: 0 -60px;
171
+ filter: brightness(0.76);
172
+ }
173
+
174
+ /* 4+ open drawers after */
175
+ .drawer[open]:has(~ .drawer[open] ~ .drawer[open] ~ .drawer[open] ~ .drawer[open]) {
176
+ scale: 0.76;
177
+ translate: 0 -80px;
178
+ filter: brightness(0.68);
179
+ }
180
+
181
+ /* 5+ open drawers after */
182
+ .drawer[open]:has(~ .drawer[open] ~ .drawer[open] ~ .drawer[open] ~ .drawer[open] ~ .drawer[open]) {
183
+ scale: 0.70;
184
+ translate: 0 -100px;
185
+ filter: brightness(0.60);
186
+ }
187
+
188
+ /* Lighter backdrop for stacked drawers */
189
+ .drawer[open] ~ .drawer[open]::backdrop {
190
+ background: hsl(0 0% 0% / 0.15);
191
+ backdrop-filter: none;
192
+ }
193
+
194
+ /* Handle */
195
+ .drawer-handle {
196
+ display: flex;
197
+ justify-content: center;
198
+ padding: 1rem 0 0.5rem;
199
+ cursor: grab;
200
+ }
201
+
202
+ .drawer-handle::before {
203
+ content: '';
204
+ width: 48px;
205
+ height: 5px;
206
+ background: var(--drawer-handle);
207
+ border-radius: 100px;
208
+ transition: width 0.2s var(--drawer-ease), height 0.2s var(--drawer-ease), background 0.2s ease;
209
+ }
210
+
211
+ .drawer-handle:hover::before {
212
+ width: 56px;
213
+ background: hsl(0 0% 60%);
214
+ }
215
+
216
+ /* Vertical handle for left/right drawers */
217
+ .drawer[data-direction="left"] .drawer-handle,
218
+ .drawer[data-direction="right"] .drawer-handle {
219
+ flex-direction: column;
220
+ align-items: center;
221
+ justify-content: center;
222
+ padding: 0.5rem 1rem 0.5rem 0.5rem;
223
+ height: 100%;
224
+ position: absolute;
225
+ top: 0;
226
+ writing-mode: vertical-lr;
227
+ }
228
+
229
+ .drawer[data-direction="left"] .drawer-handle { right: 0; }
230
+ .drawer[data-direction="right"] .drawer-handle { left: 0; }
231
+
232
+ .drawer[data-direction="left"] .drawer-handle::before,
233
+ .drawer[data-direction="right"] .drawer-handle::before {
234
+ width: 5px;
235
+ height: 48px;
236
+ }
237
+
238
+ .drawer[data-direction="left"] .drawer-handle:hover::before,
239
+ .drawer[data-direction="right"] .drawer-handle:hover::before {
240
+ width: 5px;
241
+ height: 56px;
242
+ }
243
+
244
+ /* Content */
245
+ .drawer-content {
246
+ padding: 0.5rem 1.5rem 2rem;
247
+ padding-bottom: calc(2rem + env(safe-area-inset-bottom, 0px));
248
+ overflow-x: hidden;
249
+ overflow-y: auto;
250
+ overscroll-behavior: contain;
251
+ max-height: calc(var(--drawer-max-height) - 60px);
252
+ }
253
+
254
+ /* Content adjustments for directions */
255
+ .drawer[data-direction="left"] .drawer-content,
256
+ .drawer[data-direction="right"] .drawer-content {
257
+ padding: 1.5rem;
258
+ padding-left: 2.5rem;
259
+ max-height: 100%;
260
+ height: 100%;
261
+ }
262
+
263
+ .drawer[data-direction="top"] .drawer-content {
264
+ padding-bottom: 1.5rem;
265
+ padding-top: 0.5rem;
266
+ }
267
+
268
+ /* Reduced motion */
269
+ @media (prefers-reduced-motion: reduce) {
270
+ *, *::before, *::after {
271
+ transition-duration: 0.01ms !important;
272
+ }
273
+
274
+ body:has(.drawer[open]) {
275
+ scale: 1;
276
+ }
277
+
278
+ .drawer[open]:has(~ .drawer[open]),
279
+ .drawer[open]:has(~ .drawer[open] ~ .drawer[open]),
280
+ .drawer[open]:has(~ .drawer[open] ~ .drawer[open] ~ .drawer[open]),
281
+ .drawer[open]:has(~ .drawer[open] ~ .drawer[open] ~ .drawer[open] ~ .drawer[open]),
282
+ .drawer[open]:has(~ .drawer[open] ~ .drawer[open] ~ .drawer[open] ~ .drawer[open] ~ .drawer[open]) {
283
+ scale: 1;
284
+ translate: 0 0;
285
+ filter: none;
286
+ }
287
+ }
288
+
289
+
290
+ /*# sourceMappingURL=react.css.map*/
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.css","names":[],"sources":["../src/drawer.css"],"sourcesContent":["/* CSS Drawer - Vaul-quality drawer with auto-nesting and directions */\n\n:root {\n --drawer-bg: #fff;\n --drawer-radius: 24px;\n --drawer-max-width: 500px;\n --drawer-max-height: 96dvh;\n --drawer-backdrop: hsl(0 0% 0% / 0.4);\n --drawer-handle: hsl(0 0% 80%);\n --drawer-shadow-bottom: 0 -10px 60px hsl(0 0% 0% / 0.12), 0 -4px 20px hsl(0 0% 0% / 0.08);\n --drawer-shadow-top: 0 10px 60px hsl(0 0% 0% / 0.12), 0 4px 20px hsl(0 0% 0% / 0.08);\n --drawer-shadow-right: -10px 0 60px hsl(0 0% 0% / 0.12), -4px 0 20px hsl(0 0% 0% / 0.08);\n --drawer-shadow-left: 10px 0 60px hsl(0 0% 0% / 0.12), 4px 0 20px hsl(0 0% 0% / 0.08);\n --drawer-duration: 0.5s;\n --drawer-duration-close: 0.35s;\n --drawer-ease: cubic-bezier(0.32, 0.72, 0, 1);\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --drawer-bg: hsl(0 0% 12%);\n --drawer-handle: hsl(0 0% 35%);\n --drawer-shadow-bottom: 0 -10px 60px hsl(0 0% 0% / 0.4), 0 -4px 20px hsl(0 0% 0% / 0.3);\n --drawer-shadow-top: 0 10px 60px hsl(0 0% 0% / 0.4), 0 4px 20px hsl(0 0% 0% / 0.3);\n --drawer-shadow-right: -10px 0 60px hsl(0 0% 0% / 0.4), -4px 0 20px hsl(0 0% 0% / 0.3);\n --drawer-shadow-left: 10px 0 60px hsl(0 0% 0% / 0.4), 4px 0 20px hsl(0 0% 0% / 0.3);\n }\n}\n\n/* Background scale effect */\nbody {\n transition: scale var(--drawer-duration) var(--drawer-ease), border-radius var(--drawer-duration) var(--drawer-ease);\n transform-origin: center top;\n}\n\nbody:has(.drawer[open]) {\n overflow: hidden;\n scale: 0.94;\n border-radius: var(--drawer-radius);\n}\n\n/* Base drawer */\n.drawer {\n border: none;\n padding: 0;\n margin: 0;\n max-width: 100%;\n max-height: 100%;\n position: fixed;\n background: var(--drawer-bg);\n overflow: hidden;\n opacity: 0;\n transition:\n display var(--drawer-duration-close) allow-discrete,\n overlay var(--drawer-duration-close) allow-discrete,\n translate var(--drawer-duration-close) var(--drawer-ease),\n scale var(--drawer-duration-close) var(--drawer-ease),\n filter var(--drawer-duration-close) ease,\n opacity var(--drawer-duration-close) ease;\n\n /* Default: bottom */\n --_translate-closed: 0 100%;\n inset: auto 0 0 0;\n margin-inline: auto;\n width: 100%;\n max-width: var(--drawer-max-width);\n height: auto;\n max-height: var(--drawer-max-height);\n border-radius: var(--drawer-radius) var(--drawer-radius) 0 0;\n box-shadow: var(--drawer-shadow-bottom);\n translate: var(--_translate-closed);\n}\n\n.drawer::backdrop {\n background: var(--drawer-backdrop);\n opacity: 0;\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n transition:\n display var(--drawer-duration-close) allow-discrete,\n overlay var(--drawer-duration-close) allow-discrete,\n opacity var(--drawer-duration-close) ease;\n}\n\n.drawer[open] {\n opacity: 1;\n translate: 0 0;\n transition:\n display var(--drawer-duration) allow-discrete,\n overlay var(--drawer-duration) allow-discrete,\n translate var(--drawer-duration) var(--drawer-ease),\n scale var(--drawer-duration) var(--drawer-ease),\n filter var(--drawer-duration) ease,\n opacity 0.15s ease;\n}\n\n.drawer[open]::backdrop {\n opacity: 1;\n transition: display var(--drawer-duration) allow-discrete, overlay var(--drawer-duration) allow-discrete, opacity 0.2s ease;\n}\n\n@starting-style {\n .drawer[open] { opacity: 0; translate: var(--_translate-closed); }\n .drawer[open]::backdrop { opacity: 0; }\n}\n\n/* ===== DIRECTIONS ===== */\n\n/* Right */\n.drawer[data-direction=\"right\"] {\n --_translate-closed: 100% 0;\n inset: 0 0 0 auto;\n margin: 0;\n width: 100%;\n max-width: var(--drawer-max-width);\n height: 100dvh;\n max-height: 100dvh;\n border-radius: var(--drawer-radius) 0 0 var(--drawer-radius);\n box-shadow: var(--drawer-shadow-right);\n}\n\n/* Left */\n.drawer[data-direction=\"left\"] {\n --_translate-closed: -100% 0;\n inset: 0 auto 0 0;\n margin: 0;\n width: 100%;\n max-width: var(--drawer-max-width);\n height: 100dvh;\n max-height: 100dvh;\n border-radius: 0 var(--drawer-radius) var(--drawer-radius) 0;\n box-shadow: var(--drawer-shadow-left);\n}\n\n/* Top */\n.drawer[data-direction=\"top\"] {\n --_translate-closed: 0 -100%;\n inset: 0 0 auto 0;\n margin-inline: auto;\n width: 100%;\n max-width: var(--drawer-max-width);\n height: auto;\n max-height: var(--drawer-max-height);\n border-radius: 0 0 var(--drawer-radius) var(--drawer-radius);\n box-shadow: var(--drawer-shadow-top);\n}\n\n/* ===== AUTO-NESTING (up to 5 levels) ===== */\n/* Uses sibling combinators to count open drawers */\n\n/* 1+ open drawers after */\n.drawer[open]:has(~ .drawer[open]) {\n scale: 0.94;\n translate: 0 -20px;\n border-radius: var(--drawer-radius);\n filter: brightness(0.92);\n pointer-events: none;\n}\n\n/* 2+ open drawers after */\n.drawer[open]:has(~ .drawer[open] ~ .drawer[open]) {\n scale: 0.88;\n translate: 0 -40px;\n filter: brightness(0.84);\n}\n\n/* 3+ open drawers after */\n.drawer[open]:has(~ .drawer[open] ~ .drawer[open] ~ .drawer[open]) {\n scale: 0.82;\n translate: 0 -60px;\n filter: brightness(0.76);\n}\n\n/* 4+ open drawers after */\n.drawer[open]:has(~ .drawer[open] ~ .drawer[open] ~ .drawer[open] ~ .drawer[open]) {\n scale: 0.76;\n translate: 0 -80px;\n filter: brightness(0.68);\n}\n\n/* 5+ open drawers after */\n.drawer[open]:has(~ .drawer[open] ~ .drawer[open] ~ .drawer[open] ~ .drawer[open] ~ .drawer[open]) {\n scale: 0.70;\n translate: 0 -100px;\n filter: brightness(0.60);\n}\n\n/* Lighter backdrop for stacked drawers */\n.drawer[open] ~ .drawer[open]::backdrop {\n background: hsl(0 0% 0% / 0.15);\n backdrop-filter: none;\n}\n\n/* Handle */\n.drawer-handle {\n display: flex;\n justify-content: center;\n padding: 1rem 0 0.5rem;\n cursor: grab;\n}\n\n.drawer-handle::before {\n content: '';\n width: 48px;\n height: 5px;\n background: var(--drawer-handle);\n border-radius: 100px;\n transition: width 0.2s var(--drawer-ease), height 0.2s var(--drawer-ease), background 0.2s ease;\n}\n\n.drawer-handle:hover::before {\n width: 56px;\n background: hsl(0 0% 60%);\n}\n\n/* Vertical handle for left/right drawers */\n.drawer[data-direction=\"left\"] .drawer-handle,\n.drawer[data-direction=\"right\"] .drawer-handle {\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 0.5rem 1rem 0.5rem 0.5rem;\n height: 100%;\n position: absolute;\n top: 0;\n writing-mode: vertical-lr;\n}\n\n.drawer[data-direction=\"left\"] .drawer-handle { right: 0; }\n.drawer[data-direction=\"right\"] .drawer-handle { left: 0; }\n\n.drawer[data-direction=\"left\"] .drawer-handle::before,\n.drawer[data-direction=\"right\"] .drawer-handle::before {\n width: 5px;\n height: 48px;\n}\n\n.drawer[data-direction=\"left\"] .drawer-handle:hover::before,\n.drawer[data-direction=\"right\"] .drawer-handle:hover::before {\n width: 5px;\n height: 56px;\n}\n\n/* Content */\n.drawer-content {\n padding: 0.5rem 1.5rem 2rem;\n padding-bottom: calc(2rem + env(safe-area-inset-bottom, 0px));\n overflow-x: hidden;\n overflow-y: auto;\n overscroll-behavior: contain;\n max-height: calc(var(--drawer-max-height) - 60px);\n}\n\n/* Content adjustments for directions */\n.drawer[data-direction=\"left\"] .drawer-content,\n.drawer[data-direction=\"right\"] .drawer-content {\n padding: 1.5rem;\n padding-left: 2.5rem;\n max-height: 100%;\n height: 100%;\n}\n\n.drawer[data-direction=\"top\"] .drawer-content {\n padding-bottom: 1.5rem;\n padding-top: 0.5rem;\n}\n\n/* Reduced motion */\n@media (prefers-reduced-motion: reduce) {\n *, *::before, *::after {\n transition-duration: 0.01ms !important;\n }\n\n body:has(.drawer[open]) {\n scale: 1;\n }\n\n .drawer[open]:has(~ .drawer[open]),\n .drawer[open]:has(~ .drawer[open] ~ .drawer[open]),\n .drawer[open]:has(~ .drawer[open] ~ .drawer[open] ~ .drawer[open]),\n .drawer[open]:has(~ .drawer[open] ~ .drawer[open] ~ .drawer[open] ~ .drawer[open]),\n .drawer[open]:has(~ .drawer[open] ~ .drawer[open] ~ .drawer[open] ~ .drawer[open] ~ .drawer[open]) {\n scale: 1;\n translate: 0 0;\n filter: none;\n }\n}\n"],"mappings":"AAAA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA"}
@@ -0,0 +1,29 @@
1
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
2
+ import * as react0 from "react";
3
+ import { ComponentPropsWithoutRef, ReactNode } from "react";
4
+
5
+ //#region src/react.d.ts
6
+ type Direction = 'bottom' | 'top' | 'left' | 'right';
7
+ interface RootProps {
8
+ children: ReactNode;
9
+ /** Direction the drawer opens from */
10
+ direction?: Direction;
11
+ }
12
+ declare function Root({
13
+ children,
14
+ direction
15
+ }: RootProps): react_jsx_runtime0.JSX.Element;
16
+ interface ContentProps extends Omit<ComponentPropsWithoutRef<'dialog'>, 'open'> {}
17
+ interface HandleProps extends ComponentPropsWithoutRef<'div'> {}
18
+ interface TitleProps extends ComponentPropsWithoutRef<'h2'> {}
19
+ interface DescriptionProps extends ComponentPropsWithoutRef<'p'> {}
20
+ declare const Drawer: {
21
+ Root: typeof Root;
22
+ Content: react0.ForwardRefExoticComponent<ContentProps & react0.RefAttributes<HTMLDialogElement>>;
23
+ Handle: react0.ForwardRefExoticComponent<HandleProps & react0.RefAttributes<HTMLDivElement>>;
24
+ Title: react0.ForwardRefExoticComponent<TitleProps & react0.RefAttributes<HTMLHeadingElement>>;
25
+ Description: react0.ForwardRefExoticComponent<DescriptionProps & react0.RefAttributes<HTMLParagraphElement>>;
26
+ };
27
+ //#endregion
28
+ export { Drawer, type ContentProps as DrawerContentProps, type DescriptionProps as DrawerDescriptionProps, type Direction as DrawerDirection, type HandleProps as DrawerHandleProps, type RootProps as DrawerRootProps, type TitleProps as DrawerTitleProps };
29
+ //# sourceMappingURL=react.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.cts","names":[],"sources":["../src/react.tsx"],"sourcesContent":[],"mappings":";;;;;KA8CK,SAAA;UAcK,SAAA;YACE;EAfP;EAcK,SAAA,CAAA,EAGI,SAHK;AAGI;iBAGd,IAAA,CAAO;EAAA,QAAA;EAAA;AAAA,CAAA,EAAuB,SAAvB,CAAA,EAAgC,kBAAA,CAAA,GAAA,CAAA,OAAhC;UASN,YAAA,SAAqB,IATL,CASU,wBATV,CAAA,QAAA,CAAA,EAAA,MAAA,CAAA,CAAA;UAqChB,WAAA,SAAoB,wBArCkB,CAAA,KAAA,CAAA,CAAA;AAAA,UAkDtC,UAAA,SAAmB,wBAzCO,CAAA,IAAL,CAAA,CAAA,CAAI;AA4BmB,UAqB5C,gBAAA,SAAyB,wBARkB,CAAA,GAAA,CAAA,CAAA,CAAA;AAgBxC,cAAA,MAMZ,EAAA"}
@@ -0,0 +1,29 @@
1
+ import * as react0 from "react";
2
+ import { ComponentPropsWithoutRef, ReactNode } from "react";
3
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
4
+
5
+ //#region src/react.d.ts
6
+ type Direction = 'bottom' | 'top' | 'left' | 'right';
7
+ interface RootProps {
8
+ children: ReactNode;
9
+ /** Direction the drawer opens from */
10
+ direction?: Direction;
11
+ }
12
+ declare function Root({
13
+ children,
14
+ direction
15
+ }: RootProps): react_jsx_runtime0.JSX.Element;
16
+ interface ContentProps extends Omit<ComponentPropsWithoutRef<'dialog'>, 'open'> {}
17
+ interface HandleProps extends ComponentPropsWithoutRef<'div'> {}
18
+ interface TitleProps extends ComponentPropsWithoutRef<'h2'> {}
19
+ interface DescriptionProps extends ComponentPropsWithoutRef<'p'> {}
20
+ declare const Drawer: {
21
+ Root: typeof Root;
22
+ Content: react0.ForwardRefExoticComponent<ContentProps & react0.RefAttributes<HTMLDialogElement>>;
23
+ Handle: react0.ForwardRefExoticComponent<HandleProps & react0.RefAttributes<HTMLDivElement>>;
24
+ Title: react0.ForwardRefExoticComponent<TitleProps & react0.RefAttributes<HTMLHeadingElement>>;
25
+ Description: react0.ForwardRefExoticComponent<DescriptionProps & react0.RefAttributes<HTMLParagraphElement>>;
26
+ };
27
+ //#endregion
28
+ export { Drawer, type ContentProps as DrawerContentProps, type DescriptionProps as DrawerDescriptionProps, type Direction as DrawerDirection, type HandleProps as DrawerHandleProps, type RootProps as DrawerRootProps, type TitleProps as DrawerTitleProps };
29
+ //# sourceMappingURL=react.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.mts","names":[],"sources":["../src/react.tsx"],"sourcesContent":[],"mappings":";;;;;KA8CK,SAAA;UAcK,SAAA;YACE;EAfP;EAcK,SAAA,CAAA,EAGI,SAHK;AAGI;iBAGd,IAAA,CAAO;EAAA,QAAA;EAAA;AAAA,CAAA,EAAuB,SAAvB,CAAA,EAAgC,kBAAA,CAAA,GAAA,CAAA,OAAhC;UASN,YAAA,SAAqB,IATL,CASU,wBATV,CAAA,QAAA,CAAA,EAAA,MAAA,CAAA,CAAA;UAqChB,WAAA,SAAoB,wBArCkB,CAAA,KAAA,CAAA,CAAA;AAAA,UAkDtC,UAAA,SAAmB,wBAzCO,CAAA,IAAL,CAAA,CAAA,CAAI;AA4BmB,UAqB5C,gBAAA,SAAyB,wBARkB,CAAA,GAAA,CAAA,CAAA,CAAA;AAgBxC,cAAA,MAMZ,EAAA"}
package/dist/react.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import{createContext as e,forwardRef as t,useContext as n}from"react";import{jsx as r}from"react/jsx-runtime";import './react.css';
2
+ if(typeof window<`u`){let e=()=>{let e=Array.from(document.querySelectorAll(`dialog.drawer[open]`));e.forEach((t,n)=>{n===e.length-1?t.removeAttribute(`inert`):t.setAttribute(`inert`,``)})};new MutationObserver(t=>{for(let n of t)if(n.type===`attributes`&&n.attributeName===`open`&&n.target.classList.contains(`drawer`)){e();break}}).observe(document.body,{subtree:!0,attributes:!0,attributeFilter:[`open`]})}const i=e({direction:void 0});function a(){return n(i)}function o({children:e,direction:t}){return r(i.Provider,{value:{direction:t},children:e})}const s=t(({children:e,className:t,...n},i)=>{let{direction:o}=a();return r(`dialog`,{ref:i,className:`drawer ${t??``}`.trim(),"data-direction":o,onClick:e=>{n.onClick?.(e),e.target===e.currentTarget&&e.currentTarget.close()},...n,children:e})});s.displayName=`Drawer.Content`;const c=t(({className:e,...t},n)=>r(`div`,{ref:n,className:`drawer-handle ${e??``}`.trim(),"aria-hidden":`true`,...t}));c.displayName=`Drawer.Handle`;const l=t((e,t)=>r(`h2`,{ref:t,...e}));l.displayName=`Drawer.Title`;const u=t((e,t)=>r(`p`,{ref:t,...e}));u.displayName=`Drawer.Description`;const d={Root:o,Content:s,Handle:c,Title:l,Description:u};export{d as Drawer};
3
+ //# sourceMappingURL=react.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.mjs","names":[],"sources":["../src/react.tsx"],"sourcesContent":["import {\n createContext,\n useContext,\n forwardRef,\n type ReactNode,\n type ComponentPropsWithoutRef,\n} from 'react'\nimport './drawer.css'\n\n/* ===== Auto-enable accessibility for stacked drawers ===== */\nif (typeof window !== 'undefined') {\n const updateInertState = () => {\n const openDrawers = Array.from(\n document.querySelectorAll<HTMLDialogElement>('dialog.drawer[open]')\n )\n openDrawers.forEach((drawer, index) => {\n const isTopmost = index === openDrawers.length - 1\n if (isTopmost) {\n drawer.removeAttribute('inert')\n } else {\n drawer.setAttribute('inert', '')\n }\n })\n }\n\n const observer = new MutationObserver((mutations) => {\n for (const mutation of mutations) {\n if (\n mutation.type === 'attributes' &&\n mutation.attributeName === 'open' &&\n (mutation.target as HTMLElement).classList.contains('drawer')\n ) {\n updateInertState()\n break\n }\n }\n })\n\n observer.observe(document.body, {\n subtree: true,\n attributes: true,\n attributeFilter: ['open'],\n })\n}\n\n/* ===== Types ===== */\ntype Direction = 'bottom' | 'top' | 'left' | 'right'\n\ninterface DrawerContextValue {\n direction?: Direction\n}\n\n/* ===== Context ===== */\nconst DrawerContext = createContext<DrawerContextValue>({ direction: undefined })\n\nfunction useDrawerContext() {\n return useContext(DrawerContext)\n}\n\n/* ===== Root ===== */\ninterface RootProps {\n children: ReactNode\n /** Direction the drawer opens from */\n direction?: Direction\n}\n\nfunction Root({ children, direction }: RootProps) {\n return (\n <DrawerContext.Provider value={{ direction }}>\n {children}\n </DrawerContext.Provider>\n )\n}\n\n/* ===== Content ===== */\ninterface ContentProps extends Omit<ComponentPropsWithoutRef<'dialog'>, 'open'> {}\n\nconst Content = forwardRef<HTMLDialogElement, ContentProps>(\n ({ children, className, ...props }, ref) => {\n const { direction } = useDrawerContext()\n\n return (\n <dialog\n ref={ref}\n className={`drawer ${className ?? ''}`.trim()}\n data-direction={direction}\n onClick={(e) => {\n props.onClick?.(e)\n // Backdrop click - only if clicking the dialog element itself\n if (e.target === e.currentTarget) {\n e.currentTarget.close()\n }\n }}\n {...props}\n >\n {children}\n </dialog>\n )\n }\n)\nContent.displayName = 'Drawer.Content'\n\n/* ===== Handle ===== */\ninterface HandleProps extends ComponentPropsWithoutRef<'div'> {}\n\nconst Handle = forwardRef<HTMLDivElement, HandleProps>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={`drawer-handle ${className ?? ''}`.trim()}\n aria-hidden=\"true\"\n {...props}\n />\n))\nHandle.displayName = 'Drawer.Handle'\n\n/* ===== Title ===== */\ninterface TitleProps extends ComponentPropsWithoutRef<'h2'> {}\n\nconst Title = forwardRef<HTMLHeadingElement, TitleProps>((props, ref) => (\n <h2 ref={ref} {...props} />\n))\nTitle.displayName = 'Drawer.Title'\n\n/* ===== Description ===== */\ninterface DescriptionProps extends ComponentPropsWithoutRef<'p'> {}\n\nconst Description = forwardRef<HTMLParagraphElement, DescriptionProps>((props, ref) => (\n <p ref={ref} {...props} />\n))\nDescription.displayName = 'Drawer.Description'\n\n/* ===== Namespace Export ===== */\nexport const Drawer = {\n Root,\n Content,\n Handle,\n Title,\n Description,\n}\n\nexport type {\n RootProps as DrawerRootProps,\n ContentProps as DrawerContentProps,\n HandleProps as DrawerHandleProps,\n TitleProps as DrawerTitleProps,\n DescriptionProps as DrawerDescriptionProps,\n Direction as DrawerDirection,\n}\n"],"mappings":"8GAUA,GAAI,OAAO,OAAW,IAAa,CACjC,IAAM,MAAyB,CAC7B,IAAM,EAAc,MAAM,KACxB,SAAS,iBAAoC,sBAAsB,CACpE,CACD,EAAY,SAAS,EAAQ,IAAU,CACnB,IAAU,EAAY,OAAS,EAE/C,EAAO,gBAAgB,QAAQ,CAE/B,EAAO,aAAa,QAAS,GAAG,EAElC,EAGa,IAAI,iBAAkB,GAAc,CACnD,IAAK,IAAM,KAAY,EACrB,GACE,EAAS,OAAS,cAClB,EAAS,gBAAkB,QAC1B,EAAS,OAAuB,UAAU,SAAS,SAAS,CAC7D,CACA,GAAkB,CAClB,QAGJ,CAEO,QAAQ,SAAS,KAAM,CAC9B,QAAS,GACT,WAAY,GACZ,gBAAiB,CAAC,OAAO,CAC1B,CAAC,CAWJ,MAAM,EAAgB,EAAkC,CAAE,UAAW,IAAA,GAAW,CAAC,CAEjF,SAAS,GAAmB,CAC1B,OAAO,EAAW,EAAc,CAUlC,SAAS,EAAK,CAAE,WAAU,aAAwB,CAChD,OACE,EAAC,EAAc,SAAA,CAAS,MAAO,CAAE,YAAW,CACzC,YACsB,CAO7B,MAAM,EAAU,GACb,CAAE,WAAU,YAAW,GAAG,GAAS,IAAQ,CAC1C,GAAM,CAAE,aAAc,GAAkB,CAExC,OACE,EAAC,SAAA,CACM,MACL,UAAW,UAAU,GAAa,KAAK,MAAM,CAC7C,iBAAgB,EAChB,QAAU,GAAM,CACd,EAAM,UAAU,EAAE,CAEd,EAAE,SAAW,EAAE,eACjB,EAAE,cAAc,OAAO,EAG3B,GAAI,EAEH,YACM,EAGd,CACD,EAAQ,YAAc,iBAKtB,MAAM,EAAS,GAAyC,CAAE,YAAW,GAAG,GAAS,IAC/E,EAAC,MAAA,CACM,MACL,UAAW,iBAAiB,GAAa,KAAK,MAAM,CACpD,cAAY,OACZ,GAAI,GACJ,CACF,CACF,EAAO,YAAc,gBAKrB,MAAM,EAAQ,GAA4C,EAAO,IAC/D,EAAC,KAAA,CAAQ,MAAK,GAAI,GAAS,CAC3B,CACF,EAAM,YAAc,eAKpB,MAAM,EAAc,GAAoD,EAAO,IAC7E,EAAC,IAAA,CAAO,MAAK,GAAI,GAAS,CAC1B,CACF,EAAY,YAAc,qBAG1B,MAAa,EAAS,CACpB,OACA,UACA,SACA,QACA,cACD"}
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "css-drawer",
3
+ "version": "0.1.0",
4
+ "description": "Vaul-quality drawer component using native <dialog> and pure CSS animations",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.mts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.mts",
13
+ "default": "./dist/index.mjs"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ },
20
+ "./react": {
21
+ "import": {
22
+ "types": "./dist/react.d.mts",
23
+ "default": "./dist/react.mjs"
24
+ },
25
+ "require": {
26
+ "types": "./dist/react.d.cts",
27
+ "default": "./dist/react.cjs"
28
+ }
29
+ },
30
+ "./styles": "./dist/react.css",
31
+ "./drawer.css": "./dist/react.css"
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "peerDependencies": {
37
+ "react": ">=18.0.0",
38
+ "react-dom": ">=18.0.0"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "react": {
42
+ "optional": true
43
+ },
44
+ "react-dom": {
45
+ "optional": true
46
+ }
47
+ },
48
+ "devDependencies": {
49
+ "@bosh-code/tsdown-plugin-inject-css": "^2.0.0",
50
+ "@types/react": "^19.1.6",
51
+ "@types/react-dom": "^19.1.5",
52
+ "react": "^19.1.0",
53
+ "tsdown": "^0.18.2",
54
+ "typescript": "^5.7.2"
55
+ },
56
+ "keywords": [
57
+ "drawer",
58
+ "bottom-sheet",
59
+ "dialog",
60
+ "modal",
61
+ "css",
62
+ "headless",
63
+ "vaul",
64
+ "react"
65
+ ],
66
+ "license": "MIT",
67
+ "author": "Saransh Chaudhary",
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/tark-ai/css-drawer.git"
71
+ },
72
+ "homepage": "https://github.com/tark-ai/css-drawer#readme",
73
+ "bugs": {
74
+ "url": "https://github.com/tark-ai/css-drawer/issues"
75
+ },
76
+ "publishConfig": {
77
+ "access": "public"
78
+ },
79
+ "sideEffects": [
80
+ "*.css",
81
+ "./dist/index.mjs",
82
+ "./dist/index.cjs",
83
+ "./dist/react.mjs",
84
+ "./dist/react.cjs"
85
+ ],
86
+ "scripts": {
87
+ "build": "tsdown",
88
+ "dev": "tsdown --watch"
89
+ }
90
+ }