torch-glare 2.1.4 → 2.1.7

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.
@@ -1,769 +1,628 @@
1
1
  ---
2
2
  title: Drawer
3
- description: Bottom sheet drawer component that slides up from the bottom of the screen for mobile-friendly interactions
3
+ description: Gesture-based sliding panel built on Vaul. Anchors to the bottom, left, or right of the screen, supports nested/stacked drawers, and composes with SectionBlock + InputField to build create and edit forms.
4
4
  group: Overlays & Dialogs
5
- keywords: [drawer, bottom-sheet, slide-up, mobile, vaul, sheet]
5
+ keywords: [drawer, sheet, side-panel, bottom-sheet, slide, vaul, form, create, edit, sectionblock, nested, drag-to-dismiss]
6
6
  ---
7
7
 
8
8
  # Drawer
9
9
 
10
- > A bottom sheet drawer component that slides up from the bottom of the screen. Perfect for mobile-friendly actions, forms, and content that doesn't need a full modal. Built with Vaul for smooth, gesture-based interactions.
10
+ > A gesture-based sliding panel built on [Vaul](https://vaul.emilkowal.ski/). It can slide up from the **bottom**, in from the **right**, or in from the **left**. It supports drag-to-dismiss, nested/stacked drawers with an iOS-style scale-back effect, an optional "notch" tab on the top edge, and a dark framed "tray" look. It is the recommended surface for create / edit forms, filter panels, side navigation, action sheets, and full-screen editors.
11
+
12
+ > [!IMPORTANT]
13
+ > The Drawer is **composed**, not configured. There is no single `direction` style switch on `DrawerContent`. You pick the anchor on the root `<Drawer direction="...">` and then pass layout classes (`wrapperClassName`, `className`, `trayClassName`) plus a few flags (`framed`, `showHandle`, `notch`, `notchSide`) to get the bottom / right / left look. The recipes below give you exact, copy-paste class strings for each direction.
11
14
 
12
15
  ## Installation
13
16
 
14
17
  ```bash
15
- npm install vaul
18
+ npx torch-glare@latest add Drawer
16
19
  ```
17
20
 
21
+ The Drawer's only third-party dependency is [`vaul`](https://www.npmjs.com/package/vaul), which the CLI installs automatically.
22
+
23
+ > [!NOTE]
24
+ > **Building create / edit forms?** The Drawer ships on its own. The form examples on this page also use `SectionBlock`, `InputField`, and `Button`. Install them alongside it:
25
+ >
26
+ > ```bash
27
+ > npx torch-glare@latest add Drawer SectionBlock InputField Button
28
+ > ```
29
+ >
30
+ > These are *not* auto-installed by `add Drawer`, because `Drawer.tsx` does not import them — they are only needed for the form composition pattern, not for the Drawer itself.
31
+
18
32
  ## Import
19
33
 
20
- ```typescript
34
+ ```tsx
21
35
  import {
22
36
  Drawer,
37
+ DrawerNested,
23
38
  DrawerTrigger,
39
+ DrawerClose,
24
40
  DrawerContent,
25
41
  DrawerHeader,
42
+ DrawerHeaderTitle,
43
+ DrawerHeaderActions,
44
+ DrawerBadge,
26
45
  DrawerFooter,
27
46
  DrawerTitle,
28
47
  DrawerDescription,
29
- DrawerClose,
30
- } from '@torch-ui/components'
31
- ```
32
-
33
- ## Quick Examples
34
-
35
- ### Basic Usage
36
-
37
- ```typescript
38
- import { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription } from '@torch-ui/components'
39
- import { Button } from '@torch-ui/components'
40
-
41
- function Example() {
42
- return (
43
- <Drawer>
44
- <DrawerTrigger asChild>
45
- <Button>Open Drawer</Button>
46
- </DrawerTrigger>
47
- <DrawerContent>
48
- <DrawerHeader>
49
- <DrawerTitle>Drawer Title</DrawerTitle>
50
- <DrawerDescription>
51
- This is a drawer that slides up from the bottom.
52
- </DrawerDescription>
53
- </DrawerHeader>
54
- </DrawerContent>
55
- </Drawer>
56
- )
57
- }
48
+ // Notch (the tab that sticks out of the top edge)
49
+ DrawerNotch,
50
+ DrawerNotchClose,
51
+ DrawerNotchPill,
52
+ DrawerNotchDivider,
53
+ DrawerNotchApp,
54
+ // Lower-level primitives (rarely needed directly)
55
+ DrawerPortal,
56
+ DrawerOverlay,
57
+ } from "@/components/Drawer";
58
58
  ```
59
59
 
60
- ### With Form
61
-
62
- ```typescript
63
- import { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerFooter, DrawerTitle, DrawerClose } from '@torch-ui/components'
64
- import { Button, Input } from '@torch-ui/components'
65
-
66
- function FormDrawer() {
67
- const handleSubmit = (e: React.FormEvent) => {
68
- e.preventDefault()
69
- // Handle form submission
70
- }
71
-
72
- return (
73
- <Drawer>
74
- <DrawerTrigger asChild>
75
- <Button>Add Comment</Button>
76
- </DrawerTrigger>
77
- <DrawerContent>
78
- <form onSubmit={handleSubmit}>
79
- <DrawerHeader>
80
- <DrawerTitle>Add a Comment</DrawerTitle>
81
- <DrawerDescription>
82
- Share your thoughts about this post.
83
- </DrawerDescription>
84
- </DrawerHeader>
85
-
86
- <div className="p-4 space-y-4">
87
- <Input placeholder="Your name" />
88
- <textarea
89
- placeholder="Your comment"
90
- className="w-full p-2 border rounded"
91
- rows={4}
92
- />
93
- </div>
94
-
95
- <DrawerFooter>
96
- <Button type="submit">Submit</Button>
97
- <DrawerClose asChild>
98
- <Button variant="SecondaryStyle">Cancel</Button>
99
- </DrawerClose>
100
- </DrawerFooter>
101
- </form>
102
- </DrawerContent>
103
- </Drawer>
104
- )
105
- }
106
- ```
107
-
108
- ### Controlled State
109
-
110
- ```typescript
111
- import { Drawer, DrawerContent, DrawerTitle } from '@torch-ui/components'
112
- import { useState } from 'react'
113
-
114
- function ControlledDrawer() {
115
- const [open, setOpen] = useState(false)
116
-
117
- return (
118
- <>
119
- <button onClick={() => setOpen(true)}>Open Drawer</button>
120
- <Drawer open={open} onOpenChange={setOpen}>
121
- <DrawerContent>
122
- <DrawerTitle>Controlled Drawer</DrawerTitle>
123
- <button onClick={() => setOpen(false)}>Close</button>
124
- </DrawerContent>
125
- </Drawer>
126
- </>
127
- )
128
- }
60
+ ## Anatomy
61
+
62
+ ```tsx
63
+ <Drawer> {/* root owns open state + direction */}
64
+ <DrawerTrigger /> {/* what opens the drawer (use asChild) */}
65
+ <DrawerContent> {/* the panel; controls tray/frame/handle/notch */}
66
+ <DrawerHeader>
67
+ <DrawerHeaderTitle> {/* dark pill holding the title + a badge */}
68
+ <DrawerBadge />
69
+ <DrawerTitle />
70
+ </DrawerHeaderTitle>
71
+ <DrawerHeaderActions /> {/* dark pill holding header buttons */}
72
+ </DrawerHeader>
73
+
74
+ {/* ...your body content... */}
75
+
76
+ <DrawerFooter>
77
+ <DrawerClose /> {/* anything that should close the drawer */}
78
+ </DrawerFooter>
79
+ </DrawerContent>
80
+ </Drawer>
129
81
  ```
