@compsych/mobile-ui 1.0.8 → 1.0.10

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 (77) hide show
  1. package/package.json +40 -2
  2. package/src/components/ActionSheet/ActionSheet.test.tsx +89 -0
  3. package/src/{ActionSheet.tsx → components/ActionSheet/index.tsx} +16 -8
  4. package/src/components/Alert/Alert.test.tsx +61 -0
  5. package/src/{Alert.tsx → components/Alert/index.tsx} +14 -11
  6. package/src/components/Avatar/Avatar.test.tsx +52 -0
  7. package/src/{Avatar.tsx → components/Avatar/index.tsx} +15 -13
  8. package/src/components/Badge/Badge.test.tsx +30 -0
  9. package/src/{Badge.tsx → components/Badge/index.tsx} +17 -9
  10. package/src/components/BodyText/BodyText.test.tsx +30 -0
  11. package/src/{BodyText.tsx → components/BodyText/index.tsx} +9 -5
  12. package/src/components/Breadcrumb/Breadcrumb.test.tsx +37 -0
  13. package/src/{Breadcrumb.tsx → components/Breadcrumb/index.tsx} +13 -4
  14. package/src/components/Button/Button.test.tsx +45 -0
  15. package/src/{Button.tsx → components/Button/index.tsx} +36 -17
  16. package/src/components/Checkbox/Checkbox.test.tsx +57 -0
  17. package/src/{Checkbox.tsx → components/Checkbox/index.tsx} +37 -15
  18. package/src/components/Chip/Chip.test.tsx +40 -0
  19. package/src/{Chip.tsx → components/Chip/index.tsx} +18 -9
  20. package/src/components/Divider/Divider.test.tsx +28 -0
  21. package/src/{Divider.tsx → components/Divider/index.tsx} +30 -20
  22. package/src/components/EmptyState/EmptyState.test.tsx +60 -0
  23. package/src/{EmptyState.tsx → components/EmptyState/index.tsx} +17 -7
  24. package/src/components/HeaderText/HeaderText.test.tsx +43 -0
  25. package/src/{HeaderText.tsx → components/HeaderText/index.tsx} +9 -5
  26. package/src/components/Input/Input.test.tsx +43 -0
  27. package/src/{Input.tsx → components/Input/index.tsx} +31 -11
  28. package/src/components/List/List.test.tsx +59 -0
  29. package/src/{List.tsx → components/List/index.tsx} +23 -7
  30. package/src/components/Pagination/Pagination.test.tsx +56 -0
  31. package/src/{Pagination.tsx → components/Pagination/index.tsx} +29 -13
  32. package/src/components/PlanCard/PlanCard.test.tsx +44 -0
  33. package/src/{PlanCard.tsx → components/PlanCard/index.tsx} +46 -17
  34. package/src/components/ProgressTracker/ProgressTracker.test.tsx +47 -0
  35. package/src/{ProgressTracker.tsx → components/ProgressTracker/index.tsx} +16 -25
  36. package/src/components/PromotionCard/PromotionCard.test.tsx +73 -0
  37. package/src/components/PromotionCard/index.tsx +538 -0
  38. package/src/components/RadioButton/RadioButton.test.tsx +50 -0
  39. package/src/{RadioButton.tsx → components/RadioButton/index.tsx} +21 -10
  40. package/src/components/ScreenContainer/ScreenContainer.test.tsx +29 -0
  41. package/src/components/ScreenContainer/index.tsx +41 -0
  42. package/src/components/SegmentedControl/SegmentedControl.test.tsx +60 -0
  43. package/src/{SegmentedControl.tsx → components/SegmentedControl/index.tsx} +15 -7
  44. package/src/components/SelectionCard/SelectionCard.test.tsx +48 -0
  45. package/src/components/SelectionCard/index.tsx +213 -0
  46. package/src/components/ServiceCard/ServiceCard.test.tsx +61 -0
  47. package/src/{Card.tsx → components/ServiceCard/index.tsx} +106 -49
  48. package/src/components/Slider/Slider.test.tsx +33 -0
  49. package/src/{Slider.tsx → components/Slider/index.tsx} +26 -36
  50. package/src/components/Snackbar/Snackbar.test.tsx +61 -0
  51. package/src/{Snackbar.tsx → components/Snackbar/index.tsx} +13 -7
  52. package/src/components/Switch/Switch.test.tsx +54 -0
  53. package/src/{Switch.tsx → components/Switch/index.tsx} +16 -5
  54. package/src/components/Tooltip/Tooltip.test.tsx +30 -0
  55. package/src/{Tooltip.tsx → components/Tooltip/index.tsx} +11 -7
  56. package/src/icons/AtomIcon.tsx +32 -0
  57. package/src/icons/BinocularsIcon.tsx +45 -0
  58. package/src/icons/FileChartColumnIncreasingIcon.tsx +44 -0
  59. package/src/icons/FlagIcon.tsx +20 -0
  60. package/src/icons/GlobeIcon.tsx +20 -0
  61. package/src/icons/GraduationCapIcon.tsx +32 -0
  62. package/src/icons/HandHeartIcon.tsx +38 -0
  63. package/src/icons/HandshakeIcon.tsx +39 -0
  64. package/src/icons/HazeIcon.tsx +42 -0
  65. package/src/icons/HeartHandshakeIcon.tsx +20 -0
  66. package/src/icons/HourglassIcon.tsx +33 -0
  67. package/src/icons/IdCardIcon.tsx +44 -0
  68. package/src/icons/MessageCirclePlusIcon.tsx +32 -0
  69. package/src/icons/MountainSnowIcon.tsx +26 -0
  70. package/src/icons/SnowflakeIcon.tsx +86 -0
  71. package/src/icons/StethoscopeIcon.tsx +34 -0
  72. package/src/icons/UserRoundIcon.tsx +26 -0
  73. package/src/icons/WheatIcon.tsx +62 -0
  74. package/src/icons/index.ts +63 -0
  75. package/src/icons/types.ts +45 -0
  76. package/src/index.ts +159 -71
  77. package/src/tokens.ts +135 -15
