@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,115 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { shortName, tokensOfTier, withGroupDescriptions } from "./lib/manifest";
|
|
3
|
+
import type { TokenEntry, TokenTier, TokensManifest } from "./lib/manifest-types";
|
|
4
|
+
|
|
5
|
+
/** Steps drawn for a single `--spacing` multiplier, as Tailwind's p-1, p-2, … would use it. */
|
|
6
|
+
const SPACING_STEPS = [1, 2, 3, 4, 6, 8, 12, 16, 24];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Everything that is not colour or type: space, size, radius, elevation and
|
|
10
|
+
* motion, each tier drawn the way it is used. Hover a motion row to play it.
|
|
11
|
+
* Pass `tiers` to show only some, in your order.
|
|
12
|
+
*/
|
|
13
|
+
export function Scale({
|
|
14
|
+
tokens,
|
|
15
|
+
tiers = ["spacing", "size", "radius", "shadow", "duration", "ease"],
|
|
16
|
+
}: {
|
|
17
|
+
tokens: TokensManifest;
|
|
18
|
+
tiers?: TokenTier[];
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<div data-slot="scale" className="flex flex-col gap-10">
|
|
22
|
+
{tiers.map((tier) => {
|
|
23
|
+
const rows = tokensOfTier(tokens, tier);
|
|
24
|
+
if (rows.length === 0) return null;
|
|
25
|
+
return (
|
|
26
|
+
<div key={tier} data-slot="scale-tier" className="flex flex-col gap-2">
|
|
27
|
+
<h3 className="text-sm font-medium capitalize">{tier}</h3>
|
|
28
|
+
{tier === "spacing" ? <Spacing rows={rows} /> : <Rows rows={rows} tier={tier} />}
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
})}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function Spacing({ rows }: { rows: TokenEntry[] }) {
|
|
37
|
+
const multiplier = rows.find((t) => t.name === "--spacing");
|
|
38
|
+
const named = rows.filter((t) => t !== multiplier);
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex flex-col gap-4">
|
|
41
|
+
{multiplier && (
|
|
42
|
+
<>
|
|
43
|
+
<p className="text-xs text-muted-foreground">
|
|
44
|
+
{multiplier.description ?? "One multiplier"}: <span className="font-mono">--spacing: {multiplier.values.default}</span>
|
|
45
|
+
</p>
|
|
46
|
+
<ul className="flex flex-col gap-1.5">
|
|
47
|
+
{SPACING_STEPS.map((n) => (
|
|
48
|
+
<li key={n} className="grid grid-cols-[4rem_1fr] items-center gap-3 font-mono text-xs">
|
|
49
|
+
<span className="text-muted-foreground">{n}</span>
|
|
50
|
+
<span data-slot="scale-bar" className="h-3 rounded-sm bg-primary" style={{ width: `calc(var(--spacing) * ${n})` }} />
|
|
51
|
+
</li>
|
|
52
|
+
))}
|
|
53
|
+
</ul>
|
|
54
|
+
</>
|
|
55
|
+
)}
|
|
56
|
+
{named.length > 0 && <Rows rows={named} tier="spacing" />}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function Rows({ rows, tier }: { rows: TokenEntry[]; tier: TokenTier }) {
|
|
62
|
+
return (
|
|
63
|
+
<ul className="flex flex-col divide-y divide-border border-y border-border">
|
|
64
|
+
{withGroupDescriptions(rows).map((t) => (
|
|
65
|
+
<li key={t.name} data-slot="scale-row" className="group flex flex-col gap-2 py-3">
|
|
66
|
+
{t.groupDescription && <p className="text-xs text-muted-foreground">{t.groupDescription}</p>}
|
|
67
|
+
<div className="grid grid-cols-1 items-center gap-3 md:grid-cols-[10rem_minmax(0,1fr)]">
|
|
68
|
+
<span className="flex min-w-0 flex-col font-mono text-xs text-muted-foreground">
|
|
69
|
+
<span className="font-medium text-canvas-foreground">{shortName(t, tier)}</span>
|
|
70
|
+
<span className="truncate" title={t.values.default}>
|
|
71
|
+
{t.values.default}
|
|
72
|
+
</span>
|
|
73
|
+
</span>
|
|
74
|
+
<span className="min-w-0">{sample(t, tier)}</span>
|
|
75
|
+
</div>
|
|
76
|
+
</li>
|
|
77
|
+
))}
|
|
78
|
+
</ul>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sample(t: TokenEntry, tier: TokenTier): ReactNode {
|
|
83
|
+
const v = `var(${t.name})`;
|
|
84
|
+
switch (tier) {
|
|
85
|
+
case "spacing":
|
|
86
|
+
return <span className="block h-3 rounded-sm bg-primary" style={{ width: v }} />;
|
|
87
|
+
case "size":
|
|
88
|
+
return <span className="block rounded-sm bg-primary" style={{ width: v, height: v }} />;
|
|
89
|
+
case "radius":
|
|
90
|
+
return <span className="block size-16 border-2 border-primary bg-primary-subdued" style={{ borderRadius: v }} />;
|
|
91
|
+
case "shadow":
|
|
92
|
+
return <span className="block h-16 w-32 rounded-md bg-surface" style={{ boxShadow: v }} />;
|
|
93
|
+
case "duration":
|
|
94
|
+
return <Motion duration={v} easing="var(--ease-out, ease-out)" />;
|
|
95
|
+
case "ease":
|
|
96
|
+
return <Motion duration="var(--duration-slow, 400ms)" easing={v} />;
|
|
97
|
+
default:
|
|
98
|
+
return <span className="font-mono text-xs">{t.values.default}</span>;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A dot that crosses its track on hover, with the token's duration or easing. Reduced motion jumps instead. */
|
|
103
|
+
function Motion({ duration, easing }: { duration: string; easing: string }) {
|
|
104
|
+
return (
|
|
105
|
+
<span data-slot="scale-motion" className="relative block h-4 w-full max-w-sm rounded-full bg-muted">
|
|
106
|
+
{/* the runner is the track less the dot, so translating it by its own width lands the dot at the end */}
|
|
107
|
+
<span
|
|
108
|
+
className="absolute inset-y-0 right-4 left-0 block transition-transform group-hover:translate-x-full motion-reduce:transition-none"
|
|
109
|
+
style={{ transitionDuration: duration, transitionTimingFunction: easing }}
|
|
110
|
+
>
|
|
111
|
+
<span className="block size-4 rounded-full bg-primary" />
|
|
112
|
+
</span>
|
|
113
|
+
</span>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Autocomplete } from "@base-ui/react/autocomplete";
|
|
4
|
+
import { Dialog } from "@base-ui/react/dialog";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
import { useEffect, useId, useMemo, useState } from "react";
|
|
7
|
+
import { cx, target } from "./lib/cx";
|
|
8
|
+
import { search, type SearchGroup, type SearchItem } from "./lib/search";
|
|
9
|
+
|
|
10
|
+
const PLURAL: Record<SearchGroup["kind"], string> = { Component: "Components", Rule: "Rules", Pattern: "Patterns", Page: "Pages" };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Find a component, a part, a rule, a pattern or a page by name, fuzzily, and go
|
|
14
|
+
* there. A wrong name finds the right thing ("Dropdown" finds Select, and says
|
|
15
|
+
* so). Opens with ⌘K or Ctrl+K anywhere, or / when you are not typing. Build the
|
|
16
|
+
* items with `searchIndex` from `lib/search`; the shell does, when you pass it the
|
|
17
|
+
* taxonomy. Navigation only: it does not search page text.
|
|
18
|
+
*/
|
|
19
|
+
export function DocsSearch({
|
|
20
|
+
items,
|
|
21
|
+
shortcuts = true,
|
|
22
|
+
className,
|
|
23
|
+
}: {
|
|
24
|
+
items: SearchItem[];
|
|
25
|
+
/** Listen for ⌘K, Ctrl+K and /. Give one search on a page the shortcuts. */
|
|
26
|
+
shortcuts?: boolean;
|
|
27
|
+
className?: string;
|
|
28
|
+
}) {
|
|
29
|
+
const router = useRouter();
|
|
30
|
+
const [open, setOpen] = useState(false);
|
|
31
|
+
const [query, setQuery] = useState("");
|
|
32
|
+
const [shortcut, setShortcut] = useState("Ctrl K");
|
|
33
|
+
const hintId = useId();
|
|
34
|
+
const groups = useMemo(() => search(items, query), [items, query]);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (/Mac|iPhone|iPad/.test(navigator.platform)) setShortcut("⌘K");
|
|
38
|
+
if (!shortcuts) return;
|
|
39
|
+
const onKey = (e: KeyboardEvent) => {
|
|
40
|
+
const typing = e.target instanceof HTMLElement && (e.target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName));
|
|
41
|
+
if ((e.key === "k" && (e.metaKey || e.ctrlKey)) || (e.key === "/" && !typing && !e.metaKey && !e.ctrlKey && !e.altKey)) {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
setOpen(true);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
window.addEventListener("keydown", onKey);
|
|
47
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
48
|
+
}, [shortcuts]);
|
|
49
|
+
|
|
50
|
+
const go = (item: SearchItem) => {
|
|
51
|
+
setOpen(false);
|
|
52
|
+
setQuery("");
|
|
53
|
+
router.push(item.href);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Dialog.Root open={open} onOpenChange={(next) => (setOpen(next), next || setQuery(""))}>
|
|
58
|
+
<Dialog.Trigger
|
|
59
|
+
data-slot="docs-search-trigger"
|
|
60
|
+
className={cx(
|
|
61
|
+
target,
|
|
62
|
+
"flex w-full items-center justify-between gap-3 rounded-md border border-input bg-canvas px-3 text-sm text-muted-foreground hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
|
|
63
|
+
className,
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
<span>Search</span>
|
|
67
|
+
<kbd className="font-mono text-xs pointer-coarse:hidden">{shortcut}</kbd>
|
|
68
|
+
</Dialog.Trigger>
|
|
69
|
+
<Dialog.Portal>
|
|
70
|
+
<Dialog.Backdrop className="fixed inset-0 bg-canvas-foreground/30" />
|
|
71
|
+
<Dialog.Viewport className="fixed inset-0 flex items-start justify-center px-4 pt-[12dvh]">
|
|
72
|
+
<Dialog.Popup
|
|
73
|
+
data-slot="docs-search"
|
|
74
|
+
aria-label="Search the design system"
|
|
75
|
+
className="flex max-h-[min(36rem,80dvh)] w-full max-w-lg flex-col overflow-hidden rounded-lg border border-border bg-overlay text-overlay-foreground shadow-lg"
|
|
76
|
+
>
|
|
77
|
+
<Autocomplete.Root
|
|
78
|
+
open
|
|
79
|
+
inline
|
|
80
|
+
items={groups}
|
|
81
|
+
filter={null}
|
|
82
|
+
value={query}
|
|
83
|
+
onValueChange={(next) => setQuery(next)}
|
|
84
|
+
itemToStringValue={(item: SearchItem) => item.label}
|
|
85
|
+
autoHighlight="always"
|
|
86
|
+
keepHighlight
|
|
87
|
+
>
|
|
88
|
+
<Autocomplete.Input
|
|
89
|
+
aria-label="Search components, rules, patterns and pages"
|
|
90
|
+
aria-describedby={hintId}
|
|
91
|
+
placeholder="Search components, rules, patterns…"
|
|
92
|
+
className={cx(target, "w-full border-b border-border bg-transparent px-4 text-base outline-none placeholder:text-muted-foreground")}
|
|
93
|
+
/>
|
|
94
|
+
<Dialog.Close className="sr-only">Close search</Dialog.Close>
|
|
95
|
+
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain py-2">
|
|
96
|
+
<Autocomplete.Empty>
|
|
97
|
+
<p className="px-4 py-6 text-sm text-muted-foreground">{query.trim() ? `Nothing called “${query.trim()}”.` : "Type a name, or a name you half remember."}</p>
|
|
98
|
+
</Autocomplete.Empty>
|
|
99
|
+
<Autocomplete.List>
|
|
100
|
+
{(group: SearchGroup) => (
|
|
101
|
+
<Autocomplete.Group key={group.kind} items={group.items} className="not-last:mb-2">
|
|
102
|
+
<Autocomplete.GroupLabel className="px-4 py-1 text-xs font-medium text-muted-foreground">{PLURAL[group.kind]}</Autocomplete.GroupLabel>
|
|
103
|
+
<Autocomplete.Collection>
|
|
104
|
+
{(item: SearchGroup["items"][number]) => (
|
|
105
|
+
<Autocomplete.Item
|
|
106
|
+
key={item.href}
|
|
107
|
+
value={item}
|
|
108
|
+
onClick={() => go(item)}
|
|
109
|
+
className={cx(
|
|
110
|
+
target,
|
|
111
|
+
"flex cursor-default items-center justify-between gap-3 px-4 py-2 text-sm outline-none select-none data-highlighted:bg-muted",
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
<span className="min-w-0 truncate font-medium">{item.label}</span>
|
|
115
|
+
<span className="shrink-0 text-xs text-muted-foreground">
|
|
116
|
+
{item.matched ? `for “${item.matched}”` : item.detail}
|
|
117
|
+
</span>
|
|
118
|
+
</Autocomplete.Item>
|
|
119
|
+
)}
|
|
120
|
+
</Autocomplete.Collection>
|
|
121
|
+
</Autocomplete.Group>
|
|
122
|
+
)}
|
|
123
|
+
</Autocomplete.List>
|
|
124
|
+
</div>
|
|
125
|
+
<p id={hintId} className="border-t border-border px-4 py-2 text-xs text-muted-foreground">
|
|
126
|
+
Arrow keys to move, Enter to go, Escape to close.
|
|
127
|
+
</p>
|
|
128
|
+
</Autocomplete.Root>
|
|
129
|
+
</Dialog.Popup>
|
|
130
|
+
</Dialog.Viewport>
|
|
131
|
+
</Dialog.Portal>
|
|
132
|
+
</Dialog.Root>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { type ReactNode, useMemo } from "react";
|
|
6
|
+
import { cx, target } from "./lib/cx";
|
|
7
|
+
import { groupFamilies, slugOf } from "./lib/manifest";
|
|
8
|
+
import { Prose } from "./lib/text";
|
|
9
|
+
import type { ComponentsManifest, PatternsManifest, RulesManifest, TaxonomyManifest } from "./lib/manifest-types";
|
|
10
|
+
import { searchIndex } from "./lib/search";
|
|
11
|
+
import { DocsSearch } from "./search";
|
|
12
|
+
|
|
13
|
+
export interface DocsLink {
|
|
14
|
+
label: string;
|
|
15
|
+
href: string;
|
|
16
|
+
/** Shown beneath it while you are on it or one of them: a component's parts. */
|
|
17
|
+
children?: DocsLink[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The docs frame, in the site's order: brand, foundations, rules, patterns,
|
|
22
|
+
* components by `@category`, standards. Editorial pages are the links you pass;
|
|
23
|
+
* rules, patterns and components come from the manifest. On a narrow screen
|
|
24
|
+
* the navigation folds into a disclosure. Put it in the docs layout; it marks
|
|
25
|
+
* the page you are on.
|
|
26
|
+
*/
|
|
27
|
+
export function DocsShell({
|
|
28
|
+
manifest,
|
|
29
|
+
rules,
|
|
30
|
+
patterns,
|
|
31
|
+
terms,
|
|
32
|
+
search = true,
|
|
33
|
+
title = "Design system",
|
|
34
|
+
logo,
|
|
35
|
+
basePath = "/design/system",
|
|
36
|
+
brand = [],
|
|
37
|
+
foundations = [],
|
|
38
|
+
standards = [],
|
|
39
|
+
children,
|
|
40
|
+
}: {
|
|
41
|
+
manifest: ComponentsManifest;
|
|
42
|
+
/** Listed under Rules, each linking to its anchor on `${basePath}/rules`. */
|
|
43
|
+
rules?: RulesManifest;
|
|
44
|
+
/** Listed under Patterns, each at `${basePath}/patterns/${id}`. */
|
|
45
|
+
patterns?: PatternsManifest;
|
|
46
|
+
/** The taxonomy, so search finds a component by a wrong name ("Dropdown" finds Select). */
|
|
47
|
+
terms?: TaxonomyManifest;
|
|
48
|
+
/** Search over components, parts, rules, patterns and pages, on ⌘K, Ctrl+K or /. On by default. */
|
|
49
|
+
search?: boolean;
|
|
50
|
+
title?: string;
|
|
51
|
+
/** The client's logo, shown beside the title: the site is theirs. */
|
|
52
|
+
logo?: ReactNode;
|
|
53
|
+
/** Component pages live at `${basePath}/components/${slug}`. */
|
|
54
|
+
basePath?: string;
|
|
55
|
+
/** Brand pages: principles, design language, voice, logo, imagery. */
|
|
56
|
+
brand?: DocsLink[];
|
|
57
|
+
/** Foundations pages: colour, type, scale. */
|
|
58
|
+
foundations?: DocsLink[];
|
|
59
|
+
/** Standards pages: budgets, accessibility, adherence. */
|
|
60
|
+
standards?: DocsLink[];
|
|
61
|
+
children: ReactNode;
|
|
62
|
+
}) {
|
|
63
|
+
const current = usePathname();
|
|
64
|
+
const searchItems = useMemo(
|
|
65
|
+
() =>
|
|
66
|
+
searchIndex({
|
|
67
|
+
manifest,
|
|
68
|
+
rules: rules?.rules,
|
|
69
|
+
patterns: patterns?.patterns,
|
|
70
|
+
terms: terms?.terms,
|
|
71
|
+
pages: [...brand, ...foundations, ...(rules?.rules.length ? [{ label: "Rules", href: `${basePath}/rules` }] : []), ...standards],
|
|
72
|
+
basePath,
|
|
73
|
+
}),
|
|
74
|
+
[manifest, rules, patterns, terms, brand, foundations, standards, basePath],
|
|
75
|
+
);
|
|
76
|
+
const groups = [
|
|
77
|
+
...(brand.length ? [{ heading: "Brand", links: brand }] : []),
|
|
78
|
+
...(foundations.length ? [{ heading: "Foundations", links: foundations }] : []),
|
|
79
|
+
...(rules?.rules.length
|
|
80
|
+
? [{ heading: "Rules", links: [{ label: "All rules", href: `${basePath}/rules` }] }]
|
|
81
|
+
: []),
|
|
82
|
+
...(patterns?.patterns.length
|
|
83
|
+
? [{ heading: "Patterns", links: patterns.patterns.map((p) => ({ label: p.name, href: `${basePath}/patterns/${p.id}` })) }]
|
|
84
|
+
: []),
|
|
85
|
+
...groupFamilies(manifest).map((g) => ({
|
|
86
|
+
heading: g.category,
|
|
87
|
+
links: g.families.map(({ head, parts }) => ({
|
|
88
|
+
label: head.name,
|
|
89
|
+
href: `${basePath}/components/${slugOf(head)}`,
|
|
90
|
+
children: parts.map((p) => ({ label: p.name, href: `${basePath}/components/${slugOf(p)}` })),
|
|
91
|
+
})),
|
|
92
|
+
})),
|
|
93
|
+
...(standards.length ? [{ heading: "Standards", links: standards }] : []),
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const nav = (
|
|
97
|
+
<nav aria-label={title} data-slot="docs-nav" className="flex flex-col gap-6 p-4 text-sm">
|
|
98
|
+
{groups.map((group) => (
|
|
99
|
+
<div key={group.heading} data-slot="docs-nav-group" className="flex flex-col gap-1">
|
|
100
|
+
<p data-slot="docs-nav-heading" className="px-2 text-xs font-medium text-muted-foreground">
|
|
101
|
+
{group.heading}
|
|
102
|
+
</p>
|
|
103
|
+
<ul className="flex flex-col">
|
|
104
|
+
{group.links.map((link) => {
|
|
105
|
+
const open = current === link.href || link.children?.some((c) => c.href === current);
|
|
106
|
+
return (
|
|
107
|
+
<li key={link.href}>
|
|
108
|
+
<NavLink link={link} current={current} />
|
|
109
|
+
{open && link.children?.length ? (
|
|
110
|
+
<ul data-slot="docs-nav-parts" aria-label={`${link.label} parts`} className="ml-3 flex flex-col border-l border-border pl-2">
|
|
111
|
+
{link.children.map((child) => (
|
|
112
|
+
<li key={child.href}>
|
|
113
|
+
<NavLink link={child} current={current} />
|
|
114
|
+
</li>
|
|
115
|
+
))}
|
|
116
|
+
</ul>
|
|
117
|
+
) : null}
|
|
118
|
+
</li>
|
|
119
|
+
);
|
|
120
|
+
})}
|
|
121
|
+
</ul>
|
|
122
|
+
</div>
|
|
123
|
+
))}
|
|
124
|
+
</nav>
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div data-slot="docs-shell" className="min-h-dvh bg-canvas text-canvas-foreground md:grid md:grid-cols-[16rem_1fr]">
|
|
129
|
+
<aside
|
|
130
|
+
data-slot="docs-sidebar"
|
|
131
|
+
className="hidden border-r border-border bg-surface text-surface-foreground md:sticky md:top-0 md:block md:h-dvh md:overflow-y-auto"
|
|
132
|
+
>
|
|
133
|
+
<Link href={basePath} data-slot="docs-title" className={cx(target, "flex flex-col items-start gap-2 px-6 pt-6 pb-2 font-semibold")}>
|
|
134
|
+
{logo && <span data-slot="docs-logo">{logo}</span>}
|
|
135
|
+
<span>{title}</span>
|
|
136
|
+
</Link>
|
|
137
|
+
{search && (
|
|
138
|
+
<div className="px-4 pt-2">
|
|
139
|
+
<DocsSearch items={searchItems} />
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
{nav}
|
|
143
|
+
</aside>
|
|
144
|
+
<details data-slot="docs-nav-disclosure" className="border-b border-border bg-surface text-surface-foreground md:hidden">
|
|
145
|
+
<summary className={cx(target, "flex cursor-pointer items-center gap-3 px-4 py-3 font-semibold")}>
|
|
146
|
+
{logo && <span data-slot="docs-logo">{logo}</span>}
|
|
147
|
+
<span>{title}</span>
|
|
148
|
+
</summary>
|
|
149
|
+
{nav}
|
|
150
|
+
</details>
|
|
151
|
+
{search && (
|
|
152
|
+
// outside the disclosure, so a phone can search without opening the navigation; the
|
|
153
|
+
// sidebar's search owns the keyboard shortcut, so one key press opens one dialog
|
|
154
|
+
<div className="border-b border-border bg-surface px-4 py-2 md:hidden">
|
|
155
|
+
<DocsSearch items={searchItems} shortcuts={false} />
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
<main data-slot="docs-main" className="min-w-0 px-4 py-8 md:px-10 md:py-12">
|
|
159
|
+
<div className="mx-auto flex max-w-5xl flex-col gap-12">{children}</div>
|
|
160
|
+
</main>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A page's heading, lead and source, and where it comes from: a generated page
|
|
167
|
+
* names the version and commit it was built from; an editorial page names its
|
|
168
|
+
* author, when it was last reviewed, and whether it is settled. Never both, and
|
|
169
|
+
* never a review date on a generated page, which would be invented.
|
|
170
|
+
*/
|
|
171
|
+
export function DocsHeader({
|
|
172
|
+
title,
|
|
173
|
+
lead,
|
|
174
|
+
source,
|
|
175
|
+
provenance,
|
|
176
|
+
editorial,
|
|
177
|
+
children,
|
|
178
|
+
}: {
|
|
179
|
+
title: string;
|
|
180
|
+
lead?: string;
|
|
181
|
+
/** Shown as the file the page documents. */
|
|
182
|
+
source?: string;
|
|
183
|
+
/** For a generated page: `Generated from acme-web@1.4.0 at 3f2a9c1`, from `designtools-manifest provenance`. */
|
|
184
|
+
provenance?: string;
|
|
185
|
+
/** For an editorial page. */
|
|
186
|
+
/** `reviewer` when the person who signs it off is not the author: the studio writes, the client's brand lead reviews. */
|
|
187
|
+
editorial?: { author: string; reviewer?: string; reviewed: string; status: "draft" | "published" };
|
|
188
|
+
children?: ReactNode;
|
|
189
|
+
}) {
|
|
190
|
+
return (
|
|
191
|
+
<header data-slot="docs-header" className="flex flex-col gap-3">
|
|
192
|
+
{(provenance || editorial) && (
|
|
193
|
+
<p data-slot="docs-provenance" className="text-xs text-muted-foreground">
|
|
194
|
+
{editorial
|
|
195
|
+
? `Written by ${editorial.author} · ${editorial.reviewer ? `reviewed by ${editorial.reviewer}, ` : "last reviewed "}${editorial.reviewed} · ${editorial.status}`
|
|
196
|
+
: `${provenance} · generated from the code`}
|
|
197
|
+
</p>
|
|
198
|
+
)}
|
|
199
|
+
<h1 className="text-3xl font-semibold">{title}</h1>
|
|
200
|
+
{lead && (
|
|
201
|
+
<div data-slot="docs-lead" className="flex max-w-prose flex-col gap-3 text-muted-foreground">
|
|
202
|
+
<Prose text={lead} />
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
{source && (
|
|
206
|
+
<p data-slot="docs-source" className="font-mono text-xs text-muted-foreground">
|
|
207
|
+
{source}
|
|
208
|
+
</p>
|
|
209
|
+
)}
|
|
210
|
+
{children}
|
|
211
|
+
</header>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** A titled section of a docs page. */
|
|
216
|
+
export function DocsSection({ title, lead, children }: { title: string; lead?: string; children: ReactNode }) {
|
|
217
|
+
const id = title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
218
|
+
return (
|
|
219
|
+
<section data-slot="docs-section" aria-labelledby={id} className="flex flex-col gap-4">
|
|
220
|
+
<div className="flex flex-col gap-1">
|
|
221
|
+
<h2 id={id} className="text-xl font-semibold">
|
|
222
|
+
{title}
|
|
223
|
+
</h2>
|
|
224
|
+
{lead && <p className="max-w-prose text-sm text-muted-foreground">{lead}</p>}
|
|
225
|
+
</div>
|
|
226
|
+
{children}
|
|
227
|
+
</section>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function NavLink({ link, current }: { link: DocsLink; current: string }) {
|
|
232
|
+
const active = current === link.href;
|
|
233
|
+
return (
|
|
234
|
+
<Link
|
|
235
|
+
href={link.href}
|
|
236
|
+
aria-current={active ? "page" : undefined}
|
|
237
|
+
data-slot="docs-nav-link"
|
|
238
|
+
className={cx(
|
|
239
|
+
target,
|
|
240
|
+
"flex items-center rounded-md px-2 py-1.5 hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
|
|
241
|
+
active && "bg-muted font-medium",
|
|
242
|
+
)}
|
|
243
|
+
>
|
|
244
|
+
{link.label}
|
|
245
|
+
</Link>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { standardsSections } from "./lib/standards";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The project's performance budgets, accessibility target and support baseline,
|
|
6
|
+
* rendered from the thresholds it already records (`.mxa/stack.json`
|
|
7
|
+
* `baselines`), so the docs and the checks cannot disagree. A value nobody has
|
|
8
|
+
* set is left out rather than shown as `null`; a note is shown as a sentence.
|
|
9
|
+
* Thresholds a client agrees beyond the schema (`textSize`, `touchTargets`) are
|
|
10
|
+
* titled from their keys.
|
|
11
|
+
*/
|
|
12
|
+
export function Standards({ baselines, children }: { baselines: Record<string, unknown>; children?: ReactNode }) {
|
|
13
|
+
return (
|
|
14
|
+
<div data-slot="standards" className="flex flex-col gap-8">
|
|
15
|
+
{standardsSections(baselines).map((section) => (
|
|
16
|
+
<section key={section.key} data-slot="standard" className="flex flex-col gap-2">
|
|
17
|
+
<h3 className="text-sm font-medium">{section.title}</h3>
|
|
18
|
+
{section.rows.length === 1 && !section.rows[0].label ? (
|
|
19
|
+
<p className="text-sm">{section.rows[0].value}</p>
|
|
20
|
+
) : section.rows.length ? (
|
|
21
|
+
<dl className="grid gap-x-6 gap-y-2 text-sm sm:grid-cols-[14rem_1fr]">
|
|
22
|
+
{section.rows.map((row) => (
|
|
23
|
+
<div key={row.label} className="contents">
|
|
24
|
+
<dt className="text-muted-foreground">{row.label}</dt>
|
|
25
|
+
<dd>{row.value}</dd>
|
|
26
|
+
</div>
|
|
27
|
+
))}
|
|
28
|
+
</dl>
|
|
29
|
+
) : null}
|
|
30
|
+
{section.notes.map((note) => (
|
|
31
|
+
<p key={note} className="max-w-prose text-sm text-muted-foreground">
|
|
32
|
+
{note}
|
|
33
|
+
</p>
|
|
34
|
+
))}
|
|
35
|
+
</section>
|
|
36
|
+
))}
|
|
37
|
+
{typeof baselines.$note === "string" && <p className="text-sm text-muted-foreground">{baselines.$note}</p>}
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|