@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/theme",
3
- "version": "7.34.4",
3
+ "version": "7.36.0",
4
4
  "description": "Tamagui theme system for multiplatform.one",
5
5
  "keywords": [
6
6
  "multiplatform",
@@ -38,14 +38,29 @@
38
38
  "source": "./src/audit/index.ts",
39
39
  "types": "./types/audit/index.d.ts",
40
40
  "default": "./src/audit/index.ts"
41
+ },
42
+ "./scale-attribute": {
43
+ "source": "./src/theme/scale/scaleAttribute.ts",
44
+ "types": "./types/theme/scale/scaleAttribute.d.ts",
45
+ "default": "./src/theme/scale/scaleAttribute.ts"
46
+ },
47
+ "./design-tokens": {
48
+ "source": "./src/audit/designTokens.ts",
49
+ "types": "./types/audit/designTokens.d.ts",
50
+ "default": "./src/audit/designTokens.ts"
51
+ },
52
+ "./motion": {
53
+ "source": "./src/theme/motionTiming.ts",
54
+ "types": "./types/theme/motionTiming.d.ts",
55
+ "default": "./src/theme/motionTiming.ts"
41
56
  }
42
57
  },
43
58
  "publishConfig": {
44
59
  "access": "public"
45
60
  },
46
61
  "dependencies": {
47
- "@multiplatform.one/platform": "^7.34.4",
48
- "@multiplatform.one/store": "^7.34.4",
62
+ "@multiplatform.one/platform": "^7.36.0",
63
+ "@multiplatform.one/store": "^7.36.0",
49
64
  "@tamagui/animations-react-native": "2.7.6",
50
65
  "@tamagui/colors": "2.7.6",
51
66
  "@tamagui/config": "2.7.6",
@@ -58,8 +73,8 @@
58
73
  "react-cookie": "^8.1.2"
59
74
  },
