@biruframework/native 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/button/Button.jsx +29 -0
- package/components/button/ButtonIcon.jsx +62 -0
- package/components/card/Card.jsx +31 -0
- package/components/dropdown/DropDown.jsx +145 -0
- package/components/index.js +9 -0
- package/components/list/List.jsx +123 -0
- package/components/navigation/NavigationBar.jsx +77 -0
- package/components/textfield/TextField.jsx +30 -0
- package/forms/index.jsx +21 -0
- package/hooks/index.js +1 -0
- package/hooks/useFocus.js +19 -0
- package/index.js +11 -0
- package/navigation/index.js +20 -0
- package/package.json +16 -0
- package/state/index.js +27 -0
- package/theme/Theme.js +15 -0
- package/theme/context.jsx +14 -0
- package/theme/index.js +2 -0
- package/ui/controls.jsx +73 -0
- package/ui/feedback.jsx +54 -0
- package/ui/index.js +7 -0
- package/ui/layout.jsx +42 -0
- package/ui/modalSheet.jsx +58 -0
- package/ui/screen.jsx +27 -0
- package/ui/toast.jsx +87 -0
- package/ui/typography.jsx +44 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Pressable, Text } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export function Button(props) {
|
|
4
|
+
const {
|
|
5
|
+
disabled,
|
|
6
|
+
loading,
|
|
7
|
+
onPress,
|
|
8
|
+
label,
|
|
9
|
+
style: styleProp,
|
|
10
|
+
textStyle: textStyleProp,
|
|
11
|
+
...rest
|
|
12
|
+
} = props;
|
|
13
|
+
|
|
14
|
+
const isDisabled = disabled || loading;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Pressable
|
|
18
|
+
onPress={onPress}
|
|
19
|
+
disabled={isDisabled}
|
|
20
|
+
style={styleProp}
|
|
21
|
+
{...rest}
|
|
22
|
+
>
|
|
23
|
+
<Text style={textStyleProp}>
|
|
24
|
+
{loading ? 'Loading…' : label}
|
|
25
|
+
</Text>
|
|
26
|
+
</Pressable>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Pressable, Text, View } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export function ButtonIcon(props) {
|
|
4
|
+
const {
|
|
5
|
+
disabled,
|
|
6
|
+
loading,
|
|
7
|
+
onPress,
|
|
8
|
+
label,
|
|
9
|
+
icon,
|
|
10
|
+
iconPosition = 'left',
|
|
11
|
+
style: styleProp,
|
|
12
|
+
textStyle: textStyleProp,
|
|
13
|
+
iconStyle: iconStyleProp,
|
|
14
|
+
...rest
|
|
15
|
+
} = props;
|
|
16
|
+
|
|
17
|
+
const isDisabled = disabled || loading;
|
|
18
|
+
|
|
19
|
+
const renderIcon = () => {
|
|
20
|
+
if (!icon) return null;
|
|
21
|
+
|
|
22
|
+
// Handle different icon types: React component, Image source, or string (emoji)
|
|
23
|
+
if (typeof icon === 'function' || typeof icon === 'object') {
|
|
24
|
+
const IconComponent = icon;
|
|
25
|
+
return (
|
|
26
|
+
<View style={iconStyleProp}>
|
|
27
|
+
{typeof IconComponent === 'function' ? (
|
|
28
|
+
<IconComponent />
|
|
29
|
+
) : (
|
|
30
|
+
icon
|
|
31
|
+
)}
|
|
32
|
+
</View>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// String/emoji icon
|
|
37
|
+
return (
|
|
38
|
+
<Text style={iconStyleProp}>
|
|
39
|
+
{icon}
|
|
40
|
+
</Text>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Pressable
|
|
46
|
+
onPress={onPress}
|
|
47
|
+
disabled={isDisabled}
|
|
48
|
+
style={styleProp}
|
|
49
|
+
{...rest}
|
|
50
|
+
>
|
|
51
|
+
<View>
|
|
52
|
+
{iconPosition === 'left' && renderIcon()}
|
|
53
|
+
{label && (
|
|
54
|
+
<Text style={textStyleProp}>
|
|
55
|
+
{loading ? 'Loading…' : label}
|
|
56
|
+
</Text>
|
|
57
|
+
)}
|
|
58
|
+
{iconPosition === 'right' && renderIcon()}
|
|
59
|
+
</View>
|
|
60
|
+
</Pressable>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { View, StyleSheet } from 'react-native';
|
|
2
|
+
import { defaultTheme } from '../../theme/Theme';
|
|
3
|
+
|
|
4
|
+
export function Card(props) {
|
|
5
|
+
const theme = props.theme ?? defaultTheme;
|
|
6
|
+
return (
|
|
7
|
+
<View
|
|
8
|
+
style={[
|
|
9
|
+
styles.card,
|
|
10
|
+
{
|
|
11
|
+
backgroundColor: theme.colors.surface,
|
|
12
|
+
borderColor: theme.colors.border,
|
|
13
|
+
borderRadius: theme.radius.lg,
|
|
14
|
+
},
|
|
15
|
+
props.style,
|
|
16
|
+
]}
|
|
17
|
+
>
|
|
18
|
+
{props.children}
|
|
19
|
+
</View>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const styles = StyleSheet.create({
|
|
24
|
+
card: {
|
|
25
|
+
borderWidth: 1,
|
|
26
|
+
padding: 16,
|
|
27
|
+
gap: 12,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useState, useMemo } from 'react';
|
|
2
|
+
import { View, Text, Pressable, FlatList, Modal, StyleSheet } from 'react-native';
|
|
3
|
+
import { defaultTheme } from '../../theme/Theme';
|
|
4
|
+
|
|
5
|
+
export function Dropdown(props) {
|
|
6
|
+
const theme = props.theme ?? defaultTheme;
|
|
7
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
8
|
+
const selectedLabel = useMemo(() => {
|
|
9
|
+
if (props.value == null) return null;
|
|
10
|
+
return props.items.find((i) => i.value === props.value)?.label ?? String(props.value);
|
|
11
|
+
}, [props.items, props.value]);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<View style={styles.fieldWrapper}>
|
|
15
|
+
{props.label ? (
|
|
16
|
+
<Text style={[styles.label, { color: theme.colors.textMuted }]}>{props.label}</Text>
|
|
17
|
+
) : null}
|
|
18
|
+
|
|
19
|
+
<Pressable
|
|
20
|
+
onPress={() => setIsOpen(true)}
|
|
21
|
+
style={[
|
|
22
|
+
styles.dropdownTrigger,
|
|
23
|
+
{
|
|
24
|
+
borderColor: theme.colors.border,
|
|
25
|
+
backgroundColor: theme.colors.surface,
|
|
26
|
+
borderRadius: theme.radius.md,
|
|
27
|
+
},
|
|
28
|
+
]}
|
|
29
|
+
>
|
|
30
|
+
<Text style={{ color: theme.colors.text }}>
|
|
31
|
+
{selectedLabel ?? props.placeholder ?? 'Select…'}
|
|
32
|
+
</Text>
|
|
33
|
+
<Text style={{ color: theme.colors.textMuted }}>▼</Text>
|
|
34
|
+
</Pressable>
|
|
35
|
+
|
|
36
|
+
<Modal
|
|
37
|
+
visible={isOpen}
|
|
38
|
+
animationType="slide"
|
|
39
|
+
transparent
|
|
40
|
+
onRequestClose={() => setIsOpen(false)}
|
|
41
|
+
>
|
|
42
|
+
<View style={styles.modalOverlay}>
|
|
43
|
+
<View
|
|
44
|
+
style={[
|
|
45
|
+
styles.modalSheet,
|
|
46
|
+
{
|
|
47
|
+
backgroundColor: theme.colors.background,
|
|
48
|
+
borderColor: theme.colors.border,
|
|
49
|
+
},
|
|
50
|
+
]}
|
|
51
|
+
>
|
|
52
|
+
<View style={styles.modalHeader}>
|
|
53
|
+
<Text style={[styles.modalTitle, { color: theme.colors.text }]}>
|
|
54
|
+
{props.label ?? 'Select'}
|
|
55
|
+
</Text>
|
|
56
|
+
<Pressable onPress={() => setIsOpen(false)}>
|
|
57
|
+
<Text style={{ color: theme.colors.primary }}>Close</Text>
|
|
58
|
+
</Pressable>
|
|
59
|
+
</View>
|
|
60
|
+
|
|
61
|
+
<FlatList
|
|
62
|
+
data={props.items}
|
|
63
|
+
keyExtractor={(item) => String(item.value)}
|
|
64
|
+
ItemSeparatorComponent={() => (
|
|
65
|
+
<View style={[styles.separator, { backgroundColor: theme.colors.border }]} />
|
|
66
|
+
)}
|
|
67
|
+
renderItem={({ item }) => {
|
|
68
|
+
const isSelected = props.value === item.value;
|
|
69
|
+
return (
|
|
70
|
+
<Pressable
|
|
71
|
+
onPress={() => {
|
|
72
|
+
props.onValueChange(item.value);
|
|
73
|
+
setIsOpen(false);
|
|
74
|
+
}}
|
|
75
|
+
style={[
|
|
76
|
+
styles.dropdownItem,
|
|
77
|
+
{
|
|
78
|
+
backgroundColor: isSelected ? theme.colors.surface : 'transparent',
|
|
79
|
+
},
|
|
80
|
+
]}
|
|
81
|
+
>
|
|
82
|
+
<Text style={{ color: theme.colors.text }}>{item.label}</Text>
|
|
83
|
+
{isSelected ? <Text style={{ color: theme.colors.primary }}>✓</Text> : null}
|
|
84
|
+
</Pressable>
|
|
85
|
+
);
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
</View>
|
|
89
|
+
</View>
|
|
90
|
+
</Modal>
|
|
91
|
+
</View>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
const styles = StyleSheet.create({
|
|
97
|
+
fieldWrapper: {
|
|
98
|
+
gap: 8,
|
|
99
|
+
},
|
|
100
|
+
label: {
|
|
101
|
+
fontSize: 13,
|
|
102
|
+
},
|
|
103
|
+
dropdownTrigger: {
|
|
104
|
+
borderWidth: 1,
|
|
105
|
+
paddingHorizontal: 12,
|
|
106
|
+
paddingVertical: 12,
|
|
107
|
+
flexDirection: 'row',
|
|
108
|
+
justifyContent: 'space-between',
|
|
109
|
+
alignItems: 'center',
|
|
110
|
+
},
|
|
111
|
+
modalOverlay: {
|
|
112
|
+
flex: 1,
|
|
113
|
+
justifyContent: 'flex-end',
|
|
114
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
115
|
+
},
|
|
116
|
+
modalSheet: {
|
|
117
|
+
borderTopWidth: 1,
|
|
118
|
+
paddingHorizontal: 16,
|
|
119
|
+
paddingTop: 12,
|
|
120
|
+
paddingBottom: 24,
|
|
121
|
+
maxHeight: '70%',
|
|
122
|
+
},
|
|
123
|
+
modalHeader: {
|
|
124
|
+
flexDirection: 'row',
|
|
125
|
+
justifyContent: 'space-between',
|
|
126
|
+
alignItems: 'center',
|
|
127
|
+
paddingBottom: 12,
|
|
128
|
+
},
|
|
129
|
+
modalTitle: {
|
|
130
|
+
fontSize: 16,
|
|
131
|
+
fontWeight: '700',
|
|
132
|
+
},
|
|
133
|
+
separator: {
|
|
134
|
+
height: 1,
|
|
135
|
+
},
|
|
136
|
+
dropdownItem: {
|
|
137
|
+
paddingVertical: 14,
|
|
138
|
+
paddingHorizontal: 8,
|
|
139
|
+
flexDirection: 'row',
|
|
140
|
+
justifyContent: 'space-between',
|
|
141
|
+
alignItems: 'center',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { View, Text, Image, FlatList, Pressable, StyleSheet } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export function List(props) {
|
|
4
|
+
const {
|
|
5
|
+
items,
|
|
6
|
+
onItemPress,
|
|
7
|
+
/**
|
|
8
|
+
* Optional mappers so List can work with many different data shapes.
|
|
9
|
+
* If not provided, it will use the default fields: image, title, description, location, id.
|
|
10
|
+
*/
|
|
11
|
+
getImage, // (item) => string | require(...) | ImageSource
|
|
12
|
+
getTitle, // (item) => string
|
|
13
|
+
getDescription, // (item) => string | undefined
|
|
14
|
+
getLocation, // (item) => string | undefined
|
|
15
|
+
keyExtractor, // (item, index) => string
|
|
16
|
+
/**
|
|
17
|
+
* Style customization from the consuming app:
|
|
18
|
+
* - cardStyle : style for the card / item container
|
|
19
|
+
* - thumbnailStyle : style for the image
|
|
20
|
+
* - titleStyle : style for the title
|
|
21
|
+
* - descriptionStyle : style for the description
|
|
22
|
+
* - locationStyle : style for the location
|
|
23
|
+
* - contentContainerStyle : style for the list content container
|
|
24
|
+
*/
|
|
25
|
+
cardStyle,
|
|
26
|
+
thumbnailStyle,
|
|
27
|
+
titleStyle,
|
|
28
|
+
descriptionStyle,
|
|
29
|
+
locationStyle,
|
|
30
|
+
contentContainerStyle,
|
|
31
|
+
...flatListProps
|
|
32
|
+
} = props;
|
|
33
|
+
|
|
34
|
+
const renderItem = ({ item }) => {
|
|
35
|
+
const rawImage = getImage ? getImage(item) : item.image;
|
|
36
|
+
const title = getTitle ? getTitle(item) : item.title;
|
|
37
|
+
const description = getDescription ? getDescription(item) : item.description;
|
|
38
|
+
const location = getLocation ? getLocation(item) : item.location;
|
|
39
|
+
|
|
40
|
+
const imageSource =
|
|
41
|
+
typeof rawImage === 'string' ? { uri: rawImage } : rawImage;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Pressable
|
|
45
|
+
onPress={() => onItemPress && onItemPress(item)}
|
|
46
|
+
style={({ pressed }) => [
|
|
47
|
+
styles.card,
|
|
48
|
+
cardStyle,
|
|
49
|
+
pressed && { opacity: 0.9 },
|
|
50
|
+
]}
|
|
51
|
+
>
|
|
52
|
+
<Image
|
|
53
|
+
source={imageSource}
|
|
54
|
+
style={[styles.thumbnail, thumbnailStyle]}
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
<View style={styles.content}>
|
|
58
|
+
<Text style={[styles.title, titleStyle]}>
|
|
59
|
+
{title}
|
|
60
|
+
</Text>
|
|
61
|
+
|
|
62
|
+
{description ? (
|
|
63
|
+
<Text
|
|
64
|
+
style={[styles.description, descriptionStyle]}
|
|
65
|
+
numberOfLines={2}
|
|
66
|
+
>
|
|
67
|
+
{description}
|
|
68
|
+
</Text>
|
|
69
|
+
) : null}
|
|
70
|
+
|
|
71
|
+
{location ? (
|
|
72
|
+
<Text style={[styles.location, locationStyle]}>
|
|
73
|
+
{location}
|
|
74
|
+
</Text>
|
|
75
|
+
) : null}
|
|
76
|
+
</View>
|
|
77
|
+
</Pressable>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<FlatList
|
|
83
|
+
data={items}
|
|
84
|
+
keyExtractor={
|
|
85
|
+
keyExtractor ??
|
|
86
|
+
((item, index) =>
|
|
87
|
+
item?.id != null ? String(item.id) : String(index))
|
|
88
|
+
}
|
|
89
|
+
contentContainerStyle={[
|
|
90
|
+
styles.listContent,
|
|
91
|
+
contentContainerStyle,
|
|
92
|
+
]}
|
|
93
|
+
renderItem={renderItem}
|
|
94
|
+
showsVerticalScrollIndicator={false}
|
|
95
|
+
{...flatListProps}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const styles = StyleSheet.create({
|
|
101
|
+
listContent: {
|
|
102
|
+
paddingVertical: 4,
|
|
103
|
+
},
|
|
104
|
+
card: {
|
|
105
|
+
flexDirection: 'row',
|
|
106
|
+
alignItems: 'center',
|
|
107
|
+
},
|
|
108
|
+
thumbnail: {
|
|
109
|
+
width: 72,
|
|
110
|
+
height: 72,
|
|
111
|
+
borderRadius: 12,
|
|
112
|
+
},
|
|
113
|
+
content: {
|
|
114
|
+
flex: 1,
|
|
115
|
+
},
|
|
116
|
+
title: {
|
|
117
|
+
},
|
|
118
|
+
description: {
|
|
119
|
+
},
|
|
120
|
+
location: {
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { View, Text, Pressable } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export function NavigationBar(props) {
|
|
4
|
+
const {
|
|
5
|
+
items = [], // Array of { icon, label, onPress, active }
|
|
6
|
+
activeIndex = 0,
|
|
7
|
+
style: styleProp,
|
|
8
|
+
itemStyle: itemStyleProp,
|
|
9
|
+
activeItemStyle: activeItemStyleProp,
|
|
10
|
+
iconStyle: iconStyleProp,
|
|
11
|
+
activeIconStyle: activeIconStyleProp,
|
|
12
|
+
labelStyle: labelStyleProp,
|
|
13
|
+
activeLabelStyle: activeLabelStyleProp,
|
|
14
|
+
...rest
|
|
15
|
+
} = props;
|
|
16
|
+
|
|
17
|
+
const itemCount = items.length;
|
|
18
|
+
|
|
19
|
+
// Validasi jumlah item (4, 5, atau 6)
|
|
20
|
+
if (itemCount < 4 || itemCount > 6) {
|
|
21
|
+
console.warn(`NavigationBar: Expected 4, 5, or 6 items, but got ${itemCount}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const renderIcon = (icon, isActive) => {
|
|
25
|
+
if (!icon) return null;
|
|
26
|
+
|
|
27
|
+
// Handle different icon types: React component, Image source, or string (emoji)
|
|
28
|
+
if (typeof icon === 'function' || typeof icon === 'object') {
|
|
29
|
+
const IconComponent = icon;
|
|
30
|
+
return (
|
|
31
|
+
<View style={isActive ? activeIconStyleProp : iconStyleProp}>
|
|
32
|
+
{typeof IconComponent === 'function' ? (
|
|
33
|
+
<IconComponent />
|
|
34
|
+
) : (
|
|
35
|
+
icon
|
|
36
|
+
)}
|
|
37
|
+
</View>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// String/emoji icon
|
|
42
|
+
return (
|
|
43
|
+
<Text style={isActive ? activeIconStyleProp : iconStyleProp}>
|
|
44
|
+
{icon}
|
|
45
|
+
</Text>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<View style={styleProp} {...rest}>
|
|
51
|
+
{items.map((item, index) => {
|
|
52
|
+
const isActive = item.active !== undefined ? item.active : activeIndex === index;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Pressable
|
|
56
|
+
key={index}
|
|
57
|
+
onPress={item.onPress}
|
|
58
|
+
style={[
|
|
59
|
+
itemStyleProp,
|
|
60
|
+
isActive && activeItemStyleProp,
|
|
61
|
+
]}
|
|
62
|
+
>
|
|
63
|
+
{renderIcon(item.icon, isActive)}
|
|
64
|
+
{item.label && (
|
|
65
|
+
<Text style={[
|
|
66
|
+
labelStyleProp,
|
|
67
|
+
isActive && activeLabelStyleProp,
|
|
68
|
+
]}>
|
|
69
|
+
{item.label}
|
|
70
|
+
</Text>
|
|
71
|
+
)}
|
|
72
|
+
</Pressable>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</View>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { View, Text, TextInput } from 'react-native';
|
|
2
|
+
import { defaultTheme } from '../../theme/Theme';
|
|
3
|
+
|
|
4
|
+
export function TextField(props) {
|
|
5
|
+
const theme = props.theme ?? defaultTheme;
|
|
6
|
+
const {
|
|
7
|
+
label,
|
|
8
|
+
errorText,
|
|
9
|
+
style,
|
|
10
|
+
labelStyle,
|
|
11
|
+
containerStyle,
|
|
12
|
+
...inputProps
|
|
13
|
+
} = props;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<View style={containerStyle}>
|
|
17
|
+
{label ? (
|
|
18
|
+
<Text style={labelStyle}>{label}</Text>
|
|
19
|
+
) : null}
|
|
20
|
+
<TextInput
|
|
21
|
+
placeholderTextColor={theme.colors.textMuted}
|
|
22
|
+
{...inputProps}
|
|
23
|
+
style={style}
|
|
24
|
+
/>
|
|
25
|
+
{errorText ? (
|
|
26
|
+
<Text>{errorText}</Text>
|
|
27
|
+
) : null}
|
|
28
|
+
</View>
|
|
29
|
+
);
|
|
30
|
+
}
|
package/forms/index.jsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Controller } from 'react-hook-form';
|
|
2
|
+
|
|
3
|
+
import { TextField } from '../components';
|
|
4
|
+
|
|
5
|
+
export function FormTextField(props) {
|
|
6
|
+
const { control, name, ...textFieldProps } = props;
|
|
7
|
+
return (
|
|
8
|
+
<Controller
|
|
9
|
+
control={control}
|
|
10
|
+
name={name}
|
|
11
|
+
render={({ field: { onChange, value }, fieldState }) => (
|
|
12
|
+
<TextField
|
|
13
|
+
{...textFieldProps}
|
|
14
|
+
value={value ?? ''}
|
|
15
|
+
onChangeText={onChange}
|
|
16
|
+
errorText={fieldState.error?.message}
|
|
17
|
+
/>
|
|
18
|
+
)}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
package/hooks/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as useFocus } from './useFocus.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useFocusEffect } from '@react-navigation/native';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
export default function useFocus({ callback, setState }) {
|
|
5
|
+
useFocusEffect(
|
|
6
|
+
useCallback(() => {
|
|
7
|
+
const fetchLists = async () => {
|
|
8
|
+
try {
|
|
9
|
+
const data = await callback.getLists();
|
|
10
|
+
setState(data);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error(error);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
fetchLists();
|
|
17
|
+
}, [callback, setState])
|
|
18
|
+
);
|
|
19
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @biruframework/native - React Native / Android: UI, hooks, navigation, forms, state-redux
|
|
3
|
+
* Install: pnpm add @biruframework/core @biruframework/native
|
|
4
|
+
*/
|
|
5
|
+
export * from './components';
|
|
6
|
+
export * from './theme';
|
|
7
|
+
export * from './ui';
|
|
8
|
+
export * from './hooks';
|
|
9
|
+
export * from './navigation';
|
|
10
|
+
export * from './forms';
|
|
11
|
+
export * from './state';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { NavigationContainer } from '@react-navigation/native';
|
|
2
|
+
export { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create React Navigation linking config from @biruframework/core route definitions.
|
|
6
|
+
* Minimal helper (1-level). You can still override/extend manually.
|
|
7
|
+
*/
|
|
8
|
+
export function linkingFromRoutes(
|
|
9
|
+
routes,
|
|
10
|
+
options,
|
|
11
|
+
) {
|
|
12
|
+
const screens = {};
|
|
13
|
+
for (const [screenName, route] of Object.entries(routes)) {
|
|
14
|
+
if (route.path) screens[screenName] = route.path.replace(/^\//, '');
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
prefixes: options.prefixes,
|
|
18
|
+
config: { screens },
|
|
19
|
+
};
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@biruframework/native",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React Native / Android: UI, hooks, navigation, forms, state-redux",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"exports": { ".": "./index.js" },
|
|
7
|
+
"peerDependencies": {
|
|
8
|
+
"@biruframework/core": "*",
|
|
9
|
+
"react": "*",
|
|
10
|
+
"react-native": "*",
|
|
11
|
+
"@react-navigation/native": "*",
|
|
12
|
+
"react-hook-form": "*"
|
|
13
|
+
},
|
|
14
|
+
"keywords": ["biruframework", "react-native", "android", "ui"],
|
|
15
|
+
"license": "ISC"
|
|
16
|
+
}
|
package/state/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export {
|
|
2
|
+
configureStore,
|
|
3
|
+
createAction,
|
|
4
|
+
createAsyncThunk,
|
|
5
|
+
createListenerMiddleware,
|
|
6
|
+
createReducer,
|
|
7
|
+
createSelector,
|
|
8
|
+
createSlice,
|
|
9
|
+
isAnyOf,
|
|
10
|
+
isAsyncThunkAction,
|
|
11
|
+
isFulfilled,
|
|
12
|
+
isPending,
|
|
13
|
+
isRejected,
|
|
14
|
+
isRejectedWithValue,
|
|
15
|
+
} from '@reduxjs/toolkit';
|
|
16
|
+
|
|
17
|
+
import { Provider, useDispatch, useSelector } from 'react-redux';
|
|
18
|
+
|
|
19
|
+
export { Provider, useDispatch, useSelector };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hooks helper. In your app, call:
|
|
23
|
+
* const { useAppDispatch, useAppSelector } = createReduxHooks();
|
|
24
|
+
*/
|
|
25
|
+
export function createReduxHooks() {
|
|
26
|
+
return { useAppDispatch: useDispatch, useAppSelector: useSelector };
|
|
27
|
+
}
|
package/theme/Theme.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { defaultTheme } from './Theme';
|
|
4
|
+
|
|
5
|
+
const ThemeContext = createContext(defaultTheme);
|
|
6
|
+
|
|
7
|
+
export function ThemeProvider(props) {
|
|
8
|
+
const theme = useMemo(() => props.theme ?? defaultTheme, [props.theme]);
|
|
9
|
+
return <ThemeContext.Provider value={theme}>{props.children}</ThemeContext.Provider>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useTheme() {
|
|
13
|
+
return useContext(ThemeContext);
|
|
14
|
+
}
|
package/theme/index.js
ADDED
package/ui/controls.jsx
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { Pressable, StyleSheet, Switch, Text, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { defaultTheme } from '../theme/Theme';
|
|
5
|
+
|
|
6
|
+
export function Checkbox(props) {
|
|
7
|
+
const theme = props.theme ?? defaultTheme;
|
|
8
|
+
const isDisabled = props.disabled ?? false;
|
|
9
|
+
|
|
10
|
+
const boxStyle = useMemo(
|
|
11
|
+
() => [
|
|
12
|
+
styles.checkboxBox,
|
|
13
|
+
{
|
|
14
|
+
borderColor: theme.colors.border,
|
|
15
|
+
backgroundColor: props.checked ? theme.colors.primary : theme.colors.surface,
|
|
16
|
+
opacity: isDisabled ? 0.6 : 1,
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
[isDisabled, props.checked, theme.colors.border, theme.colors.primary, theme.colors.surface],
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Pressable
|
|
24
|
+
disabled={isDisabled}
|
|
25
|
+
onPress={() => props.onCheckedChange(!props.checked)}
|
|
26
|
+
style={styles.checkboxRow}
|
|
27
|
+
>
|
|
28
|
+
<View style={boxStyle}>
|
|
29
|
+
{props.checked ? <Text style={{ color: theme.colors.text, fontWeight: '900' }}>✓</Text> : null}
|
|
30
|
+
</View>
|
|
31
|
+
{props.label ? <Text style={{ color: theme.colors.text }}>{props.label}</Text> : null}
|
|
32
|
+
</Pressable>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function SwitchRow(props) {
|
|
37
|
+
const theme = props.theme ?? defaultTheme;
|
|
38
|
+
const isDisabled = props.disabled ?? false;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View style={styles.switchRow}>
|
|
42
|
+
<Text style={{ color: theme.colors.text }}>{props.label}</Text>
|
|
43
|
+
<Switch
|
|
44
|
+
disabled={isDisabled}
|
|
45
|
+
value={props.value}
|
|
46
|
+
onValueChange={props.onValueChange}
|
|
47
|
+
trackColor={{ true: theme.colors.primary, false: theme.colors.border }}
|
|
48
|
+
thumbColor={theme.colors.text}
|
|
49
|
+
/>
|
|
50
|
+
</View>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const styles = StyleSheet.create({
|
|
55
|
+
checkboxRow: {
|
|
56
|
+
flexDirection: 'row',
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
gap: 10,
|
|
59
|
+
},
|
|
60
|
+
checkboxBox: {
|
|
61
|
+
width: 22,
|
|
62
|
+
height: 22,
|
|
63
|
+
borderWidth: 1,
|
|
64
|
+
borderRadius: 6,
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
justifyContent: 'center',
|
|
67
|
+
},
|
|
68
|
+
switchRow: {
|
|
69
|
+
flexDirection: 'row',
|
|
70
|
+
justifyContent: 'space-between',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
},
|
|
73
|
+
});
|
package/ui/feedback.jsx
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { defaultTheme } from '../theme/Theme';
|
|
5
|
+
|
|
6
|
+
export function Divider(props) {
|
|
7
|
+
const theme = props.theme ?? defaultTheme;
|
|
8
|
+
return <View style={[styles.divider, { backgroundColor: theme.colors.border }, props.style]} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Badge(props) {
|
|
12
|
+
const theme = props.theme ?? defaultTheme;
|
|
13
|
+
return (
|
|
14
|
+
<View
|
|
15
|
+
style={[
|
|
16
|
+
styles.badge,
|
|
17
|
+
{ backgroundColor: props.backgroundColor ?? theme.colors.surface, borderColor: theme.colors.border },
|
|
18
|
+
]}
|
|
19
|
+
>
|
|
20
|
+
<Text style={{ color: theme.colors.textMuted, fontSize: 12, fontWeight: '600' }}>
|
|
21
|
+
{props.label}
|
|
22
|
+
</Text>
|
|
23
|
+
</View>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Loading(props) {
|
|
28
|
+
const theme = props.theme ?? defaultTheme;
|
|
29
|
+
return (
|
|
30
|
+
<View style={styles.loading}>
|
|
31
|
+
<ActivityIndicator color={theme.colors.primary} />
|
|
32
|
+
{props.label ? <Text style={{ color: theme.colors.textMuted }}>{props.label}</Text> : null}
|
|
33
|
+
</View>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const styles = StyleSheet.create({
|
|
38
|
+
divider: {
|
|
39
|
+
height: 1,
|
|
40
|
+
width: '100%',
|
|
41
|
+
},
|
|
42
|
+
badge: {
|
|
43
|
+
paddingHorizontal: 10,
|
|
44
|
+
paddingVertical: 6,
|
|
45
|
+
borderWidth: 1,
|
|
46
|
+
borderRadius: 999,
|
|
47
|
+
alignSelf: 'flex-start',
|
|
48
|
+
},
|
|
49
|
+
loading: {
|
|
50
|
+
flexDirection: 'row',
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
gap: 10,
|
|
53
|
+
},
|
|
54
|
+
});
|
package/ui/index.js
ADDED
package/ui/layout.jsx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export function Stack(props) {
|
|
5
|
+
const childrenArray = React.Children.toArray(props.children);
|
|
6
|
+
return (
|
|
7
|
+
<View style={[{ flexDirection: 'column' }, props.style]}>
|
|
8
|
+
{childrenArray.map((child, index) => (
|
|
9
|
+
<View key={index} style={{ marginTop: index === 0 ? 0 : props.gap ?? 0 }}>
|
|
10
|
+
{child}
|
|
11
|
+
</View>
|
|
12
|
+
))}
|
|
13
|
+
</View>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function Row(props) {
|
|
18
|
+
const childrenArray = React.Children.toArray(props.children);
|
|
19
|
+
return (
|
|
20
|
+
<View
|
|
21
|
+
style={[
|
|
22
|
+
{
|
|
23
|
+
flexDirection: 'row',
|
|
24
|
+
alignItems: props.align ?? 'center',
|
|
25
|
+
justifyContent: props.justify ?? 'flex-start',
|
|
26
|
+
flexWrap: props.wrap ? 'wrap' : 'nowrap',
|
|
27
|
+
},
|
|
28
|
+
props.style,
|
|
29
|
+
]}
|
|
30
|
+
>
|
|
31
|
+
{childrenArray.map((child, index) => (
|
|
32
|
+
<View key={index} style={{ marginLeft: index === 0 ? 0 : props.gap ?? 0 }}>
|
|
33
|
+
{child}
|
|
34
|
+
</View>
|
|
35
|
+
))}
|
|
36
|
+
</View>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function Spacer(props) {
|
|
41
|
+
return <View style={props.horizontal ? { width: props.size } : { height: props.size }} />;
|
|
42
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { defaultTheme } from '../theme/Theme';
|
|
5
|
+
|
|
6
|
+
export function ModalSheet(props) {
|
|
7
|
+
const theme = props.theme ?? defaultTheme;
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Modal visible={props.visible} animationType="slide" transparent onRequestClose={props.onClose}>
|
|
11
|
+
<Pressable style={styles.overlay} onPress={props.onClose}>
|
|
12
|
+
<View />
|
|
13
|
+
</Pressable>
|
|
14
|
+
<View
|
|
15
|
+
style={[
|
|
16
|
+
styles.sheet,
|
|
17
|
+
{ backgroundColor: theme.colors.background, borderColor: theme.colors.border },
|
|
18
|
+
]}
|
|
19
|
+
>
|
|
20
|
+
<View style={styles.header}>
|
|
21
|
+
<Text style={[styles.title, { color: theme.colors.text }]}>
|
|
22
|
+
{props.title ?? 'Sheet'}
|
|
23
|
+
</Text>
|
|
24
|
+
<Pressable onPress={props.onClose}>
|
|
25
|
+
<Text style={{ color: theme.colors.primary, fontWeight: '700' }}>Close</Text>
|
|
26
|
+
</Pressable>
|
|
27
|
+
</View>
|
|
28
|
+
<View style={styles.content}>{props.children}</View>
|
|
29
|
+
</View>
|
|
30
|
+
</Modal>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const styles = StyleSheet.create({
|
|
35
|
+
overlay: {
|
|
36
|
+
flex: 1,
|
|
37
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
38
|
+
},
|
|
39
|
+
sheet: {
|
|
40
|
+
borderTopWidth: 1,
|
|
41
|
+
paddingHorizontal: 16,
|
|
42
|
+
paddingTop: 12,
|
|
43
|
+
paddingBottom: 24,
|
|
44
|
+
},
|
|
45
|
+
header: {
|
|
46
|
+
flexDirection: 'row',
|
|
47
|
+
justifyContent: 'space-between',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
},
|
|
50
|
+
title: {
|
|
51
|
+
fontSize: 16,
|
|
52
|
+
fontWeight: '800',
|
|
53
|
+
},
|
|
54
|
+
content: {
|
|
55
|
+
marginTop: 12,
|
|
56
|
+
gap: 12,
|
|
57
|
+
},
|
|
58
|
+
});
|
package/ui/screen.jsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { SafeAreaView, ScrollView, StyleSheet } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { defaultTheme } from '../theme/Theme';
|
|
5
|
+
|
|
6
|
+
export function Screen(props) {
|
|
7
|
+
const theme = props.theme ?? defaultTheme;
|
|
8
|
+
|
|
9
|
+
if (props.scroll) {
|
|
10
|
+
return (
|
|
11
|
+
<SafeAreaView style={[styles.safe, { backgroundColor: theme.colors.background }, props.style]}>
|
|
12
|
+
<ScrollView contentContainerStyle={[styles.content, props.contentStyle]}>{props.children}</ScrollView>
|
|
13
|
+
</SafeAreaView>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<SafeAreaView style={[styles.safe, { backgroundColor: theme.colors.background }, props.style]}>
|
|
19
|
+
{props.children}
|
|
20
|
+
</SafeAreaView>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const styles = StyleSheet.create({
|
|
25
|
+
safe: { flex: 1 },
|
|
26
|
+
content: { padding: 16, gap: 12 },
|
|
27
|
+
});
|
package/ui/toast.jsx
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { defaultTheme } from '../theme/Theme';
|
|
5
|
+
|
|
6
|
+
const ToastContext = createContext(null);
|
|
7
|
+
|
|
8
|
+
export function ToastProvider(props) {
|
|
9
|
+
const theme = props.theme ?? defaultTheme;
|
|
10
|
+
const [toast, setToast] = useState(null);
|
|
11
|
+
const timer = useRef(null);
|
|
12
|
+
const opacity = useRef(new Animated.Value(0)).current;
|
|
13
|
+
|
|
14
|
+
const api = useMemo(
|
|
15
|
+
() => ({
|
|
16
|
+
show: (options) => {
|
|
17
|
+
if (timer.current) clearTimeout(timer.current);
|
|
18
|
+
setToast(options);
|
|
19
|
+
Animated.timing(opacity, { toValue: 1, duration: 160, useNativeDriver: true }).start();
|
|
20
|
+
timer.current = setTimeout(() => {
|
|
21
|
+
Animated.timing(opacity, { toValue: 0, duration: 160, useNativeDriver: true }).start(() => {
|
|
22
|
+
setToast(null);
|
|
23
|
+
});
|
|
24
|
+
}, options.durationMs ?? 2500);
|
|
25
|
+
},
|
|
26
|
+
hide: () => {
|
|
27
|
+
if (timer.current) clearTimeout(timer.current);
|
|
28
|
+
Animated.timing(opacity, { toValue: 0, duration: 160, useNativeDriver: true }).start(() => {
|
|
29
|
+
setToast(null);
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
[opacity],
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const backgroundColor =
|
|
37
|
+
toast?.variant === 'danger'
|
|
38
|
+
? theme.colors.danger
|
|
39
|
+
: toast?.variant === 'success'
|
|
40
|
+
? '#16a34a'
|
|
41
|
+
: theme.colors.surface;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<ToastContext.Provider value={api}>
|
|
45
|
+
{props.children}
|
|
46
|
+
{toast ? (
|
|
47
|
+
<Animated.View style={[styles.toastWrap, { opacity }]}>
|
|
48
|
+
<Pressable
|
|
49
|
+
onPress={() => api.hide()}
|
|
50
|
+
style={[
|
|
51
|
+
styles.toast,
|
|
52
|
+
{ backgroundColor, borderColor: theme.colors.border },
|
|
53
|
+
]}
|
|
54
|
+
>
|
|
55
|
+
<Text style={{ color: theme.colors.text }}>{toast.message}</Text>
|
|
56
|
+
<View style={{ width: 10 }} />
|
|
57
|
+
<Text style={{ color: theme.colors.textMuted }}>Tap to dismiss</Text>
|
|
58
|
+
</Pressable>
|
|
59
|
+
</Animated.View>
|
|
60
|
+
) : null}
|
|
61
|
+
</ToastContext.Provider>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function useToast() {
|
|
66
|
+
const ctx = useContext(ToastContext);
|
|
67
|
+
if (!ctx) throw new Error('useToast must be used within ToastProvider');
|
|
68
|
+
return ctx;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const styles = StyleSheet.create({
|
|
72
|
+
toastWrap: {
|
|
73
|
+
position: 'absolute',
|
|
74
|
+
left: 16,
|
|
75
|
+
right: 16,
|
|
76
|
+
bottom: 24,
|
|
77
|
+
},
|
|
78
|
+
toast: {
|
|
79
|
+
borderWidth: 1,
|
|
80
|
+
borderRadius: 14,
|
|
81
|
+
paddingHorizontal: 14,
|
|
82
|
+
paddingVertical: 12,
|
|
83
|
+
flexDirection: 'row',
|
|
84
|
+
alignItems: 'center',
|
|
85
|
+
justifyContent: 'space-between',
|
|
86
|
+
},
|
|
87
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { defaultTheme } from '../theme/Theme';
|
|
5
|
+
|
|
6
|
+
export function Title(props) {
|
|
7
|
+
const theme = props.theme ?? defaultTheme;
|
|
8
|
+
return (
|
|
9
|
+
<Text
|
|
10
|
+
{...props}
|
|
11
|
+
style={[{ fontSize: 20, fontWeight: '800', color: theme.colors.text }, props.style]}
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Subtitle(props) {
|
|
17
|
+
const theme = props.theme ?? defaultTheme;
|
|
18
|
+
return (
|
|
19
|
+
<Text
|
|
20
|
+
{...props}
|
|
21
|
+
style={[{ fontSize: 14, color: theme.colors.textMuted }, props.style]}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function SectionTitle(props) {
|
|
27
|
+
const theme = props.theme ?? defaultTheme;
|
|
28
|
+
return (
|
|
29
|
+
<Text
|
|
30
|
+
{...props}
|
|
31
|
+
style={[{ fontSize: 16, fontWeight: '700', color: theme.colors.text }, props.style]}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function BodyText(props) {
|
|
37
|
+
const theme = props.theme ?? defaultTheme;
|
|
38
|
+
return (
|
|
39
|
+
<Text
|
|
40
|
+
{...props}
|
|
41
|
+
style={[{ fontSize: 14, lineHeight: 20, color: theme.colors.textMuted }, props.style]}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|