@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,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActivityIndicator,
|
|
3
|
+
Pressable,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
View,
|
|
6
|
+
type PressableProps,
|
|
7
|
+
type StyleProp,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
|
|
11
|
+
import { useTheme } from '../theme';
|
|
12
|
+
import { radius } from '../tokens/radius';
|
|
13
|
+
import { spacing } from '../tokens/spacing';
|
|
14
|
+
import { Text } from './Text';
|
|
15
|
+
|
|
16
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'danger' | 'ghost';
|
|
17
|
+
|
|
18
|
+
export interface ButtonProps extends Omit<PressableProps, 'style'> {
|
|
19
|
+
label: string;
|
|
20
|
+
variant?: ButtonVariant;
|
|
21
|
+
loading?: boolean;
|
|
22
|
+
size?: 'md' | 'sm';
|
|
23
|
+
style?: StyleProp<ViewStyle>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function palette(
|
|
27
|
+
variant: ButtonVariant,
|
|
28
|
+
colors: { textPrimary: string; textSecondary: string; shapeSecondary: string; backgroundScreen: string },
|
|
29
|
+
pressed: boolean,
|
|
30
|
+
brandPrimary?: string,
|
|
31
|
+
brandOnPrimary?: string,
|
|
32
|
+
) {
|
|
33
|
+
const primary = brandPrimary ?? '#3D8BFF';
|
|
34
|
+
const onPrimary = brandOnPrimary ?? '#FFFFFF';
|
|
35
|
+
|
|
36
|
+
switch (variant) {
|
|
37
|
+
case 'primary':
|
|
38
|
+
return {
|
|
39
|
+
background: pressed ? primary + 'CC' : primary,
|
|
40
|
+
border: 'transparent',
|
|
41
|
+
label: onPrimary,
|
|
42
|
+
};
|
|
43
|
+
case 'secondary':
|
|
44
|
+
return {
|
|
45
|
+
background: pressed ? colors.shapeSecondary + 'CC' : colors.shapeSecondary,
|
|
46
|
+
border: 'transparent',
|
|
47
|
+
label: colors.textPrimary,
|
|
48
|
+
};
|
|
49
|
+
case 'outline':
|
|
50
|
+
return {
|
|
51
|
+
background: pressed ? colors.shapeSecondary + '33' : 'transparent',
|
|
52
|
+
border: colors.shapeSecondary,
|
|
53
|
+
label: colors.textPrimary,
|
|
54
|
+
};
|
|
55
|
+
case 'danger':
|
|
56
|
+
return {
|
|
57
|
+
background: pressed ? '#FF4545' + '1A' : 'transparent',
|
|
58
|
+
border: '#FF4545' + '66',
|
|
59
|
+
label: '#FF4545',
|
|
60
|
+
};
|
|
61
|
+
case 'ghost':
|
|
62
|
+
return {
|
|
63
|
+
background: pressed ? colors.shapeSecondary + '33' : 'transparent',
|
|
64
|
+
border: 'transparent',
|
|
65
|
+
label: colors.textSecondary,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Design-system button with variants and loading state.
|
|
72
|
+
*
|
|
73
|
+
* ```tsx
|
|
74
|
+
* <Button label="Save" variant="primary" />
|
|
75
|
+
* <Button label="Cancel" variant="ghost" />
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function Button({
|
|
79
|
+
label,
|
|
80
|
+
variant = 'primary',
|
|
81
|
+
loading = false,
|
|
82
|
+
size = 'md',
|
|
83
|
+
style,
|
|
84
|
+
disabled,
|
|
85
|
+
...rest
|
|
86
|
+
}: ButtonProps) {
|
|
87
|
+
const theme = useTheme();
|
|
88
|
+
const isDisabled = disabled || loading;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Pressable
|
|
92
|
+
accessibilityRole="button"
|
|
93
|
+
disabled={isDisabled}
|
|
94
|
+
style={({ pressed }) => {
|
|
95
|
+
const colors = palette(variant, theme.colors, pressed);
|
|
96
|
+
|
|
97
|
+
return [
|
|
98
|
+
styles.base,
|
|
99
|
+
size === 'sm' && styles.small,
|
|
100
|
+
{
|
|
101
|
+
backgroundColor: colors.background,
|
|
102
|
+
borderColor: colors.border,
|
|
103
|
+
opacity: isDisabled ? 0.5 : 1,
|
|
104
|
+
},
|
|
105
|
+
style,
|
|
106
|
+
] as StyleProp<ViewStyle>;
|
|
107
|
+
}}
|
|
108
|
+
{...rest}>
|
|
109
|
+
{({ pressed }) => {
|
|
110
|
+
const colors = palette(variant, theme.colors, pressed);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<View style={styles.row}>
|
|
114
|
+
{loading ? (
|
|
115
|
+
<ActivityIndicator size="small" color={colors.label} />
|
|
116
|
+
) : null}
|
|
117
|
+
<Text
|
|
118
|
+
variant="body"
|
|
119
|
+
color="textPrimary"
|
|
120
|
+
textStyle={[styles.label, size === 'sm' && styles.labelSmall, { color: colors.label }]}>
|
|
121
|
+
{label}
|
|
122
|
+
</Text>
|
|
123
|
+
</View>
|
|
124
|
+
);
|
|
125
|
+
}}
|
|
126
|
+
</Pressable>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const styles = StyleSheet.create({
|
|
131
|
+
base: {
|
|
132
|
+
borderRadius: radius.sm,
|
|
133
|
+
borderWidth: 1,
|
|
134
|
+
paddingVertical: spacing.lg,
|
|
135
|
+
paddingHorizontal: spacing['2xl'],
|
|
136
|
+
alignItems: 'center',
|
|
137
|
+
justifyContent: 'center',
|
|
138
|
+
},
|
|
139
|
+
small: {
|
|
140
|
+
paddingVertical: spacing.sm + spacing.xxs,
|
|
141
|
+
paddingHorizontal: spacing.lg,
|
|
142
|
+
},
|
|
143
|
+
row: {
|
|
144
|
+
flexDirection: 'row',
|
|
145
|
+
alignItems: 'center',
|
|
146
|
+
justifyContent: 'center',
|
|
147
|
+
gap: spacing.sm,
|
|
148
|
+
},
|
|
149
|
+
label: {
|
|
150
|
+
fontSize: 16,
|
|
151
|
+
lineHeight: 24,
|
|
152
|
+
fontWeight: '600',
|
|
153
|
+
},
|
|
154
|
+
labelSmall: {
|
|
155
|
+
fontSize: 14,
|
|
156
|
+
lineHeight: 20,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { StyleSheet, View, type ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { useTheme } from '../theme';
|
|
4
|
+
import { radius, type RadiusToken } from '../tokens/radius';
|
|
5
|
+
import { elevation } from '../tokens/elevation';
|
|
6
|
+
import { spacing } from '../tokens/spacing';
|
|
7
|
+
|
|
8
|
+
export interface CardProps extends ViewProps {
|
|
9
|
+
/** Elevation level. Defaults to `sm`. */
|
|
10
|
+
level?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
11
|
+
/** Remove default padding. */
|
|
12
|
+
noPadding?: boolean;
|
|
13
|
+
/** Show a border around the card. */
|
|
14
|
+
bordered?: boolean;
|
|
15
|
+
/** Override the default border radius. */
|
|
16
|
+
radius?: RadiusToken;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Surface card component with design-system shadow and radius.
|
|
21
|
+
*
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <Card level="md">
|
|
24
|
+
* <Text>Content</Text>
|
|
25
|
+
* </Card>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function Card({
|
|
29
|
+
level = 'sm',
|
|
30
|
+
noPadding = false,
|
|
31
|
+
bordered = false,
|
|
32
|
+
radius: radiusToken,
|
|
33
|
+
style,
|
|
34
|
+
children,
|
|
35
|
+
...rest
|
|
36
|
+
}: CardProps) {
|
|
37
|
+
const theme = useTheme();
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View
|
|
41
|
+
style={[
|
|
42
|
+
styles.card,
|
|
43
|
+
elevation[level],
|
|
44
|
+
{ backgroundColor: theme.colors.shapePrimary },
|
|
45
|
+
radiusToken !== undefined && { borderRadius: radius[radiusToken] },
|
|
46
|
+
bordered && { borderWidth: 1, borderColor: theme.colors.shapeTertiary },
|
|
47
|
+
!noPadding && { padding: spacing.lg },
|
|
48
|
+
style,
|
|
49
|
+
]}
|
|
50
|
+
{...rest}>
|
|
51
|
+
{children}
|
|
52
|
+
</View>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const styles = StyleSheet.create({
|
|
57
|
+
card: {
|
|
58
|
+
borderRadius: radius.lg,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Pressable, StyleSheet, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { Icon } from './Icon';
|
|
5
|
+
import { Text } from './Text';
|
|
6
|
+
|
|
7
|
+
export type CheckboxType = 'checkbox' | 'radio';
|
|
8
|
+
export type CheckboxSize = 'sm' | 'md';
|
|
9
|
+
|
|
10
|
+
export interface CheckboxProps {
|
|
11
|
+
value: boolean;
|
|
12
|
+
onValueChange: (value: boolean) => void;
|
|
13
|
+
label?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
size?: CheckboxSize;
|
|
16
|
+
type?: CheckboxType;
|
|
17
|
+
indeterminate?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const sizeConfig = {
|
|
21
|
+
sm: { boxSize: 16, iconSize: 12, gap: 8, fontSize: 14, lineHeight: 20, borderRadius: 4, radioRadius: 8 },
|
|
22
|
+
md: { boxSize: 20, iconSize: 12, gap: 12, fontSize: 16, lineHeight: 24, borderRadius: 6, radioRadius: 10 },
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Checkbox or radio button for selection with label.
|
|
27
|
+
*
|
|
28
|
+
* ```tsx
|
|
29
|
+
* <Checkbox value={selected} onValueChange={setSelected} label="I agree" />
|
|
30
|
+
* <Checkbox value={v} onValueChange={setV} type="radio" label="Option A" />
|
|
31
|
+
* <Checkbox value={partial} onValueChange={setPartial} indeterminate />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function Checkbox({
|
|
35
|
+
value,
|
|
36
|
+
onValueChange,
|
|
37
|
+
label,
|
|
38
|
+
disabled = false,
|
|
39
|
+
size = 'md',
|
|
40
|
+
type = 'checkbox',
|
|
41
|
+
indeterminate = false,
|
|
42
|
+
}: CheckboxProps) {
|
|
43
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
44
|
+
const s = sizeConfig[size];
|
|
45
|
+
|
|
46
|
+
const isChecked = value || indeterminate;
|
|
47
|
+
const isRadio = type === 'radio';
|
|
48
|
+
|
|
49
|
+
const getBoxStyles = () => {
|
|
50
|
+
if (isRadio) {
|
|
51
|
+
const br = isChecked ? 500 : s.radioRadius;
|
|
52
|
+
const borderColor = isChecked || isHovered ? '#cefe00' : '#d9d8d7';
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
borderRadius: br,
|
|
56
|
+
backgroundColor: isChecked ? '#cefe00' : '#faf9f7',
|
|
57
|
+
borderColor,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let bgColor = '#faf9f7';
|
|
62
|
+
|
|
63
|
+
if (isChecked) {
|
|
64
|
+
bgColor = isHovered ? '#87ad00' : '#cefe00';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let borderColor = '#d9d8d7';
|
|
68
|
+
|
|
69
|
+
if (isChecked) {
|
|
70
|
+
borderColor = isHovered ? '#87ad00' : '#cefe00';
|
|
71
|
+
} else if (isHovered) {
|
|
72
|
+
borderColor = '#cefe00';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
borderRadius: s.borderRadius,
|
|
77
|
+
backgroundColor: bgColor,
|
|
78
|
+
borderColor,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const boxStyles = getBoxStyles();
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Pressable
|
|
86
|
+
accessibilityRole={isRadio ? 'radio' : 'checkbox'}
|
|
87
|
+
accessibilityState={{ checked: value, disabled }}
|
|
88
|
+
disabled={disabled}
|
|
89
|
+
onPress={() => onValueChange(!value)}
|
|
90
|
+
onHoverIn={() => setIsHovered(true)}
|
|
91
|
+
onHoverOut={() => setIsHovered(false)}
|
|
92
|
+
style={({ pressed }) => [
|
|
93
|
+
styles.container,
|
|
94
|
+
{ gap: s.gap },
|
|
95
|
+
pressed && styles.pressed,
|
|
96
|
+
disabled && styles.disabled,
|
|
97
|
+
]}>
|
|
98
|
+
<View
|
|
99
|
+
style={[
|
|
100
|
+
styles.box,
|
|
101
|
+
{
|
|
102
|
+
width: s.boxSize,
|
|
103
|
+
height: s.boxSize,
|
|
104
|
+
borderWidth: isChecked ? 0 : 1.5,
|
|
105
|
+
...boxStyles,
|
|
106
|
+
},
|
|
107
|
+
]}>
|
|
108
|
+
{indeterminate ? (
|
|
109
|
+
<View style={[styles.indeterminateBar, { backgroundColor: '#ffffff' }]} />
|
|
110
|
+
) : (
|
|
111
|
+
isChecked && (
|
|
112
|
+
<Icon name="check" size={s.iconSize} color="#ffffff" weight="bold" />
|
|
113
|
+
)
|
|
114
|
+
)}
|
|
115
|
+
</View>
|
|
116
|
+
|
|
117
|
+
{label && (
|
|
118
|
+
<Text
|
|
119
|
+
variant="body"
|
|
120
|
+
textStyle={[styles.label, { color: '#141414', fontSize: s.fontSize, lineHeight: s.lineHeight }]}>
|
|
121
|
+
{label}
|
|
122
|
+
</Text>
|
|
123
|
+
)}
|
|
124
|
+
</Pressable>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const styles = StyleSheet.create({
|
|
129
|
+
container: {
|
|
130
|
+
flexDirection: 'row',
|
|
131
|
+
alignItems: 'center',
|
|
132
|
+
},
|
|
133
|
+
pressed: {
|
|
134
|
+
opacity: 0.7,
|
|
135
|
+
},
|
|
136
|
+
disabled: {
|
|
137
|
+
opacity: 0.32,
|
|
138
|
+
},
|
|
139
|
+
box: {
|
|
140
|
+
alignItems: 'center',
|
|
141
|
+
justifyContent: 'center',
|
|
142
|
+
},
|
|
143
|
+
indeterminateBar: {
|
|
144
|
+
width: 10,
|
|
145
|
+
height: 2,
|
|
146
|
+
borderRadius: 1,
|
|
147
|
+
},
|
|
148
|
+
label: {
|
|
149
|
+
fontWeight: '500',
|
|
150
|
+
},
|
|
151
|
+
});
|