60
75
  "devDependencies": {
61
- "@multiplatform.one/config": "7.34.4",
62
- "@multiplatform.one/test-utils": "7.34.4",
76
+ "@multiplatform.one/config": "7.36.0",
77
+ "@multiplatform.one/test-utils": "7.36.0",
63
78
  "@tamagui/animations-css": "2.7.6",
64
79
  "@tamagui/animations-reanimated": "2.7.6",
65
80
  "@tamagui/font-inter": "2.7.6",
@@ -1,12 +1,17 @@
1
1
  import { afterEach, describe, expect, it } from "vitest";
2
2
  import { defaultConfig } from "@tamagui/config/v5";
3
3
  import { defaultHeadingFont } from "../theme/defaults/fonts";
4
+ import { fontCategoryStacks } from "../theme/fontCategoryStacks";
4
5
  import { defaultKnobs } from "../theme/knobs";
5
6
  import { resolveKnobs } from "../theme/resolveKnobs";
7
+ import { deriveRecipeInputs } from "../theme/scale/deriveRecipeInputs";
8
+ import { generateScale } from "../theme/scale/generateScale";
9
+ import { defaultScaleInputs } from "../theme/scale/scaleInputs";
6
10
  import { GAP_KNOB, GAP_LG_KNOB, runConstraintAudit } from "./constraintAudit";
7
11
 
8
12
  afterEach(() => {
9
13
  document.body.innerHTML = "";
14
+ delete (globalThis as { __MPO_SCALE__?: unknown }).__MPO_SCALE__;
10
15
  });
11
16
 
12
17
  // Deliberately loses module scope, like Playwright page.evaluate.
@@ -27,34 +32,48 @@ function measured() {
27
32
  return audit().elements.find((el) => el.id === "specimen");
28
33
  }
29
34
 
30
- function card(radius: string, space: string, tier = "content") {
35
+ function card(radius: string, tier = "content") {
31
36
  return {
32
37
  "data-constraint-container": "Card",
33
38
  "data-tier": tier,
34
39
  "data-radius-knob": radius,
35
- "data-space-knob": space,
36
40
  };
37
41
  }
38
42
 
43
+ function recipeGaps(scale: ReturnType<typeof generateScale>) {
44
+ const { tokenTable, chipTokenTable } = deriveRecipeInputs(scale);
45
+ return [
46
+ ...new Set(
47
+ [...Object.values(tokenTable), ...Object.values(chipTokenTable)].map((row) => row.radius),
48
+ ),
49
+ ].sort((a, b) => a - b);
50
+ }
51
+
52
+ function publish(scale: ReturnType<typeof generateScale>) {
53
+ (globalThis as { __MPO_SCALE__?: unknown }).__MPO_SCALE__ = {
54
+ ...scale,
55
+ controlGaps: recipeGaps(scale),
56
+ };
57
+ }
58
+
59
+ const tokens = generateScale(defaultScaleInputs).tokens;
60
+ const inset = tokens.space.$4;
61
+
39
62
  describe("Card container cap, independently measured against canonical stops", () => {
40
63
  for (const tier of ["content", "elevated"]) {
41
64
  for (const [radius, stop] of Object.entries({
42
65
  none: 0,
43
- small: 5,
44
- medium: 9,
45
- large: 16,
46
- full: 50,
66
+ small: tokens.radius[4],
67
+ medium: tokens.radius[6],
68
+ large: tokens.radius[8],
69
+ full: tokens.radius[12],
47
70
  })) {
48
- for (const [space, inset] of Object.entries({ small: 13, medium: 18, large: 32 })) {
49
- it(`${tier}/${radius}/${space} accepts all four capped corners`, () => {
50
- specimen(
51
- `border-radius:${Math.min(stop, inset)}px;padding:${inset}px`,
52
- card(radius, space, tier),
53
- );
54
- expect(measured()?.violations).toEqual([]);
55
- expect(measured()?.paddingTop?.value).toBe(inset);
56
- });
57
- }
71
+ it(`${tier}/${radius} accepts all four corners capped at the panelPadding role`, () => {
72
+ specimen(`border-radius:${Math.min(stop, inset)}px;padding:${inset}px`, card(radius, tier));
73
+ expect(measured()?.violations).toEqual([]);
74
+ expect(measured()?.paddingTop?.value).toBe(inset);
75
+ expect(measured()?.paddingTop?.knob).toBe("Card panelPadding");
76
+ });
58
77
  }
59
78
  }
60
79
 
@@ -65,30 +84,22 @@ describe("Card container cap, independently measured against canonical stops", (
65
84
  "border-radius:13px;padding:13px",
66
85
  { "data-tier": "content" },
67
86
  ],
68
- ["16 exceeds 13 inset", "border-radius:16px;padding:13px", card("large", "small")],
69
- ["wrong last corner", "border-radius:13px 13px 13px 9px;padding:13px", card("full", "small")],
70
- ["nonzero at none", "border-radius:5px;padding:13px", card("none", "small")],
71
- ["arbitrary matching inset", "border-radius:14px;padding:14px", card("full", "small")],
72
- ["wrong canonical inset", "border-radius:18px;padding:18px", card("full", "small")],
73
- ["missing inset", "border-radius:0;padding:0", card("none", "small")],
74
- [
75
- "wrong individual inset",
76
- "border-radius:13px;padding:13px 18px 13px 13px",
77
- card("full", "small"),
78
- ],
79
- ["percent radius cannot bypass cap", "border-radius:50%;padding:13px", card("full", "small")],
80
- [
81
- "elliptical corner cannot bypass cap",
82
- "border-radius:13px / 16px;padding:13px",
83
- card("full", "small"),
84
- ],
87
+ ["a 13px inset is not the panelPadding role", "border-radius:13px;padding:13px", card("full")],
88
+ ["radius past the inset", "border-radius:24px;padding:18px", card("full")],
89
+ ["wrong last corner", "border-radius:18px 18px 18px 9px;padding:18px", card("full")],
90
+ ["nonzero at none", "border-radius:5px;padding:18px", card("none")],
91
+ ["arbitrary matching inset", "border-radius:14px;padding:14px", card("full")],
92
+ ["missing inset", "border-radius:0;padding:0", card("none")],
93
+ ["wrong individual inset", "border-radius:16px;padding:16px 13px 16px 16px", card("full")],
94
+ ["percent radius cannot bypass cap", "border-radius:50%;padding:18px", card("full")],
95
+ ["elliptical corner cannot bypass cap", "border-radius:18px / 24px;padding:18px", card("full")],
85
96
  ])("rejects %s", (_label, style, attrs) => {
86
97
  specimen(style, attrs);
87
98
  expect(measured()?.violations.length).toBeGreaterThan(0);
88
99
  });
89
100
 
90
101
  it("leaves ordinary control radius rules intact", () => {
91
- specimen("border-radius:16px;padding:13px");
102
+ specimen("border-radius:14px;padding:16px");
92
103
  expect(measured()?.violations).toEqual([]);
93
104
  });
94
105
 
@@ -98,33 +109,138 @@ describe("Card container cap, independently measured against canonical stops", (
98
109
  "border-bottom-right-radius",
99
110
  "border-bottom-left-radius",
100
111
  ])("rejects a wrong %s even when every radius is on the generic scale", (corner) => {
101
- const el = specimen("border-radius:9px;padding:13px", card("medium", "small"));
102
- el.style.setProperty(corner, "5px");
112
+ const el = specimen("border-radius:14px;padding:16px", card("medium"));
113
+ el.style.setProperty(corner, "4px");
103
114
  expect(measured()?.borderRadius?.offScale).toBe(true);
104
115
  expect(measured()?.violations).toHaveLength(1);
105
116
  });
106
117
  });
107
118
 
108
- describe("canonical gapLg parity in the serialized audit", () => {
109
- for (const space of ["small", "medium", "large"] as const) {
110
- it(space, () => {
111
- const token = resolveKnobs({ ...defaultKnobs, space }).knobProps.gapLg.gap;
112
- const px = defaultConfig.tokens.space[token as keyof typeof defaultConfig.tokens.space];
113
- expect(GAP_LG_KNOB[space]).toBe(px);
114
- const gapToken = resolveKnobs({ ...defaultKnobs, space }).knobProps.gap.gap;
115
- const gapPx = defaultConfig.tokens.space[gapToken as keyof typeof defaultConfig.tokens.space];
116
- expect(GAP_KNOB[space]).toBe(gapPx);
117
- specimen(`row-gap:${px}px;column-gap:${gapPx}px`);
118
- expect(measured()?.rowGap?.offScale).toBe(false);
119
- expect(measured()?.columnGap?.offScale).toBe(false);
120
- });
121
- }
122
- it.each([22, 25, 31, 33])("rejects non-recipe gap %i", (gap) => {
119
+ describe("spacing-role parity in the serialized audit", () => {
120
+ const { knobProps } = resolveKnobs(defaultKnobs);
121
+ const px = (token: string) => tokens.space[token as keyof typeof tokens.space];
122
+
123
+ it("the module tables are the resolver's roles", () => {
124
+ expect(GAP_KNOB).toEqual({ gapSm: px(knobProps.gapSm.gap), gap: px(knobProps.gap.gap) });
125
+ expect(GAP_LG_KNOB).toEqual({ gapLg: px(knobProps.gapLg.gap) });
126
+ });
127
+
128
+ it.each([
129
+ ["gapSm", knobProps.gapSm.gap],
130
+ ["gap", knobProps.gap.gap],
131
+ ["gapLg", knobProps.gapLg.gap],
132
+ ])("accepts the %s role", (_role, token) => {
133
+ specimen(`row-gap:${px(token)}px`);
134
+ expect(measured()?.rowGap?.offScale).toBe(false);
135
+ });
136
+
137
+ it("accepts the panelPadding role as a surface inset", () => {
138
+ specimen(`border-radius:8px;padding:${px(knobProps.panelPadding.padding)}px`);
139
+ expect(measured()?.paddingTop?.offScale).toBe(false);
140
+ });
141
+
142
+ it.each([18, 22, 25, 31, 33])("rejects non-role gap %i", (gap) => {
123
143
  specimen(`row-gap:${gap}px`);
124
144
  expect(measured()?.rowGap?.offScale).toBe(true);
125
145
  });
126
146
  });
127
147
 
148
+ describe("the serialized audit reads the page's published scale", () => {
149
+ it("module tables are the default generated scale's tokens", () => {
150
+ const { space } = generateScale(defaultScaleInputs).tokens;
151
+ expect(GAP_KNOB).toEqual({ gapSm: space.$2, gap: space.$3 });
152
+ expect(GAP_LG_KNOB).toEqual({ gapLg: space.$5 });
153
+ });
154
+
155
+ it("without a published scale its fallbacks are the default scale", () => {
156
+ specimen(`row-gap:${generateScale(defaultScaleInputs).tokens.space.$3}px`);
157
+ expect(measured()?.rowGap?.offScale).toBe(false);
158
+ });
159
+
160
+ it("a published scale moves the role px with it", () => {
161
+ const scale = generateScale({ control: 44, type: 15 });
162
+ specimen(`row-gap:${scale.tokens.space.$3}px`);
163
+ expect(measured()?.rowGap?.offScale).toBe(true);
164
+ publish(scale);
165
+ expect(measured()?.rowGap?.offScale).toBe(false);
166
+ });
167
+
168
+ it("a control's inner gap is the published scale's $radius at any size key (LC-75)", () => {
169
+ const scale = generateScale({ control: 42, type: 15, radius: 0.3 });
170
+ const gap3 = scale.tokens.radius["3"];
171
+ expect(gap3).toBe(10);
172
+ specimen(`row-gap:${gap3}px`);
173
+ expect(measured()?.rowGap?.offScale).toBe(true);
174
+ publish(scale);
175
+ expect(measured()?.rowGap?.offScale).toBe(false);
176
+ });
177
+
178
+ it("rejects a radius that the published recipe does not use as a gap", () => {
179
+ const scale = generateScale({ control: 42, type: 15, radius: 0.3 });
180
+ const gap = scale.tokens.radius["7"];
181
+ expect(recipeGaps(scale)).not.toContain(gap);
182
+ publish(scale);
183
+ specimen(`row-gap:${gap}px`);
184
+ expect(measured()?.rowGap?.offScale).toBe(true);
185
+ });
186
+
187
+ it("uses the published recipe gaps instead of reconstructing them from radius tokens", () => {
188
+ const scale = generateScale({ control: 42, type: 15, radius: 0.3 });
189
+ (globalThis as { __MPO_SCALE__?: unknown }).__MPO_SCALE__ = { ...scale, controlGaps: [23] };
190
+ specimen("row-gap:23px");
191
+ expect(measured()?.rowGap?.offScale).toBe(false);
192
+ document.getElementById("specimen")!.style.rowGap = `${scale.tokens.radius["3"]}px`;
193
+ expect(measured()?.rowGap?.offScale).toBe(true);
194
+ });
195
+
196
+ it("keeps the serialized fallback exactly equal to the default recipe gaps", () => {
197
+ const fallback = runConstraintAudit.toString().match(/controlGaps\s*\?\?\s*(\[[^\]]*\])/);
198
+ expect(fallback).not.toBeNull();
199
+ expect(JSON.parse(fallback![1])).toEqual(recipeGaps(generateScale(defaultScaleInputs)));
200
+ });
201
+
202
+ it("a Card on a published scale caps at that scale's panelPadding", () => {
203
+ const scale = generateScale({ control: 36, type: 14 });
204
+ const scaleInset = scale.tokens.space.$4;
205
+ publish(scale);
206
+ specimen(`border-radius:${scaleInset}px;padding:${scaleInset}px`, card("full"));
207
+ expect(measured()?.violations).toEqual([]);
208
+ });
209
+ });
210
+
211
+ describe("font family provenance", () => {
212
+ it.each(Object.entries(fontCategoryStacks))(
213
+ "accepts the canonical %s stack",
214
+ (_category, stack) => {
215
+ specimen(`font-family:${stack}`).textContent = "Configured typography";
216
+ for (const run of [audit, runConstraintAudit]) {
217
+ const entry = run().elements.find((el) => el.id === "specimen");
218
+ expect(entry?.fontFamily?.offScale).toBe(false);
219
+ expect(entry?.violations).toEqual([]);
220
+ }
221
+ },
222
+ );
223
+
224
+ it("recognizes Slate's configured primary font as mono", () => {
225
+ specimen(`font-family:${fontCategoryStacks.mono}`).textContent = "Slate heading";
226
+ expect(measured()?.fontFamily).toMatchObject({ knob: "mono", offScale: false });
227
+ });
228
+
229
+ it.each(["Papyrus", "ui-serif", "Fira Code", "'Comic Neue', monospace"])(
230
+ "rejects unregistered primary family %s",
231
+ (stack) => {
232
+ specimen(`font-family:${stack}`).textContent = "Unregistered typography";
233
+ for (const run of [audit, runConstraintAudit]) {
234
+ const entry = run().elements.find((el) => el.id === "specimen");
235
+ expect(entry?.fontFamily?.offScale).toBe(true);
236
+ expect(entry?.violations).toEqual([
237
+ expect.stringContaining("is not a registered font token"),
238
+ ]);
239
+ }
240
+ },
241
+ );
242
+ });
243
+
128
244
  describe("font weight provenance", () => {
129
245
  it("pins the configured heading source behind the self-contained whitelist", () => {
130
246
  expect([...new Set(Object.values(defaultHeadingFont.weight).map(Number))].sort()).toEqual([
@@ -137,6 +253,33 @@ describe("font weight provenance", () => {
137
253
  expect(measured()?.fontWeight).toMatchObject({ value: weight, unit: "", offScale: false });
138
254
  expect(measured()?.fontWeight?.knob).toContain("configured heading");
139
255
  });
256
+ it.each([600, 800])("accepts semantic headings at %i in both audit entry paths", (weight) => {
257
+ for (const tag of ["h1", "h2", "h3", "h4", "h5", "h6"]) {
258
+ const el = document.createElement(tag);
259
+ el.id = "specimen";
260
+ el.style.fontWeight = String(weight);
261
+ document.body.replaceChildren(el);
262
+ for (const run of [audit, runConstraintAudit]) {
263
+ expect(run().elements.find((entry) => entry.id === "specimen")?.fontWeight?.offScale).toBe(
264
+ false,
265
+ );
266
+ }
267
+ }
268
+ });
269
+ it.each([
270
+ ["role", "heading"],
271
+ ["class", "font_headingSerif"],
272
+ ])("accepts heading provenance %s=%s", (attribute, value) => {
273
+ specimen("font-weight:800", { [attribute]: value });
274
+ expect(measured()?.fontWeight?.offScale).toBe(false);
275
+ });
276
+ it("rejects a plain span at 600", () => {
277
+ const el = document.createElement("span");
278
+ el.id = "specimen";
279
+ el.style.fontWeight = "600";
280
+ document.body.append(el);
281
+ expect(measured()?.fontWeight?.offScale).toBe(true);
282
+ });
140
283
  it.each([500, 650, 900])("rejects unconfigured weight %i", (weight) => {
141
284
  specimen(`font-weight:${weight}`, { class: "font_heading" });
142
285
  expect(measured()?.fontWeight?.offScale).toBe(true);
@@ -199,93 +342,89 @@ describe("BINARY parts measure 0 or their own h/2 (DG-RAD-03)", () => {
199
342
  });
200
343
 
201
344
  describe("declared capped containers beyond Card (DG-RAD-04)", () => {
202
- const declared = (container: string, radius: string, space: string) => ({
345
+ const declared = (container: string, radius: string) => ({
203
346
  "data-constraint-container": container,
204
347
  "data-radius-knob": radius,
205
- "data-space-knob": space,
206
348
  });
207
349
 
208
350
  it("accepts a PageSection surface capped at its own panelPadding", () => {
209
- specimen("border-radius:13px;padding:13px", declared("PageSection", "full", "small"));
351
+ specimen("border-radius:16px;padding:16px", declared("PageSection", "full"));
210
352
  expect(measured()?.violations).toEqual([]);
211
353
  });
212
354
 
213
355
  it("measures a PageSection surface inset like a Card's", () => {
214
- specimen("border-radius:13px;padding:12px", declared("PageSection", "full", "small"));
215
- expect(measured()?.violations.join(" ")).toContain("PageSection panelPadding:small");
356
+ specimen("border-radius:18px;padding:12px", declared("PageSection", "full"));
357
+ expect(measured()?.violations.join(" ")).toContain("PageSection panelPadding");
216
358
  });
217
359
 
218
360
  it("accepts a TextArea box at the cap whatever its field inset", () => {
219
- specimen("border-radius:13px;padding:0", declared("TextArea", "full", "small"));
361
+ specimen("border-radius:16px;padding:0", declared("TextArea", "full"));
220
362
  expect(measured()?.violations).toEqual([]);
221
- expect(measured()?.borderRadius?.knob).toBe("TextArea min(radius:full, panelPadding:small)");
363
+ expect(measured()?.borderRadius?.knob).toBe("TextArea min(radius:full, panelPadding)");
222
364
  });
223
365
 
224
366
  it("audits a TextArea box's own padding on the ordinary scale", () => {
225
- specimen("border-radius:9px;padding:15px", declared("TextArea", "medium", "medium"));
367
+ specimen("border-radius:9px;padding:15px", declared("TextArea", "medium"));
226
368
  expect(measured()?.violations.join(" ")).toContain("paddingTop: 15px is off-scale");
227
369
  });
228
370
 
229
371
  it.each([
230
- ["the uncapped token", "border-radius:50px", declared("TextArea", "full", "small")],
231
- ["a cap for another space", "border-radius:18px", declared("TextArea", "full", "small")],
232
- ["the token past the cap", "border-radius:16px", declared("TextArea", "large", "small")],
233
- ["an unknown radius stop", "border-radius:13px", declared("TextArea", "round", "small")],
372
+ ["the uncapped token", "border-radius:50px", declared("TextArea", "full")],
373
+ ["a cap off the panelPadding role", "border-radius:13px", declared("TextArea", "full")],
374
+ ["a radius past the cap", "border-radius:24px", declared("TextArea", "full")],
375
+ ["an unknown radius stop", "border-radius:13px", declared("TextArea", "round")],
234
376
  ])("rejects %s", (_label, style, attrs) => {
235
377
  specimen(style, attrs);
236
378
  expect(measured()?.borderRadius?.offScale).toBe(true);
237
379
  });
238
380
 
239
381
  it("names a container the audit does not know instead of trusting it", () => {
240
- specimen("border-radius:13px;padding:13px", declared("Tile", "full", "small"));
382
+ specimen("border-radius:13px;padding:13px", declared("Tile", "full"));
241
383
  expect(measured()?.violations.join(" ")).toContain('"Tile" is not a container');
242
384
  });
243
385
  });
244
386
 
245
387
  describe("stacked rows cap their outer corners and square the inner ones (MPO-56)", () => {
246
- const row = (position: string, radius: string, space: string) => ({
388
+ const row = (position: string, radius: string) => ({
247
389
  "data-constraint-container": "StackedRow",
248
390
  "data-stack-position": position,
249
391
  "data-radius-knob": radius,
250
- "data-space-knob": space,
251
392
  });
252
393
  const corners = (tl: number, tr: number, br: number, bl: number) =>
253
394
  `border-top-left-radius:${tl}px;border-top-right-radius:${tr}px;` +
254
- `border-bottom-right-radius:${br}px;border-bottom-left-radius:${bl}px;padding:7px 13px`;
395
+ `border-bottom-right-radius:${br}px;border-bottom-left-radius:${bl}px;padding:6px 12px`;
255
396
 
256
397
  it.each([
257
- ["first", corners(13, 13, 0, 0)],
398
+ ["first", corners(16, 16, 0, 0)],
258
399
  ["middle", corners(0, 0, 0, 0)],
259
- ["last", corners(0, 0, 13, 13)],
260
- ["only", corners(13, 13, 13, 13)],
261
- ])("accepts a %s row capped at the row padding", (position, style) => {
262
- specimen(style, row(position, "large", "small"));
400
+ ["last", corners(0, 0, 16, 16)],
401
+ ["only", corners(16, 16, 16, 16)],
402
+ ])("accepts a %s row capped at the panelPadding role", (position, style) => {
403
+ specimen(style, row(position, "full"));
263
404
  expect(measured()?.violations).toEqual([]);
264
405
  });
265
406
 
266
407
  it("names the stop it resolved on an outer corner", () => {
267
- specimen(corners(18, 18, 0, 0), row("first", "full", "medium"));
268
- expect(measured()?.borderTopLeftRadius?.knob).toBe(
269
- "StackedRow min(radius:full, panelPadding:medium)",
270
- );
408
+ specimen(corners(16, 16, 0, 0), row("first", "full"));
409
+ expect(measured()?.borderTopLeftRadius?.knob).toBe("StackedRow min(radius:full, panelPadding)");
271
410
  });
272
411
 
273
412
  it.each([
274
- ["an uncapped outer corner", corners(16, 16, 0, 0), row("first", "large", "small")],
275
- ["a rounded inner corner", corners(13, 13, 13, 0), row("first", "large", "small")],
276
- ["a rounded middle row", corners(13, 0, 0, 0), row("middle", "large", "small")],
277
- ["a missing position", corners(13, 13, 13, 13), row("", "large", "small")],
413
+ ["an uncapped outer corner", corners(24, 24, 0, 0), row("first", "full")],
414
+ ["a rounded inner corner", corners(16, 16, 16, 0), row("first", "full")],
415
+ ["a rounded middle row", corners(16, 0, 0, 0), row("middle", "full")],
416
+ ["a missing position", corners(16, 16, 16, 16), row("", "full")],
278
417
  ])("rejects %s", (_label, style, attrs) => {
279
418
  specimen(style, attrs);
280
419
  expect(measured()?.borderRadius?.offScale).toBe(true);
281
420
  });
282
421
 
283
422
  it("says which corner is inner", () => {
284
- specimen(corners(0, 0, 13, 9), row("last", "large", "small"));
423
+ specimen(corners(0, 0, 16, 9), row("last", "full"));
285
424
  expect(measured()?.violations).toEqual([
286
- "borderBottomLeftRadius: 9px does not match StackedRow cap (13px)",
425
+ "borderBottomLeftRadius: 9px does not match StackedRow cap (16px)",
287
426
  ]);
288
- specimen(corners(9, 0, 13, 13), row("last", "large", "small")).id = "other";
427
+ specimen(corners(9, 0, 16, 16), row("last", "full")).id = "other";
289
428
  const other = audit().elements.find((el) => el.id === "other");
290
429
  expect(other?.violations).toEqual([
291
430
  "borderTopLeftRadius: 9px is an inner corner of a last StackedRow (0px)",
@@ -313,17 +452,16 @@ describe("a fixed-padding container caps at the padding it has (DG-RAD-04, MPO-5
313
452
  const own = (radius: string) => ({
314
453
  "data-constraint-container": "OwnInset",
315
454
  "data-radius-knob": radius,
316
- "data-space-knob": "medium",
317
455
  });
318
456
 
319
- it("accepts full capped at its own 13px padding whatever the space stop", () => {
320
- specimen("border-radius:13px;padding:13px", own("full"));
457
+ it("accepts full capped at its own 12px padding", () => {
458
+ specimen("border-radius:12px;padding:12px", own("full"));
321
459
  expect(measured()?.violations).toEqual([]);
322
460
  expect(measured()?.borderRadius?.knob).toBe("OwnInset min(radius:full, own padding)");
323
461
  });
324
462
 
325
463
  it("accepts the token while it fits", () => {
326
- specimen("border-radius:9px;padding:13px", own("medium"));
464
+ specimen("border-radius:8px;padding:12px", own("small"));
327
465
  expect(measured()?.violations).toEqual([]);
328
466
  });
329
467
 
@@ -335,3 +473,52 @@ describe("a fixed-padding container caps at the padding it has (DG-RAD-04, MPO-5
335
473
  expect(measured()?.borderRadius?.offScale).toBe(true);
336
474
  });
337
475
  });
476
+
477
+ describe("container and field keys on every scale", () => {
478
+ for (const [control, type] of [
479
+ [40, 15],
480
+ [44, 15],
481
+ [36, 14],
482
+ ]) {
483
+ for (const [stop, controlKey, containerKey] of [
484
+ ["none", 0, 0],
485
+ ["small", 2, 4],
486
+ ["medium", 4, 6],
487
+ ["large", 6, 8],
488
+ ["full", 12, 12],
489
+ ] as const) {
490
+ for (const part of ["Card", "PageSection", "StackedRow", "OwnInset", "TextArea"]) {
491
+ it(`${part}/${stop} reads ${control}/${type} by key`, () => {
492
+ const scale = generateScale({ control, type });
493
+ publish(scale);
494
+ const pad = scale.tokens.space.$4;
495
+ const radius = scale.tokens.radius[part === "TextArea" ? controlKey : containerKey];
496
+ specimen(`border-radius:${Math.min(radius, pad)}px;padding:${pad}px`, {
497
+ "data-constraint-container": part,
498
+ "data-radius-knob": stop,
499
+ "data-tier": "content",
500
+ "data-stack-position": "only",
501
+ });
502
+ expect(measured()?.violations).toEqual([]);
503
+ });
504
+ }
505
+ }
506
+ }
507
+
508
+ it("every serialized fallback equals its generated default token", () => {
509
+ const fallbacks = [
510
+ ...runConstraintAudit.toString().matchAll(/_(radius|space|size)\("([^"]+)", ([\d.]+)\)/g),
511
+ ];
512
+ expect(fallbacks.length).toBeGreaterThan(20);
513
+ for (const [, group, key, value] of fallbacks) {
514
+ const table = tokens[group as keyof typeof tokens] as Record<string, number>;
515
+ expect(Number(value), `${group}/${key}`).toBe(table[group === "radius" ? key : `$${key}`]);
516
+ }
517
+ });
518
+
519
+ it.each([20, 16])("container radius %i is on the generic radius scale", (radius) => {
520
+ specimen(`border-radius:${radius}px`);
521
+ expect(measured()?.borderRadius?.offScale).toBe(false);
522
+ expect(measured()?.borderRadius?.knob).toContain("container");
523
+ });
524
+ });