zkit-ui 1.0.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 (267) hide show
  1. package/README.md +267 -0
  2. package/dist/ComponentLibProvider.d.ts +15 -0
  3. package/dist/ComponentLibProvider.d.ts.map +1 -0
  4. package/dist/ComponentLibProvider.js +18 -0
  5. package/dist/assets/animations/spinner.json +1 -0
  6. package/dist/assets/icons/check.svg +4 -0
  7. package/dist/assets/icons/toast/error.webp +0 -0
  8. package/dist/assets/icons/toast/success.webp +0 -0
  9. package/dist/assets/icons/toast/warning.webp +0 -0
  10. package/dist/config.d.ts +16 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +42 -0
  13. package/dist/i18n/I18nProvider.d.ts +11 -0
  14. package/dist/i18n/I18nProvider.d.ts.map +1 -0
  15. package/dist/i18n/I18nProvider.js +84 -0
  16. package/dist/i18n/locale.d.ts +7 -0
  17. package/dist/i18n/locale.d.ts.map +1 -0
  18. package/dist/i18n/locale.js +87 -0
  19. package/dist/i18n/locales/de.d.ts +51 -0
  20. package/dist/i18n/locales/de.d.ts.map +1 -0
  21. package/dist/i18n/locales/de.js +62 -0
  22. package/dist/i18n/locales/en-US.d.ts +51 -0
  23. package/dist/i18n/locales/en-US.d.ts.map +1 -0
  24. package/dist/i18n/locales/en-US.js +62 -0
  25. package/dist/i18n/locales/index.d.ts +9 -0
  26. package/dist/i18n/locales/index.d.ts.map +1 -0
  27. package/dist/i18n/locales/index.js +23 -0
  28. package/dist/i18n/locales/ja.d.ts +51 -0
  29. package/dist/i18n/locales/ja.d.ts.map +1 -0
  30. package/dist/i18n/locales/ja.js +62 -0
  31. package/dist/i18n/locales/zh-CN.d.ts +51 -0
  32. package/dist/i18n/locales/zh-CN.d.ts.map +1 -0
  33. package/dist/i18n/locales/zh-CN.js +62 -0
  34. package/dist/i18n/locales/zh-TW.d.ts +51 -0
  35. package/dist/i18n/locales/zh-TW.d.ts.map +1 -0
  36. package/dist/i18n/locales/zh-TW.js +62 -0
  37. package/dist/i18n/types.d.ts +11 -0
  38. package/dist/i18n/types.d.ts.map +1 -0
  39. package/dist/i18n/types.js +2 -0
  40. package/dist/i18n/useI18n.d.ts +2 -0
  41. package/dist/i18n/useI18n.d.ts.map +1 -0
  42. package/dist/i18n/useI18n.js +41 -0
  43. package/dist/index.d.ts +57 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +90 -0
  46. package/dist/services/ActionDialogService/ActionDialog.d.ts +4 -0
  47. package/dist/services/ActionDialogService/ActionDialog.d.ts.map +1 -0
  48. package/dist/services/ActionDialogService/ActionDialog.js +531 -0
  49. package/dist/services/ActionDialogService/index.d.ts +4 -0
  50. package/dist/services/ActionDialogService/index.d.ts.map +1 -0
  51. package/dist/services/ActionDialogService/index.js +8 -0
  52. package/dist/services/ActionDialogService/service.d.ts +7 -0
  53. package/dist/services/ActionDialogService/service.d.ts.map +1 -0
  54. package/dist/services/ActionDialogService/service.js +309 -0
  55. package/dist/services/ActionDialogService/shared.d.ts +17 -0
  56. package/dist/services/ActionDialogService/shared.d.ts.map +1 -0
  57. package/dist/services/ActionDialogService/shared.js +167 -0
  58. package/dist/services/ActionDialogService/types.d.ts +160 -0
  59. package/dist/services/ActionDialogService/types.d.ts.map +1 -0
  60. package/dist/services/ActionDialogService/types.js +2 -0
  61. package/dist/services/CardToastService/index.d.ts +115 -0
  62. package/dist/services/CardToastService/index.d.ts.map +1 -0
  63. package/dist/services/CardToastService/index.js +899 -0
  64. package/dist/services/ImagePreviewService/index.d.ts +179 -0
  65. package/dist/services/ImagePreviewService/index.d.ts.map +1 -0
  66. package/dist/services/ImagePreviewService/index.js +1383 -0
  67. package/dist/services/LoadingService/index.d.ts +146 -0
  68. package/dist/services/LoadingService/index.d.ts.map +1 -0
  69. package/dist/services/LoadingService/index.js +827 -0
  70. package/dist/services/PermissionPurposeDialogService/index.d.ts +99 -0
  71. package/dist/services/PermissionPurposeDialogService/index.d.ts.map +1 -0
  72. package/dist/services/PermissionPurposeDialogService/index.js +360 -0
  73. package/dist/services/PickerService/index.d.ts +128 -0
  74. package/dist/services/PickerService/index.d.ts.map +1 -0
  75. package/dist/services/PickerService/index.js +250 -0
  76. package/dist/theme/ThemeProvider.d.ts +15 -0
  77. package/dist/theme/ThemeProvider.d.ts.map +1 -0
  78. package/dist/theme/ThemeProvider.js +61 -0
  79. package/dist/theme/defaultTheme.d.ts +3 -0
  80. package/dist/theme/defaultTheme.d.ts.map +1 -0
  81. package/dist/theme/defaultTheme.js +28 -0
  82. package/dist/theme/mergeTheme.d.ts +3 -0
  83. package/dist/theme/mergeTheme.d.ts.map +1 -0
  84. package/dist/theme/mergeTheme.js +19 -0
  85. package/dist/theme/types.d.ts +17 -0
  86. package/dist/theme/types.d.ts.map +1 -0
  87. package/dist/theme/types.js +2 -0
  88. package/dist/theme/useTheme.d.ts +2 -0
  89. package/dist/theme/useTheme.d.ts.map +1 -0
  90. package/dist/theme/useTheme.js +46 -0
  91. package/dist/ui/Accordion/index.d.ts +99 -0
  92. package/dist/ui/Accordion/index.d.ts.map +1 -0
  93. package/dist/ui/Accordion/index.js +777 -0
  94. package/dist/ui/AddressCascader/index.d.ts +58 -0
  95. package/dist/ui/AddressCascader/index.d.ts.map +1 -0
  96. package/dist/ui/AddressCascader/index.js +710 -0
  97. package/dist/ui/BetweenTime/index.d.ts +82 -0
  98. package/dist/ui/BetweenTime/index.d.ts.map +1 -0
  99. package/dist/ui/BetweenTime/index.js +702 -0
  100. package/dist/ui/Button/index.d.ts +93 -0
  101. package/dist/ui/Button/index.d.ts.map +1 -0
  102. package/dist/ui/Button/index.js +832 -0
  103. package/dist/ui/Checkbox/index.d.ts +103 -0
  104. package/dist/ui/Checkbox/index.d.ts.map +1 -0
  105. package/dist/ui/Checkbox/index.js +694 -0
  106. package/dist/ui/DatePicker/index.d.ts +5 -0
  107. package/dist/ui/DatePicker/index.d.ts.map +1 -0
  108. package/dist/ui/DatePicker/index.js +157 -0
  109. package/dist/ui/DatePicker/types.d.ts +99 -0
  110. package/dist/ui/DatePicker/types.d.ts.map +1 -0
  111. package/dist/ui/DatePicker/types.js +2 -0
  112. package/dist/ui/DatePicker/utils.d.ts +32 -0
  113. package/dist/ui/DatePicker/utils.d.ts.map +1 -0
  114. package/dist/ui/DatePicker/utils.js +316 -0
  115. package/dist/ui/LinkedScroll/index.d.ts +78 -0
  116. package/dist/ui/LinkedScroll/index.d.ts.map +1 -0
  117. package/dist/ui/LinkedScroll/index.js +470 -0
  118. package/dist/ui/LoadingSpinner/index.d.ts +8 -0
  119. package/dist/ui/LoadingSpinner/index.d.ts.map +1 -0
  120. package/dist/ui/LoadingSpinner/index.js +122 -0
  121. package/dist/ui/Picker/actionBar.d.ts +14 -0
  122. package/dist/ui/Picker/actionBar.d.ts.map +1 -0
  123. package/dist/ui/Picker/actionBar.js +28 -0
  124. package/dist/ui/Picker/index.d.ts +5 -0
  125. package/dist/ui/Picker/index.d.ts.map +1 -0
  126. package/dist/ui/Picker/index.js +412 -0
  127. package/dist/ui/Picker/types.d.ts +80 -0
  128. package/dist/ui/Picker/types.d.ts.map +1 -0
  129. package/dist/ui/Picker/types.js +2 -0
  130. package/dist/ui/Picker/utils.d.ts +27 -0
  131. package/dist/ui/Picker/utils.d.ts.map +1 -0
  132. package/dist/ui/Picker/utils.js +182 -0
  133. package/dist/ui/Radio/index.d.ts +95 -0
  134. package/dist/ui/Radio/index.d.ts.map +1 -0
  135. package/dist/ui/Radio/index.js +633 -0
  136. package/dist/ui/Sheet/index.d.ts +12 -0
  137. package/dist/ui/Sheet/index.d.ts.map +1 -0
  138. package/dist/ui/Sheet/index.js +832 -0
  139. package/dist/ui/Sheet/nativeBottom.d.ts +52 -0
  140. package/dist/ui/Sheet/nativeBottom.d.ts.map +1 -0
  141. package/dist/ui/Sheet/nativeBottom.js +442 -0
  142. package/dist/ui/Sheet/nativeBottom.web.d.ts +18 -0
  143. package/dist/ui/Sheet/nativeBottom.web.d.ts.map +1 -0
  144. package/dist/ui/Sheet/nativeBottom.web.js +46 -0
  145. package/dist/ui/Sheet/slots.d.ts +5 -0
  146. package/dist/ui/Sheet/slots.d.ts.map +1 -0
  147. package/dist/ui/Sheet/slots.js +40 -0
  148. package/dist/ui/Sheet/types.d.ts +131 -0
  149. package/dist/ui/Sheet/types.d.ts.map +1 -0
  150. package/dist/ui/Sheet/types.js +2 -0
  151. package/dist/ui/SliderCaptcha/index.d.ts +57 -0
  152. package/dist/ui/SliderCaptcha/index.d.ts.map +1 -0
  153. package/dist/ui/SliderCaptcha/index.js +586 -0
  154. package/dist/ui/Switch/index.d.ts +72 -0
  155. package/dist/ui/Switch/index.d.ts.map +1 -0
  156. package/dist/ui/Switch/index.js +671 -0
  157. package/dist/ui/Text/index.d.ts +41 -0
  158. package/dist/ui/Text/index.d.ts.map +1 -0
  159. package/dist/ui/Text/index.js +325 -0
  160. package/dist/ui/TextInput/TextInputPrimitive.d.ts +8 -0
  161. package/dist/ui/TextInput/TextInputPrimitive.d.ts.map +1 -0
  162. package/dist/ui/TextInput/TextInputPrimitive.js +54 -0
  163. package/dist/ui/TextInput/index.d.ts +89 -0
  164. package/dist/ui/TextInput/index.d.ts.map +1 -0
  165. package/dist/ui/TextInput/index.js +545 -0
  166. package/dist/ui/WheelColumn/ZKitWheelPickerNativeComponent.d.ts +39 -0
  167. package/dist/ui/WheelColumn/ZKitWheelPickerNativeComponent.d.ts.map +1 -0
  168. package/dist/ui/WheelColumn/ZKitWheelPickerNativeComponent.js +60 -0
  169. package/dist/ui/WheelColumn/constants.d.ts +2 -0
  170. package/dist/ui/WheelColumn/constants.d.ts.map +1 -0
  171. package/dist/ui/WheelColumn/constants.js +4 -0
  172. package/dist/ui/WheelColumn/index.d.ts +59 -0
  173. package/dist/ui/WheelColumn/index.d.ts.map +1 -0
  174. package/dist/ui/WheelColumn/index.js +547 -0
  175. package/ios/ZKitWheelPicker.h +18 -0
  176. package/ios/ZKitWheelPicker.mm +230 -0
  177. package/ios/ZKitWheelPickerLabel.h +10 -0
  178. package/ios/ZKitWheelPickerLabel.mm +31 -0
  179. package/ios/ZKitWheelPickerManager.h +5 -0
  180. package/ios/ZKitWheelPickerManager.mm +53 -0
  181. package/package.json +75 -0
  182. package/react-native.config.js +10 -0
  183. package/src/ComponentLibProvider.tsx +56 -0
  184. package/src/assets/animations/spinner.json +1 -0
  185. package/src/assets/icons/check.svg +4 -0
  186. package/src/assets/icons/toast/error.webp +0 -0
  187. package/src/assets/icons/toast/success.webp +0 -0
  188. package/src/assets/icons/toast/warning.webp +0 -0
  189. package/src/config.ts +65 -0
  190. package/src/i18n/I18nProvider.tsx +69 -0
  191. package/src/i18n/locale.ts +108 -0
  192. package/src/i18n/locales/de.ts +67 -0
  193. package/src/i18n/locales/en-US.ts +67 -0
  194. package/src/i18n/locales/index.ts +19 -0
  195. package/src/i18n/locales/ja.ts +67 -0
  196. package/src/i18n/locales/zh-CN.ts +67 -0
  197. package/src/i18n/locales/zh-TW.ts +67 -0
  198. package/src/i18n/types.ts +21 -0
  199. package/src/i18n/useI18n.ts +7 -0
  200. package/src/index.ts +395 -0
  201. package/src/services/ActionDialogService/ActionDialog.tsx +756 -0
  202. package/src/services/ActionDialogService/README.md +146 -0
  203. package/src/services/ActionDialogService/index.tsx +39 -0
  204. package/src/services/ActionDialogService/service.tsx +355 -0
  205. package/src/services/ActionDialogService/shared.ts +204 -0
  206. package/src/services/ActionDialogService/types.ts +197 -0
  207. package/src/services/CardToastService/README.md +128 -0
  208. package/src/services/CardToastService/index.tsx +1284 -0
  209. package/src/services/ImagePreviewService/README.md +67 -0
  210. package/src/services/ImagePreviewService/index.tsx +2148 -0
  211. package/src/services/LoadingService/README.md +128 -0
  212. package/src/services/LoadingService/index.tsx +1113 -0
  213. package/src/services/PermissionPurposeDialogService/README.md +80 -0
  214. package/src/services/PermissionPurposeDialogService/index.tsx +449 -0
  215. package/src/services/PickerService/README.md +154 -0
  216. package/src/services/PickerService/index.tsx +410 -0
  217. package/src/theme/ThemeProvider.tsx +40 -0
  218. package/src/theme/defaultTheme.ts +30 -0
  219. package/src/theme/mergeTheme.ts +16 -0
  220. package/src/theme/types.ts +36 -0
  221. package/src/theme/useTheme.ts +10 -0
  222. package/src/types/svg.d.ts +9 -0
  223. package/src/types/vant-area-data.d.ts +19 -0
  224. package/src/ui/Accordion/README.md +67 -0
  225. package/src/ui/Accordion/index.tsx +1221 -0
  226. package/src/ui/AddressCascader/README.md +144 -0
  227. package/src/ui/AddressCascader/index.tsx +1084 -0
  228. package/src/ui/BetweenTime/README.md +490 -0
  229. package/src/ui/BetweenTime/index.tsx +1001 -0
  230. package/src/ui/Button/README.md +186 -0
  231. package/src/ui/Button/index.tsx +1356 -0
  232. package/src/ui/Checkbox/README.md +211 -0
  233. package/src/ui/Checkbox/index.tsx +1231 -0
  234. package/src/ui/DatePicker/README.md +117 -0
  235. package/src/ui/DatePicker/index.tsx +295 -0
  236. package/src/ui/DatePicker/types.ts +115 -0
  237. package/src/ui/DatePicker/utils.ts +405 -0
  238. package/src/ui/LinkedScroll/README.md +55 -0
  239. package/src/ui/LinkedScroll/index.tsx +809 -0
  240. package/src/ui/LoadingSpinner/index.tsx +104 -0
  241. package/src/ui/Picker/README.md +190 -0
  242. package/src/ui/Picker/actionBar.tsx +70 -0
  243. package/src/ui/Picker/index.tsx +694 -0
  244. package/src/ui/Picker/types.ts +94 -0
  245. package/src/ui/Picker/utils.ts +239 -0
  246. package/src/ui/Radio/README.md +207 -0
  247. package/src/ui/Radio/index.tsx +1117 -0
  248. package/src/ui/Sheet/README.md +82 -0
  249. package/src/ui/Sheet/index.tsx +1163 -0
  250. package/src/ui/Sheet/nativeBottom.tsx +683 -0
  251. package/src/ui/Sheet/nativeBottom.web.tsx +40 -0
  252. package/src/ui/Sheet/slots.tsx +75 -0
  253. package/src/ui/Sheet/types.ts +195 -0
  254. package/src/ui/SliderCaptcha/README.md +61 -0
  255. package/src/ui/SliderCaptcha/index.tsx +816 -0
  256. package/src/ui/Switch/README.md +143 -0
  257. package/src/ui/Switch/index.tsx +1092 -0
  258. package/src/ui/Text/README.md +157 -0
  259. package/src/ui/Text/index.tsx +450 -0
  260. package/src/ui/TextInput/README.md +197 -0
  261. package/src/ui/TextInput/TextInputPrimitive.tsx +44 -0
  262. package/src/ui/TextInput/index.tsx +942 -0
  263. package/src/ui/WheelColumn/README.md +104 -0
  264. package/src/ui/WheelColumn/ZKitWheelPickerNativeComponent.tsx +68 -0
  265. package/src/ui/WheelColumn/constants.ts +1 -0
  266. package/src/ui/WheelColumn/index.tsx +826 -0
  267. package/zkit-ui.podspec +19 -0
