@fr0mpy/component-system 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/bin/cli.js +283 -0
  2. package/index.js +12 -0
  3. package/package.json +45 -0
  4. package/templates/commands/component-harness.md +116 -0
  5. package/templates/commands/setup-styling.md +111 -0
  6. package/templates/component-recipes/accordion.md +153 -0
  7. package/templates/component-recipes/alert.md +145 -0
  8. package/templates/component-recipes/avatar.md +165 -0
  9. package/templates/component-recipes/badge.md +126 -0
  10. package/templates/component-recipes/breadcrumb.md +220 -0
  11. package/templates/component-recipes/button.md +90 -0
  12. package/templates/component-recipes/card.md +130 -0
  13. package/templates/component-recipes/carousel.md +277 -0
  14. package/templates/component-recipes/checkbox.md +117 -0
  15. package/templates/component-recipes/collapsible.md +201 -0
  16. package/templates/component-recipes/combobox.md +193 -0
  17. package/templates/component-recipes/context-menu.md +254 -0
  18. package/templates/component-recipes/dialog.md +193 -0
  19. package/templates/component-recipes/drawer.md +196 -0
  20. package/templates/component-recipes/dropdown-menu.md +263 -0
  21. package/templates/component-recipes/hover-card.md +230 -0
  22. package/templates/component-recipes/input.md +113 -0
  23. package/templates/component-recipes/label.md +259 -0
  24. package/templates/component-recipes/modal.md +155 -0
  25. package/templates/component-recipes/navigation-menu.md +310 -0
  26. package/templates/component-recipes/pagination.md +223 -0
  27. package/templates/component-recipes/popover.md +156 -0
  28. package/templates/component-recipes/progress.md +185 -0
  29. package/templates/component-recipes/radio.md +148 -0
  30. package/templates/component-recipes/select.md +154 -0
  31. package/templates/component-recipes/separator.md +124 -0
  32. package/templates/component-recipes/skeleton.md +186 -0
  33. package/templates/component-recipes/slider.md +114 -0
  34. package/templates/component-recipes/spinner.md +225 -0
  35. package/templates/component-recipes/switch.md +100 -0
  36. package/templates/component-recipes/table.md +161 -0
  37. package/templates/component-recipes/tabs.md +145 -0
  38. package/templates/component-recipes/textarea.md +234 -0
  39. package/templates/component-recipes/toast.md +209 -0
  40. package/templates/component-recipes/toggle-group.md +216 -0
  41. package/templates/component-recipes/tooltip.md +115 -0
  42. package/templates/hooks/triggers.d/styling.json +23 -0
  43. package/templates/skills/styling.md +173 -0
