@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.
|
|
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';
|
package/src/molecules/index.ts
CHANGED