@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.
- package/package.json +40 -2
- package/src/components/ActionSheet/ActionSheet.test.tsx +89 -0
- package/src/{ActionSheet.tsx → components/ActionSheet/index.tsx} +16 -8
- package/src/components/Alert/Alert.test.tsx +61 -0
- package/src/{Alert.tsx → components/Alert/index.tsx} +14 -11
- package/src/components/Avatar/Avatar.test.tsx +52 -0
- package/src/{Avatar.tsx → components/Avatar/index.tsx} +15 -13
- package/src/components/Badge/Badge.test.tsx +30 -0
- package/src/{Badge.tsx → components/Badge/index.tsx} +17 -9
- package/src/components/BodyText/BodyText.test.tsx +30 -0
- package/src/{BodyText.tsx → components/BodyText/index.tsx} +9 -5
- package/src/components/Breadcrumb/Breadcrumb.test.tsx +37 -0
- package/src/{Breadcrumb.tsx → components/Breadcrumb/index.tsx} +13 -4
- package/src/components/Button/Button.test.tsx +45 -0
- package/src/{Button.tsx → components/Button/index.tsx} +36 -17
- package/src/components/Checkbox/Checkbox.test.tsx +57 -0
- package/src/{Checkbox.tsx → components/Checkbox/index.tsx} +37 -15
- package/src/components/Chip/Chip.test.tsx +40 -0
- package/src/{Chip.tsx → components/Chip/index.tsx} +18 -9
- package/src/components/Divider/Divider.test.tsx +28 -0
- package/src/{Divider.tsx → components/Divider/index.tsx} +30 -20
- package/src/components/EmptyState/EmptyState.test.tsx +60 -0
- package/src/{EmptyState.tsx → components/EmptyState/index.tsx} +17 -7
- package/src/components/HeaderText/HeaderText.test.tsx +43 -0
- package/src/{HeaderText.tsx → components/HeaderText/index.tsx} +9 -5
- package/src/components/Input/Input.test.tsx +43 -0
- package/src/{Input.tsx → components/Input/index.tsx} +31 -11
- package/src/components/List/List.test.tsx +59 -0
- package/src/{List.tsx → components/List/index.tsx} +23 -7
- package/src/components/Pagination/Pagination.test.tsx +56 -0
- package/src/{Pagination.tsx → components/Pagination/index.tsx} +29 -13
- package/src/components/PlanCard/PlanCard.test.tsx +44 -0
- package/src/{PlanCard.tsx → components/PlanCard/index.tsx} +46 -17
- package/src/components/ProgressTracker/ProgressTracker.test.tsx +47 -0
- package/src/{ProgressTracker.tsx → components/ProgressTracker/index.tsx} +16 -25
- package/src/components/PromotionCard/PromotionCard.test.tsx +73 -0
- package/src/components/PromotionCard/index.tsx +538 -0
- package/src/components/RadioButton/RadioButton.test.tsx +50 -0
- package/src/{RadioButton.tsx → components/RadioButton/index.tsx} +21 -10
- package/src/components/ScreenContainer/ScreenContainer.test.tsx +29 -0
- package/src/components/ScreenContainer/index.tsx +41 -0
- package/src/components/SegmentedControl/SegmentedControl.test.tsx +60 -0
- package/src/{SegmentedControl.tsx → components/SegmentedControl/index.tsx} +15 -7
- package/src/components/SelectionCard/SelectionCard.test.tsx +48 -0
- package/src/components/SelectionCard/index.tsx +213 -0
- package/src/components/ServiceCard/ServiceCard.test.tsx +61 -0
- package/src/{Card.tsx → components/ServiceCard/index.tsx} +106 -49
- package/src/components/Slider/Slider.test.tsx +33 -0
- package/src/{Slider.tsx → components/Slider/index.tsx} +26 -36
- package/src/components/Snackbar/Snackbar.test.tsx +61 -0
- package/src/{Snackbar.tsx → components/Snackbar/index.tsx} +13 -7
- package/src/components/Switch/Switch.test.tsx +54 -0
- package/src/{Switch.tsx → components/Switch/index.tsx} +16 -5
- package/src/components/Tooltip/Tooltip.test.tsx +30 -0
- package/src/{Tooltip.tsx → components/Tooltip/index.tsx} +11 -7
- package/src/icons/AtomIcon.tsx +32 -0
- package/src/icons/BinocularsIcon.tsx +45 -0
- package/src/icons/FileChartColumnIncreasingIcon.tsx +44 -0
- package/src/icons/FlagIcon.tsx +20 -0
- package/src/icons/GlobeIcon.tsx +20 -0
- package/src/icons/GraduationCapIcon.tsx +32 -0
- package/src/icons/HandHeartIcon.tsx +38 -0
- package/src/icons/HandshakeIcon.tsx +39 -0
- package/src/icons/HazeIcon.tsx +42 -0
- package/src/icons/HeartHandshakeIcon.tsx +20 -0
- package/src/icons/HourglassIcon.tsx +33 -0
- package/src/icons/IdCardIcon.tsx +44 -0
- package/src/icons/MessageCirclePlusIcon.tsx +32 -0
- package/src/icons/MountainSnowIcon.tsx +26 -0
- package/src/icons/SnowflakeIcon.tsx +86 -0
- package/src/icons/StethoscopeIcon.tsx +34 -0
- package/src/icons/UserRoundIcon.tsx +26 -0
- package/src/icons/WheatIcon.tsx +62 -0
- package/src/icons/index.ts +63 -0
- package/src/icons/types.ts +45 -0
- package/src/index.ts +159 -71
- package/src/tokens.ts +135 -15
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import 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
|
-
|
|
13
|
-
import {
|
|
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
|
|
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 }) => [
|
|
91
|
+
style={({ pressed }) => [
|
|
92
|
+
itemStyles.actionBtn,
|
|
93
|
+
pressed && itemStyles.pressed,
|
|
94
|
+
]}
|
|
85
95
|
>
|
|
86
|
-
<Ionicons
|
|
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 }) => [
|
|
108
|
+
style={({ pressed }) => [
|
|
109
|
+
itemStyles.actionBtn,
|
|
110
|
+
pressed && itemStyles.pressed,
|
|
111
|
+
]}
|
|
95
112
|
>
|
|
96
|
-
<Ionicons
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
40
|
+
label?: string;
|
|
48
41
|
/**
|
|
49
42
|
* `completed` → full green bar
|
|
50
|
-
* `active` →
|
|
51
|
-
* `pending` →
|
|
43
|
+
* `active` → full green bar (current step — same 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
|
-
/**
|
|
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
|
-
|
|
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 =
|
|
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
|
+
});
|