@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
@@ -10,7 +10,7 @@ export default {
10
10
  docs: {
11
11
  description: {
12
12
  component:
13
- "Surface paints nothing. It declares size/density intent for a subtree and nested Surfaces clamp step-down only (LC-68). The probe below is a Tamagui stack that spreads what useResolvedKnobs resolved inside the wrapper, so the measured part is the stack, never the wrapper.",
13
+ "Surface paints nothing. It forwards a Tamagui size token to a subtree, and nested Surfaces clamp step-down only (LC-78). The probe below is a Tamagui stack that spreads what useResolvedKnobs resolved inside the wrapper, so the measured part is the stack, never the wrapper.",
14
14
  },
15
15
  },
16
16
  },
@@ -20,8 +20,6 @@ export default {
20
20
  function Probe({ label }: { label: string }) {
21
21
  const { knobProps } = useResolvedKnobs();
22
22
  const attrs = {
23
- "data-size": String(knobProps.size),
24
- "data-space": String(knobProps.space),
25
23
  "data-size-token": String(knobProps.sizeToken),
26
24
  "data-gap": String(knobProps.gap.gap),
27
25
  } as Record<string, unknown>;
@@ -46,46 +44,31 @@ export const main = {
46
44
  render: () => (
47
45
  <XStack gap="$4" flexWrap="wrap" alignItems="flex-start">
48
46
  <Probe label="outside" />
49
- <Surface size="small">
47
+ <Surface size="$3">
50
48
  <Probe label="small" />
51
49
  </Surface>
52
- <Surface size="large">
50
+ <Surface size="$5">
53
51
  <Probe label="large" />
54
52
  </Surface>
55
53
  </XStack>
56
54
  ),
57
55
  };
58
56
 
59
- /** LC-68: a large request inside a small Surface resolves small; small inside large stays small. */
57
+ /** LC-78: a $5 request inside a $3 Surface resolves $3; $3 inside $5 stays $3. */
60
58
  export const nestedClamp = {
61
59
  name: "Nested clamp",
62
60
  render: () => (
63
61
  <XStack gap="$4" flexWrap="wrap" alignItems="flex-start">
64
- <Surface size="small">
65
- <Surface size="large">
62
+ <Surface size="$3">
63
+ <Surface size="$5">
66
64
  <Probe label="large-in-small" />
67
65
  </Surface>
68
66
  </Surface>
69
- <Surface size="large">
70
- <Surface size="small">
67
+ <Surface size="$5">
68
+ <Surface size="$3">
71
69
  <Probe label="small-in-large" />
72
70
  </Surface>
73
71
  </Surface>
74
72
  </XStack>
75
73
  ),
76
74
  };
77
-
78
- /** Density steps the gap, not the control height. */
79
- export const density = {
80
- name: "Density",
81
- render: () => (
82
- <XStack gap="$4" flexWrap="wrap" alignItems="flex-start">
83
- <Surface density="comfortable">
84
- <Probe label="comfortable" />
85
- </Surface>
86
- <Surface density="compact">
87
- <Probe label="compact" />
88
- </Surface>
89
- </XStack>
90
- ),
91
- };
@@ -1,72 +1,116 @@
1
1
  import type { ReactNode } from "react";
2
- import { createStyledContext } from "tamagui";
2
+ import { createStyledContext, type SizeTokens } from "tamagui";
3
+ import { SIZE_KEYS } from "./scale/scaleInputs";
3
4
 
4
- export type SurfaceSize = "small" | "medium" | "large";
5
- export type SurfaceSizeIntent = SurfaceSize | "sm" | "md" | "lg";
6
- export type SurfaceDensity = "compact" | "comfortable";
7
- export type SurfaceIntent = { size: SurfaceSize | "unset"; density: SurfaceDensity | "unset" };
8
-
9
- const SIZE_INTENT_ALIASES: Record<SurfaceSizeIntent, SurfaceSize> = {
10
- sm: "small",
11
- small: "small",
12
- md: "medium",
13
- medium: "medium",
14
- lg: "large",
15
- large: "large",
16
- };
17
-
18
- export function normalizeSurfaceSize(size: SurfaceSizeIntent): SurfaceSize {
19
- return SIZE_INTENT_ALIASES[size];
20
- }
5
+ /** A Tamagui size key ("$3", "$true") or `unset` for no ceiling. */
6
+ export type SurfaceSize = SizeTokens | "unset";
7
+ /**
8
+ * `painted`: an ancestor already paints a surface box (Card, Panel), so a
9
+ * surface under it draws border only and no second shadow. Overlays (dialog,
10
+ * popover, sheet, menu, tooltip, toast) reset it: they float over the page.
11
+ */
12
+ export type SurfaceIntent = { size: SurfaceSize; painted: boolean };
21
13
 
22
14
  export const SurfaceContext = createStyledContext<SurfaceIntent>({
23
15
  size: "unset",
24
- density: "unset",
16
+ painted: false,
25
17
  });
26
18
 
27
- export const SURFACE_SIZE_RANK = { unset: 99, small: 0, medium: 1, large: 2 } as const;
28
- export const SURFACE_DENSITY_RANK = { unset: 99, compact: 0, comfortable: 1 } as const;
19
+ const sizeKeyRank = new Map<string, number>(SIZE_KEYS.map((key, index) => [key, index]));
20
+ sizeKeyRank.set("$true", sizeKeyRank.get("$4") ?? 0);
29
21
 
30
22
  /**
31
- * Nested surfaces/controls may inherit or go tighter (lower rank), never larger
32
- * than the parent (LC-68). `unset` parent = no ceiling. `unset` requested = inherit.
23
+ * Rank of a size key on the scale's sorted `$size` ladder (`$true` ranks
24
+ * with `$4`). Undefined for anything that is not a size key.
33
25
  */
34
- export function clampSurfaceSize(
35
- requested: SurfaceSize | "unset",
36
- parent: SurfaceSize | "unset",
37
- ): SurfaceSize | "unset" {
38
- if (parent === "unset") return requested;
39
- if (requested === "unset") return parent;
40
- return SURFACE_SIZE_RANK[requested] <= SURFACE_SIZE_RANK[parent] ? requested : parent;
26
+ export function surfaceSizeRank(size: SurfaceSize): number | undefined {
27
+ return typeof size === "string" ? sizeKeyRank.get(size) : undefined;
41
28
  }
42
29
 
43
- export function clampSurfaceDensity(
44
- requested: SurfaceDensity | "unset",
45
- parent: SurfaceDensity | "unset",
46
- ): SurfaceDensity | "unset" {
30
+ /**
31
+ * Order two size keys on the `$size` ladder: negative when `a` is smaller
32
+ * than `b`, 0 when equal or either is not a size key.
33
+ *
34
+ * @example A part that draws a smaller glyph below the default control size
35
+ * const small = compareSizeTokens(knobProps.sizeToken, "$true") < 0;
36
+ */
37
+ export function compareSizeTokens(a: SurfaceSize | string, b: SurfaceSize | string): number {
38
+ const rankA = surfaceSizeRank(a as SurfaceSize);
39
+ const rankB = surfaceSizeRank(b as SurfaceSize);
40
+ return rankA === undefined || rankB === undefined ? 0 : rankA - rankB;
41
+ }
42
+
43
+ /**
44
+ * Where a size token sits against the default control size: -1 below
45
+ * `$true`, 0 at it, 1 above. For parts whose own table has a row per side.
46
+ */
47
+ export function sizeTokenStep(sizeToken: SurfaceSize | string): -1 | 0 | 1 {
48
+ return Math.sign(compareSizeTokens(sizeToken, "$true")) as -1 | 0 | 1;
49
+ }
50
+
51
+ /**
52
+ * The row a part's own three-row table reads at a size token: `small` below
53
+ * `$true`, `large` above, `medium` at it. Only for per-part px tables
54
+ * (glyphs, swatches); controls read the size recipe.
55
+ */
56
+ export function sizeTokenStop(sizeToken: SurfaceSize | string): "small" | "medium" | "large" {
57
+ const step = sizeTokenStep(sizeToken);
58
+ return step < 0 ? "small" : step > 0 ? "large" : "medium";
59
+ }
60
+
61
+ /**
62
+ * The whole size key one step below `size` (`$true` reads as `$4`), floored at
63
+ * `$1`: a table's cells default one key below the table. A key that is not a
64
+ * whole step snaps to the whole key under it.
65
+ *
66
+ * @example sizeTokenBelow("$true") === "$3"
67
+ */
68
+ export function sizeTokenBelow(size: SizeTokens | string): SizeTokens {
69
+ const key = size === "$true" ? "$4" : String(size);
70
+ const step = Number(key.replace(/^\$/, ""));
71
+ if (!Number.isFinite(step)) return "$3";
72
+ return `$${Math.max(1, Math.ceil(step) - 1)}` as SizeTokens;
73
+ }
74
+
75
+ /**
76
+ * Nested surfaces and controls inherit the ceiling or go smaller, never
77
+ * larger (LC-78). `unset` parent = no ceiling; `unset` requested = inherit.
78
+ * A request that is not a size key cannot be ranked, so under a ceiling it
79
+ * takes the ceiling.
80
+ */
81
+ export function clampSurfaceSize(requested: SurfaceSize, parent: SurfaceSize): SurfaceSize {
47
82
  if (parent === "unset") return requested;
48
83
  if (requested === "unset") return parent;
49
- return SURFACE_DENSITY_RANK[requested] <= SURFACE_DENSITY_RANK[parent] ? requested : parent;
84
+ const requestedRank = surfaceSizeRank(requested);
85
+ const parentRank = surfaceSizeRank(parent);
86
+ if (parentRank === undefined) return requested;
87
+ if (requestedRank === undefined) return parent;
88
+ return requestedRank <= parentRank ? requested : parent;
50
89
  }
51
90
 
52
91
  /**
53
- * Declares size/density INTENT for a subtree. Nested Surfaces clamp step-down
54
- * only — never raw gap tokens (LC-68, LC-69). Accepts sm/md/lg aliases.
92
+ * Forwards a size token to a subtree: every knob reader under it renders its
93
+ * controls at `size` unless it asks for a smaller one (LC-78 ceiling).
94
+ * `painted` marks (true) or resets (false) the painted-ancestor flag; omitted
95
+ * inherits it.
96
+ *
97
+ * @example A table cell's editors at the row's control height
98
+ * <Surface size="$3">{cell}</Surface>
55
99
  */
56
100
  export function Surface({
57
101
  size,
58
- density,
102
+ painted,
59
103
  children,
60
104
  }: {
61
- size?: SurfaceSizeIntent;
62
- density?: SurfaceDensity;
105
+ size?: SizeTokens;
106
+ painted?: boolean;
63
107
  children?: ReactNode;
64
108
  }) {
65
109
  const parent = SurfaceContext.useStyledContext();
66
- const nextSize = clampSurfaceSize(size ? normalizeSurfaceSize(size) : "unset", parent.size);
67
- const nextDensity = clampSurfaceDensity(density ?? "unset", parent.density);
110
+ const nextSize = clampSurfaceSize(size ?? "unset", parent.size);
111
+ const nextPainted = painted ?? parent.painted ?? false;
68
112
  return (
69
- <SurfaceContext.Provider size={nextSize} density={nextDensity}>
113
+ <SurfaceContext.Provider size={nextSize} painted={nextPainted}>
70
114
  {children}
71
115
  </SurfaceContext.Provider>
72
116
  );
@@ -1,3 +1,4 @@
1
+ import { themes as stockThemes } from "@tamagui/themes/v5";
1
2
  import { describe, expect, it } from "vitest";
2
3
  import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
3
4
  import { createThemesBuilder } from "./createThemes";
@@ -38,19 +39,44 @@ describe("MPO-448 / MPO-449: the built accent pair holds Axiom 12", () => {
38
39
  });
39
40
  }
40
41
 
41
- it("light keeps the builder's pair; dark takes the nearest steps that hold", () => {
42
- expect(hex(themes.light.accentBackground)).toBe("#634fc4");
43
- expect(hex(themes.light.accentColor)).toBe("#3d2e8a");
44
- expect(themes.dark.accentBackground).toBe(themes.dark.accent9);
45
- expect(themes.dark.accentColor).toBe(themes.dark.accent11);
42
+ it("uses the nearest accent steps that hold the floors", () => {
43
+ for (const scheme of ["light", "dark"] as const) {
44
+ const theme = themes[scheme];
45
+ const steps = Array.from({ length: 12 }, (_, i) => theme[`accent${i + 1}`]);
46
+ const luminance = (value: string) => relativeLuminance(hex(value));
47
+ const nearest = (preferred: string, candidates: string[], holds: (step: string) => boolean) =>
48
+ candidates
49
+ .filter(holds)
50
+ .sort(
51
+ (a, b) =>
52
+ Math.abs(luminance(a) - luminance(preferred)) -
53
+ Math.abs(luminance(b) - luminance(preferred)),
54
+ )[0];
55
+ expect(theme.accentBackground).toBe(
56
+ nearest(
57
+ stockThemes[scheme].accentBackground,
58
+ steps.slice(0, 10),
59
+ (fill) =>
60
+ Math.max(ratio(fill, theme.color1), ratio(fill, theme.color12)) >= 4.5 &&
61
+ ratio(fill, theme.color1) >= 3,
62
+ ),
63
+ );
64
+ expect(theme.accentColor).toBe(
65
+ nearest(stockThemes[scheme].accentColor, steps, (ink) =>
66
+ [theme.color1, theme.color2, theme.color3].every((surface) => ratio(ink, surface) >= 4.5),
67
+ ),
68
+ );
69
+ }
46
70
  });
47
71
 
48
- it("the intent children carry the corrected pair and the inverse component themes keep theirs", () => {
49
- for (const name of ["dark_warning", "dark_error", "dark_success"]) {
50
- expect(themes[name].accentBackground).toBe(themes.dark.accentBackground);
51
- expect(themes[name].accentColor).toBe(themes.dark.accentColor);
72
+ it("the intent children inherit the corrected accent pair", () => {
73
+ for (const scheme of ["light", "dark"] as const) {
74
+ for (const intent of ["warning", "error", "success"]) {
75
+ expect(themes[`${scheme}_${intent}`].accentBackground).toBe(
76
+ themes[scheme].accentBackground,
77
+ );
78
+ expect(themes[`${scheme}_${intent}`].accentColor).toBe(themes[scheme].accentColor);
79
+ }
52
80
  }
53
- expect(hex(themes.dark_Tooltip.accentBackground)).toBe("#46349d");
54
- expect(hex(themes.dark_Tooltip.accentColor)).toBe("#7766cc");
55
81
  });
56
82
  });
@@ -1,6 +1,13 @@
1
- import { forceUpdateThemes, getConfig, setConfig } from "@tamagui/web";
1
+ import { type TamaguiInternalConfig, forceUpdateThemes, getConfig, setConfig } from "@tamagui/web";
2
+ import { type TokensGlobal, mergedTokens } from "./mergedTokens";
2
3
  import type { ThemeConfig } from "./shared";
3
4
 
5
+ function isThemeConfig(
6
+ target: Pick<ThemeConfig, "tamagui"> | TamaguiInternalConfig,
7
+ ): target is Pick<ThemeConfig, "tamagui"> {
8
+ return "tamagui" in target && Boolean((target as Pick<ThemeConfig, "tamagui">).tamagui);
9
+ }
10
+
4
11
  /**
5
12
  * Make `themeConfig` the config Tamagui resolves theme names against.
6
13
  *
@@ -9,12 +16,21 @@ import type { ThemeConfig } from "./shared";
9
16
  * full house config and its subset) the one created last wins whichever
10
17
  * config the provider is handed. Tamagui also caches resolved theme names,
11
18
  * so the cache is dropped here too: without that a name the new config lacks
12
- * keeps resolving to the old config's theme. Returns false when `themeConfig`
13
- * is already the active one.
19
+ * keeps resolving to the old config's theme. Takes a ThemeConfig or a built
20
+ * Tamagui config (a preset scale's `tamagui()`). Returns false when it is
21
+ * already the active one.
14
22
  */
15
- export function activateThemeConfig(themeConfig: ThemeConfig): boolean {
16
- if (getConfig() === themeConfig.tamagui) return false;
17
- setConfig(themeConfig.tamagui);
23
+ export function activateThemeConfig(
24
+ themeConfig: Pick<ThemeConfig, "tamagui"> | TamaguiInternalConfig,
25
+ ): boolean {
26
+ const tamagui = (
27
+ isThemeConfig(themeConfig) ? themeConfig.tamagui : themeConfig
28
+ ) as TamaguiInternalConfig;
29
+ if (getConfig() === tamagui) return false;
30
+ setConfig(tamagui);
31
+ if (tamagui.tokensParsed) {
32
+ (globalThis as TokensGlobal).__tamaguiTokensMerged = mergedTokens(tamagui);
33
+ }
18
34
  forceUpdateThemes();
19
35
  return true;
20
36
  }
@@ -0,0 +1,62 @@
1
+ import { animationsCSS } from "@tamagui/config/v5-css";
2
+ import { animationsReactNative } from "@tamagui/config/v5-rn";
3
+ import { describe, expect, it } from "vitest";
4
+ import { animationConfig, animationDurations, animationNames, motionStops } from "./css";
5
+ import { animationConfig as nativeConfig } from "./reactNative";
6
+
7
+ describe("Tamagui v5 motion tables", () => {
8
+ it("orders the thirteen named stops by duration", () => {
9
+ expect(motionStops).toEqual([
10
+ "quickestLessBouncy",
11
+ "quickest",
12
+ "quickerLessBouncy",
13
+ "quicker",
14
+ "quick",
15
+ "quickLessBouncy",
16
+ "medium",
17
+ "superBouncy",
18
+ "bouncy",
19
+ "slow",
20
+ "lazy",
21
+ "superLazy",
22
+ "slowest",
23
+ ]);
24
+ expect(motionStops.map((stop) => animationDurations[stop])).toEqual([
25
+ 60, 80, 100, 120, 150, 180, 300, 300, 350, 450, 500, 600, 800,
26
+ ]);
27
+ });
28
+ it("preserves every upstream CSS and native entry in order", () => {
29
+ const keys = Object.keys(animationsCSS.animations);
30
+ expect(keys).toHaveLength(22);
31
+ expect(animationNames.slice(0, 22)).toEqual(keys);
32
+ expect(Object.keys(nativeConfig).slice(0, 22)).toEqual(keys);
33
+ for (const key of keys) {
34
+ expect(animationConfig[key as keyof typeof animationConfig]).toEqual(
35
+ animationsCSS.animations[key as keyof typeof animationsCSS.animations],
36
+ );
37
+ expect(nativeConfig[key as keyof typeof nativeConfig]).toEqual(
38
+ animationsReactNative.animations[key as keyof typeof animationsReactNative.animations],
39
+ );
40
+ }
41
+ });
42
+
43
+ it("maps house aliases to their nearest v5 duration", () => {
44
+ for (const [alias, stop] of [
45
+ ["tooltip", "slow"],
46
+ ["snappy", "quickest"],
47
+ ["gentle", "slow"],
48
+ ] as const) {
49
+ expect(animationConfig[alias]).toEqual(animationConfig[stop]);
50
+ expect(nativeConfig[alias]).toEqual(nativeConfig[stop]);
51
+ }
52
+ });
53
+
54
+ it("uses v5 easing and derives every duration from its CSS value", () => {
55
+ expect(animationConfig.quick).toBe("150ms cubic-bezier(0.25, 0.1, 0.25, 1)");
56
+ for (const key of animationNames) {
57
+ expect(animationConfig[key]).not.toContain("ease-in ");
58
+ expect(animationConfig[key]).not.toContain("ease-in-out");
59
+ expect(animationDurations[key]).toBe(Number.parseInt(animationConfig[key], 10));
60
+ }
61
+ });
62
+ });
@@ -1,38 +1,12 @@
1
1
  import { createAnimations } from "@tamagui/animations-css";
2
-
3
- /**
4
- * The CSS driver's named durations.
5
- *
6
- * `bouncy`, `lazy`, `slow`, `medium` and `quick` are Tamagui's own names and
7
- * carry Tamagui's own values — a shared name that means something different
8
- * here is worse than no name, because every consumer reading `animation="medium"`
9
- * against the upstream docs gets a different answer than the one it renders.
10
- * MPO-19 X10: `medium` was `ease-in-out 250ms` against stock `ease-in 300ms`,
11
- * with no improvement argument written down anywhere.
12
- *
13
- * `tooltip`, `snappy` and `gentle` are additive — names Tamagui does not use,
14
- * so they are ours to set and are not a contradiction.
15
- */
16
- export const animationConfig = {
17
- bouncy: "ease-in 200ms",
18
- lazy: "ease-in 600ms",
19
- slow: "ease-in 500ms",
20
- medium: "ease-in 300ms",
21
- quick: "ease-in 100ms",
22
- tooltip: "ease-in 400ms",
23
- snappy: "ease-out 80ms",
24
- gentle: "ease-in-out 450ms",
25
- } as const;
26
-
27
- export type AnimationName = keyof typeof animationConfig;
28
- export const animationNames = Object.keys(animationConfig) as AnimationName[];
2
+ import { animationConfig } from "../motionTiming";
3
+
4
+ export {
5
+ animationConfig,
6
+ animationDurations,
7
+ animationNames,
8
+ type AnimationName,
9
+ } from "../motionTiming";
29
10
  export const animations = createAnimations(animationConfig);
30
11
 
31
- function parseDurationMs(timing: string): number {
32
- const match = timing.match(/(\d+)\s*ms/);
33
- return match ? Number.parseInt(match[1], 10) : 200;
34
- }
35
-
36
- export const animationDurations: Record<AnimationName, number> = Object.fromEntries(
37
- Object.entries(animationConfig).map(([k, v]) => [k, parseDurationMs(v)]),
38
- ) as Record<AnimationName, number>;
12
+ export { motionStops, type MotionStop } from "../motionStops";
@@ -1,55 +1,128 @@
1
1
  import { createAnimations } from "@tamagui/animations-react-native";
2
2
 
3
- /**
4
- * Canonical spring vocabulary. Exported so native gesture code can drive
5
- * RN `Animated.spring` (which provides completion callbacks) with the exact
6
- * same physics as Tamagui's `transition` tokens — one closed vocabulary.
7
- */
8
3
  export const animationConfig = {
4
+ "0ms": {
5
+ type: "timing",
6
+ duration: 0,
7
+ },
8
+ "50ms": {
9
+ type: "timing",
10
+ duration: 50,
11
+ },
12
+ "75ms": {
13
+ type: "timing",
14
+ duration: 75,
15
+ },
16
+ "100ms": {
17
+ type: "timing",
18
+ duration: 100,
19
+ },
20
+ "200ms": {
21
+ type: "timing",
22
+ duration: 200,
23
+ },
24
+ "250ms": {
25
+ type: "timing",
26
+ duration: 250,
27
+ },
28
+ "300ms": {
29
+ type: "timing",
30
+ duration: 300,
31
+ },
32
+ "400ms": {
33
+ type: "timing",
34
+ duration: 400,
35
+ },
36
+ "500ms": {
37
+ type: "timing",
38
+ duration: 500,
39
+ },
40
+ superBouncy: {
41
+ type: "spring",
42
+ damping: 5,
43
+ mass: 0.7,
44
+ stiffness: 180,
45
+ },
9
46
  bouncy: {
10
47
  type: "spring",
11
48
  damping: 9,
12
49
  mass: 0.9,
13
- stiffness: 150,
50
+ stiffness: 120,
51
+ },
52
+ superLazy: {
53
+ type: "spring",
54
+ damping: 25,
55
+ mass: 2,
56
+ stiffness: 25,
14
57
  },
15
58
  lazy: {
16
59
  type: "spring",
17
60
  damping: 18,
18
- stiffness: 50,
61
+ mass: 0.2,
62
+ stiffness: 10,
19
63
  },
20
- slow: {
64
+ medium: {
65
+ damping: 16,
66
+ stiffness: 90,
67
+ mass: 0.8,
68
+ },
69
+ slowest: {
21
70
  type: "spring",
22
71
  damping: 15,
23
- stiffness: 40,
72
+ stiffness: 10,
24
73
  },
25
- medium: {
74
+ slow: {
26
75
  type: "spring",
27
- damping: 15,
28
- mass: 1,
29
- stiffness: 120,
76
+ damping: 45,
77
+ stiffness: 60,
30
78
  },
31
79
  quick: {
32
80
  type: "spring",
33
- damping: 20,
34
- mass: 1.2,
35
- stiffness: 250,
81
+ damping: 25,
82
+ mass: 1,
83
+ stiffness: 550,
84
+ },
85
+ quickLessBouncy: {
86
+ type: "spring",
87
+ damping: 40,
88
+ mass: 2,
89
+ stiffness: 400,
90
+ },
91
+ quicker: {
92
+ type: "spring",
93
+ damping: 16,
94
+ mass: 0.6,
95
+ stiffness: 700,
96
+ },
97
+ quickerLessBouncy: {
98
+ type: "spring",
99
+ damping: 27,
100
+ mass: 0.45,
101
+ stiffness: 750,
102
+ },
103
+ quickest: {
104
+ damping: 15,
105
+ mass: 0.6,
106
+ stiffness: 400,
107
+ },
108
+ quickestLessBouncy: {
109
+ damping: 24,
110
+ mass: 0.35,
111
+ stiffness: 750,
36
112
  },
37
113
  tooltip: {
38
114
  type: "spring",
39
- damping: 10,
40
- mass: 0.9,
41
- stiffness: 100,
115
+ damping: 45,
116
+ stiffness: 60,
42
117
  },
43
118
  snappy: {
44
- type: "spring",
45
- damping: 28,
46
- mass: 0.8,
47
- stiffness: 350,
119
+ damping: 15,
120
+ mass: 0.6,
121
+ stiffness: 400,
48
122
  },
49
123
  gentle: {
50
124
  type: "spring",
51
- damping: 12,
52
- mass: 1.4,
125
+ damping: 45,
53
126
  stiffness: 60,
54
127
  },
55
128
  } as const;
@@ -0,0 +1,57 @@
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
+
10
+ const inputThemes: Record<string, Record<string, string>> = {};
11
+
12
+ const themes = createThemesBuilder(defaultBaseTheme, defaultAccentTheme, {
13
+ ...defaultBuilderOptions,
14
+ getTheme: (props) => {
15
+ inputThemes[props.name] = props.theme;
16
+ return defaultBuilderOptions.getTheme!(props);
17
+ },
18
+ }).themes() as Record<string, Record<string, string>>;
19
+ const config = createTamagui({ ...configWithoutAnimations, themes });
20
+
21
+ function ratio(a: string, b: string) {
22
+ const aHex = normalizeToHex(a);
23
+ const bHex = normalizeToHex(b);
24
+ if (!aHex || !bHex) throw new Error(`Expected opaque colours: ${a}, ${b}`);
25
+ return contrastRatio(relativeLuminance(aHex), relativeLuminance(bHex));
26
+ }
27
+
28
+ describe("DF-06 / WCAG 1.4.11: control boundaries hold 3:1", () => {
29
+ for (const [name, theme] of Object.entries(themes).filter(
30
+ ([, theme]) => theme.color5 !== undefined,
31
+ )) {
32
+ it(`${name} holds the boundary floor`, () => {
33
+ expect(theme.boundaryColor).toBeTruthy();
34
+ const surfaces = [theme.background, theme.color3, theme.color5];
35
+ const minimum = (ink: string) => Math.min(...surfaces.map((surface) => ratio(ink, surface)));
36
+ // Tint text is neutralized after getBaseTheme; selection uses the input ramp.
37
+ const candidates = [8, 9, 10, 11, 12].map((step) => inputThemes[name][`color${step}`]);
38
+ const firstHolding = candidates.find((ink) => minimum(ink) >= minContrastRatio);
39
+ expect(minimum(theme.boundaryColor)).toBeGreaterThanOrEqual(minContrastRatio);
40
+ expect(theme.boundaryColor).toBe(firstHolding);
41
+ });
42
+ }
43
+
44
+ it.each([
45
+ "light_Switch",
46
+ "dark_error_InputContainerFrame",
47
+ "light_ProgressIndicator",
48
+ "dark_ProgressIndicator",
49
+ "light_alt1",
50
+ "dark_error_active",
51
+ ])("%s resolves its parent's boundary through Tamagui's theme proxy", (name) => {
52
+ const parent = name.slice(0, name.lastIndexOf("_"));
53
+ expect(Object.hasOwn(themes[name], "boundaryColor")).toBe(false);
54
+ expect(config.themes[parent].boundaryColor?.val).toBeTruthy();
55
+ expect(config.themes[name].boundaryColor?.val).toBe(config.themes[parent].boundaryColor.val);
56
+ });
57
+ });