@multiplatform.one/theme 7.34.4 → 7.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/package.json +20 -5
  2. package/src/audit/constraintAudit.spec.ts +274 -87
  3. package/src/audit/constraintAudit.ts +135 -120
  4. package/src/audit/constraintScope.spec.ts +1 -1
  5. package/src/audit/designTokens.ts +42 -0
  6. package/src/audit/themeMatrix.spec.ts +83 -2
  7. package/src/audit/themeMatrix.ts +33 -1
  8. package/src/figma/figmaTokens.spec.ts +26 -34
  9. package/src/figma/figmaTokens.ts +26 -50
  10. package/src/fonts/createFontLoader.ts +4 -1
  11. package/src/menuRow.ts +4 -1
  12. package/src/theme/FontKnobStyles.tsx +2 -1
  13. package/src/theme/Preset.stories.tsx +0 -1
  14. package/src/theme/Preset.tsx +6 -1
  15. package/src/theme/PresetContext.ts +4 -0
  16. package/src/theme/Surface.spec.tsx +114 -67
  17. package/src/theme/Surface.stories.tsx +8 -25
  18. package/src/theme/Surface.tsx +87 -43
  19. package/src/theme/accentFloor.spec.ts +37 -11
  20. package/src/theme/activateThemeConfig.ts +22 -6
  21. package/src/theme/animations/animations.spec.ts +62 -0
  22. package/src/theme/animations/css.ts +9 -35
  23. package/src/theme/animations/reactNative.ts +98 -25
  24. package/src/theme/boundaryFloor.spec.ts +57 -0
  25. package/src/theme/chartPalette.spec.ts +56 -27
  26. package/src/theme/chartPalette.ts +33 -28
  27. package/src/theme/colorRules.spec.ts +32 -72
  28. package/src/theme/cookies.spec.ts +49 -18
  29. package/src/theme/cookies.ts +36 -7
  30. package/src/theme/cookiesPersistence.spec.ts +5 -5
  31. package/src/theme/createDefaultThemeConfig.spec.ts +182 -20
  32. package/src/theme/createDefaultThemeConfig.ts +34 -28
  33. package/src/theme/createThemes.ts +169 -218
  34. package/src/theme/defaults/accent.ts +3 -35
  35. package/src/theme/defaults/base.ts +14 -53
  36. package/src/theme/defaults/builderOptions.ts +68 -85
  37. package/src/theme/defaults/categoryFonts.ts +10 -0
  38. package/src/theme/defaults/fonts.spec.ts +84 -4
  39. package/src/theme/defaults/fonts.ts +166 -124
  40. package/src/theme/defaults/house.ts +48 -0
  41. package/src/theme/defaults/index.ts +1 -0
  42. package/src/theme/defaults/neutralRamp.spec.ts +52 -47
  43. package/src/theme/devtools/ColorLineVisualizer.tsx +6 -6
  44. package/src/theme/devtools/ThemeDevtoolsPanel.setMarkers.spec.tsx +13 -6
  45. package/src/theme/devtools/ThemeDevtoolsPanel.tsx +14 -77
  46. package/src/theme/devtools/animationOptions.spec.ts +23 -37
  47. package/src/theme/devtools/animationOptions.ts +4 -6
  48. package/src/theme/devtools/buildSnapshot.ts +0 -3
  49. package/src/theme/devtools/knobIcons.tsx +4 -24
  50. package/src/theme/focusState.spec.ts +9 -30
  51. package/src/theme/index.ts +17 -0
  52. package/src/theme/intentFill.spec.ts +42 -0
  53. package/src/theme/knobs.ts +14 -42
  54. package/src/theme/layoutTokens.ts +15 -1
  55. package/src/theme/layoutTokensHooks.spec.tsx +8 -4
  56. package/src/theme/mergedTokens.ts +20 -0
  57. package/src/theme/motion.spec.ts +84 -0
  58. package/src/theme/motion.ts +53 -0
  59. package/src/theme/motionStops.ts +17 -0
  60. package/src/theme/motionTiming.ts +59 -0
  61. package/src/theme/mpo13Proof.spec.ts +43 -46
  62. package/src/theme/mpo19Proof.spec.ts +13 -14
  63. package/src/theme/namedInkFloor.spec.ts +60 -0
  64. package/src/theme/overlaySurfaces.tsx +330 -0
  65. package/src/theme/preset.types.ts +16 -0
  66. package/src/theme/presetPreview.ts +32 -0
  67. package/src/theme/presetRoot.spec.tsx +89 -0
  68. package/src/theme/presetStore.persistence.spec.ts +141 -0
  69. package/src/theme/presetStore.spec.ts +205 -0
  70. package/src/theme/presetStore.ts +286 -0
  71. package/src/theme/presetUserSync.spec.tsx +168 -0
  72. package/src/theme/presetUserSync.ts +47 -0
  73. package/src/theme/presets.spec.ts +22 -15
  74. package/src/theme/presets.ts +22 -10
  75. package/src/theme/radiusClass.spec.ts +60 -41
  76. package/src/theme/radiusClass.ts +17 -4
  77. package/src/theme/radiusClassTable.generated.ts +3 -3
  78. package/src/theme/readableColor.spec.ts +59 -152
  79. package/src/theme/readableColor.ts +34 -174
  80. package/src/theme/recipeInputs.ts +153 -295
  81. package/src/theme/recipes.ts +58 -18
  82. package/src/theme/resolveKnobs.spec.ts +528 -588
  83. package/src/theme/resolveKnobs.ts +250 -325
  84. package/src/theme/scale/ScaleStyles.native.tsx +19 -0
  85. package/src/theme/scale/ScaleStyles.tsx +48 -0
  86. package/src/theme/scale/activeScale.ts +30 -0
  87. package/src/theme/scale/deriveRecipeInputs.ts +110 -0
  88. package/src/theme/scale/generateScale.spec.ts +260 -0
  89. package/src/theme/scale/generateScale.ts +147 -0
  90. package/src/theme/scale/index.ts +54 -0
  91. package/src/theme/scale/installScaleConfig.native.spec.ts +77 -0
  92. package/src/theme/scale/installScaleConfig.native.ts +57 -0
  93. package/src/theme/scale/installScaleConfig.ts +7 -0
  94. package/src/theme/scale/scaleAttribute.ts +21 -0
  95. package/src/theme/scale/scaleCss.probe.spec.tsx +64 -0
  96. package/src/theme/scale/scaleCss.spec.ts +132 -0
  97. package/src/theme/scale/scaleCss.ts +95 -0
  98. package/src/theme/scale/scaleFonts.ts +106 -0
  99. package/src/theme/scale/scaleInputs.ts +322 -0
  100. package/src/theme/scale/themeScales.ts +181 -0
  101. package/src/theme/scale/useActiveScale.spec.tsx +129 -0
  102. package/src/theme/scale/useActiveScale.ts +95 -0
  103. package/src/theme/schemeStore.spec.ts +81 -0
  104. package/src/theme/schemeStore.ts +67 -0
  105. package/src/theme/shared.spec.ts +7 -6
  106. package/src/theme/shared.tsx +37 -3
  107. package/src/theme/sizeLadder.spec.ts +37 -36
  108. package/src/theme/sizeRecipes.spec.ts +179 -85
  109. package/src/theme/sizeRecipes.ts +48 -25
  110. package/src/theme/subsetThemes.spec.ts +9 -3
  111. package/src/theme/switchThumb.spec.ts +69 -0
  112. package/src/theme/syntaxFloor.spec.ts +11 -8
  113. package/src/theme/theme.hydration.native.spec.tsx +51 -0
  114. package/src/theme/theme.native.tsx +59 -21
  115. package/src/theme/theme.palette.spec.tsx +76 -0
  116. package/src/theme/theme.preset.native.spec.tsx +73 -0
  117. package/src/theme/theme.provider.spec.ts +143 -1
  118. package/src/theme/theme.tsx +61 -25
  119. package/src/theme/tileSizes.ts +6 -7
  120. package/src/theme/transitionProps.native.ts +25 -2
  121. package/src/theme/transitionProps.spec.ts +34 -0
  122. package/src/theme/transitionProps.ts +26 -2
  123. package/src/theme/usePresetSwitch.native.spec.tsx +68 -0
  124. package/src/theme/usePresetSwitch.native.ts +32 -0
  125. package/src/theme/usePresetSwitch.spec.tsx +103 -0
  126. package/src/theme/usePresetSwitch.ts +54 -0
  127. package/src/theme/useResolvedKnobs.hydration.spec.tsx +2 -2
  128. package/src/theme/useResolvedKnobs.ts +23 -90
  129. package/src/theme/useResolvedKnobsBehavior.spec.tsx +25 -30
  130. package/src/theme/useSchemePreference.native.ts +33 -0
  131. package/src/theme/useSchemePreference.spec.tsx +51 -0
  132. package/src/theme/useSchemePreference.ts +32 -0
  133. package/src/theme/useSchemePreference.web.spec.tsx +32 -0
  134. package/src/theme/useTheme.native.spec.tsx +68 -0
  135. package/src/theme/useTheme.native.ts +49 -24
  136. package/src/theme/useTheme.ts +7 -4
  137. package/types/audit/constraintAudit.d.ts +6 -5
  138. package/types/audit/constraintAudit.d.ts.map +1 -1
  139. package/types/audit/designTokens.d.ts +13 -0
  140. package/types/audit/designTokens.d.ts.map +1 -0
  141. package/types/audit/themeMatrix.d.ts +10 -0
  142. package/types/audit/themeMatrix.d.ts.map +1 -1
  143. package/types/figma/figmaTokens.d.ts +11 -28
  144. package/types/figma/figmaTokens.d.ts.map +1 -1
  145. package/types/fonts/createFontLoader.d.ts.map +1 -1
  146. package/types/menuRow.d.ts +4 -1
  147. package/types/menuRow.d.ts.map +1 -1
  148. package/types/theme/FontKnobStyles.d.ts.map +1 -1
  149. package/types/theme/Preset.d.ts.map +1 -1
  150. package/types/theme/PresetContext.d.ts +4 -0
  151. package/types/theme/PresetContext.d.ts.map +1 -1
  152. package/types/theme/Surface.d.ts +57 -26
  153. package/types/theme/Surface.d.ts.map +1 -1
  154. package/types/theme/activateThemeConfig.d.ts +5 -3
  155. package/types/theme/activateThemeConfig.d.ts.map +1 -1
  156. package/types/theme/animations/css.d.ts +27 -34
  157. package/types/theme/animations/css.d.ts.map +1 -1
  158. package/types/theme/animations/reactNative.d.ts +196 -45
  159. package/types/theme/animations/reactNative.d.ts.map +1 -1
  160. package/types/theme/chartPalette.d.ts +11 -3
  161. package/types/theme/chartPalette.d.ts.map +1 -1
  162. package/types/theme/colorRules.d.ts +1 -1
  163. package/types/theme/cookies.d.ts +12 -1
  164. package/types/theme/cookies.d.ts.map +1 -1
  165. package/types/theme/createDefaultThemeConfig.d.ts +13 -13
  166. package/types/theme/createDefaultThemeConfig.d.ts.map +1 -1
  167. package/types/theme/createThemes.d.ts +58 -2
  168. package/types/theme/createThemes.d.ts.map +1 -1
  169. package/types/theme/defaults/accent.d.ts.map +1 -1
  170. package/types/theme/defaults/base.d.ts.map +1 -1
  171. package/types/theme/defaults/builderOptions.d.ts.map +1 -1
  172. package/types/theme/defaults/categoryFonts.d.ts +10 -0
  173. package/types/theme/defaults/categoryFonts.d.ts.map +1 -1
  174. package/types/theme/defaults/fonts.d.ts +330 -30
  175. package/types/theme/defaults/fonts.d.ts.map +1 -1
  176. package/types/theme/defaults/house.d.ts +8 -0
  177. package/types/theme/defaults/house.d.ts.map +1 -0
  178. package/types/theme/defaults/index.d.ts +1 -0
  179. package/types/theme/defaults/index.d.ts.map +1 -1
  180. package/types/theme/devtools/ThemeDevtoolsPanel.d.ts +3 -3
  181. package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
  182. package/types/theme/devtools/animationOptions.d.ts.map +1 -1
  183. package/types/theme/devtools/buildSnapshot.d.ts.map +1 -1
  184. package/types/theme/devtools/knobIcons.d.ts +0 -3
  185. package/types/theme/devtools/knobIcons.d.ts.map +1 -1
  186. package/types/theme/index.d.ts +11 -0
  187. package/types/theme/index.d.ts.map +1 -1
  188. package/types/theme/knobs.d.ts +8 -36
  189. package/types/theme/knobs.d.ts.map +1 -1
  190. package/types/theme/layoutTokens.d.ts +13 -1
  191. package/types/theme/layoutTokens.d.ts.map +1 -1
  192. package/types/theme/mergedTokens.d.ts +11 -0
  193. package/types/theme/mergedTokens.d.ts.map +1 -0
  194. package/types/theme/motion.d.ts +17 -0
  195. package/types/theme/motion.d.ts.map +1 -0
  196. package/types/theme/motionStops.d.ts +3 -0
  197. package/types/theme/motionStops.d.ts.map +1 -0
  198. package/types/theme/motionTiming.d.ts +36 -0
  199. package/types/theme/motionTiming.d.ts.map +1 -0
  200. package/types/theme/overlaySurfaces.d.ts +70 -0
  201. package/types/theme/overlaySurfaces.d.ts.map +1 -0
  202. package/types/theme/preset.types.d.ts +16 -0
  203. package/types/theme/preset.types.d.ts.map +1 -1
  204. package/types/theme/presetPreview.d.ts +14 -0
  205. package/types/theme/presetPreview.d.ts.map +1 -0
  206. package/types/theme/presetStore.d.ts +40 -0
  207. package/types/theme/presetStore.d.ts.map +1 -0
  208. package/types/theme/presetUserSync.d.ts +7 -0
  209. package/types/theme/presetUserSync.d.ts.map +1 -0
  210. package/types/theme/presets.d.ts +12 -4
  211. package/types/theme/presets.d.ts.map +1 -1
  212. package/types/theme/radiusClass.d.ts +5 -2
  213. package/types/theme/radiusClass.d.ts.map +1 -1
  214. package/types/theme/radiusClassTable.generated.d.ts +3 -3
  215. package/types/theme/readableColor.d.ts +11 -49
  216. package/types/theme/readableColor.d.ts.map +1 -1
  217. package/types/theme/recipeInputs.d.ts +39 -70
  218. package/types/theme/recipeInputs.d.ts.map +1 -1
  219. package/types/theme/recipes.d.ts +79 -18
  220. package/types/theme/recipes.d.ts.map +1 -1
  221. package/types/theme/resolveKnobs.d.ts +64 -32
  222. package/types/theme/resolveKnobs.d.ts.map +1 -1
  223. package/types/theme/scale/ScaleStyles.d.ts +18 -0
  224. package/types/theme/scale/ScaleStyles.d.ts.map +1 -0
  225. package/types/theme/scale/ScaleStyles.native.d.ts +10 -0
  226. package/types/theme/scale/ScaleStyles.native.d.ts.map +1 -0
  227. package/types/theme/scale/activeScale.d.ts +13 -0
  228. package/types/theme/scale/activeScale.d.ts.map +1 -0
  229. package/types/theme/scale/deriveRecipeInputs.d.ts +10 -0
  230. package/types/theme/scale/deriveRecipeInputs.d.ts.map +1 -0
  231. package/types/theme/scale/generateScale.d.ts +38 -0
  232. package/types/theme/scale/generateScale.d.ts.map +1 -0
  233. package/types/theme/scale/index.d.ts +17 -0
  234. package/types/theme/scale/index.d.ts.map +1 -0
  235. package/types/theme/scale/installScaleConfig.d.ts +4 -0
  236. package/types/theme/scale/installScaleConfig.d.ts.map +1 -0
  237. package/types/theme/scale/installScaleConfig.native.d.ts +13 -0
  238. package/types/theme/scale/installScaleConfig.native.d.ts.map +1 -0
  239. package/types/theme/scale/scaleAttribute.d.ts +14 -0
  240. package/types/theme/scale/scaleAttribute.d.ts.map +1 -0
  241. package/types/theme/scale/scaleCss.d.ts +25 -0
  242. package/types/theme/scale/scaleCss.d.ts.map +1 -0
  243. package/types/theme/scale/scaleFonts.d.ts +16 -0
  244. package/types/theme/scale/scaleFonts.d.ts.map +1 -0
  245. package/types/theme/scale/scaleInputs.d.ts +66 -0
  246. package/types/theme/scale/scaleInputs.d.ts.map +1 -0
  247. package/types/theme/scale/themeScales.d.ts +52 -0
  248. package/types/theme/scale/themeScales.d.ts.map +1 -0
  249. package/types/theme/scale/useActiveScale.d.ts +31 -0
  250. package/types/theme/scale/useActiveScale.d.ts.map +1 -0
  251. package/types/theme/schemeStore.d.ts +6 -0
  252. package/types/theme/schemeStore.d.ts.map +1 -0
  253. package/types/theme/shared.d.ts +18 -0
  254. package/types/theme/shared.d.ts.map +1 -1
  255. package/types/theme/sizeRecipes.d.ts +20 -15
  256. package/types/theme/sizeRecipes.d.ts.map +1 -1
  257. package/types/theme/theme.d.ts +2 -1
  258. package/types/theme/theme.d.ts.map +1 -1
  259. package/types/theme/theme.native.d.ts +10 -1
  260. package/types/theme/theme.native.d.ts.map +1 -1
  261. package/types/theme/tileSizes.d.ts +6 -7
  262. package/types/theme/tileSizes.d.ts.map +1 -1
  263. package/types/theme/transitionProps.d.ts +3 -1
  264. package/types/theme/transitionProps.d.ts.map +1 -1
  265. package/types/theme/transitionProps.native.d.ts +3 -1
  266. package/types/theme/transitionProps.native.d.ts.map +1 -1
  267. package/types/theme/usePresetSwitch.d.ts +10 -0
  268. package/types/theme/usePresetSwitch.d.ts.map +1 -0
  269. package/types/theme/usePresetSwitch.native.d.ts +10 -0
  270. package/types/theme/usePresetSwitch.native.d.ts.map +1 -0
  271. package/types/theme/useResolvedKnobs.d.ts +13 -13
  272. package/types/theme/useResolvedKnobs.d.ts.map +1 -1
  273. package/types/theme/useSchemePreference.d.ts +8 -0
  274. package/types/theme/useSchemePreference.d.ts.map +1 -0
  275. package/types/theme/useSchemePreference.native.d.ts +8 -0
  276. package/types/theme/useSchemePreference.native.d.ts.map +1 -0
  277. package/types/theme/useTheme.d.ts +4 -2
  278. package/types/theme/useTheme.d.ts.map +1 -1
  279. package/types/theme/useTheme.native.d.ts +10 -1
  280. package/types/theme/useTheme.native.d.ts.map +1 -1
