@robr0/design-system 0.5.0 → 0.7.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 (35) hide show
  1. package/LICENSE +53 -2
  2. package/README.md +54 -12
  3. package/components/AgentStatus/AgentStatus.css +26 -11
  4. package/components/Card/Card.css +9 -0
  5. package/components/Card/Card.d.ts +6 -0
  6. package/components/Card/Card.js +2 -1
  7. package/components/ChatMessage/ChatMessage.css +6 -0
  8. package/components/ChatMessage/ChatMessage.d.ts +7 -1
  9. package/components/ChatMessage/ChatMessage.js +2 -0
  10. package/components/Composer/Composer.css +23 -6
  11. package/components/Composer/Composer.d.ts +3 -0
  12. package/components/Composer/Composer.js +12 -1
  13. package/components/Prose/Prose.css +18 -1
  14. package/components/Reasoning/Reasoning.css +19 -4
  15. package/components/ShaderField/ShaderField.css +24 -0
  16. package/components/ShaderField/ShaderField.d.ts +68 -0
  17. package/components/ShaderField/ShaderField.js +54 -0
  18. package/components/ShaderField/field.glsl.d.ts +33 -0
  19. package/components/ShaderField/field.glsl.js +177 -0
  20. package/components/ShaderField/useShaderField.d.ts +86 -0
  21. package/components/ShaderField/useShaderField.js +395 -0
  22. package/components/Timeline/Timeline.css +9 -1
  23. package/components/registry.json +8 -0
  24. package/components/registry.json.d.ts +8 -0
  25. package/components/registry.json.js +1 -1
  26. package/index.d.ts +1 -0
  27. package/index.js +8 -0
  28. package/package.json +5 -1
  29. package/tokens/registry.json +2 -0
  30. package/tokens/registry.json.d.ts +2 -0
  31. package/tokens/registry.json.js +1 -1
  32. package/tokens/tokens-dark.css +1 -1
  33. package/tokens/tokens-light.css +3 -2
  34. package/tokens/tokens-motion.css +2 -0
  35. package/tokens/tokens-primitives.css +5 -0
package/LICENSE CHANGED
@@ -1,6 +1,21 @@
1
- MIT License
1
+ Copyright (c) 2026 Rob Ritacca. All rights reserved, except as granted below.
2
2
 
3
- Copyright (c) 2026 Rob Ritacca
3
+ This repository holds two kinds of material under two different licenses.
4
+ The SOFTWARE is free to use under the MIT License. The CONTENT is not
5
+ licensed for reuse.
6
+
7
+ If you installed this as an npm package, you received the Software only.
8
+ The MIT License below is the license for everything in that package.
9
+
10
+
11
+ ================================================================================
12
+ SOFTWARE — MIT License
13
+ ================================================================================
14
+
15
+ Applies to the design system and the code that builds and documents it:
16
+ component source and CSS, the design tokens, Storybook stories, the build,
17
+ generator and validator scripts, configuration, and the website's application
18
+ code (layouts, routing, hooks, and UI components).
4
19
 
5
20
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
21
  of this software and associated documentation files (the "Software"), to deal
@@ -19,3 +34,39 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
34
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
35
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
36
  SOFTWARE.
