@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.
Files changed (237) hide show
  1. package/README.md +152 -0
  2. package/dist/components/Alert/Alert.d.ts +29 -0
  3. package/dist/components/Alert/Alert.d.ts.map +1 -0
  4. package/dist/components/Alert/Alert.js +44 -0
  5. package/dist/components/Alert/Alert.js.map +1 -0
  6. package/dist/components/Alert/index.d.ts +3 -0
  7. package/dist/components/Alert/index.d.ts.map +1 -0
  8. package/dist/components/Alert/index.js +2 -0
  9. package/dist/components/Alert/index.js.map +1 -0
  10. package/dist/components/Avatar/Avatar.d.ts +28 -0
  11. package/dist/components/Avatar/Avatar.d.ts.map +1 -0
  12. package/dist/components/Avatar/Avatar.js +73 -0
  13. package/dist/components/Avatar/Avatar.js.map +1 -0
  14. package/dist/components/Avatar/index.d.ts +3 -0
  15. package/dist/components/Avatar/index.d.ts.map +1 -0
  16. package/dist/components/Avatar/index.js +2 -0
  17. package/dist/components/Avatar/index.js.map +1 -0
  18. package/dist/components/Badge/Badge.d.ts +28 -0
  19. package/dist/components/Badge/Badge.d.ts.map +1 -0
  20. package/dist/components/Badge/Badge.js +52 -0
  21. package/dist/components/Badge/Badge.js.map +1 -0
  22. package/dist/components/Badge/index.d.ts +3 -0
  23. package/dist/components/Badge/index.d.ts.map +1 -0
  24. package/dist/components/Badge/index.js +2 -0
  25. package/dist/components/Badge/index.js.map +1 -0
  26. package/dist/components/BottomNavigation/BottomNavigation.d.ts +30 -0
  27. package/dist/components/BottomNavigation/BottomNavigation.d.ts.map +1 -0
  28. package/dist/components/BottomNavigation/BottomNavigation.js +48 -0
  29. package/dist/components/BottomNavigation/BottomNavigation.js.map +1 -0
  30. package/dist/components/BottomNavigation/index.d.ts +3 -0
  31. package/dist/components/BottomNavigation/index.d.ts.map +1 -0
  32. package/dist/components/BottomNavigation/index.js +2 -0
  33. package/dist/components/BottomNavigation/index.js.map +1 -0
  34. package/dist/components/BottomSheet/BottomSheet.d.ts +26 -0
  35. package/dist/components/BottomSheet/BottomSheet.d.ts.map +1 -0
  36. package/dist/components/BottomSheet/BottomSheet.js +75 -0
  37. package/dist/components/BottomSheet/BottomSheet.js.map +1 -0
  38. package/dist/components/BottomSheet/index.d.ts +3 -0
  39. package/dist/components/BottomSheet/index.d.ts.map +1 -0
  40. package/dist/components/BottomSheet/index.js +2 -0
  41. package/dist/components/BottomSheet/index.js.map +1 -0
  42. package/dist/components/Button/Button.d.ts +53 -0
  43. package/dist/components/Button/Button.d.ts.map +1 -0
  44. package/dist/components/Button/Button.js +129 -0
  45. package/dist/components/Button/Button.js.map +1 -0
  46. package/dist/components/Button/index.d.ts +3 -0
  47. package/dist/components/Button/index.d.ts.map +1 -0
  48. package/dist/components/Button/index.js +2 -0
  49. package/dist/components/Button/index.js.map +1 -0
  50. package/dist/components/Card/Card.d.ts +36 -0
  51. package/dist/components/Card/Card.d.ts.map +1 -0
  52. package/dist/components/Card/Card.js +61 -0
  53. package/dist/components/Card/Card.js.map +1 -0
  54. package/dist/components/Card/index.d.ts +3 -0
  55. package/dist/components/Card/index.d.ts.map +1 -0
  56. package/dist/components/Card/index.js +2 -0
  57. package/dist/components/Card/index.js.map +1 -0
  58. package/dist/components/Checkbox/Checkbox.d.ts +26 -0
  59. package/dist/components/Checkbox/Checkbox.d.ts.map +1 -0
  60. package/dist/components/Checkbox/Checkbox.js +57 -0
  61. package/dist/components/Checkbox/Checkbox.js.map +1 -0
  62. package/dist/components/Checkbox/index.d.ts +3 -0
  63. package/dist/components/Checkbox/index.d.ts.map +1 -0
  64. package/dist/components/Checkbox/index.js +2 -0
  65. package/dist/components/Checkbox/index.js.map +1 -0
  66. package/dist/components/Chip/Chip.d.ts +29 -0
  67. package/dist/components/Chip/Chip.d.ts.map +1 -0
  68. package/dist/components/Chip/Chip.js +54 -0
  69. package/dist/components/Chip/Chip.js.map +1 -0
  70. package/dist/components/Chip/index.d.ts +3 -0
  71. package/dist/components/Chip/index.d.ts.map +1 -0
  72. package/dist/components/Chip/index.js +2 -0
  73. package/dist/components/Chip/index.js.map +1 -0
  74. package/dist/components/Divider/Divider.d.ts +22 -0
  75. package/dist/components/Divider/Divider.d.ts.map +1 -0
  76. package/dist/components/Divider/Divider.js +16 -0
  77. package/dist/components/Divider/Divider.js.map +1 -0
  78. package/dist/components/Divider/index.d.ts +3 -0
  79. package/dist/components/Divider/index.d.ts.map +1 -0
  80. package/dist/components/Divider/index.js +2 -0
  81. package/dist/components/Divider/index.js.map +1 -0
  82. package/dist/components/Input/Input.d.ts +33 -0
  83. package/dist/components/Input/Input.d.ts.map +1 -0
  84. package/dist/components/Input/Input.js +98 -0
  85. package/dist/components/Input/Input.js.map +1 -0
  86. package/dist/components/Input/index.d.ts +3 -0
  87. package/dist/components/Input/index.d.ts.map +1 -0
  88. package/dist/components/Input/index.js +2 -0
  89. package/dist/components/Input/index.js.map +1 -0
  90. package/dist/components/Modal/Modal.d.ts +28 -0
  91. package/dist/components/Modal/Modal.d.ts.map +1 -0
  92. package/dist/components/Modal/Modal.js +46 -0
  93. package/dist/components/Modal/Modal.js.map +1 -0
  94. package/dist/components/Modal/index.d.ts +3 -0
  95. package/dist/components/Modal/index.d.ts.map +1 -0
  96. package/dist/components/Modal/index.js +2 -0
  97. package/dist/components/Modal/index.js.map +1 -0
  98. package/dist/components/Navbar/Navbar.d.ts +50 -0
  99. package/dist/components/Navbar/Navbar.d.ts.map +1 -0
  100. package/dist/components/Navbar/Navbar.js +125 -0
  101. package/dist/components/Navbar/Navbar.js.map +1 -0
  102. package/dist/components/Navbar/index.d.ts +3 -0
  103. package/dist/components/Navbar/index.d.ts.map +1 -0
  104. package/dist/components/Navbar/index.js +2 -0
  105. package/dist/components/Navbar/index.js.map +1 -0
  106. package/dist/components/ProgressBar/ProgressBar.d.ts +27 -0
  107. package/dist/components/ProgressBar/ProgressBar.d.ts.map +1 -0
  108. package/dist/components/ProgressBar/ProgressBar.js +55 -0
  109. package/dist/components/ProgressBar/ProgressBar.js.map +1 -0
  110. package/dist/components/ProgressBar/index.d.ts +3 -0
  111. package/dist/components/ProgressBar/index.d.ts.map +1 -0
  112. package/dist/components/ProgressBar/index.js +2 -0
  113. package/dist/components/ProgressBar/index.js.map +1 -0
  114. package/dist/components/Radio/Radio.d.ts +30 -0
  115. package/dist/components/Radio/Radio.d.ts.map +1 -0
  116. package/dist/components/Radio/Radio.js +56 -0
  117. package/dist/components/Radio/Radio.js.map +1 -0
  118. package/dist/components/Radio/index.d.ts +3 -0
  119. package/dist/components/Radio/index.d.ts.map +1 -0
  120. package/dist/components/Radio/index.js +2 -0
  121. package/dist/components/Radio/index.js.map +1 -0
  122. package/dist/components/SearchBar/SearchBar.d.ts +29 -0
  123. package/dist/components/SearchBar/SearchBar.d.ts.map +1 -0
  124. package/dist/components/SearchBar/SearchBar.js +64 -0
  125. package/dist/components/SearchBar/SearchBar.js.map +1 -0
  126. package/dist/components/SearchBar/index.d.ts +3 -0
  127. package/dist/components/SearchBar/index.d.ts.map +1 -0
  128. package/dist/components/SearchBar/index.js +2 -0
  129. package/dist/components/SearchBar/index.js.map +1 -0
  130. package/dist/components/Skeleton/Skeleton.d.ts +22 -0
  131. package/dist/components/Skeleton/Skeleton.d.ts.map +1 -0
  132. package/dist/components/Skeleton/Skeleton.js +46 -0
  133. package/dist/components/Skeleton/Skeleton.js.map +1 -0
  134. package/dist/components/Skeleton/index.d.ts +3 -0
  135. package/dist/components/Skeleton/index.d.ts.map +1 -0
  136. package/dist/components/Skeleton/index.js +2 -0
  137. package/dist/components/Skeleton/index.js.map +1 -0
  138. package/dist/components/Spinner/Spinner.d.ts +23 -0
  139. package/dist/components/Spinner/Spinner.d.ts.map +1 -0
  140. package/dist/components/Spinner/Spinner.js +19 -0
  141. package/dist/components/Spinner/Spinner.js.map +1 -0
  142. package/dist/components/Spinner/index.d.ts +3 -0
  143. package/dist/components/Spinner/index.d.ts.map +1 -0
  144. package/dist/components/Spinner/index.js +2 -0
  145. package/dist/components/Spinner/index.js.map +1 -0
  146. package/dist/components/Tabs/Tabs.d.ts +30 -0
  147. package/dist/components/Tabs/Tabs.d.ts.map +1 -0
  148. package/dist/components/Tabs/Tabs.js +73 -0
  149. package/dist/components/Tabs/Tabs.js.map +1 -0
  150. package/dist/components/Tabs/index.d.ts +3 -0
  151. package/dist/components/Tabs/index.d.ts.map +1 -0
  152. package/dist/components/Tabs/index.js +2 -0
  153. package/dist/components/Tabs/index.js.map +1 -0
  154. package/dist/components/Toast/Toast.d.ts +30 -0
  155. package/dist/components/Toast/Toast.d.ts.map +1 -0
  156. package/dist/components/Toast/Toast.js +84 -0
  157. package/dist/components/Toast/Toast.js.map +1 -0
  158. package/dist/components/Toast/index.d.ts +3 -0
  159. package/dist/components/Toast/index.d.ts.map +1 -0
  160. package/dist/components/Toast/index.js +2 -0
  161. package/dist/components/Toast/index.js.map +1 -0
  162. package/dist/components/Toggle/Toggle.d.ts +26 -0
  163. package/dist/components/Toggle/Toggle.d.ts.map +1 -0
  164. package/dist/components/Toggle/Toggle.js +75 -0
  165. package/dist/components/Toggle/Toggle.js.map +1 -0
  166. package/dist/components/Toggle/index.d.ts +3 -0
  167. package/dist/components/Toggle/index.d.ts.map +1 -0
  168. package/dist/components/Toggle/index.js +2 -0
  169. package/dist/components/Toggle/index.js.map +1 -0
  170. package/dist/index.d.ts +61 -0
  171. package/dist/index.d.ts.map +1 -0
  172. package/dist/index.js +43 -0
  173. package/dist/index.js.map +1 -0
  174. package/dist/theme/ThemeProvider.d.ts +35 -0
  175. package/dist/theme/ThemeProvider.d.ts.map +1 -0
  176. package/dist/theme/ThemeProvider.js +45 -0
  177. package/dist/theme/ThemeProvider.js.map +1 -0
  178. package/dist/theme/index.d.ts +6 -0
  179. package/dist/theme/index.d.ts.map +1 -0
  180. package/dist/theme/index.js +4 -0
  181. package/dist/theme/index.js.map +1 -0
  182. package/dist/theme/tokens.d.ts +55 -0
  183. package/dist/theme/tokens.d.ts.map +1 -0
  184. package/dist/theme/tokens.js +172 -0
  185. package/dist/theme/tokens.js.map +1 -0
  186. package/dist/theme/useTheme.d.ts +25 -0
  187. package/dist/theme/useTheme.d.ts.map +1 -0
  188. package/dist/theme/useTheme.js +33 -0
  189. package/dist/theme/useTheme.js.map +1 -0
  190. package/package.json +58 -0
  191. package/src/components/Alert/Alert.tsx +105 -0
  192. package/src/components/Alert/index.ts +2 -0
  193. package/src/components/Avatar/Avatar.tsx +122 -0
  194. package/src/components/Avatar/index.ts +2 -0
  195. package/src/components/Badge/Badge.tsx +100 -0
  196. package/src/components/Badge/index.ts +2 -0
  197. package/src/components/BottomNavigation/BottomNavigation.tsx +104 -0
  198. package/src/components/BottomNavigation/index.ts +2 -0
  199. package/src/components/BottomSheet/BottomSheet.tsx +127 -0
  200. package/src/components/BottomSheet/index.ts +2 -0
  201. package/src/components/Button/Button.tsx +255 -0
  202. package/src/components/Button/index.ts +2 -0
  203. package/src/components/Card/Card.tsx +147 -0
  204. package/src/components/Card/index.ts +2 -0
  205. package/src/components/Checkbox/Checkbox.tsx +95 -0
  206. package/src/components/Checkbox/index.ts +2 -0
  207. package/src/components/Chip/Chip.tsx +108 -0
  208. package/src/components/Chip/index.ts +2 -0
  209. package/src/components/Divider/Divider.tsx +41 -0
  210. package/src/components/Divider/index.ts +2 -0
  211. package/src/components/Input/Input.tsx +199 -0
  212. package/src/components/Input/index.ts +2 -0
  213. package/src/components/Modal/Modal.tsx +117 -0
  214. package/src/components/Modal/index.ts +2 -0
  215. package/src/components/Navbar/Navbar.tsx +278 -0
  216. package/src/components/Navbar/index.ts +2 -0
  217. package/src/components/ProgressBar/ProgressBar.tsx +99 -0
  218. package/src/components/ProgressBar/index.ts +2 -0
  219. package/src/components/Radio/Radio.tsx +103 -0
  220. package/src/components/Radio/index.ts +2 -0
  221. package/src/components/SearchBar/SearchBar.tsx +115 -0
  222. package/src/components/SearchBar/index.ts +2 -0
  223. package/src/components/Skeleton/Skeleton.tsx +74 -0
  224. package/src/components/Skeleton/index.ts +2 -0
  225. package/src/components/Spinner/Spinner.tsx +58 -0
  226. package/src/components/Spinner/index.ts +2 -0
  227. package/src/components/Tabs/Tabs.tsx +124 -0
  228. package/src/components/Tabs/index.ts +2 -0
  229. package/src/components/Toast/Toast.tsx +128 -0
  230. package/src/components/Toast/index.ts +2 -0
  231. package/src/components/Toggle/Toggle.tsx +109 -0
  232. package/src/components/Toggle/index.ts +2 -0
  233. package/src/index.ts +87 -0
  234. package/src/theme/ThemeProvider.tsx +96 -0
  235. package/src/theme/index.ts +5 -0
  236. package/src/theme/tokens.ts +225 -0
  237. 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,2 @@
1
+ export { Avatar } from './Avatar';
2
+ export type { AvatarProps, AvatarSize, AvatarStatus } from './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,2 @@
1
+ export { Badge } from './Badge';
2
+ export type { BadgeProps, BadgeSize, BadgeVariant, BadgeType } from './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,2 @@
1
+ export { BottomNavigation } from './BottomNavigation';
2
+ export type { BottomNavigationProps, BottomNavItem } from './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;
@@ -0,0 +1,2 @@
1
+ export { BottomSheet } from './BottomSheet';
2
+ export type { BottomSheetProps } from './BottomSheet';