@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
@@ -13,7 +13,7 @@ import {
13
13
 
14
14
  import { Ionicons } from '@expo/vector-icons';
15
15
 
16
- import { sys } from '../../tokens';
16
+ import { useTheme } from '../../theme';
17
17
  import { BodyText } from '../BodyText';
18
18
 
19
19
  if (Platform.OS === 'android') {
@@ -36,8 +36,6 @@ interface DropdownItemProps extends PlanCardItemData {
36
36
  showDivider?: boolean;
37
37
  }
38
38
 
39
- const { colorRoles: cr, dimensions: dim } = sys;
40
-
41
39
  export function PlanCardDropdownItem({
42
40
  title,
43
41
  subtitle,
@@ -47,9 +45,19 @@ export function PlanCardDropdownItem({
47
45
  onOpen,
48
46
  showDivider = true,
49
47
  }: DropdownItemProps) {
48
+ const { colorRoles: cr, dimensions: dim } = useTheme();
49
+
50
50
  return (
51
51
  <View>
52
- <View style={itemStyles.row}>
52
+ <View
53
+ style={[
54
+ itemStyles.row,
55
+ {
56
+ paddingVertical: dim.spacing.padding.sysPadding16,
57
+ gap: dim.spacing.padding.sysPadding12,
58
+ },
59
+ ]}
60
+ >
53
61
  {/* Check indicator */}
54
62
  <Pressable
55
63
  onPress={onToggle}
@@ -58,11 +66,27 @@ export function PlanCardDropdownItem({
58
66
  style={itemStyles.checkWrap}
59
67
  >
60
68
  {checked ? (
61
- <View style={itemStyles.checkedCircle}>
69
+ <View
70
+ style={[
71
+ itemStyles.checkedCircle,
72
+ {
73
+ borderRadius: dim.borderRadius.sysRadiusFull,
74
+ backgroundColor: cr.custom.success.sysSuccess,
75
+ },
76
+ ]}
77
+ >
62
78
  <Ionicons name="checkmark" size={12} color="#fff" />
63
79
  </View>
64
80
  ) : (
65
- <View style={itemStyles.uncheckedCircle} />
81
+ <View
82
+ style={[
83
+ itemStyles.uncheckedCircle,
84
+ {
85
+ borderRadius: dim.borderRadius.sysRadiusFull,
86
+ borderColor: cr.outline.sysOutlineFixed,
87
+ },
88
+ ]}
89
+ />
66
90
  )}
67
91
  </Pressable>
68
92
 
@@ -82,7 +106,9 @@ export function PlanCardDropdownItem({
82
106
  </View>
83
107
 
84
108
  {/* Action buttons */}
85
- <View style={itemStyles.actions}>
109
+ <View
110
+ style={[itemStyles.actions, { gap: dim.spacing.padding.sysPadding8 }]}
111
+ >
86
112
  {onAudio && (
87
113
  <Pressable
88
114
  onPress={onAudio}
@@ -90,6 +116,11 @@ export function PlanCardDropdownItem({
90
116
  accessibilityLabel="Listen"
91
117
  style={({ pressed }) => [
92
118
  itemStyles.actionBtn,
119
+ {
120
+ borderRadius: dim.borderRadius.sysRadiusFull,
121
+ borderWidth: dim.borderWidth.sysStrokeThin,
122
+ borderColor: cr.outline.sysOutline,
123
+ },
93
124
  pressed && itemStyles.pressed,
94
125
  ]}
95
126
  >
@@ -107,6 +138,11 @@ export function PlanCardDropdownItem({
107
138
  accessibilityLabel="Open"
108
139
  style={({ pressed }) => [
109
140
  itemStyles.actionBtn,
141
+ {
142
+ borderRadius: dim.borderRadius.sysRadiusFull,
143
+ borderWidth: dim.borderWidth.sysStrokeThin,
144
+ borderColor: cr.outline.sysOutline,
145
+ },
110
146
  pressed && itemStyles.pressed,
111
147
  ]}
112
148
  >
@@ -120,7 +156,14 @@ export function PlanCardDropdownItem({
120
156
  </View>
121
157
  </View>
122
158
 
123
- {showDivider && <View style={itemStyles.divider} />}
159
+ {showDivider && (
160
+ <View
161
+ style={[
162
+ itemStyles.divider,
163
+ { backgroundColor: cr.outline.sysOutline },
164
+ ]}
165
+ />
166
+ )}
124
167
  </View>
125
168
  );
126
169
  }
@@ -129,8 +172,6 @@ const itemStyles = StyleSheet.create({
129
172
  row: {
130
173
  flexDirection: 'row',
131
174
  alignItems: 'center',
132
- paddingVertical: dim.spacing.padding.sysPadding16,
133
- gap: dim.spacing.padding.sysPadding12,
134
175
  },
135
176
  checkWrap: {
136
177
  width: 20,
@@ -141,17 +182,13 @@ const itemStyles = StyleSheet.create({
141
182
  checkedCircle: {
142
183
  width: 20,
143
184
  height: 20,
144
- borderRadius: dim.borderRadius.sysRadiusFull,
145
- backgroundColor: cr.custom.success.sysSuccess,
146
185
  alignItems: 'center',
147
186
  justifyContent: 'center',
148
187
  },
149
188
  uncheckedCircle: {
150
189
  width: 20,
151
190
  height: 20,
152
- borderRadius: dim.borderRadius.sysRadiusFull,
153
191
  borderWidth: 1.5,
154
- borderColor: cr.outline.sysOutlineFixed,
155
192
  },
156
193
  textCol: {
157
194
  flex: 1,
@@ -159,14 +196,10 @@ const itemStyles = StyleSheet.create({
159
196
  },
160
197
  actions: {
161
198
  flexDirection: 'row',
162
- gap: dim.spacing.padding.sysPadding8,
163
199
  },
164
200
  actionBtn: {
165
201
  width: 32,
166
202
  height: 32,
167
- borderRadius: dim.borderRadius.sysRadiusFull,
168
- borderWidth: dim.borderWidth.sysStrokeThin,
169
- borderColor: cr.outline.sysOutline,
170
203
  alignItems: 'center',
171
204
  justifyContent: 'center',
172
205
  },
@@ -175,7 +208,6 @@ const itemStyles = StyleSheet.create({
175
208
  },
176
209
  divider: {
177
210
  height: StyleSheet.hairlineWidth,
178
- backgroundColor: cr.outline.sysOutline,
179
211
  },
180
212
  });
181
213
 
@@ -202,6 +234,8 @@ export function PlanCard({
202
234
  children,
203
235
  style,
204
236
  }: PlanCardProps) {
237
+ const { colorRoles: cr, dimensions: dim } = useTheme();
238
+
205
239
  function handleToggle() {
206
240
  if (Platform.OS !== 'web') {
207
241
  LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
@@ -212,12 +246,37 @@ export function PlanCard({
212
246
  const hasContent = expanded && (children || (items && items.length > 0));
213
247
 
214
248
  return (
215
- <View style={[cardStyles.card, style]}>
249
+ <View
250
+ style={[
251
+ cardStyles.card,
252
+ {
253
+ backgroundColor:
254
+ cr.surface.surfaceContainer.sysSurfaceContainerLowest,
255
+ borderRadius: dim.borderRadius.sysRadiusMd,
256
+ borderWidth: dim.borderWidth.sysStrokeThin,
257
+ borderColor: cr.outline.sysOutline,
258
+ padding: dim.spacing.padding.sysPadding16,
259
+ },
260
+ style,
261
+ ]}
262
+ >
216
263
  {/* Header */}
217
- <View style={cardStyles.header}>
264
+ <View
265
+ style={[cardStyles.header, { gap: dim.spacing.padding.sysPadding12 }]}
266
+ >
218
267
  {/* Left: icon + title stacked */}
219
- <View style={cardStyles.left}>
220
- <View style={cardStyles.iconCircle}>
268
+ <View
269
+ style={[cardStyles.left, { gap: dim.spacing.padding.sysPadding8 }]}
270
+ >
271
+ <View
272
+ style={[
273
+ cardStyles.iconCircle,
274
+ {
275
+ borderRadius: dim.borderRadius.sysRadiusFull,
276
+ backgroundColor: cr.accent.primary.sysPrimary,
277
+ },
278
+ ]}
279
+ >
221
280
  {icon ?? (
222
281
  <Ionicons
223
282
  name="document-text-outline"
@@ -232,9 +291,21 @@ export function PlanCard({
232
291
  </View>
233
292
 
234
293
  {/* Right: tag chip + toggle */}
235
- <View style={cardStyles.right}>
294
+ <View
295
+ style={[cardStyles.right, { gap: dim.spacing.padding.sysPadding8 }]}
296
+ >
236
297
  {tag && (
237
- <View style={cardStyles.chip}>
298
+ <View
299
+ style={[
300
+ cardStyles.chip,
301
+ {
302
+ backgroundColor: cr.custom.info.sysInfoContainer,
303
+ borderRadius: dim.borderRadius.sysRadiusFull,
304
+ paddingHorizontal: dim.spacing.padding.sysPadding8,
305
+ paddingVertical: dim.spacing.padding.sysPadding4,
306
+ },
307
+ ]}
308
+ >
238
309
  <BodyText
239
310
  variant="small"
240
311
  color={cr.custom.info.sysOnInfoContainer}
@@ -249,6 +320,11 @@ export function PlanCard({
249
320
  accessibilityLabel={expanded ? 'Collapse' : 'Expand'}
250
321
  style={({ pressed }) => [
251
322
  cardStyles.toggleBtn,
323
+ {
324
+ borderRadius: dim.borderRadius.sysRadiusFull,
325
+ backgroundColor:
326
+ cr.surface.surfaceContainer.sysSurfaceContainer,
327
+ },
252
328
  pressed && cardStyles.pressed,
253
329
  ]}
254
330
  >
@@ -263,8 +339,21 @@ export function PlanCard({
263
339
 
264
340
  {/* Expanded content */}
265
341
  {hasContent && (
266
- <View style={cardStyles.content}>
267
- <View style={cardStyles.contentDivider} />
342
+ <View
343
+ style={[
344
+ cardStyles.content,
345
+ { marginTop: dim.spacing.padding.sysPadding8 },
346
+ ]}
347
+ >
348
+ <View
349
+ style={[
350
+ cardStyles.contentDivider,
351
+ {
352
+ backgroundColor: cr.outline.sysOutline,
353
+ marginBottom: dim.spacing.padding.sysPadding4,
354
+ },
355
+ ]}
356
+ />
268
357
  {children
269
358
  ? children
270
359
  : items!.map((item, index) => (
@@ -282,60 +371,39 @@ export function PlanCard({
282
371
 
283
372
  const cardStyles = StyleSheet.create({
284
373
  card: {
285
- backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
286
- borderRadius: dim.borderRadius.sysRadiusMd,
287
- borderWidth: dim.borderWidth.sysStrokeThin,
288
- borderColor: cr.outline.sysOutline,
289
- padding: dim.spacing.padding.sysPadding16,
290
374
  overflow: 'hidden',
291
375
  },
292
376
  header: {
293
377
  flexDirection: 'row',
294
378
  alignItems: 'center',
295
379
  justifyContent: 'space-between',
296
- gap: dim.spacing.padding.sysPadding12,
297
380
  },
298
381
  left: {
299
382
  flex: 1,
300
- gap: dim.spacing.padding.sysPadding8,
301
383
  },
302
384
  iconCircle: {
303
385
  width: 28,
304
386
  height: 28,
305
- borderRadius: dim.borderRadius.sysRadiusFull,
306
- backgroundColor: cr.accent.primary.sysPrimary,
307
387
  alignItems: 'center',
308
388
  justifyContent: 'center',
309
389
  },
310
390
  right: {
311
391
  flexDirection: 'row',
312
392
  alignItems: 'center',
313
- gap: dim.spacing.padding.sysPadding8,
314
393
  alignSelf: 'flex-start',
315
394
  },
316
- chip: {
317
- backgroundColor: cr.custom.info.sysInfoContainer,
318
- borderRadius: dim.borderRadius.sysRadiusFull,
319
- paddingHorizontal: dim.spacing.padding.sysPadding8,
320
- paddingVertical: dim.spacing.padding.sysPadding4,
321
- },
395
+ chip: {},
322
396
  toggleBtn: {
323
397
  width: 20,
324
398
  height: 20,
325
- borderRadius: dim.borderRadius.sysRadiusFull,
326
- backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainer,
327
399
  alignItems: 'center',
328
400
  justifyContent: 'center',
329
401
  },
330
402
  pressed: {
331
403
  opacity: 0.7,
332
404
  },
333
- content: {
334
- marginTop: dim.spacing.padding.sysPadding8,
335
- },
405
+ content: {},
336
406
  contentDivider: {
337
407
  height: StyleSheet.hairlineWidth,
338
- backgroundColor: cr.outline.sysOutline,
339
- marginBottom: dim.spacing.padding.sysPadding4,
340
408
  },
341
409
  });
@@ -1,10 +1,8 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
 
3
3
  import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native';
4
4
 
5
- import { sys } from '../../tokens';
6
-
7
- const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
5
+ import { useTheme } from '../../theme';
8
6
 
9
7
  // ─────────────────────────────────────────────────────────────────────────────
10
8
  // ProgressBar — standalone horizontal fill bar (0–100)
@@ -16,6 +14,7 @@ export interface ProgressBarProps {
16
14
  }
17
15
 
18
16
  export function ProgressBar({ progress }: ProgressBarProps) {
17
+ const { colorRoles: cr } = useTheme();
19
18
  const clamped = Math.min(100, Math.max(0, progress));
20
19
 
21
20
  return (
@@ -23,9 +22,25 @@ export function ProgressBar({ progress }: ProgressBarProps) {
23
22
  accessible
24
23
  accessibilityRole="progressbar"
25
24
  accessibilityValue={{ min: 0, max: 100, now: clamped }}
26
- style={styles.track}
25
+ style={[
26
+ styles.track,
27
+ {
28
+ backgroundColor:
29
+ cr.surface.surfaceContainer.sysSurfaceContainerHighest,
30
+ },
31
+ ]}
27
32
  >
28
- {clamped > 0 && <View style={[styles.fill, { width: `${clamped}%` }]} />}
33
+ {clamped > 0 && (
34
+ <View
35
+ style={[
36
+ styles.fill,
37
+ {
38
+ width: `${clamped}%`,
39
+ backgroundColor: cr.custom.success.sysSuccess,
40
+ },
41
+ ]}
42
+ />
43
+ )}
29
44
  </View>
30
45
  );
31
46
  }
@@ -56,23 +71,6 @@ export interface ProgressTrackerProps {
56
71
  style?: StyleProp<ViewStyle>;
57
72
  }
58
73
 
59
- const SIZE_TOKENS = {
60
- sm: {
61
- fontSize: ts.labelSmall.sysFontSize,
62
- lineHeight: ts.labelSmall.sysLineHeight,
63
- letterSpacing: ts.labelSmall.sysTracking,
64
- labelBarGap: dim.spacing.padding.sysPadding8,
65
- stepGap: dim.spacing.padding.sysPadding4,
66
- },
67
- lg: {
68
- fontSize: ts.labelMedium.sysFontSize,
69
- lineHeight: ts.labelMedium.sysLineHeight,
70
- letterSpacing: ts.labelMedium.sysTracking,
71
- labelBarGap: dim.spacing.padding.sysPadding16,
72
- stepGap: dim.spacing.padding.sysPadding8,
73
- },
74
- };
75
-
76
74
  function stepProgress(state: StepState): number {
77
75
  return state === 'pending' ? 0 : 100;
78
76
  }
@@ -83,7 +81,29 @@ export function ProgressTracker({
83
81
  showLabels = false,
84
82
  style,
85
83
  }: ProgressTrackerProps) {
86
- const s = SIZE_TOKENS[size];
84
+ const { colorRoles: cr, dimensions: dim, typeScale: ts } = useTheme();
85
+
86
+ const sizeTokens = useMemo(
87
+ () => ({
88
+ sm: {
89
+ fontSize: ts.labelSmall.sysFontSize,
90
+ lineHeight: ts.labelSmall.sysLineHeight,
91
+ letterSpacing: ts.labelSmall.sysTracking,
92
+ labelBarGap: dim.spacing.padding.sysPadding8,
93
+ stepGap: dim.spacing.padding.sysPadding4,
94
+ },
95
+ lg: {
96
+ fontSize: ts.labelMedium.sysFontSize,
97
+ lineHeight: ts.labelMedium.sysLineHeight,
98
+ letterSpacing: ts.labelMedium.sysTracking,
99
+ labelBarGap: dim.spacing.padding.sysPadding16,
100
+ stepGap: dim.spacing.padding.sysPadding8,
101
+ },
102
+ }),
103
+ [ts, dim],
104
+ );
105
+
106
+ const s = sizeTokens[size];
87
107
 
88
108
  return (
89
109
  <View style={[styles.row, { gap: s.stepGap }, style]}>
@@ -128,7 +148,6 @@ const styles = StyleSheet.create({
128
148
  height: 4,
129
149
  width: '100%',
130
150
  borderRadius: 9999,
131
- backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerHighest,
132
151
  overflow: 'hidden',
133
152
  },
134
153
  fill: {
@@ -137,7 +156,6 @@ const styles = StyleSheet.create({
137
156
  bottom: 0,
138
157
  left: 0,
139
158
  borderRadius: 9999,
140
- backgroundColor: cr.custom.success.sysSuccess,
141
159
  },
142
160
  // ProgressTracker
143
161
  row: {