@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
package/src/theme/theme.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type CreateTamaguiProps,
|
|
6
6
|
TamaguiProvider,
|
|
7
7
|
type TamaguiProviderProps,
|
|
8
|
+
Theme,
|
|
8
9
|
useDidFinishSSR,
|
|
9
10
|
} from "tamagui";
|
|
10
11
|
import type { PropsWithChildren } from "react";
|
|
@@ -12,9 +13,13 @@ import { animationDurations } from "./animations/css";
|
|
|
12
13
|
import { getCookieWatchList, readOverridesCookie, readPresetCookie } from "./cookies";
|
|
13
14
|
import { CornerSmoothingStyles } from "./cornerSmoothing";
|
|
14
15
|
import { FontKnobStyles } from "./FontKnobStyles";
|
|
16
|
+
import type { Knobs } from "./knobs";
|
|
15
17
|
import { PresetContext, type PresetContextValue } from "./PresetContext";
|
|
16
18
|
import { defaultPreset } from "./presets";
|
|
17
|
-
import {
|
|
19
|
+
import { ScaleStyles } from "./scale/ScaleStyles";
|
|
20
|
+
import { useActiveScale } from "./scale/useActiveScale";
|
|
21
|
+
import { type ThemeConfig, getPreset } from "./shared";
|
|
22
|
+
import { useSchemePreference } from "./useSchemePreference";
|
|
18
23
|
|
|
19
24
|
/**
|
|
20
25
|
* Animate a DOM mutation (typically a theme/scheme switch) using the View
|
|
@@ -53,10 +58,22 @@ export interface ThemeProviderProps<
|
|
|
53
58
|
> extends PropsWithChildren<Omit<TamaguiProviderProps, "config" | "defaultTheme">> {
|
|
54
59
|
config?: {
|
|
55
60
|
tamagui: ReturnType<typeof import("tamagui").createTamagui<T>>;
|
|
56
|
-
}
|
|
61
|
+
} & Partial<Pick<ThemeConfig, "scales" | "defaultPreset">>;
|
|
57
62
|
systemTheme?: "light" | "dark";
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
function useCookiePreset(): { presetName?: string; overrides: Partial<Knobs> } {
|
|
66
|
+
const isCookieDisabled =
|
|
67
|
+
typeof process !== "undefined" && process.env?.COOKIE_POLICY?.toLowerCase() === "disabled";
|
|
68
|
+
const watchList = isCookieDisabled ? [] : getCookieWatchList();
|
|
69
|
+
const [cookie] = useCookies<string, Record<string, string>>(watchList);
|
|
70
|
+
const presetName = readPresetCookie(cookie as Record<string, string | undefined>);
|
|
71
|
+
const overrides = readOverridesCookie(cookie as Record<string, string | undefined>);
|
|
72
|
+
const overridesKey = JSON.stringify(overrides);
|
|
73
|
+
const stableOverrides = useMemo(() => JSON.parse(overridesKey) as Partial<Knobs>, [overridesKey]);
|
|
74
|
+
return { presetName, overrides: stableOverrides };
|
|
75
|
+
}
|
|
76
|
+
|
|
60
77
|
// ---------------------------------------------------------------------------
|
|
61
78
|
// ThemeProvider (simplified — TamaguiProvider + scheme + ToastProvider)
|
|
62
79
|
// ---------------------------------------------------------------------------
|
|
@@ -64,18 +81,28 @@ export interface ThemeProviderProps<
|
|
|
64
81
|
export function ThemeProvider<T extends CreateTamaguiProps>({
|
|
65
82
|
children,
|
|
66
83
|
config,
|
|
67
|
-
systemTheme
|
|
84
|
+
systemTheme,
|
|
68
85
|
...tamaguiProps
|
|
69
86
|
}: ThemeProviderProps<T>) {
|
|
70
|
-
//
|
|
87
|
+
// A caller that resolves the scheme itself wins:
|
|
71
88
|
// - App: useUserScheme() from @vxrn/color-scheme (reacts to OS changes)
|
|
72
89
|
// - Storybook: decorator resolves "system"/"light"/"dark" with OS listener
|
|
73
|
-
//
|
|
74
|
-
const
|
|
90
|
+
// Without one, the shared scheme preference (ThemePresetPicker's) applies.
|
|
91
|
+
const scheme = useSchemePreference();
|
|
92
|
+
const activeTheme = systemTheme ?? scheme.value;
|
|
75
93
|
// A hydrating root renders a light placeholder to match SSR (MPO-278); the
|
|
76
94
|
// blocking scheme script owns <html> until hydration finishes.
|
|
77
95
|
const hydrated = useDidFinishSSR();
|
|
78
96
|
|
|
97
|
+
// The preset is resolved here, above TamaguiProvider, so its scale is
|
|
98
|
+
// installed before the first child renders (SSR and hydration alike). An
|
|
99
|
+
// ancestor PresetContext (Storybook's decorator) owns the preset instead.
|
|
100
|
+
const ancestor = useContext(PresetContext);
|
|
101
|
+
const cookiePreset = useCookiePreset();
|
|
102
|
+
const presetName =
|
|
103
|
+
(ancestor ? ancestor.presetName : cookiePreset.presetName) ?? config?.defaultPreset;
|
|
104
|
+
const scaleId = useActiveScale(config, presetName);
|
|
105
|
+
|
|
79
106
|
const prevThemeRef = useRef<string | undefined>(undefined);
|
|
80
107
|
|
|
81
108
|
// Set the initial theme class synchronously to avoid FOUC.
|
|
@@ -140,6 +167,9 @@ export function ThemeProvider<T extends CreateTamaguiProps>({
|
|
|
140
167
|
document.head.appendChild(style);
|
|
141
168
|
}, []);
|
|
142
169
|
|
|
170
|
+
// The base config, not the scale's: TamaguiProvider's stylesheet is keyed
|
|
171
|
+
// by href and never rewritten after first render, so it must hold the
|
|
172
|
+
// default token values the ScaleStyles blocks override.
|
|
143
173
|
return (
|
|
144
174
|
<TamaguiProvider
|
|
145
175
|
{...tamaguiProps}
|
|
@@ -147,23 +177,27 @@ export function ThemeProvider<T extends CreateTamaguiProps>({
|
|
|
147
177
|
defaultTheme={activeTheme}
|
|
148
178
|
disableInjectCSS={false}
|
|
149
179
|
>
|
|
180
|
+
<ScaleStyles config={config} presetName={presetName} />
|
|
150
181
|
<ToastProvider>
|
|
151
|
-
<KnobBridge
|
|
182
|
+
<KnobBridge presetName={presetName} overrides={cookiePreset.overrides} scaleId={scaleId}>
|
|
183
|
+
{children}
|
|
184
|
+
</KnobBridge>
|
|
152
185
|
</ToastProvider>
|
|
153
186
|
</TamaguiProvider>
|
|
154
187
|
);
|
|
155
188
|
}
|
|
156
189
|
|
|
157
190
|
/**
|
|
158
|
-
* Internal bridge that
|
|
159
|
-
* (reactive via CookiesProvider)
|
|
160
|
-
* useResolvedKnobs() picks up the live values.
|
|
191
|
+
* Internal bridge that provides a PresetContext from the preset name + knob
|
|
192
|
+
* overrides ThemeProvider read from cookies (reactive via CookiesProvider),
|
|
193
|
+
* so that useResolvedKnobs() picks up the live values.
|
|
161
194
|
*
|
|
162
195
|
* Resolution order (same on SSR and client):
|
|
163
|
-
* 1.
|
|
164
|
-
* 2.
|
|
196
|
+
* 1. `mp.preset` cookie → look up the preset from the registry
|
|
197
|
+
* 2. `mp.ov.*` cookies → partial knob overrides
|
|
165
198
|
* 3. Merge: preset base knobs + overrides → final knobs
|
|
166
|
-
* 4. Provide the full preset (knobs, tints, intents)
|
|
199
|
+
* 4. Provide the full preset (knobs, tints, intents) plus the active
|
|
200
|
+
* preset name and scale id via PresetContext
|
|
167
201
|
*
|
|
168
202
|
* If an ancestor PresetContext already exists (e.g. from a Storybook decorator),
|
|
169
203
|
* the bridge is a pass-through to avoid overriding it.
|
|
@@ -173,24 +207,23 @@ export function ThemeProvider<T extends CreateTamaguiProps>({
|
|
|
173
207
|
* stylesheet restyle every shipped app that uses ThemeProvider. Hairline
|
|
174
208
|
* still self-mounts on module load.
|
|
175
209
|
*/
|
|
176
|
-
function KnobBridge({
|
|
210
|
+
function KnobBridge({
|
|
211
|
+
children,
|
|
212
|
+
presetName,
|
|
213
|
+
overrides,
|
|
214
|
+
scaleId,
|
|
215
|
+
}: PropsWithChildren<{ presetName?: string; overrides: Partial<Knobs>; scaleId: string }>) {
|
|
177
216
|
const existingCtx = useContext(PresetContext);
|
|
178
217
|
|
|
179
|
-
const isCookieDisabled =
|
|
180
|
-
typeof process !== "undefined" && process.env?.COOKIE_POLICY?.toLowerCase() === "disabled";
|
|
181
|
-
const watchList = isCookieDisabled ? [] : getCookieWatchList();
|
|
182
|
-
const [cookie] = useCookies<string, Record<string, string>>(watchList);
|
|
183
|
-
|
|
184
|
-
const presetName = readPresetCookie(cookie as Record<string, string | undefined>);
|
|
185
|
-
const overrides = readOverridesCookie(cookie as Record<string, string | undefined>);
|
|
186
|
-
|
|
187
218
|
const presetCtxValue = useMemo<PresetContextValue>(() => {
|
|
188
219
|
const base = (presetName ? getPreset(presetName) : undefined) ?? defaultPreset;
|
|
189
220
|
return {
|
|
190
221
|
preset: { ...base, knobs: { ...base.knobs, ...overrides } },
|
|
191
222
|
overrides,
|
|
223
|
+
presetName,
|
|
224
|
+
scaleId,
|
|
192
225
|
};
|
|
193
|
-
}, [presetName, overrides]);
|
|
226
|
+
}, [presetName, overrides, scaleId]);
|
|
194
227
|
|
|
195
228
|
const animationValue = presetCtxValue.preset.knobs.animation;
|
|
196
229
|
useEffect(() => {
|
|
@@ -209,9 +242,12 @@ function KnobBridge({ children }: PropsWithChildren) {
|
|
|
209
242
|
</>
|
|
210
243
|
);
|
|
211
244
|
|
|
245
|
+
const theme = (existingCtx ?? presetCtxValue).preset.theme;
|
|
246
|
+
const themedTree = theme ? <Theme name={theme}>{tree}</Theme> : tree;
|
|
247
|
+
|
|
212
248
|
if (existingCtx) {
|
|
213
|
-
return
|
|
249
|
+
return themedTree;
|
|
214
250
|
}
|
|
215
251
|
|
|
216
|
-
return <PresetContext.Provider value={presetCtxValue}>{
|
|
252
|
+
return <PresetContext.Provider value={presetCtxValue}>{themedTree}</PresetContext.Provider>;
|
|
217
253
|
}
|
package/src/theme/tileSizes.ts
CHANGED
|
@@ -30,13 +30,12 @@
|
|
|
30
30
|
* here, restyled only through recipe inputs
|
|
31
31
|
* (`setSizeRecipeInputs({ tile })` / `createDefaultThemeConfig({ recipeInputs:
|
|
32
32
|
* { tile } })`). WHICH rung a surface shows is per-app (a prop, often
|
|
33
|
-
* persisted per user). Tile gutters
|
|
34
|
-
*
|
|
33
|
+
* persisted per user). Tile gutters are the spacing roles and tile rounding
|
|
34
|
+
* the borderRadius knob — this module never emits a gap or a radius.
|
|
35
35
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* here reads the density knob.
|
|
36
|
+
* SCALE (LC-100): a preset's token scale moves the gutters (role px) and
|
|
37
|
+
* never re-rungs the ladder — tile magnitude is content scale, not chrome
|
|
38
|
+
* scale.
|
|
40
39
|
*/
|
|
41
40
|
|
|
42
41
|
// ── Rungs ─────────────────────────────────────────────────────
|
|
@@ -239,7 +238,7 @@ export function tileRungFlexProps(stop: TileSizeStop): TileRungFlexProps {
|
|
|
239
238
|
/**
|
|
240
239
|
* Column count for a fixed-column tile grid (the ImageGrid / springboard
|
|
241
240
|
* idiom): as many columns as fit at the rung's minWidth. `gutter` is the
|
|
242
|
-
* inter-column gap (the
|
|
241
|
+
* inter-column gap (the gap role's px), never baked into the rung.
|
|
243
242
|
*
|
|
244
243
|
* The count is min-driven on purpose: adding a column to honor `maxWidth`
|
|
245
244
|
* would, by construction of the fit, drop every cell below `minWidth`
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MotionRole } from "./motion";
|
|
1
2
|
import type { TransitionProp } from "tamagui";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -8,8 +9,30 @@ import type { TransitionProp } from "tamagui";
|
|
|
8
9
|
* animation `none` would put motion on every part that is meant to be still.
|
|
9
10
|
* Native keeps omitting it until the driver has an instant token to pass.
|
|
10
11
|
*/
|
|
11
|
-
export function transitionProps(
|
|
12
|
+
export function transitionProps(
|
|
13
|
+
role: MotionRole | TransitionProp | undefined,
|
|
14
|
+
extra: readonly string[] = [],
|
|
15
|
+
): {
|
|
12
16
|
transition?: TransitionProp;
|
|
17
|
+
animateOnly?: string[];
|
|
13
18
|
} {
|
|
14
|
-
|
|
19
|
+
const isRole = isMotionRole(role);
|
|
20
|
+
const transition = isRole ? role.transition : role;
|
|
21
|
+
if (transition === undefined) return {};
|
|
22
|
+
if (!isRole) return { transition };
|
|
23
|
+
const properties = [...role.animateOnly, ...extra];
|
|
24
|
+
return {
|
|
25
|
+
transition,
|
|
26
|
+
animateOnly: properties,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isMotionRole(value: MotionRole | TransitionProp | undefined): value is MotionRole {
|
|
31
|
+
return (
|
|
32
|
+
typeof value === "object" &&
|
|
33
|
+
value !== null &&
|
|
34
|
+
"transition" in value &&
|
|
35
|
+
"animateOnly" in value &&
|
|
36
|
+
Array.isArray(value.animateOnly)
|
|
37
|
+
);
|
|
15
38
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { motionFor } from "./motion";
|
|
2
|
+
import { transitionProps as nativeTransitionProps } from "./transitionProps.native";
|
|
1
3
|
import { describe, expect, it } from "vitest";
|
|
2
4
|
import { transitionProps } from "./transitionProps";
|
|
3
5
|
|
|
@@ -12,3 +14,35 @@ describe("transitionProps (web)", () => {
|
|
|
12
14
|
expect(transitionProps("bouncy")).toEqual({ transition: "bouncy" });
|
|
13
15
|
});
|
|
14
16
|
});
|
|
17
|
+
|
|
18
|
+
describe("role property scopes", () => {
|
|
19
|
+
it("keeps the web transition key for a disabled role", () => {
|
|
20
|
+
expect(transitionProps(motionFor("none").control)).toEqual({
|
|
21
|
+
transition: undefined,
|
|
22
|
+
animateOnly: ["background-color", "border-color", "color", "opacity", "transform"],
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
it("hyphenates the control scope and additional properties", () => {
|
|
26
|
+
expect(transitionProps(motionFor("quick").control, ["maxHeight", "left"])).toEqual({
|
|
27
|
+
transition: "quick",
|
|
28
|
+
animateOnly: [
|
|
29
|
+
"background-color",
|
|
30
|
+
"border-color",
|
|
31
|
+
"color",
|
|
32
|
+
"opacity",
|
|
33
|
+
"transform",
|
|
34
|
+
"max-height",
|
|
35
|
+
"left",
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it("omits disabled native transitions and preserves camelCase scopes", () => {
|
|
40
|
+
expect(nativeTransitionProps(motionFor("none").control)).toEqual({});
|
|
41
|
+
expect(nativeTransitionProps(undefined)).toEqual({});
|
|
42
|
+
expect(nativeTransitionProps("quick")).toEqual({ transition: "quick" });
|
|
43
|
+
expect(nativeTransitionProps(motionFor("quick").control, ["maxHeight"])).toEqual({
|
|
44
|
+
transition: "quick",
|
|
45
|
+
animateOnly: ["backgroundColor", "borderColor", "color", "opacity", "transform", "maxHeight"],
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MotionRole } from "./motion";
|
|
1
2
|
import type { TransitionProp } from "tamagui";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -9,8 +10,31 @@ import type { TransitionProp } from "tamagui";
|
|
|
9
10
|
* knob flips to any other stop (MPO-327). On web the key therefore stays, and
|
|
10
11
|
* an undefined value paints no CSS transition. The native twin differs.
|
|
11
12
|
*/
|
|
12
|
-
export function transitionProps(
|
|
13
|
+
export function transitionProps(
|
|
14
|
+
role: MotionRole | TransitionProp | undefined,
|
|
15
|
+
extra: readonly string[] = [],
|
|
16
|
+
): {
|
|
13
17
|
transition?: TransitionProp;
|
|
18
|
+
animateOnly?: string[];
|
|
14
19
|
} {
|
|
15
|
-
|
|
20
|
+
const isRole = isMotionRole(role);
|
|
21
|
+
const transition = isRole ? role.transition : role;
|
|
22
|
+
if (!isRole) return { transition };
|
|
23
|
+
const properties = [...role.animateOnly, ...extra];
|
|
24
|
+
return {
|
|
25
|
+
transition,
|
|
26
|
+
animateOnly: properties.map((property) =>
|
|
27
|
+
property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`),
|
|
28
|
+
),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isMotionRole(value: MotionRole | TransitionProp | undefined): value is MotionRole {
|
|
33
|
+
return (
|
|
34
|
+
typeof value === "object" &&
|
|
35
|
+
value !== null &&
|
|
36
|
+
"transition" in value &&
|
|
37
|
+
"animateOnly" in value &&
|
|
38
|
+
Array.isArray(value.animateOnly)
|
|
39
|
+
);
|
|
16
40
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { act, cleanup, renderHook } from "@testing-library/react";
|
|
2
|
+
import { createElement, type ReactNode } from "react";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { PresetContext } from "./PresetContext";
|
|
5
|
+
import { defaultPreset } from "./presets";
|
|
6
|
+
import {
|
|
7
|
+
getPresetChoices,
|
|
8
|
+
getStoredPreset,
|
|
9
|
+
resetPresetChoicesForTests,
|
|
10
|
+
setStoredPreset,
|
|
11
|
+
} from "./presetStore";
|
|
12
|
+
import { usePresetSwitch } from "./usePresetSwitch.native";
|
|
13
|
+
|
|
14
|
+
const { items } = vi.hoisted(() => ({ items: new Map<string, string>() }));
|
|
15
|
+
vi.mock("@multiplatform.one/store", () => ({
|
|
16
|
+
storage: {
|
|
17
|
+
getItem: (key: string) => items.get(key) ?? null,
|
|
18
|
+
setItem: (key: string, value: string) => {
|
|
19
|
+
items.set(key, value);
|
|
20
|
+
},
|
|
21
|
+
removeItem: (key: string) => {
|
|
22
|
+
items.delete(key);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
items.clear();
|
|
28
|
+
resetPresetChoicesForTests();
|
|
29
|
+
setStoredPreset(undefined);
|
|
30
|
+
});
|
|
31
|
+
afterEach(cleanup);
|
|
32
|
+
|
|
33
|
+
describe("native preset switching", () => {
|
|
34
|
+
it("switches the shared store, clears overrides and keeps callbacks stable", async () => {
|
|
35
|
+
const { result } = renderHook(() => usePresetSwitch());
|
|
36
|
+
expect(result.current.presetName).toBe("default");
|
|
37
|
+
const { applyPreset, choosePreset } = result.current;
|
|
38
|
+
act(() => setStoredPreset("dense", { elevation: "large" }));
|
|
39
|
+
expect(result.current.presetName).toBe("dense");
|
|
40
|
+
act(() => applyPreset("roomy"));
|
|
41
|
+
expect(result.current.presetName).toBe("roomy");
|
|
42
|
+
expect(getStoredPreset().overrides).toEqual({});
|
|
43
|
+
await vi.waitFor(() => expect(items.get("mp.preset")).toBe("roomy"));
|
|
44
|
+
expect(getPresetChoices()).toEqual({ users: {} });
|
|
45
|
+
expect(result.current.applyPreset).toBe(applyPreset);
|
|
46
|
+
expect(result.current.choosePreset).toBe(choosePreset);
|
|
47
|
+
act(() => choosePreset("paper", "a"));
|
|
48
|
+
expect(getPresetChoices()).toEqual({ users: { a: "paper" }, owner: "a" });
|
|
49
|
+
expect(result.current.presetName).toBe("paper");
|
|
50
|
+
act(() => choosePreset("studio"));
|
|
51
|
+
expect(getPresetChoices().device).toBe("studio");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("prefers the provider's active name to storage", () => {
|
|
55
|
+
setStoredPreset("dense");
|
|
56
|
+
const { result } = renderHook(() => usePresetSwitch(), {
|
|
57
|
+
wrapper: ({ children }: { children: ReactNode }) =>
|
|
58
|
+
createElement(
|
|
59
|
+
PresetContext.Provider,
|
|
60
|
+
{
|
|
61
|
+
value: { preset: defaultPreset, presetName: "roomy" },
|
|
62
|
+
},
|
|
63
|
+
children,
|
|
64
|
+
),
|
|
65
|
+
});
|
|
66
|
+
expect(result.current.presetName).toBe("roomy");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
2
|
+
import { usePresetContext } from "./PresetContext";
|
|
3
|
+
import {
|
|
4
|
+
getStoredPreset,
|
|
5
|
+
recordPresetChoice,
|
|
6
|
+
setStoredPreset,
|
|
7
|
+
subscribeStoredPreset,
|
|
8
|
+
} from "./presetStore";
|
|
9
|
+
|
|
10
|
+
export interface PresetSwitch {
|
|
11
|
+
/** The provider's active preset; "default" when none is stored. */
|
|
12
|
+
presetName: string;
|
|
13
|
+
/** Switch the active preset without recording anyone's choice. */
|
|
14
|
+
applyPreset(name: string): void;
|
|
15
|
+
/** Switch and record it as this user's choice (no user = this device's signed-out choice). */
|
|
16
|
+
choosePreset(name: string, userId?: string | null): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function usePresetSwitch(): PresetSwitch {
|
|
20
|
+
const context = usePresetContext();
|
|
21
|
+
const stored = useSyncExternalStore(subscribeStoredPreset, getStoredPreset, getStoredPreset);
|
|
22
|
+
const presetName = context?.presetName || stored.presetName || "default";
|
|
23
|
+
const applyPreset = useCallback((name: string) => setStoredPreset(name), []);
|
|
24
|
+
const choosePreset = useCallback(
|
|
25
|
+
(name: string, userId?: string | null) => {
|
|
26
|
+
recordPresetChoice(name, userId);
|
|
27
|
+
applyPreset(name);
|
|
28
|
+
},
|
|
29
|
+
[applyPreset],
|
|
30
|
+
);
|
|
31
|
+
return { presetName, applyPreset, choosePreset };
|
|
32
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { act, cleanup, renderHook } from "@testing-library/react";
|
|
2
|
+
import { createElement, type ReactNode } from "react";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { cookiePreset, writeOverridesCookie } from "./cookies";
|
|
5
|
+
import { PresetContext } from "./PresetContext";
|
|
6
|
+
import { defaultPreset } from "./presets";
|
|
7
|
+
import { getPresetChoices, resetPresetChoicesForTests } from "./presetStore";
|
|
8
|
+
import { usePresetSwitch } from "./usePresetSwitch";
|
|
9
|
+
|
|
10
|
+
const { cookies, setCookie, removeCookie, items } = vi.hoisted(() => ({
|
|
11
|
+
cookies: {} as Record<string, string>,
|
|
12
|
+
setCookie: vi.fn(),
|
|
13
|
+
removeCookie: vi.fn(),
|
|
14
|
+
items: new Map<string, string>(),
|
|
15
|
+
}));
|
|
16
|
+
vi.mock("react-cookie", () => ({ useCookies: () => [cookies, setCookie, removeCookie] }));
|
|
17
|
+
vi.mock("@multiplatform.one/store", () => ({
|
|
18
|
+
storage: {
|
|
19
|
+
getItem: (key: string) => items.get(key) ?? null,
|
|
20
|
+
setItem: (key: string, value: string) => {
|
|
21
|
+
items.set(key, value);
|
|
22
|
+
},
|
|
23
|
+
removeItem: (key: string) => {
|
|
24
|
+
items.delete(key);
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.stubEnv("COOKIE_POLICY", "enabled");
|
|
31
|
+
Object.keys(cookies).forEach((key) => delete cookies[key]);
|
|
32
|
+
items.clear();
|
|
33
|
+
setCookie.mockReset();
|
|
34
|
+
removeCookie.mockReset();
|
|
35
|
+
resetPresetChoicesForTests();
|
|
36
|
+
});
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
cleanup();
|
|
39
|
+
vi.unstubAllEnvs();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("web preset switching", () => {
|
|
43
|
+
it("reads the provider before the cookie and defaults without either", () => {
|
|
44
|
+
const plain = renderHook(() => usePresetSwitch());
|
|
45
|
+
expect(plain.result.current.presetName).toBe("default");
|
|
46
|
+
cookies[cookiePreset] = "dense";
|
|
47
|
+
plain.rerender();
|
|
48
|
+
expect(plain.result.current.presetName).toBe("dense");
|
|
49
|
+
const provided = renderHook(() => usePresetSwitch(), {
|
|
50
|
+
wrapper: ({ children }: { children: ReactNode }) =>
|
|
51
|
+
createElement(
|
|
52
|
+
PresetContext.Provider,
|
|
53
|
+
{
|
|
54
|
+
value: { preset: defaultPreset, presetName: "roomy" },
|
|
55
|
+
},
|
|
56
|
+
children,
|
|
57
|
+
),
|
|
58
|
+
});
|
|
59
|
+
expect(provided.result.current.presetName).toBe("roomy");
|
|
60
|
+
expect(setCookie).not.toHaveBeenCalled();
|
|
61
|
+
expect(removeCookie).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("writes only on demand, clearing every override without recording a choice", () => {
|
|
65
|
+
const { result, rerender } = renderHook(() => usePresetSwitch());
|
|
66
|
+
const { applyPreset, choosePreset } = result.current;
|
|
67
|
+
cookies[cookiePreset] = "paper";
|
|
68
|
+
rerender();
|
|
69
|
+
expect(result.current.applyPreset).toBe(applyPreset);
|
|
70
|
+
expect(result.current.choosePreset).toBe(choosePreset);
|
|
71
|
+
act(() => applyPreset("dense"));
|
|
72
|
+
expect(setCookie).toHaveBeenCalledExactlyOnceWith(cookiePreset, "dense", {
|
|
73
|
+
path: "/",
|
|
74
|
+
sameSite: "strict",
|
|
75
|
+
secure: window.location.protocol === "https:",
|
|
76
|
+
maxAge: 60 * 60 * 24 * 365,
|
|
77
|
+
});
|
|
78
|
+
expect(removeCookie).toHaveBeenCalledWith("mp.ov.0", { path: "/" });
|
|
79
|
+
for (const key of writeOverridesCookie({}).toRemove) {
|
|
80
|
+
expect(removeCookie).toHaveBeenCalledWith(key, { path: "/" });
|
|
81
|
+
}
|
|
82
|
+
expect(getPresetChoices()).toEqual({ users: {} });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("records a choice before applying it", () => {
|
|
86
|
+
setCookie.mockImplementation(() => {
|
|
87
|
+
expect(getPresetChoices().users.a).toBe("ocean");
|
|
88
|
+
});
|
|
89
|
+
const { result } = renderHook(() => usePresetSwitch());
|
|
90
|
+
act(() => result.current.choosePreset("ocean", "a"));
|
|
91
|
+
expect(setCookie).toHaveBeenCalled();
|
|
92
|
+
expect(getPresetChoices().owner).toBe("a");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("honours disabled cookies but still remembers an explicit choice", () => {
|
|
96
|
+
vi.stubEnv("COOKIE_POLICY", "DiSaBlEd");
|
|
97
|
+
const { result } = renderHook(() => usePresetSwitch());
|
|
98
|
+
act(() => result.current.choosePreset("paper"));
|
|
99
|
+
expect(setCookie).not.toHaveBeenCalled();
|
|
100
|
+
expect(removeCookie).not.toHaveBeenCalled();
|
|
101
|
+
expect(getPresetChoices().device).toBe("paper");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isWeb } from "@multiplatform.one/platform";
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
import { useCookies } from "react-cookie";
|
|
4
|
+
import { cookiePreset, readPresetCookie, writeOverridesCookie } from "./cookies";
|
|
5
|
+
import { usePresetContext } from "./PresetContext";
|
|
6
|
+
import { recordPresetChoice } from "./presetStore";
|
|
7
|
+
|
|
8
|
+
export interface PresetSwitch {
|
|
9
|
+
/** The provider's active preset; "default" when none is stored. */
|
|
10
|
+
presetName: string;
|
|
11
|
+
/** Switch the active preset without recording anyone's choice. */
|
|
12
|
+
applyPreset(name: string): void;
|
|
13
|
+
/** Switch and record it as this user's choice (no user = this device's signed-out choice). */
|
|
14
|
+
choosePreset(name: string, userId?: string | null): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function usePresetSwitch(): PresetSwitch {
|
|
18
|
+
const isCookieDisabled =
|
|
19
|
+
typeof process !== "undefined" && process.env?.COOKIE_POLICY?.toLowerCase() === "disabled";
|
|
20
|
+
const [cookies, setCookie, removeCookie] = useCookies<string, Record<string, string>>(
|
|
21
|
+
isCookieDisabled ? [] : [cookiePreset],
|
|
22
|
+
);
|
|
23
|
+
const context = usePresetContext();
|
|
24
|
+
const presetName = context?.presetName || readPresetCookie(cookies) || "default";
|
|
25
|
+
|
|
26
|
+
const applyPreset = useCallback(
|
|
27
|
+
(name: string) => {
|
|
28
|
+
if (isCookieDisabled || typeof window === "undefined") return;
|
|
29
|
+
setCookie(cookiePreset, name, {
|
|
30
|
+
path: "/",
|
|
31
|
+
sameSite: "strict",
|
|
32
|
+
secure: isWeb && window.location.protocol === "https:",
|
|
33
|
+
maxAge: 60 * 60 * 24 * 365,
|
|
34
|
+
});
|
|
35
|
+
const { toSet, toRemove } = writeOverridesCookie({});
|
|
36
|
+
for (const key of [...toSet.map(({ key }) => key), ...toRemove]) {
|
|
37
|
+
try {
|
|
38
|
+
removeCookie(key, { path: "/" });
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
[isCookieDisabled, setCookie, removeCookie],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const choosePreset = useCallback(
|
|
46
|
+
(name: string, userId?: string | null) => {
|
|
47
|
+
recordPresetChoice(name, userId);
|
|
48
|
+
applyPreset(name);
|
|
49
|
+
},
|
|
50
|
+
[applyPreset],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return { presetName, applyPreset, choosePreset };
|
|
54
|
+
}
|
|
@@ -103,7 +103,7 @@ describe("responsive knob hydration", () => {
|
|
|
103
103
|
await hydrate();
|
|
104
104
|
expect(observed[0].height).toBe(serverHeight);
|
|
105
105
|
expect(container.querySelector("button")).toBe(serverControl);
|
|
106
|
-
expect(container.querySelector("button")?.getAttribute("data-height")).toBe("
|
|
106
|
+
expect(container.querySelector("button")?.getAttribute("data-height")).toBe("44");
|
|
107
107
|
expect(errors).toEqual([]);
|
|
108
108
|
});
|
|
109
109
|
|
|
@@ -135,7 +135,7 @@ describe("responsive knob hydration", () => {
|
|
|
135
135
|
host.web = false;
|
|
136
136
|
host.touch = true;
|
|
137
137
|
container.innerHTML = renderToString(<Fixture />);
|
|
138
|
-
expect(observed[0].height).toBe(
|
|
138
|
+
expect(observed[0].height).toBe(44);
|
|
139
139
|
expect(errors).toEqual([]);
|
|
140
140
|
});
|
|
141
141
|
});
|