@unif/react-native-chat 3.0.0 → 3.2.0

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 (76) hide show
  1. package/lib/module/components/Composer/Composer.js +12 -7
  2. package/lib/module/components/Composer/Composer.js.map +1 -1
  3. package/lib/module/components/Composer/ComposerIconAction.js +2 -2
  4. package/lib/module/components/Composer/ComposerIconAction.js.map +1 -1
  5. package/lib/module/components/Composer/ComposerVoiceWave.js +44 -0
  6. package/lib/module/components/Composer/ComposerVoiceWave.js.map +1 -0
  7. package/lib/module/components/Composer/constants.js +20 -1
  8. package/lib/module/components/Composer/constants.js.map +1 -1
  9. package/lib/module/components/Composer/styles.js +30 -3
  10. package/lib/module/components/Composer/styles.js.map +1 -1
  11. package/lib/module/components/Message/MarkdownContent/MarkdownContent.js +9 -3
  12. package/lib/module/components/Message/MarkdownContent/MarkdownContent.js.map +1 -1
  13. package/lib/module/components/Message/MarkdownContent/styles.js +21 -11
  14. package/lib/module/components/Message/MarkdownContent/styles.js.map +1 -1
  15. package/lib/module/components/Message/Message.js +16 -4
  16. package/lib/module/components/Message/Message.js.map +1 -1
  17. package/lib/module/components/Message/styles.js +20 -14
  18. package/lib/module/components/Message/styles.js.map +1 -1
  19. package/lib/module/components/MessageList/MessageList.js +29 -8
  20. package/lib/module/components/MessageList/MessageList.js.map +1 -1
  21. package/lib/module/components/MessageList/styles.js +14 -4
  22. package/lib/module/components/MessageList/styles.js.map +1 -1
  23. package/lib/module/components/Process/Process.js +11 -3
  24. package/lib/module/components/Process/Process.js.map +1 -1
  25. package/lib/module/components/Process/ProcessCompactStep.js +81 -0
  26. package/lib/module/components/Process/ProcessCompactStep.js.map +1 -0
  27. package/lib/module/components/Process/styles.js +31 -1
  28. package/lib/module/components/Process/styles.js.map +1 -1
  29. package/lib/typescript/src/components/Composer/Composer.d.ts.map +1 -1
  30. package/lib/typescript/src/components/Composer/ComposerIconAction.d.ts.map +1 -1
  31. package/lib/typescript/src/components/Composer/ComposerVoiceWave.d.ts +2 -0
  32. package/lib/typescript/src/components/Composer/ComposerVoiceWave.d.ts.map +1 -0
  33. package/lib/typescript/src/components/Composer/constants.d.ts +29 -0
  34. package/lib/typescript/src/components/Composer/constants.d.ts.map +1 -1
  35. package/lib/typescript/src/components/Composer/styles.d.ts +27 -0
  36. package/lib/typescript/src/components/Composer/styles.d.ts.map +1 -1
  37. package/lib/typescript/src/components/Message/MarkdownContent/MarkdownContent.d.ts +1 -1
  38. package/lib/typescript/src/components/Message/MarkdownContent/MarkdownContent.d.ts.map +1 -1
  39. package/lib/typescript/src/components/Message/MarkdownContent/styles.d.ts +104 -0
  40. package/lib/typescript/src/components/Message/MarkdownContent/styles.d.ts.map +1 -1
  41. package/lib/typescript/src/components/Message/MarkdownContent/types.d.ts +1 -0
  42. package/lib/typescript/src/components/Message/MarkdownContent/types.d.ts.map +1 -1
  43. package/lib/typescript/src/components/Message/Message.d.ts.map +1 -1
  44. package/lib/typescript/src/components/Message/styles.d.ts +11 -5
  45. package/lib/typescript/src/components/Message/styles.d.ts.map +1 -1
  46. package/lib/typescript/src/components/MessageList/MessageList.d.ts +1 -1
  47. package/lib/typescript/src/components/MessageList/MessageList.d.ts.map +1 -1
  48. package/lib/typescript/src/components/MessageList/styles.d.ts +26 -1
  49. package/lib/typescript/src/components/MessageList/styles.d.ts.map +1 -1
  50. package/lib/typescript/src/components/MessageList/types.d.ts +6 -2
  51. package/lib/typescript/src/components/MessageList/types.d.ts.map +1 -1
  52. package/lib/typescript/src/components/Process/Process.d.ts.map +1 -1
  53. package/lib/typescript/src/components/Process/ProcessCompactStep.d.ts +3 -0
  54. package/lib/typescript/src/components/Process/ProcessCompactStep.d.ts.map +1 -0
  55. package/lib/typescript/src/components/Process/styles.d.ts +30 -0
  56. package/lib/typescript/src/components/Process/styles.d.ts.map +1 -1
  57. package/lib/typescript/src/components/Process/types.d.ts +8 -0
  58. package/lib/typescript/src/components/Process/types.d.ts.map +1 -1
  59. package/package.json +3 -3
  60. package/src/components/Composer/Composer.tsx +21 -14
  61. package/src/components/Composer/ComposerIconAction.tsx +6 -1
  62. package/src/components/Composer/ComposerVoiceWave.tsx +67 -0
  63. package/src/components/Composer/constants.ts +14 -1
  64. package/src/components/Composer/styles.ts +23 -2
  65. package/src/components/Message/MarkdownContent/MarkdownContent.tsx +15 -3
  66. package/src/components/Message/MarkdownContent/styles.ts +23 -11
  67. package/src/components/Message/MarkdownContent/types.ts +1 -0
  68. package/src/components/Message/Message.tsx +46 -15
  69. package/src/components/Message/styles.ts +23 -12
  70. package/src/components/MessageList/MessageList.tsx +33 -12
  71. package/src/components/MessageList/styles.ts +22 -6
  72. package/src/components/MessageList/types.ts +6 -2
  73. package/src/components/Process/Process.tsx +17 -3
  74. package/src/components/Process/ProcessCompactStep.tsx +110 -0
  75. package/src/components/Process/styles.ts +27 -1
  76. package/src/components/Process/types.ts +9 -0
