@umituz/react-native-design-system 4.28.10 → 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 (61) hide show
  1. package/package.json +36 -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 +45 -42
  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 +1 -22
  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/Divider/types.ts +1 -1
  34. package/src/molecules/SearchBar/SearchBar.tsx +28 -24
  35. package/src/molecules/StepHeader/StepHeader.tsx +1 -1
  36. package/src/molecules/StepProgress/StepProgress.tsx +1 -1
  37. package/src/molecules/action-footer/ActionFooter.tsx +33 -32
  38. package/src/molecules/alerts/AlertModal.tsx +36 -20
  39. package/src/molecules/alerts/AlertService.ts +60 -15
  40. package/src/molecules/avatar/Avatar.tsx +48 -40
  41. package/src/molecules/avatar/AvatarGroup.tsx +8 -8
  42. package/src/molecules/bottom-sheet/components/BottomSheet.tsx +1 -1
  43. package/src/molecules/bottom-sheet/components/BottomSheetModal.tsx +1 -1
  44. package/src/molecules/bottom-sheet/components/filter/FilterSheet.tsx +1 -1
  45. package/src/molecules/calendar/infrastructure/utils/DateUtilities.ts +12 -1
  46. package/src/molecules/calendar/presentation/components/CalendarDayCell.tsx +48 -32
  47. package/src/molecules/circular-menu/CircularMenuItem.tsx +1 -1
  48. package/src/molecules/countdown/components/CountdownHeader.tsx +1 -1
  49. package/src/molecules/countdown/components/TimeUnit.tsx +1 -1
  50. package/src/molecules/hero-section/HeroSection.tsx +1 -1
  51. package/src/molecules/icon-grid/IconGrid.tsx +1 -1
  52. package/src/molecules/info-grid/InfoGrid.tsx +6 -4
  53. package/src/molecules/navigation/TabsNavigator.tsx +1 -1
  54. package/src/molecules/navigation/components/NavigationHeader.tsx +1 -1
  55. package/src/organisms/FormContainer.tsx +11 -1
  56. package/src/tanstack/domain/utils/ErrorHelpers.ts +2 -2
  57. package/src/tanstack/domain/utils/MetricsCalculator.ts +6 -1
  58. package/src/theme/core/colors/ColorUtils.ts +7 -4
  59. package/src/utils/formatters/stringFormatter.ts +18 -3
  60. package/src/utils/index.ts +140 -0
  61. 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.10",
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",
@@ -162,6 +162,11 @@
162
162
  "react-native": "./src/presentation/utils/variants.ts",
163
163
  "default": "./src/presentation/utils/variants.ts"
164
164
  },
165
+ "./core": {
166
+ "types": "./dist/core/index.d.ts",
167
+ "react-native": "./src/core/index.ts",
168
+ "default": "./src/core/index.ts"
169
+ },
165
170
  "./package.json": "./package.json"
166
171
  },
167
172
  "scripts": {
@@ -173,15 +178,10 @@
173
178
  "build": "tsc --project tsconfig.build.json",
174
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!');\""
175
180
  },
176
- "dependencies": {
177
- "@react-native-async-storage/async-storage": "^2.2.0",
178
- "react-native-svg": "^15.12.1",
179
- "rn-emoji-keyboard": "^1.7.0"
180
- },
181
+ "dependencies": {},
181
182
  "optionalDependencies": {
182
183
  "@tanstack/query-async-storage-persister": "^5.0.0",
183
- "@tanstack/react-query-persist-client": "^5.0.0",
184
- "expo-image-manipulator": "~14.0.0"
184
+ "@tanstack/react-query-persist-client": "^5.0.0"
185
185
  },
186
186
  "keywords": [
187
187
  "react-native",
@@ -208,6 +208,7 @@
208
208
  "url": "https://github.com/umituz/react-native-design-system"
209
209
  },
210
210
  "peerDependencies": {
211
+ "@react-native-async-storage/async-storage": ">=2.0.0",
211
212
  "@react-native-community/datetimepicker": ">=8.0.0",
212
213
  "@react-navigation/bottom-tabs": ">=7.0.0",
213
214
  "@react-navigation/native": ">=7.0.0",
@@ -218,13 +219,17 @@
218
219
  "expo": ">=54.0.0",
219
220
  "expo-application": ">=5.0.0",
220
221
  "expo-clipboard": ">=8.0.0",
222
+ "expo-constants": ">=18.0.0",
221
223
  "expo-crypto": ">=13.0.0",
222
224
  "expo-device": ">=5.0.0",
225
+ "expo-file-system": ">=19.0.0",
223
226
  "expo-font": ">=12.0.0",
224
227
  "expo-haptics": ">=14.0.0",
225
228
  "expo-image": ">=3.0.0",
226
229
  "expo-image-manipulator": ">=14.0.0",
227
230
  "expo-image-picker": ">=14.0.0",
231
+ "expo-localization": ">=17.0.0",
232
+ "expo-media-library": ">=18.0.0",
228
233
  "expo-network": ">=8.0.0",
229
234
  "expo-secure-store": ">=14.0.0",
230
235
  "expo-sharing": ">=12.0.0",
@@ -233,9 +238,14 @@
233
238
  "react-native-gesture-handler": ">=2.20.0",
234
239
  "react-native-keyboard-controller": ">=1.0.0",
235
240
  "react-native-safe-area-context": ">=5.6.2",
241
+ "react-native-svg": ">=15.0.0",
242
+ "rn-emoji-keyboard": ">=1.0.0",
236
243
  "zustand": ">=5.0.0"
237
244
  },
238
245
  "peerDependenciesMeta": {
246
+ "@react-native-async-storage/async-storage": {
247
+ "optional": true
248
+ },
239
249
  "react-native-keyboard-controller": {
240
250
  "optional": true
241
251
  },
@@ -260,9 +270,15 @@
260
270
  "expo-clipboard": {
261
271
  "optional": true
262
272
  },
273
+ "expo-constants": {
274
+ "optional": true
275
+ },
263
276
  "expo-crypto": {
264
277
  "optional": true
265
278
  },
279
+ "expo-file-system": {
280
+ "optional": true
281
+ },
266
282
  "expo-font": {
267
283
  "optional": true
268
284
  },
@@ -278,6 +294,12 @@
278
294
  "expo-image-picker": {
279
295
  "optional": true
280
296
  },
297
+ "expo-localization": {
298
+ "optional": true
299
+ },
300
+ "expo-media-library": {
301
+ "optional": true
302
+ },
281
303
  "expo-network": {
282
304
  "optional": true
283
305
  },
@@ -287,6 +309,12 @@
287
309
  "expo-sharing": {
288
310
  "optional": true
289
311
  },
312
+ "react-native-svg": {
313
+ "optional": true
314
+ },
315
+ "rn-emoji-keyboard": {
316
+ "optional": true
317
+ },
290
318
  "@react-native-community/datetimepicker": {
291
319
  "optional": true
292
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}>
@@ -12,7 +12,7 @@ import { View, StyleSheet, TouchableOpacity, ViewStyle } from 'react-native';
12
12
  import { AtomicIcon } from './icon';
13
13
  import { AtomicText } from './AtomicText';
14
14
  import { useAppDesignTokens } from '../theme';
15
- import { calculateResponsiveSize } from '../utils/responsiveUtils';
15
+ import { calculateResponsiveSize } from '../responsive';
16
16
  import { EMPTY_STATE_ICON } from '../constants';
17
17
 
18
18
  export interface EmptyStateProps {
@@ -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}