@metacells/mcellui-mcp-server 0.1.1 → 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 +8 -2
- package/package.json +5 -3
- 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,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabs
|
|
3
|
+
*
|
|
4
|
+
* A tabbed navigation component with animated indicator.
|
|
5
|
+
* Supports both controlled and uncontrolled modes.
|
|
6
|
+
* Two visual variants: "pill" (segmented control) and "underline" (bottom border).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* // Pill variant (default) - segmented control style
|
|
11
|
+
* <Tabs defaultValue="tab1">
|
|
12
|
+
* <TabsList>
|
|
13
|
+
* <TabsTrigger value="tab1">Account</TabsTrigger>
|
|
14
|
+
* <TabsTrigger value="tab2">Settings</TabsTrigger>
|
|
15
|
+
* </TabsList>
|
|
16
|
+
* <TabsContent value="tab1">
|
|
17
|
+
* <Text>Account content</Text>
|
|
18
|
+
* </TabsContent>
|
|
19
|
+
* <TabsContent value="tab2">
|
|
20
|
+
* <Text>Settings content</Text>
|
|
21
|
+
* </TabsContent>
|
|
22
|
+
* </Tabs>
|
|
23
|
+
*
|
|
24
|
+
* // Underline variant - bottom border style (great for profiles)
|
|
25
|
+
* <Tabs defaultValue="posts">
|
|
26
|
+
* <TabsList variant="underline">
|
|
27
|
+
* <TabsTrigger value="posts">Posts</TabsTrigger>
|
|
28
|
+
* <TabsTrigger value="media">Media</TabsTrigger>
|
|
29
|
+
* <TabsTrigger value="about">About</TabsTrigger>
|
|
30
|
+
* </TabsList>
|
|
31
|
+
* <TabsContent value="posts">...</TabsContent>
|
|
32
|
+
* </Tabs>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import React, {
|
|
37
|
+
createContext,
|
|
38
|
+
useContext,
|
|
39
|
+
useState,
|
|
40
|
+
useCallback,
|
|
41
|
+
useRef,
|
|
42
|
+
useEffect,
|
|
43
|
+
} from 'react';
|
|
44
|
+
import {
|
|
45
|
+
View,
|
|
46
|
+
Text,
|
|
47
|
+
Pressable,
|
|
48
|
+
StyleSheet,
|
|
49
|
+
ViewStyle,
|
|
50
|
+
TextStyle,
|
|
51
|
+
LayoutChangeEvent,
|
|
52
|
+
LayoutRectangle,
|
|
53
|
+
} from 'react-native';
|
|
54
|
+
import Animated, {
|
|
55
|
+
useSharedValue,
|
|
56
|
+
useAnimatedStyle,
|
|
57
|
+
withSpring,
|
|
58
|
+
} from 'react-native-reanimated';
|
|
59
|
+
import { useTheme } from '@nativeui/core';
|
|
60
|
+
import { haptic } from '@nativeui/core';
|
|
61
|
+
|
|
62
|
+
// Context
|
|
63
|
+
interface TabsContextValue {
|
|
64
|
+
value: string;
|
|
65
|
+
onValueChange: (value: string) => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const TabsContext = createContext<TabsContextValue | null>(null);
|
|
69
|
+
|
|
70
|
+
function useTabsContext() {
|
|
71
|
+
const context = useContext(TabsContext);
|
|
72
|
+
if (!context) {
|
|
73
|
+
throw new Error('Tabs components must be used within a Tabs provider');
|
|
74
|
+
}
|
|
75
|
+
return context;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Tabs Root
|
|
79
|
+
export interface TabsProps {
|
|
80
|
+
/** Controlled value */
|
|
81
|
+
value?: string;
|
|
82
|
+
/** Default value for uncontrolled mode */
|
|
83
|
+
defaultValue?: string;
|
|
84
|
+
/** Callback when value changes */
|
|
85
|
+
onValueChange?: (value: string) => void;
|
|
86
|
+
/** Children */
|
|
87
|
+
children: React.ReactNode;
|
|
88
|
+
/** Container style */
|
|
89
|
+
style?: ViewStyle;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function Tabs({
|
|
93
|
+
value: controlledValue,
|
|
94
|
+
defaultValue = '',
|
|
95
|
+
onValueChange,
|
|
96
|
+
children,
|
|
97
|
+
style,
|
|
98
|
+
}: TabsProps) {
|
|
99
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
100
|
+
const isControlled = controlledValue !== undefined;
|
|
101
|
+
const value = isControlled ? controlledValue : internalValue;
|
|
102
|
+
|
|
103
|
+
const handleValueChange = useCallback(
|
|
104
|
+
(newValue: string) => {
|
|
105
|
+
if (!isControlled) {
|
|
106
|
+
setInternalValue(newValue);
|
|
107
|
+
}
|
|
108
|
+
onValueChange?.(newValue);
|
|
109
|
+
},
|
|
110
|
+
[isControlled, onValueChange]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<TabsContext.Provider value={{ value, onValueChange: handleValueChange }}>
|
|
115
|
+
<View style={[styles.container, style]}>{children}</View>
|
|
116
|
+
</TabsContext.Provider>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// TabsList Context for indicator
|
|
121
|
+
export type TabsVariant = 'pill' | 'underline';
|
|
122
|
+
|
|
123
|
+
interface TabsListContextValue {
|
|
124
|
+
registerTab: (value: string, layout: LayoutRectangle) => void;
|
|
125
|
+
tabLayouts: Map<string, LayoutRectangle>;
|
|
126
|
+
variant: TabsVariant;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const TabsListContext = createContext<TabsListContextValue | null>(null);
|
|
130
|
+
|
|
131
|
+
// TabsList
|
|
132
|
+
export interface TabsListProps {
|
|
133
|
+
children: React.ReactNode;
|
|
134
|
+
/** Visual variant: "pill" (segmented control) or "underline" (bottom border) */
|
|
135
|
+
variant?: TabsVariant;
|
|
136
|
+
style?: ViewStyle;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function TabsList({ children, variant = 'pill', style }: TabsListProps) {
|
|
140
|
+
const { colors, radius } = useTheme();
|
|
141
|
+
const { value } = useTabsContext();
|
|
142
|
+
const tabLayouts = useRef(new Map<string, LayoutRectangle>()).current;
|
|
143
|
+
|
|
144
|
+
const indicatorX = useSharedValue(0);
|
|
145
|
+
const indicatorWidth = useSharedValue(0);
|
|
146
|
+
|
|
147
|
+
const registerTab = useCallback(
|
|
148
|
+
(tabValue: string, layout: LayoutRectangle) => {
|
|
149
|
+
tabLayouts.set(tabValue, layout);
|
|
150
|
+
|
|
151
|
+
// Update indicator if this is the active tab
|
|
152
|
+
if (tabValue === value) {
|
|
153
|
+
indicatorX.value = withSpring(layout.x, { damping: 20, stiffness: 200 });
|
|
154
|
+
indicatorWidth.value = withSpring(layout.width, {
|
|
155
|
+
damping: 20,
|
|
156
|
+
stiffness: 200,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
[value, tabLayouts]
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Update indicator when value changes
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const layout = tabLayouts.get(value);
|
|
166
|
+
if (layout) {
|
|
167
|
+
indicatorX.value = withSpring(layout.x, { damping: 20, stiffness: 200 });
|
|
168
|
+
indicatorWidth.value = withSpring(layout.width, {
|
|
169
|
+
damping: 20,
|
|
170
|
+
stiffness: 200,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}, [value, tabLayouts]);
|
|
174
|
+
|
|
175
|
+
const indicatorStyle = useAnimatedStyle(() => ({
|
|
176
|
+
transform: [{ translateX: indicatorX.value }],
|
|
177
|
+
width: indicatorWidth.value,
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
const isPill = variant === 'pill';
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<TabsListContext.Provider value={{ registerTab, tabLayouts, variant }}>
|
|
184
|
+
<View
|
|
185
|
+
style={[
|
|
186
|
+
styles.list,
|
|
187
|
+
isPill
|
|
188
|
+
? {
|
|
189
|
+
backgroundColor: colors.backgroundMuted,
|
|
190
|
+
borderRadius: radius.lg,
|
|
191
|
+
padding: 4,
|
|
192
|
+
}
|
|
193
|
+
: {
|
|
194
|
+
backgroundColor: 'transparent',
|
|
195
|
+
borderBottomWidth: 1,
|
|
196
|
+
borderBottomColor: colors.border,
|
|
197
|
+
},
|
|
198
|
+
style,
|
|
199
|
+
]}
|
|
200
|
+
>
|
|
201
|
+
{children}
|
|
202
|
+
<Animated.View
|
|
203
|
+
style={[
|
|
204
|
+
isPill ? styles.indicatorPill : styles.indicatorUnderline,
|
|
205
|
+
isPill
|
|
206
|
+
? {
|
|
207
|
+
backgroundColor: colors.background,
|
|
208
|
+
borderRadius: radius.md,
|
|
209
|
+
}
|
|
210
|
+
: {
|
|
211
|
+
backgroundColor: colors.primary,
|
|
212
|
+
},
|
|
213
|
+
indicatorStyle,
|
|
214
|
+
]}
|
|
215
|
+
/>
|
|
216
|
+
</View>
|
|
217
|
+
</TabsListContext.Provider>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// TabsTrigger
|
|
222
|
+
export interface TabsTriggerProps {
|
|
223
|
+
value: string;
|
|
224
|
+
children: React.ReactNode;
|
|
225
|
+
disabled?: boolean;
|
|
226
|
+
style?: ViewStyle;
|
|
227
|
+
textStyle?: TextStyle;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function TabsTrigger({
|
|
231
|
+
value: tabValue,
|
|
232
|
+
children,
|
|
233
|
+
disabled = false,
|
|
234
|
+
style,
|
|
235
|
+
textStyle,
|
|
236
|
+
}: TabsTriggerProps) {
|
|
237
|
+
const { colors, spacing } = useTheme();
|
|
238
|
+
const { value, onValueChange } = useTabsContext();
|
|
239
|
+
const listContext = useContext(TabsListContext);
|
|
240
|
+
const isActive = value === tabValue;
|
|
241
|
+
const variant = listContext?.variant ?? 'pill';
|
|
242
|
+
const isUnderline = variant === 'underline';
|
|
243
|
+
|
|
244
|
+
const handleLayout = (event: LayoutChangeEvent) => {
|
|
245
|
+
listContext?.registerTab(tabValue, event.nativeEvent.layout);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const handlePress = () => {
|
|
249
|
+
if (disabled) return;
|
|
250
|
+
haptic('selection');
|
|
251
|
+
onValueChange(tabValue);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<Pressable
|
|
256
|
+
style={[
|
|
257
|
+
styles.trigger,
|
|
258
|
+
{
|
|
259
|
+
paddingVertical: isUnderline ? spacing[3] : spacing[2],
|
|
260
|
+
paddingHorizontal: isUnderline ? spacing[4] : spacing[3],
|
|
261
|
+
opacity: disabled ? 0.5 : 1,
|
|
262
|
+
},
|
|
263
|
+
style,
|
|
264
|
+
]}
|
|
265
|
+
onLayout={handleLayout}
|
|
266
|
+
onPress={handlePress}
|
|
267
|
+
disabled={disabled}
|
|
268
|
+
accessibilityRole="tab"
|
|
269
|
+
accessibilityState={{ selected: isActive, disabled }}
|
|
270
|
+
>
|
|
271
|
+
<Text
|
|
272
|
+
style={[
|
|
273
|
+
styles.triggerText,
|
|
274
|
+
{
|
|
275
|
+
color: isActive
|
|
276
|
+
? isUnderline
|
|
277
|
+
? colors.primary
|
|
278
|
+
: colors.foreground
|
|
279
|
+
: colors.foregroundMuted,
|
|
280
|
+
fontWeight: isActive ? '600' : '500',
|
|
281
|
+
},
|
|
282
|
+
textStyle,
|
|
283
|
+
]}
|
|
284
|
+
>
|
|
285
|
+
{children}
|
|
286
|
+
</Text>
|
|
287
|
+
</Pressable>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// TabsContent
|
|
292
|
+
export interface TabsContentProps {
|
|
293
|
+
value: string;
|
|
294
|
+
children: React.ReactNode;
|
|
295
|
+
style?: ViewStyle;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function TabsContent({ value: tabValue, children, style }: TabsContentProps) {
|
|
299
|
+
const { value } = useTabsContext();
|
|
300
|
+
const { spacing } = useTheme();
|
|
301
|
+
|
|
302
|
+
if (value !== tabValue) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<View style={[styles.content, { marginTop: spacing[4] }, style]}>
|
|
308
|
+
{children}
|
|
309
|
+
</View>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const styles = StyleSheet.create({
|
|
314
|
+
container: {
|
|
315
|
+
width: '100%',
|
|
316
|
+
},
|
|
317
|
+
list: {
|
|
318
|
+
flexDirection: 'row',
|
|
319
|
+
position: 'relative',
|
|
320
|
+
},
|
|
321
|
+
indicatorPill: {
|
|
322
|
+
position: 'absolute',
|
|
323
|
+
top: 4,
|
|
324
|
+
bottom: 4,
|
|
325
|
+
left: 0,
|
|
326
|
+
shadowColor: '#000',
|
|
327
|
+
shadowOffset: { width: 0, height: 1 },
|
|
328
|
+
shadowOpacity: 0.05,
|
|
329
|
+
shadowRadius: 2,
|
|
330
|
+
elevation: 1,
|
|
331
|
+
},
|
|
332
|
+
indicatorUnderline: {
|
|
333
|
+
position: 'absolute',
|
|
334
|
+
bottom: 0,
|
|
335
|
+
left: 0,
|
|
336
|
+
height: 2,
|
|
337
|
+
},
|
|
338
|
+
trigger: {
|
|
339
|
+
flex: 1,
|
|
340
|
+
alignItems: 'center',
|
|
341
|
+
justifyContent: 'center',
|
|
342
|
+
zIndex: 1,
|
|
343
|
+
},
|
|
344
|
+
triggerText: {
|
|
345
|
+
fontSize: 14,
|
|
346
|
+
},
|
|
347
|
+
content: {},
|
|
348
|
+
});
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Textarea
|
|
3
|
+
*
|
|
4
|
+
* A multiline text input component with auto-grow support.
|
|
5
|
+
* Extends Input with multiline-specific features.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <Textarea label="Bio" placeholder="Tell us about yourself" />
|
|
10
|
+
* <Textarea label="Description" rows={5} maxLength={500} showCount />
|
|
11
|
+
* <Textarea autoGrow minRows={2} maxRows={6} />
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React, { forwardRef, useCallback, useState } from 'react';
|
|
16
|
+
import {
|
|
17
|
+
View,
|
|
18
|
+
Text,
|
|
19
|
+
TextInput,
|
|
20
|
+
StyleSheet,
|
|
21
|
+
ViewStyle,
|
|
22
|
+
TextStyle,
|
|
23
|
+
TextInputProps,
|
|
24
|
+
NativeSyntheticEvent,
|
|
25
|
+
TextInputContentSizeChangeEventData,
|
|
26
|
+
} from 'react-native';
|
|
27
|
+
import Animated, {
|
|
28
|
+
useSharedValue,
|
|
29
|
+
useAnimatedStyle,
|
|
30
|
+
withTiming,
|
|
31
|
+
interpolateColor,
|
|
32
|
+
Easing,
|
|
33
|
+
} from 'react-native-reanimated';
|
|
34
|
+
import { useTheme } from '@nativeui/core';
|
|
35
|
+
import { haptic } from '@nativeui/core';
|
|
36
|
+
|
|
37
|
+
const TIMING_CONFIG = { duration: 200, easing: Easing.out(Easing.quad) };
|
|
38
|
+
|
|
39
|
+
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
|
|
40
|
+
|
|
41
|
+
export interface TextareaProps extends Omit<TextInputProps, 'style' | 'multiline'> {
|
|
42
|
+
/** Label text above input */
|
|
43
|
+
label?: string;
|
|
44
|
+
/** Error message below input */
|
|
45
|
+
error?: string;
|
|
46
|
+
/** Helper text below input (hidden when error is present) */
|
|
47
|
+
helperText?: string;
|
|
48
|
+
/** Number of visible rows (default: 4) */
|
|
49
|
+
rows?: number;
|
|
50
|
+
/** Auto-grow based on content */
|
|
51
|
+
autoGrow?: boolean;
|
|
52
|
+
/** Minimum rows when autoGrow is true */
|
|
53
|
+
minRows?: number;
|
|
54
|
+
/** Maximum rows when autoGrow is true */
|
|
55
|
+
maxRows?: number;
|
|
56
|
+
/** Show character count */
|
|
57
|
+
showCount?: boolean;
|
|
58
|
+
/** Container style */
|
|
59
|
+
containerStyle?: ViewStyle;
|
|
60
|
+
/** Input field style */
|
|
61
|
+
style?: TextStyle;
|
|
62
|
+
/** Label style */
|
|
63
|
+
labelStyle?: TextStyle;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const LINE_HEIGHT = 20;
|
|
67
|
+
const PADDING_VERTICAL = 12;
|
|
68
|
+
|
|
69
|
+
export const Textarea = forwardRef<TextInput, TextareaProps>(
|
|
70
|
+
(
|
|
71
|
+
{
|
|
72
|
+
label,
|
|
73
|
+
error,
|
|
74
|
+
helperText,
|
|
75
|
+
rows = 4,
|
|
76
|
+
autoGrow = false,
|
|
77
|
+
minRows = 2,
|
|
78
|
+
maxRows = 8,
|
|
79
|
+
showCount = false,
|
|
80
|
+
maxLength,
|
|
81
|
+
containerStyle,
|
|
82
|
+
style,
|
|
83
|
+
labelStyle,
|
|
84
|
+
editable = true,
|
|
85
|
+
onFocus,
|
|
86
|
+
onBlur,
|
|
87
|
+
onChangeText,
|
|
88
|
+
value,
|
|
89
|
+
...props
|
|
90
|
+
},
|
|
91
|
+
ref
|
|
92
|
+
) => {
|
|
93
|
+
const { colors, radius, spacing } = useTheme();
|
|
94
|
+
const focusProgress = useSharedValue(0);
|
|
95
|
+
const [textLength, setTextLength] = useState(value?.length ?? 0);
|
|
96
|
+
const [height, setHeight] = useState(rows * LINE_HEIGHT + PADDING_VERTICAL * 2);
|
|
97
|
+
|
|
98
|
+
const hasError = !!error;
|
|
99
|
+
const isDisabled = editable === false;
|
|
100
|
+
|
|
101
|
+
const handleFocus = useCallback(
|
|
102
|
+
(e: any) => {
|
|
103
|
+
focusProgress.value = withTiming(1, TIMING_CONFIG);
|
|
104
|
+
haptic('selection');
|
|
105
|
+
onFocus?.(e);
|
|
106
|
+
},
|
|
107
|
+
[onFocus]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const handleBlur = useCallback(
|
|
111
|
+
(e: any) => {
|
|
112
|
+
focusProgress.value = withTiming(0, TIMING_CONFIG);
|
|
113
|
+
onBlur?.(e);
|
|
114
|
+
},
|
|
115
|
+
[onBlur]
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const handleChangeText = useCallback(
|
|
119
|
+
(text: string) => {
|
|
120
|
+
setTextLength(text.length);
|
|
121
|
+
onChangeText?.(text);
|
|
122
|
+
},
|
|
123
|
+
[onChangeText]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const handleContentSizeChange = useCallback(
|
|
127
|
+
(e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
|
|
128
|
+
if (!autoGrow) return;
|
|
129
|
+
|
|
130
|
+
const contentHeight = e.nativeEvent.contentSize.height;
|
|
131
|
+
const minHeight = minRows * LINE_HEIGHT + PADDING_VERTICAL * 2;
|
|
132
|
+
const maxHeight = maxRows * LINE_HEIGHT + PADDING_VERTICAL * 2;
|
|
133
|
+
|
|
134
|
+
const newHeight = Math.min(Math.max(contentHeight, minHeight), maxHeight);
|
|
135
|
+
setHeight(newHeight);
|
|
136
|
+
},
|
|
137
|
+
[autoGrow, minRows, maxRows]
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const animatedBorderStyle = useAnimatedStyle(() => {
|
|
141
|
+
if (hasError) {
|
|
142
|
+
return {
|
|
143
|
+
borderColor: colors.destructive,
|
|
144
|
+
borderWidth: 2,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
borderColor: interpolateColor(
|
|
150
|
+
focusProgress.value,
|
|
151
|
+
[0, 1],
|
|
152
|
+
[colors.border, colors.primary]
|
|
153
|
+
),
|
|
154
|
+
borderWidth: focusProgress.value > 0.5 ? 2 : 1,
|
|
155
|
+
};
|
|
156
|
+
}, [hasError, colors]);
|
|
157
|
+
|
|
158
|
+
const baseHeight = autoGrow ? height : rows * LINE_HEIGHT + PADDING_VERTICAL * 2;
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<View style={[styles.container, containerStyle]}>
|
|
162
|
+
{label && (
|
|
163
|
+
<Text
|
|
164
|
+
style={[
|
|
165
|
+
styles.label,
|
|
166
|
+
{
|
|
167
|
+
fontSize: 14,
|
|
168
|
+
marginBottom: spacing[1.5],
|
|
169
|
+
color: hasError ? colors.destructive : colors.foreground,
|
|
170
|
+
},
|
|
171
|
+
labelStyle,
|
|
172
|
+
]}
|
|
173
|
+
>
|
|
174
|
+
{label}
|
|
175
|
+
</Text>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
<AnimatedTextInput
|
|
179
|
+
ref={ref}
|
|
180
|
+
style={[
|
|
181
|
+
styles.input,
|
|
182
|
+
{
|
|
183
|
+
height: baseHeight,
|
|
184
|
+
paddingHorizontal: 12,
|
|
185
|
+
paddingVertical: PADDING_VERTICAL,
|
|
186
|
+
borderRadius: radius.md,
|
|
187
|
+
fontSize: 14,
|
|
188
|
+
lineHeight: LINE_HEIGHT,
|
|
189
|
+
backgroundColor: isDisabled ? colors.backgroundMuted : colors.background,
|
|
190
|
+
color: isDisabled ? colors.foregroundMuted : colors.foreground,
|
|
191
|
+
textAlignVertical: 'top',
|
|
192
|
+
},
|
|
193
|
+
animatedBorderStyle,
|
|
194
|
+
style,
|
|
195
|
+
]}
|
|
196
|
+
multiline
|
|
197
|
+
placeholderTextColor={colors.foregroundMuted}
|
|
198
|
+
editable={editable}
|
|
199
|
+
onFocus={handleFocus}
|
|
200
|
+
onBlur={handleBlur}
|
|
201
|
+
onChangeText={handleChangeText}
|
|
202
|
+
onContentSizeChange={handleContentSizeChange}
|
|
203
|
+
maxLength={maxLength}
|
|
204
|
+
value={value}
|
|
205
|
+
accessibilityLabel={label}
|
|
206
|
+
accessibilityState={{ disabled: isDisabled }}
|
|
207
|
+
{...props}
|
|
208
|
+
/>
|
|
209
|
+
|
|
210
|
+
<View style={styles.footer}>
|
|
211
|
+
{(error || helperText) && (
|
|
212
|
+
<Text
|
|
213
|
+
style={[
|
|
214
|
+
styles.helperText,
|
|
215
|
+
{
|
|
216
|
+
fontSize: 12,
|
|
217
|
+
marginTop: spacing[1],
|
|
218
|
+
color: hasError ? colors.destructive : colors.foregroundMuted,
|
|
219
|
+
},
|
|
220
|
+
]}
|
|
221
|
+
>
|
|
222
|
+
{error || helperText}
|
|
223
|
+
</Text>
|
|
224
|
+
)}
|
|
225
|
+
{showCount && maxLength && (
|
|
226
|
+
<Text
|
|
227
|
+
style={[
|
|
228
|
+
styles.count,
|
|
229
|
+
{
|
|
230
|
+
fontSize: 12,
|
|
231
|
+
marginTop: spacing[1],
|
|
232
|
+
color: textLength >= maxLength ? colors.destructive : colors.foregroundMuted,
|
|
233
|
+
},
|
|
234
|
+
]}
|
|
235
|
+
>
|
|
236
|
+
{textLength}/{maxLength}
|
|
237
|
+
</Text>
|
|
238
|
+
)}
|
|
239
|
+
</View>
|
|
240
|
+
</View>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
Textarea.displayName = 'Textarea';
|
|
246
|
+
|
|
247
|
+
const styles = StyleSheet.create({
|
|
248
|
+
container: {
|
|
249
|
+
width: '100%',
|
|
250
|
+
},
|
|
251
|
+
label: {
|
|
252
|
+
fontWeight: '500',
|
|
253
|
+
},
|
|
254
|
+
input: {},
|
|
255
|
+
footer: {
|
|
256
|
+
flexDirection: 'row',
|
|
257
|
+
justifyContent: 'space-between',
|
|
258
|
+
},
|
|
259
|
+
helperText: {
|
|
260
|
+
flex: 1,
|
|
261
|
+
},
|
|
262
|
+
count: {
|
|
263
|
+
marginLeft: 8,
|
|
264
|
+
},
|
|
265
|
+
});
|