@multiplatform.one/theme 7.7.6 → 7.10.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/package.json +5 -5
- package/src/audit/constraintAudit.spec.ts +157 -0
- package/src/audit/constraintAudit.ts +154 -35
- package/src/audit/constraintScope.spec.ts +48 -0
- package/src/audit/index.ts +16 -0
- package/src/audit/themeMatrix.spec.ts +292 -1
- package/src/audit/themeMatrix.ts +1015 -22
- package/src/theme/Intent.stories.tsx +88 -0
- package/src/theme/Preset.stories.tsx +80 -0
- package/src/theme/Surface.spec.tsx +6 -0
- package/src/theme/Surface.stories.tsx +91 -0
- package/src/theme/ThemeProvider.stories.tsx +50 -0
- package/src/theme/Tint.stories.tsx +98 -0
- package/src/theme/chartPalette.spec.ts +128 -5
- package/src/theme/chartPalette.ts +105 -22
- package/src/theme/colorRules.spec.ts +92 -39
- package/src/theme/colorRules.ts +4 -6
- package/src/theme/createDefaultThemeConfig.ts +1 -1
- package/src/theme/createThemes.ts +58 -3
- package/src/theme/devtools/ColorLineVisualizer.stories.tsx +36 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.stories.tsx +16 -0
- package/src/theme/glyphPaint.spec.ts +14 -0
- package/src/theme/glyphPaint.ts +2 -1
- package/src/theme/intent.spec.tsx +1 -0
- package/src/theme/layoutTokensHooks.spec.tsx +1 -0
- package/src/theme/recipeInputs.ts +9 -9
- package/src/theme/resolveKnobs.spec.ts +2 -2
- package/src/theme/sizeLadder.spec.ts +19 -10
- package/src/theme/sizeRecipes.ts +2 -2
- package/src/theme/themeValue.spec.ts +2 -2
- package/src/theme/useResolvedKnobsBehavior.spec.tsx +6 -0
- package/types/audit/constraintAudit.d.ts +17 -1
- package/types/audit/constraintAudit.d.ts.map +1 -1
- package/types/audit/index.d.ts +2 -2
- package/types/audit/index.d.ts.map +1 -1
- package/types/audit/themeMatrix.d.ts +135 -2
- package/types/audit/themeMatrix.d.ts.map +1 -1
- package/types/theme/chartPalette.d.ts +6 -4
- package/types/theme/chartPalette.d.ts.map +1 -1
- package/types/theme/colorRules.d.ts +3 -3
- package/types/theme/colorRules.d.ts.map +1 -1
- package/types/theme/createThemes.d.ts +1 -1
- package/types/theme/createThemes.d.ts.map +1 -1
- package/types/theme/glyphPaint.d.ts.map +1 -1
- package/types/theme/recipeInputs.d.ts +8 -8
- package/types/theme/recipeInputs.d.ts.map +1 -1
- package/types/theme/sizeRecipes.d.ts +2 -2
|
@@ -5,6 +5,11 @@ import {
|
|
|
5
5
|
captureMatrixSnapshot,
|
|
6
6
|
diffGeometry,
|
|
7
7
|
evaluateNoBreakage,
|
|
8
|
+
evaluateIconContrast,
|
|
9
|
+
evaluateChartProof,
|
|
10
|
+
type MatrixChartSample,
|
|
11
|
+
type MatrixIconSample,
|
|
12
|
+
type IconUnmeasurableReason,
|
|
8
13
|
evaluateTextContrast,
|
|
9
14
|
type MatrixSnapshot,
|
|
10
15
|
type MatrixTextSample,
|
|
@@ -31,9 +36,112 @@ function snapshotFromRecipeInputs(inputs: Partial<SizeRecipeInputs>): MatrixSnap
|
|
|
31
36
|
rowGap: `${recipe.gap}px`,
|
|
32
37
|
},
|
|
33
38
|
}));
|
|
34
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
storyRendered: true,
|
|
41
|
+
geometry,
|
|
42
|
+
textSamples: [],
|
|
43
|
+
iconSamples: [],
|
|
44
|
+
chartSamples: [],
|
|
45
|
+
svgRootCount: 0,
|
|
46
|
+
};
|
|
35
47
|
}
|
|
36
48
|
|
|
49
|
+
describe("declared chart proof keeps uncertainty visible", () => {
|
|
50
|
+
const sample = (overrides: Partial<MatrixChartSample> = {}): MatrixChartSample => ({
|
|
51
|
+
key: "chart:0",
|
|
52
|
+
kind: "pie",
|
|
53
|
+
context: "Revenue",
|
|
54
|
+
label: "Revenue by region",
|
|
55
|
+
background: "#111111",
|
|
56
|
+
backgroundResolved: true,
|
|
57
|
+
marks: [
|
|
58
|
+
{ key: "slice:0", label: "North", paint: "#bbbbbb", background: "#111111" },
|
|
59
|
+
{ key: "slice:1", label: "South", paint: "#dddddd", background: "#111111" },
|
|
60
|
+
],
|
|
61
|
+
labels: [],
|
|
62
|
+
separation: { declared: false },
|
|
63
|
+
datumText: { declared: true, expected: 2, observed: 2, complete: true },
|
|
64
|
+
...overrides,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("names adjacent slice contrast and does not accept a boolean gap claim", () => {
|
|
68
|
+
const result = evaluateChartProof([sample({ separation: { declared: true } })]);
|
|
69
|
+
expect(result.ok).toBe(false);
|
|
70
|
+
expect(result.violations.some((item) => item.rule === "adjacent-pair")).toBe(true);
|
|
71
|
+
expect(result.unverified.some((item) => item.reason === "unverified-separation")).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("requires two measured physical pixels between bars", () => {
|
|
75
|
+
const separated = sample({
|
|
76
|
+
kind: "bar",
|
|
77
|
+
separation: { declared: true, method: "rect-bounds", minimumGapPx: 2 },
|
|
78
|
+
});
|
|
79
|
+
expect(evaluateChartProof([separated]).ok).toBe(true);
|
|
80
|
+
expect(
|
|
81
|
+
evaluateChartProof([
|
|
82
|
+
{ ...separated, separation: { declared: true, method: "rect-bounds", minimumGapPx: 1.99 } },
|
|
83
|
+
]).ok,
|
|
84
|
+
).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("does not round a below-floor paint pair green", () => {
|
|
88
|
+
const result = evaluateChartProof([
|
|
89
|
+
sample({
|
|
90
|
+
marks: [{ key: "almost", label: "Almost", paint: "#000000", background: "#595959" }],
|
|
91
|
+
}),
|
|
92
|
+
]);
|
|
93
|
+
const violation = result.violations.find((item) => item.rule === "mark-backdrop");
|
|
94
|
+
expect(violation?.ratio).toBeGreaterThan(2.995);
|
|
95
|
+
expect(violation?.ratio).toBeLessThan(3);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("never treats a rect certificate as proof of pie center separation", () => {
|
|
99
|
+
expect(
|
|
100
|
+
evaluateChartProof([
|
|
101
|
+
sample({ separation: { declared: true, method: "rect-bounds", minimumGapPx: 8 } }),
|
|
102
|
+
]).ok,
|
|
103
|
+
).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it.each(["unknown-kind", "no-marks", "unsupported-paint"] as const)(
|
|
107
|
+
"keeps %s named and red",
|
|
108
|
+
(reason) => {
|
|
109
|
+
const input =
|
|
110
|
+
reason === "unknown-kind"
|
|
111
|
+
? sample({ kind: "radar" })
|
|
112
|
+
: reason === "no-marks"
|
|
113
|
+
? sample({ marks: [] })
|
|
114
|
+
: sample({
|
|
115
|
+
marks: [
|
|
116
|
+
{
|
|
117
|
+
key: "gradient",
|
|
118
|
+
label: "North",
|
|
119
|
+
background: "#111111",
|
|
120
|
+
unmeasurable: "unsupported-paint",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
expect(evaluateChartProof([input]).unverified.some((item) => item.reason === reason)).toBe(
|
|
125
|
+
true,
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
it("requires a root label, readable axis text and a complete datum alternative", () => {
|
|
131
|
+
const result = evaluateChartProof([
|
|
132
|
+
sample({
|
|
133
|
+
label: "",
|
|
134
|
+
labels: [{ key: "tick", label: "May", paint: "#333333", background: "#111111" }],
|
|
135
|
+
datumText: { declared: true, expected: 13, observed: 12, complete: false },
|
|
136
|
+
}),
|
|
137
|
+
]);
|
|
138
|
+
expect(result.violations.map((item) => item.rule)).toEqual(
|
|
139
|
+
expect.arrayContaining(["missing-label", "axis-text"]),
|
|
140
|
+
);
|
|
141
|
+
expect(result.unverified.some((item) => item.reason === "incomplete-datum-text")).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
37
145
|
describe("theme-axis geometry invariance (LC-80 tripwire)", () => {
|
|
38
146
|
it("identical recipe inputs across theme colours keep the diff clean", () => {
|
|
39
147
|
const base = snapshotFromRecipeInputs({});
|
|
@@ -87,6 +195,8 @@ describe("theme-axis geometry invariance (LC-80 tripwire)", () => {
|
|
|
87
195
|
});
|
|
88
196
|
});
|
|
89
197
|
|
|
198
|
+
const cleanIcons = evaluateIconContrast([]);
|
|
199
|
+
|
|
90
200
|
describe("preset-axis no-breakage (two assertions, never one)", () => {
|
|
91
201
|
const cleanContrast = { measured: 10, unmeasurable: 0, violations: [] };
|
|
92
202
|
|
|
@@ -99,6 +209,8 @@ describe("preset-axis no-breakage (two assertions, never one)", () => {
|
|
|
99
209
|
expect(diffGeometry(defaultCell, boldCell).identical).toBe(false);
|
|
100
210
|
|
|
101
211
|
const result = evaluateNoBreakage({
|
|
212
|
+
icons: cleanIcons,
|
|
213
|
+
baselineIcons: cleanIcons,
|
|
102
214
|
storyRendered: true,
|
|
103
215
|
offScaleViolations: 0,
|
|
104
216
|
baselineOffScaleViolations: 0,
|
|
@@ -110,6 +222,8 @@ describe("preset-axis no-breakage (two assertions, never one)", () => {
|
|
|
110
222
|
|
|
111
223
|
it("FAILS when a preset cell falls off the knob scales beyond the default cell", () => {
|
|
112
224
|
const result = evaluateNoBreakage({
|
|
225
|
+
icons: cleanIcons,
|
|
226
|
+
baselineIcons: cleanIcons,
|
|
113
227
|
storyRendered: true,
|
|
114
228
|
offScaleViolations: 3,
|
|
115
229
|
baselineOffScaleViolations: 1,
|
|
@@ -122,6 +236,8 @@ describe("preset-axis no-breakage (two assertions, never one)", () => {
|
|
|
122
236
|
|
|
123
237
|
it("FAILS when a preset cell loses contrast the default cell had", () => {
|
|
124
238
|
const result = evaluateNoBreakage({
|
|
239
|
+
icons: cleanIcons,
|
|
240
|
+
baselineIcons: cleanIcons,
|
|
125
241
|
storyRendered: true,
|
|
126
242
|
offScaleViolations: 0,
|
|
127
243
|
baselineOffScaleViolations: 0,
|
|
@@ -147,6 +263,8 @@ describe("preset-axis no-breakage (two assertions, never one)", () => {
|
|
|
147
263
|
|
|
148
264
|
it("FAILS when the story does not render at all", () => {
|
|
149
265
|
const result = evaluateNoBreakage({
|
|
266
|
+
icons: cleanIcons,
|
|
267
|
+
baselineIcons: cleanIcons,
|
|
150
268
|
storyRendered: false,
|
|
151
269
|
offScaleViolations: 0,
|
|
152
270
|
baselineOffScaleViolations: 0,
|
|
@@ -156,6 +274,60 @@ describe("preset-axis no-breakage (two assertions, never one)", () => {
|
|
|
156
274
|
expect(result.ok).toBe(false);
|
|
157
275
|
expect(result.failures[0]).toContain("render");
|
|
158
276
|
});
|
|
277
|
+
|
|
278
|
+
it("names a replacement structural failure even when the count stays equal or falls", () => {
|
|
279
|
+
const input = {
|
|
280
|
+
storyRendered: true,
|
|
281
|
+
offScaleViolations: 1,
|
|
282
|
+
baselineOffScaleViolations: 1,
|
|
283
|
+
offScaleSignatures: ["new control :: borderRadius: 15px is off-scale"],
|
|
284
|
+
baselineOffScaleSignatures: ["old control :: borderRadius: 14px is off-scale"],
|
|
285
|
+
contrast: cleanContrast,
|
|
286
|
+
baselineContrastViolations: 0,
|
|
287
|
+
};
|
|
288
|
+
for (const baselineOffScaleViolations of [1, 2]) {
|
|
289
|
+
const result = evaluateNoBreakage({ ...input, baselineOffScaleViolations });
|
|
290
|
+
expect(result.ok).toBe(false);
|
|
291
|
+
expect(result.failures[0]).toContain("new control");
|
|
292
|
+
expect(result.failures[0]).not.toContain("old control");
|
|
293
|
+
}
|
|
294
|
+
expect(
|
|
295
|
+
evaluateNoBreakage({ ...input, offScaleSignatures: input.baselineOffScaleSignatures }).ok,
|
|
296
|
+
).toBe(true);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("names replacement text failures regardless of count and preserves repeated identities", () => {
|
|
300
|
+
const old = {
|
|
301
|
+
key: "old text",
|
|
302
|
+
text: "Old",
|
|
303
|
+
color: "#aaaaaa",
|
|
304
|
+
background: "#ffffff",
|
|
305
|
+
ratio: 2.32,
|
|
306
|
+
required: 4.5,
|
|
307
|
+
};
|
|
308
|
+
const replacement = { ...old, key: "new text", text: "New" };
|
|
309
|
+
const input = {
|
|
310
|
+
storyRendered: true,
|
|
311
|
+
offScaleViolations: 0,
|
|
312
|
+
baselineOffScaleViolations: 0,
|
|
313
|
+
contrast: { ...cleanContrast, violations: [replacement] },
|
|
314
|
+
baselineContrastViolations: 1,
|
|
315
|
+
baselineContrast: { ...cleanContrast, violations: [old] },
|
|
316
|
+
};
|
|
317
|
+
for (const baseline of [[old], [old, old]]) {
|
|
318
|
+
const result = evaluateNoBreakage({
|
|
319
|
+
...input,
|
|
320
|
+
baselineContrastViolations: baseline.length,
|
|
321
|
+
baselineContrast: { ...cleanContrast, violations: baseline },
|
|
322
|
+
});
|
|
323
|
+
expect(result.ok).toBe(false);
|
|
324
|
+
expect(result.failures[0]).toContain("new text");
|
|
325
|
+
}
|
|
326
|
+
expect(evaluateNoBreakage({ ...input, contrast: input.baselineContrast }).ok).toBe(true);
|
|
327
|
+
expect(
|
|
328
|
+
evaluateNoBreakage({ ...input, contrast: { ...cleanContrast, violations: [old, old] } }).ok,
|
|
329
|
+
).toBe(false);
|
|
330
|
+
});
|
|
159
331
|
});
|
|
160
332
|
|
|
161
333
|
describe("evaluateTextContrast (the assertion downstream repos call)", () => {
|
|
@@ -184,6 +356,14 @@ describe("evaluateTextContrast (the assertion downstream repos call)", () => {
|
|
|
184
356
|
expect(result.violations[0].required).toBe(4.5);
|
|
185
357
|
});
|
|
186
358
|
|
|
359
|
+
it("keeps a just-below-AA text sample in the violations", () => {
|
|
360
|
+
const result = evaluateTextContrast([sample({ color: "#77767c" })]);
|
|
361
|
+
expect(result.measured).toBe(1);
|
|
362
|
+
expect(result.violations).toHaveLength(1);
|
|
363
|
+
expect(result.violations[0].ratio).toBeGreaterThan(4.495);
|
|
364
|
+
expect(result.violations[0].ratio).toBeLessThan(4.5);
|
|
365
|
+
});
|
|
366
|
+
|
|
187
367
|
it("holds large text (>=24px, or >=18.66px bold) to the 3:1 floor instead", () => {
|
|
188
368
|
// #949494 on white is ~3.5:1 — fails body text, clears large text.
|
|
189
369
|
const failing = evaluateTextContrast([sample({ color: "#949494" })]);
|
|
@@ -272,3 +452,114 @@ describe("captureMatrixSnapshot keys survive the theme axis", () => {
|
|
|
272
452
|
expect(captureMatrixSnapshot().storyRendered).toBe(false);
|
|
273
453
|
});
|
|
274
454
|
});
|
|
455
|
+
|
|
456
|
+
describe("icon contrast keeps unsupported conveying paint unverified", () => {
|
|
457
|
+
const sample = (overrides: Partial<MatrixIconSample> = {}): MatrixIconSample => ({
|
|
458
|
+
key: "button:0/svg:0",
|
|
459
|
+
context: "Open",
|
|
460
|
+
status: "conveying",
|
|
461
|
+
paint: "#ffffff",
|
|
462
|
+
background: "#111111",
|
|
463
|
+
backgroundResolved: true,
|
|
464
|
+
overImage: false,
|
|
465
|
+
shapeCount: 1,
|
|
466
|
+
distinctPaints: 1,
|
|
467
|
+
paintKinds: ["fill"],
|
|
468
|
+
rect: { x: 0, y: 0, w: 24, h: 24 },
|
|
469
|
+
...overrides,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("holds real glyph paint to exactly 3:1 regardless of text rules", () => {
|
|
473
|
+
expect(evaluateIconContrast([sample()]).ok).toBe(true);
|
|
474
|
+
const result = evaluateIconContrast([sample({ paint: "#222222" })]);
|
|
475
|
+
expect(result.ok).toBe(false);
|
|
476
|
+
expect(result.measured).toBe(1);
|
|
477
|
+
expect(result.violations[0].required).toBe(3);
|
|
478
|
+
expect(result.violations[0].context).toBe("Open");
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("keeps a just-below-3 icon sample in the violations", () => {
|
|
482
|
+
const result = evaluateIconContrast([sample({ paint: "#000000", background: "#595959" })]);
|
|
483
|
+
expect(result.ok).toBe(false);
|
|
484
|
+
expect(result.measured).toBe(1);
|
|
485
|
+
expect(result.violations).toHaveLength(1);
|
|
486
|
+
expect(result.violations[0].ratio).toBeGreaterThan(2.995);
|
|
487
|
+
expect(result.violations[0].ratio).toBeLessThan(3);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it.each<IconUnmeasurableReason>([
|
|
491
|
+
"unresolved-backdrop",
|
|
492
|
+
"masked-or-filtered",
|
|
493
|
+
"use-indirection",
|
|
494
|
+
"unsupported-paint",
|
|
495
|
+
"unsupported-compositing",
|
|
496
|
+
"multicolor",
|
|
497
|
+
"no-paint",
|
|
498
|
+
])("does not pass a candidate with %s", (reason) => {
|
|
499
|
+
const result = evaluateIconContrast([sample({ unmeasurable: reason })]);
|
|
500
|
+
expect(result.ok).toBe(false);
|
|
501
|
+
expect(result.measured).toBe(0);
|
|
502
|
+
expect(result.candidates).toBe(1);
|
|
503
|
+
expect(result.unverified).toEqual([{ key: "button:0/svg:0", context: "Open", reason }]);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("accounts for every candidate and exclusion, including invalid color", () => {
|
|
507
|
+
const result = evaluateIconContrast([
|
|
508
|
+
sample(),
|
|
509
|
+
sample({ paint: "not-a-color" }),
|
|
510
|
+
sample({ status: "disabled-control" }),
|
|
511
|
+
sample({ status: "decorative-standalone" }),
|
|
512
|
+
sample({ status: "hidden" }),
|
|
513
|
+
]);
|
|
514
|
+
expect(result.candidates).toBe(result.measured + result.unverified.length);
|
|
515
|
+
expect(
|
|
516
|
+
result.candidates +
|
|
517
|
+
result.excludedDisabled +
|
|
518
|
+
result.excludedDecorative +
|
|
519
|
+
result.excludedHidden,
|
|
520
|
+
).toBe(5);
|
|
521
|
+
expect(result.ok).toBe(false);
|
|
522
|
+
expect(evaluateIconContrast([]).ok).toBe(true);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("names a replacement icon failure even when total findings stay equal", () => {
|
|
526
|
+
const baselineIcons = evaluateIconContrast([sample({ context: "Old", paint: "#222222" })]);
|
|
527
|
+
const icons = evaluateIconContrast([sample({ context: "New", paint: "#222222" })]);
|
|
528
|
+
const result = evaluateNoBreakage({
|
|
529
|
+
storyRendered: true,
|
|
530
|
+
offScaleViolations: 0,
|
|
531
|
+
baselineOffScaleViolations: 0,
|
|
532
|
+
contrast: { measured: 1, unmeasurable: 0, violations: [] },
|
|
533
|
+
baselineContrastViolations: 0,
|
|
534
|
+
icons,
|
|
535
|
+
baselineIcons,
|
|
536
|
+
});
|
|
537
|
+
expect(result.ok).toBe(false);
|
|
538
|
+
expect(result.failures[0]).toContain("New");
|
|
539
|
+
expect(result.failures[0]).not.toContain("Old");
|
|
540
|
+
const sameDebt = evaluateNoBreakage({
|
|
541
|
+
storyRendered: true,
|
|
542
|
+
offScaleViolations: 0,
|
|
543
|
+
baselineOffScaleViolations: 0,
|
|
544
|
+
contrast: { measured: 1, unmeasurable: 0, violations: [] },
|
|
545
|
+
baselineContrastViolations: 0,
|
|
546
|
+
icons: baselineIcons,
|
|
547
|
+
baselineIcons,
|
|
548
|
+
});
|
|
549
|
+
expect(sameDebt.ok).toBe(true); // Attribution only. Absolute icons.ok still fails.
|
|
550
|
+
expect(baselineIcons.ok).toBe(false);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("preserves legacy text-only calls but rejects an incomplete icon pair", () => {
|
|
554
|
+
const textOnly = {
|
|
555
|
+
storyRendered: true,
|
|
556
|
+
offScaleViolations: 0,
|
|
557
|
+
baselineOffScaleViolations: 0,
|
|
558
|
+
contrast: { measured: 1, unmeasurable: 0, violations: [] },
|
|
559
|
+
baselineContrastViolations: 0,
|
|
560
|
+
};
|
|
561
|
+
expect(evaluateNoBreakage(textOnly).ok).toBe(true);
|
|
562
|
+
expect(evaluateNoBreakage({ ...textOnly, icons: cleanIcons }).ok).toBe(false);
|
|
563
|
+
expect(evaluateNoBreakage({ ...textOnly, baselineIcons: cleanIcons }).ok).toBe(false);
|
|
564
|
+
});
|
|
565
|
+
});
|