@ceed/cds 1.28.1 → 1.29.0-next.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.
Files changed (63) hide show
  1. package/dist/components/CurrencyInput/CurrencyInput.d.ts +1 -1
  2. package/dist/components/CurrencyInput/hooks/use-currency-setting.d.ts +2 -2
  3. package/dist/components/SearchBar/SearchBar.d.ts +21 -0
  4. package/dist/components/SearchBar/index.d.ts +3 -0
  5. package/dist/components/data-display/DataTable.md +1 -1
  6. package/dist/components/data-display/InfoSign.md +91 -74
  7. package/dist/components/data-display/Typography.md +94 -411
  8. package/dist/components/feedback/Dialog.md +62 -76
  9. package/dist/components/feedback/Modal.md +138 -430
  10. package/dist/components/feedback/llms.txt +0 -2
  11. package/dist/components/index.d.ts +2 -1
  12. package/dist/components/inputs/Autocomplete.md +107 -356
  13. package/dist/components/inputs/ButtonGroup.md +104 -115
  14. package/dist/components/inputs/CurrencyInput.md +5 -183
  15. package/dist/components/inputs/DatePicker.md +431 -108
  16. package/dist/components/inputs/DateRangePicker.md +492 -131
  17. package/dist/components/inputs/FilterableCheckboxGroup.md +19 -145
  18. package/dist/components/inputs/IconButton.md +88 -137
  19. package/dist/components/inputs/Input.md +73 -204
  20. package/dist/components/inputs/MonthPicker.md +422 -95
  21. package/dist/components/inputs/MonthRangePicker.md +466 -89
  22. package/dist/components/inputs/PercentageInput.md +16 -185
  23. package/dist/components/inputs/RadioButton.md +35 -163
  24. package/dist/components/inputs/SearchBar.md +44 -0
  25. package/dist/components/inputs/Select.md +326 -222
  26. package/dist/components/inputs/Switch.md +376 -143
  27. package/dist/components/inputs/Textarea.md +10 -213
  28. package/dist/components/inputs/Uploader/Uploader.md +66 -145
  29. package/dist/components/inputs/llms.txt +1 -4
  30. package/dist/components/navigation/Breadcrumbs.md +308 -57
  31. package/dist/components/navigation/Drawer.md +0 -180
  32. package/dist/components/navigation/Dropdown.md +215 -98
  33. package/dist/components/navigation/IconMenuButton.md +502 -40
  34. package/dist/components/navigation/InsetDrawer.md +650 -281
  35. package/dist/components/navigation/Link.md +348 -31
  36. package/dist/components/navigation/Menu.md +285 -92
  37. package/dist/components/navigation/MenuButton.md +448 -55
  38. package/dist/components/navigation/Pagination.md +338 -47
  39. package/dist/components/navigation/Stepper.md +28 -160
  40. package/dist/components/navigation/Tabs.md +316 -57
  41. package/dist/components/surfaces/Accordions.md +804 -49
  42. package/dist/components/surfaces/Card.md +157 -97
  43. package/dist/components/surfaces/Divider.md +234 -83
  44. package/dist/components/surfaces/Sheet.md +328 -153
  45. package/dist/index.cjs +411 -574
  46. package/dist/index.d.ts +1 -1
  47. package/dist/index.js +400 -507
  48. package/dist/llms.txt +1 -9
  49. package/framer/index.js +1 -1
  50. package/package.json +17 -22
  51. package/dist/chunks/rehype-accent-FZRUD7VI.js +0 -39
  52. package/dist/components/RadioTileGroup/RadioTileGroup.d.ts +0 -56
  53. package/dist/components/RadioTileGroup/index.d.ts +0 -3
  54. package/dist/components/feedback/CircularProgress.md +0 -257
  55. package/dist/components/feedback/Skeleton.md +0 -280
  56. package/dist/components/inputs/FormControl.md +0 -361
  57. package/dist/components/inputs/RadioList.md +0 -241
  58. package/dist/components/inputs/RadioTileGroup.md +0 -507
  59. package/dist/components/inputs/Slider.md +0 -334
  60. package/dist/guides/ThemeProvider.md +0 -89
  61. package/dist/guides/llms.txt +0 -9
  62. package/dist/index.browser.js +0 -224
  63. package/dist/index.browser.js.map +0 -7
@@ -2,134 +2,25 @@
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 a Modal, which overlays the entire viewport, InsetDrawer slides in from a specific edge and pushes or covers only part of the interface.
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
6
 
