@compsych/mobile-ui 1.0.9 → 1.0.11
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} +42 -14
- 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 +40 -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} +92 -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,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
|
-
|
|
12
|
-
import {
|
|
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
|
|
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 }) => [
|
|
91
|
+
style={({ pressed }) => [
|
|
92
|
+
itemStyles.actionBtn,
|
|
93
|
+
pressed && itemStyles.pressed,
|
|
94
|
+
]}
|
|
84
95
|
>
|
|
85
|
-
<Ionicons
|
|
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 }) => [
|
|
108
|
+
style={({ pressed }) => [
|
|
109
|
+
itemStyles.actionBtn,
|
|
110
|
+
pressed && itemStyles.pressed,
|
|
111
|
+
]}
|
|
94
112
|
>
|
|
95
|
-
<Ionicons
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
});
|