@fadyshawky/react-native-magic 2.2.1 → 2.3.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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -56
  3. package/index.js +4 -0
  4. package/package.json +8 -2
  5. package/scripts/askPackageName.js +10 -5
  6. package/template/.env.development +8 -6
  7. package/template/.env.example +15 -5
  8. package/template/.env.production +8 -6
  9. package/template/.env.staging +8 -6
  10. package/template/.eslintrc.js +14 -0
  11. package/template/.husky/pre-commit +1 -0
  12. package/template/App.tsx +47 -16
  13. package/template/__tests__/App.test.tsx +28 -10
  14. package/template/babel.config.js +20 -1
  15. package/template/docs/ARCHITECTURE.md +40 -10
  16. package/template/docs/BEST_PRACTICES.md +10 -1
  17. package/template/docs/CUSTOMIZATION.md +118 -5
  18. package/template/docs/design-system.html +1164 -0
  19. package/template/docs/wireframes.html +411 -0
  20. package/template/index.js +10 -0
  21. package/template/jest.config.js +16 -1
  22. package/template/jest.setup.js +61 -0
  23. package/template/package-lock.json +12178 -8293
  24. package/template/package.json +53 -20
  25. package/template/react-native.config.js +3 -0
  26. package/template/resources/fonts/.gitkeep +0 -0
  27. package/template/scripts/ci-sync-env.cjs +71 -0
  28. package/template/src/assets/brand/logo-mark.svg +8 -0
  29. package/template/src/assets/brand/logo-mono.svg +9 -0
  30. package/template/src/assets/brand/logo-primary.svg +15 -0
  31. package/template/src/assets/brand/wordmark-dark.svg +18 -0
  32. package/template/src/common/components/AppBottomSheet.tsx +87 -0
  33. package/template/src/common/components/AppSwitch.tsx +75 -0
  34. package/template/src/common/components/AppTextInput.tsx +161 -0
  35. package/template/src/common/components/Avatar.tsx +75 -0
  36. package/template/src/common/components/Badge.tsx +66 -0
  37. package/template/src/common/components/CardScroller.tsx +58 -0
  38. package/template/src/common/components/Cards.tsx +13 -7
  39. package/template/src/common/components/Carousel.tsx +196 -0
  40. package/template/src/common/components/Checkbox.tsx +85 -0
  41. package/template/src/common/components/Chip.tsx +55 -0
  42. package/template/src/common/components/Dropdown.tsx +202 -0
  43. package/template/src/common/components/ErrorBoundary.tsx +82 -0
  44. package/template/src/common/components/FlatListWrapper.tsx +4 -5
  45. package/template/src/common/components/ListItem.tsx +90 -0
  46. package/template/src/common/components/LoadingComponent.tsx +8 -2
  47. package/template/src/common/components/Logo.tsx +77 -0
  48. package/template/src/common/components/ModalDialog.tsx +141 -0
  49. package/template/src/common/components/NetworkBanner.tsx +47 -0
  50. package/template/src/common/components/OTPInput.tsx +0 -1
  51. package/template/src/common/components/PrimaryButton.tsx +0 -14
  52. package/template/src/common/components/PrimaryTextInput.tsx +66 -130
  53. package/template/src/common/components/RadioGroup.tsx +95 -0
  54. package/template/src/common/components/SafeText.tsx +4 -3
  55. package/template/src/common/components/SearchBar.tsx +7 -5
  56. package/template/src/common/components/SegmentedControl.tsx +77 -0
  57. package/template/src/common/components/Skeleton.tsx +47 -0
  58. package/template/src/common/components/TryAgain.tsx +4 -2
  59. package/template/src/common/helpers/arrayHelpers.ts +2 -2
  60. package/template/src/common/helpers/defaultKeyIdExtractor.ts +1 -1
  61. package/template/src/common/helpers/regexHelpers.ts +1 -2
  62. package/template/src/common/helpers/stringsHelpers.ts +0 -1
  63. package/template/src/common/hooks/useBackHandler.ts +5 -2
  64. package/template/src/common/hooks/useEventRegister.ts +1 -1
  65. package/template/src/common/hooks/useFlatListActions.ts +1 -1
  66. package/template/src/common/hooks/useWhyDidYouUpdate.ts +1 -1
  67. package/template/src/common/localization/LocalizationProvider.tsx +1 -1
  68. package/template/src/common/localization/RTLInitializer.tsx +1 -1
  69. package/template/src/common/localization/dateFormatter.ts +0 -1
  70. package/template/src/common/localization/intlFormatter.ts +0 -1
  71. package/template/src/common/localization/localization.ts +2 -2
  72. package/template/src/common/localization/translations/homeLocalization.ts +14 -0
  73. package/template/src/common/localization/translations/loginLocalization.ts +8 -0
  74. package/template/src/common/localization/translations/mainNavigationLocalization.ts +2 -0
  75. package/template/src/common/localization/translations/profileLocalization.ts +16 -0
  76. package/template/src/common/utils/index.tsx +0 -6
  77. package/template/src/common/validations/commonValidations.ts +2 -2
  78. package/template/src/core/api/errorHandler.ts +1 -1
  79. package/template/src/core/api/responseHandlers.ts +1 -3
  80. package/template/src/core/api/serverHeaders.ts +61 -12
  81. package/template/src/core/notifications/notificationAuth.ts +6 -0
  82. package/template/src/core/notifications/notificationService.ts +125 -0
  83. package/template/src/core/notifications/routeFromNotificationData.ts +32 -0
  84. package/template/src/core/store/categories/categoriesActions.ts +25 -0
  85. package/template/src/core/store/categories/categoriesSlice.ts +51 -0
  86. package/template/src/core/store/categories/categoriesState.ts +19 -0
  87. package/template/src/core/store/rootReducer.ts +2 -0
  88. package/template/src/core/store/store.tsx +6 -1
  89. package/template/src/core/store/user/userActions.ts +75 -14
  90. package/template/src/core/store/user/userSlice.ts +49 -26
  91. package/template/src/core/store/user/userState.ts +6 -4
  92. package/template/src/core/theme/ThemeProvider.tsx +5 -3
  93. package/template/src/core/theme/brand.ts +50 -0
  94. package/template/src/core/theme/colors.ts +113 -99
  95. package/template/src/core/theme/commonConsts.ts +2 -2
  96. package/template/src/core/theme/commonStyles.ts +1 -1
  97. package/template/src/core/theme/themes.ts +2 -0
  98. package/template/src/core/theme/types.ts +4 -2
  99. package/template/src/core/utils/stringUtils.ts +1 -1
  100. package/template/src/design-system/index.ts +2 -0
  101. package/template/src/design-system/tokens/brand.ts +6 -0
  102. package/template/src/design-system/tokens/index.ts +3 -0
  103. package/template/src/design-system/tokens/palette.ts +4 -0
  104. package/template/src/design-system/tokens/typography-spacing.ts +2 -0
  105. package/template/src/navigation/AuthStack.tsx +1 -4
  106. package/template/src/navigation/HeaderComponents.tsx +6 -3
  107. package/template/src/navigation/MainStack.tsx +18 -6
  108. package/template/src/navigation/RootNavigation.tsx +4 -7
  109. package/template/src/navigation/TabBar.tsx +7 -6
  110. package/template/src/navigation/types.ts +10 -31
  111. package/template/src/screens/Login/Login.tsx +47 -47
  112. package/template/src/screens/OTP/OTPScreen.tsx +6 -9
  113. package/template/src/screens/components/ComponentsScreen.tsx +301 -0
  114. package/template/src/screens/home/HomeScreen.tsx +143 -1
  115. package/template/src/screens/home/hooks/useHomeData.ts +19 -5
  116. package/template/src/screens/index.tsx +1 -0
  117. package/template/src/screens/profile/Profile.tsx +139 -2
  118. package/template/src/screens/splash/Splash.tsx +44 -11
  119. package/template/src/sheetManager/sheets.tsx +1 -1
  120. package/template/tsconfig.json +14 -2
  121. package/template/types/globals.d.ts +43 -0
  122. package/template/types/index.ts +2 -6
  123. package/template/types/modules.d.ts +9 -0
  124. package/template/types/react-native-config.d.ts +0 -2
  125. package/.vscode/settings.json +0 -8
  126. package/CHANGELOG.md +0 -119
  127. package/CODE_OF_CONDUCT.md +0 -83
  128. package/CONTRIBUTING.md +0 -60
  129. package/local.properties +0 -1
  130. package/template/src/common/components/ImageCropPickerButton.tsx +0 -107
  131. package/template/src/common/components/PhotoTakingButton.tsx +0 -94
  132. package/template/src/common/helpers/imageHelpers.ts +0 -5
  133. package/template/src/common/helpers/inAppReviewHelper.ts +0 -30
  134. package/template/src/common/helpers/orientationHelpers.ts +0 -25
  135. package/template/src/common/helpers/shareHelpers.ts +0 -47
  136. package/template/src/common/utils/FeesCaalculation.tsx +0 -37
  137. package/template/src/common/utils/printData.tsx +0 -161
  138. package/template/src/common/validations/examples/TextInputWithValidation.tsx +0 -229
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import {StyleSheet, View} from 'react-native';
3
+ import {CommonSizes} from '../../core/theme/commonSizes';
4
+ import {useTheme} from '../../core/theme/ThemeProvider';
5
+ import {RTLAwareText} from './RTLAwareText';
6
+
7
+ type BadgeVariant = 'primary' | 'success' | 'error' | 'neutral';
8
+
9
+ interface BadgeProps {
10
+ label?: string;
11
+ count?: number;
12
+ variant?: BadgeVariant;
13
+ }
14
+
15
+ /**
16
+ * A small pill for counts or short labels. Colored variants (primary / success
17
+ * / error) use white text; the neutral variant uses a subtle surface with the
18
+ * default text color. A `count` wins over `label` and is capped at "99+".
19
+ */
20
+ export function Badge({
21
+ label,
22
+ count,
23
+ variant = 'primary',
24
+ }: BadgeProps): JSX.Element {
25
+ const {theme} = useTheme();
26
+
27
+ const backgroundColor =
28
+ variant === 'success'
29
+ ? theme.colors.success_400
30
+ : variant === 'error'
31
+ ? theme.colors.error_400
32
+ : variant === 'neutral'
33
+ ? theme.colors.grayScale_50
34
+ : theme.colors.PlatinateBlue_400;
35
+
36
+ const isNeutral = variant === 'neutral';
37
+
38
+ const content =
39
+ count != null ? (count > 99 ? '99+' : String(count)) : label;
40
+
41
+ return (
42
+ <View style={[styles.badge, {backgroundColor}]}>
43
+ <RTLAwareText
44
+ style={
45
+ isNeutral
46
+ ? theme.text.bodySmallBold
47
+ : [theme.text.bodySmallBold, styles.labelOnColor]
48
+ }>
49
+ {content}
50
+ </RTLAwareText>
51
+ </View>
52
+ );
53
+ }
54
+
55
+ const styles = StyleSheet.create({
56
+ badge: {
57
+ alignSelf: 'flex-start',
58
+ alignItems: 'center',
59
+ justifyContent: 'center',
60
+ borderRadius: CommonSizes.borderRadius.full,
61
+ paddingHorizontal: CommonSizes.spacing.small,
62
+ },
63
+ labelOnColor: {
64
+ color: '#FFFFFF',
65
+ },
66
+ });
@@ -0,0 +1,58 @@
1
+ import React, {useCallback} from 'react';
2
+ import {FlatList, StyleSheet, View} from 'react-native';
3
+ import {CommonSizes} from '../../core/theme/commonSizes';
4
+
5
+ interface CardScrollerProps<T> {
6
+ data: T[];
7
+ renderItem: (info: {item: T; index: number}) => React.ReactElement;
8
+ cardWidth?: number;
9
+ }
10
+
11
+ const DEFAULT_CARD_WIDTH = 260;
12
+ const CARD_GAP = CommonSizes.spacing.medium;
13
+
14
+ /**
15
+ * A horizontal, snap-to-card scroller. Cards are a fixed width with a uniform
16
+ * gap between them and inset content padding. Fast deceleration + snapping
17
+ * give a premium "shelf of cards" feel.
18
+ */
19
+ export function CardScroller<T>({
20
+ data,
21
+ renderItem,
22
+ cardWidth = DEFAULT_CARD_WIDTH,
23
+ }: CardScrollerProps<T>): JSX.Element {
24
+ const renderCard = useCallback(
25
+ ({item, index}: {item: T; index: number}) => (
26
+ <View style={{width: cardWidth}}>{renderItem({item, index})}</View>
27
+ ),
28
+ [renderItem, cardWidth],
29
+ );
30
+
31
+ return (
32
+ <FlatList
33
+ data={data}
34
+ renderItem={renderCard}
35
+ keyExtractor={(_, index) => `card-${index}`}
36
+ horizontal
37
+ showsHorizontalScrollIndicator={false}
38
+ snapToInterval={cardWidth + CARD_GAP}
39
+ snapToAlignment="start"
40
+ decelerationRate="fast"
41
+ ItemSeparatorComponent={ItemSeparator}
42
+ contentContainerStyle={styles.content}
43
+ />
44
+ );
45
+ }
46
+
47
+ function ItemSeparator(): JSX.Element {
48
+ return <View style={styles.separator} />;
49
+ }
50
+
51
+ const styles = StyleSheet.create({
52
+ content: {
53
+ paddingHorizontal: CommonSizes.spacing.large,
54
+ },
55
+ separator: {
56
+ width: CARD_GAP,
57
+ },
58
+ });
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
2
  import {
3
- Image,
4
3
  StyleSheet,
5
4
  Text,
6
5
  TouchableOpacity,
@@ -36,7 +35,7 @@ export const Card = ({
36
35
  {backgroundColor: theme.colors.surface},
37
36
  createThemedStyles(theme).dropShadow,
38
37
  {marginRight: marginRight},
39
- {overflow: 'hidden'},
38
+ styles.cardOverflowHidden,
40
39
  cardStyle,
41
40
  ]}
42
41
  onPress={() => {
@@ -46,15 +45,12 @@ export const Card = ({
46
45
  {icon && icon.uri && (
47
46
  <FastImage
48
47
  source={{uri: icon.uri, cache: FastImage.cacheControl.immutable}}
49
- style={{
50
- width: '100%',
51
- height: '100%',
52
- }}
48
+ style={styles.fastImage}
53
49
  resizeMode={FastImage.resizeMode.contain}
54
50
  />
55
51
  )}
56
52
  </View>
57
- <Text style={[theme.text.cards, {textAlign: 'center'}]}>{title}</Text>
53
+ <Text style={[theme.text.cards, styles.titleCentered]}>{title}</Text>
58
54
  </TouchableOpacity>
59
55
  );
60
56
  };
@@ -69,6 +65,16 @@ const styles = StyleSheet.create({
69
65
  width: scaleWidth(187),
70
66
  height: scaleHeight(280),
71
67
  },
68
+ cardOverflowHidden: {
69
+ overflow: 'hidden',
70
+ },
71
+ fastImage: {
72
+ width: '100%',
73
+ height: '100%',
74
+ },
75
+ titleCentered: {
76
+ textAlign: 'center',
77
+ },
72
78
  blurContainer: {
73
79
  flex: 1,
74
80
  padding: CommonSizes.spacing.medium,
@@ -0,0 +1,196 @@
1
+ import React, {useCallback, useRef, useState} from 'react';
2
+ import {
3
+ Dimensions,
4
+ FlatList,
5
+ NativeScrollEvent,
6
+ NativeSyntheticEvent,
7
+ Pressable,
8
+ StyleSheet,
9
+ View,
10
+ } from 'react-native';
11
+ import Svg, {Path} from 'react-native-svg';
12
+ import {CommonSizes} from '../../core/theme/commonSizes';
13
+ import {useTheme} from '../../core/theme/ThemeProvider';
14
+
15
+ interface CarouselProps<T> {
16
+ data: T[];
17
+ renderItem: (info: {item: T; index: number}) => React.ReactElement;
18
+ height?: number;
19
+ }
20
+
21
+ const ARROW = 32;
22
+ const GAP = CommonSizes.spacing.medium;
23
+ const CONTENT_WIDTH =
24
+ Dimensions.get('window').width - 2 * CommonSizes.spacing.large;
25
+ // Card width = content minus the two arrow gutters, so arrows sit BESIDE the
26
+ // card (in the side gutters), never overlapping it.
27
+ const ITEM_WIDTH = CONTENT_WIDTH - 2 * (ARROW + GAP);
28
+
29
+ function Chevron({dir, color}: {dir: 'left' | 'right'; color: string}) {
30
+ const d = dir === 'left' ? 'M15 5 L8 12 L15 19' : 'M9 5 L16 12 L9 19';
31
+ return (
32
+ <Svg width={18} height={22} viewBox="0 0 24 24" fill="none">
33
+ <Path
34
+ d={d}
35
+ stroke={color}
36
+ strokeWidth={2.5}
37
+ strokeLinecap="round"
38
+ strokeLinejoin="round"
39
+ />
40
+ </Svg>
41
+ );
42
+ }
43
+
44
+ /**
45
+ * Horizontal paging carousel with the card flanked by prev/next arrow buttons
46
+ * in the side gutters (never over the card), plus pagination dots below.
47
+ * Active dot is wide + primary blue; the rest are small + muted.
48
+ */
49
+ export function Carousel<T>({
50
+ data,
51
+ renderItem,
52
+ height,
53
+ }: CarouselProps<T>): JSX.Element {
54
+ const {theme} = useTheme();
55
+ const listRef = useRef<FlatList<T>>(null);
56
+ const [activeIndex, setActiveIndex] = useState(0);
57
+ const lastIndex = data.length - 1;
58
+
59
+ const goTo = useCallback(
60
+ (index: number) => {
61
+ const next = Math.max(0, Math.min(index, data.length - 1));
62
+ listRef.current?.scrollToOffset({
63
+ offset: next * ITEM_WIDTH,
64
+ animated: true,
65
+ });
66
+ setActiveIndex(next);
67
+ },
68
+ [data.length],
69
+ );
70
+
71
+ const onMomentumScrollEnd = useCallback(
72
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
73
+ const offsetX = event.nativeEvent.contentOffset.x;
74
+ setActiveIndex(Math.round(offsetX / ITEM_WIDTH));
75
+ },
76
+ [],
77
+ );
78
+
79
+ const renderCarouselItem = useCallback(
80
+ ({item, index}: {item: T; index: number}) => (
81
+ <View style={[styles.itemContainer, height != null ? {height} : null]}>
82
+ {renderItem({item, index})}
83
+ </View>
84
+ ),
85
+ [renderItem, height],
86
+ );
87
+
88
+ const prevDisabled = activeIndex <= 0;
89
+ const nextDisabled = activeIndex >= lastIndex;
90
+ const arrowColor = theme.colors.PlatinateBlue_400;
91
+ const disabledColor = theme.colors.grayScale_100;
92
+
93
+ const prevArrowStyle = {
94
+ backgroundColor: theme.colors.grayScale_0,
95
+ borderColor: theme.colors.grayScale_50,
96
+ opacity: prevDisabled ? 0.4 : 1,
97
+ };
98
+ const nextArrowStyle = {
99
+ backgroundColor: theme.colors.grayScale_0,
100
+ borderColor: theme.colors.grayScale_50,
101
+ opacity: nextDisabled ? 0.4 : 1,
102
+ };
103
+
104
+ return (
105
+ <View>
106
+ <View style={styles.row}>
107
+ <Pressable
108
+ onPress={() => goTo(activeIndex - 1)}
109
+ disabled={prevDisabled}
110
+ hitSlop={8}
111
+ accessibilityRole="button"
112
+ accessibilityLabel="Previous"
113
+ style={[styles.arrow, prevArrowStyle]}>
114
+ <Chevron dir="left" color={prevDisabled ? disabledColor : arrowColor} />
115
+ </Pressable>
116
+
117
+ <FlatList
118
+ ref={listRef}
119
+ data={data}
120
+ renderItem={renderCarouselItem}
121
+ keyExtractor={(_, index) => `carousel-${index}`}
122
+ horizontal
123
+ pagingEnabled
124
+ showsHorizontalScrollIndicator={false}
125
+ snapToInterval={ITEM_WIDTH}
126
+ snapToAlignment="start"
127
+ decelerationRate="fast"
128
+ disableIntervalMomentum
129
+ onMomentumScrollEnd={onMomentumScrollEnd}
130
+ style={styles.list}
131
+ />
132
+
133
+ <Pressable
134
+ onPress={() => goTo(activeIndex + 1)}
135
+ disabled={nextDisabled}
136
+ hitSlop={8}
137
+ accessibilityRole="button"
138
+ accessibilityLabel="Next"
139
+ style={[styles.arrow, nextArrowStyle]}>
140
+ <Chevron
141
+ dir="right"
142
+ color={nextDisabled ? disabledColor : arrowColor}
143
+ />
144
+ </Pressable>
145
+ </View>
146
+
147
+ <View style={styles.dotsRow}>
148
+ {data.map((_, index) => {
149
+ const isActive = index === activeIndex;
150
+ return (
151
+ <View
152
+ key={`dot-${index}`}
153
+ style={[
154
+ styles.dot,
155
+ isActive ? styles.dotActive : styles.dotInactive,
156
+ {
157
+ backgroundColor: isActive
158
+ ? theme.colors.PlatinateBlue_400
159
+ : theme.colors.grayScale_50,
160
+ },
161
+ ]}
162
+ />
163
+ );
164
+ })}
165
+ </View>
166
+ </View>
167
+ );
168
+ }
169
+
170
+ const styles = StyleSheet.create({
171
+ row: {flexDirection: 'row', alignItems: 'center'},
172
+ list: {width: ITEM_WIDTH, flexGrow: 0},
173
+ itemContainer: {width: ITEM_WIDTH},
174
+ arrow: {
175
+ width: ARROW,
176
+ height: ARROW,
177
+ borderRadius: CommonSizes.borderRadius.full,
178
+ borderWidth: CommonSizes.borderWidth.small,
179
+ alignItems: 'center',
180
+ justifyContent: 'center',
181
+ marginHorizontal: GAP / 2,
182
+ },
183
+ dotsRow: {
184
+ flexDirection: 'row',
185
+ alignItems: 'center',
186
+ justifyContent: 'center',
187
+ marginTop: CommonSizes.spacing.large,
188
+ },
189
+ dot: {
190
+ height: 6,
191
+ borderRadius: CommonSizes.borderRadius.full,
192
+ marginHorizontal: CommonSizes.spacing.small,
193
+ },
194
+ dotActive: {width: 18},
195
+ dotInactive: {width: 6},
196
+ });
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import {Pressable, StyleSheet, View, ViewStyle} from 'react-native';
3
+ import Svg, {Path} from 'react-native-svg';
4
+ import {useTheme} from '../../core/theme/ThemeProvider';
5
+ import {CommonSizes} from '../../core/theme/commonSizes';
6
+ import {RTLAwareText} from './RTLAwareText';
7
+ import {RTLAwareView} from './RTLAwareView';
8
+
9
+ interface CheckboxProps {
10
+ checked: boolean;
11
+ onChange: (next: boolean) => void;
12
+ label?: string;
13
+ disabled?: boolean;
14
+ }
15
+
16
+ function CheckMark({color}: {color: string}): JSX.Element {
17
+ return (
18
+ <Svg width={14} height={14} viewBox="0 0 24 24" fill="none">
19
+ <Path
20
+ d="M5 12.5L10 17.5L19 7"
21
+ stroke={color}
22
+ strokeWidth={2.6}
23
+ strokeLinecap="round"
24
+ strokeLinejoin="round"
25
+ />
26
+ </Svg>
27
+ );
28
+ }
29
+
30
+ export function Checkbox(props: CheckboxProps): JSX.Element {
31
+ const {checked, onChange, label, disabled} = props;
32
+ const {theme} = useTheme();
33
+
34
+ const boxStyle: ViewStyle = {
35
+ width: 22,
36
+ height: 22,
37
+ borderRadius: CommonSizes.borderRadius.small,
38
+ alignItems: 'center',
39
+ justifyContent: 'center',
40
+ backgroundColor: checked ? theme.colors.PlatinateBlue_400 : 'transparent',
41
+ borderWidth: CommonSizes.borderWidth.medium,
42
+ borderColor: checked
43
+ ? theme.colors.PlatinateBlue_400
44
+ : theme.colors.grayScale_50,
45
+ };
46
+
47
+ const boxOpacity = {opacity: disabled ? 0.5 : 1};
48
+
49
+ return (
50
+ <Pressable
51
+ onPress={() => !disabled && onChange(!checked)}
52
+ disabled={disabled}
53
+ hitSlop={8}
54
+ accessibilityRole="checkbox"
55
+ accessibilityState={{checked, disabled}}
56
+ style={boxOpacity}>
57
+ <RTLAwareView style={styles.row}>
58
+ <View style={boxStyle}>
59
+ {checked ? <CheckMark color={'#FFFFFF'} /> : null}
60
+ </View>
61
+ {label ? (
62
+ <RTLAwareText
63
+ style={[
64
+ theme.text.bodyLargeRegular,
65
+ styles.label,
66
+ {color: theme.colors.grayScale_700},
67
+ ]}>
68
+ {label}
69
+ </RTLAwareText>
70
+ ) : null}
71
+ </RTLAwareView>
72
+ </Pressable>
73
+ );
74
+ }
75
+
76
+ const styles = StyleSheet.create({
77
+ row: {
78
+ flexDirection: 'row',
79
+ alignItems: 'center',
80
+ } as ViewStyle,
81
+ label: {
82
+ marginStart: CommonSizes.spacing.large,
83
+ flexShrink: 1,
84
+ },
85
+ });
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import {Pressable, StyleSheet} from 'react-native';
3
+ import {CommonSizes} from '../../core/theme/commonSizes';
4
+ import {useTheme} from '../../core/theme/ThemeProvider';
5
+ import {RTLAwareText} from './RTLAwareText';
6
+
7
+ interface ChipProps {
8
+ label: string;
9
+ selected?: boolean;
10
+ onPress?: () => void;
11
+ }
12
+
13
+ /**
14
+ * A pill-shaped, tappable chip. Selected → solid primary blue with white
15
+ * label; unselected → white surface with a subtle border and default label.
16
+ */
17
+ export function Chip({label, selected, onPress}: ChipProps): JSX.Element {
18
+ const {theme} = useTheme();
19
+
20
+ return (
21
+ <Pressable
22
+ onPress={onPress}
23
+ style={[
24
+ styles.chip,
25
+ selected
26
+ ? {backgroundColor: theme.colors.PlatinateBlue_400}
27
+ : {
28
+ backgroundColor: theme.colors.grayScale_0,
29
+ borderColor: theme.colors.grayScale_50,
30
+ borderWidth: CommonSizes.borderWidth.small,
31
+ },
32
+ ]}>
33
+ <RTLAwareText
34
+ style={
35
+ selected
36
+ ? [theme.text.bodyMediumBold, styles.labelSelected]
37
+ : theme.text.bodyMediumBold
38
+ }>
39
+ {label}
40
+ </RTLAwareText>
41
+ </Pressable>
42
+ );
43
+ }
44
+
45
+ const styles = StyleSheet.create({
46
+ chip: {
47
+ alignSelf: 'flex-start',
48
+ borderRadius: CommonSizes.borderRadius.full,
49
+ paddingHorizontal: CommonSizes.spacing.large,
50
+ paddingVertical: CommonSizes.spacing.small,
51
+ },
52
+ labelSelected: {
53
+ color: '#FFFFFF',
54
+ },
55
+ });