7
- It is commonly used for mobile navigation menus, filter panels, configuration sidebars, and detail views. The component supports four anchor positions (left, right, top, bottom) and three size presets, making it adaptable to a wide range of layout requirements.
8
-
9
- ```tsx
10
- <InsetDrawer {...args}>
11
- <Sheet sx={{
12
- borderRadius: 'md',
13
- p: 2,
14
- display: 'flex',
15
- flexDirection: 'column',
16
- gap: 2,
17
- height: '100%',
18
- overflow: 'auto'
19
- }}>
20
- <DialogTitle>Filters</DialogTitle>
21
- <ModalClose />
22
- <Divider sx={{
23
- mt: 'auto'
24
- }} />
25
- <DialogContent>
26
- <FormControl>
27
- <FormLabel sx={{
28
- typography: 'title-md',
29
- fontWeight: 'bold'
30
- }}>Property type</FormLabel>
31
- <RadioGroup>
32
- <Box sx={{
33
- display: 'grid',
34
- gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
35
- gap: 1.5
36
- }}>
37
- {[{
38
- name: 'House',
39
- icon: <HomeRoundedIcon />
40
- }, {
41
- name: 'Apartment',
42
- icon: <ApartmentRoundedIcon />
43
- }, {
44
- name: 'Guesthouse',
45
- icon: <MeetingRoomRoundedIcon />
46
- }, {
47
- name: 'Hotel',
48
- icon: <HotelRoundedIcon />
49
- }].map(item => <Card key={item.name} sx={{
50
- boxShadow: 'none',
51
- '&:hover': {
52
- bgcolor: 'background.level1'
53
- }
54
- }}>
55
- <CardContent>
56
- {item.icon}
57
- <Typography level="title-md">{item.name}</Typography>
58
- </CardContent>
59
- <Radio disableIcon overlay variant="outlined" color="neutral" value={item.name} sx={{
60
- mt: -2
61
- }} slotProps={{
62
- action: {
63
- sx: {
64
- '&:hover': {
65
- bgcolor: 'transparent'
66
- }
67
- }
68
- }
69
- }} />
70
- </Card>)}
71
- </Box>
72
- </RadioGroup>
73
- </FormControl>
74
-
75
- <Typography level="title-md" fontWeight="bold" sx={{
76
- mt: 2
77
- }}>
78
- Booking options
79
- </Typography>
80
- <FormControl orientation="horizontal">
81
- <Box sx={{
82
- flex: 1,
83
- pr: 1
84
- }}>
85
- <FormLabel sx={{
86
- typography: 'title-sm'
87
- }}>Instant booking</FormLabel>
88
- <FormHelperText sx={{
89
- typography: 'body-sm'
90
- }}>
91
- Listings that you can book without waiting for host approval.
92
- </FormHelperText>
93
- </Box>
94
- <Switch />
95
- </FormControl>
96
-
97
- <FormControl orientation="horizontal">
98
- <Box sx={{
99
- flex: 1,
100
- mt: 1,
101
- mr: 1
102
- }}>
103
- <FormLabel sx={{
104
- typography: 'title-sm'
105
- }}>Self check-in</FormLabel>
106
- <FormHelperText sx={{
107
- typography: 'body-sm'
108
- }}>
109
- Easy access to the property when you arrive.
110
- </FormHelperText>
111
- </Box>
112
- <Switch />
113
- </FormControl>
114
- </DialogContent>
115
-
116
- <Divider sx={{
117
- mt: 'auto'
118
- }} />
119
- <Stack direction="row" justifyContent="space-between" useFlexGap spacing={1}>
120
- <Button variant="outlined" color="neutral">
121
- Clear
122
- </Button>
123
- <Button>Show 165 properties</Button>
124
- </Stack>
125
- </Sheet>
126
- </InsetDrawer>
7
+ ```
8
+ <Canvas of={InsetDrawer.Playground} />
127
9
  ```
128
10
 
129
11
  | Field | Description | Default |
130
12
  | ---------------------------- | ----------- | ------- |
131
13
  | Controls resolved at runtime | — | — |
132
14
 
15
+ > ⚠️ **Usage Warning** ⚠️
16
+ >
17
+ > Consider these factors before using InsetDrawer:
18
+ >
19
+ > - **Mobile-first design**: InsetDrawer works best on mobile; consider persistent sidebars on desktop
20
+ > - **Content hierarchy**: Don't hide critical content in drawers that users need frequently
21
+ > - **Anchor position**: Use `left` for navigation, `right` for details/filters, `bottom` for actions
22
+ > - **Accessibility**: Ensure proper focus management and keyboard navigation
23
+
133
24
  ## Usage
134
25
 
135
26
  ```tsx
@@ -155,133 +46,71 @@ function FilterDrawer() {
155
46
  }
156
47
  ```
157
48
 
158
- ## Default
49
+ ## Examples
159
50
 
160
- The default InsetDrawer with a filter panel layout, demonstrating the component's structure with `DialogTitle`, `ModalClose`, `DialogContent`, and action buttons.
51
+ ### Playground
161
52
 
162
- ```tsx
163
- <InsetDrawer {...args}>
164
- <Sheet sx={{
165
- borderRadius: 'md',
166
- p: 2,
167
- display: 'flex',
168
- flexDirection: 'column',
169
- gap: 2,
170
- height: '100%',
171
- overflow: 'auto'
172
- }}>
173
- <DialogTitle>Filters</DialogTitle>
174
- <ModalClose />
175
- <Divider sx={{
176
- mt: 'auto'
177
- }} />
178
- <DialogContent>
179
- <FormControl>
180
- <FormLabel sx={{
181
- typography: 'title-md',
182
- fontWeight: 'bold'
183
- }}>Property type</FormLabel>
184
- <RadioGroup>
185
- <Box sx={{
186
- display: 'grid',
187
- gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
188
- gap: 1.5
189
- }}>
190
- {[{
191
- name: 'House',
192
- icon: <HomeRoundedIcon />
193
- }, {
194
- name: 'Apartment',
195
- icon: <ApartmentRoundedIcon />
196
- }, {
197
- name: 'Guesthouse',
198
- icon: <MeetingRoomRoundedIcon />
199
- }, {
200
- name: 'Hotel',
201
- icon: <HotelRoundedIcon />
202
- }].map(item => <Card key={item.name} sx={{
203
- boxShadow: 'none',
204
- '&:hover': {
205
- bgcolor: 'background.level1'
206
- }
207
- }}>
208
- <CardContent>
209
- {item.icon}
210
- <Typography level="title-md">{item.name}</Typography>
211
- </CardContent>
212
- <Radio disableIcon overlay variant="outlined" color="neutral" value={item.name} sx={{
213
- mt: -2
214
- }} slotProps={{
215
- action: {
216
- sx: {
217
- '&:hover': {
218
- bgcolor: 'transparent'
219
- }
220
- }
221
- }
222
- }} />
223
- </Card>)}
224
- </Box>
225
- </RadioGroup>
226
- </FormControl>
53
+ Interactive example with controls for anchor position and size.
227
54
 
