@metacells/mcellui-mcp-server 0.1.1 → 0.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/dist/index.js +14 -3
- 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,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sheet
|
|
3
|
+
*
|
|
4
|
+
* A bottom sheet component with snap points and gesture support.
|
|
5
|
+
* Slides up from the bottom with backdrop.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const [open, setOpen] = useState(false);
|
|
10
|
+
*
|
|
11
|
+
* <Sheet open={open} onOpenChange={setOpen}>
|
|
12
|
+
* <SheetContent>
|
|
13
|
+
* <SheetHeader>
|
|
14
|
+
* <SheetTitle>Edit Profile</SheetTitle>
|
|
15
|
+
* <SheetDescription>Make changes to your profile</SheetDescription>
|
|
16
|
+
* </SheetHeader>
|
|
17
|
+
* <View>{content}</View>
|
|
18
|
+
* </SheetContent>
|
|
19
|
+
* </Sheet>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import React, { useEffect, useCallback, createContext, useContext } from 'react';
|
|
24
|
+
import {
|
|
25
|
+
View,
|
|
26
|
+
Text,
|
|
27
|
+
Modal,
|
|
28
|
+
Pressable,
|
|
29
|
+
StyleSheet,
|
|
30
|
+
ViewStyle,
|
|
31
|
+
TextStyle,
|
|
32
|
+
Dimensions,
|
|
33
|
+
KeyboardAvoidingView,
|
|
34
|
+
Platform,
|
|
35
|
+
} from 'react-native';
|
|
36
|
+
import Animated, {
|
|
37
|
+
useSharedValue,
|
|
38
|
+
useAnimatedStyle,
|
|
39
|
+
withSpring,
|
|
40
|
+
withTiming,
|
|
41
|
+
runOnJS,
|
|
42
|
+
interpolate,
|
|
43
|
+
Extrapolation,
|
|
44
|
+
} from 'react-native-reanimated';
|
|
45
|
+
import {
|
|
46
|
+
Gesture,
|
|
47
|
+
GestureDetector,
|
|
48
|
+
GestureHandlerRootView,
|
|
49
|
+
} from 'react-native-gesture-handler';
|
|
50
|
+
import { useTheme, SHEET_CONSTANTS } from '@nativeui/core';
|
|
51
|
+
|
|
52
|
+
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
|
53
|
+
|
|
54
|
+
// Context for sheet state
|
|
55
|
+
const SheetContext = createContext<{
|
|
56
|
+
onClose: () => void;
|
|
57
|
+
} | null>(null);
|
|
58
|
+
|
|
59
|
+
const useSheet = () => {
|
|
60
|
+
const context = useContext(SheetContext);
|
|
61
|
+
if (!context) {
|
|
62
|
+
throw new Error('Sheet components must be used within a Sheet');
|
|
63
|
+
}
|
|
64
|
+
return context;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export interface SheetProps {
|
|
68
|
+
/** Controlled open state */
|
|
69
|
+
open: boolean;
|
|
70
|
+
/** Callback when open state changes */
|
|
71
|
+
onOpenChange: (open: boolean) => void;
|
|
72
|
+
/** Children (should be SheetContent) */
|
|
73
|
+
children: React.ReactNode;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function Sheet({ open, onOpenChange, children }: SheetProps) {
|
|
77
|
+
const handleClose = useCallback(() => {
|
|
78
|
+
onOpenChange(false);
|
|
79
|
+
}, [onOpenChange]);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Modal
|
|
83
|
+
visible={open}
|
|
84
|
+
transparent
|
|
85
|
+
animationType="none"
|
|
86
|
+
statusBarTranslucent
|
|
87
|
+
onRequestClose={handleClose}
|
|
88
|
+
>
|
|
89
|
+
<GestureHandlerRootView style={styles.gestureRoot}>
|
|
90
|
+
<SheetContext.Provider value={{ onClose: handleClose }}>
|
|
91
|
+
{children}
|
|
92
|
+
</SheetContext.Provider>
|
|
93
|
+
</GestureHandlerRootView>
|
|
94
|
+
</Modal>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface SheetContentProps {
|
|
99
|
+
/** Content children */
|
|
100
|
+
children: React.ReactNode;
|
|
101
|
+
/** Height of sheet (default: 50% of screen) */
|
|
102
|
+
height?: number | string;
|
|
103
|
+
/** Show drag handle */
|
|
104
|
+
showHandle?: boolean;
|
|
105
|
+
/** Threshold (0-1) für automatisches Schließen beim Ziehen */
|
|
106
|
+
closeThreshold?: number;
|
|
107
|
+
/** Geschwindigkeit (px/s) für Swipe-to-Close */
|
|
108
|
+
velocityThreshold?: number;
|
|
109
|
+
/** Additional styles */
|
|
110
|
+
style?: ViewStyle;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function SheetContent({
|
|
114
|
+
children,
|
|
115
|
+
height = '50%',
|
|
116
|
+
showHandle = true,
|
|
117
|
+
closeThreshold = SHEET_CONSTANTS.closeThreshold,
|
|
118
|
+
velocityThreshold = SHEET_CONSTANTS.velocityThreshold,
|
|
119
|
+
style,
|
|
120
|
+
}: SheetContentProps) {
|
|
121
|
+
const { colors, radius, springs } = useTheme();
|
|
122
|
+
const { onClose } = useSheet();
|
|
123
|
+
|
|
124
|
+
const translateY = useSharedValue(SCREEN_HEIGHT);
|
|
125
|
+
const backdropOpacity = useSharedValue(0);
|
|
126
|
+
const isClosing = useSharedValue(false);
|
|
127
|
+
|
|
128
|
+
// Calculate actual height
|
|
129
|
+
const sheetHeight = typeof height === 'string'
|
|
130
|
+
? (parseFloat(height) / 100) * SCREEN_HEIGHT
|
|
131
|
+
: height;
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
translateY.value = withSpring(0, springs.snappy);
|
|
135
|
+
backdropOpacity.value = withTiming(1, { duration: SHEET_CONSTANTS.backdropFadeInDuration });
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
const closeSheet = useCallback(() => {
|
|
139
|
+
if (isClosing.value) return;
|
|
140
|
+
isClosing.value = true;
|
|
141
|
+
backdropOpacity.value = withTiming(0, { duration: SHEET_CONSTANTS.backdropFadeOutDuration });
|
|
142
|
+
translateY.value = withSpring(
|
|
143
|
+
SCREEN_HEIGHT,
|
|
144
|
+
springs.snappy,
|
|
145
|
+
(finished) => {
|
|
146
|
+
if (finished) {
|
|
147
|
+
runOnJS(onClose)();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
}, [onClose, springs.snappy]);
|
|
152
|
+
|
|
153
|
+
const panGesture = Gesture.Pan()
|
|
154
|
+
.onUpdate((event) => {
|
|
155
|
+
if (event.translationY > 0) {
|
|
156
|
+
translateY.value = event.translationY;
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
.onEnd((event) => {
|
|
160
|
+
if (event.translationY > sheetHeight * closeThreshold || event.velocityY > velocityThreshold) {
|
|
161
|
+
runOnJS(closeSheet)();
|
|
162
|
+
} else {
|
|
163
|
+
translateY.value = withSpring(0, springs.snappy);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const animatedSheetStyle = useAnimatedStyle(() => ({
|
|
168
|
+
transform: [{ translateY: translateY.value }],
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
const animatedBackdropStyle = useAnimatedStyle(() => ({
|
|
172
|
+
opacity: interpolate(
|
|
173
|
+
backdropOpacity.value,
|
|
174
|
+
[0, 1],
|
|
175
|
+
[0, SHEET_CONSTANTS.backdropMaxOpacity],
|
|
176
|
+
Extrapolation.CLAMP
|
|
177
|
+
),
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<View style={styles.container}>
|
|
182
|
+
<Animated.View
|
|
183
|
+
style={[
|
|
184
|
+
styles.backdrop,
|
|
185
|
+
{ backgroundColor: colors.foreground },
|
|
186
|
+
animatedBackdropStyle,
|
|
187
|
+
]}
|
|
188
|
+
>
|
|
189
|
+
<Pressable style={styles.backdropPressable} onPress={closeSheet} />
|
|
190
|
+
</Animated.View>
|
|
191
|
+
|
|
192
|
+
<KeyboardAvoidingView
|
|
193
|
+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
194
|
+
style={styles.keyboardView}
|
|
195
|
+
>
|
|
196
|
+
<GestureDetector gesture={panGesture}>
|
|
197
|
+
<Animated.View
|
|
198
|
+
style={[
|
|
199
|
+
styles.sheet,
|
|
200
|
+
{
|
|
201
|
+
height: sheetHeight,
|
|
202
|
+
backgroundColor: colors.card,
|
|
203
|
+
borderTopLeftRadius: radius.xl,
|
|
204
|
+
borderTopRightRadius: radius.xl,
|
|
205
|
+
},
|
|
206
|
+
animatedSheetStyle,
|
|
207
|
+
style,
|
|
208
|
+
]}
|
|
209
|
+
>
|
|
210
|
+
{showHandle && (
|
|
211
|
+
<View style={styles.handleContainer}>
|
|
212
|
+
<View
|
|
213
|
+
style={[
|
|
214
|
+
styles.handle,
|
|
215
|
+
{ backgroundColor: colors.border },
|
|
216
|
+
]}
|
|
217
|
+
/>
|
|
218
|
+
</View>
|
|
219
|
+
)}
|
|
220
|
+
<View style={styles.content}>{children}</View>
|
|
221
|
+
</Animated.View>
|
|
222
|
+
</GestureDetector>
|
|
223
|
+
</KeyboardAvoidingView>
|
|
224
|
+
</View>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface SheetHeaderProps {
|
|
229
|
+
children: React.ReactNode;
|
|
230
|
+
style?: ViewStyle;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function SheetHeader({ children, style }: SheetHeaderProps) {
|
|
234
|
+
const { spacing } = useTheme();
|
|
235
|
+
return (
|
|
236
|
+
<View style={[styles.header, { paddingBottom: spacing[4] }, style]}>
|
|
237
|
+
{children}
|
|
238
|
+
</View>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface SheetTitleProps {
|
|
243
|
+
children: React.ReactNode;
|
|
244
|
+
style?: TextStyle;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function SheetTitle({ children, style }: SheetTitleProps) {
|
|
248
|
+
const { colors } = useTheme();
|
|
249
|
+
return (
|
|
250
|
+
<Text style={[styles.title, { color: colors.foreground }, style]}>
|
|
251
|
+
{children}
|
|
252
|
+
</Text>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface SheetDescriptionProps {
|
|
257
|
+
children: React.ReactNode;
|
|
258
|
+
style?: TextStyle;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function SheetDescription({ children, style }: SheetDescriptionProps) {
|
|
262
|
+
const { colors, spacing } = useTheme();
|
|
263
|
+
return (
|
|
264
|
+
<Text
|
|
265
|
+
style={[
|
|
266
|
+
styles.description,
|
|
267
|
+
{ color: colors.foregroundMuted, marginTop: spacing[1] },
|
|
268
|
+
style,
|
|
269
|
+
]}
|
|
270
|
+
>
|
|
271
|
+
{children}
|
|
272
|
+
</Text>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export interface SheetFooterProps {
|
|
277
|
+
children: React.ReactNode;
|
|
278
|
+
style?: ViewStyle;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function SheetFooter({ children, style }: SheetFooterProps) {
|
|
282
|
+
const { spacing } = useTheme();
|
|
283
|
+
return (
|
|
284
|
+
<View style={[styles.footer, { paddingTop: spacing[4] }, style]}>
|
|
285
|
+
{children}
|
|
286
|
+
</View>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface SheetCloseProps {
|
|
291
|
+
children: React.ReactNode;
|
|
292
|
+
asChild?: boolean;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function SheetClose({ children, asChild }: SheetCloseProps) {
|
|
296
|
+
const { onClose } = useSheet();
|
|
297
|
+
|
|
298
|
+
if (asChild && React.isValidElement(children)) {
|
|
299
|
+
return React.cloneElement(children as React.ReactElement<any>, {
|
|
300
|
+
onPress: onClose,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<Pressable onPress={onClose}>
|
|
306
|
+
{children}
|
|
307
|
+
</Pressable>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const styles = StyleSheet.create({
|
|
312
|
+
gestureRoot: {
|
|
313
|
+
flex: 1,
|
|
314
|
+
},
|
|
315
|
+
container: {
|
|
316
|
+
flex: 1,
|
|
317
|
+
justifyContent: 'flex-end',
|
|
318
|
+
},
|
|
319
|
+
backdrop: {
|
|
320
|
+
...StyleSheet.absoluteFillObject,
|
|
321
|
+
},
|
|
322
|
+
backdropPressable: {
|
|
323
|
+
flex: 1,
|
|
324
|
+
},
|
|
325
|
+
keyboardView: {
|
|
326
|
+
justifyContent: 'flex-end',
|
|
327
|
+
},
|
|
328
|
+
sheet: {
|
|
329
|
+
overflow: 'hidden',
|
|
330
|
+
},
|
|
331
|
+
handleContainer: {
|
|
332
|
+
alignItems: 'center',
|
|
333
|
+
paddingTop: SHEET_CONSTANTS.handlePaddingTop,
|
|
334
|
+
paddingBottom: SHEET_CONSTANTS.handlePaddingBottom,
|
|
335
|
+
},
|
|
336
|
+
handle: {
|
|
337
|
+
width: SHEET_CONSTANTS.handleWidth,
|
|
338
|
+
height: SHEET_CONSTANTS.handleHeight,
|
|
339
|
+
borderRadius: SHEET_CONSTANTS.handleHeight / 2,
|
|
340
|
+
},
|
|
341
|
+
content: {
|
|
342
|
+
flex: 1,
|
|
343
|
+
paddingHorizontal: SHEET_CONSTANTS.contentPaddingHorizontal,
|
|
344
|
+
},
|
|
345
|
+
header: {
|
|
346
|
+
alignItems: 'center',
|
|
347
|
+
},
|
|
348
|
+
title: {
|
|
349
|
+
fontSize: 18,
|
|
350
|
+
fontWeight: '600',
|
|
351
|
+
textAlign: 'center',
|
|
352
|
+
},
|
|
353
|
+
description: {
|
|
354
|
+
fontSize: 14,
|
|
355
|
+
textAlign: 'center',
|
|
356
|
+
},
|
|
357
|
+
footer: {
|
|
358
|
+
flexDirection: 'row',
|
|
359
|
+
justifyContent: 'flex-end',
|
|
360
|
+
gap: 8,
|
|
361
|
+
},
|
|
362
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skeleton
|
|
3
|
+
*
|
|
4
|
+
* A loading placeholder with shimmer animation.
|
|
5
|
+
* Use to indicate content is loading.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <Skeleton width={200} height={20} />
|
|
10
|
+
* <Skeleton width="100%" height={48} radius="lg" />
|
|
11
|
+
* <Skeleton circle size={40} />
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React, { useEffect } from 'react';
|
|
16
|
+
import { View, StyleSheet, ViewStyle, DimensionValue } from 'react-native';
|
|
17
|
+
import Animated, {
|
|
18
|
+
useSharedValue,
|
|
19
|
+
useAnimatedStyle,
|
|
20
|
+
withRepeat,
|
|
21
|
+
withTiming,
|
|
22
|
+
interpolate,
|
|
23
|
+
Easing,
|
|
24
|
+
} from 'react-native-reanimated';
|
|
25
|
+
import { useTheme } from '@nativeui/core';
|
|
26
|
+
|
|
27
|
+
export type SkeletonRadius = 'none' | 'sm' | 'md' | 'lg' | 'full';
|
|
28
|
+
|
|
29
|
+
export interface SkeletonProps {
|
|
30
|
+
/** Width of the skeleton */
|
|
31
|
+
width?: DimensionValue;
|
|
32
|
+
/** Height of the skeleton */
|
|
33
|
+
height?: DimensionValue;
|
|
34
|
+
/** Border radius preset */
|
|
35
|
+
radius?: SkeletonRadius;
|
|
36
|
+
/** Render as circle (uses size for width/height) */
|
|
37
|
+
circle?: boolean;
|
|
38
|
+
/** Size when circle is true */
|
|
39
|
+
size?: number;
|
|
40
|
+
/** Disable animation */
|
|
41
|
+
animate?: boolean;
|
|
42
|
+
/** Additional styles */
|
|
43
|
+
style?: ViewStyle;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const radiusMap: Record<SkeletonRadius, number> = {
|
|
47
|
+
none: 0,
|
|
48
|
+
sm: 4,
|
|
49
|
+
md: 8,
|
|
50
|
+
lg: 12,
|
|
51
|
+
full: 9999,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function Skeleton({
|
|
55
|
+
width = '100%',
|
|
56
|
+
height = 20,
|
|
57
|
+
radius = 'md',
|
|
58
|
+
circle = false,
|
|
59
|
+
size = 40,
|
|
60
|
+
animate = true,
|
|
61
|
+
style,
|
|
62
|
+
}: SkeletonProps) {
|
|
63
|
+
const { colors } = useTheme();
|
|
64
|
+
const shimmerProgress = useSharedValue(0);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (animate) {
|
|
68
|
+
shimmerProgress.value = withRepeat(
|
|
69
|
+
withTiming(1, {
|
|
70
|
+
duration: 1500,
|
|
71
|
+
easing: Easing.inOut(Easing.ease),
|
|
72
|
+
}),
|
|
73
|
+
-1,
|
|
74
|
+
false
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}, [animate]);
|
|
78
|
+
|
|
79
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
80
|
+
const opacity = interpolate(
|
|
81
|
+
shimmerProgress.value,
|
|
82
|
+
[0, 0.5, 1],
|
|
83
|
+
[0.3, 0.6, 0.3]
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
opacity: animate ? opacity : 0.3,
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const dimensions = circle
|
|
92
|
+
? { width: size, height: size, borderRadius: size / 2 }
|
|
93
|
+
: {
|
|
94
|
+
width,
|
|
95
|
+
height,
|
|
96
|
+
borderRadius: radiusMap[radius],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Animated.View
|
|
101
|
+
style={[
|
|
102
|
+
styles.skeleton,
|
|
103
|
+
{ backgroundColor: colors.foregroundMuted },
|
|
104
|
+
dimensions,
|
|
105
|
+
animatedStyle,
|
|
106
|
+
style,
|
|
107
|
+
]}
|
|
108
|
+
accessibilityRole="progressbar"
|
|
109
|
+
accessibilityLabel="Loading"
|
|
110
|
+
accessibilityState={{ busy: true }}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface SkeletonTextProps {
|
|
116
|
+
/** Number of lines */
|
|
117
|
+
lines?: number;
|
|
118
|
+
/** Gap between lines */
|
|
119
|
+
gap?: number;
|
|
120
|
+
/** Last line width percentage */
|
|
121
|
+
lastLineWidth?: DimensionValue;
|
|
122
|
+
/** Line height */
|
|
123
|
+
lineHeight?: number;
|
|
124
|
+
/** Container style */
|
|
125
|
+
style?: ViewStyle;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function SkeletonText({
|
|
129
|
+
lines = 3,
|
|
130
|
+
gap = 8,
|
|
131
|
+
lastLineWidth = '60%',
|
|
132
|
+
lineHeight = 16,
|
|
133
|
+
style,
|
|
134
|
+
}: SkeletonTextProps) {
|
|
135
|
+
return (
|
|
136
|
+
<View style={[styles.textContainer, { gap }, style]}>
|
|
137
|
+
{Array.from({ length: lines }).map((_, index) => (
|
|
138
|
+
<Skeleton
|
|
139
|
+
key={index}
|
|
140
|
+
width={index === lines - 1 ? lastLineWidth : '100%'}
|
|
141
|
+
height={lineHeight}
|
|
142
|
+
radius="sm"
|
|
143
|
+
/>
|
|
144
|
+
))}
|
|
145
|
+
</View>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const styles = StyleSheet.create({
|
|
150
|
+
skeleton: {
|
|
151
|
+
overflow: 'hidden',
|
|
152
|
+
},
|
|
153
|
+
textContainer: {
|
|
154
|
+
width: '100%',
|
|
155
|
+
},
|
|
156
|
+
});
|