@unif/react-native-design 0.37.0 → 0.39.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 (51) hide show
  1. package/lib/module/components/ui/ActionMenuContent/ActionMenuContent.js +62 -18
  2. package/lib/module/components/ui/ActionMenuContent/ActionMenuContent.js.map +1 -1
  3. package/lib/module/components/ui/ActionMenuContent/ActionMenuItem.js +57 -0
  4. package/lib/module/components/ui/ActionMenuContent/ActionMenuItem.js.map +1 -0
  5. package/lib/module/components/ui/ActionMenuContent/ActionMenuSheetCard.js +25 -0
  6. package/lib/module/components/ui/ActionMenuContent/ActionMenuSheetCard.js.map +1 -0
  7. package/lib/module/components/ui/ActionMenuContent/styles.js +50 -2
  8. package/lib/module/components/ui/ActionMenuContent/styles.js.map +1 -1
  9. package/lib/module/components/ui/ActionMenuContent/useMenuMousePress.js +26 -0
  10. package/lib/module/components/ui/ActionMenuContent/useMenuMousePress.js.map +1 -0
  11. package/lib/module/components/ui/ImagePreview/ImagePreview.js +81 -72
  12. package/lib/module/components/ui/ImagePreview/ImagePreview.js.map +1 -1
  13. package/lib/module/components/ui/ImagePreview/constants.js +2 -0
  14. package/lib/module/components/ui/ImagePreview/constants.js.map +1 -1
  15. package/lib/module/components/ui/ImagePreview/styles.js +5 -1
  16. package/lib/module/components/ui/ImagePreview/styles.js.map +1 -1
  17. package/lib/module/components/ui/Sheet/Sheet.js +2 -1
  18. package/lib/module/components/ui/Sheet/Sheet.js.map +1 -1
  19. package/lib/typescript/src/components/ui/ActionMenuContent/ActionMenuContent.d.ts +1 -1
  20. package/lib/typescript/src/components/ui/ActionMenuContent/ActionMenuContent.d.ts.map +1 -1
  21. package/lib/typescript/src/components/ui/ActionMenuContent/ActionMenuItem.d.ts +3 -0
  22. package/lib/typescript/src/components/ui/ActionMenuContent/ActionMenuItem.d.ts.map +1 -0
  23. package/lib/typescript/src/components/ui/ActionMenuContent/ActionMenuSheetCard.d.ts +3 -0
  24. package/lib/typescript/src/components/ui/ActionMenuContent/ActionMenuSheetCard.d.ts.map +1 -0
  25. package/lib/typescript/src/components/ui/ActionMenuContent/styles.d.ts +57 -2
  26. package/lib/typescript/src/components/ui/ActionMenuContent/styles.d.ts.map +1 -1
  27. package/lib/typescript/src/components/ui/ActionMenuContent/types.d.ts +15 -1
  28. package/lib/typescript/src/components/ui/ActionMenuContent/types.d.ts.map +1 -1
  29. package/lib/typescript/src/components/ui/ActionMenuContent/useMenuMousePress.d.ts +10 -0
  30. package/lib/typescript/src/components/ui/ActionMenuContent/useMenuMousePress.d.ts.map +1 -0
  31. package/lib/typescript/src/components/ui/ImagePreview/ImagePreview.d.ts.map +1 -1
  32. package/lib/typescript/src/components/ui/ImagePreview/constants.d.ts +1 -0
  33. package/lib/typescript/src/components/ui/ImagePreview/constants.d.ts.map +1 -1
  34. package/lib/typescript/src/components/ui/ImagePreview/styles.d.ts +4 -1
  35. package/lib/typescript/src/components/ui/ImagePreview/styles.d.ts.map +1 -1
  36. package/lib/typescript/src/components/ui/Sheet/Sheet.d.ts +1 -1
  37. package/lib/typescript/src/components/ui/Sheet/Sheet.d.ts.map +1 -1
  38. package/lib/typescript/src/components/ui/Sheet/types.d.ts +1 -0
  39. package/lib/typescript/src/components/ui/Sheet/types.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/components/ui/ActionMenuContent/ActionMenuContent.tsx +108 -50
  42. package/src/components/ui/ActionMenuContent/ActionMenuItem.tsx +56 -0
  43. package/src/components/ui/ActionMenuContent/ActionMenuSheetCard.tsx +28 -0
  44. package/src/components/ui/ActionMenuContent/styles.ts +50 -3
  45. package/src/components/ui/ActionMenuContent/types.ts +17 -1
  46. package/src/components/ui/ActionMenuContent/useMenuMousePress.ts +30 -0
  47. package/src/components/ui/ImagePreview/ImagePreview.tsx +92 -76
  48. package/src/components/ui/ImagePreview/constants.ts +3 -0
  49. package/src/components/ui/ImagePreview/styles.ts +5 -1
  50. package/src/components/ui/Sheet/Sheet.tsx +4 -1
  51. package/src/components/ui/Sheet/types.ts +1 -0
