@designtools/docs 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 +10 -0
- package/README.md +24 -2
- package/dist/anatomy.js +2 -1
- package/dist/cli.js +1 -1
- package/dist/glossary.d.ts +1 -1
- package/dist/glossary.js +13 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/lib/live.d.ts +10 -0
- package/dist/lib/live.js +10 -0
- package/dist/lib/lookup.js +3 -2
- package/dist/lib/status.d.ts +2 -3
- package/dist/lib/terms.d.ts +15 -0
- package/dist/lib/terms.js +20 -0
- package/dist/next/config.d.ts +24 -4
- package/dist/next/index.d.ts +3 -0
- package/dist/next/index.js +3 -0
- package/dist/next/markdown.js +1 -1
- package/dist/next/pages.js +82 -16
- package/dist/next/prose.d.ts +2 -2
- package/dist/next/prose.js +6 -4
- package/dist/pattern.d.ts +4 -1
- package/dist/pattern.js +11 -8
- package/dist/playground.js +1 -1
- package/dist/rule.d.ts +9 -5
- package/dist/rule.js +14 -9
- package/dist/shell.d.ts +4 -1
- package/dist/shell.js +13 -11
- package/dist/styles.css +932 -743
- package/dist/swatches.js +10 -4
- package/dist/variant-matrix.js +11 -6
- package/dist/voice-terms.js +6 -5
- package/dist/workbench.js +2 -1
- package/package.json +1 -1
- package/src/anatomy.tsx +6 -1
- package/src/glossary.tsx +39 -40
- package/src/index.ts +2 -1
- package/src/lib/live.tsx +15 -0
- package/src/lib/lookup.tsx +3 -2
- package/src/lib/status.tsx +3 -3
- package/src/lib/terms.tsx +53 -0
- package/src/next/config.ts +25 -4
- package/src/next/index.tsx +3 -0
- package/src/next/markdown.ts +1 -1
- package/src/next/pages.tsx +172 -59
- package/src/next/prose.tsx +16 -6
- package/src/pattern.tsx +42 -37
- package/src/playground.tsx +7 -2
- package/src/rule.tsx +108 -59
- package/src/shell.tsx +30 -16
- package/src/styles.css +4 -2
- package/src/swatches.tsx +68 -57
- package/src/variant-matrix.tsx +63 -80
- package/src/voice-terms.tsx +20 -29
- package/src/workbench.tsx +6 -3
package/src/rule.tsx
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { Live } from "./lib/live";
|
|
3
4
|
import { Bot, Eye, Hash, ShieldCheck } from "lucide-react";
|
|
4
|
-
import type
|
|
5
|
+
import { type ReactNode, useState } from "react";
|
|
5
6
|
import { CodeView } from "./code-view";
|
|
6
7
|
import { cx } from "./lib/cx";
|
|
7
8
|
import { ruleMarkdown } from "./lib/markdown";
|
|
8
9
|
import type { RuleEntry } from "./lib/manifest-types";
|
|
9
10
|
import { StatusBadge } from "./lib/status";
|
|
10
11
|
import { inline, Prose } from "./lib/text";
|
|
11
|
-
import { Eyebrow
|
|
12
|
+
import { Eyebrow } from "./lib/ui";
|
|
12
13
|
|
|
13
14
|
const CHECK: Record<string, string> = { test: "Tested", lint: "Linted", audit: "Audited", manual: "Checked by a person" };
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
|
-
* One rule, brand or interface, written for people
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* One rule, brand or interface, written for people. On a wide screen, its name, status and the check
|
|
18
|
+
* that proves it hold the left column while the right reads: the statement set large, the
|
|
19
|
+
* demonstration (pass it as children, for a rule that needs persuading), the facts, then why. A small
|
|
20
|
+
* switch shows the same rule as an agent reads it.
|
|
20
21
|
*/
|
|
21
22
|
export function Rule({ rule, children }: { rule: RuleEntry; children?: ReactNode }) {
|
|
23
|
+
const [view, setView] = useState<"people" | "agents">("people");
|
|
22
24
|
const chips = (items: string[], mono = false) => (
|
|
23
25
|
<span className="flex flex-wrap gap-1.5">
|
|
24
26
|
{items.map((t) => (
|
|
@@ -33,46 +35,75 @@ export function Rule({ rule, children }: { rule: RuleEntry; children?: ReactNode
|
|
|
33
35
|
if (rule.appliesTo?.length) facts.push(["Applies to", chips(rule.appliesTo)]);
|
|
34
36
|
if (rule.exceptions?.length) facts.push(["Exceptions", <span key="e">{rule.exceptions.join("; ")}</span>]);
|
|
35
37
|
if (rule.tokens?.length) facts.push(["Tokens", chips(rule.tokens, true)]);
|
|
38
|
+
const toggle = (value: "people" | "agents", label: string, Icon: typeof Eye) => (
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
aria-pressed={view === value}
|
|
42
|
+
onClick={() => setView(value)}
|
|
43
|
+
className={cx(
|
|
44
|
+
"inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-md px-2 text-xs font-medium transition-colors duration-(--dt-duration-fast) focus-visible:outline-2 focus-visible:outline-ring",
|
|
45
|
+
view === value ? "bg-muted text-canvas-foreground" : "text-muted-foreground hover:text-canvas-foreground",
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
48
|
+
<Icon aria-hidden className="size-3.5" />
|
|
49
|
+
{label}
|
|
50
|
+
</button>
|
|
51
|
+
);
|
|
36
52
|
|
|
37
53
|
return (
|
|
38
|
-
<article id={rule.id} data-slot="rule" className="scroll-mt-24 border-t border-border pt-
|
|
39
|
-
<
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
<article id={rule.id} data-slot="rule" className="grid scroll-mt-24 gap-x-12 gap-y-6 border-t border-border pt-10 xl:grid-cols-[15rem_minmax(0,1fr)]">
|
|
55
|
+
<header data-slot="rule-header" className="flex flex-col gap-4 xl:sticky xl:top-24 xl:self-start">
|
|
56
|
+
<div className="flex flex-col gap-2.5">
|
|
57
|
+
<span className="text-xs font-medium text-muted-foreground">{rule.kind === "brand" ? "Brand rule" : "Interface rule"}</span>
|
|
58
|
+
<h3 className="group flex items-start gap-1 text-2xl leading-tight tracking-tight">
|
|
59
|
+
{rule.title}
|
|
60
|
+
<a
|
|
61
|
+
href={`#${rule.id}`}
|
|
62
|
+
aria-label={`Link to ${rule.title}`}
|
|
63
|
+
className="relative mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground opacity-0 group-hover:opacity-100 hover:bg-muted focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-ring after:absolute after:-inset-2 after:content-['']"
|
|
64
|
+
>
|
|
65
|
+
<Hash aria-hidden className="size-4" />
|
|
66
|
+
</a>
|
|
67
|
+
</h3>
|
|
68
|
+
<span>
|
|
69
|
+
<StatusBadge status={rule.status} />
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
{rule.check && (
|
|
73
|
+
<p data-slot="rule-check" className="flex items-start gap-2 text-sm">
|
|
74
|
+
<ShieldCheck aria-hidden className="mt-0.5 size-(--dt-size-xs) shrink-0 text-muted-foreground" />
|
|
75
|
+
<span className="flex flex-col gap-0.5">
|
|
76
|
+
<span className="font-medium">
|
|
77
|
+
{CHECK[rule.check.kind] ?? rule.check.kind}
|
|
78
|
+
{rule.check.ref && (
|
|
79
|
+
<>
|
|
80
|
+
{" "}
|
|
81
|
+
by <code className="font-mono text-xs">{rule.check.ref}</code>
|
|
82
|
+
</>
|
|
83
|
+
)}
|
|
84
|
+
</span>
|
|
85
|
+
{rule.check.description && <span className="text-muted-foreground">{inline(rule.check.description)}</span>}
|
|
45
86
|
</span>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<SegmentedList
|
|
58
|
-
label={`${rule.title}, two ways`}
|
|
59
|
-
items={[
|
|
60
|
-
{ value: "rendered", label: "For people", icon: Eye },
|
|
61
|
-
{ value: "agent", label: "For agents", icon: Bot },
|
|
62
|
-
]}
|
|
63
|
-
/>
|
|
64
|
-
</header>
|
|
65
|
-
<SegmentedPanel value="rendered" className="flex flex-col gap-6 pt-4">
|
|
66
|
-
<p data-slot="rule-statement" className="max-w-3xl text-xl leading-snug tracking-tight">
|
|
87
|
+
</p>
|
|
88
|
+
)}
|
|
89
|
+
<div role="group" aria-label={`Read ${rule.title}`} className="-ml-2 flex gap-1">
|
|
90
|
+
{toggle("people", "For people", Eye)}
|
|
91
|
+
{toggle("agents", "For agents", Bot)}
|
|
92
|
+
</div>
|
|
93
|
+
</header>
|
|
94
|
+
|
|
95
|
+
{view === "people" ? (
|
|
96
|
+
<div data-slot="rule-body" className="flex min-w-0 flex-col gap-8">
|
|
97
|
+
<p data-slot="rule-statement" className="max-w-3xl text-2xl leading-snug tracking-tight">
|
|
67
98
|
{inline(rule.statement)}
|
|
68
99
|
</p>
|
|
69
100
|
{children && (
|
|
70
|
-
<div data-slot="rule-demonstration" className="
|
|
71
|
-
<
|
|
101
|
+
<div data-slot="rule-demonstration" data-docs-stage="" className="flex min-h-40 items-center justify-center rounded-2xl bg-muted/60 p-8 sm:p-10">
|
|
102
|
+
<Live>{children}</Live>
|
|
72
103
|
</div>
|
|
73
104
|
)}
|
|
74
105
|
{facts.length > 0 && (
|
|
75
|
-
<dl className="grid gap-x-10 gap-y-
|
|
106
|
+
<dl className="grid max-w-3xl gap-x-10 gap-y-4 sm:grid-cols-2">
|
|
76
107
|
{facts.map(([term, detail]) => (
|
|
77
108
|
<div key={term} className="flex flex-col gap-1.5">
|
|
78
109
|
<dt className="text-xs font-medium text-muted-foreground">{term}</dt>
|
|
@@ -81,36 +112,54 @@ export function Rule({ rule, children }: { rule: RuleEntry; children?: ReactNode
|
|
|
81
112
|
))}
|
|
82
113
|
</dl>
|
|
83
114
|
)}
|
|
84
|
-
{rule.check && (
|
|
85
|
-
<div className="flex items-start gap-2.5 text-sm">
|
|
86
|
-
<ShieldCheck aria-hidden className="mt-0.5 size-(--dt-size-xs) shrink-0 text-muted-foreground" />
|
|
87
|
-
<p className="flex flex-col gap-0.5">
|
|
88
|
-
<span className="font-medium">
|
|
89
|
-
{CHECK[rule.check.kind] ?? rule.check.kind}
|
|
90
|
-
{rule.check.ref && (
|
|
91
|
-
<>
|
|
92
|
-
{" "}
|
|
93
|
-
by <code className="font-mono text-xs">{rule.check.ref}</code>
|
|
94
|
-
</>
|
|
95
|
-
)}
|
|
96
|
-
</span>
|
|
97
|
-
{rule.check.description && <span className="text-muted-foreground">{inline(rule.check.description)}</span>}
|
|
98
|
-
</p>
|
|
99
|
-
</div>
|
|
100
|
-
)}
|
|
101
115
|
{rule.rationale && (
|
|
102
116
|
<div className="flex max-w-3xl flex-col gap-2">
|
|
103
117
|
<Eyebrow>Why</Eyebrow>
|
|
104
|
-
<div className="flex flex-col gap-2 text-
|
|
118
|
+
<div className="flex flex-col gap-2 text-base leading-relaxed text-muted-foreground">
|
|
105
119
|
<Prose text={rule.rationale} />
|
|
106
120
|
</div>
|
|
107
121
|
</div>
|
|
108
122
|
)}
|
|
109
|
-
</
|
|
110
|
-
|
|
123
|
+
</div>
|
|
124
|
+
) : (
|
|
125
|
+
<div className="min-w-0">
|
|
111
126
|
<CodeView code={ruleMarkdown(rule)} lang="markdown" label={`${rule.id}.md`} wrap />
|
|
112
|
-
</
|
|
113
|
-
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
114
129
|
</article>
|
|
115
130
|
);
|
|
116
131
|
}
|
|
132
|
+
|
|
133
|
+
/** Every rule on one screen, a line each, linking to the rule: what it is, how settled, how it is checked. */
|
|
134
|
+
export function RuleIndex({ rules }: { rules: RuleEntry[] }) {
|
|
135
|
+
return (
|
|
136
|
+
<div data-slot="rule-index" className="-mx-1 overflow-x-auto px-1">
|
|
137
|
+
<table className="w-full min-w-[34rem] border-collapse text-left text-sm">
|
|
138
|
+
<thead>
|
|
139
|
+
<tr className="text-xs text-muted-foreground">
|
|
140
|
+
<th scope="col" className="pr-4 pb-3 font-medium">Rule</th>
|
|
141
|
+
<th scope="col" className="px-4 pb-3 font-medium">Kind</th>
|
|
142
|
+
<th scope="col" className="px-4 pb-3 font-medium">Status</th>
|
|
143
|
+
<th scope="col" className="pb-3 pl-4 font-medium">Checked</th>
|
|
144
|
+
</tr>
|
|
145
|
+
</thead>
|
|
146
|
+
<tbody>
|
|
147
|
+
{rules.map((r) => (
|
|
148
|
+
<tr key={r.id} className="border-t border-border">
|
|
149
|
+
<th scope="row" className="py-3 pr-4 font-normal">
|
|
150
|
+
<a href={`#${r.id}`} className="font-medium underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring">
|
|
151
|
+
{r.title}
|
|
152
|
+
</a>
|
|
153
|
+
</th>
|
|
154
|
+
<td className="px-4 py-3 text-muted-foreground capitalize">{r.kind}</td>
|
|
155
|
+
<td className="px-4 py-3">
|
|
156
|
+
<StatusBadge status={r.status} />
|
|
157
|
+
</td>
|
|
158
|
+
<td className="py-3 pl-4 text-muted-foreground">{r.check ? (CHECK[r.check.kind] ?? r.check.kind) : "Not yet"}</td>
|
|
159
|
+
</tr>
|
|
160
|
+
))}
|
|
161
|
+
</tbody>
|
|
162
|
+
</table>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
package/src/shell.tsx
CHANGED
|
@@ -6,7 +6,9 @@ import { ArrowLeft, ArrowRight, BookOpen, ChevronRight, Component, Gauge, House,
|
|
|
6
6
|
import Link from "next/link";
|
|
7
7
|
import { usePathname } from "next/navigation";
|
|
8
8
|
import { createContext, type ReactNode, useContext, useEffect, useMemo, useState } from "react";
|
|
9
|
+
import { Live } from "./lib/live";
|
|
9
10
|
import { accent, cx, display, target } from "./lib/cx";
|
|
11
|
+
import { type Status, StatusBadge } from "./lib/status";
|
|
10
12
|
import { groupFamilies, slugOf } from "./lib/manifest";
|
|
11
13
|
import type { ComponentsManifest, PatternsManifest, RulesManifest, TaxonomyManifest } from "./lib/manifest-types";
|
|
12
14
|
import { searchIndex } from "./lib/search";
|
|
@@ -157,7 +159,7 @@ export function DocsShell({
|
|
|
157
159
|
<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
160
|
{logo && (
|
|
159
161
|
<span data-slot="docs-logo" className="shrink-0">
|
|
160
|
-
{logo}
|
|
162
|
+
<Live>{logo}</Live>
|
|
161
163
|
</span>
|
|
162
164
|
)}
|
|
163
165
|
<span className={cx(logo ? "sr-only" : "truncate text-base font-semibold tracking-tight")}>{title}</span>
|
|
@@ -207,7 +209,7 @@ export function DocsShell({
|
|
|
207
209
|
<X aria-hidden className="size-(--dt-size-sm)" />
|
|
208
210
|
</Dialog.Close>
|
|
209
211
|
</div>
|
|
210
|
-
<Nav sections={sections} current={current} title={title} />
|
|
212
|
+
<Nav sections={sections} current={current} title={title} className="pt-4" />
|
|
211
213
|
{themeSwitch && (
|
|
212
214
|
<div className="flex items-center justify-between gap-3 border-t border-border px-4 py-3">
|
|
213
215
|
<span className="text-xs font-medium text-muted-foreground">Appearance</span>
|
|
@@ -224,7 +226,7 @@ export function DocsShell({
|
|
|
224
226
|
|
|
225
227
|
{hero && current === basePath && (
|
|
226
228
|
<div data-slot="docs-hero" className="w-full">
|
|
227
|
-
{hero}
|
|
229
|
+
<Live>{hero}</Live>
|
|
228
230
|
</div>
|
|
229
231
|
)}
|
|
230
232
|
<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">
|
|
@@ -244,7 +246,7 @@ const iconButton =
|
|
|
244
246
|
"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
247
|
|
|
246
248
|
/** 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 }) {
|
|
249
|
+
function Nav({ sections, current, title, className }: { sections: Section[]; current: string; title: string; className?: string }) {
|
|
248
250
|
const holds = (s: Section) => s.groups.some((g) => g.links.some((l) => l.href === current || l.children?.some((c) => c.href === current)));
|
|
249
251
|
const [open, setOpen] = useState<Record<string, boolean>>(() => Object.fromEntries(sections.map((s) => [s.id, holds(s)])));
|
|
250
252
|
// moving to a page opens its section; nothing closes on its own
|
|
@@ -255,7 +257,7 @@ function Nav({ sections, current, title }: { sections: Section[]; current: strin
|
|
|
255
257
|
}, [current, sections]);
|
|
256
258
|
|
|
257
259
|
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">
|
|
260
|
+
<nav aria-label={title} data-slot="docs-nav" className={cx("min-h-0 flex-1 overflow-y-auto px-3 pb-4 text-sm", className)}>
|
|
259
261
|
<ul className="flex flex-col gap-0.5">
|
|
260
262
|
{sections.map((s) => {
|
|
261
263
|
const Icon = s.icon;
|
|
@@ -294,7 +296,7 @@ function Nav({ sections, current, title }: { sections: Section[]; current: strin
|
|
|
294
296
|
{s.groups.map((g, i) => (
|
|
295
297
|
<div key={g.heading ?? i} className="flex flex-col">
|
|
296
298
|
{g.heading && (
|
|
297
|
-
<p data-slot="docs-nav-heading" className="px-2.5 pt-
|
|
299
|
+
<p data-slot="docs-nav-heading" className="px-2.5 pt-3 pb-1.5 text-[0.6875rem] font-semibold tracking-[0.08em] text-muted-foreground uppercase first:pt-1">
|
|
298
300
|
{g.heading}
|
|
299
301
|
</p>
|
|
300
302
|
)}
|
|
@@ -339,7 +341,7 @@ function NavLink({ link, current }: { link: DocsLink; current: string }) {
|
|
|
339
341
|
data-slot="docs-nav-link"
|
|
340
342
|
className={cx(
|
|
341
343
|
target,
|
|
342
|
-
"relative flex items-center rounded-md px-2.5 text-
|
|
344
|
+
"relative flex items-center rounded-md px-2.5 text-canvas-foreground/80 hover:bg-muted hover:text-canvas-foreground focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring",
|
|
343
345
|
active && "bg-muted font-medium text-canvas-foreground",
|
|
344
346
|
)}
|
|
345
347
|
>
|
|
@@ -416,7 +418,7 @@ function Pager({ prev, next }: { prev?: DocsLink; next?: DocsLink }) {
|
|
|
416
418
|
"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
419
|
);
|
|
418
420
|
return (
|
|
419
|
-
<nav aria-label="Previous and next" data-slot="docs-pager" className="grid gap-3
|
|
421
|
+
<nav aria-label="Previous and next" data-slot="docs-pager" className="-mt-4 grid gap-3 sm:grid-cols-2 lg:-mt-8">
|
|
420
422
|
{prev ? (
|
|
421
423
|
<Link href={prev.href} className={card}>
|
|
422
424
|
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
@@ -448,24 +450,32 @@ export function DocsHeader({
|
|
|
448
450
|
title,
|
|
449
451
|
lead,
|
|
450
452
|
eyebrow,
|
|
453
|
+
status,
|
|
451
454
|
children,
|
|
452
455
|
}: {
|
|
453
456
|
title: string;
|
|
454
457
|
lead?: string;
|
|
455
458
|
/** Above the title. Defaults to the section the page sits in. */
|
|
456
459
|
eyebrow?: string;
|
|
460
|
+
/** How settled the thing the page documents is, beside the eyebrow: a pattern's status. */
|
|
461
|
+
status?: Status;
|
|
457
462
|
/** The page's actions, beside the title: `PageActions`. */
|
|
458
463
|
children?: ReactNode;
|
|
459
464
|
}) {
|
|
460
465
|
const { section } = useContext(DocsPage);
|
|
461
466
|
const over = eyebrow ?? (section && section !== title ? section : undefined);
|
|
462
467
|
return (
|
|
463
|
-
<header data-slot="docs-header" className="flex flex-col gap-6
|
|
468
|
+
<header data-slot="docs-header" className="flex flex-col gap-6">
|
|
464
469
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
465
|
-
{over ? (
|
|
466
|
-
<
|
|
467
|
-
{over
|
|
468
|
-
|
|
470
|
+
{over || status ? (
|
|
471
|
+
<span className="flex flex-wrap items-center gap-3">
|
|
472
|
+
{over && (
|
|
473
|
+
<p data-slot="docs-eyebrow" className="text-xs font-medium tracking-[var(--tracking-widest,0.16em)] text-muted-foreground uppercase">
|
|
474
|
+
{over}
|
|
475
|
+
</p>
|
|
476
|
+
)}
|
|
477
|
+
{status && <StatusBadge status={status} />}
|
|
478
|
+
</span>
|
|
469
479
|
) : (
|
|
470
480
|
<span />
|
|
471
481
|
)}
|
|
@@ -508,7 +518,11 @@ export function DocsFooter({
|
|
|
508
518
|
}) {
|
|
509
519
|
if (!provenance && !editorial && !source) return null;
|
|
510
520
|
return (
|
|
511
|
-
|
|
521
|
+
// order-last: the page renders its footer before the shell adds the pager, and where a page comes from is the last thing on it
|
|
522
|
+
<footer
|
|
523
|
+
data-slot="docs-footer"
|
|
524
|
+
className="order-last -mt-8 flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-border pt-6 text-xs text-muted-foreground lg:-mt-14"
|
|
525
|
+
>
|
|
512
526
|
{(provenance || editorial) && (
|
|
513
527
|
<p data-slot="docs-provenance" className="flex items-center gap-2">
|
|
514
528
|
<span
|
|
@@ -539,11 +553,11 @@ export function DocsSection({ title, lead, children }: { title: string; lead?: s
|
|
|
539
553
|
<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
554
|
<span
|
|
541
555
|
aria-hidden
|
|
542
|
-
className="
|
|
556
|
+
className="font-mono text-xs tracking-[var(--tracking-widest,0.16em)] text-muted-foreground [text-box:trim-both_cap_alphabetic] before:content-[counter(docs-section,decimal-leading-zero)]"
|
|
543
557
|
/>
|
|
544
558
|
<div className="flex min-w-0 flex-col gap-10">
|
|
545
559
|
<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")}>
|
|
560
|
+
<h2 id={id} data-slot="docs-section-title" className={cx(display, "scroll-mt-24 text-4xl leading-[1.1] tracking-tight [text-box:trim-both_cap_alphabetic] md:text-5xl")}>
|
|
547
561
|
{title}
|
|
548
562
|
</h2>
|
|
549
563
|
{lead && <p className="max-w-2xl text-lg leading-relaxed text-muted-foreground">{lead}</p>}
|
package/src/styles.css
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* The docs' styles, compiled at build and scoped to `[data-designtools]`, so they
|
|
3
|
-
* style the docs and nothing else on the page
|
|
4
|
-
* project's own
|
|
3
|
+
* style the docs and nothing else on the page. They never match inside the
|
|
4
|
+
* project's own components shown in the docs (`Live`, `[data-docs-content]`), and
|
|
5
|
+
* on the docs' own elements they carry one attribute more than a project utility,
|
|
6
|
+
* so neither side's utilities leak into the other, whichever loads last.
|
|
5
7
|
*
|
|
6
8
|
* Themed by the project's own variables. Colours are @designtools/tokens'
|
|
7
9
|
* semantic names (--canvas, --surface, --primary, …), so a project on our tokens
|
package/src/swatches.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { type ReactNode, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { contrastRatio, wcagGrade } from "./lib/contrast";
|
|
5
5
|
import { cx } from "./lib/cx";
|
|
6
6
|
import { ramps, semanticColours, valueIn, type Mode } from "./lib/manifest";
|
|
@@ -31,61 +31,73 @@ export function SemanticSwatches({
|
|
|
31
31
|
return token && pointsAt(valueIn(token, mode));
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
// one level of framing: a panel per mode (the dark one has to be dark), chips inside it, and nothing around the family
|
|
35
|
+
const panel = (mode: Mode) =>
|
|
36
|
+
cx("flex flex-col gap-3 rounded-2xl bg-canvas p-4 text-canvas-foreground", mode === "light" && "ring-1 ring-border ring-inset");
|
|
37
|
+
const modeLabel = (mode: Mode) => <span className="text-xs font-medium text-muted-foreground capitalize">{mode}</span>;
|
|
38
|
+
const row = (title: string, meta: ReactNode, body: ReactNode, key?: string) => (
|
|
39
|
+
<div key={key ?? title} data-slot="swatch-row" className="grid gap-x-10 gap-y-5 border-t border-border pt-8 first:border-t-0 first:pt-0 lg:grid-cols-[11rem_minmax(0,1fr)]">
|
|
40
|
+
<div className="flex flex-col gap-1.5">
|
|
41
|
+
<h3 className="text-2xl leading-tight tracking-tight">{title}</h3>
|
|
42
|
+
{meta}
|
|
43
|
+
</div>
|
|
44
|
+
{body}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
|
|
34
48
|
return (
|
|
35
49
|
<div data-slot="swatches" className="flex flex-col gap-10">
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<div
|
|
46
|
-
<
|
|
47
|
-
<Chip name={f}
|
|
48
|
-
<
|
|
49
|
-
<Chip name={`${f}-subdued`} label="subdued" step={step(`${f}-subdued`, mode)} />
|
|
50
|
-
{byName.has(`${f}-hover`) && <Chip name={`${f}-hover`} foreground={`${f}-foreground`} label="hover" />}
|
|
51
|
-
{byName.has(`${f}-active`) && <Chip name={`${f}-active`} foreground={`${f}-foreground`} label="press" />}
|
|
52
|
-
</div>
|
|
50
|
+
{families.map((f) =>
|
|
51
|
+
row(
|
|
52
|
+
(([first, ...rest]) => first.toUpperCase() + rest.join(""))(f.slice(2).replace(/-/g, " ")),
|
|
53
|
+
<code className="font-mono text-xs text-muted-foreground">{f}</code>,
|
|
54
|
+
<div className={cx("grid gap-4", modes.length > 1 && "md:grid-cols-2")}>
|
|
55
|
+
{modes.map((mode) => (
|
|
56
|
+
<div key={mode} data-theme={mode} data-slot="swatch-family" className={panel(mode)}>
|
|
57
|
+
{modeLabel(mode)}
|
|
58
|
+
<Chip name={f} label="fill" step={step(f, mode)} size="lg" />
|
|
59
|
+
<div className="grid grid-cols-3 gap-2">
|
|
60
|
+
<Chip name={`${f}-subdued`} label="subdued" step={step(`${f}-subdued`, mode)} />
|
|
61
|
+
{byName.has(`${f}-hover`) && <Chip name={`${f}-hover`} foreground={`${f}-foreground`} label="hover" />}
|
|
62
|
+
{byName.has(`${f}-active`) && <Chip name={`${f}-active`} foreground={`${f}-foreground`} label="press" />}
|
|
53
63
|
</div>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
</
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
</div>
|
|
65
|
+
))}
|
|
66
|
+
</div>,
|
|
67
|
+
f,
|
|
68
|
+
),
|
|
69
|
+
)}
|
|
59
70
|
|
|
60
|
-
{surfaces.length > 0 &&
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
71
|
+
{surfaces.length > 0 &&
|
|
72
|
+
row(
|
|
73
|
+
"Surfaces",
|
|
74
|
+
<p className="text-sm text-muted-foreground">The page, panels and popups, each with its text.</p>,
|
|
75
|
+
<div className={cx("grid gap-4", modes.length > 1 && "md:grid-cols-2")}>
|
|
64
76
|
{modes.map((mode) => (
|
|
65
|
-
<div key={mode} data-theme={mode} className=
|
|
66
|
-
|
|
67
|
-
<div className="grid grid-cols-2 gap-2
|
|
77
|
+
<div key={mode} data-theme={mode} className={panel(mode)}>
|
|
78
|
+
{modeLabel(mode)}
|
|
79
|
+
<div className="grid grid-cols-2 gap-2 xl:grid-cols-3">
|
|
68
80
|
{surfaces.map((name) => (
|
|
69
81
|
<Chip key={name} name={name} label={name.slice(2)} step={step(name, mode)} outlined />
|
|
70
82
|
))}
|
|
71
83
|
</div>
|
|
72
84
|
</div>
|
|
73
85
|
))}
|
|
74
|
-
</div
|
|
75
|
-
|
|
76
|
-
)}
|
|
86
|
+
</div>,
|
|
87
|
+
)}
|
|
77
88
|
|
|
78
|
-
{utility.length > 0 &&
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<
|
|
89
|
+
{utility.length > 0 &&
|
|
90
|
+
row(
|
|
91
|
+
"Lines, focus and shadow",
|
|
92
|
+
<p className="text-sm text-muted-foreground">Borders, input edges, the focus ring and the shadow colour.</p>,
|
|
93
|
+
<div className={cx("grid gap-4", modes.length > 1 && "md:grid-cols-2")}>
|
|
82
94
|
{modes.map((mode) => (
|
|
83
|
-
<div key={mode} data-theme={mode} className=
|
|
84
|
-
|
|
85
|
-
<ul className="grid grid-cols-2 gap-
|
|
95
|
+
<div key={mode} data-theme={mode} className={panel(mode)}>
|
|
96
|
+
{modeLabel(mode)}
|
|
97
|
+
<ul className="grid grid-cols-2 gap-x-4 gap-y-3">
|
|
86
98
|
{utility.map((name) => (
|
|
87
|
-
<li key={name} data-slot="swatch" className="flex items-center gap-
|
|
88
|
-
<span aria-hidden className="size-
|
|
99
|
+
<li key={name} data-slot="swatch" className="flex items-center gap-3">
|
|
100
|
+
<span aria-hidden className="size-9 shrink-0 rounded-lg border border-border" style={{ background: `var(${name})` }} />
|
|
89
101
|
<span className="flex min-w-0 flex-col font-mono text-xs">
|
|
90
102
|
<span className="truncate font-medium">{name.slice(2)}</span>
|
|
91
103
|
<span className="truncate text-muted-foreground">{step(name, mode)}</span>
|
|
@@ -95,9 +107,8 @@ export function SemanticSwatches({
|
|
|
95
107
|
</ul>
|
|
96
108
|
</div>
|
|
97
109
|
))}
|
|
98
|
-
</div
|
|
99
|
-
|
|
100
|
-
)}
|
|
110
|
+
</div>,
|
|
111
|
+
)}
|
|
101
112
|
</div>
|
|
102
113
|
);
|
|
103
114
|
}
|
|
@@ -191,7 +202,7 @@ export function PageContrast({ tokens, modes = ["light", "dark"] }: { tokens: To
|
|
|
191
202
|
const colours = [...names].filter((n) => names.has(`${n}-subdued`)).concat(names.has("--muted") ? ["--muted-foreground"] : []);
|
|
192
203
|
const grounds = ["--canvas", "--surface", "--overlay", ...[...names].filter((n) => n.startsWith("--surface-") && !n.endsWith("-foreground") && n !== "--surface-foreground")];
|
|
193
204
|
return (
|
|
194
|
-
<div data-slot="page-contrast" className="flex flex-col gap-
|
|
205
|
+
<div data-slot="page-contrast" className="flex flex-col gap-12">
|
|
195
206
|
{modes.map((mode) => (
|
|
196
207
|
<PageGrid key={mode} mode={mode} colours={colours} grounds={grounds} />
|
|
197
208
|
))}
|
|
@@ -215,30 +226,30 @@ function PageGrid({ mode, colours, grounds }: { mode: Mode; colours: string[]; g
|
|
|
215
226
|
setRatios(out);
|
|
216
227
|
}, [colours, grounds]);
|
|
217
228
|
return (
|
|
218
|
-
<div className="overflow-x-auto
|
|
229
|
+
<div className="-mx-1 overflow-x-auto px-1" tabIndex={0} role="region" aria-label={`Colours on each background, ${mode} mode`}>
|
|
219
230
|
<div ref={probe} data-theme={mode} hidden />
|
|
220
|
-
<table className="w-full min-w-[
|
|
221
|
-
<caption className="
|
|
231
|
+
<table className="w-full min-w-[40rem] border-collapse text-left text-sm">
|
|
232
|
+
<caption className="pb-4 text-left text-lg font-medium capitalize">{mode}</caption>
|
|
222
233
|
<thead>
|
|
223
|
-
<tr className="
|
|
224
|
-
<th scope="col" className="
|
|
234
|
+
<tr className="text-xs text-muted-foreground">
|
|
235
|
+
<th scope="col" className="pr-4 pb-3 font-medium">Colour</th>
|
|
225
236
|
{grounds.map((g) => (
|
|
226
|
-
<th key={g} scope="col" className="px-
|
|
237
|
+
<th key={g} scope="col" className="px-3 pb-3 font-mono font-normal">{g.slice(2)}</th>
|
|
227
238
|
))}
|
|
228
239
|
</tr>
|
|
229
240
|
</thead>
|
|
230
241
|
<tbody>
|
|
231
242
|
{colours.map((c) => (
|
|
232
|
-
<tr key={c} className="border-
|
|
233
|
-
<th scope="row" className="
|
|
243
|
+
<tr key={c} className="border-t border-border">
|
|
244
|
+
<th scope="row" className="py-3.5 pr-4 font-mono text-xs font-medium">{c.slice(2)}</th>
|
|
234
245
|
{grounds.map((g) => {
|
|
235
246
|
const r = ratios[`${c}|${g}`];
|
|
236
247
|
const mark = r == null ? "" : r >= 4.5 ? "text" : r >= 3 ? "shape" : "none";
|
|
237
248
|
return (
|
|
238
|
-
<td key={g} className="px-
|
|
239
|
-
<div className="flex items-center gap-2">
|
|
249
|
+
<td key={g} className="px-3 py-3.5">
|
|
250
|
+
<div className="flex items-center gap-2.5">
|
|
240
251
|
{/* only the sample takes the mode; the number is read on the page */}
|
|
241
|
-
<span data-theme={mode} className="flex h-
|
|
252
|
+
<span data-theme={mode} className="flex h-10 w-14 items-center justify-center rounded-lg border border-(--border) text-base font-bold" style={{ background: `var(${g})`, color: `var(${c})` }} aria-hidden>
|
|
242
253
|
Aa
|
|
243
254
|
</span>
|
|
244
255
|
{r != null && (
|