@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/Card.tsx +136 -48
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compsych/mobile-ui",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
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",
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
- const VARIANT_TOKENS = {
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
- outerBg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
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
- outerBg: cr.addOn.primaryFixed.sysPrimaryFixedDim,
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
- outerBg: cr.accent.primary.sysPrimaryContainer,
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.surface.surface.sysOnSurface,
58
- descColor: cr.surface.surface.sysOnSurfaceVariant,
74
+ titleColor: cr.accent.primary.sysOnPrimary,
75
+ descColor: cr.accent.primary.sysOnPrimary,
76
+ chevronColor: cr.accent.primary.sysOnPrimary,
59
77
  },
60
78
  doubled: {
61
- outerBg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
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
- outerBg: 'transparent',
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
- const SIZE_PADDING: Record<CardSize, number> = {
81
- sm: dim.spacing.padding.sysPadding8,
82
- md: dim.spacing.padding.sysPadding12,
83
- lg: dim.spacing.padding.sysPadding16,
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 padding = SIZE_PADDING[size];
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.outerBg,
166
+ backgroundColor: variant === 'doubled' ? v.bg : v.bg,
115
167
  borderColor: v.borderColor,
116
168
  borderWidth: v.borderWidth,
117
- borderRadius: v.outerRadius,
118
- padding: variant === 'doubled' ? dim.spacing.padding.sysPadding8 : padding,
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.textBlock}>
182
+ <View style={isRow ? styles.rowTextBlock : undefined}>
127
183
  {title && (
128
- <BodyText variant="large" color={v.titleColor}>
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.doubledInner,
203
+ styles.colRoot,
147
204
  {
148
- backgroundColor: cr.transparent.primary.sysPrimary08,
149
- borderRadius: dim.borderRadius.sysRadiusLg,
150
- padding,
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: cr.accent.primary.sysPrimary,
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: v.outerRadius }]}
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: v.outerRadius, backgroundColor: 'rgba(0,0,0,0.30)' },
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={styles.iconSlot}>{icon}</View>}
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={styles.iconSlot}>{icon}</View>}
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 }) => [outerStyle, pressed && !disabled && styles.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 <View style={outerStyle}>{inner}</View>;
302
+ return (
303
+ <View style={[styles.overflow, ...outerStyle]}>
304
+ {inner}
305
+ </View>
306
+ );
221
307
  }
222
308
 
223
309
  const styles = StyleSheet.create({
224
- root: {
310
+ overflow: {
225
311
  overflow: 'hidden',
226
- gap: 32,
227
- },
228
- pressed: {
229
- opacity: 0.84,
230
312
  },
231
- iconSlot: {
232
- width: 32,
233
- height: 32,
313
+ colRoot: {
314
+ flexDirection: 'column',
315
+ alignItems: 'flex-start',
234
316
  },
235
- textBlock: {
236
- gap: 0,
317
+ rowRoot: {
318
+ flexDirection: 'row',
319
+ alignItems: 'center',
320
+ gap: 12,
237
321
  },
238
- doubledInner: {
239
- gap: 24,
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
  });