@multiplatform.one/theme 8.3.0 → 8.4.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 (248) hide show
  1. package/dist/cjs/audit/constraintAudit.native.js.map +1 -1
  2. package/dist/cjs/audit/designTokens.native.js.map +1 -1
  3. package/dist/cjs/audit/diffReport.native.js.map +1 -1
  4. package/dist/cjs/audit/index.native.js.map +1 -1
  5. package/dist/cjs/audit/themeMatrix.native.js.map +1 -1
  6. package/dist/cjs/dates.native.js.map +1 -1
  7. package/dist/cjs/devWarn.native.js.map +1 -1
  8. package/dist/cjs/expo.native.js.map +1 -1
  9. package/dist/cjs/figma/figmaTokens.native.js.map +1 -1
  10. package/dist/cjs/font.native.js.map +1 -1
  11. package/dist/cjs/fonts/createFontLoader.native.js.map +1 -1
  12. package/dist/cjs/fonts/index.native.js.map +1 -1
  13. package/dist/cjs/fonts/interFaces.native.js.map +1 -1
  14. package/dist/cjs/fonts/types.native.js.map +1 -1
  15. package/dist/cjs/index.gnome.cjs +4 -1
  16. package/dist/cjs/index.gnome.native.js +3 -0
  17. package/dist/cjs/index.gnome.native.js.map +1 -1
  18. package/dist/cjs/index.native.js +1 -1
  19. package/dist/cjs/index.native.js.map +1 -1
  20. package/dist/cjs/keyboardFocusRing.native.js.map +1 -1
  21. package/dist/cjs/menuRow.cjs +1 -1
  22. package/dist/cjs/menuRow.native.js +1 -1
  23. package/dist/cjs/menuRow.native.js.map +1 -1
  24. package/dist/cjs/nativeTwins.native.js.map +1 -1
  25. package/dist/cjs/numbers.native.js.map +1 -1
  26. package/dist/cjs/theme/Heading.native.js.map +1 -1
  27. package/dist/cjs/theme/IconSlot.native.js.map +1 -1
  28. package/dist/cjs/theme/Intent.native.js.map +1 -1
  29. package/dist/cjs/theme/Preset.native.js.map +1 -1
  30. package/dist/cjs/theme/PresetContext.native.js.map +1 -1
  31. package/dist/cjs/theme/Surface.native.js.map +1 -1
  32. package/dist/cjs/theme/Text.native.js.map +1 -1
  33. package/dist/cjs/theme/Tint.native.js.map +1 -1
  34. package/dist/cjs/theme/accentInk.cjs +6 -2
  35. package/dist/cjs/theme/accentInk.native.js +6 -2
  36. package/dist/cjs/theme/accentInk.native.js.map +1 -1
  37. package/dist/cjs/theme/activateThemeConfig.native.js.map +1 -1
  38. package/dist/cjs/theme/animations/css.native.js.map +1 -1
  39. package/dist/cjs/theme/animations/index.native.js.map +1 -1
  40. package/dist/cjs/theme/animations/reactNative.native.js.map +1 -1
  41. package/dist/cjs/theme/chartPalette.cjs +3 -3
  42. package/dist/cjs/theme/chartPalette.native.js +5 -4
  43. package/dist/cjs/theme/chartPalette.native.js.map +1 -1
  44. package/dist/cjs/theme/colorRules.cjs +5 -0
  45. package/dist/cjs/theme/colorRules.native.js +6 -0
  46. package/dist/cjs/theme/colorRules.native.js.map +1 -1
  47. package/dist/cjs/theme/controlTiers.cjs +9 -5
  48. package/dist/cjs/theme/controlTiers.native.js +10 -5
  49. package/dist/cjs/theme/controlTiers.native.js.map +1 -1
  50. package/dist/cjs/theme/cookies.native.js.map +1 -1
  51. package/dist/cjs/theme/createDefaultThemeConfig.native.js.map +1 -1
  52. package/dist/cjs/theme/createThemes.cjs +212 -8
  53. package/dist/cjs/theme/createThemes.native.js +397 -12
  54. package/dist/cjs/theme/createThemes.native.js.map +1 -1
  55. package/dist/cjs/theme/dedupeInherited.native.js.map +1 -1
  56. package/dist/cjs/theme/defaults/accent.native.js.map +1 -1
  57. package/dist/cjs/theme/defaults/base.native.js.map +1 -1
  58. package/dist/cjs/theme/defaults/builderOptions.cjs +9 -4
  59. package/dist/cjs/theme/defaults/builderOptions.native.js +9 -4
  60. package/dist/cjs/theme/defaults/builderOptions.native.js.map +1 -1
  61. package/dist/cjs/theme/defaults/categoryFonts.native.js.map +1 -1
  62. package/dist/cjs/theme/defaults/fonts.native.js.map +1 -1
  63. package/dist/cjs/theme/defaults/house.native.js.map +1 -1
  64. package/dist/cjs/theme/defaults/index.native.js.map +1 -1
  65. package/dist/cjs/theme/devtools/ColorLineVisualizer.native.js.map +1 -1
  66. package/dist/cjs/theme/devtools/ThemeDevtoolsPanel.native.js.map +1 -1
  67. package/dist/cjs/theme/devtools/animationOptions.native.js.map +1 -1
  68. package/dist/cjs/theme/devtools/buildSnapshot.native.js.map +1 -1
  69. package/dist/cjs/theme/devtools/index.native.js.map +1 -1
  70. package/dist/cjs/theme/devtools/knobIcons.native.js.map +1 -1
  71. package/dist/cjs/theme/devtools/vizConstants.native.js.map +1 -1
  72. package/dist/cjs/theme/dynamicType.native.js.map +1 -1
  73. package/dist/cjs/theme/fieldFocus.native.js.map +1 -1
  74. package/dist/cjs/theme/fillContrast.cjs +4 -1
  75. package/dist/cjs/theme/fillContrast.native.js +4 -1
  76. package/dist/cjs/theme/fillContrast.native.js.map +1 -1
  77. package/dist/cjs/theme/focusState.native.js.map +1 -1
  78. package/dist/cjs/theme/fontCategoryMetrics.native.js.map +1 -1
  79. package/dist/cjs/theme/fontCategoryStacks.native.js.map +1 -1
  80. package/dist/cjs/theme/glyphPaint.native.js.map +1 -1
  81. package/dist/cjs/theme/glyphTint.native.js.map +1 -1
  82. package/dist/cjs/theme/groupGeometry.native.js.map +1 -1
  83. package/dist/cjs/theme/index.cjs +1 -0
  84. package/dist/cjs/theme/index.native.js +1 -0
  85. package/dist/cjs/theme/index.native.js.map +1 -1
  86. package/dist/cjs/theme/intents.native.js.map +1 -1
  87. package/dist/cjs/theme/knobs.native.js.map +1 -1
  88. package/dist/cjs/theme/layoutTokens.native.js.map +1 -1
  89. package/dist/cjs/theme/listRows.native.js.map +1 -1
  90. package/dist/cjs/theme/mergedTokens.native.js.map +1 -1
  91. package/dist/cjs/theme/motion.native.js.map +1 -1
  92. package/dist/cjs/theme/motionStops.native.js.map +1 -1
  93. package/dist/cjs/theme/motionTiming.native.js.map +1 -1
  94. package/dist/cjs/theme/nativeMonoFamily.native.js.map +1 -1
  95. package/dist/cjs/theme/nativePresetSwitch.native.js.map +1 -1
  96. package/dist/cjs/theme/nativeSchemePreference.native.js.map +1 -1
  97. package/dist/cjs/theme/nativeTheme.native.js.map +1 -1
  98. package/dist/cjs/theme/nativeTransitionProps.native.js.map +1 -1
  99. package/dist/cjs/theme/overlaySurfaces.native.js.map +1 -1
  100. package/dist/cjs/theme/pageGutter.native.js.map +1 -1
  101. package/dist/cjs/theme/preset.types.native.js.map +1 -1
  102. package/dist/cjs/theme/presetPreview.native.js.map +1 -1
  103. package/dist/cjs/theme/presetStore.native.js.map +1 -1
  104. package/dist/cjs/theme/presetUserSync.native.js.map +1 -1
  105. package/dist/cjs/theme/presets.native.js.map +1 -1
  106. package/dist/cjs/theme/radiusClass.native.js.map +1 -1
  107. package/dist/cjs/theme/radiusClassTable.generated.native.js.map +1 -1
  108. package/dist/cjs/theme/ramps.native.js.map +1 -1
  109. package/dist/cjs/theme/readableColor.cjs +20 -9
  110. package/dist/cjs/theme/readableColor.native.js +20 -9
  111. package/dist/cjs/theme/readableColor.native.js.map +1 -1
  112. package/dist/cjs/theme/recipeInputs.native.js.map +1 -1
  113. package/dist/cjs/theme/recipes.native.js.map +1 -1
  114. package/dist/cjs/theme/resolveKnobs.cjs +4 -1
  115. package/dist/cjs/theme/resolveKnobs.native.js +4 -1
  116. package/dist/cjs/theme/resolveKnobs.native.js.map +1 -1
  117. package/dist/cjs/theme/scale/activeScale.native.js.map +1 -1
  118. package/dist/cjs/theme/scale/deriveRecipeInputs.native.js.map +1 -1
  119. package/dist/cjs/theme/scale/generateScale.native.js.map +1 -1
  120. package/dist/cjs/theme/scale/index.native.js.map +1 -1
  121. package/dist/cjs/theme/scale/scaleAttribute.native.js.map +1 -1
  122. package/dist/cjs/theme/scale/scaleCss.native.js.map +1 -1
  123. package/dist/cjs/theme/scale/scaleFonts.native.js.map +1 -1
  124. package/dist/cjs/theme/scale/scaleInputs.native.js.map +1 -1
  125. package/dist/cjs/theme/scale/themeScales.native.js.map +1 -1
  126. package/dist/cjs/theme/scale/useActiveScale.native.js.map +1 -1
  127. package/dist/cjs/theme/schemeStore.native.js.map +1 -1
  128. package/dist/cjs/theme/shared.native.js.map +1 -1
  129. package/dist/cjs/theme/sizeRecipes.native.js.map +1 -1
  130. package/dist/cjs/theme/subsetThemes.native.js.map +1 -1
  131. package/dist/cjs/theme/themeValue.native.js.map +1 -1
  132. package/dist/cjs/theme/tileSizes.native.js.map +1 -1
  133. package/dist/cjs/theme/tintHues.native.js.map +1 -1
  134. package/dist/cjs/theme/tokenNumber.cjs +34 -0
  135. package/dist/cjs/theme/tokenNumber.native.js +39 -0
  136. package/dist/cjs/theme/tokenNumber.native.js.map +1 -0
  137. package/dist/cjs/theme/touchManipulation.native.js.map +1 -1
  138. package/dist/cjs/theme/useResolvedKnobs.native.js.map +1 -1
  139. package/dist/cjs/theme/useTouchSurface.native.js.map +1 -1
  140. package/dist/cjs/theme/v3TokenRoles.native.js.map +1 -1
  141. package/dist/esm/index.gnome.mjs +3 -2
  142. package/dist/esm/index.gnome.mjs.map +1 -1
  143. package/dist/esm/index.gnome.native.js +3 -2
  144. package/dist/esm/index.gnome.native.js.map +1 -1
  145. package/dist/esm/menuRow.mjs +1 -1
  146. package/dist/esm/menuRow.native.js +1 -1
  147. package/dist/esm/theme/accentInk.mjs +6 -3
  148. package/dist/esm/theme/accentInk.mjs.map +1 -1
  149. package/dist/esm/theme/accentInk.native.js +6 -3
  150. package/dist/esm/theme/accentInk.native.js.map +1 -1
  151. package/dist/esm/theme/chartPalette.mjs +3 -3
  152. package/dist/esm/theme/chartPalette.mjs.map +1 -1
  153. package/dist/esm/theme/chartPalette.native.js +5 -4
  154. package/dist/esm/theme/chartPalette.native.js.map +1 -1
  155. package/dist/esm/theme/colorRules.mjs +5 -1
  156. package/dist/esm/theme/colorRules.mjs.map +1 -1
  157. package/dist/esm/theme/colorRules.native.js +6 -1
  158. package/dist/esm/theme/colorRules.native.js.map +1 -1
  159. package/dist/esm/theme/controlTiers.mjs +9 -6
  160. package/dist/esm/theme/controlTiers.mjs.map +1 -1
  161. package/dist/esm/theme/controlTiers.native.js +10 -6
  162. package/dist/esm/theme/controlTiers.native.js.map +1 -1
  163. package/dist/esm/theme/createThemes.mjs +215 -12
  164. package/dist/esm/theme/createThemes.mjs.map +1 -1
  165. package/dist/esm/theme/createThemes.native.js +400 -16
  166. package/dist/esm/theme/createThemes.native.js.map +1 -1
  167. package/dist/esm/theme/defaults/builderOptions.mjs +10 -6
  168. package/dist/esm/theme/defaults/builderOptions.mjs.map +1 -1
  169. package/dist/esm/theme/defaults/builderOptions.native.js +10 -6
  170. package/dist/esm/theme/defaults/builderOptions.native.js.map +1 -1
  171. package/dist/esm/theme/fillContrast.mjs +5 -2
  172. package/dist/esm/theme/fillContrast.mjs.map +1 -1
  173. package/dist/esm/theme/fillContrast.native.js +5 -2
  174. package/dist/esm/theme/fillContrast.native.js.map +1 -1
  175. package/dist/esm/theme/index.mjs +1 -0
  176. package/dist/esm/theme/index.mjs.map +1 -1
  177. package/dist/esm/theme/index.native.js +1 -0
  178. package/dist/esm/theme/index.native.js.map +1 -1
  179. package/dist/esm/theme/nativeTheme.mjs.map +1 -1
  180. package/dist/esm/theme/nativeTheme.native.js.map +1 -1
  181. package/dist/esm/theme/readableColor.mjs +20 -9
  182. package/dist/esm/theme/readableColor.mjs.map +1 -1
  183. package/dist/esm/theme/readableColor.native.js +20 -9
  184. package/dist/esm/theme/readableColor.native.js.map +1 -1
  185. package/dist/esm/theme/resolveKnobs.mjs +4 -1
  186. package/dist/esm/theme/resolveKnobs.mjs.map +1 -1
  187. package/dist/esm/theme/resolveKnobs.native.js +4 -1
  188. package/dist/esm/theme/resolveKnobs.native.js.map +1 -1
  189. package/dist/esm/theme/tokenNumber.mjs +9 -0
  190. package/dist/esm/theme/tokenNumber.mjs.map +1 -0
  191. package/dist/esm/theme/tokenNumber.native.js +11 -0
  192. package/dist/esm/theme/tokenNumber.native.js.map +1 -0
  193. package/package.json +5 -5
  194. package/src/figma/figmaTokens.spec.ts +2 -1
  195. package/src/index.gnome.ts +2 -0
  196. package/src/menuRow.palette.spec.ts +20 -7
  197. package/src/menuRow.spec.ts +2 -2
  198. package/src/menuRow.ts +3 -3
  199. package/src/theme/accentInk.ts +8 -2
  200. package/src/theme/accentOnSurface.spec.tsx +6 -1
  201. package/src/theme/chartPalette.spec.ts +2 -2
  202. package/src/theme/chartPalette.ts +9 -9
  203. package/src/theme/colorRules.ts +13 -0
  204. package/src/theme/controlEdges.spec.ts +114 -0
  205. package/src/theme/controlTiers.ts +18 -9
  206. package/src/theme/createDefaultThemeConfig.spec.ts +135 -12
  207. package/src/theme/createThemes.ts +363 -24
  208. package/src/theme/defaults/builderOptions.ts +25 -8
  209. package/src/theme/defaults/secondaryInk.spec.ts +10 -5
  210. package/src/theme/fillContrast.spec.ts +8 -2
  211. package/src/theme/fillContrast.ts +10 -2
  212. package/src/theme/houseGround.spec.ts +1 -1
  213. package/src/theme/index.ts +1 -0
  214. package/src/theme/nativeTheme.tsx +1 -1
  215. package/src/theme/readableColor.ts +23 -11
  216. package/src/theme/recipes.ts +1 -0
  217. package/src/theme/resolveKnobs.spec.ts +4 -4
  218. package/src/theme/resolveKnobs.ts +6 -4
  219. package/src/theme/selectionFill.spec.ts +58 -0
  220. package/src/theme/switchThumb.spec.ts +13 -1
  221. package/src/theme/themeKeyRegistry.spec.ts +26 -0
  222. package/src/theme/tokenNumber.spec.tsx +47 -0
  223. package/src/theme/tokenNumber.ts +18 -0
  224. package/types/index.gnome.d.ts +2 -1
  225. package/types/index.gnome.d.ts.map +1 -1
  226. package/types/menuRow.d.ts +5 -5
  227. package/types/theme/accentInk.d.ts +1 -0
  228. package/types/theme/accentInk.d.ts.map +1 -1
  229. package/types/theme/chartPalette.d.ts +2 -2
  230. package/types/theme/colorRules.d.ts +9 -0
  231. package/types/theme/colorRules.d.ts.map +1 -1
  232. package/types/theme/controlTiers.d.ts +11 -5
  233. package/types/theme/controlTiers.d.ts.map +1 -1
  234. package/types/theme/createThemes.d.ts +21 -12
  235. package/types/theme/createThemes.d.ts.map +1 -1
  236. package/types/theme/defaults/builderOptions.d.ts +9 -1
  237. package/types/theme/defaults/builderOptions.d.ts.map +1 -1
  238. package/types/theme/fillContrast.d.ts.map +1 -1
  239. package/types/theme/index.d.ts +1 -0
  240. package/types/theme/index.d.ts.map +1 -1
  241. package/types/theme/nativeTheme.d.ts +1 -1
  242. package/types/theme/nativeTheme.d.ts.map +1 -1
  243. package/types/theme/readableColor.d.ts.map +1 -1
  244. package/types/theme/recipes.d.ts +3 -0
  245. package/types/theme/recipes.d.ts.map +1 -1
  246. package/types/theme/resolveKnobs.d.ts.map +1 -1
  247. package/types/theme/tokenNumber.d.ts +10 -0
  248. package/types/theme/tokenNumber.d.ts.map +1 -0
