@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,186 @@
|
|
|
1
|
+
# Skeleton Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Placeholder shapes that mimic content layout
|
|
5
|
+
- Subtle animation (pulse or shimmer)
|
|
6
|
+
- Various preset shapes (text, circle, rectangle)
|
|
7
|
+
- Composable for complex layouts
|
|
8
|
+
|
|
9
|
+
## Tailwind Classes
|
|
10
|
+
|
|
11
|
+
### Base
|
|
12
|
+
```
|
|
13
|
+
animate-pulse rounded-md bg-muted
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Shapes
|
|
17
|
+
```
|
|
18
|
+
text: h-4 w-full rounded
|
|
19
|
+
heading: h-6 w-3/4 rounded
|
|
20
|
+
paragraph: h-4 w-full rounded (multiple with varying widths)
|
|
21
|
+
circle: rounded-full aspect-square
|
|
22
|
+
rectangle: rounded-md
|
|
23
|
+
avatar: h-10 w-10 rounded-full
|
|
24
|
+
button: h-10 w-24 rounded-lg
|
|
25
|
+
card: h-48 w-full rounded-lg
|
|
26
|
+
image: aspect-video w-full rounded-lg
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Animation Variants
|
|
30
|
+
```
|
|
31
|
+
pulse: animate-pulse (default, uses opacity)
|
|
32
|
+
shimmer: animate-shimmer (gradient sweep)
|
|
33
|
+
|
|
34
|
+
@keyframes shimmer {
|
|
35
|
+
0% { background-position: -200% 0; }
|
|
36
|
+
100% { background-position: 200% 0; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
shimmer class:
|
|
40
|
+
bg-gradient-to-r from-muted via-muted/50 to-muted
|
|
41
|
+
bg-[length:200%_100%] animate-shimmer
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Sizes
|
|
45
|
+
```
|
|
46
|
+
sm: Scale down by 75%
|
|
47
|
+
md: Default size
|
|
48
|
+
lg: Scale up by 125%
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Props Interface
|
|
52
|
+
```typescript
|
|
53
|
+
interface SkeletonProps {
|
|
54
|
+
className?: string
|
|
55
|
+
variant?: 'pulse' | 'shimmer'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface SkeletonTextProps extends SkeletonProps {
|
|
59
|
+
lines?: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface SkeletonAvatarProps extends SkeletonProps {
|
|
63
|
+
size?: 'sm' | 'md' | 'lg'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface SkeletonCardProps extends SkeletonProps {
|
|
67
|
+
hasImage?: boolean
|
|
68
|
+
hasTitle?: boolean
|
|
69
|
+
hasDescription?: boolean
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Do
|
|
74
|
+
- Match skeleton shapes to actual content layout
|
|
75
|
+
- Use consistent animation across page
|
|
76
|
+
- Provide presets for common patterns
|
|
77
|
+
- Keep animations subtle (not distracting)
|
|
78
|
+
|
|
79
|
+
## Don't
|
|
80
|
+
- Hardcode colors
|
|
81
|
+
- Use jarring animations
|
|
82
|
+
- Show skeleton indefinitely (add timeout)
|
|
83
|
+
- Mix different animation styles
|
|
84
|
+
|
|
85
|
+
## Example
|
|
86
|
+
```tsx
|
|
87
|
+
import { cn } from '@/lib/utils'
|
|
88
|
+
|
|
89
|
+
const Skeleton = ({ className, ...props }) => (
|
|
90
|
+
<div
|
|
91
|
+
className={cn('animate-pulse rounded-md bg-muted', className)}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
// Shimmer variant
|
|
97
|
+
const SkeletonShimmer = ({ className, ...props }) => (
|
|
98
|
+
<div
|
|
99
|
+
className={cn(
|
|
100
|
+
'rounded-md bg-gradient-to-r from-muted via-muted/50 to-muted',
|
|
101
|
+
'bg-[length:200%_100%] animate-shimmer',
|
|
102
|
+
className
|
|
103
|
+
)}
|
|
104
|
+
{...props}
|
|
105
|
+
/>
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
// Text skeleton (multiple lines)
|
|
109
|
+
const SkeletonText = ({ lines = 3, className }) => (
|
|
110
|
+
<div className={cn('space-y-2', className)}>
|
|
111
|
+
{Array.from({ length: lines }).map((_, i) => (
|
|
112
|
+
<Skeleton
|
|
113
|
+
key={i}
|
|
114
|
+
className={cn(
|
|
115
|
+
'h-4',
|
|
116
|
+
i === lines - 1 ? 'w-4/5' : 'w-full' // Last line shorter
|
|
117
|
+
)}
|
|
118
|
+
/>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// Avatar skeleton
|
|
124
|
+
const SkeletonAvatar = ({ size = 'md', className }) => {
|
|
125
|
+
const sizes = {
|
|
126
|
+
sm: 'h-8 w-8',
|
|
127
|
+
md: 'h-10 w-10',
|
|
128
|
+
lg: 'h-12 w-12',
|
|
129
|
+
}
|
|
130
|
+
return <Skeleton className={cn('rounded-full', sizes[size], className)} />
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Card skeleton
|
|
134
|
+
const SkeletonCard = ({ hasImage = true, className }) => (
|
|
135
|
+
<div className={cn('rounded-lg border border-border p-4 space-y-4', className)}>
|
|
136
|
+
{hasImage && <Skeleton className="aspect-video w-full rounded-lg" />}
|
|
137
|
+
<div className="space-y-2">
|
|
138
|
+
<Skeleton className="h-5 w-2/3" />
|
|
139
|
+
<Skeleton className="h-4 w-full" />
|
|
140
|
+
<Skeleton className="h-4 w-4/5" />
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
// Table row skeleton
|
|
146
|
+
const SkeletonTableRow = ({ columns = 4 }) => (
|
|
147
|
+
<div className="flex items-center space-x-4 py-3">
|
|
148
|
+
{Array.from({ length: columns }).map((_, i) => (
|
|
149
|
+
<Skeleton
|
|
150
|
+
key={i}
|
|
151
|
+
className={cn(
|
|
152
|
+
'h-4',
|
|
153
|
+
i === 0 ? 'w-8' : 'flex-1'
|
|
154
|
+
)}
|
|
155
|
+
/>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
// Full page skeleton (dashboard example)
|
|
161
|
+
const SkeletonDashboard = () => (
|
|
162
|
+
<div className="space-y-6 p-6">
|
|
163
|
+
{/* Header */}
|
|
164
|
+
<div className="flex items-center justify-between">
|
|
165
|
+
<Skeleton className="h-8 w-48" />
|
|
166
|
+
<Skeleton className="h-10 w-32" />
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{/* Stats row */}
|
|
170
|
+
<div className="grid grid-cols-4 gap-4">
|
|
171
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
172
|
+
<div key={i} className="rounded-lg border border-border p-4 space-y-2">
|
|
173
|
+
<Skeleton className="h-4 w-20" />
|
|
174
|
+
<Skeleton className="h-8 w-16" />
|
|
175
|
+
</div>
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Content area */}
|
|
180
|
+
<div className="grid grid-cols-2 gap-6">
|
|
181
|
+
<SkeletonCard />
|
|
182
|
+
<SkeletonCard />
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
```
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Slider Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Track with filled range indicator
|
|
5
|
+
- Draggable thumb(s)
|
|
6
|
+
- Support single value or range (two thumbs)
|
|
7
|
+
- Optional step markers and value display
|
|
8
|
+
|
|
9
|
+
## Tailwind Classes
|
|
10
|
+
|
|
11
|
+
### Root Container
|
|
12
|
+
```
|
|
13
|
+
relative flex w-full touch-none select-none items-center
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Track
|
|
17
|
+
```
|
|
18
|
+
relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Range (filled portion)
|
|
22
|
+
```
|
|
23
|
+
absolute h-full bg-primary
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Thumb
|
|
27
|
+
```
|
|
28
|
+
block h-4 w-4 rounded-full border border-primary/50 bg-background shadow
|
|
29
|
+
transition-colors
|
|
30
|
+
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary
|
|
31
|
+
disabled:pointer-events-none disabled:opacity-50
|
|
32
|
+
hover:border-primary
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Sizes
|
|
36
|
+
```
|
|
37
|
+
sm: Track h-1, Thumb h-3 w-3
|
|
38
|
+
md: Track h-1.5, Thumb h-4 w-4 (default)
|
|
39
|
+
lg: Track h-2, Thumb h-5 w-5
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### With Value Display
|
|
43
|
+
```
|
|
44
|
+
Value label: absolute -top-7 left-1/2 -translate-x-1/2 text-xs font-medium
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Props Interface
|
|
48
|
+
```typescript
|
|
49
|
+
interface SliderProps {
|
|
50
|
+
value?: number[]
|
|
51
|
+
defaultValue?: number[]
|
|
52
|
+
onValueChange?: (value: number[]) => void
|
|
53
|
+
min?: number
|
|
54
|
+
max?: number
|
|
55
|
+
step?: number
|
|
56
|
+
disabled?: boolean
|
|
57
|
+
orientation?: 'horizontal' | 'vertical'
|
|
58
|
+
inverted?: boolean
|
|
59
|
+
minStepsBetweenThumbs?: number
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Do
|
|
64
|
+
- Use Radix Slider for accessibility
|
|
65
|
+
- Support keyboard navigation (arrows, Home, End)
|
|
66
|
+
- Show current value on drag (tooltip or label)
|
|
67
|
+
- Support both single and range modes
|
|
68
|
+
|
|
69
|
+
## Don't
|
|
70
|
+
- Hardcode colors
|
|
71
|
+
- Forget touch support
|
|
72
|
+
- Skip min/max/step configuration
|
|
73
|
+
- Use for non-numeric values
|
|
74
|
+
|
|
75
|
+
## Example
|
|
76
|
+
```tsx
|
|
77
|
+
import * as SliderPrimitive from '@radix-ui/react-slider'
|
|
78
|
+
import { cn } from '@/lib/utils'
|
|
79
|
+
|
|
80
|
+
const Slider = ({ className, ...props }) => (
|
|
81
|
+
<SliderPrimitive.Root
|
|
82
|
+
className={cn(
|
|
83
|
+
'relative flex w-full touch-none select-none items-center',
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
{...props}
|
|
87
|
+
>
|
|
88
|
+
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted">
|
|
89
|
+
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
|
90
|
+
</SliderPrimitive.Track>
|
|
91
|
+
<SliderPrimitive.Thumb
|
|
92
|
+
className={cn(
|
|
93
|
+
'block h-4 w-4 rounded-full border border-primary/50 bg-background shadow',
|
|
94
|
+
'transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
|
95
|
+
'disabled:pointer-events-none disabled:opacity-50 hover:border-primary'
|
|
96
|
+
)}
|
|
97
|
+
/>
|
|
98
|
+
</SliderPrimitive.Root>
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
// With value display
|
|
102
|
+
const SliderWithValue = ({ value, onValueChange, ...props }) => (
|
|
103
|
+
<div className="space-y-2">
|
|
104
|
+
<div className="flex justify-between text-sm">
|
|
105
|
+
<span className="text-muted-foreground">Value</span>
|
|
106
|
+
<span className="font-medium">{value?.[0] ?? 0}</span>
|
|
107
|
+
</div>
|
|
108
|
+
<Slider value={value} onValueChange={onValueChange} {...props} />
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// Range slider (two thumbs)
|
|
113
|
+
<Slider defaultValue={[25, 75]} max={100} step={1} />
|
|
114
|
+
```
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Spinner Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Animated loading indicator
|
|
5
|
+
- SVG-based for crisp rendering
|
|
6
|
+
- Support for multiple sizes
|
|
7
|
+
- Optional label text
|
|
8
|
+
- Accessible with aria-label
|
|
9
|
+
|
|
10
|
+
## Tailwind Classes
|
|
11
|
+
|
|
12
|
+
### Base
|
|
13
|
+
```
|
|
14
|
+
animate-spin text-primary
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### SVG Structure
|
|
18
|
+
```
|
|
19
|
+
<svg className="animate-spin" viewBox="0 0 24 24" fill="none">
|
|
20
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
21
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
22
|
+
</svg>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Sizes
|
|
26
|
+
```
|
|
27
|
+
xs: h-3 w-3
|
|
28
|
+
sm: h-4 w-4
|
|
29
|
+
md: h-6 w-6 (default)
|
|
30
|
+
lg: h-8 w-8
|
|
31
|
+
xl: h-12 w-12
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### With Label
|
|
35
|
+
```
|
|
36
|
+
Container: inline-flex items-center gap-2
|
|
37
|
+
Label: text-sm text-muted-foreground
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Overlay Spinner
|
|
41
|
+
```
|
|
42
|
+
Container: fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Button Spinner (inline)
|
|
46
|
+
```
|
|
47
|
+
mr-2 h-4 w-4 animate-spin
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Colors
|
|
51
|
+
```
|
|
52
|
+
default: text-primary
|
|
53
|
+
muted: text-muted-foreground
|
|
54
|
+
white: text-white
|
|
55
|
+
current: text-current (inherits)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Props Interface
|
|
59
|
+
```typescript
|
|
60
|
+
interface SpinnerProps {
|
|
61
|
+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
62
|
+
color?: 'default' | 'muted' | 'white' | 'current'
|
|
63
|
+
className?: string
|
|
64
|
+
label?: string
|
|
65
|
+
'aria-label'?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface LoadingOverlayProps {
|
|
69
|
+
visible: boolean
|
|
70
|
+
label?: string
|
|
71
|
+
blur?: boolean
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Do
|
|
76
|
+
- Always include aria-label for screen readers
|
|
77
|
+
- Use currentColor for flexible coloring
|
|
78
|
+
- Match spinner size to context (button, page, etc.)
|
|
79
|
+
- Consider reduced motion preferences
|
|
80
|
+
|
|
81
|
+
## Don't
|
|
82
|
+
- Hardcode colors
|
|
83
|
+
- Use CSS-only spinners (less control)
|
|
84
|
+
- Forget loading state announcements
|
|
85
|
+
- Make spinners too large or distracting
|
|
86
|
+
|
|
87
|
+
## Example
|
|
88
|
+
```tsx
|
|
89
|
+
import { cn } from '@/lib/utils'
|
|
90
|
+
|
|
91
|
+
const spinnerSizes = {
|
|
92
|
+
xs: 'h-3 w-3',
|
|
93
|
+
sm: 'h-4 w-4',
|
|
94
|
+
md: 'h-6 w-6',
|
|
95
|
+
lg: 'h-8 w-8',
|
|
96
|
+
xl: 'h-12 w-12',
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const spinnerColors = {
|
|
100
|
+
default: 'text-primary',
|
|
101
|
+
muted: 'text-muted-foreground',
|
|
102
|
+
white: 'text-white',
|
|
103
|
+
current: 'text-current',
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const Spinner = ({
|
|
107
|
+
size = 'md',
|
|
108
|
+
color = 'default',
|
|
109
|
+
className,
|
|
110
|
+
label,
|
|
111
|
+
'aria-label': ariaLabel = 'Loading',
|
|
112
|
+
}: SpinnerProps) => {
|
|
113
|
+
const spinner = (
|
|
114
|
+
<svg
|
|
115
|
+
className={cn(
|
|
116
|
+
'animate-spin',
|
|
117
|
+
spinnerSizes[size],
|
|
118
|
+
spinnerColors[color],
|
|
119
|
+
className
|
|
120
|
+
)}
|
|
121
|
+
viewBox="0 0 24 24"
|
|
122
|
+
fill="none"
|
|
123
|
+
aria-label={ariaLabel}
|
|
124
|
+
role="status"
|
|
125
|
+
>
|
|
126
|
+
<circle
|
|
127
|
+
className="opacity-25"
|
|
128
|
+
cx="12"
|
|
129
|
+
cy="12"
|
|
130
|
+
r="10"
|
|
131
|
+
stroke="currentColor"
|
|
132
|
+
strokeWidth="4"
|
|
133
|
+
/>
|
|
134
|
+
<path
|
|
135
|
+
className="opacity-75"
|
|
136
|
+
fill="currentColor"
|
|
137
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
138
|
+
/>
|
|
139
|
+
</svg>
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if (label) {
|
|
143
|
+
return (
|
|
144
|
+
<div className="inline-flex items-center gap-2">
|
|
145
|
+
{spinner}
|
|
146
|
+
<span className="text-sm text-muted-foreground">{label}</span>
|
|
147
|
+
</div>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return spinner
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Dots variant spinner
|
|
155
|
+
const DotsSpinner = ({ size = 'md', className }: SpinnerProps) => (
|
|
156
|
+
<div className={cn('flex items-center gap-1', className)}>
|
|
157
|
+
{[0, 1, 2].map((i) => (
|
|
158
|
+
<div
|
|
159
|
+
key={i}
|
|
160
|
+
className={cn(
|
|
161
|
+
'rounded-full bg-primary animate-bounce',
|
|
162
|
+
size === 'sm' ? 'h-1.5 w-1.5' : 'h-2 w-2'
|
|
163
|
+
)}
|
|
164
|
+
style={{ animationDelay: `${i * 0.1}s` }}
|
|
165
|
+
/>
|
|
166
|
+
))}
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
// Loading overlay
|
|
171
|
+
const LoadingOverlay = ({
|
|
172
|
+
visible,
|
|
173
|
+
label = 'Loading...',
|
|
174
|
+
blur = true,
|
|
175
|
+
}: LoadingOverlayProps) => {
|
|
176
|
+
if (!visible) return null
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div
|
|
180
|
+
className={cn(
|
|
181
|
+
'fixed inset-0 z-50 flex flex-col items-center justify-center',
|
|
182
|
+
'bg-background/80',
|
|
183
|
+
blur && 'backdrop-blur-sm'
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
<Spinner size="xl" />
|
|
187
|
+
{label && (
|
|
188
|
+
<p className="mt-4 text-sm text-muted-foreground">{label}</p>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Button with loading state
|
|
195
|
+
const LoadingButton = ({ loading, children, ...props }) => (
|
|
196
|
+
<Button disabled={loading} {...props}>
|
|
197
|
+
{loading && <Spinner size="sm" color="current" className="mr-2" />}
|
|
198
|
+
{children}
|
|
199
|
+
</Button>
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// Usage examples
|
|
203
|
+
<Spinner />
|
|
204
|
+
<Spinner size="lg" color="muted" />
|
|
205
|
+
<Spinner size="sm" label="Loading data..." />
|
|
206
|
+
<DotsSpinner />
|
|
207
|
+
<LoadingOverlay visible={isLoading} label="Saving changes..." />
|
|
208
|
+
|
|
209
|
+
<LoadingButton loading={isSubmitting}>
|
|
210
|
+
{isSubmitting ? 'Saving...' : 'Save'}
|
|
211
|
+
</LoadingButton>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Reduced Motion Support
|
|
215
|
+
```css
|
|
216
|
+
@media (prefers-reduced-motion: reduce) {
|
|
217
|
+
.animate-spin {
|
|
218
|
+
animation: none;
|
|
219
|
+
opacity: 0.5;
|
|
220
|
+
}
|
|
221
|
+
.animate-bounce {
|
|
222
|
+
animation: none;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Switch Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Toggle control with sliding thumb
|
|
5
|
+
- On/Off states with visual indication
|
|
6
|
+
- Optional label alongside
|
|
7
|
+
- Accessible with keyboard
|
|
8
|
+
|
|
9
|
+
## Tailwind Classes
|
|
10
|
+
|
|
11
|
+
### Root/Track
|
|
12
|
+
```
|
|
13
|
+
peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center {tokens.radius}
|
|
14
|
+
border-2 border-transparent shadow-sm transition-colors
|
|
15
|
+
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2
|
|
16
|
+
disabled:cursor-not-allowed disabled:opacity-50
|
|
17
|
+
data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Thumb
|
|
21
|
+
```
|
|
22
|
+
pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0
|
|
23
|
+
transition-transform
|
|
24
|
+
data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Sizes
|
|
28
|
+
```
|
|
29
|
+
sm: Track h-4 w-7, Thumb h-3 w-3, translate-x-3
|
|
30
|
+
md: Track h-5 w-9, Thumb h-4 w-4, translate-x-4 (default)
|
|
31
|
+
lg: Track h-6 w-11, Thumb h-5 w-5, translate-x-5
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### With Label
|
|
35
|
+
```
|
|
36
|
+
flex items-center space-x-2
|
|
37
|
+
Label: text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Props Interface
|
|
41
|
+
```typescript
|
|
42
|
+
interface SwitchProps {
|
|
43
|
+
checked?: boolean
|
|
44
|
+
onCheckedChange?: (checked: boolean) => void
|
|
45
|
+
disabled?: boolean
|
|
46
|
+
required?: boolean
|
|
47
|
+
name?: string
|
|
48
|
+
value?: string
|
|
49
|
+
id?: string
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Do
|
|
54
|
+
- Use Radix Switch for accessibility
|
|
55
|
+
- Include focus ring
|
|
56
|
+
- Animate thumb movement smoothly
|
|
57
|
+
- Use semantic colors (primary for checked)
|
|
58
|
+
|
|
59
|
+
## Don't
|
|
60
|
+
- Hardcode colors
|
|
61
|
+
- Skip keyboard support (Space to toggle)
|
|
62
|
+
- Forget disabled state
|
|
63
|
+
- Use for multiple selections (use checkboxes)
|
|
64
|
+
|
|
65
|
+
## Example
|
|
66
|
+
```tsx
|
|
67
|
+
import * as SwitchPrimitive from '@radix-ui/react-switch'
|
|
68
|
+
import { cn } from '@/lib/utils'
|
|
69
|
+
|
|
70
|
+
const Switch = ({ className, ...props }) => (
|
|
71
|
+
<SwitchPrimitive.Root
|
|
72
|
+
className={cn(
|
|
73
|
+
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full',
|
|
74
|
+
'border-2 border-transparent shadow-sm transition-colors',
|
|
75
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',
|
|
76
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
77
|
+
'data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted',
|
|
78
|
+
className
|
|
79
|
+
)}
|
|
80
|
+
{...props}
|
|
81
|
+
>
|
|
82
|
+
<SwitchPrimitive.Thumb
|
|
83
|
+
className={cn(
|
|
84
|
+
'pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0',
|
|
85
|
+
'transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
|
|
86
|
+
)}
|
|
87
|
+
/>
|
|
88
|
+
</SwitchPrimitive.Root>
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
// With label
|
|
92
|
+
const SwitchWithLabel = ({ label, id, ...props }) => (
|
|
93
|
+
<div className="flex items-center space-x-2">
|
|
94
|
+
<Switch id={id} {...props} />
|
|
95
|
+
<label htmlFor={id} className="text-sm font-medium leading-none">
|
|
96
|
+
{label}
|
|
97
|
+
</label>
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
```
|