@@ -0,0 +1,756 @@
1
+ import * as React from 'react';
2
+ import {
3
+ BackHandler,
4
+ Dimensions,
5
+ Keyboard,
6
+ Modal,
7
+ Platform,
8
+ Pressable,
9
+ ScrollView,
10
+ StyleSheet,
11
+ View,
12
+ useWindowDimensions,
13
+ type KeyboardEvent,
14
+ } from 'react-native';
15
+ import Animated, {
16
+ Easing,
17
+ ReduceMotion,
18
+ cancelAnimation,
19
+ useAnimatedStyle,
20
+ useSharedValue,
21
+ withSpring,
22
+ withTiming,
23
+ } from 'react-native-reanimated';
24
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
25
+ import { scheduleOnRN } from 'react-native-worklets';
26
+ import { wp } from 'zkit-tools';
27
+ import { useI18n } from '../../i18n/useI18n';
28
+ import { useTheme } from '../../theme/useTheme';
29
+ import { Button } from '../../ui/Button/index';
30
+ import { Text } from '../../ui/Text';
31
+ import {
32
+ ACTION_DIALOG_DEFAULT_COLORS,
33
+ ACTION_DIALOG_DEFAULT_Z_INDEX,
34
+ createActionResult,
35
+ resolveActions,
36
+ resolveDismissOptions,
37
+ resolveFooterLayout,
38
+ resolveKeyboardOptions,
39
+ resolveLayout,
40
+ } from './shared';
41
+ import type {
42
+ ActionDialogActionContext,
43
+ ActionDialogDismissReason,
44
+ ActionDialogFooterRenderContext,
45
+ ActionDialogProps,
46
+ ActionDialogRef,
47
+ ActionDialogResolvedAction,
48
+ ActionDialogResult,
49
+ } from './types';
50
+
51
+ const ENTER_BACKDROP_TIMING = {
52
+ duration: 190,
53
+ easing: Easing.out(Easing.cubic),
54
+ reduceMotion: ReduceMotion.System,
55
+ } as const;
56
+
57
+ const EXIT_BACKDROP_TIMING = {
58
+ duration: 130,
59
+ easing: Easing.in(Easing.cubic),
60
+ reduceMotion: ReduceMotion.System,
61
+ } as const;
62
+
63
+ const ENTER_CARD_OPACITY_TIMING = {
64
+ duration: 140,
65
+ easing: Easing.out(Easing.cubic),
66
+ reduceMotion: ReduceMotion.System,
67
+ } as const;
68
+
69
+ const ENTER_CARD_SCALE_TIMING = {
70
+ duration: 170,
71
+ easing: Easing.out(Easing.cubic),
72
+ reduceMotion: ReduceMotion.System,
73
+ } as const;
74
+
75
+ const EXIT_CARD_TIMING = {
76
+ duration: 100,
77
+ easing: Easing.in(Easing.cubic),
78
+ reduceMotion: ReduceMotion.System,
79
+ } as const;
80
+
81
+ const KEYBOARD_TIMING = {
82
+ duration: 260,
83
+ easing: Easing.out(Easing.cubic),
84
+ reduceMotion: ReduceMotion.System,
85
+ } as const;
86
+
87
+ const KEYBOARD_SPRING = {
88
+ damping: 28,
89
+ mass: 1,
90
+ overshootClamping: true,
91
+ stiffness: 220,
92
+ } as const;
93
+
94
+ const KEYBOARD_CLEARANCE = wp(12);
95
+ const CARD_ENTER_SCALE = 0.92;
96
+ const CARD_EXIT_SCALE = 0.985;
97
+ const BAR_FOOTER_HEIGHT = wp(52);
98
+ const COMPACT_ROW_ACTION_BUTTON_LAYOUT: React.ComponentProps<typeof Button>['layout'] = {
99
+ paddingHorizontal: wp(10),
100
+ };
101
+ const BAR_ACTION_BUTTON_LAYOUT: React.ComponentProps<typeof Button>['layout'] = {
102
+ height: BAR_FOOTER_HEIGHT,
103
+ minHeight: BAR_FOOTER_HEIGHT,
104
+ paddingVertical: 0,
105
+ };
106
+
107
+ function getKeyboardTopInWindow(event: KeyboardEvent) {
108
+ const windowHeight = Dimensions.get('window').height;
109
+ const screenHeight = Dimensions.get('screen').height;
110
+ const screenY = event.endCoordinates?.screenY;
111
+
112
+ if (typeof screenY === 'number' && Number.isFinite(screenY)) {
113
+ const windowOffset = Math.max(0, screenHeight - windowHeight);
114
+ return Math.min(windowHeight, Math.max(0, screenY - windowOffset));
115
+ }
116
+
117
+ const keyboardHeight = event.endCoordinates?.height;
118
+ if (typeof keyboardHeight === 'number' && Number.isFinite(keyboardHeight)) {
119
+ return Math.max(0, windowHeight - keyboardHeight);
120
+ }
121
+
122
+ return windowHeight;
123
+ }
124
+
125
+ function getStringText(value: React.ReactNode) {
126
+ return typeof value === 'string' ? value : undefined;
127
+ }
128
+
129
+ function getAccessibilityLabel(title: React.ReactNode, message: React.ReactNode, fallback: string) {
130
+ const titleText = getStringText(title);
131
+ const messageText = getStringText(message);
132
+ if (titleText && messageText) return `${titleText}, ${messageText}`;
133
+ return titleText ?? messageText ?? fallback;
134
+ }
135
+
136
+ const ActionDialogRoot = React.forwardRef<ActionDialogRef, ActionDialogProps>(function ActionDialog(
137
+ {
138
+ accessibilityLabel,
139
+ actions,
140
+ children,
141
+ colors,
142
+ defaultOpen = false,
143
+ disabled = false,
144
+ dismiss,
145
+ dismissible = true,
146
+ footer,
147
+ hostMode = 'modal',
148
+ keyboard,
149
+ labels,
150
+ layer,
151
+ layout,
152
+ message,
153
+ modalProps,
154
+ motion = 'fade',
155
+ onActionError,
156
+ onClose,
157
+ onDismissComplete,
158
+ onOpenChange,
159
+ open,
160
+ style,
161
+ testID,
162
+ title,
163
+ },
164
+ ref
165
+ ) {
166
+ const theme = useTheme();
167
+ const { t } = useI18n();
168
+ const dimensions = useWindowDimensions();
169
+ const insets = useSafeAreaInsets();
170
+ const isOpenControlled = open !== undefined;
171
+ const [innerOpen, setInnerOpen] = React.useState(defaultOpen);
172
+ const actualOpen = isOpenControlled ? !!open : innerOpen;
173
+ const [mounted, setMounted] = React.useState(actualOpen);
174
+ const dialogHeightRef = React.useRef(0);
175
+ const keyboardMetricsRef = React.useRef<{ top: number } | null>(null);
176
+ const closingRequestedRef = React.useRef(false);
177
+ const dismissCompletePendingRef = React.useRef(false);
178
+ const mountedRef = React.useRef(mounted);
179
+ const onDismissCompleteRef = React.useRef(onDismissComplete);
180
+ const openingFrameRef = React.useRef<number | null>(null);
181
+ const openRef = React.useRef(actualOpen);
182
+ mountedRef.current = mounted;
183
+ onDismissCompleteRef.current = onDismissComplete;
184
+ openRef.current = actualOpen;
185
+
186
+ const backdropProgress = useSharedValue(0);
187
+ const cardOpacity = useSharedValue(0);
188
+ const cardScale = useSharedValue(CARD_ENTER_SCALE);
189
+ const keyboardOffset = useSharedValue(0);
190
+
191
+ const resolvedLabels = React.useMemo(
192
+ () => ({
193
+ confirm: labels?.confirm ?? t('actionDialog.confirm'),
194
+ cancel: labels?.cancel ?? t('actionDialog.cancel'),
195
+ close: labels?.close ?? t('actionDialog.close'),
196
+ }),
197
+ [labels?.cancel, labels?.close, labels?.confirm, t]
198
+ );
199
+
200
+ const resolvedActions = React.useMemo(
201
+ () => resolveActions(actions, resolvedLabels),
202
+ [actions, resolvedLabels]
203
+ );
204
+ const resolvedFooterLayout = resolveFooterLayout(footer?.layout, resolvedActions);
205
+ const resolvedDismiss = resolveDismissOptions(dismissible, dismiss);
206
+ const resolvedKeyboard = resolveKeyboardOptions(keyboard);
207
+ const resolvedLayout = resolveLayout(layout);
208
+ const resolvedColors = {
209
+ backdrop: colors?.backdrop ?? ACTION_DIALOG_DEFAULT_COLORS.backdrop,
210
+ surface: colors?.surface || theme.colors.surface,
211
+ title: colors?.title || theme.colors.onSurface,
212
+ message: colors?.message || theme.colors.muted,
213
+ border: colors?.border || theme.colors.border,
214
+ };
215
+
216
+ const availableWidth = Math.max(0, dimensions.width - wp(32));
217
+ const dialogWidth = Math.min(wp(resolvedLayout.width), wp(resolvedLayout.maxWidth), availableWidth);
218
+ const maxDialogHeight = Math.max(
219
+ wp(180),
220
+ dimensions.height - insets.top - insets.bottom - wp(64)
221
+ );
222
+ const maxBodyHeight = Math.max(wp(88), maxDialogHeight - wp(96));
223
+
224
+ const finishDismiss = React.useCallback(() => {
225
+ if (!dismissCompletePendingRef.current) return;
226
+ dismissCompletePendingRef.current = false;
227
+ setMounted(false);
228
+ onDismissCompleteRef.current?.();
229
+ }, []);
230
+
231
+ React.useEffect(() => {
232
+ return () => {
233
+ if (openingFrameRef.current != null) {
234
+ cancelAnimationFrame(openingFrameRef.current);
235
+ openingFrameRef.current = null;
236
+ }
237
+ cancelAnimation(backdropProgress);
238
+ cancelAnimation(cardOpacity);
239
+ cancelAnimation(cardScale);
240
+ cancelAnimation(keyboardOffset);
241
+ };
242
+ }, [backdropProgress, cardOpacity, cardScale, keyboardOffset]);
243
+
244
+ React.useEffect(() => {
245
+ if (openingFrameRef.current != null) {
246
+ cancelAnimationFrame(openingFrameRef.current);
247
+ openingFrameRef.current = null;
248
+ }
249
+ cancelAnimation(backdropProgress);
250
+ cancelAnimation(cardOpacity);
251
+ cancelAnimation(cardScale);
252
+
253
+ if (actualOpen) {
254
+ closingRequestedRef.current = false;
255
+ dismissCompletePendingRef.current = false;
256
+ setMounted(true);
257
+ backdropProgress.value = 0;
258
+ cardOpacity.value = 0;
259
+ cardScale.value = motion === 'scale' ? CARD_ENTER_SCALE : 1;
260
+ openingFrameRef.current = requestAnimationFrame(() => {
261
+ openingFrameRef.current = null;
262
+ if (motion === 'none') {
263
+ backdropProgress.value = 1;
264
+ cardOpacity.value = 1;
265
+ cardScale.value = 1;
266
+ return;
267
+ }
268
+ backdropProgress.value = withTiming(1, ENTER_BACKDROP_TIMING);
269
+ cardOpacity.value = withTiming(1, ENTER_CARD_OPACITY_TIMING);
270
+ if (motion === 'scale') {
271
+ cardScale.value = withTiming(1, ENTER_CARD_SCALE_TIMING);
272
+ }
273
+ });
274
+ return;
275
+ }
276
+
277
+ if (!mountedRef.current) return;
278
+ dismissCompletePendingRef.current = true;
279
+ if (motion === 'none') {
280
+ backdropProgress.value = 0;
281
+ cardOpacity.value = 0;
282
+ cardScale.value = 1;
283
+ finishDismiss();
284
+ return;
285
+ }
286
+ backdropProgress.value = withTiming(0, EXIT_BACKDROP_TIMING);
287
+ cardScale.value = withTiming(motion === 'scale' ? CARD_EXIT_SCALE : 1, EXIT_CARD_TIMING);
288
+ cardOpacity.value = withTiming(0, EXIT_CARD_TIMING, (finished) => {
289
+ if (finished) {
290
+ scheduleOnRN(finishDismiss);
291
+ }
292
+ });
293
+ }, [actualOpen, backdropProgress, cardOpacity, cardScale, finishDismiss, motion]);
294
+
295
+ React.useEffect(() => {
296
+ if (actualOpen || !resolvedKeyboard.dismissOnClose) return;
297
+ try {
298
+ Keyboard.dismiss();
299
+ } catch {}
300
+ }, [actualOpen, resolvedKeyboard.dismissOnClose]);
301
+
302
+ const updateKeyboardOffset = React.useCallback(
303
+ (metrics: { top: number } | null) => {
304
+ keyboardMetricsRef.current = metrics;
305
+ if (!metrics || !resolvedKeyboard.avoid) {
306
+ keyboardOffset.value = withTiming(0, KEYBOARD_TIMING);
307
+ return;
308
+ }
309
+
310
+ const dialogHeight = dialogHeightRef.current;
311
+ if (dialogHeight <= 0) return;
312
+ const dialogBottom = (dimensions.height + dialogHeight) / 2;
313
+ const overlap = Math.max(0, dialogBottom + KEYBOARD_CLEARANCE - metrics.top);
314
+ keyboardOffset.value = withSpring(overlap, KEYBOARD_SPRING);
315
+ },
316
+ [dimensions.height, keyboardOffset, resolvedKeyboard.avoid]
317
+ );
318
+
319
+ React.useEffect(() => {
320
+ if (!mounted || !resolvedKeyboard.avoid) return undefined;
321
+
322
+ const handleKeyboardChange = (event: KeyboardEvent) => {
323
+ updateKeyboardOffset({ top: getKeyboardTopInWindow(event) });
324
+ };
325
+ const handleKeyboardHide = () => updateKeyboardOffset(null);
326
+
327
+ const subscriptions =
328
+ Platform.OS === 'ios'
329
+ ? [
330
+ Keyboard.addListener('keyboardWillChangeFrame', handleKeyboardChange),
331
+ Keyboard.addListener('keyboardWillHide', handleKeyboardHide),
332
+ ]
333
+ : [
334
+ Keyboard.addListener('keyboardDidShow', handleKeyboardChange),
335
+ Keyboard.addListener('keyboardDidHide', handleKeyboardHide),
336
+ ];
337
+
338
+ return () => {
339
+ subscriptions.forEach((subscription) => subscription.remove());
340
+ };
341
+ }, [mounted, resolvedKeyboard.avoid, updateKeyboardOffset]);
342
+
343
+ const requestClose = React.useCallback(
344
+ (result: ActionDialogResult) => {
345
+ if (!openRef.current || closingRequestedRef.current) return;
346
+ closingRequestedRef.current = true;
347
+
348
+ if (!isOpenControlled) setInnerOpen(false);
349
+ onClose?.(result);
350
+ onOpenChange?.(false, {
351
+ reason: result.type === 'action' ? 'action' : result.reason,
352
+ result,
353
+ });
354
+ },
355
+ [isOpenControlled, onClose, onOpenChange]
356
+ );
357
+
358
+ const dismissWithReason = React.useCallback(
359
+ (reason: ActionDialogDismissReason) => {
360
+ if (disabled) return;
361
+ requestClose({ type: 'dismiss', reason });
362
+ },
363
+ [disabled, requestClose]
364
+ );
365
+
366
+ const handleOverlayPress = React.useCallback(() => {
367
+ if (resolvedKeyboard.dismissOnOverlayPress) {
368
+ try {
369
+ Keyboard.dismiss();
370
+ } catch {}
371
+ }
372
+
373
+ if (resolvedDismiss.overlayPress) {
374
+ dismissWithReason('overlay');
375
+ }
376
+ }, [dismissWithReason, resolvedDismiss.overlayPress, resolvedKeyboard.dismissOnOverlayPress]);
377
+
378
+ const handleModalRequestClose = React.useCallback(() => {
379
+ if (resolvedDismiss.backPress) dismissWithReason('back');
380
+ }, [dismissWithReason, resolvedDismiss.backPress]);
381
+
382
+ React.useEffect(() => {
383
+ if (hostMode !== 'inline' || Platform.OS !== 'android' || !mounted || !actualOpen) return undefined;
384
+
385
+ const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
386
+ if (resolvedDismiss.backPress) dismissWithReason('back');
387
+ return true;
388
+ });
389
+
390
+ return () => subscription.remove();
391
+ }, [actualOpen, dismissWithReason, hostMode, mounted, resolvedDismiss.backPress]);
392
+
393
+ const pressAction = React.useCallback(
394
+ (key: string) => {
395
+ if (disabled) return;
396
+ const action = resolvedActions.find((item) => item.key === key);
397
+ if (!action || action.disabled || action.loading) return;
398
+
399
+ const actionResult = createActionResult(action);
400
+ let handled = false;
401
+ const context: ActionDialogActionContext = {
402
+ action,
403
+ close: () => {
404
+ if (handled) return;
405
+ handled = true;
406
+ requestClose(actionResult);
407
+ },
408
+ dismiss: () => {
409
+ if (handled) return;
410
+ handled = true;
411
+ requestClose({ type: 'dismiss', reason: 'api' });
412
+ },
413
+ };
414
+
415
+ try {
416
+ const handlerResult = action.onPress?.(context);
417
+ if (handled) return;
418
+ if (handlerResult !== false && action.closeOnPress) {
419
+ requestClose(actionResult);
420
+ }
421
+ } catch (error) {
422
+ onActionError?.(error, action);
423
+ if (!onActionError && typeof __DEV__ !== 'undefined' && __DEV__) {
424
+ console.warn('[actionDialog] action onPress failed', error);
425
+ }
426
+ }
427
+ },
428
+ [disabled, onActionError, requestClose, resolvedActions]
429
+ );
430
+
431
+ React.useImperativeHandle(
432
+ ref,
433
+ () => ({
434
+ close: () => requestClose({ type: 'dismiss', reason: 'api' }),
435
+ pressAction,
436
+ getOpen: () => openRef.current,
437
+ }),
438
+ [pressAction, requestClose]
439
+ );
440
+
441
+ const footerContext = React.useMemo<ActionDialogFooterRenderContext>(
442
+ () => ({
443
+ actions: resolvedActions,
444
+ layout: resolvedFooterLayout,
445
+ close: () => requestClose({ type: 'dismiss', reason: 'api' }),
446
+ pressAction,
447
+ }),
448
+ [pressAction, requestClose, resolvedActions, resolvedFooterLayout]
449
+ );
450
+ const overlayStyle = useAnimatedStyle(() => ({
451
+ opacity: backdropProgress.value,
452
+ }));
453
+
454
+ const dialogAnimatedStyle = useAnimatedStyle(() => ({
455
+ opacity: cardOpacity.value,
456
+ transform: [
457
+ { translateY: -keyboardOffset.value },
458
+ { scale: cardScale.value },
459
+ ],
460
+ }));
461
+
462
+ const renderAction = React.useCallback(
463
+ (action: ActionDialogResolvedAction, index: number) => {
464
+ const actionLoading = action.loading;
465
+ const actionDisabled = disabled || action.disabled;
466
+
467
+ if (resolvedFooterLayout === 'bar') {
468
+ return (
469
+ <View
470
+ key={action.key}
471
+ style={[
472
+ styles.barActionCell,
473
+ index > 0 ? [styles.barActionDivider, { borderLeftColor: resolvedColors.border }] : null,
474
+ ]}
475
+ >
476
+ <Button
477
+ accessibilityLabel={action.accessibilityLabel}
478
+ block
479
+ disabled={actionDisabled}
480
+ loading={actionLoading}
481
+ onPress={() => void pressAction(action.key)}
482
+ pressEffect="highlight"
483
+ shape="square"
484
+ size="md"
485
+ layout={BAR_ACTION_BUTTON_LAYOUT}
486
+ testID={action.testID}
487
+ tone={action.tone === 'danger' ? 'danger' : action.tone}
488
+ variant="ghost"
489
+ >
490
+ {action.label}
491
+ </Button>
492
+ </View>
493
+ );
494
+ }
495
+
496
+ return (
497
+ <View key={action.key} style={resolvedFooterLayout === 'row' ? styles.rowActionCell : null}>
498
+ <Button
499
+ accessibilityLabel={action.accessibilityLabel}
500
+ block
501
+ disabled={actionDisabled}
502
+ layout={
503
+ resolvedFooterLayout === 'row' && resolvedActions.length > 2
504
+ ? COMPACT_ROW_ACTION_BUTTON_LAYOUT
505
+ : undefined
506
+ }
507
+ loading={actionLoading}
508
+ onPress={() => void pressAction(action.key)}
509
+ pressEffect="highlight"
510
+ shape="rounded"
511
+ size="md"
512
+ testID={action.testID}
513
+ tone={action.tone === 'danger' ? 'danger' : action.tone}
514
+ variant={action.variant}
515
+ >
516
+ {action.label}
517
+ </Button>
518
+ </View>
519
+ );
520
+ },
521
+ [
522
+ disabled,
523
+ pressAction,
524
+ resolvedColors.border,
525
+ resolvedActions.length,
526
+ resolvedFooterLayout,
527
+ ]
528
+ );
529
+
530
+ const renderedFooter = React.useMemo(() => {
531
+ if (footer?.render) return footer.render(footerContext);
532
+ if (!resolvedActions.length) return null;
533
+
534
+ if (resolvedFooterLayout === 'bar') {
535
+ return (
536
+ <View style={[styles.barFooter, { borderTopColor: resolvedColors.border }]}>
537
+ {resolvedActions.map(renderAction)}
538
+ </View>
539
+ );
540
+ }
541
+
542
+ return (
543
+ <View
544
+ style={[
545
+ styles.panelFooter,
546
+ resolvedFooterLayout === 'row' ? styles.panelFooterRow : styles.panelFooterStack,
547
+ ]}
548
+ >
549
+ {resolvedActions.map(renderAction)}
550
+ </View>
551
+ );
552
+ }, [footer, footerContext, renderAction, resolvedActions, resolvedColors.border, resolvedFooterLayout]);
553
+
554
+ const hasBody = title != null || message != null || children != null;
555
+ const computedAccessibilityLabel =
556
+ accessibilityLabel ?? getAccessibilityLabel(title, message, resolvedLabels.close);
557
+
558
+ if (!mounted) return null;
559
+
560
+ const dialogNode = (
561
+ <View
562
+ accessibilityLabel={computedAccessibilityLabel}
563
+ accessibilityLiveRegion="polite"
564
+ accessibilityRole="alert"
565
+ accessibilityViewIsModal
566
+ importantForAccessibility="yes"
567
+ onAccessibilityEscape={() => {
568
+ if (resolvedDismiss.backPress) dismissWithReason('back');
569
+ }}
570
+ pointerEvents={actualOpen ? 'auto' : 'none'}
571
+ style={[
572
+ hostMode === 'inline' ? styles.inlineRoot : styles.modalRoot,
573
+ {
574
+ elevation: Platform.OS === 'android' ? layer?.zIndex ?? ACTION_DIALOG_DEFAULT_Z_INDEX : 0,
575
+ zIndex: layer?.zIndex ?? ACTION_DIALOG_DEFAULT_Z_INDEX,
576
+ },
577
+ ]}
578
+ testID={testID}
579
+ >
580
+ <Pressable accessible={false} onPress={handleOverlayPress} style={StyleSheet.absoluteFill}>
581
+ <Animated.View
582
+ pointerEvents="none"
583
+ style={[styles.backdrop, { backgroundColor: resolvedColors.backdrop }, overlayStyle]}
584
+ />
585
+ </Pressable>
586
+
587
+ <View
588
+ pointerEvents="box-none"
589
+ style={[
590
+ styles.center,
591
+ {
592
+ paddingBottom: Math.max(insets.bottom, wp(16)),
593
+ paddingTop: Math.max(insets.top, wp(16)),
594
+ },
595
+ ]}
596
+ >
597
+ <Animated.View
598
+ onLayout={(event) => {
599
+ dialogHeightRef.current = event.nativeEvent.layout.height || 0;
600
+ if (keyboardMetricsRef.current) updateKeyboardOffset(keyboardMetricsRef.current);
601
+ }}
602
+ shouldRasterizeIOS={Platform.OS === 'ios'}
603
+ style={[
604
+ styles.dialog,
605
+ {
606
+ borderRadius: wp(resolvedLayout.radius),
607
+ maxHeight: maxDialogHeight,
608
+ width: dialogWidth,
609
+ },
610
+ dialogAnimatedStyle,
611
+ style,
612
+ ]}
613
+ >
614
+ <View
615
+ style={[
616
+ styles.surface,
617
+ {
618
+ backgroundColor: resolvedColors.surface,
619
+ borderRadius: wp(resolvedLayout.radius),
620
+ },
621
+ ]}
622
+ >
623
+ {hasBody ? (
624
+ <ScrollView
625
+ bounces={false}
626
+ keyboardShouldPersistTaps="handled"
627
+ style={{ maxHeight: maxBodyHeight }}
628
+ contentContainerStyle={[
629
+ styles.body,
630
+ { padding: wp(resolvedLayout.contentPadding) },
631
+ !renderedFooter ? styles.bodyWithoutFooter : null,
632
+ ]}
633
+ >
634
+ {title != null ? (
635
+ typeof title === 'string' ? (
636
+ <Text
637
+ accessibilityRole="header"
638
+ align="center"
639
+ color={resolvedColors.title}
640
+ size="lg"
641
+ weight="semibold"
642
+ >
643
+ {title}
644
+ </Text>
645
+ ) : (
646
+ title
647
+ )
648
+ ) : null}
649
+ {message != null ? (
650
+ typeof message === 'string' ? (
651
+ <Text align="center" color={resolvedColors.message} size="md">
652
+ {message}
653
+ </Text>
654
+ ) : (
655
+ message
656
+ )
657
+ ) : null}
658
+ {children}
659
+ </ScrollView>
660
+ ) : null}
661
+ {renderedFooter}
662
+ </View>
663
+ </Animated.View>
664
+ </View>
665
+ </View>
666
+ );
667
+
668
+ if (hostMode === 'inline') return dialogNode;
669
+
670
+ return (
671
+ <Modal
672
+ {...modalProps}
673
+ animationType="none"
674
+ hardwareAccelerated
675
+ onRequestClose={handleModalRequestClose}
676
+ presentationStyle="overFullScreen"
677
+ statusBarTranslucent
678
+ supportedOrientations={[
679
+ 'portrait',
680
+ 'portrait-upside-down',
681
+ 'landscape',
682
+ 'landscape-left',
683
+ 'landscape-right',
684
+ ]}
685
+ transparent
686
+ visible={mounted}
687
+ >
688
+ {dialogNode}
689
+ </Modal>
690
+ );
691
+ });
692
+
693
+ export const ActionDialog = React.memo(ActionDialogRoot);
694
+
695
+ const styles = StyleSheet.create({
696
+ modalRoot: {
697
+ flex: 1,
698
+ },
699
+ inlineRoot: {
700
+ ...StyleSheet.absoluteFillObject,
701
+ },
702
+ backdrop: {
703
+ ...StyleSheet.absoluteFillObject,
704
+ },
705
+ center: {
706
+ alignItems: 'center',
707
+ flex: 1,
708
+ justifyContent: 'center',
709
+ paddingHorizontal: wp(16),
710
+ },
711
+ dialog: {
712
+ shadowColor: '#000000',
713
+ shadowOffset: { width: 0, height: wp(14) },
714
+ shadowOpacity: 0.18,
715
+ shadowRadius: wp(28),
716
+ elevation: wp(10),
717
+ },
718
+ surface: {
719
+ overflow: 'hidden',
720
+ },
721
+ body: {
722
+ alignItems: 'center',
723
+ gap: wp(10),
724
+ },
725
+ bodyWithoutFooter: {
726
+ paddingBottom: wp(22),
727
+ },
728
+ panelFooter: {
729
+ gap: wp(10),
730
+ paddingBottom: wp(16),
731
+ paddingHorizontal: wp(16),
732
+ paddingTop: wp(2),
733
+ },
734
+ panelFooterRow: {
735
+ flexDirection: 'row',
736
+ },
737
+ panelFooterStack: {
738
+ flexDirection: 'column',
739
+ },
740
+ rowActionCell: {
741
+ flex: 1,
742
+ minWidth: 0,
743
+ },
744
+ barFooter: {
745
+ borderTopWidth: StyleSheet.hairlineWidth,
746
+ flexDirection: 'row',
747
+ height: BAR_FOOTER_HEIGHT,
748
+ },
749
+ barActionCell: {
750
+ flex: 1,
751
+ minWidth: 0,
752
+ },
753
+ barActionDivider: {
754
+ borderLeftWidth: StyleSheet.hairlineWidth,
755
+ },
756
+ });