@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
|
@@ -33,15 +33,18 @@ describe("MPO-468: code syntax ink holds Axiom 12 on the code surface", () => {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
it("
|
|
37
|
-
for (const
|
|
38
|
-
|
|
36
|
+
it("keeps stock ink where it holds and darkens only the failing light hues", () => {
|
|
37
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
38
|
+
const theme = themes[scheme];
|
|
39
|
+
for (const hue of hues) {
|
|
40
|
+
const stock = hex(theme[`${hue}11`]);
|
|
41
|
+
const holds = [theme.color3, theme[`${hue}3`]].every(
|
|
42
|
+
(surface) => ratio(stock, surface) >= 4.5,
|
|
43
|
+
);
|
|
44
|
+
if (holds) expect(theme[token(hue)]).toBe(stock);
|
|
45
|
+
else expect(relativeLuminance(theme[token(hue)])).toBeLessThan(relativeLuminance(stock));
|
|
46
|
+
}
|
|
39
47
|
}
|
|
40
|
-
expect(themes.light.syntaxPurple).toBe(hex(themes.light.purple11));
|
|
41
|
-
expect(themes.light.syntaxRed).toBe(hex(themes.light.red11));
|
|
42
|
-
expect(themes.light.syntaxGreen).toBe("#207c54");
|
|
43
|
-
expect(themes.light.syntaxBlue).toBe("#0d6dc3");
|
|
44
|
-
expect(themes.light.syntaxOrange).toBe("#bb4904");
|
|
45
48
|
});
|
|
46
49
|
|
|
47
50
|
it("the tokens ride exactly the themes that carry the hue ramps", () => {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { act, cleanup, render, screen } from "@testing-library/react";
|
|
2
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
3
|
+
import { ThemeProvider } from "./theme.native";
|
|
4
|
+
import { createDefaultThemeConfig } from "./createDefaultThemeConfig";
|
|
5
|
+
import { PresetContext, usePresetContext } from "./PresetContext";
|
|
6
|
+
import { hydrateStoredPreset } from "./presetStore";
|
|
7
|
+
import { boldPreset, defaultPreset } from "./presets";
|
|
8
|
+
|
|
9
|
+
const pending = vi.hoisted(() => ({ finish: undefined as undefined | ((value: string) => void) }));
|
|
10
|
+
vi.mock("@multiplatform.one/store", () => ({
|
|
11
|
+
storage: {
|
|
12
|
+
getItem: (key: string) =>
|
|
13
|
+
key === "mp.preset"
|
|
14
|
+
? new Promise<string>((resolve) => {
|
|
15
|
+
pending.finish = resolve;
|
|
16
|
+
})
|
|
17
|
+
: null,
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
const config = createDefaultThemeConfig({ presets: { bold: boldPreset } });
|
|
21
|
+
afterEach(cleanup);
|
|
22
|
+
|
|
23
|
+
it("holds first paint for async hydration, but leaves ancestor contexts usable", async () => {
|
|
24
|
+
const observed: string[] = [];
|
|
25
|
+
function Probe() {
|
|
26
|
+
const context = usePresetContext();
|
|
27
|
+
observed.push(context!.preset.knobs.borderRadius);
|
|
28
|
+
return <span data-testid="hydrated">{context!.preset.knobs.borderRadius}</span>;
|
|
29
|
+
}
|
|
30
|
+
const ancestor = render(
|
|
31
|
+
<PresetContext.Provider value={{ preset: defaultPreset }}>
|
|
32
|
+
<ThemeProvider config={config}>
|
|
33
|
+
<span>ancestor</span>
|
|
34
|
+
</ThemeProvider>
|
|
35
|
+
</PresetContext.Provider>,
|
|
36
|
+
);
|
|
37
|
+
expect(screen.getByText("ancestor")).toBeDefined();
|
|
38
|
+
render(
|
|
39
|
+
<ThemeProvider config={config}>
|
|
40
|
+
<Probe />
|
|
41
|
+
</ThemeProvider>,
|
|
42
|
+
);
|
|
43
|
+
expect(screen.queryByTestId("hydrated")).toBeNull();
|
|
44
|
+
await act(async () => {
|
|
45
|
+
pending.finish!("bold");
|
|
46
|
+
await hydrateStoredPreset();
|
|
47
|
+
});
|
|
48
|
+
expect(screen.getByTestId("hydrated").textContent).toBe(boldPreset.knobs.borderRadius);
|
|
49
|
+
expect(observed.every((radius) => radius === boldPreset.knobs.borderRadius)).toBe(true);
|
|
50
|
+
ancestor.unmount();
|
|
51
|
+
});
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { ToastProvider } from "@tamagui/toast";
|
|
2
|
-
import { useContext, useMemo } from "react";
|
|
3
|
-
import {
|
|
2
|
+
import { Fragment, useContext, useMemo, useSyncExternalStore } from "react";
|
|
3
|
+
import {
|
|
4
|
+
type CreateTamaguiProps,
|
|
5
|
+
TamaguiProvider,
|
|
6
|
+
type TamaguiProviderProps,
|
|
7
|
+
Theme,
|
|
8
|
+
} from "tamagui";
|
|
4
9
|
import type { PropsWithChildren } from "react";
|
|
5
10
|
import { CornerSmoothingStyles } from "./cornerSmoothing";
|
|
6
11
|
import { FontKnobStyles } from "./FontKnobStyles";
|
|
12
|
+
import type { Knobs } from "./knobs";
|
|
7
13
|
import { PresetContext, type PresetContextValue } from "./PresetContext";
|
|
14
|
+
import { getStoredPreset, subscribeStoredPreset } from "./presetStore";
|
|
8
15
|
import { defaultPreset } from "./presets";
|
|
9
|
-
import {
|
|
16
|
+
import { resolveThemeScale, useActiveScale } from "./scale/useActiveScale";
|
|
17
|
+
import { type ThemeConfig, getPreset } from "./shared";
|
|
18
|
+
import { useSchemePreference } from "./useSchemePreference";
|
|
10
19
|
|
|
11
20
|
// ---------------------------------------------------------------------------
|
|
12
21
|
// ThemeProviderProps (simplified — knob-based theming)
|
|
@@ -17,7 +26,7 @@ export interface ThemeProviderProps<
|
|
|
17
26
|
> extends PropsWithChildren<Omit<TamaguiProviderProps, "config" | "defaultTheme">> {
|
|
18
27
|
config?: {
|
|
19
28
|
tamagui: ReturnType<typeof import("tamagui").createTamagui<T>>;
|
|
20
|
-
}
|
|
29
|
+
} & Partial<Pick<ThemeConfig, "scales" | "defaultPreset">>;
|
|
21
30
|
systemTheme?: "light" | "dark";
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -25,39 +34,65 @@ export interface ThemeProviderProps<
|
|
|
25
34
|
// ThemeProvider (simplified — TamaguiProvider + scheme + ToastProvider)
|
|
26
35
|
// ---------------------------------------------------------------------------
|
|
27
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Native resolves token values (`.val`) at render and only theme subscribers
|
|
39
|
+
* re-render on a config change, so a preset whose scale differs remounts the
|
|
40
|
+
* subtree under the provider (keyed on the scale id). That is a settings-level
|
|
41
|
+
* action: local component state under the provider does not survive it.
|
|
42
|
+
* The preset persists in `@multiplatform.one/store` storage (`mp.preset`);
|
|
43
|
+
* first render waits for storage so a relaunch paints the stored scale.
|
|
44
|
+
*/
|
|
28
45
|
export function ThemeProvider<T extends CreateTamaguiProps>({
|
|
29
46
|
children,
|
|
30
47
|
config,
|
|
31
|
-
systemTheme
|
|
48
|
+
systemTheme,
|
|
32
49
|
...tamaguiProps
|
|
33
50
|
}: ThemeProviderProps<T>) {
|
|
34
|
-
const
|
|
51
|
+
const scheme = useSchemePreference();
|
|
52
|
+
const activeTheme = systemTheme ?? scheme.value;
|
|
53
|
+
const ancestor = useContext(PresetContext);
|
|
54
|
+
const stored = useSyncExternalStore(subscribeStoredPreset, getStoredPreset, getStoredPreset);
|
|
55
|
+
const presetName = (ancestor ? ancestor.presetName : stored.presetName) ?? config?.defaultPreset;
|
|
56
|
+
const scaleId = useActiveScale(config, presetName);
|
|
57
|
+
const scaleConfig = resolveThemeScale(config, presetName)?.tamagui() ?? config?.tamagui;
|
|
35
58
|
|
|
36
59
|
return (
|
|
37
60
|
<TamaguiProvider
|
|
38
61
|
{...tamaguiProps}
|
|
39
|
-
config={config
|
|
62
|
+
config={scaleConfig as TamaguiProviderProps["config"]}
|
|
40
63
|
defaultTheme={activeTheme}
|
|
41
64
|
disableInjectCSS={false}
|
|
42
65
|
>
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
66
|
+
{ancestor || stored.hydrated ? (
|
|
67
|
+
<Fragment key={scaleId}>
|
|
68
|
+
<ToastProvider>
|
|
69
|
+
<KnobBridge presetName={presetName} overrides={stored.overrides} scaleId={scaleId}>
|
|
70
|
+
{children}
|
|
71
|
+
</KnobBridge>
|
|
72
|
+
</ToastProvider>
|
|
73
|
+
</Fragment>
|
|
74
|
+
) : null}
|
|
46
75
|
</TamaguiProvider>
|
|
47
76
|
);
|
|
48
77
|
}
|
|
49
78
|
|
|
50
|
-
function KnobBridge({
|
|
79
|
+
function KnobBridge({
|
|
80
|
+
children,
|
|
81
|
+
presetName,
|
|
82
|
+
overrides,
|
|
83
|
+
scaleId,
|
|
84
|
+
}: PropsWithChildren<{ presetName?: string; overrides: Partial<Knobs>; scaleId: string }>) {
|
|
51
85
|
const existingCtx = useContext(PresetContext);
|
|
52
|
-
const [knobs] = useTheme();
|
|
53
86
|
|
|
54
|
-
const presetCtxValue = useMemo<PresetContextValue>(
|
|
55
|
-
()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
87
|
+
const presetCtxValue = useMemo<PresetContextValue>(() => {
|
|
88
|
+
const base = (presetName ? getPreset(presetName) : undefined) ?? defaultPreset;
|
|
89
|
+
return {
|
|
90
|
+
preset: { ...base, knobs: { ...base.knobs, ...overrides } },
|
|
91
|
+
overrides,
|
|
92
|
+
presetName,
|
|
93
|
+
scaleId,
|
|
94
|
+
};
|
|
95
|
+
}, [presetName, overrides, scaleId]);
|
|
61
96
|
|
|
62
97
|
const tree = (
|
|
63
98
|
<>
|
|
@@ -67,9 +102,12 @@ function KnobBridge({ children }: PropsWithChildren) {
|
|
|
67
102
|
</>
|
|
68
103
|
);
|
|
69
104
|
|
|
105
|
+
const theme = (existingCtx ?? presetCtxValue).preset.theme;
|
|
106
|
+
const themedTree = theme ? <Theme name={theme}>{tree}</Theme> : tree;
|
|
107
|
+
|
|
70
108
|
if (existingCtx) {
|
|
71
|
-
return
|
|
109
|
+
return themedTree;
|
|
72
110
|
}
|
|
73
111
|
|
|
74
|
-
return <PresetContext.Provider value={presetCtxValue}>{
|
|
112
|
+
return <PresetContext.Provider value={presetCtxValue}>{themedTree}</PresetContext.Provider>;
|
|
75
113
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { act, cleanup, render, screen } from "@testing-library/react";
|
|
2
|
+
import Cookies from "universal-cookie";
|
|
3
|
+
import { CookiesProvider } from "react-cookie";
|
|
4
|
+
import { afterEach, expect, it } from "vitest";
|
|
5
|
+
import { useTheme, useThemeName } from "tamagui";
|
|
6
|
+
import { ThemeProvider as WebProvider } from "./theme";
|
|
7
|
+
import { ThemeProvider as NativeProvider } from "./theme.native";
|
|
8
|
+
import { createDefaultThemeConfig } from "./createDefaultThemeConfig";
|
|
9
|
+
import { PresetContext } from "./PresetContext";
|
|
10
|
+
import { boldPreset, defaultPreset } from "./presets";
|
|
11
|
+
import { setStoredPreset } from "./presetStore";
|
|
12
|
+
import { usePresetSwitch } from "./usePresetSwitch";
|
|
13
|
+
|
|
14
|
+
const config = createDefaultThemeConfig({ presets: { bold: boldPreset, default: defaultPreset } });
|
|
15
|
+
afterEach(cleanup);
|
|
16
|
+
function Probe() {
|
|
17
|
+
const theme = useTheme();
|
|
18
|
+
return (
|
|
19
|
+
<output data-testid="palette" data-color={String(theme.color5?.val)}>
|
|
20
|
+
{useThemeName()}
|
|
21
|
+
</output>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
function WebSwitch() {
|
|
25
|
+
const picker = usePresetSwitch();
|
|
26
|
+
return <button onClick={() => picker.applyPreset("bold")}>Bold</button>;
|
|
27
|
+
}
|
|
28
|
+
it.each([false, true])(
|
|
29
|
+
"applies selected preset palette to web descendants (ancestor: %s)",
|
|
30
|
+
(ancestor) => {
|
|
31
|
+
render(
|
|
32
|
+
<CookiesProvider cookies={new Cookies({ "mp.preset": ancestor ? "default" : "bold" })}>
|
|
33
|
+
<PresetContext.Provider value={ancestor ? { preset: boldPreset } : null}>
|
|
34
|
+
<WebProvider config={config} systemTheme="light">
|
|
35
|
+
<Probe />
|
|
36
|
+
</WebProvider>
|
|
37
|
+
</PresetContext.Provider>
|
|
38
|
+
</CookiesProvider>,
|
|
39
|
+
);
|
|
40
|
+
expect(screen.getByTestId("palette").textContent).toBe("light_pink");
|
|
41
|
+
expect(screen.getByTestId("palette").getAttribute("data-color")).toBe(
|
|
42
|
+
String(config.tamagui.themes.light_pink.color5.val),
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
it.each([false, true])(
|
|
47
|
+
"applies selected preset palette to native descendants (ancestor: %s)",
|
|
48
|
+
(ancestor) => {
|
|
49
|
+
setStoredPreset(ancestor ? "default" : "bold");
|
|
50
|
+
render(
|
|
51
|
+
<PresetContext.Provider value={ancestor ? { preset: boldPreset } : null}>
|
|
52
|
+
<NativeProvider config={config} systemTheme="dark">
|
|
53
|
+
<Probe />
|
|
54
|
+
</NativeProvider>
|
|
55
|
+
</PresetContext.Provider>,
|
|
56
|
+
);
|
|
57
|
+
expect(screen.getByTestId("palette").textContent).toBe("dark_pink");
|
|
58
|
+
expect(screen.getByTestId("palette").getAttribute("data-color")).toBe(
|
|
59
|
+
String(config.tamagui.themes.dark_pink.color5.val),
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
it("changes the live palette as well as the preset metadata", () => {
|
|
64
|
+
render(
|
|
65
|
+
<CookiesProvider cookies={new Cookies({})}>
|
|
66
|
+
<WebProvider config={config} systemTheme="light">
|
|
67
|
+
<Probe />
|
|
68
|
+
<WebSwitch />
|
|
69
|
+
</WebProvider>
|
|
70
|
+
</CookiesProvider>,
|
|
71
|
+
);
|
|
72
|
+
const initial = screen.getByTestId("palette").getAttribute("data-color");
|
|
73
|
+
act(() => screen.getByText("Bold").click());
|
|
74
|
+
expect(screen.getByTestId("palette").textContent).toBe("light_pink");
|
|
75
|
+
expect(screen.getByTestId("palette").getAttribute("data-color")).not.toBe(initial);
|
|
76
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
3
|
+
import { ThemeProvider } from "./theme.native";
|
|
4
|
+
import { createDefaultThemeConfig } from "./createDefaultThemeConfig";
|
|
5
|
+
import { getStoredPreset, setStoredPreset } from "./presetStore";
|
|
6
|
+
import { usePresetContext, PresetContext } from "./PresetContext";
|
|
7
|
+
import { useResolvedKnobs } from "./useResolvedKnobs";
|
|
8
|
+
import { usePresetSwitch } from "./usePresetSwitch.native";
|
|
9
|
+
import { boldPreset, defaultPreset } from "./presets";
|
|
10
|
+
|
|
11
|
+
const backend = vi.hoisted(() => ({ data: new Map<string, string>([["mp.preset", "bold"]]) }));
|
|
12
|
+
vi.mock("@multiplatform.one/store", () => ({
|
|
13
|
+
storage: {
|
|
14
|
+
getItem: (key: string) => backend.data.get(key) ?? null,
|
|
15
|
+
setItem: (key: string, value: string) => {
|
|
16
|
+
backend.data.set(key, value);
|
|
17
|
+
},
|
|
18
|
+
removeItem: (key: string) => {
|
|
19
|
+
backend.data.delete(key);
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
const config = createDefaultThemeConfig({ presets: { bold: boldPreset, default: defaultPreset } });
|
|
24
|
+
afterEach(cleanup);
|
|
25
|
+
function Probe() {
|
|
26
|
+
const context = usePresetContext();
|
|
27
|
+
const { knobProps } = useResolvedKnobs();
|
|
28
|
+
const picker = usePresetSwitch();
|
|
29
|
+
return (
|
|
30
|
+
<button
|
|
31
|
+
data-testid="probe"
|
|
32
|
+
data-radius={String(knobProps.borderRadius.borderRadius)}
|
|
33
|
+
onClick={() => picker.applyPreset("default")}
|
|
34
|
+
>
|
|
35
|
+
{context?.preset.knobs.borderRadius}:{context?.preset.tints.join(",")}
|
|
36
|
+
</button>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
it("paints hydrated preset metadata on first mount and switches resolved knobs live across remount", () => {
|
|
40
|
+
const first = render(
|
|
41
|
+
<ThemeProvider config={config}>
|
|
42
|
+
<Probe />
|
|
43
|
+
</ThemeProvider>,
|
|
44
|
+
);
|
|
45
|
+
expect(screen.getByTestId("probe").textContent).toBe(
|
|
46
|
+
`${boldPreset.knobs.borderRadius}:${boldPreset.tints.join(",")}`,
|
|
47
|
+
);
|
|
48
|
+
const boldRadius = screen.getByTestId("probe").getAttribute("data-radius");
|
|
49
|
+
fireEvent.click(screen.getByTestId("probe"));
|
|
50
|
+
expect(screen.getByTestId("probe").textContent).toBe(
|
|
51
|
+
`${defaultPreset.knobs.borderRadius}:${defaultPreset.tints.join(",")}`,
|
|
52
|
+
);
|
|
53
|
+
expect(screen.getByTestId("probe").getAttribute("data-radius")).not.toBe(boldRadius);
|
|
54
|
+
first.unmount();
|
|
55
|
+
render(
|
|
56
|
+
<ThemeProvider config={config}>
|
|
57
|
+
<Probe />
|
|
58
|
+
</ThemeProvider>,
|
|
59
|
+
);
|
|
60
|
+
expect(getStoredPreset().presetName).toBe("default");
|
|
61
|
+
expect(screen.getByTestId("probe").textContent).toContain(defaultPreset.knobs.borderRadius);
|
|
62
|
+
});
|
|
63
|
+
it("preserves an ancestor preset context", () => {
|
|
64
|
+
act(() => setStoredPreset("default"));
|
|
65
|
+
render(
|
|
66
|
+
<PresetContext.Provider value={{ preset: boldPreset }}>
|
|
67
|
+
<ThemeProvider config={config}>
|
|
68
|
+
<Probe />
|
|
69
|
+
</ThemeProvider>
|
|
70
|
+
</PresetContext.Provider>,
|
|
71
|
+
);
|
|
72
|
+
expect(screen.getByTestId("probe").textContent).toContain(boldPreset.knobs.borderRadius);
|
|
73
|
+
});
|
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
4
|
+
import { act, cleanup, render } from "@testing-library/react";
|
|
5
|
+
import { createElement, useEffect, useState } from "react";
|
|
6
|
+
import { Cookies, CookiesProvider } from "react-cookie";
|
|
7
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
|
|
9
|
+
vi.mock("./animations/index", () => ({ animations: {} }));
|
|
10
|
+
|
|
11
|
+
const { items } = vi.hoisted(() => ({ items: new Map<string, string>() }));
|
|
12
|
+
vi.mock("@multiplatform.one/store", () => ({
|
|
13
|
+
storage: {
|
|
14
|
+
getItem: (key: string) => items.get(key) ?? null,
|
|
15
|
+
setItem: (key: string, value: string) => {
|
|
16
|
+
items.set(key, value);
|
|
17
|
+
},
|
|
18
|
+
removeItem: (key: string) => {
|
|
19
|
+
items.delete(key);
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
beforeEach(() => items.clear());
|
|
25
|
+
|
|
26
|
+
import { useResolvedKnobs } from "./useResolvedKnobs";
|
|
5
27
|
|
|
6
28
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
29
|
|
|
@@ -31,3 +53,123 @@ describe("ThemeProvider knob injectors", () => {
|
|
|
31
53
|
expect(src).toContain("PresetContext.Provider");
|
|
32
54
|
});
|
|
33
55
|
});
|
|
56
|
+
|
|
57
|
+
// ── Runtime preset scale switching ───────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
describe("ThemeProvider preset scale (web)", () => {
|
|
60
|
+
it("sets <html data-mp-scale> from the mp.preset cookie and keeps mounted state across a switch", async () => {
|
|
61
|
+
const { ThemeProvider } = await import("./theme");
|
|
62
|
+
const { themeConfig, cookies, mounts, values, reset } = await scaleHarness();
|
|
63
|
+
cookies.set("mp.preset", "dense", { path: "/" });
|
|
64
|
+
|
|
65
|
+
const view = render(
|
|
66
|
+
createElement(
|
|
67
|
+
CookiesProvider,
|
|
68
|
+
{ cookies },
|
|
69
|
+
createElement(
|
|
70
|
+
ThemeProvider,
|
|
71
|
+
{ config: themeConfig, systemTheme: "light" },
|
|
72
|
+
createElement(Probe),
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
expect(document.documentElement.getAttribute("data-mp-scale")).toBe("dense");
|
|
77
|
+
expect(values.control).toBe(36);
|
|
78
|
+
expect(mounts.count).toBe(1);
|
|
79
|
+
act(() => values.setTyped?.("typed"));
|
|
80
|
+
expect(values.typed).toBe("typed");
|
|
81
|
+
const sheet = document.querySelector('style[data-href="mp-scales"], style[href="mp-scales"]');
|
|
82
|
+
expect(sheet?.textContent).toContain(':root[data-mp-scale="dense"]');
|
|
83
|
+
|
|
84
|
+
act(() => cookies.set("mp.preset", "roomy", { path: "/" }));
|
|
85
|
+
expect(document.documentElement.getAttribute("data-mp-scale")).toBe("roomy");
|
|
86
|
+
expect(values.control).toBe(48);
|
|
87
|
+
expect(mounts.count).toBe(1);
|
|
88
|
+
expect(values.typed).toBe("typed");
|
|
89
|
+
|
|
90
|
+
view.unmount();
|
|
91
|
+
reset();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("ThemeProvider preset scale (native twin)", () => {
|
|
96
|
+
it("remounts the subtree, keyed on the scale id, when the stored preset changes scale", async () => {
|
|
97
|
+
const { ThemeProvider } = await import("./theme.native");
|
|
98
|
+
const { setStoredPreset } = await import("./presetStore");
|
|
99
|
+
const { themeConfig, mounts, values, reset } = await scaleHarness();
|
|
100
|
+
setStoredPreset(undefined);
|
|
101
|
+
|
|
102
|
+
const view = render(
|
|
103
|
+
createElement(
|
|
104
|
+
ThemeProvider,
|
|
105
|
+
{ config: themeConfig, systemTheme: "light" },
|
|
106
|
+
createElement(Probe),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
expect(values.control).toBe(40);
|
|
110
|
+
expect(mounts.count).toBe(1);
|
|
111
|
+
|
|
112
|
+
act(() => setStoredPreset("dense"));
|
|
113
|
+
expect(values.control).toBe(36);
|
|
114
|
+
expect(mounts.count).toBe(2);
|
|
115
|
+
|
|
116
|
+
act(() => setStoredPreset("hero"));
|
|
117
|
+
expect(values.control).toBe(40);
|
|
118
|
+
expect(mounts.count).toBe(3);
|
|
119
|
+
|
|
120
|
+
act(() => setStoredPreset(""));
|
|
121
|
+
expect(mounts.count).toBe(3);
|
|
122
|
+
|
|
123
|
+
await vi.waitFor(() => expect(items.has("mp.preset")).toBe(false));
|
|
124
|
+
act(() => setStoredPreset("dense"));
|
|
125
|
+
await vi.waitFor(() => expect(items.get("mp.preset")).toBe("dense"));
|
|
126
|
+
|
|
127
|
+
view.unmount();
|
|
128
|
+
act(() => setStoredPreset(undefined));
|
|
129
|
+
reset();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const probeState: {
|
|
134
|
+
mounts: { count: number };
|
|
135
|
+
values: { control?: number; typed?: string; setTyped?: (value: string) => void };
|
|
136
|
+
} = { mounts: { count: 0 }, values: {} };
|
|
137
|
+
|
|
138
|
+
function Probe() {
|
|
139
|
+
const [typed, setTyped] = useState("");
|
|
140
|
+
const { knobProps } = useResolvedKnobs();
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
probeState.mounts.count += 1;
|
|
143
|
+
}, []);
|
|
144
|
+
probeState.values.control = knobProps.control.height as number;
|
|
145
|
+
probeState.values.typed = typed;
|
|
146
|
+
probeState.values.setTyped = setTyped;
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function scaleHarness() {
|
|
151
|
+
const { createDefaultThemeConfig } = await import("./createDefaultThemeConfig");
|
|
152
|
+
const { densePreset, heroPreset, roomyPreset } = await import("./presets");
|
|
153
|
+
const { applyThemeScale } = await import("./scale/useActiveScale");
|
|
154
|
+
const themeConfig = createDefaultThemeConfig({
|
|
155
|
+
presets: { hero: heroPreset, dense: densePreset, roomy: roomyPreset },
|
|
156
|
+
});
|
|
157
|
+
probeState.mounts.count = 0;
|
|
158
|
+
probeState.values = {};
|
|
159
|
+
for (const cookie of document.cookie.split(";")) {
|
|
160
|
+
const name = cookie.split("=")[0]?.trim();
|
|
161
|
+
if (name) document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
themeConfig,
|
|
165
|
+
cookies: new Cookies(),
|
|
166
|
+
mounts: probeState.mounts,
|
|
167
|
+
values: probeState.values,
|
|
168
|
+
reset: () => {
|
|
169
|
+
cleanup();
|
|
170
|
+
document.documentElement.removeAttribute("data-mp-scale");
|
|
171
|
+
const defaults = themeConfig.scales?.default;
|
|
172
|
+
if (defaults) applyThemeScale(defaults);
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|