@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.
- package/package.json +36 -8
- package/src/atoms/AtomicAvatar.tsx +69 -40
- package/src/atoms/AtomicSpinner.tsx +24 -22
- package/src/atoms/AtomicText.tsx +32 -27
- package/src/atoms/AtomicTextArea.tsx +17 -15
- package/src/atoms/EmptyState.tsx +45 -42
- package/src/atoms/button/AtomicButton.tsx +8 -9
- package/src/atoms/card/AtomicCard.tsx +26 -8
- package/src/atoms/datepicker/components/DatePickerButton.tsx +8 -8
- package/src/atoms/datepicker/components/DatePickerModal.tsx +7 -7
- package/src/atoms/fab/styles/fabStyles.ts +1 -22
- package/src/atoms/icon/index.ts +6 -20
- package/src/atoms/picker/components/PickerModal.tsx +24 -4
- package/src/atoms/skeleton/AtomicSkeleton.tsx +9 -11
- package/src/carousel/Carousel.tsx +43 -20
- package/src/carousel/carouselCalculations.ts +12 -9
- package/src/carousel/index.ts +0 -1
- package/src/device/detection/iPadDetection.ts +5 -14
- package/src/device/infrastructure/services/DeviceFeatureService.ts +89 -9
- package/src/device/infrastructure/services/DeviceInfoService.ts +33 -0
- package/src/device/infrastructure/services/UserFriendlyIdService.ts +8 -6
- package/src/device/infrastructure/utils/__tests__/stringUtils.test.ts +56 -20
- package/src/device/infrastructure/utils/nativeModuleUtils.ts +16 -2
- package/src/device/infrastructure/utils/stringUtils.ts +51 -5
- package/src/filesystem/domain/utils/FileUtils.ts +5 -1
- package/src/image/domain/utils/ImageUtils.ts +6 -0
- package/src/layouts/AppHeader/AppHeader.tsx +13 -3
- package/src/layouts/Container/Container.tsx +19 -1
- package/src/layouts/FormLayout/FormLayout.tsx +20 -1
- package/src/layouts/Grid/Grid.tsx +34 -4
- package/src/layouts/ScreenHeader/ScreenHeader.tsx +4 -0
- package/src/layouts/ScreenLayout/ScreenLayout.tsx +42 -3
- package/src/molecules/Divider/types.ts +1 -1
- package/src/molecules/SearchBar/SearchBar.tsx +28 -24
- package/src/molecules/StepHeader/StepHeader.tsx +1 -1
- package/src/molecules/StepProgress/StepProgress.tsx +1 -1
- package/src/molecules/action-footer/ActionFooter.tsx +33 -32
- package/src/molecules/alerts/AlertModal.tsx +36 -20
- package/src/molecules/alerts/AlertService.ts +60 -15
- package/src/molecules/avatar/Avatar.tsx +48 -40
- package/src/molecules/avatar/AvatarGroup.tsx +8 -8
- package/src/molecules/bottom-sheet/components/BottomSheet.tsx +1 -1
- package/src/molecules/bottom-sheet/components/BottomSheetModal.tsx +1 -1
- package/src/molecules/bottom-sheet/components/filter/FilterSheet.tsx +1 -1
- package/src/molecules/calendar/infrastructure/utils/DateUtilities.ts +12 -1
- package/src/molecules/calendar/presentation/components/CalendarDayCell.tsx +48 -32
- package/src/molecules/circular-menu/CircularMenuItem.tsx +1 -1
- package/src/molecules/countdown/components/CountdownHeader.tsx +1 -1
- package/src/molecules/countdown/components/TimeUnit.tsx +1 -1
- package/src/molecules/hero-section/HeroSection.tsx +1 -1
- package/src/molecules/icon-grid/IconGrid.tsx +1 -1
- package/src/molecules/info-grid/InfoGrid.tsx +6 -4
- package/src/molecules/navigation/TabsNavigator.tsx +1 -1
- package/src/molecules/navigation/components/NavigationHeader.tsx +1 -1
- package/src/organisms/FormContainer.tsx +11 -1
- package/src/tanstack/domain/utils/ErrorHelpers.ts +2 -2
- package/src/tanstack/domain/utils/MetricsCalculator.ts +6 -1
- package/src/theme/core/colors/ColorUtils.ts +7 -4
- package/src/utils/formatters/stringFormatter.ts +18 -3
- package/src/utils/index.ts +140 -0
- 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.
|
|
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 =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const defaultTextColor =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
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
|
|
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
|
|
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>
|
package/src/atoms/AtomicText.tsx
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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}>
|
package/src/atoms/EmptyState.tsx
CHANGED
|
@@ -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 '../
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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={
|
|
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={
|
|
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
|
|
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
|
|
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}
|