@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.
@@ -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`. Use for terminal windows, code-tour cards, and surfaces that need a stable vertical rhythm regardless of snippet length.
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
+ ```