@multiplatform.one/theme 7.34.4 → 7.35.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,22 +1,10 @@
1
1
  import type { CreateThemesBuilderOptions, GetThemeProps } from "../createThemes";
2
- import {
3
- contrastRatio,
4
- minContrastRatio,
5
- normalizeToHex,
6
- pickReadableForeground,
7
- relativeLuminance,
8
- } from "../colorRules";
2
+ import { contrastRatio, minContrastRatio, normalizeToHex, relativeLuminance } from "../colorRules";
9
3
 
10
4
  /**
11
- * DG-A11Y-01 / Axiom 12 — resolve a solid `$outlineColor` for the keyboard
12
- * focus-visible ring that clears the ≥3:1 contrast floor against BOTH page
13
- * surfaces (color1/color2) in this scheme's own ramp. The house themes never
14
- * set `$outlineColor`, so it fell through to Tamagui's stock 20%-alpha ink
15
- * (measured 1.36:1) — every keyboard user got a near-invisible ring (DF-03).
16
- *
17
- * Steps are tried strongest-visible-first; the first that clears 3:1 vs both
18
- * page steps wins, else the highest-contrast candidate. The ring sits offset
19
- * outside the control on the page, so page contrast is the governing pair.
5
+ * DG-A11Y-01 / DF-03. The external focus ring must clear 3:1 against both page
6
+ * surfaces in its own scheme. Prefer a solid palette step over a low-alpha
7
+ * outline.
20
8
  */
