@umituz/react-native-design-system 4.28.11 → 4.28.12

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 (47) hide show
  1. package/package.json +31 -8
  2. package/src/atoms/AtomicAvatar.tsx +69 -40
  3. package/src/atoms/AtomicSpinner.tsx +24 -22
  4. package/src/atoms/AtomicText.tsx +32 -27
  5. package/src/atoms/AtomicTextArea.tsx +17 -15
  6. package/src/atoms/EmptyState.tsx +44 -41
  7. package/src/atoms/button/AtomicButton.tsx +8 -9
  8. package/src/atoms/card/AtomicCard.tsx +26 -8
  9. package/src/atoms/datepicker/components/DatePickerButton.tsx +8 -8
  10. package/src/atoms/datepicker/components/DatePickerModal.tsx +7 -7
  11. package/src/atoms/fab/styles/fabStyles.ts +0 -21
  12. package/src/atoms/icon/index.ts +6 -20
  13. package/src/atoms/picker/components/PickerModal.tsx +24 -4
  14. package/src/atoms/skeleton/AtomicSkeleton.tsx +9 -11
  15. package/src/carousel/Carousel.tsx +43 -20
  16. package/src/carousel/carouselCalculations.ts +12 -9
  17. package/src/carousel/index.ts +0 -1
  18. package/src/device/detection/iPadDetection.ts +5 -14
  19. package/src/device/infrastructure/services/DeviceFeatureService.ts +89 -9
  20. package/src/device/infrastructure/services/DeviceInfoService.ts +33 -0
  21. package/src/device/infrastructure/services/UserFriendlyIdService.ts +8 -6
  22. package/src/device/infrastructure/utils/__tests__/stringUtils.test.ts +56 -20
  23. package/src/device/infrastructure/utils/nativeModuleUtils.ts +16 -2
  24. package/src/device/infrastructure/utils/stringUtils.ts +51 -5
  25. package/src/filesystem/domain/utils/FileUtils.ts +5 -1
  26. package/src/image/domain/utils/ImageUtils.ts +6 -0
  27. package/src/layouts/AppHeader/AppHeader.tsx +13 -3
  28. package/src/layouts/Container/Container.tsx +19 -1
  29. package/src/layouts/FormLayout/FormLayout.tsx +20 -1
  30. package/src/layouts/Grid/Grid.tsx +34 -4
  31. package/src/layouts/ScreenHeader/ScreenHeader.tsx +4 -0
  32. package/src/layouts/ScreenLayout/ScreenLayout.tsx +42 -3
  33. package/src/molecules/SearchBar/SearchBar.tsx +27 -23
  34. package/src/molecules/action-footer/ActionFooter.tsx +32 -31
  35. package/src/molecules/alerts/AlertService.ts +60 -15
  36. package/src/molecules/avatar/Avatar.tsx +3 -3
  37. package/src/molecules/avatar/AvatarGroup.tsx +7 -7
  38. package/src/molecules/calendar/infrastructure/utils/DateUtilities.ts +12 -1
  39. package/src/molecules/calendar/presentation/components/CalendarDayCell.tsx +48 -32
  40. package/src/molecules/info-grid/InfoGrid.tsx +5 -3
  41. package/src/organisms/FormContainer.tsx +11 -1
  42. package/src/tanstack/domain/utils/ErrorHelpers.ts +2 -2
  43. package/src/tanstack/domain/utils/MetricsCalculator.ts +6 -1
  44. package/src/theme/core/colors/ColorUtils.ts +7 -4
  45. package/src/utils/formatters/stringFormatter.ts +18 -3
  46. package/src/utils/index.ts +6 -4
  47. package/src/utils/math/CalculationUtils.ts +10 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-design-system",
3
- "version": "4.28.11",
3
+ "version": "4.28.12",
4
4
  "description": "Universal design system for React Native apps with safe navigation hooks - updated SKILL.md with navigation documentation",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./dist/index.d.ts",
@@ -178,15 +178,10 @@
178
178
  "build": "tsc --project tsconfig.build.json",
