@penpair/marwes-ui 0.2.0 → 0.3.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/dist/build-kit.js +24 -6
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +500 -94
- package/dist/components.js +375 -74
- package/dist/config.js +53 -16
- package/dist/index.js +21 -3
- package/dist/marwes.js +45 -12
- package/dist/mode-trial.js +325 -75
- package/dist/ownership.js +162 -37
- package/dist/penpot.js +447 -114
- package/dist/reconciliation.js +112 -37
- package/dist/resolved-values.d.ts +3 -8
- package/dist/resolved-values.js +99 -103
- package/dist/snapshot-file.js +255 -50
- package/dist/theme-input.js +12 -4
- package/dist/theme-lint.js +113 -27
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.js +207 -52
- package/package.json +2 -2
package/dist/snapshot-file.js
CHANGED
|
@@ -1,124 +1,329 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { atomicWriteFile, resolveProjectInput, resolveProjectOutput, validateEditableToken } from "@penpair/core";
|
|
4
|
+
import { atomicWriteFile, resolveProjectInput, resolveProjectOutput, validateEditableToken, } from "@penpair/core";
|
|
5
5
|
import { canonical } from "./tokens.js";
|
|
6
6
|
export async function readSnapshot(root, file) {
|
|
7
7
|
try {
|
|
8
8
|
const snapshot = JSON.parse(await readFile(await resolveProjectInput(root, file), "utf8"));
|
|
9
|
-
if (![
|
|
9
|
+
if (![
|
|
10
|
+
1,
|
|
11
|
+
2,
|
|
12
|
+
].includes(snapshot.schemaVersion) ||
|
|
13
|
+
!Array.isArray(snapshot.sets) ||
|
|
14
|
+
!Array.isArray(snapshot.themes) ||
|
|
15
|
+
typeof snapshot.hash !== "string" ||
|
|
16
|
+
typeof snapshot.project !== "string") {
|
|
10
17
|
throw new Error("Unsupported or malformed snapshot; regenerate with tokens snapshot --write");
|
|
11
|
-
|
|
18
|
+
}
|
|
19
|
+
if (snapshot.schemaVersion === 2 &&
|
|
20
|
+
(!snapshot.ownership ||
|
|
21
|
+
Object.keys(snapshot.ownership).length !== snapshot.sets.length ||
|
|
22
|
+
snapshot.sets.some((set) => ![
|
|
23
|
+
"adapter",
|
|
24
|
+
"project",
|
|
25
|
+
].includes(snapshot.ownership?.[set.name] ?? "")))) {
|
|
12
26
|
throw new Error("Invalid schema-2 set ownership");
|
|
27
|
+
}
|
|
13
28
|
return snapshot;
|
|
14
29
|
}
|
|
15
30
|
catch (error) {
|
|
16
|
-
if (error.code === "ENOENT")
|
|
31
|
+
if (error.code === "ENOENT") {
|
|
17
32
|
return undefined;
|
|
33
|
+
}
|
|
18
34
|
throw new Error(`${file} is not a readable snapshot: ${String(error.message)}`);
|
|
19
35
|
}
|
|
20
36
|
}
|
|
21
37
|
export async function writeSnapshot(root, file, snapshot) {
|
|
22
38
|
const target = await resolveProjectOutput(root, file);
|
|
23
|
-
await mkdir(path.dirname(target), {
|
|
24
|
-
|
|
39
|
+
await mkdir(path.dirname(target), {
|
|
40
|
+
recursive: true,
|
|
41
|
+
});
|
|
42
|
+
await atomicWriteFile(target, `${JSON.stringify(snapshot, null, 2)}\n`, {
|
|
43
|
+
replace: true,
|
|
44
|
+
});
|
|
25
45
|
}
|
|
26
|
-
const show = (token) =>
|
|
46
|
+
const show = (token) => typeof token.value === "string" ? token.value : canonical(token.value);
|
|
27
47
|
/** What differs between two expectations: the Marwes version, then set by set which tokens were added, removed or changed. */
|
|
28
48
|
export function diffSnapshots(before, after) {
|
|
29
49
|
const sets = [];
|
|
30
|
-
const previous = new Map((before?.sets ?? []).map((s) => [
|
|
31
|
-
|
|
50
|
+
const previous = new Map((before?.sets ?? []).map((s) => [
|
|
51
|
+
s.name,
|
|
52
|
+
s,
|
|
53
|
+
]));
|
|
54
|
+
const next = new Map(after.sets.map((s) => [
|
|
55
|
+
s.name,
|
|
56
|
+
s,
|
|
57
|
+
]));
|
|
32
58
|
for (const [name, set] of next) {
|
|
33
59
|
const old = previous.get(name);
|
|
34
|
-
const oldTokens = new Map((old?.tokens ?? []).map((t) => [
|
|
35
|
-
|
|
60
|
+
const oldTokens = new Map((old?.tokens ?? []).map((t) => [
|
|
61
|
+
t.name,
|
|
62
|
+
t,
|
|
63
|
+
]));
|
|
64
|
+
const newTokens = new Map(set.tokens.map((t) => [
|
|
65
|
+
t.name,
|
|
66
|
+
t,
|
|
67
|
+
]));
|
|
36
68
|
const change = {
|
|
37
69
|
set: name,
|
|
38
|
-
...(old
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
70
|
+
...(old
|
|
71
|
+
? {}
|
|
72
|
+
: {
|
|
73
|
+
setAdded: true,
|
|
74
|
+
}),
|
|
75
|
+
added: [
|
|
76
|
+
...newTokens.keys(),
|
|
77
|
+
].filter((n) => !oldTokens.has(n)),
|
|
78
|
+
removed: [
|
|
79
|
+
...oldTokens.keys(),
|
|
80
|
+
].filter((n) => !newTokens.has(n)),
|
|
81
|
+
changed: [
|
|
82
|
+
...newTokens.entries(),
|
|
83
|
+
]
|
|
84
|
+
.filter(([n, t]) => oldTokens.has(n) && show(oldTokens.get(n)) !== show(t))
|
|
85
|
+
.map(([n, t]) => ({
|
|
86
|
+
name: n,
|
|
87
|
+
before: show(oldTokens.get(n)),
|
|
88
|
+
after: show(t),
|
|
89
|
+
})),
|
|
42
90
|
};
|
|
43
|
-
if (change.setAdded || change.added.length || change.removed.length || change.changed.length)
|
|
91
|
+
if (change.setAdded || change.added.length || change.removed.length || change.changed.length) {
|
|
44
92
|
sets.push(change);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const [name, old] of previous) {
|
|
96
|
+
if (!next.has(name)) {
|
|
97
|
+
sets.push({
|
|
98
|
+
set: name,
|
|
99
|
+
setRemoved: true,
|
|
100
|
+
added: [],
|
|
101
|
+
removed: old.tokens.map((t) => t.name),
|
|
102
|
+
changed: [],
|
|
103
|
+
});
|
|
104
|
+
}
|
|
45
105
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const newThemes = new Map(after.themes.map((t) => [
|
|
106
|
+
const oldThemes = new Map((before?.themes ?? []).map((t) => [
|
|
107
|
+
`${t.group}/${t.name}`,
|
|
108
|
+
t.sets,
|
|
109
|
+
]));
|
|
110
|
+
const newThemes = new Map(after.themes.map((t) => [
|
|
111
|
+
`${t.group}/${t.name}`,
|
|
112
|
+
t.sets,
|
|
113
|
+
]));
|
|
51
114
|
const themes = {
|
|
52
|
-
added: [
|
|
53
|
-
|
|
54
|
-
|
|
115
|
+
added: [
|
|
116
|
+
...newThemes.keys(),
|
|
117
|
+
].filter((k) => !oldThemes.has(k)),
|
|
118
|
+
removed: [
|
|
119
|
+
...oldThemes.keys(),
|
|
120
|
+
].filter((k) => !newThemes.has(k)),
|
|
121
|
+
changed: [
|
|
122
|
+
...newThemes.entries(),
|
|
123
|
+
]
|
|
124
|
+
.filter(([k, s]) => oldThemes.has(k) && canonical(oldThemes.get(k)) !== canonical(s))
|
|
125
|
+
.map(([k, s]) => ({
|
|
126
|
+
theme: k,
|
|
127
|
+
before: oldThemes.get(k),
|
|
128
|
+
after: s,
|
|
129
|
+
})),
|
|
55
130
|
};
|
|
56
131
|
const wasUnmapped = new Set(before?.unmapped ?? []);
|
|
57
132
|
const isUnmapped = new Set(after.unmapped ?? []);
|
|
58
|
-
const unmapped = {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
133
|
+
const unmapped = {
|
|
134
|
+
added: [
|
|
135
|
+
...isUnmapped,
|
|
136
|
+
].filter((v) => !wasUnmapped.has(v)),
|
|
137
|
+
removed: [
|
|
138
|
+
...wasUnmapped,
|
|
139
|
+
].filter((v) => !isUnmapped.has(v)),
|
|
140
|
+
};
|
|
141
|
+
const versionChange = before && before.marwesVersion !== after.marwesVersion
|
|
142
|
+
? {
|
|
143
|
+
before: before.marwesVersion,
|
|
144
|
+
after: after.marwesVersion,
|
|
145
|
+
}
|
|
146
|
+
: undefined;
|
|
147
|
+
const identical = !!before &&
|
|
148
|
+
!versionChange &&
|
|
149
|
+
sets.length === 0 &&
|
|
150
|
+
!themes.added.length &&
|
|
151
|
+
!themes.removed.length &&
|
|
152
|
+
!themes.changed.length &&
|
|
153
|
+
!unmapped.added.length &&
|
|
154
|
+
!unmapped.removed.length;
|
|
155
|
+
return {
|
|
156
|
+
identical,
|
|
157
|
+
...(versionChange
|
|
158
|
+
? {
|
|
159
|
+
versionChange,
|
|
160
|
+
}
|
|
161
|
+
: {}),
|
|
162
|
+
sets,
|
|
163
|
+
themes,
|
|
164
|
+
unmapped,
|
|
165
|
+
};
|
|
62
166
|
}
|
|
63
167
|
/** A short, readable account of a diff for the terminal; long lists are cut with a count. */
|
|
64
168
|
export function formatDiff(diff, limit = 8) {
|
|
65
169
|
const lines = [];
|
|
66
|
-
if (diff.versionChange)
|
|
170
|
+
if (diff.versionChange) {
|
|
67
171
|
lines.push(`Marwes ${diff.versionChange.before} -> ${diff.versionChange.after}`);
|
|
68
|
-
|
|
172
|
+
}
|
|
173
|
+
const cut = (items) => items.length > limit
|
|
174
|
+
? `${items.slice(0, limit).join(", ")} and ${items.length - limit} more`
|
|
175
|
+
: items.join(", ");
|
|
69
176
|
for (const s of diff.sets) {
|
|
70
|
-
if (s.setAdded)
|
|
177
|
+
if (s.setAdded) {
|
|
71
178
|
lines.push(`${s.set}: new set with ${s.added.length} tokens`);
|
|
72
|
-
|
|
179
|
+
}
|
|
180
|
+
else if (s.setRemoved) {
|
|
73
181
|
lines.push(`${s.set}: set no longer expected (${s.removed.length} tokens)`);
|
|
182
|
+
}
|
|
74
183
|
else {
|
|
75
|
-
if (s.added.length)
|
|
184
|
+
if (s.added.length) {
|
|
76
185
|
lines.push(`${s.set}: ${s.added.length} added: ${cut(s.added)}`);
|
|
77
|
-
|
|
186
|
+
}
|
|
187
|
+
if (s.removed.length) {
|
|
78
188
|
lines.push(`${s.set}: ${s.removed.length} removed: ${cut(s.removed)}`);
|
|
79
|
-
|
|
189
|
+
}
|
|
190
|
+
if (s.changed.length) {
|
|
80
191
|
lines.push(`${s.set}: ${s.changed.length} changed: ${cut(s.changed.map((c) => `${c.name} ${c.before} -> ${c.after}`))}`);
|
|
192
|
+
}
|
|
81
193
|
}
|
|
82
194
|
}
|
|
83
|
-
if (diff.unmapped.added.length)
|
|
195
|
+
if (diff.unmapped.added.length) {
|
|
84
196
|
lines.push(`NEW Marwes variables that are not tokens (the design file does not know them yet; review, then extend the adapter or accept): ${cut(diff.unmapped.added)}`);
|
|
85
|
-
|
|
197
|
+
}
|
|
198
|
+
if (diff.unmapped.removed.length) {
|
|
86
199
|
lines.push(`Marwes variables that are no longer provided: ${cut(diff.unmapped.removed)}`);
|
|
87
|
-
|
|
200
|
+
}
|
|
201
|
+
for (const t of diff.themes.added) {
|
|
88
202
|
lines.push(`theme ${t}: new`);
|
|
89
|
-
|
|
203
|
+
}
|
|
204
|
+
for (const t of diff.themes.removed) {
|
|
90
205
|
lines.push(`theme ${t}: no longer expected`);
|
|
91
|
-
|
|
206
|
+
}
|
|
207
|
+
for (const t of diff.themes.changed) {
|
|
92
208
|
lines.push(`theme ${t.theme}: sets ${t.before.join(" + ")} -> ${t.after.join(" + ")}`);
|
|
209
|
+
}
|
|
93
210
|
return lines;
|
|
94
211
|
}
|
|
95
212
|
/** Explicitly classify every legacy set; names cannot establish ownership. Original fields survive migration. */
|
|
96
213
|
export function migrateTokenSnapshot(raw, ownership) {
|
|
97
214
|
const source = raw;
|
|
98
|
-
if (!source ||
|
|
215
|
+
if (!source ||
|
|
216
|
+
source.schemaVersion !== 1 ||
|
|
217
|
+
!Array.isArray(source.sets) ||
|
|
218
|
+
!Array.isArray(source.themes) ||
|
|
219
|
+
!Array.isArray(source.unmapped) ||
|
|
220
|
+
typeof source.marwesVersion !== "string" ||
|
|
221
|
+
typeof source.project !== "string") {
|
|
99
222
|
throw new Error("Migration requires a readable legacy schema-1 token snapshot; unsupported majors need producer guidance");
|
|
100
|
-
|
|
223
|
+
}
|
|
224
|
+
if (!ownership ||
|
|
225
|
+
typeof ownership !== "object" ||
|
|
226
|
+
Array.isArray(ownership) ||
|
|
227
|
+
Object.keys(ownership).length !== source.sets.length ||
|
|
228
|
+
source.sets.some((set) => ![
|
|
229
|
+
"adapter",
|
|
230
|
+
"project",
|
|
231
|
+
].includes(ownership[set.name] ?? "")) ||
|
|
232
|
+
new Set(source.sets.map((set) => set.name)).size !== source.sets.length) {
|
|
101
233
|
throw new Error("Explicit adapter/project ownership is required for every legacy set, with no extra names");
|
|
234
|
+
}
|
|
102
235
|
for (const set of source.sets) {
|
|
103
|
-
if (!set || typeof set.name !== "string" || !Array.isArray(set.tokens))
|
|
236
|
+
if (!set || typeof set.name !== "string" || !Array.isArray(set.tokens)) {
|
|
104
237
|
throw new Error("Malformed legacy set");
|
|
238
|
+
}
|
|
105
239
|
const names = new Set();
|
|
106
240
|
for (const token of set.tokens) {
|
|
107
241
|
validateEditableToken(token);
|
|
108
|
-
if (names.has(token.name))
|
|
242
|
+
if (names.has(token.name)) {
|
|
109
243
|
throw new Error("Duplicate legacy token");
|
|
244
|
+
}
|
|
110
245
|
names.add(token.name);
|
|
111
246
|
}
|
|
112
247
|
}
|
|
113
|
-
if (source.unmapped.some(value => typeof value !== "string") ||
|
|
248
|
+
if (source.unmapped.some((value) => typeof value !== "string") ||
|
|
249
|
+
source.themes.some((theme) => !theme ||
|
|
250
|
+
typeof theme.group !== "string" ||
|
|
251
|
+
typeof theme.name !== "string" ||
|
|
252
|
+
!Array.isArray(theme.sets) ||
|
|
253
|
+
theme.sets.some((name) => !source.sets.some((set) => set.name === name)))) {
|
|
114
254
|
throw new Error("Malformed legacy themes/unmapped variables");
|
|
115
|
-
|
|
255
|
+
}
|
|
256
|
+
const migrated = {
|
|
257
|
+
...structuredClone(source),
|
|
258
|
+
schemaVersion: 2,
|
|
259
|
+
ownership: structuredClone(ownership),
|
|
260
|
+
};
|
|
116
261
|
const { marwesVersion, project, sets, themes, unmapped } = migrated;
|
|
117
|
-
migrated.hash = createHash("sha256")
|
|
262
|
+
migrated.hash = createHash("sha256")
|
|
263
|
+
.update(canonical({
|
|
264
|
+
marwesVersion,
|
|
265
|
+
project,
|
|
266
|
+
sets,
|
|
267
|
+
themes,
|
|
268
|
+
unmapped,
|
|
269
|
+
ownership: migrated.ownership,
|
|
270
|
+
}))
|
|
271
|
+
.digest("hex");
|
|
118
272
|
return migrated;
|
|
119
273
|
}
|
|
120
274
|
export const tokenSnapshotSchema = {
|
|
121
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
122
|
-
|
|
123
|
-
|
|
275
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
276
|
+
$id: "urn:penpair:schema:v2:adapter-token-snapshot",
|
|
277
|
+
type: "object",
|
|
278
|
+
required: [
|
|
279
|
+
"schemaVersion",
|
|
280
|
+
"ownership",
|
|
281
|
+
"marwesVersion",
|
|
282
|
+
"generatedBy",
|
|
283
|
+
"project",
|
|
284
|
+
"hash",
|
|
285
|
+
"sets",
|
|
286
|
+
"themes",
|
|
287
|
+
"unmapped",
|
|
288
|
+
],
|
|
289
|
+
properties: {
|
|
290
|
+
schemaVersion: {
|
|
291
|
+
const: 2,
|
|
292
|
+
},
|
|
293
|
+
ownership: {
|
|
294
|
+
type: "object",
|
|
295
|
+
additionalProperties: {
|
|
296
|
+
enum: [
|
|
297
|
+
"adapter",
|
|
298
|
+
"project",
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
marwesVersion: {
|
|
303
|
+
type: "string",
|
|
304
|
+
},
|
|
305
|
+
generatedBy: {
|
|
306
|
+
type: "string",
|
|
307
|
+
},
|
|
308
|
+
project: {
|
|
309
|
+
type: "string",
|
|
310
|
+
},
|
|
311
|
+
hash: {
|
|
312
|
+
type: "string",
|
|
313
|
+
pattern: "^[a-f0-9]{64}$",
|
|
314
|
+
},
|
|
315
|
+
sets: {
|
|
316
|
+
type: "array",
|
|
317
|
+
},
|
|
318
|
+
themes: {
|
|
319
|
+
type: "array",
|
|
320
|
+
},
|
|
321
|
+
unmapped: {
|
|
322
|
+
type: "array",
|
|
323
|
+
items: {
|
|
324
|
+
type: "string",
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
additionalProperties: true,
|
|
124
329
|
};
|
package/dist/theme-input.js
CHANGED
|
@@ -11,14 +11,22 @@ export async function loadThemeInput(root, reference) {
|
|
|
11
11
|
source = await readFile(file);
|
|
12
12
|
}
|
|
13
13
|
catch (error) {
|
|
14
|
-
if (error.code === "ENOENT")
|
|
14
|
+
if (error.code === "ENOENT") {
|
|
15
15
|
throw new Error(`The theme file ${reference.module} does not exist (marwes.themes in penpair.config.json).`);
|
|
16
|
+
}
|
|
16
17
|
throw error;
|
|
17
18
|
}
|
|
18
|
-
const jiti = createJiti(file, {
|
|
19
|
+
const jiti = createJiti(file, {
|
|
20
|
+
interopDefault: true,
|
|
21
|
+
moduleCache: false,
|
|
22
|
+
});
|
|
19
23
|
const loaded = (await jiti.import(file));
|
|
20
24
|
const input = loaded[reference.export];
|
|
21
|
-
if (typeof input !== "object" || input === null)
|
|
25
|
+
if (typeof input !== "object" || input === null) {
|
|
22
26
|
throw new Error(`${reference.module} has no object export named ${reference.export}.`);
|
|
23
|
-
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
input: input,
|
|
30
|
+
fileHash: createHash("sha256").update(source).digest("hex").slice(0, 12),
|
|
31
|
+
};
|
|
24
32
|
}
|
package/dist/theme-lint.js
CHANGED
|
@@ -1,59 +1,145 @@
|
|
|
1
1
|
export function readThemeLint(value) {
|
|
2
|
-
if (value === undefined)
|
|
2
|
+
if (value === undefined) {
|
|
3
3
|
return {};
|
|
4
|
-
|
|
4
|
+
}
|
|
5
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5
6
|
throw new Error("marwes.themeLint must be an object");
|
|
7
|
+
}
|
|
6
8
|
const v = value;
|
|
7
|
-
if (Object.keys(v).some(k => ![
|
|
9
|
+
if (Object.keys(v).some((k) => ![
|
|
10
|
+
"hueThresholdDegrees",
|
|
11
|
+
"suppressions",
|
|
12
|
+
].includes(k)) ||
|
|
13
|
+
(v.hueThresholdDegrees !== undefined &&
|
|
14
|
+
(!Number.isFinite(v.hueThresholdDegrees) ||
|
|
15
|
+
v.hueThresholdDegrees < 0 ||
|
|
16
|
+
v.hueThresholdDegrees > 180))) {
|
|
8
17
|
throw new Error("Invalid theme lint threshold/settings");
|
|
9
|
-
|
|
18
|
+
}
|
|
19
|
+
if (v.suppressions !== undefined &&
|
|
20
|
+
(!Array.isArray(v.suppressions) ||
|
|
21
|
+
v.suppressions.length > 1000 ||
|
|
22
|
+
v.suppressions.some((s) => !s ||
|
|
23
|
+
![
|
|
24
|
+
"light",
|
|
25
|
+
"dark",
|
|
26
|
+
].includes(s.theme) ||
|
|
27
|
+
typeof s.token !== "string" ||
|
|
28
|
+
!s.token ||
|
|
29
|
+
typeof s.reason !== "string" ||
|
|
30
|
+
!s.reason.trim() ||
|
|
31
|
+
s.reason.length > 1000))) {
|
|
10
32
|
throw new Error("Theme lint suppressions need exact mode/token and a reason");
|
|
33
|
+
}
|
|
11
34
|
return structuredClone(v);
|
|
12
35
|
}
|
|
13
36
|
function hue(value) {
|
|
14
|
-
if (typeof value !== "string" || !/^#[\da-f]{6}(?:ff)?$/i.test(value))
|
|
37
|
+
if (typeof value !== "string" || !/^#[\da-f]{6}(?:ff)?$/i.test(value)) {
|
|
15
38
|
return;
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
39
|
+
}
|
|
40
|
+
const [r, g, b] = [
|
|
41
|
+
1,
|
|
42
|
+
3,
|
|
43
|
+
5,
|
|
44
|
+
].map((i) => parseInt(value.slice(i, i + 2), 16) / 255);
|
|
45
|
+
const max = Math.max(r, g, b);
|
|
46
|
+
const min = Math.min(r, g, b);
|
|
47
|
+
const delta = max - min;
|
|
48
|
+
if (!delta) {
|
|
49
|
+
return {
|
|
50
|
+
hue: 0,
|
|
51
|
+
saturation: 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
20
54
|
let h = max === r ? ((g - b) / delta) % 6 : max === g ? (b - r) / delta + 2 : (r - g) / delta + 4;
|
|
21
|
-
h = ((h * 60) % 360 + 360) % 360;
|
|
22
|
-
return {
|
|
55
|
+
h = (((h * 60) % 360) + 360) % 360;
|
|
56
|
+
return {
|
|
57
|
+
hue: h,
|
|
58
|
+
saturation: max === 0 ? 0 : delta / max,
|
|
59
|
+
};
|
|
23
60
|
}
|
|
24
61
|
function colors(snapshot, mode) {
|
|
25
|
-
const result = new Map()
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
62
|
+
const result = new Map();
|
|
63
|
+
const theme = snapshot.themes.find((t) => t.group === "Mode" && t.name.toLowerCase() === mode);
|
|
64
|
+
for (const name of theme?.sets ?? []) {
|
|
65
|
+
for (const token of snapshot.sets.find((s) => s.name === name)?.tokens ?? []) {
|
|
66
|
+
if (token.type === "color") {
|
|
29
67
|
result.set(token.name, token.value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
30
71
|
return result;
|
|
31
72
|
}
|
|
32
73
|
export function lintThemeFamily(defaults, project, rawSettings = {}) {
|
|
33
|
-
const settings = readThemeLint(rawSettings)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
74
|
+
const settings = readThemeLint(rawSettings);
|
|
75
|
+
const warnings = [];
|
|
76
|
+
const unknown = [];
|
|
77
|
+
for (const mode of [
|
|
78
|
+
"light",
|
|
79
|
+
"dark",
|
|
80
|
+
]) {
|
|
81
|
+
const base = colors(defaults, mode);
|
|
82
|
+
const current = colors(project, mode);
|
|
83
|
+
const anchor = "mw-color-primary-base";
|
|
84
|
+
const primary = current.get(anchor);
|
|
85
|
+
const original = base.get(anchor);
|
|
86
|
+
if (primary === undefined || original === undefined || primary === original) {
|
|
37
87
|
continue;
|
|
88
|
+
}
|
|
38
89
|
const anchorHue = hue(primary);
|
|
39
90
|
if (!anchorHue || anchorHue.saturation < 0.15) {
|
|
40
|
-
unknown.push({
|
|
91
|
+
unknown.push({
|
|
92
|
+
theme: mode,
|
|
93
|
+
token: anchor,
|
|
94
|
+
reason: "Primary is transparent, non-literal or achromatic; hue comparison is not meaningful",
|
|
95
|
+
});
|
|
41
96
|
continue;
|
|
42
97
|
}
|
|
43
98
|
for (const [token, value] of current) {
|
|
44
|
-
if (!/^mw-color-(?:border-brand|text-(?:brand|link)|focus(?:-.+)?|surface-brand|secondary(?:-.+)?)$/.test(token) ||
|
|
99
|
+
if (!/^mw-color-(?:border-brand|text-(?:brand|link)|focus(?:-.+)?|surface-brand|secondary(?:-.+)?)$/.test(token) ||
|
|
100
|
+
value !== base.get(token)) {
|
|
45
101
|
continue;
|
|
102
|
+
}
|
|
46
103
|
const other = hue(value);
|
|
47
104
|
if (!other || other.saturation < 0.15) {
|
|
48
|
-
unknown.push({
|
|
105
|
+
unknown.push({
|
|
106
|
+
theme: mode,
|
|
107
|
+
token,
|
|
108
|
+
reason: "Default sibling hue is unavailable or achromatic",
|
|
109
|
+
});
|
|
49
110
|
continue;
|
|
50
111
|
}
|
|
51
|
-
const delta = Math.abs(anchorHue.hue - other.hue)
|
|
52
|
-
|
|
112
|
+
const delta = Math.abs(anchorHue.hue - other.hue);
|
|
113
|
+
const distance = Math.min(delta, 360 - delta);
|
|
114
|
+
if (distance < (settings.hueThresholdDegrees ?? 30)) {
|
|
53
115
|
continue;
|
|
54
|
-
|
|
55
|
-
|
|
116
|
+
}
|
|
117
|
+
const suppression = settings.suppressions?.find((s) => s.theme === mode && s.token === token)?.reason;
|
|
118
|
+
warnings.push({
|
|
119
|
+
theme: mode,
|
|
120
|
+
token,
|
|
121
|
+
default: base.get(token),
|
|
122
|
+
actual: value,
|
|
123
|
+
primary,
|
|
124
|
+
hueDistance: distance,
|
|
125
|
+
...(suppression
|
|
126
|
+
? {
|
|
127
|
+
suppression,
|
|
128
|
+
}
|
|
129
|
+
: {}),
|
|
130
|
+
});
|
|
56
131
|
}
|
|
57
132
|
}
|
|
58
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
schemaVersion: 1,
|
|
135
|
+
kind: "theme-family-lint",
|
|
136
|
+
advisory: true,
|
|
137
|
+
warnings,
|
|
138
|
+
unknown,
|
|
139
|
+
assumptions: [
|
|
140
|
+
"Hue distance on opaque literal colors is a heuristic, not a brand or accessibility rule.",
|
|
141
|
+
"Unchanged family defaults may be intentional; retain a reasoned suppression rather than rewriting them automatically.",
|
|
142
|
+
],
|
|
143
|
+
changed: false,
|
|
144
|
+
};
|
|
59
145
|
}
|
package/dist/tokens.d.ts
CHANGED