@ceed/ads 1.20.0 → 1.20.1

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.
@@ -2,8 +2,19 @@
2
2
 
3
3
  ## Introduction
4
4
 
5
+ InsetDrawer is a slide-out panel that appears from any edge of the screen, providing a secondary space for navigation, filters, settings, or detailed content without leaving the current page. Unlike Modal which overlays the entire screen, InsetDrawer slides in from an edge and can optionally allow interaction with the main content behind it. It's commonly used for mobile navigation menus, filter panels, and configuration sidebars.
6
+
5
7
  ```tsx
6
- <InsetDrawer {...args}>
8
+ <Box sx={{
9
+ display: 'flex',
10
+ flexDirection: 'column',
11
+ gap: 2
12
+ }}>
13
+ <Button variant="outlined" color="neutral" startDecorator={<FilterListIcon />} onClick={() => setOpen(true)}>
14
+ Open Drawer
15
+ </Button>
16
+
17
+ <InsetDrawer {...args} open={open} onClose={() => setOpen(false)}>
7
18
  <Sheet sx={{
8
19
  borderRadius: 'md',
9
20
  p: 2,
@@ -116,18 +127,1154 @@ Booking options
116
127
  <Button variant="outlined" color="neutral">
117
128
  Clear
118
129
  </Button>
119
- <Button>Show 165 properties</Button>
130
+ <Button onClick={() => setOpen(false)}>Show 165 properties</Button>
120
131
  </Stack>
121
132
  </Sheet>
122
133
  </InsetDrawer>
134
+ </Box>
123
135
  ```
124
136
 
125
137
  | Field | Description | Default |
126
138
  | ---------------------------- | ----------- | ------- |
127
139
  | Controls resolved at runtime | — | — |
128
140
 
141
+ > ⚠️ **Usage Warning** ⚠️
142
+ >
143
+ > Consider these factors before using InsetDrawer:
144
+ >
145
+ > - **Mobile-first design**: InsetDrawer works best on mobile; consider persistent sidebars on desktop
146
+ > - **Content hierarchy**: Don't hide critical content in drawers that users need frequently
147
+ > - **Anchor position**: Use `left` for navigation, `right` for details/filters, `bottom` for actions
148
+ > - **Accessibility**: Ensure proper focus management and keyboard navigation
149
+
129
150
  ## Usage
130
151
 