179
179
  "setup:skill": "node -e \"const fs = require('fs'); const path = require('path'); const skillDir = path.join(process.env.HOME, '.claude', 'skills', 'react-native-design-system'); fs.mkdirSync(skillDir, {recursive: true}); fs.copyFileSync(path.join(__dirname, 'skills/SKILL.md'), path.join(skillDir, 'SKILL.md')); console.log('✅ @umituz/react-native-design-system setup skill installed to Claude Code!');\""
180
180
  },
181
- "dependencies": {
182
- "@react-native-async-storage/async-storage": "^2.2.0",
183
- "react-native-svg": "^15.12.1",
184
- "rn-emoji-keyboard": "^1.7.0"
185
- },
181
+ "dependencies": {},
186
182
  "optionalDependencies": {
187
183
  "@tanstack/query-async-storage-persister": "^5.0.0",
188
- "@tanstack/react-query-persist-client": "^5.0.0",
189
- "expo-image-manipulator": "~14.0.0"
184
+ "@tanstack/react-query-persist-client": "^5.0.0"
190
185
  },
191
186
  "keywords": [
192
187
  "react-native",
@@ -213,6 +208,7 @@
213
208
  "url": "https://github.com/umituz/react-native-design-system"
214
209
  },
215
210
  "peerDependencies": {
211
+ "@react-native-async-storage/async-storage": ">=2.0.0",
216
212
  "@react-native-community/datetimepicker": ">=8.0.0",
217
213
  "@react-navigation/bottom-tabs": ">=7.0.0",
218
214
  "@react-navigation/native": ">=7.0.0",
@@ -223,13 +219,17 @@
223
219
  "expo": ">=54.0.0",
224
220
  "expo-application": ">=5.0.0",
225
221
  "expo-clipboard": ">=8.0.0",
222
+ "expo-constants": ">=18.0.0",
226
223
  "expo-crypto": ">=13.0.0",
227
224
  "expo-device": ">=5.0.0",
225
+ "expo-file-system": ">=19.0.0",
228
226
  "expo-font": ">=12.0.0",
229
227
  "expo-haptics": ">=14.0.0",
230
228
  "expo-image": ">=3.0.0",
231
229
  "expo-image-manipulator": ">=14.0.0",
232
230
  "expo-image-picker": ">=14.0.0",
231
+ "expo-localization": ">=17.0.0",
232
+ "expo-media-library": ">=18.0.0",
233
233
  "expo-network": ">=8.0.0",
234
234
  "expo-secure-store": ">=14.0.0",
235
235
  "expo-sharing": ">=12.0.0",
@@ -238,9 +238,14 @@
238
238
  "react-native-gesture-handler": ">=2.20.0",
239
239
  "react-native-keyboard-controller": ">=1.0.0",
240
240
  "react-native-safe-area-context": ">=5.6.2",
241
+ "react-native-svg": ">=15.0.0",
242
+ "rn-emoji-keyboard": ">=1.0.0",
241
243
  "zustand": ">=5.0.0"
242
244
  },
