@aitrien/motifspec 1.13.0 → 1.13.2
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 +269 -257
- package/LICENSE +21 -21
- package/README.md +150 -150
- package/dist/backdrop.html +1 -1
- package/dist/chroma.css +36 -36
- package/dist/codex.css +1145 -1145
- package/dist/codex.js +126 -126
- package/dist/contrast.md +101 -101
- package/dist/fonts.css +3 -3
- package/dist/icons.svg +18 -18
- package/dist/plate.html +1 -1
- package/dist/plate.js +292 -292
- package/dist/theme-init.js +1 -1
- package/dist/tokens.css +466 -466
- package/dist/tokens.js +3330 -3330
- package/dist/tokens.json +3330 -3330
- package/docs/components/_template.md +37 -37
- package/docs/components/accession-label.md +106 -106
- package/docs/components/backdrop.md +83 -83
- package/docs/components/button-and-link.md +87 -87
- package/docs/components/callout.md +75 -75
- package/docs/components/code-block.md +109 -109
- package/docs/components/data-table.md +73 -73
- package/docs/components/figure.md +68 -68
- package/docs/components/heading-anchor.md +76 -76
- package/docs/components/icon.md +63 -63
- package/docs/components/ornament.md +77 -77
- package/docs/components/pagination.md +78 -78
- package/docs/components/post-list-item.md +91 -91
- package/docs/components/project-card.md +82 -82
- package/docs/components/prose.md +122 -122
- package/docs/components/site-footer.md +99 -99
- package/docs/components/site-header.md +133 -133
- package/docs/components/specimen-plate.md +92 -92
- package/docs/components/table-of-contents.md +83 -83
- package/docs/components/tag.md +70 -70
- package/docs/components/tooltip.md +67 -67
- package/docs/hugo.md +120 -120
- package/docs/syntax-highlighting.md +55 -55
- package/docs/templates.md +30 -30
- package/fonts/OFL-jetbrains-mono.txt +93 -93
- package/fonts/OFL-national-park.txt +93 -93
- package/fonts/OFL-yarndings-12.txt +93 -93
- package/icons/LICENSE +202 -202
- package/icons/manifest.json +75 -75
- package/layouts/_markup/render-blockquote.html +30 -30
- package/layouts/_markup/render-codeblock.html +18 -18
- package/layouts/_markup/render-heading.html +2 -2
- package/layouts/_markup/render-image.html +14 -14
- package/layouts/_markup/render-link.html +14 -14
- package/layouts/_markup/render-table.html +31 -31
- package/layouts/_partials/motifspec/accession/id.html +2 -2
- package/layouts/_partials/motifspec/accession/label.html +18 -18
- package/layouts/_partials/motifspec/accession/status.html +2 -2
- package/layouts/_partials/motifspec/article.html +24 -24
- package/layouts/_partials/motifspec/back-link.html +2 -2
- package/layouts/_partials/motifspec/body-end.html +5 -5
- package/layouts/_partials/motifspec/body-start.html +5 -5
- package/layouts/_partials/motifspec/figure.html +14 -14
- package/layouts/_partials/motifspec/filed-under.html +5 -5
- package/layouts/_partials/motifspec/h2s.html +7 -7
- package/layouts/_partials/motifspec/head.html +15 -15
- package/layouts/_partials/motifspec/pagination-entries.html +34 -34
- package/layouts/_partials/motifspec/pagination-pages.html +30 -30
- package/layouts/_partials/motifspec/post-list-item.html +15 -15
- package/layouts/_partials/motifspec/project-card.html +23 -23
- package/layouts/_partials/motifspec/section-heading.html +3 -3
- package/layouts/_partials/motifspec/site-header.html +27 -27
- package/layouts/_partials/motifspec/table-of-contents.html +21 -21
- package/layouts/_partials/motifspec/tag-filter.html +22 -22
- package/layouts/_partials/motifspec/tags.html +8 -8
- package/layouts/_partials/motifspec/tokens.html +9 -9
- package/package.json +68 -68
- package/scripts/check.mjs +153 -153
- package/scripts/lib.mjs +176 -176
- package/tokens/README.md +3 -3
- package/tokens/border.json +63 -63
- package/tokens/color.json +1902 -1902
- package/tokens/motion.json +207 -207
- package/tokens/radius.json +53 -53
- package/tokens/size.json +378 -378
- package/tokens/space.json +289 -289
- package/tokens/typography.json +859 -859
package/scripts/lib.mjs
CHANGED
|
@@ -1,176 +1,176 @@
|
|
|
1
|
-
// Shared token tooling for MotifSpec: load, flatten, resolve, validate, contrast.
|
|
2
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
|
-
import { join, dirname } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
|
|
6
|
-
export const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
-
export const PREFIX = 'cx';
|
|
8
|
-
|
|
9
|
-
/** Category prefix each token file may define (CONVENTIONS.md). */
|
|
10
|
-
export const FILE_CATEGORY = {
|
|
11
|
-
'color.json': 'color',
|
|
12
|
-
'typography.json': 'font',
|
|
13
|
-
'space.json': 'space',
|
|
14
|
-
'size.json': 'size',
|
|
15
|
-
'radius.json': 'radius',
|
|
16
|
-
'border.json': 'border',
|
|
17
|
-
'motion.json': 'motion',
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const SEGMENT = /^(?:[a-z][a-z0-9]*(?:-[a-z0-9]+)*|[0-9]+)$/;
|
|
21
|
-
const MAX_DEPTH = 4;
|
|
22
|
-
const ALIAS = /^\{([^{}]+)\}$/;
|
|
23
|
-
|
|
24
|
-
/** Load every token file and flatten into a Map(path -> token). */
|
|
25
|
-
export function loadTokens(dir = join(ROOT, 'tokens')) {
|
|
26
|
-
const tokens = new Map();
|
|
27
|
-
const errors = [];
|
|
28
|
-
for (const file of readdirSync(dir).filter((f) => f.endsWith('.json')).sort()) {
|
|
29
|
-
const json = JSON.parse(readFileSync(join(dir, file), 'utf8'));
|
|
30
|
-
walk(json, [], undefined, file);
|
|
31
|
-
}
|
|
32
|
-
function walk(node, path, inheritedType, file) {
|
|
33
|
-
const type = node.$type ?? inheritedType;
|
|
34
|
-
if ('$value' in node) {
|
|
35
|
-
const p = path.join('.');
|
|
36
|
-
if (tokens.has(p)) errors.push(`${p}: defined twice`);
|
|
37
|
-
tokens.set(p, {
|
|
38
|
-
path: p,
|
|
39
|
-
segments: path,
|
|
40
|
-
file,
|
|
41
|
-
type,
|
|
42
|
-
value: node.$value,
|
|
43
|
-
description: node.$description,
|
|
44
|
-
deprecated: node.$deprecated,
|
|
45
|
-
extensions: node.$extensions ?? {},
|
|
46
|
-
tier: path[0] === 'primitive' ? 'primitive' : 'semantic',
|
|
47
|
-
});
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
for (const [k, v] of Object.entries(node)) {
|
|
51
|
-
if (k.startsWith('$')) continue;
|
|
52
|
-
if (v && typeof v === 'object' && !Array.isArray(v)) walk(v, [...path, k], type, file);
|
|
53
|
-
else errors.push(`${[...path, k].join('.')}: not a token or group`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return { tokens, errors };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export const isAlias = (v) => typeof v === 'string' && ALIAS.test(v);
|
|
60
|
-
export const aliasTarget = (v) => v.match(ALIAS)[1];
|
|
61
|
-
|
|
62
|
-
/** Follow aliases to a raw $value. */
|
|
63
|
-
export function resolve(tokens, value, seen = []) {
|
|
64
|
-
if (!isAlias(value)) return value;
|
|
65
|
-
const target = aliasTarget(value);
|
|
66
|
-
if (seen.includes(target)) throw new Error(`alias cycle: ${[...seen, target].join(' -> ')}`);
|
|
67
|
-
const t = tokens.get(target);
|
|
68
|
-
if (!t) throw new Error(`unresolved alias {${target}}`);
|
|
69
|
-
return resolve(tokens, t.value, [...seen, target]);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Resolved value of a token in a mode ('light' | 'dark'). */
|
|
73
|
-
export function resolveIn(tokens, path, mode = 'light') {
|
|
74
|
-
const t = tokens.get(path);
|
|
75
|
-
if (!t) throw new Error(`unknown token ${path}`);
|
|
76
|
-
const dark = t.extensions['codex.modes']?.dark;
|
|
77
|
-
return resolve(tokens, mode === 'dark' && dark ? dark : t.value);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export const cssVar = (path) => `--${PREFIX}-${path.replace(/\./g, '-')}`;
|
|
81
|
-
|
|
82
|
-
// ------------------------------------------------------------------ value shapes (DTCG 2025.10)
|
|
83
|
-
const isNum = (n) => typeof n === 'number' && Number.isFinite(n);
|
|
84
|
-
const SHAPES = {
|
|
85
|
-
color: (v) => v && v.colorSpace === 'srgb' && Array.isArray(v.components) && v.components.length === 3 &&
|
|
86
|
-
v.components.every((c) => isNum(c) && c >= 0 && c <= 1) && (!v.hex || /^#[0-9a-f]{6}$/.test(v.hex)),
|
|
87
|
-
dimension: (v) => v && isNum(v.value) && ['px', 'rem'].includes(v.unit),
|
|
88
|
-
duration: (v) => v && isNum(v.value) && ['ms', 's'].includes(v.unit),
|
|
89
|
-
cubicBezier: (v) => Array.isArray(v) && v.length === 4 && v.every(isNum) && v[0] >= 0 && v[0] <= 1 && v[2] >= 0 && v[2] <= 1,
|
|
90
|
-
fontFamily: (v) => typeof v === 'string' || (Array.isArray(v) && v.length > 0 && v.every((s) => typeof s === 'string')),
|
|
91
|
-
fontWeight: (v) => isNum(v) && v >= 1 && v <= 1000,
|
|
92
|
-
number: isNum,
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
/** Validate naming rules, tiers, descriptions, aliases, modes and value shapes. */
|
|
96
|
-
export function validate(tokens) {
|
|
97
|
-
const errors = [];
|
|
98
|
-
for (const t of tokens.values()) {
|
|
99
|
-
const { path, segments } = t;
|
|
100
|
-
const depth = segments.length;
|
|
101
|
-
// Naming: kebab-case or integers, max 4 segments counting the `primitive` group.
|
|
102
|
-
segments.forEach((s) => { if (!SEGMENT.test(s)) errors.push(`${path}: segment "${s}" is not kebab-case or an integer`); });
|
|
103
|
-
if (depth > MAX_DEPTH) errors.push(`${path}: ${depth} segments (max ${MAX_DEPTH})`);
|
|
104
|
-
if (/[0-9a-f]{6}/i.test(path)) errors.push(`${path}: raw value in name`);
|
|
105
|
-
// Category matches file.
|
|
106
|
-
const category = FILE_CATEGORY[t.file];
|
|
107
|
-
const cat = t.tier === 'primitive' ? segments[1] : segments[0];
|
|
108
|
-
const okCat = t.file === 'size.json' && t.tier === 'primitive' ? cat === 'size' : cat === category;
|
|
109
|
-
if (!okCat) errors.push(`${path}: category "${cat}" does not belong in ${t.file} (expected "${category}")`);
|
|
110
|
-
// Type and shape.
|
|
111
|
-
if (!t.type) { errors.push(`${path}: no $type`); continue; }
|
|
112
|
-
if (!SHAPES[t.type]) errors.push(`${path}: unsupported $type ${t.type}`);
|
|
113
|
-
// Tiers.
|
|
114
|
-
if (t.tier === 'primitive') {
|
|
115
|
-
if (t.extensions['codex.tier'] !== 'primitive') errors.push(`${path}: primitive without $extensions["codex.tier"] = "primitive"`);
|
|
116
|
-
if (isAlias(t.value)) errors.push(`${path}: primitives must hold raw values, not aliases`);
|
|
117
|
-
} else {
|
|
118
|
-
if (!t.description || t.description.length < 12) errors.push(`${path}: semantic token needs a usage-rule $description`);
|
|
119
|
-
if (!isAlias(t.value)) errors.push(`${path}: semantic tokens must reference a primitive`);
|
|
120
|
-
else if (!aliasTarget(t.value).startsWith('primitive.')) errors.push(`${path}: semantic tokens must reference primitives, not {${aliasTarget(t.value)}}`);
|
|
121
|
-
}
|
|
122
|
-
// Resolution and shape.
|
|
123
|
-
try {
|
|
124
|
-
const v = resolve(tokens, t.value);
|
|
125
|
-
if (SHAPES[t.type] && !SHAPES[t.type](v)) errors.push(`${path}: $value does not match DTCG ${t.type} shape`);
|
|
126
|
-
const target = isAlias(t.value) ? tokens.get(aliasTarget(t.value)) : null;
|
|
127
|
-
if (target && target.type !== t.type) errors.push(`${path}: type ${t.type} aliases ${target.path} of type ${target.type}`);
|
|
128
|
-
} catch (e) { errors.push(`${path}: ${e.message}`); }
|
|
129
|
-
// Modes.
|
|
130
|
-
const dark = t.extensions['codex.modes']?.dark;
|
|
131
|
-
if (dark !== undefined) {
|
|
132
|
-
if (!isAlias(dark) || !tokens.has(aliasTarget(dark))) errors.push(`${path}: codex.modes.dark must alias an existing token`);
|
|
133
|
-
else if (tokens.get(aliasTarget(dark)).type !== t.type) errors.push(`${path}: dark alias type mismatch`);
|
|
134
|
-
}
|
|
135
|
-
if (t.tier === 'semantic' && t.type === 'color' && dark === undefined) errors.push(`${path}: semantic colour has no codex.modes.dark`);
|
|
136
|
-
// Deprecation (DTCG $deprecated): a semantic token kept for one more major, with a reason.
|
|
137
|
-
if (t.deprecated !== undefined) {
|
|
138
|
-
if (t.tier === 'primitive') errors.push(`${path}: deprecate the semantic token, not the primitive`);
|
|
139
|
-
if (typeof t.deprecated !== 'string' || t.deprecated.length < 12) errors.push(`${path}: $deprecated must be a sentence saying why and when it goes`);
|
|
140
|
-
}
|
|
141
|
-
// Pairs.
|
|
142
|
-
for (const pair of t.extensions['codex.pairsWith'] ?? []) {
|
|
143
|
-
if (!tokens.has(pair.token)) errors.push(`${path}: pairsWith unknown token ${pair.token}`);
|
|
144
|
-
if (!isNum(pair.min)) errors.push(`${path}: pairsWith ${pair.token} needs a numeric min`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return errors;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ------------------------------------------------------------------ contrast (WCAG 2.x)
|
|
151
|
-
export const hexOf = (c) => c.hex ?? '#' + c.components.map((x) => Math.round(x * 255).toString(16).padStart(2, '0')).join('');
|
|
152
|
-
function luminance(c) {
|
|
153
|
-
const [r, g, b] = c.components.map((x) => (x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4));
|
|
154
|
-
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
155
|
-
}
|
|
156
|
-
export function ratio(a, b) {
|
|
157
|
-
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
158
|
-
return (hi + 0.05) / (lo + 0.05);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Every declared pair, measured in both modes. */
|
|
162
|
-
export function contrastReport(tokens) {
|
|
163
|
-
const rows = [];
|
|
164
|
-
for (const t of tokens.values()) {
|
|
165
|
-
for (const pair of t.extensions['codex.pairsWith'] ?? []) {
|
|
166
|
-
const r = {};
|
|
167
|
-
for (const mode of ['light', 'dark']) {
|
|
168
|
-
const fg = resolveIn(tokens, t.path, mode);
|
|
169
|
-
const bg = resolveIn(tokens, pair.token, mode);
|
|
170
|
-
r[mode] = { ratio: ratio(fg, bg), fg: hexOf(fg), bg: hexOf(bg) };
|
|
171
|
-
}
|
|
172
|
-
rows.push({ fg: t.path, bg: pair.token, min: pair.min, ...r, pass: r.light.ratio >= pair.min && r.dark.ratio >= pair.min });
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return rows;
|
|
176
|
-
}
|
|
1
|
+
// Shared token tooling for MotifSpec: load, flatten, resolve, validate, contrast.
|
|
2
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
export const PREFIX = 'cx';
|
|
8
|
+
|
|
9
|
+
/** Category prefix each token file may define (CONVENTIONS.md). */
|
|
10
|
+
export const FILE_CATEGORY = {
|
|
11
|
+
'color.json': 'color',
|
|
12
|
+
'typography.json': 'font',
|
|
13
|
+
'space.json': 'space',
|
|
14
|
+
'size.json': 'size',
|
|
15
|
+
'radius.json': 'radius',
|
|
16
|
+
'border.json': 'border',
|
|
17
|
+
'motion.json': 'motion',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const SEGMENT = /^(?:[a-z][a-z0-9]*(?:-[a-z0-9]+)*|[0-9]+)$/;
|
|
21
|
+
const MAX_DEPTH = 4;
|
|
22
|
+
const ALIAS = /^\{([^{}]+)\}$/;
|
|
23
|
+
|
|
24
|
+
/** Load every token file and flatten into a Map(path -> token). */
|
|
25
|
+
export function loadTokens(dir = join(ROOT, 'tokens')) {
|
|
26
|
+
const tokens = new Map();
|
|
27
|
+
const errors = [];
|
|
28
|
+
for (const file of readdirSync(dir).filter((f) => f.endsWith('.json')).sort()) {
|
|
29
|
+
const json = JSON.parse(readFileSync(join(dir, file), 'utf8'));
|
|
30
|
+
walk(json, [], undefined, file);
|
|
31
|
+
}
|
|
32
|
+
function walk(node, path, inheritedType, file) {
|
|
33
|
+
const type = node.$type ?? inheritedType;
|
|
34
|
+
if ('$value' in node) {
|
|
35
|
+
const p = path.join('.');
|
|
36
|
+
if (tokens.has(p)) errors.push(`${p}: defined twice`);
|
|
37
|
+
tokens.set(p, {
|
|
38
|
+
path: p,
|
|
39
|
+
segments: path,
|
|
40
|
+
file,
|
|
41
|
+
type,
|
|
42
|
+
value: node.$value,
|
|
43
|
+
description: node.$description,
|
|
44
|
+
deprecated: node.$deprecated,
|
|
45
|
+
extensions: node.$extensions ?? {},
|
|
46
|
+
tier: path[0] === 'primitive' ? 'primitive' : 'semantic',
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
for (const [k, v] of Object.entries(node)) {
|
|
51
|
+
if (k.startsWith('$')) continue;
|
|
52
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) walk(v, [...path, k], type, file);
|
|
53
|
+
else errors.push(`${[...path, k].join('.')}: not a token or group`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { tokens, errors };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const isAlias = (v) => typeof v === 'string' && ALIAS.test(v);
|
|
60
|
+
export const aliasTarget = (v) => v.match(ALIAS)[1];
|
|
61
|
+
|
|
62
|
+
/** Follow aliases to a raw $value. */
|
|
63
|
+
export function resolve(tokens, value, seen = []) {
|
|
64
|
+
if (!isAlias(value)) return value;
|
|
65
|
+
const target = aliasTarget(value);
|
|
66
|
+
if (seen.includes(target)) throw new Error(`alias cycle: ${[...seen, target].join(' -> ')}`);
|
|
67
|
+
const t = tokens.get(target);
|
|
68
|
+
if (!t) throw new Error(`unresolved alias {${target}}`);
|
|
69
|
+
return resolve(tokens, t.value, [...seen, target]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Resolved value of a token in a mode ('light' | 'dark'). */
|
|
73
|
+
export function resolveIn(tokens, path, mode = 'light') {
|
|
74
|
+
const t = tokens.get(path);
|
|
75
|
+
if (!t) throw new Error(`unknown token ${path}`);
|
|
76
|
+
const dark = t.extensions['codex.modes']?.dark;
|
|
77
|
+
return resolve(tokens, mode === 'dark' && dark ? dark : t.value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const cssVar = (path) => `--${PREFIX}-${path.replace(/\./g, '-')}`;
|
|
81
|
+
|
|
82
|
+
// ------------------------------------------------------------------ value shapes (DTCG 2025.10)
|
|
83
|
+
const isNum = (n) => typeof n === 'number' && Number.isFinite(n);
|
|
84
|
+
const SHAPES = {
|
|
85
|
+
color: (v) => v && v.colorSpace === 'srgb' && Array.isArray(v.components) && v.components.length === 3 &&
|
|
86
|
+
v.components.every((c) => isNum(c) && c >= 0 && c <= 1) && (!v.hex || /^#[0-9a-f]{6}$/.test(v.hex)),
|
|
87
|
+
dimension: (v) => v && isNum(v.value) && ['px', 'rem'].includes(v.unit),
|
|
88
|
+
duration: (v) => v && isNum(v.value) && ['ms', 's'].includes(v.unit),
|
|
89
|
+
cubicBezier: (v) => Array.isArray(v) && v.length === 4 && v.every(isNum) && v[0] >= 0 && v[0] <= 1 && v[2] >= 0 && v[2] <= 1,
|
|
90
|
+
fontFamily: (v) => typeof v === 'string' || (Array.isArray(v) && v.length > 0 && v.every((s) => typeof s === 'string')),
|
|
91
|
+
fontWeight: (v) => isNum(v) && v >= 1 && v <= 1000,
|
|
92
|
+
number: isNum,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Validate naming rules, tiers, descriptions, aliases, modes and value shapes. */
|
|
96
|
+
export function validate(tokens) {
|
|
97
|
+
const errors = [];
|
|
98
|
+
for (const t of tokens.values()) {
|
|
99
|
+
const { path, segments } = t;
|
|
100
|
+
const depth = segments.length;
|
|
101
|
+
// Naming: kebab-case or integers, max 4 segments counting the `primitive` group.
|
|
102
|
+
segments.forEach((s) => { if (!SEGMENT.test(s)) errors.push(`${path}: segment "${s}" is not kebab-case or an integer`); });
|
|
103
|
+
if (depth > MAX_DEPTH) errors.push(`${path}: ${depth} segments (max ${MAX_DEPTH})`);
|
|
104
|
+
if (/[0-9a-f]{6}/i.test(path)) errors.push(`${path}: raw value in name`);
|
|
105
|
+
// Category matches file.
|
|
106
|
+
const category = FILE_CATEGORY[t.file];
|
|
107
|
+
const cat = t.tier === 'primitive' ? segments[1] : segments[0];
|
|
108
|
+
const okCat = t.file === 'size.json' && t.tier === 'primitive' ? cat === 'size' : cat === category;
|
|
109
|
+
if (!okCat) errors.push(`${path}: category "${cat}" does not belong in ${t.file} (expected "${category}")`);
|
|
110
|
+
// Type and shape.
|
|
111
|
+
if (!t.type) { errors.push(`${path}: no $type`); continue; }
|
|
112
|
+
if (!SHAPES[t.type]) errors.push(`${path}: unsupported $type ${t.type}`);
|
|
113
|
+
// Tiers.
|
|
114
|
+
if (t.tier === 'primitive') {
|
|
115
|
+
if (t.extensions['codex.tier'] !== 'primitive') errors.push(`${path}: primitive without $extensions["codex.tier"] = "primitive"`);
|
|
116
|
+
if (isAlias(t.value)) errors.push(`${path}: primitives must hold raw values, not aliases`);
|
|
117
|
+
} else {
|
|
118
|
+
if (!t.description || t.description.length < 12) errors.push(`${path}: semantic token needs a usage-rule $description`);
|
|
119
|
+
if (!isAlias(t.value)) errors.push(`${path}: semantic tokens must reference a primitive`);
|
|
120
|
+
else if (!aliasTarget(t.value).startsWith('primitive.')) errors.push(`${path}: semantic tokens must reference primitives, not {${aliasTarget(t.value)}}`);
|
|
121
|
+
}
|
|
122
|
+
// Resolution and shape.
|
|
123
|
+
try {
|
|
124
|
+
const v = resolve(tokens, t.value);
|
|
125
|
+
if (SHAPES[t.type] && !SHAPES[t.type](v)) errors.push(`${path}: $value does not match DTCG ${t.type} shape`);
|
|
126
|
+
const target = isAlias(t.value) ? tokens.get(aliasTarget(t.value)) : null;
|
|
127
|
+
if (target && target.type !== t.type) errors.push(`${path}: type ${t.type} aliases ${target.path} of type ${target.type}`);
|
|
128
|
+
} catch (e) { errors.push(`${path}: ${e.message}`); }
|
|
129
|
+
// Modes.
|
|
130
|
+
const dark = t.extensions['codex.modes']?.dark;
|
|
131
|
+
if (dark !== undefined) {
|
|
132
|
+
if (!isAlias(dark) || !tokens.has(aliasTarget(dark))) errors.push(`${path}: codex.modes.dark must alias an existing token`);
|
|
133
|
+
else if (tokens.get(aliasTarget(dark)).type !== t.type) errors.push(`${path}: dark alias type mismatch`);
|
|
134
|
+
}
|
|
135
|
+
if (t.tier === 'semantic' && t.type === 'color' && dark === undefined) errors.push(`${path}: semantic colour has no codex.modes.dark`);
|
|
136
|
+
// Deprecation (DTCG $deprecated): a semantic token kept for one more major, with a reason.
|
|
137
|
+
if (t.deprecated !== undefined) {
|
|
138
|
+
if (t.tier === 'primitive') errors.push(`${path}: deprecate the semantic token, not the primitive`);
|
|
139
|
+
if (typeof t.deprecated !== 'string' || t.deprecated.length < 12) errors.push(`${path}: $deprecated must be a sentence saying why and when it goes`);
|
|
140
|
+
}
|
|
141
|
+
// Pairs.
|
|
142
|
+
for (const pair of t.extensions['codex.pairsWith'] ?? []) {
|
|
143
|
+
if (!tokens.has(pair.token)) errors.push(`${path}: pairsWith unknown token ${pair.token}`);
|
|
144
|
+
if (!isNum(pair.min)) errors.push(`${path}: pairsWith ${pair.token} needs a numeric min`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return errors;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ------------------------------------------------------------------ contrast (WCAG 2.x)
|
|
151
|
+
export const hexOf = (c) => c.hex ?? '#' + c.components.map((x) => Math.round(x * 255).toString(16).padStart(2, '0')).join('');
|
|
152
|
+
function luminance(c) {
|
|
153
|
+
const [r, g, b] = c.components.map((x) => (x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4));
|
|
154
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
155
|
+
}
|
|
156
|
+
export function ratio(a, b) {
|
|
157
|
+
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
158
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Every declared pair, measured in both modes. */
|
|
162
|
+
export function contrastReport(tokens) {
|
|
163
|
+
const rows = [];
|
|
164
|
+
for (const t of tokens.values()) {
|
|
165
|
+
for (const pair of t.extensions['codex.pairsWith'] ?? []) {
|
|
166
|
+
const r = {};
|
|
167
|
+
for (const mode of ['light', 'dark']) {
|
|
168
|
+
const fg = resolveIn(tokens, t.path, mode);
|
|
169
|
+
const bg = resolveIn(tokens, pair.token, mode);
|
|
170
|
+
r[mode] = { ratio: ratio(fg, bg), fg: hexOf(fg), bg: hexOf(bg) };
|
|
171
|
+
}
|
|
172
|
+
rows.push({ fg: t.path, bg: pair.token, min: pair.min, ...r, pass: r.light.ratio >= pair.min && r.dark.ratio >= pair.min });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return rows;
|
|
176
|
+
}
|
package/tokens/README.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
# tokens/
|
|
2
|
-
|
|
3
|
-
The DTCG source of truth: `color.json`, `typography.json`, `space.json`, `size.json`, `radius.json`, `border.json` and `motion.json`. Each file holds one category, with a `primitive` group of raw values and semantic tokens that reference them. Naming rules are in `../CONVENTIONS.md`; `npm run validate` checks them.
|
|
1
|
+
# tokens/
|
|
2
|
+
|
|
3
|
+
The DTCG source of truth: `color.json`, `typography.json`, `space.json`, `size.json`, `radius.json`, `border.json` and `motion.json`. Each file holds one category, with a `primitive` group of raw values and semantic tokens that reference them. Naming rules are in `../CONVENTIONS.md`; `npm run validate` checks them.
|
package/tokens/border.json
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
{
|
|
2
|
-
"primitive": {
|
|
3
|
-
"border": {
|
|
4
|
-
"width": {
|
|
5
|
-
"$type": "dimension",
|
|
6
|
-
"1": {
|
|
7
|
-
"$value": {
|
|
8
|
-
"value": 1,
|
|
9
|
-
"unit": "px"
|
|
10
|
-
},
|
|
11
|
-
"$extensions": {
|
|
12
|
-
"codex.tier": "primitive"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"2": {
|
|
16
|
-
"$value": {
|
|
17
|
-
"value": 2,
|
|
18
|
-
"unit": "px"
|
|
19
|
-
},
|
|
20
|
-
"$extensions": {
|
|
21
|
-
"codex.tier": "primitive"
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
"3": {
|
|
25
|
-
"$value": {
|
|
26
|
-
"value": 3,
|
|
27
|
-
"unit": "px"
|
|
28
|
-
},
|
|
29
|
-
"$extensions": {
|
|
30
|
-
"codex.tier": "primitive"
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
"border": {
|
|
37
|
-
"$type": "dimension",
|
|
38
|
-
"width": {
|
|
39
|
-
"hairline": {
|
|
40
|
-
"$value": "{primitive.border.width.1}",
|
|
41
|
-
"$description": "Every structural rule and outline. The default border."
|
|
42
|
-
},
|
|
43
|
-
"emphasis": {
|
|
44
|
-
"$value": "{primitive.border.width.2}",
|
|
45
|
-
"$description": "Active-state rules (current nav item, active TOC entry) and callout leading edges."
|
|
46
|
-
},
|
|
47
|
-
"contour": {
|
|
48
|
-
"$value": "{primitive.border.width.3}",
|
|
49
|
-
"$description": "Stroke of the backdrop's contour lines; index contours are twice this. Thicker than a hairline so the lines survive the frost blur. Not for the specimen plate, which keeps hairlines."
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
"focus": {
|
|
53
|
-
"width": {
|
|
54
|
-
"$value": "{primitive.border.width.2}",
|
|
55
|
-
"$description": "Width of the focus outline."
|
|
56
|
-
},
|
|
57
|
-
"offset": {
|
|
58
|
-
"$value": "{primitive.border.width.2}",
|
|
59
|
-
"$description": "Gap between an element and its focus outline, so the ring is measured against the surface."
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"primitive": {
|
|
3
|
+
"border": {
|
|
4
|
+
"width": {
|
|
5
|
+
"$type": "dimension",
|
|
6
|
+
"1": {
|
|
7
|
+
"$value": {
|
|
8
|
+
"value": 1,
|
|
9
|
+
"unit": "px"
|
|
10
|
+
},
|
|
11
|
+
"$extensions": {
|
|
12
|
+
"codex.tier": "primitive"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"2": {
|
|
16
|
+
"$value": {
|
|
17
|
+
"value": 2,
|
|
18
|
+
"unit": "px"
|
|
19
|
+
},
|
|
20
|
+
"$extensions": {
|
|
21
|
+
"codex.tier": "primitive"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"3": {
|
|
25
|
+
"$value": {
|
|
26
|
+
"value": 3,
|
|
27
|
+
"unit": "px"
|
|
28
|
+
},
|
|
29
|
+
"$extensions": {
|
|
30
|
+
"codex.tier": "primitive"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"border": {
|
|
37
|
+
"$type": "dimension",
|
|
38
|
+
"width": {
|
|
39
|
+
"hairline": {
|
|
40
|
+
"$value": "{primitive.border.width.1}",
|
|
41
|
+
"$description": "Every structural rule and outline. The default border."
|
|
42
|
+
},
|
|
43
|
+
"emphasis": {
|
|
44
|
+
"$value": "{primitive.border.width.2}",
|
|
45
|
+
"$description": "Active-state rules (current nav item, active TOC entry) and callout leading edges."
|
|
46
|
+
},
|
|
47
|
+
"contour": {
|
|
48
|
+
"$value": "{primitive.border.width.3}",
|
|
49
|
+
"$description": "Stroke of the backdrop's contour lines; index contours are twice this. Thicker than a hairline so the lines survive the frost blur. Not for the specimen plate, which keeps hairlines."
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"focus": {
|
|
53
|
+
"width": {
|
|
54
|
+
"$value": "{primitive.border.width.2}",
|
|
55
|
+
"$description": "Width of the focus outline."
|
|
56
|
+
},
|
|
57
|
+
"offset": {
|
|
58
|
+
"$value": "{primitive.border.width.2}",
|
|
59
|
+
"$description": "Gap between an element and its focus outline, so the ring is measured against the surface."
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|