@@ -17,6 +17,10 @@ export const createStyles = (colors: ColorTokens, shadows: ShadowTokens) =>
17
17
  ...shadows.card,
18
18
  },
19
19
  accessory: { padding: space[3] },
20
+ header: {
21
+ borderBottomWidth: 0.5,
22
+ borderBottomColor: colors.outlineVariant,
23
+ },
20
24
  regular: {
21
25
  flexDirection: 'row',
22
26
  alignItems: 'flex-end',
@@ -26,7 +30,12 @@ export const createStyles = (colors: ColorTokens, shadows: ShadowTokens) =>
26
30
  },
27
31
  expanded: { flexDirection: 'column', alignItems: 'stretch', gap: 0 },
28
32
  hidden: { display: 'none' },
29
- input: { flex: 1, minWidth: 0 },
33
+ input: {
34
+ flex: 1,
35
+ minWidth: 0,
36
+ alignSelf: 'stretch',
37
+ justifyContent: 'center',
38
+ },
30
39
  inputExpanded: { flex: 0, width: '100%' },
31
40
  // 保持原生堆叠特征稳定,避免聚焦展开时重排输入所在的原生视图树。
32
41
  more: { width: fixed.hitTarget, height: fixed.hitTarget, zIndex: 1 },
@@ -119,11 +128,13 @@ export const createStyles = (colors: ColorTokens, shadows: ShadowTokens) =>
119
128
  voice: {
120
129
  flexDirection: 'row',
121
130
  alignItems: 'center',
122
- gap: space[2],
131
+ gap: space[3],
123
132
  paddingVertical: space[1],
124
133
  paddingHorizontal: space[2],
125
134
  borderRadius: radius['3xl'],
126
135
  backgroundColor: colors.primaryContainerSubtle,
136
+ borderWidth: 0.5,
137
+ borderColor: colors.brandTint14,
127
138
  },
128
139
  transcript: {
129
140
  flex: 1,
@@ -131,5 +142,15 @@ export const createStyles = (colors: ColorTokens, shadows: ShadowTokens) =>
131
142
  color: colors.foreground,
132
143
  fontSize: type.body,
133
144
  },
145
+ voiceBody: {
146
+ flex: 1,
147
+ minWidth: 0,
148
+ flexDirection: 'row',
149
+ alignItems: 'center',
150
+ gap: space[4],
151
+ minHeight: r(34),
152
+ },
153
+ wave: { flexDirection: 'row', alignItems: 'center', gap: 3, height: r(20) },
154
+ waveBar: { width: 2.5, borderRadius: 1.5, backgroundColor: colors.primary },
134
155
  transcriptPlaceholder: { color: colors.foregroundSubtle },
