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,1356 @@
1
+ import * as React from 'react';
2
+ import type {
3
+ ColorSchemeName,
4
+ DimensionValue,
5
+ GestureResponderEvent,
6
+ Insets,
7
+ StyleProp,
8
+ TextStyle,
9
+ ViewStyle,
10
+ } from 'react-native';
11
+ import {
12
+ Platform,
13
+ Pressable,
14
+ StyleSheet,
15
+ processColor,
16
+ useColorScheme,
17
+ useWindowDimensions,
18
+ View,
19
+ } from 'react-native';
20
+ import Animated, {
21
+ Easing,
22
+ ReduceMotion,
23
+ interpolate,
24
+ useAnimatedStyle,
25
+ useSharedValue,
26
+ withTiming,
27
+ } from 'react-native-reanimated';
28
+ import { wp } from 'zkit-tools';
29
+ import type { Theme } from '../../theme/types';
30
+ import { useTheme } from '../../theme/useTheme';
31
+ import { LoadingSpinner } from '../LoadingSpinner';
32
+ import { Text } from '../Text';
33
+
34
+ export type ButtonVariant = 'solid' | 'soft' | 'outline' | 'ghost' | 'link';
35
+ export type ButtonTone = 'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'info';
36
+ export type ButtonShape = 'rounded' | 'pill' | 'square';
37
+ export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
38
+ export type ButtonIconPlacement = 'start' | 'end';
39
+ export type ButtonPressEffect =
40
+ | 'auto'
41
+ | 'none'
42
+ | 'opacity'
43
+ | 'scale'
44
+ | 'highlight'
45
+ | 'scale-highlight'
46
+ | 'scale-opacity';
47
+ export type ButtonLoadingMode = 'inline' | 'overlay';
48
+ export type ButtonBorderStyle = 'solid' | 'dashed';
49
+ export type ButtonGradientDirection =
50
+ | 'to right'
51
+ | 'to left'
52
+ | 'to bottom'
53
+ | 'to top'
54
+ | 'to bottom right'
55
+ | 'to bottom left'
56
+ | 'to top right'
57
+ | 'to top left'
58
+ | `${number}deg`;
59
+ export type ButtonGradientPoint = { x: number; y: number };
60
+ export type ButtonGradient = {
61
+ colors: string[];
62
+ direction?: ButtonGradientDirection;
63
+ start?: ButtonGradientPoint;
64
+ end?: ButtonGradientPoint;
65
+ };
66
+ export type ButtonShadowSize = 'none' | 'sm' | 'md' | 'lg';
67
+ export type ButtonShadowConfig = {
68
+ color?: string;
69
+ opacity?: number;
70
+ radius?: number;
71
+ offsetX?: number;
72
+ offsetY?: number;
73
+ elevation?: number;
74
+ };
75
+ export type ButtonShadow = ButtonShadowSize | ButtonShadowConfig;
76
+ export type ButtonBorder = {
77
+ width?: number;
78
+ style?: ButtonBorderStyle;
79
+ color?: string;
80
+ };
81
+ export type ButtonLayout = {
82
+ width?: DimensionValue;
83
+ minWidth?: DimensionValue;
84
+ maxWidth?: DimensionValue;
85
+ height?: DimensionValue;
86
+ minHeight?: number;
87
+ paddingHorizontal?: number;
88
+ paddingVertical?: number;
89
+ gap?: number;
90
+ radius?: number;
91
+ iconSize?: number;
92
+ loadingSize?: number;
93
+ textSize?: number;
94
+ textLineHeight?: number;
95
+ };
96
+ export type ButtonColors = {
97
+ color?: string;
98
+ background?: string;
99
+ text?: string;
100
+ border?: string;
101
+ disabledBackground?: string;
102
+ disabledText?: string;
103
+ disabledBorder?: string;
104
+ loading?: string;
105
+ pressedOverlay?: string;
106
+ };
107
+
108
+ type NativePressableProps = Omit<
109
+ React.ComponentPropsWithoutRef<typeof Pressable>,
110
+ 'accessibilityRole' | 'accessibilityState' | 'children' | 'disabled' | 'style'
111
+ >;
112
+
113
+ export type ButtonRef = React.ComponentRef<typeof Pressable>;
114
+
115
+ export interface ButtonProps extends NativePressableProps {
116
+ variant?: ButtonVariant;
117
+ tone?: ButtonTone;
118
+ size?: ButtonSize;
119
+ shape?: ButtonShape;
120
+ block?: boolean;
121
+ disabled?: boolean;
122
+ loading?: boolean;
123
+ loadingMode?: ButtonLoadingMode;
124
+ pressEffect?: ButtonPressEffect;
125
+
126
+ icon?: React.ReactNode;
127
+ iconPlacement?: ButtonIconPlacement;
128
+ iconOnly?: boolean;
129
+
130
+ color?: string;
131
+ colors?: ButtonColors;
132
+ border?: ButtonBorder;
133
+ layout?: ButtonLayout;
134
+ gradient?: ButtonGradient;
135
+ shadow?: ButtonShadow;
136
+
137
+ children?: React.ReactNode;
138
+ style?: StyleProp<ViewStyle>;
139
+ contentStyle?: StyleProp<ViewStyle>;
140
+ textStyle?: StyleProp<TextStyle>;
141
+ accessibilityState?: React.ComponentPropsWithoutRef<typeof Pressable>['accessibilityState'];
142
+ }
143
+
144
+ type ButtonMetrics = {
145
+ minHeight: number;
146
+ paddingHorizontal: number;
147
+ paddingVertical: number;
148
+ gap: number;
149
+ radius: number;
150
+ iconOnlySide: number;
151
+ iconSize: number;
152
+ loadingSize: number;
153
+ textSize: number;
154
+ textLineHeight: number;
155
+ borderWidth: number;
156
+ linkPaddingVertical: number;
157
+ minTouchTarget: number;
158
+ };
159
+
160
+ type TonePalette = {
161
+ color: string;
162
+ onColor: string;
163
+ };
164
+
165
+ type VisualColors = {
166
+ backgroundColor: string;
167
+ borderColor: string;
168
+ textColor: string;
169
+ loadingColor: string;
170
+ };
171
+
172
+ type LinearGradientProps = {
173
+ pointerEvents?: 'none';
174
+ colors: string[];
175
+ start?: ButtonGradientPoint;
176
+ end?: ButtonGradientPoint;
177
+ style?: StyleProp<ViewStyle>;
178
+ };
179
+
180
+ type LinearGradientConfig = {
181
+ colors: string[];
182
+ start?: ButtonGradientPoint;
183
+ end?: ButtonGradientPoint;
184
+ };
185
+
186
+ type PressEffectFlags = {
187
+ highlight: boolean;
188
+ opacity: boolean;
189
+ scale: boolean;
190
+ };
191
+
192
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
193
+
194
+ const PRESS_TIMING = {
195
+ duration: 120,
196
+ easing: Easing.out(Easing.cubic),
197
+ reduceMotion: ReduceMotion.System,
198
+ } as const;
199
+ const LOADING_TIMING = {
200
+ duration: 180,
201
+ easing: Easing.out(Easing.cubic),
202
+ reduceMotion: ReduceMotion.System,
203
+ } as const;
204
+
205
+ const DISABLED_OPACITY = 0.52;
206
+ const PRESSED_OPACITY = 0.82;
207
+ const PRESSED_SCALE = 0.985;
208
+ const LOADING_SIZE_VISUAL_SCALE = 1.2;
209
+
210
+ const SEMANTIC_COLORS: Record<string, string> = {
211
+ danger: '#DC2626',
212
+ error: '#DC2626',
213
+ info: '#2563EB',
214
+ success: '#16A34A',
215
+ warn: '#D97706',
216
+ warning: '#D97706',
217
+ };
218
+
219
+ const SIZE_TOKENS = {
220
+ xs: {
221
+ minHeight: 32,
222
+ paddingHorizontal: 10,
223
+ paddingVertical: 5,
224
+ gap: 5,
225
+ radius: 10,
226
+ iconOnlySide: 32,
227
+ iconSize: 15,
228
+ textSize: 13,
229
+ textLineHeight: 18,
230
+ },
231
+ sm: {
232
+ minHeight: 36,
233
+ paddingHorizontal: 12,
234
+ paddingVertical: 7,
235
+ gap: 6,
236
+ radius: 12,
237
+ iconOnlySide: 36,
238
+ iconSize: 16,
239
+ textSize: 14,
240
+ textLineHeight: 19,
241
+ },
242
+ md: {
243
+ minHeight: 44,
244
+ paddingHorizontal: 16,
245
+ paddingVertical: 10,
246
+ gap: 8,
247
+ radius: 14,
248
+ iconOnlySide: 44,
249
+ iconSize: 18,
250
+ textSize: 15,
251
+ textLineHeight: 21,
252
+ },
253
+ lg: {
254
+ minHeight: 48,
255
+ paddingHorizontal: 20,
256
+ paddingVertical: 12,
257
+ gap: 9,
258
+ radius: 16,
259
+ iconOnlySide: 48,
260
+ iconSize: 20,
261
+ textSize: 16,
262
+ textLineHeight: 22,
263
+ },
264
+ xl: {
265
+ minHeight: 56,
266
+ paddingHorizontal: 24,
267
+ paddingVertical: 14,
268
+ gap: 10,
269
+ radius: 18,
270
+ iconOnlySide: 56,
271
+ iconSize: 22,
272
+ textSize: 17,
273
+ textLineHeight: 24,
274
+ },
275
+ } as const satisfies Record<
276
+ ButtonSize,
277
+ Omit<ButtonMetrics, 'borderWidth' | 'linkPaddingVertical' | 'loadingSize' | 'minTouchTarget'>
278
+ >;
279
+
280
+ const GRADIENT_DIRECTIONS: Record<string, Pick<LinearGradientConfig, 'start' | 'end'>> = {
281
+ 'to bottom': { start: { x: 0.5, y: 0 }, end: { x: 0.5, y: 1 } },
282
+ 'to bottom left': { start: { x: 1, y: 0 }, end: { x: 0, y: 1 } },
283
+ 'to bottom right': { start: { x: 0, y: 0 }, end: { x: 1, y: 1 } },
284
+ 'to left': { start: { x: 1, y: 0.5 }, end: { x: 0, y: 0.5 } },
285
+ 'to right': { start: { x: 0, y: 0.5 }, end: { x: 1, y: 0.5 } },
286
+ 'to top': { start: { x: 0.5, y: 1 }, end: { x: 0.5, y: 0 } },
287
+ 'to top left': { start: { x: 1, y: 1 }, end: { x: 0, y: 0 } },
288
+ 'to top right': { start: { x: 0, y: 1 }, end: { x: 1, y: 0 } },
289
+ };
290
+
291
+ let cachedLinearGradientComponent: React.ComponentType<LinearGradientProps> | null | undefined;
292
+
293
+ function resolveDefaultLoadingSize(iconSize: number, availableHeight: number | undefined) {
294
+ const scaled = iconSize * LOADING_SIZE_VISUAL_SCALE;
295
+ if (availableHeight == null || !Number.isFinite(availableHeight) || availableHeight <= 0) {
296
+ return scaled;
297
+ }
298
+
299
+ return Math.min(scaled, Math.max(0, availableHeight));
300
+ }
301
+
302
+ function resolveMetrics(size: ButtonSize): ButtonMetrics {
303
+ const token = SIZE_TOKENS[size] ?? SIZE_TOKENS.md;
304
+ const loadingContentHeight = token.minHeight - token.paddingVertical * 2;
305
+ return {
306
+ minHeight: wp(token.minHeight),
307
+ paddingHorizontal: wp(token.paddingHorizontal),
308
+ paddingVertical: wp(token.paddingVertical),
309
+ gap: wp(token.gap),
310
+ radius: wp(token.radius),
311
+ iconOnlySide: wp(token.iconOnlySide),
312
+ iconSize: wp(token.iconSize),
313
+ loadingSize: wp(resolveDefaultLoadingSize(token.iconSize, loadingContentHeight)),
314
+ textSize: wp(token.textSize),
315
+ textLineHeight: wp(token.textLineHeight),
316
+ borderWidth: wp(1),
317
+ linkPaddingVertical: wp(2),
318
+ minTouchTarget: wp(44),
319
+ };
320
+ }
321
+
322
+ function isPrimitiveTextChild(children: React.ReactNode): children is string | number {
323
+ return typeof children === 'string' || typeof children === 'number';
324
+ }
325
+
326
+ function normalizeNumber(input: unknown) {
327
+ if (typeof input === 'number') return Number.isFinite(input) ? input : undefined;
328
+ if (typeof input !== 'string') return undefined;
329
+
330
+ const value = input.trim();
331
+ if (!value || value === 'auto' || value.endsWith('%')) return undefined;
332
+
333
+ const parsed = Number.parseFloat(value);
334
+ return Number.isFinite(parsed) ? parsed : undefined;
335
+ }
336
+
337
+ function isProcessableColor(color: string) {
338
+ return processColor(color) != null;
339
+ }
340
+
341
+ function colorToRgba(color: string, alpha: number) {
342
+ const processed = processColor(color);
343
+ if (typeof processed !== 'number') return undefined;
344
+
345
+ const normalized = processed >>> 0;
346
+ const r = (normalized >> 16) & 255;
347
+ const g = (normalized >> 8) & 255;
348
+ const b = normalized & 255;
349
+ const a = Math.max(0, Math.min(1, alpha));
350
+ return `rgba(${r},${g},${b},${a})`;
351
+ }
352
+
353
+ function resolveColorToken(input: string | undefined, fallback: string, theme: Theme) {
354
+ if (input == null) return fallback;
355
+
356
+ const key = input.trim();
357
+ if (!key) return fallback;
358
+
359
+ const resolved =
360
+ key === 'primary'
361
+ ? theme.colors.primary
362
+ : key === 'onPrimary'
363
+ ? theme.colors.onPrimary
364
+ : key === 'secondary' || key === 'neutral'
365
+ ? theme.colors.secondary
366
+ : key === 'onSecondary'
367
+ ? theme.colors.onSecondary
368
+ : key === 'surface'
369
+ ? theme.colors.surface
370
+ : key === 'onSurface'
371
+ ? theme.colors.onSurface
372
+ : key === 'border'
373
+ ? theme.colors.border
374
+ : key === 'muted'
375
+ ? theme.colors.muted
376
+ : key === 'disabled'
377
+ ? theme.colors.disabled
378
+ : SEMANTIC_COLORS[key] ?? key;
379
+
380
+ return isProcessableColor(resolved) ? resolved : fallback;
381
+ }
382
+
383
+ function resolveTonePalette(tone: ButtonTone, theme: Theme): TonePalette {
384
+ if (tone === 'neutral') {
385
+ return {
386
+ color: theme.colors.onSurface,
387
+ onColor: theme.colors.surface,
388
+ };
389
+ }
390
+
391
+ if (tone === 'success') {
392
+ return {
393
+ color: SEMANTIC_COLORS.success,
394
+ onColor: '#FFFFFF',
395
+ };
396
+ }
397
+
398
+ if (tone === 'warning') {
399
+ return {
400
+ color: SEMANTIC_COLORS.warning,
401
+ onColor: '#111827',
402
+ };
403
+ }
404
+
405
+ if (tone === 'danger') {
406
+ return {
407
+ color: SEMANTIC_COLORS.danger,
408
+ onColor: '#FFFFFF',
409
+ };
410
+ }
411
+
412
+ if (tone === 'info') {
413
+ return {
414
+ color: SEMANTIC_COLORS.info,
415
+ onColor: '#FFFFFF',
416
+ };
417
+ }
418
+
419
+ return {
420
+ color: theme.colors.primary,
421
+ onColor: theme.colors.onPrimary,
422
+ };
423
+ }
424
+
425
+ function degToVector(deg: number) {
426
+ const rad = (deg * Math.PI) / 180;
427
+ const x = Math.cos(rad);
428
+ const y = Math.sin(rad);
429
+ const len = Math.sqrt(x * x + y * y) || 1;
430
+ return { x: x / len, y: y / len };
431
+ }
432
+
433
+ function parseGradient(gradient: ButtonGradient | undefined): LinearGradientConfig | undefined {
434
+ if (!gradient || gradient.colors.length < 2) return undefined;
435
+
436
+ const colors = gradient.colors.map((x) => String(x).trim()).filter(Boolean);
437
+ if (colors.length < 2) return undefined;
438
+
439
+ if (gradient.start || gradient.end) {
440
+ return { colors, start: gradient.start, end: gradient.end };
441
+ }
442
+
443
+ const direction = gradient.direction?.toLowerCase();
444
+ if (!direction) return { colors };
445
+
446
+ if (direction.startsWith('to ')) {
447
+ return { colors, ...GRADIENT_DIRECTIONS[direction] };
448
+ }
449
+
450
+ const deg = Number.parseFloat(direction.replace('deg', '').trim());
451
+ if (!Number.isFinite(deg)) return { colors };
452
+
453
+ const v = degToVector(deg);
454
+ return {
455
+ colors,
456
+ start: { x: 0.5 - v.x * 0.5, y: 0.5 - v.y * 0.5 },
457
+ end: { x: 0.5 + v.x * 0.5, y: 0.5 + v.y * 0.5 },
458
+ };
459
+ }
460
+
461
+ function pickLinearGradientComponent() {
462
+ if (cachedLinearGradientComponent !== undefined) return cachedLinearGradientComponent;
463
+
464
+ try {
465
+ const mod = require('expo-linear-gradient') as {
466
+ LinearGradient?: React.ComponentType<LinearGradientProps>;
467
+ };
468
+ cachedLinearGradientComponent = mod.LinearGradient ?? null;
469
+ } catch {
470
+ cachedLinearGradientComponent = null;
471
+ }
472
+
473
+ return cachedLinearGradientComponent;
474
+ }
475
+
476
+ function resolveShadowStyle(
477
+ shadow: ButtonShadow | undefined,
478
+ scheme: ColorSchemeName,
479
+ variant: ButtonVariant
480
+ ): ViewStyle | undefined {
481
+ if (!shadow || shadow === 'none' || variant === 'ghost' || variant === 'link') return undefined;
482
+
483
+ if (typeof shadow === 'object') {
484
+ const radius = Math.max(0, shadow.radius ?? wp(10));
485
+ return {
486
+ shadowColor: shadow.color ?? '#000000',
487
+ shadowOffset: {
488
+ width: shadow.offsetX ?? 0,
489
+ height: shadow.offsetY ?? wp(3),
490
+ },
491
+ shadowOpacity: shadow.opacity ?? (scheme === 'dark' ? 0.28 : 0.14),
492
+ shadowRadius: radius,
493
+ elevation: shadow.elevation ?? Math.max(0, Math.round(radius * 0.55)),
494
+ };
495
+ }
496
+
497
+ const level = shadow === 'sm' ? 1 : shadow === 'md' ? 2 : shadow === 'lg' ? 3 : 0;
498
+ if (level <= 0) return undefined;
499
+
500
+ const radius = wp(level * 5 + 3);
501
+ return {
502
+ shadowColor: '#000000',
503
+ shadowOffset: { width: 0, height: wp(level + 1) },
504
+ shadowOpacity: scheme === 'dark' ? 0.26 : 0.14,
505
+ shadowRadius: radius,
506
+ elevation: Math.max(0, Math.round(radius * 0.55)),
507
+ };
508
+ }
509
+
510
+ function resolveVisualColors({
511
+ accentColor,
512
+ colors,
513
+ gradientCfg,
514
+ hasGradientBackground,
515
+ palette,
516
+ theme,
517
+ variant,
518
+ visualDisabled,
519
+ }: {
520
+ accentColor: string;
521
+ colors?: ButtonColors;
522
+ gradientCfg?: LinearGradientConfig;
523
+ hasGradientBackground: boolean;
524
+ palette: TonePalette;
525
+ theme: Theme;
526
+ variant: ButtonVariant;
527
+ visualDisabled: boolean;
528
+ }): VisualColors {
529
+ const fallbackGradientColor = resolveColorToken(gradientCfg?.colors[0], accentColor, theme);
530
+ const solidBackground = gradientCfg ? fallbackGradientColor : accentColor;
531
+ const softBackground = colorToRgba(accentColor, 0.12) ?? theme.colors.secondary;
532
+ const softBorder = colorToRgba(accentColor, 0.2) ?? theme.colors.border;
533
+
534
+ let backgroundColor = 'transparent';
535
+ let borderColor = 'transparent';
536
+ let textColor = accentColor;
537
+
538
+ if (variant === 'solid') {
539
+ backgroundColor = solidBackground;
540
+ textColor = palette.onColor;
541
+ } else if (variant === 'soft') {
542
+ backgroundColor = softBackground;
543
+ borderColor = softBorder;
544
+ } else if (variant === 'outline') {
545
+ borderColor = accentColor;
546
+ }
547
+
548
+ backgroundColor = colors?.background ?? backgroundColor;
549
+ borderColor = colors?.border ?? borderColor;
550
+ textColor = colors?.text ?? textColor;
551
+
552
+ if (hasGradientBackground) {
553
+ backgroundColor = 'transparent';
554
+ borderColor = colors?.border ?? 'transparent';
555
+ textColor = colors?.text ?? palette.onColor;
556
+ }
557
+
558
+ if (visualDisabled) {
559
+ const disabledBackground =
560
+ variant === 'solid' || variant === 'soft' ? theme.colors.secondary : 'transparent';
561
+ backgroundColor = colors?.disabledBackground ?? disabledBackground;
562
+ borderColor = colors?.disabledBorder ?? (variant === 'outline' ? theme.colors.border : borderColor);
563
+ textColor = colors?.disabledText ?? theme.colors.disabled;
564
+ }
565
+
566
+ return {
567
+ backgroundColor,
568
+ borderColor,
569
+ textColor,
570
+ loadingColor: colors?.loading ?? textColor,
571
+ };
572
+ }
573
+
574
+ function resolvePressEffect(effect: ButtonPressEffect, interactionDisabled: boolean): ButtonPressEffect {
575
+ if (interactionDisabled) return 'none';
576
+ if (effect !== 'auto') return effect;
577
+ return 'scale-opacity';
578
+ }
579
+
580
+ function resolvePressEffectFlags(effect: ButtonPressEffect): PressEffectFlags {
581
+ return {
582
+ highlight: effect === 'highlight' || effect === 'scale-highlight',
583
+ opacity: effect === 'opacity' || effect === 'scale-opacity',
584
+ scale: effect === 'scale' || effect === 'scale-highlight' || effect === 'scale-opacity',
585
+ };
586
+ }
587
+
588
+ function resolveOverlay({
589
+ accentColor,
590
+ colors,
591
+ hasGradientBackground,
592
+ variant,
593
+ }: {
594
+ accentColor: string;
595
+ colors?: ButtonColors;
596
+ hasGradientBackground: boolean;
597
+ variant: ButtonVariant;
598
+ }) {
599
+ if (colors?.pressedOverlay) {
600
+ return { color: colors.pressedOverlay, strength: 0.12 };
601
+ }
602
+
603
+ if (variant === 'solid' || hasGradientBackground) {
604
+ return { color: '#000000', strength: 0.1 };
605
+ }
606
+
607
+ if (variant === 'soft') {
608
+ return { color: '#000000', strength: 0.06 };
609
+ }
610
+
611
+ return { color: accentColor, strength: 0.1 };
612
+ }
613
+
614
+ function resolveRootSizeStyle({
615
+ block,
616
+ iconOnly,
617
+ layout,
618
+ metrics,
619
+ variant,
620
+ }: {
621
+ block: boolean;
622
+ iconOnly: boolean;
623
+ layout?: ButtonLayout;
624
+ metrics: ButtonMetrics;
625
+ variant: ButtonVariant;
626
+ }): ViewStyle {
627
+ const width = layout?.width ?? (block ? '100%' : undefined);
628
+ const height = layout?.height;
629
+ const iconSide = normalizeNumber(width) ?? normalizeNumber(height) ?? metrics.iconOnlySide;
630
+
631
+ if (iconOnly) {
632
+ return {
633
+ ...(width !== undefined ? { width } : { width: iconSide }),
634
+ ...(height !== undefined ? { height } : { height: iconSide }),
635
+ ...(layout?.minWidth !== undefined ? { minWidth: layout.minWidth } : undefined),
636
+ ...(layout?.maxWidth !== undefined ? { maxWidth: layout.maxWidth } : undefined),
637
+ };
638
+ }
639
+
640
+ return {
641
+ ...(width !== undefined ? { width } : undefined),
642
+ ...(height !== undefined ? { height } : undefined),
643
+ ...(layout?.minWidth !== undefined ? { minWidth: layout.minWidth } : undefined),
644
+ ...(layout?.maxWidth !== undefined ? { maxWidth: layout.maxWidth } : undefined),
645
+ ...(layout?.minHeight !== undefined
646
+ ? { minHeight: layout.minHeight }
647
+ : variant === 'link'
648
+ ? undefined
649
+ : { minHeight: metrics.minHeight }),
650
+ };
651
+ }
652
+
653
+ function resolveContentSizeStyle(rootSizeStyle: ViewStyle, iconOnly: boolean): ViewStyle {
654
+ const nextStyle: ViewStyle = {};
655
+
656
+ if (iconOnly || rootSizeStyle.width !== undefined) nextStyle.width = '100%';
657
+ if (iconOnly || rootSizeStyle.height !== undefined) nextStyle.height = '100%';
658
+ else if (rootSizeStyle.minHeight !== undefined) nextStyle.minHeight = rootSizeStyle.minHeight;
659
+
660
+ return nextStyle;
661
+ }
662
+
663
+ function resolveSideLength(rootSizeStyle: ViewStyle, iconOnly: boolean, layout: ButtonLayout | undefined, metrics: ButtonMetrics) {
664
+ const height = normalizeNumber(rootSizeStyle.height);
665
+ if (height != null) return height;
666
+ if (iconOnly) return normalizeNumber(rootSizeStyle.width) ?? metrics.iconOnlySide;
667
+ return layout?.minHeight ?? metrics.minHeight;
668
+ }
669
+
670
+ function resolveRadiusStyle({
671
+ iconOnly,
672
+ layout,
673
+ metrics,
674
+ rootSizeStyle,
675
+ shape,
676
+ }: {
677
+ iconOnly: boolean;
678
+ layout?: ButtonLayout;
679
+ metrics: ButtonMetrics;
680
+ rootSizeStyle: ViewStyle;
681
+ shape: ButtonShape;
682
+ }): ViewStyle {
683
+ if (shape === 'square') return { borderRadius: 0 };
684
+
685
+ if (shape === 'pill') {
686
+ const radius = resolveSideLength(rootSizeStyle, iconOnly, layout, metrics) / 2;
687
+ return {
688
+ borderBottomLeftRadius: radius,
689
+ borderBottomRightRadius: radius,
690
+ borderRadius: radius,
691
+ borderTopLeftRadius: radius,
692
+ borderTopRightRadius: radius,
693
+ };
694
+ }
695
+
696
+ return { borderRadius: layout?.radius ?? metrics.radius };
697
+ }
698
+
699
+ function resolvePaddingStyle({
700
+ iconOnly,
701
+ layout,
702
+ metrics,
703
+ variant,
704
+ }: {
705
+ iconOnly: boolean;
706
+ layout?: ButtonLayout;
707
+ metrics: ButtonMetrics;
708
+ variant: ButtonVariant;
709
+ }): ViewStyle {
710
+ if (iconOnly) return {};
711
+
712
+ if (variant === 'link') {
713
+ return {
714
+ paddingHorizontal: 0,
715
+ paddingVertical: layout?.paddingVertical ?? metrics.linkPaddingVertical,
716
+ };
717
+ }
718
+
719
+ return {
720
+ paddingHorizontal: layout?.paddingHorizontal ?? metrics.paddingHorizontal,
721
+ paddingVertical: layout?.paddingVertical ?? metrics.paddingVertical,
722
+ };
723
+ }
724
+
725
+ function resolveBorderStyle({
726
+ border,
727
+ metrics,
728
+ theme,
729
+ visualColors,
730
+ variant,
731
+ }: {
732
+ border?: ButtonBorder;
733
+ metrics: ButtonMetrics;
734
+ theme: Theme;
735
+ visualColors: VisualColors;
736
+ variant: ButtonVariant;
737
+ }): ViewStyle {
738
+ const defaultBorderWidth = variant === 'outline' ? metrics.borderWidth : undefined;
739
+ const width = border?.width ?? defaultBorderWidth;
740
+ if (width == null) return {};
741
+
742
+ return {
743
+ borderColor:
744
+ border?.color ??
745
+ (visualColors.borderColor === 'transparent' ? theme.colors.border : visualColors.borderColor),
746
+ borderStyle: border?.style ?? 'solid',
747
+ borderWidth: width,
748
+ };
749
+ }
750
+
751
+ function resolveDefaultHitSlop({
752
+ iconOnly,
753
+ layout,
754
+ metrics,
755
+ rootSizeStyle,
756
+ variant,
757
+ }: {
758
+ iconOnly: boolean;
759
+ layout?: ButtonLayout;
760
+ metrics: ButtonMetrics;
761
+ rootSizeStyle: ViewStyle;
762
+ variant: ButtonVariant;
763
+ }): Insets | undefined {
764
+ if (variant === 'link') {
765
+ const visualHeight =
766
+ (layout?.textLineHeight ?? metrics.textLineHeight) +
767
+ (layout?.paddingVertical ?? metrics.linkPaddingVertical) * 2;
768
+ const vertical = Math.max(wp(6), (metrics.minTouchTarget - visualHeight) / 2);
769
+
770
+ return {
771
+ bottom: vertical,
772
+ left: wp(8),
773
+ right: wp(8),
774
+ top: vertical,
775
+ };
776
+ }
777
+
778
+ const side = resolveSideLength(rootSizeStyle, iconOnly, layout, metrics);
779
+ const slop = Math.max(0, (metrics.minTouchTarget - side) / 2);
780
+ if (slop <= 0) return undefined;
781
+
782
+ return {
783
+ bottom: slop,
784
+ left: iconOnly ? slop : 0,
785
+ right: iconOnly ? slop : 0,
786
+ top: slop,
787
+ };
788
+ }
789
+
790
+ function resolveLoadingSize({
791
+ iconOnly,
792
+ layout,
793
+ metrics,
794
+ variant,
795
+ }: {
796
+ iconOnly: boolean;
797
+ layout?: ButtonLayout;
798
+ metrics: ButtonMetrics;
799
+ variant: ButtonVariant;
800
+ }) {
801
+ if (layout?.loadingSize !== undefined) return layout.loadingSize;
802
+
803
+ const iconSize = layout?.iconSize ?? metrics.iconSize;
804
+
805
+ if (iconOnly) {
806
+ const availableHeight =
807
+ normalizeNumber(layout?.height) ?? normalizeNumber(layout?.width) ?? metrics.iconOnlySide;
808
+ return resolveDefaultLoadingSize(iconSize, availableHeight);
809
+ }
810
+
811
+ if (variant === 'link') {
812
+ const availableHeight =
813
+ (layout?.textLineHeight ?? metrics.textLineHeight) +
814
+ (layout?.paddingVertical ?? metrics.linkPaddingVertical) * 2;
815
+ return resolveDefaultLoadingSize(iconSize, availableHeight);
816
+ }
817
+
818
+ const availableHeight =
819
+ (normalizeNumber(layout?.height) ?? layout?.minHeight ?? metrics.minHeight) -
820
+ (layout?.paddingVertical ?? metrics.paddingVertical) * 2;
821
+ return resolveDefaultLoadingSize(iconSize, availableHeight);
822
+ }
823
+
824
+ function resolveIconBoxStyle(visible: boolean, iconSize: number): ViewStyle {
825
+ if (!visible) return {};
826
+
827
+ return {
828
+ alignItems: 'center',
829
+ height: iconSize,
830
+ justifyContent: 'center',
831
+ overflow: 'hidden',
832
+ };
833
+ }
834
+
835
+ function resolveWebCursorStyle(disabled: boolean): ViewStyle | undefined {
836
+ if (Platform.OS !== 'web') return undefined;
837
+ return { cursor: disabled ? 'not-allowed' : 'pointer' } as ViewStyle;
838
+ }
839
+
840
+ function ButtonImpl(
841
+ {
842
+ variant = 'solid',
843
+ tone = 'primary',
844
+ size = 'md',
845
+ shape = 'rounded',
846
+ block = false,
847
+ disabled = false,
848
+ loading = false,
849
+ loadingMode = 'inline',
850
+ pressEffect = 'auto',
851
+ icon,
852
+ iconPlacement = 'start',
853
+ iconOnly = false,
854
+ color,
855
+ colors,
856
+ border,
857
+ layout,
858
+ gradient,
859
+ shadow = 'none',
860
+ children,
861
+ style,
862
+ contentStyle,
863
+ textStyle,
864
+ accessibilityLabel,
865
+ accessibilityState,
866
+ hitSlop,
867
+ onPress,
868
+ onPressIn,
869
+ onPressOut,
870
+ testID,
871
+ ...pressableProps
872
+ }: ButtonProps,
873
+ ref: React.ForwardedRef<ButtonRef>
874
+ ) {
875
+ const theme = useTheme();
876
+ const scheme = useColorScheme();
877
+ const { width: viewportWidth } = useWindowDimensions();
878
+
879
+ const metrics = React.useMemo(() => resolveMetrics(size), [size, viewportWidth]);
880
+ const palette = React.useMemo(() => resolveTonePalette(tone, theme), [theme, tone]);
881
+ const accentColor = React.useMemo(
882
+ () => resolveColorToken(color ?? colors?.color, palette.color, theme),
883
+ [color, colors?.color, palette.color, theme]
884
+ );
885
+
886
+ const gradientCfg = React.useMemo(() => parseGradient(gradient), [gradient]);
887
+ const LinearGradientComponent = React.useMemo(
888
+ () => (variant === 'solid' && gradientCfg ? (pickLinearGradientComponent() ?? undefined) : undefined),
889
+ [gradientCfg, variant]
890
+ );
891
+ const hasGradientBackground = Boolean(variant === 'solid' && gradientCfg && LinearGradientComponent);
892
+
893
+ const interactionDisabled = disabled || loading;
894
+ const visualDisabled = disabled;
895
+ const resolvedLoadingMode = iconOnly ? 'overlay' : loadingMode;
896
+ const centeredLoading = resolvedLoadingMode !== 'inline';
897
+
898
+ const visualColors = React.useMemo(
899
+ () =>
900
+ resolveVisualColors({
901
+ accentColor,
902
+ colors,
903
+ gradientCfg,
904
+ hasGradientBackground,
905
+ palette,
906
+ theme,
907
+ variant,
908
+ visualDisabled,
909
+ }),
910
+ [accentColor, colors, gradientCfg, hasGradientBackground, palette, theme, variant, visualDisabled]
911
+ );
912
+
913
+ const rootSizeStyle = React.useMemo(
914
+ () =>
915
+ resolveRootSizeStyle({
916
+ block,
917
+ iconOnly,
918
+ layout,
919
+ metrics,
920
+ variant,
921
+ }),
922
+ [block, iconOnly, layout, metrics, variant]
923
+ );
924
+ const contentSizeStyle = React.useMemo(
925
+ () => resolveContentSizeStyle(rootSizeStyle, iconOnly),
926
+ [iconOnly, rootSizeStyle]
927
+ );
928
+ const radiusStyle = React.useMemo(
929
+ () =>
930
+ resolveRadiusStyle({
931
+ iconOnly,
932
+ layout,
933
+ metrics,
934
+ rootSizeStyle,
935
+ shape,
936
+ }),
937
+ [iconOnly, layout, metrics, rootSizeStyle, shape]
938
+ );
939
+ const paddingStyle = React.useMemo(
940
+ () =>
941
+ resolvePaddingStyle({
942
+ iconOnly,
943
+ layout,
944
+ metrics,
945
+ variant,
946
+ }),
947
+ [iconOnly, layout, metrics, variant]
948
+ );
949
+ const borderStyle = React.useMemo(
950
+ () =>
951
+ resolveBorderStyle({
952
+ border,
953
+ metrics,
954
+ theme,
955
+ visualColors,
956
+ variant,
957
+ }),
958
+ [border, metrics, theme, variant, visualColors]
959
+ );
960
+ const shadowStyle = React.useMemo(() => resolveShadowStyle(shadow, scheme, variant), [scheme, shadow, variant]);
961
+ const webCursorStyle = React.useMemo(
962
+ () => resolveWebCursorStyle(interactionDisabled),
963
+ [interactionDisabled]
964
+ );
965
+ const defaultHitSlop = React.useMemo(
966
+ () =>
967
+ resolveDefaultHitSlop({
968
+ iconOnly,
969
+ layout,
970
+ metrics,
971
+ rootSizeStyle,
972
+ variant,
973
+ }),
974
+ [iconOnly, layout, metrics, rootSizeStyle, variant]
975
+ );
976
+
977
+ const resolvedText = React.useMemo(() => {
978
+ if (children == null) return null;
979
+ if (!isPrimitiveTextChild(children)) return children;
980
+
981
+ return (
982
+ <Text
983
+ numberOfLines={1}
984
+ style={[
985
+ styles.text,
986
+ {
987
+ color: visualColors.textColor,
988
+ fontSize: layout?.textSize ?? metrics.textSize,
989
+ lineHeight: layout?.textLineHeight ?? metrics.textLineHeight,
990
+ },
991
+ variant === 'link' ? styles.linkText : null,
992
+ textStyle,
993
+ ]}
994
+ >
995
+ {children}
996
+ </Text>
997
+ );
998
+ }, [
999
+ children,
1000
+ layout?.textLineHeight,
1001
+ layout?.textSize,
1002
+ metrics.textLineHeight,
1003
+ metrics.textSize,
1004
+ textStyle,
1005
+ variant,
1006
+ visualColors.textColor,
1007
+ ]);
1008
+
1009
+ const hasText = resolvedText != null;
1010
+ const hasIcon = icon != null;
1011
+ const iconSize = layout?.iconSize ?? metrics.iconSize;
1012
+ const loadingSize = React.useMemo(
1013
+ () =>
1014
+ resolveLoadingSize({
1015
+ iconOnly,
1016
+ layout,
1017
+ metrics,
1018
+ variant,
1019
+ }),
1020
+ [iconOnly, layout, metrics, variant]
1021
+ );
1022
+ const contentGap = layout?.gap ?? metrics.gap;
1023
+ const iconBaseBoxStyle = React.useMemo(() => resolveIconBoxStyle(hasIcon, iconSize), [hasIcon, iconSize]);
1024
+ const iconPlacedBoxStyle = React.useMemo((): ViewStyle => {
1025
+ if (!hasIcon) return {};
1026
+ const gap = hasText ? contentGap : 0;
1027
+ return {
1028
+ width: iconSize,
1029
+ ...(iconPlacement === 'end' ? { marginLeft: gap } : { marginRight: gap }),
1030
+ };
1031
+ }, [contentGap, hasIcon, hasText, iconPlacement, iconSize]);
1032
+
1033
+ const inferredAccessibilityLabel =
1034
+ iconOnly && isPrimitiveTextChild(children) ? String(children) : undefined;
1035
+ const resolvedAccessibilityLabel = accessibilityLabel ?? inferredAccessibilityLabel;
1036
+
1037
+ const pressSv = useSharedValue(0);
1038
+ const loadingSv = useSharedValue(loading ? 1 : 0);
1039
+ const [spinnerMounted, setSpinnerMounted] = React.useState(loading);
1040
+ const hideSpinnerTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
1041
+
1042
+ const resolvedPressEffect = React.useMemo(
1043
+ () => resolvePressEffect(pressEffect, interactionDisabled),
1044
+ [interactionDisabled, pressEffect]
1045
+ );
1046
+ const pressEffectFlags = React.useMemo(
1047
+ () => resolvePressEffectFlags(resolvedPressEffect),
1048
+ [resolvedPressEffect]
1049
+ );
1050
+ const pressHighlightEnabled = pressEffectFlags.highlight;
1051
+ const pressOpacityEnabled = pressEffectFlags.opacity;
1052
+ const pressScaleEnabled = pressEffectFlags.scale;
1053
+ const pressOverlay = React.useMemo(
1054
+ () =>
1055
+ resolveOverlay({
1056
+ accentColor,
1057
+ colors,
1058
+ hasGradientBackground,
1059
+ variant,
1060
+ }),
1061
+ [accentColor, colors, hasGradientBackground, variant]
1062
+ );
1063
+
1064
+ React.useEffect(() => {
1065
+ if (interactionDisabled || resolvedPressEffect === 'none') {
1066
+ pressSv.value = 0;
1067
+ }
1068
+ }, [interactionDisabled, pressSv, resolvedPressEffect]);
1069
+
1070
+ React.useEffect(() => {
1071
+ if (hideSpinnerTimerRef.current) {
1072
+ clearTimeout(hideSpinnerTimerRef.current);
1073
+ hideSpinnerTimerRef.current = null;
1074
+ }
1075
+
1076
+ if (loading) {
1077
+ setSpinnerMounted(true);
1078
+ }
1079
+
1080
+ loadingSv.value = withTiming(loading ? 1 : 0, LOADING_TIMING);
1081
+
1082
+ if (!loading) {
1083
+ hideSpinnerTimerRef.current = setTimeout(() => {
1084
+ setSpinnerMounted(false);
1085
+ }, LOADING_TIMING.duration);
1086
+ }
1087
+
1088
+ return () => {
1089
+ if (hideSpinnerTimerRef.current) {
1090
+ clearTimeout(hideSpinnerTimerRef.current);
1091
+ hideSpinnerTimerRef.current = null;
1092
+ }
1093
+ };
1094
+ }, [loading, loadingSv]);
1095
+
1096
+ const rootAnimatedStyle = useAnimatedStyle(() => {
1097
+ const baseOpacity = visualDisabled ? DISABLED_OPACITY : 1;
1098
+ const scale = pressScaleEnabled
1099
+ ? interpolate(pressSv.value, [0, 1], [1, PRESSED_SCALE])
1100
+ : 1;
1101
+ const opacity = pressOpacityEnabled
1102
+ ? interpolate(pressSv.value, [0, 1], [baseOpacity, PRESSED_OPACITY])
1103
+ : baseOpacity;
1104
+
1105
+ return {
1106
+ opacity,
1107
+ transform: [{ scale }],
1108
+ };
1109
+ }, [pressOpacityEnabled, pressScaleEnabled, visualDisabled]);
1110
+
1111
+ const overlayAnimatedStyle = useAnimatedStyle(() => {
1112
+ const opacity = pressHighlightEnabled
1113
+ ? interpolate(pressSv.value, [0, 1], [0, pressOverlay.strength])
1114
+ : 0;
1115
+ return { opacity };
1116
+ }, [pressHighlightEnabled, pressOverlay.strength]);
1117
+
1118
+ const inlineSpinnerAnimatedStyle = useAnimatedStyle(() => {
1119
+ const width = interpolate(loadingSv.value, [0, 1], [0, loadingSize]);
1120
+ const marginRight = interpolate(loadingSv.value, [0, 1], [0, hasText ? contentGap : 0]);
1121
+ const opacity = interpolate(loadingSv.value, [0, 1], [0, 1]);
1122
+ const scale = interpolate(loadingSv.value, [0, 1], [0.92, 1]);
1123
+
1124
+ return {
1125
+ marginRight,
1126
+ opacity,
1127
+ transform: [{ scale }],
1128
+ width,
1129
+ };
1130
+ }, [contentGap, hasText, loadingSize, loadingSv]);
1131
+
1132
+ const centeredSpinnerAnimatedStyle = useAnimatedStyle(() => {
1133
+ const opacity = interpolate(loadingSv.value, [0, 1], [0, 1]);
1134
+ const scale = interpolate(loadingSv.value, [0, 1], [0.92, 1]);
1135
+ return {
1136
+ opacity,
1137
+ transform: [{ scale }],
1138
+ };
1139
+ }, [loadingSv]);
1140
+
1141
+ const iconAnimatedStyle = useAnimatedStyle(() => {
1142
+ if (!hasIcon || iconOnly) return { opacity: 0, width: 0 };
1143
+
1144
+ const margin = interpolate(loadingSv.value, [0, 1], [hasText ? contentGap : 0, 0]);
1145
+ const opacity = interpolate(loadingSv.value, [0, 1], [1, 0]);
1146
+ const scale = interpolate(loadingSv.value, [0, 1], [1, 0.92]);
1147
+ const width = interpolate(loadingSv.value, [0, 1], [iconSize, 0]);
1148
+
1149
+ return {
1150
+ opacity,
1151
+ transform: [{ scale }],
1152
+ width,
1153
+ ...(iconPlacement === 'end' ? { marginLeft: margin } : { marginRight: margin }),
1154
+ };
1155
+ }, [contentGap, hasIcon, hasText, iconOnly, iconPlacement, iconSize, loadingSv]);
1156
+
1157
+ const contentAnimatedStyle = useAnimatedStyle(() => {
1158
+ if (centeredLoading || iconOnly) {
1159
+ const opacity = interpolate(loadingSv.value, [0, 1], [1, 0]);
1160
+ const scale = interpolate(loadingSv.value, [0, 1], [1, 0.98]);
1161
+ return { opacity, transform: [{ scale }] };
1162
+ }
1163
+
1164
+ const translateX = interpolate(loadingSv.value, [0, 1], [0, metrics.linkPaddingVertical]);
1165
+ return { transform: [{ translateX }] };
1166
+ }, [centeredLoading, iconOnly, loadingSv, metrics.linkPaddingVertical]);
1167
+
1168
+ const spinnerVisible = loading || spinnerMounted;
1169
+ const showContent = !iconOnly;
1170
+
1171
+ const handlePress = React.useCallback(
1172
+ (event: GestureResponderEvent) => {
1173
+ if (interactionDisabled) return;
1174
+ onPress?.(event);
1175
+ },
1176
+ [interactionDisabled, onPress]
1177
+ );
1178
+
1179
+ const handlePressIn = React.useCallback(
1180
+ (event: GestureResponderEvent) => {
1181
+ if (!interactionDisabled && resolvedPressEffect !== 'none') {
1182
+ pressSv.value = withTiming(1, PRESS_TIMING);
1183
+ }
1184
+ onPressIn?.(event);
1185
+ },
1186
+ [interactionDisabled, onPressIn, pressSv, resolvedPressEffect]
1187
+ );
1188
+
1189
+ const handlePressOut = React.useCallback(
1190
+ (event: GestureResponderEvent) => {
1191
+ if (!interactionDisabled && resolvedPressEffect !== 'none') {
1192
+ pressSv.value = withTiming(0, PRESS_TIMING);
1193
+ }
1194
+ onPressOut?.(event);
1195
+ },
1196
+ [interactionDisabled, onPressOut, pressSv, resolvedPressEffect]
1197
+ );
1198
+
1199
+ return (
1200
+ <AnimatedPressable
1201
+ {...pressableProps}
1202
+ ref={ref}
1203
+ accessibilityLabel={resolvedAccessibilityLabel}
1204
+ accessibilityRole="button"
1205
+ accessibilityState={{
1206
+ ...accessibilityState,
1207
+ busy: Boolean(loading || accessibilityState?.busy),
1208
+ disabled: Boolean(interactionDisabled || accessibilityState?.disabled),
1209
+ }}
1210
+ disabled={interactionDisabled}
1211
+ hitSlop={hitSlop ?? defaultHitSlop}
1212
+ onPress={handlePress}
1213
+ onPressIn={handlePressIn}
1214
+ onPressOut={handlePressOut}
1215
+ style={[styles.root, rootSizeStyle, radiusStyle, shadowStyle, rootAnimatedStyle, webCursorStyle, style]}
1216
+ testID={testID}
1217
+ >
1218
+ <View
1219
+ pointerEvents="none"
1220
+ style={[
1221
+ styles.content,
1222
+ contentSizeStyle,
1223
+ paddingStyle,
1224
+ radiusStyle,
1225
+ borderStyle,
1226
+ { backgroundColor: visualColors.backgroundColor },
1227
+ contentStyle,
1228
+ ]}
1229
+ >
1230
+ {LinearGradientComponent && gradientCfg ? (
1231
+ <LinearGradientComponent
1232
+ pointerEvents="none"
1233
+ colors={gradientCfg.colors}
1234
+ start={gradientCfg.start}
1235
+ end={gradientCfg.end}
1236
+ style={[StyleSheet.absoluteFillObject, radiusStyle]}
1237
+ />
1238
+ ) : null}
1239
+
1240
+ <Animated.View
1241
+ pointerEvents="none"
1242
+ style={[
1243
+ StyleSheet.absoluteFillObject,
1244
+ radiusStyle,
1245
+ { backgroundColor: pressOverlay.color },
1246
+ overlayAnimatedStyle,
1247
+ ]}
1248
+ />
1249
+
1250
+ {iconOnly ? (
1251
+ <>
1252
+ <Animated.View style={[styles.centerOverlay, centeredSpinnerAnimatedStyle]}>
1253
+ {spinnerVisible ? (
1254
+ <LoadingSpinner
1255
+ animating={spinnerVisible}
1256
+ color={visualColors.loadingColor}
1257
+ size={loadingSize}
1258
+ />
1259
+ ) : null}
1260
+ </Animated.View>
1261
+ {icon ?? resolvedText ? (
1262
+ <Animated.View style={contentAnimatedStyle}>{icon ?? resolvedText}</Animated.View>
1263
+ ) : null}
1264
+ </>
1265
+ ) : centeredLoading ? (
1266
+ <>
1267
+ <Animated.View style={[styles.centerContent, contentAnimatedStyle]}>
1268
+ {icon && iconPlacement === 'start' ? (
1269
+ <View style={[iconBaseBoxStyle, iconPlacedBoxStyle]}>{icon}</View>
1270
+ ) : null}
1271
+ {showContent ? resolvedText : null}
1272
+ {icon && iconPlacement === 'end' ? (
1273
+ <View style={[iconBaseBoxStyle, iconPlacedBoxStyle]}>{icon}</View>
1274
+ ) : null}
1275
+ </Animated.View>
1276
+ <Animated.View style={[styles.centerOverlay, centeredSpinnerAnimatedStyle]}>
1277
+ {spinnerVisible ? (
1278
+ <LoadingSpinner
1279
+ animating={spinnerVisible}
1280
+ color={visualColors.loadingColor}
1281
+ size={loadingSize}
1282
+ />
1283
+ ) : null}
1284
+ </Animated.View>
1285
+ </>
1286
+ ) : (
1287
+ <>
1288
+ <Animated.View style={[styles.spinnerBox, { height: loadingSize }, inlineSpinnerAnimatedStyle]}>
1289
+ {spinnerVisible ? (
1290
+ <LoadingSpinner
1291
+ animating={spinnerVisible}
1292
+ color={visualColors.loadingColor}
1293
+ size={loadingSize}
1294
+ />
1295
+ ) : null}
1296
+ </Animated.View>
1297
+
1298
+ {icon && iconPlacement === 'start' ? (
1299
+ <Animated.View style={[iconBaseBoxStyle, iconAnimatedStyle]}>{icon}</Animated.View>
1300
+ ) : null}
1301
+
1302
+ {resolvedText ? <Animated.View style={contentAnimatedStyle}>{resolvedText}</Animated.View> : null}
1303
+
1304
+ {icon && iconPlacement === 'end' ? (
1305
+ <Animated.View style={[iconBaseBoxStyle, iconAnimatedStyle]}>{icon}</Animated.View>
1306
+ ) : null}
1307
+ </>
1308
+ )}
1309
+ </View>
1310
+ </AnimatedPressable>
1311
+ );
1312
+ }
1313
+
1314
+ const ButtonWithRef = React.forwardRef<ButtonRef, ButtonProps>(ButtonImpl);
1315
+ ButtonWithRef.displayName = 'Button';
1316
+
1317
+ export const Button = React.memo(ButtonWithRef);
1318
+ Button.displayName = 'Button';
1319
+
1320
+ const styles = StyleSheet.create({
1321
+ centerContent: {
1322
+ alignItems: 'center',
1323
+ flexDirection: 'row',
1324
+ justifyContent: 'center',
1325
+ },
1326
+ centerOverlay: {
1327
+ alignItems: 'center',
1328
+ bottom: 0,
1329
+ justifyContent: 'center',
1330
+ left: 0,
1331
+ position: 'absolute',
1332
+ right: 0,
1333
+ top: 0,
1334
+ },
1335
+ content: {
1336
+ alignItems: 'center',
1337
+ flexDirection: 'row',
1338
+ justifyContent: 'center',
1339
+ overflow: 'hidden',
1340
+ },
1341
+ linkText: {
1342
+ textDecorationLine: 'underline',
1343
+ },
1344
+ root: {},
1345
+ spinnerBox: {
1346
+ alignItems: 'center',
1347
+ justifyContent: 'center',
1348
+ overflow: 'hidden',
1349
+ },
1350
+ text: {
1351
+ flexShrink: 1,
1352
+ fontWeight: '600',
1353
+ letterSpacing: 0,
1354
+ textAlign: 'center',
1355
+ },
1356
+ });