@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
@@ -0,0 +1,322 @@
1
+ /**
2
+ * The few numbers a token scale is generated from, and the canonical
3
+ * proportions every key is generated against.
4
+ *
5
+ * The shape tables are Tamagui v5's own ladders (`@tamagui/themes/v5`
6
+ * tokens, `@tamagui/config/v5` fonts) divided by their anchor: `$size` by 44,
7
+ * `$space` by 18, `$radius` by 9, type by the 15px web body. At
8
+ * `v5ScaleInputs` the generator reproduces v5 key for key
9
+ * (`generateScale.spec.ts`), and any other inputs keep v5's proportions,
10
+ * including its `$16`/`$17` duplicate. The house default is 40/15.
11
+ */
12
+
13
+ export type ScaleInputs = {
14
+ /** `$size.$4` and `$size.true` in px: the default control height. */
15
+ control: number;
16
+ /** Body `$true` in px on web. Every type ladder is proportional to it. */
17
+ type: number;
18
+ /** Fine-grain step in px: `$size.$0.25`/`$0.5`/`$0.75` are `unit/2`, `unit` and `2·unit`. */
19
+ unit?: number;
20
+ /** `$size` keys from `$1` up round to a multiple of this (2 = even px). */
21
+ snap?: number;
22
+ /** `$radius.true` as a fraction of `control`. */
23
+ radius?: number;
24
+ };
25
+
26
+ export type ResolvedScaleInputs = Required<ScaleInputs>;
27
+
28
+ export const defaultScaleInputs: ScaleInputs = { control: 40, type: 15 };
29
+
30
+ export const v5ScaleInputs: ScaleInputs = { control: 44, type: 15 };
31
+
32
+ export const scaleInputDefaults = { unit: 4, snap: 2, radius: 9 / 44 } as const;
33
+
34
+ export function resolveScaleInputs(inputs: Partial<ScaleInputs> = {}): ResolvedScaleInputs {
35
+ return {
36
+ control: inputs.control ?? defaultScaleInputs.control,
37
+ type: inputs.type ?? defaultScaleInputs.type,
38
+ unit: inputs.unit ?? scaleInputDefaults.unit,
39
+ snap: inputs.snap ?? scaleInputDefaults.snap,
40
+ radius: inputs.radius ?? scaleInputDefaults.radius,
41
+ };
42
+ }
43
+
44
+ export const SIZE_KEYS = [
45
+ "$0",
46
+ "$0.25",
47
+ "$0.5",
48
+ "$0.75",
49
+ "$1",
50
+ "$1.5",
51
+ "$2",
52
+ "$2.5",
53
+ "$3",
54
+ "$3.5",
55
+ "$4",
56
+ "$true",
57
+ "$4.5",
58
+ "$5",
59
+ "$6",
60
+ "$7",
61
+ "$8",
62
+ "$9",
63
+ "$10",
64
+ "$11",
65
+ "$12",
66
+ "$13",
67
+ "$14",
68
+ "$15",
69
+ "$16",
70
+ "$17",
71
+ "$18",
72
+ "$19",
73
+ "$20",
74
+ ] as const;
75
+
76
+ export type SizeKey = (typeof SIZE_KEYS)[number];
77
+
78
+ export type NegativeSpaceKey = `-${SizeKey extends `$${infer Key}` ? Key : never}`;
79
+
80
+ export type SpaceKey = SizeKey | Exclude<NegativeSpaceKey, "-0">;
81
+
82
+ export const RADIUS_KEYS = [
83
+ "0",
84
+ "1",
85
+ "2",
86
+ "3",
87
+ "4",
88
+ "true",
89
+ "5",
90
+ "6",
91
+ "7",
92
+ "8",
93
+ "9",
94
+ "10",
95
+ "11",
96
+ "12",
97
+ ] as const;
98
+
99
+ export type RadiusKey = (typeof RADIUS_KEYS)[number];
100
+
101
+ export const ZINDEX_KEYS = ["0", "1", "2", "3", "4", "5"] as const;
102
+
103
+ export type ZIndexKey = (typeof ZINDEX_KEYS)[number];
104
+
105
+ export const FONT_KEYS = [
106
+ "1",
107
+ "2",
108
+ "3",
109
+ "4",
110
+ "true",
111
+ "5",
112
+ "6",
113
+ "7",
114
+ "8",
115
+ "9",
116
+ "10",
117
+ "11",
118
+ "12",
119
+ "13",
120
+ "14",
121
+ "15",
122
+ "16",
123
+ ] as const;
124
+
125
+ export type FontKey = (typeof FONT_KEYS)[number];
126
+
127
+ export type SubunitKey = "$0.25" | "$0.5" | "$0.75";
128
+
129
+ /** `$size` sub-`$1` keys in multiples of `unit`. */
130
+ export const SUBUNIT_SHAPE: Record<SubunitKey, number> = {
131
+ "$0.25": 0.5,
132
+ "$0.5": 1,
133
+ "$0.75": 2,
134
+ };
135
+
136
+ /** v5 `$size` / 44. */
137
+ export const SIZE_SHAPE: Record<Exclude<SizeKey, SubunitKey>, number> = {
138
+ $0: 0,
139
+ $1: 20 / 44,
140
+ "$1.5": 24 / 44,
141
+ $2: 28 / 44,
142
+ "$2.5": 32 / 44,
143
+ $3: 36 / 44,
144
+ "$3.5": 40 / 44,
145
+ $4: 44 / 44,
146
+ $true: 44 / 44,
147
+ "$4.5": 48 / 44,
148
+ $5: 52 / 44,
149
+ $6: 64 / 44,
150
+ $7: 74 / 44,
151
+ $8: 84 / 44,
152
+ $9: 94 / 44,
153
+ $10: 104 / 44,
154
+ $11: 124 / 44,
155
+ $12: 144 / 44,
156
+ $13: 164 / 44,
157
+ $14: 184 / 44,
158
+ $15: 204 / 44,
159
+ $16: 224 / 44,
160
+ $17: 224 / 44,
161
+ $18: 244 / 44,
162
+ $19: 264 / 44,
163
+ $20: 284 / 44,
164
+ };
165
+
166
+ /** v5 `$space` / 18 (Tamagui's `sizeToSpace` of each v5 `$size`). */
167
+ export const SPACE_SHAPE: Record<SizeKey, number> = {
168
+ $0: 0,
169
+ "$0.25": 0.5 / 18,
170
+ "$0.5": 1 / 18,
171
+ "$0.75": 1.5 / 18,
172
+ $1: 2 / 18,
173
+ "$1.5": 4 / 18,
174
+ $2: 7 / 18,
175
+ "$2.5": 10 / 18,
176
+ $3: 13 / 18,
177
+ "$3.5": 16 / 18,
178
+ $4: 18 / 18,
179
+ $true: 18 / 18,
180
+ "$4.5": 21 / 18,
181
+ $5: 24 / 18,
182
+ $6: 32 / 18,
183
+ $7: 39 / 18,
184
+ $8: 46 / 18,
185
+ $9: 53 / 18,
186
+ $10: 60 / 18,
187
+ $11: 74 / 18,
188
+ $12: 88 / 18,
189
+ $13: 102 / 18,
190
+ $14: 116 / 18,
191
+ $15: 130 / 18,
192
+ $16: 144 / 18,
193
+ $17: 144 / 18,
194
+ $18: 158 / 18,
195
+ $19: 172 / 18,
196
+ $20: 186 / 18,
197
+ };
198
+
199
+ /** v5 `$radius` / 9. */
200
+ export const RADIUS_SHAPE: Record<RadiusKey, number> = {
201
+ 0: 0,
202
+ 1: 3 / 9,
203
+ 2: 5 / 9,
204
+ 3: 7 / 9,
205
+ 4: 9 / 9,
206
+ true: 9 / 9,
207
+ 5: 10 / 9,
208
+ 6: 16 / 9,
209
+ 7: 19 / 9,
210
+ 8: 22 / 9,
211
+ 9: 26 / 9,
212
+ 10: 34 / 9,
213
+ 11: 42 / 9,
214
+ 12: 50 / 9,
215
+ };
216
+
217
+ export const ZINDEX: Record<ZIndexKey, number> = {
218
+ 0: 0,
219
+ 1: 100,
220
+ 2: 200,
221
+ 3: 300,
222
+ 4: 400,
223
+ 5: 500,
224
+ };
225
+
226
+ /** v5 web font sizes / 15. Body and heading share the ladder. */
227
+ export const TYPE_SHAPE: Record<FontKey, number> = {
228
+ 1: 12 / 15,
229
+ 2: 13 / 15,
230
+ 3: 14 / 15,
231
+ 4: 15 / 15,
232
+ true: 15 / 15,
233
+ 5: 16 / 15,
234
+ 6: 18 / 15,
235
+ 7: 22 / 15,
236
+ 8: 26 / 15,
237
+ 9: 30 / 15,
238
+ 10: 40 / 15,
239
+ 11: 46 / 15,
240
+ 12: 52 / 15,
241
+ 13: 60 / 15,
242
+ 14: 70 / 15,
243
+ 15: 85 / 15,
244
+ 16: 100 / 15,
245
+ };
246
+
247
+ /** v5 native font sizes (iOS HIG, 17pt body) / the 15px web body, so one `type` drives both. */
248
+ export const NATIVE_TYPE_SHAPE: Record<FontKey, number> = {
249
+ 1: 11 / 15,
250
+ 2: 12 / 15,
251
+ 3: 15 / 15,
252
+ 4: 17 / 15,
253
+ true: 17 / 15,
254
+ 5: 20 / 15,
255
+ 6: 22 / 15,
256
+ 7: 24 / 15,
257
+ 8: 28 / 15,
258
+ 9: 32 / 15,
259
+ 10: 40 / 15,
260
+ 11: 46 / 15,
261
+ 12: 52 / 15,
262
+ 13: 60 / 15,
263
+ 14: 70 / 15,
264
+ 15: 85 / 15,
265
+ 16: 100 / 15,
266
+ };
267
+
268
+ export const SIZE_RECIPE_TOKENS = [
269
+ "$1",
270
+ "$1.5",
271
+ "$2",
272
+ "$2.5",
273
+ "$3",
274
+ "$3.5",
275
+ "$4",
276
+ "$true",
277
+ "$5",
278
+ "$6",
279
+ ] as const;
280
+
281
+ export type SizeRecipeToken = (typeof SIZE_RECIPE_TOKENS)[number];
282
+
283
+ export const SIZE_TOKEN_EXPONENTS: Record<SizeRecipeToken, number> = {
284
+ $1: -3,
285
+ "$1.5": -2.5,
286
+ $2: -2,
287
+ "$2.5": -1.5,
288
+ $3: -1,
289
+ "$3.5": -0.5,
290
+ $4: 0,
291
+ $true: 0,
292
+ $5: 1,
293
+ $6: 2,
294
+ };
295
+
296
+ type RecipeKey = Exclude<SizeRecipeToken, "$true">;
297
+
298
+ /** Control icon px / 15 per recipe key (rendered reference §6.1). */
299
+ export const CONTROL_ICON_SHAPE: Record<RecipeKey, number> = {
300
+ $1: 10 / 15,
301
+ "$1.5": 11 / 15,
302
+ $2: 12 / 15,
303
+ "$2.5": 14 / 15,
304
+ $3: 14 / 15,
305
+ "$3.5": 15 / 15,
306
+ $4: 16 / 15,
307
+ $5: 18 / 15,
308
+ $6: 20 / 15,
309
+ };
310
+
311
+ /** chip icon px / 15 per recipe key. */
312
+ export const CHIP_ICON_SHAPE: Record<RecipeKey, number> = {
313
+ $1: 9 / 15,
314
+ "$1.5": 10 / 15,
315
+ $2: 11 / 15,
316
+ "$2.5": 12 / 15,
317
+ $3: 13 / 15,
318
+ "$3.5": 14 / 15,
319
+ $4: 14 / 15,
320
+ $5: 16 / 15,
321
+ $6: 18 / 15,
322
+ };
@@ -0,0 +1,181 @@
1
+ import {
2
+ type CreateTamaguiProps,
3
+ type GenericFont,
4
+ type TamaguiInternalConfig,
5
+ getConfig,
6
+ setConfig,
7
+ } from "@tamagui/web";
8
+ import type { Preset } from "../preset.types";
9
+ import { defaultPreset } from "../presets";
10
+ import type { SizeRecipeInputs } from "../recipeInputs";
11
+ import { type ScaleFonts, type ScaleTokens, generateScale } from "./generateScale";
12
+ import { type ScaleCssSource, scaleCss, scaleRootSelectors } from "./scaleCss";
13
+ import { type ResolvedScaleInputs, defaultScaleInputs, resolveScaleInputs } from "./scaleInputs";
14
+
15
+ /** Scale id of the default inputs, and the `scales` key of the implicit default preset. */
16
+ export const DEFAULT_SCALE_ID = "default";
17
+
18
+ /**
19
+ * One token scale a ThemeConfig can switch to at runtime. Presets with equal
20
+ * inputs share one entry (and one `tamagui()` config).
21
+ */
22
+ export type ThemeScale = {
23
+ /** `"default"` for the default inputs, else the first preset name that carries these inputs. */
24
+ id: string;
25
+ /** Every preset name that resolves to this scale. */
26
+ presets: string[];
27
+ inputs: ResolvedScaleInputs;
28
+ tokens: ScaleTokens;
29
+ fonts: ScaleFonts;
30
+ /** Size-recipe inputs read off this scale; `setSizeRecipeInputs` takes them as is. */
31
+ recipeInputs: SizeRecipeInputs;
32
+ /** Web repaint block (`:root[data-mp-scale=…]`); empty for the default scale. */
33
+ css: string;
34
+ /** The built Tamagui config with this scale's tokens and fonts, built on first call. */
35
+ tamagui: () => TamaguiInternalConfig;
36
+ };
37
+
38
+ export type ThemeScaleBuildOptions = {
39
+ /** The base config, already built on the default scale. */
40
+ base: TamaguiInternalConfig;
41
+ /** The exact props `base` was built from; a scale config swaps only `tokens` and `fonts`. */
42
+ props: CreateTamaguiProps;
43
+ presets?: Record<string, Preset>;
44
+ defaultPreset?: string;
45
+ /** Size-recipe inputs live for the default scale. */
46
+ recipeInputs: SizeRecipeInputs;
47
+ /** Consumer recipe overrides that are not scale-derived (tile ladder, touch floor). */
48
+ recipeOverrides?: Partial<SizeRecipeInputs>;
49
+ /** The font map for a scale's type tables (default: rescale `props.fonts`). */
50
+ fonts: (tables: ScaleFonts) => Record<string, GenericFont>;
51
+ createConfig: (props: CreateTamaguiProps) => TamaguiInternalConfig;
52
+ };
53
+
54
+ function inputsKey(inputs: ResolvedScaleInputs): string {
55
+ return [inputs.control, inputs.type, inputs.unit, inputs.snap, inputs.radius].join("|");
56
+ }
57
+
58
+ type TokensGlobal = { __tamaguiTokensMerged?: unknown };
59
+
60
+ /** Build a config without leaving it installed: `createTamagui` makes every config it builds the global one. */
61
+ function buildDetached(build: () => TamaguiInternalConfig): TamaguiInternalConfig {
62
+ let previous: TamaguiInternalConfig | undefined;
63
+ try {
64
+ previous = getConfig();
65
+ } catch {
66
+ previous = undefined;
67
+ }
68
+ const previousTokens = (globalThis as TokensGlobal).__tamaguiTokensMerged;
69
+ try {
70
+ return build();
71
+ } finally {
72
+ if (previous) {
73
+ setConfig(previous);
74
+ (globalThis as TokensGlobal).__tamaguiTokensMerged = previousTokens;
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * A scale's config, built on first call. It takes everything as parameters:
81
+ * Hermes shares a loop body's let/const bindings between the closures made in
82
+ * it, so a closure built inside the scale loop would read the last scale's
83
+ * tokens and every scale would share one config.
84
+ */
85
+ function lazyScaleConfig(
86
+ createConfig: ThemeScaleBuildOptions["createConfig"],
87
+ props: CreateTamaguiProps,
88
+ tokens: ScaleTokens,
89
+ fonts: Record<string, GenericFont>,
90
+ ): () => TamaguiInternalConfig {
91
+ let built: TamaguiInternalConfig | undefined;
92
+ return () => {
93
+ built ??= buildDetached(() => createConfig({ ...props, tokens, fonts }));
94
+ return built;
95
+ };
96
+ }
97
+
98
+ /**
99
+ * The scales a ThemeConfig can switch between, keyed by preset name (plus
100
+ * `"default"` for the implicit default preset). The default scale reuses
101
+ * `base`; every other scale generates its tokens, fonts and recipe inputs
102
+ * from its preset's `scale` inputs and builds its Tamagui config lazily
103
+ * from the same props, so its color themes are the base's themes object.
104
+ */
105
+ export function buildThemeScales(options: ThemeScaleBuildOptions): Record<string, ThemeScale> {
106
+ const { base, props, presets, recipeInputs, recipeOverrides } = options;
107
+ const defaultInputs = resolveScaleInputs(defaultScaleInputs);
108
+ const defaultKey = inputsKey(defaultInputs);
109
+ const defaultGenerated = generateScale(defaultInputs);
110
+
111
+ const entries: Array<[string, Preset]> = [
112
+ [
113
+ DEFAULT_SCALE_ID,
114
+ (options.defaultPreset ? presets?.[options.defaultPreset] : undefined) ?? defaultPreset,
115
+ ],
116
+ ...Object.entries(presets ?? {}),
117
+ ];
118
+
119
+ const byKey = new Map<string, ThemeScale>();
120
+ const byName: Record<string, ThemeScale> = {};
121
+ const fontMaps = new Map<ThemeScale, Record<string, GenericFont>>();
122
+
123
+ for (const [name, preset] of entries) {
124
+ const inputs = resolveScaleInputs(preset.scale ?? defaultScaleInputs);
125
+ const key = inputsKey(inputs);
126
+ let scale = byKey.get(key);
127
+ if (!scale) {
128
+ if (key === defaultKey) {
129
+ scale = {
130
+ id: DEFAULT_SCALE_ID,
131
+ presets: [],
132
+ inputs: defaultInputs,
133
+ tokens: defaultGenerated.tokens,
134
+ fonts: defaultGenerated.fonts,
135
+ recipeInputs,
136
+ css: "",
137
+ tamagui: () => base,
138
+ };
139
+ } else {
140
+ const generated = generateScale(inputs);
141
+ const fontMap = options.fonts(generated.fonts);
142
+ scale = {
143
+ id: name,
144
+ presets: [],
145
+ inputs,
146
+ tokens: generated.tokens,
147
+ fonts: generated.fonts,
148
+ recipeInputs: {
149
+ ...generated.recipeInputs,
150
+ ...(recipeOverrides?.tile !== undefined && { tile: recipeOverrides.tile }),
151
+ ...(recipeOverrides?.touchFloor !== undefined && {
152
+ touchFloor: recipeOverrides.touchFloor,
153
+ }),
154
+ },
155
+ css: "",
156
+ tamagui: lazyScaleConfig(options.createConfig, props, generated.tokens, fontMap),
157
+ };
158
+ fontMaps.set(scale, fontMap);
159
+ }
160
+ byKey.set(key, scale);
161
+ }
162
+ if (!scale.presets.includes(name)) scale.presets.push(name);
163
+ byName[name] = scale;
164
+ }
165
+
166
+ const cssSource = base as unknown as ScaleCssSource;
167
+ for (const [scale, fontMap] of fontMaps) {
168
+ const roots = scaleRootSelectors(scale.presets, scale.presets.includes(DEFAULT_SCALE_ID));
169
+ scale.css = scaleCss(roots, cssSource, scale.tokens, fontMap as Parameters<typeof scaleCss>[3]);
170
+ }
171
+
172
+ return byName;
173
+ }
174
+
175
+ /** Every distinct scale once, default first. */
176
+ export function uniqueScales(scales: Record<string, ThemeScale> | undefined): ThemeScale[] {
177
+ if (!scales) return [];
178
+ return [...new Set(Object.values(scales))].sort((a, b) =>
179
+ a.id === DEFAULT_SCALE_ID ? -1 : b.id === DEFAULT_SCALE_ID ? 1 : 0,
180
+ );
181
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Switching the active preset installs its scale for every JS reader: the
3
+ * global Tamagui config's tokens, the live size-recipe tables, and through
4
+ * them `knobProps.control` from useResolvedKnobs.
5
+ */
6
+
7
+ import { act, cleanup, render } from "@testing-library/react";
8
+ import { getConfig, getTokenValue } from "@tamagui/web";
9
+ import { useState } from "react";
10
+ import { TamaguiProvider } from "tamagui";
11
+ import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
12
+
13
+ vi.mock("../animations/index", () => ({ animations: {} }));
14
+
15
+ import { createDefaultThemeConfig } from "../createDefaultThemeConfig";
16
+ import { PresetContext } from "../PresetContext";
17
+ import { defaultPreset, densePreset, heroPreset, roomyPreset } from "../presets";
18
+ import { clearPresets } from "../shared";
19
+ import { sizeRecipeForToken } from "../sizeRecipes";
20
+ import { useResolvedKnobs } from "../useResolvedKnobs";
21
+ import { DEFAULT_SCALE_ID } from "./themeScales";
22
+ import { applyThemeScale, useActiveScale, type PublishedScale } from "./useActiveScale";
23
+
24
+ const themeConfig = createDefaultThemeConfig({
25
+ presets: { hero: heroPreset, dense: densePreset, roomy: roomyPreset },
26
+ });
27
+
28
+ afterEach(() => {
29
+ cleanup();
30
+ const defaults = themeConfig.scales?.[DEFAULT_SCALE_ID];
31
+ if (defaults) applyThemeScale(defaults);
32
+ });
33
+ afterAll(() => clearPresets());
34
+
35
+ function size4Token(): number {
36
+ return getTokenValue("$4" as Parameters<typeof getTokenValue>[0], "size") as number;
37
+ }
38
+
39
+ function size4(): number {
40
+ return (getConfig().tokensParsed.size as Record<string, { val: number }>).$4.val;
41
+ }
42
+
43
+ const seen: { scaleId?: string; control?: number } = {};
44
+
45
+ function ControlReader() {
46
+ const { knobProps } = useResolvedKnobs();
47
+ seen.control = knobProps.control.height as number;
48
+ return null;
49
+ }
50
+
51
+ let switchPreset: (name: string) => void = () => undefined;
52
+
53
+ function Harness({ initial }: { initial: string }) {
54
+ const [presetName, setPresetName] = useState(initial);
55
+ switchPreset = setPresetName;
56
+ const scaleId = useActiveScale(themeConfig, presetName);
57
+ seen.scaleId = scaleId;
58
+ return (
59
+ <TamaguiProvider config={themeConfig.tamagui} defaultTheme="light" disableInjectCSS>
60
+ <PresetContext.Provider value={{ preset: defaultPreset, presetName, scaleId }}>
61
+ <ControlReader />
62
+ </PresetContext.Provider>
63
+ </TamaguiProvider>
64
+ );
65
+ }
66
+
67
+ describe("useActiveScale", () => {
68
+ it("installs the initial preset's scale before the first child renders", () => {
69
+ render(<Harness initial="dense" />);
70
+ expect(seen.scaleId).toBe("dense");
71
+ expect(seen.control).toBe(36);
72
+ expect(size4()).toBe(36);
73
+ });
74
+
75
+ it("switching flips the global $size tokens, the recipe tables and knobProps.control", () => {
76
+ render(<Harness initial="" />);
77
+ expect(seen.scaleId).toBe(DEFAULT_SCALE_ID);
78
+ expect(size4()).toBe(40);
79
+ expect(size4Token()).toBe(40);
80
+ expect(sizeRecipeForToken("$4").height).toBe(40);
81
+ expect(seen.control).toBe(40);
82
+
83
+ act(() => switchPreset("dense"));
84
+ expect(seen.scaleId).toBe("dense");
85
+ expect(size4()).toBe(36);
86
+ expect(size4Token()).toBe(36);
87
+ expect(sizeRecipeForToken("$4").height).toBe(36);
88
+ expect(seen.control).toBe(36);
89
+
90
+ act(() => switchPreset("roomy"));
91
+ expect(size4()).toBe(48);
92
+ expect(sizeRecipeForToken("$4").height).toBe(48);
93
+ expect(seen.control).toBe(48);
94
+
95
+ act(() => switchPreset("hero"));
96
+ expect(seen.scaleId).toBe(DEFAULT_SCALE_ID);
97
+ expect(getConfig()).toBe(themeConfig.tamagui);
98
+ expect(seen.control).toBe(40);
99
+ });
100
+
101
+ it("publishes the active scale for in-page audits", () => {
102
+ render(<Harness initial="roomy" />);
103
+ const published = (globalThis as { __MPO_SCALE__?: PublishedScale }).__MPO_SCALE__;
104
+ expect(published?.id).toBe("roomy");
105
+ expect(published?.size.$4).toBe(48);
106
+ expect(published?.type).toBe(16);
107
+ const recipes = themeConfig.scales!.roomy.recipeInputs;
108
+ expect(published?.controlGaps).toEqual(
109
+ [
110
+ ...new Set(
111
+ [...Object.values(recipes.tokenTable), ...Object.values(recipes.chipTokenTable)].map(
112
+ (row) => row.radius,
113
+ ),
114
+ ),
115
+ ].sort((a, b) => a - b),
116
+ );
117
+ });
118
+
119
+ it("builds a scale's Tamagui config only when it is first installed", () => {
120
+ const fresh = createDefaultThemeConfig({ presets: { dense: densePreset } });
121
+ const dense = fresh.scales?.dense;
122
+ if (!dense) throw new Error("dense scale missing");
123
+ const before = getConfig();
124
+ const built = dense.tamagui();
125
+ expect(getConfig()).toBe(before);
126
+ expect(dense.tamagui()).toBe(built);
127
+ expect((built.tokensParsed.size as Record<string, { val: number }>).$4.val).toBe(36);
128
+ });
129
+ });