@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.
- package/lib/module/components/Composer/Composer.js +12 -7
- package/lib/module/components/Composer/Composer.js.map +1 -1
- package/lib/module/components/Composer/ComposerIconAction.js +2 -2
- package/lib/module/components/Composer/ComposerIconAction.js.map +1 -1
- package/lib/module/components/Composer/ComposerVoiceWave.js +44 -0
- package/lib/module/components/Composer/ComposerVoiceWave.js.map +1 -0
- package/lib/module/components/Composer/constants.js +20 -1
- package/lib/module/components/Composer/constants.js.map +1 -1
- package/lib/module/components/Composer/styles.js +30 -3
- package/lib/module/components/Composer/styles.js.map +1 -1
- package/lib/module/components/Message/MarkdownContent/MarkdownContent.js +9 -3
- package/lib/module/components/Message/MarkdownContent/MarkdownContent.js.map +1 -1
- package/lib/module/components/Message/MarkdownContent/styles.js +21 -11
- package/lib/module/components/Message/MarkdownContent/styles.js.map +1 -1
- package/lib/module/components/Message/Message.js +16 -4
- package/lib/module/components/Message/Message.js.map +1 -1
- package/lib/module/components/Message/styles.js +20 -14
- package/lib/module/components/Message/styles.js.map +1 -1
- package/lib/module/components/MessageList/MessageList.js +29 -8
- package/lib/module/components/MessageList/MessageList.js.map +1 -1
- package/lib/module/components/MessageList/styles.js +14 -4
- package/lib/module/components/MessageList/styles.js.map +1 -1
- package/lib/module/components/Process/Process.js +11 -3
- package/lib/module/components/Process/Process.js.map +1 -1
- package/lib/module/components/Process/ProcessCompactStep.js +81 -0
- package/lib/module/components/Process/ProcessCompactStep.js.map +1 -0
- package/lib/module/components/Process/styles.js +31 -1
- package/lib/module/components/Process/styles.js.map +1 -1
- package/lib/typescript/src/components/Composer/Composer.d.ts.map +1 -1
- package/lib/typescript/src/components/Composer/ComposerIconAction.d.ts.map +1 -1
- package/lib/typescript/src/components/Composer/ComposerVoiceWave.d.ts +2 -0
- package/lib/typescript/src/components/Composer/ComposerVoiceWave.d.ts.map +1 -0
- package/lib/typescript/src/components/Composer/constants.d.ts +29 -0
- package/lib/typescript/src/components/Composer/constants.d.ts.map +1 -1
- package/lib/typescript/src/components/Composer/styles.d.ts +27 -0
- package/lib/typescript/src/components/Composer/styles.d.ts.map +1 -1
- package/lib/typescript/src/components/Message/MarkdownContent/MarkdownContent.d.ts +1 -1
- package/lib/typescript/src/components/Message/MarkdownContent/MarkdownContent.d.ts.map +1 -1
- package/lib/typescript/src/components/Message/MarkdownContent/styles.d.ts +104 -0
- package/lib/typescript/src/components/Message/MarkdownContent/styles.d.ts.map +1 -1
- package/lib/typescript/src/components/Message/MarkdownContent/types.d.ts +1 -0
- package/lib/typescript/src/components/Message/MarkdownContent/types.d.ts.map +1 -1
- package/lib/typescript/src/components/Message/Message.d.ts.map +1 -1
- package/lib/typescript/src/components/Message/styles.d.ts +11 -5
- package/lib/typescript/src/components/Message/styles.d.ts.map +1 -1
- package/lib/typescript/src/components/MessageList/MessageList.d.ts +1 -1
- package/lib/typescript/src/components/MessageList/MessageList.d.ts.map +1 -1
- package/lib/typescript/src/components/MessageList/styles.d.ts +26 -1
- package/lib/typescript/src/components/MessageList/styles.d.ts.map +1 -1
- package/lib/typescript/src/components/MessageList/types.d.ts +6 -2
- package/lib/typescript/src/components/MessageList/types.d.ts.map +1 -1
- package/lib/typescript/src/components/Process/Process.d.ts.map +1 -1
- package/lib/typescript/src/components/Process/ProcessCompactStep.d.ts +3 -0
- package/lib/typescript/src/components/Process/ProcessCompactStep.d.ts.map +1 -0
- package/lib/typescript/src/components/Process/styles.d.ts +30 -0
- package/lib/typescript/src/components/Process/styles.d.ts.map +1 -1
- package/lib/typescript/src/components/Process/types.d.ts +8 -0
- package/lib/typescript/src/components/Process/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/components/Composer/Composer.tsx +21 -14
- package/src/components/Composer/ComposerIconAction.tsx +6 -1
- package/src/components/Composer/ComposerVoiceWave.tsx +67 -0
- package/src/components/Composer/constants.ts +14 -1
- package/src/components/Composer/styles.ts +23 -2
- package/src/components/Message/MarkdownContent/MarkdownContent.tsx +15 -3
- package/src/components/Message/MarkdownContent/styles.ts +23 -11
- package/src/components/Message/MarkdownContent/types.ts +1 -0
- package/src/components/Message/Message.tsx +46 -15
- package/src/components/Message/styles.ts +23 -12
- package/src/components/MessageList/MessageList.tsx +33 -12
- package/src/components/MessageList/styles.ts +22 -6
- package/src/components/MessageList/types.ts +6 -2
- package/src/components/Process/Process.tsx +17 -3
- package/src/components/Process/ProcessCompactStep.tsx +110 -0
- package/src/components/Process/styles.ts +27 -1
- 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: {
|
|
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[
|
|
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({
|
|
15
|
+
export function MarkdownContent({
|
|
16
|
+
text,
|
|
17
|
+
onLinkPress,
|
|
18
|
+
outgoing = false,
|
|
19
|
+
}: MarkdownContentProps) {
|
|
16
20
|
const { scheme } = useTheme();
|
|
17
|
-
const styles = useThemedStyles(
|
|
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
|
-
|
|
5
|
+
const makeMarkdownStyles = (colors: ColorTokens, outgoing: boolean) => {
|
|
6
|
+
const textColor = outgoing ? colors.onPrimary : colors.foreground;
|
|
6
7
|
const body = {
|
|
7
|
-
fontSize: type.
|
|
8
|
-
lineHeight: type.
|
|
9
|
-
color:
|
|
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: {
|
|
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:
|
|
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:
|
|
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:
|
|
44
|
+
color: textColor,
|
|
39
45
|
},
|
|
40
46
|
h4: {
|
|
41
47
|
fontSize: type.body,
|
|
42
48
|
lineHeight: type.body * 1.4,
|
|
43
|
-
color:
|
|
49
|
+
color: textColor,
|
|
44
50
|
},
|
|
45
51
|
h5: {
|
|
46
52
|
fontSize: type.sm,
|
|
47
53
|
lineHeight: type.sm * 1.4,
|
|
48
|
-
color:
|
|
54
|
+
color: textColor,
|
|
49
55
|
},
|
|
50
56
|
h6: {
|
|
51
57
|
fontSize: type.xs,
|
|
52
58
|
lineHeight: type.xs * 1.4,
|
|
53
|
-
color:
|
|
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,5 +1,11 @@
|
|
|
1
1
|
import { Pressable, Text, View } from 'react-native';
|
|
2
|
-
import {
|
|
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
|
|
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={
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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[
|
|
10
|
+
gap: space[3],
|
|
11
11
|
minWidth: 0,
|
|
12
|
-
paddingVertical: space[2],
|
|
13
12
|
},
|
|
14
13
|
end: { flexDirection: 'row-reverse' },
|
|
15
14
|
column: {
|
|
16
|
-
|
|
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: {
|
|
23
|
-
content: { minWidth: 0, maxWidth: '
|
|
21
|
+
fullWidth: { alignSelf: 'stretch', maxWidth: '100%' },
|
|
22
|
+
content: { minWidth: 0, maxWidth: '78%', gap: space[2] },
|
|
24
23
|
bubble: {
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
paddingHorizontal: space[5],
|
|
25
|
+
paddingVertical: space[4],
|
|
26
|
+
borderRadius: radius['2xl'],
|
|
27
27
|
backgroundColor: colors.surface,
|
|
28
28
|
},
|
|
29
|
-
outgoing: { backgroundColor: colors.
|
|
29
|
+
outgoing: { backgroundColor: colors.primary },
|
|
30
|
+
outgoingText: { color: colors.onPrimary },
|
|
30
31
|
outlined: {
|
|
31
|
-
|
|
32
|
-
|
|
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: {
|
|
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: {
|
|
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 {
|
|
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 {
|
|
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
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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 {
|
|
3
|
-
export const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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?: '
|
|
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
|
|
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],
|