@conductor-by-89soone/tokens 0.0.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.
@@ -0,0 +1,1510 @@
1
+ import { resolve, dirname, join } from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import { mkdirSync, writeFileSync, rmSync, renameSync, existsSync, readFileSync } from 'fs';
4
+
5
+ // src/contrast-cli.ts
6
+
7
+ // src/build/errors.ts
8
+ var EXIT_CODES = {
9
+ "TOK-CYCLE": 1,
10
+ "TOK-UNKNOWN-REF": 1,
11
+ "TOK-DEPTH": 1,
12
+ "TOK-TIER": 1,
13
+ "TOK-TIER-MISSING": 1,
14
+ "TOK-DUPLICATE-KEY": 1,
15
+ "TOK-ICON": 1,
16
+ "TOK-GROUP-SIZE": 1,
17
+ "TOK-USAGE": 1,
18
+ "TOK-VALUE": 1,
19
+ "TOK-Z-DUPLICATE": 1,
20
+ "TOK-PREFIX": 2,
21
+ "TOK-NAME-COLLISION": 2,
22
+ "TOK-MEDIA-VAR": 2,
23
+ "TOK-CP-DUPLICATE-ID": 1,
24
+ "TOK-CP-UNKNOWN-KEY": 1,
25
+ "TOK-CP-DECORATIVE": 1,
26
+ "TOK-CP-USAGE": 1,
27
+ "TOK-CP-COLOR": 1,
28
+ "TOK-CP-ALPHA": 1,
29
+ "TOK-LITERAL": 1,
30
+ "TOK-ARG": 3
31
+ };
32
+ var TokenBuildError = class extends Error {
33
+ code;
34
+ exitCode;
35
+ /** Indented lines printed under the summary: offending keys, cycle paths, hints. */
36
+ details;
37
+ constructor(code, summary, details = []) {
38
+ super(`error[${code}]: ${summary}`);
39
+ this.name = "TokenBuildError";
40
+ this.code = code;
41
+ this.exitCode = EXIT_CODES[code];
42
+ this.details = details;
43
+ }
44
+ /** The full stderr rendering, summary line first. */
45
+ format() {
46
+ return [this.message, ...this.details.map((line) => ` ${line}`)].join("\n");
47
+ }
48
+ };
49
+ function isUnchanged(path, contents) {
50
+ return existsSync(path) && readFileSync(path, "utf8") === contents;
51
+ }
52
+ function writeArtifacts(outDir, artifacts) {
53
+ mkdirSync(outDir, { recursive: true });
54
+ const staged = [];
55
+ try {
56
+ for (const [name, contents] of artifacts) {
57
+ const final = join(outDir, name);
58
+ if (isUnchanged(final, contents)) continue;
59
+ const temporary = `${final}.${process.pid}.tmp`;
60
+ writeFileSync(temporary, contents, "utf8");
61
+ staged.push({ temporary, final, name });
62
+ }
63
+ } catch (error) {
64
+ for (const { temporary } of staged) rmSync(temporary, { force: true });
65
+ throw error;
66
+ }
67
+ for (const { temporary, final } of staged) renameSync(temporary, final);
68
+ return staged.map(({ name }) => name);
69
+ }
70
+
71
+ // src/contrast/args.ts
72
+ var USAGE = "usage: conductor-check-contrast [--theme <dark|light|all>] [--report] [--out <dir>]";
73
+ function parseContrastArgs(argv) {
74
+ let theme = "all";
75
+ let report = false;
76
+ let outDir = "dist";
77
+ const requireValue = (flag, next) => {
78
+ if (next === void 0 || next.startsWith("--")) {
79
+ throw new TokenBuildError("TOK-ARG", `\`${flag}\` needs a value`, [USAGE]);
80
+ }
81
+ return next;
82
+ };
83
+ for (let index = 0; index < argv.length; index += 1) {
84
+ const arg = argv[index];
85
+ switch (arg) {
86
+ case "--report":
87
+ report = true;
88
+ break;
89
+ case "--theme":
90
+ theme = requireValue("--theme", argv[index + 1]);
91
+ index += 1;
92
+ break;
93
+ case "--out":
94
+ outDir = requireValue("--out", argv[index + 1]);
95
+ index += 1;
96
+ break;
97
+ default:
98
+ throw new TokenBuildError("TOK-ARG", `unrecognised argument \`${arg}\``, [USAGE]);
99
+ }
100
+ }
101
+ return { theme, report, outDir };
102
+ }
103
+ function selectThemes(available, requested) {
104
+ if (requested === "all") return available;
105
+ const match = available.filter((theme) => theme.theme === requested);
106
+ if (match.length > 0) return match;
107
+ throw new TokenBuildError("TOK-ARG", `theme \`${requested}\` is not defined`, [
108
+ `available themes: ${available.map((theme) => theme.theme).join(", ")}`,
109
+ USAGE
110
+ ]);
111
+ }
112
+
113
+ // src/schema.ts
114
+ var MAX_REFERENCE_DEPTH = 10;
115
+ function stripBraces(reference) {
116
+ const trimmed = reference.trim();
117
+ return trimmed.startsWith("{") && trimmed.endsWith("}") ? trimmed.slice(1, -1).trim() : trimmed;
118
+ }
119
+
120
+ // src/build/reference.ts
121
+ function buildTokenIndex(tokens) {
122
+ const index = /* @__PURE__ */ new Map();
123
+ const duplicates = [];
124
+ for (const token of tokens) {
125
+ if (index.has(token.key)) duplicates.push(token.key);
126
+ else index.set(token.key, token);
127
+ }
128
+ if (duplicates.length > 0) {
129
+ throw new TokenBuildError(
130
+ "TOK-DUPLICATE-KEY",
131
+ `${duplicates.length} token key(s) declared more than once`,
132
+ [...duplicates, "hint: a token key is unique across primitives, palette and components."]
133
+ );
134
+ }
135
+ return index;
136
+ }
137
+ function resolveReferenceTarget(index, reference) {
138
+ const key = stripBraces(reference);
139
+ if (index.has(key)) return key;
140
+ const defaulted = `${key}.DEFAULT`;
141
+ return index.has(defaulted) ? defaulted : void 0;
142
+ }
143
+ function formatCycle(path) {
144
+ return path.join(" \u2192 ");
145
+ }
146
+ function resolveTokens(index) {
147
+ const values2 = /* @__PURE__ */ new Map();
148
+ const edges = [];
149
+ function resolve2(key, chain) {
150
+ const cached = values2.get(key);
151
+ if (cached !== void 0) return cached;
152
+ const cycleStart = chain.indexOf(key);
153
+ if (cycleStart !== -1) {
154
+ const path = [...chain.slice(cycleStart), key];
155
+ throw new TokenBuildError("TOK-CYCLE", "circular token reference detected", [
156
+ formatCycle(path),
157
+ "hint: break the cycle; a token cannot resolve through itself."
158
+ ]);
159
+ }
160
+ if (chain.length > MAX_REFERENCE_DEPTH) {
161
+ throw new TokenBuildError(
162
+ "TOK-DEPTH",
163
+ `token reference chain exceeds the maximum depth of ${MAX_REFERENCE_DEPTH}`,
164
+ [formatCycle([...chain, key]), "hint: flatten the chain or point at a literal."]
165
+ );
166
+ }
167
+ const token = index.get(key);
168
+ const nextChain = [...chain, key];
169
+ const depth = chain.length;
170
+ const resolveEdge = (reference) => {
171
+ const target = resolveReferenceTarget(index, reference);
172
+ if (target === void 0) {
173
+ throw new TokenBuildError("TOK-UNKNOWN-REF", "token reference points at an unknown key", [
174
+ `from: ${key}`,
175
+ `to: ${stripBraces(reference)}`,
176
+ "hint: check the spelling, or define the missing token."
177
+ ]);
178
+ }
179
+ const resolvedValue = resolve2(target, nextChain);
180
+ edges.push({ from: key, to: target, depth, resolvedValue });
181
+ return resolvedValue;
182
+ };
183
+ let resolved;
184
+ if (token.alias !== void 0) {
185
+ resolved = resolveEdge(token.alias);
186
+ } else if (typeof token.value === "number") {
187
+ resolved = String(token.value);
188
+ } else if (typeof token.value === "string") {
189
+ resolved = token.value.replace(
190
+ /\{([^{}]+)\}/g,
191
+ (_match, reference) => resolveEdge(reference)
192
+ );
193
+ } else {
194
+ throw new TokenBuildError("TOK-VALUE", "token declares neither `value` nor `alias`", [
195
+ `key: ${key}`,
196
+ "hint: a primitive needs a literal `value`; a semantic or component token needs one of the two."
197
+ ]);
198
+ }
199
+ values2.set(key, resolved);
200
+ return resolved;
201
+ }
202
+ for (const key of index.keys()) resolve2(key, []);
203
+ return { values: values2, edges };
204
+ }
205
+
206
+ // src/contrast-pairs.ts
207
+ var CONTRAST_THRESHOLDS = {
208
+ body: 4.5,
209
+ large: 3,
210
+ nonText: 3
211
+ };
212
+ var BOTH_THEMES = ["dark", "light"];
213
+ function pair(id, foreground, background, usage, note) {
214
+ return { id, foreground, background, usage, themes: BOTH_THEMES, ...note === void 0 ? {} : { note } };
215
+ }
216
+ var textOnSurface = [
217
+ pair("CP-001", "text.primary", "surface.base", "body"),
218
+ pair("CP-002", "text.primary", "surface.raised", "body"),
219
+ pair("CP-003", "text.primary", "surface.elevated", "body"),
220
+ pair("CP-004", "text.secondary", "surface.raised", "body"),
221
+ pair("CP-005", "text.secondary", "surface.elevated", "body"),
222
+ pair("CP-006", "text.muted", "surface.base", "body"),
223
+ pair("CP-007", "text.muted", "surface.elevated", "body"),
224
+ pair("CP-008", "text.monoPayload", "surface.raised", "body")
225
+ ];
226
+ var accentPairs = [
227
+ pair("CP-009", "accent", "surface.base", "body"),
228
+ pair("CP-010", "accent", "surface.raised", "body"),
229
+ pair("CP-011", "text.inverse", "accent", "body"),
230
+ pair("CP-012", "accent.strong", "surface.base", "nonText")
231
+ ];
232
+ var correctedTokens = [
233
+ pair("CP-013", "focusRing", "surface.base", "nonText"),
234
+ pair("CP-014", "focusRing", "surface.raised", "nonText"),
235
+ pair("CP-015", "focusRing", "surface.elevated", "nonText"),
236
+ pair("CP-016", "border.control", "surface.base", "nonText"),
237
+ pair("CP-017", "border.control", "surface.raised", "nonText"),
238
+ pair("CP-018", "border.control", "surface.elevated", "nonText")
239
+ ];
240
+ var statusPairs = [
241
+ pair("CP-019", "status.running", "surface.raised", "body"),
242
+ pair("CP-020", "status.waiting", "surface.raised", "body"),
243
+ pair("CP-021", "status.success", "surface.raised", "body"),
244
+ pair("CP-022", "status.partial", "surface.raised", "body"),
245
+ pair("CP-023", "status.danger", "surface.raised", "body"),
246
+ pair("CP-024", "status.queued", "surface.raised", "nonText")
247
+ // CP-025 removed by CR-006. Do not reuse this id.
248
+ ];
249
+ var meterPairs = [
250
+ pair("CP-026", "meter.normal", "surface.raised", "body"),
251
+ pair("CP-027", "meter.warning", "surface.raised", "body"),
252
+ pair("CP-028", "meter.exceeded", "surface.raised", "body")
253
+ ];
254
+ var severityPairs = [
255
+ pair("CP-029", "badge.severity.text", "severity.read", "body"),
256
+ pair("CP-030", "badge.severity.text", "severity.write", "body"),
257
+ pair("CP-031", "badge.severity.text", "severity.destructive", "body"),
258
+ pair("CP-032", "badge.severity.text", "severity.blocked", "body")
259
+ ];
260
+ var badgePairs = [
261
+ pair("CP-033", "badge.fill.text", "status.running", "body"),
262
+ pair("CP-034", "badge.fill.text", "status.waiting", "body"),
263
+ pair("CP-035", "badge.fill.text", "status.success", "body"),
264
+ pair("CP-036", "badge.fill.text", "status.partial", "body"),
265
+ pair("CP-037", "badge.fill.text", "status.danger", "body"),
266
+ pair("CP-038", "badge.marker.text", "badge.marker.background", "body"),
267
+ pair(
268
+ "CP-039",
269
+ "status.queued",
270
+ "badge.marker.background",
271
+ "nonText",
272
+ "tokens spec 8.2 names the foreground `badge.marker.dot`; that key is a variant slot rather than a token, and it resolves to `status.queued` for the only declared variant."
273
+ )
274
+ ];
275
+ var disabledPairs = [
276
+ pair("CP-040", "text.muted", "state.disabled", "body"),
277
+ pair("CP-041", "button.policyDisabled.text", "state.disabledPolicy", "body")
278
+ ];
279
+ var contrastPairs = [
280
+ ...textOnSurface,
281
+ ...accentPairs,
282
+ ...correctedTokens,
283
+ ...statusPairs,
284
+ ...meterPairs,
285
+ ...severityPairs,
286
+ ...badgePairs,
287
+ ...disabledPairs
288
+ ];
289
+
290
+ // src/scales.ts
291
+ function semantic(key, value2, description) {
292
+ return { key, tier: "semantic", value: value2, usage: "decorative", description };
293
+ }
294
+ function aliased(key, alias, description) {
295
+ return { key, tier: "semantic", alias, usage: "decorative", description };
296
+ }
297
+ var FONT_SIZE_PX = {
298
+ "2xs": 10,
299
+ xs: 11,
300
+ sm: 12,
301
+ base: 13,
302
+ md: 14,
303
+ lg: 16,
304
+ xl: 20
305
+ };
306
+ var LINE_HEIGHT_RATIO = {
307
+ "2xs": 1.4,
308
+ xs: 1.45,
309
+ sm: 1.5,
310
+ base: 1.5,
311
+ md: 1.5,
312
+ lg: 1.5,
313
+ xl: 1.3
314
+ };
315
+ var FONT_STEPS = Object.keys(FONT_SIZE_PX);
316
+ function roundHalfUp(value2) {
317
+ return Math.floor(value2 + 0.5);
318
+ }
319
+ function lineHeightPx(step) {
320
+ return roundHalfUp(FONT_SIZE_PX[step] * LINE_HEIGHT_RATIO[step]);
321
+ }
322
+ var fontSize = FONT_STEPS.map(
323
+ (step) => semantic(`font.size.${step}`, `${FONT_SIZE_PX[step]}px`, `Font size step \`${step}\`.`)
324
+ );
325
+ var fontLineHeight = FONT_STEPS.map(
326
+ (step) => semantic(
327
+ `font.lineHeight.${step}`,
328
+ `${lineHeightPx(step)}px`,
329
+ `Line height paired with \`font.size.${step}\`.`
330
+ )
331
+ );
332
+ var space = [
333
+ aliased("space.1", "scale.4", "Tightest gap. Badge padding block."),
334
+ aliased("space.2", "scale.8", "Button padding block, button gap."),
335
+ aliased("space.3", "scale.12", "Table cell padding inline, input padding inline."),
336
+ aliased("space.4", "scale.16", "Table cell padding block."),
337
+ aliased("space.5", "scale.24", "Card padding, card grid gap, content stack gap."),
338
+ aliased("space.6", "scale.32", "Page stack gap."),
339
+ aliased("space.7", "scale.40", "Section spacing."),
340
+ aliased("space.8", "scale.48", "Widest gap.")
341
+ ];
342
+ var radius = [
343
+ aliased("radius.xs", "curve.6", "Smallest radius."),
344
+ aliased("radius.sm", "curve.9", "Form control radius."),
345
+ aliased("radius.md", "curve.12", "Button radius, compact card radius."),
346
+ aliased("radius.lg", "curve.18", "Card and overlay radius."),
347
+ aliased("radius.xl", "curve.24", "Largest radius."),
348
+ semantic("radius.pill", "9999px", "Fully rounded badge, toggle, marker and progress geometry.")
349
+ ];
350
+ var Z_LAYERS = {
351
+ base: 0,
352
+ raised: 10,
353
+ sticky: 20,
354
+ drawer: 30,
355
+ overlay: 40,
356
+ popover: 50
357
+ };
358
+ var zDescriptions = {
359
+ base: "Normal flow.",
360
+ raised: "Lifts a focus ring out of an ancestor's `overflow: hidden`.",
361
+ sticky: "The app shell top bar.",
362
+ drawer: "Side navigation and the `Drawer` panel.",
363
+ overlay: "`Dialog` and `Drawer` scrims.",
364
+ popover: "`Tooltip`, `DropdownMenu`, `Select` content, skip link."
365
+ };
366
+ var z = Object.keys(Z_LAYERS).map(
367
+ (layer) => semantic(`z.${layer}`, Z_LAYERS[layer], zDescriptions[layer])
368
+ );
369
+ var BREAKPOINT_PX = {
370
+ sm: 560,
371
+ md: 800,
372
+ lg: 1080
373
+ };
374
+ var breakpointDescriptions = {
375
+ sm: "Single-column card grid; taller form controls.",
376
+ md: "Single-column split layout; table horizontal scroll; off-canvas navigation.",
377
+ lg: "Narrower navigation; narrower minimum grid column."
378
+ };
379
+ var breakpoint = Object.keys(BREAKPOINT_PX).map(
380
+ (name) => semantic(`breakpoint.${name}`, `${BREAKPOINT_PX[name]}px`, breakpointDescriptions[name])
381
+ );
382
+ var motion = [
383
+ semantic("motion.fast", "140ms {ease.entrance}", "Colour, background and border transitions."),
384
+ semantic("motion.standard", "240ms {ease.entrance}", "Overlay and page entrance."),
385
+ semantic("motion.bounce", "300ms {ease.overshoot}", "Toggle and switch settle.")
386
+ ];
387
+ var scaleTokens = [
388
+ ...fontSize,
389
+ ...fontLineHeight,
390
+ ...space,
391
+ ...radius,
392
+ ...z,
393
+ ...breakpoint,
394
+ ...motion
395
+ ];
396
+
397
+ // src/components.ts
398
+ function component(key, source, description, usage = "decorative") {
399
+ return { key, tier: "component", usage, description, ...source };
400
+ }
401
+ var value = (literal) => ({ value: literal });
402
+ var ref = (key) => ({ alias: key });
403
+ var button = [
404
+ component("button.minHeight", value("40px"), "Minimum target height."),
405
+ component("button.minHeightCompact", value("42px"), "Minimum height below `breakpoint.sm`."),
406
+ component("button.paddingBlock", ref("space.2"), "Vertical padding."),
407
+ component("button.paddingInline", value("14px"), "Horizontal padding."),
408
+ component("button.radius", ref("radius.md"), "Corner radius."),
409
+ component("button.fontSize", ref("font.size.md"), "Label size. 14px for body-level readability."),
410
+ component("button.gap", ref("space.2"), "Gap between icon and label."),
411
+ component("button.transition", ref("motion.fast"), "State transition."),
412
+ component("button.primary.background", ref("accent.DEFAULT"), "Primary fill."),
413
+ component("button.primary.backgroundHover", ref("accent.DEFAULT"), "Primary fill stays flat on hover."),
414
+ component("button.primary.text", ref("text.inverse"), "Primary label. 5.39:1 on the fill.", "body"),
415
+ component("button.primary.border", value("transparent"), "Primary needs no edge."),
416
+ component("button.primary.shadow", ref("elevation.raised"), "Primary resting shadow."),
417
+ component("button.primary.shadowHover", ref("elevation.hover"), "Primary hover shadow."),
418
+ component("button.secondary.background", ref("surface.raised"), "Secondary fill."),
419
+ component("button.secondary.backgroundHover", ref("surface.elevated"), "Secondary hover fill."),
420
+ component("button.secondary.text", ref("text.primary"), "Secondary label.", "body"),
421
+ component(
422
+ "button.secondary.border",
423
+ ref("border.strong"),
424
+ "Secondary edge. Not `border.control`: FR-THM-005 AC-2 scopes that token to the five form controls, and a button is identified by its fill, label and focus ring."
425
+ ),
426
+ component("button.secondary.borderHover", ref("accent.DEFAULT"), "Secondary hover edge."),
427
+ component("button.ghost.background", value("transparent"), "Ghost has no fill."),
428
+ component("button.ghost.backgroundHover", ref("state.hover"), "Ghost hover wash."),
429
+ component("button.ghost.text", ref("text.muted"), "Ghost label.", "body"),
430
+ component("button.ghost.textHover", ref("text.primary"), "Ghost hover label.", "body"),
431
+ component("button.ghost.border", value("transparent"), "Ghost has no edge."),
432
+ component("button.disabled.background", ref("state.disabled"), "Disabled fill."),
433
+ component("button.disabled.text", ref("text.muted"), "Disabled label. 5.05:1 on the fill.", "body"),
434
+ component("button.disabled.border", ref("border.default"), "Disabled edge."),
435
+ component("button.policyDisabled.background", ref("state.disabledPolicy"), "Policy-blocked fill."),
436
+ component(
437
+ "button.policyDisabled.text",
438
+ ref("meter.exceeded"),
439
+ "Policy-blocked label. 5.27:1 on the dark amber fill (CP-041); `status.danger` would be 3.87:1.",
440
+ "body"
441
+ ),
442
+ component("button.policyDisabled.border", ref("status.danger"), "Policy-blocked edge."),
443
+ component("button.focusRing", ref("focusRing"), "Focus indicator.")
444
+ ];
445
+ var card = [
446
+ component("card.background", ref("surface.glass"), "Card fill."),
447
+ component("card.border", ref("border.default"), "Card edge."),
448
+ component("card.radius", ref("radius.lg"), "Card radius."),
449
+ component("card.radiusCompact", ref("radius.md"), "Card radius below `breakpoint.sm`."),
450
+ component("card.padding", ref("space.5"), "Card padding."),
451
+ component("card.paddingCompact", value("18px"), "Card padding below `breakpoint.sm`."),
452
+ component("card.shadow", ref("elevation.raised"), "Card resting shadow."),
453
+ component("card.interactive.borderHover", ref("border.strong"), "Interactive card hover edge."),
454
+ component("card.interactive.shadowHover", ref("elevation.hover"), "Interactive card hover shadow."),
455
+ component("card.interactive.translateHover", value("-2px"), "Interactive card hover lift."),
456
+ component("card.interactive.transition", ref("motion.fast"), "Interactive card transition."),
457
+ component("card.grid.minColumn", value("320px"), "`auto-fill` minimum column width."),
458
+ component("card.grid.minColumnCompact", value("260px"), "Minimum column below `breakpoint.lg`."),
459
+ component("card.grid.gap", ref("space.5"), "Grid gap.")
460
+ ];
461
+ var badge = [
462
+ component("badge.radius", value("9999px"), "Pill radius."),
463
+ component("badge.paddingBlock", ref("space.1"), "Vertical padding."),
464
+ component("badge.paddingInline", value("10px"), "Horizontal padding."),
465
+ component("badge.fontSize", ref("font.size.sm"), "Label size."),
466
+ component("badge.lineHeight", ref("font.lineHeight.sm"), "Label line height."),
467
+ component("badge.gap", value("6px"), "Gap between icon and label."),
468
+ component("badge.iconSize", value("12px"), "Icon box."),
469
+ component("badge.fill.text", ref("text.inverse"), "Label on a status fill. 5.03:1 to 9.87:1.", "body"),
470
+ component("badge.marker.background", ref("surface.raised"), "Marker-shape background."),
471
+ component("badge.marker.text", ref("text.primary"), "Marker-shape label. 15.77:1.", "body"),
472
+ component("badge.marker.border", ref("border.strong"), "Marker-shape edge."),
473
+ component("badge.marker.dotSize", value("9px"), "Status dot diameter."),
474
+ component("badge.marker.dotRing", ref("surface.raised"), "Marker dot ring."),
475
+ component("badge.marker.dotRingWidth", value("2px"), "Marker dot ring width."),
476
+ component("badge.severity.text", ref("text.primary"), "Label on a severity fill. 4.67:1 to 9.32:1.", "body")
477
+ ];
478
+ var table = [
479
+ component("table.headerText", ref("text.muted"), "Column header text.", "body"),
480
+ component("table.headerFontSize", ref("font.size.base"), "Column header size."),
481
+ component("table.headerBorder", ref("border.default"), "Header rule."),
482
+ component("table.headerBorderWidth", value("2px"), "Header rule width."),
483
+ component(
484
+ "table.cellBorder",
485
+ ref("border.default"),
486
+ "Row rule. A decorative border is correct here: row separators divide content, they do not identify a component, so WCAG 1.4.11 does not apply."
487
+ ),
488
+ component("table.cellPaddingBlock", ref("space.4"), "Cell vertical padding."),
489
+ component("table.cellPaddingInline", ref("space.3"), "Cell horizontal padding."),
490
+ component("table.rowBackgroundHover", ref("state.hover"), "Row hover wash."),
491
+ component("table.rowTransition", ref("motion.fast"), "Row hover transition."),
492
+ component("table.linkText", ref("text.secondary"), "In-cell link.", "body"),
493
+ component("table.linkTextHover", ref("text.primary"), "In-cell link hover.", "body"),
494
+ component("table.numFontFamily", ref("font.mono"), "Numeric cell font."),
495
+ component("table.scrollBreakpoint", ref("breakpoint.md"), "Below this, the table scrolls.")
496
+ ];
497
+ var timeline = [
498
+ component("timeline.background", ref("surface.timeline"), "Timeline fill."),
499
+ component("timeline.border", ref("border.default"), "Timeline edge."),
500
+ component("timeline.stepBackgroundHover", ref("state.hover"), "Interactive step hover fill."),
501
+ component("timeline.markerBackground", ref("accent.DEFAULT"), "Default marker fill."),
502
+ component("timeline.markerRing", ref("accent.glow"), "Default marker glow."),
503
+ component("timeline.markerBorder", ref("surface.timeline"), "Marker ring edge.")
504
+ ];
505
+ var codeBlock = [
506
+ component("codeBlock.background", ref("surface.timeline"), "Code block fill."),
507
+ component("codeBlock.border", ref("border.default"), "Code block edge."),
508
+ component("codeBlock.text", ref("text.monoPayload"), "Code block text.", "body")
509
+ ];
510
+ var kbd = [
511
+ component("kbd.background", ref("surface.raised"), "Keycap fill."),
512
+ component("kbd.border", ref("border.default"), "Keycap edge."),
513
+ component("kbd.borderBottom", ref("border.strong"), "Keycap lower edge."),
514
+ component("kbd.text", ref("text.secondary"), "Keycap text.", "body")
515
+ ];
516
+ var banner = [
517
+ component("banner.info.background", ref("surface.raised"), "Info banner surface."),
518
+ component("banner.info.border", ref("status.running"), "Info banner edge."),
519
+ component("banner.info.text", ref("text.secondary"), "Info banner body text.", "body"),
520
+ component("banner.warning.background", ref("surface.raised"), "Warning banner surface."),
521
+ component("banner.warning.border", ref("status.waiting"), "Warning banner edge."),
522
+ component("banner.warning.text", ref("text.secondary"), "Warning banner body text.", "body"),
523
+ component("banner.danger.background", ref("surface.raised"), "Danger banner surface."),
524
+ component("banner.danger.border", ref("status.danger"), "Danger banner edge."),
525
+ component("banner.danger.text", ref("text.secondary"), "Danger banner body text.", "body")
526
+ ];
527
+ var emptyState = [
528
+ component("emptyState.titleText", ref("text.primary"), "Empty-state title.", "body"),
529
+ component("emptyState.descriptionText", ref("text.muted"), "Empty-state description.", "body"),
530
+ component("emptyState.padding", ref("space.6"), "Empty-state padding.")
531
+ ];
532
+ var feedbackMeter = [
533
+ component("feedbackMeter.trackBackground", ref("surface.track"), "Meter track."),
534
+ component("feedbackMeter.fillNormal", ref("meter.normal"), "Normal meter fill."),
535
+ component("feedbackMeter.fillWarning", ref("meter.warning"), "Warning meter fill."),
536
+ component("feedbackMeter.fillExceeded", ref("meter.exceeded"), "Exceeded meter fill."),
537
+ component("feedbackMeter.height", value("8px"), "Meter track height.")
538
+ ];
539
+ var progressRing = [
540
+ component("progressRing.trackStroke", ref("surface.track"), "Progress-ring track."),
541
+ component("progressRing.indicatorStroke", ref("accent.DEFAULT"), "Progress-ring indicator."),
542
+ component("progressRing.labelText", ref("text.primary"), "Progress-ring label.", "body"),
543
+ component("progressRing.size", value("64px"), "Medium progress-ring diameter."),
544
+ component("progressRing.sizeCompact", value("40px"), "Small progress-ring diameter.")
545
+ ];
546
+ var spinner = [
547
+ component("spinner.trackStroke", ref("surface.track"), "Spinner track."),
548
+ component("spinner.indicatorStroke", ref("accent.DEFAULT"), "Spinner indicator."),
549
+ component("spinner.size", value("24px"), "Medium spinner diameter."),
550
+ component("spinner.sizeCompact", value("16px"), "Small spinner diameter.")
551
+ ];
552
+ var input = [
553
+ component("input.minHeight", value("40px"), "Minimum target height."),
554
+ component("input.minHeightCompact", value("42px"), "Minimum height below `breakpoint.sm`."),
555
+ component("input.background", ref("surface.base"), "Field fill."),
556
+ component("input.backgroundFocus", ref("surface.canvas"), "Focused field fill."),
557
+ component("input.text", ref("text.primary"), "Field value.", "body"),
558
+ component(
559
+ "input.placeholder",
560
+ ref("text.faint"),
561
+ "Placeholder. `text.faint` is allowed here and the dark field sits on `surface.base`, so the combination FR-THM-005 AC-3 forbids never renders."
562
+ ),
563
+ component("input.border", ref("border.control"), "Field edge. 3.23:1 on `surface.raised`."),
564
+ component("input.borderHover", ref("border.control"), "Hovered field edge."),
565
+ component("input.borderFocus", ref("accent.DEFAULT"), "Focused field edge."),
566
+ component("input.borderInvalid", ref("status.danger"), "Invalid field edge."),
567
+ component("input.radius", ref("radius.sm"), "Field radius."),
568
+ component("input.paddingBlock", value("9px"), "Field vertical padding."),
569
+ component("input.paddingInline", ref("space.3"), "Field horizontal padding."),
570
+ component("input.focusRing", ref("focusRing"), "Focus indicator."),
571
+ component("input.label.text", ref("text.secondary"), "Field label with stronger hierarchy.", "body"),
572
+ component("input.label.fontSize", ref("font.size.base"), "Field label size."),
573
+ component("input.error.text", ref("status.danger"), "Field error message.", "body"),
574
+ component("input.transition", ref("motion.fast"), "Field transition.")
575
+ ];
576
+ var overlay = [
577
+ component("overlay.scrim", ref("surface.overlay"), "Backdrop behind Dialog and Drawer."),
578
+ component("overlay.scrimBlur", value("4px"), "Backdrop blur radius."),
579
+ component("overlay.scrimZ", ref("z.overlay"), "Backdrop stacking layer."),
580
+ component("overlay.background", ref("surface.glass"), "Overlay panel fill."),
581
+ component("overlay.backgroundBlur", value("16px"), "Overlay panel backdrop blur."),
582
+ component("overlay.border", ref("border.strong"), "Overlay panel edge."),
583
+ component("overlay.radius", ref("radius.lg"), "Overlay panel radius."),
584
+ component("overlay.shadow", ref("elevation.overlay"), "Overlay panel shadow."),
585
+ component("overlay.contentZ", ref("z.popover"), "Tooltip, menu and select content layer."),
586
+ component("overlay.drawerZ", ref("z.drawer"), "Drawer panel layer."),
587
+ component("overlay.drawerBackground", ref("surface.raised"), "Drawer panel fill."),
588
+ component("overlay.drawerWidth", value("500px"), "Drawer panel width."),
589
+ component("overlay.enter", ref("motion.standard"), "Overlay entrance."),
590
+ component("overlay.exit", ref("motion.fast"), "Overlay exit.")
591
+ ];
592
+ var HEADING_MIN_PX = FONT_SIZE_PX.xl * 1.2;
593
+ var HEADING_MAX_PX = FONT_SIZE_PX.xl * 1.6;
594
+ var page = [
595
+ component(
596
+ "page.headingSize",
597
+ value(`clamp(${HEADING_MIN_PX}px, 3vw, ${HEADING_MAX_PX}px)`),
598
+ "Responsive page heading size, derived from `font.size.xl` (\xD7 1.2 and \xD7 1.6)."
599
+ ),
600
+ component(
601
+ "page.headingLineHeight",
602
+ value("1.16"),
603
+ "The only unitless line height: `clamp()` makes a px value impossible here."
604
+ ),
605
+ component("page.stackGap", ref("space.6"), "Gap between page sections."),
606
+ component("page.stackGapCompact", ref("space.5"), "Section gap below `breakpoint.sm`."),
607
+ component("page.contentStackGap", ref("space.5"), "Gap inside a content stack."),
608
+ component("page.splitBreakpoint", ref("breakpoint.md"), "Below this, a split layout stacks."),
609
+ component("page.splitAsideWidth", value("minmax(280px, 340px)"), "Split layout aside track."),
610
+ component("page.shellNavWidth", value("272px"), "App shell navigation width."),
611
+ component("page.shellNavWidthCompact", value("232px"), "Navigation width below `breakpoint.lg`."),
612
+ component("page.topbarZ", ref("z.sticky"), "Top bar stacking layer."),
613
+ component("page.navZ", ref("z.drawer"), "Navigation stacking layer."),
614
+ component("page.skipLinkZ", ref("z.popover"), "Skip link stacking layer.")
615
+ ];
616
+ var shell = [
617
+ component("appShell.navWidth", ref("page.shellNavWidth"), "AppShell desktop navigation width."),
618
+ component("appShell.mainMaxWidth", value("1480px"), "AppShell main content maximum width."),
619
+ component("appShell.overlayBackground", ref("surface.overlay"), "Mobile navigation scrim."),
620
+ component("skipLink.background", ref("accent.DEFAULT"), "Skip-link fill."),
621
+ component("skipLink.text", ref("text.inverse"), "Skip-link label."),
622
+ component("navList.background", ref("surface.raised"), "Navigation surface."),
623
+ component("navList.border", ref("border.subtle"), "Navigation surface edge."),
624
+ component("navItem.text", ref("text.muted"), "Navigation item label."),
625
+ component("navItem.textActive", ref("text.primary"), "Active navigation item label."),
626
+ component("navItem.backgroundHover", ref("state.hover"), "Navigation item hover wash."),
627
+ component("navItem.backgroundActive", ref("state.selected"), "Active navigation item wash."),
628
+ component("navItem.indicator", ref("accent.DEFAULT"), "Active navigation item indicator."),
629
+ component("navSectionLabel.text", ref("text.faint"), "Navigation section label."),
630
+ component("font.weight.sectionLabel", value(700), "Navigation section label weight."),
631
+ component("topBar.background", ref("surface.glass"), "TopBar glass surface."),
632
+ component("topBar.border", ref("border.subtle"), "TopBar lower edge."),
633
+ component("topBar.eyebrowText", ref("text.faint"), "TopBar eyebrow label."),
634
+ component("topBar.titleText", ref("text.secondary"), "TopBar context title."),
635
+ component("topBar.minHeight", value("68px"), "TopBar minimum height.")
636
+ ];
637
+ var componentTokens = [
638
+ ...button,
639
+ ...card,
640
+ ...badge,
641
+ ...table,
642
+ ...timeline,
643
+ ...codeBlock,
644
+ ...kbd,
645
+ ...banner,
646
+ ...emptyState,
647
+ ...feedbackMeter,
648
+ ...progressRing,
649
+ ...spinner,
650
+ ...input,
651
+ ...overlay,
652
+ ...page,
653
+ ...shell
654
+ ];
655
+
656
+ // src/palette.dark.ts
657
+ function darkToken(input2) {
658
+ return { tier: "semantic", ...input2 };
659
+ }
660
+ var surface = [
661
+ darkToken({
662
+ key: "surface.base",
663
+ usage: "decorative",
664
+ value: "#080b12",
665
+ description: "Application background. Contrast-exempt: surfaces are background arguments."
666
+ }),
667
+ darkToken({
668
+ key: "surface.canvas",
669
+ usage: "decorative",
670
+ value: "#0b101a",
671
+ description: "Content canvas. Contrast-exempt: surfaces are background arguments."
672
+ }),
673
+ darkToken({
674
+ key: "surface.subtle",
675
+ usage: "decorative",
676
+ value: "#101722",
677
+ description: "Recessed fill. Contrast-exempt: surfaces are background arguments."
678
+ }),
679
+ darkToken({
680
+ key: "surface.2",
681
+ usage: "decorative",
682
+ alias: "surface.subtle",
683
+ description: "Source alias of `surface.subtle`, kept as a reference (FR-THM-001 AC-2)."
684
+ }),
685
+ darkToken({
686
+ key: "surface.raised",
687
+ usage: "decorative",
688
+ value: "#141d2a",
689
+ description: "Card and panel fill. Contrast-exempt: surfaces are background arguments."
690
+ }),
691
+ darkToken({
692
+ key: "surface.elevated",
693
+ usage: "decorative",
694
+ value: "#192535",
695
+ description: "Highest surface. Contrast-exempt: surfaces are background arguments."
696
+ }),
697
+ darkToken({
698
+ key: "surface.overlay",
699
+ usage: "decorative",
700
+ value: "rgba(4, 7, 12, 0.78)",
701
+ description: "Dialog scrim. Contrast-exempt: nothing is drawn on top of a scrim."
702
+ }),
703
+ darkToken({
704
+ key: "surface.glass",
705
+ usage: "decorative",
706
+ value: "rgba(16, 23, 34, 0.82)",
707
+ description: "Translucent panel behind `backdrop-filter`. Contrast-exempt: translucent layer."
708
+ }),
709
+ darkToken({
710
+ key: "surface.timeline",
711
+ usage: "decorative",
712
+ value: "#0d141f",
713
+ description: "Timeline rail. Contrast-exempt: surfaces are background arguments."
714
+ }),
715
+ darkToken({
716
+ key: "surface.track",
717
+ usage: "decorative",
718
+ value: "rgba(255, 255, 255, 0.05)",
719
+ description: "Progress and meter track, matching the source 1:1."
720
+ })
721
+ ];
722
+ var text = [
723
+ darkToken({
724
+ key: "text.primary",
725
+ usage: "body",
726
+ value: "#f4f7fb",
727
+ description: "Top of the text hierarchy. 14.40:1 to 18.32:1 across the five surfaces."
728
+ }),
729
+ darkToken({
730
+ key: "text.secondary",
731
+ usage: "body",
732
+ value: "#c5cfdd",
733
+ description: "Supporting copy. 9.83:1 to 12.51:1 across the five surfaces."
734
+ }),
735
+ darkToken({
736
+ key: "text.muted",
737
+ usage: "body",
738
+ value: "#8290a3",
739
+ description: "De-emphasised copy, table headers, field labels. 4.76:1 to 6.06:1."
740
+ }),
741
+ darkToken({
742
+ key: "text.faint",
743
+ usage: "decorative",
744
+ value: "#5f6d80",
745
+ description: "Meta text, timestamps, uppercase labels and input placeholders only. Contrast-exempt: 2.94:1 to 3.74:1 never reaches body 4.5:1. Forbidden on `surface.elevated` (FR-THM-005 AC-3)."
746
+ }),
747
+ darkToken({
748
+ key: "text.inverse",
749
+ usage: "body",
750
+ value: "#07111f",
751
+ description: "Text on an accent, status or severity fill."
752
+ }),
753
+ darkToken({
754
+ key: "text.monoPayload",
755
+ usage: "body",
756
+ value: "#dce6f3",
757
+ description: "Monospace payload text. 13.44:1 on `surface.raised`."
758
+ })
759
+ ];
760
+ var border = [
761
+ darkToken({
762
+ key: "border.subtle",
763
+ usage: "decorative",
764
+ value: "rgba(148, 163, 184, 0.1)",
765
+ description: "Hairline divider. Contrast-exempt (1.13:1), WCAG 1.4.11 exception: card and panel edges are already identified by surface luminance and `elevation.*` (FR-THM-005 AC-4)."
766
+ }),
767
+ darkToken({
768
+ key: "border.default",
769
+ usage: "decorative",
770
+ value: "rgba(148, 163, 184, 0.18)",
771
+ description: "Default edge. Contrast-exempt (1.30:1), same WCAG 1.4.11 exception. Form controls use `border.control` instead (FR-THM-005 AC-4)."
772
+ }),
773
+ darkToken({
774
+ key: "border.strong",
775
+ usage: "decorative",
776
+ value: "rgba(148, 163, 184, 0.3)",
777
+ description: "Pronounced edge for buttons and overlays. Contrast-exempt (1.69:1), same WCAG 1.4.11 exception (FR-THM-005 AC-4)."
778
+ }),
779
+ darkToken({
780
+ key: "border.control",
781
+ usage: "nonText",
782
+ value: "rgba(148, 163, 184, 0.6)",
783
+ description: "Form control edge. Corrected token (FR-THM-005 AC-2): the source reused `border.default` at 1.30:1. Alpha 0.60 yields 3.23:1 on `surface.raised`. Applies to TextField, TextArea, Select, Switch and Checkbox only."
784
+ }),
785
+ darkToken({
786
+ key: "border.DEFAULT",
787
+ usage: "decorative",
788
+ alias: "border.default",
789
+ description: "Source alias of `border.default`, kept as a reference (FR-THM-001 AC-2)."
790
+ })
791
+ ];
792
+ var accent = [
793
+ darkToken({
794
+ key: "accent.DEFAULT",
795
+ usage: "body",
796
+ value: "#6d7cff",
797
+ description: "The single accent. 5.60:1 on `surface.base`, 4.82:1 on `surface.raised`. Body use on `surface.elevated` is forbidden (4.40:1); large text and non-text use are allowed."
798
+ }),
799
+ darkToken({
800
+ key: "accent.strong",
801
+ usage: "nonText",
802
+ value: "#5667f5",
803
+ description: "Second accent step. Never a fill beneath body text; 4.36:1 on `surface.base`."
804
+ }),
805
+ darkToken({
806
+ key: "accent.soft",
807
+ usage: "decorative",
808
+ value: "rgba(109, 124, 255, 0.14)",
809
+ description: "Faint accent wash. Contrast-exempt: text above it is always `text.primary`."
810
+ }),
811
+ darkToken({
812
+ key: "accent.glow",
813
+ usage: "decorative",
814
+ value: "rgba(109, 124, 255, 0.28)",
815
+ description: "Dark-only glow. Contrast-exempt: never the sole carrier of information."
816
+ })
817
+ ];
818
+ var status = [
819
+ darkToken({
820
+ key: "status.queued",
821
+ usage: "nonText",
822
+ value: "#64748b",
823
+ icon: "circle-dashed",
824
+ description: "Queued. 3.56:1 on `surface.raised`. Dot and marker only, never a text foreground; the component must render an icon and a label alongside it (FR-THM-005 AC-5, AC-7)."
825
+ }),
826
+ darkToken({
827
+ key: "status.running",
828
+ usage: "body",
829
+ alias: "accent.DEFAULT",
830
+ icon: "loader",
831
+ description: "Running. Resolves to the accent, matching the source 1:1 (`#6d7cff`); the reference keeps the accent family single-sourced (design principle P-5)."
832
+ }),
833
+ darkToken({
834
+ key: "status.waiting",
835
+ usage: "body",
836
+ value: "#f59e0b",
837
+ icon: "pause-circle",
838
+ description: "Waiting on input. 7.89:1 on `surface.raised`."
839
+ }),
840
+ darkToken({
841
+ key: "status.success",
842
+ usage: "body",
843
+ value: "#10b981",
844
+ icon: "check-circle-2",
845
+ description: "Completed successfully. 6.68:1 on `surface.raised`."
846
+ }),
847
+ darkToken({
848
+ key: "status.partial",
849
+ usage: "body",
850
+ value: "#eab308",
851
+ icon: "alert-circle",
852
+ description: "Completed with gaps. 8.84:1 on `surface.raised`."
853
+ }),
854
+ darkToken({
855
+ key: "status.danger",
856
+ usage: "body",
857
+ value: "#ef4444",
858
+ icon: "x-circle",
859
+ description: "Failed. 4.50:1 on `surface.raised`."
860
+ }),
861
+ darkToken({
862
+ key: "status.neutralEnd",
863
+ usage: "decorative",
864
+ value: "#475569",
865
+ icon: "circle-slash",
866
+ description: "Cancelled or superseded. Contrast-exempt per CR-006 (`srs_final.md` 12.1): 2.04:1 to 2.60:1 reaches non-text 3:1 on no surface. WCAG 1.4.11 exception \u2014 FR-THM-005 AC-7 forces an icon and a label alongside the colour, and the source's surface-coloured ring supplies the marker's shape boundary, so the dot's fill carries no identification on its own. Known cost: the dot reads faintly against a dark background."
867
+ })
868
+ ];
869
+ var meter = [
870
+ darkToken({
871
+ key: "meter.normal",
872
+ usage: "body",
873
+ value: "#34d399",
874
+ description: "Below the warning threshold. 8.82:1 on `surface.raised`."
875
+ }),
876
+ darkToken({
877
+ key: "meter.warning",
878
+ usage: "body",
879
+ value: "#fbbf24",
880
+ description: "Past the warning threshold. 10.15:1 on `surface.raised`."
881
+ }),
882
+ darkToken({
883
+ key: "meter.exceeded",
884
+ usage: "body",
885
+ alias: "red.400",
886
+ description: "Past the hard limit. 6.13:1 on `surface.raised`."
887
+ })
888
+ ];
889
+ var severity = [
890
+ darkToken({
891
+ key: "severity.read",
892
+ usage: "body",
893
+ value: "#15803d",
894
+ icon: "eye",
895
+ description: "Read-only side effect. Fill only; 4.67:1 beneath `text.primary`."
896
+ }),
897
+ darkToken({
898
+ key: "severity.write",
899
+ usage: "body",
900
+ value: "#c2410c",
901
+ icon: "pencil",
902
+ description: "Mutating side effect. Fill only; 4.82:1 beneath `text.primary`."
903
+ }),
904
+ darkToken({
905
+ key: "severity.destructive",
906
+ usage: "body",
907
+ value: "#b91c1c",
908
+ icon: "trash-2",
909
+ description: "Destructive side effect. Fill only; 6.02:1 beneath `text.primary`."
910
+ }),
911
+ darkToken({
912
+ key: "severity.blocked",
913
+ usage: "body",
914
+ value: "#7f1d1d",
915
+ icon: "shield-x",
916
+ description: "Blocked by policy. Fill only; 9.32:1 beneath `text.primary`."
917
+ })
918
+ ];
919
+ var fontFamily = [
920
+ darkToken({
921
+ key: "font.sans",
922
+ usage: "decorative",
923
+ alias: "stack.sans",
924
+ description: "Interface font stack. Not a colour."
925
+ }),
926
+ darkToken({
927
+ key: "font.mono",
928
+ usage: "decorative",
929
+ alias: "stack.mono",
930
+ description: "Payload and code font stack. Not a colour."
931
+ })
932
+ ];
933
+ var elevation = [
934
+ darkToken({
935
+ key: "elevation.raised",
936
+ usage: "decorative",
937
+ value: "0 12px 30px rgba(0, 0, 0, 0.18)",
938
+ description: "Resting card shadow. Contrast-exempt: a shadow, not a colour."
939
+ }),
940
+ darkToken({
941
+ key: "elevation.hover",
942
+ usage: "decorative",
943
+ value: "0 18px 46px rgba(0, 0, 0, 0.26)",
944
+ description: "Hovered card shadow. Contrast-exempt: a shadow, not a colour."
945
+ }),
946
+ darkToken({
947
+ key: "elevation.overlay",
948
+ usage: "decorative",
949
+ value: "0 24px 64px rgba(0, 0, 0, 0.45), 0 0 0 1px {border.strong}",
950
+ description: "Overlay shadow plus a 1px ring. The ring references `border.strong` so each theme resolves its own edge. Contrast-exempt with that border (FR-THM-005 AC-4)."
951
+ })
952
+ ];
953
+ var interaction = [
954
+ darkToken({
955
+ key: "focusRing",
956
+ usage: "nonText",
957
+ value: "0 0 0 3px rgba(109, 124, 255, 0.8)",
958
+ description: "Corrected token (FR-THM-005 AC-1). The source used accent alpha 0.30, which composites to 1.50:1 on `surface.base` and fails WCAG 2.4.11; a focus indicator cannot be decorative. Alpha 0.80 gives 3.93:1 on `surface.base` and 3.56:1 on `surface.raised`."
959
+ }),
960
+ darkToken({
961
+ key: "state.hover",
962
+ usage: "decorative",
963
+ value: "rgba(255, 255, 255, 0.055)",
964
+ description: "Hover wash. Contrast-exempt: nudges the surface, carries no meaning."
965
+ }),
966
+ darkToken({
967
+ key: "state.selected",
968
+ usage: "decorative",
969
+ value: "rgba(109, 124, 255, 0.16)",
970
+ description: "Selected wash. Contrast-exempt: selection is also announced via `aria-pressed` and text."
971
+ }),
972
+ darkToken({
973
+ key: "state.disabled",
974
+ usage: "decorative",
975
+ value: "#18202c",
976
+ description: "Disabled fill. Contrast-exempt as a fill; checked beneath `text.muted` (5.05:1)."
977
+ }),
978
+ darkToken({
979
+ key: "state.disabledPolicy",
980
+ usage: "decorative",
981
+ value: "#422006",
982
+ description: "Policy-blocked fill. Contrast-exempt as a fill; checked beneath the label (5.27:1)."
983
+ })
984
+ ];
985
+ var darkPalette = [
986
+ ...surface,
987
+ ...text,
988
+ ...border,
989
+ ...accent,
990
+ ...status,
991
+ ...meter,
992
+ ...severity,
993
+ ...fontFamily,
994
+ ...elevation,
995
+ ...interaction
996
+ ];
997
+
998
+ // src/palette.light.ts
999
+ var values = {
1000
+ "surface.base": "#e8ecf2",
1001
+ "surface.canvas": "#eef1f6",
1002
+ "surface.subtle": "#f3f6f9",
1003
+ "surface.raised": "#fbfcfd",
1004
+ "surface.elevated": "#ffffff",
1005
+ "surface.overlay": "rgba(12, 18, 28, 0.45)",
1006
+ "surface.glass": "rgba(251, 252, 253, 0.86)",
1007
+ "surface.timeline": "#f1f4f8",
1008
+ "surface.track": "rgba(12, 18, 28, 0.08)",
1009
+ "text.primary": "#0c121c",
1010
+ "text.secondary": "#33415a",
1011
+ "text.muted": "#4d5a6e",
1012
+ "text.faint": "#6b788c",
1013
+ "text.inverse": "#f4f7fb",
1014
+ "text.monoPayload": "#1b2537",
1015
+ "border.subtle": "#e2e8f0",
1016
+ "border.default": "#6b788c",
1017
+ "border.strong": "#5b6879",
1018
+ "border.control": "#64748b",
1019
+ "accent.DEFAULT": "#4f5bd5",
1020
+ "accent.strong": "#3f4ac0",
1021
+ "accent.soft": "rgba(79, 91, 213, 0.14)",
1022
+ "accent.glow": "rgba(79, 91, 213, 0.22)",
1023
+ "status.queued": "#52607a",
1024
+ "status.waiting": "#b45309",
1025
+ "status.success": "#047857",
1026
+ "status.partial": "#a16207",
1027
+ "status.danger": "#c81e1e",
1028
+ "status.neutralEnd": "#3f4b5f",
1029
+ "meter.normal": "#047857",
1030
+ "meter.warning": "#b45309",
1031
+ "meter.exceeded": "#dc2626",
1032
+ "elevation.raised": "0 12px 30px rgba(12, 18, 28, 0.08)",
1033
+ "elevation.hover": "0 18px 46px rgba(12, 18, 28, 0.12)",
1034
+ "elevation.overlay": "0 24px 64px rgba(12, 18, 28, 0.2), 0 0 0 1px {border.strong}",
1035
+ "focusRing": "0 0 0 3px rgba(79, 91, 213, 0.8)",
1036
+ "state.hover": "rgba(12, 18, 28, 0.05)",
1037
+ "state.selected": "rgba(79, 91, 213, 0.12)",
1038
+ "state.disabled": "#e2e8f0",
1039
+ "state.disabledPolicy": "#fef3c7"
1040
+ };
1041
+ var lightPalette = darkPalette.map((token) => {
1042
+ const value2 = values[token.key];
1043
+ return value2 === void 0 ? token : { ...token, value: value2, alias: void 0 };
1044
+ });
1045
+
1046
+ // src/primitives.ts
1047
+ function primitive(key, value2, description) {
1048
+ return { key, tier: "primitive", value: value2, usage: "decorative", description };
1049
+ }
1050
+ var ink = [
1051
+ primitive("ink.0", "#ffffff", "Pure white. `::selection` text, light `surface.elevated`."),
1052
+ primitive("ink.25", "#fbfcfd", "Light `surface.raised`."),
1053
+ primitive("ink.50", "#f4f7fb", "Dark `text.primary`, light `text.inverse`."),
1054
+ primitive("ink.75", "#f3f6f9", "Light `surface.subtle`."),
1055
+ primitive("ink.100", "#f1f4f8", "Light `surface.timeline`."),
1056
+ primitive("ink.150", "#eef1f6", "Light `surface.canvas`."),
1057
+ primitive("ink.200", "#e8ecf2", "Light `surface.base`."),
1058
+ primitive("ink.250", "#e2e8f0", "Light `border.subtle` and `state.disabled`."),
1059
+ primitive("ink.300", "#dce6f3", "Dark `text.monoPayload`."),
1060
+ primitive("ink.350", "#c5cfdd", "Dark `text.secondary`."),
1061
+ primitive("ink.400", "#8290a3", "Dark `text.muted`."),
1062
+ primitive("ink.500", "#6b788c", "Light `text.faint` and `border.default`."),
1063
+ primitive("ink.600", "#5f6d80", "Dark `text.faint`."),
1064
+ primitive("ink.650", "#5b6879", "Light `border.strong`."),
1065
+ primitive("ink.700", "#4d5a6e", "Light `text.muted`."),
1066
+ primitive("ink.750", "#33415a", "Light `text.secondary`."),
1067
+ primitive("ink.800", "#1b2537", "Light `text.monoPayload`."),
1068
+ primitive("ink.810", "#192535", "Dark `surface.elevated`."),
1069
+ primitive("ink.820", "#18202c", "Dark `state.disabled`."),
1070
+ primitive("ink.840", "#141d2a", "Dark `surface.raised`."),
1071
+ primitive("ink.860", "#101722", "Dark `surface.subtle`."),
1072
+ primitive("ink.870", "#0d141f", "Dark `surface.timeline`."),
1073
+ primitive("ink.880", "#0c121c", "Light `text.primary`; base colour of the light shadow ramp."),
1074
+ primitive("ink.890", "#07111f", "Dark `text.inverse`."),
1075
+ primitive("ink.895", "#0b101a", "Dark `surface.canvas`."),
1076
+ primitive("ink.900", "#080b12", "Dark `surface.base`."),
1077
+ primitive("ink.950", "#04070c", "Base colour of the dark `surface.overlay` scrim.")
1078
+ ];
1079
+ var indigo = [
1080
+ primitive("indigo.300", "#aab3ff", "Link hover in the source product."),
1081
+ primitive("indigo.400", "#7b89ff", "Upper stop of the source primary-button gradient."),
1082
+ primitive("indigo.500", "#6d7cff", "Dark `accent`."),
1083
+ primitive("indigo.600", "#5667f5", "Dark `accent.strong`."),
1084
+ primitive("indigo.700", "#4f5bd5", "Light `accent`."),
1085
+ primitive("indigo.800", "#3f4ac0", "Light `accent.strong`.")
1086
+ ];
1087
+ var slate = [
1088
+ primitive("slate.400", "#94a3b8", "Base colour of all four dark border tokens and the scrollbar."),
1089
+ primitive("slate.500", "#64748b", "Dark `status.queued`; light `border.control`."),
1090
+ primitive("slate.600", "#475569", "Dark `status.neutralEnd`."),
1091
+ primitive("slate.700", "#3f4b5f", "Light `status.neutralEnd`."),
1092
+ primitive("slate.750", "#52607a", "Light `status.queued`.")
1093
+ ];
1094
+ var statusHues = [
1095
+ primitive("emerald.400", "#34d399", "Dark `meter.normal`."),
1096
+ primitive("emerald.500", "#10b981", "Dark `status.success`."),
1097
+ primitive("emerald.700", "#047857", "Light `status.success` and `meter.normal`."),
1098
+ primitive("green.700", "#15803d", "`severity.read`, both themes."),
1099
+ primitive("amber.100", "#fef3c7", "Light `state.disabledPolicy`."),
1100
+ primitive("amber.400", "#fbbf24", "Dark `meter.warning`."),
1101
+ primitive("amber.500", "#f59e0b", "Dark `status.waiting`."),
1102
+ primitive("amber.700", "#b45309", "Light `status.waiting` and `meter.warning`."),
1103
+ primitive("amber.950", "#422006", "Dark `state.disabledPolicy`."),
1104
+ primitive("yellow.500", "#eab308", "Dark `status.partial`."),
1105
+ primitive("yellow.700", "#a16207", "Light `status.partial`."),
1106
+ primitive("orange.700", "#c2410c", "`severity.write`, both themes."),
1107
+ primitive("red.300", "#fca5a5", "Policy-disabled button label in the source product."),
1108
+ primitive("red.400", "#f87171", "Dark `meter.exceeded`."),
1109
+ primitive("red.500", "#ef4444", "Dark `status.danger`."),
1110
+ primitive("red.600", "#dc2626", "Light `meter.exceeded`."),
1111
+ primitive("red.650", "#c81e1e", "Light `status.danger`."),
1112
+ primitive("red.700", "#b91c1c", "`severity.destructive`, both themes."),
1113
+ primitive("red.900", "#7f1d1d", "`severity.blocked`, both themes.")
1114
+ ];
1115
+ var nonColour = [
1116
+ primitive(
1117
+ "stack.sans",
1118
+ 'Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
1119
+ "System sans stack. `Inter` is supplied by the consumer or falls through."
1120
+ ),
1121
+ primitive(
1122
+ "stack.mono",
1123
+ `'JetBrains Mono', ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace`,
1124
+ "System mono stack for payloads and code."
1125
+ ),
1126
+ primitive("scale.4", "4px", "Spacing step."),
1127
+ primitive("scale.8", "8px", "Spacing step."),
1128
+ primitive("scale.12", "12px", "Spacing step."),
1129
+ primitive("scale.16", "16px", "Spacing step."),
1130
+ primitive("scale.24", "24px", "Spacing step."),
1131
+ primitive("scale.32", "32px", "Spacing step."),
1132
+ primitive("scale.40", "40px", "Spacing step."),
1133
+ primitive("scale.48", "48px", "Spacing step."),
1134
+ primitive("curve.6", "6px", "Corner radius step."),
1135
+ primitive("curve.9", "9px", "Corner radius step."),
1136
+ primitive("curve.12", "12px", "Corner radius step."),
1137
+ primitive("curve.18", "18px", "Corner radius step."),
1138
+ primitive("curve.24", "24px", "Corner radius step."),
1139
+ primitive("ease.entrance", "cubic-bezier(0.2, 0, 0, 1)", "Entrance and state-change easing."),
1140
+ primitive("ease.overshoot", "cubic-bezier(0.34, 1.56, 0.64, 1)", "Toggle settle easing.")
1141
+ ];
1142
+ var primitiveTokens = [
1143
+ ...ink,
1144
+ ...indigo,
1145
+ ...slate,
1146
+ ...statusHues,
1147
+ ...nonColour
1148
+ ];
1149
+
1150
+ // src/token-source.ts
1151
+ function componentOverride(key, source) {
1152
+ const token = componentTokens.find((candidate) => candidate.key === key);
1153
+ if (token === void 0) throw new Error(`unknown component token override: ${key}`);
1154
+ return { ...token, ...source };
1155
+ }
1156
+ var THEME_SOURCES = [
1157
+ {
1158
+ theme: "dark",
1159
+ colorScheme: "dark",
1160
+ selectors: [":root", '[data-cdt-theme="dark"]'],
1161
+ palette: darkPalette
1162
+ },
1163
+ {
1164
+ theme: "light",
1165
+ colorScheme: "light",
1166
+ selectors: [':root[data-cdt-theme="light"]'],
1167
+ palette: lightPalette,
1168
+ componentOverrides: [
1169
+ componentOverride("button.policyDisabled.text", { alias: "severity.destructive" }),
1170
+ componentOverride("card.background", { alias: "surface.elevated" }),
1171
+ componentOverride("card.border", { alias: "border.strong" }),
1172
+ componentOverride("badge.severity.text", { alias: "text.inverse" }),
1173
+ componentOverride("input.background", { alias: "surface.elevated" }),
1174
+ componentOverride("input.backgroundFocus", { alias: "surface.elevated" }),
1175
+ componentOverride("overlay.background", { alias: "surface.elevated" }),
1176
+ componentOverride("overlay.backgroundBlur", { value: "0px" })
1177
+ ]
1178
+ }
1179
+ ];
1180
+ function tokensForTheme(theme) {
1181
+ const overrides = new Map((theme.componentOverrides ?? []).map((token) => [token.key, token]));
1182
+ return [
1183
+ ...primitiveTokens,
1184
+ ...theme.palette,
1185
+ ...scaleTokens,
1186
+ ...componentTokens.map((token) => overrides.get(token.key) ?? token)
1187
+ ];
1188
+ }
1189
+
1190
+ // src/contrast/color.ts
1191
+ var HEX_SOURCE = "#([0-9a-f]{3,8})\\b";
1192
+ var FUNCTIONAL_SOURCE = "\\b(rgba?|hsla?)\\(\\s*([^()]*)\\)";
1193
+ var clamp = (value2, low, high) => Math.min(Math.max(value2, low), high);
1194
+ function fromHex(digits) {
1195
+ const byte = (pair2) => parseInt(pair2.length === 1 ? pair2 + pair2 : pair2, 16);
1196
+ if (digits.length === 3 || digits.length === 4) {
1197
+ const [r, g, b, a] = [...digits];
1198
+ return { r: byte(r), g: byte(g), b: byte(b), alpha: a === void 0 ? 1 : byte(a) / 255 };
1199
+ }
1200
+ if (digits.length === 6 || digits.length === 8) {
1201
+ const at = (index) => digits.slice(index, index + 2);
1202
+ return {
1203
+ r: byte(at(0)),
1204
+ g: byte(at(2)),
1205
+ b: byte(at(4)),
1206
+ alpha: digits.length === 8 ? byte(at(6)) / 255 : 1
1207
+ };
1208
+ }
1209
+ return void 0;
1210
+ }
1211
+ function argumentsOf(body) {
1212
+ return body.replace(/\//g, " ").split(/[\s,]+/).filter((part) => part !== "");
1213
+ }
1214
+ function toAlpha(raw) {
1215
+ if (raw === void 0) return 1;
1216
+ const value2 = raw.endsWith("%") ? Number(raw.slice(0, -1)) / 100 : Number(raw);
1217
+ return Number.isFinite(value2) ? clamp(value2, 0, 1) : 1;
1218
+ }
1219
+ function fromRgb(body) {
1220
+ const parts = argumentsOf(body);
1221
+ if (parts.length < 3) return void 0;
1222
+ const channel = (raw) => {
1223
+ const value2 = raw.endsWith("%") ? Number(raw.slice(0, -1)) / 100 * 255 : Number(raw);
1224
+ return Number.isFinite(value2) ? clamp(Math.round(value2), 0, 255) : Number.NaN;
1225
+ };
1226
+ const r = channel(parts[0]);
1227
+ const g = channel(parts[1]);
1228
+ const b = channel(parts[2]);
1229
+ if ([r, g, b].some(Number.isNaN)) return void 0;
1230
+ return { r, g, b, alpha: toAlpha(parts[3]) };
1231
+ }
1232
+ function fromHsl(body) {
1233
+ const parts = argumentsOf(body);
1234
+ if (parts.length < 3) return void 0;
1235
+ const hue = Number(parts[0].replace(/deg$/i, ""));
1236
+ const saturation = Number(parts[1].replace(/%$/, "")) / 100;
1237
+ const lightness = Number(parts[2].replace(/%$/, "")) / 100;
1238
+ if (![hue, saturation, lightness].every((value2) => Number.isFinite(value2))) return void 0;
1239
+ const chroma = (1 - Math.abs(2 * clamp(lightness, 0, 1) - 1)) * clamp(saturation, 0, 1);
1240
+ const sextant = (hue % 360 + 360) % 360 / 60;
1241
+ const secondary = chroma * (1 - Math.abs(sextant % 2 - 1));
1242
+ const lift = clamp(lightness, 0, 1) - chroma / 2;
1243
+ const wheel = [
1244
+ [chroma, secondary, 0],
1245
+ [secondary, chroma, 0],
1246
+ [0, chroma, secondary],
1247
+ [0, secondary, chroma],
1248
+ [secondary, 0, chroma],
1249
+ [chroma, 0, secondary]
1250
+ ];
1251
+ const [r, g, b] = wheel[Math.floor(sextant) % 6];
1252
+ const channel = (raw) => clamp(Math.round((raw + lift) * 255), 0, 255);
1253
+ return { r: channel(r), g: channel(g), b: channel(b), alpha: toAlpha(parts[3]) };
1254
+ }
1255
+ function parseFunctional(name, body) {
1256
+ return name.toLowerCase().startsWith("hsl") ? fromHsl(body) : fromRgb(body);
1257
+ }
1258
+ function findColors(value2) {
1259
+ const found = [];
1260
+ for (const match of value2.matchAll(new RegExp(FUNCTIONAL_SOURCE, "gi"))) {
1261
+ const parsed = parseFunctional(match[1], match[2]);
1262
+ if (parsed) found.push(parsed);
1263
+ }
1264
+ for (const match of value2.matchAll(new RegExp(HEX_SOURCE, "gi"))) {
1265
+ const parsed = fromHex(match[1]);
1266
+ if (parsed) found.push(parsed);
1267
+ }
1268
+ return found;
1269
+ }
1270
+ function linearise(channel8) {
1271
+ const channel = channel8 / 255;
1272
+ return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
1273
+ }
1274
+ function relativeLuminance({ r, g, b }) {
1275
+ return 0.2126 * linearise(r) + 0.7152 * linearise(g) + 0.0722 * linearise(b);
1276
+ }
1277
+ function contrastRatio(one, other) {
1278
+ const first = relativeLuminance(one);
1279
+ const second = relativeLuminance(other);
1280
+ const lighter = Math.max(first, second);
1281
+ const darker = Math.min(first, second);
1282
+ return (lighter + 0.05) / (darker + 0.05);
1283
+ }
1284
+ function compositeOver(foreground, background) {
1285
+ if (foreground.alpha >= 1) return foreground;
1286
+ const mix = (front, back) => Math.round(front * foreground.alpha + back * (1 - foreground.alpha));
1287
+ return {
1288
+ r: mix(foreground.r, background.r),
1289
+ g: mix(foreground.g, background.g),
1290
+ b: mix(foreground.b, background.b),
1291
+ alpha: 1
1292
+ };
1293
+ }
1294
+ function toHex({ r, g, b }) {
1295
+ const pair2 = (channel) => channel.toString(16).padStart(2, "0");
1296
+ return `#${pair2(r)}${pair2(g)}${pair2(b)}`;
1297
+ }
1298
+
1299
+ // src/contrast/check.ts
1300
+ function assertUniqueIds(pairs) {
1301
+ const seen = /* @__PURE__ */ new Set();
1302
+ const duplicates = /* @__PURE__ */ new Set();
1303
+ for (const { id } of pairs) {
1304
+ if (seen.has(id)) duplicates.add(id);
1305
+ seen.add(id);
1306
+ }
1307
+ if (duplicates.size === 0) return;
1308
+ throw new TokenBuildError("TOK-CP-DUPLICATE-ID", `${duplicates.size} contrast pair id(s) declared twice`, [
1309
+ ...duplicates,
1310
+ "hint: CP ids are permanent. A removed pair leaves its number retired (tokens spec 8.2)."
1311
+ ]);
1312
+ }
1313
+ function lookup(index, pair2, role) {
1314
+ const declared = pair2[role];
1315
+ const key = resolveReferenceTarget(index, declared);
1316
+ if (key === void 0) {
1317
+ throw new TokenBuildError("TOK-CP-UNKNOWN-KEY", "contrast pair names a token that does not exist", [
1318
+ `pair: ${pair2.id}`,
1319
+ `${role}: ${declared}`,
1320
+ "hint: check the spelling, or remove the pair. Primitive keys are not addressable here."
1321
+ ]);
1322
+ }
1323
+ return index.get(key);
1324
+ }
1325
+ function assertUsageAgrees(pair2, foreground) {
1326
+ if (foreground.usage === "decorative") {
1327
+ throw new TokenBuildError(
1328
+ "TOK-CP-DECORATIVE",
1329
+ "a `decorative` token is declared as a contrast pair foreground",
1330
+ [
1331
+ `pair: ${pair2.id}`,
1332
+ `foreground: ${foreground.key}`,
1333
+ `reason it is excluded: ${foreground.description}`,
1334
+ "hint: `decorative` means excluded from contrast checking. Reclassifying it needs a CR."
1335
+ ]
1336
+ );
1337
+ }
1338
+ const agrees = foreground.usage === pair2.usage || pair2.usage === "large" && foreground.usage === "body";
1339
+ if (agrees) return;
1340
+ throw new TokenBuildError("TOK-CP-USAGE", "contrast pair threshold disagrees with the token's `usage`", [
1341
+ `pair: ${pair2.id}`,
1342
+ `foreground: ${foreground.key} declares usage \`${String(foreground.usage)}\``,
1343
+ `pair declares usage \`${pair2.usage}\``,
1344
+ "hint: `srs_final.md` 12.1 fixes each token's usage; the pair follows it."
1345
+ ]);
1346
+ }
1347
+ function colorOf(pair2, token, value2, role) {
1348
+ const colors = findColors(value2);
1349
+ if (colors.length === 1) return colors[0];
1350
+ throw new TokenBuildError(
1351
+ "TOK-CP-COLOR",
1352
+ colors.length === 0 ? "contrast pair token resolves to a value that holds no colour" : "contrast pair token resolves to a value that holds more than one colour",
1353
+ [
1354
+ `pair: ${pair2.id}`,
1355
+ `${role}: ${token.key}`,
1356
+ `value: ${value2}`,
1357
+ `colours found: ${colors.length}`,
1358
+ "hint: a pair measures one colour. `focusRing` works because its box-shadow carries exactly one."
1359
+ ]
1360
+ );
1361
+ }
1362
+ function measure(pair2, theme, index, values2) {
1363
+ const foreground = lookup(index, pair2, "foreground");
1364
+ const background = lookup(index, pair2, "background");
1365
+ assertUsageAgrees(pair2, foreground);
1366
+ const foregroundValue = values2.get(foreground.key);
1367
+ const backgroundValue = values2.get(background.key);
1368
+ const backgroundColor = colorOf(pair2, background, backgroundValue, "background");
1369
+ if (backgroundColor.alpha < 1) {
1370
+ throw new TokenBuildError("TOK-CP-ALPHA", "contrast pair background is translucent", [
1371
+ `pair: ${pair2.id}`,
1372
+ `background: ${background.key} = ${backgroundValue}`,
1373
+ "hint: a translucent background has no colour of its own. Declare the opaque surface beneath it."
1374
+ ]);
1375
+ }
1376
+ const foregroundColor = colorOf(pair2, foreground, foregroundValue, "foreground");
1377
+ const composited = compositeOver(foregroundColor, backgroundColor);
1378
+ const ratio = contrastRatio(composited, backgroundColor);
1379
+ const threshold = CONTRAST_THRESHOLDS[pair2.usage];
1380
+ return {
1381
+ id: pair2.id,
1382
+ theme,
1383
+ foreground: pair2.foreground,
1384
+ background: pair2.background,
1385
+ foregroundValue,
1386
+ backgroundValue,
1387
+ compositedForeground: toHex(composited),
1388
+ usage: pair2.usage,
1389
+ threshold,
1390
+ ratio: Number(ratio.toFixed(4)),
1391
+ pass: ratio >= threshold
1392
+ };
1393
+ }
1394
+ function exclusionsFor(tokens) {
1395
+ return tokens.filter((token) => token.tier !== "primitive" && token.usage === "decorative").map((token) => ({ key: token.key, usage: "decorative", reason: token.description }));
1396
+ }
1397
+ function checkContrast(options = {}) {
1398
+ const themes = options.themes ?? THEME_SOURCES;
1399
+ const pairs = options.pairs ?? contrastPairs;
1400
+ assertUniqueIds(pairs);
1401
+ const results = [];
1402
+ const exclusions = /* @__PURE__ */ new Map();
1403
+ for (const theme of themes) {
1404
+ const tokens = tokensForTheme(theme);
1405
+ const index = buildTokenIndex(tokens);
1406
+ const { values: values2 } = resolveTokens(index);
1407
+ for (const exclusion of exclusionsFor(tokens)) exclusions.set(exclusion.key, exclusion);
1408
+ for (const pair2 of pairs) {
1409
+ if (!pair2.themes.includes(theme.theme)) continue;
1410
+ results.push(measure(pair2, theme.theme, index, values2));
1411
+ }
1412
+ }
1413
+ const failed = results.filter((result) => !result.pass).length;
1414
+ return {
1415
+ themes: themes.map((theme) => theme.theme),
1416
+ thresholds: CONTRAST_THRESHOLDS,
1417
+ results,
1418
+ exclusions: [...exclusions.values()],
1419
+ summary: {
1420
+ declaredPairs: pairs.length,
1421
+ checks: results.length,
1422
+ passed: results.length - failed,
1423
+ failed,
1424
+ excludedTokens: exclusions.size
1425
+ }
1426
+ };
1427
+ }
1428
+
1429
+ // src/contrast/report.ts
1430
+ function formatRatio(ratio) {
1431
+ return ratio.toFixed(2);
1432
+ }
1433
+ function formatResult(result) {
1434
+ const columns = [
1435
+ `theme=${result.theme}`.padEnd(12),
1436
+ `id=${result.id}`.padEnd(11),
1437
+ `pair=${result.foreground}/${result.background}`.padEnd(52),
1438
+ `ratio=${formatRatio(result.ratio)}`.padEnd(12),
1439
+ `threshold=${result.threshold.toFixed(2)}(${result.usage})`.padEnd(26),
1440
+ result.pass ? "PASS" : "FAIL"
1441
+ ];
1442
+ return columns.join(" ").trimEnd();
1443
+ }
1444
+ function formatSummary(report) {
1445
+ return `${report.summary.failed} of ${report.summary.checks} pairs checked failed contrast threshold`;
1446
+ }
1447
+ function exitCodeFor(report) {
1448
+ return report.summary.failed > 0 ? 1 : 0;
1449
+ }
1450
+ function formatExclusions(report) {
1451
+ const lines = [
1452
+ `[contrast] ${report.summary.excludedTokens} token(s) excluded from contrast checks (usage: decorative)`
1453
+ ];
1454
+ for (const exclusion of report.exclusions) {
1455
+ lines.push(` ${exclusion.key}`);
1456
+ lines.push(` reason: ${exclusion.reason}`);
1457
+ }
1458
+ lines.push("[contrast] --report: no files written");
1459
+ return lines;
1460
+ }
1461
+ function renderReportJson(report) {
1462
+ return `${JSON.stringify(
1463
+ {
1464
+ $generatedBy: "@conductor-by-89soone/tokens conductor-check-contrast",
1465
+ themes: report.themes,
1466
+ thresholds: report.thresholds,
1467
+ summary: report.summary,
1468
+ results: report.results,
1469
+ exclusions: report.exclusions
1470
+ },
1471
+ null,
1472
+ 2
1473
+ )}
1474
+ `;
1475
+ }
1476
+
1477
+ // src/contrast-cli.ts
1478
+ var PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
1479
+ var REPORT_FILE = "contrast-report.json";
1480
+ function runOnce(options) {
1481
+ const themes = selectThemes(THEME_SOURCES, options.theme);
1482
+ const report = checkContrast({ themes });
1483
+ if (options.report) {
1484
+ for (const line of formatExclusions(report)) console.log(line);
1485
+ return 0;
1486
+ }
1487
+ for (const result of report.results) console.log(formatResult(result));
1488
+ const outDir = resolve(PACKAGE_ROOT, options.outDir);
1489
+ writeArtifacts(outDir, /* @__PURE__ */ new Map([[REPORT_FILE, renderReportJson(report)]]));
1490
+ console.log(`[contrast] wrote ${options.outDir}/${REPORT_FILE}`);
1491
+ console.log(formatSummary(report));
1492
+ return exitCodeFor(report);
1493
+ }
1494
+ function reportFailure(error) {
1495
+ if (error instanceof TokenBuildError) {
1496
+ console.error(error.format());
1497
+ if (error.exitCode === 1) console.error("no output written");
1498
+ return error.exitCode;
1499
+ }
1500
+ console.error(error instanceof Error ? error.stack ?? error.message : String(error));
1501
+ return 1;
1502
+ }
1503
+ function main(argv) {
1504
+ try {
1505
+ return runOnce(parseContrastArgs(argv));
1506
+ } catch (error) {
1507
+ return reportFailure(error);
1508
+ }
1509
+ }
1510
+ process.exitCode = main(process.argv.slice(2));