@mrmeg/expo-ui 0.1.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/README.md +96 -0
- package/dist/components/Accordion.d.ts +54 -0
- package/dist/components/Accordion.js +149 -0
- package/dist/components/Alert.d.ts +30 -0
- package/dist/components/Alert.js +25 -0
- package/dist/components/AnimatedView.d.ts +55 -0
- package/dist/components/AnimatedView.js +39 -0
- package/dist/components/Badge.d.ts +23 -0
- package/dist/components/Badge.js +74 -0
- package/dist/components/BottomSheet.d.ts +74 -0
- package/dist/components/BottomSheet.js +513 -0
- package/dist/components/Button.d.ts +129 -0
- package/dist/components/Button.js +216 -0
- package/dist/components/Card.d.ts +42 -0
- package/dist/components/Card.js +126 -0
- package/dist/components/Checkbox.d.ts +39 -0
- package/dist/components/Checkbox.js +96 -0
- package/dist/components/Collapsible.d.ts +67 -0
- package/dist/components/Collapsible.js +38 -0
- package/dist/components/Dialog.d.ts +140 -0
- package/dist/components/Dialog.js +167 -0
- package/dist/components/DismissKeyboard.d.ts +15 -0
- package/dist/components/DismissKeyboard.js +13 -0
- package/dist/components/Drawer.d.ts +74 -0
- package/dist/components/Drawer.js +423 -0
- package/dist/components/DropdownMenu.d.ts +120 -0
- package/dist/components/DropdownMenu.js +211 -0
- package/dist/components/EmptyState.d.ts +42 -0
- package/dist/components/EmptyState.js +58 -0
- package/dist/components/ErrorBoundary.d.ts +53 -0
- package/dist/components/ErrorBoundary.js +75 -0
- package/dist/components/Icon.d.ts +46 -0
- package/dist/components/Icon.js +40 -0
- package/dist/components/InputOTP.d.ts +72 -0
- package/dist/components/InputOTP.js +155 -0
- package/dist/components/Label.d.ts +61 -0
- package/dist/components/Label.js +72 -0
- package/dist/components/MaxWidthContainer.d.ts +58 -0
- package/dist/components/MaxWidthContainer.js +64 -0
- package/dist/components/Notification.d.ts +26 -0
- package/dist/components/Notification.js +230 -0
- package/dist/components/Popover.d.ts +79 -0
- package/dist/components/Popover.js +91 -0
- package/dist/components/Progress.d.ts +28 -0
- package/dist/components/Progress.js +107 -0
- package/dist/components/RadioGroup.d.ts +65 -0
- package/dist/components/RadioGroup.js +142 -0
- package/dist/components/Select.d.ts +88 -0
- package/dist/components/Select.js +172 -0
- package/dist/components/Separator.d.ts +83 -0
- package/dist/components/Separator.js +85 -0
- package/dist/components/Skeleton.d.ts +68 -0
- package/dist/components/Skeleton.js +99 -0
- package/dist/components/Slider.d.ts +24 -0
- package/dist/components/Slider.js +162 -0
- package/dist/components/StatusBar.d.ts +1 -0
- package/dist/components/StatusBar.js +19 -0
- package/dist/components/StyledText.d.ts +161 -0
- package/dist/components/StyledText.js +193 -0
- package/dist/components/Switch.d.ts +44 -0
- package/dist/components/Switch.js +129 -0
- package/dist/components/Tabs.d.ts +31 -0
- package/dist/components/Tabs.js +127 -0
- package/dist/components/TextInput.d.ts +120 -0
- package/dist/components/TextInput.js +263 -0
- package/dist/components/Toggle.d.ts +106 -0
- package/dist/components/Toggle.js +150 -0
- package/dist/components/ToggleGroup.d.ts +80 -0
- package/dist/components/ToggleGroup.js +189 -0
- package/dist/components/Tooltip.d.ts +121 -0
- package/dist/components/Tooltip.js +132 -0
- package/dist/components/index.d.ts +35 -0
- package/dist/components/index.js +35 -0
- package/dist/constants/colors.d.ts +82 -0
- package/dist/constants/colors.js +116 -0
- package/dist/constants/fonts.d.ts +32 -0
- package/dist/constants/fonts.js +91 -0
- package/dist/constants/index.d.ts +3 -0
- package/dist/constants/index.js +3 -0
- package/dist/constants/spacing.d.ts +40 -0
- package/dist/constants/spacing.js +48 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/useDimensions.d.ts +19 -0
- package/dist/hooks/useDimensions.js +55 -0
- package/dist/hooks/useReduceMotion.d.ts +5 -0
- package/dist/hooks/useReduceMotion.js +64 -0
- package/dist/hooks/useResources.d.ts +12 -0
- package/dist/hooks/useResources.js +56 -0
- package/dist/hooks/useScalePress.d.ts +57 -0
- package/dist/hooks/useScalePress.js +55 -0
- package/dist/hooks/useStaggeredEntrance.d.ts +67 -0
- package/dist/hooks/useStaggeredEntrance.js +74 -0
- package/dist/hooks/useTheme.d.ts +88 -0
- package/dist/hooks/useTheme.js +328 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/lib/animations.d.ts +1 -0
- package/dist/lib/animations.js +3 -0
- package/dist/lib/haptics.d.ts +3 -0
- package/dist/lib/haptics.js +29 -0
- package/dist/lib/index.d.ts +3 -0
- package/dist/lib/index.js +3 -0
- package/dist/lib/sentry.d.ts +16 -0
- package/dist/lib/sentry.js +55 -0
- package/dist/state/globalUIStore.d.ts +30 -0
- package/dist/state/globalUIStore.js +8 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.js +2 -0
- package/dist/state/themeStore.d.ts +6 -0
- package/dist/state/themeStore.js +38 -0
- package/package.json +92 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { Dimensions, Platform } from "react-native";
|
|
3
|
+
export const SCREEN_SIZES = {
|
|
4
|
+
SMALL: 768,
|
|
5
|
+
MEDIUM: 1000,
|
|
6
|
+
LARGE: 1200,
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Provides a consistent way to access window dimensions and screen size information across mobile and web.
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
export const useDimensions = () => {
|
|
13
|
+
const isWeb = Platform.OS === "web";
|
|
14
|
+
const [dimensions, setDimensions] = useState({
|
|
15
|
+
width: 0,
|
|
16
|
+
height: 0,
|
|
17
|
+
orientation: "portrait",
|
|
18
|
+
isSmallScreen: true,
|
|
19
|
+
isMediumScreen: false,
|
|
20
|
+
isLargeScreen: false,
|
|
21
|
+
});
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const initialDimensions = isWeb
|
|
24
|
+
? { width: window.innerWidth, height: window.innerHeight }
|
|
25
|
+
: Dimensions.get("window");
|
|
26
|
+
const updateDimensions = (width, height) => {
|
|
27
|
+
const orientation = width > height ? "landscape" : "portrait";
|
|
28
|
+
setDimensions({
|
|
29
|
+
width,
|
|
30
|
+
height,
|
|
31
|
+
orientation,
|
|
32
|
+
isSmallScreen: width <= SCREEN_SIZES.SMALL,
|
|
33
|
+
isMediumScreen: width > SCREEN_SIZES.SMALL,
|
|
34
|
+
isLargeScreen: width > SCREEN_SIZES.MEDIUM,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
updateDimensions(initialDimensions.width, initialDimensions.height);
|
|
38
|
+
if (isWeb) {
|
|
39
|
+
const handleResize = () => {
|
|
40
|
+
updateDimensions(window.innerWidth, window.innerHeight);
|
|
41
|
+
};
|
|
42
|
+
window.addEventListener("resize", handleResize);
|
|
43
|
+
return () => {
|
|
44
|
+
window.removeEventListener("resize", handleResize);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const onChange = ({ window }) => {
|
|
49
|
+
updateDimensions(window.width, window.height);
|
|
50
|
+
};
|
|
51
|
+
Dimensions.addEventListener("change", onChange);
|
|
52
|
+
}
|
|
53
|
+
}, [isWeb]);
|
|
54
|
+
return dimensions;
|
|
55
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { AccessibilityInfo, Platform } from "react-native";
|
|
3
|
+
let sharedValue = false;
|
|
4
|
+
let listenerCount = 0;
|
|
5
|
+
let subscription = null;
|
|
6
|
+
function startListening() {
|
|
7
|
+
if (Platform.OS === "web") {
|
|
8
|
+
// On web, use the media query
|
|
9
|
+
if (typeof window !== "undefined" && window.matchMedia) {
|
|
10
|
+
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
11
|
+
sharedValue = mq.matches;
|
|
12
|
+
const handler = (e) => { sharedValue = e.matches; };
|
|
13
|
+
mq.addEventListener("change", handler);
|
|
14
|
+
subscription = { remove: () => mq.removeEventListener("change", handler) };
|
|
15
|
+
}
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
AccessibilityInfo.isReduceMotionEnabled().then((enabled) => {
|
|
19
|
+
sharedValue = enabled;
|
|
20
|
+
});
|
|
21
|
+
const sub = AccessibilityInfo.addEventListener("reduceMotionChanged", (enabled) => {
|
|
22
|
+
sharedValue = enabled;
|
|
23
|
+
});
|
|
24
|
+
subscription = sub;
|
|
25
|
+
}
|
|
26
|
+
function stopListening() {
|
|
27
|
+
subscription?.remove();
|
|
28
|
+
subscription = null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Hook that returns whether the user prefers reduced motion.
|
|
32
|
+
* Uses a shared singleton listener so multiple consumers don't create duplicate subscriptions.
|
|
33
|
+
*/
|
|
34
|
+
export function useReducedMotion() {
|
|
35
|
+
const [reduceMotion, setReduceMotion] = useState(sharedValue);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
listenerCount++;
|
|
38
|
+
if (listenerCount === 1) {
|
|
39
|
+
startListening();
|
|
40
|
+
}
|
|
41
|
+
// Re-read current value on mount (may have changed since last render)
|
|
42
|
+
if (Platform.OS === "web") {
|
|
43
|
+
if (typeof window !== "undefined" && window.matchMedia) {
|
|
44
|
+
const current = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
45
|
+
setReduceMotion(current);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);
|
|
50
|
+
}
|
|
51
|
+
// Poll the shared value to pick up changes (lightweight — only boolean comparison)
|
|
52
|
+
const interval = setInterval(() => {
|
|
53
|
+
setReduceMotion((prev) => (prev !== sharedValue ? sharedValue : prev));
|
|
54
|
+
}, 500);
|
|
55
|
+
return () => {
|
|
56
|
+
clearInterval(interval);
|
|
57
|
+
listenerCount--;
|
|
58
|
+
if (listenerCount === 0) {
|
|
59
|
+
stopListening();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}, []);
|
|
63
|
+
return reduceMotion;
|
|
64
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface LoadResourcesResult {
|
|
2
|
+
loaded: boolean;
|
|
3
|
+
error: Error | null;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Loads essential app resources on startup.
|
|
7
|
+
*
|
|
8
|
+
* The UI package does not bundle font files. Web loads Lato from Google Fonts;
|
|
9
|
+
* native platforms use their system sans-serif fallback.
|
|
10
|
+
*/
|
|
11
|
+
export declare const useResources: () => LoadResourcesResult;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import * as Font from "expo-font";
|
|
3
|
+
import Feather from "@expo/vector-icons/Feather";
|
|
4
|
+
import { Platform } from "react-native";
|
|
5
|
+
const LATO_STYLESHEET_ID = "mrmeg-expo-ui-lato";
|
|
6
|
+
const LATO_STYLESHEET_URL = "https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap";
|
|
7
|
+
function ensureWebFontStylesheet() {
|
|
8
|
+
if (Platform.OS !== "web" || typeof document === "undefined") {
|
|
9
|
+
return Promise.resolve();
|
|
10
|
+
}
|
|
11
|
+
if (document.getElementById(LATO_STYLESHEET_ID)) {
|
|
12
|
+
return Promise.resolve();
|
|
13
|
+
}
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const link = document.createElement("link");
|
|
16
|
+
link.id = LATO_STYLESHEET_ID;
|
|
17
|
+
link.rel = "stylesheet";
|
|
18
|
+
link.href = LATO_STYLESHEET_URL;
|
|
19
|
+
link.onload = () => resolve();
|
|
20
|
+
link.onerror = () => reject(new Error("Lato stylesheet failed to load"));
|
|
21
|
+
document.head.appendChild(link);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Loads essential app resources on startup.
|
|
26
|
+
*
|
|
27
|
+
* The UI package does not bundle font files. Web loads Lato from Google Fonts;
|
|
28
|
+
* native platforms use their system sans-serif fallback.
|
|
29
|
+
*/
|
|
30
|
+
export const useResources = () => {
|
|
31
|
+
const [loaded, setLoaded] = useState(false);
|
|
32
|
+
const [error, setError] = useState(null);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
async function loadResourcesAndDataAsync() {
|
|
35
|
+
try {
|
|
36
|
+
const fontPromise = Promise.all([
|
|
37
|
+
Font.loadAsync(Feather.font),
|
|
38
|
+
ensureWebFontStylesheet(),
|
|
39
|
+
]);
|
|
40
|
+
// Timeout after 5 seconds — proceed with system fallback fonts
|
|
41
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Font loading timed out after 5s")), 5000));
|
|
42
|
+
await Promise.race([fontPromise, timeoutPromise]);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
46
|
+
console.warn("Font loading issue (proceeding with fallback):", error.message);
|
|
47
|
+
setError(error);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
setLoaded(true);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
loadResourcesAndDataAsync();
|
|
54
|
+
}, []);
|
|
55
|
+
return { loaded, error };
|
|
56
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
interface ScalePressOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Scale value when pressed (1 = no change, 0.97 = subtle, 0.93 = more pronounced)
|
|
4
|
+
* @default 0.97
|
|
5
|
+
*/
|
|
6
|
+
scaleTo?: number;
|
|
7
|
+
/**
|
|
8
|
+
* Whether to fire haptic feedback on press
|
|
9
|
+
* @default true
|
|
10
|
+
*/
|
|
11
|
+
haptic?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Spring damping for bounce-back
|
|
14
|
+
* @default 20
|
|
15
|
+
*/
|
|
16
|
+
damping?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Spring stiffness
|
|
19
|
+
* @default 300
|
|
20
|
+
*/
|
|
21
|
+
stiffness?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the component is disabled (skips animation)
|
|
24
|
+
* @default false
|
|
25
|
+
*/
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Hook for press-feedback scale animation using Reanimated.
|
|
30
|
+
*
|
|
31
|
+
* Returns an animated style and onPressIn/onPressOut handlers to spread onto a Pressable.
|
|
32
|
+
* Respects reduced motion preferences.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* const { animatedStyle, pressHandlers } = useScalePress();
|
|
37
|
+
*
|
|
38
|
+
* <Animated.View style={animatedStyle}>
|
|
39
|
+
* <Pressable {...pressHandlers} onPress={handlePress}>
|
|
40
|
+
* <Text>Press me</Text>
|
|
41
|
+
* </Pressable>
|
|
42
|
+
* </Animated.View>
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function useScalePress(options?: ScalePressOptions): {
|
|
46
|
+
animatedStyle: {
|
|
47
|
+
transform: {
|
|
48
|
+
scale: number;
|
|
49
|
+
}[];
|
|
50
|
+
};
|
|
51
|
+
pressHandlers: {
|
|
52
|
+
onPressIn: () => void;
|
|
53
|
+
onPressOut: () => void;
|
|
54
|
+
};
|
|
55
|
+
scale: import("react-native-reanimated").SharedValue<number>;
|
|
56
|
+
};
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useSharedValue, useAnimatedStyle, withSpring, withTiming, useReducedMotion, } from "react-native-reanimated";
|
|
3
|
+
import { hapticLight } from "../lib/haptics";
|
|
4
|
+
/**
|
|
5
|
+
* Hook for press-feedback scale animation using Reanimated.
|
|
6
|
+
*
|
|
7
|
+
* Returns an animated style and onPressIn/onPressOut handlers to spread onto a Pressable.
|
|
8
|
+
* Respects reduced motion preferences.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const { animatedStyle, pressHandlers } = useScalePress();
|
|
13
|
+
*
|
|
14
|
+
* <Animated.View style={animatedStyle}>
|
|
15
|
+
* <Pressable {...pressHandlers} onPress={handlePress}>
|
|
16
|
+
* <Text>Press me</Text>
|
|
17
|
+
* </Pressable>
|
|
18
|
+
* </Animated.View>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function useScalePress(options = {}) {
|
|
22
|
+
const { scaleTo = 0.97, haptic = true, damping = 20, stiffness = 300, disabled = false, } = options;
|
|
23
|
+
const reduceMotion = useReducedMotion();
|
|
24
|
+
const scale = useSharedValue(1);
|
|
25
|
+
const onPressIn = useCallback(() => {
|
|
26
|
+
if (disabled)
|
|
27
|
+
return;
|
|
28
|
+
if (haptic)
|
|
29
|
+
hapticLight();
|
|
30
|
+
if (reduceMotion) {
|
|
31
|
+
scale.value = withTiming(scaleTo, { duration: 0 });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
scale.value = withSpring(scaleTo, { damping, stiffness });
|
|
35
|
+
}
|
|
36
|
+
}, [disabled, haptic, reduceMotion, scale, scaleTo, damping, stiffness]);
|
|
37
|
+
const onPressOut = useCallback(() => {
|
|
38
|
+
if (disabled)
|
|
39
|
+
return;
|
|
40
|
+
if (reduceMotion) {
|
|
41
|
+
scale.value = withTiming(1, { duration: 0 });
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
scale.value = withSpring(1, { damping, stiffness });
|
|
45
|
+
}
|
|
46
|
+
}, [disabled, reduceMotion, scale, damping, stiffness]);
|
|
47
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
48
|
+
transform: [{ scale: scale.value }],
|
|
49
|
+
}));
|
|
50
|
+
return {
|
|
51
|
+
animatedStyle,
|
|
52
|
+
pressHandlers: { onPressIn, onPressOut },
|
|
53
|
+
scale,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
type EntranceType = "fade" | "fadeSlideUp" | "fadeSlideDown" | "scale";
|
|
2
|
+
interface StaggeredEntranceOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Type of entrance animation
|
|
5
|
+
* @default "fadeSlideUp"
|
|
6
|
+
*/
|
|
7
|
+
type?: EntranceType;
|
|
8
|
+
/**
|
|
9
|
+
* Delay before this item starts animating (ms).
|
|
10
|
+
* Use index * staggerMs for staggered lists.
|
|
11
|
+
* @default 0
|
|
12
|
+
*/
|
|
13
|
+
delay?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Duration of the entrance animation (ms)
|
|
16
|
+
* @default 200
|
|
17
|
+
*/
|
|
18
|
+
duration?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Slide distance in pixels (for fadeSlideUp/fadeSlideDown)
|
|
21
|
+
* @default 8
|
|
22
|
+
*/
|
|
23
|
+
slideDistance?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Initial scale (for scale type)
|
|
26
|
+
* @default 0.95
|
|
27
|
+
*/
|
|
28
|
+
initialScale?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Hook for entrance animations with stagger support using Reanimated.
|
|
32
|
+
*
|
|
33
|
+
* Returns an animated style to apply to an Animated.View.
|
|
34
|
+
* Respects reduced motion preferences.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* // Single entrance
|
|
39
|
+
* const entranceStyle = useStaggeredEntrance({ type: "fadeSlideUp" });
|
|
40
|
+
* <Animated.View style={entranceStyle}>...</Animated.View>
|
|
41
|
+
*
|
|
42
|
+
* // Staggered list
|
|
43
|
+
* {items.map((item, i) => {
|
|
44
|
+
* const style = useStaggeredEntrance({ delay: i * 50 });
|
|
45
|
+
* return <Animated.View key={item.id} style={style}>...</Animated.View>;
|
|
46
|
+
* })}
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function useStaggeredEntrance(options?: StaggeredEntranceOptions): {
|
|
50
|
+
opacity: number;
|
|
51
|
+
transform?: undefined;
|
|
52
|
+
} | {
|
|
53
|
+
opacity: number;
|
|
54
|
+
transform: {
|
|
55
|
+
translateY: number;
|
|
56
|
+
}[];
|
|
57
|
+
} | {
|
|
58
|
+
opacity: number;
|
|
59
|
+
transform: {
|
|
60
|
+
scale: number;
|
|
61
|
+
}[];
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Convenience constant: default stagger delay between items (ms)
|
|
65
|
+
*/
|
|
66
|
+
export declare const STAGGER_DELAY = 30;
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useSharedValue, useAnimatedStyle, withTiming, withSpring, withDelay, useReducedMotion, Easing, } from "react-native-reanimated";
|
|
3
|
+
/**
|
|
4
|
+
* Hook for entrance animations with stagger support using Reanimated.
|
|
5
|
+
*
|
|
6
|
+
* Returns an animated style to apply to an Animated.View.
|
|
7
|
+
* Respects reduced motion preferences.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* // Single entrance
|
|
12
|
+
* const entranceStyle = useStaggeredEntrance({ type: "fadeSlideUp" });
|
|
13
|
+
* <Animated.View style={entranceStyle}>...</Animated.View>
|
|
14
|
+
*
|
|
15
|
+
* // Staggered list
|
|
16
|
+
* {items.map((item, i) => {
|
|
17
|
+
* const style = useStaggeredEntrance({ delay: i * 50 });
|
|
18
|
+
* return <Animated.View key={item.id} style={style}>...</Animated.View>;
|
|
19
|
+
* })}
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function useStaggeredEntrance(options = {}) {
|
|
23
|
+
const { type = "fadeSlideUp", delay = 0, duration = 200, slideDistance = 8, initialScale = 0.95, } = options;
|
|
24
|
+
const reduceMotion = useReducedMotion();
|
|
25
|
+
const opacity = useSharedValue(reduceMotion ? 1 : 0);
|
|
26
|
+
const translateY = useSharedValue(reduceMotion
|
|
27
|
+
? 0
|
|
28
|
+
: type === "fadeSlideUp"
|
|
29
|
+
? slideDistance
|
|
30
|
+
: type === "fadeSlideDown"
|
|
31
|
+
? -slideDistance
|
|
32
|
+
: 0);
|
|
33
|
+
const scale = useSharedValue(reduceMotion ? 1 : type === "scale" ? initialScale : 1);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (reduceMotion) {
|
|
36
|
+
opacity.value = 1;
|
|
37
|
+
translateY.value = 0;
|
|
38
|
+
scale.value = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const timingConfig = {
|
|
42
|
+
duration,
|
|
43
|
+
easing: Easing.out(Easing.cubic),
|
|
44
|
+
};
|
|
45
|
+
opacity.value = withDelay(delay, withTiming(1, timingConfig));
|
|
46
|
+
if (type === "fadeSlideUp" || type === "fadeSlideDown") {
|
|
47
|
+
translateY.value = withDelay(delay, withTiming(0, timingConfig));
|
|
48
|
+
}
|
|
49
|
+
if (type === "scale") {
|
|
50
|
+
scale.value = withDelay(delay, withSpring(1, { damping: 14, stiffness: 250 }));
|
|
51
|
+
}
|
|
52
|
+
}, [reduceMotion]);
|
|
53
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
54
|
+
if (type === "fade") {
|
|
55
|
+
return { opacity: opacity.value };
|
|
56
|
+
}
|
|
57
|
+
if (type === "fadeSlideUp" || type === "fadeSlideDown") {
|
|
58
|
+
return {
|
|
59
|
+
opacity: opacity.value,
|
|
60
|
+
transform: [{ translateY: translateY.value }],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// scale
|
|
64
|
+
return {
|
|
65
|
+
opacity: opacity.value,
|
|
66
|
+
transform: [{ scale: scale.value }],
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
return animatedStyle;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Convenience constant: default stagger delay between items (ms)
|
|
73
|
+
*/
|
|
74
|
+
export const STAGGER_DELAY = 30;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Colors } from "../constants/colors";
|
|
2
|
+
import { ViewStyle, StyleSheet } from "react-native";
|
|
3
|
+
import { spacing as spacingConstants } from "../constants/spacing";
|
|
4
|
+
type ShadowType = "base" | "soft" | "sharp" | "subtle";
|
|
5
|
+
interface ExtendedColorScheme {
|
|
6
|
+
theme: Colors["light" | "dark"];
|
|
7
|
+
scheme: "light" | "dark";
|
|
8
|
+
getShadowStyle: (type: ShadowType) => ViewStyle;
|
|
9
|
+
getContrastingColor: (backgroundColor: string, color1?: string, color2?: string) => string;
|
|
10
|
+
getTextColorForBackground: (backgroundColor: string) => "light" | "dark";
|
|
11
|
+
withAlpha: (color: string, alpha: number) => string;
|
|
12
|
+
getContrastRatio: (color1: string, color2: string) => number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* useTheme
|
|
16
|
+
*
|
|
17
|
+
* Provides access to app colors, theme styles, and utilities for color contrast.
|
|
18
|
+
* Includes helpers to determine readable text color for any background.
|
|
19
|
+
*
|
|
20
|
+
* Returns:
|
|
21
|
+
* - theme: active theme colors (light or dark)
|
|
22
|
+
* - scheme: "light" | "dark"
|
|
23
|
+
* - getShadowStyle(type): returns cross-platform shadow style object
|
|
24
|
+
* - getContrastingColor(bg, color1?, color2?): pick best contrast of two options
|
|
25
|
+
* - getTextColorForBackground(bg): returns "light" or "dark"
|
|
26
|
+
* - withAlpha(color, alpha): adds transparency
|
|
27
|
+
* - getContrastRatio(color1, color2): returns numeric WCAG contrast ratio
|
|
28
|
+
*
|
|
29
|
+
* Examples:
|
|
30
|
+
* - getTextColorForBackground("#000") → "light"
|
|
31
|
+
* - getContrastingColor("#f4f4f4", "#222", "#fff") → "#222"
|
|
32
|
+
* - withAlpha("#336699", 0.6) → "rgba(51,102,153,0.6)"
|
|
33
|
+
* - getShadowStyle('base') → { shadowColor, shadowOffset, ... }
|
|
34
|
+
*/
|
|
35
|
+
export declare function useTheme(): ExtendedColorScheme & {
|
|
36
|
+
toggleTheme: () => void;
|
|
37
|
+
setTheme: (theme: "system" | "light" | "dark") => void;
|
|
38
|
+
currentTheme: "system" | "light" | "dark";
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Style factory context passed to the createStyles callback
|
|
42
|
+
*/
|
|
43
|
+
interface StyleContext {
|
|
44
|
+
theme: Colors["light" | "dark"];
|
|
45
|
+
spacing: typeof spacingConstants;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Return type for useStyles hook
|
|
49
|
+
*/
|
|
50
|
+
type UseStylesReturn<T extends StyleSheet.NamedStyles<T>> = {
|
|
51
|
+
styles: T;
|
|
52
|
+
theme: Colors["light" | "dark"];
|
|
53
|
+
spacing: typeof spacingConstants;
|
|
54
|
+
} & Omit<ReturnType<typeof useTheme>, "theme">;
|
|
55
|
+
/**
|
|
56
|
+
* useStyles
|
|
57
|
+
*
|
|
58
|
+
* A hook that combines useTheme with StyleSheet.create for theme-aware styling.
|
|
59
|
+
* Provides access to theme colors and spacing constants within the style factory.
|
|
60
|
+
*
|
|
61
|
+
* @param factory - A function that receives { theme, spacing } and returns style definitions
|
|
62
|
+
* @returns { styles, theme, spacing, ...themeUtilities }
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```tsx
|
|
66
|
+
* function MyComponent() {
|
|
67
|
+
* const { styles, theme } = useStyles(({ theme, spacing }) => ({
|
|
68
|
+
* container: {
|
|
69
|
+
* backgroundColor: theme.colors.background,
|
|
70
|
+
* padding: spacing.md,
|
|
71
|
+
* borderRadius: spacing.radiusMd,
|
|
72
|
+
* },
|
|
73
|
+
* text: {
|
|
74
|
+
* color: theme.colors.textPrimary,
|
|
75
|
+
* fontSize: 16,
|
|
76
|
+
* },
|
|
77
|
+
* }));
|
|
78
|
+
*
|
|
79
|
+
* return (
|
|
80
|
+
* <View style={styles.container}>
|
|
81
|
+
* <Text style={styles.text}>Hello</Text>
|
|
82
|
+
* </View>
|
|
83
|
+
* );
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare function useStyles<T extends StyleSheet.NamedStyles<T>>(factory: (context: StyleContext) => T): UseStylesReturn<T>;
|
|
88
|
+
export {};
|