@umituz/react-native-design-system 2.6.111 → 2.6.113

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 (61) hide show
  1. package/package.json +7 -3
  2. package/src/atoms/image/AtomicImage.tsx +29 -0
  3. package/src/atoms/index.ts +3 -0
  4. package/src/exports/atoms.ts +2 -0
  5. package/src/exports/image.ts +7 -0
  6. package/src/image/domain/entities/EditorTypes.ts +23 -0
  7. package/src/image/domain/entities/ImageConstants.ts +38 -0
  8. package/src/image/domain/entities/ImageFilterTypes.ts +70 -0
  9. package/src/image/domain/entities/ImageTemplateTypes.ts +18 -0
  10. package/src/image/domain/entities/ImageTypes.ts +86 -0
  11. package/src/image/domain/entities/ValidationResult.ts +15 -0
  12. package/src/image/domain/entities/editor/EditorConfigTypes.ts +35 -0
  13. package/src/image/domain/entities/editor/EditorElementTypes.ts +60 -0
  14. package/src/image/domain/entities/editor/EditorFilterTypes.ts +9 -0
  15. package/src/image/domain/entities/editor/EditorLayerTypes.ts +34 -0
  16. package/src/image/domain/entities/editor/EditorStateTypes.ts +35 -0
  17. package/src/image/domain/entities/editor/EditorToolTypes.ts +33 -0
  18. package/src/image/domain/utils/ImageUtils.ts +103 -0
  19. package/src/image/index.ts +123 -0
  20. package/src/image/infrastructure/services/ImageBatchService.ts +110 -0
  21. package/src/image/infrastructure/services/ImageConversionService.ts +74 -0
  22. package/src/image/infrastructure/services/ImageEditorService.ts +136 -0
  23. package/src/image/infrastructure/services/ImageEnhanceService.ts +123 -0
  24. package/src/image/infrastructure/services/ImageMetadataService.ts +116 -0
  25. package/src/image/infrastructure/services/ImageStorageService.ts +37 -0
  26. package/src/image/infrastructure/services/ImageTemplateService.ts +66 -0
  27. package/src/image/infrastructure/services/ImageTransformService.ts +89 -0
  28. package/src/image/infrastructure/services/ImageViewerService.ts +64 -0
  29. package/src/image/infrastructure/utils/BatchProcessor.ts +95 -0
  30. package/src/image/infrastructure/utils/FilterProcessor.ts +124 -0
  31. package/src/image/infrastructure/utils/ImageAnalysisUtils.ts +122 -0
  32. package/src/image/infrastructure/utils/ImageEditorHistoryUtils.ts +63 -0
  33. package/src/image/infrastructure/utils/ImageErrorHandler.ts +40 -0
  34. package/src/image/infrastructure/utils/ImageFilterUtils.ts +21 -0
  35. package/src/image/infrastructure/utils/ImageQualityPresets.ts +110 -0
  36. package/src/image/infrastructure/utils/ImageTransformUtils.ts +25 -0
  37. package/src/image/infrastructure/utils/ImageValidator.ts +59 -0
  38. package/src/image/infrastructure/utils/LayerManager.ts +77 -0
  39. package/src/image/infrastructure/utils/MetadataExtractor.ts +83 -0
  40. package/src/image/infrastructure/utils/filters/BasicFilters.ts +61 -0
  41. package/src/image/infrastructure/utils/filters/FilterHelpers.ts +21 -0
  42. package/src/image/infrastructure/utils/filters/SpecialFilters.ts +84 -0
  43. package/src/image/infrastructure/utils/validation/image-validator.ts +77 -0
  44. package/src/image/infrastructure/utils/validation/mime-type-validator.ts +101 -0
  45. package/src/image/infrastructure/utils/validation/mime-types.constants.ts +41 -0
  46. package/src/image/presentation/components/GalleryHeader.tsx +126 -0
  47. package/src/image/presentation/components/ImageGallery.tsx +138 -0
  48. package/src/image/presentation/components/editor/FilterPickerSheet.tsx +75 -0
  49. package/src/image/presentation/components/editor/StickerPickerSheet.tsx +62 -0
  50. package/src/image/presentation/components/editor/TextEditorSheet.tsx +98 -0
  51. package/src/image/presentation/components/editor/TextEditorTabs.tsx +111 -0
  52. package/src/image/presentation/components/image/AtomicImage.tsx +29 -0
  53. package/src/image/presentation/hooks/useImage.ts +39 -0
  54. package/src/image/presentation/hooks/useImageBatch.ts +28 -0
  55. package/src/image/presentation/hooks/useImageConversion.ts +29 -0
  56. package/src/image/presentation/hooks/useImageEnhance.ts +32 -0
  57. package/src/image/presentation/hooks/useImageGallery.ts +90 -0
  58. package/src/image/presentation/hooks/useImageMetadata.ts +28 -0
  59. package/src/image/presentation/hooks/useImageOperation.ts +37 -0
  60. package/src/image/presentation/hooks/useImageTransform.ts +42 -0
  61. package/src/index.ts +5 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * MIME Type Validator
