@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,16 +1,22 @@
1
+ import { motionFor } from "./motion";
1
2
  import { isTouchable, isWeb, isWebTouchable } from "@multiplatform.one/platform";
2
- import type { FontSizeTokens, TransitionProp } from "tamagui";
3
+ import { getConfig } from "@tamagui/web";
4
+ import type { FontSizeTokens } from "tamagui";
3
5
  // Importing the class constant also mounts the corner-shape stylesheet at
4
6
  // theme load (web only, idempotent) — see cornerSmoothing.ts.
5
7
  import { cornerSmoothClassName } from "./cornerSmoothing";
8
+ import { headingCategoryFontKeys } from "./defaults/fonts";
6
9
  import {
7
10
  ensureFocusVisibleRing,
8
11
  STATE_LAYER_HOVER,
9
12
  STATE_LAYER_PRESS,
10
13
  withDefaultStateLayer,
11
14
  } from "./focusState";
15
+ import { fontCategoryStacks } from "./fontCategoryStacks";
12
16
  import type { CornerSmoothing, Elevation, Knobs, PageTitleScale, StateKnobs } from "./knobs";
13
17
  import { pressTargetHitSlop } from "./layoutTokens";
18
+ import { getActiveScaleTokens } from "./scale/activeScale";
19
+ import { SIZE_KEYS, type SizeKey } from "./scale/scaleInputs";
14
20
  import type {
15
21
  ControlStateProps,
16
22
  DisabledRecipe,
@@ -25,45 +31,6 @@ import type {
25
31
  } from "./recipes";
26
32
  import { sizeRecipeForToken } from "./sizeRecipes";
27
33
 
28
- // ── Density (compact) stepping ────────────────────────────────
29
-
30
- /**
31
- * @deprecated Size is no longer coupled to density (LC-76). Compact density
32
- * steps space only. Kept exported because figmaTokens historically imported it.
33
- */
34
- export const compactSizeMap: Record<Knobs["size"], Knobs["size"]> = {
35
- small: "small",
36
- medium: "small",
37
- large: "medium",
38
- };
39
-
40
- /** Step space down one level for compact density. */
41
- export const compactSpaceMap: Record<Knobs["space"], Knobs["space"]> = {
42
- small: "small",
43
- medium: "small",
44
- large: "medium",
45
- };
46
-
47
- /**
48
- * Apply density mode to knobs before token resolution.
49
- *
50
- * - `density: "compact"` steps **space only** (`compactSpaceMap`). Size is an
51
- * independent axis (LC-76) — compact must not shrink controls.
52
- * - `compactOverride` wins over the density knob when provided:
53
- * `true` forces compact space stepping; `false` forces comfortable (no step).
54
- */
55
- export function applyDensity(knobs: Knobs, compactOverride?: boolean): Knobs {
56
- const useCompact = compactOverride !== undefined ? compactOverride : knobs.density === "compact";
57
- if (!useCompact) {
58
- return knobs.density === "comfortable" ? knobs : { ...knobs, density: "comfortable" };
59
- }
60
- return {
61
- ...knobs,
62
- density: "compact",
63
- space: compactSpaceMap[knobs.space],
64
- };
65
- }
66
-
67
34
  // ── Knob-to-token mapping tables ──────────────────────────────
68
35
 
69
36
  export const borderRadiusMap = {
@@ -74,6 +41,14 @@ export const borderRadiusMap = {
74
41
  full: "$12",
75
42
  } as const;
76
43
 
44
+ export const containerRadiusMap = {
45
+ none: "$0",
46
+ small: "$4",
47
+ medium: "$6",
48
+ large: "$8",
49
+ full: "$12",
50
+ } as const;
51
+
77
52
  // Radius for chrome nested INSIDE a radius-bearing frame. Smaller than
78
53
  // `borderRadiusMap` at every stop that has room to be — a child corner equal
79
54
  // to its parent's reads as a second frame rather than as part of one.
@@ -108,16 +83,21 @@ const cornerSmoothingStyle: Record<CornerSmoothing, { className: string } | unde
108
83
  ? { round: undefined, smooth: { className: cornerSmoothClassName } }
109
84
  : { round: undefined, smooth: undefined };
110
85
 
111
- const panelPaddingMap = {
112
- small: "$3",
113
- medium: "$4",
114
- large: "$6",
86
+ // ── Spacing roles (SP-GAP, A9) ────────────────────────────────
87
+ // Fixed `$space` keys. Their px come from the active scale, so a preset
88
+ // that wants more or less room changes token values, never these keys.
89
+ export const spacingRoles = {
90
+ gapSm: "$2",
91
+ gap: "$3",
92
+ gapLg: "$5",
93
+ panelPadding: "$4",
94
+ featurePadding: "$6",
115
95
  } as const;
116
96
 
117
97
  // Feature tier (SF-FEATURE, MPO-21 GL-59 ruling B): the Bento recipe's $10
118
- // radius and $6 inset at the default stops, one token per stop either side.
119
- // Uncapped by padding: the tightest pair (full 50 over small 24) still keeps
120
- // a child corner at the inset inside the arc (padding >= radius x 0.29).
98
+ // radius over the `featurePadding` role ($6). Uncapped by padding: the
99
+ // tightest pair (full 44 over 28) keeps a child corner at the inset inside
100
+ // the arc (padding >= radius x 0.29).
121
101
  const featureRadiusMap = {
122
102
  none: "$0",
123
103
  small: "$9",
@@ -126,21 +106,11 @@ const featureRadiusMap = {
126
106
  full: "$12",
127
107
  } as const;
128
108
 
129
- const featurePaddingMap = {
130
- small: "$5",
131
- medium: "$6",
132
- large: "$7",
133
- } as const;
134
-
135
109
  // ── Page-title step (D-11 hero-H1 dial) ───────────────────────
136
- // The one canonical definition of how big a page title is. On the heading
137
- // scale (1.4x Inter, defaults/fonts.ts) $8 = 32px and $10 = 64px, so
138
- // `moderate` lands in the page-title class while `display` keeps the
139
- // marketing hero step. Screens never pick a heading step by hand — they
140
- // spread `knobProps.pageTitle`, so one knob flip re-scales every title.
110
+ // Default type 15: moderate is 40px; display is 52px.
141
111
  const pageTitleSizeMap: Record<PageTitleScale, FontSizeTokens> = {
142
- moderate: "$8",
143
- display: "$10",
112
+ moderate: "$10",
113
+ display: "$12",
144
114
  };
145
115
 
146
116
  /**
@@ -157,39 +127,39 @@ export function resolvePageTitleScale(scale: PageTitleScale): { size: FontSizeTo
157
127
  // ── Container radius cap (Axiom 1 CLIP) ──────────────────────
158
128
  // A padded container's effective radius must not exceed its own padding:
159
129
  // past that, the corner curve sweeps through the cell where children sit
160
- // (radius > padding clips child corners). Px values mirror the
161
- // @tamagui/themes tokens the maps above resolve to (radius $0/$2/$4/$6/$12
162
- // = 0/5/9/16/50; space $3/$4/$6 = 13/18/32) — resolveKnobs stays a pure
163
- // module, so the comparison happens on mirrored numbers, not live tokens.
164
- // The cap only bites at `full` (every space) and `large` under
165
- // `space=small`; default knobs are unchanged.
166
-
167
- const radiusPxMap: Record<Knobs["borderRadius"], number> = {
168
- none: 0,
169
- small: 5,
170
- medium: 9,
171
- large: 16,
172
- full: 50,
130
+ // (radius > padding clips child corners). Both px are read off the installed
131
+ // scale: the knob's `$radius` key against the `panelPadding` role's `$space`.
132
+ // Containers step two radius keys above controls; large and full hit the cap.
133
+
134
+ const radiusKeyMap: Record<Knobs["borderRadius"], "0" | "4" | "6" | "8" | "12"> = {
135
+ none: "0",
136
+ small: "4",
137
+ medium: "6",
138
+ large: "8",
139
+ full: "12",
173
140
  };
174
141
 
175
- const panelPaddingPxMap: Record<Knobs["space"], number> = {
176
- small: 13,
177
- medium: 18,
178
- large: 32,
179
- };
142
+ /** The installed scale's `panelPadding` role in px. */
143
+ export function panelPaddingPx(): number {
144
+ return getActiveScaleTokens().space[spacingRoles.panelPadding];
145
+ }
180
146
 
181
147
  /**
182
148
  * Cap a container's radius at its own padding (Axiom 1 CLIP: "my children's
183
149
  * pixel clips first → cap at padding"). Returns the radius token untouched
184
150
  * while it fits, or the padding px number once the token would exceed it.
185
151
  */
186
- export function capContainerRadius(
187
- borderRadius: Knobs["borderRadius"],
188
- space: Knobs["space"],
189
- ): string | number {
190
- const radiusPx = radiusPxMap[borderRadius];
191
- const paddingPx = panelPaddingPxMap[space];
192
- return radiusPx > paddingPx ? paddingPx : borderRadiusMap[borderRadius];
152
+ export function capContainerRadius(borderRadius: Knobs["borderRadius"]): string | number {
153
+ const radiusPx = getActiveScaleTokens().radius[radiusKeyMap[borderRadius]];
154
+ const paddingPx = panelPaddingPx();
155
+ return radiusPx > paddingPx ? paddingPx : containerRadiusMap[borderRadius];
156
+ }
157
+
158
+ export function capFieldRadius(borderRadius: Knobs["borderRadius"]): string | number {
159
+ const token = borderRadiusMap[borderRadius];
160
+ const radiusPx = getActiveScaleTokens().radius[token.slice(1) as "0" | "2" | "4" | "6" | "12"];
161
+ const paddingPx = panelPaddingPx();
162
+ return radiusPx > paddingPx ? paddingPx : token;
193
163
  }
194
164
 
195
165
  /**
@@ -208,93 +178,93 @@ export function radiusStopFromToken(token: unknown): Knobs["borderRadius"] | und
208
178
  * `StackedRow` is one row of a vertical stack: it also carries
209
179
  * `data-stack-position`, and only its outer corners take the cap.
210
180
  * `OwnInset` pads with a fixed token instead of panelPadding, so the audit
211
- * caps it at its measured padding and ignores the space stop.
181
+ * caps it at its measured padding.
212
182
  */
213
183
  export type CappedContainer = "PageSection" | "TextArea" | "StackedRow" | "OwnInset";
214
184
 
215
185
  export interface ContainerCapDeclaration {
216
186
  "data-constraint-container": CappedContainer;
217
187
  "data-radius-knob": Knobs["borderRadius"];
218
- "data-space-knob": Knobs["space"];
219
188
  }
220
189
 
221
190
  /**
222
- * Declare a container that spreads `containerRadius`, with the knob stops it
191
+ * Declare a container that spreads `containerRadius`, with the knob stop it
223
192
  * resolved, so the constraint audit checks its painted corners against
224
- * `capContainerRadius` for those stops. The cap is a padding px value, not a
225
- * radius token, so an undeclared capped container reads off-scale at `full`.
226
- * Returns nothing when `radiusToken` is not a `borderRadiusMap` token or
227
- * `space` is not a space stop: a part whose radius did not come from the knob
193
+ * `capContainerRadius` for that stop on the page's scale, or `capFieldRadius`
194
+ * for TextArea's `fieldRadius`. The input is always the CONTROL token. The cap is a
195
+ * padding px value, not a radius token. Returns nothing when `radiusToken` is not a
196
+ * `borderRadiusMap` token: a part whose radius did not come from the knob
228
197
  * makes no claim.
229
198
  *
230
199
  * @example
231
- * <YStack {...knobProps.containerRadius} {...containerCapProps("PageSection", knobProps.borderRadius.borderRadius, knobProps.space)} />
200
+ * <YStack {...knobProps.containerRadius} {...containerCapProps("PageSection", knobProps.borderRadius.borderRadius)} />
232
201
  */
233
202
  export function containerCapProps(
234
203
  container: CappedContainer,
235
204
  radiusToken: unknown,
236
- space: string,
237
205
  ): ContainerCapDeclaration | undefined {
238
206
  const radiusKnob = radiusStopFromToken(radiusToken);
239
- if (!radiusKnob || !Object.hasOwn(panelPaddingPxMap, space)) return undefined;
207
+ if (!radiusKnob) return undefined;
240
208
  return {
241
209
  "data-constraint-container": container,
242
210
  "data-radius-knob": radiusKnob,
243
- "data-space-knob": space as Knobs["space"],
244
211
  };
245
212
  }
246
213
 
247
- const gapMap = {
248
- small: "$2",
249
- medium: "$4",
250
- large: "$5",
251
- } as const;
214
+ const sizeKeys = new Set<string>(SIZE_KEYS);
252
215
 
253
- // gapLg stays one step above gapMap.
254
- const gapLgMap = {
255
- small: "$3",
256
- medium: "$5",
257
- large: "$6",
258
- } as const;
216
+ /** `sizeToken` when it is a `$size` key, else `$true`. */
217
+ export function normalizeSizeToken(sizeToken: string | undefined): SizeKey {
218
+ return sizeToken !== undefined && sizeKeys.has(sizeToken) ? (sizeToken as SizeKey) : "$true";
219
+ }
259
220
 
260
- const sizeTokenMap = {
261
- small: "$3",
262
- medium: "$4",
263
- large: "$5",
264
- } as const;
221
+ const nestedControlRatio = 32 / 44;
265
222
 
266
- /** Control-chrome type — one step below the matching sizeToken height. */
267
- const labelFontSizeMap = {
268
- small: "$2",
269
- medium: "$3",
270
- large: "$4",
271
- } as const;
223
+ /** Painted px of a nested hit-target (calendar days) at `sizeToken`: 32/44 of the token's `$size`. */
224
+ export function nestedControlPx(sizeToken: string): number {
225
+ return Math.round(
226
+ getActiveScaleTokens().size[normalizeSizeToken(sizeToken)] * nestedControlRatio,
227
+ );
228
+ }
272
229
 
273
230
  /**
274
- * Painted size for nested hit-targets (calendar days). Always below the
275
- * 44px floor; press is restored with hitSlop. Steps with `size` (compact
276
- * density steps space only — LC-76 — so nested chrome follows the size axis).
231
+ * The whole key one below a size key ($4 / $true -> $3, $3 -> $2,
232
+ * $3.5 -> $3, floor $1). Parts drawn one step under their control (tags,
233
+ * avatars) read it.
277
234
  */
278
- const nestedControlPxMap = {
279
- small: 28,
280
- medium: 32,
281
- large: 36,
282
- } as const;
235
+ export function sizeKeyBelow(sizeToken: string): `$${number}` {
236
+ const key = normalizeSizeToken(sizeToken);
237
+ const step = key === "$true" ? 4 : Number.parseFloat(key.slice(1));
238
+ return `$${Math.min(16, Math.max(1, Math.ceil(step) - 1))}`;
239
+ }
240
+
241
+ /** The body font key at the size key; half steps round up, and `$true` stays `$true`. */
242
+ export function labelFontSizeFor(sizeToken: string): FontSizeTokens {
243
+ const key = normalizeSizeToken(sizeToken);
244
+ if (key === "$true") return "$true";
245
+ return `$${Math.min(16, Math.max(1, Math.ceil(Number.parseFloat(key.slice(1)))))}` as FontSizeTokens;
246
+ }
283
247
 
284
248
  const headingFontMap = {
285
249
  "sans-serif": "$heading",
286
- serif: "$serif",
287
- mono: "$mono",
288
- slab: "$slab",
289
- rounded: "$rounded",
290
- condensed: "$condensed",
291
- cursive: "$cursive",
292
- handwriting: "$handwriting",
293
- pixel: "$pixel",
294
- blackletter: "$blackletter",
295
- geometric: "$geometric",
250
+ serif: `$${headingCategoryFontKeys.serif}`,
251
+ mono: `$${headingCategoryFontKeys.mono}`,
252
+ slab: `$${headingCategoryFontKeys.slab}`,
253
+ rounded: `$${headingCategoryFontKeys.rounded}`,
254
+ condensed: `$${headingCategoryFontKeys.condensed}`,
255
+ cursive: `$${headingCategoryFontKeys.cursive}`,
256
+ handwriting: `$${headingCategoryFontKeys.handwriting}`,
257
+ pixel: `$${headingCategoryFontKeys.pixel}`,
258
+ blackletter: `$${headingCategoryFontKeys.blackletter}`,
259
+ geometric: `$${headingCategoryFontKeys.geometric}`,
296
260
  } as const;
297
261
 
262
+ export function getHeadingFontFamily(category: Knobs["headingFont"]): string {
263
+ const family = getConfig().fontsParsed[headingFontMap[category]]?.family;
264
+ const value = typeof family === "string" ? family : family?.val;
265
+ return typeof value === "string" ? value : fontCategoryStacks[category];
266
+ }
267
+
298
268
  const bodyFontMap = {
299
269
  "sans-serif": "$body",
300
270
  serif: "$serif",
@@ -314,149 +284,66 @@ const fontWeightMap = {
314
284
  bold: "700",
315
285
  } as const;
316
286
 
317
- // Web keeps Tamagui size-token strings — Tamagui resolves them to boxShadow.
318
- // Native must emit NUMBERS: knobProps.elevation gets spread onto plain RN
319
- // Views by non-Tamagui consumers, and Android's RCTView `elevation` prop is a
320
- // Double — a token string kills the ReactHost with "java.lang.String cannot
321
- // be cast to java.lang.Double" (SB-N-04). The numbers are the exact
322
- // tokens.size values Tamagui resolves those tokens to ($1=20, $2=28, $4=44),
323
- // so Tamagui consumers render identical shadows either way.
287
+ // Stops are `$size` keys; the shadow is Tamagui's getSizedElevation of that
288
+ // key's px on the active scale. Native gets the px NUMBER: knobProps.elevation
289
+ // is spread onto plain RN Views, and Android's RCTView `elevation` is a Double,
290
+ // so a token string kills the ReactHost (SB-N-04).
324
291
  //
325
- // Controls are flat at the default `small` stop (MPO-96: the reference
326
- // measures no resting shadow on a button or a card), so `small` paints
327
- // nothing on the control map and only medium / large lift a control.
328
- // Overlay elevation is a DISTINCT ladder one step above the control map, so
329
- // a dialog over a scrim and a Button on the page never read as the same
330
- // distance off the surface (DG-OVL-01 / E-OVERLAY). elevatedSurface binds
331
- // the overlay token — not the control one.
332
- export const elevationMap: Record<string, string | number | undefined> = isWeb
333
- ? { none: undefined, small: undefined, medium: "$2", large: "$4" }
334
- : { none: undefined, small: undefined, medium: 28, large: 44 };
335
-
336
- export const overlayElevationMap: Record<string, string | number | undefined> = isWeb
337
- ? { none: undefined, small: "$2", medium: "$3", large: "$5" }
338
- : { none: undefined, small: 28, medium: 36, large: 52 };
339
-
340
- /** Scheme the resolver was invoked with. Defaults light so unit tests stay stable. */
341
- export type ColorScheme = "light" | "dark";
342
-
343
- // Existing 12-step ramp (colorRules groups). Do not invent overlay tokens:
344
- // dark elevation is a lighter $color step. Overlay/card rest on $color1;
345
- // filled inputs rest on $color2 and step from there.
346
- const elevationTintOverlay: Record<Elevation, string> = {
347
- none: "$color1",
348
- small: "$color2",
349
- medium: "$color3",
350
- large: "$color4",
351
- };
352
-
353
- const elevationTintInput: Record<Elevation, string> = {
354
- none: "$color2",
355
- small: "$color3",
356
- medium: "$color4",
357
- large: "$color5",
358
- };
359
-
360
- type ElevationKey = "$1" | "$2" | "$3" | "$4" | "$5";
361
-
362
- // Axiom 14 light source: y-offset:blur = 0.5. Metrics match the maps
363
- // InputParts / getElevationWrapperProps already shipped. `$3` and `$5` are
364
- // the overlay-only stops (MPO-99): the reference §8 popper and dialog
365
- // shadows. Native paints one shadow, so it takes the first web layer.
366
- const elevationShadowMetrics: Record<
367
- ElevationKey,
368
- { offsetY: number; radius: number; android: number }
369
- > = {
370
- $1: { offsetY: 1, radius: 2, android: 2 },
371
- $2: { offsetY: 3, radius: 6, android: 4 },
372
- $3: { offsetY: 4, radius: 8, android: 6 },
373
- $4: { offsetY: 10, radius: 20, android: 8 },
374
- $5: { offsetY: 12, radius: 24, android: 12 },
375
- };
376
-
377
- const elevationKeyAlias: Record<string, ElevationKey> = {
378
- $1: "$1",
379
- $2: "$2",
380
- $3: "$3",
381
- $4: "$4",
382
- $5: "$5",
383
- "20": "$1",
384
- "28": "$2",
385
- "36": "$3",
386
- "44": "$4",
387
- "52": "$5",
388
- };
389
-
390
- const webShadowLight: Record<ElevationKey, string> = {
391
- $1: "0 1px 2px rgba(0,0,0,0.12), 0 0 1px rgba(0,0,0,0.08)",
392
- $2: "0 3px 6px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.10)",
393
- $3: "0 4px 8px rgba(0,0,0,0.10), 0 12px 32px rgba(0,0,0,0.08)",
394
- $4: "0 10px 20px rgba(0,0,0,0.15), 0 3px 6px rgba(0,0,0,0.10)",
395
- $5: "0 12px 24px rgba(0,0,0,0.12)",
292
+ // Controls are flat at the default `small` stop (MPO-96). Overlays climb a
293
+ // distinct ladder above the control map so a dialog over a scrim and a Button
294
+ // on the page never read as the same distance (DG-OVL-01 / E-OVERLAY). The
295
+ // overlay default is `$4`, the control height: on the 40px default scale `$3`
296
+ // (32px) casts a 22px band, under the 24px that lifts a dialog off its scrim.
297
+ export const elevationMap: Record<Elevation, SizeKey | undefined> = {
298
+ none: undefined,
299
+ small: undefined,
300
+ medium: "$2",
301
+ large: "$4",
396
302
  };
397
303
 
398
- // LC-85: dark keeps each stop's geometry and inverts the hue.
399
- const webShadowDark: Record<ElevationKey, string> = {
400
- $1: "0 1px 2px rgba(255,255,255,0.08), 0 0 1px rgba(255,255,255,0.06)",
401
- $2: "0 3px 6px rgba(255,255,255,0.10), 0 2px 4px rgba(255,255,255,0.06)",
402
- $3: "0 4px 8px rgba(255,255,255,0.10), 0 12px 32px rgba(255,255,255,0.08)",
403
- $4: "0 10px 20px rgba(255,255,255,0.12), 0 3px 6px rgba(255,255,255,0.08)",
404
- $5: "0 12px 24px rgba(255,255,255,0.12)",
304
+ export const overlayElevationMap: Record<Elevation, SizeKey | undefined> = {
305
+ none: undefined,
306
+ small: "$4",
307
+ medium: "$4.5",
308
+ large: "$5",
405
309
  };
406
310
 
407
- const shadowOpacityLight: Record<ElevationKey, number> = {
408
- $1: 0.12,
409
- $2: 0.15,
410
- $3: 0.1,
411
- $4: 0.15,
412
- $5: 0.12,
413
- };
311
+ export type ColorScheme = "light" | "dark";
414
312
 
415
- const shadowOpacityDark: Record<ElevationKey, number> = {
416
- $1: 0.08,
417
- $2: 0.1,
418
- $3: 0.1,
419
- $4: 0.12,
420
- $5: 0.12,
421
- };
313
+ export function sizedElevation(px: number): { offsetY: number; radius: number; android: number } {
314
+ const offsetY = Math.round(px / 4 + 1);
315
+ return { offsetY, radius: Math.round(px / 2 + 2), android: 2 * offsetY };
316
+ }
422
317
 
423
- function elevationTokenKey(token: string | number | undefined): ElevationKey | undefined {
424
- if (token == null) return undefined;
425
- return elevationKeyAlias[String(token)];
318
+ function platformElevation(key: SizeKey | undefined): string | number | undefined {
319
+ return key && (isWeb ? key : getActiveScaleTokens().size[key]);
426
320
  }
427
321
 
428
- function elevationPaint(
429
- token: string | number | undefined,
430
- scheme: ColorScheme,
431
- ): ElevationPaintProps {
432
- const key = elevationTokenKey(token);
322
+ function elevationPaint(key: SizeKey | undefined): ElevationPaintProps {
433
323
  if (!key) return {};
434
- const metrics = elevationShadowMetrics[key];
435
- const dark = scheme === "dark";
436
- const paint: ElevationPaintProps = {
437
- shadowColor: dark ? "$color12" : "#000",
438
- shadowOffset: { width: 0, height: metrics.offsetY },
439
- shadowOpacity: dark ? shadowOpacityDark[key] : shadowOpacityLight[key],
440
- shadowRadius: metrics.radius,
324
+ const { offsetY, radius } = sizedElevation(getActiveScaleTokens().size[key]);
325
+ return {
326
+ shadowColor: "$shadowColor",
327
+ shadowOffset: { width: 0, height: offsetY },
328
+ shadowOpacity: 1,
329
+ shadowRadius: radius,
330
+ ...(isWeb ? { style: { boxShadow: `0px ${offsetY}px ${radius}px var(--shadowColor)` } } : {}),
441
331
  };
442
- if (isWeb) {
443
- paint.style = { boxShadow: dark ? webShadowDark[key] : webShadowLight[key] };
444
- }
445
- return paint;
446
332
  }
447
333
 
448
- function elevationChromeFor(
449
- token: string | number | undefined,
450
- scheme: ColorScheme,
451
- ): ElevationChromeProps {
452
- const key = elevationTokenKey(token);
334
+ function elevationChromeFor(key: SizeKey | undefined): ElevationChromeProps {
453
335
  if (!key) return {};
454
336
  return {
455
- ...elevationPaint(token, scheme),
456
- elevation: elevationShadowMetrics[key].android,
337
+ ...elevationPaint(key),
338
+ elevation: sizedElevation(getActiveScaleTokens().size[key]).android,
457
339
  };
458
340
  }
459
341
 
342
+ // v5's surface2 template steps: fill and edge two palette steps above the
343
+ // base `$background`/`$borderColor` (color2/color4), one above Card's surface1.
344
+ // The fill alone would land on the base edge step and erase the border.
345
+ const surface2 = { fill: "$color4", edge: "$color6" } as const;
346
+
460
347
  // Legibility floor: body/label text must never land below ~4.5:1 against its
461
348
  // surface. $color11 is the lowest ramp step that clears AA on color1/2
462
349
  // surfaces, so "low" clamps there ($color9 measured 1.29-3.43:1 — SB-R-01).
@@ -527,10 +414,7 @@ function resolveDisabled(disabledStyle: Knobs["disabledStyle"]): DisabledRecipe
527
414
 
528
415
  // ── State style resolver ──────────────────────────────────────
529
416
 
530
- function resolveStateKnobs(
531
- state: StateKnobs,
532
- scheme: ColorScheme,
533
- ): {
417
+ function resolveStateKnobs(state: StateKnobs): {
534
418
  control: ControlStateProps;
535
419
  text: TextStateProps;
536
420
  elevation: ElevationStateProps;
@@ -548,16 +432,10 @@ function resolveStateKnobs(
548
432
  if (state.borderRadius !== undefined) {
549
433
  control.borderRadius = borderRadiusMap[state.borderRadius];
550
434
  }
551
- if (state.space !== undefined) {
552
- control.padding = gapMap[state.space];
553
- }
554
435
  if (state.elevation !== undefined) {
555
436
  const token = elevationMap[state.elevation];
556
- elevation.elevation = token;
557
- Object.assign(elevation, elevationPaint(token, scheme));
558
- if (scheme === "dark" && state.elevation !== "none") {
559
- elevation.backgroundColor = elevationTintOverlay[state.elevation];
560
- }
437
+ elevation.elevation = platformElevation(token);
438
+ Object.assign(elevation, elevationPaint(token));
561
439
  }
562
440
  if (state.textAccent !== undefined) {
563
441
  text.color = textAccentColorMap[state.textAccent];
@@ -576,28 +454,49 @@ function resolveStateKnobs(
576
454
 
577
455
  // ── Resolver ──────────────────────────────────────────────────
578
456
 
457
+ /** Vertical inset of a top bar; with the default control height it lands the bar at 52. */
458
+ const barPaddingToken = "$2";
459
+ /**
460
+ * A content card on a painted parent sits one ramp step off it, so the box
461
+ * reads without a shadow (nested surfaces never re-elevate).
462
+ */
463
+ export const paintedCardFill = "$color2";
464
+
465
+ /** What the subtree supplies beyond knobs: the pointer surface and the control size token. */
466
+ export interface ResolveKnobsEnvironment {
467
+ touch?: boolean;
468
+ /** Tamagui size key the controls render at. Default `$true`. */
469
+ sizeToken?: string;
470
+ /**
471
+ * An ancestor already paints a surface box: `elevatedSurface` goes border
472
+ * only (no second shadow) and `cardSurface` steps one fill off the parent.
473
+ */
474
+ painted?: boolean;
475
+ }
476
+
579
477
  export function resolveKnobs(
580
478
  knobs: Knobs,
581
479
  override?: KnobPropsOverride,
582
480
  scheme: ColorScheme = "light",
583
- environment?: { touch?: boolean },
481
+ environment?: ResolveKnobsEnvironment,
584
482
  ): ResolvedKnobs {
585
483
  const radiusToken = borderRadiusMap[knobs.borderRadius];
586
484
  const nestedRadiusToken = nestedRadiusMap[knobs.borderRadius];
587
- const sizeToken = sizeTokenMap[knobs.size];
588
- const nestedPx = nestedControlPxMap[knobs.size];
485
+ const sizeToken = normalizeSizeToken(environment?.sizeToken);
486
+ const painted = environment?.painted ?? false;
487
+ const nestedPx = nestedControlPx(sizeToken);
589
488
  const nestedHitSlop = pressTargetHitSlop(nestedPx);
590
- const sizeRecipe = sizeRecipeForToken(sizeToken, {
591
- touch: environment?.touch ?? (isTouchable || isWebTouchable),
592
- family: knobs.density === "compact" ? "controlCompact" : "control",
593
- });
594
- const paddingToken = panelPaddingMap[knobs.space];
595
- const gapToken = gapMap[knobs.space];
489
+ const touch = environment?.touch ?? (isTouchable || isWebTouchable);
490
+ const sizeRecipe = sizeRecipeForToken(sizeToken, { touch });
491
+ const menuRowRecipe = sizeRecipeForToken(sizeToken, { touch, family: "menuRow" });
492
+ const tabRecipe = sizeRecipeForToken(sizeToken, { touch, family: "tab" });
493
+ const scaleTokens = getActiveScaleTokens();
494
+ const paddingToken = spacingRoles.panelPadding;
596
495
  const fontWeightValue = fontWeightMap[knobs.fontWeight];
597
496
  const bodyFontValue = bodyFontMap[knobs.bodyFont] ?? "$body";
598
- const transition = knobs.animation === "none" ? undefined : (knobs.animation as TransitionProp);
497
+ const motion = motionFor(knobs.animation);
599
498
 
600
- const containerRadiusValue = capContainerRadius(knobs.borderRadius, knobs.space);
499
+ const containerRadiusValue = capContainerRadius(knobs.borderRadius);
601
500
 
602
501
  // Corner smoothing rides every radius-bearing fragment so consumers need
603
502
  // zero edits (KNOB-TOTALITY, LC-18). Identity guard (Axiom 3 NULL +
@@ -624,29 +523,18 @@ export function resolveKnobs(
624
523
  : borderWidthMap[knobs.borderWidth];
625
524
  const elevationToken = elevationMap[knobs.elevation];
626
525
  const overlayElevationToken = overlayElevationMap[knobs.elevation];
627
- const overlayPaintProps = elevationPaint(overlayElevationToken, scheme);
628
- const elevationChrome = elevationChromeFor(elevationToken, scheme);
629
- const dark = scheme === "dark";
630
- // MPO-19 X5: `$background` (not `$color1`) so surface1/2/3 sub-themes move
631
- // the fill. Dark + elevation still steps the filled rest fill up the ramp
632
- // (LC-85).
633
- const overlayFill =
634
- dark && knobs.elevation !== "none" ? elevationTintOverlay[knobs.elevation] : "$background";
526
+ const overlayPaintProps = elevationPaint(overlayElevationToken);
527
+ const elevationChrome = elevationChromeFor(elevationToken);
635
528
  const textAccentColor = textAccentColorMap[knobs.textAccent] ?? "$color";
636
529
  // One input surface (DF-19 / Axiom 5 ONE SOURCE): Timeline's composer,
637
530
  // RichEditor and form fields agree on what an input surface is.
638
531
  // MPO-19 X6: `$background` not `$color2` so a surface sub-theme can move it.
639
- // Dark + elevation steps the filled rest fill up the existing ramp (LC-85).
640
- const inputBackground = outlined
641
- ? "transparent"
642
- : dark
643
- ? elevationTintInput[knobs.elevation]
644
- : "$background";
645
-
646
- const hover = resolveStateKnobs(knobs.hover, scheme);
647
- const press = resolveStateKnobs(knobs.press, scheme);
648
- const focus = resolveStateKnobs(knobs.focus, scheme);
649
- const focusVisible = resolveStateKnobs(knobs.focusVisible, scheme);
532
+ const inputBackground = outlined ? "transparent" : "$background";
533
+
534
+ const hover = resolveStateKnobs(knobs.hover);
535
+ const press = resolveStateKnobs(knobs.press);
536
+ const focus = resolveStateKnobs(knobs.focus);
537
+ const focusVisible = resolveStateKnobs(knobs.focusVisible);
650
538
 
651
539
  const nonEmpty = <T extends object>(obj: T): T | undefined =>
652
540
  Object.keys(obj).length > 0 ? obj : undefined;
@@ -695,9 +583,11 @@ export function resolveKnobs(
695
583
  // spread this after their radius-bearing fragment. Non-container parts
696
584
  // keep the uncapped `borderRadius` fragment.
697
585
  containerRadius: { borderRadius: containerRadiusValue, ...containerSmoothing },
698
- elevation: elevationToken,
699
- overlayElevation: overlayElevationToken,
586
+ fieldRadius: { borderRadius: capFieldRadius(knobs.borderRadius), ...containerSmoothing },
587
+ elevation: platformElevation(elevationToken),
588
+ overlayElevation: platformElevation(overlayElevationToken),
700
589
  elevationChrome,
590
+ thumbShadow: elevationChromeFor("$0.5"),
701
591
  scheme,
702
592
  surface: {
703
593
  backgroundColor: "$background",
@@ -705,24 +595,33 @@ export function resolveKnobs(
705
595
  borderWidth: borderWidthValue,
706
596
  },
707
597
  cardSurface: {
708
- backgroundColor: overlayFill,
598
+ backgroundColor: painted ? paintedCardFill : "$background",
709
599
  borderRadius: containerRadiusValue,
710
600
  padding: paddingToken,
711
601
  ...containerSmoothing,
712
602
  },
713
- elevatedSurface: {
714
- backgroundColor: overlayFill,
715
- borderColor: "$borderColor",
716
- borderWidth: borderWidthValue,
717
- borderRadius: radiusToken,
718
- elevation: overlayElevationToken,
719
- ...overlayPaintProps,
720
- ...scaleSmoothing,
721
- },
603
+ elevatedSurface: painted
604
+ ? {
605
+ backgroundColor: "$background",
606
+ borderColor: "$borderColor",
607
+ borderWidth: Math.max(borderWidthValue, borderWidthMap.small),
608
+ borderRadius: radiusToken,
609
+ elevation: undefined,
610
+ ...scaleSmoothing,
611
+ }
612
+ : {
613
+ backgroundColor: scheme === "dark" ? surface2.fill : "$background",
614
+ borderColor: scheme === "dark" ? surface2.edge : "$borderColor",
615
+ borderWidth: borderWidthValue,
616
+ borderRadius: radiusToken,
617
+ elevation: platformElevation(overlayElevationToken),
618
+ ...overlayPaintProps,
619
+ ...scaleSmoothing,
620
+ },
722
621
  featureSurface: {
723
622
  backgroundColor: "$background",
724
623
  borderRadius: featureRadiusMap[knobs.borderRadius],
725
- padding: featurePaddingMap[knobs.space],
624
+ padding: spacingRoles.featurePadding,
726
625
  // Bento marketing/auth card shadow: ~0px 9px 12px rgba(0,0,0,0.02) —
727
626
  // softer/wider than any elevationMap token, so explicit shadow props.
728
627
  shadowColor: "rgba(0,0,0,0.02)",
@@ -733,13 +632,14 @@ export function resolveKnobs(
733
632
  },
734
633
  inputSurface: { borderWidth: borderWidthValue },
735
634
  panelPadding: { padding: paddingToken },
736
- gap: { gap: gapToken },
737
- gapLg: { gap: gapLgMap[knobs.space] },
635
+ gapSm: { gap: spacingRoles.gapSm },
636
+ gap: { gap: spacingRoles.gap },
637
+ gapLg: { gap: spacingRoles.gapLg },
738
638
  sizeToken,
739
639
  label: {
740
- fontSize: labelFontSizeMap[knobs.size],
640
+ fontSize: labelFontSizeFor(sizeToken),
741
641
  fontFamily: bodyFontValue,
742
- fontWeight: fontWeightValue,
642
+ ...(knobs.fontWeight === "bold" ? { fontWeight: "700" } : {}),
743
643
  },
744
644
  nestedControl: {
745
645
  px: nestedPx,
@@ -758,25 +658,50 @@ export function resolveKnobs(
758
658
  },
759
659
  controlType: { fontSize: sizeRecipe.fontSize },
760
660
  controlIcon: { width: sizeRecipe.iconSize, height: sizeRecipe.iconSize },
661
+ menuRow: {
662
+ minHeight: menuRowRecipe.height,
663
+ paddingHorizontal: menuRowRecipe.paddingHorizontal,
664
+ gap: menuRowRecipe.gap,
665
+ },
666
+ menuRowType: { fontSize: menuRowRecipe.fontSize },
667
+ tab: {
668
+ height: tabRecipe.height,
669
+ paddingHorizontal: tabRecipe.paddingHorizontal,
670
+ gap: tabRecipe.gap,
671
+ },
672
+ tabType: { fontSize: tabRecipe.fontSize },
673
+ tabHitSlop: pressTargetHitSlop(tabRecipe.height),
674
+ bar: {
675
+ frame: {
676
+ paddingVertical: barPaddingToken,
677
+ paddingHorizontal: paddingToken,
678
+ minHeight: scaleTokens.size.$true + 2 * scaleTokens.space[barPaddingToken],
679
+ },
680
+ sizeToken: "$3",
681
+ },
761
682
  heading: {
762
683
  fontFamily: headingFontMap[knobs.headingFont],
763
- fontWeight: fontWeightValue,
684
+ ...(knobs.fontWeight === "bold" ? { fontWeight: "700" } : {}),
764
685
  },
765
686
  pageTitle: resolvePageTitleScale(knobs.pageTitleScale ?? "moderate"),
766
687
  body: {
767
688
  fontFamily: bodyFontValue,
768
689
  fontWeight: fontWeightValue,
769
690
  },
691
+ typography: {
692
+ mutedMono: { fontFamily: "$mono", color: textAccentColorMap.low },
693
+ tabular: isWeb
694
+ ? { style: { fontVariantNumeric: "tabular-nums" } }
695
+ : { fontVariant: ["tabular-nums"] },
696
+ },
770
697
  textWeight: { fontWeight: fontWeightValue },
771
- transition,
698
+ motion,
699
+ transition: motion.control.transition,
772
700
  outlined,
773
701
  pointy: knobs.borderRadius === "none",
774
702
  textAccent: knobs.textAccent,
775
703
  textAccentColor,
776
704
  inputBackground,
777
- space: knobs.space,
778
- size: knobs.size,
779
- density: knobs.density,
780
705
  // House decisions — behavioral pass-through (no token mapping).
781
706
  fieldLabelPlacement: knobs.fieldLabelPlacement,
782
707
  requiredMarking: knobs.requiredMarking,