rnnovaui 0.3.0 → 0.4.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.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "react-native": "src/index.js",
@@ -1,11 +1,13 @@
1
1
  import React, { memo, useCallback, useMemo, useRef } from "react";
2
2
 
3
3
  import {
4
+ ActivityIndicator,
4
5
  Animated,
5
6
  Easing,
6
7
  Image,
7
8
  Pressable,
8
9
  StyleSheet,
10
+ Text,
9
11
  View,
10
12
  } from "react-native";
11
13
 
@@ -19,35 +21,87 @@ const CARD_VARIANTS = {
19
21
  };
20
22
 
21
23
  const CARD_RADIUS = {
22
- none: 0,
23
- sm: 8,
24
- md: 12,
25
- lg: 16,
26
- xl: 20,
24
+ none: "none",
25
+ sm: "sm",
26
+ md: "md",
27
+ lg: "lg",
28
+ xl: "xl",
29
+ };
30
+
31
+ const BADGE_POSITIONS = {
32
+ topLeft: "topLeft",
33
+ topCenter: "topCenter",
34
+ topRight: "topRight",
35
+
36
+ centerLeft: "centerLeft",
37
+ center: "center",
38
+ centerRight: "centerRight",
39
+
40
+ bottomLeft: "bottomLeft",
41
+ bottomCenter: "bottomCenter",
42
+ bottomRight: "bottomRight",
27
43
  };
28
44
 
29
45
  const FALLBACK_COLORS = {
30
- background: "#FFFFFF",
31
- surface: "#F8F8F8",
32
46
  card: "#FFFFFF",
47
+ surface: "#F8F8F8",
33
48
  border: "#E5E5E5",
34
- shadow: "#000000",
49
+
50
+ primary: "#FF5A1F",
51
+ primarySoft: "#FFF0EA",
52
+
53
+ success: "#16A34A",
54
+ danger: "#DC2626",
55
+ warning: "#D97706",
56
+ info: "#2563EB",
57
+
58
+ white: "#FFFFFF",
59
+ black: "#111111",
60
+
61
+ text: "#111111",
62
+ textSecondary: "#525252",
35
63
  };
36
64
 
