@unif/react-native-design 0.33.1 → 0.34.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/ui/ActionMenuContent/ActionMenuContent.js +97 -0
- package/lib/module/components/ui/ActionMenuContent/ActionMenuContent.js.map +1 -0
- package/lib/module/components/ui/ActionMenuContent/index.js +4 -0
- package/lib/module/components/ui/ActionMenuContent/index.js.map +1 -0
- package/lib/module/components/ui/ActionMenuContent/styles.js +54 -0
- package/lib/module/components/ui/ActionMenuContent/styles.js.map +1 -0
- package/lib/module/components/ui/ActionMenuContent/types.js.map +1 -0
- package/lib/module/components/ui/Button/ButtonBase.js +10 -2
- package/lib/module/components/ui/Button/ButtonBase.js.map +1 -1
- package/lib/module/components/ui/IconButton/IconButton.js +5 -1
- package/lib/module/components/ui/IconButton/IconButton.js.map +1 -1
- package/lib/module/components/ui/ImagePreview/ImagePreview.js +200 -0
- package/lib/module/components/ui/ImagePreview/ImagePreview.js.map +1 -0
- package/lib/module/components/ui/ImagePreview/constants.js +5 -0
- package/lib/module/components/ui/ImagePreview/constants.js.map +1 -0
- package/lib/module/components/ui/ImagePreview/index.js +4 -0
- package/lib/module/components/ui/ImagePreview/index.js.map +1 -0
- package/lib/module/components/ui/ImagePreview/styles.js +55 -0
- package/lib/module/components/ui/ImagePreview/styles.js.map +1 -0
- package/lib/module/components/ui/ImagePreview/types.js +4 -0
- package/lib/module/components/ui/ImagePreview/types.js.map +1 -0
- package/lib/module/components/ui/Sheet/Sheet.js +40 -0
- package/lib/module/components/ui/Sheet/Sheet.js.map +1 -0
- package/lib/module/components/ui/Sheet/index.js +4 -0
- package/lib/module/components/ui/Sheet/index.js.map +1 -0
- package/lib/module/components/ui/Sheet/styles.js +41 -0
- package/lib/module/components/ui/Sheet/styles.js.map +1 -0
- package/lib/module/components/ui/Sheet/types.js +4 -0
- package/lib/module/components/ui/Sheet/types.js.map +1 -0
- package/lib/module/components/ui/SmsField/SmsField.js +40 -0
- package/lib/module/components/ui/SmsField/SmsField.js.map +1 -0
- package/lib/module/components/ui/SmsField/index.js +4 -0
- package/lib/module/components/ui/SmsField/index.js.map +1 -0
- package/lib/module/components/ui/SmsField/types.js +4 -0
- package/lib/module/components/ui/SmsField/types.js.map +1 -0
- package/lib/module/components/ui/TextEntryContent/TextEntryContent.js +60 -0
- package/lib/module/components/ui/TextEntryContent/TextEntryContent.js.map +1 -0
- package/lib/module/components/ui/TextEntryContent/index.js +4 -0
- package/lib/module/components/ui/TextEntryContent/index.js.map +1 -0
- package/lib/module/components/ui/TextEntryContent/styles.js +29 -0
- package/lib/module/components/ui/TextEntryContent/styles.js.map +1 -0
- package/lib/module/components/ui/TextEntryContent/types.js +4 -0
- package/lib/module/components/ui/TextEntryContent/types.js.map +1 -0
- package/lib/module/components/ui/index.js +5 -1
- package/lib/module/components/ui/index.js.map +1 -1
- package/lib/module/icons/data.js +87 -1
- package/lib/module/icons/data.js.map +1 -1
- package/lib/typescript/src/components/ui/ActionMenuContent/ActionMenuContent.d.ts +4 -0
- package/lib/typescript/src/components/ui/ActionMenuContent/ActionMenuContent.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/ActionMenuContent/index.d.ts +3 -0
- package/lib/typescript/src/components/ui/ActionMenuContent/index.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/ActionMenuContent/styles.d.ts +51 -0
- package/lib/typescript/src/components/ui/ActionMenuContent/styles.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/ActionMenuContent/types.d.ts +25 -0
- package/lib/typescript/src/components/ui/ActionMenuContent/types.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Button/ButtonBase.d.ts +3 -1
- package/lib/typescript/src/components/ui/Button/ButtonBase.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/IconButton/IconButton.d.ts +1 -1
- package/lib/typescript/src/components/ui/IconButton/IconButton.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/IconButton/types.d.ts +4 -0
- package/lib/typescript/src/components/ui/IconButton/types.d.ts.map +1 -1
- package/lib/typescript/src/components/ui/ImagePreview/ImagePreview.d.ts +3 -0
- package/lib/typescript/src/components/ui/ImagePreview/ImagePreview.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/ImagePreview/constants.d.ts +3 -0
- package/lib/typescript/src/components/ui/ImagePreview/constants.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/ImagePreview/index.d.ts +3 -0
- package/lib/typescript/src/components/ui/ImagePreview/index.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/ImagePreview/styles.d.ts +56 -0
- package/lib/typescript/src/components/ui/ImagePreview/styles.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/ImagePreview/types.d.ts +33 -0
- package/lib/typescript/src/components/ui/ImagePreview/types.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Sheet/Sheet.d.ts +4 -0
- package/lib/typescript/src/components/ui/Sheet/Sheet.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Sheet/index.d.ts +3 -0
- package/lib/typescript/src/components/ui/Sheet/index.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Sheet/styles.d.ts +38 -0
- package/lib/typescript/src/components/ui/Sheet/styles.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Sheet/types.d.ts +13 -0
- package/lib/typescript/src/components/ui/Sheet/types.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/SmsField/SmsField.d.ts +4 -0
- package/lib/typescript/src/components/ui/SmsField/SmsField.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/SmsField/index.d.ts +3 -0
- package/lib/typescript/src/components/ui/SmsField/index.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/SmsField/types.d.ts +12 -0
- package/lib/typescript/src/components/ui/SmsField/types.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/TextEntryContent/TextEntryContent.d.ts +4 -0
- package/lib/typescript/src/components/ui/TextEntryContent/TextEntryContent.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/TextEntryContent/index.d.ts +3 -0
- package/lib/typescript/src/components/ui/TextEntryContent/index.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/TextEntryContent/styles.d.ts +26 -0
- package/lib/typescript/src/components/ui/TextEntryContent/styles.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/TextEntryContent/types.d.ts +18 -0
- package/lib/typescript/src/components/ui/TextEntryContent/types.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/index.d.ts +10 -2
- package/lib/typescript/src/components/ui/index.d.ts.map +1 -1
- package/lib/typescript/src/icons/data.d.ts +1 -1
- package/lib/typescript/src/icons/data.d.ts.map +1 -1
- package/lib/typescript/type-tests/content-api.d.ts +2 -0
- package/lib/typescript/type-tests/content-api.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/ui/ActionMenuContent/ActionMenuContent.tsx +118 -0
- package/src/components/ui/ActionMenuContent/index.ts +6 -0
- package/src/components/ui/ActionMenuContent/styles.ts +47 -0
- package/src/components/ui/ActionMenuContent/types.ts +27 -0
- package/src/components/ui/Button/ButtonBase.tsx +11 -2
- package/src/components/ui/IconButton/IconButton.tsx +9 -1
- package/src/components/ui/IconButton/types.ts +4 -0
- package/src/components/ui/ImagePreview/ImagePreview.tsx +243 -0
- package/src/components/ui/ImagePreview/constants.ts +2 -0
- package/src/components/ui/ImagePreview/index.ts +6 -0
- package/src/components/ui/ImagePreview/styles.ts +48 -0
- package/src/components/ui/ImagePreview/types.ts +37 -0
- package/src/components/ui/Sheet/Sheet.tsx +43 -0
- package/src/components/ui/Sheet/index.ts +2 -0
- package/src/components/ui/Sheet/styles.ts +40 -0
- package/src/components/ui/Sheet/types.ts +14 -0
- package/src/components/ui/SmsField/SmsField.tsx +40 -0
- package/src/components/ui/SmsField/index.ts +2 -0
- package/src/components/ui/SmsField/types.ts +15 -0
- package/src/components/ui/TextEntryContent/TextEntryContent.tsx +56 -0
- package/src/components/ui/TextEntryContent/index.ts +2 -0
- package/src/components/ui/TextEntryContent/styles.ts +17 -0
- package/src/components/ui/TextEntryContent/types.ts +18 -0
- package/src/components/ui/index.ts +18 -2
- package/src/icons/data.ts +134 -0
- package/lib/module/components/ui/BorderBeam/BorderBeam.js +0 -102
- package/lib/module/components/ui/BorderBeam/BorderBeam.js.map +0 -1
- package/lib/module/components/ui/BorderBeam/BorderBeam.web.js +0 -108
- package/lib/module/components/ui/BorderBeam/BorderBeam.web.js.map +0 -1
- package/lib/module/components/ui/BorderBeam/index.js +0 -4
- package/lib/module/components/ui/BorderBeam/index.js.map +0 -1
- package/lib/module/components/ui/BorderBeam/normalizeBorderBeam.js +0 -23
- package/lib/module/components/ui/BorderBeam/normalizeBorderBeam.js.map +0 -1
- package/lib/module/components/ui/BorderBeam/shared.js +0 -54
- package/lib/module/components/ui/BorderBeam/shared.js.map +0 -1
- package/lib/module/components/ui/BorderBeam/styles.js +0 -18
- package/lib/module/components/ui/BorderBeam/styles.js.map +0 -1
- package/lib/module/components/ui/BorderBeam/types.js.map +0 -1
- package/lib/typescript/src/components/ui/BorderBeam/BorderBeam.d.ts +0 -10
- package/lib/typescript/src/components/ui/BorderBeam/BorderBeam.d.ts.map +0 -1
- package/lib/typescript/src/components/ui/BorderBeam/BorderBeam.web.d.ts +0 -5
- package/lib/typescript/src/components/ui/BorderBeam/BorderBeam.web.d.ts.map +0 -1
- package/lib/typescript/src/components/ui/BorderBeam/index.d.ts +0 -3
- package/lib/typescript/src/components/ui/BorderBeam/index.d.ts.map +0 -1
- package/lib/typescript/src/components/ui/BorderBeam/normalizeBorderBeam.d.ts +0 -16
- package/lib/typescript/src/components/ui/BorderBeam/normalizeBorderBeam.d.ts.map +0 -1
- package/lib/typescript/src/components/ui/BorderBeam/shared.d.ts +0 -28
- package/lib/typescript/src/components/ui/BorderBeam/shared.d.ts.map +0 -1
- package/lib/typescript/src/components/ui/BorderBeam/styles.d.ts +0 -15
- package/lib/typescript/src/components/ui/BorderBeam/styles.d.ts.map +0 -1
- package/lib/typescript/src/components/ui/BorderBeam/types.d.ts +0 -23
- package/lib/typescript/src/components/ui/BorderBeam/types.d.ts.map +0 -1
- package/src/components/ui/BorderBeam/BorderBeam.tsx +0 -130
- package/src/components/ui/BorderBeam/BorderBeam.web.tsx +0 -127
- package/src/components/ui/BorderBeam/index.ts +0 -2
- package/src/components/ui/BorderBeam/normalizeBorderBeam.ts +0 -58
- package/src/components/ui/BorderBeam/shared.ts +0 -47
- package/src/components/ui/BorderBeam/styles.ts +0 -16
- package/src/components/ui/BorderBeam/types.ts +0 -23
- /package/lib/module/components/ui/{BorderBeam → ActionMenuContent}/types.js +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Pressable, Text, View } from 'react-native';
|
|
3
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
4
|
+
import { space, useThemedStyles } from '../../../theme';
|
|
5
|
+
import { Button } from '../Button';
|
|
6
|
+
import type { ActionMenuContentProps } from './types';
|
|
7
|
+
import { makeStyles } from './styles';
|
|
8
|
+
|
|
9
|
+
export function ActionMenuContent({
|
|
10
|
+
actions,
|
|
11
|
+
onClose,
|
|
12
|
+
presentation = 'dialog',
|
|
13
|
+
title,
|
|
14
|
+
cancelLabel = '取消',
|
|
15
|
+
style,
|
|
16
|
+
testID = 'action-menu',
|
|
17
|
+
}: ActionMenuContentProps): React.JSX.Element {
|
|
18
|
+
const styles = useThemedStyles(makeStyles);
|
|
19
|
+
const insets = useSafeAreaInsets();
|
|
20
|
+
const [confirmationId, setConfirmationId] = useState<string | null>(null);
|
|
21
|
+
const isSheet = presentation === 'sheet';
|
|
22
|
+
const confirming = actions.find(
|
|
23
|
+
(action) =>
|
|
24
|
+
action.id === confirmationId && action.confirmation !== undefined
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!confirming) setConfirmationId(null);
|
|
29
|
+
}, [confirming]);
|
|
30
|
+
|
|
31
|
+
const content = (
|
|
32
|
+
<Pressable
|
|
33
|
+
accessible={false}
|
|
34
|
+
testID={`${testID}-card`}
|
|
35
|
+
style={[
|
|
36
|
+
isSheet ? styles.sheetCard : styles.dialogCard,
|
|
37
|
+
isSheet && { paddingBottom: insets.bottom + space[5] },
|
|
38
|
+
]}
|
|
39
|
+
onPress={(event) => event.stopPropagation()}
|
|
40
|
+
>
|
|
41
|
+
{title ? (
|
|
42
|
+
<Text
|
|
43
|
+
testID={`${testID}-title`}
|
|
44
|
+
style={styles.title}
|
|
45
|
+
numberOfLines={1}
|
|
46
|
+
ellipsizeMode="tail"
|
|
47
|
+
>
|
|
48
|
+
{title}
|
|
49
|
+
</Text>
|
|
50
|
+
) : null}
|
|
51
|
+
{confirming?.confirmation ? (
|
|
52
|
+
<>
|
|
53
|
+
<Text style={styles.confirmation} accessibilityLiveRegion="polite">
|
|
54
|
+
{confirming.confirmation.message}
|
|
55
|
+
</Text>
|
|
56
|
+
<View style={styles.sheetActions}>
|
|
57
|
+
<Button
|
|
58
|
+
label={cancelLabel}
|
|
59
|
+
variant="neutral"
|
|
60
|
+
style={styles.sheetAction}
|
|
61
|
+
onPress={() => setConfirmationId(null)}
|
|
62
|
+
/>
|
|
63
|
+
<Button
|
|
64
|
+
label={confirming.confirmation.confirmLabel}
|
|
65
|
+
variant="danger"
|
|
66
|
+
disabled={confirming.disabled}
|
|
67
|
+
loading={confirming.loading}
|
|
68
|
+
style={styles.sheetAction}
|
|
69
|
+
onPress={() => {
|
|
70
|
+
onClose();
|
|
71
|
+
confirming.onPress();
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
</View>
|
|
75
|
+
</>
|
|
76
|
+
) : (
|
|
77
|
+
<>
|
|
78
|
+
<View
|
|
79
|
+
testID={`${testID}-actions`}
|
|
80
|
+
style={isSheet ? styles.sheetActions : undefined}
|
|
81
|
+
>
|
|
82
|
+
{actions.map((action) => (
|
|
83
|
+
<Button
|
|
84
|
+
key={action.id}
|
|
85
|
+
label={action.label}
|
|
86
|
+
disabled={action.disabled}
|
|
87
|
+
loading={action.loading}
|
|
88
|
+
variant={isSheet ? (action.tone ?? 'neutral') : 'text'}
|
|
89
|
+
style={isSheet ? styles.sheetAction : undefined}
|
|
90
|
+
onPress={() => {
|
|
91
|
+
if (action.confirmation) {
|
|
92
|
+
setConfirmationId(action.id);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
action.onPress();
|
|
96
|
+
}}
|
|
97
|
+
/>
|
|
98
|
+
))}
|
|
99
|
+
</View>
|
|
100
|
+
{!isSheet ? (
|
|
101
|
+
<Button label={cancelLabel} variant="text" onPress={onClose} />
|
|
102
|
+
) : null}
|
|
103
|
+
</>
|
|
104
|
+
)}
|
|
105
|
+
</Pressable>
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Pressable
|
|
110
|
+
accessible={false}
|
|
111
|
+
testID={testID}
|
|
112
|
+
style={[isSheet ? styles.sheetRoot : styles.dialogRoot, style]}
|
|
113
|
+
onPress={onClose}
|
|
114
|
+
>
|
|
115
|
+
{content}
|
|
116
|
+
</Pressable>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
import { fw, radius, space, type as typography } from '../../../theme';
|
|
3
|
+
import type { ColorTokens } from '../../../theme';
|
|
4
|
+
export const makeStyles = (c: ColorTokens) =>
|
|
5
|
+
StyleSheet.create({
|
|
6
|
+
dialogRoot: {
|
|
7
|
+
flex: 1,
|
|
8
|
+
justifyContent: 'center',
|
|
9
|
+
alignItems: 'center',
|
|
10
|
+
backgroundColor: c.scrim,
|
|
11
|
+
},
|
|
12
|
+
sheetRoot: {
|
|
13
|
+
flex: 1,
|
|
14
|
+
justifyContent: 'flex-end',
|
|
15
|
+
backgroundColor: c.scrim,
|
|
16
|
+
},
|
|
17
|
+
dialogCard: {
|
|
18
|
+
backgroundColor: c.surface,
|
|
19
|
+
borderRadius: radius.xl,
|
|
20
|
+
padding: space[4],
|
|
21
|
+
width: '80%',
|
|
22
|
+
maxWidth: 400,
|
|
23
|
+
gap: space[2],
|
|
24
|
+
},
|
|
25
|
+
sheetCard: {
|
|
26
|
+
backgroundColor: c.surface,
|
|
27
|
+
borderTopLeftRadius: radius.xl,
|
|
28
|
+
borderTopRightRadius: radius.xl,
|
|
29
|
+
paddingHorizontal: space[5],
|
|
30
|
+
paddingTop: space[5],
|
|
31
|
+
gap: space[3],
|
|
32
|
+
},
|
|
33
|
+
title: {
|
|
34
|
+
color: c.foreground,
|
|
35
|
+
fontSize: typography.sm,
|
|
36
|
+
fontWeight: fw.semi,
|
|
37
|
+
textAlign: 'center',
|
|
38
|
+
paddingVertical: space[3],
|
|
39
|
+
},
|
|
40
|
+
confirmation: {
|
|
41
|
+
color: c.foregroundMuted,
|
|
42
|
+
fontSize: typography.xs,
|
|
43
|
+
textAlign: 'center',
|
|
44
|
+
},
|
|
45
|
+
sheetActions: { flexDirection: 'row', flexWrap: 'wrap', gap: space[3] },
|
|
46
|
+
sheetAction: { flex: 1 },
|
|
47
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export interface ActionMenuConfirmation {
|
|
4
|
+
message: string;
|
|
5
|
+
confirmLabel: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ActionMenuAction {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
onPress(): void;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
loading?: boolean;
|
|
14
|
+
tone?: 'neutral' | 'danger';
|
|
15
|
+
confirmation?: ActionMenuConfirmation;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 操作、确认有效性和宿主关闭由调用方负责。 */
|
|
19
|
+
export interface ActionMenuContentProps {
|
|
20
|
+
actions: readonly ActionMenuAction[];
|
|
21
|
+
presentation?: 'dialog' | 'sheet';
|
|
22
|
+
title?: string;
|
|
23
|
+
cancelLabel?: string;
|
|
24
|
+
onClose(): void;
|
|
25
|
+
style?: StyleProp<ViewStyle>;
|
|
26
|
+
testID?: string;
|
|
27
|
+
}
|
|
@@ -29,6 +29,8 @@ export type ButtonBaseProps = {
|
|
|
29
29
|
block?: boolean;
|
|
30
30
|
/** 方形约束 — width=height=sizing.h,优先级高于 horizontal padding。IconButton 用。 */
|
|
31
31
|
square?: boolean;
|
|
32
|
+
surfaceSize?: number;
|
|
33
|
+
loadingSize?: number;
|
|
32
34
|
accessibilityLabel?: string;
|
|
33
35
|
accessibilityRole?: AccessibilityRole;
|
|
34
36
|
/** SR 朗读 label 后的行为说明 hint。仅在"行为不显然"时加。 */
|
|
@@ -53,6 +55,8 @@ export function ButtonBase({
|
|
|
53
55
|
loading,
|
|
54
56
|
block,
|
|
55
57
|
square,
|
|
58
|
+
surfaceSize,
|
|
59
|
+
loadingSize,
|
|
56
60
|
accessibilityLabel,
|
|
57
61
|
accessibilityRole = 'button',
|
|
58
62
|
accessibilityHint,
|
|
@@ -64,7 +68,12 @@ export function ButtonBase({
|
|
|
64
68
|
const c = useColors();
|
|
65
69
|
const styles = useThemedStyles(makeStyles);
|
|
66
70
|
|
|
67
|
-
const sizing = useMemo(() =>
|
|
71
|
+
const sizing = useMemo(() => {
|
|
72
|
+
const base = sizingFor(size);
|
|
73
|
+
return square && surfaceSize !== undefined
|
|
74
|
+
? { ...base, h: surfaceSize }
|
|
75
|
+
: base;
|
|
76
|
+
}, [size, square, surfaceSize]);
|
|
68
77
|
const palette = useMemo(() => paletteFor(variant, c), [variant, c]);
|
|
69
78
|
|
|
70
79
|
const isText = variant === 'text';
|
|
@@ -128,7 +137,7 @@ export function ButtonBase({
|
|
|
128
137
|
]}
|
|
129
138
|
>
|
|
130
139
|
{loading ? (
|
|
131
|
-
<ActivityIndicator size=
|
|
140
|
+
<ActivityIndicator size={loadingSize ?? 'small'} color={palette.fg} />
|
|
132
141
|
) : (
|
|
133
142
|
children({ sizing, palette })
|
|
134
143
|
)}
|
|
@@ -20,6 +20,8 @@ export function IconButton({
|
|
|
20
20
|
icon,
|
|
21
21
|
onPress,
|
|
22
22
|
size = 'md',
|
|
23
|
+
iconSize,
|
|
24
|
+
surfaceSize,
|
|
23
25
|
variant = 'ghost',
|
|
24
26
|
color,
|
|
25
27
|
disabled,
|
|
@@ -46,6 +48,8 @@ export function IconButton({
|
|
|
46
48
|
square
|
|
47
49
|
onPress={onPress}
|
|
48
50
|
size={size}
|
|
51
|
+
surfaceSize={surfaceSize}
|
|
52
|
+
loadingSize={iconSize}
|
|
49
53
|
variant={variant}
|
|
50
54
|
loading={loading}
|
|
51
55
|
disabled={disabled === true || hasBlankLabel}
|
|
@@ -56,7 +60,11 @@ export function IconButton({
|
|
|
56
60
|
testID={testID}
|
|
57
61
|
>
|
|
58
62
|
{({ sizing, palette }) => (
|
|
59
|
-
<Icon
|
|
63
|
+
<Icon
|
|
64
|
+
name={icon}
|
|
65
|
+
size={iconSize ?? sizing.fs + 4}
|
|
66
|
+
color={color ?? palette.fg}
|
|
67
|
+
/>
|
|
60
68
|
)}
|
|
61
69
|
</ButtonBase>
|
|
62
70
|
);
|
|
@@ -12,6 +12,10 @@ export type IconButtonProps = {
|
|
|
12
12
|
onPress: () => void;
|
|
13
13
|
/** 尺寸,默认 'md'(28 / 36 / 44 方形) */
|
|
14
14
|
size?: ButtonSize;
|
|
15
|
+
/** 独立图标及加载指示尺寸;默认沿用 size 对应值。 */
|
|
16
|
+
iconSize?: number;
|
|
17
|
+
/** 方形表面边长;同时据此补足 44pt 命中区。默认沿用 size。 */
|
|
18
|
+
surfaceSize?: number;
|
|
15
19
|
/** 视觉变体,默认 'ghost'(透明底,适合 header right-tray 场景)。
|
|
16
20
|
* 不支持 'text'——text variant 撕除高度,与 square 方形布局冲突。 */
|
|
17
21
|
variant?: IconButtonVariant;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useEffect,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { Text, View } from 'react-native';
|
|
11
|
+
import { radius, useThemedStyles } from '../../../theme';
|
|
12
|
+
import { IconButton } from '../IconButton';
|
|
13
|
+
import { Thumbnail } from '../Thumbnail';
|
|
14
|
+
import { Carousel, type CarouselRef } from 'react-native-reanimated-carousel';
|
|
15
|
+
import { DELETE_LABEL, IMAGE_UNAVAILABLE } from './constants';
|
|
16
|
+
import { makeStyles } from './styles';
|
|
17
|
+
import type {
|
|
18
|
+
ImagePreviewHandle,
|
|
19
|
+
ImagePreviewProps,
|
|
20
|
+
ImagePreviewSelection,
|
|
21
|
+
ImagePreviewScrollRequest,
|
|
22
|
+
} from './types';
|
|
23
|
+
|
|
24
|
+
export const ImagePreview = forwardRef<ImagePreviewHandle, ImagePreviewProps>(
|
|
25
|
+
function ImagePreviewInner(
|
|
26
|
+
{
|
|
27
|
+
items,
|
|
28
|
+
width,
|
|
29
|
+
height,
|
|
30
|
+
initialId,
|
|
31
|
+
onCurrentChange,
|
|
32
|
+
onRequestDelete,
|
|
33
|
+
style,
|
|
34
|
+
testID,
|
|
35
|
+
},
|
|
36
|
+
ref
|
|
37
|
+
) {
|
|
38
|
+
const styles = useThemedStyles(makeStyles);
|
|
39
|
+
const carouselRef = useRef<CarouselRef>(null);
|
|
40
|
+
const mounted = useRef(false);
|
|
41
|
+
const moving = useRef(false);
|
|
42
|
+
const pendingScroll = useRef<ImagePreviewScrollRequest | undefined>(
|
|
43
|
+
undefined
|
|
44
|
+
);
|
|
45
|
+
const [selection, setSelection] = useState<ImagePreviewSelection>(() => ({
|
|
46
|
+
items,
|
|
47
|
+
index: Math.max(
|
|
48
|
+
0,
|
|
49
|
+
items.findIndex((item) => item.id === initialId)
|
|
50
|
+
),
|
|
51
|
+
initialized: items.length > 0,
|
|
52
|
+
carouselRevision: 0,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
if (selection.items !== items) {
|
|
56
|
+
const retainedIndex = items.findIndex(
|
|
57
|
+
(item) => item.id === selection.items[selection.index]?.id
|
|
58
|
+
);
|
|
59
|
+
const identityChanged =
|
|
60
|
+
items.length !== selection.items.length ||
|
|
61
|
+
items.some((item, index) => item.id !== selection.items[index]?.id);
|
|
62
|
+
setSelection({
|
|
63
|
+
items,
|
|
64
|
+
// SDK 的非动画定位不能撤回已排队的旧完成;只重建在途且身份集合变化的轮播。
|
|
65
|
+
carouselRevision:
|
|
66
|
+
selection.carouselRevision +
|
|
67
|
+
(moving.current && identityChanged ? 1 : 0),
|
|
68
|
+
initialized: selection.initialized || items.length > 0,
|
|
69
|
+
index: !selection.initialized
|
|
70
|
+
? Math.max(
|
|
71
|
+
0,
|
|
72
|
+
items.findIndex((item) => item.id === initialId)
|
|
73
|
+
)
|
|
74
|
+
: retainedIndex >= 0
|
|
75
|
+
? retainedIndex
|
|
76
|
+
: Math.max(0, Math.min(selection.index, items.length - 1)),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const current = items[selection.index];
|
|
81
|
+
const committed = useRef(selection);
|
|
82
|
+
const currentChange = useRef(onCurrentChange);
|
|
83
|
+
useLayoutEffect(() => {
|
|
84
|
+
mounted.current = true;
|
|
85
|
+
committed.current = selection;
|
|
86
|
+
currentChange.current = onCurrentChange;
|
|
87
|
+
return () => {
|
|
88
|
+
mounted.current = false;
|
|
89
|
+
};
|
|
90
|
+
}, [selection, onCurrentChange]);
|
|
91
|
+
|
|
92
|
+
useLayoutEffect(() => {
|
|
93
|
+
moving.current = false;
|
|
94
|
+
}, [selection.carouselRevision]);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const request = pendingScroll.current;
|
|
98
|
+
pendingScroll.current = undefined;
|
|
99
|
+
if (!request) return;
|
|
100
|
+
const index = committed.current.items.findIndex(
|
|
101
|
+
(item) => item.id === request.id
|
|
102
|
+
);
|
|
103
|
+
if (index >= 0)
|
|
104
|
+
carouselRef.current?.scrollTo({ index, animated: request.animated });
|
|
105
|
+
}, [selection.carouselRevision]);
|
|
106
|
+
|
|
107
|
+
const notifiedId = useRef<string | undefined>(undefined);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
// 待交接命令可能已同步完成,通知与句柄读取同一份已提交事实。
|
|
110
|
+
const displayed = committed.current.items[committed.current.index];
|
|
111
|
+
if (displayed?.id === notifiedId.current) return;
|
|
112
|
+
notifiedId.current = displayed?.id;
|
|
113
|
+
if (displayed) currentChange.current?.(displayed);
|
|
114
|
+
}, [current, onCurrentChange]);
|
|
115
|
+
|
|
116
|
+
useImperativeHandle(
|
|
117
|
+
ref,
|
|
118
|
+
() => ({
|
|
119
|
+
getCurrent: () =>
|
|
120
|
+
mounted.current
|
|
121
|
+
? committed.current.items[committed.current.index]
|
|
122
|
+
: undefined,
|
|
123
|
+
scrollTo: (id, animated = true) => {
|
|
124
|
+
if (!mounted.current) return;
|
|
125
|
+
const index = committed.current.items.findIndex(
|
|
126
|
+
(item) => item.id === id
|
|
127
|
+
);
|
|
128
|
+
if (index < 0) return;
|
|
129
|
+
if (moving.current || pendingScroll.current) {
|
|
130
|
+
// 新命令隔离旧完成投递;新 ref 就绪后原样交接 animated。
|
|
131
|
+
pendingScroll.current = { id, animated };
|
|
132
|
+
const next = {
|
|
133
|
+
...committed.current,
|
|
134
|
+
carouselRevision: committed.current.carouselRevision + 1,
|
|
135
|
+
};
|
|
136
|
+
committed.current = next;
|
|
137
|
+
setSelection(next);
|
|
138
|
+
} else {
|
|
139
|
+
carouselRef.current?.scrollTo({ index, animated });
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
[]
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// SDK 只协调身份变化;source、label、busy 更新不应打断当前动画。
|
|
147
|
+
const data = useMemo(() => items.map((item) => item.id), [items]);
|
|
148
|
+
if (!current) return null;
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<View style={[styles.container, style]} testID={testID}>
|
|
152
|
+
<Carousel
|
|
153
|
+
key={selection.carouselRevision}
|
|
154
|
+
ref={carouselRef}
|
|
155
|
+
data={data}
|
|
156
|
+
defaultIndex={selection.index}
|
|
157
|
+
loop={false}
|
|
158
|
+
itemSize={width}
|
|
159
|
+
style={{ width, height }}
|
|
160
|
+
keyExtractor={(id) => id}
|
|
161
|
+
testID={testID ? `${testID}-carousel` : undefined}
|
|
162
|
+
onScrollStart={() => {
|
|
163
|
+
if (
|
|
164
|
+
mounted.current &&
|
|
165
|
+
selection.carouselRevision === committed.current.carouselRevision
|
|
166
|
+
) {
|
|
167
|
+
moving.current = true;
|
|
168
|
+
}
|
|
169
|
+
}}
|
|
170
|
+
onSnapToItem={(index) => {
|
|
171
|
+
const latest = committed.current;
|
|
172
|
+
if (
|
|
173
|
+
!mounted.current ||
|
|
174
|
+
selection.carouselRevision !== latest.carouselRevision ||
|
|
175
|
+
items.length !== latest.items.length ||
|
|
176
|
+
items.some(
|
|
177
|
+
(item, itemIndex) => item.id !== latest.items[itemIndex]?.id
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
return;
|
|
181
|
+
moving.current = false;
|
|
182
|
+
const id = items[index]?.id;
|
|
183
|
+
const nextIndex = latest.items.findIndex((item) => item.id === id);
|
|
184
|
+
if (nextIndex >= 0 && nextIndex !== latest.index) {
|
|
185
|
+
const next = { ...latest, index: nextIndex };
|
|
186
|
+
// SDK 已完成落位;句柄同步可读,不等待 React 下一次提交。
|
|
187
|
+
committed.current = next;
|
|
188
|
+
setSelection(next);
|
|
189
|
+
notifiedId.current = id;
|
|
190
|
+
currentChange.current?.(next.items[nextIndex]!);
|
|
191
|
+
}
|
|
192
|
+
}}
|
|
193
|
+
renderItem={({ item: id, index }) => {
|
|
194
|
+
const item = items.find((candidate) => candidate.id === id)!;
|
|
195
|
+
return (
|
|
196
|
+
<View style={[styles.media, { width, height }]}>
|
|
197
|
+
{item.source === undefined ? (
|
|
198
|
+
<View style={styles.placeholder}>
|
|
199
|
+
<Text style={styles.placeholderText}>
|
|
200
|
+
{IMAGE_UNAVAILABLE}
|
|
201
|
+
</Text>
|
|
202
|
+
</View>
|
|
203
|
+
) : (
|
|
204
|
+
<Thumbnail
|
|
205
|
+
source={item.source}
|
|
206
|
+
size={{ width, height, borderRadius: radius.xl }}
|
|
207
|
+
resizeMode="contain"
|
|
208
|
+
imageStyle={styles.image}
|
|
209
|
+
accessibilityLabel={item.label ?? `图片第 ${index + 1} 张`}
|
|
210
|
+
fallback={
|
|
211
|
+
<View style={styles.placeholder}>
|
|
212
|
+
<Text style={styles.placeholderText}>
|
|
213
|
+
{IMAGE_UNAVAILABLE}
|
|
214
|
+
</Text>
|
|
215
|
+
</View>
|
|
216
|
+
}
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
<View pointerEvents="none" style={styles.border} />
|
|
220
|
+
</View>
|
|
221
|
+
);
|
|
222
|
+
}}
|
|
223
|
+
/>
|
|
224
|
+
<View style={styles.counter}>
|
|
225
|
+
<Text style={styles.counterText}>
|
|
226
|
+
第{selection.index + 1}/{items.length}张
|
|
227
|
+
</Text>
|
|
228
|
+
</View>
|
|
229
|
+
{current.canDelete && onRequestDelete ? (
|
|
230
|
+
<IconButton
|
|
231
|
+
icon="trash"
|
|
232
|
+
size="sm"
|
|
233
|
+
variant="neutral"
|
|
234
|
+
disabled={current.deleting}
|
|
235
|
+
accessibilityLabel={DELETE_LABEL}
|
|
236
|
+
style={styles.deleteButton}
|
|
237
|
+
onPress={() => onRequestDelete(current)}
|
|
238
|
+
/>
|
|
239
|
+
) : null}
|
|
240
|
+
</View>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
import { fontMono, radius, space, type as typography } from '../../../theme';
|
|
3
|
+
import type { ColorTokens } from '../../../theme';
|
|
4
|
+
|
|
5
|
+
export const makeStyles = (colors: ColorTokens) =>
|
|
6
|
+
StyleSheet.create({
|
|
7
|
+
container: { alignItems: 'center', gap: space[2] },
|
|
8
|
+
media: {
|
|
9
|
+
backgroundColor: 'black',
|
|
10
|
+
borderRadius: radius.xl,
|
|
11
|
+
overflow: 'hidden',
|
|
12
|
+
},
|
|
13
|
+
border: {
|
|
14
|
+
...StyleSheet.absoluteFill,
|
|
15
|
+
borderColor: colors.outline,
|
|
16
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
17
|
+
borderRadius: radius.xl,
|
|
18
|
+
},
|
|
19
|
+
image: { backgroundColor: 'black' },
|
|
20
|
+
placeholder: {
|
|
21
|
+
width: '100%',
|
|
22
|
+
height: '100%',
|
|
23
|
+
backgroundColor: 'black',
|
|
24
|
+
justifyContent: 'center',
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
paddingHorizontal: space[3],
|
|
27
|
+
},
|
|
28
|
+
placeholderText: {
|
|
29
|
+
color: 'white',
|
|
30
|
+
fontSize: typography.sm,
|
|
31
|
+
textAlign: 'center',
|
|
32
|
+
},
|
|
33
|
+
counter: {
|
|
34
|
+
alignSelf: 'center',
|
|
35
|
+
paddingVertical: space[1],
|
|
36
|
+
paddingHorizontal: space[3],
|
|
37
|
+
borderRadius: radius.pill,
|
|
38
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
39
|
+
borderColor: colors.glassPillBorder,
|
|
40
|
+
backgroundColor: colors.glassHighlight,
|
|
41
|
+
},
|
|
42
|
+
counterText: {
|
|
43
|
+
color: colors.foreground,
|
|
44
|
+
fontSize: typography.sm,
|
|
45
|
+
fontFamily: fontMono,
|
|
46
|
+
},
|
|
47
|
+
deleteButton: { borderRadius: radius.pill },
|
|
48
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ImageSourcePropType, StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export interface ImagePreviewItem {
|
|
4
|
+
id: string;
|
|
5
|
+
source?: ImageSourcePropType;
|
|
6
|
+
label?: string;
|
|
7
|
+
canDelete?: boolean;
|
|
8
|
+
deleting?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ImagePreviewProps {
|
|
12
|
+
items: readonly ImagePreviewItem[];
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
initialId?: string;
|
|
16
|
+
onCurrentChange?(item: Readonly<ImagePreviewItem>): void;
|
|
17
|
+
onRequestDelete?(item: Readonly<ImagePreviewItem>): void;
|
|
18
|
+
style?: StyleProp<ViewStyle>;
|
|
19
|
+
testID?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ImagePreviewHandle {
|
|
23
|
+
scrollTo(id: string, animated?: boolean): void;
|
|
24
|
+
getCurrent(): Readonly<ImagePreviewItem> | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ImagePreviewSelection {
|
|
28
|
+
items: readonly ImagePreviewItem[];
|
|
29
|
+
index: number;
|
|
30
|
+
initialized: boolean;
|
|
31
|
+
carouselRevision: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ImagePreviewScrollRequest {
|
|
35
|
+
id: string;
|
|
36
|
+
animated: boolean;
|
|
37
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ScrollView, View } from 'react-native';
|
|
3
|
+
import { useThemedStyles } from '../../../theme';
|
|
4
|
+
import { makeStyles } from './styles';
|
|
5
|
+
import type { SheetProps } from './types';
|
|
6
|
+
|
|
7
|
+
export function Sheet({
|
|
8
|
+
children,
|
|
9
|
+
header,
|
|
10
|
+
footer,
|
|
11
|
+
contentMode = 'fixed',
|
|
12
|
+
contentContainerStyle,
|
|
13
|
+
style,
|
|
14
|
+
testID,
|
|
15
|
+
}: SheetProps): React.JSX.Element {
|
|
16
|
+
const styles = useThemedStyles(makeStyles);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<View style={[styles.root, style]} testID={testID}>
|
|
20
|
+
{header ?? (
|
|
21
|
+
<View style={styles.grabberContainer}>
|
|
22
|
+
<View style={styles.grabber} />
|
|
23
|
+
</View>
|
|
24
|
+
)}
|
|
25
|
+
{contentMode === 'scroll' ? (
|
|
26
|
+
<ScrollView
|
|
27
|
+
style={styles.contentRegion}
|
|
28
|
+
contentContainerStyle={[styles.content, contentContainerStyle]}
|
|
29
|
+
keyboardShouldPersistTaps="handled"
|
|
30
|
+
>
|
|
31
|
+
{children}
|
|
32
|
+
</ScrollView>
|
|
33
|
+
) : (
|
|
34
|
+
<View
|
|
35
|
+
style={[styles.contentRegion, styles.content, contentContainerStyle]}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</View>
|
|
39
|
+
)}
|
|
40
|
+
{footer ? <View style={styles.footer}>{footer}</View> : null}
|
|
41
|
+
</View>
|
|
42
|
+
);
|
|
43
|
+
}
|