@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compsych/mobile-ui",
3
- "version": "1.0.10",
3
+ "version": "1.0.13",
4
4
  "description": "ComPsych React Native Design System — 19 mobile UI components with self-contained design tokens",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
 
3
3
  import {
4
4
  Modal,
@@ -12,7 +12,7 @@ import {
12
12
 
13
13
  import { Ionicons } from '@expo/vector-icons';
14
14
 
15
- import { sys } from '../../tokens';
15
+ import { useTheme } from '../../theme';
16
16
  import { Button } from '../Button';
17
17
  import { HeaderText } from '../HeaderText';
18
18
 
@@ -28,11 +28,17 @@ export interface ActionSheetProps {
28
28
  children?: React.ReactNode;
29
29
  primaryAction?: ActionSheetAction;
30
30
  secondaryAction?: ActionSheetAction;
31
+ /**
32
+ * `half` — floats at the bottom with a 6 px inset gutter; all corners
33
+ * rounded; content limited to maxHeight. Default.
34
+ * `full` — fills the screen from 10 px below the top; only top corners
35
+ * rounded; content expands to fill; close button appears on both
36
+ * sides of the title for one-handed reachability.
37
+ */
38
+ variant?: 'half' | 'full';
31
39
  style?: StyleProp<ViewStyle>;
32
40
  }
33
41
 
34
- const { colorRoles: cr } = sys;
35
-
36
42
  export function ActionSheet({
37
43
  visible,
38
44
  onClose,
@@ -40,8 +46,68 @@ export function ActionSheet({
40
46
  children,
41
47
  primaryAction,
42
48
  secondaryAction,
49
+ variant = 'half',
43
50
  style,
44
51
  }: ActionSheetProps) {
52
+ const { colorRoles: cr, dimensions: dim } = useTheme();
53
+ const isFull = variant === 'full';
54
+
55
+ const dynamicStyles = useMemo(
56
+ () => ({
57
+ sheetWrapperHalf: {
58
+ paddingHorizontal: dim.spacing.padding.sysPadding6,
59
+ paddingBottom: dim.spacing.padding.sysPadding6,
60
+ },
61
+ sheetWrapperFull: {
62
+ marginTop: 10,
63
+ },
64
+ sheetHalf: {
65
+ backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
66
+ borderRadius: dim.borderRadius.sysRadiusXxl,
67
+ },
68
+ sheetFull: {
69
+ backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
70
+ borderTopLeftRadius: dim.borderRadius.sysRadiusXxl,
71
+ borderTopRightRadius: dim.borderRadius.sysRadiusXxl,
72
+ },
73
+ titleRow: {
74
+ paddingHorizontal: dim.spacing.padding.sysPadding16,
75
+ paddingVertical: dim.spacing.padding.sysPadding8,
76
+ },
77
+ closeButton: {
78
+ borderRadius: dim.borderRadius.sysRadiusFull,
79
+ backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainer,
80
+ },
81
+ contentInner: {
82
+ padding: dim.spacing.padding.sysPadding16,
83
+ },
84
+ actions: {
85
+ padding: dim.spacing.padding.sysPadding24,
86
+ gap: dim.spacing.padding.sysPadding12,
87
+ },
88
+ }),
89
+ [cr, dim],
90
+ );
91
+
92
+ const closeButton = (
93
+ <Pressable
94
+ onPress={onClose}
95
+ accessibilityRole="button"
96
+ accessibilityLabel="Close"
97
+ style={({ pressed }) => [
98
+ styles.closeButtonSize,
99
+ dynamicStyles.closeButton,
100
+ pressed && styles.closePressed,
101
+ ]}
102
+ >
103
+ <Ionicons
104
+ name="close"
105
+ size={20}
106
+ color={cr.surface.surface.sysOnSurface}
107
+ />
108
+ </Pressable>
109
+ );
110
+
45
111
  return (
46
112
  <Modal
47
113
  visible={visible}
@@ -53,9 +119,24 @@ export function ActionSheet({
53
119
  {/* Backdrop */}
54
120
  <Pressable style={styles.backdrop} onPress={onClose} accessible={false} />
55
121
 
56
- {/* Sheet */}
57
- <View style={[styles.sheetWrapper, style]} pointerEvents="box-none">
58
- <View style={styles.sheet}>
122
+ {/* Sheet wrapper */}
123
+ <View
124
+ style={[
125
+ styles.sheetWrapper,
126
+ isFull
127
+ ? dynamicStyles.sheetWrapperFull
128
+ : dynamicStyles.sheetWrapperHalf,
129
+ style,
130
+ ]}
131
+ pointerEvents="box-none"
132
+ >
133
+ <View
134
+ style={[
135
+ styles.sheet,
136
+ isFull ? styles.sheetFull : styles.sheetHalf,
137
+ isFull ? dynamicStyles.sheetFull : dynamicStyles.sheetHalf,
138
+ ]}
139
+ >
59
140
  {/* Toolbar */}
60
141
  <View style={styles.toolbar}>
61
142
  {/* Grabber */}
@@ -64,9 +145,9 @@ export function ActionSheet({
64
145
  </View>
65
146
 
66
147
  {/* Title row */}
67
- <View style={styles.titleRow}>
68
- {/* Invisible spacer to balance the close button */}
69
- <View style={styles.closeButton} />
148
+ <View style={[styles.titleRow, dynamicStyles.titleRow]}>
149
+ {/* Left slot: close button (full) or invisible spacer (half) */}
150
+ {isFull ? closeButton : <View style={styles.closeButtonSize} />}
70
151
 
71
152
  <HeaderText
72
153
  variant="titleSmall"
@@ -77,34 +158,16 @@ export function ActionSheet({
77
158
  {title ?? ''}
78
159
  </HeaderText>
79
160
 
80
- {/* Close button */}
81
- <Pressable
82
- onPress={onClose}
83
- accessibilityRole="button"
84
- accessibilityLabel="Close"
85
- style={({ pressed }) => [
86
- styles.closeButton,
87
- {
88
- backgroundColor:
89
- cr.surface.surfaceContainer.sysSurfaceContainer,
90
- },
91
- pressed && styles.closePresseed,
92
- ]}
93
- >
94
- <Ionicons
95
- name="close"
96
- size={20}
97
- color={cr.surface.surface.sysOnSurface}
98
- />
99
- </Pressable>
161
+ {/* Right close button — always shown */}
162
+ {closeButton}
100
163
  </View>
101
164
  </View>
102
165
 
103
166
  {/* Content */}
104
167
  {children && (
105
168
  <ScrollView
106
- style={styles.content}
107
- contentContainerStyle={styles.contentInner}
169
+ style={isFull ? styles.contentFull : styles.contentHalf}
170
+ contentContainerStyle={dynamicStyles.contentInner}
108
171
  showsVerticalScrollIndicator={false}
109
172
  >
110
173
  {children}
@@ -113,7 +176,7 @@ export function ActionSheet({
113
176
 
114
177
  {/* Actions */}
115
178
  {(primaryAction || secondaryAction) && (
116
- <View style={styles.actions}>
179
+ <View style={dynamicStyles.actions}>
117
180
  {primaryAction && (
118
181
  <Button
119
182
  label={primaryAction.label}
@@ -140,40 +203,57 @@ export function ActionSheet({
140
203
  );
141
204
  }
142
205
 
143
- const { colorRoles: cr2, dimensions: dim2 } = sys;
144
-
145
206
  const styles = StyleSheet.create({
146
207
  backdrop: {
147
- ...StyleSheet.absoluteFillObject,
208
+ position: 'absolute',
209
+ top: 0,
210
+ left: 0,
211
+ right: 0,
212
+ bottom: 0,
148
213
  backgroundColor: 'rgba(0,0,0,0.4)',
149
214
  },
150
215
  sheetWrapper: {
151
216
  flex: 1,
152
217
  justifyContent: 'flex-end',
153
- paddingHorizontal: dim2.spacing.padding.sysPadding6,
154
- paddingBottom: dim2.spacing.padding.sysPadding6,
155
218
  },
219
+ // ── half ────────────────────────────────────────────────────────────────────
156
220
  sheet: {
157
- backgroundColor: cr2.surface.surfaceContainer.sysSurfaceContainerLowest,
158
- borderRadius: dim2.borderRadius.sysRadiusXxl,
159
221
  borderWidth: 1,
160
222
  borderColor: 'rgba(255,255,255,0.4)',
161
223
  overflow: 'hidden',
224
+ },
225
+ sheetHalf: {
162
226
  shadowColor: '#000',
163
227
  shadowOffset: { width: 0, height: 4 },
164
228
  shadowOpacity: 0.1,
165
229
  shadowRadius: 16,
166
230
  elevation: 8,
167
231
  },
232
+ contentHalf: {
233
+ maxHeight: 400,
234
+ },
235
+ // ── full ────────────────────────────────────────────────────────────────────
236
+ sheetFull: {
237
+ flex: 1,
238
+ shadowColor: '#000',
239
+ shadowOffset: { width: 0, height: 15 },
240
+ shadowOpacity: 0.18,
241
+ shadowRadius: 75,
242
+ elevation: 16,
243
+ },
244
+ contentFull: {
245
+ flex: 1,
246
+ },
247
+ // ── shared ──────────────────────────────────────────────────────────────────
168
248
  toolbar: {
169
249
  alignItems: 'center',
170
250
  width: '100%',
171
251
  },
172
252
  grabberRow: {
253
+ width: '100%',
173
254
  height: 16,
174
- justifyContent: 'flex-end',
255
+ justifyContent: 'flex-start',
175
256
  alignItems: 'center',
176
- paddingBottom: 0,
177
257
  paddingTop: 5,
178
258
  },
179
259
  grabber: {
@@ -186,32 +266,19 @@ const styles = StyleSheet.create({
186
266
  flexDirection: 'row',
187
267
  alignItems: 'center',
188
268
  justifyContent: 'space-between',
189
- paddingHorizontal: dim2.spacing.padding.sysPadding16,
190
- paddingVertical: dim2.spacing.padding.sysPadding8,
191
269
  width: '100%',
192
270
  },
193
271
  titleText: {
194
272
  flex: 1,
195
273
  textAlign: 'center',
196
274
  },
197
- closeButton: {
275
+ closeButtonSize: {
198
276
  width: 32,
199
277
  height: 32,
200
- borderRadius: dim2.borderRadius.sysRadiusFull,
201
278
  alignItems: 'center',
202
279
  justifyContent: 'center',
203
280
  },
204
- closePresseed: {
281
+ closePressed: {
205
282
  opacity: 0.7,
206
283
  },
207
- content: {
208
- maxHeight: 400,
209
- },
210
- contentInner: {
211
- padding: dim2.spacing.padding.sysPadding16,
212
- },
213
- actions: {
214
- padding: dim2.spacing.padding.sysPadding24,
215
- gap: dim2.spacing.padding.sysPadding12,
216
- },
217
284
  });
@@ -11,7 +11,7 @@ import {
11
11
 
12
12
  import { Ionicons } from '@expo/vector-icons';
13
13
 
14
- import { sys } from '../../tokens';
14
+ import { useTheme } from '../../theme';
15
15
 
16
16
  export type AlertVariant =
17
17
  | 'default'
@@ -43,8 +43,6 @@ export interface AlertProps {
43
43
  style?: StyleProp<ViewStyle>;
44
44
  }
45
45
 
46
- const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
47
-
48
46
  // ── Variant color map ────────────────────────────────────────────────────────
49
47
 
50
48
  type VariantColors = {
@@ -55,7 +53,12 @@ type VariantColors = {
55
53
  actionBorder: boolean;
56
54
  };
57
55
 
58
- function getVariantColors(variant: AlertVariant): VariantColors {
56
+ type ColorRoles = ReturnType<typeof useTheme>['colorRoles'];
57
+
58
+ function getVariantColors(
59
+ variant: AlertVariant,
60
+ cr: ColorRoles,
61
+ ): VariantColors {
59
62
  switch (variant) {
60
63
  case 'default':
61
64
  return {
@@ -143,7 +146,8 @@ export function Alert({
143
146
  onDismiss,
144
147
  style,
145
148
  }: AlertProps) {
146
- const c = getVariantColors(variant);
149
+ const { colorRoles: cr, dimensions: dim, typeScale: ts } = useTheme();
150
+ const c = getVariantColors(variant, cr);
147
151
  const isLg = size === 'lg';
148
152
 
149
153
  const resolvedIcon =
@@ -246,7 +250,14 @@ export function Alert({
246
250
  accessibilityLabel={actionLabel}
247
251
  style={({ pressed }) => [
248
252
  styles.actionLg,
249
- c.actionBorder && styles.actionLgBorder,
253
+ {
254
+ backgroundColor:
255
+ cr.surface.surfaceContainer.sysSurfaceContainerLowest,
256
+ },
257
+ c.actionBorder && [
258
+ styles.actionLgBorderBase,
259
+ { borderColor: cr.outline.sysOutline },
260
+ ],
250
261
  pressed && { opacity: 0.84 },
251
262
  ]}
252
263
  >
@@ -332,12 +343,11 @@ const styles = StyleSheet.create({
332
343
  flex: 1,
333
344
  minWidth: 0,
334
345
  },
335
- // lg action button — elevated pill
346
+ // lg action button — elevated pill (backgroundColor and borderColor applied inline)
336
347
  actionLg: {
337
348
  height: 40,
338
349
  paddingHorizontal: 24,
339
350
  borderRadius: 9999,
340
- backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
341
351
  alignItems: 'center',
342
352
  justifyContent: 'center',
343
353
  flexShrink: 0,
@@ -347,9 +357,8 @@ const styles = StyleSheet.create({
347
357
  shadowRadius: 4,
348
358
  elevation: 1,
349
359
  },
350
- actionLgBorder: {
360
+ actionLgBorderBase: {
351
361
  borderWidth: 1,
352
- borderColor: cr.outline.sysOutline,
353
362
  },
354
363
  // sm action button — text only
355
364
  actionSm: {
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
 
3
3
  import {
4
4
  Image,
@@ -12,7 +12,7 @@ import {
12
12
 
13
13
  import { Ionicons } from '@expo/vector-icons';
14
14
 
15
- import { sys } from '../../tokens';
15
+ import { useTheme } from '../../theme';
16
16
 
17
17
  export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
18
18
  export type AvatarVariant = 'text' | 'image' | 'icon';
@@ -35,95 +35,6 @@ export interface AvatarProps {
35
35
  style?: StyleProp<ViewStyle>;
36
36
  }
37
37
 
38
- const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
39
-
40
- const SIZE_TOKENS = {
41
- xs: {
42
- diameter: 24,
43
- fontSize: ts.labelSmall.sysFontSize,
44
- lineHeight: ts.labelSmall.sysLineHeight,
45
- fontWeight: '400' as const,
46
- iconSize: 12,
47
- ringBorder: dim.borderWidth.sysStrokeMedium,
48
- ringInset: -1,
49
- badgeSize: 0, // xs has no presence badge
50
- badgeIconSize: 0,
51
- badgeOffset: 0,
52
- },
53
- sm: {
54
- diameter: 32,
55
- fontSize: ts.labelMedium.sysFontSize,
56
- lineHeight: ts.labelMedium.sysLineHeight,
57
- fontWeight: '400' as const,
58
- iconSize: 16,
59
- ringBorder: dim.borderWidth.sysStrokeMedium,
60
- ringInset: -2,
61
- badgeSize: 16,
62
- badgeIconSize: 12,
63
- badgeOffset: -4,
64
- },
65
- md: {
66
- diameter: 40,
67
- fontSize: ts.labelLarge.sysFontSize,
68
- lineHeight: ts.labelLarge.sysLineHeight,
69
- fontWeight: '500' as const,
70
- iconSize: 20,
71
- ringBorder: dim.borderWidth.sysStrokeThick,
72
- ringInset: -2,
73
- badgeSize: 20,
74
- badgeIconSize: 16,
75
- badgeOffset: -4,
76
- },
77
- lg: {
78
- diameter: 48,
79
- fontSize: ts.labelLarge.sysFontSize,
80
- lineHeight: ts.labelLarge.sysLineHeight,
81
- fontWeight: '500' as const,
82
- iconSize: 24,
83
- ringBorder: dim.borderWidth.sysStrokeThick,
84
- ringInset: -2,
85
- badgeSize: 20,
86
- badgeIconSize: 16,
87
- badgeOffset: -4,
88
- },
89
- xl: {
90
- diameter: 64,
91
- fontSize: ts.labelLarge.sysFontSize,
92
- lineHeight: ts.labelLarge.sysLineHeight,
93
- fontWeight: '500' as const,
94
- iconSize: 32,
95
- ringBorder: dim.borderWidth.sysStrokeThick,
96
- ringInset: -2,
97
- badgeSize: 24,
98
- badgeIconSize: 20,
99
- badgeOffset: -4,
100
- },
101
- '2xl': {
102
- diameter: 88,
103
- fontSize: ts.titleMedium.sysFontSize,
104
- lineHeight: ts.titleMedium.sysLineHeight,
105
- fontWeight: '400' as const,
106
- iconSize: 44,
107
- ringBorder: dim.borderWidth.sysStrokeBold,
108
- ringInset: -2,
109
- badgeSize: 32,
110
- badgeIconSize: 24,
111
- badgeOffset: 0,
112
- },
113
- '3xl': {
114
- diameter: 120,
115
- fontSize: ts.titleLarge.sysFontSize,
116
- lineHeight: ts.titleLarge.sysLineHeight,
117
- fontWeight: '500' as const,
118
- iconSize: 60,
119
- ringBorder: dim.borderWidth.sysStrokeBold,
120
- ringInset: -2,
121
- badgeSize: 32,
122
- badgeIconSize: 24,
123
- badgeOffset: 0,
124
- },
125
- };
126
-
127
38
  export function Avatar({
128
39
  variant = 'text',
129
40
  size = 'md',
@@ -135,7 +46,99 @@ export function Avatar({
135
46
  accessibilityLabel,
136
47
  style,
137
48
  }: AvatarProps) {
138
- const s = SIZE_TOKENS[size];
49
+ const { colorRoles: cr, dimensions: dim, typeScale: ts } = useTheme();
50
+
51
+ const sizeTokens = useMemo(
52
+ () => ({
53
+ xs: {
54
+ diameter: 24,
55
+ fontSize: ts.labelSmall.sysFontSize,
56
+ lineHeight: ts.labelSmall.sysLineHeight,
57
+ fontWeight: '400' as const,
58
+ iconSize: 12,
59
+ ringBorder: dim.borderWidth.sysStrokeMedium,
60
+ ringInset: -1,
61
+ badgeSize: 0,
62
+ badgeIconSize: 0,
63
+ badgeOffset: 0,
64
+ },
65
+ sm: {
66
+ diameter: 32,
67
+ fontSize: ts.labelMedium.sysFontSize,
68
+ lineHeight: ts.labelMedium.sysLineHeight,
69
+ fontWeight: '400' as const,
70
+ iconSize: 16,
71
+ ringBorder: dim.borderWidth.sysStrokeMedium,
72
+ ringInset: -2,
73
+ badgeSize: 16,
74
+ badgeIconSize: 12,
75
+ badgeOffset: -4,
76
+ },
77
+ md: {
78
+ diameter: 40,
79
+ fontSize: ts.labelLarge.sysFontSize,
80
+ lineHeight: ts.labelLarge.sysLineHeight,
81
+ fontWeight: '500' as const,
82
+ iconSize: 20,
83
+ ringBorder: dim.borderWidth.sysStrokeThick,
84
+ ringInset: -2,
85
+ badgeSize: 20,
86
+ badgeIconSize: 16,
87
+ badgeOffset: -4,
88
+ },
89
+ lg: {
90
+ diameter: 48,
91
+ fontSize: ts.labelLarge.sysFontSize,
92
+ lineHeight: ts.labelLarge.sysLineHeight,
93
+ fontWeight: '500' as const,
94
+ iconSize: 24,
95
+ ringBorder: dim.borderWidth.sysStrokeThick,
96
+ ringInset: -2,
97
+ badgeSize: 20,
98
+ badgeIconSize: 16,
99
+ badgeOffset: -4,
100
+ },
101
+ xl: {
102
+ diameter: 64,
103
+ fontSize: ts.labelLarge.sysFontSize,
104
+ lineHeight: ts.labelLarge.sysLineHeight,
105
+ fontWeight: '500' as const,
106
+ iconSize: 32,
107
+ ringBorder: dim.borderWidth.sysStrokeThick,
108
+ ringInset: -2,
109
+ badgeSize: 24,
110
+ badgeIconSize: 20,
111
+ badgeOffset: -4,
112
+ },
113
+ '2xl': {
114
+ diameter: 88,
115
+ fontSize: ts.titleMedium.sysFontSize,
116
+ lineHeight: ts.titleMedium.sysLineHeight,
117
+ fontWeight: '400' as const,
118
+ iconSize: 44,
119
+ ringBorder: dim.borderWidth.sysStrokeBold,
120
+ ringInset: -2,
121
+ badgeSize: 32,
122
+ badgeIconSize: 24,
123
+ badgeOffset: 0,
124
+ },
125
+ '3xl': {
126
+ diameter: 120,
127
+ fontSize: ts.titleLarge.sysFontSize,
128
+ lineHeight: ts.titleLarge.sysLineHeight,
129
+ fontWeight: '500' as const,
130
+ iconSize: 60,
131
+ ringBorder: dim.borderWidth.sysStrokeBold,
132
+ ringInset: -2,
133
+ badgeSize: 32,
134
+ badgeIconSize: 24,
135
+ badgeOffset: 0,
136
+ },
137
+ }),
138
+ [dim, ts],
139
+ );
140
+
141
+ const s = sizeTokens[size];
139
142
  const r = s.diameter / 2;
140
143
 
141
144
  const defaultLabel = variant === 'text' ? `Avatar: ${initials}` : 'Avatar';
@@ -1,8 +1,8 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
 
3
3
  import { StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native';
4
4
 
5
- import { sys } from '../../tokens';
5
+ import { useTheme } from '../../theme';
6
6
 
7
7
  export type BadgeSize = 'sm' | 'md' | 'lg';
8
8
  export type BadgeStyle =
@@ -21,37 +21,6 @@ export interface BadgeProps {
21
21
  style?: StyleProp<ViewStyle>;
22
22
  }
23
23
 
24
- const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
25
-
26
- // ── Size tokens ───────────────────────────────────────────────────────────────
27
-
28
- const SIZE_TOKENS = {
29
- sm: {
30
- outerSize: 16,
31
- paddingH: dim.spacing.padding.sysPadding4,
32
- fontSize: ts.labelSmall.sysFontSize,
33
- lineHeight: ts.labelSmall.sysLineHeight,
34
- letterSpacing: ts.labelSmall.sysTracking,
35
- dotInner: 6,
36
- },
37
- md: {
38
- outerSize: 20,
39
- paddingH: dim.spacing.padding.sysPadding8,
40
- fontSize: ts.labelMedium.sysFontSize,
41
- lineHeight: ts.labelMedium.sysLineHeight,
42
- letterSpacing: ts.labelMedium.sysTracking,
43
- dotInner: 8,
44
- },
45
- lg: {
46
- outerSize: 24,
47
- paddingH: dim.spacing.padding.sysPadding8,
48
- fontSize: ts.labelMedium.sysFontSize,
49
- lineHeight: ts.labelMedium.sysLineHeight,
50
- letterSpacing: ts.labelMedium.sysTracking,
51
- dotInner: 12,
52
- },
53
- };
54
-
55
24
  // ── Style/color tokens ────────────────────────────────────────────────────────
56
25
 
57
26
  type StyleColors = {
@@ -60,7 +29,9 @@ type StyleColors = {
60
29
  elevated: boolean;
61
30
  };
62
31
 
63
- function getStyleColors(style: BadgeStyle): StyleColors {
32
+ type ColorRoles = ReturnType<typeof useTheme>['colorRoles'];
33
+
34
+ function getStyleColors(style: BadgeStyle, cr: ColorRoles): StyleColors {
64
35
  switch (style) {
65
36
  case 'filled':
66
37
  return {
@@ -110,8 +81,40 @@ export function Badge({
110
81
  badgeStyle = 'filled',
111
82
  style,
112
83
  }: BadgeProps) {
113
- const s = SIZE_TOKENS[size];
114
- const c = getStyleColors(badgeStyle);
84
+ const { colorRoles: cr, dimensions: dim, typeScale: ts } = useTheme();
85
+
86
+ const sizeTokens = useMemo(
87
+ () => ({
88
+ sm: {
89
+ outerSize: 16,
90
+ paddingH: dim.spacing.padding.sysPadding4,
91
+ fontSize: ts.labelSmall.sysFontSize,
92
+ lineHeight: ts.labelSmall.sysLineHeight,
93
+ letterSpacing: ts.labelSmall.sysTracking,
94
+ dotInner: 6,
95
+ },
96
+ md: {
97
+ outerSize: 20,
98
+ paddingH: dim.spacing.padding.sysPadding8,
99
+ fontSize: ts.labelMedium.sysFontSize,
100
+ lineHeight: ts.labelMedium.sysLineHeight,
101
+ letterSpacing: ts.labelMedium.sysTracking,
102
+ dotInner: 8,
103
+ },
104
+ lg: {
105
+ outerSize: 24,
106
+ paddingH: dim.spacing.padding.sysPadding8,
107
+ fontSize: ts.labelMedium.sysFontSize,
108
+ lineHeight: ts.labelMedium.sysLineHeight,
109
+ letterSpacing: ts.labelMedium.sysTracking,
110
+ dotInner: 12,
111
+ },
112
+ }),
113
+ [dim, ts],
114
+ );
115
+
116
+ const s = sizeTokens[size];
117
+ const c = getStyleColors(badgeStyle, cr);
115
118
 
116
119
  // Dot variant — render a solid filled circle inside a transparent wrapper
117
120
  if (badgeStyle === 'dot') {