37
65
  function UICardComponent({
38
66
  children,
39
67
 
40
68
  header = null,
41
-
42
69
  content = null,
43
-
44
70
  footer = null,
45
71
 
46
72
  image = null,
47
-
48
73
  imageSource = null,
49
74
 
50
- imageHeight = 180,
75
+ imageHeight = 190,
76
+ imageResizeMode = "cover",
77
+ imageStyle,
78
+
79
+ /*
80
+ * Example:
81
+ *
82
+ * badges={[
83
+ * {
84
+ * id: 'offer',
85
+ * label: '20% OFF',
86
+ * position: 'topLeft',
87
+ * variant: 'danger',
88
+ * },
89
+ *
90
+ * {
91
+ * id: 'rating',
92
+ * label: '★ 4.8',
93
+ * position: 'topRight',
94
+ * variant: 'warning',
95
+ * }
96
+ * ]}
97
+ */
98
+ badges = [],
99
+
100
+ badgeGap = 8,
101
+
102
+ badgeStyle,
103
+ badgeTextStyle,
104
+ badgeContainerStyle,
51
105
 
52
106
  variant = "default",
53
107
 
@@ -58,27 +112,21 @@ function UICardComponent({
58
112
  gap = 12,
59
113
 
60
114
  style,
61
-
62
115
  headerStyle,
63
-
64
- imageStyle,
65
-
66
116
  contentStyle,
67
-
68
117
  footerStyle,
69
118
 
70
- disabled = false,
71
-
72
- loading = false,
119
+ imageOverlay = null,
120
+ imageOverlayStyle,
73
121
 
74
122
  pressable = false,
75
123
 
76
- onPress,
124
+ disabled = false,
125
+ loading = false,
77
126
 
127
+ onPress,
78
128
  onLongPress,
79
-
80
129
  onPressIn,
81
-
82
130
  onPressOut,
83
131
 
84
132
  pressAnimation = true,
@@ -90,7 +138,6 @@ function UICardComponent({
90
138
  testID,
91
139
 
92
140
  accessibilityLabel,
93
-
94
141
  accessibilityHint,
95
142
 
96
143
  ...props
@@ -107,7 +154,9 @@ function UICardComponent({
107
154
 
108
155
  const safeVariant = CARD_VARIANTS[variant] ? variant : CARD_VARIANTS.default;
109
156
 
110
- const resolvedRadius = CARD_RADIUS[radius] ?? CARD_RADIUS.lg;
157
+ const safeRadius = CARD_RADIUS[radius] ? radius : CARD_RADIUS.lg;
158
+
159
+ const radiusValue = getRadiusValue(safeRadius, theme);
111
160
 
112
161
  const cardColors = useMemo(
113
162
  () => getCardColors(safeVariant, colors),
@@ -215,14 +264,8 @@ function UICardComponent({
215
264
 
216
265
  borderWidth: cardColors.borderWidth,
217
266
 
218
- borderRadius: resolvedRadius,
267
+ borderRadius: radiusValue,
219
268
 
220
- padding,
221
- },
222
-
223
- cardColors.shadow,
224
-
225
- {
226
269
  opacity: isDisabled ? 0.55 : 1,
227
270
 
228
271
  transform: [
@@ -232,94 +275,152 @@ function UICardComponent({
232
275
  ],
233
276
  },
234
277
 
278
+ cardColors.shadow,
279
+
235
280
  style,
236
281
  ],
237
- [cardColors, resolvedRadius, padding, isDisabled, scale, style],
282
+ [cardColors, radiusValue, isDisabled, scale, style],
238
283
  );
239
284
 
285
+ const hasImage = Boolean(image || imageSource);
286
+
287
+ const hasBody = Boolean(header || content || children || footer);
288
+
240
289
  const cardContent = (
241
290
  <>
242
- {image || imageSource ? (
291
+ {/* ------------------------------------------ */}
292
+ {/* IMAGE */}
293
+ {/* ------------------------------------------ */}
294
+
295
+ {hasImage ? (
243
296
  <View
244
297
  style={[
245
- styles.imageWrapper,
298
+ styles.imageSection,
299
+
246
300
  {
247
301
  height: imageHeight,
248
302
 
249
- borderTopLeftRadius: resolvedRadius,
303
+ borderTopLeftRadius: radiusValue,
250
304
 
251
- borderTopRightRadius: resolvedRadius,
305
+ borderTopRightRadius: radiusValue,
252
306
  },
253
307
  ]}
254
308
  >
255
- {image || (
309
+ {image ? (
310
+ image
311
+ ) : (
256
312
  <Image
257
313
  source={imageSource}
258
- resizeMode="cover"
314
+ resizeMode={imageResizeMode}
259
315
  style={[styles.image, imageStyle]}
260
316
  />
261
317
  )}
262
- </View>
263
- ) : null}
264
318
 
265
- {header ? (
266
- <View
267
- style={[
268
- styles.header,
269
- {
270
- marginTop: image || imageSource ? gap : 0,
271
- },
272
- headerStyle,
273
- ]}
274
- >
275
- {header}
319
+ {/* -------------------------------------- */}
320
+ {/* IMAGE OVERLAY */}
321
+ {/* -------------------------------------- */}
322
+
323
+ {imageOverlay ? (
324
+ <View
325
+ pointerEvents="none"
326
+ style={[styles.imageOverlay, imageOverlayStyle]}
327
+ >
328
+ {imageOverlay}
329
+ </View>
330
+ ) : null}
331
+
332
+ {/* -------------------------------------- */}
333
+ {/* BADGES */}
334
+ {/* -------------------------------------- */}
335
+
336
+ {Array.isArray(badges) && badges.length > 0 ? (
337
+ <View pointerEvents="box-none" style={styles.badgeLayer}>
338
+ {badges.map((badge, index) => (
339
+ <CardBadge
340
+ key={badge?.id || badge?.key || `badge-${index}`}
341
+ badge={badge}
342
+ index={index}
343
+ badgeGap={badgeGap}
344
+ defaultStyle={badgeStyle}
345
+ defaultTextStyle={badgeTextStyle}
346
+ defaultContainerStyle={badgeContainerStyle}
347
+ colors={colors}
348
+ />
349
+ ))}
350
+ </View>
351
+ ) : null}
276
352
  </View>
277
353
  ) : null}
278
354
 
279
- {content ? (
280
- <View
281
- style={[
282
- styles.content,
283
- {
284
- marginTop: header ? gap : 0,
285
- },
286
- contentStyle,
287
- ]}
288
- >
289
- {content}
290
- </View>
291
- ) : null}
355
+ {/* ------------------------------------------ */}
356
+ {/* BODY */}
357
+ {/* ------------------------------------------ */}
292
358
 
293
- {children ? (
359
+ {hasBody ? (
294
360
  <View
295
361
  style={[
296
- styles.content,
362
+ styles.body,
363
+
297
364
  {
298
- marginTop: header || content ? gap : 0,
365
+ padding,
366
+ gap,
299
367
  },
300
- contentStyle,
301
368
  ]}
302
369
  >
303
- {children}
370
+ {/* HEADER */}
371
+
372
+ {header ? (
373
+ <View style={[styles.header, headerStyle]}>{header}</View>
374
+ ) : null}
375
+
376
+ {/* CONTENT */}
377
+
378
+ {content ? (
379
+ <View style={[styles.content, contentStyle]}>{content}</View>
380
+ ) : null}
381
+
382
+ {/* CHILDREN */}
383
+
384
+ {children ? (
385
+ <View style={[styles.content, contentStyle]}>{children}</View>
386
+ ) : null}
387
+
388
+ {/* FOOTER */}
389
+
390
+ {footer ? (
391
+ <View style={[styles.footer, footerStyle]}>{footer}</View>
392
+ ) : null}
304
393
  </View>
305
394
  ) : null}
306
395
 
307
- {footer ? (
396
+ {/* ------------------------------------------ */}
397
+ {/* LOADING */}
398
+ {/* ------------------------------------------ */}
399
+
400
+ {loading ? (
308
401
  <View
402
+ pointerEvents="none"
309
403
  style={[
310
- styles.footer,
404
+ styles.loadingOverlay,
405
+
311
406
  {
312
- marginTop: header || content || children ? gap : 0,
407
+ borderRadius: radiusValue,
313
408
  },
314
- footerStyle,
315
409
  ]}
316
410
  >
317
- {footer}
411
+ <ActivityIndicator
412
+ size="small"
413
+ color={colors.primary || FALLBACK_COLORS.primary}
414
+ />
318
415
  </View>
319
416
  ) : null}
320
417
  </>
321
418
  );
322
419
 
420
+ /*
421
+ * Non-pressable card
422
+ */
423
+
323
424
  if (!pressable) {
324
425
  return (
325
426
  <Animated.View
@@ -333,6 +434,10 @@ function UICardComponent({
333
434
  );
334
435
  }
335
436
 
437
+ /*
438
+ * Pressable card
439
+ */
440
+
336
441
  return (
337
442
  <Animated.View {...props} testID={testID} style={cardStyle}>
338
443
  <Pressable
@@ -357,6 +462,379 @@ function UICardComponent({
357
462
  );
358
463
  }
359
464
 
465
+ /* ================================================= */
466
+ /* CARD BADGE */
467
+ /* ================================================= */
468
+
469
+ const CardBadge = memo(function CardBadge({
470
+ badge,
471
+ index,
472
+ badgeGap,
473
+
474
+ defaultStyle,
475
+ defaultTextStyle,
476
+ defaultContainerStyle,
477
+
478
+ colors,
479
+ }) {
480
+ if (!badge) {
481
+ return null;
482
+ }
483
+
484
+ const position = BADGE_POSITIONS[badge.position]
485
+ ? badge.position
486
+ : BADGE_POSITIONS.topLeft;
487
+
488
+ const badgeColors = getBadgeColors(badge, colors);
489
+
490
+ const positionStyle = getBadgePosition(position, index, badgeGap);
491
+
492
+ const label = badge.label ?? badge.text ?? badge.content ?? "";
493
+
494
+ return (
495
+ <View
496
+ pointerEvents="none"
497
+ style={[
498
+ styles.badge,
499
+
500
+ positionStyle,
501
+
502
+ {
503
+ backgroundColor: badge.backgroundColor || badgeColors.background,
504
+
505
+ borderColor: badge.borderColor || badgeColors.border,
506
+
507
+ borderWidth: badge.borderWidth ?? badgeColors.borderWidth,
508
+
509
+ borderRadius: badge.borderRadius ?? 999,
510
+ },
511
+
512
+ defaultStyle,
513
+
514
+ badge.style,
515
+
516
+ defaultContainerStyle,
517
+ ]}
518
+ >
519
+ {badge.icon ? (
520
+ <View
521
+ style={[
522
+ styles.badgeIcon,
523
+
524
+ {
525
+ marginRight: label ? 5 : 0,
526
+ },
527
+ ]}
528
+ >
529
+ {badge.icon}
530
+ </View>
531
+ ) : null}
532
+
533
+ {label !== "" ? (
534
+ <Text
535
+ numberOfLines={1}
536
+ ellipsizeMode="tail"
537
+ style={[
538
+ styles.badgeText,
539
+
540
+ {
541
+ color: badge.textColor || badgeColors.text,
542
+
543
+ fontSize: badge.fontSize || 12,
544
+
545
+ lineHeight: badge.lineHeight || 16,
546
+
547
+ fontWeight: badge.fontWeight || "600",
548
+ },
549
+
550
+ defaultTextStyle,
551
+
552
+ badge.textStyle,
553
+ ]}
554
+ >
555
+ {label}
556
+ </Text>
557
+ ) : null}
558
+ </View>
559
+ );
560
+ });
561
+
562
+ CardBadge.displayName = "CardBadge";
563
+
564
+ /* ================================================= */
565
+ /* BADGE POSITIONING */
566
+ /* ================================================= */
567
+
568
+ function getBadgePosition(position, index, gap) {
569
+ /*
570
+ * IMPORTANT:
571
+ *
572
+ * Each position has a fixed anchor.
573
+ *
574
+ * index only creates an offset for
575
+ * multiple badges using THE SAME
576
+ * position.
577
+ */
578
+
579
+ const offset = index * (32 + gap);
580
+
581
+ switch (position) {
582
+ /* -------------------------------------------- */
583
+ /* TOP LEFT */
584
+ /* -------------------------------------------- */
585
+
586
+ case BADGE_POSITIONS.topLeft:
587
+ return {
588
+ top: 12,
589
+
590
+ left: 12 + offset,
591
+ };
592
+
593
+ /* -------------------------------------------- */
594
+ /* TOP CENTER */
595
+ /* -------------------------------------------- */
596
+
597
+ case BADGE_POSITIONS.topCenter:
598
+ return {
599
+ top: 12,
600
+
601
+ left: "50%",
602
+
603
+ transform: [
604
+ {
605
+ translateX: -50,
606
+ },
607
+ ],
608
+ };
609
+
610
+ /* -------------------------------------------- */
611
+ /* TOP RIGHT */
612
+ /* -------------------------------------------- */
613
+
614
+ case BADGE_POSITIONS.topRight:
615
+ return {
616
+ top: 12,
617
+
618
+ right: 12 + offset,
619
+ };
620
+
621
+ /* -------------------------------------------- */
622
+ /* CENTER LEFT */
623
+ /* -------------------------------------------- */
624
+
625
+ case BADGE_POSITIONS.centerLeft:
626
+ return {
627
+ left: 12,
628
+
629
+ top: "50%",
630
+
631
+ transform: [
632
+ {
633
+ translateY: -50,
634
+ },
635
+ ],
636
+ };
637
+
638
+ /* -------------------------------------------- */
639
+ /* CENTER */
640
+ /* -------------------------------------------- */
641
+
642
+ case BADGE_POSITIONS.center:
643
+ return {
644
+ left: "50%",
645
+
646
+ top: "50%",
647
+
648
+ transform: [
649
+ {
650
+ translateX: -50,
651
+ },
652
+
653
+ {
654
+ translateY: -50,
655
+ },
656
+ ],
657
+ };
658
+
659
+ /* -------------------------------------------- */
660
+ /* CENTER RIGHT */
661
+ /* -------------------------------------------- */
662
+
663
+ case BADGE_POSITIONS.centerRight:
664
+ return {
665
+ right: 12,
666
+
667
+ top: "50%",
668
+
669
+ transform: [
670
+ {
671
+ translateY: -50,
672
+ },
673
+ ],
674
+ };
675
+
676
+ /* -------------------------------------------- */
677
+ /* BOTTOM LEFT */
678
+ /* -------------------------------------------- */
679
+
680
+ case BADGE_POSITIONS.bottomLeft:
681
+ return {
682
+ bottom: 12,
683
+
684
+ left: 12 + offset,
685
+ };
686
+
687
+ /* -------------------------------------------- */
688
+ /* BOTTOM CENTER */
689
+ /* -------------------------------------------- */
690
+
691
+ case BADGE_POSITIONS.bottomCenter:
692
+ return {
693
+ bottom: 12,
694
+
695
+ left: "50%",
696
+
697
+ transform: [
698
+ {
699
+ translateX: -50,
700
+ },
701
+ ],
702
+ };
703
+
704
+ /* -------------------------------------------- */
705
+ /* BOTTOM RIGHT */
706
+ /* -------------------------------------------- */
707
+
708
+ case BADGE_POSITIONS.bottomRight:
709
+ return {
710
+ bottom: 12,
711
+
712
+ right: 12 + offset,
713
+ };
714
+
715
+ default:
716
+ return {
717
+ top: 12,
718
+
719
+ left: 12,
720
+ };
721
+ }
722
+ }
723
+
724
+ /* ================================================= */
725
+ /* BADGE COLORS */
726
+ /* ================================================= */
727
+
728
+ function getBadgeColors(badge, colors) {
729
+ const variant = badge.variant || badge.type || "primary";
730
+
731
+ switch (variant) {
732
+ case "success":
733
+ return {
734
+ background: colors.success || FALLBACK_COLORS.success,
735
+
736
+ border: colors.success || FALLBACK_COLORS.success,
737
+
738
+ borderWidth: 0,
739
+
740
+ text: colors.onSuccess || FALLBACK_COLORS.white,
741
+ };
742
+
743
+ case "danger":
744
+ return {
745
+ background: colors.danger || FALLBACK_COLORS.danger,
746
+
747
+ border: colors.danger || FALLBACK_COLORS.danger,
748
+
749
+ borderWidth: 0,
750
+
751
+ text: colors.onDanger || FALLBACK_COLORS.white,
752
+ };
753
+
754
+ case "warning":
755
+ return {
756
+ background: colors.warning || FALLBACK_COLORS.warning,
757
+
758
+ border: colors.warning || FALLBACK_COLORS.warning,
759
+
760
+ borderWidth: 0,
761
+
762
+ text: colors.onWarning || FALLBACK_COLORS.white,
763
+ };
764
+
765
+ case "info":
766
+ return {
767
+ background: colors.info || FALLBACK_COLORS.info,
768
+
769
+ border: colors.info || FALLBACK_COLORS.info,
770
+
771
+ borderWidth: 0,
772
+
773
+ text: colors.onInfo || FALLBACK_COLORS.white,
774
+ };
775
+
776
+ case "soft":
777
+ return {
778
+ background: colors.primarySoft || FALLBACK_COLORS.primarySoft,
779
+
780
+ border: "transparent",
781
+
782
+ borderWidth: 0,
783
+
784
+ text: colors.primary || FALLBACK_COLORS.primary,
785
+ };
786
+
787
+ case "secondary":
788
+ return {
789
+ background: colors.secondary || "#7CFF32",
790
+
791
+ border: colors.secondary || "#7CFF32",
792
+
793
+ borderWidth: 0,
794
+
795
+ text: colors.onSecondary || FALLBACK_COLORS.black,
796
+ };
797
+
798
+ case "neutral":
799
+ return {
800
+ background: colors.surface || FALLBACK_COLORS.surface,
801
+
802
+ border: colors.border || FALLBACK_COLORS.border,
803
+
804
+ borderWidth: 1,
805
+
806
+ text: colors.textSecondary || FALLBACK_COLORS.textSecondary,
807
+ };
808
+
809
+ case "custom":
810
+ return {
811
+ background: badge.backgroundColor || FALLBACK_COLORS.primary,
812
+
813
+ border: badge.borderColor || "transparent",
814
+
815
+ borderWidth: badge.borderWidth ?? 0,
816
+
817
+ text: badge.textColor || FALLBACK_COLORS.white,
818
+ };
819
+
820
+ case "primary":
821
+ default:
822
+ return {
823
+ background: colors.primary || FALLBACK_COLORS.primary,
824
+
825
+ border: colors.primary || FALLBACK_COLORS.primary,
826
+
827
+ borderWidth: 0,
828
+
829
+ text: colors.onPrimary || FALLBACK_COLORS.white,
830
+ };
831
+ }
832
+ }
833
+
834
+ /* ================================================= */
835
+ /* CARD COLORS */
836
+ /* ================================================= */
837
+
360
838
  function getCardColors(variant, colors) {
361
839
  const background = colors.card || colors.surface || FALLBACK_COLORS.card;
362
840
 
@@ -389,7 +867,7 @@ function getCardColors(variant, colors) {
389
867
  return {
390
868
  background: colors.surface || FALLBACK_COLORS.surface,
391
869
 
392
- border,
870
+ border: "transparent",
393
871
 
394
872
  borderWidth: 0,
395
873
 
@@ -401,7 +879,7 @@ function getCardColors(variant, colors) {
401
879
  return {
402
880
  background,
403
881
 
404
- border,
882
+ border: "transparent",
405
883
 
406
884
  borderWidth: 0,
407
885
 
@@ -410,27 +888,55 @@ function getCardColors(variant, colors) {
410
888
  }
411
889
  }
412
890
 
891
+ /* ================================================= */
892
+ /* RADIUS */
893
+ /* ================================================= */
894
+
895
+ function getRadiusValue(radius, theme) {
896
+ const themeRadius = theme?.radius || {};
897
+
898
+ switch (radius) {
899
+ case CARD_RADIUS.none:
900
+ return 0;
901
+
902
+ case CARD_RADIUS.sm:
903
+ return themeRadius.sm ?? 8;
904
+
905
+ case CARD_RADIUS.md:
906
+ return themeRadius.md ?? 12;
907
+
908
+ case CARD_RADIUS.xl:
909
+ return themeRadius.xl ?? 20;
910
+
911
+ case CARD_RADIUS.lg:
912
+ default:
913
+ return themeRadius.lg ?? 16;
914
+ }
915
+ }
916
+
917
+ /* ================================================= */
918
+ /* STYLES */
919
+ /* ================================================= */
920
+
413
921
  const styles = StyleSheet.create({
414
922
  card: {
415
923
  width: "100%",
416
924
 
417
925
  overflow: "hidden",
926
+
927
+ position: "relative",
418
928
  },
419
929
 
420
930
  pressable: {
421
931
  width: "100%",
422
932
  },
423
933
 
424
- imageWrapper: {
934
+ imageSection: {
425
935
  width: "100%",
426
936
 
427
- overflow: "hidden",
428
-
429
- marginHorizontal: -16,
937
+ position: "relative",
430
938
 
431
- marginTop: -16,
432
-
433
- width: "calc(100% + 32px)",
939
+ overflow: "hidden",
434
940
  },
435
941
 
436
942
  image: {
@@ -439,6 +945,71 @@ const styles = StyleSheet.create({
439
945
  height: "100%",
440
946
  },
441
947
 
948
+ imageOverlay: {
949
+ ...StyleSheet.absoluteFillObject,
950
+
951
+ zIndex: 5,
952
+ },
953
+
954
+ /*
955
+ * This is the key fix.
956
+ *
957
+ * The badge layer fills the complete
958
+ * image section and does not participate
959
+ * in normal layout.
960
+ */
961
+ badgeLayer: {
962
+ ...StyleSheet.absoluteFillObject,
963
+
964
+ zIndex: 20,
965
+
966
+ elevation: 20,
967
+ },
968
+
969
+ badge: {
970
+ position: "absolute",
971
+
972
+ minHeight: 28,
973
+
974
+ paddingHorizontal: 10,
975
+
976
+ paddingVertical: 5,
977
+
978
+ flexDirection: "row",
979
+
980
+ alignItems: "center",
981
+
982
+ justifyContent: "center",
983
+
984
+ maxWidth: "75%",
985
+
986
+ zIndex: 30,
987
+
988
+ elevation: 30,
989
+
990
+ overflow: "hidden",
991
+ },
992
+
993
+ badgeText: {
994
+ includeFontPadding: false,
995
+
996
+ textAlign: "center",
997
+
998
+ flexShrink: 1,
999
+ },
1000
+
1001
+ badgeIcon: {
1002
+ alignItems: "center",
1003
+
1004
+ justifyContent: "center",
1005
+
1006
+ flexShrink: 0,
1007
+ },
1008
+
1009
+ body: {
1010
+ width: "100%",
1011
+ },
1012
+
442
1013
  header: {
443
1014
  width: "100%",
444
1015
  },
@@ -451,11 +1022,24 @@ const styles = StyleSheet.create({
451
1022
  width: "100%",
452
1023
  },
453
1024
 
1025
+ loadingOverlay: {
1026
+ ...StyleSheet.absoluteFillObject,
1027
+
1028
+ alignItems: "center",
1029
+
1030
+ justifyContent: "center",
1031
+
1032
+ backgroundColor: "rgba(0,0,0,0.08)",
1033
+
1034
+ zIndex: 100,
1035
+ },
1036
+
454
1037
  elevated: {
455
- shadowColor: FALLBACK_COLORS.shadow,
1038
+ shadowColor: "#000000",
456
1039
 
457
1040
  shadowOffset: {
458
1041
  width: 0,
1042
+
459
1043
  height: 4,
460
1044
  },
461
1045
 
@@ -477,6 +1061,10 @@ export const UICard = memo(UICardComponent);
477
1061
 
478
1062
  UICard.displayName = "UICard";
479
1063
 
480
- export { CARD_VARIANTS as UICardVariants, CARD_RADIUS as UICardRadius };
1064
+ export {
1065
+ CARD_VARIANTS as UICardVariants,
1066
+ CARD_RADIUS as UICardRadius,
1067
+ BADGE_POSITIONS as UICardBadgePositions,
1068
+ };
481
1069
 
482
1070
  export default UICard;
@@ -1 +1,6 @@
1
- export { UICard, UICardVariants, UICardRadius } from "./UICard";
1
+ export {
2
+ UICard,
3
+ UICardVariants,
4
+ UICardRadius,
5
+ UICardBadgePositions,
6
+ } from "./UICard";
@@ -0,0 +1,401 @@
1
+ import React, { memo, useMemo } from "react";
2
+
3
+ import { StyleSheet, Text, View } from "react-native";
4
+
5
+ import { useUITheme } from "../../theme";
6
+
7
+ const DIVIDER_ORIENTATIONS = {
8
+ horizontal: "horizontal",
9
+ vertical: "vertical",
10
+ };
11
+
12
+ const DIVIDER_VARIANTS = {
13
+ solid: "solid",
14
+ dashed: "dashed",
15
+ };
16
+
17
+ const DIVIDER_TEXT_POSITIONS = {
18
+ left: "left",
19
+ center: "center",
20
+ right: "right",
21
+ };
22
+
23
+ const DIVIDER_ENDS = {
24
+ rounded: "rounded",
25
+ sharp: "sharp",
26
+ leftSharp: "leftSharp",
27
+ rightSharp: "rightSharp",
28
+ };
29
+
30
+ function UIDividerComponent({
31
+ orientation = "horizontal",
32
+
33
+ variant = "solid",
34
+
35
+ thickness = 1,
36
+
37
+ color,
38
+
39
+ length,
40
+
41
+ spacing = 0,
42
+
43
+ text = null,
44
+
45
+ textPosition = "center",
46
+
47
+ textStyle,
48
+
49
+ textContainerStyle,
50
+
51
+ lineStyle,
52
+
53
+ gap = 12,
54
+
55
+ ends = "rounded",
56
+
57
+ style,
58
+
59
+ testID,
60
+
61
+ ...props
62
+ }) {
63
+ const { theme } = useUITheme();
64
+
65
+ const colors = theme?.colors || {};
66
+
67
+ const safeOrientation = DIVIDER_ORIENTATIONS[orientation]
68
+ ? orientation
69
+ : DIVIDER_ORIENTATIONS.horizontal;
70
+
71
+ const safeVariant = DIVIDER_VARIANTS[variant]
72
+ ? variant
73
+ : DIVIDER_VARIANTS.solid;
74
+
75
+ const safeTextPosition = DIVIDER_TEXT_POSITIONS[textPosition]
76
+ ? textPosition
77
+ : DIVIDER_TEXT_POSITIONS.center;
78
+
79
+ const safeEnds = DIVIDER_ENDS[ends] ? ends : DIVIDER_ENDS.rounded;
80
+
81
+ const resolvedColor = color || colors.divider || colors.border || "#E5E5E5";
82
+
83
+ const resolvedTextColor =
84
+ colors.textSecondary || colors.textMuted || "#737373";
85
+
86
+ const isHorizontal = safeOrientation === DIVIDER_ORIENTATIONS.horizontal;
87
+
88
+ const hasText = isHorizontal && text !== null && text !== undefined;
89
+
90
+ const lineBaseStyle = useMemo(
91
+ () => ({
92
+ backgroundColor:
93
+ safeVariant === DIVIDER_VARIANTS.solid ? resolvedColor : "transparent",
94
+
95
+ borderColor: resolvedColor,
96
+
97
+ borderStyle: safeVariant === DIVIDER_VARIANTS.dashed ? "dashed" : "solid",
98
+
99
+ borderWidth: safeVariant === DIVIDER_VARIANTS.dashed ? thickness : 0,
100
+
101
+ borderRadius: getEndRadius(safeEnds, thickness),
102
+ }),
103
+ [safeVariant, resolvedColor, thickness, safeEnds],
104
+ );
105
+
106
+ /*
107
+ * ------------------------------------------
108
+ * VERTICAL DIVIDER
109
+ * ------------------------------------------
110
+ */
111
+
112
+ if (!isHorizontal) {
113
+ return (
114
+ <View
115
+ {...props}
116
+ testID={testID}
117
+ pointerEvents="none"
118
+ style={[
119
+ styles.vertical,
120
+
121
+ {
122
+ width: thickness,
123
+
124
+ height: length || "100%",
125
+
126
+ marginHorizontal: spacing,
127
+
128
+ backgroundColor:
129
+ safeVariant === DIVIDER_VARIANTS.solid
130
+ ? resolvedColor
131
+ : "transparent",
132
+
133
+ borderColor: resolvedColor,
134
+
135
+ borderStyle:
136
+ safeVariant === DIVIDER_VARIANTS.dashed ? "dashed" : "solid",
137
+
138
+ borderWidth:
139
+ safeVariant === DIVIDER_VARIANTS.dashed ? thickness : 0,
140
+
141
+ borderRadius: getEndRadius(safeEnds, thickness),
142
+ },
143
+
144
+ style,
145
+ ]}
146
+ />
147
+ );
148
+ }
149
+
150
+ /*
151
+ * ------------------------------------------
152
+ * HORIZONTAL WITHOUT TEXT
153
+ * ------------------------------------------
154
+ */
155
+
156
+ if (!hasText) {
157
+ return (
158
+ <View
159
+ {...props}
160
+ testID={testID}
161
+ pointerEvents="none"
162
+ style={[
163
+ styles.horizontal,
164
+
165
+ {
166
+ width: length || "100%",
167
+
168
+ height: thickness,
169
+
170
+ marginVertical: spacing,
171
+ },
172
+
173
+ lineBaseStyle,
174
+
175
+ lineStyle,
176
+
177
+ style,
178
+ ]}
179
+ />
180
+ );
181
+ }
182
+
183
+ /*
184
+ * ------------------------------------------
185
+ * HORIZONTAL WITH TEXT
186
+ * ------------------------------------------
187
+ */
188
+
189
+ return (
190
+ <View
191
+ {...props}
192
+ testID={testID}
193
+ pointerEvents="none"
194
+ style={[
195
+ styles.textDivider,
196
+
197
+ {
198
+ width: length || "100%",
199
+
200
+ marginVertical: spacing,
201
+ },
202
+
203
+ style,
204
+ ]}
205
+ >
206
+ {safeTextPosition === DIVIDER_TEXT_POSITIONS.left ? (
207
+ <>
208
+ <View style={[styles.line, lineBaseStyle, lineStyle]} />
209
+
210
+ <View
211
+ style={[
212
+ styles.textContainer,
213
+
214
+ {
215
+ marginLeft: gap,
216
+ },
217
+
218
+ textContainerStyle,
219
+ ]}
220
+ >
221
+ <Text
222
+ style={[
223
+ styles.text,
224
+
225
+ {
226
+ color: resolvedTextColor,
227
+ },
228
+
229
+ textStyle,
230
+ ]}
231
+ >
232
+ {text}
233
+ </Text>
234
+ </View>
235
+ </>
236
+ ) : null}
237
+
238
+ {safeTextPosition === DIVIDER_TEXT_POSITIONS.center ? (
239
+ <>
240
+ <View style={[styles.line, lineBaseStyle, lineStyle]} />
241
+
242
+ <View
243
+ style={[
244
+ styles.textContainer,
245
+
246
+ {
247
+ marginHorizontal: gap,
248
+ },
249
+
250
+ textContainerStyle,
251
+ ]}
252
+ >
253
+ <Text
254
+ style={[
255
+ styles.text,
256
+
257
+ {
258
+ color: resolvedTextColor,
259
+ },
260
+
261
+ textStyle,
262
+ ]}
263
+ >
264
+ {text}
265
+ </Text>
266
+ </View>
267
+
268
+ <View style={[styles.line, lineBaseStyle, lineStyle]} />
269
+ </>
270
+ ) : null}
271
+
272
+ {safeTextPosition === DIVIDER_TEXT_POSITIONS.right ? (
273
+ <>
274
+ <View
275
+ style={[
276
+ styles.textContainer,
277
+
278
+ {
279
+ marginRight: gap,
280
+ },
281
+
282
+ textContainerStyle,
283
+ ]}
284
+ >
285
+ <Text
286
+ style={[
287
+ styles.text,
288
+
289
+ {
290
+ color: resolvedTextColor,
291
+ },
292
+
293
+ textStyle,
294
+ ]}
295
+ >
296
+ {text}
297
+ </Text>
298
+ </View>
299
+
300
+ <View style={[styles.line, lineBaseStyle, lineStyle]} />
301
+ </>
302
+ ) : null}
303
+ </View>
304
+ );
305
+ }
306
+
307
+ /*
308
+ * ------------------------------------------
309
+ * END RADIUS
310
+ * ------------------------------------------
311
+ */
312
+
313
+ function getEndRadius(ends, thickness) {
314
+ switch (ends) {
315
+ case DIVIDER_ENDS.sharp:
316
+ return 0;
317
+
318
+ case DIVIDER_ENDS.leftSharp:
319
+ return {
320
+ borderTopLeftRadius: 0,
321
+ borderBottomLeftRadius: 0,
322
+
323
+ borderTopRightRadius: thickness,
324
+
325
+ borderBottomRightRadius: thickness,
326
+ };
327
+
328
+ case DIVIDER_ENDS.rightSharp:
329
+ return {
330
+ borderTopLeftRadius: thickness,
331
+
332
+ borderBottomLeftRadius: thickness,
333
+
334
+ borderTopRightRadius: 0,
335
+ borderBottomRightRadius: 0,
336
+ };
337
+
338
+ case DIVIDER_ENDS.rounded:
339
+ default:
340
+ return thickness / 2;
341
+ }
342
+ }
343
+
344
+ const styles = StyleSheet.create({
345
+ horizontal: {
346
+ flexShrink: 0,
347
+ },
348
+
349
+ vertical: {
350
+ flexShrink: 0,
351
+ },
352
+
353
+ textDivider: {
354
+ flexDirection: "row",
355
+
356
+ alignItems: "center",
357
+
358
+ flexShrink: 0,
359
+ },
360
+
361
+ line: {
362
+ flex: 1,
363
+
364
+ minWidth: 0,
365
+
366
+ height: 1,
367
+ },
368
+
369
+ textContainer: {
370
+ flexShrink: 0,
371
+
372
+ alignItems: "center",
373
+
374
+ justifyContent: "center",
375
+ },
376
+
377
+ text: {
378
+ fontSize: 13,
379
+
380
+ lineHeight: 18,
381
+
382
+ fontWeight: "500",
383
+
384
+ includeFontPadding: false,
385
+
386
+ textAlign: "center",
387
+ },
388
+ });
389
+
390
+ export const UIDivider = memo(UIDividerComponent);
391
+
392
+ UIDivider.displayName = "UIDivider";
393
+
394
+ export {
395
+ DIVIDER_ORIENTATIONS as UIDividerOrientations,
396
+ DIVIDER_VARIANTS as UIDividerVariants,
397
+ DIVIDER_TEXT_POSITIONS as UIDividerTextPositions,
398
+ DIVIDER_ENDS as UIDividerEnds,
399
+ };
400
+
401
+ export default UIDivider;
@@ -0,0 +1,7 @@
1
+ export {
2
+ UIDivider,
3
+ UIDividerOrientations,
4
+ UIDividerVariants,
5
+ UIDividerTextPositions,
6
+ UIDividerEnds,
7
+ } from "./UIDivider";
@@ -6,4 +6,16 @@ export {
6
6
  UIIconButtonSizes,
7
7
  UIIconButtonShapes,
8
8
  } from "./IconButton";
9
- export { UICard, UICardVariants, UICardRadius } from "./Card";
9
+ export {
10
+ UICard,
11
+ UICardVariants,
12
+ UICardRadius,
13
+ UICardBadgePositions,
14
+ } from "./Card";
15
+ export {
16
+ UIDivider,
17
+ UIDividerOrientations,
18
+ UIDividerVariants,
19
+ UIDividerTextPositions,
20
+ UIDividerEnds,
21
+ } from "./Divider";
package/src/index.js CHANGED
@@ -27,4 +27,10 @@ export {
27
27
  UICard,
28
28
  UICardVariants,
29
29
  UICardRadius,
30
+ UICardBadgePositions,
31
+ UIDivider,
32
+ UIDividerOrientations,
33
+ UIDividerVariants,
34
+ UIDividerTextPositions,
35
+ UIDividerEnds,
30
36
  } from "./components";