@multiplatform.one/theme 7.4.1 → 7.6.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/README.md +2 -1
- package/package.json +5 -5
- package/src/audit/index.ts +19 -0
- package/src/audit/themeMatrix.spec.ts +274 -0
- package/src/audit/themeMatrix.ts +630 -0
- package/src/figma/figmaTokens.spec.ts +25 -21
- package/src/figma/figmaTokens.ts +2 -2
- package/src/font.ts +37 -25
- package/src/index.ts +12 -0
- package/src/keyboardFocusRing.ts +12 -6
- package/src/theme/FontKnobStyles.tsx +14 -31
- package/src/theme/Tint.tsx +3 -2
- package/src/theme/animations/css.ts +14 -1
- package/src/theme/colorRules.spec.ts +306 -2
- package/src/theme/colorRules.ts +118 -0
- package/src/theme/cornerSmoothing.native.ts +39 -2
- package/src/theme/cornerSmoothing.spec.ts +270 -3
- package/src/theme/cornerSmoothing.ts +147 -10
- package/src/theme/createDefaultThemeConfig.spec.ts +107 -1
- package/src/theme/createDefaultThemeConfig.ts +27 -8
- package/src/theme/defaults/builderOptions.ts +23 -10
- package/src/theme/defaults/categoryFonts.ts +24 -0
- package/src/theme/defaults/fonts.spec.ts +163 -10
- package/src/theme/defaults/fonts.theme.spec.tsx +139 -0
- package/src/theme/defaults/fonts.ts +214 -50
- package/src/theme/defaults/index.ts +1 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +0 -11
- package/src/theme/focusState.spec.ts +119 -1
- package/src/theme/focusState.ts +32 -7
- package/src/theme/fontCategoryMetrics.ts +57 -0
- package/src/theme/fontCategoryStacks.ts +37 -0
- package/src/theme/fontKnobStyles.spec.ts +15 -2
- package/src/theme/glyphPaint.spec.ts +26 -0
- package/src/theme/glyphPaint.ts +46 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/knobs.ts +18 -2
- package/src/theme/layoutTokens.spec.ts +3 -1
- package/src/theme/layoutTokens.ts +47 -4
- package/src/theme/mpo13Proof.spec.ts +3 -3
- package/src/theme/mpo19Proof.spec.ts +285 -0
- package/src/theme/presets.ts +1 -1
- package/src/theme/radiusClass.spec.ts +394 -0
- package/src/theme/radiusClass.ts +203 -4
- package/src/theme/radiusClassTable.generated.ts +121 -0
- package/src/theme/recipeInputs.ts +223 -36
- package/src/theme/recipes.ts +68 -4
- package/src/theme/resolveKnobs.spec.ts +155 -13
- package/src/theme/resolveKnobs.ts +193 -27
- package/src/theme/sizeLadder.spec.ts +84 -0
- package/src/theme/sizeRecipes.spec.ts +19 -6
- package/src/theme/sizeRecipes.ts +43 -13
- package/src/theme/theme.native.tsx +2 -0
- package/src/theme/theme.provider.spec.ts +9 -0
- package/src/theme/theme.tsx +6 -3
- package/src/theme/tint.spec.tsx +36 -35
- package/src/theme/useResolvedKnobs.ts +6 -3
- package/types/audit/index.d.ts +2 -0
- package/types/audit/index.d.ts.map +1 -1
- package/types/audit/themeMatrix.d.ts +172 -0
- package/types/audit/themeMatrix.d.ts.map +1 -0
- package/types/figma/figmaTokens.d.ts +1 -1
- package/types/figma/figmaTokens.d.ts.map +1 -1
- package/types/font.d.ts.map +1 -1
- package/types/index.d.ts +2 -0
- package/types/index.d.ts.map +1 -1
- package/types/keyboardFocusRing.d.ts +11 -5
- package/types/keyboardFocusRing.d.ts.map +1 -1
- package/types/theme/FontKnobStyles.d.ts +8 -12
- package/types/theme/FontKnobStyles.d.ts.map +1 -1
- package/types/theme/Tint.d.ts +2 -1
- package/types/theme/Tint.d.ts.map +1 -1
- package/types/theme/animations/css.d.ts +15 -2
- package/types/theme/animations/css.d.ts.map +1 -1
- package/types/theme/colorRules.d.ts +59 -1
- package/types/theme/colorRules.d.ts.map +1 -1
- package/types/theme/cornerSmoothing.d.ts +42 -20
- package/types/theme/cornerSmoothing.d.ts.map +1 -1
- package/types/theme/cornerSmoothing.native.d.ts +22 -2
- package/types/theme/cornerSmoothing.native.d.ts.map +1 -1
- package/types/theme/createDefaultThemeConfig.d.ts +21 -3
- package/types/theme/createDefaultThemeConfig.d.ts.map +1 -1
- package/types/theme/defaults/builderOptions.d.ts.map +1 -1
- package/types/theme/defaults/categoryFonts.d.ts +23 -0
- package/types/theme/defaults/categoryFonts.d.ts.map +1 -0
- package/types/theme/defaults/fonts.d.ts +140 -2
- package/types/theme/defaults/fonts.d.ts.map +1 -1
- package/types/theme/defaults/index.d.ts +1 -0
- package/types/theme/defaults/index.d.ts.map +1 -1
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
- package/types/theme/focusState.d.ts +32 -6
- package/types/theme/focusState.d.ts.map +1 -1
- package/types/theme/fontCategoryMetrics.d.ts +36 -0
- package/types/theme/fontCategoryMetrics.d.ts.map +1 -0
- package/types/theme/fontCategoryStacks.d.ts +24 -0
- package/types/theme/fontCategoryStacks.d.ts.map +1 -0
- package/types/theme/glyphPaint.d.ts +16 -0
- package/types/theme/glyphPaint.d.ts.map +1 -0
- package/types/theme/index.d.ts +4 -0
- package/types/theme/index.d.ts.map +1 -1
- package/types/theme/knobs.d.ts +10 -2
- package/types/theme/knobs.d.ts.map +1 -1
- package/types/theme/layoutTokens.d.ts +46 -4
- package/types/theme/layoutTokens.d.ts.map +1 -1
- package/types/theme/radiusClass.d.ts +96 -4
- package/types/theme/radiusClass.d.ts.map +1 -1
- package/types/theme/radiusClassTable.generated.d.ts +146 -0
- package/types/theme/radiusClassTable.generated.d.ts.map +1 -0
- package/types/theme/recipeInputs.d.ts +85 -14
- package/types/theme/recipeInputs.d.ts.map +1 -1
- package/types/theme/recipes.d.ts +78 -4
- package/types/theme/recipes.d.ts.map +1 -1
- package/types/theme/resolveKnobs.d.ts +12 -1
- package/types/theme/resolveKnobs.d.ts.map +1 -1
- package/types/theme/sizeRecipes.d.ts +27 -14
- package/types/theme/sizeRecipes.d.ts.map +1 -1
- package/types/theme/theme.d.ts.map +1 -1
- package/types/theme/theme.native.d.ts.map +1 -1
- package/types/theme/useResolvedKnobs.d.ts +2 -2
- package/types/theme/useResolvedKnobs.d.ts.map +1 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { tokens } from "@tamagui/themes";
|
|
1
2
|
import { describe, expect, it, vi } from "vitest";
|
|
2
3
|
import {
|
|
3
4
|
defaultKnobs,
|
|
4
5
|
emptyStateKnobs,
|
|
6
|
+
FillStyle,
|
|
5
7
|
Space,
|
|
6
8
|
Size,
|
|
7
9
|
Density,
|
|
@@ -39,9 +41,13 @@ function knobsWith(overrides: Partial<Knobs>): Knobs {
|
|
|
39
41
|
|
|
40
42
|
const expectedKnobPropKeys: (keyof KnobProps)[] = [
|
|
41
43
|
"borderRadius",
|
|
44
|
+
"borderRadiusNested",
|
|
42
45
|
"borderRadiusOuter",
|
|
43
46
|
"containerRadius",
|
|
44
47
|
"elevation",
|
|
48
|
+
"overlayElevation",
|
|
49
|
+
"elevationChrome",
|
|
50
|
+
"scheme",
|
|
45
51
|
"surface",
|
|
46
52
|
"cardSurface",
|
|
47
53
|
"elevatedSurface",
|
|
@@ -222,6 +228,13 @@ describe("resolveKnobs", () => {
|
|
|
222
228
|
expect(resolveKnobs(knobsWith({ elevation: "small" })).knobProps.elevation).toBe("$1");
|
|
223
229
|
});
|
|
224
230
|
|
|
231
|
+
it("overlay elevation at small is $2, not the control $1 token", () => {
|
|
232
|
+
const { knobProps } = resolveKnobs(knobsWith({ elevation: "small" }));
|
|
233
|
+
expect(knobProps.overlayElevation).toBe("$2");
|
|
234
|
+
expect(knobProps.elevatedSurface.elevation).toBe("$2");
|
|
235
|
+
expect(knobProps.elevatedSurface.elevation).not.toBe(knobProps.elevation);
|
|
236
|
+
});
|
|
237
|
+
|
|
225
238
|
it('maps "medium" → "$2"', () => {
|
|
226
239
|
expect(resolveKnobs(knobsWith({ elevation: "medium" })).knobProps.elevation).toBe("$2");
|
|
227
240
|
});
|
|
@@ -229,6 +242,41 @@ describe("resolveKnobs", () => {
|
|
|
229
242
|
it('maps "large" → "$4"', () => {
|
|
230
243
|
expect(resolveKnobs(knobsWith({ elevation: "large" })).knobProps.elevation).toBe("$4");
|
|
231
244
|
});
|
|
245
|
+
|
|
246
|
+
it("light scheme keeps overlay fill at $background and paints a black shadow", () => {
|
|
247
|
+
const { knobProps } = resolveKnobs(knobsWith({ elevation: "large" }), undefined, "light");
|
|
248
|
+
expect(knobProps.scheme).toBe("light");
|
|
249
|
+
expect(knobProps.elevatedSurface.backgroundColor).toBe("$background");
|
|
250
|
+
expect(knobProps.cardSurface.backgroundColor).toBe("$background");
|
|
251
|
+
expect(knobProps.inputBackground).toBe("$background");
|
|
252
|
+
expect(knobProps.elevatedSurface.shadowColor).toBe("#000");
|
|
253
|
+
expect(knobProps.elevatedSurface.shadowOpacity).toBe(0.15);
|
|
254
|
+
expect(knobProps.elevationChrome.elevation).toBe(8);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("dark scheme tints overlay/card/input up the existing ramp (LC-85)", () => {
|
|
258
|
+
const none = resolveKnobs(knobsWith({ elevation: "none" }), undefined, "dark");
|
|
259
|
+
const small = resolveKnobs(knobsWith({ elevation: "small" }), undefined, "dark");
|
|
260
|
+
const large = resolveKnobs(knobsWith({ elevation: "large" }), undefined, "dark");
|
|
261
|
+
expect(none.knobProps.elevatedSurface.backgroundColor).toBe("$background");
|
|
262
|
+
expect(small.knobProps.elevatedSurface.backgroundColor).toBe("$color2");
|
|
263
|
+
expect(large.knobProps.elevatedSurface.backgroundColor).toBe("$color4");
|
|
264
|
+
expect(large.knobProps.cardSurface.backgroundColor).toBe("$color4");
|
|
265
|
+
expect(large.knobProps.inputBackground).toBe("$color5");
|
|
266
|
+
expect(large.knobProps.elevatedSurface.shadowColor).toBe("$color12");
|
|
267
|
+
expect(large.knobProps.elevatedSurface.shadowOpacity).toBe(0.12);
|
|
268
|
+
expect(none.knobProps.elevationChrome).toEqual({});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("dark outlined inputs stay transparent — tint is for filled surfaces", () => {
|
|
272
|
+
const { knobProps } = resolveKnobs(
|
|
273
|
+
knobsWith({ elevation: "large", fillStyle: "outlined" }),
|
|
274
|
+
undefined,
|
|
275
|
+
"dark",
|
|
276
|
+
);
|
|
277
|
+
expect(knobProps.inputBackground).toBe("transparent");
|
|
278
|
+
expect(knobProps.elevatedSurface.backgroundColor).toBe("$color4");
|
|
279
|
+
});
|
|
232
280
|
});
|
|
233
281
|
|
|
234
282
|
// ── elevation on NATIVE (SB-N-04) ──────────────────────────
|
|
@@ -251,10 +299,15 @@ describe("resolveKnobs", () => {
|
|
|
251
299
|
knobsWith({ elevation: "small", hover: { elevation: "medium" } }),
|
|
252
300
|
);
|
|
253
301
|
expect(small.knobProps.elevation).toBe(20);
|
|
254
|
-
expect(small.knobProps.
|
|
302
|
+
expect(small.knobProps.overlayElevation).toBe(28);
|
|
303
|
+
expect(small.knobProps.elevatedSurface.elevation).toBe(28);
|
|
304
|
+
expect(small.knobProps.elevatedSurface.elevation).not.toBe(small.knobProps.elevation);
|
|
255
305
|
expect(small.elevation.hoverKnobProps?.elevation).toBe(28);
|
|
256
306
|
expect(resolveNative(knobsWith({ elevation: "medium" })).knobProps.elevation).toBe(28);
|
|
257
307
|
expect(resolveNative(knobsWith({ elevation: "large" })).knobProps.elevation).toBe(44);
|
|
308
|
+
expect(
|
|
309
|
+
resolveNative(knobsWith({ elevation: "large" })).knobProps.elevationChrome.elevation,
|
|
310
|
+
).toBe(8);
|
|
258
311
|
} finally {
|
|
259
312
|
vi.doUnmock("@multiplatform.one/platform");
|
|
260
313
|
vi.resetModules();
|
|
@@ -424,7 +477,7 @@ describe("resolveKnobs", () => {
|
|
|
424
477
|
it("cardSurface = bg + radius + panelPadding, no border/shadow", () => {
|
|
425
478
|
const { knobProps } = resolveKnobs(defaultKnobs);
|
|
426
479
|
expect(knobProps.cardSurface).toEqual({
|
|
427
|
-
backgroundColor: "$
|
|
480
|
+
backgroundColor: "$background",
|
|
428
481
|
borderRadius: "$4",
|
|
429
482
|
padding: "$4",
|
|
430
483
|
});
|
|
@@ -443,13 +496,27 @@ describe("resolveKnobs", () => {
|
|
|
443
496
|
expect(knobProps.cardSurface.borderRadius).toBe("$6");
|
|
444
497
|
});
|
|
445
498
|
|
|
446
|
-
it("elevatedSurface = surface + radius + elevation", () => {
|
|
499
|
+
it("elevatedSurface = surface + radius + overlay elevation + paint", () => {
|
|
447
500
|
const { knobProps } = resolveKnobs(defaultKnobs);
|
|
448
|
-
expect(knobProps.elevatedSurface).
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
501
|
+
expect(knobProps.elevatedSurface.elevation).toBe(knobProps.overlayElevation);
|
|
502
|
+
expect(knobProps.elevatedSurface.elevation).not.toBe(knobProps.elevation);
|
|
503
|
+
expect(knobProps.elevatedSurface.backgroundColor).toBe(knobProps.surface.backgroundColor);
|
|
504
|
+
expect(knobProps.elevatedSurface.borderColor).toBe(knobProps.surface.borderColor);
|
|
505
|
+
expect(knobProps.elevatedSurface.borderWidth).toBe(knobProps.surface.borderWidth);
|
|
506
|
+
expect(knobProps.elevatedSurface.borderRadius).toBe(knobProps.borderRadius.borderRadius);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("overlay elevation token differs from the control elevation token", () => {
|
|
510
|
+
for (const elevation of ["small", "medium", "large"] as const) {
|
|
511
|
+
const { knobProps } = resolveKnobs(knobsWith({ elevation }));
|
|
512
|
+
expect(knobProps.overlayElevation, elevation).toBeDefined();
|
|
513
|
+
expect(knobProps.overlayElevation, elevation).not.toBe(knobProps.elevation);
|
|
514
|
+
expect(knobProps.elevatedSurface.elevation, elevation).toBe(knobProps.overlayElevation);
|
|
515
|
+
}
|
|
516
|
+
const none = resolveKnobs(knobsWith({ elevation: "none" }));
|
|
517
|
+
expect(none.knobProps.elevation).toBeUndefined();
|
|
518
|
+
expect(none.knobProps.overlayElevation).toBeUndefined();
|
|
519
|
+
expect(none.knobProps.elevatedSurface.elevation).toBeUndefined();
|
|
453
520
|
});
|
|
454
521
|
|
|
455
522
|
it("featureSurface uses big radius, $6 padding, and an ultra-soft shadow", () => {
|
|
@@ -514,8 +581,7 @@ describe("resolveKnobs", () => {
|
|
|
514
581
|
).toBe("$6");
|
|
515
582
|
});
|
|
516
583
|
|
|
517
|
-
it("mirrored px values agree with the live @tamagui/themes tokens (drift guard)",
|
|
518
|
-
const { tokens } = await import("@tamagui/themes");
|
|
584
|
+
it("mirrored px values agree with the live @tamagui/themes tokens (drift guard)", () => {
|
|
519
585
|
const radiusTokenByKnob = {
|
|
520
586
|
none: "$0",
|
|
521
587
|
small: "$2",
|
|
@@ -543,7 +609,7 @@ describe("resolveKnobs", () => {
|
|
|
543
609
|
describe("cornerSmoothing knob", () => {
|
|
544
610
|
const radiusFragments = [
|
|
545
611
|
"borderRadius",
|
|
546
|
-
"
|
|
612
|
+
"borderRadiusNested",
|
|
547
613
|
"containerRadius",
|
|
548
614
|
"cardSurface",
|
|
549
615
|
"elevatedSurface",
|
|
@@ -585,7 +651,7 @@ describe("resolveKnobs", () => {
|
|
|
585
651
|
);
|
|
586
652
|
// $0 corners have no arc — square world stays square (Axiom 3 NULL).
|
|
587
653
|
expect(knobProps.borderRadius).not.toHaveProperty("className");
|
|
588
|
-
expect(knobProps.
|
|
654
|
+
expect(knobProps.borderRadiusNested).not.toHaveProperty("className");
|
|
589
655
|
expect(knobProps.containerRadius).not.toHaveProperty("className");
|
|
590
656
|
expect(knobProps.cardSurface).not.toHaveProperty("className");
|
|
591
657
|
expect(knobProps.elevatedSurface).not.toHaveProperty("className");
|
|
@@ -604,7 +670,7 @@ describe("resolveKnobs", () => {
|
|
|
604
670
|
expect(knobProps.elevatedSurface).not.toHaveProperty("className");
|
|
605
671
|
// Capped/outer fragments stay plain arcs (cap = padding px, outer ≤ $5)
|
|
606
672
|
// and keep smoothing.
|
|
607
|
-
expect(knobProps.
|
|
673
|
+
expect(knobProps.borderRadiusNested.className).toBe(cornerSmoothClassName);
|
|
608
674
|
expect(knobProps.containerRadius.className).toBe(cornerSmoothClassName);
|
|
609
675
|
expect(knobProps.cardSurface.className).toBe(cornerSmoothClassName);
|
|
610
676
|
expect(knobProps.featureSurface.className).toBe(cornerSmoothClassName);
|
|
@@ -681,6 +747,54 @@ describe("resolveKnobs", () => {
|
|
|
681
747
|
const { knobProps } = resolveKnobs(knobsWith({ fillStyle: "outlined" }));
|
|
682
748
|
expect(knobProps.outlined).toBe(true);
|
|
683
749
|
});
|
|
750
|
+
|
|
751
|
+
it("union is filled | outlined only (R9 G9)", () => {
|
|
752
|
+
expect(Object.values(FillStyle)).toEqual(["filled", "outlined"]);
|
|
753
|
+
expect(Object.values(FillStyle)).toHaveLength(2);
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
describe("separator weight is not a knob (R9 G10)", () => {
|
|
758
|
+
it("Knobs has no separatorWeight channel", () => {
|
|
759
|
+
expect("separatorWeight" in defaultKnobs).toBe(false);
|
|
760
|
+
expect(Object.keys(defaultKnobs)).not.toContain("separatorWeight");
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
describe("fillStyle × borderWidth (LC-86 OUTLINED-HAIRLINE)", () => {
|
|
765
|
+
it("filled + none is chromeless (width 0)", () => {
|
|
766
|
+
const { knobProps } = resolveKnobs(knobsWith({ fillStyle: "filled", borderWidth: "none" }));
|
|
767
|
+
expect(knobProps.borderRadius.borderWidth).toBe(0);
|
|
768
|
+
expect(knobProps.surface.borderWidth).toBe(0);
|
|
769
|
+
expect(knobProps.elevatedSurface.borderWidth).toBe(0);
|
|
770
|
+
expect(knobProps.inputSurface.borderWidth).toBe(0);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it("outlined + none still draws the hairline (small stop, 0.5)", () => {
|
|
774
|
+
const { knobProps } = resolveKnobs(knobsWith({ fillStyle: "outlined", borderWidth: "none" }));
|
|
775
|
+
expect(knobProps.borderRadius.borderWidth).toBe(0.5);
|
|
776
|
+
expect(knobProps.surface.borderWidth).toBe(0.5);
|
|
777
|
+
expect(knobProps.elevatedSurface.borderWidth).toBe(0.5);
|
|
778
|
+
expect(knobProps.inputSurface.borderWidth).toBe(0.5);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it("outlined + small stays at the hairline, not a hidden 1px floor", () => {
|
|
782
|
+
const { knobProps } = resolveKnobs(
|
|
783
|
+
knobsWith({ fillStyle: "outlined", borderWidth: "small" }),
|
|
784
|
+
);
|
|
785
|
+
expect(knobProps.borderRadius.borderWidth).toBe(0.5);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it("outlined + medium/large keep the requested width", () => {
|
|
789
|
+
expect(
|
|
790
|
+
resolveKnobs(knobsWith({ fillStyle: "outlined", borderWidth: "medium" })).knobProps
|
|
791
|
+
.borderRadius.borderWidth,
|
|
792
|
+
).toBe(1);
|
|
793
|
+
expect(
|
|
794
|
+
resolveKnobs(knobsWith({ fillStyle: "outlined", borderWidth: "large" })).knobProps
|
|
795
|
+
.borderRadius.borderWidth,
|
|
796
|
+
).toBe(2);
|
|
797
|
+
});
|
|
684
798
|
});
|
|
685
799
|
|
|
686
800
|
// ── pointy ────────────────────────────────────────────────
|
|
@@ -1005,6 +1119,34 @@ describe("applyDensity", () => {
|
|
|
1005
1119
|
expect(resolveKnobs(knobsWith({ density: "compact" })).knobProps.density).toBe("compact");
|
|
1006
1120
|
});
|
|
1007
1121
|
|
|
1122
|
+
it("compact and comfortable differ in padX while height stays 44 (LC-76)", () => {
|
|
1123
|
+
const comfortable = resolveKnobs(knobsWith({ density: "comfortable" }));
|
|
1124
|
+
const compact = resolveKnobs(knobsWith({ density: "compact" }));
|
|
1125
|
+
expect(comfortable.knobProps.control.height).toBe(44);
|
|
1126
|
+
expect(compact.knobProps.control.height).toBe(44);
|
|
1127
|
+
expect(comfortable.knobProps.sizeToken).toBe("$4");
|
|
1128
|
+
expect(compact.knobProps.sizeToken).toBe("$4");
|
|
1129
|
+
expect(compact.knobProps.control.paddingHorizontal).not.toBe(
|
|
1130
|
+
comfortable.knobProps.control.paddingHorizontal,
|
|
1131
|
+
);
|
|
1132
|
+
expect(comfortable.knobProps.control.paddingHorizontal).toBe(18);
|
|
1133
|
+
expect(compact.knobProps.control.paddingHorizontal).toBe(16);
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
it("names which gap density moves: the LAYOUT gap, never the recipe gap (LC-79)", () => {
|
|
1137
|
+
// `control.gap` tracks the RADIUS knob, so it is invariant under density
|
|
1138
|
+
// by construction. The gap a reader sees tighten is `knobProps.gap`, the
|
|
1139
|
+
// space-ramp one, and it only moves once `applyDensity` has stepped
|
|
1140
|
+
// `space` — `resolveKnobs` alone is a pure token mapper.
|
|
1141
|
+
const comfortable = resolveKnobs(applyDensity(knobsWith({ space: "medium" })));
|
|
1142
|
+
const compact = resolveKnobs(applyDensity(knobsWith({ space: "medium", density: "compact" })));
|
|
1143
|
+
expect(compact.knobProps.control.gap).toBe(comfortable.knobProps.control.gap);
|
|
1144
|
+
expect(compact.knobProps.gap).not.toEqual(comfortable.knobProps.gap);
|
|
1145
|
+
expect(compact.knobProps.panelPadding).not.toEqual(comfortable.knobProps.panelPadding);
|
|
1146
|
+
expect(compact.knobProps.control.height).toBe(44);
|
|
1147
|
+
expect(comfortable.knobProps.control.height).toBe(44);
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1008
1150
|
it("compact density produces the same sizeToken and a smaller gap than comfortable", () => {
|
|
1009
1151
|
const comfortable = resolveKnobs(applyDensity(knobsWith({ size: "medium", space: "medium" })));
|
|
1010
1152
|
const compact = resolveKnobs(
|
|
@@ -9,11 +9,13 @@ import {
|
|
|
9
9
|
STATE_LAYER_PRESS,
|
|
10
10
|
withDefaultStateLayer,
|
|
11
11
|
} from "./focusState";
|
|
12
|
-
import type { CornerSmoothing, Knobs, PageTitleScale, StateKnobs } from "./knobs";
|
|
12
|
+
import type { CornerSmoothing, Elevation, Knobs, PageTitleScale, StateKnobs } from "./knobs";
|
|
13
13
|
import { pressTargetHitSlop } from "./layoutTokens";
|
|
14
14
|
import type {
|
|
15
15
|
ControlStateProps,
|
|
16
16
|
DisabledRecipe,
|
|
17
|
+
ElevationChromeProps,
|
|
18
|
+
ElevationPaintProps,
|
|
17
19
|
ElevationStateProps,
|
|
18
20
|
KnobProps,
|
|
19
21
|
KnobPropsOverride,
|
|
@@ -64,7 +66,7 @@ export function applyDensity(knobs: Knobs, compactOverride?: boolean): Knobs {
|
|
|
64
66
|
|
|
65
67
|
// ── Knob-to-token mapping tables ──────────────────────────────
|
|
66
68
|
|
|
67
|
-
const borderRadiusMap = {
|
|
69
|
+
export const borderRadiusMap = {
|
|
68
70
|
none: "$0",
|
|
69
71
|
small: "$2",
|
|
70
72
|
medium: "$4",
|
|
@@ -72,7 +74,12 @@ const borderRadiusMap = {
|
|
|
72
74
|
full: "$12",
|
|
73
75
|
} as const;
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
// Radius for chrome nested INSIDE a radius-bearing frame. Smaller than
|
|
78
|
+
// `borderRadiusMap` at every stop that has room to be — a child corner equal
|
|
79
|
+
// to its parent's reads as a second frame rather than as part of one.
|
|
80
|
+
// MPO-19 X12: was `outerRadiusMap`, feeding a fragment called
|
|
81
|
+
// `borderRadiusOuter` that every consumer used for inner chrome.
|
|
82
|
+
const nestedRadiusMap = {
|
|
76
83
|
none: "$0",
|
|
77
84
|
small: "$2",
|
|
78
85
|
medium: "$3",
|
|
@@ -90,8 +97,9 @@ const borderWidthMap = {
|
|
|
90
97
|
// ── Corner smoothing (Axiom 15 OPTICS) ────────────────────────
|
|
91
98
|
// `smooth` upgrades circular-arc corners to continuous curvature —
|
|
92
99
|
// `corner-shape: squircle` (≡ superellipse(2), ≈ Apple / Figma 0.6) — as a
|
|
93
|
-
// web progressive enhancement: Chromium
|
|
94
|
-
//
|
|
100
|
+
// web progressive enhancement: Chromium paints it (measured from 143; WebKit
|
|
101
|
+
// 26 and Firefox 148 drop the unknown declaration and keep the plain
|
|
102
|
+
// border-radius arc — docs/design/boards/W1-corner-smoothing/).
|
|
95
103
|
// Delivery is a class + mounted stylesheet (cornerSmoothing.ts) because
|
|
96
104
|
// Tamagui strips unknown keys from the `style` prop (skipProps) but merges
|
|
97
105
|
// `className`. Native resolves to `round` for now (SquircleView adoption is
|
|
@@ -243,10 +251,121 @@ const fontWeightMap = {
|
|
|
243
251
|
// be cast to java.lang.Double" (SB-N-04). The numbers are the exact
|
|
244
252
|
// tokens.size values Tamagui resolves those tokens to ($1=20, $2=28, $4=44),
|
|
245
253
|
// so Tamagui consumers render identical shadows either way.
|
|
246
|
-
|
|
254
|
+
//
|
|
255
|
+
// Control elevation ($1 at the default small stop) is the in-page Button
|
|
256
|
+
// token. Overlay elevation is a DISTINCT token one step above that stop so
|
|
257
|
+
// a dialog over a scrim and a Button on the page do not read as the same
|
|
258
|
+
// distance off the surface (DG-OVL-01 / E-OVERLAY). elevatedSurface binds
|
|
259
|
+
// the overlay token — not the control one.
|
|
260
|
+
export const elevationMap: Record<string, string | number | undefined> = isWeb
|
|
247
261
|
? { none: undefined, small: "$1", medium: "$2", large: "$4" }
|
|
248
262
|
: { none: undefined, small: 20, medium: 28, large: 44 };
|
|
249
263
|
|
|
264
|
+
export const overlayElevationMap: Record<string, string | number | undefined> = isWeb
|
|
265
|
+
? { none: undefined, small: "$2", medium: "$3", large: "$5" }
|
|
266
|
+
: { none: undefined, small: 28, medium: 36, large: 52 };
|
|
267
|
+
|
|
268
|
+
/** Scheme the resolver was invoked with. Defaults light so unit tests stay stable. */
|
|
269
|
+
export type ColorScheme = "light" | "dark";
|
|
270
|
+
|
|
271
|
+
// Existing 12-step ramp (colorRules groups). Do not invent overlay tokens:
|
|
272
|
+
// dark elevation is a lighter $color step. Overlay/card rest on $color1;
|
|
273
|
+
// filled inputs rest on $color2 and step from there.
|
|
274
|
+
const elevationTintOverlay: Record<Elevation, string> = {
|
|
275
|
+
none: "$color1",
|
|
276
|
+
small: "$color2",
|
|
277
|
+
medium: "$color3",
|
|
278
|
+
large: "$color4",
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const elevationTintInput: Record<Elevation, string> = {
|
|
282
|
+
none: "$color2",
|
|
283
|
+
small: "$color3",
|
|
284
|
+
medium: "$color4",
|
|
285
|
+
large: "$color5",
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Axiom 14 light source: y-offset:blur = 0.5. Metrics match the maps
|
|
289
|
+
// InputParts / getElevationWrapperProps already shipped.
|
|
290
|
+
const elevationShadowMetrics: Record<
|
|
291
|
+
"$1" | "$2" | "$4",
|
|
292
|
+
{ offsetY: number; radius: number; android: number }
|
|
293
|
+
> = {
|
|
294
|
+
$1: { offsetY: 1, radius: 2, android: 2 },
|
|
295
|
+
$2: { offsetY: 3, radius: 6, android: 4 },
|
|
296
|
+
$4: { offsetY: 10, radius: 20, android: 8 },
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const elevationKeyAlias: Record<string, "$1" | "$2" | "$4"> = {
|
|
300
|
+
$1: "$1",
|
|
301
|
+
$2: "$2",
|
|
302
|
+
$3: "$2",
|
|
303
|
+
$4: "$4",
|
|
304
|
+
$5: "$4",
|
|
305
|
+
"20": "$1",
|
|
306
|
+
"28": "$2",
|
|
307
|
+
"36": "$2",
|
|
308
|
+
"44": "$4",
|
|
309
|
+
"52": "$4",
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const webShadowLight: Record<"$1" | "$2" | "$4", string> = {
|
|
313
|
+
$1: "0 1px 2px rgba(0,0,0,0.12), 0 0 1px rgba(0,0,0,0.08)",
|
|
314
|
+
$2: "0 3px 6px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.10)",
|
|
315
|
+
$4: "0 10px 20px rgba(0,0,0,0.15), 0 3px 6px rgba(0,0,0,0.10)",
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const webShadowDark: Record<"$1" | "$2" | "$4", string> = {
|
|
319
|
+
$1: "0 1px 2px rgba(255,255,255,0.08), 0 0 1px rgba(255,255,255,0.06)",
|
|
320
|
+
$2: "0 3px 6px rgba(255,255,255,0.10), 0 2px 4px rgba(255,255,255,0.06)",
|
|
321
|
+
$4: "0 10px 20px rgba(255,255,255,0.12), 0 3px 6px rgba(255,255,255,0.08)",
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
function elevationTokenKey(token: string | number | undefined): "$1" | "$2" | "$4" | undefined {
|
|
325
|
+
if (token == null) return undefined;
|
|
326
|
+
return elevationKeyAlias[String(token)];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function elevationPaint(
|
|
330
|
+
token: string | number | undefined,
|
|
331
|
+
scheme: ColorScheme,
|
|
332
|
+
): ElevationPaintProps {
|
|
333
|
+
const key = elevationTokenKey(token);
|
|
334
|
+
if (!key) return {};
|
|
335
|
+
const metrics = elevationShadowMetrics[key];
|
|
336
|
+
const dark = scheme === "dark";
|
|
337
|
+
const paint: ElevationPaintProps = {
|
|
338
|
+
shadowColor: dark ? "$color12" : "#000",
|
|
339
|
+
shadowOffset: { width: 0, height: metrics.offsetY },
|
|
340
|
+
shadowOpacity: dark
|
|
341
|
+
? key === "$4"
|
|
342
|
+
? 0.12
|
|
343
|
+
: key === "$2"
|
|
344
|
+
? 0.1
|
|
345
|
+
: 0.08
|
|
346
|
+
: key === "$1"
|
|
347
|
+
? 0.12
|
|
348
|
+
: 0.15,
|
|
349
|
+
shadowRadius: metrics.radius,
|
|
350
|
+
};
|
|
351
|
+
if (isWeb) {
|
|
352
|
+
paint.style = { boxShadow: dark ? webShadowDark[key] : webShadowLight[key] };
|
|
353
|
+
}
|
|
354
|
+
return paint;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function elevationChromeFor(
|
|
358
|
+
token: string | number | undefined,
|
|
359
|
+
scheme: ColorScheme,
|
|
360
|
+
): ElevationChromeProps {
|
|
361
|
+
const key = elevationTokenKey(token);
|
|
362
|
+
if (!key) return {};
|
|
363
|
+
return {
|
|
364
|
+
...elevationPaint(token, scheme),
|
|
365
|
+
elevation: elevationShadowMetrics[key].android,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
250
369
|
// Legibility floor: body/label text must never land below ~4.5:1 against its
|
|
251
370
|
// surface. $color11 is the lowest ramp step that clears AA on color1/2
|
|
252
371
|
// surfaces, so "low" clamps there ($color9 measured 1.29-3.43:1 — SB-R-01).
|
|
@@ -317,7 +436,10 @@ function resolveDisabled(disabledStyle: Knobs["disabledStyle"]): DisabledRecipe
|
|
|
317
436
|
|
|
318
437
|
// ── State style resolver ──────────────────────────────────────
|
|
319
438
|
|
|
320
|
-
function resolveStateKnobs(
|
|
439
|
+
function resolveStateKnobs(
|
|
440
|
+
state: StateKnobs,
|
|
441
|
+
scheme: ColorScheme,
|
|
442
|
+
): {
|
|
321
443
|
control: ControlStateProps;
|
|
322
444
|
text: TextStateProps;
|
|
323
445
|
elevation: ElevationStateProps;
|
|
@@ -339,7 +461,12 @@ function resolveStateKnobs(state: StateKnobs): {
|
|
|
339
461
|
control.padding = gapMap[state.space];
|
|
340
462
|
}
|
|
341
463
|
if (state.elevation !== undefined) {
|
|
342
|
-
|
|
464
|
+
const token = elevationMap[state.elevation];
|
|
465
|
+
elevation.elevation = token;
|
|
466
|
+
Object.assign(elevation, elevationPaint(token, scheme));
|
|
467
|
+
if (scheme === "dark" && state.elevation !== "none") {
|
|
468
|
+
elevation.backgroundColor = elevationTintOverlay[state.elevation];
|
|
469
|
+
}
|
|
343
470
|
}
|
|
344
471
|
if (state.textAccent !== undefined) {
|
|
345
472
|
text.color = textAccentColorMap[state.textAccent];
|
|
@@ -358,15 +485,19 @@ function resolveStateKnobs(state: StateKnobs): {
|
|
|
358
485
|
|
|
359
486
|
// ── Resolver ──────────────────────────────────────────────────
|
|
360
487
|
|
|
361
|
-
export function resolveKnobs(
|
|
488
|
+
export function resolveKnobs(
|
|
489
|
+
knobs: Knobs,
|
|
490
|
+
override?: KnobPropsOverride,
|
|
491
|
+
scheme: ColorScheme = "light",
|
|
492
|
+
): ResolvedKnobs {
|
|
362
493
|
const radiusToken = borderRadiusMap[knobs.borderRadius];
|
|
363
|
-
const
|
|
364
|
-
const borderWidthValue = borderWidthMap[knobs.borderWidth];
|
|
494
|
+
const nestedRadiusToken = nestedRadiusMap[knobs.borderRadius];
|
|
365
495
|
const sizeToken = sizeTokenMap[knobs.size];
|
|
366
496
|
const nestedPx = nestedControlPxMap[knobs.size];
|
|
367
497
|
const nestedHitSlop = pressTargetHitSlop(nestedPx);
|
|
368
498
|
const sizeRecipe = sizeRecipeForToken(sizeToken, {
|
|
369
499
|
touch: isTouchable || isWebTouchable,
|
|
500
|
+
family: knobs.density === "compact" ? "controlCompact" : "control",
|
|
370
501
|
});
|
|
371
502
|
const paddingToken = panelPaddingMap[knobs.space];
|
|
372
503
|
const gapToken = gapMap[knobs.space];
|
|
@@ -393,18 +524,38 @@ export function resolveKnobs(knobs: Knobs, override?: KnobPropsOverride): Resolv
|
|
|
393
524
|
const containerSmoothing = knobs.borderRadius === "none" ? undefined : smoothing;
|
|
394
525
|
|
|
395
526
|
const outlined = knobs.fillStyle === "outlined";
|
|
527
|
+
// LC-86 OUTLINED-HAIRLINE: outlined implies a minimum border. none on an
|
|
528
|
+
// outlined control still draws the enumerated `small` stop (0.5). Chromeless
|
|
529
|
+
// is filled + none. Components must not add a second local floor.
|
|
530
|
+
const borderWidthValue =
|
|
531
|
+
outlined && knobs.borderWidth === "none"
|
|
532
|
+
? borderWidthMap.small
|
|
533
|
+
: borderWidthMap[knobs.borderWidth];
|
|
396
534
|
const elevationToken = elevationMap[knobs.elevation];
|
|
535
|
+
const overlayElevationToken = overlayElevationMap[knobs.elevation];
|
|
536
|
+
const overlayPaintProps = elevationPaint(overlayElevationToken, scheme);
|
|
537
|
+
const elevationChrome = elevationChromeFor(elevationToken, scheme);
|
|
538
|
+
const dark = scheme === "dark";
|
|
539
|
+
// MPO-19 X5: `$background` (not `$color1`) so surface1/2/3 sub-themes move
|
|
540
|
+
// the fill. Dark + elevation still steps the filled rest fill up the ramp
|
|
541
|
+
// (LC-85).
|
|
542
|
+
const overlayFill =
|
|
543
|
+
dark && knobs.elevation !== "none" ? elevationTintOverlay[knobs.elevation] : "$background";
|
|
397
544
|
const textAccentColor = textAccentColorMap[knobs.textAccent] ?? "$color";
|
|
398
|
-
// One input surface (DF-19 / Axiom 5 ONE SOURCE):
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
const inputBackground = outlined
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
545
|
+
// One input surface (DF-19 / Axiom 5 ONE SOURCE): Timeline's composer,
|
|
546
|
+
// RichEditor and form fields agree on what an input surface is.
|
|
547
|
+
// MPO-19 X6: `$background` not `$color2` so a surface sub-theme can move it.
|
|
548
|
+
// Dark + elevation steps the filled rest fill up the existing ramp (LC-85).
|
|
549
|
+
const inputBackground = outlined
|
|
550
|
+
? "transparent"
|
|
551
|
+
: dark
|
|
552
|
+
? elevationTintInput[knobs.elevation]
|
|
553
|
+
: "$background";
|
|
554
|
+
|
|
555
|
+
const hover = resolveStateKnobs(knobs.hover, scheme);
|
|
556
|
+
const press = resolveStateKnobs(knobs.press, scheme);
|
|
557
|
+
const focus = resolveStateKnobs(knobs.focus, scheme);
|
|
558
|
+
const focusVisible = resolveStateKnobs(knobs.focusVisible, scheme);
|
|
408
559
|
|
|
409
560
|
const nonEmpty = <T extends object>(obj: T): T | undefined =>
|
|
410
561
|
Object.keys(obj).length > 0 ? obj : undefined;
|
|
@@ -430,6 +581,8 @@ export function resolveKnobs(knobs: Knobs, override?: KnobPropsOverride): Resolv
|
|
|
430
581
|
focusVisibleKnobProps: nonEmpty(focusVisible.elevation),
|
|
431
582
|
};
|
|
432
583
|
|
|
584
|
+
const nestedRadius = { borderRadius: nestedRadiusToken, ...containerSmoothing };
|
|
585
|
+
|
|
433
586
|
const knobProps: KnobProps = {
|
|
434
587
|
borderRadius: {
|
|
435
588
|
borderRadius: radiusToken,
|
|
@@ -437,33 +590,46 @@ export function resolveKnobs(knobs: Knobs, override?: KnobPropsOverride): Resolv
|
|
|
437
590
|
borderWidth: borderWidthValue,
|
|
438
591
|
...scaleSmoothing,
|
|
439
592
|
},
|
|
440
|
-
|
|
593
|
+
borderRadiusNested: nestedRadius,
|
|
594
|
+
// MPO-19 X12: the old name for the same fragment, kept for one release so
|
|
595
|
+
// an out-of-tree consumer does not break on the rename. Same object, so
|
|
596
|
+
// the two cannot drift while both exist.
|
|
597
|
+
borderRadiusOuter: nestedRadius,
|
|
598
|
+
controlBorderFilled: { borderColor: "transparent", borderWidth: borderWidthValue },
|
|
599
|
+
controlBorderOutlined: {
|
|
600
|
+
borderColor: "$borderColor",
|
|
601
|
+
borderWidth: Math.max(borderWidthValue, 1),
|
|
602
|
+
},
|
|
441
603
|
// Axiom 1 child-clip cap: padded containers (Card/Panel/DialogContent)
|
|
442
604
|
// spread this after their radius-bearing fragment. Non-container parts
|
|
443
605
|
// keep the uncapped `borderRadius` fragment.
|
|
444
606
|
containerRadius: { borderRadius: containerRadiusValue, ...containerSmoothing },
|
|
445
607
|
elevation: elevationToken,
|
|
608
|
+
overlayElevation: overlayElevationToken,
|
|
609
|
+
elevationChrome,
|
|
610
|
+
scheme,
|
|
446
611
|
surface: {
|
|
447
|
-
backgroundColor: "$
|
|
612
|
+
backgroundColor: "$background",
|
|
448
613
|
borderColor: "$borderColor",
|
|
449
614
|
borderWidth: borderWidthValue,
|
|
450
615
|
},
|
|
451
616
|
cardSurface: {
|
|
452
|
-
backgroundColor:
|
|
617
|
+
backgroundColor: overlayFill,
|
|
453
618
|
borderRadius: containerRadiusValue,
|
|
454
619
|
padding: paddingToken,
|
|
455
620
|
...containerSmoothing,
|
|
456
621
|
},
|
|
457
622
|
elevatedSurface: {
|
|
458
|
-
backgroundColor:
|
|
623
|
+
backgroundColor: overlayFill,
|
|
459
624
|
borderColor: "$borderColor",
|
|
460
625
|
borderWidth: borderWidthValue,
|
|
461
626
|
borderRadius: radiusToken,
|
|
462
|
-
elevation:
|
|
627
|
+
elevation: overlayElevationToken,
|
|
628
|
+
...overlayPaintProps,
|
|
463
629
|
...scaleSmoothing,
|
|
464
630
|
},
|
|
465
631
|
featureSurface: {
|
|
466
|
-
backgroundColor: "$
|
|
632
|
+
backgroundColor: "$background",
|
|
467
633
|
borderRadius: "$10",
|
|
468
634
|
padding: "$6",
|
|
469
635
|
// Bento marketing/auth card shadow: ~0px 9px 12px rgba(0,0,0,0.02) —
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MPO-46 / MPO-19 X1/X2 — the size ladder is the measured Tamagui v5 table
|
|
3
|
+
* (R6: real TABLE, no escape). padX / label / icon / radius at $2/$3/$4/$6
|
|
4
|
+
* come from tamagui-rendered-reference.md §6.1; Input padX is §6.2
|
|
5
|
+
* (one space half-step below the button). gap == radius at every size
|
|
6
|
+
* and every borderRadius knob stop.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import {
|
|
11
|
+
SIZE_RECIPE_RADII,
|
|
12
|
+
SIZE_RECIPE_RADIUS_STOPS,
|
|
13
|
+
TAMAGUI_CONTROL_SIZE_TABLE,
|
|
14
|
+
TAMAGUI_INPUT_PADX,
|
|
15
|
+
applyRadiusStop,
|
|
16
|
+
generateSizeRecipes,
|
|
17
|
+
recipeFamilies,
|
|
18
|
+
sizeRecipeForToken,
|
|
19
|
+
sizeRecipes,
|
|
20
|
+
type SizeRecipeRadiusStop,
|
|
21
|
+
} from "./sizeRecipes";
|
|
22
|
+
|
|
23
|
+
const MEASURED = {
|
|
24
|
+
$2: { paddingHorizontal: 7, fontSize: 12, iconSize: 12, radius: 5 },
|
|
25
|
+
$3: { paddingHorizontal: 13, fontSize: 13, iconSize: 14, radius: 7 },
|
|
26
|
+
$4: { paddingHorizontal: 18, fontSize: 14, iconSize: 16, radius: 9 },
|
|
27
|
+
$6: { paddingHorizontal: 32, fontSize: 18, iconSize: 20, radius: 16 },
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
describe("MPO-46 size ladder — measured table, not a ratio (R6)", () => {
|
|
31
|
+
it("padX, label, icon and radius at $2/$3/$4/$6 match the Tamagui v5 reference", () => {
|
|
32
|
+
for (const token of ["$2", "$3", "$4", "$6"] as const) {
|
|
33
|
+
const recipe = sizeRecipeForToken(token);
|
|
34
|
+
const row = MEASURED[token];
|
|
35
|
+
expect(recipe.paddingHorizontal, `${token} padX`).toBe(row.paddingHorizontal);
|
|
36
|
+
expect(recipe.fontSize, `${token} label`).toBe(row.fontSize);
|
|
37
|
+
expect(recipe.iconSize, `${token} icon`).toBe(row.iconSize);
|
|
38
|
+
expect(SIZE_RECIPE_RADII[token], `${token} radius`).toBe(row.radius);
|
|
39
|
+
expect(TAMAGUI_CONTROL_SIZE_TABLE[token]).toEqual(row);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("does not ship the linear-ratio $2 defects (padX 11, label 9, flat radius 9)", () => {
|
|
44
|
+
const two = sizeRecipeForToken("$2");
|
|
45
|
+
expect(two.paddingHorizontal).not.toBe(11);
|
|
46
|
+
expect(two.fontSize).not.toBe(9);
|
|
47
|
+
expect(SIZE_RECIPE_RADII.$2).not.toBe(9);
|
|
48
|
+
expect(SIZE_RECIPE_RADII.$2).not.toBe(SIZE_RECIPE_RADII.$6);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("Button / Select trigger / ListItem $4 is 44 / padX 18 / radius 9; Input padX is 16", () => {
|
|
52
|
+
const button = sizeRecipes.$4;
|
|
53
|
+
expect(button.height).toBe(44);
|
|
54
|
+
expect(button.paddingHorizontal).toBe(18);
|
|
55
|
+
expect(SIZE_RECIPE_RADII.$4).toBe(9);
|
|
56
|
+
expect(TAMAGUI_INPUT_PADX.$4).toBe(16);
|
|
57
|
+
expect(recipeFamilies.controlCompact.recipes.$4.paddingHorizontal).toBe(16);
|
|
58
|
+
expect(recipeFamilies.controlCompact.recipes.$4.height).toBe(44);
|
|
59
|
+
expect(recipeFamilies.controlCompact.radii.$4).toBe(9);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("MPO-46 gap == radius", () => {
|
|
64
|
+
it("holds at every size step", () => {
|
|
65
|
+
const generated = generateSizeRecipes();
|
|
66
|
+
for (const token of Object.keys(generated.recipes)) {
|
|
67
|
+
expect(generated.recipes[token].gap, `control ${token}`).toBe(generated.radii[token]);
|
|
68
|
+
expect(generated.families.controlCompact.recipes[token].gap, `compact ${token}`).toBe(
|
|
69
|
+
generated.families.controlCompact.radii[token],
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("holds at every borderRadius knob stop", () => {
|
|
75
|
+
const stops = Object.keys(SIZE_RECIPE_RADIUS_STOPS) as SizeRecipeRadiusStop[];
|
|
76
|
+
for (const token of ["$2", "$3", "$4", "$5", "$6"] as const) {
|
|
77
|
+
const recipe = sizeRecipeForToken(token);
|
|
78
|
+
for (const stop of stops) {
|
|
79
|
+
const next = applyRadiusStop(recipe, stop);
|
|
80
|
+
expect(next.gap, `${token} @ ${stop}`).toBe(SIZE_RECIPE_RADIUS_STOPS[stop]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|