@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,185 @@
|
|
|
1
|
+
# Progress Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Track container with filled indicator
|
|
5
|
+
- Support determinate (percentage) and indeterminate (loading)
|
|
6
|
+
- Optional label and value display
|
|
7
|
+
- Circular variant option
|
|
8
|
+
|
|
9
|
+
## Tailwind Classes
|
|
10
|
+
|
|
11
|
+
### Linear Progress
|
|
12
|
+
|
|
13
|
+
#### Root/Track
|
|
14
|
+
```
|
|
15
|
+
relative h-2 w-full overflow-hidden rounded-full bg-muted
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
#### Indicator (filled)
|
|
19
|
+
```
|
|
20
|
+
h-full w-full flex-1 bg-primary transition-all
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
#### Indeterminate Animation
|
|
24
|
+
```
|
|
25
|
+
animate-progress-indeterminate
|
|
26
|
+
@keyframes progress-indeterminate {
|
|
27
|
+
0% { transform: translateX(-100%); }
|
|
28
|
+
100% { transform: translateX(100%); }
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Sizes
|
|
33
|
+
```
|
|
34
|
+
sm: h-1
|
|
35
|
+
md: h-2 (default)
|
|
36
|
+
lg: h-3
|
|
37
|
+
xl: h-4
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Variants
|
|
41
|
+
```
|
|
42
|
+
default: bg-primary
|
|
43
|
+
success: bg-green-500
|
|
44
|
+
warning: bg-yellow-500
|
|
45
|
+
destructive: bg-destructive
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### With Label
|
|
49
|
+
```
|
|
50
|
+
Container: space-y-2
|
|
51
|
+
Label row: flex justify-between text-sm
|
|
52
|
+
Label: text-muted-foreground
|
|
53
|
+
Value: font-medium
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Circular Progress
|
|
57
|
+
```
|
|
58
|
+
Container: relative inline-flex
|
|
59
|
+
SVG: -rotate-90 transform
|
|
60
|
+
Track circle: stroke-muted
|
|
61
|
+
Indicator circle: stroke-primary transition-all
|
|
62
|
+
stroke-dasharray: circumference
|
|
63
|
+
stroke-dashoffset: circumference - (value/100 * circumference)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Props Interface
|
|
67
|
+
```typescript
|
|
68
|
+
interface ProgressProps {
|
|
69
|
+
value?: number // 0-100, undefined = indeterminate
|
|
70
|
+
max?: number
|
|
71
|
+
variant?: 'default' | 'success' | 'warning' | 'destructive'
|
|
72
|
+
size?: 'sm' | 'md' | 'lg' | 'xl'
|
|
73
|
+
showValue?: boolean
|
|
74
|
+
label?: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface CircularProgressProps extends ProgressProps {
|
|
78
|
+
strokeWidth?: number
|
|
79
|
+
diameter?: number
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Do
|
|
84
|
+
- Use Radix Progress for accessibility
|
|
85
|
+
- Support both determinate and indeterminate states
|
|
86
|
+
- Include aria-valuenow, aria-valuemin, aria-valuemax
|
|
87
|
+
- Animate smoothly between values
|
|
88
|
+
|
|
89
|
+
## Don't
|
|
90
|
+
- Hardcode colors
|
|
91
|
+
- Forget accessibility attributes
|
|
92
|
+
- Use jerky animations
|
|
93
|
+
- Skip indeterminate state support
|
|
94
|
+
|
|
95
|
+
## Example
|
|
96
|
+
```tsx
|
|
97
|
+
import * as ProgressPrimitive from '@radix-ui/react-progress'
|
|
98
|
+
import { cn } from '@/lib/utils'
|
|
99
|
+
|
|
100
|
+
const progressVariants = {
|
|
101
|
+
default: 'bg-primary',
|
|
102
|
+
success: 'bg-green-500',
|
|
103
|
+
warning: 'bg-yellow-500',
|
|
104
|
+
destructive: 'bg-destructive',
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const progressSizes = {
|
|
108
|
+
sm: 'h-1',
|
|
109
|
+
md: 'h-2',
|
|
110
|
+
lg: 'h-3',
|
|
111
|
+
xl: 'h-4',
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const Progress = ({
|
|
115
|
+
value,
|
|
116
|
+
variant = 'default',
|
|
117
|
+
size = 'md',
|
|
118
|
+
className,
|
|
119
|
+
...props
|
|
120
|
+
}) => (
|
|
121
|
+
<ProgressPrimitive.Root
|
|
122
|
+
className={cn(
|
|
123
|
+
'relative w-full overflow-hidden rounded-full bg-muted',
|
|
124
|
+
progressSizes[size],
|
|
125
|
+
className
|
|
126
|
+
)}
|
|
127
|
+
{...props}
|
|
128
|
+
>
|
|
129
|
+
<ProgressPrimitive.Indicator
|
|
130
|
+
className={cn(
|
|
131
|
+
'h-full w-full flex-1 transition-all',
|
|
132
|
+
progressVariants[variant],
|
|
133
|
+
value === undefined && 'animate-progress-indeterminate'
|
|
134
|
+
)}
|
|
135
|
+
style={{ transform: value !== undefined ? `translateX(-${100 - value}%)` : undefined }}
|
|
136
|
+
/>
|
|
137
|
+
</ProgressPrimitive.Root>
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
// With label and value
|
|
141
|
+
const ProgressWithLabel = ({ label, value, ...props }) => (
|
|
142
|
+
<div className="space-y-2">
|
|
143
|
+
<div className="flex justify-between text-sm">
|
|
144
|
+
<span className="text-muted-foreground">{label}</span>
|
|
145
|
+
<span className="font-medium">{value}%</span>
|
|
146
|
+
</div>
|
|
147
|
+
<Progress value={value} {...props} />
|
|
148
|
+
</div>
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// Circular progress
|
|
152
|
+
const CircularProgress = ({ value, size = 40, strokeWidth = 4 }) => {
|
|
153
|
+
const radius = (size - strokeWidth) / 2
|
|
154
|
+
const circumference = radius * 2 * Math.PI
|
|
155
|
+
const offset = value !== undefined
|
|
156
|
+
? circumference - (value / 100) * circumference
|
|
157
|
+
: 0
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<svg width={size} height={size} className="-rotate-90 transform">
|
|
161
|
+
<circle
|
|
162
|
+
cx={size / 2}
|
|
163
|
+
cy={size / 2}
|
|
164
|
+
r={radius}
|
|
165
|
+
fill="none"
|
|
166
|
+
stroke="currentColor"
|
|
167
|
+
strokeWidth={strokeWidth}
|
|
168
|
+
className="text-muted"
|
|
169
|
+
/>
|
|
170
|
+
<circle
|
|
171
|
+
cx={size / 2}
|
|
172
|
+
cy={size / 2}
|
|
173
|
+
r={radius}
|
|
174
|
+
fill="none"
|
|
175
|
+
stroke="currentColor"
|
|
176
|
+
strokeWidth={strokeWidth}
|
|
177
|
+
strokeDasharray={circumference}
|
|
178
|
+
strokeDashoffset={offset}
|
|
179
|
+
strokeLinecap="round"
|
|
180
|
+
className="text-primary transition-all"
|
|
181
|
+
/>
|
|
182
|
+
</svg>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
```
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Radio Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Radio group container
|
|
5
|
+
- Individual radio items with circular indicator
|
|
6
|
+
- Label for each option
|
|
7
|
+
- Optional description per item
|
|
8
|
+
|
|
9
|
+
## Tailwind Classes
|
|
10
|
+
|
|
11
|
+
### Radio Group
|
|
12
|
+
```
|
|
13
|
+
grid gap-2
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Radio Item
|
|
17
|
+
```
|
|
18
|
+
aspect-square h-4 w-4 rounded-full border border-border
|
|
19
|
+
ring-offset-background
|
|
20
|
+
focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2
|
|
21
|
+
disabled:cursor-not-allowed disabled:opacity-50
|
|
22
|
+
data-[state=checked]:border-primary
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Radio Indicator (inner dot)
|
|
26
|
+
```
|
|
27
|
+
flex items-center justify-center
|
|
28
|
+
after:content-[''] after:block after:h-2 after:w-2 after:rounded-full after:bg-primary
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Radio Item Wrapper (with label)
|
|
32
|
+
```
|
|
33
|
+
flex items-center space-x-2
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Label
|
|
37
|
+
```
|
|
38
|
+
text-sm font-medium leading-none text-foreground
|
|
39
|
+
peer-disabled:cursor-not-allowed peer-disabled:opacity-70
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Card Style Variant (selectable cards)
|
|
43
|
+
```
|
|
44
|
+
flex cursor-pointer items-start space-x-3 {tokens.radius} border border-border p-4
|
|
45
|
+
hover:bg-surface
|
|
46
|
+
data-[state=checked]:border-primary data-[state=checked]:bg-primary/5
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Props Interface
|
|
50
|
+
```typescript
|
|
51
|
+
interface RadioGroupProps {
|
|
52
|
+
value?: string
|
|
53
|
+
onValueChange?: (value: string) => void
|
|
54
|
+
disabled?: boolean
|
|
55
|
+
orientation?: 'horizontal' | 'vertical'
|
|
56
|
+
children: React.ReactNode
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface RadioGroupItemProps {
|
|
60
|
+
value: string
|
|
61
|
+
disabled?: boolean
|
|
62
|
+
id?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface RadioCardProps {
|
|
66
|
+
value: string
|
|
67
|
+
label: string
|
|
68
|
+
description?: string
|
|
69
|
+
disabled?: boolean
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Do
|
|
74
|
+
- Use Radix RadioGroup for accessibility
|
|
75
|
+
- Support horizontal and vertical layouts
|
|
76
|
+
- Include focus ring
|
|
77
|
+
- Animate indicator appearance
|
|
78
|
+
- Support card-style radio buttons
|
|
79
|
+
|
|
80
|
+
## Don't
|
|
81
|
+
- Hardcode colors
|
|
82
|
+
- Use actual `<input type="radio">` without styling
|
|
83
|
+
- Forget group semantics
|
|
84
|
+
- Skip disabled state
|
|
85
|
+
|
|
86
|
+
## Example
|
|
87
|
+
```tsx
|
|
88
|
+
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
|
|
89
|
+
import { Circle } from 'lucide-react'
|
|
90
|
+
import { cn } from '@/lib/utils'
|
|
91
|
+
|
|
92
|
+
const RadioGroup = ({ className, ...props }) => (
|
|
93
|
+
<RadioGroupPrimitive.Root className={cn('grid gap-2', className)} {...props} />
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const RadioGroupItem = ({ className, ...props }) => (
|
|
97
|
+
<RadioGroupPrimitive.Item
|
|
98
|
+
className={cn(
|
|
99
|
+
'aspect-square h-4 w-4 rounded-full border border-border',
|
|
100
|
+
'ring-offset-background',
|
|
101
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',
|
|
102
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
103
|
+
'data-[state=checked]:border-primary',
|
|
104
|
+
className
|
|
105
|
+
)}
|
|
106
|
+
{...props}
|
|
107
|
+
>
|
|
108
|
+
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
109
|
+
<Circle className="h-2 w-2 fill-primary text-primary" />
|
|
110
|
+
</RadioGroupPrimitive.Indicator>
|
|
111
|
+
</RadioGroupPrimitive.Item>
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// With label
|
|
115
|
+
const RadioWithLabel = ({ value, label, description, id }) => (
|
|
116
|
+
<div className="flex items-start space-x-2">
|
|
117
|
+
<RadioGroupItem value={value} id={id} />
|
|
118
|
+
<div className="grid gap-1.5 leading-none">
|
|
119
|
+
<label htmlFor={id} className="text-sm font-medium text-foreground">
|
|
120
|
+
{label}
|
|
121
|
+
</label>
|
|
122
|
+
{description && (
|
|
123
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
// Card style variant
|
|
130
|
+
const RadioCard = ({ value, label, description }) => (
|
|
131
|
+
<RadioGroupPrimitive.Item
|
|
132
|
+
value={value}
|
|
133
|
+
className={cn(
|
|
134
|
+
'flex cursor-pointer items-start space-x-3 rounded-lg border border-border p-4',
|
|
135
|
+
'hover:bg-surface',
|
|
136
|
+
'data-[state=checked]:border-primary data-[state=checked]:bg-primary/5'
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
<RadioGroupPrimitive.Indicator className="mt-1">
|
|
140
|
+
<Circle className="h-2 w-2 fill-primary text-primary" />
|
|
141
|
+
</RadioGroupPrimitive.Indicator>
|
|
142
|
+
<div>
|
|
143
|
+
<p className="font-medium text-foreground">{label}</p>
|
|
144
|
+
{description && <p className="text-sm text-muted-foreground">{description}</p>}
|
|
145
|
+
</div>
|
|
146
|
+
</RadioGroupPrimitive.Item>
|
|
147
|
+
)
|
|
148
|
+
```
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Select/Dropdown Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Trigger button that shows current value
|
|
5
|
+
- Dropdown content with scrollable list
|
|
6
|
+
- Option items with optional icons/checkmarks
|
|
7
|
+
- Support for groups and separators
|
|
8
|
+
|
|
9
|
+
## Tailwind Classes
|
|
10
|
+
|
|
11
|
+
### Trigger
|
|
12
|
+
```
|
|
13
|
+
flex h-10 w-full items-center justify-between {tokens.radius} border border-border
|
|
14
|
+
bg-background px-3 py-2 text-sm text-foreground
|
|
15
|
+
placeholder:text-muted-foreground
|
|
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
|
+
### Content (Dropdown)
|
|
21
|
+
```
|
|
22
|
+
relative z-50 max-h-96 min-w-[8rem] overflow-hidden {tokens.radius}
|
|
23
|
+
border border-border bg-background text-foreground {tokens.shadow}
|
|
24
|
+
data-[state=open]:animate-in data-[state=closed]:animate-out
|
|
25
|
+
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
|
|
26
|
+
data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95
|
|
27
|
+
data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Viewport (scrollable area)
|
|
31
|
+
```
|
|
32
|
+
p-1 max-h-[300px] overflow-y-auto
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Item
|
|
36
|
+
```
|
|
37
|
+
relative flex w-full cursor-pointer select-none items-center {tokens.radius} py-1.5 pl-8 pr-2
|
|
38
|
+
text-sm outline-none
|
|
39
|
+
focus:bg-surface focus:text-foreground
|
|
40
|
+
data-[disabled]:pointer-events-none data-[disabled]:opacity-50
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Selected Indicator
|
|
44
|
+
```
|
|
45
|
+
absolute left-2 flex h-3.5 w-3.5 items-center justify-center
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Separator
|
|
49
|
+
```
|
|
50
|
+
-mx-1 my-1 h-px bg-border
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Group Label
|
|
54
|
+
```
|
|
55
|
+
py-1.5 pl-8 pr-2 text-sm font-semibold text-foreground
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Props Interface
|
|
59
|
+
```typescript
|
|
60
|
+
interface SelectProps {
|
|
61
|
+
value?: string
|
|
62
|
+
onValueChange?: (value: string) => void
|
|
63
|
+
placeholder?: string
|
|
64
|
+
disabled?: boolean
|
|
65
|
+
children: React.ReactNode
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface SelectItemProps {
|
|
69
|
+
value: string
|
|
70
|
+
disabled?: boolean
|
|
71
|
+
children: React.ReactNode
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface SelectGroupProps {
|
|
75
|
+
label: string
|
|
76
|
+
children: React.ReactNode
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Do
|
|
81
|
+
- Use Radix Select for accessibility
|
|
82
|
+
- Show checkmark for selected item
|
|
83
|
+
- Include chevron icon on trigger
|
|
84
|
+
- Support keyboard navigation
|
|
85
|
+
- Handle long lists with scrolling
|
|
86
|
+
|
|
87
|
+
## Don't
|
|
88
|
+
- Hardcode colors
|
|
89
|
+
- Forget hover/focus states on items
|
|
90
|
+
- Skip selected state indicator
|
|
91
|
+
- Use custom scroll behavior
|
|
92
|
+
|
|
93
|
+
## Example
|
|
94
|
+
```tsx
|
|
95
|
+
import * as SelectPrimitive from '@radix-ui/react-select'
|
|
96
|
+
import { Check, ChevronDown } from 'lucide-react'
|
|
97
|
+
import { cn } from '@/lib/utils'
|
|
98
|
+
|
|
99
|
+
const Select = SelectPrimitive.Root
|
|
100
|
+
|
|
101
|
+
const SelectTrigger = ({ className, children, ...props }) => (
|
|
102
|
+
<SelectPrimitive.Trigger
|
|
103
|
+
className={cn(
|
|
104
|
+
'flex h-10 w-full items-center justify-between rounded-lg border border-border',
|
|
105
|
+
'bg-background px-3 py-2 text-sm',
|
|
106
|
+
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
|
|
107
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
108
|
+
className
|
|
109
|
+
)}
|
|
110
|
+
{...props}
|
|
111
|
+
>
|
|
112
|
+
{children}
|
|
113
|
+
<SelectPrimitive.Icon>
|
|
114
|
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
115
|
+
</SelectPrimitive.Icon>
|
|
116
|
+
</SelectPrimitive.Trigger>
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const SelectContent = ({ className, children, ...props }) => (
|
|
120
|
+
<SelectPrimitive.Portal>
|
|
121
|
+
<SelectPrimitive.Content
|
|
122
|
+
className={cn(
|
|
123
|
+
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-lg',
|
|
124
|
+
'border border-border bg-background shadow-md',
|
|
125
|
+
className
|
|
126
|
+
)}
|
|
127
|
+
{...props}
|
|
128
|
+
>
|
|
129
|
+
<SelectPrimitive.Viewport className="p-1">
|
|
130
|
+
{children}
|
|
131
|
+
</SelectPrimitive.Viewport>
|
|
132
|
+
</SelectPrimitive.Content>
|
|
133
|
+
</SelectPrimitive.Portal>
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
const SelectItem = ({ className, children, ...props }) => (
|
|
137
|
+
<SelectPrimitive.Item
|
|
138
|
+
className={cn(
|
|
139
|
+
'relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm',
|
|
140
|
+
'outline-none focus:bg-surface',
|
|
141
|
+
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
142
|
+
className
|
|
143
|
+
)}
|
|
144
|
+
{...props}
|
|
145
|
+
>
|
|
146
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
147
|
+
<SelectPrimitive.ItemIndicator>
|
|
148
|
+
<Check className="h-4 w-4" />
|
|
149
|
+
</SelectPrimitive.ItemIndicator>
|
|
150
|
+
</span>
|
|
151
|
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
152
|
+
</SelectPrimitive.Item>
|
|
153
|
+
)
|
|
154
|
+
```
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Separator Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Simple divider line
|
|
5
|
+
- Support horizontal and vertical orientations
|
|
6
|
+
- Decorative by default (not announced by screen readers)
|
|
7
|
+
- Optional label in center
|
|
8
|
+
|
|
9
|
+
## Tailwind Classes
|
|
10
|
+
|
|
11
|
+
### Base
|
|
12
|
+
```
|
|
13
|
+
shrink-0 bg-border
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Horizontal (default)
|
|
17
|
+
```
|
|
18
|
+
h-px w-full
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Vertical
|
|
22
|
+
```
|
|
23
|
+
h-full w-px
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### With Label
|
|
27
|
+
```
|
|
28
|
+
Container: relative flex items-center
|
|
29
|
+
Line: flex-1 h-px bg-border
|
|
30
|
+
Label: mx-4 text-xs text-muted-foreground uppercase tracking-wider
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Spacing Variants
|
|
34
|
+
```
|
|
35
|
+
sm: my-2 (horizontal) / mx-2 (vertical)
|
|
36
|
+
md: my-4 (horizontal) / mx-4 (vertical)
|
|
37
|
+
lg: my-6 (horizontal) / mx-6 (vertical)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Style Variants
|
|
41
|
+
```
|
|
42
|
+
solid: bg-border (default)
|
|
43
|
+
dashed: border-t border-dashed border-border bg-transparent h-0
|
|
44
|
+
dotted: border-t border-dotted border-border bg-transparent h-0
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Props Interface
|
|
48
|
+
```typescript
|
|
49
|
+
interface SeparatorProps {
|
|
50
|
+
orientation?: 'horizontal' | 'vertical'
|
|
51
|
+
decorative?: boolean
|
|
52
|
+
className?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface SeparatorWithLabelProps extends SeparatorProps {
|
|
56
|
+
label: string
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Do
|
|
61
|
+
- Use Radix Separator for semantic correctness
|
|
62
|
+
- Set decorative={true} for visual-only separators
|
|
63
|
+
- Use consistent spacing in lists/groups
|
|
64
|
+
- Support both orientations
|
|
65
|
+
|
|
66
|
+
## Don't
|
|
67
|
+
- Hardcode colors
|
|
68
|
+
- Use <hr> without proper styling
|
|
69
|
+
- Forget to set decorative prop appropriately
|
|
70
|
+
- Over-use separators (whitespace often suffices)
|
|
71
|
+
|
|
72
|
+
## Example
|
|
73
|
+
```tsx
|
|
74
|
+
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
|
75
|
+
import { cn } from '@/lib/utils'
|
|
76
|
+
|
|
77
|
+
const Separator = ({
|
|
78
|
+
className,
|
|
79
|
+
orientation = 'horizontal',
|
|
80
|
+
decorative = true,
|
|
81
|
+
...props
|
|
82
|
+
}) => (
|
|
83
|
+
<SeparatorPrimitive.Root
|
|
84
|
+
decorative={decorative}
|
|
85
|
+
orientation={orientation}
|
|
86
|
+
className={cn(
|
|
87
|
+
'shrink-0 bg-border',
|
|
88
|
+
orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',
|
|
89
|
+
className
|
|
90
|
+
)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
// With label
|
|
96
|
+
const SeparatorWithLabel = ({ label, className, ...props }) => (
|
|
97
|
+
<div className={cn('relative flex items-center', className)} {...props}>
|
|
98
|
+
<div className="flex-1 h-px bg-border" />
|
|
99
|
+
<span className="mx-4 text-xs text-muted-foreground uppercase tracking-wider">
|
|
100
|
+
{label}
|
|
101
|
+
</span>
|
|
102
|
+
<div className="flex-1 h-px bg-border" />
|
|
103
|
+
</div>
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
// Usage examples
|
|
107
|
+
<div className="space-y-4">
|
|
108
|
+
<div>Content above</div>
|
|
109
|
+
<Separator />
|
|
110
|
+
<div>Content below</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
// Vertical in flex container
|
|
114
|
+
<div className="flex h-5 items-center space-x-4 text-sm">
|
|
115
|
+
<div>Blog</div>
|
|
116
|
+
<Separator orientation="vertical" />
|
|
117
|
+
<div>Docs</div>
|
|
118
|
+
<Separator orientation="vertical" />
|
|
119
|
+
<div>Source</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
// With label
|
|
123
|
+
<SeparatorWithLabel label="Or continue with" />
|
|
124
|
+
```
|