@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,153 @@
|
|
|
1
|
+
# Accordion Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Root container with multiple items
|
|
5
|
+
- Each item has trigger (header) and content
|
|
6
|
+
- Chevron indicator that rotates on open
|
|
7
|
+
- Support single or multiple items open
|
|
8
|
+
|
|
9
|
+
## Tailwind Classes
|
|
10
|
+
|
|
11
|
+
### Item
|
|
12
|
+
```
|
|
13
|
+
border-b border-border
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Trigger
|
|
17
|
+
```
|
|
18
|
+
flex flex-1 items-center justify-between py-4 text-sm font-medium
|
|
19
|
+
transition-all hover:underline
|
|
20
|
+
[&[data-state=open]>svg]:rotate-180
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Chevron Icon
|
|
24
|
+
```
|
|
25
|
+
h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Content
|
|
29
|
+
```
|
|
30
|
+
overflow-hidden text-sm
|
|
31
|
+
data-[state=closed]:animate-accordion-up
|
|
32
|
+
data-[state=open]:animate-accordion-down
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Content Inner
|
|
36
|
+
```
|
|
37
|
+
pb-4 pt-0
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Animations (add to tailwind.config.js)
|
|
41
|
+
```js
|
|
42
|
+
keyframes: {
|
|
43
|
+
'accordion-down': {
|
|
44
|
+
from: { height: '0' },
|
|
45
|
+
to: { height: 'var(--radix-accordion-content-height)' },
|
|
46
|
+
},
|
|
47
|
+
'accordion-up': {
|
|
48
|
+
from: { height: 'var(--radix-accordion-content-height)' },
|
|
49
|
+
to: { height: '0' },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
animation: {
|
|
53
|
+
'accordion-down': 'accordion-down 0.2s ease-out',
|
|
54
|
+
'accordion-up': 'accordion-up 0.2s ease-out',
|
|
55
|
+
},
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Props Interface
|
|
59
|
+
```typescript
|
|
60
|
+
interface AccordionProps {
|
|
61
|
+
type?: 'single' | 'multiple'
|
|
62
|
+
defaultValue?: string | string[]
|
|
63
|
+
value?: string | string[]
|
|
64
|
+
onValueChange?: (value: string | string[]) => void
|
|
65
|
+
collapsible?: boolean // only for type="single"
|
|
66
|
+
children: React.ReactNode
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface AccordionItemProps {
|
|
70
|
+
value: string
|
|
71
|
+
disabled?: boolean
|
|
72
|
+
children: React.ReactNode
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface AccordionTriggerProps {
|
|
76
|
+
className?: string
|
|
77
|
+
children: React.ReactNode
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface AccordionContentProps {
|
|
81
|
+
className?: string
|
|
82
|
+
children: React.ReactNode
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Do
|
|
87
|
+
- Use Radix Accordion for accessibility
|
|
88
|
+
- Animate height changes smoothly
|
|
89
|
+
- Rotate chevron on open
|
|
90
|
+
- Support both single and multiple modes
|
|
91
|
+
|
|
92
|
+
## Don't
|
|
93
|
+
- Hardcode colors
|
|
94
|
+
- Skip keyboard accessibility
|
|
95
|
+
- Use abrupt open/close (animate it)
|
|
96
|
+
- Forget border between items
|
|
97
|
+
|
|
98
|
+
## Example
|
|
99
|
+
```tsx
|
|
100
|
+
import * as AccordionPrimitive from '@radix-ui/react-accordion'
|
|
101
|
+
import { ChevronDown } from 'lucide-react'
|
|
102
|
+
import { cn } from '@/lib/utils'
|
|
103
|
+
|
|
104
|
+
const Accordion = AccordionPrimitive.Root
|
|
105
|
+
|
|
106
|
+
const AccordionItem = ({ className, ...props }) => (
|
|
107
|
+
<AccordionPrimitive.Item
|
|
108
|
+
className={cn('border-b border-border', className)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const AccordionTrigger = ({ className, children, ...props }) => (
|
|
114
|
+
<AccordionPrimitive.Header className="flex">
|
|
115
|
+
<AccordionPrimitive.Trigger
|
|
116
|
+
className={cn(
|
|
117
|
+
'flex flex-1 items-center justify-between py-4 text-sm font-medium',
|
|
118
|
+
'transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
|
119
|
+
className
|
|
120
|
+
)}
|
|
121
|
+
{...props}
|
|
122
|
+
>
|
|
123
|
+
{children}
|
|
124
|
+
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
|
125
|
+
</AccordionPrimitive.Trigger>
|
|
126
|
+
</AccordionPrimitive.Header>
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const AccordionContent = ({ className, children, ...props }) => (
|
|
130
|
+
<AccordionPrimitive.Content
|
|
131
|
+
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
132
|
+
{...props}
|
|
133
|
+
>
|
|
134
|
+
<div className={cn('pb-4 pt-0', className)}>{children}</div>
|
|
135
|
+
</AccordionPrimitive.Content>
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
// Usage
|
|
139
|
+
<Accordion type="single" collapsible>
|
|
140
|
+
<AccordionItem value="item-1">
|
|
141
|
+
<AccordionTrigger>Is it accessible?</AccordionTrigger>
|
|
142
|
+
<AccordionContent>
|
|
143
|
+
Yes. It adheres to the WAI-ARIA design pattern.
|
|
144
|
+
</AccordionContent>
|
|
145
|
+
</AccordionItem>
|
|
146
|
+
<AccordionItem value="item-2">
|
|
147
|
+
<AccordionTrigger>Is it styled?</AccordionTrigger>
|
|
148
|
+
<AccordionContent>
|
|
149
|
+
Yes. It comes with default styles that match your design system.
|
|
150
|
+
</AccordionContent>
|
|
151
|
+
</AccordionItem>
|
|
152
|
+
</Accordion>
|
|
153
|
+
```
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Alert Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Container with icon, title, and description
|
|
5
|
+
- Support variants: info, success, warning, error/destructive
|
|
6
|
+
- Optional dismiss button
|
|
7
|
+
- Optional action buttons
|
|
8
|
+
|
|
9
|
+
## Tailwind Classes
|
|
10
|
+
|
|
11
|
+
### Container
|
|
12
|
+
```
|
|
13
|
+
relative w-full {tokens.radius} border p-4
|
|
14
|
+
[&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:h-4 [&>svg]:w-4
|
|
15
|
+
[&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Variants
|
|
19
|
+
```
|
|
20
|
+
default: bg-background border-border text-foreground [&>svg]:text-foreground
|
|
21
|
+
info: bg-blue-500/10 border-blue-500/20 text-blue-600 [&>svg]:text-blue-600
|
|
22
|
+
success: bg-green-500/10 border-green-500/20 text-green-600 [&>svg]:text-green-600
|
|
23
|
+
warning: bg-yellow-500/10 border-yellow-500/20 text-yellow-600 [&>svg]:text-yellow-600
|
|
24
|
+
destructive: bg-destructive/10 border-destructive/20 text-destructive [&>svg]:text-destructive
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Title
|
|
28
|
+
```
|
|
29
|
+
mb-1 font-medium leading-none tracking-tight
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Description
|
|
33
|
+
```
|
|
34
|
+
text-sm [&_p]:leading-relaxed
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Dismiss Button
|
|
38
|
+
```
|
|
39
|
+
absolute right-2 top-2 {tokens.radius} p-1 opacity-70
|
|
40
|
+
hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-primary
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Props Interface
|
|
44
|
+
```typescript
|
|
45
|
+
interface AlertProps {
|
|
46
|
+
variant?: 'default' | 'info' | 'success' | 'warning' | 'destructive'
|
|
47
|
+
icon?: React.ReactNode
|
|
48
|
+
title?: string
|
|
49
|
+
onDismiss?: () => void
|
|
50
|
+
children: React.ReactNode
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface AlertTitleProps {
|
|
54
|
+
className?: string
|
|
55
|
+
children: React.ReactNode
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface AlertDescriptionProps {
|
|
59
|
+
className?: string
|
|
60
|
+
children: React.ReactNode
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Icons per Variant
|
|
65
|
+
```
|
|
66
|
+
info: <Info /> or <AlertCircle />
|
|
67
|
+
success: <CheckCircle />
|
|
68
|
+
warning: <AlertTriangle />
|
|
69
|
+
destructive: <XCircle /> or <AlertOctagon />
|
|
70
|
+
default: <Info /> or custom
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Do
|
|
74
|
+
- Include appropriate icon for each variant
|
|
75
|
+
- Use semantic colors from tokens where possible
|
|
76
|
+
- Support composable pattern (Alert.Title, Alert.Description)
|
|
77
|
+
- Add dismiss functionality when needed
|
|
78
|
+
|
|
79
|
+
## Don't
|
|
80
|
+
- Hardcode colors
|
|
81
|
+
- Make alerts too visually heavy
|
|
82
|
+
- Forget to position icon properly
|
|
83
|
+
- Skip the semantic meaning of variants
|
|
84
|
+
|
|
85
|
+
## Example
|
|
86
|
+
```tsx
|
|
87
|
+
import { cn } from '@/lib/utils'
|
|
88
|
+
import { AlertCircle, CheckCircle, AlertTriangle, XCircle, X, Info } from 'lucide-react'
|
|
89
|
+
|
|
90
|
+
const alertVariants = {
|
|
91
|
+
default: 'bg-background border-border text-foreground',
|
|
92
|
+
info: 'bg-blue-500/10 border-blue-500/20 text-blue-600 [&>svg]:text-blue-600',
|
|
93
|
+
success: 'bg-green-500/10 border-green-500/20 text-green-600 [&>svg]:text-green-600',
|
|
94
|
+
warning: 'bg-yellow-500/10 border-yellow-500/20 text-yellow-600 [&>svg]:text-yellow-600',
|
|
95
|
+
destructive: 'bg-destructive/10 border-destructive/20 text-destructive [&>svg]:text-destructive',
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const alertIcons = {
|
|
99
|
+
default: Info,
|
|
100
|
+
info: AlertCircle,
|
|
101
|
+
success: CheckCircle,
|
|
102
|
+
warning: AlertTriangle,
|
|
103
|
+
destructive: XCircle,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const Alert = ({ variant = 'default', title, onDismiss, className, children, ...props }) => {
|
|
107
|
+
const Icon = alertIcons[variant]
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div
|
|
111
|
+
role="alert"
|
|
112
|
+
className={cn(
|
|
113
|
+
'relative w-full rounded-lg border p-4',
|
|
114
|
+
'[&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:h-4 [&>svg]:w-4',
|
|
115
|
+
'[&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11',
|
|
116
|
+
alertVariants[variant],
|
|
117
|
+
className
|
|
118
|
+
)}
|
|
119
|
+
{...props}
|
|
120
|
+
>
|
|
121
|
+
<Icon className="h-4 w-4" />
|
|
122
|
+
<div>
|
|
123
|
+
{title && <AlertTitle>{title}</AlertTitle>}
|
|
124
|
+
<AlertDescription>{children}</AlertDescription>
|
|
125
|
+
</div>
|
|
126
|
+
{onDismiss && (
|
|
127
|
+
<button
|
|
128
|
+
onClick={onDismiss}
|
|
129
|
+
className="absolute right-2 top-2 rounded p-1 opacity-70 hover:opacity-100"
|
|
130
|
+
>
|
|
131
|
+
<X className="h-4 w-4" />
|
|
132
|
+
</button>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const AlertTitle = ({ className, ...props }) => (
|
|
139
|
+
<h5 className={cn('mb-1 font-medium leading-none tracking-tight', className)} {...props} />
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const AlertDescription = ({ className, ...props }) => (
|
|
143
|
+
<div className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
|
|
144
|
+
)
|
|
145
|
+
```
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Avatar Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Circular container for image or fallback
|
|
5
|
+
- Support image, initials fallback, or icon fallback
|
|
6
|
+
- Multiple sizes
|
|
7
|
+
- Optional status indicator (online/offline)
|
|
8
|
+
- Avatar group for stacking
|
|
9
|
+
|
|
10
|
+
## Tailwind Classes
|
|
11
|
+
|
|
12
|
+
### Container
|
|
13
|
+
```
|
|
14
|
+
relative flex shrink-0 overflow-hidden rounded-full
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Sizes
|
|
18
|
+
```
|
|
19
|
+
xs: h-6 w-6 text-xs
|
|
20
|
+
sm: h-8 w-8 text-xs
|
|
21
|
+
md: h-10 w-10 text-sm (default)
|
|
22
|
+
lg: h-12 w-12 text-base
|
|
23
|
+
xl: h-16 w-16 text-lg
|
|
24
|
+
2xl: h-24 w-24 text-xl
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Image
|
|
28
|
+
```
|
|
29
|
+
aspect-square h-full w-full object-cover
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Fallback (initials or icon)
|
|
33
|
+
```
|
|
34
|
+
flex h-full w-full items-center justify-center rounded-full
|
|
35
|
+
bg-muted text-muted-foreground font-medium
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Status Indicator
|
|
39
|
+
```
|
|
40
|
+
absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-background
|
|
41
|
+
online: bg-green-500
|
|
42
|
+
offline: bg-gray-400
|
|
43
|
+
busy: bg-red-500
|
|
44
|
+
away: bg-yellow-500
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Avatar Group
|
|
48
|
+
```
|
|
49
|
+
flex -space-x-2
|
|
50
|
+
[&>*]:ring-2 [&>*]:ring-background
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Props Interface
|
|
54
|
+
```typescript
|
|
55
|
+
interface AvatarProps {
|
|
56
|
+
src?: string
|
|
57
|
+
alt?: string
|
|
58
|
+
fallback?: string | React.ReactNode
|
|
59
|
+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
|
60
|
+
status?: 'online' | 'offline' | 'busy' | 'away'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface AvatarGroupProps {
|
|
64
|
+
max?: number
|
|
65
|
+
children: React.ReactNode
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Do
|
|
70
|
+
- Use Radix Avatar for image loading states
|
|
71
|
+
- Show fallback immediately while image loads
|
|
72
|
+
- Support both initials and icon fallbacks
|
|
73
|
+
- Use ring for avatar group overlap effect
|
|
74
|
+
|
|
75
|
+
## Don't
|
|
76
|
+
- Hardcode colors
|
|
77
|
+
- Forget alt text for images
|
|
78
|
+
- Use non-square aspect ratios
|
|
79
|
+
- Skip loading state handling
|
|
80
|
+
|
|
81
|
+
## Example
|
|
82
|
+
```tsx
|
|
83
|
+
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
|
84
|
+
import { cn } from '@/lib/utils'
|
|
85
|
+
import { User } from 'lucide-react'
|
|
86
|
+
|
|
87
|
+
const avatarSizes = {
|
|
88
|
+
xs: 'h-6 w-6 text-xs',
|
|
89
|
+
sm: 'h-8 w-8 text-xs',
|
|
90
|
+
md: 'h-10 w-10 text-sm',
|
|
91
|
+
lg: 'h-12 w-12 text-base',
|
|
92
|
+
xl: 'h-16 w-16 text-lg',
|
|
93
|
+
'2xl': 'h-24 w-24 text-xl',
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const statusColors = {
|
|
97
|
+
online: 'bg-green-500',
|
|
98
|
+
offline: 'bg-gray-400',
|
|
99
|
+
busy: 'bg-red-500',
|
|
100
|
+
away: 'bg-yellow-500',
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const Avatar = ({ src, alt, fallback, size = 'md', status, className }) => (
|
|
104
|
+
<div className="relative inline-block">
|
|
105
|
+
<AvatarPrimitive.Root
|
|
106
|
+
className={cn(
|
|
107
|
+
'relative flex shrink-0 overflow-hidden rounded-full',
|
|
108
|
+
avatarSizes[size],
|
|
109
|
+
className
|
|
110
|
+
)}
|
|
111
|
+
>
|
|
112
|
+
<AvatarPrimitive.Image
|
|
113
|
+
src={src}
|
|
114
|
+
alt={alt}
|
|
115
|
+
className="aspect-square h-full w-full object-cover"
|
|
116
|
+
/>
|
|
117
|
+
<AvatarPrimitive.Fallback
|
|
118
|
+
className="flex h-full w-full items-center justify-center rounded-full bg-muted text-muted-foreground font-medium"
|
|
119
|
+
>
|
|
120
|
+
{fallback || <User className="h-1/2 w-1/2" />}
|
|
121
|
+
</AvatarPrimitive.Fallback>
|
|
122
|
+
</AvatarPrimitive.Root>
|
|
123
|
+
{status && (
|
|
124
|
+
<span
|
|
125
|
+
className={cn(
|
|
126
|
+
'absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-background',
|
|
127
|
+
statusColors[status]
|
|
128
|
+
)}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
// Helper to get initials from name
|
|
135
|
+
const getInitials = (name: string) => {
|
|
136
|
+
return name
|
|
137
|
+
.split(' ')
|
|
138
|
+
.map(n => n[0])
|
|
139
|
+
.join('')
|
|
140
|
+
.toUpperCase()
|
|
141
|
+
.slice(0, 2)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Avatar group for stacking
|
|
145
|
+
const AvatarGroup = ({ max = 4, children }) => {
|
|
146
|
+
const avatars = React.Children.toArray(children)
|
|
147
|
+
const shown = avatars.slice(0, max)
|
|
148
|
+
const remaining = avatars.length - max
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div className="flex -space-x-2">
|
|
152
|
+
{shown.map((avatar, i) => (
|
|
153
|
+
<div key={i} className="ring-2 ring-background rounded-full">
|
|
154
|
+
{avatar}
|
|
155
|
+
</div>
|
|
156
|
+
))}
|
|
157
|
+
{remaining > 0 && (
|
|
158
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted text-muted-foreground text-sm font-medium ring-2 ring-background">
|
|
159
|
+
+{remaining}
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
```
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Badge Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Inline `<span>` or `<div>` element
|
|
5
|
+
- Support variants for different contexts
|
|
6
|
+
- Optional icon or close button
|
|
7
|
+
- Compact sizing
|
|
8
|
+
|
|
9
|
+
## Tailwind Classes
|
|
10
|
+
|
|
11
|
+
### Base
|
|
12
|
+
```
|
|
13
|
+
inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium
|
|
14
|
+
transition-colors
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Variants
|
|
18
|
+
```
|
|
19
|
+
default: bg-primary text-primary-foreground
|
|
20
|
+
secondary: bg-secondary text-secondary-foreground
|
|
21
|
+
outline: border border-border text-foreground bg-transparent
|
|
22
|
+
destructive: bg-destructive text-destructive-foreground
|
|
23
|
+
success: bg-green-500/10 text-green-600 border border-green-500/20
|
|
24
|
+
warning: bg-yellow-500/10 text-yellow-600 border border-yellow-500/20
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Sizes
|
|
28
|
+
```
|
|
29
|
+
sm: px-2 py-0.5 text-xs
|
|
30
|
+
md: px-2.5 py-0.5 text-xs (default)
|
|
31
|
+
lg: px-3 py-1 text-sm
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### With Icon
|
|
35
|
+
```
|
|
36
|
+
gap-1
|
|
37
|
+
Icon: h-3 w-3
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### With Close Button
|
|
41
|
+
```
|
|
42
|
+
pr-1
|
|
43
|
+
Close button: ml-1 h-3 w-3 hover:bg-black/10 rounded-full
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Interactive (clickable)
|
|
47
|
+
```
|
|
48
|
+
cursor-pointer hover:opacity-80
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Props Interface
|
|
52
|
+
```typescript
|
|
53
|
+
interface BadgeProps {
|
|
54
|
+
variant?: 'default' | 'secondary' | 'outline' | 'destructive' | 'success' | 'warning'
|
|
55
|
+
size?: 'sm' | 'md' | 'lg'
|
|
56
|
+
icon?: React.ReactNode
|
|
57
|
+
onClose?: () => void
|
|
58
|
+
children: React.ReactNode
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Do
|
|
63
|
+
- Use `rounded-full` for pill shape (or `{tokens.radius}` for consistency)
|
|
64
|
+
- Keep text short (1-2 words typically)
|
|
65
|
+
- Use semantic variants (success, warning, destructive)
|
|
66
|
+
- Support icon placement
|
|
67
|
+
|
|
68
|
+
## Don't
|
|
69
|
+
- Make badges too large
|
|
70
|
+
- Use for long text content
|
|
71
|
+
- Hardcode colors
|
|
72
|
+
- Forget hover state for interactive badges
|
|
73
|
+
|
|
74
|
+
## Example
|
|
75
|
+
```tsx
|
|
76
|
+
import { cn } from '@/lib/utils'
|
|
77
|
+
import { X } from 'lucide-react'
|
|
78
|
+
|
|
79
|
+
const badgeVariants = {
|
|
80
|
+
default: 'bg-primary text-primary-foreground',
|
|
81
|
+
secondary: 'bg-secondary text-secondary-foreground',
|
|
82
|
+
outline: 'border border-border text-foreground bg-transparent',
|
|
83
|
+
destructive: 'bg-destructive text-destructive-foreground',
|
|
84
|
+
success: 'bg-green-500/10 text-green-600 border border-green-500/20',
|
|
85
|
+
warning: 'bg-yellow-500/10 text-yellow-600 border border-yellow-500/20',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const badgeSizes = {
|
|
89
|
+
sm: 'px-2 py-0.5 text-xs',
|
|
90
|
+
md: 'px-2.5 py-0.5 text-xs',
|
|
91
|
+
lg: 'px-3 py-1 text-sm',
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const Badge = ({
|
|
95
|
+
variant = 'default',
|
|
96
|
+
size = 'md',
|
|
97
|
+
icon,
|
|
98
|
+
onClose,
|
|
99
|
+
className,
|
|
100
|
+
children,
|
|
101
|
+
...props
|
|
102
|
+
}) => (
|
|
103
|
+
<span
|
|
104
|
+
className={cn(
|
|
105
|
+
'inline-flex items-center rounded-full font-medium transition-colors',
|
|
106
|
+
badgeVariants[variant],
|
|
107
|
+
badgeSizes[size],
|
|
108
|
+
icon && 'gap-1',
|
|
109
|
+
onClose && 'pr-1',
|
|
110
|
+
className
|
|
111
|
+
)}
|
|
112
|
+
{...props}
|
|
113
|
+
>
|
|
114
|
+
{icon && <span className="h-3 w-3">{icon}</span>}
|
|
115
|
+
{children}
|
|
116
|
+
{onClose && (
|
|
117
|
+
<button
|
|
118
|
+
onClick={onClose}
|
|
119
|
+
className="ml-1 rounded-full p-0.5 hover:bg-black/10"
|
|
120
|
+
>
|
|
121
|
+
<X className="h-3 w-3" />
|
|
122
|
+
</button>
|
|
123
|
+
)}
|
|
124
|
+
</span>
|
|
125
|
+
)
|
|
126
|
+
```
|