@compsych/mobile-ui 1.0.22 → 1.0.24
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 +4 -8
- package/src/components/ChatBubble/index.tsx +14 -16
- package/src/components/ChatInput/index.tsx +118 -109
- package/src/components/ServiceCard/index.tsx +41 -22
- package/src/components/StatusChip/StatusChip.test.tsx +47 -0
- package/src/components/StatusChip/index.tsx +90 -0
- package/src/index.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@compsych/mobile-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.24",
|
|
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",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"test": "jest",
|
|
14
14
|
"test:watch": "jest --watch",
|
|
15
15
|
"test:coverage": "jest --coverage",
|
|
16
|
-
"prepare": "husky"
|
|
16
|
+
"prepare": "husky",
|
|
17
|
+
"postinstall": "rm -rf node_modules/react-native node_modules/react node_modules/react-native-linear-gradient node_modules/react-native-svg node_modules/lucide-react-native"
|
|
17
18
|
},
|
|
18
19
|
"keywords": [
|
|
19
20
|
"react-native",
|
|
@@ -32,17 +33,12 @@
|
|
|
32
33
|
"peerDependencies": {
|
|
33
34
|
"@expo/vector-icons": ">=15.0.0",
|
|
34
35
|
"expo-font": ">=13.0.0",
|
|
35
|
-
"expo-linear-gradient": ">=13.0.0",
|
|
36
36
|
"lucide-react-native": ">=0.400.0",
|
|
37
37
|
"react": ">=18.0.0",
|
|
38
38
|
"react-native": ">=0.73.0",
|
|
39
|
+
"react-native-linear-gradient": ">=2.0.0",
|
|
39
40
|
"react-native-svg": ">=15.0.0"
|
|
40
41
|
},
|
|
41
|
-
"peerDependenciesMeta": {
|
|
42
|
-
"expo-linear-gradient": {
|
|
43
|
-
"optional": true
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
42
|
"devDependencies": {
|
|
47
43
|
"@babel/core": "^7.25.2",
|
|
48
44
|
"@babel/plugin-transform-flow-strip-types": "^7.27.1",
|
|
@@ -39,19 +39,21 @@ export function ChatBubble({
|
|
|
39
39
|
const tokenStyles = useMemo(
|
|
40
40
|
() => ({
|
|
41
41
|
bubble: {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
paddingHorizontal: dim.spacing.padding.sysPadding12,
|
|
45
|
-
borderRadius: dim.borderRadius.sysRadiusMd,
|
|
42
|
+
paddingVertical: dim.spacing.padding.sysPadding12,
|
|
43
|
+
paddingHorizontal: dim.spacing.padding.sysPadding16,
|
|
46
44
|
},
|
|
47
45
|
bubbleIncoming: {
|
|
48
|
-
backgroundColor: cr.surface.surfaceContainer.
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
backgroundColor: cr.surface.surfaceContainer.sysSurfaceContainer,
|
|
47
|
+
borderTopLeftRadius: dim.borderRadius.sysRadiusLg,
|
|
48
|
+
borderTopRightRadius: dim.borderRadius.sysRadiusLg,
|
|
51
49
|
borderBottomLeftRadius: dim.borderRadius.sysRadiusXs,
|
|
50
|
+
borderBottomRightRadius: dim.borderRadius.sysRadiusXs,
|
|
52
51
|
},
|
|
53
52
|
bubbleOutgoing: {
|
|
54
53
|
backgroundColor: cr.accent.primary.sysPrimary,
|
|
54
|
+
borderTopLeftRadius: dim.borderRadius.sysRadiusXl,
|
|
55
|
+
borderTopRightRadius: dim.borderRadius.sysRadiusXl,
|
|
56
|
+
borderBottomLeftRadius: dim.borderRadius.sysRadiusXl,
|
|
55
57
|
borderBottomRightRadius: dim.borderRadius.sysRadiusXs,
|
|
56
58
|
},
|
|
57
59
|
metaRow: {
|
|
@@ -84,7 +86,7 @@ export function ChatBubble({
|
|
|
84
86
|
style,
|
|
85
87
|
]}
|
|
86
88
|
>
|
|
87
|
-
<View>
|
|
89
|
+
<View style={styles.bubbleWrapper}>
|
|
88
90
|
{/* Bubble */}
|
|
89
91
|
<View
|
|
90
92
|
style={[
|
|
@@ -94,11 +96,7 @@ export function ChatBubble({
|
|
|
94
96
|
: tokenStyles.bubbleIncoming,
|
|
95
97
|
]}
|
|
96
98
|
>
|
|
97
|
-
<BodyText
|
|
98
|
-
variant="medium"
|
|
99
|
-
color={textColor}
|
|
100
|
-
style={styles.messageText}
|
|
101
|
-
>
|
|
99
|
+
<BodyText variant="medium" color={textColor}>
|
|
102
100
|
{message}
|
|
103
101
|
</BodyText>
|
|
104
102
|
</View>
|
|
@@ -168,9 +166,11 @@ const styles = StyleSheet.create({
|
|
|
168
166
|
},
|
|
169
167
|
rowIncoming: {
|
|
170
168
|
justifyContent: 'flex-start',
|
|
169
|
+
paddingRight: 64,
|
|
171
170
|
},
|
|
172
171
|
rowOutgoing: {
|
|
173
172
|
justifyContent: 'flex-end',
|
|
173
|
+
paddingLeft: 32,
|
|
174
174
|
},
|
|
175
175
|
metaIncoming: {
|
|
176
176
|
justifyContent: 'flex-start',
|
|
@@ -178,9 +178,7 @@ const styles = StyleSheet.create({
|
|
|
178
178
|
metaOutgoing: {
|
|
179
179
|
justifyContent: 'flex-end',
|
|
180
180
|
},
|
|
181
|
-
|
|
182
|
-
flexShrink: 1,
|
|
183
|
-
},
|
|
181
|
+
bubbleWrapper: {},
|
|
184
182
|
pressed: {
|
|
185
183
|
opacity: 0.6,
|
|
186
184
|
},
|
|
@@ -32,6 +32,8 @@ export interface ChatInputProps {
|
|
|
32
32
|
attachments?: ChatAttachment[];
|
|
33
33
|
onRemoveAttachment?: (index: number) => void;
|
|
34
34
|
disabled?: boolean;
|
|
35
|
+
/** Optional status banner rendered above the input (e.g. <StatusChip />) */
|
|
36
|
+
statusChip?: React.ReactNode;
|
|
35
37
|
style?: StyleProp<ViewStyle>;
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -44,6 +46,7 @@ export function ChatInput({
|
|
|
44
46
|
attachments,
|
|
45
47
|
onRemoveAttachment,
|
|
46
48
|
disabled = false,
|
|
49
|
+
statusChip,
|
|
47
50
|
style,
|
|
48
51
|
}: ChatInputProps) {
|
|
49
52
|
const { colorRoles: cr, dimensions: dim, iconography: ico } = useTheme();
|
|
@@ -111,128 +114,134 @@ export function ChatInput({
|
|
|
111
114
|
);
|
|
112
115
|
|
|
113
116
|
return (
|
|
114
|
-
<View style={[styles.wrapper,
|
|
115
|
-
{/*
|
|
116
|
-
{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
<View style={[styles.wrapper, style]}>
|
|
118
|
+
{/* Status chip */}
|
|
119
|
+
{statusChip}
|
|
120
|
+
|
|
121
|
+
{/* Input row */}
|
|
122
|
+
<View style={[disabled && styles.disabled]}>
|
|
123
|
+
{/* Attachment strip */}
|
|
124
|
+
{hasAttachments && (
|
|
125
|
+
<ScrollView
|
|
126
|
+
horizontal
|
|
127
|
+
showsHorizontalScrollIndicator={false}
|
|
128
|
+
style={styles.attachStrip}
|
|
129
|
+
contentContainerStyle={styles.attachStripContent}
|
|
130
|
+
>
|
|
131
|
+
{attachments!.map((att, i) => (
|
|
132
|
+
<View key={i} style={styles.attachItem}>
|
|
133
|
+
<Image
|
|
134
|
+
source={att.source ?? { uri: att.uri }}
|
|
135
|
+
style={tokenStyles.attachThumb}
|
|
136
|
+
resizeMode="cover"
|
|
137
|
+
accessibilityLabel={`Attachment ${i + 1}`}
|
|
138
|
+
/>
|
|
139
|
+
{onRemoveAttachment && (
|
|
140
|
+
<Pressable
|
|
141
|
+
onPress={() => onRemoveAttachment(i)}
|
|
142
|
+
style={styles.removeBtn}
|
|
143
|
+
accessibilityRole="button"
|
|
144
|
+
accessibilityLabel={`Remove attachment ${i + 1}`}
|
|
145
|
+
>
|
|
146
|
+
<Ionicons
|
|
147
|
+
name="close-circle"
|
|
148
|
+
size={16}
|
|
149
|
+
color={cr.surface.surface.sysOnSurfaceVariant}
|
|
150
|
+
/>
|
|
151
|
+
</Pressable>
|
|
152
|
+
)}
|
|
153
|
+
</View>
|
|
154
|
+
))}
|
|
155
|
+
</ScrollView>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{/* Input row */}
|
|
159
|
+
<View
|
|
160
|
+
style={[
|
|
161
|
+
tokenStyles.container,
|
|
162
|
+
styles.inputRow,
|
|
163
|
+
isFocused && {
|
|
164
|
+
// focus ring via transparent primary halo
|
|
165
|
+
shadowColor: cr.accent.primary.sysPrimary,
|
|
166
|
+
shadowOffset: { width: 0, height: 0 },
|
|
167
|
+
shadowOpacity: 0.08,
|
|
168
|
+
shadowRadius: 4,
|
|
169
|
+
},
|
|
170
|
+
]}
|
|
122
171
|
>
|
|
123
|
-
{
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
172
|
+
{/* Attach button */}
|
|
173
|
+
{onAttach && (
|
|
174
|
+
<Pressable
|
|
175
|
+
onPress={onAttach}
|
|
176
|
+
disabled={disabled}
|
|
177
|
+
style={({ pressed }) => [
|
|
178
|
+
tokenStyles.attachBtn,
|
|
179
|
+
pressed && styles.pressed,
|
|
180
|
+
]}
|
|
181
|
+
accessibilityRole="button"
|
|
182
|
+
accessibilityLabel="Add attachment"
|
|
183
|
+
>
|
|
184
|
+
<Ionicons
|
|
185
|
+
name="add"
|
|
186
|
+
size={ico.sysSizeSm}
|
|
187
|
+
color={
|
|
188
|
+
disabled
|
|
189
|
+
? cr.surface.surface.sysOnSurfaceVariant
|
|
190
|
+
: cr.surface.surface.sysOnSurface
|
|
191
|
+
}
|
|
130
192
|
/>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
onPress={() => onRemoveAttachment(i)}
|
|
134
|
-
style={styles.removeBtn}
|
|
135
|
-
accessibilityRole="button"
|
|
136
|
-
accessibilityLabel={`Remove attachment ${i + 1}`}
|
|
137
|
-
>
|
|
138
|
-
<Ionicons
|
|
139
|
-
name="close-circle"
|
|
140
|
-
size={16}
|
|
141
|
-
color={cr.surface.surface.sysOnSurfaceVariant}
|
|
142
|
-
/>
|
|
143
|
-
</Pressable>
|
|
144
|
-
)}
|
|
145
|
-
</View>
|
|
146
|
-
))}
|
|
147
|
-
</ScrollView>
|
|
148
|
-
)}
|
|
193
|
+
</Pressable>
|
|
194
|
+
)}
|
|
149
195
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
196
|
+
{/* Text field */}
|
|
197
|
+
<TextInput
|
|
198
|
+
ref={inputRef}
|
|
199
|
+
value={value}
|
|
200
|
+
onChangeText={onChangeText}
|
|
201
|
+
placeholder={placeholder}
|
|
202
|
+
placeholderTextColor={cr.surface.surface.sysOnSurfaceVariant}
|
|
203
|
+
editable={!disabled}
|
|
204
|
+
multiline
|
|
205
|
+
onFocus={() => setIsFocused(true)}
|
|
206
|
+
onBlur={() => setIsFocused(false)}
|
|
207
|
+
onContentSizeChange={handleContentSizeChange}
|
|
208
|
+
scrollEnabled={inputHeight >= 120}
|
|
209
|
+
style={[
|
|
210
|
+
styles.textInput,
|
|
211
|
+
{
|
|
212
|
+
color: cr.surface.surface.sysOnSurface,
|
|
213
|
+
fontSize: 16,
|
|
214
|
+
lineHeight: 24,
|
|
215
|
+
height: inputHeight,
|
|
216
|
+
},
|
|
217
|
+
]}
|
|
218
|
+
accessibilityLabel={placeholder}
|
|
219
|
+
returnKeyType="default"
|
|
220
|
+
/>
|
|
221
|
+
|
|
222
|
+
{/* Send button */}
|
|
166
223
|
<Pressable
|
|
167
|
-
onPress={
|
|
168
|
-
disabled={
|
|
224
|
+
onPress={canSend ? onSend : undefined}
|
|
225
|
+
disabled={!canSend}
|
|
169
226
|
style={({ pressed }) => [
|
|
170
|
-
tokenStyles.
|
|
171
|
-
pressed && styles.pressed,
|
|
227
|
+
tokenStyles.sendBtn,
|
|
228
|
+
pressed && canSend && styles.pressed,
|
|
172
229
|
]}
|
|
173
230
|
accessibilityRole="button"
|
|
174
|
-
accessibilityLabel="
|
|
231
|
+
accessibilityLabel="Send"
|
|
232
|
+
accessibilityState={{ disabled: !canSend }}
|
|
175
233
|
>
|
|
176
234
|
<Ionicons
|
|
177
|
-
name="
|
|
235
|
+
name="arrow-up"
|
|
178
236
|
size={ico.sysSizeSm}
|
|
179
237
|
color={
|
|
180
|
-
|
|
181
|
-
? cr.
|
|
182
|
-
: cr.surface.surface.
|
|
238
|
+
canSend
|
|
239
|
+
? cr.custom.warning.sysOnWarning
|
|
240
|
+
: cr.surface.surface.sysOnSurfaceVariant
|
|
183
241
|
}
|
|
184
242
|
/>
|
|
185
243
|
</Pressable>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
{/* Text field */}
|
|
189
|
-
<TextInput
|
|
190
|
-
ref={inputRef}
|
|
191
|
-
value={value}
|
|
192
|
-
onChangeText={onChangeText}
|
|
193
|
-
placeholder={placeholder}
|
|
194
|
-
placeholderTextColor={cr.surface.surface.sysOnSurfaceVariant}
|
|
195
|
-
editable={!disabled}
|
|
196
|
-
multiline
|
|
197
|
-
onFocus={() => setIsFocused(true)}
|
|
198
|
-
onBlur={() => setIsFocused(false)}
|
|
199
|
-
onContentSizeChange={handleContentSizeChange}
|
|
200
|
-
scrollEnabled={inputHeight >= 120}
|
|
201
|
-
style={[
|
|
202
|
-
styles.textInput,
|
|
203
|
-
{
|
|
204
|
-
color: cr.surface.surface.sysOnSurface,
|
|
205
|
-
fontSize: 16,
|
|
206
|
-
lineHeight: 24,
|
|
207
|
-
height: inputHeight,
|
|
208
|
-
},
|
|
209
|
-
]}
|
|
210
|
-
accessibilityLabel={placeholder}
|
|
211
|
-
returnKeyType="default"
|
|
212
|
-
/>
|
|
213
|
-
|
|
214
|
-
{/* Send button */}
|
|
215
|
-
<Pressable
|
|
216
|
-
onPress={canSend ? onSend : undefined}
|
|
217
|
-
disabled={!canSend}
|
|
218
|
-
style={({ pressed }) => [
|
|
219
|
-
tokenStyles.sendBtn,
|
|
220
|
-
pressed && canSend && styles.pressed,
|
|
221
|
-
]}
|
|
222
|
-
accessibilityRole="button"
|
|
223
|
-
accessibilityLabel="Send"
|
|
224
|
-
accessibilityState={{ disabled: !canSend }}
|
|
225
|
-
>
|
|
226
|
-
<Ionicons
|
|
227
|
-
name="arrow-up"
|
|
228
|
-
size={ico.sysSizeSm}
|
|
229
|
-
color={
|
|
230
|
-
canSend
|
|
231
|
-
? cr.custom.warning.sysOnWarning
|
|
232
|
-
: cr.surface.surface.sysOnSurfaceVariant
|
|
233
|
-
}
|
|
234
|
-
/>
|
|
235
|
-
</Pressable>
|
|
244
|
+
</View>
|
|
236
245
|
</View>
|
|
237
246
|
</View>
|
|
238
247
|
);
|
|
@@ -240,7 +249,7 @@ export function ChatInput({
|
|
|
240
249
|
|
|
241
250
|
const styles = StyleSheet.create({
|
|
242
251
|
wrapper: {
|
|
243
|
-
gap:
|
|
252
|
+
gap: 12,
|
|
244
253
|
},
|
|
245
254
|
disabled: {
|
|
246
255
|
opacity: 0.48,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
I18nManager,
|
|
4
5
|
Image,
|
|
5
6
|
type ImageSourcePropType,
|
|
6
7
|
Pressable,
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
ViewStyle,
|
|
11
12
|
} from 'react-native';
|
|
12
13
|
|
|
13
|
-
import
|
|
14
|
+
import LinearGradient from 'react-native-linear-gradient';
|
|
14
15
|
|
|
15
16
|
import { type IconName, SIZE_MAP, resolveIcon } from '../../icons';
|
|
16
17
|
import { useTheme } from '../../theme';
|
|
@@ -202,13 +203,14 @@ export function ServiceCard({
|
|
|
202
203
|
borderWidth: v.borderWidth,
|
|
203
204
|
borderRadius:
|
|
204
205
|
variant === 'doubled' ? dim.borderRadius.sysRadiusXl : s.borderRadius,
|
|
205
|
-
|
|
206
|
-
variant === 'doubled' ? dim.spacing.padding.sysPadding8 : s.paddingH,
|
|
207
|
-
paddingVertical:
|
|
208
|
-
variant === 'doubled' ? dim.spacing.padding.sysPadding8 : s.paddingV,
|
|
206
|
+
|
|
209
207
|
opacity: disabled ? 0.48 : 1,
|
|
210
208
|
},
|
|
211
209
|
v.elevated && ELEVATION,
|
|
210
|
+
variant !== 'doubled' && {
|
|
211
|
+
paddingHorizontal: s.paddingH,
|
|
212
|
+
paddingVertical: s.paddingV,
|
|
213
|
+
},
|
|
212
214
|
fullWidth && { alignSelf: 'stretch' as const },
|
|
213
215
|
isRow ? styles.rowRoot : styles.colRoot,
|
|
214
216
|
!isRow && variant !== 'image' && { gap: s.gap },
|
|
@@ -240,10 +242,13 @@ export function ServiceCard({
|
|
|
240
242
|
const doubledInnerStyle: StyleProp<ViewStyle> = [
|
|
241
243
|
styles.colRoot,
|
|
242
244
|
{
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
alignSelf: 'stretch',
|
|
246
|
+
borderRadius: dim.borderRadius.sysRadiusXl,
|
|
247
|
+
borderColor: cr.accent.primary.sysOnPrimary,
|
|
248
|
+
borderWidth: dim.spacing.padding.sysPadding8,
|
|
249
|
+
paddingHorizontal: dim.spacing.padding.sysPadding8,
|
|
250
|
+
paddingVertical: dim.spacing.padding.sysPadding16,
|
|
251
|
+
gap: dim.spacing.padding.sysPadding12,
|
|
247
252
|
},
|
|
248
253
|
];
|
|
249
254
|
|
|
@@ -267,17 +272,21 @@ export function ServiceCard({
|
|
|
267
272
|
</>
|
|
268
273
|
);
|
|
269
274
|
|
|
270
|
-
inner =
|
|
271
|
-
<
|
|
272
|
-
|
|
273
|
-
start={gradientStart ?? { x: 0, y: 0 }}
|
|
274
|
-
end={gradientEnd ?? { x: 0, y: 1 }}
|
|
275
|
-
style={doubledInnerStyle}
|
|
275
|
+
inner = (
|
|
276
|
+
<View
|
|
277
|
+
style={[doubledInnerStyle, !gradient && { backgroundColor: v.innerBg }]}
|
|
276
278
|
>
|
|
277
|
-
{
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
{gradient && (
|
|
280
|
+
<LinearGradient
|
|
281
|
+
colors={gradient}
|
|
282
|
+
start={gradientStart ?? { x: 0, y: 0 }}
|
|
283
|
+
end={gradientEnd ?? { x: 0, y: 1 }}
|
|
284
|
+
style={[
|
|
285
|
+
StyleSheet.absoluteFillObject,
|
|
286
|
+
{ borderRadius: dim.borderRadius.sysRadiusLg },
|
|
287
|
+
]}
|
|
288
|
+
/>
|
|
289
|
+
)}
|
|
281
290
|
{doubledInnerContent}
|
|
282
291
|
</View>
|
|
283
292
|
);
|
|
@@ -296,7 +305,11 @@ export function ServiceCard({
|
|
|
296
305
|
accessible={false}
|
|
297
306
|
/>
|
|
298
307
|
<LinearGradient
|
|
299
|
-
colors={[
|
|
308
|
+
colors={[
|
|
309
|
+
'rgba(0,0,0,0.4)',
|
|
310
|
+
'rgba(0,0,0,0.05)',
|
|
311
|
+
'rgba(0,0,0,0.5)',
|
|
312
|
+
]}
|
|
300
313
|
locations={[0.012, 0.268, 1]}
|
|
301
314
|
style={[
|
|
302
315
|
{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
|
|
@@ -313,7 +326,11 @@ export function ServiceCard({
|
|
|
313
326
|
<View style={{ flex: 1 }}>{textBlock}</View>
|
|
314
327
|
{onPress && ChevronRight && (
|
|
315
328
|
<View style={styles.imageArrowButton}>
|
|
316
|
-
<ChevronRight
|
|
329
|
+
<ChevronRight
|
|
330
|
+
size={16}
|
|
331
|
+
color={cr.surface.surface.sysOnSurface}
|
|
332
|
+
strokeWidth={1.5}
|
|
333
|
+
/>
|
|
317
334
|
</View>
|
|
318
335
|
)}
|
|
319
336
|
</View>
|
|
@@ -352,7 +369,9 @@ export function ServiceCard({
|
|
|
352
369
|
style={{
|
|
353
370
|
position: 'absolute',
|
|
354
371
|
bottom: s.paddingV,
|
|
355
|
-
|
|
372
|
+
...(I18nManager.isRTL
|
|
373
|
+
? { left: s.paddingH }
|
|
374
|
+
: { right: s.paddingH }),
|
|
356
375
|
}}
|
|
357
376
|
pointerEvents="none"
|
|
358
377
|
>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { render, screen } from '@testing-library/react-native';
|
|
4
|
+
|
|
5
|
+
import { ThemeProvider } from '../../theme';
|
|
6
|
+
import { StatusChip } from './index';
|
|
7
|
+
|
|
8
|
+
function renderWithTheme(ui: React.ReactElement) {
|
|
9
|
+
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('StatusChip', () => {
|
|
13
|
+
it('renders the label', () => {
|
|
14
|
+
renderWithTheme(<StatusChip label="10 minutes remaining" />);
|
|
15
|
+
expect(screen.getByText('10 minutes remaining')).toBeTruthy();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders warning variant by default', () => {
|
|
19
|
+
renderWithTheme(<StatusChip label="Warning" />);
|
|
20
|
+
expect(screen.getByText('Warning')).toBeTruthy();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('renders error variant', () => {
|
|
24
|
+
renderWithTheme(<StatusChip label="Session ended" variant="error" />);
|
|
25
|
+
expect(screen.getByText('Session ended')).toBeTruthy();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('accepts a custom icon', () => {
|
|
29
|
+
renderWithTheme(<StatusChip label="Custom" icon="TimerIcon" />);
|
|
30
|
+
expect(screen.getByText('Custom')).toBeTruthy();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('renders with left alignment', () => {
|
|
34
|
+
renderWithTheme(<StatusChip label="Left" align="left" />);
|
|
35
|
+
expect(screen.getByText('Left')).toBeTruthy();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders with right alignment', () => {
|
|
39
|
+
renderWithTheme(<StatusChip label="Right" align="right" />);
|
|
40
|
+
expect(screen.getByText('Right')).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders fullWidth', () => {
|
|
44
|
+
renderWithTheme(<StatusChip label="Full" fullWidth />);
|
|
45
|
+
expect(screen.getByText('Full')).toBeTruthy();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { type IconName, SIZE_MAP, resolveIcon } from '../../icons';
|
|
6
|
+
import { useTheme } from '../../theme';
|
|
7
|
+
import { BodyText } from '../BodyText';
|
|
8
|
+
|
|
9
|
+
export type StatusChipVariant = 'warning' | 'error';
|
|
10
|
+
export type StatusChipAlign = 'left' | 'center' | 'right';
|
|
11
|
+
|
|
12
|
+
export interface StatusChipProps {
|
|
13
|
+
label: string;
|
|
14
|
+
variant?: StatusChipVariant;
|
|
15
|
+
/** Icon name — defaults to ClockFadingIcon (warning) or ClockAlertIcon (error) */
|
|
16
|
+
icon?: IconName;
|
|
17
|
+
align?: StatusChipAlign;
|
|
18
|
+
fullWidth?: boolean;
|
|
19
|
+
style?: StyleProp<ViewStyle>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_ICONS: Record<StatusChipVariant, IconName> = {
|
|
23
|
+
warning: 'ClockFadingIcon',
|
|
24
|
+
error: 'ClockAlertIcon',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const JUSTIFY: Record<StatusChipAlign, 'flex-start' | 'center' | 'flex-end'> = {
|
|
28
|
+
left: 'flex-start',
|
|
29
|
+
center: 'center',
|
|
30
|
+
right: 'flex-end',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function StatusChip({
|
|
34
|
+
label,
|
|
35
|
+
variant = 'warning',
|
|
36
|
+
icon,
|
|
37
|
+
align = 'center',
|
|
38
|
+
fullWidth = false,
|
|
39
|
+
style,
|
|
40
|
+
}: StatusChipProps) {
|
|
41
|
+
const { colorRoles: cr } = useTheme();
|
|
42
|
+
|
|
43
|
+
const colors =
|
|
44
|
+
variant === 'warning'
|
|
45
|
+
? {
|
|
46
|
+
bg: cr.custom.warning.sysWarningContainer,
|
|
47
|
+
text: cr.custom.warning.sysOnWarningContainer,
|
|
48
|
+
}
|
|
49
|
+
: {
|
|
50
|
+
bg: cr.error.sysErrorContainer,
|
|
51
|
+
text: cr.error.sysOnErrorContainer,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const iconName = icon ?? DEFAULT_ICONS[variant];
|
|
55
|
+
const IconComponent = resolveIcon(iconName);
|
|
56
|
+
const { size: iconPx, strokeWidth: iconSW } = SIZE_MAP['small'];
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<View
|
|
60
|
+
style={[
|
|
61
|
+
styles.chip,
|
|
62
|
+
{
|
|
63
|
+
backgroundColor: colors.bg,
|
|
64
|
+
justifyContent: JUSTIFY[align],
|
|
65
|
+
alignSelf: fullWidth ? ('stretch' as const) : ('flex-start' as const),
|
|
66
|
+
},
|
|
67
|
+
style,
|
|
68
|
+
]}
|
|
69
|
+
>
|
|
70
|
+
{IconComponent && (
|
|
71
|
+
<IconComponent size={iconPx} strokeWidth={iconSW} color={colors.text} />
|
|
72
|
+
)}
|
|
73
|
+
<BodyText variant="labelMedium" color={colors.text} numberOfLines={1}>
|
|
74
|
+
{label}
|
|
75
|
+
</BodyText>
|
|
76
|
+
</View>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const styles = StyleSheet.create({
|
|
81
|
+
chip: {
|
|
82
|
+
flexDirection: 'row',
|
|
83
|
+
alignItems: 'center',
|
|
84
|
+
borderRadius: 9999,
|
|
85
|
+
paddingHorizontal: 16,
|
|
86
|
+
paddingVertical: 4,
|
|
87
|
+
gap: 8,
|
|
88
|
+
height: 32,
|
|
89
|
+
},
|
|
90
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -132,6 +132,13 @@ export type {
|
|
|
132
132
|
export { ChatInput } from './components/ChatInput';
|
|
133
133
|
export type { ChatInputProps, ChatAttachment } from './components/ChatInput';
|
|
134
134
|
|
|
135
|
+
export { StatusChip } from './components/StatusChip';
|
|
136
|
+
export type {
|
|
137
|
+
StatusChipProps,
|
|
138
|
+
StatusChipVariant,
|
|
139
|
+
StatusChipAlign,
|
|
140
|
+
} from './components/StatusChip';
|
|
141
|
+
|
|
135
142
|
export { BodyText } from './components/BodyText';
|
|
136
143
|
export type { BodyTextProps, BodyVariant } from './components/BodyText';
|
|
137
144
|
|