243
245
  "peerDependenciesMeta": {
246
+ "@react-native-async-storage/async-storage": {
247
+ "optional": true
248
+ },
244
249
  "react-native-keyboard-controller": {
245
250
  "optional": true
246
251
  },
@@ -265,9 +270,15 @@
265
270
  "expo-clipboard": {
266
271
  "optional": true
267
272
  },
273
+ "expo-constants": {
274
+ "optional": true
275
+ },
268
276
  "expo-crypto": {
269
277
  "optional": true
270
278
  },
279
+ "expo-file-system": {
280
+ "optional": true
281
+ },
271
282
  "expo-font": {
272
283
  "optional": true
273
284
  },
@@ -283,6 +294,12 @@
283
294
  "expo-image-picker": {
284
295
  "optional": true
285
296
  },
297
+ "expo-localization": {
298
+ "optional": true
299
+ },
300
+ "expo-media-library": {
301
+ "optional": true
302
+ },
286
303
  "expo-network": {
287
304
  "optional": true
288
305
  },
@@ -292,6 +309,12 @@
292
309
  "expo-sharing": {
293
310
  "optional": true
294
311
  },
312
+ "react-native-svg": {
313
+ "optional": true
314
+ },
315
+ "rn-emoji-keyboard": {
316
+ "optional": true
317
+ },
295
318
  "@react-native-community/datetimepicker": {
296
319
  "optional": true
297
320
  }
@@ -14,11 +14,37 @@
14
14
  * - Default user placeholders
15
15
  */
16
16
 
17
- import React from 'react';
17
+ import React, { useMemo } from 'react';
18
18
  import { View, Image, StyleSheet, ViewStyle, ImageStyle, ImageSourcePropType } from 'react-native';
19
19
  import { AtomicText } from './AtomicText';
20
20
  import { useAppDesignTokens } from '../theme';
21
21
 
22
+ // =============================================================================
23
+ // UTILITY FUNCTIONS (moved outside component)
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Generate initials from name
28
+ */
29
+ const getInitials = (name: string): string => {
30
+ return name
31
+ .split(' ')
32
+ .map(word => word.charAt(0))
33
+ .join('')
34
+ .toUpperCase()
35
+ .slice(0, 2);
36
+ };
37
+
38
+ /**
39
+ * Calculate font size based on avatar size
40
+ */
41
+ const getAvatarFontSize = (sizeValue: number, spacingMultiplier: number): number => {
42
+ const baseFontSize = sizeValue <= 32 ? 12 :
43
+ sizeValue <= 48 ? 16 :
44
+ sizeValue <= 64 ? 20 : 24;
45
+ return baseFontSize * spacingMultiplier;
46
+ };
47
+
22
48
  // =============================================================================
23
49
  // TYPE DEFINITIONS
24
50
  // =============================================================================
@@ -70,49 +96,58 @@ export const AtomicAvatar: React.FC<AtomicAvatarProps> = React.memo(({
70
96
  }) => {
71
97
  const tokens = useAppDesignTokens();
72
98
 
73
- const avatarSize = customSize ? customSize * tokens.spacingMultiplier : tokens.avatarSizes[size];
74
- const avatarRadius = borderRadius ?? avatarSize / 2;
75
-
76
- // Generate initials from name
77
- const getInitials = (name: string): string => {
78
- return name
79
- .split(' ')
80
- .map(word => word.charAt(0))
81
- .join('')
82
- .toUpperCase()
83
- .slice(0, 2);
84
- };
85
-
86
- // Default colors
87
- const defaultBackgroundColor = backgroundColor || tokens.colors.primary;
88
- const defaultTextColor = textColor || tokens.colors.onPrimary;
89
- const defaultBorderColor = borderColor || tokens.colors.border;
90
-
91
- const avatarStyle: ViewStyle = {
99
+ const avatarSize = useMemo(() =>
100
+ customSize ? customSize * tokens.spacingMultiplier : tokens.avatarSizes[size],
101
+ [customSize, size, tokens.spacingMultiplier, tokens.avatarSizes]
102
+ );
103
+
104
+ const avatarRadius = useMemo(() =>
105
+ borderRadius ?? avatarSize / 2,
106
+ [borderRadius, avatarSize]
107
+ );
108
+
109
+ const defaultBackgroundColor = useMemo(() =>
110
+ backgroundColor || tokens.colors.primary,
111
+ [backgroundColor, tokens.colors.primary]
112
+ );
113
+
114
+ const defaultTextColor = useMemo(() =>
115
+ textColor || tokens.colors.onPrimary,
116
+ [textColor, tokens.colors.onPrimary]
117
+ );
118
+
119
+ const defaultBorderColor = useMemo(() =>
120
+ borderColor || tokens.colors.border,
121
+ [borderColor, tokens.colors.border]
122
+ );
123
+
124
+ const avatarStyle = useMemo<ViewStyle>(() => ({
92
125
  width: avatarSize,
93
126
  height: avatarSize,
94
127
  borderRadius: avatarRadius,
95
128
  backgroundColor: defaultBackgroundColor,
96
129
  borderWidth,
97
130
  borderColor: defaultBorderColor,
98
- alignItems: 'center',
99
- justifyContent: 'center',
131
+ alignItems: 'center' as const,
132
+ justifyContent: 'center' as const,
100
133
  overflow: 'hidden',
101
- };
134
+ }), [avatarSize, avatarRadius, defaultBackgroundColor, borderWidth, defaultBorderColor]);
102
135
 
103
- const imageStyleFinal: ImageStyle = {
136
+ const imageStyleFinal = useMemo<ImageStyle>(() => ({
104
137
  width: avatarSize,
105
138
  height: avatarSize,
106
139
  borderRadius: avatarRadius,
107
- };
140
+ }), [avatarSize, avatarRadius]);
141
+
142
+ const avatarFontSize = useMemo(() =>
143
+ getAvatarFontSize(avatarSize, tokens.spacingMultiplier),
144
+ [avatarSize, tokens.spacingMultiplier]
145
+ );
108
146
 
109
- // Font size based on avatar size (scaled)
110
- const getAvatarFontSize = (sizeValue: number): number => {
111
- const baseFontSize = sizeValue <= 32 ? 12 :
112
- sizeValue <= 48 ? 16 :
113
- sizeValue <= 64 ? 20 : 24;
114
- return baseFontSize * tokens.spacingMultiplier;
115
- };
147
+ const textStyle = useMemo(() => ({
148
+ fontSize: avatarFontSize,
149
+ fontWeight: tokens.typography.semibold,
150
+ }), [avatarFontSize, tokens.typography.semibold]);
116
151
 
117
152
  return (
118
153
  <View
@@ -131,10 +166,7 @@ export const AtomicAvatar: React.FC<AtomicAvatarProps> = React.memo(({
131
166
  <AtomicText
132
167
  type="labelLarge"
133
168
  color={defaultTextColor}
134
- style={{
135
- fontSize: getAvatarFontSize(avatarSize),
136
- fontWeight: tokens.typography.semibold,
137
- }}
169
+ style={textStyle}
138
170
  >
139
171
  {getInitials(name)}
140
172
  </AtomicText>
@@ -142,10 +174,7 @@ export const AtomicAvatar: React.FC<AtomicAvatarProps> = React.memo(({
142
174
  <AtomicText
143
175
  type="labelLarge"
144
176
  color={defaultTextColor}
145
- style={{
146
- fontSize: getAvatarFontSize(avatarSize),
147
- fontWeight: tokens.typography.semibold,
148
- }}
177
+ style={textStyle}
149
178
  >
150
179
  ?
151
180
  </AtomicText>
@@ -13,7 +13,7 @@
13
13
  * - Async operation feedback
14
14
  */
15
15
 
16
- import React from 'react';
16
+ import React, { useMemo } from 'react';
17
17
  import { View, ActivityIndicator, StyleSheet, ViewStyle } from 'react-native';
18
18
  import { useAppDesignTokens } from '../theme';
19
19
  import { AtomicText } from './AtomicText';
@@ -84,13 +84,13 @@ export const AtomicSpinner: React.FC<AtomicSpinnerProps> = ({
84
84
  // Resolve size (scaled)
85
85
  const baseSize = typeof size === 'number' ? size : SIZE_MAP[size];
86
86
  const resolvedSize = baseSize * tokens.spacingMultiplier;
87
-
87
+
88
88
  const activitySize = typeof size === 'number'
89
89
  ? (size >= 30 ? 'large' : 'small')
90
90
  : ACTIVITY_SIZE_MAP[size];
91
91
 
92
- // Resolve color
93
- const resolveColor = (): string => {
92
+ // Resolve color (memoized)
93
+ const spinnerColor = useMemo(() => {
94
94
  if (color.startsWith('#') || color.startsWith('rgb')) {
95
95
  return color;
96
96
  }
@@ -103,26 +103,34 @@ export const AtomicSpinner: React.FC<AtomicSpinnerProps> = ({
103
103
  white: '#FFFFFF',
104
104
  };
105
105
  return colorMap[color as SpinnerColor] || tokens.colors.primary;
106
- };
106
+ }, [color, tokens.colors.primary, tokens.colors.secondary, tokens.colors.success, tokens.colors.error, tokens.colors.warning]);
107
107
 
108
- const spinnerColor = resolveColor();
109
108
  const resolvedOverlayColor = overlayColor || 'rgba(0, 0, 0, 0.5)';
110
109
 
111
- // Container styles
112
- const containerStyles: ViewStyle[] = [
110
+ // Container styles (memoized)
111
+ const containerStyles = useMemo<ViewStyle[]>(() => [
113
112
  styles.container,
114
- textPosition === 'right' && { flexDirection: 'row' },
113
+ textPosition === 'right' && { flexDirection: 'row' as const },
115
114
  fullContainer && styles.fullContainer,
116
115
  overlay && [styles.overlay, { backgroundColor: resolvedOverlayColor }],
117
- ].filter(Boolean) as ViewStyle[];
116
+ ].filter(Boolean) as ViewStyle[], [textPosition, fullContainer, overlay, resolvedOverlayColor]);
118
117
 
119
- // Spinner wrapper styles
120
- const spinnerWrapperStyles: ViewStyle = {
118
+ // Spinner wrapper styles (memoized)
119
+ const spinnerWrapperStyles = useMemo<ViewStyle>(() => ({
121
120
  width: resolvedSize,
122
121
  height: resolvedSize,
123
- justifyContent: 'center',
124
- alignItems: 'center',
125
- };
122
+ justifyContent: 'center' as const,
123
+ alignItems: 'center' as const,
124
+ }), [resolvedSize]);
125
+
126
+ // Text style (memoized)
127
+ const textStyle = useMemo(() => [
128
+ styles.text,
129
+ textPosition === 'right'
130
+ ? { marginLeft: 12 * tokens.spacingMultiplier }
131
+ : { marginTop: 12 * tokens.spacingMultiplier },
132
+ { color: overlay ? '#FFFFFF' : tokens.colors.textSecondary },
133
+ ], [textPosition, overlay, tokens.spacingMultiplier, tokens.colors.textSecondary]);
126
134
 
127
135
  return (
128
136
  <View
@@ -142,13 +150,7 @@ export const AtomicSpinner: React.FC<AtomicSpinnerProps> = ({
142
150
  {text && (
143
151
  <AtomicText
144
152
  type="bodyMedium"
145
- style={[
146
- styles.text,
147
- textPosition === 'right'
148
- ? { marginLeft: 12 * tokens.spacingMultiplier }
149
- : { marginTop: 12 * tokens.spacingMultiplier },
150
- { color: overlay ? '#FFFFFF' : tokens.colors.textSecondary },
151
- ]}
153
+ style={textStyle}
152
154
  >
153
155
  {text}
154
156
  </AtomicText>
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { Text, type StyleProp, type TextStyle, type TextProps } from 'react-native';
3
3
  import { useAppDesignTokens } from '../theme';
4
4
  import { getTextColor, type TextStyleVariant, type ColorVariant } from '../typography';
@@ -36,12 +36,13 @@ export interface AtomicTextProps extends Omit<TextProps, 'style'> {
36
36
 
37
37
  /**
38
38
  * AtomicText - Primitive Text Component
39
- *
39
+ *
40
40
  * ✅ Responsive by default
41
41
  * ✅ Theme-aware
42
+ * ✅ Memoized for performance
42
43
  * ✅ SOLID, DRY, KISS
43
44
  */
44
- export const AtomicText = ({
45
+ export const AtomicText = React.memo(({
45
46
  type = 'bodyMedium',
46
47
  color = 'textPrimary',
47
48
  align,
@@ -55,29 +56,31 @@ export const AtomicText = ({
55
56
  }: AtomicTextProps) => {
56
57
  const tokens = useAppDesignTokens();
57
58
 
58
- // Get typography style from tokens
59
- const typographyStyle = tokens.typography[type] as TextStyle & { responsiveFontSize?: number };
60
-
61
- // Use responsive font size if available
62
- const fontSize = typographyStyle?.responsiveFontSize || typographyStyle?.fontSize;
63
-
64
- // Resolve color
65
- const resolvedColor = typeof color === 'string' && !color.includes('.')
66
- ? getTextColor(color as ColorVariant, tokens)
67
- : color;
68
-
69
- const textStyle: StyleProp<TextStyle> = [
70
- typographyStyle,
71
- {
72
- color: resolvedColor as string,
73
- ...(fontSize && { fontSize }),
74
- ...(align && { textAlign: align }),
75
- ...(fontWeight && { fontWeight }),
76
- ...(marginTop && { marginTop: tokens.spacing[marginTop] as number }),
77
- ...(marginBottom && { marginBottom: tokens.spacing[marginBottom] as number }),
78
- },
79
- style,
80
- ];
59
+ const textStyle = useMemo<StyleProp<TextStyle>>(() => {
60
+ // Get typography style from tokens
61
+ const typographyStyle = tokens.typography[type] as TextStyle & { responsiveFontSize?: number };
62
+
63
+ // Use responsive font size if available
64
+ const fontSize = typographyStyle?.responsiveFontSize || typographyStyle?.fontSize;
65
+
66
+ // Resolve color
67
+ const resolvedColor = typeof color === 'string' && !color.includes('.')
68
+ ? getTextColor(color as ColorVariant, tokens)
69
+ : color;
70
+
71
+ return [
72
+ typographyStyle,
73
+ {
74
+ color: resolvedColor as string,
75
+ ...(fontSize && { fontSize }),
76
+ ...(align && { textAlign: align }),
77
+ ...(fontWeight && { fontWeight }),
78
+ ...(marginTop && { marginTop: tokens.spacing[marginTop] as number }),
79
+ ...(marginBottom && { marginBottom: tokens.spacing[marginBottom] as number }),
80
+ },
81
+ style,
82
+ ];
83
+ }, [tokens, type, color, align, fontWeight, marginTop, marginBottom, style]);
81
84
 
82
85
  return (
83
86
  <Text
@@ -88,4 +91,6 @@ export const AtomicText = ({
88
91
  {children}
89
92
  </Text>
90
93
  );
91
- };
94
+ });
95
+
96
+ AtomicText.displayName = 'AtomicText';
@@ -1,4 +1,4 @@
1
- import React, { forwardRef } from 'react';
1
+ import React, { forwardRef, useMemo } from 'react';
2
2
  import { View, TextInput, StyleSheet, type ViewStyle, type StyleProp, type TextStyle } from 'react-native';
3
3
  import { useAppDesignTokens } from '../theme';
4
4
  import { AtomicText } from './AtomicText';
@@ -68,6 +68,21 @@ export const AtomicTextArea = forwardRef<React.ElementRef<typeof TextInput>, Ato
68
68
  const tokens = useAppDesignTokens();
69
69
  const hasError = !!errorText;
70
70
 
71
+ const textInputStyle = useMemo<StyleProp<TextStyle>>(() => [
72
+ styles.input,
73
+ {
74
+ backgroundColor: tokens.colors.surface,
75
+ borderColor: hasError ? tokens.colors.error : tokens.colors.border,
76
+ color: tokens.colors.textPrimary,
77
+ minHeight: calculatedMinHeight,
78
+ padding: tokens.spacing.md,
79
+ borderRadius: tokens.borderRadius.md,
80
+ fontSize: 16,
81
+ },
82
+ inputStyle,
83
+ disabled && { opacity: 0.5 },
84
+ ], [tokens, hasError, calculatedMinHeight, inputStyle, disabled]);
85
+
71
86
  return (
72
87
  <View style={[styles.container, style]} testID={testID}>
73
88
  {label && (
@@ -93,20 +108,7 @@ export const AtomicTextArea = forwardRef<React.ElementRef<typeof TextInput>, Ato
93
108
  onSubmitEditing={onSubmitEditing}
94
109
  blurOnSubmit={blurOnSubmit}
95
110
  textAlignVertical="top"
96
- style={[
97
- styles.input,
98
- {
99
- backgroundColor: tokens.colors.surface,
100
- borderColor: hasError ? tokens.colors.error : tokens.colors.border,
101
- color: tokens.colors.textPrimary,
102
- minHeight: calculatedMinHeight,
103
- padding: tokens.spacing.md,
104
- borderRadius: tokens.borderRadius.md,
105
- fontSize: 16,
106
- },
107
- inputStyle,
108
- disabled && { opacity: 0.5 },
109
- ]}
111
+ style={textInputStyle}
110
112
  />
111
113
  {(helperText || errorText) && (
112
114
  <View style={styles.helperRow}>
@@ -43,49 +43,55 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
43
43
  const spacingMultiplier = tokens.spacingMultiplier;
44
44
 
45
45
  const themedStyles = useMemo(
46
- () =>
47
- StyleSheet.create({
48
- container: {
49
- flex: 1,
50
- alignItems: 'flex-start',
51
- justifyContent: 'flex-start',
52
- padding: tokens.spacing.xl,
53
- },
54
- iconContainer: {
55
- width: calculateResponsiveSize(EMPTY_STATE_ICON.width, spacingMultiplier),
56
- height: calculateResponsiveSize(EMPTY_STATE_ICON.height, spacingMultiplier),
57
- borderRadius: calculateResponsiveSize(EMPTY_STATE_ICON.borderRadius, spacingMultiplier),
58
- alignItems: 'flex-start',
59
- justifyContent: 'flex-start',
60
- marginBottom: tokens.spacing.lg,
61
- },
62
- title: {
63
- marginBottom: tokens.spacing.sm,
64
- },
65
- description: {
66
- marginBottom: tokens.spacing.lg,
67
- },
68
- actionButton: {
69
- paddingHorizontal: tokens.spacing.lg,
70
- paddingVertical: tokens.spacing.md,
71
- borderRadius: tokens.borders.radius.md,
72
- marginTop: tokens.spacing.sm,
73
- },
74
- }),
46
+ () => ({
47
+ container: {
48
+ flex: 1,
49
+ alignItems: 'flex-start' as const,
50
+ justifyContent: 'flex-start' as const,
51
+ padding: tokens.spacing.xl,
52
+ },
53
+ iconContainer: {
54
+ width: calculateResponsiveSize(EMPTY_STATE_ICON.width, spacingMultiplier),
55
+ height: calculateResponsiveSize(EMPTY_STATE_ICON.height, spacingMultiplier),
56
+ borderRadius: calculateResponsiveSize(EMPTY_STATE_ICON.borderRadius, spacingMultiplier),
57
+ alignItems: 'flex-start' as const,
58
+ justifyContent: 'flex-start' as const,
59
+ marginBottom: tokens.spacing.lg,
60
+ },
61
+ title: {
62
+ marginBottom: tokens.spacing.sm,
63
+ textAlign: 'left' as const,
64
+ },
65
+ description: {
66
+ marginBottom: tokens.spacing.lg,
67
+ textAlign: 'left' as const,
68
+ },
69
+ actionButton: {
70
+ paddingHorizontal: tokens.spacing.lg,
71
+ paddingVertical: tokens.spacing.md,
72
+ borderRadius: tokens.borders.radius.md,
73
+ marginTop: tokens.spacing.sm,
74
+ },
75
+ }),
75
76
  [tokens, spacingMultiplier],
76
77
  );
77
78
 
79
+ const iconContainerStyle = useMemo(() => [
80
+ themedStyles.iconContainer,
81
+ { backgroundColor: tokens.colors.surface },
82
+ ], [themedStyles.iconContainer, tokens.colors.surface]);
83
+
84
+ const actionButtonStyle = useMemo(() => [
85
+ themedStyles.actionButton,
86
+ { backgroundColor: tokens.colors.primary },
87
+ ], [themedStyles.actionButton, tokens.colors.primary]);
88
+
78
89
  return (
79
90
  <View style={[themedStyles.container, style]} testID={testID}>
80
91
  {illustration ? (
81
92
  illustration
82
93
  ) : (
83
- <View
84
- style={[
85
- themedStyles.iconContainer,
86
- { backgroundColor: tokens.colors.surface },
87
- ]}
88
- >
94
+ <View style={iconContainerStyle}>
89
95
  <AtomicIcon name={icon} size="xxl" color="secondary" />
90
96
  </View>
91
97
  )}
@@ -93,7 +99,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
93
99
  <AtomicText
94
100
  type="headlineSmall"
95
101
  color="primary"
96
- style={[themedStyles.title, { textAlign: 'left' }]}
102
+ style={themedStyles.title}
97
103
  >
98
104
  {title}
99
105
  </AtomicText>
@@ -102,7 +108,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
102
108
  <AtomicText
103
109
  type="bodyMedium"
104
110
  color="secondary"
105
- style={[themedStyles.description, { textAlign: 'left' }]}
111
+ style={themedStyles.description}
106
112
  >
107
113
  {displayDescription}
108
114
  </AtomicText>
@@ -110,10 +116,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
110
116
 
111
117
  {actionLabel && onAction && (
112
118
  <TouchableOpacity
113
- style={[
114
- themedStyles.actionButton,
115
- { backgroundColor: tokens.colors.primary },
116
- ]}
119
+ style={actionButtonStyle}
117
120
  onPress={onAction}
118
121
  activeOpacity={0.8}
119
122
  >
@@ -3,7 +3,7 @@
3
3
  * Refactored: Extracted configs, styles, and types
4
4
  */
5
5
 
6
- import React, { useCallback } from 'react';
6
+ import React, { useCallback, useMemo } from 'react';
7
7
  import { StyleProp, ViewStyle, TextStyle, TouchableOpacity } from 'react-native';
8
8
  import { AtomicText } from '../AtomicText';
9
9
  import { AtomicIcon } from '../icon';
@@ -46,7 +46,7 @@ export const AtomicButton: React.FC<AtomicButtonProps> = React.memo(({
46
46
  const variantStyles = getVariantStyles(variant, tokens);
47
47
  const iconColor = variantStyles.text.color;
48
48
 
49
- const containerStyle: StyleProp<ViewStyle> = [
49
+ const containerStyle = useMemo<StyleProp<ViewStyle>>(() => [
50
50
  buttonStyles.button,
51
51
  {
52
52
  paddingVertical: config.paddingVertical,
@@ -58,9 +58,9 @@ export const AtomicButton: React.FC<AtomicButtonProps> = React.memo(({
58
58
  fullWidth ? buttonStyles.fullWidth : undefined,
59
59
  isDisabled ? buttonStyles.disabled : undefined,
60
60
  style,
61
- ];
61
+ ], [config, variantStyles.container, fullWidth, isDisabled, style, tokens.borders.radius.md]);
62
62
 
63
- const buttonTextStyle: StyleProp<TextStyle> = [
63
+ const buttonTextStyle = useMemo<StyleProp<TextStyle>>(() => [
64
64
  {
65
65
  fontSize: config.fontSize,
66
66
  fontWeight: '600',
@@ -68,17 +68,16 @@ export const AtomicButton: React.FC<AtomicButtonProps> = React.memo(({
68
68
  variantStyles.text,
69
69
  isDisabled ? buttonStyles.disabledText : undefined,
70
70
  textStyle,
71
- ];
71
+ ], [config.fontSize, variantStyles.text, isDisabled, textStyle]);
72
+
73
+ const rowReverseStyle = useMemo(() => iconPosition === 'right' ? buttonStyles.rowReverse : undefined, [iconPosition]);
72
74
 
73
75
  const buttonText = title || children;
74
76
  const showIcon = icon;
75
77
 
76
78
  return (
77
79
  <TouchableOpacity
78
- style={[
79
- containerStyle,
80
- iconPosition === 'right' && buttonStyles.rowReverse,
81
- ]}
80
+ style={[containerStyle, rowReverseStyle]}
82
81
  onPress={handlePress}
83
82
  activeOpacity={activeOpacity}
84
83
  disabled={isDisabled}