@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,310 @@
1
+ # Navigation Menu Component Recipe
2
+
3
+ ## Structure
4
+ - Horizontal navigation bar
5
+ - Support for dropdown menus with content panels
6
+ - Trigger items with optional indicators
7
+ - Content areas for mega-menu style layouts
8
+ - Animated transitions for open/close
9
+
10
+ ## Tailwind Classes
11
+
12
+ ### Root
13
+ ```
14
+ relative z-10 flex max-w-max flex-1 items-center justify-center
15
+ ```
16
+
17
+ ### List
18
+ ```
19
+ group flex flex-1 list-none items-center justify-center space-x-1
20
+ ```
21
+
22
+ ### Trigger
23
+ ```
24
+ group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2
25
+ text-sm font-medium transition-colors
26
+ hover:bg-muted hover:text-foreground
27
+ focus:bg-muted focus:text-foreground focus:outline-none
28
+ disabled:pointer-events-none disabled:opacity-50
29
+ data-[state=open]:bg-muted/50
30
+ ```
31
+
32
+ ### Trigger Indicator (Chevron)
33
+ ```
34
+ relative top-[1px] ml-1 h-3 w-3 transition duration-300
35
+ group-data-[state=open]:rotate-180
36
+ ```
37
+
38
+ ### Content
39
+ ```
40
+ left-0 top-0 w-full
41
+ data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out
42
+ data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out
43
+ data-[motion=from-end]:slide-in-from-right-52
44
+ data-[motion=from-start]:slide-in-from-left-52
45
+ data-[motion=to-end]:slide-out-to-right-52
46
+ data-[motion=to-start]:slide-out-to-left-52
47
+ md:absolute md:w-auto
48
+ ```
49
+
50
+ ### Viewport
51
+ ```
52
+ origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)]
53
+ w-full overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-lg
54
+ data-[state=open]:animate-in data-[state=closed]:animate-out
55
+ data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90
56
+ md:w-[var(--radix-navigation-menu-viewport-width)]
57
+ ```
58
+
59
+ ### Link
60
+ ```
61
+ block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none
62
+ transition-colors hover:bg-muted hover:text-foreground focus:bg-muted focus:text-foreground
63
+ ```
64
+
65
+ ### Link Title
66
+ ```
67
+ text-sm font-medium leading-none
68
+ ```
69
+
70
+ ### Link Description
71
+ ```
72
+ line-clamp-2 text-sm leading-snug text-muted-foreground
73
+ ```
74
+
75
+ ### Indicator
76
+ ```
77
+ top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden
78
+ data-[state=visible]:animate-in data-[state=hidden]:animate-out
79
+ data-[state=hidden]:fade-out data-[state=visible]:fade-in
80
+ ```
81
+
82
+ ### Indicator Arrow
83
+ ```
84
+ relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md
85
+ ```
86
+
87
+ ## Props Interface
88
+ ```typescript
89
+ interface NavigationMenuProps {
90
+ children: React.ReactNode
91
+ className?: string
92
+ orientation?: 'horizontal' | 'vertical'
93
+ delayDuration?: number
94
+ skipDelayDuration?: number
95
+ }
96
+
97
+ interface NavigationMenuListProps {
98
+ children: React.ReactNode
99
+ className?: string
100
+ }
101
+
102
+ interface NavigationMenuItemProps {
103
+ children: React.ReactNode
104
+ value?: string
105
+ }
106
+
107
+ interface NavigationMenuTriggerProps {
108
+ children: React.ReactNode
109
+ className?: string
110
+ }
111
+
112
+ interface NavigationMenuContentProps {
113
+ children: React.ReactNode
114
+ className?: string
115
+ }
116
+
117
+ interface NavigationMenuLinkProps {
118
+ href: string
119
+ children: React.ReactNode
120
+ className?: string
121
+ active?: boolean
122
+ }
123
+ ```
124
+
125
+ ## Do
126
+ - Use Radix NavigationMenu primitive
127
+ - Support keyboard navigation (arrow keys)
128
+ - Include proper ARIA attributes
129
+ - Allow for mega-menu content layouts
130
+ - Handle hover/focus delays appropriately
131
+
132
+ ## Don't
133
+ - Hardcode colors or sizes
134
+ - Nest navigation menus
135
+ - Skip focus management
136
+ - Forget mobile responsiveness (consider hamburger on small screens)
137
+
138
+ ## Example
139
+ ```tsx
140
+ import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu'
141
+ import { ChevronDown } from 'lucide-react'
142
+ import { cn } from '@/lib/utils'
143
+
144
+ const NavigationMenu = ({ className, children, ...props }) => (
145
+ <NavigationMenuPrimitive.Root
146
+ className={cn(
147
+ 'relative z-10 flex max-w-max flex-1 items-center justify-center',
148
+ className
149
+ )}
150
+ {...props}
151
+ >
152
+ {children}
153
+ <NavigationMenuViewport />
154
+ </NavigationMenuPrimitive.Root>
155
+ )
156
+
157
+ const NavigationMenuList = ({ className, ...props }) => (
158
+ <NavigationMenuPrimitive.List
159
+ className={cn(
160
+ 'group flex flex-1 list-none items-center justify-center space-x-1',
161
+ className
162
+ )}
163
+ {...props}
164
+ />
165
+ )
166
+
167
+ const NavigationMenuItem = NavigationMenuPrimitive.Item
168
+
169
+ const NavigationMenuTrigger = ({ className, children, ...props }) => (
170
+ <NavigationMenuPrimitive.Trigger
171
+ className={cn(
172
+ 'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2',
173
+ 'text-sm font-medium transition-colors',
174
+ 'hover:bg-muted hover:text-foreground',
175
+ 'focus:bg-muted focus:text-foreground focus:outline-none',
176
+ 'disabled:pointer-events-none disabled:opacity-50',
177
+ 'data-[state=open]:bg-muted/50',
178
+ className
179
+ )}
180
+ {...props}
181
+ >
182
+ {children}
183
+ <ChevronDown
184
+ className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
185
+ aria-hidden="true"
186
+ />
187
+ </NavigationMenuPrimitive.Trigger>
188
+ )
189
+
190
+ const NavigationMenuContent = ({ className, ...props }) => (
191
+ <NavigationMenuPrimitive.Content
192
+ className={cn(
193
+ 'left-0 top-0 w-full',
194
+ 'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out',
195
+ 'data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out',
196
+ 'data-[motion=from-end]:slide-in-from-right-52',
197
+ 'data-[motion=from-start]:slide-in-from-left-52',
198
+ 'data-[motion=to-end]:slide-out-to-right-52',
199
+ 'data-[motion=to-start]:slide-out-to-left-52',
200
+ 'md:absolute md:w-auto',
201
+ className
202
+ )}
203
+ {...props}
204
+ />
205
+ )
206
+
207
+ const NavigationMenuLink = NavigationMenuPrimitive.Link
208
+
209
+ const NavigationMenuViewport = ({ className, ...props }) => (
210
+ <div className="absolute left-0 top-full flex justify-center">
211
+ <NavigationMenuPrimitive.Viewport
212
+ className={cn(
213
+ 'origin-top-center relative mt-1.5',
214
+ 'h-[var(--radix-navigation-menu-viewport-height)]',
215
+ 'w-full overflow-hidden rounded-lg border border-border',
216
+ 'bg-popover text-popover-foreground shadow-lg',
217
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out',
218
+ 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90',
219
+ 'md:w-[var(--radix-navigation-menu-viewport-width)]',
220
+ className
221
+ )}
222
+ {...props}
223
+ />
224
+ </div>
225
+ )
226
+
227
+ const NavigationMenuIndicator = ({ className, ...props }) => (
228
+ <NavigationMenuPrimitive.Indicator
229
+ className={cn(
230
+ 'top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden',
231
+ 'data-[state=visible]:animate-in data-[state=hidden]:animate-out',
232
+ 'data-[state=hidden]:fade-out data-[state=visible]:fade-in',
233
+ className
234
+ )}
235
+ {...props}
236
+ >
237
+ <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
238
+ </NavigationMenuPrimitive.Indicator>
239
+ )
240
+
241
+ // List item component for content
242
+ const ListItem = ({ className, title, children, href, ...props }) => (
243
+ <li>
244
+ <NavigationMenuLink asChild>
245
+ <a
246
+ href={href}
247
+ className={cn(
248
+ 'block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none',
249
+ 'transition-colors hover:bg-muted hover:text-foreground focus:bg-muted focus:text-foreground',
250
+ className
251
+ )}
252
+ {...props}
253
+ >
254
+ <div className="text-sm font-medium leading-none">{title}</div>
255
+ <p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
256
+ {children}
257
+ </p>
258
+ </a>
259
+ </NavigationMenuLink>
260
+ </li>
261
+ )
262
+
263
+ // Usage example
264
+ <NavigationMenu>
265
+ <NavigationMenuList>
266
+ <NavigationMenuItem>
267
+ <NavigationMenuTrigger>Products</NavigationMenuTrigger>
268
+ <NavigationMenuContent>
269
+ <ul className="grid gap-3 p-6 md:w-[400px] lg:w-[500px] lg:grid-cols-2">
270
+ <ListItem href="/products/analytics" title="Analytics">
271
+ Track user behavior and measure performance.
272
+ </ListItem>
273
+ <ListItem href="/products/automation" title="Automation">
274
+ Automate workflows and save time.
275
+ </ListItem>
276
+ <ListItem href="/products/security" title="Security">
277
+ Protect your data with enterprise-grade security.
278
+ </ListItem>
279
+ <ListItem href="/products/integrations" title="Integrations">
280
+ Connect with your favorite tools.
281
+ </ListItem>
282
+ </ul>
283
+ </NavigationMenuContent>
284
+ </NavigationMenuItem>
285
+
286
+ <NavigationMenuItem>
287
+ <NavigationMenuTrigger>Resources</NavigationMenuTrigger>
288
+ <NavigationMenuContent>
289
+ <ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2">
290
+ <ListItem href="/docs" title="Documentation">
291
+ Learn how to use our platform.
292
+ </ListItem>
293
+ <ListItem href="/blog" title="Blog">
294
+ Read the latest updates and tutorials.
295
+ </ListItem>
296
+ </ul>
297
+ </NavigationMenuContent>
298
+ </NavigationMenuItem>
299
+
300
+ <NavigationMenuItem>
301
+ <NavigationMenuLink
302
+ href="/pricing"
303
+ className="inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-muted"
304
+ >
305
+ Pricing
306
+ </NavigationMenuLink>
307
+ </NavigationMenuItem>
308
+ </NavigationMenuList>
309
+ </NavigationMenu>
310
+ ```
@@ -0,0 +1,223 @@
1
+ # Pagination Component Recipe
2
+
3
+ ## Structure
4
+ - Previous/Next navigation buttons
5
+ - Page number buttons
6
+ - Ellipsis for truncated ranges
7
+ - Current page indicator
8
+ - Optional: items per page selector, total count
9
+
10
+ ## Tailwind Classes
11
+
12
+ ### Container
13
+ ```
14
+ flex items-center justify-center space-x-1
15
+ ```
16
+
17
+ ### Navigation Button (Previous/Next)
18
+ ```
19
+ inline-flex items-center justify-center whitespace-nowrap {tokens.radius} text-sm font-medium
20
+ h-9 px-3 gap-1
21
+ border border-border bg-transparent
22
+ hover:bg-muted hover:text-foreground
23
+ disabled:pointer-events-none disabled:opacity-50
24
+ ```
25
+
26
+ ### Page Button
27
+ ```
28
+ inline-flex items-center justify-center whitespace-nowrap {tokens.radius} text-sm font-medium
29
+ h-9 w-9
30
+ border border-border bg-transparent
31
+ hover:bg-muted hover:text-foreground
32
+ ```
33
+
34
+ ### Active Page Button
35
+ ```
36
+ border-primary bg-primary text-primary-foreground
37
+ hover:bg-primary/90 hover:text-primary-foreground
38
+ ```
39
+
40
+ ### Ellipsis
41
+ ```
42
+ flex h-9 w-9 items-center justify-center
43
+ ```
44
+
45
+ ### With Info
46
+ ```
47
+ Container: flex items-center justify-between
48
+ Info: text-sm text-muted-foreground
49
+ Controls: flex items-center space-x-6 lg:space-x-8
50
+ ```
51
+
52
+ ### Items Per Page Selector
53
+ ```
54
+ flex items-center space-x-2
55
+ Label: text-sm font-medium
56
+ Select: h-8 w-16
57
+ ```
58
+
59
+ ## Props Interface
60
+ ```typescript
61
+ interface PaginationProps {
62
+ currentPage: number
63
+ totalPages: number
64
+ onPageChange: (page: number) => void
65
+ siblingCount?: number // Pages shown on each side of current
66
+ showFirstLast?: boolean
67
+ }
68
+
69
+ interface PaginationWithInfoProps extends PaginationProps {
70
+ totalItems: number
71
+ itemsPerPage: number
72
+ onItemsPerPageChange?: (count: number) => void
73
+ itemsPerPageOptions?: number[]
74
+ }
75
+ ```
76
+
77
+ ## Logic
78
+ ```typescript
79
+ // Generate page numbers with ellipsis
80
+ function generatePagination(current, total, siblings = 1) {
81
+ const range = (start, end) =>
82
+ Array.from({ length: end - start + 1 }, (_, i) => start + i)
83
+
84
+ const totalNumbers = siblings * 2 + 3 // siblings + current + first + last
85
+ const totalBlocks = totalNumbers + 2 // + 2 ellipsis
86
+
87
+ if (total <= totalBlocks) {
88
+ return range(1, total)
89
+ }
90
+
91
+ const leftSiblingIndex = Math.max(current - siblings, 1)
92
+ const rightSiblingIndex = Math.min(current + siblings, total)
93
+
94
+ const showLeftEllipsis = leftSiblingIndex > 2
95
+ const showRightEllipsis = rightSiblingIndex < total - 1
96
+
97
+ if (!showLeftEllipsis && showRightEllipsis) {
98
+ const leftRange = range(1, 3 + 2 * siblings)
99
+ return [...leftRange, 'ellipsis', total]
100
+ }
101
+
102
+ if (showLeftEllipsis && !showRightEllipsis) {
103
+ const rightRange = range(total - (2 + 2 * siblings), total)
104
+ return [1, 'ellipsis', ...rightRange]
105
+ }
106
+
107
+ const middleRange = range(leftSiblingIndex, rightSiblingIndex)
108
+ return [1, 'ellipsis', ...middleRange, 'ellipsis', total]
109
+ }
110
+ ```
111
+
112
+ ## Do
113
+ - Show current page clearly
114
+ - Disable prev on first page, next on last
115
+ - Use ellipsis for large page counts
116
+ - Keep total width manageable
117
+
118
+ ## Don't
119
+ - Hardcode colors
120
+ - Show all page numbers for large sets
121
+ - Forget mobile responsiveness
122
+ - Skip keyboard navigation
123
+
124
+ ## Example
125
+ ```tsx
126
+ import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react'
127
+ import { cn } from '@/lib/utils'
128
+ import { Button } from './Button'
129
+
130
+ const Pagination = ({
131
+ currentPage,
132
+ totalPages,
133
+ onPageChange,
134
+ siblingCount = 1,
135
+ }) => {
136
+ const pages = generatePagination(currentPage, totalPages, siblingCount)
137
+
138
+ return (
139
+ <nav className="flex items-center justify-center space-x-1" aria-label="Pagination">
140
+ <Button
141
+ variant="outline"
142
+ size="sm"
143
+ onClick={() => onPageChange(currentPage - 1)}
144
+ disabled={currentPage <= 1}
145
+ className="gap-1"
146
+ >
147
+ <ChevronLeft className="h-4 w-4" />
148
+ <span className="sr-only sm:not-sr-only">Previous</span>
149
+ </Button>
150
+
151
+ {pages.map((page, i) =>
152
+ page === 'ellipsis' ? (
153
+ <span key={`ellipsis-${i}`} className="flex h-9 w-9 items-center justify-center">
154
+ <MoreHorizontal className="h-4 w-4" />
155
+ </span>
156
+ ) : (
157
+ <Button
158
+ key={page}
159
+ variant={currentPage === page ? 'default' : 'outline'}
160
+ size="sm"
161
+ onClick={() => onPageChange(page)}
162
+ className="h-9 w-9"
163
+ >
164
+ {page}
165
+ </Button>
166
+ )
167
+ )}
168
+
169
+ <Button
170
+ variant="outline"
171
+ size="sm"
172
+ onClick={() => onPageChange(currentPage + 1)}
173
+ disabled={currentPage >= totalPages}
174
+ className="gap-1"
175
+ >
176
+ <span className="sr-only sm:not-sr-only">Next</span>
177
+ <ChevronRight className="h-4 w-4" />
178
+ </Button>
179
+ </nav>
180
+ )
181
+ }
182
+
183
+ // With info and items per page
184
+ const PaginationWithInfo = ({
185
+ currentPage,
186
+ totalPages,
187
+ totalItems,
188
+ itemsPerPage,
189
+ onPageChange,
190
+ onItemsPerPageChange,
191
+ }) => {
192
+ const start = (currentPage - 1) * itemsPerPage + 1
193
+ const end = Math.min(currentPage * itemsPerPage, totalItems)
194
+
195
+ return (
196
+ <div className="flex items-center justify-between px-2">
197
+ <div className="text-sm text-muted-foreground">
198
+ Showing {start} to {end} of {totalItems} results
199
+ </div>
200
+ <div className="flex items-center space-x-6">
201
+ {onItemsPerPageChange && (
202
+ <div className="flex items-center space-x-2">
203
+ <span className="text-sm font-medium">Rows per page</span>
204
+ <Select
205
+ value={String(itemsPerPage)}
206
+ onValueChange={(v) => onItemsPerPageChange(Number(v))}
207
+ >
208
+ {[10, 20, 50, 100].map((n) => (
209
+ <SelectItem key={n} value={String(n)}>{n}</SelectItem>
210
+ ))}
211
+ </Select>
212
+ </div>
213
+ )}
214
+ <Pagination
215
+ currentPage={currentPage}
216
+ totalPages={totalPages}
217
+ onPageChange={onPageChange}
218
+ />
219
+ </div>
220
+ </div>
221
+ )
222
+ }
223
+ ```
@@ -0,0 +1,156 @@
1
+ # Popover Component Recipe
2
+
3
+ ## Structure
4
+ - Trigger element
5
+ - Floating content panel
6
+ - Arrow pointing to trigger (optional)
7
+ - Support multiple placements
8
+ - Click outside to close
9
+
10
+ ## Tailwind Classes
11
+
12
+ ### Content
13
+ ```
14
+ z-50 w-72 {tokens.radius} border border-border bg-background p-4 text-foreground {tokens.shadow}
15
+ outline-none
16
+ data-[state=open]:animate-in data-[state=closed]:animate-out
17
+ data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
18
+ data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95
19
+ data-[side=bottom]:slide-in-from-top-2
20
+ data-[side=left]:slide-in-from-right-2
21
+ data-[side=right]:slide-in-from-left-2
22
+ data-[side=top]:slide-in-from-bottom-2
23
+ ```
24
+
25
+ ### Arrow
26
+ ```
27
+ fill-background stroke-border
28
+ ```
29
+
30
+ ### Close Button (optional)
31
+ ```
32
+ absolute right-2 top-2 rounded-sm opacity-70 hover:opacity-100
33
+ focus:outline-none focus:ring-1 focus:ring-primary
34
+ ```
35
+
36
+ ### Header (if structured content)
37
+ ```
38
+ mb-2
39
+ ```
40
+
41
+ ### Title
42
+ ```
43
+ font-medium leading-none
44
+ ```
45
+
46
+ ### Description
47
+ ```
48
+ text-sm text-muted-foreground mt-1
49
+ ```
50
+
51
+ ## Props Interface
52
+ ```typescript
53
+ interface PopoverProps {
54
+ open?: boolean
55
+ onOpenChange?: (open: boolean) => void
56
+ defaultOpen?: boolean
57
+ modal?: boolean
58
+ children: React.ReactNode
59
+ }
60
+
61
+ interface PopoverTriggerProps {
62
+ asChild?: boolean
63
+ children: React.ReactNode
64
+ }
65
+
66
+ interface PopoverContentProps {
67
+ side?: 'top' | 'right' | 'bottom' | 'left'
68
+ sideOffset?: number
69
+ align?: 'start' | 'center' | 'end'
70
+ alignOffset?: number
71
+ className?: string
72
+ children: React.ReactNode
73
+ }
74
+
75
+ interface PopoverArrowProps {
76
+ className?: string
77
+ width?: number
78
+ height?: number
79
+ }
80
+ ```
81
+
82
+ ## Do
83
+ - Use Radix Popover for accessibility
84
+ - Include enter/exit animations
85
+ - Support arrow pointing to trigger
86
+ - Handle viewport collision (flip/shift)
87
+
88
+ ## Don't
89
+ - Hardcode dimensions (use sensible defaults but allow override)
90
+ - Forget focus management
91
+ - Skip keyboard support (Escape to close)
92
+ - Use for tooltips (use Tooltip component)
93
+
94
+ ## Example
95
+ ```tsx
96
+ import * as PopoverPrimitive from '@radix-ui/react-popover'
97
+ import { cn } from '@/lib/utils'
98
+
99
+ const Popover = PopoverPrimitive.Root
100
+ const PopoverTrigger = PopoverPrimitive.Trigger
101
+ const PopoverAnchor = PopoverPrimitive.Anchor
102
+
103
+ const PopoverContent = ({
104
+ className,
105
+ align = 'center',
106
+ sideOffset = 4,
107
+ ...props
108
+ }) => (
109
+ <PopoverPrimitive.Portal>
110
+ <PopoverPrimitive.Content
111
+ align={align}
112
+ sideOffset={sideOffset}
113
+ className={cn(
114
+ 'z-50 w-72 rounded-lg border border-border bg-background p-4 shadow-md outline-none',
115
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out',
116
+ 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
117
+ 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
118
+ 'data-[side=bottom]:slide-in-from-top-2',
119
+ 'data-[side=left]:slide-in-from-right-2',
120
+ 'data-[side=right]:slide-in-from-left-2',
121
+ 'data-[side=top]:slide-in-from-bottom-2',
122
+ className
123
+ )}
124
+ {...props}
125
+ />
126
+ </PopoverPrimitive.Portal>
127
+ )
128
+
129
+ const PopoverArrow = ({ className, ...props }) => (
130
+ <PopoverPrimitive.Arrow
131
+ className={cn('fill-background stroke-border', className)}
132
+ {...props}
133
+ />
134
+ )
135
+
136
+ // Usage
137
+ <Popover>
138
+ <PopoverTrigger asChild>
139
+ <Button variant="outline">Open popover</Button>
140
+ </PopoverTrigger>
141
+ <PopoverContent>
142
+ <div className="grid gap-4">
143
+ <div className="space-y-2">
144
+ <h4 className="font-medium leading-none">Dimensions</h4>
145
+ <p className="text-sm text-muted-foreground">
146
+ Set the dimensions for the layer.
147
+ </p>
148
+ </div>
149
+ <div className="grid gap-2">
150
+ <Input placeholder="Width" />
151
+ <Input placeholder="Height" />
152
+ </div>
153
+ </div>
154
+ </PopoverContent>
155
+ </Popover>
156
+ ```