@unif/react-native-design 0.33.1 → 0.35.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 (229) hide show
  1. package/jest-preset.js +1 -1
  2. package/jest-setup.js +16 -1
  3. package/lib/module/components/business/GlassStats/GlassStats.js +6 -7
  4. package/lib/module/components/business/GlassStats/GlassStats.js.map +1 -1
  5. package/lib/module/components/business/GlassStats/styles.js +5 -1
  6. package/lib/module/components/business/GlassStats/styles.js.map +1 -1
  7. package/lib/module/components/ui/ActionMenuContent/ActionMenuContent.js +97 -0
  8. package/lib/module/components/ui/ActionMenuContent/ActionMenuContent.js.map +1 -0
  9. package/lib/module/components/ui/ActionMenuContent/index.js +4 -0
  10. package/lib/module/components/ui/ActionMenuContent/index.js.map +1 -0
  11. package/lib/module/components/ui/ActionMenuContent/styles.js +54 -0
  12. package/lib/module/components/ui/ActionMenuContent/styles.js.map +1 -0
  13. package/lib/module/components/ui/ActionMenuContent/types.js.map +1 -0
  14. package/lib/module/components/ui/Button/ButtonBase.js +10 -2
  15. package/lib/module/components/ui/Button/ButtonBase.js.map +1 -1
  16. package/lib/module/components/ui/GlassLayer/GlassLayer.js +39 -0
  17. package/lib/module/components/ui/GlassLayer/GlassLayer.js.map +1 -0
  18. package/lib/module/components/ui/GlassLayer/GlassLayer.web.js +36 -0
  19. package/lib/module/components/ui/GlassLayer/GlassLayer.web.js.map +1 -0
  20. package/lib/module/components/ui/GlassLayer/constants.js +8 -0
  21. package/lib/module/components/ui/GlassLayer/constants.js.map +1 -0
  22. package/lib/module/components/ui/GlassLayer/index.js +4 -0
  23. package/lib/module/components/ui/GlassLayer/index.js.map +1 -0
  24. package/lib/module/components/ui/{BorderBeam → GlassLayer}/types.js.map +1 -1
  25. package/lib/module/components/ui/IconButton/IconButton.js +5 -1
  26. package/lib/module/components/ui/IconButton/IconButton.js.map +1 -1
  27. package/lib/module/components/ui/ImagePreview/ImagePreview.js +200 -0
  28. package/lib/module/components/ui/ImagePreview/ImagePreview.js.map +1 -0
  29. package/lib/module/components/ui/ImagePreview/constants.js +5 -0
  30. package/lib/module/components/ui/ImagePreview/constants.js.map +1 -0
  31. package/lib/module/components/ui/ImagePreview/index.js +4 -0
  32. package/lib/module/components/ui/ImagePreview/index.js.map +1 -0
  33. package/lib/module/components/ui/ImagePreview/styles.js +55 -0
  34. package/lib/module/components/ui/ImagePreview/styles.js.map +1 -0
  35. package/lib/module/components/ui/ImagePreview/types.js +4 -0
  36. package/lib/module/components/ui/ImagePreview/types.js.map +1 -0
  37. package/lib/module/components/ui/Sheet/Sheet.js +40 -0
  38. package/lib/module/components/ui/Sheet/Sheet.js.map +1 -0
  39. package/lib/module/components/ui/Sheet/index.js +4 -0
  40. package/lib/module/components/ui/Sheet/index.js.map +1 -0
  41. package/lib/module/components/ui/Sheet/styles.js +41 -0
  42. package/lib/module/components/ui/Sheet/styles.js.map +1 -0
  43. package/lib/module/components/ui/Sheet/types.js +4 -0
  44. package/lib/module/components/ui/Sheet/types.js.map +1 -0
  45. package/lib/module/components/ui/SmsField/SmsField.js +40 -0
  46. package/lib/module/components/ui/SmsField/SmsField.js.map +1 -0
  47. package/lib/module/components/ui/SmsField/index.js +4 -0
  48. package/lib/module/components/ui/SmsField/index.js.map +1 -0
  49. package/lib/module/components/ui/SmsField/types.js +4 -0
  50. package/lib/module/components/ui/SmsField/types.js.map +1 -0
  51. package/lib/module/components/ui/TextEntryContent/TextEntryContent.js +60 -0
  52. package/lib/module/components/ui/TextEntryContent/TextEntryContent.js.map +1 -0
  53. package/lib/module/components/ui/TextEntryContent/index.js +4 -0
  54. package/lib/module/components/ui/TextEntryContent/index.js.map +1 -0
  55. package/lib/module/components/ui/TextEntryContent/styles.js +29 -0
  56. package/lib/module/components/ui/TextEntryContent/styles.js.map +1 -0
  57. package/lib/module/components/ui/TextEntryContent/types.js +4 -0
  58. package/lib/module/components/ui/TextEntryContent/types.js.map +1 -0
  59. package/lib/module/components/ui/index.js +6 -2
  60. package/lib/module/components/ui/index.js.map +1 -1
  61. package/lib/module/icons/data.js +87 -1
  62. package/lib/module/icons/data.js.map +1 -1
  63. package/lib/module/theme/colors.js +4 -7
  64. package/lib/module/theme/colors.js.map +1 -1
  65. package/lib/module/theme/index.js +0 -1
  66. package/lib/module/theme/index.js.map +1 -1
  67. package/lib/typescript/src/components/business/GlassStats/GlassStats.d.ts +2 -4
  68. package/lib/typescript/src/components/business/GlassStats/GlassStats.d.ts.map +1 -1
  69. package/lib/typescript/src/components/business/GlassStats/styles.d.ts +4 -1
  70. package/lib/typescript/src/components/business/GlassStats/styles.d.ts.map +1 -1
  71. package/lib/typescript/src/components/ui/ActionMenuContent/ActionMenuContent.d.ts +4 -0
  72. package/lib/typescript/src/components/ui/ActionMenuContent/ActionMenuContent.d.ts.map +1 -0
  73. package/lib/typescript/src/components/ui/ActionMenuContent/index.d.ts +3 -0
  74. package/lib/typescript/src/components/ui/ActionMenuContent/index.d.ts.map +1 -0
  75. package/lib/typescript/src/components/ui/ActionMenuContent/styles.d.ts +51 -0
  76. package/lib/typescript/src/components/ui/ActionMenuContent/styles.d.ts.map +1 -0
  77. package/lib/typescript/src/components/ui/ActionMenuContent/types.d.ts +25 -0
  78. package/lib/typescript/src/components/ui/ActionMenuContent/types.d.ts.map +1 -0
  79. package/lib/typescript/src/components/ui/Button/ButtonBase.d.ts +3 -1
  80. package/lib/typescript/src/components/ui/Button/ButtonBase.d.ts.map +1 -1
  81. package/lib/typescript/src/components/ui/GlassLayer/GlassLayer.d.ts +5 -0
  82. package/lib/typescript/src/components/ui/GlassLayer/GlassLayer.d.ts.map +1 -0
  83. package/lib/typescript/src/components/ui/GlassLayer/GlassLayer.web.d.ts +5 -0
  84. package/lib/typescript/src/components/ui/GlassLayer/GlassLayer.web.d.ts.map +1 -0
  85. package/lib/typescript/src/components/ui/GlassLayer/constants.d.ts +6 -0
  86. package/lib/typescript/src/components/ui/GlassLayer/constants.d.ts.map +1 -0
  87. package/lib/typescript/src/components/ui/GlassLayer/index.d.ts +3 -0
  88. package/lib/typescript/src/components/ui/{BorderBeam → GlassLayer}/index.d.ts.map +1 -1
  89. package/lib/typescript/src/components/ui/GlassLayer/types.d.ts +13 -0
  90. package/lib/typescript/src/components/ui/GlassLayer/types.d.ts.map +1 -0
  91. package/lib/typescript/src/components/ui/IconButton/IconButton.d.ts +1 -1
  92. package/lib/typescript/src/components/ui/IconButton/IconButton.d.ts.map +1 -1
  93. package/lib/typescript/src/components/ui/IconButton/types.d.ts +4 -0
  94. package/lib/typescript/src/components/ui/IconButton/types.d.ts.map +1 -1
  95. package/lib/typescript/src/components/ui/ImagePreview/ImagePreview.d.ts +3 -0
  96. package/lib/typescript/src/components/ui/ImagePreview/ImagePreview.d.ts.map +1 -0
  97. package/lib/typescript/src/components/ui/ImagePreview/constants.d.ts +3 -0
  98. package/lib/typescript/src/components/ui/ImagePreview/constants.d.ts.map +1 -0
  99. package/lib/typescript/src/components/ui/ImagePreview/index.d.ts +3 -0
  100. package/lib/typescript/src/components/ui/ImagePreview/index.d.ts.map +1 -0
  101. package/lib/typescript/src/components/ui/ImagePreview/styles.d.ts +56 -0
  102. package/lib/typescript/src/components/ui/ImagePreview/styles.d.ts.map +1 -0
  103. package/lib/typescript/src/components/ui/ImagePreview/types.d.ts +33 -0
  104. package/lib/typescript/src/components/ui/ImagePreview/types.d.ts.map +1 -0
  105. package/lib/typescript/src/components/ui/Sheet/Sheet.d.ts +4 -0
  106. package/lib/typescript/src/components/ui/Sheet/Sheet.d.ts.map +1 -0
  107. package/lib/typescript/src/components/ui/Sheet/index.d.ts +3 -0
  108. package/lib/typescript/src/components/ui/Sheet/index.d.ts.map +1 -0
  109. package/lib/typescript/src/components/ui/Sheet/styles.d.ts +38 -0
  110. package/lib/typescript/src/components/ui/Sheet/styles.d.ts.map +1 -0
  111. package/lib/typescript/src/components/ui/Sheet/types.d.ts +13 -0
  112. package/lib/typescript/src/components/ui/Sheet/types.d.ts.map +1 -0
  113. package/lib/typescript/src/components/ui/SmsField/SmsField.d.ts +4 -0
  114. package/lib/typescript/src/components/ui/SmsField/SmsField.d.ts.map +1 -0
  115. package/lib/typescript/src/components/ui/SmsField/index.d.ts +3 -0
  116. package/lib/typescript/src/components/ui/SmsField/index.d.ts.map +1 -0
  117. package/lib/typescript/src/components/ui/SmsField/types.d.ts +12 -0
  118. package/lib/typescript/src/components/ui/SmsField/types.d.ts.map +1 -0
  119. package/lib/typescript/src/components/ui/TextEntryContent/TextEntryContent.d.ts +4 -0
  120. package/lib/typescript/src/components/ui/TextEntryContent/TextEntryContent.d.ts.map +1 -0
  121. package/lib/typescript/src/components/ui/TextEntryContent/index.d.ts +3 -0
  122. package/lib/typescript/src/components/ui/TextEntryContent/index.d.ts.map +1 -0
  123. package/lib/typescript/src/components/ui/TextEntryContent/styles.d.ts +26 -0
  124. package/lib/typescript/src/components/ui/TextEntryContent/styles.d.ts.map +1 -0
  125. package/lib/typescript/src/components/ui/TextEntryContent/types.d.ts +18 -0
  126. package/lib/typescript/src/components/ui/TextEntryContent/types.d.ts.map +1 -0
  127. package/lib/typescript/src/components/ui/index.d.ts +12 -4
  128. package/lib/typescript/src/components/ui/index.d.ts.map +1 -1
  129. package/lib/typescript/src/icons/data.d.ts +1 -1
  130. package/lib/typescript/src/icons/data.d.ts.map +1 -1
  131. package/lib/typescript/src/theme/colors.d.ts +2 -5
  132. package/lib/typescript/src/theme/colors.d.ts.map +1 -1
  133. package/lib/typescript/src/theme/index.d.ts +0 -2
  134. package/lib/typescript/src/theme/index.d.ts.map +1 -1
  135. package/lib/typescript/type-tests/content-api.d.ts +2 -0
  136. package/lib/typescript/type-tests/content-api.d.ts.map +1 -0
  137. package/package.json +3 -3
  138. package/src/components/business/GlassStats/GlassStats.tsx +4 -6
  139. package/src/components/business/GlassStats/styles.ts +3 -1
  140. package/src/components/ui/ActionMenuContent/ActionMenuContent.tsx +118 -0
  141. package/src/components/ui/ActionMenuContent/index.ts +6 -0
  142. package/src/components/ui/ActionMenuContent/styles.ts +47 -0
  143. package/src/components/ui/ActionMenuContent/types.ts +27 -0
  144. package/src/components/ui/Button/ButtonBase.tsx +11 -2
  145. package/src/components/ui/GlassLayer/GlassLayer.tsx +42 -0
  146. package/src/components/ui/GlassLayer/GlassLayer.web.tsx +37 -0
  147. package/src/components/ui/GlassLayer/constants.ts +2 -0
  148. package/src/components/ui/GlassLayer/index.ts +2 -0
  149. package/src/components/ui/GlassLayer/types.ts +14 -0
  150. package/src/components/ui/IconButton/IconButton.tsx +9 -1
  151. package/src/components/ui/IconButton/types.ts +4 -0
  152. package/src/components/ui/ImagePreview/ImagePreview.tsx +243 -0
  153. package/src/components/ui/ImagePreview/constants.ts +2 -0
  154. package/src/components/ui/ImagePreview/index.ts +6 -0
  155. package/src/components/ui/ImagePreview/styles.ts +48 -0
  156. package/src/components/ui/ImagePreview/types.ts +37 -0
  157. package/src/components/ui/Sheet/Sheet.tsx +43 -0
  158. package/src/components/ui/Sheet/index.ts +2 -0
  159. package/src/components/ui/Sheet/styles.ts +40 -0
  160. package/src/components/ui/Sheet/types.ts +14 -0
  161. package/src/components/ui/SmsField/SmsField.tsx +40 -0
  162. package/src/components/ui/SmsField/index.ts +2 -0
  163. package/src/components/ui/SmsField/types.ts +15 -0
  164. package/src/components/ui/TextEntryContent/TextEntryContent.tsx +56 -0
  165. package/src/components/ui/TextEntryContent/index.ts +2 -0
  166. package/src/components/ui/TextEntryContent/styles.ts +17 -0
  167. package/src/components/ui/TextEntryContent/types.ts +18 -0
  168. package/src/components/ui/index.ts +20 -4
  169. package/src/icons/data.ts +134 -0
  170. package/src/theme/colors.ts +4 -7
  171. package/src/theme/index.ts +0 -3
  172. package/lib/module/components/ui/BlurLayer/BlurLayer.js +0 -45
  173. package/lib/module/components/ui/BlurLayer/BlurLayer.js.map +0 -1
  174. package/lib/module/components/ui/BlurLayer/BlurLayer.web.js +0 -46
  175. package/lib/module/components/ui/BlurLayer/BlurLayer.web.js.map +0 -1
  176. package/lib/module/components/ui/BlurLayer/index.js +0 -4
  177. package/lib/module/components/ui/BlurLayer/index.js.map +0 -1
  178. package/lib/module/components/ui/BlurLayer/types.js.map +0 -1
  179. package/lib/module/components/ui/BorderBeam/BorderBeam.js +0 -102
  180. package/lib/module/components/ui/BorderBeam/BorderBeam.js.map +0 -1
  181. package/lib/module/components/ui/BorderBeam/BorderBeam.web.js +0 -108
  182. package/lib/module/components/ui/BorderBeam/BorderBeam.web.js.map +0 -1
  183. package/lib/module/components/ui/BorderBeam/index.js +0 -4
  184. package/lib/module/components/ui/BorderBeam/index.js.map +0 -1
  185. package/lib/module/components/ui/BorderBeam/normalizeBorderBeam.js +0 -23
  186. package/lib/module/components/ui/BorderBeam/normalizeBorderBeam.js.map +0 -1
  187. package/lib/module/components/ui/BorderBeam/shared.js +0 -54
  188. package/lib/module/components/ui/BorderBeam/shared.js.map +0 -1
  189. package/lib/module/components/ui/BorderBeam/styles.js +0 -18
  190. package/lib/module/components/ui/BorderBeam/styles.js.map +0 -1
  191. package/lib/module/theme/blur.js +0 -12
  192. package/lib/module/theme/blur.js.map +0 -1
  193. package/lib/typescript/src/components/ui/BlurLayer/BlurLayer.d.ts +0 -10
  194. package/lib/typescript/src/components/ui/BlurLayer/BlurLayer.d.ts.map +0 -1
  195. package/lib/typescript/src/components/ui/BlurLayer/BlurLayer.web.d.ts +0 -12
  196. package/lib/typescript/src/components/ui/BlurLayer/BlurLayer.web.d.ts.map +0 -1
  197. package/lib/typescript/src/components/ui/BlurLayer/index.d.ts +0 -3
  198. package/lib/typescript/src/components/ui/BlurLayer/index.d.ts.map +0 -1
  199. package/lib/typescript/src/components/ui/BlurLayer/types.d.ts +0 -15
  200. package/lib/typescript/src/components/ui/BlurLayer/types.d.ts.map +0 -1
  201. package/lib/typescript/src/components/ui/BorderBeam/BorderBeam.d.ts +0 -10
  202. package/lib/typescript/src/components/ui/BorderBeam/BorderBeam.d.ts.map +0 -1
  203. package/lib/typescript/src/components/ui/BorderBeam/BorderBeam.web.d.ts +0 -5
  204. package/lib/typescript/src/components/ui/BorderBeam/BorderBeam.web.d.ts.map +0 -1
  205. package/lib/typescript/src/components/ui/BorderBeam/index.d.ts +0 -3
  206. package/lib/typescript/src/components/ui/BorderBeam/normalizeBorderBeam.d.ts +0 -16
  207. package/lib/typescript/src/components/ui/BorderBeam/normalizeBorderBeam.d.ts.map +0 -1
  208. package/lib/typescript/src/components/ui/BorderBeam/shared.d.ts +0 -28
  209. package/lib/typescript/src/components/ui/BorderBeam/shared.d.ts.map +0 -1
  210. package/lib/typescript/src/components/ui/BorderBeam/styles.d.ts +0 -15
  211. package/lib/typescript/src/components/ui/BorderBeam/styles.d.ts.map +0 -1
  212. package/lib/typescript/src/components/ui/BorderBeam/types.d.ts +0 -23
  213. package/lib/typescript/src/components/ui/BorderBeam/types.d.ts.map +0 -1
  214. package/lib/typescript/src/theme/blur.d.ts +0 -11
  215. package/lib/typescript/src/theme/blur.d.ts.map +0 -1
  216. package/src/components/ui/BlurLayer/BlurLayer.tsx +0 -47
  217. package/src/components/ui/BlurLayer/BlurLayer.web.tsx +0 -46
  218. package/src/components/ui/BlurLayer/index.ts +0 -2
  219. package/src/components/ui/BlurLayer/types.ts +0 -15
  220. package/src/components/ui/BorderBeam/BorderBeam.tsx +0 -130
  221. package/src/components/ui/BorderBeam/BorderBeam.web.tsx +0 -127
  222. package/src/components/ui/BorderBeam/index.ts +0 -2
  223. package/src/components/ui/BorderBeam/normalizeBorderBeam.ts +0 -58
  224. package/src/components/ui/BorderBeam/shared.ts +0 -47
  225. package/src/components/ui/BorderBeam/styles.ts +0 -16
  226. package/src/components/ui/BorderBeam/types.ts +0 -23
  227. package/src/theme/blur.ts +0 -11
  228. /package/lib/module/components/ui/{BlurLayer → ActionMenuContent}/types.js +0 -0
  229. /package/lib/module/components/ui/{BorderBeam → GlassLayer}/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,6 @@
