@designbasekorea/ui-native 0.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 +152 -0
- package/dist/components/Alert/Alert.d.ts +29 -0
- package/dist/components/Alert/Alert.d.ts.map +1 -0
- package/dist/components/Alert/Alert.js +44 -0
- package/dist/components/Alert/Alert.js.map +1 -0
- package/dist/components/Alert/index.d.ts +3 -0
- package/dist/components/Alert/index.d.ts.map +1 -0
- package/dist/components/Alert/index.js +2 -0
- package/dist/components/Alert/index.js.map +1 -0
- package/dist/components/Avatar/Avatar.d.ts +28 -0
- package/dist/components/Avatar/Avatar.d.ts.map +1 -0
- package/dist/components/Avatar/Avatar.js +73 -0
- package/dist/components/Avatar/Avatar.js.map +1 -0
- package/dist/components/Avatar/index.d.ts +3 -0
- package/dist/components/Avatar/index.d.ts.map +1 -0
- package/dist/components/Avatar/index.js +2 -0
- package/dist/components/Avatar/index.js.map +1 -0
- package/dist/components/Badge/Badge.d.ts +28 -0
- package/dist/components/Badge/Badge.d.ts.map +1 -0
- package/dist/components/Badge/Badge.js +52 -0
- package/dist/components/Badge/Badge.js.map +1 -0
- package/dist/components/Badge/index.d.ts +3 -0
- package/dist/components/Badge/index.d.ts.map +1 -0
- package/dist/components/Badge/index.js +2 -0
- package/dist/components/Badge/index.js.map +1 -0
- package/dist/components/BottomNavigation/BottomNavigation.d.ts +30 -0
- package/dist/components/BottomNavigation/BottomNavigation.d.ts.map +1 -0
- package/dist/components/BottomNavigation/BottomNavigation.js +48 -0
- package/dist/components/BottomNavigation/BottomNavigation.js.map +1 -0
- package/dist/components/BottomNavigation/index.d.ts +3 -0
- package/dist/components/BottomNavigation/index.d.ts.map +1 -0
- package/dist/components/BottomNavigation/index.js +2 -0
- package/dist/components/BottomNavigation/index.js.map +1 -0
- package/dist/components/BottomSheet/BottomSheet.d.ts +26 -0
- package/dist/components/BottomSheet/BottomSheet.d.ts.map +1 -0
- package/dist/components/BottomSheet/BottomSheet.js +75 -0
- package/dist/components/BottomSheet/BottomSheet.js.map +1 -0
- package/dist/components/BottomSheet/index.d.ts +3 -0
- package/dist/components/BottomSheet/index.d.ts.map +1 -0
- package/dist/components/BottomSheet/index.js +2 -0
- package/dist/components/BottomSheet/index.js.map +1 -0
- package/dist/components/Button/Button.d.ts +53 -0
- package/dist/components/Button/Button.d.ts.map +1 -0
- package/dist/components/Button/Button.js +129 -0
- package/dist/components/Button/Button.js.map +1 -0
- package/dist/components/Button/index.d.ts +3 -0
- package/dist/components/Button/index.d.ts.map +1 -0
- package/dist/components/Button/index.js +2 -0
- package/dist/components/Button/index.js.map +1 -0
- package/dist/components/Card/Card.d.ts +36 -0
- package/dist/components/Card/Card.d.ts.map +1 -0
- package/dist/components/Card/Card.js +61 -0
- package/dist/components/Card/Card.js.map +1 -0
- package/dist/components/Card/index.d.ts +3 -0
- package/dist/components/Card/index.d.ts.map +1 -0
- package/dist/components/Card/index.js +2 -0
- package/dist/components/Card/index.js.map +1 -0
- package/dist/components/Checkbox/Checkbox.d.ts +26 -0
- package/dist/components/Checkbox/Checkbox.d.ts.map +1 -0
- package/dist/components/Checkbox/Checkbox.js +57 -0
- package/dist/components/Checkbox/Checkbox.js.map +1 -0
- package/dist/components/Checkbox/index.d.ts +3 -0
- package/dist/components/Checkbox/index.d.ts.map +1 -0
- package/dist/components/Checkbox/index.js +2 -0
- package/dist/components/Checkbox/index.js.map +1 -0
- package/dist/components/Chip/Chip.d.ts +29 -0
- package/dist/components/Chip/Chip.d.ts.map +1 -0
- package/dist/components/Chip/Chip.js +54 -0
- package/dist/components/Chip/Chip.js.map +1 -0
- package/dist/components/Chip/index.d.ts +3 -0
- package/dist/components/Chip/index.d.ts.map +1 -0
- package/dist/components/Chip/index.js +2 -0
- package/dist/components/Chip/index.js.map +1 -0
- package/dist/components/Divider/Divider.d.ts +22 -0
- package/dist/components/Divider/Divider.d.ts.map +1 -0
- package/dist/components/Divider/Divider.js +16 -0
- package/dist/components/Divider/Divider.js.map +1 -0
- package/dist/components/Divider/index.d.ts +3 -0
- package/dist/components/Divider/index.d.ts.map +1 -0
- package/dist/components/Divider/index.js +2 -0
- package/dist/components/Divider/index.js.map +1 -0
- package/dist/components/Input/Input.d.ts +33 -0
- package/dist/components/Input/Input.d.ts.map +1 -0
- package/dist/components/Input/Input.js +98 -0
- package/dist/components/Input/Input.js.map +1 -0
- package/dist/components/Input/index.d.ts +3 -0
- package/dist/components/Input/index.d.ts.map +1 -0
- package/dist/components/Input/index.js +2 -0
- package/dist/components/Input/index.js.map +1 -0
- package/dist/components/Modal/Modal.d.ts +28 -0
- package/dist/components/Modal/Modal.d.ts.map +1 -0
- package/dist/components/Modal/Modal.js +46 -0
- package/dist/components/Modal/Modal.js.map +1 -0
- package/dist/components/Modal/index.d.ts +3 -0
- package/dist/components/Modal/index.d.ts.map +1 -0
- package/dist/components/Modal/index.js +2 -0
- package/dist/components/Modal/index.js.map +1 -0
- package/dist/components/Navbar/Navbar.d.ts +50 -0
- package/dist/components/Navbar/Navbar.d.ts.map +1 -0
- package/dist/components/Navbar/Navbar.js +125 -0
- package/dist/components/Navbar/Navbar.js.map +1 -0
- package/dist/components/Navbar/index.d.ts +3 -0
- package/dist/components/Navbar/index.d.ts.map +1 -0
- package/dist/components/Navbar/index.js +2 -0
- package/dist/components/Navbar/index.js.map +1 -0
- package/dist/components/ProgressBar/ProgressBar.d.ts +27 -0
- package/dist/components/ProgressBar/ProgressBar.d.ts.map +1 -0
- package/dist/components/ProgressBar/ProgressBar.js +55 -0
- package/dist/components/ProgressBar/ProgressBar.js.map +1 -0
- package/dist/components/ProgressBar/index.d.ts +3 -0
- package/dist/components/ProgressBar/index.d.ts.map +1 -0
- package/dist/components/ProgressBar/index.js +2 -0
- package/dist/components/ProgressBar/index.js.map +1 -0
- package/dist/components/Radio/Radio.d.ts +30 -0
- package/dist/components/Radio/Radio.d.ts.map +1 -0
- package/dist/components/Radio/Radio.js +56 -0
- package/dist/components/Radio/Radio.js.map +1 -0
- package/dist/components/Radio/index.d.ts +3 -0
- package/dist/components/Radio/index.d.ts.map +1 -0
- package/dist/components/Radio/index.js +2 -0
- package/dist/components/Radio/index.js.map +1 -0
- package/dist/components/SearchBar/SearchBar.d.ts +29 -0
- package/dist/components/SearchBar/SearchBar.d.ts.map +1 -0
- package/dist/components/SearchBar/SearchBar.js +64 -0
- package/dist/components/SearchBar/SearchBar.js.map +1 -0
- package/dist/components/SearchBar/index.d.ts +3 -0
- package/dist/components/SearchBar/index.d.ts.map +1 -0
- package/dist/components/SearchBar/index.js +2 -0
- package/dist/components/SearchBar/index.js.map +1 -0
- package/dist/components/Skeleton/Skeleton.d.ts +22 -0
- package/dist/components/Skeleton/Skeleton.d.ts.map +1 -0
- package/dist/components/Skeleton/Skeleton.js +46 -0
- package/dist/components/Skeleton/Skeleton.js.map +1 -0
- package/dist/components/Skeleton/index.d.ts +3 -0
- package/dist/components/Skeleton/index.d.ts.map +1 -0
- package/dist/components/Skeleton/index.js +2 -0
- package/dist/components/Skeleton/index.js.map +1 -0
- package/dist/components/Spinner/Spinner.d.ts +23 -0
- package/dist/components/Spinner/Spinner.d.ts.map +1 -0
- package/dist/components/Spinner/Spinner.js +19 -0
- package/dist/components/Spinner/Spinner.js.map +1 -0
- package/dist/components/Spinner/index.d.ts +3 -0
- package/dist/components/Spinner/index.d.ts.map +1 -0
- package/dist/components/Spinner/index.js +2 -0
- package/dist/components/Spinner/index.js.map +1 -0
- package/dist/components/Tabs/Tabs.d.ts +30 -0
- package/dist/components/Tabs/Tabs.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.js +73 -0
- package/dist/components/Tabs/Tabs.js.map +1 -0
- package/dist/components/Tabs/index.d.ts +3 -0
- package/dist/components/Tabs/index.d.ts.map +1 -0
- package/dist/components/Tabs/index.js +2 -0
- package/dist/components/Tabs/index.js.map +1 -0
- package/dist/components/Toast/Toast.d.ts +30 -0
- package/dist/components/Toast/Toast.d.ts.map +1 -0
- package/dist/components/Toast/Toast.js +84 -0
- package/dist/components/Toast/Toast.js.map +1 -0
- package/dist/components/Toast/index.d.ts +3 -0
- package/dist/components/Toast/index.d.ts.map +1 -0
- package/dist/components/Toast/index.js +2 -0
- package/dist/components/Toast/index.js.map +1 -0
- package/dist/components/Toggle/Toggle.d.ts +26 -0
- package/dist/components/Toggle/Toggle.d.ts.map +1 -0
- package/dist/components/Toggle/Toggle.js +75 -0
- package/dist/components/Toggle/Toggle.js.map +1 -0
- package/dist/components/Toggle/index.d.ts +3 -0
- package/dist/components/Toggle/index.d.ts.map +1 -0
- package/dist/components/Toggle/index.js +2 -0
- package/dist/components/Toggle/index.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/theme/ThemeProvider.d.ts +35 -0
- package/dist/theme/ThemeProvider.d.ts.map +1 -0
- package/dist/theme/ThemeProvider.js +45 -0
- package/dist/theme/ThemeProvider.js.map +1 -0
- package/dist/theme/index.d.ts +6 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +4 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/tokens.d.ts +55 -0
- package/dist/theme/tokens.d.ts.map +1 -0
- package/dist/theme/tokens.js +172 -0
- package/dist/theme/tokens.js.map +1 -0
- package/dist/theme/useTheme.d.ts +25 -0
- package/dist/theme/useTheme.d.ts.map +1 -0
- package/dist/theme/useTheme.js +33 -0
- package/dist/theme/useTheme.js.map +1 -0
- package/package.json +58 -0
- package/src/components/Alert/Alert.tsx +105 -0
- package/src/components/Alert/index.ts +2 -0
- package/src/components/Avatar/Avatar.tsx +122 -0
- package/src/components/Avatar/index.ts +2 -0
- package/src/components/Badge/Badge.tsx +100 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/BottomNavigation/BottomNavigation.tsx +104 -0
- package/src/components/BottomNavigation/index.ts +2 -0
- package/src/components/BottomSheet/BottomSheet.tsx +127 -0
- package/src/components/BottomSheet/index.ts +2 -0
- package/src/components/Button/Button.tsx +255 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Card/Card.tsx +147 -0
- package/src/components/Card/index.ts +2 -0
- package/src/components/Checkbox/Checkbox.tsx +95 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/Chip/Chip.tsx +108 -0
- package/src/components/Chip/index.ts +2 -0
- package/src/components/Divider/Divider.tsx +41 -0
- package/src/components/Divider/index.ts +2 -0
- package/src/components/Input/Input.tsx +199 -0
- package/src/components/Input/index.ts +2 -0
- package/src/components/Modal/Modal.tsx +117 -0
- package/src/components/Modal/index.ts +2 -0
- package/src/components/Navbar/Navbar.tsx +278 -0
- package/src/components/Navbar/index.ts +2 -0
- package/src/components/ProgressBar/ProgressBar.tsx +99 -0
- package/src/components/ProgressBar/index.ts +2 -0
- package/src/components/Radio/Radio.tsx +103 -0
- package/src/components/Radio/index.ts +2 -0
- package/src/components/SearchBar/SearchBar.tsx +115 -0
- package/src/components/SearchBar/index.ts +2 -0
- package/src/components/Skeleton/Skeleton.tsx +74 -0
- package/src/components/Skeleton/index.ts +2 -0
- package/src/components/Spinner/Spinner.tsx +58 -0
- package/src/components/Spinner/index.ts +2 -0
- package/src/components/Tabs/Tabs.tsx +124 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/Toast/Toast.tsx +128 -0
- package/src/components/Toast/index.ts +2 -0
- package/src/components/Toggle/Toggle.tsx +109 -0
- package/src/components/Toggle/index.ts +2 -0
- package/src/index.ts +87 -0
- package/src/theme/ThemeProvider.tsx +96 -0
- package/src/theme/index.ts +5 -0
- package/src/theme/tokens.ts +225 -0
- package/src/theme/useTheme.ts +37 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Avatar 컴포넌트 (React Native)
|
|
3
|
+
*
|
|
4
|
+
* 스타일링 토큰 경로:
|
|
5
|
+
* - 사이즈: theme.size.semantic.avatar.{xs|s|m|l|xl|2xl}
|
|
6
|
+
* - 색상: theme.color.aliases.{surface|text|feedback}.*
|
|
7
|
+
* - 폰트: theme.typography.foundation.fontSize.*
|
|
8
|
+
*
|
|
9
|
+
* 원본: packages/ui/src/components/Avatar/Avatar.tsx
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React, { useState } from 'react';
|
|
13
|
+
import {
|
|
14
|
+
View, Image, Text, Pressable, StyleSheet, type ViewStyle,
|
|
15
|
+
} from 'react-native';
|
|
16
|
+
import { useTheme } from '../../theme/useTheme';
|
|
17
|
+
|
|
18
|
+
export type AvatarSize = 'xs' | 's' | 'm' | 'l' | 'xl' | '2xl';
|
|
19
|
+
export type AvatarStatus = 'online' | 'offline' | 'away' | 'busy';
|
|
20
|
+
|
|
21
|
+
export interface AvatarProps {
|
|
22
|
+
src?: string;
|
|
23
|
+
alt?: string;
|
|
24
|
+
initials?: string;
|
|
25
|
+
icon?: React.ReactNode;
|
|
26
|
+
size?: AvatarSize;
|
|
27
|
+
status?: AvatarStatus;
|
|
28
|
+
onPress?: () => void;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
style?: ViewStyle;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const INITIALS_COLORS = [
|
|
34
|
+
{ bg: '#E3F2FD', text: '#1976D2' }, { bg: '#F3E5F5', text: '#7B1FA2' },
|
|
35
|
+
{ bg: '#E8F5E8', text: '#388E3C' }, { bg: '#FFF3E0', text: '#F57C00' },
|
|
36
|
+
{ bg: '#FCE4EC', text: '#C2185B' }, { bg: '#E0F2F1', text: '#00796B' },
|
|
37
|
+
{ bg: '#FFF8E1', text: '#F9A825' }, { bg: '#F1F8E9', text: '#689F38' },
|
|
38
|
+
{ bg: '#E8EAF6', text: '#3F51B5' }, { bg: '#FFEBEE', text: '#D32F2F' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function getColorForText(text: string) {
|
|
42
|
+
let hash = 0;
|
|
43
|
+
for (let i = 0; i < text.length; i++) hash = text.charCodeAt(i) + ((hash << 5) - hash);
|
|
44
|
+
return INITIALS_COLORS[Math.abs(hash) % INITIALS_COLORS.length];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Avatar: React.FC<AvatarProps> = ({
|
|
48
|
+
src, alt, initials, icon, size = 'm', status, onPress, disabled = false, style,
|
|
49
|
+
}) => {
|
|
50
|
+
const { theme } = useTheme();
|
|
51
|
+
const [imageError, setImageError] = useState(false);
|
|
52
|
+
|
|
53
|
+
// ─── 토큰에서 가져오기 ───
|
|
54
|
+
const avatarSizeTokens = theme.size.semantic?.avatar ?? {};
|
|
55
|
+
const aliases = theme.color.aliases ?? {};
|
|
56
|
+
const feedback = aliases.feedback ?? {};
|
|
57
|
+
const disabledOpacity = theme.opacity.foundation?.[40] ?? 0.4;
|
|
58
|
+
|
|
59
|
+
const avatarSize = avatarSizeTokens[size] ?? { xs: 16, s: 20, m: 24, l: 32, xl: 40, '2xl': 48 }[size];
|
|
60
|
+
const fontSizeMap: Record<AvatarSize, number> = { xs: 8, s: 10, m: 12, l: 16, xl: 20, '2xl': 26 };
|
|
61
|
+
const statusSizeMap: Record<AvatarSize, number> = { xs: 6, s: 8, m: 10, l: 12, xl: 14, '2xl': 16 };
|
|
62
|
+
|
|
63
|
+
const statusColors: Record<AvatarStatus, string> = {
|
|
64
|
+
online: feedback.success?.fg ?? '#16A34A',
|
|
65
|
+
offline: aliases.text?.tertiary ?? '#757B80',
|
|
66
|
+
away: feedback.warning?.fg ?? '#D1B400',
|
|
67
|
+
busy: feedback.error?.fg ?? '#DC2626',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const shouldShowImage = src && !imageError;
|
|
71
|
+
const shouldShowInitials = !shouldShowImage && initials;
|
|
72
|
+
const initialsColor = shouldShowInitials ? getColorForText(initials!) : null;
|
|
73
|
+
|
|
74
|
+
const content = (
|
|
75
|
+
<View style={[
|
|
76
|
+
styles.container,
|
|
77
|
+
{
|
|
78
|
+
width: avatarSize, height: avatarSize, borderRadius: avatarSize / 2,
|
|
79
|
+
backgroundColor: shouldShowInitials ? initialsColor?.bg : (aliases.surface?.['layer-2'] ?? '#E8EEF2'),
|
|
80
|
+
},
|
|
81
|
+
disabled && { opacity: disabledOpacity }, style,
|
|
82
|
+
]} accessibilityRole="image" accessibilityLabel={alt || initials || '아바타'}>
|
|
83
|
+
{shouldShowImage && (
|
|
84
|
+
<Image source={{ uri: src }} style={[styles.image, { width: avatarSize, height: avatarSize, borderRadius: avatarSize / 2 }]} onError={() => setImageError(true)} />
|
|
85
|
+
)}
|
|
86
|
+
{shouldShowInitials && (
|
|
87
|
+
<Text style={{ fontSize: fontSizeMap[size], color: initialsColor?.text, fontWeight: '600' }}>
|
|
88
|
+
{initials!.charAt(0).toUpperCase()}
|
|
89
|
+
</Text>
|
|
90
|
+
)}
|
|
91
|
+
{!shouldShowImage && !shouldShowInitials && icon}
|
|
92
|
+
{status && (
|
|
93
|
+
<View style={[styles.status, {
|
|
94
|
+
width: statusSizeMap[size], height: statusSizeMap[size],
|
|
95
|
+
borderRadius: statusSizeMap[size] / 2,
|
|
96
|
+
backgroundColor: statusColors[status],
|
|
97
|
+
borderColor: aliases.surface?.base ?? '#FFFFFF', borderWidth: 2,
|
|
98
|
+
}]} />
|
|
99
|
+
)}
|
|
100
|
+
</View>
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (onPress) {
|
|
104
|
+
return (
|
|
105
|
+
<Pressable onPress={onPress} disabled={disabled} accessibilityRole="button"
|
|
106
|
+
style={({ pressed }) => pressed ? { opacity: 0.7 } : undefined}>
|
|
107
|
+
{content}
|
|
108
|
+
</Pressable>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return content;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
Avatar.displayName = 'Avatar';
|
|
115
|
+
|
|
116
|
+
const styles = StyleSheet.create({
|
|
117
|
+
container: { alignItems: 'center', justifyContent: 'center', overflow: 'hidden' },
|
|
118
|
+
image: { position: 'absolute' },
|
|
119
|
+
status: { position: 'absolute', bottom: 0, right: 0 },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export default Avatar;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Badge 컴포넌트 (React Native)
|
|
3
|
+
*
|
|
4
|
+
* 스타일링 토큰 경로:
|
|
5
|
+
* - 색상: theme.color.aliases.{brand|feedback}.*, theme.color.aliases.text.*
|
|
6
|
+
* - 폰트: theme.typography.foundation.fontSize.{xs|s}
|
|
7
|
+
* - 간격: theme.spacing.aliases.padding.{xxs|xs|s}
|
|
8
|
+
*
|
|
9
|
+
* 원본: packages/ui/src/components/Badge/Badge.tsx
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { View, Text, StyleSheet, type ViewStyle } from 'react-native';
|
|
14
|
+
import { useTheme } from '../../theme/useTheme';
|
|
15
|
+
|
|
16
|
+
export type BadgeSize = 's' | 'm' | 'l';
|
|
17
|
+
export type BadgeVariant = 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'danger';
|
|
18
|
+
export type BadgeType = 'dot' | 'number' | 'text' | 'outlined';
|
|
19
|
+
|
|
20
|
+
export interface BadgeProps {
|
|
21
|
+
children?: React.ReactNode;
|
|
22
|
+
size?: BadgeSize;
|
|
23
|
+
variant?: BadgeVariant;
|
|
24
|
+
type?: BadgeType;
|
|
25
|
+
count?: number;
|
|
26
|
+
maxCount?: number;
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
style?: ViewStyle;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const Badge: React.FC<BadgeProps> = ({
|
|
32
|
+
children, size = 'm', variant = 'primary', type = 'text',
|
|
33
|
+
count, maxCount = 99, disabled = false, style,
|
|
34
|
+
}) => {
|
|
35
|
+
const { theme } = useTheme();
|
|
36
|
+
|
|
37
|
+
const aliases = theme.color.aliases ?? {};
|
|
38
|
+
const feedback = aliases.feedback ?? {};
|
|
39
|
+
const padding = theme.spacing.aliases?.padding ?? {};
|
|
40
|
+
const fontSize = theme.typography.foundation?.fontSize ?? {};
|
|
41
|
+
const disabledOpacity = theme.opacity.foundation?.[40] ?? 0.4;
|
|
42
|
+
|
|
43
|
+
const sizeConfig = {
|
|
44
|
+
s: { minWidth: 16, height: 16, fontSize: fontSize.xs ?? 12 - 2, paddingH: padding.xxs ?? 2, dotSize: 6 },
|
|
45
|
+
m: { minWidth: 20, height: 20, fontSize: fontSize.xs ?? 12 - 1, paddingH: padding.xs ?? 4, dotSize: 8 },
|
|
46
|
+
l: { minWidth: 24, height: 24, fontSize: fontSize.xs ?? 12, paddingH: padding.s ?? 8, dotSize: 10 },
|
|
47
|
+
}[size];
|
|
48
|
+
|
|
49
|
+
const variantColors = {
|
|
50
|
+
primary: { bg: aliases.brand?.primary ?? '#006FFF', text: '#FFFFFF' },
|
|
51
|
+
secondary: { bg: aliases.surface?.['layer-2'] ?? '#E8EEF2', text: aliases.text?.secondary ?? '#464A4D' },
|
|
52
|
+
info: { bg: feedback.info?.bg ?? '#EDF9FF', text: feedback.info?.fg ?? '#0683FF' },
|
|
53
|
+
success: { bg: feedback.success?.bg ?? '#F0FDF5', text: feedback.success?.fg ?? '#16A34A' },
|
|
54
|
+
warning: { bg: feedback.warning?.bg ?? '#FFFFE7', text: feedback.warning?.fg ?? '#D1B400' },
|
|
55
|
+
danger: { bg: feedback.error?.bg ?? '#FEF2F2', text: feedback.error?.fg ?? '#DC2626' },
|
|
56
|
+
}[variant];
|
|
57
|
+
|
|
58
|
+
if (type === 'dot') {
|
|
59
|
+
return (
|
|
60
|
+
<View style={[
|
|
61
|
+
{ width: sizeConfig.dotSize, height: sizeConfig.dotSize, borderRadius: sizeConfig.dotSize / 2, backgroundColor: variantColors.bg },
|
|
62
|
+
disabled && { opacity: disabledOpacity }, style,
|
|
63
|
+
]} />
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const displayContent = type === 'number' && count !== undefined
|
|
68
|
+
? (count > maxCount ? `${maxCount}+` : String(count))
|
|
69
|
+
: children;
|
|
70
|
+
const isOutlined = type === 'outlined';
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<View style={[
|
|
74
|
+
styles.badge,
|
|
75
|
+
{
|
|
76
|
+
minWidth: sizeConfig.minWidth, height: sizeConfig.height,
|
|
77
|
+
borderRadius: sizeConfig.height / 2, paddingHorizontal: sizeConfig.paddingH,
|
|
78
|
+
backgroundColor: isOutlined ? 'transparent' : variantColors.bg,
|
|
79
|
+
borderWidth: isOutlined ? 1 : 0,
|
|
80
|
+
borderColor: isOutlined ? variantColors.bg : undefined,
|
|
81
|
+
},
|
|
82
|
+
disabled && { opacity: disabledOpacity }, style,
|
|
83
|
+
]} accessibilityRole="text">
|
|
84
|
+
{typeof displayContent === 'string' || typeof displayContent === 'number' ? (
|
|
85
|
+
<Text style={[styles.text, { fontSize: sizeConfig.fontSize, color: isOutlined ? variantColors.bg : variantColors.text }]}>
|
|
86
|
+
{displayContent}
|
|
87
|
+
</Text>
|
|
88
|
+
) : displayContent}
|
|
89
|
+
</View>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
Badge.displayName = 'Badge';
|
|
94
|
+
|
|
95
|
+
const styles = StyleSheet.create({
|
|
96
|
+
badge: { alignItems: 'center', justifyContent: 'center', alignSelf: 'flex-start' },
|
|
97
|
+
text: { fontWeight: '600', textAlign: 'center' },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export default Badge;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BottomNavigation 컴포넌트 (React Native)
|
|
3
|
+
*
|
|
4
|
+
* 스타일링 토큰 경로:
|
|
5
|
+
* - 배경: theme.color.aliases.surface.base
|
|
6
|
+
* - 보더: theme.color.aliases.border.base
|
|
7
|
+
* - 활성: theme.color.aliases.brand.primary
|
|
8
|
+
* - 비활성: theme.color.aliases.text.tertiary
|
|
9
|
+
* - 폰트: theme.typography.foundation.fontSize.xs
|
|
10
|
+
* - 그림자: theme.shadow.foundation.m
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { View, Text, Pressable, StyleSheet, type ViewStyle } from 'react-native';
|
|
15
|
+
import { useTheme } from '../../theme/useTheme';
|
|
16
|
+
|
|
17
|
+
export interface BottomNavItem {
|
|
18
|
+
key: string;
|
|
19
|
+
label: string;
|
|
20
|
+
icon: React.ReactNode;
|
|
21
|
+
activeIcon?: React.ReactNode;
|
|
22
|
+
badge?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BottomNavigationProps {
|
|
26
|
+
items: BottomNavItem[];
|
|
27
|
+
activeKey: string;
|
|
28
|
+
onChange: (key: string) => void;
|
|
29
|
+
showLabels?: boolean;
|
|
30
|
+
style?: ViewStyle;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const BottomNavigation: React.FC<BottomNavigationProps> = ({
|
|
34
|
+
items, activeKey, onChange, showLabels = true, style,
|
|
35
|
+
}) => {
|
|
36
|
+
const { theme } = useTheme();
|
|
37
|
+
|
|
38
|
+
const surface = theme.color.aliases?.surface ?? {};
|
|
39
|
+
const brand = theme.color.aliases?.brand ?? {};
|
|
40
|
+
const textColors = theme.color.aliases?.text ?? {};
|
|
41
|
+
const borders = theme.color.aliases?.border ?? {};
|
|
42
|
+
const fontSize = theme.typography.foundation?.fontSize ?? {};
|
|
43
|
+
const shadow = theme.shadow.foundation ?? {};
|
|
44
|
+
const padding = theme.spacing.aliases?.padding ?? {};
|
|
45
|
+
|
|
46
|
+
const toShadowStyle = (s: any) => s ? {
|
|
47
|
+
shadowColor: s.shadowColor ?? '#000',
|
|
48
|
+
shadowOffset: { width: s.shadowOffsetX ?? 0, height: -(s.shadowOffsetY ?? 4) },
|
|
49
|
+
shadowOpacity: s.shadowOpacity ?? 0.1,
|
|
50
|
+
shadowRadius: s.shadowRadius ?? 6,
|
|
51
|
+
elevation: s.elevation ?? 8,
|
|
52
|
+
} : {};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<View style={[
|
|
56
|
+
styles.container,
|
|
57
|
+
{
|
|
58
|
+
backgroundColor: surface.base ?? '#FFFFFF',
|
|
59
|
+
borderTopColor: borders.base ?? '#E8EEF2',
|
|
60
|
+
paddingBottom: padding.xs ?? 4,
|
|
61
|
+
...toShadowStyle(shadow.m),
|
|
62
|
+
},
|
|
63
|
+
style,
|
|
64
|
+
]}>
|
|
65
|
+
{items.map((item) => {
|
|
66
|
+
const isActive = activeKey === item.key;
|
|
67
|
+
const activeColor = brand.primary ?? '#006FFF';
|
|
68
|
+
const inactiveColor = textColors.tertiary ?? '#757B80';
|
|
69
|
+
return (
|
|
70
|
+
<Pressable key={item.key} onPress={() => onChange(item.key)}
|
|
71
|
+
accessibilityRole="tab" accessibilityState={{ selected: isActive }}
|
|
72
|
+
style={[styles.item, { paddingTop: padding.s ?? 8 }]}>
|
|
73
|
+
<View>
|
|
74
|
+
{isActive ? (item.activeIcon ?? item.icon) : item.icon}
|
|
75
|
+
{item.badge !== undefined && item.badge > 0 && (
|
|
76
|
+
<View style={[styles.badge, { backgroundColor: theme.color.aliases?.feedback?.error?.fg ?? '#DC2626' }]}>
|
|
77
|
+
<Text style={styles.badgeText}>{item.badge > 99 ? '99+' : item.badge}</Text>
|
|
78
|
+
</View>
|
|
79
|
+
)}
|
|
80
|
+
</View>
|
|
81
|
+
{showLabels && (
|
|
82
|
+
<Text style={{
|
|
83
|
+
fontSize: fontSize.xs ?? 12 - 2,
|
|
84
|
+
color: isActive ? activeColor : inactiveColor,
|
|
85
|
+
fontWeight: isActive ? '600' : '400', marginTop: 2,
|
|
86
|
+
}}>{item.label}</Text>
|
|
87
|
+
)}
|
|
88
|
+
</Pressable>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
91
|
+
</View>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
BottomNavigation.displayName = 'BottomNavigation';
|
|
96
|
+
|
|
97
|
+
const styles = StyleSheet.create({
|
|
98
|
+
container: { flexDirection: 'row', borderTopWidth: 1 },
|
|
99
|
+
item: { flex: 1, alignItems: 'center', justifyContent: 'center' },
|
|
100
|
+
badge: { position: 'absolute', top: -4, right: -8, minWidth: 16, height: 16, borderRadius: 8, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 3 },
|
|
101
|
+
badgeText: { color: '#FFFFFF', fontSize: 9, fontWeight: '700' },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export default BottomNavigation;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BottomSheet 컴포넌트 (React Native)
|
|
3
|
+
*
|
|
4
|
+
* 스타일링 토큰 경로:
|
|
5
|
+
* - 배경: theme.color.aliases.surface.base
|
|
6
|
+
* - 오버레이: theme.color.aliases.overlay.{base|soft}
|
|
7
|
+
* - 라운드: theme.border.semantic.radius.xl (상단만)
|
|
8
|
+
* - 패딩: theme.spacing.aliases.padding.{m|l}
|
|
9
|
+
* - 그림자: theme.shadow.foundation.xl
|
|
10
|
+
* - 핸들: theme.color.aliases.border.layer-1
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React, { useRef, useEffect } from 'react';
|
|
14
|
+
import {
|
|
15
|
+
Modal, View, Text, Pressable, Animated, PanResponder,
|
|
16
|
+
Dimensions, StyleSheet, type ViewStyle,
|
|
17
|
+
} from 'react-native';
|
|
18
|
+
import { useTheme } from '../../theme/useTheme';
|
|
19
|
+
|
|
20
|
+
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
|
21
|
+
|
|
22
|
+
export interface BottomSheetProps {
|
|
23
|
+
visible: boolean;
|
|
24
|
+
onClose: () => void;
|
|
25
|
+
title?: string;
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
height?: number | string;
|
|
28
|
+
closable?: boolean;
|
|
29
|
+
showHandle?: boolean;
|
|
30
|
+
style?: ViewStyle;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const BottomSheet: React.FC<BottomSheetProps> = ({
|
|
34
|
+
visible, onClose, title, children, height = '50%',
|
|
35
|
+
closable = true, showHandle = true, style,
|
|
36
|
+
}) => {
|
|
37
|
+
const { theme } = useTheme();
|
|
38
|
+
const translateY = useRef(new Animated.Value(SCREEN_HEIGHT)).current;
|
|
39
|
+
|
|
40
|
+
const surface = theme.color.aliases?.surface ?? {};
|
|
41
|
+
const text = theme.color.aliases?.text ?? {};
|
|
42
|
+
const overlay = theme.color.aliases?.overlay ?? {};
|
|
43
|
+
const borders = theme.color.aliases?.border ?? {};
|
|
44
|
+
const padding = theme.spacing.aliases?.padding ?? {};
|
|
45
|
+
const fontSize = theme.typography.foundation?.fontSize ?? {};
|
|
46
|
+
const radiusXl = theme.border.semantic?.radius?.xl ?? 16;
|
|
47
|
+
const gap = theme.spacing.aliases?.gap ?? {};
|
|
48
|
+
|
|
49
|
+
const sheetHeight = typeof height === 'string' ? SCREEN_HEIGHT * (parseInt(height) / 100) : height;
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (visible) {
|
|
53
|
+
Animated.spring(translateY, { toValue: 0, useNativeDriver: true, friction: 9, tension: 65 }).start();
|
|
54
|
+
} else {
|
|
55
|
+
Animated.timing(translateY, { toValue: SCREEN_HEIGHT, duration: 250, useNativeDriver: true }).start();
|
|
56
|
+
}
|
|
57
|
+
}, [visible, translateY]);
|
|
58
|
+
|
|
59
|
+
const panResponder = useRef(
|
|
60
|
+
PanResponder.create({
|
|
61
|
+
onStartShouldSetPanResponder: () => closable,
|
|
62
|
+
onMoveShouldSetPanResponder: (_, g) => closable && g.dy > 5,
|
|
63
|
+
onPanResponderMove: (_, g) => { if (g.dy > 0) translateY.setValue(g.dy); },
|
|
64
|
+
onPanResponderRelease: (_, g) => {
|
|
65
|
+
if (g.dy > sheetHeight * 0.3 || g.vy > 0.5) {
|
|
66
|
+
Animated.timing(translateY, { toValue: SCREEN_HEIGHT, duration: 200, useNativeDriver: true }).start(onClose);
|
|
67
|
+
} else {
|
|
68
|
+
Animated.spring(translateY, { toValue: 0, useNativeDriver: true }).start();
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
).current;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Modal visible={visible} transparent animationType="none" onRequestClose={closable ? onClose : undefined}>
|
|
76
|
+
<View style={styles.overlay}>
|
|
77
|
+
<Pressable style={[styles.backdrop, { backgroundColor: overlay.base ?? 'rgba(0,0,0,0.6)' }]}
|
|
78
|
+
onPress={closable ? onClose : undefined} />
|
|
79
|
+
<Animated.View
|
|
80
|
+
{...panResponder.panHandlers}
|
|
81
|
+
style={[
|
|
82
|
+
styles.sheet,
|
|
83
|
+
{
|
|
84
|
+
height: sheetHeight,
|
|
85
|
+
backgroundColor: surface.base ?? '#FFFFFF',
|
|
86
|
+
borderTopLeftRadius: radiusXl,
|
|
87
|
+
borderTopRightRadius: radiusXl,
|
|
88
|
+
transform: [{ translateY }],
|
|
89
|
+
},
|
|
90
|
+
style,
|
|
91
|
+
]}>
|
|
92
|
+
{showHandle && (
|
|
93
|
+
<View style={styles.handleContainer}>
|
|
94
|
+
<View style={[styles.handle, { backgroundColor: borders['layer-1'] ?? '#BBC5CC' }]} />
|
|
95
|
+
</View>
|
|
96
|
+
)}
|
|
97
|
+
{(title || closable) && (
|
|
98
|
+
<View style={[styles.header, { paddingHorizontal: padding.l ?? 20, paddingTop: padding.s ?? 8 }]}>
|
|
99
|
+
{title && <Text style={[styles.title, { color: text.primary ?? '#17191A', fontSize: fontSize.l ?? 18 }]}>{title}</Text>}
|
|
100
|
+
{closable && (
|
|
101
|
+
<Pressable onPress={onClose} hitSlop={8} accessibilityLabel="닫기">
|
|
102
|
+
<Text style={{ fontSize: fontSize.xl ?? 20, color: text.tertiary ?? '#757B80' }}>✕</Text>
|
|
103
|
+
</Pressable>
|
|
104
|
+
)}
|
|
105
|
+
</View>
|
|
106
|
+
)}
|
|
107
|
+
<View style={[styles.content, { padding: padding.l ?? 20 }]}>{children}</View>
|
|
108
|
+
</Animated.View>
|
|
109
|
+
</View>
|
|
110
|
+
</Modal>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
BottomSheet.displayName = 'BottomSheet';
|
|
115
|
+
|
|
116
|
+
const styles = StyleSheet.create({
|
|
117
|
+
overlay: { flex: 1, justifyContent: 'flex-end' },
|
|
118
|
+
backdrop: { ...StyleSheet.absoluteFillObject },
|
|
119
|
+
sheet: { overflow: 'hidden' },
|
|
120
|
+
handleContainer: { alignItems: 'center', paddingTop: 10, paddingBottom: 6 },
|
|
121
|
+
handle: { width: 36, height: 4, borderRadius: 2 },
|
|
122
|
+
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingBottom: 8 },
|
|
123
|
+
title: { fontWeight: '600', flex: 1 },
|
|
124
|
+
content: { flex: 1 },
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export default BottomSheet;
|