Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
295 changes: 295 additions & 0 deletions .design-sync/NOTES.md

Large diffs are not rendered by default.

107 changes: 107 additions & 0 deletions .design-sync/build-css.mjs
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;
}
`;
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(', ')}`,
);
37 changes: 37 additions & 0 deletions .design-sync/build.mjs
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);
114 changes: 114 additions & 0 deletions .design-sync/config.json
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"
}
}
}
88 changes: 88 additions & 0 deletions .design-sync/conventions.md
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.
33 changes: 33 additions & 0 deletions .design-sync/copy-assets.mjs
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 });
console.error(`[ASSETS] integration-logos → ${dest} (${readdirSync(dest).length} files)`);
3 changes: 3 additions & 0 deletions .design-sync/groups/AgentSetupPrompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
category: Site
---
3 changes: 3 additions & 0 deletions .design-sync/groups/AgentToolLogo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
category: Media
---
3 changes: 3 additions & 0 deletions .design-sync/groups/Badge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
category: Primitives
---
3 changes: 3 additions & 0 deletions .design-sync/groups/Button.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
category: Primitives
---
Loading
Loading