37
+
38
+
39
+ ================================================================================
40
+ CONTENT — All rights reserved
41
+ ================================================================================
42
+
43
+ Applies to the authored material published on robertritacca.com and stored in
44
+ this repository, including:
45
+
46
+ - Written prose on any page: body copy, headings, descriptions, and
47
+ microcopy across the website and the root specification documents.
48
+ - Case studies and the client work they describe
49
+ (website/src/app/work/, website/src/data/case-studies.json).
50
+ - Essays and long-form writing
51
+ (website/src/app/writing/, website/src/data/essays.json).
52
+ - The project journal entries (website/src/data/site-updates.json).
53
+ - Photographs, illustrations, client logos, cover images, and other media.
54
+ - The name "Rob Ritacca", the RR logo and wordmark, and any likeness,
55
+ photograph, biography, or professional history of Rob Ritacca.
56
+
57
+ No license is granted to this material. Without prior written permission you
58
+ may not copy, reproduce, republish, mirror, redistribute, sell, or create
59
+ derivative works from it, and you may not use it to train, fine-tune, or
60
+ otherwise build a machine learning model. You may not present it as your own
61
+ work, and you may not use the name, logo, likeness, or professional history of
62
+ Rob Ritacca to imply authorship, endorsement, or affiliation.
63
+
64
+ You are welcome to read the site, quote short passages with attribution and a
65
+ link to the source, link to any page, and use automated tools to read and cite
66
+ pages as a human reader would. Nothing here restricts rights you already have
67
+ under applicable copyright law.
68
+
69
+ Client logos and trademarks shown in case studies belong to their respective
70
+ owners and appear as references to real work, not as a grant of any rights.
71
+
72
+ Permission requests: rob.ritacca@gmail.com
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 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 -->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)** |
21
+ | **Design system** (`/src`) | <!-- component-count -->79<!-- /component-count --> React components, a three-tier token architecture, dark mode, a WebGL2 ambient background that themes itself from your tokens, 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,35 @@ 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 · 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
+ 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 · Shader field · 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
 
