@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.
Files changed (38) hide show
  1. package/README.md +195 -0
  2. package/__tests__/Button.test.tsx +114 -0
  3. package/__tests__/Card.test.tsx +73 -0
  4. package/__tests__/Container.test.tsx +71 -0
  5. package/__tests__/Divider.test.tsx +41 -0
  6. package/__tests__/Input.test.tsx +69 -0
  7. package/__tests__/Stack.test.tsx +85 -0
  8. package/__tests__/Text.test.tsx +64 -0
  9. package/__tests__/__snapshots__/Button.test.tsx.snap +143 -0
  10. package/__tests__/__snapshots__/Card.test.tsx.snap +47 -0
  11. package/__tests__/__snapshots__/Container.test.tsx.snap +51 -0
  12. package/__tests__/__snapshots__/Divider.test.tsx.snap +37 -0
  13. package/__tests__/__snapshots__/Input.test.tsx.snap +73 -0
  14. package/__tests__/__snapshots__/Stack.test.tsx.snap +101 -0
  15. package/__tests__/__snapshots__/Text.test.tsx.snap +39 -0
  16. package/package.json +47 -0
  17. package/src/Button/Button.tsx +119 -0
  18. package/src/Button/index.ts +2 -0
  19. package/src/Button/types.ts +43 -0
  20. package/src/Card/Card.tsx +62 -0
  21. package/src/Card/index.ts +2 -0
  22. package/src/Card/types.ts +21 -0
  23. package/src/Container/Container.tsx +42 -0
  24. package/src/Container/index.ts +2 -0
  25. package/src/Container/types.ts +23 -0
  26. package/src/Divider/Divider.tsx +40 -0
  27. package/src/Divider/index.ts +2 -0
  28. package/src/Divider/types.ts +21 -0
  29. package/src/Input/Input.tsx +103 -0
  30. package/src/Input/index.ts +2 -0
  31. package/src/Input/types.ts +23 -0
  32. package/src/Stack/Stack.tsx +77 -0
  33. package/src/Stack/index.ts +2 -0
  34. package/src/Stack/types.ts +30 -0
  35. package/src/Text/Text.tsx +50 -0
  36. package/src/Text/index.ts +2 -0
  37. package/src/Text/types.ts +21 -0
  38. 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,2 @@
1
+ export { Button } from './Button';
2
+ export type { ButtonProps, ButtonVariant, ButtonSize } from './types';
@@ -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,2 @@
1
+ export { Card } from './Card';
2
+ export type { CardProps, CardVariant } from './types';
@@ -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,2 @@
1
+ export { Container } from './Container';
2
+ export type { ContainerProps } from './types';
@@ -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,2 @@
1
+ export { Divider } from './Divider';
2
+ export type { DividerProps, DividerVariant } from './types';
@@ -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,2 @@
1
+ export { Input } from './Input';
2
+ export type { InputProps } from './types';
@@ -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,2 @@
1
+ export { VStack, HStack } from './Stack';
2
+ export type { StackProps, StackSpacing, StackAlign, StackJustify } from './types';
@@ -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
+ });
@@ -0,0 +1,2 @@
1
+ export { Text } from './Text';
2
+ export type { TextProps, TextVariant } from './types';