@umituz/react-native-design-system 4.27.16 → 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 (38) 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 +1 -1
  33. package/src/atoms/picker/components/PickerChips.tsx +27 -21
  34. package/src/image/presentation/components/ImageGallery.tsx +25 -21
  35. package/src/molecules/avatar/AvatarGroup.tsx +137 -62
  36. package/src/molecules/filter-group/FilterGroup.tsx +31 -22
  37. package/src/molecules/icon-grid/IconGrid.tsx +52 -20
  38. package/src/onboarding/presentation/components/BackgroundImageCollage.tsx +32 -23
@@ -2,18 +2,61 @@
2
2
  * Media Domain - Media Picker Service
3
3
  *
4
4
  * Service for picking images/videos using expo-image-picker.
5
- * Handles camera, gallery, and media library permissions.
5
+ * Refactored to use strategy pattern to reduce code duplication.
6
+ *
7
+ * Before: 182 LOC with 4 similar methods
8
+ * After: ~120 LOC with 1 generic launcher + convenience wrappers - 34% reduction
6
9
  */
7
- import type { MediaPickerOptions, MediaPickerResult, CameraOptions } from "../../domain/entities/Media";
10
+ import type { MediaPickerOptions, MediaPickerResult, CameraOptions } from '../../domain/entities/Media';
11
+ import type { PickerStrategy, LaunchOptions } from '../../domain/strategies/PickerStrategy';
8
12
  /**
9
13
  * Media picker service for selecting images/videos
14
+ * Uses strategy pattern to support different picker types
10
15
  */
11
16
  export declare class MediaPickerService {
17
+ /**
18
+ * Generic media picker launcher using strategy pattern
19
+ *
20
+ * @param strategy - Picker strategy to use
21
+ * @param options - Picker options
22
+ * @returns Picker result
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const strategy = new CameraPickerStrategy({ mediaType: 'images' });
27
+ * const result = await MediaPickerService.launchMediaPicker(strategy, {
28
+ * quality: 0.8,
29
+ * allowsEditing: true
30
+ * });
31
+ * ```
32
+ */
33
+ static launchMediaPicker(strategy: PickerStrategy, options?: LaunchOptions): Promise<MediaPickerResult>;
34
+ /**
35
+ * Launch camera for image capture
36
+ */
12
37
  static launchCamera(options?: CameraOptions): Promise<MediaPickerResult>;
38
+ /**
39
+ * Launch camera for video capture
40
+ */
13
41
  static launchCameraForVideo(options?: CameraOptions): Promise<MediaPickerResult>;
14
- static pickImage(options?: MediaPickerOptions): Promise<MediaPickerResult>;
15
- static pickSingleImage(options?: Omit<MediaPickerOptions, "allowsMultipleSelection">): Promise<MediaPickerResult>;
16
- static pickMultipleImages(options?: Omit<MediaPickerOptions, "allowsMultipleSelection">): Promise<MediaPickerResult>;
17
- static pickVideo(options?: Omit<MediaPickerOptions, "mediaTypes">): Promise<MediaPickerResult>;
42
+ /**
43
+ * Pick from library with file size validation
44
+ */
45
+ static pickFromLibrary(options?: MediaPickerOptions): Promise<MediaPickerResult>;
46
+ /**
47
+ * Pick single image from library
48
+ */
49
+ static pickSingleImage(options?: Omit<MediaPickerOptions, 'allowsMultipleSelection'>): Promise<MediaPickerResult>;
50
+ /**
51
+ * Pick multiple images from library
52
+ */
53
+ static pickMultipleImages(options?: Omit<MediaPickerOptions, 'allowsMultipleSelection'>): Promise<MediaPickerResult>;
54
+ /**
55
+ * Pick video from library
56
+ */
57
+ static pickVideo(options?: Omit<MediaPickerOptions, 'mediaTypes'>): Promise<MediaPickerResult>;
58
+ /**
59
+ * Pick any media from library
60
+ */
18
61
  static pickMedia(options?: MediaPickerOptions): Promise<MediaPickerResult>;
19
62
  }
