@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,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The markdown form of every block, generated from the manifest rather than
|
|
3
|
+
* converted from HTML, so the same manifest always gives the same markdown.
|
|
4
|
+
* A docs page answers `Accept: text/markdown` (or `.md`) with these; the agent
|
|
5
|
+
* tabs show them; copy-page copies them.
|
|
6
|
+
*
|
|
7
|
+
* Structure survives as typed fences (```rule, ```example, ```do, ```dont) so
|
|
8
|
+
* an agent can tell what each part is. Status travels with everything.
|
|
9
|
+
*/
|
|
10
|
+
import { groupByCategory, importOf, ramps, semanticColours, shortName, slugOf, tokensOfTier, typeScale, valueIn } from "./manifest.js";
|
|
11
|
+
import { standardsSections } from "./standards.js";
|
|
12
|
+
// ------------------------------------------------------------ headers
|
|
13
|
+
/** A generated page: the system version and commit it came from. Never a review date, which would be invented. */
|
|
14
|
+
export function generatedHeader(provenance) {
|
|
15
|
+
return `> ${provenance}. Generated from the code; do not edit this page by hand.\n\n`;
|
|
16
|
+
}
|
|
17
|
+
/** An editorial page: who wrote it, when it was last reviewed, and whether it is settled. */
|
|
18
|
+
export function editorialHeader({ author, reviewer, reviewed, status }) {
|
|
19
|
+
return `> Written by ${author}. ${reviewer ? `Reviewed by ${reviewer}, ${reviewed}` : `Last reviewed ${reviewed}`}. Status: ${status}.\n\n`;
|
|
20
|
+
}
|
|
21
|
+
// ------------------------------------------------------------ components
|
|
22
|
+
/**
|
|
23
|
+
* A component as an agent reads it. `level` is its heading's: 1 when it is the
|
|
24
|
+
* whole page (`/components/button.md`), 2 inside a longer document (`context.md`),
|
|
25
|
+
* so a page never opens with the same heading twice.
|
|
26
|
+
*/
|
|
27
|
+
export function componentMarkdown(entry, level = 2) {
|
|
28
|
+
const out = [`${"#".repeat(level)} ${entry.name}`, ""];
|
|
29
|
+
out.push(`Status: ${entry.status ?? "not set"}${entry.statusNote ? `. ${entry.statusNote}` : ""}`, "");
|
|
30
|
+
out.push(`\`import { ${entry.export} } from "${importOf(entry)}"\``, "");
|
|
31
|
+
if (entry.description)
|
|
32
|
+
out.push(entry.description, "");
|
|
33
|
+
if (entry.usage) {
|
|
34
|
+
if (entry.usage.use)
|
|
35
|
+
out.push(`Use it for: ${entry.usage.use}`);
|
|
36
|
+
if (entry.usage.avoid)
|
|
37
|
+
out.push(`Not for: ${entry.usage.avoid}`);
|
|
38
|
+
if (entry.usage.instead?.length)
|
|
39
|
+
out.push(`Instead: ${entry.usage.instead.join(", ")}`);
|
|
40
|
+
out.push("");
|
|
41
|
+
}
|
|
42
|
+
if (entry.examples)
|
|
43
|
+
out.push(...examplesMarkdown(entry.examples.canonical, entry.examples.do, entry.examples.dont, entry.examples.other));
|
|
44
|
+
const own = Object.entries(entry.props).filter(([, p]) => !p.from);
|
|
45
|
+
const inherited = Object.entries(entry.props).filter(([, p]) => p.from);
|
|
46
|
+
if (own.length) {
|
|
47
|
+
out.push("Props:");
|
|
48
|
+
for (const [name, p] of own)
|
|
49
|
+
out.push(`- ${propLine(name, p)}`);
|
|
50
|
+
out.push("");
|
|
51
|
+
}
|
|
52
|
+
const also = [
|
|
53
|
+
...(entry.inherits ?? []).map((x) => (/^[a-z][\w-]*$/.test(x) ? `every <${x}> prop` : `every prop of ${x}`)),
|
|
54
|
+
...[...new Set(inherited.map(([, p]) => p.from))].map((pkg) => `${inherited.filter(([, p]) => p.from === pkg).length} props from ${pkg}`),
|
|
55
|
+
];
|
|
56
|
+
if (also.length)
|
|
57
|
+
out.push(`Also accepts ${also.join(", ")}.`, "");
|
|
58
|
+
const variants = entry.variants;
|
|
59
|
+
if (variants?.axes.length) {
|
|
60
|
+
const from = variants.from === "props" ? "its props" : `the ${variants.from}() config \`${variants.config}\``;
|
|
61
|
+
out.push(`Variants, from ${from}. Use these, not classes, to change how it looks:`);
|
|
62
|
+
for (const axis of variants.axes) {
|
|
63
|
+
out.push(`- \`${axis.name}\`: ${axis.options.map((o) => (o.name === axis.default ? `${o.name} (default)` : o.name)).join(" | ")}`);
|
|
64
|
+
for (const o of axis.options)
|
|
65
|
+
if (o.description)
|
|
66
|
+
out.push(` - \`${o.name}\`: ${o.description}`);
|
|
67
|
+
}
|
|
68
|
+
out.push("");
|
|
69
|
+
}
|
|
70
|
+
if (entry.slots.length)
|
|
71
|
+
out.push(`Parts (\`data-slot\`): ${entry.slots.map((s) => `\`${s}\``).join(", ")}. Keep these on the elements they mark.`, "");
|
|
72
|
+
if (entry.a11y?.role || entry.a11y?.keyboard) {
|
|
73
|
+
out.push(`Accessibility: ${[entry.a11y.role && `role \`${entry.a11y.role}\``, entry.a11y.keyboard && `keys ${entry.a11y.keyboard.join(", ")}`].filter(Boolean).join("; ")}.`, "");
|
|
74
|
+
}
|
|
75
|
+
if (entry.replaces?.length)
|
|
76
|
+
out.push(`Use it instead of a raw ${entry.replaces.map((r) => `\`<${r}>\``).join(" or ")}.`, "");
|
|
77
|
+
for (const snippet of entry.snippets ?? [])
|
|
78
|
+
out.push("```tsx", snippet, "```", "");
|
|
79
|
+
if (entry.primitive)
|
|
80
|
+
out.push(`Wraps: ${entry.primitive}`, "");
|
|
81
|
+
if (entry.aria)
|
|
82
|
+
out.push(`ARIA pattern: ${entry.aria}`, "");
|
|
83
|
+
if (entry.see?.length)
|
|
84
|
+
out.push(`See also: ${entry.see.join(", ")}`, "");
|
|
85
|
+
return end(out);
|
|
86
|
+
}
|
|
87
|
+
function examplesMarkdown(canonical, dos, donts, other) {
|
|
88
|
+
const out = [];
|
|
89
|
+
const fence = (kind, ex) => {
|
|
90
|
+
if (ex.description)
|
|
91
|
+
out.push(ex.description);
|
|
92
|
+
out.push(`\`\`\`${kind} ${ex.name}`, ex.code ?? `// ${ex.name}`, "```", "");
|
|
93
|
+
};
|
|
94
|
+
if (canonical) {
|
|
95
|
+
out.push("Copy this one:");
|
|
96
|
+
fence("example", canonical);
|
|
97
|
+
}
|
|
98
|
+
// in pairs, as the page shows them: the first do beside the first don't
|
|
99
|
+
for (let i = 0; i < Math.max(dos.length, donts.length); i++) {
|
|
100
|
+
if (dos[i])
|
|
101
|
+
fence("do", dos[i]);
|
|
102
|
+
if (donts[i])
|
|
103
|
+
fence("dont", donts[i]);
|
|
104
|
+
}
|
|
105
|
+
for (const o of other)
|
|
106
|
+
fence("example", o);
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
function propLine(name, p) {
|
|
110
|
+
const type = p.values ? p.values.map((v) => `"${v}"`).join(" | ") : p.type;
|
|
111
|
+
const bits = [`\`${name}${p.required ? "" : "?"}\`: \`${type}\``];
|
|
112
|
+
if (p.default !== undefined)
|
|
113
|
+
bits.push(`default \`${p.default}\``);
|
|
114
|
+
if (p.deprecated !== undefined)
|
|
115
|
+
bits.push(`deprecated${p.deprecated ? `: ${p.deprecated}` : ""}`);
|
|
116
|
+
let line = bits.join(", ");
|
|
117
|
+
if (p.description)
|
|
118
|
+
line += `. ${p.description.replace(/\s*\n\s*/g, " ")}`;
|
|
119
|
+
return line;
|
|
120
|
+
}
|
|
121
|
+
// ------------------------------------------------------------ tokens
|
|
122
|
+
const TIER_TITLES = [
|
|
123
|
+
["spacing", "Spacing"],
|
|
124
|
+
["size", "Size"],
|
|
125
|
+
["radius", "Radius"],
|
|
126
|
+
["shadow", "Shadow"],
|
|
127
|
+
["duration", "Duration"],
|
|
128
|
+
["ease", "Easing"],
|
|
129
|
+
["motion", "Motion"],
|
|
130
|
+
];
|
|
131
|
+
export function coloursMarkdown(tokens) {
|
|
132
|
+
const out = ["## Colour", "", "Write the semantic names in components (`bg-primary text-primary-foreground`). The ramps are what they point at.", ""];
|
|
133
|
+
out.push("| Token | Light | Dark |", "| --- | --- | --- |");
|
|
134
|
+
for (const c of semanticColours(tokens)) {
|
|
135
|
+
out.push(`| \`${c.name}\`${c.foreground ? ` with \`${c.foreground.name}\`` : ""} | ${cell(valueIn(c.background, "light"))} | ${cell(valueIn(c.background, "dark"))} |`);
|
|
136
|
+
}
|
|
137
|
+
out.push("");
|
|
138
|
+
for (const ramp of ramps(tokens))
|
|
139
|
+
out.push(`- ${ramp.family}: ${ramp.steps.map((s) => s.step).join(", ")}`);
|
|
140
|
+
return end(out);
|
|
141
|
+
}
|
|
142
|
+
export function typeMarkdown(tokens) {
|
|
143
|
+
const out = ["## Type", "", "| Size | Value | Line height |", "| --- | --- | --- |"];
|
|
144
|
+
for (const s of typeScale(tokens))
|
|
145
|
+
out.push(`| \`text-${shortName(s.size, "text")}\` | ${cell(s.size.values.default)} | ${cell(s.lineHeight?.values.default)} |`);
|
|
146
|
+
for (const [tier, title] of [["font", "Families"], ["font-weight", "Weights"], ["leading", "Leading"], ["tracking", "Tracking"]]) {
|
|
147
|
+
const rows = tokensOfTier(tokens, tier);
|
|
148
|
+
if (rows.length)
|
|
149
|
+
out.push("", `${title}: ${rows.map((t) => `\`${shortName(t, tier)}\` ${t.values.default}`).join(", ")}`);
|
|
150
|
+
}
|
|
151
|
+
return end(out);
|
|
152
|
+
}
|
|
153
|
+
export function scaleMarkdown(tokens) {
|
|
154
|
+
const out = ["## Scale", ""];
|
|
155
|
+
for (const [tier, title] of TIER_TITLES) {
|
|
156
|
+
const rows = tokensOfTier(tokens, tier);
|
|
157
|
+
if (!rows.length)
|
|
158
|
+
continue;
|
|
159
|
+
out.push(`### ${title}`, "", "| Token | Value | Notes |", "| --- | --- | --- |");
|
|
160
|
+
for (const t of rows)
|
|
161
|
+
out.push(`| \`${t.name}\` | ${cell(t.values.default)} | ${cell(t.description)} |`);
|
|
162
|
+
out.push("");
|
|
163
|
+
}
|
|
164
|
+
return end(out);
|
|
165
|
+
}
|
|
166
|
+
// ------------------------------------------------------------ rules, patterns, taxonomy, brand
|
|
167
|
+
export function ruleMarkdown(rule) {
|
|
168
|
+
const out = [`### ${rule.title}`, "", `\`\`\`rule id=${rule.id} kind=${rule.kind} status=${rule.status}`, rule.statement, "```", ""];
|
|
169
|
+
if (rule.threshold)
|
|
170
|
+
out.push(`- Threshold: ${rule.threshold}`);
|
|
171
|
+
if (rule.appliesTo?.length)
|
|
172
|
+
out.push(`- Applies to: ${rule.appliesTo.join(", ")}`);
|
|
173
|
+
if (rule.exceptions?.length)
|
|
174
|
+
out.push(`- Exceptions: ${rule.exceptions.join("; ")}`);
|
|
175
|
+
if (rule.tokens?.length)
|
|
176
|
+
out.push(`- Tokens: ${rule.tokens.map((t) => `\`${t}\``).join(", ")}`);
|
|
177
|
+
if (rule.check)
|
|
178
|
+
out.push(`- Checked by: ${rule.check.kind}${rule.check.ref ? ` (\`${rule.check.ref}\`)` : ""}${rule.check.description ? `. ${rule.check.description}` : ""}`);
|
|
179
|
+
if (rule.status === "assumption")
|
|
180
|
+
out.push("- Still an assumption: say so when you rely on it.");
|
|
181
|
+
if (out[out.length - 1] !== "")
|
|
182
|
+
out.push("");
|
|
183
|
+
if (rule.rationale)
|
|
184
|
+
out.push(rule.rationale, "");
|
|
185
|
+
return end(out);
|
|
186
|
+
}
|
|
187
|
+
/** A pattern; `level` as for `componentMarkdown`. */
|
|
188
|
+
export function patternMarkdown(pattern, level = 2) {
|
|
189
|
+
const out = [`${"#".repeat(level)} ${pattern.name}`, "", `Status: ${pattern.status ?? "not set"}`, ""];
|
|
190
|
+
if (pattern.description)
|
|
191
|
+
out.push(pattern.description, "");
|
|
192
|
+
if (pattern.components.length)
|
|
193
|
+
out.push(`Composes: ${pattern.components.join(", ")}`, "");
|
|
194
|
+
for (const ex of pattern.examples) {
|
|
195
|
+
if (ex.description)
|
|
196
|
+
out.push(ex.description);
|
|
197
|
+
out.push(`\`\`\`example ${ex.name}`, ex.code ?? `// ${ex.name}`, "```", "");
|
|
198
|
+
}
|
|
199
|
+
return end(out);
|
|
200
|
+
}
|
|
201
|
+
export function glossaryMarkdown(terms) {
|
|
202
|
+
const out = ["## Vocabulary", "", "| Say | Not | Kind | Note |", "| --- | --- | --- | --- |"];
|
|
203
|
+
for (const t of terms)
|
|
204
|
+
out.push(`| ${t.name} | ${cell(t.wrong?.join(", "))} | ${t.kind} | ${cell(t.note)} |`);
|
|
205
|
+
return end(out);
|
|
206
|
+
}
|
|
207
|
+
export function assetsMarkdown(assets) {
|
|
208
|
+
const out = ["## Brand assets", "", "| Asset | Kind | Minimum size | Clear space | Surfaces | Usage |", "| --- | --- | --- | --- | --- | --- |"];
|
|
209
|
+
for (const a of assets) {
|
|
210
|
+
out.push(`| ${a.name} (\`${a.file}\`) | ${a.kind} | ${cell(a.minSize)} | ${cell(a.clearSpace)} | ${cell(a.surfaces?.join(", "))} | ${cell(a.usage)} |`);
|
|
211
|
+
}
|
|
212
|
+
return end(out);
|
|
213
|
+
}
|
|
214
|
+
/** `.mxa/stack.json` baselines, or anything shaped like them: a table of name and target. */
|
|
215
|
+
export function standardsMarkdown(baselines) {
|
|
216
|
+
const out = ["## Standards", ""];
|
|
217
|
+
for (const section of standardsSections(baselines)) {
|
|
218
|
+
out.push(`### ${section.title}`, "");
|
|
219
|
+
for (const row of section.rows)
|
|
220
|
+
out.push(row.label ? `- ${row.label}: ${row.value}` : row.value);
|
|
221
|
+
if (section.rows.length)
|
|
222
|
+
out.push("");
|
|
223
|
+
for (const note of section.notes)
|
|
224
|
+
out.push(note, "");
|
|
225
|
+
}
|
|
226
|
+
return end(out);
|
|
227
|
+
}
|
|
228
|
+
/** The adherence report, as a page: totals, then routes. It informs; it never blocks. */
|
|
229
|
+
export function adherenceMarkdown(report) {
|
|
230
|
+
const pct = (v) => (v === null ? "n/a" : `${Math.round(v * 1000) / 10}%`);
|
|
231
|
+
const s = report.summary;
|
|
232
|
+
const out = [
|
|
233
|
+
"## Adherence",
|
|
234
|
+
"",
|
|
235
|
+
"A report, never a gate.",
|
|
236
|
+
"",
|
|
237
|
+
`- From the system: ${pct(s.share)} (${s.systemUses} system uses, ${s.rawElements} raw elements a component replaces)`,
|
|
238
|
+
`- Off-system values: ${s.offSystemValues} (${s.offSystem.palette} palette steps, ${s.offSystem.arbitrary} arbitrary values, ${s.offSystem["status-text"] ?? 0} status colours as text)`,
|
|
239
|
+
`- Overrides: ${s.overrides}`,
|
|
240
|
+
`- Components never used: ${s.componentsUnused.length ? s.componentsUnused.join(", ") : "none"}`,
|
|
241
|
+
"",
|
|
242
|
+
];
|
|
243
|
+
if (report.routes.length) {
|
|
244
|
+
out.push("| Route | From the system | Raw | Off-system | Overrides |", "| --- | --- | --- | --- | --- |");
|
|
245
|
+
for (const r of report.routes)
|
|
246
|
+
out.push(`| \`${r.route}\` | ${pct(r.counts.share)} | ${r.counts.rawElements} | ${r.counts.offSystemValues} | ${r.counts.overrides} |`);
|
|
247
|
+
}
|
|
248
|
+
return end(out);
|
|
249
|
+
}
|
|
250
|
+
// ------------------------------------------------------------ the whole system
|
|
251
|
+
/** Everything an agent needs, in one file: tokens first, then rules, patterns and components by category. */
|
|
252
|
+
export function systemMarkdown(input, title = "Design system", provenance) {
|
|
253
|
+
const out = [`# ${title}`, ""];
|
|
254
|
+
if (provenance)
|
|
255
|
+
out.push(generatedHeader(provenance).trimEnd(), "");
|
|
256
|
+
out.push(`Generated from \`${input.components.system}/manifest\`. Build screens from these components and tokens; do not restyle them with raw values. Carry each thing's status when you quote it.`, "");
|
|
257
|
+
if (input.tokens)
|
|
258
|
+
out.push(coloursMarkdown(input.tokens), typeMarkdown(input.tokens), scaleMarkdown(input.tokens));
|
|
259
|
+
if (input.rules?.length)
|
|
260
|
+
out.push("# Rules", "", ...input.rules.map(ruleMarkdown));
|
|
261
|
+
if (input.patterns?.length)
|
|
262
|
+
out.push("# Patterns", "", ...input.patterns.map((p) => patternMarkdown(p)));
|
|
263
|
+
for (const group of groupByCategory(input.components))
|
|
264
|
+
out.push(`# ${group.category}`, "", ...group.components.map((c) => componentMarkdown(c)));
|
|
265
|
+
if (input.terms?.length)
|
|
266
|
+
out.push(glossaryMarkdown(input.terms));
|
|
267
|
+
return end(out);
|
|
268
|
+
}
|
|
269
|
+
/** Links an agent outside the repo can follow to the exact data. */
|
|
270
|
+
export function manifestLinks(base) {
|
|
271
|
+
return end([
|
|
272
|
+
"## The manifest, as data",
|
|
273
|
+
"",
|
|
274
|
+
...["components", "tokens", "rules", "patterns", "taxonomy", "assets"].map((f) => `- [${f}.json](${base}/${f}.json)`),
|
|
275
|
+
]);
|
|
276
|
+
}
|
|
277
|
+
export { slugOf };
|
|
278
|
+
// ------------------------------------------------------------ helpers
|
|
279
|
+
function cell(value) {
|
|
280
|
+
if (value === undefined || value === null || value === "")
|
|
281
|
+
return "";
|
|
282
|
+
return String(value).replace(/\|/g, "\\|").replace(/\s*\n\s*/g, " ");
|
|
283
|
+
}
|
|
284
|
+
function end(lines) {
|
|
285
|
+
return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
286
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Point at anything in the preview to see its box: the element outlined with its
|
|
4
|
+
* size, its padding tinted inside it and its margin tinted outside, each side
|
|
5
|
+
* labelled where it is not zero. Drawn over `area`, measuring elements inside
|
|
6
|
+
* `target`. Colours are the docs' own: margin in warning, padding in success, the
|
|
7
|
+
* box in the focus ring.
|
|
8
|
+
*/
|
|
9
|
+
export declare function Measure({ area, target }: {
|
|
10
|
+
area: RefObject<HTMLElement | null>;
|
|
11
|
+
target: RefObject<HTMLElement | null>;
|
|
12
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
const px = (v) => Number.parseFloat(v) || 0;
|
|
5
|
+
const round = (n) => Math.round(n * 10) / 10;
|
|
6
|
+
/**
|
|
7
|
+
* Point at anything in the preview to see its box: the element outlined with its
|
|
8
|
+
* size, its padding tinted inside it and its margin tinted outside, each side
|
|
9
|
+
* labelled where it is not zero. Drawn over `area`, measuring elements inside
|
|
10
|
+
* `target`. Colours are the docs' own: margin in warning, padding in success, the
|
|
11
|
+
* box in the focus ring.
|
|
12
|
+
*/
|
|
13
|
+
export function Measure({ area, target }) {
|
|
14
|
+
const [box, setBox] = useState(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const host = area.current;
|
|
17
|
+
if (!host)
|
|
18
|
+
return;
|
|
19
|
+
const move = (e) => {
|
|
20
|
+
const inside = target.current;
|
|
21
|
+
const el = e.target instanceof HTMLElement || e.target instanceof SVGElement ? e.target : null;
|
|
22
|
+
if (!inside || !el || !inside.contains(el) || el === inside)
|
|
23
|
+
return setBox(null);
|
|
24
|
+
const r = el.getBoundingClientRect();
|
|
25
|
+
const origin = host.getBoundingClientRect();
|
|
26
|
+
const s = getComputedStyle(el);
|
|
27
|
+
setBox({
|
|
28
|
+
tag: el.tagName.toLowerCase(),
|
|
29
|
+
x: r.left - origin.left + host.scrollLeft,
|
|
30
|
+
y: r.top - origin.top + host.scrollTop,
|
|
31
|
+
w: r.width,
|
|
32
|
+
h: r.height,
|
|
33
|
+
padding: [px(s.paddingTop), px(s.paddingRight), px(s.paddingBottom), px(s.paddingLeft)],
|
|
34
|
+
margin: [px(s.marginTop), px(s.marginRight), px(s.marginBottom), px(s.marginLeft)],
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
const leave = () => setBox(null);
|
|
38
|
+
host.addEventListener("pointermove", move);
|
|
39
|
+
host.addEventListener("pointerleave", leave);
|
|
40
|
+
return () => {
|
|
41
|
+
host.removeEventListener("pointermove", move);
|
|
42
|
+
host.removeEventListener("pointerleave", leave);
|
|
43
|
+
};
|
|
44
|
+
}, [area, target]);
|
|
45
|
+
if (!box)
|
|
46
|
+
return null;
|
|
47
|
+
const [pt, pr, pb, pl] = box.padding;
|
|
48
|
+
const [mt, mr, mb, ml] = box.margin;
|
|
49
|
+
const tint = (token) => `color-mix(in oklch, var(${token}) 32%, transparent)`;
|
|
50
|
+
const label = "absolute rounded-sm bg-canvas-foreground px-1 font-mono text-[0.625rem] leading-4 text-canvas whitespace-nowrap";
|
|
51
|
+
return (_jsxs("div", { "aria-hidden": true, "data-slot": "measure", className: "pointer-events-none absolute inset-0 z-20", children: [_jsx("div", { className: "absolute border-solid", style: {
|
|
52
|
+
left: box.x - ml,
|
|
53
|
+
top: box.y - mt,
|
|
54
|
+
width: box.w + ml + mr,
|
|
55
|
+
height: box.h + mt + mb,
|
|
56
|
+
borderColor: tint("--warning"),
|
|
57
|
+
borderWidth: `${mt}px ${mr}px ${mb}px ${ml}px`,
|
|
58
|
+
} }), _jsx("div", { className: "absolute border-solid", style: { left: box.x, top: box.y, width: box.w, height: box.h, borderColor: tint("--success"), borderWidth: `${pt}px ${pr}px ${pb}px ${pl}px` } }), _jsx("div", { className: "absolute outline-1 outline-[var(--ring)] outline-solid", style: { left: box.x, top: box.y, width: box.w, height: box.h } }), _jsxs("span", { className: label, style: { left: box.x, top: Math.max(0, box.y - mt - 18) }, children: [box.tag, " \u00B7 ", round(box.w), " \u00D7 ", round(box.h)] }), pt > 0 && _jsx(Side, { className: label, x: box.x + box.w / 2, y: box.y + pt / 2, value: pt }), pb > 0 && _jsx(Side, { className: label, x: box.x + box.w / 2, y: box.y + box.h - pb / 2, value: pb }), pl > 0 && _jsx(Side, { className: label, x: box.x + pl / 2, y: box.y + box.h / 2, value: pl }), pr > 0 && _jsx(Side, { className: label, x: box.x + box.w - pr / 2, y: box.y + box.h / 2, value: pr })] }));
|
|
59
|
+
}
|
|
60
|
+
function Side({ className, x, y, value }) {
|
|
61
|
+
return (_jsx("span", { className: className, style: { left: x, top: y, transform: "translate(-50%, -50%)" }, children: round(value) }));
|
|
62
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
export type Pseudo = "hover" | "focus" | "active";
|
|
10
|
+
/** 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. */
|
|
11
|
+
export declare function installPseudoStates(): void;
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
const PATTERNS = [
|
|
10
|
+
["hover", /:hover(?![-\w])/g],
|
|
11
|
+
["focus", /:focus-visible(?![-\w])/g],
|
|
12
|
+
["focus", /:focus(?![-\w])/g],
|
|
13
|
+
["active", /:active(?![-\w])/g],
|
|
14
|
+
];
|
|
15
|
+
/** A state inside :not(), :has(), :is() or :where() changes what the selector means when removed, so those are left alone. */
|
|
16
|
+
const NESTED = /:(?:not|has|is|where)\([^)]*:(?:hover|focus|active)/;
|
|
17
|
+
const done = new WeakSet();
|
|
18
|
+
/** 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. */
|
|
19
|
+
export function installPseudoStates() {
|
|
20
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
21
|
+
if (done.has(sheet))
|
|
22
|
+
continue;
|
|
23
|
+
try {
|
|
24
|
+
walk(sheet);
|
|
25
|
+
done.add(sheet);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// a stylesheet from another origin cannot be read; its states stay unpinnable
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function walk(parent) {
|
|
33
|
+
const rules = parent.cssRules;
|
|
34
|
+
// backwards, so a twin inserted after its rule never shifts the rules still to visit
|
|
35
|
+
for (let i = rules.length - 1; i >= 0; i--) {
|
|
36
|
+
const rule = rules[i];
|
|
37
|
+
if (rule instanceof CSSStyleRule) {
|
|
38
|
+
const twin = twinOf(rule.selectorText);
|
|
39
|
+
if (twin) {
|
|
40
|
+
try {
|
|
41
|
+
parent.insertRule(`${twin} { ${rule.style.cssText} }`, i + 1);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// a selector the browser will not take back (a vendor pseudo-element): skip it
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (rule.cssRules?.length)
|
|
48
|
+
walk(rule);
|
|
49
|
+
}
|
|
50
|
+
else if ("cssRules" in rule) {
|
|
51
|
+
walk(rule);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function twinOf(selector) {
|
|
56
|
+
const out = [];
|
|
57
|
+
// split on commas that are not inside parentheses
|
|
58
|
+
for (const part of selector.split(/,(?![^(]*\))/)) {
|
|
59
|
+
if (NESTED.test(part))
|
|
60
|
+
continue;
|
|
61
|
+
for (const [state, pattern] of PATTERNS) {
|
|
62
|
+
pattern.lastIndex = 0;
|
|
63
|
+
if (!pattern.test(part))
|
|
64
|
+
continue;
|
|
65
|
+
const rest = part.replace(pattern, "").trim();
|
|
66
|
+
out.push(`:where([data-pseudo~="${state}"]) ${rest || "*"}`);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out.length ? out.join(", ") : null;
|
|
71
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ComponentType, type ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Everything the docs render that lives in the project: each component, examples
|
|
4
|
+
* module, pattern module and brand asset, keyed the way the manifest names them.
|
|
5
|
+
* The project's generated registry (`designtools-docs map`) imports them and
|
|
6
|
+
* renders a `RegistryProvider` around the docs; a server page cannot hand a
|
|
7
|
+
* component to a client block, so this is how the blocks reach them.
|
|
8
|
+
*/
|
|
9
|
+
export interface Registry {
|
|
10
|
+
components: Record<string, ComponentType<any>>;
|
|
11
|
+
examples: Record<string, Record<string, unknown>>;
|
|
12
|
+
patterns: Record<string, Record<string, unknown>>;
|
|
13
|
+
assets: Record<string, string | {
|
|
14
|
+
src: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export declare function RegistryProvider({ value, children }: {
|
|
18
|
+
value: Registry;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
export declare function useRegistry(): Registry;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
const EMPTY = { components: {}, examples: {}, patterns: {}, assets: {} };
|
|
5
|
+
const RegistryContext = createContext(EMPTY);
|
|
6
|
+
export function RegistryProvider({ value, children }) {
|
|
7
|
+
return _jsx(RegistryContext.Provider, { value: value, children: children });
|
|
8
|
+
}
|
|
9
|
+
export function useRegistry() {
|
|
10
|
+
return useContext(RegistryContext);
|
|
11
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
import type { ComponentsManifest, PatternEntry, RuleEntry, TermEntry } from "./manifest-types.js";
|
|
9
|
+
export type SearchKind = "Component" | "Rule" | "Pattern" | "Page";
|
|
10
|
+
export interface SearchItem {
|
|
11
|
+
kind: SearchKind;
|
|
12
|
+
label: string;
|
|
13
|
+
href: string;
|
|
14
|
+
/** The taxonomy's wrong forms for it: typing one finds it, and the result says so. */
|
|
15
|
+
wrong: string[];
|
|
16
|
+
/** Anything else it answers to quietly, such as a rule's id. */
|
|
17
|
+
also?: string[];
|
|
18
|
+
/** One short line under the label: a category, "part of Card", a rule's kind. */
|
|
19
|
+
detail?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface SearchGroup {
|
|
22
|
+
kind: SearchKind;
|
|
23
|
+
/** `matched` is the wrong form a result was found by ("Dropdown" for Select). */
|
|
24
|
+
items: (SearchItem & {
|
|
25
|
+
matched?: string;
|
|
26
|
+
})[];
|
|
27
|
+
}
|
|
28
|
+
export declare function searchIndex({ manifest, rules, patterns, terms, pages, basePath, }: {
|
|
29
|
+
manifest: ComponentsManifest;
|
|
30
|
+
rules?: RuleEntry[];
|
|
31
|
+
patterns?: PatternEntry[];
|
|
32
|
+
terms?: TermEntry[];
|
|
33
|
+
pages?: {
|
|
34
|
+
label: string;
|
|
35
|
+
href: string;
|
|
36
|
+
}[];
|
|
37
|
+
basePath?: string;
|
|
38
|
+
}): SearchItem[];
|
|
39
|
+
/**
|
|
40
|
+
* The matches for a query, grouped by kind (components, rules, patterns, pages),
|
|
41
|
+
* best first within each, at most `perGroup` of each. A match found only through
|
|
42
|
+
* an alias says which: `matched` is the wrong form typed ("Dropdown").
|
|
43
|
+
*/
|
|
44
|
+
export declare function search(items: SearchItem[], query: string, perGroup?: number): SearchGroup[];
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
import fuzzysort from "fuzzysort";
|
|
9
|
+
import { groupFamilies, slugOf } from "./manifest.js";
|
|
10
|
+
const ORDER = ["Component", "Rule", "Pattern", "Page"];
|
|
11
|
+
export function searchIndex({ manifest, rules = [], patterns = [], terms = [], pages = [], basePath = "/design/system", }) {
|
|
12
|
+
const wrong = new Map();
|
|
13
|
+
for (const t of terms)
|
|
14
|
+
if (t.wrong?.length)
|
|
15
|
+
wrong.set(t.name.toLowerCase(), t.wrong);
|
|
16
|
+
const wrongOf = (name) => wrong.get(name.toLowerCase()) ?? [];
|
|
17
|
+
const items = [];
|
|
18
|
+
for (const { category, families } of groupFamilies(manifest)) {
|
|
19
|
+
for (const { head, parts } of families) {
|
|
20
|
+
items.push({ kind: "Component", label: head.name, href: `${basePath}/components/${slugOf(head)}`, wrong: wrongOf(head.name), detail: category });
|
|
21
|
+
for (const p of parts) {
|
|
22
|
+
items.push({ kind: "Component", label: p.name, href: `${basePath}/components/${slugOf(p)}`, wrong: wrongOf(p.name), detail: `part of ${head.name}` });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (const r of rules) {
|
|
27
|
+
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" });
|
|
28
|
+
}
|
|
29
|
+
for (const p of patterns)
|
|
30
|
+
items.push({ kind: "Pattern", label: p.name, href: `${basePath}/patterns/${p.id}`, wrong: wrongOf(p.name) });
|
|
31
|
+
for (const p of pages)
|
|
32
|
+
items.push({ kind: "Page", label: p.label, href: p.href, wrong: [] });
|
|
33
|
+
return items;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The matches for a query, grouped by kind (components, rules, patterns, pages),
|
|
37
|
+
* best first within each, at most `perGroup` of each. A match found only through
|
|
38
|
+
* an alias says which: `matched` is the wrong form typed ("Dropdown").
|
|
39
|
+
*/
|
|
40
|
+
export function search(items, query, perGroup = 6) {
|
|
41
|
+
const q = query.trim();
|
|
42
|
+
if (!q)
|
|
43
|
+
return [];
|
|
44
|
+
const results = fuzzysort.go(q, items, { keys: ["label", (item) => item.wrong.join(" / "), (item) => (item.also ?? []).join(" / ")], limit: 0, threshold: 0.3 });
|
|
45
|
+
const ranked = [...results].sort((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));
|
|
46
|
+
return ORDER.map((kind) => ({
|
|
47
|
+
kind,
|
|
48
|
+
items: ranked
|
|
49
|
+
.filter((r) => r.obj.kind === kind)
|
|
50
|
+
.slice(0, perGroup)
|
|
51
|
+
.map((r) => {
|
|
52
|
+
const viaWrong = (r[1]?.score ?? 0) > Math.max(r[0]?.score ?? 0, r[2]?.score ?? 0);
|
|
53
|
+
// the wrong form that matched best, to say why a result is here ("Dropdown" for Select)
|
|
54
|
+
const alias = viaWrong
|
|
55
|
+
? r.obj.wrong
|
|
56
|
+
.map((a) => ({ a, score: fuzzysort.single(q, a)?.score ?? 0 }))
|
|
57
|
+
.sort((x, y) => y.score - x.score)[0]?.a
|
|
58
|
+
: undefined;
|
|
59
|
+
return alias ? { ...r.obj, matched: alias } : r.obj;
|
|
60
|
+
}),
|
|
61
|
+
})).filter((g) => g.items.length > 0);
|
|
62
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
export interface StandardsSection {
|
|
7
|
+
key: string;
|
|
8
|
+
title: string;
|
|
9
|
+
rows: {
|
|
10
|
+
label: string;
|
|
11
|
+
value: string;
|
|
12
|
+
}[];
|
|
13
|
+
notes: string[];
|
|
14
|
+
}
|
|
15
|
+
/** `textSize` to "Text size", `aiCrawlable` to "Ai crawlable": a fallback, never as good as a title. */
|
|
16
|
+
export declare function humanise(key: string): string;
|
|
17
|
+
export declare function standardsSections(baselines: Record<string, unknown>): StandardsSection[];
|