@@ -0,0 +1,254 @@
1
+ # Context Menu Component Recipe
2
+
3
+ ## Structure
4
+ - Right-click triggered menu
5
+ - Same item types as Dropdown Menu
6
+ - Support for submenus
7
+ - Keyboard shortcuts display
8
+ - Checkboxes and radio items
9
+
10
+ ## Tailwind Classes
11
+
12
+ ### Trigger (wrapper around target element)
13
+ ```
14
+ (invisible wrapper, no styles)
15
+ ```
16
+
17
+ ### Content
18
+ ```
19
+ z-50 min-w-[8rem] overflow-hidden {tokens.radius} border border-border
20
+ bg-background p-1 text-foreground {tokens.shadow}
21
+ animate-in fade-in-80
22
+ data-[state=open]:animate-in data-[state=closed]:animate-out
23
+ data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
24
+ data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95
25
+ data-[side=bottom]:slide-in-from-top-2
26
+ data-[side=left]:slide-in-from-right-2
27
+ data-[side=right]:slide-in-from-left-2
28
+ data-[side=top]:slide-in-from-bottom-2
29
+ ```
30
+
31
+ ### Item
32
+ ```
33
+ relative flex cursor-pointer select-none items-center {tokens.radius} px-2 py-1.5
34
+ text-sm outline-none
35
+ focus:bg-muted focus:text-foreground
36
+ data-[disabled]:pointer-events-none data-[disabled]:opacity-50
37
+ ```
38
+
39
+ ### Item with Inset (for alignment with checkbox items)
40
+ ```
41
+ pl-8
42
+ ```
43
+
44
+ ### Checkbox Item
45
+ ```
46
+ relative flex cursor-pointer select-none items-center {tokens.radius} py-1.5 pl-8 pr-2
47
+ text-sm outline-none
48
+ focus:bg-muted focus:text-foreground
49
+ data-[disabled]:pointer-events-none data-[disabled]:opacity-50
50
+ ```
51
+
52
+ ### Radio Item
53
+ ```
54
+ relative flex cursor-pointer select-none items-center {tokens.radius} py-1.5 pl-8 pr-2
55
+ text-sm outline-none
56
+ focus:bg-muted focus:text-foreground
57
+ data-[disabled]:pointer-events-none data-[disabled]:opacity-50
58
+ ```
59
+
60
+ ### Item Indicator
61
+ ```
62
+ absolute left-2 flex h-3.5 w-3.5 items-center justify-center
63
+ ```
64
+
65
+ ### Label
66
+ ```
67
+ px-2 py-1.5 text-sm font-semibold text-foreground
68
+ ```
69
+
70
+ ### Separator
71
+ ```
72
+ -mx-1 my-1 h-px bg-border
73
+ ```
74
+
75
+ ### Shortcut
76
+ ```
77
+ ml-auto text-xs tracking-widest text-muted-foreground
78
+ ```
79
+
80
+ ### Sub Trigger
81
+ ```
82
+ flex cursor-pointer select-none items-center {tokens.radius} px-2 py-1.5
83
+ text-sm outline-none
84
+ focus:bg-muted
85
+ data-[state=open]:bg-muted
86
+ ```
87
+
88
+ ### Sub Content
89
+ ```
90
+ z-50 min-w-[8rem] overflow-hidden {tokens.radius} border border-border
91
+ bg-background p-1 {tokens.shadow}
92
+ data-[state=open]:animate-in data-[state=closed]:animate-out
93
+ ```
94
+
95
+ ## Props Interface
96
+ ```typescript
97
+ interface ContextMenuProps {
98
+ children: React.ReactNode
99
+ onOpenChange?: (open: boolean) => void
100
+ }
101
+
102
+ interface ContextMenuTriggerProps {
103
+ children: React.ReactNode
104
+ disabled?: boolean
105
+ asChild?: boolean
106
+ }
107
+
108
+ interface ContextMenuContentProps {
109
+ className?: string
110
+ alignOffset?: number
111
+ children: React.ReactNode
112
+ }
113
+
114
+ interface ContextMenuItemProps {
115
+ inset?: boolean
116
+ disabled?: boolean
117
+ onSelect?: () => void
118
+ children: React.ReactNode
119
+ }
120
+
121
+ // Same as DropdownMenu for CheckboxItem, RadioGroup, RadioItem, etc.
122
+ ```
123
+
124
+ ## Do
125
+ - Use Radix ContextMenu for accessibility
126
+ - Support all item types (checkbox, radio, sub-menus)
127
+ - Show keyboard shortcuts
128
+ - Handle touch devices (long-press)
129
+
130
+ ## Don't
131
+ - Hardcode colors
132
+ - Use for primary navigation
133
+ - Forget disabled states
134
+ - Skip keyboard navigation
135
+
136
+ ## Example
137
+ ```tsx
138
+ import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'
139
+ import { Check, ChevronRight, Circle } from 'lucide-react'
140
+ import { cn } from '@/lib/utils'
141
+
142
+ const ContextMenu = ContextMenuPrimitive.Root
143
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger
144
+ const ContextMenuGroup = ContextMenuPrimitive.Group
145
+ const ContextMenuPortal = ContextMenuPrimitive.Portal
146
+ const ContextMenuSub = ContextMenuPrimitive.Sub
147
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
148
+
149
+ const ContextMenuContent = ({ className, ...props }) => (
150
+ <ContextMenuPrimitive.Portal>
151
+ <ContextMenuPrimitive.Content
152
+ className={cn(
153
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-background p-1 shadow-md',
154
+ 'animate-in fade-in-80',
155
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out',
156
+ 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
157
+ 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
158
+ 'data-[side=bottom]:slide-in-from-top-2',
159
+ className
160
+ )}
161
+ {...props}
162
+ />
163
+ </ContextMenuPrimitive.Portal>
164
+ )
165
+
166
+ const ContextMenuItem = ({ className, inset, ...props }) => (
167
+ <ContextMenuPrimitive.Item
168
+ className={cn(
169
+ 'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
170
+ 'focus:bg-muted focus:text-foreground',
171
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
172
+ inset && 'pl-8',
173
+ className
174
+ )}
175
+ {...props}
176
+ />
177
+ )
178
+
179
+ const ContextMenuCheckboxItem = ({ className, checked, children, ...props }) => (
180
+ <ContextMenuPrimitive.CheckboxItem
181
+ className={cn(
182
+ 'relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none',
183
+ 'focus:bg-muted focus:text-foreground',
184
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
185
+ className
186
+ )}
187
+ checked={checked}
188
+ {...props}
189
+ >
190
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
191
+ <ContextMenuPrimitive.ItemIndicator>
192
+ <Check className="h-4 w-4" />
193
+ </ContextMenuPrimitive.ItemIndicator>
194
+ </span>
195
+ {children}
196
+ </ContextMenuPrimitive.CheckboxItem>
197
+ )
198
+
199
+ const ContextMenuRadioItem = ({ className, children, ...props }) => (
200
+ <ContextMenuPrimitive.RadioItem
201
+ className={cn(
202
+ 'relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none',
203
+ 'focus:bg-muted focus:text-foreground',
204
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
205
+ className
206
+ )}
207
+ {...props}
208
+ >
209
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
210
+ <ContextMenuPrimitive.ItemIndicator>
211
+ <Circle className="h-2 w-2 fill-current" />
212
+ </ContextMenuPrimitive.ItemIndicator>
213
+ </span>
214
+ {children}
215
+ </ContextMenuPrimitive.RadioItem>
216
+ )
217
+
218
+ const ContextMenuLabel = ({ className, inset, ...props }) => (
219
+ <ContextMenuPrimitive.Label
220
+ className={cn('px-2 py-1.5 text-sm font-semibold text-foreground', inset && 'pl-8', className)}
221
+ {...props}
222
+ />
223
+ )
224
+
225
+ const ContextMenuSeparator = ({ className, ...props }) => (
226
+ <ContextMenuPrimitive.Separator className={cn('-mx-1 my-1 h-px bg-border', className)} {...props} />
227
+ )
228
+
229
+ const ContextMenuShortcut = ({ className, ...props }) => (
230
+ <span className={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)} {...props} />
231
+ )
232
+
233
+ // Usage
234
+ <ContextMenu>
235
+ <ContextMenuTrigger className="flex h-32 w-full items-center justify-center rounded-md border border-dashed">
236
+ Right click here
237
+ </ContextMenuTrigger>
238
+ <ContextMenuContent className="w-64">
239
+ <ContextMenuItem>
240
+ Back
241
+ <ContextMenuShortcut>⌘[</ContextMenuShortcut>
242
+ </ContextMenuItem>
243
+ <ContextMenuItem disabled>
244
+ Forward
245
+ <ContextMenuShortcut>⌘]</ContextMenuShortcut>
246
+ </ContextMenuItem>
247
+ <ContextMenuSeparator />
248
+ <ContextMenuCheckboxItem checked>
249
+ Show Bookmarks
250
+ <ContextMenuShortcut>⌘⇧B</ContextMenuShortcut>
251
+ </ContextMenuCheckboxItem>
252
+ </ContextMenuContent>
253
+ </ContextMenu>
254
+ ```
@@ -0,0 +1,193 @@
1
+ # Dialog Component Recipe
2
+
3
+ ## Structure
4
+ - Same as Modal but semantically for confirmations/actions
5
+ - Typically smaller, more focused
6
+ - Usually has cancel + confirm actions
7
+ - Used for alerts, confirmations, forms
8
+
9
+ ## Tailwind Classes
10
+
11
+ ### Overlay
12
+ ```
13
+ fixed inset-0 z-50 bg-black/80
14
+ data-[state=open]:animate-in data-[state=closed]:animate-out
15
+ data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
16
+ ```
17
+
18
+ ### Content
19
+ ```
20
+ fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2
21
+ gap-4 border border-border bg-background p-6 {tokens.shadow} {tokens.radius}
22
+ duration-200
23
+ data-[state=open]:animate-in data-[state=closed]:animate-out
24
+ data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
25
+ data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95
26
+ data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]
27
+ data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]
28
+ ```
29
+
30
+ ### Header
31
+ ```
32
+ flex flex-col space-y-1.5 text-center sm:text-left
33
+ ```
34
+
35
+ ### Footer
36
+ ```
37
+ flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2
38
+ ```
39
+
40
+ ### Title
41
+ ```
42
+ text-lg font-semibold leading-none tracking-tight
43
+ ```
44
+
45
+ ### Description
46
+ ```
47
+ text-sm text-muted-foreground
48
+ ```
49
+
50
+ ### Close Button
51
+ ```
52
+ absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background
53
+ transition-opacity hover:opacity-100
54
+ focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2
55
+ disabled:pointer-events-none
56
+ data-[state=open]:bg-accent data-[state=open]:text-muted-foreground
57
+ ```
58
+
59
+ ## Alert Dialog Variant (for destructive confirmations)
60
+ ```
61
+ Same structure but:
62
+ - No close button (must make explicit choice)
63
+ - Cannot close by clicking overlay
64
+ - Destructive action uses destructive button variant
65
+ ```
66
+
67
+ ## Props Interface
68
+ ```typescript
69
+ interface DialogProps {
70
+ open?: boolean
71
+ onOpenChange?: (open: boolean) => void
72
+ children: React.ReactNode
73
+ }
74
+
75
+ interface DialogContentProps {
76
+ className?: string
77
+ children: React.ReactNode
78
+ }
79
+
80
+ interface DialogHeaderProps {
81
+ className?: string
82
+ children: React.ReactNode
83
+ }
84
+
85
+ interface DialogFooterProps {
86
+ className?: string
87
+ children: React.ReactNode
88
+ }
89
+
90
+ interface DialogTitleProps {
91
+ className?: string
92
+ children: React.ReactNode
93
+ }
94
+
95
+ interface DialogDescriptionProps {
96
+ className?: string
97
+ children: React.ReactNode
98
+ }
99
+ ```
100
+
101
+ ## Do
102
+ - Use for focused interactions requiring user decision
103
+ - Include clear action buttons
104
+ - Use AlertDialog for destructive actions
105
+ - Focus first interactive element on open
106
+
107
+ ## Don't
108
+ - Hardcode colors
109
+ - Use for complex multi-step flows (use full modal)
110
+ - Allow dismissal of AlertDialog by clicking outside
111
+ - Forget keyboard navigation
112
+
113
+ ## Example
114
+ ```tsx
115
+ import * as DialogPrimitive from '@radix-ui/react-dialog'
116
+ import { X } from 'lucide-react'
117
+ import { cn } from '@/lib/utils'
118
+
119
+ const Dialog = DialogPrimitive.Root
120
+ const DialogTrigger = DialogPrimitive.Trigger
121
+ const DialogPortal = DialogPrimitive.Portal
122
+ const DialogClose = DialogPrimitive.Close
123
+
124
+ const DialogOverlay = ({ className, ...props }) => (
125
+ <DialogPrimitive.Overlay
126
+ className={cn(
127
+ 'fixed inset-0 z-50 bg-black/80',
128
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out',
129
+ 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
130
+ className
131
+ )}
132
+ {...props}
133
+ />
134
+ )
135
+
136
+ const DialogContent = ({ className, children, ...props }) => (
137
+ <DialogPortal>
138
+ <DialogOverlay />
139
+ <DialogPrimitive.Content
140
+ className={cn(
141
+ 'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2',
142
+ 'gap-4 border border-border bg-background p-6 shadow-lg rounded-lg duration-200',
143
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out',
144
+ 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
145
+ 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
146
+ className
147
+ )}
148
+ {...props}
149
+ >
150
+ {children}
151
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-primary">
152
+ <X className="h-4 w-4" />
153
+ <span className="sr-only">Close</span>
154
+ </DialogPrimitive.Close>
155
+ </DialogPrimitive.Content>
156
+ </DialogPortal>
157
+ )
158
+
159
+ const DialogHeader = ({ className, ...props }) => (
160
+ <div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
161
+ )
162
+
163
+ const DialogFooter = ({ className, ...props }) => (
164
+ <div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...props} />
165
+ )
166
+
167
+ const DialogTitle = ({ className, ...props }) => (
168
+ <DialogPrimitive.Title className={cn('text-lg font-semibold leading-none tracking-tight', className)} {...props} />
169
+ )
170
+
171
+ const DialogDescription = ({ className, ...props }) => (
172
+ <DialogPrimitive.Description className={cn('text-sm text-muted-foreground', className)} {...props} />
173
+ )
174
+
175
+ // Usage
176
+ <Dialog>
177
+ <DialogTrigger asChild>
178
+ <Button variant="outline">Edit Profile</Button>
179
+ </DialogTrigger>
180
+ <DialogContent>
181
+ <DialogHeader>
182
+ <DialogTitle>Edit profile</DialogTitle>
183
+ <DialogDescription>Make changes to your profile here.</DialogDescription>
184
+ </DialogHeader>
185
+ <div className="grid gap-4 py-4">
186
+ {/* Form fields */}
187
+ </div>
188
+ <DialogFooter>
189
+ <Button type="submit">Save changes</Button>
190
+ </DialogFooter>
191
+ </DialogContent>
192
+ </Dialog>
193
+ ```
@@ -0,0 +1,196 @@
1
+ # Drawer Component Recipe
2
+
3
+ ## Structure
4
+ - Slide-out panel from edge of screen
5
+ - Support for left, right, top, bottom positions
6
+ - Overlay backdrop
7
+ - Optional close button
8
+ - Content area with header/body/footer
9
+
10
+ ## Tailwind Classes
11
+
12
+ ### Overlay
13
+ ```
14
+ fixed inset-0 z-50 bg-black/80
15
+ data-[state=open]:animate-in data-[state=closed]:animate-out
16
+ data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
17
+ ```
18
+
19
+ ### Content (Base)
20
+ ```
21
+ fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out
22
+ data-[state=open]:animate-in data-[state=closed]:animate-out
23
+ data-[state=closed]:duration-300 data-[state=open]:duration-500
24
+ ```
25
+
26
+ ### Position Variants
27
+ ```
28
+ left:
29
+ inset-y-0 left-0 h-full w-3/4 sm:max-w-sm border-r border-border
30
+ data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left
31
+
32
+ right:
33
+ inset-y-0 right-0 h-full w-3/4 sm:max-w-sm border-l border-border
34
+ data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right
35
+
36
+ top:
37
+ inset-x-0 top-0 border-b border-border
38
+ data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top
39
+
40
+ bottom:
41
+ inset-x-0 bottom-0 border-t border-border
42
+ data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom
43
+ ```
44
+
45
+ ### Header
46
+ ```
47
+ flex flex-col space-y-2
48
+ ```
49
+
50
+ ### Title
51
+ ```
52
+ text-lg font-semibold text-foreground
53
+ ```
54
+
55
+ ### Description
56
+ ```
57
+ text-sm text-muted-foreground
58
+ ```
59
+
60
+ ### Footer
61
+ ```
62
+ flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2
63
+ ```
64
+
65
+ ### Close Button
66
+ ```
67
+ absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background
68
+ transition-opacity hover:opacity-100
69
+ focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2
70
+ ```
71
+
72
+ ## Props Interface
73
+ ```typescript
74
+ interface DrawerProps {
75
+ open?: boolean
76
+ onOpenChange?: (open: boolean) => void
77
+ side?: 'left' | 'right' | 'top' | 'bottom'
78
+ children: React.ReactNode
79
+ }
80
+
81
+ interface DrawerContentProps {
82
+ className?: string
83
+ children: React.ReactNode
84
+ }
85
+
86
+ interface DrawerHeaderProps {
87
+ className?: string
88
+ children: React.ReactNode
89
+ }
90
+
91
+ interface DrawerTitleProps {
92
+ className?: string
93
+ children: React.ReactNode
94
+ }
95
+
96
+ interface DrawerDescriptionProps {
97
+ className?: string
98
+ children: React.ReactNode
99
+ }
100
+
101
+ interface DrawerFooterProps {
102
+ className?: string
103
+ children: React.ReactNode
104
+ }
105
+ ```
106
+
107
+ ## Do
108
+ - Use Radix Dialog as base (or vaul for mobile-friendly)
109
+ - Trap focus within drawer when open
110
+ - Close on Escape key
111
+ - Close on overlay click
112
+ - Support swipe-to-close on mobile
113
+
114
+ ## Don't
115
+ - Hardcode colors or dimensions
116
+ - Forget scroll handling (body scroll lock)
117
+ - Skip keyboard accessibility
118
+ - Allow content behind to be interactive
119
+
120
+ ## Example
121
+ ```tsx
122
+ import * as DialogPrimitive from '@radix-ui/react-dialog'
123
+ import { X } from 'lucide-react'
124
+ import { cn } from '@/lib/utils'
125
+
126
+ const Drawer = DialogPrimitive.Root
127
+ const DrawerTrigger = DialogPrimitive.Trigger
128
+ const DrawerClose = DialogPrimitive.Close
129
+ const DrawerPortal = DialogPrimitive.Portal
130
+
131
+ const drawerSideVariants = {
132
+ left: 'inset-y-0 left-0 h-full w-3/4 sm:max-w-sm border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left',
133
+ right: 'inset-y-0 right-0 h-full w-3/4 sm:max-w-sm border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
134
+ top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
135
+ bottom: 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
136
+ }
137
+
138
+ const DrawerOverlay = ({ className, ...props }) => (
139
+ <DialogPrimitive.Overlay
140
+ className={cn(
141
+ 'fixed inset-0 z-50 bg-black/80',
142
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out',
143
+ 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
144
+ className
145
+ )}
146
+ {...props}
147
+ />
148
+ )
149
+
150
+ const DrawerContent = ({ side = 'right', className, children, ...props }) => (
151
+ <DrawerPortal>
152
+ <DrawerOverlay />
153
+ <DialogPrimitive.Content
154
+ className={cn(
155
+ 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out',
156
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out',
157
+ 'data-[state=closed]:duration-300 data-[state=open]:duration-500',
158
+ drawerSideVariants[side],
159
+ className
160
+ )}
161
+ {...props}
162
+ >
163
+ {children}
164
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-primary">
165
+ <X className="h-4 w-4" />
166
+ <span className="sr-only">Close</span>
167
+ </DialogPrimitive.Close>
168
+ </DialogPrimitive.Content>
169
+ </DrawerPortal>
170
+ )
171
+
172
+ const DrawerHeader = ({ className, ...props }) => (
173
+ <div className={cn('flex flex-col space-y-2', className)} {...props} />
174
+ )
175
+
176
+ const DrawerTitle = ({ className, ...props }) => (
177
+ <DialogPrimitive.Title
178
+ className={cn('text-lg font-semibold text-foreground', className)}
179
+ {...props}
180
+ />
181
+ )
182
+
183
+ const DrawerDescription = ({ className, ...props }) => (
184
+ <DialogPrimitive.Description
185
+ className={cn('text-sm text-muted-foreground', className)}
186
+ {...props}
187
+ />
188
+ )
189
+
190
+ const DrawerFooter = ({ className, ...props }) => (
191
+ <div
192
+ className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
193
+ {...props}
194
+ />
195
+ )
196
+ ```