34
+ ### Ambient background
35
+
36
+ The package ships more than components. **Shader field** is a WebGL2 canvas that sums soft Gaussian light sources into an ambient field of colour, and every source reads a semantic colour token at runtime. Override a primitive and the background re-themes with the rest of the system, in both themes, with nothing wired up:
37
+
38
+ ```tsx
39
+ import { ShaderField, type ShaderFieldStatus } from '@robr0/design-system';
40
+
41
+ const [status, setStatus] = useState<ShaderFieldStatus>('pending');
42
+
43
+ <div style={{ position: 'fixed', inset: 0, zIndex: -1 }}>
44
+ {status === 'unavailable' && <YourCssFallback />}
45
+ <ShaderField params={{ streak: 0.4 }} onStatusChange={setStatus} />
46
+ </div>
47
+ ```
48
+
49
+ Seven parameters describe the look, and a composition is a table of token names with positions and drift periods, so a negative weight cuts a shadow through the field instead of adding light to it.
50
+
51
+ It asks two things of you, both deliberate. It fills a positioned ancestor you provide, because where a background sits is a layout decision rather than a rendering one. And it never decides what to paint instead of itself: it reports `pending`, `active` or `unavailable`, and one fallback covers every way it can fail (no WebGL2, a blocked or lost GPU context, a renderer that stalls before its first frame, or `enabled={false}` as a kill switch). `pending` is the state worth handling. Paint neither layer while the context comes up, or the swap a frame later reads as two backgrounds loading in sequence.
52
+
53
+ It is also the one place a `prefers-reduced-motion` check lives in JavaScript rather than in the motion tokens, because the CSS guard cannot see a `requestAnimationFrame` loop. Set the preference and it draws a single static frame.
54
+
55
+ The background behind this site is that component, with eight blurred CSS discs kept painted underneath as its fallback. The **[component page](https://robertritacca.com/components/shader-field)** is a live demo, and the **[setup guide](https://robertritacca.com/docs/get-started)** has the wiring.
56
+
33
57
  ### Using the package
34
58
 
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:
59
+ 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
60
 
37
61
  ```bash
38
62
  npm install @robr0/design-system
@@ -86,39 +110,49 @@ Dark mode is driven by `data-theme="dark"` on the root element, with no `prefers
86
110
 
87
111
  ### Design principles
88
112
 
89
- Stated as token roles on purpose what each role resolves to is the theme, and yours to override:
113
+ Stated as token roles on purpose. What each role resolves to is the theme, and yours to override:
90
114
 
91
- - **Style with semantic tokens, never raw values** that is what makes one primitive override cascade through everything
115
+ - **Style with semantic tokens, never raw values**: that is what makes one primitive override cascade through everything
92
116
  - **One typeface**, hierarchy carried by weight contrast
93
117
  - **The primary-action token means "click here"**: CTAs and focus only, never decoration
94
118
  - **Shape is a per-element-type token**, not a per-instance choice
95
119
  - **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
120
+ - **Depth is token-owned**: the container ramp plus the system's elevation tokens, and components don't bring their own shadows
97
121
 
98
122
  The defaults behind each role, and every component spec, live in [`design.md`](design.md).
99
123
 
100
124
  ---
101
125
 
126
+ ## AI layer
127
+
128
+ 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.
129
+
130
+ 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.
131
+
132
+ 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.
133
+
134
+ ---
135
+
102
136
  ## Tech
103
137
 
104
138
  - **React 19 + TypeScript**: component library
105
139
  - **Vite 7**: dev server and library build
106
140
  - **Next.js 16**: portfolio site and design system documentation
107
141
  - **Storybook 10**: component explorer
108
- - **Vitest + Playwright**: every Storybook story runs as a render test in headless Chromium
142
+ - **Vitest + Playwright + axe**: every Storybook story runs as a render test in headless Chromium, with an accessibility audit on each
109
143
  - **CSS custom properties**: all theming via semantic tokens, no CSS-in-JS
110
144
 
111
145
  ---
112
146
 
113
147
  ## Quality & CI
114
148
 
115
- Every push and pull request runs a GitHub Actions pipeline ([`ci.yml`](.github/workflows/ci.yml)) with four jobs: lint + library build, story tests (every Storybook story rendered in headless Chromium via Vitest), Storybook build, and website lint + build. The same checklist runs locally with one command:
149
+ Every push and pull request runs a GitHub Actions pipeline ([`ci.yml`](.github/workflows/ci.yml)) with four jobs: lint + library build, story tests, Storybook build, and website lint + build. The story tests render every Storybook story in headless Chromium via Vitest and run an axe accessibility audit on each, so a violation fails the build exactly like a render error. The same checklist runs locally with one command:
116
150
 
117
151
  ```bash
118
152
  npm run verify # lint + library type-check + package build + story tests + Storybook build + website lint + build
119
153
  ```
120
154
 
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.
155
+ 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
156
 
123
157
  ---
124
158
 
@@ -132,3 +166,11 @@ npm run storybook # http://localhost:6006
132
166
  npm install
133
167
  npm run dev --workspace website # http://localhost:3000
134
168
  ```
169
+
170
+ ---
171
+
172
+ ## License
173
+
174
+ Two licenses, one repository. The **software** (components, tokens, scripts, and the website's application code) is MIT, so use it in anything. The **content** (page prose, case studies, essays, images, and the name and likeness of Rob Ritacca) is all rights reserved and not licensed for reuse. See [`LICENSE`](LICENSE) for the full terms.
175
+
176
+ The npm package contains the software only.
@@ -22,8 +22,8 @@
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
+ --ds-agent-shimmer-base: var(--color-text-tertiary);
26
+ --ds-agent-shimmer-sweep: var(--color-text-primary);
27
27
  --ds-agent-dot-dim: 0.2;
28
28
  --ds-agent-dot-rest: 0.82;
29
29
  --ds-agent-cycle: var(--motion-duration-loop-matrix);
@@ -33,11 +33,16 @@
33
33
  display: inline-flex;
34
34
  align-items: center;
35
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);
36
+
37
+ /* The label is set in the title-body face, not the paragraph one: a status
38
+ is a heading for the work underneath it, not prose. The two sets share
39
+ 16px/24px metrics, so the row's geometry is identical either way — only
40
+ the weight and tracking change. */
41
+ font-family: var(--font-title-body-family);
42
+ font-size: var(--font-title-body-size);
43
+ font-weight: var(--font-title-body-weight);
44
+ line-height: var(--font-title-body-line-height);
45
+ letter-spacing: var(--font-title-body-letter-spacing);
41
46
  }
42
47
 
43
48
  /* ============================================
@@ -112,13 +117,19 @@
112
117
  /* A gradient between the shimmer base and sweep colours, moving left to
113
118
  right: with an oversized background image, background-position 100% shows
114
119
  its right edge, 0% its left, so counting down walks the sweep band across
115
- the text. */
120
+ the text.
121
+
122
+ The base is the dimmer colour and the sweep the brighter one, so what
123
+ travels is a highlight rather than a shadow — the same "the sweep only
124
+ brightens it" reading `waiting` has always had. The band is narrow
125
+ (12% of a 250% image, so under a third of the text at a time): spread
126
+ wide it washes the whole label at once and stops reading as movement. */
116
127
  .ds-agent-status__label--shimmer {
117
128
  background-image: linear-gradient(
118
129
  100deg,
119
- var(--ds-agent-shimmer-base) 35%,
130
+ var(--ds-agent-shimmer-base) 44%,
120
131
  var(--ds-agent-shimmer-sweep) 50%,
121
- var(--ds-agent-shimmer-base) 65%
132
+ var(--ds-agent-shimmer-base) 56%
122
133
  );
123
134
  background-size: 250% 100%;
124
135
  background-clip: text;
@@ -189,9 +200,13 @@
189
200
  --ds-agent-matrix-size: var(--icon-size-sm);
190
201
 
191
202
  gap: var(--gap-sm);
203
+
204
+ /* The small metrics, carrying the same title weight down a size: the scale
205
+ has no 14px title face, and dropping to the paragraph weight here would
206
+ make one size of the component read as a heading and the other as prose. */
192
207
  font-family: var(--font-paragraph-sm-family);
193
208
  font-size: var(--font-paragraph-sm-size);
194
- font-weight: var(--font-paragraph-sm-weight);
209
+ font-weight: var(--font-title-body-weight);
195
210
  line-height: var(--font-paragraph-sm-line-height);
196
211
  letter-spacing: var(--font-paragraph-sm-letter-spacing);
197
212
  }
@@ -106,6 +106,15 @@
106
106
  background: var(--color-bg-container-secondary);
107
107
  }
108
108
 
109
+ /* A drawn cover (the `cover` prop) fills the same slot the image would, so a
110
+ grid of cards keeps one cover shape whichever kind it was given. */
111
+ .ds-card--case-study__cover-slot,
112
+ .ds-card--case-study__cover-slot > * {
113
+ display: block;
114
+ width: 100%;
115
+ height: 100%;
116
+ }
117
+
109
118
  .ds-card--case-study__cover-image {
110
119
  width: 100%;
111
120
  height: 100%;
@@ -18,6 +18,12 @@ type CardOwnProps = {
18
18
  href?: string;
19
19
  /** Cover image src */
20
20
  coverSrc?: string;
21
+ /**
22
+ * Cover content rendered in place of the image — for a cover that is drawn
23
+ * rather than photographed (an SVG, a chart, a live preview). Takes
24
+ * precedence over `coverSrc`, and fills the same fixed-ratio slot.
25
+ */
26
+ cover?: React.ReactNode;
21
27
  /** Cover image alt text */
22
28
  coverAlt?: string;
23
29
  /** Path to the company logo shown in the eyebrow */
@@ -13,6 +13,7 @@ const Card = React.forwardRef(
13
13
  className = "",
14
14
  href,
15
15
  coverSrc,
16
+ cover,
16
17
  coverAlt,
17
18
  companyLogo,
18
19
  companyName,
@@ -22,7 +23,7 @@ const Card = React.forwardRef(
22
23
  }, ref) => {
23
24
  if (variant === "case-study") {
24
25
  const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
25
- /* @__PURE__ */ jsx("div", { className: "ds-card--case-study__cover-wrap", children: coverSrc ? /* @__PURE__ */ jsx(
26
+ /* @__PURE__ */ jsx("div", { className: "ds-card--case-study__cover-wrap", children: cover ? /* @__PURE__ */ jsx("div", { className: "ds-card--case-study__cover-slot", children: cover }) : coverSrc ? /* @__PURE__ */ jsx(
26
27
  "img",
27
28
  {
28
29
  src: coverSrc,
@@ -253,6 +253,12 @@
253
253
  opacity: 1;
254
254
  }
255
255
 
256
+ /* showActions pins the row on: the actions are part of the response, not a
257
+ secondary affordance to discover. */
258
+ .ds-chat-message--show-actions .ds-chat-message__actions {
259
+ opacity: 1;
260
+ }
261
+
256
262
  @media (hover: none) {
257
263
  .ds-chat-message__actions {
258
264
  opacity: 1;
@@ -41,8 +41,14 @@ type ChatMessageOwnProps = {
41
41
  pending?: boolean;
42
42
  /** Accessible text announced for the pending state. */
43
43
  pendingLabel?: string;
44
- /** Action row under the content, revealed on hover and keyboard focus (always visible on touch). */
44
+ /** Action row under the content, revealed on hover and keyboard focus (always visible on touch) — `showActions` pins it on. */
45
45
  actions?: React.ReactNode;
46
+ /**
47
+ * Always show the action row instead of revealing it on hover and focus.
48
+ * For surfaces where the actions are part of the response — a copy or
49
+ * feedback row — rather than a secondary affordance.
50
+ */
51
+ showActions?: boolean;
46
52
  /** Footer slot under the content — a sources row, an edited note. */
47
53
  footer?: React.ReactNode;
48
54
  /** Additional CSS classes */
@@ -15,6 +15,7 @@ const ChatMessage = React.forwardRef(
15
15
  pending = false,
16
16
  pendingLabel = "Waiting for a reply",
17
17
  actions,
18
+ showActions = false,
18
19
  footer,
19
20
  className = "",
20
21
  children,
@@ -31,6 +32,7 @@ const ChatMessage = React.forwardRef(
31
32
  grouped ? `${baseClass}--grouped` : "",
32
33
  size === "compact" ? `${baseClass}--compact` : "",
33
34
  pending ? `${baseClass}--pending` : "",
35
+ showActions ? `${baseClass}--show-actions` : "",
34
36
  className
35
37
  ].filter(Boolean).join(" ");
36
38
  return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
@@ -13,12 +13,11 @@
13
13
  --ds-composer-send-size: 40px;
14
14
  background-color: var(--color-input-bg-primary);
15
15
  border: var(--border-xs) solid var(--color-input-border-primary);
16
- /* Corner concentric with the send button it wraps: half the button,
17
- plus the action bar's padding-sm ring, plus the border width
18
- (border-radius applies to the border's outer edge). */
19
- border-radius: calc(
20
- var(--ds-composer-send-size) / 2 + var(--padding-sm) + var(--border-xs)
21
- );
16
+ /* The token's default is concentric with the send button: half the
17
+ button, plus the action bar's padding-sm ring, plus the border width
18
+ (border-radius applies to the border's outer edge). A token rather
19
+ than the calc so radius re-theming reaches this corner too. */
20
+ border-radius: var(--radius-composer);
22
21
  box-shadow: var(--shadow-floating);
23
22
  transition: border-color var(--motion-duration-fast) var(--motion-ease-standard);
24
23
  /* The whole shell is the input affordance — clicking anywhere that is
@@ -59,8 +58,26 @@
59
58
 
60
59
  .ds-composer__content {
61
60
  display: flex;
61
+ /* The textarea keeps its own natural height rather than stretching to the
62
+ zone's — the measurement below reads it, so a stretched textarea would
63
+ feed its own height back in and the zone could never shrink. */
64
+ align-items: flex-start;
65
+ box-sizing: border-box;
62
66
  width: 100%;
63
67
  padding: var(--padding-md) var(--padding-lg) var(--padding-sm);
68
+ /* Growing and shrinking is animated, which needs two pixel heights to run
69
+ between — `auto` has nothing to interpolate. So the zone states its own
70
+ height: the measured textarea (published as --ds-composer-text-height on
71
+ every resize) plus the block padding it owns. The fallback is a single
72
+ row, which is what the shell renders before the first measurement. */
73
+ height: calc(
74
+ var(--ds-composer-text-height, var(--font-paragraph-line-height)) + var(--padding-md) +
75
+ var(--padding-sm)
76
+ );
77
+ /* Mid-transition the textarea is already at its new height, so the extra
78
+ line is clipped on the way in and released on the way out. */
79
+ overflow: hidden;
80
+ transition: height var(--motion-duration-instant) var(--motion-ease-emphasized);
64
81
  }
65
82
 
66
83
  .ds-composer__textarea {
@@ -59,6 +59,9 @@ export interface ComposerProps extends ComposerOwnProps, Omit<React.ComponentPro
59
59
  * The textarea grows with its content up to `maxRows`, then scrolls
60
60
  * internally. Where the browser supports `field-sizing: content` the sizing
61
61
  * is fully native; elsewhere a measurement effect keeps the height in step.
62
+ * Either way the text zone glides between the two heights rather than
63
+ * snapping — 75ms, short enough to soften the step without reading as an
64
+ * animation. The action bar's buttons never move relative to the bar.
62
65
  *
63
66
  * Forwards a ref to the underlying `<textarea>` and spreads unrecognised
64
67
  * props onto it; `className` lands on the shell.
@@ -23,6 +23,7 @@ const Composer = React.forwardRef(
23
23
  }, ref) => {
24
24
  const baseClass = "ds-composer";
25
25
  const textareaRef = useRef(null);
26
+ const contentRef = useRef(null);
26
27
  const isControlled = value !== void 0;
27
28
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? "");
28
29
  const currentValue = isControlled ? value : uncontrolledValue;
@@ -40,6 +41,16 @@ const Composer = React.forwardRef(
40
41
  node.style.height = "auto";
41
42
  node.style.height = `${node.scrollHeight}px`;
42
43
  }, [currentValue]);
44
+ useLayoutEffect(() => {
45
+ const node = textareaRef.current;
46
+ const content = contentRef.current;
47
+ if (!node || !content) return;
48
+ const observer = new ResizeObserver(() => {
49
+ content.style.setProperty("--ds-composer-text-height", `${node.offsetHeight}px`);
50
+ });
51
+ observer.observe(node);
52
+ return () => observer.disconnect();
53
+ }, []);
43
54
  const submit = () => {
44
55
  if (streaming || !canSend) return;
45
56
  onSubmit?.(currentValue);
@@ -71,7 +82,7 @@ const Composer = React.forwardRef(
71
82
  onClick: handleShellClick,
72
83
  children: [
73
84
  attachments && /* @__PURE__ */ jsx("div", { className: `${baseClass}__attachments`, children: attachments }),
74
- /* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, children: /* @__PURE__ */ jsx(
85
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__content`, ref: contentRef, children: /* @__PURE__ */ jsx(
75
86
  "textarea",
76
87
  {
77
88
  ...rest,
@@ -192,8 +192,17 @@
192
192
  TABLES
193
193
  ============================================ */
194
194
 
195
+ /* The table is its own scroll container. Prose styles markup it does not
196
+ render, so it cannot wrap the table in a scrolling div — `display: block`
197
+ makes the table element itself the scroller. The cost is deliberate: the
198
+ table sizes to its content instead of filling the container, because the
199
+ inner anonymous table box a block table generates cannot be stretched.
200
+ A clipped or shattered table is the worse failure. */
195
201
  .ds-prose table {
196
- width: 100%;
202
+ display: block;
203
+ width: max-content;
204
+ max-width: 100%;
205
+ overflow-x: auto;
197
206
  border-collapse: collapse;
198
207
  }
199
208
 
@@ -207,10 +216,18 @@
207
216
  text-align: start;
208
217
  }
209
218
 
219
+ /* Cells never break words, whatever the surrounding context does. An
220
+ ancestor that sets `overflow-wrap: anywhere` (ChatMessage does, so a long
221
+ URL cannot widen a bubble) otherwise inherits in and collapses every
222
+ column's minimum width to one character, which lets auto table layout
223
+ squeeze a heading into "Co / m / pa / ny" rather than let the table reach
224
+ its natural width. Holding the floor is what gives the scroll above
225
+ something to scroll. */
210
226
  .ds-prose th,
211
227
  .ds-prose td {
212
228
  padding: var(--padding-xs) var(--padding-sm-md);
213
229
  border-block-end: var(--border-xs) solid var(--color-divider);
230
+ overflow-wrap: normal;
214
231
  }
215
232
 
216
233
  /* ============================================
@@ -128,12 +128,16 @@
128
128
  align-items: center;
129
129
  }
130
130
 
131
+ /* The band travels dim → bright → dim, and stays narrow, matching
132
+ AgentStatus's sweep stop for stop: the two sit in the same conversation,
133
+ so a shimmer that read differently here would look like a second effect
134
+ rather than the same one. */
131
135
  .ds-reasoning--streaming .ds-reasoning__summary:not(.ds-reasoning__summary--node) {
132
136
  background-image: linear-gradient(
133
137
  100deg,
134
- var(--color-text-primary) 35%,
135
- var(--color-text-tertiary) 50%,
136
- var(--color-text-primary) 65%
138
+ var(--color-text-tertiary) 44%,
139
+ var(--color-text-primary) 50%,
140
+ var(--color-text-tertiary) 56%
137
141
  );
138
142
  background-size: 250% 100%;
139
143
  background-clip: text;
@@ -183,7 +187,18 @@
183
187
  the thought-for line never shifts the trace. */
184
188
  margin-left: calc(var(--icon-size-md) / 2 - var(--border-md) / 2);
185
189
  padding-left: calc(var(--icon-size-md) / 2 + var(--gap-sm-md) - var(--border-md) / 2);
186
- border-left: var(--border-md) solid var(--color-divider);
190
+ /* The rail dissolves at its foot — the Timeline's end-of-line treatment.
191
+ A border can't carry a gradient, so the border only reserves the
192
+ geometry (transparent, same width) and the stripe is painted under it:
193
+ a border-box background sized to the border's own column. */
194
+ border-left: var(--border-md) solid transparent;
195
+ background:
196
+ linear-gradient(
197
+ to bottom,
198
+ var(--color-divider) calc(100% - var(--gap-lg) * 2),
199
+ var(--color-bg-container-primary-transparent)
200
+ )
201
+ left top / var(--border-md) 100% no-repeat border-box;
187
202
  color: var(--color-text-secondary);
188
203
  font-family: var(--font-paragraph-family);
189
204
  font-size: var(--font-paragraph-size);
@@ -0,0 +1,24 @@
1
+ /* ShaderField — the canvas only.
2
+ Placement, clipping and any fade-to-page mask belong to the positioned
3
+ ancestor the caller provides: where the field sits is a layout decision,
4
+ and baking one in would make the component unusable anywhere else. */
5
+
6
+ .ds-shader-field {
7
+ position: absolute;
8
+ inset: 0;
9
+ width: 100%;
10
+ height: 100%;
11
+ display: block;
12
+ /* Scenery, never a target — a full-bleed canvas that swallowed clicks would
13
+ make whatever it covers unreachable. */
14
+ pointer-events: none;
15
+ /* Reveals only once a context is live and the first frame is drawn, so a
16
+ machine that cannot run the shader never shows an empty canvas — it stays
17
+ on the caller's fallback instead. */
18
+ opacity: 0;
19
+ transition: opacity var(--motion-duration-slower) var(--motion-ease-entrance);
20
+ }
21
+
22
+ .ds-shader-field[data-status='active'] {
23
+ opacity: 1;
24
+ }
@@ -0,0 +1,68 @@
1
+ import { default as React } from 'react';
2
+ import { ShaderBlob, ShaderParams } from './useShaderField';
3
+ export type { ShaderBlob, ShaderParams, ShaderFieldParams, ShaderFieldState, } from './useShaderField';
4
+ export { useShaderField, DEFAULT_SHADER_BLOBS, DEFAULT_SHADER_PARAMS } from './useShaderField';
5
+ export { BLOB_COUNT } from './field.glsl';
6
+ /**
7
+ * How the renderer has resolved.
8
+ *
9
+ * `pending` is the one that matters to a caller: the context is still coming
10
+ * up, so neither the field nor a fallback should be painted yet — showing a
11
+ * fallback here and swapping it a frame later reads as two backgrounds loading
12
+ * in sequence.
13
+ */
14
+ export type ShaderFieldStatus = 'pending' | 'active' | 'unavailable';
15
+ /** Props owned by ShaderField itself — everything else falls through to the canvas. */
16
+ type ShaderFieldOwnProps = {
17
+ /**
18
+ * Field parameters, merged over the shipped defaults, so passing one
19
+ * property changes one thing. Changing them never rebuilds the GL state.
20
+ */
21
+ params?: Partial<ShaderParams>;
22
+ /**
23
+ * The light sources. Defaults to an eight-source composition on the core
24
+ * accent tokens. Fewer than `BLOB_COUNT` entries is fine — unused slots park
25
+ * off-field; more are ignored, because the shader's uniform arrays are
26
+ * fixed-size.
27
+ */
28
+ blobs?: readonly ShaderBlob[];
29
+ /**
30
+ * False keeps WebGL entirely unmounted and reports `unavailable`, so the
31
+ * caller's fallback is the whole background. This is the kill switch: a
32
+ * config lever, or an A/B against the fallback.
33
+ */
34
+ enabled?: boolean;
35
+ /**
36
+ * Called whenever the renderer's status changes. Drive the fallback layer's
37
+ * visibility from this — the component deliberately does not own one, since
38
+ * what to paint instead is a design decision, not a rendering one.
39
+ */
40
+ onStatusChange?: (status: ShaderFieldStatus) => void;
41
+ /** Additional CSS classes */
42
+ className?: string;
43
+ };
44
+ export interface ShaderFieldProps extends ShaderFieldOwnProps, Omit<React.ComponentPropsWithoutRef<'canvas'>, keyof ShaderFieldOwnProps> {
45
+ }
46
+ /**
47
+ * ShaderField paints an ambient WebGL2 field of soft Gaussian light sources,
48
+ * each sampling a semantic colour token at runtime — so the whole thing
49
+ * re-themes with `data-theme`, and with any scoped custom-property override,
50
+ * without the caller wiring anything up.
51
+ *
52
+ * It is scenery, not interface: nothing about it is a signal, so its motion
53
+ * cannot be mistaken for feedback. Two consequences follow, and both are
54
+ * handled here rather than left to the caller.
55
+ *
56
+ * It honours reduced motion in JavaScript. The system's motion tokens collapse
57
+ * under `prefers-reduced-motion`, but that guard is CSS-only and cannot see a
58
+ * `requestAnimationFrame` loop; a GPU-driven surface has to check the query
59
+ * itself. This one renders a single static frame and never starts its loop.
60
+ *
61
+ * And it always reports how it resolved. No WebGL2, a blocked or lost GPU
62
+ * context, a renderer that stalls before its first frame, or `enabled={false}`
63
+ * all arrive at `unavailable` through `onStatusChange`, so the caller can keep
64
+ * a plain CSS fallback painted underneath. The canvas fills its positioned
65
+ * ancestor and is inert to pointer events; give that ancestor the placement,
66
+ * the clipping and any mask.
67
+ */
68
+ export declare const ShaderField: React.ForwardRefExoticComponent<ShaderFieldProps & React.RefAttributes<HTMLCanvasElement>>;