@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,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image
|
|
3
|
+
*
|
|
4
|
+
* An enhanced image component with loading skeleton, error fallback,
|
|
5
|
+
* and optional blur-up placeholder effect.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // Basic usage
|
|
10
|
+
* <Image
|
|
11
|
+
* source={{ uri: 'https://example.com/image.jpg' }}
|
|
12
|
+
* style={{ width: 200, height: 200 }}
|
|
13
|
+
* />
|
|
14
|
+
*
|
|
15
|
+
* // With blur-up placeholder
|
|
16
|
+
* <Image
|
|
17
|
+
* source={{ uri: 'https://example.com/image.jpg' }}
|
|
18
|
+
* placeholder={{ uri: 'https://example.com/image-blur.jpg' }}
|
|
19
|
+
* style={{ width: '100%', aspectRatio: 16 / 9 }}
|
|
20
|
+
* />
|
|
21
|
+
*
|
|
22
|
+
* // With error fallback
|
|
23
|
+
* <Image
|
|
24
|
+
* source={{ uri: 'https://example.com/image.jpg' }}
|
|
25
|
+
* fallback={<Text>Failed to load</Text>}
|
|
26
|
+
* style={{ width: 100, height: 100, borderRadius: 8 }}
|
|
27
|
+
* />
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import React, { useState, useCallback } from 'react';
|
|
32
|
+
import {
|
|
33
|
+
View,
|
|
34
|
+
Image as RNImage,
|
|
35
|
+
ImageProps as RNImageProps,
|
|
36
|
+
ImageSourcePropType,
|
|
37
|
+
StyleSheet,
|
|
38
|
+
ViewStyle,
|
|
39
|
+
ImageStyle,
|
|
40
|
+
} from 'react-native';
|
|
41
|
+
import Animated, {
|
|
42
|
+
useSharedValue,
|
|
43
|
+
useAnimatedStyle,
|
|
44
|
+
withTiming,
|
|
45
|
+
interpolate,
|
|
46
|
+
Easing,
|
|
47
|
+
} from 'react-native-reanimated';
|
|
48
|
+
import Svg, { Path, Rect } from 'react-native-svg';
|
|
49
|
+
import { useTheme } from '@nativeui/core';
|
|
50
|
+
|
|
51
|
+
const AnimatedImage = Animated.createAnimatedComponent(RNImage);
|
|
52
|
+
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
// Types
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export interface ImageProps extends Omit<RNImageProps, 'source' | 'style'> {
|
|
58
|
+
/** Image source */
|
|
59
|
+
source: ImageSourcePropType;
|
|
60
|
+
/** Low-res placeholder for blur-up effect */
|
|
61
|
+
placeholder?: ImageSourcePropType;
|
|
62
|
+
/** Fallback content when image fails to load */
|
|
63
|
+
fallback?: React.ReactNode;
|
|
64
|
+
/** Show skeleton while loading */
|
|
65
|
+
showSkeleton?: boolean;
|
|
66
|
+
/** Container/image style */
|
|
67
|
+
style?: ImageStyle | ViewStyle;
|
|
68
|
+
/** Fade-in duration in ms */
|
|
69
|
+
fadeDuration?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
73
|
+
// Default Fallback Icon
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
function ImageFallbackIcon({ color, size = 48 }: { color: string; size?: number }) {
|
|
77
|
+
return (
|
|
78
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
79
|
+
<Rect
|
|
80
|
+
x="3"
|
|
81
|
+
y="3"
|
|
82
|
+
width="18"
|
|
83
|
+
height="18"
|
|
84
|
+
rx="2"
|
|
85
|
+
stroke={color}
|
|
86
|
+
strokeWidth="2"
|
|
87
|
+
/>
|
|
88
|
+
<Path
|
|
89
|
+
d="M3 16l5-5 4 4 4-4 5 5"
|
|
90
|
+
stroke={color}
|
|
91
|
+
strokeWidth="2"
|
|
92
|
+
strokeLinecap="round"
|
|
93
|
+
strokeLinejoin="round"
|
|
94
|
+
/>
|
|
95
|
+
<Path
|
|
96
|
+
d="M8.5 10a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"
|
|
97
|
+
fill={color}
|
|
98
|
+
/>
|
|
99
|
+
</Svg>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
104
|
+
// Skeleton Component
|
|
105
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
function ImageSkeleton({ style }: { style?: ViewStyle }) {
|
|
108
|
+
const { colors } = useTheme();
|
|
109
|
+
const shimmer = useSharedValue(0);
|
|
110
|
+
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
shimmer.value = withTiming(1, {
|
|
113
|
+
duration: 1500,
|
|
114
|
+
easing: Easing.inOut(Easing.ease),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const interval = setInterval(() => {
|
|
118
|
+
shimmer.value = 0;
|
|
119
|
+
shimmer.value = withTiming(1, {
|
|
120
|
+
duration: 1500,
|
|
121
|
+
easing: Easing.inOut(Easing.ease),
|
|
122
|
+
});
|
|
123
|
+
}, 1500);
|
|
124
|
+
|
|
125
|
+
return () => clearInterval(interval);
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
129
|
+
opacity: interpolate(shimmer.value, [0, 0.5, 1], [0.3, 0.6, 0.3]),
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Animated.View
|
|
134
|
+
style={[
|
|
135
|
+
StyleSheet.absoluteFill,
|
|
136
|
+
{ backgroundColor: colors.backgroundMuted },
|
|
137
|
+
animatedStyle,
|
|
138
|
+
style,
|
|
139
|
+
]}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
145
|
+
// Component
|
|
146
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
export function Image({
|
|
149
|
+
source,
|
|
150
|
+
placeholder,
|
|
151
|
+
fallback,
|
|
152
|
+
showSkeleton = true,
|
|
153
|
+
style,
|
|
154
|
+
fadeDuration = 300,
|
|
155
|
+
onLoad,
|
|
156
|
+
onError,
|
|
157
|
+
...props
|
|
158
|
+
}: ImageProps) {
|
|
159
|
+
const { colors, radius } = useTheme();
|
|
160
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
161
|
+
const [hasError, setHasError] = useState(false);
|
|
162
|
+
const opacity = useSharedValue(0);
|
|
163
|
+
|
|
164
|
+
const handleLoad = useCallback(
|
|
165
|
+
(e: any) => {
|
|
166
|
+
setIsLoading(false);
|
|
167
|
+
opacity.value = withTiming(1, {
|
|
168
|
+
duration: fadeDuration,
|
|
169
|
+
easing: Easing.out(Easing.ease),
|
|
170
|
+
});
|
|
171
|
+
onLoad?.(e);
|
|
172
|
+
},
|
|
173
|
+
[fadeDuration, onLoad]
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const handleError = useCallback(
|
|
177
|
+
(e: any) => {
|
|
178
|
+
setIsLoading(false);
|
|
179
|
+
setHasError(true);
|
|
180
|
+
onError?.(e);
|
|
181
|
+
},
|
|
182
|
+
[onError]
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const imageAnimatedStyle = useAnimatedStyle(() => ({
|
|
186
|
+
opacity: opacity.value,
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
// Extract dimensions from style
|
|
190
|
+
const flatStyle = StyleSheet.flatten(style) || {};
|
|
191
|
+
const { width, height, aspectRatio, borderRadius, ...containerStyle } = flatStyle as ImageStyle & ViewStyle;
|
|
192
|
+
|
|
193
|
+
const imageStyle: ImageStyle = {
|
|
194
|
+
width: width || '100%',
|
|
195
|
+
height: height || '100%',
|
|
196
|
+
aspectRatio,
|
|
197
|
+
borderRadius: borderRadius ?? radius.md,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Error state
|
|
201
|
+
if (hasError) {
|
|
202
|
+
return (
|
|
203
|
+
<View
|
|
204
|
+
style={[
|
|
205
|
+
styles.container,
|
|
206
|
+
{
|
|
207
|
+
width,
|
|
208
|
+
height,
|
|
209
|
+
aspectRatio,
|
|
210
|
+
borderRadius: borderRadius ?? radius.md,
|
|
211
|
+
backgroundColor: colors.backgroundMuted,
|
|
212
|
+
},
|
|
213
|
+
containerStyle,
|
|
214
|
+
]}
|
|
215
|
+
accessibilityRole="image"
|
|
216
|
+
accessibilityLabel="Image failed to load"
|
|
217
|
+
>
|
|
218
|
+
{fallback || (
|
|
219
|
+
<View style={styles.fallbackContent}>
|
|
220
|
+
<ImageFallbackIcon color={colors.foregroundMuted} />
|
|
221
|
+
</View>
|
|
222
|
+
)}
|
|
223
|
+
</View>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<View
|
|
229
|
+
style={[
|
|
230
|
+
styles.container,
|
|
231
|
+
{
|
|
232
|
+
width,
|
|
233
|
+
height,
|
|
234
|
+
aspectRatio,
|
|
235
|
+
borderRadius: borderRadius ?? radius.md,
|
|
236
|
+
overflow: 'hidden',
|
|
237
|
+
},
|
|
238
|
+
containerStyle,
|
|
239
|
+
]}
|
|
240
|
+
>
|
|
241
|
+
{/* Skeleton/Placeholder */}
|
|
242
|
+
{isLoading && (
|
|
243
|
+
<>
|
|
244
|
+
{placeholder ? (
|
|
245
|
+
<RNImage
|
|
246
|
+
source={placeholder}
|
|
247
|
+
style={[StyleSheet.absoluteFill, { opacity: 0.5 }]}
|
|
248
|
+
blurRadius={10}
|
|
249
|
+
/>
|
|
250
|
+
) : showSkeleton ? (
|
|
251
|
+
<ImageSkeleton />
|
|
252
|
+
) : null}
|
|
253
|
+
</>
|
|
254
|
+
)}
|
|
255
|
+
|
|
256
|
+
{/* Main Image */}
|
|
257
|
+
<AnimatedImage
|
|
258
|
+
source={source}
|
|
259
|
+
style={[imageStyle, imageAnimatedStyle]}
|
|
260
|
+
onLoad={handleLoad}
|
|
261
|
+
onError={handleError}
|
|
262
|
+
accessibilityRole="image"
|
|
263
|
+
{...props}
|
|
264
|
+
/>
|
|
265
|
+
</View>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
270
|
+
// Styles
|
|
271
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
const styles = StyleSheet.create({
|
|
274
|
+
container: {
|
|
275
|
+
justifyContent: 'center',
|
|
276
|
+
alignItems: 'center',
|
|
277
|
+
},
|
|
278
|
+
fallbackContent: {
|
|
279
|
+
...StyleSheet.absoluteFillObject,
|
|
280
|
+
justifyContent: 'center',
|
|
281
|
+
alignItems: 'center',
|
|
282
|
+
},
|
|
283
|
+
});
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input
|
|
3
|
+
*
|
|
4
|
+
* Text input with label, placeholder, error states, and animated focus.
|
|
5
|
+
* Uses design tokens for consistent styling.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <Input label="Email" placeholder="you@example.com" />
|
|
10
|
+
* <Input label="Password" secureTextEntry error="Invalid password" />
|
|
11
|
+
* <Input size="lg" label="Name" icon={<Icon name="user" />} />
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React, { forwardRef, useCallback } from 'react';
|
|
16
|
+
import {
|
|
17
|
+
View,
|
|
18
|
+
Text,
|
|
19
|
+
TextInput,
|
|
20
|
+
StyleSheet,
|
|
21
|
+
ViewStyle,
|
|
22
|
+
TextStyle,
|
|
23
|
+
TextInputProps,
|
|
24
|
+
} from 'react-native';
|
|
25
|
+
import Animated, {
|
|
26
|
+
useSharedValue,
|
|
27
|
+
useAnimatedStyle,
|
|
28
|
+
withTiming,
|
|
29
|
+
interpolateColor,
|
|
30
|
+
} from 'react-native-reanimated';
|
|
31
|
+
import { useTheme } from '@nativeui/core';
|
|
32
|
+
import { haptic } from '@nativeui/core';
|
|
33
|
+
|
|
34
|
+
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
|
|
35
|
+
|
|
36
|
+
export type InputSize = 'sm' | 'md' | 'lg';
|
|
37
|
+
|
|
38
|
+
export interface InputProps extends Omit<TextInputProps, 'style'> {
|
|
39
|
+
/** Label text above input */
|
|
40
|
+
label?: string;
|
|
41
|
+
/** Error message below input */
|
|
42
|
+
error?: string;
|
|
43
|
+
/** Helper text below input (hidden when error is present) */
|
|
44
|
+
helperText?: string;
|
|
45
|
+
/** Size variant */
|
|
46
|
+
size?: InputSize;
|
|
47
|
+
/** Icon element (left side) */
|
|
48
|
+
icon?: React.ReactNode;
|
|
49
|
+
/** Icon element (right side) */
|
|
50
|
+
iconRight?: React.ReactNode;
|
|
51
|
+
/** Container style */
|
|
52
|
+
containerStyle?: ViewStyle;
|
|
53
|
+
/** Input field style */
|
|
54
|
+
style?: TextStyle;
|
|
55
|
+
/** Label style */
|
|
56
|
+
labelStyle?: TextStyle;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const Input = forwardRef<TextInput, InputProps>(
|
|
60
|
+
(
|
|
61
|
+
{
|
|
62
|
+
label,
|
|
63
|
+
error,
|
|
64
|
+
helperText,
|
|
65
|
+
size = 'md',
|
|
66
|
+
icon,
|
|
67
|
+
iconRight,
|
|
68
|
+
containerStyle,
|
|
69
|
+
style,
|
|
70
|
+
labelStyle,
|
|
71
|
+
editable = true,
|
|
72
|
+
onFocus,
|
|
73
|
+
onBlur,
|
|
74
|
+
...props
|
|
75
|
+
},
|
|
76
|
+
ref
|
|
77
|
+
) => {
|
|
78
|
+
const { colors, components, timing, spacing } = useTheme();
|
|
79
|
+
const tokens = components.input[size];
|
|
80
|
+
const focusProgress = useSharedValue(0);
|
|
81
|
+
|
|
82
|
+
const hasError = !!error;
|
|
83
|
+
const isDisabled = editable === false;
|
|
84
|
+
|
|
85
|
+
const handleFocus = useCallback(
|
|
86
|
+
(e: any) => {
|
|
87
|
+
focusProgress.value = withTiming(1, timing.default);
|
|
88
|
+
haptic('selection');
|
|
89
|
+
onFocus?.(e);
|
|
90
|
+
},
|
|
91
|
+
[onFocus, timing.default]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const handleBlur = useCallback(
|
|
95
|
+
(e: any) => {
|
|
96
|
+
focusProgress.value = withTiming(0, timing.default);
|
|
97
|
+
onBlur?.(e);
|
|
98
|
+
},
|
|
99
|
+
[onBlur, timing.default]
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const animatedBorderStyle = useAnimatedStyle(() => {
|
|
103
|
+
// Error state takes priority
|
|
104
|
+
if (hasError) {
|
|
105
|
+
return {
|
|
106
|
+
borderColor: colors.destructive,
|
|
107
|
+
borderWidth: 2,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
borderColor: interpolateColor(
|
|
113
|
+
focusProgress.value,
|
|
114
|
+
[0, 1],
|
|
115
|
+
[colors.border, colors.primary]
|
|
116
|
+
),
|
|
117
|
+
borderWidth: focusProgress.value > 0.5 ? 2 : tokens.borderWidth,
|
|
118
|
+
};
|
|
119
|
+
}, [hasError, colors, tokens.borderWidth]);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<View style={[styles.container, containerStyle]}>
|
|
123
|
+
{label && (
|
|
124
|
+
<Text
|
|
125
|
+
style={[
|
|
126
|
+
styles.label,
|
|
127
|
+
{
|
|
128
|
+
fontSize: tokens.labelFontSize,
|
|
129
|
+
marginBottom: spacing[1.5],
|
|
130
|
+
color: hasError ? colors.destructive : colors.foreground,
|
|
131
|
+
},
|
|
132
|
+
labelStyle,
|
|
133
|
+
]}
|
|
134
|
+
>
|
|
135
|
+
{label}
|
|
136
|
+
</Text>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
<View style={styles.inputWrapper}>
|
|
140
|
+
{icon && (
|
|
141
|
+
<View
|
|
142
|
+
style={[
|
|
143
|
+
styles.iconContainer,
|
|
144
|
+
styles.iconLeft,
|
|
145
|
+
{ marginLeft: tokens.paddingHorizontal },
|
|
146
|
+
]}
|
|
147
|
+
>
|
|
148
|
+
{icon}
|
|
149
|
+
</View>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
<AnimatedTextInput
|
|
153
|
+
ref={ref}
|
|
154
|
+
style={[
|
|
155
|
+
styles.input,
|
|
156
|
+
{
|
|
157
|
+
minHeight: tokens.height,
|
|
158
|
+
paddingHorizontal: tokens.paddingHorizontal,
|
|
159
|
+
paddingVertical: tokens.paddingVertical,
|
|
160
|
+
borderRadius: tokens.borderRadius,
|
|
161
|
+
fontSize: tokens.fontSize,
|
|
162
|
+
backgroundColor: isDisabled ? colors.backgroundMuted : colors.background,
|
|
163
|
+
color: isDisabled ? colors.foregroundMuted : colors.foreground,
|
|
164
|
+
paddingLeft: icon ? tokens.paddingHorizontal + tokens.iconSize + spacing[2] : tokens.paddingHorizontal,
|
|
165
|
+
paddingRight: iconRight ? tokens.paddingHorizontal + tokens.iconSize + spacing[2] : tokens.paddingHorizontal,
|
|
166
|
+
},
|
|
167
|
+
animatedBorderStyle,
|
|
168
|
+
style,
|
|
169
|
+
]}
|
|
170
|
+
placeholderTextColor={colors.foregroundMuted}
|
|
171
|
+
editable={editable}
|
|
172
|
+
onFocus={handleFocus}
|
|
173
|
+
onBlur={handleBlur}
|
|
174
|
+
accessibilityLabel={label}
|
|
175
|
+
accessibilityState={{ disabled: isDisabled }}
|
|
176
|
+
{...props}
|
|
177
|
+
/>
|
|
178
|
+
|
|
179
|
+
{iconRight && (
|
|
180
|
+
<View
|
|
181
|
+
style={[
|
|
182
|
+
styles.iconContainer,
|
|
183
|
+
styles.iconRight,
|
|
184
|
+
{ marginRight: tokens.paddingHorizontal },
|
|
185
|
+
]}
|
|
186
|
+
>
|
|
187
|
+
{iconRight}
|
|
188
|
+
</View>
|
|
189
|
+
)}
|
|
190
|
+
</View>
|
|
191
|
+
|
|
192
|
+
{(error || helperText) && (
|
|
193
|
+
<Text
|
|
194
|
+
style={[
|
|
195
|
+
styles.helperText,
|
|
196
|
+
{
|
|
197
|
+
fontSize: tokens.helperFontSize,
|
|
198
|
+
marginTop: spacing[1],
|
|
199
|
+
color: hasError ? colors.destructive : colors.foregroundMuted,
|
|
200
|
+
},
|
|
201
|
+
]}
|
|
202
|
+
>
|
|
203
|
+
{error || helperText}
|
|
204
|
+
</Text>
|
|
205
|
+
)}
|
|
206
|
+
</View>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
Input.displayName = 'Input';
|
|
212
|
+
|
|
213
|
+
const styles = StyleSheet.create({
|
|
214
|
+
container: {
|
|
215
|
+
width: '100%',
|
|
216
|
+
},
|
|
217
|
+
inputWrapper: {
|
|
218
|
+
position: 'relative',
|
|
219
|
+
justifyContent: 'center',
|
|
220
|
+
},
|
|
221
|
+
label: {
|
|
222
|
+
fontWeight: '500',
|
|
223
|
+
},
|
|
224
|
+
input: {
|
|
225
|
+
// Dynamic styles applied inline
|
|
226
|
+
},
|
|
227
|
+
iconContainer: {
|
|
228
|
+
position: 'absolute',
|
|
229
|
+
zIndex: 1,
|
|
230
|
+
justifyContent: 'center',
|
|
231
|
+
alignItems: 'center',
|
|
232
|
+
},
|
|
233
|
+
iconLeft: {
|
|
234
|
+
left: 0,
|
|
235
|
+
},
|
|
236
|
+
iconRight: {
|
|
237
|
+
right: 0,
|
|
238
|
+
},
|
|
239
|
+
helperText: {
|
|
240
|
+
// Dynamic styles applied inline
|
|
241
|
+
},
|
|
242
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label
|
|
3
|
+
*
|
|
4
|
+
* A text label component for form inputs with consistent styling.
|
|
5
|
+
* Supports required indicator and disabled state.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <Label>Email</Label>
|
|
10
|
+
* <Label required>Password</Label>
|
|
11
|
+
* <Label disabled>Disabled Field</Label>
|
|
12
|
+
* <Label size="lg" htmlFor="name">Name</Label>
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import React from 'react';
|
|
17
|
+
import {
|
|
18
|
+
Text,
|
|
19
|
+
StyleSheet,
|
|
20
|
+
TextStyle,
|
|
21
|
+
TextProps,
|
|
22
|
+
} from 'react-native';
|
|
23
|
+
import { useTheme } from '@nativeui/core';
|
|
24
|
+
|
|
25
|
+
export type LabelSize = 'sm' | 'md' | 'lg';
|
|
26
|
+
|
|
27
|
+
export interface LabelProps extends Omit<TextProps, 'style'> {
|
|
28
|
+
/** Label content */
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
/** Size variant */
|
|
31
|
+
size?: LabelSize;
|
|
32
|
+
/** Show required asterisk */
|
|
33
|
+
required?: boolean;
|
|
34
|
+
/** Disabled state (muted color) */
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
/** Error state (red color) */
|
|
37
|
+
error?: boolean;
|
|
38
|
+
/** Associated input id (for web compatibility) */
|
|
39
|
+
htmlFor?: string;
|
|
40
|
+
/** Additional text styles */
|
|
41
|
+
style?: TextStyle;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sizeTokens: Record<LabelSize, { fontSize: number; lineHeight: number }> = {
|
|
45
|
+
sm: { fontSize: 12, lineHeight: 16 },
|
|
46
|
+
md: { fontSize: 14, lineHeight: 20 },
|
|
47
|
+
lg: { fontSize: 16, lineHeight: 24 },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function Label({
|
|
51
|
+
children,
|
|
52
|
+
size = 'md',
|
|
53
|
+
required = false,
|
|
54
|
+
disabled = false,
|
|
55
|
+
error = false,
|
|
56
|
+
style,
|
|
57
|
+
...props
|
|
58
|
+
}: LabelProps) {
|
|
59
|
+
const { colors } = useTheme();
|
|
60
|
+
const tokens = sizeTokens[size];
|
|
61
|
+
|
|
62
|
+
const textColor = error
|
|
63
|
+
? colors.destructive
|
|
64
|
+
: disabled
|
|
65
|
+
? colors.foregroundMuted
|
|
66
|
+
: colors.foreground;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Text
|
|
70
|
+
style={[
|
|
71
|
+
styles.label,
|
|
72
|
+
{
|
|
73
|
+
fontSize: tokens.fontSize,
|
|
74
|
+
lineHeight: tokens.lineHeight,
|
|
75
|
+
color: textColor,
|
|
76
|
+
},
|
|
77
|
+
style,
|
|
78
|
+
]}
|
|
79
|
+
accessibilityRole="text"
|
|
80
|
+
{...props}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
{required && (
|
|
84
|
+
<Text style={[styles.required, { color: colors.destructive }]}>
|
|
85
|
+
{' *'}
|
|
86
|
+
</Text>
|
|
87
|
+
)}
|
|
88
|
+
</Text>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const styles = StyleSheet.create({
|
|
93
|
+
label: {
|
|
94
|
+
fontWeight: '500',
|
|
95
|
+
},
|
|
96
|
+
required: {
|
|
97
|
+
fontWeight: '400',
|
|
98
|
+
},
|
|
99
|
+
});
|