@unif/react-native-chat 3.2.0 → 3.2.2

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 (32) hide show
  1. package/lib/module/components/Attachments/AttachmentItem.js +203 -72
  2. package/lib/module/components/Attachments/AttachmentItem.js.map +1 -1
  3. package/lib/module/components/Attachments/Attachments.js +6 -18
  4. package/lib/module/components/Attachments/Attachments.js.map +1 -1
  5. package/lib/module/components/Attachments/constants.js +6 -2
  6. package/lib/module/components/Attachments/constants.js.map +1 -1
  7. package/lib/module/components/Attachments/styles.js +116 -23
  8. package/lib/module/components/Attachments/styles.js.map +1 -1
  9. package/lib/module/components/Process/ProcessCompactStep.js +1 -0
  10. package/lib/module/components/Process/ProcessCompactStep.js.map +1 -1
  11. package/lib/module/components/Process/styles.js +4 -0
  12. package/lib/module/components/Process/styles.js.map +1 -1
  13. package/lib/typescript/src/components/Attachments/AttachmentItem.d.ts +1 -1
  14. package/lib/typescript/src/components/Attachments/AttachmentItem.d.ts.map +1 -1
  15. package/lib/typescript/src/components/Attachments/Attachments.d.ts.map +1 -1
  16. package/lib/typescript/src/components/Attachments/constants.d.ts +5 -2
  17. package/lib/typescript/src/components/Attachments/constants.d.ts.map +1 -1
  18. package/lib/typescript/src/components/Attachments/styles.d.ts +103 -11
  19. package/lib/typescript/src/components/Attachments/styles.d.ts.map +1 -1
  20. package/lib/typescript/src/components/Attachments/types.d.ts +1 -0
  21. package/lib/typescript/src/components/Attachments/types.d.ts.map +1 -1
  22. package/lib/typescript/src/components/Process/ProcessCompactStep.d.ts.map +1 -1
  23. package/lib/typescript/src/components/Process/styles.d.ts +4 -0
  24. package/lib/typescript/src/components/Process/styles.d.ts.map +1 -1
  25. package/package.json +1 -1
  26. package/src/components/Attachments/AttachmentItem.tsx +267 -76
  27. package/src/components/Attachments/Attachments.tsx +10 -20
  28. package/src/components/Attachments/constants.ts +9 -2
  29. package/src/components/Attachments/styles.ts +114 -20
  30. package/src/components/Attachments/types.ts +1 -0
  31. package/src/components/Process/ProcessCompactStep.tsx +1 -0
  32. package/src/components/Process/styles.ts +1 -0
