@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.
Files changed (49) hide show
  1. package/dist/index.js +14 -3
  2. package/package.json +5 -3
  3. package/registry/registry.json +717 -0
  4. package/registry/ui/accordion.tsx +416 -0
  5. package/registry/ui/action-sheet.tsx +396 -0
  6. package/registry/ui/alert-dialog.tsx +355 -0
  7. package/registry/ui/avatar-stack.tsx +278 -0
  8. package/registry/ui/avatar.tsx +116 -0
  9. package/registry/ui/badge.tsx +125 -0
  10. package/registry/ui/button.tsx +240 -0
  11. package/registry/ui/card.tsx +675 -0
  12. package/registry/ui/carousel.tsx +431 -0
  13. package/registry/ui/checkbox.tsx +252 -0
  14. package/registry/ui/chip.tsx +271 -0
  15. package/registry/ui/column.tsx +133 -0
  16. package/registry/ui/datetime-picker.tsx +578 -0
  17. package/registry/ui/dialog.tsx +292 -0
  18. package/registry/ui/fab.tsx +225 -0
  19. package/registry/ui/form.tsx +323 -0
  20. package/registry/ui/horizontal-list.tsx +200 -0
  21. package/registry/ui/icon-button.tsx +244 -0
  22. package/registry/ui/image-gallery.tsx +455 -0
  23. package/registry/ui/image.tsx +283 -0
  24. package/registry/ui/input.tsx +242 -0
  25. package/registry/ui/label.tsx +99 -0
  26. package/registry/ui/list.tsx +519 -0
  27. package/registry/ui/progress.tsx +168 -0
  28. package/registry/ui/pull-to-refresh.tsx +231 -0
  29. package/registry/ui/radio-group.tsx +294 -0
  30. package/registry/ui/rating.tsx +311 -0
  31. package/registry/ui/row.tsx +136 -0
  32. package/registry/ui/screen.tsx +153 -0
  33. package/registry/ui/search-input.tsx +281 -0
  34. package/registry/ui/section-header.tsx +258 -0
  35. package/registry/ui/segmented-control.tsx +229 -0
  36. package/registry/ui/select.tsx +311 -0
  37. package/registry/ui/separator.tsx +74 -0
  38. package/registry/ui/sheet.tsx +362 -0
  39. package/registry/ui/skeleton.tsx +156 -0
  40. package/registry/ui/slider.tsx +307 -0
  41. package/registry/ui/spinner.tsx +100 -0
  42. package/registry/ui/stepper.tsx +314 -0
  43. package/registry/ui/stories.tsx +463 -0
  44. package/registry/ui/swipeable-row.tsx +362 -0
  45. package/registry/ui/switch.tsx +246 -0
  46. package/registry/ui/tabs.tsx +348 -0
  47. package/registry/ui/textarea.tsx +265 -0
  48. package/registry/ui/toast.tsx +316 -0
  49. package/registry/ui/tooltip.tsx +369 -0
