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,1231 @@
1
+ import * as React from 'react';
2
+ import type {
3
+ GestureResponderEvent,
4
+ Insets,
5
+ PressableStateCallbackType,
6
+ StyleProp,
7
+ TextStyle,
8
+ ViewStyle,
9
+ } from 'react-native';
10
+ import {
11
+ I18nManager,
12
+ Platform,
13
+ Pressable,
14
+ processColor,
15
+ StyleSheet,
16
+ useColorScheme,
17
+ useWindowDimensions,
18
+ View,
19
+ } from 'react-native';
20
+ import Animated, {
21
+ Easing,
22
+ interpolate,
23
+ interpolateColor,
24
+ ReduceMotion,
25
+ useAnimatedStyle,
26
+ useSharedValue,
27
+ withTiming,
28
+ } from 'react-native-reanimated';
29
+ import type { SharedValue } from 'react-native-reanimated';
30
+ import { wp } from 'zkit-tools';
31
+ import CheckSvg from '../../assets/icons/check.svg';
32
+ import type { Theme } from '../../theme/types';
33
+ import { useTheme } from '../../theme/useTheme';
34
+ import { Text } from '../Text';
35
+
36
+ export type CheckboxCheckedState = boolean | 'indeterminate';
37
+ export type CheckboxValue = string | number;
38
+ export type CheckboxSize = 'sm' | 'md' | 'lg';
39
+ export type CheckboxTone = 'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'info';
40
+ export type CheckboxVariant = 'solid' | 'soft' | 'outline';
41
+ export type CheckboxShape = 'rounded' | 'square' | 'circle';
42
+ export type CheckboxLabelPlacement = 'start' | 'end';
43
+ export type CheckboxGroupOrientation = 'horizontal' | 'vertical';
44
+ export type CheckboxGroupAlign = 'start' | 'center' | 'end' | 'stretch';
45
+
46
+ export type CheckboxLayout = {
47
+ indicatorSize?: number;
48
+ indicatorRadius?: number;
49
+ indicatorBorderWidth?: number;
50
+ indicatorIconSize?: number;
51
+ gap?: number;
52
+ minTouchTarget?: number;
53
+ focusRingWidth?: number;
54
+ focusRingOffset?: number;
55
+ };
56
+
57
+ export type CheckboxColors = {
58
+ checkedBackground?: string;
59
+ checkedBorder?: string;
60
+ checkedIndicator?: string;
61
+ indeterminateBackground?: string;
62
+ indeterminateBorder?: string;
63
+ indeterminateIndicator?: string;
64
+ uncheckedBackground?: string;
65
+ uncheckedBorder?: string;
66
+ focusRing?: string;
67
+ };
68
+
69
+ export type CheckboxSlotProps = {
70
+ checked: boolean;
71
+ indeterminate: boolean;
72
+ state: CheckboxCheckedState;
73
+ value?: CheckboxValue;
74
+ disabled: boolean;
75
+ interactive: boolean;
76
+ size: CheckboxSize;
77
+ tone: CheckboxTone;
78
+ variant: CheckboxVariant;
79
+ toggle: () => void;
80
+ };
81
+
82
+ type CheckboxMetrics = {
83
+ indicatorSize: number;
84
+ indicatorRadius: number;
85
+ indicatorBorderWidth: number;
86
+ indicatorIconSize: number;
87
+ gap: number;
88
+ minTouchTarget: number;
89
+ focusRingWidth: number;
90
+ focusRingOffset: number;
91
+ };
92
+
93
+ type CheckboxTonePalette = {
94
+ accent: string;
95
+ onAccent: string;
96
+ };
97
+
98
+ type ResolvedCheckboxColors = {
99
+ checkedBackground: string;
100
+ checkedBorder: string;
101
+ checkedIndicator: string;
102
+ indeterminateBackground: string;
103
+ indeterminateBorder: string;
104
+ indeterminateIndicator: string;
105
+ uncheckedBackground: string;
106
+ uncheckedBorder: string;
107
+ focusRing: string;
108
+ };
109
+
110
+ type NativePressableProps = Omit<
111
+ React.ComponentPropsWithoutRef<typeof Pressable>,
112
+ 'accessibilityRole' | 'accessibilityState' | 'children' | 'disabled' | 'style' | 'onChange'
113
+ >;
114
+
115
+ type PressableFocusEvent = Parameters<
116
+ NonNullable<React.ComponentPropsWithoutRef<typeof Pressable>['onFocus']>
117
+ >[0];
118
+ type PressableBlurEvent = Parameters<
119
+ NonNullable<React.ComponentPropsWithoutRef<typeof Pressable>['onBlur']>
120
+ >[0];
121
+
122
+ export type CheckboxRef = React.ComponentRef<typeof Pressable>;
123
+
124
+ export interface CheckboxProps extends NativePressableProps {
125
+ value?: CheckboxValue;
126
+
127
+ checked?: CheckboxCheckedState;
128
+ defaultChecked?: CheckboxCheckedState;
129
+ onChange?: (checked: CheckboxCheckedState) => void;
130
+
131
+ disabled?: boolean;
132
+
133
+ size?: CheckboxSize;
134
+ tone?: CheckboxTone;
135
+ variant?: CheckboxVariant;
136
+ shape?: CheckboxShape;
137
+ duration?: number;
138
+
139
+ color?: string;
140
+ colors?: CheckboxColors;
141
+ layout?: CheckboxLayout;
142
+
143
+ label?: React.ReactNode;
144
+ description?: React.ReactNode;
145
+ labelPlacement?: CheckboxLabelPlacement;
146
+ children?: React.ReactNode | ((slot: CheckboxSlotProps) => React.ReactNode);
147
+
148
+ showIndicator?: boolean;
149
+ indicator?: React.ReactNode | ((slot: CheckboxSlotProps) => React.ReactNode);
150
+
151
+ style?: React.ComponentPropsWithoutRef<typeof Pressable>['style'];
152
+ contentStyle?: StyleProp<ViewStyle>;
153
+ indicatorStyle?: StyleProp<ViewStyle>;
154
+ labelStyle?: StyleProp<TextStyle>;
155
+ descriptionStyle?: StyleProp<TextStyle>;
156
+ accessibilityState?: React.ComponentPropsWithoutRef<typeof Pressable>['accessibilityState'];
157
+ }
158
+
159
+ type CheckboxIndicatorContextValue = {
160
+ checkedSv: SharedValue<number>;
161
+ indeterminateSv: SharedValue<number>;
162
+ boxSize: number;
163
+ slot: CheckboxSlotProps;
164
+ indeterminateIndicatorColor: string;
165
+ defaultIcon: React.ReactNode;
166
+ customIndicator?: React.ReactNode | ((slot: CheckboxSlotProps) => React.ReactNode);
167
+ };
168
+
169
+ const CheckboxIndicatorContext = React.createContext<CheckboxIndicatorContextValue | null>(null);
170
+
171
+ type CheckboxGroupStoreListener = () => void;
172
+
173
+ const noopSubscribe = () => () => {};
174
+
175
+ class CheckboxGroupStore<T extends CheckboxValue> {
176
+ private values: Set<T>;
177
+ private listeners = new Set<CheckboxGroupStoreListener>();
178
+
179
+ constructor(initialValues: readonly T[]) {
180
+ this.values = new Set(initialValues);
181
+ }
182
+
183
+ subscribe = (listener: CheckboxGroupStoreListener) => {
184
+ this.listeners.add(listener);
185
+ return () => {
186
+ this.listeners.delete(listener);
187
+ };
188
+ };
189
+
190
+ has = (value: T) => this.values.has(value);
191
+
192
+ getToggledValues = (value: T) => {
193
+ const next = new Set(this.values);
194
+ if (next.has(value)) next.delete(value);
195
+ else next.add(value);
196
+ return Array.from(next);
197
+ };
198
+
199
+ setValues = (values: readonly T[]) => {
200
+ const next = new Set(values);
201
+ if (next.size === this.values.size) {
202
+ let same = true;
203
+ for (const value of next) {
204
+ if (!this.values.has(value)) {
205
+ same = false;
206
+ break;
207
+ }
208
+ }
209
+ if (same) return false;
210
+ }
211
+
212
+ this.values = next;
213
+ this.emit();
214
+ return true;
215
+ };
216
+
217
+ private emit() {
218
+ for (const listener of this.listeners) listener();
219
+ }
220
+ }
221
+
222
+ type CheckboxGroupContextValue<T extends CheckboxValue> = {
223
+ store: CheckboxGroupStore<T>;
224
+ disabled: boolean;
225
+ isControlled: boolean;
226
+ toggleValue: (value: T) => void;
227
+ size?: CheckboxSize;
228
+ tone?: CheckboxTone;
229
+ variant?: CheckboxVariant;
230
+ shape?: CheckboxShape;
231
+ color?: string;
232
+ colors?: CheckboxColors;
233
+ layout?: CheckboxLayout;
234
+ };
235
+
236
+ const CheckboxGroupContext = React.createContext<CheckboxGroupContextValue<CheckboxValue> | null>(null);
237
+
238
+ const SEMANTIC_COLORS: Record<string, string> = {
239
+ danger: '#DC2626',
240
+ error: '#DC2626',
241
+ info: '#2563EB',
242
+ success: '#16A34A',
243
+ warn: '#D97706',
244
+ warning: '#D97706',
245
+ };
246
+
247
+ const SIZE_TOKENS = {
248
+ sm: {
249
+ indicatorSize: 18,
250
+ indicatorRadius: 4,
251
+ indicatorBorderWidth: 1.5,
252
+ indicatorIconSize: 12,
253
+ gap: 8,
254
+ },
255
+ md: {
256
+ indicatorSize: 20,
257
+ indicatorRadius: 5,
258
+ indicatorBorderWidth: 1.5,
259
+ indicatorIconSize: 14,
260
+ gap: 10,
261
+ },
262
+ lg: {
263
+ indicatorSize: 24,
264
+ indicatorRadius: 6,
265
+ indicatorBorderWidth: 1.5,
266
+ indicatorIconSize: 16,
267
+ gap: 12,
268
+ },
269
+ } as const satisfies Record<
270
+ CheckboxSize,
271
+ Omit<CheckboxMetrics, 'minTouchTarget' | 'focusRingWidth' | 'focusRingOffset'>
272
+ >;
273
+
274
+ const DISABLED_OPACITY = 0.48;
275
+ const PRESSED_OPACITY = 0.86;
276
+ const PRESSED_SCALE = 0.965;
277
+ const VALUE_TIMING_DURATION = 170;
278
+ const PRESS_IN_DURATION = 90;
279
+ const PRESS_OUT_DURATION = 140;
280
+ const FOCUS_DURATION = 120;
281
+
282
+ function useCheckboxIndicatorContext() {
283
+ const context = React.useContext(CheckboxIndicatorContext);
284
+ if (!context) {
285
+ throw new Error('[zkit-ui] CheckboxIndicator must be rendered inside <Checkbox />.');
286
+ }
287
+ return context;
288
+ }
289
+
290
+ function isProcessableColor(color: string) {
291
+ return processColor(color) != null;
292
+ }
293
+
294
+ function colorToRgba(color: string, alpha: number) {
295
+ const processed = processColor(color);
296
+ if (typeof processed !== 'number') return undefined;
297
+
298
+ const normalized = processed >>> 0;
299
+ const r = (normalized >> 16) & 255;
300
+ const g = (normalized >> 8) & 255;
301
+ const b = normalized & 255;
302
+ const a = Math.max(0, Math.min(1, alpha));
303
+ return `rgba(${r},${g},${b},${a})`;
304
+ }
305
+
306
+ function resolveColorToken(input: string | undefined, fallback: string, theme: Theme) {
307
+ if (input == null) return fallback;
308
+
309
+ const key = input.trim();
310
+ if (!key) return fallback;
311
+
312
+ const resolved =
313
+ key === 'default' || key === 'neutral' || key === 'onSurface'
314
+ ? theme.colors.onSurface
315
+ : key === 'primary'
316
+ ? theme.colors.primary
317
+ : key === 'onPrimary' || key === 'inverse'
318
+ ? theme.colors.onPrimary
319
+ : key === 'secondary'
320
+ ? theme.colors.secondary
321
+ : key === 'onSecondary'
322
+ ? theme.colors.onSecondary
323
+ : key === 'surface'
324
+ ? theme.colors.surface
325
+ : key === 'border'
326
+ ? theme.colors.border
327
+ : key === 'muted'
328
+ ? theme.colors.muted
329
+ : key === 'disabled' || key === 'subtle'
330
+ ? theme.colors.disabled
331
+ : SEMANTIC_COLORS[key] ?? key;
332
+
333
+ return isProcessableColor(resolved) ? resolved : fallback;
334
+ }
335
+
336
+ function resolveTonePalette(tone: CheckboxTone, theme: Theme): CheckboxTonePalette {
337
+ if (tone === 'neutral') {
338
+ return {
339
+ accent: theme.colors.onSurface,
340
+ onAccent: theme.colors.surface,
341
+ };
342
+ }
343
+
344
+ if (tone === 'success') {
345
+ return {
346
+ accent: SEMANTIC_COLORS.success,
347
+ onAccent: '#FFFFFF',
348
+ };
349
+ }
350
+
351
+ if (tone === 'warning') {
352
+ return {
353
+ accent: SEMANTIC_COLORS.warning,
354
+ onAccent: '#FFFFFF',
355
+ };
356
+ }
357
+
358
+ if (tone === 'danger') {
359
+ return {
360
+ accent: SEMANTIC_COLORS.danger,
361
+ onAccent: '#FFFFFF',
362
+ };
363
+ }
364
+
365
+ if (tone === 'info') {
366
+ return {
367
+ accent: SEMANTIC_COLORS.info,
368
+ onAccent: '#FFFFFF',
369
+ };
370
+ }
371
+
372
+ return {
373
+ accent: theme.colors.primary,
374
+ onAccent: theme.colors.onPrimary,
375
+ };
376
+ }
377
+
378
+ function resolveVariantColors(
379
+ variant: CheckboxVariant,
380
+ accent: string,
381
+ onAccent: string,
382
+ colors: CheckboxColors | undefined,
383
+ theme: Theme,
384
+ scheme: ReturnType<typeof useColorScheme>
385
+ ): ResolvedCheckboxColors {
386
+ const softAlpha = scheme === 'dark' ? 0.24 : 0.14;
387
+ const fallbackSoftBackground = colorToRgba(accent, softAlpha) ?? theme.colors.secondary;
388
+ const fallbackUncheckedBorder =
389
+ colorToRgba(theme.colors.onSurface, scheme === 'dark' ? 0.28 : 0.18) ?? theme.colors.border;
390
+ const checkedBackground =
391
+ variant === 'solid'
392
+ ? accent
393
+ : variant === 'soft'
394
+ ? fallbackSoftBackground
395
+ : theme.colors.surface;
396
+ const checkedIndicator = variant === 'solid' ? onAccent : accent;
397
+
398
+ const resolvedCheckedBackground = resolveColorToken(
399
+ colors?.checkedBackground,
400
+ checkedBackground,
401
+ theme
402
+ );
403
+ const resolvedCheckedBorder = resolveColorToken(colors?.checkedBorder, accent, theme);
404
+ const resolvedCheckedIndicator = resolveColorToken(
405
+ colors?.checkedIndicator,
406
+ checkedIndicator,
407
+ theme
408
+ );
409
+
410
+ return {
411
+ checkedBackground: resolvedCheckedBackground,
412
+ checkedBorder: resolvedCheckedBorder,
413
+ checkedIndicator: resolvedCheckedIndicator,
414
+ indeterminateBackground: resolveColorToken(
415
+ colors?.indeterminateBackground,
416
+ resolvedCheckedBackground,
417
+ theme
418
+ ),
419
+ indeterminateBorder: resolveColorToken(colors?.indeterminateBorder, resolvedCheckedBorder, theme),
420
+ indeterminateIndicator: resolveColorToken(
421
+ colors?.indeterminateIndicator,
422
+ resolvedCheckedIndicator,
423
+ theme
424
+ ),
425
+ uncheckedBackground: resolveColorToken(colors?.uncheckedBackground, theme.colors.surface, theme),
426
+ uncheckedBorder: resolveColorToken(colors?.uncheckedBorder, fallbackUncheckedBorder, theme),
427
+ focusRing: resolveColorToken(
428
+ colors?.focusRing,
429
+ colorToRgba(accent, scheme === 'dark' ? 0.36 : 0.28) ?? accent,
430
+ theme
431
+ ),
432
+ };
433
+ }
434
+
435
+ function resolveCheckboxMetrics(
436
+ size: CheckboxSize,
437
+ shape: CheckboxShape,
438
+ layout: CheckboxLayout | undefined
439
+ ): CheckboxMetrics {
440
+ const token = SIZE_TOKENS[size] ?? SIZE_TOKENS.md;
441
+ const indicatorSize = resolvePositiveNumber(layout?.indicatorSize, wp(token.indicatorSize));
442
+ const fallbackRadius =
443
+ shape === 'circle' ? indicatorSize / 2 : shape === 'square' ? 0 : wp(token.indicatorRadius);
444
+
445
+ return {
446
+ indicatorSize,
447
+ indicatorRadius: resolveNonNegativeNumber(layout?.indicatorRadius, fallbackRadius),
448
+ indicatorBorderWidth: resolvePositiveNumber(
449
+ layout?.indicatorBorderWidth,
450
+ wp(token.indicatorBorderWidth)
451
+ ),
452
+ indicatorIconSize: resolvePositiveNumber(layout?.indicatorIconSize, wp(token.indicatorIconSize)),
453
+ gap: resolveNonNegativeNumber(layout?.gap, wp(token.gap)),
454
+ minTouchTarget: resolvePositiveNumber(layout?.minTouchTarget, wp(44)),
455
+ focusRingWidth: resolvePositiveNumber(layout?.focusRingWidth, wp(2)),
456
+ focusRingOffset: resolveNonNegativeNumber(layout?.focusRingOffset, wp(3)),
457
+ };
458
+ }
459
+
460
+ function resolvePositiveNumber(value: number | undefined, fallback: number) {
461
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
462
+ }
463
+
464
+ function resolveNonNegativeNumber(value: number | undefined, fallback: number) {
465
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : fallback;
466
+ }
467
+
468
+ function resolveDuration(duration: number | undefined) {
469
+ if (duration == null || !Number.isFinite(duration)) return VALUE_TIMING_DURATION;
470
+ return Math.max(0, duration);
471
+ }
472
+
473
+ function toTimingConfig(duration: number) {
474
+ return {
475
+ duration,
476
+ easing: Easing.out(Easing.cubic),
477
+ reduceMotion: ReduceMotion.System,
478
+ } as const;
479
+ }
480
+
481
+ function resolveHitSlop(width: number, height: number, minTouchTarget: number): Insets | undefined {
482
+ const vertical = Math.max(0, (minTouchTarget - height) / 2);
483
+ const horizontal = Math.max(0, (minTouchTarget - width) / 2);
484
+ if (vertical === 0 && horizontal === 0) return undefined;
485
+ return {
486
+ top: vertical,
487
+ bottom: vertical,
488
+ left: horizontal,
489
+ right: horizontal,
490
+ };
491
+ }
492
+
493
+ function resolveWebCursorStyle(disabled: boolean): ViewStyle | undefined {
494
+ if (Platform.OS !== 'web') return undefined;
495
+ return { cursor: disabled ? 'not-allowed' : 'pointer' } as ViewStyle;
496
+ }
497
+
498
+ function isPrimitiveText(node: React.ReactNode): node is string | number {
499
+ return typeof node === 'string' || typeof node === 'number';
500
+ }
501
+
502
+ function isRenderProp(children: CheckboxProps['children']): children is (slot: CheckboxSlotProps) => React.ReactNode {
503
+ return typeof children === 'function';
504
+ }
505
+
506
+ function isIndicatorRenderProp(
507
+ indicator: CheckboxIndicatorProps['children'] | CheckboxProps['indicator']
508
+ ): indicator is (slot: CheckboxSlotProps) => React.ReactNode {
509
+ return typeof indicator === 'function';
510
+ }
511
+
512
+ function getNextCheckedState(current: CheckboxCheckedState): CheckboxCheckedState {
513
+ if (current === 'indeterminate') return true;
514
+ return !current;
515
+ }
516
+
517
+ function resolveAlignItems(align: CheckboxGroupAlign): ViewStyle['alignItems'] {
518
+ if (align === 'center') return 'center';
519
+ if (align === 'end') return 'flex-end';
520
+ if (align === 'stretch') return 'stretch';
521
+ return 'flex-start';
522
+ }
523
+
524
+ function CheckboxImpl(
525
+ {
526
+ value,
527
+ checked: checkedProp,
528
+ defaultChecked = false,
529
+ onChange,
530
+ disabled = false,
531
+ size: sizeProp,
532
+ tone: toneProp,
533
+ variant: variantProp,
534
+ shape: shapeProp,
535
+ duration,
536
+ color: colorProp,
537
+ colors: colorsProp,
538
+ layout: layoutProp,
539
+ label,
540
+ description,
541
+ labelPlacement = 'end',
542
+ children,
543
+ showIndicator = true,
544
+ indicator,
545
+ style,
546
+ contentStyle,
547
+ indicatorStyle,
548
+ labelStyle,
549
+ descriptionStyle,
550
+ accessibilityLabel,
551
+ accessibilityState,
552
+ hitSlop,
553
+ onPress,
554
+ onPressIn,
555
+ onPressOut,
556
+ onFocus,
557
+ onBlur,
558
+ testID,
559
+ ...pressableProps
560
+ }: CheckboxProps,
561
+ ref: React.ForwardedRef<CheckboxRef>
562
+ ) {
563
+ const theme = useTheme();
564
+ const scheme = useColorScheme();
565
+ const { width: viewportWidth } = useWindowDimensions();
566
+ const group = React.useContext(CheckboxGroupContext);
567
+ const isRTL = I18nManager.isRTL;
568
+
569
+ const isStandaloneControlled = checkedProp !== undefined;
570
+ const isGroupItem = group != null && value !== undefined && !isStandaloneControlled;
571
+ const [uncontrolledChecked, setUncontrolledChecked] = React.useState<CheckboxCheckedState>(() => defaultChecked);
572
+
573
+ const getGroupCheckedSnapshot = React.useCallback(
574
+ () => (group && value !== undefined ? group.store.has(value) : false),
575
+ [group, value]
576
+ );
577
+ const groupChecked = React.useSyncExternalStore(
578
+ group?.store.subscribe ?? noopSubscribe,
579
+ getGroupCheckedSnapshot,
580
+ getGroupCheckedSnapshot
581
+ );
582
+
583
+ const state: CheckboxCheckedState = isGroupItem
584
+ ? groupChecked
585
+ : isStandaloneControlled
586
+ ? checkedProp!
587
+ : uncontrolledChecked;
588
+ const checked = state === true;
589
+ const indeterminate = state === 'indeterminate';
590
+ const active = state !== false;
591
+ const resolvedDisabled = disabled || Boolean(group?.disabled);
592
+ const interactive = !resolvedDisabled;
593
+
594
+ const size = sizeProp ?? group?.size ?? 'md';
595
+ const tone = toneProp ?? group?.tone ?? 'primary';
596
+ const variant = variantProp ?? group?.variant ?? 'solid';
597
+ const shape = shapeProp ?? group?.shape ?? 'rounded';
598
+ const color = colorProp ?? group?.color;
599
+ const colors = React.useMemo(
600
+ () =>
601
+ group?.colors || colorsProp
602
+ ? {
603
+ ...group?.colors,
604
+ ...colorsProp,
605
+ }
606
+ : undefined,
607
+ [colorsProp, group?.colors]
608
+ );
609
+ const layout = React.useMemo(
610
+ () =>
611
+ group?.layout || layoutProp
612
+ ? {
613
+ ...group?.layout,
614
+ ...layoutProp,
615
+ }
616
+ : undefined,
617
+ [group?.layout, layoutProp]
618
+ );
619
+
620
+ const metrics = React.useMemo(
621
+ () => resolveCheckboxMetrics(size, shape, layout),
622
+ [layout, shape, size, viewportWidth]
623
+ );
624
+ const tonePalette = React.useMemo(() => resolveTonePalette(tone, theme), [theme, tone]);
625
+ const accent = React.useMemo(
626
+ () => resolveColorToken(color, tonePalette.accent, theme),
627
+ [color, theme, tonePalette.accent]
628
+ );
629
+ const resolvedColors = React.useMemo(
630
+ () => resolveVariantColors(variant, accent, tonePalette.onAccent, colors, theme, scheme),
631
+ [accent, colors, scheme, theme, tonePalette.onAccent, variant]
632
+ );
633
+ const webCursorStyle = React.useMemo(
634
+ () => resolveWebCursorStyle(resolvedDisabled),
635
+ [resolvedDisabled]
636
+ );
637
+ const resolvedDuration = resolveDuration(duration);
638
+ const valueTiming = React.useMemo(() => toTimingConfig(resolvedDuration), [resolvedDuration]);
639
+ const pressInTiming = React.useMemo(() => toTimingConfig(PRESS_IN_DURATION), []);
640
+ const pressOutTiming = React.useMemo(() => toTimingConfig(PRESS_OUT_DURATION), []);
641
+ const focusTiming = React.useMemo(() => toTimingConfig(FOCUS_DURATION), []);
642
+ const defaultHitSlop = React.useMemo(
643
+ () => resolveHitSlop(metrics.indicatorSize, metrics.indicatorSize, metrics.minTouchTarget),
644
+ [metrics.indicatorSize, metrics.minTouchTarget]
645
+ );
646
+
647
+ const checkedSv = useSharedValue(active ? 1 : 0);
648
+ const indeterminateSv = useSharedValue(indeterminate ? 1 : 0);
649
+ const pressSv = useSharedValue(0);
650
+ const focusSv = useSharedValue(0);
651
+
652
+ const stateRef = React.useRef<CheckboxCheckedState>(state);
653
+ stateRef.current = state;
654
+ const groupCheckedRef = React.useRef(groupChecked);
655
+ groupCheckedRef.current = groupChecked;
656
+
657
+ React.useEffect(() => {
658
+ if (!isStandaloneControlled && !isGroupItem) return;
659
+ checkedSv.value = withTiming(active ? 1 : 0, valueTiming);
660
+ indeterminateSv.value = withTiming(indeterminate ? 1 : 0, valueTiming);
661
+ }, [
662
+ active,
663
+ checkedSv,
664
+ indeterminate,
665
+ indeterminateSv,
666
+ isGroupItem,
667
+ isStandaloneControlled,
668
+ valueTiming,
669
+ ]);
670
+
671
+ React.useEffect(() => {
672
+ if (interactive) return;
673
+ pressSv.value = withTiming(0, pressOutTiming);
674
+ focusSv.value = withTiming(0, focusTiming);
675
+ }, [focusSv, focusTiming, interactive, pressOutTiming, pressSv]);
676
+
677
+ const commitStandaloneState = React.useCallback(
678
+ (next: CheckboxCheckedState) => {
679
+ if (!isStandaloneControlled) {
680
+ stateRef.current = next;
681
+ checkedSv.value = withTiming(next !== false ? 1 : 0, valueTiming);
682
+ indeterminateSv.value = withTiming(next === 'indeterminate' ? 1 : 0, valueTiming);
683
+ setUncontrolledChecked(next);
684
+ }
685
+ onChange?.(next);
686
+ },
687
+ [checkedSv, indeterminateSv, isStandaloneControlled, onChange, valueTiming]
688
+ );
689
+
690
+ const toggle = React.useCallback(() => {
691
+ if (!interactive) return;
692
+
693
+ if (isGroupItem && group && value !== undefined) {
694
+ const next = !groupCheckedRef.current;
695
+ if (!group.isControlled) {
696
+ checkedSv.value = withTiming(next ? 1 : 0, valueTiming);
697
+ indeterminateSv.value = withTiming(0, valueTiming);
698
+ }
699
+ group.toggleValue(value);
700
+ onChange?.(next);
701
+ return;
702
+ }
703
+
704
+ commitStandaloneState(getNextCheckedState(stateRef.current));
705
+ }, [
706
+ checkedSv,
707
+ commitStandaloneState,
708
+ group,
709
+ indeterminateSv,
710
+ interactive,
711
+ isGroupItem,
712
+ onChange,
713
+ value,
714
+ valueTiming,
715
+ ]);
716
+
717
+ const handlePress = React.useCallback(
718
+ (event: GestureResponderEvent) => {
719
+ if (interactive) toggle();
720
+ onPress?.(event);
721
+ },
722
+ [interactive, onPress, toggle]
723
+ );
724
+
725
+ const handlePressIn = React.useCallback(
726
+ (event: GestureResponderEvent) => {
727
+ if (interactive) {
728
+ pressSv.value = withTiming(1, pressInTiming);
729
+ }
730
+ onPressIn?.(event);
731
+ },
732
+ [interactive, onPressIn, pressInTiming, pressSv]
733
+ );
734
+
735
+ const handlePressOut = React.useCallback(
736
+ (event: GestureResponderEvent) => {
737
+ pressSv.value = withTiming(0, pressOutTiming);
738
+ onPressOut?.(event);
739
+ },
740
+ [onPressOut, pressOutTiming, pressSv]
741
+ );
742
+
743
+ const handleFocus = React.useCallback(
744
+ (event: PressableFocusEvent) => {
745
+ if (interactive) {
746
+ focusSv.value = withTiming(1, focusTiming);
747
+ }
748
+ onFocus?.(event);
749
+ },
750
+ [focusSv, focusTiming, interactive, onFocus]
751
+ );
752
+
753
+ const handleBlur = React.useCallback(
754
+ (event: PressableBlurEvent) => {
755
+ focusSv.value = withTiming(0, focusTiming);
756
+ onBlur?.(event);
757
+ },
758
+ [focusSv, focusTiming, onBlur]
759
+ );
760
+
761
+ const slot = React.useMemo<CheckboxSlotProps>(
762
+ () => ({
763
+ checked,
764
+ indeterminate,
765
+ state,
766
+ value,
767
+ disabled: resolvedDisabled,
768
+ interactive,
769
+ size,
770
+ tone,
771
+ variant,
772
+ toggle,
773
+ }),
774
+ [checked, indeterminate, interactive, resolvedDisabled, size, state, tone, toggle, value, variant]
775
+ );
776
+
777
+ const renderedContent = React.useMemo(() => {
778
+ if (isRenderProp(children)) return children(slot);
779
+ if (children != null) return children;
780
+ if (label == null && description == null) return null;
781
+
782
+ return (
783
+ <View style={styles.copy}>
784
+ {label != null ? (
785
+ isPrimitiveText(label) ? (
786
+ <Text
787
+ numberOfLines={1}
788
+ variant="label"
789
+ style={labelStyle}
790
+ >
791
+ {label}
792
+ </Text>
793
+ ) : (
794
+ label
795
+ )
796
+ ) : null}
797
+ {description != null ? (
798
+ isPrimitiveText(description) ? (
799
+ <Text
800
+ numberOfLines={2}
801
+ tone="muted"
802
+ variant="caption"
803
+ style={descriptionStyle}
804
+ >
805
+ {description}
806
+ </Text>
807
+ ) : (
808
+ description
809
+ )
810
+ ) : null}
811
+ </View>
812
+ );
813
+ }, [children, description, descriptionStyle, label, labelStyle, resolvedDisabled, slot]);
814
+
815
+ const defaultIcon = React.useMemo(
816
+ () => (
817
+ <CheckSvg
818
+ width={metrics.indicatorIconSize}
819
+ height={metrics.indicatorIconSize}
820
+ color={resolvedColors.checkedIndicator}
821
+ />
822
+ ),
823
+ [metrics.indicatorIconSize, resolvedColors.checkedIndicator]
824
+ );
825
+
826
+ const indicatorContext = React.useMemo<CheckboxIndicatorContextValue>(
827
+ () => ({
828
+ checkedSv,
829
+ indeterminateSv,
830
+ boxSize: metrics.indicatorSize,
831
+ slot,
832
+ indeterminateIndicatorColor: resolvedColors.indeterminateIndicator,
833
+ defaultIcon,
834
+ customIndicator: indicator,
835
+ }),
836
+ [
837
+ checkedSv,
838
+ defaultIcon,
839
+ indicator,
840
+ indeterminateSv,
841
+ metrics.indicatorSize,
842
+ resolvedColors.indeterminateIndicator,
843
+ slot,
844
+ ]
845
+ );
846
+
847
+ const rootAnimatedStyle = useAnimatedStyle(() => {
848
+ const opacity = resolvedDisabled
849
+ ? DISABLED_OPACITY
850
+ : interpolate(pressSv.value, [0, 1], [1, PRESSED_OPACITY]);
851
+ return { opacity };
852
+ }, [resolvedDisabled]);
853
+
854
+ const indicatorPressAnimatedStyle = useAnimatedStyle(() => {
855
+ const scale = interpolate(pressSv.value, [0, 1], [1, PRESSED_SCALE]);
856
+ return { transform: [{ scale }] };
857
+ });
858
+
859
+ const focusRingAnimatedStyle = useAnimatedStyle(() => {
860
+ return {
861
+ opacity: focusSv.value,
862
+ transform: [{ scale: interpolate(focusSv.value, [0, 1], [0.96, 1]) }],
863
+ };
864
+ });
865
+
866
+ const indicatorAnimatedStyle = useAnimatedStyle(() => {
867
+ const activeBackground = interpolateColor(
868
+ indeterminateSv.value,
869
+ [0, 1],
870
+ [resolvedColors.checkedBackground, resolvedColors.indeterminateBackground]
871
+ );
872
+ const activeBorder = interpolateColor(
873
+ indeterminateSv.value,
874
+ [0, 1],
875
+ [resolvedColors.checkedBorder, resolvedColors.indeterminateBorder]
876
+ );
877
+
878
+ return {
879
+ backgroundColor: interpolateColor(
880
+ checkedSv.value,
881
+ [0, 1],
882
+ [resolvedColors.uncheckedBackground, activeBackground]
883
+ ),
884
+ borderColor: interpolateColor(
885
+ checkedSv.value,
886
+ [0, 1],
887
+ [resolvedColors.uncheckedBorder, activeBorder]
888
+ ),
889
+ };
890
+ }, [resolvedColors]);
891
+
892
+ const indicatorFrameStyle = React.useMemo<ViewStyle>(
893
+ () => ({
894
+ width: metrics.indicatorSize,
895
+ height: metrics.indicatorSize,
896
+ borderRadius: metrics.indicatorRadius,
897
+ borderWidth: metrics.indicatorBorderWidth,
898
+ }),
899
+ [metrics.indicatorBorderWidth, metrics.indicatorRadius, metrics.indicatorSize]
900
+ );
901
+
902
+ const indicatorNode = showIndicator ? (
903
+ <Animated.View
904
+ style={[
905
+ styles.indicatorFrame,
906
+ indicatorFrameStyle,
907
+ indicatorPressAnimatedStyle,
908
+ indicatorAnimatedStyle,
909
+ indicatorStyle,
910
+ ]}
911
+ >
912
+ <Animated.View
913
+ pointerEvents="none"
914
+ style={[
915
+ styles.focusRing,
916
+ {
917
+ top: -metrics.focusRingOffset,
918
+ bottom: -metrics.focusRingOffset,
919
+ left: -metrics.focusRingOffset,
920
+ right: -metrics.focusRingOffset,
921
+ borderRadius: metrics.indicatorRadius + metrics.focusRingOffset,
922
+ borderWidth: metrics.focusRingWidth,
923
+ borderColor: resolvedColors.focusRing,
924
+ },
925
+ focusRingAnimatedStyle,
926
+ ]}
927
+ />
928
+ <CheckboxIndicator />
929
+ </Animated.View>
930
+ ) : null;
931
+
932
+ const hasContent = renderedContent != null;
933
+ const contentBeforeIndicator =
934
+ (labelPlacement === 'start' && !isRTL) || (labelPlacement === 'end' && isRTL);
935
+ const contentNode = hasContent ? (
936
+ <View style={[styles.content, contentStyle]}>
937
+ {contentBeforeIndicator ? (
938
+ <>
939
+ <View style={[styles.contentSlot, showIndicator ? { marginEnd: metrics.gap } : null]}>
940
+ {renderedContent}
941
+ </View>
942
+ {indicatorNode}
943
+ </>
944
+ ) : (
945
+ <>
946
+ {indicatorNode}
947
+ <View style={[styles.contentSlot, showIndicator ? { marginStart: metrics.gap } : null]}>
948
+ {renderedContent}
949
+ </View>
950
+ </>
951
+ )}
952
+ </View>
953
+ ) : (
954
+ indicatorNode
955
+ );
956
+ const inferredAccessibilityLabel = isPrimitiveText(label) ? String(label) : undefined;
957
+ const resolvedStyle = React.useCallback(
958
+ (stateArg: PressableStateCallbackType) => {
959
+ const userStyle = typeof style === 'function' ? style(stateArg) : style;
960
+ return [styles.root, webCursorStyle, userStyle];
961
+ },
962
+ [style, webCursorStyle]
963
+ );
964
+
965
+ return (
966
+ <CheckboxIndicatorContext.Provider value={indicatorContext}>
967
+ <Pressable
968
+ {...pressableProps}
969
+ ref={ref}
970
+ accessibilityLabel={accessibilityLabel ?? inferredAccessibilityLabel}
971
+ accessibilityRole="checkbox"
972
+ accessibilityState={{
973
+ ...accessibilityState,
974
+ checked: indeterminate ? 'mixed' : checked,
975
+ disabled: Boolean(resolvedDisabled),
976
+ }}
977
+ disabled={resolvedDisabled}
978
+ hitSlop={hitSlop ?? defaultHitSlop}
979
+ onBlur={handleBlur}
980
+ onFocus={handleFocus}
981
+ onPress={handlePress}
982
+ onPressIn={handlePressIn}
983
+ onPressOut={handlePressOut}
984
+ style={resolvedStyle}
985
+ testID={testID}
986
+ >
987
+ <Animated.View style={[styles.visual, rootAnimatedStyle]}>{contentNode}</Animated.View>
988
+ </Pressable>
989
+ </CheckboxIndicatorContext.Provider>
990
+ );
991
+ }
992
+
993
+ const CheckboxWithRef = React.forwardRef<CheckboxRef, CheckboxProps>(CheckboxImpl);
994
+ CheckboxWithRef.displayName = 'Checkbox';
995
+
996
+ export const Checkbox = React.memo(CheckboxWithRef);
997
+ Checkbox.displayName = 'Checkbox';
998
+
999
+ export type CheckboxIndicatorProps = {
1000
+ children?: React.ReactNode | ((slot: CheckboxSlotProps) => React.ReactNode);
1001
+ style?: StyleProp<ViewStyle>;
1002
+ };
1003
+
1004
+ export function CheckboxIndicator({ children, style }: CheckboxIndicatorProps) {
1005
+ const context = useCheckboxIndicatorContext();
1006
+ const customIndicator = children ?? context.customIndicator;
1007
+
1008
+ const checkedIconAnimatedStyle = useAnimatedStyle(() => {
1009
+ const opacity = context.checkedSv.value * (1 - context.indeterminateSv.value);
1010
+ const scale = interpolate(context.checkedSv.value, [0, 1], [0.78, 1]);
1011
+ return { opacity, transform: [{ scale }] };
1012
+ });
1013
+
1014
+ const indeterminateAnimatedStyle = useAnimatedStyle(() => {
1015
+ const opacity = context.checkedSv.value * context.indeterminateSv.value;
1016
+ const scale = interpolate(context.indeterminateSv.value, [0, 1], [0.82, 1]);
1017
+ return { opacity, transform: [{ scale }] };
1018
+ });
1019
+
1020
+ const customIndicatorAnimatedStyle = useAnimatedStyle(() => {
1021
+ const opacity = context.checkedSv.value;
1022
+ const scale = interpolate(context.checkedSv.value, [0, 1], [0.78, 1]);
1023
+ return { opacity, transform: [{ scale }] };
1024
+ });
1025
+
1026
+ const indeterminateWidth = Math.max(wp(8), Math.round(context.boxSize * 0.56));
1027
+ const indeterminateHeight = Math.max(wp(2), Math.round(context.boxSize * 0.11));
1028
+ const resolvedCustomIndicator = isIndicatorRenderProp(customIndicator)
1029
+ ? customIndicator(context.slot)
1030
+ : customIndicator;
1031
+
1032
+ return (
1033
+ <View
1034
+ pointerEvents="none"
1035
+ style={[
1036
+ styles.indicatorLayer,
1037
+ {
1038
+ width: context.boxSize,
1039
+ height: context.boxSize,
1040
+ },
1041
+ style,
1042
+ ]}
1043
+ >
1044
+ {resolvedCustomIndicator != null ? (
1045
+ <Animated.View style={customIndicatorAnimatedStyle}>{resolvedCustomIndicator}</Animated.View>
1046
+ ) : (
1047
+ <>
1048
+ <Animated.View style={indeterminateAnimatedStyle}>
1049
+ <View
1050
+ style={{
1051
+ width: indeterminateWidth,
1052
+ height: indeterminateHeight,
1053
+ borderRadius: indeterminateHeight / 2,
1054
+ backgroundColor: context.indeterminateIndicatorColor,
1055
+ }}
1056
+ />
1057
+ </Animated.View>
1058
+ <Animated.View style={[styles.iconLayer, checkedIconAnimatedStyle]}>
1059
+ {context.defaultIcon}
1060
+ </Animated.View>
1061
+ </>
1062
+ )}
1063
+ </View>
1064
+ );
1065
+ }
1066
+
1067
+ export type CheckboxGroupProps<T extends CheckboxValue = CheckboxValue> = Omit<
1068
+ React.ComponentPropsWithoutRef<typeof View>,
1069
+ 'children'
1070
+ > & {
1071
+ value?: readonly T[];
1072
+ defaultValue?: readonly T[];
1073
+ onChange?: (value: T[]) => void;
1074
+ disabled?: boolean;
1075
+
1076
+ orientation?: CheckboxGroupOrientation;
1077
+ align?: CheckboxGroupAlign;
1078
+ wrap?: boolean;
1079
+ gap?: number;
1080
+ rowGap?: number;
1081
+ columnGap?: number;
1082
+
1083
+ size?: CheckboxSize;
1084
+ tone?: CheckboxTone;
1085
+ variant?: CheckboxVariant;
1086
+ shape?: CheckboxShape;
1087
+ color?: string;
1088
+ colors?: CheckboxColors;
1089
+ layout?: CheckboxLayout;
1090
+
1091
+ children: React.ReactNode;
1092
+ };
1093
+
1094
+ export function CheckboxGroup<T extends CheckboxValue = CheckboxValue>({
1095
+ value,
1096
+ defaultValue,
1097
+ onChange,
1098
+ disabled = false,
1099
+ orientation = 'horizontal',
1100
+ align = 'start',
1101
+ wrap = false,
1102
+ gap,
1103
+ rowGap,
1104
+ columnGap,
1105
+ size,
1106
+ tone,
1107
+ variant,
1108
+ shape,
1109
+ color,
1110
+ colors,
1111
+ layout,
1112
+ style,
1113
+ children,
1114
+ ...viewProps
1115
+ }: CheckboxGroupProps<T>) {
1116
+ const { width: viewportWidth } = useWindowDimensions();
1117
+ const isControlled = value !== undefined;
1118
+ const storeRef = React.useRef<CheckboxGroupStore<T> | null>(null);
1119
+ if (!storeRef.current) {
1120
+ storeRef.current = new CheckboxGroupStore<T>(value ?? defaultValue ?? []);
1121
+ }
1122
+
1123
+ React.useLayoutEffect(() => {
1124
+ if (!isControlled) return;
1125
+ storeRef.current!.setValues(value ?? []);
1126
+ }, [isControlled, value]);
1127
+
1128
+ const toggleValue = React.useCallback(
1129
+ (nextValue: T) => {
1130
+ const next = storeRef.current!.getToggledValues(nextValue);
1131
+ if (!isControlled) storeRef.current!.setValues(next);
1132
+ onChange?.(next);
1133
+ },
1134
+ [isControlled, onChange]
1135
+ );
1136
+
1137
+ const context = React.useMemo<CheckboxGroupContextValue<T>>(
1138
+ () => ({
1139
+ store: storeRef.current!,
1140
+ disabled,
1141
+ isControlled,
1142
+ toggleValue,
1143
+ size,
1144
+ tone,
1145
+ variant,
1146
+ shape,
1147
+ color,
1148
+ colors,
1149
+ layout,
1150
+ }),
1151
+ [
1152
+ color,
1153
+ colors,
1154
+ disabled,
1155
+ isControlled,
1156
+ layout,
1157
+ shape,
1158
+ size,
1159
+ tone,
1160
+ toggleValue,
1161
+ variant,
1162
+ ]
1163
+ );
1164
+
1165
+ const defaultGap = React.useMemo(() => wp(12), [viewportWidth]);
1166
+ const resolvedGap = resolveNonNegativeNumber(gap, defaultGap);
1167
+ const resolvedRowGap = resolveNonNegativeNumber(rowGap, resolvedGap);
1168
+ const resolvedColumnGap = resolveNonNegativeNumber(columnGap, resolvedGap);
1169
+ const layoutStyle = React.useMemo<ViewStyle>(
1170
+ () => ({
1171
+ alignItems: resolveAlignItems(align),
1172
+ columnGap: resolvedColumnGap,
1173
+ flexDirection: orientation === 'vertical' ? 'column' : 'row',
1174
+ flexWrap: wrap ? 'wrap' : 'nowrap',
1175
+ rowGap: resolvedRowGap,
1176
+ }),
1177
+ [align, orientation, resolvedColumnGap, resolvedRowGap, wrap]
1178
+ );
1179
+
1180
+ return (
1181
+ <CheckboxGroupContext.Provider
1182
+ value={context as unknown as CheckboxGroupContextValue<CheckboxValue>}
1183
+ >
1184
+ <View {...viewProps} style={[layoutStyle, style]}>
1185
+ {children}
1186
+ </View>
1187
+ </CheckboxGroupContext.Provider>
1188
+ );
1189
+ }
1190
+
1191
+ const styles = StyleSheet.create({
1192
+ root: {
1193
+ alignSelf: 'flex-start',
1194
+ },
1195
+ visual: {
1196
+ alignItems: 'center',
1197
+ flexDirection: 'row',
1198
+ minWidth: 0,
1199
+ },
1200
+ content: {
1201
+ alignItems: 'center',
1202
+ flexDirection: 'row',
1203
+ minWidth: 0,
1204
+ },
1205
+ contentSlot: {
1206
+ flexShrink: 1,
1207
+ minWidth: 0,
1208
+ },
1209
+ copy: {
1210
+ flexShrink: 1,
1211
+ minWidth: 0,
1212
+ },
1213
+ indicatorFrame: {
1214
+ alignItems: 'center',
1215
+ flexShrink: 0,
1216
+ justifyContent: 'center',
1217
+ overflow: 'visible',
1218
+ },
1219
+ focusRing: {
1220
+ position: 'absolute',
1221
+ },
1222
+ indicatorLayer: {
1223
+ alignItems: 'center',
1224
+ justifyContent: 'center',
1225
+ },
1226
+ iconLayer: {
1227
+ ...StyleSheet.absoluteFillObject,
1228
+ alignItems: 'center',
1229
+ justifyContent: 'center',
1230
+ },
1231
+ });