@@ -1,4 +1,4 @@
1
- import { Text, View } from 'react-native';
1
+ import { Pressable, Text, View } from 'react-native';
2
2
  import {
3
3
  Button,
4
4
  CircularProgress,
@@ -6,11 +6,17 @@ import {
6
6
  IconButton,
7
7
  radius,
8
8
  Spinner,
9
+ Thumbnail,
10
+ useColors,
9
11
  useThemedStyles,
10
12
  } from '@unif/react-native-design';
11
- import { ATTACHMENT_ICONS, STATUS_LABELS } from './constants';
13
+ import {
14
+ ATTACHMENT_ICONS,
15
+ ATTACHMENT_STATUS_OWNER_SIZE,
16
+ ROW_IMAGE_SIZE,
17
+ STATUS_LABELS,
18
+ } from './constants';
12
19
  import { createStyles } from './styles';
13
- import { Thumbnail } from '@unif/react-native-design';
14
20
  import type { AttachmentItemProps } from './types';
15
21
 
16
22
  export function AttachmentItem({
@@ -18,15 +24,18 @@ export function AttachmentItem({
18
24
  width,
19
25
  imageSize,
20
26
  row,
27
+ mixed = false,
21
28
  showProgressLabel = false,
22
29
  onPreview,
23
30
  onRemove,
24
31
  }: AttachmentItemProps) {
25
32
  const styles = useThemedStyles(createStyles);
33
+ const colors = useColors();
26
34
  const name = item.name || '未命名附件';
27
35
  const status = item.status ?? 'idle';
28
- const loadingVisual = item.loadingVisual ?? 'center';
29
36
  const busy = status === 'uploading' || status === 'processing';
37
+ const failed = status === 'failed';
38
+ const caption = item.loadingVisual === 'caption';
30
39
  const progress =
31
40
  status === 'uploading' &&
32
41
  typeof item.progress === 'number' &&
@@ -36,99 +45,281 @@ export function AttachmentItem({
36
45
  ? item.progress
37
46
  : undefined;
38
47
  const statusText = item.statusText ?? STATUS_LABELS[status];
39
- const icon = <Icon name={ATTACHMENT_ICONS[item.kind ?? 'file']} size={28} />;
40
- const media = (
41
- <View style={[styles.media, { minWidth: imageSize, minHeight: imageSize }]}>
42
- <View pointerEvents="none" style={styles.imageLayer}>
43
- {item.thumbnail ? (
44
- <Thumbnail
45
- source={item.thumbnail}
46
- size={{
47
- width: imageSize,
48
- height: imageSize,
49
- borderRadius: radius.sm,
50
- }}
51
- fallback={icon}
48
+ const previewable = item.previewable && onPreview;
49
+ const separatePreview = previewable && (busy || failed);
50
+ const removable = item.removable && onRemove;
51
+ const singleAction = item.actions?.length === 1 ? item.actions[0] : undefined;
52
+ const retry =
53
+ failed &&
54
+ !caption &&
55
+ singleAction &&
56
+ (singleAction.id === 'retry' ||
57
+ singleAction.icon === 'retry' ||
58
+ singleAction.icon === 'refresh')
59
+ ? singleAction
60
+ : undefined;
61
+ const icon = (
62
+ <Icon name={ATTACHMENT_ICONS[item.kind ?? 'file']} size={ROW_IMAGE_SIZE} />
63
+ );
64
+ const thumbnail = item.thumbnail ? (
65
+ <Thumbnail
66
+ source={item.thumbnail}
67
+ size={{ width: imageSize, height: imageSize, borderRadius: radius.sm }}
68
+ fallback={icon}
69
+ />
70
+ ) : (
71
+ <View style={[styles.placeholder, { width: imageSize, height: imageSize }]}>
72
+ {icon}
73
+ </View>
74
+ );
75
+ const statusVisual =
76
+ busy || failed ? (
77
+ <View style={styles.statusVisual}>
78
+ {failed ? (
79
+ <Icon
80
+ name={retry?.icon ?? 'refresh'}
81
+ size={16}
82
+ color={colors.onPrimary}
83
+ />
84
+ ) : progress !== undefined ? (
85
+ <CircularProgress
86
+ value={progress}
87
+ size={36}
88
+ showLabel={showProgressLabel}
89
+ color={colors.onPrimary}
90
+ accessibilityLabel={`${name}上传进度`}
52
91
  />
53
92
  ) : (
54
- <View
55
- style={[
56
- styles.media,
57
- styles.placeholder,
58
- { width: imageSize, height: imageSize },
59
- ]}
60
- >
61
- {icon}
62
- </View>
93
+ <Spinner size={16} color={colors.onPrimary} />
63
94
  )}
64
95
  </View>
65
- {busy && loadingVisual === 'center' ? (
66
- <View pointerEvents="none" style={styles.progress}>
67
- {progress !== undefined ? (
68
- <CircularProgress
69
- value={progress}
70
- showLabel={showProgressLabel}
71
- accessibilityLabel={`${name}上传进度`}
72
- />
73
- ) : (
74
- <Spinner />
75
- )}
96
+ ) : null;
97
+ const statusOwner =
98
+ !caption && statusVisual ? (
99
+ retry ? (
100
+ <Pressable
101
+ style={[
102
+ styles.statusOwner,
103
+ {
104
+ top: (imageSize - ATTACHMENT_STATUS_OWNER_SIZE) / 2,
105
+ left: (imageSize - ATTACHMENT_STATUS_OWNER_SIZE) / 2,
106
+ },
107
+ styles.retryOwner,
108
+ ]}
109
+ accessibilityRole="button"
110
+ accessibilityLabel={retry.label}
111
+ accessibilityHint={retry.accessibilityHint}
112
+ accessibilityState={{
113
+ disabled: !!(retry.disabled || retry.loading),
114
+ busy: !!retry.loading,
115
+ }}
116
+ disabled={retry.disabled || retry.loading}
117
+ onPress={() => retry.onPress()}
118
+ >
119
+ {statusVisual}
120
+ </Pressable>
121
+ ) : (
122
+ <View
123
+ style={[
124
+ styles.statusOwner,
125
+ {
126
+ top: (imageSize - ATTACHMENT_STATUS_OWNER_SIZE) / 2,
127
+ left: (imageSize - ATTACHMENT_STATUS_OWNER_SIZE) / 2,
128
+ },
129
+ ]}
130
+ accessible
131
+ accessibilityLabel={statusText}
132
+ accessibilityState={{ busy }}
133
+ >
134
+ {statusVisual}
76
135
  </View>
77
- ) : null}
78
- </View>
79
- );
136
+ )
137
+ ) : null;
138
+ const actions = item.actions
139
+ ?.filter((action) => action !== retry)
140
+ .map((action) => (
141
+ <Button
142
+ key={action.id}
143
+ label={action.label}
144
+ leftIcon={action.icon}
145
+ variant="secondary"
146
+ style={styles.action}
147
+ size="sm"
148
+ disabled={action.disabled}
149
+ loading={action.loading}
150
+ accessibilityHint={action.accessibilityHint}
151
+ onPress={action.onPress}
152
+ />
153
+ ));
80
154
 
81
- return (
82
- <View style={[styles.item, row ? styles.row : { width }]}>
83
- {media}
84
- <View style={[styles.content, row && styles.rowContent]}>
85
- <Text style={styles.name}>{name}</Text>
86
- {item.meta ? <Text style={styles.meta}>{item.meta}</Text> : null}
87
- {statusText ? (
88
- <Text
89
- style={[styles.meta, status === 'failed' && styles.failed]}
90
- accessibilityLiveRegion="polite"
91
- >
92
- {statusText}
155
+ if (row) {
156
+ const content = (
157
+ <>
158
+ <View style={styles.media}>
159
+ {thumbnail}
160
+ {statusOwner}
161
+ </View>
162
+ <View style={[styles.content, styles.rowContent]}>
163
+ <Text style={styles.name} numberOfLines={1} ellipsizeMode="middle">
164
+ {name}
93
165
  </Text>
94
- ) : null}
95
- {busy &&
96
- loadingVisual === 'caption' &&
97
- showProgressLabel &&
98
- progress !== undefined ? (
99
- <Text style={styles.meta}>{`${Math.round(progress * 100)}%`}</Text>
100
- ) : null}
166
+ {item.meta ? (
167
+ <Text
168
+ style={[styles.meta, !mixed && styles.fileMeta]}
169
+ numberOfLines={1}
170
+ >
171
+ {item.meta}
172
+ </Text>
173
+ ) : null}
174
+ {statusText ? (
175
+ <Text
176
+ style={[styles.meta, failed && styles.failed]}
177
+ accessibilityLiveRegion="polite"
178
+ >
179
+ {statusText}
180
+ </Text>
181
+ ) : null}
182
+ {busy && caption && showProgressLabel && progress !== undefined ? (
183
+ <Text style={styles.meta}>{`${Math.round(progress * 100)}%`}</Text>
184
+ ) : null}
185
+ </View>
186
+ </>
187
+ );
188
+ return (
189
+ <View style={[styles.item, styles.row, mixed && styles.mixedRow]}>
190
+ {previewable && !retry ? (
191
+ <Pressable
192
+ style={[styles.rowBody, mixed && styles.mixedRowBody]}
193
+ accessibilityRole="button"
194
+ accessibilityLabel={`预览${name}`}
195
+ onPress={() => onPreview?.(item)}
196
+ >
197
+ {content}
198
+ </Pressable>
199
+ ) : (
200
+ <View style={[styles.rowBody, mixed && styles.mixedRowBody]}>
201
+ {content}
202
+ </View>
203
+ )}
101
204
  <View style={styles.actions}>
102
- {item.previewable && onPreview ? (
205
+ {previewable && retry ? (
103
206
  <IconButton
104
207
  icon="eye"
208
+ size="sm"
105
209
  accessibilityLabel={`预览${name}`}
106
- onPress={() => onPreview(item)}
210
+ onPress={() => onPreview?.(item)}
107
211
  />
108
212
  ) : null}
109
- {item.removable && onRemove ? (
213
+ {removable ? (
110
214
  <IconButton
111
215
  icon="close"
216
+ size="sm"
112
217
  accessibilityLabel={`移除${name}`}
113
218
  disabled={item.removeDisabled}
114
- onPress={() => onRemove(item)}
219
+ onPress={() => onRemove?.(item)}
115
220
  />
116
221
  ) : null}
117
- {item.actions?.map((action) => (
118
- <Button
119
- key={action.id}
120
- label={action.label}
121
- leftIcon={action.icon}
122
- variant="secondary"
123
- style={styles.action}
124
- disabled={action.disabled}
125
- loading={action.loading}
126
- accessibilityHint={action.accessibilityHint}
127
- onPress={action.onPress}
128
- />
129
- ))}
222
+ {actions}
130
223
  </View>
131
224
  </View>
225
+ );
226
+ }
227
+
228
+ const removeLeft =
229
+ previewable && !separatePreview
230
+ ? width!
231
+ : (busy && !caption) || failed
232
+ ? (imageSize - ATTACHMENT_STATUS_OWNER_SIZE) / 2 +
233
+ ATTACHMENT_STATUS_OWNER_SIZE
234
+ : imageSize - ATTACHMENT_STATUS_OWNER_SIZE;
235
+ const ownerWidth = removable
236
+ ? Math.max(width!, removeLeft + ATTACHMENT_STATUS_OWNER_SIZE)
237
+ : width;
238
+ const picture = (
239
+ <>
240
+ {thumbnail}
241
+ {item.kind !== 'image' ? (
242
+ <Text style={styles.documentName} numberOfLines={2}>
243
+ {name}
244
+ </Text>
245
+ ) : null}
246
+ </>
247
+ );
248
+ return (
249
+ <View style={[styles.tileOwner, { width: ownerWidth }]}>
250
+ {previewable && !separatePreview ? (
251
+ <Pressable
252
+ style={[
253
+ styles.tile,
254
+ { width, height: imageSize },
255
+ failed && styles.tileFailed,
256
+ ]}
257
+ accessibilityRole="button"
258
+ accessibilityLabel={`预览${name}`}
259
+ onPress={() => onPreview?.(item)}
260
+ >
261
+ {picture}
262
+ </Pressable>
263
+ ) : (
264
+ <View
265
+ style={[
266
+ styles.tile,
267
+ { width, height: imageSize },
268
+ failed && styles.tileFailed,
269
+ ]}
270
+ accessible={!statusOwner}
271
+ accessibilityLabel={name}
272
+ >
273
+ {picture}
274
+ </View>
275
+ )}
276
+ {statusOwner}
277
+ {removable ? (
278
+ <Pressable
279
+ style={[styles.removeOwner, { left: removeLeft }]}
280
+ accessibilityRole="button"
281
+ accessibilityLabel={`移除${name}`}
282
+ disabled={item.removeDisabled}
283
+ accessibilityState={{ disabled: !!item.removeDisabled }}
284
+ onPress={() => onRemove?.(item)}
285
+ >
286
+ <View style={[styles.removeVisual, failed && styles.removeFailed]}>
287
+ <Icon
288
+ name="close"
289
+ size={10}
290
+ color={colors.onPrimary}
291
+ strokeWidth={2.5}
292
+ />
293
+ </View>
294
+ </Pressable>
295
+ ) : null}
296
+ {caption && (busy || failed) ? (
297
+ <View style={styles.caption} accessibilityLiveRegion="polite">
298
+ {status === 'processing' ? <Spinner size={10} /> : null}
299
+ <Text
300
+ style={[styles.captionText, failed && styles.failed]}
301
+ numberOfLines={1}
302
+ >
303
+ {statusText}
304
+ </Text>
305
+ {showProgressLabel && progress !== undefined ? (
306
+ <Text
307
+ style={styles.captionText}
308
+ >{`${Math.round(progress * 100)}%`}</Text>
309
+ ) : null}
310
+ </View>
311
+ ) : null}
312
+ {separatePreview ? (
313
+ <View style={styles.previewAction}>
314
+ <IconButton
315
+ icon="eye"
316
+ size="sm"
317
+ accessibilityLabel={`预览${name}`}
318
+ onPress={() => onPreview?.(item)}
319
+ />
320
+ </View>
321
+ ) : null}
322
+ {actions?.length ? <View style={styles.actions}>{actions}</View> : null}
132
323
  </View>
133
324
  );
134
325
  }
@@ -1,7 +1,6 @@
1
- import { useState } from 'react';
2
1
  import { ScrollView, View } from 'react-native';
3
- import { space, useThemedStyles } from '@unif/react-native-design';
4
- import { CARD_WIDTH, ROW_IMAGE_SIZE } from './constants';
2
+ import { useThemedStyles } from '@unif/react-native-design';
3
+ import { CARD_WIDTH, MIXED_IMAGE_SIZE, ROW_IMAGE_SIZE } from './constants';
5
4
  import { createStyles } from './styles';
6
5
  import { AttachmentItem } from './AttachmentItem';
7
6
  import type { AttachmentsProps } from './types';
@@ -16,15 +15,6 @@ export function Attachments({
16
15
  testID,
17
16
  }: AttachmentsProps) {
18
17
  const styles = useThemedStyles(createStyles);
19
- const [availableWidth, setAvailableWidth] = useState(0);
20
- const columns = Math.max(
21
- 1,
22
- Math.floor((availableWidth + space[2]) / (CARD_WIDTH + space[2]))
23
- );
24
- const cardWidth =
25
- layout === 'grid' && availableWidth > 0
26
- ? (availableWidth - space[2] * (columns - 1)) / columns
27
- : Math.min(CARD_WIDTH, availableWidth || CARD_WIDTH);
28
18
  const content = items.map((item) => {
29
19
  const row =
30
20
  layout === 'list' || (layout === 'mixed' && item.kind !== 'image');
@@ -33,9 +23,14 @@ export function Attachments({
33
23
  key={item.id}
34
24
  item={item}
35
25
  row={row}
36
- width={cardWidth}
26
+ mixed={layout === 'mixed'}
27
+ width={CARD_WIDTH}
37
28
  imageSize={
38
- row ? ROW_IMAGE_SIZE : Math.max(1, cardWidth - space[2] * 2 - 2)
29
+ row
30
+ ? layout === 'mixed'
31
+ ? MIXED_IMAGE_SIZE
32
+ : ROW_IMAGE_SIZE
33
+ : CARD_WIDTH
39
34
  }
40
35
  showProgressLabel={showProgressLabel}
41
36
  onPreview={onPreview}
@@ -49,10 +44,8 @@ export function Attachments({
49
44
  <ScrollView
50
45
  horizontal
51
46
  keyboardShouldPersistTaps="handled"
47
+ showsHorizontalScrollIndicator={false}
52
48
  contentContainerStyle={styles.carousel}
53
- onLayout={({ nativeEvent: { layout: measured } }) =>
54
- setAvailableWidth(measured.width)
55
- }
56
49
  >
57
50
  {content}
58
51
  </ScrollView>
@@ -60,9 +53,6 @@ export function Attachments({
60
53
  <View
61
54
  testID={testID ? `${testID}-content` : undefined}
62
55
  style={styles.collection}
63
- onLayout={({ nativeEvent: { layout: measured } }) =>
64
- setAvailableWidth(measured.width)
65
- }
66
56
  >
67
57
  {content}
68
58
  </View>
@@ -1,8 +1,15 @@
1
+ import { icon, r } from '@unif/react-native-design';
1
2
  import type { IconName } from '@unif/react-native-design';
2
3
  import type { ChatAttachmentItem } from './types';
3
4
 
4
- export const CARD_WIDTH = 144;
5
- export const ROW_IMAGE_SIZE = 56;
5
+ export const CARD_WIDTH = r(76);
6
+ export const ROW_IMAGE_SIZE = icon.xl;
7
+ export const MIXED_IMAGE_SIZE = r(36);
8
+ export const ATTACHMENT_STATUS_SIZE = r(40);
9
+ export const ATTACHMENT_STATUS_OWNER_SIZE = Math.max(
10
+ 44,
11
+ ATTACHMENT_STATUS_SIZE
12
+ );
6
13
  export const ATTACHMENT_ICONS: Record<
7
14
  NonNullable<ChatAttachmentItem['kind']>,
8
15
  IconName
@@ -1,6 +1,19 @@
1
1
  import { StyleSheet } from 'react-native';
2
- import { radius, space, type } from '@unif/react-native-design';
2
+ import {
3
+ fontMono,
4
+ fw,
5
+ icon,
6
+ r,
7
+ radius,
8
+ space,
9
+ type,
10
+ } from '@unif/react-native-design';
3
11
  import type { ColorTokens } from '@unif/react-native-design';
12
+ import {
13
+ ATTACHMENT_STATUS_OWNER_SIZE,
14
+ ATTACHMENT_STATUS_SIZE,
15
+ CARD_WIDTH,
16
+ } from './constants';
4
17
 
5
18
  export const createStyles = (colors: ColorTokens) =>
6
19
  StyleSheet.create({
@@ -9,45 +22,126 @@ export const createStyles = (colors: ColorTokens) =>
9
22
  flexDirection: 'row',
10
23
  flexWrap: 'wrap',
11
24
  alignItems: 'flex-start',
12
- gap: space[2],
25
+ gap: space[3],
26
+ },
27
+ carousel: { gap: space[3], alignItems: 'flex-start' },
28
+ tileOwner: { position: 'relative', minWidth: 0 },
29
+ tile: {
30
+ borderWidth: StyleSheet.hairlineWidth,
31
+ borderColor: colors.outline,
32
+ borderRadius: radius.md,
33
+ backgroundColor: colors.surfaceContainerHigh,
34
+ overflow: 'hidden',
35
+ },
36
+ tileFailed: { borderColor: colors.error, borderWidth: 2 },
37
+ documentName: {
38
+ position: 'absolute',
39
+ left: space[1],
40
+ right: space[1],
41
+ bottom: space[1],
42
+ fontSize: type.micro,
43
+ color: colors.foreground,
44
+ textAlign: 'center',
45
+ backgroundColor: colors.surfaceContainerHigh,
13
46
  },
14
- carousel: { gap: space[2], alignItems: 'flex-start' },
15
47
  item: {
16
- gap: space[2],
17
- padding: space[2],
48
+ gap: space[4],
49
+ padding: space[4],
18
50
  borderWidth: 1,
19
51
  borderColor: colors.outline,
20
52
  borderRadius: radius.md,
21
53
  backgroundColor: colors.surface,
22
54
  },
23
55
  row: { width: '100%', flexDirection: 'row', alignItems: 'center' },
24
- content: { minWidth: 0, flexShrink: 1, gap: space[1] },
56
+ rowBody: {
57
+ flex: 1,
58
+ minWidth: 0,
59
+ flexDirection: 'row',
60
+ alignItems: 'center',
61
+ gap: space[4],
62
+ },
63
+ mixedRowBody: { gap: space[3] },
64
+ mixedRow: {
65
+ height: r(64),
66
+ paddingVertical: 0,
67
+ paddingHorizontal: space[3],
68
+ borderWidth: 0,
69
+ borderRadius: 0,
70
+ },
71
+ content: { minWidth: 0, flexShrink: 1 },
25
72
  rowContent: { flex: 1 },
26
73
  media: {
27
74
  alignItems: 'center',
28
75
  justifyContent: 'center',
29
- alignSelf: 'center',
76
+ position: 'relative',
77
+ },
78
+ placeholder: {
79
+ backgroundColor: colors.surfaceContainerHigh,
80
+ borderRadius: radius.sm,
81
+ alignItems: 'center',
82
+ justifyContent: 'center',
30
83
  },
31
- imageLayer: {
84
+ statusOwner: {
32
85
  position: 'absolute',
86
+ top: (CARD_WIDTH - ATTACHMENT_STATUS_OWNER_SIZE) / 2,
87
+ left: (CARD_WIDTH - ATTACHMENT_STATUS_OWNER_SIZE) / 2,
88
+ width: ATTACHMENT_STATUS_OWNER_SIZE,
89
+ height: ATTACHMENT_STATUS_OWNER_SIZE,
33
90
  alignItems: 'center',
34
91
  justifyContent: 'center',
92
+ },
93
+ retryOwner: { zIndex: 3, elevation: 3 },
94
+ statusVisual: {
95
+ width: ATTACHMENT_STATUS_SIZE,
96
+ height: ATTACHMENT_STATUS_SIZE,
97
+ borderRadius: radius.pill,
98
+ backgroundColor: colors.scrim,
99
+ alignItems: 'center',
100
+ justifyContent: 'center',
101
+ },
102
+ removeOwner: {
103
+ position: 'absolute',
35
104
  top: 0,
36
- bottom: 0,
37
- left: 0,
38
- right: 0,
105
+ width: 44,
106
+ height: 44,
107
+ paddingTop: space[1],
108
+ zIndex: 2,
109
+ elevation: 2,
39
110
  },
40
- placeholder: {
41
- backgroundColor: colors.surfaceContainer,
42
- borderRadius: radius.sm,
111
+ removeVisual: {
112
+ width: icon.xs,
113
+ height: icon.xs,
114
+ borderRadius: radius.pill,
115
+ backgroundColor: colors.scrim,
116
+ alignItems: 'center',
117
+ justifyContent: 'center',
43
118
  },
44
- progress: {
45
- padding: space[1],
46
- borderRadius: radius.sm,
47
- backgroundColor: colors.surface,
119
+ removeFailed: { backgroundColor: colors.error },
120
+ caption: {
121
+ flexDirection: 'row',
122
+ alignItems: 'center',
123
+ justifyContent: 'center',
124
+ gap: space[1],
125
+ paddingTop: space[1],
126
+ width: CARD_WIDTH,
127
+ },
128
+ captionText: {
129
+ flexShrink: 1,
130
+ fontSize: type.micro,
131
+ color: colors.foregroundMuted,
132
+ },
133
+ previewAction: { width: CARD_WIDTH, alignItems: 'center' },
134
+ name: {
135
+ color: colors.foreground,
136
+ fontSize: type.xs,
137
+ fontWeight: fw.medium,
138
+ },
139
+ meta: {
140
+ color: colors.foregroundSubtle,
141
+ fontSize: type.micro,
142
+ marginTop: r(2),
48
143
  },
49
- name: { color: colors.foreground, fontSize: type.body },
50
- meta: { color: colors.foregroundMuted, fontSize: type.sm },
144
+ fileMeta: { fontFamily: fontMono },
51
145
  failed: { color: colors.error },
52
146
  action: { maxWidth: '100%' },
53
147
  actions: { flexDirection: 'row', flexWrap: 'wrap', gap: space[1] },
@@ -35,4 +35,5 @@ export interface AttachmentItemProps extends Pick<
35
35
  width?: number;
36
36
  imageSize: number;
37
37
  row: boolean;
38
+ mixed?: boolean;
38
39
  }
@@ -93,6 +93,7 @@ export function ProcessCompactStep({
93
93
  {step.actions.map((action) => (
94
94
  <Button
95
95
  key={action.id}
96
+ style={styles.compactAction}
96
97
  label={action.label}
97
98
  leftIcon={action.icon}
98
99
  onPress={action.onPress}
@@ -6,6 +6,7 @@ export const createStyles = (colors: ColorTokens) =>
6
6
  StyleSheet.create({
7
7
  compactRoot: { gap: space[2] },
8
8
  compactSteps: { gap: space[2] },
9
+ compactAction: { minWidth: 44, minHeight: 44 },
9
10
  compactHeader: {
10
11
  flexDirection: 'row',
11
12
  alignItems: 'center',