@valancex/style 0.0.0-stage → 0.14.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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -2
  3. package/contract/style-contract-v1.json +207 -0
  4. package/dist/check.d.ts +31 -0
  5. package/dist/check.js +250 -0
  6. package/dist/color.d.ts +17 -0
  7. package/dist/color.js +82 -0
  8. package/dist/contrast.d.ts +5 -0
  9. package/dist/contrast.js +13 -0
  10. package/dist/coverage.d.ts +21 -0
  11. package/dist/coverage.js +60 -0
  12. package/dist/css.d.ts +9 -0
  13. package/dist/css.js +201 -0
  14. package/dist/desktop.d.ts +33 -0
  15. package/dist/desktop.js +112 -0
  16. package/dist/index.d.ts +17 -0
  17. package/dist/index.js +11 -0
  18. package/dist/load.d.ts +5 -0
  19. package/dist/load.js +8 -0
  20. package/dist/primitives.d.ts +2 -0
  21. package/dist/primitives.js +364 -0
  22. package/dist/suggest.d.ts +2 -0
  23. package/dist/suggest.js +22 -0
  24. package/dist/theme.d.ts +5 -0
  25. package/dist/theme.js +129 -0
  26. package/dist/tokens.d.ts +21 -0
  27. package/dist/tokens.js +257 -0
  28. package/dist/types.d.ts +114 -0
  29. package/dist/types.js +3 -0
  30. package/package.json +64 -3
  31. package/schema/dtcg-2025.10/NOTICE.md +6 -0
  32. package/schema/dtcg-2025.10/format/group.json +54 -0
  33. package/schema/dtcg-2025.10/format/groupOrToken.json +14 -0
  34. package/schema/dtcg-2025.10/format/token.json +450 -0
  35. package/schema/dtcg-2025.10/format/tokenType.json +22 -0
  36. package/schema/dtcg-2025.10/format/values/border.json +44 -0
  37. package/schema/dtcg-2025.10/format/values/color.json +570 -0
  38. package/schema/dtcg-2025.10/format/values/cubicBezier.json +56 -0
  39. package/schema/dtcg-2025.10/format/values/dimension.json +34 -0
  40. package/schema/dtcg-2025.10/format/values/duration.json +34 -0
  41. package/schema/dtcg-2025.10/format/values/fontFamily.json +33 -0
  42. package/schema/dtcg-2025.10/format/values/fontWeight.json +38 -0
  43. package/schema/dtcg-2025.10/format/values/gradient.json +52 -0
  44. package/schema/dtcg-2025.10/format/values/number.json +7 -0
  45. package/schema/dtcg-2025.10/format/values/shadow.json +102 -0
  46. package/schema/dtcg-2025.10/format/values/strokeStyle.json +63 -0
  47. package/schema/dtcg-2025.10/format/values/transition.json +44 -0
  48. package/schema/dtcg-2025.10/format/values/typography.json +72 -0
  49. package/schema/dtcg-2025.10/format.json +105 -0
  50. package/schema/recipe-v1.schema.json +43 -0
  51. package/schema/style-contract-v1.schema.json +171 -0
  52. package/schema/theme-v1.schema.json +78 -0
  53. package/themes/default/recipes/button.json +87 -0
  54. package/themes/default/recipes/checkbox.json +26 -0
  55. package/themes/default/recipes/column.json +10 -0
  56. package/themes/default/recipes/heading.json +11 -0
  57. package/themes/default/recipes/row.json +10 -0
  58. package/themes/default/recipes/text.json +12 -0
  59. package/themes/default/recipes/textfield.json +37 -0
  60. package/themes/default/theme.json +61 -0
  61. package/themes/default/tokens/base.tokens.json +193 -0
  62. package/themes/default/tokens/contrast.tokens.json +127 -0
  63. package/themes/default/tokens/dark.tokens.json +127 -0
  64. package/themes/default/tokens/light.tokens.json +127 -0
