@create-ui/cli 0.1.0-beta.1 → 0.5.1
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/LICENSE.md +1 -1
- package/README.md +194 -24
- package/dist/{chunk-UPXNWTZZ.js → chunk-EWAP55CF.js} +9 -9
- package/dist/chunk-EWAP55CF.js.map +1 -0
- package/dist/chunk-MK3CCMH4.js +3 -0
- package/dist/{chunk-RJOEUUDA.js.map → chunk-MK3CCMH4.js.map} +1 -1
- package/dist/{chunk-HRI6QVOR.js → chunk-UVIUVCLG.js} +6 -6
- package/dist/chunk-UVIUVCLG.js.map +1 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +61 -59
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/skills/createui/SKILL.md +212 -0
- package/dist/skills/createui/agents/openai.yml +5 -0
- package/dist/skills/createui/assets/createui-small.png +0 -0
- package/dist/skills/createui/assets/createui.png +0 -0
- package/dist/skills/createui/cli.md +309 -0
- package/dist/skills/createui/contributing.md +213 -0
- package/dist/skills/createui/customization.md +284 -0
- package/dist/skills/createui/evals/evals.json +47 -0
- package/dist/skills/createui/mcp.md +151 -0
- package/dist/skills/createui/rules/composition.md +249 -0
- package/dist/skills/createui/rules/forms.md +301 -0
- package/dist/skills/createui/rules/icons.md +130 -0
- package/dist/skills/createui/rules/styling.md +253 -0
- package/dist/utils/index.js +1 -1
- package/package.json +4 -3
- package/dist/chunk-HRI6QVOR.js.map +0 -1
- package/dist/chunk-RJOEUUDA.js +0 -3
- package/dist/chunk-UPXNWTZZ.js.map +0 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Component Composition
|
|
2
|
+
|
|
3
|
+
How Create UI components fit together. Compose primitives — never reroll a `<select>`, a custom callout, or a hand-styled loading button when a component already exists. Every component name below is in the Create UI registry; add any of them with `npx @create-ui/cli add <name>`.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- Items always inside their Group component
|
|
8
|
+
- Callouts use Alert
|
|
9
|
+
- Empty states use the Empty component
|
|
10
|
+
- Toast notifications use sonner
|
|
11
|
+
- Choosing between overlay components
|
|
12
|
+
- Dialog, Sheet, and Drawer always need a Title
|
|
13
|
+
- Card structure
|
|
14
|
+
- Button has a `loading` prop — never hand-build a spinner button
|
|
15
|
+
- TabsTrigger must be inside TabsList
|
|
16
|
+
- Avatar always needs AvatarFallback
|
|
17
|
+
- Use existing components instead of custom markup
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Items always inside their Group component
|
|
22
|
+
|
|
23
|
+
Never render menu/list items directly inside the content container — always wrap them in the matching `*Group`.
|
|
24
|
+
|
|
25
|
+
**Incorrect:**
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
<SelectContent>
|
|
29
|
+
<SelectItem value="apple">Apple</SelectItem>
|
|
30
|
+
<SelectItem value="banana">Banana</SelectItem>
|
|
31
|
+
</SelectContent>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Correct:**
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
<SelectContent>
|
|
38
|
+
<SelectGroup>
|
|
39
|
+
<SelectLabel>Fruit</SelectLabel>
|
|
40
|
+
<SelectItem value="apple">Apple</SelectItem>
|
|
41
|
+
<SelectItem value="banana">Banana</SelectItem>
|
|
42
|
+
</SelectGroup>
|
|
43
|
+
</SelectContent>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This applies to every group-based component:
|
|
47
|
+
|
|
48
|
+
| Item | Group |
|
|
49
|
+
|------|-------|
|
|
50
|
+
| `SelectItem`, `SelectLabel` | `SelectGroup` |
|
|
51
|
+
| `DropdownMenuItem`, `DropdownMenuLabel` | `DropdownMenuGroup` |
|
|
52
|
+
| `MenubarItem`, `MenubarLabel` | `MenubarGroup` |
|
|
53
|
+
| `ContextMenuItem`, `ContextMenuLabel` | `ContextMenuGroup` |
|
|
54
|
+
| `CommandItem` | `CommandGroup` |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Callouts use Alert
|
|
59
|
+
|
|
60
|
+
Use `Alert` for inline callouts. Set the tone with the `variant` prop (`default` or `danger`) and compose the parts — `AlertTitle`, `AlertDescription`, and `AlertAction`. Don't hand-roll a styled `<div>`.
|
|
61
|
+
|
|
62
|
+
**Incorrect:**
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
<div className="rounded-lg border bg-error-weak p-4">
|
|
66
|
+
<p className="font-medium">Payment failed</p>
|
|
67
|
+
<p>Update your card to continue.</p>
|
|
68
|
+
</div>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Correct:**
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<Alert variant="danger">
|
|
75
|
+
<AlertTitle>Payment failed</AlertTitle>
|
|
76
|
+
<AlertDescription>Update your card to continue.</AlertDescription>
|
|
77
|
+
<AlertAction>
|
|
78
|
+
<Button variant="danger" size="sm">Update card</Button>
|
|
79
|
+
</AlertAction>
|
|
80
|
+
</Alert>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Related callouts ship as their own components: `inline-alert` for compact, text-row callouts and `alert-banner` for full-width page-level banners. Reach for those instead of restyling `Alert` into a different shape.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Empty states use the Empty component
|
|
88
|
+
|
|
89
|
+
Don't hand-build empty/zero states. `Empty` composes a header (`EmptyHeader` → `EmptyMedia`, `EmptyTitle`, `EmptyDescription`) and a `EmptyContent` action area. Use `EmptyMedia variant="icon"` to get the boxed icon treatment.
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { RiFolderLine } from "lucide-react" // or your project's iconLibrary
|
|
93
|
+
|
|
94
|
+
<Empty>
|
|
95
|
+
<EmptyHeader>
|
|
96
|
+
<EmptyMedia variant="icon">
|
|
97
|
+
<RiFolderLine />
|
|
98
|
+
</EmptyMedia>
|
|
99
|
+
<EmptyTitle>No projects yet</EmptyTitle>
|
|
100
|
+
<EmptyDescription>Get started by creating a new project.</EmptyDescription>
|
|
101
|
+
</EmptyHeader>
|
|
102
|
+
<EmptyContent>
|
|
103
|
+
<Button>Create Project</Button>
|
|
104
|
+
</EmptyContent>
|
|
105
|
+
</Empty>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Toast notifications use sonner
|
|
111
|
+
|
|
112
|
+
Toasts come from `sonner`. Import the `toast` function directly — don't build a custom toast component.
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { toast } from "sonner"
|
|
116
|
+
|
|
117
|
+
toast.success("Changes saved.")
|
|
118
|
+
toast.error("Something went wrong.")
|
|
119
|
+
toast("File deleted.", {
|
|
120
|
+
action: { label: "Undo", onClick: () => undoDelete() },
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Choosing between overlay components
|
|
127
|
+
|
|
128
|
+
Pick the overlay that matches the interaction — they all exist in the registry.
|
|
129
|
+
|
|
130
|
+
| Use case | Component |
|
|
131
|
+
|----------|-----------|
|
|
132
|
+
| Focused task that requires input | `Dialog` |
|
|
133
|
+
| Destructive action confirmation | `AlertDialog` |
|
|
134
|
+
| Side panel with details or filters | `Sheet` |
|
|
135
|
+
| Mobile-first bottom panel | `Drawer` |
|
|
136
|
+
| Quick info on hover | `HoverCard` |
|
|
137
|
+
| Small contextual content on click | `Popover` |
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Dialog, Sheet, and Drawer always need a Title
|
|
142
|
+
|
|
143
|
+
`DialogTitle`, `SheetTitle`, and `DrawerTitle` are required for accessibility — screen readers announce them. Keep one even when the design hides it visually; add `className="sr-only"` in that case.
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
<DialogContent>
|
|
147
|
+
<DialogHeader>
|
|
148
|
+
<DialogTitle>Edit Profile</DialogTitle>
|
|
149
|
+
<DialogDescription>Update your profile.</DialogDescription>
|
|
150
|
+
</DialogHeader>
|
|
151
|
+
...
|
|
152
|
+
</DialogContent>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Card structure
|
|
158
|
+
|
|
159
|
+
Compose `Card` from its parts — don't dump everything into `CardContent`. `CardAction` slots an action into the header row.
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
<Card>
|
|
163
|
+
<CardHeader>
|
|
164
|
+
<CardTitle>Team Members</CardTitle>
|
|
165
|
+
<CardDescription>Manage your team.</CardDescription>
|
|
166
|
+
<CardAction>
|
|
167
|
+
<Button appearance="ghost" size="sm">Settings</Button>
|
|
168
|
+
</CardAction>
|
|
169
|
+
</CardHeader>
|
|
170
|
+
<CardContent>...</CardContent>
|
|
171
|
+
<CardFooter>
|
|
172
|
+
<Button>Invite</Button>
|
|
173
|
+
</CardFooter>
|
|
174
|
+
</Card>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Button has a `loading` prop — never hand-build a spinner button
|
|
180
|
+
|
|
181
|
+
`Button` ships a real `loading` prop. It renders the `Spinner` and disables interaction for you — do not compose a `Spinner` + `disabled` button by hand.
|
|
182
|
+
|
|
183
|
+
**Incorrect:**
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
<Button disabled>
|
|
187
|
+
<Spinner />
|
|
188
|
+
Saving…
|
|
189
|
+
</Button>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Correct:**
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
<Button loading>Saving…</Button>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
For icons, use the `leadingIcon` / `trailingIcon` props (or `iconOnly` for an icon-only button) — never wrap raw `<svg>` children or add sizing classes; the component sizes the icon per `size`.
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
import { RiSearchLine } from "lucide-react" // or your project's iconLibrary
|
|
202
|
+
|
|
203
|
+
<Button leadingIcon={<RiSearchLine />}>Search</Button>
|
|
204
|
+
<Button iconOnly aria-label="Search" leadingIcon={<RiSearchLine />} />
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Remember the Button API: `variant` is `primary | neutral-solid | neutral-light | danger | success | inverse-solid | inverse-light`, and the outlined/ghost looks come from `appearance` (`solid | outline | ghost | soft`). There is no outline or destructive `variant` value — use `appearance="outline"` for the outlined look and `variant="danger"` for destructive actions. See `rules/styling.md` for the full variant/appearance reference.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## TabsTrigger must be inside TabsList
|
|
212
|
+
|
|
213
|
+
Never render `TabsTrigger` directly inside `Tabs` — always wrap the triggers in `TabsList`.
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
<Tabs defaultValue="account">
|
|
217
|
+
<TabsList>
|
|
218
|
+
<TabsTrigger value="account">Account</TabsTrigger>
|
|
219
|
+
<TabsTrigger value="password">Password</TabsTrigger>
|
|
220
|
+
</TabsList>
|
|
221
|
+
<TabsContent value="account">...</TabsContent>
|
|
222
|
+
</Tabs>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Avatar always needs AvatarFallback
|
|
228
|
+
|
|
229
|
+
Always include `AvatarFallback` so something renders when the image is missing or fails to load.
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
<Avatar>
|
|
233
|
+
<AvatarImage src="/avatar.png" alt="User" />
|
|
234
|
+
<AvatarFallback>JD</AvatarFallback>
|
|
235
|
+
</Avatar>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Use existing components instead of custom markup
|
|
241
|
+
|
|
242
|
+
If a primitive already covers the job, use it — don't reach for raw elements or utility-class fakes.
|
|
243
|
+
|
|
244
|
+
| Instead of | Use |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `<hr>` or `<div className="border-t">` | `<Separator />` |
|
|
247
|
+
| `<div className="animate-pulse">` with styled divs | `<Skeleton className="h-4 w-3/4" />` |
|
|
248
|
+
| `<span className="rounded-full bg-green-100 …">` | `<Badge variant="success">Active</Badge>` |
|
|
249
|
+
| A status dot built from a styled `<span>` | `<StatusBadge variant="success">Online</StatusBadge>` |
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Forms & Inputs
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- Forms use FieldGroup + Field
|
|
6
|
+
- Choosing the right form control
|
|
7
|
+
- InputGroup requires InputGroupControl/InputGroupTextarea
|
|
8
|
+
- Buttons inside inputs use InputGroup + InputGroupButton
|
|
9
|
+
- Option sets (2–7 choices) use ToggleGroup
|
|
10
|
+
- Dropdowns use Select
|
|
11
|
+
- FieldSet + FieldLegend for grouping related fields
|
|
12
|
+
- Field validation and disabled states
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Forms use FieldGroup + Field
|
|
17
|
+
|
|
18
|
+
Always lay out a form with `FieldGroup` + `Field` — never a raw `div` with `space-y-*`. `Field` owns the size, invalid, disabled and loading state for everything inside it, and the nested control reads that state automatically.
|
|
19
|
+
|
|
20
|
+
**Incorrect:**
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<div className="space-y-4">
|
|
24
|
+
<div className="flex flex-col gap-2">
|
|
25
|
+
<label htmlFor="email">Email</label>
|
|
26
|
+
<input id="email" type="email" />
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Correct:**
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { Field, FieldGroup, FieldLabel, FieldDescription } from "@/components/ui/field"
|
|
35
|
+
import { Input } from "@/components/ui/input"
|
|
36
|
+
|
|
37
|
+
<FieldGroup>
|
|
38
|
+
<Field>
|
|
39
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
40
|
+
<Input id="email" type="email" />
|
|
41
|
+
<FieldDescription>We'll never share your address.</FieldDescription>
|
|
42
|
+
</Field>
|
|
43
|
+
<Field>
|
|
44
|
+
<FieldLabel htmlFor="password">Password</FieldLabel>
|
|
45
|
+
<Input id="password" type="password" />
|
|
46
|
+
</Field>
|
|
47
|
+
</FieldGroup>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`Field` accepts `size` (`"xs" | "sm" | "md"`, default `"sm"`) and `orientation` (`"vertical" | "horizontal" | "responsive"`, default `"vertical"`). Size cascades top-down: set it once on `Field` and the control, label, and description inherit it — never re-set the size on each child.
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// Horizontal layout for settings rows.
|
|
54
|
+
<Field orientation="horizontal">
|
|
55
|
+
<FieldLabel htmlFor="notifications">Email notifications</FieldLabel>
|
|
56
|
+
<Switch id="notifications" />
|
|
57
|
+
</Field>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use `FieldLabel className="sr-only"` for a visually hidden but accessible label.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Choosing the right form control
|
|
65
|
+
|
|
66
|
+
Every control below exists in the registry. Pick by intent:
|
|
67
|
+
|
|
68
|
+
| Need | Use |
|
|
69
|
+
|---|---|
|
|
70
|
+
| Single-line text | `Input` |
|
|
71
|
+
| Multi-line text | `Textarea` |
|
|
72
|
+
| Dropdown of predefined options | `Select` |
|
|
73
|
+
| Searchable dropdown | `Combobox` |
|
|
74
|
+
| Native HTML select (no JS) | `native-select` |
|
|
75
|
+
| Boolean in a settings row | `Switch` |
|
|
76
|
+
| Boolean in a form | `Checkbox` |
|
|
77
|
+
| One choice from a few options | `RadioGroup` |
|
|
78
|
+
| One choice across 2–7 visible options | `ToggleGroup` |
|
|
79
|
+
| Verification / OTP code | `InputOTP` |
|
|
80
|
+
| Numeric value with step controls | `input-stepper` |
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## InputGroup requires InputGroupControl/InputGroupTextarea
|
|
85
|
+
|
|
86
|
+
Never put a raw `Input` or `Textarea` inside an `InputGroup`. The group manages size and state through its own context, so use `InputGroupControl` (single-line) or `InputGroupTextarea` (multi-line).
|
|
87
|
+
|
|
88
|
+
**Incorrect:**
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
<InputGroup>
|
|
92
|
+
<Input placeholder="Search…" />
|
|
93
|
+
</InputGroup>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Correct:**
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { InputGroup, InputGroupControl } from "@/components/ui/input-group"
|
|
100
|
+
|
|
101
|
+
<InputGroup>
|
|
102
|
+
<InputGroupControl placeholder="Search…" />
|
|
103
|
+
</InputGroup>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For multi-line input inside a group, use the `multiline` prop with `InputGroupTextarea`:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { InputGroup, InputGroupTextarea } from "@/components/ui/input-group"
|
|
110
|
+
|
|
111
|
+
<InputGroup multiline>
|
|
112
|
+
<InputGroupTextarea placeholder="Leave a comment…" rows={4} />
|
|
113
|
+
</InputGroup>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Buttons inside inputs use InputGroup + InputGroupButton
|
|
119
|
+
|
|
120
|
+
Never absolutely-position a `Button` over an `Input`. Compose `InputGroup` + `InputGroupButton` (or `InputGroupAddon` for non-interactive affordances). `InputGroupButton` inherits the group's size and state, so you don't set `size` on it. Pass the icon through `leadingIcon`, or use `iconOnly` for an icon-only button — never a `data-icon` attribute.
|
|
121
|
+
|
|
122
|
+
**Incorrect:**
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<div className="relative">
|
|
126
|
+
<Input placeholder="Search…" className="pr-10" />
|
|
127
|
+
<Button className="absolute right-0 top-0" iconOnly aria-label="Search">
|
|
128
|
+
<RiSearchLine />
|
|
129
|
+
</Button>
|
|
130
|
+
</div>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Correct:**
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { InputGroup, InputGroupControl, InputGroupButton } from "@/components/ui/input-group"
|
|
137
|
+
import { RiSearchLine } from "lucide-react"
|
|
138
|
+
|
|
139
|
+
<InputGroup>
|
|
140
|
+
<InputGroupControl placeholder="Search…" />
|
|
141
|
+
<InputGroupButton iconOnly aria-label="Search" leadingIcon={<RiSearchLine />} />
|
|
142
|
+
</InputGroup>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
For a static prefix/suffix (currency, unit, helper text) use `InputGroupAddon` instead of a button:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
import { InputGroup, InputGroupControl, InputGroupAddon } from "@/components/ui/input-group"
|
|
149
|
+
|
|
150
|
+
<InputGroup>
|
|
151
|
+
<InputGroupAddon>$</InputGroupAddon>
|
|
152
|
+
<InputGroupControl placeholder="0.00" inputMode="decimal" />
|
|
153
|
+
<InputGroupAddon>USD</InputGroupAddon>
|
|
154
|
+
</InputGroup>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Option sets (2–7 choices) use ToggleGroup
|
|
160
|
+
|
|
161
|
+
For a small set of mutually-exclusive (or multi-select) choices, use `ToggleGroup` + `ToggleGroupItem`. Don't hand-roll a row of `Button`s with manual active state. `ToggleGroup` is built on Radix, so it takes `type="single"` or `type="multiple"` and the matching value props (`value` / `defaultValue` / `onValueChange`).
|
|
162
|
+
|
|
163
|
+
**Incorrect:**
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
const [selected, setSelected] = useState("daily")
|
|
167
|
+
|
|
168
|
+
<div className="flex gap-2">
|
|
169
|
+
{["daily", "weekly", "monthly"].map((option) => (
|
|
170
|
+
<Button
|
|
171
|
+
key={option}
|
|
172
|
+
appearance={selected === option ? "solid" : "outline"}
|
|
173
|
+
onClick={() => setSelected(option)}
|
|
174
|
+
>
|
|
175
|
+
{option}
|
|
176
|
+
</Button>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Correct:**
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
|
|
185
|
+
|
|
186
|
+
<ToggleGroup type="single" defaultValue="daily">
|
|
187
|
+
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
|
|
188
|
+
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
|
|
189
|
+
<ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
|
|
190
|
+
</ToggleGroup>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Use `type="multiple"` (value is a string array) when more than one option can be active at once. Wrap a labelled toggle group in a `Field` and connect them with `aria-labelledby`:
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
import { Field, FieldTitle } from "@/components/ui/field"
|
|
197
|
+
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
|
|
198
|
+
|
|
199
|
+
<Field orientation="horizontal">
|
|
200
|
+
<FieldTitle id="theme-label">Theme</FieldTitle>
|
|
201
|
+
<ToggleGroup type="single" defaultValue="system" aria-labelledby="theme-label">
|
|
202
|
+
<ToggleGroupItem value="light">Light</ToggleGroupItem>
|
|
203
|
+
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
|
|
204
|
+
<ToggleGroupItem value="system">System</ToggleGroupItem>
|
|
205
|
+
</ToggleGroup>
|
|
206
|
+
</Field>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Dropdowns use Select
|
|
212
|
+
|
|
213
|
+
`Select` is composed from Radix parts: `SelectTrigger`, `SelectValue`, `SelectContent`, and `SelectItem`. There is no `items` prop — render `SelectItem` children. Inside a `Field`, the trigger inherits the field's size, invalid, and disabled state automatically, so set them on `Field`, not on each part.
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
import { Field, FieldLabel } from "@/components/ui/field"
|
|
217
|
+
import {
|
|
218
|
+
Select,
|
|
219
|
+
SelectContent,
|
|
220
|
+
SelectItem,
|
|
221
|
+
SelectTrigger,
|
|
222
|
+
SelectValue,
|
|
223
|
+
} from "@/components/ui/select"
|
|
224
|
+
|
|
225
|
+
<Field>
|
|
226
|
+
<FieldLabel htmlFor="role">Role</FieldLabel>
|
|
227
|
+
<Select>
|
|
228
|
+
<SelectTrigger id="role">
|
|
229
|
+
<SelectValue placeholder="Select a role" />
|
|
230
|
+
</SelectTrigger>
|
|
231
|
+
<SelectContent>
|
|
232
|
+
<SelectItem value="admin">Admin</SelectItem>
|
|
233
|
+
<SelectItem value="editor">Editor</SelectItem>
|
|
234
|
+
<SelectItem value="viewer">Viewer</SelectItem>
|
|
235
|
+
</SelectContent>
|
|
236
|
+
</Select>
|
|
237
|
+
</Field>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`Select` accepts `size` (`"xs" | "sm" | "md"`), `invalid`, `disabled`, and `loading`. When it's not inside a `Field`, set these on the `Select` itself. For a dropdown with no JS, reach for `native-select` instead. For a searchable list, use `Combobox`.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## FieldSet + FieldLegend for grouping related fields
|
|
245
|
+
|
|
246
|
+
Use `FieldSet` + `FieldLegend` to group related checkboxes, radios, or switches — not a `div` with a heading. `FieldLegend` takes `variant="legend"` (default, section-sized) or `variant="label"` (form-control-sized).
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
import {
|
|
250
|
+
FieldSet,
|
|
251
|
+
FieldLegend,
|
|
252
|
+
FieldDescription,
|
|
253
|
+
FieldGroup,
|
|
254
|
+
Field,
|
|
255
|
+
FieldLabel,
|
|
256
|
+
} from "@/components/ui/field"
|
|
257
|
+
import { Checkbox } from "@/components/ui/checkbox"
|
|
258
|
+
|
|
259
|
+
<FieldSet>
|
|
260
|
+
<FieldLegend variant="label">Notifications</FieldLegend>
|
|
261
|
+
<FieldDescription>Choose what you want to hear about.</FieldDescription>
|
|
262
|
+
<FieldGroup>
|
|
263
|
+
<Field orientation="horizontal">
|
|
264
|
+
<Checkbox id="product" />
|
|
265
|
+
<FieldLabel htmlFor="product">Product updates</FieldLabel>
|
|
266
|
+
</Field>
|
|
267
|
+
<Field orientation="horizontal">
|
|
268
|
+
<Checkbox id="security" />
|
|
269
|
+
<FieldLabel htmlFor="security">Security alerts</FieldLabel>
|
|
270
|
+
</Field>
|
|
271
|
+
</FieldGroup>
|
|
272
|
+
</FieldSet>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
For a single-choice group, swap the checkboxes for a `RadioGroup` inside the same `FieldSet`.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Field validation and disabled states
|
|
280
|
+
|
|
281
|
+
`Field` exposes the state two ways: the boolean props `invalid` / `disabled` / `loading`, or the matching `data-invalid` / `data-disabled` / `data-loading` attributes. Either form drives the field styling (label, description, error). The control itself still needs `aria-invalid` / `disabled` so its own visuals and assistive tech reflect the state. Use `FieldError` to render the message.
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
import { Field, FieldLabel, FieldError } from "@/components/ui/field"
|
|
285
|
+
import { Input } from "@/components/ui/input"
|
|
286
|
+
|
|
287
|
+
// Invalid.
|
|
288
|
+
<Field invalid>
|
|
289
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
290
|
+
<Input id="email" type="email" aria-invalid />
|
|
291
|
+
<FieldError>Enter a valid email address.</FieldError>
|
|
292
|
+
</Field>
|
|
293
|
+
|
|
294
|
+
// Disabled.
|
|
295
|
+
<Field disabled>
|
|
296
|
+
<FieldLabel htmlFor="email">Email</FieldLabel>
|
|
297
|
+
<Input id="email" type="email" disabled />
|
|
298
|
+
</Field>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
This pattern works for every control: `Input`, `Textarea`, `Select`, `Checkbox`, `RadioGroup`, `Switch`, `Slider`, `native-select`, and `InputOTP`.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Icons
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- [Import from the configured icon library](#import-from-the-configured-icon-library)
|
|
6
|
+
- [Icons in Button use the leadingIcon / trailingIcon props](#icons-in-button-use-the-leadingicon--trailingicon-props)
|
|
7
|
+
- [Icon-only buttons](#icon-only-buttons)
|
|
8
|
+
- [No sizing classes on icons inside components](#no-sizing-classes-on-icons-inside-components)
|
|
9
|
+
- [Pass icons as component values, not string keys](#pass-icons-as-component-values-not-string-keys)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Import from the configured icon library
|
|
14
|
+
|
|
15
|
+
**Always import icons from the project's configured `iconLibrary`.** Read the `iconLibrary` field from `createui info` (or `components.json`): `lucide` → `lucide-react`, `tabler` → `@tabler/icons-react`, etc. The resolved default is `lucide`. Never hardcode an icon package.
|
|
16
|
+
|
|
17
|
+
Inside this monorepo, components import their icons from `@create-ui/assets/icons` (RemixIcon `Ri*`, e.g. `RiSearchLine`, `RiArrowRightLine`, `RiCheckLine`). In an end-user project, use whatever `iconLibrary` resolves to.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Icons in Button use the leadingIcon / trailingIcon props
|
|
22
|
+
|
|
23
|
+
`Button` accepts icons through the `leadingIcon` and `trailingIcon` props — not as children. The button sizes the icon automatically per `size` (via its CVA `[&_svg]:size-N`), so the icon never needs a sizing class. There is no `data-icon` attribute in Create UI.
|
|
24
|
+
|
|
25
|
+
**Incorrect:**
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
<Button>
|
|
29
|
+
<SearchIcon className="mr-2 size-4" />
|
|
30
|
+
Search
|
|
31
|
+
</Button>
|
|
32
|
+
|
|
33
|
+
<Button>
|
|
34
|
+
Next
|
|
35
|
+
<ArrowRightIcon className="ml-2 size-4" />
|
|
36
|
+
</Button>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Correct:**
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
<Button leadingIcon={<SearchIcon />}>Search</Button>
|
|
43
|
+
|
|
44
|
+
<Button trailingIcon={<ArrowRightIcon />}>Next</Button>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Icon-only buttons
|
|
50
|
+
|
|
51
|
+
For a button that shows only an icon, set `iconOnly`, pass the icon via `leadingIcon`, and always supply an `aria-label`. There is no separate icon-button component. For a close affordance, use the `close-button` component instead.
|
|
52
|
+
|
|
53
|
+
**Incorrect:**
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
<Button>
|
|
57
|
+
<SearchIcon className="size-4" />
|
|
58
|
+
</Button>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Correct:**
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
<Button iconOnly aria-label="Search" leadingIcon={<SearchIcon />} />
|
|
65
|
+
|
|
66
|
+
<Button iconOnly aria-label="More options" appearance="ghost" leadingIcon={<MoreIcon />} />
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## No sizing classes on icons inside components
|
|
72
|
+
|
|
73
|
+
Components handle icon sizing via CSS. Don't add `size-4`, `w-4 h-4`, or other sizing classes to icons rendered inside `Button`, `DropdownMenuItem`, `Alert`, `Sidebar*`, or other Create UI components — unless the user explicitly asks for a custom icon size.
|
|
74
|
+
|
|
75
|
+
**Incorrect:**
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<Button leadingIcon={<SearchIcon className="size-4" />}>Search</Button>
|
|
79
|
+
|
|
80
|
+
<DropdownMenuItem>
|
|
81
|
+
<SettingsIcon className="mr-2 size-4" />
|
|
82
|
+
Settings
|
|
83
|
+
</DropdownMenuItem>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Correct:**
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
<Button leadingIcon={<SearchIcon />}>Search</Button>
|
|
90
|
+
|
|
91
|
+
<DropdownMenuItem>
|
|
92
|
+
<SettingsIcon />
|
|
93
|
+
Settings
|
|
94
|
+
</DropdownMenuItem>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Pass icons as component values, not string keys
|
|
100
|
+
|
|
101
|
+
Use `icon={CheckIcon}`, not a string key into a lookup map.
|
|
102
|
+
|
|
103
|
+
**Incorrect:**
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
const iconMap = {
|
|
107
|
+
check: CheckIcon,
|
|
108
|
+
alert: AlertIcon,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function StatusBadge({ icon }: { icon: string }) {
|
|
112
|
+
const Icon = iconMap[icon]
|
|
113
|
+
return <Icon />
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
<StatusBadge icon="check" />
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Correct:**
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
// Import from the project's configured iconLibrary (e.g. lucide-react, @tabler/icons-react).
|
|
123
|
+
import { CheckIcon } from "lucide-react"
|
|
124
|
+
|
|
125
|
+
function StatusBadge({ icon: Icon }: { icon: React.ComponentType }) {
|
|
126
|
+
return <Icon />
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
<StatusBadge icon={CheckIcon} />
|
|
130
|
+
```
|