@@ -0,0 +1,114 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { contrastRatio, minContrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
3
+ import { createThemesBuilder } from "./createThemes";
4
+ import { resolveThemeThroughParents } from "./dedupeInherited";
5
+ import { defaultAccentTheme } from "./defaults/accent";
6
+ import { defaultBaseTheme } from "./defaults/base";
7
+ import { defaultBuilderOptions } from "./defaults/builderOptions";
8
+
9
+ const themes = createThemesBuilder(
10
+ defaultBaseTheme,
11
+ defaultAccentTheme,
12
+ defaultBuilderOptions,
13
+ ).themes() as Record<string, Record<string, string>>;
14
+
15
+ const intents = new Set(["error", "warning", "success"]);
16
+
17
+ function ratio(a: string | undefined, b: string | undefined) {
18
+ const aHex = normalizeToHex(a ?? "");
19
+ const bHex = normalizeToHex(b ?? "");
20
+ if (!aHex || !bHex) throw new Error(`Expected opaque colours: ${a}, ${b}`);
21
+ return contrastRatio(relativeLuminance(aHex), relativeLuminance(bHex));
22
+ }
23
+
24
+ const resolve = (name: string) =>
25
+ resolveThemeThroughParents(themes, name) as unknown as Record<string, string>;
26
+
27
+ const scopes = Object.keys(themes).filter(
28
+ (name) => /^(light|dark)(_[a-z]+)?$/.test(name) && !/_(accent|active)$/.test(name),
29
+ );
30
+ const hueScopes = scopes.filter((name) => !intents.has(name.split("_")[1] ?? ""));
31
+ const intentScopes = scopes.filter((name) => intents.has(name.split("_")[1] ?? ""));
32
+
33
+ describe("LC-299 CONTROL-EDGE: control edges clear WCAG 1.4.11's 3:1", () => {
34
+ it("covers both schemes and every preset hue", () => {
35
+ for (const hue of ["blue", "purple", "green", "orange", "pink"]) {
36
+ expect(hueScopes).toContain(`light_${hue}`);
37
+ expect(hueScopes).toContain(`dark_${hue}`);
38
+ }
39
+ });
40
+
41
+ for (const scope of hueScopes) {
42
+ it(`${scope}: field edges hold 3:1 on the page and on their own fill`, () => {
43
+ const page = resolve(scope);
44
+ for (const control of ["Input", "TextArea", "SelectTrigger", "InputContainerFrame"]) {
45
+ const field = resolve(`${scope}_${control}`);
46
+ for (const key of ["borderColorField", "borderColorFieldHover"]) {
47
+ expect(
48
+ ratio(field[key], page.background),
49
+ `${control} ${key} on page`,
50
+ ).toBeGreaterThanOrEqual(minContrastRatio);
51
+ expect(
52
+ ratio(field[key], field.background),
53
+ `${control} ${key} on its fill`,
54
+ ).toBeGreaterThanOrEqual(minContrastRatio);
55
+ }
56
+ }
57
+ });
58
+
59
+ it(`${scope}: the segmented frame edge holds 3:1 on the page`, () => {
60
+ const page = resolve(scope);
61
+ expect(ratio(page.borderColorControl, page.background)).toBeGreaterThanOrEqual(
62
+ minContrastRatio,
63
+ );
64
+ });
65
+
66
+ it(`${scope}: the off Switch track edge`, () => {
67
+ const page = resolve(scope);
68
+ const track = resolve(`${scope}_Switch`);
69
+ if (scope.startsWith("light")) {
70
+ expect(ratio(track.switchTrackOffBorder, page.background)).toBeGreaterThanOrEqual(
71
+ minContrastRatio,
72
+ );
73
+ } else {
74
+ expect(track.switchTrackOffBorder).toBe(track.switchTrackOff);
75
+ }
76
+ });
77
+ }
78
+
79
+ for (const scope of intentScopes) {
80
+ it(`${scope}: an intent field edge comes from its own ramp and holds 3:1`, () => {
81
+ const scheme = scope.split("_")[0]!;
82
+ const own = Array.from({ length: 12 }, (_, index) => themes[scope]![`color${index + 1}`]);
83
+ const pages = hueScopes.filter((name) => name.startsWith(`${scheme}_`)).concat(scheme);
84
+ for (const control of ["Input", "TextArea", "SelectTrigger", "InputContainerFrame"]) {
85
+ const field = resolve(`${scope}_${control}`);
86
+ for (const key of ["borderColorField", "borderColorFieldHover"]) {
87
+ expect(own, `${control} ${key}`).toContain(field[key]);
88
+ for (const page of pages)
89
+ expect(
90
+ ratio(field[key], resolve(page).background),
91
+ `${control} ${key} on ${page}`,
92
+ ).toBeGreaterThanOrEqual(minContrastRatio);
93
+ expect(
94
+ ratio(field[key], field.background),
95
+ `${control} ${key} on its fill`,
96
+ ).toBeGreaterThanOrEqual(minContrastRatio);
97
+ }
98
+ }
99
+ });
100
+ }
101
+
102
+ it("picks a neutral edge, not a tint that reads as the accent", () => {
103
+ for (const scope of hueScopes) {
104
+ const scheme = scope.split("_")[0]!;
105
+ const neutral = Array.from(
106
+ { length: 12 },
107
+ (_, index) => themes[scheme]![`color${index + 1}`],
108
+ );
109
+ const page = resolve(scope);
110
+ expect(neutral, `${scope} field`).toContain(page.borderColorField);
111
+ expect(neutral, `${scope} control`).toContain(page.borderColorControl);
112
+ }
113
+ });
114
+ });
@@ -4,11 +4,11 @@
4
4
  *
5
5
  * - `tinted`, in content: a filled wash with no edge, the preset's radius and
6
6
  * the size token's height. Light washes at `$color3` (#ededed on the stock
7
- * ramp), dark one step further up at `$color4`, because dark `$color3` sits
8
- * a single lightness point off the page and the button would vanish. On an
9
- * elevated ground (`useElevatedSurface()`: an overlay frame or an unnested
10
- * Panel) dark climbs one more step to `$color5`, because the dark elevated
11
- * tint is `$color4` itself (HIG: a control's fill sits above the elevated
7
+ * ramp), dark at `$color5`, one step above the `$color4` card and field it
8
+ * usually sits in (LC-151, FZ-8400). On an elevated ground
9
+ * (`useElevatedSurface()`: an overlay frame or an unnested Panel) dark
10
+ * climbs one more step to `$color6`, because the dark elevated tint is
11
+ * `$color5` itself (HIG: a control's fill sits above the elevated
12
12
  * background it is on). Light's elevated ground is the page step, so its
