@multiplatform.one/theme 7.4.0 → 7.5.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.
- package/README.md +2 -1
- package/package.json +5 -5
- package/src/figma/figmaTokens.spec.ts +25 -21
- package/src/figma/figmaTokens.ts +2 -2
- package/src/font.ts +6 -19
- package/src/keyboardFocusRing.ts +12 -6
- package/src/theme/FontKnobStyles.tsx +5 -3
- package/src/theme/Tint.tsx +3 -2
- package/src/theme/animations/css.ts +14 -1
- package/src/theme/colorRules.spec.ts +233 -2
- package/src/theme/colorRules.ts +118 -0
- package/src/theme/cornerSmoothing.native.ts +39 -2
- package/src/theme/cornerSmoothing.spec.ts +213 -3
- package/src/theme/cornerSmoothing.ts +132 -10
- package/src/theme/createDefaultThemeConfig.spec.ts +107 -1
- package/src/theme/createDefaultThemeConfig.ts +27 -8
- package/src/theme/defaults/builderOptions.ts +23 -10
- package/src/theme/defaults/categoryFonts.ts +21 -0
- package/src/theme/defaults/fonts.spec.ts +93 -10
- package/src/theme/defaults/fonts.theme.spec.tsx +128 -0
- package/src/theme/defaults/fonts.ts +155 -27
- package/src/theme/defaults/index.ts +1 -0
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +0 -11
- package/src/theme/focusState.spec.ts +110 -1
- package/src/theme/focusState.ts +32 -7
- package/src/theme/fontCategoryStacks.ts +24 -0
- package/src/theme/fontKnobStyles.spec.ts +15 -2
- package/src/theme/glyphPaint.spec.ts +26 -0
- package/src/theme/glyphPaint.ts +46 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/knobs.ts +18 -2
- package/src/theme/layoutTokens.spec.ts +3 -1
- package/src/theme/layoutTokens.ts +47 -4
- package/src/theme/mpo13Proof.spec.ts +3 -3
- package/src/theme/mpo19Proof.spec.ts +285 -0
- package/src/theme/presets.ts +1 -1
- package/src/theme/radiusClass.spec.ts +394 -0
- package/src/theme/radiusClass.ts +203 -4
- package/src/theme/radiusClassTable.generated.ts +121 -0
- package/src/theme/recipeInputs.ts +223 -36
- package/src/theme/recipes.ts +68 -4
- package/src/theme/resolveKnobs.spec.ts +139 -11
- package/src/theme/resolveKnobs.ts +190 -25
- package/src/theme/sizeLadder.spec.ts +84 -0
- package/src/theme/sizeRecipes.spec.ts +19 -6
- package/src/theme/sizeRecipes.ts +43 -13
- package/src/theme/theme.native.tsx +2 -0
- package/src/theme/theme.provider.spec.ts +9 -0
- package/src/theme/theme.tsx +6 -3
- package/src/theme/tint.spec.tsx +36 -35
- package/src/theme/useResolvedKnobs.ts +6 -3
- package/types/figma/figmaTokens.d.ts +1 -1
- package/types/figma/figmaTokens.d.ts.map +1 -1
- package/types/font.d.ts.map +1 -1
- package/types/keyboardFocusRing.d.ts +11 -5
- package/types/keyboardFocusRing.d.ts.map +1 -1
- package/types/theme/FontKnobStyles.d.ts +2 -1
- package/types/theme/FontKnobStyles.d.ts.map +1 -1
- package/types/theme/Tint.d.ts +2 -1
- package/types/theme/Tint.d.ts.map +1 -1
- package/types/theme/animations/css.d.ts +15 -2
- package/types/theme/animations/css.d.ts.map +1 -1
- package/types/theme/colorRules.d.ts +59 -1
- package/types/theme/colorRules.d.ts.map +1 -1
- package/types/theme/cornerSmoothing.d.ts +42 -20
- package/types/theme/cornerSmoothing.d.ts.map +1 -1
- package/types/theme/cornerSmoothing.native.d.ts +22 -2
- package/types/theme/cornerSmoothing.native.d.ts.map +1 -1
- package/types/theme/createDefaultThemeConfig.d.ts +21 -3
- package/types/theme/createDefaultThemeConfig.d.ts.map +1 -1
- package/types/theme/defaults/builderOptions.d.ts.map +1 -1
- package/types/theme/defaults/categoryFonts.d.ts +19 -0
- package/types/theme/defaults/categoryFonts.d.ts.map +1 -0
- package/types/theme/defaults/fonts.d.ts +135 -2
- package/types/theme/defaults/fonts.d.ts.map +1 -1
- package/types/theme/defaults/index.d.ts +1 -0
- package/types/theme/defaults/index.d.ts.map +1 -1
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
- package/types/theme/focusState.d.ts +32 -6
- package/types/theme/focusState.d.ts.map +1 -1
- package/types/theme/fontCategoryStacks.d.ts +11 -0
- package/types/theme/fontCategoryStacks.d.ts.map +1 -0
- package/types/theme/glyphPaint.d.ts +16 -0
- package/types/theme/glyphPaint.d.ts.map +1 -0
- package/types/theme/index.d.ts +3 -0
- package/types/theme/index.d.ts.map +1 -1
- package/types/theme/knobs.d.ts +10 -2
- package/types/theme/knobs.d.ts.map +1 -1
- package/types/theme/layoutTokens.d.ts +46 -4
- package/types/theme/layoutTokens.d.ts.map +1 -1
- package/types/theme/radiusClass.d.ts +96 -4
- package/types/theme/radiusClass.d.ts.map +1 -1
- package/types/theme/radiusClassTable.generated.d.ts +146 -0
- package/types/theme/radiusClassTable.generated.d.ts.map +1 -0
- package/types/theme/recipeInputs.d.ts +85 -14
- package/types/theme/recipeInputs.d.ts.map +1 -1
- package/types/theme/recipes.d.ts +78 -4
- package/types/theme/recipes.d.ts.map +1 -1
- package/types/theme/resolveKnobs.d.ts +12 -1
- package/types/theme/resolveKnobs.d.ts.map +1 -1
- package/types/theme/sizeRecipes.d.ts +27 -14
- package/types/theme/sizeRecipes.d.ts.map +1 -1
- package/types/theme/theme.d.ts.map +1 -1
- package/types/theme/theme.native.d.ts.map +1 -1
- package/types/theme/useResolvedKnobs.d.ts +2 -2
- package/types/theme/useResolvedKnobs.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,8 @@ Requires `react` and `tamagui` as peers.
|
|
|
22
22
|
including intent + component overrides
|
|
23
23
|
- **Recipes** — control/text/elevation state recipes (hover, press, focus-visible)
|
|
24
24
|
- **Optics** — the `cornerSmoothing` knob (`round` | `smooth` squircle corners:
|
|
25
|
-
web progressive enhancement via CSS `corner-shape`,
|
|
25
|
+
web progressive enhancement via CSS `corner-shape: squircle`, computed
|
|
26
|
+
`superellipse(2)` on Chromium 152; native stays round),
|
|
26
27
|
`hairline` separator treatments (0.5 device-pixel rules with a 1px low-DPI
|
|
27
28
|
fallback), and tracking-by-size in the default font faces
|
|
28
29
|
- **Chart palette** — `useChartPalette()`: identity-led series colors
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/theme",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.5.0",
|
|
4
4
|
"description": "Tamagui theme system for multiplatform.one",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"multiplatform",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"@tamagui/toast": "2.7.6",
|
|
54
54
|
"@tamagui/web": "2.7.6",
|
|
55
55
|
"react-cookie": "^8.1.2",
|
|
56
|
-
"@multiplatform.one/platform": "7.
|
|
57
|
-
"@multiplatform.one/store": "7.
|
|
56
|
+
"@multiplatform.one/platform": "7.5.0",
|
|
57
|
+
"@multiplatform.one/store": "7.5.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@tamagui/animations-css": "2.7.6",
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
"tamagui": "2.7.6",
|
|
70
70
|
"typescript": "~5.9.3",
|
|
71
71
|
"vitest": "^4.1.5",
|
|
72
|
-
"@multiplatform.one/
|
|
73
|
-
"@multiplatform.one/
|
|
72
|
+
"@multiplatform.one/test-utils": "7.5.0",
|
|
73
|
+
"@multiplatform.one/config": "7.5.0"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"@tamagui/animations-css": "^2.0.0-rc",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as Colors from "@tamagui/colors";
|
|
2
|
-
import {
|
|
2
|
+
import { defaultConfig } from "@tamagui/config/v5";
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
4
|
import { normalizeToHex } from "../theme/colorRules";
|
|
5
5
|
import { defaultAccentTheme } from "../theme/defaults/accent";
|
|
@@ -89,13 +89,13 @@ describe("buildFigmaTokens", () => {
|
|
|
89
89
|
|
|
90
90
|
describe("semantic", () => {
|
|
91
91
|
it("derives the current interaction-ramp aliases (post-knobs rebuild)", () => {
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
// MPO-19 X4: the seven pinned ramp steps were deleted from
|
|
93
|
+
// `defaultBuilderOptions.getTheme`, so these aliases are the theme
|
|
94
|
+
// BUILDER's derivation again rather than mpo's re-anchoring. That is
|
|
95
|
+
// the correction landing, and what this pins is that it landed —
|
|
96
|
+
// borderColor back off color5, the background states no longer forced.
|
|
97
|
+
expect(result.semantic.borderColor?.alias).toBe("color4");
|
|
96
98
|
expect(result.semantic.textMuted?.alias).toBe("color11");
|
|
97
|
-
expect(result.semantic.backgroundPress?.alias).toBe("color5");
|
|
98
|
-
expect(result.semantic.backgroundActive?.alias).toBe("color4");
|
|
99
99
|
expect(result.semantic.placeholderColor?.alias).toBe("color10");
|
|
100
100
|
expect(result.semantic.surfaceMuted?.alias).toBe("color10");
|
|
101
101
|
expect(result.semantic.borderStrong?.alias).toBe("color7");
|
|
@@ -135,13 +135,16 @@ describe("buildFigmaTokens", () => {
|
|
|
135
135
|
});
|
|
136
136
|
|
|
137
137
|
describe("sizing", () => {
|
|
138
|
-
it("mirrors
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
expect(result.sizing["
|
|
138
|
+
it("mirrors the v5 defaultConfig tokens with figma-safe names", () => {
|
|
139
|
+
// Read against the ERA the package pins (MPO-19 X13), not against a
|
|
140
|
+
// transcribed table: v5 keys carry the `$`, values are raw numbers.
|
|
141
|
+
const size = defaultConfig.tokens.size as Record<string, number>;
|
|
142
|
+
expect(result.sizing["size-true"]).toBe(size.$true);
|
|
143
|
+
expect(result.sizing["radius-4"]).toBe(
|
|
144
|
+
(defaultConfig.tokens.radius as Record<string, number>)["4"],
|
|
145
|
+
);
|
|
143
146
|
expect(result.sizing["space-1-5"]).toBe(
|
|
144
|
-
(tokens.space as Record<string,
|
|
147
|
+
(defaultConfig.tokens.space as Record<string, number>)["$1.5"],
|
|
145
148
|
);
|
|
146
149
|
});
|
|
147
150
|
|
|
@@ -181,7 +184,7 @@ describe("buildFigmaTokens", () => {
|
|
|
181
184
|
const { modes } = result.knobs.elevation;
|
|
182
185
|
expect(modes.none).toEqual({ sizeToken: null, px: null });
|
|
183
186
|
expect(modes.small.sizeToken).toBe("$1");
|
|
184
|
-
expect(modes.small.px).toBe((tokens.size as Record<string,
|
|
187
|
+
expect(modes.small.px).toBe((defaultConfig.tokens.size as Record<string, number>).$1);
|
|
185
188
|
expect(modes.medium.sizeToken).toBe("$2");
|
|
186
189
|
expect(modes.large.sizeToken).toBe("$4");
|
|
187
190
|
});
|
|
@@ -216,17 +219,18 @@ describe("buildFigmaTokens", () => {
|
|
|
216
219
|
|
|
217
220
|
describe("typography (closes deferred task 10.0)", () => {
|
|
218
221
|
it("exports heading/body size + lineHeight tables from the built default fonts", () => {
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
expect(result.typography.body.
|
|
224
|
-
expect(result.typography.
|
|
222
|
+
// MPO-19 X8/X9: heading rides the body scale now, so H1 $10 is 40 at
|
|
223
|
+
// weight 800 rather than 64 at 400, and body $true is v5's 15 with the
|
|
224
|
+
// tapering leading instead of v4's 14 with `size + 10`.
|
|
225
|
+
expect(result.typography.heading.size["10"]).toBe(40);
|
|
226
|
+
expect(result.typography.body.size.true).toBe(15);
|
|
227
|
+
expect(result.typography.body.lineHeight.true).toBe(23);
|
|
228
|
+
expect(result.typography.heading.lineHeight["10"]).toBe(50);
|
|
225
229
|
});
|
|
226
230
|
|
|
227
231
|
it("exports Inter dynamic tracking per size", () => {
|
|
228
232
|
const body = result.typography.body;
|
|
229
|
-
expect(body.letterSpacing.true).toBe(interTrackingPx(
|
|
233
|
+
expect(body.letterSpacing.true).toBe(interTrackingPx(15));
|
|
230
234
|
expect(Object.keys(body.letterSpacing).length).toBeGreaterThanOrEqual(16);
|
|
231
235
|
});
|
|
232
236
|
|
package/src/figma/figmaTokens.ts
CHANGED
|
@@ -93,7 +93,7 @@ export interface FigmaRadiusMode {
|
|
|
93
93
|
/** `Sizing` collection variable to alias (e.g. `radius-4`). */
|
|
94
94
|
figmaName: string;
|
|
95
95
|
px: number;
|
|
96
|
-
/** Outer/nested radius fragment (`knobProps.
|
|
96
|
+
/** Outer/nested radius fragment (`knobProps.borderRadiusNested`). */
|
|
97
97
|
outerToken: string;
|
|
98
98
|
outerFigmaName: string;
|
|
99
99
|
outerPx: number;
|
|
@@ -344,7 +344,7 @@ function buildKnobs(): FigmaKnobsExport {
|
|
|
344
344
|
for (const mode of RADIUS_KNOB_MODES) {
|
|
345
345
|
const { knobProps } = resolveKnobs({ ...defaultKnobs, borderRadius: mode });
|
|
346
346
|
const token = String(knobProps.borderRadius.borderRadius);
|
|
347
|
-
const outerToken = String(knobProps.
|
|
347
|
+
const outerToken = String(knobProps.borderRadiusNested.borderRadius);
|
|
348
348
|
borderRadius[mode] = {
|
|
349
349
|
token,
|
|
350
350
|
figmaName: tokenToFigma(token, "radius"),
|
package/src/font.ts
CHANGED
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
import type { GenericFont } from "@tamagui/web";
|
|
2
2
|
import { createFont } from "@tamagui/web";
|
|
3
|
+
import { bodyFontSizes, bodyLineHeightPx } from "./theme/defaults/fonts";
|
|
3
4
|
|
|
4
5
|
const defaults = {
|
|
5
|
-
size:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
4: 14,
|
|
10
|
-
5: 15,
|
|
11
|
-
6: 16,
|
|
12
|
-
7: 18,
|
|
13
|
-
8: 21,
|
|
14
|
-
9: 28,
|
|
15
|
-
10: 42,
|
|
16
|
-
11: 52,
|
|
17
|
-
12: 62,
|
|
18
|
-
13: 72,
|
|
19
|
-
14: 92,
|
|
20
|
-
15: 114,
|
|
21
|
-
16: 124,
|
|
22
|
-
} as const,
|
|
6
|
+
// Same size table as Inter body, including `true: 14`. A missing `true`
|
|
7
|
+
// key is what collapsed `$serif`/`$mono`/`$rounded` to -apple-system
|
|
8
|
+
// 14px/normal on the label text node (MPO-44).
|
|
9
|
+
size: bodyFontSizes,
|
|
23
10
|
// Neutral by default (LC-11): tracking is a per-family function of size,
|
|
24
11
|
// so a shared constant table is always wrong for someone — the previous
|
|
25
12
|
// Inter-ish table leaked +3px tracking into mono/script faces at $5/$6
|
|
@@ -37,7 +24,7 @@ export function createDefaultFont<A extends GenericFont>(
|
|
|
37
24
|
const size = font.size || defaults.size;
|
|
38
25
|
return createFont({
|
|
39
26
|
lineHeight: Object.fromEntries(
|
|
40
|
-
Object.entries(size).map(([k, v]) => [k, typeof v === "number" ? v
|
|
27
|
+
Object.entries(size).map(([k, v]) => [k, typeof v === "number" ? bodyLineHeightPx(v) : v]),
|
|
41
28
|
) as typeof size,
|
|
42
29
|
letterSpacing: defaults.letterSpacing,
|
|
43
30
|
weight: defaults.weight,
|
package/src/keyboardFocusRing.ts
CHANGED
|
@@ -36,15 +36,21 @@ export function wasKeyboardFocus(): boolean {
|
|
|
36
36
|
return lastInputWasKeyboard;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
import {
|
|
39
|
+
import { FOCUS_VISIBLE_RING_INSET } from "./theme/focusState";
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Standard manual ring props — the
|
|
43
|
-
* (2px solid `$outlineColor`,
|
|
44
|
-
*
|
|
42
|
+
* Standard manual ring props — the INSET ring geometry
|
|
43
|
+
* (`FOCUS_VISIBLE_RING_INSET`: 2px solid `$outlineColor`, offset −2), spread
|
|
44
|
+
* here so manually-painted rings can never drift from the theme contract.
|
|
45
|
+
* Manual painting exists for roving-focus / activedescendant composites
|
|
46
|
+
* (ToggleGroup segments inside a clipped Group, menu/listbox rows under a
|
|
47
|
+
* clipped overlay, table rows and cells) — items inside clipping ancestors,
|
|
48
|
+
* which is exactly the LC-71 §4 inset carve-out; an offset-0 band there
|
|
49
|
+
* would be severed by the clip. Standalone controls take the offset-0
|
|
50
|
+
* `FOCUS_VISIBLE_RING` via `focusVisibleStyle` / `ensureFocusVisibleRing()`.
|
|
45
51
|
*/
|
|
46
52
|
export const keyboardFocusRingProps = {
|
|
47
|
-
...
|
|
53
|
+
...FOCUS_VISIBLE_RING_INSET,
|
|
48
54
|
} as const;
|
|
49
55
|
|
|
50
56
|
/**
|
|
@@ -85,7 +91,7 @@ export const compositeFocusRingCss = `/* LC-71: ring on the composite outer box;
|
|
|
85
91
|
}
|
|
86
92
|
.mp-chip-dismiss:focus-visible .mp-chip-dismiss-ring {
|
|
87
93
|
outline: 2px solid var(--outlineColor, var(--c-outlineColor, CanvasText));
|
|
88
|
-
outline-offset:
|
|
94
|
+
outline-offset: 0;
|
|
89
95
|
}`;
|
|
90
96
|
|
|
91
97
|
const COMPOSITE_RING_STYLE_ID = "mp-composite-focus-ring";
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { getConfig } from "@tamagui/web";
|
|
29
29
|
import { useEffect } from "react";
|
|
30
|
+
import { fontCategoryStacks } from "./fontCategoryStacks";
|
|
30
31
|
import type { FontCategory } from "./knobs";
|
|
31
32
|
import { defaultKnobs } from "./knobs";
|
|
32
33
|
import { usePresetContext } from "./PresetContext";
|
|
@@ -90,7 +91,8 @@ export interface FontScopeTables {
|
|
|
90
91
|
/**
|
|
91
92
|
* Resolve the concrete font-family stack registered in the Tamagui config for
|
|
92
93
|
* a font category. Returns undefined when the category font is not registered
|
|
93
|
-
* (the caller then
|
|
94
|
+
* (the caller then uses `fontCategoryStacks` so the knob still writes
|
|
95
|
+
* `--f-family` — live SHC console had the style tag and skipped the family).
|
|
94
96
|
*/
|
|
95
97
|
export function getConfiguredFontFamily(category: FontCategory): string | undefined {
|
|
96
98
|
let config: ReturnType<typeof getConfig> | undefined;
|
|
@@ -202,13 +204,13 @@ export function buildFontKnobCss(
|
|
|
202
204
|
defaultLeadingRule("body", bodyFont),
|
|
203
205
|
];
|
|
204
206
|
if (headingFont !== "sans-serif") {
|
|
205
|
-
const family = resolveFamily(headingFont);
|
|
207
|
+
const family = resolveFamily(headingFont) ?? fontCategoryStacks[headingFont];
|
|
206
208
|
if (family) {
|
|
207
209
|
rules.push(buildScopeRule("heading", headingFont, family, resolveTables("heading")));
|
|
208
210
|
}
|
|
209
211
|
}
|
|
210
212
|
if (bodyFont !== "sans-serif") {
|
|
211
|
-
const family = resolveFamily(bodyFont);
|
|
213
|
+
const family = resolveFamily(bodyFont) ?? fontCategoryStacks[bodyFont];
|
|
212
214
|
if (family) {
|
|
213
215
|
rules.push(buildScopeRule("body", bodyFont, family, resolveTables("body")));
|
|
214
216
|
}
|
package/src/theme/Tint.tsx
CHANGED
|
@@ -13,7 +13,8 @@ export interface TintProps extends PropsWithChildren {
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Depth-based tint component. Each nesting level increments the depth
|
|
16
|
-
* and selects a tint from the preset's tints array via
|
|
16
|
+
* and selects a tint from the preset's tints array via
|
|
17
|
+
* `tints[(depth - 1) % length]` so the first Tint is `tints[0]`.
|
|
17
18
|
*
|
|
18
19
|
* Applies a tint sub-theme based on nesting depth.
|
|
19
20
|
*/
|
|
@@ -21,7 +22,7 @@ export function Tint({ children, alt = 0, disable, ...props }: TintProps) {
|
|
|
21
22
|
const parentDepth = useContext(TintDepthContext);
|
|
22
23
|
const depth = parentDepth + 1 + alt;
|
|
23
24
|
const tints = usePresetTints();
|
|
24
|
-
const tint = tints.length > 0 ? tints[depth % tints.length] : undefined;
|
|
25
|
+
const tint = tints.length > 0 ? tints[(depth - 1) % tints.length] : undefined;
|
|
25
26
|
|
|
26
27
|
return (
|
|
27
28
|
<TintDepthContext.Provider value={depth}>
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { createAnimations } from "@tamagui/animations-css";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The CSS driver's named durations.
|
|
5
|
+
*
|
|
6
|
+
* `bouncy`, `lazy`, `slow`, `medium` and `quick` are Tamagui's own names and
|
|
7
|
+
* carry Tamagui's own values — a shared name that means something different
|
|
8
|
+
* here is worse than no name, because every consumer reading `animation="medium"`
|
|
9
|
+
* against the upstream docs gets a different answer than the one it renders.
|
|
10
|
+
* MPO-19 X10: `medium` was `ease-in-out 250ms` against stock `ease-in 300ms`,
|
|
11
|
+
* with no improvement argument written down anywhere.
|
|
12
|
+
*
|
|
13
|
+
* `tooltip`, `snappy` and `gentle` are additive — names Tamagui does not use,
|
|
14
|
+
* so they are ours to set and are not a contradiction.
|
|
15
|
+
*/
|
|
3
16
|
export const animationConfig = {
|
|
4
17
|
bouncy: "ease-in 200ms",
|
|
5
18
|
lazy: "ease-in 600ms",
|
|
6
19
|
slow: "ease-in 500ms",
|
|
7
|
-
medium: "ease-in
|
|
20
|
+
medium: "ease-in 300ms",
|
|
8
21
|
quick: "ease-in 100ms",
|
|
9
22
|
tooltip: "ease-in 400ms",
|
|
10
23
|
snappy: "ease-out 80ms",
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
aaTextContrastRatio,
|
|
4
|
+
assertContrast,
|
|
5
5
|
contrastRatio,
|
|
6
6
|
findReadableStep,
|
|
7
|
+
measureContrast,
|
|
8
|
+
minContrastRatio,
|
|
7
9
|
normalizeColorScale,
|
|
10
|
+
normalizeToHex,
|
|
11
|
+
relativeLuminance,
|
|
12
|
+
resolveThemeContrast,
|
|
8
13
|
$color,
|
|
9
14
|
semanticGroups,
|
|
10
15
|
} from "./colorRules";
|
|
16
|
+
import { createThemesBuilder } from "./createThemes";
|
|
17
|
+
import { defaultAccentTheme } from "./defaults/accent";
|
|
18
|
+
import { defaultBaseTheme } from "./defaults/base";
|
|
19
|
+
import { defaultBuilderOptions } from "./defaults/builderOptions";
|
|
11
20
|
|
|
12
21
|
describe("normalizeToHex", () => {
|
|
13
22
|
it('normalizes "white" to #ffffff', () => {
|
|
@@ -193,3 +202,225 @@ describe("semanticGroups", () => {
|
|
|
193
202
|
expect(semanticGroups[semanticGroups.length - 1].to).toBe(12);
|
|
194
203
|
});
|
|
195
204
|
});
|
|
205
|
+
|
|
206
|
+
describe("measureContrast", () => {
|
|
207
|
+
it("reports 21:1 for black on white and passes the AA floor", () => {
|
|
208
|
+
const report = measureContrast({ foreground: "#000000", background: "#ffffff" });
|
|
209
|
+
expect(report).toMatchObject({
|
|
210
|
+
foreground: "#000000",
|
|
211
|
+
background: "#ffffff",
|
|
212
|
+
ratio: 21,
|
|
213
|
+
floor: aaTextContrastRatio,
|
|
214
|
+
pass: true,
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("defaults the label to 'foreground on background'", () => {
|
|
219
|
+
expect(measureContrast({ foreground: "black", background: "white" }).label).toBe(
|
|
220
|
+
"black on white",
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("keeps a custom label and normalizes named/shorthand colors", () => {
|
|
225
|
+
const report = measureContrast({
|
|
226
|
+
foreground: "black",
|
|
227
|
+
background: "#fff",
|
|
228
|
+
label: "ink on paper",
|
|
229
|
+
});
|
|
230
|
+
expect(report.label).toBe("ink on paper");
|
|
231
|
+
expect(report.foreground).toBe("#000000");
|
|
232
|
+
expect(report.background).toBe("#ffffff");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("fails the MPO-48 dark accent toast pill (1.94:1)", () => {
|
|
236
|
+
const report = measureContrast({
|
|
237
|
+
foreground: "#46349d",
|
|
238
|
+
background: "#161519",
|
|
239
|
+
label: "MPO-48 dark accent toast action",
|
|
240
|
+
});
|
|
241
|
+
expect(report.ratio).toBe(1.94);
|
|
242
|
+
expect(report.floor).toBe(aaTextContrastRatio);
|
|
243
|
+
expect(report.pass).toBe(false);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("uses the reported 2-decimal ratio for the verdict", () => {
|
|
247
|
+
// #808080 on white is 3.9489…, reported 3.95 — pass at 3, miss at 4.5.
|
|
248
|
+
const at3 = measureContrast({
|
|
249
|
+
foreground: "#808080",
|
|
250
|
+
background: "#ffffff",
|
|
251
|
+
floor: minContrastRatio,
|
|
252
|
+
});
|
|
253
|
+
const atAa = measureContrast({ foreground: "#808080", background: "#ffffff" });
|
|
254
|
+
expect(at3.ratio).toBe(3.95);
|
|
255
|
+
expect(at3.pass).toBe(true);
|
|
256
|
+
expect(atAa.ratio).toBe(3.95);
|
|
257
|
+
expect(atAa.pass).toBe(false);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("throws on an unparseable foreground — never a skip", () => {
|
|
261
|
+
expect(() => measureContrast({ foreground: "potato", background: "#ffffff" })).toThrow(
|
|
262
|
+
/foreground is not an opaque color: potato/,
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("throws on an alpha background — never a skip", () => {
|
|
267
|
+
expect(() =>
|
|
268
|
+
measureContrast({ foreground: "#000000", background: "hsla(0, 0%, 100%, 0.2)" }),
|
|
269
|
+
).toThrow(/background is not an opaque color/);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("assertContrast", () => {
|
|
274
|
+
it("returns the reports when every pair clears its floor", () => {
|
|
275
|
+
const reports = assertContrast({ foreground: "black", background: "white" });
|
|
276
|
+
expect(reports).toHaveLength(1);
|
|
277
|
+
expect(reports[0].pass).toBe(true);
|
|
278
|
+
expect(reports[0].ratio).toBe(21);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("throws ONE error naming every miss, not the first", () => {
|
|
282
|
+
expect(() =>
|
|
283
|
+
assertContrast([
|
|
284
|
+
{ foreground: "black", background: "white", label: "ok" },
|
|
285
|
+
{ foreground: "#46349d", background: "#161519", label: "toast pill" },
|
|
286
|
+
{ foreground: "#808080", background: "#ffffff", label: "gray" },
|
|
287
|
+
]),
|
|
288
|
+
).toThrow(
|
|
289
|
+
/contrast floor missed \(2 of 3 pairs\):[\s\S]*toast pill: #46349d on #161519 = 1.94:1[\s\S]*gray: #808080 on #ffffff = 3.95:1/,
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("resolveThemeContrast", () => {
|
|
295
|
+
it("addresses a pair by theme name and keys", () => {
|
|
296
|
+
expect(
|
|
297
|
+
resolveThemeContrast(
|
|
298
|
+
{ light: { color: "#242226", background: "#ffffff" } },
|
|
299
|
+
{ theme: "light", foreground: "color", background: "background" },
|
|
300
|
+
),
|
|
301
|
+
).toEqual({
|
|
302
|
+
foreground: "#242226",
|
|
303
|
+
background: "#ffffff",
|
|
304
|
+
floor: undefined,
|
|
305
|
+
label: "light: color on background",
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("carries an optional floor through to the pair", () => {
|
|
310
|
+
expect(
|
|
311
|
+
resolveThemeContrast(
|
|
312
|
+
{ light: { color: "#fff", background: "#000" } },
|
|
313
|
+
{ theme: "light", foreground: "color", background: "background", floor: minContrastRatio },
|
|
314
|
+
).floor,
|
|
315
|
+
).toBe(minContrastRatio);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("throws on a missing theme rather than skipping", () => {
|
|
319
|
+
expect(() =>
|
|
320
|
+
resolveThemeContrast({}, { theme: "nope", foreground: "color", background: "background" }),
|
|
321
|
+
).toThrow("no such theme: nope");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("throws on a missing key rather than skipping", () => {
|
|
325
|
+
expect(() =>
|
|
326
|
+
resolveThemeContrast(
|
|
327
|
+
{ light: { color: "#000" } },
|
|
328
|
+
{ theme: "light", foreground: "color", background: "background" },
|
|
329
|
+
),
|
|
330
|
+
).toThrow("light has no background");
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("contrast floor on built themes (DG-A11Y-01 / DG-COL-01)", () => {
|
|
335
|
+
const themes = createThemesBuilder(
|
|
336
|
+
defaultBaseTheme,
|
|
337
|
+
defaultAccentTheme,
|
|
338
|
+
defaultBuilderOptions,
|
|
339
|
+
).themes() as Record<string, Record<string, string>>;
|
|
340
|
+
|
|
341
|
+
it("base text tiers clear AA on page surfaces in both schemes", () => {
|
|
342
|
+
const pairs = [];
|
|
343
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
344
|
+
for (const foreground of ["color", "color11", "color12"]) {
|
|
345
|
+
for (const background of ["background", "color1", "color2"]) {
|
|
346
|
+
pairs.push(resolveThemeContrast(themes, { theme: scheme, foreground, background }));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const reports = assertContrast(pairs);
|
|
351
|
+
expect(reports.every((report) => report.pass)).toBe(true);
|
|
352
|
+
expect(reports).toHaveLength(18);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("accent Button color-on-background clears AA in both schemes", () => {
|
|
356
|
+
const reports = assertContrast(
|
|
357
|
+
(["light_accent_Button", "dark_accent_Button"] as const).map((theme) =>
|
|
358
|
+
resolveThemeContrast(themes, { theme, foreground: "color", background: "background" }),
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
expect(reports.map((report) => report.ratio)).toEqual([7.31, 7.61]);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("a colour pair below the floor FAILS", () => {
|
|
365
|
+
// MPO-48 measured the dark accent toast action pill on the page ground.
|
|
366
|
+
// Built: dark_accent.backgroundHover is #46349d; dark.background is #161519.
|
|
367
|
+
expect(() =>
|
|
368
|
+
assertContrast({
|
|
369
|
+
foreground: themes.dark_accent.backgroundHover,
|
|
370
|
+
background: themes.dark.background,
|
|
371
|
+
label: "MPO-48 dark accent toast action",
|
|
372
|
+
}),
|
|
373
|
+
).toThrow(/MPO-48 dark accent toast action: #46349d on #161519 = 1.94:1/);
|
|
374
|
+
|
|
375
|
+
// MPO-40: outlined warning $color11 on the warning page surface is 4.44:1.
|
|
376
|
+
expect(() =>
|
|
377
|
+
assertContrast(
|
|
378
|
+
resolveThemeContrast(themes, {
|
|
379
|
+
theme: "light_warning",
|
|
380
|
+
foreground: "color11",
|
|
381
|
+
background: "color1",
|
|
382
|
+
}),
|
|
383
|
+
),
|
|
384
|
+
).toThrow(/light_warning: color11 on color1: #9e6c00 on #fcfcf7 = 4.44:1/);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("current light_accent color-on-background clears the 3:1 non-text floor", () => {
|
|
388
|
+
// Parent accent is 3.96:1 — under AA, over the large-text/UI floor. The
|
|
389
|
+
// Button pair (asserted above) is the AA text contract; this pair is the
|
|
390
|
+
// trip-wire's control so the old pale-lavender step is the thing that fails.
|
|
391
|
+
const reports = assertContrast(
|
|
392
|
+
resolveThemeContrast(themes, {
|
|
393
|
+
theme: "light_accent",
|
|
394
|
+
foreground: "color",
|
|
395
|
+
background: "background",
|
|
396
|
+
floor: minContrastRatio,
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
expect(reports[0].ratio).toBe(3.96);
|
|
400
|
+
expect(reports[0].pass).toBe(true);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("re-introducing the old accent lightPalette[0] fails the test", () => {
|
|
404
|
+
// The pale-lavender step that shipped a ~1.17:1 CTA. Swapping only [0]
|
|
405
|
+
// leaves Button (palette[3]) alone and drops light_accent.background.
|
|
406
|
+
const broken = createThemesBuilder(
|
|
407
|
+
defaultBaseTheme,
|
|
408
|
+
{
|
|
409
|
+
...defaultAccentTheme,
|
|
410
|
+
lightPalette: ["hsla(250, 50%, 95%, 1)", ...defaultAccentTheme.lightPalette.slice(1)],
|
|
411
|
+
},
|
|
412
|
+
defaultBuilderOptions,
|
|
413
|
+
).themes() as Record<string, Record<string, string>>;
|
|
414
|
+
|
|
415
|
+
expect(() =>
|
|
416
|
+
assertContrast(
|
|
417
|
+
resolveThemeContrast(broken, {
|
|
418
|
+
theme: "light_accent",
|
|
419
|
+
foreground: "color",
|
|
420
|
+
background: "background",
|
|
421
|
+
floor: minContrastRatio,
|
|
422
|
+
}),
|
|
423
|
+
),
|
|
424
|
+
).toThrow(/light_accent: color on background: #f9f8fc on #eeecf9 = 1.1:1/);
|
|
425
|
+
});
|
|
426
|
+
});
|