@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,99 @@
1
+ /**
2
+ * ProgressBar 컴포넌트 (React Native)
3
+ *
4
+ * 스타일링 토큰 경로:
5
+ * - 트랙: theme.color.aliases.surface.{layer-2}
6
+ * - 바: theme.color.aliases.{brand.primary|feedback.*.fg}
7
+ * - 라운드: theme.border.semantic.radius.full
8
+ * - 폰트: theme.typography.foundation.fontSize.xs
9
+ * - 텍스트: theme.color.aliases.text.secondary
10
+ */
11
+
12
+ import React, { useRef, useEffect } from 'react';
13
+ import { View, Text, Animated, StyleSheet, type ViewStyle } from 'react-native';
14
+ import { useTheme } from '../../theme/useTheme';
15
+
16
+ export type ProgressBarVariant = 'primary' | 'success' | 'warning' | 'danger' | 'info';
17
+ export type ProgressBarSize = 's' | 'm' | 'l';
18
+
19
+ export interface ProgressBarProps {
20
+ value: number;
21
+ max?: number;
22
+ variant?: ProgressBarVariant;
23
+ size?: ProgressBarSize;
24
+ showLabel?: boolean;
25
+ label?: string;
26
+ animated?: boolean;
27
+ style?: ViewStyle;
28
+ }
29
+
30
+ export const ProgressBar: React.FC<ProgressBarProps> = ({
31
+ value, max = 100, variant = 'primary', size = 'm',
32
+ showLabel = false, label, animated = true, style,
33
+ }) => {
34
+ const { theme } = useTheme();
35
+ const animatedWidth = useRef(new Animated.Value(0)).current;
36
+
37
+ const surface = theme.color.aliases?.surface ?? {};
38
+ const brand = theme.color.aliases?.brand ?? {};
39
+ const feedback = theme.color.aliases?.feedback ?? {};
40
+ const textColors = theme.color.aliases?.text ?? {};
41
+ const fontSize = theme.typography.foundation?.fontSize ?? {};
42
+
43
+ const percentage = Math.min(100, Math.max(0, (value / max) * 100));
44
+ const trackHeight = { s: 4, m: 8, l: 12 }[size];
45
+
46
+ const barColors: Record<ProgressBarVariant, string> = {
47
+ primary: brand.primary ?? '#006FFF',
48
+ success: feedback.success?.fg ?? '#16A34A',
49
+ warning: feedback.warning?.fg ?? '#D1B400',
50
+ danger: feedback.error?.fg ?? '#DC2626',
51
+ info: feedback.info?.fg ?? '#0683FF',
52
+ };
53
+
54
+ useEffect(() => {
55
+ if (animated) {
56
+ Animated.spring(animatedWidth, { toValue: percentage, useNativeDriver: false, friction: 10, tension: 40 }).start();
57
+ } else {
58
+ animatedWidth.setValue(percentage);
59
+ }
60
+ }, [percentage, animated, animatedWidth]);
61
+
62
+ const barWidthInterpolation = animatedWidth.interpolate({ inputRange: [0, 100], outputRange: ['0%', '100%'] });
63
+
64
+ return (
65
+ <View style={[styles.container, style]} accessibilityRole="progressbar"
66
+ accessibilityValue={{ min: 0, max, now: value }}>
67
+ {(showLabel || label) && (
68
+ <View style={styles.labelRow}>
69
+ <Text style={{ fontSize: fontSize.xs ?? 12, color: textColors.secondary ?? '#464A4D' }}>
70
+ {label ?? `${Math.round(percentage)}%`}
71
+ </Text>
72
+ {label && showLabel && (
73
+ <Text style={{ fontSize: fontSize.xs ?? 12, color: textColors.tertiary ?? '#757B80' }}>
74
+ {Math.round(percentage)}%
75
+ </Text>
76
+ )}
77
+ </View>
78
+ )}
79
+ <View style={[styles.track, { height: trackHeight, borderRadius: trackHeight / 2, backgroundColor: surface['layer-2'] ?? '#E8EEF2' }]}>
80
+ <Animated.View style={[styles.bar, {
81
+ height: trackHeight, borderRadius: trackHeight / 2,
82
+ backgroundColor: barColors[variant],
83
+ width: barWidthInterpolation,
84
+ }]} />
85
+ </View>
86
+ </View>
87
+ );
88
+ };
89
+
90
+ ProgressBar.displayName = 'ProgressBar';
91
+
92
+ const styles = StyleSheet.create({
93
+ container: { width: '100%' },
94
+ labelRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 },
95
+ track: { overflow: 'hidden' },
96
+ bar: {},
97
+ });
98
+
99
+ export default ProgressBar;
@@ -0,0 +1,2 @@
1
+ export { ProgressBar } from './ProgressBar';
2
+ export type { ProgressBarProps, ProgressBarVariant, ProgressBarSize } from './ProgressBar';
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Radio 컴포넌트 (React Native)
3
+ *
4
+ * 스타일링 토큰 경로:
5
+ * - 색상: theme.color.aliases.{brand.primary|border.base|surface.base}
6
+ * - 폰트: theme.typography.foundation.fontSize.*
7
+ * - 간격: theme.spacing.aliases.gap.*
8
+ */
9
+
10
+ import React from 'react';
11
+ import { View, Text, Pressable, StyleSheet, type ViewStyle } from 'react-native';
12
+ import { useTheme } from '../../theme/useTheme';
13
+
14
+ export type RadioSize = 's' | 'm' | 'l';
15
+
16
+ export interface RadioOption {
17
+ value: string;
18
+ label: string;
19
+ description?: string;
20
+ disabled?: boolean;
21
+ }
22
+
23
+ export interface RadioProps {
24
+ options: RadioOption[];
25
+ value?: string;
26
+ defaultValue?: string;
27
+ onChange?: (value: string) => void;
28
+ size?: RadioSize;
29
+ disabled?: boolean;
30
+ direction?: 'vertical' | 'horizontal';
31
+ style?: ViewStyle;
32
+ }
33
+
34
+ export const Radio: React.FC<RadioProps> = ({
35
+ options, value: controlledValue, defaultValue, onChange,
36
+ size = 'm', disabled = false, direction = 'vertical', style,
37
+ }) => {
38
+ const { theme } = useTheme();
39
+ const [internalValue, setInternalValue] = React.useState(controlledValue ?? defaultValue ?? '');
40
+ const selectedValue = controlledValue ?? internalValue;
41
+
42
+ const brand = theme.color.aliases?.brand ?? {};
43
+ const borders = theme.color.aliases?.border ?? {};
44
+ const surface = theme.color.aliases?.surface ?? {};
45
+ const textColors = theme.color.aliases?.text ?? {};
46
+ const fontSize = theme.typography.foundation?.fontSize ?? {};
47
+ const gap = theme.spacing.aliases?.gap ?? {};
48
+ const disabledOpacity = theme.opacity.foundation?.[40] ?? 0.4;
49
+
50
+ const outerSize = { s: 16, m: 20, l: 24 }[size];
51
+ const innerSize = { s: 8, m: 10, l: 14 }[size];
52
+ const labelFontSize = { s: fontSize.xs ?? 12, m: fontSize.s ?? 14, l: fontSize.base ?? 16 }[size];
53
+
54
+ const handleSelect = (val: string) => {
55
+ if (disabled) return;
56
+ setInternalValue(val);
57
+ onChange?.(val);
58
+ };
59
+
60
+ return (
61
+ <View style={[
62
+ direction === 'horizontal' ? styles.horizontal : styles.vertical,
63
+ { gap: gap.m ?? 12 }, style,
64
+ ]} accessibilityRole="radiogroup">
65
+ {options.map((opt) => {
66
+ const isSelected = selectedValue === opt.value;
67
+ const isDisabled = disabled || opt.disabled;
68
+ return (
69
+ <Pressable key={opt.value} onPress={() => handleSelect(opt.value)} disabled={isDisabled}
70
+ accessibilityRole="radio" accessibilityState={{ checked: isSelected, disabled: isDisabled }}
71
+ style={[styles.option, { gap: gap.s ?? 8 }, isDisabled && { opacity: disabledOpacity }]}>
72
+ <View style={[styles.outer, {
73
+ width: outerSize, height: outerSize, borderRadius: outerSize / 2,
74
+ borderColor: isSelected ? (brand.primary ?? '#006FFF') : (borders.base ?? '#E8EEF2'),
75
+ backgroundColor: surface.base ?? '#FFFFFF',
76
+ }]}>
77
+ {isSelected && (
78
+ <View style={[styles.inner, { width: innerSize, height: innerSize, borderRadius: innerSize / 2, backgroundColor: brand.primary ?? '#006FFF' }]} />
79
+ )}
80
+ </View>
81
+ <View style={styles.labelContainer}>
82
+ <Text style={{ fontSize: labelFontSize, color: textColors.primary ?? '#17191A' }}>{opt.label}</Text>
83
+ {opt.description && <Text style={{ fontSize: fontSize.xs ?? 12, color: textColors.tertiary ?? '#757B80' }}>{opt.description}</Text>}
84
+ </View>
85
+ </Pressable>
86
+ );
87
+ })}
88
+ </View>
89
+ );
90
+ };
91
+
92
+ Radio.displayName = 'Radio';
93
+
94
+ const styles = StyleSheet.create({
95
+ vertical: {},
96
+ horizontal: { flexDirection: 'row', flexWrap: 'wrap' },
97
+ option: { flexDirection: 'row', alignItems: 'flex-start' },
98
+ outer: { borderWidth: 2, alignItems: 'center', justifyContent: 'center' },
99
+ inner: {},
100
+ labelContainer: { flex: 1, gap: 2, paddingTop: 1 },
101
+ });
102
+
103
+ export default Radio;
@@ -0,0 +1,2 @@
1
+ export { Radio } from './Radio';
2
+ export type { RadioProps, RadioOption, RadioSize } from './Radio';
@@ -0,0 +1,115 @@
1
+ /**
2
+ * SearchBar 컴포넌트 (React Native)
3
+ *
4
+ * 스타일링 토큰 경로:
5
+ * - 색상: theme.color.semantic.field.input.*
6
+ * - 사이즈: theme.size.semantic.search.{s|m|l}
7
+ * - 라운드: theme.border.semantic.radius.{l|full}
8
+ * - 패딩: theme.spacing.aliases.padding.*
9
+ * - 폰트: theme.typography.foundation.fontSize.*
10
+ */
11
+
12
+ import React, { useState } from 'react';
13
+ import { View, TextInput, Text, Pressable, StyleSheet, type ViewStyle } from 'react-native';
14
+ import { useTheme } from '../../theme/useTheme';
15
+
16
+ export type SearchBarSize = 's' | 'm' | 'l';
17
+
18
+ export interface SearchBarProps {
19
+ value?: string;
20
+ defaultValue?: string;
21
+ placeholder?: string;
22
+ size?: SearchBarSize;
23
+ onChangeText?: (text: string) => void;
24
+ onSubmit?: (text: string) => void;
25
+ onClear?: () => void;
26
+ disabled?: boolean;
27
+ showClearButton?: boolean;
28
+ startIcon?: React.ReactNode;
29
+ style?: ViewStyle;
30
+ }
31
+
32
+ export const SearchBar: React.FC<SearchBarProps> = ({
33
+ value: controlledValue, defaultValue = '', placeholder = '검색...',
34
+ size = 'm', onChangeText, onSubmit, onClear, disabled = false,
35
+ showClearButton = true, startIcon, style,
36
+ }) => {
37
+ const { theme } = useTheme();
38
+ const [internalValue, setInternalValue] = useState(controlledValue ?? defaultValue);
39
+ const [isFocused, setIsFocused] = useState(false);
40
+ const currentValue = controlledValue ?? internalValue;
41
+
42
+ const fieldTokens = theme.color.semantic?.field?.input ?? {};
43
+ const aliases = theme.color.aliases ?? {};
44
+ const searchSize = theme.size.semantic?.search ?? {};
45
+ const padding = theme.spacing.aliases?.padding ?? {};
46
+ const fontSize = theme.typography.foundation?.fontSize ?? {};
47
+ const disabledOpacity = theme.opacity.foundation?.[40] ?? 0.4;
48
+
49
+ const sizeConfig = {
50
+ s: { height: searchSize.s ?? 32, fontSize: fontSize.xs ?? 12, paddingH: padding.s ?? 8 },
51
+ m: { height: searchSize.m ?? 40, fontSize: fontSize.s ?? 14, paddingH: padding.m ?? 12 },
52
+ l: { height: searchSize.l ?? 48, fontSize: fontSize.base ?? 16, paddingH: padding.l ?? 20 },
53
+ }[size];
54
+
55
+ const bgColor = disabled
56
+ ? (fieldTokens['bg-disabled'] ?? aliases.surface?.muted ?? '#E8EEF2')
57
+ : (fieldTokens['bg-default'] ?? aliases.surface?.['layer-1'] ?? '#F2F8FC');
58
+ const borderColor = isFocused
59
+ ? (fieldTokens['border-focus'] ?? aliases.brand?.primary ?? '#006FFF')
60
+ : 'transparent';
61
+ const placeholderColor = fieldTokens?.placeholder ?? aliases.text?.tertiary ?? '#757B80';
62
+ const textColor = fieldTokens?.['text-default'] ?? aliases.text?.primary ?? '#17191A';
63
+
64
+ const handleChange = (text: string) => {
65
+ setInternalValue(text);
66
+ onChangeText?.(text);
67
+ };
68
+
69
+ const handleClear = () => {
70
+ setInternalValue('');
71
+ onChangeText?.('');
72
+ onClear?.();
73
+ };
74
+
75
+ return (
76
+ <View style={[
77
+ styles.container,
78
+ {
79
+ height: sizeConfig.height, borderRadius: sizeConfig.height / 2,
80
+ backgroundColor: bgColor, paddingHorizontal: sizeConfig.paddingH,
81
+ borderWidth: isFocused ? 2 : 0, borderColor,
82
+ },
83
+ disabled && { opacity: disabledOpacity }, style,
84
+ ]}>
85
+ {startIcon ?? <Text style={{ fontSize: sizeConfig.fontSize, color: placeholderColor }}>🔍</Text>}
86
+ <TextInput
87
+ value={currentValue}
88
+ onChangeText={handleChange}
89
+ onSubmitEditing={() => onSubmit?.(currentValue)}
90
+ placeholder={placeholder}
91
+ placeholderTextColor={placeholderColor}
92
+ editable={!disabled}
93
+ onFocus={() => setIsFocused(true)}
94
+ onBlur={() => setIsFocused(false)}
95
+ returnKeyType="search"
96
+ style={[styles.input, { fontSize: sizeConfig.fontSize, color: textColor }]}
97
+ accessibilityLabel={placeholder}
98
+ />
99
+ {showClearButton && currentValue.length > 0 && (
100
+ <Pressable onPress={handleClear} hitSlop={8} accessibilityLabel="검색 지우기">
101
+ <Text style={{ fontSize: sizeConfig.fontSize, color: aliases.text?.tertiary ?? '#757B80' }}>✕</Text>
102
+ </Pressable>
103
+ )}
104
+ </View>
105
+ );
106
+ };
107
+
108
+ SearchBar.displayName = 'SearchBar';
109
+
110
+ const styles = StyleSheet.create({
111
+ container: { flexDirection: 'row', alignItems: 'center', gap: 8 },
112
+ input: { flex: 1, height: '100%', padding: 0 },
113
+ });
114
+
115
+ export default SearchBar;
@@ -0,0 +1,2 @@
1
+ export { SearchBar } from './SearchBar';
2
+ export type { SearchBarProps, SearchBarSize } from './SearchBar';
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Skeleton 컴포넌트 (React Native)
3
+ *
4
+ * 스타일링 토큰 경로:
5
+ * - 배경: theme.color.aliases.surface.{layer-1|layer-2}
6
+ * - 라운드: theme.border.semantic.radius.{s|m|l}
7
+ */
8
+
9
+ import React, { useRef, useEffect } from 'react';
10
+ import { View, Animated, StyleSheet, type ViewStyle } from 'react-native';
11
+ import { useTheme } from '../../theme/useTheme';
12
+
13
+ export type SkeletonVariant = 'text' | 'rectangular' | 'circular';
14
+
15
+ export interface SkeletonProps {
16
+ width?: number | string;
17
+ height?: number | string;
18
+ variant?: SkeletonVariant;
19
+ borderRadius?: number;
20
+ count?: number;
21
+ gap?: number;
22
+ style?: ViewStyle;
23
+ }
24
+
25
+ export const Skeleton: React.FC<SkeletonProps> = ({
26
+ width = '100%', height = 16, variant = 'text',
27
+ borderRadius: customRadius, count = 1, gap: customGap, style,
28
+ }) => {
29
+ const { theme } = useTheme();
30
+ const animValue = useRef(new Animated.Value(0)).current;
31
+
32
+ const surface = theme.color.aliases?.surface ?? {};
33
+ const radiusTokens = theme.border.semantic?.radius ?? {};
34
+ const gapValue = customGap ?? theme.spacing.aliases?.gap?.s ?? 8;
35
+
36
+ const bgColor = surface['layer-1'] ?? '#F2F8FC';
37
+ const pulseColor = surface['layer-2'] ?? '#E8EEF2';
38
+
39
+ const resolvedRadius = customRadius ?? (() => {
40
+ switch (variant) {
41
+ case 'circular': return typeof width === 'number' ? width / 2 : 9999;
42
+ case 'rectangular': return radiusTokens.s ?? 4;
43
+ default: return radiusTokens.s ?? 4;
44
+ }
45
+ })();
46
+
47
+ const resolvedSize = variant === 'circular' ? { width: width ?? 40, height: width ?? 40 } : { width, height };
48
+
49
+ useEffect(() => {
50
+ const animation = Animated.loop(
51
+ Animated.sequence([
52
+ Animated.timing(animValue, { toValue: 1, duration: 800, useNativeDriver: false }),
53
+ Animated.timing(animValue, { toValue: 0, duration: 800, useNativeDriver: false }),
54
+ ])
55
+ );
56
+ animation.start();
57
+ return () => animation.stop();
58
+ }, [animValue]);
59
+
60
+ const backgroundColor = animValue.interpolate({ inputRange: [0, 1], outputRange: [bgColor, pulseColor] });
61
+
62
+ const items = Array.from({ length: count }, (_, i) => (
63
+ <Animated.View key={i} style={[
64
+ { ...resolvedSize, borderRadius: resolvedRadius, backgroundColor } as any,
65
+ i > 0 && { marginTop: gapValue },
66
+ style,
67
+ ]} />
68
+ ));
69
+
70
+ return count === 1 ? items[0]! : <View>{items}</View>;
71
+ };
72
+
73
+ Skeleton.displayName = 'Skeleton';
74
+ export default Skeleton;
@@ -0,0 +1,2 @@
1
+ export { Skeleton } from './Skeleton';
2
+ export type { SkeletonProps, SkeletonVariant } from './Skeleton';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Spinner 컴포넌트 (React Native)
3
+ *
4
+ * 스타일링 토큰 경로:
5
+ * - 색상: theme.color.aliases.brand.primary
6
+ * - 폰트: theme.typography.foundation.fontSize.xs
7
+ * - 텍스트: theme.color.aliases.text.secondary
8
+ *
9
+ * 원본: packages/ui/src/components/Spinner/Spinner.tsx
10
+ */
11
+
12
+ import React from 'react';
13
+ import { View, Text, ActivityIndicator, StyleSheet, type ViewStyle } from 'react-native';
14
+ import { useTheme } from '../../theme/useTheme';
15
+
16
+ export type SpinnerSize = 'xs' | 's' | 'm' | 'l' | 'xl';
17
+
18
+ export interface SpinnerProps {
19
+ size?: SpinnerSize;
20
+ color?: string;
21
+ label?: string;
22
+ showLabel?: boolean;
23
+ style?: ViewStyle;
24
+ }
25
+
26
+ export const Spinner: React.FC<SpinnerProps> = ({
27
+ size = 'm', color, label = '로딩 중...', showLabel = false, style,
28
+ }) => {
29
+ const { theme } = useTheme();
30
+
31
+ const spinnerColor = color ?? theme.color.aliases?.brand?.primary ?? '#006FFF';
32
+ const textColor = theme.color.aliases?.text?.secondary ?? '#464A4D';
33
+ const labelFontSize = theme.typography.foundation?.fontSize?.xs ?? 12;
34
+
35
+ const rnSize = size === 'xs' || size === 's' ? 'small' : 'large';
36
+ const scale = { xs: 0.6, s: 0.8, m: 1, l: 1.2, xl: 1.5 }[size];
37
+ const gap = theme.spacing.aliases?.gap?.s ?? 8;
38
+
39
+ return (
40
+ <View style={[styles.container, { gap }, style]}
41
+ accessibilityRole="progressbar" accessibilityLabel={label} accessibilityState={{ busy: true }}>
42
+ <View style={{ transform: [{ scale }] }}>
43
+ <ActivityIndicator size={rnSize} color={spinnerColor} />
44
+ </View>
45
+ {showLabel && label && (
46
+ <Text style={{ color: textColor, fontSize: labelFontSize }}>{label}</Text>
47
+ )}
48
+ </View>
49
+ );
50
+ };
51
+
52
+ Spinner.displayName = 'Spinner';
53
+
54
+ const styles = StyleSheet.create({
55
+ container: { alignItems: 'center', justifyContent: 'center' },
56
+ });
57
+
58
+ export default Spinner;
@@ -0,0 +1,2 @@
1
+ export { Spinner } from './Spinner';
2
+ export type { SpinnerProps, SpinnerSize } from './Spinner';
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Tabs 컴포넌트 (React Native)
3
+ *
4
+ * 스타일링 토큰 경로:
5
+ * - 색상: theme.color.aliases.{brand.primary|text.*|border.base|surface.*}
6
+ * - 폰트: theme.typography.foundation.fontSize.{s|base}
7
+ * - 간격: theme.spacing.aliases.padding.*
8
+ */
9
+
10
+ import React, { useState } from 'react';
11
+ import { View, Text, Pressable, ScrollView, StyleSheet, type ViewStyle } from 'react-native';
12
+ import { useTheme } from '../../theme/useTheme';
13
+
14
+ export interface TabItem {
15
+ key: string;
16
+ label: string;
17
+ icon?: React.ReactNode;
18
+ badge?: number;
19
+ disabled?: boolean;
20
+ }
21
+
22
+ export interface TabsProps {
23
+ items: TabItem[];
24
+ activeKey?: string;
25
+ defaultActiveKey?: string;
26
+ onChange?: (key: string) => void;
27
+ variant?: 'underline' | 'pill' | 'enclosed';
28
+ fullWidth?: boolean;
29
+ children?: React.ReactNode;
30
+ style?: ViewStyle;
31
+ }
32
+
33
+ export const Tabs: React.FC<TabsProps> = ({
34
+ items, activeKey, defaultActiveKey, onChange,
35
+ variant = 'underline', fullWidth = false, children, style,
36
+ }) => {
37
+ const { theme } = useTheme();
38
+ const [internalKey, setInternalKey] = useState(activeKey ?? defaultActiveKey ?? items[0]?.key ?? '');
39
+ const currentKey = activeKey ?? internalKey;
40
+
41
+ const brand = theme.color.aliases?.brand ?? {};
42
+ const textColors = theme.color.aliases?.text ?? {};
43
+ const borders = theme.color.aliases?.border ?? {};
44
+ const surface = theme.color.aliases?.surface ?? {};
45
+ const fontSize = theme.typography.foundation?.fontSize ?? {};
46
+ const padding = theme.spacing.aliases?.padding ?? {};
47
+ const disabledOpacity = theme.opacity.foundation?.[40] ?? 0.4;
48
+
49
+ const handlePress = (key: string) => {
50
+ setInternalKey(key);
51
+ onChange?.(key);
52
+ };
53
+
54
+ const renderTab = (item: TabItem) => {
55
+ const isActive = currentKey === item.key;
56
+ const tabColor = isActive ? (brand.primary ?? '#006FFF') : (textColors.tertiary ?? '#757B80');
57
+
58
+ const tabStyle: ViewStyle = (() => {
59
+ switch (variant) {
60
+ case 'pill':
61
+ return {
62
+ backgroundColor: isActive ? (brand.primary ?? '#006FFF') : 'transparent',
63
+ borderRadius: 9999, paddingHorizontal: padding.m ?? 12, paddingVertical: padding.xs ?? 4,
64
+ };
65
+ case 'enclosed':
66
+ return {
67
+ backgroundColor: isActive ? (surface.base ?? '#FFFFFF') : (surface['layer-1'] ?? '#F2F8FC'),
68
+ borderWidth: isActive ? 1 : 0, borderColor: borders.base ?? '#E8EEF2',
69
+ borderBottomWidth: isActive ? 0 : 1,
70
+ borderTopLeftRadius: 8, borderTopRightRadius: 8,
71
+ paddingHorizontal: padding.m ?? 12, paddingVertical: padding.s ?? 8,
72
+ };
73
+ default:
74
+ return {
75
+ borderBottomWidth: isActive ? 2 : 0, borderBottomColor: brand.primary ?? '#006FFF',
76
+ paddingHorizontal: padding.m ?? 12, paddingVertical: padding.s ?? 8,
77
+ };
78
+ }
79
+ })();
80
+
81
+ return (
82
+ <Pressable key={item.key} onPress={() => handlePress(item.key)} disabled={item.disabled}
83
+ accessibilityRole="tab" accessibilityState={{ selected: isActive, disabled: item.disabled }}
84
+ style={[styles.tab, tabStyle, fullWidth && styles.tabFull, item.disabled && { opacity: disabledOpacity }]}>
85
+ {item.icon}
86
+ <Text style={{
87
+ fontSize: fontSize.s ?? 14, fontWeight: isActive ? '600' : '400',
88
+ color: variant === 'pill' && isActive ? '#FFFFFF' : tabColor,
89
+ }}>{item.label}</Text>
90
+ {item.badge !== undefined && item.badge > 0 && (
91
+ <View style={[styles.badge, { backgroundColor: brand.primary ?? '#006FFF' }]}>
92
+ <Text style={styles.badgeText}>{item.badge > 99 ? '99+' : item.badge}</Text>
93
+ </View>
94
+ )}
95
+ </Pressable>
96
+ );
97
+ };
98
+
99
+ return (
100
+ <View style={style}>
101
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}
102
+ contentContainerStyle={[
103
+ styles.tabBar,
104
+ variant === 'underline' && { borderBottomWidth: 1, borderBottomColor: borders.base ?? '#E8EEF2' },
105
+ fullWidth && { flex: 1 },
106
+ ]}>
107
+ {items.map(renderTab)}
108
+ </ScrollView>
109
+ {children}
110
+ </View>
111
+ );
112
+ };
113
+
114
+ Tabs.displayName = 'Tabs';
115
+
116
+ const styles = StyleSheet.create({
117
+ tabBar: { flexDirection: 'row' },
118
+ tab: { flexDirection: 'row', alignItems: 'center', gap: 6 },
119
+ tabFull: { flex: 1, justifyContent: 'center' },
120
+ badge: { minWidth: 18, height: 18, borderRadius: 9, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 4 },
121
+ badgeText: { color: '#FFFFFF', fontSize: 10, fontWeight: '700' },
122
+ });
123
+
124
+ export default Tabs;
@@ -0,0 +1,2 @@
1
+ export { Tabs } from './Tabs';
2
+ export type { TabsProps, TabItem } from './Tabs';