@multiplatform.one/theme 7.34.4 → 7.36.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 (280) hide show
  1. package/package.json +20 -5
  2. package/src/audit/constraintAudit.spec.ts +274 -87
  3. package/src/audit/constraintAudit.ts +135 -120
  4. package/src/audit/constraintScope.spec.ts +1 -1
  5. package/src/audit/designTokens.ts +42 -0
  6. package/src/audit/themeMatrix.spec.ts +83 -2
  7. package/src/audit/themeMatrix.ts +33 -1
  8. package/src/figma/figmaTokens.spec.ts +26 -34
  9. package/src/figma/figmaTokens.ts +26 -50
  10. package/src/fonts/createFontLoader.ts +4 -1
  11. package/src/menuRow.ts +4 -1
  12. package/src/theme/FontKnobStyles.tsx +2 -1
  13. package/src/theme/Preset.stories.tsx +0 -1
  14. package/src/theme/Preset.tsx +6 -1
  15. package/src/theme/PresetContext.ts +4 -0
  16. package/src/theme/Surface.spec.tsx +114 -67
  17. package/src/theme/Surface.stories.tsx +8 -25
  18. package/src/theme/Surface.tsx +87 -43
  19. package/src/theme/accentFloor.spec.ts +37 -11
  20. package/src/theme/activateThemeConfig.ts +22 -6
  21. package/src/theme/animations/animations.spec.ts +62 -0
  22. package/src/theme/animations/css.ts +9 -35
  23. package/src/theme/animations/reactNative.ts +98 -25
  24. package/src/theme/boundaryFloor.spec.ts +57 -0
  25. package/src/theme/chartPalette.spec.ts +56 -27
  26. package/src/theme/chartPalette.ts +33 -28
  27. package/src/theme/colorRules.spec.ts +32 -72
  28. package/src/theme/cookies.spec.ts +49 -18
  29. package/src/theme/cookies.ts +36 -7
  30. package/src/theme/cookiesPersistence.spec.ts +5 -5
  31. package/src/theme/createDefaultThemeConfig.spec.ts +182 -20
  32. package/src/theme/createDefaultThemeConfig.ts +34 -28
  33. package/src/theme/createThemes.ts +169 -218
  34. package/src/theme/defaults/accent.ts +3 -35
  35. package/src/theme/defaults/base.ts +14 -53
  36. package/src/theme/defaults/builderOptions.ts +68 -85
  37. package/src/theme/defaults/categoryFonts.ts +10 -0
  38. package/src/theme/defaults/fonts.spec.ts +84 -4
  39. package/src/theme/defaults/fonts.ts +166 -124
  40. package/src/theme/defaults/house.ts +48 -0
  41. package/src/theme/defaults/index.ts +1 -0
  42. package/src/theme/defaults/neutralRamp.spec.ts +52 -47
  43. package/src/theme/devtools/ColorLineVisualizer.tsx +6 -6
  44. package/src/theme/devtools/ThemeDevtoolsPanel.setMarkers.spec.tsx +13 -6
  45. package/src/theme/devtools/ThemeDevtoolsPanel.tsx +14 -77
  46. package/src/theme/devtools/animationOptions.spec.ts +23 -37
  47. package/src/theme/devtools/animationOptions.ts +4 -6
  48. package/src/theme/devtools/buildSnapshot.ts +0 -3
  49. package/src/theme/devtools/knobIcons.tsx +4 -24
  50. package/src/theme/focusState.spec.ts +9 -30
  51. package/src/theme/index.ts +17 -0
  52. package/src/theme/intentFill.spec.ts +42 -0
  53. package/src/theme/knobs.ts +14 -42
  54. package/src/theme/layoutTokens.ts +15 -1
  55. package/src/theme/layoutTokensHooks.spec.tsx +8 -4
  56. package/src/theme/mergedTokens.ts +20 -0
  57. package/src/theme/motion.spec.ts +84 -0
  58. package/src/theme/motion.ts +53 -0
  59. package/src/theme/motionStops.ts +17 -0
  60. package/src/theme/motionTiming.ts +59 -0
  61. package/src/theme/mpo13Proof.spec.ts +43 -46
  62. package/src/theme/mpo19Proof.spec.ts +13 -14
  63. package/src/theme/namedInkFloor.spec.ts +60 -0
  64. package/src/theme/overlaySurfaces.tsx +330 -0
  65. package/src/theme/preset.types.ts +16 -0
  66. package/src/theme/presetPreview.ts +32 -0
  67. package/src/theme/presetRoot.spec.tsx +89 -0
  68. package/src/theme/presetStore.persistence.spec.ts +141 -0
  69. package/src/theme/presetStore.spec.ts +205 -0
  70. package/src/theme/presetStore.ts +286 -0
  71. package/src/theme/presetUserSync.spec.tsx +168 -0
  72. package/src/theme/presetUserSync.ts +47 -0
  73. package/src/theme/presets.spec.ts +22 -15
  74. package/src/theme/presets.ts +22 -10
  75. package/src/theme/radiusClass.spec.ts +60 -41
  76. package/src/theme/radiusClass.ts +17 -4
  77. package/src/theme/radiusClassTable.generated.ts +3 -3
  78. package/src/theme/readableColor.spec.ts +59 -152
  79. package/src/theme/readableColor.ts +34 -174
  80. package/src/theme/recipeInputs.ts +153 -295
  81. package/src/theme/recipes.ts +58 -18
  82. package/src/theme/resolveKnobs.spec.ts +528 -588
  83. package/src/theme/resolveKnobs.ts +250 -325
  84. package/src/theme/scale/ScaleStyles.native.tsx +19 -0
  85. package/src/theme/scale/ScaleStyles.tsx +48 -0
  86. package/src/theme/scale/activeScale.ts +30 -0
  87. package/src/theme/scale/deriveRecipeInputs.ts +110 -0
  88. package/src/theme/scale/generateScale.spec.ts +260 -0
  89. package/src/theme/scale/generateScale.ts +147 -0
  90. package/src/theme/scale/index.ts +54 -0
  91. package/src/theme/scale/installScaleConfig.native.spec.ts +77 -0
  92. package/src/theme/scale/installScaleConfig.native.ts +57 -0
  93. package/src/theme/scale/installScaleConfig.ts +7 -0
  94. package/src/theme/scale/scaleAttribute.ts +21 -0
  95. package/src/theme/scale/scaleCss.probe.spec.tsx +64 -0
  96. package/src/theme/scale/scaleCss.spec.ts +132 -0
  97. package/src/theme/scale/scaleCss.ts +95 -0
  98. package/src/theme/scale/scaleFonts.ts +106 -0
  99. package/src/theme/scale/scaleInputs.ts +322 -0
  100. package/src/theme/scale/themeScales.ts +181 -0
  101. package/src/theme/scale/useActiveScale.spec.tsx +129 -0
  102. package/src/theme/scale/useActiveScale.ts +95 -0
  103. package/src/theme/schemeStore.spec.ts +81 -0
  104. package/src/theme/schemeStore.ts +67 -0
  105. package/src/theme/shared.spec.ts +7 -6
  106. package/src/theme/shared.tsx +37 -3
  107. package/src/theme/sizeLadder.spec.ts +37 -36
  108. package/src/theme/sizeRecipes.spec.ts +179 -85
  109. package/src/theme/sizeRecipes.ts +48 -25
  110. package/src/theme/subsetThemes.spec.ts +9 -3
  111. package/src/theme/switchThumb.spec.ts +69 -0
  112. package/src/theme/syntaxFloor.spec.ts +11 -8
  113. package/src/theme/theme.hydration.native.spec.tsx +51 -0
  114. package/src/theme/theme.native.tsx +59 -21
  115. package/src/theme/theme.palette.spec.tsx +76 -0
  116. package/src/theme/theme.preset.native.spec.tsx +73 -0
  117. package/src/theme/theme.provider.spec.ts +143 -1
  118. package/src/theme/theme.tsx +61 -25
  119. package/src/theme/tileSizes.ts +6 -7
  120. package/src/theme/transitionProps.native.ts +25 -2
  121. package/src/theme/transitionProps.spec.ts +34 -0
  122. package/src/theme/transitionProps.ts +26 -2
  123. package/src/theme/usePresetSwitch.native.spec.tsx +68 -0
  124. package/src/theme/usePresetSwitch.native.ts +32 -0
  125. package/src/theme/usePresetSwitch.spec.tsx +103 -0
  126. package/src/theme/usePresetSwitch.ts +54 -0
  127. package/src/theme/useResolvedKnobs.hydration.spec.tsx +2 -2
  128. package/src/theme/useResolvedKnobs.ts +23 -90
  129. package/src/theme/useResolvedKnobsBehavior.spec.tsx +25 -30
  130. package/src/theme/useSchemePreference.native.ts +33 -0
  131. package/src/theme/useSchemePreference.spec.tsx +51 -0
  132. package/src/theme/useSchemePreference.ts +32 -0
  133. package/src/theme/useSchemePreference.web.spec.tsx +32 -0
  134. package/src/theme/useTheme.native.spec.tsx +68 -0
  135. package/src/theme/useTheme.native.ts +49 -24
  136. package/src/theme/useTheme.ts +7 -4
  137. package/types/audit/constraintAudit.d.ts +6 -5
  138. package/types/audit/constraintAudit.d.ts.map +1 -1
  139. package/types/audit/designTokens.d.ts +13 -0
  140. package/types/audit/designTokens.d.ts.map +1 -0
  141. package/types/audit/themeMatrix.d.ts +10 -0
  142. package/types/audit/themeMatrix.d.ts.map +1 -1
  143. package/types/figma/figmaTokens.d.ts +11 -28
  144. package/types/figma/figmaTokens.d.ts.map +1 -1
  145. package/types/fonts/createFontLoader.d.ts.map +1 -1
  146. package/types/menuRow.d.ts +4 -1
  147. package/types/menuRow.d.ts.map +1 -1
  148. package/types/theme/FontKnobStyles.d.ts.map +1 -1
  149. package/types/theme/Preset.d.ts.map +1 -1
  150. package/types/theme/PresetContext.d.ts +4 -0
  151. package/types/theme/PresetContext.d.ts.map +1 -1
  152. package/types/theme/Surface.d.ts +57 -26
  153. package/types/theme/Surface.d.ts.map +1 -1
  154. package/types/theme/activateThemeConfig.d.ts +5 -3
  155. package/types/theme/activateThemeConfig.d.ts.map +1 -1
  156. package/types/theme/animations/css.d.ts +27 -34
  157. package/types/theme/animations/css.d.ts.map +1 -1
  158. package/types/theme/animations/reactNative.d.ts +196 -45
  159. package/types/theme/animations/reactNative.d.ts.map +1 -1
  160. package/types/theme/chartPalette.d.ts +11 -3
  161. package/types/theme/chartPalette.d.ts.map +1 -1
  162. package/types/theme/colorRules.d.ts +1 -1
  163. package/types/theme/cookies.d.ts +12 -1
  164. package/types/theme/cookies.d.ts.map +1 -1
  165. package/types/theme/createDefaultThemeConfig.d.ts +13 -13
  166. package/types/theme/createDefaultThemeConfig.d.ts.map +1 -1
  167. package/types/theme/createThemes.d.ts +58 -2
  168. package/types/theme/createThemes.d.ts.map +1 -1
  169. package/types/theme/defaults/accent.d.ts.map +1 -1
  170. package/types/theme/defaults/base.d.ts.map +1 -1
  171. package/types/theme/defaults/builderOptions.d.ts.map +1 -1
  172. package/types/theme/defaults/categoryFonts.d.ts +10 -0
  173. package/types/theme/defaults/categoryFonts.d.ts.map +1 -1
  174. package/types/theme/defaults/fonts.d.ts +330 -30
  175. package/types/theme/defaults/fonts.d.ts.map +1 -1
  176. package/types/theme/defaults/house.d.ts +8 -0
  177. package/types/theme/defaults/house.d.ts.map +1 -0
  178. package/types/theme/defaults/index.d.ts +1 -0
  179. package/types/theme/defaults/index.d.ts.map +1 -1
  180. package/types/theme/devtools/ThemeDevtoolsPanel.d.ts +3 -3
  181. package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
  182. package/types/theme/devtools/animationOptions.d.ts.map +1 -1
  183. package/types/theme/devtools/buildSnapshot.d.ts.map +1 -1
  184. package/types/theme/devtools/knobIcons.d.ts +0 -3
  185. package/types/theme/devtools/knobIcons.d.ts.map +1 -1
  186. package/types/theme/index.d.ts +11 -0
  187. package/types/theme/index.d.ts.map +1 -1
  188. package/types/theme/knobs.d.ts +8 -36
  189. package/types/theme/knobs.d.ts.map +1 -1
  190. package/types/theme/layoutTokens.d.ts +13 -1
  191. package/types/theme/layoutTokens.d.ts.map +1 -1
  192. package/types/theme/mergedTokens.d.ts +11 -0
  193. package/types/theme/mergedTokens.d.ts.map +1 -0
  194. package/types/theme/motion.d.ts +17 -0
  195. package/types/theme/motion.d.ts.map +1 -0
  196. package/types/theme/motionStops.d.ts +3 -0
  197. package/types/theme/motionStops.d.ts.map +1 -0
  198. package/types/theme/motionTiming.d.ts +36 -0
  199. package/types/theme/motionTiming.d.ts.map +1 -0
  200. package/types/theme/overlaySurfaces.d.ts +70 -0
  201. package/types/theme/overlaySurfaces.d.ts.map +1 -0
  202. package/types/theme/preset.types.d.ts +16 -0
  203. package/types/theme/preset.types.d.ts.map +1 -1
  204. package/types/theme/presetPreview.d.ts +14 -0
  205. package/types/theme/presetPreview.d.ts.map +1 -0
  206. package/types/theme/presetStore.d.ts +40 -0
  207. package/types/theme/presetStore.d.ts.map +1 -0
  208. package/types/theme/presetUserSync.d.ts +7 -0
  209. package/types/theme/presetUserSync.d.ts.map +1 -0
  210. package/types/theme/presets.d.ts +12 -4
  211. package/types/theme/presets.d.ts.map +1 -1
  212. package/types/theme/radiusClass.d.ts +5 -2
  213. package/types/theme/radiusClass.d.ts.map +1 -1
  214. package/types/theme/radiusClassTable.generated.d.ts +3 -3
  215. package/types/theme/readableColor.d.ts +11 -49
  216. package/types/theme/readableColor.d.ts.map +1 -1
  217. package/types/theme/recipeInputs.d.ts +39 -70
  218. package/types/theme/recipeInputs.d.ts.map +1 -1
  219. package/types/theme/recipes.d.ts +79 -18
  220. package/types/theme/recipes.d.ts.map +1 -1
  221. package/types/theme/resolveKnobs.d.ts +64 -32
  222. package/types/theme/resolveKnobs.d.ts.map +1 -1
  223. package/types/theme/scale/ScaleStyles.d.ts +18 -0
  224. package/types/theme/scale/ScaleStyles.d.ts.map +1 -0
  225. package/types/theme/scale/ScaleStyles.native.d.ts +10 -0
  226. package/types/theme/scale/ScaleStyles.native.d.ts.map +1 -0
  227. package/types/theme/scale/activeScale.d.ts +13 -0
  228. package/types/theme/scale/activeScale.d.ts.map +1 -0
  229. package/types/theme/scale/deriveRecipeInputs.d.ts +10 -0
  230. package/types/theme/scale/deriveRecipeInputs.d.ts.map +1 -0
  231. package/types/theme/scale/generateScale.d.ts +38 -0
  232. package/types/theme/scale/generateScale.d.ts.map +1 -0
  233. package/types/theme/scale/index.d.ts +17 -0
  234. package/types/theme/scale/index.d.ts.map +1 -0
  235. package/types/theme/scale/installScaleConfig.d.ts +4 -0
  236. package/types/theme/scale/installScaleConfig.d.ts.map +1 -0
  237. package/types/theme/scale/installScaleConfig.native.d.ts +13 -0
  238. package/types/theme/scale/installScaleConfig.native.d.ts.map +1 -0
  239. package/types/theme/scale/scaleAttribute.d.ts +14 -0
  240. package/types/theme/scale/scaleAttribute.d.ts.map +1 -0
  241. package/types/theme/scale/scaleCss.d.ts +25 -0
  242. package/types/theme/scale/scaleCss.d.ts.map +1 -0
  243. package/types/theme/scale/scaleFonts.d.ts +16 -0
  244. package/types/theme/scale/scaleFonts.d.ts.map +1 -0
  245. package/types/theme/scale/scaleInputs.d.ts +66 -0
  246. package/types/theme/scale/scaleInputs.d.ts.map +1 -0
  247. package/types/theme/scale/themeScales.d.ts +52 -0
  248. package/types/theme/scale/themeScales.d.ts.map +1 -0
  249. package/types/theme/scale/useActiveScale.d.ts +31 -0
  250. package/types/theme/scale/useActiveScale.d.ts.map +1 -0
  251. package/types/theme/schemeStore.d.ts +6 -0
  252. package/types/theme/schemeStore.d.ts.map +1 -0
  253. package/types/theme/shared.d.ts +18 -0
  254. package/types/theme/shared.d.ts.map +1 -1
  255. package/types/theme/sizeRecipes.d.ts +20 -15
  256. package/types/theme/sizeRecipes.d.ts.map +1 -1
  257. package/types/theme/theme.d.ts +2 -1
  258. package/types/theme/theme.d.ts.map +1 -1
  259. package/types/theme/theme.native.d.ts +10 -1
  260. package/types/theme/theme.native.d.ts.map +1 -1
  261. package/types/theme/tileSizes.d.ts +6 -7
  262. package/types/theme/tileSizes.d.ts.map +1 -1
  263. package/types/theme/transitionProps.d.ts +3 -1
  264. package/types/theme/transitionProps.d.ts.map +1 -1
  265. package/types/theme/transitionProps.native.d.ts +3 -1
  266. package/types/theme/transitionProps.native.d.ts.map +1 -1
  267. package/types/theme/usePresetSwitch.d.ts +10 -0
  268. package/types/theme/usePresetSwitch.d.ts.map +1 -0
  269. package/types/theme/usePresetSwitch.native.d.ts +10 -0
  270. package/types/theme/usePresetSwitch.native.d.ts.map +1 -0
  271. package/types/theme/useResolvedKnobs.d.ts +13 -13
  272. package/types/theme/useResolvedKnobs.d.ts.map +1 -1
  273. package/types/theme/useSchemePreference.d.ts +8 -0
  274. package/types/theme/useSchemePreference.d.ts.map +1 -0
  275. package/types/theme/useSchemePreference.native.d.ts +8 -0
  276. package/types/theme/useSchemePreference.native.d.ts.map +1 -0
  277. package/types/theme/useTheme.d.ts +4 -2
  278. package/types/theme/useTheme.d.ts.map +1 -1
  279. package/types/theme/useTheme.native.d.ts +10 -1
  280. package/types/theme/useTheme.native.d.ts.map +1 -1
