@multiplatform.one/theme 7.34.4 → 7.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
@@ -10,7 +10,7 @@ export default {
|
|
|
10
10
|
docs: {
|
|
11
11
|
description: {
|
|
12
12
|
component:
|
|
13
|
-
"Surface paints nothing. It
|
|
13
|
+
"Surface paints nothing. It forwards a Tamagui size token to a subtree, and nested Surfaces clamp step-down only (LC-78). The probe below is a Tamagui stack that spreads what useResolvedKnobs resolved inside the wrapper, so the measured part is the stack, never the wrapper.",
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
},
|
|
@@ -20,8 +20,6 @@ export default {
|
|
|
20
20
|
function Probe({ label }: { label: string }) {
|
|
21
21
|
const { knobProps } = useResolvedKnobs();
|
|
22
22
|
const attrs = {
|
|
23
|
-
"data-size": String(knobProps.size),
|
|
24
|
-
"data-space": String(knobProps.space),
|
|
25
23
|
"data-size-token": String(knobProps.sizeToken),
|
|
26
24
|
"data-gap": String(knobProps.gap.gap),
|
|
27
25
|
} as Record<string, unknown>;
|
|
@@ -46,46 +44,31 @@ export const main = {
|
|
|
46
44
|
render: () => (
|
|
47
45
|
<XStack gap="$4" flexWrap="wrap" alignItems="flex-start">
|
|
48
46
|
<Probe label="outside" />
|
|
49
|
-
<Surface size="
|
|
47
|
+
<Surface size="$3">
|
|
50
48
|
<Probe label="small" />
|
|
51
49
|
</Surface>
|
|
52
|
-
<Surface size="
|
|
50
|
+
<Surface size="$5">
|
|
53
51
|
<Probe label="large" />
|
|
54
52
|
</Surface>
|
|
55
53
|
</XStack>
|
|
56
54
|
),
|
|
57
55
|
};
|
|
58
56
|
|
|
59
|
-
/** LC-
|
|
57
|
+
/** LC-78: a $5 request inside a $3 Surface resolves $3; $3 inside $5 stays $3. */
|
|
60
58
|
export const nestedClamp = {
|
|
61
59
|
name: "Nested clamp",
|
|
62
60
|
render: () => (
|
|
63
61
|
<XStack gap="$4" flexWrap="wrap" alignItems="flex-start">
|
|
64
|
-
<Surface size="
|
|
65
|
-
<Surface size="
|
|
62
|
+
<Surface size="$3">
|
|
63
|
+
<Surface size="$5">
|
|
66
64
|
<Probe label="large-in-small" />
|
|
67
65
|
</Surface>
|
|
68
66
|
</Surface>
|
|
69
|
-
<Surface size="
|
|
70
|
-
<Surface size="
|
|
67
|
+
<Surface size="$5">
|
|
68
|
+
<Surface size="$3">
|
|
71
69
|
<Probe label="small-in-large" />
|
|
72
70
|
</Surface>
|
|
73
71
|
</Surface>
|
|
74
72
|
</XStack>
|
|
75
73
|
),
|
|
76
74
|
};
|
|
77
|
-
|
|
78
|
-
/** Density steps the gap, not the control height. */
|
|
79
|
-
export const density = {
|
|
80
|
-
name: "Density",
|
|
81
|
-
render: () => (
|
|
82
|
-
<XStack gap="$4" flexWrap="wrap" alignItems="flex-start">
|
|
83
|
-
<Surface density="comfortable">
|
|
84
|
-
<Probe label="comfortable" />
|
|
85
|
-
</Surface>
|
|
86
|
-
<Surface density="compact">
|
|
87
|
-
<Probe label="compact" />
|
|
88
|
-
</Surface>
|
|
89
|
-
</XStack>
|
|
90
|
-
),
|
|
91
|
-
};
|
package/src/theme/Surface.tsx
CHANGED
|
@@ -1,72 +1,116 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import { createStyledContext } from "tamagui";
|
|
2
|
+
import { createStyledContext, type SizeTokens } from "tamagui";
|
|
3
|
+
import { SIZE_KEYS } from "./scale/scaleInputs";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
export type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
md: "medium",
|
|
13
|
-
medium: "medium",
|
|
14
|
-
lg: "large",
|
|
15
|
-
large: "large",
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export function normalizeSurfaceSize(size: SurfaceSizeIntent): SurfaceSize {
|
|
19
|
-
return SIZE_INTENT_ALIASES[size];
|
|
20
|
-
}
|
|
5
|
+
/** A Tamagui size key ("$3", "$true") or `unset` for no ceiling. */
|
|
6
|
+
export type SurfaceSize = SizeTokens | "unset";
|
|
7
|
+
/**
|
|
8
|
+
* `painted`: an ancestor already paints a surface box (Card, Panel), so a
|
|
9
|
+
* surface under it draws border only and no second shadow. Overlays (dialog,
|
|
10
|
+
* popover, sheet, menu, tooltip, toast) reset it: they float over the page.
|
|
11
|
+
*/
|
|
12
|
+
export type SurfaceIntent = { size: SurfaceSize; painted: boolean };
|
|
21
13
|
|
|
22
14
|
export const SurfaceContext = createStyledContext<SurfaceIntent>({
|
|
23
15
|
size: "unset",
|
|
24
|
-
|
|
16
|
+
painted: false,
|
|
25
17
|
});
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
const sizeKeyRank = new Map<string, number>(SIZE_KEYS.map((key, index) => [key, index]));
|
|
20
|
+
sizeKeyRank.set("$true", sizeKeyRank.get("$4") ?? 0);
|
|
29
21
|
|
|
30
22
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
23
|
+
* Rank of a size key on the scale's sorted `$size` ladder (`$true` ranks
|
|
24
|
+
* with `$4`). Undefined for anything that is not a size key.
|
|
33
25
|
*/
|
|
34
|
-
export function
|
|
35
|
-
|
|
36
|
-
parent: SurfaceSize | "unset",
|
|
37
|
-
): SurfaceSize | "unset" {
|
|
38
|
-
if (parent === "unset") return requested;
|
|
39
|
-
if (requested === "unset") return parent;
|
|
40
|
-
return SURFACE_SIZE_RANK[requested] <= SURFACE_SIZE_RANK[parent] ? requested : parent;
|
|
26
|
+
export function surfaceSizeRank(size: SurfaceSize): number | undefined {
|
|
27
|
+
return typeof size === "string" ? sizeKeyRank.get(size) : undefined;
|
|
41
28
|
}
|
|
42
29
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Order two size keys on the `$size` ladder: negative when `a` is smaller
|
|
32
|
+
* than `b`, 0 when equal or either is not a size key.
|
|
33
|
+
*
|
|
34
|
+
* @example A part that draws a smaller glyph below the default control size
|
|
35
|
+
* const small = compareSizeTokens(knobProps.sizeToken, "$true") < 0;
|
|
36
|
+
*/
|
|
37
|
+
export function compareSizeTokens(a: SurfaceSize | string, b: SurfaceSize | string): number {
|
|
38
|
+
const rankA = surfaceSizeRank(a as SurfaceSize);
|
|
39
|
+
const rankB = surfaceSizeRank(b as SurfaceSize);
|
|
40
|
+
return rankA === undefined || rankB === undefined ? 0 : rankA - rankB;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Where a size token sits against the default control size: -1 below
|
|
45
|
+
* `$true`, 0 at it, 1 above. For parts whose own table has a row per side.
|
|
46
|
+
*/
|
|
47
|
+
export function sizeTokenStep(sizeToken: SurfaceSize | string): -1 | 0 | 1 {
|
|
48
|
+
return Math.sign(compareSizeTokens(sizeToken, "$true")) as -1 | 0 | 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The row a part's own three-row table reads at a size token: `small` below
|
|
53
|
+
* `$true`, `large` above, `medium` at it. Only for per-part px tables
|
|
54
|
+
* (glyphs, swatches); controls read the size recipe.
|
|
55
|
+
*/
|
|
56
|
+
export function sizeTokenStop(sizeToken: SurfaceSize | string): "small" | "medium" | "large" {
|
|
57
|
+
const step = sizeTokenStep(sizeToken);
|
|
58
|
+
return step < 0 ? "small" : step > 0 ? "large" : "medium";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The whole size key one step below `size` (`$true` reads as `$4`), floored at
|
|
63
|
+
* `$1`: a table's cells default one key below the table. A key that is not a
|
|
64
|
+
* whole step snaps to the whole key under it.
|
|
65
|
+
*
|
|
66
|
+
* @example sizeTokenBelow("$true") === "$3"
|
|
67
|
+
*/
|
|
68
|
+
export function sizeTokenBelow(size: SizeTokens | string): SizeTokens {
|
|
69
|
+
const key = size === "$true" ? "$4" : String(size);
|
|
70
|
+
const step = Number(key.replace(/^\$/, ""));
|
|
71
|
+
if (!Number.isFinite(step)) return "$3";
|
|
72
|
+
return `$${Math.max(1, Math.ceil(step) - 1)}` as SizeTokens;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Nested surfaces and controls inherit the ceiling or go smaller, never
|
|
77
|
+
* larger (LC-78). `unset` parent = no ceiling; `unset` requested = inherit.
|
|
78
|
+
* A request that is not a size key cannot be ranked, so under a ceiling it
|
|
79
|
+
* takes the ceiling.
|
|
80
|
+
*/
|
|
81
|
+
export function clampSurfaceSize(requested: SurfaceSize, parent: SurfaceSize): SurfaceSize {
|
|
47
82
|
if (parent === "unset") return requested;
|
|
48
83
|
if (requested === "unset") return parent;
|
|
49
|
-
|
|
84
|
+
const requestedRank = surfaceSizeRank(requested);
|
|
85
|
+
const parentRank = surfaceSizeRank(parent);
|
|
86
|
+
if (parentRank === undefined) return requested;
|
|
87
|
+
if (requestedRank === undefined) return parent;
|
|
88
|
+
return requestedRank <= parentRank ? requested : parent;
|
|
50
89
|
}
|
|
51
90
|
|
|
52
91
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
92
|
+
* Forwards a size token to a subtree: every knob reader under it renders its
|
|
93
|
+
* controls at `size` unless it asks for a smaller one (LC-78 ceiling).
|
|
94
|
+
* `painted` marks (true) or resets (false) the painted-ancestor flag; omitted
|
|
95
|
+
* inherits it.
|
|
96
|
+
*
|
|
97
|
+
* @example A table cell's editors at the row's control height
|
|
98
|
+
* <Surface size="$3">{cell}</Surface>
|
|
55
99
|
*/
|
|
56
100
|
export function Surface({
|
|
57
101
|
size,
|
|
58
|
-
|
|
102
|
+
painted,
|
|
59
103
|
children,
|
|
60
104
|
}: {
|
|
61
|
-
size?:
|
|
62
|
-
|
|
105
|
+
size?: SizeTokens;
|
|
106
|
+
painted?: boolean;
|
|
63
107
|
children?: ReactNode;
|
|
64
108
|
}) {
|
|
65
109
|
const parent = SurfaceContext.useStyledContext();
|
|
66
|
-
const nextSize = clampSurfaceSize(size
|
|
67
|
-
const
|
|
110
|
+
const nextSize = clampSurfaceSize(size ?? "unset", parent.size);
|
|
111
|
+
const nextPainted = painted ?? parent.painted ?? false;
|
|
68
112
|
return (
|
|
69
|
-
<SurfaceContext.Provider size={nextSize}
|
|
113
|
+
<SurfaceContext.Provider size={nextSize} painted={nextPainted}>
|
|
70
114
|
{children}
|
|
71
115
|
</SurfaceContext.Provider>
|
|
72
116
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { themes as stockThemes } from "@tamagui/themes/v5";
|
|
1
2
|
import { describe, expect, it } from "vitest";
|
|
2
3
|
import { contrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
|
|
3
4
|
import { createThemesBuilder } from "./createThemes";
|
|
@@ -38,19 +39,44 @@ describe("MPO-448 / MPO-449: the built accent pair holds Axiom 12", () => {
|
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
it("
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
it("uses the nearest accent steps that hold the floors", () => {
|
|
43
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
44
|
+
const theme = themes[scheme];
|
|
45
|
+
const steps = Array.from({ length: 12 }, (_, i) => theme[`accent${i + 1}`]);
|
|
46
|
+
const luminance = (value: string) => relativeLuminance(hex(value));
|
|
47
|
+
const nearest = (preferred: string, candidates: string[], holds: (step: string) => boolean) =>
|
|
48
|
+
candidates
|
|
49
|
+
.filter(holds)
|
|
50
|
+
.sort(
|
|
51
|
+
(a, b) =>
|
|
52
|
+
Math.abs(luminance(a) - luminance(preferred)) -
|
|
53
|
+
Math.abs(luminance(b) - luminance(preferred)),
|
|
54
|
+
)[0];
|
|
55
|
+
expect(theme.accentBackground).toBe(
|
|
56
|
+
nearest(
|
|
57
|
+
stockThemes[scheme].accentBackground,
|
|
58
|
+
steps.slice(0, 10),
|
|
59
|
+
(fill) =>
|
|
60
|
+
Math.max(ratio(fill, theme.color1), ratio(fill, theme.color12)) >= 4.5 &&
|
|
61
|
+
ratio(fill, theme.color1) >= 3,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
expect(theme.accentColor).toBe(
|
|
65
|
+
nearest(stockThemes[scheme].accentColor, steps, (ink) =>
|
|
66
|
+
[theme.color1, theme.color2, theme.color3].every((surface) => ratio(ink, surface) >= 4.5),
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
46
70
|
});
|
|
47
71
|
|
|
48
|
-
it("the intent children
|
|
49
|
-
for (const
|
|
50
|
-
|
|
51
|
-
|
|
72
|
+
it("the intent children inherit the corrected accent pair", () => {
|
|
73
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
74
|
+
for (const intent of ["warning", "error", "success"]) {
|
|
75
|
+
expect(themes[`${scheme}_${intent}`].accentBackground).toBe(
|
|
76
|
+
themes[scheme].accentBackground,
|
|
77
|
+
);
|
|
78
|
+
expect(themes[`${scheme}_${intent}`].accentColor).toBe(themes[scheme].accentColor);
|
|
79
|
+
}
|
|
52
80
|
}
|
|
53
|
-
expect(hex(themes.dark_Tooltip.accentBackground)).toBe("#46349d");
|
|
54
|
-
expect(hex(themes.dark_Tooltip.accentColor)).toBe("#7766cc");
|
|
55
81
|
});
|
|
56
82
|
});
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { forceUpdateThemes, getConfig, setConfig } from "@tamagui/web";
|
|
1
|
+
import { type TamaguiInternalConfig, forceUpdateThemes, getConfig, setConfig } from "@tamagui/web";
|
|
2
|
+
import { type TokensGlobal, mergedTokens } from "./mergedTokens";
|
|
2
3
|
import type { ThemeConfig } from "./shared";
|
|
3
4
|
|
|
5
|
+
function isThemeConfig(
|
|
6
|
+
target: Pick<ThemeConfig, "tamagui"> | TamaguiInternalConfig,
|
|
7
|
+
): target is Pick<ThemeConfig, "tamagui"> {
|
|
8
|
+
return "tamagui" in target && Boolean((target as Pick<ThemeConfig, "tamagui">).tamagui);
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
/**
|
|
5
12
|
* Make `themeConfig` the config Tamagui resolves theme names against.
|
|
6
13
|
*
|
|
@@ -9,12 +16,21 @@ import type { ThemeConfig } from "./shared";
|
|
|
9
16
|
* full house config and its subset) the one created last wins whichever
|
|
10
17
|
* config the provider is handed. Tamagui also caches resolved theme names,
|
|
11
18
|
* so the cache is dropped here too: without that a name the new config lacks
|
|
12
|
-
* keeps resolving to the old config's theme.
|
|
13
|
-
*
|
|
19
|
+
* keeps resolving to the old config's theme. Takes a ThemeConfig or a built
|
|
20
|
+
* Tamagui config (a preset scale's `tamagui()`). Returns false when it is
|
|
21
|
+
* already the active one.
|
|
14
22
|
*/
|
|
15
|
-
export function activateThemeConfig(
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
export function activateThemeConfig(
|
|
24
|
+
themeConfig: Pick<ThemeConfig, "tamagui"> | TamaguiInternalConfig,
|
|
25
|
+
): boolean {
|
|
26
|
+
const tamagui = (
|
|
27
|
+
isThemeConfig(themeConfig) ? themeConfig.tamagui : themeConfig
|
|
28
|
+
) as TamaguiInternalConfig;
|
|
29
|
+
if (getConfig() === tamagui) return false;
|
|
30
|
+
setConfig(tamagui);
|
|
31
|
+
if (tamagui.tokensParsed) {
|
|
32
|
+
(globalThis as TokensGlobal).__tamaguiTokensMerged = mergedTokens(tamagui);
|
|
33
|
+
}
|
|
18
34
|
forceUpdateThemes();
|
|
19
35
|
return true;
|
|
20
36
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { animationsCSS } from "@tamagui/config/v5-css";
|
|
2
|
+
import { animationsReactNative } from "@tamagui/config/v5-rn";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { animationConfig, animationDurations, animationNames, motionStops } from "./css";
|
|
5
|
+
import { animationConfig as nativeConfig } from "./reactNative";
|
|
6
|
+
|
|
7
|
+
describe("Tamagui v5 motion tables", () => {
|
|
8
|
+
it("orders the thirteen named stops by duration", () => {
|
|
9
|
+
expect(motionStops).toEqual([
|
|
10
|
+
"quickestLessBouncy",
|
|
11
|
+
"quickest",
|
|
12
|
+
"quickerLessBouncy",
|
|
13
|
+
"quicker",
|
|
14
|
+
"quick",
|
|
15
|
+
"quickLessBouncy",
|
|
16
|
+
"medium",
|
|
17
|
+
"superBouncy",
|
|
18
|
+
"bouncy",
|
|
19
|
+
"slow",
|
|
20
|
+
"lazy",
|
|
21
|
+
"superLazy",
|
|
22
|
+
"slowest",
|
|
23
|
+
]);
|
|
24
|
+
expect(motionStops.map((stop) => animationDurations[stop])).toEqual([
|
|
25
|
+
60, 80, 100, 120, 150, 180, 300, 300, 350, 450, 500, 600, 800,
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
it("preserves every upstream CSS and native entry in order", () => {
|
|
29
|
+
const keys = Object.keys(animationsCSS.animations);
|
|
30
|
+
expect(keys).toHaveLength(22);
|
|
31
|
+
expect(animationNames.slice(0, 22)).toEqual(keys);
|
|
32
|
+
expect(Object.keys(nativeConfig).slice(0, 22)).toEqual(keys);
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
expect(animationConfig[key as keyof typeof animationConfig]).toEqual(
|
|
35
|
+
animationsCSS.animations[key as keyof typeof animationsCSS.animations],
|
|
36
|
+
);
|
|
37
|
+
expect(nativeConfig[key as keyof typeof nativeConfig]).toEqual(
|
|
38
|
+
animationsReactNative.animations[key as keyof typeof animationsReactNative.animations],
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("maps house aliases to their nearest v5 duration", () => {
|
|
44
|
+
for (const [alias, stop] of [
|
|
45
|
+
["tooltip", "slow"],
|
|
46
|
+
["snappy", "quickest"],
|
|
47
|
+
["gentle", "slow"],
|
|
48
|
+
] as const) {
|
|
49
|
+
expect(animationConfig[alias]).toEqual(animationConfig[stop]);
|
|
50
|
+
expect(nativeConfig[alias]).toEqual(nativeConfig[stop]);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("uses v5 easing and derives every duration from its CSS value", () => {
|
|
55
|
+
expect(animationConfig.quick).toBe("150ms cubic-bezier(0.25, 0.1, 0.25, 1)");
|
|
56
|
+
for (const key of animationNames) {
|
|
57
|
+
expect(animationConfig[key]).not.toContain("ease-in ");
|
|
58
|
+
expect(animationConfig[key]).not.toContain("ease-in-out");
|
|
59
|
+
expect(animationDurations[key]).toBe(Number.parseInt(animationConfig[key], 10));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -1,38 +1,12 @@
|
|
|
1
1
|
import { createAnimations } from "@tamagui/animations-css";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
*/
|
|
16
|
-
export const animationConfig = {
|
|
17
|
-
bouncy: "ease-in 200ms",
|
|
18
|
-
lazy: "ease-in 600ms",
|
|
19
|
-
slow: "ease-in 500ms",
|
|
20
|
-
medium: "ease-in 300ms",
|
|
21
|
-
quick: "ease-in 100ms",
|
|
22
|
-
tooltip: "ease-in 400ms",
|
|
23
|
-
snappy: "ease-out 80ms",
|
|
24
|
-
gentle: "ease-in-out 450ms",
|
|
25
|
-
} as const;
|
|
26
|
-
|
|
27
|
-
export type AnimationName = keyof typeof animationConfig;
|
|
28
|
-
export const animationNames = Object.keys(animationConfig) as AnimationName[];
|
|
2
|
+
import { animationConfig } from "../motionTiming";
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
animationConfig,
|
|
6
|
+
animationDurations,
|
|
7
|
+
animationNames,
|
|
8
|
+
type AnimationName,
|
|
9
|
+
} from "../motionTiming";
|
|
29
10
|
export const animations = createAnimations(animationConfig);
|
|
30
11
|
|
|
31
|
-
|
|
32
|
-
const match = timing.match(/(\d+)\s*ms/);
|
|
33
|
-
return match ? Number.parseInt(match[1], 10) : 200;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export const animationDurations: Record<AnimationName, number> = Object.fromEntries(
|
|
37
|
-
Object.entries(animationConfig).map(([k, v]) => [k, parseDurationMs(v)]),
|
|
38
|
-
) as Record<AnimationName, number>;
|
|
12
|
+
export { motionStops, type MotionStop } from "../motionStops";
|
|
@@ -1,55 +1,128 @@
|
|
|
1
1
|
import { createAnimations } from "@tamagui/animations-react-native";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Canonical spring vocabulary. Exported so native gesture code can drive
|
|
5
|
-
* RN `Animated.spring` (which provides completion callbacks) with the exact
|
|
6
|
-
* same physics as Tamagui's `transition` tokens — one closed vocabulary.
|
|
7
|
-
*/
|
|
8
3
|
export const animationConfig = {
|
|
4
|
+
"0ms": {
|
|
5
|
+
type: "timing",
|
|
6
|
+
duration: 0,
|
|
7
|
+
},
|
|
8
|
+
"50ms": {
|
|
9
|
+
type: "timing",
|
|
10
|
+
duration: 50,
|
|
11
|
+
},
|
|
12
|
+
"75ms": {
|
|
13
|
+
type: "timing",
|
|
14
|
+
duration: 75,
|
|
15
|
+
},
|
|
16
|
+
"100ms": {
|
|
17
|
+
type: "timing",
|
|
18
|
+
duration: 100,
|
|
19
|
+
},
|
|
20
|
+
"200ms": {
|
|
21
|
+
type: "timing",
|
|
22
|
+
duration: 200,
|
|
23
|
+
},
|
|
24
|
+
"250ms": {
|
|
25
|
+
type: "timing",
|
|
26
|
+
duration: 250,
|
|
27
|
+
},
|
|
28
|
+
"300ms": {
|
|
29
|
+
type: "timing",
|
|
30
|
+
duration: 300,
|
|
31
|
+
},
|
|
32
|
+
"400ms": {
|
|
33
|
+
type: "timing",
|
|
34
|
+
duration: 400,
|
|
35
|
+
},
|
|
36
|
+
"500ms": {
|
|
37
|
+
type: "timing",
|
|
38
|
+
duration: 500,
|
|
39
|
+
},
|
|
40
|
+
superBouncy: {
|
|
41
|
+
type: "spring",
|
|
42
|
+
damping: 5,
|
|
43
|
+
mass: 0.7,
|
|
44
|
+
stiffness: 180,
|
|
45
|
+
},
|
|
9
46
|
bouncy: {
|
|
10
47
|
type: "spring",
|
|
11
48
|
damping: 9,
|
|
12
49
|
mass: 0.9,
|
|
13
|
-
stiffness:
|
|
50
|
+
stiffness: 120,
|
|
51
|
+
},
|
|
52
|
+
superLazy: {
|
|
53
|
+
type: "spring",
|
|
54
|
+
damping: 25,
|
|
55
|
+
mass: 2,
|
|
56
|
+
stiffness: 25,
|
|
14
57
|
},
|
|
15
58
|
lazy: {
|
|
16
59
|
type: "spring",
|
|
17
60
|
damping: 18,
|
|
18
|
-
|
|
61
|
+
mass: 0.2,
|
|
62
|
+
stiffness: 10,
|
|
19
63
|
},
|
|
20
|
-
|
|
64
|
+
medium: {
|
|
65
|
+
damping: 16,
|
|
66
|
+
stiffness: 90,
|
|
67
|
+
mass: 0.8,
|
|
68
|
+
},
|
|
69
|
+
slowest: {
|
|
21
70
|
type: "spring",
|
|
22
71
|
damping: 15,
|
|
23
|
-
stiffness:
|
|
72
|
+
stiffness: 10,
|
|
24
73
|
},
|
|
25
|
-
|
|
74
|
+
slow: {
|
|
26
75
|
type: "spring",
|
|
27
|
-
damping:
|
|
28
|
-
|
|
29
|
-
stiffness: 120,
|
|
76
|
+
damping: 45,
|
|
77
|
+
stiffness: 60,
|
|
30
78
|
},
|
|
31
79
|
quick: {
|
|
32
80
|
type: "spring",
|
|
33
|
-
damping:
|
|
34
|
-
mass: 1
|
|
35
|
-
stiffness:
|
|
81
|
+
damping: 25,
|
|
82
|
+
mass: 1,
|
|
83
|
+
stiffness: 550,
|
|
84
|
+
},
|
|
85
|
+
quickLessBouncy: {
|
|
86
|
+
type: "spring",
|
|
87
|
+
damping: 40,
|
|
88
|
+
mass: 2,
|
|
89
|
+
stiffness: 400,
|
|
90
|
+
},
|
|
91
|
+
quicker: {
|
|
92
|
+
type: "spring",
|
|
93
|
+
damping: 16,
|
|
94
|
+
mass: 0.6,
|
|
95
|
+
stiffness: 700,
|
|
96
|
+
},
|
|
97
|
+
quickerLessBouncy: {
|
|
98
|
+
type: "spring",
|
|
99
|
+
damping: 27,
|
|
100
|
+
mass: 0.45,
|
|
101
|
+
stiffness: 750,
|
|
102
|
+
},
|
|
103
|
+
quickest: {
|
|
104
|
+
damping: 15,
|
|
105
|
+
mass: 0.6,
|
|
106
|
+
stiffness: 400,
|
|
107
|
+
},
|
|
108
|
+
quickestLessBouncy: {
|
|
109
|
+
damping: 24,
|
|
110
|
+
mass: 0.35,
|
|
111
|
+
stiffness: 750,
|
|
36
112
|
},
|
|
37
113
|
tooltip: {
|
|
38
114
|
type: "spring",
|
|
39
|
-
damping:
|
|
40
|
-
|
|
41
|
-
stiffness: 100,
|
|
115
|
+
damping: 45,
|
|
116
|
+
stiffness: 60,
|
|
42
117
|
},
|
|
43
118
|
snappy: {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
stiffness: 350,
|
|
119
|
+
damping: 15,
|
|
120
|
+
mass: 0.6,
|
|
121
|
+
stiffness: 400,
|
|
48
122
|
},
|
|
49
123
|
gentle: {
|
|
50
124
|
type: "spring",
|
|
51
|
-
damping:
|
|
52
|
-
mass: 1.4,
|
|
125
|
+
damping: 45,
|
|
53
126
|
stiffness: 60,
|
|
54
127
|
},
|
|
55
128
|
} as const;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { configWithoutAnimations } from "@tamagui/config";
|
|
2
|
+
import { createTamagui } from "tamagui";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { contrastRatio, minContrastRatio, normalizeToHex, relativeLuminance } from "./colorRules";
|
|
5
|
+
import { createThemesBuilder } from "./createThemes";
|
|
6
|
+
import { defaultAccentTheme } from "./defaults/accent";
|
|
7
|
+
import { defaultBaseTheme } from "./defaults/base";
|
|
8
|
+
import { defaultBuilderOptions } from "./defaults/builderOptions";
|
|
9
|
+
|
|
10
|
+
const inputThemes: Record<string, Record<string, string>> = {};
|
|
11
|
+
|
|
12
|
+
const themes = createThemesBuilder(defaultBaseTheme, defaultAccentTheme, {
|
|
13
|
+
...defaultBuilderOptions,
|
|
14
|
+
getTheme: (props) => {
|
|
15
|
+
inputThemes[props.name] = props.theme;
|
|
16
|
+
return defaultBuilderOptions.getTheme!(props);
|
|
17
|
+
},
|
|
18
|
+
}).themes() as Record<string, Record<string, string>>;
|
|
19
|
+
const config = createTamagui({ ...configWithoutAnimations, themes });
|
|
20
|
+
|
|
21
|
+
function ratio(a: string, b: string) {
|
|
22
|
+
const aHex = normalizeToHex(a);
|
|
23
|
+
const bHex = normalizeToHex(b);
|
|
24
|
+
if (!aHex || !bHex) throw new Error(`Expected opaque colours: ${a}, ${b}`);
|
|
25
|
+
return contrastRatio(relativeLuminance(aHex), relativeLuminance(bHex));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("DF-06 / WCAG 1.4.11: control boundaries hold 3:1", () => {
|
|
29
|
+
for (const [name, theme] of Object.entries(themes).filter(
|
|
30
|
+
([, theme]) => theme.color5 !== undefined,
|
|
31
|
+
)) {
|
|
32
|
+
it(`${name} holds the boundary floor`, () => {
|
|
33
|
+
expect(theme.boundaryColor).toBeTruthy();
|
|
34
|
+
const surfaces = [theme.background, theme.color3, theme.color5];
|
|
35
|
+
const minimum = (ink: string) => Math.min(...surfaces.map((surface) => ratio(ink, surface)));
|
|
36
|
+
// Tint text is neutralized after getBaseTheme; selection uses the input ramp.
|
|
37
|
+
const candidates = [8, 9, 10, 11, 12].map((step) => inputThemes[name][`color${step}`]);
|
|
38
|
+
const firstHolding = candidates.find((ink) => minimum(ink) >= minContrastRatio);
|
|
39
|
+
expect(minimum(theme.boundaryColor)).toBeGreaterThanOrEqual(minContrastRatio);
|
|
40
|
+
expect(theme.boundaryColor).toBe(firstHolding);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
it.each([
|
|
45
|
+
"light_Switch",
|
|
46
|
+
"dark_error_InputContainerFrame",
|
|
47
|
+
"light_ProgressIndicator",
|
|
48
|
+
"dark_ProgressIndicator",
|
|
49
|
+
"light_alt1",
|
|
50
|
+
"dark_error_active",
|
|
51
|
+
])("%s resolves its parent's boundary through Tamagui's theme proxy", (name) => {
|
|
52
|
+
const parent = name.slice(0, name.lastIndexOf("_"));
|
|
53
|
+
expect(Object.hasOwn(themes[name], "boundaryColor")).toBe(false);
|
|
54
|
+
expect(config.themes[parent].boundaryColor?.val).toBeTruthy();
|
|
55
|
+
expect(config.themes[name].boundaryColor?.val).toBe(config.themes[parent].boundaryColor.val);
|
|
56
|
+
});
|
|
57
|
+
});
|