@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
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ The **live site** is the portfolio built on the design system. **Storybook** is
|
|
|
18
18
|
| Part | Description |
|
|
19
19
|
|---|---|
|
|
20
20
|
| **Portfolio website** (`/website`) | Next.js app with case studies, work history, writing, and about pages, all built exclusively with the design system components below. **[Live site →](https://robertritacca.com/)** |
|
|
21
|
-
| **Design system** (`/src`) | <!-- component-count -->
|
|
21
|
+
| **Design system** (`/src`) | <!-- component-count -->78<!-- /component-count --> React components, a three-tier token architecture, dark mode, and a full documentation site. Built to production standards. **[Storybook →](https://design-system-iota-one.vercel.app/?path=/docs/robr0-ds--docs)** |
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
@@ -27,7 +27,7 @@ The **live site** is the portfolio built on the design system. **Storybook** is
|
|
|
27
27
|
### Components
|
|
28
28
|
|
|
29
29
|
<!-- component-list:start -->
|
|
30
|
-
Accordion · Agent status · Alert · Alert dialog · App layout · App sidebar · Avatar · Badge · Breadcrumb · Button · Button group · Card · Carousel · Chart · Checkbox · Chip · Circular button · Code block · Colour picker · Combobox · Command palette · Contact card · Context menu · Contribution graph · Date input · Date picker · Dialog · Divider · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Field · Figure · File input · Input · Instructions · Kbd · Link list · Navigation · Pagination · Popover · Progress bar · Quote · Radio button · Reasoning · Section title · Segmented control · Selection card · Skeleton · Slider · Spinner · Stat · Swatch · Table · Tabs · Textarea · Timeline · Toast · Toggle group · Toggle switch · Tool call · Tooltip
|
|
30
|
+
Accordion · Agent status · AI button · Alert · Alert dialog · App layout · App sidebar · Avatar · Badge · Breadcrumb · Button · Button group · Card · Carousel · Chart · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Colour picker · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Field · Figure · File input · Input · Instructions · Interrupt card · Kbd · Link list · Message actions · Message card · Navigation · Nav list · Pagination · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radio button · Reasoning · Section title · Segmented control · Selection card · Skeleton · Slider · Source chip · Spinner · Stat · Swatch · Table · Tabs · Textarea · Timeline · Toast · Toggle group · Toggle switch · Tool call · Tooltip
|
|
31
31
|
<!-- component-list:end -->
|
|
32
32
|
|
|
33
33
|
### Using the package
|
|
@@ -22,20 +22,22 @@
|
|
|
22
22
|
.ds-agent-status {
|
|
23
23
|
--ds-agent-color: var(--color-text-secondary);
|
|
24
24
|
--ds-agent-label-color: var(--color-text-secondary);
|
|
25
|
+
--ds-agent-shimmer-base: var(--color-text-primary);
|
|
26
|
+
--ds-agent-shimmer-sweep: var(--color-text-tertiary);
|
|
25
27
|
--ds-agent-dot-dim: 0.2;
|
|
26
28
|
--ds-agent-dot-rest: 0.82;
|
|
27
29
|
--ds-agent-cycle: var(--motion-duration-loop-matrix);
|
|
28
30
|
--ds-agent-slot: calc(var(--ds-agent-cycle) / 12);
|
|
29
|
-
--ds-agent-matrix-size: var(--icon-size-
|
|
31
|
+
--ds-agent-matrix-size: var(--icon-size-md);
|
|
30
32
|
|
|
31
33
|
display: inline-flex;
|
|
32
34
|
align-items: center;
|
|
33
|
-
gap: var(--gap-sm);
|
|
34
|
-
font-family: var(--font-paragraph-
|
|
35
|
-
font-size: var(--font-paragraph-
|
|
36
|
-
font-weight: var(--font-paragraph-
|
|
37
|
-
line-height: var(--font-paragraph-
|
|
38
|
-
letter-spacing: var(--font-paragraph-
|
|
35
|
+
gap: var(--gap-sm-md);
|
|
36
|
+
font-family: var(--font-paragraph-family);
|
|
37
|
+
font-size: var(--font-paragraph-size);
|
|
38
|
+
font-weight: var(--font-paragraph-weight);
|
|
39
|
+
line-height: var(--font-paragraph-line-height);
|
|
40
|
+
letter-spacing: var(--font-paragraph-letter-spacing);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/* ============================================
|
|
@@ -63,20 +65,60 @@
|
|
|
63
65
|
============================================ */
|
|
64
66
|
|
|
65
67
|
.ds-agent-status__label {
|
|
68
|
+
position: relative;
|
|
66
69
|
color: var(--ds-agent-label-color);
|
|
67
70
|
min-width: 0;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
/*
|
|
71
|
-
the
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
/* Label crossfade: when the label string changes, the outgoing copy sits
|
|
74
|
+
absolutely over the incoming one and fades up and out while the new one
|
|
75
|
+
fades up and in — one continuous upward motion. */
|
|
76
|
+
.ds-agent-status__label-text {
|
|
77
|
+
display: inline-block;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* The animations live only on the modifier classes, and the modifiers come
|
|
81
|
+
off once the swap settles: a filled animation can pin the span to its
|
|
82
|
+
own compositor layer, which would escape the shimmer's text clip and
|
|
83
|
+
leave the label invisible for good. */
|
|
84
|
+
.ds-agent-status__label-text--in {
|
|
85
|
+
animation: ds-agent-label-in var(--motion-duration-slow) var(--motion-ease-entrance) both;
|
|
86
|
+
animation-delay: calc(var(--motion-duration-slow) / 2);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.ds-agent-status__label-text--out {
|
|
90
|
+
position: absolute;
|
|
91
|
+
left: 0;
|
|
92
|
+
top: 0;
|
|
93
|
+
white-space: nowrap;
|
|
94
|
+
animation: ds-agent-label-out var(--motion-duration-slow) var(--motion-ease-standard) both;
|
|
95
|
+
animation-delay: 0s;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@keyframes ds-agent-label-in {
|
|
99
|
+
from {
|
|
100
|
+
opacity: 0;
|
|
101
|
+
transform: translateY(8px);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@keyframes ds-agent-label-out {
|
|
106
|
+
to {
|
|
107
|
+
opacity: 0;
|
|
108
|
+
transform: translateY(-8px);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* A gradient between the shimmer base and sweep colours, moving left to
|
|
113
|
+
right: with an oversized background image, background-position 100% shows
|
|
114
|
+
its right edge, 0% its left, so counting down walks the sweep band across
|
|
115
|
+
the text. */
|
|
74
116
|
.ds-agent-status__label--shimmer {
|
|
75
117
|
background-image: linear-gradient(
|
|
76
118
|
100deg,
|
|
77
|
-
var(--ds-agent-
|
|
78
|
-
var(--
|
|
79
|
-
var(--ds-agent-
|
|
119
|
+
var(--ds-agent-shimmer-base) 35%,
|
|
120
|
+
var(--ds-agent-shimmer-sweep) 50%,
|
|
121
|
+
var(--ds-agent-shimmer-base) 65%
|
|
80
122
|
);
|
|
81
123
|
background-size: 250% 100%;
|
|
82
124
|
background-clip: text;
|
|
@@ -86,6 +128,16 @@
|
|
|
86
128
|
var(--motion-ease-linear) infinite;
|
|
87
129
|
}
|
|
88
130
|
|
|
131
|
+
/* While a label crossfade runs, the shimmer's text clip stands down: an
|
|
132
|
+
animated descendant composites on its own layer, which escapes
|
|
133
|
+
background-clip: text and renders the text invisible. The swap motion
|
|
134
|
+
covers the pause; the sweep resumes on the settled text. */
|
|
135
|
+
.ds-agent-status__label--shimmer.ds-agent-status__label--swapping {
|
|
136
|
+
background-image: none;
|
|
137
|
+
animation: none;
|
|
138
|
+
color: var(--ds-agent-label-color);
|
|
139
|
+
}
|
|
140
|
+
|
|
89
141
|
/* ============================================
|
|
90
142
|
STATES
|
|
91
143
|
The three working states are deliberately
|
|
@@ -102,6 +154,11 @@
|
|
|
102
154
|
.ds-agent-status--waiting {
|
|
103
155
|
--ds-agent-color: var(--color-status-warning-text);
|
|
104
156
|
--ds-agent-label-color: var(--color-status-warning-text);
|
|
157
|
+
|
|
158
|
+
/* Waiting keeps its warning colour through the shimmer — colour is the
|
|
159
|
+
signal, so the sweep only brightens it rather than going monochrome. */
|
|
160
|
+
--ds-agent-shimmer-base: var(--color-status-warning-text);
|
|
161
|
+
--ds-agent-shimmer-sweep: var(--color-text-primary);
|
|
105
162
|
}
|
|
106
163
|
|
|
107
164
|
.ds-agent-status--done {
|
|
@@ -123,17 +180,20 @@
|
|
|
123
180
|
|
|
124
181
|
/* ============================================
|
|
125
182
|
SIZES
|
|
183
|
+
The scale pairs with ChatMessage: default
|
|
184
|
+
matches default message text, compact
|
|
185
|
+
matches compact message text.
|
|
126
186
|
============================================ */
|
|
127
187
|
|
|
128
|
-
.ds-agent-status--
|
|
129
|
-
--ds-agent-matrix-size: var(--icon-size-
|
|
188
|
+
.ds-agent-status--compact {
|
|
189
|
+
--ds-agent-matrix-size: var(--icon-size-sm);
|
|
130
190
|
|
|
131
|
-
gap: var(--gap-sm
|
|
132
|
-
font-family: var(--font-paragraph-family);
|
|
133
|
-
font-size: var(--font-paragraph-size);
|
|
134
|
-
font-weight: var(--font-paragraph-weight);
|
|
135
|
-
line-height: var(--font-paragraph-line-height);
|
|
136
|
-
letter-spacing: var(--font-paragraph-letter-spacing);
|
|
191
|
+
gap: var(--gap-sm);
|
|
192
|
+
font-family: var(--font-paragraph-sm-family);
|
|
193
|
+
font-size: var(--font-paragraph-sm-size);
|
|
194
|
+
font-weight: var(--font-paragraph-sm-weight);
|
|
195
|
+
line-height: var(--font-paragraph-sm-line-height);
|
|
196
|
+
letter-spacing: var(--font-paragraph-sm-letter-spacing);
|
|
137
197
|
}
|
|
138
198
|
|
|
139
199
|
/* ============================================
|
|
@@ -13,8 +13,12 @@ type AgentStatusOwnProps = {
|
|
|
13
13
|
* on for `thinking`, `working` and `waiting`, off for the resting states.
|
|
14
14
|
*/
|
|
15
15
|
shimmer?: boolean;
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Indicator and text scale, paired with ChatMessage's sizes: `default`
|
|
18
|
+
* matches default message text, `compact` matches compact message text.
|
|
19
|
+
* `sm` and `md` are legacy aliases for `compact` and `default`.
|
|
20
|
+
*/
|
|
21
|
+
size?: 'default' | 'compact' | 'sm' | 'md';
|
|
18
22
|
/** `inline` sits in a line of content; `bar` is a full-width row for the top of a panel. */
|
|
19
23
|
variant?: 'inline' | 'bar';
|
|
20
24
|
/** Additional CSS classes */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import React from "react";
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useRef, useEffect } from "react";
|
|
3
3
|
import "./AgentStatus.css";
|
|
4
4
|
const DOT_COUNT = 12;
|
|
5
5
|
const DEFAULT_LABELS = {
|
|
@@ -17,7 +17,7 @@ const AgentStatus = React.forwardRef(
|
|
|
17
17
|
label,
|
|
18
18
|
pattern = "orbit",
|
|
19
19
|
shimmer,
|
|
20
|
-
size = "
|
|
20
|
+
size = "default",
|
|
21
21
|
variant = "inline",
|
|
22
22
|
className = "",
|
|
23
23
|
children,
|
|
@@ -26,11 +26,23 @@ const AgentStatus = React.forwardRef(
|
|
|
26
26
|
const baseClass = "ds-agent-status";
|
|
27
27
|
const isActive = ACTIVE_STATES.includes(state);
|
|
28
28
|
const isShimmering = shimmer ?? isActive;
|
|
29
|
+
const resolvedSize = size === "sm" ? "compact" : size === "md" ? "default" : size;
|
|
30
|
+
const resolvedLabel = label ?? DEFAULT_LABELS[state];
|
|
31
|
+
const [outgoing, setOutgoing] = useState(null);
|
|
32
|
+
const lastLabel = useRef(resolvedLabel);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (children != null) return;
|
|
35
|
+
if (lastLabel.current !== resolvedLabel) {
|
|
36
|
+
setOutgoing(lastLabel.current);
|
|
37
|
+
lastLabel.current = resolvedLabel;
|
|
38
|
+
}
|
|
39
|
+
}, [resolvedLabel, children]);
|
|
40
|
+
const isSwapping = outgoing !== null;
|
|
29
41
|
const classes = [
|
|
30
42
|
baseClass,
|
|
31
43
|
`${baseClass}--${state}`,
|
|
32
44
|
`${baseClass}--${pattern}`,
|
|
33
|
-
`${baseClass}--${
|
|
45
|
+
`${baseClass}--${resolvedSize}`,
|
|
34
46
|
`${baseClass}--${variant}`,
|
|
35
47
|
isActive ? `${baseClass}--animated` : "",
|
|
36
48
|
className
|
|
@@ -40,8 +52,28 @@ const AgentStatus = React.forwardRef(
|
|
|
40
52
|
/* @__PURE__ */ jsx(
|
|
41
53
|
"span",
|
|
42
54
|
{
|
|
43
|
-
className: `${baseClass}__label${isShimmering ? ` ${baseClass}__label--shimmer` : ""}`,
|
|
44
|
-
children: children ??
|
|
55
|
+
className: `${baseClass}__label${isShimmering ? ` ${baseClass}__label--shimmer` : ""}${isSwapping ? ` ${baseClass}__label--swapping` : ""}`,
|
|
56
|
+
children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
57
|
+
isSwapping && /* @__PURE__ */ jsx(
|
|
58
|
+
"span",
|
|
59
|
+
{
|
|
60
|
+
className: `${baseClass}__label-text ${baseClass}__label-text--out`,
|
|
61
|
+
"aria-hidden": "true",
|
|
62
|
+
children: outgoing
|
|
63
|
+
}
|
|
64
|
+
),
|
|
65
|
+
/* @__PURE__ */ jsx(
|
|
66
|
+
"span",
|
|
67
|
+
{
|
|
68
|
+
className: `${baseClass}__label-text${isSwapping ? ` ${baseClass}__label-text--in` : ""}`,
|
|
69
|
+
onAnimationEnd: (e) => {
|
|
70
|
+
if (e.target === e.currentTarget) setOutgoing(null);
|
|
71
|
+
},
|
|
72
|
+
children: resolvedLabel
|
|
73
|
+
},
|
|
74
|
+
resolvedLabel
|
|
75
|
+
)
|
|
76
|
+
] })
|
|
45
77
|
}
|
|
46
78
|
)
|
|
47
79
|
] });
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
AI BUTTON
|
|
3
|
+
The AI entry point: transparent field, slowly
|
|
4
|
+
rotating gradient ring, soft glow of the same
|
|
5
|
+
gradient. The ring is the system's "a model
|
|
6
|
+
answers here" signal — ordinary actions keep
|
|
7
|
+
the flat action teal.
|
|
8
|
+
============================================ */
|
|
9
|
+
|
|
10
|
+
/* The rotation animates a registered custom property: a conic gradient's
|
|
11
|
+
angle cannot tween otherwise. Browsers without @property render the ring
|
|
12
|
+
at its initial angle, static — a correct, quieter fallback. */
|
|
13
|
+
@property --ds-ai-button-angle {
|
|
14
|
+
syntax: '<angle>';
|
|
15
|
+
initial-value: 0deg;
|
|
16
|
+
inherits: false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Base */
|
|
20
|
+
.ds-ai-button {
|
|
21
|
+
/* Layout */
|
|
22
|
+
position: relative;
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
gap: var(--gap-sm);
|
|
27
|
+
|
|
28
|
+
/* Spacing — matches Button so the two sit on one control scale */
|
|
29
|
+
padding: var(--padding-sm) var(--padding-lg);
|
|
30
|
+
|
|
31
|
+
/* Buttons are always pill-shaped (design.md invariant) */
|
|
32
|
+
border-radius: var(--radius-full);
|
|
33
|
+
|
|
34
|
+
/* Typography — the clickable-control face, same as Button */
|
|
35
|
+
font-family: var(--font-paragraph-em-family);
|
|
36
|
+
font-weight: var(--font-paragraph-em-weight);
|
|
37
|
+
font-size: var(--font-paragraph-em-size);
|
|
38
|
+
line-height: var(--font-paragraph-em-line-height);
|
|
39
|
+
letter-spacing: var(--font-paragraph-em-letter-spacing);
|
|
40
|
+
text-align: center;
|
|
41
|
+
|
|
42
|
+
/* The field is transparent by design: the ring and glow are the button */
|
|
43
|
+
background: transparent;
|
|
44
|
+
color: var(--color-text-primary);
|
|
45
|
+
|
|
46
|
+
transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
|
|
47
|
+
|
|
48
|
+
/* Reset */
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
border: none;
|
|
51
|
+
text-decoration: none;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* The gradient ring: a conic gradient painted across the whole button, then
|
|
55
|
+
masked to the border band (two fills, exclude-composited, inset by the
|
|
56
|
+
border width). The gradient repeats back to its start colour so the seam
|
|
57
|
+
at 360° is invisible while it turns. */
|
|
58
|
+
.ds-ai-button::before {
|
|
59
|
+
content: '';
|
|
60
|
+
position: absolute;
|
|
61
|
+
inset: 0;
|
|
62
|
+
border-radius: inherit;
|
|
63
|
+
padding: var(--border-xs);
|
|
64
|
+
background: conic-gradient(
|
|
65
|
+
from var(--ds-ai-button-angle),
|
|
66
|
+
var(--color-ai-gradient-start),
|
|
67
|
+
var(--color-ai-gradient-mid),
|
|
68
|
+
var(--color-ai-gradient-end),
|
|
69
|
+
var(--color-ai-gradient-mid),
|
|
70
|
+
var(--color-ai-gradient-start)
|
|
71
|
+
);
|
|
72
|
+
/* Standard properties only — the build pipeline prefixes per its browser
|
|
73
|
+
targets, and hand-written -webkit- duplicates confuse its merging. */
|
|
74
|
+
mask:
|
|
75
|
+
linear-gradient(#fff 0 0) content-box,
|
|
76
|
+
linear-gradient(#fff 0 0);
|
|
77
|
+
mask-composite: exclude;
|
|
78
|
+
pointer-events: none;
|
|
79
|
+
/* ds-allow(motion): ambient signature rotation, tuned by eye — deliberately
|
|
80
|
+
far slower than the interaction-scale motion tokens */
|
|
81
|
+
animation: ds-ai-button-turn 7s linear infinite;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* The glow: the same gradient, blurred behind the button at low opacity.
|
|
85
|
+
A colour effect, not an elevation shadow — depth stays token-owned. */
|
|
86
|
+
.ds-ai-button::after {
|
|
87
|
+
content: '';
|
|
88
|
+
position: absolute;
|
|
89
|
+
inset: 0;
|
|
90
|
+
border-radius: inherit;
|
|
91
|
+
background: conic-gradient(
|
|
92
|
+
from var(--ds-ai-button-angle),
|
|
93
|
+
var(--color-ai-gradient-start),
|
|
94
|
+
var(--color-ai-gradient-mid),
|
|
95
|
+
var(--color-ai-gradient-end),
|
|
96
|
+
var(--color-ai-gradient-mid),
|
|
97
|
+
var(--color-ai-gradient-start)
|
|
98
|
+
);
|
|
99
|
+
/* The glow's spread; a length with no token family, chosen by eye. */
|
|
100
|
+
--ds-ai-button-glow: 14px;
|
|
101
|
+
filter: blur(var(--ds-ai-button-glow));
|
|
102
|
+
opacity: 0.3;
|
|
103
|
+
z-index: -1;
|
|
104
|
+
pointer-events: none;
|
|
105
|
+
transition: opacity var(--motion-duration-base) var(--motion-ease-standard);
|
|
106
|
+
/* ds-allow(motion): same ambient rotation as the ring, kept in phase */
|
|
107
|
+
animation: ds-ai-button-turn 7s linear infinite;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@keyframes ds-ai-button-turn {
|
|
111
|
+
to {
|
|
112
|
+
--ds-ai-button-angle: 360deg;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Icon rides the icon-size scale */
|
|
117
|
+
.ds-ai-button__icon {
|
|
118
|
+
--icon-size: var(--icon-size-md);
|
|
119
|
+
line-height: 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* States */
|
|
123
|
+
.ds-ai-button:hover::after {
|
|
124
|
+
opacity: 0.55;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.ds-ai-button:focus-visible {
|
|
128
|
+
outline: var(--border-md) solid var(--color-action-primary-bg);
|
|
129
|
+
outline-offset: 2px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ds-ai-button--disabled,
|
|
133
|
+
.ds-ai-button:disabled {
|
|
134
|
+
opacity: 0.4;
|
|
135
|
+
cursor: not-allowed;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.ds-ai-button--disabled::before,
|
|
139
|
+
.ds-ai-button--disabled::after,
|
|
140
|
+
.ds-ai-button:disabled::before,
|
|
141
|
+
.ds-ai-button:disabled::after {
|
|
142
|
+
animation-play-state: paused;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* Sizes */
|
|
146
|
+
.ds-ai-button--compact {
|
|
147
|
+
padding: var(--padding-xs) var(--padding-sm-md);
|
|
148
|
+
font-size: var(--font-paragraph-sm-em-size);
|
|
149
|
+
line-height: var(--font-paragraph-sm-em-line-height);
|
|
150
|
+
letter-spacing: var(--font-paragraph-sm-em-letter-spacing);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.ds-ai-button--compact .ds-ai-button__icon {
|
|
154
|
+
--icon-size: var(--icon-size-sm);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Reduced motion: the ring and glow stay, the turning stops. */
|
|
158
|
+
@media (prefers-reduced-motion: reduce) {
|
|
159
|
+
.ds-ai-button::before,
|
|
160
|
+
.ds-ai-button::after {
|
|
161
|
+
animation: none;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by AiButton itself — everything else falls through to the DOM node. */
|
|
3
|
+
type AiButtonOwnProps = {
|
|
4
|
+
/** Button text content */
|
|
5
|
+
label?: string;
|
|
6
|
+
/** Leading icon — Material Symbol name (string) or custom element (ReactNode) */
|
|
7
|
+
icon?: string | React.ReactNode;
|
|
8
|
+
/** Button size */
|
|
9
|
+
size?: 'default' | 'compact';
|
|
10
|
+
/** Whether the button is disabled */
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
/** Optional href — renders as <a> instead of <button> */
|
|
13
|
+
href?: string;
|
|
14
|
+
/** Optional target attribute for links */
|
|
15
|
+
target?: string;
|
|
16
|
+
/** Optional rel attribute for links */
|
|
17
|
+
rel?: string;
|
|
18
|
+
/** Additional CSS classes */
|
|
19
|
+
className?: string;
|
|
20
|
+
};
|
|
21
|
+
export interface AiButtonProps extends AiButtonOwnProps, Omit<React.ComponentPropsWithoutRef<'button'>, keyof AiButtonOwnProps | 'type'> {
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The AI entry point. Icon and label on a transparent field, ringed by the
|
|
25
|
+
* AI gradient (`--color-ai-gradient-*`) rotating slowly, with a soft glow of
|
|
26
|
+
* the same gradient behind it. The treatment is the design system's signal
|
|
27
|
+
* for "a model answers here": ordinary actions keep the flat action teal,
|
|
28
|
+
* and this ring is reserved for AI surfaces so neither affordance dilutes
|
|
29
|
+
* the other.
|
|
30
|
+
*
|
|
31
|
+
* Renders a `<button>`, or an `<a>` when `href` is supplied. Forwards a ref
|
|
32
|
+
* to whichever element it renders, and spreads unrecognised props onto it.
|
|
33
|
+
* The rotation pauses under `prefers-reduced-motion`; the ring and glow stay.
|
|
34
|
+
*/
|
|
35
|
+
export declare const AiButton: React.ForwardRefExoticComponent<AiButtonProps & React.RefAttributes<HTMLButtonElement | HTMLAnchorElement>>;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import "./AiButton.css";
|
|
4
|
+
import "../../fonts/material-symbols.css";
|
|
5
|
+
const AiButton = React.forwardRef(
|
|
6
|
+
({
|
|
7
|
+
label = "Ask AI",
|
|
8
|
+
icon = "auto_awesome",
|
|
9
|
+
size = "default",
|
|
10
|
+
disabled,
|
|
11
|
+
href,
|
|
12
|
+
target,
|
|
13
|
+
rel,
|
|
14
|
+
className = "",
|
|
15
|
+
...rest
|
|
16
|
+
}, ref) => {
|
|
17
|
+
const baseClass = "ds-ai-button";
|
|
18
|
+
const classes = [
|
|
19
|
+
baseClass,
|
|
20
|
+
`${baseClass}--${size}`,
|
|
21
|
+
disabled && `${baseClass}--disabled`,
|
|
22
|
+
className
|
|
23
|
+
].filter(Boolean).join(" ");
|
|
24
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
25
|
+
icon && (typeof icon === "string" ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon material-symbols-rounded`, "aria-hidden": "true", children: icon }) : /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon`, "aria-hidden": "true", children: icon })),
|
|
26
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__text`, children: label })
|
|
27
|
+
] });
|
|
28
|
+
if (href && !disabled) {
|
|
29
|
+
return /* @__PURE__ */ jsx(
|
|
30
|
+
"a",
|
|
31
|
+
{
|
|
32
|
+
...rest,
|
|
33
|
+
ref,
|
|
34
|
+
className: classes,
|
|
35
|
+
href,
|
|
36
|
+
target,
|
|
37
|
+
rel,
|
|
38
|
+
children: content
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return /* @__PURE__ */ jsx(
|
|
43
|
+
"button",
|
|
44
|
+
{
|
|
45
|
+
...rest,
|
|
46
|
+
ref,
|
|
47
|
+
type: "button",
|
|
48
|
+
className: classes,
|
|
49
|
+
disabled,
|
|
50
|
+
children: content
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
AiButton.displayName = "AiButton";
|
|
56
|
+
export {
|
|
57
|
+
AiButton
|
|
58
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
CHAT HEADER COMPONENT
|
|
3
|
+
Top row of a chat surface: title on the
|
|
4
|
+
left, controls on the right. No divider —
|
|
5
|
+
it floats free of the transcript.
|
|
6
|
+
============================================ */
|
|
7
|
+
|
|
8
|
+
/* Base */
|
|
9
|
+
.ds-chat-header {
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: space-between;
|
|
13
|
+
gap: var(--gap-md);
|
|
14
|
+
padding: var(--padding-xs) var(--padding-md);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Title */
|
|
18
|
+
.ds-chat-header__title {
|
|
19
|
+
font-family: var(--font-title-body-family);
|
|
20
|
+
font-size: var(--font-title-body-size);
|
|
21
|
+
font-weight: var(--font-title-body-weight);
|
|
22
|
+
line-height: var(--font-title-body-line-height);
|
|
23
|
+
color: var(--color-text-primary);
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
text-overflow: ellipsis;
|
|
26
|
+
white-space: nowrap;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Actions */
|
|
30
|
+
.ds-chat-header__actions {
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: var(--gap-xs);
|
|
34
|
+
flex-shrink: 0;
|
|
35
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by ChatHeader itself — everything else falls through to the root element. */
|
|
3
|
+
type ChatHeaderOwnProps = {
|
|
4
|
+
/**
|
|
5
|
+
* The chat or assistant name. Deliberately shadows the native `title`
|
|
6
|
+
* tooltip attribute — a header's title is content, not a tooltip. Pass a
|
|
7
|
+
* string for the default treatment, or your own heading element.
|
|
8
|
+
*/
|
|
9
|
+
title?: React.ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* Trailing controls: a new-chat CircularButton, a view switcher, a close
|
|
12
|
+
* button. The slot only lays them out — each control owns its behaviour.
|
|
13
|
+
*/
|
|
14
|
+
actions?: React.ReactNode;
|
|
15
|
+
/** Additional CSS classes */
|
|
16
|
+
className?: string;
|
|
17
|
+
};
|
|
18
|
+
export interface ChatHeaderProps extends ChatHeaderOwnProps, Omit<React.ComponentPropsWithoutRef<'header'>, keyof ChatHeaderOwnProps> {
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* ChatHeader is the top row of a chat surface: the conversation's name on the
|
|
22
|
+
* left, its controls on the right. It draws no bottom border — like the
|
|
23
|
+
* composer below, it floats free of the transcript, and the thread's own
|
|
24
|
+
* padding provides the separation.
|
|
25
|
+
*
|
|
26
|
+
* Purely presentational, so it renders from a Server Component; the controls
|
|
27
|
+
* passed into `actions` bring their own interactivity.
|
|
28
|
+
*/
|
|
29
|
+
export declare const ChatHeader: React.ForwardRefExoticComponent<ChatHeaderProps & React.RefAttributes<HTMLElement>>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import "./ChatHeader.css";
|
|
4
|
+
const ChatHeader = React.forwardRef(
|
|
5
|
+
({ title, actions, className = "", ...rest }, ref) => {
|
|
6
|
+
const baseClass = "ds-chat-header";
|
|
7
|
+
const classes = [baseClass, className].filter(Boolean).join(" ");
|
|
8
|
+
return /* @__PURE__ */ jsxs("header", { ...rest, ref, className: classes, children: [
|
|
9
|
+
typeof title === "string" ? /* @__PURE__ */ jsx("span", { className: `${baseClass}__title`, children: title }) : title,
|
|
10
|
+
actions && /* @__PURE__ */ jsx("div", { className: `${baseClass}__actions`, children: actions })
|
|
11
|
+
] });
|
|
12
|
+
}
|
|
13
|
+
);
|
|
14
|
+
ChatHeader.displayName = "ChatHeader";
|
|
15
|
+
export {
|
|
16
|
+
ChatHeader
|
|
17
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
CHAT MARKER COMPONENT
|
|
3
|
+
Inline conversation separator for dates and system notes
|
|
4
|
+
============================================ */
|
|
5
|
+
|
|
6
|
+
.ds-chat-marker {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: var(--gap-sm-md);
|
|
10
|
+
width: 100%;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Flanking lines — kept as flex spacers when bare, so the label
|
|
14
|
+
stays centred either way */
|
|
15
|
+
.ds-chat-marker::before,
|
|
16
|
+
.ds-chat-marker::after {
|
|
17
|
+
content: '';
|
|
18
|
+
flex: 1;
|
|
19
|
+
height: var(--border-xs);
|
|
20
|
+
background-color: var(--color-divider);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.ds-chat-marker--bare::before,
|
|
24
|
+
.ds-chat-marker--bare::after {
|
|
25
|
+
background-color: transparent;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* ============================================
|
|
29
|
+
LABEL
|
|
30
|
+
============================================ */
|
|
31
|
+
|
|
32
|
+
.ds-chat-marker__label {
|
|
33
|
+
flex-shrink: 0;
|
|
34
|
+
display: inline-flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: var(--gap-xs);
|
|
37
|
+
font-family: var(--font-paragraph-sm-family);
|
|
38
|
+
font-size: var(--font-paragraph-sm-size);
|
|
39
|
+
font-weight: var(--font-paragraph-sm-weight);
|
|
40
|
+
line-height: var(--font-paragraph-sm-line-height);
|
|
41
|
+
letter-spacing: var(--font-paragraph-sm-letter-spacing);
|
|
42
|
+
color: var(--color-text-tertiary);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.ds-chat-marker__icon {
|
|
46
|
+
display: inline-flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.ds-chat-marker__icon .material-symbols-rounded {
|
|
51
|
+
--icon-size: var(--icon-size-sm);
|
|
52
|
+
}
|