@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.
- package/package.json +7 -3
- package/src/atoms/image/AtomicImage.tsx +29 -0
- package/src/atoms/index.ts +3 -0
- package/src/exports/atoms.ts +2 -0
- package/src/exports/image.ts +7 -0
- package/src/image/domain/entities/EditorTypes.ts +23 -0
- package/src/image/domain/entities/ImageConstants.ts +38 -0
- package/src/image/domain/entities/ImageFilterTypes.ts +70 -0
- package/src/image/domain/entities/ImageTemplateTypes.ts +18 -0
- package/src/image/domain/entities/ImageTypes.ts +86 -0
- package/src/image/domain/entities/ValidationResult.ts +15 -0
- package/src/image/domain/entities/editor/EditorConfigTypes.ts +35 -0
- package/src/image/domain/entities/editor/EditorElementTypes.ts +60 -0
- package/src/image/domain/entities/editor/EditorFilterTypes.ts +9 -0
- package/src/image/domain/entities/editor/EditorLayerTypes.ts +34 -0
- package/src/image/domain/entities/editor/EditorStateTypes.ts +35 -0
- package/src/image/domain/entities/editor/EditorToolTypes.ts +33 -0
- package/src/image/domain/utils/ImageUtils.ts +103 -0
- package/src/image/index.ts +123 -0
- package/src/image/infrastructure/services/ImageBatchService.ts +110 -0
- package/src/image/infrastructure/services/ImageConversionService.ts +74 -0
- package/src/image/infrastructure/services/ImageEditorService.ts +136 -0
- package/src/image/infrastructure/services/ImageEnhanceService.ts +123 -0
- package/src/image/infrastructure/services/ImageMetadataService.ts +116 -0
- package/src/image/infrastructure/services/ImageStorageService.ts +37 -0
- package/src/image/infrastructure/services/ImageTemplateService.ts +66 -0
- package/src/image/infrastructure/services/ImageTransformService.ts +89 -0
- package/src/image/infrastructure/services/ImageViewerService.ts +64 -0
- package/src/image/infrastructure/utils/BatchProcessor.ts +95 -0
- package/src/image/infrastructure/utils/FilterProcessor.ts +124 -0
- package/src/image/infrastructure/utils/ImageAnalysisUtils.ts +122 -0
- package/src/image/infrastructure/utils/ImageEditorHistoryUtils.ts +63 -0
- package/src/image/infrastructure/utils/ImageErrorHandler.ts +40 -0
- package/src/image/infrastructure/utils/ImageFilterUtils.ts +21 -0
- package/src/image/infrastructure/utils/ImageQualityPresets.ts +110 -0
- package/src/image/infrastructure/utils/ImageTransformUtils.ts +25 -0
- package/src/image/infrastructure/utils/ImageValidator.ts +59 -0
- package/src/image/infrastructure/utils/LayerManager.ts +77 -0
- package/src/image/infrastructure/utils/MetadataExtractor.ts +83 -0
- package/src/image/infrastructure/utils/filters/BasicFilters.ts +61 -0
- package/src/image/infrastructure/utils/filters/FilterHelpers.ts +21 -0
- package/src/image/infrastructure/utils/filters/SpecialFilters.ts +84 -0
- package/src/image/infrastructure/utils/validation/image-validator.ts +77 -0
- package/src/image/infrastructure/utils/validation/mime-type-validator.ts +101 -0
- package/src/image/infrastructure/utils/validation/mime-types.constants.ts +41 -0
- package/src/image/presentation/components/GalleryHeader.tsx +126 -0
- package/src/image/presentation/components/ImageGallery.tsx +138 -0
- package/src/image/presentation/components/editor/FilterPickerSheet.tsx +75 -0
- package/src/image/presentation/components/editor/StickerPickerSheet.tsx +62 -0
- package/src/image/presentation/components/editor/TextEditorSheet.tsx +98 -0
- package/src/image/presentation/components/editor/TextEditorTabs.tsx +111 -0
- package/src/image/presentation/components/image/AtomicImage.tsx +29 -0
- package/src/image/presentation/hooks/useImage.ts +39 -0
- package/src/image/presentation/hooks/useImageBatch.ts +28 -0
- package/src/image/presentation/hooks/useImageConversion.ts +29 -0
- package/src/image/presentation/hooks/useImageEnhance.ts +32 -0
- package/src/image/presentation/hooks/useImageGallery.ts +90 -0
- package/src/image/presentation/hooks/useImageMetadata.ts +28 -0
- package/src/image/presentation/hooks/useImageOperation.ts +37 -0
- package/src/image/presentation/hooks/useImageTransform.ts +42 -0
- 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';
|