@@ -2,6 +2,10 @@
2
2
  * Permission Manager
3
3
  *
4
4
  * Centralized permission handling for media operations.
5
+ * Refactored to use generic PermissionHandler to reduce duplication.
6
+ *
7
+ * Before: 4 similar methods (~80 LOC)
8
+ * After: 1 generic handler (~50 LOC) - 37% reduction
5
9
  */
6
10
  import { MediaLibraryPermission } from "../../domain/entities/Media";
7
11
  /**
@@ -10,8 +14,10 @@ import { MediaLibraryPermission } from "../../domain/entities/Media";
10
14
  export type PermissionType = 'camera' | 'mediaLibrary';
11
15
  /**
12
16
  * Permission manager for media operations
17
+ * Uses generic PermissionHandler to reduce code duplication
13
18
  */
14
19
  export declare class PermissionManager {
20
+ private static handler;
15
21
  /**
16
22
  * Requests camera permission
17
23
  */
@@ -7,7 +7,7 @@ import { MediaLibraryPermission, MediaType, type MediaPickerResult } from "../..
7
7
  /**
8
8
  * Map expo-image-picker permission status to MediaLibraryPermission
9
9
  */
10
- export declare const mapPermissionStatus: (status: ImagePicker.PermissionStatus) => MediaLibraryPermission;
10
+ export declare const mapPermissionStatus: (status: string) => MediaLibraryPermission;
11
11
  /**
12
12
  * Map MediaType to expo-image-picker media types
13
13
  */
@@ -16,3 +16,17 @@ export declare const mapMediaType: (type?: MediaType) => ImagePicker.MediaType[]
16
16
  * Map expo-image-picker result to MediaPickerResult
17
17
  */
18
18
  export declare const mapPickerResult: (result: ImagePicker.ImagePickerResult) => MediaPickerResult;
19
+ /**
20
+ * Map PickerStrategy result to MediaPickerResult
21
+ */
22
+ export declare const mapPickerResultFromStrategy: (result: {
23
+ canceled: boolean;
24
+ assets?: Array<{
25
+ uri: string;
26
+ width?: number;
27
+ height?: number;
28
+ type?: "image" | "video";
29
+ duration?: number;
30
+ fileSize?: number;
31
+ }>;
32
+ }) => MediaPickerResult;
@@ -17,6 +17,7 @@ import type { CalendarEvent } from '../../domain/entities/CalendarEvent.entity';
17
17
  * Follows SOLID principles with composition over inheritance.
18
18
  */
19
19
  export declare class CalendarService {
20
+ private static weekdayNamesCache;
20
21
  /**
21
22
  * Generate calendar days for a specific month
22
23
  */
@@ -30,7 +30,7 @@ export declare const useCalendar: () => {
30
30
  setCurrentMonth: (date: Date) => void;
31
31
  viewMode: import("./CalendarStore").CalendarViewMode;
32
32
  setViewMode: (mode: import("./CalendarStore").CalendarViewMode) => void;
33
- getEventsForDate: (date: Date) => import("../..").CalendarEvent[];
33
+ getEventsForDate: (date: Date | null | undefined) => import("../..").CalendarEvent[];
34
34
  getEventsForMonth: (year: number, month: number) => import("../..").CalendarEvent[];
35
35
  };
36
36
  /**
@@ -55,7 +55,7 @@ export declare const useCalendarStore: () => {
55
55
  navigateMonth: (direction: "prev" | "next") => void;
56
56
  setCurrentMonth: (date: Date) => void;
57
57
  setViewMode: (mode: import("./CalendarStore").CalendarViewMode) => void;
58
- getEventsForDate: (date: Date) => import("../..").CalendarEvent[];
58
+ getEventsForDate: (date: Date | null | undefined) => import("../..").CalendarEvent[];
59
59
  getEventsForMonth: (year: number, month: number) => import("../..").CalendarEvent[];
60
60
  clearError: () => void;
61
61
  clearAllEvents: () => Promise<void>;
@@ -1,3 +1,3 @@
1
1
  import React from 'react';
2
2
  import type { FilterGroupProps } from './types';
3
- export declare function FilterGroup<T = string>({ items, selectedValue, onSelect, multiSelect, style, contentContainerStyle, itemStyle, }: FilterGroupProps<T>): React.JSX.Element;
3
+ export declare const FilterGroup: React.MemoExoticComponent<(<T = string>({ items, selectedValue, onSelect, multiSelect, style, contentContainerStyle, itemStyle, }: FilterGroupProps<T>) => React.JSX.Element)>;
@@ -21,7 +21,7 @@ export type { NavigationCleanup } from "./utils/NavigationCleanup";
21
21
  */
22
22
  export { AppNavigation } from "./utils/AppNavigation";
23
23
  export { TabLabel, type TabLabelProps } from "./components/TabLabel";
24
- export * from "./components/NavigationHeader";
24
+ export { NavigationHeader, type NavigationHeaderProps } from "./components/NavigationHeader";
25
25
  export { useTabBarStyles, type TabBarConfig } from "./hooks/useTabBarStyles";
26
26
  export { useTabConfig, type UseTabConfigProps } from "./hooks/useTabConfig";
27
27
  /**
@@ -5,7 +5,7 @@
5
5
  export type { NetworkState, OfflineState, OfflineStore, OfflineConfig, ConnectionQuality, } from './types';
6
6
  export { useOfflineStore } from './infrastructure/storage/OfflineStore';
7
7
  export { useOfflineConfigStore } from './infrastructure/storage/OfflineConfigStore';
8
- export { useOffline, configureOffline } from './presentation/hooks/useOffline';
8
+ export { useOffline } from './presentation/hooks/useOffline';
9
9
  export { useOfflineState } from './presentation/hooks/useOfflineState';
10
10
  export { useOfflineWithMutations } from './presentation/hooks/useOfflineWithMutations';
11
11
  export { OfflineBanner } from './presentation/components/OfflineBanner';
@@ -4,11 +4,6 @@
4
4
  * Automatically subscribes to network changes via expo-network (lazy loaded)
5
5
  */
6
6
  import type { OfflineConfig } from '../../types';
7
- /**
8
- * Configure offline settings globally
9
- * This is a facade over the config store for backward compatibility
10
- */
11
- export declare const configureOffline: (config: OfflineConfig) => void;
12
7
  export declare const useOffline: (config?: OfflineConfig) => {
13
8
  isOnline: boolean;
14
9
  isOffline: boolean;
@@ -3,7 +3,7 @@
3
3
  * Domain layer - Abstract repository for data operations
4
4
  *
5
5
  * Provides generic CRUD operations with TanStack Query integration.
6
- * Subclass this for specific entities to get type-safe data operations.
6
+ * Now uses common repository utilities from core/repositories.
7
7
  *
8
8
  * @example
9
9
  * ```typescript
@@ -51,6 +51,10 @@ export declare abstract class BaseRepository<TData, TCreateVariables = unknown,
51
51
  * Query key factory for this repository
52
52
  */
53
53
  readonly keys: ReturnType<typeof createQueryKeyFactory>;
54
+ /**
55
+ * Debug logger for this repository
56
+ */
57
+ protected readonly log: (method: string, ...args: unknown[]) => void;
54
58
  constructor(resource: string, options?: RepositoryOptions);
55
59
  /**
56
60
  * Get query client instance
@@ -14,6 +14,7 @@ declare class DevMonitorClass {
14
14
  private statsInterval;
15
15
  private cacheSubscription;
16
16
  private isEnabled;
17
+ private maxMetrics;
17
18
  constructor(options?: DevMonitorOptions);
18
19
  private init;
19
20
  private trackQuery;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Time Constants
3
+ *
4
+ * Centralized time-related constants to replace magic numbers
5
+ */
6
+ export declare const MILLISECONDS_PER_SECOND = 1000;
7
+ export declare const MILLISECONDS_PER_MINUTE: number;
8
+ export declare const MILLISECONDS_PER_HOUR: number;
9
+ export declare const MILLISECONDS_PER_DAY: number;
10
+ export declare const SECONDS_PER_MINUTE = 60;
11
+ export declare const SECONDS_PER_HOUR: number;
12
+ export declare const SECONDS_PER_DAY: number;
13
+ export declare const MINUTES_PER_HOUR = 60;
14
+ export declare const MINUTES_PER_DAY: number;
15
+ export declare const ONE_SECOND_MS = 1000;
16
+ export declare const FIVE_SECONDS_MS: number;
17
+ export declare const TEN_SECONDS_MS: number;
18
+ export declare const THIRTY_SECONDS_MS: number;
19
+ export declare const ONE_MINUTE_MS: number;
20
+ export declare const FIVE_MINUTES_MS: number;
21
+ export declare const TEN_MINUTES_MS: number;
22
+ export declare const THIRTY_MINUTES_MS: number;
23
+ export declare const ONE_HOUR_MS: number;
24
+ export declare const ONE_DAY_MS: number;
25
+ export declare const DEFAULT_TIMEOUT_MS: number;
26
+ export declare const DEFAULT_LONG_TIMEOUT_MS: number;
27
+ export declare const DEFAULT_CACHE_TTL_MS: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-design-system",
3
- "version": "4.27.16",
3
+ "version": "4.27.17",
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 - TanStack persistence and expo-image-manipulator now lazy loaded",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./dist/index.d.ts",
@@ -5,7 +5,7 @@
5
5
  * Extracted from AtomicPicker for better separation of concerns.
6
6
  */
7
7
 
8
- import React from 'react';
8
+ import React, { useMemo, useCallback } from 'react';
9
9
  import { View, TouchableOpacity, GestureResponderEvent } from 'react-native';
10
10
  import { useAppDesignTokens } from '../../../theme';
11
11
  import { PickerOption } from '../types';
@@ -30,30 +30,36 @@ export const PickerChips: React.FC<PickerChipsProps> = React.memo(({
30
30
  const tokens = useAppDesignTokens();
31
31
  const closeIcon = useIconName('close');
32
32
 
33
- const chipContainerStyles = getChipContainerStyles(tokens);
34
- const chipStyles = getChipStyles(tokens);
35
- const chipTextStyles = getChipTextStyles(tokens);
33
+ // Memoize styles to prevent recalculation
34
+ const chipContainerStyles = useMemo(() => getChipContainerStyles(tokens), [tokens]);
35
+ const chipStyles = useMemo(() => getChipStyles(tokens), [tokens]);
36
+ const chipTextStyles = useMemo(() => getChipTextStyles(tokens), [tokens]);
36
37
 
37
- if (selectedOptions.length === 0) return null;
38
+ // Memoized chip renderer - handleRemove created inline to avoid useCallback inside callback
39
+ const renderChip = useCallback((opt: PickerOption) => {
40
+ const handleRemove = (e: GestureResponderEvent) => {
41
+ e.stopPropagation();
42
+ onRemoveChip(opt.value);
43
+ };
44
+
45
+ return (
46
+ <View key={opt.value} style={chipStyles}>
47
+ <AtomicText style={chipTextStyles}>{opt.label}</AtomicText>
48
+ <TouchableOpacity
49
+ onPress={handleRemove}
50
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
51
+ accessibilityRole="button"
52
+ accessibilityLabel={`Remove ${opt.label}`}
53
+ >
54
+ <AtomicIcon name={closeIcon} size="sm" color="primary" />
55
+ </TouchableOpacity>
56
+ </View>
57
+ );
58
+ }, [chipStyles, chipTextStyles, closeIcon, onRemoveChip]);
38
59
 
39
60
  return (
40
61
  <View style={chipContainerStyles}>
41
- {selectedOptions.map((opt) => (
42
- <View key={opt.value} style={chipStyles}>
43
- <AtomicText style={chipTextStyles}>{opt.label}</AtomicText>
44
- <TouchableOpacity
45
- onPress={(e: GestureResponderEvent) => {
46
- e.stopPropagation();
47
- onRemoveChip(opt.value);
48
- }}
49
- hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
50
- accessibilityRole="button"
51
- accessibilityLabel={`Remove ${opt.label}`}
52
- >
53
- <AtomicIcon name={closeIcon} size="sm" color="primary" />
54
- </TouchableOpacity>
55
- </View>
56
- ))}
62
+ {selectedOptions.map(renderChip)}
57
63
  </View>
58
64
  );
59
65
  });
@@ -20,6 +20,29 @@ try {
20
20
  // expo-image not installed — using React Native Image fallback
21
21
  }
22
22
 
23
+ // Memoized image component (outside main component to avoid dependency issues)
24
+ const GalleryImage = React.memo<{ item: ImageViewerItem; style: any }>(({ item, style }) => (
25
+ <View style={style.imageWrapper}>
26
+ {ExpoImage ? (
27
+ <ExpoImage
28
+ source={{ uri: item.uri }}
29
+ style={style.fullImage}
30
+ contentFit="contain"
31
+ cachePolicy="memory-disk"
32
+ onError={() => { if (__DEV__) console.warn('[ImageGallery] Failed to load image:', item.uri); }}
33
+ />
34
+ ) : (
35
+ <RNImage
36
+ source={{ uri: item.uri }}
37
+ style={style.fullImage}
38
+ resizeMode="contain"
39
+ onError={() => { if (__DEV__) console.warn('[ImageGallery] Failed to load image:', item.uri); }}
40
+ />
41
+ )}
42
+ </View>
43
+ ));
44
+ GalleryImage.displayName = 'GalleryImage';
45
+
23
46
  export interface ImageGalleryProps extends ImageGalleryOptions {
24
47
  images: ImageViewerItem[];
25
48
  visible: boolean;
@@ -44,7 +67,6 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
44
67
  const insets = useSafeAreaInsets();
45
68
  const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = useWindowDimensions();
46
69
  const currentIndexRef = useRef(index);
47
- const [, forceRender] = React.useReducer((x: number) => x + 1, 0);
48
70
 
49
71
  const styles = useMemo(() => StyleSheet.create({
50
72
  container: { flex: 1 },
@@ -81,30 +103,12 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
81
103
  if (nextIndex !== currentIndexRef.current) {
82
104
  currentIndexRef.current = nextIndex;
83
105
  onIndexChange?.(nextIndex);
84
- forceRender();
85
106
  }
86
107
  }, [onIndexChange, SCREEN_WIDTH, images.length]);
87
108
 
88
109
  const renderItem = useCallback(({ item }: { item: ImageViewerItem }) => (
89
- <View style={styles.imageWrapper}>
90
- {ExpoImage ? (
91
- <ExpoImage
92
- source={{ uri: item.uri }}
93
- style={styles.fullImage}
94
- contentFit="contain"
95
- cachePolicy="memory-disk"
96
- onError={() => { if (__DEV__) console.warn('[ImageGallery] Failed to load image:', item.uri); }}
97
- />
98
- ) : (
99
- <RNImage
100
- source={{ uri: item.uri }}
101
- style={styles.fullImage}
102
- resizeMode="contain"
103
- onError={() => { if (__DEV__) console.warn('[ImageGallery] Failed to load image:', item.uri); }}
104
- />
105
- )}
106
- </View>
107
- ), [styles]);
110
+ <GalleryImage item={item} style={{ imageWrapper: styles.imageWrapper, fullImage: styles.fullImage }} />
111
+ ), [styles.imageWrapper, styles.fullImage]);
108
112
 
109
113
  const getItemLayout = useCallback((_: unknown, i: number) => ({
110
114
  length: SCREEN_WIDTH,
@@ -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: {