@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,259 @@
|
|
|
1
|
+
# Label Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Text label associated with form controls
|
|
5
|
+
- Uses htmlFor to link to input elements
|
|
6
|
+
- Support for required indicator
|
|
7
|
+
- Support for disabled state
|
|
8
|
+
- Optional description/helper text
|
|
9
|
+
|
|
10
|
+
## Tailwind Classes
|
|
11
|
+
|
|
12
|
+
### Base
|
|
13
|
+
```
|
|
14
|
+
text-sm font-medium leading-none
|
|
15
|
+
peer-disabled:cursor-not-allowed peer-disabled:opacity-70
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Required Indicator
|
|
19
|
+
```
|
|
20
|
+
text-destructive ml-1
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Optional Indicator
|
|
24
|
+
```
|
|
25
|
+
text-muted-foreground ml-1 font-normal
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Description
|
|
29
|
+
```
|
|
30
|
+
text-sm text-muted-foreground
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### With Input Group
|
|
34
|
+
```
|
|
35
|
+
Container: space-y-2
|
|
36
|
+
Label + Input: flex flex-col gap-1.5
|
|
37
|
+
Inline: flex items-center gap-2
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Sizes
|
|
41
|
+
```
|
|
42
|
+
sm: text-xs
|
|
43
|
+
md: text-sm (default)
|
|
44
|
+
lg: text-base
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Error State
|
|
48
|
+
```
|
|
49
|
+
text-destructive
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Props Interface
|
|
53
|
+
```typescript
|
|
54
|
+
interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
|
55
|
+
required?: boolean
|
|
56
|
+
optional?: boolean
|
|
57
|
+
disabled?: boolean
|
|
58
|
+
error?: boolean
|
|
59
|
+
className?: string
|
|
60
|
+
children: React.ReactNode
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface LabelWithDescriptionProps extends LabelProps {
|
|
64
|
+
description?: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface FormFieldProps {
|
|
68
|
+
label: string
|
|
69
|
+
description?: string
|
|
70
|
+
error?: string
|
|
71
|
+
required?: boolean
|
|
72
|
+
children: React.ReactNode
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Do
|
|
77
|
+
- Always use htmlFor to associate with input
|
|
78
|
+
- Use Radix Label primitive for accessibility
|
|
79
|
+
- Include required indicator for required fields
|
|
80
|
+
- Keep labels concise and clear
|
|
81
|
+
|
|
82
|
+
## Don't
|
|
83
|
+
- Hardcode colors
|
|
84
|
+
- Use placeholder as label substitute
|
|
85
|
+
- Hide labels (use sr-only if visually hidden)
|
|
86
|
+
- Forget to link label to input
|
|
87
|
+
|
|
88
|
+
## Example
|
|
89
|
+
```tsx
|
|
90
|
+
import * as LabelPrimitive from '@radix-ui/react-label'
|
|
91
|
+
import { cn } from '@/lib/utils'
|
|
92
|
+
|
|
93
|
+
const Label = ({
|
|
94
|
+
className,
|
|
95
|
+
required,
|
|
96
|
+
optional,
|
|
97
|
+
disabled,
|
|
98
|
+
error,
|
|
99
|
+
children,
|
|
100
|
+
...props
|
|
101
|
+
}: LabelProps) => (
|
|
102
|
+
<LabelPrimitive.Root
|
|
103
|
+
className={cn(
|
|
104
|
+
'text-sm font-medium leading-none',
|
|
105
|
+
'peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
|
106
|
+
disabled && 'cursor-not-allowed opacity-70',
|
|
107
|
+
error && 'text-destructive',
|
|
108
|
+
className
|
|
109
|
+
)}
|
|
110
|
+
{...props}
|
|
111
|
+
>
|
|
112
|
+
{children}
|
|
113
|
+
{required && <span className="text-destructive ml-1" aria-hidden="true">*</span>}
|
|
114
|
+
{optional && <span className="text-muted-foreground ml-1 font-normal">(optional)</span>}
|
|
115
|
+
</LabelPrimitive.Root>
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
// Label with description
|
|
119
|
+
const LabelWithDescription = ({
|
|
120
|
+
description,
|
|
121
|
+
children,
|
|
122
|
+
...props
|
|
123
|
+
}: LabelWithDescriptionProps) => (
|
|
124
|
+
<div className="space-y-1">
|
|
125
|
+
<Label {...props}>{children}</Label>
|
|
126
|
+
{description && (
|
|
127
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
// Complete form field wrapper
|
|
133
|
+
const FormField = ({
|
|
134
|
+
label,
|
|
135
|
+
description,
|
|
136
|
+
error,
|
|
137
|
+
required,
|
|
138
|
+
children,
|
|
139
|
+
className,
|
|
140
|
+
}: FormFieldProps) => {
|
|
141
|
+
const id = React.useId()
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div className={cn('space-y-2', className)}>
|
|
145
|
+
<Label htmlFor={id} required={required} error={!!error}>
|
|
146
|
+
{label}
|
|
147
|
+
</Label>
|
|
148
|
+
{description && !error && (
|
|
149
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
150
|
+
)}
|
|
151
|
+
{React.cloneElement(children as React.ReactElement, { id, 'aria-invalid': !!error })}
|
|
152
|
+
{error && (
|
|
153
|
+
<p className="text-sm text-destructive" role="alert">{error}</p>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Inline label (for checkboxes/radios)
|
|
160
|
+
const InlineLabel = ({ className, children, ...props }: LabelProps) => (
|
|
161
|
+
<Label
|
|
162
|
+
className={cn('font-normal cursor-pointer', className)}
|
|
163
|
+
{...props}
|
|
164
|
+
>
|
|
165
|
+
{children}
|
|
166
|
+
</Label>
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// Usage examples
|
|
170
|
+
|
|
171
|
+
// Basic label with input
|
|
172
|
+
<div className="space-y-2">
|
|
173
|
+
<Label htmlFor="email">Email address</Label>
|
|
174
|
+
<Input id="email" type="email" placeholder="you@example.com" />
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
// Required field
|
|
178
|
+
<div className="space-y-2">
|
|
179
|
+
<Label htmlFor="name" required>Full name</Label>
|
|
180
|
+
<Input id="name" required />
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
// Optional field
|
|
184
|
+
<div className="space-y-2">
|
|
185
|
+
<Label htmlFor="phone" optional>Phone number</Label>
|
|
186
|
+
<Input id="phone" type="tel" />
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
// With description
|
|
190
|
+
<LabelWithDescription
|
|
191
|
+
htmlFor="password"
|
|
192
|
+
description="Must be at least 8 characters with one number"
|
|
193
|
+
>
|
|
194
|
+
Password
|
|
195
|
+
</LabelWithDescription>
|
|
196
|
+
<Input id="password" type="password" />
|
|
197
|
+
|
|
198
|
+
// With error
|
|
199
|
+
<div className="space-y-2">
|
|
200
|
+
<Label htmlFor="username" error>Username</Label>
|
|
201
|
+
<Input id="username" aria-invalid className="border-destructive" />
|
|
202
|
+
<p className="text-sm text-destructive">Username is already taken</p>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
// Form field wrapper
|
|
206
|
+
<FormField
|
|
207
|
+
label="Email"
|
|
208
|
+
description="We'll never share your email"
|
|
209
|
+
required
|
|
210
|
+
error={errors.email?.message}
|
|
211
|
+
>
|
|
212
|
+
<Input type="email" {...register('email')} />
|
|
213
|
+
</FormField>
|
|
214
|
+
|
|
215
|
+
// Inline with checkbox
|
|
216
|
+
<div className="flex items-center gap-2">
|
|
217
|
+
<Checkbox id="terms" />
|
|
218
|
+
<InlineLabel htmlFor="terms">
|
|
219
|
+
I agree to the <a href="/terms" className="underline">terms and conditions</a>
|
|
220
|
+
</InlineLabel>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
// Inline with switch
|
|
224
|
+
<div className="flex items-center justify-between">
|
|
225
|
+
<Label htmlFor="notifications" className="flex flex-col gap-1">
|
|
226
|
+
<span>Email notifications</span>
|
|
227
|
+
<span className="text-sm font-normal text-muted-foreground">
|
|
228
|
+
Receive emails about your account activity
|
|
229
|
+
</span>
|
|
230
|
+
</Label>
|
|
231
|
+
<Switch id="notifications" />
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
// Horizontal layout
|
|
235
|
+
<div className="grid grid-cols-4 items-center gap-4">
|
|
236
|
+
<Label htmlFor="name" className="text-right">Name</Label>
|
|
237
|
+
<Input id="name" className="col-span-3" />
|
|
238
|
+
</div>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Accessibility Notes
|
|
242
|
+
```typescript
|
|
243
|
+
// Always associate label with input
|
|
244
|
+
<Label htmlFor="input-id">Label text</Label>
|
|
245
|
+
<Input id="input-id" />
|
|
246
|
+
|
|
247
|
+
// For screen readers, hidden but accessible
|
|
248
|
+
<Label htmlFor="search" className="sr-only">Search</Label>
|
|
249
|
+
<Input id="search" placeholder="Search..." />
|
|
250
|
+
|
|
251
|
+
// Required fields should have aria-required
|
|
252
|
+
<Label htmlFor="email" required>Email</Label>
|
|
253
|
+
<Input id="email" aria-required="true" required />
|
|
254
|
+
|
|
255
|
+
// Error states need aria-invalid and aria-describedby
|
|
256
|
+
<Label htmlFor="password">Password</Label>
|
|
257
|
+
<Input id="password" aria-invalid="true" aria-describedby="password-error" />
|
|
258
|
+
<p id="password-error" role="alert">Password is too short</p>
|
|
259
|
+
```
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Modal/Dialog Component Recipe
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
- Overlay backdrop with `<div>` for dimming
|
|
5
|
+
- Container panel with `<div role="dialog">`
|
|
6
|
+
- Optional: Header, Body, Footer sections
|
|
7
|
+
- Close button in header or corner
|
|
8
|
+
- Focus trap and keyboard handling
|
|
9
|
+
|
|
10
|
+
## Tailwind Classes
|
|
11
|
+
|
|
12
|
+
### Overlay/Backdrop
|
|
13
|
+
```
|
|
14
|
+
fixed inset-0 z-50 bg-black/50 backdrop-blur-sm
|
|
15
|
+
data-[state=open]:animate-in data-[state=closed]:animate-out
|
|
16
|
+
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Panel/Content
|
|
20
|
+
```
|
|
21
|
+
fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2
|
|
22
|
+
w-full max-w-lg {tokens.radius} border border-border bg-background p-6 {tokens.shadow}
|
|
23
|
+
data-[state=open]:animate-in data-[state=closed]:animate-out
|
|
24
|
+
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
|
|
25
|
+
data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95
|
|
26
|
+
data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]
|
|
27
|
+
data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Header
|
|
31
|
+
```
|
|
32
|
+
flex flex-col space-y-1.5 text-center sm:text-left
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Title
|
|
36
|
+
```
|
|
37
|
+
{tokens.typography.heading} text-lg text-foreground
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Description
|
|
41
|
+
```
|
|
42
|
+
text-sm text-muted-foreground
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Footer
|
|
46
|
+
```
|
|
47
|
+
flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 mt-4
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Close Button
|
|
51
|
+
```
|
|
52
|
+
absolute right-4 top-4 {tokens.radius} opacity-70 ring-offset-background
|
|
53
|
+
transition-opacity hover:opacity-100
|
|
54
|
+
focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2
|
|
55
|
+
disabled:pointer-events-none
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Props Interface
|
|
59
|
+
```typescript
|
|
60
|
+
interface ModalProps {
|
|
61
|
+
open: boolean
|
|
62
|
+
onOpenChange: (open: boolean) => void
|
|
63
|
+
children: React.ReactNode
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ModalContentProps {
|
|
67
|
+
className?: string
|
|
68
|
+
children: React.ReactNode
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface ModalHeaderProps {
|
|
72
|
+
className?: string
|
|
73
|
+
children: React.ReactNode
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface ModalTitleProps {
|
|
77
|
+
className?: string
|
|
78
|
+
children: React.ReactNode
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface ModalDescriptionProps {
|
|
82
|
+
className?: string
|
|
83
|
+
children: React.ReactNode
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface ModalFooterProps {
|
|
87
|
+
className?: string
|
|
88
|
+
children: React.ReactNode
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Accessibility
|
|
93
|
+
- Use `role="dialog"` and `aria-modal="true"`
|
|
94
|
+
- Implement focus trap (focus stays within modal)
|
|
95
|
+
- Close on Escape key
|
|
96
|
+
- Close on backdrop click (optional)
|
|
97
|
+
- Return focus to trigger element on close
|
|
98
|
+
|
|
99
|
+
## Do
|
|
100
|
+
- Use Radix Dialog or similar for accessibility
|
|
101
|
+
- Add enter/exit animations
|
|
102
|
+
- Use `bg-background` for panel (not hardcoded white)
|
|
103
|
+
- Include close button with icon
|
|
104
|
+
- Trap focus within modal
|
|
105
|
+
|
|
106
|
+
## Don't
|
|
107
|
+
- Hardcode colors or shadows
|
|
108
|
+
- Forget backdrop blur/dim
|
|
109
|
+
- Skip keyboard handling (Escape to close)
|
|
110
|
+
- Allow body scroll when open
|
|
111
|
+
|
|
112
|
+
## Example
|
|
113
|
+
```tsx
|
|
114
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
|
115
|
+
import { X } from 'lucide-react'
|
|
116
|
+
import { cn } from '@/lib/utils'
|
|
117
|
+
|
|
118
|
+
const Modal = DialogPrimitive.Root
|
|
119
|
+
const ModalTrigger = DialogPrimitive.Trigger
|
|
120
|
+
|
|
121
|
+
const ModalContent = ({ className, children, ...props }) => (
|
|
122
|
+
<DialogPrimitive.Portal>
|
|
123
|
+
<DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm" />
|
|
124
|
+
<DialogPrimitive.Content
|
|
125
|
+
className={cn(
|
|
126
|
+
'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',
|
|
127
|
+
'w-full max-w-lg rounded-lg border border-border bg-background p-6 shadow-lg',
|
|
128
|
+
className
|
|
129
|
+
)}
|
|
130
|
+
{...props}
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100">
|
|
134
|
+
<X className="h-4 w-4" />
|
|
135
|
+
</DialogPrimitive.Close>
|
|
136
|
+
</DialogPrimitive.Content>
|
|
137
|
+
</DialogPrimitive.Portal>
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const ModalHeader = ({ className, ...props }) => (
|
|
141
|
+
<div className={cn('flex flex-col space-y-1.5', className)} {...props} />
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
const ModalTitle = ({ className, ...props }) => (
|
|
145
|
+
<DialogPrimitive.Title className={cn('font-semibold text-lg', className)} {...props} />
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const ModalDescription = ({ className, ...props }) => (
|
|
149
|
+
<DialogPrimitive.Description className={cn('text-sm text-muted-foreground', className)} {...props} />
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
const ModalFooter = ({ className, ...props }) => (
|
|
153
|
+
<div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 mt-4', className)} {...props} />
|
|
154
|
+
)
|
|
155
|
+
```
|