@umituz/react-native-design-system 2.3.7 → 2.3.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-design-system",
3
- "version": "2.3.7",
3
+ "version": "2.3.9",
4
4
  "description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive and safe area utilities",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Avatar Domain - Avatar Component
3
+ *
4
+ * Universal avatar component with image, initials, and icon support.
5
+ * Handles loading states, fallbacks, and status indicators.
6
+ */
7
+
8
+ import React from 'react';
9
+ import { View, Image, StyleSheet, type StyleProp, type ViewStyle, type ImageStyle } from 'react-native';
10
+ import { useAppDesignTokens } from '../../theme';
11
+ import { AtomicText, AtomicIcon } from '../../atoms';
12
+ import type { AvatarSize, AvatarShape } from './Avatar.utils';
13
+ import {
14
+ SIZE_CONFIGS,
15
+ AvatarUtils,
16
+ AVATAR_CONSTANTS,
17
+ } from './Avatar.utils';
18
+
19
+ /**
20
+ * Avatar component props
21
+ */
22
+ export interface AvatarProps {
23
+ /** Image URI */
24
+ uri?: string;
25
+ /** User name for initials */
26
+ name?: string;
27
+ /** Icon name (fallback when no image/name) */
28
+ icon?: string;
29
+ /** Size preset */
30
+ size?: AvatarSize;
31
+ /** Shape */
32
+ shape?: AvatarShape;
33
+ /** Custom background color */
34
+ backgroundColor?: string;
35
+ /** Show status indicator */
36
+ showStatus?: boolean;
37
+ /** Status (online/offline/away/busy) */
38
+ status?: 'online' | 'offline' | 'away' | 'busy';
39
+ /** Custom container style */
40
+ style?: StyleProp<ViewStyle>;
41
+ /** Custom image style */
42
+ imageStyle?: StyleProp<ImageStyle>;
43
+ /** OnPress handler */
44
+ onPress?: () => void;
45
+ }
46
+
47
+ /**
48
+ * Avatar Component
49
+ *
50
+ * Displays user avatars with automatic fallback hierarchy:
51
+ * 1. Image (if uri provided)
52
+ * 2. Initials (if name provided)
53
+ * 3. Icon (fallback)
54
+ *
55
+ * USAGE:
56
+ * ```typescript
57
+ * // Image avatar
58
+ * <Avatar uri="https://..." name="Ümit Uz" size="lg" />
59
+ *
60
+ * // Initials avatar (no image)
61
+ * <Avatar name="Ümit Uz" size="md" />
62
+ *
63
+ * // Icon avatar (fallback)
64
+ * <Avatar size="sm" />
65
+ *
66
+ * // With status indicator
67
+ * <Avatar
68
+ * name="Ümit Uz"
69
+ * showStatus
70
+ * status="online"
71
+ * />
72
+ *
73
+ * // Custom shape
74
+ * <Avatar name="John Doe" shape="rounded" />
75
+ * ```
76
+ */
77
+ export const Avatar: React.FC<AvatarProps> = ({
78
+ uri,
79
+ name,
80
+ icon = AVATAR_CONSTANTS.DEFAULT_ICON,
81
+ size = AVATAR_CONSTANTS.DEFAULT_SIZE,
82
+ shape = AVATAR_CONSTANTS.DEFAULT_SHAPE,
83
+ backgroundColor,
84
+ showStatus = false,
85
+ status = 'offline',
86
+ style,
87
+ imageStyle,
88
+ onPress,
89
+ }) => {
90
+ const tokens = useAppDesignTokens();
91
+ const config = SIZE_CONFIGS[size];
92
+
93
+ // Determine avatar type and content
94
+ const hasImage = !!uri;
95
+ const hasName = !!name;
96
+ const initials = hasName ? AvatarUtils.generateInitials(name) : AVATAR_CONSTANTS.FALLBACK_INITIALS;
97
+ const bgColor = backgroundColor || (hasName ? AvatarUtils.getColorForName(name) : tokens.colors.surfaceSecondary);
98
+ const borderRadius = AvatarUtils.getBorderRadius(shape, config.size);
99
+
100
+ // Status indicator position
101
+ const statusPosition = {
102
+ bottom: 0,
103
+ right: 0,
104
+ };
105
+
106
+ const renderContent = () => {
107
+ if (hasImage) {
108
+ return (
109
+ <Image
110
+ source={{ uri }}
111
+ style={[
112
+ styles.image,
113
+ {
114
+ width: config.size,
115
+ height: config.size,
116
+ borderRadius,
117
+ },
118
+ imageStyle,
119
+ ]}
120
+ />
121
+ );
122
+ }
123
+
124
+ if (hasName) {
125
+ return (
126
+ <AtomicText
127
+ type="bodyMedium"
128
+ style={[
129
+ styles.initials,
130
+ {
131
+ fontSize: config.fontSize,
132
+ color: '#FFFFFF',
133
+ },
134
+ ]}
135
+ >
136
+ {initials}
137
+ </AtomicText>
138
+ );
139
+ }
140
+
141
+ // Fallback to icon
142
+ return (
143
+ <AtomicIcon
144
+ name={icon}
145
+ customSize={config.iconSize}
146
+ customColor="#FFFFFF"
147
+ />
148
+ );
149
+ };
150
+
151
+ return (
152
+ <View
153
+ style={[
154
+ styles.container,
155
+ {
156
+ width: config.size,
157
+ height: config.size,
158
+ borderRadius,
159
+ backgroundColor: bgColor,
160
+ },
161
+ style,
162
+ ]}
163
+ onTouchEnd={onPress}
164
+ >
165
+ {renderContent()}
166
+
167
+ {/* Status Indicator */}
168
+ {showStatus && (
169
+ <View
170
+ style={[
171
+ styles.statusIndicator,
172
+ {
173
+ width: config.statusSize,
174
+ height: config.statusSize,
175
+ borderRadius: config.statusSize / 2,
176
+ backgroundColor: AvatarUtils.getStatusColor(status),
177
+ borderWidth: config.borderWidth,
178
+ borderColor: tokens.colors.onBackground,
179
+ ...statusPosition,
180
+ },
181
+ ]}
182
+ />
183
+ )}
184
+ </View>
185
+ );
186
+ };
187
+
188
+ const styles = StyleSheet.create({
189
+ container: {
190
+ justifyContent: 'center',
191
+ alignItems: 'center',
192
+ overflow: 'hidden',
193
+ position: 'relative',
194
+ },
195
+ image: {
196
+ resizeMode: 'cover',
197
+ },
198
+ initials: {
199
+ fontWeight: '600',
200
+ textAlign: 'center',
201
+ },
202
+ statusIndicator: {
203
+ position: 'absolute',
204
+ },
205
+ });
206
+
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Avatar Domain - Entity Definitions
3
+ *
4
+ * Core types and interfaces for user avatars.
5
+ * Supports images, initials, icons with Turkish character support.
6
+ */
7
+
8
+ /**
9
+ * Avatar size preset
10
+ */
11
+ export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
12
+
13
+ /**
14
+ * Avatar shape
15
+ */
16
+ export type AvatarShape = 'circle' | 'square' | 'rounded';
17
+
18
+ /**
19
+ * Avatar type
20
+ */
21
+ export type AvatarType = 'image' | 'initials' | 'icon';
22
+
23
+ /**
24
+ * Avatar configuration
25
+ */
26
+ export interface AvatarConfig {
27
+ /** Avatar type */
28
+ type: AvatarType;
29
+ /** Size preset */
30
+ size: AvatarSize;
31
+ /** Shape */
32
+ shape: AvatarShape;
33
+ /** Image URI */
34
+ uri?: string;
35
+ /** User name for initials */
36
+ name?: string;
37
+ /** Icon name (if type is icon) */
38
+ icon?: string;
39
+ /** Custom background color */
40
+ backgroundColor?: string;
41
+ /** Show status indicator */
42
+ showStatus?: boolean;
43
+ /** Status (online/offline) */
44
+ status?: 'online' | 'offline' | 'away' | 'busy';
45
+ }
46
+
47
+ /**
48
+ * Size configuration
49
+ */
50
+ export interface SizeConfig {
51
+ size: number;
52
+ fontSize: number;
53
+ iconSize: number;
54
+ statusSize: number;
55
+ borderWidth: number;
56
+ }
57
+
58
+ /**
59
+ * Avatar group configuration
60
+ */
61
+ export interface AvatarGroupConfig {
62
+ maxVisible: number;
63
+ spacing: number;
64
+ size: AvatarSize;
65
+ shape: AvatarShape;
66
+ }
67
+
68
+ /**
69
+ * Size configurations (px)
70
+ */
71
+ export const SIZE_CONFIGS: Record<AvatarSize, SizeConfig> = {
72
+ xs: {
73
+ size: 24,
74
+ fontSize: 10,
75
+ iconSize: 12,
76
+ statusSize: 6,
77
+ borderWidth: 1,
78
+ },
79
+ sm: {
80
+ size: 32,
81
+ fontSize: 12,
82
+ iconSize: 16,
83
+ statusSize: 8,
84
+ borderWidth: 1.5,
85
+ },
86
+ md: {
87
+ size: 40,
88
+ fontSize: 14,
89
+ iconSize: 20,
90
+ statusSize: 10,
91
+ borderWidth: 2,
92
+ },
93
+ lg: {
94
+ size: 56,
95
+ fontSize: 18,
96
+ iconSize: 28,
97
+ statusSize: 12,
98
+ borderWidth: 2,
99
+ },
100
+ xl: {
101
+ size: 80,
102
+ fontSize: 24,
103
+ iconSize: 40,
104
+ statusSize: 16,
105
+ borderWidth: 2.5,
106
+ },
107
+ xxl: {
108
+ size: 120,
109
+ fontSize: 36,
110
+ iconSize: 60,
111
+ statusSize: 20,
112
+ borderWidth: 3,
113
+ },
114
+ };
115
+
116
+ /**
117
+ * Avatar background colors
118
+ * Vibrant, accessible colors with good contrast
119
+ */
120
+ export const AVATAR_COLORS = [
121
+ '#EF4444', // Red
122
+ '#F59E0B', // Orange
123
+ '#10B981', // Green
124
+ '#3B82F6', // Blue
125
+ '#8B5CF6', // Purple
126
+ '#EC4899', // Pink
127
+ '#14B8A6', // Teal
128
+ '#F97316', // Orange-Red
129
+ '#06B6D4', // Cyan
130
+ '#84CC16', // Lime
131
+ ] as const;
132
+
133
+ /**
134
+ * Status indicator colors
135
+ */
136
+ export const STATUS_COLORS = {
137
+ online: '#10B981', // Green
138
+ offline: '#9CA3AF', // Gray
139
+ away: '#F59E0B', // Orange
140
+ busy: '#EF4444', // Red
141
+ } as const;
142
+
143
+ /**
144
+ * Border radius configurations
145
+ */
146
+ export const SHAPE_CONFIGS = {
147
+ circle: 9999, // Full circle
148
+ square: 0, // No radius
149
+ rounded: 8, // Rounded corners
150
+ } as const;
151
+
152
+ /**
153
+ * Avatar utility class
154
+ */
155
+ export class AvatarUtils {
156
+ /**
157
+ * Generate initials from name
158
+ * Supports Turkish characters (Ümit Uz → ÜU)
159
+ */
160
+ static generateInitials(name: string): string {
161
+ const trimmed = name.trim();
162
+ if (!trimmed) return '?';
163
+
164
+ const words = trimmed.split(/\s+/);
165
+
166
+ if (words.length >= 2) {
167
+ // Full name: First letter of first + first letter of last
168
+ const first = words[0][0];
169
+ const last = words[words.length - 1][0];
170
+ return (first + last).toLocaleUpperCase('tr-TR');
171
+ } else {
172
+ // Single word: First 2 letters
173
+ return trimmed.slice(0, 2).toLocaleUpperCase('tr-TR');
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Generate initials from email
179
+ * umit@example.com → UE
180
+ */
181
+ static generateInitialsFromEmail(email: string): string {
182
+ const trimmed = email.trim();
183
+ if (!trimmed) return '?';
184
+
185
+ const [username] = trimmed.split('@');
186
+ if (!username) return '?';
187
+
188
+ return username.slice(0, 2).toLocaleUpperCase('tr-TR');
189
+ }
190
+
191
+ /**
192
+ * Hash string to number (for consistent color assignment)
193
+ */
194
+ static hashString(str: string): number {
195
+ let hash = 0;
196
+ for (let i = 0; i < str.length; i++) {
197
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
198
+ }
199
+ return Math.abs(hash);
200
+ }
201
+
202
+ /**
203
+ * Get consistent color for name
204
+ * Same name always returns same color
205
+ */
206
+ static getColorForName(name: string): string {
207
+ if (!name) return AVATAR_COLORS[0];
208
+
209
+ const hash = this.hashString(name);
210
+ return AVATAR_COLORS[hash % AVATAR_COLORS.length];
211
+ }
212
+
213
+ /**
214
+ * Get size config
215
+ */
216
+ static getSizeConfig(size: AvatarSize): SizeConfig {
217
+ return SIZE_CONFIGS[size];
218
+ }
219
+
220
+ /**
221
+ * Get border radius for shape
222
+ */
223
+ static getBorderRadius(shape: AvatarShape, size: number): number {
224
+ if (shape === 'circle') {
225
+ return size / 2;
226
+ }
227
+ return SHAPE_CONFIGS[shape];
228
+ }
229
+
230
+ /**
231
+ * Get status color
232
+ */
233
+ static getStatusColor(status: 'online' | 'offline' | 'away' | 'busy'): string {
234
+ return STATUS_COLORS[status];
235
+ }
236
+
237
+ /**
238
+ * Validate avatar config
239
+ */
240
+ static validateConfig(config: Partial<AvatarConfig>): AvatarConfig {
241
+ return {
242
+ type: config.type || 'initials',
243
+ size: config.size || 'md',
244
+ shape: config.shape || 'circle',
245
+ uri: config.uri,
246
+ name: config.name,
247
+ icon: config.icon,
248
+ backgroundColor: config.backgroundColor,
249
+ showStatus: config.showStatus ?? false,
250
+ status: config.status,
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Check if avatar has image
256
+ */
257
+ static hasImage(config: AvatarConfig): boolean {
258
+ return config.type === 'image' && !!config.uri;
259
+ }
260
+
261
+ /**
262
+ * Check if avatar has initials
263
+ */
264
+ static hasInitials(config: AvatarConfig): boolean {
265
+ return config.type === 'initials' && !!config.name;
266
+ }
267
+
268
+ /**
269
+ * Check if avatar has icon
270
+ */
271
+ static hasIcon(config: AvatarConfig): boolean {
272
+ return config.type === 'icon' && !!config.icon;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Avatar constants
278
+ */
279
+ export const AVATAR_CONSTANTS = {
280
+ DEFAULT_SIZE: 'md' as AvatarSize,
281
+ DEFAULT_SHAPE: 'circle' as AvatarShape,
282
+ DEFAULT_TYPE: 'initials' as AvatarType,
283
+ DEFAULT_ICON: 'user',
284
+ MAX_GROUP_VISIBLE: 3,
285
+ GROUP_SPACING: -8, // Negative for overlap
286
+ FALLBACK_INITIALS: '?',
287
+ } as const;
288
+
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Avatar Domain - AvatarGroup Component
3
+ *
4
+ * Displays multiple avatars in a stacked layout.
5
+ * Shows overflow count when exceeding max visible avatars.
6
+ */
7
+
8
+ import React from 'react';
9
+ import { View, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
10
+ import { useAppDesignTokens } from '../../theme';
11
+ import { AtomicText } from '../../atoms';
12
+ import { Avatar } from './Avatar';
13
+ import type { AvatarSize, AvatarShape } from './Avatar.utils';
14
+ import {
15
+ SIZE_CONFIGS,
16
+ AVATAR_CONSTANTS,
17
+ } from './Avatar.utils';
18
+
19
+ /**
20
+ * Avatar item for group
21
+ */
22
+ export interface AvatarGroupItem {
23
+ uri?: string;
24
+ name?: string;
25
+ icon?: string;
26
+ }
27
+
28
+ /**
29
+ * AvatarGroup component props
30
+ */
31
+ export interface AvatarGroupProps {
32
+ /** Array of avatar items */
33
+ items: AvatarGroupItem[];
34
+ /** Maximum visible avatars */
35
+ maxVisible?: number;
36
+ /** Avatar size */
37
+ size?: AvatarSize;
38
+ /** Avatar shape */
39
+ shape?: AvatarShape;
40
+ /** Spacing between avatars (negative for overlap) */
41
+ spacing?: number;
42
+ /** Custom container style */
43
+ style?: StyleProp<ViewStyle>;
44
+ }
45
+
46
+ /**
47
+ * AvatarGroup Component
48
+ *
49
+ * Displays multiple avatars in a horizontal stack.
50
+ * Shows "+N" indicator when exceeding max visible count.
51
+ *
52
+ * USAGE:
53
+ * ```typescript
54
+ * const users = [
55
+ * { name: 'Ümit Uz', uri: 'https://...' },
56
+ * { name: 'John Doe', uri: 'https://...' },
57
+ * { name: 'Jane Smith' },
58
+ * { name: 'Bob Johnson' },
59
+ * { name: 'Alice Brown' },
60
+ * ];
61
+ *
62
+ * // Show 3 avatars + overflow
63
+ * <AvatarGroup items={users} maxVisible={3} />
64
+ *
65
+ * // Custom spacing
66
+ * <AvatarGroup items={users} spacing={-12} />
67
+ *
68
+ * // Different size
69
+ * <AvatarGroup items={users} size="lg" />
70
+ * ```
71
+ */
72
+ export const AvatarGroup: React.FC<AvatarGroupProps> = ({
73
+ items,
74
+ maxVisible = AVATAR_CONSTANTS.MAX_GROUP_VISIBLE,
75
+ size = AVATAR_CONSTANTS.DEFAULT_SIZE,
76
+ shape = AVATAR_CONSTANTS.DEFAULT_SHAPE,
77
+ spacing = AVATAR_CONSTANTS.GROUP_SPACING,
78
+ style,
79
+ }) => {
80
+ const tokens = useAppDesignTokens();
81
+ const config = SIZE_CONFIGS[size];
82
+
83
+ // Calculate visible avatars and overflow count
84
+ const visibleItems = items.slice(0, maxVisible);
85
+ const overflowCount = items.length - maxVisible;
86
+ const hasOverflow = overflowCount > 0;
87
+
88
+ return (
89
+ <View style={[styles.container, style]}>
90
+ {/* Render visible avatars */}
91
+ {visibleItems.map((item, index) => (
92
+ <View
93
+ key={index}
94
+ style={[
95
+ styles.avatarWrapper,
96
+ index > 0 && { marginLeft: spacing },
97
+ ]}
98
+ >
99
+ <Avatar
100
+ uri={item.uri}
101
+ name={item.name}
102
+ icon={item.icon}
103
+ size={size}
104
+ shape={shape}
105
+ style={[
106
+ styles.avatar,
107
+ {
108
+ borderWidth: 2,
109
+ borderColor: tokens.colors.onBackground,
110
+ },
111
+ ]}
112
+ />
113
+ </View>
114
+ ))}
115
+
116
+ {/* Overflow indicator */}
117
+ {hasOverflow && (
118
+ <View
119
+ style={[
120
+ styles.avatarWrapper,
121
+ { marginLeft: spacing },
122
+ ]}
123
+ >
124
+ <View
125
+ style={[
126
+ styles.overflow,
127
+ {
128
+ width: config.size,
129
+ height: config.size,
130
+ borderRadius: shape === 'circle' ? config.size / 2 : shape === 'rounded' ? 8 : 0,
131
+ backgroundColor: tokens.colors.surfaceSecondary,
132
+ borderWidth: 2,
133
+ borderColor: tokens.colors.onBackground,
134
+ },
135
+ ]}
136
+ >
137
+ <AtomicText
138
+ type="bodySmall"
139
+ style={[
140
+ styles.overflowText,
141
+ {
142
+ fontSize: config.fontSize,
143
+ color: tokens.colors.textSecondary,
144
+ },
145
+ ]}
146
+ >
147
+ +{overflowCount}
148
+ </AtomicText>
149
+ </View>
150
+ </View>
151
+ )}
152
+ </View>
153
+ );
154
+ };
155
+
156
+ const styles = StyleSheet.create({
157
+ container: {
158
+ flexDirection: 'row',
159
+ alignItems: 'center',
160
+ },
161
+ avatarWrapper: {
162
+ // Wrapper for easier spacing control
163
+ },
164
+ avatar: {
165
+ // Avatar styles
166
+ },
167
+ overflow: {
168
+ justifyContent: 'center',
169
+ alignItems: 'center',
170
+ },
171
+ overflowText: {
172
+ fontWeight: '600',
173
+ },
174
+ });
175
+
@@ -0,0 +1,10 @@
1
+ export { Avatar, type AvatarProps } from './Avatar';
2
+ export { AvatarGroup, type AvatarGroupProps, type AvatarGroupItem } from './AvatarGroup';
3
+ export {
4
+ AvatarUtils,
5
+ AVATAR_CONSTANTS,
6
+ type AvatarSize,
7
+ type AvatarShape,
8
+ type AvatarConfig,
9
+ type AvatarType
10
+ } from './Avatar.utils';
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  // Component exports
7
+ export * from './avatar';
7
8
  export { FormField, type FormFieldProps } from './FormField';
8
9
  export { ListItem, type ListItemProps } from './ListItem';
9
10
  export { SearchBar, type SearchBarProps } from './SearchBar';