130
82
 
131
- ### Action Sheet
132
-
133
- ```typescript
134
- import { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerClose } from '@torch-ui/components'
135
- import { Button } from '@torch-ui/components'
136
-
137
- function ActionSheet() {
138
- const handleShare = (platform: string) => {
139
- console.log('Sharing to', platform)
140
- }
83
+ ## Quick Examples
141
84
 
142
- return (
143
- <Drawer>
144
- <DrawerTrigger asChild>
145
- <Button>Share</Button>
146
- </DrawerTrigger>
147
- <DrawerContent>
148
- <DrawerHeader>
149
- <DrawerTitle>Share this post</DrawerTitle>
150
- </DrawerHeader>
151
- <div className="p-4 space-y-2">
152
- <DrawerClose asChild>
153
- <button
154
- onClick={() => handleShare('twitter')}
155
- className="w-full p-3 text-left hover:bg-gray-100 rounded"
156
- >
157
- <i className="ri-twitter-line mr-2" />
158
- Share on Twitter
159
- </button>
160
- </DrawerClose>
161
- <DrawerClose asChild>
162
- <button
163
- onClick={() => handleShare('facebook')}
164
- className="w-full p-3 text-left hover:bg-gray-100 rounded"
165
- >
166
- <i className="ri-facebook-line mr-2" />
167
- Share on Facebook
168
- </button>
169
- </DrawerClose>
170
- <DrawerClose asChild>
171
- <button
172
- onClick={() => handleShare('copy')}
173
- className="w-full p-3 text-left hover:bg-gray-100 rounded"
174
- >
175
- <i className="ri-link mr-2" />
176
- Copy Link
177
- </button>
178
- </DrawerClose>
179
- </div>
180
- </DrawerContent>
181
- </Drawer>
182
- )
183
- }
85
+ ### Basic (bottom sheet)
86
+
87
+ The default. Slides up from the bottom with a drag handle. Use `framed={false}` for the clean sheet look (no dark tray border).
88
+
89
+ ```tsx
90
+ <Drawer>
91
+ <DrawerTrigger asChild>
92
+ <Button variant="PrimeStyle">Open drawer</Button>
93
+ </DrawerTrigger>
94
+ <DrawerContent framed={false}>
95
+ <DrawerHeader>
96
+ <DrawerTitle>Basic drawer</DrawerTitle>
97
+ <DrawerDescription>
98
+ Drag the handle down or press Escape to close.
99
+ </DrawerDescription>
100
+ </DrawerHeader>
101
+
102
+ <div className="px-4 pb-4">
103
+ <p className="typography-body-small-regular text-content-presentation-action-light-secondary">
104
+ Put any content here — forms, lists, settings.
105
+ </p>
106
+ </div>
107
+
108
+ <DrawerFooter>
109
+ <DrawerClose asChild>
110
+ <Button variant="PrimeStyle">Done</Button>
111
+ </DrawerClose>
112
+ <DrawerClose asChild>
113
+ <Button variant="BorderStyle">Cancel</Button>
114
+ </DrawerClose>
115
+ </DrawerFooter>
116
+ </DrawerContent>
117
+ </Drawer>
184
118
  ```
185
119
 
186
- ### Product Details Drawer
187
-
188
- ```typescript
189
- import { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerFooter, DrawerClose } from '@torch-ui/components'
190
- import { Button, Badge } from '@torch-ui/components'
191
-
192
- function ProductDrawer({ product }: { product: Product }) {
193
- return (
194
- <Drawer>
195
- <DrawerTrigger asChild>
196
- <button className="w-full text-left p-4 border rounded hover:bg-gray-50">
197
- <h3>{product.name}</h3>
198
- <p>${product.price}</p>
199
- </button>
200
- </DrawerTrigger>
201
- <DrawerContent>
202
- <DrawerHeader>
203
- <DrawerTitle>{product.name}</DrawerTitle>
204
- <DrawerDescription>
205
- <Badge variant="green" label={product.stock > 0 ? 'In Stock' : 'Out of Stock'} />
206
- </DrawerDescription>
207
- </DrawerHeader>
120
+ ### Create / Edit form (Drawer + SectionBlock)
208
121
 
209
- <div className="p-4">
210
- <img src={product.image} alt={product.name} className="w-full rounded-lg mb-4" />
211
- <p className="text-2xl font-bold mb-2">${product.price}</p>
212
- <p className="text-gray-600">{product.description}</p>
213
- </div>
122
+ This is the headline pattern: a **right-anchored** drawer that holds a form, grouped into `SectionBlock`s, with the inputs laid out as label-on-left / field-on-right rows. The same layout works for both **"New item"** (empty fields) and **"Edit item"** (pass `defaultValue` to each field and swap the badge/title).
214
123
 
215
- <DrawerFooter>
216
- <Button className="w-full">Add to Cart</Button>
217
- <DrawerClose asChild>
218
- <Button variant="SecondaryStyle" className="w-full">
219
- Continue Shopping
220
- </Button>
221
- </DrawerClose>
222
- </DrawerFooter>
223
- </DrawerContent>
224
- </Drawer>
225
- )
226
- }
227
- ```
228
-
229
- ### Filter Drawer
230
-
231
- ```typescript
232
- import { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerFooter, DrawerClose } from '@torch-ui/components'
233
- import { Button, Checkbox } from '@torch-ui/components'
234
- import { useState } from 'react'
124
+ ```tsx
125
+ import {
126
+ Drawer,
127
+ DrawerTrigger,
128
+ DrawerContent,
129
+ DrawerHeader,
130
+ DrawerHeaderTitle,
131
+ DrawerHeaderActions,
132
+ DrawerBadge,
133
+ DrawerTitle,
134
+ DrawerClose,
135
+ DrawerNotch,
136
+ DrawerNotchClose,
137
+ DrawerNotchPill,
138
+ } from "@/components/Drawer";
139
+ import { Button } from "@/components/Button";
140
+ import { SectionBlock } from "@/components/SectionBlock";
141
+ import { InputField } from "@/components/InputField";
235
142
 
236
- function FilterDrawer() {
237
- const [filters, setFilters] = useState({
238
- inStock: false,
239
- onSale: false,
240
- freeShipping: false,
241
- })
143
+ function ContactDrawer({ mode = "create", contact }) {
144
+ const isEdit = mode === "edit";
242
145
 
243
146
  return (
244
- <Drawer>
147
+ <Drawer direction="right">
245
148
  <DrawerTrigger asChild>
246
- <Button>
247
- <i className="ri-filter-line mr-2" />
248
- Filters
149
+ <Button variant="PrimeStyle">
150
+ {isEdit ? "Edit contact" : "New contact"}
249
151
  </Button>
250
152
  </DrawerTrigger>
251
- <DrawerContent>
153
+
154
+ <DrawerContent
155
+ showHandle={false}
156
+ /* The top-edge notch: close button + "Open in new tab" pill. */
157
+ notch={
158
+ <DrawerNotch>
159
+ <DrawerClose asChild>
160
+ <DrawerNotchClose />
161
+ </DrawerClose>
162
+ <DrawerNotchPill color="Yellow">
163
+ Open in new tab
164
+ <i className="ri-arrow-right-up-line text-[12px]" />
165
+ </DrawerNotchPill>
166
+ </DrawerNotch>
167
+ }
168
+ wrapperClassName="top-2 right-2 bottom-2 left-auto mt-0 h-auto w-[1046px] max-w-[calc(100vw-16px)]"
169
+ /* With a notch, round all corners EXCEPT the top one the notch sits on. */
170
+ className="rounded-tr-[16px] rounded-b-[16px]"
171
+ >
172
+ {/* Header: badge flips New / Edit, actions live in their own dark pill */}
252
173
  <DrawerHeader>
253
- <DrawerTitle>Filter Products</DrawerTitle>
174
+ <DrawerHeaderTitle>
175
+ <DrawerBadge color={isEdit ? "Yellow" : "Blue"}>
176
+ {isEdit ? "Edit" : "New"}
177
+ </DrawerBadge>
178
+ <DrawerTitle>Individual Contact</DrawerTitle>
179
+ </DrawerHeaderTitle>
180
+ <div className="flex items-center">
181
+ <Button variant="PrimeStyle" size="L">Save Draft</Button>
182
+ </div>
254
183
  </DrawerHeader>
255
184
 
256
- <div className="p-4 space-y-4">
257
- <label className="flex items-center gap-2">
258
- <Checkbox
259
- checked={filters.inStock}
260
- onCheckedChange={(checked) =>
261
- setFilters({ ...filters, inStock: !!checked })
185
+ {/* Scrollable body holds the grouped form sections */}
186
+ <div className="flex-1 overflow-y-auto px-12 py-6 space-y-3">
187
+ <SectionBlock
188
+ color="Blue"
189
+ containerClassName="w-full"
190
+ title={
191
+ <span className="flex items-center gap-[6px]">
192
+ <i className="ri-draft-fill" />
193
+ Identity
194
+ </span>
195
+ }
196
+ >
197
+ <FieldRow
198
+ label="Name"
199
+ required
200
+ right={
201
+ <div className="flex flex-1 min-w-0 items-center gap-3">
202
+ <InputField
203
+ placeholder="First Name*"
204
+ defaultValue={contact?.firstName}
205
+ className="flex-1 min-w-0"
206
+ />
207
+ <InputField
208
+ placeholder="Last Name*"
209
+ defaultValue={contact?.lastName}
210
+ className="flex-1 min-w-0"
211
+ />
212
+ </div>
262
213
  }
263
214
  />
264
- <span>In Stock Only</span>
265
- </label>
266
- <label className="flex items-center gap-2">
267
- <Checkbox
268
- checked={filters.onSale}
269
- onCheckedChange={(checked) =>
270
- setFilters({ ...filters, onSale: !!checked })
215
+ <RowDivider />
216
+ <FieldRow
217
+ label="Email"
218
+ required
219
+ right={
220
+ <InputField
221
+ type="email"
222
+ placeholder="name@example.com"
223
+ defaultValue={contact?.email}
224
+ className="flex-1"
225
+ />
271
226
  }
272
227
  />
273
- <span>On Sale</span>
274
- </label>
275
- <label className="flex items-center gap-2">
276
- <Checkbox
277
- checked={filters.freeShipping}
278
- onCheckedChange={(checked) =>
279
- setFilters({ ...filters, freeShipping: !!checked })
228
+ <RowDivider />
229
+ <FieldRow
230
+ label="Phone"
231
+ right={
232
+ <InputField
233
+ placeholder="+1 555 0000"
234
+ defaultValue={contact?.phone}
235
+ className="flex-1"
236
+ />
280
237
  }
281
238
  />
282
- <span>Free Shipping</span>
283
- </label>
284
- </div>
285
-
286
- <DrawerFooter>
287
- <Button>Apply Filters</Button>
288
- <DrawerClose asChild>
289
- <Button variant="SecondaryStyle">Reset</Button>
290
- </DrawerClose>
291
- </DrawerFooter>
292
- </DrawerContent>
293
- </Drawer>
294
- )
295
- }
296
- ```
297
-
298
- ### Settings Drawer
299
-
300
- ```typescript
301
- import { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription } from '@torch-ui/components'
302
- import { Switch } from '@torch-ui/components'
303
- import { useState } from 'react'
304
-
305
- function SettingsDrawer() {
306
- const [settings, setSettings] = useState({
307
- notifications: true,
308
- darkMode: false,
309
- autoPlay: true,
310
- })
311
-
312
- return (
313
- <Drawer>
314
- <DrawerTrigger asChild>
315
- <button>
316
- <i className="ri-settings-line text-2xl" />
317
- </button>
318
- </DrawerTrigger>
319
- <DrawerContent>
320
- <DrawerHeader>
321
- <DrawerTitle>Settings</DrawerTitle>
322
- <DrawerDescription>
323
- Manage your preferences
324
- </DrawerDescription>
325
- </DrawerHeader>
326
-
327
- <div className="p-4 space-y-4">
328
- <div className="flex items-center justify-between">
329
- <span>Notifications</span>
330
- <Switch
331
- checked={settings.notifications}
332
- onCheckedChange={(checked) =>
333
- setSettings({ ...settings, notifications: checked })
334
- }
239
+ </SectionBlock>
240
+
241
+ <SectionBlock
242
+ color="Purple"
243
+ containerClassName="w-full"
244
+ title={
245
+ <span className="flex items-center gap-[6px]">
246
+ <i className="ri-map-pin-line" />
247
+ Address
248
+ </span>
249
+ }
250
+ >
251
+ <FieldRow
252
+ label="Street"
253
+ right={<InputField placeholder="123 Main St" className="flex-1" />}
335
254
  />
336
- </div>
337
- <div className="flex items-center justify-between">
338
- <span>Dark Mode</span>
339
- <Switch
340
- checked={settings.darkMode}
341
- onCheckedChange={(checked) =>
342
- setSettings({ ...settings, darkMode: checked })
343
- }
344
- />
345
- </div>
346
- <div className="flex items-center justify-between">
347
- <span>Auto-play Videos</span>
348
- <Switch
349
- checked={settings.autoPlay}
350
- onCheckedChange={(checked) =>
351
- setSettings({ ...settings, autoPlay: checked })
352
- }
255
+ <RowDivider />
256
+ <FieldRow
257
+ label="City"
258
+ right={<InputField placeholder="San Francisco" className="flex-1" />}
353
259
  />
354
- </div>
260
+ </SectionBlock>
355
261
  </div>
356
262
  </DrawerContent>
357
263
  </Drawer>
358
- )
264
+ );
359
265
  }
360
- ```
361
-
362
- ### Confirmation Drawer
363
266
 