228
- <Typography level="title-md" fontWeight="bold" sx={{
229
- mt: 2
230
- }}>
231
- Booking options
232
- </Typography>
233
- <FormControl orientation="horizontal">
234
- <Box sx={{
235
- flex: 1,
236
- pr: 1
237
- }}>
238
- <FormLabel sx={{
239
- typography: 'title-sm'
240
- }}>Instant booking</FormLabel>
241
- <FormHelperText sx={{
242
- typography: 'body-sm'
243
- }}>
244
- Listings that you can book without waiting for host approval.
245
- </FormHelperText>
246
- </Box>
247
- <Switch />
248
- </FormControl>
249
-
250
- <FormControl orientation="horizontal">
251
- <Box sx={{
252
- flex: 1,
253
- mt: 1,
254
- mr: 1
255
- }}>
256
- <FormLabel sx={{
257
- typography: 'title-sm'
258
- }}>Self check-in</FormLabel>
259
- <FormHelperText sx={{
260
- typography: 'body-sm'
261
- }}>
262
- Easy access to the property when you arrive.
263
- </FormHelperText>
264
- </Box>
265
- <Switch />
266
- </FormControl>
267
- </DialogContent>
268
-
269
- <Divider sx={{
270
- mt: 'auto'
271
- }} />
272
- <Stack direction="row" justifyContent="space-between" useFlexGap spacing={1}>
273
- <Button variant="outlined" color="neutral">
274
- Clear
275
- </Button>
276
- <Button>Show 165 properties</Button>
277
- </Stack>
278
- </Sheet>
279
- </InsetDrawer>
280
55
  ```
56
+ <Canvas of={InsetDrawer.Playground} />
57
+ ```
58
+
59
+ ### Anchors
60
+
61
+ Different anchor positions: left, right, top, bottom.
62
+
63
+ ```
64
+ <Canvas of={InsetDrawer.Anchors} />
65
+ ```
66
+
67
+ ### Sizes
68
+
69
+ Available size options: small, medium, large.
70
+
71
+ ```
72
+ <Canvas of={InsetDrawer.Sizes} />
73
+ ```
74
+
75
+ ### Navigation Menu
76
+
77
+ A mobile navigation menu using InsetDrawer.
78
+
79
+ ```
80
+ <Canvas of={InsetDrawer.NavigationMenu} />
81
+ ```
82
+
83
+ ### Filter Panel
84
+
85
+ A filter panel with form controls sliding from the right.
86
+
87
+ ```
88
+ <Canvas of={InsetDrawer.FilterPanel} />
89
+ ```
90
+
91
+ ## When to Use
92
+
93
+ ### ✅ Good Use Cases
94
+
95
+ - **Mobile navigation**: Hamburger menu that slides in from the left
96
+ - **Filter panels**: Complex filters that would clutter the main UI
97
+ - **Detail views**: Additional information without navigating away
98
+ - **Settings panels**: Quick access to preferences or configurations
99
+ - **Shopping cart**: E-commerce cart preview on the side
100
+ - **Help/Support**: Context-sensitive help or chat interfaces
101
+
102
+ ### ❌ When Not to Use
103
+
104
+ - **Critical actions**: Don't hide essential features in drawers
105
+ - **Primary navigation on desktop**: Consider a persistent sidebar instead
106
+ - **Simple choices**: Use Select or Dropdown for simple selections
107
+ - **Confirmations**: Use Dialog for confirmation messages
108
+ - **Notifications**: Use Toast or Alert for feedback messages
109
+ - **Large forms**: Complex forms should have their own page
281
110
 
282
111
  ## Common Use Cases
283
112
 
284
- ### Navigation Drawer
113
+ ### Mobile Navigation Drawer
285
114
 
