@robr0/design-system 0.4.0 → 0.5.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 +2 -2
- package/components/AgentStatus/AgentStatus.css +82 -22
- package/components/AgentStatus/AgentStatus.d.ts +6 -2
- package/components/AgentStatus/AgentStatus.js +38 -6
- package/components/AiButton/AiButton.css +163 -0
- package/components/AiButton/AiButton.d.ts +36 -0
- package/components/AiButton/AiButton.js +58 -0
- package/components/ChatHeader/ChatHeader.css +35 -0
- package/components/ChatHeader/ChatHeader.d.ts +30 -0
- package/components/ChatHeader/ChatHeader.js +17 -0
- package/components/ChatMarker/ChatMarker.css +52 -0
- package/components/ChatMarker/ChatMarker.d.ts +22 -0
- package/components/ChatMarker/ChatMarker.js +18 -0
- package/components/ChatMessage/ChatMessage.css +304 -0
- package/components/ChatMessage/ChatMessage.d.ts +68 -0
- package/components/ChatMessage/ChatMessage.js +69 -0
- package/components/ChatThread/ChatThread.css +171 -0
- package/components/ChatThread/ChatThread.d.ts +41 -0
- package/components/ChatThread/ChatThread.js +174 -0
- package/components/Chip/Chip.css +27 -0
- package/components/Chip/Chip.d.ts +2 -2
- package/components/CircularButton/CircularButton.d.ts +23 -8
- package/components/CircularButton/CircularButton.js +54 -52
- package/components/CodeBlock/CodeBlock.css +2 -2
- package/components/Composer/Composer.css +164 -0
- package/components/Composer/Composer.d.ts +67 -0
- package/components/Composer/Composer.js +120 -0
- package/components/DocumentChip/DocumentChip.css +181 -0
- package/components/DocumentChip/DocumentChip.d.ts +41 -0
- package/components/DocumentChip/DocumentChip.js +78 -0
- package/components/InterruptCard/InterruptCard.css +164 -0
- package/components/InterruptCard/InterruptCard.d.ts +55 -0
- package/components/InterruptCard/InterruptCard.js +57 -0
- package/components/MessageActions/MessageActions.css +76 -0
- package/components/MessageActions/MessageActions.d.ts +38 -0
- package/components/MessageActions/MessageActions.js +33 -0
- package/components/MessageCard/MessageCard.css +112 -0
- package/components/MessageCard/MessageCard.d.ts +35 -0
- package/components/MessageCard/MessageCard.js +28 -0
- package/components/NavList/NavList.css +155 -0
- package/components/NavList/NavList.d.ts +56 -0
- package/components/NavList/NavList.js +99 -0
- package/components/PromptSuggestions/PromptSuggestions.css +87 -0
- package/components/PromptSuggestions/PromptSuggestions.d.ts +51 -0
- package/components/PromptSuggestions/PromptSuggestions.js +34 -0
- package/components/Prose/Prose.css +252 -0
- package/components/Prose/Prose.d.ts +21 -0
- package/components/Prose/Prose.js +14 -0
- package/components/Reasoning/Reasoning.css +149 -25
- package/components/Reasoning/Reasoning.d.ts +19 -0
- package/components/Reasoning/Reasoning.js +32 -3
- package/components/SourceChip/SourceChip.css +102 -0
- package/components/SourceChip/SourceChip.d.ts +32 -0
- package/components/SourceChip/SourceChip.js +31 -0
- package/components/Stat/Stat.css +2 -2
- package/components/ToolCall/ToolCall.css +3 -3
- package/components/registry.json +113 -1
- package/components/registry.json.d.ts +113 -1
- package/components/registry.json.js +1 -1
- package/index.d.ts +14 -0
- package/index.js +28 -0
- package/package.json +1 -1
- package/tokens/registry.json +12 -0
- package/tokens/registry.json.d.ts +12 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +13 -0
- package/tokens/tokens-light.css +42 -0
- package/tokens/tokens-typography.css +24 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by ChatMarker itself — everything else falls through to the root element. */
|
|
3
|
+
type ChatMarkerOwnProps = {
|
|
4
|
+
/** Leading icon — a Material Symbol name, or any custom element. */
|
|
5
|
+
icon?: string | React.ReactNode;
|
|
6
|
+
/** Draw the flanking divider lines. Turn off for a bare centred note. */
|
|
7
|
+
line?: boolean;
|
|
8
|
+
/** Additional CSS classes */
|
|
9
|
+
className?: string;
|
|
10
|
+
/** The marker text, e.g. "Today" or "Chat renamed". */
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
};
|
|
13
|
+
export interface ChatMarkerProps extends ChatMarkerOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof ChatMarkerOwnProps> {
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* ChatMarker is the inline separator a conversation uses for anything that
|
|
17
|
+
* is not a turn: date breaks, joins, mode changes, system notes. It reads
|
|
18
|
+
* as furniture rather than as a message — quiet tertiary text between two
|
|
19
|
+
* divider lines, so the eye skips it while scanning turns.
|
|
20
|
+
*/
|
|
21
|
+
export declare const ChatMarker: React.ForwardRefExoticComponent<ChatMarkerProps & React.RefAttributes<HTMLDivElement>>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import "./ChatMarker.css";
|
|
4
|
+
import "../../fonts/material-symbols.css";
|
|
5
|
+
const ChatMarker = React.forwardRef(
|
|
6
|
+
({ icon, line = true, className = "", children, ...rest }, ref) => {
|
|
7
|
+
const baseClass = "ds-chat-marker";
|
|
8
|
+
const classes = [baseClass, line ? "" : `${baseClass}--bare`, className].filter(Boolean).join(" ");
|
|
9
|
+
return /* @__PURE__ */ jsx("div", { ...rest, ref, className: classes, role: "separator", children: /* @__PURE__ */ jsxs("span", { className: `${baseClass}__label`, children: [
|
|
10
|
+
icon && /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: typeof icon === "string" ? /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: icon }) : icon }),
|
|
11
|
+
children
|
|
12
|
+
] }) });
|
|
13
|
+
}
|
|
14
|
+
);
|
|
15
|
+
ChatMarker.displayName = "ChatMarker";
|
|
16
|
+
export {
|
|
17
|
+
ChatMarker
|
|
18
|
+
};
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
CHAT MESSAGE COMPONENT
|
|
3
|
+
A single chat turn: avatar, author, timestamp,
|
|
4
|
+
and bubble or plain content aligned by role.
|
|
5
|
+
|
|
6
|
+
Bubble surfaces come only from the four
|
|
7
|
+
--color-chat-bubble-* tokens — never the
|
|
8
|
+
container ramp directly — so re-theming a chat
|
|
9
|
+
means repointing those four and nothing else.
|
|
10
|
+
============================================ */
|
|
11
|
+
|
|
12
|
+
.ds-chat-message {
|
|
13
|
+
/* Gutter width matches the Avatar sm footprint, so a run of turns keeps
|
|
14
|
+
its left edge whether or not each row shows its avatar. Consumers can
|
|
15
|
+
repoint it for a larger avatar. */
|
|
16
|
+
--ds-chat-message-gutter: var(--icon-size-lg);
|
|
17
|
+
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: flex-start;
|
|
20
|
+
gap: var(--gap-sm-md);
|
|
21
|
+
width: 100%;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.ds-chat-message--user {
|
|
25
|
+
flex-direction: row-reverse;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* ============================================
|
|
29
|
+
GUTTER
|
|
30
|
+
============================================ */
|
|
31
|
+
|
|
32
|
+
.ds-chat-message__gutter {
|
|
33
|
+
flex: none;
|
|
34
|
+
display: flex;
|
|
35
|
+
justify-content: center;
|
|
36
|
+
width: var(--ds-chat-message-gutter);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Hidden, not removed — the reserved width is what keeps a run aligned */
|
|
40
|
+
.ds-chat-message__gutter--hidden {
|
|
41
|
+
visibility: hidden;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* ============================================
|
|
45
|
+
BODY
|
|
46
|
+
============================================ */
|
|
47
|
+
|
|
48
|
+
.ds-chat-message__body {
|
|
49
|
+
flex: 1;
|
|
50
|
+
min-width: 0;
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
align-items: flex-start;
|
|
54
|
+
gap: var(--gap-xs);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.ds-chat-message--user .ds-chat-message__body {
|
|
58
|
+
align-items: flex-end;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ============================================
|
|
62
|
+
META ROW
|
|
63
|
+
============================================ */
|
|
64
|
+
|
|
65
|
+
.ds-chat-message__meta {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: baseline;
|
|
68
|
+
gap: var(--gap-sm);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.ds-chat-message__author {
|
|
72
|
+
font-family: var(--font-paragraph-sm-em-family);
|
|
73
|
+
font-size: var(--font-paragraph-sm-em-size);
|
|
74
|
+
font-weight: var(--font-paragraph-sm-em-weight);
|
|
75
|
+
line-height: var(--font-paragraph-sm-em-line-height);
|
|
76
|
+
letter-spacing: var(--font-paragraph-sm-em-letter-spacing);
|
|
77
|
+
color: var(--color-text-secondary);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.ds-chat-message__timestamp {
|
|
81
|
+
font-family: var(--font-paragraph-sm-family);
|
|
82
|
+
font-size: var(--font-paragraph-sm-size);
|
|
83
|
+
font-weight: var(--font-paragraph-sm-weight);
|
|
84
|
+
line-height: var(--font-paragraph-sm-line-height);
|
|
85
|
+
letter-spacing: var(--font-paragraph-sm-letter-spacing);
|
|
86
|
+
font-variant-numeric: tabular-nums;
|
|
87
|
+
color: var(--color-text-tertiary);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ============================================
|
|
91
|
+
CONTENT
|
|
92
|
+
============================================ */
|
|
93
|
+
|
|
94
|
+
.ds-chat-message__content {
|
|
95
|
+
font-family: var(--font-paragraph-family);
|
|
96
|
+
font-size: var(--font-paragraph-size);
|
|
97
|
+
font-weight: var(--font-paragraph-weight);
|
|
98
|
+
line-height: var(--font-paragraph-line-height);
|
|
99
|
+
letter-spacing: var(--font-paragraph-letter-spacing);
|
|
100
|
+
/* Content never pushes the turn wider than its container — an unbroken
|
|
101
|
+
run (a URL, a token) wraps inside the bubble or column instead. */
|
|
102
|
+
min-width: 0;
|
|
103
|
+
max-width: 100%;
|
|
104
|
+
overflow-wrap: anywhere;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Plain — the assistant default: no surface, full width, primary text */
|
|
108
|
+
.ds-chat-message--plain .ds-chat-message__content {
|
|
109
|
+
width: 100%;
|
|
110
|
+
color: var(--color-text-primary);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Bubble — capped width so a short line never drags a surface across the
|
|
114
|
+
whole row; the fallback is the sanctioned consumer-override hook */
|
|
115
|
+
.ds-chat-message--bubble .ds-chat-message__content {
|
|
116
|
+
max-width: var(--ds-chat-message-max-width, 75%);
|
|
117
|
+
padding: var(--padding-sm-md) var(--padding-lg);
|
|
118
|
+
border-radius: var(--radius-xl);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Sent — the user's own turns */
|
|
122
|
+
.ds-chat-message--bubble.ds-chat-message--user .ds-chat-message__content {
|
|
123
|
+
background-color: var(--color-chat-bubble-sent-bg);
|
|
124
|
+
color: var(--color-chat-bubble-sent-text);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Received — an assistant turn opted into a bubble */
|
|
128
|
+
.ds-chat-message--bubble.ds-chat-message--assistant .ds-chat-message__content {
|
|
129
|
+
background-color: var(--color-chat-bubble-received-bg);
|
|
130
|
+
color: var(--color-chat-bubble-received-text);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Tail — the speaker-side bottom corner squares off to point at them */
|
|
134
|
+
.ds-chat-message--tail.ds-chat-message--bubble.ds-chat-message--user
|
|
135
|
+
.ds-chat-message__content {
|
|
136
|
+
border-bottom-right-radius: var(--radius-xs);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.ds-chat-message--tail.ds-chat-message--bubble.ds-chat-message--assistant
|
|
140
|
+
.ds-chat-message__content {
|
|
141
|
+
border-bottom-left-radius: var(--radius-xs);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ============================================
|
|
145
|
+
GROUPED
|
|
146
|
+
A follow-on row in the same speaker's run:
|
|
147
|
+
no meta, avatar hidden but gutter kept, and
|
|
148
|
+
the row pulls up towards the one above so the
|
|
149
|
+
run reads as one utterance. The negative
|
|
150
|
+
margin offsets part of the container's gap.
|
|
151
|
+
============================================ */
|
|
152
|
+
|
|
153
|
+
.ds-chat-message--grouped {
|
|
154
|
+
margin-top: calc(-1 * var(--gap-sm));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ============================================
|
|
158
|
+
COMPACT
|
|
159
|
+
============================================ */
|
|
160
|
+
|
|
161
|
+
.ds-chat-message--compact .ds-chat-message__content {
|
|
162
|
+
font-family: var(--font-paragraph-sm-family);
|
|
163
|
+
font-size: var(--font-paragraph-sm-size);
|
|
164
|
+
font-weight: var(--font-paragraph-sm-weight);
|
|
165
|
+
line-height: var(--font-paragraph-sm-line-height);
|
|
166
|
+
letter-spacing: var(--font-paragraph-sm-letter-spacing);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.ds-chat-message--compact.ds-chat-message--bubble .ds-chat-message__content {
|
|
170
|
+
padding: var(--padding-xs) var(--padding-sm-md);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* ============================================
|
|
174
|
+
PENDING
|
|
175
|
+
Three dots pulsing in sequence while the
|
|
176
|
+
first content is still on its way.
|
|
177
|
+
============================================ */
|
|
178
|
+
|
|
179
|
+
.ds-chat-message__pending {
|
|
180
|
+
--ds-chat-message-pending-cycle: var(--motion-duration-loop-matrix);
|
|
181
|
+
|
|
182
|
+
display: inline-flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.ds-chat-message__dots {
|
|
187
|
+
display: inline-flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
gap: var(--gap-xs);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.ds-chat-message__dot {
|
|
193
|
+
width: var(--gap-sm);
|
|
194
|
+
height: var(--gap-sm);
|
|
195
|
+
border-radius: var(--radius-full);
|
|
196
|
+
background-color: var(--color-text-tertiary);
|
|
197
|
+
opacity: 0.25;
|
|
198
|
+
animation: ds-chat-message-dot var(--ds-chat-message-pending-cycle)
|
|
199
|
+
var(--motion-ease-linear) infinite;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.ds-chat-message__dot:nth-child(2) {
|
|
203
|
+
animation-delay: calc(var(--ds-chat-message-pending-cycle) / 6);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.ds-chat-message__dot:nth-child(3) {
|
|
207
|
+
animation-delay: calc(var(--ds-chat-message-pending-cycle) / 3);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Each dot brightens and falls back within the first half of the cycle, so
|
|
211
|
+
the stagger reads as a wave rather than three independent blinkers */
|
|
212
|
+
@keyframes ds-chat-message-dot {
|
|
213
|
+
0%,
|
|
214
|
+
60%,
|
|
215
|
+
100% {
|
|
216
|
+
opacity: 0.25;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
30% {
|
|
220
|
+
opacity: 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* ============================================
|
|
225
|
+
ACTIONS
|
|
226
|
+
Hidden until the row is hovered or holds
|
|
227
|
+
keyboard focus; touch has no hover, so there
|
|
228
|
+
they stay visible.
|
|
229
|
+
|
|
230
|
+
Opacity only, never visibility: hidden would
|
|
231
|
+
drop the buttons from the tab order, so a
|
|
232
|
+
message with no other focusable child could
|
|
233
|
+
never match :focus-within and keyboard users
|
|
234
|
+
could never reach the actions. Invisible-but-
|
|
235
|
+
tabbable is the point — the first Tab into a
|
|
236
|
+
button reveals the row.
|
|
237
|
+
============================================ */
|
|
238
|
+
|
|
239
|
+
.ds-chat-message__actions {
|
|
240
|
+
display: flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
gap: var(--gap-xs);
|
|
243
|
+
/* The row hides with opacity, not display, so this offset is always
|
|
244
|
+
reserved — the body's own gap plus this margin sets the row a full
|
|
245
|
+
--gap-md off the content without loosening the meta stack above. */
|
|
246
|
+
margin-top: var(--gap-sm-md);
|
|
247
|
+
opacity: 0;
|
|
248
|
+
transition: opacity var(--motion-duration-fast) var(--motion-ease-standard);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.ds-chat-message:hover .ds-chat-message__actions,
|
|
252
|
+
.ds-chat-message:focus-within .ds-chat-message__actions {
|
|
253
|
+
opacity: 1;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@media (hover: none) {
|
|
257
|
+
.ds-chat-message__actions {
|
|
258
|
+
opacity: 1;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* ============================================
|
|
263
|
+
FOOTER
|
|
264
|
+
============================================ */
|
|
265
|
+
|
|
266
|
+
.ds-chat-message__footer {
|
|
267
|
+
display: flex;
|
|
268
|
+
align-items: center;
|
|
269
|
+
flex-wrap: wrap;
|
|
270
|
+
gap: var(--gap-xs);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* ============================================
|
|
274
|
+
VISUALLY HIDDEN
|
|
275
|
+
The pending label is for screen readers only.
|
|
276
|
+
============================================ */
|
|
277
|
+
|
|
278
|
+
.ds-chat-message__sr-only {
|
|
279
|
+
position: absolute;
|
|
280
|
+
width: 1px;
|
|
281
|
+
height: 1px;
|
|
282
|
+
padding: 0;
|
|
283
|
+
margin: -1px;
|
|
284
|
+
overflow: hidden;
|
|
285
|
+
clip: rect(0, 0, 0, 0);
|
|
286
|
+
white-space: nowrap;
|
|
287
|
+
border: 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* ============================================
|
|
291
|
+
REDUCED MOTION
|
|
292
|
+
The global guard collapses the loop to its
|
|
293
|
+
final frame, which is the dim phase — three
|
|
294
|
+
near-invisible dots. Park them at a legible
|
|
295
|
+
static opacity instead; the visually hidden
|
|
296
|
+
label still says what is happening.
|
|
297
|
+
============================================ */
|
|
298
|
+
|
|
299
|
+
@media (prefers-reduced-motion: reduce) {
|
|
300
|
+
.ds-chat-message__dot {
|
|
301
|
+
animation: none;
|
|
302
|
+
opacity: 0.55;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by ChatMessage itself — everything else falls through to the root element. */
|
|
3
|
+
type ChatMessageOwnProps = {
|
|
4
|
+
/**
|
|
5
|
+
* Which side of the conversation this turn belongs to. Drives alignment
|
|
6
|
+
* and the default surface: user turns are right-aligned bubbles,
|
|
7
|
+
* assistant turns are surface-less full-width text. Shadows the ARIA
|
|
8
|
+
* role attribute; the root renders no ARIA role.
|
|
9
|
+
*/
|
|
10
|
+
role?: 'user' | 'assistant';
|
|
11
|
+
/** Avatar slot, e.g. an `<Avatar>`. Omit for no gutter at all. */
|
|
12
|
+
avatar?: React.ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* Show the passed avatar. When false the avatar is hidden but its gutter
|
|
15
|
+
* space is kept, so consecutive rows in a run stay aligned.
|
|
16
|
+
*/
|
|
17
|
+
showAvatar?: boolean;
|
|
18
|
+
/** Display name shown above the content. */
|
|
19
|
+
author?: string;
|
|
20
|
+
/** Time shown beside the author, e.g. "2:41 PM". Free text, so callers keep their own formatting. */
|
|
21
|
+
timestamp?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Consecutive-message mode: hides the avatar (keeping its gutter), drops
|
|
24
|
+
* the author and timestamp, and tightens the spacing to the row above.
|
|
25
|
+
*/
|
|
26
|
+
grouped?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Override the role's default surface. Explicit true on an assistant turn
|
|
29
|
+
* renders a received bubble; explicit false on a user turn renders plain text.
|
|
30
|
+
*/
|
|
31
|
+
bubble?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Square the speaker-side bottom corner of the bubble — bottom-right on a
|
|
34
|
+
* sent bubble, bottom-left on a received one. Only meaningful when a
|
|
35
|
+
* bubble renders.
|
|
36
|
+
*/
|
|
37
|
+
tail?: boolean;
|
|
38
|
+
/** Compact drops the type one size step and tightens the bubble padding. */
|
|
39
|
+
size?: 'default' | 'compact';
|
|
40
|
+
/** Waiting for the first content: renders a three-dot pulse in place of children. */
|
|
41
|
+
pending?: boolean;
|
|
42
|
+
/** Accessible text announced for the pending state. */
|
|
43
|
+
pendingLabel?: string;
|
|
44
|
+
/** Action row under the content, revealed on hover and keyboard focus (always visible on touch). */
|
|
45
|
+
actions?: React.ReactNode;
|
|
46
|
+
/** Footer slot under the content — a sources row, an edited note. */
|
|
47
|
+
footer?: React.ReactNode;
|
|
48
|
+
/** Additional CSS classes */
|
|
49
|
+
className?: string;
|
|
50
|
+
/**
|
|
51
|
+
* The message content. The package ships no markdown renderer; render
|
|
52
|
+
* markdown yourself, ideally wrapped in Prose, and pass the result.
|
|
53
|
+
*/
|
|
54
|
+
children?: React.ReactNode;
|
|
55
|
+
};
|
|
56
|
+
export interface ChatMessageProps extends ChatMessageOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof ChatMessageOwnProps> {
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* ChatMessage is a single chat turn: avatar, author, timestamp, and the
|
|
60
|
+
* content itself, aligned by role. User turns read as right-aligned bubbles;
|
|
61
|
+
* assistant turns read as plain full-width text, so a transcript keeps the
|
|
62
|
+
* question-and-answer rhythm without every row wearing a surface.
|
|
63
|
+
*
|
|
64
|
+
* The bubble colours come exclusively from the four `--color-chat-bubble-*`
|
|
65
|
+
* tokens, so re-theming a chat means repointing those and nothing else.
|
|
66
|
+
*/
|
|
67
|
+
export declare const ChatMessage: React.ForwardRefExoticComponent<ChatMessageProps & React.RefAttributes<HTMLDivElement>>;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import "./ChatMessage.css";
|
|
4
|
+
const ChatMessage = React.forwardRef(
|
|
5
|
+
({
|
|
6
|
+
role = "assistant",
|
|
7
|
+
avatar,
|
|
8
|
+
showAvatar = true,
|
|
9
|
+
author,
|
|
10
|
+
timestamp,
|
|
11
|
+
grouped = false,
|
|
12
|
+
bubble,
|
|
13
|
+
tail = false,
|
|
14
|
+
size = "default",
|
|
15
|
+
pending = false,
|
|
16
|
+
pendingLabel = "Waiting for a reply",
|
|
17
|
+
actions,
|
|
18
|
+
footer,
|
|
19
|
+
className = "",
|
|
20
|
+
children,
|
|
21
|
+
...rest
|
|
22
|
+
}, ref) => {
|
|
23
|
+
const baseClass = "ds-chat-message";
|
|
24
|
+
const hasBubble = bubble ?? role === "user";
|
|
25
|
+
const hideAvatar = grouped || !showAvatar;
|
|
26
|
+
const classes = [
|
|
27
|
+
baseClass,
|
|
28
|
+
`${baseClass}--${role}`,
|
|
29
|
+
hasBubble ? `${baseClass}--bubble` : `${baseClass}--plain`,
|
|
30
|
+
tail ? `${baseClass}--tail` : "",
|
|
31
|
+
grouped ? `${baseClass}--grouped` : "",
|
|
32
|
+
size === "compact" ? `${baseClass}--compact` : "",
|
|
33
|
+
pending ? `${baseClass}--pending` : "",
|
|
34
|
+
className
|
|
35
|
+
].filter(Boolean).join(" ");
|
|
36
|
+
return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
|
|
37
|
+
avatar !== void 0 && /* @__PURE__ */ jsx(
|
|
38
|
+
"span",
|
|
39
|
+
{
|
|
40
|
+
className: [
|
|
41
|
+
`${baseClass}__gutter`,
|
|
42
|
+
hideAvatar ? `${baseClass}__gutter--hidden` : ""
|
|
43
|
+
].filter(Boolean).join(" "),
|
|
44
|
+
children: avatar
|
|
45
|
+
}
|
|
46
|
+
),
|
|
47
|
+
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__body`, children: [
|
|
48
|
+
!grouped && (author || timestamp) && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__meta`, children: [
|
|
49
|
+
author && /* @__PURE__ */ jsx("span", { className: `${baseClass}__author`, children: author }),
|
|
50
|
+
timestamp && /* @__PURE__ */ jsx("span", { className: `${baseClass}__timestamp`, children: timestamp })
|
|
51
|
+
] }),
|
|
52
|
+
/* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, children: pending ? /* @__PURE__ */ jsxs("span", { className: `${baseClass}__pending`, role: "status", children: [
|
|
53
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__sr-only`, children: pendingLabel }),
|
|
54
|
+
/* @__PURE__ */ jsxs("span", { className: `${baseClass}__dots`, "aria-hidden": "true", children: [
|
|
55
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__dot` }),
|
|
56
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__dot` }),
|
|
57
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__dot` })
|
|
58
|
+
] })
|
|
59
|
+
] }) : children }),
|
|
60
|
+
actions && /* @__PURE__ */ jsx("div", { className: `${baseClass}__actions`, children: actions }),
|
|
61
|
+
footer && /* @__PURE__ */ jsx("div", { className: `${baseClass}__footer`, children: footer })
|
|
62
|
+
] })
|
|
63
|
+
] });
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
ChatMessage.displayName = "ChatMessage";
|
|
67
|
+
export {
|
|
68
|
+
ChatMessage
|
|
69
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
CHAT THREAD COMPONENT
|
|
3
|
+
Scrollable conversation column: edge fades,
|
|
4
|
+
send-to-top anchoring, subtle scrollbar.
|
|
5
|
+
============================================ */
|
|
6
|
+
|
|
7
|
+
/* Base — the root is the scroll container. Inline gutters live on
|
|
8
|
+
__content, so the scrollbar rides the component's far edge. */
|
|
9
|
+
.ds-chat-thread {
|
|
10
|
+
position: relative;
|
|
11
|
+
overflow-y: auto;
|
|
12
|
+
min-height: 0;
|
|
13
|
+
/* The component owns anchoring; the browser's native scroll anchoring
|
|
14
|
+
otherwise latches onto the trailing spacer and drags the viewport
|
|
15
|
+
down while a response streams in. */
|
|
16
|
+
overflow-anchor: none;
|
|
17
|
+
/* Anchoring scrolls glide; the reduced-motion guard in tokens-motion.css
|
|
18
|
+
forces scroll-behavior back to auto. */
|
|
19
|
+
scroll-behavior: smooth;
|
|
20
|
+
|
|
21
|
+
/* Subtle scrollbar — invisible until the thread is actually scrolling.
|
|
22
|
+
The gutter is reserved permanently: on classic-scrollbar platforms a
|
|
23
|
+
scrollbar popping in (the first overflowing turn, or a rounding
|
|
24
|
+
flicker mid-animation) would otherwise reflow the content sideways. */
|
|
25
|
+
scrollbar-width: thin;
|
|
26
|
+
scrollbar-gutter: stable;
|
|
27
|
+
scrollbar-color: transparent transparent;
|
|
28
|
+
transition: scrollbar-color var(--motion-duration-base) var(--motion-ease-standard);
|
|
29
|
+
|
|
30
|
+
/* Edge fades — a CSS mask, not a scroll listener: content scrolling out
|
|
31
|
+
at either end passes through the fade.
|
|
32
|
+
ds-allow(color): mask alpha keywords (black/transparent), not palette colours */
|
|
33
|
+
-webkit-mask-image: linear-gradient(
|
|
34
|
+
to bottom,
|
|
35
|
+
transparent,
|
|
36
|
+
black var(--gap-lg),
|
|
37
|
+
black calc(100% - var(--gap-lg)),
|
|
38
|
+
transparent
|
|
39
|
+
);
|
|
40
|
+
mask-image: linear-gradient(
|
|
41
|
+
to bottom,
|
|
42
|
+
transparent,
|
|
43
|
+
black var(--gap-lg),
|
|
44
|
+
black calc(100% - var(--gap-lg)),
|
|
45
|
+
transparent
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* States */
|
|
50
|
+
.ds-chat-thread--scrolling {
|
|
51
|
+
scrollbar-color: var(--color-divider) transparent;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.ds-chat-thread:focus-visible {
|
|
55
|
+
outline: 2px solid var(--color-action-primary-bg);
|
|
56
|
+
outline-offset: -2px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Content — the turns. Consumers override the inline gutters (mobile
|
|
60
|
+
densities) through --ds-chat-thread-padding-inline, and can cap the
|
|
61
|
+
conversation column through --ds-chat-thread-content-max-width: the
|
|
62
|
+
column centres itself while the scroll container, its fades, and the
|
|
63
|
+
scrollbar keep spanning the full component width. */
|
|
64
|
+
.ds-chat-thread__content {
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
gap: var(--gap-xl);
|
|
68
|
+
max-width: var(--ds-chat-thread-content-max-width, none);
|
|
69
|
+
margin-inline: auto;
|
|
70
|
+
/* Roomy bottom padding: the end of a long response rests 40px clear of
|
|
71
|
+
the composer, so the last lines stay readable. The right padding
|
|
72
|
+
subtracts the measured scrollbar gutter (--ds-chat-thread-gutter, set
|
|
73
|
+
by the component) so the column stays symmetric and aligned with its
|
|
74
|
+
surroundings on classic-scrollbar platforms. */
|
|
75
|
+
padding: var(--padding-lg)
|
|
76
|
+
max(0px, calc(var(--ds-chat-thread-padding-inline, var(--padding-lg)) - var(--ds-chat-thread-gutter, 0px)))
|
|
77
|
+
var(--padding-xl) var(--ds-chat-thread-padding-inline, var(--padding-lg));
|
|
78
|
+
/* The first send into an empty thread floats up from composer level —
|
|
79
|
+
a transform glide, since no scroll distance exists yet. Later sends
|
|
80
|
+
glide via scroll instead; this transition only ever sees the
|
|
81
|
+
component-driven translateY. */
|
|
82
|
+
transition: transform var(--motion-duration-slower) var(--motion-ease-entrance);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* The list scrolls; entries never shrink. Without this, children that clip
|
|
86
|
+
their own overflow collapse when the column overflows. */
|
|
87
|
+
.ds-chat-thread__content > * {
|
|
88
|
+
flex-shrink: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Trailing spacer — sized by the component so the newest turn can always
|
|
92
|
+
reach the top of the viewport, and holds no more room than that: it
|
|
93
|
+
shrinks away as a response grows into the space it was reserving. */
|
|
94
|
+
.ds-chat-thread__spacer {
|
|
95
|
+
height: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Scroll-to-bottom — a sticky, zero-height slot pinned just above the
|
|
99
|
+
bottom fade, so the control floats centred over the transcript right
|
|
100
|
+
on top of the composer. */
|
|
101
|
+
.ds-chat-thread__jump-slot {
|
|
102
|
+
position: sticky;
|
|
103
|
+
bottom: var(--gap-lg);
|
|
104
|
+
height: 0;
|
|
105
|
+
display: flex;
|
|
106
|
+
justify-content: center;
|
|
107
|
+
/* Bottom-align against the zero-height line — the default stretch
|
|
108
|
+
would squash the circle flat. */
|
|
109
|
+
align-items: flex-end;
|
|
110
|
+
/* The slot spans the content box, which excludes the gutter; this
|
|
111
|
+
re-centres the control on the full component width. */
|
|
112
|
+
padding-left: var(--ds-chat-thread-gutter, 0px);
|
|
113
|
+
z-index: 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* The passive ramp is a ghost family meant to paint over a surface, so
|
|
117
|
+
the floating pill supplies its own: container surface + border below,
|
|
118
|
+
the passive layer above stepping through rest, hover, and active. */
|
|
119
|
+
.ds-chat-thread__jump {
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
justify-content: center;
|
|
123
|
+
/* The send button's exact footprint — CircularButton's default size,
|
|
124
|
+
a raw value there too (icon-button sizes are not on a token scale). */
|
|
125
|
+
width: 40px;
|
|
126
|
+
height: 40px;
|
|
127
|
+
padding: 0;
|
|
128
|
+
border: var(--border-xs) solid var(--color-bg-container-border);
|
|
129
|
+
border-radius: var(--radius-full);
|
|
130
|
+
background-color: var(--color-bg-container-primary);
|
|
131
|
+
background-image: linear-gradient(
|
|
132
|
+
var(--color-action-passive-bg),
|
|
133
|
+
var(--color-action-passive-bg)
|
|
134
|
+
);
|
|
135
|
+
color: var(--color-action-passive-text);
|
|
136
|
+
box-shadow: var(--shadow-floating);
|
|
137
|
+
cursor: pointer;
|
|
138
|
+
opacity: 0;
|
|
139
|
+
pointer-events: none;
|
|
140
|
+
transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.ds-chat-thread__jump--visible {
|
|
144
|
+
opacity: 1;
|
|
145
|
+
pointer-events: auto;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.ds-chat-thread__jump:hover {
|
|
149
|
+
background-image: linear-gradient(
|
|
150
|
+
var(--color-action-passive-bg-hover),
|
|
151
|
+
var(--color-action-passive-bg-hover)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.ds-chat-thread__jump:active {
|
|
156
|
+
background-image: linear-gradient(
|
|
157
|
+
var(--color-action-passive-bg-active),
|
|
158
|
+
var(--color-action-passive-bg-active)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.ds-chat-thread__jump:focus-visible {
|
|
163
|
+
outline: 2px solid var(--color-action-primary-bg);
|
|
164
|
+
outline-offset: 2px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* The glyph matches the send button's: --icon-size-md in a 40px circle
|
|
168
|
+
(icon plus the padding ring), pointing the other way. */
|
|
169
|
+
.ds-chat-thread__jump-icon {
|
|
170
|
+
--icon-size: var(--icon-size-md);
|
|
171
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by ChatThread itself — everything else falls through to the root element. */
|
|
3
|
+
type ChatThreadOwnProps = {
|
|
4
|
+
/**
|
|
5
|
+
* Scroll a newly appended turn to the top of the viewport, pushing the
|
|
6
|
+
* prior conversation upward. When a user turn and the agent's pending
|
|
7
|
+
* turn are appended in the same update, the first new child is the one
|
|
8
|
+
* anchored — the response streams in below it. Detection diffs the DOM
|
|
9
|
+
* child count, so replacing turns in place (a future regenerate or
|
|
10
|
+
* edit-last-turn) neither re-anchors nor resizes the spacer.
|
|
11
|
+
*/
|
|
12
|
+
anchor?: boolean;
|
|
13
|
+
/** Accessible name for the scrollable conversation region. */
|
|
14
|
+
ariaLabel?: string;
|
|
15
|
+
/** Accessible name for the scroll-to-bottom control. */
|
|
16
|
+
jumpLabel?: string;
|
|
17
|
+
/** Additional CSS classes */
|
|
18
|
+
className?: string;
|
|
19
|
+
/** The conversation, in order: ChatMessages, ChatMarkers. */
|
|
20
|
+
children?: React.ReactNode;
|
|
21
|
+
};
|
|
22
|
+
export interface ChatThreadProps extends ChatThreadOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof ChatThreadOwnProps> {
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* ChatThread is the scrollable conversation column: consistent gutters, a
|
|
26
|
+
* pure-CSS fade at either end so content scrolling out passes through it,
|
|
27
|
+
* and the send choreography — a newly appended turn floats to the top of
|
|
28
|
+
* the viewport while the prior conversation is pushed upward, leaving the
|
|
29
|
+
* response room to stream in below.
|
|
30
|
+
*
|
|
31
|
+
* A trailing spacer grows just enough that even a short newest exchange can
|
|
32
|
+
* reach the top, so the anchor position never depends on how much has been
|
|
33
|
+
* said. The scrollbar stays hidden until the thread is actually scrolling,
|
|
34
|
+
* and rides the component's far edge — the content gutters live on an inner
|
|
35
|
+
* wrapper, overridable via `--ds-chat-thread-padding-inline`.
|
|
36
|
+
*
|
|
37
|
+
* The smooth scroll respects reduced motion (the motion tokens' guard forces
|
|
38
|
+
* instant scrolling). Forwards a ref to the scrollable root element.
|
|
39
|
+
*/
|
|
40
|
+
export declare const ChatThread: React.ForwardRefExoticComponent<ChatThreadProps & React.RefAttributes<HTMLDivElement>>;
|
|
41
|
+
export {};
|