364
- ```typescript
365
- import { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerFooter, DrawerClose } from '@torch-ui/components'
366
- import { Button } from '@torch-ui/components'
367
-
368
- function ConfirmationDrawer({ onConfirm }: { onConfirm: () => void }) {
267
+ /* Reusable label-on-left / field-on-right row. */
268
+ function FieldRow({ label, required, right }) {
369
269
  return (
370
- <Drawer>
371
- <DrawerTrigger asChild>
372
- <Button variant="DestructiveStyle">Delete Item</Button>
373
- </DrawerTrigger>
374
- <DrawerContent>
375
- <DrawerHeader>
376
- <DrawerTitle>Confirm Deletion</DrawerTitle>
377
- <DrawerDescription>
378
- This action cannot be undone. This will permanently delete the item.
379
- </DrawerDescription>
380
- </DrawerHeader>
381
-
382
- <DrawerFooter>
383
- <DrawerClose asChild>
384
- <Button variant="DestructiveStyle" onClick={onConfirm}>
385
- Yes, Delete
386
- </Button>
387
- </DrawerClose>
388
- <DrawerClose asChild>
389
- <Button variant="SecondaryStyle">Cancel</Button>
390
- </DrawerClose>
391
- </DrawerFooter>
392
- </DrawerContent>
393
- </Drawer>
394
- )
270
+ <div className="flex items-center gap-6 py-[18px]">
271
+ <div className="flex w-[180px] shrink-0 items-center gap-[6px]">
272
+ <span className="typography-body-medium-regular text-content-presentation-action-light-primary">
273
+ {label}
274
+ </span>
275
+ {required && (
276
+ <span className="typography-body-small-medium text-content-presentation-state-negative">
277
+ (Required)
278
+ </span>
279
+ )}
280
+ </div>
281
+ <div className="flex flex-1 min-w-0 items-center">{right}</div>
282
+ </div>
283
+ );
395
284
  }
396
- ```
397
-
398
- ### Scrollable Content
399
285
 
