@robr0/design-system 0.4.0 → 0.6.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 +21 -10
- 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 +310 -0
- package/components/ChatMessage/ChatMessage.d.ts +74 -0
- package/components/ChatMessage/ChatMessage.js +71 -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 +163 -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/Timeline/Timeline.css +9 -1
- 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 +13 -0
- package/tokens/registry.json.d.ts +13 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +13 -0
- package/tokens/tokens-light.css +43 -0
- package/tokens/tokens-primitives.css +5 -0
- package/tokens/tokens-typography.css +24 -0
|
@@ -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 {};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useRef, useEffect, useState, useLayoutEffect } from "react";
|
|
3
|
+
import "./ChatThread.css";
|
|
4
|
+
import "../../fonts/material-symbols.css";
|
|
5
|
+
const SCROLLBAR_SETTLE_MS = 600;
|
|
6
|
+
const JUMP_THRESHOLD_PX = 24;
|
|
7
|
+
const ChatThread = React.forwardRef(
|
|
8
|
+
({
|
|
9
|
+
anchor = true,
|
|
10
|
+
ariaLabel = "Conversation",
|
|
11
|
+
jumpLabel = "Scroll to the latest message",
|
|
12
|
+
className = "",
|
|
13
|
+
children,
|
|
14
|
+
onScroll,
|
|
15
|
+
...rest
|
|
16
|
+
}, ref) => {
|
|
17
|
+
const baseClass = "ds-chat-thread";
|
|
18
|
+
const scrollRef = useRef(null);
|
|
19
|
+
const contentRef = useRef(null);
|
|
20
|
+
const spacerRef = useRef(null);
|
|
21
|
+
const prevCount = useRef(0);
|
|
22
|
+
const mounted = useRef(false);
|
|
23
|
+
const anchorTop = useRef(null);
|
|
24
|
+
const sizeSpacer = () => {
|
|
25
|
+
const container = scrollRef.current;
|
|
26
|
+
const content = contentRef.current;
|
|
27
|
+
const spacer = spacerRef.current;
|
|
28
|
+
const top = anchorTop.current;
|
|
29
|
+
if (!container || !content || !spacer || top == null) return;
|
|
30
|
+
const needed = Math.max(0, top + container.clientHeight - content.offsetHeight);
|
|
31
|
+
if (Math.abs(needed - spacer.offsetHeight) > 1) spacer.style.height = `${needed}px`;
|
|
32
|
+
};
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const container = scrollRef.current;
|
|
35
|
+
const content = contentRef.current;
|
|
36
|
+
if (!container || !content) return;
|
|
37
|
+
const syncGutter = () => {
|
|
38
|
+
const gutter = container.offsetWidth - container.clientWidth;
|
|
39
|
+
container.style.setProperty("--ds-chat-thread-gutter", `${gutter}px`);
|
|
40
|
+
};
|
|
41
|
+
const observer = new ResizeObserver(() => {
|
|
42
|
+
syncGutter();
|
|
43
|
+
const top = anchorTop.current;
|
|
44
|
+
const wasClamped = top != null && container.scrollTop < top && container.scrollTop >= container.scrollHeight - container.clientHeight - 1;
|
|
45
|
+
sizeSpacer();
|
|
46
|
+
if (top != null && wasClamped) container.scrollTo({ top, behavior: "instant" });
|
|
47
|
+
updateJump();
|
|
48
|
+
});
|
|
49
|
+
observer.observe(container);
|
|
50
|
+
observer.observe(content);
|
|
51
|
+
return () => observer.disconnect();
|
|
52
|
+
}, []);
|
|
53
|
+
const [scrolling, setScrolling] = useState(false);
|
|
54
|
+
const settleTimer = useRef(void 0);
|
|
55
|
+
const [canScrollDown, setCanScrollDown] = useState(false);
|
|
56
|
+
const updateJump = () => {
|
|
57
|
+
const container = scrollRef.current;
|
|
58
|
+
const content = contentRef.current;
|
|
59
|
+
if (!container || !content) return;
|
|
60
|
+
const remaining = content.offsetHeight - container.scrollTop - container.clientHeight;
|
|
61
|
+
const next = remaining > JUMP_THRESHOLD_PX;
|
|
62
|
+
setCanScrollDown((prev) => prev === next ? prev : next);
|
|
63
|
+
};
|
|
64
|
+
const setRef = (node) => {
|
|
65
|
+
scrollRef.current = node;
|
|
66
|
+
if (typeof ref === "function") ref(node);
|
|
67
|
+
else if (ref) ref.current = node;
|
|
68
|
+
};
|
|
69
|
+
useLayoutEffect(() => {
|
|
70
|
+
const container = scrollRef.current;
|
|
71
|
+
const content = contentRef.current;
|
|
72
|
+
const spacer = spacerRef.current;
|
|
73
|
+
if (!container || !content || !spacer) return;
|
|
74
|
+
const childCount = content.children.length;
|
|
75
|
+
const previous = prevCount.current;
|
|
76
|
+
const isFirstRun = !mounted.current;
|
|
77
|
+
mounted.current = true;
|
|
78
|
+
if (childCount === previous) return;
|
|
79
|
+
prevCount.current = childCount;
|
|
80
|
+
if (childCount < previous) {
|
|
81
|
+
anchorTop.current = null;
|
|
82
|
+
spacer.style.height = "0px";
|
|
83
|
+
container.scrollTo({ top: 0 });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (isFirstRun) {
|
|
87
|
+
if (childCount > 0 && anchor) {
|
|
88
|
+
const target2 = Math.max(0, content.offsetHeight - container.clientHeight);
|
|
89
|
+
anchorTop.current = target2;
|
|
90
|
+
sizeSpacer();
|
|
91
|
+
container.scrollTo({ top: target2, behavior: "instant" });
|
|
92
|
+
updateJump();
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!anchor) return;
|
|
97
|
+
const contentStyle = getComputedStyle(content);
|
|
98
|
+
const paddingTop = parseFloat(contentStyle.paddingTop) || 0;
|
|
99
|
+
if (previous === 0) {
|
|
100
|
+
const target2 = content.children[0];
|
|
101
|
+
if (!target2) return;
|
|
102
|
+
anchorTop.current = 0;
|
|
103
|
+
sizeSpacer();
|
|
104
|
+
const paddingBottom = parseFloat(contentStyle.paddingBottom) || 0;
|
|
105
|
+
const rise = Math.max(
|
|
106
|
+
0,
|
|
107
|
+
container.clientHeight - target2.offsetHeight - paddingTop - paddingBottom
|
|
108
|
+
);
|
|
109
|
+
content.style.transition = "none";
|
|
110
|
+
content.style.transform = `translateY(${rise}px)`;
|
|
111
|
+
void content.offsetHeight;
|
|
112
|
+
content.style.transition = "";
|
|
113
|
+
content.style.transform = "";
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const target = content.children[previous];
|
|
117
|
+
if (!target) return;
|
|
118
|
+
const targetTop = target.offsetTop - paddingTop;
|
|
119
|
+
anchorTop.current = targetTop;
|
|
120
|
+
sizeSpacer();
|
|
121
|
+
container.scrollTo({ top: targetTop });
|
|
122
|
+
});
|
|
123
|
+
useEffect(updateJump);
|
|
124
|
+
const handleScroll = (e) => {
|
|
125
|
+
onScroll?.(e);
|
|
126
|
+
updateJump();
|
|
127
|
+
setScrolling(true);
|
|
128
|
+
window.clearTimeout(settleTimer.current);
|
|
129
|
+
settleTimer.current = window.setTimeout(
|
|
130
|
+
() => setScrolling(false),
|
|
131
|
+
SCROLLBAR_SETTLE_MS
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
const jumpToBottom = () => {
|
|
135
|
+
const container = scrollRef.current;
|
|
136
|
+
const content = contentRef.current;
|
|
137
|
+
if (!container || !content) return;
|
|
138
|
+
container.scrollTo({ top: content.offsetHeight - container.clientHeight });
|
|
139
|
+
};
|
|
140
|
+
useEffect(() => () => window.clearTimeout(settleTimer.current), []);
|
|
141
|
+
const classes = [baseClass, scrolling ? `${baseClass}--scrolling` : "", className].filter(Boolean).join(" ");
|
|
142
|
+
return /* @__PURE__ */ jsxs(
|
|
143
|
+
"div",
|
|
144
|
+
{
|
|
145
|
+
...rest,
|
|
146
|
+
ref: setRef,
|
|
147
|
+
className: classes,
|
|
148
|
+
role: "region",
|
|
149
|
+
"aria-label": ariaLabel,
|
|
150
|
+
tabIndex: 0,
|
|
151
|
+
onScroll: handleScroll,
|
|
152
|
+
children: [
|
|
153
|
+
/* @__PURE__ */ jsx("div", { ref: contentRef, className: `${baseClass}__content`, children }),
|
|
154
|
+
/* @__PURE__ */ jsx("div", { ref: spacerRef, className: `${baseClass}__spacer`, "aria-hidden": "true" }),
|
|
155
|
+
/* @__PURE__ */ jsx("div", { className: `${baseClass}__jump-slot`, "aria-hidden": !canScrollDown, children: /* @__PURE__ */ jsx(
|
|
156
|
+
"button",
|
|
157
|
+
{
|
|
158
|
+
type: "button",
|
|
159
|
+
className: `${baseClass}__jump${canScrollDown ? ` ${baseClass}__jump--visible` : ""}`,
|
|
160
|
+
tabIndex: canScrollDown ? 0 : -1,
|
|
161
|
+
"aria-label": jumpLabel,
|
|
162
|
+
onClick: jumpToBottom,
|
|
163
|
+
children: /* @__PURE__ */ jsx("span", { className: `${baseClass}__jump-icon material-symbols-rounded`, "aria-hidden": "true", children: "arrow_downward" })
|
|
164
|
+
}
|
|
165
|
+
) })
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
ChatThread.displayName = "ChatThread";
|
|
172
|
+
export {
|
|
173
|
+
ChatThread
|
|
174
|
+
};
|
package/components/Chip/Chip.css
CHANGED
|
@@ -32,6 +32,33 @@
|
|
|
32
32
|
color var(--motion-duration-base) var(--motion-ease-standard), opacity var(--motion-duration-base) var(--motion-ease-standard);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/* ============================================
|
|
36
|
+
SIZE — LARGE (40px tall)
|
|
37
|
+
The default Button's footprint and the body
|
|
38
|
+
paragraph scale, so a chip used as a primary
|
|
39
|
+
tap target reads at the same weight as the
|
|
40
|
+
text around it.
|
|
41
|
+
============================================ */
|
|
42
|
+
|
|
43
|
+
.ds-chip--large {
|
|
44
|
+
padding: var(--padding-sm) var(--padding-lg); /* 8px 20px */
|
|
45
|
+
font-family: var(--font-paragraph-em-family);
|
|
46
|
+
font-size: var(--font-paragraph-em-size);
|
|
47
|
+
font-weight: var(--font-paragraph-em-weight);
|
|
48
|
+
line-height: var(--font-paragraph-em-line-height);
|
|
49
|
+
letter-spacing: var(--font-paragraph-em-letter-spacing);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.ds-chip--large .ds-chip__icon,
|
|
53
|
+
.ds-chip--large .ds-chip__remove .material-symbols-rounded {
|
|
54
|
+
--icon-size: var(--icon-size-md);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.ds-chip--large .ds-chip__icon svg {
|
|
58
|
+
width: 24px;
|
|
59
|
+
height: 24px;
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
/* ============================================
|
|
36
63
|
SIZE — COMPACT (24px tall)
|
|
37
64
|
============================================ */
|
|
@@ -8,8 +8,8 @@ export interface ChipProps {
|
|
|
8
8
|
selected?: boolean;
|
|
9
9
|
/** Disabled state */
|
|
10
10
|
disabled?: boolean;
|
|
11
|
-
/** Chip size */
|
|
12
|
-
size?: 'default' | 'compact';
|
|
11
|
+
/** Chip size — `large` matches the default Button and paragraph body scale, for pills that are a primary tap target rather than metadata */
|
|
12
|
+
size?: 'large' | 'default' | 'compact';
|
|
13
13
|
/** Click handler — presence makes the chip an interactive <button> */
|
|
14
14
|
onClick?: () => void;
|
|
15
15
|
/** Remove handler — renders a trailing close button (input-style chips) */
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by CircularButton itself — everything else falls through to the DOM node. */
|
|
3
|
+
type CircularButtonOwnProps = {
|
|
2
4
|
/** Material Symbol icon name */
|
|
3
5
|
icon: string;
|
|
4
6
|
/** Visual treatment */
|
|
@@ -9,7 +11,18 @@ export interface CircularButtonProps {
|
|
|
9
11
|
* @deprecated Use `variant` instead.
|
|
10
12
|
*/
|
|
11
13
|
priority?: 'primary' | 'secondary' | 'tertiary';
|
|
12
|
-
/**
|
|
14
|
+
/** Whether the button is disabled */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Documentation-only interaction state.
|
|
18
|
+
*
|
|
19
|
+
* @deprecated Use `disabled` for the disabled state.
|
|
20
|
+
*
|
|
21
|
+
* Documentation-only affordance for rendering a *static* interaction state in
|
|
22
|
+
* Storybook and the showcase site. Real hover/active styling comes from CSS
|
|
23
|
+
* pseudo-classes and needs no prop — for docs, prefer
|
|
24
|
+
* `className="ds-circular-button--hover"`.
|
|
25
|
+
*/
|
|
13
26
|
state?: 'default' | 'hover' | 'active' | 'disabled';
|
|
14
27
|
/** Button size */
|
|
15
28
|
size?: 'default' | 'compact';
|
|
@@ -19,24 +32,26 @@ export interface CircularButtonProps {
|
|
|
19
32
|
* the disabled state) and sets `aria-busy` on the rendered element.
|
|
20
33
|
*/
|
|
21
34
|
loading?: boolean;
|
|
22
|
-
/** Accessible label */
|
|
35
|
+
/** Accessible label — required, because the button has no visible text */
|
|
23
36
|
ariaLabel: string;
|
|
24
|
-
/** Optional click handler */
|
|
25
|
-
onClick?: () => void;
|
|
26
37
|
/** Optional href — renders as <a> instead of <button> */
|
|
27
38
|
href?: string;
|
|
28
39
|
/** Optional target attribute for links */
|
|
29
40
|
target?: string;
|
|
30
41
|
/** Optional rel attribute for links */
|
|
31
42
|
rel?: string;
|
|
32
|
-
/** Id of an element describing this button — set automatically by Tooltip */
|
|
33
|
-
'aria-describedby'?: string;
|
|
34
43
|
/** Additional CSS classes */
|
|
35
44
|
className?: string;
|
|
45
|
+
};
|
|
46
|
+
export interface CircularButtonProps extends CircularButtonOwnProps, Omit<React.ComponentPropsWithoutRef<'button'>, keyof CircularButtonOwnProps | 'type'> {
|
|
36
47
|
}
|
|
37
48
|
/**
|
|
38
49
|
* Circular icon button component.
|
|
39
50
|
* A round button containing a single icon, available in
|
|
40
51
|
* primary, secondary and tertiary variants with default and compact sizes.
|
|
52
|
+
*
|
|
53
|
+
* Renders a `<button>`, or an `<a>` when `href` is supplied. Forwards a ref to
|
|
54
|
+
* whichever element it renders, and spreads unrecognised props onto it.
|
|
41
55
|
*/
|
|
42
|
-
export declare const CircularButton:
|
|
56
|
+
export declare const CircularButton: React.ForwardRefExoticComponent<CircularButtonProps & React.RefAttributes<HTMLButtonElement | HTMLAnchorElement>>;
|
|
57
|
+
export {};
|
|
@@ -1,67 +1,69 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
2
3
|
import { Spinner } from "../Spinner/Spinner.js";
|
|
3
4
|
import "./CircularButton.css";
|
|
4
5
|
import "../../fonts/material-symbols.css";
|
|
5
|
-
const CircularButton = (
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
6
|
+
const CircularButton = React.forwardRef(
|
|
7
|
+
({
|
|
8
|
+
icon,
|
|
9
|
+
variant,
|
|
10
|
+
priority,
|
|
11
|
+
disabled,
|
|
12
|
+
state,
|
|
13
|
+
size = "default",
|
|
14
|
+
loading,
|
|
15
|
+
ariaLabel,
|
|
16
|
+
href,
|
|
17
|
+
target,
|
|
18
|
+
rel,
|
|
19
|
+
className = "",
|
|
20
|
+
...rest
|
|
21
|
+
}, ref) => {
|
|
22
|
+
const baseClass = "ds-circular-button";
|
|
23
|
+
const resolvedVariant = variant ?? priority ?? "primary";
|
|
24
|
+
const isDisabled = disabled ?? state === "disabled";
|
|
25
|
+
const resolvedState = state ?? (isDisabled ? "disabled" : "default");
|
|
26
|
+
const isLoading = Boolean(loading);
|
|
27
|
+
const classes = [
|
|
28
|
+
baseClass,
|
|
29
|
+
`${baseClass}--${resolvedVariant}`,
|
|
30
|
+
`${baseClass}--${resolvedState}`,
|
|
31
|
+
`${baseClass}--${size}`,
|
|
32
|
+
isLoading && `${baseClass}--loading`,
|
|
33
|
+
className
|
|
34
|
+
].filter(Boolean).join(" ");
|
|
35
|
+
const children = isLoading ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: /* @__PURE__ */ jsx(Spinner, { size: size === "compact" ? "sm" : "md", variant: "inherit" }) }) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, "aria-hidden": "true", children: icon });
|
|
36
|
+
if (href && !isDisabled && !isLoading) {
|
|
37
|
+
return /* @__PURE__ */ jsx(
|
|
38
|
+
"a",
|
|
39
|
+
{
|
|
40
|
+
...rest,
|
|
41
|
+
ref,
|
|
42
|
+
className: classes,
|
|
43
|
+
href,
|
|
44
|
+
target,
|
|
45
|
+
rel,
|
|
46
|
+
"aria-label": ariaLabel,
|
|
47
|
+
children
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|
|
37
51
|
return /* @__PURE__ */ jsx(
|
|
38
|
-
"
|
|
52
|
+
"button",
|
|
39
53
|
{
|
|
54
|
+
...rest,
|
|
55
|
+
ref,
|
|
56
|
+
type: "button",
|
|
40
57
|
className: classes,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
rel,
|
|
44
|
-
onClick,
|
|
58
|
+
disabled: isDisabled || isLoading,
|
|
59
|
+
"aria-busy": isLoading || void 0,
|
|
45
60
|
"aria-label": ariaLabel,
|
|
46
|
-
"aria-describedby": ariaDescribedby,
|
|
47
61
|
children
|
|
48
62
|
}
|
|
49
63
|
);
|
|
50
64
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{
|
|
54
|
-
type: "button",
|
|
55
|
-
className: classes,
|
|
56
|
-
onClick,
|
|
57
|
-
disabled: isDisabled || isLoading,
|
|
58
|
-
"aria-busy": isLoading || void 0,
|
|
59
|
-
"aria-label": ariaLabel,
|
|
60
|
-
"aria-describedby": ariaDescribedby,
|
|
61
|
-
children
|
|
62
|
-
}
|
|
63
|
-
);
|
|
64
|
-
};
|
|
65
|
+
);
|
|
66
|
+
CircularButton.displayName = "CircularButton";
|
|
65
67
|
export {
|
|
66
68
|
CircularButton
|
|
67
69
|
};
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
.ds-code-block__filename {
|
|
83
|
-
font-family:
|
|
83
|
+
font-family: var(--font-family-code);
|
|
84
84
|
font-size: var(--font-paragraph-sm-size);
|
|
85
85
|
line-height: var(--font-paragraph-sm-line-height);
|
|
86
86
|
color: var(--color-text-secondary);
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
.ds-code-block__code {
|
|
175
|
-
font-family:
|
|
175
|
+
font-family: var(--font-family-code);
|
|
176
176
|
font-size: var(--font-paragraph-sm-size);
|
|
177
177
|
line-height: var(--font-paragraph-sm-line-height);
|
|
178
178
|
color: var(--color-text-primary);
|