@compsych/mobile-ui 1.0.4 → 1.0.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compsych/mobile-ui",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
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
@@ -1,152 +1,247 @@
1
1
  import React from 'react';
2
- import { Pressable, StyleSheet, View, type PressableProps, type ViewProps } from 'react-native';
2
+ import {
3
+ Image,
4
+ type ImageSourcePropType,
5
+ Pressable,
6
+ StyleSheet,
7
+ View,
8
+ } from 'react-native';
3
9
  import { sys } from './tokens';
10
+ import { BodyText } from './BodyText';
4
11
 
5
- export type CardVariant = 'outlined' | 'filled' | 'gradient';
6
- export type CardSize = 'sm' | 'md' | 'lg' | 'xl';
12
+ export type CardVariant = 'outlined' | 'tonal' | 'filled' | 'doubled' | 'image';
13
+ export type CardSize = 'sm' | 'md' | 'lg';
7
14
 
8
15
  export interface CardProps {
9
16
  variant?: CardVariant;
10
17
  size?: CardSize;
18
+ title?: string;
19
+ description?: string;
20
+ icon?: React.ReactNode;
21
+ image?: ImageSourcePropType;
11
22
  interactive?: boolean;
12
23
  disabled?: boolean;
13
- current?: boolean;
14
- fullWidth?: boolean;
15
24
  onPress?: () => void;
16
25
  children?: React.ReactNode;
17
26
  accessibilityLabel?: string;
27
+ fullWidth?: boolean;
18
28
  }
19
29
 
20
30
  const { colorRoles: cr, dimensions: dim } = sys;
21
31
 
