@mrmeg/expo-ui 0.6.1 → 0.7.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/LLM_USAGE.md +4 -5
- package/README.md +6 -6
- package/dist/components/Accordion.js +21 -16
- package/dist/components/AnimatedView.d.ts +1 -1
- package/dist/components/AnimatedView.js +2 -2
- package/dist/components/Badge.d.ts +3 -2
- package/dist/components/Badge.js +4 -3
- package/dist/components/BottomSheet.js +31 -29
- package/dist/components/BottomSheetKeyboard.d.ts +7 -0
- package/dist/components/BottomSheetKeyboard.js +35 -0
- package/dist/components/Button.d.ts +55 -13
- package/dist/components/Button.js +72 -28
- package/dist/components/Card.js +8 -10
- package/dist/components/Checkbox.js +22 -25
- package/dist/components/Collapsible.js +3 -7
- package/dist/components/Dialog.js +1 -1
- package/dist/components/DismissKeyboard.js +3 -3
- package/dist/components/Drawer.js +21 -10
- package/dist/components/DropdownMenu.d.ts +3 -2
- package/dist/components/DropdownMenu.js +29 -29
- package/dist/components/EmptyState.js +1 -1
- package/dist/components/InputOTP.js +16 -40
- package/dist/components/Notification.js +50 -25
- package/dist/components/Popover.js +1 -1
- package/dist/components/Progress.d.ts +2 -2
- package/dist/components/Progress.js +36 -34
- package/dist/components/RadioGroup.js +22 -20
- package/dist/components/Select.js +30 -20
- package/dist/components/Skeleton.js +6 -6
- package/dist/components/Slider.js +90 -97
- package/dist/components/StyledText.context.d.ts +6 -0
- package/dist/components/StyledText.context.js +5 -0
- package/dist/components/StyledText.d.ts +7 -58
- package/dist/components/StyledText.js +8 -28
- package/dist/components/Switch.js +30 -26
- package/dist/components/Tabs.d.ts +23 -3
- package/dist/components/Tabs.js +39 -17
- package/dist/components/TextInput.d.ts +6 -2
- package/dist/components/TextInput.js +6 -7
- package/dist/components/Toggle.js +12 -7
- package/dist/components/ToggleGroup.js +17 -11
- package/dist/components/Tooltip.js +1 -1
- package/dist/hooks/useDimensions.js +25 -26
- package/dist/hooks/useReduceMotion.d.ts +5 -1
- package/dist/hooks/useReduceMotion.js +46 -41
- package/dist/hooks/useResources.js +6 -1
- package/dist/hooks/useScalePress.d.ts +6 -5
- package/dist/hooks/useScalePress.js +25 -21
- package/dist/hooks/useStaggeredEntrance.d.ts +9 -8
- package/dist/hooks/useStaggeredEntrance.js +48 -21
- package/dist/state/themeColorScope.js +3 -3
- package/llms-full.md +4 -5
- package/llms.txt +2 -2
- package/package.json +1 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { use, useEffect, useState } from "react";
|
|
2
|
+
import { Platform, useWindowDimensions } from "react-native";
|
|
3
3
|
import { SsrViewportContext, SSR_VIEWPORT_DEFAULT_HEIGHT, } from "../state/SsrViewportContext.js";
|
|
4
4
|
export const SCREEN_SIZES = {
|
|
5
5
|
SMALL: 768,
|
|
@@ -50,35 +50,34 @@ function writeViewportCookie(width) {
|
|
|
50
50
|
*/
|
|
51
51
|
export const useDimensions = () => {
|
|
52
52
|
const isWeb = Platform.OS === "web";
|
|
53
|
-
const ssrWidth =
|
|
53
|
+
const ssrWidth = use(SsrViewportContext);
|
|
54
|
+
// Native reads come from useWindowDimensions, which subscribes to rotation /
|
|
55
|
+
// split-screen / resize and tears the listener down for us — no manual
|
|
56
|
+
// Dimensions.addEventListener to leak. On web we ignore it and drive layout
|
|
57
|
+
// from the SSR context + the resize listener below so hydration stays exact.
|
|
58
|
+
const native = useWindowDimensions();
|
|
54
59
|
// Lazy initializer: both server and client first render compute identical
|
|
55
60
|
// flags from the context value, so hydration matches.
|
|
56
61
|
const [dimensions, setDimensions] = useState(() => calculateDimensionFlags(ssrWidth, SSR_VIEWPORT_DEFAULT_HEIGHT));
|
|
62
|
+
// Web: read the real viewport after mount and follow resize events. Keeping
|
|
63
|
+
// this in an effect (not render) preserves the SSR-matched first paint.
|
|
57
64
|
useEffect(() => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
if (!isWeb)
|
|
66
|
+
return;
|
|
67
|
+
const syncFromWindow = () => {
|
|
68
|
+
setDimensions(calculateDimensionFlags(window.innerWidth, window.innerHeight));
|
|
69
|
+
writeViewportCookie(window.innerWidth);
|
|
70
|
+
};
|
|
71
|
+
syncFromWindow();
|
|
72
|
+
window.addEventListener("resize", syncFromWindow);
|
|
73
|
+
return () => {
|
|
74
|
+
window.removeEventListener("resize", syncFromWindow);
|
|
63
75
|
};
|
|
64
|
-
updateDimensions(initialDimensions.width, initialDimensions.height);
|
|
65
|
-
if (isWeb) {
|
|
66
|
-
writeViewportCookie(initialDimensions.width);
|
|
67
|
-
const handleResize = () => {
|
|
68
|
-
updateDimensions(window.innerWidth, window.innerHeight);
|
|
69
|
-
writeViewportCookie(window.innerWidth);
|
|
70
|
-
};
|
|
71
|
-
window.addEventListener("resize", handleResize);
|
|
72
|
-
return () => {
|
|
73
|
-
window.removeEventListener("resize", handleResize);
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
const onChange = ({ window }) => {
|
|
78
|
-
updateDimensions(window.width, window.height);
|
|
79
|
-
};
|
|
80
|
-
Dimensions.addEventListener("change", onChange);
|
|
81
|
-
}
|
|
82
76
|
}, [isWeb]);
|
|
77
|
+
// Native: useWindowDimensions already reacts to changes; mirror it into our
|
|
78
|
+
// enriched flags. (On web `native` is unused — the effect above wins.)
|
|
79
|
+
if (!isWeb) {
|
|
80
|
+
return calculateDimensionFlags(native.width, native.height);
|
|
81
|
+
}
|
|
83
82
|
return dimensions;
|
|
84
83
|
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hook that returns whether the user prefers reduced motion.
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Backed by useSyncExternalStore so the value is read straight from a shared
|
|
5
|
+
* OS subscription — no mount-time setState, no polling, and a clean SSR
|
|
6
|
+
* snapshot that hydrates without a mismatch. A single singleton listener is
|
|
7
|
+
* shared across all consumers.
|
|
4
8
|
*/
|
|
5
9
|
export declare function useReducedMotion(): boolean;
|
|
@@ -1,64 +1,69 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
2
|
import { AccessibilityInfo, Platform } from "react-native";
|
|
3
3
|
let sharedValue = false;
|
|
4
|
-
|
|
4
|
+
const listeners = new Set();
|
|
5
5
|
let subscription = null;
|
|
6
|
+
function notify() {
|
|
7
|
+
for (const listener of listeners)
|
|
8
|
+
listener();
|
|
9
|
+
}
|
|
10
|
+
function setSharedValue(next) {
|
|
11
|
+
if (next === sharedValue)
|
|
12
|
+
return;
|
|
13
|
+
sharedValue = next;
|
|
14
|
+
notify();
|
|
15
|
+
}
|
|
6
16
|
function startListening() {
|
|
7
17
|
if (Platform.OS === "web") {
|
|
8
18
|
// On web, use the media query
|
|
9
19
|
if (typeof window !== "undefined" && window.matchMedia) {
|
|
10
20
|
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
11
|
-
|
|
12
|
-
const handler = (e) =>
|
|
21
|
+
setSharedValue(mq.matches);
|
|
22
|
+
const handler = (e) => setSharedValue(e.matches);
|
|
13
23
|
mq.addEventListener("change", handler);
|
|
14
24
|
subscription = { remove: () => mq.removeEventListener("change", handler) };
|
|
15
25
|
}
|
|
16
26
|
return;
|
|
17
27
|
}
|
|
18
|
-
AccessibilityInfo.isReduceMotionEnabled().then(
|
|
19
|
-
|
|
20
|
-
});
|
|
21
|
-
const sub = AccessibilityInfo.addEventListener("reduceMotionChanged", (enabled) => {
|
|
22
|
-
sharedValue = enabled;
|
|
23
|
-
});
|
|
24
|
-
subscription = sub;
|
|
28
|
+
AccessibilityInfo.isReduceMotionEnabled().then(setSharedValue);
|
|
29
|
+
subscription = AccessibilityInfo.addEventListener("reduceMotionChanged", setSharedValue);
|
|
25
30
|
}
|
|
26
31
|
function stopListening() {
|
|
27
32
|
subscription?.remove();
|
|
28
33
|
subscription = null;
|
|
29
34
|
}
|
|
35
|
+
// useSyncExternalStore contract: register the consumer, lazily starting the
|
|
36
|
+
// shared OS subscription on the first listener and tearing it down after the
|
|
37
|
+
// last unmounts.
|
|
38
|
+
function subscribe(listener) {
|
|
39
|
+
listeners.add(listener);
|
|
40
|
+
if (listeners.size === 1) {
|
|
41
|
+
startListening();
|
|
42
|
+
}
|
|
43
|
+
return () => {
|
|
44
|
+
listeners.delete(listener);
|
|
45
|
+
if (listeners.size === 0) {
|
|
46
|
+
stopListening();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function getSnapshot() {
|
|
51
|
+
return sharedValue;
|
|
52
|
+
}
|
|
53
|
+
// The server can't know the user's accessibility preference; default to
|
|
54
|
+
// "motion allowed" so SSR and the first client render agree (the real value
|
|
55
|
+
// arrives via subscribe() after hydration).
|
|
56
|
+
function getServerSnapshot() {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
30
59
|
/**
|
|
31
60
|
* Hook that returns whether the user prefers reduced motion.
|
|
32
|
-
*
|
|
61
|
+
*
|
|
62
|
+
* Backed by useSyncExternalStore so the value is read straight from a shared
|
|
63
|
+
* OS subscription — no mount-time setState, no polling, and a clean SSR
|
|
64
|
+
* snapshot that hydrates without a mismatch. A single singleton listener is
|
|
65
|
+
* shared across all consumers.
|
|
33
66
|
*/
|
|
34
67
|
export function useReducedMotion() {
|
|
35
|
-
|
|
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;
|
|
68
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
64
69
|
}
|
|
@@ -40,6 +40,7 @@ export const useResources = () => {
|
|
|
40
40
|
const [loaded, setLoaded] = useState(false);
|
|
41
41
|
const [error, setError] = useState(null);
|
|
42
42
|
useEffect(() => {
|
|
43
|
+
let timeoutId;
|
|
43
44
|
async function loadResourcesAndDataAsync() {
|
|
44
45
|
try {
|
|
45
46
|
const fontPromise = Promise.all([
|
|
@@ -47,7 +48,9 @@ export const useResources = () => {
|
|
|
47
48
|
ensureWebFontStylesheet(),
|
|
48
49
|
]);
|
|
49
50
|
// Timeout after 5 seconds — proceed with system fallback fonts
|
|
50
|
-
const timeoutPromise = new Promise((_, reject) =>
|
|
51
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
52
|
+
timeoutId = setTimeout(() => reject(new Error("Font loading timed out after 5s")), 5000);
|
|
53
|
+
});
|
|
51
54
|
await Promise.race([fontPromise, timeoutPromise]);
|
|
52
55
|
}
|
|
53
56
|
catch (e) {
|
|
@@ -56,10 +59,12 @@ export const useResources = () => {
|
|
|
56
59
|
setError(error);
|
|
57
60
|
}
|
|
58
61
|
finally {
|
|
62
|
+
clearTimeout(timeoutId);
|
|
59
63
|
setLoaded(true);
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
loadResourcesAndDataAsync();
|
|
67
|
+
return () => clearTimeout(timeoutId);
|
|
63
68
|
}, []);
|
|
64
69
|
return { loaded, error };
|
|
65
70
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Animated } from "react-native";
|
|
1
2
|
interface ScalePressOptions {
|
|
2
3
|
/**
|
|
3
4
|
* Scale value when pressed (1 = no change, 0.97 = subtle, 0.93 = more pronounced)
|
|
@@ -26,7 +27,7 @@ interface ScalePressOptions {
|
|
|
26
27
|
disabled?: boolean;
|
|
27
28
|
}
|
|
28
29
|
/**
|
|
29
|
-
* Hook for press-feedback scale animation using
|
|
30
|
+
* Hook for press-feedback scale animation using React Native Animated.
|
|
30
31
|
*
|
|
31
32
|
* Returns an animated style and onPressIn/onPressOut handlers to spread onto a Pressable.
|
|
32
33
|
* Respects reduced motion preferences.
|
|
@@ -43,15 +44,15 @@ interface ScalePressOptions {
|
|
|
43
44
|
* ```
|
|
44
45
|
*/
|
|
45
46
|
export declare function useScalePress(options?: ScalePressOptions): {
|
|
46
|
-
animatedStyle:
|
|
47
|
+
animatedStyle: {
|
|
47
48
|
transform: {
|
|
48
|
-
scale:
|
|
49
|
+
scale: Animated.Value;
|
|
49
50
|
}[];
|
|
50
|
-
}
|
|
51
|
+
};
|
|
51
52
|
pressHandlers: {
|
|
52
53
|
onPressIn: () => void;
|
|
53
54
|
onPressOut: () => void;
|
|
54
55
|
};
|
|
55
|
-
scale:
|
|
56
|
+
scale: Animated.Value;
|
|
56
57
|
};
|
|
57
58
|
export {};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
2
|
+
import { Animated } from "react-native";
|
|
3
3
|
import { hapticLight } from "../lib/haptics.js";
|
|
4
|
+
import { useReducedMotion } from "./useReduceMotion.js";
|
|
4
5
|
/**
|
|
5
|
-
* Hook for press-feedback scale animation using
|
|
6
|
+
* Hook for press-feedback scale animation using React Native Animated.
|
|
6
7
|
*
|
|
7
8
|
* Returns an animated style and onPressIn/onPressOut handlers to spread onto a Pressable.
|
|
8
9
|
* Respects reduced motion preferences.
|
|
@@ -21,32 +22,35 @@ import { hapticLight } from "../lib/haptics.js";
|
|
|
21
22
|
export function useScalePress(options = {}) {
|
|
22
23
|
const { scaleTo = 0.97, haptic = true, damping = 20, stiffness = 300, disabled = false, } = options;
|
|
23
24
|
const reduceMotion = useReducedMotion();
|
|
24
|
-
const scale =
|
|
25
|
+
const scale = useRef(new Animated.Value(1)).current;
|
|
26
|
+
const animateTo = useCallback((toValue) => {
|
|
27
|
+
scale.stopAnimation();
|
|
28
|
+
if (reduceMotion) {
|
|
29
|
+
scale.setValue(toValue);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
Animated.spring(scale, {
|
|
33
|
+
toValue,
|
|
34
|
+
damping,
|
|
35
|
+
stiffness,
|
|
36
|
+
useNativeDriver: true,
|
|
37
|
+
}).start();
|
|
38
|
+
}, [damping, reduceMotion, scale, stiffness]);
|
|
25
39
|
const onPressIn = useCallback(() => {
|
|
26
40
|
if (disabled)
|
|
27
41
|
return;
|
|
28
42
|
if (haptic)
|
|
29
43
|
hapticLight();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
scale.value = withSpring(scaleTo, { damping, stiffness });
|
|
35
|
-
}
|
|
36
|
-
}, [disabled, haptic, reduceMotion, scale, scaleTo, damping, stiffness]);
|
|
44
|
+
animateTo(scaleTo);
|
|
45
|
+
}, [animateTo, disabled, haptic, scaleTo]);
|
|
37
46
|
const onPressOut = useCallback(() => {
|
|
38
47
|
if (disabled)
|
|
39
48
|
return;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
}, [disabled, reduceMotion, scale, damping, stiffness]);
|
|
47
|
-
const animatedStyle = useAnimatedStyle(() => ({
|
|
48
|
-
transform: [{ scale: scale.value }],
|
|
49
|
-
}));
|
|
49
|
+
animateTo(1);
|
|
50
|
+
}, [animateTo, disabled]);
|
|
51
|
+
const animatedStyle = useMemo(() => ({
|
|
52
|
+
transform: [{ scale }],
|
|
53
|
+
}), [scale]);
|
|
50
54
|
return {
|
|
51
55
|
animatedStyle,
|
|
52
56
|
pressHandlers: { onPressIn, onPressOut },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Animated } from "react-native";
|
|
1
2
|
type EntranceType = "fade" | "fadeSlideUp" | "fadeSlideDown" | "scale";
|
|
2
3
|
interface StaggeredEntranceOptions {
|
|
3
4
|
/**
|
|
@@ -28,7 +29,7 @@ interface StaggeredEntranceOptions {
|
|
|
28
29
|
initialScale?: number;
|
|
29
30
|
}
|
|
30
31
|
/**
|
|
31
|
-
* Hook for entrance animations with stagger support using
|
|
32
|
+
* Hook for entrance animations with stagger support using React Native Animated.
|
|
32
33
|
*
|
|
33
34
|
* Returns an animated style to apply to an Animated.View.
|
|
34
35
|
* Respects reduced motion preferences.
|
|
@@ -46,20 +47,20 @@ interface StaggeredEntranceOptions {
|
|
|
46
47
|
* })}
|
|
47
48
|
* ```
|
|
48
49
|
*/
|
|
49
|
-
export declare function useStaggeredEntrance(options?: StaggeredEntranceOptions):
|
|
50
|
-
opacity:
|
|
50
|
+
export declare function useStaggeredEntrance(options?: StaggeredEntranceOptions): {
|
|
51
|
+
opacity: Animated.Value;
|
|
51
52
|
transform?: undefined;
|
|
52
53
|
} | {
|
|
53
|
-
opacity:
|
|
54
|
+
opacity: Animated.Value;
|
|
54
55
|
transform: {
|
|
55
|
-
translateY:
|
|
56
|
+
translateY: Animated.Value;
|
|
56
57
|
}[];
|
|
57
58
|
} | {
|
|
58
|
-
opacity:
|
|
59
|
+
opacity: Animated.Value;
|
|
59
60
|
transform: {
|
|
60
|
-
scale:
|
|
61
|
+
scale: Animated.Value;
|
|
61
62
|
}[];
|
|
62
|
-
}
|
|
63
|
+
};
|
|
63
64
|
/**
|
|
64
65
|
* Convenience constant: default stagger delay between items (ms)
|
|
65
66
|
*/
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { Animated, Easing } from "react-native";
|
|
3
|
+
import { useReducedMotion } from "./useReduceMotion.js";
|
|
3
4
|
/**
|
|
4
|
-
* Hook for entrance animations with stagger support using
|
|
5
|
+
* Hook for entrance animations with stagger support using React Native Animated.
|
|
5
6
|
*
|
|
6
7
|
* Returns an animated style to apply to an Animated.View.
|
|
7
8
|
* Respects reduced motion preferences.
|
|
@@ -22,50 +23,76 @@ import { useSharedValue, useAnimatedStyle, withTiming, withSpring, withDelay, us
|
|
|
22
23
|
export function useStaggeredEntrance(options = {}) {
|
|
23
24
|
const { type = "fadeSlideUp", delay = 0, duration = 200, slideDistance = 8, initialScale = 0.95, } = options;
|
|
24
25
|
const reduceMotion = useReducedMotion();
|
|
25
|
-
const opacity =
|
|
26
|
-
const translateY =
|
|
26
|
+
const opacity = useRef(new Animated.Value(reduceMotion ? 1 : 0)).current;
|
|
27
|
+
const translateY = useRef(new Animated.Value(reduceMotion
|
|
27
28
|
? 0
|
|
28
29
|
: type === "fadeSlideUp"
|
|
29
30
|
? slideDistance
|
|
30
31
|
: type === "fadeSlideDown"
|
|
31
32
|
? -slideDistance
|
|
32
|
-
: 0);
|
|
33
|
-
const scale =
|
|
33
|
+
: 0)).current;
|
|
34
|
+
const scale = useRef(new Animated.Value(reduceMotion ? 1 : type === "scale" ? initialScale : 1)).current;
|
|
34
35
|
useEffect(() => {
|
|
35
36
|
if (reduceMotion) {
|
|
36
|
-
opacity.
|
|
37
|
-
translateY.
|
|
38
|
-
scale.
|
|
37
|
+
opacity.setValue(1);
|
|
38
|
+
translateY.setValue(0);
|
|
39
|
+
scale.setValue(1);
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
42
|
+
opacity.setValue(0);
|
|
43
|
+
translateY.setValue(type === "fadeSlideUp"
|
|
44
|
+
? slideDistance
|
|
45
|
+
: type === "fadeSlideDown"
|
|
46
|
+
? -slideDistance
|
|
47
|
+
: 0);
|
|
48
|
+
scale.setValue(type === "scale" ? initialScale : 1);
|
|
41
49
|
const timingConfig = {
|
|
42
50
|
duration,
|
|
43
51
|
easing: Easing.out(Easing.cubic),
|
|
52
|
+
useNativeDriver: true,
|
|
44
53
|
};
|
|
45
|
-
|
|
54
|
+
const animations = [
|
|
55
|
+
Animated.timing(opacity, {
|
|
56
|
+
toValue: 1,
|
|
57
|
+
...timingConfig,
|
|
58
|
+
}),
|
|
59
|
+
];
|
|
46
60
|
if (type === "fadeSlideUp" || type === "fadeSlideDown") {
|
|
47
|
-
|
|
61
|
+
animations.push(Animated.timing(translateY, {
|
|
62
|
+
toValue: 0,
|
|
63
|
+
...timingConfig,
|
|
64
|
+
}));
|
|
48
65
|
}
|
|
49
66
|
if (type === "scale") {
|
|
50
|
-
|
|
67
|
+
animations.push(Animated.spring(scale, {
|
|
68
|
+
toValue: 1,
|
|
69
|
+
damping: 14,
|
|
70
|
+
stiffness: 250,
|
|
71
|
+
useNativeDriver: true,
|
|
72
|
+
}));
|
|
51
73
|
}
|
|
52
|
-
|
|
53
|
-
|
|
74
|
+
const animation = delay > 0
|
|
75
|
+
? Animated.sequence([Animated.delay(delay), Animated.parallel(animations)])
|
|
76
|
+
: Animated.parallel(animations);
|
|
77
|
+
animation.start();
|
|
78
|
+
return () => animation.stop();
|
|
79
|
+
}, [delay, duration, initialScale, opacity, reduceMotion, scale, slideDistance, translateY, type]);
|
|
80
|
+
const animatedStyle = useMemo(() => {
|
|
54
81
|
if (type === "fade") {
|
|
55
|
-
return { opacity
|
|
82
|
+
return { opacity };
|
|
56
83
|
}
|
|
57
84
|
if (type === "fadeSlideUp" || type === "fadeSlideDown") {
|
|
58
85
|
return {
|
|
59
|
-
opacity
|
|
60
|
-
transform: [{ translateY
|
|
86
|
+
opacity,
|
|
87
|
+
transform: [{ translateY }],
|
|
61
88
|
};
|
|
62
89
|
}
|
|
63
90
|
// scale
|
|
64
91
|
return {
|
|
65
|
-
opacity
|
|
66
|
-
transform: [{ scale
|
|
92
|
+
opacity,
|
|
93
|
+
transform: [{ scale }],
|
|
67
94
|
};
|
|
68
|
-
});
|
|
95
|
+
}, [opacity, scale, translateY, type]);
|
|
69
96
|
return animatedStyle;
|
|
70
97
|
}
|
|
71
98
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext,
|
|
2
|
+
import { createContext, use, useMemo } from "react";
|
|
3
3
|
/**
|
|
4
4
|
* Per-subtree color overrides, layered on top of the global theme by `useTheme`.
|
|
5
5
|
*
|
|
@@ -13,7 +13,7 @@ import { createContext, useContext, useMemo } from "react";
|
|
|
13
13
|
const ThemeColorScopeContext = createContext(null);
|
|
14
14
|
/** Read the active scoped override (null when not inside a scope). */
|
|
15
15
|
export function useThemeColorScope() {
|
|
16
|
-
return
|
|
16
|
+
return use(ThemeColorScopeContext);
|
|
17
17
|
}
|
|
18
18
|
// Nested scopes layer: the inner scope's keys win, the outer scope's fill in.
|
|
19
19
|
function mergeScopes(parent, next) {
|
|
@@ -25,7 +25,7 @@ function mergeScopes(parent, next) {
|
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
export function ThemeColorScope({ colors, children, }) {
|
|
28
|
-
const parent =
|
|
28
|
+
const parent = use(ThemeColorScopeContext);
|
|
29
29
|
const value = useMemo(() => mergeScopes(parent, colors), [parent, colors]);
|
|
30
30
|
return (_jsx(ThemeColorScopeContext.Provider, { value: value, children: children }));
|
|
31
31
|
}
|
package/llms-full.md
CHANGED
|
@@ -36,10 +36,9 @@ the root when the app uses package feedback or overlay components.
|
|
|
36
36
|
`BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`, `SelectContent`,
|
|
37
37
|
`Tooltip`, or `globalUIStore` notifications.
|
|
38
38
|
|
|
39
|
-
On native, `BottomSheet.Content` composes its sheet transform with
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
text inputs, or pass `avoidKeyboard={false}` for sheets that should not move.
|
|
39
|
+
On native, `BottomSheet.Content` composes its sheet transform with React Native
|
|
40
|
+
keyboard event values. Pass `avoidKeyboard={false}` for sheets that should not
|
|
41
|
+
move.
|
|
43
42
|
|
|
44
43
|
i18n is optional. Plain children and `text` props work without `i18next` or
|
|
45
44
|
`react-i18next`. Use `configureExpoUiI18n()` only when a consuming app wants
|
|
@@ -87,7 +86,7 @@ Use this catalog before creating a new app-local primitive.
|
|
|
87
86
|
| `Alert` | `@mrmeg/expo-ui/components` | Cross-platform imperative alerts | Avoid direct `window.alert` and duplicated native/web branching. |
|
|
88
87
|
| `AnimatedView` | `@mrmeg/expo-ui/components` | Entrance and visibility animation | Keep simple reveal effects in the package wrapper. |
|
|
89
88
|
| `Badge` | `@mrmeg/expo-ui/components` | Short status labels | Prefer over custom pill views. |
|
|
90
|
-
| `BottomSheet` | `@mrmeg/expo-ui/components` | Mobile-first modal sheets | Requires root `UIProvider`;
|
|
89
|
+
| `BottomSheet` | `@mrmeg/expo-ui/components` | Mobile-first modal sheets | Requires root `UIProvider`; text-input sheets can disable `avoidKeyboard`. |
|
|
91
90
|
| `Button` | `@mrmeg/expo-ui/components` | Commands and CTAs | Use `preset`, not `variant`; visible heights are compact. |
|
|
92
91
|
| `Card` | `@mrmeg/expo-ui/components` | Individual framed content groups | Do not wrap whole page sections in cards. |
|
|
93
92
|
| `Checkbox` | `@mrmeg/expo-ui/components` | Boolean selection in forms or lists | Prefer over custom checkmark controls. |
|
package/llms.txt
CHANGED
|
@@ -25,8 +25,8 @@ Call useResources() once near the Expo app root. Mount UIProvider once near the
|
|
|
25
25
|
root before using package notifications or overlay primitives: Dialog,
|
|
26
26
|
AlertDialog, BottomSheet, Drawer, DropdownMenu, Popover, SelectContent, or
|
|
27
27
|
Tooltip.
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
BottomSheet.Content uses React Native keyboard events for text inputs;
|
|
29
|
+
avoidKeyboard defaults to true.
|
|
30
30
|
|
|
31
31
|
Use useTheme(), useStyles(), semantic tokens, StyledText, and package controls.
|
|
32
32
|
Do not add app-local Appearance or matchMedia listeners for package theme sync.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrmeg/expo-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Reusable Expo and React Native UI primitives for MrMeg projects.",
|
|
6
6
|
"keywords": [
|
|
@@ -109,12 +109,9 @@
|
|
|
109
109
|
"react": ">=19.2.0 <20.0.0",
|
|
110
110
|
"react-native": ">=0.83.0 <0.86.0",
|
|
111
111
|
"react-native-gesture-handler": ">=2.30.0 <2.32.0",
|
|
112
|
-
"react-native-keyboard-controller": ">=1.20.0 <2.0.0",
|
|
113
|
-
"react-native-reanimated": ">=4.2.0 <5.0.0",
|
|
114
112
|
"react-native-safe-area-context": ">=5.6.0 <6.0.0",
|
|
115
113
|
"react-native-screens": ">=4.23.0 <5.0.0",
|
|
116
114
|
"react-native-web": ">=0.21.0 <0.22.0",
|
|
117
|
-
"react-native-worklets": ">=0.7.0 <0.9.0",
|
|
118
115
|
"zustand": ">=5.0.0 <6.0.0"
|
|
119
116
|
},
|
|
120
117
|
"devDependencies": {
|