@autono/open-pages 0.1.0 → 0.3.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 +8 -5
- package/dist/{build-TP72kiw7.js → build-Cq6uszaI.js} +3 -3
- package/dist/cli/bin.js +4 -4
- package/dist/{config-L0IbktJ0.js → config-DniCVIl6.js} +34 -7
- package/dist/{dev-I2IyEh5W.js → dev-DyY9SvwF.js} +2 -2
- package/dist/export-DqLDEQkL.js +4 -0
- package/dist/{export-ic28osQP.js → export-pYjzc4Cl.js} +10 -3
- package/dist/{open-pages-plugin-D_DN_zjh.js → open-pages-plugin-CACQ7TVo.js} +40 -6
- package/dist/{preview-BrDuvgAN.js → preview-CLb35jHx.js} +2 -2
- package/dist/vite/index.js +2 -2
- package/package.json +1 -1
- package/skills/apply-comments/SKILL.md +4 -4
- package/skills/create-page/SKILL.md +26 -8
- package/skills/create-theme/SKILL.md +137 -150
- package/skills/current-page/SKILL.md +1 -1
- package/skills/page-authoring/SKILL.md +97 -42
- package/skills/page-authoring/references/interactivity.md +70 -22
- package/skills/page-authoring/references/layout-and-responsive.md +38 -17
- package/skills/page-authoring/references/typography-and-color.md +50 -32
- package/skills/shadcn/SKILL.md +277 -0
- package/skills/shadcn/assets/shadcn-small.png +0 -0
- package/skills/shadcn/assets/shadcn.png +0 -0
- package/skills/shadcn/cli.md +290 -0
- package/skills/shadcn/customization.md +209 -0
- package/skills/shadcn/mcp.md +105 -0
- package/skills/shadcn/registry.md +277 -0
- package/skills/shadcn/rules/base-vs-radix.md +306 -0
- package/skills/shadcn/rules/chat.md +224 -0
- package/skills/shadcn/rules/composition.md +213 -0
- package/skills/shadcn/rules/forms.md +192 -0
- package/skills/shadcn/rules/icons.md +101 -0
- package/skills/shadcn/rules/styling.md +185 -0
- package/src/app/components/asset-view.tsx +11 -11
- package/src/app/components/command/command-menu.tsx +6 -6
- package/src/app/components/command/command.tsx +2 -2
- package/src/app/components/command/home-command-menu.tsx +2 -2
- package/src/app/components/icon-tooltip.tsx +1 -1
- package/src/app/components/language-toggle.tsx +7 -7
- package/src/app/components/sidebar/folder-item.tsx +5 -5
- package/src/app/components/sidebar/icon-picker.tsx +3 -3
- package/src/app/components/sidebar/sidebar-footer.tsx +4 -4
- package/src/app/components/sidebar/sidebar.tsx +6 -6
- package/src/app/components/theme-toggle.tsx +6 -6
- package/src/app/components/themes/theme-detail.tsx +4 -4
- package/src/app/components/themes/themes-gallery.tsx +1 -1
- package/src/app/components/ui/badge.tsx +1 -1
- package/src/app/components/ui/button.tsx +1 -1
- package/src/app/components/ui/card.tsx +1 -1
- package/src/app/components/ui/context-menu.tsx +1 -1
- package/src/app/components/ui/dialog.tsx +2 -2
- package/src/app/components/ui/dropdown-menu.tsx +1 -1
- package/src/app/components/ui/input.tsx +1 -1
- package/src/app/components/ui/label.tsx +1 -1
- package/src/app/components/ui/popover.tsx +1 -1
- package/src/app/components/ui/progress.tsx +1 -1
- package/src/app/components/ui/scroll-area.tsx +1 -1
- package/src/app/components/ui/select.tsx +1 -1
- package/src/app/components/ui/separator.tsx +1 -1
- package/src/app/components/ui/slider.tsx +1 -1
- package/src/app/components/ui/tabs.tsx +1 -1
- package/src/app/components/ui/textarea.tsx +1 -1
- package/src/app/components/ui/toggle-group.tsx +2 -2
- package/src/app/components/ui/toggle.tsx +1 -1
- package/src/app/components/ui/tooltip.tsx +1 -1
- package/src/app/frame/main.tsx +3 -1
- package/src/app/lib/themes.ts +1 -0
- package/src/app/lib/use-restart-server.ts +1 -1
- package/src/app/routes/home-shell.tsx +7 -7
- package/src/app/routes/home.tsx +5 -5
- package/src/app/routes/page.tsx +2 -2
- package/src/app/routes/themes.tsx +1 -1
- package/src/app/virtual.d.ts +3 -0
- package/dist/export-B-kBRWB1.js +0 -4
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Base vs Radix
|
|
2
|
+
|
|
3
|
+
API differences between `base` and `radix`. Check the `base` field from `npx shadcn@latest info`.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- Composition: asChild vs render
|
|
8
|
+
- Button / trigger as non-button element
|
|
9
|
+
- Select (items prop, placeholder, positioning, multiple, object values)
|
|
10
|
+
- ToggleGroup (type vs multiple)
|
|
11
|
+
- Slider (scalar vs array)
|
|
12
|
+
- Accordion (type and defaultValue)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Composition: asChild (radix) vs render (base)
|
|
17
|
+
|
|
18
|
+
Radix uses `asChild` to replace the default element. Base uses `render`. Don't wrap triggers in extra elements.
|
|
19
|
+
|
|
20
|
+
**Incorrect:**
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<DialogTrigger>
|
|
24
|
+
<div>
|
|
25
|
+
<Button>Open</Button>
|
|
26
|
+
</div>
|
|
27
|
+
</DialogTrigger>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Correct (radix):**
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
<DialogTrigger asChild>
|
|
34
|
+
<Button>Open</Button>
|
|
35
|
+
</DialogTrigger>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Correct (base):**
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
<DialogTrigger render={<Button />}>Open</DialogTrigger>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This applies to all trigger and close components: `DialogTrigger`, `SheetTrigger`, `AlertDialogTrigger`, `DropdownMenuTrigger`, `PopoverTrigger`, `TooltipTrigger`, `CollapsibleTrigger`, `DialogClose`, `SheetClose`, `NavigationMenuLink`, `BreadcrumbLink`, `SidebarMenuButton`, `Badge`, `Item`.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Button / trigger as non-button element (base only)
|
|
49
|
+
|
|
50
|
+
When `render` changes an element to a non-button (`<a>`, `<span>`), add `nativeButton={false}`.
|
|
51
|
+
|
|
52
|
+
**Incorrect (base):** missing `nativeButton={false}`.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
<Button render={<a href="/docs" />}>Read the docs</Button>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Correct (base):**
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
<Button render={<a href="/docs" />} nativeButton={false}>
|
|
62
|
+
Read the docs
|
|
63
|
+
</Button>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Correct (radix):**
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
<Button asChild>
|
|
70
|
+
<a href="/docs">Read the docs</a>
|
|
71
|
+
</Button>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Same for triggers whose `render` is not a `Button`:
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
// base.
|
|
78
|
+
<PopoverTrigger render={<InputGroupAddon />} nativeButton={false}>
|
|
79
|
+
Pick date
|
|
80
|
+
</PopoverTrigger>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Select
|
|
86
|
+
|
|
87
|
+
**items prop (base only).** Base requires an `items` prop on the root. Radix uses inline JSX only.
|
|
88
|
+
|
|
89
|
+
**Incorrect (base):**
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
<Select>
|
|
93
|
+
<SelectTrigger><SelectValue placeholder="Select a fruit" /></SelectTrigger>
|
|
94
|
+
</Select>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Correct (base):**
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
const items = [
|
|
101
|
+
{ label: "Select a fruit", value: null },
|
|
102
|
+
{ label: "Apple", value: "apple" },
|
|
103
|
+
{ label: "Banana", value: "banana" },
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
<Select items={items}>
|
|
107
|
+
<SelectTrigger>
|
|
108
|
+
<SelectValue />
|
|
109
|
+
</SelectTrigger>
|
|
110
|
+
<SelectContent>
|
|
111
|
+
<SelectGroup>
|
|
112
|
+
{items.map((item) => (
|
|
113
|
+
<SelectItem key={item.value} value={item.value}>{item.label}</SelectItem>
|
|
114
|
+
))}
|
|
115
|
+
</SelectGroup>
|
|
116
|
+
</SelectContent>
|
|
117
|
+
</Select>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Correct (radix):**
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
<Select>
|
|
124
|
+
<SelectTrigger>
|
|
125
|
+
<SelectValue placeholder="Select a fruit" />
|
|
126
|
+
</SelectTrigger>
|
|
127
|
+
<SelectContent>
|
|
128
|
+
<SelectGroup>
|
|
129
|
+
<SelectItem value="apple">Apple</SelectItem>
|
|
130
|
+
<SelectItem value="banana">Banana</SelectItem>
|
|
131
|
+
</SelectGroup>
|
|
132
|
+
</SelectContent>
|
|
133
|
+
</Select>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Placeholder.** Base uses a `{ value: null }` item in the items array. Radix uses `<SelectValue placeholder="...">`.
|
|
137
|
+
|
|
138
|
+
**Content positioning.** Base uses `alignItemWithTrigger`. Radix uses `position`.
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
// base.
|
|
142
|
+
<SelectContent alignItemWithTrigger={false} side="bottom">
|
|
143
|
+
|
|
144
|
+
// radix.
|
|
145
|
+
<SelectContent position="popper">
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Select — multiple selection and object values (base only)
|
|
151
|
+
|
|
152
|
+
Base supports `multiple`, render-function children on `SelectValue`, and object values with `itemToStringValue`. Radix is single-select with string values only.
|
|
153
|
+
|
|
154
|
+
**Correct (base — multiple selection):**
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
<Select items={items} multiple defaultValue={[]}>
|
|
158
|
+
<SelectTrigger>
|
|
159
|
+
<SelectValue>
|
|
160
|
+
{(value: string[]) => value.length === 0 ? "Select fruits" : `${value.length} selected`}
|
|
161
|
+
</SelectValue>
|
|
162
|
+
</SelectTrigger>
|
|
163
|
+
...
|
|
164
|
+
</Select>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Correct (base — object values):**
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
<Select defaultValue={plans[0]} itemToStringValue={(plan) => plan.name}>
|
|
171
|
+
<SelectTrigger>
|
|
172
|
+
<SelectValue>{(value) => value.name}</SelectValue>
|
|
173
|
+
</SelectTrigger>
|
|
174
|
+
...
|
|
175
|
+
</Select>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## ToggleGroup
|
|
181
|
+
|
|
182
|
+
Base uses a `multiple` boolean prop. Radix uses `type="single"` or `type="multiple"`.
|
|
183
|
+
|
|
184
|
+
**Incorrect (base):**
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
<ToggleGroup type="single" defaultValue="daily">
|
|
188
|
+
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
|
|
189
|
+
</ToggleGroup>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Correct (base):**
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
// Single (no prop needed), defaultValue is always an array.
|
|
196
|
+
<ToggleGroup defaultValue={["daily"]} spacing={2}>
|
|
197
|
+
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
|
|
198
|
+
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
|
|
199
|
+
</ToggleGroup>
|
|
200
|
+
|
|
201
|
+
// Multi-selection.
|
|
202
|
+
<ToggleGroup multiple>
|
|
203
|
+
<ToggleGroupItem value="bold">Bold</ToggleGroupItem>
|
|
204
|
+
<ToggleGroupItem value="italic">Italic</ToggleGroupItem>
|
|
205
|
+
</ToggleGroup>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Correct (radix):**
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
// Single, defaultValue is a string.
|
|
212
|
+
<ToggleGroup type="single" defaultValue="daily" spacing={2}>
|
|
213
|
+
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
|
|
214
|
+
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
|
|
215
|
+
</ToggleGroup>
|
|
216
|
+
|
|
217
|
+
// Multi-selection.
|
|
218
|
+
<ToggleGroup type="multiple">
|
|
219
|
+
<ToggleGroupItem value="bold">Bold</ToggleGroupItem>
|
|
220
|
+
<ToggleGroupItem value="italic">Italic</ToggleGroupItem>
|
|
221
|
+
</ToggleGroup>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Controlled single value:**
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
// base — wrap/unwrap arrays.
|
|
228
|
+
const [value, setValue] = React.useState("normal")
|
|
229
|
+
<ToggleGroup value={[value]} onValueChange={(v) => setValue(v[0])}>
|
|
230
|
+
|
|
231
|
+
// radix — plain string.
|
|
232
|
+
const [value, setValue] = React.useState("normal")
|
|
233
|
+
<ToggleGroup type="single" value={value} onValueChange={setValue}>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Slider
|
|
239
|
+
|
|
240
|
+
Base accepts a plain number for a single thumb. Radix always requires an array.
|
|
241
|
+
|
|
242
|
+
**Incorrect (base):**
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
<Slider defaultValue={[50]} max={100} step={1} />
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Correct (base):**
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
<Slider defaultValue={50} max={100} step={1} />
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Correct (radix):**
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
<Slider defaultValue={[50]} max={100} step={1} />
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Both use arrays for range sliders. Controlled `onValueChange` in base may need a cast:
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
// base.
|
|
264
|
+
const [value, setValue] = React.useState([0.3, 0.7])
|
|
265
|
+
<Slider value={value} onValueChange={(v) => setValue(v as number[])} />
|
|
266
|
+
|
|
267
|
+
// radix.
|
|
268
|
+
const [value, setValue] = React.useState([0.3, 0.7])
|
|
269
|
+
<Slider value={value} onValueChange={setValue} />
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Accordion
|
|
275
|
+
|
|
276
|
+
Radix requires `type="single"` or `type="multiple"` and supports `collapsible`. `defaultValue` is a string. Base uses no `type` prop, uses `multiple` boolean, and `defaultValue` is always an array.
|
|
277
|
+
|
|
278
|
+
**Incorrect (base):**
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
<Accordion type="single" collapsible defaultValue="item-1">
|
|
282
|
+
<AccordionItem value="item-1">...</AccordionItem>
|
|
283
|
+
</Accordion>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Correct (base):**
|
|
287
|
+
|
|
288
|
+
```tsx
|
|
289
|
+
<Accordion defaultValue={["item-1"]}>
|
|
290
|
+
<AccordionItem value="item-1">...</AccordionItem>
|
|
291
|
+
</Accordion>
|
|
292
|
+
|
|
293
|
+
// Multi-select.
|
|
294
|
+
<Accordion multiple defaultValue={["item-1", "item-2"]}>
|
|
295
|
+
<AccordionItem value="item-1">...</AccordionItem>
|
|
296
|
+
<AccordionItem value="item-2">...</AccordionItem>
|
|
297
|
+
</Accordion>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Correct (radix):**
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
<Accordion type="single" collapsible defaultValue="item-1">
|
|
304
|
+
<AccordionItem value="item-1">...</AccordionItem>
|
|
305
|
+
</Accordion>
|
|
306
|
+
```
|
|
@@ -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.
|