-
Notifications
You must be signed in to change notification settings - Fork 1
Sync the design system to claude.ai/design #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| #!/usr/bin/env node | ||
| // Builds the single compiled stylesheet the design-sync converter ships as | ||
| // `cssEntry`. The site's CSS can't be consumed from source: `app/globals.css` | ||
| // starts with `@import 'tailwindcss'` (needs the Tailwind v4 compiler) and the | ||
| // brand fonts come from `next/font/google`, which only emits @font-face at | ||
| // Next build time. | ||
| // | ||
| // Output: web/.ds-css/compiled.css — Tailwind utilities for the whole | ||
| // component tree + brand tokens + globals + self-hosted @font-face rules, | ||
| // with font URLs pointing at ./fonts/ next to it. | ||
|
|
||
| import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; | ||
| import { dirname, join, resolve } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..'); | ||
| const WEB = join(REPO, 'web'); | ||
| const OUT_DIR = join(WEB, '.ds-css'); | ||
| const NEXT_STATIC = join(WEB, '.next', 'static'); | ||
|
|
||
| if (!existsSync(NEXT_STATIC)) { | ||
| console.error( | ||
| `[CSS_GEN] ${NEXT_STATIC} missing — run \`npm --workspace web run build\` first ` + | ||
| `(next/font emits @font-face only at build time).`, | ||
| ); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| rmSync(OUT_DIR, { recursive: true, force: true }); | ||
| mkdirSync(join(OUT_DIR, 'fonts'), { recursive: true }); | ||
|
|
||
| // ── 1. Harvest @font-face rules from the Next build output ──────────────── | ||
| // next/font self-hosts Google Fonts into .next/static/media and emits the | ||
| // @font-face rules into one of the built stylesheets. | ||
| const cssFiles = readdirSync(join(NEXT_STATIC, 'css')).filter((f) => f.endsWith('.css')); | ||
| const faces = []; | ||
| for (const f of cssFiles) { | ||
| const text = readFileSync(join(NEXT_STATIC, 'css', f), 'utf8'); | ||
| for (const m of text.matchAll(/@font-face\s*\{[^}]*\}/g)) faces.push(m[0]); | ||
| } | ||
| if (!faces.length) { | ||
| console.error('[CSS_GEN] no @font-face rules found in the Next build output'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| // Copy each referenced woff2 next to the stylesheet and repoint the url(). | ||
| const copied = new Set(); | ||
| const localFaces = [...new Set(faces)].map((rule) => | ||
| rule.replace(/url\((\/_next\/static\/media\/([^)]+))\)/g, (_all, _abs, file) => { | ||
| const src = join(NEXT_STATIC, 'media', file); | ||
| if (!existsSync(src)) { | ||
| console.error(`[CSS_GEN] referenced font missing: ${src}`); | ||
| process.exit(1); | ||
| } | ||
| if (!copied.has(file)) { | ||
| cpSync(src, join(OUT_DIR, 'fonts', file)); | ||
| copied.add(file); | ||
| } | ||
| return `url(./fonts/${file})`; | ||
| }), | ||
| ); | ||
|
|
||
| const families = [...new Set(localFaces.map((r) => /font-family:\s*([^;]+);/.exec(r)?.[1]?.trim()))]; | ||
|
|
||
| // The site wires families to CSS variables through next/font's generated | ||
| // classNames on <html>; nothing in the component tree defines them. Bind them | ||
| // here so components render in the real brand faces outside Next. | ||
| const fontsCss = `/* Generated by .design-sync/build-css.mjs — do not edit. */ | ||
| ${localFaces.join('\n')} | ||
|
|
||
| :root { | ||
| --font-geist-sans: 'Inter', ui-sans-serif, system-ui, sans-serif; | ||
| --font-heading: 'Sora', ui-sans-serif, system-ui, sans-serif; | ||
| --font-geist-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace; | ||
|
Comment on lines
+71
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When this runs against Next's Useful? React with 👍 / 👎. |
||
| } | ||
| `; | ||
| writeFileSync(join(OUT_DIR, 'fonts.css'), fontsCss); | ||
|
|
||
| // ── 2. Compile the Tailwind + globals + brand entry ─────────────────────── | ||
| // Explicit @source: automatic detection is rooted at the compiler's cwd, which | ||
| // is not the component tree we actually ship. | ||
| writeFileSync( | ||
| join(OUT_DIR, 'entry.css'), | ||
| `@import '../app/globals.css'; | ||
| @source '../components/**/*.{ts,tsx}'; | ||
| @source '../app/**/*.{ts,tsx}'; | ||
| @import './fonts.css'; | ||
| `, | ||
| ); | ||
|
|
||
| // Bare specifiers resolve from this script's location up into the repo root | ||
| // node_modules, where the site's own postcss + Tailwind v4 are installed — | ||
| // the compiler that produces the CSS is the one the site itself ships with. | ||
| const postcss = (await import('postcss')).default; | ||
| const tailwind = (await import('@tailwindcss/postcss')).default; | ||
|
|
||
| const entryPath = join(OUT_DIR, 'entry.css'); | ||
| const result = await postcss([tailwind()]).process(readFileSync(entryPath, 'utf8'), { | ||
| from: entryPath, | ||
| to: join(OUT_DIR, 'compiled.css'), | ||
| }); | ||
| writeFileSync(join(OUT_DIR, 'compiled.css'), result.css); | ||
|
|
||
| console.error( | ||
| `[CSS_GEN] compiled.css ${(result.css.length / 1024).toFixed(0)}KB · ` + | ||
| `${copied.size} font files · families: ${families.join(', ')}`, | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| #!/usr/bin/env node | ||
| // The build entry point for this repo. Use this, never `package-build.mjs` | ||
| // directly. | ||
| // | ||
| // `package-build.mjs` rewrites the output directory and preserves only its own | ||
| // outputs, so it deletes `ds-bundle/integration-logos/` every run. Components | ||
| // load those marks by absolute URL, so a build followed by a capture silently | ||
| // produces blank tiles while any existing grade still reads green — the grade | ||
| // file and the artifact disagree and nothing in the pipeline notices. That | ||
| // regression happened four times before this wrapper existed. | ||
| // | ||
| // Ordering that matters: build -> copy assets -> capture -> upload, with no | ||
| // build after the copy. | ||
| // | ||
| // node .design-sync/build.mjs --config .design-sync/config.json \ | ||
| // --node-modules ./node_modules --out ./ds-bundle | ||
|
|
||
| import { spawnSync } from 'node:child_process'; | ||
| import { dirname, join, resolve } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..'); | ||
| const argv = process.argv.slice(2); | ||
| const outIdx = argv.indexOf('--out'); | ||
| const OUT = outIdx >= 0 ? argv[outIdx + 1] : join(REPO, 'ds-bundle'); | ||
|
|
||
| const build = spawnSync(process.execPath, [join(REPO, '.ds-sync/package-build.mjs'), ...argv], { | ||
| stdio: 'inherit', | ||
| cwd: REPO, | ||
| }); | ||
| if (build.status !== 0) process.exit(build.status ?? 1); | ||
|
|
||
| const assets = spawnSync(process.execPath, [join(REPO, '.design-sync/copy-assets.mjs'), OUT], { | ||
| stdio: 'inherit', | ||
| cwd: REPO, | ||
| }); | ||
| process.exit(assets.status ?? 1); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| { | ||
| "projectId": "3abe2a61-fb2c-4167-8142-92840a218217", | ||
| "shape": "package", | ||
| "pkg": "web", | ||
| "globalName": "AgentRelay", | ||
| "entry": "web/.design-sync-entry.tsx", | ||
| "srcDir": "components", | ||
| "tsconfig": ".design-sync-tsconfig.json", | ||
| "cssEntry": ".ds-css/compiled.css", | ||
| "buildCmd": "npm --workspace web run build && node .design-sync/build-css.mjs", | ||
| "componentSrcMap": { | ||
| "Badge": "components/ui/badge.tsx", | ||
| "Button": "components/ui/button.tsx", | ||
| "Input": "components/ui/input.tsx", | ||
| "Card": "components/ui/card.tsx", | ||
| "CardHeader": "components/ui/card.tsx", | ||
| "CardFooter": "components/ui/card.tsx", | ||
| "CardTitle": "components/ui/card.tsx", | ||
| "CardDescription": "components/ui/card.tsx", | ||
| "CardContent": "components/ui/card.tsx", | ||
| "DocsCard": "components/docs/Card.tsx", | ||
| "CardGroup": "components/docs/CardGroup.tsx", | ||
| "BannerLink": "components/docs/BannerLink.tsx", | ||
| "CodeGroup": "components/docs/CodeGroup.tsx", | ||
| "CopyCodeButton": "components/docs/CopyCodeButton.tsx", | ||
| "DocsLanguageProvider": "components/docs/DocsLanguageContext.tsx", | ||
| "DocsNav": "components/docs/DocsNav.tsx", | ||
| "DocsProductSwitcher": "components/docs/DocsNav.tsx", | ||
| "DocsPageActions": "components/docs/DocsPageActions.tsx", | ||
| "DocsSearch": "components/docs/DocsSearch.tsx", | ||
| "DocsVersionSelect": "components/docs/DocsVersionSelect.tsx", | ||
| "IntegrationGrid": "components/docs/IntegrationGrid.tsx", | ||
| "LegacySpawnOptionsTable": "components/docs/LegacySpawnOptionsTable.tsx", | ||
| "Note": "components/docs/Note.tsx", | ||
| "Warning": "components/docs/Warning.tsx", | ||
| "TableOfContents": "components/docs/TableOfContents.tsx", | ||
| "BlogTableOfContents": "components/blog/BlogTableOfContents.tsx", | ||
| "SiteNav": "components/SiteNav.tsx", | ||
| "LogoIcon": "components/SiteNav.tsx", | ||
| "LogoWordmark": "components/SiteNav.tsx", | ||
| "SiteFooter": "components/SiteFooter.tsx", | ||
| "WaitlistForm": "components/WaitlistForm.tsx", | ||
| "InstallCommand": "components/InstallCommand.tsx", | ||
| "AgentSetupPrompt": "components/InstallCommand.tsx", | ||
| "CopyInstructionsButton": "components/CopyInstructionsButton.tsx", | ||
| "DocsGitHubStarsBadge": "components/DocsGitHubStarsBadge.tsx", | ||
| "SdkCodeExample": "components/SdkCodeExample.tsx", | ||
| "AgentToolLogo": "components/AgentToolLogos.tsx", | ||
| "FadeIn": "components/FadeIn.tsx", | ||
| "RelayAnimation": "components/RelayAnimation.tsx", | ||
| "MessageRelayAnimation": "components/MessageRelayAnimation.tsx", | ||
| "NodeRelayAnimation": "components/NodeRelayAnimation.tsx", | ||
| "ChannelMessagesPreview": "components/ChannelMessagesPreview.tsx", | ||
| "AgentArt": "components/agents/AgentArt.tsx", | ||
| "BuildYourOwn": "components/agents/BuildYourOwn.tsx", | ||
| "ForkAgentButton": "components/agents/ForkAgentButton.tsx", | ||
| "IntegrationLogos": "components/agents/IntegrationLogos.tsx", | ||
| "BrandTheme": null | ||
| }, | ||
| "docsDir": "../.design-sync/groups", | ||
| "readmeHeader": ".design-sync/conventions.md", | ||
| "provider": { | ||
| "component": "BrandTheme", | ||
| "props": { | ||
| "theme": "dark" | ||
| } | ||
| }, | ||
| "overrides": { | ||
| "SiteNav": { | ||
| "cardMode": "single", | ||
| "primaryStory": "DocsHeader", | ||
| "viewport": "1280x720" | ||
| }, | ||
| "CardGroup": { | ||
| "viewport": "1240x900" | ||
| }, | ||
| "BannerLink": { | ||
| "viewport": "1240x900" | ||
| }, | ||
| "DocsPageActions": { | ||
| "viewport": "1240x900" | ||
| }, | ||
| "TableOfContents": { | ||
| "viewport": "1240x900" | ||
| }, | ||
| "DocsCard": { | ||
| "viewport": "1240x900" | ||
| }, | ||
| "CodeGroup": { | ||
| "viewport": "1240x900" | ||
| }, | ||
| "LegacySpawnOptionsTable": { | ||
| "viewport": "1240x900" | ||
| }, | ||
| "IntegrationGrid": { | ||
| "viewport": "1240x900" | ||
| }, | ||
| "DocsNav": { | ||
| "viewport": "1240x900" | ||
| }, | ||
| "DocsSearch": { | ||
| "viewport": "1240x900" | ||
| }, | ||
| "BlogTableOfContents": { | ||
| "viewport": "1240x900" | ||
| }, | ||
| "DocsProductSwitcher": { | ||
| "viewport": "1240x900" | ||
| }, | ||
| "DocsVersionSelect": { | ||
| "viewport": "1240x900" | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| ## How to build with Agent Relay | ||
|
|
||
| Components come from `window.AgentRelay` (root `_ds_bundle.js`). Import `styles.css` once — it | ||
| carries the brand tokens, the compiled component CSS, and the self-hosted brand faces | ||
| (Inter body, Sora headings, Geist Mono code). | ||
|
|
||
| ### Agent Relay is a dark product — start there | ||
|
|
||
| The site renders `<html data-theme="dark">` on every route, marketing and docs alike. Dark is the | ||
| brand. Set it once, at the root of what you build: | ||
|
|
||
| ```jsx | ||
| <BrandTheme>{/* your page */}</BrandTheme> // or: <html data-theme="dark"> | ||
| ``` | ||
|
|
||
| Without it the tokens fall back to a light `:root` set that the shipped product never displays — | ||
| components still render, they just aren't Agent Relay. `BrandTheme` also accepts `theme="light"`; | ||
| the light palette is a supported alternate, not the default. | ||
|
|
||
| Because dark re-defines ~70 tokens, anything written **in tokens** themes itself and anything | ||
| written in hardcoded hex does not. That is the whole reason to use the token names below. | ||
|
|
||
| One other wrapper: **`CodeGroup`** reads the docs language from context — wrap it in | ||
| `DocsLanguageProvider` when you want its TypeScript/Python tabs to work. | ||
|
|
||
| ### Style with CSS custom properties — that is the vocabulary | ||
|
|
||
| The components' own class names are hashed CSS-module names (`site-nav_navLink__aB3x`). Never | ||
| target or reproduce them. The public styling surface is the token set, and it is what your own | ||
| layout CSS should use too: | ||
|
|
||
| | Role | Tokens | | ||
| |---|---| | ||
| | Surfaces | `--bg` `--bg-elevated` `--surface` `--surface-strong` `--section-bg` `--card-bg` | | ||
| | Text | `--fg` `--fg-muted` `--fg-faint` | | ||
| | Brand | `--primary` `--primary-hover` `--primary-fg` `--primary-50`…`--primary-950` | | ||
| | Secondary (warm) | `--secondary-bg` `--secondary-fg` `--secondary-500`…`--secondary-950` | | ||
| | Lines | `--line` `--card-border` `--card-hover-border` | | ||
| | Code | `--code-bg` `--code-fg` `--inline-code-bg` `--inline-code-fg` | | ||
| | Terminal | `--terminal-bg` `--terminal-panel` `--terminal-fg` `--terminal-keyword` `--terminal-string` | | ||
| | Type | `--font-geist-sans` (body) `--font-heading` (Sora) `--font-geist-mono` (code) | | ||
| | Easing | `--ease-out-quint` `--ease-out-expo` | | ||
|
|
||
| Two global classes are part of the system and safe to use on your own anchors and buttons: | ||
| `.btn` plus `.btn-primary` or `.btn-secondary` — the canonical pill button, shared site-wide. | ||
|
|
||
| **Do not assume arbitrary Tailwind utilities exist.** The shipped stylesheet is content-scanned | ||
| from this site, so it contains only the utilities the site itself uses — `p-0`, `pb-4`, `w-full`, | ||
| `gap-2`, `text-sm`, `inline-flex`, `items-center` are present; `pt-5` is not. Write your layout as | ||
| plain CSS (or inline styles) against the tokens above and you are never guessing. | ||
|
|
||
| ### Where the truth is | ||
|
|
||
| - `styles.css` and its `@import` closure — the real tokens and compiled component CSS. | ||
| - `components/<group>/<Name>/<Name>.prompt.md` — usage and examples for one component. | ||
| - `components/<group>/<Name>/<Name>.d.ts` — the exact prop contract. | ||
|
|
||
| Groups: **Primitives** (9 — Button, Badge, Input, Card + its parts) · **Docs** (16 — Note, Warning, | ||
| DocsCard, CardGroup, CodeGroup, nav, search, version select) · **Site** (9 — SiteNav, SiteFooter, | ||
| WaitlistForm, InstallCommand, logos) · **Media** (7 — relay animations, SdkCodeExample, | ||
| ChannelMessagesPreview) · **Agents** (4) · **Blog** (1). | ||
|
|
||
| ### Idiomatic composition | ||
|
|
||
| ```jsx | ||
| const { Card, CardHeader, CardTitle, CardDescription, CardContent, Button, Badge } = window.AgentRelay; | ||
|
|
||
| <section style={{ background: 'var(--section-bg)', padding: '48px 24px' }}> | ||
| <h2 style={{ font: '600 1.75rem/1.2 var(--font-heading)', color: 'var(--fg)' }}> | ||
| Your agents, talking | ||
| </h2> | ||
| <Card style={{ maxWidth: 380, padding: 24, marginTop: 24 }}> | ||
| <CardHeader className="p-0 pb-4"> | ||
| <Badge>Relay Ready</Badge> | ||
| <CardTitle>prod-pipeline-fix</CardTitle> | ||
| <CardDescription>4 agents · 128 messages</CardDescription> | ||
| </CardHeader> | ||
| <CardContent className="p-0"> | ||
| <p style={{ color: 'var(--fg-muted)', fontSize: '0.9rem' }}> | ||
| Planner assigned the review to Builder. | ||
| </p> | ||
| <Button style={{ marginTop: 16 }}>Open channel</Button> | ||
| </CardContent> | ||
| </Card> | ||
| </section> | ||
| ``` | ||
|
|
||
| Use library components for controls and surfaces; use tokens for your own layout glue. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| #!/usr/bin/env node | ||
| // Copies the runtime image assets that scoped components fetch by absolute URL | ||
| // into the bundle root, so the paths resolve in previews and in built designs. | ||
| // | ||
| // `lib/integration-logos.ts` returns `/integration-logos/<file>` and the | ||
| // components render it as a plain <img src>. The design project has no | ||
| // `public/` tree, so without this the marks 404 and every IntegrationGrid / | ||
| // IntegrationLogos render shows broken images. | ||
| // | ||
| // `package-build.mjs` wipes the output dir, so this must run AFTER every build. | ||
| // agent-art is deliberately NOT copied: 15 MB of committed artwork is content, | ||
| // not design system, and those components have a gradient fallback. | ||
|
|
||
| import { cpSync, existsSync, readdirSync } from 'node:fs'; | ||
| import { dirname, join, resolve } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..'); | ||
| const OUT = process.argv[2] ?? join(REPO, 'ds-bundle'); | ||
|
|
||
| const src = join(REPO, 'web', 'public', 'integration-logos'); | ||
| if (!existsSync(src)) { | ||
| console.error(`[ASSETS] ${src} missing — nothing to copy`); | ||
| process.exit(1); | ||
| } | ||
| if (!existsSync(OUT)) { | ||
| console.error(`[ASSETS] ${OUT} missing — run package-build.mjs first`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const dest = join(OUT, 'integration-logos'); | ||
| cpSync(src, dest, { recursive: true }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: cpSync recursively merges into an existing Prompt for AI agents |
||
| console.error(`[ASSETS] integration-logos → ${dest} (${readdirSync(dest).length} files)`); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| --- | ||
| category: Site | ||
| --- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| --- | ||
| category: Media | ||
| --- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| --- | ||
| category: Primitives | ||
| --- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| --- | ||
| category: Primitives | ||
| --- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: The :root font-variable values are hardcoded string literals ('Inter', 'Sora', 'Geist Mono') even though the script already extracts the real family names from the harvested @font-face rules into
families(lineconst families = [...new Set(...)]), which is only used in the final log line. These literals silently duplicate next/font's output: today they match layout.tsx (Inter→--font-geist-sans, Sora→--font-heading, Geist_Mono→--font-geist-mono), but if the site swaps or renames a font the compiled.css keeps stale family names and only the console log reveals the mismatch. Derive the values from the harvested rules or add a check that the hardcoded names are present infamilies, so the two cannot drift.Prompt for AI agents