@compsych/mobile-ui 1.0.5 → 1.0.6
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/Card.tsx +136 -48
package/package.json
CHANGED
package/src/Card.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
StyleSheet,
|
|
7
7
|
View,
|
|
8
8
|
} from 'react-native';
|
|
9
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
9
10
|
import { sys } from './tokens';
|
|
10
11
|
import { BodyText } from './BodyText';
|
|
11
12
|
|
|
@@ -29,58 +30,107 @@ export interface CardProps {
|
|
|
29
30
|
|
|
30
31
|
const { colorRoles: cr, dimensions: dim } = sys;
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
// ── Variant tokens ────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
const VARIANT_TOKENS: Record<
|
|
36
|
+
CardVariant,
|
|
37
|
+
{
|
|
38
|
+
bg: string;
|
|
39
|
+
borderColor: string;
|
|
40
|
+
borderWidth: number;
|
|
41
|
+
elevated: boolean;
|
|
42
|
+
titleColor: string;
|
|
43
|
+
descColor: string;
|
|
44
|
+
chevronColor: string;
|
|
45
|
+
// doubled inner gradient overlay color
|
|
46
|
+
innerBg?: string;
|
|
47
|
+
iconBadgeBg?: string;
|
|
48
|
+
iconBadgeColor?: string;
|
|
49
|
+
}
|
|
50
|
+
> = {
|
|
33
51
|
outlined: {
|
|
34
|
-
|
|
52
|
+
bg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
|
|
35
53
|
borderColor: cr.outline.sysOutline,
|
|
36
54
|
borderWidth: dim.borderWidth.sysStrokeThin,
|
|
37
|
-
outerRadius: dim.borderRadius.sysRadiusLg,
|
|
38
55
|
elevated: true,
|
|
39
56
|
titleColor: cr.surface.surface.sysOnSurface,
|
|
40
57
|
descColor: cr.surface.surface.sysOnSurfaceVariant,
|
|
58
|
+
chevronColor: cr.outline.sysOutlineFixed,
|
|
41
59
|
},
|
|
42
60
|
tonal: {
|
|
43
|
-
|
|
61
|
+
bg: cr.addOn.primaryFixed.sysPrimaryFixedDim,
|
|
44
62
|
borderColor: 'transparent',
|
|
45
63
|
borderWidth: 0,
|
|
46
|
-
outerRadius: dim.borderRadius.sysRadiusLg,
|
|
47
64
|
elevated: false,
|
|
48
65
|
titleColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
|
|
49
66
|
descColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
|
|
67
|
+
chevronColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
|
|
50
68
|
},
|
|
51
69
|
filled: {
|
|
52
|
-
|
|
70
|
+
bg: cr.accent.primary.sysPrimary,
|
|
53
71
|
borderColor: 'transparent',
|
|
54
72
|
borderWidth: 0,
|
|
55
|
-
outerRadius: dim.borderRadius.sysRadiusLg,
|
|
56
73
|
elevated: true,
|
|
57
|
-
titleColor: cr.
|
|
58
|
-
descColor: cr.
|
|
74
|
+
titleColor: cr.accent.primary.sysOnPrimary,
|
|
75
|
+
descColor: cr.accent.primary.sysOnPrimary,
|
|
76
|
+
chevronColor: cr.accent.primary.sysOnPrimary,
|
|
59
77
|
},
|
|
60
78
|
doubled: {
|
|
61
|
-
|
|
79
|
+
bg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
|
|
62
80
|
borderColor: cr.outline.sysOutline,
|
|
63
81
|
borderWidth: dim.borderWidth.sysStrokeThin,
|
|
64
|
-
outerRadius: dim.borderRadius.sysRadiusXl,
|
|
65
82
|
elevated: false,
|
|
66
83
|
titleColor: cr.surface.surface.sysOnSurface,
|
|
67
84
|
descColor: cr.surface.surface.sysOnSurfaceVariant,
|
|
85
|
+
chevronColor: cr.outline.sysOutlineFixed,
|
|
86
|
+
innerBg: cr.transparent.primary.sysPrimary08,
|
|
87
|
+
iconBadgeBg: cr.accent.primary.sysPrimary,
|
|
88
|
+
iconBadgeColor: cr.accent.primary.sysOnPrimary,
|
|
68
89
|
},
|
|
69
90
|
image: {
|
|
70
|
-
|
|
91
|
+
bg: 'transparent',
|
|
71
92
|
borderColor: 'transparent',
|
|
72
93
|
borderWidth: 0,
|
|
73
|
-
outerRadius: dim.borderRadius.sysRadiusLg,
|
|
74
94
|
elevated: false,
|
|
75
95
|
titleColor: '#ffffff',
|
|
76
96
|
descColor: cr.transparent.neutral.sysWhite80,
|
|
97
|
+
chevronColor: '#ffffff',
|
|
77
98
|
},
|
|
78
99
|
};
|
|
79
100
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
101
|
+
// ── Size tokens ───────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
const SIZE_TOKENS = {
|
|
104
|
+
sm: {
|
|
105
|
+
layout: 'row' as const,
|
|
106
|
+
paddingH: dim.spacing.padding.sysPadding16,
|
|
107
|
+
paddingV: dim.spacing.padding.sysPadding12,
|
|
108
|
+
gap: dim.spacing.padding.sysPadding12,
|
|
109
|
+
borderRadius: dim.borderRadius.sysRadiusMd,
|
|
110
|
+
iconSize: 24,
|
|
111
|
+
titleVariant: 'medium' as const,
|
|
112
|
+
showChevron: true,
|
|
113
|
+
},
|
|
114
|
+
md: {
|
|
115
|
+
layout: 'column' as const,
|
|
116
|
+
paddingH: dim.spacing.padding.sysPadding16,
|
|
117
|
+
paddingV: dim.spacing.padding.sysPadding16,
|
|
118
|
+
gap: dim.spacing.padding.sysPadding24,
|
|
119
|
+
borderRadius: dim.borderRadius.sysRadiusLg,
|
|
120
|
+
iconSize: 32,
|
|
121
|
+
titleVariant: 'medium' as const,
|
|
122
|
+
showChevron: false,
|
|
123
|
+
},
|
|
124
|
+
lg: {
|
|
125
|
+
layout: 'column' as const,
|
|
126
|
+
paddingH: dim.spacing.padding.sysPadding16,
|
|
127
|
+
paddingV: dim.spacing.padding.sysPadding16,
|
|
128
|
+
gap: dim.spacing.padding.sysPadding32,
|
|
129
|
+
borderRadius: dim.borderRadius.sysRadiusLg,
|
|
130
|
+
iconSize: 32,
|
|
131
|
+
titleVariant: 'large' as const,
|
|
132
|
+
showChevron: false,
|
|
133
|
+
},
|
|
84
134
|
};
|
|
85
135
|
|
|
86
136
|
const ELEVATION = {
|
|
@@ -91,6 +141,8 @@ const ELEVATION = {
|
|
|
91
141
|
elevation: 2,
|
|
92
142
|
};
|
|
93
143
|
|
|
144
|
+
// ── Component ─────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
94
146
|
export function Card({
|
|
95
147
|
variant = 'outlined',
|
|
96
148
|
size = 'lg',
|
|
@@ -106,30 +158,34 @@ export function Card({
|
|
|
106
158
|
fullWidth = false,
|
|
107
159
|
}: CardProps) {
|
|
108
160
|
const v = VARIANT_TOKENS[variant];
|
|
109
|
-
const
|
|
161
|
+
const s = SIZE_TOKENS[size];
|
|
162
|
+
const isRow = s.layout === 'row';
|
|
110
163
|
|
|
111
164
|
const outerStyle = [
|
|
112
|
-
styles.root,
|
|
113
165
|
{
|
|
114
|
-
backgroundColor: v.
|
|
166
|
+
backgroundColor: variant === 'doubled' ? v.bg : v.bg,
|
|
115
167
|
borderColor: v.borderColor,
|
|
116
168
|
borderWidth: v.borderWidth,
|
|
117
|
-
borderRadius:
|
|
118
|
-
|
|
169
|
+
borderRadius: variant === 'doubled' ? dim.borderRadius.sysRadiusXl : s.borderRadius,
|
|
170
|
+
paddingHorizontal: variant === 'doubled' ? dim.spacing.padding.sysPadding8 : s.paddingH,
|
|
171
|
+
paddingVertical: variant === 'doubled' ? dim.spacing.padding.sysPadding8 : s.paddingV,
|
|
119
172
|
opacity: disabled ? 0.48 : 1,
|
|
120
173
|
},
|
|
121
174
|
v.elevated && ELEVATION,
|
|
122
175
|
fullWidth && { alignSelf: 'stretch' as const },
|
|
176
|
+
isRow ? styles.rowRoot : styles.colRoot,
|
|
177
|
+
!isRow && { gap: s.gap },
|
|
123
178
|
];
|
|
124
179
|
|
|
180
|
+
// ── Text block ──────────────────────────────────────────────────────────────
|
|
125
181
|
const textBlock = (title || description) ? (
|
|
126
|
-
<View style={styles.
|
|
182
|
+
<View style={isRow ? styles.rowTextBlock : undefined}>
|
|
127
183
|
{title && (
|
|
128
|
-
<BodyText variant=
|
|
184
|
+
<BodyText variant={s.titleVariant} color={v.titleColor}>
|
|
129
185
|
{title}
|
|
130
186
|
</BodyText>
|
|
131
187
|
)}
|
|
132
|
-
{description && (
|
|
188
|
+
{!isRow && description && (
|
|
133
189
|
<BodyText variant="small" color={v.descColor}>
|
|
134
190
|
{description}
|
|
135
191
|
</BodyText>
|
|
@@ -137,17 +193,20 @@ export function Card({
|
|
|
137
193
|
</View>
|
|
138
194
|
) : null;
|
|
139
195
|
|
|
196
|
+
// ── Inner content by variant ────────────────────────────────────────────────
|
|
140
197
|
let inner: React.ReactNode;
|
|
141
198
|
|
|
142
199
|
if (variant === 'doubled') {
|
|
143
200
|
inner = (
|
|
144
201
|
<View
|
|
145
202
|
style={[
|
|
146
|
-
styles.
|
|
203
|
+
styles.colRoot,
|
|
147
204
|
{
|
|
148
|
-
backgroundColor:
|
|
149
|
-
borderRadius:
|
|
150
|
-
|
|
205
|
+
backgroundColor: v.innerBg,
|
|
206
|
+
borderRadius: s.borderRadius,
|
|
207
|
+
paddingHorizontal: s.paddingH,
|
|
208
|
+
paddingVertical: s.paddingV,
|
|
209
|
+
gap: s.gap,
|
|
151
210
|
},
|
|
152
211
|
]}
|
|
153
212
|
>
|
|
@@ -156,7 +215,7 @@ export function Card({
|
|
|
156
215
|
style={[
|
|
157
216
|
styles.doubledIconBadge,
|
|
158
217
|
{
|
|
159
|
-
backgroundColor:
|
|
218
|
+
backgroundColor: v.iconBadgeBg,
|
|
160
219
|
borderRadius: dim.borderRadius.sysRadiusFull,
|
|
161
220
|
},
|
|
162
221
|
]}
|
|
@@ -175,34 +234,53 @@ export function Card({
|
|
|
175
234
|
<>
|
|
176
235
|
<Image
|
|
177
236
|
source={image}
|
|
178
|
-
style={[StyleSheet.absoluteFillObject, { borderRadius:
|
|
237
|
+
style={[StyleSheet.absoluteFillObject, { borderRadius: s.borderRadius }]}
|
|
179
238
|
resizeMode="cover"
|
|
180
239
|
accessible={false}
|
|
181
240
|
/>
|
|
182
241
|
<View
|
|
183
242
|
style={[
|
|
184
243
|
StyleSheet.absoluteFillObject,
|
|
185
|
-
{ borderRadius:
|
|
244
|
+
{ borderRadius: s.borderRadius, backgroundColor: 'rgba(0,0,0,0.30)' },
|
|
186
245
|
]}
|
|
187
246
|
pointerEvents="none"
|
|
188
247
|
/>
|
|
189
248
|
</>
|
|
190
249
|
)}
|
|
191
|
-
{icon && <View style={
|
|
250
|
+
{icon && <View style={{ width: s.iconSize, height: s.iconSize }}>{icon}</View>}
|
|
192
251
|
{textBlock}
|
|
193
252
|
{children}
|
|
194
253
|
</>
|
|
195
254
|
);
|
|
255
|
+
} else if (isRow) {
|
|
256
|
+
// sm — horizontal row
|
|
257
|
+
inner = (
|
|
258
|
+
<>
|
|
259
|
+
{icon && <View style={{ width: s.iconSize, height: s.iconSize }}>{icon}</View>}
|
|
260
|
+
{textBlock}
|
|
261
|
+
{s.showChevron && (
|
|
262
|
+
<Ionicons
|
|
263
|
+
name="chevron-forward"
|
|
264
|
+
size={20}
|
|
265
|
+
color={v.chevronColor}
|
|
266
|
+
accessible={false}
|
|
267
|
+
/>
|
|
268
|
+
)}
|
|
269
|
+
{children}
|
|
270
|
+
</>
|
|
271
|
+
);
|
|
196
272
|
} else {
|
|
273
|
+
// md / lg — vertical column
|
|
197
274
|
inner = (
|
|
198
275
|
<>
|
|
199
|
-
{icon && <View style={
|
|
276
|
+
{icon && <View style={{ width: s.iconSize, height: s.iconSize }}>{icon}</View>}
|
|
200
277
|
{textBlock}
|
|
201
278
|
{children}
|
|
202
279
|
</>
|
|
203
280
|
);
|
|
204
281
|
}
|
|
205
282
|
|
|
283
|
+
// ── Wrapper ─────────────────────────────────────────────────────────────────
|
|
206
284
|
if (interactive || onPress) {
|
|
207
285
|
return (
|
|
208
286
|
<Pressable
|
|
@@ -210,33 +288,40 @@ export function Card({
|
|
|
210
288
|
accessibilityRole="button"
|
|
211
289
|
accessibilityLabel={accessibilityLabel ?? title}
|
|
212
290
|
accessibilityState={{ disabled }}
|
|
213
|
-
style={({ pressed }) => [
|
|
291
|
+
style={({ pressed }) => [
|
|
292
|
+
styles.overflow,
|
|
293
|
+
...outerStyle,
|
|
294
|
+
pressed && !disabled && styles.pressed,
|
|
295
|
+
]}
|
|
214
296
|
>
|
|
215
297
|
{inner}
|
|
216
298
|
</Pressable>
|
|
217
299
|
);
|
|
218
300
|
}
|
|
219
301
|
|
|
220
|
-
return
|
|
302
|
+
return (
|
|
303
|
+
<View style={[styles.overflow, ...outerStyle]}>
|
|
304
|
+
{inner}
|
|
305
|
+
</View>
|
|
306
|
+
);
|
|
221
307
|
}
|
|
222
308
|
|
|
223
309
|
const styles = StyleSheet.create({
|
|
224
|
-
|
|
310
|
+
overflow: {
|
|
225
311
|
overflow: 'hidden',
|
|
226
|
-
gap: 32,
|
|
227
|
-
},
|
|
228
|
-
pressed: {
|
|
229
|
-
opacity: 0.84,
|
|
230
312
|
},
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
313
|
+
colRoot: {
|
|
314
|
+
flexDirection: 'column',
|
|
315
|
+
alignItems: 'flex-start',
|
|
234
316
|
},
|
|
235
|
-
|
|
236
|
-
|
|
317
|
+
rowRoot: {
|
|
318
|
+
flexDirection: 'row',
|
|
319
|
+
alignItems: 'center',
|
|
320
|
+
gap: 12,
|
|
237
321
|
},
|
|
238
|
-
|
|
239
|
-
|
|
322
|
+
rowTextBlock: {
|
|
323
|
+
flex: 1,
|
|
324
|
+
minWidth: 0,
|
|
240
325
|
},
|
|
241
326
|
doubledIconBadge: {
|
|
242
327
|
width: 48,
|
|
@@ -244,4 +329,7 @@ const styles = StyleSheet.create({
|
|
|
244
329
|
alignItems: 'center',
|
|
245
330
|
justifyContent: 'center',
|
|
246
331
|
},
|
|
332
|
+
pressed: {
|
|
333
|
+
opacity: 0.84,
|
|
334
|
+
},
|
|
247
335
|
});
|