@multiplatform.one/theme 7.34.3 → 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,173 +1,80 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import {
3
- resolveAaSolidFill,
4
- resolveAccentTintedSurface,
5
- resolveLegibleInkOn,
6
- } from "./readableColor";
2
+ import { resolveAccentTintedSurface, resolveLegibleInkOn } from "./readableColor";
7
3
  import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
8
4
 
9
- // The stock accent ramp (defaults/accent.ts) as the theme resolves it.
10
- const lightAccent = [
11
- "hsla(250, 50%, 62%, 1)",
12
- "hsla(250, 50%, 58%, 1)",
13
- "hsla(250, 50%, 54%, 1)",
14
- "hsla(250, 50%, 48%, 1)",
15
- "hsla(250, 50%, 46%, 1)",
16
- "hsla(250, 50%, 44%, 1)",
17
- "hsla(250, 50%, 42%, 1)",
18
- "hsla(250, 50%, 40%, 1)",
19
- "hsla(250, 50%, 38%, 1)",
20
- "hsla(250, 50%, 36%, 1)",
21
- "hsla(250, 50%, 95%, 1)",
22
- "hsla(250, 50%, 98%, 1)",
23
- ];
24
- const darkAccent = [
25
- "hsla(250, 50%, 35%, 1)",
26
- "hsla(250, 50%, 38%, 1)",
27
- "hsla(250, 50%, 41%, 1)",
28
- "hsla(250, 50%, 43%, 1)",
29
- "hsla(250, 50%, 46%, 1)",
30
- "hsla(250, 50%, 49%, 1)",
31
- "hsla(250, 50%, 52%, 1)",
32
- "hsla(250, 50%, 54%, 1)",
33
- "hsla(250, 50%, 57%, 1)",
34
- "hsla(250, 50%, 60%, 1)",
35
- "hsla(250, 50%, 90%, 1)",
36
- "hsla(250, 50%, 95%, 1)",
37
- ];
38
-
39
5
  const lightPaper = "#ffffff";
40
6
  const lightInk = "#242226";
41
7
  const darkPaper = "#161519";
42
8
  const darkInk = "#f0eff1";
43
-
44
9
  const lum = (raw: string) => relativeLuminance(normalizeToHex(raw)!);
45
10
 
