@compsych/mobile-ui 1.0.10 → 1.0.13

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 (45) hide show
  1. package/package.json +1 -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 +22 -12
  28. package/src/components/SegmentedControl/index.tsx +37 -30
  29. package/src/components/SelectionCard/index.tsx +38 -26
  30. package/src/components/ServiceCard/index.tsx +162 -130
  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/index.ts +13 -0
  36. package/src/theme/ThemeContext.ts +11 -0
  37. package/src/theme/ThemeProvider.tsx +40 -0
  38. package/src/theme/__tests__/ThemeProvider.test.tsx +101 -0
  39. package/src/theme/__tests__/defaultTheme.test.ts +36 -0
  40. package/src/theme/__tests__/mergeTheme.test.ts +94 -0
  41. package/src/theme/defaultTheme.ts +15 -0
  42. package/src/theme/index.ts +8 -0
  43. package/src/theme/mergeTheme.ts +47 -0
  44. package/src/theme/types.ts +39 -0
  45. package/src/theme/useTheme.ts +16 -0
@@ -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,
@@ -11,7 +11,7 @@ import {
11
11
  import { Ionicons } from '@expo/vector-icons';
12
12
 
13
13
  import { ICON_MAP, type IconName } from '../../icons';
14
- import { sys } from '../../tokens';
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,6 +40,19 @@ export function SelectionCard({
50
40
  accessibilityLabel,
51
41
  style,
52
42
  }: SelectionCardProps) {
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
+
53
56
  const IconComponent = icon ? ICON_MAP[icon] : null;
54
57
 
55
58
  const a11yState = {
@@ -80,6 +83,12 @@ export function SelectionCard({
80
83
  style={({ pressed }) => [
81
84
  styles.smRoot,
82
85
  {
86
+ gap: dim.spacing.padding.sysPadding12,
87
+ paddingHorizontal: dim.spacing.padding.sysPadding20,
88
+ paddingVertical: dim.spacing.padding.sysPadding16,
89
+ borderRadius: dim.borderRadius.sysRadiusMd,
90
+ backgroundColor:
91
+ cr.surface.surfaceContainer.sysSurfaceContainerLowest,
83
92
  borderColor: selected
84
93
  ? cr.accent.primary.sysPrimary
85
94
  : cr.outline.sysOutline,
@@ -88,7 +97,7 @@ export function SelectionCard({
88
97
  : dim.borderWidth.sysStrokeThin,
89
98
  opacity: disabled ? 0.38 : 1,
90
99
  },
91
- selected && SELECTED_RING,
100
+ selected && selectedRing,
92
101
  pressed && !disabled && styles.pressed,
93
102
  style,
94
103
  ]}
@@ -123,7 +132,16 @@ export function SelectionCard({
123
132
  : cr.surface.surface.sysOnSurface;
124
133
 
125
134
  const iconCircle = IconComponent ? (
126
- <View style={[styles.mdIconCircle, { backgroundColor: iconBgColor }]}>
135
+ <View
136
+ style={[
137
+ styles.mdIconCircle,
138
+ {
139
+ padding: dim.spacing.padding.sysPadding12,
140
+ borderRadius: dim.borderRadius.sysRadiusFull,
141
+ backgroundColor: iconBgColor,
142
+ },
143
+ ]}
144
+ >
127
145
  <IconComponent size="small" color={iconColor} />
128
146
  </View>
129
147
  ) : null;
@@ -137,6 +155,11 @@ export function SelectionCard({
137
155
  style={({ pressed }) => [
138
156
  styles.mdRoot,
139
157
  {
158
+ padding: dim.spacing.padding.sysPadding24,
159
+ gap: dim.spacing.padding.sysPadding24,
160
+ borderRadius: dim.borderRadius.sysRadiusLg,
161
+ backgroundColor:
162
+ cr.surface.surfaceContainer.sysSurfaceContainerLowest,
140
163
  borderColor: selected
141
164
  ? cr.accent.primary.sysPrimary
142
165
  : cr.outline.sysOutline,
@@ -145,7 +168,7 @@ export function SelectionCard({
145
168
  : dim.borderWidth.sysStrokeMedium,
146
169
  opacity: disabled ? 0.38 : 1,
147
170
  },
148
- selected && SELECTED_RING,
171
+ selected && selectedRing,
149
172
  pressed && !disabled && styles.pressed,
150
173
  style,
151
174
  ]}
@@ -169,17 +192,11 @@ const styles = StyleSheet.create({
169
192
  mdRoot: {
170
193
  flexDirection: 'column',
171
194
  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
195
  overflow: 'hidden',
177
196
  },
178
197
  mdIconCircle: {
179
198
  alignItems: 'center',
180
199
  justifyContent: 'center',
181
- padding: dim.spacing.padding.sysPadding12,
182
- borderRadius: dim.borderRadius.sysRadiusFull,
183
200
  overflow: 'hidden',
184
201
  },
185
202
  mdIconCheckboxRow: {
@@ -191,11 +208,6 @@ const styles = StyleSheet.create({
191
208
  smRoot: {
192
209
  flexDirection: 'row',
193
210
  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
211
  overflow: 'hidden',
200
212
  },
201
213
  smIconWrap: {
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
 
3
3
  import {
4
4
  Image,
@@ -13,7 +13,7 @@ import {
13
13
  import { Ionicons } from '@expo/vector-icons';
14
14
 
15
15
  import { ICON_MAP, type IconName } from '../../icons';
16
- import { sys } from '../../tokens';
16
+ import { useTheme } from '../../theme';
17
17
  import { BodyText } from '../BodyText';
18
18
 
19
19
  export type ServiceCardVariant =
@@ -41,114 +41,6 @@ export interface ServiceCardProps {
41
41
  style?: StyleProp<ViewStyle>;
42
42
  }
43
43
 
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
44
  const ELEVATION = {
153
45
  shadowColor: '#000',
154
46
  shadowOffset: { width: 0, height: 2 },
@@ -175,8 +67,118 @@ export function ServiceCard({
175
67
  fullWidth = false,
176
68
  style,
177
69
  }: ServiceCardProps) {
178
- const v = VARIANT_TOKENS[variant];
179
- const s = SIZE_TOKENS[size];
70
+ const { colorRoles: cr, dimensions: dim } = useTheme();
71
+
72
+ const variantTokens = useMemo(
73
+ () => ({
74
+ outlined: {
75
+ bg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
76
+ borderColor: cr.outline.sysOutline,
77
+ borderWidth: dim.borderWidth.sysStrokeThin,
78
+ elevated: true,
79
+ titleColor: cr.surface.surface.sysOnSurface,
80
+ descColor: cr.surface.surface.sysOnSurfaceVariant,
81
+ chevronColor: cr.outline.sysOutlineFixed,
82
+ innerBg: undefined,
83
+ iconBadgeBg: undefined,
84
+ iconBadgeColor: undefined,
85
+ },
86
+ tonal: {
87
+ bg: cr.addOn.primaryFixed.sysPrimaryFixedDim,
88
+ borderColor: 'transparent',
89
+ borderWidth: 0,
90
+ elevated: false,
91
+ titleColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
92
+ descColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
93
+ chevronColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
94
+ innerBg: undefined,
95
+ iconBadgeBg: undefined,
96
+ iconBadgeColor: undefined,
97
+ },
98
+ filled: {
99
+ bg: cr.accent.primary.sysPrimaryContainer,
100
+ borderColor: 'transparent',
101
+ borderWidth: 0,
102
+ elevated: false,
103
+ titleColor: cr.accent.primary.sysOnPrimary,
104
+ descColor: cr.transparent.neutral.sysWhite80,
105
+ chevronColor: cr.accent.primary.sysOnPrimary,
106
+ innerBg: undefined,
107
+ iconBadgeBg: undefined,
108
+ iconBadgeColor: undefined,
109
+ },
110
+ doubled: {
111
+ bg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
112
+ borderColor: cr.outline.sysOutline,
113
+ borderWidth: dim.borderWidth.sysStrokeThin,
114
+ elevated: false,
115
+ titleColor: cr.surface.surface.sysOnSurface,
116
+ descColor: cr.surface.surface.sysOnSurfaceVariant,
117
+ chevronColor: cr.outline.sysOutlineFixed,
118
+ innerBg: cr.transparent.primary.sysPrimary08,
119
+ iconBadgeBg: cr.accent.primary.sysPrimary,
120
+ iconBadgeColor: cr.accent.primary.sysOnPrimary,
121
+ },
122
+ image: {
123
+ bg: 'transparent',
124
+ borderColor: 'transparent',
125
+ borderWidth: 0,
126
+ elevated: false,
127
+ titleColor: cr.surface.inverse.sysInverseOnSurface,
128
+ descColor: cr.transparent.neutral.sysWhite80,
129
+ chevronColor: cr.surface.inverse.sysInverseOnSurface,
130
+ innerBg: undefined,
131
+ iconBadgeBg: undefined,
132
+ iconBadgeColor: undefined,
133
+ },
134
+ }),
135
+ [cr, dim],
136
+ );
137
+
138
+ const sizeTokens = useMemo(
139
+ () => ({
140
+ sm: {
141
+ layout: 'row' as const,
142
+ paddingH: dim.spacing.padding.sysPadding16,
143
+ paddingV: dim.spacing.padding.sysPadding12,
144
+ gap: dim.spacing.padding.sysPadding12,
145
+ borderRadius: dim.borderRadius.sysRadiusMd,
146
+ iconSize: 24,
147
+ iconSizeName: 'small' as const,
148
+ titleVariant: 'medium' as const,
149
+ descVariant: 'small' as const,
150
+ showChevron: true,
151
+ },
152
+ md: {
153
+ layout: 'column' as const,
154
+ paddingH: dim.spacing.padding.sysPadding16,
155
+ paddingV: dim.spacing.padding.sysPadding16,
156
+ gap: dim.spacing.padding.sysPadding24,
157
+ borderRadius: dim.borderRadius.sysRadiusLg,
158
+ iconSize: 32,
159
+ iconSizeName: 'medium' as const,
160
+ titleVariant: 'medium' as const,
161
+ descVariant: 'small' as const,
162
+ showChevron: false,
163
+ },
164
+ lg: {
165
+ layout: 'column' as const,
166
+ paddingH: dim.spacing.padding.sysPadding16,
167
+ paddingV: dim.spacing.padding.sysPadding16,
168
+ gap: dim.spacing.padding.sysPadding32,
169
+ borderRadius: dim.borderRadius.sysRadiusLg,
170
+ iconSize: 32,
171
+ iconSizeName: 'large' as const,
172
+ titleVariant: 'large' as const,
173
+ descVariant: 'small' as const,
174
+ showChevron: false,
175
+ },
176
+ }),
177
+ [dim],
178
+ );
179
+
180
+ const v = variantTokens[variant];
181
+ const s = sizeTokens[size];
180
182
  const isRow = s.layout === 'row';
181
183
 
182
184
  const IconComponent = icon ? ICON_MAP[icon] : null;
@@ -187,7 +189,7 @@ export function ServiceCard({
187
189
 
188
190
  const outerStyle = [
189
191
  {
190
- backgroundColor: variant === 'doubled' ? v.bg : v.bg,
192
+ backgroundColor: v.bg,
191
193
  borderColor: v.borderColor,
192
194
  borderWidth: v.borderWidth,
193
195
  borderRadius:
@@ -215,7 +217,7 @@ export function ServiceCard({
215
217
  </BodyText>
216
218
  )}
217
219
  {!isRow && description && (
218
- <BodyText variant="small" color={v.descColor}>
220
+ <BodyText variant={s.descVariant} color={v.descColor}>
219
221
  {description}
220
222
  </BodyText>
221
223
  )}
@@ -264,7 +266,7 @@ export function ServiceCard({
264
266
  <Image
265
267
  source={image}
266
268
  style={[
267
- StyleSheet.absoluteFillObject,
269
+ { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
268
270
  { borderRadius: s.borderRadius },
269
271
  ]}
270
272
  resizeMode="cover"
@@ -272,7 +274,7 @@ export function ServiceCard({
272
274
  />
273
275
  <View
274
276
  style={[
275
- StyleSheet.absoluteFillObject,
277
+ { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
276
278
  {
277
279
  borderRadius: s.borderRadius,
278
280
  backgroundColor: 'rgba(0,0,0,0.30)',
@@ -287,7 +289,23 @@ export function ServiceCard({
287
289
  {renderedIcon}
288
290
  </View>
289
291
  )}
290
- {textBlock}
292
+ <View style={styles.contentRow}>
293
+ {(title || description) && (
294
+ <View style={styles.contentText}>
295
+ {title && (
296
+ <BodyText variant={s.titleVariant} color={v.titleColor}>
297
+ {title}
298
+ </BodyText>
299
+ )}
300
+ {description && (
301
+ <BodyText variant={s.descVariant} color={v.descColor}>
302
+ {description}
303
+ </BodyText>
304
+ )}
305
+ </View>
306
+ )}
307
+ {buttonIcon}
308
+ </View>
291
309
  {children}
292
310
  </>
293
311
  );
@@ -321,20 +339,24 @@ export function ServiceCard({
321
339
  {renderedIcon}
322
340
  </View>
323
341
  )}
324
- {textBlock}
342
+ <View style={styles.contentRow}>
343
+ {(title || description) && (
344
+ <View style={styles.contentText}>
345
+ {title && (
346
+ <BodyText variant={s.titleVariant} color={v.titleColor}>
347
+ {title}
348
+ </BodyText>
349
+ )}
350
+ {description && (
351
+ <BodyText variant={s.descVariant} color={v.descColor}>
352
+ {description}
353
+ </BodyText>
354
+ )}
355
+ </View>
356
+ )}
357
+ {buttonIcon}
358
+ </View>
325
359
  {children}
326
- {buttonIcon && (
327
- <View
328
- style={{
329
- position: 'absolute',
330
- bottom: s.paddingV,
331
- right: s.paddingH,
332
- }}
333
- pointerEvents="none"
334
- >
335
- {buttonIcon}
336
- </View>
337
- )}
338
360
  </>
339
361
  );
340
362
  }
@@ -380,6 +402,16 @@ const styles = StyleSheet.create({
380
402
  flex: 1,
381
403
  minWidth: 0,
382
404
  },
405
+ contentRow: {
406
+ flexDirection: 'row',
407
+ alignItems: 'flex-end',
408
+ alignSelf: 'stretch',
409
+ gap: 16,
410
+ },
411
+ contentText: {
412
+ flex: 1,
413
+ minWidth: 0,
414
+ },
383
415
  doubledIconBadge: {
384
416
  width: 48,
385
417
  height: 48,