@@ -0,0 +1,244 @@
1
+ /**
2
+ * IconButton
3
+ *
4
+ * A pressable button that displays only an icon.
5
+ * Square or circular shape with multiple variants and sizes.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // Basic usage
10
+ * <IconButton icon={<PlusIcon />} onPress={handleAdd} />
11
+ *
12
+ * // Variants
13
+ * <IconButton icon={<HeartIcon />} variant="ghost" />
14
+ * <IconButton icon={<TrashIcon />} variant="destructive" />
15
+ *
16
+ * // Sizes
17
+ * <IconButton icon={<MenuIcon />} size="sm" />
18
+ * <IconButton icon={<MenuIcon />} size="lg" rounded />
19
+ * ```
20
+ */
21
+
22
+ import React, { useCallback, forwardRef } from 'react';
23
+ import {
24
+ Pressable,
25
+ StyleSheet,
26
+ ViewStyle,
27
+ View,
28
+ PressableProps,
29
+ ActivityIndicator,
30
+ } from 'react-native';
31
+ import Animated, {
32
+ useSharedValue,
33
+ useAnimatedStyle,
34
+ withSpring,
35
+ } from 'react-native-reanimated';
36
+ import { useTheme } from '@nativeui/core';
37
+ import { haptic } from '@nativeui/core';
38
+
39
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
40
+
41
+ // ─────────────────────────────────────────────────────────────────────────────
42
+ // Types
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+
45
+ export type IconButtonVariant = 'default' | 'secondary' | 'outline' | 'ghost' | 'destructive';
46
+ export type IconButtonSize = 'sm' | 'md' | 'lg' | 'xl';
47
+
48
+ export interface IconButtonProps extends Omit<PressableProps, 'style'> {
49
+ /** Icon element to display */
50
+ icon: React.ReactNode;
51
+ /** Visual style variant */
52
+ variant?: IconButtonVariant;
53
+ /** Size preset */
54
+ size?: IconButtonSize;
55
+ /** Circular shape instead of rounded square */
56
+ rounded?: boolean;
57
+ /** Show loading spinner instead of icon */
58
+ loading?: boolean;
59
+ /** Disabled state */
60
+ disabled?: boolean;
61
+ /** Container style override */
62
+ style?: ViewStyle;
63
+ /** Accessibility label (required for icon-only buttons) */
64
+ accessibilityLabel: string;
65
+ }
66
+
67
+ // ─────────────────────────────────────────────────────────────────────────────
68
+ // Size Configuration
69
+ // ─────────────────────────────────────────────────────────────────────────────
70
+
71
+ const sizeConfig = {
72
+ sm: { size: 32, iconSize: 16 },
73
+ md: { size: 40, iconSize: 20 },
74
+ lg: { size: 48, iconSize: 24 },
75
+ xl: { size: 56, iconSize: 28 },
76
+ };
77
+
78
+ // ─────────────────────────────────────────────────────────────────────────────
79
+ // Component
80
+ // ─────────────────────────────────────────────────────────────────────────────
81
+
82
+ export const IconButton = forwardRef(function IconButton(
83
+ {
84
+ icon,
85
+ variant = 'default',
86
+ size = 'md',
87
+ rounded = false,
88
+ loading = false,
89
+ disabled,
90
+ style,
91
+ accessibilityLabel,
92
+ onPressIn,
93
+ onPressOut,
94
+ onPress,
95
+ ...props
96
+ }: IconButtonProps,
97
+ ref: React.ForwardedRef<View>
98
+ ) {
99
+ const { colors, radius, springs, platformShadow } = useTheme();
100
+ const config = sizeConfig[size];
101
+ const isDisabled = disabled || loading;
102
+
103
+ const scale = useSharedValue(1);
104
+
105
+ const handlePressIn = useCallback(
106
+ (e: any) => {
107
+ scale.value = withSpring(0.9, springs.snappy);
108
+ onPressIn?.(e);
109
+ },
110
+ [onPressIn, springs.snappy]
111
+ );
112
+
113
+ const handlePressOut = useCallback(
114
+ (e: any) => {
115
+ scale.value = withSpring(1, springs.snappy);
116
+ onPressOut?.(e);
117
+ },
118
+ [onPressOut, springs.snappy]
119
+ );
120
+
121
+ const handlePress = useCallback(
122
+ (e: any) => {
123
+ haptic('light');
124
+ onPress?.(e);
125
+ },
126
+ [onPress]
127
+ );
128
+
129
+ const animatedStyle = useAnimatedStyle(() => ({
130
+ transform: [{ scale: scale.value }],
131
+ }));
132
+
133
+ // Get variant-specific styles
134
+ const variantStyles = getVariantStyles(variant, colors);
135
+
136
+ // Determine border radius
137
+ const borderRadiusValue = rounded ? config.size / 2 : radius.md;
138
+
139
+ // Add shadow for solid buttons
140
+ const shadowStyle =
141
+ variant !== 'ghost' && variant !== 'outline' && !isDisabled
142
+ ? platformShadow('sm')
143
+ : {};
144
+
145
+ return (
146
+ <AnimatedPressable
147
+ ref={ref}
148
+ style={[
149
+ styles.base,
150
+ {
151
+ width: config.size,
152
+ height: config.size,
153
+ borderRadius: borderRadiusValue,
154
+ },
155
+ variantStyles.container,
156
+ shadowStyle,
157
+ isDisabled && styles.disabled,
158
+ animatedStyle,
159
+ style,
160
+ ]}
161
+ disabled={isDisabled}
162
+ onPressIn={handlePressIn}
163
+ onPressOut={handlePressOut}
164
+ onPress={handlePress}
165
+ accessibilityRole="button"
166
+ accessibilityLabel={accessibilityLabel}
167
+ accessibilityState={{ disabled: isDisabled }}
168
+ {...props}
169
+ >
170
+ {loading ? (
171
+ <ActivityIndicator
172
+ size={size === 'sm' ? 'small' : 'small'}
173
+ color={variantStyles.iconColor}
174
+ />
175
+ ) : (
176
+ React.cloneElement(icon as React.ReactElement<{ width?: number; height?: number; color?: string }>, {
177
+ width: config.iconSize,
178
+ height: config.iconSize,
179
+ color: variantStyles.iconColor,
180
+ })
181
+ )}
182
+ </AnimatedPressable>
183
+ );
184
+ });
185
+
186
+ // ─────────────────────────────────────────────────────────────────────────────
187
+ // Helpers
188
+ // ─────────────────────────────────────────────────────────────────────────────
189
+
190
+ function getVariantStyles(
191
+ variant: IconButtonVariant,
192
+ colors: ReturnType<typeof useTheme>['colors']
193
+ ) {
194
+ switch (variant) {
195
+ case 'default':
196
+ return {
197
+ container: { backgroundColor: colors.primary } as ViewStyle,
198
+ iconColor: colors.primaryForeground,
199
+ };
200
+ case 'secondary':
201
+ return {
202
+ container: { backgroundColor: colors.secondary } as ViewStyle,
203
+ iconColor: colors.secondaryForeground,
204
+ };
205
+ case 'outline':
206
+ return {
207
+ container: {
208
+ backgroundColor: 'transparent',
209
+ borderWidth: 1,
210
+ borderColor: colors.border,
211
+ } as ViewStyle,
212
+ iconColor: colors.foreground,
213
+ };
214
+ case 'ghost':
215
+ return {
216
+ container: { backgroundColor: 'transparent' } as ViewStyle,
217
+ iconColor: colors.foreground,
218
+ };
219
+ case 'destructive':
220
+ return {
221
+ container: { backgroundColor: colors.destructive } as ViewStyle,
222
+ iconColor: colors.destructiveForeground,
223
+ };
224
+ default:
225
+ return {
226
+ container: { backgroundColor: colors.primary } as ViewStyle,
227
+ iconColor: colors.primaryForeground,
228
+ };
229
+ }
230
+ }
231
+
232
+ // ─────────────────────────────────────────────────────────────────────────────
233
+ // Styles
234
+ // ─────────────────────────────────────────────────────────────────────────────
235
+
236
+ const styles = StyleSheet.create({
237
+ base: {
238
+ alignItems: 'center',
239
+ justifyContent: 'center',
240
+ },
241
+ disabled: {
242
+ opacity: 0.5,
243
+ },
244
+ });
@@ -0,0 +1,455 @@
1
+ /**
2
+ * ImageGallery
3
+ *
4
+ * Grid of images with fullscreen viewer on tap.
5
+ * Supports pinch-to-zoom and swipe navigation in fullscreen mode.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // Basic gallery
10
+ * <ImageGallery
11
+ * images={[
12
+ * { uri: 'https://...' },
13
+ * { uri: 'https://...' },
14
+ * { uri: 'https://...' },
15
+ * ]}
16
+ * />
17
+ *
18
+ * // Custom columns
19
+ * <ImageGallery
20
+ * images={photos}
21
+ * columns={2}
22
+ * spacing={8}
23
+ * />
24
+ *
25
+ * // With aspect ratio
26
+ * <ImageGallery
27
+ * images={photos}
28
+ * aspectRatio={1} // Square
29
+ * />
30
+ *
31
+ * // With onImagePress callback
32
+ * <ImageGallery
33
+ * images={photos}
34
+ * onImagePress={(index) => console.log('Pressed image:', index)}
35
+ * />
36
+ * ```
37
+ */
38
+
39
+ import React, { useState, useCallback } from 'react';
40
+ import {
41
+ View,
42
+ Image,
43
+ Pressable,
44
+ StyleSheet,
45
+ Modal,
46
+ Dimensions,
47
+ FlatList,
48
+ ImageSourcePropType,
49
+ StatusBar,
50
+ Platform,
51
+ } from 'react-native';
52
+ import Animated, {
53
+ useAnimatedStyle,
54
+ useSharedValue,
55
+ withTiming,
56
+ withSpring,
57
+ runOnJS,
58
+ Easing,
59
+ } from 'react-native-reanimated';
60
+ import {
61
+ Gesture,
62
+ GestureDetector,
63
+ GestureHandlerRootView,
64
+ } from 'react-native-gesture-handler';
65
+ import { useTheme, haptic } from '@nativeui/core';
66
+
67
+ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
68
+
69
+ const AnimatedImage = Animated.createAnimatedComponent(Image);
70
+
71
+ // ─────────────────────────────────────────────────────────────────────────────
72
+ // Types
73
+ // ─────────────────────────────────────────────────────────────────────────────
74
+
75
+ export interface GalleryImage {
76
+ /** Image URI */
77
+ uri: string;
78
+ /** Optional thumbnail URI (for grid) */
79
+ thumbnail?: string;
80
+ /** Optional alt text */
81
+ alt?: string;
82
+ }
83
+
84
+ export interface ImageGalleryProps {
85
+ /** Array of images */
86
+ images: GalleryImage[];
87
+ /** Number of columns in grid */
88
+ columns?: number;
89
+ /** Spacing between images */
90
+ spacing?: number;
91
+ /** Aspect ratio of grid images (width/height) */
92
+ aspectRatio?: number;
93
+ /** Border radius for grid images */
94
+ borderRadius?: number;
95
+ /** Called when an image is pressed */
96
+ onImagePress?: (index: number) => void;
97
+ /** Disable fullscreen viewer */
98
+ disableViewer?: boolean;
99
+ }
100
+
101
+ // ─────────────────────────────────────────────────────────────────────────────
102
+ // FullscreenViewer Component
103
+ // ─────────────────────────────────────────────────────────────────────────────
104
+
105
+ interface FullscreenViewerProps {
106
+ images: GalleryImage[];
107
+ initialIndex: number;
108
+ visible: boolean;
109
+ onClose: () => void;
110
+ }
111
+
112
+ function FullscreenViewer({
113
+ images,
114
+ initialIndex,
115
+ visible,
116
+ onClose,
117
+ }: FullscreenViewerProps) {
118
+ const { colors } = useTheme();
119
+ const [currentIndex, setCurrentIndex] = useState(initialIndex);
120
+
121
+ const opacity = useSharedValue(0);
122
+ const scale = useSharedValue(1);
123
+ const translateX = useSharedValue(0);
124
+ const translateY = useSharedValue(0);
125
+
126
+ React.useEffect(() => {
127
+ if (visible) {
128
+ opacity.value = withTiming(1, { duration: 200 });
129
+ setCurrentIndex(initialIndex);
130
+ } else {
131
+ opacity.value = withTiming(0, { duration: 150 });
132
+ }
133
+ }, [visible, initialIndex, opacity]);
134
+
135
+ const close = useCallback(() => {
136
+ opacity.value = withTiming(0, { duration: 150 });
137
+ setTimeout(onClose, 150);
138
+ }, [opacity, onClose]);
139
+
140
+ // Pinch to zoom gesture
141
+ const pinchGesture = Gesture.Pinch()
142
+ .onUpdate((event) => {
143
+ scale.value = Math.max(1, Math.min(event.scale, 3));
144
+ })
145
+ .onEnd(() => {
146
+ if (scale.value < 1.1) {
147
+ scale.value = withSpring(1);
148
+ translateX.value = withSpring(0);
149
+ translateY.value = withSpring(0);
150
+ }
151
+ });
152
+
153
+ // Pan gesture (when zoomed)
154
+ const panGesture = Gesture.Pan()
155
+ .onUpdate((event) => {
156
+ if (scale.value > 1) {
157
+ translateX.value = event.translationX;
158
+ translateY.value = event.translationY;
159
+ } else if (Math.abs(event.translationY) > 50) {
160
+ // Swipe down to close
161
+ opacity.value = withTiming(0, { duration: 150 });
162
+ runOnJS(close)();
163
+ }
164
+ })
165
+ .onEnd(() => {
166
+ if (scale.value <= 1) {
167
+ translateX.value = withSpring(0);
168
+ translateY.value = withSpring(0);
169
+ }
170
+ });
171
+
172
+ // Double tap to zoom
173
+ const doubleTapGesture = Gesture.Tap()
174
+ .numberOfTaps(2)
175
+ .onEnd(() => {
176
+ if (scale.value > 1) {
177
+ scale.value = withSpring(1);
178
+ translateX.value = withSpring(0);
179
+ translateY.value = withSpring(0);
180
+ } else {
181
+ scale.value = withSpring(2);
182
+ }
183
+ });
184
+
185
+ const composedGesture = Gesture.Simultaneous(
186
+ pinchGesture,
187
+ Gesture.Race(doubleTapGesture, panGesture)
188
+ );
189
+
190
+ const backdropStyle = useAnimatedStyle(() => ({
191
+ opacity: opacity.value,
192
+ }));
193
+
194
+ const imageStyle = useAnimatedStyle(() => ({
195
+ transform: [
196
+ { scale: scale.value },
197
+ { translateX: translateX.value },
198
+ { translateY: translateY.value },
199
+ ],
200
+ }));
201
+
202
+ const handleScroll = useCallback(
203
+ (event: { nativeEvent: { contentOffset: { x: number } } }) => {
204
+ const index = Math.round(event.nativeEvent.contentOffset.x / SCREEN_WIDTH);
205
+ if (index !== currentIndex && index >= 0 && index < images.length) {
206
+ setCurrentIndex(index);
207
+ // Reset zoom when changing images
208
+ scale.value = withTiming(1, { duration: 100 });
209
+ translateX.value = withTiming(0, { duration: 100 });
210
+ translateY.value = withTiming(0, { duration: 100 });
211
+ }
212
+ },
213
+ [currentIndex, images.length, scale, translateX, translateY]
214
+ );
215
+
216
+ const renderImage = useCallback(
217
+ ({ item }: { item: GalleryImage }) => (
218
+ <GestureDetector gesture={composedGesture}>
219
+ <View style={styles.fullscreenImageContainer}>
220
+ <AnimatedImage
221
+ source={{ uri: item.uri }}
222
+ style={[styles.fullscreenImage, imageStyle]}
223
+ resizeMode="contain"
224
+ accessibilityLabel={item.alt}
225
+ />
226
+ </View>
227
+ </GestureDetector>
228
+ ),
229
+ [composedGesture, imageStyle]
230
+ );
231
+
232
+ if (!visible) return null;
233
+
234
+ return (
235
+ <Modal visible={visible} transparent animationType="none" statusBarTranslucent>
236
+ <GestureHandlerRootView style={styles.fullscreenContainer}>
237
+ <Animated.View style={[styles.fullscreenBackdrop, backdropStyle]}>
238
+ {/* Close button */}
239
+ <Pressable
240
+ style={styles.closeButton}
241
+ onPress={() => {
242
+ haptic('light');
243
+ close();
244
+ }}
245
+ accessibilityRole="button"
246
+ accessibilityLabel="Close"
247
+ >
248
+ <View style={[styles.closeIcon, { backgroundColor: colors.background }]}>
249
+ <View style={[styles.closeLine, styles.closeLine1, { backgroundColor: colors.foreground }]} />
250
+ <View style={[styles.closeLine, styles.closeLine2, { backgroundColor: colors.foreground }]} />
251
+ </View>
252
+ </Pressable>
253
+
254
+ {/* Images */}
255
+ <FlatList
256
+ data={images}
257
+ renderItem={renderImage}
258
+ keyExtractor={(_, index) => index.toString()}
259
+ horizontal
260
+ pagingEnabled
261
+ showsHorizontalScrollIndicator={false}
262
+ initialScrollIndex={initialIndex}
263
+ getItemLayout={(_, index) => ({
264
+ length: SCREEN_WIDTH,
265
+ offset: SCREEN_WIDTH * index,
266
+ index,
267
+ })}
268
+ onMomentumScrollEnd={handleScroll}
269
+ />
270
+
271
+ {/* Page indicator */}
272
+ {images.length > 1 && (
273
+ <View style={styles.pageIndicator}>
274
+ {images.map((_, index) => (
275
+ <View
276
+ key={index}
277
+ style={[
278
+ styles.pageDot,
279
+ {
280
+ backgroundColor:
281
+ index === currentIndex
282
+ ? colors.background
283
+ : 'rgba(255,255,255,0.4)',
284
+ },
285
+ ]}
286
+ />
287
+ ))}
288
+ </View>
289
+ )}
290
+ </Animated.View>
291
+ </GestureHandlerRootView>
292
+ </Modal>
293
+ );
294
+ }
295
+
296
+ // ─────────────────────────────────────────────────────────────────────────────
297
+ // ImageGallery Component
298
+ // ─────────────────────────────────────────────────────────────────────────────
299
+
300
+ export function ImageGallery({
301
+ images,
302
+ columns = 3,
303
+ spacing = 2,
304
+ aspectRatio = 1,
305
+ borderRadius = 0,
306
+ onImagePress,
307
+ disableViewer = false,
308
+ }: ImageGalleryProps) {
309
+ const { colors, radius } = useTheme();
310
+ const [viewerVisible, setViewerVisible] = useState(false);
311
+ const [selectedIndex, setSelectedIndex] = useState(0);
312
+
313
+ const imageSize = (SCREEN_WIDTH - spacing * (columns + 1)) / columns;
314
+
315
+ const handleImagePress = useCallback(
316
+ (index: number) => {
317
+ haptic('light');
318
+ onImagePress?.(index);
319
+
320
+ if (!disableViewer) {
321
+ setSelectedIndex(index);
322
+ setViewerVisible(true);
323
+ }
324
+ },
325
+ [onImagePress, disableViewer]
326
+ );
327
+
328
+ const handleCloseViewer = useCallback(() => {
329
+ setViewerVisible(false);
330
+ }, []);
331
+
332
+ const renderItem = useCallback(
333
+ ({ item, index }: { item: GalleryImage; index: number }) => (
334
+ <Pressable
335
+ onPress={() => handleImagePress(index)}
336
+ style={({ pressed }) => [
337
+ styles.gridItem,
338
+ {
339
+ width: imageSize,
340
+ height: imageSize / aspectRatio,
341
+ marginLeft: spacing,
342
+ marginTop: spacing,
343
+ borderRadius: borderRadius || radius.sm,
344
+ opacity: pressed ? 0.8 : 1,
345
+ },
346
+ ]}
347
+ accessibilityRole="button"
348
+ accessibilityLabel={item.alt || `Image ${index + 1}`}
349
+ >
350
+ <Image
351
+ source={{ uri: item.thumbnail || item.uri }}
352
+ style={[
353
+ styles.gridImage,
354
+ { borderRadius: borderRadius || radius.sm },
355
+ ]}
356
+ />
357
+ </Pressable>
358
+ ),
359
+ [handleImagePress, imageSize, aspectRatio, spacing, borderRadius, radius.sm]
360
+ );
361
+
362
+ return (
363
+ <View style={styles.container}>
364
+ <FlatList
365
+ data={images}
366
+ renderItem={renderItem}
367
+ keyExtractor={(_, index) => index.toString()}
368
+ numColumns={columns}
369
+ scrollEnabled={false}
370
+ style={styles.grid}
371
+ contentContainerStyle={{ paddingBottom: spacing }}
372
+ />
373
+
374
+ <FullscreenViewer
375
+ images={images}
376
+ initialIndex={selectedIndex}
377
+ visible={viewerVisible}
378
+ onClose={handleCloseViewer}
379
+ />
380
+ </View>
381
+ );
382
+ }
383
+
384
+ // ─────────────────────────────────────────────────────────────────────────────
385
+ // Styles
386
+ // ─────────────────────────────────────────────────────────────────────────────
387
+
388
+ const styles = StyleSheet.create({
389
+ container: {},
390
+ grid: {},
391
+ gridItem: {
392
+ overflow: 'hidden',
393
+ },
394
+ gridImage: {
395
+ width: '100%',
396
+ height: '100%',
397
+ resizeMode: 'cover',
398
+ },
399
+ fullscreenContainer: {
400
+ flex: 1,
401
+ },
402
+ fullscreenBackdrop: {
403
+ flex: 1,
404
+ backgroundColor: 'rgba(0, 0, 0, 0.95)',
405
+ },
406
+ fullscreenImageContainer: {
407
+ width: SCREEN_WIDTH,
408
+ height: SCREEN_HEIGHT,
409
+ justifyContent: 'center',
410
+ alignItems: 'center',
411
+ },
412
+ fullscreenImage: {
413
+ width: SCREEN_WIDTH,
414
+ height: SCREEN_HEIGHT * 0.8,
415
+ },
416
+ closeButton: {
417
+ position: 'absolute',
418
+ top: Platform.OS === 'ios' ? 50 : 20,
419
+ right: 20,
420
+ zIndex: 100,
421
+ },
422
+ closeIcon: {
423
+ width: 36,
424
+ height: 36,
425
+ borderRadius: 18,
426
+ alignItems: 'center',
427
+ justifyContent: 'center',
428
+ },
429
+ closeLine: {
430
+ position: 'absolute',
431
+ width: 18,
432
+ height: 2,
433
+ borderRadius: 1,
434
+ },
435
+ closeLine1: {
436
+ transform: [{ rotate: '45deg' }],
437
+ },
438
+ closeLine2: {
439
+ transform: [{ rotate: '-45deg' }],
440
+ },
441
+ pageIndicator: {
442
+ position: 'absolute',
443
+ bottom: 50,
444
+ left: 0,
445
+ right: 0,
446
+ flexDirection: 'row',
447
+ justifyContent: 'center',
448
+ gap: 6,
449
+ },
450
+ pageDot: {
451
+ width: 6,
452
+ height: 6,
453
+ borderRadius: 3,
454
+ },
455
+ });