@compsych/mobile-ui 1.0.11 → 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.
- package/package.json +1 -1
- package/src/components/ActionSheet/index.tsx +124 -57
- package/src/components/Alert/index.tsx +19 -10
- package/src/components/Avatar/index.tsx +95 -92
- package/src/components/Badge/index.tsx +39 -36
- package/src/components/BodyText/index.tsx +5 -5
- package/src/components/Breadcrumb/index.tsx +32 -30
- package/src/components/Button/Button.test.tsx +13 -7
- package/src/components/Button/index.tsx +114 -92
- package/src/components/ChatBubble/ChatBubble.test.tsx +77 -0
- package/src/components/ChatBubble/index.tsx +187 -0
- package/src/components/ChatInput/ChatInput.test.tsx +90 -0
- package/src/components/ChatInput/index.tsx +263 -0
- package/src/components/Checkbox/index.tsx +24 -21
- package/src/components/Chip/index.tsx +75 -72
- package/src/components/Divider/index.tsx +3 -3
- package/src/components/EmptyState/index.tsx +72 -55
- package/src/components/HeaderText/index.tsx +5 -5
- package/src/components/Input/index.tsx +40 -37
- package/src/components/List/index.tsx +2 -3
- package/src/components/Pagination/index.tsx +56 -43
- package/src/components/PlanCard/index.tsx +118 -50
- package/src/components/ProgressTracker/index.tsx +44 -26
- package/src/components/PromotionCard/index.tsx +217 -212
- package/src/components/RadioButton/index.tsx +27 -25
- package/src/components/ScreenContainer/ScreenContainer.test.tsx +12 -2
- package/src/components/ScreenContainer/index.tsx +21 -10
- package/src/components/SegmentedControl/index.tsx +37 -30
- package/src/components/SelectionCard/index.tsx +38 -26
- package/src/components/ServiceCard/index.tsx +162 -130
- package/src/components/Slider/index.tsx +77 -77
- package/src/components/Snackbar/index.tsx +69 -57
- package/src/components/Switch/index.tsx +13 -12
- package/src/components/Tooltip/index.tsx +3 -3
- package/src/index.ts +13 -0
- package/src/theme/ThemeContext.ts +11 -0
- package/src/theme/ThemeProvider.tsx +40 -0
- package/src/theme/__tests__/ThemeProvider.test.tsx +101 -0
- package/src/theme/__tests__/defaultTheme.test.ts +36 -0
- package/src/theme/__tests__/mergeTheme.test.ts +94 -0
- package/src/theme/defaultTheme.ts +15 -0
- package/src/theme/index.ts +8 -0
- package/src/theme/mergeTheme.ts +47 -0
- package/src/theme/types.ts +39 -0
- package/src/theme/useTheme.ts +16 -0
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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,
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 ?
|
|
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
|
-
|
|
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
|
|
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
|
);
|