@umituz/react-native-design-system 4.28.11 → 4.28.13
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 +31 -8
- package/src/atoms/AtomicAvatar.tsx +69 -40
- package/src/atoms/AtomicDatePicker.tsx +6 -6
- 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 +44 -41
- 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 +0 -21
- 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/SearchBar/SearchBar.tsx +27 -23
- package/src/molecules/action-footer/ActionFooter.tsx +32 -31
- package/src/molecules/alerts/AlertService.ts +60 -15
- package/src/molecules/avatar/Avatar.tsx +3 -3
- package/src/molecules/avatar/AvatarGroup.tsx +7 -7
- package/src/molecules/bottom-sheet/components/BottomSheet.tsx +3 -3
- package/src/molecules/calendar/infrastructure/utils/DateUtilities.ts +12 -1
- package/src/molecules/calendar/presentation/components/CalendarDayCell.tsx +48 -32
- package/src/molecules/info-grid/InfoGrid.tsx +5 -3
- 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 +6 -4
- 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.13",
|
|
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 =
|
|
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>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @module AtomicDatePicker
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import React, { useState } from 'react';
|
|
14
|
+
import React, { useState, useCallback, useMemo } from 'react';
|
|
15
15
|
import {
|
|
16
16
|
View,
|
|
17
17
|
Platform,
|
|
@@ -82,7 +82,7 @@ export const AtomicDatePicker: React.FC<AtomicDatePickerProps> = ({
|
|
|
82
82
|
const tokens = useAppDesignTokens();
|
|
83
83
|
const [showPicker, setShowPicker] = useState(false);
|
|
84
84
|
|
|
85
|
-
const handleChange = (event: DateTimePickerEvent, selectedDate?: Date) => {
|
|
85
|
+
const handleChange = useCallback((event: DateTimePickerEvent, selectedDate?: Date) => {
|
|
86
86
|
if (Platform.OS === 'android') {
|
|
87
87
|
setShowPicker(false);
|
|
88
88
|
if (event.type === 'set' && selectedDate) {
|
|
@@ -96,9 +96,9 @@ export const AtomicDatePicker: React.FC<AtomicDatePickerProps> = ({
|
|
|
96
96
|
setShowPicker(false);
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
-
};
|
|
99
|
+
}, [onChange]);
|
|
100
100
|
|
|
101
|
-
const handleOpen = () => {
|
|
101
|
+
const handleOpen = useCallback(() => {
|
|
102
102
|
if (!DateTimePicker) {
|
|
103
103
|
console.warn(
|
|
104
104
|
'[AtomicDatePicker] @react-native-community/datetimepicker is not installed. ' +
|
|
@@ -107,10 +107,10 @@ export const AtomicDatePicker: React.FC<AtomicDatePickerProps> = ({
|
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
109
109
|
setShowPicker(true);
|
|
110
|
-
};
|
|
110
|
+
}, [DateTimePicker]);
|
|
111
111
|
|
|
112
112
|
const { displayText } = useDatePickerText({ value, placeholder, mode });
|
|
113
|
-
const styles = getDatePickerStyles(tokens);
|
|
113
|
+
const styles = useMemo(() => getDatePickerStyles(tokens), [tokens]);
|
|
114
114
|
|
|
115
115
|
return (
|
|
116
116
|
<View style={[styles.container, style]} testID={testID}>
|
|
@@ -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
|
@@ -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
|
>
|