21
9
  function pickFocusRingColorFromSteps(step: (n: number) => string | undefined): string {
22
10
  const fallback = step(11) ?? step(12) ?? step(9) ?? "transparent";
@@ -49,38 +37,38 @@ function pickFocusRingColor(theme: Record<string, string>): string {
49
37
  return pickFocusRingColorFromSteps((n) => theme[`color${n}`]);
50
38
  }
51
39
 
40
+ // DF-06 / WCAG 1.4.11: control shapes need 3:1 on page, surface1 and control wells.
41
+ function pickBoundaryColor(theme: Record<string, string>): string {
42
+ const fallback = theme.color12 ?? "transparent";
43
+ const surfaces = [theme.background, theme.color3, theme.color5].map((raw) =>
44
+ normalizeToHex(raw ?? ""),
45
+ );
46
+ if (surfaces.some((hex) => !hex)) return fallback;
47
+ const luminances = surfaces.map((hex) => relativeLuminance(hex!));
48
+ let best = fallback;
49
+ let bestMin = 0;
50
+ for (const key of [8, 9, 10, 11, 12]) {
51
+ const raw = theme[`color${key}`];
52
+ const hex = raw ? normalizeToHex(raw) : null;
53
+ if (!raw || !hex) continue;
54
+ const luminance = relativeLuminance(hex);
55
+ const minC = Math.min(...luminances.map((surface) => contrastRatio(luminance, surface)));
56
+ if (minC >= minContrastRatio) return raw;
57
+ if (minC > bestMin) {
58
+ bestMin = minC;
59
+ best = raw;
60
+ }
61
+ }
62
+ return best;
63
+ }
64
+
52
65
  function getBaseTheme(theme: Record<string, string>) {
53
66
  return {
54
- // DG-A11Y-01: solid, scheme-aware focus ring (≥3:1 vs page surfaces).
55
- // Additive — Tamagui's own ring is `rgba(ink, 0.2)`, which measured
56
- // 1.36:1. This is an improvement and stays (M5).
57
67
  outlineColor: pickFocusRingColor(theme),
58
- // Align placeholder/disabled text with the dedicated muted slot.
59
- placeholderColor: theme.color10,
60
- //
61
- // MPO-19 X4 — SEVEN LINES DELETED HERE, deliberately.
62
- //
63
- // `backgroundPress: color5`, `backgroundFocus: color6`,
64
- // `backgroundActive: color4`, `borderColor: color5`,
65
- // `borderColorHover: color6`, `borderColorFocus: color7` and
66
- // `borderColorPress: color7` used to sit in this object, re-anchoring
67
- // Tamagui's whole interaction ramp one to three steps deeper. The only
68
- // justification written down was "Normalize interaction ramp across
69
- // components", and uniformity is an argument for HAVING a ramp, not for
70
- // these values. The effect was that every mpo border read heavier and
71
- // every focus fill landed darker than the same component in Tamagui.
72
- //
73
- // The theme builder's own templates already derive all seven from the
74
- // background index, so deleting the pins does not leave a hole: it hands
75
- // the keys back to the layer that owns them. If uniformity is wanted
76
- // again, it gets re-derived from the template offsets rather than pinned
77
- // as literal ramp steps.
78
- //
79
- // The semantic aliases below are a different thing and stay: they name
80
- // slots Tamagui has no token for at all, so they contradict nothing.
81
- // Project-level semantic aliases for palette slots not covered by Tamagui defaults.
82
- // textMuted sits on the $color11 tier: the lowest ramp step that keeps
83
- // secondary text AA-readable on color1/2 surfaces (SB-R contrast sweep).
68
+ boundaryColor: pickBoundaryColor(theme),
69
+ switchThumb: "#ffffff",
70
+ // The v5 makeSurface templates own the interaction ramp and borders.
71
+ // No ramp step is pinned here; only semantic aliases are added.
84
72
  textMuted: theme.color11,
85
73
  textSubtle: theme.color6,
86
74
  borderStrong: theme.color7,
@@ -96,25 +84,35 @@ function getBaseTheme(theme: Record<string, string>) {
96
84
  // accent — SB-E-01). The accent theme is solid by palette shape already.
97
85
  const solidIntentParents = new Set(["error", "warning", "success"]);
98
86
 
87
+ function parseHsl(raw: string): [number, number, number] | undefined {
88
+ const match = raw.match(/hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%/);
89
+ if (!match) return undefined;
90
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
91
+ }
92
+
93
+ function lowerLightness(raw: string, points: number): string {
94
+ const hsl = parseHsl(raw);
95
+ if (!hsl) return raw;
96
+ const [h, s, l] = hsl;
97
+ return `hsl(${h}, ${s}%, ${Math.max(0, l - points)}%)`;
98
+ }
99
+
99
100
  /**
100
- * Adjust a background's lightness until it clears WCAG AA (4.5:1) against
101
- * the given foreground. Steps away from the foreground's lightness 1.5% at a
102
- * time; bails (returns the original) on unparseable colors.
101
+ * Darken a background until its pale foreground clears WCAG AA (4.5:1).
102
+ * Unparseable colors retain their original background.
103
103
  */
104
104
  function clampBgForReadableText(bgRaw: string, fgRaw: string): string {
105
105
  const fgHex = normalizeToHex(fgRaw);
106
106
  let bgHex = normalizeToHex(bgRaw);
107
107
  if (!fgHex || !bgHex) return bgRaw;
108
108
  const fgLum = relativeLuminance(fgHex);
109
- const hslMatch = bgRaw.match(/hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%/);
110
- if (!hslMatch) return bgRaw;
111
- const h = Number(hslMatch[1]);
112
- const s = Number(hslMatch[2]);
113
- let l = Number(hslMatch[3]);
114
- const dir = fgLum > relativeLuminance(bgHex) ? -1 : 1;
115
- for (let i = 0; i < 24; i++) {
109
+ const hsl = parseHsl(bgRaw);
110
+ if (!hsl) return bgRaw;
111
+ const [h, s, initialLightness] = hsl;
112
+ let l = initialLightness;
113
+ for (let i = 0; i < 40; i++) {
116
114
  if (contrastRatio(fgLum, relativeLuminance(bgHex)) >= 4.5) break;
117
- l = Math.min(100, Math.max(0, l + dir * 1.5));
115
+ l = Math.max(0, l - 1.5);
118
116
  const next = normalizeToHex(`hsl(${h}, ${s}%, ${l}%)`);
119
117
  if (!next) return bgRaw;
120
118
  bgHex = next;
@@ -124,39 +122,28 @@ function clampBgForReadableText(bgRaw: string, fgRaw: string): string {
124
122
 
125
123
  export const defaultBuilderOptions: CreateThemesBuilderOptions = {
126
124
  getTheme: ({ name, theme, palette }: GetThemeProps): Record<string, string> => {
127
- // Component sub-themes (t_Card, t_ListItem, t_Button, ...) are narrow
128
- // templates -- background/color steps only, NO colorN/border tokens. An
129
- // element inside one that sets borderColor="$borderColor" resolves
130
- // nothing and CSS falls back to currentColor: near-black borders on
131
- // every bordered card/row. Anchor border tokens from the scheme
132
- // palette (padded shape: [(len-12)/2 alpha anchors] + 12 colors +
133
- // [anchors]; colorN = colors[N-1]).
125
+ // Surface templates carry their own borders and states, but no colorN ramp.
134
126
  if (!theme.color5) {
135
127
  if (!palette) return {};
136
128
  const colors0 = Math.max(0, Math.floor((palette.length - 12) / 2));
137
129
  const step = (i: number): string => palette[colors0 + i] ?? "transparent";
138
- // DF-03: component sub-theme templates (t_Input, t_Button, ...) carry
139
- // Tamagui's stock 20%-alpha `outlineColor`, which silently overrides the
140
- // base theme's solid ring inside those scopes. Re-derive the solid,
141
- // scheme-aware ring from this sub-theme's own palette steps.
130
+ // A sub-theme can replace the palette, so its ring must follow that palette.
142
131
  const outlineColor = pickFocusRingColorFromSteps((n) => step(n - 1));
143
132
 
144
- // Solid semantic-intent Buttons (light_error_Button, dark_warning_Button,
145
- // ...): surface on the hue's step 11 with a LUMINANCE-COMPUTED foreground
146
- // (Axiom 15 / LC-05) — the hue's step-1 or step-12 anchor, whichever
147
- // reads better on the resting fill. The sub-theme `color` carries it so
148
- // labels ($color via useResolvedKnobs), icons (currentColor) and any
149
- // inherited text all pair correctly. Every interaction fill is then
150
- // darkened/lightened until it holds that foreground at the 4.5 AA floor.
133
+ // Solid semantic-intent Buttons pair Radix 9 with the hue's pale anchor
134
+ // at a 4.5:1 floor. Hover and press darken that fill in both schemes so
135
+ // inherited labels and icons keep the same readable foreground.
151
136
  const parts = name.split("_");
152
137
  if (parts[parts.length - 1] === "Button" && solidIntentParents.has(parts[parts.length - 2])) {
153
- const scheme = parts[0];
154
- // Luminance-computed on-fill foreground (Axiom 15): the hue's step-1
155
- // (pale) or step-12 (deep) anchor, whichever reads better on the fill.
156
- const fg = pickReadableForeground(step(10), step(0), step(11));
157
- const bg = clampBgForReadableText(step(10), fg);
158
- const hover = clampBgForReadableText(scheme === "light" ? step(9) : step(11), fg);
159
- const press = clampBgForReadableText(scheme === "light" ? step(11) : step(9), fg);
138
+ const firstHex = normalizeToHex(step(0));
139
+ const lastHex = normalizeToHex(step(11));
140
+ const fg =
141
+ firstHex && lastHex && relativeLuminance(lastHex) > relativeLuminance(firstHex)
142
+ ? step(11)
143
+ : step(0);
144
+ const bg = clampBgForReadableText(step(8), fg);
145
+ const hover = lowerLightness(bg, 4);
146
+ const press = lowerLightness(bg, 8);
160
147
  return {
161
148
  outlineColor,
162
149
  background: bg,
@@ -176,10 +163,6 @@ export const defaultBuilderOptions: CreateThemesBuilderOptions = {
176
163
 
177
164
  return {
178
165
  outlineColor,
179
- borderColor: step(4),
180
- borderColorHover: step(5),
181
- borderColorFocus: step(6),
182
- borderColorPress: step(6),
183
166
  };
184
167
  }
185
168
  return getBaseTheme(theme);
@@ -11,6 +11,16 @@ import { defaultFonts } from "./fonts";
11
11
  * and another there, which is the same one-source failure MPO-44 was about.
12
12
  */
13
13
  export const defaultCategoryFonts = {
14
+ headingSerif: defaultFonts.headingSerif,
15
+ headingMono: defaultFonts.headingMono,
16
+ headingSlab: defaultFonts.headingSlab,
17
+ headingRounded: defaultFonts.headingRounded,
18
+ headingCondensed: defaultFonts.headingCondensed,
19
+ headingCursive: defaultFonts.headingCursive,
20
+ headingHandwriting: defaultFonts.headingHandwriting,
21
+ headingPixel: defaultFonts.headingPixel,
22
+ headingBlackletter: defaultFonts.headingBlackletter,
23
+ headingGeometric: defaultFonts.headingGeometric,
14
24
  serif: defaultFonts.serif,
15
25
  mono: defaultFonts.mono,
16
26
  slab: defaultFonts.slab,
@@ -1,4 +1,10 @@
1
+ import { createRequire } from "node:module";
1
2
  import { describe, expect, it } from "vitest";
3
+ import { createFontLoader } from "../../fonts/createFontLoader.js";
4
+ import { generateScale } from "../scale/generateScale";
5
+ import { defaultScaleInputs } from "../scale/scaleInputs";
6
+ import { scaleFontSet } from "../scale/scaleFonts";
7
+ import * as fontDefaults from "./fonts";
2
8
  import { createDefaultFont } from "../../font";
3
9
  import { flooredLineHeight, fontCategoryMetrics } from "../fontCategoryMetrics";
4
10
  import { fontCategoryStacks } from "../fontCategoryStacks";
@@ -78,9 +84,7 @@ describe("default heading/body fonts — tracking-by-size (Axiom 15 OPTICS, LC-1
78
84
  it("heading H1–H4 land on the expected optical tracking (snapshot)", () => {
79
85
  const size = table(defaultHeadingFont, "size");
80
86
  const letterSpacing = table(defaultHeadingFont, "letterSpacing");
81
- // MPO-19 X8: heading rides the body scale now, so H1 $10 = 40px at
82
- // weight 800 rather than 64px at 400. Tracking follows the rendered
83
- // size, so every value moves with it — that IS the invariant.
87
+ // Tracking follows the rendered size on the shared heading/body ladder.
84
88
  expect({
85
89
  h1: { size: size["10"], tracking: letterSpacing["10"] },
86
90
  h2: { size: size["9"], tracking: letterSpacing["9"] },
@@ -113,7 +117,7 @@ describe("bodyFont category fonts — one ladder, distinct families (MPO-44)", (
113
117
  const bodyStops = ["body", "serif", "mono", "rounded"] as const;
114
118
  const firstFamily = (stack: string) => stack.split(",")[0]?.replace(/['"]/g, "").trim();
115
119
  const categories = Object.keys(defaultFonts).filter(
116
- (name) => name !== "heading" && name !== "body",
120
+ (name) => !name.startsWith("heading") && name !== "body",
117
121
  ) as FontCategory[];
118
122
  const bodySize = table(defaultBodyFont, "size");
119
123
  const bodyLineHeight = table(defaultBodyFont, "lineHeight");
@@ -238,3 +242,79 @@ function fontFamily(font: unknown): string {
238
242
  if (family && typeof family.val === "string") return family.val;
239
243
  return "";
240
244
  }
245
+
246
+ describe("heading scope categories", () => {
247
+ it("keeps true at the size-four weight", () => {
248
+ expect(strTable(defaultHeadingFont, "weight").true).toBe("600");
249
+ });
250
+
251
+ it("registers ten heading categories on heading tables with category leading floors", () => {
252
+ const keys = fontDefaults.headingCategoryFontKeys;
253
+ expect(Object.keys(keys ?? {})).toHaveLength(10);
254
+ const headingSizes = table(defaultHeadingFont, "size");
255
+ const headingLeading = table(defaultHeadingFont, "lineHeight");
256
+ for (const [category, key] of Object.entries(keys)) {
257
+ const font = defaultFonts[key];
258
+ expect(fontFamily(font)).toBe(fontCategoryStacks[category as FontCategory]);
259
+ expect(table(font, "size")).toEqual(headingSizes);
260
+ expect(strTable(font, "weight")).toEqual(strTable(defaultHeadingFont, "weight"));
261
+ for (const [step, size] of Object.entries(headingSizes)) {
262
+ expect(table(font, "lineHeight")[step]).toBe(
263
+ flooredLineHeight(
264
+ size,
265
+ headingLeading[step]!,
266
+ fontCategoryMetrics[category as FontCategory].heading,
267
+ ),
268
+ );
269
+ expect(table(font, "letterSpacing")[step]).toBe(0);
270
+ }
271
+ }
272
+ });
273
+
274
+ it("rebuilds heading categories on the target scale and preserves consumer families and weights", () => {
275
+ const from = generateScale(defaultScaleInputs).fonts;
276
+ const to = generateScale({ ...defaultScaleInputs, type: 19 }).fonts;
277
+ const scaled = scaleFontSet(defaultFonts, from, to);
278
+ expect(scaled.headingSerif).toBeDefined();
279
+ for (const [category, key] of Object.entries(fontDefaults.headingCategoryFontKeys)) {
280
+ expect(table(scaled[key], "size")).toEqual(to.headingSizes);
281
+ for (const [step, size] of Object.entries(to.headingSizes)) {
282
+ expect(table(scaled[key], "lineHeight")[step]).toBe(
283
+ flooredLineHeight(
284
+ size,
285
+ to.headingLineHeights[step]!,
286
+ fontCategoryMetrics[category as FontCategory].heading,
287
+ ),
288
+ );
289
+ }
290
+ expect(strTable(scaled[key], "weight").true).toBe("600");
291
+ }
292
+ const custom = { ...defaultFonts.heading, family: "Custom", weight: { 4: "500", true: "500" } };
293
+ const overridden = scaleFontSet({ headingSerif: custom }, from, to);
294
+ expect(fontFamily(overridden.headingSerif)).toBe("Custom");
295
+ expect(strTable(overridden.headingSerif, "weight").true).toBe("500");
296
+ });
297
+
298
+ it("loads every native face declared by the purpose fonts", () => {
299
+ const require = createRequire(import.meta.url);
300
+ const original = require.extensions[".otf"];
301
+ require.extensions[".otf"] = (module) => {
302
+ module.exports = 1;
303
+ };
304
+ try {
305
+ const fonts = createFontLoader({ inter: true, fonts: [] }).importFonts();
306
+ expect(Object.keys(fonts).sort()).toEqual(
307
+ ["Inter", "InterMedium", "InterSemiBold", "InterBold", "InterExtraBold"].sort(),
308
+ );
309
+ for (const font of [defaultHeadingFont, defaultBodyFont]) {
310
+ expect(font.face[500]?.normal).toBe("InterMedium");
311
+ for (const face of Object.values(font.face)) {
312
+ expect(fonts).toHaveProperty(face.normal!.split(",")[0]!.trim());
313
+ }
314
+ }
315
+ } finally {
316
+ if (original) require.extensions[".otf"] = original;
317
+ else delete require.extensions[".otf"];
318
+ }
319
+ });
320
+ });