Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ None.
## Checklist

- [ ] Component(s) follow the conventions in [AGENTS.md](../AGENTS.md).
- [ ] UI changes follow [DESIGN.md](../DESIGN.md). Deviations:
- [ ] New exports added to `packages/ui/src/index.ts`.
- [ ] Tests added (unit + visual, as applicable).
- [ ] CHANGELOG note added under `[Unreleased]` for user-facing changes.
Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ Repository-wide docs (audience = contributors, not just agents):

## Quick reference

The two most-violated rules in PR review history:
The most-violated rules in PR review history:

0. **Design rules are mandatory for UI work.** Any UI suggestion or component/site change must follow [`DESIGN.md`](./DESIGN.md) and the machine-readable tokens in [`packages/design/tokens.json`](./packages/design/tokens.json). List any intentional deviation in the PR body.
1. **Workspace gates green at HEAD** ([R6](./docs/agents/RULES.md#r6--workspace-gates-green-at-head)). Touched-file passes alone are not ship-OK. Run `pnpm -F @vllnt/ui lint && pnpm -F @vllnt/ui exec tsc --noEmit --project tsconfig.build.json && pnpm build && pnpm test:once` — all must be green.
2. **PR body matches HEAD** ([R3](./docs/agents/RULES.md#r3--pr-body-matches-head)). After every push, rewrite the body to match the current head. Stale claims block ship.
3. **Linked issue required** ([R5](./docs/agents/RULES.md#r5--linked-issue-required)). Every PR must reference a GitHub issue. Accepted keywords: `close` / `closes` / `closed`, `fix` / `fixes` / `fixed`, `resolve` / `resolves` / `resolved`, or repo phrases `Part of` / `Related to` — all case-insensitive, optional colon — followed by `#123` or `owner/repo#123`.
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Thanks for wanting to contribute. This repo welcomes issues, bug reports, and PR
- Be respectful. See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
- Security-sensitive reports go through [SECURITY.md](SECURITY.md), **not** public issues.
- Every change keeps the repo shippable: lint, typecheck, tests, and build pass on `main`.
- UI changes must follow [DESIGN.md](DESIGN.md) and the token contract in [packages/design/tokens.json](packages/design/tokens.json). Document intentional deviations in the PR body.

## Development setup

Expand Down
10 changes: 7 additions & 3 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
> Humans read this file. Agents read it too — every UI suggestion must follow it.

This is the **canonical** brand & design guideline for the library. The full
machine-readable token set lives in `packages/design/tokens.json` (shipping in a
follow-up to keep this PR scoped). Site renderings at `/design` will mirror this
file once that route lands.
machine-readable token set lives in `packages/design/tokens.json`, is documented
in `packages/design/README.md`, and is mirrored publicly at `/r/design.json`.
The site rendering at `/design` reads this file directly.

---

Expand Down Expand Up @@ -51,6 +51,10 @@ Tokens live as CSS variables in `packages/ui/themes/default.css`. Components
**always** consume tokens — never raw hex. The token set is versioned with the
library.

Machine clients use `packages/design/tokens.json`. The JSON schema is
documented in `packages/design/tokens.schema.json` and groups tokens by color,
typography, spacing, radius, elevation, motion, and iconography.

### Semantic tokens (consumer-facing)

| Token | Light role | Dark role |
Expand Down
20 changes: 20 additions & 0 deletions apps/registry/app/design.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getDesignGuideMarkdown } from "@/lib/design-guide";

const TEXT_HEADERS = new Headers([
[
"Cache-Control",
"public, max-age=0, s-maxage=86400, stale-while-revalidate=604800",
],
["Content-Type", "text/plain; charset=utf-8"],
]);

export const dynamic = "force-static";
export const revalidate = 86_400;

async function getDesignText(): Promise<Response> {
const body = await getDesignGuideMarkdown();

return new Response(body, { headers: TEXT_HEADERS });
}

export { getDesignText as GET };
180 changes: 180 additions & 0 deletions apps/registry/app/design/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import React, { type ComponentProps, type ReactNode } from "react";

import { MDXContent, Sidebar } from "@vllnt/ui";
import type { Metadata } from "next";

import {
designTokens,
extractDesignGuideSections,
getDesignGuideMarkdown,
slugifyHeading,
} from "@/lib/design-guide";
import { jsonLdScript } from "@/lib/jsonld";
import { generateOGMetadata, generateTwitterMetadata } from "@/lib/og";
import { canonical } from "@/lib/seo";
import { getSidebarSections } from "@/lib/sidebar-sections";

const DESCRIPTION =
"Canonical VLLNT UI design rules, tokens, component patterns, accessibility expectations, and agent-facing guidance.";

export const dynamic = "force-static";
export const revalidate = 86_400;

export const metadata: Metadata = {
alternates: { canonical: canonical("/design") },
description: DESCRIPTION,
openGraph: generateOGMetadata({
description: DESCRIPTION,
title: "VLLNT UI Design Guide",
type: "docs",
}),
title: "Design Guide",
twitter: generateTwitterMetadata({
description: DESCRIPTION,
title: "VLLNT UI Design Guide",
type: "docs",
}),
};

function getNodeText(children: ReactNode): string {
return React.Children.toArray(children)
.map((child) => {
if (typeof child === "string" || typeof child === "number") {
return String(child);
}
return "";
})
.join(" ")
.trim();
}

function DesignHeadingTwo({ children, ...props }: ComponentProps<"h2">) {
const id = slugifyHeading(getNodeText(children));

return (
<h2
className="mt-10 scroll-mt-24 text-2xl font-semibold"
id={id}
{...props}
>
{children}
</h2>
);
}

function DesignHeadingThree({ children, ...props }: ComponentProps<"h3">) {
const id = slugifyHeading(getNodeText(children));

return (
<h3 className="mt-8 scroll-mt-24 text-xl font-semibold" id={id} {...props}>
{children}
</h3>
);
}

export default async function DesignPage() {
const markdown = await getDesignGuideMarkdown();
const sections = extractDesignGuideSections(markdown);
const semanticColorCount = Object.keys(designTokens.color.semantic).length;
const spacingCount = Object.keys(designTokens.spacing.scale).length;

const techArticleLd = {
"@context": "https://schema.org",
"@type": "TechArticle",
about: ["React component library", "design system", "accessibility"],
author: {
"@type": "Organization",
name: "VLLNT",
},
description: DESCRIPTION,
headline: "VLLNT UI Design Guide",
publisher: {
"@type": "Organization",
name: "VLLNT",
},
url: canonical("/design"),
};

return (
<>
<Sidebar sections={getSidebarSections()} />
<main className="flex-1 overflow-y-auto bg-background">
<script
dangerouslySetInnerHTML={{ __html: jsonLdScript(techArticleLd) }}
type="application/ld+json"
/>
<div className="mx-auto grid max-w-7xl gap-8 px-4 py-12 lg:grid-cols-[minmax(0,1fr)_18rem] lg:px-8">
<article className="min-w-0">
<div className="mb-8 border-b border-border pb-8">
<p className="mb-3 text-sm font-medium text-muted-foreground">
Agent-consumable brand guide
</p>
<h1 className="text-4xl font-semibold">VLLNT UI Design Guide</h1>
<p className="mt-4 max-w-3xl text-lg leading-8 text-muted-foreground">
The canonical rules for brand, tokens, layout, motion,
accessibility, and component composition across the library.
</p>
</div>
<MDXContent
components={{
h2: DesignHeadingTwo,
h3: DesignHeadingThree,
}}
content={markdown}
/>
</article>

<aside className="lg:sticky lg:top-8 lg:self-start">
<div className="rounded-lg border border-border bg-card p-4">
<h2 className="text-sm font-semibold">Contents</h2>
<nav aria-label="Design guide sections" className="mt-4">
<ol className="space-y-2">
{sections.map((section) => (
<li key={section.id}>
<a
className="text-sm text-muted-foreground hover:text-foreground"
href={`#${section.id}`}
>
{section.title}
</a>
</li>
))}
</ol>
</nav>
</div>

<div className="mt-4 rounded-lg border border-border bg-card p-4">
<h2 className="text-sm font-semibold">Token contract</h2>
<dl className="mt-4 grid grid-cols-2 gap-3 text-sm">
<div>
<dt className="text-muted-foreground">Version</dt>
<dd className="font-medium">{designTokens.version}</dd>
</div>
<div>
<dt className="text-muted-foreground">Colors</dt>
<dd className="font-medium">{semanticColorCount}</dd>
</div>
<div>
<dt className="text-muted-foreground">Spacing</dt>
<dd className="font-medium">{spacingCount}</dd>
</div>
<div>
<dt className="text-muted-foreground">Icons</dt>
<dd className="font-medium">
{designTokens.iconography.library}
</dd>
</div>
</dl>
<a
className="mt-4 inline-flex text-sm font-medium text-primary underline underline-offset-4"
href="/r/design.json"
>
View design tokens JSON
</a>
</div>
</aside>
</div>
</main>
</>
);
}
Loading
Loading