@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.
@@ -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
+ });