@umituz/react-native-design-system 2.1.0 → 2.1.3
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/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/molecules/SearchBar/SearchBar.tsx +142 -0
- package/src/molecules/SearchBar/SearchHistory.tsx +131 -0
- package/src/molecules/SearchBar/SearchSuggestions.tsx +113 -0
- package/src/molecules/SearchBar/index.ts +4 -0
- package/src/molecules/SearchBar/types.ts +46 -0
- package/src/molecules/StepProgress/StepProgress.tsx +50 -0
- package/src/molecules/StepProgress/index.ts +1 -0
- package/src/molecules/index.ts +1 -0
- package/src/molecules/SearchBar.tsx +0 -198
- package/src/theme/hooks/useAppDesignTokens.ts +0 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive and safe area utilities",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
TextInput,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
ActivityIndicator,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import { useAppDesignTokens } from '../../theme';
|
|
10
|
+
import { AtomicIcon } from '../../atoms/AtomicIcon';
|
|
11
|
+
import type { SearchBarProps } from './types';
|
|
12
|
+
|
|
13
|
+
export const SearchBar = forwardRef<TextInput, SearchBarProps>(({
|
|
14
|
+
value,
|
|
15
|
+
onChangeText,
|
|
16
|
+
onSubmit,
|
|
17
|
+
onClear,
|
|
18
|
+
onFocus,
|
|
19
|
+
onBlur,
|
|
20
|
+
placeholder = 'Search...',
|
|
21
|
+
autoFocus = false,
|
|
22
|
+
loading = false,
|
|
23
|
+
disabled = false,
|
|
24
|
+
containerStyle,
|
|
25
|
+
inputStyle,
|
|
26
|
+
testID,
|
|
27
|
+
}, ref) => {
|
|
28
|
+
const tokens = useAppDesignTokens();
|
|
29
|
+
|
|
30
|
+
const handleClear = () => {
|
|
31
|
+
onChangeText('');
|
|
32
|
+
onClear?.();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const showClear = value.length > 0 && !loading;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<View
|
|
39
|
+
style={[
|
|
40
|
+
styles.container,
|
|
41
|
+
{
|
|
42
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
43
|
+
borderColor: tokens.colors.border,
|
|
44
|
+
},
|
|
45
|
+
containerStyle,
|
|
46
|
+
]}
|
|
47
|
+
testID={testID}
|
|
48
|
+
>
|
|
49
|
+
<View style={styles.iconContainer}>
|
|
50
|
+
<AtomicIcon
|
|
51
|
+
name="search"
|
|
52
|
+
size="md"
|
|
53
|
+
customColor={tokens.colors.textSecondary}
|
|
54
|
+
/>
|
|
55
|
+
</View>
|
|
56
|
+
|
|
57
|
+
<TextInput
|
|
58
|
+
ref={ref}
|
|
59
|
+
value={value}
|
|
60
|
+
onChangeText={onChangeText}
|
|
61
|
+
onSubmitEditing={onSubmit}
|
|
62
|
+
onFocus={onFocus}
|
|
63
|
+
onBlur={onBlur}
|
|
64
|
+
placeholder={placeholder}
|
|
65
|
+
placeholderTextColor={tokens.colors.textSecondary}
|
|
66
|
+
autoFocus={autoFocus}
|
|
67
|
+
editable={!disabled}
|
|
68
|
+
returnKeyType="search"
|
|
69
|
+
autoCapitalize="none"
|
|
70
|
+
autoCorrect={false}
|
|
71
|
+
style={[
|
|
72
|
+
styles.input,
|
|
73
|
+
{
|
|
74
|
+
color: tokens.colors.textPrimary,
|
|
75
|
+
fontSize: 16, // Body medium usually
|
|
76
|
+
},
|
|
77
|
+
inputStyle,
|
|
78
|
+
]}
|
|
79
|
+
/>
|
|
80
|
+
|
|
81
|
+
{(loading || showClear) && (
|
|
82
|
+
<View style={styles.rightActions}>
|
|
83
|
+
{loading && (
|
|
84
|
+
<ActivityIndicator
|
|
85
|
+
size="small"
|
|
86
|
+
color={tokens.colors.primary}
|
|
87
|
+
style={styles.loader}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
{showClear && (
|
|
92
|
+
<TouchableOpacity
|
|
93
|
+
onPress={handleClear}
|
|
94
|
+
style={styles.clearButton}
|
|
95
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
96
|
+
accessibilityRole="button"
|
|
97
|
+
accessibilityLabel="Clear search"
|
|
98
|
+
>
|
|
99
|
+
<AtomicIcon
|
|
100
|
+
name="close-circle"
|
|
101
|
+
size="md"
|
|
102
|
+
customColor={tokens.colors.textSecondary}
|
|
103
|
+
/>
|
|
104
|
+
</TouchableOpacity>
|
|
105
|
+
)}
|
|
106
|
+
</View>
|
|
107
|
+
)}
|
|
108
|
+
</View>
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
SearchBar.displayName = 'SearchBar';
|
|
113
|
+
|
|
114
|
+
const styles = StyleSheet.create({
|
|
115
|
+
container: {
|
|
116
|
+
flexDirection: 'row',
|
|
117
|
+
alignItems: 'center',
|
|
118
|
+
paddingHorizontal: 12,
|
|
119
|
+
height: 48,
|
|
120
|
+
borderRadius: 24, // Pill shape
|
|
121
|
+
borderWidth: 1,
|
|
122
|
+
},
|
|
123
|
+
iconContainer: {
|
|
124
|
+
marginRight: 8,
|
|
125
|
+
},
|
|
126
|
+
input: {
|
|
127
|
+
flex: 1,
|
|
128
|
+
height: '100%',
|
|
129
|
+
paddingVertical: 0, // Reset default padding
|
|
130
|
+
},
|
|
131
|
+
rightActions: {
|
|
132
|
+
flexDirection: 'row',
|
|
133
|
+
alignItems: 'center',
|
|
134
|
+
marginLeft: 8,
|
|
135
|
+
},
|
|
136
|
+
loader: {
|
|
137
|
+
marginRight: 8,
|
|
138
|
+
},
|
|
139
|
+
clearButton: {
|
|
140
|
+
padding: 2,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
TouchableOpacity,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
} from 'react-native';
|
|
7
|
+
import { AtomicText } from '../../atoms/AtomicText';
|
|
8
|
+
import { AtomicIcon } from '../../atoms/AtomicIcon';
|
|
9
|
+
import { useAppDesignTokens } from '../../theme';
|
|
10
|
+
import type { SearchHistoryProps } from './types';
|
|
11
|
+
|
|
12
|
+
export const SearchHistory: React.FC<SearchHistoryProps> = ({
|
|
13
|
+
history,
|
|
14
|
+
onSelectItem,
|
|
15
|
+
onRemoveItem,
|
|
16
|
+
onClearAll,
|
|
17
|
+
maxItems = 10,
|
|
18
|
+
style,
|
|
19
|
+
title = 'Recent Searches',
|
|
20
|
+
clearLabel = 'Clear All',
|
|
21
|
+
}) => {
|
|
22
|
+
const tokens = useAppDesignTokens();
|
|
23
|
+
|
|
24
|
+
if (!history || history.length === 0) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const displayedHistory = history.slice(0, maxItems);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<View style={[styles.container, style]}>
|
|
32
|
+
<View style={styles.header}>
|
|
33
|
+
<AtomicText
|
|
34
|
+
type="labelLarge"
|
|
35
|
+
style={{ color: tokens.colors.textSecondary }}
|
|
36
|
+
>
|
|
37
|
+
{title}
|
|
38
|
+
</AtomicText>
|
|
39
|
+
<TouchableOpacity
|
|
40
|
+
onPress={onClearAll}
|
|
41
|
+
style={styles.clearButton}
|
|
42
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
43
|
+
>
|
|
44
|
+
<AtomicText
|
|
45
|
+
type="labelMedium"
|
|
46
|
+
style={{ color: tokens.colors.primary }}
|
|
47
|
+
>
|
|
48
|
+
{clearLabel}
|
|
49
|
+
</AtomicText>
|
|
50
|
+
</TouchableOpacity>
|
|
51
|
+
</View>
|
|
52
|
+
|
|
53
|
+
{displayedHistory.map((item) => (
|
|
54
|
+
<TouchableOpacity
|
|
55
|
+
key={item.id}
|
|
56
|
+
onPress={() => onSelectItem(item.query)}
|
|
57
|
+
style={styles.item}
|
|
58
|
+
>
|
|
59
|
+
<View style={styles.itemLeft}>
|
|
60
|
+
<AtomicIcon
|
|
61
|
+
name="time-outline"
|
|
62
|
+
size="sm"
|
|
63
|
+
customColor={tokens.colors.textSecondary}
|
|
64
|
+
/>
|
|
65
|
+
<AtomicText
|
|
66
|
+
type="bodyMedium"
|
|
67
|
+
style={[
|
|
68
|
+
styles.itemText,
|
|
69
|
+
{ color: tokens.colors.textPrimary }
|
|
70
|
+
]}
|
|
71
|
+
numberOfLines={1}
|
|
72
|
+
>
|
|
73
|
+
{item.query}
|
|
74
|
+
</AtomicText>
|
|
75
|
+
</View>
|
|
76
|
+
|
|
77
|
+
<TouchableOpacity
|
|
78
|
+
onPress={() => onRemoveItem(item.id)}
|
|
79
|
+
style={styles.removeButton}
|
|
80
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
81
|
+
>
|
|
82
|
+
<AtomicIcon
|
|
83
|
+
name="close"
|
|
84
|
+
size="sm"
|
|
85
|
+
customColor={tokens.colors.textSecondary}
|
|
86
|
+
/>
|
|
87
|
+
</TouchableOpacity>
|
|
88
|
+
</TouchableOpacity>
|
|
89
|
+
))}
|
|
90
|
+
</View>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const styles = StyleSheet.create({
|
|
95
|
+
container: {
|
|
96
|
+
paddingVertical: 8,
|
|
97
|
+
},
|
|
98
|
+
header: {
|
|
99
|
+
flexDirection: 'row',
|
|
100
|
+
justifyContent: 'space-between',
|
|
101
|
+
alignItems: 'center',
|
|
102
|
+
paddingHorizontal: 16,
|
|
103
|
+
paddingVertical: 8,
|
|
104
|
+
marginBottom: 4,
|
|
105
|
+
},
|
|
106
|
+
clearButton: {
|
|
107
|
+
paddingVertical: 4,
|
|
108
|
+
paddingHorizontal: 8,
|
|
109
|
+
},
|
|
110
|
+
item: {
|
|
111
|
+
flexDirection: 'row',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
justifyContent: 'space-between',
|
|
114
|
+
paddingHorizontal: 16,
|
|
115
|
+
paddingVertical: 12,
|
|
116
|
+
minHeight: 48,
|
|
117
|
+
},
|
|
118
|
+
itemLeft: {
|
|
119
|
+
flexDirection: 'row',
|
|
120
|
+
alignItems: 'center',
|
|
121
|
+
flex: 1,
|
|
122
|
+
marginRight: 12,
|
|
123
|
+
},
|
|
124
|
+
itemText: {
|
|
125
|
+
marginLeft: 12,
|
|
126
|
+
flex: 1,
|
|
127
|
+
},
|
|
128
|
+
removeButton: {
|
|
129
|
+
padding: 4,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
TouchableOpacity,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
} from 'react-native';
|
|
7
|
+
import { AtomicText } from '../../atoms/AtomicText';
|
|
8
|
+
import { useAppDesignTokens } from '../../theme';
|
|
9
|
+
import type { SearchSuggestionsProps } from './types';
|
|
10
|
+
|
|
11
|
+
export function SearchSuggestions<T>({
|
|
12
|
+
query,
|
|
13
|
+
suggestions,
|
|
14
|
+
renderItem,
|
|
15
|
+
onSelectSuggestion,
|
|
16
|
+
maxSuggestions = 5,
|
|
17
|
+
style,
|
|
18
|
+
emptyComponent,
|
|
19
|
+
}: SearchSuggestionsProps<T>) {
|
|
20
|
+
const tokens = useAppDesignTokens();
|
|
21
|
+
|
|
22
|
+
if (!query.trim() && !emptyComponent) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (suggestions.length === 0) {
|
|
27
|
+
return <>{emptyComponent}</>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const displayedSuggestions = suggestions.slice(0, maxSuggestions);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<View
|
|
34
|
+
style={[
|
|
35
|
+
styles.container,
|
|
36
|
+
{
|
|
37
|
+
backgroundColor: tokens.colors.surface,
|
|
38
|
+
},
|
|
39
|
+
style,
|
|
40
|
+
]}
|
|
41
|
+
>
|
|
42
|
+
{displayedSuggestions.map((item, index) => (
|
|
43
|
+
<TouchableOpacity
|
|
44
|
+
key={index}
|
|
45
|
+
onPress={() => onSelectSuggestion(item)}
|
|
46
|
+
style={[
|
|
47
|
+
styles.item,
|
|
48
|
+
index > 0 ? {
|
|
49
|
+
borderTopWidth: 1,
|
|
50
|
+
borderTopColor: tokens.colors.border,
|
|
51
|
+
} : undefined,
|
|
52
|
+
]}
|
|
53
|
+
>
|
|
54
|
+
{renderItem(item, query)}
|
|
55
|
+
</TouchableOpacity>
|
|
56
|
+
))}
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const styles = StyleSheet.create({
|
|
62
|
+
container: {
|
|
63
|
+
borderRadius: 12,
|
|
64
|
+
overflow: 'hidden',
|
|
65
|
+
},
|
|
66
|
+
item: {
|
|
67
|
+
paddingHorizontal: 16,
|
|
68
|
+
paddingVertical: 12,
|
|
69
|
+
minHeight: 48,
|
|
70
|
+
justifyContent: 'center',
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Default suggestion renderer (simple text with highlighting)
|
|
76
|
+
*/
|
|
77
|
+
export const DefaultSuggestionRenderer = (
|
|
78
|
+
text: string,
|
|
79
|
+
query: string,
|
|
80
|
+
tokens: ReturnType<typeof useAppDesignTokens>
|
|
81
|
+
) => {
|
|
82
|
+
const lowerText = text.toLowerCase();
|
|
83
|
+
const lowerQuery = query.toLowerCase();
|
|
84
|
+
const index = lowerText.indexOf(lowerQuery);
|
|
85
|
+
|
|
86
|
+
if (index === -1) {
|
|
87
|
+
return (
|
|
88
|
+
<AtomicText
|
|
89
|
+
type="bodyMedium"
|
|
90
|
+
style={{ color: tokens.colors.textPrimary }}
|
|
91
|
+
>
|
|
92
|
+
{text}
|
|
93
|
+
</AtomicText>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const before = text.slice(0, index);
|
|
98
|
+
const match = text.slice(index, index + query.length);
|
|
99
|
+
const after = text.slice(index + query.length);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<AtomicText
|
|
103
|
+
type="bodyMedium"
|
|
104
|
+
style={{ color: tokens.colors.textPrimary }}
|
|
105
|
+
>
|
|
106
|
+
{before}
|
|
107
|
+
<AtomicText type="bodyMedium" style={{ fontWeight: '700', color: tokens.colors.primary }}>
|
|
108
|
+
{match}
|
|
109
|
+
</AtomicText>
|
|
110
|
+
{after}
|
|
111
|
+
</AtomicText>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export interface SearchHistoryItem {
|
|
4
|
+
id: string;
|
|
5
|
+
query: string;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SearchBarProps {
|
|
10
|
+
value: string;
|
|
11
|
+
onChangeText: (text: string) => void;
|
|
12
|
+
onSubmit?: () => void;
|
|
13
|
+
onClear?: () => void;
|
|
14
|
+
onFocus?: () => void;
|
|
15
|
+
onBlur?: () => void;
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
autoFocus?: boolean;
|
|
18
|
+
loading?: boolean;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
showCancelButton?: boolean;
|
|
21
|
+
onCancel?: () => void;
|
|
22
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
23
|
+
inputStyle?: StyleProp<TextStyle>;
|
|
24
|
+
testID?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SearchHistoryProps {
|
|
28
|
+
history: SearchHistoryItem[];
|
|
29
|
+
onSelectItem: (query: string) => void;
|
|
30
|
+
onRemoveItem: (id: string) => void;
|
|
31
|
+
onClearAll: () => void;
|
|
32
|
+
maxItems?: number;
|
|
33
|
+
style?: StyleProp<ViewStyle>;
|
|
34
|
+
title?: string;
|
|
35
|
+
clearLabel?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SearchSuggestionsProps<T> {
|
|
39
|
+
query: string;
|
|
40
|
+
suggestions: T[];
|
|
41
|
+
renderItem: (item: T, query: string) => React.ReactNode;
|
|
42
|
+
onSelectSuggestion: (item: T) => void;
|
|
43
|
+
maxSuggestions?: number;
|
|
44
|
+
style?: StyleProp<ViewStyle>;
|
|
45
|
+
emptyComponent?: React.ReactNode;
|
|
46
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { View, StyleSheet, ViewStyle } from "react-native";
|
|
3
|
+
import { useAppDesignTokens } from "../../theme/hooks/useAppDesignTokens";
|
|
4
|
+
|
|
5
|
+
export interface StepProgressProps {
|
|
6
|
+
currentStep: number;
|
|
7
|
+
totalSteps: number;
|
|
8
|
+
style?: ViewStyle;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const StepProgress: React.FC<StepProgressProps> = ({
|
|
12
|
+
currentStep,
|
|
13
|
+
totalSteps,
|
|
14
|
+
style,
|
|
15
|
+
}) => {
|
|
16
|
+
const tokens = useAppDesignTokens();
|
|
17
|
+
|
|
18
|
+
const styles = useMemo(
|
|
19
|
+
() =>
|
|
20
|
+
StyleSheet.create({
|
|
21
|
+
container: {
|
|
22
|
+
flexDirection: "row",
|
|
23
|
+
gap: 8,
|
|
24
|
+
paddingHorizontal: 24,
|
|
25
|
+
paddingVertical: 16,
|
|
26
|
+
},
|
|
27
|
+
step: {
|
|
28
|
+
flex: 1,
|
|
29
|
+
height: 4,
|
|
30
|
+
borderRadius: 2,
|
|
31
|
+
backgroundColor: tokens.colors.border,
|
|
32
|
+
},
|
|
33
|
+
activeStep: {
|
|
34
|
+
backgroundColor: tokens.colors.primary,
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
[tokens],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View style={[styles.container, style]}>
|
|
42
|
+
{Array.from({ length: totalSteps }).map((_, index) => (
|
|
43
|
+
<View
|
|
44
|
+
key={index}
|
|
45
|
+
style={[styles.step, index < currentStep && styles.activeStep]}
|
|
46
|
+
/>
|
|
47
|
+
))}
|
|
48
|
+
</View>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./StepProgress";
|
package/src/molecules/index.ts
CHANGED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SearchBar Molecule - Enhanced Search Input with Clear Button
|
|
3
|
-
*
|
|
4
|
-
* Universal search input with clear functionality and loading state
|
|
5
|
-
* Fully configurable for general purpose use
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import React from 'react';
|
|
9
|
-
import {
|
|
10
|
-
View,
|
|
11
|
-
TextInput,
|
|
12
|
-
TouchableOpacity,
|
|
13
|
-
ActivityIndicator,
|
|
14
|
-
StyleSheet,
|
|
15
|
-
type StyleProp,
|
|
16
|
-
type ViewStyle,
|
|
17
|
-
} from 'react-native';
|
|
18
|
-
import { useAppDesignTokens } from '../theme';
|
|
19
|
-
import { AtomicIcon } from '../atoms';
|
|
20
|
-
|
|
21
|
-
export interface SearchBarProps {
|
|
22
|
-
value: string;
|
|
23
|
-
onChangeText: (text: string) => void;
|
|
24
|
-
onSubmit?: () => void;
|
|
25
|
-
onClear?: () => void;
|
|
26
|
-
placeholder?: string;
|
|
27
|
-
autoFocus?: boolean;
|
|
28
|
-
loading?: boolean;
|
|
29
|
-
disabled?: boolean;
|
|
30
|
-
style?: StyleProp<ViewStyle>;
|
|
31
|
-
containerStyle?: ViewStyle;
|
|
32
|
-
searchIconName?: string;
|
|
33
|
-
clearIconName?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const useSearchBarLogic = (
|
|
37
|
-
value: string,
|
|
38
|
-
loading: boolean,
|
|
39
|
-
onChangeText: (text: string) => void,
|
|
40
|
-
onClear?: () => void,
|
|
41
|
-
onSubmit?: () => void
|
|
42
|
-
) => {
|
|
43
|
-
const handleClear = React.useCallback(() => {
|
|
44
|
-
onChangeText('');
|
|
45
|
-
onClear?.();
|
|
46
|
-
}, [onChangeText, onClear]);
|
|
47
|
-
|
|
48
|
-
const handleSubmit = React.useCallback(() => {
|
|
49
|
-
onSubmit?.();
|
|
50
|
-
}, [onSubmit]);
|
|
51
|
-
|
|
52
|
-
const showClear = React.useMemo(() =>
|
|
53
|
-
value.length > 0 && !loading, [value, loading]
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
return { handleClear, handleSubmit, showClear };
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const SearchBarInput: React.FC<{
|
|
60
|
-
value: string;
|
|
61
|
-
placeholder?: string;
|
|
62
|
-
autoFocus?: boolean;
|
|
63
|
-
disabled?: boolean;
|
|
64
|
-
style?: StyleProp<ViewStyle>;
|
|
65
|
-
onChangeText: (text: string) => void;
|
|
66
|
-
onSubmitEditing?: () => void;
|
|
67
|
-
tokens: ReturnType<typeof useAppDesignTokens>;
|
|
68
|
-
}> = ({ value, placeholder, autoFocus, disabled, style, onChangeText, onSubmitEditing, tokens }) => {
|
|
69
|
-
const styles = React.useMemo(() => getSearchBarStyles(), []);
|
|
70
|
-
|
|
71
|
-
const inputStyle = React.useMemo(() => [
|
|
72
|
-
styles.input,
|
|
73
|
-
{
|
|
74
|
-
color: tokens.colors.textPrimary,
|
|
75
|
-
...tokens.typography.bodyMedium,
|
|
76
|
-
},
|
|
77
|
-
], [styles.input, tokens.colors.textPrimary, tokens.typography.bodyMedium]);
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<TextInput
|
|
81
|
-
value={value}
|
|
82
|
-
onChangeText={onChangeText}
|
|
83
|
-
onSubmitEditing={onSubmitEditing}
|
|
84
|
-
placeholder={placeholder}
|
|
85
|
-
placeholderTextColor={tokens.colors.textSecondary}
|
|
86
|
-
autoFocus={autoFocus}
|
|
87
|
-
editable={!disabled}
|
|
88
|
-
returnKeyType="search"
|
|
89
|
-
autoCapitalize="none"
|
|
90
|
-
autoCorrect={false}
|
|
91
|
-
style={[inputStyle, style]}
|
|
92
|
-
/>
|
|
93
|
-
);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const SearchBarIcons: React.FC<{
|
|
97
|
-
searchIconName: string;
|
|
98
|
-
clearIconName: string;
|
|
99
|
-
loading: boolean;
|
|
100
|
-
showClear: boolean;
|
|
101
|
-
onClear: () => void;
|
|
102
|
-
tokens: ReturnType<typeof useAppDesignTokens>;
|
|
103
|
-
}> = ({ searchIconName, clearIconName, loading, showClear, onClear, tokens }) => {
|
|
104
|
-
const styles = React.useMemo(() => getSearchBarStyles(), []);
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<>
|
|
108
|
-
<AtomicIcon
|
|
109
|
-
name={searchIconName}
|
|
110
|
-
customSize={20}
|
|
111
|
-
customColor={tokens.colors.textSecondary}
|
|
112
|
-
style={styles.searchIcon}
|
|
113
|
-
/>
|
|
114
|
-
|
|
115
|
-
{loading && (
|
|
116
|
-
<ActivityIndicator
|
|
117
|
-
size="small"
|
|
118
|
-
color={tokens.colors.primary}
|
|
119
|
-
style={styles.icon}
|
|
120
|
-
/>
|
|
121
|
-
)}
|
|
122
|
-
|
|
123
|
-
{showClear && (
|
|
124
|
-
<TouchableOpacity onPress={onClear} style={styles.icon}>
|
|
125
|
-
<AtomicIcon name={clearIconName} customSize={20} customColor={tokens.colors.textSecondary} />
|
|
126
|
-
</TouchableOpacity>
|
|
127
|
-
)}
|
|
128
|
-
</>
|
|
129
|
-
);
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const SearchBarComponent: React.FC<SearchBarProps> = ({
|
|
133
|
-
value,
|
|
134
|
-
onChangeText,
|
|
135
|
-
onSubmit,
|
|
136
|
-
onClear,
|
|
137
|
-
placeholder,
|
|
138
|
-
autoFocus = false,
|
|
139
|
-
loading = false,
|
|
140
|
-
disabled = false,
|
|
141
|
-
style,
|
|
142
|
-
containerStyle,
|
|
143
|
-
searchIconName = 'search',
|
|
144
|
-
clearIconName = 'close-circle',
|
|
145
|
-
}) => {
|
|
146
|
-
const tokens = useAppDesignTokens();
|
|
147
|
-
const { handleClear, handleSubmit, showClear } = useSearchBarLogic(
|
|
148
|
-
value, loading, onChangeText, onClear, onSubmit
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
return (
|
|
152
|
-
<View style={containerStyle}>
|
|
153
|
-
<SearchBarIcons
|
|
154
|
-
searchIconName={searchIconName}
|
|
155
|
-
clearIconName={clearIconName}
|
|
156
|
-
loading={loading}
|
|
157
|
-
showClear={showClear}
|
|
158
|
-
onClear={handleClear}
|
|
159
|
-
tokens={tokens}
|
|
160
|
-
/>
|
|
161
|
-
|
|
162
|
-
<SearchBarInput
|
|
163
|
-
value={value}
|
|
164
|
-
placeholder={placeholder}
|
|
165
|
-
autoFocus={autoFocus}
|
|
166
|
-
disabled={disabled}
|
|
167
|
-
style={style}
|
|
168
|
-
onChangeText={onChangeText}
|
|
169
|
-
onSubmitEditing={handleSubmit}
|
|
170
|
-
tokens={tokens}
|
|
171
|
-
/>
|
|
172
|
-
</View>
|
|
173
|
-
);
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
export const SearchBar = SearchBarComponent;
|
|
177
|
-
|
|
178
|
-
const getSearchBarStyles = () => StyleSheet.create({
|
|
179
|
-
container: {
|
|
180
|
-
flexDirection: 'row',
|
|
181
|
-
alignItems: 'center',
|
|
182
|
-
paddingHorizontal: 12,
|
|
183
|
-
paddingVertical: 8,
|
|
184
|
-
borderRadius: 12,
|
|
185
|
-
minHeight: 44,
|
|
186
|
-
},
|
|
187
|
-
searchIcon: {
|
|
188
|
-
marginRight: 12,
|
|
189
|
-
},
|
|
190
|
-
input: {
|
|
191
|
-
flex: 1,
|
|
192
|
-
paddingVertical: 0,
|
|
193
|
-
},
|
|
194
|
-
icon: {
|
|
195
|
-
marginLeft: 8,
|
|
196
|
-
},
|
|
197
|
-
});
|
|
198
|
-
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useAppDesignTokens Hook - Theme-Aware Design Tokens
|
|
3
|
-
*
|
|
4
|
-
* ✅ Automatically reads theme from global store
|
|
5
|
-
* ✅ No parameters needed - fully automatic!
|
|
6
|
-
* ✅ Returns tokens for current theme (light/dark)
|
|
7
|
-
* ✅ Single source of truth
|
|
8
|
-
*
|
|
9
|
-
* @example Usage (fully automatic theme-aware)
|
|
10
|
-
* ```typescript
|
|
11
|
-
* import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
|
|
12
|
-
*
|
|
13
|
-
* const MyComponent = () => {
|
|
14
|
-
* const tokens = useAppDesignTokens(); // Automatically uses current theme!
|
|
15
|
-
* return (
|
|
16
|
-
* <View style={{
|
|
17
|
-
* backgroundColor: tokens.colors.primary,
|
|
18
|
-
* padding: tokens.spacing.md
|
|
19
|
-
* }}>
|
|
20
|
-
* <Text style={tokens.typography.bodyLarge}>Hello!</Text>
|
|
21
|
-
* </View>
|
|
22
|
-
* );
|
|
23
|
-
* };
|
|
24
|
-
* ```
|
|
25
|
-
*
|
|
26
|
-
* How it works:
|
|
27
|
-
* - Reads themeMode from global store (useDesignSystemTheme)
|
|
28
|
-
* - App's theme store syncs to global store automatically
|
|
29
|
-
* - All components get correct tokens without prop drilling
|
|
30
|
-
* - Change theme once, everything updates!
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
import { useMemo } from 'react';
|
|
34
|
-
import { createDesignTokens, type DesignTokens } from '../core/TokenFactory';
|
|
35
|
-
import { useDesignSystemTheme } from '../infrastructure/globalThemeStore';
|
|
36
|
-
|
|
37
|
-
export const useAppDesignTokens = (): DesignTokens => {
|
|
38
|
-
const { themeMode, customColors } = useDesignSystemTheme();
|
|
39
|
-
|
|
40
|
-
return useMemo(() => {
|
|
41
|
-
const mode = themeMode === 'dark' ? 'dark' : 'light';
|
|
42
|
-
return createDesignTokens(mode, customColors);
|
|
43
|
-
}, [themeMode, customColors]);
|
|
44
|
-
};
|
|
45
|
-
|