@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,84 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { animationConfig, animationDurations, motionStops } from "./animations/css";
3
+ import {
4
+ motionFor,
5
+ motionDurationMs,
6
+ motionSeconds,
7
+ motionSettleMs,
8
+ motionHideDelayMs,
9
+ motionRoleProperties,
10
+ } from "./motion";
11
+
12
+ describe("motion roles", () => {
13
+ it("assigns quick's control, overlay, scrim, reveal and slide stops", () => {
14
+ expect(
15
+ Object.fromEntries(
16
+ Object.entries(motionFor("quick")).map(([role, value]) => [role, value.transition]),
17
+ ),
18
+ ).toEqual({
19
+ control: "quick",
20
+ overlay: "quicker",
21
+ scrim: "slow",
22
+ reveal: "medium",
23
+ slide: "medium",
24
+ });
25
+ });
26
+ it("silences every role at none while retaining property scopes", () => {
27
+ for (const [name, role] of Object.entries(motionFor("none"))) {
28
+ expect(role.transition).toBeUndefined();
29
+ expect(role.animateOnly).toEqual(
30
+ motionRoleProperties[name as keyof typeof motionRoleProperties],
31
+ );
32
+ }
33
+ });
34
+ it("selects registered non-bouncy role stops by nearest duration, ties faster", () => {
35
+ const candidates = motionStops.filter((stop) => stop !== "bouncy" && stop !== "superBouncy");
36
+ for (const stop of motionStops) {
37
+ const roles = motionFor(stop);
38
+ expect(roles.control.transition).toBe(stop);
39
+ for (const [name, factor] of [
40
+ ["overlay", 0.8],
41
+ ["scrim", 3],
42
+ ["reveal", 2],
43
+ ["slide", 2],
44
+ ] as const) {
45
+ const closest = [...candidates].sort(
46
+ (a, b) =>
47
+ Math.abs(animationDurations[a] - animationDurations[stop] * factor) -
48
+ Math.abs(animationDurations[b] - animationDurations[stop] * factor) ||
49
+ animationDurations[a] - animationDurations[b],
50
+ )[0];
51
+ expect(roles[name].transition).toBe(closest);
52
+ expect(roles[name].transition).toBeOneOf(Object.keys(animationConfig));
53
+ }
54
+ }
55
+ expect(motionFor("quickestLessBouncy").reveal.transition).toBe("quicker");
56
+ });
57
+ it("limits properties to each role's anatomy", () => {
58
+ expect(motionRoleProperties).toEqual({
59
+ control: ["backgroundColor", "borderColor", "color", "opacity", "transform"],
60
+ overlay: ["opacity", "transform"],
61
+ scrim: ["opacity"],
62
+ reveal: ["height", "opacity", "transform"],
63
+ slide: ["opacity", "transform"],
64
+ });
65
+ });
66
+ });
67
+
68
+ describe("motion durations", () => {
69
+ it("derives duration, seconds, settle and hide delays from every table entry", () => {
70
+ for (const token of Object.keys(animationConfig) as (keyof typeof animationConfig)[]) {
71
+ expect(motionDurationMs(token)).toBe(animationDurations[token]);
72
+ expect(motionSeconds(token)).toBe(animationDurations[token] / 1000);
73
+ expect(motionSettleMs(token)).toBe(Math.max(300, 3 * animationDurations[token]) + 80);
74
+ expect(motionHideDelayMs(token)).toBe(animationDurations[token] + 50);
75
+ }
76
+ });
77
+ it("uses zero at none and 300ms for structured transitions", () => {
78
+ expect(motionDurationMs(undefined)).toBe(0);
79
+ expect(motionSeconds(undefined)).toBe(0);
80
+ expect(motionSettleMs(undefined)).toBe(0);
81
+ expect(motionDurationMs(["quick", { opacity: "slow" }])).toBe(300);
82
+ expect(motionSettleMs(["quick", { opacity: "slow" }])).toBe(980);
83
+ });
84
+ });
@@ -0,0 +1,53 @@
1
+ import type { TransitionProp } from "tamagui";
2
+ import type { Animation } from "./knobs";
3
+ import { motionStops, type MotionStop } from "./motionStops";
4
+ import { animationDurations, motionDurationMs } from "./motionTiming";
5
+
6
+ export { motionDurationMs, motionHideDelayMs, motionSeconds, motionSettleMs } from "./motionTiming";
7
+
8
+ export type MotionRoleName = "control" | "overlay" | "scrim" | "reveal" | "slide";
9
+
10
+ export interface MotionRole {
11
+ transition: TransitionProp | undefined;
12
+ animateOnly: readonly string[];
13
+ }
14
+
15
+ export const motionRoleProperties = {
16
+ control: ["backgroundColor", "borderColor", "color", "opacity", "transform"],
17
+ overlay: ["opacity", "transform"],
18
+ scrim: ["opacity"],
19
+ reveal: ["height", "opacity", "transform"],
20
+ slide: ["opacity", "transform"],
21
+ } as const satisfies Record<MotionRoleName, readonly string[]>;
22
+
23
+ const nonBouncyStops = motionStops.filter((stop) => stop !== "superBouncy" && stop !== "bouncy");
24
+
25
+ function nearestStop(duration: number): MotionStop {
26
+ return nonBouncyStops.reduce((nearest, candidate) =>
27
+ Math.abs(animationDurations[candidate] - duration) <
28
+ Math.abs(animationDurations[nearest] - duration)
29
+ ? candidate
30
+ : nearest,
31
+ );
32
+ }
33
+
34
+ export function motionFor(stop: Animation): Record<MotionRoleName, MotionRole> {
35
+ const transition = stop === "none" ? undefined : stop;
36
+ const duration = motionDurationMs(transition);
37
+ const role = (name: MotionRoleName, factor: number): MotionRole => ({
38
+ transition:
39
+ transition === undefined
40
+ ? undefined
41
+ : name === "control"
42
+ ? transition
43
+ : nearestStop(duration * factor),
44
+ animateOnly: motionRoleProperties[name],
45
+ });
46
+ return {
47
+ control: role("control", 1),
48
+ overlay: role("overlay", 0.8),
49
+ scrim: role("scrim", 3),
50
+ reveal: role("reveal", 2),
51
+ slide: role("slide", 2),
52
+ };
53
+ }
@@ -0,0 +1,17 @@
1
+ export const motionStops = [
2
+ "quickestLessBouncy",
3
+ "quickest",
4
+ "quickerLessBouncy",
5
+ "quicker",
6
+ "quick",
7
+ "quickLessBouncy",
8
+ "medium",
9
+ "superBouncy",
10
+ "bouncy",
11
+ "slow",
12
+ "lazy",
13
+ "superLazy",
14
+ "slowest",
15
+ ] as const;
16
+
17
+ export type MotionStop = (typeof motionStops)[number];
@@ -0,0 +1,59 @@
1
+ import type { TransitionProp } from "tamagui";
2
+
3
+ export const animationConfig = {
4
+ "0ms": "0ms linear",
5
+ "50ms": "50ms linear",
6
+ "75ms": "75ms linear",
7
+ "100ms": "100ms ease-out",
8
+ "200ms": "200ms ease-out",
9
+ "250ms": "250ms ease-out",
10
+ "300ms": "300ms ease-out",
11
+ "400ms": "400ms ease-out",
12
+ "500ms": "500ms ease-out",
13
+ superBouncy: "300ms cubic-bezier(0.175, 0.885, 0.32, 1.5)",
14
+ bouncy: "350ms cubic-bezier(0.175, 0.885, 0.32, 1.275)",
15
+ superLazy: "600ms cubic-bezier(0.25, 0.1, 0.25, 1)",
16
+ lazy: "500ms cubic-bezier(0.25, 0.1, 0.25, 1)",
17
+ medium: "300ms cubic-bezier(0.25, 0.1, 0.25, 1)",
18
+ slowest: "800ms cubic-bezier(0.25, 0.1, 0.25, 1)",
19
+ slow: "450ms cubic-bezier(0.25, 0.1, 0.25, 1)",
20
+ quick: "150ms cubic-bezier(0.25, 0.1, 0.25, 1)",
21
+ quickLessBouncy: "180ms cubic-bezier(0.25, 0.1, 0.25, 1)",
22
+ quicker: "120ms cubic-bezier(0.25, 0.1, 0.25, 1)",
23
+ quickerLessBouncy: "100ms cubic-bezier(0.25, 0.1, 0.25, 1)",
24
+ quickest: "80ms cubic-bezier(0.25, 0.1, 0.25, 1)",
25
+ quickestLessBouncy: "60ms cubic-bezier(0.25, 0.1, 0.25, 1)",
26
+ tooltip: "450ms cubic-bezier(0.25, 0.1, 0.25, 1)",
27
+ snappy: "80ms cubic-bezier(0.25, 0.1, 0.25, 1)",
28
+ gentle: "450ms cubic-bezier(0.25, 0.1, 0.25, 1)",
29
+ } as const;
30
+ export type AnimationName = keyof typeof animationConfig;
31
+ export const animationNames = Object.keys(animationConfig) as AnimationName[];
32
+
33
+ function parseDurationMs(timing: string): number {
34
+ const match = timing.match(/(\d+)\s*ms/);
35
+ return match ? Number.parseInt(match[1], 10) : 200;
36
+ }
37
+
38
+ export const animationDurations: Record<AnimationName, number> = Object.fromEntries(
39
+ Object.entries(animationConfig).map(([k, v]) => [k, parseDurationMs(v)]),
40
+ ) as Record<AnimationName, number>;
41
+
42
+ export function motionDurationMs(transition: TransitionProp | undefined): number {
43
+ if (transition === undefined) return 0;
44
+ return typeof transition === "string" && Object.hasOwn(animationDurations, transition)
45
+ ? animationDurations[transition as AnimationName]
46
+ : 300;
47
+ }
48
+
49
+ export function motionSeconds(transition: TransitionProp | undefined): number {
50
+ return motionDurationMs(transition) / 1000;
51
+ }
52
+
53
+ export function motionSettleMs(transition: TransitionProp | undefined): number {
54
+ return transition === undefined ? 0 : Math.max(300, 3 * motionDurationMs(transition)) + 80;
55
+ }
56
+
57
+ export function motionHideDelayMs(transition: TransitionProp | undefined): number {
58
+ return motionDurationMs(transition) + 50;
59
+ }
@@ -14,53 +14,48 @@
14
14
 