400
- ```typescript
401
- import { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerFooter } from '@torch-ui/components'
402
- import { Button, ScrollArea } from '@torch-ui/components'
403
-
404
- function ScrollableDrawer() {
405
- const items = Array.from({ length: 50 }, (_, i) => `Item ${i + 1}`)
406
-
407
- return (
408
- <Drawer>
409
- <DrawerTrigger asChild>
410
- <Button>View List</Button>
411
- </DrawerTrigger>
412
- <DrawerContent>
413
- <DrawerHeader>
414
- <DrawerTitle>Long List</DrawerTitle>
415
- </DrawerHeader>
416
-
417
- <ScrollArea className="h-96">
418
- <div className="p-4 space-y-2">
419
- {items.map((item) => (
420
- <div key={item} className="p-2 border-b">
421
- {item}
422
- </div>
423
- ))}
424
- </div>
425
- </ScrollArea>
426
-
427
- <DrawerFooter>
428
- <Button>Done</Button>
429
- </DrawerFooter>
430
- </DrawerContent>
431
- </Drawer>
432
- )
286
+ /* Thin separator between rows inside a SectionBlock. */
287
+ function RowDivider() {
288
+ return <div className="h-px w-full bg-border-presentation-global-primary" />;
433
289
  }
434
290
  ```
435
291
 