@@ -1,5 +1,6 @@
1
1
  import * as Colors from "@tamagui/colors";
2
- import { defaultConfig } from "@tamagui/config/v5";
2
+ import { generateScale } from "../theme/scale/generateScale";
3
+ import { defaultScaleInputs } from "../theme/scale/scaleInputs";
3
4
  import { describe, expect, it } from "vitest";
4
5
  import { normalizeToHex } from "../theme/colorRules";
5
6
  import { defaultAccentTheme } from "../theme/defaults/accent";
@@ -57,8 +58,12 @@ describe("buildFigmaTokens", () => {
57
58
  expect(sameColor(built[i], source[i])).toBe(true);
58
59
  }
59
60
  }
60
- expect(defaultAccentTheme.lightPalette).toContain(result.palette.themes.accent.light[0]);
61
- expect(defaultAccentTheme.darkPalette).toContain(result.palette.themes.accent.dark[0]);
61
+ expect(
62
+ sameColor(defaultAccentTheme.lightPalette[0], result.palette.themes.accent.light[0]),
63
+ ).toBe(true);
64
+ expect(
65
+ sameColor(defaultAccentTheme.darkPalette[0], result.palette.themes.accent.dark[0]),
66
+ ).toBe(true);
62
67
  });
63
68
 
64
69
  it("derives intent ramps from @tamagui/colors (error=red, success=green, warning=yellow)", () => {
@@ -96,7 +101,7 @@ describe("buildFigmaTokens", () => {
96
101
  // borderColor back off color5, the background states no longer forced.
97
102
  expect(result.semantic.borderColor?.alias).toBe("color4");
98
103
  expect(result.semantic.textMuted?.alias).toBe("color11");
99
- expect(result.semantic.placeholderColor?.alias).toBe("color10");
104
+ expect(result.semantic.placeholderColor?.alias).toBe("color9");
100
105
  expect(result.semantic.surfaceMuted?.alias).toBe("color10");
101
106
  expect(result.semantic.borderStrong?.alias).toBe("color7");
102
107
  });
@@ -135,17 +140,12 @@ describe("buildFigmaTokens", () => {
135
140
  });
136
141
 
137
142
  describe("sizing", () => {
138
- it("mirrors the v5 defaultConfig tokens with figma-safe names", () => {
139
- // Read against the ERA the package pins (MPO-19 X13), not against a
140
- // transcribed table: v5 keys carry the `$`, values are raw numbers.
141
- const size = defaultConfig.tokens.size as Record<string, number>;
143
+ it("mirrors the generated default tokens with figma-safe names", () => {
144
+ const tokens = generateScale(defaultScaleInputs).tokens;
145
+ const size = tokens.size as Record<string, number>;
142
146
  expect(result.sizing["size-true"]).toBe(size.$true);
143
- expect(result.sizing["radius-4"]).toBe(
144
- (defaultConfig.tokens.radius as Record<string, number>)["4"],
145
- );
146
- expect(result.sizing["space-1-5"]).toBe(
147
- (defaultConfig.tokens.space as Record<string, number>)["$1.5"],
148
- );
147
+ expect(result.sizing["radius-4"]).toBe((tokens.radius as Record<string, number>)["4"]);
148
+ expect(result.sizing["space-1-5"]).toBe((tokens.space as Record<string, number>)["$1.5"]);
149
149
  });
150
150
 
151
151
  it("exports knob-shaped borderWidth values including the 0.5 hairline", () => {
@@ -188,31 +188,23 @@ describe("buildFigmaTokens", () => {
188
188
  expect(modes.large.sizeToken).toBe("$4");
189
189
  });
190
190
 
191
- it("exports fontWeight, space, size and cornerSmoothing modes", () => {
191
+ it("exports fontWeight and cornerSmoothing modes", () => {
192
192
  expect(result.knobs.fontWeight.modes).toEqual({ regular: "400", bold: "700" });
193
- expect(result.knobs.space.modes.medium.paddingFigmaName).toBe("space-4");
194
- expect(result.knobs.space.modes.medium.gapFigmaName).toBe("space-4");
195
- expect(result.knobs.space.modes.medium.gapLgFigmaName).toBe("space-5");
196
- expect(result.knobs.size.modes).toEqual({
197
- small: { token: "$3", figmaName: "size-3" },
198
- medium: { token: "$4", figmaName: "size-4" },
199
- large: { token: "$5", figmaName: "size-5" },
200
- });
201
193
  expect(result.knobs.cornerSmoothing.modes).toEqual({ round: 0, smooth: 0.6 });
202
194
  });
203
195
 
204
- it("exports density compact stepping for space only (size is uncoupled)", () => {
205
- expect(result.knobs.density.default).toBe("comfortable");
206
- expect(result.knobs.density.compact.size).toEqual({
207
- small: "small",
208
- medium: "medium",
209
- large: "large",
210
- });
211
- expect(result.knobs.density.compact.space).toEqual({
212
- small: "small",
213
- medium: "small",
214
- large: "medium",
196
+ it("exports the spacing roles and the default size token, not size/space/density modes", () => {
197
+ expect(result.knobs.spacing).toEqual({
198
+ gapSm: { token: "$2", figmaName: "space-2" },
199
+ gap: { token: "$3", figmaName: "space-3" },
200
+ gapLg: { token: "$5", figmaName: "space-5" },
201
+ panelPadding: { token: "$4", figmaName: "space-4" },
202
+ featurePadding: { token: "$6", figmaName: "space-6" },
215
203
  });
204
+ expect(result.knobs.sizeToken).toEqual({ token: "$true", figmaName: "size-true" });
205
+ expect(result.knobs).not.toHaveProperty("space");
206
+ expect(result.knobs).not.toHaveProperty("size");
207
+ expect(result.knobs).not.toHaveProperty("density");
216
208
  });
217
209
  });
218
210
 
@@ -13,8 +13,8 @@
13
13
  * the built base themes (a key only becomes a `colorN` alias when light
14
14
  * AND dark agree on the step index; otherwise raw per-scheme values are
15
15
  * exported).
16
- * - Sizing: `tokens` from `@tamagui/themes` (the tokens
17
- * `createDefaultThemeConfig` installs).
16
+ * - Sizing: the default generated scale (`generateScale`), the tokens
17
+ * `createDefaultThemeConfig` installs.
18
18
  * - Knob outputs: `resolveKnobs()` is executed per knob mode — the export
19
19
  * shows exactly what the runtime resolver produces.
20
20
  * - Typography: the built `defaultHeadingFont` / `defaultBodyFont` tables
@@ -25,14 +25,18 @@
25
25
  * stamps `generatedAt` when writing to disk.
26
26
  */
27
27
 
28
- import { tokens } from "@tamagui/themes";
28
+ import { createTokens } from "@tamagui/web";
29
29
  import { createThemesBuilder, tintHueNames } from "../theme/createThemes";
30
30
  import { defaultAccentTheme } from "../theme/defaults/accent";
31
31
  import { defaultBaseTheme } from "../theme/defaults/base";
32
32
  import { defaultBuilderOptions } from "../theme/defaults/builderOptions";
33
33
  import { defaultBodyFont, defaultHeadingFont } from "../theme/defaults/fonts";
34
34
  import { type Knobs, defaultKnobs } from "../theme/knobs";
35
- import { compactSpaceMap, resolveKnobs } from "../theme/resolveKnobs";
35
+ import { resolveKnobs, spacingRoles } from "../theme/resolveKnobs";
36
+ import { generateScale } from "../theme/scale/generateScale";
37
+ import { defaultScaleInputs } from "../theme/scale/scaleInputs";
38
+
39
+ const tokens = createTokens(generateScale(defaultScaleInputs).tokens);
36
40
 
37
41
  // ── Names / constants ─────────────────────────────────────────
38
42
 
@@ -105,13 +109,10 @@ export interface FigmaElevationMode {
105
109
  px: number | null;
106
110
  }
107
111
 
108
- export interface FigmaSpaceMode {
109
- paddingToken: string;
110
- paddingFigmaName: string;
111
- gapToken: string;
112
- gapFigmaName: string;
113
- gapLgToken: string;
114
- gapLgFigmaName: string;
112
+ /** One spacing role: a fixed `$space` key whose px the scale supplies. */
113
+ export interface FigmaSpacingRole {
114
+ token: string;
115
+ figmaName: string;
115
116
  }
116
117
 
117
118
  export interface FigmaKnobsExport {
@@ -119,14 +120,11 @@ export interface FigmaKnobsExport {
119
120
  borderWidth: { default: Knobs["borderWidth"]; modes: Record<string, number> };
120
121
  elevation: { default: Knobs["elevation"]; modes: Record<string, FigmaElevationMode> };
121
122
  fontWeight: { default: Knobs["fontWeight"]; modes: Record<string, string> };
122
- space: { default: Knobs["space"]; modes: Record<string, FigmaSpaceMode> };
123
- size: { default: Knobs["size"]; modes: Record<string, { token: string; figmaName: string }> };
124
123
  cornerSmoothing: { default: string; modes: Record<string, number> };
125
- density: {
126
- default: Knobs["density"];
127
- /** compact steps space only (applyDensity); size is an independent axis. */
128
- compact: { size: Record<string, string>; space: Record<string, string> };
129
- };
124
+ /** SP-GAP roles: fixed `$space` keys, not knob modes. */
125
+ spacing: Record<keyof typeof spacingRoles, FigmaSpacingRole>;
126
+ /** The control size token at the default (`$true`); a use site picks another with `size`. */
127
+ sizeToken: FigmaSpacingRole;
130
128
  }
131
129
 
132
130
  export interface FigmaFontExport {
@@ -288,7 +286,7 @@ function buildSizing(borderWidthModes: Record<string, number>): Record<string, n
288
286
  ];
289
287
  for (const [prefix, group] of groups) {
290
288
  if (prefix !== "zIndex" && Object.keys(group).length === 0) {
291
- throw new Error(`figmaTokens: @tamagui/themes tokens.${prefix} is empty`);
289
+ throw new Error(`figmaTokens: generated tokens.${prefix} is empty`);
292
290
  }
293
291
  for (const [key, val] of Object.entries(group)) {
294
292
  sizing[figmaTokenName(prefix, key)] = val;
@@ -306,8 +304,6 @@ const RADIUS_KNOB_MODES: Knobs["borderRadius"][] = ["none", "small", "medium", "
306
304
  const BORDER_WIDTH_KNOB_MODES: Knobs["borderWidth"][] = ["none", "small", "medium", "large"];
307
305
  const ELEVATION_KNOB_MODES: Knobs["elevation"][] = ["none", "small", "medium", "large"];
308
306
  const FONT_WEIGHT_KNOB_MODES: Knobs["fontWeight"][] = ["regular", "bold"];
309
- const SPACE_KNOB_MODES: Knobs["space"][] = ["small", "medium", "large"];
310
- const SIZE_KNOB_MODES: Knobs["size"][] = ["small", "medium", "large"];
311
307
 
312
308
  function radiusPx(token: string): number {
313
309
  const radius = unwrapTokenGroup(tokens.radius);
@@ -373,47 +369,27 @@ function buildKnobs(): FigmaKnobsExport {
373
369
  fontWeight[mode] = String(knobProps.textWeight.fontWeight);
374
370
  }
375
371
 
376
- const space: Record<string, FigmaSpaceMode> = {};
377
- for (const mode of SPACE_KNOB_MODES) {
378
- const { knobProps } = resolveKnobs({ ...defaultKnobs, space: mode });
379
- const paddingToken = String(knobProps.panelPadding.padding);
380
- const gapToken = String(knobProps.gap.gap);
381
- const gapLgToken = String(knobProps.gapLg.gap);
382
- space[mode] = {
383
- paddingToken,
384
- paddingFigmaName: tokenToFigma(paddingToken, "space"),
385
- gapToken,
386
- gapFigmaName: tokenToFigma(gapToken, "space"),
387
- gapLgToken,
388
- gapLgFigmaName: tokenToFigma(gapLgToken, "space"),
389
- };
372
+ const spacing = {} as Record<keyof typeof spacingRoles, FigmaSpacingRole>;
373
+ for (const [role, token] of Object.entries(spacingRoles) as [
374
+ keyof typeof spacingRoles,
375
+ string,
376
+ ][]) {
377
+ spacing[role] = { token, figmaName: tokenToFigma(token, "space") };
390
378
  }
391
379
 
392
- const size: Record<string, { token: string; figmaName: string }> = {};
393
- for (const mode of SIZE_KNOB_MODES) {
394
- const { knobProps } = resolveKnobs({ ...defaultKnobs, size: mode });
395
- const token = String(knobProps.sizeToken);
396
- size[mode] = { token, figmaName: tokenToFigma(token, "size") };
397
- }
380
+ const sizeToken = String(resolveKnobs(defaultKnobs).knobProps.sizeToken);
398
381
 
399
382
  return {
400
383
  borderRadius: { default: defaultKnobs.borderRadius, modes: borderRadius },
401
384
  borderWidth: { default: defaultKnobs.borderWidth, modes: borderWidth },
402
385
  elevation: { default: defaultKnobs.elevation, modes: elevation },
403
386
  fontWeight: { default: defaultKnobs.fontWeight, modes: fontWeight },
404
- space: { default: defaultKnobs.space, modes: space },
405
- size: { default: defaultKnobs.size, modes: size },
406
387
  cornerSmoothing: {
407
388
  default: defaultKnobs.cornerSmoothing ?? "round",
408
389
  modes: { ...FIGMA_CORNER_SMOOTHING },
409
390
  },
410
- density: {
411
- default: defaultKnobs.density,
412
- compact: {
413
- size: { small: "small", medium: "medium", large: "large" },
414
- space: { ...compactSpaceMap },
415
- },
416
- },
391
+ spacing,
392
+ sizeToken: { token: sizeToken, figmaName: tokenToFigma(sizeToken, "size") },
417
393
  };
418
394
  }
419
395
 
@@ -3,7 +3,10 @@ import type { FontLoader, FontLoaderConfig } from "./types";
3
3
  export function createFontLoader(config: FontLoaderConfig): FontLoader {
4
4
  const fontMap: Record<string, number> = {};
5
5
  if (config.inter) {
6
- fontMap.Inter = require("@tamagui/font-inter/otf/Inter-Medium.otf");
6
+ fontMap.Inter = require("@tamagui/font-inter/otf/Inter-Regular.otf");
7
+ fontMap.InterMedium = require("@tamagui/font-inter/otf/Inter-Medium.otf");
8
+ fontMap.InterSemiBold = require("@tamagui/font-inter/otf/Inter-SemiBold.otf");
9
+ fontMap.InterExtraBold = require("@tamagui/font-inter/otf/Inter-ExtraBold.otf");
7
10
  fontMap.InterBold = require("@tamagui/font-inter/otf/Inter-Bold.otf");
8
11
  }
9
12
  for (const font of config.fonts) {
package/src/menuRow.ts CHANGED
@@ -18,7 +18,10 @@
18
18
  * - color ramps: menus highlight with `componentColors.interactive.*`,
19
19
  * form fields with `formInputColors.background.*` (package-boundary rule);
20
20
  * - cursor: `pointer` for menu items, `default` for listbox options;
21
- * - vertical sizing: height/minHeight from the size token, or row padding;
21
+ * - vertical sizing: spread `knobProps.menuRow` (the `menuRow` recipe
22
+ * family — one size key below the control token, 36 at default, 44 on
23
+ * touch, padX `$3`); Select reads `knobProps.menuRow.minHeight` as its row
24
+ * pitch. MentionInput keeps its own suggestion pitch;
22
25
  * - in-row gap and the state-variant model (selected/active/disabled).
23
26
  */
24
27
  export const menuRowFrame = {
@@ -209,6 +209,7 @@ export function FontKnobStyles() {
209
209
  const knobs = presetCtx ? { ...presetCtx.preset.knobs, ...presetCtx.overrides } : defaultKnobs;
210
210
  const headingFont = knobs.headingFont ?? defaultKnobs.headingFont;
211
211
  const bodyFont = knobs.bodyFont ?? defaultKnobs.bodyFont;
212
+ const scaleId = presetCtx?.scaleId;
212
213
 
213
214
  useEffect(() => {
214
215
  if (typeof document === "undefined") return;
@@ -224,7 +225,7 @@ export function FontKnobStyles() {
224
225
  document.head.appendChild(tag);
225
226
  }
226
227
  if (tag.textContent !== css) tag.textContent = css;
227
- }, [headingFont, bodyFont]);
228
+ }, [headingFont, bodyFont, scaleId]);
228
229
 
229
230
  useEffect(
230
231
  () => () => {
@@ -22,7 +22,6 @@ function Probe({ label }: { label: string }) {
22
22
  "data-border-radius": String(knobProps.borderRadius.borderRadius),
23
23
  "data-border-width": String(knobProps.surface.borderWidth),
24
24
  "data-fill-style": knobProps.outlined ? "outlined" : "filled",
25
- "data-space": String(knobProps.space),
26
25
  } as Record<string, unknown>;
27
26
  return (
28
27
  <YStack
@@ -51,6 +51,8 @@ export function Preset({
51
51
  knobs: { ...base.knobs, ...parentCtx.overrides, ...overrides },
52
52
  },
53
53
  overrides: Object.keys(mergedOverrides).length > 0 ? mergedOverrides : undefined,
54
+ presetName: parentCtx.presetName,
55
+ scaleId: parentCtx.scaleId,
54
56
  };
55
57
  }
56
58
 
@@ -72,14 +74,17 @@ export function Preset({
72
74
  base = freshDefault;
73
75
  }
74
76
 
77
+ const scope = { presetName: parentCtx?.presetName, scaleId: parentCtx?.scaleId };
78
+
75
79
  if (overrides) {
76
80
  return {
77
81
  preset: { ...base, knobs: { ...base.knobs, ...overrides } },
78
82
  overrides,
83
+ ...scope,
79
84
  };
80
85
  }
81
86
 
82
- return { preset: base };
87
+ return { preset: base, ...scope };
83
88
  }, [cascade, parentCtx, presetName, overrides]);
84
89
 
85
90
  let element = <PresetContext.Provider value={ctxValue}>{children}</PresetContext.Provider>;
@@ -6,6 +6,10 @@ import type { ThemeName } from "@tamagui/web";
6
6
  export interface PresetContextValue {
7
7
  preset: Preset;
8
8
  overrides?: Partial<Knobs>;
9
+ /** Name of the provider's active preset, whose scale is installed. */
10
+ presetName?: string;
11
+ /** Id of the installed token scale; changes after a switch re-render every knob reader. */
12
+ scaleId?: string;
9
13
  }
10
14
 
11
15
  export const PresetContext = createContext<PresetContextValue | null>(null);
@@ -1,16 +1,23 @@
1
1
  import { TestProviders } from "@multiplatform.one/test-utils";
2
2
  /**
3
- * Surface context — nested size/density step-down (LC-68).
3
+ * Surface context: a forwarded size token with a step-down ceiling (LC-78).
4
4
  */
5
5
 
6
6
  import { render, renderHook } from "@testing-library/react";
7
7
  import { createElement, type ReactNode } from "react";
8
8
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
9
+ import type { SizeTokens } from "tamagui";
10
+ import { generateScale } from "./scale/generateScale";
11
+ import { defaultScaleInputs } from "./scale/scaleInputs";
9
12
  import {
10
13
  Surface,
11
14
  SurfaceContext,
12
- clampSurfaceDensity,
13
15
  clampSurfaceSize,
16
+ compareSizeTokens,
17
+ sizeTokenStep,
18
+ sizeTokenBelow,
19
+ sizeTokenStop,
20
+ surfaceSizeRank,
14
21
  type SurfaceIntent,
15
22
  } from "./Surface";
16
23
  import { useResolvedKnobs } from "./useResolvedKnobs";
@@ -44,114 +51,154 @@ function Probe({ onRead }: { onRead: (intent: SurfaceIntent) => void }) {
44
51
  return null;
45
52
  }
46
53
 
47
- function renderNested(
48
- outer: { size?: "small" | "medium" | "large"; density?: "compact" | "comfortable" },
49
- inner: { size?: "small" | "medium" | "large"; density?: "compact" | "comfortable" },
50
- ): SurfaceIntent {
54
+ function renderNested(outer: SizeTokens | undefined, inner: SizeTokens | undefined): SurfaceIntent {
51
55
  let intent: SurfaceIntent | undefined;
52
56
  render(
53
57
  createElement(
54
58
  Surface,
55
- outer,
56
- createElement(Surface, inner, createElement(Probe, { onRead: (value) => (intent = value) })),
59
+ { size: outer },
60
+ createElement(
61
+ Surface,
62
+ { size: inner },
63
+ createElement(Probe, { onRead: (value) => (intent = value) }),
64
+ ),
57
65
  ),
58
66
  );
59
67
  if (!intent) throw new Error("Surface probe did not render");
60
68
  return intent;
61
69
  }
62
70
 
63
- describe("clampSurfaceSize", () => {
64
- it("unset parent is no ceiling", () => {
65
- expect(clampSurfaceSize("large", "unset")).toBe("large");
66
- expect(clampSurfaceSize("unset", "unset")).toBe("unset");
71
+ const scale = generateScale(defaultScaleInputs).tokens;
72
+
73
+ describe("surfaceSizeRank", () => {
74
+ it("ranks size keys by the scale's sorted $size ladder; $true ranks with $4", () => {
75
+ expect(surfaceSizeRank("$2")!).toBeLessThan(surfaceSizeRank("$3")!);
76
+ expect(surfaceSizeRank("$3")!).toBeLessThan(surfaceSizeRank("$3.5")!);
77
+ expect(surfaceSizeRank("$3.5")!).toBeLessThan(surfaceSizeRank("$4")!);
78
+ expect(surfaceSizeRank("$true")).toBe(surfaceSizeRank("$4"));
79
+ expect(surfaceSizeRank("$5")!).toBeGreaterThan(surfaceSizeRank("$4")!);
67
80
  });
68
81
 
69
- it("unset requested inherits parent", () => {
70
- expect(clampSurfaceSize("unset", "small")).toBe("small");
71
- expect(clampSurfaceSize("unset", "large")).toBe("large");
82
+ it("does not rank anything that is not a size key (no sm/md/lg aliases)", () => {
83
+ for (const alias of ["sm", "small", "md", "medium", "lg", "large"]) {
84
+ expect(surfaceSizeRank(alias as SizeTokens), alias).toBeUndefined();
85
+ }
72
86
  });
87
+ });
73
88
 
74
- it("never exceeds parent", () => {
75
- expect(clampSurfaceSize("large", "small")).toBe("small");
76
- expect(clampSurfaceSize("medium", "small")).toBe("small");
77
- expect(clampSurfaceSize("large", "medium")).toBe("medium");
89
+ describe("compareSizeTokens", () => {
90
+ it("orders size keys on the ladder and treats anything else as equal", () => {
91
+ expect(compareSizeTokens("$3", "$true")).toBeLessThan(0);
92
+ expect(compareSizeTokens("$5", "$4")).toBeGreaterThan(0);
93
+ expect(compareSizeTokens("$4", "$true")).toBe(0);
94
+ expect(compareSizeTokens("small", "$true")).toBe(0);
78
95
  });
79
96
 
80
- it("tighter than parent is allowed", () => {
81
- expect(clampSurfaceSize("small", "large")).toBe("small");
82
- expect(clampSurfaceSize("medium", "large")).toBe("medium");
83
- expect(clampSurfaceSize("small", "medium")).toBe("small");
97
+ it("sizeTokenStep says which side of the default control size a token sits", () => {
98
+ expect(sizeTokenStep("$3")).toBe(-1);
99
+ expect(sizeTokenStep("$true")).toBe(0);
100
+ expect(sizeTokenStep("$4")).toBe(0);
101
+ expect(sizeTokenStep("$6")).toBe(1);
102
+ expect([sizeTokenStop("$3"), sizeTokenStop("$true"), sizeTokenStop("$5")]).toEqual([
103
+ "small",
104
+ "medium",
105
+ "large",
106
+ ]);
84
107
  });
85
108
  });
86
109
 
87
- describe("clampSurfaceDensity", () => {
110
+ describe("clampSurfaceSize", () => {
88
111
  it("unset parent is no ceiling", () => {
89
- expect(clampSurfaceDensity("comfortable", "unset")).toBe("comfortable");
112
+ expect(clampSurfaceSize("$5", "unset")).toBe("$5");
113
+ expect(clampSurfaceSize("unset", "unset")).toBe("unset");
90
114
  });
91
115
 
92
116
  it("unset requested inherits parent", () => {
93
- expect(clampSurfaceDensity("unset", "compact")).toBe("compact");
117
+ expect(clampSurfaceSize("unset", "$3")).toBe("$3");
118
+ expect(clampSurfaceSize("unset", "$5")).toBe("$5");
94
119
  });
95
120
 
96
- it("never exceeds parent; tighter is allowed", () => {
97
- expect(clampSurfaceDensity("comfortable", "compact")).toBe("compact");
98
- expect(clampSurfaceDensity("compact", "comfortable")).toBe("compact");
121
+ it("never exceeds parent", () => {
122
+ expect(clampSurfaceSize("$5", "$3")).toBe("$3");
123
+ expect(clampSurfaceSize("$4", "$3")).toBe("$3");
124
+ expect(clampSurfaceSize("$true", "$3")).toBe("$3");
125
+ expect(clampSurfaceSize("$5", "$true")).toBe("$true");
126
+ });
127
+
128
+ it("smaller than parent is allowed", () => {
129
+ expect(clampSurfaceSize("$3", "$5")).toBe("$3");
130
+ expect(clampSurfaceSize("$2", "$true")).toBe("$2");
131
+ expect(clampSurfaceSize("$true", "$4")).toBe("$true");
132
+ });
133
+
134
+ it("an unranked request under a ceiling takes the ceiling", () => {
135
+ expect(clampSurfaceSize(40 as SizeTokens, "$3")).toBe("$3");
99
136
  });
100
137
  });
101
138
 
102
- describe("Surface nesting (LC-68)", () => {
103
- it("nested Surface size large inside small resolves small", () => {
104
- expect(renderNested({ size: "small" }, { size: "large" }).size).toBe("small");
139
+ describe("Surface nesting (LC-78)", () => {
140
+ it("nested $5 inside $3 resolves $3", () => {
141
+ expect(renderNested("$3", "$5").size).toBe("$3");
105
142
  });
106
143
 
107
- it("nested comfortable inside compact resolves compact", () => {
108
- expect(renderNested({ density: "compact" }, { density: "comfortable" }).density).toBe(
109
- "compact",
110
- );
144
+ it("nested $3 inside $5 resolves $3 (smaller allowed)", () => {
145
+ expect(renderNested("$5", "$3").size).toBe("$3");
111
146
  });
112
147
 
113
- it("nested small inside large resolves small (tighter allowed)", () => {
114
- expect(renderNested({ size: "large" }, { size: "small" }).size).toBe("small");
148
+ it("an unset inner Surface inherits the outer token", () => {
149
+ expect(renderNested("$3", undefined).size).toBe("$3");
150
+ });
151
+
152
+ it("the context carries only a size: no density", () => {
153
+ expect(renderNested("$3", undefined)).not.toHaveProperty("density");
115
154
  });
116
155
  });
117
156
 
118
157
  describe("useResolvedKnobs inside Surface", () => {
119
- it("size=small density=compact → sizeToken $3, compact gap, small desktop control height", () => {
120
- const wrapper = ({ children }: { children: ReactNode }) =>
121
- createElement(
122
- TestProviders,
123
- null,
124
- createElement(Surface, { size: "small", density: "compact" }, children),
125
- );
126
- const { result } = renderHook(() => useResolvedKnobs(), { wrapper });
127
- expect(result.current.knobProps.size).toBe("small");
158
+ const inSurface =
159
+ (size: SizeTokens) =>
160
+ ({ children }: { children: ReactNode }) =>
161
+ createElement(TestProviders, null, createElement(Surface, { size }, children));
162
+
163
+ it('<Surface size="$3"> renders controls at the scale\'s $3 height', () => {
164
+ const { result } = renderHook(() => useResolvedKnobs(), { wrapper: inSurface("$3") });
128
165
  expect(result.current.knobProps.sizeToken).toBe("$3");
129
- expect(result.current.knobProps.density).toBe("compact");
130
- expect(result.current.knobProps.gap).toEqual({ gap: "$2" });
131
- expect(result.current.knobProps.control.height).toBe(36);
166
+ expect(result.current.knobProps.control.height).toBe(scale.size.$3);
167
+ expect(result.current.knobProps.label.fontSize).toBe("$3");
132
168
  });
133
169
 
134
- it("a large size request inside a medium Surface resolves medium", () => {
135
- const wrapper = ({ children }: { children: ReactNode }) =>
136
- createElement(TestProviders, null, createElement(Surface, { size: "medium" }, children));
137
- const { result } = renderHook(() => useResolvedKnobs({ size: "$5" }), { wrapper });
138
- expect(result.current.knobProps.size).toBe("medium");
139
- expect(result.current.knobProps.sizeToken).toBe("$4");
170
+ it('a size="$5" request inside <Surface size="$3"> clamps to $3', () => {
171
+ const { result } = renderHook(() => useResolvedKnobs({ size: "$5" }), {
172
+ wrapper: inSurface("$3"),
173
+ });
174
+ expect(result.current.knobProps.sizeToken).toBe("$3");
175
+ expect(result.current.knobProps.control.height).toBe(scale.size.$3);
140
176
  });
141
177
 
142
- it("Surface size is inherited as the subtree default (lg over global medium)", () => {
143
- const wrapper = ({ children }: { children: ReactNode }) =>
144
- createElement(TestProviders, null, createElement(Surface, { size: "lg" }, children));
145
- const { result } = renderHook(() => useResolvedKnobs(), { wrapper });
146
- expect(result.current.knobProps.size).toBe("large");
178
+ it("a smaller request inside the Surface keeps its own token", () => {
179
+ const { result } = renderHook(() => useResolvedKnobs({ size: "$2" }), {
180
+ wrapper: inSurface("$3"),
181
+ });
182
+ expect(result.current.knobProps.sizeToken).toBe("$2");
183
+ expect(result.current.knobProps.control.height).toBe(scale.size.$2);
184
+ });
185
+
186
+ it("the Surface token is the subtree default, and the spacing roles do not move", () => {
187
+ const { result } = renderHook(() => useResolvedKnobs(), { wrapper: inSurface("$5") });
147
188
  expect(result.current.knobProps.sizeToken).toBe("$5");
189
+ expect(result.current.knobProps.control.height).toBe(scale.size.$5);
190
+ expect(result.current.knobProps.gap).toEqual({ gap: "$3" });
191
+ expect(result.current.knobProps.panelPadding).toEqual({ padding: "$4" });
148
192
  });
193
+ });
149
194
 
150
- it("compact:false inside a compact Surface stays compact and still steps space", () => {
151
- const wrapper = ({ children }: { children: ReactNode }) =>
152
- createElement(TestProviders, null, createElement(Surface, { density: "compact" }, children));
153
- const { result } = renderHook(() => useResolvedKnobs({ compact: false }), { wrapper });
154
- expect(result.current.knobProps.density).toBe("compact");
155
- expect(result.current.knobProps.gap).toEqual({ gap: "$2" });
195
+ describe("sizeTokenBelow", () => {
196
+ it("steps one whole key down, $true reading as $4, floored at $1", () => {
197
+ expect(sizeTokenBelow("$true")).toBe("$3");
198
+ expect(sizeTokenBelow("$4")).toBe("$3");
199
+ expect(sizeTokenBelow("$3")).toBe("$2");
200
+ expect(sizeTokenBelow("$5")).toBe("$4");
201
+ expect(sizeTokenBelow("$1")).toBe("$1");
202
+ expect(sizeTokenBelow("$3.5")).toBe("$3");
156
203
  });
157
204
  });