22
32
  const VARIANT_TOKENS = {
23
33
  outlined: {
24
- bg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
34
+ outerBg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
25
35
  borderColor: cr.outline.sysOutline,
26
36
  borderWidth: dim.borderWidth.sysStrokeThin,
37
+ outerRadius: dim.borderRadius.sysRadiusLg,
38
+ elevated: true,
39
+ titleColor: cr.surface.surface.sysOnSurface,
40
+ descColor: cr.surface.surface.sysOnSurfaceVariant,
41
+ },
42
+ tonal: {
43
+ outerBg: cr.addOn.primaryFixed.sysPrimaryFixedDim,
44
+ borderColor: 'transparent',
45
+ borderWidth: 0,
46
+ outerRadius: dim.borderRadius.sysRadiusLg,
27
47
  elevated: false,
48
+ titleColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
49
+ descColor: cr.addOn.primaryFixed.sysOnPrimaryFixed,
28
50
  },
29
51
  filled: {
30
- bg: cr.accent.primary.sysPrimaryContainer,
52
+ outerBg: cr.accent.primary.sysPrimaryContainer,
31
53
  borderColor: 'transparent',
32
54
  borderWidth: 0,
55
+ outerRadius: dim.borderRadius.sysRadiusLg,
33
56
  elevated: true,
57
+ titleColor: cr.surface.surface.sysOnSurface,
58
+ descColor: cr.surface.surface.sysOnSurfaceVariant,
34
59
  },
35
- gradient: {
36
- // Approximates the gradient variant with a tinted surface; expo-linear-gradient not required.
37
- bg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
60
+ doubled: {
61
+ outerBg: cr.surface.surfaceContainer.sysSurfaceContainerLowest,
38
62
  borderColor: cr.outline.sysOutline,
39
63
  borderWidth: dim.borderWidth.sysStrokeThin,
64
+ outerRadius: dim.borderRadius.sysRadiusXl,
65
+ elevated: false,
66
+ titleColor: cr.surface.surface.sysOnSurface,
67
+ descColor: cr.surface.surface.sysOnSurfaceVariant,
68
+ },
69
+ image: {
70
+ outerBg: 'transparent',
71
+ borderColor: 'transparent',
72
+ borderWidth: 0,
73
+ outerRadius: dim.borderRadius.sysRadiusLg,
40
74
  elevated: false,
75
+ titleColor: '#ffffff',
76
+ descColor: cr.transparent.neutral.sysWhite80,
41
77
  },
42
78
  };
43
79
 
44
- const SIZE_TOKENS = {
45
- sm: {
46
- padding: dim.spacing.padding.sysPadding24,
47
- borderRadius: dim.borderRadius.sysRadiusLg,
48
- gap: dim.spacing.padding.sysPadding12,
49
- },
50
- md: {
51
- padding: dim.spacing.padding.sysPadding32,
52
- borderRadius: dim.borderRadius.sysRadiusLg,
53
- gap: dim.spacing.padding.sysPadding16,
54
- },
55
- lg: {
56
- padding: dim.spacing.padding.sysPadding32,
57
- borderRadius: dim.borderRadius.sysRadiusLg,
58
- gap: dim.spacing.padding.sysPadding24,
59
- },
60
- xl: {
61
- padding: dim.spacing.padding.sysPadding48,
62
- borderRadius: dim.borderRadius.sysRadiusXl,
63
- gap: dim.spacing.padding.sysPadding24,
64
- },
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,
84
+ };
85
+
86
+ const ELEVATION = {
87
+ shadowColor: '#000',
88
+ shadowOffset: { width: 0, height: 2 },
89
+ shadowOpacity: 0.08,
90
+ shadowRadius: 8,
91
+ elevation: 2,
65
92
  };
66
93
 
67
94
  export function Card({
68
95
  variant = 'outlined',
69
- size = 'md',
96
+ size = 'lg',
97
+ title,
98
+ description,
99
+ icon,
100
+ image,
70
101
  interactive = false,
71
102
  disabled = false,
72
- current = false,
73
- fullWidth = false,
74
103
  onPress,
75
104
  children,
76
105
  accessibilityLabel,
106
+ fullWidth = false,
77
107
  }: CardProps) {
78
108
  const v = VARIANT_TOKENS[variant];
79
- const s = SIZE_TOKENS[size];
109
+ const padding = SIZE_PADDING[size];
80
110
 
81
- const borderColor = current ? cr.accent.primary.sysPrimary : v.borderColor;
82
- const borderWidth = current ? dim.borderWidth.sysStrokeThick : v.borderWidth;
111
+ const outerStyle = [
112
+ styles.root,
113
+ {
114
+ backgroundColor: v.outerBg,
115
+ borderColor: v.borderColor,
116
+ borderWidth: v.borderWidth,
117
+ borderRadius: v.outerRadius,
118
+ padding: variant === 'doubled' ? dim.spacing.padding.sysPadding8 : padding,
119
+ opacity: disabled ? 0.48 : 1,
120
+ },
121
+ v.elevated && ELEVATION,
122
+ fullWidth && { alignSelf: 'stretch' as const },
123
+ ];
83
124
 
84
- const baseStyle = {
85
- backgroundColor: v.bg,
86
- borderColor,
87
- borderWidth,
88
- borderRadius: s.borderRadius,
89
- padding: s.padding,
90
- gap: s.gap,
91
- ...(fullWidth ? { alignSelf: 'stretch' as const } : {}),
92
- ...(v.elevated
93
- ? {
94
- shadowColor: '#000',
95
- shadowOffset: { width: 0, height: 2 },
96
- shadowOpacity: 0.06,
97
- shadowRadius: 4,
98
- elevation: 1,
99
- }
100
- : {}),
101
- opacity: disabled ? 0.48 : 1,
102
- };
125
+ const textBlock = (title || description) ? (
126
+ <View style={styles.textBlock}>
127
+ {title && (
128
+ <BodyText variant="large" color={v.titleColor}>
129
+ {title}
130
+ </BodyText>
131
+ )}
132
+ {description && (
133
+ <BodyText variant="small" color={v.descColor}>
134
+ {description}
135
+ </BodyText>
136
+ )}
137
+ </View>
138
+ ) : null;
139
+
140
+ let inner: React.ReactNode;
103
141
 
104
- // Gradient variant: overlay a subtle primary tint over the content area
105
- const gradientOverlay =
106
- variant === 'gradient' ? (
142
+ if (variant === 'doubled') {
143
+ inner = (
107
144
  <View
108
145
  style={[
109
- StyleSheet.absoluteFillObject,
146
+ styles.doubledInner,
110
147
  {
111
- borderRadius: s.borderRadius,
112
148
  backgroundColor: cr.transparent.primary.sysPrimary08,
149
+ borderRadius: dim.borderRadius.sysRadiusLg,
150
+ padding,
113
151
  },
114
152
  ]}
115
- pointerEvents="none"
116
- />
117
- ) : null;
153
+ >
154
+ {icon && (
155
+ <View
156
+ style={[
157
+ styles.doubledIconBadge,
158
+ {
159
+ backgroundColor: cr.accent.primary.sysPrimary,
160
+ borderRadius: dim.borderRadius.sysRadiusFull,
161
+ },
162
+ ]}
163
+ >
164
+ {icon}
165
+ </View>
166
+ )}
167
+ {textBlock}
168
+ {children}
169
+ </View>
170
+ );
171
+ } else if (variant === 'image') {
172
+ inner = (
173
+ <>
174
+ {image && (
175
+ <>
176
+ <Image
177
+ source={image}
178
+ style={[StyleSheet.absoluteFillObject, { borderRadius: v.outerRadius }]}
179
+ resizeMode="cover"
180
+ accessible={false}
181
+ />
182
+ <View
183
+ style={[
184
+ StyleSheet.absoluteFillObject,
185
+ { borderRadius: v.outerRadius, backgroundColor: 'rgba(0,0,0,0.30)' },
186
+ ]}
187
+ pointerEvents="none"
188
+ />
189
+ </>
190
+ )}
191
+ {icon && <View style={styles.iconSlot}>{icon}</View>}
192
+ {textBlock}
193
+ {children}
194
+ </>
195
+ );
196
+ } else {
197
+ inner = (
198
+ <>
199
+ {icon && <View style={styles.iconSlot}>{icon}</View>}
200
+ {textBlock}
201
+ {children}
202
+ </>
203
+ );
204
+ }
118
205
 
119
206
  if (interactive || onPress) {
120
207
  return (
121
208
  <Pressable
122
209
  onPress={disabled ? undefined : onPress}
123
210
  accessibilityRole="button"
124
- accessibilityLabel={accessibilityLabel}
125
- accessibilityState={{ disabled, selected: current }}
126
- style={({ pressed }) => [
127
- styles.root,
128
- baseStyle,
129
- pressed && !disabled
130
- ? { opacity: 0.84 }
131
- : {},
132
- ]}
211
+ accessibilityLabel={accessibilityLabel ?? title}
212
+ accessibilityState={{ disabled }}
213
+ style={({ pressed }) => [outerStyle, pressed && !disabled && styles.pressed]}
133
214
  >
134
- {gradientOverlay}
135
- {children}
215
+ {inner}
136
216
  </Pressable>
137
217
  );
138
218
  }
139
219
 
140
- return (
141
- <View style={[styles.root, baseStyle]}>
142
- {gradientOverlay}
143
- {children}
144
- </View>
145
- );
220
+ return <View style={outerStyle}>{inner}</View>;
146
221
  }
147
222
 
148
223
  const styles = StyleSheet.create({
149
224
  root: {
150
225
  overflow: 'hidden',
226
+ gap: 32,
227
+ },
228
+ pressed: {
229
+ opacity: 0.84,
230
+ },
231
+ iconSlot: {
232
+ width: 32,
233
+ height: 32,
234
+ },
235
+ textBlock: {
236
+ gap: 0,
237
+ },
238
+ doubledInner: {
239
+ gap: 24,
240
+ },
241
+ doubledIconBadge: {
242
+ width: 48,
243
+ height: 48,
244
+ alignItems: 'center',
245
+ justifyContent: 'center',
151
246
  },
152
247
  });
package/src/tokens.ts CHANGED
@@ -65,7 +65,9 @@ export const sys = {
65
65
  transparent: {
66
66
  neutral: {
67
67
  sysBlack10: '#0000001a',
68
+ sysWhite10: '#ffffff1a',
68
69
  sysWhite20: '#ffffff33',
70
+ sysWhite80: 'rgba(255,255,255,0.8)',
69
71
  },
70
72
  primary: {
71
73
  sysPrimary08: '#075cba14',