@@ -1,8 +1,55 @@
1
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) =>
2
+ import {
3
+ fixed,
4
+ fw,
5
+ r,
6
+ radius,
7
+ space,
8
+ type as typography,
9
+ } from '../../../theme';
10
+ import type { ColorTokens, ShadowTokens } from '../../../theme';
11
+ export const makeStyles = (c: ColorTokens, shadows: ShadowTokens) =>
5
12
  StyleSheet.create({
13
+ popoverRoot: {
14
+ width: r(200),
15
+ maxWidth: '100%',
16
+ borderRadius: r(20),
17
+ borderWidth: 0.5,
18
+ borderColor: c.outline,
19
+ backgroundColor: c.surface,
20
+ ...shadows.card,
21
+ },
22
+ popoverCard: {
23
+ borderRadius: r(20),
24
+ overflow: 'hidden',
25
+ paddingVertical: space[2],
26
+ },
27
+ popoverRow: {
28
+ minHeight: fixed.hitTarget,
29
+ flexDirection: 'row',
30
+ alignItems: 'center',
31
+ gap: space[5],
32
+ paddingVertical: space[3],
33
+ paddingHorizontal: space[6],
34
+ },
35
+ popoverRowPressed: { backgroundColor: c.surfaceContainer },
36
+ popoverRowDisabled: { opacity: 0.4 },
37
+ popoverIcon: {
38
+ width: 40,
39
+ height: 40,
40
+ borderRadius: 20,
41
+ alignItems: 'center',
42
+ justifyContent: 'center',
43
+ backgroundColor: c.surfaceContainer,
44
+ },
45
+ popoverLabel: {
46
+ flex: 1,
47
+ minWidth: 0,
48
+ fontSize: typography.body,
49
+ color: c.foreground,
50
+ },
51
+ popoverDanger: { color: c.error },
52
+ popoverConfirmationActions: { paddingTop: space[2] },
6
53
  dialogRoot: {
7
54
  flex: 1,
8
55
  justifyContent: 'center',
@@ -1,4 +1,6 @@
1
+ import type { ReactNode } from 'react';
1
2
  import type { StyleProp, ViewStyle } from 'react-native';
3
+ import type { IconName } from '../Icon';
2
4
 
3
5
  export interface ActionMenuConfirmation {
4
6
  message: string;
@@ -8,6 +10,8 @@ export interface ActionMenuConfirmation {
8
10
  export interface ActionMenuAction {
9
11
  id: string;
10
12
  label: string;
13
+ icon?: IconName;
14
+ accessibilityHint?: string;
11
15
  onPress(): void;
12
16
  disabled?: boolean;
13
17
  loading?: boolean;
@@ -18,10 +22,22 @@ export interface ActionMenuAction {
18
22
  /** 操作、确认有效性和宿主关闭由调用方负责。 */
19
23
  export interface ActionMenuContentProps {
20
24
  actions: readonly ActionMenuAction[];
21
- presentation?: 'dialog' | 'sheet';
25
+ presentation?: 'dialog' | 'sheet' | 'popover';
22
26
  title?: string;
23
27
  cancelLabel?: string;
24
28
  onClose(): void;
25
29
  style?: StyleProp<ViewStyle>;
30
+ contentStyle?: StyleProp<ViewStyle>;
26
31
  testID?: string;
27
32
  }
33
+
34
+ export interface ActionMenuItemProps {
35
+ action: ActionMenuAction;
36
+ onPress(): void;
37
+ }
38
+
39
+ export interface ActionMenuSheetCardProps {
40
+ children: ReactNode;
41
+ contentStyle?: StyleProp<ViewStyle>;
42
+ testID: string;
43
+ }
@@ -0,0 +1,30 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Platform } from 'react-native';
3
+ import type { PointerEvent } from 'react-native';
4
+
5
+ /** Web 菜单按下时保留原输入焦点;实际选择只由 Pressable.onPress 交付。 */
6
+ export function useMenuMousePress(disabled: boolean) {
7
+ const [pressed, setPressed] = useState(false);
8
+ const enabled = Platform.OS === 'web' && !disabled;
9
+ useEffect(() => {
10
+ if (disabled) setPressed(false);
11
+ }, [disabled]);
12
+ const begin = (event: PointerEvent) => {
13
+ if (
14
+ !enabled ||
15
+ event.nativeEvent.pointerType !== 'mouse' ||
16
+ event.nativeEvent.button !== 0
17
+ )
18
+ return;
19
+ event.preventDefault();
20
+ setPressed(true);
21
+ };
22
+ const end = () => setPressed(false);
23
+ return {
24
+ mousePressed: enabled && pressed,
25
+ onPointerDown: begin,
26
+ onPointerUp: end,
27
+ onPointerCancel: end,
28
+ onPointerLeave: end,
29
+ };
30
+ }
@@ -12,7 +12,11 @@ import { radius, useThemedStyles } from '../../../theme';
12
12
  import { IconButton } from '../IconButton';
13
13
  import { Thumbnail } from '../Thumbnail';
14
14
  import { Carousel, type CarouselRef } from 'react-native-reanimated-carousel';
15
- import { DELETE_LABEL, IMAGE_UNAVAILABLE } from './constants';
15
+ import {
16
+ DELETE_LABEL,
17
+ IMAGE_COUNTER_HEIGHT,
18
+ IMAGE_UNAVAILABLE,
19
+ } from './constants';
16
20
  import { makeStyles } from './styles';
17
21
  import type {
18
22
  ImagePreviewHandle,
@@ -149,82 +153,94 @@ export const ImagePreview = forwardRef<ImagePreviewHandle, ImagePreviewProps>(
149
153
 
150
154
  return (
151
155
  <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
156
+ <View style={{ width, height: height + IMAGE_COUNTER_HEIGHT / 2 }}>
157
+ <Carousel
158
+ key={selection.carouselRevision}
159
+ ref={carouselRef}
160
+ data={data}
161
+ defaultIndex={selection.index}
162
+ loop={false}
163
+ itemSize={width}
164
+ style={{ width, height }}
165
+ keyExtractor={(id) => id}
166
+ testID={testID ? `${testID}-carousel` : undefined}
167
+ onScrollStart={() => {
168
+ if (
169
+ mounted.current &&
170
+ selection.carouselRevision ===
171
+ committed.current.carouselRevision
172
+ ) {
173
+ moving.current = true;
174
+ }
175
+ }}
176
+ onSnapToItem={(index) => {
177
+ const latest = committed.current;
178
+ if (
179
+ !mounted.current ||
180
+ selection.carouselRevision !== latest.carouselRevision ||
181
+ items.length !== latest.items.length ||
182
+ items.some(
183
+ (item, itemIndex) => item.id !== latest.items[itemIndex]?.id
184
+ )
178
185
  )
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>
186
+ return;
187
+ moving.current = false;
188
+ const id = items[index]?.id;
189
+ const nextIndex = latest.items.findIndex(
190
+ (item) => item.id === id
191
+ );
192
+ if (nextIndex >= 0 && nextIndex !== latest.index) {
193
+ const next = { ...latest, index: nextIndex };
194
+ // SDK 已完成落位;句柄同步可读,不等待 React 下一次提交。
195
+ committed.current = next;
196
+ setSelection(next);
197
+ notifiedId.current = id;
198
+ currentChange.current?.(next.items[nextIndex]!);
199
+ }
200
+ }}
201
+ renderItem={({ item: id, index }) => {
202
+ const item = items.find((candidate) => candidate.id === id)!;
203
+ return (
204
+ <View style={[styles.media, { width, height }]}>
205
+ {item.source === undefined ? (
206
+ <View style={styles.placeholder}>
207
+ <Text style={styles.placeholderText}>
208
+ {IMAGE_UNAVAILABLE}
209
+ </Text>
210
+ </View>
211
+ ) : (
212
+ <Thumbnail
213
+ source={item.source}
214
+ size={{ width, height, borderRadius: radius.xl }}
215
+ resizeMode="contain"
216
+ imageStyle={styles.image}
217
+ accessibilityLabel={
218
+ item.label ?? `图片第 ${index + 1} 张`
219
+ }
220
+ fallback={
221
+ <View style={styles.placeholder}>
222
+ <Text style={styles.placeholderText}>
223
+ {IMAGE_UNAVAILABLE}
224
+ </Text>
225
+ </View>
226
+ }
227
+ />
228
+ )}
229
+ <View pointerEvents="none" style={styles.border} />
230
+ </View>
231
+ );
232
+ }}
233
+ />
234
+ <View
235
+ style={styles.counter}
236
+ accessible
237
+ accessibilityLiveRegion="polite"
238
+ accessibilityLabel={`第${selection.index + 1}张,共${items.length}张`}
239
+ >
240
+ <Text style={styles.counterText}>
241
+ 第{selection.index + 1}/{items.length}张
242
+ </Text>
243
+ </View>
228
244
  </View>
229
245
  {current.canDelete && onRequestDelete ? (
230
246
  <IconButton
@@ -1,2 +1,5 @@
1
+ import { r } from '../../../theme';
2
+
1
3
  export const IMAGE_UNAVAILABLE = '图片暂不可预览';
2
4
  export const DELETE_LABEL = '删除当前照片';
5
+ export const IMAGE_COUNTER_HEIGHT = r(26);
@@ -1,6 +1,7 @@
1
1
  import { StyleSheet } from 'react-native';
2
2
  import { fontMono, radius, space, type as typography } from '../../../theme';
3
3
  import type { ColorTokens } from '../../../theme';
4
+ import { IMAGE_COUNTER_HEIGHT } from './constants';
4
5
 
5
6
  export const makeStyles = (colors: ColorTokens) =>
6
7
  StyleSheet.create({
@@ -31,8 +32,11 @@ export const makeStyles = (colors: ColorTokens) =>
31
32
  textAlign: 'center',
32
33
  },
33
34
  counter: {
35
+ position: 'absolute',
36
+ bottom: 0,
34
37
  alignSelf: 'center',
35
- paddingVertical: space[1],
38
+ height: IMAGE_COUNTER_HEIGHT,
39
+ justifyContent: 'center',
36
40
  paddingHorizontal: space[3],
37
41
  borderRadius: radius.pill,
38
42
  borderWidth: StyleSheet.hairlineWidth,
@@ -10,6 +10,7 @@ export function Sheet({
10
10
  footer,
11
11
  contentMode = 'fixed',
12
12
  contentContainerStyle,
13
+ footerStyle,
13
14
  style,
14
15
  testID,
15
16
  }: SheetProps): React.JSX.Element {
@@ -37,7 +38,9 @@ export function Sheet({
37
38
  {children}
38
39
  </View>
39
40
  )}
40
- {footer ? <View style={styles.footer}>{footer}</View> : null}
41
+ {footer ? (
42
+ <View style={[styles.footer, footerStyle]}>{footer}</View>
43
+ ) : null}
41
44
  </View>
42
45
  );
43
46
  }
@@ -9,6 +9,7 @@ export interface SheetProps {
9
9
  footer?: ReactNode;
10
10
  contentMode?: SheetContentMode;
11
11
  contentContainerStyle?: StyleProp<ViewStyle>;
12
+ footerStyle?: StyleProp<ViewStyle>;
12
13
  style?: StyleProp<ViewStyle>;
13
14
  testID?: string;
14
15
  }