@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.
Files changed (78) hide show
  1. package/dist/core/cache/domain/CleanupStrategy.d.ts +62 -0
  2. package/dist/core/cache/domain/UnifiedCache.d.ts +82 -0
  3. package/dist/core/cache/domain/types.d.ts +15 -0
  4. package/dist/core/cache/index.d.ts +13 -0
  5. package/dist/core/cache/infrastructure/CacheFactory.d.ts +63 -0
  6. package/dist/core/index.d.ts +17 -0
  7. package/dist/core/permissions/domain/PermissionHandler.d.ts +82 -0
  8. package/dist/core/permissions/domain/types.d.ts +42 -0
  9. package/dist/core/permissions/index.d.ts +7 -0
  10. package/dist/core/repositories/domain/RepositoryKeyFactory.d.ts +25 -0
  11. package/dist/core/repositories/domain/RepositoryUtils.d.ts +39 -0
  12. package/dist/core/repositories/domain/types.d.ts +53 -0
  13. package/dist/core/repositories/index.d.ts +10 -0
  14. package/dist/hooks/index.d.ts +27 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/media/domain/strategies/CameraPickerStrategy.d.ts +25 -0
  17. package/dist/media/domain/strategies/LibraryPickerStrategy.d.ts +18 -0
  18. package/dist/media/domain/strategies/PickerStrategy.d.ts +55 -0
  19. package/dist/media/domain/strategies/index.d.ts +10 -0
  20. package/dist/media/infrastructure/services/MediaPickerService.d.ts +49 -6
  21. package/dist/media/infrastructure/utils/PermissionManager.d.ts +6 -0
  22. package/dist/media/infrastructure/utils/mediaPickerMappers.d.ts +15 -1
  23. package/dist/molecules/calendar/infrastructure/services/CalendarService.d.ts +1 -0
  24. package/dist/molecules/calendar/infrastructure/storage/CalendarStore.d.ts +2 -2
  25. package/dist/molecules/filter-group/FilterGroup.d.ts +1 -1
  26. package/dist/molecules/navigation/index.d.ts +1 -1
  27. package/dist/offline/index.d.ts +1 -1
  28. package/dist/offline/presentation/hooks/useOffline.d.ts +0 -5
  29. package/dist/tanstack/domain/repositories/BaseRepository.d.ts +5 -1
  30. package/dist/tanstack/infrastructure/monitoring/DevMonitor.d.ts +1 -0
  31. package/dist/utils/constants/TimeConstants.d.ts +27 -0
  32. package/package.json +11 -6
  33. package/src/atoms/GlassView/GlassView.tsx +0 -2
  34. package/src/atoms/picker/components/PickerChips.tsx +27 -21
  35. package/src/core/cache/index.ts +3 -2
  36. package/src/core/index.ts +5 -3
  37. package/src/core/repositories/domain/RepositoryUtils.ts +4 -4
  38. package/src/core/repositories/domain/types.ts +2 -2
  39. package/src/hooks/index.ts +22 -25
  40. package/src/image/presentation/components/ImageGallery.tsx +25 -21
  41. package/src/infinite-scroll/presentation/hooks/useInfiniteScroll.ts +32 -29
  42. package/src/media/domain/strategies/CameraPickerStrategy.ts +4 -8
  43. package/src/media/domain/strategies/index.ts +1 -1
  44. package/src/media/infrastructure/services/MediaPickerService.ts +3 -14
  45. package/src/media/infrastructure/utils/mediaPickerMappers.ts +29 -6
  46. package/src/molecules/avatar/AvatarGroup.tsx +137 -62
  47. package/src/molecules/filter-group/FilterGroup.tsx +31 -22
  48. package/src/molecules/icon-grid/IconGrid.tsx +52 -20
  49. package/src/molecules/swipe-actions/domain/entities/SwipeAction.ts +0 -1
  50. package/src/offline/index.ts +1 -1
  51. package/src/offline/presentation/hooks/useOffline.ts +0 -8
  52. package/src/onboarding/presentation/components/BackgroundImageCollage.tsx +32 -23
  53. package/src/storage/README.md +0 -1
  54. package/src/storage/cache/domain/types/README.md +0 -1
  55. package/src/storage/cache/presentation/README.md +0 -1
  56. package/src/storage/cache/presentation/useCachedValue.ts +12 -2
  57. package/src/storage/domain/constants/README.md +0 -1
  58. package/src/storage/infrastructure/adapters/README.md +0 -1
  59. package/src/storage/presentation/hooks/README.md +0 -6
  60. package/src/storage/presentation/hooks/useStorageState.ts +13 -4
  61. package/src/tanstack/domain/repositories/BaseRepository.ts +1 -1
  62. package/src/theme/hooks/useAppDesignTokens.ts +29 -3
  63. package/src/timezone/infrastructure/services/TimezoneProvider.ts +2 -2
  64. package/src/init/index.ts +0 -29
  65. package/src/layouts/ScreenLayout/index.ts +0 -1
  66. package/src/layouts/index.ts +0 -5
  67. package/src/molecules/SearchBar/index.ts +0 -4
  68. package/src/molecules/StepProgress/index.ts +0 -1
  69. package/src/molecules/alerts/index.ts +0 -47
  70. package/src/molecules/bottom-sheet/index.ts +0 -10
  71. package/src/molecules/circular-menu/index.ts +0 -3
  72. package/src/molecules/filter-group/index.ts +0 -3
  73. package/src/molecules/index.ts +0 -38
  74. package/src/molecules/info-grid/index.ts +0 -3
  75. package/src/molecules/swipe-actions/index.ts +0 -6
  76. package/src/presentation/utils/variants/index.ts +0 -6
  77. package/src/timezone/infrastructure/utils/SimpleCache.ts +0 -64
  78. 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) loadInitial();