13
13
  * wash stays put.
14
14
  * - `plain`, inside a bar (`useBarSurface()`): ink only at rest, the tinted
@@ -30,11 +30,11 @@ export interface ControlTierPaint {
30
30
  }
31
31
 
32
32
  const lightWash: ControlTierPaint = { rest: "$color3", hover: "$color4", press: "$color5" };
33
- const darkWash: ControlTierPaint = { rest: "$color4", hover: "$color5", press: "$color6" };
33
+ const darkWash: ControlTierPaint = { rest: "$color5", hover: "$color6", press: "$color7" };
34
34
  const darkElevatedWash: ControlTierPaint = {
35
- rest: "$color5",
36
- hover: "$color6",
37
- press: "$color7",
35
+ rest: "$color6",
36
+ hover: "$color7",
37
+ press: "$color8",
38
38
  };
39
39
 
40
40
  /** The tinted wash for a scheme and ground: rest, hover and press fills. */
@@ -43,6 +43,15 @@ export function controlTierWash(dark: boolean, elevated = false): ControlTierPai
43
43
  return elevated ? darkElevatedWash : darkWash;
44
44
  }
45
45
 
46
+ /**
47
+ * A Slider or Progress track: `$color5`, a step above a dark `$color4` card;
48
+ * on a dark elevated ground `$color6`, a step above the `$color5` overlay
49
+ * (FZ-7944, FZ-8400). Light keeps `$color5`.
50
+ */
51
+ export function controlTrackFill(dark: boolean, elevated = false): string {
52
+ return dark && elevated ? "$color6" : "$color5";
53
+ }
54
+
46
55
  /** Rest, hover and press fills for a tier in a scheme, on the page or an elevated ground. */