@@ -0,0 +1,364 @@
1
+ export const primitives = {
2
+ "a": {
3
+ "status": "web-only",
4
+ "interactive": false,
5
+ "states": [],
6
+ "variants": [],
7
+ "stateSelectors": {}
8
+ },
9
+ "alert": {
10
+ "status": "portable",
11
+ "interactive": false,
12
+ "states": [],
13
+ "variants": [],
14
+ "stateSelectors": {}
15
+ },
16
+ "article": {
17
+ "status": "web-only",
18
+ "interactive": false,
19
+ "states": [],
20
+ "variants": [],
21
+ "stateSelectors": {}
22
+ },
23
+ "aside": {
24
+ "status": "web-only",
25
+ "interactive": false,
26
+ "states": [],
27
+ "variants": [],
28
+ "stateSelectors": {}
29
+ },
30
+ "blockquote": {
31
+ "status": "portable",
32
+ "interactive": false,
33
+ "states": [],
34
+ "variants": [],
35
+ "stateSelectors": {}
36
+ },
37
+ "button": {
38
+ "status": "portable",
39
+ "interactive": true,
40
+ "states": [
41
+ "disabled"
42
+ ],
43
+ "variants": [
44
+ {
45
+ "name": "intent",
46
+ "attribute": "data-intent"
47
+ },
48
+ {
49
+ "name": "size",
50
+ "attribute": "data-size"
51
+ }
52
+ ],
53
+ "hook": "button",
54
+ "stateSelectors": {
55
+ "disabled": ":disabled"
56
+ }
57
+ },
58
+ "cell": {
59
+ "status": "desktop-only",
60
+ "interactive": false,
61
+ "states": [],
62
+ "variants": [],
63
+ "stateSelectors": {}
64
+ },
65
+ "checkbox": {
66
+ "status": "portable",
67
+ "interactive": true,
68
+ "states": [
69
+ "checked",
70
+ "invalid"
71
+ ],
72
+ "variants": [],
73
+ "hook": "checkbox",
74
+ "stateSelectors": {
75
+ "checked": ":checked",
76
+ "invalid": "[aria-invalid=\"true\"]"
77
+ }
78
+ },
79
+ "code": {
80
+ "status": "portable",
81
+ "interactive": false,
82
+ "states": [],
83
+ "variants": [],
84
+ "stateSelectors": {}
85
+ },
86
+ "column": {
87
+ "status": "portable",
88
+ "interactive": false,
89
+ "states": [],
90
+ "variants": [],
91
+ "hook": "column",
92
+ "stateSelectors": {}
93
+ },
94
+ "columnheader": {
95
+ "status": "desktop-only",
96
+ "interactive": false,
97
+ "states": [],
98
+ "variants": [],
99
+ "stateSelectors": {}
100
+ },
101
+ "dialog": {
102
+ "status": "portable",
103
+ "interactive": true,
104
+ "states": [],
105
+ "variants": [],
106
+ "stateSelectors": {}
107
+ },
108
+ "div": {
109
+ "status": "web-only",
110
+ "interactive": false,
111
+ "states": [],
112
+ "variants": [],
113
+ "stateSelectors": {}
114
+ },
115
+ "emphasis": {
116
+ "status": "portable",
117
+ "interactive": false,
118
+ "states": [],
119
+ "variants": [],
120
+ "stateSelectors": {}
121
+ },
122
+ "footer": {
123
+ "status": "web-only",
124
+ "interactive": false,
125
+ "states": [],
126
+ "variants": [],
127
+ "stateSelectors": {}
128
+ },
129
+ "h1": {
130
+ "status": "web-only",
131
+ "interactive": false,
132
+ "states": [],
133
+ "variants": [],
134
+ "stateSelectors": {}
135
+ },
136
+ "h2": {
137
+ "status": "web-only",
138
+ "interactive": false,
139
+ "states": [],
140
+ "variants": [],
141
+ "stateSelectors": {}
142
+ },
143
+ "h3": {
144
+ "status": "web-only",
145
+ "interactive": false,
146
+ "states": [],
147
+ "variants": [],
148
+ "stateSelectors": {}
149
+ },
150
+ "header": {
151
+ "status": "web-only",
152
+ "interactive": false,
153
+ "states": [],
154
+ "variants": [],
155
+ "stateSelectors": {}
156
+ },
157
+ "heading": {
158
+ "status": "portable",
159
+ "interactive": false,
160
+ "states": [
161
+ "level"
162
+ ],
163
+ "variants": [],
164
+ "hook": "heading",
165
+ "stateSelectors": {}
166
+ },
167
+ "image": {
168
+ "status": "portable",
169
+ "interactive": false,
170
+ "states": [],
171
+ "variants": [],
172
+ "stateSelectors": {}
173
+ },
174
+ "label": {
175
+ "status": "portable",
176
+ "interactive": false,
177
+ "states": [],
178
+ "variants": [],
179
+ "stateSelectors": {}
180
+ },
181
+ "link": {
182
+ "status": "portable",
183
+ "interactive": true,
184
+ "states": [],
185
+ "variants": [],
186
+ "stateSelectors": {}
187
+ },
188
+ "list": {
189
+ "status": "portable",
190
+ "interactive": false,
191
+ "states": [],
192
+ "variants": [],
193
+ "stateSelectors": {}
194
+ },
195
+ "listitem": {
196
+ "status": "portable",
197
+ "interactive": false,
198
+ "states": [],
199
+ "variants": [],
200
+ "stateSelectors": {}
201
+ },
202
+ "main": {
203
+ "status": "web-only",
204
+ "interactive": false,
205
+ "states": [],
206
+ "variants": [],
207
+ "stateSelectors": {}
208
+ },
209
+ "nav": {
210
+ "status": "web-only",
211
+ "interactive": false,
212
+ "states": [],
213
+ "variants": [],
214
+ "stateSelectors": {}
215
+ },
216
+ "paragraph": {
217
+ "status": "portable",
218
+ "interactive": false,
219
+ "states": [],
220
+ "variants": [],
221
+ "stateSelectors": {}
222
+ },
223
+ "popover": {
224
+ "status": "portable",
225
+ "interactive": true,
226
+ "states": [],
227
+ "variants": [],
228
+ "stateSelectors": {}
229
+ },
230
+ "pre": {
231
+ "status": "web-only",
232
+ "interactive": false,
233
+ "states": [],
234
+ "variants": [],
235
+ "stateSelectors": {}
236
+ },
237
+ "radio": {
238
+ "status": "portable",
239
+ "interactive": true,
240
+ "states": [
241
+ "checked",
242
+ "disabled"
243
+ ],
244
+ "variants": [],
245
+ "stateSelectors": {}
246
+ },
247
+ "radiogroup": {
248
+ "status": "portable",
249
+ "interactive": false,
250
+ "states": [
251
+ "invalid"
252
+ ],
253
+ "variants": [],
254
+ "stateSelectors": {}
255
+ },
256
+ "row": {
257
+ "status": "portable",
258
+ "interactive": false,
259
+ "states": [],
260
+ "variants": [],
261
+ "hook": "row",
262
+ "stateSelectors": {}
263
+ },
264
+ "rowheader": {
265
+ "status": "desktop-only",
266
+ "interactive": false,
267
+ "states": [],
268
+ "variants": [],
269
+ "stateSelectors": {}
270
+ },
271
+ "section": {
272
+ "status": "web-only",
273
+ "interactive": false,
274
+ "states": [],
275
+ "variants": [],
276
+ "stateSelectors": {}
277
+ },
278
+ "shell": {
279
+ "status": "web-only",
280
+ "interactive": false,
281
+ "states": [],
282
+ "variants": [],
283
+ "stateSelectors": {}
284
+ },
285
+ "span": {
286
+ "status": "web-only",
287
+ "interactive": false,
288
+ "states": [],
289
+ "variants": [],
290
+ "stateSelectors": {}
291
+ },
292
+ "status": {
293
+ "status": "portable",
294
+ "interactive": false,
295
+ "states": [],
296
+ "variants": [],
297
+ "stateSelectors": {}
298
+ },
299
+ "strong": {
300
+ "status": "portable",
301
+ "interactive": false,
302
+ "states": [],
303
+ "variants": [],
304
+ "stateSelectors": {}
305
+ },
306
+ "table": {
307
+ "status": "desktop-only",
308
+ "interactive": false,
309
+ "states": [],
310
+ "variants": [],
311
+ "stateSelectors": {}
312
+ },
313
+ "tablebody": {
314
+ "status": "desktop-only",
315
+ "interactive": false,
316
+ "states": [],
317
+ "variants": [],
318
+ "stateSelectors": {}
319
+ },
320
+ "tablehead": {
321
+ "status": "desktop-only",
322
+ "interactive": false,
323
+ "states": [],
324
+ "variants": [],
325
+ "stateSelectors": {}
326
+ },
327
+ "tablerow": {
328
+ "status": "desktop-only",
329
+ "interactive": false,
330
+ "states": [],
331
+ "variants": [],
332
+ "stateSelectors": {}
333
+ },
334
+ "text": {
335
+ "status": "portable",
336
+ "interactive": false,
337
+ "states": [],
338
+ "variants": [],
339
+ "hook": "text",
340
+ "stateSelectors": {}
341
+ },
342
+ "textfield": {
343
+ "status": "portable",
344
+ "interactive": true,
345
+ "states": [
346
+ "invalid"
347
+ ],
348
+ "variants": [],
349
+ "hook": "textfield",
350
+ "stateSelectors": {
351
+ "invalid": "[aria-invalid=\"true\"]"
352
+ }
353
+ },
354
+ "toggle": {
355
+ "status": "portable",
356
+ "interactive": true,
357
+ "states": [
358
+ "pressed",
359
+ "disabled"
360
+ ],
361
+ "variants": [],
362
+ "stateSelectors": {}
363
+ }
364
+ };
@@ -0,0 +1,2 @@
1
+ export declare const nearest: (word: string, names: ReadonlyArray<string>) => string | undefined;
2
+ export declare const didYouMean: (word: string, names: ReadonlyArray<string>) => string;
@@ -0,0 +1,22 @@
1
+ // "Did you mean": the nearest of some names to a word, when one is near enough to be a slip of the keyboard.
2
+ const distance = (a, b) => {
3
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
4
+ for (let i = 1; i <= a.length; i += 1) {
5
+ let diagonal = row[0];
6
+ row[0] = i;
7
+ for (let j = 1; j <= b.length; j += 1) {
8
+ const above = row[j];
9
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
10
+ diagonal = above;
11
+ }
12
+ }
13
+ return row[b.length];
14
+ };
15
+ export const nearest = (word, names) => {
16
+ const ranked = names.map((name) => ({ name, d: distance(word, name) })).sort((x, y) => x.d - y.d)[0];
17
+ return ranked !== undefined && ranked.d <= Math.max(2, Math.floor(word.length / 3)) ? ranked.name : undefined;
18
+ };
19
+ export const didYouMean = (word, names) => {
20
+ const found = nearest(word, names);
21
+ return found === undefined ? "" : ` (did you mean "${found}"?)`;
22
+ };
@@ -0,0 +1,5 @@
1
+ import Ajv from "ajv";
2
+ import type { LoadedTheme } from "./types.js";
3
+ export declare const validateRecipe: Ajv.ValidateFunction<unknown>;
4
+ /** Reads the theme at `themeFile`. Never throws for a mistake in the theme: that is what `problems` is for. */
5
+ export declare const loadTheme: (themeFile: string, display?: (path: string) => string) => LoadedTheme;
package/dist/theme.js ADDED
@@ -0,0 +1,129 @@
1
+ // A theme on disk: `theme.json`, its token documents by mode and its recipes. Loading reads and checks everything that can be checked from the files alone (the schemas, the Design Tokens format, the
2
+ // graph pass, that every mode defines the same tokens); `check.ts` then checks the recipes against the primitives' specifications.
3
+ import Ajv from "ajv";
4
+ import { readFileSync } from "node:fs";
5
+ import { dirname, join, relative } from "node:path";
6
+ import { loadSchema } from "./load.js";
7
+ import { resolveDocument, validateDocument } from "./tokens.js";
8
+ const ajv = new Ajv.default({ strict: false, allErrors: true });
9
+ const validateTheme = ajv.compile(loadSchema("theme-v1.schema.json"));
10
+ export const validateRecipe = ajv.compile(loadSchema("recipe-v1.schema.json"));
11
+ const read = (path) => {
12
+ try {
13
+ return { value: JSON.parse(readFileSync(path, "utf8")) };
14
+ }
15
+ catch (error) {
16
+ return { problem: error instanceof Error ? error.message : String(error) };
17
+ }
18
+ };
19
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
20
+ /** `over` laid on `base`, group by group (a token of `over` replaces the token of `base`). */
21
+ const lay = (base, over) => {
22
+ const out = { ...base };
23
+ for (const [key, value] of Object.entries(over)) {
24
+ const was = out[key];
25
+ out[key] = isObject(value) && isObject(was) && !("$value" in value) && !("$value" in was) ? lay(was, value) : value;
26
+ }
27
+ return out;
28
+ };
29
+ const schemaProblems = (file, errors) => (errors ?? [])
30
+ .slice(0, 5)
31
+ .map((each) => ({ file, path: each.instancePath, code: "schema", message: `${each.instancePath === "" ? "(document)" : each.instancePath} ${each.message ?? "is invalid"}` }));
32
+ /** Reads the theme at `themeFile`. Never throws for a mistake in the theme: that is what `problems` is for. */
33
+ export const loadTheme = (themeFile, display = (path) => relative(process.cwd(), path)) => {
34
+ const directory = dirname(themeFile);
35
+ const problems = [];
36
+ const shown = display(themeFile);
37
+ const parsed = read(themeFile);
38
+ if (parsed.problem !== undefined) {
39
+ return {
40
+ theme: { version: 1, name: "unreadable", modes: { light: "" }, recipes: [] },
41
+ directory,
42
+ modes: {},
43
+ recipes: [],
44
+ problems: [{ file: shown, code: "theme-unreadable", message: parsed.problem }],
45
+ };
46
+ }
47
+ if (!validateTheme(parsed.value)) {
48
+ problems.push(...schemaProblems(shown, validateTheme.errors));
49
+ return { theme: { version: 1, name: "invalid", modes: { light: "" }, recipes: [] }, directory, modes: {}, recipes: [], problems };
50
+ }
51
+ const theme = parsed.value;
52
+ // the tokens, by mode: the base document laid under each mode's own
53
+ const documents = {};
54
+ const baseFile = theme.base === undefined ? undefined : join(directory, theme.base);
55
+ const base = baseFile === undefined ? undefined : read(baseFile);
56
+ if (base?.problem !== undefined) {
57
+ problems.push({ file: display(baseFile), code: "tokens-unreadable", message: base.problem });
58
+ }
59
+ for (const [mode, relativeFile] of Object.entries(theme.modes)) {
60
+ const file = join(directory, relativeFile);
61
+ const own = read(file);
62
+ if (own.problem !== undefined) {
63
+ problems.push({ file: display(file), code: "tokens-unreadable", message: own.problem });
64
+ continue;
65
+ }
66
+ for (const [name, document] of [[display(file), own.value], ...(base?.value === undefined ? [] : [[display(baseFile), base.value]])]) {
67
+ const checked = validateDocument(document);
68
+ if (!checked.valid && !problems.some((each) => each.file === name && each.code === "tokens-format")) {
69
+ problems.push({ file: name, code: "tokens-format", message: `not a Design Tokens (2025.10) document: ${checked.error}` });
70
+ }
71
+ }
72
+ documents[mode] = lay(isObject(base?.value) ? base.value : {}, isObject(own.value) ? own.value : {});
73
+ }
74
+ const modes = {};
75
+ for (const [mode, document] of Object.entries(documents)) {
76
+ const file = display(join(directory, theme.modes[mode]));
77
+ const resolved = resolveDocument(document, file);
78
+ problems.push(...resolved.problems);
79
+ // an alias can put a value where its type is wrong: the resolved document is judged by the schemas again
80
+ if (resolved.problems.length === 0) {
81
+ const again = validateDocument(resolved.resolved);
82
+ if (!again.valid) {
83
+ problems.push({ file, code: "tokens-format", message: `after its references are followed the ${mode} mode is not a Design Tokens document: ${again.error}` });
84
+ }
85
+ }
86
+ modes[mode] = resolved.tokens;
87
+ }
88
+ // every mode defines the same tokens as `light`
89
+ const light = modes["light"];
90
+ if (light !== undefined) {
91
+ for (const [mode, tokens] of Object.entries(modes)) {
92
+ for (const path of light.keys()) {
93
+ if (!tokens.has(path)) {
94
+ problems.push({
95
+ file: display(join(directory, theme.modes[mode])),
96
+ path,
97
+ code: "mode-missing-token",
98
+ message: `the ${mode} mode does not define ${path}, which the light mode does`,
99
+ });
100
+ }
101
+ }
102
+ for (const path of tokens.keys()) {
103
+ if (!light.has(path)) {
104
+ problems.push({
105
+ file: display(join(directory, theme.modes[mode])),
106
+ path,
107
+ code: "mode-extra-token",
108
+ message: `the ${mode} mode defines ${path}, which the light mode does not`,
109
+ });
110
+ }
111
+ }
112
+ }
113
+ }
114
+ const recipes = [];
115
+ for (const relativeFile of theme.recipes) {
116
+ const file = join(directory, relativeFile);
117
+ const own = read(file);
118
+ if (own.problem !== undefined) {
119
+ problems.push({ file: display(file), code: "recipe-unreadable", message: own.problem });
120
+ }
121
+ else if (!validateRecipe(own.value)) {
122
+ problems.push(...schemaProblems(display(file), validateRecipe.errors).map((each) => ({ ...each, code: "recipe-schema" })));
123
+ }
124
+ else {
125
+ recipes.push({ file: display(file), recipe: own.value });
126
+ }
127
+ }
128
+ return { theme, directory, modes, recipes, problems };
129
+ };
@@ -0,0 +1,21 @@
1
+ import { type ValidateFunction } from "ajv";
2
+ import type { Problem, ResolvedToken } from "./types.js";
3
+ /** The validator for a whole token document: the group's `format.json` with every schema it references. */
4
+ export declare const dtcgValidator: () => ValidateFunction;
5
+ /** Whether `document` is a valid token document, and the first error if not. */
6
+ export declare const validateDocument: (document: unknown) => {
7
+ readonly valid: boolean;
8
+ readonly error?: string;
9
+ };
10
+ type Json = null | boolean | number | string | ReadonlyArray<Json> | {
11
+ readonly [key: string]: Json;
12
+ };
13
+ export interface Resolved {
14
+ readonly tokens: ReadonlyMap<string, ResolvedToken>;
15
+ readonly problems: ReadonlyArray<Problem>;
16
+ /** The document with every `$extends`, alias and pointer replaced by what it stands for, and a `$type` on every token: what the schemas judge a second time, because an alias can put a value where its type is wrong. */
17
+ readonly resolved: Json;
18
+ }
19
+ /** The pass over one document that has already been checked against the schemas: follows `$extends`, curly-brace aliases and JSON Pointer `$ref`s to their ends. */
20
+ export declare const resolveDocument: (document: unknown, file: string) => Resolved;
21
+ export {};