@robr0/design-system 0.5.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 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
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
 
@@ -32,7 +33,7 @@ Accordion · Agent status · AI button · Alert · Alert dialog · App layout ·
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
 
@@ -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
@@ -332,8 +332,16 @@
332
332
  bottom: calc(8px - var(--gap-lg)); /* stop 8px above the next logo */
333
333
  }
334
334
 
335
- /* Last entry has no following logo — stop the bar at its own content */
335
+ /* Last entry has no following logo — the bar runs to its own content's end
336
+ and fades out: the hairline treatment (1px, dissolving to the transparent
337
+ container colour) that marks the end of the timeline. */
336
338
  .ds-timeline--company .ds-timeline__item:last-child::before {
337
339
  display: block;
338
340
  bottom: 0;
341
+ width: var(--border-xs);
342
+ background: linear-gradient(
343
+ to bottom,
344
+ var(--color-divider),
345
+ var(--color-bg-container-primary-transparent)
346
+ );
339
347
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robr0/design-system",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "An AI-ready React design system: accessible components on composable tokens, light/dark theming, and CSS-variable overrides.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -174,6 +174,7 @@
174
174
  "--padding-xxxs"
175
175
  ],
176
176
  "radius": [
177
+ "--radius-composer",
177
178
  "--radius-full",
178
179
  "--radius-lg",
179
180
  "--radius-md",
@@ -174,6 +174,7 @@ declare const _default: {
174
174
  "--padding-xxxs"
175
175
  ],
176
176
  "radius": [
177
+ "--radius-composer",
177
178
  "--radius-full",
178
179
  "--radius-lg",
179
180
  "--radius-md",
@@ -1,4 +1,4 @@
1
- const categories = { "colour": ["--color-action-icon-active", "--color-action-icon-default", "--color-action-passive-bg", "--color-action-passive-bg-active", "--color-action-passive-bg-hover", "--color-action-passive-text", "--color-action-primary-bg", "--color-action-primary-bg-active", "--color-action-primary-bg-hover", "--color-action-primary-border", "--color-action-primary-border-secondary", "--color-action-primary-border-tertiary", "--color-action-primary-text", "--color-action-primary-text-active", "--color-action-primary-text-secondary", "--color-action-primary-text-tertiary", "--color-ai-gradient-end", "--color-ai-gradient-mid", "--color-ai-gradient-start", "--color-bg-container-border", "--color-bg-container-inverse", "--color-bg-container-primary", "--color-bg-container-primary-semi", "--color-bg-container-primary-transparent", "--color-bg-container-secondary", "--color-bg-container-tertiary", "--color-bg-page-inverse", "--color-bg-page-primary", "--color-chart-contribution-0", "--color-chart-contribution-1", "--color-chart-contribution-2", "--color-chart-contribution-3", "--color-chart-contribution-4", "--color-chat-bubble-received-bg", "--color-chat-bubble-received-text", "--color-chat-bubble-sent-bg", "--color-chat-bubble-sent-text", "--color-control-thumb", "--color-core-accent-amber", "--color-core-accent-cobalt", "--color-core-accent-coral", "--color-core-accent-gold", "--color-core-accent-mint", "--color-core-accent-violet", "--color-core-ui-primary", "--color-core-ui-secondary", "--color-divider", "--color-icon-primary", "--color-icon-secondary", "--color-input-bg-disabled", "--color-input-bg-primary", "--color-input-border-disabled", "--color-input-border-hover", "--color-input-border-primary", "--color-input-border-selected", "--color-input-text-disabled", "--color-input-text-inverse", "--color-input-text-placeholder", "--color-input-text-primary", "--color-scrim", "--color-status-error-bg", "--color-status-error-border", "--color-status-error-text", "--color-status-info-bg", "--color-status-info-border", "--color-status-info-text", "--color-status-neutral-bg", "--color-status-neutral-border", "--color-status-neutral-text", "--color-status-positive-bg", "--color-status-positive-border", "--color-status-positive-text", "--color-status-warning-bg", "--color-status-warning-border", "--color-status-warning-text", "--color-text-inverse", "--color-text-on-inverse", "--color-text-primary", "--color-text-secondary", "--color-text-tertiary"], "typography": ["--font-display-1-family", "--font-display-1-letter-spacing", "--font-display-1-line-height", "--font-display-1-size", "--font-display-1-weight", "--font-display-2-family", "--font-display-2-letter-spacing", "--font-display-2-line-height", "--font-display-2-size", "--font-display-2-weight", "--font-family-code", "--font-family-primary", "--font-heading-1-family", "--font-heading-1-letter-spacing", "--font-heading-1-line-height", "--font-heading-1-size", "--font-heading-1-weight", "--font-heading-2-family", "--font-heading-2-letter-spacing", "--font-heading-2-line-height", "--font-heading-2-size", "--font-heading-2-weight", "--font-heading-3-family", "--font-heading-3-letter-spacing", "--font-heading-3-line-height", "--font-heading-3-size", "--font-heading-3-weight", "--font-mega-1-family", "--font-mega-1-letter-spacing", "--font-mega-1-line-height", "--font-mega-1-size", "--font-mega-1-weight", "--font-mega-2-family", "--font-mega-2-letter-spacing", "--font-mega-2-line-height", "--font-mega-2-size", "--font-mega-2-weight", "--font-paragraph-em-family", "--font-paragraph-em-letter-spacing", "--font-paragraph-em-line-height", "--font-paragraph-em-size", "--font-paragraph-em-weight", "--font-paragraph-family", "--font-paragraph-letter-spacing", "--font-paragraph-line-height", "--font-paragraph-size", "--font-paragraph-sm-em-family", "--font-paragraph-sm-em-letter-spacing", "--font-paragraph-sm-em-line-height", "--font-paragraph-sm-em-size", "--font-paragraph-sm-em-weight", "--font-paragraph-sm-family", "--font-paragraph-sm-letter-spacing", "--font-paragraph-sm-line-height", "--font-paragraph-sm-size", "--font-paragraph-sm-weight", "--font-paragraph-weight", "--font-sub-display-family", "--font-sub-display-letter-spacing", "--font-sub-display-line-height", "--font-sub-display-size", "--font-sub-display-weight", "--font-title-body-family", "--font-title-body-letter-spacing", "--font-title-body-line-height", "--font-title-body-size", "--font-title-body-weight"], "spacing": ["--gap-lg", "--gap-md", "--gap-sm", "--gap-sm-md", "--gap-xl", "--gap-xs", "--gap-xxl", "--gap-xxs", "--gap-xxxl", "--gap-xxxxl", "--padding-lg", "--padding-md", "--padding-sm", "--padding-sm-md", "--padding-xl", "--padding-xs", "--padding-xxl", "--padding-xxs", "--padding-xxxs"], "radius": ["--radius-full", "--radius-lg", "--radius-md", "--radius-sm", "--radius-xl", "--radius-xs", "--radius-xxl", "--radius-xxs"], "border": ["--border-md", "--border-xs"], "shadow": ["--shadow-floating", "--shadow-modal"], "icons": ["--icon-size-lg", "--icon-size-md", "--icon-size-sm", "--icon-size-xl"], "motion": ["--motion-duration-base", "--motion-duration-deliberate", "--motion-duration-fast", "--motion-duration-loop-matrix", "--motion-duration-loop-shimmer", "--motion-duration-loop-spin", "--motion-duration-slow", "--motion-duration-slower", "--motion-ease-emphasized", "--motion-ease-entrance", "--motion-ease-linear", "--motion-ease-spring", "--motion-ease-standard"] };
1
+ const categories = { "colour": ["--color-action-icon-active", "--color-action-icon-default", "--color-action-passive-bg", "--color-action-passive-bg-active", "--color-action-passive-bg-hover", "--color-action-passive-text", "--color-action-primary-bg", "--color-action-primary-bg-active", "--color-action-primary-bg-hover", "--color-action-primary-border", "--color-action-primary-border-secondary", "--color-action-primary-border-tertiary", "--color-action-primary-text", "--color-action-primary-text-active", "--color-action-primary-text-secondary", "--color-action-primary-text-tertiary", "--color-ai-gradient-end", "--color-ai-gradient-mid", "--color-ai-gradient-start", "--color-bg-container-border", "--color-bg-container-inverse", "--color-bg-container-primary", "--color-bg-container-primary-semi", "--color-bg-container-primary-transparent", "--color-bg-container-secondary", "--color-bg-container-tertiary", "--color-bg-page-inverse", "--color-bg-page-primary", "--color-chart-contribution-0", "--color-chart-contribution-1", "--color-chart-contribution-2", "--color-chart-contribution-3", "--color-chart-contribution-4", "--color-chat-bubble-received-bg", "--color-chat-bubble-received-text", "--color-chat-bubble-sent-bg", "--color-chat-bubble-sent-text", "--color-control-thumb", "--color-core-accent-amber", "--color-core-accent-cobalt", "--color-core-accent-coral", "--color-core-accent-gold", "--color-core-accent-mint", "--color-core-accent-violet", "--color-core-ui-primary", "--color-core-ui-secondary", "--color-divider", "--color-icon-primary", "--color-icon-secondary", "--color-input-bg-disabled", "--color-input-bg-primary", "--color-input-border-disabled", "--color-input-border-hover", "--color-input-border-primary", "--color-input-border-selected", "--color-input-text-disabled", "--color-input-text-inverse", "--color-input-text-placeholder", "--color-input-text-primary", "--color-scrim", "--color-status-error-bg", "--color-status-error-border", "--color-status-error-text", "--color-status-info-bg", "--color-status-info-border", "--color-status-info-text", "--color-status-neutral-bg", "--color-status-neutral-border", "--color-status-neutral-text", "--color-status-positive-bg", "--color-status-positive-border", "--color-status-positive-text", "--color-status-warning-bg", "--color-status-warning-border", "--color-status-warning-text", "--color-text-inverse", "--color-text-on-inverse", "--color-text-primary", "--color-text-secondary", "--color-text-tertiary"], "typography": ["--font-display-1-family", "--font-display-1-letter-spacing", "--font-display-1-line-height", "--font-display-1-size", "--font-display-1-weight", "--font-display-2-family", "--font-display-2-letter-spacing", "--font-display-2-line-height", "--font-display-2-size", "--font-display-2-weight", "--font-family-code", "--font-family-primary", "--font-heading-1-family", "--font-heading-1-letter-spacing", "--font-heading-1-line-height", "--font-heading-1-size", "--font-heading-1-weight", "--font-heading-2-family", "--font-heading-2-letter-spacing", "--font-heading-2-line-height", "--font-heading-2-size", "--font-heading-2-weight", "--font-heading-3-family", "--font-heading-3-letter-spacing", "--font-heading-3-line-height", "--font-heading-3-size", "--font-heading-3-weight", "--font-mega-1-family", "--font-mega-1-letter-spacing", "--font-mega-1-line-height", "--font-mega-1-size", "--font-mega-1-weight", "--font-mega-2-family", "--font-mega-2-letter-spacing", "--font-mega-2-line-height", "--font-mega-2-size", "--font-mega-2-weight", "--font-paragraph-em-family", "--font-paragraph-em-letter-spacing", "--font-paragraph-em-line-height", "--font-paragraph-em-size", "--font-paragraph-em-weight", "--font-paragraph-family", "--font-paragraph-letter-spacing", "--font-paragraph-line-height", "--font-paragraph-size", "--font-paragraph-sm-em-family", "--font-paragraph-sm-em-letter-spacing", "--font-paragraph-sm-em-line-height", "--font-paragraph-sm-em-size", "--font-paragraph-sm-em-weight", "--font-paragraph-sm-family", "--font-paragraph-sm-letter-spacing", "--font-paragraph-sm-line-height", "--font-paragraph-sm-size", "--font-paragraph-sm-weight", "--font-paragraph-weight", "--font-sub-display-family", "--font-sub-display-letter-spacing", "--font-sub-display-line-height", "--font-sub-display-size", "--font-sub-display-weight", "--font-title-body-family", "--font-title-body-letter-spacing", "--font-title-body-line-height", "--font-title-body-size", "--font-title-body-weight"], "spacing": ["--gap-lg", "--gap-md", "--gap-sm", "--gap-sm-md", "--gap-xl", "--gap-xs", "--gap-xxl", "--gap-xxs", "--gap-xxxl", "--gap-xxxxl", "--padding-lg", "--padding-md", "--padding-sm", "--padding-sm-md", "--padding-xl", "--padding-xs", "--padding-xxl", "--padding-xxs", "--padding-xxxs"], "radius": ["--radius-composer", "--radius-full", "--radius-lg", "--radius-md", "--radius-sm", "--radius-xl", "--radius-xs", "--radius-xxl", "--radius-xxs"], "border": ["--border-md", "--border-xs"], "shadow": ["--shadow-floating", "--shadow-modal"], "icons": ["--icon-size-lg", "--icon-size-md", "--icon-size-sm", "--icon-size-xl"], "motion": ["--motion-duration-base", "--motion-duration-deliberate", "--motion-duration-fast", "--motion-duration-loop-matrix", "--motion-duration-loop-shimmer", "--motion-duration-loop-spin", "--motion-duration-slow", "--motion-duration-slower", "--motion-ease-emphasized", "--motion-ease-entrance", "--motion-ease-linear", "--motion-ease-spring", "--motion-ease-standard"] };
2
2
  const registry = {
3
3
  categories
4
4
  };
@@ -144,6 +144,7 @@
144
144
  --radius-xl: var(--primitive-radius-xl);
145
145
  --radius-xxl: var(--primitive-radius-xxl);
146
146
  --radius-full: var(--primitive-radius-full);
147
+ --radius-composer: var(--primitive-radius-composer);
147
148
 
148
149
  --padding-xxxs: var(--primitive-padding-xxxs);
149
150
  --padding-xxs: var(--primitive-padding-xxs);
@@ -136,6 +136,11 @@
136
136
  --primitive-radius-xl: 24px;
137
137
  --primitive-radius-xxl: 48px;
138
138
  --primitive-radius-full: 999px;
139
+ /* The composer shell's corner: concentric with the 40px send button it
140
+ wraps (half the button + the padding-sm ring + the border width).
141
+ Held as a resolved constant so consumers re-theme it like any other
142
+ radius step. */
143
+ --primitive-radius-composer: 29px;
139
144
 
140
145
  /* Padding */
141
146
  --primitive-padding-xxxs: 2px;