436
- ### Without Background Scale
437
-
438
- ```typescript
439
- import { Drawer, DrawerTrigger, DrawerContent, DrawerTitle } from '@torch-ui/components'
440
-
441
- function NoScaleDrawer() {
442
- return (
443
- <Drawer shouldScaleBackground={false}>
444
- <DrawerTrigger asChild>
445
- <button>Open (No Scale)</button>
446
- </DrawerTrigger>
447
- <DrawerContent>
448
- <DrawerTitle>Drawer without background scaling</DrawerTitle>
449
- <p className="p-4">The background won't scale when this opens.</p>
450
- </DrawerContent>
451
- </Drawer>
452
- )
453
- }
292
+ > **Create vs. Edit in one component.** Keep a single drawer and branch on a `mode` prop:
293
+ > - **Badge / title** — `<DrawerBadge color={isEdit ? "Yellow" : "Blue"}>{isEdit ? "Edit" : "New"}</DrawerBadge>`.
294
+ > - **Fields** — spread the existing record into each input's `defaultValue` (or `value` if controlled). Empty in create mode, pre-filled in edit mode.
295
+ > - **Footer / actions** keep "Save Draft" / "Save" identical; only the submit handler changes (POST vs. PATCH).
296
+
297
+ ### Nested drawers (multi-step)
298
+
299
+ `DrawerNested` must live **inside** an already-open `DrawerContent`. The parent scales down and slides back; the child slides in on top. Great for `cart → shipping → payment` style flows or drill-down settings.
300
+
301
+ ```tsx
302
+ <Drawer>
303
+ <DrawerTrigger asChild>
304
+ <Button variant="PrimeStyle">Checkout</Button>
305
+ </DrawerTrigger>
306
+ <DrawerContent framed={false}>
307
+ <DrawerHeader>
308
+ <DrawerTitle>Your cart</DrawerTitle>
309
+ <DrawerDescription>1 item — $129.00</DrawerDescription>
310
+ </DrawerHeader>
311
+
312
+ <DrawerFooter>
313
+ <DrawerNested>
314
+ <DrawerTrigger asChild>
315
+ <Button variant="PrimeStyle">Continue to shipping</Button>
316
+ </DrawerTrigger>
317
+ <DrawerContent framed={false}>
318
+ <DrawerHeader>
319
+ <DrawerTitle>Shipping</DrawerTitle>
320
+ </DrawerHeader>
321
+ {/* ...another DrawerNested inside here for payment... */}
322
+ </DrawerContent>
323
+ </DrawerNested>
324
+ </DrawerFooter>
325
+ </DrawerContent>
326
+ </Drawer>
454
327
  ```
455
328
 
456
- ## API Reference
457
-
458
- ### Drawer (Root)
459
-
460
- | Prop | Type | Default | Description |
461
- |------|------|---------|-------------|
462
- | `open` | `boolean` | - | Controlled open state |
463
- | `onOpenChange` | `(open: boolean) => void` | - | Callback when open state changes |
464
- | `defaultOpen` | `boolean` | `false` | Uncontrolled default open state |
465
- | `shouldScaleBackground` | `boolean` | `true` | Whether to scale and blur background |
466
- | `modal` | `boolean` | `true` | Whether to render as modal |
467
-
468
- ### DrawerTrigger
469
-
470
- | Prop | Type | Default | Description |
471
- |------|------|---------|-------------|
472
- | `asChild` | `boolean` | `false` | Render trigger as child element |
329
+ ### Notch (top-edge tab)
330
+
331
+ The notch is an optional tab that sticks out of the top edge of the drawer — used for a close button plus an "Open in new tab" / "Open in the app" affordance. Pass it to `DrawerContent` via the `notch` prop. When a notch is present, hide the drag handle (`showHandle={false}`) and use the asymmetric corner rounding (`className="rounded-tr-[16px] rounded-b-[16px]"` for a right drawer) so the panel tucks under the notch.
332
+
333
+ **Simple notch** close button + "Open in new tab" pill. This is the most common form, used on the create/edit drawer above:
334
+
335
+ ```tsx
336
+ <DrawerContent
337
+ showHandle={false}
338
+ className="rounded-tr-[16px] rounded-b-[16px]"
339
+ notch={
340
+ <DrawerNotch>
341
+ <DrawerClose asChild>
342
+ <DrawerNotchClose />
343
+ </DrawerClose>
344
+ <DrawerNotchPill color="Yellow">
345
+ Open in new tab
346
+ <i className="ri-arrow-right-up-line text-[12px]" />
347
+ </DrawerNotchPill>
348
+ </DrawerNotch>
349
+ }
350
+ >
351
+ {/* ... */}
352
+ </DrawerContent>
353
+ ```
473
354
 
474
- ### DrawerContent
355
+ **App notch** — adds an app icon + name and a colored "Open in the app" pill, separated by a divider:
356
+
357
+ ```tsx
358
+ <DrawerContent
359
+ showHandle={false}
360
+ className="rounded-tr-[16px] rounded-b-[16px]"
361
+ notch={
362
+ <DrawerNotch>
363
+ <DrawerClose asChild>
364
+ <DrawerNotchClose />
365
+ </DrawerClose>
366
+ <DrawerNotchDivider />
367
+ <DrawerNotchApp
368
+ icon={<i className="ri-customer-service-2-fill text-white text-[14px]" />}
369
+ name="Sales & Services App"
370
+ />
371
+ <DrawerNotchPill color="Blue">
372
+ Open in the app
373
+ <i className="ri-arrow-right-up-line text-[12px]" />
374
+ </DrawerNotchPill>
375
+ </DrawerNotch>
376
+ }
377
+ >
378
+ {/* ... */}
379
+ </DrawerContent>
380
+ ```
475
381
 
476
- | Prop | Type | Default | Description |
477
- |------|------|---------|-------------|
478
- | `className` | `string` | - | Additional CSS classes |
479
- | `onPointerDownOutside` | `(event) => void` | - | Callback when clicking outside |
480
- | `onEscapeKeyDown` | `(event) => void` | - | Callback when Escape is pressed |
382
+ #### What "Open in new tab" does
481
383
 
482
- ### DrawerHeader, DrawerFooter
384
+ > [!IMPORTANT]
385
+ > **`DrawerNotchPill` has no built-in navigation — it is a styled `<button>`.** The "Open in new tab" interaction is a pattern *you* wire up. The intended behavior is:
386
+ >
387
+ > **Clicking "Open in new tab" opens the *same content* the drawer is showing, but as a standalone full page (its own route) in a new browser tab — not as a drawer.**
388
+ >
389
+ > The drawer is the *quick/inline* way to view or edit a record; the full page is the *expanded* view of the exact same thing (same form, same data), just rendered at full width instead of in the side panel. So a "New contact" drawer opens a full-page "New contact" form; an "Edit contact #42" drawer opens the full-page edit route for contact 42.
483
390
 
484
- | Prop | Type | Default | Description |
485
- |------|------|---------|-------------|
486
- | `className` | `string` | - | Additional CSS classes |
391
+ **How to wire it.** Point the pill at the route that renders the same section as a page, and open it in a new tab. `DrawerNotchPill` forwards all button props, so use `onClick`:
487
392
 
488
- ### DrawerTitle
393
+ ```tsx
394
+ // `href` is the full-page route that renders the SAME section/form as the drawer.
395
+ // For an edit drawer, include the record id so the page opens pre-filled:
396
+ // const href = isEdit ? `/contacts/${contact.id}/edit` : "/contacts/new";
489
397
 
490
- | Prop | Type | Default | Description |
491
- |------|------|---------|-------------|
492
- | `className` | `string` | - | Additional CSS classes |
493
-
494
- ### DrawerDescription
398
+ <DrawerNotchPill
399
+ color="Yellow"
400
+ onClick={() => window.open(href, "_blank", "noopener")}
401
+ >
402
+ Open in new tab
403
+ <i className="ri-arrow-right-up-line text-[12px]" />
404
+ </DrawerNotchPill>
405
+ ```
495
406
 
496
- | Prop | Type | Default | Description |
497
- |------|------|---------|-------------|
498
- | `className` | `string` | - | Additional CSS classes |
407
+ Or, if you prefer a real anchor (better for middle-click / "copy link"), wrap a styled link instead of the pill — the pill is just a button, so for true link semantics render your own `<a target="_blank">` with the same classes, or in Next.js:
499
408
 
500
- ### DrawerClose
409
+ ```tsx
410
+ import Link from "next/link";
501
411
 
502
- | Prop | Type | Default | Description |
503
- |------|------|---------|-------------|
504
- | `asChild` | `boolean` | `false` | Render close trigger as child |
412
+ <Link href={href} target="_blank" rel="noopener" className="contents">
413
+ <DrawerNotchPill color="Yellow" tabIndex={-1}>
414
+ Open in new tab
415
+ <i className="ri-arrow-right-up-line text-[12px]" />
416
+ </DrawerNotchPill>
417
+ </Link>
418
+ ```
505
419
 
506
- ## TypeScript
420
+ **The contract:** the drawer and the full page must render the **same section component** with the **same data**. Build the form body (the `SectionBlock` groups + fields) as a shared component, drop it into the `DrawerContent` for the inline view and into a normal page route for the full-page view. That guarantees "open in new tab" shows an identical form — just full-page instead of in the drawer.
507
421
 
508
- ### Full Type Definitions
422
+ > The **"Open in the app"** pill (App notch) follows the same idea but targets a different surface — e.g. a deep link into a separate application — rather than a full-page route in the same app.
509
423
 
510
- ```typescript
511
- import { Drawer as DrawerPrimitive } from 'vaul'
424
+ ## Direction — how bottom, right, and left differ
512
425
 
