@compsych/mobile-ui 1.0.11 → 1.0.13

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 (45) hide show
  1. package/package.json +1 -1
  2. package/src/components/ActionSheet/index.tsx +124 -57
  3. package/src/components/Alert/index.tsx +19 -10
  4. package/src/components/Avatar/index.tsx +95 -92
  5. package/src/components/Badge/index.tsx +39 -36
  6. package/src/components/BodyText/index.tsx +5 -5
  7. package/src/components/Breadcrumb/index.tsx +32 -30
  8. package/src/components/Button/Button.test.tsx +13 -7
  9. package/src/components/Button/index.tsx +114 -92
  10. package/src/components/ChatBubble/ChatBubble.test.tsx +77 -0
  11. package/src/components/ChatBubble/index.tsx +187 -0
  12. package/src/components/ChatInput/ChatInput.test.tsx +90 -0
  13. package/src/components/ChatInput/index.tsx +263 -0
  14. package/src/components/Checkbox/index.tsx +24 -21
  15. package/src/components/Chip/index.tsx +75 -72
  16. package/src/components/Divider/index.tsx +3 -3
  17. package/src/components/EmptyState/index.tsx +72 -55
  18. package/src/components/HeaderText/index.tsx +5 -5
  19. package/src/components/Input/index.tsx +40 -37
  20. package/src/components/List/index.tsx +2 -3
  21. package/src/components/Pagination/index.tsx +56 -43
  22. package/src/components/PlanCard/index.tsx +118 -50
  23. package/src/components/ProgressTracker/index.tsx +44 -26
  24. package/src/components/PromotionCard/index.tsx +217 -212
  25. package/src/components/RadioButton/index.tsx +27 -25
  26. package/src/components/ScreenContainer/ScreenContainer.test.tsx +12 -2
  27. package/src/components/ScreenContainer/index.tsx +21 -10
  28. package/src/components/SegmentedControl/index.tsx +37 -30
  29. package/src/components/SelectionCard/index.tsx +38 -26
  30. package/src/components/ServiceCard/index.tsx +162 -130
  31. package/src/components/Slider/index.tsx +77 -77
  32. package/src/components/Snackbar/index.tsx +69 -57
  33. package/src/components/Switch/index.tsx +13 -12
  34. package/src/components/Tooltip/index.tsx +3 -3
  35. package/src/index.ts +13 -0
  36. package/src/theme/ThemeContext.ts +11 -0
  37. package/src/theme/ThemeProvider.tsx +40 -0
  38. package/src/theme/__tests__/ThemeProvider.test.tsx +101 -0
  39. package/src/theme/__tests__/defaultTheme.test.ts +36 -0
  40. package/src/theme/__tests__/mergeTheme.test.ts +94 -0
  41. package/src/theme/defaultTheme.ts +15 -0
  42. package/src/theme/index.ts +8 -0
  43. package/src/theme/mergeTheme.ts +47 -0
  44. package/src/theme/types.ts +39 -0
  45. package/src/theme/useTheme.ts +16 -0
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
 
