@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,12 +1,17 @@
1
- import { Ionicons } from '@expo/vector-icons';
2
1
  import React from 'react';
2
+
3
3
  import {
4
4
  Pressable,
5
+ StyleProp,
5
6
  StyleSheet,
6
7
  Text,
7
8
  View,
9
+ ViewStyle,
8
10
  } from 'react-native';
9
- import { sys } from './tokens';
11
+
12
+ import { Ionicons } from '@expo/vector-icons';
13
+
14
+ import { sys } from '../../tokens';
10
15
 
11
16
  export type EmptyStateStyle = 'icon' | 'illustration';
12
17
  export type EmptyStateViewport = 'desktop' | 'mobile';
@@ -29,6 +34,7 @@ export interface EmptyStateProps {
29
34
  onAction?: () => void;
30
35
  /** Whether the action button is shown (default: true when actionLabel is set) */
31
36
  showAction?: boolean;
37
+ containerStyle?: StyleProp<ViewStyle>;
32
38
  }
33
39
 
34
40
  const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
@@ -79,7 +85,7 @@ const ILLUS_H = 157;
79
85
  const SCALE = ILLUS_W / 398; // ≈ 0.703
80
86
 
81
87
  const CARD_W = Math.round(160 * SCALE); // 112
82
- const CARD_H = Math.round(98 * SCALE); // 69
88
+ const CARD_H = Math.round(98 * SCALE); // 69
83
89
 
84
90
  function scaledPos(left: number, top: number) {
85
91
  return {
@@ -120,7 +126,8 @@ function EmptyIllustration() {
120
126
  width: CARD_W,
121
127
  height: CARD_H,
122
128
  borderColor: cr.outline.sysOutlineVariant,
123
- backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
129
+ backgroundColor:
130
+ cr.surface.surfaceContainer.sysSurfaceContainerLowest,
124
131
  },
125
132
  styles.cardFaded,
126
133
  ]}
@@ -138,7 +145,8 @@ function EmptyIllustration() {
138
145
  width: CARD_W,
139
146
  height: CARD_H,
140
147
  borderColor: cr.accent.primary.sysPrimary,
141
- backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
148
+ backgroundColor:
149
+ cr.surface.surfaceContainer.sysSurfaceContainerLowest,
142
150
  },
143
151
  ]}
144
152
  >