286
115
  ```tsx
287
116
  function NavigationDrawer({ open, onClose, currentPath }) {
@@ -295,14 +124,20 @@ function NavigationDrawer({ open, onClose, currentPath }) {
295
124
  <InsetDrawer open={open} onClose={onClose} anchor="left">
296
125
  <Sheet sx={{ width: 280, height: '100%', p: 2 }}>
297
126
  <Stack gap={1}>
298
- <Typography level="title-lg">My App</Typography>
127
+ <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
128
+ <Avatar src="/logo.png" />
129
+ <Typography level="title-lg" sx={{ ml: 1 }}>My App</Typography>
130
+ </Box>
299
131
  <Divider />
300
132
  <List>
301
133
  {navItems.map((item) => (
302
134
  <ListItem key={item.path}>
303
135
  <ListItemButton
304
136
  selected={currentPath === item.path}
305
- onClick={() => { navigate(item.path); onClose(); }}
137
+ onClick={() => {
138
+ navigate(item.path);
139
+ onClose();
140
+ }}
306
141
  >
307
142
  <ListItemDecorator>{item.icon}</ListItemDecorator>
308
143
  {item.label}
@@ -320,8 +155,17 @@ function NavigationDrawer({ open, onClose, currentPath }) {
320
155
  ### Filter Panel
321
156
 
322
157
  ```tsx
