@create-ui/cli 0.1.0-beta.0 → 0.5.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.
Files changed (42) hide show
  1. package/README.md +194 -24
  2. package/dist/chunk-EWAP55CF.js +18 -0
  3. package/dist/chunk-EWAP55CF.js.map +1 -0
  4. package/dist/chunk-MK3CCMH4.js +3 -0
  5. package/dist/chunk-MK3CCMH4.js.map +1 -0
  6. package/dist/chunk-UVIUVCLG.js +64 -0
  7. package/dist/chunk-UVIUVCLG.js.map +1 -0
  8. package/dist/chunk-Y7WZRQWW.js +2 -0
  9. package/dist/chunk-Y7WZRQWW.js.map +1 -0
  10. package/dist/index.d.ts +43 -111
  11. package/dist/index.js +62 -61
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp/index.js +1 -1
  14. package/dist/registry/index.d.ts +3 -20
  15. package/dist/registry/index.js +1 -1
  16. package/dist/schema/index.d.ts +123 -432
  17. package/dist/schema/index.js +1 -1
  18. package/dist/skills/createui/SKILL.md +212 -0
  19. package/dist/skills/createui/agents/openai.yml +5 -0
  20. package/dist/skills/createui/assets/createui-small.png +0 -0
  21. package/dist/skills/createui/assets/createui.png +0 -0
  22. package/dist/skills/createui/cli.md +309 -0
  23. package/dist/skills/createui/contributing.md +213 -0
  24. package/dist/skills/createui/customization.md +284 -0
  25. package/dist/skills/createui/evals/evals.json +47 -0
  26. package/dist/skills/createui/mcp.md +151 -0
  27. package/dist/skills/createui/rules/composition.md +249 -0
  28. package/dist/skills/createui/rules/forms.md +301 -0
  29. package/dist/skills/createui/rules/icons.md +130 -0
  30. package/dist/skills/createui/rules/styling.md +253 -0
  31. package/dist/utils/index.d.ts +2 -5
  32. package/dist/utils/index.js +1 -1
  33. package/dist/utils/index.js.map +1 -1
  34. package/package.json +4 -3
  35. package/dist/chunk-7MKTQPYI.js +0 -72
  36. package/dist/chunk-7MKTQPYI.js.map +0 -1
  37. package/dist/chunk-BVZRYLRW.js +0 -32
  38. package/dist/chunk-BVZRYLRW.js.map +0 -1
  39. package/dist/chunk-JWZJQI2B.js +0 -3
  40. package/dist/chunk-JWZJQI2B.js.map +0 -1
  41. package/dist/chunk-TIYHWTW7.js +0 -2
  42. package/dist/chunk-TIYHWTW7.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
+ ```