131
152
  ```tsx
132
- import { InsetDrawer } from '@ceed/ads';
153
+ import { InsetDrawer, DialogTitle, DialogContent, ModalClose, Sheet } from '@ceed/ads';
154
+
155
+ function FilterDrawer() {
156
+ const [open, setOpen] = useState(false);
157
+
158
+ return (
159
+ <>
160
+ <Button onClick={() => setOpen(true)}>Open Filters</Button>
161
+ <InsetDrawer open={open} onClose={() => setOpen(false)}>
162
+ <Sheet sx={{ p: 2, height: '100%' }}>
163
+ <DialogTitle>Filters</DialogTitle>
164
+ <ModalClose />
165
+ <DialogContent>
166
+ {/* Filter content */}
167
+ </DialogContent>
168
+ </Sheet>
169
+ </InsetDrawer>
170
+ </>
171
+ );
172
+ }
173
+ ```
174
+
175
+ ## Examples
176
+
177
+ ### Playground
178
+
179
+ Interactive example with controls for anchor position and size.
180
+
181
+ ```tsx
182
+ <Box sx={{
183
+ display: 'flex',
184
+ flexDirection: 'column',
185
+ gap: 2
186
+ }}>
187
+ <Button variant="outlined" color="neutral" startDecorator={<FilterListIcon />} onClick={() => setOpen(true)}>
188
+ Open Drawer
189
+ </Button>
190
+
191
+ <InsetDrawer {...args} open={open} onClose={() => setOpen(false)}>
192
+ <Sheet sx={{
193
+ borderRadius: 'md',
194
+ p: 2,
195
+ display: 'flex',
196
+ flexDirection: 'column',
197
+ gap: 2,
198
+ height: '100%',
199
+ overflow: 'auto'
200
+ }}>
201
+ <DialogTitle>Filters</DialogTitle>
202
+ <ModalClose />
203
+ <Divider sx={{
204
+ mt: 'auto'
205
+ }} />
206
+ <DialogContent>
207
+ <FormControl>
208
+ <FormLabel sx={{
209
+ typography: 'title-md',
210
+ fontWeight: 'bold'
211
+ }}>Property type</FormLabel>
212
+ <RadioGroup>
213
+ <Box sx={{
214
+ display: 'grid',
215
+ gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
216
+ gap: 1.5
217
+ }}>
218
+ {[{
219
+ name: 'House',
220
+ icon: <HomeRoundedIcon />
221
+ }, {
222
+ name: 'Apartment',
223
+ icon: <ApartmentRoundedIcon />
224
+ }, {
225
+ name: 'Guesthouse',
226
+ icon: <MeetingRoomRoundedIcon />
227
+ }, {
228
+ name: 'Hotel',
229
+ icon: <HotelRoundedIcon />
230
+ }].map(item => <Card key={item.name} sx={{
231
+ boxShadow: 'none',
232
+ '&:hover': {
233
+ bgcolor: 'background.level1'
234
+ }
235
+ }}>
236
+ <CardContent>
237
+ {item.icon}
238
+ <Typography level="title-md">{item.name}</Typography>
239
+ </CardContent>
240
+ <Radio disableIcon overlay variant="outlined" color="neutral" value={item.name} sx={{
241
+ mt: -2
242
+ }} slotProps={{
243
+ action: {
244
+ sx: {
245
+ '&:hover': {
246
+ bgcolor: 'transparent'
247
+ }
248
+ }
249
+ }
250
+ }} />
251
+ </Card>)}
252
+ </Box>
253
+ </RadioGroup>
254
+ </FormControl>
255
+
256
+ <Typography level="title-md" fontWeight="bold" sx={{
257
+ mt: 2
258
+ }}>
259
+ Booking options
260
+ </Typography>
261
+ <FormControl orientation="horizontal">
262
+ <Box sx={{
263
+ flex: 1,
264
+ pr: 1
265
+ }}>
266
+ <FormLabel sx={{
267
+ typography: 'title-sm'
268
+ }}>Instant booking</FormLabel>
269
+ <FormHelperText sx={{
270
+ typography: 'body-sm'
271
+ }}>
272
+ Listings that you can book without waiting for host approval.
273
+ </FormHelperText>
274
+ </Box>
275
+ <Switch />
276
+ </FormControl>
277
+
278
+ <FormControl orientation="horizontal">
279
+ <Box sx={{
280
+ flex: 1,
281
+ mt: 1,
282
+ mr: 1
283
+ }}>
284
+ <FormLabel sx={{
285
+ typography: 'title-sm'
286
+ }}>Self check-in</FormLabel>
287
+ <FormHelperText sx={{
288
+ typography: 'body-sm'
289
+ }}>
290
+ Easy access to the property when you arrive.
291
+ </FormHelperText>
292
+ </Box>
293
+ <Switch />
294
+ </FormControl>
295
+ </DialogContent>
296
+
297
+ <Divider sx={{
298
+ mt: 'auto'
299
+ }} />
300
+ <Stack direction="row" justifyContent="space-between" useFlexGap spacing={1}>
301
+ <Button variant="outlined" color="neutral">
302
+ Clear
303
+ </Button>
304
+ <Button onClick={() => setOpen(false)}>Show 165 properties</Button>
305
+ </Stack>
306
+ </Sheet>
307
+ </InsetDrawer>
308
+ </Box>
309
+ ```
310
+
311
+ ### Anchors
312
+
313
+ Different anchor positions: left, right, top, bottom.
314
+
315
+ ```tsx
316
+ <Box sx={{
317
+ display: 'flex',
318
+ flexDirection: 'column',
319
+ gap: 2
320
+ }}>
321
+ <Stack direction="row" spacing={2}>
322
+ <Button variant="outlined" onClick={() => setAnchor('left')}>
323
+ Left
324
+ </Button>
325
+ <Button variant="outlined" onClick={() => setAnchor('right')}>
326
+ Right
327
+ </Button>
328
+ <Button variant="outlined" onClick={() => setAnchor('top')}>
329
+ Top
330
+ </Button>
331
+ <Button variant="outlined" onClick={() => setAnchor('bottom')}>
332
+ Bottom
333
+ </Button>
334
+ </Stack>
335
+
336
+ {(['left', 'right', 'top', 'bottom'] as const).map(anchorValue => <InsetDrawer key={anchorValue} anchor={anchorValue} open={anchor === anchorValue} onClose={() => setAnchor(null)}>
337
+ <Sheet sx={{
338
+ borderRadius: 'md',
339
+ p: 2,
340
+ display: 'flex',
341
+ flexDirection: 'column',
342
+ gap: 2,
343
+ height: '100%'
344
+ }}>
345
+ <DialogTitle>Drawer from {anchorValue}</DialogTitle>
346
+ <ModalClose />
347
+ <Divider />
348
+ <DialogContent>
349
+ <Typography>This drawer slides in from the {anchorValue} side of the screen.</Typography>
350
+ </DialogContent>
351
+ <Divider sx={{
352
+ mt: 'auto'
353
+ }} />
354
+ <Button onClick={() => setAnchor(null)}>Close</Button>
355
+ </Sheet>
356
+ </InsetDrawer>)}
357
+ </Box>
358
+ ```
359
+
360
+ ### Sizes
361
+
362
+ Available size options: small, medium, large.
363
+
364
+ ```tsx
365
+ <Box sx={{
366
+ display: 'flex',
367
+ flexDirection: 'column',
368
+ gap: 2
369
+ }}>
370
+ <Stack direction="row" spacing={2}>
371
+ <Button variant="outlined" size="sm" onClick={() => setSize('sm')}>
372
+ Small
373
+ </Button>
374
+ <Button variant="outlined" onClick={() => setSize('md')}>
375
+ Medium
376
+ </Button>
377
+ <Button variant="outlined" size="lg" onClick={() => setSize('lg')}>
378
+ Large
379
+ </Button>
380
+ </Stack>
381
+
382
+ {(['sm', 'md', 'lg'] as const).map(sizeValue => <InsetDrawer key={sizeValue} size={sizeValue} open={size === sizeValue} onClose={() => setSize(null)}>
383
+ <Sheet sx={{
384
+ borderRadius: 'md',
385
+ p: 2,
386
+ display: 'flex',
387
+ flexDirection: 'column',
388
+ gap: 2,
389
+ height: '100%'
390
+ }}>
391
+ <DialogTitle>Size: {sizeValue}</DialogTitle>
392
+ <ModalClose />
393
+ <Divider />
394
+ <DialogContent>
395
+ <Typography>This is a {sizeValue} sized drawer.</Typography>
396
+ </DialogContent>
397
+ <Divider sx={{
398
+ mt: 'auto'
399
+ }} />
400
+ <Button onClick={() => setSize(null)}>Close</Button>
401
+ </Sheet>
402
+ </InsetDrawer>)}
403
+ </Box>
404
+ ```
405
+
406
+ ### Navigation Menu
407
+
408
+ A mobile navigation menu using InsetDrawer.
409
+
410
+ ```tsx
411
+ <Box>
412
+ <IconButton variant="outlined" color="neutral" onClick={() => setOpen(true)} aria-label="Open navigation menu">
413
+ <MenuIcon />
414
+ </IconButton>
415
+
416
+ <InsetDrawer open={open} onClose={() => setOpen(false)} anchor="left" size="sm">
417
+ <Sheet sx={{
418
+ p: 2,
419
+ display: 'flex',
420
+ flexDirection: 'column',
421
+ height: '100%'
422
+ }}>
423
+ <DialogTitle>Navigation</DialogTitle>
424
+ <ModalClose />
425
+ <Divider sx={{
426
+ my: 2
427
+ }} />
428
+ <Stack spacing={1}>
429
+ {menuItems.map(item => <Button key={item.label} variant="plain" color="neutral" startDecorator={item.icon} sx={{
430
+ justifyContent: 'flex-start'
431
+ }} onClick={() => setOpen(false)}>
432
+ {item.label}
433
+ </Button>)}
434
+ </Stack>
435
+ </Sheet>
436
+ </InsetDrawer>
437
+ </Box>
438
+ ```
439
+
440
+ ### Filter Panel
441
+
442
+ A filter panel with form controls sliding from the right.
443
+
444
+ ```tsx
445
+ <Box>
446
+ <Button variant="outlined" color="neutral" startDecorator={<FilterListIcon />} onClick={() => setOpen(true)}>
447
+ Filters
448
+ </Button>
449
+
450
+ <InsetDrawer open={open} onClose={() => setOpen(false)} anchor="right">
451
+ <Sheet sx={{
452
+ borderRadius: 'md',
453
+ p: 2,
454
+ display: 'flex',
455
+ flexDirection: 'column',
456
+ gap: 2,
457
+ height: '100%',
458
+ overflow: 'auto'
459
+ }}>
460
+ <DialogTitle>Filters</DialogTitle>
461
+ <ModalClose />
462
+ <Divider />
463
+ <DialogContent>
464
+ <FormControl>
465
+ <FormLabel sx={{
466
+ typography: 'title-md',
467
+ fontWeight: 'bold'
468
+ }}>Property type</FormLabel>
469
+ <RadioGroup value={filters.propertyType} onChange={e => setFilters(prev => ({
470
+ ...prev,
471
+ propertyType: e.target.value
472
+ }))}>
473
+ <Box sx={{
474
+ display: 'grid',
475
+ gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
476
+ gap: 1.5
477
+ }}>
478
+ {[{
479
+ name: 'House',
480
+ icon: <HomeRoundedIcon />
481
+ }, {
482
+ name: 'Apartment',
483
+ icon: <ApartmentRoundedIcon />
484
+ }, {
485
+ name: 'Guesthouse',
486
+ icon: <MeetingRoomRoundedIcon />
487
+ }, {
488
+ name: 'Hotel',
489
+ icon: <HotelRoundedIcon />
490
+ }].map(item => <Card key={item.name} sx={{
491
+ boxShadow: 'none',
492
+ '&:hover': {
493
+ bgcolor: 'background.level1'
494
+ }
495
+ }}>
496
+ <CardContent>
497
+ {item.icon}
498
+ <Typography level="title-md">{item.name}</Typography>
499
+ </CardContent>
500
+ <Radio disableIcon overlay variant="outlined" color="neutral" value={item.name} sx={{
501
+ mt: -2
502
+ }} slotProps={{
503
+ action: {
504
+ sx: {
505
+ '&:hover': {
506
+ bgcolor: 'transparent'
507
+ }
508
+ }
509
+ }
510
+ }} />
511
+ </Card>)}
512
+ </Box>
513
+ </RadioGroup>
514
+ </FormControl>
515
+
516
+ <Typography level="title-md" fontWeight="bold" sx={{
517
+ mt: 2
518
+ }}>
519
+ Booking options
520
+ </Typography>
521
+ <FormControl orientation="horizontal">
522
+ <Box sx={{
523
+ flex: 1,
524
+ pr: 1
525
+ }}>
526
+ <FormLabel sx={{
527
+ typography: 'title-sm'
528
+ }}>Instant booking</FormLabel>
529
+ <FormHelperText sx={{
530
+ typography: 'body-sm'
531
+ }}>
532
+ Listings that you can book without waiting for host approval.
533
+ </FormHelperText>
534
+ </Box>
535
+ <Switch checked={filters.instantBooking} onChange={e => setFilters(prev => ({
536
+ ...prev,
537
+ instantBooking: e.target.checked
538
+ }))} />
539
+ </FormControl>
540
+
541
+ <FormControl orientation="horizontal">
542
+ <Box sx={{
543
+ flex: 1,
544
+ mt: 1,
545
+ mr: 1
546
+ }}>
547
+ <FormLabel sx={{
548
+ typography: 'title-sm'
549
+ }}>Self check-in</FormLabel>
550
+ <FormHelperText sx={{
551
+ typography: 'body-sm'
552
+ }}>
553
+ Easy access to the property when you arrive.
554
+ </FormHelperText>
555
+ </Box>
556
+ <Switch checked={filters.selfCheckIn} onChange={e => setFilters(prev => ({
557
+ ...prev,
558
+ selfCheckIn: e.target.checked
559
+ }))} />
560
+ </FormControl>
561
+ </DialogContent>
562
+
563
+ <Divider sx={{
564
+ mt: 'auto'
565
+ }} />
566
+ <Stack direction="row" justifyContent="space-between" useFlexGap spacing={1}>
567
+ <Button variant="outlined" color="neutral" onClick={() => setFilters({
568
+ propertyType: '',
569
+ instantBooking: false,
570
+ selfCheckIn: false
571
+ })}>
572
+ Clear
573
+ </Button>
574
+ <Button onClick={() => setOpen(false)}>Show 165 properties</Button>
575
+ </Stack>
576
+ </Sheet>
577
+ </InsetDrawer>
578
+ </Box>
579
+ ```
580
+
581
+ ## When to Use
582
+
583
+ ### ✅ Good Use Cases
584
+
585
+ - **Mobile navigation**: Hamburger menu that slides in from the left
586
+ - **Filter panels**: Complex filters that would clutter the main UI
587
+ - **Detail views**: Additional information without navigating away
588
+ - **Settings panels**: Quick access to preferences or configurations
589
+ - **Shopping cart**: E-commerce cart preview on the side
590
+ - **Help/Support**: Context-sensitive help or chat interfaces
591
+
592
+ ### ❌ When Not to Use
593
+
594
+ - **Critical actions**: Don't hide essential features in drawers
595
+ - **Primary navigation on desktop**: Consider a persistent sidebar instead
596
+ - **Simple choices**: Use Select or Dropdown for simple selections
597
+ - **Confirmations**: Use Dialog for confirmation messages
598
+ - **Notifications**: Use Toast or Alert for feedback messages
599
+ - **Large forms**: Complex forms should have their own page
600
+
601
+ ## Common Use Cases
602
+
603
+ ### Mobile Navigation Drawer
604
+
605
+ ```tsx
606
+ function NavigationDrawer({ open, onClose, currentPath }) {
607
+ const navItems = [
608
+ { label: 'Dashboard', path: '/dashboard', icon: <DashboardIcon /> },
609
+ { label: 'Users', path: '/users', icon: <PeopleIcon /> },
610
+ { label: 'Settings', path: '/settings', icon: <SettingsIcon /> },
611
+ ];
612
+
613
+ return (
614
+ <InsetDrawer open={open} onClose={onClose} anchor="left">
615
+ <Sheet sx={{ width: 280, height: '100%', p: 2 }}>
616
+ <Stack gap={1}>
617
+ <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
618
+ <Avatar src="/logo.png" />
619
+ <Typography level="title-lg" sx={{ ml: 1 }}>My App</Typography>
620
+ </Box>
621
+ <Divider />
622
+ <List>
623
+ {navItems.map((item) => (
624
+ <ListItem key={item.path}>
625
+ <ListItemButton
626
+ selected={currentPath === item.path}
627
+ onClick={() => {
628
+ navigate(item.path);
629
+ onClose();
630
+ }}
631
+ >
632
+ <ListItemDecorator>{item.icon}</ListItemDecorator>
633
+ {item.label}
634
+ </ListItemButton>
635
+ </ListItem>
636
+ ))}
637
+ </List>
638
+ </Stack>
639
+ </Sheet>
640
+ </InsetDrawer>
641
+ );
642
+ }
643
+ ```
644
+
645
+ ### Filter Panel
646
+
647
+ ```tsx
648
+ function ProductFilters({ open, onClose, filters, onApply }) {
649
+ const [localFilters, setLocalFilters] = useState(filters);
650
+
651
+ const handleApply = () => {
652
+ onApply(localFilters);
653
+ onClose();
654
+ };
655
+
656
+ const handleClear = () => {
657
+ setLocalFilters({});
658
+ };
659
+
660
+ return (
661
+ <InsetDrawer open={open} onClose={onClose} anchor="right" size="md">
662
+ <Sheet sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
663
+ <Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
664
+ <DialogTitle>Filters</DialogTitle>
665
+ <ModalClose />
666
+ </Box>
667
+
668
+ <DialogContent sx={{ flex: 1, overflow: 'auto', p: 2 }}>
669
+ <Stack gap={3}>
670
+ <FormControl>
671
+ <FormLabel>Category</FormLabel>
672
+ <Select
673
+ value={localFilters.category || ''}
674
+ onChange={(_, value) =>
675
+ setLocalFilters({ ...localFilters, category: value })
676
+ }
677
+ >
678
+ <Option value="electronics">Electronics</Option>
679
+ <Option value="clothing">Clothing</Option>
680
+ <Option value="books">Books</Option>
681
+ </Select>
682
+ </FormControl>
683
+
684
+ <FormControl>
685
+ <FormLabel>Price Range</FormLabel>
686
+ <Stack direction="row" gap={1}>
687
+ <Input
688
+ type="number"
689
+ placeholder="Min"
690
+ value={localFilters.minPrice || ''}
691
+ onChange={(e) =>
692
+ setLocalFilters({ ...localFilters, minPrice: e.target.value })
693
+ }
694
+ />
695
+ <Input
696
+ type="number"
697
+ placeholder="Max"
698
+ value={localFilters.maxPrice || ''}
699
+ onChange={(e) =>
700
+ setLocalFilters({ ...localFilters, maxPrice: e.target.value })
701
+ }
702
+ />
703
+ </Stack>
704
+ </FormControl>
705
+
706
+ <FormControl>
707
+ <FormLabel>Rating</FormLabel>
708
+ <RadioGroup
709
+ value={localFilters.rating || ''}
710
+ onChange={(e) =>
711
+ setLocalFilters({ ...localFilters, rating: e.target.value })
712
+ }
713
+ >
714
+ {[4, 3, 2, 1].map((rating) => (
715
+ <Radio
716
+ key={rating}
717
+ value={String(rating)}
718
+ label={`${rating}+ stars`}
719
+ />
720
+ ))}
721
+ </RadioGroup>
722
+ </FormControl>
723
+ </Stack>
724
+ </DialogContent>
725
+
726
+ <Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
727
+ <Stack direction="row" gap={1} justifyContent="space-between">
728
+ <Button variant="outlined" color="neutral" onClick={handleClear}>
729
+ Clear All
730
+ </Button>
731
+ <Button onClick={handleApply}>Apply Filters</Button>
732
+ </Stack>
733
+ </Box>
734
+ </Sheet>
735
+ </InsetDrawer>
736
+ );
737
+ }
738
+ ```
739
+
740
+ ### Shopping Cart Drawer
741
+
742
+ ```tsx
743
+ function CartDrawer({ open, onClose, items, onCheckout }) {
744
+ const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
745
+
746
+ return (
747
+ <InsetDrawer open={open} onClose={onClose} anchor="right" size="lg">
748
+ <Sheet sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
749
+ <Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
750
+ <DialogTitle>Shopping Cart ({items.length})</DialogTitle>
751
+ <ModalClose />
752
+ </Box>
753
+
754
+ <DialogContent sx={{ flex: 1, overflow: 'auto', p: 0 }}>
755
+ {items.length === 0 ? (
756
+ <Box sx={{ p: 4, textAlign: 'center' }}>
757
+ <ShoppingCartIcon sx={{ fontSize: 48, color: 'neutral.400' }} />
758
+ <Typography level="body-lg" sx={{ mt: 2 }}>
759
+ Your cart is empty
760
+ </Typography>
761
+ </Box>
762
+ ) : (
763
+ <List>
764
+ {items.map((item) => (
765
+ <ListItem key={item.id}>
766
+ <ListItemDecorator>
767
+ <img
768
+ src={item.image}
769
+ alt={item.name}
770
+ style={{ width: 60, height: 60, objectFit: 'cover' }}
771
+ />
772
+ </ListItemDecorator>
773
+ <ListItemContent>
774
+ <Typography level="title-sm">{item.name}</Typography>
775
+ <Typography level="body-sm">
776
+ ${item.price} x {item.quantity}
777
+ </Typography>
778
+ </ListItemContent>
779
+ <IconButton size="sm" color="danger">
780
+ <DeleteIcon />
781
+ </IconButton>
782
+ </ListItem>
783
+ ))}
784
+ </List>
785
+ )}
786
+ </DialogContent>
787
+
788
+ {items.length > 0 && (
789
+ <Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
790
+ <Stack gap={2}>
791
+ <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
792
+ <Typography level="title-md">Total</Typography>
793
+ <Typography level="title-md">${total.toFixed(2)}</Typography>
794
+ </Box>
795
+ <Button fullWidth onClick={onCheckout}>
796
+ Proceed to Checkout
797
+ </Button>
798
+ <Button variant="outlined" fullWidth onClick={onClose}>
799
+ Continue Shopping
800
+ </Button>
801
+ </Stack>
802
+ </Box>
803
+ )}
804
+ </Sheet>
805
+ </InsetDrawer>
806
+ );
807
+ }
133
808
  ```
