@funtools/create-react-native-app 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +2 -0
- package/index.js +93 -0
- package/package.json +12 -0
- package/templates/base/babel.config.js +11 -0
- package/templates/base/global.css +3 -0
- package/templates/base/index.js +6 -0
- package/templates/base/metro.config.js +18 -0
- package/templates/base/nativewind-env.d.ts +1 -0
- package/templates/base/package.json +54 -0
- package/templates/base/react-native.config.js +4 -0
- package/templates/base/src/App.tsx +16 -0
- package/templates/base/src/Navigation/NavigationContainer/App.tsx +16 -0
- package/templates/base/src/Navigation/StackNavigators/Root/index.tsx +23 -0
- package/templates/base/src/Navigation/StackNavigators/Root/types.ts +5 -0
- package/templates/base/src/Navigation/index.ts +2 -0
- package/templates/base/src/Screens/Home.tsx +10 -0
- package/templates/base/src/Shared/Assets/Fonts/Roboto-Bold.ttf +0 -0
- package/templates/base/src/Shared/Assets/Fonts/Roboto-ExtraBold.ttf +0 -0
- package/templates/base/src/Shared/Assets/Fonts/Roboto-ExtraLight.ttf +0 -0
- package/templates/base/src/Shared/Assets/Fonts/Roboto-Light.ttf +0 -0
- package/templates/base/src/Shared/Assets/Fonts/Roboto-Medium.ttf +0 -0
- package/templates/base/src/Shared/Assets/Fonts/Roboto-Regular.ttf +0 -0
- package/templates/base/src/Shared/Assets/Fonts/Roboto-SemiBold.ttf +0 -0
- package/templates/base/src/Shared/Assets/Fonts/Roboto-Thin.ttf +0 -0
- package/templates/base/src/Shared/Components/Core/Icon.tsx +24 -0
- package/templates/base/src/Shared/Components/Core/Modals/CenterModal.tsx +134 -0
- package/templates/base/src/Shared/Components/Core/Modals/index.tsx +1 -0
- package/templates/base/src/Shared/Components/Core/RippleContainer.tsx +91 -0
- package/templates/base/src/Shared/Components/Core/ShowWhen.tsx +11 -0
- package/templates/base/src/Shared/Components/UI/Buttons/Button.tsx +53 -0
- package/templates/base/src/Shared/Components/UI/Buttons/IconButton.tsx +32 -0
- package/templates/base/src/Shared/Components/UI/Buttons/Utils/constance.ts +39 -0
- package/templates/base/src/Shared/Components/UI/Buttons/Utils/functions.ts +55 -0
- package/templates/base/src/Shared/Components/UI/Buttons/Utils/types.ts +12 -0
- package/templates/base/src/Shared/Components/UI/Buttons/index.ts +2 -0
- package/templates/base/src/Shared/Hooks/useUpdateEffect.ts +14 -0
- package/templates/base/src/Shared/Stores/Theme/Components/ThemeText.tsx +22 -0
- package/templates/base/src/Shared/Stores/Theme/Components/ThemeView.tsx +25 -0
- package/templates/base/src/Shared/Stores/Theme/Components/index.ts +2 -0
- package/templates/base/src/Shared/Stores/Theme/constance.ts +25 -0
- package/templates/base/src/Shared/Stores/Theme/index.ts +19 -0
- package/templates/base/src/Shared/Stores/Theme/types.ts +3 -0
- package/templates/base/src/Shared/Types/native.type.ts +4 -0
- package/templates/base/src/Shared/Types/number.type.ts +8 -0
- package/templates/base/src/Shared/Types/svg.d.ts +7 -0
- package/templates/base/tailwind.config.js +22 -0
- package/templates/base/tsconfig.json +22 -0
package/.gitattributes
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import prompts from "prompts";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
let appName = "MyApp";
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
console.log("Welcome to @funtools");
|
|
16
|
+
|
|
17
|
+
await getAppName();
|
|
18
|
+
|
|
19
|
+
initApp();
|
|
20
|
+
initTemplates();
|
|
21
|
+
updatePackageJson();
|
|
22
|
+
installDependencies();
|
|
23
|
+
|
|
24
|
+
console.log("✅ App setup complete");
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error("❌ Failed to create app");
|
|
27
|
+
console.error(error);
|
|
28
|
+
} finally {
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async function getAppName() {
|
|
34
|
+
const response = await prompts({
|
|
35
|
+
type: "text",
|
|
36
|
+
name: "appName",
|
|
37
|
+
message: "What is your app name?",
|
|
38
|
+
initial: appName,
|
|
39
|
+
validate: (value) => {
|
|
40
|
+
if (/^[A-Z][A-Za-z0-9]*$/.test(value)) return true;
|
|
41
|
+
return "❌ App name must start with an uppercase letter and contain only letters and numbers.";
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!response.appName) throw Error("❌ App creation cancelled");
|
|
46
|
+
|
|
47
|
+
appName = response.appName;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
function initApp() {
|
|
52
|
+
console.log(`🚀 Initializing React Native app: ${appName}`);
|
|
53
|
+
|
|
54
|
+
execSync(
|
|
55
|
+
`npx @react-native-community/cli@latest init ${appName} --skip-install`,
|
|
56
|
+
{
|
|
57
|
+
stdio: "inherit",
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
process.chdir(appName);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
function initTemplates() {
|
|
66
|
+
console.log("Initializing templates...");
|
|
67
|
+
|
|
68
|
+
fs.cpSync(path.join(__dirname, `templates/base`), process.cwd(), {
|
|
69
|
+
recursive: true,
|
|
70
|
+
focus: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
function updatePackageJson() {
|
|
76
|
+
console.log("Updating package.json...");
|
|
77
|
+
|
|
78
|
+
fs.writeFileSync(
|
|
79
|
+
`${process.cwd()}/package.json`,
|
|
80
|
+
fs
|
|
81
|
+
.readFileSync(`${process.cwd()}/package.json`, "utf-8")
|
|
82
|
+
.replaceAll("$AppName", appName),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
function installDependencies() {
|
|
88
|
+
console.log("Installing dependencies...");
|
|
89
|
+
|
|
90
|
+
execSync("npm install", {
|
|
91
|
+
stdio: "inherit",
|
|
92
|
+
});
|
|
93
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@funtools/create-react-native-app",
|
|
3
|
+
"description": "Create a React Native app with preconfigured setup",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-react-native-app": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"prompts": "^2.4.2"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
|
|
2
|
+
const { withNativeWind } = require("nativewind/metro");
|
|
3
|
+
|
|
4
|
+
const defaultConfig = getDefaultConfig(__dirname);
|
|
5
|
+
const { assetExts, sourceExts } = defaultConfig.resolver;
|
|
6
|
+
|
|
7
|
+
const config = mergeConfig(defaultConfig, {
|
|
8
|
+
/* your config */
|
|
9
|
+
transformer: {
|
|
10
|
+
babelTransformerPath: require.resolve("react-native-svg-transformer")
|
|
11
|
+
},
|
|
12
|
+
resolver: {
|
|
13
|
+
assetExts: assetExts.filter((ext) => ext !== "svg"),
|
|
14
|
+
sourceExts: [...sourceExts, "svg"]
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
module.exports = withNativeWind(config, { input: "./global.css" });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="nativewind/types" />
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "$AppName",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"android": "react-native run-android",
|
|
7
|
+
"ios": "react-native run-ios",
|
|
8
|
+
"lint": "eslint .",
|
|
9
|
+
"start": "react-native start",
|
|
10
|
+
"test": "jest"
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@fun-tools/store": "^1.0.3",
|
|
15
|
+
"@react-navigation/native": "^7.1.26",
|
|
16
|
+
"@react-navigation/native-stack": "^7.9.0",
|
|
17
|
+
"lucide-react-native": "^0.562.0",
|
|
18
|
+
"nativewind": "^4.2.1",
|
|
19
|
+
"react": "19.2.0",
|
|
20
|
+
"react-native": "0.83.1",
|
|
21
|
+
"react-native-gesture-handler": "^2.30.0",
|
|
22
|
+
"react-native-safe-area-context": "^5.6.2",
|
|
23
|
+
"react-native-screens": "^4.19.0",
|
|
24
|
+
"react-native-svg": "^15.15.1"
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@babel/core": "^7.25.2",
|
|
29
|
+
"@babel/preset-env": "^7.25.3",
|
|
30
|
+
"@babel/runtime": "^7.25.0",
|
|
31
|
+
"@react-native-community/cli": "20.0.0",
|
|
32
|
+
"@react-native-community/cli-platform-android": "20.0.0",
|
|
33
|
+
"@react-native-community/cli-platform-ios": "20.0.0",
|
|
34
|
+
"@react-native/babel-preset": "0.83.1",
|
|
35
|
+
"@react-native/eslint-config": "0.83.1",
|
|
36
|
+
"@react-native/metro-config": "0.83.1",
|
|
37
|
+
"@react-native/typescript-config": "0.83.1",
|
|
38
|
+
"@types/jest": "^29.5.13",
|
|
39
|
+
"@types/react": "^19.2.0",
|
|
40
|
+
"@types/react-test-renderer": "^19.1.0",
|
|
41
|
+
"babel-plugin-module-resolver": "^5.0.2",
|
|
42
|
+
"eslint": "^8.19.0",
|
|
43
|
+
"jest": "^29.6.3",
|
|
44
|
+
"prettier": "2.8.8",
|
|
45
|
+
"react-native-svg-transformer": "^1.5.2",
|
|
46
|
+
"react-test-renderer": "19.2.0",
|
|
47
|
+
"tailwindcss": "^3.4.19",
|
|
48
|
+
"typescript": "^5.8.3"
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=20"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|
2
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
3
|
+
import { AppNavigationContainer } from './Navigation';
|
|
4
|
+
import ThemeView from './Shared/Stores/Theme/Components/ThemeView';
|
|
5
|
+
|
|
6
|
+
export default function App() {
|
|
7
|
+
return (
|
|
8
|
+
<GestureHandlerRootView className='flex-1' >
|
|
9
|
+
<SafeAreaProvider>
|
|
10
|
+
<ThemeView className='flex-1 w-full h-full' >
|
|
11
|
+
<AppNavigationContainer/>
|
|
12
|
+
</ThemeView>
|
|
13
|
+
</SafeAreaProvider>
|
|
14
|
+
</GestureHandlerRootView>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createNavigationContainerRef,
|
|
3
|
+
NavigationContainer,
|
|
4
|
+
} from '@react-navigation/native';
|
|
5
|
+
import RootStackNavigator from '../StackNavigators/Root';
|
|
6
|
+
import { RootStackParamList } from '../StackNavigators/Root/types';
|
|
7
|
+
|
|
8
|
+
export const navigationRef = createNavigationContainerRef<RootStackParamList>();
|
|
9
|
+
|
|
10
|
+
export default function AppNavigationContainer() {
|
|
11
|
+
return (
|
|
12
|
+
<NavigationContainer ref={navigationRef}>
|
|
13
|
+
<RootStackNavigator />
|
|
14
|
+
</NavigationContainer>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
2
|
+
import { RootStackParamList } from './types';
|
|
3
|
+
import HomeScreen from '@/Screens/Home';
|
|
4
|
+
|
|
5
|
+
const Stack = createNativeStackNavigator<RootStackParamList>();
|
|
6
|
+
|
|
7
|
+
const screens: Array<Parameters<typeof Stack.Screen>[0]> = [
|
|
8
|
+
{ name: 'Home', component: HomeScreen },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export default function RootStackNavigator() {
|
|
12
|
+
return (
|
|
13
|
+
<Stack.Navigator
|
|
14
|
+
screenOptions={{
|
|
15
|
+
headerShown: false,
|
|
16
|
+
}}
|
|
17
|
+
>
|
|
18
|
+
{screens.map((screen, index) => (
|
|
19
|
+
<Stack.Screen key={index} {...screen} />
|
|
20
|
+
))}
|
|
21
|
+
</Stack.Navigator>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as icons from 'lucide-react-native/icons';
|
|
2
|
+
import { LucideProps } from "lucide-react-native";
|
|
3
|
+
import { useThemeStore } from "../../Stores/Theme";
|
|
4
|
+
import { ColorStates } from '../../Stores/Theme/types';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export type IconName = keyof typeof icons;
|
|
8
|
+
|
|
9
|
+
export type IconProps = LucideProps & {
|
|
10
|
+
name: IconName,
|
|
11
|
+
|
|
12
|
+
size?: number,
|
|
13
|
+
color?: ColorStates,
|
|
14
|
+
customColor?: string,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function Icon({name, size=16, color='text', customColor, ...props}: IconProps){
|
|
18
|
+
|
|
19
|
+
const colors = useThemeStore(states => customColor ?? states.colors[color])
|
|
20
|
+
|
|
21
|
+
const LucideIcon = icons[name];
|
|
22
|
+
|
|
23
|
+
return <LucideIcon {...props} color={colors} size={size} />;
|
|
24
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Dispatch, ReactNode, SetStateAction, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Animated, Modal, ModalProps, PanResponder, useAnimatedValue, useWindowDimensions } from "react-native";
|
|
3
|
+
import RippleContainer from "../RippleContainer";
|
|
4
|
+
import ThemeView, { ThemeViewProps } from "../../../Stores/Theme/Components/ThemeView";
|
|
5
|
+
import { useThemeStore } from "../../../Stores/Theme";
|
|
6
|
+
import { ColorStates } from "../../../Stores/Theme/types";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export type CenterModalProps = Omit<ModalProps, 'animationType'> & {
|
|
11
|
+
children: ReactNode,
|
|
12
|
+
visible: boolean,
|
|
13
|
+
setVisible: Dispatch<SetStateAction<boolean>>,
|
|
14
|
+
preventCloseRequest?: boolean,
|
|
15
|
+
containerProps?: ThemeViewProps,
|
|
16
|
+
backdropColor?: ColorStates
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
export default function CenterModal({children, visible, setVisible, preventCloseRequest=false, onRequestClose, style, containerProps, backdropColor: backdropVariant='text', ...props}: CenterModalProps) {
|
|
21
|
+
|
|
22
|
+
const backdropColor = useThemeStore(states => states.colors[backdropVariant].replace(')', ', 0.8)'));
|
|
23
|
+
|
|
24
|
+
const {width: windowWidth, height: windowHeight} = useWindowDimensions();
|
|
25
|
+
|
|
26
|
+
const [show, setShow] = useState(visible);
|
|
27
|
+
|
|
28
|
+
const animatedValue = useAnimatedValue(0);
|
|
29
|
+
|
|
30
|
+
const translate = useRef(new Animated.ValueXY({x: 0, y: 0})).current;
|
|
31
|
+
|
|
32
|
+
const {panHandlers} = useRef(PanResponder.create({
|
|
33
|
+
onStartShouldSetPanResponder: () => true,
|
|
34
|
+
onMoveShouldSetPanResponder: (_, gestureState) => {
|
|
35
|
+
return Math.abs(gestureState.dy) > 5;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
onPanResponderTerminationRequest: () => false,
|
|
39
|
+
|
|
40
|
+
onPanResponderMove: (_, {dx, dy}) => {
|
|
41
|
+
translate.setValue({x: dx, y: dy});
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
onPanResponderRelease: (_, {vx, vy, dx, dy}) => {
|
|
45
|
+
const isNearEdge = [
|
|
46
|
+
Math.abs(dx) > windowWidth * 0.4,
|
|
47
|
+
Math.abs(dy) > windowHeight * 0.4
|
|
48
|
+
].some(Boolean);
|
|
49
|
+
|
|
50
|
+
const isMovingFast = [Math.abs(vx) > 2, Math.abs(vy) > 2].some(Boolean);
|
|
51
|
+
|
|
52
|
+
if((isNearEdge || isMovingFast) && !preventCloseRequest) {
|
|
53
|
+
return setVisible(false);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Animated.spring(translate, {
|
|
57
|
+
toValue: {x: 0, y: 0},
|
|
58
|
+
bounciness: 12,
|
|
59
|
+
useNativeDriver: true
|
|
60
|
+
}).start()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
})).current;
|
|
64
|
+
|
|
65
|
+
function handleClose() {
|
|
66
|
+
setTimeout(() => setShow(false), 150)
|
|
67
|
+
|
|
68
|
+
Animated.spring(animatedValue, {
|
|
69
|
+
toValue: 0,
|
|
70
|
+
bounciness: 12,
|
|
71
|
+
useNativeDriver: true
|
|
72
|
+
}).start()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function handleOnRequestClose() {
|
|
76
|
+
if(preventCloseRequest) return;
|
|
77
|
+
|
|
78
|
+
setVisible(false);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if(visible){
|
|
83
|
+
setShow(true);
|
|
84
|
+
translate.setValue({x: 0, y: 0});
|
|
85
|
+
|
|
86
|
+
Animated.spring(animatedValue, {
|
|
87
|
+
toValue: 1,
|
|
88
|
+
bounciness: 12,
|
|
89
|
+
useNativeDriver: true
|
|
90
|
+
}).start()
|
|
91
|
+
} else {
|
|
92
|
+
handleClose();
|
|
93
|
+
}
|
|
94
|
+
}, [visible])
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Modal {...props}
|
|
99
|
+
visible={show}
|
|
100
|
+
transparent
|
|
101
|
+
|
|
102
|
+
onRequestClose={handleOnRequestClose}
|
|
103
|
+
>
|
|
104
|
+
<Animated.View className="flex-1 w-full h-full items-center justify-center"
|
|
105
|
+
style={{
|
|
106
|
+
opacity: animatedValue,
|
|
107
|
+
backgroundColor: backdropColor
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
<RippleContainer className="flex-1 w-full" onPress={handleOnRequestClose} rippleOpacity={0.2} />
|
|
111
|
+
|
|
112
|
+
<Animated.View {...panHandlers}
|
|
113
|
+
className={'w-full p-2 relative'}
|
|
114
|
+
style={{
|
|
115
|
+
opacity: animatedValue,
|
|
116
|
+
transform: [
|
|
117
|
+
{translateX: translate.x}, {translateY: translate.y},
|
|
118
|
+
{scale: animatedValue.interpolate({inputRange: [0, 1], outputRange: [0.4, 1]})}
|
|
119
|
+
]
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
<ThemeView
|
|
123
|
+
{...containerProps}
|
|
124
|
+
style={[{ borderRadius: 12, padding: 4 }, style, {overflow: 'hidden', width: '100%'}]}
|
|
125
|
+
>
|
|
126
|
+
{children}
|
|
127
|
+
</ThemeView>
|
|
128
|
+
</Animated.View>
|
|
129
|
+
|
|
130
|
+
<RippleContainer className="flex-1 w-full" onPress={handleOnRequestClose} rippleOpacity={0.2} />
|
|
131
|
+
</Animated.View>
|
|
132
|
+
</Modal>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {default as CenterModal} from './CenterModal';
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ReactNode, useRef, useState } from "react";
|
|
2
|
+
import { useThemeStore } from "../../Stores/Theme";
|
|
3
|
+
import { ColorStates } from "../../Stores/Theme/types";
|
|
4
|
+
import { Animated, GestureResponderEvent, Pressable, PressableProps, useAnimatedValue, View, ViewStyle } from "react-native";
|
|
5
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export type RippleContainerProps = PressableProps & {
|
|
9
|
+
children?: ReactNode,
|
|
10
|
+
color?: ColorStates
|
|
11
|
+
style?: ViewStyle
|
|
12
|
+
rippleOpacity?: number,
|
|
13
|
+
rippleColor?: string,
|
|
14
|
+
rippleScale?: number,
|
|
15
|
+
rippleCount?: number,
|
|
16
|
+
duration?: number,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
export default function RippleContainer({children, style, onPress, color='text', rippleColor, rippleOpacity=0.4, rippleScale=1, duration=300, rippleCount=3, ...props}: RippleContainerProps) {
|
|
21
|
+
|
|
22
|
+
const {top, left} = useSafeAreaInsets();
|
|
23
|
+
|
|
24
|
+
const rgb = useThemeStore(s => s.colors[color]);
|
|
25
|
+
rippleColor ??= `rgb(${rgb})`;
|
|
26
|
+
|
|
27
|
+
const [position, setPosition] = useState<{top: number, left: number}>({top: 0, left: 0});
|
|
28
|
+
|
|
29
|
+
const animatedValue = useAnimatedValue(0);
|
|
30
|
+
|
|
31
|
+
const button = useRef<View>(null);
|
|
32
|
+
|
|
33
|
+
function handleOnPress(event: GestureResponderEvent) {
|
|
34
|
+
const {pageX, pageY} = event.nativeEvent;
|
|
35
|
+
|
|
36
|
+
button.current?.measureInWindow((x, y, w) => {
|
|
37
|
+
x += left;
|
|
38
|
+
y += top;
|
|
39
|
+
|
|
40
|
+
setPosition({top: pageY - y - w / 2, left: pageX - x - w / 2})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
startAnimation();
|
|
44
|
+
|
|
45
|
+
onPress?.(event);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
function startAnimation() {
|
|
50
|
+
Animated.timing(animatedValue, {
|
|
51
|
+
toValue: 1,
|
|
52
|
+
duration,
|
|
53
|
+
useNativeDriver: true
|
|
54
|
+
}).start(() => {
|
|
55
|
+
animatedValue.setValue(0);
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Pressable ref={button} {...props} onPress={handleOnPress} style={[style, { overflow: 'hidden', position: 'relative' }]}>
|
|
62
|
+
<View className="absolute aspect-square" style={{...position, width: '100%'}} >
|
|
63
|
+
{
|
|
64
|
+
[...new Array(Math.min(rippleCount, 5))].map((_, index) => (
|
|
65
|
+
<Animated.View key={index}
|
|
66
|
+
className={'absolute w-full aspect-square rounded-full'}
|
|
67
|
+
style={{
|
|
68
|
+
backgroundColor: rippleColor,
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
opacity: animatedValue.interpolate({
|
|
72
|
+
inputRange: [0, 1],
|
|
73
|
+
outputRange: [rippleOpacity * ((rippleCount - index) / rippleCount), 0],
|
|
74
|
+
}),
|
|
75
|
+
|
|
76
|
+
transform: [{
|
|
77
|
+
scale: animatedValue.interpolate({
|
|
78
|
+
inputRange: [0, 0.1, 1],
|
|
79
|
+
outputRange: [0, rippleScale * 0.1, rippleScale + (index * 0.1)]
|
|
80
|
+
})
|
|
81
|
+
}]
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
))
|
|
85
|
+
}
|
|
86
|
+
</View>
|
|
87
|
+
|
|
88
|
+
{children}
|
|
89
|
+
</Pressable>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import RippleContainer, { RippleContainerProps } from '@/Shared/Components/Core/RippleContainer';
|
|
2
|
+
import Icon, { IconName } from '@/Shared/Components/Core/Icon';
|
|
3
|
+
import { ButtonSize, ButtonVariants } from './Utils/types';
|
|
4
|
+
import { RANGE } from '@/Shared/Types/number.type';
|
|
5
|
+
import { getButtonStyle } from './Utils/functions';
|
|
6
|
+
import { BUTTON_LAYOUT } from './Utils/constance';
|
|
7
|
+
import ThemeText from '@/Shared/Stores/Theme/Components/ThemeText';
|
|
8
|
+
import ShowWhen from '@/Shared/Components/Core/ShowWhen';
|
|
9
|
+
|
|
10
|
+
type ButtonProp = Omit<RippleContainerProps, 'rippleColor' | 'rippleScale'> & {
|
|
11
|
+
title: string;
|
|
12
|
+
|
|
13
|
+
startIcon?: IconName;
|
|
14
|
+
endIcon?: IconName;
|
|
15
|
+
variant?: ButtonVariants;
|
|
16
|
+
size?: ButtonSize;
|
|
17
|
+
rounded?: number | `${RANGE<0, 100>}%`;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export default function Button({ title, startIcon, endIcon, variant = 'solid', color = 'primary', size = 'md', rounded, style, ...props}: ButtonProp) {
|
|
22
|
+
|
|
23
|
+
const { color: textColor, borderColor, backgroundColor } = getButtonStyle(variant, color);
|
|
24
|
+
|
|
25
|
+
const {fontSize, ...containerStyle} = {
|
|
26
|
+
...BUTTON_LAYOUT[size],
|
|
27
|
+
...rounded !== undefined ? {borderRadius: rounded} : {}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<RippleContainer
|
|
32
|
+
{...props}
|
|
33
|
+
rippleColor={textColor}
|
|
34
|
+
rippleScale={2}
|
|
35
|
+
|
|
36
|
+
style={{
|
|
37
|
+
backgroundColor, borderColor, flexDirection: 'row', gap: Math.floor(fontSize / 2), alignItems: 'center', justifyContent: 'center',
|
|
38
|
+
...containerStyle,
|
|
39
|
+
...style,
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<ShowWhen when={!!startIcon}>
|
|
43
|
+
<Icon name={startIcon as IconName} customColor={textColor} />
|
|
44
|
+
</ShowWhen>
|
|
45
|
+
|
|
46
|
+
<ThemeText textColor={textColor} style={{fontSize}} >{title}</ThemeText>
|
|
47
|
+
|
|
48
|
+
<ShowWhen when={!!endIcon} >
|
|
49
|
+
<Icon name={endIcon as IconName} customColor={textColor} />
|
|
50
|
+
</ShowWhen>
|
|
51
|
+
</RippleContainer>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { RANGE } from "../../../Types/number.type"
|
|
2
|
+
import Icon, { IconName } from "../../Core/Icon"
|
|
3
|
+
import RippleContainer, { RippleContainerProps } from "../../Core/RippleContainer"
|
|
4
|
+
import { BUTTON_LAYOUT } from "./Utils/constance"
|
|
5
|
+
import { getButtonStyle } from "./Utils/functions"
|
|
6
|
+
import { ButtonSize, ButtonVariants } from "./Utils/types"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export type IconButtonProps = RippleContainerProps & {
|
|
10
|
+
icon: IconName,
|
|
11
|
+
|
|
12
|
+
variant?: ButtonVariants,
|
|
13
|
+
size?: number | ButtonSize,
|
|
14
|
+
rounded?: number | `${RANGE<0, 100>}%`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function IconButton({variant='soft', color='primary', icon, size='md', rounded='50%', ...props}: IconButtonProps) {
|
|
18
|
+
|
|
19
|
+
const {color: textColor, ...style} = getButtonStyle(variant, color);
|
|
20
|
+
|
|
21
|
+
const height = typeof size === 'number' ? size : BUTTON_LAYOUT[size].height;
|
|
22
|
+
const borderWidth = typeof size === 'number' ? 1 : BUTTON_LAYOUT[size].borderWidth;
|
|
23
|
+
return (
|
|
24
|
+
<RippleContainer {...props}
|
|
25
|
+
rippleScale={2}
|
|
26
|
+
rippleColor={textColor}
|
|
27
|
+
style={{...style, height, borderRadius: rounded, borderWidth, alignItems: 'center', justifyContent: 'center', aspectRatio: 1}}
|
|
28
|
+
>
|
|
29
|
+
<Icon customColor={textColor} name={icon} size={Math.floor(height * 0.6)} />
|
|
30
|
+
</RippleContainer>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ButtonLayout, ButtonSize } from "./types";
|
|
2
|
+
|
|
3
|
+
export const BUTTON_LAYOUT: Record<ButtonSize, ButtonLayout> = {
|
|
4
|
+
'xs': {
|
|
5
|
+
height: 24,
|
|
6
|
+
borderRadius: 8,
|
|
7
|
+
paddingInline: 8,
|
|
8
|
+
fontSize: 10,
|
|
9
|
+
borderWidth: 1
|
|
10
|
+
},
|
|
11
|
+
'sm': {
|
|
12
|
+
height: 32,
|
|
13
|
+
borderRadius: 12,
|
|
14
|
+
paddingInline: 12,
|
|
15
|
+
fontSize: 14,
|
|
16
|
+
borderWidth: 1
|
|
17
|
+
},
|
|
18
|
+
'md': {
|
|
19
|
+
height: 40,
|
|
20
|
+
borderRadius: 14,
|
|
21
|
+
paddingInline: 14,
|
|
22
|
+
fontSize: 16,
|
|
23
|
+
borderWidth: 1
|
|
24
|
+
},
|
|
25
|
+
'lg': {
|
|
26
|
+
height: 48,
|
|
27
|
+
borderRadius: 16,
|
|
28
|
+
paddingInline: 16,
|
|
29
|
+
fontSize: 20,
|
|
30
|
+
borderWidth: 2
|
|
31
|
+
},
|
|
32
|
+
'xl': {
|
|
33
|
+
height: 56,
|
|
34
|
+
borderRadius: 18,
|
|
35
|
+
paddingInline: 18,
|
|
36
|
+
fontSize: 24,
|
|
37
|
+
borderWidth: 2
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useThemeStore } from "../../../../Stores/Theme";
|
|
3
|
+
import { ColorStates } from "../../../../Stores/Theme/types";
|
|
4
|
+
import { ButtonVariants } from "./types";
|
|
5
|
+
|
|
6
|
+
export function getButtonStyle(variant: ButtonVariants, color: ColorStates) {
|
|
7
|
+
const {textColor, bgColor} = useThemeStore((states) => {
|
|
8
|
+
if(['text', 'bg'].includes(color ?? '')) {
|
|
9
|
+
return {
|
|
10
|
+
bgColor: states.colors[color],
|
|
11
|
+
textColor: states.colors[color === 'text' ? 'bg' : 'text']
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
bgColor: states.colors[color],
|
|
17
|
+
textColor: 'rgb(255,255,255)'
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
const style = useMemo(() => ({
|
|
23
|
+
'solid': {
|
|
24
|
+
color: textColor,
|
|
25
|
+
backgroundColor: bgColor,
|
|
26
|
+
borderColor: bgColor,
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
'outlined': {
|
|
30
|
+
color: bgColor,
|
|
31
|
+
backgroundColor: 'transparent',
|
|
32
|
+
borderColor: bgColor,
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
'soft': {
|
|
36
|
+
color: bgColor,
|
|
37
|
+
backgroundColor: bgColor.replace(')', ', 0.2)'),
|
|
38
|
+
borderColor: bgColor.replace(')', ', 0.2)'),
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
'soft-outlined': {
|
|
42
|
+
color: bgColor,
|
|
43
|
+
backgroundColor: bgColor.replace(')', ', 0.2)'),
|
|
44
|
+
borderColor: bgColor
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
'text': {
|
|
48
|
+
color: bgColor,
|
|
49
|
+
backgroundColor: 'transparent',
|
|
50
|
+
borderColor: 'transparent',
|
|
51
|
+
}
|
|
52
|
+
}[variant]), [bgColor, textColor]);
|
|
53
|
+
|
|
54
|
+
return style;
|
|
55
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type ButtonVariants = 'solid' | 'outlined' | 'soft' | 'soft-outlined' | 'text';
|
|
2
|
+
|
|
3
|
+
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
4
|
+
|
|
5
|
+
// add more according to need;
|
|
6
|
+
export type ButtonLayout = {
|
|
7
|
+
height: number,
|
|
8
|
+
borderRadius: number,
|
|
9
|
+
paddingInline: number,
|
|
10
|
+
fontSize: number,
|
|
11
|
+
borderWidth: number
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DependencyList, EffectCallback, useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export default function useUpdateEffect(effect: EffectCallback, deps: DependencyList) {
|
|
5
|
+
const mountedRef = useRef(false);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (mountedRef.current) {
|
|
9
|
+
return effect();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
mountedRef.current = true;
|
|
13
|
+
}, deps);
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Animated, TextProps } from "react-native";
|
|
2
|
+
import { useThemeStore } from "..";
|
|
3
|
+
import { ColorStates } from "../types";
|
|
4
|
+
import { AnimatedInterpolValue } from "../../../Types/native.type";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export type ThemeTextProps = TextProps & {
|
|
8
|
+
color?: ColorStates,
|
|
9
|
+
textColor?: string | AnimatedInterpolValue
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function ThemeText({style, color: _color = 'text', textColor, className, ...props}: ThemeTextProps): React.JSX.Element {
|
|
13
|
+
|
|
14
|
+
const color = useThemeStore(states => textColor ?? states.colors[_color]);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Animated.Text {...props}
|
|
18
|
+
style={[style, {color}]}
|
|
19
|
+
className={`font-regular ${className}`}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Animated, ViewProps } from "react-native";
|
|
2
|
+
import { useThemeStore } from "..";
|
|
3
|
+
import { ColorStates } from "../types";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export type ThemeViewProps = ViewProps & {
|
|
7
|
+
color?: ColorStates,
|
|
8
|
+
backgroundColor?: string,
|
|
9
|
+
useWindBackground?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function ThemeView({style, color = 'bg', backgroundColor, useWindBackground=false, ...props}: ThemeViewProps): React.JSX.Element {
|
|
13
|
+
|
|
14
|
+
const {_backgroundColor} = useThemeStore(states => ({
|
|
15
|
+
_backgroundColor: states.colors[color]
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
if(!backgroundColor) backgroundColor = _backgroundColor;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Animated.View {...props}
|
|
22
|
+
style={[style, useWindBackground === false ? {backgroundColor} : null]}
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Theme, ColorStates } from './types';
|
|
2
|
+
|
|
3
|
+
export const _theme: Theme = 'light';
|
|
4
|
+
|
|
5
|
+
export const _colors: Record<Theme, Record<ColorStates, string>> = {
|
|
6
|
+
light: {
|
|
7
|
+
text: 'rgb(0, 0, 0)',
|
|
8
|
+
bg: 'rgb(240, 242, 245)',
|
|
9
|
+
|
|
10
|
+
primary: 'rgb(40, 120, 255)',
|
|
11
|
+
error: 'rgb(245, 34, 45)',
|
|
12
|
+
warning: 'rgb(255, 184, 0)',
|
|
13
|
+
info: 'rgb(74, 108, 135)',
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
dark: {
|
|
17
|
+
text: 'rgb(240, 242, 245)',
|
|
18
|
+
bg: 'rgb(0, 0, 0)',
|
|
19
|
+
|
|
20
|
+
primary: 'rgb(40, 120, 255)',
|
|
21
|
+
error: 'rgb(245, 34, 45)',
|
|
22
|
+
warning: 'rgb(255, 184, 0)',
|
|
23
|
+
info: 'rgb(74, 108, 135)',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Theme } from './types';
|
|
2
|
+
import { _colors, _theme } from './constance';
|
|
3
|
+
import { createStore } from '@fun-tools/store';
|
|
4
|
+
|
|
5
|
+
const { useStore, useHandlers } = createStore({
|
|
6
|
+
states: {
|
|
7
|
+
theme: _theme,
|
|
8
|
+
colors: _colors[_theme],
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
syncHandlers: {
|
|
12
|
+
toggleTheme(state) {
|
|
13
|
+
state.theme = state.theme === 'dark' ? 'light' : 'dark';
|
|
14
|
+
state.colors = _colors[state.theme];
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export { useStore as useThemeStore, useHandlers as useThemeHandlers };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type FINITE_NUMBER<E extends number, A extends unknown[] = []> = (
|
|
2
|
+
A['length'] extends E ? A[number] | E : FINITE_NUMBER<E, [...A, A['length']]>
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export type RANGE<S extends number, E extends number> = (
|
|
7
|
+
Exclude<FINITE_NUMBER<E>, FINITE_NUMBER<S>> | S
|
|
8
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
content: [
|
|
4
|
+
"./src/**/*.{js,jsx,ts,tsx}",
|
|
5
|
+
],
|
|
6
|
+
presets: [require("nativewind/preset")],
|
|
7
|
+
theme: {
|
|
8
|
+
extend: {
|
|
9
|
+
fontFamily: {
|
|
10
|
+
thin: ['Roboto-Thin'],
|
|
11
|
+
extralight: ['Roboto-ExtraLight'],
|
|
12
|
+
light: ['Roboto-Light'],
|
|
13
|
+
regular: ['Roboto-Regular'],
|
|
14
|
+
medium: ['Roboto-Medium'],
|
|
15
|
+
semibold: ['Roboto-SemiBold'],
|
|
16
|
+
bold: ['Roboto-Bold'],
|
|
17
|
+
extrabold: ['Roboto-ExtraBold'],
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
plugins: [],
|
|
22
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@react-native/typescript-config",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"types": [
|
|
5
|
+
"jest"
|
|
6
|
+
],
|
|
7
|
+
"paths": {
|
|
8
|
+
"@/*": [
|
|
9
|
+
"./src/*"
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"include": [
|
|
14
|
+
"**/*.ts",
|
|
15
|
+
"**/*.tsx",
|
|
16
|
+
"nativewind-env.d.ts"
|
|
17
|
+
, "react-native.config.js" ],
|
|
18
|
+
"exclude": [
|
|
19
|
+
"**/node_modules",
|
|
20
|
+
"**/Pods"
|
|
21
|
+
]
|
|
22
|
+
}
|