@umituz/react-native-design-system 1.0.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/LICENSE +21 -0
- package/README.md +157 -0
- package/package.json +43 -0
- package/src/index.ts +345 -0
- package/src/presentation/atoms/AtomicAvatar.tsx +157 -0
- package/src/presentation/atoms/AtomicAvatarGroup.tsx +169 -0
- package/src/presentation/atoms/AtomicBadge.tsx +232 -0
- package/src/presentation/atoms/AtomicButton.tsx +124 -0
- package/src/presentation/atoms/AtomicCard.tsx +112 -0
- package/src/presentation/atoms/AtomicChip.tsx +223 -0
- package/src/presentation/atoms/AtomicDatePicker.tsx +347 -0
- package/src/presentation/atoms/AtomicDivider.tsx +114 -0
- package/src/presentation/atoms/AtomicFab.tsx +104 -0
- package/src/presentation/atoms/AtomicFilter.tsx +154 -0
- package/src/presentation/atoms/AtomicFormError.tsx +105 -0
- package/src/presentation/atoms/AtomicIcon.tsx +29 -0
- package/src/presentation/atoms/AtomicImage.tsx +149 -0
- package/src/presentation/atoms/AtomicInput.tsx +232 -0
- package/src/presentation/atoms/AtomicNumberInput.tsx +182 -0
- package/src/presentation/atoms/AtomicPicker.tsx +458 -0
- package/src/presentation/atoms/AtomicProgress.tsx +143 -0
- package/src/presentation/atoms/AtomicSearchBar.tsx +114 -0
- package/src/presentation/atoms/AtomicSkeleton.tsx +146 -0
- package/src/presentation/atoms/AtomicSort.tsx +145 -0
- package/src/presentation/atoms/AtomicSwitch.tsx +166 -0
- package/src/presentation/atoms/AtomicText.tsx +50 -0
- package/src/presentation/atoms/AtomicTextArea.tsx +198 -0
- package/src/presentation/atoms/AtomicTouchable.tsx +233 -0
- package/src/presentation/atoms/fab/styles/fabStyles.ts +69 -0
- package/src/presentation/atoms/fab/types/index.ts +88 -0
- package/src/presentation/atoms/filter/styles/filterStyles.ts +32 -0
- package/src/presentation/atoms/filter/types/index.ts +89 -0
- package/src/presentation/atoms/index.ts +378 -0
- package/src/presentation/atoms/input/hooks/useInputState.ts +15 -0
- package/src/presentation/atoms/input/styles/inputStyles.ts +66 -0
- package/src/presentation/atoms/input/types/index.ts +25 -0
- package/src/presentation/atoms/picker/styles/pickerStyles.ts +200 -0
- package/src/presentation/atoms/picker/types/index.ts +40 -0
- package/src/presentation/atoms/touchable/styles/touchableStyles.ts +71 -0
- package/src/presentation/atoms/touchable/types/index.ts +162 -0
- package/src/presentation/hooks/useAppDesignTokens.ts +78 -0
- package/src/presentation/hooks/useResponsive.ts +180 -0
- package/src/presentation/loading/index.ts +40 -0
- package/src/presentation/loading/presentation/components/LoadingSpinner.tsx +116 -0
- package/src/presentation/loading/presentation/components/LoadingState.tsx +200 -0
- package/src/presentation/loading/presentation/hooks/useLoading.ts +100 -0
- package/src/presentation/molecules/AtomicConfirmationModal.tsx +263 -0
- package/src/presentation/molecules/EmptyState.tsx +130 -0
- package/src/presentation/molecules/FormField.tsx +128 -0
- package/src/presentation/molecules/GridContainer.tsx +124 -0
- package/src/presentation/molecules/IconContainer.tsx +94 -0
- package/src/presentation/molecules/LanguageSwitcher.tsx +42 -0
- package/src/presentation/molecules/ListItem.tsx +36 -0
- package/src/presentation/molecules/ScreenHeader.tsx +140 -0
- package/src/presentation/molecules/SearchBar.tsx +85 -0
- package/src/presentation/molecules/SectionCard.tsx +74 -0
- package/src/presentation/molecules/SectionContainer.tsx +106 -0
- package/src/presentation/molecules/SectionHeader.tsx +125 -0
- package/src/presentation/molecules/confirmation-modal/styles/confirmationModalStyles.ts +133 -0
- package/src/presentation/molecules/confirmation-modal/types/index.ts +107 -0
- package/src/presentation/molecules/index.ts +42 -0
- package/src/presentation/molecules/languageswitcher/config/languageSwitcherConfig.ts +5 -0
- package/src/presentation/molecules/languageswitcher/hooks/useLanguageNavigation.ts +15 -0
- package/src/presentation/molecules/listitem/styles/listItemStyles.ts +19 -0
- package/src/presentation/molecules/listitem/types/index.ts +17 -0
- package/src/presentation/organisms/AppHeader.tsx +136 -0
- package/src/presentation/organisms/FormContainer.tsx +180 -0
- package/src/presentation/organisms/ScreenLayout.tsx +209 -0
- package/src/presentation/organisms/index.ts +25 -0
- package/src/presentation/tokens/AppDesignTokens.ts +57 -0
- package/src/presentation/tokens/commonStyles.ts +253 -0
- package/src/presentation/tokens/core/BaseTokens.ts +394 -0
- package/src/presentation/tokens/core/ColorPalette.ts +398 -0
- package/src/presentation/tokens/core/TokenFactory.ts +120 -0
- package/src/presentation/utils/platformConstants.ts +124 -0
- package/src/presentation/utils/responsive.ts +516 -0
- package/src/presentation/utils/variants/compound.ts +29 -0
- package/src/presentation/utils/variants/core.ts +39 -0
- package/src/presentation/utils/variants/helpers.ts +13 -0
- package/src/presentation/utils/variants.ts +3 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AtomicSkeleton - Universal Skeleton Loading Component
|
|
3
|
+
*
|
|
4
|
+
* Displays animated skeleton placeholders for loading states
|
|
5
|
+
* Theme: {{THEME_NAME}} ({{CATEGORY}} category)
|
|
6
|
+
*
|
|
7
|
+
* Atomic Design Level: ATOM
|
|
8
|
+
* Purpose: Loading state placeholder
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* - Content loading placeholders
|
|
12
|
+
* - List item skeletons
|
|
13
|
+
* - Card skeletons
|
|
14
|
+
* - Text line skeletons
|
|
15
|
+
* - Image placeholders
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import React, { useEffect, useRef } from 'react';
|
|
19
|
+
import { View, StyleSheet, ViewStyle, Animated, DimensionValue } from 'react-native';
|
|
20
|
+
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// TYPE DEFINITIONS
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
export interface AtomicSkeletonProps {
|
|
27
|
+
/** Skeleton width */
|
|
28
|
+
width?: number | string;
|
|
29
|
+
/** Skeleton height */
|
|
30
|
+
height?: number | string;
|
|
31
|
+
/** Skeleton shape */
|
|
32
|
+
shape?: 'rectangle' | 'circle' | 'rounded';
|
|
33
|
+
/** Border radius for rounded shapes */
|
|
34
|
+
borderRadius?: number;
|
|
35
|
+
/** Animation duration in milliseconds */
|
|
36
|
+
duration?: number;
|
|
37
|
+
/** Whether to show animation */
|
|
38
|
+
animated?: boolean;
|
|
39
|
+
/** Skeleton color */
|
|
40
|
+
color?: string;
|
|
41
|
+
/** Highlight color for animation */
|
|
42
|
+
highlightColor?: string;
|
|
43
|
+
/** Style overrides */
|
|
44
|
+
style?: ViewStyle;
|
|
45
|
+
/** Test ID for testing */
|
|
46
|
+
testID?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// COMPONENT IMPLEMENTATION
|
|
51
|
+
// =============================================================================
|
|
52
|
+
|
|
53
|
+
export const AtomicSkeleton: React.FC<AtomicSkeletonProps> = ({
|
|
54
|
+
width = '100%',
|
|
55
|
+
height = 20,
|
|
56
|
+
shape = 'rectangle',
|
|
57
|
+
borderRadius,
|
|
58
|
+
duration,
|
|
59
|
+
animated = true,
|
|
60
|
+
color,
|
|
61
|
+
highlightColor,
|
|
62
|
+
style,
|
|
63
|
+
testID,
|
|
64
|
+
}) => {
|
|
65
|
+
const tokens = useAppDesignTokens();
|
|
66
|
+
const animatedValue = useRef(new Animated.Value(0)).current;
|
|
67
|
+
|
|
68
|
+
// Default values
|
|
69
|
+
const finalDuration = duration ?? tokens.animations.slowest;
|
|
70
|
+
const skeletonColor = color || tokens.colors.surfaceVariant;
|
|
71
|
+
const skeletonHighlight = highlightColor || tokens.colors.surface;
|
|
72
|
+
|
|
73
|
+
// Animation effect
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (animated) {
|
|
76
|
+
const animation = Animated.loop(
|
|
77
|
+
Animated.sequence([
|
|
78
|
+
Animated.timing(animatedValue, {
|
|
79
|
+
toValue: 1,
|
|
80
|
+
duration: finalDuration,
|
|
81
|
+
useNativeDriver: false,
|
|
82
|
+
}),
|
|
83
|
+
Animated.timing(animatedValue, {
|
|
84
|
+
toValue: 0,
|
|
85
|
+
duration: finalDuration,
|
|
86
|
+
useNativeDriver: false,
|
|
87
|
+
}),
|
|
88
|
+
])
|
|
89
|
+
);
|
|
90
|
+
animation.start();
|
|
91
|
+
|
|
92
|
+
return () => animation.stop();
|
|
93
|
+
}
|
|
94
|
+
}, [animated, finalDuration, animatedValue]);
|
|
95
|
+
|
|
96
|
+
// Calculate border radius based on shape
|
|
97
|
+
const getBorderRadius = (): number => {
|
|
98
|
+
if (borderRadius !== undefined) return borderRadius;
|
|
99
|
+
|
|
100
|
+
switch (shape) {
|
|
101
|
+
case 'circle':
|
|
102
|
+
return typeof height === 'number' ? height / 2 : 20;
|
|
103
|
+
case 'rounded':
|
|
104
|
+
return tokens.borders.radius.md;
|
|
105
|
+
case 'rectangle':
|
|
106
|
+
default:
|
|
107
|
+
return tokens.borders.radius.sm;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const skeletonStyle: ViewStyle = {
|
|
112
|
+
width: width as DimensionValue,
|
|
113
|
+
height: height as DimensionValue,
|
|
114
|
+
backgroundColor: skeletonColor,
|
|
115
|
+
borderRadius: getBorderRadius(),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (animated) {
|
|
119
|
+
const animatedStyle = {
|
|
120
|
+
backgroundColor: animatedValue.interpolate({
|
|
121
|
+
inputRange: [0, 1],
|
|
122
|
+
outputRange: [skeletonColor, skeletonHighlight],
|
|
123
|
+
}),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<Animated.View
|
|
128
|
+
style={[skeletonStyle, animatedStyle, style]}
|
|
129
|
+
testID={testID}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<View
|
|
136
|
+
style={[skeletonStyle, style]}
|
|
137
|
+
testID={testID}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// =============================================================================
|
|
143
|
+
// EXPORTS
|
|
144
|
+
// =============================================================================
|
|
145
|
+
|
|
146
|
+
export default AtomicSkeleton;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ScrollView, View } from 'react-native';
|
|
3
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
4
|
+
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
5
|
+
import { AtomicChip } from './AtomicChip';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sort option interface
|
|
9
|
+
*/
|
|
10
|
+
export interface SortOption {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
icon?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sort direction type
|
|
18
|
+
*/
|
|
19
|
+
export type SortDirection = 'asc' | 'desc';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* AtomicSort component props
|
|
23
|
+
*/
|
|
24
|
+
export interface AtomicSortProps {
|
|
25
|
+
options: SortOption[];
|
|
26
|
+
selectedId: string | null;
|
|
27
|
+
sortDirection: SortDirection;
|
|
28
|
+
onSortChange: (optionId: string, direction: SortDirection) => void;
|
|
29
|
+
showDirectionToggle?: boolean;
|
|
30
|
+
variant?: 'outlined' | 'filled' | 'soft';
|
|
31
|
+
color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info';
|
|
32
|
+
size?: 'sm' | 'md' | 'lg';
|
|
33
|
+
style?: StyleProp<ViewStyle>;
|
|
34
|
+
testID?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* AtomicSort - Horizontal Sort Chip Component
|
|
39
|
+
*
|
|
40
|
+
* A Material Design 3 compliant sort component using chip selection.
|
|
41
|
+
* Supports single selection with ascending/descending direction toggle.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* const [sortBy, setSortBy] = useState<string | null>('name');
|
|
46
|
+
* const [sortDir, setSortDir] = useState<SortDirection>('asc');
|
|
47
|
+
*
|
|
48
|
+
* <AtomicSort
|
|
49
|
+
* options={[
|
|
50
|
+
* { id: 'name', label: 'Name', icon: 'sort-alpha' },
|
|
51
|
+
* { id: 'date', label: 'Date', icon: 'schedule' },
|
|
52
|
+
* { id: 'priority', label: 'Priority', icon: 'flag' },
|
|
53
|
+
* ]}
|
|
54
|
+
* selectedId={sortBy}
|
|
55
|
+
* sortDirection={sortDir}
|
|
56
|
+
* onSortChange={(id, dir) => {
|
|
57
|
+
* setSortBy(id);
|
|
58
|
+
* setSortDir(dir);
|
|
59
|
+
* }}
|
|
60
|
+
* showDirectionToggle={true}
|
|
61
|
+
* />
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* Features:
|
|
65
|
+
* - Horizontal scrollable sort chips
|
|
66
|
+
* - Single selection (one active sort at a time)
|
|
67
|
+
* - Direction toggle (click active chip to switch asc/desc)
|
|
68
|
+
* - Visual arrow indicators (↑ asc, ↓ desc)
|
|
69
|
+
* - Theme-aware colors from design tokens
|
|
70
|
+
* - Icon support per sort option
|
|
71
|
+
* - Fully controlled component
|
|
72
|
+
*
|
|
73
|
+
* Behavior:
|
|
74
|
+
* - Click inactive chip → Selects it with ascending direction
|
|
75
|
+
* - Click active chip → Toggles direction (asc ↔ desc)
|
|
76
|
+
* - Visual feedback via filled variant for active sort
|
|
77
|
+
*/
|
|
78
|
+
export const AtomicSort: React.FC<AtomicSortProps> = ({
|
|
79
|
+
options,
|
|
80
|
+
selectedId,
|
|
81
|
+
sortDirection,
|
|
82
|
+
onSortChange,
|
|
83
|
+
showDirectionToggle = true,
|
|
84
|
+
variant = 'outlined',
|
|
85
|
+
color = 'primary',
|
|
86
|
+
size = 'md',
|
|
87
|
+
style,
|
|
88
|
+
testID,
|
|
89
|
+
}) => {
|
|
90
|
+
const tokens = useAppDesignTokens();
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Handle sort chip press
|
|
94
|
+
* - If clicking active chip: Toggle direction
|
|
95
|
+
* - If clicking inactive chip: Select it with 'asc' direction
|
|
96
|
+
*/
|
|
97
|
+
const handleSortPress = (optionId: string) => {
|
|
98
|
+
if (selectedId === optionId) {
|
|
99
|
+
const newDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
|
100
|
+
onSortChange(optionId, newDirection);
|
|
101
|
+
} else {
|
|
102
|
+
onSortChange(optionId, 'asc');
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const directionIcon = sortDirection === 'asc' ? 'arrow-upward' : 'arrow-downward';
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<ScrollView
|
|
110
|
+
horizontal
|
|
111
|
+
showsHorizontalScrollIndicator={false}
|
|
112
|
+
contentContainerStyle={{
|
|
113
|
+
paddingHorizontal: tokens.spacing.sm,
|
|
114
|
+
gap: tokens.spacing.sm,
|
|
115
|
+
}}
|
|
116
|
+
style={[style]}
|
|
117
|
+
testID={testID}
|
|
118
|
+
>
|
|
119
|
+
<View style={{ flexDirection: 'row', gap: tokens.spacing.sm }}>
|
|
120
|
+
{options.map((option) => {
|
|
121
|
+
const isSelected = selectedId === option.id;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<AtomicChip
|
|
125
|
+
key={option.id}
|
|
126
|
+
variant={isSelected ? 'filled' : variant}
|
|
127
|
+
color={color}
|
|
128
|
+
size={size}
|
|
129
|
+
leadingIcon={option.icon}
|
|
130
|
+
trailingIcon={
|
|
131
|
+
isSelected && showDirectionToggle ? directionIcon : undefined
|
|
132
|
+
}
|
|
133
|
+
selected={isSelected}
|
|
134
|
+
clickable={true}
|
|
135
|
+
onPress={() => handleSortPress(option.id)}
|
|
136
|
+
testID={`sort-chip-${option.id}`}
|
|
137
|
+
>
|
|
138
|
+
{option.label}
|
|
139
|
+
</AtomicChip>
|
|
140
|
+
);
|
|
141
|
+
})}
|
|
142
|
+
</View>
|
|
143
|
+
</ScrollView>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AtomicSwitch - Universal Switch Component
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent switch/toggle functionality with theme integration
|
|
5
|
+
* Theme: {{THEME_NAME}} ({{CATEGORY}} category)
|
|
6
|
+
*
|
|
7
|
+
* Atomic Design Level: ATOM
|
|
8
|
+
* Purpose: Basic switch/toggle input
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* - Settings toggles
|
|
12
|
+
* - Feature enable/disable
|
|
13
|
+
* - Boolean preferences
|
|
14
|
+
* - Form inputs
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import { Switch, SwitchProps, StyleSheet, ViewStyle } from 'react-native';
|
|
19
|
+
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// TYPE DEFINITIONS
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
export interface AtomicSwitchProps extends Omit<SwitchProps, 'style'> {
|
|
26
|
+
/** Switch value */
|
|
27
|
+
value: boolean;
|
|
28
|
+
/** Value change handler */
|
|
29
|
+
onValueChange: (value: boolean) => void;
|
|
30
|
+
/** Size variant */
|
|
31
|
+
size?: 'sm' | 'md' | 'lg';
|
|
32
|
+
/** Color variant */
|
|
33
|
+
variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'error';
|
|
34
|
+
/** Disabled state */
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
/** Container style override */
|
|
37
|
+
style?: ViewStyle;
|
|
38
|
+
/** Track color override */
|
|
39
|
+
trackColor?: { false: string; true: string };
|
|
40
|
+
/** Thumb color override */
|
|
41
|
+
thumbColor?: string;
|
|
42
|
+
/** iOS specific props */
|
|
43
|
+
ios_backgroundColor?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// SIZE CONFIGURATION
|
|
48
|
+
// =============================================================================
|
|
49
|
+
|
|
50
|
+
const SIZE_CONFIG = {
|
|
51
|
+
sm: { scaleX: 0.8, scaleY: 0.8 },
|
|
52
|
+
md: { scaleX: 1, scaleY: 1 },
|
|
53
|
+
lg: { scaleX: 1.2, scaleY: 1.2 },
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// COMPONENT IMPLEMENTATION
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
export const AtomicSwitch: React.FC<AtomicSwitchProps> = ({
|
|
61
|
+
value,
|
|
62
|
+
onValueChange,
|
|
63
|
+
size = 'md',
|
|
64
|
+
variant = 'primary',
|
|
65
|
+
disabled = false,
|
|
66
|
+
style,
|
|
67
|
+
trackColor,
|
|
68
|
+
thumbColor,
|
|
69
|
+
ios_backgroundColor,
|
|
70
|
+
...props
|
|
71
|
+
}) => {
|
|
72
|
+
const tokens = useAppDesignTokens();
|
|
73
|
+
const styles = getStyles(tokens);
|
|
74
|
+
|
|
75
|
+
const sizeConfig = SIZE_CONFIG[size];
|
|
76
|
+
const colors = getVariantColors(tokens, variant);
|
|
77
|
+
|
|
78
|
+
const defaultTrackColor = trackColor || {
|
|
79
|
+
false: colors.trackFalse,
|
|
80
|
+
true: colors.trackTrue,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const defaultThumbColor = thumbColor || colors.thumb;
|
|
84
|
+
const defaultIosBackgroundColor = ios_backgroundColor || colors.trackFalse;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Switch
|
|
88
|
+
value={value}
|
|
89
|
+
onValueChange={onValueChange}
|
|
90
|
+
disabled={disabled}
|
|
91
|
+
trackColor={defaultTrackColor}
|
|
92
|
+
thumbColor={defaultThumbColor}
|
|
93
|
+
ios_backgroundColor={defaultIosBackgroundColor}
|
|
94
|
+
style={[
|
|
95
|
+
styles.switch,
|
|
96
|
+
{
|
|
97
|
+
transform: [{ scaleX: sizeConfig.scaleX }, { scaleY: sizeConfig.scaleY }],
|
|
98
|
+
},
|
|
99
|
+
style,
|
|
100
|
+
]}
|
|
101
|
+
{...props}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// =============================================================================
|
|
107
|
+
// HELPER FUNCTIONS
|
|
108
|
+
// =============================================================================
|
|
109
|
+
|
|
110
|
+
const getVariantColors = (tokens: ReturnType<typeof useAppDesignTokens>, variant: AtomicSwitchProps['variant']) => {
|
|
111
|
+
switch (variant) {
|
|
112
|
+
case 'primary':
|
|
113
|
+
return {
|
|
114
|
+
trackFalse: tokens.colors.surfaceSecondary,
|
|
115
|
+
trackTrue: tokens.colors.primary,
|
|
116
|
+
thumb: tokens.colors.surface,
|
|
117
|
+
};
|
|
118
|
+
case 'secondary':
|
|
119
|
+
return {
|
|
120
|
+
trackFalse: tokens.colors.surfaceSecondary,
|
|
121
|
+
trackTrue: tokens.colors.secondary,
|
|
122
|
+
thumb: tokens.colors.surface,
|
|
123
|
+
};
|
|
124
|
+
case 'success':
|
|
125
|
+
return {
|
|
126
|
+
trackFalse: tokens.colors.surfaceSecondary,
|
|
127
|
+
trackTrue: tokens.colors.success,
|
|
128
|
+
thumb: tokens.colors.surface,
|
|
129
|
+
};
|
|
130
|
+
case 'warning':
|
|
131
|
+
return {
|
|
132
|
+
trackFalse: tokens.colors.surfaceSecondary,
|
|
133
|
+
trackTrue: tokens.colors.warning,
|
|
134
|
+
thumb: tokens.colors.surface,
|
|
135
|
+
};
|
|
136
|
+
case 'error':
|
|
137
|
+
return {
|
|
138
|
+
trackFalse: tokens.colors.surfaceSecondary,
|
|
139
|
+
trackTrue: tokens.colors.error,
|
|
140
|
+
thumb: tokens.colors.surface,
|
|
141
|
+
};
|
|
142
|
+
default:
|
|
143
|
+
return {
|
|
144
|
+
trackFalse: tokens.colors.surfaceSecondary,
|
|
145
|
+
trackTrue: tokens.colors.primary,
|
|
146
|
+
thumb: tokens.colors.surface,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// =============================================================================
|
|
152
|
+
// STYLES
|
|
153
|
+
// =============================================================================
|
|
154
|
+
|
|
155
|
+
const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
|
|
156
|
+
StyleSheet.create({
|
|
157
|
+
switch: {
|
|
158
|
+
// Default switch styling is handled by platform
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// EXPORTS
|
|
164
|
+
// =============================================================================
|
|
165
|
+
|
|
166
|
+
export default AtomicSwitch;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text as PaperText } from 'react-native-paper';
|
|
3
|
+
import { StyleProp, TextStyle } from 'react-native';
|
|
4
|
+
|
|
5
|
+
export type TextStyleVariant =
|
|
6
|
+
| 'displayLarge' | 'displayMedium' | 'displaySmall'
|
|
7
|
+
| 'headlineLarge' | 'headlineMedium' | 'headlineSmall'
|
|
8
|
+
| 'titleLarge' | 'titleMedium' | 'titleSmall'
|
|
9
|
+
| 'bodyLarge' | 'bodyMedium' | 'bodySmall'
|
|
10
|
+
| 'labelLarge' | 'labelMedium' | 'labelSmall';
|
|
11
|
+
|
|
12
|
+
export type ColorVariant =
|
|
13
|
+
| 'primary'
|
|
14
|
+
| 'secondary'
|
|
15
|
+
| 'tertiary'
|
|
16
|
+
| 'disabled'
|
|
17
|
+
| 'inverse'
|
|
18
|
+
| 'success'
|
|
19
|
+
| 'error'
|
|
20
|
+
| 'warning'
|
|
21
|
+
| 'info';
|
|
22
|
+
|
|
23
|
+
export interface AtomicTextProps {
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
type?: TextStyleVariant;
|
|
26
|
+
color?: ColorVariant | string;
|
|
27
|
+
numberOfLines?: number;
|
|
28
|
+
style?: StyleProp<TextStyle>;
|
|
29
|
+
testID?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const AtomicText: React.FC<AtomicTextProps> = ({
|
|
33
|
+
children,
|
|
34
|
+
type = 'bodyMedium',
|
|
35
|
+
color,
|
|
36
|
+
numberOfLines,
|
|
37
|
+
style,
|
|
38
|
+
testID,
|
|
39
|
+
}) => {
|
|
40
|
+
return (
|
|
41
|
+
<PaperText
|
|
42
|
+
variant={type}
|
|
43
|
+
numberOfLines={numberOfLines}
|
|
44
|
+
style={style}
|
|
45
|
+
testID={testID}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</PaperText>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AtomicTextArea Component
|
|
3
|
+
*
|
|
4
|
+
* A multiline text input component with Material Design 3 integration
|
|
5
|
+
* for longer text entry with consistent styling.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - React Native Paper TextInput with multiline
|
|
9
|
+
* - Material Design 3 outlined/filled/flat variants
|
|
10
|
+
* - Error, success, disabled states
|
|
11
|
+
* - Character counter with max length
|
|
12
|
+
* - Helper text for guidance or errors
|
|
13
|
+
* - Configurable rows for height
|
|
14
|
+
* - Theme-aware styling
|
|
15
|
+
* - Full accessibility support
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const [description, setDescription] = useState('');
|
|
20
|
+
*
|
|
21
|
+
* <AtomicTextArea
|
|
22
|
+
* value={description}
|
|
23
|
+
* onChangeText={setDescription}
|
|
24
|
+
* label="Description"
|
|
25
|
+
* placeholder="Enter description..."
|
|
26
|
+
* maxLength={500}
|
|
27
|
+
* showCharacterCount
|
|
28
|
+
* rows={6}
|
|
29
|
+
* helperText="Provide a detailed description"
|
|
30
|
+
* />
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import React from 'react';
|
|
35
|
+
import { View, StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
36
|
+
import { TextInput, HelperText } from 'react-native-paper';
|
|
37
|
+
import { useAppDesignTokens } from '../hooks/useAppDesignTokens';
|
|
38
|
+
|
|
39
|
+
export type AtomicTextAreaVariant = 'outlined' | 'filled' | 'flat';
|
|
40
|
+
export type AtomicTextAreaState = 'default' | 'error' | 'success' | 'disabled';
|
|
41
|
+
export type AtomicTextAreaSize = 'sm' | 'md' | 'lg';
|
|
42
|
+
|
|
43
|
+
export interface AtomicTextAreaProps {
|
|
44
|
+
/** Textarea label */
|
|
45
|
+
label?: string;
|
|
46
|
+
/** Current textarea value */
|
|
47
|
+
value?: string;
|
|
48
|
+
/** Value change callback */
|
|
49
|
+
onChangeText?: (text: string) => void;
|
|
50
|
+
/** Textarea variant (outlined, filled, flat) */
|
|
51
|
+
variant?: AtomicTextAreaVariant;
|
|
52
|
+
/** Textarea state (default, error, success, disabled) */
|
|
53
|
+
state?: AtomicTextAreaState;
|
|
54
|
+
/** Textarea size (sm, md, lg) */
|
|
55
|
+
size?: AtomicTextAreaSize;
|
|
56
|
+
/** Placeholder text */
|
|
57
|
+
placeholder?: string;
|
|
58
|
+
/** Helper text below textarea */
|
|
59
|
+
helperText?: string;
|
|
60
|
+
/** Maximum character length */
|
|
61
|
+
maxLength?: number;
|
|
62
|
+
/** Show character counter */
|
|
63
|
+
showCharacterCount?: boolean;
|
|
64
|
+
/** Number of visible text rows */
|
|
65
|
+
rows?: number;
|
|
66
|
+
/** Minimum height in pixels */
|
|
67
|
+
minHeight?: number;
|
|
68
|
+
/** Auto-capitalize */
|
|
69
|
+
autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
|
|
70
|
+
/** Auto-correct */
|
|
71
|
+
autoCorrect?: boolean;
|
|
72
|
+
/** Disabled state */
|
|
73
|
+
disabled?: boolean;
|
|
74
|
+
/** Container style */
|
|
75
|
+
style?: StyleProp<ViewStyle>;
|
|
76
|
+
/** Input text style */
|
|
77
|
+
inputStyle?: StyleProp<TextStyle>;
|
|
78
|
+
/** Test ID for E2E testing */
|
|
79
|
+
testID?: string;
|
|
80
|
+
/** Blur callback */
|
|
81
|
+
onBlur?: () => void;
|
|
82
|
+
/** Focus callback */
|
|
83
|
+
onFocus?: () => void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* AtomicTextArea - Material Design 3 Multiline Text Input
|
|
88
|
+
*/
|
|
89
|
+
export const AtomicTextArea: React.FC<AtomicTextAreaProps> = ({
|
|
90
|
+
variant = 'outlined',
|
|
91
|
+
state = 'default',
|
|
92
|
+
size = 'md',
|
|
93
|
+
label,
|
|
94
|
+
value = '',
|
|
95
|
+
onChangeText,
|
|
96
|
+
placeholder,
|
|
97
|
+
helperText,
|
|
98
|
+
maxLength,
|
|
99
|
+
showCharacterCount = false,
|
|
100
|
+
rows = 4,
|
|
101
|
+
minHeight,
|
|
102
|
+
autoCapitalize = 'sentences',
|
|
103
|
+
autoCorrect = true,
|
|
104
|
+
disabled = false,
|
|
105
|
+
style,
|
|
106
|
+
inputStyle,
|
|
107
|
+
testID,
|
|
108
|
+
onBlur,
|
|
109
|
+
onFocus,
|
|
110
|
+
}) => {
|
|
111
|
+
const tokens = useAppDesignTokens();
|
|
112
|
+
const isDisabled = state === 'disabled' || disabled;
|
|
113
|
+
const characterCount = value?.toString().length || 0;
|
|
114
|
+
|
|
115
|
+
// Map variant to Paper mode
|
|
116
|
+
const getPaperMode = (): 'outlined' | 'flat' => {
|
|
117
|
+
if (variant === 'outlined') return 'outlined';
|
|
118
|
+
return 'flat'; // filled and flat both use 'flat' mode
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Map state to Paper error prop
|
|
122
|
+
const hasError = state === 'error';
|
|
123
|
+
|
|
124
|
+
// Calculate height based on rows
|
|
125
|
+
const getTextAreaHeight = () => {
|
|
126
|
+
if (minHeight) return minHeight;
|
|
127
|
+
|
|
128
|
+
// Base line height: 24px per row (approximate)
|
|
129
|
+
const lineHeight = 24;
|
|
130
|
+
const padding = 32; // Top and bottom padding
|
|
131
|
+
return (rows * lineHeight) + padding;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Get text color based on state
|
|
135
|
+
const getTextColor = () => {
|
|
136
|
+
if (state === 'error') return tokens.colors.error;
|
|
137
|
+
if (state === 'success') return tokens.colors.success;
|
|
138
|
+
return tokens.colors.onSurface;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<View style={style} testID={testID}>
|
|
143
|
+
<TextInput
|
|
144
|
+
mode={getPaperMode()}
|
|
145
|
+
label={label}
|
|
146
|
+
value={value}
|
|
147
|
+
onChangeText={onChangeText}
|
|
148
|
+
placeholder={placeholder}
|
|
149
|
+
error={hasError}
|
|
150
|
+
disabled={isDisabled}
|
|
151
|
+
maxLength={maxLength}
|
|
152
|
+
autoCapitalize={autoCapitalize}
|
|
153
|
+
autoCorrect={autoCorrect}
|
|
154
|
+
multiline={true}
|
|
155
|
+
numberOfLines={rows}
|
|
156
|
+
style={[
|
|
157
|
+
{ height: getTextAreaHeight(), textAlignVertical: 'top' },
|
|
158
|
+
inputStyle,
|
|
159
|
+
]}
|
|
160
|
+
textColor={getTextColor()}
|
|
161
|
+
onBlur={onBlur}
|
|
162
|
+
onFocus={onFocus}
|
|
163
|
+
testID={testID ? `${testID}-input` : undefined}
|
|
164
|
+
/>
|
|
165
|
+
|
|
166
|
+
{(helperText || showCharacterCount) && (
|
|
167
|
+
<View style={{
|
|
168
|
+
flexDirection: 'row',
|
|
169
|
+
justifyContent: 'space-between',
|
|
170
|
+
marginTop: tokens.spacing.xs,
|
|
171
|
+
}}>
|
|
172
|
+
{helperText && (
|
|
173
|
+
<HelperText
|
|
174
|
+
type={hasError ? 'error' : 'info'}
|
|
175
|
+
visible={Boolean(helperText)}
|
|
176
|
+
style={{ flex: 1 }}
|
|
177
|
+
testID={testID ? `${testID}-helper` : undefined}
|
|
178
|
+
>
|
|
179
|
+
{helperText}
|
|
180
|
+
</HelperText>
|
|
181
|
+
)}
|
|
182
|
+
{showCharacterCount && maxLength && (
|
|
183
|
+
<HelperText
|
|
184
|
+
type="info"
|
|
185
|
+
visible={true}
|
|
186
|
+
style={{ marginLeft: tokens.spacing.xs }}
|
|
187
|
+
testID={testID ? `${testID}-count` : undefined}
|
|
188
|
+
>
|
|
189
|
+
{characterCount}/{maxLength}
|
|
190
|
+
</HelperText>
|
|
191
|
+
)}
|
|
192
|
+
</View>
|
|
193
|
+
)}
|
|
194
|
+
</View>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export type { AtomicTextAreaProps as TextAreaProps };
|