@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +201 -0
  3. package/package.json +85 -0
  4. package/src/audit/baselines/home-login.json +171 -0
  5. package/src/audit/baselines/home-login.png +0 -0
  6. package/src/audit/baselines/home-pokemon.json +355 -0
  7. package/src/audit/baselines/home-pokemon.png +0 -0
  8. package/src/audit/baselines/home.json +194 -0
  9. package/src/audit/baselines/home.png +0 -0
  10. package/src/audit/baselines/pokemon.json +251 -0
  11. package/src/audit/baselines/pokemon.png +0 -0
  12. package/src/audit/constraintAudit.ts +874 -0
  13. package/src/audit/diffReport.spec.ts +191 -0
  14. package/src/audit/diffReport.ts +344 -0
  15. package/src/audit/index.ts +10 -0
  16. package/src/font.ts +49 -0
  17. package/src/fonts/createFontLoader.ts +15 -0
  18. package/src/fonts/createFontLoader.web.ts +9 -0
  19. package/src/fonts/index.ts +2 -0
  20. package/src/fonts/types.ts +13 -0
  21. package/src/index.ts +3 -0
  22. package/src/theme/Intent.tsx +30 -0
  23. package/src/theme/Preset.tsx +93 -0
  24. package/src/theme/PresetContext.ts +27 -0
  25. package/src/theme/Tint.tsx +44 -0
  26. package/src/theme/__snapshots__/resolveKnobs.spec.ts.snap +69 -0
  27. package/src/theme/animations/css.ts +25 -0
  28. package/src/theme/animations/index.ts +1 -0
  29. package/src/theme/animations/index.web.ts +1 -0
  30. package/src/theme/animations/reactNative.ts +54 -0
  31. package/src/theme/colorRules.spec.ts +195 -0
  32. package/src/theme/colorRules.ts +166 -0
  33. package/src/theme/cookies.spec.ts +185 -0
  34. package/src/theme/cookies.ts +209 -0
  35. package/src/theme/createDefaultThemeConfig.ts +86 -0
  36. package/src/theme/createThemes.ts +286 -0
  37. package/src/theme/devtools/ColorLineVisualizer.tsx +261 -0
  38. package/src/theme/devtools/ThemeDevtoolsPanel.tsx +1657 -0
  39. package/src/theme/devtools/buildSnapshot.spec.ts +113 -0
  40. package/src/theme/devtools/buildSnapshot.ts +103 -0
  41. package/src/theme/devtools/knobIcons.tsx +217 -0
  42. package/src/theme/devtools/vizConstants.spec.ts +58 -0
  43. package/src/theme/devtools/vizConstants.ts +28 -0
  44. package/src/theme/index.ts +21 -0
  45. package/src/theme/intent.spec.tsx +276 -0
  46. package/src/theme/intents.ts +22 -0
  47. package/src/theme/knobs.ts +147 -0
  48. package/src/theme/pokemonScreen.spec.ts +109 -0
  49. package/src/theme/preset.spec.tsx +242 -0
  50. package/src/theme/preset.types.ts +22 -0
  51. package/src/theme/presets.spec.ts +252 -0
  52. package/src/theme/presets.ts +49 -0
  53. package/src/theme/recipes.ts +69 -0
  54. package/src/theme/resolveKnobs.spec.ts +571 -0
  55. package/src/theme/resolveKnobs.ts +226 -0
  56. package/src/theme/shared.spec.ts +84 -0
  57. package/src/theme/shared.tsx +131 -0
  58. package/src/theme/theme.native.tsx +65 -0
  59. package/src/theme/theme.tsx +190 -0
  60. package/src/theme/tint.spec.tsx +179 -0
  61. package/src/theme/useColorScale.native.ts +7 -0
  62. package/src/theme/useColorScale.ts +29 -0
  63. package/src/theme/useResolvedKnobs.ts +121 -0
  64. package/src/theme/useTheme.native.ts +49 -0
  65. package/src/theme/useTheme.ts +213 -0
  66. package/tsconfig.json +11 -0
  67. 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
+ });