@mrmeg/expo-ui 0.27.0 → 0.28.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/CHANGELOG.md +712 -0
- package/LLM_USAGE.md +260 -32
- package/README.md +558 -101
- package/dist/components/Accordion.js +12 -6
- package/dist/components/AnimatedView.d.ts +1 -1
- package/dist/components/AnimatedView.js +5 -2
- package/dist/components/Avatar.d.ts +1 -1
- package/dist/components/Avatar.js +2 -2
- package/dist/components/Badge.js +5 -1
- package/dist/components/BottomSheet.d.ts +66 -9
- package/dist/components/BottomSheet.js +143 -39
- package/dist/components/Button.d.ts +20 -4
- package/dist/components/Button.js +64 -43
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +24 -4
- package/dist/components/Carousel.js +2 -2
- package/dist/components/Checkbox.js +51 -44
- package/dist/components/Collapsible.js +7 -3
- package/dist/components/Dialog.js +178 -29
- package/dist/components/Drawer.d.ts +1 -1
- package/dist/components/Drawer.js +65 -55
- package/dist/components/DropdownMenu.js +11 -7
- package/dist/components/EmptyState.d.ts +2 -2
- package/dist/components/EmptyState.js +5 -2
- package/dist/components/Hydrated.d.ts +31 -0
- package/dist/components/Hydrated.js +30 -0
- package/dist/components/Icon.d.ts +17 -5
- package/dist/components/Icon.js +31 -10
- package/dist/components/InputOTP.js +7 -3
- package/dist/components/Item.d.ts +103 -4
- package/dist/components/Item.js +168 -15
- package/dist/components/KeyboardAvoidingView.d.ts +1 -1
- package/dist/components/KeyboardAvoidingView.js +4 -1
- package/dist/components/Label.js +3 -2
- package/dist/components/MaxWidthContainer.js +1 -1
- package/dist/components/Notification.js +45 -27
- package/dist/components/Popover.d.ts +18 -5
- package/dist/components/Popover.js +72 -8
- package/dist/components/Progress.js +8 -6
- package/dist/components/RadioGroup.js +52 -49
- package/dist/components/Screen.d.ts +61 -0
- package/dist/components/Screen.js +62 -0
- package/dist/components/SectionHeader.d.ts +1 -1
- package/dist/components/SegmentedControl.d.ts +12 -4
- package/dist/components/SegmentedControl.js +94 -4
- package/dist/components/Select.js +21 -26
- package/dist/components/Skeleton.js +24 -10
- package/dist/components/Slider.d.ts +11 -3
- package/dist/components/Slider.js +74 -6
- package/dist/components/StatCard.d.ts +4 -2
- package/dist/components/StatCard.js +3 -1
- package/dist/components/StyledText.d.ts +7 -1
- package/dist/components/StyledText.js +5 -3
- package/dist/components/Switch.d.ts +1 -1
- package/dist/components/Switch.js +23 -32
- package/dist/components/Tabs.d.ts +2 -2
- package/dist/components/Tabs.js +13 -24
- package/dist/components/TextInput.d.ts +1 -1
- package/dist/components/TextInput.js +73 -27
- package/dist/components/Toggle.d.ts +2 -42
- package/dist/components/Toggle.js +38 -27
- package/dist/components/ToggleGroup.d.ts +1 -1
- package/dist/components/ToggleGroup.js +37 -32
- package/dist/components/Tooltip.d.ts +1 -1
- package/dist/components/Tooltip.js +9 -3
- package/dist/components/UIProvider.d.ts +22 -2
- package/dist/components/UIProvider.js +15 -3
- package/dist/components/bottomSheetDismiss.d.ts +19 -0
- package/dist/components/bottomSheetDismiss.js +44 -0
- package/dist/components/bottomSheetHostSupport.d.ts +46 -0
- package/dist/components/bottomSheetHostSupport.js +67 -0
- package/dist/components/index.d.ts +46 -44
- package/dist/components/index.js +2 -0
- package/dist/components/keyboardDismiss.d.ts +25 -0
- package/dist/components/keyboardDismiss.js +60 -3
- package/dist/components/nativeTextField.android.js +13 -9
- package/dist/constants/colors.d.ts +40 -5
- package/dist/constants/colors.js +26 -8
- package/dist/constants/fonts.d.ts +41 -2
- package/dist/constants/fonts.js +46 -7
- package/dist/constants/index.d.ts +5 -4
- package/dist/constants/index.js +1 -0
- package/dist/constants/interaction.d.ts +18 -0
- package/dist/constants/interaction.js +18 -0
- package/dist/constants/spacing.d.ts +2 -1
- package/dist/constants/spacing.js +2 -1
- package/dist/design-system.json +154 -2
- package/dist/hooks/index.d.ts +12 -7
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/useDimensions.d.ts +13 -8
- package/dist/hooks/useDimensions.js +123 -48
- package/dist/hooks/useFocusVisible.d.ts +30 -0
- package/dist/hooks/useFocusVisible.js +45 -0
- package/dist/hooks/useFontStyle.d.ts +2 -2
- package/dist/hooks/useFontStyle.js +3 -2
- package/dist/hooks/useHydrated.d.ts +13 -0
- package/dist/hooks/useHydrated.js +19 -0
- package/dist/hooks/useNotificationOffset.d.ts +20 -0
- package/dist/hooks/useNotificationOffset.js +34 -0
- package/dist/hooks/useResources.d.ts +21 -20
- package/dist/hooks/useResources.js +72 -68
- package/dist/hooks/useScalePress.d.ts +8 -5
- package/dist/hooks/useScalePress.js +16 -9
- package/dist/hooks/useShape.d.ts +24 -0
- package/dist/hooks/useShape.js +24 -0
- package/dist/hooks/useStaggeredEntrance.js +9 -7
- package/dist/hooks/useTheme.d.ts +3 -3
- package/dist/hooks/useTheme.js +177 -66
- package/dist/hooks/useWindowInsets.d.ts +13 -0
- package/dist/hooks/useWindowInsets.js +23 -0
- package/dist/index.d.ts +5 -5
- package/dist/lib/composeRefs.d.ts +8 -0
- package/dist/lib/composeRefs.js +20 -0
- package/dist/lib/haptics.d.ts +12 -0
- package/dist/lib/haptics.js +21 -0
- package/dist/lib/index.d.ts +6 -4
- package/dist/lib/index.js +2 -0
- package/dist/lib/interFonts.d.ts +15 -0
- package/dist/lib/interFonts.js +1 -0
- package/dist/lib/interFonts.native.d.ts +3 -0
- package/dist/lib/interFonts.native.js +20 -0
- package/dist/lib/popoverPlacement.d.ts +36 -0
- package/dist/lib/popoverPlacement.js +29 -0
- package/dist/lib/stateSurface.d.ts +28 -0
- package/dist/lib/stateSurface.js +30 -0
- package/dist/lib/themedStyles.d.ts +1 -1
- package/dist/lib/useAnimatedValue.d.ts +15 -0
- package/dist/lib/useAnimatedValue.js +19 -0
- package/dist/lib/webHitTarget.d.ts +40 -0
- package/dist/lib/webHitTarget.js +37 -0
- package/dist/state/feedbackStore.d.ts +24 -0
- package/dist/state/feedbackStore.js +16 -0
- package/dist/state/globalUIStore.d.ts +27 -0
- package/dist/state/globalUIStore.js +23 -0
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +2 -0
- package/dist/state/initialScheme.d.ts +27 -0
- package/dist/state/initialScheme.js +29 -0
- package/dist/state/notify.d.ts +1 -1
- package/dist/state/themeColorScope.d.ts +1 -1
- package/dist/state/themeStore.d.ts +69 -16
- package/dist/state/themeStore.js +65 -38
- package/llms-full.md +56 -8
- package/llms.txt +10 -1
- package/package.json +69 -2
|
@@ -1,25 +1,26 @@
|
|
|
1
|
+
import type { SerifPreset } from "../constants/fonts.js";
|
|
1
2
|
interface LoadResourcesResult {
|
|
2
3
|
loaded: boolean;
|
|
3
4
|
error: Error | null;
|
|
4
5
|
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export declare const useResources: () => LoadResourcesResult;
|
|
6
|
+
export interface UseResourcesOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Which serif to load and switch the `serif` variant to. `"georgia"`
|
|
9
|
+
* (default) loads nothing: the system face. `"newsreader"` injects the
|
|
10
|
+
* Google Fonts Newsreader stylesheet on web and, on native, registers the
|
|
11
|
+
* files passed as `serifFonts`, then sets the theme store's `serifPreset`
|
|
12
|
+
* so serif text resolves to Newsreader. An app `setFonts` serif override
|
|
13
|
+
* skips the preset entirely (the override wins anyway).
|
|
14
|
+
*/
|
|
15
|
+
serif?: SerifPreset;
|
|
16
|
+
/**
|
|
17
|
+
* Native font map for the serif preset, from the app's own dependency:
|
|
18
|
+
* `@expo-google-fonts/newsreader/400Regular` … `700Bold` and
|
|
19
|
+
* `400Regular_Italic` (the names `newsreaderFamilies.native` expects). The
|
|
20
|
+
* package does not depend on the font package, so an app that never opts in
|
|
21
|
+
* ships none of it; on web this is ignored (the stylesheet serves the faces).
|
|
22
|
+
*/
|
|
23
|
+
serifFonts?: Record<string, number | string> | null;
|
|
24
|
+
}
|
|
25
|
+
export declare const useResources: (options?: UseResourcesOptions) => LoadResourcesResult;
|
|
25
26
|
export {};
|
|
@@ -1,96 +1,100 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import * as Font from "expo-font";
|
|
3
|
-
import { Inter_400Regular, Inter_500Medium, Inter_600SemiBold, Inter_700Bold, } from "@expo-google-fonts/inter";
|
|
4
3
|
import { Platform } from "react-native";
|
|
4
|
+
import { interFontMap } from "../lib/interFonts";
|
|
5
5
|
import { useThemeStore } from "../state/themeStore.js";
|
|
6
|
-
// The four static Inter weights StyledText's native family keys point at
|
|
7
|
-
// (see constants/fonts.ts). Native-only: web never renders these family names
|
|
8
|
-
// ("Inter_400Regular" etc.) — fontFamilies.sansSerif resolves every weight to
|
|
9
|
-
// the single "Inter" CSS family on web (loaded via ensureWebFontStylesheet
|
|
10
|
-
// below), so fetching these .ttf assets there would just be ~1.3MB of dead
|
|
11
|
-
// weight with nothing pointing at them.
|
|
12
|
-
const interFontMap = {
|
|
13
|
-
Inter_400Regular,
|
|
14
|
-
Inter_500Medium,
|
|
15
|
-
Inter_600SemiBold,
|
|
16
|
-
Inter_700Bold,
|
|
17
|
-
};
|
|
18
6
|
function loadNativeInterFonts() {
|
|
19
|
-
if (Platform.OS === "web") {
|
|
7
|
+
if (Platform.OS === "web" || !interFontMap) {
|
|
20
8
|
return Promise.resolve();
|
|
21
9
|
}
|
|
22
10
|
return Font.loadAsync(interFontMap);
|
|
23
11
|
}
|
|
24
12
|
const INTER_STYLESHEET_ID = "mrmeg-expo-ui-inter";
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
// Four upright weights plus the 400 italic, so `italic` renders a real italic
|
|
14
|
+
// on web at body weight (other weights synthesize).
|
|
15
|
+
const INTER_STYLESHEET_URL = "https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap";
|
|
16
|
+
const NEWSREADER_STYLESHEET_ID = "mrmeg-expo-ui-newsreader";
|
|
17
|
+
const NEWSREADER_STYLESHEET_URL = "https://fonts.googleapis.com/css2?family=Newsreader:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap";
|
|
18
|
+
function ensureWebFontStylesheet(id, href, label) {
|
|
27
19
|
if (Platform.OS !== "web" || typeof document === "undefined") {
|
|
28
20
|
return Promise.resolve();
|
|
29
21
|
}
|
|
30
|
-
if (document.getElementById(
|
|
22
|
+
if (document.getElementById(id)) {
|
|
31
23
|
return Promise.resolve();
|
|
32
24
|
}
|
|
33
25
|
return new Promise((resolve, reject) => {
|
|
34
26
|
const link = document.createElement("link");
|
|
35
|
-
link.id =
|
|
27
|
+
link.id = id;
|
|
36
28
|
link.rel = "stylesheet";
|
|
37
|
-
link.href =
|
|
29
|
+
link.href = href;
|
|
38
30
|
link.onload = () => resolve();
|
|
39
|
-
link.onerror = () => reject(new Error(
|
|
31
|
+
link.onerror = () => reject(new Error(`${label} stylesheet failed to load`));
|
|
40
32
|
document.head.appendChild(link);
|
|
41
33
|
});
|
|
42
34
|
}
|
|
35
|
+
let warnedMissingSerifFonts = false;
|
|
43
36
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* font files. Web loads Inter from Google Fonts as a single CSS family;
|
|
49
|
-
* weight differentiation there comes from a numeric fontWeight instead.
|
|
50
|
-
*
|
|
51
|
-
* Icons need nothing here: `Icon` renders `lucide-react-native` SVGs, which
|
|
52
|
-
* have no font face to register on any platform, so server-rendered HTML
|
|
53
|
-
* carries the glyph markup and hydrates without a font round trip.
|
|
54
|
-
*
|
|
55
|
-
* A host app that overrides the sans-serif families via `setFonts` owns
|
|
56
|
-
* loading its own faces (typically through `expo-font`), so the Inter fetch
|
|
57
|
-
* is skipped entirely — nothing would reference those files. For the skip to
|
|
58
|
-
* apply, call `setFonts` before this hook mounts (module scope or ahead of
|
|
59
|
-
* rendering the root); a later call still re-skins text, it just doesn't
|
|
60
|
-
* un-download Inter.
|
|
37
|
+
* The Newsreader preset: the stylesheet on web (the preset switches at once;
|
|
38
|
+
* the stack falls back to Georgia until the faces arrive), the app-supplied
|
|
39
|
+
* files on native (the preset switches after they register, since a native
|
|
40
|
+
* font name that is not loaded is an error, not a fallback).
|
|
61
41
|
*/
|
|
62
|
-
|
|
42
|
+
async function loadSerifPreset(options) {
|
|
43
|
+
if (options.serif !== "newsreader")
|
|
44
|
+
return;
|
|
45
|
+
if (useThemeStore.getState().fontOverrides.families?.serif)
|
|
46
|
+
return;
|
|
47
|
+
if (Platform.OS === "web") {
|
|
48
|
+
useThemeStore.getState().setSerifPreset("newsreader");
|
|
49
|
+
await ensureWebFontStylesheet(NEWSREADER_STYLESHEET_ID, NEWSREADER_STYLESHEET_URL, "Newsreader");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!options.serifFonts) {
|
|
53
|
+
if (!warnedMissingSerifFonts) {
|
|
54
|
+
warnedMissingSerifFonts = true;
|
|
55
|
+
console.warn("useResources({ serif: \"newsreader\" }) needs `serifFonts` on native: pass the map of " +
|
|
56
|
+
"@expo-google-fonts/newsreader faces (400Regular, 500Medium, 600SemiBold, 700Bold, " +
|
|
57
|
+
"400Regular_Italic). Keeping Georgia.");
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await Font.loadAsync(options.serifFonts);
|
|
62
|
+
useThemeStore.getState().setSerifPreset("newsreader");
|
|
63
|
+
}
|
|
64
|
+
export const useResources = (options = {}) => {
|
|
63
65
|
const [loaded, setLoaded] = useState(false);
|
|
64
66
|
const [error, setError] = useState(null);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// Read once at mount, not subscribed: font loading is a one-shot startup
|
|
68
|
-
// effect and cannot be undone by a later override.
|
|
69
|
-
const sansSerifOverridden = !!useThemeStore.getState().fontOverrides.families?.sansSerif;
|
|
70
|
-
async function loadResourcesAndDataAsync() {
|
|
71
|
-
try {
|
|
72
|
-
const fontPromise = Promise.all([
|
|
73
|
-
sansSerifOverridden ? Promise.resolve() : loadNativeInterFonts(),
|
|
74
|
-
sansSerifOverridden ? Promise.resolve() : ensureWebFontStylesheet(),
|
|
75
|
-
]);
|
|
76
|
-
// Timeout after 5 seconds — proceed with system fallback fonts
|
|
77
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
78
|
-
timeoutId = setTimeout(() => reject(new Error("Font loading timed out after 5s")), 5000);
|
|
79
|
-
});
|
|
80
|
-
await Promise.race([fontPromise, timeoutPromise]);
|
|
81
|
-
}
|
|
82
|
-
catch (e) {
|
|
83
|
-
const error = e instanceof Error ? e : new Error(String(e));
|
|
84
|
-
console.warn("Font loading issue (proceeding with fallback):", error.message);
|
|
85
|
-
setError(error);
|
|
86
|
-
}
|
|
87
|
-
finally {
|
|
88
|
-
clearTimeout(timeoutId);
|
|
89
|
-
setLoaded(true);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
loadResourcesAndDataAsync();
|
|
93
|
-
return () => clearTimeout(timeoutId);
|
|
94
|
-
}, []);
|
|
67
|
+
const { serif, serifFonts } = options;
|
|
68
|
+
useEffect(() => startResourceLoad({ serif, serifFonts }, setError, () => setLoaded(true)), [serif, serifFonts]);
|
|
95
69
|
return { loaded, error };
|
|
96
70
|
};
|
|
71
|
+
function startResourceLoad(options, onError, onLoaded) {
|
|
72
|
+
let timeoutId;
|
|
73
|
+
const sansSerifOverridden = !!useThemeStore.getState().fontOverrides.families?.sansSerif;
|
|
74
|
+
async function loadResourcesAndDataAsync() {
|
|
75
|
+
try {
|
|
76
|
+
const fontPromise = Promise.all([
|
|
77
|
+
sansSerifOverridden ? Promise.resolve() : loadNativeInterFonts(),
|
|
78
|
+
sansSerifOverridden
|
|
79
|
+
? Promise.resolve()
|
|
80
|
+
: ensureWebFontStylesheet(INTER_STYLESHEET_ID, INTER_STYLESHEET_URL, "Inter"),
|
|
81
|
+
loadSerifPreset(options),
|
|
82
|
+
]);
|
|
83
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
84
|
+
timeoutId = setTimeout(() => reject(new Error("Font loading timed out after 5s")), 5000);
|
|
85
|
+
});
|
|
86
|
+
await Promise.race([fontPromise, timeoutPromise]);
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
90
|
+
console.warn("Font loading issue (proceeding with fallback):", error.message);
|
|
91
|
+
onError(error);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
clearTimeout(timeoutId);
|
|
95
|
+
onLoaded();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
loadResourcesAndDataAsync();
|
|
99
|
+
return () => clearTimeout(timeoutId);
|
|
100
|
+
}
|
|
@@ -6,10 +6,12 @@ interface ScalePressOptions {
|
|
|
6
6
|
*/
|
|
7
7
|
scaleTo?: number;
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Haptic on press-in. `"setting"` follows the provider-level `haptics`
|
|
10
|
+
* setting (a light tap only under `"all"`); `true` always taps; `false`
|
|
11
|
+
* never does.
|
|
12
|
+
* @default "setting"
|
|
11
13
|
*/
|
|
12
|
-
haptic?: boolean;
|
|
14
|
+
haptic?: boolean | "setting";
|
|
13
15
|
/**
|
|
14
16
|
* Spring damping for bounce-back
|
|
15
17
|
* @default 20
|
|
@@ -29,8 +31,9 @@ interface ScalePressOptions {
|
|
|
29
31
|
/**
|
|
30
32
|
* Hook for press-feedback scale animation using React Native Animated.
|
|
31
33
|
*
|
|
32
|
-
* Returns an animated style and onPressIn/onPressOut handlers to spread onto a
|
|
33
|
-
*
|
|
34
|
+
* Returns an animated style and onPressIn/onPressOut handlers to spread onto a
|
|
35
|
+
* Pressable. Under reduced motion the scale stays at 1 — the pressed opacity
|
|
36
|
+
* the components layer on carries the feedback instead.
|
|
34
37
|
*
|
|
35
38
|
* @example
|
|
36
39
|
* ```tsx
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { useCallback, useMemo
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
2
|
import { Animated } from "react-native";
|
|
3
|
-
import { hapticLight } from "../lib/haptics.js";
|
|
3
|
+
import { hapticLight, hapticPress } from "../lib/haptics.js";
|
|
4
|
+
import { useAnimatedValue } from "../lib/useAnimatedValue.js";
|
|
5
|
+
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
4
6
|
import { useReducedMotion } from "./useReduceMotion.js";
|
|
5
7
|
/**
|
|
6
8
|
* Hook for press-feedback scale animation using React Native Animated.
|
|
7
9
|
*
|
|
8
|
-
* Returns an animated style and onPressIn/onPressOut handlers to spread onto a
|
|
9
|
-
*
|
|
10
|
+
* Returns an animated style and onPressIn/onPressOut handlers to spread onto a
|
|
11
|
+
* Pressable. Under reduced motion the scale stays at 1 — the pressed opacity
|
|
12
|
+
* the components layer on carries the feedback instead.
|
|
10
13
|
*
|
|
11
14
|
* @example
|
|
12
15
|
* ```tsx
|
|
@@ -20,27 +23,31 @@ import { useReducedMotion } from "./useReduceMotion.js";
|
|
|
20
23
|
* ```
|
|
21
24
|
*/
|
|
22
25
|
export function useScalePress(options = {}) {
|
|
23
|
-
const { scaleTo = 0.97, haptic =
|
|
26
|
+
const { scaleTo = 0.97, haptic = "setting", damping = 20, stiffness = 300, disabled = false, } = options;
|
|
24
27
|
const reduceMotion = useReducedMotion();
|
|
25
|
-
const scale =
|
|
28
|
+
const scale = useAnimatedValue(1);
|
|
26
29
|
const animateTo = useCallback((toValue) => {
|
|
27
30
|
scale.stopAnimation();
|
|
31
|
+
// Reduced motion: no scale change at all (not even an instant jump), so
|
|
32
|
+
// the surface stays still while its pressed opacity does the signalling.
|
|
28
33
|
if (reduceMotion) {
|
|
29
|
-
scale.setValue(
|
|
34
|
+
scale.setValue(1);
|
|
30
35
|
return;
|
|
31
36
|
}
|
|
32
37
|
Animated.spring(scale, {
|
|
33
38
|
toValue,
|
|
34
39
|
damping,
|
|
35
40
|
stiffness,
|
|
36
|
-
useNativeDriver:
|
|
41
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
37
42
|
}).start();
|
|
38
43
|
}, [damping, reduceMotion, scale, stiffness]);
|
|
39
44
|
const onPressIn = useCallback(() => {
|
|
40
45
|
if (disabled)
|
|
41
46
|
return;
|
|
42
|
-
if (haptic)
|
|
47
|
+
if (haptic === true)
|
|
43
48
|
hapticLight();
|
|
49
|
+
else if (haptic === "setting")
|
|
50
|
+
hapticPress();
|
|
44
51
|
animateTo(scaleTo);
|
|
45
52
|
}, [animateTo, disabled, haptic, scaleTo]);
|
|
46
53
|
const onPressOut = useCallback(() => {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ShapeOverrides, type ShapeSlot } from "../state/themeStore.js";
|
|
2
|
+
/**
|
|
3
|
+
* The host app's `setShape` override for one slot, or `undefined` when the
|
|
4
|
+
* slot is untouched. One store subscription per component, re-rendering only
|
|
5
|
+
* when that slot changes.
|
|
6
|
+
*
|
|
7
|
+
* Layer it right after the component's static radius and before the caller's
|
|
8
|
+
* `style`, so the precedence stays caller → app → package:
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const cardShape = useShape("card");
|
|
12
|
+
* <View style={[styles.card, shapeRadius(cardShape), style]} />
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function useShape<S extends ShapeSlot>(slot: S): ShapeOverrides[S] | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* `{ borderRadius }` when the override sets one (zero included — square
|
|
18
|
+
* corners are a choice), otherwise `undefined` so a style array skips it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function shapeRadius(override: {
|
|
21
|
+
borderRadius?: number;
|
|
22
|
+
} | undefined): {
|
|
23
|
+
borderRadius: number;
|
|
24
|
+
} | undefined;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useThemeStore } from "../state/themeStore.js";
|
|
2
|
+
/**
|
|
3
|
+
* The host app's `setShape` override for one slot, or `undefined` when the
|
|
4
|
+
* slot is untouched. One store subscription per component, re-rendering only
|
|
5
|
+
* when that slot changes.
|
|
6
|
+
*
|
|
7
|
+
* Layer it right after the component's static radius and before the caller's
|
|
8
|
+
* `style`, so the precedence stays caller → app → package:
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const cardShape = useShape("card");
|
|
12
|
+
* <View style={[styles.card, shapeRadius(cardShape), style]} />
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function useShape(slot) {
|
|
16
|
+
return useThemeStore((s) => s.shapeOverrides[slot]);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* `{ borderRadius }` when the override sets one (zero included — square
|
|
20
|
+
* corners are a choice), otherwise `undefined` so a style array skips it.
|
|
21
|
+
*/
|
|
22
|
+
export function shapeRadius(override) {
|
|
23
|
+
return override?.borderRadius === undefined ? undefined : { borderRadius: override.borderRadius };
|
|
24
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { useEffect, useMemo
|
|
1
|
+
import { useEffect, useMemo } from "react";
|
|
2
2
|
import { Animated, Easing } from "react-native";
|
|
3
3
|
import { useReducedMotion } from "./useReduceMotion.js";
|
|
4
4
|
import { durations } from "../constants/motion.js";
|
|
5
|
+
import { useAnimatedValue } from "../lib/useAnimatedValue.js";
|
|
6
|
+
import { shouldUseNativeDriver } from "../lib/animations.js";
|
|
5
7
|
/**
|
|
6
8
|
* Hook for entrance animations with stagger support using React Native Animated.
|
|
7
9
|
*
|
|
@@ -24,15 +26,15 @@ import { durations } from "../constants/motion.js";
|
|
|
24
26
|
export function useStaggeredEntrance(options = {}) {
|
|
25
27
|
const { type = "fadeSlideUp", delay = 0, duration = durations.normal, slideDistance = 8, initialScale = 0.95, } = options;
|
|
26
28
|
const reduceMotion = useReducedMotion();
|
|
27
|
-
const opacity =
|
|
28
|
-
const translateY =
|
|
29
|
+
const opacity = useAnimatedValue(reduceMotion ? 1 : 0);
|
|
30
|
+
const translateY = useAnimatedValue(reduceMotion
|
|
29
31
|
? 0
|
|
30
32
|
: type === "fadeSlideUp"
|
|
31
33
|
? slideDistance
|
|
32
34
|
: type === "fadeSlideDown"
|
|
33
35
|
? -slideDistance
|
|
34
|
-
: 0)
|
|
35
|
-
const scale =
|
|
36
|
+
: 0);
|
|
37
|
+
const scale = useAnimatedValue(reduceMotion ? 1 : type === "scale" ? initialScale : 1);
|
|
36
38
|
useEffect(() => {
|
|
37
39
|
if (reduceMotion) {
|
|
38
40
|
opacity.setValue(1);
|
|
@@ -50,7 +52,7 @@ export function useStaggeredEntrance(options = {}) {
|
|
|
50
52
|
const timingConfig = {
|
|
51
53
|
duration,
|
|
52
54
|
easing: Easing.out(Easing.cubic),
|
|
53
|
-
useNativeDriver:
|
|
55
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
54
56
|
};
|
|
55
57
|
const animations = [
|
|
56
58
|
Animated.timing(opacity, {
|
|
@@ -69,7 +71,7 @@ export function useStaggeredEntrance(options = {}) {
|
|
|
69
71
|
toValue: 1,
|
|
70
72
|
damping: 14,
|
|
71
73
|
stiffness: 250,
|
|
72
|
-
useNativeDriver:
|
|
74
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
73
75
|
}));
|
|
74
76
|
}
|
|
75
77
|
const animation = delay > 0
|
package/dist/hooks/useTheme.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Colors } from "../constants/colors";
|
|
1
|
+
import { Colors } from "../constants/colors.js";
|
|
2
2
|
import { ImageStyle, TextStyle, ViewStyle } from "react-native";
|
|
3
|
-
import { spacing as spacingConstants } from "../constants/spacing";
|
|
3
|
+
import { spacing as spacingConstants } from "../constants/spacing.js";
|
|
4
4
|
type ShadowType = "base" | "soft" | "sharp" | "subtle" | "elevated" | "glow" | "glass" | "card" | "cardHover" | "cardSubtle";
|
|
5
5
|
interface ExtendedColorScheme {
|
|
6
6
|
theme: Colors["light" | "dark"];
|
|
@@ -32,7 +32,7 @@ interface ExtendedColorScheme {
|
|
|
32
32
|
* - getTextColorForBackground("#000") → "light"
|
|
33
33
|
* - getContrastingColor("#f4f4f4", "#222", "#fff") → "#222"
|
|
34
34
|
* - withAlpha("#336699", 0.6) → "rgba(51,102,153,0.6)"
|
|
35
|
-
* - getShadowStyle('base') → { boxShadow: "0px 2px 6px rgba(0, 0, 0, 0.
|
|
35
|
+
* - getShadowStyle('base') → { boxShadow: "0px 2px 6px rgba(0, 0, 0, 0.1), 0px 4px 12px rgba(0, 0, 0, 0.06)" } (light)
|
|
36
36
|
*/
|
|
37
37
|
export declare function useTheme(): ExtendedColorScheme & {
|
|
38
38
|
toggleTheme: () => void;
|