@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.
- package/bin/cli.js +283 -0
- package/index.js +12 -0
- package/package.json +45 -0
- package/templates/commands/component-harness.md +116 -0
- package/templates/commands/setup-styling.md +111 -0
- package/templates/component-recipes/accordion.md +153 -0
- package/templates/component-recipes/alert.md +145 -0
- package/templates/component-recipes/avatar.md +165 -0
- package/templates/component-recipes/badge.md +126 -0
- package/templates/component-recipes/breadcrumb.md +220 -0
- package/templates/component-recipes/button.md +90 -0
- package/templates/component-recipes/card.md +130 -0
- package/templates/component-recipes/carousel.md +277 -0
- package/templates/component-recipes/checkbox.md +117 -0
- package/templates/component-recipes/collapsible.md +201 -0
- package/templates/component-recipes/combobox.md +193 -0
- package/templates/component-recipes/context-menu.md +254 -0
- package/templates/component-recipes/dialog.md +193 -0
- package/templates/component-recipes/drawer.md +196 -0
- package/templates/component-recipes/dropdown-menu.md +263 -0
- package/templates/component-recipes/hover-card.md +230 -0
- package/templates/component-recipes/input.md +113 -0
- package/templates/component-recipes/label.md +259 -0
- package/templates/component-recipes/modal.md +155 -0
- package/templates/component-recipes/navigation-menu.md +310 -0
- package/templates/component-recipes/pagination.md +223 -0
- package/templates/component-recipes/popover.md +156 -0
- package/templates/component-recipes/progress.md +185 -0
- package/templates/component-recipes/radio.md +148 -0
- package/templates/component-recipes/select.md +154 -0
- package/templates/component-recipes/separator.md +124 -0
- package/templates/component-recipes/skeleton.md +186 -0
- package/templates/component-recipes/slider.md +114 -0
- package/templates/component-recipes/spinner.md +225 -0
- package/templates/component-recipes/switch.md +100 -0
- package/templates/component-recipes/table.md +161 -0
- package/templates/component-recipes/tabs.md +145 -0
- package/templates/component-recipes/textarea.md +234 -0
- package/templates/component-recipes/toast.md +209 -0
- package/templates/component-recipes/toggle-group.md +216 -0
- package/templates/component-recipes/tooltip.md +115 -0
- package/templates/hooks/triggers.d/styling.json +23 -0
- 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
|
+
```
|