@metacells/mcellui-mcp-server 0.1.0 → 0.1.2
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/dist/index.js +70 -7
- package/package.json +7 -5
- package/registry/registry.json +717 -0
- package/registry/ui/accordion.tsx +416 -0
- package/registry/ui/action-sheet.tsx +396 -0
- package/registry/ui/alert-dialog.tsx +355 -0
- package/registry/ui/avatar-stack.tsx +278 -0
- package/registry/ui/avatar.tsx +116 -0
- package/registry/ui/badge.tsx +125 -0
- package/registry/ui/button.tsx +240 -0
- package/registry/ui/card.tsx +675 -0
- package/registry/ui/carousel.tsx +431 -0
- package/registry/ui/checkbox.tsx +252 -0
- package/registry/ui/chip.tsx +271 -0
- package/registry/ui/column.tsx +133 -0
- package/registry/ui/datetime-picker.tsx +578 -0
- package/registry/ui/dialog.tsx +292 -0
- package/registry/ui/fab.tsx +225 -0
- package/registry/ui/form.tsx +323 -0
- package/registry/ui/horizontal-list.tsx +200 -0
- package/registry/ui/icon-button.tsx +244 -0
- package/registry/ui/image-gallery.tsx +455 -0
- package/registry/ui/image.tsx +283 -0
- package/registry/ui/input.tsx +242 -0
- package/registry/ui/label.tsx +99 -0
- package/registry/ui/list.tsx +519 -0
- package/registry/ui/progress.tsx +168 -0
- package/registry/ui/pull-to-refresh.tsx +231 -0
- package/registry/ui/radio-group.tsx +294 -0
- package/registry/ui/rating.tsx +311 -0
- package/registry/ui/row.tsx +136 -0
- package/registry/ui/screen.tsx +153 -0
- package/registry/ui/search-input.tsx +281 -0
- package/registry/ui/section-header.tsx +258 -0
- package/registry/ui/segmented-control.tsx +229 -0
- package/registry/ui/select.tsx +311 -0
- package/registry/ui/separator.tsx +74 -0
- package/registry/ui/sheet.tsx +362 -0
- package/registry/ui/skeleton.tsx +156 -0
- package/registry/ui/slider.tsx +307 -0
- package/registry/ui/spinner.tsx +100 -0
- package/registry/ui/stepper.tsx +314 -0
- package/registry/ui/stories.tsx +463 -0
- package/registry/ui/swipeable-row.tsx +362 -0
- package/registry/ui/switch.tsx +246 -0
- package/registry/ui/tabs.tsx +348 -0
- package/registry/ui/textarea.tsx +265 -0
- package/registry/ui/toast.tsx +316 -0
- package/registry/ui/tooltip.tsx +369 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchInput
|
|
3
|
+
*
|
|
4
|
+
* A search input with icon, clear button, and loading state.
|
|
5
|
+
* Commonly used in list headers and search screens.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // Basic usage
|
|
10
|
+
* <SearchInput
|
|
11
|
+
* value={query}
|
|
12
|
+
* onChangeText={setQuery}
|
|
13
|
+
* placeholder="Search..."
|
|
14
|
+
* />
|
|
15
|
+
*
|
|
16
|
+
* // With loading state
|
|
17
|
+
* <SearchInput
|
|
18
|
+
* value={query}
|
|
19
|
+
* onChangeText={setQuery}
|
|
20
|
+
* loading={isSearching}
|
|
21
|
+
* />
|
|
22
|
+
*
|
|
23
|
+
* // Uncontrolled
|
|
24
|
+
* <SearchInput
|
|
25
|
+
* defaultValue=""
|
|
26
|
+
* onChangeText={(text) => debouncedSearch(text)}
|
|
27
|
+
* />
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import React, { useState, useRef, useCallback } from 'react';
|
|
32
|
+
import {
|
|
33
|
+
View,
|
|
34
|
+
TextInput,
|
|
35
|
+
Pressable,
|
|
36
|
+
StyleSheet,
|
|
37
|
+
ViewStyle,
|
|
38
|
+
TextStyle,
|
|
39
|
+
ActivityIndicator,
|
|
40
|
+
TextInputProps,
|
|
41
|
+
} from 'react-native';
|
|
42
|
+
import Animated, {
|
|
43
|
+
useSharedValue,
|
|
44
|
+
useAnimatedStyle,
|
|
45
|
+
withSpring,
|
|
46
|
+
withTiming,
|
|
47
|
+
interpolate,
|
|
48
|
+
} from 'react-native-reanimated';
|
|
49
|
+
import Svg, { Path, Circle } from 'react-native-svg';
|
|
50
|
+
import { useTheme } from '@nativeui/core';
|
|
51
|
+
import { haptic } from '@nativeui/core';
|
|
52
|
+
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
// Types
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export interface SearchInputProps extends Omit<TextInputProps, 'style'> {
|
|
58
|
+
/** Controlled value */
|
|
59
|
+
value?: string;
|
|
60
|
+
/** Default value for uncontrolled mode */
|
|
61
|
+
defaultValue?: string;
|
|
62
|
+
/** Callback when text changes */
|
|
63
|
+
onChangeText?: (text: string) => void;
|
|
64
|
+
/** Placeholder text */
|
|
65
|
+
placeholder?: string;
|
|
66
|
+
/** Show loading spinner */
|
|
67
|
+
loading?: boolean;
|
|
68
|
+
/** Show clear button when there's text */
|
|
69
|
+
showClearButton?: boolean;
|
|
70
|
+
/** Called when clear button is pressed */
|
|
71
|
+
onClear?: () => void;
|
|
72
|
+
/** Called when search is submitted */
|
|
73
|
+
onSubmit?: (text: string) => void;
|
|
74
|
+
/** Container style */
|
|
75
|
+
style?: ViewStyle;
|
|
76
|
+
/** Input style */
|
|
77
|
+
inputStyle?: TextStyle;
|
|
78
|
+
/** Auto focus on mount */
|
|
79
|
+
autoFocus?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
// Icons
|
|
84
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function SearchIcon({ color, size = 20 }: { color: string; size?: number }) {
|
|
87
|
+
return (
|
|
88
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
89
|
+
<Circle cx="11" cy="11" r="8" stroke={color} strokeWidth="2" />
|
|
90
|
+
<Path
|
|
91
|
+
d="M21 21l-4.35-4.35"
|
|
92
|
+
stroke={color}
|
|
93
|
+
strokeWidth="2"
|
|
94
|
+
strokeLinecap="round"
|
|
95
|
+
/>
|
|
96
|
+
</Svg>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function ClearIcon({ color, size = 18 }: { color: string; size?: number }) {
|
|
101
|
+
return (
|
|
102
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
103
|
+
<Circle cx="12" cy="12" r="10" fill={color} fillOpacity={0.1} />
|
|
104
|
+
<Path
|
|
105
|
+
d="M15 9l-6 6M9 9l6 6"
|
|
106
|
+
stroke={color}
|
|
107
|
+
strokeWidth="2"
|
|
108
|
+
strokeLinecap="round"
|
|
109
|
+
/>
|
|
110
|
+
</Svg>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
115
|
+
// Component
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
export function SearchInput({
|
|
119
|
+
value: controlledValue,
|
|
120
|
+
defaultValue = '',
|
|
121
|
+
onChangeText,
|
|
122
|
+
placeholder = 'Search...',
|
|
123
|
+
loading = false,
|
|
124
|
+
showClearButton = true,
|
|
125
|
+
onClear,
|
|
126
|
+
onSubmit,
|
|
127
|
+
style,
|
|
128
|
+
inputStyle,
|
|
129
|
+
autoFocus = false,
|
|
130
|
+
...props
|
|
131
|
+
}: SearchInputProps) {
|
|
132
|
+
const { colors, spacing, radius, springs } = useTheme();
|
|
133
|
+
const inputRef = useRef<TextInput>(null);
|
|
134
|
+
|
|
135
|
+
// Controlled/uncontrolled state
|
|
136
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
137
|
+
const isControlled = controlledValue !== undefined;
|
|
138
|
+
const value = isControlled ? controlledValue : internalValue;
|
|
139
|
+
|
|
140
|
+
// Focus animation
|
|
141
|
+
const isFocused = useSharedValue(0);
|
|
142
|
+
|
|
143
|
+
const handleChangeText = useCallback(
|
|
144
|
+
(text: string) => {
|
|
145
|
+
if (!isControlled) {
|
|
146
|
+
setInternalValue(text);
|
|
147
|
+
}
|
|
148
|
+
onChangeText?.(text);
|
|
149
|
+
},
|
|
150
|
+
[isControlled, onChangeText]
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const handleClear = useCallback(() => {
|
|
154
|
+
haptic('light');
|
|
155
|
+
handleChangeText('');
|
|
156
|
+
onClear?.();
|
|
157
|
+
inputRef.current?.focus();
|
|
158
|
+
}, [handleChangeText, onClear]);
|
|
159
|
+
|
|
160
|
+
const handleSubmit = useCallback(() => {
|
|
161
|
+
haptic('light');
|
|
162
|
+
onSubmit?.(value);
|
|
163
|
+
}, [onSubmit, value]);
|
|
164
|
+
|
|
165
|
+
const handleFocus = useCallback(() => {
|
|
166
|
+
isFocused.value = withSpring(1, springs.snappy);
|
|
167
|
+
}, [springs.snappy]);
|
|
168
|
+
|
|
169
|
+
const handleBlur = useCallback(() => {
|
|
170
|
+
isFocused.value = withSpring(0, springs.snappy);
|
|
171
|
+
}, [springs.snappy]);
|
|
172
|
+
|
|
173
|
+
const containerAnimatedStyle = useAnimatedStyle(() => ({
|
|
174
|
+
borderColor: interpolate(
|
|
175
|
+
isFocused.value,
|
|
176
|
+
[0, 1],
|
|
177
|
+
[0, 1]
|
|
178
|
+
) === 1
|
|
179
|
+
? colors.primary
|
|
180
|
+
: colors.border,
|
|
181
|
+
transform: [
|
|
182
|
+
{
|
|
183
|
+
scale: interpolate(isFocused.value, [0, 1], [1, 1.01]),
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
const showClear = showClearButton && value.length > 0 && !loading;
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<Animated.View
|
|
192
|
+
style={[
|
|
193
|
+
styles.container,
|
|
194
|
+
{
|
|
195
|
+
backgroundColor: colors.backgroundMuted,
|
|
196
|
+
borderRadius: radius.lg,
|
|
197
|
+
borderWidth: 1,
|
|
198
|
+
borderColor: colors.border,
|
|
199
|
+
paddingHorizontal: spacing[3],
|
|
200
|
+
height: 44,
|
|
201
|
+
},
|
|
202
|
+
containerAnimatedStyle,
|
|
203
|
+
style,
|
|
204
|
+
]}
|
|
205
|
+
>
|
|
206
|
+
{/* Search Icon */}
|
|
207
|
+
<View style={[styles.iconContainer, { marginRight: spacing[2] }]}>
|
|
208
|
+
<SearchIcon color={colors.foregroundMuted} />
|
|
209
|
+
</View>
|
|
210
|
+
|
|
211
|
+
{/* Input */}
|
|
212
|
+
<TextInput
|
|
213
|
+
ref={inputRef}
|
|
214
|
+
style={[
|
|
215
|
+
styles.input,
|
|
216
|
+
{
|
|
217
|
+
color: colors.foreground,
|
|
218
|
+
fontSize: 16,
|
|
219
|
+
},
|
|
220
|
+
inputStyle,
|
|
221
|
+
]}
|
|
222
|
+
value={value}
|
|
223
|
+
onChangeText={handleChangeText}
|
|
224
|
+
placeholder={placeholder}
|
|
225
|
+
placeholderTextColor={colors.foregroundMuted}
|
|
226
|
+
onFocus={handleFocus}
|
|
227
|
+
onBlur={handleBlur}
|
|
228
|
+
onSubmitEditing={handleSubmit}
|
|
229
|
+
returnKeyType="search"
|
|
230
|
+
autoCapitalize="none"
|
|
231
|
+
autoCorrect={false}
|
|
232
|
+
autoFocus={autoFocus}
|
|
233
|
+
accessibilityRole="search"
|
|
234
|
+
accessibilityLabel={placeholder}
|
|
235
|
+
{...props}
|
|
236
|
+
/>
|
|
237
|
+
|
|
238
|
+
{/* Loading / Clear Button */}
|
|
239
|
+
<View style={[styles.rightContainer, { marginLeft: spacing[2] }]}>
|
|
240
|
+
{loading && (
|
|
241
|
+
<ActivityIndicator size="small" color={colors.foregroundMuted} />
|
|
242
|
+
)}
|
|
243
|
+
{showClear && (
|
|
244
|
+
<Pressable
|
|
245
|
+
onPress={handleClear}
|
|
246
|
+
hitSlop={8}
|
|
247
|
+
accessibilityRole="button"
|
|
248
|
+
accessibilityLabel="Clear search"
|
|
249
|
+
>
|
|
250
|
+
<ClearIcon color={colors.foregroundMuted} />
|
|
251
|
+
</Pressable>
|
|
252
|
+
)}
|
|
253
|
+
</View>
|
|
254
|
+
</Animated.View>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
259
|
+
// Styles
|
|
260
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
const styles = StyleSheet.create({
|
|
263
|
+
container: {
|
|
264
|
+
flexDirection: 'row',
|
|
265
|
+
alignItems: 'center',
|
|
266
|
+
},
|
|
267
|
+
iconContainer: {
|
|
268
|
+
flexShrink: 0,
|
|
269
|
+
},
|
|
270
|
+
input: {
|
|
271
|
+
flex: 1,
|
|
272
|
+
padding: 0,
|
|
273
|
+
margin: 0,
|
|
274
|
+
},
|
|
275
|
+
rightContainer: {
|
|
276
|
+
flexShrink: 0,
|
|
277
|
+
width: 24,
|
|
278
|
+
alignItems: 'center',
|
|
279
|
+
justifyContent: 'center',
|
|
280
|
+
},
|
|
281
|
+
});
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SectionHeader
|
|
3
|
+
*
|
|
4
|
+
* A section header with title and optional "See All" action.
|
|
5
|
+
* Commonly used above horizontal lists and content sections.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // Basic usage
|
|
10
|
+
* <SectionHeader title="Popular Items" />
|
|
11
|
+
*
|
|
12
|
+
* // With "See All" action
|
|
13
|
+
* <SectionHeader
|
|
14
|
+
* title="Recent Orders"
|
|
15
|
+
* actionText="View All"
|
|
16
|
+
* onAction={() => navigation.navigate('Orders')}
|
|
17
|
+
* />
|
|
18
|
+
*
|
|
19
|
+
* // With subtitle
|
|
20
|
+
* <SectionHeader
|
|
21
|
+
* title="Recommendations"
|
|
22
|
+
* subtitle="Based on your preferences"
|
|
23
|
+
* onAction={handleSeeAll}
|
|
24
|
+
* />
|
|
25
|
+
*
|
|
26
|
+
* // Custom styling
|
|
27
|
+
* <SectionHeader
|
|
28
|
+
* title="Featured"
|
|
29
|
+
* size="lg"
|
|
30
|
+
* style={{ paddingHorizontal: 24 }}
|
|
31
|
+
* />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import React, { useCallback } from 'react';
|
|
36
|
+
import {
|
|
37
|
+
View,
|
|
38
|
+
Text,
|
|
39
|
+
Pressable,
|
|
40
|
+
StyleSheet,
|
|
41
|
+
ViewStyle,
|
|
42
|
+
TextStyle,
|
|
43
|
+
} from 'react-native';
|
|
44
|
+
import Animated, {
|
|
45
|
+
useSharedValue,
|
|
46
|
+
useAnimatedStyle,
|
|
47
|
+
withSpring,
|
|
48
|
+
} from 'react-native-reanimated';
|
|
49
|
+
import Svg, { Path } from 'react-native-svg';
|
|
50
|
+
import { useTheme } from '@nativeui/core';
|
|
51
|
+
import { haptic } from '@nativeui/core';
|
|
52
|
+
|
|
53
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
54
|
+
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
// Types
|
|
57
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export type SectionHeaderSize = 'sm' | 'md' | 'lg';
|
|
60
|
+
|
|
61
|
+
export interface SectionHeaderProps {
|
|
62
|
+
/** Section title */
|
|
63
|
+
title: string;
|
|
64
|
+
/** Optional subtitle below title */
|
|
65
|
+
subtitle?: string;
|
|
66
|
+
/** Text for the action button */
|
|
67
|
+
actionText?: string;
|
|
68
|
+
/** Show arrow icon next to action text */
|
|
69
|
+
showActionArrow?: boolean;
|
|
70
|
+
/** Action button press handler */
|
|
71
|
+
onAction?: () => void;
|
|
72
|
+
/** Size variant */
|
|
73
|
+
size?: SectionHeaderSize;
|
|
74
|
+
/** Container style */
|
|
75
|
+
style?: ViewStyle;
|
|
76
|
+
/** Title text style */
|
|
77
|
+
titleStyle?: TextStyle;
|
|
78
|
+
/** Subtitle text style */
|
|
79
|
+
subtitleStyle?: TextStyle;
|
|
80
|
+
/** Action text style */
|
|
81
|
+
actionStyle?: TextStyle;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
85
|
+
// Size Configuration
|
|
86
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
const sizeConfig = {
|
|
89
|
+
sm: { titleSize: 'sm' as const, subtitleSize: 'xs' as const, actionSize: 'xs' as const },
|
|
90
|
+
md: { titleSize: 'lg' as const, subtitleSize: 'sm' as const, actionSize: 'sm' as const },
|
|
91
|
+
lg: { titleSize: 'xl' as const, subtitleSize: 'base' as const, actionSize: 'base' as const },
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
95
|
+
// Icons
|
|
96
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
function ChevronRightIcon({ color, size = 16 }: { color: string; size?: number }) {
|
|
99
|
+
return (
|
|
100
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
101
|
+
<Path
|
|
102
|
+
d="M9 18l6-6-6-6"
|
|
103
|
+
stroke={color}
|
|
104
|
+
strokeWidth="2"
|
|
105
|
+
strokeLinecap="round"
|
|
106
|
+
strokeLinejoin="round"
|
|
107
|
+
/>
|
|
108
|
+
</Svg>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
113
|
+
// Component
|
|
114
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
export function SectionHeader({
|
|
117
|
+
title,
|
|
118
|
+
subtitle,
|
|
119
|
+
actionText = 'See All',
|
|
120
|
+
showActionArrow = true,
|
|
121
|
+
onAction,
|
|
122
|
+
size = 'md',
|
|
123
|
+
style,
|
|
124
|
+
titleStyle,
|
|
125
|
+
subtitleStyle,
|
|
126
|
+
actionStyle,
|
|
127
|
+
}: SectionHeaderProps) {
|
|
128
|
+
const { colors, spacing, fontSize, fontWeight, springs } = useTheme();
|
|
129
|
+
const config = sizeConfig[size];
|
|
130
|
+
const actionScale = useSharedValue(1);
|
|
131
|
+
|
|
132
|
+
const handleActionPressIn = useCallback(() => {
|
|
133
|
+
actionScale.value = withSpring(0.95, springs.snappy);
|
|
134
|
+
}, [springs.snappy]);
|
|
135
|
+
|
|
136
|
+
const handleActionPressOut = useCallback(() => {
|
|
137
|
+
actionScale.value = withSpring(1, springs.snappy);
|
|
138
|
+
}, [springs.snappy]);
|
|
139
|
+
|
|
140
|
+
const handleAction = useCallback(() => {
|
|
141
|
+
haptic('light');
|
|
142
|
+
onAction?.();
|
|
143
|
+
}, [onAction]);
|
|
144
|
+
|
|
145
|
+
const actionAnimatedStyle = useAnimatedStyle(() => ({
|
|
146
|
+
transform: [{ scale: actionScale.value }],
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<View
|
|
151
|
+
style={[
|
|
152
|
+
styles.container,
|
|
153
|
+
{
|
|
154
|
+
paddingHorizontal: spacing[4],
|
|
155
|
+
paddingVertical: spacing[2],
|
|
156
|
+
},
|
|
157
|
+
style,
|
|
158
|
+
]}
|
|
159
|
+
>
|
|
160
|
+
{/* Title & Subtitle */}
|
|
161
|
+
<View style={styles.titleContainer}>
|
|
162
|
+
<Text
|
|
163
|
+
style={[
|
|
164
|
+
styles.title,
|
|
165
|
+
{
|
|
166
|
+
color: colors.foreground,
|
|
167
|
+
fontSize: fontSize[config.titleSize],
|
|
168
|
+
fontWeight: fontWeight.semibold,
|
|
169
|
+
},
|
|
170
|
+
titleStyle,
|
|
171
|
+
]}
|
|
172
|
+
numberOfLines={1}
|
|
173
|
+
>
|
|
174
|
+
{title}
|
|
175
|
+
</Text>
|
|
176
|
+
{subtitle && (
|
|
177
|
+
<Text
|
|
178
|
+
style={[
|
|
179
|
+
styles.subtitle,
|
|
180
|
+
{
|
|
181
|
+
color: colors.foregroundMuted,
|
|
182
|
+
fontSize: fontSize[config.subtitleSize],
|
|
183
|
+
marginTop: spacing[0.5],
|
|
184
|
+
},
|
|
185
|
+
subtitleStyle,
|
|
186
|
+
]}
|
|
187
|
+
numberOfLines={1}
|
|
188
|
+
>
|
|
189
|
+
{subtitle}
|
|
190
|
+
</Text>
|
|
191
|
+
)}
|
|
192
|
+
</View>
|
|
193
|
+
|
|
194
|
+
{/* Action Button */}
|
|
195
|
+
{onAction && (
|
|
196
|
+
<AnimatedPressable
|
|
197
|
+
style={[styles.actionButton, actionAnimatedStyle]}
|
|
198
|
+
onPressIn={handleActionPressIn}
|
|
199
|
+
onPressOut={handleActionPressOut}
|
|
200
|
+
onPress={handleAction}
|
|
201
|
+
hitSlop={8}
|
|
202
|
+
accessibilityRole="button"
|
|
203
|
+
accessibilityLabel={actionText}
|
|
204
|
+
>
|
|
205
|
+
<Text
|
|
206
|
+
style={[
|
|
207
|
+
styles.actionText,
|
|
208
|
+
{
|
|
209
|
+
color: colors.primary,
|
|
210
|
+
fontSize: fontSize[config.actionSize],
|
|
211
|
+
fontWeight: fontWeight.medium,
|
|
212
|
+
},
|
|
213
|
+
actionStyle,
|
|
214
|
+
]}
|
|
215
|
+
>
|
|
216
|
+
{actionText}
|
|
217
|
+
</Text>
|
|
218
|
+
{showActionArrow && (
|
|
219
|
+
<ChevronRightIcon
|
|
220
|
+
color={colors.primary}
|
|
221
|
+
size={fontSize[config.actionSize]}
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
224
|
+
</AnimatedPressable>
|
|
225
|
+
)}
|
|
226
|
+
</View>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
231
|
+
// Styles
|
|
232
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
const styles = StyleSheet.create({
|
|
235
|
+
container: {
|
|
236
|
+
flexDirection: 'row',
|
|
237
|
+
alignItems: 'center',
|
|
238
|
+
justifyContent: 'space-between',
|
|
239
|
+
},
|
|
240
|
+
titleContainer: {
|
|
241
|
+
flex: 1,
|
|
242
|
+
marginRight: 16,
|
|
243
|
+
},
|
|
244
|
+
title: {
|
|
245
|
+
// Dynamic styles applied inline
|
|
246
|
+
},
|
|
247
|
+
subtitle: {
|
|
248
|
+
// Dynamic styles applied inline
|
|
249
|
+
},
|
|
250
|
+
actionButton: {
|
|
251
|
+
flexDirection: 'row',
|
|
252
|
+
alignItems: 'center',
|
|
253
|
+
gap: 2,
|
|
254
|
+
},
|
|
255
|
+
actionText: {
|
|
256
|
+
// Dynamic styles applied inline
|
|
257
|
+
},
|
|
258
|
+
});
|