@penpair/marwes-ui 0.2.0 → 0.3.1

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.
@@ -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 (![1, 2].includes(snapshot.schemaVersion) || !Array.isArray(snapshot.sets) || !Array.isArray(snapshot.themes) || typeof snapshot.hash !== "string" || typeof snapshot.project !== "string")
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
- if (snapshot.schemaVersion === 2 && (!snapshot.ownership || Object.keys(snapshot.ownership).length !== snapshot.sets.length || snapshot.sets.some(set => !["adapter", "project"].includes(snapshot.ownership?.[set.name] ?? ""))))
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), { recursive: true });
24
- await atomicWriteFile(target, `${JSON.stringify(snapshot, null, 2)}\n`, { replace: true });
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) => (typeof token.value === "string" ? token.value : canonical(token.value));
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) => [s.name, s]));
31
- const next = new Map(after.sets.map((s) => [s.name, s]));
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) => [t.name, t]));
35
- const newTokens = new Map(set.tokens.map((t) => [t.name, t]));
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 ? {} : { setAdded: true }),
39
- added: [...newTokens.keys()].filter((n) => !oldTokens.has(n)),
40
- removed: [...oldTokens.keys()].filter((n) => !newTokens.has(n)),
41
- changed: [...newTokens.entries()].filter(([n, t]) => oldTokens.has(n) && show(oldTokens.get(n)) !== show(t)).map(([n, t]) => ({ name: n, before: show(oldTokens.get(n)), after: show(t) })),
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
- for (const [name, old] of previous)
47
- if (!next.has(name))
48
- sets.push({ set: name, setRemoved: true, added: [], removed: old.tokens.map((t) => t.name), changed: [] });
49
- const oldThemes = new Map((before?.themes ?? []).map((t) => [`${t.group}/${t.name}`, t.sets]));
50
- const newThemes = new Map(after.themes.map((t) => [`${t.group}/${t.name}`, t.sets]));
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: [...newThemes.keys()].filter((k) => !oldThemes.has(k)),
53
- removed: [...oldThemes.keys()].filter((k) => !newThemes.has(k)),
54
- changed: [...newThemes.entries()].filter(([k, s]) => oldThemes.has(k) && canonical(oldThemes.get(k)) !== canonical(s)).map(([k, s]) => ({ theme: k, before: oldThemes.get(k), after: s })),
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 = { added: [...isUnmapped].filter((v) => !wasUnmapped.has(v)), removed: [...wasUnmapped].filter((v) => !isUnmapped.has(v)) };
59
- const versionChange = before && before.marwesVersion !== after.marwesVersion ? { before: before.marwesVersion, after: after.marwesVersion } : undefined;
60
- const identical = !!before && !versionChange && sets.length === 0 && !themes.added.length && !themes.removed.length && !themes.changed.length && !unmapped.added.length && !unmapped.removed.length;
61
- return { identical, ...(versionChange ? { versionChange } : {}), sets, themes, unmapped };
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
- const cut = (items) => (items.length > limit ? `${items.slice(0, limit).join(", ")} and ${items.length - limit} more` : items.join(", "));
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
- else if (s.setRemoved)
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
- if (s.removed.length)
186
+ }
187
+ if (s.removed.length) {
78
188
  lines.push(`${s.set}: ${s.removed.length} removed: ${cut(s.removed)}`);
79
- if (s.changed.length)
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
- if (diff.unmapped.removed.length)
197
+ }
198
+ if (diff.unmapped.removed.length) {
86
199
  lines.push(`Marwes variables that are no longer provided: ${cut(diff.unmapped.removed)}`);
87
- for (const t of diff.themes.added)
200
+ }
201
+ for (const t of diff.themes.added) {
88
202
  lines.push(`theme ${t}: new`);
89
- for (const t of diff.themes.removed)
203
+ }
204
+ for (const t of diff.themes.removed) {
90
205
  lines.push(`theme ${t}: no longer expected`);
91
- for (const t of diff.themes.changed)
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 || source.schemaVersion !== 1 || !Array.isArray(source.sets) || !Array.isArray(source.themes) || !Array.isArray(source.unmapped) || typeof source.marwesVersion !== "string" || typeof source.project !== "string")
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
- if (!ownership || typeof ownership !== "object" || Array.isArray(ownership) || Object.keys(ownership).length !== source.sets.length || source.sets.some(set => !["adapter", "project"].includes(ownership[set.name] ?? "")) || new Set(source.sets.map(set => set.name)).size !== source.sets.length)
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") || source.themes.some(theme => !theme || typeof theme.group !== "string" || typeof theme.name !== "string" || !Array.isArray(theme.sets) || theme.sets.some(name => !source.sets.some(set => set.name === name))))
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
- const migrated = { ...structuredClone(source), schemaVersion: 2, ownership: structuredClone(ownership) };
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").update(canonical({ marwesVersion, project, sets, themes, unmapped, ownership: migrated.ownership })).digest("hex");
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", $id: "urn:penpair:schema:v2:adapter-token-snapshot", type: "object",
122
- required: ["schemaVersion", "ownership", "marwesVersion", "generatedBy", "project", "hash", "sets", "themes", "unmapped"],
123
- properties: { schemaVersion: { const: 2 }, ownership: { type: "object", additionalProperties: { enum: ["adapter", "project"] } }, marwesVersion: { type: "string" }, generatedBy: { type: "string" }, project: { type: "string" }, hash: { type: "string", pattern: "^[a-f0-9]{64}$" }, sets: { type: "array" }, themes: { type: "array" }, unmapped: { type: "array", items: { type: "string" } } }, additionalProperties: true,
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
  };