169
- }, [autoLoad, loadInitial]);
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' ? ['videos'] : ['images'];
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 { mapPickerResult } from '../utils/mediaPickerMappers';
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 result = await strategy.launch(options ?? {});
66
- return mapPickerResult(result);
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: ImagePicker.PermissionStatus
18
+ status: string
19
19
  ): MediaLibraryPermission => {
20
20
  switch (status) {
21
- case ImagePicker.PermissionStatus.GRANTED:
21
+ case 'granted':
22
22
  return MediaLibraryPermission.GRANTED;
23
- case ImagePicker.PermissionStatus.DENIED:
24
- return MediaLibraryPermission.DENIED;
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
- export const AvatarGroup: React.FC<AvatarGroupProps> = ({
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
- // Calculate visible avatars and overflow count
55
- const visibleItems = items.slice(0, maxVisible);
56
- const overflowCount = items.length - maxVisible;
57
- const hasOverflow = overflowCount > 0;
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
- <View
63
- key={item.uri || item.name || item.icon || `avatar-${index}`}
64
- style={[
65
- styles.avatarWrapper,
66
- index > 0 && { marginLeft: spacing },
67
- ]}
68
- >
69
- <Avatar
70
- uri={item.uri}
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
- <View
88
- style={[
89
- styles.avatarWrapper,
90
- { marginLeft: spacing },
91
- ]}
92
- >
93
- <View
94
- style={[
95
- styles.overflow,
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((item) => {
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={item.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
- const placeholderStyle = useMemo(
137
- () => ({ width: 0, height: 0 }),
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
- {items.map((item) =>
144
- itemWidth > 0 ? (
145
- <GridItem
146
- key={item.id}
147
- item={item}
148
- itemWidth={itemWidth}
149
- cardBackground={cardBackground}
150
- borderLight={borderLight}
151
- textPrimary={textPrimary}
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
  });
@@ -69,7 +69,6 @@ export const DEFAULT_SWIPE_CONFIG: Required<Omit<SwipeableConfig, 'leftActions'
69
69
  friction: 2,
70
70
  };
71
71
 
72
- // Re-export utilities for backward compatibility
73
72
  export {
74
73
  ACTION_PRESETS,
75
74
  getPreset,
@@ -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, configureOffline } from './presentation/hooks/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);