parent
35b5f45de9
commit
854af00711
@ -0,0 +1,41 @@ |
|||||||
|
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files |
||||||
|
|
||||||
|
# dependencies |
||||||
|
node_modules/ |
||||||
|
|
||||||
|
# Expo |
||||||
|
.expo/ |
||||||
|
dist/ |
||||||
|
web-build/ |
||||||
|
expo-env.d.ts |
||||||
|
|
||||||
|
# Native |
||||||
|
.kotlin/ |
||||||
|
*.orig.* |
||||||
|
*.jks |
||||||
|
*.p8 |
||||||
|
*.p12 |
||||||
|
*.key |
||||||
|
*.mobileprovision |
||||||
|
|
||||||
|
# Metro |
||||||
|
.metro-health-check* |
||||||
|
|
||||||
|
# debug |
||||||
|
npm-debug.* |
||||||
|
yarn-debug.* |
||||||
|
yarn-error.* |
||||||
|
|
||||||
|
# macOS |
||||||
|
.DS_Store |
||||||
|
*.pem |
||||||
|
|
||||||
|
# local env files |
||||||
|
.env*.local |
||||||
|
|
||||||
|
# typescript |
||||||
|
*.tsbuildinfo |
||||||
|
|
||||||
|
# generated native folders |
||||||
|
/ios |
||||||
|
/android |
@ -0,0 +1,22 @@ |
|||||||
|
import { StatusBar } from 'expo-status-bar'; |
||||||
|
import { useState } from 'react'; |
||||||
|
import { PaperProvider } from 'react-native-paper'; |
||||||
|
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; |
||||||
|
import { appTheme } from './src/theme'; |
||||||
|
import TestPage from './pages/TestScreen'; |
||||||
|
import { CounterProvider } from './stores/CounterStore'; |
||||||
|
|
||||||
|
export default function App() { |
||||||
|
return ( |
||||||
|
<PaperProvider theme={appTheme}> |
||||||
|
<SafeAreaProvider> |
||||||
|
<SafeAreaView style={{ flex: 1 }}> |
||||||
|
<StatusBar style="auto" /> |
||||||
|
<CounterProvider> |
||||||
|
<TestPage /> |
||||||
|
</CounterProvider> |
||||||
|
</SafeAreaView> |
||||||
|
</SafeAreaProvider> |
||||||
|
</PaperProvider> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
{ |
||||||
|
"expo": { |
||||||
|
"name": "my-expo-app--paper", |
||||||
|
"slug": "my-expo-app--paper", |
||||||
|
"version": "1.0.0", |
||||||
|
"orientation": "portrait", |
||||||
|
"icon": "./assets/icon.png", |
||||||
|
"userInterfaceStyle": "light", |
||||||
|
"newArchEnabled": true, |
||||||
|
"splash": { |
||||||
|
"image": "./assets/splash-icon.png", |
||||||
|
"resizeMode": "contain", |
||||||
|
"backgroundColor": "#ffffff" |
||||||
|
}, |
||||||
|
"ios": { |
||||||
|
"supportsTablet": true |
||||||
|
}, |
||||||
|
"android": { |
||||||
|
"adaptiveIcon": { |
||||||
|
"foregroundImage": "./assets/adaptive-icon.png", |
||||||
|
"backgroundColor": "#ffffff" |
||||||
|
}, |
||||||
|
"edgeToEdgeEnabled": true, |
||||||
|
"predictiveBackGestureEnabled": false |
||||||
|
}, |
||||||
|
"web": { |
||||||
|
"favicon": "./assets/favicon.png" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 17 KiB |
@ -0,0 +1,71 @@ |
|||||||
|
import { View, ViewStyle, StyleProp } from 'react-native'; |
||||||
|
|
||||||
|
type Align = 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline'; |
||||||
|
type Justify = |
||||||
|
| 'flex-start' |
||||||
|
| 'center' |
||||||
|
| 'flex-end' |
||||||
|
| 'space-between' |
||||||
|
| 'space-around' |
||||||
|
| 'space-evenly'; |
||||||
|
|
||||||
|
type StackProps = { |
||||||
|
gap?: number; |
||||||
|
align?: Align; |
||||||
|
justify?: Justify; |
||||||
|
wrap?: boolean; |
||||||
|
style?: StyleProp<ViewStyle>; |
||||||
|
children?: React.ReactNode; |
||||||
|
}; |
||||||
|
|
||||||
|
export function HStack({ |
||||||
|
gap = 0, |
||||||
|
align = 'center', |
||||||
|
justify = 'flex-start', |
||||||
|
wrap = false, |
||||||
|
style, |
||||||
|
children, |
||||||
|
}: StackProps) { |
||||||
|
return ( |
||||||
|
<View |
||||||
|
style={[ |
||||||
|
{ |
||||||
|
flexDirection: 'row', |
||||||
|
alignItems: align, |
||||||
|
justifyContent: justify, |
||||||
|
gap, |
||||||
|
flexWrap: wrap ? 'wrap' : 'nowrap', |
||||||
|
}, |
||||||
|
style, |
||||||
|
]}> |
||||||
|
{children} |
||||||
|
</View> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function VStack({ |
||||||
|
gap = 0, |
||||||
|
align = 'stretch', |
||||||
|
justify = 'flex-start', |
||||||
|
wrap = false, |
||||||
|
style, |
||||||
|
children, |
||||||
|
}: StackProps) { |
||||||
|
return ( |
||||||
|
<View |
||||||
|
style={[ |
||||||
|
{ |
||||||
|
flexDirection: 'column', |
||||||
|
alignItems: align, |
||||||
|
justifyContent: justify, |
||||||
|
gap, |
||||||
|
flexWrap: wrap ? 'wrap' : 'nowrap', |
||||||
|
}, |
||||||
|
style, |
||||||
|
]}> |
||||||
|
{children} |
||||||
|
</View> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export const Spacer = ({ flex = 1 }: { flex?: number }) => <View style={{ flex }} />; |
@ -0,0 +1,17 @@ |
|||||||
|
/* eslint-env node */ |
||||||
|
const { defineConfig } = require('eslint/config'); |
||||||
|
const expoConfig = require('eslint-config-expo/flat'); |
||||||
|
const prettierConfig = require('eslint-config-prettier'); |
||||||
|
|
||||||
|
module.exports = defineConfig([ |
||||||
|
...expoConfig, |
||||||
|
{ |
||||||
|
ignores: ['dist/*'], |
||||||
|
}, |
||||||
|
{ |
||||||
|
rules: { |
||||||
|
'react/display-name': 'off', |
||||||
|
}, |
||||||
|
}, |
||||||
|
prettierConfig, |
||||||
|
]); |
@ -0,0 +1,8 @@ |
|||||||
|
import { registerRootComponent } from 'expo'; |
||||||
|
|
||||||
|
import App from './App'; |
||||||
|
|
||||||
|
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
||||||
|
// It also ensures that whether you load the app in Expo Go or in a native build,
|
||||||
|
// the environment is set up appropriately
|
||||||
|
registerRootComponent(App); |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@ |
|||||||
|
{ |
||||||
|
"name": "my-expo-app--paper", |
||||||
|
"version": "1.0.0", |
||||||
|
"main": "index.ts", |
||||||
|
"scripts": { |
||||||
|
"start": "expo start", |
||||||
|
"android": "expo start --android", |
||||||
|
"ios": "expo start --ios", |
||||||
|
"web": "expo start --web" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"expo": "~54.0.9", |
||||||
|
"expo-status-bar": "~3.0.8", |
||||||
|
"react": "19.1.0", |
||||||
|
"react-native": "0.81.4", |
||||||
|
"react-native-paper": "^5.14.5", |
||||||
|
"react-native-reanimated": "~4.1.0", |
||||||
|
"react-native-safe-area-context": "~5.6.0" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@types/react": "~19.1.0", |
||||||
|
"eslint": "^9.35.0", |
||||||
|
"eslint-config-expo": "^10.0.0", |
||||||
|
"eslint-config-prettier": "^10.1.8", |
||||||
|
"typescript": "~5.9.2" |
||||||
|
}, |
||||||
|
"private": true |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
import { View } from 'react-native'; |
||||||
|
import { useCounterDispatch, useCounterState } from '../stores/CounterStore'; |
||||||
|
import { styles } from '../src/theme'; |
||||||
|
import { Button, Card, Text } from 'react-native-paper'; |
||||||
|
import { HStack, VStack } from '../components/stack'; |
||||||
|
import { useBounce } from '../src/animation/useBounce'; |
||||||
|
import Animated from 'react-native-reanimated'; |
||||||
|
|
||||||
|
export default function TestPage() { |
||||||
|
const state = useCounterState(); |
||||||
|
const dispatch = useCounterDispatch(); |
||||||
|
|
||||||
|
const { style: bounceStyle, trigger: bounce } = useBounce({ amplitude: 16, duration: 200 }); |
||||||
|
|
||||||
|
return ( |
||||||
|
<View style={styles.flexCenter}> |
||||||
|
<Card mode="outlined" style={{ width: '80%' }}> |
||||||
|
<Card.Title title={`Count: ${state.count}`} /> |
||||||
|
<Card.Content> |
||||||
|
<VStack gap={12}> |
||||||
|
<HStack justify="space-between"> |
||||||
|
<Text variant="bodyMedium" style={{ color: '#0000FF' }}> |
||||||
|
Text1 |
||||||
|
</Text> |
||||||
|
<Text variant="bodyMedium" style={{ color: '#FF0000' }}> |
||||||
|
Text2 |
||||||
|
</Text> |
||||||
|
</HStack> |
||||||
|
<Animated.View style={bounceStyle}> |
||||||
|
<Button |
||||||
|
mode="outlined" |
||||||
|
onPress={() => { |
||||||
|
bounce(); |
||||||
|
}}> |
||||||
|
🍞Action! |
||||||
|
</Button> |
||||||
|
</Animated.View> |
||||||
|
<HStack justify="space-between"> |
||||||
|
<Button mode="contained" onPress={() => dispatch({ type: 'INCREMENT' })}> |
||||||
|
+ |
||||||
|
</Button> |
||||||
|
<Button mode="contained" onPress={() => dispatch({ type: 'DECREMENT' })}> |
||||||
|
- |
||||||
|
</Button> |
||||||
|
</HStack> |
||||||
|
</VStack> |
||||||
|
</Card.Content> |
||||||
|
</Card> |
||||||
|
</View> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
module.exports = { |
||||||
|
printWidth: 100, |
||||||
|
tabWidth: 2, |
||||||
|
singleQuote: true, |
||||||
|
bracketSameLine: true, |
||||||
|
trailingComma: 'es5', |
||||||
|
semi: true, |
||||||
|
arrowParens: 'always', |
||||||
|
endOfLine: 'auto', |
||||||
|
}; |
@ -0,0 +1,37 @@ |
|||||||
|
import { |
||||||
|
useSharedValue, |
||||||
|
useAnimatedStyle, |
||||||
|
withSequence, |
||||||
|
withTiming, |
||||||
|
} from 'react-native-reanimated'; |
||||||
|
|
||||||
|
type UseBounceOptions = { |
||||||
|
amplitude?: number; // 최대 위/아래 이동량(px)
|
||||||
|
duration?: number; // 전체 지속시간(ms)
|
||||||
|
}; |
||||||
|
|
||||||
|
export function useBounce({ amplitude = 16, duration = 200 }: UseBounceOptions = {}) { |
||||||
|
const y = useSharedValue(0); |
||||||
|
|
||||||
|
const style = useAnimatedStyle( |
||||||
|
() => ({ |
||||||
|
transform: [{ translateY: y.value }], |
||||||
|
}), |
||||||
|
[] |
||||||
|
); |
||||||
|
|
||||||
|
const trigger = () => { |
||||||
|
'worklet'; |
||||||
|
const d = duration; |
||||||
|
const A = amplitude; |
||||||
|
y.value = 0; |
||||||
|
y.value = withSequence( |
||||||
|
withTiming(-A, { duration: d * 0.3 }), |
||||||
|
withTiming(A * 0.25, { duration: d * 0.3 }), |
||||||
|
withTiming(-A * 0.5, { duration: d * 0.2 }), |
||||||
|
withTiming(0, { duration: d * 0.2 }) |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
return { style, trigger, y }; |
||||||
|
} |
@ -0,0 +1,71 @@ |
|||||||
|
import { MD3LightTheme, MD3Theme } from 'react-native-paper'; |
||||||
|
import { StyleSheet } from 'react-native'; |
||||||
|
|
||||||
|
export const spacing = { xs: 6, sm: 8, md: 12, lg: 16, xl: 20 } as const; |
||||||
|
export const radius = { sm: 2, md: 6, lg: 12 } as const; |
||||||
|
|
||||||
|
export const appTheme: MD3Theme = { |
||||||
|
...MD3LightTheme, |
||||||
|
roundness: radius.sm, |
||||||
|
colors: { |
||||||
|
primary: 'rgb(52, 92, 168)', |
||||||
|
onPrimary: 'rgb(255, 255, 255)', |
||||||
|
primaryContainer: 'rgb(217, 226, 255)', |
||||||
|
onPrimaryContainer: 'rgb(0, 26, 67)', |
||||||
|
secondary: 'rgb(87, 94, 113)', |
||||||
|
onSecondary: 'rgb(255, 255, 255)', |
||||||
|
secondaryContainer: 'rgb(219, 226, 249)', |
||||||
|
onSecondaryContainer: 'rgb(20, 27, 44)', |
||||||
|
tertiary: 'rgb(114, 85, 115)', |
||||||
|
onTertiary: 'rgb(255, 255, 255)', |
||||||
|
tertiaryContainer: 'rgb(252, 215, 251)', |
||||||
|
onTertiaryContainer: 'rgb(42, 19, 45)', |
||||||
|
error: 'rgb(186, 26, 26)', |
||||||
|
onError: 'rgb(255, 255, 255)', |
||||||
|
errorContainer: 'rgb(255, 218, 214)', |
||||||
|
onErrorContainer: 'rgb(65, 0, 2)', |
||||||
|
background: 'rgb(254, 251, 255)', |
||||||
|
onBackground: 'rgb(27, 27, 31)', |
||||||
|
surface: 'rgb(254, 251, 255)', |
||||||
|
onSurface: 'rgb(27, 27, 31)', |
||||||
|
surfaceVariant: 'rgb(225, 226, 236)', |
||||||
|
onSurfaceVariant: 'rgb(68, 71, 79)', |
||||||
|
outline: 'rgb(117, 119, 128)', |
||||||
|
outlineVariant: 'rgb(197, 198, 208)', |
||||||
|
shadow: 'rgb(0, 0, 0)', |
||||||
|
scrim: 'rgb(0, 0, 0)', |
||||||
|
inverseSurface: 'rgb(48, 48, 52)', |
||||||
|
inverseOnSurface: 'rgb(242, 240, 244)', |
||||||
|
inversePrimary: 'rgb(175, 198, 255)', |
||||||
|
elevation: { |
||||||
|
level0: 'transparent', |
||||||
|
level1: 'rgb(244, 243, 251)', |
||||||
|
level2: 'rgb(238, 238, 248)', |
||||||
|
level3: 'rgb(232, 234, 245)', |
||||||
|
level4: 'rgb(230, 232, 245)', |
||||||
|
level5: 'rgb(226, 229, 243)', |
||||||
|
}, |
||||||
|
surfaceDisabled: 'rgba(27, 27, 31, 0.12)', |
||||||
|
onSurfaceDisabled: 'rgba(27, 27, 31, 0.38)', |
||||||
|
backdrop: 'rgba(46, 48, 56, 0.4)', |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
export const styles = StyleSheet.create({ |
||||||
|
flex: { |
||||||
|
flex: 1, |
||||||
|
padding: spacing.sm, |
||||||
|
}, |
||||||
|
flexCenter: { |
||||||
|
flex: 1, |
||||||
|
justifyContent: 'center', |
||||||
|
alignItems: 'center', |
||||||
|
padding: spacing.sm, |
||||||
|
}, |
||||||
|
flexJustifyBetween: { |
||||||
|
flex: 1, |
||||||
|
justifyContent: 'space-between', |
||||||
|
alignItems: 'center', |
||||||
|
padding: spacing.sm, |
||||||
|
}, |
||||||
|
}); |
@ -0,0 +1,55 @@ |
|||||||
|
import { |
||||||
|
createContext, |
||||||
|
Dispatch, |
||||||
|
ReactNode, |
||||||
|
useContext, |
||||||
|
useReducer, |
||||||
|
} from "react"; |
||||||
|
|
||||||
|
type State = { count: number }; |
||||||
|
type Action = { type: "INCREMENT" | "DECREMENT" }; |
||||||
|
|
||||||
|
const initialState: State = { count: 0 }; |
||||||
|
|
||||||
|
function counterReducer(state: State, action: Action): State { |
||||||
|
switch (action.type) { |
||||||
|
case "INCREMENT": |
||||||
|
return { count: state.count + 1 }; |
||||||
|
case "DECREMENT": |
||||||
|
return { count: state.count - 1 }; |
||||||
|
default: |
||||||
|
throw new Error("Unhandled action"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const CounterStateContext = createContext<State | undefined>(undefined); |
||||||
|
const CounterDispatchContext = createContext<Dispatch<Action> | undefined>( |
||||||
|
undefined |
||||||
|
); |
||||||
|
|
||||||
|
export function CounterProvider({ children }: { children: ReactNode }) { |
||||||
|
const [state, dispatch] = useReducer(counterReducer, initialState); |
||||||
|
return ( |
||||||
|
<CounterStateContext.Provider value={state}> |
||||||
|
<CounterDispatchContext.Provider value={dispatch}> |
||||||
|
{children} |
||||||
|
</CounterDispatchContext.Provider> |
||||||
|
</CounterStateContext.Provider> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function useCounterState() { |
||||||
|
const context = useContext(CounterStateContext); |
||||||
|
if (context === undefined) |
||||||
|
throw new Error("useCounterState must be used within a CounterProvider"); |
||||||
|
|
||||||
|
return context; |
||||||
|
} |
||||||
|
|
||||||
|
export function useCounterDispatch() { |
||||||
|
const context = useContext(CounterDispatchContext); |
||||||
|
if (context === undefined) |
||||||
|
throw new Error("useCounterDispatch must be used within a CounterProvider"); |
||||||
|
|
||||||
|
return context; |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
{ |
||||||
|
"extends": "expo/tsconfig.base", |
||||||
|
"compilerOptions": { |
||||||
|
"strict": true |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue