@compsych/mobile-ui 1.0.11 → 1.0.14

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 (65) hide show
  1. package/package.json +2 -1
  2. package/src/components/ActionSheet/index.tsx +124 -57
  3. package/src/components/Alert/index.tsx +19 -10
  4. package/src/components/Avatar/index.tsx +95 -92
  5. package/src/components/Badge/index.tsx +39 -36
  6. package/src/components/BodyText/index.tsx +5 -5
  7. package/src/components/Breadcrumb/index.tsx +32 -30
  8. package/src/components/Button/Button.test.tsx +13 -7
  9. package/src/components/Button/index.tsx +114 -92
  10. package/src/components/ChatBubble/ChatBubble.test.tsx +77 -0
  11. package/src/components/ChatBubble/index.tsx +187 -0
  12. package/src/components/ChatInput/ChatInput.test.tsx +90 -0
  13. package/src/components/ChatInput/index.tsx +263 -0
  14. package/src/components/Checkbox/index.tsx +24 -21
  15. package/src/components/Chip/index.tsx +75 -72
  16. package/src/components/Divider/index.tsx +3 -3
  17. package/src/components/EmptyState/index.tsx +72 -55
  18. package/src/components/HeaderText/index.tsx +5 -5
  19. package/src/components/Input/index.tsx +40 -37
  20. package/src/components/List/index.tsx +2 -3
  21. package/src/components/Pagination/index.tsx +56 -43
  22. package/src/components/PlanCard/index.tsx +118 -50
  23. package/src/components/ProgressTracker/index.tsx +44 -26
  24. package/src/components/PromotionCard/index.tsx +217 -212
  25. package/src/components/RadioButton/index.tsx +27 -25
  26. package/src/components/ScreenContainer/ScreenContainer.test.tsx +12 -2
  27. package/src/components/ScreenContainer/index.tsx +21 -10
  28. package/src/components/SegmentedControl/index.tsx +37 -30
  29. package/src/components/SelectionCard/index.tsx +45 -32
  30. package/src/components/ServiceCard/index.tsx +125 -128
  31. package/src/components/Slider/index.tsx +77 -77
  32. package/src/components/Snackbar/index.tsx +69 -57
  33. package/src/components/Switch/index.tsx +13 -12
  34. package/src/components/Tooltip/index.tsx +3 -3
  35. package/src/icons/index.ts +12 -59
  36. package/src/icons/types.ts +7 -23
  37. package/src/index.ts +15 -22
  38. package/src/theme/ThemeContext.ts +11 -0
  39. package/src/theme/ThemeProvider.tsx +40 -0
  40. package/src/theme/__tests__/ThemeProvider.test.tsx +101 -0
  41. package/src/theme/__tests__/defaultTheme.test.ts +36 -0
  42. package/src/theme/__tests__/mergeTheme.test.ts +94 -0
  43. package/src/theme/defaultTheme.ts +15 -0
  44. package/src/theme/index.ts +8 -0
  45. package/src/theme/mergeTheme.ts +47 -0
  46. package/src/theme/types.ts +39 -0
  47. package/src/theme/useTheme.ts +16 -0
  48. package/src/icons/AtomIcon.tsx +0 -32
  49. package/src/icons/BinocularsIcon.tsx +0 -45
  50. package/src/icons/FileChartColumnIncreasingIcon.tsx +0 -44
  51. package/src/icons/FlagIcon.tsx +0 -20
  52. package/src/icons/GlobeIcon.tsx +0 -20
  53. package/src/icons/GraduationCapIcon.tsx +0 -32
  54. package/src/icons/HandHeartIcon.tsx +0 -38
  55. package/src/icons/HandshakeIcon.tsx +0 -39
  56. package/src/icons/HazeIcon.tsx +0 -42
  57. package/src/icons/HeartHandshakeIcon.tsx +0 -20
  58. package/src/icons/HourglassIcon.tsx +0 -33
  59. package/src/icons/IdCardIcon.tsx +0 -44
  60. package/src/icons/MessageCirclePlusIcon.tsx +0 -32
  61. package/src/icons/MountainSnowIcon.tsx +0 -26
  62. package/src/icons/SnowflakeIcon.tsx +0 -86
  63. package/src/icons/StethoscopeIcon.tsx +0 -34
  64. package/src/icons/UserRoundIcon.tsx +0 -26
  65. package/src/icons/WheatIcon.tsx +0 -62