@@ -190,12 +198,13 @@ export function EmptyState({
190
198
  actionLabel,
191
199
  onAction,
192
200
  showAction,
201
+ containerStyle,
193
202
  }: EmptyStateProps) {
194
203
  const vp = VIEWPORT[viewport];
195
204
  const hasAction = showAction !== undefined ? showAction : !!actionLabel;
196
205
 
197
206
  return (
198
- <View style={styles.root}>
207
+ <View style={[styles.root, containerStyle]}>
199
208
  {/* ── Graphic ───────────────────────────────────────────────────────── */}
200
209
  {style === 'illustration' ? (
201
210
  <EmptyIllustration />
@@ -206,7 +215,8 @@ export function EmptyState({
206
215
  {
207
216
  width: vp.iconCircle,
208
217
  height: vp.iconCircle,
209
- backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLow,
218
+ backgroundColor:
219
+ cr.surface.surfaceContainer.sysSurfaceContainerLow,
210
220
  },
211
221
  ]}
212
222
  >
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+
3
+ import { render } from '@testing-library/react-native';
4
+
5
+ import { HeaderText } from './index';
6
+
7
+ describe('HeaderText', () => {
8
+ it('renders children', () => {
9
+ const { getByText } = render(<HeaderText>Page Title</HeaderText>);
10
+ expect(getByText('Page Title')).toBeTruthy();
11
+ });
12
+
13
+ it.each([
14
+ 'large',
15
+ 'medium',
16
+ 'small',
17
+ 'headlineLarge',
18
+ 'headlineMedium',
19
+ 'headlineSmall',
20
+ 'titleLarge',
21
+ 'titleMedium',
22
+ 'titleSmall',
23
+ ] as const)('renders variant "%s" without crashing', (variant) => {
24
+ const { getByText } = render(
25
+ <HeaderText variant={variant}>Title</HeaderText>,
26
+ );
27
+ expect(getByText('Title')).toBeTruthy();
28
+ });
29
+
30
+ it('applies custom color', () => {
31
+ const { getByText } = render(
32
+ <HeaderText color="#0000ff">Blue Title</HeaderText>,
33
+ );
34
+ expect(getByText('Blue Title')).toBeTruthy();
35
+ });
36
+
37
+ it('renders emphasized text', () => {
38
+ const { getByText } = render(
39
+ <HeaderText emphasized>Emphasized</HeaderText>,
40
+ );
41
+ expect(getByText('Emphasized')).toBeTruthy();
42
+ });
43
+ });
@@ -1,8 +1,10 @@
1
1
  import React from 'react';
2
+
2
3
  import { Text, type TextProps } from 'react-native';
4
+
3
5
  import { useFonts } from 'expo-font';
4
6
 
5
- import { sys } from './tokens';
7
+ import { sys } from '../../tokens';
6
8
 
7
9
  export type HeaderVariant =
8
10
  | 'large'
@@ -49,13 +51,15 @@ export function HeaderText({
49
51
  ...rest
50
52
  }: HeaderTextProps) {
51
53
  const [fontsLoaded] = useFonts({
52
- GoogleSans_400Regular: require('./fonts/GoogleSans_400Regular.ttf'),
53
- GoogleSans_500Medium: require('./fonts/GoogleSans_500Medium.ttf'),
54
- GoogleSans_600SemiBold: require('./fonts/GoogleSans_600SemiBold.ttf'),
54
+ GoogleSans_400Regular: require('../../fonts/GoogleSans_400Regular.ttf'),
55
+ GoogleSans_500Medium: require('../../fonts/GoogleSans_500Medium.ttf'),
56
+ GoogleSans_600SemiBold: require('../../fonts/GoogleSans_600SemiBold.ttf'),
55
57
  });
56
58
 
57
59
  const token = ts[VARIANT_TOKEN_KEY[variant]];
58
- const weight = emphasized ? token.sysFontWeightEmphasized : token.sysFontWeight;
60
+ const weight = emphasized
61
+ ? token.sysFontWeightEmphasized
62
+ : token.sysFontWeight;
59
63
 
60
64
  return (
61
65
  <Text
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+
3
+ import { fireEvent, render } from '@testing-library/react-native';
4
+
5
+ import { Input } from './index';
6
+
7
+ describe('Input', () => {
8
+ it('renders label', () => {
9
+ const { getByText } = render(<Input label="Email" />);
10
+ expect(getByText('Email')).toBeTruthy();
11
+ });
12
+
13
+ it('renders helper text', () => {
14
+ const { getByText } = render(
15
+ <Input label="Email" helperText="Enter your work email" />,
16
+ );
17
+ expect(getByText('Enter your work email')).toBeTruthy();
18
+ });
19
+
20
+ it('renders error text when invalid', () => {
21
+ const { getByText } = render(
22
+ <Input label="Email" invalid errorText="Invalid email address" />,
23
+ );
24
+ expect(getByText('Invalid email address')).toBeTruthy();
25
+ });
26
+
27
+ it('calls onChangeText when text changes', () => {
28
+ const onChangeText = jest.fn();
29
+ const { getByDisplayValue } = render(
30
+ <Input label="Name" value="" onChangeText={onChangeText} />,
31
+ );
32
+ fireEvent.changeText(getByDisplayValue(''), 'John');
33
+ expect(onChangeText).toHaveBeenCalledWith('John');
34
+ });
35
+
36
+ it.each(['sm', 'md', 'lg'] as const)(
37
+ 'renders size "%s" without crashing',
38
+ (size) => {
39
+ const { getByText } = render(<Input label="Field" size={size} />);
40
+ expect(getByText('Field')).toBeTruthy();
41
+ },
42
+ );
43
+ });
@@ -1,12 +1,14 @@
1
1
  import React, { useState } from 'react';
2
+
2
3
  import {
3
- TextInput,
4
- TextInputProps,
5
4
  StyleSheet,
6
5
  Text,
6
+ TextInput,
7
+ TextInputProps,
7
8
  View,
8
9
  } from 'react-native';
9
- import { sys } from './tokens';
10
+
11
+ import { sys } from '../../tokens';
10
12
 
11
13
  export type InputSize = 'sm' | 'md' | 'lg';
12
14
 
@@ -78,15 +80,16 @@ export function Input({
78
80
  const hasError = invalid || !!errorText;
79
81
 
80
82
  // ── Border ──────────────────────────────────────────────────────────────
81
- const borderWidth = isFocused || hasError
82
- ? dim.borderWidth.sysStrokeMedium
83
- : dim.borderWidth.sysStrokeThin;
83
+ const borderWidth =
84
+ isFocused || hasError
85
+ ? dim.borderWidth.sysStrokeMedium
86
+ : dim.borderWidth.sysStrokeThin;
84
87
 
85
88
  const borderColor = hasError
86
89
  ? cr.error.sysError
87
90
  : isFocused
88
- ? cr.accent.primary.sysPrimary
89
- : cr.outline.sysOutlineVariant;
91
+ ? cr.accent.primary.sysPrimary
92
+ : cr.outline.sysOutlineVariant;
90
93
 
91
94
  // ── Text colours ─────────────────────────────────────────────────────────
92
95
  const textColor = cr.surface.surface.sysOnSurface;
@@ -136,7 +139,8 @@ export function Input({
136
139
  borderRadius: s.radius,
137
140
  borderWidth,
138
141
  borderColor,
139
- backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
142
+ backgroundColor:
143
+ cr.surface.surfaceContainer.sysSurfaceContainerLowest,
140
144
  flexDirection: 'row',
141
145
  alignItems: 'center',
142
146
  paddingHorizontal: s.paddingH,
@@ -147,7 +151,15 @@ export function Input({
147
151
  >
148
152
  {/* Leading icon */}
149
153
  {leadingIcon && (
150
- <View style={{ width: s.iconSize, height: s.iconSize, alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
154
+ <View
155
+ style={{
156
+ width: s.iconSize,
157
+ height: s.iconSize,
158
+ alignItems: 'center',
159
+ justifyContent: 'center',
160
+ flexShrink: 0,
161
+ }}
162
+ >
151
163
  {leadingIcon}
152
164
  </View>
153
165
  )}
@@ -182,7 +194,15 @@ export function Input({
182
194
 
183
195
  {/* Trailing icon */}
184
196
  {trailingIcon && (
185
- <View style={{ width: s.iconSize, height: s.iconSize, alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
197
+ <View
198
+ style={{
199
+ width: s.iconSize,
200
+ height: s.iconSize,
201
+ alignItems: 'center',
202
+ justifyContent: 'center',
203
+ flexShrink: 0,
204
+ }}
205
+ >
186
206
  {trailingIcon}
187
207
  </View>
188
208
  )}
@@ -0,0 +1,59 @@
1
+ import React from 'react';
2
+
3
+ import { fireEvent, render } from '@testing-library/react-native';
4
+
5
+ import { List, ListItem } from './index';
6
+
7
+ const items = [{ label: 'Item one' }, { label: 'Item two' }];
8
+
9
+ describe('List', () => {
10
+ it('renders list items from items prop', () => {
11
+ const { getByText } = render(<List items={items} />);
12
+ expect(getByText('Item one')).toBeTruthy();
13
+ expect(getByText('Item two')).toBeTruthy();
14
+ });
15
+
16
+ it('renders items with onPress callbacks', () => {
17
+ const onPress = jest.fn();
18
+ const { getByText } = render(
19
+ <List items={[{ label: 'Tappable', onPress }]} />,
20
+ );
21
+ fireEvent.press(getByText('Tappable'));
22
+ expect(onPress).toHaveBeenCalledTimes(1);
23
+ });
24
+ });
25
+
26
+ describe('ListItem', () => {
27
+ it('renders label', () => {
28
+ const { getByText } = render(<ListItem label="Settings" />);
29
+ expect(getByText('Settings')).toBeTruthy();
30
+ });
31
+
32
+ it('renders subLabel when type is detailed', () => {
33
+ const { getByText } = render(
34
+ <ListItem
35
+ type="detailed"
36
+ label="Notifications"
37
+ subLabel="Manage your alerts"
38
+ />,
39
+ );
40
+ expect(getByText('Manage your alerts')).toBeTruthy();
41
+ });
42
+
43
+ it('calls onPress when tapped', () => {
44
+ const onPress = jest.fn();
45
+ const { getByRole } = render(
46
+ <ListItem label="Tappable" onPress={onPress} />,
47
+ );
48
+ fireEvent.press(getByRole('button'));
49
+ expect(onPress).toHaveBeenCalledTimes(1);
50
+ });
51
+
52
+ it.each(['simple', 'detailed'] as const)(
53
+ 'renders type "%s" without crashing',
54
+ (type) => {
55
+ const { getByText } = render(<ListItem label="Item" type={type} />);
56
+ expect(getByText('Item')).toBeTruthy();
57
+ },
58
+ );
59
+ });
@@ -1,8 +1,17 @@
1
1
  import React from 'react';
2
- import { Pressable, StyleSheet, View } from 'react-native';
2
+
3
+ import {
4
+ Pressable,
5
+ StyleProp,
6
+ StyleSheet,
7
+ View,
8
+ ViewStyle,
9
+ } from 'react-native';
10
+
3
11
  import { Ionicons } from '@expo/vector-icons';
4
- import { sys } from './tokens';
5
- import { BodyText } from './BodyText';
12
+
13
+ import { sys } from '../../tokens';
14
+ import { BodyText } from '../BodyText';
6
15
 
7
16
  export type ListItemType = 'simple' | 'detailed';
8
17
 
@@ -19,6 +28,7 @@ export interface ListItemProps {
19
28
  export interface ListProps {
20
29
  items: Omit<ListItemProps, 'showDivider'>[];
21
30
  type?: ListItemType;
31
+ style?: StyleProp<ViewStyle>;
22
32
  }
23
33
 
24
34
  const { colorRoles: cr, dimensions: dim } = sys;
@@ -43,7 +53,10 @@ export function ListItem({
43
53
  <View
44
54
  style={[
45
55
  styles.iconBadge,
46
- { backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLow },
56
+ {
57
+ backgroundColor:
58
+ cr.surface.surfaceContainer.sysSurfaceContainerLow,
59
+ },
47
60
  ]}
48
61
  >
49
62
  {icon}
@@ -62,7 +75,10 @@ export function ListItem({
62
75
  {label}
63
76
  </BodyText>
64
77
  {isDetailed && subLabel && (
65
- <BodyText variant="small" color={cr.surface.surface.sysOnSurfaceVariant}>
78
+ <BodyText
79
+ variant="small"
80
+ color={cr.surface.surface.sysOnSurfaceVariant}
81
+ >
66
82
  {subLabel}
67
83
  </BodyText>
68
84
  )}
@@ -132,9 +148,9 @@ export function ListItem({
132
148
 
133
149
  // ── List ──────────────────────────────────────────────────────────────────────
134
150
 
135
- export function List({ items, type = 'simple' }: ListProps) {
151
+ export function List({ items, type = 'simple', style }: ListProps) {
136
152
  return (
137
- <View>
153
+ <View style={style}>
138
154
  {items.map((item, index) => (
139
155
  <ListItem
140
156
  key={index}
@@ -0,0 +1,56 @@
1
+ import React from 'react';
2
+
3
+ import { fireEvent, render } from '@testing-library/react-native';
4
+
5
+ import { Pagination } from './index';
6
+
7
+ describe('Pagination', () => {
8
+ it('renders current and total page info', () => {
9
+ const { getByText } = render(
10
+ <Pagination currentPage={2} totalPages={10} onPageChange={jest.fn()} />,
11
+ );
12
+ expect(getByText('2')).toBeTruthy();
13
+ expect(getByText('10')).toBeTruthy();
14
+ });
15
+
16
+ it('calls onPageChange with next page when next is pressed', () => {
17
+ const onPageChange = jest.fn();
18
+ const { getByLabelText } = render(
19
+ <Pagination
20
+ currentPage={3}
21
+ totalPages={10}
22
+ onPageChange={onPageChange}
23
+ />,
24
+ );
25
+ fireEvent.press(getByLabelText('Next page'));
26
+ expect(onPageChange).toHaveBeenCalledWith(4);
27
+ });
28
+
29
+ it('calls onPageChange with previous page when prev is pressed', () => {
30
+ const onPageChange = jest.fn();
31
+ const { getByLabelText } = render(
32
+ <Pagination
33
+ currentPage={3}
34
+ totalPages={10}
35
+ onPageChange={onPageChange}
36
+ />,
37
+ );
38
+ fireEvent.press(getByLabelText('Previous page'));
39
+ expect(onPageChange).toHaveBeenCalledWith(2);
40
+ });
41
+
42
+ it.each(['sm', 'lg'] as const)(
43
+ 'renders size "%s" without crashing',
44
+ (size) => {
45
+ const { toJSON } = render(
46
+ <Pagination
47
+ currentPage={1}
48
+ totalPages={5}
49
+ onPageChange={jest.fn()}
50
+ size={size}
51
+ />,
52
+ );
53
+ expect(toJSON()).toBeTruthy();
54
+ },
55
+ );
56
+ });
@@ -1,7 +1,17 @@
1
- import { Ionicons } from '@expo/vector-icons';
2
1
  import React from 'react';
3
- import { Pressable, StyleSheet, Text, View } from 'react-native';
4
- import { sys } from './tokens';
2
+
3
+ import {
4
+ Pressable,
5
+ StyleProp,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ ViewStyle,
10
+ } from 'react-native';
11
+
12
+ import { Ionicons } from '@expo/vector-icons';
13
+
14
+ import { sys } from '../../tokens';
5
15
 
6
16
  export type PaginationSize = 'sm' | 'lg';
7
17
 
@@ -29,6 +39,7 @@ export interface PaginationProps {
29
39
  siblingCount?: number;
30
40
  /** On mobile/compact layouts — renders only prev + next arrows (no page numbers) */
31
41
  compact?: boolean;
42
+ style?: StyleProp<ViewStyle>;
32
43
  }
33
44
 
34
45
  const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
@@ -149,9 +160,10 @@ function PageItem({ s, page, isActive, onPress }: PageItemProps) {
149
160
  borderColor,
150
161
  alignItems: 'center',
151
162
  justifyContent: 'center',
152
- backgroundColor: pressed && !isActive
153
- ? cr.transparent.neutral.sysBlack10
154
- : 'transparent',
163
+ backgroundColor:
164
+ pressed && !isActive
165
+ ? cr.transparent.neutral.sysBlack10
166
+ : 'transparent',
155
167
  }}
156
168
  >
157
169
  <Text
@@ -211,8 +223,7 @@ interface NavButtonProps {
211
223
  }
212
224
 
213
225
  function NavButton({ s, direction, disabled, onPress }: NavButtonProps) {
214
- const iconName =
215
- direction === 'prev' ? 'chevron-back' : 'chevron-forward';
226
+ const iconName = direction === 'prev' ? 'chevron-back' : 'chevron-forward';
216
227
 
217
228
  return (
218
229
  <Pressable
@@ -230,9 +241,10 @@ function NavButton({ s, direction, disabled, onPress }: NavButtonProps) {
230
241
  borderRadius: s.itemSize / 2,
231
242
  alignItems: 'center',
232
243
  justifyContent: 'center',
233
- backgroundColor: pressed && !disabled
234
- ? cr.transparent.neutral.sysBlack10
235
- : 'transparent',
244
+ backgroundColor:
245
+ pressed && !disabled
246
+ ? cr.transparent.neutral.sysBlack10
247
+ : 'transparent',
236
248
  }}
237
249
  >
238
250
  <Ionicons
@@ -255,6 +267,7 @@ export function Pagination({
255
267
  onPageChange,
256
268
  siblingCount = 1,
257
269
  compact = false,
270
+ style,
258
271
  }: PaginationProps) {
259
272
  const s = SIZE_TOKENS[size];
260
273
  const pages = buildPageRange(totalPages, currentPage, siblingCount);
@@ -322,10 +335,12 @@ export function Pagination({
322
335
  {
323
336
  paddingHorizontal: s.pillPaddingH,
324
337
  paddingVertical: s.pillPaddingV,
325
- backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
338
+ backgroundColor:
339
+ cr.surface.surfaceContainer.sysSurfaceContainerLowest,
326
340
  borderColor: cr.outline.sysOutline,
327
341
  borderWidth: dim.borderWidth.sysStrokeThin,
328
342
  },
343
+ style,
329
344
  ]}
330
345
  >
331
346
  {content}
@@ -333,7 +348,8 @@ export function Pagination({
333
348
  );
334
349
  }
335
350
 
336
- return content;
351
+ // sm — wrap content in a View so style can be applied
352
+ return <View style={style}>{content}</View>;
337
353
  }
338
354
 
339
355
  const styles = StyleSheet.create({
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+
3
+ import { fireEvent, render } from '@testing-library/react-native';
4
+
5
+ import { PlanCard } from './index';
6
+
7
+ const items = [
8
+ { id: '1', title: 'Session 1', subtitle: 'Introduction', checked: false },
9
+ { id: '2', title: 'Session 2', subtitle: 'Deep work', checked: true },
10
+ ];
11
+
12
+ describe('PlanCard', () => {
13
+ it('renders title', () => {
14
+ const { getByText } = render(<PlanCard title="My Plan" />);
15
+ expect(getByText('My Plan')).toBeTruthy();
16
+ });
17
+
18
+ it('renders tag when provided', () => {
19
+ const { getByText } = render(<PlanCard title="Plan" tag="Recommended" />);
20
+ expect(getByText('Recommended')).toBeTruthy();
21
+ });
22
+
23
+ it('renders items when expanded', () => {
24
+ const { getByText } = render(
25
+ <PlanCard title="Plan" items={items} expanded />,
26
+ );
27
+ expect(getByText('Session 1')).toBeTruthy();
28
+ expect(getByText('Session 2')).toBeTruthy();
29
+ });
30
+
31
+ it('calls onToggle when header is pressed', () => {
32
+ const onToggle = jest.fn();
33
+ const { getByLabelText } = render(
34
+ <PlanCard title="Plan" onToggle={onToggle} />,
35
+ );
36
+ fireEvent.press(getByLabelText('Expand'));
37
+ expect(onToggle).toHaveBeenCalledTimes(1);
38
+ });
39
+
40
+ it('renders without items', () => {
41
+ const { toJSON } = render(<PlanCard title="Empty Plan" />);
42
+ expect(toJSON()).toBeTruthy();
43
+ });
44
+ });