@umituz/react-native-design-system 4.23.113 → 4.23.115
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 +1 -1
- package/src/atoms/AtomicTouchable.tsx +22 -0
- package/src/atoms/badge/AtomicBadge.tsx +26 -28
- package/src/atoms/chip/AtomicChip.tsx +5 -5
- package/src/atoms/datepicker/components/DatePickerModal.tsx +4 -3
- package/src/atoms/input/hooks/useInputState.ts +1 -1
- package/src/atoms/picker/components/PickerModal.tsx +1 -1
- package/src/atoms/picker/hooks/usePickerState.ts +28 -15
- package/src/atoms/skeleton/AtomicSkeleton.tsx +5 -5
- package/src/device/infrastructure/services/DeviceCapabilityService.ts +1 -12
- package/src/filesystem/infrastructure/services/directory.service.ts +37 -9
- package/src/filesystem/infrastructure/services/download.service.ts +62 -11
- package/src/filesystem/infrastructure/services/file-manager.service.ts +42 -11
- package/src/filesystem/infrastructure/services/file-writer.service.ts +8 -3
- package/src/media/infrastructure/services/MediaPickerService.ts +32 -8
- package/src/media/infrastructure/services/MediaSaveService.ts +7 -2
- package/src/media/presentation/hooks/useMedia.ts +60 -22
- package/src/molecules/BaseModal.tsx +1 -0
- package/src/molecules/ConfirmationModalMain.tsx +1 -0
- package/src/molecules/ListItem.tsx +15 -1
- package/src/molecules/avatar/Avatar.tsx +28 -11
- package/src/molecules/bottom-sheet/components/BottomSheet.tsx +1 -0
- package/src/molecules/calendar/presentation/components/AtomicCalendar.tsx +1 -1
- package/src/responsive/useResponsive.ts +1 -1
- package/src/services/api/ApiClient.ts +37 -6
- package/src/storage/presentation/hooks/usePersistentCache.ts +20 -12
- package/src/storage/presentation/hooks/useStore.ts +1 -0
- package/src/tanstack/presentation/hooks/usePrefetch.ts +14 -0
- package/src/theme/infrastructure/stores/themeStore.ts +13 -11
- package/src/timezone/infrastructure/services/BusinessCalendarManager.ts +1 -0
- package/src/timezone/infrastructure/services/CalendarManager.ts +2 -2
- package/src/timezone/infrastructure/services/DateComparisonUtils.ts +1 -0
- package/src/timezone/infrastructure/services/DateFormatter.ts +3 -2
- package/src/timezone/infrastructure/services/DateRangeUtils.ts +1 -0
- package/src/timezone/infrastructure/utils/TimezoneParsers.ts +27 -0
- package/src/utilities/sharing/presentation/hooks/useSharing.ts +44 -17
- package/src/utils/async/index.ts +12 -0
- package/src/utils/async/retryWithBackoff.ts +177 -0
- package/src/utils/errors/DesignSystemError.ts +117 -0
- package/src/utils/errors/ErrorHandler.ts +137 -0
- package/src/utils/errors/index.ts +7 -0
|
@@ -20,6 +20,8 @@ interface ThemeState {
|
|
|
20
20
|
defaultThemeMode: ThemeMode;
|
|
21
21
|
isDark: boolean;
|
|
22
22
|
isInitialized: boolean;
|
|
23
|
+
_updateInProgress: boolean;
|
|
24
|
+
_initInProgress: boolean;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
interface ThemeActions {
|
|
@@ -32,9 +34,6 @@ interface ThemeActions {
|
|
|
32
34
|
initialize: () => Promise<void>;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
let themeUpdateInProgress = false;
|
|
36
|
-
let themeInitInProgress = false;
|
|
37
|
-
|
|
38
37
|
export const useTheme = createStore<ThemeState, ThemeActions>({
|
|
39
38
|
name: 'theme-store',
|
|
40
39
|
initialState: {
|
|
@@ -45,14 +44,16 @@ export const useTheme = createStore<ThemeState, ThemeActions>({
|
|
|
45
44
|
defaultThemeMode: 'dark',
|
|
46
45
|
isDark: true,
|
|
47
46
|
isInitialized: false,
|
|
47
|
+
_updateInProgress: false,
|
|
48
|
+
_initInProgress: false,
|
|
48
49
|
},
|
|
49
50
|
persist: false,
|
|
50
51
|
actions: (set, get) => ({
|
|
51
52
|
initialize: async () => {
|
|
52
|
-
const { isInitialized, customColors: currentColors, defaultThemeMode } = get();
|
|
53
|
-
if (isInitialized ||
|
|
53
|
+
const { isInitialized, _initInProgress, customColors: currentColors, defaultThemeMode } = get();
|
|
54
|
+
if (isInitialized || _initInProgress) return;
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
set({ _initInProgress: true });
|
|
56
57
|
|
|
57
58
|
try {
|
|
58
59
|
const [savedMode, savedColors] = await Promise.all([
|
|
@@ -77,16 +78,17 @@ export const useTheme = createStore<ThemeState, ThemeActions>({
|
|
|
77
78
|
dsTheme.setThemeMode(mode);
|
|
78
79
|
dsTheme.setCustomColors(colors);
|
|
79
80
|
} catch {
|
|
80
|
-
set({ isInitialized: true });
|
|
81
|
+
set({ isInitialized: true, _initInProgress: false });
|
|
81
82
|
useDesignSystemTheme.getState().setThemeMode(defaultThemeMode);
|
|
82
83
|
} finally {
|
|
83
|
-
|
|
84
|
+
set({ _initInProgress: false });
|
|
84
85
|
}
|
|
85
86
|
},
|
|
86
87
|
|
|
87
88
|
setThemeMode: async (mode: ThemeMode) => {
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
const { _updateInProgress } = get();
|
|
90
|
+
if (_updateInProgress) return;
|
|
91
|
+
set({ _updateInProgress: true });
|
|
90
92
|
|
|
91
93
|
try {
|
|
92
94
|
const theme = mode === 'light' ? lightTheme : darkTheme;
|
|
@@ -96,7 +98,7 @@ export const useTheme = createStore<ThemeState, ThemeActions>({
|
|
|
96
98
|
} catch {
|
|
97
99
|
// Silent failure
|
|
98
100
|
} finally {
|
|
99
|
-
|
|
101
|
+
set({ _updateInProgress: false });
|
|
100
102
|
}
|
|
101
103
|
},
|
|
102
104
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TimezoneCalendarDay } from '../../domain/entities/Timezone';
|
|
2
|
+
import { parseDate } from '../utils/TimezoneParsers';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* CalendarManager
|
|
@@ -123,8 +124,7 @@ export class CalendarManager {
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
parse(date: Date | string | number): Date {
|
|
126
|
-
|
|
127
|
-
return new Date(date);
|
|
127
|
+
return parseDate(date);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
isValid(date: Date | string | number): boolean {
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* DateFormatter
|
|
3
3
|
* Handles locale-aware formatting of dates and times
|
|
4
4
|
*/
|
|
5
|
+
import { parseDate } from '../utils/TimezoneParsers';
|
|
6
|
+
|
|
5
7
|
export class DateFormatter {
|
|
6
8
|
formatDate(
|
|
7
9
|
date: Date | string | number,
|
|
@@ -98,8 +100,7 @@ export class DateFormatter {
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
parse(date: Date | string | number): Date {
|
|
101
|
-
|
|
102
|
-
return new Date(date);
|
|
103
|
+
return parseDate(date);
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
formatDuration(milliseconds: number): string {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timezone Parsers Utility
|
|
3
|
+
*
|
|
4
|
+
* Shared parsing functions for timezone services.
|
|
5
|
+
* Extracted from duplicate methods across DateFormatter, CalendarManager,
|
|
6
|
+
* BusinessCalendarManager, DateRangeUtils, and DateComparisonUtils.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse date from various input types
|
|
11
|
+
* Ensures a Date object is returned from Date, string, or number input
|
|
12
|
+
*/
|
|
13
|
+
export function parseDate(date: Date | string | number): Date {
|
|
14
|
+
if (date instanceof Date) return new Date(date.getTime());
|
|
15
|
+
return new Date(date);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse timezone offset string to number
|
|
20
|
+
* @param offset - Offset string (e.g., "+05:30", "-08:00")
|
|
21
|
+
* @returns Offset in minutes
|
|
22
|
+
*/
|
|
23
|
+
export function parseTimezoneOffset(offset: string): number {
|
|
24
|
+
const sign = offset[0] === '-' ? -1 : 1;
|
|
25
|
+
const [hours, minutes] = offset.slice(1).split(':').map(Number);
|
|
26
|
+
return sign * (hours * 60 + (minutes || 0));
|
|
27
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @layer presentation/hooks
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { useState, useCallback, useEffect, useMemo } from 'react';
|
|
11
|
+
import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
12
12
|
import { SharingService } from '../../infrastructure/services/SharingService';
|
|
13
13
|
import type { ShareOptions } from '../../domain/entities/Share';
|
|
14
14
|
|
|
@@ -47,29 +47,40 @@ export const useSharing = () => {
|
|
|
47
47
|
const [isSharing, setIsSharing] = useState(false);
|
|
48
48
|
const [error, setError] = useState<string | null>(null);
|
|
49
49
|
|
|
50
|
+
// Track mounted state to prevent setState on unmounted component
|
|
51
|
+
const isMountedRef = useRef(true);
|
|
52
|
+
|
|
50
53
|
/**
|
|
51
54
|
* Check sharing availability on mount
|
|
52
55
|
*/
|
|
53
56
|
useEffect(() => {
|
|
54
57
|
const checkAvailability = async () => {
|
|
55
58
|
const available = await SharingService.isAvailable();
|
|
56
|
-
|
|
59
|
+
if (isMountedRef.current) {
|
|
60
|
+
setIsAvailable(available);
|
|
61
|
+
}
|
|
57
62
|
};
|
|
58
63
|
|
|
59
64
|
checkAvailability();
|
|
65
|
+
|
|
66
|
+
return () => {
|
|
67
|
+
isMountedRef.current = false;
|
|
68
|
+
};
|
|
60
69
|
}, []);
|
|
61
70
|
|
|
62
71
|
/**
|
|
63
72
|
* Share a file via system share sheet
|
|
64
73
|
*/
|
|
65
74
|
const share = useCallback(async (uri: string, options?: ShareOptions): Promise<boolean> => {
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
if (isMountedRef.current) {
|
|
76
|
+
setIsSharing(true);
|
|
77
|
+
setError(null);
|
|
78
|
+
}
|
|
68
79
|
|
|
69
80
|
try {
|
|
70
81
|
const result = await SharingService.shareFile(uri, options);
|
|
71
82
|
|
|
72
|
-
if (!result.success) {
|
|
83
|
+
if (!result.success && isMountedRef.current) {
|
|
73
84
|
setError(result.error || 'Failed to share file');
|
|
74
85
|
return false;
|
|
75
86
|
}
|
|
@@ -77,10 +88,14 @@ export const useSharing = () => {
|
|
|
77
88
|
return true;
|
|
78
89
|
} catch (err) {
|
|
79
90
|
const errorMessage = err instanceof Error ? err.message : 'Failed to share file';
|
|
80
|
-
|
|
91
|
+
if (isMountedRef.current) {
|
|
92
|
+
setError(errorMessage);
|
|
93
|
+
}
|
|
81
94
|
return false;
|
|
82
95
|
} finally {
|
|
83
|
-
|
|
96
|
+
if (isMountedRef.current) {
|
|
97
|
+
setIsSharing(false);
|
|
98
|
+
}
|
|
84
99
|
}
|
|
85
100
|
}, []);
|
|
86
101
|
|
|
@@ -89,13 +104,15 @@ export const useSharing = () => {
|
|
|
89
104
|
*/
|
|
90
105
|
const shareWithAutoType = useCallback(
|
|
91
106
|
async (uri: string, filename: string, dialogTitle?: string): Promise<boolean> => {
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
if (isMountedRef.current) {
|
|
108
|
+
setIsSharing(true);
|
|
109
|
+
setError(null);
|
|
110
|
+
}
|
|
94
111
|
|
|
95
112
|
try {
|
|
96
113
|
const result = await SharingService.shareWithAutoType(uri, filename, dialogTitle);
|
|
97
114
|
|
|
98
|
-
if (!result.success) {
|
|
115
|
+
if (!result.success && isMountedRef.current) {
|
|
99
116
|
setError(result.error || 'Failed to share file');
|
|
100
117
|
return false;
|
|
101
118
|
}
|
|
@@ -103,10 +120,14 @@ export const useSharing = () => {
|
|
|
103
120
|
return true;
|
|
104
121
|
} catch (err) {
|
|
105
122
|
const errorMessage = err instanceof Error ? err.message : 'Failed to share file';
|
|
106
|
-
|
|
123
|
+
if (isMountedRef.current) {
|
|
124
|
+
setError(errorMessage);
|
|
125
|
+
}
|
|
107
126
|
return false;
|
|
108
127
|
} finally {
|
|
109
|
-
|
|
128
|
+
if (isMountedRef.current) {
|
|
129
|
+
setIsSharing(false);
|
|
130
|
+
}
|
|
110
131
|
}
|
|
111
132
|
},
|
|
112
133
|
[]
|
|
@@ -117,13 +138,15 @@ export const useSharing = () => {
|
|
|
117
138
|
*/
|
|
118
139
|
const shareMultiple = useCallback(
|
|
119
140
|
async (uris: string[], options?: ShareOptions): Promise<boolean> => {
|
|
120
|
-
|
|
121
|
-
|
|
141
|
+
if (isMountedRef.current) {
|
|
142
|
+
setIsSharing(true);
|
|
143
|
+
setError(null);
|
|
144
|
+
}
|
|
122
145
|
|
|
123
146
|
try {
|
|
124
147
|
const result = await SharingService.shareMultipleFiles(uris, options);
|
|
125
148
|
|
|
126
|
-
if (!result.success) {
|
|
149
|
+
if (!result.success && isMountedRef.current) {
|
|
127
150
|
setError(result.error || 'Failed to share files');
|
|
128
151
|
return false;
|
|
129
152
|
}
|
|
@@ -131,10 +154,14 @@ export const useSharing = () => {
|
|
|
131
154
|
return true;
|
|
132
155
|
} catch (err) {
|
|
133
156
|
const errorMessage = err instanceof Error ? err.message : 'Failed to share files';
|
|
134
|
-
|
|
157
|
+
if (isMountedRef.current) {
|
|
158
|
+
setError(errorMessage);
|
|
159
|
+
}
|
|
135
160
|
return false;
|
|
136
161
|
} finally {
|
|
137
|
-
|
|
162
|
+
if (isMountedRef.current) {
|
|
163
|
+
setIsSharing(false);
|
|
164
|
+
}
|
|
138
165
|
}
|
|
139
166
|
},
|
|
140
167
|
[]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* retryWithBackoff
|
|
3
|
+
*
|
|
4
|
+
* Retry utility with exponential backoff for async operations.
|
|
5
|
+
* Useful for network requests, file operations, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ErrorHandler } from '../errors/ErrorHandler';
|
|
9
|
+
|
|
10
|
+
export interface RetryOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Maximum number of retry attempts
|
|
13
|
+
* @default 3
|
|
14
|
+
*/
|
|
15
|
+
maxRetries?: number;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initial delay in milliseconds before first retry
|
|
19
|
+
* @default 1000
|
|
20
|
+
*/
|
|
21
|
+
baseDelay?: number;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Maximum delay in milliseconds (caps exponential growth)
|
|
25
|
+
* @default 10000
|
|
26
|
+
*/
|
|
27
|
+
maxDelay?: number;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Multiplier for exponential backoff
|
|
31
|
+
* @default 2
|
|
32
|
+
*/
|
|
33
|
+
backoffMultiplier?: number;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Function to determine if error is retryable
|
|
37
|
+
* @default () => true (retry all errors)
|
|
38
|
+
*/
|
|
39
|
+
shouldRetry?: (error: Error, attempt: number) => boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Callback on each retry attempt
|
|
43
|
+
*/
|
|
44
|
+
onRetry?: (error: Error, attempt: number, delay: number) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Retry an async function with exponential backoff
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* const result = await retryWithBackoff(
|
|
53
|
+
* () => fetch('https://api.example.com'),
|
|
54
|
+
* { maxRetries: 3, baseDelay: 1000 }
|
|
55
|
+
* );
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export async function retryWithBackoff<T>(
|
|
59
|
+
fn: () => Promise<T>,
|
|
60
|
+
options: RetryOptions = {}
|
|
61
|
+
): Promise<T> {
|
|
62
|
+
const {
|
|
63
|
+
maxRetries = 3,
|
|
64
|
+
baseDelay = 1000,
|
|
65
|
+
maxDelay = 10000,
|
|
66
|
+
backoffMultiplier = 2,
|
|
67
|
+
shouldRetry = () => true,
|
|
68
|
+
onRetry,
|
|
69
|
+
} = options;
|
|
70
|
+
|
|
71
|
+
let lastError: Error;
|
|
72
|
+
|
|
73
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
74
|
+
try {
|
|
75
|
+
// Attempt the operation
|
|
76
|
+
return await fn();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
79
|
+
|
|
80
|
+
// Check if we should retry
|
|
81
|
+
const isLastAttempt = attempt === maxRetries;
|
|
82
|
+
if (isLastAttempt || !shouldRetry(lastError, attempt)) {
|
|
83
|
+
throw lastError;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Calculate delay with exponential backoff
|
|
87
|
+
const delay = Math.min(
|
|
88
|
+
baseDelay * Math.pow(backoffMultiplier, attempt),
|
|
89
|
+
maxDelay
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Call onRetry callback if provided
|
|
93
|
+
if (onRetry) {
|
|
94
|
+
onRetry(lastError, attempt + 1, delay);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Log retry in development
|
|
98
|
+
if (__DEV__) {
|
|
99
|
+
console.log(
|
|
100
|
+
`[Retry] Attempt ${attempt + 1}/${maxRetries} failed. Retrying in ${delay}ms...`,
|
|
101
|
+
lastError.message
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Wait before retrying
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// This should never be reached, but TypeScript needs it
|
|
111
|
+
throw lastError!;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Retry with timeout
|
|
116
|
+
* Combines retry logic with a timeout
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* const result = await retryWithTimeout(
|
|
121
|
+
* () => fetch('https://api.example.com'),
|
|
122
|
+
* { timeout: 5000, maxRetries: 3 }
|
|
123
|
+
* );
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export async function retryWithTimeout<T>(
|
|
127
|
+
fn: () => Promise<T>,
|
|
128
|
+
options: RetryOptions & { timeout?: number } = {}
|
|
129
|
+
): Promise<T> {
|
|
130
|
+
const { timeout = 30000, ...retryOptions } = options;
|
|
131
|
+
|
|
132
|
+
return retryWithBackoff(
|
|
133
|
+
() => withTimeout(fn(), timeout),
|
|
134
|
+
retryOptions
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Add timeout to a promise
|
|
140
|
+
*/
|
|
141
|
+
function withTimeout<T>(
|
|
142
|
+
promise: Promise<T>,
|
|
143
|
+
timeoutMs: number
|
|
144
|
+
): Promise<T> {
|
|
145
|
+
return Promise.race([
|
|
146
|
+
promise,
|
|
147
|
+
new Promise<T>((_, reject) =>
|
|
148
|
+
setTimeout(
|
|
149
|
+
() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)),
|
|
150
|
+
timeoutMs
|
|
151
|
+
)
|
|
152
|
+
),
|
|
153
|
+
]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if error is a network error
|
|
158
|
+
* Useful for shouldRetry callback
|
|
159
|
+
*/
|
|
160
|
+
export function isNetworkError(error: Error): boolean {
|
|
161
|
+
return (
|
|
162
|
+
error.message.includes('network') ||
|
|
163
|
+
error.message.includes('timeout') ||
|
|
164
|
+
error.message.includes('fetch') ||
|
|
165
|
+
error.message.includes('ECONNREFUSED') ||
|
|
166
|
+
error.message.includes('ETIMEDOUT')
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Check if error is retryable HTTP status
|
|
172
|
+
* Useful for shouldRetry callback
|
|
173
|
+
*/
|
|
174
|
+
export function isRetryableHttpStatus(status: number): boolean {
|
|
175
|
+
// Retry on 5xx server errors and 429 (rate limit)
|
|
176
|
+
return status >= 500 || status === 429 || status === 408;
|
|
177
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DesignSystemError
|
|
3
|
+
*
|
|
4
|
+
* Unified error class for the design system package.
|
|
5
|
+
* Provides consistent error handling with error codes and context.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class DesignSystemError extends Error {
|
|
9
|
+
/**
|
|
10
|
+
* Error code for categorization
|
|
11
|
+
*/
|
|
12
|
+
public readonly code: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Additional context about the error
|
|
16
|
+
*/
|
|
17
|
+
public readonly context?: Record<string, any>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Timestamp when error was created
|
|
21
|
+
*/
|
|
22
|
+
public readonly timestamp: Date;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
message: string,
|
|
26
|
+
code: string,
|
|
27
|
+
context?: Record<string, any>
|
|
28
|
+
) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'DesignSystemError';
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.context = context;
|
|
33
|
+
this.timestamp = new Date();
|
|
34
|
+
|
|
35
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
36
|
+
if (Error.captureStackTrace) {
|
|
37
|
+
Error.captureStackTrace(this, DesignSystemError);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert error to JSON for logging/debugging
|
|
43
|
+
*/
|
|
44
|
+
toJSON(): Record<string, any> {
|
|
45
|
+
return {
|
|
46
|
+
name: this.name,
|
|
47
|
+
message: this.message,
|
|
48
|
+
code: this.code,
|
|
49
|
+
context: this.context,
|
|
50
|
+
timestamp: this.timestamp.toISOString(),
|
|
51
|
+
stack: this.stack,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get user-friendly error message
|
|
57
|
+
*/
|
|
58
|
+
getUserMessage(): string {
|
|
59
|
+
// You can customize user-facing messages based on error codes
|
|
60
|
+
switch (this.code) {
|
|
61
|
+
case 'FILE_NOT_FOUND':
|
|
62
|
+
return 'The requested file could not be found.';
|
|
63
|
+
case 'PERMISSION_DENIED':
|
|
64
|
+
return 'Permission denied. Please check app permissions.';
|
|
65
|
+
case 'NETWORK_ERROR':
|
|
66
|
+
return 'Network error. Please check your connection.';
|
|
67
|
+
case 'STORAGE_FULL':
|
|
68
|
+
return 'Storage is full. Please free up some space.';
|
|
69
|
+
case 'INVALID_INPUT':
|
|
70
|
+
return 'Invalid input provided.';
|
|
71
|
+
default:
|
|
72
|
+
return this.message;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Common error codes used across the design system
|
|
79
|
+
*/
|
|
80
|
+
export const ErrorCodes = {
|
|
81
|
+
// File system errors
|
|
82
|
+
FILE_NOT_FOUND: 'FILE_NOT_FOUND',
|
|
83
|
+
FILE_READ_ERROR: 'FILE_READ_ERROR',
|
|
84
|
+
FILE_WRITE_ERROR: 'FILE_WRITE_ERROR',
|
|
85
|
+
FILE_DELETE_ERROR: 'FILE_DELETE_ERROR',
|
|
86
|
+
DIRECTORY_CREATE_ERROR: 'DIRECTORY_CREATE_ERROR',
|
|
87
|
+
PERMISSION_DENIED: 'PERMISSION_DENIED',
|
|
88
|
+
STORAGE_FULL: 'STORAGE_FULL',
|
|
89
|
+
|
|
90
|
+
// Network errors
|
|
91
|
+
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
92
|
+
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
|
|
93
|
+
API_ERROR: 'API_ERROR',
|
|
94
|
+
|
|
95
|
+
// Media errors
|
|
96
|
+
MEDIA_PICKER_ERROR: 'MEDIA_PICKER_ERROR',
|
|
97
|
+
MEDIA_SAVE_ERROR: 'MEDIA_SAVE_ERROR',
|
|
98
|
+
IMAGE_LOAD_ERROR: 'IMAGE_LOAD_ERROR',
|
|
99
|
+
|
|
100
|
+
// Storage errors
|
|
101
|
+
CACHE_ERROR: 'CACHE_ERROR',
|
|
102
|
+
STORAGE_ERROR: 'STORAGE_ERROR',
|
|
103
|
+
|
|
104
|
+
// Validation errors
|
|
105
|
+
INVALID_INPUT: 'INVALID_INPUT',
|
|
106
|
+
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
107
|
+
|
|
108
|
+
// Theme errors
|
|
109
|
+
THEME_LOAD_ERROR: 'THEME_LOAD_ERROR',
|
|
110
|
+
THEME_SAVE_ERROR: 'THEME_SAVE_ERROR',
|
|
111
|
+
|
|
112
|
+
// Generic errors
|
|
113
|
+
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
|
|
114
|
+
INITIALIZATION_ERROR: 'INITIALIZATION_ERROR',
|
|
115
|
+
} as const;
|
|
116
|
+
|
|
117
|
+
export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes];
|