@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.
- package/README.md +6 -3
- package/dist/cli.js +1 -1
- package/package.json +1 -1
- package/template/.agents/skills/apply-comments/SKILL.md +4 -4
- package/template/.agents/skills/create-page/SKILL.md +26 -8
- package/template/.agents/skills/create-theme/SKILL.md +137 -150
- package/template/.agents/skills/current-page/SKILL.md +1 -1
- package/template/.agents/skills/page-authoring/SKILL.md +97 -42
- package/template/.agents/skills/page-authoring/references/interactivity.md +71 -23
- package/template/.agents/skills/page-authoring/references/layout-and-responsive.md +38 -17
- package/template/.agents/skills/page-authoring/references/typography-and-color.md +50 -32
- package/template/.agents/skills/shadcn/SKILL.md +277 -0
- package/template/.agents/skills/shadcn/assets/shadcn-small.png +0 -0
- package/template/.agents/skills/shadcn/assets/shadcn.png +0 -0
- package/template/.agents/skills/shadcn/cli.md +290 -0
- package/template/.agents/skills/shadcn/customization.md +209 -0
- package/template/.agents/skills/shadcn/mcp.md +105 -0
- package/template/.agents/skills/shadcn/registry.md +277 -0
- package/template/.agents/skills/shadcn/rules/base-vs-radix.md +306 -0
- package/template/.agents/skills/shadcn/rules/chat.md +224 -0
- package/template/.agents/skills/shadcn/rules/composition.md +213 -0
- package/template/.agents/skills/shadcn/rules/forms.md +192 -0
- package/template/.agents/skills/shadcn/rules/icons.md +101 -0
- package/template/.agents/skills/shadcn/rules/styling.md +185 -0
- package/template/AGENTS.md +7 -4
- package/template/README.md +21 -4
- package/template/components.json +9 -0
- package/template/hooks/use-mobile.ts +19 -0
- package/template/lib/utils.ts +6 -0
- package/template/package.json +27 -5
- package/template/pages/getting-started/index.tsx +51 -45
- package/template/styles/globals.css +118 -0
- package/template/tsconfig.json +13 -2
- package/template/ui/accordion.tsx +64 -0
- package/template/ui/alert-dialog.tsx +196 -0
- package/template/ui/alert.tsx +66 -0
- package/template/ui/aspect-ratio.tsx +11 -0
- package/template/ui/attachment.tsx +204 -0
- package/template/ui/avatar.tsx +107 -0
- package/template/ui/badge.tsx +48 -0
- package/template/ui/breadcrumb.tsx +109 -0
- package/template/ui/bubble.tsx +125 -0
- package/template/ui/button-group.tsx +83 -0
- package/template/ui/button.tsx +64 -0
- package/template/ui/calendar.tsx +218 -0
- package/template/ui/card.tsx +92 -0
- package/template/ui/carousel.tsx +241 -0
- package/template/ui/chart.tsx +374 -0
- package/template/ui/checkbox.tsx +32 -0
- package/template/ui/collapsible.tsx +31 -0
- package/template/ui/combobox.tsx +308 -0
- package/template/ui/command.tsx +182 -0
- package/template/ui/context-menu.tsx +252 -0
- package/template/ui/dialog.tsx +156 -0
- package/template/ui/direction.tsx +22 -0
- package/template/ui/drawer.tsx +133 -0
- package/template/ui/dropdown-menu.tsx +257 -0
- package/template/ui/empty.tsx +104 -0
- package/template/ui/field.tsx +246 -0
- package/template/ui/form.tsx +167 -0
- package/template/ui/hover-card.tsx +42 -0
- package/template/ui/input-group.tsx +170 -0
- package/template/ui/input-otp.tsx +77 -0
- package/template/ui/input.tsx +21 -0
- package/template/ui/item.tsx +193 -0
- package/template/ui/kbd.tsx +28 -0
- package/template/ui/label.tsx +22 -0
- package/template/ui/marker.tsx +69 -0
- package/template/ui/menubar.tsx +276 -0
- package/template/ui/message-scroller.tsx +128 -0
- package/template/ui/message.tsx +92 -0
- package/template/ui/native-select.tsx +62 -0
- package/template/ui/navigation-menu.tsx +168 -0
- package/template/ui/pagination.tsx +127 -0
- package/template/ui/popover.tsx +87 -0
- package/template/ui/progress.tsx +31 -0
- package/template/ui/radio-group.tsx +43 -0
- package/template/ui/resizable.tsx +53 -0
- package/template/ui/scroll-area.tsx +56 -0
- package/template/ui/select.tsx +190 -0
- package/template/ui/separator.tsx +26 -0
- package/template/ui/sheet.tsx +143 -0
- package/template/ui/sidebar.tsx +726 -0
- package/template/ui/skeleton.tsx +13 -0
- package/template/ui/slider.tsx +61 -0
- package/template/ui/sonner.tsx +40 -0
- package/template/ui/spinner.tsx +16 -0
- package/template/ui/switch.tsx +33 -0
- package/template/ui/table.tsx +116 -0
- package/template/ui/tabs.tsx +89 -0
- package/template/ui/textarea.tsx +18 -0
- package/template/ui/toggle-group.tsx +81 -0
- package/template/ui/toggle.tsx +47 -0
- 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`.
|