@multiplatform.one/theme 6.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.
- package/CHANGELOG.md +18 -0
- package/LICENSE +201 -0
- package/package.json +85 -0
- package/src/audit/baselines/home-login.json +171 -0
- package/src/audit/baselines/home-login.png +0 -0
- package/src/audit/baselines/home-pokemon.json +355 -0
- package/src/audit/baselines/home-pokemon.png +0 -0
- package/src/audit/baselines/home.json +194 -0
- package/src/audit/baselines/home.png +0 -0
- package/src/audit/baselines/pokemon.json +251 -0
- package/src/audit/baselines/pokemon.png +0 -0
- package/src/audit/constraintAudit.ts +874 -0
- package/src/audit/diffReport.spec.ts +191 -0
- package/src/audit/diffReport.ts +344 -0
- package/src/audit/index.ts +10 -0
- package/src/font.ts +49 -0
- package/src/fonts/createFontLoader.ts +15 -0
- package/src/fonts/createFontLoader.web.ts +9 -0
- package/src/fonts/index.ts +2 -0
- package/src/fonts/types.ts +13 -0
- package/src/index.ts +3 -0
- package/src/theme/Intent.tsx +30 -0
- package/src/theme/Preset.tsx +93 -0
- package/src/theme/PresetContext.ts +27 -0
- package/src/theme/Tint.tsx +44 -0
- package/src/theme/__snapshots__/resolveKnobs.spec.ts.snap +69 -0
- package/src/theme/animations/css.ts +25 -0
- package/src/theme/animations/index.ts +1 -0
- package/src/theme/animations/index.web.ts +1 -0
- package/src/theme/animations/reactNative.ts +54 -0
- package/src/theme/colorRules.spec.ts +195 -0
- package/src/theme/colorRules.ts +166 -0
- package/src/theme/cookies.spec.ts +185 -0
- package/src/theme/cookies.ts +209 -0
- package/src/theme/createDefaultThemeConfig.ts +86 -0
- package/src/theme/createThemes.ts +286 -0
- package/src/theme/devtools/ColorLineVisualizer.tsx +261 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +1657 -0
- package/src/theme/devtools/buildSnapshot.spec.ts +113 -0
- package/src/theme/devtools/buildSnapshot.ts +103 -0
- package/src/theme/devtools/knobIcons.tsx +217 -0
- package/src/theme/devtools/vizConstants.spec.ts +58 -0
- package/src/theme/devtools/vizConstants.ts +28 -0
- package/src/theme/index.ts +21 -0
- package/src/theme/intent.spec.tsx +276 -0
- package/src/theme/intents.ts +22 -0
- package/src/theme/knobs.ts +147 -0
- package/src/theme/pokemonScreen.spec.ts +109 -0
- package/src/theme/preset.spec.tsx +242 -0
- package/src/theme/preset.types.ts +22 -0
- package/src/theme/presets.spec.ts +252 -0
- package/src/theme/presets.ts +49 -0
- package/src/theme/recipes.ts +69 -0
- package/src/theme/resolveKnobs.spec.ts +571 -0
- package/src/theme/resolveKnobs.ts +226 -0
- package/src/theme/shared.spec.ts +84 -0
- package/src/theme/shared.tsx +131 -0
- package/src/theme/theme.native.tsx +65 -0
- package/src/theme/theme.tsx +190 -0
- package/src/theme/tint.spec.tsx +179 -0
- package/src/theme/useColorScale.native.ts +7 -0
- package/src/theme/useColorScale.ts +29 -0
- package/src/theme/useResolvedKnobs.ts +121 -0
- package/src/theme/useTheme.native.ts +49 -0
- package/src/theme/useTheme.ts +213 -0
- package/tsconfig.json +11 -0
- package/vitest.config.mjs +9 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { defaultKnobs, emptyStateKnobs, Space, Size } from "./knobs";
|
|
3
|
+
import type { Knobs } from "./knobs";
|
|
4
|
+
import type { KnobProps } from "./recipes";
|
|
5
|
+
import { resolveKnobs } from "./resolveKnobs";
|
|
6
|
+
import {
|
|
7
|
+
normalizeToHex,
|
|
8
|
+
relativeLuminance,
|
|
9
|
+
contrastRatio,
|
|
10
|
+
findReadableStep,
|
|
11
|
+
semanticGroups,
|
|
12
|
+
} from "./colorRules";
|
|
13
|
+
|
|
14
|
+
function knobsWith(overrides: Partial<Knobs>): Knobs {
|
|
15
|
+
return { ...defaultKnobs, ...overrides };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const expectedKnobPropKeys: (keyof KnobProps)[] = [
|
|
19
|
+
"borderRadius",
|
|
20
|
+
"borderRadiusOuter",
|
|
21
|
+
"elevation",
|
|
22
|
+
"surface",
|
|
23
|
+
"inputSurface",
|
|
24
|
+
"panelPadding",
|
|
25
|
+
"gap",
|
|
26
|
+
"gapLg",
|
|
27
|
+
"sizeToken",
|
|
28
|
+
"heading",
|
|
29
|
+
"body",
|
|
30
|
+
"textWeight",
|
|
31
|
+
"transition",
|
|
32
|
+
"outlined",
|
|
33
|
+
"pointy",
|
|
34
|
+
"textAccent",
|
|
35
|
+
"textAccentColor",
|
|
36
|
+
"inputBackground",
|
|
37
|
+
"space",
|
|
38
|
+
"size",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// ── colorRules unit tests ────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
describe("colorRules", () => {
|
|
44
|
+
describe("luminance utilities", () => {
|
|
45
|
+
it("normalizeToHex handles hex, short hex, hsl, and named colors", () => {
|
|
46
|
+
expect(normalizeToHex("#626262")).toBe("#626262");
|
|
47
|
+
expect(normalizeToHex("#fff")).toBe("#ffffff");
|
|
48
|
+
expect(normalizeToHex("white")).toBe("#ffffff");
|
|
49
|
+
expect(normalizeToHex("black")).toBe("#000000");
|
|
50
|
+
expect(normalizeToHex("hsl(0, 0%, 50%)")).toBe("#808080");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("contrastRatio of black vs white ≈ 21", () => {
|
|
54
|
+
const lBlack = relativeLuminance("#000000");
|
|
55
|
+
const lWhite = relativeLuminance("#ffffff");
|
|
56
|
+
expect(contrastRatio(lBlack, lWhite)).toBeCloseTo(21, 0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("findReadableStep picks a step meeting MIN_CONTRAST_RATIO", () => {
|
|
60
|
+
const grayDark = [
|
|
61
|
+
"#050505",
|
|
62
|
+
"#151515",
|
|
63
|
+
"#191919",
|
|
64
|
+
"#232323",
|
|
65
|
+
"#282828",
|
|
66
|
+
"#323232",
|
|
67
|
+
"#424242",
|
|
68
|
+
"#494949",
|
|
69
|
+
"#545454",
|
|
70
|
+
"#626262",
|
|
71
|
+
"#a5a5a5",
|
|
72
|
+
"#ffffff",
|
|
73
|
+
];
|
|
74
|
+
const bg = relativeLuminance(grayDark[9]);
|
|
75
|
+
const fg = relativeLuminance(grayDark[5]);
|
|
76
|
+
expect(contrastRatio(bg, fg)).toBeLessThan(3);
|
|
77
|
+
|
|
78
|
+
const readable = findReadableStep(grayDark, 10, 6, -1);
|
|
79
|
+
const readableLum = relativeLuminance(grayDark[readable - 1]);
|
|
80
|
+
expect(contrastRatio(bg, readableLum)).toBeGreaterThanOrEqual(3);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("semantic groups", () => {
|
|
85
|
+
it("covers all 12 steps without gaps", () => {
|
|
86
|
+
const covered = new Set<number>();
|
|
87
|
+
for (const g of semanticGroups) {
|
|
88
|
+
for (let s = g.from; s <= g.to; s++) {
|
|
89
|
+
covered.add(s);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (let i = 1; i <= 12; i++) {
|
|
93
|
+
expect(covered.has(i)).toBe(true);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ── resolveKnobs tests ───────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
describe("resolveKnobs", () => {
|
|
102
|
+
it("produces all knobProps keys from defaultKnobs", () => {
|
|
103
|
+
const { knobProps } = resolveKnobs(defaultKnobs);
|
|
104
|
+
for (const key of expectedKnobPropKeys) {
|
|
105
|
+
expect(knobProps).toHaveProperty(key);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("is deterministic (same input → same output)", () => {
|
|
110
|
+
const a = resolveKnobs(defaultKnobs);
|
|
111
|
+
const b = resolveKnobs(defaultKnobs);
|
|
112
|
+
expect(a).toEqual(b);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("matches snapshot for defaultKnobs", () => {
|
|
116
|
+
const resolved = resolveKnobs(defaultKnobs);
|
|
117
|
+
expect(resolved).toMatchSnapshot();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ── T1.1g — expectedKnobPropKeys includes "size" ────────
|
|
121
|
+
it('expectedKnobPropKeys includes "size"', () => {
|
|
122
|
+
expect(expectedKnobPropKeys).toContain("size");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── borderRadius ──────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
describe("borderRadius knob", () => {
|
|
128
|
+
it('maps "none" → "$0"', () => {
|
|
129
|
+
const { knobProps } = resolveKnobs(knobsWith({ borderRadius: "none" }));
|
|
130
|
+
expect(knobProps.borderRadius.borderRadius).toBe("$0");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('maps "small" → "$2"', () => {
|
|
134
|
+
const { knobProps } = resolveKnobs(knobsWith({ borderRadius: "small" }));
|
|
135
|
+
expect(knobProps.borderRadius.borderRadius).toBe("$2");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('maps "medium" → "$4"', () => {
|
|
139
|
+
const { knobProps } = resolveKnobs(knobsWith({ borderRadius: "medium" }));
|
|
140
|
+
expect(knobProps.borderRadius.borderRadius).toBe("$4");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('maps "large" → "$6"', () => {
|
|
144
|
+
const { knobProps } = resolveKnobs(knobsWith({ borderRadius: "large" }));
|
|
145
|
+
expect(knobProps.borderRadius.borderRadius).toBe("$6");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('maps "full" → "$12"', () => {
|
|
149
|
+
const { knobProps } = resolveKnobs(knobsWith({ borderRadius: "full" }));
|
|
150
|
+
expect(knobProps.borderRadius.borderRadius).toBe("$12");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ── borderWidth ───────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
describe("borderWidth knob", () => {
|
|
157
|
+
it('maps "none" → 0', () => {
|
|
158
|
+
const { knobProps } = resolveKnobs(knobsWith({ borderWidth: "none" }));
|
|
159
|
+
expect(knobProps.borderRadius.borderWidth).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('maps "large" → 2', () => {
|
|
163
|
+
const { knobProps } = resolveKnobs(knobsWith({ borderWidth: "large" }));
|
|
164
|
+
expect(knobProps.borderRadius.borderWidth).toBe(2);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("propagates to surface", () => {
|
|
168
|
+
const { knobProps } = resolveKnobs(knobsWith({ borderWidth: "large" }));
|
|
169
|
+
expect(knobProps.surface.borderWidth).toBe(2);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ── elevation ──────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
describe("elevation knob", () => {
|
|
176
|
+
it('maps "none" → undefined', () => {
|
|
177
|
+
expect(resolveKnobs(knobsWith({ elevation: "none" })).knobProps.elevation).toBeUndefined();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('maps "small" → "$1"', () => {
|
|
181
|
+
expect(resolveKnobs(knobsWith({ elevation: "small" })).knobProps.elevation).toBe("$1");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('maps "medium" → "$2"', () => {
|
|
185
|
+
expect(resolveKnobs(knobsWith({ elevation: "medium" })).knobProps.elevation).toBe("$2");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('maps "large" → "$4"', () => {
|
|
189
|
+
expect(resolveKnobs(knobsWith({ elevation: "large" })).knobProps.elevation).toBe("$4");
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ── interaction state overrides ──────────────────────────────
|
|
194
|
+
|
|
195
|
+
describe("interaction state overrides", () => {
|
|
196
|
+
it("hover.elevation maps to elevation.hoverKnobProps.elevation", () => {
|
|
197
|
+
const { elevation } = resolveKnobs(knobsWith({ hover: { elevation: "medium" } }));
|
|
198
|
+
expect(elevation.hoverKnobProps?.elevation).toBe("$2");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("press.elevation maps to elevation.pressKnobProps.elevation", () => {
|
|
202
|
+
const { elevation } = resolveKnobs(knobsWith({ press: { elevation: "large" } }));
|
|
203
|
+
expect(elevation.pressKnobProps?.elevation).toBe("$4");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("hover.borderWidth maps to control.hoverKnobProps.borderWidth", () => {
|
|
207
|
+
const { control } = resolveKnobs(knobsWith({ hover: { borderWidth: "large" } }));
|
|
208
|
+
expect(control.hoverKnobProps?.borderWidth).toBe(2);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("hover.borderRadius maps to control.hoverKnobProps.borderRadius", () => {
|
|
212
|
+
const { control } = resolveKnobs(knobsWith({ hover: { borderRadius: "full" } }));
|
|
213
|
+
expect(control.hoverKnobProps?.borderRadius).toBe("$12");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("empty state overrides produce undefined", () => {
|
|
217
|
+
const { control } = resolveKnobs(defaultKnobs);
|
|
218
|
+
expect(control.hoverKnobProps).toBeUndefined();
|
|
219
|
+
expect(control.pressKnobProps).toBeUndefined();
|
|
220
|
+
expect(control.focusKnobProps).toBeUndefined();
|
|
221
|
+
expect(control.focusVisibleKnobProps).toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("focus.elevation maps to elevation.focusKnobProps.elevation", () => {
|
|
225
|
+
const { elevation } = resolveKnobs(knobsWith({ focus: { elevation: "small" } }));
|
|
226
|
+
expect(elevation.focusKnobProps?.elevation).toBe("$1");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("focusVisible.borderWidth maps to control.focusVisibleKnobProps.borderWidth", () => {
|
|
230
|
+
const { control } = resolveKnobs(knobsWith({ focusVisible: { borderWidth: "medium" } }));
|
|
231
|
+
expect(control.focusVisibleKnobProps?.borderWidth).toBe(1);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ── space ──────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
describe("space knob", () => {
|
|
238
|
+
it('maps panelPadding "small" → "$3"', () => {
|
|
239
|
+
const { knobProps } = resolveKnobs(knobsWith({ space: "small" }));
|
|
240
|
+
expect(knobProps.panelPadding.padding).toBe("$3");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('maps gap "large" → "$4"', () => {
|
|
244
|
+
const { knobProps } = resolveKnobs(knobsWith({ space: "large" }));
|
|
245
|
+
expect(knobProps.gap.gap).toBe("$4");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("passes space value through to knobProps", () => {
|
|
249
|
+
const { knobProps } = resolveKnobs(knobsWith({ space: "large" }));
|
|
250
|
+
expect(knobProps.space).toBe("large");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// T2.1a — space "large" no longer changes sizeToken
|
|
254
|
+
it("space does not affect sizeToken", () => {
|
|
255
|
+
const { knobProps } = resolveKnobs(knobsWith({ space: "large", size: "medium" }));
|
|
256
|
+
expect(knobProps.sizeToken).toBe("$4");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// T2.1b — space "small" still maps panelPadding regardless of size
|
|
260
|
+
it('space "small" maps panelPadding → "$3" regardless of size', () => {
|
|
261
|
+
const { knobProps } = resolveKnobs(knobsWith({ space: "small", size: "large" }));
|
|
262
|
+
expect(knobProps.panelPadding.padding).toBe("$3");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// T2.1c — space "large" still maps gapLg → "$5" regardless of size
|
|
266
|
+
it('space "large" maps gapLg → "$5" regardless of size', () => {
|
|
267
|
+
const { knobProps } = resolveKnobs(knobsWith({ space: "large", size: "small" }));
|
|
268
|
+
expect(knobProps.gapLg.gap).toBe("$5");
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// ── size ──────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
describe("size knob", () => {
|
|
275
|
+
// T1.1b — size "large" maps sizeToken → "$5"
|
|
276
|
+
it('maps sizeToken "large" → "$5"', () => {
|
|
277
|
+
const { knobProps } = resolveKnobs(knobsWith({ size: "large" }));
|
|
278
|
+
expect(knobProps.sizeToken).toBe("$5");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// T1.1c — size "small" maps sizeToken → "$3"
|
|
282
|
+
it('maps sizeToken "small" → "$3"', () => {
|
|
283
|
+
const { knobProps } = resolveKnobs(knobsWith({ size: "small" }));
|
|
284
|
+
expect(knobProps.sizeToken).toBe("$3");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// T1.1e — knobProps.size passthrough
|
|
288
|
+
it("passes size value through to knobProps", () => {
|
|
289
|
+
for (const val of Object.values(Size)) {
|
|
290
|
+
const { knobProps } = resolveKnobs(knobsWith({ size: val }));
|
|
291
|
+
expect(knobProps.size).toBe(val);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// T1.1f — independent axes: size "large" + space "small"
|
|
296
|
+
it('size "large" + space "small" → sizeToken "$5" AND gap "$2"', () => {
|
|
297
|
+
const { knobProps } = resolveKnobs(knobsWith({ size: "large", space: "small" }));
|
|
298
|
+
expect(knobProps.sizeToken).toBe("$5");
|
|
299
|
+
expect(knobProps.gap.gap).toBe("$2");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// T2.1e — changing size does NOT affect panelPadding, gap, or gapLg
|
|
303
|
+
it("changing size does not affect panelPadding, gap, or gapLg", () => {
|
|
304
|
+
const baseline = resolveKnobs(knobsWith({ size: "medium", space: "medium" }));
|
|
305
|
+
for (const sizeVal of Object.values(Size)) {
|
|
306
|
+
const { knobProps } = resolveKnobs(knobsWith({ size: sizeVal, space: "medium" }));
|
|
307
|
+
expect(knobProps.panelPadding).toEqual(baseline.knobProps.panelPadding);
|
|
308
|
+
expect(knobProps.gap).toEqual(baseline.knobProps.gap);
|
|
309
|
+
expect(knobProps.gapLg).toEqual(baseline.knobProps.gapLg);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// T2.1f — default knobs produce identical output to prior default
|
|
314
|
+
it("default knobs (size+space both medium) produce consistent output", () => {
|
|
315
|
+
const resolved = resolveKnobs(defaultKnobs);
|
|
316
|
+
expect(resolved.knobProps.sizeToken).toBe("$4");
|
|
317
|
+
expect(resolved.knobProps.gap.gap).toBe("$3");
|
|
318
|
+
expect(resolved.knobProps.panelPadding.padding).toBe("$4");
|
|
319
|
+
expect(resolved.knobProps.gapLg.gap).toBe("$4");
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// ── fillStyle ─────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
describe("fillStyle knob", () => {
|
|
326
|
+
it("filled → outlined is false", () => {
|
|
327
|
+
const { knobProps } = resolveKnobs(knobsWith({ fillStyle: "filled" }));
|
|
328
|
+
expect(knobProps.outlined).toBe(false);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("outlined → outlined is true", () => {
|
|
332
|
+
const { knobProps } = resolveKnobs(knobsWith({ fillStyle: "outlined" }));
|
|
333
|
+
expect(knobProps.outlined).toBe(true);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ── pointy ────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
describe("pointy derivation", () => {
|
|
340
|
+
it('is true when borderRadius is "none"', () => {
|
|
341
|
+
const { knobProps } = resolveKnobs(knobsWith({ borderRadius: "none" }));
|
|
342
|
+
expect(knobProps.pointy).toBe(true);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('is false when borderRadius is "small"', () => {
|
|
346
|
+
const { knobProps } = resolveKnobs(knobsWith({ borderRadius: "small" }));
|
|
347
|
+
expect(knobProps.pointy).toBe(false);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('is false when borderRadius is "medium"', () => {
|
|
351
|
+
const { knobProps } = resolveKnobs(knobsWith({ borderRadius: "medium" }));
|
|
352
|
+
expect(knobProps.pointy).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('is false when borderRadius is "full"', () => {
|
|
356
|
+
const { knobProps } = resolveKnobs(knobsWith({ borderRadius: "full" }));
|
|
357
|
+
expect(knobProps.pointy).toBe(false);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("is false for default knobs", () => {
|
|
361
|
+
const { knobProps } = resolveKnobs(defaultKnobs);
|
|
362
|
+
expect(knobProps.pointy).toBe(false);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// ── textAccent ────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
describe("textAccent knob", () => {
|
|
369
|
+
it("passes through to knobProps", () => {
|
|
370
|
+
const high = resolveKnobs(knobsWith({ textAccent: "high" }));
|
|
371
|
+
expect(high.knobProps.textAccent).toBe("high");
|
|
372
|
+
|
|
373
|
+
const medium = resolveKnobs(knobsWith({ textAccent: "medium" }));
|
|
374
|
+
expect(medium.knobProps.textAccent).toBe("medium");
|
|
375
|
+
|
|
376
|
+
const low = resolveKnobs(knobsWith({ textAccent: "low" }));
|
|
377
|
+
expect(low.knobProps.textAccent).toBe("low");
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// ── typography knobs ────────────────────────────────────────
|
|
382
|
+
|
|
383
|
+
describe("typography knobs", () => {
|
|
384
|
+
it("heading font maps correctly", () => {
|
|
385
|
+
expect(
|
|
386
|
+
resolveKnobs(knobsWith({ headingFont: "sans-serif" })).knobProps.heading.fontFamily,
|
|
387
|
+
).toBe("$heading");
|
|
388
|
+
expect(resolveKnobs(knobsWith({ headingFont: "serif" })).knobProps.heading.fontFamily).toBe(
|
|
389
|
+
"$serif",
|
|
390
|
+
);
|
|
391
|
+
expect(resolveKnobs(knobsWith({ headingFont: "mono" })).knobProps.heading.fontFamily).toBe(
|
|
392
|
+
"$mono",
|
|
393
|
+
);
|
|
394
|
+
expect(resolveKnobs(knobsWith({ headingFont: "slab" })).knobProps.heading.fontFamily).toBe(
|
|
395
|
+
"$slab",
|
|
396
|
+
);
|
|
397
|
+
expect(resolveKnobs(knobsWith({ headingFont: "pixel" })).knobProps.heading.fontFamily).toBe(
|
|
398
|
+
"$pixel",
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("body font maps correctly", () => {
|
|
403
|
+
expect(resolveKnobs(knobsWith({ bodyFont: "sans-serif" })).knobProps.body.fontFamily).toBe(
|
|
404
|
+
"$body",
|
|
405
|
+
);
|
|
406
|
+
expect(resolveKnobs(knobsWith({ bodyFont: "serif" })).knobProps.body.fontFamily).toBe(
|
|
407
|
+
"$serif",
|
|
408
|
+
);
|
|
409
|
+
expect(resolveKnobs(knobsWith({ bodyFont: "mono" })).knobProps.body.fontFamily).toBe("$mono");
|
|
410
|
+
expect(resolveKnobs(knobsWith({ bodyFont: "slab" })).knobProps.body.fontFamily).toBe("$slab");
|
|
411
|
+
expect(resolveKnobs(knobsWith({ bodyFont: "pixel" })).knobProps.body.fontFamily).toBe(
|
|
412
|
+
"$pixel",
|
|
413
|
+
);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("fontWeight maps correctly", () => {
|
|
417
|
+
expect(resolveKnobs(knobsWith({ fontWeight: "regular" })).knobProps.heading.fontWeight).toBe(
|
|
418
|
+
"400",
|
|
419
|
+
);
|
|
420
|
+
expect(resolveKnobs(knobsWith({ fontWeight: "bold" })).knobProps.heading.fontWeight).toBe(
|
|
421
|
+
"700",
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// ── animation ─────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
describe("animation knob", () => {
|
|
429
|
+
it('"none" → transition is undefined', () => {
|
|
430
|
+
const { knobProps } = resolveKnobs(knobsWith({ animation: "none" }));
|
|
431
|
+
expect(knobProps.transition).toBeUndefined();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('"bouncy" → transition is "bouncy"', () => {
|
|
435
|
+
const { knobProps } = resolveKnobs(knobsWith({ animation: "bouncy" }));
|
|
436
|
+
expect(knobProps.transition).toBe("bouncy");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('"quick" → transition is "quick"', () => {
|
|
440
|
+
const { knobProps } = resolveKnobs(knobsWith({ animation: "quick" }));
|
|
441
|
+
expect(knobProps.transition).toBe("quick");
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// ── override callback ─────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
describe("override callback", () => {
|
|
448
|
+
it("merges partial knobProps overrides on top", () => {
|
|
449
|
+
const { knobProps } = resolveKnobs(defaultKnobs, () => ({
|
|
450
|
+
elevation: "$4",
|
|
451
|
+
}));
|
|
452
|
+
expect(knobProps.elevation).toBe("$4");
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("does not mutate original knobProps", () => {
|
|
456
|
+
const baseline = resolveKnobs(defaultKnobs);
|
|
457
|
+
const overridden = resolveKnobs(defaultKnobs, () => ({
|
|
458
|
+
elevation: "$4",
|
|
459
|
+
}));
|
|
460
|
+
expect(baseline.knobProps.elevation).toBeUndefined();
|
|
461
|
+
expect(overridden.knobProps.elevation).toBe("$4");
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// T4.1g — override callback receives knobProps with both size and space
|
|
465
|
+
it("override callback receives knobProps with both size and space", () => {
|
|
466
|
+
let receivedProps: KnobProps | undefined;
|
|
467
|
+
resolveKnobs(knobsWith({ size: "large", space: "small" }), (props) => {
|
|
468
|
+
receivedProps = props;
|
|
469
|
+
return {};
|
|
470
|
+
});
|
|
471
|
+
expect(receivedProps).toBeDefined();
|
|
472
|
+
expect(receivedProps!.size).toBe("large");
|
|
473
|
+
expect(receivedProps!.space).toBe("small");
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// ── Group 4: Gap review tests ─────────────────────────────
|
|
478
|
+
|
|
479
|
+
describe("gap review", () => {
|
|
480
|
+
const sizeTokenMap: Record<string, string> = {
|
|
481
|
+
small: "$3",
|
|
482
|
+
medium: "$4",
|
|
483
|
+
large: "$5",
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const gapMap: Record<string, string> = {
|
|
487
|
+
small: "$2",
|
|
488
|
+
medium: "$3",
|
|
489
|
+
large: "$4",
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const panelPaddingMap: Record<string, string> = {
|
|
493
|
+
small: "$3",
|
|
494
|
+
medium: "$4",
|
|
495
|
+
large: "$6",
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const gapLgMap: Record<string, string> = {
|
|
499
|
+
small: "$3",
|
|
500
|
+
medium: "$4",
|
|
501
|
+
large: "$5",
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
// T4.1a — every Size enum value produces a valid sizeToken
|
|
505
|
+
it("every Size enum value produces a valid sizeToken", () => {
|
|
506
|
+
for (const sizeVal of Object.values(Size)) {
|
|
507
|
+
const { knobProps } = resolveKnobs(knobsWith({ size: sizeVal }));
|
|
508
|
+
expect(knobProps.sizeToken).toBe(sizeTokenMap[sizeVal]);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// T4.1b — every Space enum value produces valid panelPadding, gap, gapLg
|
|
513
|
+
it("every Space enum value produces valid panelPadding, gap, gapLg", () => {
|
|
514
|
+
for (const spaceVal of Object.values(Space)) {
|
|
515
|
+
const { knobProps } = resolveKnobs(knobsWith({ space: spaceVal }));
|
|
516
|
+
expect(knobProps.panelPadding.padding).toBe(panelPaddingMap[spaceVal]);
|
|
517
|
+
expect(knobProps.gap.gap).toBe(gapMap[spaceVal]);
|
|
518
|
+
expect(knobProps.gapLg.gap).toBe(gapLgMap[spaceVal]);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// T4.1c — all 9 combinations of size × space
|
|
523
|
+
it("all 9 size × space combinations produce expected sizeToken and gap pairs", () => {
|
|
524
|
+
for (const sizeVal of Object.values(Size)) {
|
|
525
|
+
for (const spaceVal of Object.values(Space)) {
|
|
526
|
+
const { knobProps } = resolveKnobs(knobsWith({ size: sizeVal, space: spaceVal }));
|
|
527
|
+
expect(knobProps.sizeToken).toBe(sizeTokenMap[sizeVal]);
|
|
528
|
+
expect(knobProps.gap.gap).toBe(gapMap[spaceVal]);
|
|
529
|
+
expect(knobProps.panelPadding.padding).toBe(panelPaddingMap[spaceVal]);
|
|
530
|
+
expect(knobProps.gapLg.gap).toBe(gapLgMap[spaceVal]);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// T4.1e — knobProps.space still equals knobs.space
|
|
536
|
+
it("knobProps.space equals knobs.space", () => {
|
|
537
|
+
for (const spaceVal of Object.values(Space)) {
|
|
538
|
+
const { knobProps } = resolveKnobs(knobsWith({ space: spaceVal }));
|
|
539
|
+
expect(knobProps.space).toBe(spaceVal);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// T4.1f — knobProps.size equals knobs.size
|
|
544
|
+
it("knobProps.size equals knobs.size", () => {
|
|
545
|
+
for (const sizeVal of Object.values(Size)) {
|
|
546
|
+
const { knobProps } = resolveKnobs(knobsWith({ size: sizeVal }));
|
|
547
|
+
expect(knobProps.size).toBe(sizeVal);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// T4.1h — defaultKnobs has both size and space set to "medium"
|
|
552
|
+
it('defaultKnobs has both size and space set to "medium"', () => {
|
|
553
|
+
expect(defaultKnobs.size).toBe("medium");
|
|
554
|
+
expect(defaultKnobs.space).toBe("medium");
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// T4.1i — emptyStateKnobs is still valid
|
|
558
|
+
it("emptyStateKnobs is still valid (no required size field breaks it)", () => {
|
|
559
|
+
const knobs = knobsWith({ hover: emptyStateKnobs });
|
|
560
|
+
const { control } = resolveKnobs(knobs);
|
|
561
|
+
expect(control.hoverKnobProps).toBeUndefined();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// T4.1j — size "large" does not change borderRadius
|
|
565
|
+
it('size "large" does not change borderRadius', () => {
|
|
566
|
+
const baseline = resolveKnobs(knobsWith({ size: "medium" }));
|
|
567
|
+
const large = resolveKnobs(knobsWith({ size: "large" }));
|
|
568
|
+
expect(large.knobProps.borderRadius).toEqual(baseline.knobProps.borderRadius);
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
});
|