3
3
  import {
4
4
  Image,
@@ -13,7 +13,7 @@ import {
13
13
  import { Ionicons } from '@expo/vector-icons';
14
14
  import Svg, { Circle as SvgCircle } from 'react-native-svg';
15
15
 
16
- import { sys } from '../../tokens';
16
+ import { useTheme } from '../../theme';
17
17
  import { BodyText } from '../BodyText';
18
18
  import { HeaderText } from '../HeaderText';
19
19
 
@@ -45,8 +45,6 @@ export interface PromotionCardProps {
45
45
  style?: StyleProp<ViewStyle>;
46
46
  }
47
47
 
48
- const { colorRoles: cr, dimensions: dim } = sys;
49
-
50
48
  type VariantTokens = {
51
49
  cardBg: string;
52
50
  chipBg: string;
@@ -59,154 +57,6 @@ type VariantTokens = {
59
57
  progressColor: string;
60
58
  };
61
59
 
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
60
  const MD_HEIGHT = 197;
211
61
  const LG_HEIGHT = 363;
212
62
  const MD_BUTTON = 32;
@@ -238,8 +88,161 @@ export function PromotionCard({
238
88
  accessibilityLabel,
239
89
  style,
240
90
  }: PromotionCardProps) {
91
+ const { colorRoles: cr, dimensions: dim } = useTheme();
92
+
93
+ const tokens = useMemo(
94
+ (): Record<
95
+ PromotionCardVariant,
96
+ Record<PromotionCardUsage, VariantTokens>
97
+ > => ({
98
+ filled: {
99
+ neutral: {
100
+ cardBg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
101
+ chipBg: cr.surface.surfaceContainer.sysSurfaceContainerHighest,
102
+ chipTextColor: cr.surface.surface.sysOnSurface,
103
+ titleColor: '#0f0f0f',
104
+ descColor: cr.surface.surface.sysOnSurfaceVariant,
105
+ descOpacity: 1,
106
+ buttonBg: cr.addOn.primaryFixed.sysPrimaryFixedDim,
107
+ buttonIconColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
108
+ progressColor: cr.accent.primary.sysPrimary,
109
+ },
110
+ informative: {
111
+ cardBg: cr.custom.info.sysOnInfoContainer,
112
+ chipBg: cr.custom.info.sysInfoContainer,
113
+ chipTextColor: cr.custom.info.sysOnInfoContainer,
114
+ titleColor: cr.custom.info.sysOnInfo,
115
+ descColor: cr.transparent.neutral.sysWhite80,
116
+ descOpacity: 1,
117
+ buttonBg: cr.custom.info.sysOnInfo,
118
+ buttonIconColor: cr.custom.info.sysOnInfoContainer,
119
+ progressColor: cr.custom.info.sysOnInfo,
120
+ },
121
+ positive: {
122
+ cardBg: cr.custom.success.sysOnSuccessContainer,
123
+ chipBg: cr.custom.success.sysSuccessContainer,
124
+ chipTextColor: cr.custom.success.sysOnSuccessContainer,
125
+ titleColor: cr.custom.success.sysOnSuccess,
126
+ descColor: cr.transparent.neutral.sysWhite80,
127
+ descOpacity: 1,
128
+ buttonBg: cr.custom.success.sysOnSuccess,
129
+ buttonIconColor: cr.custom.success.sysOnSuccessContainer,
130
+ progressColor: cr.custom.success.sysOnSuccess,
131
+ },
132
+ danger: {
133
+ cardBg: cr.error.sysOnErrorContainer,
134
+ chipBg: cr.error.sysErrorContainer,
135
+ chipTextColor: cr.error.sysOnErrorContainer,
136
+ titleColor: cr.error.sysOnError,
137
+ descColor: cr.transparent.neutral.sysWhite80,
138
+ descOpacity: 1,
139
+ buttonBg: cr.error.sysOnError,
140
+ buttonIconColor: cr.error.sysOnErrorContainer,
141
+ progressColor: cr.error.sysOnError,
142
+ },
143
+ warning: {
144
+ cardBg: cr.custom.warning.sysOnWarningContainer,
145
+ chipBg: cr.custom.warning.sysWarningContainer,
146
+ chipTextColor: cr.custom.warning.sysOnWarningContainer,
147
+ titleColor: cr.custom.warning.sysOnWarning,
148
+ descColor: cr.transparent.neutral.sysWhite80,
149
+ descOpacity: 1,
150
+ buttonBg: cr.custom.warning.sysOnWarning,
151
+ buttonIconColor: cr.custom.warning.sysOnWarningContainer,
152
+ progressColor: cr.custom.warning.sysOnWarning,
153
+ },
154
+ image: {
155
+ cardBg: 'transparent',
156
+ chipBg: 'rgba(255,255,255,0.2)',
157
+ chipTextColor: cr.surface.inverse.sysInverseOnSurface,
158
+ titleColor: cr.accent.primary.sysOnPrimary,
159
+ descColor: cr.transparent.neutral.sysWhite80,
160
+ descOpacity: 1,
161
+ buttonBg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
162
+ buttonIconColor: cr.surface.surface.sysOnSurface,
163
+ progressColor: cr.accent.primary.sysOnPrimary,
164
+ },
165
+ },
166
+ tonal: {
167
+ neutral: {
168
+ cardBg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
169
+ chipBg: cr.surface.surfaceContainer.sysSurfaceContainerHighest,
170
+ chipTextColor: cr.surface.surface.sysOnSurface,
171
+ titleColor: '#0f0f0f',
172
+ descColor: cr.surface.surface.sysOnSurfaceVariant,
173
+ descOpacity: 1,
174
+ buttonBg: cr.addOn.primaryFixed.sysPrimaryFixedDim,
175
+ buttonIconColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
176
+ progressColor: cr.accent.primary.sysPrimary,
177
+ },
178
+ informative: {
179
+ cardBg: cr.custom.info.sysInfoContainer,
180
+ chipBg: cr.custom.info.sysOnInfo,
181
+ chipTextColor: cr.custom.info.sysOnInfoContainer,
182
+ titleColor: cr.custom.info.sysOnInfoContainer,
183
+ descColor: cr.custom.info.sysOnInfoContainer,
184
+ descOpacity: 0.64,
185
+ buttonBg: cr.custom.info.sysInfo,
186
+ buttonIconColor: cr.custom.info.sysOnInfo,
187
+ progressColor: cr.custom.info.sysInfo,
188
+ },
189
+ positive: {
190
+ cardBg: cr.custom.success.sysSuccessContainer,
191
+ chipBg: cr.custom.success.sysOnSuccess,
192
+ chipTextColor: cr.custom.success.sysOnSuccessContainer,
193
+ titleColor: cr.custom.success.sysOnSuccessContainer,
194
+ descColor: cr.custom.success.sysOnSuccessContainer,
195
+ descOpacity: 0.64,
196
+ buttonBg: cr.custom.success.sysSuccess,
197
+ buttonIconColor: cr.custom.success.sysOnSuccess,
198
+ progressColor: cr.custom.success.sysSuccess,
199
+ },
200
+ danger: {
201
+ cardBg: cr.error.sysErrorContainer,
202
+ chipBg: cr.error.sysOnError,
203
+ chipTextColor: cr.error.sysOnErrorContainer,
204
+ titleColor: cr.error.sysOnErrorContainer,
205
+ descColor: cr.error.sysOnErrorContainer,
206
+ descOpacity: 0.64,
207
+ buttonBg: cr.error.sysError,
208
+ buttonIconColor: cr.error.sysOnError,
209
+ progressColor: cr.error.sysError,
210
+ },
211
+ warning: {
212
+ cardBg: cr.custom.warning.sysWarningContainer,
213
+ chipBg: cr.custom.warning.sysOnWarning,
214
+ chipTextColor: cr.custom.warning.sysOnWarningContainer,
215
+ titleColor: cr.custom.warning.sysOnWarningContainer,
216
+ descColor: cr.custom.warning.sysOnWarningContainer,
217
+ descOpacity: 0.64,
218
+ buttonBg: cr.custom.warning.sysWarning,
219
+ buttonIconColor: cr.custom.warning.sysOnWarning,
220
+ progressColor: cr.custom.warning.sysWarning,
221
+ },
222
+ image: {
223
+ cardBg: 'transparent',
224
+ chipBg: 'rgba(255,255,255,0.2)',
225
+ chipTextColor: cr.surface.inverse.sysInverseOnSurface,
226
+ titleColor: cr.accent.primary.sysOnPrimary,
227
+ descColor: cr.transparent.neutral.sysWhite80,
228
+ descOpacity: 1,
229
+ buttonBg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
230
+ buttonIconColor: cr.surface.surface.sysOnSurface,
231
+ progressColor: cr.accent.primary.sysOnPrimary,
232
+ },
233
+ },
234
+ }),
235
+ [cr],
236
+ );
237
+
238
+ // lg tokens: neutral uses filled, others use tonal
239
+ function getLgTokens(u: PromotionCardUsage): VariantTokens {
240
+ if (u === 'neutral') return tokens.filled.neutral;
241
+ return tokens.tonal[u];
242
+ }
243
+
241
244
  const isLg = size === 'lg';
242
- const t = isLg ? getLgTokens(usage) : TOKENS[variant][usage];
245
+ const t = isLg ? getLgTokens(usage) : tokens[variant][usage];
243
246
  const isImage = usage === 'image';
244
247
  const p = Math.max(0, Math.min(1, progress));
245
248
 
@@ -250,7 +253,21 @@ export function PromotionCard({
250
253
 
251
254
  const containerStyle = [
252
255
  styles.card,
253
- isLg ? styles.cardLg : styles.cardMd,
256
+ isLg
257
+ ? {
258
+ height: LG_HEIGHT,
259
+ borderRadius: dim.borderRadius.sysRadiusXl,
260
+ padding: dim.spacing.padding.sysPadding32,
261
+ flexDirection: 'column' as const,
262
+ gap: dim.spacing.padding.sysPadding24,
263
+ }
264
+ : {
265
+ height: MD_HEIGHT,
266
+ borderRadius: dim.borderRadius.sysRadiusLg,
267
+ paddingHorizontal: dim.spacing.padding.sysPadding16,
268
+ paddingVertical: dim.spacing.padding.sysPadding16,
269
+ justifyContent: 'space-between' as const,
270
+ },
254
271
  { backgroundColor: t.cardBg, opacity: disabled ? 0.48 : 1 },
255
272
  style,
256
273
  ];
@@ -291,12 +308,48 @@ export function PromotionCard({
291
308
  {chipLabel && (
292
309
  <View
293
310
  style={[
294
- isLg ? styles.chipLg : styles.chipMd,
311
+ isLg
312
+ ? {
313
+ flexDirection: 'row' as const,
314
+ alignItems: 'center' as const,
315
+ alignSelf: 'flex-start' as const,
316
+ borderRadius: dim.borderRadius.sysRadiusFull,
317
+ paddingHorizontal: dim.spacing.padding.sysPadding16,
318
+ paddingVertical: dim.spacing.padding.sysPadding4,
319
+ gap: dim.spacing.padding.sysPadding8,
320
+ height: 32,
321
+ }
322
+ : {
323
+ flexDirection: 'row' as const,
324
+ alignItems: 'center' as const,
325
+ alignSelf: 'flex-start' as const,
326
+ borderRadius: dim.borderRadius.sysRadiusFull,
327
+ paddingHorizontal: dim.spacing.padding.sysPadding12,
328
+ paddingVertical: dim.spacing.padding.sysPadding4,
329
+ gap: dim.spacing.padding.sysPadding4,
330
+ height: 24,
331
+ },
295
332
  { backgroundColor: t.chipBg },
296
333
  ]}
297
334
  >
298
335
  {chipIcon && (
299
- <View style={isLg ? styles.chipIconWrapLg : styles.chipIconWrapMd}>
336
+ <View
337
+ style={
338
+ isLg
339
+ ? {
340
+ width: 20,
341
+ height: 20,
342
+ alignItems: 'center',
343
+ justifyContent: 'center',
344
+ }
345
+ : {
346
+ width: 16,
347
+ height: 16,
348
+ alignItems: 'center',
349
+ justifyContent: 'center',
350
+ }
351
+ }
352
+ >
300
353
  {chipIcon}
301
354
  </View>
302
355
  )}
@@ -307,7 +360,13 @@ export function PromotionCard({
307
360
  )}
308
361
 
309
362
  {/* Content (title + description) */}
310
- <View style={isLg ? styles.contentLg : styles.contentMd}>
363
+ <View
364
+ style={
365
+ isLg
366
+ ? { gap: dim.spacing.padding.sysPadding8, flex: 1 }
367
+ : { gap: dim.spacing.padding.sysPadding4, maxWidth: '78%' }
368
+ }
369
+ >
311
370
  {title &&
312
371
  (isLg ? (
313
372
  <HeaderText variant="titleLarge" color={t.titleColor}>
@@ -349,6 +408,7 @@ export function PromotionCard({
349
408
  {
350
409
  width: buttonSize,
351
410
  height: buttonSize,
411
+ borderRadius: dim.borderRadius.sysRadiusFull,
352
412
  backgroundColor: t.buttonBg,
353
413
  },
354
414
  ]}
@@ -448,65 +508,10 @@ const styles = StyleSheet.create({
448
508
  overflow: 'hidden',
449
509
  alignSelf: 'stretch',
450
510
  },
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
511
  buttonWrap: {
506
512
  position: 'absolute',
507
513
  },
508
514
  button: {
509
- borderRadius: sys.dimensions.borderRadius.sysRadiusFull,
510
515
  alignItems: 'center',
511
516
  justifyContent: 'center',
512
517
  overflow: 'hidden',
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
1
+ import React, { useMemo, useState } from 'react';
2
2
 
3
3
  import {
4
4
  Pressable,
@@ -9,7 +9,7 @@ import {
9
9
  ViewStyle,
10
10
  } from 'react-native';
11
11
 
12
- import { sys } from '../../tokens';
12
+ import { useTheme } from '../../theme';
13
13
 
14
14
  export type RadioButtonSize = 'sm' | 'md';
15
15
 
@@ -25,28 +25,6 @@ export interface RadioButtonProps {
25
25
  style?: StyleProp<ViewStyle>;
26
26
  }
27
27
 
28
- const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
29
-
30
- const SIZE_TOKENS = {
31
- sm: {
32
- // Container (overall hit area)
33
- hitArea: 36,
34
- // Visible circle
35
- circle: 20,
36
- // Inner dot when selected
37
- dot: 8,
38
- fontSize: ts.bodySmall.sysFontSize,
39
- lineHeight: ts.bodySmall.sysLineHeight,
40
- },
41
- md: {
42
- hitArea: 40,
43
- circle: 24,
44
- dot: 10,
45
- fontSize: ts.bodyMedium.sysFontSize,
46
- lineHeight: ts.bodyMedium.sysLineHeight,
47
- },
48
- };
49
-
50
28
  export function RadioButton({
51
29
  checked: checkedProp,
52
30
  defaultChecked = false,
@@ -58,6 +36,7 @@ export function RadioButton({
58
36
  invalid = false,
59
37
  style,
60
38
  }: RadioButtonProps) {
39
+ const { colorRoles: cr, dimensions: dim, typeScale: ts } = useTheme();
61
40
  const [internalChecked, setInternalChecked] =
62
41
  useState<boolean>(defaultChecked);
63
42
  const isControlled = checkedProp !== undefined;
@@ -65,7 +44,30 @@ export function RadioButton({
65
44
  ? (checkedProp as boolean)
66
45
  : internalChecked;
67
46
 
68
- const s = SIZE_TOKENS[size];
47
+ const sizeTokens = useMemo(
48
+ () => ({
49
+ sm: {
50
+ // Container (overall hit area)
51
+ hitArea: 36,
52
+ // Visible circle
53
+ circle: 20,
54
+ // Inner dot when selected
55
+ dot: 8,
56
+ fontSize: ts.bodySmall.sysFontSize,
57
+ lineHeight: ts.bodySmall.sysLineHeight,
58
+ },
59
+ md: {
60
+ hitArea: 40,
61
+ circle: 24,
62
+ dot: 10,
63
+ fontSize: ts.bodyMedium.sysFontSize,
64
+ lineHeight: ts.bodyMedium.sysLineHeight,
65
+ },
66
+ }),
67
+ [ts],
68
+ );
69
+
70
+ const s = sizeTokens[size];
69
71
 
70
72
  function handlePress() {
71
73
  if (disabled) return;
@@ -1,6 +1,6 @@
1
- import React from 'react';
1
+ import React, { createRef } from 'react';
2
2
 
3
- import { Text } from 'react-native';
3
+ import { ScrollView, Text } from 'react-native';
4
4
 
5
5
  import { render } from '@testing-library/react-native';
6
6
 
@@ -26,4 +26,14 @@ describe('ScreenContainer', () => {
26
26
  expect(getByText('First')).toBeTruthy();
27
27
  expect(getByText('Second')).toBeTruthy();
28
28
  });
29
+
30
+ it('forwards ref to the underlying ScrollView', () => {
31
+ const ref = createRef<ScrollView>();
32
+ render(
33
+ <ScreenContainer ref={ref}>
34
+ <Text>content</Text>
35
+ </ScreenContainer>,
36
+ );
37
+ expect(ref.current).not.toBeNull();
38
+ });
29
39
  });
@@ -2,22 +2,33 @@ import React from 'react';
2
2
 
3
3
  import { ScrollView, type ScrollViewProps } from 'react-native';
4
4
 
5
- import { sys } from '../../tokens';
6
-
7
- const { dimensions: dim, colorRoles: cr } = sys;
5
+ import { useTheme } from '../../theme';
8
6
 
9
7
  export interface ScreenContainerProps extends ScrollViewProps {
10
8
  children?: React.ReactNode;
11
9
  }
12
10
 
13
- export function ScreenContainer({
14
- children,
15
- style,
16
- contentContainerStyle,
17
- ...rest
18
- }: ScreenContainerProps) {
11
+ /**
12
+ * Scrollable screen wrapper that applies the default surface background and
13
+ * standard padding. The ref is forwarded to the underlying ScrollView so
14
+ * callers can imperatively scroll, measure, or attach scroll listeners.
15
+ *
16
+ * @example
17
+ * const scrollRef = useRef<ScrollView>(null);
18
+ * <ScreenContainer ref={scrollRef}>...</ScreenContainer>
19
+ */
20
+ export const ScreenContainer = React.forwardRef<
21
+ ScrollView,
22
+ ScreenContainerProps
23
+ >(function ScreenContainer(
24
+ { children, style, contentContainerStyle, ...rest },
25
+ ref,
26
+ ) {
27
+ const { dimensions: dim, colorRoles: cr } = useTheme();
28
+
19
29
  return (
20
30
  <ScrollView
31
+ ref={ref}
21
32
  style={[
22
33
  {
23
34
  flex: 1,
@@ -37,4 +48,4 @@ export function ScreenContainer({
37
48
  {children}
38
49
  </ScrollView>
39
50
  );
40
- }
51
+ });