@umituz/react-native-design-system 4.27.15 → 4.27.17
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/dist/core/cache/domain/CleanupStrategy.d.ts +62 -0
- package/dist/core/cache/domain/UnifiedCache.d.ts +82 -0
- package/dist/core/cache/domain/types.d.ts +15 -0
- package/dist/core/cache/index.d.ts +13 -0
- package/dist/core/cache/infrastructure/CacheFactory.d.ts +63 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/permissions/domain/PermissionHandler.d.ts +82 -0
- package/dist/core/permissions/domain/types.d.ts +42 -0
- package/dist/core/permissions/index.d.ts +7 -0
- package/dist/core/repositories/domain/RepositoryKeyFactory.d.ts +25 -0
- package/dist/core/repositories/domain/RepositoryUtils.d.ts +39 -0
- package/dist/core/repositories/domain/types.d.ts +53 -0
- package/dist/core/repositories/index.d.ts +10 -0
- package/dist/hooks/index.d.ts +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/media/domain/strategies/CameraPickerStrategy.d.ts +25 -0
- package/dist/media/domain/strategies/LibraryPickerStrategy.d.ts +18 -0
- package/dist/media/domain/strategies/PickerStrategy.d.ts +55 -0
- package/dist/media/domain/strategies/index.d.ts +10 -0
- package/dist/media/infrastructure/services/MediaPickerService.d.ts +49 -6
- package/dist/media/infrastructure/utils/PermissionManager.d.ts +6 -0
- package/dist/media/infrastructure/utils/mediaPickerMappers.d.ts +15 -1
- package/dist/molecules/calendar/infrastructure/services/CalendarService.d.ts +1 -0
- package/dist/molecules/calendar/infrastructure/storage/CalendarStore.d.ts +2 -2
- package/dist/molecules/filter-group/FilterGroup.d.ts +1 -1
- package/dist/molecules/navigation/index.d.ts +1 -1
- package/dist/offline/index.d.ts +1 -1
- package/dist/offline/presentation/hooks/useOffline.d.ts +0 -5
- package/dist/tanstack/domain/repositories/BaseRepository.d.ts +5 -1
- package/dist/tanstack/infrastructure/monitoring/DevMonitor.d.ts +1 -0
- package/dist/utils/constants/TimeConstants.d.ts +27 -0
- package/package.json +11 -6
- package/src/atoms/GlassView/GlassView.tsx +0 -2
- package/src/atoms/picker/components/PickerChips.tsx +27 -21
- package/src/core/cache/index.ts +3 -2
- package/src/core/index.ts +5 -3
- package/src/core/repositories/domain/RepositoryUtils.ts +4 -4
- package/src/core/repositories/domain/types.ts +2 -2
- package/src/hooks/index.ts +22 -25
- package/src/image/presentation/components/ImageGallery.tsx +25 -21
- package/src/infinite-scroll/presentation/hooks/useInfiniteScroll.ts +32 -29
- package/src/media/domain/strategies/CameraPickerStrategy.ts +4 -8
- package/src/media/domain/strategies/index.ts +1 -1
- package/src/media/infrastructure/services/MediaPickerService.ts +3 -14
- package/src/media/infrastructure/utils/mediaPickerMappers.ts +29 -6
- package/src/molecules/avatar/AvatarGroup.tsx +137 -62
- package/src/molecules/filter-group/FilterGroup.tsx +31 -22
- package/src/molecules/icon-grid/IconGrid.tsx +52 -20
- package/src/molecules/swipe-actions/domain/entities/SwipeAction.ts +0 -1
- package/src/offline/index.ts +1 -1
- package/src/offline/presentation/hooks/useOffline.ts +0 -8
- package/src/onboarding/presentation/components/BackgroundImageCollage.tsx +32 -23
- package/src/storage/README.md +0 -1
- package/src/storage/cache/domain/types/README.md +0 -1
- package/src/storage/cache/presentation/README.md +0 -1
- package/src/storage/cache/presentation/useCachedValue.ts +12 -2
- package/src/storage/domain/constants/README.md +0 -1
- package/src/storage/infrastructure/adapters/README.md +0 -1
- package/src/storage/presentation/hooks/README.md +0 -6
- package/src/storage/presentation/hooks/useStorageState.ts +13 -4
- package/src/tanstack/domain/repositories/BaseRepository.ts +1 -1
- package/src/theme/hooks/useAppDesignTokens.ts +29 -3
- package/src/timezone/infrastructure/services/TimezoneProvider.ts +2 -2
- package/src/init/index.ts +0 -29
- package/src/layouts/ScreenLayout/index.ts +0 -1
- package/src/layouts/index.ts +0 -5
- package/src/molecules/SearchBar/index.ts +0 -4
- package/src/molecules/StepProgress/index.ts +0 -1
- package/src/molecules/alerts/index.ts +0 -47
- package/src/molecules/bottom-sheet/index.ts +0 -10
- package/src/molecules/circular-menu/index.ts +0 -3
- package/src/molecules/filter-group/index.ts +0 -3
- package/src/molecules/index.ts +0 -38
- package/src/molecules/info-grid/index.ts +0 -3
- package/src/molecules/swipe-actions/index.ts +0 -6
- package/src/presentation/utils/variants/index.ts +0 -6
- package/src/timezone/infrastructure/utils/SimpleCache.ts +0 -64
- package/src/utilities/index.ts +0 -6
|
@@ -67,33 +67,6 @@ export function useInfiniteScroll<T>(
|
|
|
67
67
|
abortControllerRef.current = new AbortController();
|
|
68
68
|
}, []);
|
|
69
69
|
|
|
70
|
-
const loadInitial = useCallback(async () => {
|
|
71
|
-
if (isLoadingRef.current) return;
|
|
72
|
-
isLoadingRef.current = true;
|
|
73
|
-
cancelPendingRequests();
|
|
74
|
-
|
|
75
|
-
if (isMountedRef.current) setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const newState = await retryWithBackoff(
|
|
79
|
-
() => loadData(config, initialPage, pageSize, totalItems),
|
|
80
|
-
maxRetries,
|
|
81
|
-
retryDelay,
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
if (isMountedRef.current) {
|
|
85
|
-
setState(newState);
|
|
86
|
-
}
|
|
87
|
-
} catch (error) {
|
|
88
|
-
if (isMountedRef.current) {
|
|
89
|
-
const errorMessage = error instanceof Error ? error.message : "Failed to load data";
|
|
90
|
-
setState((prev) => ({ ...prev, isLoading: false, error: errorMessage }));
|
|
91
|
-
}
|
|
92
|
-
} finally {
|
|
93
|
-
isLoadingRef.current = false;
|
|
94
|
-
}
|
|
95
|
-
}, [config, initialPage, pageSize, totalItems, maxRetries, retryDelay, cancelPendingRequests]);
|
|
96
|
-
|
|
97
70
|
const loadMore = useCallback(async () => {
|
|
98
71
|
// Get fresh state from ref to avoid stale closure
|
|
99
72
|
const currentState = stateRef.current;
|
|
@@ -165,8 +138,38 @@ export function useInfiniteScroll<T>(
|
|
|
165
138
|
}, [initialPage, totalItems, cancelPendingRequests]);
|
|
166
139
|
|
|
167
140
|
useEffect(() => {
|
|
168
|
-
if (autoLoad)
|
|
169
|
-
|
|
141
|
+
if (autoLoad) {
|
|
142
|
+
const initialize = async () => {
|
|
143
|
+
if (isLoadingRef.current) return;
|
|
144
|
+
|
|
145
|
+
isLoadingRef.current = true;
|
|
146
|
+
cancelPendingRequests();
|
|
147
|
+
|
|
148
|
+
if (isMountedRef.current) setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const newState = await retryWithBackoff(
|
|
152
|
+
() => loadData(config, initialPage, pageSize, totalItems),
|
|
153
|
+
maxRetries,
|
|
154
|
+
retryDelay,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (isMountedRef.current) {
|
|
158
|
+
setState(newState);
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (isMountedRef.current) {
|
|
162
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to load data";
|
|
163
|
+
setState((prev) => ({ ...prev, isLoading: false, error: errorMessage }));
|
|
164
|
+
}
|
|
165
|
+
} finally {
|
|
166
|
+
isLoadingRef.current = false;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
initialize();
|
|
170
|
+
}
|
|
171
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
172
|
+
}, [autoLoad]);
|
|
170
173
|
|
|
171
174
|
const canLoadMore = state.hasMore && !state.isLoadingMore && !state.isLoading;
|
|
172
175
|
|
|
@@ -32,8 +32,10 @@ export class CameraPickerStrategy implements PickerStrategy {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
async launch(options: LaunchOptions): Promise<any> {
|
|
35
|
-
const mediaTypes =
|
|
36
|
-
this.config.mediaType === 'videos'
|
|
35
|
+
const mediaTypes: ImagePicker.MediaTypeOptions =
|
|
36
|
+
this.config.mediaType === 'videos'
|
|
37
|
+
? ImagePicker.MediaTypeOptions.Videos
|
|
38
|
+
: ImagePicker.MediaTypeOptions.Images;
|
|
37
39
|
|
|
38
40
|
const launchOptions: ImagePicker.ImagePickerOptions = {
|
|
39
41
|
mediaTypes,
|
|
@@ -52,12 +54,6 @@ export class CameraPickerStrategy implements PickerStrategy {
|
|
|
52
54
|
if (options.videoMaxDuration !== undefined) {
|
|
53
55
|
launchOptions.videoMaxDuration = options.videoMaxDuration;
|
|
54
56
|
}
|
|
55
|
-
if (options.videoMaxBitrate !== undefined) {
|
|
56
|
-
launchOptions.videoMaxBitrate = options.videoMaxBitrate;
|
|
57
|
-
}
|
|
58
|
-
if (options.videoQuality) {
|
|
59
|
-
launchOptions.videoQuality = options.videoQuality;
|
|
60
|
-
}
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
return ImagePicker.launchCameraAsync(launchOptions);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Strategy pattern implementations for different picker types.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export { PickerStrategy } from './PickerStrategy';
|
|
7
|
+
export type { PickerStrategy } from './PickerStrategy';
|
|
8
8
|
export type { LaunchOptions, PickerLaunchResult } from './PickerStrategy';
|
|
9
9
|
|
|
10
10
|
export { CameraPickerStrategy } from './CameraPickerStrategy';
|
|
@@ -16,9 +16,8 @@ import type {
|
|
|
16
16
|
import {
|
|
17
17
|
MediaType,
|
|
18
18
|
MediaValidationError,
|
|
19
|
-
MEDIA_CONSTANTS,
|
|
20
19
|
} from '../../domain/entities/Media';
|
|
21
|
-
import {
|
|
20
|
+
import { mapPickerResultFromStrategy } from '../utils/mediaPickerMappers';
|
|
22
21
|
import { PermissionManager } from '../utils/PermissionManager';
|
|
23
22
|
import { FileValidator } from '../../domain/utils/FileValidator';
|
|
24
23
|
import { ErrorHandler } from '../../../utils/errors';
|
|
@@ -62,8 +61,8 @@ export class MediaPickerService {
|
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
try {
|
|
65
|
-
const
|
|
66
|
-
return
|
|
64
|
+
const pickerResult = await strategy.launch(options ?? {});
|
|
65
|
+
return mapPickerResultFromStrategy(pickerResult);
|
|
67
66
|
} catch (error) {
|
|
68
67
|
ErrorHandler.handleAndLog(error, 'launchMediaPicker', {
|
|
69
68
|
strategy: strategy.name,
|
|
@@ -178,14 +177,4 @@ export class MediaPickerService {
|
|
|
178
177
|
mediaTypes: MediaType.ALL,
|
|
179
178
|
});
|
|
180
179
|
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Legacy method for backward compatibility
|
|
184
|
-
* @deprecated Use pickSingleImage instead
|
|
185
|
-
*/
|
|
186
|
-
static async pickImage(
|
|
187
|
-
options?: MediaPickerOptions
|
|
188
|
-
): Promise<MediaPickerResult> {
|
|
189
|
-
return this.pickFromLibrary(options);
|
|
190
|
-
}
|
|
191
180
|
}
|
|
@@ -15,15 +15,13 @@ import {
|
|
|
15
15
|
* Map expo-image-picker permission status to MediaLibraryPermission
|
|
16
16
|
*/
|
|
17
17
|
export const mapPermissionStatus = (
|
|
18
|
-
status:
|
|
18
|
+
status: string
|
|
19
19
|
): MediaLibraryPermission => {
|
|
20
20
|
switch (status) {
|
|
21
|
-
case
|
|
21
|
+
case 'granted':
|
|
22
22
|
return MediaLibraryPermission.GRANTED;
|
|
23
|
-
case
|
|
24
|
-
|
|
25
|
-
case ImagePicker.PermissionStatus.UNDETERMINED:
|
|
26
|
-
return MediaLibraryPermission.DENIED;
|
|
23
|
+
case 'denied':
|
|
24
|
+
case 'undetermined':
|
|
27
25
|
default:
|
|
28
26
|
return MediaLibraryPermission.DENIED;
|
|
29
27
|
}
|
|
@@ -74,3 +72,28 @@ export const mapPickerResult = (
|
|
|
74
72
|
assets,
|
|
75
73
|
};
|
|
76
74
|
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Map PickerStrategy result to MediaPickerResult
|
|
78
|
+
*/
|
|
79
|
+
export const mapPickerResultFromStrategy = (
|
|
80
|
+
result: { canceled: boolean; assets?: Array<{ uri: string; width?: number; height?: number; type?: 'image' | 'video'; duration?: number; fileSize?: number }> }
|
|
81
|
+
): MediaPickerResult => {
|
|
82
|
+
if (result.canceled) {
|
|
83
|
+
return { canceled: true };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const assets: MediaAsset[] = (result.assets ?? []).map((asset) => ({
|
|
87
|
+
uri: asset.uri,
|
|
88
|
+
width: asset.width ?? 0,
|
|
89
|
+
height: asset.height ?? 0,
|
|
90
|
+
type: asset.type === 'video' ? MediaType.VIDEO : MediaType.IMAGE,
|
|
91
|
+
fileSize: asset.fileSize,
|
|
92
|
+
duration: asset.duration,
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
canceled: false,
|
|
97
|
+
assets,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Shows overflow count when exceeding max visible avatars.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React from 'react';
|
|
8
|
+
import React, { useCallback, useMemo } from 'react';
|
|
9
9
|
import { View, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
10
10
|
import { useAppDesignTokens } from '../../theme';
|
|
11
11
|
import { AtomicText } from '../../atoms';
|
|
@@ -40,7 +40,96 @@ export interface AvatarGroupProps {
|
|
|
40
40
|
style?: StyleProp<ViewStyle>;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
// Memoized individual avatar item
|
|
44
|
+
const AvatarItem = React.memo<{
|
|
45
|
+
item: AvatarGroupItem;
|
|
46
|
+
index: number;
|
|
47
|
+
size: AvatarSize;
|
|
48
|
+
shape: AvatarShape;
|
|
49
|
+
spacing: number;
|
|
50
|
+
avatarStyle: any;
|
|
51
|
+
}>(({ item, index, size, shape, spacing, avatarStyle }) => {
|
|
52
|
+
const wrapperStyle = useMemo(
|
|
53
|
+
() => [
|
|
54
|
+
styles.avatarWrapper,
|
|
55
|
+
index > 0 && { marginLeft: spacing },
|
|
56
|
+
],
|
|
57
|
+
[index, spacing]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<View style={wrapperStyle}>
|
|
62
|
+
<Avatar
|
|
63
|
+
uri={item.uri}
|
|
64
|
+
name={item.name}
|
|
65
|
+
icon={item.icon}
|
|
66
|
+
size={size}
|
|
67
|
+
shape={shape}
|
|
68
|
+
style={avatarStyle}
|
|
69
|
+
/>
|
|
70
|
+
</View>
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Memoized overflow badge
|
|
75
|
+
const OverflowBadge = React.memo<{
|
|
76
|
+
count: number;
|
|
77
|
+
spacing: number;
|
|
78
|
+
config: any;
|
|
79
|
+
shape: AvatarShape;
|
|
80
|
+
surfaceSecondary: string;
|
|
81
|
+
onBackground: string;
|
|
82
|
+
textSecondary: string;
|
|
83
|
+
}>(({ count, spacing, config, shape, surfaceSecondary, onBackground, textSecondary }) => {
|
|
84
|
+
const wrapperStyle = useMemo(
|
|
85
|
+
() => [
|
|
86
|
+
styles.avatarWrapper,
|
|
87
|
+
{ marginLeft: spacing },
|
|
88
|
+
],
|
|
89
|
+
[spacing]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const badgeStyle = useMemo(
|
|
93
|
+
() => [
|
|
94
|
+
styles.overflow,
|
|
95
|
+
{
|
|
96
|
+
width: config.size,
|
|
97
|
+
height: config.size,
|
|
98
|
+
borderRadius: shape === 'circle' ? config.size / 2 : shape === 'rounded' ? 8 : 0,
|
|
99
|
+
backgroundColor: surfaceSecondary,
|
|
100
|
+
borderWidth: 2,
|
|
101
|
+
borderColor: onBackground,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
[config.size, shape, surfaceSecondary, onBackground]
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const textStyle = useMemo(
|
|
108
|
+
() => [
|
|
109
|
+
styles.overflowText,
|
|
110
|
+
{
|
|
111
|
+
fontSize: config.fontSize,
|
|
112
|
+
color: textSecondary,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
[config.fontSize, textSecondary]
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<View style={wrapperStyle}>
|
|
120
|
+
<View style={badgeStyle}>
|
|
121
|
+
<AtomicText
|
|
122
|
+
type="bodySmall"
|
|
123
|
+
style={textStyle}
|
|
124
|
+
>
|
|
125
|
+
+{count}
|
|
126
|
+
</AtomicText>
|
|
127
|
+
</View>
|
|
128
|
+
</View>
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export const AvatarGroup: React.FC<AvatarGroupProps> = React.memo(({
|
|
44
133
|
items,
|
|
45
134
|
maxVisible = AVATAR_CONSTANTS.MAX_GROUP_VISIBLE,
|
|
46
135
|
size = AVATAR_CONSTANTS.DEFAULT_SIZE,
|
|
@@ -51,76 +140,62 @@ export const AvatarGroup: React.FC<AvatarGroupProps> = ({
|
|
|
51
140
|
const tokens = useAppDesignTokens();
|
|
52
141
|
const config = SIZE_CONFIGS[size];
|
|
53
142
|
|
|
54
|
-
//
|
|
55
|
-
const visibleItems =
|
|
56
|
-
|
|
57
|
-
|
|
143
|
+
// Memoize calculations to prevent recalculation on every render
|
|
144
|
+
const { visibleItems, overflowCount, hasOverflow } = useMemo(() => {
|
|
145
|
+
const visItems = items.slice(0, maxVisible);
|
|
146
|
+
const overflow = items.length - maxVisible;
|
|
147
|
+
return {
|
|
148
|
+
visibleItems: visItems,
|
|
149
|
+
overflowCount: overflow,
|
|
150
|
+
hasOverflow: overflow > 0,
|
|
151
|
+
};
|
|
152
|
+
}, [items, maxVisible]);
|
|
153
|
+
|
|
154
|
+
// Memoize avatar style
|
|
155
|
+
const avatarStyle = useMemo(
|
|
156
|
+
() => [
|
|
157
|
+
styles.avatar,
|
|
158
|
+
{
|
|
159
|
+
borderWidth: 2,
|
|
160
|
+
borderColor: tokens.colors.onBackground,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
[tokens.colors.onBackground]
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Stable key extractor
|
|
167
|
+
const keyExtractor = useCallback((item: AvatarGroupItem, index: number) => {
|
|
168
|
+
return item.uri || item.name || item.icon || `avatar-${index}`;
|
|
169
|
+
}, []);
|
|
58
170
|
|
|
59
171
|
return (
|
|
60
172
|
<View style={[styles.container, style]}>
|
|
61
173
|
{visibleItems.map((item, index) => (
|
|
62
|
-
<
|
|
63
|
-
key={item
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
name={item.name}
|
|
72
|
-
icon={item.icon}
|
|
73
|
-
size={size}
|
|
74
|
-
shape={shape}
|
|
75
|
-
style={[
|
|
76
|
-
styles.avatar,
|
|
77
|
-
{
|
|
78
|
-
borderWidth: 2,
|
|
79
|
-
borderColor: tokens.colors.onBackground,
|
|
80
|
-
},
|
|
81
|
-
]}
|
|
82
|
-
/>
|
|
83
|
-
</View>
|
|
174
|
+
<AvatarItem
|
|
175
|
+
key={keyExtractor(item, index)}
|
|
176
|
+
item={item}
|
|
177
|
+
index={index}
|
|
178
|
+
size={size}
|
|
179
|
+
shape={shape}
|
|
180
|
+
spacing={spacing}
|
|
181
|
+
avatarStyle={avatarStyle}
|
|
182
|
+
/>
|
|
84
183
|
))}
|
|
85
184
|
|
|
86
185
|
{hasOverflow && (
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
{
|
|
97
|
-
width: config.size,
|
|
98
|
-
height: config.size,
|
|
99
|
-
borderRadius: shape === 'circle' ? config.size / 2 : shape === 'rounded' ? 8 : 0,
|
|
100
|
-
backgroundColor: tokens.colors.surfaceSecondary,
|
|
101
|
-
borderWidth: 2,
|
|
102
|
-
borderColor: tokens.colors.onBackground,
|
|
103
|
-
},
|
|
104
|
-
]}
|
|
105
|
-
>
|
|
106
|
-
<AtomicText
|
|
107
|
-
type="bodySmall"
|
|
108
|
-
style={[
|
|
109
|
-
styles.overflowText,
|
|
110
|
-
{
|
|
111
|
-
fontSize: config.fontSize,
|
|
112
|
-
color: tokens.colors.textSecondary,
|
|
113
|
-
},
|
|
114
|
-
]}
|
|
115
|
-
>
|
|
116
|
-
+{overflowCount}
|
|
117
|
-
</AtomicText>
|
|
118
|
-
</View>
|
|
119
|
-
</View>
|
|
186
|
+
<OverflowBadge
|
|
187
|
+
count={overflowCount}
|
|
188
|
+
spacing={spacing}
|
|
189
|
+
config={config}
|
|
190
|
+
shape={shape}
|
|
191
|
+
surfaceSecondary={tokens.colors.surfaceSecondary}
|
|
192
|
+
onBackground={tokens.colors.onBackground}
|
|
193
|
+
textSecondary={tokens.colors.textSecondary}
|
|
194
|
+
/>
|
|
120
195
|
)}
|
|
121
196
|
</View>
|
|
122
197
|
);
|
|
123
|
-
};
|
|
198
|
+
});
|
|
124
199
|
|
|
125
200
|
const styles = StyleSheet.create({
|
|
126
201
|
container: {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
|
|
2
|
-
import React, { useMemo } from 'react';
|
|
2
|
+
import React, { useMemo, useCallback } from 'react';
|
|
3
3
|
import { ScrollView, StyleSheet } from 'react-native';
|
|
4
4
|
import { AtomicChip } from '../../atoms/chip/AtomicChip';
|
|
5
5
|
import { useAppDesignTokens } from '../../theme';
|
|
6
6
|
import type { FilterGroupProps } from './types';
|
|
7
7
|
|
|
8
|
-
export function FilterGroup<T = string>({
|
|
8
|
+
export const FilterGroup = React.memo(function FilterGroup<T = string>({
|
|
9
9
|
items,
|
|
10
10
|
selectedValue,
|
|
11
11
|
onSelect,
|
|
@@ -29,6 +29,33 @@ export function FilterGroup<T = string>({
|
|
|
29
29
|
},
|
|
30
30
|
}), [tokens.spacing.md]);
|
|
31
31
|
|
|
32
|
+
// Memoize selected items to prevent unnecessary re-renders
|
|
33
|
+
const selectedSet = useMemo(() => {
|
|
34
|
+
if (multiSelect && Array.isArray(selectedValue)) {
|
|
35
|
+
return new Set(selectedValue);
|
|
36
|
+
}
|
|
37
|
+
return new Set(selectedValue !== undefined ? [selectedValue] : []);
|
|
38
|
+
}, [selectedValue, multiSelect]);
|
|
39
|
+
|
|
40
|
+
// Memoize isSelected calculation for each item
|
|
41
|
+
const isSelected = useCallback((value: any) => selectedSet.has(value), [selectedSet]);
|
|
42
|
+
|
|
43
|
+
// Memoized chip renderer
|
|
44
|
+
const renderChip = useCallback((item: any) => (
|
|
45
|
+
<AtomicChip
|
|
46
|
+
key={String(item.value)}
|
|
47
|
+
variant={isSelected(item.value) ? 'filled' : 'outlined'}
|
|
48
|
+
color={isSelected(item.value) ? 'primary' : 'secondary'}
|
|
49
|
+
selected={isSelected(item.value)}
|
|
50
|
+
onPress={() => onSelect(item.value)}
|
|
51
|
+
clickable
|
|
52
|
+
style={[styles.item, itemStyle]}
|
|
53
|
+
testID={item.testID}
|
|
54
|
+
>
|
|
55
|
+
{item.label}
|
|
56
|
+
</AtomicChip>
|
|
57
|
+
), [isSelected, onSelect, styles.item, itemStyle]);
|
|
58
|
+
|
|
32
59
|
return (
|
|
33
60
|
<ScrollView
|
|
34
61
|
horizontal
|
|
@@ -37,25 +64,7 @@ export function FilterGroup<T = string>({
|
|
|
37
64
|
style={[styles.container, style]}
|
|
38
65
|
contentContainerStyle={[styles.content, contentContainerStyle]}
|
|
39
66
|
>
|
|
40
|
-
{items.map(
|
|
41
|
-
const isSelected = multiSelect
|
|
42
|
-
? Array.isArray(selectedValue) && selectedValue.includes(item.value)
|
|
43
|
-
: item.value === selectedValue;
|
|
44
|
-
return (
|
|
45
|
-
<AtomicChip
|
|
46
|
-
key={`${item.value}`}
|
|
47
|
-
variant={isSelected ? 'filled' : 'outlined'}
|
|
48
|
-
color={isSelected ? 'primary' : 'secondary'}
|
|
49
|
-
selected={isSelected}
|
|
50
|
-
onPress={() => onSelect(item.value)}
|
|
51
|
-
clickable
|
|
52
|
-
style={[styles.item, itemStyle]}
|
|
53
|
-
testID={item.testID}
|
|
54
|
-
>
|
|
55
|
-
{item.label}
|
|
56
|
-
</AtomicChip>
|
|
57
|
-
);
|
|
58
|
-
})}
|
|
67
|
+
{items.map(renderChip)}
|
|
59
68
|
</ScrollView>
|
|
60
69
|
);
|
|
61
|
-
}
|
|
70
|
+
});
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type LayoutChangeEvent,
|
|
15
15
|
type StyleProp,
|
|
16
16
|
type ViewStyle,
|
|
17
|
+
FlatList,
|
|
17
18
|
} from 'react-native';
|
|
18
19
|
import { useAppDesignTokens } from '../../theme';
|
|
19
20
|
import { AtomicIcon } from '../../atoms';
|
|
@@ -53,6 +54,8 @@ const GridItem = React.memo<{
|
|
|
53
54
|
borderLight: string;
|
|
54
55
|
textPrimary: string;
|
|
55
56
|
}>(({ item, itemWidth, cardBackground, borderLight, textPrimary }) => {
|
|
57
|
+
const { onPress: handlePress } = item;
|
|
58
|
+
|
|
56
59
|
const cardStyle = useMemo(
|
|
57
60
|
() => [styles.card, { width: itemWidth }],
|
|
58
61
|
[itemWidth]
|
|
@@ -74,7 +77,7 @@ const GridItem = React.memo<{
|
|
|
74
77
|
return (
|
|
75
78
|
<TouchableOpacity
|
|
76
79
|
activeOpacity={0.7}
|
|
77
|
-
onPress={
|
|
80
|
+
onPress={handlePress}
|
|
78
81
|
style={cardStyle}
|
|
79
82
|
>
|
|
80
83
|
<View style={iconBoxStyle}>
|
|
@@ -133,28 +136,57 @@ export const IconGrid = React.memo<IconGridProps>(({
|
|
|
133
136
|
[gap, rowGap, style]
|
|
134
137
|
);
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
// Memoize color props to prevent unnecessary GridItem re-renders
|
|
140
|
+
const colorProps = useMemo(() => ({
|
|
141
|
+
cardBackground,
|
|
142
|
+
borderLight,
|
|
143
|
+
textPrimary,
|
|
144
|
+
}), [cardBackground, borderLight, textPrimary]);
|
|
145
|
+
|
|
146
|
+
// Stable renderItem with memoization
|
|
147
|
+
const renderItem = useCallback(({ item }: { item: IconGridItem }) => {
|
|
148
|
+
if (itemWidth === 0) {
|
|
149
|
+
return <View key={item.id} style={{ width: 0, height: 0 }} />;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<GridItem
|
|
154
|
+
item={item}
|
|
155
|
+
itemWidth={itemWidth}
|
|
156
|
+
{...colorProps}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
}, [itemWidth, colorProps]);
|
|
160
|
+
|
|
161
|
+
const keyExtractor = useCallback((item: IconGridItem) => item.id, []);
|
|
162
|
+
|
|
163
|
+
const getItemLayout = useCallback((_?: unknown, index?: number) => ({
|
|
164
|
+
length: itemWidth,
|
|
165
|
+
offset: itemWidth * (index || 0),
|
|
166
|
+
index: index || 0,
|
|
167
|
+
}), [itemWidth]);
|
|
168
|
+
|
|
169
|
+
if (itemWidth === 0) {
|
|
170
|
+
return (
|
|
171
|
+
<View style={gridStyle} onLayout={handleLayout}>
|
|
172
|
+
{items.map((item) => (
|
|
173
|
+
<View key={item.id} style={{ width: 0, height: 0 }} />
|
|
174
|
+
))}
|
|
175
|
+
</View>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
140
178
|
|
|
141
179
|
return (
|
|
142
180
|
<View style={gridStyle} onLayout={handleLayout}>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
/>
|
|
153
|
-
) : (
|
|
154
|
-
// Placeholder — keeps grid stable before first layout measurement
|
|
155
|
-
<View key={item.id} style={placeholderStyle} />
|
|
156
|
-
),
|
|
157
|
-
)}
|
|
181
|
+
<FlatList
|
|
182
|
+
data={items}
|
|
183
|
+
renderItem={renderItem}
|
|
184
|
+
keyExtractor={keyExtractor}
|
|
185
|
+
getItemLayout={getItemLayout}
|
|
186
|
+
numColumns={columns}
|
|
187
|
+
scrollEnabled={false}
|
|
188
|
+
contentContainerStyle={gridStyle}
|
|
189
|
+
/>
|
|
158
190
|
</View>
|
|
159
191
|
);
|
|
160
192
|
});
|
package/src/offline/index.ts
CHANGED
|
@@ -17,7 +17,7 @@ export { useOfflineStore } from './infrastructure/storage/OfflineStore';
|
|
|
17
17
|
export { useOfflineConfigStore } from './infrastructure/storage/OfflineConfigStore';
|
|
18
18
|
|
|
19
19
|
// Hooks
|
|
20
|
-
export { useOffline
|
|
20
|
+
export { useOffline } from './presentation/hooks/useOffline';
|
|
21
21
|
export { useOfflineState } from './presentation/hooks/useOfflineState';
|
|
22
22
|
export { useOfflineWithMutations } from './presentation/hooks/useOfflineWithMutations';
|
|
23
23
|
|
|
@@ -38,14 +38,6 @@ const toNetworkState = (state: ExpoNetworkState): NetworkState => ({
|
|
|
38
38
|
details: null,
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
* Configure offline settings globally
|
|
43
|
-
* This is a facade over the config store for backward compatibility
|
|
44
|
-
*/
|
|
45
|
-
export const configureOffline = (config: OfflineConfig): void => {
|
|
46
|
-
useOfflineConfigStore.getState().setConfig(config);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
41
|
export const useOffline = (config?: OfflineConfig) => {
|
|
50
42
|
const store = useOfflineStore();
|
|
51
43
|
const globalConfig = useOfflineConfigStore((state) => state.config);
|