@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
@@ -5,10 +5,10 @@
5
5
  *
6
6
  * Controls:
7
7
  * - Color scheme (system / light / dark) via @vxrn/color-scheme
8
- * - Preset picker (using registered presets)
8
+ * - Preset picker (using registered presets), each listed with its token
9
+ * scale (control / type px): size and spacing come from the preset's scale
9
10
  * - All knobs: fillStyle, borderRadius, cornerSmoothing, borderWidth,
10
- * elevation, space, size, density, textAccent, headingFont, bodyFont,
11
- * fontWeight, animation,
11
+ * elevation, textAccent, headingFont, bodyFont, fontWeight, animation,
12
12
  * plus house decisions (fieldLabelPlacement, requiredMarking, tableZebra,
13
13
  * bulkBarPlacement, selectAllScope, timestampStyle, disabledStyle,
14
14
  * formAutofocus)
@@ -55,9 +55,6 @@ import {
55
55
  CornerSmoothing,
56
56
  BorderWidth,
57
57
  Elevation,
58
- Space,
59
- Size,
60
- Density,
61
58
  TextAccent,
62
59
  HeadingFont,
63
60
  BodyFont,
@@ -73,14 +70,12 @@ import {
73
70
  interactionStates,
74
71
  } from "../knobs";
75
72
  import { getPresetNames, getPreset } from "../shared";
73
+ import { defaultScaleInputs } from "../scale/scaleInputs";
76
74
  import { extractAnimationNames } from "./animationOptions";
77
75
  import { ColorLineVisualizer } from "./ColorLineVisualizer";
78
76
  import { buildSnapshot } from "./buildSnapshot";
79
77
  import {
80
78
  fillIcon as _fillIcon,
81
- spaceIcon as _spaceIcon,
82
- sizeIcon as _sizeIcon,
83
- densityIcon as _densityIcon,
84
79
  borderRadiusIcon as _borderRadiusIcon,
85
80
  cornerSmoothingIcon as _cornerSmoothingIcon,
86
81
  borderWidthIcon as _borderWidthIcon,
@@ -128,9 +123,6 @@ const knobKeys: (keyof Knobs)[] = [
128
123
  "cornerSmoothing",
129
124
  "borderWidth",
130
125
  "elevation",
131
- "space",
132
- "size",
133
- "density",
134
126
  "textAccent",
135
127
  "headingFont",
136
128
  "bodyFont",
@@ -152,7 +144,6 @@ const stateKnobKeys: (keyof Knobs)[] = [
152
144
  "borderRadius",
153
145
  "borderWidth",
154
146
  "elevation",
155
- "space",
156
147
  "textAccent",
157
148
  "headingFont",
158
149
  "bodyFont",
@@ -170,8 +161,6 @@ const designProfiles: Record<string, DesignProfile> = {
170
161
  borderRadius: "small",
171
162
  borderWidth: "medium",
172
163
  elevation: "none",
173
- space: "medium",
174
- size: "medium",
175
164
  textAccent: "high",
176
165
  headingFont: "sans-serif",
177
166
  bodyFont: "sans-serif",
@@ -184,8 +173,6 @@ const designProfiles: Record<string, DesignProfile> = {
184
173
  borderRadius: "large",
185
174
  borderWidth: "none",
186
175
  elevation: "small",
187
- space: "medium",
188
- size: "medium",
189
176
  textAccent: "high",
190
177
  headingFont: "sans-serif",
191
178
  bodyFont: "sans-serif",
@@ -199,8 +186,6 @@ const designProfiles: Record<string, DesignProfile> = {
199
186
  borderRadius: "small",
200
187
  borderWidth: "small",
201
188
  elevation: "none",
202
- space: "medium",
203
- size: "medium",
204
189
  textAccent: "high",
205
190
  headingFont: "sans-serif",
206
191
  bodyFont: "sans-serif",
@@ -213,8 +198,6 @@ const designProfiles: Record<string, DesignProfile> = {
213
198
  borderRadius: "small",
214
199
  borderWidth: "small",
215
200
  elevation: "small",
216
- space: "medium",
217
- size: "medium",
218
201
  textAccent: "high",
219
202
  headingFont: "sans-serif",
220
203
  bodyFont: "sans-serif",
@@ -231,6 +214,15 @@ const profileNames = ["", ...Object.keys(designProfiles)] as const;
231
214
  // Helpers
232
215
  // ---------------------------------------------------------------------------
233
216
 
217
+ /** A preset picker option with the token scale it renders on (control / type px). */
218
+ function formatPresetOption(name: string): string {
219
+ if (name === "") return "custom";
220
+ const scale = getPreset(name)?.scale;
221
+ const control = scale?.control ?? defaultScaleInputs.control;
222
+ const type = scale?.type ?? defaultScaleInputs.type;
223
+ return `${name} · ${control}/${type}`;
224
+ }
225
+
234
226
  type SchemeHook = {
235
227
  setting: "system" | "light" | "dark";
236
228
  value: "light" | "dark";
@@ -705,18 +697,6 @@ function fillIcon(v: string, active: boolean, dark: boolean): ReactNode {
705
697
  return _fillIcon(v, ic(active, dark));
706
698
  }
707
699
 
708
- function spaceIcon(v: string, active: boolean, dark: boolean): ReactNode {
709
- return _spaceIcon(v, ic(active, dark));
710
- }
711
-
712
- function sizeIcon(v: string, active: boolean, dark: boolean): ReactNode {
713
- return _sizeIcon(v, ic(active, dark));
714
- }
715
-
716
- function densityIcon(v: string, active: boolean, dark: boolean): ReactNode {
717
- return _densityIcon(v, ic(active, dark));
718
- }
719
-
720
700
  function borderRadiusIcon(v: string, active: boolean, dark: boolean): ReactNode {
721
701
  return _borderRadiusIcon(v, ic(active, dark));
722
702
  }
@@ -1134,8 +1114,6 @@ export function ThemeDevtoolsPanel({
1134
1114
  borderRadius: Object.values(BorderRadius),
1135
1115
  borderWidth: Object.values(BorderWidth),
1136
1116
  elevation: Object.values(Elevation),
1137
- space: Object.values(Space),
1138
- size: Object.values(Size),
1139
1117
  textAccent: Object.values(TextAccent),
1140
1118
  headingFont: Object.values(HeadingFont),
1141
1119
  bodyFont: Object.values(BodyFont),
@@ -1155,7 +1133,6 @@ export function ThemeDevtoolsPanel({
1155
1133
  borderRadius: Object.values(BorderRadius),
1156
1134
  borderWidth: Object.values(BorderWidth),
1157
1135
  elevation: Object.values(Elevation),
1158
- space: Object.values(Space),
1159
1136
  textAccent: Object.values(TextAccent),
1160
1137
  headingFont: Object.values(HeadingFont),
1161
1138
  bodyFont: Object.values(BodyFont),
@@ -1353,7 +1330,7 @@ export function ThemeDevtoolsPanel({
1353
1330
  options={presetOptions}
1354
1331
  value={activePresetName ?? ""}
1355
1332
  onChange={(v) => handlePresetChange(v)}
1356
- format={(v) => (v === "" ? "custom" : String(v))}
1333
+ format={formatPresetOption}
1357
1334
  styles={styles}
1358
1335
  />
1359
1336
  )}
@@ -1406,46 +1383,6 @@ export function ThemeDevtoolsPanel({
1406
1383
  >
1407
1384
  {stateRows("fillStyle", Object.values(FillStyle), fillIcon)}
1408
1385
  </IconGroup>
1409
- <IconGroup
1410
- label="Size"
1411
- options={Object.values(Size)}
1412
- value={getOverride("size")}
1413
- onChange={(v) => set("size", v)}
1414
- icon={sizeIcon}
1415
- styles={styles}
1416
- isDark={isDark}
1417
- pinned={pinnedKnobs.has("size")}
1418
- onTogglePin={() => togglePin("size")}
1419
- onUnset={() => unset("size")}
1420
- />
1421
- <IconGroup
1422
- label="Space"
1423
- options={Object.values(Space)}
1424
- value={getOverride("space")}
1425
- onChange={(v) => set("space", v)}
1426
- icon={spaceIcon}
1427
- styles={styles}
1428
- isDark={isDark}
1429
- pinned={pinnedKnobs.has("space")}
1430
- onTogglePin={() => togglePin("space")}
1431
- expanded={expandedKnobs.has("space")}
1432
- onToggleExpand={() => toggleExpand("space")}
1433
- onUnset={() => unset("space")}
1434
- >
1435
- {stateRows("space", Object.values(Space), spaceIcon)}
1436
- </IconGroup>
1437
- <IconGroup
1438
- label="Density"
1439
- options={Object.values(Density)}
1440
- value={getOverride("density")}
1441
- onChange={(v) => set("density", v)}
1442
- icon={densityIcon}
1443
- styles={styles}
1444
- isDark={isDark}
1445
- pinned={pinnedKnobs.has("density")}
1446
- onTogglePin={() => togglePin("density")}
1447
- onUnset={() => unset("density")}
1448
- />
1449
1386
  <IconGroup
1450
1387
  label="Radius"
1451
1388
  options={Object.values(BorderRadius)}
@@ -1,44 +1,30 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { animationNames, animations } from "../animations/css";
2
+ import { motionStops, animations } from "../animations/css";
3
3
  import { extractAnimationNames } from "./animationOptions";
4
4
 
5
- describe("extractAnimationNames (MPO-447)", () => {
6
- it("lists the registered animations of a createTamagui config, not the driver's API", () => {
7
- const names = extractAnimationNames({ animations });
8
- expect(names).toEqual([...animationNames]);
9
- for (const member of [
10
- "usePresence",
11
- "ResetPresence",
12
- "inputStyle",
13
- "outputStyle",
14
- "useAnimatedNumber",
15
- "useAnimations",
16
- ]) {
17
- expect(names).not.toContain(member);
18
- }
5
+ describe("extractAnimationNames", () => {
6
+ it("offers only ordered motion stops on the house driver", () => {
7
+ expect(extractAnimationNames({ animations })).toEqual([...motionStops]);
19
8
  });
20
-
21
- it("offers exactly the house names on the house driver", () => {
22
- expect(extractAnimationNames({ animations })).toEqual([
23
- "bouncy",
24
- "lazy",
25
- "slow",
26
- "medium",
27
- "quick",
28
- "tooltip",
29
- "snappy",
30
- "gentle",
31
- ]);
32
- });
33
-
34
- it("reads an app's own driver names when it registers different ones", () => {
35
- const driver = { ...animations, animations: { fast: "ease-in 50ms", calm: "ease-in 900ms" } };
36
- expect(extractAnimationNames({ animations: driver })).toEqual(["fast", "calm"]);
9
+ it("filters custom drivers to registered knob stops in duration order", () => {
10
+ expect(
11
+ extractAnimationNames({
12
+ animations: {
13
+ animations: {
14
+ slow: "450ms",
15
+ quick: "150ms",
16
+ quickest: "80ms",
17
+ tooltip: "450ms",
18
+ "100ms": "100ms",
19
+ foreign: "1s",
20
+ },
21
+ },
22
+ }),
23
+ ).toEqual(["quickest", "quick", "slow"]);
37
24
  });
38
-
39
- it("falls back to the house names without a config or without named animations", () => {
40
- expect(extractAnimationNames(undefined)).toEqual([...animationNames]);
41
- expect(extractAnimationNames({})).toEqual([...animationNames]);
42
- expect(extractAnimationNames({ animations: { animations: {} } })).toEqual([...animationNames]);
25
+ it("returns no stops for an empty driver, and defaults when no driver is available", () => {
26
+ expect(extractAnimationNames({ animations: { animations: {} } })).toEqual([]);
27
+ expect(extractAnimationNames(undefined)).toEqual([...motionStops]);
28
+ expect(extractAnimationNames({})).toEqual([...motionStops]);
43
29
  });
44
30
  });
@@ -1,4 +1,4 @@
1
- import { animationNames } from "../animations/index";
1
+ import { motionStops } from "../animations/css";
2
2
 
3
3
  function isRecord(value: unknown): value is Record<string, unknown> {
4
4
  return typeof value === "object" && value !== null;
@@ -14,9 +14,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
14
14
  export function extractAnimationNames(tamaguiConfig: unknown): string[] {
15
15
  const driver = isRecord(tamaguiConfig) ? tamaguiConfig.animations : undefined;
16
16
  const named = isRecord(driver) ? driver.animations : undefined;
17
- if (isRecord(named)) {
18
- const keys = Object.keys(named);
19
- if (keys.length > 0) return keys;
20
- }
21
- return [...animationNames];
17
+ return isRecord(named)
18
+ ? motionStops.filter((stop) => Object.hasOwn(named, stop))
19
+ : [...motionStops];
22
20
  }
@@ -6,9 +6,6 @@ const knobKeys = [
6
6
  "cornerSmoothing",
7
7
  "borderWidth",
8
8
  "elevation",
9
- "space",
10
- "size",
11
- "density",
12
9
  "textAccent",
13
10
  "headingFont",
14
11
  "bodyFont",
@@ -18,29 +18,6 @@ export function fillIcon(value: string, color: string): ReactNode {
18
18
  return <div style={{ width: 14, height: 14, border: `1.5px solid ${color}`, borderRadius: 2 }} />;
19
19
  }
20
20
 
21
- export function spaceIcon(value: string, color: string): ReactNode {
22
- const size = ({ small: 8, medium: 12, large: 16 } as Record<string, number>)[value] ?? 12;
23
- return <div style={{ width: size, height: size, background: color, borderRadius: 2 }} />;
24
- }
25
-
26
- export function sizeIcon(value: string, color: string): ReactNode {
27
- const height = ({ small: 8, medium: 11, large: 14 } as Record<string, number>)[value] ?? 11;
28
- return <div style={{ width: 4, height: height, background: color, borderRadius: 1 }} />;
29
- }
30
-
31
- export function densityIcon(value: string, color: string): ReactNode {
32
- // Comfortable = taller stack; compact = compressed stack.
33
- const gap = value === "compact" ? 1 : 2;
34
- const barH = value === "compact" ? 3 : 4;
35
- return (
36
- <div style={{ display: "flex", flexDirection: "column", gap, width: 14 }}>
37
- <div style={{ height: barH, background: color, borderRadius: 1 }} />
38
- <div style={{ height: barH, background: color, borderRadius: 1 }} />
39
- <div style={{ height: barH, background: color, borderRadius: 1 }} />
40
- </div>
41
- );
42
- }
43
-
44
21
  export function borderRadiusIcon(value: string, color: string): ReactNode {
45
22
  const radius =
46
23
  ({ none: 0, small: 2, medium: 4, large: 7, full: 10 } as Record<string, number>)[value] ?? 0;
@@ -350,7 +327,10 @@ export function formAutofocusIcon(value: string, color: string): ReactNode {
350
327
  height: 14,
351
328
  border: `1.5px solid ${color}`,
352
329
  borderRadius: 2,
353
- boxShadow: value === "on" ? `0 0 0 2px ${color}` : undefined,
330
+ outlineWidth: value === "on" ? 2 : 0,
331
+ outlineStyle: "solid",
332
+ outlineColor: color,
333
+ outlineOffset: 0,
354
334
  opacity: value === "on" ? 1 : 0.45,
355
335
  }}
356
336
  />
@@ -142,36 +142,15 @@ describe("$outlineColor token contrast (DF-03 / DG-A11Y-01)", () => {
142
142
  }
143
143
  }
144
144
 
145
- it("the retired flat inset failed the floor over the solid intent fills (why offset is 0)", () => {
146
- // The failures that forced the flip (MPO-41 / MPO-19 X7): a −2 band paints
147
- // INSIDE the control, so it had to contrast against the solid intent fill.
148
- //
149
- // Provenance, because the two numbers are easy to conflate. The headline
150
- // figures — light accent 1.27:1, light error 1.17:1, dark error 1.31:1 —
151
- // are RENDERED measurements off the action-and-feedback board at DPR 2,
152
- // ring node against the Button's painted background. This test computes a
153
- // DIFFERENT pair: the base `outlineColor` against the intent sub-theme's
154
- // `color9` ink step (1.67 / 1.55 / 1.44 in this build). Same verdict, same
155
- // reason, different surfaces — the board's error fills (#ce2c31 / #ff9794)
156
- // are not in these default sub-theme ramps at all, so the fill side is the
157
- // board's to own and not this package's.
158
- //
159
- // What this assertion IS: no sub-theme ink step the inset band could land
160
- // on clears the floor, which keeps the offset-0 geometry load-bearing. If
161
- // the ramps ever lighten past 3:1 this test goes stale, not wrong.
162
- for (const [scheme, intent] of [
163
- ["light", "accent"],
164
- ["light", "error"],
165
- ["dark", "error"],
166
- ] as const) {
167
- const ring = normalizeToHex(themes[scheme].outlineColor);
168
- const fill = normalizeToHex(themes[`${scheme}_${intent}`].color9);
169
- expect(ring).toBeTruthy();
170
- expect(fill).toBeTruthy();
171
- expect(contrastRatio(relativeLuminance(ring!), relativeLuminance(fill!))).toBeLessThan(
172
- FOCUS_RING_MIN_CONTRAST,
173
- );
174
- }
145
+ it("an inset ring cannot guarantee contrast over every solid intent fill", () => {
146
+ const ratios = (["light", "dark"] as const).flatMap((scheme) =>
147
+ ["accent", "error", "success", "warning"].map((intent) => {
148
+ const ring = normalizeToHex(themes[scheme].outlineColor)!;
149
+ const fill = normalizeToHex(themes[`${scheme}_${intent}_Button`].background)!;
150
+ return contrastRatio(relativeLuminance(ring), relativeLuminance(fill));
151
+ }),
152
+ );
153
+ expect(ratios.some((ratio) => ratio < FOCUS_RING_MIN_CONTRAST)).toBe(true);
175
154
  });
176
155
  });
177
156
 
@@ -22,12 +22,17 @@ export * from "./layoutTokens";
22
22
  export * from "./Preset";
23
23
  export type { Preset as PresetDefinition, IntentOverride } from "./preset.types";
24
24
  export * from "./PresetContext";
25
+ export * from "./presetPreview";
25
26
  export * from "./presets";
27
+ export * from "./presetStore";
28
+ export * from "./presetUserSync";
26
29
  export * from "./radiusClass";
27
30
  export * from "./radiusClassTable.generated";
28
31
  export * from "./readableColor";
29
32
  export * from "./recipes";
30
33
  export * from "./resolveKnobs";
34
+ export * from "./schemeStore";
35
+ export * from "./scale/index";
31
36
  export * from "./shared";
32
37
  export * from "./themeValue";
33
38
  export * from "./sizeRecipes";
@@ -40,4 +45,16 @@ export * from "./theme";
40
45
  export * from "./Tint";
41
46
  export * from "./useResolvedKnobs";
42
47
  export * from "./useTheme";
48
+ export * from "./usePresetSwitch";
49
+ export * from "./useSchemePreference";
43
50
  export * from "./useTouchSurface";
51
+ export * from "./overlaySurfaces";
52
+
53
+ export * from "./motion";
54
+ export {
55
+ animationConfig as cssAnimationConfig,
56
+ animationDurations,
57
+ motionStops,
58
+ type MotionStop,
59
+ } from "./animations/css";
60
+ export { animationConfig as nativeAnimationConfig } from "./animations/reactNative";
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
3
+ import { createThemesBuilder } from "./createThemes";
4
+ import { defaultAccentTheme } from "./defaults/accent";
5
+ import { defaultBaseTheme } from "./defaults/base";
6
+ import { defaultBuilderOptions } from "./defaults/builderOptions";
7
+
8
+ const themes = createThemesBuilder(
9
+ defaultBaseTheme,
10
+ defaultAccentTheme,
11
+ defaultBuilderOptions,
12
+ ).themes() as Record<string, Record<string, string>>;
13
+
14
+ const hex = (raw: string) => {
15
+ const value = normalizeToHex(raw);
16
+ if (!value) throw new Error(`not an opaque colour: ${raw}`);
17
+ return value;
18
+ };
19
+ const luminance = (raw: string) => relativeLuminance(hex(raw));
20
+ const ratio = (a: string, b: string) => contrastRatio(luminance(a), luminance(b));
21
+ const lightness = (raw: string) => {
22
+ const value = hex(raw);
23
+ const channels = [1, 3, 5].map((offset) => Number.parseInt(value.slice(offset, offset + 2), 16));
24
+ return ((Math.max(...channels) + Math.min(...channels)) / (2 * 255)) * 100;
25
+ };
26
+
27
+ describe("FZ-7158 WP-b5: semantic Button fills hold their pale ink", () => {
28
+ for (const scheme of ["light", "dark"] as const) {
29
+ for (const intent of ["error", "warning", "success"] as const) {
30
+ it(`${scheme}_${intent}_Button darkens from its resting fill in both interaction states`, () => {
31
+ const theme = themes[`${scheme}_${intent}_Button`];
32
+ expect(lightness(theme.background)).toBeLessThan(60);
33
+ expect(luminance(theme.color)).toBeGreaterThan(0.5);
34
+ for (const fill of [theme.background, theme.backgroundHover, theme.backgroundPress]) {
35
+ expect(ratio(theme.color, fill)).toBeGreaterThanOrEqual(4.5);
36
+ }
37
+ expect(lightness(theme.backgroundHover)).toBeLessThan(lightness(theme.background));
38
+ expect(lightness(theme.backgroundPress)).toBeLessThan(lightness(theme.backgroundHover));
39
+ });
40
+ }
41
+ }
42
+ });
@@ -55,27 +55,6 @@ export const Elevation = {
55
55
  } as const;
56
56
  export type Elevation = (typeof Elevation)[keyof typeof Elevation];
57
57
 
58
- export const Space = {
59
- Small: "small",
60
- Medium: "medium",
61
- Large: "large",
62
- } as const;
63
- export type Space = (typeof Space)[keyof typeof Space];
64
-
65
- export const Size = {
66
- Small: "small",
67
- Medium: "medium",
68
- Large: "large",
69
- } as const;
70
- export type Size = (typeof Size)[keyof typeof Size];
71
-
72
- /** Global density mode — compact steps space down one level; size is independent. */
73
- export const Density = {
74
- Comfortable: "comfortable",
75
- Compact: "compact",
76
- } as const;
77
- export type Density = (typeof Density)[keyof typeof Density];
78
-
79
58
  // ── House decisions (design-guidelines.md) ─────────────────
80
59
  // Behavioral knobs with recommended defaults — not hard constraints.
81
60
 
@@ -191,17 +170,11 @@ export const FontWeight = {
191
170
  export type FontWeight = (typeof FontWeight)[keyof typeof FontWeight];
192
171
 
193
172
  /**
194
- * Page-title type step (D-11 hero-H1 dial, owner ruling 2026-08-12).
195
- *
196
- * `moderate` ($8 = 32px) is the product default: a page title is wayfinding,
197
- * not content, so it does not spend the display scale's emphasis budget
198
- * (Axiom 10) — and the 64px step is the direct amplifier of the DF-02 title
199
- * clip. `display` ($10 = 64px) is the deliberate marketing/hero opt-in,
200
- * reached through the `hero` preset or an explicit knob override; it is never
201
- * the ambient default.
202
- *
203
- * Two values, not a token dial: a free size invites per-screen drift
204
- * (Axiom 5), and the semantic level stays `headingLevel`'s job either way.
173
+ * Page-title type step (D-11 dial). `moderate` ($10, 40 px at type 15) is the
174
+ * product default: a page title is wayfinding, not content (Axiom 10).
175
+ * `display` ($12, 52 px) is the hero opt-in, through `heroPreset` or an
176
+ * explicit override. Two values, not a free size: a free size invites
177
+ * per-screen drift (Axiom 5), and the semantic level stays `headingLevel`'s.
205
178
  */
206
179
  export const PageTitleScale = {
207
180
  Moderate: "moderate",
@@ -211,9 +184,16 @@ export type PageTitleScale = (typeof PageTitleScale)[keyof typeof PageTitleScale
211
184
 
212
185
  // ── Animation ──────────────────────────────────────────────
213
186
 
214
- import type { AnimationName } from "./animations/index";
187
+ import { motionStops, type MotionStop } from "./motionStops";
188
+
189
+ export type Animation = "none" | MotionStop;
215
190
 
216
- export type Animation = "none" | AnimationName;
191
+ export function normalizeAnimationKnob(value: unknown): Animation | undefined {
192
+ if (value === "none" || motionStops.includes(value as MotionStop)) return value as Animation;
193
+ if (value === "tooltip" || value === "gentle") return "slow";
194
+ if (value === "snappy") return "quickest";
195
+ return undefined;
196
+ }
217
197
 
218
198
  // ── Interaction state overrides ────────────────────────────
219
199
  // Structural knobs that can vary per interaction state.
@@ -223,7 +203,6 @@ export interface StateKnobs {
223
203
  borderRadius?: BorderRadius;
224
204
  borderWidth?: BorderWidth;
225
205
  elevation?: Elevation;
226
- space?: Space;
227
206
  textAccent?: TextAccent;
228
207
  headingFont?: HeadingFont;
229
208
  bodyFont?: BodyFont;
@@ -250,10 +229,6 @@ export interface Knobs {
250
229
  cornerSmoothing?: CornerSmoothing;
251
230
  borderWidth: BorderWidth;
252
231
  elevation: Elevation;
253
- space: Space;
254
- size: Size;
255
- /** Propagating density mode. Default comfortable; compact steps space only. */
256
- density: Density;
257
232
  textAccent: TextAccent;
258
233
  headingFont: HeadingFont;
259
234
  bodyFont: BodyFont;
@@ -296,9 +271,6 @@ export const defaultKnobs: Knobs = {
296
271
  cornerSmoothing: "round",
297
272
  borderWidth: "medium",
298
273
  elevation: "small",
299
- space: "medium",
300
- size: "medium",
301
- density: "comfortable",
302
274
  textAccent: "high",
303
275
  headingFont: "sans-serif",
304
276
  bodyFont: "sans-serif",
@@ -205,6 +205,17 @@ export function readingWidthStyle(
205
205
  return { maxWidth: `${ch}ch`, width: "100%" };
206
206
  }
207
207
 
208
+ // ── Form readable width ───────────────────────────────────────────────────
209
+
210
+ /**
211
+ * A form column's readable max width: wide enough for two FormGrid columns
212
+ * (FORM_GRID_COLLAPSE_WIDTH 480), narrow enough that a label, its field and
213
+ * the actions read as one group on a wide screen. Inside the 480-640 band.
214
+ */
215
+ export const FORM_READABLE_WIDTH = 560;
216
+ export const FORM_READABLE_WIDTH_MIN = 480;
217
+ export const FORM_READABLE_WIDTH_MAX = 640;
218
+
208
219
  // ── Min press target (DG-LAY-04) ──────────────────────────────────────────
209
220
 
210
221
  /** Platform floor for interactive targets (Fluent/HIG web+iOS 44; M3 48 on Android). */
@@ -283,15 +294,18 @@ export const OVERLAY_ATTACH_GAP = 0;
283
294
  // ── Semantic gaps (DG-LAY-03) ─────────────────────────────────────────────
284
295
 
285
296
  /**
286
- * Semantic spacing roles map onto the space-knob recipes:
297
+ * Semantic spacing roles (SP-GAP): `tight` → `knobProps.gapSm`,
287
298
  * `within-group` → `knobProps.gap`, `between-groups` → `knobProps.gapLg`.
299
+ * Fixed `$space` keys; the active scale supplies their px.
288
300
  */
289
301
  export function useSemanticGaps(): {
302
+ tight: { gap: string };
290
303
  withinGroup: { gap: string };
291
304
  betweenGroups: { gap: string };
292
305
  } {
293
306
  const { knobProps } = useResolvedKnobs();
294
307
  return {
308
+ tight: knobProps.gapSm,
295
309
  withinGroup: knobProps.gap,
296
310
  betweenGroups: knobProps.gapLg,
297
311
  };
@@ -4,7 +4,7 @@ import { TestProviders } from "@multiplatform.one/test-utils";
4
4
  * viewport-driven `useLayoutSizeClass`, the pane budget `useMultiPane`, and
5
5
  * the semantic gap roles `useSemanticGaps`. Locks down: widths resolve to the
6
6
  * canonical size classes at mount, one matchMedia listener per breakpoint is
7
- * registered and removed, and gap roles ride the space-knob recipes.
7
+ * registered and removed, and gap roles are the fixed spacing roles.
8
8
  */
9
9
 
10
10
  import { act, renderHook } from "@testing-library/react";
@@ -147,13 +147,17 @@ describe("useMultiPane", () => {
147
147
  });
148
148
 
149
149
  describe("useSemanticGaps", () => {
150
- it("maps within-group to knob gap and between-groups to the large gap", () => {
150
+ it("maps tight to gapSm, within-group to gap and between-groups to gapLg", () => {
151
151
  setViewportWidth(1000);
152
152
  const { result: knobs } = renderHook(() => useResolvedKnobs(), { wrapper: TestProviders });
153
153
  const { result } = renderHook(() => useSemanticGaps(), { wrapper: TestProviders });
154
+ expect(result.current.tight).toEqual(knobs.current.knobProps.gapSm);
154
155
  expect(result.current.withinGroup).toEqual(knobs.current.knobProps.gap);
155
156
  expect(result.current.betweenGroups).toEqual(knobs.current.knobProps.gapLg);
156
- // The roles must differ, or grouping hierarchy is invisible.
157
- expect(result.current.withinGroup.gap).not.toBe(result.current.betweenGroups.gap);
157
+ expect(result.current).toEqual({
158
+ tight: { gap: "$2" },
159
+ withinGroup: { gap: "$3" },
160
+ betweenGroups: { gap: "$5" },
161
+ });
158
162
  });
159
163
  });
@@ -0,0 +1,20 @@
1
+ import type { TamaguiInternalConfig } from "@tamagui/web";
2
+
3
+ export type TokensGlobal = { __tamaguiTokensMerged?: unknown };
4
+
5
+ /**
6
+ * The `$`-prefixed and bare token lookup `getTokenValue("$4", "size")` reads.
7
+ * `createTamagui` publishes it as a global beside the config, so installing a
8
+ * config without it would leave token reads on the previous config's values.
9
+ */
10
+ export function mergedTokens(
11
+ config: TamaguiInternalConfig,
12
+ ): Record<string, Record<string, unknown>> {
13
+ const merged: Record<string, Record<string, unknown>> = {};
14
+ const tokens = config.tokens as unknown as Record<string, Record<string, unknown>> | undefined;
15
+ const parsed = config.tokensParsed as unknown as Record<string, Record<string, unknown>>;
16
+ for (const category of Object.keys(parsed)) {
17
+ merged[category] = { ...tokens?.[category], ...parsed[category] };
18
+ }
19
+ return merged;
20
+ }