47
56
  export function controlTierPaint(
48
57
  tier: ControlTier,
@@ -36,6 +36,7 @@ import { createTamagui } from "tamagui";
36
36
  import { boldPreset, densePreset, heroPreset, roomyPreset } from "./presets";
37
37
  import { createThemesBuilder, type GetThemeProps } from "./createThemes";
38
38
  import { defaultBaseTheme, defaultAccentTheme } from "./defaults";
39
+ import { darkHairlineContrast } from "./defaults/builderOptions";
39
40
  import { setSizeRecipeInputs } from "./sizeRecipes";
40
41
  import { resolveThemeThroughParents } from "./dedupeInherited";
41
42
 
@@ -157,8 +158,9 @@ describe("createDefaultThemeConfig — builderOptions.getTheme composition (DF-0
157
158
  for (const scheme of ["light", "dark"] as const) {
158
159
  const theme = themes[scheme];
159
160
  expect(normalizeToHex(theme.outlineColor)).toBeTruthy();
160
- // LC-152: textMuted is the secondary ink, $color9 in light, $color10 in dark.
161
- expect(theme.textMuted).toBe(theme[scheme === "light" ? "color9" : "color10"]);
161
+ // LC-152: textMuted is the secondary ink, $color9 in light and, on the
162
+ // dark $color4 card (FZ-8400), $color11 in dark.
163
+ expect(theme.textMuted).toBe(theme[scheme === "light" ? "color9" : "color11"]);
162
164
  }
163
165
  expect(config.recipeFamilies?.control.boxVariants.$4.height).toBe(40);
164
166
  });
