@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
@@ -1,16 +1,20 @@
1
- import React, { useRef } from 'react';
1
+ import React from 'react';
2
+
2
3
  import {
3
- Animated,
4
4
  LayoutAnimation,
5
5
  Platform,
6
6
  Pressable,
7
+ StyleProp,
7
8
  StyleSheet,
8
9
  UIManager,
9
10
  View,
11
+ ViewStyle,
10
12
  } from 'react-native';
13
+
11
14
  import { Ionicons } from '@expo/vector-icons';
12
- import { sys } from './tokens';
13
- import { BodyText } from './BodyText';
15
+
16
+ import { sys } from '../../tokens';
17
+ import { BodyText } from '../BodyText';
14
18
 
15
19
  if (Platform.OS === 'android') {
16
20
  UIManager.setLayoutAnimationEnabledExperimental?.(true);
@@ -68,7 +72,10 @@ export function PlanCardDropdownItem({
68
72
  {title}
69
73
  </BodyText>
70
74
  {subtitle && (
71
- <BodyText variant="small" color={cr.surface.surface.sysOnSurfaceVariant}>
75
+ <BodyText
76
+ variant="small"
77
+ color={cr.surface.surface.sysOnSurfaceVariant}
78
+ >
72
79
  {subtitle}
73
80
  </BodyText>
74
81
  )}
@@ -81,9 +88,16 @@ export function PlanCardDropdownItem({
81
88
  onPress={onAudio}
82
89
  accessibilityRole="button"
83
90
  accessibilityLabel="Listen"
84
- style={({ pressed }) => [itemStyles.actionBtn, pressed && itemStyles.pressed]}
91
+ style={({ pressed }) => [
92
+ itemStyles.actionBtn,
93
+ pressed && itemStyles.pressed,
94
+ ]}
85
95
  >
86
- <Ionicons name="headset-outline" size={16} color={cr.surface.surface.sysOnSurface} />
96
+ <Ionicons
97
+ name="headset-outline"
98
+ size={16}
99
+ color={cr.surface.surface.sysOnSurface}
100
+ />
87
101
  </Pressable>
88
102
  )}
89
103
  {onOpen && (
@@ -91,9 +105,16 @@ export function PlanCardDropdownItem({
91
105
  onPress={onOpen}
92
106
  accessibilityRole="button"
93
107
  accessibilityLabel="Open"
94
- style={({ pressed }) => [itemStyles.actionBtn, pressed && itemStyles.pressed]}
108
+ style={({ pressed }) => [
109
+ itemStyles.actionBtn,
110
+ pressed && itemStyles.pressed,
111
+ ]}
95
112
  >
96
- <Ionicons name="arrow-up-circle-outline" size={16} color={cr.surface.surface.sysOnSurface} />
113
+ <Ionicons
114
+ name="arrow-up-circle-outline"
115
+ size={16}
116
+ color={cr.surface.surface.sysOnSurface}
117
+ />
97
118
  </Pressable>
98
119
  )}
99
120
  </View>
@@ -168,6 +189,7 @@ export interface PlanCardProps {
168
189
  expanded?: boolean;
169
190
  onToggle?: () => void;
170
191
  children?: React.ReactNode;
192
+ style?: StyleProp<ViewStyle>;
171
193
  }
172
194
 
173
195
  export function PlanCard({
@@ -178,29 +200,33 @@ export function PlanCard({
178
200
  expanded = false,
179
201
  onToggle,
180
202
  children,
203
+ style,
181
204
  }: PlanCardProps) {
182
205
  function handleToggle() {
183
- LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
206
+ if (Platform.OS !== 'web') {
207
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
208
+ }
184
209
  onToggle?.();
185
210
  }
186
211
 
187
212
  const hasContent = expanded && (children || (items && items.length > 0));
188
213
 
189
214
  return (
190
- <View style={cardStyles.card}>
215
+ <View style={[cardStyles.card, style]}>
191
216
  {/* Header */}
192
217
  <View style={cardStyles.header}>
193
218
  {/* Left: icon + title stacked */}
194
219
  <View style={cardStyles.left}>
195
220
  <View style={cardStyles.iconCircle}>
196
221
  {icon ?? (
197
- <Ionicons name="document-text-outline" size={16} color={cr.accent.primary.sysOnPrimary} />
222
+ <Ionicons
223
+ name="document-text-outline"
224
+ size={16}
225
+ color={cr.accent.primary.sysOnPrimary}
226
+ />
198
227
  )}
199
228
  </View>
200
- <BodyText
201
- variant="medium"
202
- color={cr.surface.surface.sysOnSurface}
203
- >
229
+ <BodyText variant="medium" color={cr.surface.surface.sysOnSurface}>
204
230
  {title}
205
231
  </BodyText>
206
232
  </View>
@@ -209,7 +235,10 @@ export function PlanCard({
209
235
  <View style={cardStyles.right}>
210
236
  {tag && (
211
237
  <View style={cardStyles.chip}>
212
- <BodyText variant="small" color={cr.custom.info.sysOnInfoContainer}>
238
+ <BodyText
239
+ variant="small"
240
+ color={cr.custom.info.sysOnInfoContainer}
241
+ >
213
242
  {tag}
214
243
  </BodyText>
215
244
  </View>
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+
3
+ import { render } from '@testing-library/react-native';
4
+
5
+ import { ProgressBar, ProgressTracker } from './index';
6
+
7
+ const steps = [
8
+ { label: 'Start', state: 'completed' as const },
9
+ { label: 'In progress', state: 'active' as const },
10
+ { label: 'End', state: 'pending' as const },
11
+ ];
12
+
13
+ describe('ProgressTracker', () => {
14
+ it('renders step labels when showLabels is true', () => {
15
+ const { getByText } = render(<ProgressTracker steps={steps} showLabels />);
16
+ expect(getByText('Start')).toBeTruthy();
17
+ expect(getByText('In progress')).toBeTruthy();
18
+ expect(getByText('End')).toBeTruthy();
19
+ });
20
+
21
+ it('renders without labels by default', () => {
22
+ const { toJSON } = render(<ProgressTracker steps={steps} />);
23
+ expect(toJSON()).toBeTruthy();
24
+ });
25
+
26
+ it.each(['sm', 'lg'] as const)(
27
+ 'renders size "%s" without crashing',
28
+ (size) => {
29
+ const { toJSON } = render(<ProgressTracker steps={steps} size={size} />);
30
+ expect(toJSON()).toBeTruthy();
31
+ },
32
+ );
33
+ });
34
+
35
+ describe('ProgressBar', () => {
36
+ it('renders with progress value', () => {
37
+ const { toJSON } = render(<ProgressBar progress={0.5} />);
38
+ expect(toJSON()).toBeTruthy();
39
+ });
40
+
41
+ it('clamps progress to 0–1 range', () => {
42
+ const { toJSON: toJSON1 } = render(<ProgressBar progress={-0.5} />);
43
+ const { toJSON: toJSON2 } = render(<ProgressBar progress={1.5} />);
44
+ expect(toJSON1()).toBeTruthy();
45
+ expect(toJSON2()).toBeTruthy();
46
+ });
47
+ });
@@ -1,11 +1,13 @@
1
1
  import React from 'react';
2
- import { StyleSheet, Text, View } from 'react-native';
3
- import { sys } from './tokens';
2
+
3
+ import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native';
4
+
5
+ import { sys } from '../../tokens';
4
6
 
5
7
  const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
6
8
 
7
9
  // ─────────────────────────────────────────────────────────────────────────────
8
- // ProgressBar — standalone horizontal bar
10
+ // ProgressBar — standalone horizontal fill bar (0–100)
9
11
  // ─────────────────────────────────────────────────────────────────────────────
10
12
 
11
13
  export interface ProgressBarProps {
@@ -23,32 +25,23 @@ export function ProgressBar({ progress }: ProgressBarProps) {
23
25
  accessibilityValue={{ min: 0, max: 100, now: clamped }}
24
26
  style={styles.track}
25
27
  >
26
- {clamped > 0 && (
27
- <View
28
- style={[
29
- styles.fill,
30
- // Use percentage width — relies on the parent having a defined
31
- // width (which it always does as flex-stretch in the tracker).
32
- { width: `${clamped}%` },
33
- ]}
34
- />
35
- )}
28
+ {clamped > 0 && <View style={[styles.fill, { width: `${clamped}%` }]} />}
36
29
  </View>
37
30
  );
38
31
  }
39
32
 
40
33
  // ─────────────────────────────────────────────────────────────────────────────
41
- // ProgressTracker — multi-step labelled tracker
34
+ // ProgressTracker — multi-step bar tracker
42
35
  // ─────────────────────────────────────────────────────────────────────────────
43
36
 
44
37
  export type StepState = 'completed' | 'active' | 'pending';
45
38
 
46
39
  export interface TrackerStep {
47
- label: string;
40
+ label?: string;
48
41
  /**
49
42
  * `completed` → full green bar
50
- * `active` → partial green bar (25 % fill the "in-progress" indicator)
51
- * `pending` → empty track
43
+ * `active` → full green bar (current stepsame fill as completed)
44
+ * `pending` → gray track only
52
45
  */
53
46
  state: StepState;
54
47
  }
@@ -58,8 +51,9 @@ export type ProgressTrackerSize = 'sm' | 'lg';
58
51
  export interface ProgressTrackerProps {
59
52
  steps: TrackerStep[];
60
53
  size?: ProgressTrackerSize;
61
- /** Show step labels default true */
54
+ /** Render step labels above each bar. Default false. */
62
55
  showLabels?: boolean;
56
+ style?: StyleProp<ViewStyle>;
63
57
  }
64
58
 
65
59
  const SIZE_TOKENS = {
@@ -80,22 +74,19 @@ const SIZE_TOKENS = {
80
74
  };
81
75
 
82
76
  function stepProgress(state: StepState): number {
83
- switch (state) {
84
- case 'completed': return 100;
85
- case 'active': return 25;
86
- case 'pending': return 0;
87
- }
77
+ return state === 'pending' ? 0 : 100;
88
78
  }
89
79
 
90
80
  export function ProgressTracker({
91
81
  steps,
92
82
  size = 'lg',
93
- showLabels = true,
83
+ showLabels = false,
84
+ style,
94
85
  }: ProgressTrackerProps) {
95
86
  const s = SIZE_TOKENS[size];
96
87
 
97
88
  return (
98
- <View style={[styles.row, { gap: s.stepGap }]}>
89
+ <View style={[styles.row, { gap: s.stepGap }, style]}>
99
90
  {steps.map((step, i) => {
100
91
  const isActive = step.state !== 'pending';
101
92
  const labelColor = isActive
@@ -0,0 +1,73 @@
1
+ import React from 'react';
2
+
3
+ import { fireEvent, render } from '@testing-library/react-native';
4
+
5
+ import { PromotionCard } from './index';
6
+
7
+ describe('PromotionCard', () => {
8
+ it('renders title and description', () => {
9
+ const { getByText } = render(
10
+ <PromotionCard title="New feature" description="Check it out" />,
11
+ );
12
+ expect(getByText('New feature')).toBeTruthy();
13
+ expect(getByText('Check it out')).toBeTruthy();
14
+ });
15
+
16
+ it('renders chip label when provided', () => {
17
+ const { getByText } = render(
18
+ <PromotionCard title="Card" chipLabel="NEW" />,
19
+ );
20
+ expect(getByText('NEW')).toBeTruthy();
21
+ });
22
+
23
+ it('calls onPress when tapped', () => {
24
+ const onPress = jest.fn();
25
+ const { getByRole } = render(
26
+ <PromotionCard
27
+ title="Promo"
28
+ accessibilityLabel="Promo card"
29
+ onPress={onPress}
30
+ />,
31
+ );
32
+ fireEvent.press(getByRole('button', { name: 'Promo card' }));
33
+ expect(onPress).toHaveBeenCalledTimes(1);
34
+ });
35
+
36
+ it('renders disabled state without crashing', () => {
37
+ const { toJSON } = render(
38
+ <PromotionCard
39
+ title="Promo"
40
+ accessibilityLabel="Promo card"
41
+ onPress={jest.fn()}
42
+ disabled
43
+ />,
44
+ );
45
+ expect(toJSON()).toBeTruthy();
46
+ });
47
+
48
+ it.each(['filled', 'tonal'] as const)(
49
+ 'renders variant "%s" without crashing',
50
+ (variant) => {
51
+ const { toJSON } = render(
52
+ <PromotionCard title="Card" variant={variant} />,
53
+ );
54
+ expect(toJSON()).toBeTruthy();
55
+ },
56
+ );
57
+
58
+ it.each(['md', 'lg'] as const)(
59
+ 'renders size "%s" without crashing',
60
+ (size) => {
61
+ const { toJSON } = render(<PromotionCard title="Card" size={size} />);
62
+ expect(toJSON()).toBeTruthy();
63
+ },
64
+ );
65
+
66
+ it.each(['neutral', 'informative', 'positive', 'danger', 'warning'] as const)(
67
+ 'renders usage "%s" without crashing',
68
+ (usage) => {
69
+ const { toJSON } = render(<PromotionCard title="Card" usage={usage} />);
70
+ expect(toJSON()).toBeTruthy();
71
+ },
72
+ );
73
+ });