@autono/create-open-pages 0.1.0 → 0.4.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 (94) hide show
  1. package/README.md +6 -3
  2. package/dist/cli.js +1 -1
  3. package/package.json +1 -1
  4. package/template/.agents/skills/apply-comments/SKILL.md +4 -4
  5. package/template/.agents/skills/create-page/SKILL.md +26 -8
  6. package/template/.agents/skills/create-theme/SKILL.md +137 -150
  7. package/template/.agents/skills/current-page/SKILL.md +1 -1
  8. package/template/.agents/skills/page-authoring/SKILL.md +97 -42
  9. package/template/.agents/skills/page-authoring/references/interactivity.md +71 -23
  10. package/template/.agents/skills/page-authoring/references/layout-and-responsive.md +38 -17
  11. package/template/.agents/skills/page-authoring/references/typography-and-color.md +50 -32
  12. package/template/.agents/skills/shadcn/SKILL.md +277 -0
  13. package/template/.agents/skills/shadcn/assets/shadcn-small.png +0 -0
  14. package/template/.agents/skills/shadcn/assets/shadcn.png +0 -0
  15. package/template/.agents/skills/shadcn/cli.md +290 -0
  16. package/template/.agents/skills/shadcn/customization.md +209 -0
  17. package/template/.agents/skills/shadcn/mcp.md +105 -0
  18. package/template/.agents/skills/shadcn/registry.md +277 -0
  19. package/template/.agents/skills/shadcn/rules/base-vs-radix.md +306 -0
  20. package/template/.agents/skills/shadcn/rules/chat.md +224 -0
  21. package/template/.agents/skills/shadcn/rules/composition.md +213 -0
  22. package/template/.agents/skills/shadcn/rules/forms.md +192 -0
  23. package/template/.agents/skills/shadcn/rules/icons.md +101 -0
  24. package/template/.agents/skills/shadcn/rules/styling.md +185 -0
  25. package/template/AGENTS.md +7 -4
  26. package/template/README.md +21 -4
  27. package/template/components.json +9 -0
  28. package/template/hooks/use-mobile.ts +19 -0
  29. package/template/lib/utils.ts +6 -0
  30. package/template/package.json +27 -5
  31. package/template/pages/getting-started/index.tsx +51 -45
  32. package/template/styles/globals.css +118 -0
  33. package/template/tsconfig.json +13 -2
  34. package/template/ui/accordion.tsx +64 -0
  35. package/template/ui/alert-dialog.tsx +196 -0
  36. package/template/ui/alert.tsx +66 -0
  37. package/template/ui/aspect-ratio.tsx +11 -0
  38. package/template/ui/attachment.tsx +204 -0
  39. package/template/ui/avatar.tsx +107 -0
  40. package/template/ui/badge.tsx +48 -0
  41. package/template/ui/breadcrumb.tsx +109 -0
  42. package/template/ui/bubble.tsx +125 -0
  43. package/template/ui/button-group.tsx +83 -0
  44. package/template/ui/button.tsx +64 -0
  45. package/template/ui/calendar.tsx +218 -0
  46. package/template/ui/card.tsx +92 -0
  47. package/template/ui/carousel.tsx +241 -0
  48. package/template/ui/chart.tsx +374 -0
  49. package/template/ui/checkbox.tsx +32 -0
  50. package/template/ui/collapsible.tsx +31 -0
  51. package/template/ui/combobox.tsx +308 -0
  52. package/template/ui/command.tsx +182 -0
  53. package/template/ui/context-menu.tsx +252 -0
  54. package/template/ui/dialog.tsx +156 -0
  55. package/template/ui/direction.tsx +22 -0
  56. package/template/ui/drawer.tsx +133 -0
  57. package/template/ui/dropdown-menu.tsx +257 -0
  58. package/template/ui/empty.tsx +104 -0
  59. package/template/ui/field.tsx +246 -0
  60. package/template/ui/form.tsx +167 -0
  61. package/template/ui/hover-card.tsx +42 -0
  62. package/template/ui/input-group.tsx +170 -0
  63. package/template/ui/input-otp.tsx +77 -0
  64. package/template/ui/input.tsx +21 -0
  65. package/template/ui/item.tsx +193 -0
  66. package/template/ui/kbd.tsx +28 -0
  67. package/template/ui/label.tsx +22 -0
  68. package/template/ui/marker.tsx +69 -0
  69. package/template/ui/menubar.tsx +276 -0
  70. package/template/ui/message-scroller.tsx +128 -0
  71. package/template/ui/message.tsx +92 -0
  72. package/template/ui/native-select.tsx +62 -0
  73. package/template/ui/navigation-menu.tsx +168 -0
  74. package/template/ui/pagination.tsx +127 -0
  75. package/template/ui/popover.tsx +87 -0
  76. package/template/ui/progress.tsx +31 -0
  77. package/template/ui/radio-group.tsx +43 -0
  78. package/template/ui/resizable.tsx +53 -0
  79. package/template/ui/scroll-area.tsx +56 -0
  80. package/template/ui/select.tsx +190 -0
  81. package/template/ui/separator.tsx +26 -0
  82. package/template/ui/sheet.tsx +143 -0
  83. package/template/ui/sidebar.tsx +726 -0
  84. package/template/ui/skeleton.tsx +13 -0
  85. package/template/ui/slider.tsx +61 -0
  86. package/template/ui/sonner.tsx +40 -0
  87. package/template/ui/spinner.tsx +16 -0
  88. package/template/ui/switch.tsx +33 -0
  89. package/template/ui/table.tsx +116 -0
  90. package/template/ui/tabs.tsx +89 -0
  91. package/template/ui/textarea.tsx +18 -0
  92. package/template/ui/toggle-group.tsx +81 -0
  93. package/template/ui/toggle.tsx +47 -0
  94. package/template/ui/tooltip.tsx +55 -0
