@nwartz/design-system 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.
- package/components/Autocomplete.tsx +259 -0
- package/components/Badge.tsx +103 -0
- package/components/BottomNavBar.tsx +118 -0
- package/components/Box.tsx +99 -0
- package/components/Button.tsx +158 -0
- package/components/Card.tsx +60 -0
- package/components/Checkbox.tsx +151 -0
- package/components/DatePicker.tsx +410 -0
- package/components/DropdownSelect.tsx +158 -0
- package/components/Icon.tsx +131 -0
- package/components/InputField.tsx +202 -0
- package/components/LinkButton.tsx +69 -0
- package/components/ProgressBar.tsx +96 -0
- package/components/SearchBar.tsx +147 -0
- package/components/SegmentedTabs.tsx +88 -0
- package/components/Slider.tsx +159 -0
- package/components/TabMenu.tsx +86 -0
- package/components/TablePagination.tsx +176 -0
- package/components/Tag.tsx +128 -0
- package/components/Text.tsx +32 -0
- package/components/Toggle.tsx +112 -0
- package/components/index.ts +42 -0
- package/dist/index.d.mts +698 -0
- package/dist/index.d.ts +698 -0
- package/dist/index.js +2531 -0
- package/dist/index.mjs +2549 -0
- package/index.ts +89 -0
- package/package.json +63 -0
- package/theme/index.ts +2 -0
- package/theme/theme.tsx +61 -0
- package/tokens/colors.ts +55 -0
- package/tokens/elevation.ts +52 -0
- package/tokens/index.ts +11 -0
- package/tokens/mapping.ts +57 -0
- package/tokens/radius.ts +19 -0
- package/tokens/spacing.ts +36 -0
- package/tokens/typography.ts +53 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
TextInput,
|
|
6
|
+
View,
|
|
7
|
+
type TextInputProps,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
type StyleProp,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
|
|
12
|
+
import { useTheme } from '../theme';
|
|
13
|
+
import { radius } from '../tokens/radius';
|
|
14
|
+
import { spacing } from '../tokens/spacing';
|
|
15
|
+
import { elevation } from '../tokens/elevation';
|
|
16
|
+
import { Icon } from './Icon';
|
|
17
|
+
import { Text } from './Text';
|
|
18
|
+
import { DropdownSelect, type DropdownItem } from './DropdownSelect';
|
|
19
|
+
|
|
20
|
+
export type AutocompleteSize = 'sm' | 'md';
|
|
21
|
+
export type AutocompleteState = 'default' | 'hover' | 'focus' | 'typing' | 'selected' | 'filled' | 'error' | 'disabled';
|
|
22
|
+
|
|
23
|
+
export interface AutocompleteProps extends Omit<TextInputProps, 'value' | 'onChangeText'> {
|
|
24
|
+
/** Label displayed above the input. */
|
|
25
|
+
label: string;
|
|
26
|
+
/** Current input value. */
|
|
27
|
+
value?: string;
|
|
28
|
+
/** Called when text changes. */
|
|
29
|
+
onChangeText?: (text: string) => void;
|
|
30
|
+
/** Input size. */
|
|
31
|
+
size?: AutocompleteSize;
|
|
32
|
+
/** Whether the input is disabled. */
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
/** Error message displayed below the input. */
|
|
35
|
+
errorMessage?: string;
|
|
36
|
+
/** Dropdown items. */
|
|
37
|
+
items?: DropdownItem[];
|
|
38
|
+
/** Selected item key. */
|
|
39
|
+
selectedItem?: string | null;
|
|
40
|
+
/** Called when a dropdown item is selected. */
|
|
41
|
+
onSelectItem?: (item: DropdownItem) => void;
|
|
42
|
+
/** Whether the dropdown is open. */
|
|
43
|
+
open?: boolean;
|
|
44
|
+
/** Called to toggle the dropdown. */
|
|
45
|
+
onToggle?: () => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Autocomplete input with dropdown, matching the Nwartz DS spec.
|
|
50
|
+
*
|
|
51
|
+
* Supports sizes (sm/md), states (default/hover/focus/typing/selected/filled/error/disabled),
|
|
52
|
+
* label, error message, and a dropdown list.
|
|
53
|
+
*
|
|
54
|
+
* ```tsx
|
|
55
|
+
* <Autocomplete
|
|
56
|
+
* label="Country"
|
|
57
|
+
* placeholder="Start typing..."
|
|
58
|
+
* items={countries}
|
|
59
|
+
* onSelectItem={handleSelect}
|
|
60
|
+
* />
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function Autocomplete({
|
|
64
|
+
label,
|
|
65
|
+
value,
|
|
66
|
+
onChangeText,
|
|
67
|
+
size = 'md',
|
|
68
|
+
disabled = false,
|
|
69
|
+
errorMessage,
|
|
70
|
+
items = [],
|
|
71
|
+
selectedItem,
|
|
72
|
+
onSelectItem,
|
|
73
|
+
open = false,
|
|
74
|
+
onToggle,
|
|
75
|
+
placeholder,
|
|
76
|
+
...rest
|
|
77
|
+
}: AutocompleteProps) {
|
|
78
|
+
const theme = useTheme();
|
|
79
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
80
|
+
|
|
81
|
+
const hasValue = !!value && value.length > 0;
|
|
82
|
+
const showError = !!errorMessage;
|
|
83
|
+
const isOpen = open;
|
|
84
|
+
|
|
85
|
+
const inputHeight = size === 'sm' ? 40 : 44;
|
|
86
|
+
const paddingHorizontal = size === 'sm'
|
|
87
|
+
? spacing.md
|
|
88
|
+
: spacing.lg;
|
|
89
|
+
|
|
90
|
+
const getBorderColor = (): string => {
|
|
91
|
+
if (showError) {
|
|
92
|
+
return theme.colors.dangerMid;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isFocused) {
|
|
96
|
+
return theme.colors.primaryMid;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return theme.colors.shapeTertiary;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const getBoxShadow = (): ViewStyle => {
|
|
103
|
+
if (isFocused || showError) {
|
|
104
|
+
const focusColor = showError ? theme.colors.dangerMid : '#FFF6D4';
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
shadowColor: focusColor,
|
|
108
|
+
shadowOffset: { width: 0, height: 0 },
|
|
109
|
+
shadowOpacity: 1,
|
|
110
|
+
shadowRadius: 0,
|
|
111
|
+
elevation: 0,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return elevation.xs;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const inputContainerStyle: StyleProp<ViewStyle> = [
|
|
119
|
+
styles.inputContainer,
|
|
120
|
+
{
|
|
121
|
+
height: inputHeight,
|
|
122
|
+
paddingHorizontal,
|
|
123
|
+
backgroundColor: theme.colors.shapePrimary,
|
|
124
|
+
borderColor: getBorderColor(),
|
|
125
|
+
borderWidth: 1,
|
|
126
|
+
borderRadius: radius.sm,
|
|
127
|
+
},
|
|
128
|
+
getBoxShadow(),
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<View style={[styles.wrapper, disabled && styles.disabledWrapper]}>
|
|
133
|
+
<Text
|
|
134
|
+
variant="body"
|
|
135
|
+
color="textSecondary"
|
|
136
|
+
textStyle={styles.label}>
|
|
137
|
+
{label}
|
|
138
|
+
</Text>
|
|
139
|
+
|
|
140
|
+
<View style={inputContainerStyle}>
|
|
141
|
+
<TextInput
|
|
142
|
+
style={[
|
|
143
|
+
styles.input,
|
|
144
|
+
{
|
|
145
|
+
color: theme.colors.textPrimary,
|
|
146
|
+
fontSize: 14,
|
|
147
|
+
},
|
|
148
|
+
disabled && styles.inputDisabled,
|
|
149
|
+
]}
|
|
150
|
+
value={value}
|
|
151
|
+
onChangeText={onChangeText}
|
|
152
|
+
placeholder={placeholder}
|
|
153
|
+
placeholderTextColor={theme.colors.textTertiary}
|
|
154
|
+
editable={!disabled}
|
|
155
|
+
onFocus={() => setIsFocused(true)}
|
|
156
|
+
onBlur={() => setIsFocused(false)}
|
|
157
|
+
{...rest}
|
|
158
|
+
/>
|
|
159
|
+
|
|
160
|
+
<View style={styles.trailingIcons}>
|
|
161
|
+
{hasValue && !disabled && (
|
|
162
|
+
<Pressable
|
|
163
|
+
onPress={() => onChangeText?.('')}
|
|
164
|
+
hitSlop={8}
|
|
165
|
+
style={styles.iconButton}>
|
|
166
|
+
<Icon name="x-circle" size={16} color={theme.colors.textSecondary} />
|
|
167
|
+
</Pressable>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
<Pressable
|
|
171
|
+
onPress={onToggle}
|
|
172
|
+
disabled={disabled}
|
|
173
|
+
hitSlop={8}
|
|
174
|
+
style={styles.iconButton}>
|
|
175
|
+
<Icon
|
|
176
|
+
name={isOpen ? 'caret-up' : 'caret-down'}
|
|
177
|
+
size={16}
|
|
178
|
+
color={theme.colors.textTertiary}
|
|
179
|
+
/>
|
|
180
|
+
</Pressable>
|
|
181
|
+
</View>
|
|
182
|
+
</View>
|
|
183
|
+
|
|
184
|
+
{showError && (
|
|
185
|
+
<View style={styles.errorRow}>
|
|
186
|
+
<Icon name="warning-circle" size={16} color={theme.colors.dangerMid} />
|
|
187
|
+
<Text
|
|
188
|
+
variant="body"
|
|
189
|
+
color="textSecondary"
|
|
190
|
+
textStyle={[styles.errorText, { color: theme.colors.textSecondary }]}>
|
|
191
|
+
{errorMessage}
|
|
192
|
+
</Text>
|
|
193
|
+
</View>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{isOpen && items.length > 0 && (
|
|
197
|
+
<DropdownSelect
|
|
198
|
+
items={items}
|
|
199
|
+
value={selectedItem}
|
|
200
|
+
onSelect={(item) => {
|
|
201
|
+
onSelectItem?.(item);
|
|
202
|
+
onToggle?.();
|
|
203
|
+
}}
|
|
204
|
+
open={isOpen}
|
|
205
|
+
style={styles.dropdown}
|
|
206
|
+
/>
|
|
207
|
+
)}
|
|
208
|
+
</View>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const styles = StyleSheet.create({
|
|
213
|
+
wrapper: {
|
|
214
|
+
gap: spacing.xs,
|
|
215
|
+
},
|
|
216
|
+
disabledWrapper: {
|
|
217
|
+
opacity: 0.32,
|
|
218
|
+
},
|
|
219
|
+
label: {
|
|
220
|
+
fontSize: 14,
|
|
221
|
+
lineHeight: 21,
|
|
222
|
+
fontWeight: '500',
|
|
223
|
+
},
|
|
224
|
+
inputContainer: {
|
|
225
|
+
flexDirection: 'row',
|
|
226
|
+
alignItems: 'center',
|
|
227
|
+
gap: spacing.sm,
|
|
228
|
+
},
|
|
229
|
+
input: {
|
|
230
|
+
flex: 1,
|
|
231
|
+
paddingVertical: 0,
|
|
232
|
+
includeFontPadding: false,
|
|
233
|
+
fontSize: 14,
|
|
234
|
+
lineHeight: 21,
|
|
235
|
+
},
|
|
236
|
+
inputDisabled: {
|
|
237
|
+
opacity: 1,
|
|
238
|
+
},
|
|
239
|
+
trailingIcons: {
|
|
240
|
+
flexDirection: 'row',
|
|
241
|
+
alignItems: 'center',
|
|
242
|
+
gap: spacing.xs,
|
|
243
|
+
},
|
|
244
|
+
iconButton: {
|
|
245
|
+
padding: 2,
|
|
246
|
+
},
|
|
247
|
+
errorRow: {
|
|
248
|
+
flexDirection: 'row',
|
|
249
|
+
alignItems: 'center',
|
|
250
|
+
gap: spacing.xs,
|
|
251
|
+
},
|
|
252
|
+
errorText: {
|
|
253
|
+
fontSize: 14,
|
|
254
|
+
lineHeight: 21,
|
|
255
|
+
},
|
|
256
|
+
dropdown: {
|
|
257
|
+
marginTop: spacing.xxs,
|
|
258
|
+
},
|
|
259
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { StyleSheet, View, type ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { spacing } from '../tokens/spacing';
|
|
4
|
+
import { Icon, type IconName } from './Icon';
|
|
5
|
+
import { Text } from './Text';
|
|
6
|
+
|
|
7
|
+
export type BadgeSize = 'sm' | 'md' | 'lg';
|
|
8
|
+
export type BadgeColor = 'primary' | 'secondary' | 'info' | 'warning' | 'success' | 'error' | 'gray';
|
|
9
|
+
export type BadgeMode = 'solid' | 'outline';
|
|
10
|
+
|
|
11
|
+
export interface BadgeProps extends ViewProps {
|
|
12
|
+
label: string;
|
|
13
|
+
size?: BadgeSize;
|
|
14
|
+
color?: BadgeColor;
|
|
15
|
+
mode?: BadgeMode;
|
|
16
|
+
icon?: IconName;
|
|
17
|
+
trailingIcon?: IconName;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const colorPalette: Record<BadgeColor, { stroke: string; solidBg: string; solidText: string; outlineText: string }> = {
|
|
21
|
+
primary: { stroke: '#cefe00', solidBg: '#f8ffc5', solidText: '#536e00', outlineText: '#cefe00' },
|
|
22
|
+
secondary: { stroke: '#4fa971', solidBg: '#ddfad9', solidText: '#25584d', outlineText: '#25584d' },
|
|
23
|
+
info: { stroke: '#1048d6', solidBg: '#ebf4ff', solidText: '#1048d6', outlineText: '#1048d6' },
|
|
24
|
+
warning: { stroke: '#e3993a', solidBg: '#fef9ec', solidText: '#642000', outlineText: '#e3993a' },
|
|
25
|
+
success: { stroke: '#138135', solidBg: '#f1feeb', solidText: '#05482c', outlineText: '#138135' },
|
|
26
|
+
error: { stroke: '#bb4355', solidBg: '#fdefed', solidText: '#bb4355', outlineText: '#bb4355' },
|
|
27
|
+
gray: { stroke: '#d9d8d7', solidBg: '#e9e8e7', solidText: '#737271', outlineText: '#737271' },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const sizeStyles = {
|
|
31
|
+
sm: { height: 22, paddingHorizontal: 8, fontSize: 12, lineHeight: 18, iconSize: 12 },
|
|
32
|
+
md: { height: 24, paddingHorizontal: 8, fontSize: 14, lineHeight: 21, iconSize: 14 },
|
|
33
|
+
lg: { height: 28, paddingHorizontal: 12, fontSize: 14, lineHeight: 21, iconSize: 14 },
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Inline badge for status indicators, counts, and labels.
|
|
38
|
+
*
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <Badge label="Active" color="success" />
|
|
41
|
+
* <Badge label="3" color="error" size="sm" />
|
|
42
|
+
* <Badge label="Info" color="info" mode="outline" />
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function Badge({
|
|
46
|
+
label,
|
|
47
|
+
size = 'md',
|
|
48
|
+
color = 'gray',
|
|
49
|
+
mode = 'solid',
|
|
50
|
+
icon,
|
|
51
|
+
trailingIcon,
|
|
52
|
+
style,
|
|
53
|
+
...rest
|
|
54
|
+
}: BadgeProps) {
|
|
55
|
+
const palette = colorPalette[color];
|
|
56
|
+
const s = sizeStyles[size];
|
|
57
|
+
|
|
58
|
+
const bgColor = mode === 'solid' ? palette.solidBg : 'transparent';
|
|
59
|
+
const textColor = mode === 'solid' ? palette.solidText : palette.outlineText;
|
|
60
|
+
const borderColor = mode === 'outline' ? palette.stroke : 'transparent';
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<View
|
|
64
|
+
style={[
|
|
65
|
+
styles.badge,
|
|
66
|
+
{
|
|
67
|
+
height: s.height,
|
|
68
|
+
paddingHorizontal: s.paddingHorizontal,
|
|
69
|
+
backgroundColor: bgColor,
|
|
70
|
+
borderColor,
|
|
71
|
+
borderWidth: 1,
|
|
72
|
+
borderRadius: 16,
|
|
73
|
+
},
|
|
74
|
+
style,
|
|
75
|
+
]}
|
|
76
|
+
{...rest}>
|
|
77
|
+
{icon && <Icon name={icon} size={s.iconSize} color={textColor} />}
|
|
78
|
+
|
|
79
|
+
<Text
|
|
80
|
+
variant="caption"
|
|
81
|
+
textStyle={[
|
|
82
|
+
styles.label,
|
|
83
|
+
{ color: textColor, fontSize: s.fontSize, lineHeight: s.lineHeight },
|
|
84
|
+
]}>
|
|
85
|
+
{label}
|
|
86
|
+
</Text>
|
|
87
|
+
|
|
88
|
+
{trailingIcon && <Icon name={trailingIcon} size={s.iconSize} color={textColor} />}
|
|
89
|
+
</View>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const styles = StyleSheet.create({
|
|
94
|
+
badge: {
|
|
95
|
+
alignSelf: 'flex-start',
|
|
96
|
+
flexDirection: 'row',
|
|
97
|
+
alignItems: 'center',
|
|
98
|
+
gap: spacing.xs,
|
|
99
|
+
},
|
|
100
|
+
label: {
|
|
101
|
+
fontWeight: '500',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Pressable, StyleSheet, View } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { Icon, type IconName } from './Icon';
|
|
4
|
+
|
|
5
|
+
export type BottomNavBarItem = {
|
|
6
|
+
id: string;
|
|
7
|
+
icon: IconName;
|
|
8
|
+
badge?: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface BottomNavBarProps {
|
|
12
|
+
items: BottomNavBarItem[];
|
|
13
|
+
activeItemId: string;
|
|
14
|
+
onItemPress: (itemId: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Bottom navigation bar with lime circle active item indicator.
|
|
19
|
+
*
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <BottomNavBar
|
|
22
|
+
* items={[
|
|
23
|
+
* { id: 'home', icon: 'house' },
|
|
24
|
+
* { id: 'stats', icon: 'chart-bar' },
|
|
25
|
+
* { id: 'profile', icon: 'user' },
|
|
26
|
+
* ]}
|
|
27
|
+
* activeItemId="home"
|
|
28
|
+
* onItemPress={navigate}
|
|
29
|
+
* />
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function BottomNavBar({ items, activeItemId, onItemPress }: BottomNavBarProps) {
|
|
33
|
+
return (
|
|
34
|
+
<View
|
|
35
|
+
style={[
|
|
36
|
+
styles.container,
|
|
37
|
+
{
|
|
38
|
+
backgroundColor: '#201f1f' + 'CC',
|
|
39
|
+
borderColor: '#ffffff1A',
|
|
40
|
+
},
|
|
41
|
+
]}>
|
|
42
|
+
{items.map((item) => {
|
|
43
|
+
const isActive = item.id === activeItemId;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Pressable
|
|
47
|
+
key={item.id}
|
|
48
|
+
accessibilityRole="button"
|
|
49
|
+
accessibilityState={{ selected: isActive }}
|
|
50
|
+
onPress={() => onItemPress(item.id)}
|
|
51
|
+
style={({ pressed }) => [
|
|
52
|
+
styles.item,
|
|
53
|
+
pressed && styles.pressed,
|
|
54
|
+
]}>
|
|
55
|
+
{isActive ? (
|
|
56
|
+
<View style={styles.activeItem}>
|
|
57
|
+
<Icon
|
|
58
|
+
name={item.icon}
|
|
59
|
+
size={25}
|
|
60
|
+
color="#141414"
|
|
61
|
+
weight="regular"
|
|
62
|
+
/>
|
|
63
|
+
</View>
|
|
64
|
+
) : (
|
|
65
|
+
<View style={styles.inactiveItem}>
|
|
66
|
+
<Icon
|
|
67
|
+
name={item.icon}
|
|
68
|
+
size={25}
|
|
69
|
+
color="#8e8d8b"
|
|
70
|
+
weight="regular"
|
|
71
|
+
/>
|
|
72
|
+
</View>
|
|
73
|
+
)}
|
|
74
|
+
</Pressable>
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</View>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const styles = StyleSheet.create({
|
|
82
|
+
container: {
|
|
83
|
+
flexDirection: 'row',
|
|
84
|
+
alignItems: 'center',
|
|
85
|
+
justifyContent: 'space-between',
|
|
86
|
+
padding: 12,
|
|
87
|
+
borderRadius: 9999,
|
|
88
|
+
borderWidth: 1,
|
|
89
|
+
},
|
|
90
|
+
item: {
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
justifyContent: 'center',
|
|
93
|
+
},
|
|
94
|
+
pressed: {
|
|
95
|
+
opacity: 0.6,
|
|
96
|
+
},
|
|
97
|
+
activeItem: {
|
|
98
|
+
width: 55,
|
|
99
|
+
height: 55,
|
|
100
|
+
borderRadius: 9999,
|
|
101
|
+
backgroundColor: '#cefe00',
|
|
102
|
+
alignItems: 'center',
|
|
103
|
+
justifyContent: 'center',
|
|
104
|
+
shadowColor: '#caf300',
|
|
105
|
+
shadowOffset: { width: 0, height: 0 },
|
|
106
|
+
shadowOpacity: 0.4,
|
|
107
|
+
shadowRadius: 15,
|
|
108
|
+
elevation: 8,
|
|
109
|
+
},
|
|
110
|
+
inactiveItem: {
|
|
111
|
+
width: 60,
|
|
112
|
+
height: 60,
|
|
113
|
+
borderRadius: 9999,
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
justifyContent: 'center',
|
|
116
|
+
padding: 8,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { View, type ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { useTheme } from '../theme';
|
|
4
|
+
import { spacing, type SpacingToken } from '../tokens/spacing';
|
|
5
|
+
import { radius, type RadiusToken } from '../tokens/radius';
|
|
6
|
+
import { elevation, type ElevationToken } from '../tokens/elevation';
|
|
7
|
+
|
|
8
|
+
export interface BoxProps extends ViewProps {
|
|
9
|
+
/** Background color token key from the resolved theme. */
|
|
10
|
+
bg?: 'backgroundScreen' | 'shapePrimary' | 'shapeSecondary';
|
|
11
|
+
/** Apply a named spacing token as padding. */
|
|
12
|
+
p?: SpacingToken;
|
|
13
|
+
/** Apply a named spacing token as horizontal padding. */
|
|
14
|
+
px?: SpacingToken;
|
|
15
|
+
/** Apply a named spacing token as vertical padding. */
|
|
16
|
+
py?: SpacingToken;
|
|
17
|
+
/** Apply a named spacing token as margin. */
|
|
18
|
+
m?: SpacingToken;
|
|
19
|
+
/** Apply a named spacing token as horizontal margin. */
|
|
20
|
+
mx?: SpacingToken;
|
|
21
|
+
/** Apply a named spacing token as vertical margin. */
|
|
22
|
+
my?: SpacingToken;
|
|
23
|
+
/** Apply a named radius token as border radius. */
|
|
24
|
+
rounded?: RadiusToken;
|
|
25
|
+
/** Apply a named elevation token as shadow. */
|
|
26
|
+
elevation?: ElevationToken;
|
|
27
|
+
/** Flex direction shortcut. */
|
|
28
|
+
direction?: 'row' | 'column';
|
|
29
|
+
/** Align items shortcut. */
|
|
30
|
+
align?: 'flex-start' | 'center' | 'flex-end' | 'stretch';
|
|
31
|
+
/** Justify content shortcut. */
|
|
32
|
+
justify?: 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around';
|
|
33
|
+
/** Gap between children. Raw pixel value. */
|
|
34
|
+
gap?: number;
|
|
35
|
+
/** Full width. */
|
|
36
|
+
fullWidth?: boolean;
|
|
37
|
+
/** Full height. */
|
|
38
|
+
fullHeight?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Primitive layout component that maps design tokens to React Native styles.
|
|
43
|
+
*
|
|
44
|
+
* ```tsx
|
|
45
|
+
* <Box bg="shapePrimary" p="lg" rounded="xl" elevation="md">
|
|
46
|
+
* <Text>Hello</Text>
|
|
47
|
+
* </Box>
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function Box({
|
|
51
|
+
bg,
|
|
52
|
+
p,
|
|
53
|
+
px,
|
|
54
|
+
py,
|
|
55
|
+
m,
|
|
56
|
+
mx,
|
|
57
|
+
my,
|
|
58
|
+
rounded,
|
|
59
|
+
elevation: elev,
|
|
60
|
+
direction,
|
|
61
|
+
align,
|
|
62
|
+
justify,
|
|
63
|
+
gap,
|
|
64
|
+
fullWidth,
|
|
65
|
+
fullHeight,
|
|
66
|
+
style,
|
|
67
|
+
children,
|
|
68
|
+
...rest
|
|
69
|
+
}: BoxProps) {
|
|
70
|
+
const theme = useTheme();
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<View
|
|
74
|
+
style={[
|
|
75
|
+
bg && { backgroundColor: theme.colors[bg] },
|
|
76
|
+
p !== undefined && { padding: spacing[p] },
|
|
77
|
+
px !== undefined && { paddingHorizontal: spacing[px] },
|
|
78
|
+
py !== undefined && { paddingVertical: spacing[py] },
|
|
79
|
+
m !== undefined && { margin: spacing[m] },
|
|
80
|
+
mx !== undefined && { marginHorizontal: spacing[mx] },
|
|
81
|
+
my !== undefined && { marginVertical: spacing[my] },
|
|
82
|
+
rounded !== undefined && { borderRadius: radius[rounded] },
|
|
83
|
+
elev !== undefined && elevation[elev],
|
|
84
|
+
direction !== undefined && { flexDirection: direction },
|
|
85
|
+
align !== undefined && { alignItems: align },
|
|
86
|
+
justify !== undefined && { justifyContent: justify },
|
|
87
|
+
gap !== undefined && { gap },
|
|
88
|
+
fullWidth && { width: '100%' as never },
|
|
89
|
+
fullHeight && { height: '100%' as never },
|
|
90
|
+
style,
|
|
91
|
+
]}
|
|
92
|
+
{...rest}>
|
|
93
|
+
{children}
|
|
94
|
+
</View>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Re-export ViewProps so consumers don't need to import react-native separately.
|
|
99
|
+
export type { ViewProps } from 'react-native';
|