15
15
  import { describe, expect, it } from "vitest";
16
16
  import { layoutBreakpoints, OVERLAY_BREAKPOINT } from "./layoutTokens";
17
+ import { defaultScaleInputs, v5ScaleInputs } from "./scale/scaleInputs";
18
+ import { generateScale } from "./scale/generateScale";
17
19
  import { DEFAULT_SIZE_RECIPE_INPUTS, generateSizeRecipes, sizeRecipeForToken } from "./sizeRecipes";
18
20
 
19
- /** Stock Tamagui token scales, read from the live CSS variables (§4). */
20
- const tamaguiSize = {
21
- $1: 20,
22
- "$1.5": 24,
23
- $2: 28,
24
- "$2.5": 32,
25
- $3: 36,
26
- "$3.5": 40,
27
- $4: 44,
28
- $true: 44,
29
- $5: 52,
30
- $6: 64,
31
- } as const;
32
-
33
- const tamaguiRadiusTrue = 9;
34
- const tamaguiSpaceTrue = 18;
35
-
36
- describe("MPO-13 C1/C2 — $4 means ONE height, and it is 44", () => {
37
- it("every recipe height equals the raw Tamagui $size token at the same key", () => {
21
+ const houseScale = generateScale(defaultScaleInputs);
22
+
23
+ describe("MPO-13 C1/C2 — $4 means ONE height, and it is 40", () => {
24
+ it("every recipe height equals the generated $size token at the same key", () => {
38
25
  const { heights } = generateSizeRecipes(DEFAULT_SIZE_RECIPE_INPUTS);
39
- for (const [token, px] of Object.entries(tamaguiSize)) {
26
+ for (const token of Object.keys(heights)) {
27
+ const px = houseScale.tokens.size[token as keyof typeof houseScale.tokens.size];
40
28
  expect(heights[token], `heights.${token}`).toBe(px);
41
29
  }
42
30
  });
43
31
 
44
- it("baseHeight is $size.true, so recipe and token cannot re-diverge", () => {
45
- expect(DEFAULT_SIZE_RECIPE_INPUTS.baseHeight).toBe(tamaguiSize.$true);
46
- expect(sizeRecipeForToken("$4").height).toBe(tamaguiSize.$4);
32
+ it("control is $size.true, so recipe and token cannot re-diverge", () => {
33
+ expect(DEFAULT_SIZE_RECIPE_INPUTS.control).toBe(houseScale.tokens.size.$true);
34
+ expect(sizeRecipeForToken("$4").height).toBe(houseScale.tokens.size.$4);
47
35
  expect(sizeRecipeForToken("$true").height).toBe(sizeRecipeForToken("$4").height);
48
36
  });
49
37
 
50
- it("the $4 control is the measured 44 / 18 / 14 / 16 / 9", () => {
38
+ it("the $4 control is 40 / 16 / 15 / 16 / 8", () => {
51
39
  expect(sizeRecipeForToken("$4")).toEqual({
52
- height: 44,
53
- paddingHorizontal: 18,
54
- fontSize: 14,
40
+ height: 40,
41
+ paddingHorizontal: 16,
42
+ fontSize: 15,
55
43
  iconSize: 16,
56
- gap: 9,
44
+ gap: 8,
57
45
  });
58
46
  });
59
47
  });
60
48
 
61
- describe("MPO-13 C3 — radius is $radius.true = 9", () => {
49
+ it("v5 inputs retain the original size, space and radius anchors", () => {
50
+ const { tokens } = generateScale(v5ScaleInputs);
51
+ expect(tokens.size.$4).toBe(44);
52
+ expect(tokens.space.$4).toBe(18);
53
+ expect(tokens.radius.true).toBe(9);
54
+ });
55
+
56
+ describe("MPO-13 C3 — radius is $radius.true = 8", () => {
62
57
  it("radiusPx is the stored input, at the measured value", () => {
63
- expect(DEFAULT_SIZE_RECIPE_INPUTS.radiusPx).toBe(tamaguiRadiusTrue);
58
+ expect(DEFAULT_SIZE_RECIPE_INPUTS.radiusPx).toBe(houseScale.tokens.radius.true);
64
59
  });
65
60
 
66
61
  it("gap tracks radius exactly at $4 (the reference's stated rule)", () => {
@@ -78,28 +73,30 @@ describe("MPO-13 C3 — radius is $radius.true = 9", () => {
78
73
  describe("MPO-13 — the ratios are relationships, not literals", () => {
79
74
  it("padding at $4 is $space.true, ~40% of the size step (reference §4)", () => {
80
75
  const recipe = sizeRecipeForToken("$4");
81
- expect(recipe.paddingHorizontal).toBe(tamaguiSpaceTrue);
76
+ expect(recipe.paddingHorizontal).toBe(houseScale.tokens.space.$true);
82
77
  const spaceToSize = recipe.paddingHorizontal / recipe.height;
83
78
  expect(spaceToSize).toBeGreaterThan(0.38);
84
79
  expect(spaceToSize).toBeLessThan(0.42);
85
80
  });
86
81
 
87
- it("compact is one space step below control at the SAME height (LC-76)", () => {
82
+ it("the chip family is one space step below control at the SAME height (LC-79)", () => {
88
83
  const control = sizeRecipeForToken("$4");
89
- const compact = sizeRecipeForToken("$4", { family: "controlCompact" });
90
- expect(compact.height).toBe(control.height);
91
- expect(compact.paddingHorizontal).toBeLessThan(control.paddingHorizontal);
92
- // $space.3.5 = 16 vs $space.true = 18 — Input is one space half-step below Button.
93
- expect(compact.paddingHorizontal).toBe(16);
84
+ const chip = sizeRecipeForToken("$4", { family: "chip" });
85
+ expect(chip.height).toBe(control.height);
86
+ expect(chip.paddingHorizontal).toBeLessThan(control.paddingHorizontal);
87
+ expect(chip.paddingHorizontal).toBe(houseScale.tokens.space["$3.5"]);
94
88
  });
95
89
 
96
- it("re-pointing baseHeight re-derives every channel — no hardcoded 44", () => {
97
- const scaled = generateSizeRecipes({ baseHeight: 88 });
90
+ it("re-pointing control re-derives every channel from the scale — no hardcoded 44", () => {
91
+ const scale = generateScale({ control: 80, type: 30 });
92
+ const scaled = generateSizeRecipes({ control: 80, type: 30 });
98
93
  const base = sizeRecipeForToken("$4");
99
94
  expect(scaled.recipes.$4.height).toBe(base.height * 2);
100
- expect(scaled.recipes.$4.paddingHorizontal).toBe(base.paddingHorizontal * 2);
95
+ expect(scaled.recipes.$4.height).toBe(scale.tokens.size.$4);
96
+ // $space follows Tamagui's own sizeToSpace, which is not linear in size.
97
+ expect(scaled.recipes.$4.paddingHorizontal).toBe(scale.tokens.space.$4);
101
98
  expect(scaled.recipes.$4.fontSize).toBe(base.fontSize * 2);
102
- expect(scaled.recipes.$4.gap).toBe(base.gap * 2);
99
+ expect(scaled.recipes.$4.gap).toBe(scale.tokens.radius[4]);
103
100
  });
104
101
  });
105
102
 
@@ -131,18 +128,18 @@ describe("MPO-13 C4/C5 — one breakpoint system", () => {
131
128
  });
132
129
 
133
130
  describe("MPO-13 — the touch floor stops fighting the desktop ramp", () => {
134
- it("$4 meets the 44 floor natively; touch lifts it, never shrinks it", () => {
131
+ it("$4 is below the touch floor; touch lifts it, never shrinks it", () => {
135
132
  const desktop = sizeRecipeForToken("$4");
136
133
  const touch = sizeRecipeForToken("$4", { touch: true });
137
- expect(desktop.height).toBe(DEFAULT_SIZE_RECIPE_INPUTS.touchFloor);
138
- expect(touch.height).toBeGreaterThanOrEqual(desktop.height);
134
+ expect(desktop.height).toBeLessThan(DEFAULT_SIZE_RECIPE_INPUTS.touchFloor);
135
+ expect(touch).toEqual({ ...desktop, height: Math.max(desktop.height, 44) });
139
136
  });
140
137
 
141
138
  it("every touch height clears the floor", () => {
142
139
  const { heightsTouch } = generateSizeRecipes(DEFAULT_SIZE_RECIPE_INPUTS);
143
140
  for (const [token, px] of Object.entries(heightsTouch)) {
144
- expect(px, `heightsTouch.${token}`).toBeGreaterThanOrEqual(
145
- DEFAULT_SIZE_RECIPE_INPUTS.touchFloor,
141
+ expect(px, `heightsTouch.${token}`).toBe(
142
+ Math.max(sizeRecipeForToken(token).height, DEFAULT_SIZE_RECIPE_INPUTS.touchFloor),
146
143
  );
147
144
  }
148
145
  });
@@ -21,6 +21,7 @@
21
21
  * implement until that is settled.
22
22
  */
23
23
 
24
+ import { animationsCSS } from "@tamagui/config/v5-css";
24
25
  import { defaultConfig } from "@tamagui/config/v5";
25
26
  import { describe, expect, it } from "vitest";
26
27
  import { animationConfig } from "./animations/css";
@@ -71,7 +72,7 @@ describe("MPO-19 X13 — one Tamagui era, and it is load-bearing", () => {
71
72
  });
72
73
 
73
74
  describe("MPO-19 X8/X9 — the type scale IS the era's, not a copy of it", () => {
74
- it("body and heading tables are read from defaultConfig, not transcribed", () => {
75
+ it("body and heading tables follow v5 with heading true matched to size four", () => {
75
76
  // The relationship, not the literals: re-point the era and the whole
76
77
  // scale follows. A transcribed table would pass a literal snapshot and
77
78
  // fail this.
@@ -81,7 +82,10 @@ describe("MPO-19 X8/X9 — the type scale IS the era's, not a copy of it", () =>
81
82
  ] as const) {
82
83
  expect(table(ours.size), `${name}.size`).toEqual(table(theirs.size));
83
84
  expect(table(ours.lineHeight), `${name}.lineHeight`).toEqual(table(theirs.lineHeight));
84
- expect(table(ours.weight), `${name}.weight`).toEqual(table(theirs.weight));
85
+ const weight = table(theirs.weight);
86
+ expect(table(ours.weight), `${name}.weight`).toEqual(
87
+ name === "heading" ? { ...weight, true: weight["4"] } : weight,
88
+ );
85
89
  }
86
90
  });
87
91
 
@@ -110,7 +114,7 @@ describe("MPO-19 X8/X9 — the type scale IS the era's, not a copy of it", () =>
110
114
  expect(weight["1"]).toBe("600");
111
115
  expect(weight["6"]).toBe("700");
112
116
  expect(weight["9"]).toBe("800");
113
- // H1 is $10: compact and heavy, where mpo used to draw 64px at 400.
117
+ // H1 is $10 at 40px/800 on the default scale.
114
118
  expect(table(defaultHeadingFont.size)["10"]).toBe(40);
115
119
  expect(weight["10"]).toBe("800");
116
120
  });
@@ -172,7 +176,7 @@ describe("MPO-19 X4 — the interaction ramp is the template's again", () => {
172
176
  // M5's focus ring is an improvement (stock is rgba(ink, 0.2), measured
173
177
  // 1.36:1), and the semantic aliases contradict nothing.
174
178
  expect(derived).toHaveProperty("outlineColor");
175
- expect(derived).toHaveProperty("placeholderColor");
179
+ expect(derived).not.toHaveProperty("placeholderColor");
176
180
  for (const alias of ["textMuted", "textSubtle", "borderStrong", "surfaceStrong"]) {
177
181
  expect(derived, `alias ${alias}`).toHaveProperty(alias);
178
182
  }
@@ -191,9 +195,6 @@ describe("MPO-19 X5/X6 — surfaces address semantic tokens, so sub-themes work"
191
195
  });
192
196
 
193
197
  it("a raw ramp step is inert inside surface1/surface2, which is the whole point", () => {
194
- // Structural, not chromatic: `$background` resolves to `$color1` under the
195
- // default template so the rendered colour did not change. What changed is
196
- // that `<Theme name="surface1">` can now move it.
197
198
  const painted = Object.values(knobProps())
198
199
  .filter((v): v is Record<string, unknown> => !!v && typeof v === "object")
199
200
  .map((fragment) => fragment.backgroundColor)
@@ -225,15 +226,13 @@ describe("MPO-19 X7 — the focus ring is inset only where a clip demands it", (
225
226
  });
226
227
 
227
228
  describe("MPO-19 X10 — a shared animation name means the shared thing", () => {
228
- it("medium is the stock ease and duration", () => {
229
- expect(animationConfig.medium).toBe("ease-in 300ms");
229
+ it("medium is the v5 ease and duration", () => {
230
+ expect(animationConfig.medium).toBe(animationsCSS.animations.medium);
230
231
  });
231
232
 
232
- it("every other shared name already matched, and still does", () => {
233
- expect(animationConfig.bouncy).toBe("ease-in 200ms");
234
- expect(animationConfig.lazy).toBe("ease-in 600ms");
235
- expect(animationConfig.slow).toBe("ease-in 500ms");
236
- expect(animationConfig.quick).toBe("ease-in 100ms");
233
+ it("every other shared name matches v5", () => {
234
+ for (const stop of ["bouncy", "lazy", "slow", "quick"] as const)
235
+ expect(animationConfig[stop]).toBe(animationsCSS.animations[stop]);
237
236
  });
238
237
  });
239
238
 
@@ -0,0 +1,60 @@
1
+ import { defaultChildrenThemes } from "@tamagui/themes/v5";
2
+ import { describe, expect, it } from "vitest";
3
+ import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
4
+ import { createThemesBuilder } from "./createThemes";
5
+ import { defaultAccentTheme } from "./defaults/accent";
6
+ import { defaultBaseTheme } from "./defaults/base";
7
+ import { defaultBuilderOptions } from "./defaults/builderOptions";
8
+
9
+ const themes = createThemesBuilder(
10
+ defaultBaseTheme,
11
+ defaultAccentTheme,
12
+ defaultBuilderOptions,
13
+ ).themes() as Record<string, Record<string, string>>;
14
+
15
+ const hex = (raw: string) => {
16
+ const value = normalizeToHex(raw);
17
+ if (!value) throw new Error(`not an opaque colour: ${raw}`);
18
+ return value;
19
+ };
20
+ const ratio = (a: string, b: string) =>
21
+ contrastRatio(relativeLuminance(hex(a)), relativeLuminance(hex(b)));
22
+
23
+ const hues = Object.keys(defaultChildrenThemes).filter((hue) => hue !== "neutral");
24
+ const stock = (scheme: "light" | "dark", hue: string) =>
25
+ hex(
26
+ (defaultChildrenThemes as Record<string, Record<string, Record<string, string>>>)[hue][scheme][
27
+ `${hue}11`
28
+ ],
29
+ );
30
+
31
+ describe("named step-11 ink holds 4.5:1 on the v5 page and surface1 card", () => {
32
+ for (const scheme of ["light", "dark"] as const) {
33
+ const theme = themes[scheme];
34
+ for (const hue of hues) {
35
+ it(`${scheme} $${hue}11 reads on $background and $color3`, () => {
36
+ expect(ratio(theme[`${hue}11`], theme.background)).toBeGreaterThanOrEqual(4.5);
37
+ expect(ratio(theme[`${hue}11`], theme.color3)).toBeGreaterThanOrEqual(4.5);
38
+ });
39
+ }
40
+ }
41
+
42
+ it("keeps v5's step 11 where it holds and darkens only the failing light hues", () => {
43
+ for (const scheme of ["light", "dark"] as const) {
44
+ const theme = themes[scheme];
45
+ for (const hue of hues) {
46
+ const shipped = stock(scheme, hue);
47
+ const holds = [theme.background, theme.color3].every(
48
+ (surface) => ratio(shipped, surface) >= 4.5,
49
+ );
50
+ if (holds) expect(hex(theme[`${hue}11`])).toBe(shipped);
51
+ else
52
+ expect(relativeLuminance(hex(theme[`${hue}11`]))).toBeLessThan(
53
+ relativeLuminance(shipped),
54
+ );
55
+ }
56
+ }
57
+ expect(hex(themes.light.green11)).not.toBe(stock("light", "green"));
58
+ expect(hex(themes.dark.green11)).toBe(stock("dark", "green"));
59
+ });
60
+ });