@@ -0,0 +1,538 @@
1
+ import React from 'react';
2
+
3
+ import {
4
+ Image,
5
+ type ImageSourcePropType,
6
+ Pressable,
7
+ StyleProp,
8
+ StyleSheet,
9
+ View,
10
+ ViewStyle,
11
+ } from 'react-native';
12
+
13
+ import { Ionicons } from '@expo/vector-icons';
14
+ import Svg, { Circle as SvgCircle } from 'react-native-svg';
15
+
16
+ import { sys } from '../../tokens';
17
+ import { BodyText } from '../BodyText';
18
+ import { HeaderText } from '../HeaderText';
19
+
20
+ export type PromotionCardVariant = 'filled' | 'tonal';
21
+ export type PromotionCardUsage =
22
+ | 'neutral'
23
+ | 'informative'
24
+ | 'positive'
25
+ | 'danger'
26
+ | 'warning'
27
+ | 'image';
28
+ export type PromotionCardSize = 'md' | 'lg';
29
+
30
+ export interface PromotionCardProps {
31
+ size?: PromotionCardSize;
32
+ variant?: PromotionCardVariant;
33
+ usage?: PromotionCardUsage;
34
+ title?: string;
35
+ description?: string;
36
+ chipLabel?: string;
37
+ chipIcon?: React.ReactNode;
38
+ image?: ImageSourcePropType;
39
+ progress?: number;
40
+ showProgressBar?: boolean;
41
+ showRingTimer?: boolean;
42
+ onPress?: () => void;
43
+ disabled?: boolean;
44
+ accessibilityLabel?: string;
45
+ style?: StyleProp<ViewStyle>;
46
+ }
47
+
48
+ const { colorRoles: cr, dimensions: dim } = sys;
49
+
50
+ type VariantTokens = {
51
+ cardBg: string;
52
+ chipBg: string;
53
+ chipTextColor: string;
54
+ titleColor: string;
55
+ descColor: string;
56
+ descOpacity: number;
57
+ buttonBg: string;
58
+ buttonIconColor: string;
59
+ progressColor: string;
60
+ };
61
+
62
+ const TOKENS: Record<
63
+ PromotionCardVariant,
64
+ Record<PromotionCardUsage, VariantTokens>
65
+ > = {
66
+ filled: {
67
+ neutral: {
68
+ cardBg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
69
+ chipBg: cr.surface.surfaceContainer.sysSurfaceContainerHighest,
70
+ chipTextColor: cr.surface.surface.sysOnSurface,
71
+ titleColor: '#0f0f0f',
72
+ descColor: cr.surface.surface.sysOnSurfaceVariant,
73
+ descOpacity: 1,
74
+ buttonBg: cr.addOn.primaryFixed.sysPrimaryFixedDim,
75
+ buttonIconColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
76
+ progressColor: cr.accent.primary.sysPrimary,
77
+ },
78
+ informative: {
79
+ cardBg: cr.custom.info.sysOnInfoContainer,
80
+ chipBg: cr.custom.info.sysInfoContainer,
81
+ chipTextColor: cr.custom.info.sysOnInfoContainer,
82
+ titleColor: cr.custom.info.sysOnInfo,
83
+ descColor: cr.transparent.neutral.sysWhite80,
84
+ descOpacity: 1,
85
+ buttonBg: cr.custom.info.sysOnInfo,
86
+ buttonIconColor: cr.custom.info.sysOnInfoContainer,
87
+ progressColor: cr.custom.info.sysOnInfo,
88
+ },
89
+ positive: {
90
+ cardBg: cr.custom.success.sysOnSuccessContainer,
91
+ chipBg: cr.custom.success.sysSuccessContainer,
92
+ chipTextColor: cr.custom.success.sysOnSuccessContainer,
93
+ titleColor: cr.custom.success.sysOnSuccess,
94
+ descColor: cr.transparent.neutral.sysWhite80,
95
+ descOpacity: 1,
96
+ buttonBg: cr.custom.success.sysOnSuccess,
97
+ buttonIconColor: cr.custom.success.sysOnSuccessContainer,
98
+ progressColor: cr.custom.success.sysOnSuccess,
99
+ },
100
+ danger: {
101
+ cardBg: cr.error.sysOnErrorContainer,
102
+ chipBg: cr.error.sysErrorContainer,
103
+ chipTextColor: cr.error.sysOnErrorContainer,
104
+ titleColor: cr.error.sysOnError,
105
+ descColor: cr.transparent.neutral.sysWhite80,
106
+ descOpacity: 1,
107
+ buttonBg: cr.error.sysOnError,
108
+ buttonIconColor: cr.error.sysOnErrorContainer,
109
+ progressColor: cr.error.sysOnError,
110
+ },
111
+ warning: {
112
+ cardBg: cr.custom.warning.sysOnWarningContainer,
113
+ chipBg: cr.custom.warning.sysWarningContainer,
114
+ chipTextColor: cr.custom.warning.sysOnWarningContainer,
115
+ titleColor: cr.custom.warning.sysOnWarning,
116
+ descColor: cr.transparent.neutral.sysWhite80,
117
+ descOpacity: 1,
118
+ buttonBg: cr.custom.warning.sysOnWarning,
119
+ buttonIconColor: cr.custom.warning.sysOnWarningContainer,
120
+ progressColor: cr.custom.warning.sysOnWarning,
121
+ },
122
+ image: {
123
+ cardBg: 'transparent',
124
+ chipBg: 'rgba(255,255,255,0.2)',
125
+ chipTextColor: cr.surface.inverse.sysInverseOnSurface,
126
+ titleColor: cr.accent.primary.sysOnPrimary,
127
+ descColor: cr.transparent.neutral.sysWhite80,
128
+ descOpacity: 1,
129
+ buttonBg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
130
+ buttonIconColor: cr.surface.surface.sysOnSurface,
131
+ progressColor: cr.accent.primary.sysOnPrimary,
132
+ },
133
+ },
134
+ tonal: {
135
+ neutral: {
136
+ cardBg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
137
+ chipBg: cr.surface.surfaceContainer.sysSurfaceContainerHighest,
138
+ chipTextColor: cr.surface.surface.sysOnSurface,
139
+ titleColor: '#0f0f0f',
140
+ descColor: cr.surface.surface.sysOnSurfaceVariant,
141
+ descOpacity: 1,
142
+ buttonBg: cr.addOn.primaryFixed.sysPrimaryFixedDim,
143
+ buttonIconColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
144
+ progressColor: cr.accent.primary.sysPrimary,
145
+ },
146
+ informative: {
147
+ cardBg: cr.custom.info.sysInfoContainer,
148
+ chipBg: cr.custom.info.sysOnInfo,
149
+ chipTextColor: cr.custom.info.sysOnInfoContainer,
150
+ titleColor: cr.custom.info.sysOnInfoContainer,
151
+ descColor: cr.custom.info.sysOnInfoContainer,
152
+ descOpacity: 0.64,
153
+ buttonBg: cr.custom.info.sysInfo,
154
+ buttonIconColor: cr.custom.info.sysOnInfo,
155
+ progressColor: cr.custom.info.sysInfo,
156
+ },
157
+ positive: {
158
+ cardBg: cr.custom.success.sysSuccessContainer,
159
+ chipBg: cr.custom.success.sysOnSuccess,
160
+ chipTextColor: cr.custom.success.sysOnSuccessContainer,
161
+ titleColor: cr.custom.success.sysOnSuccessContainer,
162
+ descColor: cr.custom.success.sysOnSuccessContainer,
163
+ descOpacity: 0.64,
164
+ buttonBg: cr.custom.success.sysSuccess,
165
+ buttonIconColor: cr.custom.success.sysOnSuccess,
166
+ progressColor: cr.custom.success.sysSuccess,
167
+ },
168
+ danger: {
169
+ cardBg: cr.error.sysErrorContainer,
170
+ chipBg: cr.error.sysOnError,
171
+ chipTextColor: cr.error.sysOnErrorContainer,
172
+ titleColor: cr.error.sysOnErrorContainer,
173
+ descColor: cr.error.sysOnErrorContainer,
174
+ descOpacity: 0.64,
175
+ buttonBg: cr.error.sysError,
176
+ buttonIconColor: cr.error.sysOnError,
177
+ progressColor: cr.error.sysError,
178
+ },
179
+ warning: {
180
+ cardBg: cr.custom.warning.sysWarningContainer,
181
+ chipBg: cr.custom.warning.sysOnWarning,
182
+ chipTextColor: cr.custom.warning.sysOnWarningContainer,
183
+ titleColor: cr.custom.warning.sysOnWarningContainer,
184
+ descColor: cr.custom.warning.sysOnWarningContainer,
185
+ descOpacity: 0.64,
186
+ buttonBg: cr.custom.warning.sysWarning,
187
+ buttonIconColor: cr.custom.warning.sysOnWarning,
188
+ progressColor: cr.custom.warning.sysWarning,
189
+ },
190
+ image: {
191
+ cardBg: 'transparent',
192
+ chipBg: 'rgba(255,255,255,0.2)',
193
+ chipTextColor: cr.surface.inverse.sysInverseOnSurface,
194
+ titleColor: cr.accent.primary.sysOnPrimary,
195
+ descColor: cr.transparent.neutral.sysWhite80,
196
+ descOpacity: 1,
197
+ buttonBg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
198
+ buttonIconColor: cr.surface.surface.sysOnSurface,
199
+ progressColor: cr.accent.primary.sysOnPrimary,
200
+ },
201
+ },
202
+ };
203
+
204
+ // lg tokens: neutral uses filled, others use tonal
205
+ function getLgTokens(usage: PromotionCardUsage): VariantTokens {
206
+ if (usage === 'neutral') return TOKENS.filled.neutral;
207
+ return TOKENS.tonal[usage];
208
+ }
209
+
210
+ const MD_HEIGHT = 197;
211
+ const LG_HEIGHT = 363;
212
+ const MD_BUTTON = 32;
213
+ const LG_BUTTON = 48;
214
+ const MD_RING_R = 14;
215
+ const LG_RING_R = 21;
216
+ const MD_RING_C = 2 * Math.PI * MD_RING_R;
217
+ const LG_RING_C = 2 * Math.PI * LG_RING_R;
218
+ const LG_DOTS_BOTTOM = 53;
219
+ const LG_DOTS_LEFT = 32;
220
+ const LG_DOT_SIZE = 6;
221
+ const LG_DOT_GAP = 4;
222
+ const LG_DOT_COUNT = 5;
223
+
224
+ export function PromotionCard({
225
+ size = 'md',
226
+ variant = 'filled',
227
+ usage = 'neutral',
228
+ title,
229
+ description,
230
+ chipLabel,
231
+ chipIcon,
232
+ image,
233
+ progress = 0.25,
234
+ showProgressBar = true,
235
+ showRingTimer = true,
236
+ onPress,
237
+ disabled = false,
238
+ accessibilityLabel,
239
+ style,
240
+ }: PromotionCardProps) {
241
+ const isLg = size === 'lg';
242
+ const t = isLg ? getLgTokens(usage) : TOKENS[variant][usage];
243
+ const isImage = usage === 'image';
244
+ const p = Math.max(0, Math.min(1, progress));
245
+
246
+ const buttonSize = isLg ? LG_BUTTON : MD_BUTTON;
247
+ const ringR = isLg ? LG_RING_R : MD_RING_R;
248
+ const ringC = isLg ? LG_RING_C : MD_RING_C;
249
+ const ringArcLength = ringC * p;
250
+
251
+ const containerStyle = [
252
+ styles.card,
253
+ isLg ? styles.cardLg : styles.cardMd,
254
+ { backgroundColor: t.cardBg, opacity: disabled ? 0.48 : 1 },
255
+ style,
256
+ ];
257
+
258
+ const inner = (
259
+ <>
260
+ {isImage && image && (
261
+ <>
262
+ <Image
263
+ source={image}
264
+ style={[
265
+ StyleSheet.absoluteFillObject,
266
+ {
267
+ borderRadius: isLg
268
+ ? dim.borderRadius.sysRadiusXl
269
+ : dim.borderRadius.sysRadiusLg,
270
+ },
271
+ ]}
272
+ resizeMode="cover"
273
+ accessible={false}
274
+ />
275
+ <View
276
+ style={[
277
+ StyleSheet.absoluteFillObject,
278
+ {
279
+ borderRadius: isLg
280
+ ? dim.borderRadius.sysRadiusXl
281
+ : dim.borderRadius.sysRadiusLg,
282
+ backgroundColor: 'rgba(0,0,0,0.35)',
283
+ },
284
+ ]}
285
+ pointerEvents="none"
286
+ />
287
+ </>
288
+ )}
289
+
290
+ {/* Chip */}
291
+ {chipLabel && (
292
+ <View
293
+ style={[
294
+ isLg ? styles.chipLg : styles.chipMd,
295
+ { backgroundColor: t.chipBg },
296
+ ]}
297
+ >
298
+ {chipIcon && (
299
+ <View style={isLg ? styles.chipIconWrapLg : styles.chipIconWrapMd}>
300
+ {chipIcon}
301
+ </View>
302
+ )}
303
+ <BodyText variant="labelMedium" color={t.chipTextColor}>
304
+ {chipLabel}
305
+ </BodyText>
306
+ </View>
307
+ )}
308
+
309
+ {/* Content (title + description) */}
310
+ <View style={isLg ? styles.contentLg : styles.contentMd}>
311
+ {title &&
312
+ (isLg ? (
313
+ <HeaderText variant="titleLarge" color={t.titleColor}>
314
+ {title}
315
+ </HeaderText>
316
+ ) : (
317
+ <HeaderText variant="titleSmall" color={t.titleColor}>
318
+ {title}
319
+ </HeaderText>
320
+ ))}
321
+ {description && (
322
+ <View style={{ opacity: t.descOpacity }}>
323
+ <BodyText variant={isLg ? 'medium' : 'small'} color={t.descColor}>
324
+ {description}
325
+ </BodyText>
326
+ </View>
327
+ )}
328
+ </View>
329
+
330
+ {/* Arrow button (absolute, bottom-right) */}
331
+ <View
332
+ style={[
333
+ styles.buttonWrap,
334
+ {
335
+ bottom: isLg
336
+ ? dim.spacing.padding.sysPadding32
337
+ : dim.spacing.padding.sysPadding16,
338
+ right: isLg
339
+ ? dim.spacing.padding.sysPadding32
340
+ : dim.spacing.padding.sysPadding16,
341
+ width: buttonSize,
342
+ height: buttonSize,
343
+ },
344
+ ]}
345
+ >
346
+ <View
347
+ style={[
348
+ styles.button,
349
+ {
350
+ width: buttonSize,
351
+ height: buttonSize,
352
+ backgroundColor: t.buttonBg,
353
+ },
354
+ ]}
355
+ >
356
+ <Ionicons
357
+ name="arrow-forward"
358
+ size={isLg ? 24 : 16}
359
+ color={t.buttonIconColor}
360
+ accessible={false}
361
+ />
362
+ </View>
363
+ {showRingTimer && (
364
+ <Svg
365
+ width={buttonSize}
366
+ height={buttonSize}
367
+ style={StyleSheet.absoluteFillObject}
368
+ >
369
+ <SvgCircle
370
+ cx={buttonSize / 2}
371
+ cy={buttonSize / 2}
372
+ r={ringR}
373
+ stroke={t.progressColor}
374
+ strokeWidth={2}
375
+ fill="transparent"
376
+ strokeDasharray={`${ringArcLength} ${ringC - ringArcLength}`}
377
+ strokeLinecap="round"
378
+ rotation={-90}
379
+ origin={`${buttonSize / 2}, ${buttonSize / 2}`}
380
+ />
381
+ </Svg>
382
+ )}
383
+ </View>
384
+
385
+ {/* Pagination dots (lg only) */}
386
+ {isLg && (
387
+ <View style={styles.dotsRow}>
388
+ {Array.from({ length: LG_DOT_COUNT }).map((_, i) => (
389
+ <View
390
+ key={i}
391
+ style={[
392
+ styles.dot,
393
+ {
394
+ backgroundColor: i === 0 ? t.progressColor : 'transparent',
395
+ borderColor: t.progressColor,
396
+ borderWidth: i === 0 ? 0 : 1.5,
397
+ opacity: i === 0 ? 1 : 0.4,
398
+ },
399
+ ]}
400
+ />
401
+ ))}
402
+ </View>
403
+ )}
404
+
405
+ {/* Progress bar */}
406
+ {showProgressBar && (
407
+ <View
408
+ style={[styles.progressTrack, { height: isLg ? 4 : 2 }]}
409
+ pointerEvents="none"
410
+ >
411
+ <View
412
+ style={[
413
+ styles.progressFill,
414
+ {
415
+ width: `${p * 100}%` as any,
416
+ height: isLg ? 4 : 2,
417
+ backgroundColor: t.progressColor,
418
+ },
419
+ ]}
420
+ />
421
+ </View>
422
+ )}
423
+ </>
424
+ );
425
+
426
+ if (onPress) {
427
+ return (
428
+ <Pressable
429
+ onPress={disabled ? undefined : onPress}
430
+ accessibilityRole="button"
431
+ accessibilityLabel={accessibilityLabel ?? title}
432
+ accessibilityState={{ disabled }}
433
+ style={({ pressed }: { pressed: boolean }) => [
434
+ ...containerStyle,
435
+ pressed && !disabled && styles.pressed,
436
+ ]}
437
+ >
438
+ {inner}
439
+ </Pressable>
440
+ );
441
+ }
442
+
443
+ return <View style={containerStyle}>{inner}</View>;
444
+ }
445
+
446
+ const styles = StyleSheet.create({
447
+ card: {
448
+ overflow: 'hidden',
449
+ alignSelf: 'stretch',
450
+ },
451
+ cardMd: {
452
+ height: MD_HEIGHT,
453
+ borderRadius: sys.dimensions.borderRadius.sysRadiusLg,
454
+ paddingHorizontal: sys.dimensions.spacing.padding.sysPadding16,
455
+ paddingVertical: sys.dimensions.spacing.padding.sysPadding16,
456
+ justifyContent: 'space-between',
457
+ },
458
+ cardLg: {
459
+ height: LG_HEIGHT,
460
+ borderRadius: sys.dimensions.borderRadius.sysRadiusXl,
461
+ padding: sys.dimensions.spacing.padding.sysPadding32,
462
+ flexDirection: 'column',
463
+ gap: sys.dimensions.spacing.padding.sysPadding24,
464
+ },
465
+ chipMd: {
466
+ flexDirection: 'row',
467
+ alignItems: 'center',
468
+ alignSelf: 'flex-start',
469
+ borderRadius: sys.dimensions.borderRadius.sysRadiusFull,
470
+ paddingHorizontal: sys.dimensions.spacing.padding.sysPadding12,
471
+ paddingVertical: sys.dimensions.spacing.padding.sysPadding4,
472
+ gap: sys.dimensions.spacing.padding.sysPadding4,
473
+ height: 24,
474
+ },
475
+ chipLg: {
476
+ flexDirection: 'row',
477
+ alignItems: 'center',
478
+ alignSelf: 'flex-start',
479
+ borderRadius: sys.dimensions.borderRadius.sysRadiusFull,
480
+ paddingHorizontal: sys.dimensions.spacing.padding.sysPadding16,
481
+ paddingVertical: sys.dimensions.spacing.padding.sysPadding4,
482
+ gap: sys.dimensions.spacing.padding.sysPadding8,
483
+ height: 32,
484
+ },
485
+ chipIconWrapMd: {
486
+ width: 16,
487
+ height: 16,
488
+ alignItems: 'center',
489
+ justifyContent: 'center',
490
+ },
491
+ chipIconWrapLg: {
492
+ width: 20,
493
+ height: 20,
494
+ alignItems: 'center',
495
+ justifyContent: 'center',
496
+ },
497
+ contentMd: {
498
+ gap: sys.dimensions.spacing.padding.sysPadding4,
499
+ maxWidth: '78%',
500
+ },
501
+ contentLg: {
502
+ gap: sys.dimensions.spacing.padding.sysPadding8,
503
+ flex: 1,
504
+ },
505
+ buttonWrap: {
506
+ position: 'absolute',
507
+ },
508
+ button: {
509
+ borderRadius: sys.dimensions.borderRadius.sysRadiusFull,
510
+ alignItems: 'center',
511
+ justifyContent: 'center',
512
+ overflow: 'hidden',
513
+ },
514
+ dotsRow: {
515
+ position: 'absolute',
516
+ bottom: LG_DOTS_BOTTOM,
517
+ left: LG_DOTS_LEFT,
518
+ flexDirection: 'row',
519
+ alignItems: 'center',
520
+ gap: LG_DOT_GAP,
521
+ },
522
+ dot: {
523
+ width: LG_DOT_SIZE,
524
+ height: LG_DOT_SIZE,
525
+ borderRadius: LG_DOT_SIZE / 2,
526
+ },
527
+ progressTrack: {
528
+ position: 'absolute',
529
+ bottom: 0,
530
+ left: 0,
531
+ right: 0,
532
+ backgroundColor: 'rgba(0,0,0,0.08)',
533
+ },
534
+ progressFill: {},
535
+ pressed: {
536
+ opacity: 0.84,
537
+ },
538
+ });
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+
3
+ import { fireEvent, render } from '@testing-library/react-native';
4
+
5
+ import { RadioButton } from './index';
6
+
7
+ describe('RadioButton', () => {
8
+ it('renders label', () => {
9
+ const { getByText } = render(<RadioButton label="Option A" />);
10
+ expect(getByText('Option A')).toBeTruthy();
11
+ });
12
+
13
+ it('renders description when provided', () => {
14
+ const { getByText } = render(
15
+ <RadioButton label="Option A" description="Best for most users" />,
16
+ );
17
+ expect(getByText('Best for most users')).toBeTruthy();
18
+ });
19
+
20
+ it('calls onChange when pressed', () => {
21
+ const onChange = jest.fn();
22
+ const { getByRole } = render(
23
+ <RadioButton label="Option A" checked={false} onChange={onChange} />,
24
+ );
25
+ fireEvent.press(getByRole('radio'));
26
+ expect(onChange).toHaveBeenCalledWith(true);
27
+ });
28
+
29
+ it('does not call onChange when disabled', () => {
30
+ const onChange = jest.fn();
31
+ const { getByRole } = render(
32
+ <RadioButton
33
+ label="Option A"
34
+ checked={false}
35
+ onChange={onChange}
36
+ disabled
37
+ />,
38
+ );
39
+ fireEvent.press(getByRole('radio'));
40
+ expect(onChange).not.toHaveBeenCalled();
41
+ });
42
+
43
+ it.each(['sm', 'md'] as const)(
44
+ 'renders size "%s" without crashing',
45
+ (size) => {
46
+ const { getByText } = render(<RadioButton label="Option" size={size} />);
47
+ expect(getByText('Option')).toBeTruthy();
48
+ },
49
+ );
50
+ });
@@ -1,6 +1,15 @@
1
1
  import React, { useState } from 'react';