@@ -0,0 +1,224 @@
1
+ # Chat & Messaging
2
+
3
+ Components for conversation and chat UI. Compose these instead of hand-rolling
4
+ bubbles, scroll containers, dividers, or attachment cards.
5
+
6
+ Install: `npx shadcn@latest add message-scroller message bubble attachment marker`
7
+
8
+ The same component names and props ship for both `base` and `radix`; only
9
+ composition differs (`render` vs `asChild`). See [base-vs-radix.md](./base-vs-radix.md).
10
+
11
+ ## Contents
12
+
13
+ - Scrollable threads use MessageScroller
14
+ - Message rows use Message
15
+ - Message surfaces use Bubble
16
+ - Attachments use Attachment
17
+ - System notes and dividers use Marker
18
+ - Streaming, anchoring, and jump-to-latest are built in
19
+ - Escape hatch: the scroller hooks
20
+
21
+ ---
22
+
23
+ ## Scrollable threads use MessageScroller
24
+
25
+ A conversation that scrolls, follows new messages, restores position, or jumps
26
+ to a message uses `MessageScroller`. Don't build a raw overflow container with
27
+ manual scroll wiring, and don't reach for `ScrollArea`.
28
+
29
+ The parts nest in a fixed order. Every direct child of the content is wrapped in
30
+ a `MessageScrollerItem` so the scroller can measure, anchor, preserve position,
31
+ track visibility, and jump to it. `MessageScrollerButton` sits inside
32
+ `MessageScroller`, after the viewport.
33
+
34
+ **Incorrect:**
35
+
36
+ ```tsx
37
+ // Hand-rolled scroll container with manual stick-to-bottom logic.
38
+ <div ref={scrollRef} onScroll={handleScroll} className="flex-1 overflow-y-auto">
39
+ <div className="flex flex-col gap-6 p-4">
40
+ {messages.map((m) => (
41
+ <ChatMessage key={m.id} message={m} />
42
+ ))}
43
+ </div>
44
+ </div>
45
+ ```
46
+
47
+ **Correct:**
48
+
49
+ ```tsx
50
+ <MessageScrollerProvider autoScroll>
51
+ <MessageScroller>
52
+ <MessageScrollerViewport>
53
+ <MessageScrollerContent>
54
+ {messages.map((message) => (
55
+ <MessageScrollerItem
56
+ key={message.id}
57
+ messageId={message.id}
58
+ scrollAnchor={message.role === "user"}
59
+ >
60
+ <Message align={message.role === "user" ? "end" : "start"}>
61
+ {/* ...message content... */}
62
+ </Message>
63
+ </MessageScrollerItem>
64
+ ))}
65
+ </MessageScrollerContent>
66
+ </MessageScrollerViewport>
67
+ <MessageScrollerButton />
68
+ </MessageScroller>
69
+ </MessageScrollerProvider>
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Message rows use Message
75
+
76
+ `Message` lays out a single row: avatar, header, content, footer, with
77
+ alignment. Group consecutive rows from one sender with `MessageGroup`. Don't
78
+ rebuild the row from flex divs.
79
+
80
+ `align="end"` is the current user's side; `align="start"` is everyone else.
81
+
82
+ ```tsx
83
+ <Message align="start">
84
+ <MessageAvatar>
85
+ <Avatar>
86
+ <AvatarImage src={sender.avatar} alt={sender.name} />
87
+ <AvatarFallback>{initials}</AvatarFallback>
88
+ </Avatar>
89
+ </MessageAvatar>
90
+ <MessageContent>
91
+ <MessageHeader>{sender.name}</MessageHeader>
92
+ <Bubble>
93
+ <BubbleContent>{text}</BubbleContent>
94
+ </Bubble>
95
+ <MessageFooter>{time}</MessageFooter>
96
+ </MessageContent>
97
+ </Message>
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Message surfaces use Bubble
103
+
104
+ The colored message surface is `Bubble` + `BubbleContent`, never a styled `div`
105
+ with `bg-muted` / `bg-primary` and hand-managed corners.
106
+
107
+ - `variant`: `default`, `secondary`, `muted`, `tinted`, `outline`, `ghost`, `destructive`.
108
+ - `align`: `start` or `end` (matches the `Message` side).
109
+
110
+ `BubbleReactions` renders the reaction cluster. `side` (`top` | `bottom`) and
111
+ `align` (`start` | `end`) position it against the bubble. Don't lay reactions out
112
+ with absolutely-positioned `Badge`s.
113
+
114
+ **Incorrect:**
115
+
116
+ ```tsx
117
+ <div className="w-fit rounded-2xl bg-primary px-3 py-2 text-primary-foreground">
118
+ {text}
119
+ </div>
120
+ ```
121
+
122
+ **Correct:**
123
+
124
+ ```tsx
125
+ <Bubble variant="default" align="end">
126
+ <BubbleContent>{text}</BubbleContent>
127
+ <BubbleReactions side="bottom" align="end">
128
+ <Badge variant="secondary">👍 2</Badge>
129
+ </BubbleReactions>
130
+ </Bubble>
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Attachments use Attachment
136
+
137
+ File and image attachments use `Attachment`, not `Item` or a custom card. It
138
+ carries upload state, so wire `state` to the real status rather than rendering a
139
+ separate spinner.
140
+
141
+ - `state`: `idle`, `uploading`, `processing`, `error`, `done`. `uploading` and
142
+ `processing` apply the `shimmer` animation to the title automatically.
143
+ - `size`: `default`, `sm`, `xs`. `orientation`: `horizontal`, `vertical`.
144
+ - Use `AttachmentGroup` to lay out several attachments in a scrolling row.
145
+
146
+ ```tsx
147
+ <Attachment state="done">
148
+ <AttachmentMedia variant="icon">
149
+ <FileTextIcon />
150
+ </AttachmentMedia>
151
+ <AttachmentContent>
152
+ <AttachmentTitle>homepage-feedback.pdf</AttachmentTitle>
153
+ <AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
154
+ </AttachmentContent>
155
+ <AttachmentActions>
156
+ <AttachmentAction>
157
+ <DownloadIcon />
158
+ </AttachmentAction>
159
+ </AttachmentActions>
160
+ </Attachment>
161
+ ```
162
+
163
+ For an image, use `<AttachmentMedia variant="image">` with an `img` child.
164
+
165
+ ---
166
+
167
+ ## System notes and dividers use Marker
168
+
169
+ Status lines ("Sarah joined the conversation"), date dividers ("Today"), and
170
+ labeled separators are `Marker`, not a `Separator` plus a centered span.
171
+
172
+ - `variant`: `default` (plain row), `separator` (centered label with rules on
173
+ each side), `border` (bottom-bordered row).
174
+ - `MarkerIcon` holds a leading icon; `MarkerContent` holds the label.
175
+
176
+ **Incorrect:**
177
+
178
+ ```tsx
179
+ <div className="flex items-center gap-3 py-2">
180
+ <Separator className="flex-1" />
181
+ <span className="text-xs text-muted-foreground">Today</span>
182
+ <Separator className="flex-1" />
183
+ </div>
184
+ ```
185
+
186
+ **Correct:**
187
+
188
+ ```tsx
189
+ <Marker variant="separator">
190
+ <MarkerContent>Today</MarkerContent>
191
+ </Marker>
192
+ ```
193
+
194
+ ---
195
+
196
+ ## Streaming, anchoring, and jump-to-latest are built in
197
+
198
+ `MessageScroller` handles the behavior that chat UIs usually reinvent. Don't
199
+ write a `useStickToBottom` hook, a `ResizeObserver`, or manual `scrollTop` math.
200
+
201
+ - **Follow the live edge while streaming.** `MessageScrollerProvider` with
202
+ `autoScroll` keeps the view pinned to new content and yields the moment the
203
+ user scrolls up. Streaming token updates that grow the last message are
204
+ followed automatically.
205
+ - **Anchor a turn.** `scrollAnchor` on a `MessageScrollerItem` marks the row to
206
+ hold in view (typically the user's message that started the turn).
207
+ - **Jump to latest.** `MessageScrollerButton` appears when the user scrolls away
208
+ and scrolls back on click. `direction="end"` (default) or `direction="start"`.
209
+ It is a self-managing control, so don't gate it behind your own scroll-position
210
+ state.
211
+
212
+ For a "thinking…" indicator while the model generates, apply the `shimmer`
213
+ utility to text. Don't author a custom keyframe animation. See
214
+ [styling.md](./styling.md).
215
+
216
+ ---
217
+
218
+ ## Escape hatch: the scroller hooks
219
+
220
+ For behavior the parts don't expose, read state from the hooks rather than
221
+ re-implementing the scroller: `useMessageScroller`,
222
+ `useMessageScrollerVisibility`, and `useMessageScrollerScrollable`. They come
223
+ from the auto-installed `@shadcn/react` dependency, so there's nothing extra to
224
+ install. Reach for them only when composition can't express what you need.
@@ -0,0 +1,213 @@
1
+ # Component Composition
2
+
3
+ ## Contents
4
+
5
+ - Items always inside their Group component
6
+ - Callouts use Alert
7
+ - Empty states use Empty component
8
+ - Toast notifications follow the project base
9
+ - Choosing between overlay components
10
+ - Dialog, Sheet, and Drawer always need a Title
11
+ - Card structure
12
+ - Button has no isPending or isLoading prop
13
+ - TabsTrigger must be inside TabsList
14
+ - Avatar always needs AvatarFallback
15
+ - Use Separator instead of raw hr or border divs
16
+ - Use Skeleton for loading placeholders
17
+ - Use Badge instead of custom styled spans
18
+
19
+ ---
20
+
21
+ ## Items always inside their Group component
22
+
23
+ Never render items directly inside the content container.
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
+ <SelectItem value="apple">Apple</SelectItem>
40
+ <SelectItem value="banana">Banana</SelectItem>
41
+ </SelectGroup>
42
+ </SelectContent>
43
+ ```
44
+
45
+ This applies to all group-based components:
46
+
47
+ | Item | Group |
48
+ |------|-------|
49
+ | `SelectItem`, `SelectLabel` | `SelectGroup` |
50
+ | `DropdownMenuItem`, `DropdownMenuLabel`, `DropdownMenuSub` | `DropdownMenuGroup` |
51
+ | `MenubarItem` | `MenubarGroup` |
52
+ | `ContextMenuItem` | `ContextMenuGroup` |
53
+ | `CommandItem` | `CommandGroup` |
54
+ | `MessageScrollerItem` | `MessageScrollerContent` |
55
+ | `Message` (consecutive, same sender) | `MessageGroup` |
56
+ | `Bubble` (stacked) | `BubbleGroup` |
57
+ | `Attachment` (in a row) | `AttachmentGroup` |
58
+
59
+ Chat components nest in a fixed order (`MessageScrollerProvider` → `MessageScroller` → `MessageScrollerViewport` → `MessageScrollerContent` → `MessageScrollerItem`). See [chat.md](./chat.md).
60
+
61
+ ---
62
+
63
+ ## Callouts use Alert
64
+
65
+ ```tsx
66
+ <Alert>
67
+ <AlertTitle>Warning</AlertTitle>
68
+ <AlertDescription>Something needs attention.</AlertDescription>
69
+ </Alert>
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Empty states use Empty component
75
+
76
+ ```tsx
77
+ <Empty>
78
+ <EmptyHeader>
79
+ <EmptyMedia variant="icon"><FolderIcon /></EmptyMedia>
80
+ <EmptyTitle>No projects yet</EmptyTitle>
81
+ <EmptyDescription>Get started by creating a new project.</EmptyDescription>
82
+ </EmptyHeader>
83
+ <EmptyContent>
84
+ <Button>Create Project</Button>
85
+ </EmptyContent>
86
+ </Empty>
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Toast notifications follow the project base
92
+
93
+ For Base UI projects, use the `toast` component:
94
+
95
+ ```tsx
96
+ import { toast } from "@/components/ui/toast"
97
+
98
+ toast.add({
99
+ title: "Changes saved.",
100
+ })
101
+ ```
102
+
103
+ For Radix and React Aria projects, use Sonner:
104
+
105
+ ```tsx
106
+ import { toast } from "sonner"
107
+
108
+ toast.success("Changes saved.")
109
+ toast.error("Something went wrong.")
110
+ toast("File deleted.", {
111
+ action: { label: "Undo", onClick: () => undoDelete() },
112
+ })
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Choosing between overlay components
118
+
119
+ | Use case | Component |
120
+ |----------|-----------|
121
+ | Focused task that requires input | `Dialog` |
122
+ | Destructive action confirmation | `AlertDialog` |
123
+ | Side panel with details or filters | `Sheet` |
124
+ | Mobile-first bottom panel | `Drawer` |
125
+ | Quick info on hover | `HoverCard` |
126
+ | Small contextual content on click | `Popover` |
127
+
128
+ ---
129
+
130
+ ## Dialog, Sheet, and Drawer always need a Title
131
+
132
+ `DialogTitle`, `SheetTitle`, `DrawerTitle` are required for accessibility. Use `className="sr-only"` if visually hidden.
133
+
134
+ ```tsx
135
+ <DialogContent>
136
+ <DialogHeader>
137
+ <DialogTitle>Edit Profile</DialogTitle>
138
+ <DialogDescription>Update your profile.</DialogDescription>
139
+ </DialogHeader>
140
+ ...
141
+ </DialogContent>
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Card structure
147
+
148
+ Use full composition — don't dump everything into `CardContent`:
149
+
150
+ ```tsx
151
+ <Card>
152
+ <CardHeader>
153
+ <CardTitle>Team Members</CardTitle>
154
+ <CardDescription>Manage your team.</CardDescription>
155
+ </CardHeader>
156
+ <CardContent>...</CardContent>
157
+ <CardFooter>
158
+ <Button>Invite</Button>
159
+ </CardFooter>
160
+ </Card>
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Button has no isPending or isLoading prop
166
+
167
+ Compose with `Spinner` + `data-icon` + `disabled`:
168
+
169
+ ```tsx
170
+ <Button disabled>
171
+ <Spinner data-icon="inline-start" />
172
+ Saving...
173
+ </Button>
174
+ ```
175
+
176
+ ---
177
+
178
+ ## TabsTrigger must be inside TabsList
179
+
180
+ Never render `TabsTrigger` directly inside `Tabs` — always wrap in `TabsList`:
181
+
182
+ ```tsx
183
+ <Tabs defaultValue="account">
184
+ <TabsList>
185
+ <TabsTrigger value="account">Account</TabsTrigger>
186
+ <TabsTrigger value="password">Password</TabsTrigger>
187
+ </TabsList>
188
+ <TabsContent value="account">...</TabsContent>
189
+ </Tabs>
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Avatar always needs AvatarFallback
195
+
196
+ Always include `AvatarFallback` for when the image fails to load:
197
+
198
+ ```tsx
199
+ <Avatar>
200
+ <AvatarImage src="/avatar.png" alt="User" />
201
+ <AvatarFallback>JD</AvatarFallback>
202
+ </Avatar>
203
+ ```
204
+
205
+ ---
206
+
207
+ ## Use existing components instead of custom markup
208
+
209
+ | Instead of | Use |
210
+ |---|---|
211
+ | `<hr>` or `<div className="border-t">` | `<Separator />` |
212
+ | `<div className="animate-pulse">` with styled divs | `<Skeleton className="h-4 w-3/4" />` |
213
+ | `<span className="rounded-full bg-green-100 ...">` | `<Badge variant="secondary">` |
@@ -0,0 +1,192 @@
1
+ # Forms & Inputs
2
+
3
+ ## Contents
4
+
5
+ - Forms use FieldGroup + Field
6
+ - InputGroup requires InputGroupInput/InputGroupTextarea
7
+ - Buttons inside inputs use InputGroup + InputGroupAddon
8
+ - Option sets (2–7 choices) use ToggleGroup
9
+ - FieldSet + FieldLegend for grouping related fields
10
+ - Field validation and disabled states
11
+
12
+ ---
13
+
14
+ ## Forms use FieldGroup + Field
15
+
16
+ Always use `FieldGroup` + `Field` — never raw `div` with `space-y-*`:
17
+
18
+ ```tsx
19
+ <FieldGroup>
20
+ <Field>
21
+ <FieldLabel htmlFor="email">Email</FieldLabel>
22
+ <Input id="email" type="email" />
23
+ </Field>
24
+ <Field>
25
+ <FieldLabel htmlFor="password">Password</FieldLabel>
26
+ <Input id="password" type="password" />
27
+ </Field>
28
+ </FieldGroup>
29
+ ```
30
+
31
+ Use `Field orientation="horizontal"` for settings pages. Use `FieldLabel className="sr-only"` for visually hidden labels.
32
+
33
+ **Choosing form controls:**
34
+
35
+ - Simple text input → `Input`
36
+ - Dropdown with predefined options → `Select`
37
+ - Searchable dropdown → `Combobox`
38
+ - Native HTML select (no JS) → `native-select`
39
+ - Boolean toggle → `Switch` (for settings) or `Checkbox` (for forms)
40
+ - Single choice from few options → `RadioGroup`
41
+ - Toggle between 2–5 options → `ToggleGroup` + `ToggleGroupItem`
42
+ - OTP/verification code → `InputOTP`
43
+ - Multi-line text → `Textarea`
44
+
45
+ ---
46
+
47
+ ## InputGroup requires InputGroupInput/InputGroupTextarea
48
+
49
+ Never use raw `Input` or `Textarea` inside an `InputGroup`.
50
+
51
+ **Incorrect:**
52
+
53
+ ```tsx
54
+ <InputGroup>
55
+ <Input placeholder="Search..." />
56
+ </InputGroup>
57
+ ```
58
+
59
+ **Correct:**
60
+
61
+ ```tsx
62
+ import { InputGroup, InputGroupInput } from "@/components/ui/input-group"
63
+
64
+ <InputGroup>
65
+ <InputGroupInput placeholder="Search..." />
66
+ </InputGroup>
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Buttons inside inputs use InputGroup + InputGroupAddon
72
+
73
+ Never place a `Button` directly inside or adjacent to an `Input` with custom positioning.
74
+
75
+ **Incorrect:**
76
+
77
+ ```tsx
78
+ <div className="relative">
79
+ <Input placeholder="Search..." className="pr-10" />
80
+ <Button className="absolute right-0 top-0" size="icon">
81
+ <SearchIcon />
82
+ </Button>
83
+ </div>
84
+ ```
85
+
86
+ **Correct:**
87
+
88
+ ```tsx
89
+ import { InputGroup, InputGroupInput, InputGroupAddon } from "@/components/ui/input-group"
90
+
91
+ <InputGroup>
92
+ <InputGroupInput placeholder="Search..." />
93
+ <InputGroupAddon>
94
+ <Button size="icon">
95
+ <SearchIcon data-icon="inline-start" />
96
+ </Button>
97
+ </InputGroupAddon>
98
+ </InputGroup>
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Option sets (2–7 choices) use ToggleGroup
104
+
105
+ Don't manually loop `Button` components with active state.
106
+
107
+ **Incorrect:**
108
+
109
+ ```tsx
110
+ const [selected, setSelected] = useState("daily")
111
+
112
+ <div className="flex gap-2">
113
+ {["daily", "weekly", "monthly"].map((option) => (
114
+ <Button
115
+ key={option}
116
+ variant={selected === option ? "default" : "outline"}
117
+ onClick={() => setSelected(option)}
118
+ >
119
+ {option}
120
+ </Button>
121
+ ))}
122
+ </div>
123
+ ```
124
+
125
+ **Correct:**
126
+
127
+ ```tsx
128
+ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
129
+
130
+ <ToggleGroup spacing={2}>
131
+ <ToggleGroupItem value="daily">Daily</ToggleGroupItem>
132
+ <ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
133
+ <ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
134
+ </ToggleGroup>
135
+ ```
136
+
137
+ Combine with `Field` for labelled toggle groups:
138
+
139
+ ```tsx
140
+ <Field orientation="horizontal">
141
+ <FieldTitle id="theme-label">Theme</FieldTitle>
142
+ <ToggleGroup aria-labelledby="theme-label" spacing={2}>
143
+ <ToggleGroupItem value="light">Light</ToggleGroupItem>
144
+ <ToggleGroupItem value="dark">Dark</ToggleGroupItem>
145
+ <ToggleGroupItem value="system">System</ToggleGroupItem>
146
+ </ToggleGroup>
147
+ </Field>
148
+ ```
149
+
150
+ > **Note:** `defaultValue` and `type`/`multiple` props differ between base and radix. See [base-vs-radix.md](./base-vs-radix.md#togglegroup).
151
+
152
+ ---
153
+
154
+ ## FieldSet + FieldLegend for grouping related fields
155
+
156
+ Use `FieldSet` + `FieldLegend` for related checkboxes, radios, or switches — not `div` with a heading:
157
+
158
+ ```tsx
159
+ <FieldSet>
160
+ <FieldLegend variant="label">Preferences</FieldLegend>
161
+ <FieldDescription>Select all that apply.</FieldDescription>
162
+ <FieldGroup className="gap-3">
163
+ <Field orientation="horizontal">
164
+ <Checkbox id="dark" />
165
+ <FieldLabel htmlFor="dark" className="font-normal">Dark mode</FieldLabel>
166
+ </Field>
167
+ </FieldGroup>
168
+ </FieldSet>
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Field validation and disabled states
174
+
175
+ Both attributes are needed — `data-invalid`/`data-disabled` styles the field (label, description), while `aria-invalid`/`disabled` styles the control.
176
+
177
+ ```tsx
178
+ // Invalid.
179
+ <Field data-invalid>
180
+ <FieldLabel htmlFor="email">Email</FieldLabel>
181
+ <Input id="email" aria-invalid />
182
+ <FieldDescription>Invalid email address.</FieldDescription>
183
+ </Field>
184
+
185
+ // Disabled.
186
+ <Field data-disabled>
187
+ <FieldLabel htmlFor="email">Email</FieldLabel>
188
+ <Input id="email" disabled />
189
+ </Field>
190
+ ```
191
+
192
+ Works for all controls: `Input`, `Textarea`, `Select`, `Checkbox`, `RadioGroupItem`, `Switch`, `Slider`, `NativeSelect`, `InputOTP`.