46
- describe("resolveAccentTintedSurface", () => {
47
- it("takes the ramp's pale surface end in a light scheme", () => {
48
- const tint = resolveAccentTintedSurface({
49
- steps: lightAccent,
50
- paper: lightPaper,
51
- ink: lightInk,
52
- });
53
- expect(tint).toBe(lightAccent[10]);
54
- });
55
-
56
- it("takes the ramp's deep surface end in a dark scheme", () => {
57
- const tint = resolveAccentTintedSurface({ steps: darkAccent, paper: darkPaper, ink: darkInk });
58
- expect(tint).toBe(darkAccent[0]);
59
- });
60
-
61
- // The bug this rule replaced: keying the pick off a SURFACE token let a tint
62
- // sub-theme (which re-ramps surfaces) slide the chip a step per tint. Scheme
63
- // polarity comes from the paper/ink anchors, which a tint leaves alone.
64
- it("does not drift when surrounding surfaces are re-ramped by a tint", () => {
65
- const untinted = resolveAccentTintedSurface({
66
- steps: lightAccent,
67
- paper: lightPaper,
68
- ink: lightInk,
69
- });
70
- for (const tintedPaper of ["#fffce8", "#fff8f8", "#f2fbf5"]) {
71
- expect(
72
- resolveAccentTintedSurface({ steps: lightAccent, paper: tintedPaper, ink: lightInk }),
73
- ).toBe(untinted);
74
- }
75
- });
76
-
77
- it("stays a SURFACE tier — never the solid an accent CTA takes", () => {
78
- // The solid is accentBackground (= the ramp's mid, palette-shape solid).
79
- const lightSolid = lightAccent[2];
80
- const darkSolid = darkAccent[9];
81
- expect(
82
- resolveAccentTintedSurface({ steps: lightAccent, paper: lightPaper, ink: lightInk }),
83
- ).not.toBe(lightSolid);
84
- expect(
85
- resolveAccentTintedSurface({ steps: darkAccent, paper: darkPaper, ink: darkInk }),
86
- ).not.toBe(darkSolid);
87
- });
11
+ function hsl(raw: string) {
12
+ const hex = normalizeToHex(raw)!;
13
+ const [r, g, b] = [1, 3, 5].map((offset) => parseInt(hex.slice(offset, offset + 2), 16) / 255);
14
+ const max = Math.max(r, g, b);
15
+ const min = Math.min(r, g, b);
16
+ const delta = max - min;
17
+ const hue =
18
+ delta === 0
19
+ ? 0
20
+ : max === r
21
+ ? ((g - b) / delta) % 6
22
+ : max === g
23
+ ? (b - r) / delta + 2
24
+ : (r - g) / delta + 4;
25
+ return { hue: (hue * 60 + 360) % 360, lightness: (max + min) * 50 };
26
+ }
27
+
28
+ const v5Schemes = [
29
+ {
30
+ scheme: "light",
31
+ paper: "hsla(0, 0%, 100%, 1)",
32
+ solid: "hsla(0, 0%, 10%, 1)",
33
+ ink: "hsla(0, 0%, 2%, 1)",
34
+ },
35
+ {
36
+ scheme: "dark",
37
+ paper: "hsla(0, 0%, 4%, 1)",
38
+ solid: "hsla(0, 0%, 45%, 1)",
39
+ ink: "hsla(0, 0%, 100%, 1)",
40
+ },
41
+ ];
88
42
 
89
- it("keeps a luminance-picked label readable on the tint in both schemes", () => {
90
- const cases = [
91
- { steps: lightAccent, paper: lightPaper, ink: lightInk },
92
- { steps: darkAccent, paper: darkPaper, ink: darkInk },
93
- ];
94
- for (const c of cases) {
95
- const tint = resolveAccentTintedSurface(c)!;
96
- const best = Math.max(
97
- contrastRatio(lum(tint), lum(c.paper)),
98
- contrastRatio(lum(tint), lum(c.ink)),
99
- );
100
- expect(best).toBeGreaterThanOrEqual(4.5);
101
- }
43
+ describe("resolveAccentTintedSurface", () => {
44
+ it.each(v5Schemes)("keeps the $scheme v5 wash near the paper", ({ scheme, solid, paper }) => {
45
+ const wash = resolveAccentTintedSurface({ solid, paper })!;
46
+ expect(wash).toMatch(/^#[0-9a-f]{6}$/);
47
+ const contrast = contrastRatio(lum(wash), lum(paper));
48
+ expect(contrast).toBeGreaterThanOrEqual(1.18);
49
+ expect(contrast).toBeLessThanOrEqual(1.3);
50
+ if (scheme === "light") expect(hsl(wash).lightness).toBeGreaterThan(85);
51
+ else expect(lum(wash)).toBeGreaterThan(lum(paper));
102
52
  });
103
53
 
104
- it("returns undefined when the accent ramp or an anchor is missing", () => {
105
- expect(
106
- resolveAccentTintedSurface({ steps: [], paper: lightPaper, ink: lightInk }),
107
- ).toBeUndefined();
108
- expect(
109
- resolveAccentTintedSurface({ steps: lightAccent, paper: undefined, ink: lightInk }),
110
- ).toBeUndefined();
111
- expect(
112
- resolveAccentTintedSurface({
113
- steps: Array.from({ length: 12 }, () => "var(--nope)"),
114
- paper: lightPaper,
115
- ink: lightInk,
116
- }),
117
- ).toBeUndefined();
54
+ it("retains the hue of a chromatic solid in a pale wash", () => {
55
+ const solid = "#634fc4";
56
+ const wash = resolveAccentTintedSurface({ solid, paper: "#ffffff" })!;
57
+ const hueDistance = Math.abs(hsl(wash).hue - hsl(solid).hue);
58
+ expect(Math.min(hueDistance, 360 - hueDistance)).toBeLessThan(10);
59
+ expect(hsl(wash).lightness).toBeGreaterThan(85);
118
60
  });
119
- });
120
61
 
121
- describe("resolveAaSolidFill", () => {
122
- it("keeps the preferred solid when it already clears the text floor", () => {
123
- const solids = lightAccent.slice(0, 10).map((value, i) => ({
124
- token: `$accent${i + 1}`,
125
- value,
126
- }));
127
- // Stock light accentBackground is the mid solid (step 3, 6.09:1 on paper).
62
+ it.each(v5Schemes)("keeps an anchor readable on the $scheme v5 wash", ({ solid, paper, ink }) => {
63
+ const wash = resolveAccentTintedSurface({ solid, paper })!;
128
64
  expect(
129
- resolveAaSolidFill({
130
- preferred: { token: "$accentBackground", value: lightAccent[2] },
131
- solids,
132
- paper: lightPaper,
133
- ink: lightInk,
134
- }),
135
- ).toBe("$accentBackground");
65
+ Math.max(contrastRatio(lum(wash), lum(paper)), contrastRatio(lum(wash), lum(ink))),
66
+ ).toBeGreaterThanOrEqual(4.5);
136
67
  });
137
68
 
138
- it("walks off the dark mid-solid that tops out at 4.02:1", () => {
139
- const solids = darkAccent.slice(0, 10).map((value, i) => ({
140
- token: `$accent${i + 1}`,
141
- value,
142
- }));
143
- const token = resolveAaSolidFill({
144
- preferred: { token: "$accentBackground", value: darkAccent[9] },
145
- solids,
146
- paper: darkPaper,
147
- ink: darkInk,
148
- });
149
- expect(token).not.toBe("$accentBackground");
150
- const picked = solids.find((s) => s.token === token);
151
- expect(picked?.value).toBeTruthy();
152
- const fillLum = lum(picked!.value!);
153
- const best = Math.max(
154
- contrastRatio(fillLum, lum(darkPaper)),
155
- contrastRatio(fillLum, lum(darkInk)),
156
- );
157
- expect(best).toBeGreaterThanOrEqual(4.5);
158
- // Nearest passing solid to the 60% mid is accent9 (57%, 4.60:1).
159
- expect(token).toBe("$accent9");
69
+ it("returns the solid when no mix reaches the wash contrast", () => {
70
+ expect(resolveAccentTintedSurface({ solid: "#fafafa", paper: "#ffffff" })).toBe("#fafafa");
160
71
  });
161
72
 
162
- it("returns the preferred token when anchors cannot be parsed", () => {
163
- expect(
164
- resolveAaSolidFill({
165
- preferred: { token: "$accentBackground", value: darkAccent[9] },
166
- solids: [],
167
- paper: undefined,
168
- ink: darkInk,
169
- }),
170
- ).toBe("$accentBackground");
73
+ it("returns undefined for missing or unparseable inputs", () => {
74
+ for (const invalid of [undefined, "var(--nope)"]) {
75
+ expect(resolveAccentTintedSurface({ solid: invalid, paper: "#ffffff" })).toBeUndefined();
76
+ expect(resolveAccentTintedSurface({ solid: "#634fc4", paper: invalid })).toBeUndefined();
77
+ }
171
78
  });
172
79
  });
173
80
 
@@ -186,7 +93,7 @@ describe("resolveLegibleInkOn (MPO-448)", () => {
186
93
  it("hands a muted title on the dark accent tint to the readable anchor (4.05:1)", () => {
187
94
  expect(
188
95
  resolveLegibleInkOn({
189
- fill: darkAccent[0],
96
+ fill: "hsla(250, 50%, 35%, 1)",
190
97
  preferred: "#a09ba6",
191
98
  paper: darkPaper,
192
99
  ink: darkInk,
@@ -10,27 +10,9 @@ import {
10
10
  } from "./colorRules";
11
11
 
12
12
  /**
13
- * Luminance-computed readable text/glyph colors (Axiom 15 OPTICS + Axiom 12
14
- * LEGIBLE FLOOR).
15
- *
16
- * Two catalog-wide contrast traps this module closes:
17
- *
18
- * 1. Text sitting ON an accent/solid FILL (`useReadableTextOn`). Components
19
- * that paint a selected/active row, chip or avatar with `$accentBackground`
20
- * (or any solid step) and then let the label inherit the scheme's `$color`
21
- * render ink-on-accent (~2.6:1) or, worse, near-white-on-bright-tint
22
- * (~1.2:1). The readable foreground is whichever scheme anchor
23
- * (paper `$color1` / ink `$color12`) carries more contrast on the ACTUAL
24
- * fill — never assumed.
25
- *
26
- * 2. Accent-hued text on the PAGE (`useAccentOnSurface`). The readable accent
27
- * is the most chromatic step of the theme's own accent ramp that still
28
- * clears AA (4.5:1) on both page surfaces — a mid accent in light, a pale
29
- * accent in dark.
30
- *
31
- * Both read live theme values so they stay correct across schemes, tints and
32
- * custom accent palettes; they mirror the runtime pattern already used by the
33
- * Switch thumb, Toast and Calendar chip.
13
+ * Live-theme contrast helpers for text on solids and accent ink on surfaces.
14
+ * The theme build holds the accent solid to the text floor. Accent-intent
15
+ * surfaces use a wash; selected rows and segments use the neutral tier.
34
16
  */
35
17
 
36
18
  function resolveTokenValue(
@@ -43,10 +25,10 @@ function resolveTokenValue(
43
25
  }
44
26
 
45
27
  /**
46
- * Readable text/glyph color to sit ON `fill` (a token like "$accentBackground"
47
- * / "$color8", or an already-resolved color value — e.g. the step returned by
48
- * `useAccentTintedSurface`): the scheme's paper (`$color1`) or ink (`$color12`)
49
- * anchor, whichever contrasts more on the resolved fill, returned as a concrete
28
+ * Readable text/glyph color to sit ON `fill` (a token like "$color8" or a
29
+ * resolved value, such as the wash from `useAccentTintedSurface`): the
30
+ * scheme's paper (`$color1`) or ink (`$color12`) anchor, whichever contrasts
31
+ * more on the resolved fill, returned as a concrete
50
32
  * value so no nested sub-theme can flip it. When neither anchor clears AA on
51
33
  * the fill, the pole that does (`holdInkFloor`). Returns `undefined` when any
52
34
  * color fails to resolve/parse — callers keep their own default in that case.
@@ -153,168 +135,46 @@ export function useAccentOnSurface(): string {
153
135
  }, [c1, c2, ink, steps.join("|")]);
154
136
  }
155
137
 
156
- /**
157
- * Accent SURFACE tier of the accent ramp, mirrored by scheme — the fill for a
158
- * selected/current chip that must carry the accent language at SURFACE
159
- * strength rather than as a solid (LC-05: selection is accent; the ViewSwitcher
160
- * rider pins segmented-control strength at "tinted", not the solid an accent
161
- * CTA takes).
162
- *
163
- * The step is a declared ramp POSITION, the same way the neutral ramp declares
164
- * its tiers (surface `$color2`, UI background `$color3`-`$color5`, text
165
- * `$color11`-`$color12`): the accent ramp's surface end is its palest step in a
166
- * light scheme and its deepest in a dark one, so the chip reads as a tinted
167
- * surface on either page. Scheme polarity comes from the paper/ink ANCHORS,
168
- * never from a surface token — a tint re-ramps surfaces, and keying off one
169
- * made this pick drift a step per tint (measured: the chip slid from the pale
170
- * accent to the palest accent under a yellow tint).
171
- *
172
- * Returns `undefined` when the accent ramp is absent or unparseable (tint
173
- * sub-themes without an accent ramp) — callers keep their neutral default
174
- * rather than render an unthemed chip.
175
- */
176
- // Ramp positions (1-based) of the accent surface tier per scheme.
177
- const ACCENT_SURFACE_STEP_LIGHT = 11;
178
- const ACCENT_SURFACE_STEP_DARK = 1;
138
+ const ACCENT_WASH_RATIO = 1.2;
179
139
 
180
140
  export interface ResolveAccentTintedSurfaceInput {
181
- /** Accent ramp steps 1..12, as raw color values; holes allowed. */
182
- steps: (string | undefined)[];
183
- /** Scheme paper anchor (`$color1`). */
141
+ solid?: string;
184
142
  paper?: string;
185
- /** Scheme ink anchor (`$color12`). */
186
- ink?: string;
187
- }
188
-
189
- /** Pure resolver behind {@link useAccentTintedSurface} — see its docs. */
190
- export function resolveAccentTintedSurface({
191
- steps,
192
- paper,
193
- ink,
194
- }: ResolveAccentTintedSurfaceInput): string | undefined {
195
- const paperHex = paper ? normalizeToHex(paper) : null;
196
- const inkHex = ink ? normalizeToHex(ink) : null;
197
- if (!paperHex || !inkHex) return undefined;
198
- const isLight = relativeLuminance(paperHex) > relativeLuminance(inkHex);
199
- const step = steps[(isLight ? ACCENT_SURFACE_STEP_LIGHT : ACCENT_SURFACE_STEP_DARK) - 1];
200
- if (!step || !normalizeToHex(step)) return undefined;
201
- return step;
202
- }
203
-
204
- export function useAccentTintedSurface(): string | undefined {
205
- const theme = useTamaguiTheme() as unknown as Record<string, { val?: unknown } | undefined>;
206
- const paper = resolveTokenValue(theme, "color1");
207
- const ink = resolveTokenValue(theme, "color12");
208
- const steps: (string | undefined)[] = [];
209
- for (let i = 1; i <= 12; i++) steps.push(resolveTokenValue(theme, `accent${i}`));
210
- // eslint-disable-next-line react-hooks/exhaustive-deps
211
- return useMemo(
212
- () => resolveAccentTintedSurface({ steps, paper, ink }),
213
- [paper, ink, steps.join("|")],
214
- );
215
143
  }
216
144
 
217
145
  /**
218
- * A named solid in the accent ramp. `token` is what a caller paints
219
- * (`$accentBackground`, `$accent9`); `value` is the resolved color.
146
+ * Accent wash for an accent-intent surface, mixed from the accent solid and
147
+ * paper. Selection uses the neutral tier in `componentColors.selection` or
148
+ * `formSelectionColors`, never this wash.
220
149
  */
221
- export interface AaSolidCandidate {
222
- token: string;
223
- value: string | undefined;
224
- }
225
-
226
- export interface ResolveAaSolidFillInput {
227
- /** The LC-05 selected solid, usually `$accentBackground`. */
228
- preferred: AaSolidCandidate;
229
- /**
230
- * Accent solids (steps 1–10). Pale anchors 11–12 are tinted surfaces, not
231
- * selection solids — they would pass AA as chips and fail LC-05 as marks.
232
- */
233
- solids: AaSolidCandidate[];
234
- paper?: string;
235
- ink?: string;
236
- /** Defaults to {@link aaTextContrastRatio} (4.5). */
237
- floor?: number;
238
- }
239
-
240
- function bestAnchorContrast(fill: string, paperLum: number, inkLum: number): number {
241
- const hex = normalizeToHex(fill);
242
- if (!hex) return 0;
243
- const fillLum = relativeLuminance(hex);
244
- return Math.max(contrastRatio(fillLum, paperLum), contrastRatio(fillLum, inkLum));
245
- }
246
-
247
- /**
248
- * Pick an accent solid that can carry AA text (paper or ink ≥ 4.5).
249
- *
250
- * A mid-chroma solid can clear the graphic 3:1 tier and still miss the text
251
- * floor with both scheme anchors (the stock dark `#7766cc` measured 4.02:1
252
- * before the build held `$accentBackground` to it, MPO-448). Walking to the
253
- * nearest passing solid keeps the mark on the accent channel (LC-05) instead
254
- * of dropping to ink or a tinted surface.
255
- *
256
- * Returns the preferred token when it already clears the floor, otherwise
257
- * the solid token whose resolved luminance is closest to the preferred fill.
258
- */
259
- export function resolveAaSolidFill({
260
- preferred,
261
- solids,
150
+ export function resolveAccentTintedSurface({
151
+ solid,
262
152
  paper,
263
- ink,
264
- floor = aaTextContrastRatio,
265
- }: ResolveAaSolidFillInput): string {
153
+ }: ResolveAccentTintedSurfaceInput): string | undefined {
154
+ const solidHex = solid ? normalizeToHex(solid) : null;
266
155
  const paperHex = paper ? normalizeToHex(paper) : null;
267
- const inkHex = ink ? normalizeToHex(ink) : null;
268
- if (!paperHex || !inkHex) return preferred.token;
156
+ if (!solidHex || !paperHex) return undefined;
269
157
  const paperLum = relativeLuminance(paperHex);
270
- const inkLum = relativeLuminance(inkHex);
271
-
272
- if (preferred.value && bestAnchorContrast(preferred.value, paperLum, inkLum) >= floor) {
273
- return preferred.token;
158
+ const solidRgb = [1, 3, 5].map((offset) => parseInt(solidHex.slice(offset, offset + 2), 16));
159
+ const paperRgb = [1, 3, 5].map((offset) => parseInt(paperHex.slice(offset, offset + 2), 16));
160
+ for (let step = 1; step <= 50; step++) {
161
+ const share = step * 0.02;
162
+ const mix = `#${paperRgb
163
+ .map((channel, index) =>
164
+ Math.round(channel + (solidRgb[index] - channel) * share)
165
+ .toString(16)
166
+ .padStart(2, "0"),
167
+ )
168
+ .join("")}`;
169
+ if (contrastRatio(relativeLuminance(mix), paperLum) >= ACCENT_WASH_RATIO) return mix;
274
170
  }
275
-
276
- const preferredHex = preferred.value ? normalizeToHex(preferred.value) : null;
277
- const preferredLum = preferredHex ? relativeLuminance(preferredHex) : undefined;
278
- let bestToken: string | undefined;
279
- let bestDist = Number.POSITIVE_INFINITY;
280
- for (const { token, value } of solids) {
281
- if (!value || bestAnchorContrast(value, paperLum, inkLum) < floor) continue;
282
- const hex = normalizeToHex(value);
283
- if (!hex) continue;
284
- const dist = preferredLum === undefined ? 0 : Math.abs(relativeLuminance(hex) - preferredLum);
285
- if (dist < bestDist) {
286
- bestDist = dist;
287
- bestToken = token;
288
- }
289
- }
290
- return bestToken ?? preferred.token;
171
+ return solidHex;
291
172
  }
292
173
 
293
- /**
294
- * Token to paint as a selected/current solid so on-fill text clears AA.
295
- * See {@link resolveAaSolidFill}.
296
- */
297
- export function useAaSolidFill(preferredToken: string): string {
174
+ /** Accent-intent wash; selection belongs to the shared neutral tier. */
175
+ export function useAccentTintedSurface(): string | undefined {
298
176
  const theme = useTamaguiTheme() as unknown as Record<string, { val?: unknown } | undefined>;
177
+ const solid = resolveTokenValue(theme, "accentBackground");
299
178
  const paper = resolveTokenValue(theme, "color1");
300
- const ink = resolveTokenValue(theme, "color12");
301
- const preferredValue = preferredToken.startsWith("$")
302
- ? resolveTokenValue(theme, preferredToken)
303
- : preferredToken;
304
- const solids: AaSolidCandidate[] = [];
305
- for (let i = 1; i <= 10; i++) {
306
- solids.push({ token: `$accent${i}`, value: resolveTokenValue(theme, `accent${i}`) });
307
- }
308
- const solidsKey = solids.map((s) => s.value ?? "").join("|");
309
- // eslint-disable-next-line react-hooks/exhaustive-deps
310
- return useMemo(
311
- () =>
312
- resolveAaSolidFill({
313
- preferred: { token: preferredToken, value: preferredValue },
314
- solids,
315
- paper,
316
- ink,
317
- }),
318
- [preferredToken, preferredValue, paper, ink, solidsKey],
319
- );
179
+ return useMemo(() => resolveAccentTintedSurface({ solid, paper }), [solid, paper]);
320
180
  }