@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.
- package/package.json +20 -5
- package/src/audit/constraintAudit.spec.ts +274 -87
- package/src/audit/constraintAudit.ts +135 -120
- package/src/audit/constraintScope.spec.ts +1 -1
- package/src/audit/designTokens.ts +42 -0
- package/src/audit/themeMatrix.spec.ts +83 -2
- package/src/audit/themeMatrix.ts +33 -1
- package/src/figma/figmaTokens.spec.ts +26 -34
- package/src/figma/figmaTokens.ts +26 -50
- package/src/fonts/createFontLoader.ts +4 -1
- package/src/menuRow.ts +4 -1
- package/src/theme/FontKnobStyles.tsx +2 -1
- package/src/theme/Preset.stories.tsx +0 -1
- package/src/theme/Preset.tsx +6 -1
- package/src/theme/PresetContext.ts +4 -0
- package/src/theme/Surface.spec.tsx +114 -67
- package/src/theme/Surface.stories.tsx +8 -25
- package/src/theme/Surface.tsx +87 -43
- package/src/theme/accentFloor.spec.ts +37 -11
- package/src/theme/activateThemeConfig.ts +22 -6
- package/src/theme/animations/animations.spec.ts +62 -0
- package/src/theme/animations/css.ts +9 -35
- package/src/theme/animations/reactNative.ts +98 -25
- package/src/theme/boundaryFloor.spec.ts +57 -0
- package/src/theme/chartPalette.spec.ts +56 -27
- package/src/theme/chartPalette.ts +33 -28
- package/src/theme/colorRules.spec.ts +32 -72
- package/src/theme/cookies.spec.ts +49 -18
- package/src/theme/cookies.ts +36 -7
- package/src/theme/cookiesPersistence.spec.ts +5 -5
- package/src/theme/createDefaultThemeConfig.spec.ts +182 -20
- package/src/theme/createDefaultThemeConfig.ts +34 -28
- package/src/theme/createThemes.ts +169 -218
- package/src/theme/defaults/accent.ts +3 -35
- package/src/theme/defaults/base.ts +14 -53
- package/src/theme/defaults/builderOptions.ts +68 -85
- package/src/theme/defaults/categoryFonts.ts +10 -0
- package/src/theme/defaults/fonts.spec.ts +84 -4
- package/src/theme/defaults/fonts.ts +166 -124
- package/src/theme/defaults/house.ts +48 -0
- package/src/theme/defaults/index.ts +1 -0
- package/src/theme/defaults/neutralRamp.spec.ts +52 -47
- package/src/theme/devtools/ColorLineVisualizer.tsx +6 -6
- package/src/theme/devtools/ThemeDevtoolsPanel.setMarkers.spec.tsx +13 -6
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +14 -77
- package/src/theme/devtools/animationOptions.spec.ts +23 -37
- package/src/theme/devtools/animationOptions.ts +4 -6
- package/src/theme/devtools/buildSnapshot.ts +0 -3
- package/src/theme/devtools/knobIcons.tsx +4 -24
- package/src/theme/focusState.spec.ts +9 -30
- package/src/theme/index.ts +17 -0
- package/src/theme/intentFill.spec.ts +42 -0
- package/src/theme/knobs.ts +14 -42
- package/src/theme/layoutTokens.ts +15 -1
- package/src/theme/layoutTokensHooks.spec.tsx +8 -4
- package/src/theme/mergedTokens.ts +20 -0
- package/src/theme/motion.spec.ts +84 -0
- package/src/theme/motion.ts +53 -0
- package/src/theme/motionStops.ts +17 -0
- package/src/theme/motionTiming.ts +59 -0
- package/src/theme/mpo13Proof.spec.ts +43 -46
- package/src/theme/mpo19Proof.spec.ts +13 -14
- package/src/theme/namedInkFloor.spec.ts +60 -0
- package/src/theme/overlaySurfaces.tsx +330 -0
- package/src/theme/preset.types.ts +16 -0
- package/src/theme/presetPreview.ts +32 -0
- package/src/theme/presetRoot.spec.tsx +89 -0
- package/src/theme/presetStore.persistence.spec.ts +141 -0
- package/src/theme/presetStore.spec.ts +205 -0
- package/src/theme/presetStore.ts +286 -0
- package/src/theme/presetUserSync.spec.tsx +168 -0
- package/src/theme/presetUserSync.ts +47 -0
- package/src/theme/presets.spec.ts +22 -15
- package/src/theme/presets.ts +22 -10
- package/src/theme/radiusClass.spec.ts +60 -41
- package/src/theme/radiusClass.ts +17 -4
- package/src/theme/radiusClassTable.generated.ts +3 -3
- package/src/theme/readableColor.spec.ts +59 -152
- package/src/theme/readableColor.ts +34 -174
- package/src/theme/recipeInputs.ts +153 -295
- package/src/theme/recipes.ts +58 -18
- package/src/theme/resolveKnobs.spec.ts +528 -588
- package/src/theme/resolveKnobs.ts +250 -325
- package/src/theme/scale/ScaleStyles.native.tsx +19 -0
- package/src/theme/scale/ScaleStyles.tsx +48 -0
- package/src/theme/scale/activeScale.ts +30 -0
- package/src/theme/scale/deriveRecipeInputs.ts +110 -0
- package/src/theme/scale/generateScale.spec.ts +260 -0
- package/src/theme/scale/generateScale.ts +147 -0
- package/src/theme/scale/index.ts +54 -0
- package/src/theme/scale/installScaleConfig.native.spec.ts +77 -0
- package/src/theme/scale/installScaleConfig.native.ts +57 -0
- package/src/theme/scale/installScaleConfig.ts +7 -0
- package/src/theme/scale/scaleAttribute.ts +21 -0
- package/src/theme/scale/scaleCss.probe.spec.tsx +64 -0
- package/src/theme/scale/scaleCss.spec.ts +132 -0
- package/src/theme/scale/scaleCss.ts +95 -0
- package/src/theme/scale/scaleFonts.ts +106 -0
- package/src/theme/scale/scaleInputs.ts +322 -0
- package/src/theme/scale/themeScales.ts +181 -0
- package/src/theme/scale/useActiveScale.spec.tsx +129 -0
- package/src/theme/scale/useActiveScale.ts +95 -0
- package/src/theme/schemeStore.spec.ts +81 -0
- package/src/theme/schemeStore.ts +67 -0
- package/src/theme/shared.spec.ts +7 -6
- package/src/theme/shared.tsx +37 -3
- package/src/theme/sizeLadder.spec.ts +37 -36
- package/src/theme/sizeRecipes.spec.ts +179 -85
- package/src/theme/sizeRecipes.ts +48 -25
- package/src/theme/subsetThemes.spec.ts +9 -3
- package/src/theme/switchThumb.spec.ts +69 -0
- package/src/theme/syntaxFloor.spec.ts +11 -8
- package/src/theme/theme.hydration.native.spec.tsx +51 -0
- package/src/theme/theme.native.tsx +59 -21
- package/src/theme/theme.palette.spec.tsx +76 -0
- package/src/theme/theme.preset.native.spec.tsx +73 -0
- package/src/theme/theme.provider.spec.ts +143 -1
- package/src/theme/theme.tsx +61 -25
- package/src/theme/tileSizes.ts +6 -7
- package/src/theme/transitionProps.native.ts +25 -2
- package/src/theme/transitionProps.spec.ts +34 -0
- package/src/theme/transitionProps.ts +26 -2
- package/src/theme/usePresetSwitch.native.spec.tsx +68 -0
- package/src/theme/usePresetSwitch.native.ts +32 -0
- package/src/theme/usePresetSwitch.spec.tsx +103 -0
- package/src/theme/usePresetSwitch.ts +54 -0
- package/src/theme/useResolvedKnobs.hydration.spec.tsx +2 -2
- package/src/theme/useResolvedKnobs.ts +23 -90
- package/src/theme/useResolvedKnobsBehavior.spec.tsx +25 -30
- package/src/theme/useSchemePreference.native.ts +33 -0
- package/src/theme/useSchemePreference.spec.tsx +51 -0
- package/src/theme/useSchemePreference.ts +32 -0
- package/src/theme/useSchemePreference.web.spec.tsx +32 -0
- package/src/theme/useTheme.native.spec.tsx +68 -0
- package/src/theme/useTheme.native.ts +49 -24
- package/src/theme/useTheme.ts +7 -4
- package/types/audit/constraintAudit.d.ts +6 -5
- package/types/audit/constraintAudit.d.ts.map +1 -1
- package/types/audit/designTokens.d.ts +13 -0
- package/types/audit/designTokens.d.ts.map +1 -0
- package/types/audit/themeMatrix.d.ts +10 -0
- package/types/audit/themeMatrix.d.ts.map +1 -1
- package/types/figma/figmaTokens.d.ts +11 -28
- package/types/figma/figmaTokens.d.ts.map +1 -1
- package/types/fonts/createFontLoader.d.ts.map +1 -1
- package/types/menuRow.d.ts +4 -1
- package/types/menuRow.d.ts.map +1 -1
- package/types/theme/FontKnobStyles.d.ts.map +1 -1
- package/types/theme/Preset.d.ts.map +1 -1
- package/types/theme/PresetContext.d.ts +4 -0
- package/types/theme/PresetContext.d.ts.map +1 -1
- package/types/theme/Surface.d.ts +57 -26
- package/types/theme/Surface.d.ts.map +1 -1
- package/types/theme/activateThemeConfig.d.ts +5 -3
- package/types/theme/activateThemeConfig.d.ts.map +1 -1
- package/types/theme/animations/css.d.ts +27 -34
- package/types/theme/animations/css.d.ts.map +1 -1
- package/types/theme/animations/reactNative.d.ts +196 -45
- package/types/theme/animations/reactNative.d.ts.map +1 -1
- package/types/theme/chartPalette.d.ts +11 -3
- package/types/theme/chartPalette.d.ts.map +1 -1
- package/types/theme/colorRules.d.ts +1 -1
- package/types/theme/cookies.d.ts +12 -1
- package/types/theme/cookies.d.ts.map +1 -1
- package/types/theme/createDefaultThemeConfig.d.ts +13 -13
- package/types/theme/createDefaultThemeConfig.d.ts.map +1 -1
- package/types/theme/createThemes.d.ts +58 -2
- package/types/theme/createThemes.d.ts.map +1 -1
- package/types/theme/defaults/accent.d.ts.map +1 -1
- package/types/theme/defaults/base.d.ts.map +1 -1
- package/types/theme/defaults/builderOptions.d.ts.map +1 -1
- package/types/theme/defaults/categoryFonts.d.ts +10 -0
- package/types/theme/defaults/categoryFonts.d.ts.map +1 -1
- package/types/theme/defaults/fonts.d.ts +330 -30
- package/types/theme/defaults/fonts.d.ts.map +1 -1
- package/types/theme/defaults/house.d.ts +8 -0
- package/types/theme/defaults/house.d.ts.map +1 -0
- 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 +3 -3
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
- package/types/theme/devtools/animationOptions.d.ts.map +1 -1
- package/types/theme/devtools/buildSnapshot.d.ts.map +1 -1
- package/types/theme/devtools/knobIcons.d.ts +0 -3
- package/types/theme/devtools/knobIcons.d.ts.map +1 -1
- package/types/theme/index.d.ts +11 -0
- package/types/theme/index.d.ts.map +1 -1
- package/types/theme/knobs.d.ts +8 -36
- package/types/theme/knobs.d.ts.map +1 -1
- package/types/theme/layoutTokens.d.ts +13 -1
- package/types/theme/layoutTokens.d.ts.map +1 -1
- package/types/theme/mergedTokens.d.ts +11 -0
- package/types/theme/mergedTokens.d.ts.map +1 -0
- package/types/theme/motion.d.ts +17 -0
- package/types/theme/motion.d.ts.map +1 -0
- package/types/theme/motionStops.d.ts +3 -0
- package/types/theme/motionStops.d.ts.map +1 -0
- package/types/theme/motionTiming.d.ts +36 -0
- package/types/theme/motionTiming.d.ts.map +1 -0
- package/types/theme/overlaySurfaces.d.ts +70 -0
- package/types/theme/overlaySurfaces.d.ts.map +1 -0
- package/types/theme/preset.types.d.ts +16 -0
- package/types/theme/preset.types.d.ts.map +1 -1
- package/types/theme/presetPreview.d.ts +14 -0
- package/types/theme/presetPreview.d.ts.map +1 -0
- package/types/theme/presetStore.d.ts +40 -0
- package/types/theme/presetStore.d.ts.map +1 -0
- package/types/theme/presetUserSync.d.ts +7 -0
- package/types/theme/presetUserSync.d.ts.map +1 -0
- package/types/theme/presets.d.ts +12 -4
- package/types/theme/presets.d.ts.map +1 -1
- package/types/theme/radiusClass.d.ts +5 -2
- package/types/theme/radiusClass.d.ts.map +1 -1
- package/types/theme/radiusClassTable.generated.d.ts +3 -3
- package/types/theme/readableColor.d.ts +11 -49
- package/types/theme/readableColor.d.ts.map +1 -1
- package/types/theme/recipeInputs.d.ts +39 -70
- package/types/theme/recipeInputs.d.ts.map +1 -1
- package/types/theme/recipes.d.ts +79 -18
- package/types/theme/recipes.d.ts.map +1 -1
- package/types/theme/resolveKnobs.d.ts +64 -32
- package/types/theme/resolveKnobs.d.ts.map +1 -1
- package/types/theme/scale/ScaleStyles.d.ts +18 -0
- package/types/theme/scale/ScaleStyles.d.ts.map +1 -0
- package/types/theme/scale/ScaleStyles.native.d.ts +10 -0
- package/types/theme/scale/ScaleStyles.native.d.ts.map +1 -0
- package/types/theme/scale/activeScale.d.ts +13 -0
- package/types/theme/scale/activeScale.d.ts.map +1 -0
- package/types/theme/scale/deriveRecipeInputs.d.ts +10 -0
- package/types/theme/scale/deriveRecipeInputs.d.ts.map +1 -0
- package/types/theme/scale/generateScale.d.ts +38 -0
- package/types/theme/scale/generateScale.d.ts.map +1 -0
- package/types/theme/scale/index.d.ts +17 -0
- package/types/theme/scale/index.d.ts.map +1 -0
- package/types/theme/scale/installScaleConfig.d.ts +4 -0
- package/types/theme/scale/installScaleConfig.d.ts.map +1 -0
- package/types/theme/scale/installScaleConfig.native.d.ts +13 -0
- package/types/theme/scale/installScaleConfig.native.d.ts.map +1 -0
- package/types/theme/scale/scaleAttribute.d.ts +14 -0
- package/types/theme/scale/scaleAttribute.d.ts.map +1 -0
- package/types/theme/scale/scaleCss.d.ts +25 -0
- package/types/theme/scale/scaleCss.d.ts.map +1 -0
- package/types/theme/scale/scaleFonts.d.ts +16 -0
- package/types/theme/scale/scaleFonts.d.ts.map +1 -0
- package/types/theme/scale/scaleInputs.d.ts +66 -0
- package/types/theme/scale/scaleInputs.d.ts.map +1 -0
- package/types/theme/scale/themeScales.d.ts +52 -0
- package/types/theme/scale/themeScales.d.ts.map +1 -0
- package/types/theme/scale/useActiveScale.d.ts +31 -0
- package/types/theme/scale/useActiveScale.d.ts.map +1 -0
- package/types/theme/schemeStore.d.ts +6 -0
- package/types/theme/schemeStore.d.ts.map +1 -0
- package/types/theme/shared.d.ts +18 -0
- package/types/theme/shared.d.ts.map +1 -1
- package/types/theme/sizeRecipes.d.ts +20 -15
- package/types/theme/sizeRecipes.d.ts.map +1 -1
- package/types/theme/theme.d.ts +2 -1
- package/types/theme/theme.d.ts.map +1 -1
- package/types/theme/theme.native.d.ts +10 -1
- package/types/theme/theme.native.d.ts.map +1 -1
- package/types/theme/tileSizes.d.ts +6 -7
- package/types/theme/tileSizes.d.ts.map +1 -1
- package/types/theme/transitionProps.d.ts +3 -1
- package/types/theme/transitionProps.d.ts.map +1 -1
- package/types/theme/transitionProps.native.d.ts +3 -1
- package/types/theme/transitionProps.native.d.ts.map +1 -1
- package/types/theme/usePresetSwitch.d.ts +10 -0
- package/types/theme/usePresetSwitch.d.ts.map +1 -0
- package/types/theme/usePresetSwitch.native.d.ts +10 -0
- package/types/theme/usePresetSwitch.native.d.ts.map +1 -0
- package/types/theme/useResolvedKnobs.d.ts +13 -13
- package/types/theme/useResolvedKnobs.d.ts.map +1 -1
- package/types/theme/useSchemePreference.d.ts +8 -0
- package/types/theme/useSchemePreference.d.ts.map +1 -0
- package/types/theme/useSchemePreference.native.d.ts +8 -0
- package/types/theme/useSchemePreference.native.d.ts.map +1 -0
- package/types/theme/useTheme.d.ts +4 -2
- package/types/theme/useTheme.d.ts.map +1 -1
- package/types/theme/useTheme.native.d.ts +10 -1
- package/types/theme/useTheme.native.d.ts.map +1 -1
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import * as Colors from "@tamagui/colors";
|
|
2
|
-
import { themes as stockThemes } from "@tamagui/themes";
|
|
3
2
|
import { describe, expect, it } from "vitest";
|
|
4
3
|
import { accentIdentityCandidates, resolveChartPalette } from "./chartPalette";
|
|
5
4
|
import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
|
|
6
|
-
import { tintHueNames } from "./createThemes";
|
|
5
|
+
import { createThemesBuilder, tintHueNames } from "./createThemes";
|
|
6
|
+
import { defaultAccentTheme } from "./defaults/accent";
|
|
7
|
+
import { defaultBaseTheme } from "./defaults/base";
|
|
8
|
+
import { defaultBuilderOptions } from "./defaults/builderOptions";
|
|
9
|
+
|
|
10
|
+
const builtThemes = createThemesBuilder(
|
|
11
|
+
defaultBaseTheme,
|
|
12
|
+
defaultAccentTheme,
|
|
13
|
+
defaultBuilderOptions,
|
|
14
|
+
).themes() as unknown as Record<string, Record<string, string>>;
|
|
7
15
|
|
|
8
16
|
// The default accent solid (accentBackground) from defaults/accent.ts.
|
|
9
17
|
const lightAccent = "hsla(250, 50%, 54%, 1)";
|
|
@@ -57,30 +65,26 @@ function adjacentDistinguishable(a: string, b: string): boolean {
|
|
|
57
65
|
describe("resolveChartPalette", () => {
|
|
58
66
|
it("floors every categorical mark across the registered tint identities", () => {
|
|
59
67
|
for (const scheme of ["light", "dark"] as const) {
|
|
60
|
-
const
|
|
61
|
-
.filter(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
name.split("_").length === 3,
|
|
67
|
-
)
|
|
68
|
-
.map(([, theme]) => theme.background);
|
|
69
|
-
expect(cardSurfaces.length).toBeGreaterThan(0);
|
|
70
|
-
const surfaces = [
|
|
71
|
-
...cardSurfaces,
|
|
72
|
-
...(scheme === "light"
|
|
68
|
+
const builtSurfaces = [scheme, ...[...tintHueNames].map((hue) => `${scheme}_${hue}`)]
|
|
69
|
+
.filter((name) => builtThemes[name])
|
|
70
|
+
.flatMap((name) => [builtThemes[name].background, builtThemes[`${name}_Card`].background]);
|
|
71
|
+
expect(builtSurfaces.length).toBeGreaterThan(2);
|
|
72
|
+
const references =
|
|
73
|
+
scheme === "light"
|
|
73
74
|
? ["#ffffff", Colors.gray.gray2, Colors.mauve.mauve2]
|
|
74
|
-
: [Colors.grayDark.gray2, Colors.mauveDark.mauve2]
|
|
75
|
-
];
|
|
75
|
+
: [Colors.grayDark.gray2, Colors.mauveDark.mauve2];
|
|
76
|
+
const surfaces = [...builtSurfaces, ...references];
|
|
76
77
|
for (const hue of tintHueNames) {
|
|
77
78
|
const ramp = (Colors as unknown as Record<string, Record<string, string>>)[
|
|
78
79
|
scheme === "dark" ? `${hue}Dark` : hue
|
|
79
80
|
];
|
|
81
|
+
const tint = builtThemes[`${scheme}_${hue}`];
|
|
82
|
+
const tintSurfaces = tint ? [tint.color2, tint.color3] : [];
|
|
80
83
|
const palette = resolveChartPalette({
|
|
81
84
|
scheme,
|
|
82
85
|
identitySolid: ramp[`${hue}9`],
|
|
83
86
|
identityCandidates: [ramp[`${hue}10`], ramp[`${hue}11`]],
|
|
87
|
+
surfaces: tintSurfaces,
|
|
84
88
|
});
|
|
85
89
|
for (let i = 0; i < palette.categorical.length; i++) {
|
|
86
90
|
expect(
|
|
@@ -91,14 +95,19 @@ describe("resolveChartPalette", () => {
|
|
|
91
95
|
`${scheme}/${hue}: adjacent series ${i}`,
|
|
92
96
|
).toBe(true);
|
|
93
97
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
const [identity, ...cycle] = palette.categorical;
|
|
99
|
+
const marks = [
|
|
100
|
+
...[...references, ...tintSurfaces].map((surface) => [identity, surface]),
|
|
101
|
+
...[...cycle, ...Object.values(palette.semantic)].flatMap((paint) =>
|
|
102
|
+
surfaces.map((surface) => [paint, surface]),
|
|
103
|
+
),
|
|
104
|
+
];
|
|
105
|
+
for (const [paint, surface] of marks) {
|
|
106
|
+
const ratio = contrastRatio(
|
|
107
|
+
relativeLuminance(normalizeToHex(paint)!),
|
|
108
|
+
relativeLuminance(normalizeToHex(surface)!),
|
|
109
|
+
);
|
|
110
|
+
expect(ratio, `${scheme}/${hue}: ${paint} on ${surface}`).toBeGreaterThanOrEqual(3);
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
113
|
}
|
|
@@ -128,7 +137,7 @@ describe("resolveChartPalette", () => {
|
|
|
128
137
|
Colors.green.green10,
|
|
129
138
|
Colors.amber.amber11,
|
|
130
139
|
Colors.pink.pink9,
|
|
131
|
-
Colors.teal.
|
|
140
|
+
Colors.teal.teal11,
|
|
132
141
|
Colors.red.red9,
|
|
133
142
|
]);
|
|
134
143
|
const grass = resolveChartPalette({
|
|
@@ -144,7 +153,7 @@ describe("resolveChartPalette", () => {
|
|
|
144
153
|
Colors.orange.orange11,
|
|
145
154
|
Colors.amber.amber11,
|
|
146
155
|
Colors.pink.pink9,
|
|
147
|
-
Colors.teal.
|
|
156
|
+
Colors.teal.teal11,
|
|
148
157
|
Colors.violet.violet9,
|
|
149
158
|
Colors.red.red9,
|
|
150
159
|
].sort(),
|
|
@@ -175,6 +184,25 @@ describe("resolveChartPalette", () => {
|
|
|
175
184
|
).toBe(Colors.violet.violet9);
|
|
176
185
|
});
|
|
177
186
|
|
|
187
|
+
it("steps the identity down when it misses the tint's Card fill", () => {
|
|
188
|
+
const tint = builtThemes.light_gray;
|
|
189
|
+
const card = builtThemes.light_gray_Card.background;
|
|
190
|
+
expect(card).toBe(tint.color3);
|
|
191
|
+
const onCard = resolveChartPalette({
|
|
192
|
+
scheme: "light",
|
|
193
|
+
identitySolid: tint.color9,
|
|
194
|
+
identityCandidates: [tint.color10, tint.color11],
|
|
195
|
+
surfaces: [tint.color2, tint.color3],
|
|
196
|
+
});
|
|
197
|
+
expect(onCard.single).toBe(tint.color10);
|
|
198
|
+
expect(
|
|
199
|
+
contrastRatio(
|
|
200
|
+
relativeLuminance(normalizeToHex(onCard.single)!),
|
|
201
|
+
relativeLuminance(normalizeToHex(card)!),
|
|
202
|
+
),
|
|
203
|
+
).toBeGreaterThanOrEqual(3);
|
|
204
|
+
});
|
|
205
|
+
|
|
178
206
|
it("single-series takes the theme identity solid", () => {
|
|
179
207
|
expect(resolveChartPalette({ scheme: "light", identitySolid: lightAccent }).single).toBe(
|
|
180
208
|
lightAccent,
|
|
@@ -295,6 +323,7 @@ describe("chart identity under the held dark accent (MPO-448)", () => {
|
|
|
295
323
|
...accentIdentityCandidates(identitySolid, darkAccentSolids),
|
|
296
324
|
"hsla(270, 6%, 54%, 1)",
|
|
297
325
|
],
|
|
326
|
+
surfaces: [builtThemes.dark_purple.color2, builtThemes.dark_purple.color3],
|
|
298
327
|
});
|
|
299
328
|
expect(palette.single).toBe(darkAccentSolids[9]);
|
|
300
329
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as Colors from "@tamagui/colors";
|
|
2
|
-
import { themes as stockThemes } from "@tamagui/themes";
|
|
3
2
|
import { useMemo } from "react";
|
|
4
3
|
import { useTheme as useTamaguiTheme, useThemeName } from "tamagui";
|
|
5
4
|
import { contrastRatio, minContrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
|
|
@@ -41,20 +40,29 @@ export interface ChartPalette {
|
|
|
41
40
|
export interface ResolveChartPaletteInput {
|
|
42
41
|
scheme: "light" | "dark";
|
|
43
42
|
/**
|
|
44
|
-
* Resolved theme-identity solid —
|
|
43
|
+
* Resolved theme-identity solid — the accent solid in base themes, the
|
|
45
44
|
* re-ramped `$color9` under a tint. Tries the ordered candidates, then
|
|
46
45
|
* falls back to violet when missing, unparseable or below the contrast floor.
|
|
47
46
|
*/
|
|
48
47
|
identitySolid?: string;
|
|
49
48
|
/** Ordered alternatives when the identity cannot meet the mark contrast floor. */
|
|
50
49
|
identityCandidates?: readonly (string | undefined)[];
|
|
50
|
+
/**
|
|
51
|
+
* The active colour theme's page and Card fills (`$color2` and `$color3`
|
|
52
|
+
* under v5's templates, the same in every sub-theme of that colour). A
|
|
53
|
+
* tint's surfaces are deeper than the reference whites, so the identity
|
|
54
|
+
* must clear them too, and one palette serves a chart on the page or in a
|
|
55
|
+
* Card.
|
|
56
|
+
*/
|
|
57
|
+
surfaces?: readonly (string | undefined)[];
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
/**
|
|
54
61
|
* Fixed categorical hue cycle: Radix solids ordered so neighbouring
|
|
55
62
|
* entries sit far apart on the hue wheel (min adjacent distance ~78°).
|
|
56
63
|
* Light blue, orange, green, amber and teal deepen within their hue to clear
|
|
57
|
-
* 3:1 on
|
|
64
|
+
* 3:1 on every built page and Card surface. Dark violet deepens for tinted
|
|
65
|
+
* card backgrounds.
|
|
58
66
|
*/
|
|
59
67
|
const categoricalHueCycle = [
|
|
60
68
|
{ family: "blue", light: Colors.blue.blue10, dark: Colors.blueDark.blue9 },
|
|
@@ -62,37 +70,31 @@ const categoricalHueCycle = [
|
|
|
62
70
|
{ family: "green", light: Colors.green.green10, dark: Colors.greenDark.green9 },
|
|
63
71
|
{ family: "amber", light: Colors.amber.amber11, dark: Colors.amberDark.amber9 },
|
|
64
72
|
{ family: "pink", light: Colors.pink.pink9, dark: Colors.pinkDark.pink9 },
|
|
65
|
-
{ family: "teal", light: Colors.teal.
|
|
73
|
+
{ family: "teal", light: Colors.teal.teal11, dark: Colors.tealDark.teal9 },
|
|
66
74
|
{ family: "violet", light: Colors.violet.violet9, dark: Colors.violetDark.violet10 },
|
|
67
75
|
{ family: "red", light: Colors.red.red9, dark: Colors.redDark.red9 },
|
|
68
76
|
] as const;
|
|
69
77
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return [...tintHueNames].flatMap((hue) => {
|
|
73
|
-
const color = themes[`${scheme}_${hue}_Card`]?.background;
|
|
74
|
-
return color ? [color] : [];
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Use the shipped tinted Card backgrounds, which are deeper than their page
|
|
79
|
-
// colors. Missing theme names are not invented surfaces. The rendered audit
|
|
80
|
-
// still owns actual custom consumer backgrounds and elevation variants.
|
|
78
|
+
// The rendered audit still owns custom consumer backgrounds and elevation
|
|
79
|
+
// variants.
|
|
81
80
|
const chartReferenceSurfaces = {
|
|
82
|
-
light: [
|
|
83
|
-
|
|
84
|
-
Colors.gray.gray2,
|
|
85
|
-
Colors.mauve.mauve2,
|
|
86
|
-
...configuredTintCardSurfaces("light"),
|
|
87
|
-
],
|
|
88
|
-
dark: [Colors.grayDark.gray2, Colors.mauveDark.mauve2, ...configuredTintCardSurfaces("dark")],
|
|
81
|
+
light: ["#ffffff", Colors.gray.gray2, Colors.mauve.mauve2],
|
|
82
|
+
dark: [Colors.grayDark.gray2, Colors.mauveDark.mauve2],
|
|
89
83
|
} as const;
|
|
90
84
|
|
|
91
|
-
function readableIdentity(
|
|
85
|
+
function readableIdentity(
|
|
86
|
+
color: string | undefined,
|
|
87
|
+
scheme: "light" | "dark",
|
|
88
|
+
themeSurfaces: readonly (string | undefined)[],
|
|
89
|
+
): color is string {
|
|
92
90
|
const hex = color ? normalizeToHex(color) : null;
|
|
93
91
|
if (!hex) return false;
|
|
94
92
|
const luminance = relativeLuminance(hex);
|
|
95
|
-
|
|
93
|
+
const surfaces = [
|
|
94
|
+
...chartReferenceSurfaces[scheme],
|
|
95
|
+
...themeSurfaces.filter((surface): surface is string => !!surface),
|
|
96
|
+
];
|
|
97
|
+
return surfaces.every((surface) => {
|
|
96
98
|
const backdrop = normalizeToHex(surface);
|
|
97
99
|
return !!backdrop && contrastRatio(luminance, relativeLuminance(backdrop)) >= minContrastRatio;
|
|
98
100
|
});
|
|
@@ -204,7 +206,7 @@ export function resolveChartPalette(input: ResolveChartPaletteInput): ChartPalet
|
|
|
204
206
|
const fallbackIdentity = scheme === "dark" ? Colors.violetDark.violet10 : Colors.violet.violet9;
|
|
205
207
|
const single =
|
|
206
208
|
[input.identitySolid, ...(input.identityCandidates ?? [])].find((candidate) =>
|
|
207
|
-
readableIdentity(candidate, scheme),
|
|
209
|
+
readableIdentity(candidate, scheme, input.surfaces ?? []),
|
|
208
210
|
) ?? fallbackIdentity;
|
|
209
211
|
const identityHsl = colorToHsl(single) as Hsl;
|
|
210
212
|
|
|
@@ -229,7 +231,7 @@ export function resolveChartPalette(input: ResolveChartPaletteInput): ChartPalet
|
|
|
229
231
|
/**
|
|
230
232
|
* The accent solids ordered nearest-first in luminance to `identity`, so a
|
|
231
233
|
* chart identity that misses the 3:1 mark floor on the reference cards falls
|
|
232
|
-
* to the next accent step before a neutral one.
|
|
234
|
+
* to the next accent step before a neutral one. The accent solid is held
|
|
233
235
|
* to the text floor (MPO-448), which in dark lands it one step deeper than
|
|
234
236
|
* the marks need; the chart keeps the accent channel on the step beside it.
|
|
235
237
|
*/
|
|
@@ -256,7 +258,7 @@ export function accentIdentityCandidates(
|
|
|
256
258
|
*
|
|
257
259
|
* Reads scheme and tint from the resolved theme name and the identity solid
|
|
258
260
|
* from the active theme (`$color9` under a tint — the re-ramped neutral
|
|
259
|
-
* solid — otherwise
|
|
261
|
+
* solid — otherwise the accent solid). Chart chrome MUST take its default
|
|
260
262
|
* series colors from this hook; explicit `color`/`colors` props on chart
|
|
261
263
|
* data are user values and pass through untouched.
|
|
262
264
|
*/
|
|
@@ -268,6 +270,8 @@ export function useChartPalette(): ChartPalette {
|
|
|
268
270
|
const isTint = nameParts.length > 1 && tintHueNames.has(nameParts[1]);
|
|
269
271
|
const color9 = theme.color9?.val;
|
|
270
272
|
const accent = theme.accentBackground?.val;
|
|
273
|
+
const page = theme.color2?.val;
|
|
274
|
+
const card = theme.color3?.val;
|
|
271
275
|
const identitySolid = isTint ? (color9 ?? accent) : (accent ?? color9);
|
|
272
276
|
const color10 = theme.color10?.val;
|
|
273
277
|
const color11 = theme.color11?.val;
|
|
@@ -283,8 +287,9 @@ export function useChartPalette(): ChartPalette {
|
|
|
283
287
|
color10,
|
|
284
288
|
color11,
|
|
285
289
|
],
|
|
290
|
+
surfaces: [page, card],
|
|
286
291
|
}),
|
|
287
292
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
288
|
-
[scheme, identitySolid, isTint, accentKey, color10, color11],
|
|
293
|
+
[scheme, identitySolid, isTint, accentKey, color10, color11, page, card],
|
|
289
294
|
);
|
|
290
295
|
}
|
|
@@ -382,34 +382,21 @@ describe("contrast floor on built themes (DG-A11Y-01 / DG-COL-01)", () => {
|
|
|
382
382
|
resolveThemeContrast(themes, { theme, foreground: "color", background: "background" }),
|
|
383
383
|
),
|
|
384
384
|
);
|
|
385
|
-
expect(reports.
|
|
386
|
-
|
|
387
|
-
expect.closeTo(7.61, 2),
|
|
388
|
-
]);
|
|
385
|
+
expect(reports).toHaveLength(2);
|
|
386
|
+
expect(reports.every((report) => report.ratio >= aaTextContrastRatio)).toBe(true);
|
|
389
387
|
});
|
|
390
388
|
|
|
391
|
-
it("a colour pair below the floor
|
|
392
|
-
// MPO-48 measured the dark accent toast action pill on the page ground.
|
|
393
|
-
// Built: dark_accent.backgroundHover is #46349d; dark.background is #161519.
|
|
389
|
+
it("a colour pair below the floor fails", () => {
|
|
394
390
|
expect(() =>
|
|
395
391
|
assertContrast({
|
|
396
|
-
foreground: themes.
|
|
397
|
-
background: themes.
|
|
398
|
-
label: "
|
|
392
|
+
foreground: themes.light.color3,
|
|
393
|
+
background: themes.light.background,
|
|
394
|
+
label: "low-contrast ink",
|
|
399
395
|
}),
|
|
400
|
-
).toThrow(/
|
|
401
|
-
|
|
402
|
-
// MPO-40's warning-on-its-own-color1 arm used to sit here as the second
|
|
403
|
-
// negative control at 4.44:1. The `intentInkFloor` step closed it — the
|
|
404
|
-
// same pair now measures 4.71:1 — so it can no longer prove the trip-wire
|
|
405
|
-
// bites, and it moved to the passing assertion below rather than being
|
|
406
|
-
// deleted. MPO-48 above is still a live miss and still carries this test.
|
|
396
|
+
).toThrow(/low-contrast ink/);
|
|
407
397
|
});
|
|
408
398
|
|
|
409
|
-
it("
|
|
410
|
-
// The pair that used to be the negative control. The palette step was
|
|
411
|
-
// computed against the BASE color2 (#f9f9fa, the strictest ground), and
|
|
412
|
-
// this checks the intent's own surface came along with it. Was 4.44.
|
|
399
|
+
it("warning ink clears the floor on its own color1 too", () => {
|
|
413
400
|
const [report] = assertContrast(
|
|
414
401
|
resolveThemeContrast(themes, {
|
|
415
402
|
theme: "light_warning",
|
|
@@ -417,47 +404,42 @@ describe("contrast floor on built themes (DG-A11Y-01 / DG-COL-01)", () => {
|
|
|
417
404
|
background: "color1",
|
|
418
405
|
}),
|
|
419
406
|
);
|
|
420
|
-
expect(report.ratio).
|
|
407
|
+
expect(report.ratio).toBeGreaterThanOrEqual(aaTextContrastRatio);
|
|
421
408
|
});
|
|
422
409
|
|
|
423
|
-
it("
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
);
|
|
435
|
-
expect(reports[0].ratio).toBeCloseTo(3.96, 2);
|
|
436
|
-
expect(reports[0].pass).toBe(true);
|
|
410
|
+
it("the inverted-base accent text clears AA on its background", () => {
|
|
411
|
+
for (const scheme of ["light", "dark"]) {
|
|
412
|
+
const [report] = assertContrast(
|
|
413
|
+
resolveThemeContrast(themes, {
|
|
414
|
+
theme: `${scheme}_accent`,
|
|
415
|
+
foreground: "color",
|
|
416
|
+
background: "background",
|
|
417
|
+
}),
|
|
418
|
+
);
|
|
419
|
+
expect(report.ratio).toBeGreaterThanOrEqual(aaTextContrastRatio);
|
|
420
|
+
}
|
|
437
421
|
});
|
|
438
422
|
|
|
439
|
-
it("
|
|
440
|
-
|
|
441
|
-
|
|
423
|
+
it("a pale accent page fails the contrast guard", () => {
|
|
424
|
+
const palette = [...defaultAccentTheme.lightPalette];
|
|
425
|
+
palette[1] = defaultAccentTheme.lightPalette[11];
|
|
442
426
|
const broken = createThemesBuilder(
|
|
443
427
|
defaultBaseTheme,
|
|
444
428
|
{
|
|
445
429
|
...defaultAccentTheme,
|
|
446
|
-
lightPalette:
|
|
430
|
+
lightPalette: palette,
|
|
447
431
|
},
|
|
448
432
|
defaultBuilderOptions,
|
|
449
|
-
).themes()
|
|
450
|
-
|
|
433
|
+
).themes();
|
|
451
434
|
expect(() =>
|
|
452
435
|
assertContrast(
|
|
453
436
|
resolveThemeContrast(broken, {
|
|
454
437
|
theme: "light_accent",
|
|
455
438
|
foreground: "color",
|
|
456
439
|
background: "background",
|
|
457
|
-
floor: minContrastRatio,
|
|
458
440
|
}),
|
|
459
441
|
),
|
|
460
|
-
).toThrow(/light_accent: color on background
|
|
442
|
+
).toThrow(/light_accent: color on background/);
|
|
461
443
|
});
|
|
462
444
|
});
|
|
463
445
|
|
|
@@ -486,23 +468,6 @@ describe("intent color11 on the base page surfaces (MPO-40)", () => {
|
|
|
486
468
|
),
|
|
487
469
|
);
|
|
488
470
|
|
|
489
|
-
/**
|
|
490
|
-
* MPO-40 measured the outlined intent label on the gallery harness ground
|
|
491
|
-
* and recorded warning 4.34:1 and success 4.48:1. That ground is the BASE
|
|
492
|
-
* theme's `color2` (#f9f9fa) — NOT `background`/`color1`, which are both
|
|
493
|
-
* #ffffff and where the same two pairs cleared the floor even unfixed. The
|
|
494
|
-
* miss was therefore surface-specific: an outlined warning or success Button
|
|
495
|
-
* only went unreadable once it sat on a color2 surface (a card, a striped
|
|
496
|
-
* row, the gallery ground). Measuring against the plain page is exactly how
|
|
497
|
-
* a check reports a pass on a control that is failing.
|
|
498
|
-
*
|
|
499
|
-
* Both are CLOSED by the `intentInkFloor` step in `createThemes.ts`, which
|
|
500
|
-
* darkens light `yellow11` and `green11` to the nearest hue-holding value
|
|
501
|
-
* that clears the floor on `color2`. The exemption table that used to stand
|
|
502
|
-
* here retired itself exactly as designed: the step moved, its pinned ratios
|
|
503
|
-
* stopped matching, and the entries had to come out. There is now no
|
|
504
|
-
* exemption to rot.
|
|
505
|
-
*/
|
|
506
471
|
it("sweeps every intent x base surface x scheme — 18 pairs, none dropped", () => {
|
|
507
472
|
expect(intentInkPairs).toHaveLength(18);
|
|
508
473
|
});
|
|
@@ -516,9 +481,6 @@ describe("intent color11 on the base page surfaces (MPO-40)", () => {
|
|
|
516
481
|
});
|
|
517
482
|
|
|
518
483
|
it("pins the two MPO-40 stops on color2, the ground they used to fail", () => {
|
|
519
|
-
// The regression guard proper: color2 is the STRICTEST base surface, so a
|
|
520
|
-
// future palette move that quietly re-lightens either stop lands here
|
|
521
|
-
// first. Was 4.34 / 4.48 against a 4.5 floor.
|
|
522
484
|
const reports = assertContrast(
|
|
523
485
|
(["warning", "success"] as const).map((intent) => ({
|
|
524
486
|
foreground: themes[`light_${intent}`].color11,
|
|
@@ -527,14 +489,12 @@ describe("intent color11 on the base page surfaces (MPO-40)", () => {
|
|
|
527
489
|
})),
|
|
528
490
|
);
|
|
529
491
|
expect(reports.map((report) => report.ratio)).toEqual([
|
|
530
|
-
expect.closeTo(4.
|
|
531
|
-
expect.closeTo(4.
|
|
492
|
+
expect.closeTo(4.54, 2),
|
|
493
|
+
expect.closeTo(4.53, 2),
|
|
532
494
|
]);
|
|
533
495
|
});
|
|
534
496
|
|
|
535
497
|
it("the same warning/success ink still clears the floor on background and color1", () => {
|
|
536
|
-
// Locks the surface-specificity so a future change cannot be signed off by
|
|
537
|
-
// re-measuring against the white page. Was 4.57 / 4.72.
|
|
538
498
|
const reports = assertContrast(
|
|
539
499
|
(["warning", "success"] as const).flatMap((intent) =>
|
|
540
500
|
(["background", "color1"] as const).map((surface) => ({
|
|
@@ -545,10 +505,10 @@ describe("intent color11 on the base page surfaces (MPO-40)", () => {
|
|
|
545
505
|
),
|
|
546
506
|
);
|
|
547
507
|
expect(reports.map((report) => report.ratio)).toEqual([
|
|
548
|
-
expect.closeTo(4.
|
|
549
|
-
expect.closeTo(4.
|
|
550
|
-
expect.closeTo(4.
|
|
551
|
-
expect.closeTo(4.
|
|
508
|
+
expect.closeTo(4.54, 2),
|
|
509
|
+
expect.closeTo(4.87, 2),
|
|
510
|
+
expect.closeTo(4.53, 2),
|
|
511
|
+
expect.closeTo(4.85, 2),
|
|
552
512
|
]);
|
|
553
513
|
});
|
|
554
514
|
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it, beforeEach } from "vitest";
|
|
2
2
|
import { defaultKnobs, type Knobs } from "./knobs";
|
|
3
3
|
import {
|
|
4
|
+
sanitizeOverrides,
|
|
4
5
|
splitCookieValue,
|
|
5
6
|
joinSplitCookieValues,
|
|
7
|
+
isKnobKey,
|
|
6
8
|
readOverridesCookie,
|
|
7
9
|
readPresetCookie,
|
|
8
10
|
writePresetCookie,
|
|
@@ -78,7 +80,7 @@ describe("readOverridesCookie", () => {
|
|
|
78
80
|
});
|
|
79
81
|
|
|
80
82
|
it("reads and joins multiple chunks", () => {
|
|
81
|
-
const overrides: Partial<Knobs> = { fillStyle: "outlined",
|
|
83
|
+
const overrides: Partial<Knobs> = { fillStyle: "outlined", elevation: "large" };
|
|
82
84
|
const json = JSON.stringify(overrides);
|
|
83
85
|
const mid = Math.floor(json.length / 2);
|
|
84
86
|
const cookies: Record<string, string> = {
|
|
@@ -110,12 +112,12 @@ describe("readOverridesCookie", () => {
|
|
|
110
112
|
// JSON.parse threw, and overrides silently resolved to {} app-wide.
|
|
111
113
|
describe("readOverridesCookie with universal-cookie auto-parsed values", () => {
|
|
112
114
|
it("accepts an already-parsed object cookie value (universal-cookie shape)", () => {
|
|
113
|
-
const overrides: Partial<Knobs> = { borderRadius: "none",
|
|
115
|
+
const overrides: Partial<Knobs> = { borderRadius: "none", textAccent: "low" };
|
|
114
116
|
expect(readOverridesCookie({ "mp.ov.0": overrides })).toEqual(overrides);
|
|
115
117
|
});
|
|
116
118
|
|
|
117
119
|
it("accepts a raw JSON string cookie value (document.cookie / SSR shape)", () => {
|
|
118
|
-
const overrides: Partial<Knobs> = { borderRadius: "none",
|
|
120
|
+
const overrides: Partial<Knobs> = { borderRadius: "none", textAccent: "low" };
|
|
119
121
|
expect(readOverridesCookie({ "mp.ov.0": JSON.stringify(overrides) })).toEqual(overrides);
|
|
120
122
|
});
|
|
121
123
|
|
|
@@ -125,7 +127,7 @@ describe("readOverridesCookie with universal-cookie auto-parsed values", () => {
|
|
|
125
127
|
});
|
|
126
128
|
|
|
127
129
|
it("round-trips writeOverridesCookie output through an auto-parsing jar", () => {
|
|
128
|
-
const overrides: Partial<Knobs> = {
|
|
130
|
+
const overrides: Partial<Knobs> = { elevation: "none", fillStyle: "outlined" };
|
|
129
131
|
const { toSet } = writeOverridesCookie(overrides);
|
|
130
132
|
// Simulate universal-cookie readCookie: JSON.parse succeeds on the
|
|
131
133
|
// single-chunk value, so the jar hands back an object.
|
|
@@ -135,7 +137,7 @@ describe("readOverridesCookie with universal-cookie auto-parsed values", () => {
|
|
|
135
137
|
|
|
136
138
|
it("round-trips oversized multi-chunk payloads through an auto-parsing jar", () => {
|
|
137
139
|
const bigValue = "x".repeat(8000);
|
|
138
|
-
const overrides = {
|
|
140
|
+
const overrides = { headingFont: bigValue } as unknown as Partial<Knobs>;
|
|
139
141
|
const { toSet } = writeOverridesCookie(overrides);
|
|
140
142
|
expect(toSet.length).toBeGreaterThan(1);
|
|
141
143
|
// universal-cookie leaves each chunk a string: a JSON fragment throws in
|
|
@@ -239,9 +241,9 @@ describe("computeOverrides", () => {
|
|
|
239
241
|
});
|
|
240
242
|
|
|
241
243
|
it("returns only changed keys", () => {
|
|
242
|
-
const current: Knobs = { ...defaultKnobs, fillStyle: "outlined",
|
|
244
|
+
const current: Knobs = { ...defaultKnobs, fillStyle: "outlined", elevation: "large" };
|
|
243
245
|
const overrides = computeOverrides(defaultKnobs, current);
|
|
244
|
-
expect(overrides).toEqual({ fillStyle: "outlined",
|
|
246
|
+
expect(overrides).toEqual({ fillStyle: "outlined", elevation: "large" });
|
|
245
247
|
});
|
|
246
248
|
|
|
247
249
|
it("does not include unchanged keys", () => {
|
|
@@ -250,17 +252,22 @@ describe("computeOverrides", () => {
|
|
|
250
252
|
expect(Object.keys(overrides)).toEqual(["fillStyle"]);
|
|
251
253
|
});
|
|
252
254
|
|
|
253
|
-
it("
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
255
|
+
it("drops a stale size / space / density override a cookie still carries", () => {
|
|
256
|
+
const stale = JSON.stringify({
|
|
257
|
+
size: "large",
|
|
258
|
+
space: "small",
|
|
259
|
+
density: "compact",
|
|
260
|
+
fillStyle: "outlined",
|
|
261
|
+
hover: { space: "large", elevation: "medium" },
|
|
262
|
+
});
|
|
263
|
+
expect(readOverridesCookie({ "mp.ov.0": stale })).toEqual({
|
|
264
|
+
fillStyle: "outlined",
|
|
265
|
+
hover: { elevation: "medium" },
|
|
266
|
+
});
|
|
267
|
+
expect(isKnobKey("density")).toBe(false);
|
|
268
|
+
expect(isKnobKey("space")).toBe(false);
|
|
269
|
+
expect(isKnobKey("size")).toBe(false);
|
|
270
|
+
expect(isKnobKey("borderRadius")).toBe(true);
|
|
264
271
|
});
|
|
265
272
|
|
|
266
273
|
it("persists house-decision overrides via mp.ov.* pattern", () => {
|
|
@@ -299,3 +306,27 @@ describe("getCookieWatchList", () => {
|
|
|
299
306
|
expect(getCookieWatchList()).toHaveLength(11);
|
|
300
307
|
});
|
|
301
308
|
});
|
|
309
|
+
|
|
310
|
+
describe("persisted animation knobs", () => {
|
|
311
|
+
it.each([
|
|
312
|
+
["tooltip", "slow"],
|
|
313
|
+
["snappy", "quickest"],
|
|
314
|
+
["gentle", "slow"],
|
|
315
|
+
["quick", "quick"],
|
|
316
|
+
["none", "none"],
|
|
317
|
+
["superLazy", "superLazy"],
|
|
318
|
+
])("normalizes %s to %s", (stored, expected) => {
|
|
319
|
+
expect(sanitizeOverrides({ animation: stored })).toEqual({ animation: expected });
|
|
320
|
+
expect(readOverridesCookie({ "mp.ov.0": JSON.stringify({ animation: stored }) })).toEqual({
|
|
321
|
+
animation: expected,
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
it.each(["100ms", "ease", "unknown", 100, null, { hover: "quick" }])(
|
|
325
|
+
"drops unknown animation %s",
|
|
326
|
+
(animation) => {
|
|
327
|
+
expect(sanitizeOverrides({ animation, borderRadius: "small" })).toEqual({
|
|
328
|
+
borderRadius: "small",
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
);
|
|
332
|
+
});
|
package/src/theme/cookies.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { getCookie, setCookie, deleteCookie } from "@multiplatform.one/platform";
|
|
14
|
-
import type
|
|
14
|
+
import { type Knobs, type StateKnobs, defaultKnobs, normalizeAnimationKnob } from "./knobs";
|
|
15
15
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
// Cookie key constants
|
|
@@ -69,25 +69,54 @@ function chunkKey(index: number): string {
|
|
|
69
69
|
// Cookie read helpers (work with raw cookie objects from react-cookie)
|
|
70
70
|
// ---------------------------------------------------------------------------
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The knob names an overrides payload may carry; anything else is dropped,
|
|
74
|
+
* including the retired `size` / `space` / `density` a stale cookie holds.
|
|
75
|
+
*/
|
|
76
|
+
const knobKeys = new Set<string>(Object.keys(defaultKnobs));
|
|
77
|
+
|
|
78
|
+
/** True when `key` names a live knob (stale and foreign names are not). */
|
|
79
|
+
export function isKnobKey(key: string): key is keyof Knobs {
|
|
80
|
+
return knobKeys.has(key);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const stateKnobKeys = new Set<string>([
|
|
84
|
+
"fillStyle",
|
|
85
|
+
"borderRadius",
|
|
86
|
+
"borderWidth",
|
|
87
|
+
"elevation",
|
|
88
|
+
"textAccent",
|
|
89
|
+
"headingFont",
|
|
90
|
+
"bodyFont",
|
|
91
|
+
"fontWeight",
|
|
92
|
+
] satisfies (keyof StateKnobs)[]);
|
|
93
|
+
|
|
72
94
|
/**
|
|
73
95
|
* Shape-validate a parsed overrides payload before it reaches knob
|
|
74
96
|
* resolution: keep knob-shaped entries — string values plus one-level
|
|
75
|
-
* StateKnobs-style objects of string values (`hover`, `press`, …) —
|
|
76
|
-
* everything else, so a corrupt
|
|
77
|
-
*
|
|
97
|
+
* StateKnobs-style objects of string values (`hover`, `press`, …) — under a
|
|
98
|
+
* known knob name, and drop everything else, so a corrupt, foreign or stale
|
|
99
|
+
* cookie can never spread garbage (unknown keys, arrays, numbers, deep
|
|
100
|
+
* objects) into `{ ...preset.knobs, ...overrides }`.
|
|
78
101
|
*/
|
|
79
|
-
function sanitizeOverrides(parsed: unknown): Partial<Knobs> {
|
|
102
|
+
export function sanitizeOverrides(parsed: unknown): Partial<Knobs> {
|
|
80
103
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
81
104
|
return {};
|
|
82
105
|
}
|
|
83
106
|
const overrides: Record<string, unknown> = {};
|
|
84
107
|
for (const [key, value] of Object.entries(parsed)) {
|
|
85
|
-
if (
|
|
108
|
+
if (!knobKeys.has(key)) continue;
|
|
109
|
+
if (key === "animation") {
|
|
110
|
+
const animation = normalizeAnimationKnob(value);
|
|
111
|
+
if (animation !== undefined) overrides.animation = animation;
|
|
112
|
+
} else if (typeof value === "string") {
|
|
86
113
|
overrides[key] = value;
|
|
87
114
|
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
88
115
|
const nested: Record<string, string> = {};
|
|
89
116
|
for (const [stateKey, stateValue] of Object.entries(value)) {
|
|
90
|
-
if (typeof stateValue === "string")
|
|
117
|
+
if (stateKnobKeys.has(stateKey) && typeof stateValue === "string") {
|
|
118
|
+
nested[stateKey] = stateValue;
|
|
119
|
+
}
|
|
91
120
|
}
|
|
92
121
|
overrides[key] = nested;
|
|
93
122
|
}
|