rnnovaui 0.5.0 → 0.7.0

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": "rnnovaui",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "react-native": "src/index.js",
@@ -0,0 +1,659 @@
1
+ import React, { forwardRef, memo, useMemo } from "react";
2
+
3
+ import {
4
+ ActivityIndicator,
5
+ Image,
6
+ Pressable,
7
+ StyleSheet,
8
+ Text,
9
+ View,
10
+ } from "react-native";
11
+
12
+ import { useUITheme } from "../../theme";
13
+
14
+ const UI_AVATAR_SIZES = {
15
+ xs: "xs",
16
+ sm: "sm",
17
+ md: "md",
18
+ lg: "lg",
19
+ xl: "xl",
20
+ };
21
+
22
+ const UI_AVATAR_SHAPES = {
23
+ circle: "circle",
24
+ rounded: "rounded",
25
+ square: "square",
26
+ };
27
+
28
+ const UI_AVATAR_STATUSES = {
29
+ online: "online",
30
+ offline: "offline",
31
+ busy: "busy",
32
+ away: "away",
33
+ none: "none",
34
+ };
35
+
36
+ const UI_AVATAR_STATUS_POSITIONS = {
37
+ bottomRight: "bottomRight",
38
+ bottomLeft: "bottomLeft",
39
+ topRight: "topRight",
40
+ topLeft: "topLeft",
41
+ };
42
+
43
+ const SIZE_CONFIG = {
44
+ xs: {
45
+ size: 28,
46
+ text: 10,
47
+ status: 8,
48
+ badgeText: 8,
49
+ badgeMin: 15,
50
+ },
51
+
52
+ sm: {
53
+ size: 36,
54
+ text: 12,
55
+ status: 10,
56
+ badgeText: 9,
57
+ badgeMin: 17,
58
+ },
59
+
60
+ md: {
61
+ size: 48,
62
+ text: 15,
63
+ status: 12,
64
+ badgeText: 10,
65
+ badgeMin: 19,
66
+ },
67
+
68
+ lg: {
69
+ size: 64,
70
+ text: 19,
71
+ status: 15,
72
+ badgeText: 11,
73
+ badgeMin: 22,
74
+ },
75
+
76
+ xl: {
77
+ size: 88,
78
+ text: 26,
79
+ status: 19,
80
+ badgeText: 12,
81
+ badgeMin: 25,
82
+ },
83
+ };
84
+
85
+ const STATUS_COLORS = {
86
+ online: "#16A34A",
87
+ offline: "#737373",
88
+ busy: "#DC2626",
89
+ away: "#D97706",
90
+ };
91
+
92
+ const UIAvatarComponent = forwardRef(function UIAvatar(
93
+ {
94
+ source,
95
+ uri,
96
+
97
+ name,
98
+ initials,
99
+
100
+ size = "md",
101
+ shape = "circle",
102
+
103
+ backgroundColor,
104
+ textColor,
105
+
106
+ status = "none",
107
+ statusColor,
108
+ statusPosition = "bottomRight",
109
+
110
+ badge,
111
+ badgeColor,
112
+ badgeTextColor,
113
+
114
+ icon,
115
+ fallbackIcon,
116
+
117
+ borderWidth = 0,
118
+ borderColor,
119
+
120
+ loading = false,
121
+ disabled = false,
122
+
123
+ onPress,
124
+ onLongPress,
125
+
126
+ activeOpacity = 0.7,
127
+
128
+ accessibilityLabel,
129
+
130
+ containerStyle,
131
+ imageStyle,
132
+ textStyle,
133
+ iconStyle,
134
+ statusStyle,
135
+ badgeStyle,
136
+
137
+ testID,
138
+
139
+ ...props
140
+ },
141
+ ref,
142
+ ) {
143
+ const { theme } = useUITheme();
144
+
145
+ const colors = theme?.colors || {};
146
+
147
+ /*
148
+ * --------------------------------------------------
149
+ * SAFE SIZE
150
+ * --------------------------------------------------
151
+ */
152
+
153
+ const safeSize = UI_AVATAR_SIZES[size] ? size : UI_AVATAR_SIZES.md;
154
+
155
+ const config = SIZE_CONFIG[safeSize];
156
+
157
+ /*
158
+ * --------------------------------------------------
159
+ * SAFE SHAPE
160
+ * --------------------------------------------------
161
+ */
162
+
163
+ const safeShape = UI_AVATAR_SHAPES[shape] ? shape : UI_AVATAR_SHAPES.circle;
164
+
165
+ /*
166
+ * --------------------------------------------------
167
+ * SAFE STATUS
168
+ * --------------------------------------------------
169
+ */
170
+
171
+ const safeStatus = UI_AVATAR_STATUSES[status]
172
+ ? status
173
+ : UI_AVATAR_STATUSES.none;
174
+
175
+ const safeStatusPosition = UI_AVATAR_STATUS_POSITIONS[statusPosition]
176
+ ? statusPosition
177
+ : UI_AVATAR_STATUS_POSITIONS.bottomRight;
178
+
179
+ /*
180
+ * --------------------------------------------------
181
+ * SOURCE
182
+ * --------------------------------------------------
183
+ */
184
+
185
+ const imageSource = source || (uri ? { uri } : null);
186
+
187
+ /*
188
+ * --------------------------------------------------
189
+ * INITIALS
190
+ * --------------------------------------------------
191
+ */
192
+
193
+ const resolvedInitials = initials || getInitials(name);
194
+
195
+ /*
196
+ * --------------------------------------------------
197
+ * COLORS
198
+ * --------------------------------------------------
199
+ */
200
+
201
+ const resolvedBackground = backgroundColor || colors.primarySoft || "#FFF0EA";
202
+
203
+ const resolvedTextColor = textColor || colors.primary || "#FF5A1F";
204
+
205
+ const resolvedBorderColor = borderColor || colors.border || "#E5E5E5";
206
+
207
+ const resolvedStatusColor =
208
+ statusColor || STATUS_COLORS[safeStatus] || colors.primary || "#FF5A1F";
209
+
210
+ const resolvedBadgeColor = badgeColor || colors.danger || "#DC2626";
211
+
212
+ const resolvedBadgeTextColor =
213
+ badgeTextColor || colors.onPrimary || "#FFFFFF";
214
+
215
+ /*
216
+ * --------------------------------------------------
217
+ * BORDER RADIUS
218
+ * --------------------------------------------------
219
+ */
220
+
221
+ const borderRadius = getBorderRadius(safeShape, config.size);
222
+
223
+ /*
224
+ * --------------------------------------------------
225
+ * BADGE
226
+ * --------------------------------------------------
227
+ */
228
+
229
+ const badgeValue =
230
+ badge !== undefined && badge !== null ? formatBadge(badge) : null;
231
+
232
+ /*
233
+ * --------------------------------------------------
234
+ * CONTENT
235
+ * --------------------------------------------------
236
+ */
237
+
238
+ const content = loading ? (
239
+ <View
240
+ style={[
241
+ styles.fallback,
242
+
243
+ {
244
+ width: config.size,
245
+
246
+ height: config.size,
247
+
248
+ borderRadius,
249
+
250
+ backgroundColor: resolvedBackground,
251
+ },
252
+ ]}
253
+ >
254
+ <ActivityIndicator size="small" color={resolvedTextColor} />
255
+ </View>
256
+ ) : imageSource ? (
257
+ <Image
258
+ source={imageSource}
259
+ style={[
260
+ styles.image,
261
+
262
+ {
263
+ width: config.size,
264
+
265
+ height: config.size,
266
+
267
+ borderRadius,
268
+
269
+ opacity: disabled ? 0.5 : 1,
270
+ },
271
+
272
+ imageStyle,
273
+ ]}
274
+ resizeMode="cover"
275
+ />
276
+ ) : icon || fallbackIcon ? (
277
+ <View
278
+ style={[
279
+ styles.fallback,
280
+
281
+ {
282
+ width: config.size,
283
+
284
+ height: config.size,
285
+
286
+ borderRadius,
287
+
288
+ backgroundColor: resolvedBackground,
289
+
290
+ opacity: disabled ? 0.5 : 1,
291
+ },
292
+ ]}
293
+ >
294
+ <View style={[iconStyle]}>{icon || fallbackIcon}</View>
295
+ </View>
296
+ ) : (
297
+ <View
298
+ style={[
299
+ styles.fallback,
300
+
301
+ {
302
+ width: config.size,
303
+
304
+ height: config.size,
305
+
306
+ borderRadius,
307
+
308
+ backgroundColor: resolvedBackground,
309
+
310
+ opacity: disabled ? 0.5 : 1,
311
+ },
312
+ ]}
313
+ >
314
+ <Text
315
+ style={[
316
+ styles.initials,
317
+
318
+ {
319
+ fontSize: config.text,
320
+
321
+ color: resolvedTextColor,
322
+ },
323
+
324
+ textStyle,
325
+ ]}
326
+ >
327
+ {resolvedInitials || "?"}
328
+ </Text>
329
+ </View>
330
+ );
331
+
332
+ /*
333
+ * --------------------------------------------------
334
+ * WRAPPER
335
+ * --------------------------------------------------
336
+ */
337
+
338
+ const avatarContent = (
339
+ <View
340
+ style={[
341
+ styles.avatar,
342
+
343
+ {
344
+ width: config.size,
345
+
346
+ height: config.size,
347
+
348
+ borderRadius,
349
+
350
+ borderWidth,
351
+
352
+ borderColor: resolvedBorderColor,
353
+
354
+ opacity: disabled ? 0.6 : 1,
355
+ },
356
+ ]}
357
+ >
358
+ {content}
359
+
360
+ {safeStatus !== UI_AVATAR_STATUSES.none ? (
361
+ <View
362
+ style={[
363
+ styles.status,
364
+
365
+ getStatusPosition(safeStatusPosition, config.size, config.status),
366
+
367
+ {
368
+ width: config.status,
369
+
370
+ height: config.status,
371
+
372
+ borderRadius: config.status / 2,
373
+
374
+ backgroundColor: resolvedStatusColor,
375
+
376
+ borderColor: colors.background || "#FFFFFF",
377
+ },
378
+
379
+ statusStyle,
380
+ ]}
381
+ />
382
+ ) : null}
383
+
384
+ {badgeValue !== null ? (
385
+ <View
386
+ style={[
387
+ styles.badge,
388
+
389
+ {
390
+ minWidth: config.badgeMin,
391
+
392
+ height: config.badgeMin,
393
+
394
+ borderRadius: config.badgeMin / 2,
395
+
396
+ backgroundColor: resolvedBadgeColor,
397
+
398
+ borderColor: colors.background || "#FFFFFF",
399
+ },
400
+
401
+ getBadgePosition(safeStatusPosition, config.size),
402
+
403
+ badgeStyle,
404
+ ]}
405
+ >
406
+ <Text
407
+ numberOfLines={1}
408
+ style={[
409
+ styles.badgeText,
410
+
411
+ {
412
+ fontSize: config.badgeText,
413
+
414
+ color: resolvedBadgeTextColor,
415
+ },
416
+ ]}
417
+ >
418
+ {badgeValue}
419
+ </Text>
420
+ </View>
421
+ ) : null}
422
+ </View>
423
+ );
424
+
425
+ /*
426
+ * --------------------------------------------------
427
+ * PRESSABLE
428
+ * --------------------------------------------------
429
+ */
430
+
431
+ if (onPress || onLongPress) {
432
+ return (
433
+ <Pressable
434
+ {...props}
435
+ ref={ref}
436
+ testID={testID}
437
+ disabled={disabled}
438
+ onPress={onPress}
439
+ onLongPress={onLongPress}
440
+ accessibilityRole="imagebutton"
441
+ accessibilityLabel={accessibilityLabel || name || "Avatar"}
442
+ style={({ pressed }) => [
443
+ styles.container,
444
+
445
+ {
446
+ opacity: pressed ? activeOpacity : 1,
447
+ },
448
+
449
+ containerStyle,
450
+ ]}
451
+ >
452
+ {avatarContent}
453
+ </Pressable>
454
+ );
455
+ }
456
+
457
+ /*
458
+ * --------------------------------------------------
459
+ * NON PRESSABLE
460
+ * --------------------------------------------------
461
+ */
462
+
463
+ return (
464
+ <View
465
+ {...props}
466
+ ref={ref}
467
+ testID={testID}
468
+ accessible
469
+ accessibilityRole="image"
470
+ accessibilityLabel={accessibilityLabel || name || "Avatar"}
471
+ style={[styles.container, containerStyle]}
472
+ >
473
+ {avatarContent}
474
+ </View>
475
+ );
476
+ });
477
+
478
+ function getInitials(name) {
479
+ if (!name || typeof name !== "string") {
480
+ return "";
481
+ }
482
+
483
+ const words = name.trim().split(/\s+/).filter(Boolean);
484
+
485
+ if (words.length === 1) {
486
+ return words[0].slice(0, 2).toUpperCase();
487
+ }
488
+
489
+ return (words[0][0] + words[words.length - 1][0]).toUpperCase();
490
+ }
491
+
492
+ function getBorderRadius(shape, size) {
493
+ switch (shape) {
494
+ case UI_AVATAR_SHAPES.square:
495
+ return 0;
496
+
497
+ case UI_AVATAR_SHAPES.rounded:
498
+ return Math.min(14, size * 0.25);
499
+
500
+ case UI_AVATAR_SHAPES.circle:
501
+ default:
502
+ return size / 2;
503
+ }
504
+ }
505
+
506
+ function getStatusPosition(position, size, statusSize) {
507
+ const offset = Math.max(0, statusSize * 0.05);
508
+
509
+ switch (position) {
510
+ case UI_AVATAR_STATUS_POSITIONS.topLeft:
511
+ return {
512
+ top: offset,
513
+ left: offset,
514
+ };
515
+
516
+ case UI_AVATAR_STATUS_POSITIONS.topRight:
517
+ return {
518
+ top: offset,
519
+ right: offset,
520
+ };
521
+
522
+ case UI_AVATAR_STATUS_POSITIONS.bottomLeft:
523
+ return {
524
+ bottom: offset,
525
+ left: offset,
526
+ };
527
+
528
+ case UI_AVATAR_STATUS_POSITIONS.bottomRight:
529
+ default:
530
+ return {
531
+ bottom: offset,
532
+ right: offset,
533
+ };
534
+ }
535
+ }
536
+
537
+ function getBadgePosition(position) {
538
+ switch (position) {
539
+ case UI_AVATAR_STATUS_POSITIONS.topLeft:
540
+ return {
541
+ top: -4,
542
+ left: -4,
543
+ };
544
+
545
+ case UI_AVATAR_STATUS_POSITIONS.topRight:
546
+ return {
547
+ top: -4,
548
+ right: -4,
549
+ };
550
+
551
+ case UI_AVATAR_STATUS_POSITIONS.bottomLeft:
552
+ return {
553
+ bottom: -4,
554
+ left: -4,
555
+ };
556
+
557
+ case UI_AVATAR_STATUS_POSITIONS.bottomRight:
558
+ default:
559
+ return {
560
+ bottom: -4,
561
+ right: -4,
562
+ };
563
+ }
564
+ }
565
+
566
+ function formatBadge(value) {
567
+ if (typeof value === "number") {
568
+ if (value > 99) {
569
+ return "99+";
570
+ }
571
+
572
+ return String(value);
573
+ }
574
+
575
+ return String(value);
576
+ }
577
+
578
+ const styles = StyleSheet.create({
579
+ container: {
580
+ alignSelf: "flex-start",
581
+
582
+ position: "relative",
583
+ },
584
+
585
+ avatar: {
586
+ position: "relative",
587
+
588
+ overflow: "visible",
589
+
590
+ alignItems: "center",
591
+
592
+ justifyContent: "center",
593
+ },
594
+
595
+ image: {
596
+ position: "absolute",
597
+
598
+ top: 0,
599
+
600
+ left: 0,
601
+ },
602
+
603
+ fallback: {
604
+ alignItems: "center",
605
+
606
+ justifyContent: "center",
607
+
608
+ overflow: "hidden",
609
+ },
610
+
611
+ initials: {
612
+ fontWeight: "700",
613
+
614
+ includeFontPadding: false,
615
+
616
+ textAlign: "center",
617
+ },
618
+
619
+ status: {
620
+ position: "absolute",
621
+
622
+ borderWidth: 2,
623
+ },
624
+
625
+ badge: {
626
+ position: "absolute",
627
+
628
+ paddingHorizontal: 4,
629
+
630
+ alignItems: "center",
631
+
632
+ justifyContent: "center",
633
+
634
+ borderWidth: 2,
635
+
636
+ zIndex: 10,
637
+ },
638
+
639
+ badgeText: {
640
+ fontWeight: "700",
641
+
642
+ includeFontPadding: false,
643
+
644
+ textAlign: "center",
645
+ },
646
+ });
647
+
648
+ export const UIAvatar = memo(UIAvatarComponent);
649
+
650
+ UIAvatar.displayName = "UIAvatar";
651
+
652
+ export {
653
+ UI_AVATAR_SIZES as UIAvatarSizes,
654
+ UI_AVATAR_SHAPES as UIAvatarShapes,
655
+ UI_AVATAR_STATUSES as UIAvatarStatuses,
656
+ UI_AVATAR_STATUS_POSITIONS as UIAvatarStatusPositions,
657
+ };
658
+
659
+ export default UIAvatar;
@@ -0,0 +1,7 @@
1
+ export {
2
+ UIAvatar,
3
+ UIAvatarSizes,
4
+ UIAvatarShapes,
5
+ UIAvatarStatuses,
6
+ UIAvatarStatusPositions,
7
+ } from "./UIAvatar";