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