@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,263 @@
|
|
|
1
|
+
# Dropdown Menu Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Trigger button/element
|
|
5
|
+
- Menu content with items
|
|
6
|
+
- Support for submenus
|
|
7
|
+
- Checkboxes and radio items
|
|
8
|
+
- Keyboard navigation
|
|
9
|
+
- Separators and labels
|
|
10
|
+
|
|
11
|
+
## Tailwind Classes
|
|
12
|
+
|
|
13
|
+
### Content
|
|
14
|
+
```
|
|
15
|
+
z-50 min-w-[8rem] overflow-hidden {tokens.radius} border border-border
|
|
16
|
+
bg-background p-1 text-foreground {tokens.shadow}
|
|
17
|
+
data-[state=open]:animate-in data-[state=closed]:animate-out
|
|
18
|
+
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
|
|
19
|
+
data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95
|
|
20
|
+
data-[side=bottom]:slide-in-from-top-2
|
|
21
|
+
data-[side=left]:slide-in-from-right-2
|
|
22
|
+
data-[side=right]:slide-in-from-left-2
|
|
23
|
+
data-[side=top]:slide-in-from-bottom-2
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Item
|
|
27
|
+
```
|
|
28
|
+
relative flex cursor-pointer select-none items-center {tokens.radius} px-2 py-1.5
|
|
29
|
+
text-sm outline-none
|
|
30
|
+
transition-colors
|
|
31
|
+
focus:bg-muted focus:text-foreground
|
|
32
|
+
data-[disabled]:pointer-events-none data-[disabled]:opacity-50
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Item with Icon
|
|
36
|
+
```
|
|
37
|
+
[&>svg]:mr-2 [&>svg]:h-4 [&>svg]:w-4
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Checkbox Item
|
|
41
|
+
```
|
|
42
|
+
relative flex cursor-pointer select-none items-center {tokens.radius} py-1.5 pl-8 pr-2
|
|
43
|
+
text-sm outline-none
|
|
44
|
+
focus:bg-muted focus:text-foreground
|
|
45
|
+
data-[disabled]:pointer-events-none data-[disabled]:opacity-50
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Radio Item
|
|
49
|
+
```
|
|
50
|
+
relative flex cursor-pointer select-none items-center {tokens.radius} py-1.5 pl-8 pr-2
|
|
51
|
+
text-sm outline-none
|
|
52
|
+
focus:bg-muted focus:text-foreground
|
|
53
|
+
data-[disabled]:pointer-events-none data-[disabled]:opacity-50
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Item Indicator (check/radio)
|
|
57
|
+
```
|
|
58
|
+
absolute left-2 flex h-3.5 w-3.5 items-center justify-center
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Label
|
|
62
|
+
```
|
|
63
|
+
px-2 py-1.5 text-sm font-semibold text-foreground
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Separator
|
|
67
|
+
```
|
|
68
|
+
-mx-1 my-1 h-px bg-border
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Shortcut
|
|
72
|
+
```
|
|
73
|
+
ml-auto text-xs tracking-widest text-muted-foreground
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Sub Trigger
|
|
77
|
+
```
|
|
78
|
+
flex cursor-pointer select-none items-center {tokens.radius} px-2 py-1.5
|
|
79
|
+
text-sm outline-none
|
|
80
|
+
focus:bg-muted
|
|
81
|
+
data-[state=open]:bg-muted
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Sub Content
|
|
85
|
+
```
|
|
86
|
+
z-50 min-w-[8rem] overflow-hidden {tokens.radius} border border-border
|
|
87
|
+
bg-background p-1 {tokens.shadow}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Props Interface
|
|
91
|
+
```typescript
|
|
92
|
+
interface DropdownMenuProps {
|
|
93
|
+
open?: boolean
|
|
94
|
+
onOpenChange?: (open: boolean) => void
|
|
95
|
+
modal?: boolean
|
|
96
|
+
children: React.ReactNode
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface DropdownMenuItemProps {
|
|
100
|
+
disabled?: boolean
|
|
101
|
+
onSelect?: () => void
|
|
102
|
+
children: React.ReactNode
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface DropdownMenuCheckboxItemProps {
|
|
106
|
+
checked?: boolean
|
|
107
|
+
onCheckedChange?: (checked: boolean) => void
|
|
108
|
+
disabled?: boolean
|
|
109
|
+
children: React.ReactNode
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface DropdownMenuRadioGroupProps {
|
|
113
|
+
value?: string
|
|
114
|
+
onValueChange?: (value: string) => void
|
|
115
|
+
children: React.ReactNode
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface DropdownMenuRadioItemProps {
|
|
119
|
+
value: string
|
|
120
|
+
disabled?: boolean
|
|
121
|
+
children: React.ReactNode
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Do
|
|
126
|
+
- Use Radix DropdownMenu for accessibility
|
|
127
|
+
- Support keyboard navigation (arrows, enter, escape)
|
|
128
|
+
- Include focus management
|
|
129
|
+
- Support nested submenus
|
|
130
|
+
|
|
131
|
+
## Don't
|
|
132
|
+
- Hardcode colors
|
|
133
|
+
- Forget keyboard shortcuts display
|
|
134
|
+
- Skip disabled state handling
|
|
135
|
+
- Use for navigation (use NavigationMenu)
|
|
136
|
+
|
|
137
|
+
## Example
|
|
138
|
+
```tsx
|
|
139
|
+
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
|
140
|
+
import { Check, ChevronRight, Circle } from 'lucide-react'
|
|
141
|
+
import { cn } from '@/lib/utils'
|
|
142
|
+
|
|
143
|
+
const DropdownMenu = DropdownMenuPrimitive.Root
|
|
144
|
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
145
|
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
146
|
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
147
|
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
148
|
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
149
|
+
|
|
150
|
+
const DropdownMenuContent = ({ className, sideOffset = 4, ...props }) => (
|
|
151
|
+
<DropdownMenuPrimitive.Portal>
|
|
152
|
+
<DropdownMenuPrimitive.Content
|
|
153
|
+
sideOffset={sideOffset}
|
|
154
|
+
className={cn(
|
|
155
|
+
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-background p-1 shadow-md',
|
|
156
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
157
|
+
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
158
|
+
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
159
|
+
'data-[side=bottom]:slide-in-from-top-2',
|
|
160
|
+
className
|
|
161
|
+
)}
|
|
162
|
+
{...props}
|
|
163
|
+
/>
|
|
164
|
+
</DropdownMenuPrimitive.Portal>
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
const DropdownMenuItem = ({ className, inset, ...props }) => (
|
|
168
|
+
<DropdownMenuPrimitive.Item
|
|
169
|
+
className={cn(
|
|
170
|
+
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
|
|
171
|
+
'transition-colors focus:bg-muted focus:text-foreground',
|
|
172
|
+
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
173
|
+
inset && 'pl-8',
|
|
174
|
+
className
|
|
175
|
+
)}
|
|
176
|
+
{...props}
|
|
177
|
+
/>
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const DropdownMenuCheckboxItem = ({ className, checked, children, ...props }) => (
|
|
181
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
182
|
+
className={cn(
|
|
183
|
+
'relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none',
|
|
184
|
+
'focus:bg-muted focus:text-foreground',
|
|
185
|
+
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
186
|
+
className
|
|
187
|
+
)}
|
|
188
|
+
checked={checked}
|
|
189
|
+
{...props}
|
|
190
|
+
>
|
|
191
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
192
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
193
|
+
<Check className="h-4 w-4" />
|
|
194
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
195
|
+
</span>
|
|
196
|
+
{children}
|
|
197
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
const DropdownMenuRadioItem = ({ className, children, ...props }) => (
|
|
201
|
+
<DropdownMenuPrimitive.RadioItem
|
|
202
|
+
className={cn(
|
|
203
|
+
'relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none',
|
|
204
|
+
'focus:bg-muted focus:text-foreground',
|
|
205
|
+
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
206
|
+
className
|
|
207
|
+
)}
|
|
208
|
+
{...props}
|
|
209
|
+
>
|
|
210
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
211
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
212
|
+
<Circle className="h-2 w-2 fill-current" />
|
|
213
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
214
|
+
</span>
|
|
215
|
+
{children}
|
|
216
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
const DropdownMenuLabel = ({ className, inset, ...props }) => (
|
|
220
|
+
<DropdownMenuPrimitive.Label
|
|
221
|
+
className={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
|
|
222
|
+
{...props}
|
|
223
|
+
/>
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
const DropdownMenuSeparator = ({ className, ...props }) => (
|
|
227
|
+
<DropdownMenuPrimitive.Separator
|
|
228
|
+
className={cn('-mx-1 my-1 h-px bg-border', className)}
|
|
229
|
+
{...props}
|
|
230
|
+
/>
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
const DropdownMenuShortcut = ({ className, ...props }) => (
|
|
234
|
+
<span className={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)} {...props} />
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
const DropdownMenuSubTrigger = ({ className, inset, children, ...props }) => (
|
|
238
|
+
<DropdownMenuPrimitive.SubTrigger
|
|
239
|
+
className={cn(
|
|
240
|
+
'flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
|
|
241
|
+
'focus:bg-muted data-[state=open]:bg-muted',
|
|
242
|
+
inset && 'pl-8',
|
|
243
|
+
className
|
|
244
|
+
)}
|
|
245
|
+
{...props}
|
|
246
|
+
>
|
|
247
|
+
{children}
|
|
248
|
+
<ChevronRight className="ml-auto h-4 w-4" />
|
|
249
|
+
</DropdownMenuPrimitive.SubTrigger>
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
const DropdownMenuSubContent = ({ className, ...props }) => (
|
|
253
|
+
<DropdownMenuPrimitive.SubContent
|
|
254
|
+
className={cn(
|
|
255
|
+
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-background p-1 shadow-lg',
|
|
256
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
257
|
+
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
258
|
+
className
|
|
259
|
+
)}
|
|
260
|
+
{...props}
|
|
261
|
+
/>
|
|
262
|
+
)
|
|
263
|
+
```
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Hover Card Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Trigger element (usually a link or button)
|
|
5
|
+
- Floating card that appears on hover
|
|
6
|
+
- Arrow pointing to trigger
|
|
7
|
+
- Rich content support (images, text, actions)
|
|
8
|
+
- Delay before showing/hiding
|
|
9
|
+
|
|
10
|
+
## Tailwind Classes
|
|
11
|
+
|
|
12
|
+
### Content
|
|
13
|
+
```
|
|
14
|
+
z-50 w-64 {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
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Arrow with Border
|
|
31
|
+
```
|
|
32
|
+
fill-background stroke-border
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### User Card Layout
|
|
36
|
+
```
|
|
37
|
+
Container: flex flex-col gap-4
|
|
38
|
+
Avatar section: flex items-center gap-4
|
|
39
|
+
Info section: space-y-1
|
|
40
|
+
Stats section: flex gap-4 pt-2
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Link Preview Layout
|
|
44
|
+
```
|
|
45
|
+
Container: space-y-3
|
|
46
|
+
Image: aspect-video w-full rounded-md object-cover
|
|
47
|
+
Title: font-medium leading-none
|
|
48
|
+
Description: text-sm text-muted-foreground line-clamp-2
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Props Interface
|
|
52
|
+
```typescript
|
|
53
|
+
interface HoverCardProps {
|
|
54
|
+
open?: boolean
|
|
55
|
+
onOpenChange?: (open: boolean) => void
|
|
56
|
+
defaultOpen?: boolean
|
|
57
|
+
openDelay?: number // default 700ms
|
|
58
|
+
closeDelay?: number // default 300ms
|
|
59
|
+
children: React.ReactNode
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface HoverCardTriggerProps {
|
|
63
|
+
asChild?: boolean
|
|
64
|
+
children: React.ReactNode
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface HoverCardContentProps {
|
|
68
|
+
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
69
|
+
sideOffset?: number
|
|
70
|
+
align?: 'start' | 'center' | 'end'
|
|
71
|
+
alignOffset?: number
|
|
72
|
+
className?: string
|
|
73
|
+
children: React.ReactNode
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface HoverCardArrowProps {
|
|
77
|
+
className?: string
|
|
78
|
+
width?: number
|
|
79
|
+
height?: number
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Configuration
|
|
84
|
+
- Default open delay: 700ms (prevents accidental triggers)
|
|
85
|
+
- Default close delay: 300ms (allows moving to card)
|
|
86
|
+
- Supports collision detection
|
|
87
|
+
|
|
88
|
+
## Do
|
|
89
|
+
- Use for supplementary, non-essential information
|
|
90
|
+
- Keep content scannable (not too much)
|
|
91
|
+
- Include visual hierarchy in content
|
|
92
|
+
- Support touch devices with click fallback
|
|
93
|
+
|
|
94
|
+
## Don't
|
|
95
|
+
- Use for critical information (use tooltip or inline)
|
|
96
|
+
- Put interactive forms inside
|
|
97
|
+
- Use too short delay (annoying)
|
|
98
|
+
- Block content underneath
|
|
99
|
+
|
|
100
|
+
## Example
|
|
101
|
+
```tsx
|
|
102
|
+
import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
|
|
103
|
+
import { cn } from '@/lib/utils'
|
|
104
|
+
|
|
105
|
+
const HoverCard = HoverCardPrimitive.Root
|
|
106
|
+
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
|
107
|
+
|
|
108
|
+
const HoverCardContent = ({
|
|
109
|
+
className,
|
|
110
|
+
align = 'center',
|
|
111
|
+
sideOffset = 4,
|
|
112
|
+
...props
|
|
113
|
+
}) => (
|
|
114
|
+
<HoverCardPrimitive.Content
|
|
115
|
+
align={align}
|
|
116
|
+
sideOffset={sideOffset}
|
|
117
|
+
className={cn(
|
|
118
|
+
'z-50 w-64 rounded-lg border border-border bg-background p-4 shadow-md outline-none',
|
|
119
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
120
|
+
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
121
|
+
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
122
|
+
'data-[side=bottom]:slide-in-from-top-2',
|
|
123
|
+
'data-[side=left]:slide-in-from-right-2',
|
|
124
|
+
'data-[side=right]:slide-in-from-left-2',
|
|
125
|
+
'data-[side=top]:slide-in-from-bottom-2',
|
|
126
|
+
className
|
|
127
|
+
)}
|
|
128
|
+
{...props}
|
|
129
|
+
/>
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const HoverCardArrow = ({ className, ...props }) => (
|
|
133
|
+
<HoverCardPrimitive.Arrow
|
|
134
|
+
className={cn('fill-background', className)}
|
|
135
|
+
{...props}
|
|
136
|
+
/>
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
// User profile card
|
|
140
|
+
const UserHoverCard = ({ user, children }) => (
|
|
141
|
+
<HoverCard>
|
|
142
|
+
<HoverCardTrigger asChild>{children}</HoverCardTrigger>
|
|
143
|
+
<HoverCardContent className="w-80">
|
|
144
|
+
<div className="flex justify-between space-x-4">
|
|
145
|
+
<Avatar>
|
|
146
|
+
<AvatarImage src={user.avatar} />
|
|
147
|
+
<AvatarFallback>{user.initials}</AvatarFallback>
|
|
148
|
+
</Avatar>
|
|
149
|
+
<div className="space-y-1">
|
|
150
|
+
<h4 className="text-sm font-semibold">{user.name}</h4>
|
|
151
|
+
<p className="text-sm text-muted-foreground">@{user.username}</p>
|
|
152
|
+
<p className="text-sm">{user.bio}</p>
|
|
153
|
+
<div className="flex items-center pt-2">
|
|
154
|
+
<span className="text-xs text-muted-foreground">
|
|
155
|
+
Joined {user.joinedDate}
|
|
156
|
+
</span>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<HoverCardArrow />
|
|
161
|
+
</HoverCardContent>
|
|
162
|
+
</HoverCard>
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
// Link preview card
|
|
166
|
+
const LinkHoverCard = ({ url, title, description, image, children }) => (
|
|
167
|
+
<HoverCard>
|
|
168
|
+
<HoverCardTrigger asChild>{children}</HoverCardTrigger>
|
|
169
|
+
<HoverCardContent className="w-80">
|
|
170
|
+
<div className="space-y-3">
|
|
171
|
+
{image && (
|
|
172
|
+
<img
|
|
173
|
+
src={image}
|
|
174
|
+
alt={title}
|
|
175
|
+
className="aspect-video w-full rounded-md object-cover"
|
|
176
|
+
/>
|
|
177
|
+
)}
|
|
178
|
+
<div className="space-y-1">
|
|
179
|
+
<h4 className="text-sm font-semibold leading-none">{title}</h4>
|
|
180
|
+
<p className="text-sm text-muted-foreground line-clamp-2">
|
|
181
|
+
{description}
|
|
182
|
+
</p>
|
|
183
|
+
<p className="text-xs text-muted-foreground">{url}</p>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
<HoverCardArrow />
|
|
187
|
+
</HoverCardContent>
|
|
188
|
+
</HoverCard>
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
// Product preview card
|
|
192
|
+
const ProductHoverCard = ({ product, children }) => (
|
|
193
|
+
<HoverCard>
|
|
194
|
+
<HoverCardTrigger asChild>{children}</HoverCardTrigger>
|
|
195
|
+
<HoverCardContent className="w-72">
|
|
196
|
+
<div className="space-y-3">
|
|
197
|
+
<img
|
|
198
|
+
src={product.image}
|
|
199
|
+
alt={product.name}
|
|
200
|
+
className="aspect-square w-full rounded-md object-cover"
|
|
201
|
+
/>
|
|
202
|
+
<div className="space-y-1">
|
|
203
|
+
<div className="flex items-center justify-between">
|
|
204
|
+
<h4 className="font-medium">{product.name}</h4>
|
|
205
|
+
<span className="font-semibold">${product.price}</span>
|
|
206
|
+
</div>
|
|
207
|
+
<p className="text-sm text-muted-foreground line-clamp-2">
|
|
208
|
+
{product.description}
|
|
209
|
+
</p>
|
|
210
|
+
<div className="flex items-center gap-1">
|
|
211
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
212
|
+
<Star
|
|
213
|
+
key={i}
|
|
214
|
+
className={cn(
|
|
215
|
+
'h-3 w-3',
|
|
216
|
+
i < product.rating ? 'fill-yellow-400 text-yellow-400' : 'text-muted'
|
|
217
|
+
)}
|
|
218
|
+
/>
|
|
219
|
+
))}
|
|
220
|
+
<span className="text-xs text-muted-foreground">
|
|
221
|
+
({product.reviewCount})
|
|
222
|
+
</span>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
<HoverCardArrow />
|
|
227
|
+
</HoverCardContent>
|
|
228
|
+
</HoverCard>
|
|
229
|
+
)
|
|
230
|
+
```
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Input Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Use `<input>` element
|
|
5
|
+
- Support common types: text, email, password, number, search, tel, url
|
|
6
|
+
- Include states: disabled, error, with icon
|
|
7
|
+
- Optionally wrap with label and error message
|
|
8
|
+
|
|
9
|
+
## Tailwind Classes
|
|
10
|
+
|
|
11
|
+
### Base Input
|
|
12
|
+
```
|
|
13
|
+
flex h-10 w-full {tokens.radius} border border-border bg-background px-3 py-2
|
|
14
|
+
text-sm text-foreground placeholder:text-muted-foreground
|
|
15
|
+
transition-colors
|
|
16
|
+
focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2
|
|
17
|
+
disabled:cursor-not-allowed disabled:opacity-50
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Error State
|
|
21
|
+
```
|
|
22
|
+
border-destructive focus:ring-destructive
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### With Icon (left)
|
|
26
|
+
```
|
|
27
|
+
pl-10
|
|
28
|
+
Icon wrapper: absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### With Icon (right)
|
|
32
|
+
```
|
|
33
|
+
pr-10
|
|
34
|
+
Icon wrapper: absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Props Interface
|
|
38
|
+
```typescript
|
|
39
|
+
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
40
|
+
error?: boolean
|
|
41
|
+
errorMessage?: string
|
|
42
|
+
label?: string
|
|
43
|
+
leftIcon?: React.ReactNode
|
|
44
|
+
rightIcon?: React.ReactNode
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Wrapper Components
|
|
49
|
+
|
|
50
|
+
### InputWrapper (with label and error)
|
|
51
|
+
```tsx
|
|
52
|
+
<div className="space-y-2">
|
|
53
|
+
{label && <Label htmlFor={id}>{label}</Label>}
|
|
54
|
+
<Input ... />
|
|
55
|
+
{errorMessage && <p className="text-sm text-destructive">{errorMessage}</p>}
|
|
56
|
+
</div>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Do
|
|
60
|
+
- Use `bg-background` for input background
|
|
61
|
+
- Include `placeholder:text-muted-foreground`
|
|
62
|
+
- Add focus ring with primary color
|
|
63
|
+
- Support error states with destructive color
|
|
64
|
+
- Use relative/absolute for icon positioning
|
|
65
|
+
|
|
66
|
+
## Don't
|
|
67
|
+
- Hardcode colors
|
|
68
|
+
- Forget focus states
|
|
69
|
+
- Skip placeholder styling
|
|
70
|
+
- Use fixed widths (default to `w-full`)
|
|
71
|
+
|
|
72
|
+
## Example
|
|
73
|
+
```tsx
|
|
74
|
+
import { cn } from '@/lib/utils'
|
|
75
|
+
import { forwardRef } from 'react'
|
|
76
|
+
|
|
77
|
+
const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
78
|
+
({ className, type = 'text', error, leftIcon, rightIcon, ...props }, ref) => {
|
|
79
|
+
return (
|
|
80
|
+
<div className="relative">
|
|
81
|
+
{leftIcon && (
|
|
82
|
+
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
|
|
83
|
+
{leftIcon}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
<input
|
|
87
|
+
type={type}
|
|
88
|
+
className={cn(
|
|
89
|
+
'flex h-10 w-full rounded-lg border border-border bg-background px-3 py-2',
|
|
90
|
+
'text-sm text-foreground placeholder:text-muted-foreground',
|
|
91
|
+
'transition-colors',
|
|
92
|
+
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
|
|
93
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
94
|
+
error && 'border-destructive focus:ring-destructive',
|
|
95
|
+
leftIcon && 'pl-10',
|
|
96
|
+
rightIcon && 'pr-10',
|
|
97
|
+
className
|
|
98
|
+
)}
|
|
99
|
+
ref={ref}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
{rightIcon && (
|
|
103
|
+
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground">
|
|
104
|
+
{rightIcon}
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
Input.displayName = 'Input'
|
|
113
|
+
```
|