@designtools/docs 0.0.0-stage → 0.1.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 +11 -0
- package/LICENSE +21 -0
- package/README.md +133 -2
- package/dist/a11y-panel.d.ts +58 -0
- package/dist/a11y-panel.js +75 -0
- package/dist/adherence-summary.d.ts +9 -0
- package/dist/adherence-summary.js +27 -0
- package/dist/agent-view.d.ts +19 -0
- package/dist/agent-view.js +23 -0
- package/dist/anatomy.d.ts +12 -0
- package/dist/anatomy.js +38 -0
- package/dist/ask-claude.d.ts +14 -0
- package/dist/ask-claude.js +40 -0
- package/dist/cli.js +322 -0
- package/dist/code-view.d.ts +17 -0
- package/dist/code-view.js +42 -0
- package/dist/examples.d.ts +25 -0
- package/dist/examples.js +48 -0
- package/dist/glossary.d.ts +5 -0
- package/dist/glossary.js +14 -0
- package/dist/imagery.d.ts +6 -0
- package/dist/imagery.js +17 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +33 -0
- package/dist/lib/adherence-types.d.ts +168 -0
- package/dist/lib/adherence-types.js +13 -0
- package/dist/lib/boundary.d.ts +16 -0
- package/dist/lib/boundary.js +15 -0
- package/dist/lib/contrast.d.ts +8 -0
- package/dist/lib/contrast.js +42 -0
- package/dist/lib/cx.d.ts +24 -0
- package/dist/lib/cx.js +26 -0
- package/dist/lib/jsx.d.ts +6 -0
- package/dist/lib/jsx.js +29 -0
- package/dist/lib/lookup.d.ts +35 -0
- package/dist/lib/lookup.js +41 -0
- package/dist/lib/manifest-types.d.ts +253 -0
- package/dist/lib/manifest-types.js +14 -0
- package/dist/lib/manifest.d.ts +98 -0
- package/dist/lib/manifest.js +173 -0
- package/dist/lib/markdown.d.ts +50 -0
- package/dist/lib/markdown.js +286 -0
- package/dist/lib/measure.d.ts +12 -0
- package/dist/lib/measure.js +62 -0
- package/dist/lib/pseudo.d.ts +11 -0
- package/dist/lib/pseudo.js +71 -0
- package/dist/lib/registry.d.ts +21 -0
- package/dist/lib/registry.js +11 -0
- package/dist/lib/search.d.ts +44 -0
- package/dist/lib/search.js +62 -0
- package/dist/lib/standards.d.ts +17 -0
- package/dist/lib/standards.js +81 -0
- package/dist/lib/status.d.ts +7 -0
- package/dist/lib/status.js +17 -0
- package/dist/lib/text.d.ts +8 -0
- package/dist/lib/text.js +9 -0
- package/dist/lib/ui.d.ts +100 -0
- package/dist/lib/ui.js +89 -0
- package/dist/lib/vision.d.ts +18 -0
- package/dist/lib/vision.js +27 -0
- package/dist/logo-usage.d.ts +14 -0
- package/dist/logo-usage.js +45 -0
- package/dist/next/config.d.ts +85 -0
- package/dist/next/config.js +1 -0
- package/dist/next/index.d.ts +62 -0
- package/dist/next/index.js +63 -0
- package/dist/next/markdown.d.ts +20 -0
- package/dist/next/markdown.js +95 -0
- package/dist/next/pages.d.ts +6 -0
- package/dist/next/pages.js +122 -0
- package/dist/next/prose.d.ts +9 -0
- package/dist/next/prose.js +37 -0
- package/dist/next/proxy.d.ts +19 -0
- package/dist/next/proxy.js +56 -0
- package/dist/next/site.d.ts +53 -0
- package/dist/next/site.js +136 -0
- package/dist/page-actions.d.ts +9 -0
- package/dist/page-actions.js +32 -0
- package/dist/pattern.d.ts +8 -0
- package/dist/pattern.js +19 -0
- package/dist/playground.d.ts +62 -0
- package/dist/playground.js +111 -0
- package/dist/preview-frame.d.ts +31 -0
- package/dist/preview-frame.js +63 -0
- package/dist/props-table.d.ts +10 -0
- package/dist/props-table.js +45 -0
- package/dist/rule.d.ts +12 -0
- package/dist/rule.js +32 -0
- package/dist/scale.d.ts +9 -0
- package/dist/scale.js +58 -0
- package/dist/search.d.ts +16 -0
- package/dist/search.js +46 -0
- package/dist/shell.d.ts +89 -0
- package/dist/shell.js +199 -0
- package/dist/standards.d.ts +13 -0
- package/dist/standards.js +19 -0
- package/dist/styles.css +3519 -0
- package/dist/swatches.d.ts +28 -0
- package/dist/swatches.js +94 -0
- package/dist/theme-switch.d.ts +20 -0
- package/dist/theme-switch.js +68 -0
- package/dist/type-ramp.d.ts +10 -0
- package/dist/type-ramp.js +31 -0
- package/dist/usage.d.ts +13 -0
- package/dist/usage.js +55 -0
- package/dist/variant-matrix.d.ts +15 -0
- package/dist/variant-matrix.js +53 -0
- package/dist/voice-terms.d.ts +5 -0
- package/dist/voice-terms.js +13 -0
- package/dist/workbench.d.ts +33 -0
- package/dist/workbench.js +273 -0
- package/package.json +107 -3
- package/src/a11y-panel.tsx +297 -0
- package/src/adherence-summary.tsx +134 -0
- package/src/agent-view.tsx +54 -0
- package/src/anatomy.tsx +114 -0
- package/src/ask-claude.tsx +90 -0
- package/src/code-view.tsx +112 -0
- package/src/css.d.ts +1 -0
- package/src/examples.tsx +141 -0
- package/src/glossary.tsx +71 -0
- package/src/imagery.tsx +64 -0
- package/src/index.ts +34 -0
- package/src/lib/adherence-types.ts +181 -0
- package/src/lib/boundary.tsx +21 -0
- package/src/lib/contrast.ts +44 -0
- package/src/lib/cx.ts +30 -0
- package/src/lib/jsx.ts +29 -0
- package/src/lib/lookup.tsx +49 -0
- package/src/lib/manifest-types.ts +284 -0
- package/src/lib/manifest.ts +228 -0
- package/src/lib/markdown.ts +288 -0
- package/src/lib/measure.tsx +101 -0
- package/src/lib/pseudo.ts +74 -0
- package/src/lib/registry.tsx +29 -0
- package/src/lib/search.ts +99 -0
- package/src/lib/standards.ts +82 -0
- package/src/lib/status.tsx +32 -0
- package/src/lib/text.tsx +27 -0
- package/src/lib/ui.tsx +291 -0
- package/src/lib/vision.tsx +44 -0
- package/src/logo-usage.tsx +140 -0
- package/src/next/config.ts +80 -0
- package/src/next/index.tsx +100 -0
- package/src/next/markdown.ts +119 -0
- package/src/next/pages.tsx +370 -0
- package/src/next/prose.tsx +87 -0
- package/src/next/proxy.ts +55 -0
- package/src/next/site.ts +173 -0
- package/src/page-actions.tsx +92 -0
- package/src/pattern.tsx +67 -0
- package/src/playground.tsx +352 -0
- package/src/preview-frame.tsx +131 -0
- package/src/props-table.tsx +154 -0
- package/src/rule.tsx +116 -0
- package/src/scale.tsx +175 -0
- package/src/search.tsx +155 -0
- package/src/shell.tsx +555 -0
- package/src/standards.tsx +58 -0
- package/src/styles.css +155 -0
- package/src/swatches.tsx +287 -0
- package/src/theme-switch.tsx +90 -0
- package/src/type-ramp.tsx +150 -0
- package/src/usage.tsx +148 -0
- package/src/variant-matrix.tsx +195 -0
- package/src/voice-terms.tsx +46 -0
- package/src/workbench.tsx +584 -0
package/src/shell.tsx
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Collapsible } from "@base-ui/react/collapsible";
|
|
4
|
+
import { Dialog } from "@base-ui/react/dialog";
|
|
5
|
+
import { ArrowLeft, ArrowRight, BookOpen, ChevronRight, Component, Gauge, House, LayoutTemplate, type LucideIcon, Menu, Palette, Scale, X } from "lucide-react";
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
import { usePathname } from "next/navigation";
|
|
8
|
+
import { createContext, type ReactNode, useContext, useEffect, useMemo, useState } from "react";
|
|
9
|
+
import { accent, cx, display, target } from "./lib/cx";
|
|
10
|
+
import { groupFamilies, slugOf } from "./lib/manifest";
|
|
11
|
+
import type { ComponentsManifest, PatternsManifest, RulesManifest, TaxonomyManifest } from "./lib/manifest-types";
|
|
12
|
+
import { searchIndex } from "./lib/search";
|
|
13
|
+
import { Prose } from "./lib/text";
|
|
14
|
+
import { DocsSearch } from "./search";
|
|
15
|
+
import { ThemeSwitch, useThemeScript } from "./theme-switch";
|
|
16
|
+
|
|
17
|
+
export interface DocsLink {
|
|
18
|
+
label: string;
|
|
19
|
+
href: string;
|
|
20
|
+
/** Shown beneath it while you are on it or one of them: a component's parts. */
|
|
21
|
+
children?: DocsLink[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A top-level section of the navigation: one page, or groups of pages that fold away. */
|
|
25
|
+
interface Section {
|
|
26
|
+
id: string;
|
|
27
|
+
label: string;
|
|
28
|
+
icon: LucideIcon;
|
|
29
|
+
/** A section of one page is a link, not a fold. */
|
|
30
|
+
href?: string;
|
|
31
|
+
groups: { heading?: string; links: DocsLink[] }[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** What the page knows from the navigation: the section it sits in, and its neighbours. */
|
|
35
|
+
const DocsPage = createContext<{ section?: string; prev?: DocsLink; next?: DocsLink }>({});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The docs frame, in the site's order: overview, brand, foundations, rules,
|
|
39
|
+
* patterns, components by `@category`, standards. Editorial pages are the links
|
|
40
|
+
* you pass; rules, patterns and components come from the manifest.
|
|
41
|
+
*
|
|
42
|
+
* On a laptop the navigation is an inset panel down the left, each section
|
|
43
|
+
* folding open where the current page is, with search at the top and the colour
|
|
44
|
+
* mode at the foot; wide screens add an "On this page" rail. On a phone it is a
|
|
45
|
+
* bar with search and a drawer. Every page ends with the previous and next page,
|
|
46
|
+
* and `DocsHeader` names the section it sits in. Put it in the docs layout.
|
|
47
|
+
*/
|
|
48
|
+
export function DocsShell({
|
|
49
|
+
manifest,
|
|
50
|
+
rules,
|
|
51
|
+
patterns,
|
|
52
|
+
terms,
|
|
53
|
+
search = true,
|
|
54
|
+
themeSwitch = true,
|
|
55
|
+
title = "Design system",
|
|
56
|
+
logo,
|
|
57
|
+
basePath = "/design/system",
|
|
58
|
+
brand = [],
|
|
59
|
+
foundations = [],
|
|
60
|
+
standards = [],
|
|
61
|
+
hero,
|
|
62
|
+
children,
|
|
63
|
+
}: {
|
|
64
|
+
manifest: ComponentsManifest;
|
|
65
|
+
/** Listed under Rules, each linking to its anchor on `${basePath}/rules`. */
|
|
66
|
+
rules?: RulesManifest;
|
|
67
|
+
/** Listed under Patterns, each at `${basePath}/patterns/${id}`. */
|
|
68
|
+
patterns?: PatternsManifest;
|
|
69
|
+
/** The taxonomy, so search finds a component by a wrong name ("Dropdown" finds Select). */
|
|
70
|
+
terms?: TaxonomyManifest;
|
|
71
|
+
/** Search over components, parts, rules, patterns and pages, on ⌘K, Ctrl+K or /. On by default. */
|
|
72
|
+
search?: boolean;
|
|
73
|
+
/** System, light and dark, at the foot of the navigation. Off for a project with its own switch. */
|
|
74
|
+
themeSwitch?: boolean;
|
|
75
|
+
title?: string;
|
|
76
|
+
/** The client's logo, shown beside the title: the site is theirs. */
|
|
77
|
+
logo?: ReactNode;
|
|
78
|
+
/** Component pages live at `${basePath}/components/${slug}`. */
|
|
79
|
+
basePath?: string;
|
|
80
|
+
/** Brand pages: principles, design language, voice, logo, imagery. */
|
|
81
|
+
brand?: DocsLink[];
|
|
82
|
+
/** Foundations pages: colour, type, scale. */
|
|
83
|
+
foundations?: DocsLink[];
|
|
84
|
+
/** Standards pages: budgets, accessibility, adherence. */
|
|
85
|
+
standards?: DocsLink[];
|
|
86
|
+
/**
|
|
87
|
+
* A full-bleed introduction across the top of the overview, edge to edge of the
|
|
88
|
+
* content area: the brand's splash. The overview's own header can then be left out.
|
|
89
|
+
*/
|
|
90
|
+
hero?: ReactNode;
|
|
91
|
+
children: ReactNode;
|
|
92
|
+
}) {
|
|
93
|
+
const current = usePathname();
|
|
94
|
+
const searchItems = useMemo(
|
|
95
|
+
() =>
|
|
96
|
+
searchIndex({
|
|
97
|
+
manifest,
|
|
98
|
+
rules: rules?.rules,
|
|
99
|
+
patterns: patterns?.patterns,
|
|
100
|
+
terms: terms?.terms,
|
|
101
|
+
pages: [...brand, ...foundations, ...(rules?.rules.length ? [{ label: "Rules", href: `${basePath}/rules` }] : []), ...standards],
|
|
102
|
+
basePath,
|
|
103
|
+
}),
|
|
104
|
+
[manifest, rules, patterns, terms, brand, foundations, standards, basePath],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const sections = useMemo<Section[]>(() => {
|
|
108
|
+
const families = groupFamilies(manifest);
|
|
109
|
+
const all: Section[] = [
|
|
110
|
+
{ id: "overview", label: "Overview", icon: House, href: basePath, groups: [] },
|
|
111
|
+
{ id: "brand", label: "Brand", icon: BookOpen, groups: [{ links: brand }] },
|
|
112
|
+
{ id: "foundations", label: "Foundations", icon: Palette, groups: [{ links: foundations }] },
|
|
113
|
+
...(rules?.rules.length ? [{ id: "rules", label: "Rules", icon: Scale, href: `${basePath}/rules`, groups: [] }] : []),
|
|
114
|
+
{
|
|
115
|
+
id: "patterns",
|
|
116
|
+
label: "Patterns",
|
|
117
|
+
icon: LayoutTemplate,
|
|
118
|
+
groups: [{ links: (patterns?.patterns ?? []).map((p) => ({ label: p.name, href: `${basePath}/patterns/${p.id}` })) }],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "components",
|
|
122
|
+
label: "Components",
|
|
123
|
+
icon: Component,
|
|
124
|
+
groups: families.map((g) => ({
|
|
125
|
+
heading: families.length > 1 ? g.category : undefined,
|
|
126
|
+
links: g.families.map(({ head, parts }) => ({
|
|
127
|
+
label: head.name,
|
|
128
|
+
href: `${basePath}/components/${slugOf(head)}`,
|
|
129
|
+
children: parts.map((p) => ({ label: p.name, href: `${basePath}/components/${slugOf(p)}` })),
|
|
130
|
+
})),
|
|
131
|
+
})),
|
|
132
|
+
},
|
|
133
|
+
{ id: "standards", label: "Standards", icon: Gauge, groups: [{ links: standards }] },
|
|
134
|
+
];
|
|
135
|
+
return all.filter((s) => s.href || s.groups.some((g) => g.links.length));
|
|
136
|
+
}, [manifest, rules, patterns, brand, foundations, standards, basePath]);
|
|
137
|
+
|
|
138
|
+
// every page in navigation order, for the section a page sits in and its neighbours
|
|
139
|
+
const page = useMemo(() => {
|
|
140
|
+
// a section of one page has no section above it: the page is the section
|
|
141
|
+
const order: { link: DocsLink; section?: string }[] = [];
|
|
142
|
+
for (const s of sections) {
|
|
143
|
+
if (s.href) order.push({ link: { label: s.label, href: s.href } });
|
|
144
|
+
for (const g of s.groups) for (const l of g.links) order.push({ link: l, section: s.label });
|
|
145
|
+
}
|
|
146
|
+
const at = order.findIndex(({ link }) => link.href === current || link.children?.some((c) => c.href === current));
|
|
147
|
+
if (at < 0) return {};
|
|
148
|
+
return { section: order[at].section, prev: order[at - 1]?.link, next: order[at + 1]?.link };
|
|
149
|
+
}, [sections, current]);
|
|
150
|
+
|
|
151
|
+
// a hook, so it runs whether or not the switch is shown; the script only reads a saved choice
|
|
152
|
+
useThemeScript();
|
|
153
|
+
const [menu, setMenu] = useState(false);
|
|
154
|
+
useEffect(() => setMenu(false), [current]);
|
|
155
|
+
|
|
156
|
+
const brandLink = (
|
|
157
|
+
<Link href={basePath} data-slot="docs-title" className={cx(target, "flex min-w-0 items-center gap-3 rounded-md focus-visible:outline-2 focus-visible:outline-ring")}>
|
|
158
|
+
{logo && (
|
|
159
|
+
<span data-slot="docs-logo" className="shrink-0">
|
|
160
|
+
{logo}
|
|
161
|
+
</span>
|
|
162
|
+
)}
|
|
163
|
+
<span className={cx(logo ? "sr-only" : "truncate text-base font-semibold tracking-tight")}>{title}</span>
|
|
164
|
+
</Link>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<DocsPage.Provider value={page}>
|
|
169
|
+
<div data-designtools="" data-slot="docs-shell" className="min-h-dvh bg-canvas text-canvas-foreground lg:grid lg:grid-cols-[19rem_minmax(0,1fr)]">
|
|
170
|
+
|
|
171
|
+
<aside data-slot="docs-sidebar" className="sticky top-0 hidden h-dvh p-3 lg:block">
|
|
172
|
+
<div className="flex h-full flex-col overflow-hidden rounded-xl border border-border bg-surface text-surface-foreground shadow-xs">
|
|
173
|
+
<div className="flex flex-col gap-3 p-4 pb-3">
|
|
174
|
+
{brandLink}
|
|
175
|
+
{search && <DocsSearch items={searchItems} />}
|
|
176
|
+
</div>
|
|
177
|
+
<Nav sections={sections} current={current} title={title} />
|
|
178
|
+
{themeSwitch && (
|
|
179
|
+
<div data-slot="docs-sidebar-foot" className="flex items-center justify-between gap-3 border-t border-border px-4 py-3">
|
|
180
|
+
<span className="text-xs font-medium text-muted-foreground">Appearance</span>
|
|
181
|
+
<ThemeSwitch />
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
</aside>
|
|
186
|
+
|
|
187
|
+
<div className="min-w-0">
|
|
188
|
+
<header
|
|
189
|
+
data-slot="docs-topbar"
|
|
190
|
+
className="sticky top-0 z-20 flex h-16 items-center gap-2 border-b border-border bg-canvas/85 px-3 backdrop-blur-md lg:hidden"
|
|
191
|
+
>
|
|
192
|
+
<Dialog.Root open={menu} onOpenChange={setMenu}>
|
|
193
|
+
<Dialog.Trigger aria-label="Open the navigation" className={iconButton}>
|
|
194
|
+
<Menu aria-hidden className="size-(--dt-size-sm)" />
|
|
195
|
+
</Dialog.Trigger>
|
|
196
|
+
<Dialog.Portal>
|
|
197
|
+
<Dialog.Backdrop data-designtools="" className="fixed inset-0 z-40 bg-canvas-foreground/30 transition-opacity duration-(--dt-duration-base) data-ending-style:opacity-0 data-starting-style:opacity-0" />
|
|
198
|
+
<Dialog.Popup
|
|
199
|
+
data-designtools=""
|
|
200
|
+
data-slot="docs-drawer"
|
|
201
|
+
aria-label={title}
|
|
202
|
+
className="fixed inset-y-0 left-0 z-50 flex w-[min(21rem,88vw)] flex-col bg-surface text-surface-foreground shadow-xl transition-transform duration-(--dt-duration-slow) ease-(--dt-ease-out) data-ending-style:-translate-x-full data-starting-style:-translate-x-full"
|
|
203
|
+
>
|
|
204
|
+
<div className="flex items-center justify-between gap-2 border-b border-border p-3 pl-4">
|
|
205
|
+
{brandLink}
|
|
206
|
+
<Dialog.Close aria-label="Close the navigation" className={iconButton}>
|
|
207
|
+
<X aria-hidden className="size-(--dt-size-sm)" />
|
|
208
|
+
</Dialog.Close>
|
|
209
|
+
</div>
|
|
210
|
+
<Nav sections={sections} current={current} title={title} />
|
|
211
|
+
{themeSwitch && (
|
|
212
|
+
<div className="flex items-center justify-between gap-3 border-t border-border px-4 py-3">
|
|
213
|
+
<span className="text-xs font-medium text-muted-foreground">Appearance</span>
|
|
214
|
+
<ThemeSwitch />
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
</Dialog.Popup>
|
|
218
|
+
</Dialog.Portal>
|
|
219
|
+
</Dialog.Root>
|
|
220
|
+
<div className="min-w-0 flex-1">{brandLink}</div>
|
|
221
|
+
{/* the sidebar's search owns the keyboard shortcut, so one key press opens one dialog */}
|
|
222
|
+
{search && <DocsSearch items={searchItems} shortcuts={false} variant="icon" />}
|
|
223
|
+
</header>
|
|
224
|
+
|
|
225
|
+
{hero && current === basePath && (
|
|
226
|
+
<div data-slot="docs-hero" className="w-full">
|
|
227
|
+
{hero}
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
<div className="mx-auto flex w-full max-w-[90rem] gap-16 px-5 pt-12 pb-20 sm:px-8 lg:px-12 lg:pt-20 xl:px-20">
|
|
231
|
+
<main data-slot="docs-main" className="flex min-w-0 flex-1 flex-col gap-16 [counter-reset:docs-section] lg:gap-24">
|
|
232
|
+
{children}
|
|
233
|
+
<Pager prev={page.prev} next={page.next} />
|
|
234
|
+
</main>
|
|
235
|
+
<OnThisPage current={current} />
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</DocsPage.Provider>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const iconButton =
|
|
244
|
+
"inline-flex size-(--docs-target,2.75rem) shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring";
|
|
245
|
+
|
|
246
|
+
/** The sections, each a link or a fold that opens where the current page is. */
|
|
247
|
+
function Nav({ sections, current, title }: { sections: Section[]; current: string; title: string }) {
|
|
248
|
+
const holds = (s: Section) => s.groups.some((g) => g.links.some((l) => l.href === current || l.children?.some((c) => c.href === current)));
|
|
249
|
+
const [open, setOpen] = useState<Record<string, boolean>>(() => Object.fromEntries(sections.map((s) => [s.id, holds(s)])));
|
|
250
|
+
// moving to a page opens its section; nothing closes on its own
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
const here = sections.find(holds);
|
|
253
|
+
if (here) setOpen((o) => (o[here.id] ? o : { ...o, [here.id]: true }));
|
|
254
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
255
|
+
}, [current, sections]);
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<nav aria-label={title} data-slot="docs-nav" className="min-h-0 flex-1 overflow-y-auto px-3 pb-4 text-sm">
|
|
259
|
+
<ul className="flex flex-col gap-0.5">
|
|
260
|
+
{sections.map((s) => {
|
|
261
|
+
const Icon = s.icon;
|
|
262
|
+
const row = cx(
|
|
263
|
+
target,
|
|
264
|
+
"group flex w-full items-center gap-3 rounded-md px-2.5 font-medium text-canvas-foreground hover:bg-muted focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring",
|
|
265
|
+
);
|
|
266
|
+
const icon = <Icon aria-hidden className="size-(--dt-size-xs) shrink-0 text-muted-foreground group-hover:text-canvas-foreground" />;
|
|
267
|
+
if (s.href) {
|
|
268
|
+
const active = current === s.href;
|
|
269
|
+
return (
|
|
270
|
+
<li key={s.id}>
|
|
271
|
+
<Link href={s.href} aria-current={active ? "page" : undefined} data-slot="docs-nav-section" className={cx(row, active && "bg-muted")}>
|
|
272
|
+
{icon}
|
|
273
|
+
{s.label}
|
|
274
|
+
</Link>
|
|
275
|
+
</li>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return (
|
|
279
|
+
<li key={s.id}>
|
|
280
|
+
<Collapsible.Root open={open[s.id] ?? false} onOpenChange={(next) => setOpen((o) => ({ ...o, [s.id]: next }))}>
|
|
281
|
+
<Collapsible.Trigger data-slot="docs-nav-section" className={row}>
|
|
282
|
+
{icon}
|
|
283
|
+
<span className="flex-1 text-left">{s.label}</span>
|
|
284
|
+
<ChevronRight
|
|
285
|
+
aria-hidden
|
|
286
|
+
className="size-(--dt-size-xs) text-muted-foreground transition-transform duration-(--dt-duration-fast) group-data-panel-open:rotate-90"
|
|
287
|
+
/>
|
|
288
|
+
</Collapsible.Trigger>
|
|
289
|
+
<Collapsible.Panel
|
|
290
|
+
data-slot="docs-nav-panel"
|
|
291
|
+
className="h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-(--dt-duration-base) ease-(--dt-ease-out) data-ending-style:h-0 data-starting-style:h-0"
|
|
292
|
+
>
|
|
293
|
+
<div className="ml-[1.1875rem] flex flex-col gap-3 border-l border-border py-1 pl-3">
|
|
294
|
+
{s.groups.map((g, i) => (
|
|
295
|
+
<div key={g.heading ?? i} className="flex flex-col">
|
|
296
|
+
{g.heading && (
|
|
297
|
+
<p data-slot="docs-nav-heading" className="px-2.5 pt-1 pb-1 text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
|
298
|
+
{g.heading}
|
|
299
|
+
</p>
|
|
300
|
+
)}
|
|
301
|
+
<ul className="flex flex-col">
|
|
302
|
+
{g.links.map((link) => {
|
|
303
|
+
const here = current === link.href || link.children?.some((c) => c.href === current);
|
|
304
|
+
return (
|
|
305
|
+
<li key={link.href}>
|
|
306
|
+
<NavLink link={link} current={current} />
|
|
307
|
+
{here && link.children?.length ? (
|
|
308
|
+
<ul data-slot="docs-nav-parts" aria-label={`${link.label} parts`} className="ml-3 flex flex-col border-l border-border pl-2">
|
|
309
|
+
{link.children.map((child) => (
|
|
310
|
+
<li key={child.href}>
|
|
311
|
+
<NavLink link={child} current={current} />
|
|
312
|
+
</li>
|
|
313
|
+
))}
|
|
314
|
+
</ul>
|
|
315
|
+
) : null}
|
|
316
|
+
</li>
|
|
317
|
+
);
|
|
318
|
+
})}
|
|
319
|
+
</ul>
|
|
320
|
+
</div>
|
|
321
|
+
))}
|
|
322
|
+
</div>
|
|
323
|
+
</Collapsible.Panel>
|
|
324
|
+
</Collapsible.Root>
|
|
325
|
+
</li>
|
|
326
|
+
);
|
|
327
|
+
})}
|
|
328
|
+
</ul>
|
|
329
|
+
</nav>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function NavLink({ link, current }: { link: DocsLink; current: string }) {
|
|
334
|
+
const active = current === link.href;
|
|
335
|
+
return (
|
|
336
|
+
<Link
|
|
337
|
+
href={link.href}
|
|
338
|
+
aria-current={active ? "page" : undefined}
|
|
339
|
+
data-slot="docs-nav-link"
|
|
340
|
+
className={cx(
|
|
341
|
+
target,
|
|
342
|
+
"relative flex items-center rounded-md px-2.5 text-muted-foreground hover:bg-muted hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring",
|
|
343
|
+
active && "bg-muted font-medium text-canvas-foreground",
|
|
344
|
+
)}
|
|
345
|
+
>
|
|
346
|
+
{active && <span aria-hidden data-slot="docs-nav-marker" className={cx(accent, "absolute inset-y-2.5 -left-[0.8125rem] w-0.5 rounded-full")} />}
|
|
347
|
+
{link.label}
|
|
348
|
+
</Link>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** The page's sections, beside it on a wide screen, marking the one being read. */
|
|
353
|
+
function OnThisPage({ current }: { current: string }) {
|
|
354
|
+
const [items, setItems] = useState<{ id: string; label: string }[]>([]);
|
|
355
|
+
const [active, setActive] = useState<string>();
|
|
356
|
+
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
const headings = [...document.querySelectorAll<HTMLElement>('[data-slot="docs-main"] [data-slot="docs-section-title"][id]')];
|
|
359
|
+
setItems(headings.map((h) => ({ id: h.id, label: h.textContent ?? "" })));
|
|
360
|
+
setActive(headings[0]?.id);
|
|
361
|
+
if (!headings.length) return;
|
|
362
|
+
// the section being read is the last whose heading has passed the top quarter of the window
|
|
363
|
+
let frame = 0;
|
|
364
|
+
const update = () => {
|
|
365
|
+
frame = 0;
|
|
366
|
+
const line = window.innerHeight * 0.25;
|
|
367
|
+
let at = headings[0];
|
|
368
|
+
for (const h of headings) if (h.getBoundingClientRect().top <= line) at = h;
|
|
369
|
+
// at the foot of the page the last section is the one in view, however short
|
|
370
|
+
if (window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 4) at = headings[headings.length - 1];
|
|
371
|
+
setActive(at.id);
|
|
372
|
+
};
|
|
373
|
+
const onScroll = () => {
|
|
374
|
+
if (!frame) frame = requestAnimationFrame(update);
|
|
375
|
+
};
|
|
376
|
+
update();
|
|
377
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
378
|
+
window.addEventListener("resize", onScroll);
|
|
379
|
+
return () => {
|
|
380
|
+
cancelAnimationFrame(frame);
|
|
381
|
+
window.removeEventListener("scroll", onScroll);
|
|
382
|
+
window.removeEventListener("resize", onScroll);
|
|
383
|
+
};
|
|
384
|
+
}, [current]);
|
|
385
|
+
|
|
386
|
+
if (items.length < 2) return null;
|
|
387
|
+
return (
|
|
388
|
+
<nav aria-label="On this page" data-slot="docs-toc" className="sticky top-16 hidden h-fit w-48 shrink-0 2xl:block">
|
|
389
|
+
<p className="mb-3 text-xs font-medium tracking-wide text-muted-foreground uppercase">On this page</p>
|
|
390
|
+
<ul className="flex flex-col border-l border-border text-sm">
|
|
391
|
+
{items.map((item) => (
|
|
392
|
+
<li key={item.id}>
|
|
393
|
+
<a
|
|
394
|
+
href={`#${item.id}`}
|
|
395
|
+
aria-current={active === item.id ? "location" : undefined}
|
|
396
|
+
className={cx(
|
|
397
|
+
"relative -ml-px block border-l border-transparent py-1.5 pl-4 text-muted-foreground hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring",
|
|
398
|
+
active === item.id && "font-medium text-canvas-foreground",
|
|
399
|
+
)}
|
|
400
|
+
>
|
|
401
|
+
{active === item.id && <span aria-hidden className={cx(accent, "absolute inset-y-1.5 -left-px w-0.5 rounded-full")} />}
|
|
402
|
+
{item.label}
|
|
403
|
+
</a>
|
|
404
|
+
</li>
|
|
405
|
+
))}
|
|
406
|
+
</ul>
|
|
407
|
+
</nav>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** The previous and next page in the navigation's order. */
|
|
412
|
+
function Pager({ prev, next }: { prev?: DocsLink; next?: DocsLink }) {
|
|
413
|
+
if (!prev && !next) return null;
|
|
414
|
+
const card = cx(
|
|
415
|
+
target,
|
|
416
|
+
"group flex flex-col gap-1 rounded-lg border border-border p-5 transition-colors duration-(--dt-duration-fast) hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring",
|
|
417
|
+
);
|
|
418
|
+
return (
|
|
419
|
+
<nav aria-label="Previous and next" data-slot="docs-pager" className="grid gap-3 border-t border-border pt-10 sm:grid-cols-2">
|
|
420
|
+
{prev ? (
|
|
421
|
+
<Link href={prev.href} className={card}>
|
|
422
|
+
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
423
|
+
<ArrowLeft aria-hidden className="size-(--dt-size-xs) transition-transform group-hover:-translate-x-0.5" /> Previous
|
|
424
|
+
</span>
|
|
425
|
+
<span className="text-base font-medium">{prev.label}</span>
|
|
426
|
+
</Link>
|
|
427
|
+
) : (
|
|
428
|
+
<span />
|
|
429
|
+
)}
|
|
430
|
+
{next && (
|
|
431
|
+
<Link href={next.href} className={cx(card, "sm:items-end sm:text-right")}>
|
|
432
|
+
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
433
|
+
Next <ArrowRight aria-hidden className="size-(--dt-size-xs) transition-transform group-hover:translate-x-0.5" />
|
|
434
|
+
</span>
|
|
435
|
+
<span className="text-base font-medium">{next.label}</span>
|
|
436
|
+
</Link>
|
|
437
|
+
)}
|
|
438
|
+
</nav>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* A page's heading: the section it sits in (from the navigation), the title, the
|
|
444
|
+
* lead, and the page's actions as children. Where the page comes from goes at the
|
|
445
|
+
* foot, in `DocsFooter`.
|
|
446
|
+
*/
|
|
447
|
+
export function DocsHeader({
|
|
448
|
+
title,
|
|
449
|
+
lead,
|
|
450
|
+
eyebrow,
|
|
451
|
+
children,
|
|
452
|
+
}: {
|
|
453
|
+
title: string;
|
|
454
|
+
lead?: string;
|
|
455
|
+
/** Above the title. Defaults to the section the page sits in. */
|
|
456
|
+
eyebrow?: string;
|
|
457
|
+
/** The page's actions, beside the title: `PageActions`. */
|
|
458
|
+
children?: ReactNode;
|
|
459
|
+
}) {
|
|
460
|
+
const { section } = useContext(DocsPage);
|
|
461
|
+
const over = eyebrow ?? (section && section !== title ? section : undefined);
|
|
462
|
+
return (
|
|
463
|
+
<header data-slot="docs-header" className="flex flex-col gap-6 border-b border-border pb-12 lg:pb-16">
|
|
464
|
+
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
465
|
+
{over ? (
|
|
466
|
+
<p data-slot="docs-eyebrow" className="text-xs font-medium tracking-[var(--tracking-widest,0.16em)] text-muted-foreground uppercase">
|
|
467
|
+
{over}
|
|
468
|
+
</p>
|
|
469
|
+
) : (
|
|
470
|
+
<span />
|
|
471
|
+
)}
|
|
472
|
+
{children && (
|
|
473
|
+
<div data-slot="docs-header-actions" className="flex flex-wrap items-center gap-2">
|
|
474
|
+
{children}
|
|
475
|
+
</div>
|
|
476
|
+
)}
|
|
477
|
+
</div>
|
|
478
|
+
<h1 className={cx(display, "text-5xl leading-[1.05] tracking-tight md:text-7xl")}>{title}</h1>
|
|
479
|
+
{lead && (
|
|
480
|
+
// a lead is read, not studied: light, large, and code in it set in the text's own colour, without a chip
|
|
481
|
+
<div
|
|
482
|
+
data-slot="docs-lead"
|
|
483
|
+
className="flex max-w-3xl flex-col gap-3 text-xl leading-snug font-light text-muted-foreground md:text-2xl md:leading-snug [&_code]:bg-transparent [&_code]:p-0"
|
|
484
|
+
>
|
|
485
|
+
<Prose text={lead} />
|
|
486
|
+
</div>
|
|
487
|
+
)}
|
|
488
|
+
</header>
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Where a page comes from, at its foot: a generated page names the version and
|
|
494
|
+
* commit it was built from; an editorial page names its author, who reviewed it,
|
|
495
|
+
* when, and whether it is settled. Never both, and never a review date on a
|
|
496
|
+
* generated page, which would be invented. `source` names the file it documents.
|
|
497
|
+
*/
|
|
498
|
+
export function DocsFooter({
|
|
499
|
+
provenance,
|
|
500
|
+
editorial,
|
|
501
|
+
source,
|
|
502
|
+
}: {
|
|
503
|
+
/** For a generated page: `Generated from acme-web@1.4.0 at 3f2a9c1`, from `designtools-manifest provenance`. */
|
|
504
|
+
provenance?: string;
|
|
505
|
+
/** For an editorial page. `reviewer` when the person who signs it off is not the author. */
|
|
506
|
+
editorial?: { author: string; reviewer?: string; reviewed: string; status: "draft" | "published" };
|
|
507
|
+
source?: string;
|
|
508
|
+
}) {
|
|
509
|
+
if (!provenance && !editorial && !source) return null;
|
|
510
|
+
return (
|
|
511
|
+
<footer data-slot="docs-footer" className="flex flex-wrap items-center gap-x-6 gap-y-2 text-xs text-muted-foreground">
|
|
512
|
+
{(provenance || editorial) && (
|
|
513
|
+
<p data-slot="docs-provenance" className="flex items-center gap-2">
|
|
514
|
+
<span
|
|
515
|
+
aria-hidden
|
|
516
|
+
className={cx("size-1.5 shrink-0 rounded-full", editorial ? (editorial.status === "published" ? "bg-success" : "bg-warning") : "bg-primary")}
|
|
517
|
+
/>
|
|
518
|
+
{editorial
|
|
519
|
+
? `Written by ${editorial.author} · ${editorial.reviewer ? `reviewed by ${editorial.reviewer}, ` : "last reviewed "}${editorial.reviewed} · ${editorial.status}`
|
|
520
|
+
: `${provenance} · generated from the code`}
|
|
521
|
+
</p>
|
|
522
|
+
)}
|
|
523
|
+
{source && (
|
|
524
|
+
<p data-slot="docs-source" className="font-mono">
|
|
525
|
+
{source}
|
|
526
|
+
</p>
|
|
527
|
+
)}
|
|
528
|
+
</footer>
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* A titled section of a docs page, numbered in a gutter beside it (01, 02, …,
|
|
534
|
+
* counted by CSS in page order) and listed in "On this page".
|
|
535
|
+
*/
|
|
536
|
+
export function DocsSection({ title, lead, children }: { title: string; lead?: string; children: ReactNode }) {
|
|
537
|
+
const id = title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
538
|
+
return (
|
|
539
|
+
<section data-slot="docs-section" aria-labelledby={id} className="grid gap-x-10 gap-y-8 [counter-increment:docs-section] xl:grid-cols-[5rem_minmax(0,1fr)]">
|
|
540
|
+
<span
|
|
541
|
+
aria-hidden
|
|
542
|
+
className="pt-2 font-mono text-xs tracking-[var(--tracking-widest,0.16em)] text-muted-foreground before:content-[counter(docs-section,decimal-leading-zero)]"
|
|
543
|
+
/>
|
|
544
|
+
<div className="flex min-w-0 flex-col gap-10">
|
|
545
|
+
<div className="flex flex-col gap-4">
|
|
546
|
+
<h2 id={id} data-slot="docs-section-title" className={cx(display, "scroll-mt-24 text-4xl leading-[1.1] tracking-tight md:text-5xl")}>
|
|
547
|
+
{title}
|
|
548
|
+
</h2>
|
|
549
|
+
{lead && <p className="max-w-2xl text-lg leading-relaxed text-muted-foreground">{lead}</p>}
|
|
550
|
+
</div>
|
|
551
|
+
{children}
|
|
552
|
+
</div>
|
|
553
|
+
</section>
|
|
554
|
+
);
|
|
555
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Gauge, Info, Monitor, PersonStanding, Ruler } from "lucide-react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { standardsSections } from "./lib/standards";
|
|
4
|
+
|
|
5
|
+
const ICON: Record<string, typeof Gauge> = { a11y: PersonStanding, performance: Gauge, support: Monitor };
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The project's performance budgets, accessibility target and support baseline,
|
|
9
|
+
* rendered from the thresholds it already records (`.mxa/stack.json`
|
|
10
|
+
* `baselines`), so the docs and the checks cannot disagree. A value nobody has
|
|
11
|
+
* set is left out rather than shown as `null`; a note is shown as a sentence.
|
|
12
|
+
* Thresholds a client agrees beyond the schema (`textSize`, `touchTargets`) are
|
|
13
|
+
* titled from their keys.
|
|
14
|
+
*/
|
|
15
|
+
export function Standards({ baselines, children }: { baselines: Record<string, unknown>; children?: ReactNode }) {
|
|
16
|
+
return (
|
|
17
|
+
<div data-slot="standards" className="flex flex-col gap-6">
|
|
18
|
+
<div className="grid gap-x-12 gap-y-10 md:grid-cols-2 xl:grid-cols-3">
|
|
19
|
+
{standardsSections(baselines).map((section) => {
|
|
20
|
+
const Icon = ICON[section.key] ?? Ruler;
|
|
21
|
+
const single = section.rows.length === 1 && !section.rows[0].label;
|
|
22
|
+
return (
|
|
23
|
+
<section key={section.key} data-slot="standard" className="flex flex-col gap-4 border-t border-border pt-5">
|
|
24
|
+
<h3 className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
|
25
|
+
<Icon aria-hidden className="size-(--dt-size-xs)" />
|
|
26
|
+
{section.title}
|
|
27
|
+
</h3>
|
|
28
|
+
{single ? (
|
|
29
|
+
<p className="text-5xl font-light tracking-tight">{section.rows[0].value}</p>
|
|
30
|
+
) : section.rows.length ? (
|
|
31
|
+
<dl className="flex flex-col gap-3">
|
|
32
|
+
{section.rows.map((row) => (
|
|
33
|
+
<div key={row.label} className="flex flex-col gap-0.5">
|
|
34
|
+
<dt className="text-xs text-muted-foreground">{row.label}</dt>
|
|
35
|
+
<dd className="text-2xl font-light tracking-tight tabular-nums">{row.value}</dd>
|
|
36
|
+
</div>
|
|
37
|
+
))}
|
|
38
|
+
</dl>
|
|
39
|
+
) : null}
|
|
40
|
+
{section.notes.map((note) => (
|
|
41
|
+
<p key={note} className="text-sm text-muted-foreground">
|
|
42
|
+
{note}
|
|
43
|
+
</p>
|
|
44
|
+
))}
|
|
45
|
+
</section>
|
|
46
|
+
);
|
|
47
|
+
})}
|
|
48
|
+
</div>
|
|
49
|
+
{typeof baselines.$note === "string" && (
|
|
50
|
+
<p className="flex items-start gap-2 text-sm text-muted-foreground">
|
|
51
|
+
<Info aria-hidden className="mt-0.5 size-(--dt-size-xs) shrink-0" />
|
|
52
|
+
{baselines.$note}
|
|
53
|
+
</p>
|
|
54
|
+
)}
|
|
55
|
+
{children}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|