@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
@@ -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
 
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 EmptyStateStyle = 'icon' | 'illustration';
17
17
  export type EmptyStateViewport = 'desktop' | 'mobile';
@@ -37,45 +37,6 @@ export interface EmptyStateProps {
37
37
  containerStyle?: StyleProp<ViewStyle>;
38
38
  }
39
39
 
40
- const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
41
-
42
- // ── Size tokens by viewport ───────────────────────────────────────────────────
43
-
44
- const VIEWPORT = {
45
- desktop: {
46
- iconCircle: 60,
47
- iconSize: 32,
48
- titleFontSize: ts.titleMedium.sysFontSize,
49
- titleLineHeight: ts.titleMedium.sysLineHeight,
50
- descFontSize: ts.bodyMedium.sysFontSize,
51
- descLineHeight: ts.bodyMedium.sysLineHeight,
52
- contentGap: dim.spacing.padding.sysPadding24,
53
- btnHeight: 40,
54
- btnPaddingL: dim.spacing.padding.sysPadding16,
55
- btnPaddingR: dim.spacing.padding.sysPadding24,
56
- btnGap: dim.spacing.padding.sysPadding8,
57
- btnFontSize: ts.labelMedium.sysFontSize,
58
- btnLineHeight: ts.labelMedium.sysLineHeight,
59
- btnTracking: ts.labelMedium.sysTracking,
60
- },
61
- mobile: {
62
- iconCircle: 48,
63
- iconSize: 24,
64
- titleFontSize: ts.labelLarge.sysFontSize,
65
- titleLineHeight: ts.labelLarge.sysLineHeight,
66
- descFontSize: ts.bodySmall.sysFontSize,
67
- descLineHeight: ts.bodySmall.sysLineHeight,
68
- contentGap: dim.spacing.padding.sysPadding16,
69
- btnHeight: 32,
70
- btnPaddingL: dim.spacing.padding.sysPadding12,
71
- btnPaddingR: dim.spacing.padding.sysPadding16,
72
- btnGap: dim.spacing.padding.sysPadding4,
73
- btnFontSize: ts.labelSmall.sysFontSize,
74
- btnLineHeight: ts.labelSmall.sysLineHeight,
75
- btnTracking: ts.labelSmall.sysTracking,
76
- },
77
- } as const;
78
-
79
40
  // ── Illustration ──────────────────────────────────────────────────────────────
80
41
  // A pure-View recreation of the Figma "collage of cards" illustration.
81
42
  // Built at 280×157 px (scaled from the Figma 398×224 desktop version).
@@ -94,7 +55,10 @@ function scaledPos(left: number, top: number) {
94
55
  };
95
56
  }
96
57
 
97
- function EmptyIllustration() {
58
+ type ColorRoles = ReturnType<typeof useTheme>['colorRoles'];
59
+ type Dimensions = ReturnType<typeof useTheme>['dimensions'];
60
+
61
+ function EmptyIllustration({ cr, dim }: { cr: ColorRoles; dim: Dimensions }) {
98
62
  // Purely decorative — hidden from accessibility tree
99
63
  // Central active card
100
64
  const central = scaledPos(119, 63);
@@ -125,6 +89,8 @@ function EmptyIllustration() {
125
89
  top: pos.top,
126
90
  width: CARD_W,
127
91
  height: CARD_H,
92
+ borderWidth: dim.borderWidth.sysStrokeThin,
93
+ borderRadius: dim.borderRadius.sysRadiusMd,
128
94
  borderColor: cr.outline.sysOutlineVariant,
129
95
  backgroundColor:
130
96
  cr.surface.surfaceContainer.sysSurfaceContainerLowest,
@@ -144,9 +110,12 @@ function EmptyIllustration() {
144
110
  top: central.top,
145
111
  width: CARD_W,
146
112
  height: CARD_H,
113
+ borderWidth: dim.borderWidth.sysStrokeThin,
114
+ borderRadius: dim.borderRadius.sysRadiusMd,
147
115
  borderColor: cr.accent.primary.sysPrimary,
148
116
  backgroundColor:
149
117
  cr.surface.surfaceContainer.sysSurfaceContainerLowest,
118
+ shadowColor: cr.accent.primary.sysPrimary,
150
119
  },
151
120
  ]}
152
121
  >
@@ -158,6 +127,7 @@ function EmptyIllustration() {
158
127
  top: Math.round(15 * SCALE),
159
128
  left: Math.round(15 * SCALE),
160
129
  width: Math.round(32 * SCALE),
130
+ backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainer,
161
131
  },
162
132
  ]}
