@designtools/blocks 0.0.0-stage → 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 +31 -0
- package/LICENSE +21 -0
- package/README.md +121 -2
- package/dist/cli.js +385 -0
- package/package.json +61 -3
- 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 +99 -0
- package/registry/adherence-summary.tsx +110 -0
- package/registry/agent-view.tsx +58 -0
- package/registry/anatomy.tsx +89 -0
- package/registry/ask-claude.tsx +73 -0
- package/registry/code-view.tsx +77 -0
- package/registry/examples.tsx +97 -0
- package/registry/glossary.tsx +59 -0
- package/registry/imagery.tsx +39 -0
- package/registry/lib/adherence-types.ts +181 -0
- package/registry/lib/boundary.tsx +21 -0
- package/registry/lib/contrast.ts +44 -0
- package/registry/lib/cx.ts +12 -0
- package/registry/lib/jsx.ts +29 -0
- package/registry/lib/lookup.tsx +46 -0
- package/registry/lib/manifest-types.ts +284 -0
- package/registry/lib/manifest.ts +228 -0
- package/registry/lib/markdown.ts +288 -0
- package/registry/lib/search.ts +99 -0
- package/registry/lib/standards.ts +82 -0
- package/registry/lib/status.tsx +32 -0
- package/registry/lib/text.tsx +27 -0
- package/registry/logo-usage.tsx +112 -0
- package/registry/pattern.tsx +69 -0
- package/registry/playground.tsx +181 -0
- package/registry/preview-frame.tsx +106 -0
- package/registry/props-table.tsx +110 -0
- package/registry/registry.generated.tsx +9 -0
- package/registry/rule.tsx +99 -0
- package/registry/scale.tsx +115 -0
- package/registry/search.tsx +134 -0
- package/registry/shell.tsx +247 -0
- package/registry/standards.tsx +41 -0
- package/registry/swatches.tsx +225 -0
- package/registry/tsconfig.json +13 -0
- package/registry/type-ramp.tsx +105 -0
- package/registry/usage.tsx +73 -0
- package/registry/variant-matrix.tsx +105 -0
- package/registry/voice-terms.tsx +45 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CopyPage } from "../../../_components/copy-page";
|
|
2
|
+
import { Imagery } from "../../../_blocks/imagery";
|
|
3
|
+
import { DocsHeader } from "../../../_blocks/shell";
|
|
4
|
+
import { assets, provenance } from "../../../_data";
|
|
5
|
+
|
|
6
|
+
export default function ImageryPage() {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<DocsHeader title="Imagery and icons" lead="The approved icons and imagery, and how to use each." provenance={provenance}>
|
|
10
|
+
<CopyPage />
|
|
11
|
+
</DocsHeader>
|
|
12
|
+
<Imagery assets={assets.assets} />
|
|
13
|
+
</>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CopyPage } from "../../../_components/copy-page";
|
|
2
|
+
import { LogoUsage } from "../../../_blocks/logo-usage";
|
|
3
|
+
import { DocsHeader } from "../../../_blocks/shell";
|
|
4
|
+
import { assets, BASE, provenance, rules } from "../../../_data";
|
|
5
|
+
|
|
6
|
+
export default function Logo() {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<DocsHeader title="Logo" lead="Each logo on the surfaces it may sit on, at its smallest, with its clear space." provenance={provenance}>
|
|
10
|
+
<CopyPage />
|
|
11
|
+
</DocsHeader>
|
|
12
|
+
<LogoUsage assets={assets.assets} rules={rules.rules} rulesHref={`${BASE}/rules`} />
|
|
13
|
+
</>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CopyPage } from "../../_components/copy-page";
|
|
2
|
+
import { Markdown } from "../../_components/prose";
|
|
3
|
+
import { DocsHeader } from "../../_blocks/shell";
|
|
4
|
+
import { editorial } from "../../_data";
|
|
5
|
+
|
|
6
|
+
export default function Brand() {
|
|
7
|
+
const page = editorial.brand;
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<DocsHeader title={page.title} lead={page.lead} editorial={page.by}>
|
|
11
|
+
<CopyPage />
|
|
12
|
+
</DocsHeader>
|
|
13
|
+
<Markdown text={page.body} />
|
|
14
|
+
</>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CopyPage } from "../../../_components/copy-page";
|
|
2
|
+
import { DocsHeader } from "../../../_blocks/shell";
|
|
3
|
+
import { VoiceTerms } from "../../../_blocks/voice-terms";
|
|
4
|
+
import { provenance, taxonomy } from "../../../_data";
|
|
5
|
+
|
|
6
|
+
export default function Voice() {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<DocsHeader title="Voice and terms" lead="The words we use, beside the ones we do not." provenance={provenance}>
|
|
10
|
+
<CopyPage />
|
|
11
|
+
</DocsHeader>
|
|
12
|
+
<VoiceTerms terms={taxonomy.terms} />
|
|
13
|
+
</>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CopyPage } from "../../_components/copy-page";
|
|
2
|
+
import { DocsHeader, DocsSection } from "../../_blocks/shell";
|
|
3
|
+
import { PageContrast, RampSwatches, SemanticSwatches } from "../../_blocks/swatches";
|
|
4
|
+
import { provenance, tokens } from "../../_data";
|
|
5
|
+
|
|
6
|
+
export default function Colour() {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<DocsHeader title="Colour" lead="Generated from one brand colour by @designtools/tokens. Write the semantic names in components; the ramps are what they point at." provenance={provenance}>
|
|
10
|
+
<CopyPage />
|
|
11
|
+
</DocsHeader>
|
|
12
|
+
<DocsSection title="Semantic colours" lead="Each pair is measured on the colours the browser paints, in each mode.">
|
|
13
|
+
<SemanticSwatches tokens={tokens} />
|
|
14
|
+
</DocsSection>
|
|
15
|
+
<DocsSection
|
|
16
|
+
title="On the page"
|
|
17
|
+
lead="Each colour on each background. A label is measured on its own fill above; this is the rest: a colour used as text on the page needs 4.5:1, as a shape (a meter fill, an icon without words) 3:1."
|
|
18
|
+
>
|
|
19
|
+
<PageContrast tokens={tokens} />
|
|
20
|
+
</DocsSection>
|
|
21
|
+
<DocsSection title="Ramps">
|
|
22
|
+
<RampSwatches tokens={tokens} />
|
|
23
|
+
</DocsSection>
|
|
24
|
+
</>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
import { CopyPage } from "../../../_components/copy-page";
|
|
3
|
+
import { A11yPanel } from "../../../_blocks/a11y-panel";
|
|
4
|
+
import { AgentView } from "../../../_blocks/agent-view";
|
|
5
|
+
import { Anatomy } from "../../../_blocks/anatomy";
|
|
6
|
+
import { AskClaude } from "../../../_blocks/ask-claude";
|
|
7
|
+
import { Examples } from "../../../_blocks/examples";
|
|
8
|
+
import { Playground } from "../../../_blocks/playground";
|
|
9
|
+
import { PreviewFrame } from "../../../_blocks/preview-frame";
|
|
10
|
+
import { PropsTable } from "../../../_blocks/props-table";
|
|
11
|
+
import { DocsHeader, DocsSection } from "../../../_blocks/shell";
|
|
12
|
+
import { Usage } from "../../../_blocks/usage";
|
|
13
|
+
import { VariantMatrix } from "../../../_blocks/variant-matrix";
|
|
14
|
+
import { findBySlug, slugOf } from "../../../_blocks/lib/manifest";
|
|
15
|
+
import { BASE, components, provenance } from "../../../_data";
|
|
16
|
+
|
|
17
|
+
/** What a component needs to render at all. The rest comes from the controls. */
|
|
18
|
+
const needs: Record<string, { props?: Record<string, unknown>; children?: string }> = {
|
|
19
|
+
Select: {
|
|
20
|
+
props: { label: "Project", value: "beacon", options: [{ value: "atlas", label: "Atlas" }, { value: "beacon", label: "Beacon" }] },
|
|
21
|
+
children: "",
|
|
22
|
+
},
|
|
23
|
+
Switch: { props: { "aria-label": "Notifications" }, children: "" },
|
|
24
|
+
Alert: { props: { title: "Timesheets close on Friday" } },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function generateStaticParams() {
|
|
28
|
+
return components.components.map((c) => ({ slug: slugOf(c) }));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default async function ComponentPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
32
|
+
const { slug } = await params;
|
|
33
|
+
const entry = findBySlug(components, slug);
|
|
34
|
+
if (!entry) notFound();
|
|
35
|
+
const need = needs[entry.name] ?? {};
|
|
36
|
+
const preview = `${BASE}/preview/${slug}`;
|
|
37
|
+
const first = entry.examples?.canonical?.name;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<DocsHeader title={entry.name} lead={entry.description} source={entry.source} provenance={provenance}>
|
|
42
|
+
<CopyPage />
|
|
43
|
+
</DocsHeader>
|
|
44
|
+
|
|
45
|
+
<DocsSection title="Usage">
|
|
46
|
+
<Usage entry={entry} manifest={components} basePath={BASE} />
|
|
47
|
+
</DocsSection>
|
|
48
|
+
|
|
49
|
+
<DocsSection title="Examples" lead="Each is the real component. Copy the first one.">
|
|
50
|
+
<A11yPanel entry={entry}>
|
|
51
|
+
<Examples entry={entry} previewHref={preview} />
|
|
52
|
+
</A11yPanel>
|
|
53
|
+
</DocsSection>
|
|
54
|
+
|
|
55
|
+
<DocsSection title="Playground">
|
|
56
|
+
<AgentView entry={entry}>
|
|
57
|
+
<Playground entry={entry} props={need.props} children={need.children} />
|
|
58
|
+
</AgentView>
|
|
59
|
+
</DocsSection>
|
|
60
|
+
|
|
61
|
+
{!!entry.variants?.axes.length && (
|
|
62
|
+
<DocsSection title="Variants" lead={`From the ${entry.variants.from === "props" ? "props' types" : `${entry.variants.from}() config`}.`}>
|
|
63
|
+
<VariantMatrix entry={entry} props={need.props}>
|
|
64
|
+
{need.children === "" ? undefined : entry.name}
|
|
65
|
+
</VariantMatrix>
|
|
66
|
+
</DocsSection>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{first && (
|
|
70
|
+
<DocsSection title="At each width">
|
|
71
|
+
<PreviewFrame src={`${preview}/${first}`} title={`${entry.name}: ${first}`} />
|
|
72
|
+
</DocsSection>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
<DocsSection title="Anatomy">
|
|
76
|
+
<Anatomy entry={entry} />
|
|
77
|
+
</DocsSection>
|
|
78
|
+
|
|
79
|
+
<DocsSection title="Props">
|
|
80
|
+
<PropsTable entry={entry} />
|
|
81
|
+
</DocsSection>
|
|
82
|
+
|
|
83
|
+
<AskClaude entry={entry} />
|
|
84
|
+
</>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import wordmark from "@ds/brand/logos/wordmark.svg";
|
|
3
|
+
import { DocsShell } from "../_blocks/shell";
|
|
4
|
+
import { BASE, components, patterns, rules, taxonomy, TITLE } from "../_data";
|
|
5
|
+
|
|
6
|
+
export default function DocsLayout({ children }: { children: ReactNode }) {
|
|
7
|
+
return (
|
|
8
|
+
<DocsShell
|
|
9
|
+
manifest={components}
|
|
10
|
+
rules={rules}
|
|
11
|
+
patterns={patterns}
|
|
12
|
+
terms={taxonomy}
|
|
13
|
+
title={TITLE}
|
|
14
|
+
logo={<img src={wordmark.src} alt="" className="h-6 w-auto" />}
|
|
15
|
+
basePath={BASE}
|
|
16
|
+
brand={[
|
|
17
|
+
{ label: "Principles and language", href: `${BASE}/brand` },
|
|
18
|
+
{ label: "Logo", href: `${BASE}/brand/logo` },
|
|
19
|
+
{ label: "Imagery and icons", href: `${BASE}/brand/imagery` },
|
|
20
|
+
{ label: "Voice and terms", href: `${BASE}/brand/voice` },
|
|
21
|
+
]}
|
|
22
|
+
foundations={[
|
|
23
|
+
{ label: "Colour", href: `${BASE}/colour` },
|
|
24
|
+
{ label: "Type", href: `${BASE}/type` },
|
|
25
|
+
{ label: "Scale", href: `${BASE}/scale` },
|
|
26
|
+
]}
|
|
27
|
+
standards={[
|
|
28
|
+
{ label: "Standards", href: `${BASE}/standards` },
|
|
29
|
+
{ label: "Vocabulary", href: `${BASE}/vocabulary` },
|
|
30
|
+
]}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</DocsShell>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { CopyPage } from "../_components/copy-page";
|
|
3
|
+
import { DocsHeader, DocsSection } from "../_blocks/shell";
|
|
4
|
+
import { groupFamilies, slugOf } from "../_blocks/lib/manifest";
|
|
5
|
+
import { StatusBadge } from "../_blocks/lib/status";
|
|
6
|
+
import { BASE, components, provenance, TITLE } from "../_data";
|
|
7
|
+
|
|
8
|
+
export default function Overview() {
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<DocsHeader
|
|
12
|
+
title={TITLE}
|
|
13
|
+
lead="The brand and the design system in one place, for the people who make the product and the agents that build it with them. Every page is built from the code."
|
|
14
|
+
provenance={provenance}
|
|
15
|
+
>
|
|
16
|
+
<p className="text-sm">
|
|
17
|
+
For agents: <a href={`${BASE}.md`} className="underline underline-offset-2">this site as markdown</a>, the whole
|
|
18
|
+
system in <a href={`${BASE}/md/context`} className="underline underline-offset-2">one file</a>, or the{" "}
|
|
19
|
+
<a href={`${BASE}/manifest/components.json`} className="underline underline-offset-2">manifest as data</a>. Add{" "}
|
|
20
|
+
<code className="font-mono text-xs">.md</code> to any page.
|
|
21
|
+
</p>
|
|
22
|
+
<CopyPage />
|
|
23
|
+
</DocsHeader>
|
|
24
|
+
{/* a component's parts (CardHeader, CardTitle) are named on its tile, not given tiles of their own */}
|
|
25
|
+
{groupFamilies(components).map((group) => (
|
|
26
|
+
<DocsSection key={group.category} title={group.category}>
|
|
27
|
+
<ul className="grid gap-3 sm:grid-cols-2">
|
|
28
|
+
{group.families.map(({ head: c, parts }) => (
|
|
29
|
+
<li key={c.source + c.export}>
|
|
30
|
+
<Link
|
|
31
|
+
href={`${BASE}/components/${slugOf(c)}`}
|
|
32
|
+
className="flex h-full flex-col gap-2 rounded-lg border border-border p-4 hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring"
|
|
33
|
+
>
|
|
34
|
+
<span className="flex items-center justify-between gap-2">
|
|
35
|
+
<span className="font-medium">{c.name}</span>
|
|
36
|
+
<StatusBadge status={c.status} />
|
|
37
|
+
</span>
|
|
38
|
+
{c.description && <span className="line-clamp-2 text-sm text-muted-foreground">{c.description}</span>}
|
|
39
|
+
{parts.length > 0 && <span className="font-mono text-xs text-muted-foreground">with {parts.map((p) => p.name).join(", ")}</span>}
|
|
40
|
+
</Link>
|
|
41
|
+
</li>
|
|
42
|
+
))}
|
|
43
|
+
</ul>
|
|
44
|
+
</DocsSection>
|
|
45
|
+
))}
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
import { CopyPage } from "../../../_components/copy-page";
|
|
3
|
+
import { Pattern } from "../../../_blocks/pattern";
|
|
4
|
+
import { DocsHeader } from "../../../_blocks/shell";
|
|
5
|
+
import { BASE, components, patterns, provenance } from "../../../_data";
|
|
6
|
+
|
|
7
|
+
export function generateStaticParams() {
|
|
8
|
+
return patterns.patterns.map((p) => ({ id: p.id }));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default async function PatternPage({ params }: { params: Promise<{ id: string }> }) {
|
|
12
|
+
const { id } = await params;
|
|
13
|
+
const pattern = patterns.patterns.find((p) => p.id === id);
|
|
14
|
+
if (!pattern) notFound();
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<DocsHeader title={pattern.name} lead={pattern.description} source={pattern.source} provenance={provenance}>
|
|
18
|
+
<CopyPage />
|
|
19
|
+
</DocsHeader>
|
|
20
|
+
<Pattern pattern={pattern} manifest={components} basePath={BASE} />
|
|
21
|
+
</>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { CopyPage } from "../../_components/copy-page";
|
|
2
|
+
import { Rule } from "../../_blocks/rule";
|
|
3
|
+
import { DocsHeader, DocsSection } from "../../_blocks/shell";
|
|
4
|
+
import { provenance, rules } from "../../_data";
|
|
5
|
+
|
|
6
|
+
/** A demonstration for the rule that needs persuading: the same control, too small and big enough. */
|
|
7
|
+
function TouchTargets() {
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex flex-wrap items-end gap-8 rounded-lg bg-muted p-6">
|
|
10
|
+
{[32, 48].map((size) => (
|
|
11
|
+
<figure key={size} className="flex flex-col items-center gap-2">
|
|
12
|
+
<span className="flex items-center justify-center border border-dashed border-destructive" style={{ width: size, height: size }}>
|
|
13
|
+
<span className="size-4 rounded-full bg-primary" />
|
|
14
|
+
</span>
|
|
15
|
+
<figcaption className="text-xs text-muted-foreground">
|
|
16
|
+
{size} px {size < 48 ? "is missed" : "is hit"}
|
|
17
|
+
</figcaption>
|
|
18
|
+
</figure>
|
|
19
|
+
))}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function Rules() {
|
|
25
|
+
const groups = (["brand", "interface"] as const).map((kind) => ({ kind, rules: rules.rules.filter((r) => r.kind === kind) }));
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
<DocsHeader title="Rules" lead="Brand and interface rules in one format, each with the check that proves it. Written for people; agents read the same rule." provenance={provenance}>
|
|
29
|
+
<CopyPage />
|
|
30
|
+
</DocsHeader>
|
|
31
|
+
{groups.map((g) => (
|
|
32
|
+
<DocsSection key={g.kind} title={g.kind === "brand" ? "Brand" : "Interface"}>
|
|
33
|
+
{g.rules.map((r) => (
|
|
34
|
+
<Rule key={r.id} rule={r}>
|
|
35
|
+
{r.id === "touch-target" ? <TouchTargets /> : null}
|
|
36
|
+
</Rule>
|
|
37
|
+
))}
|
|
38
|
+
</DocsSection>
|
|
39
|
+
))}
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CopyPage } from "../../_components/copy-page";
|
|
2
|
+
import { Scale } from "../../_blocks/scale";
|
|
3
|
+
import { DocsHeader } from "../../_blocks/shell";
|
|
4
|
+
import { provenance, tokens } from "../../_data";
|
|
5
|
+
|
|
6
|
+
export default function ScalePage() {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<DocsHeader title="Scale" lead="Space, size, radius, elevation and motion. Hover a motion row to play it." provenance={provenance}>
|
|
10
|
+
<CopyPage />
|
|
11
|
+
</DocsHeader>
|
|
12
|
+
<Scale tokens={tokens} />
|
|
13
|
+
</>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CopyPage } from "../../_components/copy-page";
|
|
2
|
+
import { DocsHeader, DocsSection } from "../../_blocks/shell";
|
|
3
|
+
import { AdherenceSummary } from "../../_blocks/adherence-summary";
|
|
4
|
+
import { Standards } from "../../_blocks/standards";
|
|
5
|
+
import { adherence, baselines, provenance } from "../../_data";
|
|
6
|
+
|
|
7
|
+
export default function StandardsPage() {
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<DocsHeader title="Standards" lead="The budgets and targets the project holds itself to, read from .mxa/stack.json, where the checks read them too." provenance={provenance}>
|
|
11
|
+
<CopyPage />
|
|
12
|
+
</DocsHeader>
|
|
13
|
+
<DocsSection title="Targets">
|
|
14
|
+
<Standards baselines={baselines} />
|
|
15
|
+
</DocsSection>
|
|
16
|
+
<DocsSection title="Adherence" lead="How closely the product follows the system, from @designtools/adherence. It informs; it never blocks.">
|
|
17
|
+
<AdherenceSummary report={adherence} />
|
|
18
|
+
</DocsSection>
|
|
19
|
+
</>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CopyPage } from "../../_components/copy-page";
|
|
2
|
+
import { DocsHeader } from "../../_blocks/shell";
|
|
3
|
+
import { TypeRamp } from "../../_blocks/type-ramp";
|
|
4
|
+
import { provenance, tokens } from "../../_data";
|
|
5
|
+
|
|
6
|
+
export default function Type() {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<DocsHeader title="Type" lead="A size is never set without its leading." provenance={provenance}>
|
|
10
|
+
<CopyPage />
|
|
11
|
+
</DocsHeader>
|
|
12
|
+
<TypeRamp tokens={tokens} />
|
|
13
|
+
</>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CopyPage } from "../../_components/copy-page";
|
|
2
|
+
import { Glossary } from "../../_blocks/glossary";
|
|
3
|
+
import { DocsHeader } from "../../_blocks/shell";
|
|
4
|
+
import { provenance, taxonomy } from "../../_data";
|
|
5
|
+
|
|
6
|
+
export default function Vocabulary() {
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
<DocsHeader title="Vocabulary" lead="Every name in the system. Search by a wrong name and it finds the right one." provenance={provenance}>
|
|
10
|
+
<CopyPage />
|
|
11
|
+
</DocsHeader>
|
|
12
|
+
<Glossary terms={taxonomy.terms} />
|
|
13
|
+
</>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { usePathname } from "next/navigation";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
|
|
6
|
+
/** Copies this page's markdown, for people handing it to an assistant. The same markdown any agent gets at `.md`. */
|
|
7
|
+
export function CopyPage() {
|
|
8
|
+
const path = usePathname();
|
|
9
|
+
const [state, setState] = useState<"idle" | "copied" | "failed">("idle");
|
|
10
|
+
const copy = async () => {
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch(`${path.replace(/\/$/, "")}.md`);
|
|
13
|
+
await navigator.clipboard.writeText(await res.text());
|
|
14
|
+
setState("copied");
|
|
15
|
+
} catch {
|
|
16
|
+
setState("failed");
|
|
17
|
+
}
|
|
18
|
+
setTimeout(() => setState("idle"), 1500);
|
|
19
|
+
};
|
|
20
|
+
return (
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={copy}
|
|
24
|
+
className="min-h-[var(--docs-target,2.75rem)] self-start rounded-md border border-border px-2.5 py-1 text-xs hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring"
|
|
25
|
+
>
|
|
26
|
+
{state === "copied" ? "Copied" : state === "failed" ? "Could not copy" : "Copy page as markdown"}
|
|
27
|
+
<span className="sr-only" aria-live="polite">
|
|
28
|
+
{state === "copied" ? "Page copied" : ""}
|
|
29
|
+
</span>
|
|
30
|
+
</button>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -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
|
+
}
|