135
156
  });
@@ -8,15 +8,27 @@ import {
8
8
  import { Pressable, Text } from 'react-native';
9
9
  import { Renderer, useMarkdown } from 'react-native-marked';
10
10
  import { useTheme, useThemedStyles } from '@unif/react-native-design';
11
- import { createStyles } from './styles';
11
+ import { createStyles, createOutgoingStyles } from './styles';
12
12
  import type { MarkdownContentProps, MarkdownSvgLabelProps } from './types';
13
13
  import { MarkdownImage } from './MarkdownImage/MarkdownImage';
14
14
 
15
- export function MarkdownContent({ text, onLinkPress }: MarkdownContentProps) {
15
+ export function MarkdownContent({
16
+ text,
17
+ onLinkPress,
18
+ outgoing = false,
19
+ }: MarkdownContentProps) {
16
20
  const { scheme } = useTheme();
17
- const styles = useThemedStyles(createStyles);
21
+ const styles = useThemedStyles(
22
+ outgoing ? createOutgoingStyles : createStyles
23
+ );
18
24
  const markdown = useMemo(() => {
19
25
  const instance = new Renderer();
26
+ const renderCode = instance.code.bind(instance);
27
+ instance.code = (value, language, containerStyle, textStyle) =>
28
+ renderCode(value, language, containerStyle, {
29
+ ...textStyle,
30
+ ...styles.codeText,
31
+ });
20
32
  // 上游 table 节点未提供 key,保留其渲染并补齐稳定的节点身份。
21
33
  const renderTable = instance.table.bind(instance);
22
34
  instance.table = (...args) => (
@@ -2,60 +2,72 @@ import { StyleSheet } from 'react-native';
2
2
  import { type, space, fontMono } from '@unif/react-native-design';
3
3
  import type { ColorTokens } from '@unif/react-native-design';
4
4
 
5
- export const createStyles = (colors: ColorTokens) => {
5
+ const makeMarkdownStyles = (colors: ColorTokens, outgoing: boolean) => {
6
+ const textColor = outgoing ? colors.onPrimary : colors.foreground;
6
7
  const body = {
7
- fontSize: type.body,
8
- lineHeight: type.body * 1.5,
9
- color: colors.foreground,
8
+ fontSize: type.sm,
9
+ lineHeight: type.sm * 1.5,
10
+ color: textColor,
10
11
  };
11
12
  return StyleSheet.create({
12
13
  text: body,
13
14
  em: { ...body, fontStyle: 'italic' },
14
15
  strong: { ...body, fontWeight: 'bold' },
15
16
  strikethrough: { ...body, textDecorationLine: 'line-through' },
16
- link: { ...body, color: colors.primary },
17
+ link: {
18
+ ...body,
19
+ color: outgoing ? colors.onPrimary : colors.primary,
20
+ textDecorationLine: 'underline',
21
+ },
17
22
  li: body,
18
23
  codespan: {
19
24
  ...body,
20
25
  fontFamily: fontMono,
26
+ color: colors.foreground,
21
27
  backgroundColor: colors.surfaceContainer,
22
28
  },
23
29
  h1: {
24
30
  fontSize: type.h1,
25
31
  lineHeight: type.h1 * 1.4,
26
- color: colors.foreground,
32
+ color: textColor,
27
33
  borderBottomColor: colors.outline,
28
34
  },
29
35
  h2: {
30
36
  fontSize: type.h2,
31
37
  lineHeight: type.h2 * 1.4,
32
- color: colors.foreground,
38
+ color: textColor,
33
39
  borderBottomColor: colors.outline,
34
40
  },
35
41
  h3: {
36
42
  fontSize: type.h3,
37
43
  lineHeight: type.h3 * 1.4,
38
- color: colors.foreground,
44
+ color: textColor,
39
45
  },
40
46
  h4: {
41
47
  fontSize: type.body,
42
48
  lineHeight: type.body * 1.4,
43
- color: colors.foreground,
49
+ color: textColor,
44
50
  },
45
51
  h5: {
46
52
  fontSize: type.sm,
47
53
  lineHeight: type.sm * 1.4,
48
- color: colors.foreground,
54
+ color: textColor,
49
55
  },
50
56
  h6: {
51
57
  fontSize: type.xs,
52
58
  lineHeight: type.xs * 1.4,
53
- color: colors.foreground,
59
+ color: textColor,
54
60
  },
55
61
  paragraph: { paddingVertical: space[1] },
56
62
  code: { padding: space[2], backgroundColor: colors.surfaceContainer },
63
+ codeText: { ...body, color: colors.foreground, fontFamily: fontMono },
57
64
  blockquote: { borderLeftColor: colors.outline },
58
65
  hr: { borderBottomColor: colors.outline },
59
66
  table: { borderColor: colors.outline },
60
67
  });
61
68
  };
69
+
70
+ export const createStyles = (colors: ColorTokens) =>
71
+ makeMarkdownStyles(colors, false);
72
+ export const createOutgoingStyles = (colors: ColorTokens) =>
73
+ makeMarkdownStyles(colors, true);
@@ -1,6 +1,7 @@
1
1
  export interface MarkdownContentProps {
2
2
  text: string;
3
3
  onLinkPress?(url: string): void;
4
+ outgoing?: boolean;
4
5
  }
5
6
 
6
7
  export interface MarkdownSvgLabelProps {
@@ -1,5 +1,11 @@
1
1
  import { Pressable, Text, View } from 'react-native';
2
- import { Button, Spinner, useThemedStyles } from '@unif/react-native-design';
2
+ import {
3
+ Button,
4
+ IconButton,
5
+ Spinner,
6
+ useThemedStyles,
7
+ useColors,
8
+ } from '@unif/react-native-design';
3
9
  import { MarkdownContent } from './MarkdownContent/MarkdownContent';
4
10
  import { createStyles } from './styles';
5
11
  import { isNestedAction } from './isNestedAction';
@@ -23,6 +29,7 @@ export function Message(props: MessageProps) {
23
29
  testID,
24
30
  } = props;
25
31
  const styles = useThemedStyles(createStyles);
32
+ const colors = useColors();
26
33
  const hasContent =
27
34
  Boolean(props.text?.length) ||
28
35
  (props.children !== undefined &&
@@ -47,11 +54,20 @@ export function Message(props: MessageProps) {
47
54
  {header}
48
55
  {props.text !== undefined ? (
49
56
  props.format === 'markdown' ? (
50
- <MarkdownContent text={props.text} onLinkPress={props.onLinkPress} />
57
+ <MarkdownContent
58
+ text={props.text}
59
+ onLinkPress={props.onLinkPress}
60
+ outgoing={surface === 'bubble' && placement === 'end'}
61
+ />
51
62
  ) : (
52
63
  <Text
53
64
  selectable
54
- style={styles.text}
65
+ style={[
66
+ styles.text,
67
+ surface === 'bubble' &&
68
+ placement === 'end' &&
69
+ styles.outgoingText,
70
+ ]}
55
71
  numberOfLines={props.numberOfLines}
56
72
  >
57
73
  {props.text}
@@ -117,18 +133,33 @@ export function Message(props: MessageProps) {
117
133
  )}
118
134
  {actions.length ? (
119
135
  <View style={styles.actions}>
120
- {actions.map((action) => (
121
- <Button
122
- key={action.id}
123
- label={action.label}
124
- leftIcon={action.icon}
125
- disabled={action.disabled}
126
- loading={action.loading}
127
- accessibilityHint={action.accessibilityHint}
128
- variant="text"
129
- onPress={action.onPress}
130
- />
131
- ))}
136
+ {actions.map((action) =>
137
+ action.icon ? (
138
+ <IconButton
139
+ key={action.id}
140
+ icon={action.icon}
141
+ size="sm"
142
+ variant="ghost"
143
+ color={colors.foregroundSubtle}
144
+ accessibilityLabel={action.label}
145
+ accessibilityHint={action.accessibilityHint}
146
+ disabled={action.disabled}
147
+ loading={action.loading}
148
+ onPress={action.onPress}
149
+ />
150
+ ) : (
151
+ <Button
152
+ key={action.id}
153
+ label={action.label}
154
+ leftIcon={action.icon}
155
+ disabled={action.disabled}
156
+ loading={action.loading}
157
+ accessibilityHint={action.accessibilityHint}
158
+ variant="text"
159
+ onPress={action.onPress}
160
+ />
161
+ )
162
+ )}
132
163
  </View>
133
164
  ) : null}
134
165
  </View>
@@ -7,36 +7,47 @@ export const createStyles = (colors: ColorTokens) =>
7
7
  root: {
8
8
  flexDirection: 'row',
9
9
  alignItems: 'flex-start',
10
- gap: space[2],
10
+ gap: space[3],
11
11
  minWidth: 0,
12
- paddingVertical: space[2],
13
12
  },
14
13
  end: { flexDirection: 'row-reverse' },
15
14
  column: {
16
- flexShrink: 1,
15
+ flex: 1,
17
16
  maxWidth: '100%',
18
17
  gap: space[1],
19
18
  alignItems: 'flex-start',
20
19
  },
21
20
  columnEnd: { alignItems: 'flex-end' },
22
- fullWidth: { flex: 1, alignSelf: 'stretch' },
23
- content: { minWidth: 0, maxWidth: '100%', gap: space[2] },
21
+ fullWidth: { alignSelf: 'stretch', maxWidth: '100%' },
22
+ content: { minWidth: 0, maxWidth: '78%', gap: space[2] },
24
23
  bubble: {
25
- padding: space[3],
26
- borderRadius: radius.lg,
24
+ paddingHorizontal: space[5],
25
+ paddingVertical: space[4],
26
+ borderRadius: radius['2xl'],
27
27
  backgroundColor: colors.surface,
28
28
  },
29
- outgoing: { backgroundColor: colors.primaryContainer },
29
+ outgoing: { backgroundColor: colors.primary },
30
+ outgoingText: { color: colors.onPrimary },
30
31
  outlined: {
31
- padding: space[3],
32
- borderRadius: radius.lg,
32
+ paddingHorizontal: space[5],
33
+ paddingVertical: space[4],
34
+ borderRadius: radius['2xl'],
33
35
  borderWidth: 1,
34
36
  borderColor: colors.outline,
35
37
  },
36
38
  failed: { borderWidth: 1, borderColor: colors.error },
37
- text: { fontSize: type.body, color: colors.foreground },
39
+ text: {
40
+ fontSize: type.sm,
41
+ lineHeight: type.sm * 1.5,
42
+ color: colors.foreground,
43
+ },
38
44
  name: { fontSize: type.sm, color: colors.foregroundMuted },
39
45
  status: { fontSize: type.sm, color: colors.foregroundMuted },
40
46
  errorText: { color: colors.error },
41
- actions: { flexDirection: 'row', flexWrap: 'wrap', gap: space[2] },
47
+ actions: {
48
+ flexDirection: 'row',
49
+ flexWrap: 'wrap',
50
+ gap: space[1],
51
+ marginTop: space[1],
52
+ },
42
53
  });
@@ -2,9 +2,14 @@ import { useCallback, useImperativeHandle, useMemo, useRef } from 'react';
2
2
  import type { RefAttributes } from 'react';
3
3
  import { FlatList, View } from 'react-native';
4
4
  import type { ListRenderItemInfo } from 'react-native';
5
- import { Button } from '@unif/react-native-design';
5
+ import {
6
+ Button,
7
+ IconButton,
8
+ Spinner,
9
+ useThemedStyles,
10
+ } from '@unif/react-native-design';
6
11
  import { DEFAULT_END_THRESHOLD } from './constants';
7
- import { styles } from './styles';
12
+ import { createStyles } from './styles';
8
13
  import { useWebPrependAnchor } from './webPrependAnchor';
9
14
  import { useMessageListScroll } from './useMessageListScroll';
10
15
  import { MessageListRow } from './MessageListRow';
@@ -25,15 +30,19 @@ export function MessageList<T>({
25
30
  followOutput = 'whenAtEnd',
26
31
  endThreshold = DEFAULT_END_THRESHOLD,
27
32
  showScrollToEnd = true,
33
+ scrollToEndBusy = false,
28
34
  onAtEndChange,
29
35
  hasEarlier = false,
30
36
  loadingEarlier = false,
31
37
  onRequestEarlier,
32
38
  keyboardDismissMode = 'on-drag',
39
+ contentContainerStyle,
40
+ showsVerticalScrollIndicator = false,
33
41
  style,
34
42
  testID,
35
43
  ref,
36
44
  }: MessageListProps<T> & RefAttributes<MessageListHandle>) {
45
+ const styles = useThemedStyles(createStyles);
37
46
  const listRef = useRef<FlatList<T>>(null);
38
47
  const keys = useMemo(() => items.map(keyExtractor), [items, keyExtractor]);
39
48
  const { getRowNativeID, preservingPrependRef, cancelPrependPreservation } =
@@ -97,10 +106,7 @@ export function MessageList<T>({
97
106
  () => (footer ? <View>{footer}</View> : undefined),
98
107
  [footer]
99
108
  );
100
- const listEmpty = useMemo(
101
- () => (empty ? <View>{empty}</View> : undefined),
102
- [empty]
103
- );
109
+ const listEmpty = useMemo(() => (empty ? <>{empty}</> : undefined), [empty]);
104
110
  return (
105
111
  <View style={[styles.root, style]} testID={testID}>
106
112
  <FlatList
@@ -116,6 +122,8 @@ export function MessageList<T>({
116
122
  ListEmptyComponent={listEmpty}
117
123
  maintainVisibleContentPosition={MAINTAIN_VISIBLE_POSITION}
118
124
  keyboardDismissMode={keyboardDismissMode}
125
+ contentContainerStyle={contentContainerStyle}
126
+ showsVerticalScrollIndicator={showsVerticalScrollIndicator}
119
127
  keyboardShouldPersistTaps="handled"
120
128
  scrollEventThrottle={16}
121
129
  onLayout={onLayout}
@@ -127,12 +135,25 @@ export function MessageList<T>({
127
135
  onMomentumScrollEnd={onMomentumScrollEnd}
128
136
  />
129
137
  {showScrollToEnd && items.length > 0 && !atEnd ? (
130
- <Button
131
- label="回到最新消息"
132
- variant="secondary"
133
- style={styles.returnToEnd}
134
- onPress={() => scrollToEnd()}
135
- />
138
+ <View style={styles.returnToEnd} pointerEvents="box-none">
139
+ {scrollToEndBusy ? (
140
+ <View style={styles.returnProgress} pointerEvents="none">
141
+ <Spinner size={44} thickness={2} />
142
+ </View>
143
+ ) : null}
144
+ <IconButton
145
+ icon="arrow-down"
146
+ accessibilityLabel={
147
+ scrollToEndBusy ? '回到最新消息,正在处理' : '回到最新消息'
148
+ }
149
+ variant="ghost"
150
+ size="md"
151
+ surfaceSize={36}
152
+ iconSize={18}
153
+ style={styles.returnButton}
154
+ onPress={() => scrollToEnd()}
155
+ />
156
+ </View>
136
157
  ) : null}
137
158
  </View>
138
159
  );
@@ -1,7 +1,23 @@
1
1
  import { StyleSheet } from 'react-native';
2
- import { space } from '@unif/react-native-design';
3
- export const styles = StyleSheet.create({
4
- root: { flex: 1, minHeight: 0, minWidth: 0 },
5
- list: { flex: 1 },
6
- returnToEnd: { position: 'absolute', bottom: space[3], alignSelf: 'center' },
7
- });
2
+ import type { ColorTokens, ShadowTokens } from '@unif/react-native-design';
3
+ export const createStyles = (colors: ColorTokens, shadows: ShadowTokens) =>
4
+ StyleSheet.create({
5
+ root: { flex: 1, minHeight: 0, minWidth: 0 },
6
+ list: { flex: 1 },
7
+ returnToEnd: {
8
+ position: 'absolute',
9
+ bottom: 0,
10
+ alignSelf: 'center',
11
+ width: 44,
12
+ height: 44,
13
+ alignItems: 'center',
14
+ justifyContent: 'center',
15
+ },
16
+ returnProgress: StyleSheet.absoluteFill,
17
+ returnButton: {
18
+ alignSelf: 'center',
19
+ borderRadius: 18,
20
+ backgroundColor: colors.surface,
21
+ ...shadows.card,
22
+ },
23
+ });
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { StyleProp, ViewStyle } from 'react-native';
2
+ import type { ScrollViewProps, StyleProp, ViewStyle } from 'react-native';
3
3
 
4
4
  export interface MessageListScrollOptions {
5
5
  animated?: boolean;
@@ -20,11 +20,15 @@ export interface MessageListProps<T> {
20
20
  followOutput?: 'whenAtEnd' | 'never';
21
21
  endThreshold?: number;
22
22
  showScrollToEnd?: boolean;
23
+ /** 外部仍在生成内容时显示返回入口的进度外环,不影响滚动操作。 */
24
+ scrollToEndBusy?: boolean;
23
25
  onAtEndChange?(atEnd: boolean): void;
24
26
  hasEarlier?: boolean;
25
27
  loadingEarlier?: boolean;
26
28
  onRequestEarlier?(): void;
27
- keyboardDismissMode?: 'none' | 'on-drag';
29
+ keyboardDismissMode?: ScrollViewProps['keyboardDismissMode'];
30
+ contentContainerStyle?: StyleProp<ViewStyle>;
31
+ showsVerticalScrollIndicator?: boolean;
28
32
  style?: StyleProp<ViewStyle>;
29
33
  testID?: string;
30
34
  }
@@ -8,6 +8,7 @@ import {
8
8
  } from '@unif/react-native-design';
9
9
  import { PROCESS_STATUS_ICONS, PROCESS_STATUS_LABELS } from './constants';
10
10
  import { createStyles } from './styles';
11
+ import { ProcessCompactStep } from './ProcessCompactStep';
11
12
  import type { ProcessProps, ProcessStep } from './types';
12
13
 
13
14
  function idsWithDetails(
@@ -43,7 +44,7 @@ function formatElapsedMs(elapsedMs: number | undefined): string | undefined {
43
44
  }
44
45
 
45
46
  export function Process(props: ProcessProps): React.JSX.Element {
46
- const { steps, title, identity, style, testID } = props;
47
+ const { steps, title, identity, style, testID, variant = 'card' } = props;
47
48
  const styles = useThemedStyles(createStyles);
48
49
  const colors = useColors();
49
50
  const isControlled = props.expandedIds !== undefined;
@@ -71,18 +72,31 @@ export function Process(props: ProcessProps): React.JSX.Element {
71
72
  };
72
73
 
73
74
  return (
74
- <View style={[styles.root, style]} testID={testID}>
75
+ <View
76
+ style={[variant === 'compact' ? styles.compactRoot : styles.root, style]}
77
+ testID={testID}
78
+ >
75
79
  {title || identity ? (
76
80
  <View style={styles.heading}>
77
81
  {identity}
78
82
  {title ? <Text style={styles.headingTitle}>{title}</Text> : null}
79
83
  </View>
80
84
  ) : null}
81
- <View style={styles.steps}>
85
+ <View style={variant === 'compact' ? styles.compactSteps : styles.steps}>
82
86
  {steps.map((step, index) => {
83
87
  const statusLabel = PROCESS_STATUS_LABELS[step.status];
84
88
  const elapsed = formatElapsedMs(step.elapsedMs);
85
89
  const isExpanded = expandedIds.includes(step.id);
90
+ if (variant === 'compact')
91
+ return (
92
+ <ProcessCompactStep
93
+ key={step.id}
94
+ step={step}
95
+ expanded={isExpanded}
96
+ elapsed={elapsed}
97
+ onToggle={() => toggleDetails(step.id)}
98
+ />
99
+ );
86
100
  return (
87
101
  <View
88
102
  key={step.id}
@@ -0,0 +1,110 @@
1
+ import { Pressable, ScrollView, Text, View } from 'react-native';
2
+ import {
3
+ Button,
4
+ Icon,
5
+ Pulse,
6
+ Reveal,
7
+ pressedOpacity,
8
+ r,
9
+ useColors,
10
+ useThemedStyles,
11
+ } from '@unif/react-native-design';
12
+ import { PROCESS_STATUS_ICONS, PROCESS_STATUS_LABELS } from './constants';
13
+ import { createStyles } from './styles';
14
+ import type { ProcessCompactStepProps } from './types';
15
+
16
+ export function ProcessCompactStep({
17
+ step,
18
+ expanded,
19
+ elapsed,
20
+ onToggle,
21
+ }: ProcessCompactStepProps) {
22
+ const styles = useThemedStyles(createStyles);
23
+ const colors = useColors();
24
+ const hasDetails = step.details != null;
25
+ const running = step.status === 'running';
26
+ const color = running
27
+ ? colors.primary
28
+ : step.status === 'completed'
29
+ ? colors.success
30
+ : step.status === 'failed'
31
+ ? colors.error
32
+ : colors.foregroundMuted;
33
+ const icon = (
34
+ <Icon
35
+ name={
36
+ running || step.status === 'completed'
37
+ ? 'spark'
38
+ : PROCESS_STATUS_ICONS[step.status]
39
+ }
40
+ size={r(14)}
41
+ color={color}
42
+ />
43
+ );
44
+ return (
45
+ <View>
46
+ <Pressable
47
+ testID={`process-step-${step.id}-toggle`}
48
+ accessible
49
+ accessibilityRole={hasDetails ? 'button' : undefined}
50
+ accessibilityLabel={`${step.title},${PROCESS_STATUS_LABELS[step.status]}${elapsed ? `,${elapsed}` : ''}`}
51
+ accessibilityState={hasDetails ? { expanded } : undefined}
52
+ disabled={!hasDetails}
53
+ onPress={onToggle}
54
+ style={({ pressed }) => [
55
+ styles.compactHeader,
56
+ pressed && { opacity: pressedOpacity },
57
+ ]}
58
+ >
59
+ {running ? <Pulse>{icon}</Pulse> : icon}
60
+ <Text
61
+ style={styles.compactTitle}
62
+ testID={`process-step-title-${step.id}`}
63
+ numberOfLines={1}
64
+ >
65
+ {step.title}
66
+ </Text>
67
+ {elapsed ? <Text style={styles.elapsed}>{elapsed}</Text> : null}
68
+ {hasDetails ? (
69
+ <Icon
70
+ name={expanded ? 'chevron-up' : 'chevron-down'}
71
+ size={r(14)}
72
+ color={colors.foregroundSubtle}
73
+ />
74
+ ) : null}
75
+ </Pressable>
76
+ {step.description ? (
77
+ <Text style={styles.compactDescription}>{step.description}</Text>
78
+ ) : null}
79
+ {hasDetails && expanded ? (
80
+ <Reveal>
81
+ <ScrollView
82
+ testID={`process-step-${step.id}-details`}
83
+ style={styles.compactDetails}
84
+ nestedScrollEnabled
85
+ keyboardShouldPersistTaps="handled"
86
+ >
87
+ {step.details}
88
+ </ScrollView>
89
+ </Reveal>
90
+ ) : null}
91
+ {step.actions?.length ? (
92
+ <View style={styles.actions}>
93
+ {step.actions.map((action) => (
94
+ <Button
95
+ key={action.id}
96
+ label={action.label}
97
+ leftIcon={action.icon}
98
+ onPress={action.onPress}
99
+ disabled={action.disabled}
100
+ loading={action.loading}
101
+ accessibilityHint={action.accessibilityHint}
102
+ size="sm"
103
+ variant="text"
104
+ />
105
+ ))}
106
+ </View>
107
+ ) : null}
108
+ </View>
109
+ );
110
+ }
@@ -1,9 +1,35 @@
1
1
  import { StyleSheet } from 'react-native';
2
- import { fw, radius, space, type } from '@unif/react-native-design';
2
+ import { fw, r, radius, space, type } from '@unif/react-native-design';
3
3
  import type { ColorTokens } from '@unif/react-native-design';
4
4
 
5
5
  export const createStyles = (colors: ColorTokens) =>
6
6
  StyleSheet.create({
7
+ compactRoot: { gap: space[2] },
8
+ compactSteps: { gap: space[2] },
9
+ compactHeader: {
10
+ flexDirection: 'row',
11
+ alignItems: 'center',
12
+ gap: space[2],
13
+ minHeight: 44,
14
+ paddingVertical: space[1],
15
+ },
16
+ compactTitle: {
17
+ flex: 1,
18
+ minWidth: 0,
19
+ color: colors.foregroundMuted,
20
+ fontSize: type.xs,
21
+ fontWeight: fw.medium,
22
+ },
23
+ compactDescription: {
24
+ color: colors.foregroundMuted,
25
+ fontSize: type.xs,
26
+ lineHeight: type.xs * 1.6,
27
+ },
28
+ compactDetails: {
29
+ maxHeight: r(180),
30
+ paddingTop: space[3],
31
+ paddingBottom: space[2],
32
+ },
7
33
  root: {
8
34
  gap: space[4],
9
35
  padding: space[5],