@multiplatform.one/theme 7.90.6 → 8.0.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/dist/cjs/index.gnome.cjs +432 -0
- package/dist/cjs/index.gnome.native.js +435 -0
- package/dist/cjs/index.gnome.native.js.map +1 -0
- package/dist/cjs/{theme/useColorScale.native.js → nativeTwins.cjs} +11 -10
- package/dist/cjs/{theme/useColorScale.cjs → nativeTwins.native.js} +14 -24
- package/dist/cjs/nativeTwins.native.js.map +1 -0
- package/dist/cjs/theme/Surface.cjs +2 -10
- package/dist/cjs/theme/Surface.native.js +3 -12
- package/dist/cjs/theme/Surface.native.js.map +1 -1
- package/dist/cjs/theme/devtools/ColorLineVisualizer.cjs +91 -89
- package/dist/cjs/theme/devtools/ColorLineVisualizer.native.js +79 -88
- package/dist/cjs/theme/devtools/ColorLineVisualizer.native.js.map +1 -1
- package/dist/cjs/theme/devtools/index.cjs +25 -0
- package/dist/cjs/theme/devtools/index.native.js +28 -0
- package/dist/cjs/theme/devtools/index.native.js.map +1 -0
- package/dist/cjs/theme/devtools/knobIcons.cjs +147 -149
- package/dist/cjs/theme/devtools/knobIcons.native.js +135 -149
- package/dist/cjs/theme/devtools/knobIcons.native.js.map +1 -1
- package/dist/cjs/theme/nativePresetSwitch.cjs +45 -0
- package/dist/cjs/theme/nativePresetSwitch.native.js +50 -0
- package/dist/cjs/theme/nativePresetSwitch.native.js.map +1 -0
- package/dist/cjs/theme/nativeSchemePreference.cjs +72 -0
- package/dist/cjs/theme/nativeSchemePreference.native.js +80 -0
- package/dist/cjs/theme/nativeSchemePreference.native.js.map +1 -0
- package/dist/cjs/theme/nativeTheme.cjs +107 -0
- package/dist/cjs/theme/nativeTheme.native.js +115 -0
- package/dist/cjs/theme/nativeTheme.native.js.map +1 -0
- package/dist/cjs/theme/nativeTransitionProps.cjs +69 -0
- package/dist/cjs/theme/nativeTransitionProps.native.js +81 -0
- package/dist/cjs/theme/nativeTransitionProps.native.js.map +1 -0
- package/dist/cjs/theme/recipeInputs.native.js.map +1 -1
- package/dist/cjs/theme/resolveKnobs.cjs +0 -5
- package/dist/cjs/theme/resolveKnobs.native.js +0 -5
- package/dist/cjs/theme/resolveKnobs.native.js.map +1 -1
- package/dist/cjs/theme/sizeRecipes.cjs +3 -13
- package/dist/cjs/theme/sizeRecipes.native.js +3 -13
- package/dist/cjs/theme/sizeRecipes.native.js.map +1 -1
- package/dist/cjs/theme/theme.native.js +2 -94
- package/dist/cjs/theme/theme.native.js.map +1 -1
- package/dist/cjs/theme/transitionProps.native.js +2 -60
- package/dist/cjs/theme/transitionProps.native.js.map +1 -1
- package/dist/cjs/theme/usePresetSwitch.native.js +2 -29
- package/dist/cjs/theme/usePresetSwitch.native.js.map +1 -1
- package/dist/cjs/theme/useSchemePreference.native.js +2 -59
- package/dist/cjs/theme/useSchemePreference.native.js.map +1 -1
- package/dist/esm/index.gnome.mjs +48 -0
- package/dist/esm/index.gnome.mjs.map +1 -0
- package/dist/esm/index.gnome.native.js +48 -0
- package/dist/esm/index.gnome.native.js.map +1 -0
- package/dist/esm/nativeTwins.mjs +6 -0
- package/dist/esm/nativeTwins.mjs.map +1 -0
- package/dist/esm/nativeTwins.native.js +6 -0
- package/dist/esm/nativeTwins.native.js.map +1 -0
- package/dist/esm/theme/Surface.mjs +2 -9
- package/dist/esm/theme/Surface.mjs.map +1 -1
- package/dist/esm/theme/Surface.native.js +3 -11
- package/dist/esm/theme/Surface.native.js.map +1 -1
- package/dist/esm/theme/devtools/ColorLineVisualizer.mjs +80 -89
- package/dist/esm/theme/devtools/ColorLineVisualizer.mjs.map +1 -1
- package/dist/esm/theme/devtools/ColorLineVisualizer.native.js +79 -88
- package/dist/esm/theme/devtools/ColorLineVisualizer.native.js.map +1 -1
- package/dist/esm/theme/devtools/index.mjs +7 -0
- package/dist/esm/theme/devtools/index.mjs.map +1 -0
- package/dist/esm/theme/devtools/index.native.js +7 -0
- package/dist/esm/theme/devtools/index.native.js.map +1 -0
- package/dist/esm/theme/devtools/knobIcons.mjs +136 -149
- package/dist/esm/theme/devtools/knobIcons.mjs.map +1 -1
- package/dist/esm/theme/devtools/knobIcons.native.js +135 -149
- package/dist/esm/theme/devtools/knobIcons.native.js.map +1 -1
- package/dist/esm/theme/nativePresetSwitch.mjs +20 -0
- package/dist/esm/theme/nativePresetSwitch.mjs.map +1 -0
- package/dist/esm/theme/nativePresetSwitch.native.js +22 -0
- package/dist/esm/theme/nativePresetSwitch.native.js.map +1 -0
- package/dist/esm/theme/nativeSchemePreference.mjs +47 -0
- package/dist/esm/theme/nativeSchemePreference.mjs.map +1 -0
- package/dist/esm/theme/nativeSchemePreference.native.js +52 -0
- package/dist/esm/theme/nativeSchemePreference.native.js.map +1 -0
- package/dist/esm/theme/nativeTheme.mjs +82 -0
- package/dist/esm/theme/nativeTheme.mjs.map +1 -0
- package/dist/esm/theme/nativeTheme.native.js +87 -0
- package/dist/esm/theme/nativeTheme.native.js.map +1 -0
- package/dist/esm/theme/nativeTransitionProps.mjs +44 -0
- package/dist/esm/theme/nativeTransitionProps.mjs.map +1 -0
- package/dist/esm/theme/nativeTransitionProps.native.js +53 -0
- package/dist/esm/theme/nativeTransitionProps.native.js.map +1 -0
- package/dist/esm/theme/recipeInputs.mjs.map +1 -1
- package/dist/esm/theme/recipeInputs.native.js.map +1 -1
- package/dist/esm/theme/resolveKnobs.mjs +0 -5
- package/dist/esm/theme/resolveKnobs.mjs.map +1 -1
- package/dist/esm/theme/resolveKnobs.native.js +0 -5
- package/dist/esm/theme/resolveKnobs.native.js.map +1 -1
- package/dist/esm/theme/sizeRecipes.mjs +4 -9
- package/dist/esm/theme/sizeRecipes.mjs.map +1 -1
- package/dist/esm/theme/sizeRecipes.native.js +4 -9
- package/dist/esm/theme/sizeRecipes.native.js.map +1 -1
- package/dist/esm/theme/theme.native.js +1 -86
- package/dist/esm/theme/theme.native.js.map +1 -1
- package/dist/esm/theme/transitionProps.native.js +1 -52
- package/dist/esm/theme/transitionProps.native.js.map +1 -1
- package/dist/esm/theme/usePresetSwitch.native.js +1 -21
- package/dist/esm/theme/usePresetSwitch.native.js.map +1 -1
- package/dist/esm/theme/useSchemePreference.native.js +1 -51
- package/dist/esm/theme/useSchemePreference.native.js.map +1 -1
- package/package.json +54 -6
- package/src/index.gnome.ts +544 -0
- package/src/nativeTwins.ts +11 -0
- package/src/theme/Surface.spec.tsx +0 -34
- package/src/theme/Surface.tsx +1 -20
- package/src/theme/devtools/ColorLineVisualizer.tsx +1 -0
- package/src/theme/devtools/index.ts +14 -0
- package/src/theme/devtools/knobIcons.tsx +1 -0
- package/src/theme/mpo19Proof.spec.ts +0 -5
- package/src/theme/nativePresetSwitch.ts +32 -0
- package/src/theme/nativeSchemePreference.ts +58 -0
- package/src/theme/nativeTheme.tsx +113 -0
- package/src/theme/nativeTransitionProps.ts +74 -0
- package/src/theme/recipeInputs.ts +0 -5
- package/src/theme/recipes.ts +1 -7
- package/src/theme/resolveKnobs.spec.ts +4 -7
- package/src/theme/resolveKnobs.ts +0 -5
- package/src/theme/shared.tsx +4 -4
- package/src/theme/sizeLadder.spec.ts +4 -5
- package/src/theme/sizeRecipes.spec.ts +11 -16
- package/src/theme/sizeRecipes.ts +12 -19
- package/src/theme/theme.native.tsx +1 -113
- package/src/theme/theme.provider.spec.ts +1 -1
- package/src/theme/transitionProps.native.ts +1 -74
- package/src/theme/usePresetSwitch.native.ts +1 -32
- package/src/theme/useResolvedKnobs.hydration.spec.tsx +1 -1
- package/src/theme/useResolvedKnobsBehavior.spec.tsx +3 -3
- package/src/theme/useSchemePreference.native.ts +1 -58
- package/types/index.gnome.d.ts +84 -0
- package/types/index.gnome.d.ts.map +1 -0
- package/types/nativeTwins.d.ts +11 -0
- package/types/nativeTwins.d.ts.map +1 -0
- package/types/theme/Surface.d.ts +2 -13
- package/types/theme/Surface.d.ts.map +1 -1
- package/types/theme/devtools/ColorLineVisualizer.d.ts +3 -1
- package/types/theme/devtools/ColorLineVisualizer.d.ts.map +1 -1
- package/types/theme/devtools/index.d.ts +14 -0
- package/types/theme/devtools/index.d.ts.map +1 -0
- package/types/theme/devtools/knobIcons.d.ts +1 -0
- package/types/theme/devtools/knobIcons.d.ts.map +1 -1
- package/types/theme/nativePresetSwitch.d.ts +10 -0
- package/types/theme/nativePresetSwitch.d.ts.map +1 -0
- package/types/theme/nativeSchemePreference.d.ts +8 -0
- package/types/theme/nativeSchemePreference.d.ts.map +1 -0
- package/types/theme/nativeTheme.d.ts +19 -0
- package/types/theme/nativeTheme.d.ts.map +1 -0
- package/types/theme/nativeTransitionProps.d.ts +15 -0
- package/types/theme/nativeTransitionProps.d.ts.map +1 -0
- package/types/theme/recipeInputs.d.ts +0 -4
- package/types/theme/recipeInputs.d.ts.map +1 -1
- package/types/theme/recipes.d.ts +2 -9
- package/types/theme/recipes.d.ts.map +1 -1
- package/types/theme/resolveKnobs.d.ts.map +1 -1
- package/types/theme/shared.d.ts +4 -4
- package/types/theme/shared.d.ts.map +1 -1
- package/types/theme/sizeRecipes.d.ts +8 -13
- package/types/theme/sizeRecipes.d.ts.map +1 -1
- package/types/theme/theme.native.d.ts +1 -18
- package/types/theme/theme.native.d.ts.map +1 -1
- package/types/theme/transitionProps.native.d.ts +1 -14
- package/types/theme/transitionProps.native.d.ts.map +1 -1
- package/types/theme/usePresetSwitch.native.d.ts +1 -9
- package/types/theme/usePresetSwitch.native.d.ts.map +1 -1
- package/types/theme/useSchemePreference.native.d.ts +1 -7
- package/types/theme/useSchemePreference.native.d.ts.map +1 -1
- package/dist/cjs/theme/useColorScale.native.js.map +0 -1
- package/dist/esm/theme/useColorScale.mjs +0 -22
- package/dist/esm/theme/useColorScale.mjs.map +0 -1
- package/dist/esm/theme/useColorScale.native.js +0 -5
- package/dist/esm/theme/useColorScale.native.js.map +0 -1
- package/src/theme/useColorScale.native.ts +0 -7
- package/src/theme/useColorScale.ts +0 -29
- package/types/theme/useColorScale.d.ts +0 -7
- package/types/theme/useColorScale.d.ts.map +0 -1
- package/types/theme/useColorScale.native.d.ts +0 -6
- package/types/theme/useColorScale.native.d.ts.map +0 -1
package/src/theme/Surface.tsx
CHANGED
|
@@ -22,11 +22,6 @@ export type SurfaceSize = SizeTokens | "unset";
|
|
|
22
22
|
* is part of the field: a bare tinted glyph, never glass or a plate, even when
|
|
23
23
|
* the field sits in a bar (LC-176, FZ-8711). Any explicit `painted` resets it,
|
|
24
24
|
* so a menu opened from that glyph is content again.
|
|
25
|
-
*
|
|
26
|
-
* `topBar`: the bar is the page's top bar (a navigation bar, a page header's
|
|
27
|
-
* actions), not an overlay's header, so a confirm there is a bar glyph like
|
|
28
|
-
* back and "…", never the overlay's filled ✓ (LC-176, FZ-8989). Any explicit
|
|
29
|
-
* `bar` or `painted` resets it, so a sheet's own header bar is not a top bar.
|
|
30
25
|
*/
|
|
31
26
|
export type SurfaceIntent = {
|
|
32
27
|
size: SurfaceSize;
|
|
@@ -34,7 +29,6 @@ export type SurfaceIntent = {
|
|
|
34
29
|
bar: boolean;
|
|
35
30
|
elevated: boolean;
|
|
36
31
|
field: boolean;
|
|
37
|
-
topBar: boolean;
|
|
38
32
|
};
|
|
39
33
|
|
|
40
34
|
export const SurfaceContext = createStyledContext<SurfaceIntent>({
|
|
@@ -43,7 +37,6 @@ export const SurfaceContext = createStyledContext<SurfaceIntent>({
|
|
|
43
37
|
bar: false,
|
|
44
38
|
elevated: false,
|
|
45
39
|
field: false,
|
|
46
|
-
topBar: false,
|
|
47
40
|
});
|
|
48
41
|
|
|
49
42
|
/** True inside a bar (LC-153): neutral buttons there are plain, not tinted. */
|
|
@@ -56,11 +49,6 @@ export function useFieldSurface(): boolean {
|
|
|
56
49
|
return SurfaceContext.useStyledContext().field ?? false;
|
|
57
50
|
}
|
|
58
51
|
|
|
59
|
-
/** True inside the page's top bar (LC-176): a confirm there is a bar glyph, not the overlay's ✓ plate. */
|
|
60
|
-
export function useTopBarSurface(): boolean {
|
|
61
|
-
return SurfaceContext.useStyledContext().topBar ?? false;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
52
|
/** True on an elevated ground (an overlay frame, an unnested Panel): controls step their wash up in dark. */
|
|
65
53
|
export function useElevatedSurface(): boolean {
|
|
66
54
|
return SurfaceContext.useStyledContext().elevated ?? false;
|
|
@@ -145,9 +133,7 @@ export function clampSurfaceSize(requested: SurfaceSize, parent: SurfaceSize): S
|
|
|
145
133
|
* inherits it. `bar` marks a bar subtree (LC-153); omitted, it inherits unless
|
|
146
134
|
* `painted` is given, which resets it. `elevated` marks a frame that paints
|
|
147
135
|
* the elevated ground; omitted it inherits unless `painted` is given, which
|
|
148
|
-
* resets it. `field` marks a text field's frame the same way.
|
|
149
|
-
* the page's top bar; omitted it inherits unless `bar` or `painted` is given,
|
|
150
|
-
* which resets it. Either way a
|
|
136
|
+
* resets it. `field` marks a text field's frame the same way. Either way a
|
|
151
137
|
* painted value opens a new surface with its own
|
|
152
138
|
* inset, so the page gutter does not reach inside it (LC-146).
|
|
153
139
|
*
|
|
@@ -160,7 +146,6 @@ export function Surface({
|
|
|
160
146
|
bar,
|
|
161
147
|
elevated,
|
|
162
148
|
field,
|
|
163
|
-
topBar,
|
|
164
149
|
children,
|
|
165
150
|
}: {
|
|
166
151
|
size?: SizeTokens;
|
|
@@ -168,7 +153,6 @@ export function Surface({
|
|
|
168
153
|
bar?: boolean;
|
|
169
154
|
elevated?: boolean;
|
|
170
155
|
field?: boolean;
|
|
171
|
-
topBar?: boolean;
|
|
172
156
|
children?: ReactNode;
|
|
173
157
|
}) {
|
|
174
158
|
const parent = SurfaceContext.useStyledContext();
|
|
@@ -177,8 +161,6 @@ export function Surface({
|
|
|
177
161
|
const nextBar = bar ?? (painted === undefined ? (parent.bar ?? false) : false);
|
|
178
162
|
const nextElevated = elevated ?? (painted === undefined ? (parent.elevated ?? false) : false);
|
|
179
163
|
const nextField = field ?? (painted === undefined ? (parent.field ?? false) : false);
|
|
180
|
-
const nextTopBar =
|
|
181
|
-
topBar ?? (painted === undefined && bar === undefined ? (parent.topBar ?? false) : false);
|
|
182
164
|
return (
|
|
183
165
|
<SurfaceContext.Provider
|
|
184
166
|
size={nextSize}
|
|
@@ -186,7 +168,6 @@ export function Surface({
|
|
|
186
168
|
bar={nextBar}
|
|
187
169
|
elevated={nextElevated}
|
|
188
170
|
field={nextField}
|
|
189
|
-
topBar={nextTopBar}
|
|
190
171
|
>
|
|
191
172
|
{painted === undefined ? children : <PageGutter inset={null}>{children}</PageGutter>}
|
|
192
173
|
</SurfaceContext.Provider>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The theme devtools pieces that load without tamagui or React Native: knob
|
|
3
|
+
* definitions, knob icons, the snapshot builder and the colour-line
|
|
4
|
+
* visualizer. Exported as `@multiplatform.one/theme/devtools` for hosts such
|
|
5
|
+
* as the Storybook manager frame, which renders outside TamaguiProvider.
|
|
6
|
+
* ThemeDevtoolsPanel needs tamagui, so it stays on the package root.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export * from "../knobs";
|
|
10
|
+
export * from "../motionStops";
|
|
11
|
+
export * from "./knobIcons";
|
|
12
|
+
export * from "./buildSnapshot";
|
|
13
|
+
export * from "./ColorLineVisualizer";
|
|
14
|
+
export * from "./vizConstants";
|
|
@@ -279,9 +279,4 @@ describe("MPO-19 X12 — the nested radius is named for what it resolves to", ()
|
|
|
279
279
|
expect(nested, `borderRadius: ${knob}`).toBeLessThanOrEqual(outer);
|
|
280
280
|
}
|
|
281
281
|
});
|
|
282
|
-
|
|
283
|
-
it("the old name is the SAME object, so the two cannot drift while both exist", () => {
|
|
284
|
-
const props = knobProps();
|
|
285
|
-
expect(props.borderRadiusOuter).toBe(props.borderRadiusNested);
|
|
286
|
-
});
|
|
287
282
|
});
|
|
@@ -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,58 @@
|
|
|
1
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
2
|
+
import { Appearance } from "react-native";
|
|
3
|
+
import { useDidFinishSSR } from "tamagui";
|
|
4
|
+
import { isWeb } from "@multiplatform.one/platform";
|
|
5
|
+
import { getStoredScheme, setStoredScheme, subscribeStoredScheme } from "./schemeStore";
|
|
6
|
+
import type { ThemeScheme } from "./useTheme";
|
|
7
|
+
|
|
8
|
+
export interface SchemePreference {
|
|
9
|
+
setting: ThemeScheme;
|
|
10
|
+
value: "light" | "dark";
|
|
11
|
+
set(scheme: ThemeScheme): void;
|
|
12
|
+
}
|
|
13
|
+
const serverScheme = (): ThemeScheme => "system";
|
|
14
|
+
|
|
15
|
+
// RN's Appearance.setColorScheme("unspecified") writes "unspecified" into the
|
|
16
|
+
// JS cache that getColorScheme/useColorScheme read, with no change event, so
|
|
17
|
+
// System resolved light until the OS flipped (FZ-8708). The native scheme is
|
|
18
|
+
// read once before the first override and then only from change events.
|
|
19
|
+
let nativeScheme: "light" | "dark" | undefined;
|
|
20
|
+
const nativeListeners = new Set<() => void>();
|
|
21
|
+
|
|
22
|
+
function getNativeScheme(): "light" | "dark" {
|
|
23
|
+
if (nativeScheme === undefined) {
|
|
24
|
+
nativeScheme = Appearance.getColorScheme() === "dark" ? "dark" : "light";
|
|
25
|
+
Appearance.addChangeListener(({ colorScheme }) => {
|
|
26
|
+
if ((colorScheme !== "light" && colorScheme !== "dark") || colorScheme === nativeScheme) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
nativeScheme = colorScheme;
|
|
30
|
+
nativeListeners.forEach((listener) => listener());
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return nativeScheme;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function subscribeNativeScheme(listener: () => void) {
|
|
37
|
+
getNativeScheme();
|
|
38
|
+
nativeListeners.add(listener);
|
|
39
|
+
return () => {
|
|
40
|
+
nativeListeners.delete(listener);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useSchemePreference(): SchemePreference {
|
|
45
|
+
const setting = useSyncExternalStore(subscribeStoredScheme, getStoredScheme, serverScheme);
|
|
46
|
+
const os = useSyncExternalStore(subscribeNativeScheme, getNativeScheme, getNativeScheme);
|
|
47
|
+
const hydrated = useDidFinishSSR();
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (isWeb) return;
|
|
50
|
+
getNativeScheme();
|
|
51
|
+
Appearance.setColorScheme(setting === "system" ? "unspecified" : setting);
|
|
52
|
+
}, [setting]);
|
|
53
|
+
return {
|
|
54
|
+
setting: hydrated ? setting : "system",
|
|
55
|
+
value: !hydrated ? "light" : setting === "system" ? os : setting,
|
|
56
|
+
set: setStoredScheme,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { ToastProvider } from "@tamagui/toast";
|
|
2
|
+
import { Fragment, useContext, useMemo, useSyncExternalStore } from "react";
|
|
3
|
+
import {
|
|
4
|
+
type CreateTamaguiProps,
|
|
5
|
+
TamaguiProvider,
|
|
6
|
+
type TamaguiProviderProps,
|
|
7
|
+
Theme,
|
|
8
|
+
} from "tamagui";
|
|
9
|
+
import type { PropsWithChildren } from "react";
|
|
10
|
+
import { CornerSmoothingStyles } from "./cornerSmoothing";
|
|
11
|
+
import { FontKnobStyles } from "./FontKnobStyles";
|
|
12
|
+
import type { Knobs } from "./knobs";
|
|
13
|
+
import { PresetContext, type PresetContextValue } from "./PresetContext";
|
|
14
|
+
import { getStoredPreset, subscribeStoredPreset } from "./presetStore";
|
|
15
|
+
import { defaultPreset } from "./presets";
|
|
16
|
+
import { resolveThemeScale, useActiveScale } from "./scale/useActiveScale";
|
|
17
|
+
import { type ThemeConfig, getPreset } from "./shared";
|
|
18
|
+
import { useSchemePreference } from "./useSchemePreference";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// ThemeProviderProps (simplified — knob-based theming)
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export interface ThemeProviderProps<
|
|
25
|
+
T extends CreateTamaguiProps = CreateTamaguiProps,
|
|
26
|
+
> extends PropsWithChildren<Omit<TamaguiProviderProps, "config" | "defaultTheme">> {
|
|
27
|
+
config?: {
|
|
28
|
+
tamagui: ReturnType<typeof import("tamagui").createTamagui<T>>;
|
|
29
|
+
} & Partial<Pick<ThemeConfig, "scales" | "defaultPreset">>;
|
|
30
|
+
systemTheme?: "light" | "dark";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// ThemeProvider (simplified — TamaguiProvider + scheme + ToastProvider)
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
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
|
+
*/
|
|
45
|
+
export function ThemeProvider<T extends CreateTamaguiProps>({
|
|
46
|
+
children,
|
|
47
|
+
config,
|
|
48
|
+
systemTheme,
|
|
49
|
+
...tamaguiProps
|
|
50
|
+
}: ThemeProviderProps<T>) {
|
|
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;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<TamaguiProvider
|
|
61
|
+
{...tamaguiProps}
|
|
62
|
+
config={scaleConfig as TamaguiProviderProps["config"]}
|
|
63
|
+
defaultTheme={activeTheme}
|
|
64
|
+
disableInjectCSS={false}
|
|
65
|
+
>
|
|
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}
|
|
75
|
+
</TamaguiProvider>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function KnobBridge({
|
|
80
|
+
children,
|
|
81
|
+
presetName,
|
|
82
|
+
overrides,
|
|
83
|
+
scaleId,
|
|
84
|
+
}: PropsWithChildren<{ presetName?: string; overrides: Partial<Knobs>; scaleId: string }>) {
|
|
85
|
+
const existingCtx = useContext(PresetContext);
|
|
86
|
+
|
|
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]);
|
|
96
|
+
|
|
97
|
+
const tree = (
|
|
98
|
+
<>
|
|
99
|
+
<FontKnobStyles />
|
|
100
|
+
<CornerSmoothingStyles />
|
|
101
|
+
{children}
|
|
102
|
+
</>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const theme = (existingCtx ?? presetCtxValue).preset.theme;
|
|
106
|
+
const themedTree = theme ? <Theme name={theme}>{tree}</Theme> : tree;
|
|
107
|
+
|
|
108
|
+
if (existingCtx) {
|
|
109
|
+
return themedTree;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return <PresetContext.Provider value={presetCtxValue}>{themedTree}</PresetContext.Provider>;
|
|
113
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { MotionRole } from "./motion";
|
|
2
|
+
import type { TransitionProp } from "tamagui";
|
|
3
|
+
import { animationConfig as nativeAnimationConfig } from "./animations/reactNative";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Native twin of `transitionProps.ts`.
|
|
7
|
+
*
|
|
8
|
+
* The React Native driver resolves an undefined `transition` to an empty
|
|
9
|
+
* config and animates with a default spring (LC-59), so keeping the key at
|
|
10
|
+
* animation `none` would put motion on every part that is meant to be still.
|
|
11
|
+
* Native keeps omitting it until the driver has an instant token to pass.
|
|
12
|
+
*/
|
|
13
|
+
export function transitionProps(
|
|
14
|
+
role: MotionRole | TransitionProp | undefined,
|
|
15
|
+
extra: readonly string[] = [],
|
|
16
|
+
): {
|
|
17
|
+
transition?: TransitionProp;
|
|
18
|
+
animateOnly?: string[];
|
|
19
|
+
} {
|
|
20
|
+
const isRole = isMotionRole(role);
|
|
21
|
+
const transition = isRole ? role.transition : role;
|
|
22
|
+
if (transition === undefined) return {};
|
|
23
|
+
if (!isRole) return { transition };
|
|
24
|
+
const properties = [...role.animateOnly, ...extra].flatMap(
|
|
25
|
+
(property) => nativeLonghands[property] ?? [property],
|
|
26
|
+
);
|
|
27
|
+
if (typeof transition !== "string") return { transition, animateOnly: properties };
|
|
28
|
+
const exit = typeof role.exit === "string" && role.exit !== transition ? role.exit : undefined;
|
|
29
|
+
const clamp = isSpring(transition) && !presetBounce.has(transition);
|
|
30
|
+
if (!exit && !clamp) return { transition, animateOnly: properties };
|
|
31
|
+
return {
|
|
32
|
+
transition: [
|
|
33
|
+
transition,
|
|
34
|
+
{ ...(clamp ? { overshootClamping: true } : {}), ...(exit ? { exit } : {}) },
|
|
35
|
+
],
|
|
36
|
+
animateOnly: properties,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isSpring(stop: string): boolean {
|
|
41
|
+
const config = nativeAnimationConfig[stop as keyof typeof nativeAnimationConfig] as
|
|
42
|
+
| { type?: string }
|
|
43
|
+
| undefined;
|
|
44
|
+
return config !== undefined && config.type !== "timing";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* FZ-7919: tamagui expands `borderColor` into its four sides before the React
|
|
49
|
+
* Native driver sees the style, and the driver matches `animateOnly` by exact
|
|
50
|
+
* key. A render therefore painted the sides as static, while a pseudo state
|
|
51
|
+
* (the bubbled focus of a field's text input) animated them. The animated
|
|
52
|
+
* copy outranked the static one and held the previous focus or theme colour.
|
|
53
|
+
*/
|
|
54
|
+
const presetBounce = new Set(["bouncy", "superBouncy"]);
|
|
55
|
+
|
|
56
|
+
const nativeLonghands: Record<string, readonly string[]> = {
|
|
57
|
+
borderColor: [
|
|
58
|
+
"borderColor",
|
|
59
|
+
"borderTopColor",
|
|
60
|
+
"borderRightColor",
|
|
61
|
+
"borderBottomColor",
|
|
62
|
+
"borderLeftColor",
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function isMotionRole(value: MotionRole | TransitionProp | undefined): value is MotionRole {
|
|
67
|
+
return (
|
|
68
|
+
typeof value === "object" &&
|
|
69
|
+
value !== null &&
|
|
70
|
+
"transition" in value &&
|
|
71
|
+
"animateOnly" in value &&
|
|
72
|
+
Array.isArray(value.animateOnly)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -32,8 +32,6 @@ export { SIZE_RECIPE_TOKENS, SIZE_TOKEN_EXPONENTS } from "./scale/scaleInputs";
|
|
|
32
32
|
export type { SizeRecipeToken } from "./scale/scaleInputs";
|
|
33
33
|
|
|
34
34
|
export type SizeRecipeFamilyName = "control" | "chip" | "menuRow" | "tab";
|
|
35
|
-
/** @deprecated Use `SizeRecipeFamilyName`. */
|
|
36
|
-
export type RecipeFamilyName = SizeRecipeFamilyName;
|
|
37
35
|
|
|
38
36
|
export type SizeRecipeRatios = {
|
|
39
37
|
padX: number;
|
|
@@ -99,9 +97,6 @@ export type SizeRecipeInputs = {
|
|
|
99
97
|
tile?: Partial<TileSizeInputs>;
|
|
100
98
|
};
|
|
101
99
|
|
|
102
|
-
/** @deprecated Use `SizeRecipeInputs`. */
|
|
103
|
-
export type RecipeInputs = SizeRecipeInputs;
|
|
104
|
-
|
|
105
100
|
export type SizeRecipe = {
|
|
106
101
|
height: number;
|
|
107
102
|
paddingHorizontal: number;
|
package/src/theme/recipes.ts
CHANGED
|
@@ -4,7 +4,6 @@ import type {
|
|
|
4
4
|
FontLetterSpacingTokens,
|
|
5
5
|
FontLineHeightTokens,
|
|
6
6
|
FontSizeTokens,
|
|
7
|
-
TransitionProp,
|
|
8
7
|
} from "tamagui";
|
|
9
8
|
import type { Knobs } from "./knobs";
|
|
10
9
|
import type { HitSlopInsets } from "./layoutTokens";
|
|
@@ -186,12 +185,9 @@ export interface KnobProps {
|
|
|
186
185
|
*
|
|
187
186
|
* MPO-19 X12 (00 L9c): this was `borderRadiusOuter`, which named the
|
|
188
187
|
* opposite of what it resolves to — every one of its consumers uses it for
|
|
189
|
-
* inner chrome, so the name told each of them the wrong story. Renamed
|
|
190
|
-
* `borderRadiusOuter` stays as a deprecated alias for one release.
|
|
188
|
+
* inner chrome, so the name told each of them the wrong story. Renamed.
|
|
191
189
|
*/
|
|
192
190
|
borderRadiusNested: { borderRadius: string } & CornerSmoothingCarrier;
|
|
193
|
-
/** @deprecated MPO-19 X12 — use `borderRadiusNested`. Removed after one release. */
|
|
194
|
-
borderRadiusOuter: { borderRadius: string } & CornerSmoothingCarrier;
|
|
195
191
|
/**
|
|
196
192
|
* A FILLED control's border. Same width as the outlined one, so switching
|
|
197
193
|
* fill style never moves a layout by 2px, but `transparent` — which is what
|
|
@@ -380,8 +376,6 @@ export interface KnobProps {
|
|
|
380
376
|
};
|
|
381
377
|
textWeight: { fontWeight: string };
|
|
382
378
|
motion: Record<MotionRoleName, MotionRole>;
|
|
383
|
-
/** @deprecated read a role from knobProps.motion */
|
|
384
|
-
transition: TransitionProp | undefined;
|
|
385
379
|
outlined: boolean;
|
|
386
380
|
pointy: boolean;
|
|
387
381
|
textAccent: string;
|
|
@@ -65,7 +65,6 @@ const defaultScaleTokens = generateScale(defaultScaleInputs).tokens;
|
|
|
65
65
|
const expectedKnobPropKeys: (keyof KnobProps)[] = [
|
|
66
66
|
"borderRadius",
|
|
67
67
|
"borderRadiusNested",
|
|
68
|
-
"borderRadiusOuter",
|
|
69
68
|
"containerRadius",
|
|
70
69
|
"fieldRadius",
|
|
71
70
|
"elevation",
|
|
@@ -99,7 +98,6 @@ const expectedKnobPropKeys: (keyof KnobProps)[] = [
|
|
|
99
98
|
"body",
|
|
100
99
|
"typography",
|
|
101
100
|
"textWeight",
|
|
102
|
-
"transition",
|
|
103
101
|
"outlined",
|
|
104
102
|
"pointy",
|
|
105
103
|
"textAccent",
|
|
@@ -1349,14 +1347,13 @@ describe("resolveKnobs", () => {
|
|
|
1349
1347
|
// ── animation ─────────────────────────────────────────────
|
|
1350
1348
|
|
|
1351
1349
|
describe("animation knob", () => {
|
|
1352
|
-
it("resolves each role
|
|
1350
|
+
it("resolves each role", () => {
|
|
1353
1351
|
const { knobProps } = resolveKnobs(knobsWith({ animation: "quick" }));
|
|
1354
1352
|
expect(knobProps.motion.control.transition).toBe("quick");
|
|
1355
1353
|
expect(knobProps.motion.overlay.transition).toBe("quicker");
|
|
1356
1354
|
expect(knobProps.motion.scrim.transition).toBe("slow");
|
|
1357
1355
|
expect(knobProps.motion.reveal.transition).toBe("medium");
|
|
1358
1356
|
expect(knobProps.motion.slide.transition).toBe("medium");
|
|
1359
|
-
expect(knobProps.transition).toBe(knobProps.motion.control.transition);
|
|
1360
1357
|
for (const role of Object.values(
|
|
1361
1358
|
resolveKnobs(knobsWith({ animation: "none" })).knobProps.motion,
|
|
1362
1359
|
))
|
|
@@ -1364,17 +1361,17 @@ describe("resolveKnobs", () => {
|
|
|
1364
1361
|
});
|
|
1365
1362
|
it('"none" → transition is undefined', () => {
|
|
1366
1363
|
const { knobProps } = resolveKnobs(knobsWith({ animation: "none" }));
|
|
1367
|
-
expect(knobProps.transition).toBeUndefined();
|
|
1364
|
+
expect(knobProps.motion.control.transition).toBeUndefined();
|
|
1368
1365
|
});
|
|
1369
1366
|
|
|
1370
1367
|
it('"bouncy" → transition is "bouncy"', () => {
|
|
1371
1368
|
const { knobProps } = resolveKnobs(knobsWith({ animation: "bouncy" }));
|
|
1372
|
-
expect(knobProps.transition).toBe("bouncy");
|
|
1369
|
+
expect(knobProps.motion.control.transition).toBe("bouncy");
|
|
1373
1370
|
});
|
|
1374
1371
|
|
|
1375
1372
|
it('"quick" → transition is "quick"', () => {
|
|
1376
1373
|
const { knobProps } = resolveKnobs(knobsWith({ animation: "quick" }));
|
|
1377
|
-
expect(knobProps.transition).toBe("quick");
|
|
1374
|
+
expect(knobProps.motion.control.transition).toBe("quick");
|
|
1378
1375
|
});
|
|
1379
1376
|
});
|
|
1380
1377
|
|
|
@@ -869,10 +869,6 @@ export function resolveKnobs(
|
|
|
869
869
|
...scaleSmoothing,
|
|
870
870
|
},
|
|
871
871
|
borderRadiusNested: nestedRadius,
|
|
872
|
-
// MPO-19 X12: the old name for the same fragment, kept for one release so
|
|
873
|
-
// an out-of-tree consumer does not break on the rename. Same object, so
|
|
874
|
-
// the two cannot drift while both exist.
|
|
875
|
-
borderRadiusOuter: nestedRadius,
|
|
876
872
|
controlBorderFilled: { borderColor: "transparent", borderWidth: borderWidthValue },
|
|
877
873
|
controlBorderOutlined: {
|
|
878
874
|
borderColor: edge("$borderColor", Math.max(borderWidthValue, 1)),
|
|
@@ -992,7 +988,6 @@ export function resolveKnobs(
|
|
|
992
988
|
},
|
|
993
989
|
textWeight: { fontWeight: fontWeightValue },
|
|
994
990
|
motion,
|
|
995
|
-
transition: motion.control.transition,
|
|
996
991
|
outlined,
|
|
997
992
|
pointy: knobs.borderRadius === "none",
|
|
998
993
|
textAccent: knobs.textAccent,
|
package/src/theme/shared.tsx
CHANGED
|
@@ -3,7 +3,7 @@ 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
|
-
import type { RecipeFamilies,
|
|
6
|
+
import type { RecipeFamilies, SizeRecipeInputs } from "./recipeInputs";
|
|
7
7
|
import type { ScaleFonts } from "./scale/generateScale";
|
|
8
8
|
import { type ThemeScale, buildThemeScales } from "./scale/themeScales";
|
|
9
9
|
|
|
@@ -90,7 +90,7 @@ export type ThemeConfig<T extends CreateTamaguiProps = CreateTamaguiProps> = {
|
|
|
90
90
|
* Size-recipe inputs + generated families (LC-70). Orthogonal to Tamagui
|
|
91
91
|
* `themes` (those are color palettes from createThemes / theme-builder).
|
|
92
92
|
*/
|
|
93
|
-
recipeInputs?:
|
|
93
|
+
recipeInputs?: SizeRecipeInputs;
|
|
94
94
|
recipeFamilies?: RecipeFamilies;
|
|
95
95
|
/**
|
|
96
96
|
* The token scales this config can switch to at runtime, keyed by preset
|
|
@@ -106,7 +106,7 @@ export interface ThemeConfigScaleOptions {
|
|
|
106
106
|
/** The font map a scale's type tables produce. */
|
|
107
107
|
fonts: (tables: ScaleFonts) => Record<string, GenericFont>;
|
|
108
108
|
/** Consumer recipe overrides that are not scale-derived (tile ladder, touch floor). */
|
|
109
|
-
recipeOverrides?: Partial<
|
|
109
|
+
recipeOverrides?: Partial<SizeRecipeInputs>;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
@@ -148,7 +148,7 @@ export function createThemeConfig<T extends CreateTamaguiProps>(
|
|
|
148
148
|
options?: {
|
|
149
149
|
presets?: Record<string, Preset>;
|
|
150
150
|
defaultPreset?: string;
|
|
151
|
-
recipeInputs?:
|
|
151
|
+
recipeInputs?: SizeRecipeInputs;
|
|
152
152
|
recipeFamilies?: RecipeFamilies;
|
|
153
153
|
scale?: ThemeConfigScaleOptions;
|
|
154
154
|
},
|
|
@@ -3,7 +3,6 @@ import { defaultScaleInputs } from "./scale/scaleInputs";
|
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_SIZE_RECIPE_INPUTS,
|
|
6
|
-
SIZE_RECIPE_RADII,
|
|
7
6
|
SIZE_RECIPE_RADIUS_STOPS,
|
|
8
7
|
applyRadiusStop,
|
|
9
8
|
generateSizeRecipes,
|
|
@@ -25,7 +24,7 @@ describe("MPO-46 size ladder — generated table, not a ratio (R6)", () => {
|
|
|
25
24
|
expect(recipe.paddingHorizontal, `${token} padX`).toBe(row.paddingHorizontal);
|
|
26
25
|
expect(recipe.fontSize, `${token} label`).toBe(row.fontSize);
|
|
27
26
|
expect(recipe.iconSize, `${token} icon`).toBe(row.iconSize);
|
|
28
|
-
expect(
|
|
27
|
+
expect(recipeFamilies.control.radii[token], `${token} radius`).toBe(row.radius);
|
|
29
28
|
expect(DEFAULT_SIZE_RECIPE_INPUTS.tokenTable[token]).toEqual(row);
|
|
30
29
|
}
|
|
31
30
|
});
|
|
@@ -34,15 +33,15 @@ describe("MPO-46 size ladder — generated table, not a ratio (R6)", () => {
|
|
|
34
33
|
const two = sizeRecipeForToken("$2");
|
|
35
34
|
expect(two.paddingHorizontal).not.toBe(11);
|
|
36
35
|
expect(two.fontSize).not.toBe(9);
|
|
37
|
-
expect(
|
|
38
|
-
expect(
|
|
36
|
+
expect(recipeFamilies.control.radii.$2).not.toBe(9);
|
|
37
|
+
expect(recipeFamilies.control.radii.$2).not.toBe(recipeFamilies.control.radii.$6);
|
|
39
38
|
});
|
|
40
39
|
|
|
41
40
|
it("Button / Select trigger / ListItem $4 is 40 / padX 16 / radius 8; Input padX is 14", () => {
|
|
42
41
|
const button = sizeRecipes.$4;
|
|
43
42
|
expect(button.height).toBe(40);
|
|
44
43
|
expect(button.paddingHorizontal).toBe(16);
|
|
45
|
-
expect(
|
|
44
|
+
expect(recipeFamilies.control.radii.$4).toBe(8);
|
|
46
45
|
expect(DEFAULT_SIZE_RECIPE_INPUTS.inputPadX.$4).toBe(14);
|
|
47
46
|
expect(recipeFamilies.chip.recipes.$4.paddingHorizontal).toBe(14);
|
|
48
47
|
expect(recipeFamilies.chip.recipes.$4.height).toBe(40);
|