@designtools/docs 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +155 -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 +39 -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 +21 -0
- package/dist/imagery.d.ts +6 -0
- package/dist/imagery.js +17 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +34 -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/live.d.ts +10 -0
- package/dist/lib/live.js +10 -0
- package/dist/lib/lookup.d.ts +35 -0
- package/dist/lib/lookup.js +42 -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 +6 -0
- package/dist/lib/status.js +17 -0
- package/dist/lib/terms.d.ts +15 -0
- package/dist/lib/terms.js +20 -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 +105 -0
- package/dist/next/config.js +1 -0
- package/dist/next/index.d.ts +65 -0
- package/dist/next/index.js +66 -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 +188 -0
- package/dist/next/prose.d.ts +9 -0
- package/dist/next/prose.js +39 -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 +11 -0
- package/dist/pattern.js +22 -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 +16 -0
- package/dist/rule.js +37 -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 +92 -0
- package/dist/shell.js +201 -0
- package/dist/standards.d.ts +13 -0
- package/dist/standards.js +19 -0
- package/dist/styles.css +3708 -0
- package/dist/swatches.d.ts +28 -0
- package/dist/swatches.js +100 -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 +58 -0
- package/dist/voice-terms.d.ts +5 -0
- package/dist/voice-terms.js +14 -0
- package/dist/workbench.d.ts +33 -0
- package/dist/workbench.js +274 -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 +119 -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 +70 -0
- package/src/imagery.tsx +64 -0
- package/src/index.ts +35 -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/live.tsx +15 -0
- package/src/lib/lookup.tsx +50 -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/terms.tsx +53 -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 +101 -0
- package/src/next/index.tsx +103 -0
- package/src/next/markdown.ts +119 -0
- package/src/next/pages.tsx +483 -0
- package/src/next/prose.tsx +97 -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 +72 -0
- package/src/playground.tsx +357 -0
- package/src/preview-frame.tsx +131 -0
- package/src/props-table.tsx +154 -0
- package/src/rule.tsx +165 -0
- package/src/scale.tsx +175 -0
- package/src/search.tsx +155 -0
- package/src/shell.tsx +569 -0
- package/src/standards.tsx +58 -0
- package/src/styles.css +157 -0
- package/src/swatches.tsx +298 -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 +178 -0
- package/src/voice-terms.tsx +37 -0
- package/src/workbench.tsx +587 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pinned states. A pointer cannot be faked, so instead every `:hover`, `:focus`
|
|
3
|
+
* and `:active` rule on the page gets a twin that applies inside an element
|
|
4
|
+
* carrying `data-pseudo="hover"` (or `focus`, `active`; space-separated for more
|
|
5
|
+
* than one). The twin sits beside the original, in the same layer and media
|
|
6
|
+
* query, so it wins and loses exactly where the real state would. This is how
|
|
7
|
+
* Storybook's pseudo-states addon works, done in the browser with no build step.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type Pseudo = "hover" | "focus" | "active";
|
|
11
|
+
|
|
12
|
+
const PATTERNS: [Pseudo, RegExp][] = [
|
|
13
|
+
["hover", /:hover(?![-\w])/g],
|
|
14
|
+
["focus", /:focus-visible(?![-\w])/g],
|
|
15
|
+
["focus", /:focus(?![-\w])/g],
|
|
16
|
+
["active", /:active(?![-\w])/g],
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/** A state inside :not(), :has(), :is() or :where() changes what the selector means when removed, so those are left alone. */
|
|
20
|
+
const NESTED = /:(?:not|has|is|where)\([^)]*:(?:hover|focus|active)/;
|
|
21
|
+
|
|
22
|
+
const done = new WeakSet<CSSStyleSheet>();
|
|
23
|
+
|
|
24
|
+
/** Adds the twins to every stylesheet not yet seen. Cheap to call again: it runs once per sheet, and again only for sheets added since. */
|
|
25
|
+
export function installPseudoStates(): void {
|
|
26
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
27
|
+
if (done.has(sheet)) continue;
|
|
28
|
+
try {
|
|
29
|
+
walk(sheet);
|
|
30
|
+
done.add(sheet);
|
|
31
|
+
} catch {
|
|
32
|
+
// a stylesheet from another origin cannot be read; its states stay unpinnable
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Parent = CSSStyleSheet | CSSGroupingRule | CSSStyleRule;
|
|
38
|
+
|
|
39
|
+
function walk(parent: Parent): void {
|
|
40
|
+
const rules = parent.cssRules;
|
|
41
|
+
// backwards, so a twin inserted after its rule never shifts the rules still to visit
|
|
42
|
+
for (let i = rules.length - 1; i >= 0; i--) {
|
|
43
|
+
const rule = rules[i];
|
|
44
|
+
if (rule instanceof CSSStyleRule) {
|
|
45
|
+
const twin = twinOf(rule.selectorText);
|
|
46
|
+
if (twin) {
|
|
47
|
+
try {
|
|
48
|
+
parent.insertRule(`${twin} { ${rule.style.cssText} }`, i + 1);
|
|
49
|
+
} catch {
|
|
50
|
+
// a selector the browser will not take back (a vendor pseudo-element): skip it
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (rule.cssRules?.length) walk(rule);
|
|
54
|
+
} else if ("cssRules" in rule) {
|
|
55
|
+
walk(rule as CSSGroupingRule);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function twinOf(selector: string): string | null {
|
|
61
|
+
const out: string[] = [];
|
|
62
|
+
// split on commas that are not inside parentheses
|
|
63
|
+
for (const part of selector.split(/,(?![^(]*\))/)) {
|
|
64
|
+
if (NESTED.test(part)) continue;
|
|
65
|
+
for (const [state, pattern] of PATTERNS) {
|
|
66
|
+
pattern.lastIndex = 0;
|
|
67
|
+
if (!pattern.test(part)) continue;
|
|
68
|
+
const rest = part.replace(pattern, "").trim();
|
|
69
|
+
out.push(`:where([data-pseudo~="${state}"]) ${rest || "*"}`);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out.length ? out.join(", ") : null;
|
|
74
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type ComponentType, createContext, type ReactNode, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Everything the docs render that lives in the project: each component, examples
|
|
7
|
+
* module, pattern module and brand asset, keyed the way the manifest names them.
|
|
8
|
+
* The project's generated registry (`designtools-docs map`) imports them and
|
|
9
|
+
* renders a `RegistryProvider` around the docs; a server page cannot hand a
|
|
10
|
+
* component to a client block, so this is how the blocks reach them.
|
|
11
|
+
*/
|
|
12
|
+
export interface Registry {
|
|
13
|
+
// biome-ignore lint/suspicious/noExplicitAny: components of every shape
|
|
14
|
+
components: Record<string, ComponentType<any>>;
|
|
15
|
+
examples: Record<string, Record<string, unknown>>;
|
|
16
|
+
patterns: Record<string, Record<string, unknown>>;
|
|
17
|
+
assets: Record<string, string | { src: string }>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const EMPTY: Registry = { components: {}, examples: {}, patterns: {}, assets: {} };
|
|
21
|
+
const RegistryContext = createContext<Registry>(EMPTY);
|
|
22
|
+
|
|
23
|
+
export function RegistryProvider({ value, children }: { value: Registry; children: ReactNode }) {
|
|
24
|
+
return <RegistryContext.Provider value={value}>{children}</RegistryContext.Provider>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useRegistry(): Registry {
|
|
28
|
+
return useContext(RegistryContext);
|
|
29
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The docs search: components, their parts, rules, patterns and pages, matched
|
|
3
|
+
* fuzzily on their names, with the taxonomy's wrong forms as aliases, so
|
|
4
|
+
* "dropdown" finds Select and "buton" finds Button. It aids navigation and does
|
|
5
|
+
* nothing else: no full text, no index to build, because the manifest is small
|
|
6
|
+
* enough to search in the browser. The same query always gives the same order.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fuzzysort from "fuzzysort";
|
|
10
|
+
import { groupFamilies, slugOf } from "./manifest";
|
|
11
|
+
import type { ComponentsManifest, PatternEntry, RuleEntry, TermEntry } from "./manifest-types";
|
|
12
|
+
|
|
13
|
+
export type SearchKind = "Component" | "Rule" | "Pattern" | "Page";
|
|
14
|
+
|
|
15
|
+
export interface SearchItem {
|
|
16
|
+
kind: SearchKind;
|
|
17
|
+
label: string;
|
|
18
|
+
href: string;
|
|
19
|
+
/** The taxonomy's wrong forms for it: typing one finds it, and the result says so. */
|
|
20
|
+
wrong: string[];
|
|
21
|
+
/** Anything else it answers to quietly, such as a rule's id. */
|
|
22
|
+
also?: string[];
|
|
23
|
+
/** One short line under the label: a category, "part of Card", a rule's kind. */
|
|
24
|
+
detail?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SearchGroup {
|
|
28
|
+
kind: SearchKind;
|
|
29
|
+
/** `matched` is the wrong form a result was found by ("Dropdown" for Select). */
|
|
30
|
+
items: (SearchItem & { matched?: string })[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ORDER: SearchKind[] = ["Component", "Rule", "Pattern", "Page"];
|
|
34
|
+
|
|
35
|
+
export function searchIndex({
|
|
36
|
+
manifest,
|
|
37
|
+
rules = [],
|
|
38
|
+
patterns = [],
|
|
39
|
+
terms = [],
|
|
40
|
+
pages = [],
|
|
41
|
+
basePath = "/design/system",
|
|
42
|
+
}: {
|
|
43
|
+
manifest: ComponentsManifest;
|
|
44
|
+
rules?: RuleEntry[];
|
|
45
|
+
patterns?: PatternEntry[];
|
|
46
|
+
terms?: TermEntry[];
|
|
47
|
+
pages?: { label: string; href: string }[];
|
|
48
|
+
basePath?: string;
|
|
49
|
+
}): SearchItem[] {
|
|
50
|
+
const wrong = new Map<string, string[]>();
|
|
51
|
+
for (const t of terms) if (t.wrong?.length) wrong.set(t.name.toLowerCase(), t.wrong);
|
|
52
|
+
const wrongOf = (name: string) => wrong.get(name.toLowerCase()) ?? [];
|
|
53
|
+
|
|
54
|
+
const items: SearchItem[] = [];
|
|
55
|
+
for (const { category, families } of groupFamilies(manifest)) {
|
|
56
|
+
for (const { head, parts } of families) {
|
|
57
|
+
items.push({ kind: "Component", label: head.name, href: `${basePath}/components/${slugOf(head)}`, wrong: wrongOf(head.name), detail: category });
|
|
58
|
+
for (const p of parts) {
|
|
59
|
+
items.push({ kind: "Component", label: p.name, href: `${basePath}/components/${slugOf(p)}`, wrong: wrongOf(p.name), detail: `part of ${head.name}` });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const r of rules) {
|
|
64
|
+
items.push({ kind: "Rule", label: r.title, href: `${basePath}/rules#${r.id}`, wrong: wrongOf(r.title), also: [r.id.replace(/-/g, " ")], detail: r.kind === "brand" ? "Brand rule" : "Interface rule" });
|
|
65
|
+
}
|
|
66
|
+
for (const p of patterns) items.push({ kind: "Pattern", label: p.name, href: `${basePath}/patterns/${p.id}`, wrong: wrongOf(p.name) });
|
|
67
|
+
for (const p of pages) items.push({ kind: "Page", label: p.label, href: p.href, wrong: [] });
|
|
68
|
+
return items;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The matches for a query, grouped by kind (components, rules, patterns, pages),
|
|
73
|
+
* best first within each, at most `perGroup` of each. A match found only through
|
|
74
|
+
* an alias says which: `matched` is the wrong form typed ("Dropdown").
|
|
75
|
+
*/
|
|
76
|
+
export function search(items: SearchItem[], query: string, perGroup = 6): SearchGroup[] {
|
|
77
|
+
const q = query.trim();
|
|
78
|
+
if (!q) return [];
|
|
79
|
+
const results = fuzzysort.go(q, items, { keys: ["label", (item: SearchItem) => item.wrong.join(" / "), (item: SearchItem) => (item.also ?? []).join(" / ")], limit: 0, threshold: 0.3 });
|
|
80
|
+
const ranked = [...results].sort(
|
|
81
|
+
(a, b) => b.score - a.score || ORDER.indexOf(a.obj.kind) - ORDER.indexOf(b.obj.kind) || (a.obj.label < b.obj.label ? -1 : a.obj.label > b.obj.label ? 1 : 0),
|
|
82
|
+
);
|
|
83
|
+
return ORDER.map((kind) => ({
|
|
84
|
+
kind,
|
|
85
|
+
items: ranked
|
|
86
|
+
.filter((r) => r.obj.kind === kind)
|
|
87
|
+
.slice(0, perGroup)
|
|
88
|
+
.map((r) => {
|
|
89
|
+
const viaWrong = (r[1]?.score ?? 0) > Math.max(r[0]?.score ?? 0, r[2]?.score ?? 0);
|
|
90
|
+
// the wrong form that matched best, to say why a result is here ("Dropdown" for Select)
|
|
91
|
+
const alias = viaWrong
|
|
92
|
+
? r.obj.wrong
|
|
93
|
+
.map((a) => ({ a, score: fuzzysort.single(q, a)?.score ?? 0 }))
|
|
94
|
+
.sort((x, y) => y.score - x.score)[0]?.a
|
|
95
|
+
: undefined;
|
|
96
|
+
return alias ? { ...r.obj, matched: alias } : r.obj;
|
|
97
|
+
}),
|
|
98
|
+
})).filter((g) => g.items.length > 0);
|
|
99
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The baselines a project records (`.mxa/stack.json` `baselines`), as a person
|
|
3
|
+
* reads them: titled sections, labelled rows, notes as sentences, and nothing
|
|
4
|
+
* shown for a value nobody has set. One shape, so the page and its markdown agree.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface StandardsSection {
|
|
8
|
+
key: string;
|
|
9
|
+
title: string;
|
|
10
|
+
rows: { label: string; value: string }[];
|
|
11
|
+
notes: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const TITLES: Record<string, string> = {
|
|
15
|
+
a11y: "Accessibility",
|
|
16
|
+
performance: "Performance budgets",
|
|
17
|
+
support: "Support",
|
|
18
|
+
seo: "Search and discovery",
|
|
19
|
+
licences: "Licences allowed",
|
|
20
|
+
textSize: "Text size",
|
|
21
|
+
touchTargets: "Touch targets",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const LABELS: Record<string, string> = {
|
|
25
|
+
browsers: "Browsers",
|
|
26
|
+
devices: "Devices",
|
|
27
|
+
viewports: "Viewports",
|
|
28
|
+
LCP: "Largest contentful paint",
|
|
29
|
+
CLS: "Cumulative layout shift",
|
|
30
|
+
TBT: "Total blocking time",
|
|
31
|
+
INP: "Interaction to next paint",
|
|
32
|
+
lighthouse: "Lighthouse score",
|
|
33
|
+
aiCrawlable: "Readable by AI crawlers",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const UNITS: Record<string, string> = { viewports: "px" };
|
|
37
|
+
|
|
38
|
+
/** `textSize` to "Text size", `aiCrawlable` to "Ai crawlable": a fallback, never as good as a title. */
|
|
39
|
+
export function humanise(key: string): string {
|
|
40
|
+
const words = key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_]+/g, " ").toLowerCase();
|
|
41
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function shown(key: string, value: unknown): string | undefined {
|
|
45
|
+
if (value === null || value === undefined || value === "") return undefined;
|
|
46
|
+
if (typeof value === "boolean") return value ? "yes" : "no";
|
|
47
|
+
if (Array.isArray(value)) return value.length ? value.join(", ") + (UNITS[key] ? ` ${UNITS[key]}` : "") : "not agreed yet";
|
|
48
|
+
if (typeof value === "object") return undefined;
|
|
49
|
+
return String(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function standardsSections(baselines: Record<string, unknown>): StandardsSection[] {
|
|
53
|
+
const keys = Object.keys(baselines).filter((k) => !k.startsWith("$"));
|
|
54
|
+
// `a11yNotes` is the note on `a11y`, not a section of its own
|
|
55
|
+
const noteOf = (k: string) => (/Notes?$/.test(k) && keys.includes(k.replace(/Notes?$/, "")) ? k.replace(/Notes?$/, "") : undefined);
|
|
56
|
+
const out: StandardsSection[] = [];
|
|
57
|
+
for (const key of keys) {
|
|
58
|
+
if (noteOf(key)) continue;
|
|
59
|
+
const value = baselines[key];
|
|
60
|
+
const section: StandardsSection = { key, title: TITLES[key] ?? humanise(key), rows: [], notes: [] };
|
|
61
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
62
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
63
|
+
if (k.startsWith("$")) continue;
|
|
64
|
+
if (/^notes?$/i.test(k)) {
|
|
65
|
+
if (typeof v === "string" && v) section.notes.push(v);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const text = shown(k, v);
|
|
69
|
+
if (text !== undefined) section.rows.push({ label: LABELS[k] ?? humanise(k), value: text });
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
const text = shown(key, value);
|
|
73
|
+
if (text !== undefined) section.rows.push({ label: "", value: text });
|
|
74
|
+
}
|
|
75
|
+
for (const k of keys) {
|
|
76
|
+
const v = baselines[k];
|
|
77
|
+
if (noteOf(k) === key && typeof v === "string" && v) section.notes.push(v);
|
|
78
|
+
}
|
|
79
|
+
if (section.rows.length || section.notes.length) out.push(section);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { cx } from "./cx";
|
|
2
|
+
|
|
3
|
+
export type Status = "stable" | "emerging" | "deprecated" | "confirmed" | "assumption" | "measured" | "draft" | "published";
|
|
4
|
+
|
|
5
|
+
const LOOK: Record<Status, string> = {
|
|
6
|
+
stable: "bg-success-subdued text-success-subdued-foreground",
|
|
7
|
+
confirmed: "bg-success-subdued text-success-subdued-foreground",
|
|
8
|
+
published: "bg-success-subdued text-success-subdued-foreground",
|
|
9
|
+
emerging: "bg-warning-subdued text-warning-subdued-foreground",
|
|
10
|
+
assumption: "bg-warning-subdued text-warning-subdued-foreground",
|
|
11
|
+
// generated from the tokens: a fact, not a decision, so neither settled nor pending
|
|
12
|
+
measured: "bg-muted text-muted-foreground",
|
|
13
|
+
draft: "bg-warning-subdued text-warning-subdued-foreground",
|
|
14
|
+
deprecated: "bg-destructive-subdued text-destructive-subdued-foreground",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** A status, everywhere one is shown: components, patterns, rules and editorial pages. Unset says so. */
|
|
18
|
+
export function StatusBadge({ status, className }: { status?: Status; className?: string }) {
|
|
19
|
+
return (
|
|
20
|
+
<span
|
|
21
|
+
data-slot="status-badge"
|
|
22
|
+
data-status={status ?? "unset"}
|
|
23
|
+
className={cx(
|
|
24
|
+
"inline-flex h-6 items-center rounded-full px-2.5 text-xs font-medium",
|
|
25
|
+
status ? LOOK[status] : "border border-dashed border-border text-muted-foreground",
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
>
|
|
29
|
+
{status ?? "status not set"}
|
|
30
|
+
</span>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { X } from "lucide-react";
|
|
2
|
+
import type { TermEntry } from "./manifest-types";
|
|
3
|
+
import { inline } from "./text";
|
|
4
|
+
|
|
5
|
+
/** The wrong forms of a term: quiet chips, each with a cross, so "not this" reads without shouting. */
|
|
6
|
+
export function WrongForms({ wrong }: { wrong?: string[] }) {
|
|
7
|
+
if (!wrong?.length) return <span className="text-sm text-muted-foreground">None</span>;
|
|
8
|
+
return (
|
|
9
|
+
<span className="flex flex-wrap gap-1.5">
|
|
10
|
+
<span className="sr-only">Not </span>
|
|
11
|
+
{wrong.map((w) => (
|
|
12
|
+
<span key={w} data-slot="term-wrong" className="inline-flex min-h-7 items-center gap-1 rounded-md bg-muted px-2 text-sm text-muted-foreground">
|
|
13
|
+
<X aria-hidden className="size-3 shrink-0" />
|
|
14
|
+
<span className="line-through decoration-current/40">{w}</span>
|
|
15
|
+
</span>
|
|
16
|
+
))}
|
|
17
|
+
</span>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One term on one row: the word to use, the words not to, and why. Three columns on a wide screen, under
|
|
23
|
+
* the labels `TermHead` draws; stacked on a phone.
|
|
24
|
+
*/
|
|
25
|
+
export function TermRow({ term, kind }: { term: TermEntry; kind?: boolean }) {
|
|
26
|
+
return (
|
|
27
|
+
<div data-slot="term" className="grid gap-x-8 gap-y-2 border-t border-border py-5 md:grid-cols-[minmax(10rem,14rem)_minmax(0,1fr)_minmax(0,1.3fr)] md:items-baseline">
|
|
28
|
+
<dt className="flex items-baseline gap-2">
|
|
29
|
+
<span className="text-xl leading-snug tracking-tight">{term.name}</span>
|
|
30
|
+
{kind && <span className="font-mono text-xs text-muted-foreground">{term.kind}</span>}
|
|
31
|
+
<span className="sr-only">, say this</span>
|
|
32
|
+
</dt>
|
|
33
|
+
<dd>
|
|
34
|
+
<WrongForms wrong={term.wrong} />
|
|
35
|
+
</dd>
|
|
36
|
+
<dd className="text-sm leading-relaxed text-muted-foreground">
|
|
37
|
+
{term.note ? inline(term.note) : term.kind !== "product" ? `The ${term.kind}'s name in the code.` : null}
|
|
38
|
+
{term.ref && <code className="ml-1 font-mono text-xs">{term.ref}</code>}
|
|
39
|
+
</dd>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The column labels over a list of `TermRow`s, on a wide screen. */
|
|
45
|
+
export function TermHead() {
|
|
46
|
+
return (
|
|
47
|
+
<div aria-hidden className="hidden gap-x-8 pb-3 text-xs font-medium text-muted-foreground md:grid md:grid-cols-[minmax(10rem,14rem)_minmax(0,1fr)_minmax(0,1.3fr)]">
|
|
48
|
+
<span>Say</span>
|
|
49
|
+
<span>Not</span>
|
|
50
|
+
<span>Why</span>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
package/src/lib/text.tsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/** JSDoc prose: paragraphs split on blank lines, `code` spans set as code. Nothing else is interpreted. */
|
|
4
|
+
export function Prose({ text, className }: { text: string; className?: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
{text.split(/\n{2,}/).map((p, i) => (
|
|
8
|
+
<p key={i} className={className}>
|
|
9
|
+
{inline(p)}
|
|
10
|
+
</p>
|
|
11
|
+
))}
|
|
12
|
+
</>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** One line of prose with its `code` spans. */
|
|
17
|
+
export function inline(text: string): ReactNode[] {
|
|
18
|
+
return text.split(/(`[^`]+`)/).map((part, i) =>
|
|
19
|
+
/^`[^`]+`$/.test(part) ? (
|
|
20
|
+
<code key={i} className="rounded-sm bg-muted px-1 font-mono text-[0.9em]">
|
|
21
|
+
{part.slice(1, -1)}
|
|
22
|
+
</code>
|
|
23
|
+
) : (
|
|
24
|
+
part
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
}
|