@designtools/blocks 0.1.0 → 0.2.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/CHANGELOG.md +22 -0
- package/README.md +13 -9
- package/dist/cli.js +6 -2
- package/package.json +4 -2
- package/reference/README.md +20 -0
- package/reference/app/(design)/design/system/(docs)/brand/imagery/page.tsx +15 -0
- package/reference/app/(design)/design/system/(docs)/brand/logo/page.tsx +15 -0
- package/reference/app/(design)/design/system/(docs)/brand/page.tsx +16 -0
- package/reference/app/(design)/design/system/(docs)/brand/voice/page.tsx +15 -0
- package/reference/app/(design)/design/system/(docs)/colour/page.tsx +26 -0
- package/reference/app/(design)/design/system/(docs)/components/[slug]/page.tsx +86 -0
- package/reference/app/(design)/design/system/(docs)/layout.tsx +35 -0
- package/reference/app/(design)/design/system/(docs)/page.tsx +48 -0
- package/reference/app/(design)/design/system/(docs)/patterns/[id]/page.tsx +23 -0
- package/reference/app/(design)/design/system/(docs)/rules/page.tsx +42 -0
- package/reference/app/(design)/design/system/(docs)/scale/page.tsx +15 -0
- package/reference/app/(design)/design/system/(docs)/standards/page.tsx +21 -0
- package/reference/app/(design)/design/system/(docs)/type/page.tsx +15 -0
- package/reference/app/(design)/design/system/(docs)/vocabulary/page.tsx +15 -0
- package/reference/app/(design)/design/system/_components/copy-page.tsx +32 -0
- package/reference/app/(design)/design/system/_components/prose.tsx +26 -0
- package/reference/app/(design)/design/system/_data.ts +53 -0
- package/reference/app/(design)/design/system/manifest/[file]/route.ts +15 -0
- package/reference/app/(design)/design/system/md/[[...path]]/route.ts +94 -0
- package/reference/app/(design)/design/system/preview/[slug]/[example]/page.tsx +23 -0
- package/reference/app/(design)/layout.tsx +16 -0
- package/reference/app/globals.css +14 -0
- package/reference/app/layout.tsx +16 -0
- package/reference/designtools.json +14 -0
- package/reference/package.json +38 -0
- package/reference/proxy.ts +55 -0
- package/registry/a11y-panel.tsx +3 -3
- package/registry/adherence-summary.tsx +4 -4
- package/registry/agent-view.tsx +2 -1
- package/registry/anatomy.tsx +3 -2
- package/registry/ask-claude.tsx +4 -2
- package/registry/code-view.tsx +6 -3
- package/registry/examples.tsx +7 -6
- package/registry/glossary.tsx +2 -1
- package/registry/lib/adherence-types.ts +3 -1
- package/registry/lib/cx.ts +8 -0
- package/registry/lib/manifest-types.ts +13 -2
- package/registry/lib/manifest.ts +46 -0
- package/registry/lib/markdown.ts +30 -25
- package/registry/lib/search.ts +99 -0
- package/registry/lib/standards.ts +82 -0
- package/registry/lib/status.tsx +3 -1
- package/registry/logo-usage.tsx +44 -19
- package/registry/pattern.tsx +3 -2
- package/registry/playground.tsx +7 -6
- package/registry/preview-frame.tsx +3 -3
- package/registry/props-table.tsx +4 -4
- package/registry/rule.tsx +3 -2
- package/registry/search.tsx +134 -0
- package/registry/shell.tsx +86 -23
- package/registry/standards.tsx +22 -26
- package/registry/swatches.tsx +82 -3
- package/registry/usage.tsx +2 -1
- package/registry/variant-matrix.tsx +1 -1
- package/registry/voice-terms.tsx +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The few markdown shapes editorial pages use: headings, paragraphs and bullet lists, with bold and code. */
|
|
2
|
+
export function Markdown({ text }: { text: string }) {
|
|
3
|
+
const blocks = text.split(/\n{2,}/);
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex max-w-prose flex-col gap-4">
|
|
6
|
+
{blocks.map((b, i) => {
|
|
7
|
+
if (b.startsWith("## ")) return <h2 key={i} className="pt-4 text-xl font-semibold">{b.slice(3)}</h2>;
|
|
8
|
+
if (b.split("\n").every((l) => l.startsWith("- ")))
|
|
9
|
+
return (
|
|
10
|
+
<ul key={i} className="flex list-disc flex-col gap-2 pl-5">
|
|
11
|
+
{b.split("\n").map((l) => (
|
|
12
|
+
<li key={l}>{inline(l.slice(2))}</li>
|
|
13
|
+
))}
|
|
14
|
+
</ul>
|
|
15
|
+
);
|
|
16
|
+
return <p key={i}>{inline(b)}</p>;
|
|
17
|
+
})}
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function inline(text: string) {
|
|
23
|
+
return text.split(/(\*\*[^*]+\*\*|`[^`]+`)/).map((part, i) =>
|
|
24
|
+
part.startsWith("**") ? <strong key={i}>{part.slice(2, -2)}</strong> : part.startsWith("`") ? <code key={i} className="font-mono text-[0.9em]">{part.slice(1, -1)}</code> : part,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything the docs read, in one place, for the HTML pages and the markdown
|
|
3
|
+
* route alike: the manifest, the project's standards, the provenance of this
|
|
4
|
+
* build, and the editorial pages people write.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import assetsJson from "@ds/manifest/assets.json";
|
|
8
|
+
import componentsJson from "@ds/manifest/components.json";
|
|
9
|
+
import patternsJson from "@ds/manifest/patterns.json";
|
|
10
|
+
import rulesJson from "@ds/manifest/rules.json";
|
|
11
|
+
import taxonomyJson from "@ds/manifest/taxonomy.json";
|
|
12
|
+
import tokensJson from "@ds/manifest/tokens.json";
|
|
13
|
+
import stack from "../../../../.mxa/stack.json";
|
|
14
|
+
import adherenceJson from "./_adherence.json";
|
|
15
|
+
import provenanceJson from "./_provenance.json";
|
|
16
|
+
import type { AdherenceReport } from "./_blocks/lib/adherence-types";
|
|
17
|
+
import type { AssetsManifest, PatternsManifest, RulesManifest, TaxonomyManifest } from "./_blocks/lib/manifest-types";
|
|
18
|
+
import { readComponents, readTokens } from "./_blocks/lib/manifest";
|
|
19
|
+
|
|
20
|
+
export const BASE = "/design/system";
|
|
21
|
+
export const TITLE = "Demo design system";
|
|
22
|
+
|
|
23
|
+
export const components = readComponents(componentsJson);
|
|
24
|
+
export const tokens = readTokens(tokensJson);
|
|
25
|
+
export const rules = rulesJson as RulesManifest;
|
|
26
|
+
export const patterns = patternsJson as PatternsManifest;
|
|
27
|
+
export const taxonomy = taxonomyJson as TaxonomyManifest;
|
|
28
|
+
export const assets = assetsJson as AssetsManifest;
|
|
29
|
+
export const baselines = stack.baselines as Record<string, unknown>;
|
|
30
|
+
export const provenance = (provenanceJson as { line: string }).line;
|
|
31
|
+
export const adherence = adherenceJson as unknown as AdherenceReport;
|
|
32
|
+
|
|
33
|
+
export const manifestFiles = { components: componentsJson, tokens: tokensJson, rules: rulesJson, patterns: patternsJson, taxonomy: taxonomyJson, assets: assetsJson };
|
|
34
|
+
|
|
35
|
+
/** Editorial pages: written by people, so they carry an author and a review date, never a commit. */
|
|
36
|
+
export const editorial = {
|
|
37
|
+
brand: {
|
|
38
|
+
title: "Brand",
|
|
39
|
+
by: { author: "Demo studio", reviewed: "10 October 2026", status: "draft" as const },
|
|
40
|
+
lead: "Plain, calm and exact. We sound like the person in the room who has read the contract.",
|
|
41
|
+
body: [
|
|
42
|
+
"## Principles",
|
|
43
|
+
"",
|
|
44
|
+
"- **Say the thing.** A label names what happens; a message says what to do next.",
|
|
45
|
+
"- **Quiet by default.** One primary action a screen. Colour is for meaning, not decoration.",
|
|
46
|
+
"- **Nothing invented.** Every value comes from a token, every example from the real component.",
|
|
47
|
+
"",
|
|
48
|
+
"## Design language",
|
|
49
|
+
"",
|
|
50
|
+
"Generous space, a single teal for action, and greys that lean cool. Corners are soft but small: this is a ledger, not a toy. Motion is short and settles; nothing bounces.",
|
|
51
|
+
].join("\n"),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { manifestFiles } from "../../_data";
|
|
2
|
+
|
|
3
|
+
export const dynamic = "force-static";
|
|
4
|
+
|
|
5
|
+
export function generateStaticParams() {
|
|
6
|
+
return Object.keys(manifestFiles).map((name) => ({ file: `${name}.json` }));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The manifest served beside the docs, so an agent outside the repo gets the exact data, not prose about it. */
|
|
10
|
+
export async function GET(_: Request, { params }: { params: Promise<{ file: string }> }) {
|
|
11
|
+
const { file } = await params;
|
|
12
|
+
const data = manifestFiles[file.replace(/\.json$/, "") as keyof typeof manifestFiles];
|
|
13
|
+
if (!data) return new Response(`# Not found\n\nNo manifest file called ${file}.\n`, { status: 404, headers: { "content-type": "text/markdown; charset=utf-8" } });
|
|
14
|
+
return Response.json(data);
|
|
15
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
adherenceMarkdown,
|
|
3
|
+
assetsMarkdown,
|
|
4
|
+
coloursMarkdown,
|
|
5
|
+
componentMarkdown,
|
|
6
|
+
generatedHeader,
|
|
7
|
+
editorialHeader,
|
|
8
|
+
glossaryMarkdown,
|
|
9
|
+
manifestLinks,
|
|
10
|
+
patternMarkdown,
|
|
11
|
+
ruleMarkdown,
|
|
12
|
+
scaleMarkdown,
|
|
13
|
+
standardsMarkdown,
|
|
14
|
+
systemMarkdown,
|
|
15
|
+
typeMarkdown,
|
|
16
|
+
} from "../../_blocks/lib/markdown";
|
|
17
|
+
import { findBySlug, groupByCategory, slugOf } from "../../_blocks/lib/manifest";
|
|
18
|
+
import { adherence, assets, BASE, baselines, components, editorial, patterns, provenance, rules, taxonomy, TITLE, tokens } from "../../_data";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The markdown form of every docs page, generated from the manifest rather
|
|
22
|
+
* than converted from HTML. proxy.ts sends `Accept: text/markdown` and `.md`
|
|
23
|
+
* requests here; a page that does not exist gets a markdown 404.
|
|
24
|
+
*/
|
|
25
|
+
export async function GET(_: Request, { params }: { params: Promise<{ path?: string[] }> }) {
|
|
26
|
+
const path = ((await params).path ?? []).join("/").replace(/\.md$/, "");
|
|
27
|
+
const page = render(path);
|
|
28
|
+
const canonical = `${BASE}${path ? `/${path}` : ""}`;
|
|
29
|
+
return new Response(page ?? `# Not found\n\nThere is no page at \`${canonical}\`. The index is at [${BASE}.md](${BASE}.md).\n`, {
|
|
30
|
+
status: page ? 200 : 404,
|
|
31
|
+
headers: {
|
|
32
|
+
"content-type": "text/markdown; charset=utf-8",
|
|
33
|
+
vary: "Accept",
|
|
34
|
+
link: `<${canonical}>; rel="canonical"`,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function render(path: string): string | undefined {
|
|
40
|
+
const generated = (md: string) => generatedHeader(provenance) + md;
|
|
41
|
+
const [section, id] = path.split("/");
|
|
42
|
+
switch (section) {
|
|
43
|
+
case "":
|
|
44
|
+
return generated(
|
|
45
|
+
[
|
|
46
|
+
`# ${TITLE}`,
|
|
47
|
+
"",
|
|
48
|
+
"The brand and the design system in one place. Every page here has a markdown form: add `.md` to its URL.",
|
|
49
|
+
"",
|
|
50
|
+
"## Pages",
|
|
51
|
+
"",
|
|
52
|
+
`- [Brand](${BASE}/brand.md), [logo](${BASE}/brand/logo.md), [imagery](${BASE}/brand/imagery.md), [voice and terms](${BASE}/brand/voice.md)`,
|
|
53
|
+
`- [Colour](${BASE}/colour.md), [type](${BASE}/type.md), [scale](${BASE}/scale.md)`,
|
|
54
|
+
`- [Rules](${BASE}/rules.md)`,
|
|
55
|
+
...patterns.patterns.map((p) => `- Pattern: [${p.name}](${BASE}/patterns/${p.id}.md)`),
|
|
56
|
+
...groupByCategory(components).flatMap((g) => g.components.map((c) => `- ${g.category}: [${c.name}](${BASE}/components/${slugOf(c)}.md), ${c.status ?? "status not set"}`)),
|
|
57
|
+
`- [Standards](${BASE}/standards.md), [vocabulary](${BASE}/vocabulary.md)`,
|
|
58
|
+
`- Everything in [one file](${BASE}/context.md)`,
|
|
59
|
+
"",
|
|
60
|
+
manifestLinks(`${BASE}/manifest`),
|
|
61
|
+
].join("\n"),
|
|
62
|
+
);
|
|
63
|
+
case "context":
|
|
64
|
+
return systemMarkdown({ components, tokens, rules: rules.rules, patterns: patterns.patterns, terms: taxonomy.terms }, TITLE, provenance);
|
|
65
|
+
case "brand":
|
|
66
|
+
if (id === "logo") return generated(assetsMarkdown(assets.assets.filter((a) => a.kind === "logo")) + "\n" + rules.rules.filter((r) => r.kind === "brand").map(ruleMarkdown).join("\n"));
|
|
67
|
+
if (id === "imagery") return generated(assetsMarkdown(assets.assets.filter((a) => a.kind !== "logo")));
|
|
68
|
+
if (id === "voice") return generated(glossaryMarkdown(taxonomy.terms.filter((t) => t.kind === "product" || t.wrong?.length)));
|
|
69
|
+
if (!id) return editorialHeader(editorial.brand.by) + `# ${editorial.brand.title}\n\n${editorial.brand.lead}\n\n${editorial.brand.body}\n`;
|
|
70
|
+
return undefined;
|
|
71
|
+
case "colour":
|
|
72
|
+
return generated(coloursMarkdown(tokens));
|
|
73
|
+
case "type":
|
|
74
|
+
return generated(typeMarkdown(tokens));
|
|
75
|
+
case "scale":
|
|
76
|
+
return generated(scaleMarkdown(tokens));
|
|
77
|
+
case "rules":
|
|
78
|
+
return generated(["# Rules", "", ...rules.rules.map(ruleMarkdown)].join("\n"));
|
|
79
|
+
case "patterns": {
|
|
80
|
+
const p = patterns.patterns.find((x) => x.id === id);
|
|
81
|
+
return p && generated(patternMarkdown(p, 1));
|
|
82
|
+
}
|
|
83
|
+
case "components": {
|
|
84
|
+
const c = id ? findBySlug(components, id) : undefined;
|
|
85
|
+
return c && generated(componentMarkdown(c, 1));
|
|
86
|
+
}
|
|
87
|
+
case "standards":
|
|
88
|
+
return generated(standardsMarkdown(baselines) + "\n" + adherenceMarkdown(adherence));
|
|
89
|
+
case "vocabulary":
|
|
90
|
+
return generated(glossaryMarkdown(taxonomy.terms));
|
|
91
|
+
default:
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
import { PreviewExample, PreviewSurface } from "../../../_blocks/preview-frame";
|
|
3
|
+
import { findBySlug } from "../../../_blocks/lib/manifest";
|
|
4
|
+
import { components } from "../../../_data";
|
|
5
|
+
|
|
6
|
+
/** One example on its own: the target for the preview frame and for visual regression snapshots. */
|
|
7
|
+
export default async function Preview({
|
|
8
|
+
params,
|
|
9
|
+
searchParams,
|
|
10
|
+
}: {
|
|
11
|
+
params: Promise<{ slug: string; example: string }>;
|
|
12
|
+
searchParams: Promise<{ theme?: string }>;
|
|
13
|
+
}) {
|
|
14
|
+
const { slug, example } = await params;
|
|
15
|
+
const { theme } = await searchParams;
|
|
16
|
+
const entry = findBySlug(components, slug);
|
|
17
|
+
if (!entry?.examples) notFound();
|
|
18
|
+
return (
|
|
19
|
+
<PreviewSurface theme={theme}>
|
|
20
|
+
<PreviewExample entry={entry} example={decodeURIComponent(example)} />
|
|
21
|
+
</PreviewSurface>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { notFound } from "next/navigation";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Every design page, under /design: never public. A 404 on a production deployment
|
|
7
|
+
* (VERCEL_ENV, or the host's equivalent), never indexed, kept on previews so a
|
|
8
|
+
* colleague can open a link. proxy.ts does the same for every docs URL, including
|
|
9
|
+
* the markdown and manifest route handlers, which no layout wraps.
|
|
10
|
+
*/
|
|
11
|
+
export const metadata: Metadata = { robots: { index: false, follow: false } };
|
|
12
|
+
|
|
13
|
+
export default function DesignLayout({ children }: { children: ReactNode }) {
|
|
14
|
+
if (process.env.VERCEL_ENV === "production") notFound();
|
|
15
|
+
return children;
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "./styles/tokens.css";
|
|
3
|
+
@import "./styles/scale.css";
|
|
4
|
+
|
|
5
|
+
/* The demo gitignores its generated blocks, and Tailwind skips ignored files unless told. */
|
|
6
|
+
@source "./(design)/design/system/_blocks";
|
|
7
|
+
|
|
8
|
+
@layer base {
|
|
9
|
+
html {
|
|
10
|
+
background: var(--canvas);
|
|
11
|
+
color: var(--canvas-foreground);
|
|
12
|
+
font-family: var(--font-sans);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "designtools demo",
|
|
7
|
+
robots: { index: false },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
11
|
+
return (
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<body>{children}</body>
|
|
14
|
+
</html>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest": {
|
|
3
|
+
"system": "src/ds",
|
|
4
|
+
"tokens": [
|
|
5
|
+
"app/styles/tokens.css",
|
|
6
|
+
"app/styles/scale.css"
|
|
7
|
+
],
|
|
8
|
+
"docs": "/design/system",
|
|
9
|
+
"usage": "app/styles/tokens.usage.json"
|
|
10
|
+
},
|
|
11
|
+
"blocks": {
|
|
12
|
+
"dir": "app/(design)/design/system/_blocks"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@designtools/example-demo",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"setup": "pnpm tokens && pnpm manifest && pnpm blocks && pnpm adherence",
|
|
7
|
+
"tokens": "generate-tokens --primary \"#4e9caf\" --neutral \"#8a8f98\" --background \"sand=#efe9dc/#2b2721\" --format tailwind-v4 --out app/styles/tokens.css --usage app/styles/tokens.usage.json",
|
|
8
|
+
"manifest": "designtools-manifest build && designtools-manifest provenance --json > 'app/(design)/design/system/_provenance.json'",
|
|
9
|
+
"blocks": "rm -rf 'app/(design)/design/system/_blocks' && designtools-blocks add --all && designtools-blocks map",
|
|
10
|
+
"dev": "next dev --port 4310",
|
|
11
|
+
"build": "next build",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"adherence": "designtools-adherence --json 'app/(design)/design/system/_adherence.json' > /dev/null"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@base-ui/react": "1.6.0",
|
|
17
|
+
"axe-core": "4.14.0",
|
|
18
|
+
"fuzzysort": "4.0.2",
|
|
19
|
+
"next": "16.3.8",
|
|
20
|
+
"react": "19.2.7",
|
|
21
|
+
"react-dom": "19.2.7",
|
|
22
|
+
"shiki": "4.5.0",
|
|
23
|
+
"tailwind-merge": "3.6.0",
|
|
24
|
+
"tailwind-variants": "3.3.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@designtools/adherence": "workspace:*",
|
|
28
|
+
"@designtools/blocks": "workspace:*",
|
|
29
|
+
"@designtools/manifest": "workspace:*",
|
|
30
|
+
"@designtools/tokens": "workspace:*",
|
|
31
|
+
"@tailwindcss/postcss": "4.3.3",
|
|
32
|
+
"@types/node": "^24",
|
|
33
|
+
"@types/react": "^19.2.0",
|
|
34
|
+
"@types/react-dom": "^19.2.0",
|
|
35
|
+
"tailwindcss": "4.3.3",
|
|
36
|
+
"typescript": "~6.0.3"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NextResponse, type NextRequest } from "next/server";
|
|
2
|
+
|
|
3
|
+
const BASE = "/design/system";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One URL, two formats. A docs URL answers `Accept: text/markdown`, or the same
|
|
7
|
+
* path with `.md`, with markdown generated from the manifest. Negotiation is on
|
|
8
|
+
* Accept only, never the user agent, so every response says `Vary: Accept`,
|
|
9
|
+
* and the HTML links to its markdown.
|
|
10
|
+
*
|
|
11
|
+
* Never public: on a production deployment every docs URL, in every format, is a
|
|
12
|
+
* 404. The (design) layout does the same for the pages, the preview route among
|
|
13
|
+
* them; this also covers the markdown and manifest route handlers, which no
|
|
14
|
+
* layout wraps.
|
|
15
|
+
*/
|
|
16
|
+
export function proxy(request: NextRequest) {
|
|
17
|
+
const { pathname } = request.nextUrl;
|
|
18
|
+
if (process.env.VERCEL_ENV === "production") {
|
|
19
|
+
return new NextResponse("Not found\n", { status: 404, headers: { "x-robots-tag": "noindex" } });
|
|
20
|
+
}
|
|
21
|
+
if (pathname.startsWith(`${BASE}/md`) || pathname.startsWith(`${BASE}/manifest/`) || pathname.startsWith(`${BASE}/preview/`)) {
|
|
22
|
+
return NextResponse.next();
|
|
23
|
+
}
|
|
24
|
+
const wantsMarkdown = pathname.endsWith(".md") || prefersMarkdown(request.headers.get("accept"));
|
|
25
|
+
const page = pathname.replace(/\.md$/, "").replace(/\/$/, "");
|
|
26
|
+
if (wantsMarkdown) {
|
|
27
|
+
const url = request.nextUrl.clone();
|
|
28
|
+
url.pathname = `${BASE}/md${page.slice(BASE.length)}`;
|
|
29
|
+
return NextResponse.rewrite(url, { headers: { vary: "Accept", "x-robots-tag": "noindex" } });
|
|
30
|
+
}
|
|
31
|
+
// Next 16 replaces Vary on page responses with its own, so this one does not survive on HTML;
|
|
32
|
+
// the markdown responses keep theirs. See the demo README.
|
|
33
|
+
const res = NextResponse.next();
|
|
34
|
+
res.headers.set("vary", "Accept");
|
|
35
|
+
res.headers.set("link", `<${page}.md>; rel="alternate"; type="text/markdown"`);
|
|
36
|
+
res.headers.set("x-robots-tag", "noindex");
|
|
37
|
+
return res;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** text/markdown ranked above text/html in the Accept header. */
|
|
41
|
+
function prefersMarkdown(accept: string | null): boolean {
|
|
42
|
+
if (!accept) return false;
|
|
43
|
+
const q = (type: string) => {
|
|
44
|
+
for (const part of accept.split(",")) {
|
|
45
|
+
const [name, ...params] = part.trim().split(";");
|
|
46
|
+
if (name.trim() !== type) continue;
|
|
47
|
+
const weight = params.map((p) => p.trim()).find((p) => p.startsWith("q="));
|
|
48
|
+
return weight ? Number(weight.slice(2)) : 1;
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
};
|
|
52
|
+
return q("text/markdown") > 0 && q("text/markdown") >= q("text/html");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const config = { matcher: ["/design/system", "/design/system.md", "/design/system/:path*"] };
|
package/registry/a11y-panel.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useRef, useState, type ReactNode } from "react";
|
|
4
|
-
import { cx } from "./lib/cx";
|
|
4
|
+
import { cx, target } from "./lib/cx";
|
|
5
5
|
import type { ComponentEntry } from "./lib/manifest-types";
|
|
6
6
|
|
|
7
7
|
interface Finding {
|
|
@@ -51,12 +51,12 @@ export function A11yPanel({ entry, children }: { entry?: ComponentEntry; childre
|
|
|
51
51
|
type="button"
|
|
52
52
|
onClick={run}
|
|
53
53
|
disabled={status === "running"}
|
|
54
|
-
className="rounded-md border border-border px-3 py-1.5 font-medium hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50"
|
|
54
|
+
className={cx(target, "rounded-md border border-border px-3 py-1.5 font-medium hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50")}
|
|
55
55
|
>
|
|
56
56
|
{status === "running" ? "Checking…" : status === "done" ? "Check again" : "Check with axe"}
|
|
57
57
|
</button>
|
|
58
58
|
{entry?.aria && (
|
|
59
|
-
<a href={entry.aria} className="text-xs underline underline-offset-2">
|
|
59
|
+
<a href={entry.aria} className={cx(target, "inline-flex items-center text-xs underline underline-offset-2")}>
|
|
60
60
|
ARIA pattern it follows
|
|
61
61
|
</a>
|
|
62
62
|
)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AdherenceReport, Counts } from "./lib/adherence-types";
|
|
2
|
-
import { cx } from "./lib/cx";
|
|
2
|
+
import { cx, target } from "./lib/cx";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The latest adherence report: how much of what the system could render comes
|
|
@@ -32,7 +32,7 @@ export function AdherenceSummary({ report }: { report: AdherenceReport }) {
|
|
|
32
32
|
</p>
|
|
33
33
|
)}
|
|
34
34
|
{report.routes.length > 0 && (
|
|
35
|
-
<div className="overflow-x-auto">
|
|
35
|
+
<div className="overflow-x-auto" tabIndex={0} role="region" aria-label="Adherence by route">
|
|
36
36
|
<table className="w-full min-w-[32rem] border-collapse text-left text-sm">
|
|
37
37
|
<caption className="sr-only">Adherence by route</caption>
|
|
38
38
|
<thead>
|
|
@@ -54,7 +54,7 @@ export function AdherenceSummary({ report }: { report: AdherenceReport }) {
|
|
|
54
54
|
)}
|
|
55
55
|
{report.files.some((f) => f.raw.length || f.offSystem.length || f.overrides.length) && (
|
|
56
56
|
<details data-slot="adherence-findings" className="rounded-md border border-border">
|
|
57
|
-
<summary className="cursor-pointer px-3 py-2 text-sm">Findings by file</summary>
|
|
57
|
+
<summary className={cx(target, "flex cursor-pointer items-center px-3 py-2 text-sm")}>Findings by file</summary>
|
|
58
58
|
<ul className="flex flex-col divide-y divide-border border-t border-border text-sm">
|
|
59
59
|
{report.files
|
|
60
60
|
.filter((f) => f.raw.length || f.offSystem.length || f.overrides.length)
|
|
@@ -69,7 +69,7 @@ export function AdherenceSummary({ report }: { report: AdherenceReport }) {
|
|
|
69
69
|
))}
|
|
70
70
|
{f.offSystem.map((o) => (
|
|
71
71
|
<li key={`o${o.line}:${o.column}:${o.class}`}>
|
|
72
|
-
line {o.line}: <code className="font-mono text-xs">{o.class}</code> is {o.kind === "palette" ? "a palette step, not a semantic token" : "an arbitrary value"}
|
|
72
|
+
line {o.line}: <code className="font-mono text-xs">{o.class}</code> is {o.kind === "status-text" ? "a status fill used as text; use its subdued foreground" : o.kind === "palette" ? "a palette step, not a semantic token" : "an arbitrary value"}
|
|
73
73
|
</li>
|
|
74
74
|
))}
|
|
75
75
|
{f.overrides.map((o) => (
|
package/registry/agent-view.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import type { ReactNode } from "react";
|
|
|
5
5
|
import { CodeView } from "./code-view";
|
|
6
6
|
import { componentMarkdown } from "./lib/markdown";
|
|
7
7
|
import type { ComponentEntry } from "./lib/manifest-types";
|
|
8
|
+
import { target } from "./lib/cx";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* What an agent needs to know, as copyable markdown: a component's status,
|
|
@@ -22,7 +23,7 @@ export function AgentInstructions({ entry, markdown }: { entry?: ComponentEntry;
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const tab =
|
|
25
|
-
"-mb-px border-b-2 border-transparent px-1 pb-2 text-sm text-muted-foreground hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring data-[active]:border-primary data-[active]:font-medium data-[active]:text-canvas-foreground";
|
|
26
|
+
target + " -mb-px border-b-2 border-transparent px-1 pb-2 text-sm text-muted-foreground hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring data-[active]:border-primary data-[active]:font-medium data-[active]:text-canvas-foreground";
|
|
26
27
|
|
|
27
28
|
/** The same thing two ways: as a person sees it, and as an agent reads it. Pass a component entry, or any block's markdown. */
|
|
28
29
|
export function AgentView({
|
package/registry/anatomy.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useId, useRef, useState, type ReactNode } from "react";
|
|
4
|
-
import { cx } from "./lib/cx";
|
|
4
|
+
import { cx, target } from "./lib/cx";
|
|
5
5
|
import { componentFor, examplesFor, renderExample } from "./lib/lookup";
|
|
6
6
|
import type { ComponentEntry } from "./lib/manifest-types";
|
|
7
7
|
|
|
@@ -68,7 +68,8 @@ export function Anatomy({
|
|
|
68
68
|
onBlur={() => setActive(null)}
|
|
69
69
|
onClick={() => setActive(slot)}
|
|
70
70
|
className={cx(
|
|
71
|
-
|
|
71
|
+
target,
|
|
72
|
+
"flex w-full items-center justify-between gap-3 rounded-md px-2 py-1.5 text-left hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring",
|
|
72
73
|
active === slot && "bg-muted",
|
|
73
74
|
)}
|
|
74
75
|
>
|
package/registry/ask-claude.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useId, useState } from "react";
|
|
4
|
+
import { importOf } from "./lib/manifest";
|
|
4
5
|
import type { ComponentEntry } from "./lib/manifest-types";
|
|
6
|
+
import { target } from "./lib/cx";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Copies a ready-made prompt for Claude: the component, its source file, the
|
|
@@ -47,7 +49,7 @@ export function AskClaude({
|
|
|
47
49
|
<button
|
|
48
50
|
type="button"
|
|
49
51
|
onClick={copy}
|
|
50
|
-
className=
|
|
52
|
+
className={`${target} rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring`}
|
|
51
53
|
>
|
|
52
54
|
{copied ? "Copied" : "Copy prompt"}
|
|
53
55
|
</button>
|
|
@@ -60,7 +62,7 @@ export function AskClaude({
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export function promptFor(entry: ComponentEntry, request: string, variant?: Record<string, string>, example?: string): string {
|
|
63
|
-
const lines = [`Component: ${entry.name}, exported as ${entry.export} from ${entry.source}`];
|
|
65
|
+
const lines = [`Component: ${entry.name}, exported as ${entry.export} from ${entry.source} (imported from "${importOf(entry)}")`];
|
|
64
66
|
const shown = Object.entries(variant ?? {});
|
|
65
67
|
if (shown.length) lines.push(`Variant in view: ${shown.map(([k, v]) => `${k}="${v}"`).join(", ")}`);
|
|
66
68
|
if (example && entry.examples) lines.push(`Example in view: ${example}, from ${entry.examples.source}`);
|
package/registry/code-view.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
-
import { cx } from "./lib/cx";
|
|
4
|
+
import { cx, target } from "./lib/cx";
|
|
5
5
|
|
|
6
6
|
/* Shiki writes both themes as CSS variables; these pick the one for the page's mode. */
|
|
7
7
|
const THEME_CSS = `
|
|
@@ -52,7 +52,7 @@ export function CodeView({
|
|
|
52
52
|
<style>{THEME_CSS}</style>
|
|
53
53
|
<figcaption className="flex items-center justify-between border-b border-border px-4 py-2 text-xs text-muted-foreground">
|
|
54
54
|
<span>{label}</span>
|
|
55
|
-
<button type="button" onClick={copy} className="rounded-sm px-2 py-1 hover:bg-canvas focus-visible:outline-2 focus-visible:outline-ring">
|
|
55
|
+
<button type="button" onClick={copy} className={cx(target, "rounded-sm px-2 py-1 hover:bg-canvas focus-visible:outline-2 focus-visible:outline-ring")}>
|
|
56
56
|
{copied ? "Copied" : "Copy"}
|
|
57
57
|
</button>
|
|
58
58
|
</figcaption>
|
|
@@ -62,10 +62,13 @@ export function CodeView({
|
|
|
62
62
|
{html ? (
|
|
63
63
|
<div
|
|
64
64
|
className={cx("overflow-x-auto p-4 font-mono text-xs leading-relaxed", wrap && "[&_pre]:break-words [&_pre]:whitespace-pre-wrap")}
|
|
65
|
+
tabIndex={0}
|
|
66
|
+
role="region"
|
|
67
|
+
aria-label={label}
|
|
65
68
|
dangerouslySetInnerHTML={{ __html: html }}
|
|
66
69
|
/>
|
|
67
70
|
) : (
|
|
68
|
-
<pre className={cx("overflow-x-auto p-4 font-mono text-xs leading-relaxed", wrap && "break-words whitespace-pre-wrap")}>
|
|
71
|
+
<pre tabIndex={0} aria-label={label} className={cx("overflow-x-auto p-4 font-mono text-xs leading-relaxed", wrap && "break-words whitespace-pre-wrap")}>
|
|
69
72
|
<code>{code}</code>
|
|
70
73
|
</pre>
|
|
71
74
|
)}
|
package/registry/examples.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { CodeView } from "./code-view";
|
|
4
4
|
import { Boundary } from "./lib/boundary";
|
|
5
|
-
import { cx } from "./lib/cx";
|
|
5
|
+
import { cx, target } from "./lib/cx";
|
|
6
6
|
import { examplesFor, Missing, renderExample } from "./lib/lookup";
|
|
7
7
|
import type { ComponentEntry, ExampleRef } from "./lib/manifest-types";
|
|
8
8
|
import { inline } from "./lib/text";
|
|
@@ -63,10 +63,11 @@ function Example({ example, kind, value, href }: { example: ExampleRef; kind: ke
|
|
|
63
63
|
<span className="flex items-baseline gap-2">
|
|
64
64
|
{LABEL[kind] && (
|
|
65
65
|
<span
|
|
66
|
+
// a measured pairing, not a fill colour used as text (text-success read 3.9:1 on the canvas)
|
|
66
67
|
className={cx(
|
|
67
|
-
"font-medium",
|
|
68
|
-
kind === "do" && "text-success",
|
|
69
|
-
kind === "dont" && "text-destructive",
|
|
68
|
+
"rounded-sm px-1.5 font-medium",
|
|
69
|
+
kind === "do" && "bg-success-subdued text-success-subdued-foreground",
|
|
70
|
+
kind === "dont" && "bg-destructive-subdued text-destructive-subdued-foreground",
|
|
70
71
|
)}
|
|
71
72
|
>
|
|
72
73
|
{kind === "do" ? "✓ " : kind === "dont" ? "✕ " : ""}
|
|
@@ -76,7 +77,7 @@ function Example({ example, kind, value, href }: { example: ExampleRef; kind: ke
|
|
|
76
77
|
<code className="font-mono text-xs text-muted-foreground">{example.name}</code>
|
|
77
78
|
</span>
|
|
78
79
|
{href && (
|
|
79
|
-
<a href={href} className="text-xs underline underline-offset-2">
|
|
80
|
+
<a href={href} className={cx(target, "inline-flex items-center text-xs underline underline-offset-2")}>
|
|
80
81
|
Open alone
|
|
81
82
|
</a>
|
|
82
83
|
)}
|
|
@@ -84,7 +85,7 @@ function Example({ example, kind, value, href }: { example: ExampleRef; kind: ke
|
|
|
84
85
|
{example.description && <span className="text-muted-foreground">{inline(example.description)}</span>}
|
|
85
86
|
{example.code && (
|
|
86
87
|
<details>
|
|
87
|
-
<summary className="cursor-pointer text-xs text-muted-foreground">Code</summary>
|
|
88
|
+
<summary className={cx(target, "flex w-fit cursor-pointer items-center text-xs text-muted-foreground")}>Code</summary>
|
|
88
89
|
<div className="mt-2">
|
|
89
90
|
<CodeView code={example.code} label={example.name} />
|
|
90
91
|
</div>
|
package/registry/glossary.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useId, useState } from "react";
|
|
4
4
|
import type { TermEntry } from "./lib/manifest-types";
|
|
5
5
|
import { inline } from "./lib/text";
|
|
6
|
+
import { target } from "./lib/cx";
|
|
6
7
|
|
|
7
8
|
/** Every name in the system, searchable by its wrong forms too, so a wrong name finds the right one. */
|
|
8
9
|
export function Glossary({ terms }: { terms: TermEntry[] }) {
|
|
@@ -24,7 +25,7 @@ export function Glossary({ terms }: { terms: TermEntry[] }) {
|
|
|
24
25
|
value={query}
|
|
25
26
|
onChange={(e) => setQuery(e.target.value)}
|
|
26
27
|
placeholder="Try a name you half remember"
|
|
27
|
-
className=
|
|
28
|
+
className={`${target} h-(--size-xl) max-w-sm rounded-md border border-input bg-canvas px-3 text-sm focus-visible:outline-2 focus-visible:outline-ring`}
|
|
28
29
|
/>
|
|
29
30
|
</div>
|
|
30
31
|
<p aria-live="polite" className="text-sm text-muted-foreground">
|
|
@@ -127,8 +127,10 @@ export interface RawElement {
|
|
|
127
127
|
/**
|
|
128
128
|
* `palette`: a primitive ramp step used directly, `bg-blue-500` or `text-primary-500`.
|
|
129
129
|
* `arbitrary`: a value in brackets, `p-[13px]` or `bg-[#fff]`.
|
|
130
|
+
* `status-text`: a status fill used as text, `text-destructive`: use `text-destructive-subdued-foreground`,
|
|
131
|
+
* which reads on every background, or neutral text with an icon for a warning.
|
|
130
132
|
*/
|
|
131
|
-
export type OffSystemKind = "palette" | "arbitrary";
|
|
133
|
+
export type OffSystemKind = "palette" | "arbitrary" | "status-text";
|
|
132
134
|
|
|
133
135
|
export interface OffSystemValue {
|
|
134
136
|
/** The class as written, variants included: `hover:bg-blue-500`. */
|
package/registry/lib/cx.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The smallest a pressable thing in the docs may be: 44px by default (WCAG 2.5.5).
|
|
3
|
+
* A client with its own rule sets `--docs-target` on the docs layout (`3rem` for
|
|
4
|
+
* 48px), so the site that states the rule also meets it. Links inside a sentence
|
|
5
|
+
* are exempt, as WCAG exempts them.
|
|
6
|
+
*/
|
|
7
|
+
export const target = "min-h-[var(--docs-target,2.75rem)] min-w-[var(--docs-target,2.75rem)]";
|
|
8
|
+
|
|
1
9
|
/** Join class names, skipping the falsy ones. */
|
|
2
10
|
export function cx(...classes: (string | false | null | undefined)[]): string {
|
|
3
11
|
return classes.filter(Boolean).join(" ");
|