@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
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+
3
+ import { fireEvent, render } from '@testing-library/react-native';
4
+
5
+ import { Breadcrumb } from './index';
6
+
7
+ const items = [
8
+ { label: 'Home', onPress: jest.fn() },
9
+ { label: 'Settings', onPress: jest.fn() },
10
+ { label: 'Profile' },
11
+ ];
12
+
13
+ describe('Breadcrumb', () => {
14
+ it('renders all item labels', () => {
15
+ const { getByText } = render(<Breadcrumb items={items} />);
16
+ expect(getByText('Home')).toBeTruthy();
17
+ expect(getByText('Settings')).toBeTruthy();
18
+ expect(getByText('Profile')).toBeTruthy();
19
+ });
20
+
21
+ it('calls onPress for a tappable item', () => {
22
+ const onPress = jest.fn();
23
+ const { getByText } = render(
24
+ <Breadcrumb items={[{ label: 'Home', onPress }, { label: 'Current' }]} />,
25
+ );
26
+ fireEvent.press(getByText('Home'));
27
+ expect(onPress).toHaveBeenCalledTimes(1);
28
+ });
29
+
30
+ it.each(['sm', 'lg'] as const)(
31
+ 'renders size "%s" without crashing',
32
+ (size) => {
33
+ const { getByText } = render(<Breadcrumb items={items} size={size} />);
34
+ expect(getByText('Home')).toBeTruthy();
35
+ },
36
+ );
37
+ });
@@ -1,13 +1,18 @@
1
- import { Ionicons } from '@expo/vector-icons';
2
1
  import React from 'react';
2
+
3
3
  import {
4
4
  Pressable,
5
5
  ScrollView,
6
+ StyleProp,
6
7
  StyleSheet,
7
8
  Text,
8
9
  View,
10
+ ViewStyle,
9
11
  } from 'react-native';
10
- import { sys } from './tokens';
12
+
13
+ import { Ionicons } from '@expo/vector-icons';
14
+
15
+ import { sys } from '../../tokens';
11
16
 
12
17
  export type BreadcrumbSize = 'sm' | 'lg';
13
18
 
@@ -30,6 +35,7 @@ export interface BreadcrumbItem {
30
35
  export interface BreadcrumbProps {
31
36
  items: BreadcrumbItem[];
32
37
  size?: BreadcrumbSize;
38
+ style?: StyleProp<ViewStyle>;
33
39
  }
34
40
 
35
41
  const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
@@ -55,7 +61,7 @@ const SIZE_TOKENS = {
55
61
  },
56
62
  };
57
63
 
