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,1221 @@
1
+ import * as React from 'react';
2
+ import type {
3
+ GestureResponderEvent,
4
+ PressableStateCallbackType,
5
+ StyleProp,
6
+ TextStyle,
7
+ ViewStyle,
8
+ } from 'react-native';
9
+ import { Pressable, StyleSheet, View, processColor } from 'react-native';
10
+ import Animated, {
11
+ Easing,
12
+ ReduceMotion,
13
+ interpolate,
14
+ useAnimatedStyle,
15
+ useSharedValue,
16
+ withTiming,
17
+ } from 'react-native-reanimated';
18
+ import type { EasingFunction, SharedValue, WithTimingConfig } from 'react-native-reanimated';
19
+ import { wp } from 'zkit-tools';
20
+ import type { Theme } from '../../theme/types';
21
+ import { useTheme } from '../../theme/useTheme';
22
+ import { Text } from '../Text';
23
+
24
+ export type AccordionType = 'single' | 'multiple';
25
+ export type AccordionValue = string | number;
26
+ export type AccordionVariant = 'card' | 'filled' | 'plain';
27
+ export type AccordionTone = 'neutral' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
28
+ export type AccordionSize = 'sm' | 'md' | 'lg';
29
+ export type AccordionMountStrategy = 'eager' | 'lazy' | 'unmountOnExit';
30
+ export type AccordionPressEffect = 'none' | 'opacity' | 'scale' | 'scale-opacity';
31
+
32
+ export type AccordionAnimationConfig = {
33
+ duration?: number;
34
+ easing?: EasingFunction;
35
+ reduceMotion?: ReduceMotion;
36
+ };
37
+
38
+ export type AccordionItemState<T extends AccordionValue = AccordionValue> = {
39
+ open: boolean;
40
+ disabled: boolean;
41
+ value: T;
42
+ toggle: () => void;
43
+ setOpen: (open: boolean) => void;
44
+ };
45
+
46
+ export type AccordionSlot<T extends AccordionValue = AccordionValue> =
47
+ | React.ReactNode
48
+ | ((state: AccordionItemState<T>) => React.ReactNode);
49
+
50
+ export type AccordionTriggerRenderState<T extends AccordionValue = AccordionValue> =
51
+ AccordionItemState<T>;
52
+
53
+ type AccordionStoreListener = () => void;
54
+
55
+ type AccordionMetrics = {
56
+ itemGap: number;
57
+ itemRadius: number;
58
+ triggerMinHeight: number;
59
+ triggerPaddingHorizontal: number;
60
+ triggerPaddingVertical: number;
61
+ triggerGap: number;
62
+ titleFontSize: number;
63
+ titleLineHeight: number;
64
+ descriptionFontSize: number;
65
+ descriptionLineHeight: number;
66
+ contentPaddingHorizontal: number;
67
+ contentPaddingTop: number;
68
+ contentPaddingBottom: number;
69
+ contentOffset: number;
70
+ indicatorSize: number;
71
+ indicatorStrokeWidth: number;
72
+ borderWidth: number;
73
+ };
74
+
75
+ type AccordionColors = {
76
+ accent: string;
77
+ surface: string;
78
+ border: string;
79
+ divider: string;
80
+ title: string;
81
+ description: string;
82
+ disabled: string;
83
+ };
84
+
85
+ type ResolvedAccordionAnimation = {
86
+ duration: number;
87
+ easing: EasingFunction;
88
+ timing: WithTimingConfig;
89
+ };
90
+
91
+ type AccordionContextValue<T extends AccordionValue> = {
92
+ store: AccordionStore<T>;
93
+ type: AccordionType;
94
+ collapsible: boolean;
95
+ disabled: boolean;
96
+ variant: AccordionVariant;
97
+ size: AccordionSize;
98
+ tone: AccordionTone;
99
+ mountStrategy: AccordionMountStrategy;
100
+ metrics: AccordionMetrics;
101
+ colors: AccordionColors;
102
+ animation: ResolvedAccordionAnimation;
103
+ itemStyle?: StyleProp<ViewStyle>;
104
+ setItemOpen: (value: T, open: boolean) => void;
105
+ };
106
+
107
+ type AccordionItemContextValue<T extends AccordionValue> = AccordionItemState<T> & {
108
+ openProgress: SharedValue<number>;
109
+ metrics: AccordionMetrics;
110
+ colors: AccordionColors;
111
+ animation: ResolvedAccordionAnimation;
112
+ mountStrategy: AccordionMountStrategy;
113
+ triggerId: string;
114
+ contentId: string;
115
+ };
116
+
117
+ const DEFAULT_DURATION = 220;
118
+ const DEFAULT_EASING = Easing.out(Easing.cubic);
119
+ const PRESS_IN_DURATION = 90;
120
+ const PRESS_OUT_DURATION = 140;
121
+
122
+ const SEMANTIC_COLORS: Record<string, string> = {
123
+ danger: '#DC2626',
124
+ error: '#DC2626',
125
+ info: '#2563EB',
126
+ success: '#16A34A',
127
+ warn: '#D97706',
128
+ warning: '#D97706',
129
+ };
130
+
131
+ const SIZE_TOKENS = {
132
+ sm: {
133
+ itemGap: 8,
134
+ itemRadius: 12,
135
+ triggerMinHeight: 44,
136
+ triggerPaddingHorizontal: 12,
137
+ triggerPaddingVertical: 10,
138
+ triggerGap: 8,
139
+ titleFontSize: 14,
140
+ titleLineHeight: 19,
141
+ descriptionFontSize: 12,
142
+ descriptionLineHeight: 17,
143
+ contentPaddingHorizontal: 12,
144
+ contentPaddingTop: 8,
145
+ contentPaddingBottom: 12,
146
+ contentOffset: 4,
147
+ indicatorSize: 20,
148
+ indicatorStrokeWidth: 2,
149
+ },
150
+ md: {
151
+ itemGap: 10,
152
+ itemRadius: 14,
153
+ triggerMinHeight: 46,
154
+ triggerPaddingHorizontal: 14,
155
+ triggerPaddingVertical: 12,
156
+ triggerGap: 10,
157
+ titleFontSize: 15,
158
+ titleLineHeight: 21,
159
+ descriptionFontSize: 13,
160
+ descriptionLineHeight: 18,
161
+ contentPaddingHorizontal: 14,
162
+ contentPaddingTop: 10,
163
+ contentPaddingBottom: 14,
164
+ contentOffset: 5,
165
+ indicatorSize: 22,
166
+ indicatorStrokeWidth: 2,
167
+ },
168
+ lg: {
169
+ itemGap: 12,
170
+ itemRadius: 16,
171
+ triggerMinHeight: 52,
172
+ triggerPaddingHorizontal: 16,
173
+ triggerPaddingVertical: 14,
174
+ triggerGap: 12,
175
+ titleFontSize: 16,
176
+ titleLineHeight: 23,
177
+ descriptionFontSize: 14,
178
+ descriptionLineHeight: 20,
179
+ contentPaddingHorizontal: 16,
180
+ contentPaddingTop: 12,
181
+ contentPaddingBottom: 16,
182
+ contentOffset: 6,
183
+ indicatorSize: 24,
184
+ indicatorStrokeWidth: 2.25,
185
+ },
186
+ } as const satisfies Record<AccordionSize, Omit<AccordionMetrics, 'borderWidth'>>;
187
+
188
+ const AccordionContext = React.createContext<AccordionContextValue<AccordionValue> | null>(null);
189
+ const AccordionItemContext = React.createContext<AccordionItemContextValue<AccordionValue> | null>(null);
190
+
191
+ function sameValue(a: AccordionValue, b: AccordionValue) {
192
+ return Object.is(a, b) || a === b;
193
+ }
194
+
195
+ function hasValue<T extends AccordionValue>(values: readonly T[], value: T) {
196
+ return values.some((item) => sameValue(item, value));
197
+ }
198
+
199
+ function uniqueValues<T extends AccordionValue>(values: readonly T[]) {
200
+ const next: T[] = [];
201
+ for (const value of values) {
202
+ if (!hasValue(next, value)) next.push(value);
203
+ }
204
+ return next;
205
+ }
206
+
207
+ function areValuesEqual<T extends AccordionValue>(a: readonly T[], b: readonly T[]) {
208
+ if (a.length !== b.length) return false;
209
+ for (let i = 0; i < a.length; i += 1) {
210
+ if (!sameValue(a[i], b[i])) return false;
211
+ }
212
+ return true;
213
+ }
214
+
215
+ function normalizePropValues<T extends AccordionValue>(
216
+ type: AccordionType,
217
+ value: T | readonly T[] | null | undefined
218
+ ) {
219
+ if (type === 'multiple') {
220
+ return Array.isArray(value) ? uniqueValues(value) : [];
221
+ }
222
+ if (value == null || Array.isArray(value)) return [];
223
+ return [value];
224
+ }
225
+
226
+ function getNextValues<T extends AccordionValue>(
227
+ current: readonly T[],
228
+ value: T,
229
+ open: boolean,
230
+ type: AccordionType,
231
+ collapsible: boolean
232
+ ) {
233
+ const isOpen = hasValue(current, value);
234
+
235
+ if (type === 'single') {
236
+ if (open) return isOpen ? current.slice() : [value];
237
+ if (!isOpen) return current.slice();
238
+ return collapsible ? [] : current.slice();
239
+ }
240
+
241
+ if (open) return isOpen ? current.slice() : [...current, value];
242
+ return isOpen ? current.filter((item) => !sameValue(item, value)) : current.slice();
243
+ }
244
+
245
+ function resolveDuration(duration: number | undefined) {
246
+ if (duration == null || !Number.isFinite(duration)) return DEFAULT_DURATION;
247
+ return Math.max(0, duration);
248
+ }
249
+
250
+ function resolveAnimation(animation: AccordionAnimationConfig | false | undefined): ResolvedAccordionAnimation {
251
+ const duration = animation === false ? 0 : resolveDuration(animation?.duration);
252
+ const easing = animation === false ? DEFAULT_EASING : animation?.easing ?? DEFAULT_EASING;
253
+ const reduceMotion = animation === false ? ReduceMotion.Always : animation?.reduceMotion ?? ReduceMotion.System;
254
+
255
+ return {
256
+ duration,
257
+ easing,
258
+ timing: {
259
+ duration,
260
+ easing,
261
+ reduceMotion,
262
+ },
263
+ };
264
+ }
265
+
266
+ function resolveMetrics(size: AccordionSize): AccordionMetrics {
267
+ const token = SIZE_TOKENS[size] ?? SIZE_TOKENS.md;
268
+ return {
269
+ itemGap: wp(token.itemGap),
270
+ itemRadius: wp(token.itemRadius),
271
+ triggerMinHeight: wp(token.triggerMinHeight),
272
+ triggerPaddingHorizontal: wp(token.triggerPaddingHorizontal),
273
+ triggerPaddingVertical: wp(token.triggerPaddingVertical),
274
+ triggerGap: wp(token.triggerGap),
275
+ titleFontSize: wp(token.titleFontSize),
276
+ titleLineHeight: wp(token.titleLineHeight),
277
+ descriptionFontSize: wp(token.descriptionFontSize),
278
+ descriptionLineHeight: wp(token.descriptionLineHeight),
279
+ contentPaddingHorizontal: wp(token.contentPaddingHorizontal),
280
+ contentPaddingTop: wp(token.contentPaddingTop),
281
+ contentPaddingBottom: wp(token.contentPaddingBottom),
282
+ contentOffset: wp(token.contentOffset),
283
+ indicatorSize: wp(token.indicatorSize),
284
+ indicatorStrokeWidth: wp(token.indicatorStrokeWidth),
285
+ borderWidth: wp(1),
286
+ };
287
+ }
288
+
289
+ function isProcessableColor(color: string) {
290
+ return processColor(color) != null;
291
+ }
292
+
293
+ function colorToRgba(color: string, alpha: number) {
294
+ const processed = processColor(color);
295
+ if (typeof processed !== 'number') return undefined;
296
+
297
+ const normalized = processed >>> 0;
298
+ const r = (normalized >> 16) & 255;
299
+ const g = (normalized >> 8) & 255;
300
+ const b = normalized & 255;
301
+ const a = Math.max(0, Math.min(1, alpha));
302
+ return `rgba(${r},${g},${b},${a})`;
303
+ }
304
+
305
+ function resolveColorToken(input: string | undefined, fallback: string, theme: Theme) {
306
+ if (input == null) return fallback;
307
+
308
+ const key = input.trim();
309
+ if (!key) return fallback;
310
+
311
+ const resolved =
312
+ key === 'primary'
313
+ ? theme.colors.primary
314
+ : key === 'onPrimary'
315
+ ? theme.colors.onPrimary
316
+ : key === 'secondary' || key === 'neutral'
317
+ ? theme.colors.secondary
318
+ : key === 'onSecondary'
319
+ ? theme.colors.onSecondary
320
+ : key === 'surface'
321
+ ? theme.colors.surface
322
+ : key === 'onSurface'
323
+ ? theme.colors.onSurface
324
+ : key === 'border'
325
+ ? theme.colors.border
326
+ : key === 'muted'
327
+ ? theme.colors.muted
328
+ : key === 'disabled'
329
+ ? theme.colors.disabled
330
+ : SEMANTIC_COLORS[key] ?? key;
331
+
332
+ return isProcessableColor(resolved) ? resolved : fallback;
333
+ }
334
+
335
+ function resolveToneColor(tone: AccordionTone, theme: Theme) {
336
+ if (tone === 'primary') return theme.colors.primary;
337
+ if (tone === 'success') return SEMANTIC_COLORS.success;
338
+ if (tone === 'warning') return SEMANTIC_COLORS.warning;
339
+ if (tone === 'danger') return SEMANTIC_COLORS.danger;
340
+ if (tone === 'info') return SEMANTIC_COLORS.info;
341
+ return theme.colors.muted;
342
+ }
343
+
344
+ function resolveColors(
345
+ variant: AccordionVariant,
346
+ tone: AccordionTone,
347
+ color: string | undefined,
348
+ theme: Theme
349
+ ): AccordionColors {
350
+ const accent = resolveColorToken(color, resolveToneColor(tone, theme), theme);
351
+ const accentSoft = colorToRgba(accent, 0.08);
352
+ const dividerSoft = colorToRgba(accent, 0.16);
353
+
354
+ if (variant === 'filled') {
355
+ return {
356
+ accent,
357
+ surface: tone === 'neutral' ? theme.colors.secondary : accentSoft ?? theme.colors.secondary,
358
+ border: 'transparent',
359
+ divider: dividerSoft ?? theme.colors.border,
360
+ title: theme.colors.onSurface,
361
+ description: theme.colors.muted,
362
+ disabled: theme.colors.disabled,
363
+ };
364
+ }
365
+
366
+ if (variant === 'plain') {
367
+ return {
368
+ accent,
369
+ surface: 'transparent',
370
+ border: 'transparent',
371
+ divider: theme.colors.border,
372
+ title: theme.colors.onSurface,
373
+ description: theme.colors.muted,
374
+ disabled: theme.colors.disabled,
375
+ };
376
+ }
377
+
378
+ return {
379
+ accent,
380
+ surface: theme.colors.surface,
381
+ border: theme.colors.border,
382
+ divider: theme.colors.border,
383
+ title: theme.colors.onSurface,
384
+ description: theme.colors.muted,
385
+ disabled: theme.colors.disabled,
386
+ };
387
+ }
388
+
389
+ function resolveSlot<T extends AccordionValue>(
390
+ slot: AccordionSlot<T> | undefined,
391
+ state: AccordionItemState<T>
392
+ ) {
393
+ return typeof slot === 'function' ? slot(state) : slot;
394
+ }
395
+
396
+ function hasRenderableNode(node: React.ReactNode) {
397
+ return node !== null && node !== undefined && node !== false;
398
+ }
399
+
400
+ function isPrimitiveTextNode(node: React.ReactNode): node is string | number {
401
+ return typeof node === 'string' || typeof node === 'number';
402
+ }
403
+
404
+ function sanitizeNativeId(input: string) {
405
+ return input.replace(/[^a-zA-Z0-9_-]/g, '');
406
+ }
407
+
408
+ function useAccordionContext<T extends AccordionValue = AccordionValue>() {
409
+ const ctx = React.useContext(AccordionContext);
410
+ if (!ctx) {
411
+ throw new Error('[zkit-ui] Accordion components must be wrapped in <Accordion />');
412
+ }
413
+ return ctx as unknown as AccordionContextValue<T>;
414
+ }
415
+
416
+ function useAccordionItemContext<T extends AccordionValue = AccordionValue>() {
417
+ const ctx = React.useContext(AccordionItemContext);
418
+ if (!ctx) {
419
+ throw new Error('[zkit-ui] AccordionTrigger/Content/Indicator must be wrapped in <AccordionItem />');
420
+ }
421
+ return ctx as AccordionItemContextValue<T>;
422
+ }
423
+
424
+ class AccordionStore<T extends AccordionValue> {
425
+ private values: T[];
426
+ private listeners = new Set<AccordionStoreListener>();
427
+
428
+ constructor(initialValues: readonly T[]) {
429
+ this.values = uniqueValues(initialValues);
430
+ }
431
+
432
+ subscribe = (listener: AccordionStoreListener) => {
433
+ this.listeners.add(listener);
434
+ return () => {
435
+ this.listeners.delete(listener);
436
+ };
437
+ };
438
+
439
+ getValues = () => this.values;
440
+
441
+ isOpen = (value: T) => hasValue(this.values, value);
442
+
443
+ setValues = (values: readonly T[]) => {
444
+ const next = uniqueValues(values);
445
+ if (areValuesEqual(this.values, next)) return false;
446
+
447
+ this.values = next;
448
+ this.emit();
449
+ return true;
450
+ };
451
+
452
+ private emit() {
453
+ for (const listener of this.listeners) listener();
454
+ }
455
+ }
456
+
457
+ type AccordionBaseProps = Omit<React.ComponentPropsWithoutRef<typeof View>, 'children' | 'style'> & {
458
+ children: React.ReactNode;
459
+ disabled?: boolean;
460
+ variant?: AccordionVariant;
461
+ tone?: AccordionTone;
462
+ size?: AccordionSize;
463
+ color?: string;
464
+ animation?: AccordionAnimationConfig | false;
465
+ mountStrategy?: AccordionMountStrategy;
466
+ itemGap?: number;
467
+ itemStyle?: StyleProp<ViewStyle>;
468
+ style?: StyleProp<ViewStyle>;
469
+ };
470
+
471
+ export type AccordionSingleProps<T extends AccordionValue = AccordionValue> = AccordionBaseProps & {
472
+ type?: 'single';
473
+ collapsible?: boolean;
474
+ value?: T | null;
475
+ defaultValue?: T | null;
476
+ onChange?: (value: T | null) => void;
477
+ };
478
+
479
+ export type AccordionMultipleProps<T extends AccordionValue = AccordionValue> = AccordionBaseProps & {
480
+ type: 'multiple';
481
+ value?: readonly T[];
482
+ defaultValue?: readonly T[];
483
+ onChange?: (value: T[]) => void;
484
+ };
485
+
486
+ export type AccordionProps<T extends AccordionValue = AccordionValue> =
487
+ | AccordionSingleProps<T>
488
+ | AccordionMultipleProps<T>;
489
+
490
+ export function Accordion<T extends AccordionValue = AccordionValue>(props: AccordionProps<T>) {
491
+ const {
492
+ children,
493
+ disabled = false,
494
+ variant = 'card',
495
+ tone = 'neutral',
496
+ size = 'md',
497
+ color,
498
+ animation,
499
+ mountStrategy = 'eager',
500
+ itemGap,
501
+ itemStyle,
502
+ style,
503
+ ...viewProps
504
+ } = props;
505
+ const theme = useTheme();
506
+
507
+ const type: AccordionType = props.type ?? 'single';
508
+ const collapsible = type === 'single' ? (props as AccordionSingleProps<T>).collapsible ?? true : true;
509
+ const isControlled = props.value !== undefined;
510
+ const controlledValues = React.useMemo(
511
+ () => (isControlled ? normalizePropValues(type, props.value as T | readonly T[] | null | undefined) : []),
512
+ [isControlled, props.value, type]
513
+ );
514
+
515
+ const storeRef = React.useRef<AccordionStore<T> | null>(null);
516
+ if (!storeRef.current) {
517
+ const initialValue = isControlled ? props.value : props.defaultValue;
518
+ storeRef.current = new AccordionStore<T>(
519
+ normalizePropValues(type, initialValue as T | readonly T[] | null | undefined)
520
+ );
521
+ }
522
+ const store = storeRef.current;
523
+ const onChange = props.onChange;
524
+
525
+ React.useLayoutEffect(() => {
526
+ if (!isControlled) return;
527
+ store.setValues(controlledValues);
528
+ }, [controlledValues, isControlled, store]);
529
+
530
+ React.useLayoutEffect(() => {
531
+ if (isControlled || type !== 'single') return;
532
+ const current = store.getValues();
533
+ if (current.length > 1) store.setValues(current.slice(0, 1));
534
+ }, [isControlled, store, type]);
535
+
536
+ const emitChange = React.useCallback(
537
+ (next: readonly T[]) => {
538
+ if (type === 'multiple') {
539
+ (onChange as ((value: T[]) => void) | undefined)?.(next.slice());
540
+ return;
541
+ }
542
+ (onChange as ((value: T | null) => void) | undefined)?.(next[0] ?? null);
543
+ },
544
+ [onChange, type]
545
+ );
546
+
547
+ const setItemOpen = React.useCallback(
548
+ (value: T, open: boolean) => {
549
+ if (disabled) return;
550
+
551
+ const current = store.getValues();
552
+ const next = getNextValues(current, value, open, type, collapsible);
553
+ if (areValuesEqual(current, next)) return;
554
+
555
+ if (!isControlled) store.setValues(next);
556
+ emitChange(next);
557
+ },
558
+ [collapsible, disabled, emitChange, isControlled, store, type]
559
+ );
560
+
561
+ const metrics = React.useMemo(() => {
562
+ const resolved = resolveMetrics(size);
563
+ return itemGap === undefined ? resolved : { ...resolved, itemGap };
564
+ }, [itemGap, size]);
565
+ const colors = React.useMemo(() => resolveColors(variant, tone, color, theme), [color, theme, tone, variant]);
566
+ const resolvedAnimation = React.useMemo(() => resolveAnimation(animation), [animation]);
567
+ const rootStyle = React.useMemo(() => [{ gap: metrics.itemGap }, style], [metrics.itemGap, style]);
568
+
569
+ const ctx = React.useMemo<AccordionContextValue<T>>(
570
+ () => ({
571
+ store,
572
+ type,
573
+ collapsible,
574
+ disabled,
575
+ variant,
576
+ size,
577
+ tone,
578
+ mountStrategy,
579
+ metrics,
580
+ colors,
581
+ animation: resolvedAnimation,
582
+ itemStyle,
583
+ setItemOpen,
584
+ }),
585
+ [
586
+ store,
587
+ type,
588
+ collapsible,
589
+ disabled,
590
+ variant,
591
+ size,
592
+ tone,
593
+ mountStrategy,
594
+ metrics,
595
+ colors,
596
+ resolvedAnimation,
597
+ itemStyle,
598
+ setItemOpen,
599
+ ]
600
+ );
601
+
602
+ return (
603
+ <AccordionContext.Provider value={ctx as unknown as AccordionContextValue<AccordionValue>}>
604
+ <View {...viewProps} style={rootStyle}>
605
+ {children}
606
+ </View>
607
+ </AccordionContext.Provider>
608
+ );
609
+ }
610
+
611
+ export type AccordionItemProps<T extends AccordionValue = AccordionValue> = Omit<
612
+ React.ComponentPropsWithoutRef<typeof View>,
613
+ 'children' | 'style'
614
+ > & {
615
+ value: T;
616
+ disabled?: boolean;
617
+ style?: StyleProp<ViewStyle>;
618
+ children: React.ReactNode;
619
+ };
620
+
621
+ export function AccordionItem<T extends AccordionValue = AccordionValue>({
622
+ value,
623
+ disabled = false,
624
+ style,
625
+ children,
626
+ accessibilityState,
627
+ ...viewProps
628
+ }: AccordionItemProps<T>) {
629
+ const accordion = useAccordionContext<T>();
630
+ const reactId = React.useId();
631
+ const idBase = React.useMemo(() => `zkit-accordion-${sanitizeNativeId(reactId)}`, [reactId]);
632
+ const triggerId = `${idBase}-trigger`;
633
+ const contentId = `${idBase}-content`;
634
+
635
+ const getOpenSnapshot = React.useCallback(() => accordion.store.isOpen(value), [accordion.store, value]);
636
+ const open = React.useSyncExternalStore(
637
+ accordion.store.subscribe,
638
+ getOpenSnapshot,
639
+ getOpenSnapshot
640
+ );
641
+ const resolvedDisabled = accordion.disabled || disabled;
642
+ const openProgress = useSharedValue(open ? 1 : 0);
643
+
644
+ React.useEffect(() => {
645
+ openProgress.value = withTiming(open ? 1 : 0, accordion.animation.timing);
646
+ }, [accordion.animation, open, openProgress]);
647
+
648
+ const setOpen = React.useCallback(
649
+ (nextOpen: boolean) => {
650
+ if (resolvedDisabled) return;
651
+ accordion.setItemOpen(value, nextOpen);
652
+ },
653
+ [accordion, resolvedDisabled, value]
654
+ );
655
+
656
+ const toggle = React.useCallback(() => {
657
+ setOpen(!open);
658
+ }, [open, setOpen]);
659
+
660
+ const itemCtx = React.useMemo<AccordionItemContextValue<T>>(
661
+ () => ({
662
+ open,
663
+ disabled: resolvedDisabled,
664
+ value,
665
+ toggle,
666
+ setOpen,
667
+ openProgress,
668
+ metrics: accordion.metrics,
669
+ colors: accordion.colors,
670
+ animation: accordion.animation,
671
+ mountStrategy: accordion.mountStrategy,
672
+ triggerId,
673
+ contentId,
674
+ }),
675
+ [
676
+ accordion.animation,
677
+ accordion.colors,
678
+ accordion.metrics,
679
+ accordion.mountStrategy,
680
+ contentId,
681
+ open,
682
+ openProgress,
683
+ resolvedDisabled,
684
+ setOpen,
685
+ toggle,
686
+ triggerId,
687
+ value,
688
+ ]
689
+ );
690
+
691
+ const itemStyle = React.useMemo(
692
+ () => [
693
+ styles.item,
694
+ {
695
+ backgroundColor: accordion.colors.surface,
696
+ borderColor: accordion.colors.border,
697
+ borderRadius: accordion.metrics.itemRadius,
698
+ borderWidth: accordion.variant === 'plain' ? 0 : accordion.metrics.borderWidth,
699
+ },
700
+ accordion.itemStyle,
701
+ style,
702
+ ],
703
+ [
704
+ accordion.colors.border,
705
+ accordion.colors.surface,
706
+ accordion.itemStyle,
707
+ accordion.metrics.borderWidth,
708
+ accordion.metrics.itemRadius,
709
+ accordion.variant,
710
+ style,
711
+ ]
712
+ );
713
+
714
+ return (
715
+ <AccordionItemContext.Provider value={itemCtx as AccordionItemContextValue<AccordionValue>}>
716
+ <Animated.View
717
+ {...viewProps}
718
+ accessibilityState={{
719
+ ...accessibilityState,
720
+ disabled: resolvedDisabled,
721
+ expanded: open,
722
+ }}
723
+ style={itemStyle}
724
+ >
725
+ {children}
726
+ </Animated.View>
727
+ </AccordionItemContext.Provider>
728
+ );
729
+ }
730
+
731
+ type NativeTriggerProps = Omit<
732
+ React.ComponentPropsWithoutRef<typeof Pressable>,
733
+ 'children' | 'disabled' | 'style'
734
+ >;
735
+
736
+ export type AccordionTriggerProps<T extends AccordionValue = AccordionValue> = NativeTriggerProps & {
737
+ children?: AccordionSlot<T>;
738
+ title?: AccordionSlot<T>;
739
+ description?: AccordionSlot<T>;
740
+ leading?: AccordionSlot<T>;
741
+ trailing?: AccordionSlot<T>;
742
+ indicator?: AccordionSlot<T> | false;
743
+ disabled?: boolean;
744
+ pressEffect?: AccordionPressEffect;
745
+ titleNumberOfLines?: number;
746
+ descriptionNumberOfLines?: number;
747
+ style?: React.ComponentPropsWithoutRef<typeof Pressable>['style'];
748
+ contentStyle?: StyleProp<ViewStyle>;
749
+ textContainerStyle?: StyleProp<ViewStyle>;
750
+ titleStyle?: StyleProp<TextStyle>;
751
+ descriptionStyle?: StyleProp<TextStyle>;
752
+ };
753
+
754
+ export function AccordionTrigger<T extends AccordionValue = AccordionValue>({
755
+ children,
756
+ title,
757
+ description,
758
+ leading,
759
+ trailing,
760
+ indicator,
761
+ disabled: disabledProp = false,
762
+ pressEffect = 'opacity',
763
+ titleNumberOfLines = 1,
764
+ descriptionNumberOfLines = 2,
765
+ style,
766
+ contentStyle,
767
+ textContainerStyle,
768
+ titleStyle,
769
+ descriptionStyle,
770
+ onPress,
771
+ onPressIn,
772
+ onPressOut,
773
+ accessibilityRole,
774
+ accessibilityState,
775
+ nativeID,
776
+ ...pressableProps
777
+ }: AccordionTriggerProps<T>) {
778
+ const item = useAccordionItemContext<T>();
779
+ const disabled = item.disabled || disabledProp;
780
+ const pressSv = useSharedValue(0);
781
+
782
+ const state = React.useMemo<AccordionItemState<T>>(
783
+ () => ({
784
+ open: item.open,
785
+ disabled,
786
+ value: item.value,
787
+ toggle: item.toggle,
788
+ setOpen: item.setOpen,
789
+ }),
790
+ [disabled, item.open, item.setOpen, item.toggle, item.value]
791
+ );
792
+
793
+ const leadingNode = resolveSlot(leading, state);
794
+ const trailingNode = resolveSlot(trailing, state);
795
+ const indicatorNode = indicator === false
796
+ ? null
797
+ : resolveSlot(indicator === undefined ? <AccordionIndicator /> : indicator, state);
798
+ const childNode = resolveSlot(children, state);
799
+ const titleNode = resolveSlot(title, state);
800
+ const descriptionNode = resolveSlot(description, state);
801
+ const hasCustomChildren = hasRenderableNode(childNode);
802
+
803
+ const pressInTiming = React.useMemo(
804
+ () => ({
805
+ duration: PRESS_IN_DURATION,
806
+ easing: item.animation.easing,
807
+ reduceMotion: ReduceMotion.System,
808
+ }),
809
+ [item.animation.easing]
810
+ );
811
+ const pressOutTiming = React.useMemo(
812
+ () => ({
813
+ duration: PRESS_OUT_DURATION,
814
+ easing: item.animation.easing,
815
+ reduceMotion: ReduceMotion.System,
816
+ }),
817
+ [item.animation.easing]
818
+ );
819
+
820
+ React.useEffect(() => {
821
+ if (!disabled) return;
822
+ pressSv.value = withTiming(0, pressOutTiming);
823
+ }, [disabled, pressOutTiming, pressSv]);
824
+
825
+ const animatedContentStyle = useAnimatedStyle(() => {
826
+ const opacityEnabled = pressEffect === 'opacity' || pressEffect === 'scale-opacity';
827
+ const scaleEnabled = pressEffect === 'scale' || pressEffect === 'scale-opacity';
828
+ const opacity = disabled ? 0.52 : opacityEnabled ? interpolate(pressSv.value, [0, 1], [1, 0.86]) : 1;
829
+ const scale = scaleEnabled ? interpolate(pressSv.value, [0, 1], [1, 0.992]) : 1;
830
+
831
+ return {
832
+ opacity,
833
+ transform: [{ scale }],
834
+ };
835
+ }, [disabled, pressEffect]);
836
+
837
+ const renderedTitle = isPrimitiveTextNode(titleNode) ? (
838
+ <Text
839
+ numberOfLines={titleNumberOfLines}
840
+ style={[
841
+ styles.title,
842
+ {
843
+ color: disabled ? item.colors.disabled : item.colors.title,
844
+ fontSize: item.metrics.titleFontSize,
845
+ lineHeight: item.metrics.titleLineHeight,
846
+ },
847
+ titleStyle,
848
+ ]}
849
+ >
850
+ {titleNode}
851
+ </Text>
852
+ ) : (
853
+ titleNode
854
+ );
855
+ const renderedDescription = isPrimitiveTextNode(descriptionNode) ? (
856
+ <Text
857
+ numberOfLines={descriptionNumberOfLines}
858
+ style={[
859
+ styles.description,
860
+ {
861
+ color: disabled ? item.colors.disabled : item.colors.description,
862
+ fontSize: item.metrics.descriptionFontSize,
863
+ lineHeight: item.metrics.descriptionLineHeight,
864
+ },
865
+ descriptionStyle,
866
+ ]}
867
+ >
868
+ {descriptionNode}
869
+ </Text>
870
+ ) : (
871
+ descriptionNode
872
+ );
873
+
874
+ const renderedMain = hasCustomChildren ? (
875
+ childNode
876
+ ) : (
877
+ <View style={[styles.textStack, textContainerStyle]}>
878
+ {hasRenderableNode(renderedTitle) ? renderedTitle : null}
879
+ {hasRenderableNode(renderedDescription) ? renderedDescription : null}
880
+ </View>
881
+ );
882
+
883
+ const resolvedStyle = React.useCallback(
884
+ (stateArg: PressableStateCallbackType) => {
885
+ const userStyle = typeof style === 'function' ? style(stateArg) : style;
886
+ return [
887
+ styles.trigger,
888
+ {
889
+ minHeight: item.metrics.triggerMinHeight,
890
+ paddingHorizontal: item.metrics.triggerPaddingHorizontal,
891
+ paddingVertical: item.metrics.triggerPaddingVertical,
892
+ borderBottomColor: item.open ? item.colors.divider : 'transparent',
893
+ borderBottomWidth: item.metrics.borderWidth,
894
+ },
895
+ userStyle,
896
+ ];
897
+ },
898
+ [
899
+ item.colors.divider,
900
+ item.metrics.borderWidth,
901
+ item.metrics.triggerMinHeight,
902
+ item.metrics.triggerPaddingHorizontal,
903
+ item.metrics.triggerPaddingVertical,
904
+ item.open,
905
+ style,
906
+ ]
907
+ );
908
+
909
+ const handlePress = React.useCallback(
910
+ (event: GestureResponderEvent) => {
911
+ if (disabled) return;
912
+ item.toggle();
913
+ onPress?.(event);
914
+ },
915
+ [disabled, item, onPress]
916
+ );
917
+
918
+ const handlePressIn = React.useCallback(
919
+ (event: GestureResponderEvent) => {
920
+ if (!disabled && pressEffect !== 'none') {
921
+ pressSv.value = withTiming(1, pressInTiming);
922
+ }
923
+ onPressIn?.(event);
924
+ },
925
+ [disabled, onPressIn, pressEffect, pressInTiming, pressSv]
926
+ );
927
+
928
+ const handlePressOut = React.useCallback(
929
+ (event: GestureResponderEvent) => {
930
+ pressSv.value = withTiming(0, pressOutTiming);
931
+ onPressOut?.(event);
932
+ },
933
+ [onPressOut, pressOutTiming, pressSv]
934
+ );
935
+
936
+ return (
937
+ <Pressable
938
+ {...pressableProps}
939
+ nativeID={nativeID ?? item.triggerId}
940
+ accessibilityRole={accessibilityRole ?? 'button'}
941
+ accessibilityState={{
942
+ ...accessibilityState,
943
+ expanded: item.open,
944
+ disabled,
945
+ }}
946
+ disabled={disabled}
947
+ onPress={handlePress}
948
+ onPressIn={handlePressIn}
949
+ onPressOut={handlePressOut}
950
+ style={resolvedStyle}
951
+ >
952
+ <Animated.View
953
+ style={[
954
+ styles.triggerContent,
955
+ { gap: item.metrics.triggerGap },
956
+ animatedContentStyle,
957
+ contentStyle,
958
+ ]}
959
+ >
960
+ {hasRenderableNode(leadingNode) ? <View style={styles.leading}>{leadingNode}</View> : null}
961
+ <View style={styles.main}>{renderedMain}</View>
962
+ {hasRenderableNode(trailingNode) ? <View style={styles.trailing}>{trailingNode}</View> : null}
963
+ {hasRenderableNode(indicatorNode) ? <View style={styles.indicatorSlot}>{indicatorNode}</View> : null}
964
+ </Animated.View>
965
+ </Pressable>
966
+ );
967
+ }
968
+
969
+ export type AccordionContentProps = Omit<
970
+ React.ComponentPropsWithoutRef<typeof View>,
971
+ 'children' | 'style'
972
+ > & {
973
+ children: React.ReactNode;
974
+ mountStrategy?: AccordionMountStrategy;
975
+ style?: StyleProp<ViewStyle>;
976
+ containerStyle?: StyleProp<ViewStyle>;
977
+ };
978
+
979
+ export function AccordionContent({
980
+ children,
981
+ mountStrategy,
982
+ style,
983
+ containerStyle,
984
+ nativeID,
985
+ accessibilityElementsHidden,
986
+ importantForAccessibility,
987
+ onLayout,
988
+ ...viewProps
989
+ }: AccordionContentProps) {
990
+ const item = useAccordionItemContext();
991
+ const strategy = mountStrategy ?? item.mountStrategy;
992
+ const measuredHeight = useSharedValue(0);
993
+ const lastMeasuredHeightRef = React.useRef(0);
994
+ const [hasOpened, setHasOpened] = React.useState(item.open);
995
+ const [renderForExit, setRenderForExit] = React.useState(
996
+ strategy === 'eager' || strategy === 'lazy' || item.open
997
+ );
998
+
999
+ React.useEffect(() => {
1000
+ if (item.open) {
1001
+ setHasOpened(true);
1002
+ setRenderForExit(true);
1003
+ return undefined;
1004
+ }
1005
+
1006
+ if (strategy === 'eager' || strategy === 'lazy') {
1007
+ setRenderForExit(true);
1008
+ return undefined;
1009
+ }
1010
+
1011
+ if (item.animation.duration <= 0) {
1012
+ setRenderForExit(false);
1013
+ return undefined;
1014
+ }
1015
+
1016
+ let cancelled = false;
1017
+ const timeout = setTimeout(() => {
1018
+ if (!cancelled) setRenderForExit(false);
1019
+ }, item.animation.duration);
1020
+
1021
+ return () => {
1022
+ cancelled = true;
1023
+ clearTimeout(timeout);
1024
+ };
1025
+ }, [item.animation.duration, item.open, strategy]);
1026
+
1027
+ const shouldRender =
1028
+ strategy === 'eager' ||
1029
+ item.open ||
1030
+ (strategy === 'lazy' && hasOpened) ||
1031
+ (strategy === 'unmountOnExit' && renderForExit);
1032
+
1033
+ const animatedContainerStyle = useAnimatedStyle(() => {
1034
+ const progress = item.openProgress.value;
1035
+ return {
1036
+ height: measuredHeight.value * progress,
1037
+ opacity: interpolate(progress, [0, 0.2, 1], [0, 1, 1]),
1038
+ };
1039
+ }, []);
1040
+
1041
+ const animatedInnerStyle = useAnimatedStyle(() => {
1042
+ return {
1043
+ transform: [
1044
+ {
1045
+ translateY: interpolate(
1046
+ item.openProgress.value,
1047
+ [0, 1],
1048
+ [-item.metrics.contentOffset, 0]
1049
+ ),
1050
+ },
1051
+ ],
1052
+ };
1053
+ }, [item.metrics.contentOffset]);
1054
+
1055
+ const handleLayout = React.useCallback<NonNullable<AccordionContentProps['onLayout']>>(
1056
+ (event) => {
1057
+ const nextHeight = event.nativeEvent.layout.height;
1058
+ const prevHeight = lastMeasuredHeightRef.current;
1059
+ lastMeasuredHeightRef.current = nextHeight;
1060
+
1061
+ if (item.open && prevHeight > 0 && item.animation.duration > 0) {
1062
+ measuredHeight.value = withTiming(nextHeight, item.animation.timing);
1063
+ } else {
1064
+ measuredHeight.value = nextHeight;
1065
+ }
1066
+
1067
+ onLayout?.(event);
1068
+ },
1069
+ [item.animation.duration, item.animation.timing, item.open, measuredHeight, onLayout]
1070
+ );
1071
+
1072
+ React.useEffect(() => {
1073
+ if (shouldRender) return;
1074
+ lastMeasuredHeightRef.current = 0;
1075
+ measuredHeight.value = 0;
1076
+ }, [measuredHeight, shouldRender]);
1077
+
1078
+ if (!shouldRender) return null;
1079
+
1080
+ const hidden = !item.open;
1081
+
1082
+ return (
1083
+ <Animated.View
1084
+ pointerEvents={item.open ? 'auto' : 'none'}
1085
+ style={[styles.contentContainer, animatedContainerStyle, containerStyle]}
1086
+ >
1087
+ <Animated.View style={[styles.contentInner, animatedInnerStyle]}>
1088
+ <View
1089
+ {...viewProps}
1090
+ collapsable={false}
1091
+ nativeID={nativeID ?? item.contentId}
1092
+ accessibilityElementsHidden={accessibilityElementsHidden ?? hidden}
1093
+ importantForAccessibility={importantForAccessibility ?? (hidden ? 'no-hide-descendants' : 'auto')}
1094
+ onLayout={handleLayout}
1095
+ style={[
1096
+ styles.content,
1097
+ {
1098
+ paddingBottom: item.metrics.contentPaddingBottom,
1099
+ paddingHorizontal: item.metrics.contentPaddingHorizontal,
1100
+ paddingTop: item.metrics.contentPaddingTop,
1101
+ },
1102
+ style,
1103
+ ]}
1104
+ >
1105
+ {children}
1106
+ </View>
1107
+ </Animated.View>
1108
+ </Animated.View>
1109
+ );
1110
+ }
1111
+
1112
+ export type AccordionIndicatorProps = {
1113
+ children?: React.ReactNode;
1114
+ style?: StyleProp<ViewStyle>;
1115
+ color?: string;
1116
+ size?: number;
1117
+ strokeWidth?: number;
1118
+ closedRotation?: number;
1119
+ openRotation?: number;
1120
+ };
1121
+
1122
+ export function AccordionIndicator({
1123
+ children,
1124
+ style,
1125
+ color,
1126
+ size,
1127
+ strokeWidth,
1128
+ closedRotation = -90,
1129
+ openRotation = 0,
1130
+ }: AccordionIndicatorProps) {
1131
+ const item = useAccordionItemContext();
1132
+ const resolvedSize = size ?? item.metrics.indicatorSize;
1133
+ const resolvedStrokeWidth = strokeWidth ?? item.metrics.indicatorStrokeWidth;
1134
+ const chevronSize = Math.max(wp(6), Math.round(resolvedSize * 0.34));
1135
+ const resolvedColor = color ?? (item.disabled ? item.colors.disabled : item.colors.accent);
1136
+
1137
+ const animatedStyle = useAnimatedStyle(() => {
1138
+ const deg = closedRotation + (openRotation - closedRotation) * item.openProgress.value;
1139
+ return {
1140
+ transform: [{ rotateZ: `${deg}deg` }],
1141
+ };
1142
+ }, [closedRotation, openRotation]);
1143
+
1144
+ return (
1145
+ <Animated.View
1146
+ pointerEvents="none"
1147
+ style={[styles.indicator, { width: resolvedSize, height: resolvedSize }, animatedStyle, style]}
1148
+ >
1149
+ {children ?? (
1150
+ <View
1151
+ style={[
1152
+ styles.chevron,
1153
+ {
1154
+ width: chevronSize,
1155
+ height: chevronSize,
1156
+ borderRightWidth: resolvedStrokeWidth,
1157
+ borderBottomWidth: resolvedStrokeWidth,
1158
+ borderRightColor: resolvedColor,
1159
+ borderBottomColor: resolvedColor,
1160
+ },
1161
+ ]}
1162
+ />
1163
+ )}
1164
+ </Animated.View>
1165
+ );
1166
+ }
1167
+
1168
+ const styles = StyleSheet.create({
1169
+ item: {
1170
+ overflow: 'hidden',
1171
+ },
1172
+ trigger: {
1173
+ justifyContent: 'center',
1174
+ },
1175
+ triggerContent: {
1176
+ alignItems: 'center',
1177
+ flexDirection: 'row',
1178
+ },
1179
+ leading: {
1180
+ flexShrink: 0,
1181
+ },
1182
+ main: {
1183
+ flex: 1,
1184
+ minWidth: 0,
1185
+ },
1186
+ trailing: {
1187
+ flexShrink: 0,
1188
+ },
1189
+ indicatorSlot: {
1190
+ flexShrink: 0,
1191
+ },
1192
+ textStack: {
1193
+ flexShrink: 1,
1194
+ minWidth: 0,
1195
+ },
1196
+ title: {
1197
+ fontWeight: '600',
1198
+ includeFontPadding: false,
1199
+ },
1200
+ description: {
1201
+ includeFontPadding: false,
1202
+ marginTop: wp(2),
1203
+ },
1204
+ contentContainer: {
1205
+ overflow: 'hidden',
1206
+ },
1207
+ contentInner: {
1208
+ left: 0,
1209
+ position: 'absolute',
1210
+ right: 0,
1211
+ top: 0,
1212
+ },
1213
+ content: {},
1214
+ indicator: {
1215
+ alignItems: 'center',
1216
+ justifyContent: 'center',
1217
+ },
1218
+ chevron: {
1219
+ transform: [{ rotate: '45deg' }],
1220
+ },
1221
+ });