323
- function ProductFilters({ open, onClose, onApply }) {
324
- const [localFilters, setLocalFilters] = useState({});
158
+ function ProductFilters({ open, onClose, filters, onApply }) {
159
+ const [localFilters, setLocalFilters] = useState(filters);
160
+
161
+ const handleApply = () => {
162
+ onApply(localFilters);
163
+ onClose();
164
+ };
165
+
166
+ const handleClear = () => {
167
+ setLocalFilters({});
168
+ };
325
169
 
326
170
  return (
327
171
  <InsetDrawer open={open} onClose={onClose} anchor="right" size="md">
@@ -330,17 +174,71 @@ function ProductFilters({ open, onClose, onApply }) {
330
174
  <DialogTitle>Filters</DialogTitle>
331
175
  <ModalClose />
332
176
  </Box>
177
+
333
178
  <DialogContent sx={{ flex: 1, overflow: 'auto', p: 2 }}>
334
- {/* Filter form controls */}
179
+ <Stack gap={3}>
180
+ <FormControl>
181
+ <FormLabel>Category</FormLabel>
182
+ <Select
183
+ value={localFilters.category || ''}
184
+ onChange={(_, value) =>
185
+ setLocalFilters({ ...localFilters, category: value })
186
+ }
187
+ >
188
+ <Option value="electronics">Electronics</Option>
189
+ <Option value="clothing">Clothing</Option>
190
+ <Option value="books">Books</Option>
191
+ </Select>
192
+ </FormControl>
193
+
194
+ <FormControl>
195
+ <FormLabel>Price Range</FormLabel>
196
+ <Stack direction="row" gap={1}>
197
+ <Input
198
+ type="number"
199
+ placeholder="Min"
200
+ value={localFilters.minPrice || ''}
201
+ onChange={(e) =>
202
+ setLocalFilters({ ...localFilters, minPrice: e.target.value })
203
+ }
204
+ />
205
+ <Input
206
+ type="number"
207
+ placeholder="Max"
208
+ value={localFilters.maxPrice || ''}
209
+ onChange={(e) =>
210
+ setLocalFilters({ ...localFilters, maxPrice: e.target.value })
211
+ }
212
+ />
213
+ </Stack>
214
+ </FormControl>
215
+
216
+ <FormControl>
217
+ <FormLabel>Rating</FormLabel>
218
+ <RadioGroup
219
+ value={localFilters.rating || ''}
220
+ onChange={(e) =>
221
+ setLocalFilters({ ...localFilters, rating: e.target.value })
222
+ }
223
+ >
224
+ {[4, 3, 2, 1].map((rating) => (
225
+ <Radio
226
+ key={rating}
227
+ value={String(rating)}
228
+ label={`${rating}+ stars`}
229
+ />
230
+ ))}
231
+ </RadioGroup>
232
+ </FormControl>
233
+ </Stack>
335
234
  </DialogContent>
235
+
336
236
  <Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
337
237
  <Stack direction="row" gap={1} justifyContent="space-between">
338
- <Button variant="outlined" color="neutral" onClick={() => setLocalFilters({})}>
238
+ <Button variant="outlined" color="neutral" onClick={handleClear}>
339
239
  Clear All
340
240
  </Button>
341
- <Button onClick={() => { onApply(localFilters); onClose(); }}>
342
- Apply Filters
343
- </Button>
241
+ <Button onClick={handleApply}>Apply Filters</Button>
344
242
  </Stack>
345
243
  </Box>
346
244
  </Sheet>
@@ -349,26 +247,189 @@ function ProductFilters({ open, onClose, onApply }) {
349
247
  }
350
248
  ```
351
249
 
352
- ### Bottom Sheet for Mobile
250
+ ### Shopping Cart Drawer
251
+
252
+ ```tsx
253
+ function CartDrawer({ open, onClose, items, onCheckout }) {
254
+ const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
255
+
256
+ return (
257
+ <InsetDrawer open={open} onClose={onClose} anchor="right" size="lg">
258
+ <Sheet sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
259
+ <Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
260
+ <DialogTitle>Shopping Cart ({items.length})</DialogTitle>
261
+ <ModalClose />
262
+ </Box>
263
+
264
+ <DialogContent sx={{ flex: 1, overflow: 'auto', p: 0 }}>
265
+ {items.length === 0 ? (
266
+ <Box sx={{ p: 4, textAlign: 'center' }}>
267
+ <ShoppingCartIcon sx={{ fontSize: 48, color: 'neutral.400' }} />
268
+ <Typography level="body-lg" sx={{ mt: 2 }}>
269
+ Your cart is empty
270
+ </Typography>
271
+ </Box>
272
+ ) : (
273
+ <List>
274
+ {items.map((item) => (
275
+ <ListItem key={item.id}>
276
+ <ListItemDecorator>
277
+ <img
278
+ src={item.image}
279
+ alt={item.name}
280
+ style={{ width: 60, height: 60, objectFit: 'cover' }}
281
+ />
282
+ </ListItemDecorator>
283
+ <ListItemContent>
284
+ <Typography level="title-sm">{item.name}</Typography>
285
+ <Typography level="body-sm">
286
+ ${item.price} x {item.quantity}
287
+ </Typography>
288
+ </ListItemContent>
289
+ <IconButton size="sm" color="danger">
290
+ <DeleteIcon />
291
+ </IconButton>
292
+ </ListItem>
293
+ ))}
294
+ </List>
295
+ )}
296
+ </DialogContent>
297
+
298
+ {items.length > 0 && (
299
+ <Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
300
+ <Stack gap={2}>
301
+ <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
302
+ <Typography level="title-md">Total</Typography>
303
+ <Typography level="title-md">${total.toFixed(2)}</Typography>
304
+ </Box>
305
+ <Button fullWidth onClick={onCheckout}>
306
+ Proceed to Checkout
307
+ </Button>
308
+ <Button variant="outlined" fullWidth onClick={onClose}>
309
+ Continue Shopping
310
+ </Button>
311
+ </Stack>
312
+ </Box>
313
+ )}
314
+ </Sheet>
315
+ </InsetDrawer>
316
+ );
317
+ }
318
+ ```
319
+
320
+ ### Settings Drawer
321
+
322
+ ```tsx
323
+ function SettingsDrawer({ open, onClose }) {
324
+ const [settings, setSettings] = useState({
325
+ notifications: true,
326
+ darkMode: false,
327
+ language: 'en',
328
+ });
329
+
330
+ return (
331
+ <InsetDrawer open={open} onClose={onClose} anchor="right" size="sm">
332
+ <Sheet sx={{ height: '100%', p: 2 }}>
333
+ <DialogTitle>Settings</DialogTitle>
334
+ <ModalClose />
335
+ <Divider sx={{ my: 2 }} />
336
+
337
+ <DialogContent>
338
+ <Stack gap={3}>
339
+ <FormControl orientation="horizontal">
340
+ <Box sx={{ flex: 1 }}>
341
+ <FormLabel>Notifications</FormLabel>
342
+ <FormHelperText>Receive push notifications</FormHelperText>
343
+ </Box>
344
+ <Switch
345
+ checked={settings.notifications}
346
+ onChange={(e) =>
347
+ setSettings({ ...settings, notifications: e.target.checked })
348
+ }
349
+ />
350
+ </FormControl>
351
+
352
+ <FormControl orientation="horizontal">
353
+ <Box sx={{ flex: 1 }}>
354
+ <FormLabel>Dark Mode</FormLabel>
355
+ <FormHelperText>Use dark color theme</FormHelperText>
356
+ </Box>
357
+ <Switch
358
+ checked={settings.darkMode}
359
+ onChange={(e) =>
360
+ setSettings({ ...settings, darkMode: e.target.checked })
361
+ }
362
+ />
363
+ </FormControl>
364
+
365
+ <FormControl>
366
+ <FormLabel>Language</FormLabel>
367
+ <Select
368
+ value={settings.language}
369
+ onChange={(_, value) =>
370
+ setSettings({ ...settings, language: value || 'en' })
371
+ }
372
+ >
373
+ <Option value="en">English</Option>
374
+ <Option value="ko">한국어</Option>
375
+ <Option value="ja">日本語</Option>
376
+ </Select>
377
+ </FormControl>
378
+ </Stack>
379
+ </DialogContent>
380
+ </Sheet>
381
+ </InsetDrawer>
382
+ );
383
+ }
384
+ ```
385
+
386
+ ### Bottom Sheet for Mobile Actions
353
387
 
354
388
  ```tsx
355
389
  function ActionSheet({ open, onClose, onAction }) {
390
+ const actions = [
391
+ { id: 'edit', label: 'Edit', icon: <EditIcon /> },
392
+ { id: 'share', label: 'Share', icon: <ShareIcon /> },
393
+ { id: 'duplicate', label: 'Duplicate', icon: <ContentCopyIcon /> },
394
+ { id: 'delete', label: 'Delete', icon: <DeleteIcon />, color: 'danger' },
395
+ ];
396
+
356
397
  return (
357
398
  <InsetDrawer open={open} onClose={onClose} anchor="bottom">
358
399
  <Sheet sx={{ borderRadius: 'lg lg 0 0', p: 2 }}>
400
+ <Box
401
+ sx={{
402
+ width: 40,
403
+ height: 4,
404
+ bgcolor: 'neutral.300',
405
+ borderRadius: 'xl',
406
+ mx: 'auto',
407
+ mb: 2,
408
+ }}
409
+ />
359
410
  <List>
360
- <ListItem>
361
- <ListItemButton onClick={() => { onAction('edit'); onClose(); }}>
362
- Edit
363
- </ListItemButton>
364
- </ListItem>
365
- <ListItem>
366
- <ListItemButton onClick={() => { onAction('delete'); onClose(); }}>
367
- Delete
368
- </ListItemButton>
369
- </ListItem>
411
+ {actions.map((action) => (
412
+ <ListItem key={action.id}>
413
+ <ListItemButton
414
+ color={action.color}
415
+ onClick={() => {
416
+ onAction(action.id);
417
+ onClose();
418
+ }}
419
+ >
420
+ <ListItemDecorator>{action.icon}</ListItemDecorator>
421
+ {action.label}
422
+ </ListItemButton>
423
+ </ListItem>
424
+ ))}
370
425
  </List>
371
- <Button variant="soft" color="neutral" fullWidth onClick={onClose}>
426
+ <Button
427
+ variant="soft"
428
+ color="neutral"
429
+ fullWidth
430
+ sx={{ mt: 1 }}
431
+ onClick={onClose}
432
+ >
372
433
  Cancel
373
434
  </Button>
374
435
  </Sheet>
@@ -377,45 +438,353 @@ function ActionSheet({ open, onClose, onAction }) {
377
438
  }
378
439
  ```
379
440
 
441
+ ### Help/Support Panel
442
+
443
+ ```tsx
444
+ function HelpDrawer({ open, onClose }) {
445
+ const [searchQuery, setSearchQuery] = useState('');
446
+
447
+ const faqItems = [
448
+ { question: 'How do I reset my password?', answer: '...' },
449
+ { question: 'How do I change my email?', answer: '...' },
450
+ { question: 'How do I delete my account?', answer: '...' },
451
+ ];
452
+
453
+ return (
454
+ <InsetDrawer open={open} onClose={onClose} anchor="right" size="lg">
455
+ <Sheet sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
456
+ <Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
457
+ <DialogTitle>Help Center</DialogTitle>
458
+ <ModalClose />
459
+ <Input
460
+ placeholder="Search for help..."
461
+ startDecorator={<SearchIcon />}
462
+ value={searchQuery}
463
+ onChange={(e) => setSearchQuery(e.target.value)}
464
+ sx={{ mt: 2 }}
465
+ />
466
+ </Box>
467
+
468
+ <DialogContent sx={{ flex: 1, overflow: 'auto', p: 2 }}>
469
+ <Typography level="title-md" sx={{ mb: 2 }}>
470
+ Frequently Asked Questions
471
+ </Typography>
472
+ <Accordion>
473
+ {faqItems
474
+ .filter((item) =>
475
+ item.question.toLowerCase().includes(searchQuery.toLowerCase())
476
+ )
477
+ .map((item, index) => (
478
+ <AccordionItem key={index}>
479
+ <AccordionSummary>{item.question}</AccordionSummary>
480
+ <AccordionDetails>{item.answer}</AccordionDetails>
481
+ </AccordionItem>
482
+ ))}
483
+ </Accordion>
484
+ </DialogContent>
485
+
486
+ <Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
487
+ <Typography level="body-sm" sx={{ mb: 1 }}>
488
+ Can't find what you're looking for?
489
+ </Typography>
490
+ <Button fullWidth startDecorator={<ChatIcon />}>
491
+ Contact Support
492
+ </Button>
493
+ </Box>
494
+ </Sheet>
495
+ </InsetDrawer>
496
+ );
497
+ }
498
+ ```
499
+
500
+ ## Props and Customization
501
+
502
+ ### Key Props
503
+
504
+ | Prop | Type | Default | Description |
505
+ | ---------- | ---------------------------------------- | -------- | ------------------------------------------------ |
506
+ | `open` | `boolean` | `false` | Controls whether the drawer is visible |
507
+ | `onClose` | `() => void` | - | Callback fired when the drawer requests to close |
508
+ | `anchor` | `'left' \| 'right' \| 'top' \| 'bottom'` | `'left'` | Edge from which the drawer slides in |
509
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size of the drawer |
510
+ | `children` | `ReactNode` | - | Content to render inside the drawer |
511
+
512
+ ### Anchor Positions
513
+
514
+ ```tsx
515
+ // Left drawer (navigation menus)
516
+ <InsetDrawer anchor="left" open={open}>
517
+ <Sheet sx={{ width: 280 }}>Navigation</Sheet>
518
+ </InsetDrawer>
519
+
520
+ // Right drawer (details, filters)
521
+ <InsetDrawer anchor="right" open={open}>
522
+ <Sheet sx={{ width: 400 }}>Details</Sheet>
523
+ </InsetDrawer>
524
+
525
+ // Top drawer (search, notifications)
526
+ <InsetDrawer anchor="top" open={open}>
527
+ <Sheet sx={{ height: 200 }}>Search</Sheet>
528
+ </InsetDrawer>
529
+
530
+ // Bottom drawer (mobile actions)
531
+ <InsetDrawer anchor="bottom" open={open}>
532
+ <Sheet sx={{ maxHeight: '80vh' }}>Actions</Sheet>
533
+ </InsetDrawer>
534
+ ```
535
+
536
+ ### Size Options
537
+
538
+ The `size` prop controls the width (for left/right anchors) or height (for top/bottom anchors):
539
+
540
+ ```tsx
541
+ // Small drawer (~280px)
542
+ <InsetDrawer size="sm" anchor="right" />
543
+
544
+ // Medium drawer (~400px) - default
545
+ <InsetDrawer size="md" anchor="right" />
546
+
547
+ // Large drawer (~600px)
548
+ <InsetDrawer size="lg" anchor="right" />
549
+ ```
550
+
551
+ ### Custom Sizing
552
+
553
+ For more control, use `Sheet` with custom styles:
554
+
555
+ ```tsx
556
+ <InsetDrawer open={open} anchor="right">
557
+ <Sheet sx={{ width: { xs: '100vw', sm: 450 }, height: '100%' }}>
558
+ Content
559
+ </Sheet>
560
+ </InsetDrawer>
561
+ ```
562
+
563
+ ### Drawer Content Structure
564
+
565
+ Use these components for consistent drawer layout:
566
+
567
+ ```tsx
568
+ <InsetDrawer open={open}>
569
+ <Sheet sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
570
+ {/* Header */}
571
+ <Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
572
+ <DialogTitle>Drawer Title</DialogTitle>
573
+ <ModalClose />
574
+ </Box>
575
+
576
+ {/* Scrollable Content */}
577
+ <DialogContent sx={{ flex: 1, overflow: 'auto', p: 2 }}>
578
+ {/* Main content */}
579
+ </DialogContent>
580
+
581
+ {/* Footer */}
582
+ <Box sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
583
+ <Stack direction="row" gap={1}>
584
+ <Button variant="outlined">Cancel</Button>
585
+ <Button>Save</Button>
586
+ </Stack>
587
+ </Box>
588
+ </Sheet>
589
+ </InsetDrawer>
590
+ ```
591
+
592
+ ## Accessibility
593
+
594
+ InsetDrawer includes built-in accessibility features:
595
+
596
+ ### ARIA Attributes
597
+
598
+ - Drawer container has appropriate `role="dialog"` or `role="presentation"`
599
+ - Focus is trapped within the drawer when open
600
+ - Proper `aria-modal` attribute when modal
601
+
602
+ ### Keyboard Navigation
603
+
604
+ - **Escape**: Close the drawer
605
+ - **Tab**: Navigate between focusable elements within drawer
606
+ - **Shift + Tab**: Navigate backwards
607
+
608
+ ### Focus Management
609
+
610
+ ```tsx
611
+ // Focus automatically moves to drawer content when opened
612
+ <InsetDrawer
613
+ open={open}
614
+ onClose={onClose}
615
+ // Focus is trapped within drawer
616
+ >
617
+ <Sheet>
618
+ {/* First focusable element receives focus */}
619
+ <Input autoFocus placeholder="Search..." />
620
+ </Sheet>
621
+ </InsetDrawer>
622
+ ```
623
+
624
+ ### Screen Reader Support
625
+
626
+ ```tsx
627
+ // Provide descriptive title for screen readers
628
+ <InsetDrawer open={open}>
629
+ <Sheet role="dialog" aria-labelledby="drawer-title">
630
+ <DialogTitle id="drawer-title">Filters</DialogTitle>
631
+ {/* Content */}
632
+ </Sheet>
633
+ </InsetDrawer>
634
+ ```
635
+
380
636
  ## Best Practices
381
637
 
382
- 1. **Use appropriate anchor positions**: Match the drawer position to the content type for intuitive UX.
638
+ ### Do
639
+
640
+ 1. **Use appropriate anchor positions**: Match drawer position to content type
383
641
 
384
642
  ```tsx
385
- // ✅ Good: Navigation on the left, details on the right
386
- <InsetDrawer anchor="left"> {/* Navigation menu */}
387
- <InsetDrawer anchor="right"> {/* Detail panel or filters */}
388
- <InsetDrawer anchor="bottom">{/* Mobile action sheet */}
643
+ // ✅ Good: Navigation on left, details on right
644
+ <InsetDrawer anchor="left"> {/* Navigation */}
645
+ <InsetDrawer anchor="right"> {/* Details/filters */}
646
+ <InsetDrawer anchor="bottom">{/* Mobile actions */}
389
647
  ```
390
648
 
391
- 2. **Provide clear close affordances**: Always give users multiple ways to close the drawer -- a close button, a cancel button, and backdrop click.
649
+ 2. **Provide clear close affordances**: Include close button and backdrop click
392
650
 
393
651
  ```tsx
394
- // ✅ Good: Multiple close methods
652
+ // ✅ Good: Multiple ways to close
395
653
  <InsetDrawer open={open} onClose={handleClose}>
396
654
  <Sheet>
397
- <ModalClose />
398
- <Button onClick={handleClose}>Cancel</Button>
655
+ <ModalClose /> {/* X button */}
656
+ <Button onClick={handleClose}>Cancel</Button> {/* Cancel button */}
399
657
  </Sheet>
400
658
  </InsetDrawer>
401
659
  ```
402
660
 
403
- 3. **Structure content consistently**: Use a header/body/footer layout with clear dividers so users can quickly scan the drawer's content.
661
+ 3. **Use consistent header/footer patterns**: Structure content predictably
404
662
 
405
- 4. **Do not nest drawers**: Opening a drawer inside another drawer creates a confusing experience.
663
+ ```tsx
664
+ // ✅ Good: Clear structure
665
+ <Sheet>
666
+ <Box sx={{ borderBottom: '1px solid', borderColor: 'divider' }}>
667
+ <DialogTitle>Title</DialogTitle>
668
+ <ModalClose />
669
+ </Box>
670
+ <DialogContent>{/* Scrollable content */}</DialogContent>
671
+ <Box sx={{ borderTop: '1px solid', borderColor: 'divider' }}>
672
+ {/* Action buttons */}
673
+ </Box>
674
+ </Sheet>
675
+ ```
676
+
677
+ 4. **Handle mobile responsiveness**: Full width on small screens
678
+
679
+ ```tsx
680
+ // ✅ Good: Responsive width
681
+ <Sheet sx={{ width: { xs: '100vw', sm: 400 } }}>
682
+ ```
683
+
684
+ ### ❌ Don't
685
+
686
+ 1. **Don't hide critical content**: Keep essential features accessible
406
687
 
407
688
  ```tsx
408
- // ❌ Bad: Nested drawers
689
+ // ❌ Bad: Primary action hidden in drawer
409
690
  <InsetDrawer>
410
- <InsetDrawer>{/* Avoid this pattern */}</InsetDrawer>
691
+ <Button>Complete Purchase</Button> {/* Should be on main page */}
411
692
  </InsetDrawer>
412
693
  ```
413
694
 
414
- 5. **Do not use for confirmations**: Use Dialog or Modal for simple confirmation prompts. InsetDrawer is designed for rich, scrollable content.
695
+ 2. **Don't nest drawers**: Leads to confusing UX
415
696
 
416
- ## Accessibility
697
+ ```tsx
698
+ // ❌ Bad: Drawer within drawer
699
+ <InsetDrawer>
700
+ <Button onClick={() => setNestedOpen(true)}>Open Another</Button>
701
+ <InsetDrawer open={nestedOpen}>{/* Nested drawer */}</InsetDrawer>
702
+ </InsetDrawer>
703
+ ```
704
+
705
+ 3. **Don't use for confirmations**: Use Dialog instead
706
+
707
+ ```tsx
708
+ // ❌ Bad: Drawer for confirmation
709
+ <InsetDrawer>
710
+ <Typography>Are you sure?</Typography>
711
+ <Button>Confirm</Button>
712
+ </InsetDrawer>
713
+
714
+ // ✅ Good: Use Dialog
715
+ <Dialog>
716
+ <Typography>Are you sure?</Typography>
717
+ <Button>Confirm</Button>
718
+ </Dialog>
719
+ ```
720
+
721
+ 4. **Don't forget about keyboard users**: Ensure proper focus management
722
+
723
+ ## Performance Considerations
724
+
725
+ ### Lazy Loading Content
726
+
727
+ For drawers with heavy content, load data when drawer opens:
728
+
729
+ ```tsx
730
+ function DetailDrawer({ open, itemId }) {
731
+ const [data, setData] = useState(null);
732
+
733
+ useEffect(() => {
734
+ if (open && itemId) {
735
+ fetchItemDetails(itemId).then(setData);
736
+ }
737
+ }, [open, itemId]);
738
+
739
+ return (
740
+ <InsetDrawer open={open}>
741
+ <Sheet>
742
+ {data ? <ItemDetails data={data} /> : <Skeleton />}
743
+ </Sheet>
744
+ </InsetDrawer>
745
+ );
746
+ }
747
+ ```
748
+
749
+ ### Memoize Handlers
750
+
751
+ ```tsx
752
+ const handleClose = useCallback(() => {
753
+ setOpen(false);
754
+ }, []);
755
+
756
+ const handleApply = useCallback((filters) => {
757
+ applyFilters(filters);
758
+ setOpen(false);
759
+ }, [applyFilters]);
760
+
761
+ <InsetDrawer open={open} onClose={handleClose}>
762
+ <FilterPanel onApply={handleApply} />
763
+ </InsetDrawer>
764
+ ```
765
+
766
+ ### Unmount When Closed
767
+
768
+ For memory-intensive content, conditionally render:
769
+
770
+ ```tsx
771
+ {open && (
772
+ <InsetDrawer open={open} onClose={handleClose}>
773
+ <HeavyContentComponent />
774
+ </InsetDrawer>
775
+ )}
776
+ ```
777
+
778
+ ### Avoid Layout Shifts
779
+
780
+ Use fixed dimensions to prevent content shifts:
781
+
782
+ ```tsx
783
+ <Sheet sx={{
784
+ width: 400,
785
+ height: '100%',
786
+ // Content won't cause width changes
787
+ }}>
788
+ ```
417
789
 
418
- - InsetDrawer traps focus within the drawer when open, ensuring keyboard users cannot accidentally interact with content behind it.
419
- - Pressing **Escape** closes the drawer, and **Tab** / **Shift+Tab** cycles through focusable elements inside.
420
- - Use `DialogTitle` with a matching `id` and `aria-labelledby` on the Sheet to provide a descriptive label for screen readers.
421
- - When the drawer closes, focus returns to the element that triggered it, maintaining a predictable navigation flow.
790
+ 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.