@@ -8,13 +8,14 @@ import {
8
8
  ViewStyle,
9
9
  } from 'react-native';
10
10
 
11
- import { sys } from '../../tokens';
11
+ import { ICON_MAP, type IconName } from '../../icons';
12
+ import { useTheme } from '../../theme';
12
13
  import { BodyText } from '../BodyText';
13
14
 
14
15
  export interface SegmentedControlOption {
15
16
  value: string;
16
17
  label: string;
17
- icon?: React.ReactNode;
18
+ icon?: IconName;
18
19
  disabled?: boolean;
19
20
  }
20
21
 
@@ -26,8 +27,6 @@ export interface SegmentedControlProps {
26
27
  style?: StyleProp<ViewStyle>;
27
28
  }
28
29
 
29
- const { colorRoles: cr, dimensions: dim } = sys;
30
-
31
30
  export function SegmentedControl({
32
31
  options,
33
32
  value,
@@ -35,12 +34,34 @@ export function SegmentedControl({
35
34
  fullWidth = false,
36
35
  style,
37
36
  }: SegmentedControlProps) {
37
+ const { colorRoles: cr, dimensions: dim } = useTheme();
38
+
38
39
  return (
39
- <View style={[styles.container, fullWidth && styles.fullWidth, style]}>
40
+ <View
41
+ style={[
42
+ styles.container,
43
+ {
44
+ backgroundColor:
45
+ cr.surface.surfaceContainer.sysSurfaceContainerLowest,
46
+ borderWidth: dim.borderWidth.sysStrokeThin,
47
+ borderColor: cr.outline.sysOutline,
48
+ borderRadius: dim.borderRadius.sysRadiusFull,
49
+ padding: dim.spacing.padding.sysPadding4,
50
+ gap: dim.spacing.padding.sysPadding4,
51
+ },
52
+ fullWidth && styles.fullWidth,
53
+ style,
54
+ ]}
55
+ >
40
56
  {options.map((option) => {
41
57
  const isActive = option.value === value;
42
58
  const isDisabled = option.disabled ?? false;
43
59
 
60
+ const iconColor = isActive
61
+ ? cr.accent.primary.sysOnPrimary
62
+ : cr.surface.surface.sysOnSurfaceVariant;
63
+ const IconComponent = option.icon ? ICON_MAP[option.icon] : null;
64
+
44
65
  return (
45
66
  <Pressable
46
67
  key={option.value}
@@ -57,19 +78,19 @@ export function SegmentedControl({
57
78
  borderRadius: dim.borderRadius.sysRadiusFull,
58
79
  },
59
80
  isActive && { backgroundColor: cr.accent.primary.sysPrimary },
60
- !isActive && pressed && !isDisabled && styles.pressed,
81
+ !isActive &&
82
+ pressed &&
83
+ !isDisabled && {
84
+ backgroundColor: cr.transparent.neutral.sysBlack10,
85
+ },
61
86
  isDisabled && styles.disabled,
62
87
  ]}
63
88
  >
64
- {option.icon && <View style={styles.iconSlot}>{option.icon}</View>}
89
+ {IconComponent && <IconComponent size="xsmall" color={iconColor} />}
65
90
  <BodyText
66
91
  variant="small"
67
92
  emphasized={isActive}
68
- color={
69
- isActive
70
- ? cr.accent.primary.sysOnPrimary
71
- : cr.surface.surface.sysOnSurfaceVariant
72
- }
93
+ color={iconColor}
73
94
  numberOfLines={1}
74
95
  >
75
96
  {option.label}
@@ -86,17 +107,12 @@ const styles = StyleSheet.create({
86
107
  flexDirection: 'row',
87
108
  alignItems: 'center',
88
109
  alignSelf: 'flex-start',
89
- backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
90
- borderWidth: dim.borderWidth.sysStrokeThin,
91
- borderColor: cr.outline.sysOutline,
92
- borderRadius: dim.borderRadius.sysRadiusFull,
93
- padding: dim.spacing.padding.sysPadding4,
94
- gap: dim.spacing.padding.sysPadding4,
110
+ // Elevation/lv1: two Figma drop shadows combined into one RN shadow
95
111
  shadowColor: '#000',
96
112
  shadowOffset: { width: 0, height: 2 },
97
- shadowOpacity: 0.06,
98
- shadowRadius: 4,
99
- elevation: 1,
113
+ shadowOpacity: 0.08,
114
+ shadowRadius: 8,
115
+ elevation: 2,
100
116
  },
101
117
  fullWidth: {
102
118
  alignSelf: 'stretch',
@@ -107,15 +123,6 @@ const styles = StyleSheet.create({
107
123
  alignItems: 'center',
108
124
  justifyContent: 'center',
109
125
  },
110
- iconSlot: {
111
- width: 16,
112
- height: 16,
113
- alignItems: 'center',
114
- justifyContent: 'center',
115
- },
116
- pressed: {
117
- backgroundColor: cr.transparent.neutral.sysBlack10,
118
- },
119
126
  disabled: {
120
127
  opacity: 0.48,
121
128
  },
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
 
3
3
  import {
4
4
  Pressable,
@@ -10,8 +10,8 @@ import {
10
10
 
11
11
  import { Ionicons } from '@expo/vector-icons';
12
12
 
13
- import { ICON_MAP, type IconName } from '../../icons';
14
- import { sys } from '../../tokens';
13
+ import { type IconName, SIZE_MAP, resolveIcon } from '../../icons';
14
+ import { useTheme } from '../../theme';
15
15
  import { BodyText } from '../BodyText';
16
16
  import { HeaderText } from '../HeaderText';
17
17
 
@@ -29,16 +29,6 @@ export interface SelectionCardProps {
29
29
  style?: StyleProp<ViewStyle>;
30
30
  }
31
31
 
32
- const { colorRoles: cr, dimensions: dim } = sys;
33
-
34
- const SELECTED_RING = {
35
- shadowColor: cr.accent.primary.sysPrimary,
36
- shadowOffset: { width: 1, height: 2 },
37
- shadowOpacity: 1,
38
- shadowRadius: 0,
39
- elevation: 2,
40
- };
41
-
42
32
  export function SelectionCard({
43
33
  title,
44
34
  icon,
@@ -50,7 +40,21 @@ export function SelectionCard({
50
40
  accessibilityLabel,
51
41
  style,
52
42
  }: SelectionCardProps) {
53
- const IconComponent = icon ? ICON_MAP[icon] : null;
43
+ const { colorRoles: cr, dimensions: dim } = useTheme();
44
+
45
+ const selectedRing = useMemo(
46
+ () => ({
47
+ shadowColor: cr.accent.primary.sysPrimary,
48
+ shadowOffset: { width: 1, height: 2 },
49
+ shadowOpacity: 1,
50
+ shadowRadius: 0,
51
+ elevation: 2,
52
+ }),
53
+ [cr],
54
+ );
55
+
56
+ const LucideIcon = icon ? resolveIcon(icon) : null;
57
+ const { size: iconPx, strokeWidth: iconSW } = SIZE_MAP['small'];
54
58
 
55
59
  const a11yState = {
56
60
  disabled,
@@ -80,6 +84,12 @@ export function SelectionCard({
80
84
  style={({ pressed }) => [
81
85
  styles.smRoot,
82
86
  {
87
+ gap: dim.spacing.padding.sysPadding12,
88
+ paddingHorizontal: dim.spacing.padding.sysPadding20,
89
+ paddingVertical: dim.spacing.padding.sysPadding16,
90
+ borderRadius: dim.borderRadius.sysRadiusMd,
91
+ backgroundColor:
92
+ cr.surface.surfaceContainer.sysSurfaceContainerLowest,
83
93
  borderColor: selected
84
94
  ? cr.accent.primary.sysPrimary
85
95
  : cr.outline.sysOutline,
@@ -88,14 +98,14 @@ export function SelectionCard({
88
98
  : dim.borderWidth.sysStrokeThin,
89
99
  opacity: disabled ? 0.38 : 1,
90
100
  },
91
- selected && SELECTED_RING,
101
+ selected && selectedRing,
92
102
  pressed && !disabled && styles.pressed,
93
103
  style,
94
104
  ]}
95
105
  >
96
- {IconComponent && (
106
+ {LucideIcon && (
97
107
  <View style={styles.smIconWrap}>
98
- <IconComponent size="small" color={iconColor} />
108
+ <LucideIcon size={iconPx} color={iconColor} strokeWidth={iconSW} />
99
109
  </View>
100
110
  )}
101
111
  <BodyText
@@ -122,9 +132,18 @@ export function SelectionCard({
122
132
  ? cr.accent.primary.sysPrimary
123
133
  : cr.surface.surface.sysOnSurface;
124
134
 
125
- const iconCircle = IconComponent ? (
126
- <View style={[styles.mdIconCircle, { backgroundColor: iconBgColor }]}>
127
- <IconComponent size="small" color={iconColor} />
135
+ const iconCircle = LucideIcon ? (
136
+ <View
137
+ style={[
138
+ styles.mdIconCircle,
139
+ {
140
+ padding: dim.spacing.padding.sysPadding12,
141
+ borderRadius: dim.borderRadius.sysRadiusFull,
142
+ backgroundColor: iconBgColor,
143
+ },
144
+ ]}
145
+ >
146
+ <LucideIcon size={iconPx} color={iconColor} strokeWidth={iconSW} />
128
147
  </View>
129
148
  ) : null;
130
149
 
@@ -137,6 +156,11 @@ export function SelectionCard({
137
156
  style={({ pressed }) => [
138
157
  styles.mdRoot,
139
158
  {
159
+ padding: dim.spacing.padding.sysPadding24,
160
+ gap: dim.spacing.padding.sysPadding24,
161
+ borderRadius: dim.borderRadius.sysRadiusLg,
162
+ backgroundColor:
163
+ cr.surface.surfaceContainer.sysSurfaceContainerLowest,
140
164
  borderColor: selected
141
165
  ? cr.accent.primary.sysPrimary
142
166
  : cr.outline.sysOutline,
@@ -145,7 +169,7 @@ export function SelectionCard({
145
169
  : dim.borderWidth.sysStrokeMedium,
146
170
  opacity: disabled ? 0.38 : 1,
147
171
  },
148
- selected && SELECTED_RING,
172
+ selected && selectedRing,
149
173
  pressed && !disabled && styles.pressed,
150
174
  style,
151
175
  ]}
@@ -169,17 +193,11 @@ const styles = StyleSheet.create({
169
193
  mdRoot: {
170
194
  flexDirection: 'column',
171
195
  alignItems: 'flex-start',
172
- padding: dim.spacing.padding.sysPadding24,
173
- gap: dim.spacing.padding.sysPadding24,
174
- borderRadius: dim.borderRadius.sysRadiusLg,
175
- backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
176
196
  overflow: 'hidden',
177
197
  },
178
198
  mdIconCircle: {
179
199
  alignItems: 'center',
180
200
  justifyContent: 'center',
181
- padding: dim.spacing.padding.sysPadding12,
182
- borderRadius: dim.borderRadius.sysRadiusFull,
183
201
  overflow: 'hidden',
184
202
  },
185
203
  mdIconCheckboxRow: {
@@ -191,11 +209,6 @@ const styles = StyleSheet.create({
191
209
  smRoot: {
192
210
  flexDirection: 'row',
193
211
  alignItems: 'center',
194
- gap: dim.spacing.padding.sysPadding12,
195
- paddingHorizontal: dim.spacing.padding.sysPadding20,
196
- paddingVertical: dim.spacing.padding.sysPadding16,
197
- borderRadius: dim.borderRadius.sysRadiusMd,
198
- backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
199
212
  overflow: 'hidden',
200
213
  },
201
214
  smIconWrap: {
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
 
3
3
  import {
4
4
  Image,
@@ -10,10 +10,8 @@ import {
10
10
  ViewStyle,
11
11
  } from 'react-native';
12
12
 
13
- import { Ionicons } from '@expo/vector-icons';
14
-
15
- import { ICON_MAP, type IconName } from '../../icons';
16
- import { sys } from '../../tokens';
13
+ import { type IconName, SIZE_MAP, resolveIcon } from '../../icons';
14
+ import { useTheme } from '../../theme';
17
15
  import { BodyText } from '../BodyText';
18
16
 
19
17
  export type ServiceCardVariant =
@@ -41,114 +39,6 @@ export interface ServiceCardProps {
41
39
  style?: StyleProp<ViewStyle>;
42
40
  }
43
41
 
44
- const { colorRoles: cr, dimensions: dim } = sys;
45
-
46
- // ── Variant tokens ────────────────────────────────────────────────────────────
47
-
48
- const VARIANT_TOKENS: Record<
49
- ServiceCardVariant,
50
- {
51
- bg: string;
52
- borderColor: string;
53
- borderWidth: number;
54
- elevated: boolean;
55
- titleColor: string;
56
- descColor: string;
57
- chevronColor: string;
58
- // doubled inner gradient overlay color
59
- innerBg?: string;
60
- iconBadgeBg?: string;
61
- iconBadgeColor?: string;
62
- }
63
- > = {
64
- outlined: {
65
- bg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
66
- borderColor: cr.outline.sysOutline,
67
- borderWidth: dim.borderWidth.sysStrokeThin,
68
- elevated: true,
69
- titleColor: cr.surface.surface.sysOnSurface,
70
- descColor: cr.surface.surface.sysOnSurfaceVariant,
71
- chevronColor: cr.outline.sysOutlineFixed,
72
- },
73
- tonal: {
74
- bg: cr.addOn.primaryFixed.sysPrimaryFixedDim,
75
- borderColor: 'transparent',
76
- borderWidth: 0,
77
- elevated: false,
78
- titleColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
79
- descColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
80
- chevronColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
81
- },
82
- filled: {
83
- bg: cr.accent.primary.sysPrimaryContainer,
84
- borderColor: 'transparent',
85
- borderWidth: 0,
86
- elevated: false,
87
- titleColor: cr.accent.primary.sysOnPrimary,
88
- descColor: cr.transparent.neutral.sysWhite80,
89
- chevronColor: cr.accent.primary.sysOnPrimary,
90
- },
91
- doubled: {
92
- bg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
93
- borderColor: cr.outline.sysOutline,
94
- borderWidth: dim.borderWidth.sysStrokeThin,
95
- elevated: false,
96
- titleColor: cr.surface.surface.sysOnSurface,
97
- descColor: cr.surface.surface.sysOnSurfaceVariant,
98
- chevronColor: cr.outline.sysOutlineFixed,
99
- innerBg: cr.transparent.primary.sysPrimary08,
100
- iconBadgeBg: cr.accent.primary.sysPrimary,
101
- iconBadgeColor: cr.accent.primary.sysOnPrimary,
102
- },
103
- image: {
104
- bg: 'transparent',
105
- borderColor: 'transparent',
106
- borderWidth: 0,
107
- elevated: false,
108
- titleColor: cr.surface.surface.sysInverseOnSurface,
109
- descColor: cr.transparent.neutral.sysWhite80,
110
- chevronColor: cr.surface.surface.sysInverseOnSurface,
111
- },
112
- };
113
-
114
- // ── Size tokens ───────────────────────────────────────────────────────────────
115
-
116
- const SIZE_TOKENS = {
117
- sm: {
118
- layout: 'row' as const,
119
- paddingH: dim.spacing.padding.sysPadding16,
120
- paddingV: dim.spacing.padding.sysPadding12,
121
- gap: dim.spacing.padding.sysPadding12,
122
- borderRadius: dim.borderRadius.sysRadiusMd,
123
- iconSize: 24,
124
- iconSizeName: 'small' as const,
125
- titleVariant: 'medium' as const,
126
- showChevron: true,
127
- },
128
- md: {
129
- layout: 'column' as const,
130
- paddingH: dim.spacing.padding.sysPadding16,
131
- paddingV: dim.spacing.padding.sysPadding16,
132
- gap: dim.spacing.padding.sysPadding24,
133
- borderRadius: dim.borderRadius.sysRadiusLg,
134
- iconSize: 32,
135
- iconSizeName: 'medium' as const,
136
- titleVariant: 'medium' as const,
137
- showChevron: false,
138
- },
139
- lg: {
140
- layout: 'column' as const,
141
- paddingH: dim.spacing.padding.sysPadding16,
142
- paddingV: dim.spacing.padding.sysPadding16,
143
- gap: dim.spacing.padding.sysPadding32,
144
- borderRadius: dim.borderRadius.sysRadiusLg,
145
- iconSize: 32,
146
- iconSizeName: 'large' as const,
147
- titleVariant: 'large' as const,
148
- showChevron: false,
149
- },
150
- };
151
-
152
42
  const ELEVATION = {
153
43
  shadowColor: '#000',
154
44
  shadowOffset: { width: 0, height: 2 },
@@ -175,19 +65,128 @@ export function ServiceCard({
175
65
  fullWidth = false,
176
66
  style,
177
67
  }: ServiceCardProps) {
178
- const v = VARIANT_TOKENS[variant];
179
- const s = SIZE_TOKENS[size];
68
+ const { colorRoles: cr, dimensions: dim } = useTheme();
69
+
70
+ const variantTokens = useMemo(
71
+ () => ({
72
+ outlined: {
73
+ bg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
74
+ borderColor: cr.outline.sysOutline,
75
+ borderWidth: dim.borderWidth.sysStrokeThin,
76
+ elevated: true,
77
+ titleColor: cr.surface.surface.sysOnSurface,
78
+ descColor: cr.surface.surface.sysOnSurfaceVariant,
79
+ chevronColor: cr.outline.sysOutlineFixed,
80
+ innerBg: undefined,
81
+ iconBadgeBg: undefined,
82
+ iconBadgeColor: undefined,
83
+ },
84
+ tonal: {
85
+ bg: cr.addOn.primaryFixed.sysPrimaryFixedDim,
86
+ borderColor: 'transparent',
87
+ borderWidth: 0,
88
+ elevated: false,
89
+ titleColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
90
+ descColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
91
+ chevronColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
92
+ innerBg: undefined,
93
+ iconBadgeBg: undefined,
94
+ iconBadgeColor: undefined,
95
+ },
96
+ filled: {
97
+ bg: cr.accent.primary.sysPrimaryContainer,
98
+ borderColor: 'transparent',
99
+ borderWidth: 0,
100
+ elevated: false,
101
+ titleColor: cr.accent.primary.sysOnPrimary,
102
+ descColor: cr.transparent.neutral.sysWhite80,
103
+ chevronColor: cr.accent.primary.sysOnPrimary,
104
+ innerBg: undefined,
105
+ iconBadgeBg: undefined,
106
+ iconBadgeColor: undefined,
107
+ },
108
+ doubled: {
109
+ bg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
110
+ borderColor: cr.outline.sysOutline,
111
+ borderWidth: dim.borderWidth.sysStrokeThin,
112
+ elevated: false,
113
+ titleColor: cr.surface.surface.sysOnSurface,
114
+ descColor: cr.surface.surface.sysOnSurfaceVariant,
115
+ chevronColor: cr.outline.sysOutlineFixed,
116
+ innerBg: cr.transparent.primary.sysPrimary08,
117
+ iconBadgeBg: cr.accent.primary.sysPrimary,
118
+ iconBadgeColor: cr.accent.primary.sysOnPrimary,
119
+ },
120
+ image: {
121
+ bg: 'transparent',
122
+ borderColor: 'transparent',
123
+ borderWidth: 0,
124
+ elevated: false,
125
+ titleColor: cr.surface.inverse.sysInverseOnSurface,
126
+ descColor: cr.transparent.neutral.sysWhite80,
127
+ chevronColor: cr.surface.inverse.sysInverseOnSurface,
128
+ innerBg: undefined,
129
+ iconBadgeBg: undefined,
130
+ iconBadgeColor: undefined,
131
+ },
132
+ }),
133
+ [cr, dim],
134
+ );
135
+
136
+ const sizeTokens = useMemo(
137
+ () => ({
138
+ sm: {
139
+ layout: 'row' as const,
140
+ paddingH: dim.spacing.padding.sysPadding16,
141
+ paddingV: dim.spacing.padding.sysPadding12,
142
+ gap: dim.spacing.padding.sysPadding12,
143
+ borderRadius: dim.borderRadius.sysRadiusMd,
144
+ iconSize: 24,
145
+ iconSizeName: 'small' as const,
146
+ titleVariant: 'medium' as const,
147
+ showChevron: true,
148
+ },
149
+ md: {
150
+ layout: 'column' as const,
151
+ paddingH: dim.spacing.padding.sysPadding16,
152
+ paddingV: dim.spacing.padding.sysPadding16,
153
+ gap: dim.spacing.padding.sysPadding24,
154
+ borderRadius: dim.borderRadius.sysRadiusLg,
155
+ iconSize: 32,
156
+ iconSizeName: 'medium' as const,
157
+ titleVariant: 'medium' as const,
158
+ showChevron: false,
159
+ },
160
+ lg: {
161
+ layout: 'column' as const,
162
+ paddingH: dim.spacing.padding.sysPadding16,
163
+ paddingV: dim.spacing.padding.sysPadding16,
164
+ gap: dim.spacing.padding.sysPadding32,
165
+ borderRadius: dim.borderRadius.sysRadiusLg,
166
+ iconSize: 32,
167
+ iconSizeName: 'large' as const,
168
+ titleVariant: 'large' as const,
169
+ showChevron: false,
170
+ },
171
+ }),
172
+ [dim],
173
+ );
174
+
175
+ const v = variantTokens[variant];
176
+ const s = sizeTokens[size];
180
177
  const isRow = s.layout === 'row';
181
178
 
182
- const IconComponent = icon ? ICON_MAP[icon] : null;
179
+ const LucideIcon = icon ? resolveIcon(icon) : null;
180
+ const ChevronRight = resolveIcon('ChevronRight');
183
181
  const iconColor = v.iconBadgeColor ?? v.titleColor;
184
- const renderedIcon = IconComponent ? (
185
- <IconComponent size={s.iconSizeName} color={iconColor} />
182
+ const { size: iconPx, strokeWidth: iconSW } = SIZE_MAP[s.iconSizeName];
183
+ const renderedIcon = LucideIcon ? (
184
+ <LucideIcon size={iconPx} color={iconColor} strokeWidth={iconSW} />
186
185
  ) : null;
187
186
 
188
187
  const outerStyle = [
189
188
  {
190
- backgroundColor: variant === 'doubled' ? v.bg : v.bg,
189
+ backgroundColor: v.bg,
191
190
  borderColor: v.borderColor,
192
191
  borderWidth: v.borderWidth,
193
192
  borderRadius:
@@ -264,7 +263,7 @@ export function ServiceCard({
264
263
  <Image
265
264
  source={image}
266
265
  style={[
267
- StyleSheet.absoluteFillObject,
266
+ { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
268
267
  { borderRadius: s.borderRadius },
269
268
  ]}
270
269
  resizeMode="cover"
@@ -272,7 +271,7 @@ export function ServiceCard({
272
271
  />
273
272
  <View
274
273
  style={[
275
- StyleSheet.absoluteFillObject,
274
+ { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
276
275
  {
277
276
  borderRadius: s.borderRadius,
278
277
  backgroundColor: 'rgba(0,0,0,0.30)',
@@ -288,6 +287,9 @@ export function ServiceCard({
288
287
  </View>
289
288
  )}
290
289
  {textBlock}
290
+ {isRow && ChevronRight && (
291
+ <ChevronRight size={20} color="#ffffff" strokeWidth={1.5} />
292
+ )}
291
293
  {children}
292
294
  </>
293
295
  );
@@ -301,13 +303,8 @@ export function ServiceCard({
301
303
  </View>
302
304
  )}
303
305
  {textBlock}
304
- {s.showChevron && (
305
- <Ionicons
306
- name="chevron-forward"
307
- size={20}
308
- color={v.chevronColor}
309
- accessible={false}
310
- />
306
+ {s.showChevron && ChevronRight && (
307
+ <ChevronRight size={20} color={v.chevronColor} strokeWidth={1.5} />
311
308
  )}
312
309
  {children}
313
310
  </>