58
- export function Breadcrumb({ items, size = 'lg' }: BreadcrumbProps) {
64
+ export function Breadcrumb({ items, size = 'lg', style }: BreadcrumbProps) {
59
65
  const s = SIZE_TOKENS[size];
60
66
 
61
67
  return (
@@ -64,6 +70,7 @@ export function Breadcrumb({ items, size = 'lg' }: BreadcrumbProps) {
64
70
  horizontal
65
71
  showsHorizontalScrollIndicator={false}
66
72
  contentContainerStyle={styles.row}
73
+ style={style}
67
74
  >
68
75
  {items.map((item, index) => {
69
76
  const isLast = index === items.length - 1;
@@ -81,7 +88,9 @@ export function Breadcrumb({ items, size = 'lg' }: BreadcrumbProps) {
81
88
  styles.content,
82
89
  {
83
90
  paddingHorizontal: s.paddingH,
84
- paddingVertical: item.isHome ? dim.spacing.padding.sysPadding2 : s.paddingV,
91
+ paddingVertical: item.isHome
92
+ ? dim.spacing.padding.sysPadding2
93
+ : s.paddingV,
85
94
  gap: item.isHome ? 0 : dim.spacing.padding.sysPadding4,
86
95
  },
87
96
  item.disabled && styles.disabled,
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+
3
+ import { fireEvent, render } from '@testing-library/react-native';
4
+
5
+ import { Button } from './index';
6
+
7
+ describe('Button', () => {
8
+ it('renders label', () => {
9
+ const { getByText } = render(<Button label="Submit" />);
10
+ expect(getByText('Submit')).toBeTruthy();
11
+ });
12
+
13
+ it('calls onPress when tapped', () => {
14
+ const onPress = jest.fn();
15
+ const { getByRole } = render(<Button label="Submit" onPress={onPress} />);
16
+ fireEvent.press(getByRole('button'));
17
+ expect(onPress).toHaveBeenCalledTimes(1);
18
+ });
19
+
20
+ it('is accessible as disabled when disabled prop is set', () => {
21
+ const { getByRole } = render(<Button label="Submit" disabled />);
22
+ expect(getByRole('button').props.accessibilityState.disabled).toBe(true);
23
+ });
24
+
25
+ it.each(['filled', 'tonal', 'outlined', 'elevated', 'text'] as const)(
26
+ 'renders variant "%s" without crashing',
27
+ (variant) => {
28
+ const { getByText } = render(<Button label="Button" variant={variant} />);
29
+ expect(getByText('Button')).toBeTruthy();
30
+ },
31
+ );
32
+
33
+ it.each(['sm', 'md', 'lg', 'xl'] as const)(
34
+ 'renders size "%s" without crashing',
35
+ (size) => {
36
+ const { getByText } = render(<Button label="Button" size={size} />);
37
+ expect(getByText('Button')).toBeTruthy();
38
+ },
39
+ );
40
+
41
+ it('renders in full width mode', () => {
42
+ const { getByText } = render(<Button label="Full" fullWidth />);
43
+ expect(getByText('Full')).toBeTruthy();
44
+ });
45
+ });
@@ -1,13 +1,15 @@
1
1
  import React from 'react';
2
+
2
3
  import {
3
4
  ActivityIndicator,
4
5
  Pressable,
6
+ type PressableProps,
5
7
  StyleSheet,
6
8
  Text,
7
9
  View,
8
- type PressableProps,
9
10
  } from 'react-native';
10
- import { sys } from './tokens';
11
+
12
+ import { sys } from '../../tokens';
11
13
 
12
14
  export type ButtonVariant =
13
15
  | 'filled'
@@ -20,7 +22,10 @@ export type ButtonVariant =
20
22
 
21
23
  export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
22
24
 
23
- export interface ButtonProps extends Omit<PressableProps, 'children' | 'style'> {
25
+ export interface ButtonProps extends Omit<
26
+ PressableProps,
27
+ 'children' | 'style'
28
+ > {
24
29
  variant?: ButtonVariant;
25
30
  size?: ButtonSize;
26
31
  label: string;
@@ -153,18 +158,18 @@ export function Button({
153
158
  ...(iconOnly
154
159
  ? { width: s.height, height: s.height }
155
160
  : variant === 'text'
156
- ? {
157
- // Text variant is content-height (no fixed container). Figma:
158
- // sm=26px, md=28px, lg=31px, xl=32px — each resolves to
159
- // lineHeight + 2×paddingV, not the standard fixed height.
160
- paddingHorizontal: 0,
161
- paddingVertical: s.paddingV,
162
- }
163
- : {
164
- height: s.height,
165
- paddingHorizontal: paddingH,
166
- paddingVertical: s.paddingV,
167
- }),
161
+ ? {
162
+ // Text variant is content-height (no fixed container). Figma:
163
+ // sm=26px, md=28px, lg=31px, xl=32px — each resolves to
164
+ // lineHeight + 2×paddingV, not the standard fixed height.
165
+ paddingHorizontal: 0,
166
+ paddingVertical: s.paddingV,
167
+ }
168
+ : {
169
+ height: s.height,
170
+ paddingHorizontal: paddingH,
171
+ paddingVertical: s.paddingV,
172
+ }),
168
173
  ...(fullWidth && !iconOnly ? { alignSelf: 'stretch' } : {}),
169
174
  opacity: isDisabled ? 0.48 : pressed ? 0.82 : 1,
170
175
  ...(variant === 'elevated'
@@ -183,7 +188,14 @@ export function Button({
183
188
  {loading ? (
184
189
  <ActivityIndicator size={s.iconSize} color={v.label} />
185
190
  ) : leadingIcon ? (
186
- <View style={{ width: s.iconSize, height: s.iconSize, alignItems: 'center', justifyContent: 'center' }}>
191
+ <View
192
+ style={{
193
+ width: s.iconSize,
194
+ height: s.iconSize,
195
+ alignItems: 'center',
196
+ justifyContent: 'center',
197
+ }}
198
+ >
187
199
  {leadingIcon}
188
200
  </View>
189
201
  ) : null}
@@ -204,7 +216,14 @@ export function Button({
204
216
  )}
205
217
 
206
218
  {trailingIcon && (
207
- <View style={{ width: s.iconSize, height: s.iconSize, alignItems: 'center', justifyContent: 'center' }}>
219
+ <View
220
+ style={{
221
+ width: s.iconSize,
222
+ height: s.iconSize,
223
+ alignItems: 'center',
224
+ justifyContent: 'center',
225
+ }}
226
+ >
208
227
  {trailingIcon}
209
228
  </View>
210
229
  )}
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+
3
+ import { fireEvent, render } from '@testing-library/react-native';
4
+
5
+ import { Checkbox } from './index';
6
+
7
+ describe('Checkbox', () => {
8
+ it('renders label', () => {
9
+ const { getByText } = render(<Checkbox label="Accept terms" />);
10
+ expect(getByText('Accept terms')).toBeTruthy();
11
+ });
12
+
13
+ it('renders description when provided', () => {
14
+ const { getByText } = render(
15
+ <Checkbox label="Subscribe" description="Get weekly updates" />,
16
+ );
17
+ expect(getByText('Get weekly updates')).toBeTruthy();
18
+ });
19
+
20
+ it('calls onChange when pressed', () => {
21
+ const onChange = jest.fn();
22
+ const { getByRole } = render(
23
+ <Checkbox label="Check me" checked={false} onChange={onChange} />,
24
+ );
25
+ fireEvent.press(getByRole('checkbox'));
26
+ expect(onChange).toHaveBeenCalledWith(true);
27
+ });
28
+
29
+ it('does not call onChange when disabled', () => {
30
+ const onChange = jest.fn();
31
+ const { getByRole } = render(
32
+ <Checkbox
33
+ label="Disabled"
34
+ checked={false}
35
+ onChange={onChange}
36
+ disabled
37
+ />,
38
+ );
39
+ fireEvent.press(getByRole('checkbox'));
40
+ expect(onChange).not.toHaveBeenCalled();
41
+ });
42
+
43
+ it('renders indeterminate state', () => {
44
+ const { getByRole } = render(
45
+ <Checkbox label="Indeterminate" checked="indeterminate" />,
46
+ );
47
+ expect(getByRole('checkbox')).toBeTruthy();
48
+ });
49
+
50
+ it.each(['sm', 'md'] as const)(
51
+ 'renders size "%s" without crashing',
52
+ (size) => {
53
+ const { getByText } = render(<Checkbox label="Size test" size={size} />);
54
+ expect(getByText('Size test')).toBeTruthy();
55
+ },
56
+ );
57
+ });
@@ -1,7 +1,17 @@
1
- import { Ionicons } from '@expo/vector-icons';
2
1
  import React, { useState } 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 CheckboxSize = 'sm' | 'md';
7
17
  export type CheckboxCheckedState = boolean | 'indeterminate';
@@ -15,13 +25,26 @@ export interface CheckboxProps {
15
25
  description?: string;
16
26
  disabled?: boolean;
17
27
  invalid?: boolean;
28
+ style?: StyleProp<ViewStyle>;
18
29
  }
19
30
 
20
31
  const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
21
32
 
22
33
  const SIZE_TOKENS = {
23
- sm: { box: 20, hitArea: 36, fontSize: ts.bodySmall.sysFontSize, lineHeight: ts.bodySmall.sysLineHeight, iconSize: 13 },
24
- md: { box: 24, hitArea: 40, fontSize: ts.bodyMedium.sysFontSize, lineHeight: ts.bodyMedium.sysLineHeight, iconSize: 16 },
34
+ sm: {
35
+ box: 20,
36
+ hitArea: 36,
37
+ fontSize: ts.bodySmall.sysFontSize,
38
+ lineHeight: ts.bodySmall.sysLineHeight,
39
+ iconSize: 13,
40
+ },
41
+ md: {
42
+ box: 24,
43
+ hitArea: 40,
44
+ fontSize: ts.bodyMedium.sysFontSize,
45
+ lineHeight: ts.bodyMedium.sysLineHeight,
46
+ iconSize: 16,
47
+ },
25
48
  };
26
49
 
27
50
  export function Checkbox({
@@ -33,18 +56,20 @@ export function Checkbox({
33
56
  description,
34
57
  disabled = false,
35
58
  invalid = false,
59
+ style,
36
60
  }: CheckboxProps) {
37
- const [internalChecked, setInternalChecked] = useState<boolean>(defaultChecked);
61
+ const [internalChecked, setInternalChecked] =
62
+ useState<boolean>(defaultChecked);
38
63
  const isControlled = checkedProp !== undefined;
39
- const checkedState: CheckboxCheckedState = isControlled ? checkedProp : internalChecked;
64
+ const checkedState: CheckboxCheckedState = isControlled
65
+ ? checkedProp
66
+ : internalChecked;
40
67
 
41
68
  const isChecked = checkedState === true;
42
69
  const isIndeterminate = checkedState === 'indeterminate';
43
70
  const isSelected = isChecked || isIndeterminate;
44
71
 
45
72
  const s = SIZE_TOKENS[size];
46
- const hitPad = (s.hitArea - s.box) / 2;
47
-
48
73
  function handlePress() {
49
74
  if (disabled) return;
50
75
  // Indeterminate → true on activation (matches HTML spec)
@@ -56,8 +81,8 @@ export function Checkbox({
56
81
  const borderColor = invalid
57
82
  ? cr.error.sysError
58
83
  : isSelected
59
- ? 'transparent'
60
- : cr.outline.sysOutlineFixed;
84
+ ? 'transparent'
85
+ : cr.outline.sysOutlineFixed;
61
86
 
62
87
  const boxBg = isSelected
63
88
  ? invalid
@@ -74,10 +99,7 @@ export function Checkbox({
74
99
  checked: isIndeterminate ? 'mixed' : isChecked,
75
100
  disabled,
76
101
  }}
77
- style={({ pressed }) => [
78
- styles.root,
79
- disabled && styles.disabled,
80
- ]}
102
+ style={() => [styles.root, disabled && styles.disabled, style]}
81
103
  >
82
104
  {({ pressed }) => (
83
105
  <>
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+
3
+ import { fireEvent, render } from '@testing-library/react-native';
4
+
5
+ import { Chip } from './index';
6
+
7
+ describe('Chip', () => {
8
+ it('renders label', () => {
9
+ const { getByText } = render(<Chip label="Filter" />);
10
+ expect(getByText('Filter')).toBeTruthy();
11
+ });
12
+
13
+ it('calls onPress when tapped', () => {
14
+ const onPress = jest.fn();
15
+ const { getByText } = render(<Chip label="Filter" onPress={onPress} />);
16
+ fireEvent.press(getByText('Filter'));
17
+ expect(onPress).toHaveBeenCalledTimes(1);
18
+ });
19
+
20
+ it('renders selected state', () => {
21
+ const { toJSON } = render(<Chip label="Selected" selected />);
22
+ expect(toJSON()).toBeTruthy();
23
+ });
24
+
25
+ it.each(['sm', 'md', 'lg', 'xl'] as const)(
26
+ 'renders size "%s" without crashing',
27
+ (size) => {
28
+ const { getByText } = render(<Chip label="Chip" size={size} />);
29
+ expect(getByText('Chip')).toBeTruthy();
30
+ },
31
+ );
32
+
33
+ it.each(['neutral', 'informative', 'positive', 'danger', 'warning'] as const)(
34
+ 'renders usage "%s" without crashing',
35
+ (usage) => {
36
+ const { getByText } = render(<Chip label="Chip" usage={usage} />);
37
+ expect(getByText('Chip')).toBeTruthy();
38
+ },
39
+ );
40
+ });
@@ -1,15 +1,25 @@
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 ChipSize = 'sm' | 'md' | 'lg' | 'xl';
12
- export type ChipUsage = 'neutral' | 'informative' | 'positive' | 'danger' | 'warning';
17
+ export type ChipUsage =
18
+ | 'neutral'
19
+ | 'informative'
20
+ | 'positive'
21
+ | 'danger'
22
+ | 'warning';
13
23
 
14
24
  export interface ChipProps {
15
25
  label: string;
@@ -22,6 +32,7 @@ export interface ChipProps {
22
32
  onDismiss?: () => void;
23
33
  /** Renders a small count badge after the label */
24
34
  badge?: number | string;
35
+ style?: StyleProp<ViewStyle>;
25
36
  }
26
37
 
27
38
  const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
@@ -163,6 +174,7 @@ export function Chip({
163
174
  dismissible = false,
164
175
  onDismiss,
165
176
  badge,
177
+ style,
166
178
  }: ChipProps) {
167
179
  const s = SIZE_TOKENS[size];
168
180
  const c = getUsageColors(usage);
@@ -182,6 +194,7 @@ export function Chip({
182
194
  backgroundColor: c.bg,
183
195
  },
184
196
  c.elevated && styles.elevated,
197
+ style,
185
198
  ]}
186
199
  >
187
200
  {/* Leading icon */}
@@ -260,11 +273,7 @@ export function Chip({
260
273
  overflow: 'hidden',
261
274
  })}
262
275
  >
263
- <Ionicons
264
- name="close"
265
- size={s.closeIconSize}
266
- color={c.text}
267
- />
276
+ <Ionicons name="close" size={s.closeIconSize} color={c.text} />
268
277
  </Pressable>
269
278
  )}
270
279
  </View>
@@ -282,7 +291,7 @@ const styles = StyleSheet.create({
282
291
  elevated: {
283
292
  shadowColor: '#000',
284
293
  shadowOffset: { width: 0, height: 2 },
285
- shadowOpacity: 0.10,
294
+ shadowOpacity: 0.1,
286
295
  shadowRadius: 8,
287
296
  elevation: 2,
288
297
  // overflow must be visible for shadow on iOS — override here
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+
3
+ import { render } from '@testing-library/react-native';
4
+
5
+ import { Divider } from './index';
6
+
7
+ describe('Divider', () => {
8
+ it('renders without crashing', () => {
9
+ const { toJSON } = render(<Divider />);
10
+ expect(toJSON()).toBeTruthy();
11
+ });
12
+
13
+ it.each(['full', 'inset', 'middle'] as const)(
14
+ 'renders variant "%s" without crashing',
15
+ (variant) => {
16
+ const { toJSON } = render(<Divider variant={variant} />);
17
+ expect(toJSON()).toBeTruthy();
18
+ },
19
+ );
20
+
21
+ it.each(['thin', 'medium', 'thick'] as const)(
22
+ 'renders weight "%s" without crashing',
23
+ (weight) => {
24
+ const { toJSON } = render(<Divider weight={weight} />);
25
+ expect(toJSON()).toBeTruthy();
26
+ },
27
+ );
28
+ });
@@ -1,6 +1,8 @@
1
1
  import React from 'react';
2
- import { View } from 'react-native';
3
- import { sys } from './tokens';
2
+
3
+ import { StyleProp, View, ViewStyle } from 'react-native';
4
+
5
+ import { sys } from '../../tokens';
4
6
 
5
7
  export type DividerVariant = 'horizontal' | 'vertical';
6
8
  export type DividerWeight = 'thin' | 'thick';
@@ -9,6 +11,7 @@ export interface DividerProps {
9
11
  variant?: DividerVariant;
10
12
  weight?: DividerWeight;
11
13
  dashed?: boolean;
14
+ style?: StyleProp<ViewStyle>;
12
15
  }
13
16
 
14
17
  const { colorRoles: cr, dimensions: dim } = sys;
@@ -17,6 +20,7 @@ export function Divider({
17
20
  variant = 'horizontal',
18
21
  weight = 'thin',
19
22
  dashed = false,
23
+ style,
20
24
  }: DividerProps) {
21
25
  const thickness =
22
26
  weight === 'thin'
@@ -28,15 +32,18 @@ export function Divider({
28
32
  if (variant === 'horizontal') {
29
33
  return (
30
34
  <View
31
- style={{
32
- // Zero height — the line is drawn as a top border
33
- height: 0,
34
- // Stretch to fill parent width
35
- alignSelf: 'stretch',
36
- borderTopWidth: thickness,
37
- borderTopColor: color,
38
- borderStyle: dashed ? 'dashed' : 'solid',
39
- }}
35
+ style={[
36
+ {
37
+ // Zero height — the line is drawn as a top border
38
+ height: 0,
39
+ // Stretch to fill parent width
40
+ alignSelf: 'stretch',
41
+ borderTopWidth: thickness,
42
+ borderTopColor: color,
43
+ borderStyle: dashed ? 'dashed' : 'solid',
44
+ },
45
+ style,
46
+ ]}
40
47
  accessible={false}
41
48
  />
42
49
  );
@@ -45,15 +52,18 @@ export function Divider({
45
52
  // Vertical
46
53
  return (
47
54
  <View
48
- style={{
49
- // Zero width — the line is drawn as a left border
50
- width: 0,
51
- // Stretch to fill parent height
52
- alignSelf: 'stretch',
53
- borderLeftWidth: thickness,
54
- borderLeftColor: color,
55
- borderStyle: dashed ? 'dashed' : 'solid',
56
- }}
55
+ style={[
56
+ {
57
+ // Zero width — the line is drawn as a left border
58
+ width: 0,
59
+ // Stretch to fill parent height
60
+ alignSelf: 'stretch',
61
+ borderLeftWidth: thickness,
62
+ borderLeftColor: color,
63
+ borderStyle: dashed ? 'dashed' : 'solid',
64
+ },
65
+ style,
66
+ ]}
57
67
  accessible={false}
58
68
  />
59
69
  );
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+
3
+ import { fireEvent, render } from '@testing-library/react-native';
4
+
5
+ import { EmptyState } from './index';
6
+
7
+ describe('EmptyState', () => {
8
+ it('renders title and description', () => {
9
+ const { getByText } = render(
10
+ <EmptyState
11
+ title="Nothing here"
12
+ description="Add something to get started"
13
+ />,
14
+ );
15
+ expect(getByText('Nothing here')).toBeTruthy();
16
+ expect(getByText('Add something to get started')).toBeTruthy();
17
+ });
18
+
19
+ it('renders action button and handles press', () => {
20
+ const onAction = jest.fn();
21
+ const { getByText } = render(
22
+ <EmptyState
23
+ title="No results"
24
+ description="Try a different search"
25
+ actionLabel="Clear filters"
26
+ onAction={onAction}
27
+ />,
28
+ );
29
+ fireEvent.press(getByText('Clear filters'));
30
+ expect(onAction).toHaveBeenCalledTimes(1);
31
+ });
32
+
33
+ it.each(['desktop', 'mobile'] as const)(
34
+ 'renders viewport "%s" without crashing',
35
+ (viewport) => {
36
+ const { getByText } = render(
37
+ <EmptyState
38
+ title="Empty"
39
+ description="Nothing to show"
40
+ viewport={viewport}
41
+ />,
42
+ );
43
+ expect(getByText('Empty')).toBeTruthy();
44
+ },
45
+ );
46
+
47
+ it.each(['icon', 'illustration'] as const)(
48
+ 'renders style "%s" without crashing',
49
+ (emptyStyle) => {
50
+ const { getByText } = render(
51
+ <EmptyState
52
+ title="Empty"
53
+ description="Nothing"
54
+ emptyStyle={emptyStyle}
55
+ />,
56
+ );
57
+ expect(getByText('Empty')).toBeTruthy();
58
+ },
59
+ );
60
+ });