@gradeui/ui 1.1.0 → 1.2.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/components/ui/code.md +2 -1
- package/components/ui/composer.md +226 -0
- package/components/ui/message.md +229 -0
- package/dist/contracts.js +48 -4
- package/dist/contracts.js.map +1 -1
- package/dist/contracts.mjs +48 -4
- package/dist/contracts.mjs.map +1 -1
- package/dist/index.d.mts +698 -18
- package/dist/index.d.ts +698 -18
- package/dist/index.js +92 -48
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +92 -48
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +11 -1
package/components/ui/code.md
CHANGED
|
@@ -18,8 +18,9 @@ props:
|
|
|
18
18
|
- filename?: string — optional label rendered in the header chrome
|
|
19
19
|
- wrap?: boolean — wrap long lines instead of horizontal scroll
|
|
20
20
|
- bare?: boolean — drop chrome (border, header, padding) — for inline use
|
|
21
|
+
- size? (xs | sm | md) — type-scale preset. `xs` (12px) for dense changelog cards / inline blocks; `sm` (14px, default) for marketing heroes and docs; `md` (16px) for focal-point displays.
|
|
21
22
|
- height? (auto | number | string) — container sizing. `auto` (default) grows with content. Number = pixels (`300` → `300px`). String passes through as CSS (`"20rem"`, `"50vh"`).
|
|
22
|
-
- maxLines?: number — cap the visible line count at exactly N line-heights. Wins over `height`.
|
|
23
|
+
- maxLines?: number — cap the visible line count at exactly N line-heights. Wins over `height`. Inherits the current size's line-height automatically.
|
|
23
24
|
when_to_use: Read-only code surface for marketing heroes, docs, changelog entries, AI-output displays. Use `diff` for the "diff hero" pattern (before/after side-by-side or stacked). Use `reveal="lines"` with `trigger="inView"` for scroll-driven marketing pages. Use `reveal="typewriter"` for AI-output / chat-style displays. Use `bare` for inline code inside prose. NOT a code editor — for editable code, reach for an external editor primitive (CodeMirror / Monaco).
|
|
24
25
|
composes_with: [SectionBlock, Card, Tabs (for multi-file examples), Carousel (slide-to-slide code progression)]
|
|
25
26
|
aliases: [code block, code, code snippet, code surface, syntax highlighted code, diff hero, diff view, diff block, changelog code, before after code, scroll-triggered code, typewriter code]
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Composer
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- placeholder?: string
|
|
6
|
+
- initialText?: string — plain text content to seed on mount
|
|
7
|
+
- initialJson?: string — Lexical state JSON (from a previous onSubmit round-trip)
|
|
8
|
+
- formats?: ComposerFormat[] | false — available formats (defaults to bold/italic/underline/strikethrough/code/h1/h2/blockquote/ul/ol); pass false for plain text only
|
|
9
|
+
- toolbar?: boolean | "top" — show the formatting toolbar above the editor; default false
|
|
10
|
+
- triggers?: ComposerTriggerConfig[] — mention/slash configs, eg. `[{ char: "@", items: people }, { char: "/", items: commands }]`
|
|
11
|
+
- attachments?: boolean | ComposerAttachmentConfig — enable image paste + paperclip when true/object; default off
|
|
12
|
+
- onSubmit?: (content: ComposerContent, attachments?: ComposerAttachment[]) => void
|
|
13
|
+
- isLoading?: boolean — disables editor, swaps default Send for Stop
|
|
14
|
+
- onStop?: () => void
|
|
15
|
+
- maxLength?: number
|
|
16
|
+
- autoFocus?: boolean
|
|
17
|
+
- submitOnEnter?: boolean — default true (Shift-Enter still inserts newline)
|
|
18
|
+
- leftActions?: ReactNode — override the default paperclip
|
|
19
|
+
- rightActions?: ReactNode — override the default Send/Stop
|
|
20
|
+
- hideSend?: boolean — hide the default Send without replacing it
|
|
21
|
+
- steps?: ComposerStep[] — scripted demo sequence
|
|
22
|
+
- trigger?: DemoTrigger — "mount" | "inView" | "manual"; default "mount"
|
|
23
|
+
- play?: boolean — for trigger="manual"
|
|
24
|
+
- speed?: DemoSpeed — "slow" | "normal" | "fast"; default "normal"
|
|
25
|
+
- loop?: boolean
|
|
26
|
+
- loopDelay?: number — ms between loop iterations, default 2000
|
|
27
|
+
- readOnly?: boolean — disables editing AND focusability; programmatic playback still works; use for marketing demos so the script doesn't steal focus
|
|
28
|
+
- bare?: boolean — strip the card chrome
|
|
29
|
+
- className?: string
|
|
30
|
+
when_to_use: |
|
|
31
|
+
THE PRIMITIVE for any text composition surface — Slack / Discord /
|
|
32
|
+
Teams chat input, AI chat / copilot prompt box, comment thread input,
|
|
33
|
+
GitHub / Linear / Jira comment box, Reddit / Twitter reply box,
|
|
34
|
+
Notion / Linear document body, email composer, post body, anywhere
|
|
35
|
+
a user types text and submits.
|
|
36
|
+
|
|
37
|
+
CONCRETE TEST — if you find yourself writing a `<textarea>` (or
|
|
38
|
+
`<Input>` styled tall) with a row of `<Bold>` / `<Italic>` /
|
|
39
|
+
`<Paperclip>` / `<Send>` buttons below or beside it, STOP. That is
|
|
40
|
+
`<Composer>`. Use it.
|
|
41
|
+
|
|
42
|
+
Common shapes:
|
|
43
|
+
Chat input with formatting + attachments + send
|
|
44
|
+
→ <Composer formats={["bold","italic","code"]} toolbar attachments />
|
|
45
|
+
AI prompt box with paperclip + send
|
|
46
|
+
→ <AIChatComposer /> (preset wrapping Composer)
|
|
47
|
+
Comment / reply input
|
|
48
|
+
→ <ComposerReply triggers={[{char:"@", items: people}]} />
|
|
49
|
+
Document body editor
|
|
50
|
+
→ <Composer toolbar formats={[...]} bare />
|
|
51
|
+
|
|
52
|
+
Built on Lexical for rich text, mentions, slash commands. The
|
|
53
|
+
`attachments` prop wires image paste + paperclip + chip preview row
|
|
54
|
+
with object URL lifecycle handled internally — don't roll that
|
|
55
|
+
plumbing yourself. The `triggers` prop wires @mentions and /slash
|
|
56
|
+
commands with a typeahead popover. The `formats` array picks which
|
|
57
|
+
toolbar buttons render when `toolbar` is on.
|
|
58
|
+
|
|
59
|
+
Shares the lib/demo step vocabulary with <Code> so scripted
|
|
60
|
+
typing/format/mention demos animate in the same rhythm as your
|
|
61
|
+
terminal demos.
|
|
62
|
+
composes_with: [AIChatComposer (preset wrapping this with paperclip + send + attachments), ComposerReply (preset for comment threads), AIChat (uses AIChatComposer internally), Card (host above for reply boxes), Avatar (in leftActions slot for "your" avatar next to the input)]
|
|
63
|
+
aliases: [
|
|
64
|
+
composer, message input, message bar, rich text editor, rich text input,
|
|
65
|
+
mention input, slash input, text editor, prompt input, comment composer,
|
|
66
|
+
comment input, reply input, reply box,
|
|
67
|
+
chat input, chat box, chat input bar, chat composer, chat field,
|
|
68
|
+
slack input, slack composer, slack message box, discord input,
|
|
69
|
+
discord composer, teams chat input, message composer, post composer,
|
|
70
|
+
textarea with toolbar, formatting input, formatted text input,
|
|
71
|
+
message field, send message input, write a message, compose message
|
|
72
|
+
]
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
```jsx
|
|
76
|
+
// Plain text chat-style composer
|
|
77
|
+
<Composer
|
|
78
|
+
placeholder="Ask anything…"
|
|
79
|
+
onSubmit={(content) => send(content.text)}
|
|
80
|
+
formats={false}
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
// Comment composer with mentions
|
|
84
|
+
<Composer
|
|
85
|
+
placeholder="Add a comment…"
|
|
86
|
+
triggers={[{ char: "@", items: teamMembers }]}
|
|
87
|
+
onSubmit={(content) => postComment(content.text, content.mentions)}
|
|
88
|
+
submitOnEnter={false}
|
|
89
|
+
formats={["bold", "italic", "code"]}
|
|
90
|
+
toolbar
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
// AI chat composer with attachments, mentions AND slash commands
|
|
94
|
+
<Composer
|
|
95
|
+
placeholder="Describe a UI, or paste a screenshot…"
|
|
96
|
+
triggers={[
|
|
97
|
+
{ char: "@", items: docs },
|
|
98
|
+
{ char: "/", items: commands, stripTrigger: true },
|
|
99
|
+
]}
|
|
100
|
+
attachments
|
|
101
|
+
onSubmit={(content, atts) => {
|
|
102
|
+
sendToAssistant(content.text, content.mentions, atts?.map(a => a.file));
|
|
103
|
+
}}
|
|
104
|
+
isLoading={isStreaming}
|
|
105
|
+
onStop={stop}
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
// Marketing demo — scripted playback
|
|
109
|
+
<Composer
|
|
110
|
+
placeholder="Type a message…"
|
|
111
|
+
triggers={[{ char: "@", items: [{ id: "1", value: "alice" }] }]}
|
|
112
|
+
steps={[
|
|
113
|
+
{ type: "type", text: "Hey " },
|
|
114
|
+
{ type: "mention", trigger: "@", value: "alice", query: "ali" },
|
|
115
|
+
{ type: "type", text: ", check out " },
|
|
116
|
+
{ type: "select", text: "check out" },
|
|
117
|
+
{ type: "format", format: "italic" },
|
|
118
|
+
{ type: "wait", ms: 800 },
|
|
119
|
+
{ type: "submit" },
|
|
120
|
+
]}
|
|
121
|
+
trigger="inView"
|
|
122
|
+
speed="normal"
|
|
123
|
+
loop
|
|
124
|
+
/>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Demo step vocabulary
|
|
128
|
+
|
|
129
|
+
Shares `type` / `wait` / `clear` with `<Code>` (driven by the same `useScriptedDemo` hook). Adds Composer-specific verbs:
|
|
130
|
+
|
|
131
|
+
- `{ type: "mention", trigger, value, query? }` — insert a mention/slash token. Pass `query` to show the typeahead in flight, then resolve to `value`.
|
|
132
|
+
- `{ type: "format", format }` — apply a format to the current selection.
|
|
133
|
+
- `{ type: "select", text }` — select a substring (precondition for `format`).
|
|
134
|
+
- `{ type: "newline" }` — insert a paragraph break.
|
|
135
|
+
- `{ type: "submit" }` — fire `onSubmit`.
|
|
136
|
+
|
|
137
|
+
## Imperative handle
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
const ref = useRef<ComposerHandle>(null);
|
|
141
|
+
ref.current?.focus();
|
|
142
|
+
ref.current?.clear();
|
|
143
|
+
ref.current?.insert("…");
|
|
144
|
+
ref.current?.restart(); // replay scripted steps from the start
|
|
145
|
+
ref.current?.restart(3000); // replay after a 3s delay
|
|
146
|
+
ref.current?.getContent(); // { text, json, mentions }
|
|
147
|
+
ref.current?.getEditor(); // underlying Lexical editor (escape hatch)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Themes
|
|
151
|
+
|
|
152
|
+
All colours read from CSS variables (`--gds-composer-*` palette in `globals.css`). The mention pills, toolbar buttons, attachment chips, and editor surface all rebrand with the active gradeui theme without component changes.
|
|
153
|
+
|
|
154
|
+
## Anti-patterns
|
|
155
|
+
|
|
156
|
+
```jsx
|
|
157
|
+
// ❌ Rolling a chat / Slack / Discord input as <textarea> + manual
|
|
158
|
+
// toolbar buttons + Send button. This is the EXACT shape Composer
|
|
159
|
+
// exists to consolidate — caught in the wild on a "Slack clone"
|
|
160
|
+
// generation where the model assembled this inline.
|
|
161
|
+
// Loses: attachment intake + object URL lifecycle, mention popover,
|
|
162
|
+
// slash commands, action-row slots, the Lexical state graph for
|
|
163
|
+
// rich content round-trip, the scripted-demo step machine.
|
|
164
|
+
<div className="border rounded-xl bg-card">
|
|
165
|
+
<textarea
|
|
166
|
+
placeholder="Message #general"
|
|
167
|
+
value={inputText}
|
|
168
|
+
onChange={(e) => setInputText(e.target.value)}
|
|
169
|
+
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) handleSend(); }}
|
|
170
|
+
rows={3}
|
|
171
|
+
className="w-full bg-transparent p-3 resize-none focus:outline-none"
|
|
172
|
+
/>
|
|
173
|
+
<Row justify="between" align="center" className="px-3 py-2 border-t">
|
|
174
|
+
<Row gap="xs">
|
|
175
|
+
<Button size="icon" variant="ghost"><Bold /></Button>
|
|
176
|
+
<Button size="icon" variant="ghost"><Italic /></Button>
|
|
177
|
+
<Button size="icon" variant="ghost"><List /></Button>
|
|
178
|
+
<Button size="icon" variant="ghost"><Smile /></Button>
|
|
179
|
+
<Button size="icon" variant="ghost"><Paperclip /></Button>
|
|
180
|
+
</Row>
|
|
181
|
+
<Button onClick={handleSend}>Send</Button>
|
|
182
|
+
</Row>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
// ✅ The Grade way. Same shape, every affordance free.
|
|
186
|
+
<Composer
|
|
187
|
+
placeholder="Message #general"
|
|
188
|
+
formats={["bold", "italic", "code", "ul"]}
|
|
189
|
+
toolbar
|
|
190
|
+
attachments
|
|
191
|
+
triggers={[{ char: "@", items: teamMembers }]}
|
|
192
|
+
onSubmit={(content, atts) => handleSend(content.text, atts)}
|
|
193
|
+
/>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```jsx
|
|
197
|
+
// ❌ Reaching for <Input> (single-line) for a multi-line chat / reply
|
|
198
|
+
// surface. Input is for one-line text fields. Use Composer for any
|
|
199
|
+
// surface where the user might type more than one line — chat,
|
|
200
|
+
// comments, post bodies.
|
|
201
|
+
<Input
|
|
202
|
+
placeholder="Reply to thread…"
|
|
203
|
+
value={reply}
|
|
204
|
+
onChange={(e) => setReply(e.target.value)}
|
|
205
|
+
/>
|
|
206
|
+
<Button onClick={postReply}>Reply</Button>
|
|
207
|
+
|
|
208
|
+
// ✅ ComposerReply preset has the right defaults for a reply box.
|
|
209
|
+
<ComposerReply
|
|
210
|
+
placeholder="Reply to thread…"
|
|
211
|
+
triggers={[{ char: "@", items: people }]}
|
|
212
|
+
onSubmit={(content) => postReply(content.text)}
|
|
213
|
+
/>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```jsx
|
|
217
|
+
// ❌ Importing TipTap, Lexical, Slate, or any other editor framework
|
|
218
|
+
// directly into a scaffold. Composer already wraps Lexical and
|
|
219
|
+
// handles all the plumbing.
|
|
220
|
+
import { useEditor, EditorContent } from "@tiptap/react";
|
|
221
|
+
const editor = useEditor({ extensions: [StarterKit, ...] });
|
|
222
|
+
<EditorContent editor={editor} />
|
|
223
|
+
|
|
224
|
+
// ✅ Use Composer. Same capability, integrated with the design system.
|
|
225
|
+
<Composer toolbar formats={["bold", "italic", "h1", "h2", "blockquote", "ul", "ol"]} />
|
|
226
|
+
```
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Message
|
|
3
|
+
import: "@gradeui/ui"
|
|
4
|
+
props:
|
|
5
|
+
- author: string — display name of the message author
|
|
6
|
+
- timestamp?: ReactNode — string ("11:24", "2 hours ago") or any node for custom formatting
|
|
7
|
+
- avatar?: ReactNode — slot for any `<Avatar>` composition; omit for grouped messages from the same author
|
|
8
|
+
- badge?: ReactNode — small chip(s) next to the author name (OP, Bot, Admin, role tag)
|
|
9
|
+
- edited?: boolean | string — renders "(edited)" hint next to timestamp; pass a string to customise ("(edited 2 minutes ago)")
|
|
10
|
+
- pinned?: boolean — renders a pin glyph + "Pinned" label above the header row for sticky / pinned messages
|
|
11
|
+
- actions?: ReactNode — end-of-header slot, typically hover-revealed icon buttons (reply / react / more)
|
|
12
|
+
- reactions?: ReactNode — slot below the body, typically a Row of reaction chips (emoji + count)
|
|
13
|
+
- threadCount?: number — renders a "N replies" link affordance below the body
|
|
14
|
+
- onThreadClick?: () => void — handler for the threadCount affordance
|
|
15
|
+
- align?: "start" | "end" — `start` (default) puts the avatar on the left; `end` mirrors for "your messages" in DM threads
|
|
16
|
+
- children: ReactNode — body content (plain text or rich nodes)
|
|
17
|
+
- className?: string
|
|
18
|
+
when_to_use: |
|
|
19
|
+
The canonical "avatar + author + timestamp + body" row. THE PRIMITIVE
|
|
20
|
+
for any chat surface, comment thread, post-reply, activity log, or
|
|
21
|
+
notification feed that follows the people-and-text shape.
|
|
22
|
+
|
|
23
|
+
CONCRETE TEST — if you find yourself composing an `<Avatar>` followed
|
|
24
|
+
by a `<Row>` of author name + timestamp, with a `<p>` or `<span>`
|
|
25
|
+
body below, STOP. That is `<Message>`. Reach for it directly.
|
|
26
|
+
|
|
27
|
+
Slack-style channel feed, Discord messages, Teams chat, Linear /
|
|
28
|
+
GitHub / Jira comments, Reddit replies, Twitter/X posts in a thread,
|
|
29
|
+
Notion comment sidebars, in-app activity logs, notification rows —
|
|
30
|
+
every one of these IS `<Message>`. Do not roll the layout inline.
|
|
31
|
+
|
|
32
|
+
For non-people activity (system events, log lines, status pings) use
|
|
33
|
+
Callout or a plain Row instead — Message implies a human author.
|
|
34
|
+
composes_with: [Avatar (in the avatar slot — pair with AvatarFallback tone="..." for stable per-author colour), Badge (in the badge slot for role / OP / bot tags), Button (in actions, typically size="icon" + variant="ghost"), Stack (host multiple Messages in a thread), Card (wrap a Stack of Messages for a comment-thread block)]
|
|
35
|
+
aliases: [
|
|
36
|
+
message, chat message, comment, post, reply, activity row, notification row,
|
|
37
|
+
thread row, channel message, dm message, slack message, discord message,
|
|
38
|
+
teams message, channel feed message, feed item, feed row, message row,
|
|
39
|
+
user message, user post, conversation message, conversation row,
|
|
40
|
+
inline comment, threaded reply, message bubble, chat bubble, talk bubble
|
|
41
|
+
]
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
```jsx
|
|
45
|
+
// Comment thread shape — avatar left, body below the author row.
|
|
46
|
+
<Stack gap="md">
|
|
47
|
+
<Message
|
|
48
|
+
author="alice"
|
|
49
|
+
timestamp="2 hours ago"
|
|
50
|
+
avatar={
|
|
51
|
+
<Avatar size="sm">
|
|
52
|
+
<AvatarFallback tone="violet">A</AvatarFallback>
|
|
53
|
+
</Avatar>
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
Splitting this into two PRs makes the review tractable.
|
|
57
|
+
</Message>
|
|
58
|
+
<Message
|
|
59
|
+
author="ben"
|
|
60
|
+
timestamp="1 hour ago"
|
|
61
|
+
badge={<Badge variant="outline" className="text-[10px]">OP</Badge>}
|
|
62
|
+
avatar={
|
|
63
|
+
<Avatar size="sm">
|
|
64
|
+
<AvatarFallback tone="amber">B</AvatarFallback>
|
|
65
|
+
</Avatar>
|
|
66
|
+
}
|
|
67
|
+
>
|
|
68
|
+
Agreed. I'll take the schema PR.
|
|
69
|
+
</Message>
|
|
70
|
+
</Stack>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```jsx
|
|
74
|
+
// Chat shape — your messages right-aligned via align="end".
|
|
75
|
+
<Stack gap="md">
|
|
76
|
+
<Message
|
|
77
|
+
author="alice"
|
|
78
|
+
timestamp="11:24"
|
|
79
|
+
avatar={
|
|
80
|
+
<Avatar size="xs">
|
|
81
|
+
<AvatarFallback tone="violet">A</AvatarFallback>
|
|
82
|
+
</Avatar>
|
|
83
|
+
}
|
|
84
|
+
>
|
|
85
|
+
Hey, how's the launch going?
|
|
86
|
+
</Message>
|
|
87
|
+
<Message
|
|
88
|
+
author="you"
|
|
89
|
+
timestamp="11:26"
|
|
90
|
+
align="end"
|
|
91
|
+
avatar={
|
|
92
|
+
<Avatar size="xs">
|
|
93
|
+
<AvatarFallback tone="emerald">Y</AvatarFallback>
|
|
94
|
+
</Avatar>
|
|
95
|
+
}
|
|
96
|
+
>
|
|
97
|
+
Launch image is in, scheduling now.
|
|
98
|
+
</Message>
|
|
99
|
+
</Stack>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```jsx
|
|
103
|
+
// Full Slack-style message — edited indicator, pinned flag, reactions
|
|
104
|
+
// row, threaded reply count, role badge, hover actions.
|
|
105
|
+
<Message
|
|
106
|
+
author="alice"
|
|
107
|
+
timestamp="11:24"
|
|
108
|
+
edited
|
|
109
|
+
pinned
|
|
110
|
+
badge={<Badge variant="secondary" className="text-[10px]">Designer</Badge>}
|
|
111
|
+
avatar={
|
|
112
|
+
<Avatar size="md">
|
|
113
|
+
<AvatarFallback tone="violet">A</AvatarFallback>
|
|
114
|
+
</Avatar>
|
|
115
|
+
}
|
|
116
|
+
reactions={
|
|
117
|
+
<>
|
|
118
|
+
<Badge variant="outline" className="gap-1 cursor-pointer">👍 4</Badge>
|
|
119
|
+
<Badge variant="outline" className="gap-1 cursor-pointer">🎉 2</Badge>
|
|
120
|
+
</>
|
|
121
|
+
}
|
|
122
|
+
threadCount={3}
|
|
123
|
+
onThreadClick={() => openThread(messageId)}
|
|
124
|
+
>
|
|
125
|
+
Updated the token spec — review when you have a chance.
|
|
126
|
+
</Message>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```jsx
|
|
130
|
+
// Slack / Discord channel feed — with role badge + hover-revealed actions.
|
|
131
|
+
<Stack gap="lg">
|
|
132
|
+
{messages.map((m) => (
|
|
133
|
+
<Message
|
|
134
|
+
key={m.id}
|
|
135
|
+
author={m.user}
|
|
136
|
+
timestamp={m.time}
|
|
137
|
+
badge={<Badge variant="secondary" className="text-[10px]">{m.role}</Badge>}
|
|
138
|
+
avatar={
|
|
139
|
+
<Avatar size="md">
|
|
140
|
+
<AvatarImage src={m.avatar} />
|
|
141
|
+
<AvatarFallback tone="sky">{m.user.charAt(0)}</AvatarFallback>
|
|
142
|
+
</Avatar>
|
|
143
|
+
}
|
|
144
|
+
actions={
|
|
145
|
+
<Row gap="xs" className="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
146
|
+
<Button size="icon" variant="ghost" className="h-6 w-6"><Smile className="h-3 w-3" /></Button>
|
|
147
|
+
<Button size="icon" variant="ghost" className="h-6 w-6"><Reply className="h-3 w-3" /></Button>
|
|
148
|
+
<Button size="icon" variant="ghost" className="h-6 w-6"><MoreHorizontal className="h-3 w-3" /></Button>
|
|
149
|
+
</Row>
|
|
150
|
+
}
|
|
151
|
+
className="group"
|
|
152
|
+
>
|
|
153
|
+
{m.text}
|
|
154
|
+
</Message>
|
|
155
|
+
))}
|
|
156
|
+
</Stack>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Anti-patterns
|
|
160
|
+
|
|
161
|
+
```jsx
|
|
162
|
+
// ❌ Rolling the message layout by hand from Avatar + Row + Badge + spans.
|
|
163
|
+
// This is the EXACT shape Message exists to consolidate — caught in
|
|
164
|
+
// the wild on a "Slack clone" prompt where the model assembled this
|
|
165
|
+
// inline instead of reaching for Message. The result loses the
|
|
166
|
+
// align="end" knob, the actions slot, the data-gds-part hooks, and
|
|
167
|
+
// duplicates the same flex template across every consumer.
|
|
168
|
+
{messages.map((msg) => (
|
|
169
|
+
<div className="group flex gap-4">
|
|
170
|
+
<Avatar className="w-9 h-9 shrink-0">
|
|
171
|
+
<AvatarImage src={msg.avatar} />
|
|
172
|
+
<AvatarFallback>{msg.user.charAt(0)}</AvatarFallback>
|
|
173
|
+
</Avatar>
|
|
174
|
+
<div className="flex-1 min-w-0">
|
|
175
|
+
<Row gap="sm" align="baseline">
|
|
176
|
+
<span className="font-semibold text-sm">{msg.user}</span>
|
|
177
|
+
<Badge variant="secondary" className="text-[10px]">{msg.role}</Badge>
|
|
178
|
+
<span className="text-[10px] text-muted-foreground">{msg.time}</span>
|
|
179
|
+
</Row>
|
|
180
|
+
<p className="text-sm mt-1">{msg.text}</p>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
))}
|
|
184
|
+
|
|
185
|
+
// ✅ The Grade way.
|
|
186
|
+
{messages.map((msg) => (
|
|
187
|
+
<Message
|
|
188
|
+
key={msg.id}
|
|
189
|
+
author={msg.user}
|
|
190
|
+
timestamp={msg.time}
|
|
191
|
+
badge={<Badge variant="secondary" className="text-[10px]">{msg.role}</Badge>}
|
|
192
|
+
avatar={
|
|
193
|
+
<Avatar size="md">
|
|
194
|
+
<AvatarImage src={msg.avatar} />
|
|
195
|
+
<AvatarFallback>{msg.user.charAt(0)}</AvatarFallback>
|
|
196
|
+
</Avatar>
|
|
197
|
+
}
|
|
198
|
+
>
|
|
199
|
+
{msg.text}
|
|
200
|
+
</Message>
|
|
201
|
+
))}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```jsx
|
|
205
|
+
// ❌ Building a custom "AuthorDot" or "MessageRow" component inline as
|
|
206
|
+
// a one-off helper inside a scaffold. Three scaffolds did this before
|
|
207
|
+
// Message landed; the pattern is always identical.
|
|
208
|
+
function MessageRow({ user, body, time }) {
|
|
209
|
+
return (
|
|
210
|
+
<div className="flex gap-3 items-start">
|
|
211
|
+
<div className="h-7 w-7 rounded-full bg-violet-500/20 ...">{user[0]}</div>
|
|
212
|
+
<div>
|
|
213
|
+
<Row><strong>{user}</strong> <small>{time}</small></Row>
|
|
214
|
+
<p>{body}</p>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ✅ Use Message. The colored-initials avatar pattern is covered by
|
|
221
|
+
// Avatar + AvatarFallback tone="...".
|
|
222
|
+
<Message
|
|
223
|
+
author={user}
|
|
224
|
+
timestamp={time}
|
|
225
|
+
avatar={<Avatar size="sm"><AvatarFallback tone="violet">{user[0]}</AvatarFallback></Avatar>}
|
|
226
|
+
>
|
|
227
|
+
{body}
|
|
228
|
+
</Message>
|
|
229
|
+
```
|