internhub-v2-mobile-ui-kit 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -0
- package/package.json +2 -2
- package/src/Avatar/Avatar.tsx +56 -0
- package/src/Badge/Badge.tsx +59 -0
- package/src/DatePicker/DatePicker.tsx +0 -0
- package/src/Divider/Divider.tsx +31 -0
- package/src/EmptyState/EmptyState.tsx +73 -0
- package/src/Select/Select.tsx +85 -0
- package/src/TimePicker/TimePicker.tsx +0 -0
- package/src/Toast/Toast.tsx +0 -0
- package/src/index.ts +13 -1
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# 🎨 InternHub v2 Mobile UI Kit
|
|
2
|
+
|
|
3
|
+
Bộ thư viện giao diện chuẩn (Design System) dành cho ứng dụng InternHub v2 Mobile.
|
|
4
|
+
Được xây dựng trên React Native, hỗ trợ TypeScript và Dark Mode (future proof).
|
|
5
|
+
|
|
6
|
+
## 📦 Cài Đặt
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install internhub-v2-mobile-ui-kit
|
|
10
|
+
# Hoặc
|
|
11
|
+
yarn add internhub-v2-mobile-ui-kit
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Yêu cầu cài thêm thư viện bổ trợ (Peer Dependencies):
|
|
15
|
+
```bash
|
|
16
|
+
yarn add react-native-linear-gradient react-native-svg
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 🚀 Sử Dụng
|
|
20
|
+
|
|
21
|
+
### 1. Màu Sắc & Tokens
|
|
22
|
+
Dùng `Colors` để đảm bảo đồng bộ giao diện toàn App.
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { Colors, Spacing } from 'internhub-v2-mobile-ui-kit';
|
|
26
|
+
|
|
27
|
+
const styles = StyleSheet.create({
|
|
28
|
+
container: {
|
|
29
|
+
backgroundColor: Colors.background, // Màu kem mềm
|
|
30
|
+
padding: Spacing.md,
|
|
31
|
+
},
|
|
32
|
+
title: {
|
|
33
|
+
color: Colors.primary, // Màu cam thương hiệu (#E18308)
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Components Cơ Bản
|
|
39
|
+
|
|
40
|
+
#### Button
|
|
41
|
+
Button chuẩn có hỗ trợ gradient và các trạng thái loading/disabled.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { Button } from 'internhub-v2-mobile-ui-kit';
|
|
45
|
+
|
|
46
|
+
<Button
|
|
47
|
+
title="Đăng Nhập"
|
|
48
|
+
onPress={handleLogin}
|
|
49
|
+
variant="primary" // primary | secondary | outline | ghost
|
|
50
|
+
loading={isLoading}
|
|
51
|
+
/>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
#### Input
|
|
55
|
+
Ô nhập liệu chuẩn form.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { Input } from 'internhub-v2-mobile-ui-kit';
|
|
59
|
+
|
|
60
|
+
<Input
|
|
61
|
+
label="Họ tên"
|
|
62
|
+
placeholder="Nhập họ tên của bạn"
|
|
63
|
+
error={errors.fullName}
|
|
64
|
+
icon="user"
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### Card
|
|
69
|
+
Thẻ nội dung có đổ bóng và bo góc chuẩn.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { Card } from 'internhub-v2-mobile-ui-kit';
|
|
73
|
+
|
|
74
|
+
<Card variant="elevated">
|
|
75
|
+
<Text>Nội dung thẻ chấm công...</Text>
|
|
76
|
+
</Card>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### Badge & Avatar
|
|
80
|
+
Dùng cho status và ảnh đại diện.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { Badge, Avatar } from 'internhub-v2-mobile-ui-kit';
|
|
84
|
+
|
|
85
|
+
<Avatar name="Nhan Nguyen" size="md" />
|
|
86
|
+
<Badge label="Đang chờ duyệt" type="warning" />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 🧩 Danh Sách Components
|
|
90
|
+
|
|
91
|
+
| Component | Trạng Thái | Mô Tả |
|
|
92
|
+
|-----------|------------|-------|
|
|
93
|
+
| `Alert` | ✅ Ready | Hiển thị thông báo warning/info |
|
|
94
|
+
| `Avatar` | ✅ Ready | Ảnh đại diện tròn hoặc chữ cái đầu |
|
|
95
|
+
| `Badge` | ✅ Ready | Nhãn trạng thái (Success, Error...) |
|
|
96
|
+
| `Button` | ✅ Ready | Nút bấm chính/phụ |
|
|
97
|
+
| `Card` | ✅ Ready | Khung chứa nội dung container |
|
|
98
|
+
| `Divider` | ✅ Ready | Đường kẻ phân cách (mới) |
|
|
99
|
+
| `EmptyState`| ✅ Ready | Màn hình hiển thị khi không có dữ liệu (mới)|
|
|
100
|
+
| `Header` | ✅ Ready | Thanh tiêu đề điều hướng (mới) |
|
|
101
|
+
| `Input` | ✅ Ready | Ô nhập liệu văn bản |
|
|
102
|
+
| `Loading` | ✅ Ready | Vòng xoay chờ tải trang (mới) |
|
|
103
|
+
| `Modal` | ✅ Ready | Hộp thoại popup |
|
|
104
|
+
| `Select` | ✅ Ready | Dropdown chọn options |
|
|
105
|
+
| `Toast` | 🚧 W.I.P | Thông báo nổi tự tắt |
|
|
106
|
+
|
|
107
|
+
## 👨💻 Tác Giả
|
|
108
|
+
**Nguyen Thanh Nhan** - InternHub Team
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "internhub-v2-mobile-ui-kit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Premium UI Kit for InternHub Mobile App",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -23,4 +23,4 @@
|
|
|
23
23
|
"react-native-linear-gradient": "^2.8.3",
|
|
24
24
|
"react-native-svg": "^14.1.0"
|
|
25
25
|
}
|
|
26
|
-
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Image, Text, StyleSheet, ImageSourcePropType, ViewStyle } from 'react-native';
|
|
3
|
+
import { Colors } from '../tokens';
|
|
4
|
+
|
|
5
|
+
interface AvatarProps {
|
|
6
|
+
source?: ImageSourcePropType;
|
|
7
|
+
name?: string;
|
|
8
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
9
|
+
style?: ViewStyle;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const SIZES = {
|
|
13
|
+
sm: 32,
|
|
14
|
+
md: 48,
|
|
15
|
+
lg: 80,
|
|
16
|
+
xl: 120,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const Avatar: React.FC<AvatarProps> = ({ source, name, size = 'md', style }) => {
|
|
20
|
+
const dimension = SIZES[size];
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<View style={[
|
|
24
|
+
styles.container,
|
|
25
|
+
{ width: dimension, height: dimension, borderRadius: dimension / 2 },
|
|
26
|
+
style
|
|
27
|
+
]}>
|
|
28
|
+
{source ? (
|
|
29
|
+
<Image source={source} style={styles.image} resizeMode="cover" />
|
|
30
|
+
) : (
|
|
31
|
+
<Text style={[styles.text, { fontSize: dimension * 0.4 }]}>
|
|
32
|
+
{name ? name.charAt(0).toUpperCase() : '?'}
|
|
33
|
+
</Text>
|
|
34
|
+
)}
|
|
35
|
+
</View>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const styles = StyleSheet.create({
|
|
40
|
+
container: {
|
|
41
|
+
backgroundColor: Colors.avatarBg,
|
|
42
|
+
justifyContent: 'center',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
overflow: 'hidden',
|
|
45
|
+
borderWidth: 1.5,
|
|
46
|
+
borderColor: Colors.primary,
|
|
47
|
+
},
|
|
48
|
+
image: {
|
|
49
|
+
width: '100%',
|
|
50
|
+
height: '100%',
|
|
51
|
+
},
|
|
52
|
+
text: {
|
|
53
|
+
fontWeight: '800',
|
|
54
|
+
color: Colors.primary,
|
|
55
|
+
}
|
|
56
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, StyleSheet, ViewStyle, TextStyle } from 'react-native';
|
|
3
|
+
import { Colors } from '../tokens';
|
|
4
|
+
|
|
5
|
+
export type BadgeType = 'success' | 'warning' | 'error' | 'info' | 'default';
|
|
6
|
+
|
|
7
|
+
interface BadgeProps {
|
|
8
|
+
label: string;
|
|
9
|
+
type?: BadgeType;
|
|
10
|
+
size?: 'sm' | 'md';
|
|
11
|
+
style?: ViewStyle;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const BADGE_COLORS = {
|
|
15
|
+
success: { bg: Colors.successLight, text: Colors.success },
|
|
16
|
+
warning: { bg: Colors.warningLight, text: Colors.warning },
|
|
17
|
+
error: { bg: Colors.errorLight, text: Colors.error },
|
|
18
|
+
info: { bg: Colors.infoLight, text: Colors.info },
|
|
19
|
+
default: { bg: Colors.surfaceAlt, text: Colors.textSecondary },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Badge: React.FC<BadgeProps> = ({ label, type = 'default', size = 'sm', style }) => {
|
|
23
|
+
const colors = BADGE_COLORS[type] || BADGE_COLORS.default;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<View style={[
|
|
27
|
+
styles.container,
|
|
28
|
+
{ backgroundColor: colors.bg },
|
|
29
|
+
size === 'md' ? styles.md : styles.sm,
|
|
30
|
+
style
|
|
31
|
+
]}>
|
|
32
|
+
<Text style={[
|
|
33
|
+
styles.text,
|
|
34
|
+
{ color: colors.text },
|
|
35
|
+
(size === 'md' ? { fontSize: 12 } : { fontSize: 10 }) as TextStyle
|
|
36
|
+
]}>{label}</Text>
|
|
37
|
+
</View>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const styles = StyleSheet.create({
|
|
42
|
+
container: {
|
|
43
|
+
borderRadius: 99,
|
|
44
|
+
justifyContent: 'center',
|
|
45
|
+
alignItems: 'center',
|
|
46
|
+
alignSelf: 'flex-start',
|
|
47
|
+
},
|
|
48
|
+
sm: {
|
|
49
|
+
paddingHorizontal: 8,
|
|
50
|
+
paddingVertical: 4,
|
|
51
|
+
},
|
|
52
|
+
md: {
|
|
53
|
+
paddingHorizontal: 12,
|
|
54
|
+
paddingVertical: 6,
|
|
55
|
+
},
|
|
56
|
+
text: {
|
|
57
|
+
fontWeight: '700',
|
|
58
|
+
}
|
|
59
|
+
});
|
|
Binary file
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet, ViewStyle } from 'react-native';
|
|
3
|
+
import { Colors } from '../tokens';
|
|
4
|
+
|
|
5
|
+
interface DividerProps {
|
|
6
|
+
vertical?: boolean;
|
|
7
|
+
color?: string;
|
|
8
|
+
size?: number; // thickness
|
|
9
|
+
spacing?: number; // margin
|
|
10
|
+
style?: ViewStyle;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Divider: React.FC<DividerProps> = ({
|
|
14
|
+
vertical = false,
|
|
15
|
+
color = Colors.border,
|
|
16
|
+
size = 1,
|
|
17
|
+
spacing = 16,
|
|
18
|
+
style
|
|
19
|
+
}) => {
|
|
20
|
+
return (
|
|
21
|
+
<View
|
|
22
|
+
style={[
|
|
23
|
+
vertical
|
|
24
|
+
? { height: '100%', width: size, marginHorizontal: spacing }
|
|
25
|
+
: { width: '100%', height: size, marginVertical: spacing },
|
|
26
|
+
{ backgroundColor: color },
|
|
27
|
+
style
|
|
28
|
+
]}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, StyleSheet, Image, ImageSourcePropType, ViewStyle } from 'react-native';
|
|
3
|
+
import { Colors } from '../tokens';
|
|
4
|
+
|
|
5
|
+
interface EmptyStateProps {
|
|
6
|
+
title?: string;
|
|
7
|
+
message: string;
|
|
8
|
+
image?: ImageSourcePropType;
|
|
9
|
+
style?: ViewStyle;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const EmptyState: React.FC<EmptyStateProps> = ({
|
|
13
|
+
title,
|
|
14
|
+
message,
|
|
15
|
+
image,
|
|
16
|
+
style
|
|
17
|
+
}) => {
|
|
18
|
+
return (
|
|
19
|
+
<View style={[styles.container, style]}>
|
|
20
|
+
<View style={styles.circle}>
|
|
21
|
+
{image ? (
|
|
22
|
+
<Image source={image} style={styles.image} resizeMode="contain" />
|
|
23
|
+
) : (
|
|
24
|
+
<Text style={styles.icon}>📋</Text>
|
|
25
|
+
)}
|
|
26
|
+
</View>
|
|
27
|
+
{title && <Text style={styles.title}>{title}</Text>}
|
|
28
|
+
<Text style={styles.message}>{message}</Text>
|
|
29
|
+
</View>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const styles = StyleSheet.create({
|
|
34
|
+
container: {
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
justifyContent: 'center',
|
|
37
|
+
padding: 32,
|
|
38
|
+
flex: 1,
|
|
39
|
+
},
|
|
40
|
+
circle: {
|
|
41
|
+
width: 120,
|
|
42
|
+
height: 120,
|
|
43
|
+
backgroundColor: Colors.surface,
|
|
44
|
+
borderRadius: 60,
|
|
45
|
+
justifyContent: 'center',
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
marginBottom: 24,
|
|
48
|
+
borderWidth: 2,
|
|
49
|
+
borderColor: Colors.border,
|
|
50
|
+
borderStyle: 'dashed',
|
|
51
|
+
},
|
|
52
|
+
image: {
|
|
53
|
+
width: 80,
|
|
54
|
+
height: 80,
|
|
55
|
+
},
|
|
56
|
+
icon: {
|
|
57
|
+
fontSize: 48,
|
|
58
|
+
},
|
|
59
|
+
title: {
|
|
60
|
+
fontSize: 20,
|
|
61
|
+
fontWeight: '700',
|
|
62
|
+
color: Colors.textPrimary,
|
|
63
|
+
marginBottom: 8,
|
|
64
|
+
textAlign: 'center',
|
|
65
|
+
},
|
|
66
|
+
message: {
|
|
67
|
+
fontSize: 15,
|
|
68
|
+
color: Colors.textSecondary,
|
|
69
|
+
textAlign: 'center',
|
|
70
|
+
lineHeight: 22,
|
|
71
|
+
maxWidth: 280,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity, StyleSheet, Modal, FlatList, ViewStyle } from 'react-native';
|
|
3
|
+
import { Colors } from '../tokens';
|
|
4
|
+
|
|
5
|
+
interface Option {
|
|
6
|
+
label: string;
|
|
7
|
+
value: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface SelectProps {
|
|
11
|
+
options: Option[];
|
|
12
|
+
value: any;
|
|
13
|
+
onChange: (value: any) => void;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
style?: ViewStyle;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Select: React.FC<SelectProps> = ({ options, value, onChange, placeholder = 'Select...', label, style }) => {
|
|
20
|
+
const [visible, setVisible] = React.useState(false);
|
|
21
|
+
const selected = options.find(o => o.value === value);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<View style={[styles.wrapper, style]}>
|
|
25
|
+
{label && <Text style={styles.label}>{label}</Text>}
|
|
26
|
+
<TouchableOpacity
|
|
27
|
+
style={styles.field}
|
|
28
|
+
onPress={() => setVisible(true)}
|
|
29
|
+
activeOpacity={0.8}
|
|
30
|
+
>
|
|
31
|
+
<Text style={[styles.text, !selected && styles.placeholder]}>
|
|
32
|
+
{selected ? selected.label : placeholder}
|
|
33
|
+
</Text>
|
|
34
|
+
</TouchableOpacity>
|
|
35
|
+
|
|
36
|
+
<Modal visible={visible} transparent animationType="fade">
|
|
37
|
+
<View style={styles.modalOverlay}>
|
|
38
|
+
<TouchableOpacity style={styles.modalBg} onPress={() => setVisible(false)} />
|
|
39
|
+
<View style={styles.modalContent}>
|
|
40
|
+
<Text style={styles.header}>{label || placeholder}</Text>
|
|
41
|
+
<FlatList
|
|
42
|
+
data={options}
|
|
43
|
+
keyExtractor={(item) => String(item.value)}
|
|
44
|
+
renderItem={({ item }) => (
|
|
45
|
+
<TouchableOpacity
|
|
46
|
+
style={[styles.option, item.value === value && styles.selectedOption]}
|
|
47
|
+
onPress={() => {
|
|
48
|
+
onChange(item.value);
|
|
49
|
+
setVisible(false);
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<Text style={[styles.optionText, item.value === value && styles.selectedOptionText]}>
|
|
53
|
+
{item.label}
|
|
54
|
+
</Text>
|
|
55
|
+
</TouchableOpacity>
|
|
56
|
+
)}
|
|
57
|
+
/>
|
|
58
|
+
</View>
|
|
59
|
+
</View>
|
|
60
|
+
</Modal>
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const styles = StyleSheet.create({
|
|
66
|
+
wrapper: { marginBottom: 16 },
|
|
67
|
+
label: { marginBottom: 8, fontSize: 14, fontWeight: '600', color: Colors.textSecondary },
|
|
68
|
+
field: {
|
|
69
|
+
borderWidth: 1,
|
|
70
|
+
borderColor: Colors.border,
|
|
71
|
+
borderRadius: 8,
|
|
72
|
+
padding: 12,
|
|
73
|
+
backgroundColor: Colors.inputBackground
|
|
74
|
+
},
|
|
75
|
+
text: { fontSize: 16, color: Colors.textPrimary },
|
|
76
|
+
placeholder: { color: Colors.textDisabled },
|
|
77
|
+
modalOverlay: { flex: 1, justifyContent: 'center', backgroundColor: 'rgba(0,0,0,0.5)', padding: 20 },
|
|
78
|
+
modalBg: { ...StyleSheet.absoluteFillObject },
|
|
79
|
+
modalContent: { backgroundColor: Colors.surface, borderRadius: 12, maxHeight: 400, padding: 16 },
|
|
80
|
+
header: { fontSize: 18, fontWeight: '700', marginBottom: 12, textAlign: 'center' },
|
|
81
|
+
option: { paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: Colors.borderAlt },
|
|
82
|
+
selectedOption: { backgroundColor: Colors.glassOrange },
|
|
83
|
+
optionText: { fontSize: 16, color: Colors.textPrimary },
|
|
84
|
+
selectedOptionText: { color: Colors.primary, fontWeight: '700' },
|
|
85
|
+
});
|
|
Binary file
|
|
Binary file
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Design Tokens
|
|
2
2
|
export * from './tokens';
|
|
3
3
|
|
|
4
|
-
// Components
|
|
4
|
+
// Core Components
|
|
5
5
|
export { Button } from './Button/Button';
|
|
6
6
|
export { Input } from './Input/Input';
|
|
7
7
|
export { Card } from './Card/Card';
|
|
@@ -10,3 +10,15 @@ export { Alert } from './Alert/Alert';
|
|
|
10
10
|
export { GradientBackground } from './GradientBackground/GradientBackground';
|
|
11
11
|
export { Modal } from './Modal/Modal';
|
|
12
12
|
export { PasswordChecker } from './PasswordChecker/PasswordChecker';
|
|
13
|
+
|
|
14
|
+
// New Components (v0.0.3)
|
|
15
|
+
export { Avatar } from './Avatar/Avatar';
|
|
16
|
+
export { Badge } from './Badge/Badge';
|
|
17
|
+
export { Select } from './Select/Select';
|
|
18
|
+
export { Divider } from './Divider/Divider';
|
|
19
|
+
export { EmptyState } from './EmptyState/EmptyState';
|
|
20
|
+
export { Header } from './Header/Header';
|
|
21
|
+
export { Loading } from './Loading/Loading';
|
|
22
|
+
export { DatePicker } from './DatePicker/DatePicker';
|
|
23
|
+
export { TimePicker } from './TimePicker/TimePicker';
|
|
24
|
+
export { Toast } from './Toast/Toast';
|