@astacinco/rn-primitives 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/README.md +195 -0
- package/__tests__/Button.test.tsx +114 -0
- package/__tests__/Card.test.tsx +73 -0
- package/__tests__/Container.test.tsx +71 -0
- package/__tests__/Divider.test.tsx +41 -0
- package/__tests__/Input.test.tsx +69 -0
- package/__tests__/Stack.test.tsx +85 -0
- package/__tests__/Text.test.tsx +64 -0
- package/__tests__/__snapshots__/Button.test.tsx.snap +143 -0
- package/__tests__/__snapshots__/Card.test.tsx.snap +47 -0
- package/__tests__/__snapshots__/Container.test.tsx.snap +51 -0
- package/__tests__/__snapshots__/Divider.test.tsx.snap +37 -0
- package/__tests__/__snapshots__/Input.test.tsx.snap +73 -0
- package/__tests__/__snapshots__/Stack.test.tsx.snap +101 -0
- package/__tests__/__snapshots__/Text.test.tsx.snap +39 -0
- package/package.json +47 -0
- package/src/Button/Button.tsx +119 -0
- package/src/Button/index.ts +2 -0
- package/src/Button/types.ts +43 -0
- package/src/Card/Card.tsx +62 -0
- package/src/Card/index.ts +2 -0
- package/src/Card/types.ts +21 -0
- package/src/Container/Container.tsx +42 -0
- package/src/Container/index.ts +2 -0
- package/src/Container/types.ts +23 -0
- package/src/Divider/Divider.tsx +40 -0
- package/src/Divider/index.ts +2 -0
- package/src/Divider/types.ts +21 -0
- package/src/Input/Input.tsx +103 -0
- package/src/Input/index.ts +2 -0
- package/src/Input/types.ts +23 -0
- package/src/Stack/Stack.tsx +77 -0
- package/src/Stack/index.ts +2 -0
- package/src/Stack/types.ts +30 -0
- package/src/Text/Text.tsx +50 -0
- package/src/Text/index.ts +2 -0
- package/src/Text/types.ts +21 -0
- package/src/index.ts +51 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, Text, StyleSheet, ActivityIndicator, ViewStyle, StyleProp } from 'react-native';
|
|
3
|
+
import { useTheme } from '@astacinco/rn-theming';
|
|
4
|
+
import type { ButtonProps, ButtonVariant, ButtonSize } from './types';
|
|
5
|
+
|
|
6
|
+
const sizeStyles: Record<ButtonSize, { paddingVertical: number; paddingHorizontal: number; fontSize: number }> = {
|
|
7
|
+
sm: { paddingVertical: 8, paddingHorizontal: 12, fontSize: 14 },
|
|
8
|
+
md: { paddingVertical: 12, paddingHorizontal: 20, fontSize: 16 },
|
|
9
|
+
lg: { paddingVertical: 16, paddingHorizontal: 28, fontSize: 18 },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function Button({
|
|
13
|
+
label,
|
|
14
|
+
variant = 'primary',
|
|
15
|
+
size = 'md',
|
|
16
|
+
disabled = false,
|
|
17
|
+
loading = false,
|
|
18
|
+
onPress,
|
|
19
|
+
style,
|
|
20
|
+
testID,
|
|
21
|
+
...props
|
|
22
|
+
}: ButtonProps) {
|
|
23
|
+
const { colors } = useTheme();
|
|
24
|
+
const sizeStyle = sizeStyles[size];
|
|
25
|
+
|
|
26
|
+
const getVariantStyles = (v: ButtonVariant, pressed: boolean) => {
|
|
27
|
+
const opacity = pressed ? 0.8 : 1;
|
|
28
|
+
|
|
29
|
+
switch (v) {
|
|
30
|
+
case 'primary':
|
|
31
|
+
return {
|
|
32
|
+
backgroundColor: colors.primary,
|
|
33
|
+
borderWidth: 0,
|
|
34
|
+
textColor: colors.textInverse,
|
|
35
|
+
opacity,
|
|
36
|
+
};
|
|
37
|
+
case 'secondary':
|
|
38
|
+
return {
|
|
39
|
+
backgroundColor: colors.secondary,
|
|
40
|
+
borderWidth: 0,
|
|
41
|
+
textColor: colors.textInverse,
|
|
42
|
+
opacity,
|
|
43
|
+
};
|
|
44
|
+
case 'outline':
|
|
45
|
+
return {
|
|
46
|
+
backgroundColor: 'transparent',
|
|
47
|
+
borderWidth: 1,
|
|
48
|
+
borderColor: colors.primary,
|
|
49
|
+
textColor: colors.primary,
|
|
50
|
+
opacity,
|
|
51
|
+
};
|
|
52
|
+
case 'ghost':
|
|
53
|
+
return {
|
|
54
|
+
backgroundColor: pressed ? colors.surface : 'transparent',
|
|
55
|
+
borderWidth: 0,
|
|
56
|
+
textColor: colors.primary,
|
|
57
|
+
opacity: 1,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const isDisabled = disabled || loading;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Pressable
|
|
66
|
+
testID={testID}
|
|
67
|
+
onPress={onPress}
|
|
68
|
+
disabled={isDisabled}
|
|
69
|
+
style={({ pressed }): StyleProp<ViewStyle> => {
|
|
70
|
+
const variantStyle = getVariantStyles(variant, pressed);
|
|
71
|
+
return [
|
|
72
|
+
styles.base,
|
|
73
|
+
{
|
|
74
|
+
backgroundColor: variantStyle.backgroundColor,
|
|
75
|
+
borderWidth: variantStyle.borderWidth,
|
|
76
|
+
borderColor: variantStyle.borderColor,
|
|
77
|
+
paddingVertical: sizeStyle.paddingVertical,
|
|
78
|
+
paddingHorizontal: sizeStyle.paddingHorizontal,
|
|
79
|
+
opacity: isDisabled ? 0.5 : variantStyle.opacity,
|
|
80
|
+
},
|
|
81
|
+
style as ViewStyle,
|
|
82
|
+
];
|
|
83
|
+
}}
|
|
84
|
+
{...props}
|
|
85
|
+
>
|
|
86
|
+
{({ pressed }) => {
|
|
87
|
+
const variantStyle = getVariantStyles(variant, pressed);
|
|
88
|
+
return loading ? (
|
|
89
|
+
<ActivityIndicator color={variantStyle.textColor} size="small" />
|
|
90
|
+
) : (
|
|
91
|
+
<Text
|
|
92
|
+
style={[
|
|
93
|
+
styles.label,
|
|
94
|
+
{
|
|
95
|
+
color: variantStyle.textColor,
|
|
96
|
+
fontSize: sizeStyle.fontSize,
|
|
97
|
+
},
|
|
98
|
+
]}
|
|
99
|
+
>
|
|
100
|
+
{label}
|
|
101
|
+
</Text>
|
|
102
|
+
);
|
|
103
|
+
}}
|
|
104
|
+
</Pressable>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const styles = StyleSheet.create({
|
|
109
|
+
base: {
|
|
110
|
+
borderRadius: 8,
|
|
111
|
+
alignItems: 'center',
|
|
112
|
+
justifyContent: 'center',
|
|
113
|
+
flexDirection: 'row',
|
|
114
|
+
},
|
|
115
|
+
label: {
|
|
116
|
+
fontWeight: '600',
|
|
117
|
+
textAlign: 'center',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { PressableProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
|
4
|
+
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
5
|
+
|
|
6
|
+
export interface ButtonProps extends Omit<PressableProps, 'children'> {
|
|
7
|
+
/**
|
|
8
|
+
* Button label text (required)
|
|
9
|
+
*/
|
|
10
|
+
label: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Button variant
|
|
14
|
+
* @default 'primary'
|
|
15
|
+
*/
|
|
16
|
+
variant?: ButtonVariant;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Button size
|
|
20
|
+
* @default 'md'
|
|
21
|
+
*/
|
|
22
|
+
size?: ButtonSize;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Disable the button
|
|
26
|
+
*/
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Show loading state
|
|
31
|
+
*/
|
|
32
|
+
loading?: boolean;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Press handler (required)
|
|
36
|
+
*/
|
|
37
|
+
onPress: () => void;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Test ID for testing
|
|
41
|
+
*/
|
|
42
|
+
testID?: string;
|
|
43
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import { useTheme } from '@astacinco/rn-theming';
|
|
4
|
+
import type { CardProps } from './types';
|
|
5
|
+
|
|
6
|
+
export function Card({
|
|
7
|
+
children,
|
|
8
|
+
variant = 'filled',
|
|
9
|
+
padding,
|
|
10
|
+
style,
|
|
11
|
+
testID,
|
|
12
|
+
...props
|
|
13
|
+
}: CardProps) {
|
|
14
|
+
const { colors, spacing, shadows } = useTheme();
|
|
15
|
+
|
|
16
|
+
const getVariantStyles = () => {
|
|
17
|
+
switch (variant) {
|
|
18
|
+
case 'filled':
|
|
19
|
+
return {
|
|
20
|
+
backgroundColor: colors.surface,
|
|
21
|
+
borderWidth: 0,
|
|
22
|
+
};
|
|
23
|
+
case 'outlined':
|
|
24
|
+
return {
|
|
25
|
+
backgroundColor: colors.background,
|
|
26
|
+
borderWidth: 1,
|
|
27
|
+
borderColor: colors.border,
|
|
28
|
+
};
|
|
29
|
+
case 'elevated':
|
|
30
|
+
return {
|
|
31
|
+
backgroundColor: colors.surfaceElevated,
|
|
32
|
+
borderWidth: 0,
|
|
33
|
+
...shadows.md,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const variantStyle = getVariantStyles();
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View
|
|
42
|
+
testID={testID}
|
|
43
|
+
style={[
|
|
44
|
+
styles.base,
|
|
45
|
+
{
|
|
46
|
+
padding: padding ?? spacing.md,
|
|
47
|
+
...variantStyle,
|
|
48
|
+
},
|
|
49
|
+
style,
|
|
50
|
+
]}
|
|
51
|
+
{...props}
|
|
52
|
+
>
|
|
53
|
+
{children}
|
|
54
|
+
</View>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const styles = StyleSheet.create({
|
|
59
|
+
base: {
|
|
60
|
+
borderRadius: 12,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export type CardVariant = 'filled' | 'outlined' | 'elevated';
|
|
4
|
+
|
|
5
|
+
export interface CardProps extends ViewProps {
|
|
6
|
+
/**
|
|
7
|
+
* Card variant
|
|
8
|
+
* @default 'filled'
|
|
9
|
+
*/
|
|
10
|
+
variant?: CardVariant;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Override padding
|
|
14
|
+
*/
|
|
15
|
+
padding?: number;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Test ID for testing
|
|
19
|
+
*/
|
|
20
|
+
testID?: string;
|
|
21
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import { useTheme } from '@astacinco/rn-theming';
|
|
4
|
+
import type { ContainerProps } from './types';
|
|
5
|
+
|
|
6
|
+
export function Container({
|
|
7
|
+
children,
|
|
8
|
+
padding,
|
|
9
|
+
maxWidth,
|
|
10
|
+
centered = false,
|
|
11
|
+
style,
|
|
12
|
+
testID,
|
|
13
|
+
...props
|
|
14
|
+
}: ContainerProps) {
|
|
15
|
+
const { colors, spacing } = useTheme();
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<View
|
|
19
|
+
testID={testID}
|
|
20
|
+
style={[
|
|
21
|
+
styles.base,
|
|
22
|
+
{
|
|
23
|
+
backgroundColor: colors.background,
|
|
24
|
+
padding: padding ?? spacing.md,
|
|
25
|
+
maxWidth,
|
|
26
|
+
alignSelf: centered ? 'center' : undefined,
|
|
27
|
+
width: centered && maxWidth ? '100%' : undefined,
|
|
28
|
+
},
|
|
29
|
+
style,
|
|
30
|
+
]}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</View>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const styles = StyleSheet.create({
|
|
39
|
+
base: {
|
|
40
|
+
flex: 1,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export interface ContainerProps extends ViewProps {
|
|
4
|
+
/**
|
|
5
|
+
* Override padding
|
|
6
|
+
*/
|
|
7
|
+
padding?: number;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Maximum width in pixels
|
|
11
|
+
*/
|
|
12
|
+
maxWidth?: number;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Center the container horizontally
|
|
16
|
+
*/
|
|
17
|
+
centered?: boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Test ID for testing
|
|
21
|
+
*/
|
|
22
|
+
testID?: string;
|
|
23
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
import { useTheme } from '@astacinco/rn-theming';
|
|
4
|
+
import type { DividerProps, DividerVariant } from './types';
|
|
5
|
+
|
|
6
|
+
const heightMap: Record<DividerVariant, number> = {
|
|
7
|
+
thin: 1,
|
|
8
|
+
thick: 2,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function Divider({
|
|
12
|
+
variant = 'thin',
|
|
13
|
+
color,
|
|
14
|
+
style,
|
|
15
|
+
testID,
|
|
16
|
+
...props
|
|
17
|
+
}: DividerProps) {
|
|
18
|
+
const { colors } = useTheme();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<View
|
|
22
|
+
testID={testID}
|
|
23
|
+
style={[
|
|
24
|
+
styles.base,
|
|
25
|
+
{
|
|
26
|
+
backgroundColor: color ?? colors.border,
|
|
27
|
+
height: heightMap[variant],
|
|
28
|
+
},
|
|
29
|
+
style,
|
|
30
|
+
]}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const styles = StyleSheet.create({
|
|
37
|
+
base: {
|
|
38
|
+
width: '100%',
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export type DividerVariant = 'thin' | 'thick';
|
|
4
|
+
|
|
5
|
+
export interface DividerProps extends ViewProps {
|
|
6
|
+
/**
|
|
7
|
+
* Divider variant
|
|
8
|
+
* @default 'thin'
|
|
9
|
+
*/
|
|
10
|
+
variant?: DividerVariant;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Override divider color
|
|
14
|
+
*/
|
|
15
|
+
color?: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Test ID for testing
|
|
19
|
+
*/
|
|
20
|
+
testID?: string;
|
|
21
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, TextInput, Text, StyleSheet, NativeSyntheticEvent, TextInputFocusEventData } from 'react-native';
|
|
3
|
+
import { useTheme } from '@astacinco/rn-theming';
|
|
4
|
+
import type { InputProps } from './types';
|
|
5
|
+
|
|
6
|
+
export function Input({
|
|
7
|
+
label,
|
|
8
|
+
error,
|
|
9
|
+
disabled = false,
|
|
10
|
+
style,
|
|
11
|
+
testID,
|
|
12
|
+
onFocus,
|
|
13
|
+
onBlur,
|
|
14
|
+
...props
|
|
15
|
+
}: InputProps) {
|
|
16
|
+
const { colors, spacing, typography } = useTheme();
|
|
17
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
18
|
+
|
|
19
|
+
const handleFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
|
20
|
+
setIsFocused(true);
|
|
21
|
+
onFocus?.(e);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const handleBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
|
25
|
+
setIsFocused(false);
|
|
26
|
+
onBlur?.(e);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const borderColor = error
|
|
30
|
+
? colors.error
|
|
31
|
+
: isFocused
|
|
32
|
+
? colors.borderFocus
|
|
33
|
+
: colors.border;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<View style={styles.container}>
|
|
37
|
+
{label && (
|
|
38
|
+
<Text
|
|
39
|
+
style={[
|
|
40
|
+
styles.label,
|
|
41
|
+
{
|
|
42
|
+
color: colors.textSecondary,
|
|
43
|
+
fontSize: typography.fontSize.sm,
|
|
44
|
+
marginBottom: spacing.xs,
|
|
45
|
+
},
|
|
46
|
+
]}
|
|
47
|
+
>
|
|
48
|
+
{label}
|
|
49
|
+
</Text>
|
|
50
|
+
)}
|
|
51
|
+
<TextInput
|
|
52
|
+
testID={testID}
|
|
53
|
+
editable={!disabled}
|
|
54
|
+
onFocus={handleFocus}
|
|
55
|
+
onBlur={handleBlur}
|
|
56
|
+
placeholderTextColor={colors.textMuted}
|
|
57
|
+
style={[
|
|
58
|
+
styles.input,
|
|
59
|
+
{
|
|
60
|
+
backgroundColor: disabled ? colors.surface : colors.background,
|
|
61
|
+
borderColor,
|
|
62
|
+
color: colors.text,
|
|
63
|
+
fontSize: typography.fontSize.md,
|
|
64
|
+
padding: spacing.sm,
|
|
65
|
+
opacity: disabled ? 0.6 : 1,
|
|
66
|
+
},
|
|
67
|
+
style,
|
|
68
|
+
]}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
{error && (
|
|
72
|
+
<Text
|
|
73
|
+
style={[
|
|
74
|
+
styles.error,
|
|
75
|
+
{
|
|
76
|
+
color: colors.error,
|
|
77
|
+
fontSize: typography.fontSize.sm,
|
|
78
|
+
marginTop: spacing.xs,
|
|
79
|
+
},
|
|
80
|
+
]}
|
|
81
|
+
>
|
|
82
|
+
{error}
|
|
83
|
+
</Text>
|
|
84
|
+
)}
|
|
85
|
+
</View>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const styles = StyleSheet.create({
|
|
90
|
+
container: {
|
|
91
|
+
width: '100%',
|
|
92
|
+
},
|
|
93
|
+
label: {
|
|
94
|
+
fontWeight: '500',
|
|
95
|
+
},
|
|
96
|
+
input: {
|
|
97
|
+
borderWidth: 1,
|
|
98
|
+
borderRadius: 8,
|
|
99
|
+
},
|
|
100
|
+
error: {
|
|
101
|
+
// Error text styles
|
|
102
|
+
},
|
|
103
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TextInputProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export interface InputProps extends TextInputProps {
|
|
4
|
+
/**
|
|
5
|
+
* Input label
|
|
6
|
+
*/
|
|
7
|
+
label?: string;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Error message to display
|
|
11
|
+
*/
|
|
12
|
+
error?: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Disable the input
|
|
16
|
+
*/
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Test ID for testing
|
|
21
|
+
*/
|
|
22
|
+
testID?: string;
|
|
23
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet, FlexAlignType } from 'react-native';
|
|
3
|
+
import { useTheme } from '@astacinco/rn-theming';
|
|
4
|
+
import type { StackProps, StackAlign, StackJustify } from './types';
|
|
5
|
+
|
|
6
|
+
const alignMap: Record<StackAlign, FlexAlignType> = {
|
|
7
|
+
start: 'flex-start',
|
|
8
|
+
center: 'center',
|
|
9
|
+
end: 'flex-end',
|
|
10
|
+
stretch: 'stretch',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const justifyMap: Record<StackJustify, 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around'> = {
|
|
14
|
+
start: 'flex-start',
|
|
15
|
+
center: 'center',
|
|
16
|
+
end: 'flex-end',
|
|
17
|
+
'space-between': 'space-between',
|
|
18
|
+
'space-around': 'space-around',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
interface InternalStackProps extends StackProps {
|
|
22
|
+
direction: 'column' | 'row';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function BaseStack({
|
|
26
|
+
children,
|
|
27
|
+
direction,
|
|
28
|
+
spacing = 'none',
|
|
29
|
+
align = 'stretch',
|
|
30
|
+
justify = 'start',
|
|
31
|
+
style,
|
|
32
|
+
testID,
|
|
33
|
+
...props
|
|
34
|
+
}: InternalStackProps) {
|
|
35
|
+
const { spacing: themeSpacing } = useTheme();
|
|
36
|
+
|
|
37
|
+
const gap = spacing === 'none' ? 0 : themeSpacing[spacing];
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View
|
|
41
|
+
testID={testID}
|
|
42
|
+
style={[
|
|
43
|
+
styles.base,
|
|
44
|
+
{
|
|
45
|
+
flexDirection: direction,
|
|
46
|
+
alignItems: alignMap[align],
|
|
47
|
+
justifyContent: justifyMap[justify],
|
|
48
|
+
gap,
|
|
49
|
+
},
|
|
50
|
+
style,
|
|
51
|
+
]}
|
|
52
|
+
{...props}
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
</View>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Vertical stack - arranges children in a column
|
|
61
|
+
*/
|
|
62
|
+
export function VStack(props: StackProps) {
|
|
63
|
+
return <BaseStack {...props} direction="column" />;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Horizontal stack - arranges children in a row
|
|
68
|
+
*/
|
|
69
|
+
export function HStack(props: StackProps) {
|
|
70
|
+
return <BaseStack {...props} direction="row" />;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const styles = StyleSheet.create({
|
|
74
|
+
base: {
|
|
75
|
+
// Base stack styles
|
|
76
|
+
},
|
|
77
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export type StackSpacing = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
4
|
+
export type StackAlign = 'start' | 'center' | 'end' | 'stretch';
|
|
5
|
+
export type StackJustify = 'start' | 'center' | 'end' | 'space-between' | 'space-around';
|
|
6
|
+
|
|
7
|
+
export interface StackProps extends ViewProps {
|
|
8
|
+
/**
|
|
9
|
+
* Gap between items
|
|
10
|
+
* @default 'none'
|
|
11
|
+
*/
|
|
12
|
+
spacing?: StackSpacing;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Align items (cross axis)
|
|
16
|
+
* @default 'stretch'
|
|
17
|
+
*/
|
|
18
|
+
align?: StackAlign;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Justify content (main axis)
|
|
22
|
+
* @default 'start'
|
|
23
|
+
*/
|
|
24
|
+
justify?: StackJustify;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Test ID for testing
|
|
28
|
+
*/
|
|
29
|
+
testID?: string;
|
|
30
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text as RNText, StyleSheet } from 'react-native';
|
|
3
|
+
import { useTheme } from '@astacinco/rn-theming';
|
|
4
|
+
import type { TextProps, TextVariant } from './types';
|
|
5
|
+
|
|
6
|
+
const variantStyles: Record<TextVariant, { fontSize: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'; fontWeight: 'regular' | 'medium' | 'bold' }> = {
|
|
7
|
+
title: { fontSize: 'xl', fontWeight: 'bold' },
|
|
8
|
+
subtitle: { fontSize: 'lg', fontWeight: 'medium' },
|
|
9
|
+
body: { fontSize: 'md', fontWeight: 'regular' },
|
|
10
|
+
caption: { fontSize: 'sm', fontWeight: 'regular' },
|
|
11
|
+
label: { fontSize: 'xs', fontWeight: 'medium' },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function Text({
|
|
15
|
+
children,
|
|
16
|
+
variant = 'body',
|
|
17
|
+
color,
|
|
18
|
+
style,
|
|
19
|
+
testID,
|
|
20
|
+
...props
|
|
21
|
+
}: TextProps) {
|
|
22
|
+
const { colors, typography } = useTheme();
|
|
23
|
+
const variantStyle = variantStyles[variant];
|
|
24
|
+
|
|
25
|
+
const textColor = color ?? (variant === 'caption' ? colors.textSecondary : colors.text);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<RNText
|
|
29
|
+
testID={testID}
|
|
30
|
+
style={[
|
|
31
|
+
styles.base,
|
|
32
|
+
{
|
|
33
|
+
color: textColor,
|
|
34
|
+
fontSize: typography.fontSize[variantStyle.fontSize],
|
|
35
|
+
fontFamily: typography.fontFamily[variantStyle.fontWeight],
|
|
36
|
+
},
|
|
37
|
+
style,
|
|
38
|
+
]}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</RNText>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const styles = StyleSheet.create({
|
|
47
|
+
base: {
|
|
48
|
+
// Base text styles
|
|
49
|
+
},
|
|
50
|
+
});
|