809
+
810
+ ### Settings Drawer
811
+
812
+ ```tsx
813
+ function SettingsDrawer({ open, onClose }) {
814
+ const [settings, setSettings] = useState({
815
+ notifications: true,
816
+ darkMode: false,
817
+ language: 'en',
818
+ });
819
+
820
+ return (
821
+ <InsetDrawer open={open} onClose={onClose} anchor="right" size="sm">
822
+ <Sheet sx={{ height: '100%', p: 2 }}>
823
+ <DialogTitle>Settings</DialogTitle>
824
+ <ModalClose />
825
+ <Divider sx={{ my: 2 }} />
826
+
827
+ <DialogContent>
828
+ <Stack gap={3}>
829
+ <FormControl orientation="horizontal">
830
+ <Box sx={{ flex: 1 }}>
831
+ <FormLabel>Notifications</FormLabel>
832
+ <FormHelperText>Receive push notifications</FormHelperText>
833
+ </Box>
834
+ <Switch
835
+ checked={settings.notifications}
836
+ onChange={(e) =>
837
+ setSettings({ ...settings, notifications: e.target.checked })
838
+ }
839
+ />
840
+ </FormControl>
841
+
842
+ <FormControl orientation="horizontal">
843
+ <Box sx={{ flex: 1 }}>
844
+ <FormLabel>Dark Mode</FormLabel>
845
+ <FormHelperText>Use dark color theme</FormHelperText>
846
+ </Box>
847
+ <Switch
848
+ checked={settings.darkMode}
849
+ onChange={(e) =>
850
+ setSettings({ ...settings, darkMode: e.target.checked })
851
+ }
852
+ />
853
+ </FormControl>
854
+
855
+ <FormControl>
856
+ <FormLabel>Language</FormLabel>
857
+ <Select
858
+ value={settings.language}
859
+ onChange={(_, value) =>
860
+ setSettings({ ...settings, language: value || 'en' })
861
+ }
862
+ >
863
+ <Option value="en">English</Option>
864
+ <Option value="ko">한국어</Option>
865
+ <Option value="ja">日本語</Option>
866
+ </Select>
867
+ </FormControl>
868
+ </Stack>
869
+ </DialogContent>
870
+ </Sheet>
871
+ </InsetDrawer>
872
+ );
873
+ }
874
+ ```
875
+
876
+ ### Bottom Sheet for Mobile Actions
877
+
878
+ ```tsx
879
+ function ActionSheet({ open, onClose, onAction }) {
880
+ const actions = [
881
+ { id: 'edit', label: 'Edit', icon: <EditIcon /> },
882
+ { id: 'share', label: 'Share', icon: <ShareIcon /> },
883
+ { id: 'duplicate', label: 'Duplicate', icon: <ContentCopyIcon /> },
884
+ { id: 'delete', label: 'Delete', icon: <DeleteIcon />, color: 'danger' },
885
+ ];
886
+
887
+ return (
888
+ <InsetDrawer open={open} onClose={onClose} anchor="bottom">
889
+ <Sheet sx={{ borderRadius: 'lg lg 0 0', p: 2 }}>
890
+ <Box
891
+ sx={{
892
+ width: 40,
893
+ height: 4,
894
+ bgcolor: 'neutral.300',
895
+ borderRadius: 'xl',
896
+ mx: 'auto',
897
+ mb: 2,
898
+ }}
899
+ />
900
+ <List>
901
+ {actions.map((action) => (
902
+ <ListItem key={action.id}>
903
+ <ListItemButton
904
+ color={action.color}
905
+ onClick={() => {
906
+ onAction(action.id);
907
+ onClose();
908
+ }}
909
+ >
910
+ <ListItemDecorator>{action.icon}</ListItemDecorator>
911
+ {action.label}
912
+ </ListItemButton>
913
+ </ListItem>
914
+ ))}
915
+ </List>
916
+ <Button
917
+ variant="soft"
918
+ color="neutral"
919
+ fullWidth
920
+ sx={{ mt: 1 }}
921
+ onClick={onClose}
922
+ >
923
+ Cancel
924
+ </Button>
925
+ </Sheet>
926
+ </InsetDrawer>
927
+ );
928
+ }
929
+ ```
930
+
931
+ ### Help/Support Panel
932
+
933
+ ```tsx
934
+ function HelpDrawer({ open, onClose }) {
935
+ const [searchQuery, setSearchQuery] = useState('');
936
+
937
+ const faqItems = [
938
+ { question: 'How do I reset my password?', answer: '...' },
939
+ { question: 'How do I change my email?', answer: '...' },
940
+ { question: 'How do I delete my account?', answer: '...' },
941
+ ];
942
+
943
+ return (
944
+ <InsetDrawer open={open} onClose={onClose} anchor="right" size="lg">
945
+ <Sheet sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
946
+ <Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
947
+ <DialogTitle>Help Center</DialogTitle>
948
+ <ModalClose />
949
+ <Input
950
+ placeholder="Search for help..."
951
+ startDecorator={<SearchIcon />}
952
+ value={searchQuery}
953
+ onChange={(e) => setSearchQuery(e.target.value)}
954
+ sx={{ mt: 2 }}
955
+ />
956
+ </Box>
957
+
958
+ <DialogContent sx={{ flex: 1, overflow: 'auto', p: 2 }}>
959
+ <Typography level="title-md" sx={{ mb: 2 }}>
960
+ Frequently Asked Questions
961
+ </Typography>
962
+ <Accordion>
963
+ {faqItems
964
+ .filter((item) =>
965
+ item.question.toLowerCase().includes(searchQuery.toLowerCase())
966
+ )
967
+ .map((item, index) => (
968
+ <AccordionItem key={index}>
969
+ <AccordionSummary>{item.question}</AccordionSummary>
970
+ <AccordionDetails>{item.answer}</AccordionDetails>
971
+ </AccordionItem>
972
+ ))}
973
+ </Accordion>
974
+ </DialogContent>
975
+
976
+ <Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
977
+ <Typography level="body-sm" sx={{ mb: 1 }}>
978
+ Can't find what you're looking for?
979
+ </Typography>
980
+ <Button fullWidth startDecorator={<ChatIcon />}>
981
+ Contact Support
982
+ </Button>
983
+ </Box>
984
+ </Sheet>
985
+ </InsetDrawer>
986
+ );
987
+ }
988
+ ```
989
+
990
+ ## Props and Customization
991
+
992
+ ### Key Props
993
+
994
+ | Prop | Type | Default | Description |
995
+ | ---------- | ---------------------------------------- | -------- | ------------------------------------------------ |
996
+ | `open` | `boolean` | `false` | Controls whether the drawer is visible |
997
+ | `onClose` | `() => void` | - | Callback fired when the drawer requests to close |
998
+ | `anchor` | `'left' \| 'right' \| 'top' \| 'bottom'` | `'left'` | Edge from which the drawer slides in |
999
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size of the drawer |
1000
+ | `children` | `ReactNode` | - | Content to render inside the drawer |
1001
+
1002
+ ### Anchor Positions
1003
+
1004
+ ```tsx
1005
+ // Left drawer (navigation menus)
1006
+ <InsetDrawer anchor="left" open={open}>
1007
+ <Sheet sx={{ width: 280 }}>Navigation</Sheet>
1008
+ </InsetDrawer>
1009
+
1010
+ // Right drawer (details, filters)
1011
+ <InsetDrawer anchor="right" open={open}>
1012
+ <Sheet sx={{ width: 400 }}>Details</Sheet>
1013
+ </InsetDrawer>
1014
+
1015
+ // Top drawer (search, notifications)
1016
+ <InsetDrawer anchor="top" open={open}>
1017
+ <Sheet sx={{ height: 200 }}>Search</Sheet>
1018
+ </InsetDrawer>
1019
+
1020
+ // Bottom drawer (mobile actions)
1021
+ <InsetDrawer anchor="bottom" open={open}>
1022
+ <Sheet sx={{ maxHeight: '80vh' }}>Actions</Sheet>
1023
+ </InsetDrawer>
1024
+ ```
1025
+
1026
+ ### Size Options
1027
+
1028
+ The `size` prop controls the width (for left/right anchors) or height (for top/bottom anchors):
1029
+
1030
+ ```tsx
1031
+ // Small drawer (~280px)
1032
+ <InsetDrawer size="sm" anchor="right" />
1033
+
1034
+ // Medium drawer (~400px) - default
1035
+ <InsetDrawer size="md" anchor="right" />
1036
+
1037
+ // Large drawer (~600px)
1038
+ <InsetDrawer size="lg" anchor="right" />
1039
+ ```
1040
+
1041
+ ### Custom Sizing
1042
+
1043
+ For more control, use `Sheet` with custom styles:
1044
+
1045
+ ```tsx
1046
+ <InsetDrawer open={open} anchor="right">
1047
+ <Sheet sx={{ width: { xs: '100vw', sm: 450 }, height: '100%' }}>
1048
+ Content
1049
+ </Sheet>
1050
+ </InsetDrawer>
1051
+ ```
1052
+
1053
+ ### Drawer Content Structure
1054
+
1055
+ Use these components for consistent drawer layout:
1056
+
1057
+ ```tsx
1058
+ <InsetDrawer open={open}>
1059
+ <Sheet sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
1060
+ {/* Header */}
1061
+ <Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
1062
+ <DialogTitle>Drawer Title</DialogTitle>
1063
+ <ModalClose />
1064
+ </Box>
1065
+
1066
+ {/* Scrollable Content */}
1067
+ <DialogContent sx={{ flex: 1, overflow: 'auto', p: 2 }}>
1068
+ {/* Main content */}
1069
+ </DialogContent>
1070
+
1071
+ {/* Footer */}
1072
+ <Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
1073
+ <Stack direction="row" gap={1}>
1074
+ <Button variant="outlined">Cancel</Button>
1075
+ <Button>Save</Button>
1076
+ </Stack>
1077
+ </Box>
1078
+ </Sheet>
1079
+ </InsetDrawer>
1080
+ ```
1081
+
1082
+ ## Accessibility
1083
+
1084
+ InsetDrawer includes built-in accessibility features:
1085
+
1086
+ ### ARIA Attributes
1087
+
1088
+ - Drawer container has appropriate `role="dialog"` or `role="presentation"`
1089
+ - Focus is trapped within the drawer when open
1090
+ - Proper `aria-modal` attribute when modal
1091
+
1092
+ ### Keyboard Navigation
1093
+
1094
+ - **Escape**: Close the drawer
1095
+ - **Tab**: Navigate between focusable elements within drawer
1096
+ - **Shift + Tab**: Navigate backwards
1097
+
1098
+ ### Focus Management
1099
+
1100
+ ```tsx
1101
+ // Focus automatically moves to drawer content when opened
1102
+ <InsetDrawer
1103
+ open={open}
1104
+ onClose={onClose}
1105
+ // Focus is trapped within drawer
1106
+ >
1107
+ <Sheet>
1108
+ {/* First focusable element receives focus */}
1109
+ <Input autoFocus placeholder="Search..." />
1110
+ </Sheet>
1111
+ </InsetDrawer>
1112
+ ```
1113
+
1114
+ ### Screen Reader Support
1115
+
1116
+ ```tsx
1117
+ // Provide descriptive title for screen readers
1118
+ <InsetDrawer open={open}>
1119
+ <Sheet role="dialog" aria-labelledby="drawer-title">
1120
+ <DialogTitle id="drawer-title">Filters</DialogTitle>
1121
+ {/* Content */}
1122
+ </Sheet>
1123
+ </InsetDrawer>
1124
+ ```
1125
+
1126
+ ## Best Practices
1127
+
1128
+ ### ✅ Do
1129
+
1130
+ 1. **Use appropriate anchor positions**: Match drawer position to content type
1131
+
1132
+ ```tsx
1133
+ // ✅ Good: Navigation on left, details on right
1134
+ <InsetDrawer anchor="left"> {/* Navigation */}
1135
+ <InsetDrawer anchor="right"> {/* Details/filters */}
1136
+ <InsetDrawer anchor="bottom">{/* Mobile actions */}
1137
+ ```
1138
+
1139
+ 2. **Provide clear close affordances**: Include close button and backdrop click
1140
+
1141
+ ```tsx
1142
+ // ✅ Good: Multiple ways to close
1143
+ <InsetDrawer open={open} onClose={handleClose}>
1144
+ <Sheet>
1145
+ <ModalClose /> {/* X button */}
1146
+ <Button onClick={handleClose}>Cancel</Button> {/* Cancel button */}
1147
+ </Sheet>
1148
+ </InsetDrawer>
1149
+ ```
1150
+
1151
+ 3. **Use consistent header/footer patterns**: Structure content predictably
1152
+
1153
+ ```tsx
1154
+ // ✅ Good: Clear structure
1155
+ <Sheet>
1156
+ <Box sx={{ borderBottom: '1px solid', borderColor: 'divider' }}>
1157
+ <DialogTitle>Title</DialogTitle>
1158
+ <ModalClose />
1159
+ </Box>
1160
+ <DialogContent>{/* Scrollable content */}</DialogContent>
1161
+ <Box sx={{ borderTop: '1px solid', borderColor: 'divider' }}>
1162
+ {/* Action buttons */}
1163
+ </Box>
1164
+ </Sheet>
1165
+ ```
1166
+
1167
+ 4. **Handle mobile responsiveness**: Full width on small screens
1168
+
1169
+ ```tsx
1170
+ // ✅ Good: Responsive width
1171
+ <Sheet sx={{ width: { xs: '100vw', sm: 400 } }}>
1172
+ ```
1173
+
1174
+ ### ❌ Don't
1175
+
1176
+ 1. **Don't hide critical content**: Keep essential features accessible
1177
+
1178
+ ```tsx
1179
+ // ❌ Bad: Primary action hidden in drawer
1180
+ <InsetDrawer>
1181
+ <Button>Complete Purchase</Button> {/* Should be on main page */}
1182
+ </InsetDrawer>
1183
+ ```
1184
+
1185
+ 2. **Don't nest drawers**: Leads to confusing UX
1186
+
1187
+ ```tsx
1188
+ // ❌ Bad: Drawer within drawer
1189
+ <InsetDrawer>
1190
+ <Button onClick={() => setNestedOpen(true)}>Open Another</Button>
1191
+ <InsetDrawer open={nestedOpen}>{/* Nested drawer */}</InsetDrawer>
1192
+ </InsetDrawer>
1193
+ ```
1194
+
1195
+ 3. **Don't use for confirmations**: Use Dialog instead
1196
+
1197
+ ```tsx
1198
+ // ❌ Bad: Drawer for confirmation
1199
+ <InsetDrawer>
1200
+ <Typography>Are you sure?</Typography>
1201
+ <Button>Confirm</Button>
1202
+ </InsetDrawer>
1203
+
1204
+ // ✅ Good: Use Dialog
1205
+ <Dialog>
1206
+ <Typography>Are you sure?</Typography>
1207
+ <Button>Confirm</Button>
1208
+ </Dialog>
1209
+ ```
1210
+
1211
+ 4. **Don't forget about keyboard users**: Ensure proper focus management
1212
+
1213
+ ## Performance Considerations
1214
+
1215
+ ### Lazy Loading Content
1216
+
1217
+ For drawers with heavy content, load data when drawer opens:
1218
+
1219
+ ```tsx
1220
+ function DetailDrawer({ open, itemId }) {
1221
+ const [data, setData] = useState(null);
1222
+
1223
+ useEffect(() => {
1224
+ if (open && itemId) {
1225
+ fetchItemDetails(itemId).then(setData);
1226
+ }
1227
+ }, [open, itemId]);
1228
+
1229
+ return (
1230
+ <InsetDrawer open={open}>
1231
+ <Sheet>
1232
+ {data ? <ItemDetails data={data} /> : <Skeleton />}
1233
+ </Sheet>
1234
+ </InsetDrawer>
1235
+ );
1236
+ }
1237
+ ```
1238
+
1239
+ ### Memoize Handlers
1240
+
1241
+ ```tsx
1242
+ const handleClose = useCallback(() => {
1243
+ setOpen(false);
1244
+ }, []);
1245
+
1246
+ const handleApply = useCallback((filters) => {
1247
+ applyFilters(filters);
1248
+ setOpen(false);
1249
+ }, [applyFilters]);
1250
+
1251
+ <InsetDrawer open={open} onClose={handleClose}>
1252
+ <FilterPanel onApply={handleApply} />
1253
+ </InsetDrawer>
1254
+ ```
1255
+
1256
+ ### Unmount When Closed
1257
+
1258
+ For memory-intensive content, conditionally render:
1259
+
1260
+ ```tsx
1261
+ {open && (
1262
+ <InsetDrawer open={open} onClose={handleClose}>
1263
+ <HeavyContentComponent />
1264
+ </InsetDrawer>
1265
+ )}
1266
+ ```
1267
+
1268
+ ### Avoid Layout Shifts
1269
+
1270
+ Use fixed dimensions to prevent content shifts:
1271
+
1272
+ ```tsx
1273
+ <Sheet sx={{
1274
+ width: 400,
1275
+ height: '100%',
1276
+ // Content won't cause width changes
1277
+ }}>
1278
+ ```
1279
+
1280
+ InsetDrawer provides flexible secondary content areas that slide in from screen edges. Use it for navigation menus, filter panels, and detail views while keeping primary actions visible on the main screen. Always consider the user's workflow and ensure drawers enhance rather than hinder the experience.