1
+ export { ActionMenuContent } from './ActionMenuContent';
2
+ export type {
3
+ ActionMenuAction,
4
+ ActionMenuConfirmation,
5
+ ActionMenuContentProps,
6
+ } from './types';
@@ -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(() => sizingFor(size), [size]);
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="small" color={palette.fg} />
140
+ <ActivityIndicator size={loadingSize ?? 'small'} color={palette.fg} />
132
141
  ) : (
133
142
  children({ sizing, palette })
134
143
  )}
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import {
4
+ LiquidGlassView,
5
+ isLiquidGlassSupported,
6
+ } from '@callstack/liquid-glass';
7
+ import { useColors, useTheme } from '../../../theme';
8
+ import type { GlassLayerProps } from './types';
9
+
10
+ /** 装饰玻璃层;不支持原生材质的平台使用主题半透明背景。 */
11
+ export function GlassLayer({
12
+ effect = 'regular',
13
+ tintColor,
14
+ style,
15
+ testID,
16
+ }: GlassLayerProps): React.JSX.Element {
17
+ const c = useColors();
18
+ const { scheme } = useTheme();
19
+ const resolvedTint =
20
+ tintColor ?? (effect === 'clear' ? c.glassTintLight : c.sheetBackdrop);
21
+
22
+ return (
23
+ <LiquidGlassView
24
+ // 支持标记也可能因宿主设计兼容设置关闭,必须显式禁用原生材质。
25
+ effect={isLiquidGlassSupported ? effect : 'none'}
26
+ tintColor={resolvedTint}
27
+ colorScheme={scheme}
28
+ // 纯装饰层无需材质切换动画,也不引入减少动态效果的额外状态。
29
+ animated={false}
30
+ style={[
31
+ StyleSheet.absoluteFill,
32
+ style,
33
+ !isLiquidGlassSupported && { backgroundColor: resolvedTint },
34
+ ]}
35
+ pointerEvents="none"
36
+ accessible={false}
37
+ accessibilityElementsHidden
38
+ importantForAccessibility="no-hide-descendants"
39
+ testID={testID}
40
+ />
41
+ );
42
+ }
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import { StyleSheet, View, type ViewStyle } from 'react-native';
3
+ import { useColors, useTheme } from '../../../theme';
4
+ import { webGlassBlur } from './constants';
5
+ import type { GlassLayerProps } from './types';
6
+
7
+ /** Web 保留 CSS 玻璃近似效果,不加载原生 Liquid Glass 模块。 */
8
+ export function GlassLayer({
9
+ effect = 'regular',
10
+ tintColor,
11
+ style,
12
+ testID,
13
+ }: GlassLayerProps): React.JSX.Element {
14
+ const c = useColors();
15
+ const { scheme } = useTheme();
16
+ const resolvedTint =
17
+ tintColor ?? (effect === 'clear' ? c.glassTintLight : c.sheetBackdrop);
18
+ // RN 类型不含 CSS backdrop-filter。保留原有饱和度和暗色透明度近似值。
19
+ const webGlassStyle: ViewStyle &
20
+ Pick<React.CSSProperties, 'backdropFilter' | 'WebkitBackdropFilter'> = {
21
+ backdropFilter: `blur(${webGlassBlur[effect]}px) saturate(180%)`,
22
+ WebkitBackdropFilter: `blur(${webGlassBlur[effect]}px) saturate(180%)`,
23
+ backgroundColor: resolvedTint,
24
+ opacity: scheme === 'dark' ? 0.96 : 1,
25
+ };
26
+
27
+ return (
28
+ <View
29
+ style={[StyleSheet.absoluteFill, style, webGlassStyle]}
30
+ pointerEvents="none"
31
+ accessible={false}
32
+ accessibilityElementsHidden
33
+ importantForAccessibility="no-hide-descendants"
34
+ testID={testID}
35
+ />
36
+ );
37
+ }
@@ -0,0 +1,2 @@
1
+ /** 仅用于 Web CSS 近似效果的像素值,不表示原生材质的模糊半径。 */
2
+ export const webGlassBlur = { clear: 10, regular: 40 } as const;
@@ -0,0 +1,2 @@
1
+ export { GlassLayer } from './GlassLayer';
2
+ export type { GlassEffect, GlassLayerProps } from './types';
@@ -0,0 +1,14 @@
1
+ import type { ColorValue, StyleProp, ViewStyle } from 'react-native';
2
+
3
+ export type GlassEffect = 'clear' | 'regular';
4
+
5
+ export interface GlassLayerProps {
6
+ /** 玻璃材质,默认 regular;clear 更通透。Web 使用对应的 CSS 近似效果。 */
7
+ effect?: GlassEffect;
8
+ /** 默认 clear 使用 glassTintLight,regular 使用 sheetBackdrop;降级时作为背景色。 */
9
+ tintColor?: ColorValue;
10
+ /** 布局、定位与圆角;默认 absoluteFill,材质颜色通过 tintColor 设置。 */
11
+ style?: StyleProp<ViewStyle>;
12
+ /** E2E / 测试定位。 */
13
+ testID?: string;
14
+ }
@@ -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 name={icon} size={sizing.fs + 4} color={color ?? palette.fg} />
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,2 @@
1
+ export const IMAGE_UNAVAILABLE = '图片暂不可预览';
2
+ export const DELETE_LABEL = '删除当前照片';
@@ -0,0 +1,6 @@
1
+ export { ImagePreview } from './ImagePreview';
2
+ export type {
3
+ ImagePreviewItem,
4
+ ImagePreviewProps,
5
+ ImagePreviewHandle,
6
+ } from './types';
@@ -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
+ });