163
133
  />
@@ -168,6 +138,7 @@ function EmptyIllustration() {
168
138
  top: Math.round(51 * SCALE),
169
139
  left: Math.round(15 * SCALE),
170
140
  width: Math.round(96 * SCALE),
141
+ backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainer,
171
142
  },
172
143
  ]}
173
144
  />
@@ -178,6 +149,7 @@ function EmptyIllustration() {
178
149
  top: Math.round(63 * SCALE),
179
150
  left: Math.round(15 * SCALE),
180
151
  width: Math.round(122 * SCALE),
152
+ backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainer,
181
153
  },
182
154
  ]}
183
155
  />
@@ -200,14 +172,65 @@ export function EmptyState({
200
172
  showAction,
201
173
  containerStyle,
202
174
  }: EmptyStateProps) {
203
- const vp = VIEWPORT[viewport];
175
+ const { colorRoles: cr, dimensions: dim, typeScale: ts } = useTheme();
176
+
177
+ const viewportTokens = useMemo(
178
+ () =>
179
+ ({
180
+ desktop: {
181
+ iconCircle: 60,
182
+ iconSize: 32,
183
+ titleFontSize: ts.titleMedium.sysFontSize,
184
+ titleLineHeight: ts.titleMedium.sysLineHeight,
185
+ descFontSize: ts.bodyMedium.sysFontSize,
186
+ descLineHeight: ts.bodyMedium.sysLineHeight,
187
+ contentGap: dim.spacing.padding.sysPadding24,
188
+ btnHeight: 40,
189
+ btnPaddingL: dim.spacing.padding.sysPadding16,
190
+ btnPaddingR: dim.spacing.padding.sysPadding24,
191
+ btnGap: dim.spacing.padding.sysPadding8,
192
+ btnFontSize: ts.labelMedium.sysFontSize,
193
+ btnLineHeight: ts.labelMedium.sysLineHeight,
194
+ btnTracking: ts.labelMedium.sysTracking,
195
+ },
196
+ mobile: {
197
+ iconCircle: 48,
198
+ iconSize: 24,
199
+ titleFontSize: ts.labelLarge.sysFontSize,
200
+ titleLineHeight: ts.labelLarge.sysLineHeight,
201
+ descFontSize: ts.bodySmall.sysFontSize,
202
+ descLineHeight: ts.bodySmall.sysLineHeight,
203
+ contentGap: dim.spacing.padding.sysPadding16,
204
+ btnHeight: 32,
205
+ btnPaddingL: dim.spacing.padding.sysPadding12,
206
+ btnPaddingR: dim.spacing.padding.sysPadding16,
207
+ btnGap: dim.spacing.padding.sysPadding4,
208
+ btnFontSize: ts.labelSmall.sysFontSize,
209
+ btnLineHeight: ts.labelSmall.sysLineHeight,
210
+ btnTracking: ts.labelSmall.sysTracking,
211
+ },
212
+ }) as const,
213
+ [dim, ts],
214
+ );
215
+
216
+ const vp = viewportTokens[viewport];
204
217
  const hasAction = showAction !== undefined ? showAction : !!actionLabel;
205
218
 
206
219
  return (
207
- <View style={[styles.root, containerStyle]}>
220
+ <View
221
+ style={[
222
+ styles.root,
223
+ {
224
+ padding: dim.spacing.padding.sysPadding24,
225
+ borderRadius: dim.borderRadius.sysRadiusLg,
226
+ gap: dim.spacing.padding.sysPadding24,
227
+ },
228
+ containerStyle,
229
+ ]}
230
+ >
208
231
  {/* ── Graphic ───────────────────────────────────────────────────────── */}
209
232
  {style === 'illustration' ? (
210
- <EmptyIllustration />
233
+ <EmptyIllustration cr={cr} dim={dim} />
211
234
  ) : (
212
235
  <View
213
236
  style={[
@@ -233,7 +256,9 @@ export function EmptyState({
233
256
  {/* ── Content ───────────────────────────────────────────────────────── */}
234
257
  <View style={[styles.content, { gap: vp.contentGap }]}>
235
258
  {/* Text block */}
236
- <View style={styles.textBlock}>
259
+ <View
260
+ style={[styles.textBlock, { gap: dim.spacing.padding.sysPadding4 }]}
261
+ >
237
262
  <Text
238
263
  style={{
239
264
  color: cr.surface.surface.sysOnSurface,
@@ -276,6 +301,7 @@ export function EmptyState({
276
301
  paddingLeft: vp.btnPaddingL,
277
302
  paddingRight: vp.btnPaddingR,
278
303
  gap: vp.btnGap,
304
+ borderWidth: dim.borderWidth.sysStrokeThin,
279
305
  borderColor: cr.outline.sysOutline,
280
306
  },
281
307
  pressed && { opacity: 0.76 },
@@ -307,10 +333,7 @@ export function EmptyState({
307
333
 
308
334
  const styles = StyleSheet.create({
309
335
  root: {
310
- padding: dim.spacing.padding.sysPadding24,
311
- borderRadius: dim.borderRadius.sysRadiusLg,
312
336
  alignItems: 'center',
313
- gap: dim.spacing.padding.sysPadding24,
314
337
  alignSelf: 'center',
315
338
  },
316
339
 
@@ -329,7 +352,6 @@ const styles = StyleSheet.create({
329
352
  },
330
353
  textBlock: {
331
354
  alignSelf: 'stretch',
332
- gap: dim.spacing.padding.sysPadding4,
333
355
  alignItems: 'center',
334
356
  },
335
357
 
@@ -338,7 +360,6 @@ const styles = StyleSheet.create({
338
360
  flexDirection: 'row',
339
361
  alignItems: 'center',
340
362
  justifyContent: 'center',
341
- borderWidth: dim.borderWidth.sysStrokeThin,
342
363
  borderRadius: 9999,
343
364
  overflow: 'hidden',
344
365
  },
@@ -350,8 +371,6 @@ const styles = StyleSheet.create({
350
371
  },
351
372
  card: {
352
373
  position: 'absolute',
353
- borderWidth: dim.borderWidth.sysStrokeThin,
354
- borderRadius: dim.borderRadius.sysRadiusMd,
355
374
  overflow: 'hidden',
356
375
  },
357
376
  cardFaded: {
@@ -359,7 +378,6 @@ const styles = StyleSheet.create({
359
378
  },
360
379
  cardActive: {
361
380
  // Active ring: sysPrimary08 spread shadow
362
- shadowColor: cr.accent.primary.sysPrimary,
363
381
  shadowOffset: { width: 0, height: 0 },
364
382
  shadowOpacity: 0.08,
365
383
  shadowRadius: 0,
@@ -369,6 +387,5 @@ const styles = StyleSheet.create({
369
387
  position: 'absolute',
370
388
  height: Math.round(6 * SCALE),
371
389
  borderRadius: 9999,
372
- backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainer,
373
390
  },
374
391
  });
@@ -4,7 +4,7 @@ import { Text, type TextProps } from 'react-native';
4
4
 
5
5
  import { useFonts } from 'expo-font';
6
6
 
7
- import { sys } from '../../tokens';
7
+ import { useTheme } from '../../theme';
8
8
 
9
9
  export type HeaderVariant =
10
10
  | 'large'
@@ -29,7 +29,7 @@ const FONT_FAMILY: Record<string, string> = {
29
29
  semibold: 'GoogleSans_600SemiBold',
30
30
  };
31
31
 
32
- const VARIANT_TOKEN_KEY: Record<HeaderVariant, keyof typeof sys.typeScale> = {
32
+ const VARIANT_TOKEN_KEY: Record<HeaderVariant, string> = {
33
33
  large: 'displayLarge',
34
34
  medium: 'displayMedium',
35
35
  small: 'displaySmall',
@@ -41,8 +41,6 @@ const VARIANT_TOKEN_KEY: Record<HeaderVariant, keyof typeof sys.typeScale> = {
41
41
  titleSmall: 'titleSmall',
42
42
  };
43
43
 
44
- const { typeScale: ts } = sys;
45
-
46
44
  export function HeaderText({
47
45
  variant = 'medium',
48
46
  emphasized = false,
@@ -50,13 +48,15 @@ export function HeaderText({
50
48
  style,
51
49
  ...rest
52
50
  }: HeaderTextProps) {
51
+ const { typeScale: ts } = useTheme();
52
+
53
53
  const [fontsLoaded] = useFonts({
54
54
  GoogleSans_400Regular: require('../../fonts/GoogleSans_400Regular.ttf'),
55
55
  GoogleSans_500Medium: require('../../fonts/GoogleSans_500Medium.ttf'),
56
56
  GoogleSans_600SemiBold: require('../../fonts/GoogleSans_600SemiBold.ttf'),
57
57
  });
58
58
 
59
- const token = ts[VARIANT_TOKEN_KEY[variant]];
59
+ const token = ts[VARIANT_TOKEN_KEY[variant] as keyof typeof ts];
60
60
  const weight = emphasized
61
61
  ? token.sysFontWeightEmphasized
62
62
  : token.sysFontWeight;
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
1
+ import React, { useMemo, useState } from 'react';
2
2
 
3
3
  import {
4
4
  StyleSheet,
@@ -8,7 +8,7 @@ import {
8
8
  View,
9
9
  } from 'react-native';
10
10
 
11
- import { sys } from '../../tokens';
11
+ import { useTheme } from '../../theme';
12
12
 
13
13
  export type InputSize = 'sm' | 'md' | 'lg';
14
14
 
@@ -22,41 +22,6 @@ export interface InputProps extends Omit<TextInputProps, 'style'> {
22
22
  trailingIcon?: React.ReactNode;
23
23
  }
24
24
 
25
- const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
26
-
27
- const SIZE_TOKENS = {
28
- sm: {
29
- height: 40,
30
- paddingH: dim.spacing.padding.sysPadding12,
31
- paddingV: dim.spacing.padding.sysPadding8,
32
- gap: dim.spacing.padding.sysPadding4,
33
- fontSize: ts.bodySmall.sysFontSize,
34
- lineHeight: ts.bodySmall.sysLineHeight,
35
- radius: dim.borderRadius.sysRadiusSm,
36
- iconSize: 20,
37
- },
38
- md: {
39
- height: 48,
40
- paddingH: dim.spacing.padding.sysPadding16,
41
- paddingV: dim.spacing.padding.sysPadding12,
42
- gap: dim.spacing.padding.sysPadding8,
43
- fontSize: ts.bodyMedium.sysFontSize,
44
- lineHeight: ts.bodyMedium.sysLineHeight,
45
- radius: dim.borderRadius.sysRadiusSm,
46
- iconSize: 20,
47
- },
48
- lg: {
49
- height: 56,
50
- paddingH: dim.spacing.padding.sysPadding20,
51
- paddingV: dim.spacing.padding.sysPadding16,
52
- gap: dim.spacing.padding.sysPadding8,
53
- fontSize: ts.bodyMedium.sysFontSize,
54
- lineHeight: ts.bodyMedium.sysLineHeight,
55
- radius: dim.borderRadius.sysRadiusMd,
56
- iconSize: 20,
57
- },
58
- };
59
-
60
25
  // Focus-ring halo: 4px spread, sysPrimary08 colour — achieved with a fixed
61
26
  // 4px-padded wrapper whose background switches on focus.
62
27
  const RING_SIZE = 4;
@@ -74,7 +39,45 @@ export function Input({
74
39
  onBlur,
75
40
  ...rest
76
41
  }: InputProps) {
42
+ const { colorRoles: cr, dimensions: dim, typeScale: ts } = useTheme();
77
43
  const [isFocused, setIsFocused] = useState(false);
44
+
45
+ const SIZE_TOKENS = useMemo(
46
+ () => ({
47
+ sm: {
48
+ height: 40,
49
+ paddingH: dim.spacing.padding.sysPadding12,
50
+ paddingV: dim.spacing.padding.sysPadding8,
51
+ gap: dim.spacing.padding.sysPadding4,
52
+ fontSize: ts.bodySmall.sysFontSize,
53
+ lineHeight: ts.bodySmall.sysLineHeight,
54
+ radius: dim.borderRadius.sysRadiusSm,
55
+ iconSize: 20,
56
+ },
57
+ md: {
58
+ height: 48,
59
+ paddingH: dim.spacing.padding.sysPadding16,
60
+ paddingV: dim.spacing.padding.sysPadding12,
61
+ gap: dim.spacing.padding.sysPadding8,
62
+ fontSize: ts.bodyMedium.sysFontSize,
63
+ lineHeight: ts.bodyMedium.sysLineHeight,
64
+ radius: dim.borderRadius.sysRadiusSm,
65
+ iconSize: 20,
66
+ },
67
+ lg: {
68
+ height: 56,
69
+ paddingH: dim.spacing.padding.sysPadding20,
70
+ paddingV: dim.spacing.padding.sysPadding16,
71
+ gap: dim.spacing.padding.sysPadding8,
72
+ fontSize: ts.bodyMedium.sysFontSize,
73
+ lineHeight: ts.bodyMedium.sysLineHeight,
74
+ radius: dim.borderRadius.sysRadiusMd,
75
+ iconSize: 20,
76
+ },
77
+ }),
78
+ [dim, ts],
79
+ );
80
+
78
81
  const s = SIZE_TOKENS[size];
79
82
  const isDisabled = editable === false;
80
83
  const hasError = invalid || !!errorText;
@@ -10,7 +10,7 @@ import {
10
10
 
11
11
  import { Ionicons } from '@expo/vector-icons';
12
12
 
13
- import { sys } from '../../tokens';
13
+ import { useTheme } from '../../theme';
14
14
  import { BodyText } from '../BodyText';
15
15
 
16
16
  export type ListItemType = 'simple' | 'detailed';
@@ -31,8 +31,6 @@ export interface ListProps {
31
31
  style?: StyleProp<ViewStyle>;
32
32
  }
33
33
 
34
- const { colorRoles: cr, dimensions: dim } = sys;
35
-
36
34
  // ── ListItem ──────────────────────────────────────────────────────────────────
37
35
 
38
36
  export function ListItem({
@@ -44,6 +42,7 @@ export function ListItem({
44
42
  disabled = false,
45
43
  showDivider = true,
46
44
  }: ListItemProps) {
45
+ const { colorRoles: cr, dimensions: dim } = useTheme();
47
46
  const isDetailed = type === 'detailed';
48
47
 
49
48
  const content = (
@@ -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
 
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 PaginationSize = 'sm' | 'lg';
17
17
 
@@ -42,41 +42,6 @@ export interface PaginationProps {
42
42
  style?: StyleProp<ViewStyle>;
43
43
  }
44
44
 
45
- const { colorRoles: cr, dimensions: dim, typeScale: ts } = sys;
46
-
47
- // ── Size tokens ───────────────────────────────────────────────────────────────
48
-
49
- const SIZE_TOKENS = {
50
- sm: {
51
- itemSize: 32,
52
- iconSize: 20,
53
- fontSize: ts.labelMedium.sysFontSize,
54
- lineHeight: ts.labelMedium.sysLineHeight,
55
- activeWeight: '500' as const,
56
- inactiveWeight: '400' as const,
57
- gap: dim.spacing.padding.sysPadding8,
58
- // Pill container: sm has NO outer pill
59
- hasPill: false,
60
- pillPaddingH: 0,
61
- pillPaddingV: 0,
62
- pillGap: dim.spacing.padding.sysPadding8,
63
- },
64
- lg: {
65
- itemSize: 40,
66
- iconSize: 24,
67
- fontSize: ts.labelLarge.sysFontSize,
68
- lineHeight: ts.labelLarge.sysLineHeight,
69
- activeWeight: '500' as const,
70
- inactiveWeight: '400' as const,
71
- gap: dim.spacing.padding.sysPadding16,
72
- // Pill container: lg wraps in pill border
73
- hasPill: true,
74
- pillPaddingH: dim.spacing.padding.sysPadding12,
75
- pillPaddingV: dim.spacing.padding.sysPadding8,
76
- pillGap: dim.spacing.padding.sysPadding16,
77
- },
78
- };
79
-
80
45
  // ── Pagination range logic ────────────────────────────────────────────────────
81
46
 
82
47
  function buildPageRange(
@@ -122,10 +87,11 @@ interface PageItemProps {
122
87
  page: number;
123
88
  isActive: boolean;
124
89
  onPress: () => void;
90
+ cr: ReturnType<typeof useTheme>['colorRoles'];
125
91
  }
126
92
 
127
- function PageItem({ s, page, isActive, onPress }: PageItemProps) {
128
- const borderWidth = isActive ? dim.borderWidth.sysStrokeMedium : 0;
93
+ function PageItem({ s, page, isActive, onPress, cr }: PageItemProps) {
94
+ const borderWidth = isActive ? 1.5 : 0;
129
95
  const borderColor = isActive ? cr.accent.primary.sysPrimary : 'transparent';
130
96
  const textColor = isActive
131
97
  ? cr.custom.info.sysOnInfoContainer
@@ -186,7 +152,12 @@ function PageItem({ s, page, isActive, onPress }: PageItemProps) {
186
152
 
187
153
  // ── Ellipsis item ─────────────────────────────────────────────────────────────
188
154
 
189
- function EllipsisItem({ s }: { s: SizeTokens }) {
155
+ interface EllipsisItemProps {
156
+ s: SizeTokens;
157
+ cr: ReturnType<typeof useTheme>['colorRoles'];
158
+ }
159
+
160
+ function EllipsisItem({ s, cr }: EllipsisItemProps) {
190
161
  return (
191
162
  <View
192
163
  accessible
@@ -220,9 +191,10 @@ interface NavButtonProps {
220
191
  direction: 'prev' | 'next';
221
192
  disabled: boolean;
222
193
  onPress: () => void;
194
+ cr: ReturnType<typeof useTheme>['colorRoles'];
223
195
  }
224
196
 
225
- function NavButton({ s, direction, disabled, onPress }: NavButtonProps) {
197
+ function NavButton({ s, direction, disabled, onPress, cr }: NavButtonProps) {
226
198
  const iconName = direction === 'prev' ? 'chevron-back' : 'chevron-forward';
227
199
 
228
200
  return (
@@ -269,7 +241,43 @@ export function Pagination({
269
241
  compact = false,
270
242
  style,
271
243
  }: PaginationProps) {
272
- const s = SIZE_TOKENS[size];
244
+ const { colorRoles: cr, dimensions: dim, typeScale: ts } = useTheme();
245
+
246
+ const sizeTokens = useMemo(
247
+ () => ({
248
+ sm: {
249
+ itemSize: 32,
250
+ iconSize: 20,
251
+ fontSize: ts.labelMedium.sysFontSize,
252
+ lineHeight: ts.labelMedium.sysLineHeight,
253
+ activeWeight: '500' as const,
254
+ inactiveWeight: '400' as const,
255
+ gap: dim.spacing.padding.sysPadding8,
256
+ // Pill container: sm has NO outer pill
257
+ hasPill: false,
258
+ pillPaddingH: 0,
259
+ pillPaddingV: 0,
260
+ pillGap: dim.spacing.padding.sysPadding8,
261
+ },
262
+ lg: {
263
+ itemSize: 40,
264
+ iconSize: 24,
265
+ fontSize: ts.labelLarge.sysFontSize,
266
+ lineHeight: ts.labelLarge.sysLineHeight,
267
+ activeWeight: '500' as const,
268
+ inactiveWeight: '400' as const,
269
+ gap: dim.spacing.padding.sysPadding16,
270
+ // Pill container: lg wraps in pill border
271
+ hasPill: true,
272
+ pillPaddingH: dim.spacing.padding.sysPadding12,
273
+ pillPaddingV: dim.spacing.padding.sysPadding8,
274
+ pillGap: dim.spacing.padding.sysPadding16,
275
+ },
276
+ }),
277
+ [ts, dim],
278
+ );
279
+
280
+ const s = sizeTokens[size];
273
281
  const pages = buildPageRange(totalPages, currentPage, siblingCount);
274
282
 
275
283
  const prevDisabled = currentPage <= 1;
@@ -283,12 +291,14 @@ export function Pagination({
283
291
  direction="prev"
284
292
  disabled={prevDisabled}
285
293
  onPress={() => onPageChange(currentPage - 1)}
294
+ cr={cr}
286
295
  />
287
296
  <NavButton
288
297
  s={s}
289
298
  direction="next"
290
299
  disabled={nextDisabled}
291
300
  onPress={() => onPageChange(currentPage + 1)}
301
+ cr={cr}
292
302
  />
293
303
  </View>
294
304
  ) : (
@@ -299,12 +309,13 @@ export function Pagination({
299
309
  direction="prev"
300
310
  disabled={prevDisabled}
301
311
  onPress={() => onPageChange(currentPage - 1)}
312
+ cr={cr}
302
313
  />
303
314
 
304
315
  <View style={[styles.pages, { gap: s.hasPill ? 0 : s.gap }]}>
305
316
  {pages.map((p, i) =>
306
317
  p === '...' ? (
307
- <EllipsisItem key={`ellipsis-${i}`} s={s} />
318
+ <EllipsisItem key={`ellipsis-${i}`} s={s} cr={cr} />
308
319
  ) : (
309
320
  <PageItem
310
321
  key={p}
@@ -312,6 +323,7 @@ export function Pagination({
312
323
  page={p}
313
324
  isActive={p === currentPage}
314
325
  onPress={() => onPageChange(p)}
326
+ cr={cr}
315
327
  />
316
328
  ),
317
329
  )}
@@ -322,6 +334,7 @@ export function Pagination({
322
334
  direction="next"
323
335
  disabled={nextDisabled}
324
336
  onPress={() => onPageChange(currentPage + 1)}
337
+ cr={cr}
325
338
  />
326
339
  </View>
327
340
  );