@designtools/blocks 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +13 -9
- package/dist/cli.js +6 -2
- package/package.json +4 -2
- package/reference/README.md +20 -0
- package/reference/app/(design)/design/system/(docs)/brand/imagery/page.tsx +15 -0
- package/reference/app/(design)/design/system/(docs)/brand/logo/page.tsx +15 -0
- package/reference/app/(design)/design/system/(docs)/brand/page.tsx +16 -0
- package/reference/app/(design)/design/system/(docs)/brand/voice/page.tsx +15 -0
- package/reference/app/(design)/design/system/(docs)/colour/page.tsx +26 -0
- package/reference/app/(design)/design/system/(docs)/components/[slug]/page.tsx +86 -0
- package/reference/app/(design)/design/system/(docs)/layout.tsx +35 -0
- package/reference/app/(design)/design/system/(docs)/page.tsx +48 -0
- package/reference/app/(design)/design/system/(docs)/patterns/[id]/page.tsx +23 -0
- package/reference/app/(design)/design/system/(docs)/rules/page.tsx +42 -0
- package/reference/app/(design)/design/system/(docs)/scale/page.tsx +15 -0
- package/reference/app/(design)/design/system/(docs)/standards/page.tsx +21 -0
- package/reference/app/(design)/design/system/(docs)/type/page.tsx +15 -0
- package/reference/app/(design)/design/system/(docs)/vocabulary/page.tsx +15 -0
- package/reference/app/(design)/design/system/_components/copy-page.tsx +32 -0
- package/reference/app/(design)/design/system/_components/prose.tsx +26 -0
- package/reference/app/(design)/design/system/_data.ts +53 -0
- package/reference/app/(design)/design/system/manifest/[file]/route.ts +15 -0
- package/reference/app/(design)/design/system/md/[[...path]]/route.ts +94 -0
- package/reference/app/(design)/design/system/preview/[slug]/[example]/page.tsx +23 -0
- package/reference/app/(design)/layout.tsx +16 -0
- package/reference/app/globals.css +14 -0
- package/reference/app/layout.tsx +16 -0
- package/reference/designtools.json +14 -0
- package/reference/package.json +38 -0
- package/reference/proxy.ts +55 -0
- package/registry/a11y-panel.tsx +3 -3
- package/registry/adherence-summary.tsx +4 -4
- package/registry/agent-view.tsx +2 -1
- package/registry/anatomy.tsx +3 -2
- package/registry/ask-claude.tsx +4 -2
- package/registry/code-view.tsx +6 -3
- package/registry/examples.tsx +7 -6
- package/registry/glossary.tsx +2 -1
- package/registry/lib/adherence-types.ts +3 -1
- package/registry/lib/cx.ts +8 -0
- package/registry/lib/manifest-types.ts +13 -2
- package/registry/lib/manifest.ts +46 -0
- package/registry/lib/markdown.ts +30 -25
- package/registry/lib/search.ts +99 -0
- package/registry/lib/standards.ts +82 -0
- package/registry/lib/status.tsx +3 -1
- package/registry/logo-usage.tsx +44 -19
- package/registry/pattern.tsx +3 -2
- package/registry/playground.tsx +7 -6
- package/registry/preview-frame.tsx +3 -3
- package/registry/props-table.tsx +4 -4
- package/registry/rule.tsx +3 -2
- package/registry/search.tsx +134 -0
- package/registry/shell.tsx +86 -23
- package/registry/standards.tsx +22 -26
- package/registry/swatches.tsx +82 -3
- package/registry/usage.tsx +2 -1
- package/registry/variant-matrix.tsx +1 -1
- package/registry/voice-terms.tsx +1 -1
package/registry/props-table.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cx } from "./lib/cx";
|
|
1
|
+
import { cx, target } from "./lib/cx";
|
|
2
2
|
import { inline } from "./lib/text";
|
|
3
3
|
import type { ComponentEntry, PropEntry } from "./lib/manifest-types";
|
|
4
4
|
|
|
@@ -20,7 +20,7 @@ export function PropsTable({ entry }: { entry: ComponentEntry }) {
|
|
|
20
20
|
{own.length > 0 ? <Table rows={own} /> : <p className="text-sm text-muted-foreground">No props of its own.</p>}
|
|
21
21
|
{[...byPackage.entries()].map(([pkg, inherited]) => (
|
|
22
22
|
<details key={pkg} data-slot="props-inherited" className="rounded-md border border-border">
|
|
23
|
-
<summary className="cursor-pointer px-3 py-2 text-sm">
|
|
23
|
+
<summary className={cx(target, "flex cursor-pointer items-center px-3 py-2 text-sm")}>
|
|
24
24
|
{inherited.length} props from <span className="font-mono">{pkg}</span>
|
|
25
25
|
</summary>
|
|
26
26
|
<div className="border-t border-border p-3">
|
|
@@ -46,7 +46,7 @@ export function PropsTable({ entry }: { entry: ComponentEntry }) {
|
|
|
46
46
|
|
|
47
47
|
function Table({ rows }: { rows: [string, PropEntry][] }) {
|
|
48
48
|
return (
|
|
49
|
-
<div className="overflow-x-auto">
|
|
49
|
+
<div className="overflow-x-auto" tabIndex={0} role="region" aria-label="Props">
|
|
50
50
|
<table className="w-full min-w-[36rem] border-collapse text-left text-sm">
|
|
51
51
|
<thead>
|
|
52
52
|
<tr className="border-b border-border text-xs text-muted-foreground">
|
|
@@ -71,7 +71,7 @@ function Table({ rows }: { rows: [string, PropEntry][] }) {
|
|
|
71
71
|
<span className="flex flex-col items-start gap-1">
|
|
72
72
|
<code className={cx("font-mono text-xs font-medium", prop.deprecated !== undefined && "line-through")}>{name}</code>
|
|
73
73
|
{prop.required && (
|
|
74
|
-
<span data-slot="props-required" className="rounded-sm bg-muted px-1 text-
|
|
74
|
+
<span data-slot="props-required" className="rounded-sm bg-muted px-1 text-xs text-muted-foreground">
|
|
75
75
|
required
|
|
76
76
|
</span>
|
|
77
77
|
)}
|
package/registry/rule.tsx
CHANGED
|
@@ -7,9 +7,10 @@ import { ruleMarkdown } from "./lib/markdown";
|
|
|
7
7
|
import type { RuleEntry } from "./lib/manifest-types";
|
|
8
8
|
import { StatusBadge } from "./lib/status";
|
|
9
9
|
import { inline, Prose } from "./lib/text";
|
|
10
|
+
import { target } from "./lib/cx";
|
|
10
11
|
|
|
11
12
|
const tab =
|
|
12
|
-
"-mb-px border-b-2 border-transparent px-1 pb-2 text-sm text-muted-foreground hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring data-[active]:border-primary data-[active]:font-medium data-[active]:text-canvas-foreground";
|
|
13
|
+
target + " -mb-px border-b-2 border-transparent px-1 pb-2 text-sm text-muted-foreground hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-ring data-[active]:border-primary data-[active]:font-medium data-[active]:text-canvas-foreground";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* One rule, brand or interface, written for people: the statement, the line it
|
|
@@ -50,7 +51,7 @@ export function Rule({ rule, children }: { rule: RuleEntry; children?: ReactNode
|
|
|
50
51
|
<article id={rule.id} data-slot="rule" className="flex scroll-mt-8 flex-col gap-4 rounded-lg border border-border p-5">
|
|
51
52
|
<header className="flex flex-wrap items-center justify-between gap-3">
|
|
52
53
|
<h3 className="text-lg font-semibold">
|
|
53
|
-
<a href={`#${rule.id}`} className=
|
|
54
|
+
<a href={`#${rule.id}`} className={`${target} inline-flex items-center hover:underline`}>
|
|
54
55
|
{rule.title}
|
|
55
56
|
</a>
|
|
56
57
|
</h3>
|
|
@@ -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
|
+
}
|
package/registry/shell.tsx
CHANGED
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
4
|
import { usePathname } from "next/navigation";
|
|
5
|
-
import type
|
|
6
|
-
import { cx } from "./lib/cx";
|
|
7
|
-
import {
|
|
5
|
+
import { type ReactNode, useMemo } from "react";
|
|
6
|
+
import { cx, target } from "./lib/cx";
|
|
7
|
+
import { groupFamilies, slugOf } from "./lib/manifest";
|
|
8
8
|
import { Prose } from "./lib/text";
|
|
9
|
-
import type { ComponentsManifest, PatternsManifest, RulesManifest } from "./lib/manifest-types";
|
|
9
|
+
import type { ComponentsManifest, PatternsManifest, RulesManifest, TaxonomyManifest } from "./lib/manifest-types";
|
|
10
|
+
import { searchIndex } from "./lib/search";
|
|
11
|
+
import { DocsSearch } from "./search";
|
|
10
12
|
|
|
11
13
|
export interface DocsLink {
|
|
12
14
|
label: string;
|
|
13
15
|
href: string;
|
|
16
|
+
/** Shown beneath it while you are on it or one of them: a component's parts. */
|
|
17
|
+
children?: DocsLink[];
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
/**
|
|
@@ -24,7 +28,10 @@ export function DocsShell({
|
|
|
24
28
|
manifest,
|
|
25
29
|
rules,
|
|
26
30
|
patterns,
|
|
31
|
+
terms,
|
|
32
|
+
search = true,
|
|
27
33
|
title = "Design system",
|
|
34
|
+
logo,
|
|
28
35
|
basePath = "/design/system",
|
|
29
36
|
brand = [],
|
|
30
37
|
foundations = [],
|
|
@@ -36,7 +43,13 @@ export function DocsShell({
|
|
|
36
43
|
rules?: RulesManifest;
|
|
37
44
|
/** Listed under Patterns, each at `${basePath}/patterns/${id}`. */
|
|
38
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;
|
|
39
50
|
title?: string;
|
|
51
|
+
/** The client's logo, shown beside the title: the site is theirs. */
|
|
52
|
+
logo?: ReactNode;
|
|
40
53
|
/** Component pages live at `${basePath}/components/${slug}`. */
|
|
41
54
|
basePath?: string;
|
|
42
55
|
/** Brand pages: principles, design language, voice, logo, imagery. */
|
|
@@ -48,6 +61,18 @@ export function DocsShell({
|
|
|
48
61
|
children: ReactNode;
|
|
49
62
|
}) {
|
|
50
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
|
+
);
|
|
51
76
|
const groups = [
|
|
52
77
|
...(brand.length ? [{ heading: "Brand", links: brand }] : []),
|
|
53
78
|
...(foundations.length ? [{ heading: "Foundations", links: foundations }] : []),
|
|
@@ -57,9 +82,13 @@ export function DocsShell({
|
|
|
57
82
|
...(patterns?.patterns.length
|
|
58
83
|
? [{ heading: "Patterns", links: patterns.patterns.map((p) => ({ label: p.name, href: `${basePath}/patterns/${p.id}` })) }]
|
|
59
84
|
: []),
|
|
60
|
-
...
|
|
85
|
+
...groupFamilies(manifest).map((g) => ({
|
|
61
86
|
heading: g.category,
|
|
62
|
-
links: g.
|
|
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
|
+
})),
|
|
63
92
|
})),
|
|
64
93
|
...(standards.length ? [{ heading: "Standards", links: standards }] : []),
|
|
65
94
|
];
|
|
@@ -73,20 +102,19 @@ export function DocsShell({
|
|
|
73
102
|
</p>
|
|
74
103
|
<ul className="flex flex-col">
|
|
75
104
|
{group.links.map((link) => {
|
|
76
|
-
const
|
|
105
|
+
const open = current === link.href || link.children?.some((c) => c.href === current);
|
|
77
106
|
return (
|
|
78
107
|
<li key={link.href}>
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
aria-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
</Link>
|
|
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}
|
|
90
118
|
</li>
|
|
91
119
|
);
|
|
92
120
|
})}
|
|
@@ -102,15 +130,31 @@ export function DocsShell({
|
|
|
102
130
|
data-slot="docs-sidebar"
|
|
103
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"
|
|
104
132
|
>
|
|
105
|
-
<Link href={basePath} data-slot="docs-title" className="
|
|
106
|
-
{
|
|
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>
|
|
107
136
|
</Link>
|
|
137
|
+
{search && (
|
|
138
|
+
<div className="px-4 pt-2">
|
|
139
|
+
<DocsSearch items={searchItems} />
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
108
142
|
{nav}
|
|
109
143
|
</aside>
|
|
110
144
|
<details data-slot="docs-nav-disclosure" className="border-b border-border bg-surface text-surface-foreground md:hidden">
|
|
111
|
-
<summary className="cursor-pointer px-4 py-3 font-semibold"
|
|
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>
|
|
112
149
|
{nav}
|
|
113
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
|
+
)}
|
|
114
158
|
<main data-slot="docs-main" className="min-w-0 px-4 py-8 md:px-10 md:py-12">
|
|
115
159
|
<div className="mx-auto flex max-w-5xl flex-col gap-12">{children}</div>
|
|
116
160
|
</main>
|
|
@@ -139,7 +183,8 @@ export function DocsHeader({
|
|
|
139
183
|
/** For a generated page: `Generated from acme-web@1.4.0 at 3f2a9c1`, from `designtools-manifest provenance`. */
|
|
140
184
|
provenance?: string;
|
|
141
185
|
/** For an editorial page. */
|
|
142
|
-
|
|
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" };
|
|
143
188
|
children?: ReactNode;
|
|
144
189
|
}) {
|
|
145
190
|
return (
|
|
@@ -147,7 +192,7 @@ export function DocsHeader({
|
|
|
147
192
|
{(provenance || editorial) && (
|
|
148
193
|
<p data-slot="docs-provenance" className="text-xs text-muted-foreground">
|
|
149
194
|
{editorial
|
|
150
|
-
? `Written by ${editorial.author} · last reviewed ${editorial.reviewed} · ${editorial.status}`
|
|
195
|
+
? `Written by ${editorial.author} · ${editorial.reviewer ? `reviewed by ${editorial.reviewer}, ` : "last reviewed "}${editorial.reviewed} · ${editorial.status}`
|
|
151
196
|
: `${provenance} · generated from the code`}
|
|
152
197
|
</p>
|
|
153
198
|
)}
|
|
@@ -182,3 +227,21 @@ export function DocsSection({ title, lead, children }: { title: string; lead?: s
|
|
|
182
227
|
</section>
|
|
183
228
|
);
|
|
184
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
|
+
}
|
package/registry/standards.tsx
CHANGED
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import { standardsSections } from "./lib/standards";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The project's performance budgets, accessibility target and support baseline,
|
|
5
6
|
* rendered from the thresholds it already records (`.mxa/stack.json`
|
|
6
|
-
* `baselines`), so the docs and the checks cannot disagree.
|
|
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.
|
|
7
11
|
*/
|
|
8
12
|
export function Standards({ baselines, children }: { baselines: Record<string, unknown>; children?: ReactNode }) {
|
|
9
|
-
const sections = Object.entries(baselines).filter(([k]) => !k.startsWith("$"));
|
|
10
13
|
return (
|
|
11
14
|
<div data-slot="standards" className="flex flex-col gap-8">
|
|
12
|
-
{
|
|
13
|
-
<section key={
|
|
14
|
-
<h3 className="text-sm font-medium">{
|
|
15
|
-
{
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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>
|
|
21
26
|
</div>
|
|
22
27
|
))}
|
|
23
28
|
</dl>
|
|
24
|
-
) :
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
))}
|
|
27
35
|
</section>
|
|
28
36
|
))}
|
|
29
37
|
{typeof baselines.$note === "string" && <p className="text-sm text-muted-foreground">{baselines.$note}</p>}
|
|
@@ -31,15 +39,3 @@ export function Standards({ baselines, children }: { baselines: Record<string, u
|
|
|
31
39
|
</div>
|
|
32
40
|
);
|
|
33
41
|
}
|
|
34
|
-
|
|
35
|
-
const TITLES: Record<string, string> = {
|
|
36
|
-
a11y: "Accessibility",
|
|
37
|
-
performance: "Performance budgets",
|
|
38
|
-
support: "Support",
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
function show(v: unknown): string {
|
|
42
|
-
if (Array.isArray(v)) return v.length ? v.join(", ") : "none set";
|
|
43
|
-
if (v && typeof v === "object") return JSON.stringify(v);
|
|
44
|
-
return String(v);
|
|
45
|
-
}
|
package/registry/swatches.tsx
CHANGED
|
@@ -21,7 +21,7 @@ export function SemanticSwatches({
|
|
|
21
21
|
}) {
|
|
22
22
|
const colours = semanticColours(tokens);
|
|
23
23
|
return (
|
|
24
|
-
<div data-slot="swatches" className="overflow-x-auto">
|
|
24
|
+
<div data-slot="swatches" className="overflow-x-auto" tabIndex={0} role="region" aria-label="Colours">
|
|
25
25
|
<table className="w-full min-w-[32rem] border-collapse text-left text-sm">
|
|
26
26
|
<thead>
|
|
27
27
|
<tr className="border-b border-border text-xs text-muted-foreground">
|
|
@@ -110,7 +110,7 @@ function Swatch({
|
|
|
110
110
|
{foreground && <span className="text-muted-foreground"> / {pointsAt(foreground)}</span>}
|
|
111
111
|
</span>
|
|
112
112
|
{grade && ratio !== null && (
|
|
113
|
-
<span data-slot="swatch-contrast" className={cx("text-muted-foreground", grade === "Fail" && "font-medium text-destructive")}>
|
|
113
|
+
<span data-slot="swatch-contrast" className={cx("text-muted-foreground", grade === "Fail" && "w-fit rounded-sm bg-destructive-subdued px-1 font-medium text-destructive-subdued-foreground")}>
|
|
114
114
|
{ratio.toFixed(2)}:1 · {grade}
|
|
115
115
|
</span>
|
|
116
116
|
)}
|
|
@@ -119,6 +119,85 @@ function Swatch({
|
|
|
119
119
|
);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Each colour on each background, per mode: a label is measured on its own fill, and this is
|
|
124
|
+
* everything else, red text on the page, a brand fill on a dark section. Backgrounds are the
|
|
125
|
+
* canvas, surface and overlay and every named `--surface-<name>`; colours are each semantic fill
|
|
126
|
+
* and muted text. T: fine as text (4.5:1); S: fine as a shape only (3:1); none: neither.
|
|
127
|
+
* Measured on what the browser paints, inside each mode's `data-theme`.
|
|
128
|
+
*/
|
|
129
|
+
export function PageContrast({ tokens, modes = ["light", "dark"] }: { tokens: TokensManifest; modes?: Mode[] }) {
|
|
130
|
+
const names = new Set(semanticColours(tokens).map((c) => c.name));
|
|
131
|
+
const colours = [...names].filter((n) => names.has(`${n}-subdued`)).concat(names.has("--muted") ? ["--muted-foreground"] : []);
|
|
132
|
+
const grounds = ["--canvas", "--surface", "--overlay", ...[...names].filter((n) => n.startsWith("--surface-") && !n.endsWith("-foreground") && n !== "--surface-foreground")];
|
|
133
|
+
return (
|
|
134
|
+
<div data-slot="page-contrast" className="flex flex-col gap-6">
|
|
135
|
+
{modes.map((mode) => (
|
|
136
|
+
<PageGrid key={mode} mode={mode} colours={colours} grounds={grounds} />
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function PageGrid({ mode, colours, grounds }: { mode: Mode; colours: string[]; grounds: string[] }) {
|
|
143
|
+
const probe = useRef<HTMLDivElement>(null);
|
|
144
|
+
const [ratios, setRatios] = useState<Record<string, number | null>>({});
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const node = probe.current;
|
|
147
|
+
if (!node) return;
|
|
148
|
+
const paint = (v: string) => {
|
|
149
|
+
node.style.color = `var(${v})`;
|
|
150
|
+
return getComputedStyle(node).color;
|
|
151
|
+
};
|
|
152
|
+
const painted = Object.fromEntries([...colours, ...grounds].map((v) => [v, paint(v)]));
|
|
153
|
+
const out: Record<string, number | null> = {};
|
|
154
|
+
for (const c of colours) for (const g of grounds) out[`${c}|${g}`] = contrastRatio(painted[c], painted[g]);
|
|
155
|
+
setRatios(out);
|
|
156
|
+
}, [colours, grounds]);
|
|
157
|
+
return (
|
|
158
|
+
<div className="overflow-x-auto" tabIndex={0} role="region" aria-label={`Colours on each background, ${mode} mode`}>
|
|
159
|
+
<div ref={probe} data-theme={mode} hidden />
|
|
160
|
+
<table className="w-full min-w-[32rem] border-collapse text-left text-sm">
|
|
161
|
+
<caption className="pb-2 text-left text-sm font-medium capitalize">{mode}</caption>
|
|
162
|
+
<thead>
|
|
163
|
+
<tr className="border-b border-border text-xs text-muted-foreground">
|
|
164
|
+
<th scope="col" className="py-2 pr-4 font-medium">Colour</th>
|
|
165
|
+
{grounds.map((g) => (
|
|
166
|
+
<th key={g} scope="col" className="py-2 pr-4 font-mono font-medium">{g.slice(2)}</th>
|
|
167
|
+
))}
|
|
168
|
+
</tr>
|
|
169
|
+
</thead>
|
|
170
|
+
<tbody>
|
|
171
|
+
{colours.map((c) => (
|
|
172
|
+
<tr key={c} className="border-b border-border">
|
|
173
|
+
<th scope="row" className="py-2 pr-4 font-mono text-xs font-medium">{c.slice(2)}</th>
|
|
174
|
+
{grounds.map((g) => {
|
|
175
|
+
const r = ratios[`${c}|${g}`];
|
|
176
|
+
const mark = r == null ? "" : r >= 4.5 ? "text" : r >= 3 ? "shape" : "none";
|
|
177
|
+
return (
|
|
178
|
+
<td key={g} className="py-2 pr-4">
|
|
179
|
+
<div className="flex items-center gap-2">
|
|
180
|
+
{/* only the sample takes the mode; the number is read on the page */}
|
|
181
|
+
<span data-theme={mode} className="flex h-9 w-12 items-center justify-center rounded-md border border-(--border) text-base font-bold" style={{ background: `var(${g})`, color: `var(${c})` }} aria-hidden>
|
|
182
|
+
Aa
|
|
183
|
+
</span>
|
|
184
|
+
{r != null && (
|
|
185
|
+
<span className={cx("font-mono text-xs", mark === "none" && "rounded-sm bg-destructive-subdued px-1 text-destructive-subdued-foreground")}>
|
|
186
|
+
{r.toFixed(1)} {mark}
|
|
187
|
+
</span>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
</td>
|
|
191
|
+
);
|
|
192
|
+
})}
|
|
193
|
+
</tr>
|
|
194
|
+
))}
|
|
195
|
+
</tbody>
|
|
196
|
+
</table>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
122
201
|
/** The primitive ramps, one row per colour family, each step painted from its own token. */
|
|
123
202
|
export function RampSwatches({ tokens }: { tokens: TokensManifest }) {
|
|
124
203
|
return (
|
|
@@ -135,7 +214,7 @@ export function RampSwatches({ tokens }: { tokens: TokensManifest }) {
|
|
|
135
214
|
className="flex h-14 items-end p-1.5"
|
|
136
215
|
style={{ background: `var(${token.name})` }}
|
|
137
216
|
>
|
|
138
|
-
<span className="rounded-sm bg-canvas/80 px-1 font-mono text-
|
|
217
|
+
<span className="rounded-sm bg-canvas/80 px-1 font-mono text-xs text-canvas-foreground">{step}</span>
|
|
139
218
|
</li>
|
|
140
219
|
))}
|
|
141
220
|
</ol>
|
package/registry/usage.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { slugOf } from "./lib/manifest";
|
|
|
4
4
|
import type { ComponentEntry, ComponentsManifest } from "./lib/manifest-types";
|
|
5
5
|
import { StatusBadge } from "./lib/status";
|
|
6
6
|
import { inline } from "./lib/text";
|
|
7
|
+
import { target } from "./lib/cx";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* When to use a component, when not, and what to use instead, under its status.
|
|
@@ -34,7 +35,7 @@ export function Usage({
|
|
|
34
35
|
<span key={name}>
|
|
35
36
|
{i > 0 && ", "}
|
|
36
37
|
{c ? (
|
|
37
|
-
<Link href={`${basePath}/components/${slugOf(c)}`} className=
|
|
38
|
+
<Link href={`${basePath}/components/${slugOf(c)}`} className={`${target} inline-flex items-center underline underline-offset-2`}>
|
|
38
39
|
{name}
|
|
39
40
|
</Link>
|
|
40
41
|
) : (
|
|
@@ -39,7 +39,7 @@ export function VariantMatrix({
|
|
|
39
39
|
return (
|
|
40
40
|
<div data-slot="variant-matrix" className="flex flex-col gap-8">
|
|
41
41
|
{cols ? (
|
|
42
|
-
<div className="overflow-x-auto">
|
|
42
|
+
<div className="overflow-x-auto" tabIndex={0} role="region" aria-label="Variants">
|
|
43
43
|
<table className="border-collapse text-sm">
|
|
44
44
|
<caption className="sr-only">
|
|
45
45
|
{entry.name}: {rows.name} by {cols.name}
|
package/registry/voice-terms.tsx
CHANGED
|
@@ -6,7 +6,7 @@ export function VoiceTerms({ terms }: { terms: TermEntry[] }) {
|
|
|
6
6
|
const shown = [...terms.filter((t) => t.kind === "product"), ...terms.filter((t) => t.kind !== "product" && t.wrong?.length)];
|
|
7
7
|
if (shown.length === 0) return <p className="text-sm text-muted-foreground">No terms yet. Add them to the taxonomy file.</p>;
|
|
8
8
|
return (
|
|
9
|
-
<div data-slot="voice-terms" className="overflow-x-auto">
|
|
9
|
+
<div data-slot="voice-terms" className="overflow-x-auto" tabIndex={0} role="region" aria-label="Terms">
|
|
10
10
|
<table className="w-full min-w-[28rem] border-collapse text-left text-sm">
|
|
11
11
|
<thead>
|
|
12
12
|
<tr className="border-b border-border text-xs text-muted-foreground">
|