2
- import { Pressable, StyleSheet, Text, View } from 'react-native';
3
- import { sys } from './tokens';
2
+
3
+ import {
4
+ Pressable,
5
+ StyleProp,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ ViewStyle,
10
+ } from 'react-native';
11
+
12
+ import { sys } from '../../tokens';
4
13
 
5
14
  export type RadioButtonSize = 'sm' | 'md';
6
15
 
@@ -13,6 +22,7 @@ export interface RadioButtonProps {
13
22
  description?: string;
14
23
  disabled?: boolean;
15
24
  invalid?: boolean;
25
+ style?: StyleProp<ViewStyle>;
16
26
  }
17
27
 
18
28
  const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
@@ -46,10 +56,14 @@ export function RadioButton({
46
56
  description,
47
57
  disabled = false,
48
58
  invalid = false,
59
+ style,
49
60
  }: RadioButtonProps) {
50
- const [internalChecked, setInternalChecked] = useState<boolean>(defaultChecked);
61
+ const [internalChecked, setInternalChecked] =
62
+ useState<boolean>(defaultChecked);
51
63
  const isControlled = checkedProp !== undefined;
52
- const isChecked: boolean = isControlled ? (checkedProp as boolean) : internalChecked;
64
+ const isChecked: boolean = isControlled
65
+ ? (checkedProp as boolean)
66
+ : internalChecked;
53
67
 
54
68
  const s = SIZE_TOKENS[size];
55
69
 
@@ -64,8 +78,8 @@ export function RadioButton({
64
78
  const borderColor = invalid
65
79
  ? cr.error.sysError
66
80
  : isChecked
67
- ? cr.accent.primary.sysPrimary
68
- : cr.outline.sysOutlineFixed;
81
+ ? cr.accent.primary.sysPrimary
82
+ : cr.outline.sysOutlineFixed;
69
83
 
70
84
  const circleBg = isChecked ? cr.accent.primary.sysPrimary : 'transparent';
71
85
 
@@ -75,10 +89,7 @@ export function RadioButton({
75
89
  accessibilityRole="radio"
76
90
  accessibilityLabel={label}
77
91
  accessibilityState={{ checked: isChecked, disabled }}
78
- style={({ pressed }) => [
79
- styles.root,
80
- disabled && styles.disabled,
81
- ]}
92
+ style={() => [styles.root, disabled && styles.disabled, style]}
82
93
  >
83
94
  {({ pressed }) => (
84
95
  <>
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+
3
+ import { Text } from 'react-native';
4
+
5
+ import { render } from '@testing-library/react-native';
6
+
7
+ import { ScreenContainer } from './index';
8
+
9
+ describe('ScreenContainer', () => {
10
+ it('renders children', () => {
11
+ const { getByText } = render(
12
+ <ScreenContainer>
13
+ <Text>Screen content</Text>
14
+ </ScreenContainer>,
15
+ );
16
+ expect(getByText('Screen content')).toBeTruthy();
17
+ });
18
+
19
+ it('renders multiple children', () => {
20
+ const { getByText } = render(
21
+ <ScreenContainer>
22
+ <Text>First</Text>
23
+ <Text>Second</Text>
24
+ </ScreenContainer>,
25
+ );
26
+ expect(getByText('First')).toBeTruthy();
27
+ expect(getByText('Second')).toBeTruthy();
28
+ });
29
+ });