@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
package/README.md
CHANGED
|
@@ -5,20 +5,21 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@robr0/design-system)
|
|
6
6
|
<!-- npm-badge:end -->
|
|
7
7
|
|
|
8
|
-
A personal portfolio site
|
|
8
|
+
A personal portfolio site, the AI-ready React design system behind it, and the AI layer that lets the site answer questions about itself. Claude Code builds all of it from the written specs in this repo (`CLAUDE.md` for the rules, `design.md` for the design language, `content-design.md` for how every word reads, and `porting-guide.md` for rebuilding the site chat inside another design system), and generated registries keep this README and the docs site from drifting. The design system is the backbone every portfolio page is built on, so the work is presented consistently and to the same craft standards throughout.
|
|
9
9
|
|
|
10
10
|
**[→ Live site](https://robertritacca.com/)** · **[→ Storybook](https://design-system-iota-one.vercel.app/?path=/docs/robr0-ds--docs)**
|
|
11
11
|
|
|
12
|
-
The **live site** is the portfolio built on the design system. **Storybook** is the interactive component explorer for the design system itself. They
|
|
12
|
+
The **live site** is the portfolio built on the design system. **Storybook** is the interactive component explorer for the design system itself. They deploy separately: two Vercel projects from one repo.
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Three parts, one repo
|
|
17
17
|
|
|
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
|
+
| **AI layer** (`ai` components in `/src`, chat in `/website`, `/evals`) | A site-wide chat that answers questions about the work: built from the library's own `ai` components, grounded in a corpus generated from the site's published content, and scored by a golden-set eval. |
|
|
22
23
|
|
|
23
24
|
---
|
|
24
25
|
|
|
@@ -27,12 +28,12 @@ The **live site** is the portfolio built on the design system. **Storybook** is
|
|
|
27
28
|
### Components
|
|
28
29
|
|
|
29
30
|
<!-- 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
|
|
31
|
+
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
32
|
<!-- component-list:end -->
|
|
32
33
|
|
|
33
34
|
### Using the package
|
|
34
35
|
|
|
35
|
-
The design system is published as [`@robr0/design-system`](https://www.npmjs.com/package/@robr0/design-system) (React 19+ is a peer dependency). The **[full setup guide](https://robertritacca.com/docs/get-started)**
|
|
36
|
+
The design system is published as [`@robr0/design-system`](https://www.npmjs.com/package/@robr0/design-system) (React 19+ is a peer dependency). The **[full setup guide](https://robertritacca.com/docs/get-started)** expands on everything in this section:
|
|
36
37
|
|
|
37
38
|
```bash
|
|
38
39
|
npm install @robr0/design-system
|
|
@@ -86,19 +87,29 @@ Dark mode is driven by `data-theme="dark"` on the root element, with no `prefers
|
|
|
86
87
|
|
|
87
88
|
### Design principles
|
|
88
89
|
|
|
89
|
-
Stated as token roles on purpose
|
|
90
|
+
Stated as token roles on purpose. What each role resolves to is the theme, and yours to override:
|
|
90
91
|
|
|
91
|
-
- **Style with semantic tokens, never raw values
|
|
92
|
+
- **Style with semantic tokens, never raw values**: that is what makes one primitive override cascade through everything
|
|
92
93
|
- **One typeface**, hierarchy carried by weight contrast
|
|
93
94
|
- **The primary-action token means "click here"**: CTAs and focus only, never decoration
|
|
94
95
|
- **Shape is a per-element-type token**, not a per-instance choice
|
|
95
96
|
- **Five status roles** (info, positive, warning, error, neutral) shared by every status-bearing component
|
|
96
|
-
- **Depth is token-owned**: the container ramp plus the system's elevation tokens
|
|
97
|
+
- **Depth is token-owned**: the container ramp plus the system's elevation tokens, and components don't bring their own shadows
|
|
97
98
|
|
|
98
99
|
The defaults behind each role, and every component spec, live in [`design.md`](design.md).
|
|
99
100
|
|
|
100
101
|
---
|
|
101
102
|
|
|
103
|
+
## AI layer
|
|
104
|
+
|
|
105
|
+
The library's `ai` category is the interface half: chat primitives (Chat thread, Chat message, Composer) and agent-state components (Tool call, Reasoning, Agent status). Those components ship in the npm package; the rest of the chat does not. The conversation state hook, the streaming transport, the backend route, and the corpus are this site's own code under `website/`, and a consumer builds their own equivalents, with their LLM API key held server-side, never in the client. The site's own chat, robr0 GPT, is built from the shipped primitives, so it doubles as a live demo of the components it is made of.
|
|
106
|
+
|
|
107
|
+
The answering half is a Claude-backed route (`website/src/app/api/chat/route.ts`) with a persona and guardrails. There is no retrieval step: `scripts/generate-site-corpus.mjs` compiles every published page's prose, the data registries, and the root specs into one corpus at build time, and the model reads it whole. The corpus is public-only, and a validator enforces that boundary. Nothing reaches the model that is not already on the site, so a prompt injection has nothing private to leak. The route also deploys ahead of its keys, so an unconfigured deploy answers with a polite notice rather than throwing.
|
|
108
|
+
|
|
109
|
+
Answers are measured, not assumed. `evals/chat` holds a golden set that runs through the real route (persona, corpus, guardrails, all of it) with `npm run eval:chat`; it costs real API calls, so it runs on demand and never in CI. A second validator fails the build when a golden-set fact is missing from the corpus, which keeps the eval and the site describing the same system.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
102
113
|
## Tech
|
|
103
114
|
|
|
104
115
|
- **React 19 + TypeScript**: component library
|
|
@@ -118,7 +129,7 @@ Every push and pull request runs a GitHub Actions pipeline ([`ci.yml`](.github/w
|
|
|
118
129
|
npm run verify # lint + library type-check + package build + story tests + Storybook build + website lint + build
|
|
119
130
|
```
|
|
120
131
|
|
|
121
|
-
CI also guards against documentation drift: generated surfaces (this README's component count and list, the website's skills pages, the published
|
|
132
|
+
CI also guards against documentation drift: generated surfaces (this README's component count and list, the website's skills pages, the published blueprint copies of the root markdown specs) are rebuilt from their source registries on every build, and CI fails if the committed copies are stale. The numbers on the site are never hand-written.
|
|
122
133
|
|
|
123
134
|
---
|
|
124
135
|
|
|
@@ -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 {};
|