@compsych/mobile-ui 1.0.9 → 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} +42 -14
  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} +92 -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,15 +1,20 @@
1
1
  import React from 'react';
2
+
2
3
  import {
3
4
  LayoutAnimation,
4
5
  Platform,
5
6
  Pressable,
7
+ StyleProp,
6
8
  StyleSheet,
7
9
  UIManager,
8
10
  View,
11
+ ViewStyle,
9
12
  } from 'react-native';
13
+
10
14
  import { Ionicons } from '@expo/vector-icons';
11
- import { sys } from './tokens';
12
- import { BodyText } from './BodyText';
15
+
16
+ import { sys } from '../../tokens';
17
+ import { BodyText } from '../BodyText';
13
18
 
14
19
  if (Platform.OS === 'android') {
15
20
  UIManager.setLayoutAnimationEnabledExperimental?.(true);
@@ -67,7 +72,10 @@ export function PlanCardDropdownItem({
67
72
  {title}
68
73
  </BodyText>
69
74
  {subtitle && (
70
- <BodyText variant="small" color={cr.surface.surface.sysOnSurfaceVariant}>
75
+ <BodyText
76
+ variant="small"
77
+ color={cr.surface.surface.sysOnSurfaceVariant}
78
+ >
71
79
  {subtitle}
72
80
  </BodyText>
73
81
  )}
@@ -80,9 +88,16 @@ export function PlanCardDropdownItem({
80
88
  onPress={onAudio}
81
89
  accessibilityRole="button"
82
90
  accessibilityLabel="Listen"
83
- style={({ pressed }) => [itemStyles.actionBtn, pressed && itemStyles.pressed]}
91
+ style={({ pressed }) => [
92
+ itemStyles.actionBtn,
93
+ pressed && itemStyles.pressed,
94
+ ]}
84
95
  >
85
- <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
+ />
86
101
  </Pressable>
87
102
  )}
88
103
  {onOpen && (
@@ -90,9 +105,16 @@ export function PlanCardDropdownItem({
90
105
  onPress={onOpen}
91
106
  accessibilityRole="button"
92
107
  accessibilityLabel="Open"
93
- style={({ pressed }) => [itemStyles.actionBtn, pressed && itemStyles.pressed]}
108
+ style={({ pressed }) => [
109
+ itemStyles.actionBtn,
110
+ pressed && itemStyles.pressed,
111
+ ]}
94
112
  >
95
- <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
+ />
96
118
  </Pressable>
97
119
  )}
98
120
  </View>
@@ -167,6 +189,7 @@ export interface PlanCardProps {
167
189
  expanded?: boolean;
168
190
  onToggle?: () => void;
169
191
  children?: React.ReactNode;
192
+ style?: StyleProp<ViewStyle>;
170
193
  }
171
194
 
172
195
  export function PlanCard({
@@ -177,6 +200,7 @@ export function PlanCard({
177
200
  expanded = false,
178
201
  onToggle,
179
202
  children,
203
+ style,
180
204
  }: PlanCardProps) {
181
205
  function handleToggle() {
182
206
  if (Platform.OS !== 'web') {
@@ -188,20 +212,21 @@ export function PlanCard({
188
212
  const hasContent = expanded && (children || (items && items.length > 0));
189
213
 
190
214
  return (
191
- <View style={cardStyles.card}>
215
+ <View style={[cardStyles.card, style]}>
192
216
  {/* Header */}
193
217
  <View style={cardStyles.header}>
194
218
  {/* Left: icon + title stacked */}
195
219
  <View style={cardStyles.left}>
196
220
  <View style={cardStyles.iconCircle}>
197
221
  {icon ?? (
198
- <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
+ />
199
227
  )}
200
228
  </View>
201
- <BodyText
202
- variant="medium"
203
- color={cr.surface.surface.sysOnSurface}
204
- >
229
+ <BodyText variant="medium" color={cr.surface.surface.sysOnSurface}>
205
230
  {title}
206
231
  </BodyText>
207
232
  </View>
@@ -210,7 +235,10 @@ export function PlanCard({
210
235
  <View style={cardStyles.right}>
211
236
  {tag && (
212
237
  <View style={cardStyles.chip}>
213
- <BodyText variant="small" color={cr.custom.info.sysOnInfoContainer}>
238
+ <BodyText
239
+ variant="small"
240
+ color={cr.custom.info.sysOnInfoContainer}
241
+ >
214
242
  {tag}
215
243
  </BodyText>
216
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
+ });