@@ -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, { interopDefault: true, moduleCache: false });
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
- return { input: input, fileHash: createHash("sha256").update(source).digest("hex").slice(0, 12) };
27
+ }
28
+ return {
29
+ input: input,
30
+ fileHash: createHash("sha256").update(source).digest("hex").slice(0, 12),
31
+ };
24
32
  }
@@ -1,59 +1,145 @@
1
1
  export function readThemeLint(value) {
2
- if (value === undefined)
2
+ if (value === undefined) {
3
3
  return {};
4
- if (!value || typeof value !== "object" || Array.isArray(value))
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 => !["hueThresholdDegrees", "suppressions"].includes(k)) || v.hueThresholdDegrees !== undefined && (!Number.isFinite(v.hueThresholdDegrees) || v.hueThresholdDegrees < 0 || v.hueThresholdDegrees > 180))
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
- if (v.suppressions !== undefined && (!Array.isArray(v.suppressions) || v.suppressions.length > 1000 || v.suppressions.some(s => !s || !["light", "dark"].includes(s.theme) || typeof s.token !== "string" || !s.token || typeof s.reason !== "string" || !s.reason.trim() || s.reason.length > 1000)))
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
- const [r, g, b] = [1, 3, 5].map(i => parseInt(value.slice(i, i + 2), 16) / 255);
17
- const max = Math.max(r, g, b), min = Math.min(r, g, b), delta = max - min;
18
- if (!delta)
19
- return { hue: 0, saturation: 0 };
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 { hue: h, saturation: max === 0 ? 0 : delta / max };
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(), theme = snapshot.themes.find(t => t.group === "Mode" && t.name.toLowerCase() === mode);
26
- for (const name of theme?.sets ?? [])
27
- for (const token of snapshot.sets.find(s => s.name === name)?.tokens ?? [])
28
- if (token.type === "color")
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), warnings = [], unknown = [];
34
- for (const mode of ["light", "dark"]) {
35
- const base = colors(defaults, mode), current = colors(project, mode), anchor = "mw-color-primary-base", primary = current.get(anchor), original = base.get(anchor);
36
- if (primary === undefined || original === undefined || primary === original)
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({ theme: mode, token: anchor, reason: "Primary is transparent, non-literal or achromatic; hue comparison is not meaningful" });
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) || value !== base.get(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({ theme: mode, token, reason: "Default sibling hue is unavailable or achromatic" });
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), distance = Math.min(delta, 360 - delta);
52
- if (distance < (settings.hueThresholdDegrees ?? 30))
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
- const suppression = settings.suppressions?.find(s => s.theme === mode && s.token === token)?.reason;
55
- warnings.push({ theme: mode, token, default: base.get(token), actual: value, primary, hueDistance: distance, ...(suppression ? { suppression } : {}) });
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 { schemaVersion: 1, kind: "theme-family-lint", advisory: true, warnings, unknown, assumptions: ["Hue distance on opaque literal colors is a heuristic, not a brand or accessibility rule.", "Unchanged family defaults may be intentional; retain a reasoned suppression rather than rewriting them automatically."], changed: false };
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
@@ -1,5 +1,5 @@
1
1
  import { tokenTypes } from "@penpair/core";
2
- export type PenpotTokenType = typeof tokenTypes[number];
2
+ export type PenpotTokenType = (typeof tokenTypes)[number];
3
3
  export interface PenpotToken {
4
4
  name: string;
5
5
  type: PenpotTokenType;