@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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useState } from "react";
|
|
2
|
+
import type { ThemeConfig } from "../shared";
|
|
3
|
+
import { setSizeRecipeInputs } from "../sizeRecipes";
|
|
4
|
+
import { setActiveScaleTokens } from "./activeScale";
|
|
5
|
+
import { installScaleConfig } from "./installScaleConfig";
|
|
6
|
+
import { DEFAULT_SCALE_ID, type ThemeScale } from "./themeScales";
|
|
7
|
+
|
|
8
|
+
/** What a provider needs to pick a scale: the config's scales and its default preset name. */
|
|
9
|
+
export type ScaleSource = Partial<Pick<ThemeConfig, "scales" | "defaultPreset">>;
|
|
10
|
+
|
|
11
|
+
/** The px maps `globalThis.__MPO_SCALE__` publishes for in-page audits. */
|
|
12
|
+
export type PublishedScale = Pick<ThemeScale, "id" | "inputs" | "tokens" | "fonts"> & {
|
|
13
|
+
size: ThemeScale["tokens"]["size"];
|
|
14
|
+
space: ThemeScale["tokens"]["space"];
|
|
15
|
+
radius: ThemeScale["tokens"]["radius"];
|
|
16
|
+
type: number;
|
|
17
|
+
controlGaps: number[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
21
|
+
|
|
22
|
+
let appliedScale: ThemeScale | undefined;
|
|
23
|
+
|
|
24
|
+
/** The scale a preset name resolves to; unknown and empty names take the default preset's. */
|
|
25
|
+
export function resolveThemeScale(
|
|
26
|
+
config: ScaleSource | undefined,
|
|
27
|
+
presetName?: string,
|
|
28
|
+
): ThemeScale | undefined {
|
|
29
|
+
const scales = config?.scales;
|
|
30
|
+
if (!scales) return undefined;
|
|
31
|
+
const name = presetName || config.defaultPreset || DEFAULT_SCALE_ID;
|
|
32
|
+
return scales[name] ?? scales[DEFAULT_SCALE_ID];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function publishScale(scale: ThemeScale): void {
|
|
36
|
+
const published: PublishedScale = {
|
|
37
|
+
id: scale.id,
|
|
38
|
+
inputs: scale.inputs,
|
|
39
|
+
tokens: scale.tokens,
|
|
40
|
+
fonts: scale.fonts,
|
|
41
|
+
size: scale.tokens.size,
|
|
42
|
+
space: scale.tokens.space,
|
|
43
|
+
radius: scale.tokens.radius,
|
|
44
|
+
type: scale.inputs.type,
|
|
45
|
+
controlGaps: [
|
|
46
|
+
...new Set(
|
|
47
|
+
[
|
|
48
|
+
...Object.values(scale.recipeInputs.tokenTable),
|
|
49
|
+
...Object.values(scale.recipeInputs.chipTokenTable),
|
|
50
|
+
].map((row) => row.radius),
|
|
51
|
+
),
|
|
52
|
+
].sort((a, b) => a - b),
|
|
53
|
+
};
|
|
54
|
+
(globalThis as { __MPO_SCALE__?: PublishedScale }).__MPO_SCALE__ = published;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Install `scale` for every JS reader: its Tamagui config becomes the global
|
|
59
|
+
* one (`getTokenValue`, native `.val` resolution) and its recipe inputs
|
|
60
|
+
* rewrite the live size-recipe tables (`knobProps.control`, Button/Input).
|
|
61
|
+
* Idempotent; returns true when anything changed.
|
|
62
|
+
*/
|
|
63
|
+
export function applyThemeScale(scale: ThemeScale): boolean {
|
|
64
|
+
const configChanged = installScaleConfig(scale.tamagui());
|
|
65
|
+
const scaleChanged = appliedScale !== scale;
|
|
66
|
+
if (configChanged || scaleChanged) setSizeRecipeInputs(scale.recipeInputs);
|
|
67
|
+
setActiveScaleTokens(scale.tokens);
|
|
68
|
+
appliedScale = scale;
|
|
69
|
+
publishScale(scale);
|
|
70
|
+
return configChanged || scaleChanged;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Keep the active preset's scale installed and return its id.
|
|
75
|
+
*
|
|
76
|
+
* The first render installs it synchronously, so SSR markup and the
|
|
77
|
+
* hydrating render read the same recipe tables. Later switches install in a
|
|
78
|
+
* layout effect and then return the new id, so a context keyed on it
|
|
79
|
+
* re-renders every reader after the tables changed and before paint.
|
|
80
|
+
*/
|
|
81
|
+
export function useActiveScale(config: ScaleSource | undefined, presetName?: string): string {
|
|
82
|
+
const scale = resolveThemeScale(config, presetName);
|
|
83
|
+
const [active, setActive] = useState(() => {
|
|
84
|
+
if (scale) applyThemeScale(scale);
|
|
85
|
+
return scale?.id ?? DEFAULT_SCALE_ID;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
useIsomorphicLayoutEffect(() => {
|
|
89
|
+
if (!scale) return;
|
|
90
|
+
applyThemeScale(scale);
|
|
91
|
+
setActive(scale.id);
|
|
92
|
+
}, [scale]);
|
|
93
|
+
|
|
94
|
+
return active;
|
|
95
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { beforeEach, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const backend = vi.hoisted(() => ({
|
|
4
|
+
data: new Map<string, string>(),
|
|
5
|
+
getItem: vi.fn(),
|
|
6
|
+
setItem: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("@multiplatform.one/store", () => ({ storage: backend }));
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.resetModules();
|
|
11
|
+
backend.data.clear();
|
|
12
|
+
backend.getItem.mockReset().mockImplementation((key) => backend.data.get(key) ?? null);
|
|
13
|
+
backend.setItem.mockReset().mockImplementation((key, value) => {
|
|
14
|
+
backend.data.set(key, value);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("persists the scheme under the existing preload key across a fresh module", async () => {
|
|
19
|
+
let store = await import("./schemeStore");
|
|
20
|
+
store.setStoredScheme("dark");
|
|
21
|
+
await vi.waitFor(() => expect(backend.data.get("vxrn-scheme")).toBe("dark"));
|
|
22
|
+
vi.resetModules();
|
|
23
|
+
store = await import("./schemeStore");
|
|
24
|
+
await store.hydrateStoredScheme();
|
|
25
|
+
expect(store.getStoredScheme()).toBe("dark");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("ignores invalid storage and protects early choices from late hydration", async () => {
|
|
29
|
+
backend.data.set("vxrn-scheme", "invalid");
|
|
30
|
+
let store = await import("./schemeStore");
|
|
31
|
+
await store.hydrateStoredScheme();
|
|
32
|
+
expect(store.getStoredScheme()).toBe("system");
|
|
33
|
+
vi.resetModules();
|
|
34
|
+
let resolve!: (value: string) => void;
|
|
35
|
+
backend.getItem.mockImplementation(
|
|
36
|
+
() =>
|
|
37
|
+
new Promise((r) => {
|
|
38
|
+
resolve = r;
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
store = await import("./schemeStore");
|
|
42
|
+
const hydration = store.hydrateStoredScheme();
|
|
43
|
+
store.setStoredScheme("light");
|
|
44
|
+
resolve("dark");
|
|
45
|
+
await hydration;
|
|
46
|
+
expect(store.getStoredScheme()).toBe("light");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("serializes scheme writes and recovers after a rejected write", async () => {
|
|
50
|
+
let finish!: () => void;
|
|
51
|
+
backend.setItem.mockImplementationOnce(
|
|
52
|
+
() =>
|
|
53
|
+
new Promise<void>((resolve) => {
|
|
54
|
+
finish = resolve;
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
const store = await import("./schemeStore");
|
|
58
|
+
store.setStoredScheme("dark");
|
|
59
|
+
store.setStoredScheme("light");
|
|
60
|
+
await vi.waitFor(() => expect(backend.setItem).toHaveBeenCalledTimes(1));
|
|
61
|
+
finish();
|
|
62
|
+
await vi.waitFor(() => expect(backend.data.get("vxrn-scheme")).toBe("light"));
|
|
63
|
+
backend.setItem.mockRejectedValueOnce(new Error("offline"));
|
|
64
|
+
store.setStoredScheme("dark");
|
|
65
|
+
store.setStoredScheme("system");
|
|
66
|
+
await vi.waitFor(() => expect(backend.data.get("vxrn-scheme")).toBe("system"));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("does not hydrate or mutate a process-wide preference on web SSR", async () => {
|
|
70
|
+
const store = await import("./schemeStore");
|
|
71
|
+
vi.stubGlobal("window", undefined);
|
|
72
|
+
try {
|
|
73
|
+
store.setStoredScheme("dark");
|
|
74
|
+
await store.hydrateStoredScheme();
|
|
75
|
+
expect(store.getStoredScheme()).toBe("system");
|
|
76
|
+
expect(backend.getItem).not.toHaveBeenCalled();
|
|
77
|
+
expect(backend.setItem).not.toHaveBeenCalled();
|
|
78
|
+
} finally {
|
|
79
|
+
vi.unstubAllGlobals();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isWeb } from "@multiplatform.one/platform";
|
|
2
|
+
import { storage } from "@multiplatform.one/store";
|
|
3
|
+
import type { ThemeScheme } from "./useTheme";
|
|
4
|
+
|
|
5
|
+
const schemeKey = "vxrn-scheme";
|
|
6
|
+
const listeners = new Set<() => void>();
|
|
7
|
+
let setting: ThemeScheme = "system";
|
|
8
|
+
let hydration: Promise<void> | undefined;
|
|
9
|
+
let written = false;
|
|
10
|
+
let writes = Promise.resolve();
|
|
11
|
+
const isServer = () => isWeb && typeof window === "undefined";
|
|
12
|
+
|
|
13
|
+
function emit(value: ThemeScheme) {
|
|
14
|
+
if (value === setting) return;
|
|
15
|
+
setting = value;
|
|
16
|
+
listeners.forEach((listener) => listener());
|
|
17
|
+
}
|
|
18
|
+
function adopt(value: unknown) {
|
|
19
|
+
if (!written) emit(value === "light" || value === "dark" ? value : "system");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hydrateStoredScheme(): Promise<void> {
|
|
23
|
+
if (isServer()) return Promise.resolve();
|
|
24
|
+
if (hydration) return hydration;
|
|
25
|
+
let raw: ReturnType<typeof storage.getItem>;
|
|
26
|
+
try {
|
|
27
|
+
raw = storage.getItem(schemeKey);
|
|
28
|
+
} catch {
|
|
29
|
+
raw = null;
|
|
30
|
+
}
|
|
31
|
+
if (!raw || typeof raw === "string") {
|
|
32
|
+
adopt(raw);
|
|
33
|
+
hydration = Promise.resolve();
|
|
34
|
+
} else {
|
|
35
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
36
|
+
const timeout = new Promise<null>((resolve) => {
|
|
37
|
+
timer = setTimeout(() => resolve(null), 1000);
|
|
38
|
+
});
|
|
39
|
+
hydration = Promise.race([raw, timeout])
|
|
40
|
+
.then(adopt)
|
|
41
|
+
.catch(() => adopt(null))
|
|
42
|
+
.finally(() => clearTimeout(timer));
|
|
43
|
+
}
|
|
44
|
+
return hydration;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getStoredScheme(): ThemeScheme {
|
|
48
|
+
if (isServer()) return "system";
|
|
49
|
+
if (!hydration) void hydrateStoredScheme();
|
|
50
|
+
return setting;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function subscribeStoredScheme(listener: () => void): () => void {
|
|
54
|
+
if (isServer()) return () => {};
|
|
55
|
+
if (!hydration) void hydrateStoredScheme();
|
|
56
|
+
listeners.add(listener);
|
|
57
|
+
return () => {
|
|
58
|
+
listeners.delete(listener);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function setStoredScheme(value: ThemeScheme): void {
|
|
63
|
+
if (isServer()) return;
|
|
64
|
+
written = true;
|
|
65
|
+
emit(value);
|
|
66
|
+
writes = writes.then(() => storage.setItem(schemeKey, value)).catch(() => undefined);
|
|
67
|
+
}
|
package/src/theme/shared.spec.ts
CHANGED
|
@@ -65,20 +65,21 @@ describe("resolveThemeFromPreset", () => {
|
|
|
65
65
|
|
|
66
66
|
it("merges overrides on top of preset knobs", () => {
|
|
67
67
|
registerPreset("myPreset", testPreset);
|
|
68
|
-
const resolved = resolveThemeFromPreset("myPreset", {
|
|
69
|
-
expect(resolved.knobs.
|
|
68
|
+
const resolved = resolveThemeFromPreset("myPreset", { elevation: "large" });
|
|
69
|
+
expect(resolved.knobs.elevation).toBe("large");
|
|
70
70
|
expect(resolved.knobs.fillStyle).toBe("outlined");
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
it("falls back to defaultKnobs for unknown preset", () => {
|
|
74
|
-
const resolved = resolveThemeFromPreset("unknown", {
|
|
75
|
-
expect(resolved.knobs.
|
|
74
|
+
const resolved = resolveThemeFromPreset("unknown", { elevation: "none" });
|
|
75
|
+
expect(resolved.knobs.elevation).toBe("none");
|
|
76
76
|
expect(resolved.knobs.fillStyle).toBe(defaultKnobs.fillStyle);
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
it("does not mutate the original preset", () => {
|
|
80
80
|
registerPreset("immutable", testPreset);
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
const before = testPreset.knobs.elevation;
|
|
82
|
+
resolveThemeFromPreset("immutable", { elevation: before === "large" ? "none" : "large" });
|
|
83
|
+
expect(testPreset.knobs.elevation).toBe(before);
|
|
83
84
|
});
|
|
84
85
|
});
|
package/src/theme/shared.tsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { ThemeName } from "@tamagui/web";
|
|
1
|
+
import type { GenericFont, TamaguiInternalConfig, ThemeName } from "@tamagui/web";
|
|
2
2
|
import { type CreateTamaguiProps, createTamagui } from "tamagui";
|
|
3
3
|
import type { Knobs } from "./knobs";
|
|
4
4
|
import { defaultKnobs } from "./knobs";
|
|
5
5
|
import type { Preset } from "./preset.types";
|
|
6
6
|
import type { RecipeFamilies, RecipeInputs } from "./recipeInputs";
|
|
7
|
+
import type { ScaleFonts } from "./scale/generateScale";
|
|
8
|
+
import { type ThemeScale, buildThemeScales } from "./scale/themeScales";
|
|
7
9
|
|
|
8
10
|
// ── Preset registry ───────────────────────────────────────────────────────
|
|
9
11
|
|
|
@@ -90,8 +92,23 @@ export type ThemeConfig<T extends CreateTamaguiProps = CreateTamaguiProps> = {
|
|
|
90
92
|
*/
|
|
91
93
|
recipeInputs?: RecipeInputs;
|
|
92
94
|
recipeFamilies?: RecipeFamilies;
|
|
95
|
+
/**
|
|
96
|
+
* The token scales this config can switch to at runtime, keyed by preset
|
|
97
|
+
* name (`"default"` = the implicit default preset). Presets on equal scale
|
|
98
|
+
* inputs share one entry. Present when the config was built from a
|
|
99
|
+
* generated scale (`createDefaultThemeConfig`).
|
|
100
|
+
*/
|
|
101
|
+
scales?: Record<string, ThemeScale>;
|
|
93
102
|
};
|
|
94
103
|
|
|
104
|
+
/** How `createThemeConfig` builds the preset scales of a generated-scale config. */
|
|
105
|
+
export interface ThemeConfigScaleOptions {
|
|
106
|
+
/** The font map a scale's type tables produce. */
|
|
107
|
+
fonts: (tables: ScaleFonts) => Record<string, GenericFont>;
|
|
108
|
+
/** Consumer recipe overrides that are not scale-derived (tile ladder, touch floor). */
|
|
109
|
+
recipeOverrides?: Partial<RecipeInputs>;
|
|
110
|
+
}
|
|
111
|
+
|
|
95
112
|
/**
|
|
96
113
|
* Semantic / internal sub-theme names that should never appear in a
|
|
97
114
|
* user-facing "Theme color" picker.
|
|
@@ -133,6 +150,7 @@ export function createThemeConfig<T extends CreateTamaguiProps>(
|
|
|
133
150
|
defaultPreset?: string;
|
|
134
151
|
recipeInputs?: RecipeInputs;
|
|
135
152
|
recipeFamilies?: RecipeFamilies;
|
|
153
|
+
scale?: ThemeConfigScaleOptions;
|
|
136
154
|
},
|
|
137
155
|
): ThemeConfig<T> {
|
|
138
156
|
// Extract color names from the raw themes BEFORE createTamagui processes them.
|
|
@@ -140,14 +158,15 @@ export function createThemeConfig<T extends CreateTamaguiProps>(
|
|
|
140
158
|
? extractThemeColors(tamagui.themes as Record<string, unknown>)
|
|
141
159
|
: [];
|
|
142
160
|
|
|
143
|
-
const
|
|
161
|
+
const props = {
|
|
144
162
|
...tamagui,
|
|
145
163
|
settings: {
|
|
146
164
|
disableRootThemeClass: false,
|
|
147
165
|
themeClassNameOnRoot: true,
|
|
148
166
|
...tamagui.settings,
|
|
149
167
|
},
|
|
150
|
-
}
|
|
168
|
+
};
|
|
169
|
+
const tamaguiConfig = createTamagui(props) as ReturnType<typeof createTamagui<T>>;
|
|
151
170
|
|
|
152
171
|
if (options?.presets) {
|
|
153
172
|
for (const [name, preset] of Object.entries(options.presets)) {
|
|
@@ -155,6 +174,20 @@ export function createThemeConfig<T extends CreateTamaguiProps>(
|
|
|
155
174
|
}
|
|
156
175
|
}
|
|
157
176
|
|
|
177
|
+
const scales =
|
|
178
|
+
options?.scale && options.recipeInputs
|
|
179
|
+
? buildThemeScales({
|
|
180
|
+
base: tamaguiConfig as unknown as TamaguiInternalConfig,
|
|
181
|
+
props,
|
|
182
|
+
presets: options.presets,
|
|
183
|
+
defaultPreset: options.defaultPreset,
|
|
184
|
+
recipeInputs: options.recipeInputs,
|
|
185
|
+
recipeOverrides: options.scale.recipeOverrides,
|
|
186
|
+
fonts: options.scale.fonts,
|
|
187
|
+
createConfig: (next) => createTamagui(next) as unknown as TamaguiInternalConfig,
|
|
188
|
+
})
|
|
189
|
+
: undefined;
|
|
190
|
+
|
|
158
191
|
return {
|
|
159
192
|
tamagui: tamaguiConfig,
|
|
160
193
|
presets: options?.presets,
|
|
@@ -162,5 +195,6 @@ export function createThemeConfig<T extends CreateTamaguiProps>(
|
|
|
162
195
|
themeColors,
|
|
163
196
|
recipeInputs: options?.recipeInputs,
|
|
164
197
|
recipeFamilies: options?.recipeFamilies,
|
|
198
|
+
scales,
|
|
165
199
|
};
|
|
166
200
|
}
|
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* (R6: real TABLE, no escape). padX / label / icon / radius at $2/$3/$4/$6
|
|
4
|
-
* come from tamagui-rendered-reference.md §6.1; Input padX is §6.2
|
|
5
|
-
* (one space half-step below the button). Default gap equals size-table
|
|
6
|
-
* radius; borderRadius shape overrides preserve that internal spacing.
|
|
7
|
-
*/
|
|
8
|
-
|
|
1
|
+
import { generateScale } from "./scale/generateScale";
|
|
2
|
+
import { defaultScaleInputs } from "./scale/scaleInputs";
|
|
9
3
|
import { describe, expect, it } from "vitest";
|
|
10
4
|
import {
|
|
5
|
+
DEFAULT_SIZE_RECIPE_INPUTS,
|
|
11
6
|
SIZE_RECIPE_RADII,
|
|
12
7
|
SIZE_RECIPE_RADIUS_STOPS,
|
|
13
|
-
TAMAGUI_CONTROL_SIZE_TABLE,
|
|
14
|
-
TAMAGUI_INPUT_PADX,
|
|
15
8
|
applyRadiusStop,
|
|
16
9
|
generateSizeRecipes,
|
|
17
10
|
recipeFamilies,
|
|
@@ -21,23 +14,19 @@ import {
|
|
|
21
14
|
type SizeRecipeRadiusStop,
|
|
22
15
|
} from "./sizeRecipes";
|
|
23
16
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
$3: { paddingHorizontal: 13, fontSize: 13, iconSize: 14, radius: 7 },
|
|
27
|
-
$4: { paddingHorizontal: 18, fontSize: 14, iconSize: 16, radius: 9 },
|
|
28
|
-
$6: { paddingHorizontal: 32, fontSize: 18, iconSize: 20, radius: 16 },
|
|
29
|
-
} as const;
|
|
17
|
+
const scale = generateScale(defaultScaleInputs);
|
|
18
|
+
const MEASURED = scale.recipeInputs.tokenTable;
|
|
30
19
|
|
|
31
|
-
describe("MPO-46 size ladder —
|
|
32
|
-
it("padX, label, icon and radius at $2/$3/$4/$6 match the
|
|
20
|
+
describe("MPO-46 size ladder — generated table, not a ratio (R6)", () => {
|
|
21
|
+
it("padX, label, icon and radius at $2/$3/$4/$6 match the generated scale", () => {
|
|
33
22
|
for (const token of ["$2", "$3", "$4", "$6"] as const) {
|
|
34
23
|
const recipe = sizeRecipeForToken(token);
|
|
35
|
-
const row = MEASURED[token]
|
|
24
|
+
const row = MEASURED[token]!;
|
|
36
25
|
expect(recipe.paddingHorizontal, `${token} padX`).toBe(row.paddingHorizontal);
|
|
37
26
|
expect(recipe.fontSize, `${token} label`).toBe(row.fontSize);
|
|
38
27
|
expect(recipe.iconSize, `${token} icon`).toBe(row.iconSize);
|
|
39
28
|
expect(SIZE_RECIPE_RADII[token], `${token} radius`).toBe(row.radius);
|
|
40
|
-
expect(
|
|
29
|
+
expect(DEFAULT_SIZE_RECIPE_INPUTS.tokenTable[token]).toEqual(row);
|
|
41
30
|
}
|
|
42
31
|
});
|
|
43
32
|
|
|
@@ -49,15 +38,15 @@ describe("MPO-46 size ladder — measured table, not a ratio (R6)", () => {
|
|
|
49
38
|
expect(SIZE_RECIPE_RADII.$2).not.toBe(SIZE_RECIPE_RADII.$6);
|
|
50
39
|
});
|
|
51
40
|
|
|
52
|
-
it("Button / Select trigger / ListItem $4 is
|
|
41
|
+
it("Button / Select trigger / ListItem $4 is 40 / padX 16 / radius 8; Input padX is 14", () => {
|
|
53
42
|
const button = sizeRecipes.$4;
|
|
54
|
-
expect(button.height).toBe(
|
|
55
|
-
expect(button.paddingHorizontal).toBe(
|
|
56
|
-
expect(SIZE_RECIPE_RADII.$4).toBe(
|
|
57
|
-
expect(
|
|
58
|
-
expect(recipeFamilies.
|
|
59
|
-
expect(recipeFamilies.
|
|
60
|
-
expect(recipeFamilies.
|
|
43
|
+
expect(button.height).toBe(40);
|
|
44
|
+
expect(button.paddingHorizontal).toBe(16);
|
|
45
|
+
expect(SIZE_RECIPE_RADII.$4).toBe(8);
|
|
46
|
+
expect(DEFAULT_SIZE_RECIPE_INPUTS.inputPadX.$4).toBe(14);
|
|
47
|
+
expect(recipeFamilies.chip.recipes.$4.paddingHorizontal).toBe(14);
|
|
48
|
+
expect(recipeFamilies.chip.recipes.$4.height).toBe(40);
|
|
49
|
+
expect(recipeFamilies.chip.radii.$4).toBe(8);
|
|
61
50
|
});
|
|
62
51
|
});
|
|
63
52
|
|
|
@@ -66,8 +55,8 @@ describe("MPO-46 default size-table gap and explicit recipe transformation", ()
|
|
|
66
55
|
const generated = generateSizeRecipes();
|
|
67
56
|
for (const token of Object.keys(generated.recipes)) {
|
|
68
57
|
expect(generated.recipes[token].gap, `control ${token}`).toBe(generated.radii[token]);
|
|
69
|
-
expect(generated.families.
|
|
70
|
-
generated.families.
|
|
58
|
+
expect(generated.families.chip.recipes[token].gap, `compact ${token}`).toBe(
|
|
59
|
+
generated.families.chip.radii[token],
|
|
71
60
|
);
|
|
72
61
|
}
|
|
73
62
|
});
|
|
@@ -85,17 +74,17 @@ describe("MPO-46 default size-table gap and explicit recipe transformation", ()
|
|
|
85
74
|
});
|
|
86
75
|
|
|
87
76
|
describe("MPO-46 resolveControlRadius", () => {
|
|
88
|
-
it("default medium follows the size table, not the
|
|
77
|
+
it("default medium follows the size table, not the base radius knob", () => {
|
|
89
78
|
const two = resolveControlRadius(sizeRecipeForToken("$2"), "medium");
|
|
90
|
-
expect(two.radius).toBe(
|
|
91
|
-
expect(two.gap).toBe(
|
|
79
|
+
expect(two.radius).toBe(scale.tokens.radius[2]);
|
|
80
|
+
expect(two.gap).toBe(scale.tokens.radius[2]);
|
|
92
81
|
const six = resolveControlRadius(sizeRecipeForToken("$6"), "medium");
|
|
93
|
-
expect(six.radius).toBe(
|
|
94
|
-
expect(six.gap).toBe(
|
|
82
|
+
expect(six.radius).toBe(scale.tokens.radius[6]);
|
|
83
|
+
expect(six.gap).toBe(scale.tokens.radius[6]);
|
|
95
84
|
});
|
|
96
85
|
|
|
97
86
|
it("corner shape preserves the size recipe's icon-label grouping", () => {
|
|
98
|
-
for (const family of ["control", "
|
|
87
|
+
for (const family of ["control", "chip"] as const) {
|
|
99
88
|
for (const token of ["$2", "$3", "$4", "$6"]) {
|
|
100
89
|
const recipe = sizeRecipeForToken(token, { family });
|
|
101
90
|
for (const stop of Object.keys(SIZE_RECIPE_RADIUS_STOPS) as SizeRecipeRadiusStop[]) {
|
|
@@ -111,3 +100,15 @@ describe("MPO-46 resolveControlRadius", () => {
|
|
|
111
100
|
}
|
|
112
101
|
});
|
|
113
102
|
});
|
|
103
|
+
|
|
104
|
+
describe("one touch ladder", () => {
|
|
105
|
+
it("floors the generated height and preserves every other channel", () => {
|
|
106
|
+
for (const token of Object.keys(sizeRecipes)) {
|
|
107
|
+
const recipe = sizeRecipes[token];
|
|
108
|
+
expect(sizeRecipeForToken(token, { touch: true })).toEqual({
|
|
109
|
+
...recipe,
|
|
110
|
+
height: Math.max(recipe.height, 44),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|