@umituz/react-native-design-system 4.23.83 → 4.23.84

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 (23) hide show
  1. package/package.json +1 -1
  2. package/src/atoms/input/hooks/useInputState.ts +2 -4
  3. package/src/image/presentation/components/editor/text-editor/TextTransformTab.tsx +40 -152
  4. package/src/image/presentation/components/editor/text-editor/components/TransformButtonRow.tsx +124 -0
  5. package/src/media/domain/utils/FileValidator.ts +156 -0
  6. package/src/media/infrastructure/services/MediaPickerService.ts +18 -57
  7. package/src/media/infrastructure/utils/PermissionManager.ts +92 -0
  8. package/src/media/presentation/hooks/useMedia.ts +5 -4
  9. package/src/molecules/alerts/AlertBanner.tsx +9 -25
  10. package/src/molecules/alerts/AlertInline.tsx +4 -23
  11. package/src/molecules/alerts/AlertModal.tsx +4 -11
  12. package/src/molecules/alerts/AlertToast.tsx +14 -13
  13. package/src/molecules/alerts/utils/alertUtils.ts +133 -0
  14. package/src/molecules/calendar/infrastructure/storage/CalendarStore.ts +65 -25
  15. package/src/molecules/countdown/hooks/useCountdown.ts +13 -5
  16. package/src/molecules/swipe-actions/domain/entities/SwipeAction.ts +15 -123
  17. package/src/molecules/swipe-actions/domain/utils/swipeActionHelpers.ts +109 -0
  18. package/src/molecules/swipe-actions/domain/utils/swipeActionValidator.ts +54 -0
  19. package/src/molecules/swipe-actions/presentation/components/SwipeActionButton.tsx +10 -5
  20. package/src/tanstack/domain/utils/MetricsCalculator.ts +103 -0
  21. package/src/tanstack/infrastructure/monitoring/DevMonitor.ts +35 -29
  22. package/src/timezone/infrastructure/utils/SimpleCache.ts +24 -2
  23. package/src/molecules/alerts/utils/alertToastHelpers.ts +0 -70
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-design-system",
3
- "version": "4.23.83",
3
+ "version": "4.23.84",
4
4
  "description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive, safe area, exception, infinite scroll, UUID, image, timezone, offline, onboarding, and loading utilities",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -24,12 +24,10 @@ export const useInputState = ({
24
24
  value = '',
25
25
  onChangeText,
26
26
  secureTextEntry = false,
27
- showPasswordToggle = false,
27
+ showPasswordToggle: _showPasswordToggle = false,
28
28
  maxLength,
29
- showCharacterCount = false,
29
+ showCharacterCount: _showCharacterCount = false,
30
30
  }: UseInputStateProps = {}): UseInputStateReturn => {
31
- void showPasswordToggle;
32
- void showCharacterCount;
33
31
  const [localValue, setLocalValue] = useState(value);
34
32
  const [isFocused, setIsFocused] = useState(false);
35
33
  const [isPasswordVisible, setIsPasswordVisible] = useState(!secureTextEntry);
@@ -3,10 +3,10 @@
3
3
  */
4
4
 
5
5
  import React from "react";
6
- import { View, ScrollView, TouchableOpacity } from "react-native";
7
- import { AtomicText, AtomicIcon } from "../../../../../atoms";
6
+ import { View } from "react-native";
8
7
  import { useAppDesignTokens } from "../../../../../theme/hooks/useAppDesignTokens";
9
- import { textEditorStyles, type TabProps } from "./TextEditorTabs.styles";
8
+ import type { TabProps } from "./TextEditorTabs.styles";
9
+ import { TransformButtonRow, DeleteButton } from "./components/TransformButtonRow";
10
10
 
11
11
  const DEFAULT_SCALES = [0.5, 0.75, 1, 1.25, 1.5, 2];
12
12
  const DEFAULT_ROTATIONS = [0, 45, 90, 135, 180, 225, 270, 315];
@@ -33,160 +33,48 @@ export const TextTransformTab: React.FC<TextTransformTabProps> = ({
33
33
  }) => {
34
34
  const tokens = useAppDesignTokens();
35
35
 
36
+ const scaleButtons = DEFAULT_SCALES.map((s) => ({
37
+ value: s,
38
+ label: s.toFixed(1) + "x",
39
+ }));
40
+
41
+ const rotationButtons = DEFAULT_ROTATIONS.map((r) => ({
42
+ value: r,
43
+ label: r + "°",
44
+ }));
45
+
46
+ const opacityButtons = DEFAULT_OPACITIES.map((o) => ({
47
+ value: o,
48
+ label: Math.round(o * 100) + "%",
49
+ }));
50
+
36
51
  return (
37
52
  <View style={{ gap: tokens.spacing.xl }}>
38
- {/* Scale Selection */}
39
- <View>
40
- <AtomicText
41
- style={{
42
- ...tokens.typography.labelMedium,
43
- marginBottom: tokens.spacing.xs,
44
- }}
45
- >
46
- Scale: {scale.toFixed(2)}x
47
- </AtomicText>
48
- <ScrollView
49
- horizontal
50
- showsHorizontalScrollIndicator={false}
51
- contentContainerStyle={{ gap: tokens.spacing.sm }}
52
- >
53
- {DEFAULT_SCALES.map((s) => (
54
- <TouchableOpacity
55
- key={s}
56
- onPress={() => setScale(s)}
57
- style={[
58
- textEditorStyles.transformButton,
59
- {
60
- paddingHorizontal: tokens.spacing.md,
61
- paddingVertical: tokens.spacing.sm,
62
- borderRadius: tokens.borders.radius.md,
63
- borderWidth: 1,
64
- borderColor: scale === s ? tokens.colors.primary : tokens.colors.border,
65
- backgroundColor: scale === s ? tokens.colors.primary : tokens.colors.surface,
66
- },
67
- ]}
68
- >
69
- <AtomicText
70
- style={{ color: scale === s ? "white" : tokens.colors.textPrimary }}
71
- >
72
- {s.toFixed(1)}x
73
- </AtomicText>
74
- </TouchableOpacity>
75
- ))}
76
- </ScrollView>
77
- </View>
53
+ <TransformButtonRow
54
+ title="Scale"
55
+ buttons={scaleButtons}
56
+ selectedValue={scale}
57
+ onSelect={setScale}
58
+ formatTitle={(v) => v.toFixed(2) + "x"}
59
+ />
78
60
 
79
- {/* Rotation Selection */}
80
- <View>
81
- <AtomicText
82
- style={{
83
- ...tokens.typography.labelMedium,
84
- marginBottom: tokens.spacing.xs,
85
- }}
86
- >
87
- Rotation: {Math.round(rotation)}°
88
- </AtomicText>
89
- <ScrollView
90
- horizontal
91
- showsHorizontalScrollIndicator={false}
92
- contentContainerStyle={{ gap: tokens.spacing.sm }}
93
- >
94
- {DEFAULT_ROTATIONS.map((r) => (
95
- <TouchableOpacity
96
- key={r}
97
- onPress={() => setRotation(r)}
98
- style={[
99
- textEditorStyles.transformButton,
100
- {
101
- paddingHorizontal: tokens.spacing.md,
102
- paddingVertical: tokens.spacing.sm,
103
- borderRadius: tokens.borders.radius.md,
104
- borderWidth: 1,
105
- borderColor: rotation === r ? tokens.colors.primary : tokens.colors.border,
106
- backgroundColor: rotation === r ? tokens.colors.primary : tokens.colors.surface,
107
- },
108
- ]}
109
- >
110
- <AtomicText
111
- style={{ color: rotation === r ? "white" : tokens.colors.textPrimary }}
112
- >
113
- {r}°
114
- </AtomicText>
115
- </TouchableOpacity>
116
- ))}
117
- </ScrollView>
118
- </View>
61
+ <TransformButtonRow
62
+ title="Rotation"
63
+ buttons={rotationButtons}
64
+ selectedValue={rotation}
65
+ onSelect={setRotation}
66
+ formatTitle={(v) => Math.round(v) + "°"}
67
+ />
119
68
 
120
- {/* Opacity Selection */}
121
- <View>
122
- <AtomicText
123
- style={{
124
- ...tokens.typography.labelMedium,
125
- marginBottom: tokens.spacing.xs,
126
- }}
127
- >
128
- Opacity: {(opacity * 100).toFixed(0)}%
129
- </AtomicText>
130
- <ScrollView
131
- horizontal
132
- showsHorizontalScrollIndicator={false}
133
- contentContainerStyle={{ gap: tokens.spacing.sm }}
134
- >
135
- {DEFAULT_OPACITIES.map((o) => (
136
- <TouchableOpacity
137
- key={o}
138
- onPress={() => setOpacity(o)}
139
- style={[
140
- textEditorStyles.transformButton,
141
- {
142
- paddingHorizontal: tokens.spacing.md,
143
- paddingVertical: tokens.spacing.sm,
144
- borderRadius: tokens.borders.radius.md,
145
- borderWidth: 1,
146
- borderColor: opacity === o ? tokens.colors.primary : tokens.colors.border,
147
- backgroundColor: opacity === o ? tokens.colors.primary : tokens.colors.surface,
148
- },
149
- ]}
150
- >
151
- <AtomicText
152
- style={{ color: opacity === o ? "white" : tokens.colors.textPrimary }}
153
- >
154
- {Math.round(o * 100)}%
155
- </AtomicText>
156
- </TouchableOpacity>
157
- ))}
158
- </ScrollView>
159
- </View>
69
+ <TransformButtonRow
70
+ title="Opacity"
71
+ buttons={opacityButtons}
72
+ selectedValue={opacity}
73
+ onSelect={setOpacity}
74
+ formatTitle={(v) => (v * 100).toFixed(0) + "%"}
75
+ />
160
76
 
161
- {/* Delete Button */}
162
- {onDelete && (
163
- <TouchableOpacity
164
- onPress={onDelete}
165
- style={[
166
- textEditorStyles.deleteButton,
167
- {
168
- flexDirection: "row",
169
- alignItems: "center",
170
- justifyContent: "center",
171
- gap: tokens.spacing.sm,
172
- padding: tokens.spacing.md,
173
- borderRadius: tokens.borders.radius.md,
174
- borderWidth: 1,
175
- borderColor: tokens.colors.error,
176
- },
177
- ]}
178
- >
179
- <AtomicIcon name="trash" size={20} color="error" />
180
- <AtomicText
181
- style={{
182
- ...tokens.typography.labelMedium,
183
- color: tokens.colors.error,
184
- }}
185
- >
186
- Delete Layer
187
- </AtomicText>
188
- </TouchableOpacity>
189
- )}
77
+ {onDelete && <DeleteButton onPress={onDelete} />}
190
78
  </View>
191
79
  );
192
80
  };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Transform Button Row Component
3
+ *
4
+ * Reusable horizontal scrollable row of transform buttons.
5
+ */
6
+
7
+ import React from "react";
8
+ import { ScrollView, TouchableOpacity, View, type StyleProp, type ViewStyle } from "react-native";
9
+ import { AtomicText, AtomicIcon } from "../../../../../../atoms";
10
+ import { useAppDesignTokens } from "../../../../../../theme/hooks/useAppDesignTokens";
11
+ import { textEditorStyles } from "../TextEditorTabs.styles";
12
+
13
+ interface TransformButton {
14
+ value: number;
15
+ label: string;
16
+ }
17
+
18
+ interface TransformButtonRowProps {
19
+ title: string;
20
+ buttons: TransformButton[];
21
+ selectedValue: number;
22
+ onSelect: (value: number) => void;
23
+ formatLabel?: (value: number) => string;
24
+ formatTitle?: (selectedValue: number) => string;
25
+ style?: StyleProp<ViewStyle>;
26
+ }
27
+
28
+ export const TransformButtonRow: React.FC<TransformButtonRowProps> = ({
29
+ title,
30
+ buttons,
31
+ selectedValue,
32
+ onSelect,
33
+ formatLabel = (v) => v.toString(),
34
+ formatTitle = (v) => v.toString(),
35
+ style,
36
+ }) => {
37
+ const tokens = useAppDesignTokens();
38
+
39
+ return (
40
+ <View style={style}>
41
+ <AtomicText
42
+ style={{
43
+ ...tokens.typography.labelMedium,
44
+ marginBottom: tokens.spacing.xs,
45
+ }}
46
+ >
47
+ {title}: {formatTitle(selectedValue)}
48
+ </AtomicText>
49
+ <ScrollView
50
+ horizontal
51
+ showsHorizontalScrollIndicator={false}
52
+ contentContainerStyle={{ gap: tokens.spacing.sm }}
53
+ >
54
+ {buttons.map((button) => {
55
+ const isSelected = selectedValue === button.value;
56
+ return (
57
+ <TouchableOpacity
58
+ key={button.value}
59
+ onPress={() => onSelect(button.value)}
60
+ style={[
61
+ textEditorStyles.transformButton,
62
+ {
63
+ paddingHorizontal: tokens.spacing.md,
64
+ paddingVertical: tokens.spacing.sm,
65
+ borderRadius: tokens.borders.radius.md,
66
+ borderWidth: 1,
67
+ borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
68
+ backgroundColor: isSelected ? tokens.colors.primary : tokens.colors.surface,
69
+ },
70
+ ]}
71
+ >
72
+ <AtomicText
73
+ style={{ color: isSelected ? "white" : tokens.colors.textPrimary }}
74
+ >
75
+ {formatLabel(button.value)}
76
+ </AtomicText>
77
+ </TouchableOpacity>
78
+ );
79
+ })}
80
+ </ScrollView>
81
+ </View>
82
+ );
83
+ };
84
+
85
+ interface DeleteButtonProps {
86
+ onPress: () => void;
87
+ label?: string;
88
+ }
89
+
90
+ export const DeleteButton: React.FC<DeleteButtonProps> = ({
91
+ onPress,
92
+ label = "Delete Layer",
93
+ }) => {
94
+ const tokens = useAppDesignTokens();
95
+
96
+ return (
97
+ <TouchableOpacity
98
+ onPress={onPress}
99
+ style={[
100
+ textEditorStyles.deleteButton,
101
+ {
102
+ flexDirection: "row",
103
+ alignItems: "center",
104
+ justifyContent: "center",
105
+ gap: tokens.spacing.sm,
106
+ padding: tokens.spacing.md,
107
+ borderRadius: tokens.borders.radius.md,
108
+ borderWidth: 1,
109
+ borderColor: tokens.colors.error,
110
+ },
111
+ ]}
112
+ >
113
+ <AtomicIcon name="trash" size={20} color="error" />
114
+ <AtomicText
115
+ style={{
116
+ ...tokens.typography.labelMedium,
117
+ color: tokens.colors.error,
118
+ }}
119
+ >
120
+ {label}
121
+ </AtomicText>
122
+ </TouchableOpacity>
123
+ );
124
+ };
@@ -0,0 +1,156 @@
1
+ /**
2
+ * File Validator
3
+ *
4
+ * File size and format validation utilities for media operations.
5
+ */
6
+
7
+ import type { MediaAsset } from "../entities/Media";
8
+ import { MediaValidationError, MEDIA_CONSTANTS } from "../entities/Media";
9
+
10
+ /**
11
+ * File validation options
12
+ */
13
+ export interface FileValidationOptions {
14
+ maxFileSizeMB?: number;
15
+ allowedFormats?: string[];
16
+ }
17
+
18
+ /**
19
+ * File validation result
20
+ */
21
+ export interface FileValidationResult {
22
+ valid: boolean;
23
+ error?: MediaValidationError;
24
+ errorMessage?: string;
25
+ }
26
+
27
+ /**
28
+ * File validator for media operations
29
+ */
30
+ export class FileValidator {
31
+ /**
32
+ * Validates file size against maximum limit
33
+ *
34
+ * @param fileSize - File size in bytes
35
+ * @param maxSizeMB - Maximum file size in megabytes
36
+ * @returns Validation result
37
+ */
38
+ static validateFileSize(
39
+ fileSize: number,
40
+ maxSizeMB: number = MEDIA_CONSTANTS.MAX_IMAGE_SIZE_MB
41
+ ): FileValidationResult {
42
+ const maxSizeBytes = maxSizeMB * 1024 * 1024;
43
+
44
+ if (fileSize > maxSizeBytes) {
45
+ return {
46
+ valid: false,
47
+ error: MediaValidationError.FILE_TOO_LARGE,
48
+ errorMessage: `File size exceeds ${maxSizeMB}MB limit`,
49
+ };
50
+ }
51
+
52
+ return { valid: true };
53
+ }
54
+
55
+ /**
56
+ * Validates file format against allowed formats
57
+ *
58
+ * @param fileName - File name to check
59
+ * @param allowedFormats - Array of allowed file extensions
60
+ * @returns Validation result
61
+ */
62
+ static validateFileFormat(
63
+ fileName: string,
64
+ allowedFormats?: string[]
65
+ ): FileValidationResult {
66
+ const formats = allowedFormats ?? [...MEDIA_CONSTANTS.SUPPORTED_IMAGE_FORMATS];
67
+ const fileExtension = fileName
68
+ .substring(fileName.lastIndexOf("."))
69
+ .toLowerCase();
70
+
71
+ if (!formats.includes(fileExtension)) {
72
+ return {
73
+ valid: false,
74
+ error: MediaValidationError.INVALID_FORMAT,
75
+ errorMessage: `File format ${fileExtension} is not supported`,
76
+ };
77
+ }
78
+
79
+ return { valid: true };
80
+ }
81
+
82
+ /**
83
+ * Validates a media asset
84
+ *
85
+ * @param asset - Media asset to validate
86
+ * @param options - Validation options
87
+ * @returns Validation result
88
+ */
89
+ static validateAsset(
90
+ asset: MediaAsset,
91
+ options: FileValidationOptions = {}
92
+ ): FileValidationResult {
93
+ // Validate file size if present
94
+ if (asset.fileSize !== undefined) {
95
+ const maxSizeMB = options.maxFileSizeMB ?? MEDIA_CONSTANTS.MAX_IMAGE_SIZE_MB;
96
+ const sizeValidation = this.validateFileSize(asset.fileSize, maxSizeMB);
97
+ if (!sizeValidation.valid) {
98
+ return sizeValidation;
99
+ }
100
+ }
101
+
102
+ // Validate file format if file name is present
103
+ if (asset.fileName && options.allowedFormats) {
104
+ const formatValidation = this.validateFileFormat(
105
+ asset.fileName,
106
+ options.allowedFormats
107
+ );
108
+ if (!formatValidation.valid) {
109
+ return formatValidation;
110
+ }
111
+ }
112
+
113
+ return { valid: true };
114
+ }
115
+
116
+ /**
117
+ * Validates multiple media assets
118
+ *
119
+ * @param assets - Array of media assets to validate
120
+ * @param options - Validation options
121
+ * @returns First validation error found, or { valid: true } if all pass
122
+ */
123
+ static validateAssets(
124
+ assets: MediaAsset[],
125
+ options: FileValidationOptions = {}
126
+ ): FileValidationResult {
127
+ for (const asset of assets) {
128
+ const validation = this.validateAsset(asset, options);
129
+ if (!validation.valid) {
130
+ return validation;
131
+ }
132
+ }
133
+
134
+ return { valid: true };
135
+ }
136
+
137
+ /**
138
+ * Formats file size for display
139
+ *
140
+ * @param fileSize - File size in bytes
141
+ * @returns Formatted file size string (e.g., "1.5 MB")
142
+ */
143
+ static formatFileSize(fileSize: number): string {
144
+ const bytes = fileSize;
145
+ const kilobytes = bytes / 1024;
146
+ const megabytes = kilobytes / 1024;
147
+
148
+ if (megabytes >= 1) {
149
+ return `${megabytes.toFixed(1)} MB`;
150
+ } else if (kilobytes >= 1) {
151
+ return `${kilobytes.toFixed(0)} KB`;
152
+ } else {
153
+ return `${bytes} B`;
154
+ }
155
+ }
156
+ }
@@ -12,64 +12,27 @@ import type {
12
12
  CameraOptions,
13
13
  } from "../../domain/entities/Media";
14
14
  import {
15
- MediaLibraryPermission,
16
15
  MediaType,
17
16
  MediaValidationError,
18
17
  MEDIA_CONSTANTS,
19
18
  } from "../../domain/entities/Media";
20
19
  import {
21
20
  mapMediaType,
22
- mapPermissionStatus,
23
21
  mapPickerResult,
24
22
  } from "../utils/mediaPickerMappers";
23
+ import { PermissionManager } from "../utils/PermissionManager";
24
+ import { FileValidator } from "../../domain/utils/FileValidator";
25
25
 
26
26
  /**
27
27
  * Media picker service for selecting images/videos
28
28
  */
29
29
  export class MediaPickerService {
30
- static async requestCameraPermission(): Promise<MediaLibraryPermission> {
31
- try {
32
- const { status } = await ImagePicker.requestCameraPermissionsAsync();
33
- return mapPermissionStatus(status);
34
- } catch {
35
- return MediaLibraryPermission.DENIED;
36
- }
37
- }
38
-
39
- static async requestMediaLibraryPermission(): Promise<MediaLibraryPermission> {
40
- try {
41
- const { status } =
42
- await ImagePicker.requestMediaLibraryPermissionsAsync();
43
- return mapPermissionStatus(status);
44
- } catch {
45
- return MediaLibraryPermission.DENIED;
46
- }
47
- }
48
-
49
- static async getCameraPermissionStatus(): Promise<MediaLibraryPermission> {
50
- try {
51
- const { status } = await ImagePicker.getCameraPermissionsAsync();
52
- return mapPermissionStatus(status);
53
- } catch {
54
- return MediaLibraryPermission.DENIED;
55
- }
56
- }
57
-
58
- static async getMediaLibraryPermissionStatus(): Promise<MediaLibraryPermission> {
59
- try {
60
- const { status } = await ImagePicker.getMediaLibraryPermissionsAsync();
61
- return mapPermissionStatus(status);
62
- } catch {
63
- return MediaLibraryPermission.DENIED;
64
- }
65
- }
66
-
67
30
  static async launchCamera(
68
31
  options?: CameraOptions
69
32
  ): Promise<MediaPickerResult> {
70
33
  try {
71
- const permission = await MediaPickerService.requestCameraPermission();
72
- if (permission === MediaLibraryPermission.DENIED) {
34
+ const permission = await PermissionManager.requestCameraPermission();
35
+ if (!PermissionManager.isPermissionGranted(permission)) {
73
36
  return { canceled: true };
74
37
  }
75
38
 
@@ -91,8 +54,8 @@ export class MediaPickerService {
91
54
  options?: CameraOptions
92
55
  ): Promise<MediaPickerResult> {
93
56
  try {
94
- const permission = await MediaPickerService.requestCameraPermission();
95
- if (permission === MediaLibraryPermission.DENIED) {
57
+ const permission = await PermissionManager.requestCameraPermission();
58
+ if (!PermissionManager.isPermissionGranted(permission)) {
96
59
  return { canceled: true };
97
60
  }
98
61
 
@@ -113,9 +76,8 @@ export class MediaPickerService {
113
76
  options?: MediaPickerOptions
114
77
  ): Promise<MediaPickerResult> {
115
78
  try {
116
- const permission =
117
- await MediaPickerService.requestMediaLibraryPermission();
118
- if (permission === MediaLibraryPermission.DENIED) {
79
+ const permission = await PermissionManager.requestMediaLibraryPermission();
80
+ if (!PermissionManager.isPermissionGranted(permission)) {
119
81
  return {
120
82
  canceled: true,
121
83
  error: MediaValidationError.PERMISSION_DENIED,
@@ -138,17 +100,16 @@ export class MediaPickerService {
138
100
 
139
101
  // Validate file size if not canceled and has assets
140
102
  if (!mappedResult.canceled && mappedResult.assets && mappedResult.assets.length > 0) {
141
- const maxSizeMB = options?.maxFileSizeMB ?? MEDIA_CONSTANTS.MAX_IMAGE_SIZE_MB;
142
- const maxSizeBytes = maxSizeMB * 1024 * 1024;
143
-
144
- for (const asset of mappedResult.assets) {
145
- if (asset.fileSize && asset.fileSize > maxSizeBytes) {
146
- return {
147
- canceled: true,
148
- error: MediaValidationError.FILE_TOO_LARGE,
149
- errorMessage: `File size exceeds ${maxSizeMB}MB limit`,
150
- };
151
- }
103
+ const validation = FileValidator.validateAssets(mappedResult.assets, {
104
+ maxFileSizeMB: options?.maxFileSizeMB,
105
+ });
106
+
107
+ if (!validation.valid) {
108
+ return {
109
+ canceled: true,
110
+ error: validation.error,
111
+ errorMessage: validation.errorMessage,
112
+ };
152
113
  }
153
114
  }
154
115