@@ -1,10 +1,12 @@
1
1
  import { afterEach, describe, expect, it } from "vitest";
2
2
  import { defaultKnobs } from "./knobs";
3
- import type { Knobs } from "./knobs";
4
3
  import { resolveKnobs } from "./resolveKnobs";
4
+ import { getActiveScaleTokens, setActiveScaleTokens } from "./scale/activeScale";
5
+ import { generateScale } from "./scale/generateScale";
5
6
  import {
6
7
  DEFAULT_SIZE_RECIPE_INPUTS,
7
- SIZE_RECIPE_COMPACT_RATIOS,
8
+ SIZE_RECIPE_CHIP_RATIOS,
9
+ SIZE_RECIPE_FAMILY_NAMES,
8
10
  SIZE_RECIPE_HEIGHTS,
9
11
  SIZE_RECIPE_HEIGHTS_TOUCH,
10
12
  SIZE_RECIPE_RADII,
@@ -14,6 +16,7 @@ import {
14
16
  generateSizeRecipeTables,
15
17
  generateSizeRecipes,
16
18
  getSizeRecipeFamily,
19
+ recipeKeyBelow,
17
20
  recipeFamilies,
18
21
  setSizeRecipeInputs,
19
22
  sizeRecipeBoxVariants,
@@ -27,10 +30,6 @@ import {
27
30
  type SizeRecipe,
28
31
  } from "./sizeRecipes";
29
32
 
30
- function knobsWith(overrides: Partial<Knobs>): Knobs {
31
- return { ...defaultKnobs, ...overrides };
32
- }
33
-
34
33
  const RECIPE_KEYS = ["$1", "$1.5", "$2", "$2.5", "$3", "$3.5", "$4", "$true", "$5", "$6"] as const;
35
34
 
36
35
  function expectNoRadius(value: object) {
@@ -39,23 +38,23 @@ function expectNoRadius(value: object) {
39
38
  }
40
39
 
41
40
  describe("size recipes", () => {
42
- it("medium / $4 matches the measured tamagui.dev 44/18/14/16/9 reference", () => {
41
+ it("medium / $4 matches the default 40/16/15/16/8 scale", () => {
43
42
  const recipe = sizeRecipes.$4;
44
43
  expect(recipe).toEqual({
45
- height: 44,
46
- paddingHorizontal: 18,
47
- fontSize: 14,
44
+ height: 40,
45
+ paddingHorizontal: 16,
46
+ fontSize: 15,
48
47
  iconSize: 16,
49
- gap: 9,
48
+ gap: 8,
50
49
  });
51
- expect(sizeRecipeFromHeight(44)).toEqual(recipe);
50
+ expect(sizeRecipeFromHeight(40)).toEqual(recipe);
52
51
  expect(sizeRecipeForToken("$4")).toEqual(recipe);
53
- expect(SIZE_RECIPE_HEIGHTS.$4).toBe(44);
52
+ expect(SIZE_RECIPE_HEIGHTS.$4).toBe(40);
54
53
  expect(SIZE_RECIPE_RATIOS).toEqual({
55
- padX: 18 / 44,
56
- font: 14 / 44,
57
- icon: 16 / 44,
58
- gap: 9 / 44,
54
+ padX: 16 / 40,
55
+ font: 15 / 40,
56
+ icon: 16 / 40,
57
+ gap: 8 / 40,
59
58
  });
60
59
  });
61
60
 
@@ -67,7 +66,7 @@ describe("size recipes", () => {
67
66
 
68
67
  it("radius is not on the recipe or any variant object", () => {
69
68
  expectNoRadius(sizeRecipes.$4);
70
- expectNoRadius(sizeRecipeFromHeight(44));
69
+ expectNoRadius(sizeRecipeFromHeight(40));
71
70
  expectNoRadius(sizeRecipeForToken("$4"));
72
71
  for (const token of RECIPE_KEYS) {
73
72
  expectNoRadius(sizeRecipes[token]);
@@ -91,7 +90,7 @@ describe("size recipes", () => {
91
90
  height: recipe.iconSize,
92
91
  });
93
92
  }
94
- expect(sizeRecipeFromHeight(44)).toEqual(sizeRecipes.$4);
93
+ expect(sizeRecipeFromHeight(40)).toEqual(sizeRecipes.$4);
95
94
  });
96
95
 
97
96
  it("sizeRecipeForToken falls back to $4", () => {
@@ -99,9 +98,9 @@ describe("size recipes", () => {
99
98
  expect(sizeRecipeForToken("")).toEqual(sizeRecipes.$4);
100
99
  });
101
100
 
102
- it("small vs large: height/padX/font/icon/gap all change; borderRadius stays identical", () => {
103
- const small = resolveKnobs(knobsWith({ size: "small" }));
104
- const large = resolveKnobs(knobsWith({ size: "large" }));
101
+ it("$3 vs $5: height/padX/font/icon/gap all change; borderRadius stays identical", () => {
102
+ const small = resolveKnobs(defaultKnobs, undefined, "light", { sizeToken: "$3" });
103
+ const large = resolveKnobs(defaultKnobs, undefined, "light", { sizeToken: "$5" });
105
104
  const smallRecipe = sizeRecipeForToken(small.knobProps.sizeToken);
106
105
  const largeRecipe = sizeRecipeForToken(large.knobProps.sizeToken);
107
106
 
@@ -137,26 +136,44 @@ describe("size recipes", () => {
137
136
  expect(small.knobProps.borderRadius).toEqual(large.knobProps.borderRadius);
138
137
  });
139
138
 
140
- it("layout gap still follows the space knob independently of size (T2.1a)", () => {
141
- const { knobProps } = resolveKnobs(knobsWith({ space: "large", size: "medium" }));
142
- expect(knobProps.sizeToken).toBe("$4");
143
- expect(knobProps.gap.gap).toBe("$5");
139
+ it("the layout gap is a spacing role, not the recipe gap (LC-79)", () => {
140
+ const { knobProps } = resolveKnobs(defaultKnobs);
141
+ expect(knobProps.sizeToken).toBe("$true");
142
+ expect(knobProps.gap.gap).toBe("$3");
144
143
  expect(knobProps.control.gap).toBe(sizeRecipes.$4.gap);
145
144
  });
146
- it("touch heights floor at 44 and still differentiate size steps", () => {
145
+ it("touch floors only height at 44", () => {
147
146
  const desktop = sizeRecipeForToken("$4");
148
147
  expect(desktop).toEqual(sizeRecipes.$4);
149
- expect(applyTouchFloor(desktop, false).height).toBe(44);
150
- expect(applyTouchFloor(desktop, true)).toEqual(sizeRecipeFromHeight(48));
151
- expect(sizeRecipeForToken("$4", { touch: true })).toEqual(sizeRecipeFromHeight(48));
148
+ expect(applyTouchFloor(desktop, false).height).toBe(40);
149
+ expect(applyTouchFloor(desktop, true)).toEqual({ ...desktop, height: 44 });
150
+ expect(sizeRecipeForToken("$4", { touch: true })).toEqual({ ...desktop, height: 44 });
152
151
  expect(sizeRecipeForToken("$3", { touch: true }).height).toBe(44);
153
- expect(sizeRecipeForToken("$5", { touch: true }).height).toBe(56);
154
- expect(sizeRecipeForToken("$6", { touch: true }).height).toBe(68);
155
- expect(sizeRecipeBoxVariants.$4.height).toBe(44);
152
+ expect(sizeRecipeForToken("$5", { touch: true }).height).toBe(48);
153
+ expect(sizeRecipeForToken("$6", { touch: true }).height).toBe(58);
154
+ expect(sizeRecipeBoxVariants.$4.height).toBe(40);
156
155
  expectNoRadius(applyTouchFloor(desktop, true));
157
156
  });
158
157
  });
159
158
 
159
+ describe("touch preserves every other recipe channel", () => {
160
+ for (const family of SIZE_RECIPE_FAMILY_NAMES) {
161
+ it(`${family} keeps padding, text, icon and gap at every token`, () => {
162
+ for (const token of RECIPE_KEYS) {
163
+ const desktop = sizeRecipeForToken(token, { family });
164
+ const expected = { ...desktop, height: Math.max(desktop.height, 44) };
165
+ expect(sizeRecipeForToken(token, { family, touch: true })).toEqual(expected);
166
+ expect(applyTouchFloor(desktop, true, family)).toEqual(expected);
167
+ }
168
+ });
169
+ }
170
+ it("preserves custom off-table channels in the fallback", () => {
171
+ const recipe = { height: 31, paddingHorizontal: 7, fontSize: 13, iconSize: 17, gap: 5 };
172
+ expect(applyTouchFloor(recipe, true)).toEqual({ ...recipe, height: 44 });
173
+ expect(applyTouchFloor({ ...recipe, height: 51 }, true)).toEqual({ ...recipe, height: 51 });
174
+ });
175
+ });
176
+
160
177
  describe("SizeRecipe shape", () => {
161
178
  it("has no radius field on the type-level contract", () => {
162
179
  const recipe: SizeRecipe = sizeRecipes.$4;
@@ -170,30 +187,21 @@ describe("generateSizeRecipes (LC-70)", () => {
170
187
  setSizeRecipeInputs();
171
188
  });
172
189
 
173
- it("DEFAULT $4 is the measured 44/18/14/16/9 with no radius", () => {
190
+ it("DEFAULT $4 is the default 40/16/15/16/8 with no radius", () => {
174
191
  const generated = generateSizeRecipes(DEFAULT_SIZE_RECIPE_INPUTS);
175
192
  expect(generated.recipes.$4).toEqual({
176
- height: 44,
177
- paddingHorizontal: 18,
178
- fontSize: 14,
193
+ height: 40,
194
+ paddingHorizontal: 16,
195
+ fontSize: 15,
179
196
  iconSize: 16,
180
- gap: 9,
197
+ gap: 8,
181
198
  });
182
199
  expectNoRadius(generated.recipes.$4);
183
200
  expectNoRadius(generated.boxVariants.$4);
184
- expect(generated.inputs.radiusPx).toBe(9);
185
- expect(generated.heights).toEqual({
186
- $1: 20,
187
- "$1.5": 24,
188
- $2: 28,
189
- "$2.5": 32,
190
- $3: 36,
191
- "$3.5": 40,
192
- $4: 44,
193
- $true: 44,
194
- $5: 52,
195
- $6: 64,
196
- });
201
+ expect(generated.inputs.radiusPx).toBe(8);
202
+ expect(generated.heights).toEqual(
203
+ Object.fromEntries(RECIPE_KEYS.map((token) => [token, generateScale().tokens.size[token]])),
204
+ );
197
205
  expect(generated.heightsTouch).toEqual({
198
206
  $1: 44,
199
207
  "$1.5": 44,
@@ -201,39 +209,48 @@ describe("generateSizeRecipes (LC-70)", () => {
201
209
  "$2.5": 44,
202
210
  $3: 44,
203
211
  "$3.5": 44,
204
- $4: 48,
205
- $true: 48,
206
- $5: 56,
207
- $6: 68,
212
+ $4: 44,
213
+ $true: 44,
214
+ $5: 48,
215
+ $6: 58,
208
216
  });
209
217
  expect(generateSizeRecipeTables(defaultRecipeInputs).touchHeights).toEqual(
210
218
  generated.heightsTouch,
211
219
  );
212
- expect(SIZE_RECIPE_HEIGHTS_TOUCH.$4).toBe(48);
220
+ expect(SIZE_RECIPE_HEIGHTS_TOUCH.$4).toBe(44);
213
221
  });
214
222
 
215
- it("baseHeight: 40 without tokenHeights regenerates $4 and keeps $5 distinct", () => {
216
- const generated = generateSizeRecipes({ baseHeight: 40 });
217
- expect(generated.inputs.tokenHeights).toBeUndefined();
223
+ it("control: 40 regenerates $4 from the scale and keeps $5 distinct", () => {
224
+ const generated = generateSizeRecipes({ control: 40 });
225
+ const { size, space, radius } = generateScale({ control: 40, type: 15 }).tokens;
226
+ expect(generated.inputs.control).toBe(40);
218
227
  expect(generated.heights.$4).toBe(40);
228
+ expect(generated.recipes.$4).toEqual({
229
+ height: size.$4,
230
+ paddingHorizontal: space.$4,
231
+ fontSize: 15,
232
+ iconSize: 16,
233
+ gap: radius[4],
234
+ });
219
235
  expect(generated.recipes.$4).toEqual({
220
236
  height: 40,
221
- paddingHorizontal: Math.round(40 * (18 / 44)),
222
- fontSize: Math.round(40 * (14 / 44)),
223
- iconSize: Math.round(40 * (16 / 44)),
224
- gap: Math.round(40 * (9 / 44)),
237
+ paddingHorizontal: 16,
238
+ fontSize: 15,
239
+ iconSize: 16,
240
+ gap: 8,
225
241
  });
242
+ expect(generated.heights.$5).toBe(size.$5);
226
243
  expect(generated.heights.$5).not.toBe(generated.heights.$4);
227
244
  expectNoRadius(generated.recipes.$4);
228
245
  });
229
246
 
230
- it("controlCompact padX is tighter than control at the same $4 height", () => {
247
+ it("chip padX is tighter than control at the same $4 height", () => {
231
248
  const generated = generateSizeRecipes(DEFAULT_SIZE_RECIPE_INPUTS);
232
- expect(generated.families.control.recipes.$4.height).toBe(44);
233
- expect(generated.families.controlCompact.recipes.$4.height).toBe(44);
234
- expect(generated.families.control.recipes.$4.paddingHorizontal).toBe(18);
235
- expect(generated.families.controlCompact.recipes.$4.paddingHorizontal).toBe(16);
236
- expect(generated.families.controlCompact.recipes.$4.paddingHorizontal).toBeLessThan(
249
+ expect(generated.families.control.recipes.$4.height).toBe(40);
250
+ expect(generated.families.chip.recipes.$4.height).toBe(40);
251
+ expect(generated.families.control.recipes.$4.paddingHorizontal).toBe(16);
252
+ expect(generated.families.chip.recipes.$4.paddingHorizontal).toBe(14);
253
+ expect(generated.families.chip.recipes.$4.paddingHorizontal).toBeLessThan(
237
254
  generated.families.control.recipes.$4.paddingHorizontal,
238
255
  );
239
256
  });
@@ -244,11 +261,11 @@ describe("generateSizeRecipes (LC-70)", () => {
244
261
  expectNoRadius(generated.recipes.$4);
245
262
  expectNoRadius(generated.recipesTouch.$4);
246
263
  expectNoRadius(generated.boxVariants.$4);
247
- expectNoRadius(generated.families.controlCompact.recipes.$4);
264
+ expectNoRadius(generated.families.chip.recipes.$4);
248
265
  });
249
266
 
250
- it("setSizeRecipeInputs({ baseHeight }) regenerates live tables from stepRatio", () => {
251
- setSizeRecipeInputs({ baseHeight: 36 });
267
+ it("setSizeRecipeInputs({ control }) regenerates live tables from the scale", () => {
268
+ setSizeRecipeInputs({ control: 36 });
252
269
  expect(SIZE_RECIPE_HEIGHTS.$4).toBe(36);
253
270
  expect(sizeRecipes.$4.height).toBe(36);
254
271
  expect(sizeRecipeForToken("$4").height).toBe(36);
@@ -270,15 +287,40 @@ describe("generateSizeRecipes (LC-70)", () => {
270
287
  expect(sizeRecipeEscape("keep")).toBe("keep");
271
288
  });
272
289
 
273
- it("textFieldInset is min(padX, floor((height-fontSize)/2)) — $4 → 15 (LC-84)", () => {
274
- expect(textFieldInset(sizeRecipes.$4)).toBe(15);
275
- expect(textFieldInset({ height: 44, paddingHorizontal: 18, fontSize: 14 })).toBe(15);
290
+ it("textFieldInset snaps min(padX, floor((height-fontSize)/2)) down to a $space value — $4 → 12", () => {
291
+ expect(textFieldInset(sizeRecipes.$4)).toBe(12);
292
+ expect(textFieldInset({ height: 40, paddingHorizontal: 16, fontSize: 15 })).toBe(12);
276
293
  expect(textFieldInset({ height: 32, paddingHorizontal: 12, fontSize: 14 })).toBe(9);
277
- expect(textFieldInset({ height: 48, paddingHorizontal: 8, fontSize: 14 })).toBe(8);
294
+ expect(textFieldInset({ height: 48, paddingHorizontal: 8, fontSize: 14 })).toBe(6);
278
295
  expect(textFieldInset(sizeRecipes.$4)).toBeLessThanOrEqual(
279
296
  Math.floor((sizeRecipes.$4.height - sizeRecipes.$4.fontSize) / 2),
280
297
  );
281
298
  });
299
+
300
+ it("textFieldInset is a $space value of the active scale on every preset", () => {
301
+ const installed = getActiveScaleTokens();
302
+ for (const inputs of [
303
+ { control: 36, type: 14 },
304
+ { control: 40, type: 15 },
305
+ { control: 48, type: 16 },
306
+ ]) {
307
+ const scale = generateScale(inputs);
308
+ setActiveScaleTokens(scale.tokens);
309
+ try {
310
+ const row = scale.recipeInputs.tokenTable.$4;
311
+ const inset = textFieldInset({
312
+ height: scale.tokens.size.$4,
313
+ paddingHorizontal: row?.paddingHorizontal ?? 0,
314
+ fontSize: row?.fontSize ?? 0,
315
+ });
316
+ expect(Object.values(scale.tokens.space), `${inputs.control}/${inputs.type}`).toContain(
317
+ inset,
318
+ );
319
+ } finally {
320
+ setActiveScaleTokens(installed);
321
+ }
322
+ }
323
+ });
282
324
  });
283
325
 
284
326
  describe("size recipe families", () => {
@@ -290,22 +332,74 @@ describe("size recipe families", () => {
290
332
  expect(control.heightVariants[token]).toEqual({ height: SIZE_RECIPE_HEIGHTS[token] });
291
333
  }
292
334
  expect(control.boxVariants.$4).toEqual({
293
- height: 44,
294
- paddingHorizontal: 18,
295
- gap: 9,
335
+ height: 40,
336
+ paddingHorizontal: 16,
337
+ gap: 8,
296
338
  });
297
339
  });
298
340
 
299
- it("controlCompact keeps height and tightens padX", () => {
300
- const compact = getSizeRecipeFamily("controlCompact");
341
+ it("chip keeps height and tightens padX", () => {
342
+ const compact = getSizeRecipeFamily("chip");
301
343
  const control = getSizeRecipeFamily("control");
302
- expect(compact.ratios).toEqual(SIZE_RECIPE_COMPACT_RATIOS);
344
+ expect(compact.ratios).toEqual(SIZE_RECIPE_CHIP_RATIOS);
303
345
  expect(compact.boxVariants.$4.height).toBe(control.boxVariants.$4.height);
304
346
  expect(compact.boxVariants.$4.paddingHorizontal).toBeLessThan(
305
347
  control.boxVariants.$4.paddingHorizontal,
306
348
  );
307
- expect(compact.boxVariants.$4.paddingHorizontal).toBe(16);
308
- expect(compact.boxVariants.$4.gap).toBe(9);
309
- expect(sizeRecipeForToken("$4", { family: "controlCompact" })).toEqual(compact.recipes.$4);
349
+ expect(compact.boxVariants.$4.paddingHorizontal).toBe(14);
350
+ expect(compact.boxVariants.$4.gap).toBe(8);
351
+ expect(sizeRecipeForToken("$4", { family: "chip" })).toEqual(compact.recipes.$4);
352
+ });
353
+
354
+ it("names every generated family", () => {
355
+ expect(SIZE_RECIPE_FAMILY_NAMES).toEqual(["control", "chip", "menuRow", "tab"]);
356
+ expect(Object.keys(generateSizeRecipes().families).sort()).toEqual(
357
+ [...SIZE_RECIPE_FAMILY_NAMES].sort(),
358
+ );
359
+ });
360
+
361
+ it("chip reads input padX and type one key below at the control's heights", () => {
362
+ const chip = getSizeRecipeFamily("chip");
363
+ for (const token of RECIPE_KEYS) {
364
+ expect(chip.heights[token]).toBe(SIZE_RECIPE_HEIGHTS[token]);
365
+ expect(chip.recipes[token].paddingHorizontal).toBe(
366
+ DEFAULT_SIZE_RECIPE_INPUTS.inputPadX[token],
367
+ );
368
+ }
369
+ });
370
+
371
+ it("menuRow is one key below the control token, padX $3, the token's own type", () => {
372
+ const menuRow = getSizeRecipeFamily("menuRow");
373
+ expect(menuRow.recipes.$4).toMatchObject({ height: 36, paddingHorizontal: 12, fontSize: 15 });
374
+ expect(menuRow.recipes.$true).toEqual(menuRow.recipes.$4);
375
+ expect(sizeRecipeForToken("$4", { family: "menuRow", touch: true }).height).toBe(44);
376
+ for (const token of RECIPE_KEYS) {
377
+ expect(menuRow.heights[token]).toBe(SIZE_RECIPE_HEIGHTS[recipeKeyBelow(token)]);
378
+ expect(menuRow.recipes[token].paddingHorizontal).toBe(sizeRecipes.$3.paddingHorizontal);
379
+ expect(menuRow.recipes[token].fontSize).toBe(sizeRecipes[token].fontSize);
380
+ }
381
+ });
382
+
383
+ it("tab takes height and padX one key below and the token's own type", () => {
384
+ const tab = getSizeRecipeFamily("tab");
385
+ expect(tab.recipes.$4).toMatchObject({ height: 36, fontSize: 15 });
386
+ for (const token of RECIPE_KEYS) {
387
+ const below = sizeRecipes[recipeKeyBelow(token)];
388
+ expect(tab.heights[token]).toBe(below.height);
389
+ expect(tab.recipes[token].paddingHorizontal).toBe(below.paddingHorizontal);
390
+ expect(tab.recipes[token].fontSize).toBe(sizeRecipes[token].fontSize);
391
+ }
392
+ });
393
+
394
+ it("row families follow a regenerated scale", () => {
395
+ try {
396
+ setSizeRecipeInputs({ control: 44 });
397
+ expect(getSizeRecipeFamily("menuRow").heights.$4).toBe(
398
+ getSizeRecipeFamily("control").heights["$3.5"],
399
+ );
400
+ } finally {
401
+ setSizeRecipeInputs();
402
+ }
403
+ expect(getSizeRecipeFamily("menuRow").heights.$4).toBe(36);
310
404
  });
311
405
  });
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * Coordinated control-size recipes (LC-75 SIZE-RECIPE, LC-80 RECIPE-INPUTS).
3
3
  *
4
- * The default ladder is the measured Tamagui v5 table in recipeInputs
4
+ * The default ladder is read off the generated token scale in recipeInputs
5
5
  * (padX / font / icon / radius per size token). Default gap equals the
6
6
  * size-table radius; corner shape overrides leave that spacing intact.
7
- * A linear ratio is only the `{ baseHeight }` fallback — not a
8
- * documented divergence (MPO-46 / R6).
9
7
  *
10
8
  * Tables are GENERATED from `defaultRecipeInputs` at module eval (Tamagui
11
9
  * 2.0.0-rc.41 flattens `styled(View, { variants: { size: tables } })` when
@@ -15,13 +13,18 @@
15
13
  * reserve it for non-enumerable values only. Do NOT put recipe tables on a
16
14
  * `createStyledContext` primitive (`neverFlatten`).
17
15
  *
18
- * Reference md ($4 / $true): height 44, padX 18, font 14, icon 16, radius 9,
19
- * gap 9 (the measured tamagui.dev $4 control — MPO-13 / MPO-19 X1/X2).
16
+ * Reference md ($4 / $true): height 40, padX 16, font 15, icon 16, radius 8,
17
+ * gap 8. The v5 44px control is generated with `v5ScaleInputs`.
20
18
  * `sizeToken` remains the recipe KEY (string "$4") so styled() variants
21
19
  * can switch; it is not a height scalar.
22
20
  *
23
- * Families: `control` reads `inputs.tokenTable`. `controlCompact` reads
24
- * `compactTokenTable` at the SAME heights (Input padX, tighter type).
21
+ * Families (`SIZE_RECIPE_FAMILY_NAMES`), all generated from the scale:
22
+ * - `control` reads `inputs.tokenTable`.
23
+ * - `chip` reads `chipTokenTable` at the SAME heights (Input padX, type one
24
+ * key below).
25
+ * - `menuRow` (menu / listbox / command rows): height one key below the
26
+ * control token (36 at default), padX `$3`, the token's own type.
27
+ * - `tab`: height and padX one key below, the token's own type.
25
28
  *
26
29
  * On touch (LC-77), `applyTouchFloor` / `sizeRecipeForToken(token, { touch: true })`
27
30
  * lift painted height to ≥ 44. Static `sizeRecipe*Variants` stay on the
@@ -34,9 +37,6 @@ export {
34
37
  SIZE_RECIPE_RADIUS_STOPS,
35
38
  SIZE_RECIPE_TOKENS,
36
39
  SIZE_TOKEN_EXPONENTS,
37
- TAMAGUI_COMPACT_SIZE_TABLE,
38
- TAMAGUI_CONTROL_SIZE_TABLE,
39
- TAMAGUI_INPUT_PADX,
40
40
  TOUCH_HEIGHT_FLOOR,
41
41
  applyRadiusStop,
42
42
  SIZE_RECIPE_RADIUS_TOKEN_TO_STOP,
@@ -44,7 +44,8 @@ export {
44
44
  resolveControlRadius,
45
45
  createRecipeFamilies,
46
46
  defaultRecipeInputs,
47
- defaultSizeFactors,
47
+ recipeKeyAbove,
48
+ recipeKeyBelow,
48
49
  generateSizeRecipeTables,
49
50
  generateSizeRecipes,
50
51
  mergeRecipeInputs,
@@ -71,6 +72,7 @@ export type {
71
72
  SizeRecipeTypeVariant,
72
73
  } from "./recipeInputs";
73
74
 
75
+ import { getActiveScaleTokens } from "./scale/activeScale";
74
76
  import {
75
77
  cloneSizeRecipeInputs,
76
78
  defaultRecipeInputs,
@@ -123,9 +125,9 @@ export const SIZE_RECIPE_HEIGHTS = recipeFamilies.control.heights;
123
125
  export const SIZE_RECIPE_HEIGHTS_TOUCH = recipeFamilies.control.touchHeights;
124
126
  export const SIZE_RECIPE_RADII = recipeFamilies.control.radii;
125
127
  export const SIZE_RECIPE_RATIOS = recipeFamilies.control.ratios;
126
- export const SIZE_RECIPE_COMPACT_RATIOS = recipeFamilies.controlCompact.ratios;
128
+ export const SIZE_RECIPE_CHIP_RATIOS = recipeFamilies.chip.ratios;
127
129
 
128
- export const SIZE_RECIPE_FAMILY_NAMES = ["control", "controlCompact"] as const;
130
+ export const SIZE_RECIPE_FAMILY_NAMES = ["control", "chip", "menuRow", "tab"] as const;
129
131
 
130
132
  /**
131
133
  * Live tile-size ladder (MPO-31) — generated from `inputs.tile` over the
@@ -139,8 +141,8 @@ export function getTileSizeLadder(): typeof tileSizeLadder {
139
141
  }
140
142
 
141
143
  function adoptGenerated(next: GeneratedSizeRecipes): GeneratedSizeRecipes {
142
- adoptFamily(recipeFamilies.control, next.families.control);
143
- adoptFamily(recipeFamilies.controlCompact, next.families.controlCompact);
144
+ for (const name of SIZE_RECIPE_FAMILY_NAMES)
145
+ adoptFamily(recipeFamilies[name], next.families[name]);
144
146
  replaceRecord(
145
147
  tileSizeLadder as unknown as Record<string, unknown>,
146
148
  next.tiles as unknown as Record<string, unknown>,
@@ -197,11 +199,22 @@ export function applyTouchFloor(
197
199
  ): SizeRecipe {
198
200
  if (!touch) return recipe;
199
201
  const tables = resolveFamily(family);
200
- const token = (Object.keys(tables.heights) as SizeRecipeToken[]).find(
201
- (key) => tables.heights[key] === recipe.height,
202
- );
202
+ // Shifted families repeat a height at the floor ($1 / $1.5), so match the
203
+ // whole recipe first and the height only for an off-table recipe.
204
+ const keys = Object.keys(tables.heights) as SizeRecipeToken[];
205
+ const token =
206
+ keys.find((key) => {
207
+ const row = tables.recipes[key];
208
+ return (
209
+ row.height === recipe.height &&
210
+ row.paddingHorizontal === recipe.paddingHorizontal &&
211
+ row.fontSize === recipe.fontSize &&
212
+ row.iconSize === recipe.iconSize &&
213
+ row.gap === recipe.gap
214
+ );
215
+ }) ?? keys.find((key) => tables.heights[key] === recipe.height);
203
216
  if (token && tables.recipesTouch[token]) return tables.recipesTouch[token];
204
- return recipeFromHeight(Math.max(recipe.height, tables.inputs.touchFloor), tables.ratios);
217
+ return { ...recipe, height: Math.max(recipe.height, tables.inputs.touchFloor) };
205
218
  }
206
219
 
207
220
  export function sizeRecipeForToken(
@@ -232,16 +245,26 @@ export function sizeRecipeEscape<T>(value: T, _reason?: string): T {
232
245
  }
233
246
 
234
247
  /**
235
- * LC-84 TEXT-INSET — optical vertical inset of a text field, never larger
236
- * than the recipe padX. Extra horizontal space is legal only when an icon
237
- * well occupies that side, not as empty-field padX.
248
+ * TEXT-INSET — optical vertical inset of a text field, never larger than the
249
+ * recipe padX, snapped down to the largest `$space` value of the active
250
+ * scale at or under it so the inset is a token on every preset (LC-100).
251
+ * Extra horizontal space is legal only when an icon well occupies that side,
252
+ * not as empty-field padX.
238
253
  *
239
- * min(paddingHorizontal, floor((height − fontSize) / 2))
254
+ * snap(min(paddingHorizontal, floor((height − fontSize) / 2)))
240
255
  *
241
- * `$4` (44 / 18 / 14) → 15. Button ladders keep recipe.paddingHorizontal (18).
256
+ * `$4` (40 / 16 / 15) → 12 (`$3`); roomy 48 / 21 / 16 → 15; dense 36 / 13 / 14 → 9.
242
257
  */
243
258
  export function textFieldInset(
244
259
  recipe: Pick<SizeRecipe, "height" | "paddingHorizontal" | "fontSize">,
245
260
  ): number {
246
- return Math.min(recipe.paddingHorizontal, Math.floor((recipe.height - recipe.fontSize) / 2));
261
+ const optical = Math.min(
262
+ recipe.paddingHorizontal,
263
+ Math.floor((recipe.height - recipe.fontSize) / 2),
264
+ );
265
+ let inset = 0;
266
+ for (const [key, px] of Object.entries(getActiveScaleTokens().space)) {
267
+ if (!key.startsWith("-") && px <= optical && px > inset) inset = px;
268
+ }
269
+ return inset;
247
270
  }
@@ -13,7 +13,7 @@ vi.mock("tamagui", () => ({
13
13
  vi.mock("./animations/index", () => ({ animations: {} }));
14
14
 
15
15
  import { createDefaultThemeConfig } from "./createDefaultThemeConfig";
16
- import { createThemesBuilder } from "./createThemes";
16
+ import { createThemesBuilder, houseComponentThemes } from "./createThemes";
17
17
  import { defaultAccentTheme } from "./defaults/accent";
18
18
  import { defaultBaseTheme } from "./defaults/base";
19
19
  import { defaultBuilderOptions } from "./defaults/builderOptions";
@@ -79,16 +79,22 @@ describe("subsetThemes over the house matrix", () => {
79
79
  components: ["Button"],
80
80
  tints: ["accent", "active", "alt1", "alt2", "error", "success", "warning"],
81
81
  });
82
- expect(Object.keys(kept).length).toBeLessThan(Object.keys(built).length / 4);
82
+ expect(Object.keys(built)).toHaveLength(462);
83
+ expect(Object.keys(kept)).toHaveLength(44);
83
84
  for (const name of ["light", "dark", "light_accent", "light_Button", "dark_error_Button"]) {
84
85
  expect(kept).toHaveProperty(name);
85
86
  }
86
- for (const name of ["light_blue", "dark_red", "light_Tooltip", "light_accent_Input"]) {
87
+ for (const name of ["light_blue", "dark_red", "light_Slider", "light_accent_Input"]) {
87
88
  expect(built).toHaveProperty(name);
88
89
  expect(kept).not.toHaveProperty(name);
89
90
  }
90
91
  });
91
92
 
93
+ it("the shipped component list matches every v5 house surface", async () => {
94
+ const { themeSubset } = await import("../../../../packages/themes/subset");
95
+ expect(themeSubset.components).toEqual(Object.keys(houseComponentThemes).sort());
96
+ });
97
+
92
98
  it("is what createDefaultThemeConfig({ subset }) ships, and omitting it ships everything", () => {
93
99
  const full = createDefaultThemeConfig().tamagui.themes as Record<string, unknown>;
94
100
  const subset = createDefaultThemeConfig({ subset: narrow }).tamagui.themes as Record<
@@ -0,0 +1,69 @@
1
+ import { configWithoutAnimations } from "@tamagui/config";
2
+ import { createTamagui } from "tamagui";
3
+ import { describe, expect, it } from "vitest";
4
+ import { contrastRatio, minContrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
5
+ import { createThemesBuilder } from "./createThemes";
6
+ import { defaultAccentTheme } from "./defaults/accent";
7
+ import { defaultBaseTheme } from "./defaults/base";
8
+ import { defaultBuilderOptions } from "./defaults/builderOptions";
9
+ import { defaultKnobs } from "./knobs";
10
+ import { resolveKnobs } from "./resolveKnobs";
11
+
12
+ const themes = createThemesBuilder(
13
+ defaultBaseTheme,
14
+ defaultAccentTheme,
15
+ defaultBuilderOptions,
16
+ ).themes() as Record<string, Record<string, string>>;
17
+ const config = createTamagui({ ...configWithoutAnimations, themes });
18
+
19
+ const tintedSchemes = Object.keys(themes).filter(
20
+ (name) => /^(light|dark)(_[a-z]+)?$/.test(name) && !/_(active|accent)$/.test(name),
21
+ );
22
+
23
+ function ratio(a: string, b: string) {
24
+ const aHex = normalizeToHex(a);
25
+ const bHex = normalizeToHex(b);
26
+ if (!aHex || !bHex) throw new Error(`Expected opaque colours: ${a}, ${b}`);
27
+ return contrastRatio(relativeLuminance(aHex), relativeLuminance(bHex));
28
+ }
29
+
30
+ describe("switch thumb", () => {
31
+ it.each(["light", "dark"])("%s keeps a white thumb through its Switch child", (scheme) => {
32
+ expect(themes[scheme].switchThumb).toBe("#ffffff");
33
+ expect(Object.hasOwn(themes[`${scheme}_Switch`], "switchThumb")).toBe(false);
34
+ expect(config.themes[`${scheme}_Switch`].switchThumb?.val).toBe("#ffffff");
35
+ });
36
+
37
+ it("resolves the web thumb shadow through the knob recipe", () => {
38
+ const { thumbShadow } = resolveKnobs(defaultKnobs).knobProps;
39
+ expect(thumbShadow.style?.boxShadow).toEqual(expect.any(String));
40
+ expect(thumbShadow.style?.boxShadow).not.toBe("");
41
+ });
42
+
43
+ it.each(tintedSchemes)(
44
+ "%s: the off track holds 3:1 under the thumb and stays apart from the on track",
45
+ (name) => {
46
+ const theme = config.themes[name] as Record<string, { val: string }>;
47
+ const scheme = name.split("_")[0] as "light" | "dark";
48
+ const off = theme.switchTrackOff.val;
49
+ expect(off).toBe(theme[scheme === "light" ? "gray10" : "gray7"].val);
50
+ expect(theme.switchTrackOffHover.val).toBe(theme[scheme === "light" ? "gray9" : "gray6"].val);
51
+ expect(theme.switchTrackOffPress.val).toBe(
52
+ theme[scheme === "light" ? "gray11" : "gray8"].val,
53
+ );
54
+ expect(ratio(off, theme.switchThumb.val)).toBeGreaterThanOrEqual(minContrastRatio);
55
+ expect(ratio(off, theme.accentBackground.val)).toBeGreaterThanOrEqual(1.5);
56
+ if (scheme === "light") {
57
+ for (const against of [theme.background.val, theme.color3.val]) {
58
+ expect(ratio(off, against)).toBeGreaterThanOrEqual(minContrastRatio);
59
+ }
60
+ }
61
+ },
62
+ );
63
+
64
+ it.each(["light", "dark"])("%s_Switch inherits its scheme's off track", (scheme) => {
65
+ expect(config.themes[`${scheme}_Switch`].switchTrackOff?.val).toBe(
66
+ config.themes[scheme].switchTrackOff?.val,
67
+ );
68
+ });
69
+ });