@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,199 @@
1
+ /**
2
+ * Input 컴포넌트 (React Native)
3
+ *
4
+ * 스타일링 토큰 경로:
5
+ * - 색상: theme.color.semantic.field.input.{bg-default|border-default|border-focus|...}
6
+ * - 사이즈: theme.size.semantic.input.{s|m|l}
7
+ * - 패딩: theme.spacing.aliases.padding.{xs|s|m}
8
+ * - 라운드: theme.border.semantic.radius.input.{s|m|l}
9
+ * - 폰트: theme.typography.foundation.fontSize.{xs|s|base|l}
10
+ *
11
+ * 원본: packages/ui/src/components/Input/Input.tsx
12
+ */
13
+
14
+ import React, { useState } from 'react';
15
+ import {
16
+ View,
17
+ TextInput,
18
+ Text,
19
+ StyleSheet,
20
+ type ViewStyle,
21
+ type TextStyle,
22
+ type TextInputProps as RNTextInputProps,
23
+ } from 'react-native';
24
+ import { useTheme } from '../../theme/useTheme';
25
+
26
+ export type InputSize = 's' | 'm' | 'l';
27
+
28
+ export interface InputProps extends Omit<RNTextInputProps, 'style'> {
29
+ label?: string;
30
+ size?: InputSize;
31
+ disabled?: boolean;
32
+ required?: boolean;
33
+ error?: boolean;
34
+ errorMessage?: string;
35
+ helperText?: string;
36
+ startIcon?: React.ReactNode;
37
+ endIcon?: React.ReactNode;
38
+ fullWidth?: boolean;
39
+ containerStyle?: ViewStyle;
40
+ inputStyle?: TextStyle;
41
+ onChangeText?: (text: string) => void;
42
+ }
43
+
44
+ export const Input: React.FC<InputProps> = ({
45
+ label,
46
+ size = 'm',
47
+ disabled = false,
48
+ required = false,
49
+ error = false,
50
+ errorMessage,
51
+ helperText,
52
+ startIcon,
53
+ endIcon,
54
+ fullWidth = true,
55
+ containerStyle,
56
+ inputStyle,
57
+ onChangeText,
58
+ onFocus,
59
+ onBlur,
60
+ ...props
61
+ }) => {
62
+ const { theme } = useTheme();
63
+ const [isFocused, setIsFocused] = useState(false);
64
+
65
+ // ─── 토큰에서 가져오기 ───
66
+ const fieldTokens = theme.color.semantic?.field?.input ?? {};
67
+ const aliases = theme.color.aliases;
68
+ const inputSizeTokens = theme.size.semantic?.input;
69
+ const padding = theme.spacing.aliases?.padding ?? theme.spacing.aliases?.scale;
70
+ const fontSize = theme.typography.foundation?.fontSize;
71
+ const radiusTokens = theme.border.semantic?.radius?.input;
72
+ const disabledOpacity = theme.opacity.foundation?.[40] ?? 0.4;
73
+
74
+ const sizeConfig = {
75
+ s: {
76
+ height: inputSizeTokens?.s ?? 24,
77
+ fontSize: fontSize?.xs ?? 12,
78
+ paddingH: padding?.xs ?? 4,
79
+ },
80
+ m: {
81
+ height: inputSizeTokens?.m ?? 32,
82
+ fontSize: fontSize?.s ?? 14,
83
+ paddingH: padding?.s ?? 8,
84
+ },
85
+ l: {
86
+ height: inputSizeTokens?.l ?? 40,
87
+ fontSize: fontSize?.base ?? 16,
88
+ paddingH: padding?.m ?? 12,
89
+ },
90
+ }[size];
91
+
92
+ const borderRadius = radiusTokens?.[size] ?? 8;
93
+
94
+ // 색상 (semantic 토큰 > aliases 폴백)
95
+ const bgColor = disabled
96
+ ? (fieldTokens['bg-disabled'] ?? aliases?.surface?.muted ?? '#E8EEF2')
97
+ : (fieldTokens['bg-default'] ?? aliases?.surface?.base ?? '#FFFFFF');
98
+
99
+ const borderColor = error
100
+ ? (fieldTokens['border-error'] ?? aliases?.feedback?.error?.fg ?? '#DC2626')
101
+ : isFocused
102
+ ? (fieldTokens['border-focus'] ?? aliases?.brand?.primary ?? '#006FFF')
103
+ : (fieldTokens['border-default'] ?? aliases?.border?.base ?? '#E8EEF2');
104
+
105
+ const textColor = disabled
106
+ ? (fieldTokens['text-disabled'] ?? aliases?.text?.disabled ?? '#A4ADB2')
107
+ : (fieldTokens['text-default'] ?? aliases?.text?.primary ?? '#17191A');
108
+
109
+ const placeholderColor = fieldTokens?.placeholder ?? aliases?.text?.tertiary ?? '#757B80';
110
+ const labelColor = error
111
+ ? (aliases?.feedback?.error?.fg ?? '#DC2626')
112
+ : (aliases?.text?.primary ?? '#17191A');
113
+
114
+ return (
115
+ <View style={[fullWidth && styles.fullWidth, containerStyle]}>
116
+ {label && (
117
+ <Text style={[
118
+ styles.label,
119
+ { color: labelColor, fontSize: fontSize?.xs ?? 12 },
120
+ disabled && { opacity: disabledOpacity },
121
+ ]}>
122
+ {label}
123
+ {required && <Text style={{ color: aliases?.feedback?.error?.fg ?? '#DC2626' }}> *</Text>}
124
+ </Text>
125
+ )}
126
+
127
+ <View style={[
128
+ styles.inputWrapper,
129
+ {
130
+ height: sizeConfig.height,
131
+ borderColor,
132
+ borderRadius,
133
+ backgroundColor: bgColor,
134
+ },
135
+ isFocused && !error && { borderWidth: 2 },
136
+ ]}>
137
+ {startIcon && (
138
+ <View style={[styles.iconContainer, { paddingLeft: sizeConfig.paddingH }]}>
139
+ {startIcon}
140
+ </View>
141
+ )}
142
+
143
+ <TextInput
144
+ {...props}
145
+ onChangeText={onChangeText}
146
+ editable={!disabled}
147
+ onFocus={(e) => { setIsFocused(true); onFocus?.(e); }}
148
+ onBlur={(e) => { setIsFocused(false); onBlur?.(e); }}
149
+ placeholderTextColor={placeholderColor}
150
+ style={[
151
+ styles.input,
152
+ {
153
+ fontSize: sizeConfig.fontSize,
154
+ color: textColor,
155
+ paddingHorizontal: sizeConfig.paddingH,
156
+ },
157
+ startIcon ? { paddingLeft: 0 } : null,
158
+ endIcon ? { paddingRight: 0 } : null,
159
+ inputStyle,
160
+ ]}
161
+ accessibilityLabel={label}
162
+ accessibilityState={{ disabled }}
163
+ />
164
+
165
+ {endIcon && (
166
+ <View style={[styles.iconContainer, { paddingRight: sizeConfig.paddingH }]}>
167
+ {endIcon}
168
+ </View>
169
+ )}
170
+ </View>
171
+
172
+ {helperText && !error && (
173
+ <Text style={[styles.helperText, { color: aliases?.text?.tertiary ?? '#757B80', fontSize: fontSize?.xs ?? 12 }]}>
174
+ {helperText}
175
+ </Text>
176
+ )}
177
+
178
+ {error && errorMessage && (
179
+ <Text style={[styles.errorText, { color: aliases?.feedback?.error?.fg ?? '#DC2626', fontSize: fontSize?.xs ?? 12 }]}>
180
+ {errorMessage}
181
+ </Text>
182
+ )}
183
+ </View>
184
+ );
185
+ };
186
+
187
+ Input.displayName = 'Input';
188
+
189
+ const styles = StyleSheet.create({
190
+ fullWidth: { width: '100%' },
191
+ label: { fontWeight: '500', marginBottom: 6 },
192
+ inputWrapper: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, overflow: 'hidden' },
193
+ input: { flex: 1, height: '100%', padding: 0 },
194
+ iconContainer: { alignItems: 'center', justifyContent: 'center', paddingHorizontal: 8 },
195
+ helperText: { marginTop: 4 },
196
+ errorText: { marginTop: 4 },
197
+ });
198
+
199
+ export default Input;
@@ -0,0 +1,2 @@
1
+ export { Input } from './Input';
2
+ export type { InputProps, InputSize } from './Input';
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Modal 컴포넌트 (React Native)
3
+ *
4
+ * 스타일링 토큰 경로:
5
+ * - 배경: theme.color.aliases.surface.base
6
+ * - 오버레이: theme.color.aliases.overlay.base
7
+ * - 라운드: theme.border.semantic.radius.modal.{s|m|l}
8
+ * - 패딩: theme.spacing.aliases.padding.{m|l}
9
+ * - 그림자: theme.shadow.foundation.xl
10
+ * - 폰트: theme.typography.foundation.fontSize.*
11
+ */
12
+
13
+ import React from 'react';
14
+ import {
15
+ Modal as RNModal,
16
+ View, Text, Pressable, StyleSheet,
17
+ type ViewStyle, KeyboardAvoidingView, Platform,
18
+ } from 'react-native';
19
+ import { useTheme } from '../../theme/useTheme';
20
+
21
+ export type ModalSize = 's' | 'm' | 'l';
22
+
23
+ export interface ModalProps {
24
+ visible: boolean;
25
+ onClose: () => void;
26
+ title?: string;
27
+ children: React.ReactNode;
28
+ size?: ModalSize;
29
+ closable?: boolean;
30
+ footer?: React.ReactNode;
31
+ animationType?: 'none' | 'slide' | 'fade';
32
+ style?: ViewStyle;
33
+ }
34
+
35
+ export const Modal: React.FC<ModalProps> = ({
36
+ visible, onClose, title, children, size = 'm',
37
+ closable = true, footer, animationType = 'fade', style,
38
+ }) => {
39
+ const { theme } = useTheme();
40
+
41
+ const surface = theme.color.aliases?.surface ?? {};
42
+ const text = theme.color.aliases?.text ?? {};
43
+ const overlay = theme.color.aliases?.overlay ?? {};
44
+ const borders = theme.color.aliases?.border ?? {};
45
+ const padding = theme.spacing.aliases?.padding ?? {};
46
+ const fontSize = theme.typography.foundation?.fontSize ?? {};
47
+ const modalRadius = theme.border.semantic?.radius?.modal ?? {};
48
+ const shadow = theme.shadow.foundation ?? {};
49
+ const gap = theme.spacing.aliases?.gap ?? {};
50
+
51
+ const borderRadius = modalRadius[size] ?? { s: 8, m: 12, l: 16 }[size];
52
+ const maxWidth = { s: 320, m: 400, l: 500 }[size];
53
+
54
+ const toShadowStyle = (s: any) => s ? {
55
+ shadowColor: s.shadowColor ?? '#000',
56
+ shadowOffset: { width: s.shadowOffsetX ?? 0, height: s.shadowOffsetY ?? 0 },
57
+ shadowOpacity: s.shadowOpacity ?? 0.1,
58
+ shadowRadius: s.shadowRadius ?? 15,
59
+ elevation: s.elevation ?? 8,
60
+ } : {};
61
+
62
+ return (
63
+ <RNModal visible={visible} transparent animationType={animationType}
64
+ onRequestClose={closable ? onClose : undefined}>
65
+ <KeyboardAvoidingView style={styles.overlay}
66
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
67
+ <Pressable style={[styles.backdrop, { backgroundColor: overlay.base ?? 'rgba(0,0,0,0.6)' }]}
68
+ onPress={closable ? onClose : undefined} />
69
+ <View style={[
70
+ styles.container,
71
+ {
72
+ maxWidth, borderRadius,
73
+ backgroundColor: surface.base ?? '#FFFFFF',
74
+ padding: padding.l ?? 20,
75
+ ...toShadowStyle(shadow.xl),
76
+ },
77
+ style,
78
+ ]}>
79
+ {(title || closable) && (
80
+ <View style={[styles.header, { marginBottom: gap.m ?? 12 }]}>
81
+ {title && (
82
+ <Text style={[styles.title, { color: text.primary ?? '#17191A', fontSize: fontSize.l ?? 18 }]}>
83
+ {title}
84
+ </Text>
85
+ )}
86
+ {closable && (
87
+ <Pressable onPress={onClose} hitSlop={8} accessibilityLabel="닫기" accessibilityRole="button">
88
+ <Text style={{ fontSize: fontSize.xl ?? 20, color: text.tertiary ?? '#757B80' }}>✕</Text>
89
+ </Pressable>
90
+ )}
91
+ </View>
92
+ )}
93
+ <View style={styles.body}>{children}</View>
94
+ {footer && (
95
+ <View style={[styles.footer, { borderTopColor: borders.base ?? '#E8EEF2', marginTop: gap.l ?? 20, paddingTop: gap.m ?? 12 }]}>
96
+ {footer}
97
+ </View>
98
+ )}
99
+ </View>
100
+ </KeyboardAvoidingView>
101
+ </RNModal>
102
+ );
103
+ };
104
+
105
+ Modal.displayName = 'Modal';
106
+
107
+ const styles = StyleSheet.create({
108
+ overlay: { flex: 1, justifyContent: 'center', alignItems: 'center' },
109
+ backdrop: { ...StyleSheet.absoluteFillObject },
110
+ container: { width: '90%' },
111
+ header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
112
+ title: { fontWeight: '600', flex: 1 },
113
+ body: {},
114
+ footer: { flexDirection: 'row', justifyContent: 'flex-end', gap: 8, borderTopWidth: 1 },
115
+ });
116
+
117
+ export default Modal;
@@ -0,0 +1,2 @@
1
+ export { Modal } from './Modal';
2
+ export type { ModalProps, ModalSize } from './Modal';
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Navbar 컴포넌트 (React Native)
3
+ *
4
+ * 두 가지 모드 지원:
5
+ * 1. stack (기본) — 왼쪽 뒤로가기, 가운데 타이틀, 오른쪽 액션 버튼(최대 2개)
6
+ * 2. tab — 왼쪽 타이틀 텍스트, 오른쪽 액션 버튼(최대 3개)
7
+ *
8
+ * 스타일링 토큰 경로:
9
+ * - 배경: theme.color.aliases.surface.base
10
+ * - 텍스트: theme.color.aliases.text.{primary|secondary}
11
+ * - 보더: theme.color.aliases.border.base
12
+ * - 브랜드: theme.color.aliases.brand.primary
13
+ * - 폰트: theme.typography.foundation.fontSize.*
14
+ * - 패딩: theme.spacing.aliases.padding.*
15
+ * - 아이콘: theme.size.semantic.icon.*
16
+ */
17
+
18
+ import React from 'react';
19
+ import {
20
+ View, Text, Pressable, StyleSheet, Platform, StatusBar,
21
+ type ViewStyle,
22
+ } from 'react-native';
23
+ import { useTheme } from '../../theme/useTheme';
24
+
25
+ export interface NavbarAction {
26
+ /** 아이콘 (ReactNode) 또는 텍스트 */
27
+ icon?: React.ReactNode;
28
+ label?: string;
29
+ onPress: () => void;
30
+ disabled?: boolean;
31
+ }
32
+
33
+ export interface NavbarProps {
34
+ /** 네비게이션 모드: stack = 뒤로가기+중앙타이틀, tab = 좌측타이틀+우측액션 */
35
+ mode?: 'stack' | 'tab';
36
+ /** 타이틀 */
37
+ title: string;
38
+ /** 서브타이틀 (tab 모드에서만 표시) */
39
+ subtitle?: string;
40
+ /** 뒤로가기 핸들러 (stack 모드) */
41
+ onBack?: () => void;
42
+ /** 뒤로가기 커스텀 아이콘 */
43
+ backIcon?: React.ReactNode;
44
+ /** 오른쪽 액션 버튼 (stack: 최대 2개, tab: 최대 3개) */
45
+ actions?: NavbarAction[];
46
+ /** 하단 보더 표시 여부 */
47
+ showBorder?: boolean;
48
+ /** 배경 투명 */
49
+ transparent?: boolean;
50
+ /** 타이틀 아래 추가 컨텐츠 (검색바 등) */
51
+ children?: React.ReactNode;
52
+ /** 추가 스타일 */
53
+ style?: ViewStyle;
54
+ }
55
+
56
+ export const Navbar: React.FC<NavbarProps> = ({
57
+ mode = 'stack',
58
+ title,
59
+ subtitle,
60
+ onBack,
61
+ backIcon,
62
+ actions = [],
63
+ showBorder = true,
64
+ transparent = false,
65
+ children,
66
+ style,
67
+ }) => {
68
+ const { theme } = useTheme();
69
+
70
+ // ─── 토큰에서 가져오기 ───
71
+ const surface = theme.color.aliases?.surface ?? {};
72
+ const textColors = theme.color.aliases?.text ?? {};
73
+ const borders = theme.color.aliases?.border ?? {};
74
+ const brand = theme.color.aliases?.brand ?? {};
75
+ const padding = theme.spacing.aliases?.padding ?? {};
76
+ const fontSize = theme.typography.foundation?.fontSize ?? {};
77
+ const gap = theme.spacing.aliases?.gap ?? {};
78
+ const iconSize = theme.size.semantic?.icon ?? {};
79
+
80
+ // stack 모드 = 최대 2개, tab 모드 = 최대 3개
81
+ const maxActions = mode === 'stack' ? 2 : 3;
82
+ const visibleActions = actions.slice(0, maxActions);
83
+
84
+ const NAVBAR_HEIGHT = 56;
85
+ const statusBarHeight = Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) : 0;
86
+
87
+ const renderAction = (action: NavbarAction, index: number) => (
88
+ <Pressable
89
+ key={index}
90
+ onPress={action.onPress}
91
+ disabled={action.disabled}
92
+ accessibilityRole="button"
93
+ accessibilityLabel={action.label}
94
+ hitSlop={8}
95
+ style={({ pressed }) => [
96
+ styles.actionButton,
97
+ {
98
+ width: iconSize.xl ?? 32,
99
+ height: iconSize.xl ?? 32,
100
+ },
101
+ action.disabled && { opacity: theme.opacity.foundation?.[40] ?? 0.4 },
102
+ pressed && { opacity: 0.6 },
103
+ ]}
104
+ >
105
+ {action.icon ?? (
106
+ <Text style={{
107
+ fontSize: fontSize.s ?? 14,
108
+ fontWeight: '500',
109
+ color: brand.primary ?? '#006FFF',
110
+ }}>
111
+ {action.label}
112
+ </Text>
113
+ )}
114
+ </Pressable>
115
+ );
116
+
117
+ return (
118
+ <View style={[
119
+ styles.wrapper,
120
+ {
121
+ paddingTop: statusBarHeight,
122
+ backgroundColor: transparent ? 'transparent' : (surface.base ?? '#FFFFFF'),
123
+ borderBottomWidth: showBorder ? 1 : 0,
124
+ borderBottomColor: borders.base ?? '#E8EEF2',
125
+ },
126
+ style,
127
+ ]}>
128
+ <View style={[
129
+ styles.container,
130
+ {
131
+ height: NAVBAR_HEIGHT,
132
+ paddingHorizontal: padding.m ?? 12,
133
+ },
134
+ ]}>
135
+ {mode === 'stack' ? (
136
+ /* ── Stack 모드: [뒤로가기] [중앙 타이틀] [액션 버튼] ── */
137
+ <>
138
+ <View style={styles.left}>
139
+ {onBack && (
140
+ <Pressable
141
+ onPress={onBack}
142
+ accessibilityRole="button"
143
+ accessibilityLabel="뒤로 가기"
144
+ hitSlop={12}
145
+ style={({ pressed }) => [
146
+ styles.backButton,
147
+ {
148
+ width: iconSize.xl ?? 32,
149
+ height: iconSize.xl ?? 32,
150
+ },
151
+ pressed && { opacity: 0.6 },
152
+ ]}
153
+ >
154
+ {backIcon ?? (
155
+ <Text style={{
156
+ fontSize: fontSize.xl ?? 20,
157
+ color: textColors.primary ?? '#17191A',
158
+ }}>
159
+
160
+ </Text>
161
+ )}
162
+ </Pressable>
163
+ )}
164
+ </View>
165
+
166
+ <View style={styles.center}>
167
+ <Text
168
+ style={[styles.stackTitle, {
169
+ fontSize: fontSize.base ?? 16,
170
+ color: textColors.primary ?? '#17191A',
171
+ }]}
172
+ numberOfLines={1}
173
+ >
174
+ {title}
175
+ </Text>
176
+ </View>
177
+
178
+ <View style={[styles.right, { gap: gap.xs ?? 4 }]}>
179
+ {visibleActions.map(renderAction)}
180
+ </View>
181
+ </>
182
+ ) : (
183
+ /* ── Tab 모드: [좌측 타이틀] [우측 액션 버튼] ── */
184
+ <>
185
+ <View style={styles.tabLeft}>
186
+ <Text
187
+ style={[styles.tabTitle, {
188
+ fontSize: fontSize['2xl'] ?? 24,
189
+ color: textColors.primary ?? '#17191A',
190
+ }]}
191
+ numberOfLines={1}
192
+ >
193
+ {title}
194
+ </Text>
195
+ {subtitle && (
196
+ <Text style={{
197
+ fontSize: fontSize.xs ?? 12,
198
+ color: textColors.tertiary ?? '#757B80',
199
+ marginTop: 1,
200
+ }}>
201
+ {subtitle}
202
+ </Text>
203
+ )}
204
+ </View>
205
+
206
+ <View style={[styles.right, { gap: gap.xs ?? 4 }]}>
207
+ {visibleActions.map(renderAction)}
208
+ </View>
209
+ </>
210
+ )}
211
+ </View>
212
+
213
+ {/* 추가 컨텐츠 (검색바 등) */}
214
+ {children && (
215
+ <View style={{
216
+ paddingHorizontal: padding.m ?? 12,
217
+ paddingBottom: padding.s ?? 8,
218
+ }}>
219
+ {children}
220
+ </View>
221
+ )}
222
+ </View>
223
+ );
224
+ };
225
+
226
+ Navbar.displayName = 'Navbar';
227
+
228
+ const styles = StyleSheet.create({
229
+ wrapper: {},
230
+ container: {
231
+ flexDirection: 'row',
232
+ alignItems: 'center',
233
+ },
234
+
235
+ /* ─── Stack 모드 ─── */
236
+ left: {
237
+ width: 44,
238
+ alignItems: 'flex-start',
239
+ justifyContent: 'center',
240
+ },
241
+ center: {
242
+ flex: 1,
243
+ alignItems: 'center',
244
+ justifyContent: 'center',
245
+ },
246
+ right: {
247
+ minWidth: 44,
248
+ flexDirection: 'row',
249
+ alignItems: 'center',
250
+ justifyContent: 'flex-end',
251
+ },
252
+ backButton: {
253
+ alignItems: 'center',
254
+ justifyContent: 'center',
255
+ },
256
+ stackTitle: {
257
+ fontWeight: '600',
258
+ textAlign: 'center',
259
+ },
260
+
261
+ /* ─── Tab 모드 ─── */
262
+ tabLeft: {
263
+ flex: 1,
264
+ justifyContent: 'center',
265
+ },
266
+ tabTitle: {
267
+ fontWeight: '700',
268
+ },
269
+
270
+ /* ─── 공통 ─── */
271
+ actionButton: {
272
+ alignItems: 'center',
273
+ justifyContent: 'center',
274
+ borderRadius: 8,
275
+ },
276
+ });
277
+
278
+ export default Navbar;
@@ -0,0 +1,2 @@
1
+ export { Navbar } from './Navbar';
2
+ export type { NavbarProps, NavbarAction } from './Navbar';