@@ -333,9 +335,10 @@ describe("the v5 theme matrix", () => {
333
335
  const themes = config.tamagui.themes as unknown as RawThemes;
334
336
 
335
337
  // LC-151 SURFACE-LADDER: the grouped step. Light lifts cards, list items
336
- // and fields to white on the grey page; dark keeps them one step lighter
337
- // than the page (the LC-85 pair). The grouped step is light surface1 itself,
338
- // so it costs no CSS; tracks move to surface2 to stay off a white card.
338
+ // and fields to white on the grey page; dark drops the page to $color1 and
339
+ // lifts them to $color4, iOS's grouped step (FZ-8400). The grouped step is
340
+ // surface1 itself, so it costs no CSS; tracks move to surface2 to stay off
341
+ // a white card.
339
342
  it("puts cards, list items and fields on the grouped step", () => {
340
343
  for (const component of [
341
344
  "Card",
@@ -349,7 +352,7 @@ describe("the v5 theme matrix", () => {
349
352
  expect(themes[`light_${component}`].background, `light_${component}`).toBe(
350
353
  themes.light.color1,
351
354
  );
352
- expect(themes[`dark_${component}`].background, `dark_${component}`).toBe(themes.dark.color3);
355
+ expect(themes[`dark_${component}`].background, `dark_${component}`).toBe(themes.dark.color4);
353
356
  expect(themes[`light_${component}`].backgroundPress, `light_${component} press`).toBe(
354
357
  themes.light.color3,
355
358
  );
@@ -364,6 +367,123 @@ describe("the v5 theme matrix", () => {
364
367
  expect(themes.light_orange_Card.background).toBe(themes.light_orange.color1);
365
368
  });
366
369
 
370
+ const edgeSurfaces = ["_Card", "_ListItem", "_Input", "_TextArea", "_SelectTrigger"];
371
+
372
+ // FZ-9281 / LC-148: iOS's dark opaqueSeparator #38383A over its grouped cell
373
+ // #1C1C1E is 1.454:1. Every dark ramp theme's hairline clears that against
374
+ // the page and against the grouped surface (a Card) it divides.
375
+ it("lifts every dark hairline off the page and the card by iOS's separator step", () => {
376
+ const iosSeparatorStep = contrastRatio(
377
+ relativeLuminance("#38383a"),
378
+ relativeLuminance("#1c1c1e"),
379
+ );
380
+ expect(darkHairlineContrast).toBeCloseTo(iosSeparatorStep, 2);
381
+ const hex = (value: unknown) =>
382
+ normalizeToHex(String(typeof value === "string" ? value : (value as { val?: string })?.val))!;
383
+ const contrastOf = (a: unknown, b: unknown) =>
384
+ contrastRatio(relativeLuminance(hex(a)), relativeLuminance(hex(b)));
385
+ const names = Object.keys(themes).filter((name) => /^dark(_[a-z]+)?$/.test(name));
386
+ expect(names.length).toBeGreaterThan(8);
387
+ const luminanceOf = (value: unknown) => relativeLuminance(hex(value));
388
+ for (const name of names) {
389
+ if (name === "dark_active" || name === "dark_accent") continue;
390
+ const page = resolveThemeThroughParents(themes, name);
391
+ for (const surface of ["", ...edgeSurfaces]) {
392
+ const theme = resolveThemeThroughParents(themes, `${name}${surface}`);
393
+ const label = `${name}${surface}`;
394
+ const grounds = surface ? [page.background, theme.background] : [page.background];
395
+ for (const ground of grounds) {
396
+ expect(
397
+ contrastOf(theme.borderColorHairline, ground),
398
+ `${label} hairline on ${ground === page.background ? "page" : "surface"}`,
399
+ ).toBeGreaterThanOrEqual(iosSeparatorStep);
400
+ }
401
+ for (const ground of grounds) {
402
+ expect(
403
+ contrastOf(theme.borderColor, ground),
404
+ `${label} medium edge on ${ground === page.background ? "page" : "surface"}`,
405
+ ).toBeGreaterThanOrEqual(iosSeparatorStep);
406
+ }
407
+ expect(luminanceOf(theme.borderColor), `${label} medium above hairline`).toBeGreaterThan(
408
+ luminanceOf(theme.borderColorHairline),
409
+ );
410
+ }
411
+ }
412
+ });
413
+
414
+ it("keeps every light edge where it was (FZ-9281 is dark only)", () => {
415
+ const value = (raw: unknown) =>
416
+ String(typeof raw === "string" ? raw : (raw as { val?: string })?.val);
417
+ const edges = Object.keys(themes)
418
+ .filter((name) => /^light(_[a-z]+)?$/.test(name))
419
+ .sort()
420
+ .flatMap((name) =>
421
+ ["", ...edgeSurfaces].map((surface) => {
422
+ const theme = resolveThemeThroughParents(themes, `${name}${surface}`);
423
+ return `${name}${surface} ${value(theme.borderColorHairline)} ${value(theme.borderColor)}`;
424
+ }),
425
+ );
426
+ expect(edges.join("\n")).toMatchSnapshot();
427
+ });
428
+
429
+ // FZ-8400: iOS's grouped step is secondarySystemGroupedBackground #1C1C1E
430
+ // on systemGroupedBackground #000000, 1.23:1. Every preset's dark page and
431
+ // its inset group (a Card) clear it, the group's own small edge reads off
432
+ // its fill, and a Button and an overlay still step off the card.
433
+ it("steps dark grouped surfaces off the page by at least iOS's grouped step", () => {
434
+ const iosGroupedStep = contrastRatio(
435
+ relativeLuminance("#1c1c1e"),
436
+ relativeLuminance("#000000"),
437
+ );
438
+ const contrastOf = (a: string, b: string) =>
439
+ contrastRatio(relativeLuminance(normalizeToHex(a)!), relativeLuminance(normalizeToHex(b)!));
440
+ const read = (name: string) => resolveThemeThroughParents(themes, name);
441
+ // The neutral ramp, the built-in presets' hues and the app presets' hues.
442
+ const presetHues = [boldPreset, heroPreset, densePreset, roomyPreset].map((p) => p.theme);
443
+ for (const hue of new Set([
444
+ "",
445
+ ...presetHues,
446
+ "blue",
447
+ "gray",
448
+ "purple",
449
+ "green",
450
+ "orange",
451
+ "pink",
452
+ ])) {
453
+ const at = (suffix: string) => read(["dark", hue, suffix].filter(Boolean).join("_"));
454
+ const page = at("");
455
+ const card = at("Card");
456
+ const label = hue || "neutral";
457
+ expect(
458
+ contrastOf(card.background, page.background),
459
+ `${label} card on page`,
460
+ ).toBeGreaterThanOrEqual(iosGroupedStep);
461
+ expect(
462
+ relativeLuminance(normalizeToHex(at("Button").background)!),
463
+ `${label} button over card`,
464
+ ).toBeGreaterThan(relativeLuminance(normalizeToHex(card.background)!));
465
+ // LC-05 selection fills inside the group never land on its own fill,
466
+ // and on the page they stay the ramp steps they replaced.
467
+ for (const key of ["selectFill", "selectPress"]) {
468
+ expect(contrastOf(card[key], card.background), `${label} ${key} on card`).toBeGreaterThan(
469
+ 1.1,
470
+ );
471
+ }
472
+ expect(page.selectFill, `${label} page selectFill`).toBe(page.color4);
473
+ expect(page.selectPress, `${label} page selectPress`).toBe(page.color5);
474
+ }
475
+ for (const scheme of ["light", "dark"] as const) {
476
+ for (const name of [scheme, `${scheme}_blue`, `${scheme}_gray`]) {
477
+ const page = themes[name];
478
+ expect([page.selectFill, page.selectPress], name).toEqual([page.color4, page.color5]);
479
+ }
480
+ }
481
+ expect(read("light_Card").selectFill).toBe(themes.light.color4);
482
+ // Light keeps the grey page and the white grouped step.
483
+ expect(themes.light.background).toBe(themes.light.color2);
484
+ expect(themes.light_Card.background).toBe(themes.light.color1);
485
+ });
486
+
367
487
  it("binds the v5 shadow3 alpha in both schemes", () => {
368
488
  for (const [scheme, alphas] of [
369
489
  ["light", [0.04, 0.08, 0.12, 0.22, 0.33, 0.44]],
@@ -377,9 +497,9 @@ describe("the v5 theme matrix", () => {
377
497
  });
378
498
 
379
499
  it("uses v5 surface templates for pages and components", () => {
380
- for (const scheme of ["light", "dark"] as const) {
381
- expect(themes[scheme].background).toBe(themes[scheme].color2);
382
- }
500
+ expect(themes.light.background).toBe(themes.light.color2);
501
+ // FZ-8400: the dark page is the ramp's darkest step, iOS's black ground.
502
+ expect(themes.dark.background).toBe(themes.dark.color1);
383
503
  const button = themes.light_Button;
384
504
  expect(relativeLuminance(normalizeToHex(button.backgroundHover)!)).toBeGreaterThan(
385
505
  relativeLuminance(normalizeToHex(button.background)!),
@@ -415,10 +535,13 @@ describe("the v5 theme matrix", () => {
415
535
  builderOptions: { componentThemes: { Custom: { template: "surface1" } } },
416
536
  }).tamagui.themes as unknown as RawThemes;
417
537
  for (const scheme of ["light", "dark"] as const) {
418
- expect(custom[`${scheme}_brand`].background).toBe(custom[`${scheme}_brand`].color2);
419
- // surface1 is the LC-151 grouped step: white in light, one step up in dark.
538
+ // The page is $color2 in light and $color1 in dark (LC-151, FZ-8400).
539
+ expect(custom[`${scheme}_brand`].background).toBe(
540
+ custom[`${scheme}_brand`][scheme === "light" ? "color2" : "color1"],
541
+ );
542
+ // surface1 is the LC-151 grouped step: white in light, $color4 in dark.
420
543
  expect(custom[`${scheme}_Custom`].background).toBe(
421
- custom[scheme][scheme === "light" ? "color1" : "color3"],
544
+ custom[scheme][scheme === "light" ? "color1" : "color4"],
422
545
  );
423
546
  expect(custom).not.toHaveProperty(`${scheme}_Button`);
424
547
  }