@runbooks/design 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/color.d.ts +17 -0
- package/dist/color.js +24 -0
- package/dist/geometry.test.d.ts +1 -0
- package/dist/geometry.test.js +155 -0
- package/dist/icons.d.ts +69 -0
- package/dist/icons.js +103 -0
- package/dist/icons.test.d.ts +1 -0
- package/dist/icons.test.js +140 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/mark.d.ts +42 -0
- package/dist/mark.js +102 -0
- package/dist/primitives.d.ts +75 -0
- package/dist/primitives.js +126 -0
- package/dist/primitives.test.d.ts +1 -0
- package/dist/primitives.test.js +134 -0
- package/dist/render.d.ts +97 -0
- package/dist/render.js +1085 -0
- package/dist/render.test.d.ts +1 -0
- package/dist/render.test.js +179 -0
- package/dist/specimen.d.ts +2 -0
- package/dist/specimen.gen.d.ts +1 -0
- package/dist/specimen.gen.js +9 -0
- package/dist/specimen.js +81 -0
- package/dist/stylesheet.d.ts +95 -0
- package/dist/stylesheet.js +987 -0
- package/dist/stylesheet.test.d.ts +1 -0
- package/dist/stylesheet.test.js +265 -0
- package/dist/text.d.ts +28 -0
- package/dist/text.js +89 -0
- package/dist/tokens.d.ts +104 -0
- package/dist/tokens.js +142 -0
- package/dist/tokens.test.d.ts +1 -0
- package/dist/tokens.test.js +125 -0
- package/fonts/IBMPlexMono-Regular-Latin1.woff2 +0 -0
- package/fonts/IBMPlexMono-SemiBold-Latin1.woff2 +0 -0
- package/fonts/IBMPlexSans-Italic-Latin1.woff2 +0 -0
- package/fonts/IBMPlexSans-Medium-Latin1.woff2 +0 -0
- package/fonts/IBMPlexSans-Regular-Latin1.woff2 +0 -0
- package/fonts/IBMPlexSans-SemiBold-Latin1.woff2 +0 -0
- package/fonts/LICENSE.txt +93 -0
- package/package.json +40 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join, dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { toGraph, topologicalOrder } from "@runbooks/graph";
|
|
6
|
+
import { PALETTES } from "./tokens.js";
|
|
7
|
+
import { renderGraph } from "./render.js";
|
|
8
|
+
const CORPUS = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "schema", "fixtures", "p1-valid");
|
|
9
|
+
const corpus = readdirSync(CORPUS).map((name) => ({
|
|
10
|
+
name,
|
|
11
|
+
graph: toGraph(JSON.parse(readFileSync(join(CORPUS, name), "utf8"))),
|
|
12
|
+
}));
|
|
13
|
+
const THEMES = ["dark", "light"];
|
|
14
|
+
/**
|
|
15
|
+
* §18.4 puts the graph above the fold. A graph that appears only after JavaScript loads
|
|
16
|
+
* is invisible to a crawler and to a reader on a slow connection during an incident,
|
|
17
|
+
* which is exactly when this page is opened.
|
|
18
|
+
*/
|
|
19
|
+
describe("the graph renders without JavaScript", () => {
|
|
20
|
+
it.each(corpus)("$name produces complete markup from a pure call", ({ graph }) => {
|
|
21
|
+
const { svg, list } = renderGraph(graph, { theme: "light" });
|
|
22
|
+
expect(svg).toMatch(/^<svg /);
|
|
23
|
+
expect(svg).toContain("</svg>");
|
|
24
|
+
for (const node of graph.nodes) {
|
|
25
|
+
expect(svg, `${node.id} is missing from the picture`).toContain(`data-node="${node.id}"`);
|
|
26
|
+
expect(list, `${node.id} is missing from the list`).toContain(`step-${node.id}`);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
it.each(corpus)("$name renders identically twice", ({ graph }) => {
|
|
30
|
+
expect(renderGraph(graph, { theme: "dark" })).toEqual(renderGraph(graph, { theme: "dark" }));
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* The list is not a fallback, it is the accessible representation: the SVG is
|
|
35
|
+
* aria-hidden and this is what a screen reader reads. A procedure that cannot be read
|
|
36
|
+
* linearly is inaccessible.
|
|
37
|
+
*/
|
|
38
|
+
describe("the graph is also a structured list", () => {
|
|
39
|
+
it("hides the picture from assistive technology", () => {
|
|
40
|
+
const { svg } = renderGraph(corpus[0].graph, { theme: "light" });
|
|
41
|
+
expect(svg).toContain('aria-hidden="true"');
|
|
42
|
+
expect(svg).toContain('focusable="false"');
|
|
43
|
+
});
|
|
44
|
+
it.each(corpus)("$name announces kind, risk and where each step goes", ({ graph }) => {
|
|
45
|
+
const { list } = renderGraph(graph, { theme: "light" });
|
|
46
|
+
for (const node of graph.nodes) {
|
|
47
|
+
const entry = list.split("<li ").find((chunk) => chunk.includes(`step-${node.id}"`));
|
|
48
|
+
expect(entry).toContain(node.kind);
|
|
49
|
+
if (node.risk)
|
|
50
|
+
expect(entry).toContain(node.risk);
|
|
51
|
+
expect(entry).toMatch(/Goes |Terminal/);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
it("names an approval gate in the list, not only as a mark", () => {
|
|
55
|
+
const graph = toGraph({
|
|
56
|
+
runbook: {
|
|
57
|
+
steps: [
|
|
58
|
+
{ id: "s1", kind: "action", title: "Drop it", risk: "destructive", requires_approval: true, next: "end:success" },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
expect(renderGraph(graph, { theme: "light" }).list).toContain("requires approval");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe("keyboard traversal reaches every node, in topological order", () => {
|
|
66
|
+
it.each(corpus)("$name is focusable in order", ({ graph }) => {
|
|
67
|
+
const { svg } = renderGraph(graph, { theme: "light" });
|
|
68
|
+
const focusable = [...svg.matchAll(/data-node="([^"]+)" data-order="(\d+)"/g)];
|
|
69
|
+
expect(focusable).toHaveLength(graph.nodes.length);
|
|
70
|
+
const ordered = focusable
|
|
71
|
+
.sort((a, b) => Number(a[2]) - Number(b[2]))
|
|
72
|
+
.map((m) => m[1]);
|
|
73
|
+
expect(ordered).toEqual(topologicalOrder(graph));
|
|
74
|
+
expect(svg.match(/tabindex="0"/g)).toHaveLength(graph.nodes.length);
|
|
75
|
+
});
|
|
76
|
+
// A mini-graph in a card is decoration beside a link; making 7 nodes focusable inside
|
|
77
|
+
// every card would flood the tab order of the catalog.
|
|
78
|
+
it("takes the mini-graph out of the tab order", () => {
|
|
79
|
+
const { svg } = renderGraph(corpus[0].graph, { theme: "light", mode: "mini" });
|
|
80
|
+
expect(svg).not.toContain('tabindex="0"');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe("mini mode stays legible at card size", () => {
|
|
84
|
+
it.each(corpus)("$name fits a card without dropping a node", ({ graph }) => {
|
|
85
|
+
const mini = renderGraph(graph, { theme: "light", mode: "mini" });
|
|
86
|
+
expect(mini.width).toBeLessThanOrEqual(320);
|
|
87
|
+
expect(mini.height).toBeLessThanOrEqual(200);
|
|
88
|
+
for (const node of graph.nodes)
|
|
89
|
+
expect(mini.svg).toContain(`data-node="${node.id}"`);
|
|
90
|
+
});
|
|
91
|
+
it("drops labels rather than shrinking them past reading", () => {
|
|
92
|
+
const mini = renderGraph(corpus[0].graph, { theme: "light", mode: "mini" });
|
|
93
|
+
expect(mini.svg).not.toContain("<text");
|
|
94
|
+
});
|
|
95
|
+
// The silhouette's job: show whether there is red in the procedure.
|
|
96
|
+
it("keeps risk colour in the silhouette", () => {
|
|
97
|
+
const graph = toGraph({
|
|
98
|
+
runbook: {
|
|
99
|
+
steps: [{ id: "s1", kind: "action", title: "Drop it", risk: "destructive", requires_approval: true, next: "end:success" }],
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
const mini = renderGraph(graph, { theme: "light", mode: "mini" });
|
|
103
|
+
expect(mini.svg).toContain("#96452F");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe("motion and print", () => {
|
|
107
|
+
it("disables animation when the reader asks for none", () => {
|
|
108
|
+
expect(renderGraph(corpus[0].graph, { theme: "light" }).svg)
|
|
109
|
+
.toContain("prefers-reduced-motion: reduce");
|
|
110
|
+
});
|
|
111
|
+
it("carries a hatch per risk for print", () => {
|
|
112
|
+
const { svg } = renderGraph(corpus[0].graph, { theme: "light" });
|
|
113
|
+
expect(svg).toMatch(/data-hatch="[a-z-]+"/);
|
|
114
|
+
expect(svg).toContain("@media print");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe("two graphs on one page do not collide", () => {
|
|
118
|
+
it("namespaces marker ids", () => {
|
|
119
|
+
const a = renderGraph(corpus[0].graph, { theme: "light", idPrefix: "a" });
|
|
120
|
+
const b = renderGraph(corpus[0].graph, { theme: "light", idPrefix: "b" });
|
|
121
|
+
expect(a.svg).toContain('id="a-arrow"');
|
|
122
|
+
expect(b.svg).toContain('id="b-arrow"');
|
|
123
|
+
expect(a.svg).not.toContain("b-arrow");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe("both themes render", () => {
|
|
127
|
+
it.each(THEMES)("%s uses its own palette", (theme) => {
|
|
128
|
+
const { svg } = renderGraph(corpus[0].graph, { theme });
|
|
129
|
+
// Read from the palette rather than written again here: a hex in a test is a second
|
|
130
|
+
// copy of a decision, and the copy is the one that goes stale.
|
|
131
|
+
expect(svg).toContain(PALETTES[theme].background);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
/**
|
|
135
|
+
* Risk survives the loss of colour (§18.2, D-02's hatching variants).
|
|
136
|
+
*
|
|
137
|
+
* Hue is risk's channel and paper does not have one. `RISK_HATCH` named a pattern per
|
|
138
|
+
* level and `data-hatch` carried the name for as long as the renderer has existed — and
|
|
139
|
+
* nothing painted one, while the print rule set `fill:none` on exactly those nodes. A
|
|
140
|
+
* printed procedure showed a destructive step and a read-only step as the same grey
|
|
141
|
+
* outline: the substitute was declared, and the thing it substitutes for was removed.
|
|
142
|
+
*/
|
|
143
|
+
describe("printing keeps the one property a reader must not miss", () => {
|
|
144
|
+
const drawn = (idPrefix) => renderGraph(toGraph({
|
|
145
|
+
runbook: {
|
|
146
|
+
steps: [
|
|
147
|
+
{ id: "s1", kind: "check", title: "Look", risk: "read-only", expect: "ok", on_fail: "s9", next: "s2" },
|
|
148
|
+
{ id: "s2", kind: "action", title: "Change it", risk: "reversible-write", next: "s3" },
|
|
149
|
+
{ id: "s3", kind: "action", title: "Drop it", risk: "destructive", requires_approval: true, next: "s4" },
|
|
150
|
+
{ id: "s4", kind: "action", title: "Wipe it", risk: "irreversible", requires_approval: true, next: "end:success" },
|
|
151
|
+
{ id: "s9", kind: "escalate", title: "Escalate", to: "team:x" },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
}), { theme: "light", idPrefix }).svg;
|
|
155
|
+
const svg = drawn("g1");
|
|
156
|
+
it("marks every risk with the hatch the design package names for it", () => {
|
|
157
|
+
for (const hatch of ["none", "diagonal-thin", "diagonal-dense", "cross"]) {
|
|
158
|
+
expect(svg, `no node carries ${hatch}`).toContain(`data-hatch="${hatch}"`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
it.each(["diagonal-thin", "diagonal-dense", "cross"])("defines the %s pattern", (hatch) => {
|
|
162
|
+
expect(svg).toContain(`<pattern id="g1-hatch-${hatch}"`);
|
|
163
|
+
});
|
|
164
|
+
it.each(["diagonal-thin", "diagonal-dense", "cross"])("fills %s with it on paper", (hatch) => {
|
|
165
|
+
const print = /@media print\{(.*?)\}\}/s.exec(svg)?.[1] ?? "";
|
|
166
|
+
expect(print, "the print rule paints no pattern for this level").toContain(`[data-hatch="${hatch}"]{fill:url(#g1-hatch-${hatch})`);
|
|
167
|
+
});
|
|
168
|
+
it("leaves read-only unfilled, because its hatch is the absence of one", () => {
|
|
169
|
+
expect(svg).toMatch(/@media print\{\.runbook-graph \[data-hatch="none"\]\{fill:none\}/);
|
|
170
|
+
});
|
|
171
|
+
/*
|
|
172
|
+
* A catalog page holds twenty graphs. Bare ids would make the twentieth borrow the
|
|
173
|
+
* first one's fill, which is the defect that makes a shared `<defs>` worth checking.
|
|
174
|
+
*/
|
|
175
|
+
it("names the patterns per graph, so two on a page do not collide", () => {
|
|
176
|
+
expect(drawn("g2")).toContain('<pattern id="g2-hatch-cross"');
|
|
177
|
+
expect(drawn("g2")).not.toContain("g1-hatch-");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Regenerate the checked-in baselines: `pnpm --filter @runbooks/design exec tsx src/specimen.gen.ts` */
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { specimenSheet } from "./specimen.js";
|
|
6
|
+
const out = join(dirname(fileURLToPath(import.meta.url)), "..", "specimen");
|
|
7
|
+
for (const theme of ["dark", "light"]) {
|
|
8
|
+
writeFileSync(join(out, `${theme}.svg`), specimenSheet(theme), "utf8");
|
|
9
|
+
}
|
package/dist/specimen.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { RISK_ORDER } from "@runbooks/schema";
|
|
2
|
+
import { PALETTES, riskColor, RISK_LABELS, FONT_STACKS } from "./tokens.js";
|
|
3
|
+
import { nodeShape, edgeStyle, stateStyle, badge } from "./primitives.js";
|
|
4
|
+
const KINDS = ["start", "check", "action", "decision", "wait", "escalate", "end"];
|
|
5
|
+
const EDGES = ["next", "branch", "on_fail", "rollback", "approval", "retry"];
|
|
6
|
+
const STATES = ["not-reached", "current", "passed"];
|
|
7
|
+
const BADGES = ["approval", "untrusted", "capability-mcp", "capability-cli", "capability-iam"];
|
|
8
|
+
const W = 132;
|
|
9
|
+
const H = 34;
|
|
10
|
+
export function specimenSheet(theme) {
|
|
11
|
+
const p = PALETTES[theme];
|
|
12
|
+
const rows = [];
|
|
13
|
+
let y = 24;
|
|
14
|
+
rows.push(label(16, y, "kinds", p.textSecondary));
|
|
15
|
+
y += 12;
|
|
16
|
+
KINDS.forEach((kind, i) => {
|
|
17
|
+
const shape = nodeShape(kind, { width: W, height: H });
|
|
18
|
+
const x = 16 + (i % 4) * (W + 16);
|
|
19
|
+
const row = y + Math.floor(i / 4) * (H + 16);
|
|
20
|
+
rows.push(`<g transform="translate(${x} ${row})">` +
|
|
21
|
+
`<path d="${shape.path}" fill="${p.surface}" stroke="${p.text}" stroke-width="1.5"` +
|
|
22
|
+
`${shape.outlineDashed ? ' stroke-dasharray="5 3"' : ""}/>` +
|
|
23
|
+
(shape.detail ? `<path d="${shape.detail}" fill="${p.text}"/>` : "") +
|
|
24
|
+
`<text x="${W / 2}" y="${H / 2 + 4}" fill="${p.text}" font-size="12" text-anchor="middle" font-family='${FONT_STACKS.sans}'>${kind}</text>` +
|
|
25
|
+
`</g>`);
|
|
26
|
+
});
|
|
27
|
+
y += 2 * (H + 16) + 16;
|
|
28
|
+
rows.push(label(16, y, "risk", p.textSecondary));
|
|
29
|
+
y += 12;
|
|
30
|
+
RISK_ORDER.forEach((risk, i) => {
|
|
31
|
+
const x = 16 + i * (W + 16);
|
|
32
|
+
const shape = nodeShape("action", { width: W, height: H });
|
|
33
|
+
rows.push(`<g transform="translate(${x} ${y})">` +
|
|
34
|
+
`<path d="${shape.path}" fill="${p.surface}" stroke="${riskColor(risk, theme)}" stroke-width="2"/>` +
|
|
35
|
+
`<text x="${W / 2}" y="${H / 2 + 4}" fill="${p.text}" font-size="12" text-anchor="middle" font-family='${FONT_STACKS.sans}'>${RISK_LABELS[risk]}</text>` +
|
|
36
|
+
`</g>`);
|
|
37
|
+
});
|
|
38
|
+
y += H + 32;
|
|
39
|
+
rows.push(label(16, y, "state (weight and fill only, never hue)", p.textSecondary));
|
|
40
|
+
y += 12;
|
|
41
|
+
STATES.forEach((state, i) => {
|
|
42
|
+
const s = stateStyle(state);
|
|
43
|
+
const shape = nodeShape("action", { width: W, height: H });
|
|
44
|
+
rows.push(`<g transform="translate(${16 + i * (W + 16)} ${y})">` +
|
|
45
|
+
`<path d="${shape.path}" fill="${p.text}" fill-opacity="${s.fillOpacity}" stroke="${p.text}" stroke-width="${s.strokeWidth}"/>` +
|
|
46
|
+
`<text x="${W / 2}" y="${H / 2 + 4}" fill="${p.text}" font-size="11" text-anchor="middle" font-family='${FONT_STACKS.sans}'>${state}</text>` +
|
|
47
|
+
`</g>`);
|
|
48
|
+
});
|
|
49
|
+
y += H + 32;
|
|
50
|
+
rows.push(label(16, y, "edges", p.textSecondary));
|
|
51
|
+
y += 20;
|
|
52
|
+
EDGES.forEach((kind, i) => {
|
|
53
|
+
const e = edgeStyle(kind);
|
|
54
|
+
const row = y + i * 22;
|
|
55
|
+
const x1 = 96;
|
|
56
|
+
const x2 = x1 + 160;
|
|
57
|
+
const line = e.arc
|
|
58
|
+
? `<path d="M ${x1} ${row} q 80 -26 ${x2 - x1} 0" fill="none" stroke="${p.text}" stroke-width="${e.width}"/>`
|
|
59
|
+
: `<line x1="${e.reversed ? x2 : x1}" y1="${row}" x2="${e.reversed ? x1 : x2}" y2="${row}" stroke="${p.text}" stroke-width="${e.width}"${e.dash ? ` stroke-dasharray="${e.dash}"` : ""}/>`;
|
|
60
|
+
const second = e.doubled
|
|
61
|
+
? `<line x1="${x1}" y1="${row + 4}" x2="${x2}" y2="${row + 4}" stroke="${p.text}" stroke-width="${e.width}"/>`
|
|
62
|
+
: "";
|
|
63
|
+
rows.push(`<text x="16" y="${row + 4}" fill="${p.textSecondary}" font-size="12" font-family='${FONT_STACKS.mono}'>${kind}</text>${line}${second}`);
|
|
64
|
+
});
|
|
65
|
+
y += EDGES.length * 22 + 24;
|
|
66
|
+
rows.push(label(16, y, "badges", p.textSecondary));
|
|
67
|
+
y += 12;
|
|
68
|
+
BADGES.forEach((kind, i) => {
|
|
69
|
+
rows.push(`<g transform="translate(${16 + i * 48} ${y})">` +
|
|
70
|
+
`<path d="${badge(kind)}" fill="none" stroke="${p.text}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>` +
|
|
71
|
+
`</g>`);
|
|
72
|
+
});
|
|
73
|
+
y += 40;
|
|
74
|
+
return (`<svg xmlns="http://www.w3.org/2000/svg" width="640" height="${y}" viewBox="0 0 640 ${y}">` +
|
|
75
|
+
`<rect width="640" height="${y}" fill="${p.background}"/>` +
|
|
76
|
+
rows.join("") +
|
|
77
|
+
`</svg>\n`);
|
|
78
|
+
}
|
|
79
|
+
function label(x, y, text, fill) {
|
|
80
|
+
return `<text x="${x}" y="${y}" fill="${fill}" font-size="11" font-family='${FONT_STACKS.mono}'>${text}</text>`;
|
|
81
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tokens, as the stylesheet the pages actually get.
|
|
3
|
+
*
|
|
4
|
+
* D-01 produced `PALETTES`, `TYPE_SCALE`, `FONT_STACKS` and `NUMERIC_FEATURES`, and its
|
|
5
|
+
* contrast tests measured them against each other. Nothing consumed them. The site
|
|
6
|
+
* shipped with no stylesheet at all — default serif, no measure, no tabular figures — so
|
|
7
|
+
* every token in this package was a value the product had agreed on and never applied,
|
|
8
|
+
* and the tests passed because they were checking the agreement rather than the page.
|
|
9
|
+
*
|
|
10
|
+
* Emitted rather than written beside the tokens, for the reason the rest of this
|
|
11
|
+
* repository keeps arriving at: a hand-kept CSS file with `#141A1F` in it is a second
|
|
12
|
+
* copy of a decision, and the copy is the one that goes stale. Every colour, size and
|
|
13
|
+
* stack below is read from the token objects; a test asserts the output contains no
|
|
14
|
+
* literal that did not come from one.
|
|
15
|
+
*
|
|
16
|
+
* §18.2's prohibitions are kept the way that file keeps them — by having nothing to
|
|
17
|
+
* reach for. There is no shadow token, no gradient except the risk legend's, and no
|
|
18
|
+
* uppercase transform, so none of the three can be spelled here.
|
|
19
|
+
*/
|
|
20
|
+
import { type Theme } from "./tokens.js";
|
|
21
|
+
/** Where the self-hosted faces are served from. */
|
|
22
|
+
export declare const FONT_DIR = "/fonts";
|
|
23
|
+
/**
|
|
24
|
+
* Where they are kept, relative to this package.
|
|
25
|
+
*
|
|
26
|
+
* Source, not build output: `apps/web/public` is emitted in its entirety and gitignored
|
|
27
|
+
* as such, so a font left there would vanish on a fresh clone and the site would render
|
|
28
|
+
* in the fallback stack with nothing failing. The package that declares the faces owns
|
|
29
|
+
* the files, and the catalog build copies them out with the rest of what it serves.
|
|
30
|
+
*/
|
|
31
|
+
export declare const FONT_SOURCE_DIR = "fonts";
|
|
32
|
+
/**
|
|
33
|
+
* The faces, and only the ones the type scale asks for.
|
|
34
|
+
*
|
|
35
|
+
* 400 and 500 are the scale's two weights; 600 is what `strong` needs and italic is what
|
|
36
|
+
* `em` needs, and synthesising either from a regular face is how a careful type choice
|
|
37
|
+
* ends up looking careless. Six files, Latin1 subsets, ~124 kB in total.
|
|
38
|
+
*/
|
|
39
|
+
export declare const FACES: readonly [{
|
|
40
|
+
readonly family: "IBM Plex Sans";
|
|
41
|
+
readonly file: "IBMPlexSans-Regular-Latin1.woff2";
|
|
42
|
+
readonly weight: 400;
|
|
43
|
+
readonly style: "normal";
|
|
44
|
+
}, {
|
|
45
|
+
readonly family: "IBM Plex Sans";
|
|
46
|
+
readonly file: "IBMPlexSans-Italic-Latin1.woff2";
|
|
47
|
+
readonly weight: 400;
|
|
48
|
+
readonly style: "italic";
|
|
49
|
+
}, {
|
|
50
|
+
readonly family: "IBM Plex Sans";
|
|
51
|
+
readonly file: "IBMPlexSans-Medium-Latin1.woff2";
|
|
52
|
+
readonly weight: 500;
|
|
53
|
+
readonly style: "normal";
|
|
54
|
+
}, {
|
|
55
|
+
readonly family: "IBM Plex Sans";
|
|
56
|
+
readonly file: "IBMPlexSans-SemiBold-Latin1.woff2";
|
|
57
|
+
readonly weight: 600;
|
|
58
|
+
readonly style: "normal";
|
|
59
|
+
}, {
|
|
60
|
+
readonly family: "IBM Plex Mono";
|
|
61
|
+
readonly file: "IBMPlexMono-Regular-Latin1.woff2";
|
|
62
|
+
readonly weight: 400;
|
|
63
|
+
readonly style: "normal";
|
|
64
|
+
}, {
|
|
65
|
+
readonly family: "IBM Plex Mono";
|
|
66
|
+
readonly file: "IBMPlexMono-SemiBold-Latin1.woff2";
|
|
67
|
+
readonly weight: 600;
|
|
68
|
+
readonly style: "normal";
|
|
69
|
+
}];
|
|
70
|
+
/**
|
|
71
|
+
* Upstream's own range for the Latin1 split, copied rather than narrowed.
|
|
72
|
+
*
|
|
73
|
+
* It carries U+2013-2014 and U+2018-201E and U+2026 — the dashes, the curly quotes and
|
|
74
|
+
* the ellipsis this catalog's prose is full of. A range invented here would be a
|
|
75
|
+
* subsetting decision made twice, and the half that is wrong shows up as one character in
|
|
76
|
+
* a different typeface in the middle of a sentence.
|
|
77
|
+
*/
|
|
78
|
+
export declare const LATIN1_RANGE: string;
|
|
79
|
+
export interface StylesheetOptions {
|
|
80
|
+
/**
|
|
81
|
+
* Which theme the surface defaults to (§18.2, Q10). A viewer's explicit choice still
|
|
82
|
+
* wins: the default is a `prefers-color-scheme` question, not a lock, and
|
|
83
|
+
* `[data-theme]` overrides both.
|
|
84
|
+
*/
|
|
85
|
+
readonly defaultTheme: Theme;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The whole stylesheet.
|
|
89
|
+
*
|
|
90
|
+
* Deliberately one string rather than a file Next imports. A generated file on disk is a
|
|
91
|
+
* file somebody edits, and then the tokens and the page disagree again with nothing to
|
|
92
|
+
* catch it. Inlined in the document head it also costs no second request and cannot
|
|
93
|
+
* flash unstyled, which on a static catalog of this size is the better trade anyway.
|
|
94
|
+
*/
|
|
95
|
+
export declare function stylesheet(options: StylesheetOptions): string;
|