513
- // Root component
514
- interface DrawerProps extends React.ComponentProps<typeof DrawerPrimitive.Root> {
515
- shouldScaleBackground?: boolean
516
- open?: boolean
517
- onOpenChange?: (open: boolean) => void
518
- defaultOpen?: boolean
519
- modal?: boolean
520
- children: React.ReactNode
521
- }
426
+ There is **no automatic per-direction styling**. The anchor is set on the root with `direction`, and the *look* (where it sits, which corners round, whether there's a drag handle or a frame) comes from props you pass to `DrawerContent`. Here is exactly what changes per direction and the copy-paste recipe for each.
522
427
 
523
- export const Drawer: React.FC<DrawerProps>
428
+ | Aspect | Bottom (default) | Right | Left |
429
+ |---|---|---|---|
430
+ | Root prop | _(none)_ | `direction="right"` | `direction="left"` |
431
+ | Sits at | full width, pinned to bottom | floating panel on the right | floating panel on the left |
432
+ | Drag handle | **shown** (`showHandle` default `true`) | hidden (`showHandle={false}`) | hidden (`showHandle={false}`) |
433
+ | Frame / tray | usually off (`framed={false}`) for clean sheet | on (default) for the dark tray | on (default) |
434
+ | Rounded corners | top corners only | all/left corners | top-left + bottom corners |
435
+ | Notch side | top-left (`notchSide="left"`) | top-left | mirror to `notchSide="right"` |
436
+ | Best for | mobile sheets, action sheets, comments | create/edit forms, filters, detail panels | RTL panels, side navigation |
524
437
 
525
- // Compound components
526
- export const DrawerTrigger: React.ForwardRefExoticComponent<
527
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Trigger>
528
- >
438
+ ### Bottom (default)
529
439
 
530
- export const DrawerContent: React.ForwardRefExoticComponent<
531
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
532
- >
440
+ No `direction` prop. Keep the drag handle; drop the frame for a clean sheet.
533
441
 
534
- export const DrawerHeader: React.FC<React.HTMLAttributes<HTMLDivElement>>
535
- export const DrawerFooter: React.FC<React.HTMLAttributes<HTMLDivElement>>
536
- export const DrawerTitle: React.ForwardRefExoticComponent<
537
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
538
- >
539
- export const DrawerDescription: React.ForwardRefExoticComponent<
540
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
541
- >
542
- export const DrawerClose: React.ForwardRefExoticComponent<
543
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Close>
544
- >
442
+ ```tsx
443
+ <Drawer>
444
+ <DrawerTrigger asChild>
445
+ <Button variant="PrimeStyle">Open bottom sheet</Button>
446
+ </DrawerTrigger>
447
+ <DrawerContent framed={false}>
448
+ {/* drag handle appears automatically */}
449
+ <DrawerHeader>
450
+ <DrawerTitle>Comments</DrawerTitle>
451
+ </DrawerHeader>
452
+ {/* ... */}
453
+ </DrawerContent>
454
+ </Drawer>
545
455
  ```
546
456
 
547
- ## Common Patterns
548
-
549
- ### useDrawer Hook
550
-
551
- ```typescript
552
- import { useState } from 'react'
553
-
554
- function useDrawer() {
555
- const [open, setOpen] = useState(false)
457
+ **Half-screen / snap points** — the bottom drawer can open to half height then snap to full. Drive it with Vaul's `snapPoints` on the root:
556
458
 
557
- const openDrawer = () => setOpen(true)
558
- const closeDrawer = () => setOpen(false)
559
- const toggleDrawer = () => setOpen(!open)
560
-
561
- return {
562
- open,
563
- setOpen,
564
- openDrawer,
565
- closeDrawer,
566
- toggleDrawer,
567
- }
568
- }
569
-
570
- // Usage
571
- function App() {
572
- const drawer = useDrawer()
573
-
574
- return (
575
- <>
576
- <button onClick={drawer.openDrawer}>Open</button>
577
- <Drawer open={drawer.open} onOpenChange={drawer.setOpen}>
578
- <DrawerContent>Content</DrawerContent>
579
- </Drawer>
580
- </>
581
- )
582
- }
459
+ ```tsx
460
+ <Drawer snapPoints={[0.5, 1]} fadeFromIndex={1}>
461
+ <DrawerTrigger asChild>
462
+ <Button variant="PrimeStyle">Open half-screen</Button>
463
+ </DrawerTrigger>
464
+ <DrawerContent framed={false} wrapperClassName="h-full max-h-[97vh]">
465
+ {/* ... */}
466
+ </DrawerContent>
467
+ </Drawer>
583
468
  ```
584
469
 
585
- ## Gesture Features
470
+ ### Right
471
+
472
+ A floating panel anchored to the right edge — the canonical home for create/edit forms and filter panels. Hide the handle, anchor with `wrapperClassName`, round the corners.
473
+
474
+ ```tsx
475
+ <Drawer direction="right">
476
+ <DrawerTrigger asChild>
477
+ <Button variant="PrimeStyle">Open right drawer</Button>
478
+ </DrawerTrigger>
479
+ <DrawerContent
480
+ showHandle={false}
481
+ wrapperClassName="top-2 right-2 bottom-2 left-auto mt-0 h-auto w-[420px] max-w-[calc(100vw-16px)]"
482
+ trayClassName="rounded-[16px]"
483
+ className="rounded-[10px]"
484
+ >
485
+ <DrawerHeader>
486
+ <DrawerTitle>Filters</DrawerTitle>
487
+ <DrawerDescription>Status, owner, tags, priority…</DrawerDescription>
488
+ </DrawerHeader>
489
+ <div className="px-4 pb-4 space-y-3 flex-1 overflow-y-auto">
490
+ {/* ...filter rows... */}
491
+ </div>
492
+ <DrawerFooter>
493
+ <DrawerClose asChild>
494
+ <Button variant="PrimeStyle">Apply</Button>
495
+ </DrawerClose>
496
+ <DrawerClose asChild>
497
+ <Button variant="BorderStyle">Reset</Button>
498
+ </DrawerClose>
499
+ </DrawerFooter>
500
+ </DrawerContent>
501
+ </Drawer>
502
+ ```
586
503
 
587
- ### Drag to Close
588
- - Users can drag the drawer down to close it
589
- - Smooth spring animation follows finger/pointer
590
- - Automatic threshold detection for close vs. snap-back
504
+ > Widen `w-[420px]` to `w-[1046px]` (as in the form example) for a roomy two-column form. Always pair a fixed width with `max-w-[calc(100vw-16px)]` so it never overflows on small screens.
505
+
506
+ ### Left (RTL / navigation)
507
+
508
+ Mirror of the right recipe: `direction="left"`, anchor to the left edge, and if you use a notch, set `notchSide="right"` so the tab mirrors correctly.
509
+
510
+ ```tsx
511
+ <Drawer direction="left">
512
+ <DrawerTrigger asChild>
513
+ <Button variant="PrimeStyle">Open left drawer</Button>
514
+ </DrawerTrigger>
515
+ <DrawerContent
516
+ showHandle={false}
517
+ notchSide="right"
518
+ wrapperClassName="top-2 left-2 bottom-2 right-auto mt-0 h-auto w-[420px] max-w-[calc(100vw-16px)]"
519
+ className="rounded-tl-[16px] rounded-b-[16px]"
520
+ notch={
521
+ <DrawerNotch>
522
+ <DrawerClose asChild>
523
+ <DrawerNotchClose />
524
+ </DrawerClose>
525
+ <DrawerNotchPill color="Yellow">
526
+ Open in new tab
527
+ <i className="ri-arrow-right-up-line text-[12px]" />
528
+ </DrawerNotchPill>
529
+ </DrawerNotch>
530
+ }
531
+ >
532
+ <DrawerHeader>
533
+ <DrawerHeaderTitle>
534
+ <DrawerBadge color="Purple">Menu</DrawerBadge>
535
+ <DrawerTitle>Navigation</DrawerTitle>
536
+ </DrawerHeaderTitle>
537
+ </DrawerHeader>
538
+ {/* ...nav items... */}
539
+ </DrawerContent>
540
+ </Drawer>
541
+ ```
591
542
 
592
- ### Snap Points (Advanced)
593
- ```typescript
594
- // Vaul supports snap points for partial drawer heights
595
- <Drawer snapPoints={[0.5, 1]} activeSnapPoint={1}>
596
- <DrawerContent>Content</DrawerContent>
543
+ ### Full-screen
544
+
545
+ Anchor a bottom drawer to every edge via `wrapperClassName`.
546
+
547
+ ```tsx
548
+ <Drawer>
549
+ <DrawerTrigger asChild>
550
+ <Button variant="PrimeStyle">Open full-screen</Button>
551
+ </DrawerTrigger>
552
+ <DrawerContent
553
+ framed={false}
554
+ wrapperClassName="inset-x-0 top-0 bottom-0 m-0 h-screen w-screen max-w-none"
555
+ >
556
+ {/* immersive editor / media viewer */}
557
+ </DrawerContent>
597
558
  </Drawer>
598
559
  ```
599
560
 
600
- ## Testing
561
+ ## API Reference
601
562
 
602
- ### Unit Test Example
563
+ ### `Drawer` (root)
603
564
 
604
- ```typescript
605
- import { render, screen, fireEvent } from '@testing-library/react'
606
- import { Drawer, DrawerTrigger, DrawerContent, DrawerTitle } from '@torch-ui/components'
565
+ Wraps Vaul's `Drawer.Root` and defaults `shouldScaleBackground` to `true`. Accepts all [Vaul Root props](https://vaul.emilkowal.ski/api).
607
566
 
608
- describe('Drawer', () => {
609
- it('opens when trigger is clicked', () => {
610
- render(
611
- <Drawer>
612
- <DrawerTrigger>Open</DrawerTrigger>
613
- <DrawerContent>
614
- <DrawerTitle>Test Drawer</DrawerTitle>
615
- </DrawerContent>
616
- </Drawer>
617
- )
618
-
619
- fireEvent.click(screen.getByText('Open'))
620
- expect(screen.getByText('Test Drawer')).toBeInTheDocument()
621
- })
622
-
623
- it('closes on close button click', () => {
624
- render(
625
- <Drawer defaultOpen>
626
- <DrawerContent>
627
- <DrawerTitle>Test</DrawerTitle>
628
- <DrawerClose>Close</DrawerClose>
629
- </DrawerContent>
630
- </Drawer>
631
- )
632
-
633
- fireEvent.click(screen.getByText('Close'))
634
- expect(screen.queryByText('Test')).not.toBeInTheDocument()
635
- })
636
-
637
- it('handles controlled state', () => {
638
- const handleChange = jest.fn()
639
-
640
- render(
641
- <Drawer onOpenChange={handleChange}>
642
- <DrawerTrigger>Open</DrawerTrigger>
643
- <DrawerContent>Content</DrawerContent>
644
- </Drawer>
645
- )
646
-
647
- fireEvent.click(screen.getByText('Open'))
648
- expect(handleChange).toHaveBeenCalledWith(true)
649
- })
650
- })
651
- ```
567
+ | Prop | Type | Default | Description |
568
+ |---|---|---|---|
569
+ | `direction` | `"bottom" \| "top" \| "left" \| "right"` | `"bottom"` | Edge the drawer slides in from. |
570
+ | `shouldScaleBackground` | `boolean` | `true` | Scales/pushes the page back when open (the iOS sheet effect). |
571
+ | `open` | `boolean` | — | Controlled open state. |
572
+ | `onOpenChange` | `(open: boolean) => void` | — | Fires when open state changes. |
573
+ | `snapPoints` | `(number \| string)[]` | — | Snap stops, e.g. `[0.5, 1]` for half then full. |
574
+ | `fadeFromIndex` | `number` | — | Snap index from which the overlay starts to fade. |
575
+ | `nested` | `boolean` | — | Prefer the `DrawerNested` component instead. |
652
576
 
653
- ## Accessibility
654
-
655
- - **Keyboard Support**:
656
- - Escape: Close drawer
657
- - Tab: Navigate focusable elements
658
- - Shift+Tab: Navigate backwards
659
- - **ARIA Attributes**:
660
- - `role="dialog"` automatically applied
661
- - `aria-labelledby` links to title
662
- - `aria-describedby` links to description
663
- - **Focus Management**:
664
- - Focus trapped within drawer when open
665
- - Focus returned to trigger on close
666
- - **Touch/Pointer**: Gesture-based closing with drag down
667
-
668
- ### Accessibility Best Practices
669
-
670
- ```typescript
671
- // Always provide a title
672
- <DrawerContent>
673
- <DrawerTitle>Drawer Title</DrawerTitle> {/* Required */}
674
- <DrawerDescription>Description</DrawerDescription>
675
- </DrawerContent>
577
+ ### `DrawerContent`
676
578
 
677
- // Ensure close affordance
678
- <DrawerClose asChild>
679
- <Button aria-label="Close drawer">Close</Button>
680
- </DrawerClose>
681
- ```
579
+ The panel. This is where direction-specific styling is applied.
682
580
 
683
- ## Performance
684
-
685
- | Metric | Value |
686
- |--------|-------|
687
- | Bundle size (minified) | ~6kb |
688
- | Bundle size (gzipped) | ~2.5kb |
689
- | Dependencies | vaul (~8kb) |
690
- | Animation | Hardware accelerated |
691
- | Gesture latency | <16ms (60fps) |
692
- | Tree-shakeable | |
693
-
694
- ### Performance Tips
695
-
696
- 1. **Lazy load content**: Render content only when open
697
- ```typescript
698
- {open && <DrawerContent>{/* Heavy content */}</DrawerContent>}
699
- ```
700
-
701
- 2. **Use CSS transforms**: Already optimized by Vaul
702
- 3. **Avoid heavy renders**: Memoize drawer content
703
- ```typescript
704
- const DrawerBody = useMemo(() => <HeavyComponent />, [deps])
705
- ```
706
-
707
- ## Migration from Dialog
708
-
709
- ```diff
710
- - import { Dialog, DialogTrigger, DialogContent } from '@torch-ui/components'
711
- + import { Drawer, DrawerTrigger, DrawerContent } from '@torch-ui/components'
712
-
713
- - <Dialog>
714
- - <DialogTrigger>Open</DialogTrigger>
715
- - <DialogContent>Content</DialogContent>
716
- - </Dialog>
717
-
718
- + <Drawer>
719
- + <DrawerTrigger>Open</DrawerTrigger>
720
- + <DrawerContent>Content</DrawerContent>
721
- + </Drawer>
722
- ```
581
+ | Prop | Type | Default | Description |
582
+ |---|---|---|---|
583
+ | `framed` | `boolean` | `true` | Show the dark "tray" frame (border + inset shadow) around the panel. Set `false` for clean bottom sheets. |
584
+ | `showHandle` | `boolean` | `true` | Show the centered drag handle. Auto-hidden when a `notch` is present. Set `false` for side drawers. |
585
+ | `notch` | `ReactNode` | | A `DrawerNotch` tab rendered on the top edge. |
586
+ | `notchSide` | `"left" \| "right"` | `"left"` | Which side the notch attaches to (and which corner stays square). Use `"right"` for left-anchored drawers. |
587
+ | `wrapperClassName` | `string` | | Classes on the outer positioned element — this is how you anchor/size the panel per direction. |
588
+ | `trayClassName` | `string` | | Classes on the dark tray frame (e.g. corner rounding). |
589
+ | `className` | `string` | | Classes on the inner light content surface. |
590
+ | `...props` | Vaul `Content` props | — | Forwarded to `Drawer.Content`. |
591
+
592
+ ### Sub-components
593
+
594
+ | Component | Description |
595
+ |---|---|
596
+ | `DrawerTrigger` | Opens the drawer. Use `asChild` to wrap your own button. |
597
+ | `DrawerClose` | Closes the drawer. Use `asChild` to wrap any element. |
598
+ | `DrawerNested` | A nested/stacked drawer. Must be rendered inside an open `DrawerContent`. |
599
+ | `DrawerHeader` | Header row (space-between layout) holding title + actions. |
600
+ | `DrawerHeaderTitle` | Dark rounded pill that groups a `DrawerBadge` + `DrawerTitle`. |
601
+ | `DrawerHeaderActions` | Dark rounded pill (right-aligned) for header buttons. |
602
+ | `DrawerTitle` | Accessible title (maps to Vaul `Drawer.Title`). |
603
+ | `DrawerDescription` | Muted supporting text (maps to Vaul `Drawer.Description`). |
604
+ | `DrawerBadge` | Small uppercase status pill. `color`: `Blue \| Green \| Red \| Yellow \| Purple \| Gray` (default `Blue`). |
605
+ | `DrawerFooter` | Bottom action area (`mt-auto`, stacked). |
606
+ | `DrawerNotch` | The top-edge tab container. `side`: `"left" \| "right"`. |
607
+ | `DrawerNotchClose` | Round close button for inside a notch. |
608
+ | `DrawerNotchPill` | Pill button for inside a notch. `color`: `Yellow \| Blue \| Gray` (default `Yellow`). Styled `<button>` only — wire navigation yourself via `onClick` (see ["What 'Open in new tab' does"](#what-open-in-new-tab-does)). |
609
+ | `DrawerNotchDivider` | Thin vertical divider between notch items. |
610
+ | `DrawerNotchApp` | App identity block (icon + name) for inside a notch. Props: `icon?`, `name`. |
611
+ | `DrawerPortal` / `DrawerOverlay` | Lower-level primitives; rarely used directly. |
723
612
 
724
613
  ## Best Practices
725
614
 
726
- 1. **Use on mobile/tablet**: Drawer is better than dialog on touch devices
727
- ```typescript
728
- const isMobile = useMediaQuery('(max-width: 768px)')
729
- return isMobile ? <Drawer /> : <Dialog />
730
- ```
731
-
732
- 2. **Keep height reasonable**: Don't make drawers full-screen height
733
- ```typescript
734
- <DrawerContent className="max-h-[80vh]">
735
- ```
736
-
737
- 3. **Provide visual drag handle**: Built-in drag indicator included
738
- ```typescript
739
- // Automatic drag handle bar rendered at top
740
- ```
741
-
742
- 4. **Use for actions and forms**: Perfect for quick interactions
743
- ```typescript
744
- // Good: Share sheet, filters, settings
745
- // Avoid: Long articles, complex wizards
746
- ```
747
-
748
- 5. **Handle background scroll**: Automatically prevents scroll
749
- ```typescript
750
- // Background scroll blocked when drawer is open
751
- ```
752
-
753
- 6. **Test gesture interactions**: Ensure drag-to-close works smoothly
754
- ```typescript
755
- // Test on actual touch devices
756
- ```
757
-
758
- 7. **Provide close button**: Don't rely solely on gesture
759
- ```typescript
760
- <DrawerClose asChild>
761
- <Button>Done</Button>
762
- </DrawerClose>
763
- ```
615
+ - **Pick the anchor by job.** Bottom for mobile sheets / action sheets / comments; right for create-edit forms and filter/detail panels; left for RTL panels and side navigation.
616
+ - **Side drawers: hide the handle.** Always pass `showHandle={false}` on left/right drawers — the drag handle only makes sense on a bottom sheet.
617
+ - **Always cap the width.** Pair any fixed `w-[…]` with `max-w-[calc(100vw-16px)]` so the panel never overflows narrow viewports.
618
+ - **One drawer for create *and* edit.** Branch on a `mode` prop for the badge/title and feed `defaultValue`/`value` from the record. Don't build two near-identical drawers.
619
+ - **Group form fields with `SectionBlock`.** Use a colored `SectionBlock` per logical group (Identity, Address, …) and the `FieldRow` / `RowDivider` helpers for consistent label/field rows.
620
+ - **Make the body scroll, not the panel.** Put `flex-1 overflow-y-auto` on the body wrapper so the header/footer stay pinned while content scrolls.
621
+ - **`DrawerNested` only inside an open drawer.** It will not work as a standalone trigger.
764
622
 
765
623
  ## Related Components
766
624
 
767
- - [Dialog](./dialog.md) - Desktop-oriented modal alternative
768
- - [AlertDialog](./alert-dialog.md) - For confirmations
769
- - [Popover](./popover.md) - Smaller contextual overlays
625
+ - **[Dialog](./dialog.md)** centered modal for short confirmations and compact forms.
626
+ - **[AlertDialog](./alert-dialog.md)** blocking confirmation for destructive actions.
627
+ - **[SectionBlock](./section-block.md)** the grouping container used for the form body above.
628
+ - **[InputField](./input-field.md)** — the input used in the form rows.