3
+ * Single Responsibility: Validate MIME types
4
+ */
5
+
6
+ import type { ValidationResult } from '../../../domain/entities/ValidationResult';
7
+ import {
8
+ SUPPORTED_IMAGE_MIME_TYPES,
9
+ EXTENSION_TO_MIME_TYPE,
10
+ } from './mime-types.constants';
11
+
12
+ /**
13
+ * Get file extension from URI
14
+ */
15
+ export function getFileExtension(uri: string): string | null {
16
+ const match = uri.match(/\.([a-z0-9]+)(?:\?|$)/i);
17
+ return match ? match[1].toLowerCase() : null;
18
+ }
19
+
20
+ /**
21
+ * Get MIME type from file extension
22
+ */
23
+ export function getMimeTypeFromExtension(uri: string): string | null {
24
+ const extension = getFileExtension(uri);
25
+ if (!extension) {
26
+ return null;
27
+ }
28
+ return EXTENSION_TO_MIME_TYPE[extension] || null;
29
+ }
30
+
31
+ /**
32
+ * Get MIME type from data URL
33
+ */
34
+ export function getMimeTypeFromDataUrl(dataUrl: string): string | null {
35
+ const match = dataUrl.match(/^data:([^;]+);/);
36
+ return match ? match[1] : null;
37
+ }
38
+
39
+ /**
40
+ * Validate MIME type is supported image type
41
+ */
42
+ export function validateImageMimeType(
43
+ mimeType: string,
44
+ fieldName: string = 'File'
45
+ ): ValidationResult {
46
+ if (!mimeType) {
47
+ return { isValid: false, error: `${fieldName} MIME type is required` };
48
+ }
49
+
50
+ if (!SUPPORTED_IMAGE_MIME_TYPES.includes(mimeType as any)) {
51
+ return {
52
+ isValid: false,
53
+ error: `${fieldName} must be an image (JPEG, PNG, WEBP, or GIF)`,
54
+ };
55
+ }
56
+
57
+ return { isValid: true };
58
+ }
59
+
60
+ /**
61
+ * Validate file extension is supported image type
62
+ */
63
+ export function validateImageExtension(
64
+ uri: string,
65
+ fieldName: string = 'File'
66
+ ): ValidationResult {
67
+ const mimeType = getMimeTypeFromExtension(uri);
68
+ if (!mimeType) {
69
+ return {
70
+ isValid: false,
71
+ error: `${fieldName} must have a valid image extension (jpg, jpeg, png, webp, gif)`,
72
+ };
73
+ }
74
+
75
+ return validateImageMimeType(mimeType, fieldName);
76
+ }
77
+
78
+ /**
79
+ * Validate data URL is image type
80
+ */
81
+ export function validateImageDataUrl(
82
+ dataUrl: string,
83
+ fieldName: string = 'File'
84
+ ): ValidationResult {
85
+ if (!dataUrl || !dataUrl.startsWith('data:')) {
86
+ return {
87
+ isValid: false,
88
+ error: `${fieldName} must be a valid data URL`,
89
+ };
90
+ }
91
+
92
+ const mimeType = getMimeTypeFromDataUrl(dataUrl);
93
+ if (!mimeType) {
94
+ return {
95
+ isValid: false,
96
+ error: `${fieldName} must have a valid MIME type`,
97
+ };
98
+ }
99
+
100
+ return validateImageMimeType(mimeType, fieldName);
101
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * MIME Type Constants
3
+ * Single Responsibility: Define supported MIME types for validation
4
+ */
5
+
6
+ /**
7
+ * Supported image MIME types
8
+ */
9
+ export const IMAGE_MIME_TYPES = {
10
+ JPEG: 'image/jpeg',
11
+ JPG: 'image/jpeg',
12
+ PNG: 'image/png',
13
+ WEBP: 'image/webp',
14
+ GIF: 'image/gif',
15
+ } as const;
16
+
17
+ /**
18
+ * All supported image MIME types as array
19
+ */
20
+ export const SUPPORTED_IMAGE_MIME_TYPES = Object.values(IMAGE_MIME_TYPES);
21
+
22
+ /**
23
+ * File extension to MIME type mapping
24
+ */
25
+ export const EXTENSION_TO_MIME_TYPE: Record<string, string> = {
26
+ jpg: IMAGE_MIME_TYPES.JPEG,
27
+ jpeg: IMAGE_MIME_TYPES.JPEG,
28
+ png: IMAGE_MIME_TYPES.PNG,
29
+ webp: IMAGE_MIME_TYPES.WEBP,
30
+ gif: IMAGE_MIME_TYPES.GIF,
31
+ };
32
+
33
+ /**
34
+ * MIME type to file extension mapping
35
+ */
36
+ export const MIME_TYPE_TO_EXTENSION: Record<string, string> = {
37
+ [IMAGE_MIME_TYPES.JPEG]: 'jpg',
38
+ [IMAGE_MIME_TYPES.PNG]: 'png',
39
+ [IMAGE_MIME_TYPES.WEBP]: 'webp',
40
+ [IMAGE_MIME_TYPES.GIF]: 'gif',
41
+ };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Presentation - Gallery Header Component
3
+ *
4
+ * High-performance, premium header for the Image Gallery.
5
+ * Uses design system tokens and handles safe areas.
6
+ */
7
+
8
+ import React from 'react';
9
+ import { View, TouchableOpacity, StyleSheet } from 'react-native';
10
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
11
+ import { useAppDesignTokens } from '@umituz/react-native-design-system';
12
+ import { AtomicText } from '@umituz/react-native-design-system';
13
+
14
+ interface GalleryHeaderProps {
15
+ onEdit?: () => void;
16
+ onClose: () => void;
17
+ title?: string;
18
+ }
19
+
20
+ export function GalleryHeader({ onEdit, onClose, title }: GalleryHeaderProps) {
21
+ const insets = useSafeAreaInsets();
22
+ const tokens = useAppDesignTokens();
23
+
24
+ return (
25
+ <View style={[
26
+ styles.container,
27
+ {
28
+ paddingTop: Math.max(insets.top, 24),
29
+ backgroundColor: 'rgba(0, 0, 0, 0.4)'
30
+ }
31
+ ]}>
32
+ <View style={styles.leftSection}>
33
+ {onEdit ? (
34
+ <TouchableOpacity
35
+ style={[styles.actionButton, { backgroundColor: 'rgba(255, 255, 255, 0.15)' }]}
36
+ onPress={onEdit}
37
+ activeOpacity={0.7}
38
+ >
39
+ <AtomicText style={styles.buttonText}>Edit</AtomicText>
40
+ </TouchableOpacity>
41
+ ) : (
42
+ <View style={styles.spacer} />
43
+ )}
44
+ </View>
45
+
46
+ <View style={styles.centerSection}>
47
+ {title ? (
48
+ <AtomicText type="bodyMedium" style={styles.titleText}>
49
+ {title}
50
+ </AtomicText>
51
+ ) : null}
52
+ </View>
53
+
54
+ <View style={styles.rightSection}>
55
+ <TouchableOpacity
56
+ style={[styles.closeButton, { backgroundColor: 'rgba(0, 0, 0, 0.4)' }]}
57
+ onPress={onClose}
58
+ activeOpacity={0.7}
59
+ hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
60
+ >
61
+ <AtomicText style={styles.closeIcon}>✕</AtomicText>
62
+ </TouchableOpacity>
63
+ </View>
64
+ </View>
65
+ );
66
+ }
67
+
68
+ const styles = StyleSheet.create({
69
+ container: {
70
+ position: 'absolute',
71
+ top: 0,
72
+ left: 0,
73
+ right: 0,
74
+ paddingHorizontal: 20,
75
+ paddingBottom: 16,
76
+ flexDirection: 'row',
77
+ justifyContent: 'space-between',
78
+ alignItems: 'center',
79
+ zIndex: 1000,
80
+ },
81
+ leftSection: {
82
+ flex: 1,
83
+ alignItems: 'flex-start',
84
+ },
85
+ centerSection: {
86
+ flex: 2,
87
+ alignItems: 'center',
88
+ },
89
+ rightSection: {
90
+ flex: 1,
91
+ alignItems: 'flex-end',
92
+ },
93
+ spacer: {
94
+ width: 44,
95
+ height: 44,
96
+ },
97
+ actionButton: {
98
+ paddingVertical: 8,
99
+ paddingHorizontal: 16,
100
+ borderRadius: 20,
101
+ justifyContent: 'center',
102
+ alignItems: 'center',
103
+ },
104
+ closeButton: {
105
+ width: 50,
106
+ height: 50,
107
+ borderRadius: 25,
108
+ justifyContent: 'center',
109
+ alignItems: 'center',
110
+ },
111
+ buttonText: {
112
+ fontSize: 14,
113
+ color: '#FFFFFF',
114
+ fontWeight: 'bold',
115
+ },
116
+ closeIcon: {
117
+ fontSize: 28,
118
+ color: '#FFFFFF',
119
+ fontWeight: '300',
120
+ },
121
+ titleText: {
122
+ color: '#FFFFFF',
123
+ fontWeight: '600',
124
+ textAlign: 'center',
125
+ },
126
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Presentation - Image Gallery Component
3
+ *
4
+ * High-performance, premium image gallery using expo-image.
5
+ * Replaces slow standard image components for instant loading.
6
+ */
7
+
8
+ import React, { useCallback, useMemo, useState, useEffect } from 'react';
9
+ import { Modal, View, StyleSheet, FlatList, Dimensions, TouchableOpacity } from 'react-native';
10
+ import { Image } from 'expo-image';
11
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
12
+ import type { ImageViewerItem, ImageGalleryOptions } from '../../domain/entities/ImageTypes';
13
+ import { GalleryHeader } from './GalleryHeader';
14
+
15
+ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
16
+
17
+ export interface ImageGalleryProps extends ImageGalleryOptions {
18
+ images: ImageViewerItem[];
19
+ visible: boolean;
20
+ onDismiss: () => void;
21
+ index?: number;
22
+ onImageChange?: (uri: string, index: number) => void | Promise<void>;
23
+ enableEditing?: boolean;
24
+ title?: string;
25
+ }
26
+
27
+ export const ImageGallery: React.FC<ImageGalleryProps> = ({
28
+ images,
29
+ visible,
30
+ onDismiss,
31
+ index = 0,
32
+ backgroundColor = '#000000',
33
+ onIndexChange,
34
+ onImageChange,
35
+ enableEditing = false,
36
+ title,
37
+ }) => {
38
+ const insets = useSafeAreaInsets();
39
+ const [currentIndex, setCurrentIndex] = useState(index);
40
+
41
+ useEffect(() => {
42
+ if (visible) setCurrentIndex(index);
43
+ }, [visible, index]);
44
+
45
+ const handleEdit = useCallback(async () => {
46
+ const currentImage = images[currentIndex];
47
+ if (!currentImage || !onImageChange) return;
48
+ await onImageChange(currentImage.uri, currentIndex);
49
+ }, [images, currentIndex, onImageChange]);
50
+
51
+ const handleScroll = useCallback((event: any) => {
52
+ const nextIndex = Math.round(event.nativeEvent.contentOffset.x / SCREEN_WIDTH);
53
+ if (nextIndex !== currentIndex) {
54
+ setCurrentIndex(nextIndex);
55
+ onIndexChange?.(nextIndex);
56
+ }
57
+ }, [currentIndex, onIndexChange]);
58
+
59
+ const renderItem = useCallback(({ item }: { item: ImageViewerItem }) => (
60
+ <View style={styles.imageWrapper}>
61
+ <Image
62
+ source={{ uri: item.uri }}
63
+ style={styles.fullImage}
64
+ contentFit="contain"
65
+ transition={200}
66
+ cachePolicy="memory-disk"
67
+ />
68
+ </View>
69
+ ), []);
70
+
71
+ if (!visible && !currentIndex) return null;
72
+
73
+ return (
74
+ <Modal
75
+ visible={visible}
76
+ transparent
77
+ animationType="fade"
78
+ onRequestClose={onDismiss}
79
+ statusBarTranslucent
80
+ >
81
+ <View style={[styles.container, { backgroundColor }]}>
82
+ <GalleryHeader
83
+ onClose={onDismiss}
84
+ onEdit={enableEditing ? handleEdit : undefined}
85
+ title={title || `${currentIndex + 1} / ${images.length}`}
86
+ />
87
+
88
+ <FlatList
89
+ data={images}
90
+ renderItem={renderItem}
91
+ horizontal
92
+ pagingEnabled
93
+ showsHorizontalScrollIndicator={false}
94
+ initialScrollIndex={index}
95
+ getItemLayout={(_, i) => ({
96
+ length: SCREEN_WIDTH,
97
+ offset: SCREEN_WIDTH * i,
98
+ index: i,
99
+ })}
100
+ onScroll={handleScroll}
101
+ scrollEventThrottle={16}
102
+ keyExtractor={(item, i) => `${item.uri}-${i}`}
103
+ style={styles.list}
104
+ />
105
+
106
+ <View style={[styles.footer, { paddingBottom: Math.max(insets.bottom, 20) }]}>
107
+ {/* Potential for thumbnail strip or captions in future */}
108
+ </View>
109
+ </View>
110
+ </Modal>
111
+ );
112
+ };
113
+
114
+ const styles = StyleSheet.create({
115
+ container: {
116
+ flex: 1,
117
+ },
118
+ list: {
119
+ flex: 1,
120
+ },
121
+ imageWrapper: {
122
+ width: SCREEN_WIDTH,
123
+ height: SCREEN_HEIGHT,
124
+ justifyContent: 'center',
125
+ alignItems: 'center',
126
+ },
127
+ fullImage: {
128
+ width: '100%',
129
+ height: '100%',
130
+ },
131
+ footer: {
132
+ position: 'absolute',
133
+ bottom: 0,
134
+ left: 0,
135
+ right: 0,
136
+ alignItems: 'center',
137
+ }
138
+ });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Presentation - Filter Picker Sheet
3
+ */
4
+
5
+ import React, { forwardRef } from 'react';
6
+ import { View, TouchableOpacity, ScrollView } from 'react-native';
7
+ import {
8
+ BottomSheetModal,
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ BottomSheetModalRef
13
+ } from '@umituz/react-native-design-system';
14
+ import { FilterProcessor, type FilterPreset } from '../../../infrastructure/utils/FilterProcessor';
15
+
16
+ export interface FilterPickerSheetProps {
17
+ onSelectFilter: (filterId: string) => void;
18
+ onDismiss: () => void;
19
+ activeFilterId?: string;
20
+ snapPoints?: string[];
21
+ title?: string;
22
+ }
23
+
24
+ export const FilterPickerSheet = forwardRef<BottomSheetModalRef, FilterPickerSheetProps>(
25
+ ({ onSelectFilter, onDismiss, activeFilterId, snapPoints = ['50%'], title = 'Filters' }, ref) => {
26
+ const tokens = useAppDesignTokens();
27
+ const presets = FilterProcessor.getPresets();
28
+
29
+ return (
30
+ <BottomSheetModal ref={ref} snapPoints={snapPoints} onDismiss={onDismiss}>
31
+ <View style={{ padding: tokens.spacing.lg, flex: 1 }}>
32
+ <AtomicText style={{ ...tokens.typography.headingSmall, marginBottom: tokens.spacing.md }}>
33
+ {title}
34
+ </AtomicText>
35
+
36
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
37
+ <View style={{ flexDirection: 'row', gap: tokens.spacing.md }}>
38
+ {presets.map((preset: FilterPreset) => (
39
+ <TouchableOpacity
40
+ key={preset.id}
41
+ onPress={() => onSelectFilter(preset.id)}
42
+ style={{
43
+ width: 100,
44
+ height: 120,
45
+ backgroundColor: activeFilterId === preset.id ? tokens.colors.primaryContainer : tokens.colors.surfaceVariant,
46
+ borderRadius: tokens.radius.lg,
47
+ alignItems: 'center',
48
+ justifyContent: 'center',
49
+ borderWidth: 2,
50
+ borderColor: activeFilterId === preset.id ? tokens.colors.primary : 'transparent',
51
+ }}
52
+ >
53
+ <AtomicIcon
54
+ name="color-filter"
55
+ size={32}
56
+ color={activeFilterId === preset.id ? 'primary' : 'secondary'}
57
+ />
58
+ <AtomicText style={{
59
+ ...tokens.typography.labelSmall,
60
+ marginTop: tokens.spacing.sm,
61
+ color: activeFilterId === preset.id ? tokens.colors.primary : tokens.colors.textSecondary
62
+ }}>
63
+ {preset.name}
64
+ </AtomicText>
65
+ </TouchableOpacity>
66
+ ))}
67
+ </View>
68
+ </ScrollView>
69
+ </View>
70
+ </BottomSheetModal>
71
+ );
72
+ }
73
+ );
74
+
75
+ FilterPickerSheet.displayName = 'FilterPickerSheet';
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Presentation - Sticker Picker Sheet
3
+ */
4
+
5
+ import React, { forwardRef } from 'react';
6
+ import { View, TouchableOpacity, ScrollView, Image, Dimensions } from 'react-native';
7
+ import {
8
+ BottomSheetModal,
9
+ AtomicText,
10
+ useAppDesignTokens,
11
+ BottomSheetModalRef
12
+ } from '@umituz/react-native-design-system';
13
+
14
+ const { width: SCREEN_WIDTH } = Dimensions.get('window');
15
+
16
+ export interface StickerPickerSheetProps {
17
+ stickers: string[];
18
+ onSelectSticker: (uri: string) => void;
19
+ onDismiss: () => void;
20
+ title?: string;
21
+ snapPoints?: string[];
22
+ }
23
+
24
+ export const StickerPickerSheet = forwardRef<BottomSheetModalRef, StickerPickerSheetProps>(
25
+ ({ stickers, onSelectSticker, onDismiss, title = 'Select Sticker', snapPoints = ['60%'] }, ref) => {
26
+ const tokens = useAppDesignTokens();
27
+
28
+ return (
29
+ <BottomSheetModal ref={ref} snapPoints={snapPoints} onDismiss={onDismiss}>
30
+ <View style={{ padding: tokens.spacing.lg, flex: 1 }}>
31
+ <AtomicText style={{ ...tokens.typography.headingSmall, marginBottom: tokens.spacing.md }}>
32
+ {title}
33
+ </AtomicText>
34
+
35
+ <ScrollView showsVerticalScrollIndicator={false}>
36
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: tokens.spacing.md }}>
37
+ {stickers.map((uri, index) => (
38
+ <TouchableOpacity
39
+ key={index}
40
+ onPress={() => onSelectSticker(uri)}
41
+ style={{
42
+ width: (SCREEN_WIDTH - 64) / 3,
43
+ aspectRatio: 1,
44
+ backgroundColor: tokens.colors.surfaceVariant,
45
+ borderRadius: tokens.radius.md,
46
+ padding: tokens.spacing.sm,
47
+ alignItems: 'center',
48
+ justifyContent: 'center',
49
+ }}
50
+ >
51
+ <Image source={{ uri }} style={{ width: '100%', height: '100%' }} resizeMode="contain" />
52
+ </TouchableOpacity>
53
+ ))}
54
+ </View>
55
+ </ScrollView>
56
+ </View>
57
+ </BottomSheetModal>
58
+ );
59
+ }
60
+ );
61
+
62
+ StickerPickerSheet.displayName = 'StickerPickerSheet';
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Presentation - Text Editor Sheet
3
+ */
4
+
5
+ import React, { forwardRef, useState } from 'react';
6
+ import { View, TouchableOpacity, ScrollView } from 'react-native';
7
+ import {
8
+ BottomSheetModal,
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ BottomSheetModalRef
13
+ } from '@umituz/react-native-design-system';
14
+ import { TextContentTab, TextStyleTab, TextTransformTab } from './TextEditorTabs';
15
+
16
+ export interface TextEditorSheetProps {
17
+ text: string;
18
+ onTextChange: (text: string) => void;
19
+ color: string;
20
+ onColorChange: (color: string) => void;
21
+ fontSize: number;
22
+ onFontSizeChange: (size: number) => void;
23
+ fontFamily: string;
24
+ onFontFamilyChange: (font: string) => void;
25
+ scale: number;
26
+ onScaleChange: (scale: number) => void;
27
+ rotation: number;
28
+ onRotationChange: (rotation: number) => void;
29
+ opacity: number;
30
+ onOpacityChange: (opacity: number) => void;
31
+ onDelete?: () => void;
32
+ onDismiss: () => void;
33
+ snapPoints?: string[];
34
+ t: (key: string) => string;
35
+ }
36
+
37
+ export const TextEditorSheet = forwardRef<BottomSheetModalRef, TextEditorSheetProps>((props, ref) => {
38
+ const tokens = useAppDesignTokens();
39
+ const [activeTab, setActiveTab] = useState<'content' | 'style' | 'transform'>('content');
40
+ const { onDismiss, snapPoints = ['75%'], t } = props;
41
+
42
+ const tabs = [
43
+ { id: 'content', label: 'Text', icon: 'text' },
44
+ { id: 'style', label: 'Style', icon: 'color-palette' },
45
+ { id: 'transform', label: 'Edit', icon: 'options' },
46
+ ];
47
+
48
+ return (
49
+ <BottomSheetModal ref={ref} snapPoints={snapPoints} onDismiss={onDismiss}>
50
+ <View style={{ padding: tokens.spacing.lg, flex: 1 }}>
51
+ <View style={{ flexDirection: 'row', gap: tokens.spacing.md, marginBottom: tokens.spacing.lg }}>
52
+ {tabs.map(tab => (
53
+ <TouchableOpacity
54
+ key={tab.id}
55
+ onPress={() => setActiveTab(tab.id as any)}
56
+ style={{
57
+ flex: 1, alignItems: 'center', paddingVertical: tokens.spacing.sm,
58
+ borderBottomWidth: 3, borderBottomColor: activeTab === tab.id ? tokens.colors.primary : 'transparent'
59
+ }}
60
+ >
61
+ <AtomicIcon name={tab.icon as any} size={20} color={activeTab === tab.id ? 'primary' : 'secondary'} />
62
+ <AtomicText style={{
63
+ ...tokens.typography.labelSmall,
64
+ color: activeTab === tab.id ? tokens.colors.primary : tokens.colors.textSecondary,
65
+ marginTop: 4
66
+ }}>
67
+ {tab.label}
68
+ </AtomicText>
69
+ </TouchableOpacity>
70
+ ))}
71
+ </View>
72
+
73
+ <ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 60 }}>
74
+ {activeTab === 'content' && <TextContentTab text={props.text} onTextChange={props.onTextChange} t={t} />}
75
+ {activeTab === 'style' && (
76
+ <TextStyleTab
77
+ fontSize={props.fontSize} setFontSize={props.onFontSizeChange}
78
+ color={props.color} setColor={props.onColorChange}
79
+ fontFamily={props.fontFamily} setFontFamily={props.onFontFamilyChange}
80
+ t={t}
81
+ />
82
+ )}
83
+ {activeTab === 'transform' && (
84
+ <TextTransformTab
85
+ scale={props.scale} setScale={props.onScaleChange}
86
+ rotation={props.rotation} setRotation={props.onRotationChange}
87
+ opacity={props.opacity} setOpacity={props.onOpacityChange}
88
+ onDelete={props.onDelete}
89
+ t={t}
90
+ />
91
+ )}
92
+ </ScrollView>
93
+ </View>
94
+ </BottomSheetModal>
95
+ );
96
+ });
97
+
98
+ TextEditorSheet.displayName = 'TextEditorSheet';