@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.
Files changed (70) hide show
  1. package/README.md +21 -10
  2. package/components/AgentStatus/AgentStatus.css +82 -22
  3. package/components/AgentStatus/AgentStatus.d.ts +6 -2
  4. package/components/AgentStatus/AgentStatus.js +38 -6
  5. package/components/AiButton/AiButton.css +163 -0
  6. package/components/AiButton/AiButton.d.ts +36 -0
  7. package/components/AiButton/AiButton.js +58 -0
  8. package/components/ChatHeader/ChatHeader.css +35 -0
  9. package/components/ChatHeader/ChatHeader.d.ts +30 -0
  10. package/components/ChatHeader/ChatHeader.js +17 -0
  11. package/components/ChatMarker/ChatMarker.css +52 -0
  12. package/components/ChatMarker/ChatMarker.d.ts +22 -0
  13. package/components/ChatMarker/ChatMarker.js +18 -0
  14. package/components/ChatMessage/ChatMessage.css +310 -0
  15. package/components/ChatMessage/ChatMessage.d.ts +74 -0
  16. package/components/ChatMessage/ChatMessage.js +71 -0
  17. package/components/ChatThread/ChatThread.css +171 -0
  18. package/components/ChatThread/ChatThread.d.ts +41 -0
  19. package/components/ChatThread/ChatThread.js +174 -0
  20. package/components/Chip/Chip.css +27 -0
  21. package/components/Chip/Chip.d.ts +2 -2
  22. package/components/CircularButton/CircularButton.d.ts +23 -8
  23. package/components/CircularButton/CircularButton.js +54 -52
  24. package/components/CodeBlock/CodeBlock.css +2 -2
  25. package/components/Composer/Composer.css +163 -0
  26. package/components/Composer/Composer.d.ts +67 -0
  27. package/components/Composer/Composer.js +120 -0
  28. package/components/DocumentChip/DocumentChip.css +181 -0
  29. package/components/DocumentChip/DocumentChip.d.ts +41 -0
  30. package/components/DocumentChip/DocumentChip.js +78 -0
  31. package/components/InterruptCard/InterruptCard.css +164 -0
  32. package/components/InterruptCard/InterruptCard.d.ts +55 -0
  33. package/components/InterruptCard/InterruptCard.js +57 -0
  34. package/components/MessageActions/MessageActions.css +76 -0
  35. package/components/MessageActions/MessageActions.d.ts +38 -0
  36. package/components/MessageActions/MessageActions.js +33 -0
  37. package/components/MessageCard/MessageCard.css +112 -0
  38. package/components/MessageCard/MessageCard.d.ts +35 -0
  39. package/components/MessageCard/MessageCard.js +28 -0
  40. package/components/NavList/NavList.css +155 -0
  41. package/components/NavList/NavList.d.ts +56 -0
  42. package/components/NavList/NavList.js +99 -0
  43. package/components/PromptSuggestions/PromptSuggestions.css +87 -0
  44. package/components/PromptSuggestions/PromptSuggestions.d.ts +51 -0
  45. package/components/PromptSuggestions/PromptSuggestions.js +34 -0
  46. package/components/Prose/Prose.css +252 -0
  47. package/components/Prose/Prose.d.ts +21 -0
  48. package/components/Prose/Prose.js +14 -0
  49. package/components/Reasoning/Reasoning.css +149 -25
  50. package/components/Reasoning/Reasoning.d.ts +19 -0
  51. package/components/Reasoning/Reasoning.js +32 -3
  52. package/components/SourceChip/SourceChip.css +102 -0
  53. package/components/SourceChip/SourceChip.d.ts +32 -0
  54. package/components/SourceChip/SourceChip.js +31 -0
  55. package/components/Stat/Stat.css +2 -2
  56. package/components/Timeline/Timeline.css +9 -1
  57. package/components/ToolCall/ToolCall.css +3 -3
  58. package/components/registry.json +113 -1
  59. package/components/registry.json.d.ts +113 -1
  60. package/components/registry.json.js +1 -1
  61. package/index.d.ts +14 -0
  62. package/index.js +28 -0
  63. package/package.json +1 -1
  64. package/tokens/registry.json +13 -0
  65. package/tokens/registry.json.d.ts +13 -0
  66. package/tokens/registry.json.js +1 -1
  67. package/tokens/tokens-dark.css +13 -0
  68. package/tokens/tokens-light.css +43 -0
  69. package/tokens/tokens-primitives.css +5 -0
  70. package/tokens/tokens-typography.css +24 -0
package/README.md CHANGED
@@ -5,20 +5,21 @@
5
5
  [![npm](https://img.shields.io/npm/v/@robr0%2Fdesign-system?logo=npm&color=CB3837)](https://www.npmjs.com/package/@robr0/design-system)
6
6
  <!-- npm-badge:end -->
7
7
 
8
- A personal portfolio site built on an AI-ready React design system, designed and engineered from scratch. Claude Code builds the components 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 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.
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 are two separate parts of this repo.
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
- ## Two things, one repo
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 -->64<!-- /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)** |
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)** covers install, dark mode, fonts, and re-theming:
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 what each role resolves to is the theme, and yours to override:
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** that is what makes one primitive override cascade through everything
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 components don't bring their own shadows
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 CLAUDE.md, design.md, and content-design.md blueprints) 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.
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-sm);
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-sm-family);
35
- font-size: var(--font-paragraph-sm-size);
36
- font-weight: var(--font-paragraph-sm-weight);
37
- line-height: var(--font-paragraph-sm-line-height);
38
- letter-spacing: var(--font-paragraph-sm-letter-spacing);
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
- /* A gradient between the secondary text colour the label already sits at and
71
- the primary text colour, sweeping left to right: with an oversized
72
- background image, background-position 100% shows its right edge, 0% its
73
- left, so counting down walks the bright band across the text. */
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-label-color) 35%,
78
- var(--color-text-primary) 50%,
79
- var(--ds-agent-label-color) 65%
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--md {
129
- --ds-agent-matrix-size: var(--icon-size-md);
188
+ .ds-agent-status--compact {
189
+ --ds-agent-matrix-size: var(--icon-size-sm);
130
190
 
131
- gap: var(--gap-sm-md);
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
- /** Indicator and text scale. */
17
- size?: 'sm' | 'md';
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 = "sm",
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}--${size}`,
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 ?? label ?? DEFAULT_LABELS[state]
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 {};