expotesting2 4.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 +289 -0
- package/apps/expo-app/app.json +60 -0
- package/apps/expo-app/babel.config.js +7 -0
- package/apps/expo-app/index.js +6 -0
- package/apps/expo-app/package.json +46 -0
- package/apps/expo-app/src/App.jsx +37 -0
- package/apps/expo-app/src/navigation/RootNavigator.jsx +82 -0
- package/apps/expo-app/src/navigation/types.js +5 -0
- package/apps/expo-app/src/screens/HomeScreen.jsx +178 -0
- package/package.json +24 -0
- package/packages/animations/package.json +20 -0
- package/packages/animations/src/components/FadeView.jsx +42 -0
- package/packages/animations/src/components/ScaleView.jsx +28 -0
- package/packages/animations/src/components/SlideView.jsx +32 -0
- package/packages/animations/src/hooks/useFade.js +50 -0
- package/packages/animations/src/hooks/useScale.js +59 -0
- package/packages/animations/src/hooks/useSlide.js +53 -0
- package/packages/animations/src/index.js +21 -0
- package/packages/animations/src/reanimated.js +83 -0
- package/packages/core/package.json +22 -0
- package/packages/core/src/components/Button.jsx +92 -0
- package/packages/core/src/components/Card.jsx +47 -0
- package/packages/core/src/components/Container.jsx +61 -0
- package/packages/core/src/components/Input.jsx +83 -0
- package/packages/core/src/components/List.jsx +80 -0
- package/packages/core/src/components/index.js +9 -0
- package/packages/core/src/hooks/index.js +5 -0
- package/packages/core/src/hooks/useAsync.js +60 -0
- package/packages/core/src/hooks/useCounter.js +36 -0
- package/packages/core/src/hooks/useToggle.js +18 -0
- package/packages/core/src/index.js +5 -0
- package/packages/core/src/theme/index.js +67 -0
- package/packages/core/src/utils/helpers.js +93 -0
- package/packages/core/src/utils/index.js +10 -0
- package/packages/device/package.json +24 -0
- package/packages/device/src/hooks/useCamera.js +45 -0
- package/packages/device/src/hooks/useGallery.js +70 -0
- package/packages/device/src/hooks/useLocation.js +99 -0
- package/packages/device/src/index.js +5 -0
- package/packages/examples/package.json +36 -0
- package/packages/examples/src/experiments/animations-device/AnimationsDeviceScreen.jsx +291 -0
- package/packages/examples/src/experiments/basic-app/BasicAppScreen.jsx +162 -0
- package/packages/examples/src/experiments/components-props-state/ComponentsStateScreen.jsx +280 -0
- package/packages/examples/src/experiments/navigation/NavigationScreen.jsx +202 -0
- package/packages/examples/src/experiments/network-storage/NetworkStorageScreen.jsx +367 -0
- package/packages/examples/src/experiments/state-management/StateManagementScreen.jsx +255 -0
- package/packages/examples/src/index.js +76 -0
- package/packages/navigation/package.json +20 -0
- package/packages/navigation/src/DrawerNavigator.jsx +35 -0
- package/packages/navigation/src/StackNavigator.jsx +51 -0
- package/packages/navigation/src/TabNavigator.jsx +44 -0
- package/packages/navigation/src/createAppNavigator.jsx +48 -0
- package/packages/navigation/src/index.js +8 -0
- package/packages/navigation/src/types.js +18 -0
- package/packages/network/package.json +19 -0
- package/packages/network/src/apiClient.js +90 -0
- package/packages/network/src/fetchHelpers.js +97 -0
- package/packages/network/src/hooks/useFetch.js +56 -0
- package/packages/network/src/index.js +3 -0
- package/packages/network/src/types.js +4 -0
- package/packages/state/package.json +22 -0
- package/packages/state/src/context/AuthContext.jsx +94 -0
- package/packages/state/src/context/ThemeContext.jsx +79 -0
- package/packages/state/src/context/index.js +3 -0
- package/packages/state/src/index.js +5 -0
- package/packages/state/src/redux/hooks.js +12 -0
- package/packages/state/src/redux/index.js +7 -0
- package/packages/state/src/redux/slices/counterSlice.js +39 -0
- package/packages/state/src/redux/slices/postsSlice.js +92 -0
- package/packages/state/src/redux/store.js +32 -0
- package/packages/storage/package.json +24 -0
- package/packages/storage/src/asyncStorage.js +82 -0
- package/packages/storage/src/index.js +2 -0
- package/packages/storage/src/sqlite/database.js +65 -0
- package/packages/storage/src/sqlite/index.js +3 -0
- package/packages/storage/src/sqlite/operations.js +112 -0
- package/packages/storage/src/sqlite/useSQLite.js +45 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HomeScreen — experiment catalogue.
|
|
3
|
+
*
|
|
4
|
+
* Lists all 6 experiments grouped by module.
|
|
5
|
+
* Tapping an experiment navigates to its dedicated screen.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import {
|
|
10
|
+
Pressable,
|
|
11
|
+
SectionList,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
Text,
|
|
14
|
+
View } from 'react-native';
|
|
15
|
+
import { EXPERIMENT_REGISTRY } from '@expotesting/examples';
|
|
16
|
+
|
|
17
|
+
// Group experiments by module number
|
|
18
|
+
function buildSections(){
|
|
19
|
+
const map = new Map();
|
|
20
|
+
for (const exp of EXPERIMENT_REGISTRY) {
|
|
21
|
+
const existing = map.get(exp.module) ?? [];
|
|
22
|
+
existing.push(exp);
|
|
23
|
+
map.set(exp.module, existing);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MODULE_LABELS= {
|
|
27
|
+
1: 'Module 1 — React Basics',
|
|
28
|
+
2: 'Module 2 — State, Networking & Data',
|
|
29
|
+
3: 'Module 3 — Advanced' };
|
|
30
|
+
|
|
31
|
+
return Array.from(map.entries()).map(([mod, data]) => ({
|
|
32
|
+
title: MODULE_LABELS[mod] ?? `Module ${mod}`,
|
|
33
|
+
data }));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SECTIONS = buildSections();
|
|
37
|
+
|
|
38
|
+
export function HomeScreen({ navigation }){
|
|
39
|
+
function navigateTo(route){
|
|
40
|
+
switch (route) {
|
|
41
|
+
case 'Exp01':
|
|
42
|
+
navigation.navigate('Exp01');
|
|
43
|
+
break;
|
|
44
|
+
case 'Exp02':
|
|
45
|
+
navigation.navigate('Exp02');
|
|
46
|
+
break;
|
|
47
|
+
case 'Exp03':
|
|
48
|
+
navigation.navigate('Exp03Home');
|
|
49
|
+
break;
|
|
50
|
+
case 'Exp04':
|
|
51
|
+
navigation.navigate('Exp04');
|
|
52
|
+
break;
|
|
53
|
+
case 'Exp05':
|
|
54
|
+
navigation.navigate('Exp05');
|
|
55
|
+
break;
|
|
56
|
+
case 'Exp06':
|
|
57
|
+
navigation.navigate('Exp06');
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<SectionList
|
|
66
|
+
style={styles.container}
|
|
67
|
+
contentContainerStyle={styles.content}
|
|
68
|
+
sections={SECTIONS}
|
|
69
|
+
keyExtractor={(item) => item.id}
|
|
70
|
+
stickySectionHeadersEnabled={false}
|
|
71
|
+
ListHeaderComponent={
|
|
72
|
+
<View style={styles.header}>
|
|
73
|
+
<Text style={styles.logo}>expotesting</Text>
|
|
74
|
+
<Text style={styles.tagline}>React Native (Expo) Curriculum Platform</Text>
|
|
75
|
+
<Text style={styles.badge}>
|
|
76
|
+
{EXPERIMENT_REGISTRY.length} experiments · 3 modules
|
|
77
|
+
</Text>
|
|
78
|
+
</View>
|
|
79
|
+
}
|
|
80
|
+
renderSectionHeader={({ section }) => (
|
|
81
|
+
<View style={styles.sectionHeader}>
|
|
82
|
+
<Text style={styles.sectionHeaderText}>{section.title}</Text>
|
|
83
|
+
</View>
|
|
84
|
+
)}
|
|
85
|
+
renderItem={({ item, index }) => (
|
|
86
|
+
<Pressable
|
|
87
|
+
style={({ pressed }) => [
|
|
88
|
+
styles.experimentCard,
|
|
89
|
+
{ borderLeftColor: item.color },
|
|
90
|
+
pressed && styles.cardPressed,
|
|
91
|
+
]}
|
|
92
|
+
onPress={() => navigateTo(item.route)}
|
|
93
|
+
accessibilityRole="button"
|
|
94
|
+
accessibilityLabel={`Open ${item.title} experiment`}
|
|
95
|
+
>
|
|
96
|
+
<View style={[styles.expBadge, { backgroundColor: item.color }]}>
|
|
97
|
+
<Text style={styles.expBadgeText}>
|
|
98
|
+
{String(index + 1).padStart(2, '0')}
|
|
99
|
+
</Text>
|
|
100
|
+
</View>
|
|
101
|
+
<View style={styles.expInfo}>
|
|
102
|
+
<Text style={styles.expTitle}>{item.title}</Text>
|
|
103
|
+
<Text style={styles.expSubtitle}>{item.subtitle}</Text>
|
|
104
|
+
</View>
|
|
105
|
+
<Text style={styles.chevron}>›</Text>
|
|
106
|
+
</Pressable>
|
|
107
|
+
)}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const styles = StyleSheet.create({
|
|
113
|
+
container: { flex: 1, backgroundColor: '#fafafa' },
|
|
114
|
+
content: { paddingBottom: 48 },
|
|
115
|
+
header: {
|
|
116
|
+
backgroundColor: '#6200EE',
|
|
117
|
+
paddingTop: 48,
|
|
118
|
+
paddingBottom: 28,
|
|
119
|
+
paddingHorizontal: 20 },
|
|
120
|
+
logo: {
|
|
121
|
+
fontSize: 28,
|
|
122
|
+
fontWeight: '800',
|
|
123
|
+
color: '#fff',
|
|
124
|
+
letterSpacing: -0.5 },
|
|
125
|
+
tagline: {
|
|
126
|
+
fontSize: 14,
|
|
127
|
+
color: 'rgba(255,255,255,0.75)',
|
|
128
|
+
marginTop: 4 },
|
|
129
|
+
badge: {
|
|
130
|
+
marginTop: 12,
|
|
131
|
+
fontSize: 12,
|
|
132
|
+
color: '#fff',
|
|
133
|
+
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
134
|
+
alignSelf: 'flex-start',
|
|
135
|
+
paddingVertical: 4,
|
|
136
|
+
paddingHorizontal: 10,
|
|
137
|
+
borderRadius: 20,
|
|
138
|
+
overflow: 'hidden' },
|
|
139
|
+
sectionHeader: {
|
|
140
|
+
paddingHorizontal: 16,
|
|
141
|
+
paddingTop: 20,
|
|
142
|
+
paddingBottom: 8 },
|
|
143
|
+
sectionHeaderText: {
|
|
144
|
+
fontSize: 13,
|
|
145
|
+
fontWeight: '700',
|
|
146
|
+
color: '#888',
|
|
147
|
+
textTransform: 'uppercase',
|
|
148
|
+
letterSpacing: 0.8 },
|
|
149
|
+
experimentCard: {
|
|
150
|
+
flexDirection: 'row',
|
|
151
|
+
alignItems: 'center',
|
|
152
|
+
backgroundColor: '#fff',
|
|
153
|
+
marginHorizontal: 16,
|
|
154
|
+
marginBottom: 8,
|
|
155
|
+
borderRadius: 12,
|
|
156
|
+
borderLeftWidth: 4,
|
|
157
|
+
padding: 14,
|
|
158
|
+
shadowColor: '#000',
|
|
159
|
+
shadowOffset: { width: 0, height: 1 },
|
|
160
|
+
shadowOpacity: 0.06,
|
|
161
|
+
shadowRadius: 3,
|
|
162
|
+
elevation: 2 },
|
|
163
|
+
cardPressed: { opacity: 0.8 },
|
|
164
|
+
expBadge: {
|
|
165
|
+
width: 36,
|
|
166
|
+
height: 36,
|
|
167
|
+
borderRadius: 8,
|
|
168
|
+
alignItems: 'center',
|
|
169
|
+
justifyContent: 'center',
|
|
170
|
+
marginRight: 12 },
|
|
171
|
+
expBadgeText: {
|
|
172
|
+
fontSize: 14,
|
|
173
|
+
fontWeight: '800',
|
|
174
|
+
color: '#fff' },
|
|
175
|
+
expInfo: { flex: 1 },
|
|
176
|
+
expTitle: { fontSize: 15, fontWeight: '700', color: '#212121' },
|
|
177
|
+
expSubtitle: { fontSize: 12, color: '#888', marginTop: 2 },
|
|
178
|
+
chevron: { fontSize: 22, color: '#ccc', fontWeight: '300' } });
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "expotesting2",
|
|
3
|
+
"version": "4.1.0",
|
|
4
|
+
"description": "React Native (Expo) learning library",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"packages/",
|
|
8
|
+
"apps/",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"expo",
|
|
13
|
+
"react-native",
|
|
14
|
+
"react",
|
|
15
|
+
"learning",
|
|
16
|
+
"syllabus",
|
|
17
|
+
"experiments",
|
|
18
|
+
"monorepo"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/omlanke/newexpo"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@expotesting/animations",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Animation hooks and components (Animated API + Reanimated) for expotesting",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"react": ">=18",
|
|
13
|
+
"react-native": ">=0.73",
|
|
14
|
+
"react-native-reanimated": ">=3"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"react-native-reanimated": "*"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT"
|
|
20
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FadeView — an Animated.View that fades in on mount.
|
|
3
|
+
*
|
|
4
|
+
* A convenience wrapper around useFade for the most common case.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* <FadeView duration={500}>
|
|
8
|
+
* <Text>I fade in automatically</Text>
|
|
9
|
+
* </FadeView>
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React, { useEffect } from 'react';
|
|
13
|
+
import { Animated } from 'react-native';
|
|
14
|
+
import { useFade } from '../hooks/useFade';
|
|
15
|
+
|
|
16
|
+
export function FadeView({
|
|
17
|
+
children,
|
|
18
|
+
duration = 400,
|
|
19
|
+
delay = 0,
|
|
20
|
+
style,
|
|
21
|
+
fadeOut: startFadeOut = false }){
|
|
22
|
+
const { fadeAnim, fadeIn, fadeOut } = useFade({
|
|
23
|
+
initialValue: startFadeOut ? 1 : 0,
|
|
24
|
+
duration });
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
if (startFadeOut) {
|
|
29
|
+
fadeOut();
|
|
30
|
+
} else {
|
|
31
|
+
fadeIn();
|
|
32
|
+
}
|
|
33
|
+
}, delay);
|
|
34
|
+
return () => clearTimeout(timer);
|
|
35
|
+
}, [delay, fadeIn, fadeOut, startFadeOut]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Animated.View style={[style, { opacity: fadeAnim }]}>
|
|
39
|
+
{children}
|
|
40
|
+
</Animated.View>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScaleView — a pressable view that scales down on press using Animated API.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* <ScaleView onPress={() => console.log('pressed')} style={styles.card}>
|
|
6
|
+
* <Text>Press me</Text>
|
|
7
|
+
* </ScaleView>
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { Animated, Pressable } from 'react-native';
|
|
12
|
+
import { useScale } from '../hooks/useScale';
|
|
13
|
+
|
|
14
|
+
export function ScaleView({
|
|
15
|
+
children,
|
|
16
|
+
onPress,
|
|
17
|
+
pressedScale = 0.95,
|
|
18
|
+
style }){
|
|
19
|
+
const { style: scaleStyle, pressIn, pressOut } = useScale({ to: pressedScale });
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Pressable onPress={onPress} onPressIn={pressIn} onPressOut={pressOut}>
|
|
23
|
+
<Animated.View style={[style, scaleStyle]}>
|
|
24
|
+
{children}
|
|
25
|
+
</Animated.View>
|
|
26
|
+
</Pressable>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SlideView — slides its children in from offscreen on mount.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* <SlideView axis="x" from={-300}>
|
|
6
|
+
* <SidebarContent />
|
|
7
|
+
* </SlideView>
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useEffect } from 'react';
|
|
11
|
+
import { Animated } from 'react-native';
|
|
12
|
+
import { useSlide } from '../hooks/useSlide';
|
|
13
|
+
|
|
14
|
+
export function SlideView({
|
|
15
|
+
children,
|
|
16
|
+
axis = 'y',
|
|
17
|
+
from = 100,
|
|
18
|
+
delay = 0,
|
|
19
|
+
style }){
|
|
20
|
+
const { slideAnim, slideIn, style: slideStyle } = useSlide({ axis, from });
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const timer = setTimeout(slideIn, delay);
|
|
24
|
+
return () => clearTimeout(timer);
|
|
25
|
+
}, [slideIn, delay]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Animated.View style={[style, slideStyle]}>
|
|
29
|
+
{children}
|
|
30
|
+
</Animated.View>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFade — controls opacity with smooth enter/exit transitions.
|
|
3
|
+
* Uses React Native's built-in Animated API (no extra dependencies).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* const { fadeAnim, fadeIn, fadeOut, style } = useFade({ duration: 400 });
|
|
7
|
+
*
|
|
8
|
+
* <Animated.View style={style}>
|
|
9
|
+
* <Text>Fading content</Text>
|
|
10
|
+
* </Animated.View>
|
|
11
|
+
*
|
|
12
|
+
* <Button onPress={fadeIn}>Show</Button>
|
|
13
|
+
* <Button onPress={fadeOut}>Hide</Button>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useCallback, useRef } from 'react';
|
|
17
|
+
import { Animated } from 'react-native';
|
|
18
|
+
|
|
19
|
+
export function useFade(options = {}){
|
|
20
|
+
const { initialValue = 0, duration = 300, useNativeDriver = true } = options;
|
|
21
|
+
|
|
22
|
+
const fadeAnim = useRef(new Animated.Value(initialValue)).current;
|
|
23
|
+
|
|
24
|
+
const fadeTo = useCallback(
|
|
25
|
+
(toValue, callback) => {
|
|
26
|
+
Animated.timing(fadeAnim, {
|
|
27
|
+
toValue,
|
|
28
|
+
duration,
|
|
29
|
+
useNativeDriver }).start(callback);
|
|
30
|
+
},
|
|
31
|
+
[fadeAnim, duration, useNativeDriver],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const fadeIn = useCallback(
|
|
35
|
+
(callback) => fadeTo(1, callback),
|
|
36
|
+
[fadeTo],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const fadeOut = useCallback(
|
|
40
|
+
(callback) => fadeTo(0, callback),
|
|
41
|
+
[fadeTo],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
fadeAnim,
|
|
46
|
+
fadeIn,
|
|
47
|
+
fadeOut,
|
|
48
|
+
fadeTo,
|
|
49
|
+
style: { opacity: fadeAnim } };
|
|
50
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useScale — press-to-scale interaction via React Native Animated API.
|
|
3
|
+
* Great for card tap effects, button feedback, etc.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* const { scaleAnim, pressIn, pressOut, style } = useScale({ to: 0.95 });
|
|
7
|
+
*
|
|
8
|
+
* <Animated.View style={[cardStyle, style]}>
|
|
9
|
+
* <Pressable onPressIn={pressIn} onPressOut={pressOut}>
|
|
10
|
+
* <Text>Tap me</Text>
|
|
11
|
+
* </Pressable>
|
|
12
|
+
* </Animated.View>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useCallback, useRef } from 'react';
|
|
16
|
+
import { Animated } from 'react-native';
|
|
17
|
+
|
|
18
|
+
export function useScale(options = {}){
|
|
19
|
+
const {
|
|
20
|
+
from = 1,
|
|
21
|
+
to = 0.95,
|
|
22
|
+
duration = 100,
|
|
23
|
+
useNativeDriver = true } = options;
|
|
24
|
+
|
|
25
|
+
const scaleAnim = useRef(new Animated.Value(from)).current;
|
|
26
|
+
|
|
27
|
+
const scaleTo = useCallback(
|
|
28
|
+
(value) => {
|
|
29
|
+
Animated.spring(scaleAnim, {
|
|
30
|
+
toValue: value,
|
|
31
|
+
useNativeDriver,
|
|
32
|
+
speed: 50,
|
|
33
|
+
bounciness: 0 }).start();
|
|
34
|
+
},
|
|
35
|
+
[scaleAnim, useNativeDriver],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const pressIn = useCallback(() => {
|
|
39
|
+
Animated.timing(scaleAnim, {
|
|
40
|
+
toValue: to,
|
|
41
|
+
duration,
|
|
42
|
+
useNativeDriver }).start();
|
|
43
|
+
}, [scaleAnim, to, duration, useNativeDriver]);
|
|
44
|
+
|
|
45
|
+
const pressOut = useCallback(() => {
|
|
46
|
+
Animated.spring(scaleAnim, {
|
|
47
|
+
toValue: from,
|
|
48
|
+
useNativeDriver,
|
|
49
|
+
speed: 50,
|
|
50
|
+
bounciness: 4 }).start();
|
|
51
|
+
}, [scaleAnim, from, useNativeDriver]);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
scaleAnim,
|
|
55
|
+
pressIn,
|
|
56
|
+
pressOut,
|
|
57
|
+
scaleTo,
|
|
58
|
+
style: { transform: [{ scale: scaleAnim }] } };
|
|
59
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSlide — translates a component in/out along X or Y axis.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* const { slideAnim, slideIn, slideOut, style } = useSlide({ axis: 'x', from: -300 });
|
|
6
|
+
*
|
|
7
|
+
* // Slide a sidebar in from the left:
|
|
8
|
+
* <Animated.View style={[drawerStyle, style]}>
|
|
9
|
+
* <DrawerContent />
|
|
10
|
+
* </Animated.View>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback, useRef } from 'react';
|
|
14
|
+
import { Animated } from 'react-native';
|
|
15
|
+
|
|
16
|
+
export function useSlide(options = {}){
|
|
17
|
+
const {
|
|
18
|
+
axis = 'y',
|
|
19
|
+
from = 300,
|
|
20
|
+
to = 0,
|
|
21
|
+
duration = 350,
|
|
22
|
+
useNativeDriver = true } = options;
|
|
23
|
+
|
|
24
|
+
const slideAnim = useRef(new Animated.Value(from)).current;
|
|
25
|
+
|
|
26
|
+
const slideIn = useCallback(
|
|
27
|
+
(callback) => {
|
|
28
|
+
Animated.spring(slideAnim, {
|
|
29
|
+
toValue: to,
|
|
30
|
+
useNativeDriver,
|
|
31
|
+
damping: 20,
|
|
32
|
+
stiffness: 120 }).start(callback);
|
|
33
|
+
},
|
|
34
|
+
[slideAnim, to, useNativeDriver],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const slideOut = useCallback(
|
|
38
|
+
(callback) => {
|
|
39
|
+
Animated.timing(slideAnim, {
|
|
40
|
+
toValue: from,
|
|
41
|
+
duration,
|
|
42
|
+
useNativeDriver }).start(callback);
|
|
43
|
+
},
|
|
44
|
+
[slideAnim, from, duration, useNativeDriver],
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const transform =
|
|
48
|
+
axis === 'x'
|
|
49
|
+
? [{ translateX: slideAnim }]
|
|
50
|
+
: [{ translateY: slideAnim }];
|
|
51
|
+
|
|
52
|
+
return { slideAnim, slideIn, slideOut, style: { transform } };
|
|
53
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Animated API hooks
|
|
2
|
+
export { useFade } from './hooks/useFade';
|
|
3
|
+
|
|
4
|
+
export { useScale } from './hooks/useScale';
|
|
5
|
+
|
|
6
|
+
export { useSlide } from './hooks/useSlide';
|
|
7
|
+
|
|
8
|
+
// Animated API components
|
|
9
|
+
export { FadeView } from './components/FadeView';
|
|
10
|
+
|
|
11
|
+
export { ScaleView } from './components/ScaleView';
|
|
12
|
+
|
|
13
|
+
export { SlideView } from './components/SlideView';
|
|
14
|
+
|
|
15
|
+
// Reanimated utilities
|
|
16
|
+
export {
|
|
17
|
+
reanimatedFadeIn,
|
|
18
|
+
reanimatedFadeOut,
|
|
19
|
+
reanimatedSpringScale,
|
|
20
|
+
reanimatedBounce,
|
|
21
|
+
reanimatedShake } from './reanimated';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reanimated utilities — worklet-based animations using react-native-reanimated v3.
|
|
3
|
+
*
|
|
4
|
+
* These offer superior performance by running entirely on the UI thread.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
|
8
|
+
* import { reanimatedFadeIn, reanimatedBounce } from '@expotesting/animations';
|
|
9
|
+
*
|
|
10
|
+
* const opacity = useSharedValue(0);
|
|
11
|
+
* const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value }));
|
|
12
|
+
* reanimatedFadeIn(opacity); // triggers on UI thread
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
withTiming,
|
|
17
|
+
withSpring,
|
|
18
|
+
withSequence,
|
|
19
|
+
withDelay,
|
|
20
|
+
Easing } from 'react-native-reanimated';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fades an opacity shared value to 1.
|
|
24
|
+
* Must be called from a worklet or useEffect (not from a button press directly).
|
|
25
|
+
*/
|
|
26
|
+
export function reanimatedFadeIn(
|
|
27
|
+
opacity,
|
|
28
|
+
config = {},
|
|
29
|
+
){
|
|
30
|
+
const { duration = 300, delay = 0 } = config;
|
|
31
|
+
if (delay > 0) {
|
|
32
|
+
opacity.value = withDelay(delay, withTiming(1, { duration }));
|
|
33
|
+
} else {
|
|
34
|
+
opacity.value = withTiming(1, { duration });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fades an opacity shared value to 0.
|
|
40
|
+
*/
|
|
41
|
+
export function reanimatedFadeOut(
|
|
42
|
+
opacity,
|
|
43
|
+
config = {},
|
|
44
|
+
){
|
|
45
|
+
const { duration = 300 } = config;
|
|
46
|
+
opacity.value = withTiming(0, { duration });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Spring-scales a shared value to a target.
|
|
51
|
+
*/
|
|
52
|
+
export function reanimatedSpringScale(
|
|
53
|
+
scale,
|
|
54
|
+
to,
|
|
55
|
+
){
|
|
56
|
+
scale.value = withSpring(to, {
|
|
57
|
+
damping: 15,
|
|
58
|
+
stiffness: 150 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Bounce effect — scales up then back to normal.
|
|
63
|
+
* Great for "success" confirmations.
|
|
64
|
+
*/
|
|
65
|
+
export function reanimatedBounce(scale){
|
|
66
|
+
scale.value = withSequence(
|
|
67
|
+
withSpring(1.2, { damping: 8 }),
|
|
68
|
+
withSpring(1, { damping: 10 }),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Shake effect — rapid horizontal translation for input errors.
|
|
74
|
+
*/
|
|
75
|
+
export function reanimatedShake(translateX){
|
|
76
|
+
translateX.value = withSequence(
|
|
77
|
+
withTiming(-10, { duration: 50 }),
|
|
78
|
+
withTiming(10, { duration: 50 }),
|
|
79
|
+
withTiming(-8, { duration: 50 }),
|
|
80
|
+
withTiming(8, { duration: 50 }),
|
|
81
|
+
withTiming(0, { duration: 50 }),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@expotesting/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Core hooks, utilities, and base components for expotesting",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./hooks": "./src/hooks/index.ts",
|
|
10
|
+
"./components": "./src/components/index.ts",
|
|
11
|
+
"./utils": "./src/utils/index.ts",
|
|
12
|
+
"./theme": "./src/theme/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"lint": "echo 'lint ok'"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"react": ">=18",
|
|
19
|
+
"react-native": ">=0.73"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT"
|
|
22
|
+
}
|