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,94 @@
1
+ import type * as React from 'react';
2
+
3
+ export type PickerPrimitiveValue = string | number;
4
+ export type PickerValue = PickerPrimitiveValue | PickerPrimitiveValue[];
5
+ export type PickerValueMode = 'auto' | 'single' | 'path';
6
+
7
+ export type PickerOption<TValue extends PickerPrimitiveValue = PickerPrimitiveValue> = {
8
+ value: TValue;
9
+ label: string;
10
+ disabled?: boolean;
11
+ children?: PickerOption<TValue>[];
12
+ key?: React.Key;
13
+ testID?: string;
14
+ accessibilityLabel?: string;
15
+ [key: string]: unknown;
16
+ };
17
+
18
+ export type PickerSelection<TOption = PickerOption> = {
19
+ value: PickerValue;
20
+ values: PickerPrimitiveValue[];
21
+ label: string;
22
+ labels: string[];
23
+ items: TOption[];
24
+ columns: TOption[][];
25
+ indices: number[];
26
+ isComplete: boolean;
27
+ };
28
+
29
+ export type PickerConfirmPayload<TOption = PickerOption> = PickerSelection<TOption>;
30
+ export type PickerChangePayload<TOption = PickerOption> = PickerSelection<TOption>;
31
+ export type PickerDraftChangePayload<TOption = PickerOption> = PickerSelection<TOption>;
32
+
33
+ export type PickerHandle = {
34
+ open: () => void;
35
+ close: () => void;
36
+ };
37
+
38
+ export type PickerTriggerContext<TOption = PickerOption> = PickerSelection<TOption> & {
39
+ open: () => void;
40
+ close: () => void;
41
+ isOpen: boolean;
42
+ disabled: boolean;
43
+ placeholder: string;
44
+ };
45
+
46
+ export type PickerColumnHeaderContext<TOption = PickerOption> = {
47
+ columnIndex: number;
48
+ columnCount: number;
49
+ options: TOption[];
50
+ selectedItem?: TOption;
51
+ selectedValue?: PickerPrimitiveValue;
52
+ selectedLabel?: string;
53
+ };
54
+
55
+ export type PickerOptionAccessors<TOption> = {
56
+ getOptionValue?: (option: TOption, index: number, path: TOption[]) => PickerPrimitiveValue | undefined;
57
+ getOptionLabel?: (option: TOption, index: number, path: TOption[]) => string | number | undefined;
58
+ getOptionChildren?: (option: TOption, index: number, path: TOption[]) => readonly TOption[] | undefined;
59
+ isOptionDisabled?: (option: TOption, index: number, path: TOption[]) => boolean;
60
+ };
61
+
62
+ export type PickerProps<TOption = PickerOption> = PickerOptionAccessors<TOption> & {
63
+ options: readonly TOption[];
64
+
65
+ value?: PickerValue;
66
+ defaultValue?: PickerValue;
67
+ onChange?: (value: PickerValue, selection: PickerChangePayload<TOption>) => void;
68
+ valueMode?: PickerValueMode;
69
+
70
+ open?: boolean;
71
+ defaultOpen?: boolean;
72
+ onOpenChange?: (open: boolean) => void;
73
+ onDismissComplete?: () => void;
74
+
75
+ title?: string;
76
+ placeholder?: string;
77
+ cancelText?: string;
78
+ confirmText?: string;
79
+ emptyText?: string;
80
+ separator?: string;
81
+ formatLabel?: (selection: PickerSelection<TOption>) => string;
82
+ renderColumnHeader?: (context: PickerColumnHeaderContext<TOption>) => React.ReactNode;
83
+ maxColumns?: number;
84
+
85
+ lazyContent?: boolean;
86
+ sheetHeight?: number | 'auto';
87
+ disabled?: boolean;
88
+
89
+ onCancel?: () => void;
90
+ onConfirm?: (selection: PickerConfirmPayload<TOption>) => void;
91
+ onDraftChange?: (selection: PickerDraftChangePayload<TOption>) => void;
92
+
93
+ children?: React.ReactNode | ((context: PickerTriggerContext<TOption>) => React.ReactNode);
94
+ };
@@ -0,0 +1,239 @@
1
+ import type {
2
+ PickerOptionAccessors,
3
+ PickerPrimitiveValue,
4
+ PickerSelection,
5
+ PickerValue,
6
+ PickerValueMode,
7
+ } from './types';
8
+
9
+ export type PickerCascadeState<TOption> = {
10
+ columns: TOption[][];
11
+ indices: number[];
12
+ values: PickerPrimitiveValue[];
13
+ labels: string[];
14
+ items: TOption[];
15
+ };
16
+
17
+ type SelectionOptions<TOption> = {
18
+ separator: string;
19
+ valueMode: PickerValueMode;
20
+ preferPathValue: boolean;
21
+ formatLabel?: (selection: PickerSelection<TOption>) => string;
22
+ };
23
+
24
+ export function clampNumber(n: number, min: number, max: number) {
25
+ return Math.max(min, Math.min(max, n));
26
+ }
27
+
28
+ export function toArrayValue(value: PickerValue | undefined): PickerPrimitiveValue[] {
29
+ if (Array.isArray(value)) return value;
30
+ if (typeof value === 'string' || typeof value === 'number') return [value];
31
+ return [];
32
+ }
33
+
34
+ export function toOutputValue(
35
+ values: PickerPrimitiveValue[],
36
+ valueMode: PickerValueMode,
37
+ preferPathValue: boolean
38
+ ): PickerValue {
39
+ if (valueMode === 'path') return values;
40
+ if (valueMode === 'single') return values[values.length - 1] ?? '';
41
+ if (preferPathValue || values.length !== 1) return values;
42
+ return values[0] ?? '';
43
+ }
44
+
45
+ function isPrimitiveValue(value: unknown): value is PickerPrimitiveValue {
46
+ return typeof value === 'string' || typeof value === 'number';
47
+ }
48
+
49
+ function toOptionRecord(option: unknown): Record<string, unknown> {
50
+ if (option != null && typeof option === 'object') {
51
+ return option as Record<string, unknown>;
52
+ }
53
+ return {};
54
+ }
55
+
56
+ export function resolveOptionValue<TOption>(
57
+ option: TOption,
58
+ index: number,
59
+ path: TOption[],
60
+ accessors: PickerOptionAccessors<TOption>
61
+ ): PickerPrimitiveValue {
62
+ const customValue = accessors.getOptionValue?.(option, index, path);
63
+ if (isPrimitiveValue(customValue)) return customValue;
64
+
65
+ const record = toOptionRecord(option);
66
+ const fallbackValue = record.value ?? record.id ?? record.code ?? record.key;
67
+ if (isPrimitiveValue(fallbackValue)) return fallbackValue;
68
+
69
+ return index;
70
+ }
71
+
72
+ export function resolveOptionLabel<TOption>(
73
+ option: TOption,
74
+ index: number,
75
+ path: TOption[],
76
+ accessors: PickerOptionAccessors<TOption>
77
+ ): string {
78
+ const customLabel = accessors.getOptionLabel?.(option, index, path);
79
+ if (typeof customLabel === 'string') return customLabel;
80
+ if (typeof customLabel === 'number') return String(customLabel);
81
+
82
+ const record = toOptionRecord(option);
83
+ const fallbackLabel = record.label ?? record.title ?? record.text ?? record.name;
84
+ if (typeof fallbackLabel === 'string') return fallbackLabel;
85
+ if (typeof fallbackLabel === 'number') return String(fallbackLabel);
86
+
87
+ return String(resolveOptionValue(option, index, path, accessors));
88
+ }
89
+
90
+ export function resolveOptionChildren<TOption>(
91
+ option: TOption,
92
+ index: number,
93
+ path: TOption[],
94
+ accessors: PickerOptionAccessors<TOption>
95
+ ): TOption[] {
96
+ const customChildren = accessors.getOptionChildren?.(option, index, path);
97
+ if (Array.isArray(customChildren)) return customChildren as TOption[];
98
+
99
+ const record = toOptionRecord(option);
100
+ return Array.isArray(record.children) ? (record.children as TOption[]) : [];
101
+ }
102
+
103
+ export function resolveOptionDisabled<TOption>(
104
+ option: TOption,
105
+ index: number,
106
+ path: TOption[],
107
+ accessors: PickerOptionAccessors<TOption>
108
+ ): boolean {
109
+ const customDisabled = accessors.isOptionDisabled?.(option, index, path);
110
+ if (typeof customDisabled === 'boolean') return customDisabled;
111
+ return toOptionRecord(option).disabled === true;
112
+ }
113
+
114
+ export function findNearestEnabledIndex<TOption>(
115
+ options: readonly TOption[],
116
+ startIndex: number,
117
+ path: TOption[],
118
+ accessors: PickerOptionAccessors<TOption>
119
+ ) {
120
+ if (!options.length) return -1;
121
+
122
+ const index = clampNumber(Math.round(startIndex), 0, options.length - 1);
123
+ const isEnabled = (optionIndex: number) => {
124
+ const option = options[optionIndex] as TOption;
125
+ return !resolveOptionDisabled(option, optionIndex, [...path, option], accessors);
126
+ };
127
+
128
+ if (isEnabled(index)) return index;
129
+
130
+ for (let i = index + 1; i < options.length; i += 1) {
131
+ if (isEnabled(i)) return i;
132
+ }
133
+
134
+ for (let i = index - 1; i >= 0; i -= 1) {
135
+ if (isEnabled(i)) return i;
136
+ }
137
+
138
+ return -1;
139
+ }
140
+
141
+ export function resolveCascade<TOption>(
142
+ rootOptions: readonly TOption[],
143
+ desiredValues: PickerPrimitiveValue[],
144
+ accessors: PickerOptionAccessors<TOption>,
145
+ maxColumns: number
146
+ ): PickerCascadeState<TOption> {
147
+ const columns: TOption[][] = [];
148
+ const indices: number[] = [];
149
+ const values: PickerPrimitiveValue[] = [];
150
+ const labels: string[] = [];
151
+ const items: TOption[] = [];
152
+
153
+ let currentOptions = Array.isArray(rootOptions) ? (rootOptions as TOption[]) : [];
154
+ let path: TOption[] = [];
155
+
156
+ for (let columnIndex = 0; columnIndex < maxColumns; columnIndex += 1) {
157
+ if (!currentOptions.length) break;
158
+ columns.push(currentOptions);
159
+
160
+ const desired = desiredValues[columnIndex];
161
+ let selectedIndex = 0;
162
+
163
+ if (desired !== undefined) {
164
+ const foundIndex = currentOptions.findIndex((option, optionIndex) => {
165
+ const nextPath = [...path, option];
166
+ return Object.is(resolveOptionValue(option, optionIndex, nextPath, accessors), desired);
167
+ });
168
+ selectedIndex = foundIndex >= 0 ? foundIndex : 0;
169
+ }
170
+
171
+ selectedIndex = findNearestEnabledIndex(currentOptions, selectedIndex, path, accessors);
172
+ if (selectedIndex < 0) break;
173
+
174
+ const item = currentOptions[selectedIndex];
175
+ if (item == null) break;
176
+
177
+ const nextPath = [...path, item];
178
+ indices.push(selectedIndex);
179
+ values.push(resolveOptionValue(item, selectedIndex, nextPath, accessors));
180
+ labels.push(resolveOptionLabel(item, selectedIndex, nextPath, accessors));
181
+ items.push(item);
182
+
183
+ currentOptions = resolveOptionChildren(item, selectedIndex, nextPath, accessors);
184
+ path = nextPath;
185
+ }
186
+
187
+ return { columns, indices, values, labels, items };
188
+ }
189
+
190
+ export function createPickerSelection<TOption>(
191
+ state: PickerCascadeState<TOption>,
192
+ options: SelectionOptions<TOption>
193
+ ): PickerSelection<TOption> {
194
+ const value = toOutputValue(state.values, options.valueMode, options.preferPathValue);
195
+ const baseLabel = state.labels.filter(Boolean).join(options.separator);
196
+ const baseSelection: PickerSelection<TOption> = {
197
+ value,
198
+ values: state.values,
199
+ label: baseLabel,
200
+ labels: state.labels,
201
+ items: state.items,
202
+ columns: state.columns,
203
+ indices: state.indices,
204
+ isComplete:
205
+ state.columns.length > 0 &&
206
+ state.items.length === state.columns.length &&
207
+ state.values.length === state.columns.length,
208
+ };
209
+
210
+ const formattedLabel = options.formatLabel?.(baseSelection);
211
+ if (typeof formattedLabel === 'string') {
212
+ return { ...baseSelection, label: formattedLabel };
213
+ }
214
+
215
+ return baseSelection;
216
+ }
217
+
218
+ export function transparentizeColor(color: string) {
219
+ const input = color.trim();
220
+ const fullHex = /^#([0-9a-f]{6})$/i.exec(input);
221
+ if (fullHex) {
222
+ const n = Number.parseInt(fullHex[1] as string, 16);
223
+ const r = (n >> 16) & 255;
224
+ const g = (n >> 8) & 255;
225
+ const b = n & 255;
226
+ return `rgba(${r},${g},${b},0)`;
227
+ }
228
+
229
+ const shortHex = /^#([0-9a-f]{3})$/i.exec(input);
230
+ if (shortHex) {
231
+ const chars = shortHex[1] as string;
232
+ const r = Number.parseInt(`${chars[0]}${chars[0]}`, 16);
233
+ const g = Number.parseInt(`${chars[1]}${chars[1]}`, 16);
234
+ const b = Number.parseInt(`${chars[2]}${chars[2]}`, 16);
235
+ return `rgba(${r},${g},${b},0)`;
236
+ }
237
+
238
+ return 'transparent';
239
+ }
@@ -0,0 +1,207 @@
1
+ # Radio
2
+
3
+ Radio 是三端一致的单选组件,适合互斥选项、设置项模式选择和表单中的单一值选择。组件使用自绘 Pressable + Reanimated 路径,而不是依赖平台原生 radio,以保证 iOS / Android / Web 的尺寸、动效、主题和禁用态表现一致。
4
+
5
+ ## 设计约定
6
+
7
+ - `RadioGroup` 使用 `value/defaultValue/onChange` 管理单一选中值
8
+ - 组内 Radio 使用 `value` 作为身份标识,类型为 `string | number`
9
+ - 受控模式完全跟随外部 `value/checked`,不会乐观漂移
10
+ - 非受控模式先在 UI 线程推进动画,再同步内部状态
11
+ - 已选项默认再次点击不取消;筛选类场景需要清空时显式开启 `allowDeselect`
12
+ - `size/tone/variant` 提供语义化外观,`colors/layout` 作为集中 escape hatch
13
+ - 默认触控热区至少补到 `wp(44)`,小尺寸也保持可点性
14
+
15
+ ## RadioGroup
16
+
17
+ ```tsx
18
+ import * as React from 'react';
19
+ import { Radio, RadioGroup, Text } from 'zkit-ui';
20
+ import { wp } from 'zkit-tools';
21
+
22
+ type Density = 'compact' | 'comfortable' | 'spacious';
23
+
24
+ export function Demo() {
25
+ const [value, setValue] = React.useState<Density | null>('comfortable');
26
+
27
+ return (
28
+ <>
29
+ <RadioGroup<Density>
30
+ value={value}
31
+ onChange={setValue}
32
+ orientation="vertical"
33
+ gap={wp(10)}
34
+ >
35
+ <Radio value="compact" label="紧凑" />
36
+ <Radio value="comfortable" label="舒适" />
37
+ <Radio value="spacious" label="宽松" />
38
+ </RadioGroup>
39
+ <Text>当前:{value ?? '未选择'}</Text>
40
+ </>
41
+ );
42
+ }
43
+ ```
44
+
45
+ ## 可清空选择
46
+
47
+ Radio 默认遵循单选控件直觉:点击已选项不会取消。需要筛选器这类“可回到未选择”的场景时,在组上开启 `allowDeselect`,再次点击当前项会触发 `onChange(null)`。
48
+
49
+ ```tsx
50
+ <RadioGroup
51
+ value={sort}
52
+ onChange={setSort}
53
+ allowDeselect
54
+ gap={wp(10)}
55
+ >
56
+ <Radio value="latest" label="最新" />
57
+ <Radio value="popular" label="最热" />
58
+ </RadioGroup>
59
+ ```
60
+
61
+ ## 外观与主题
62
+
63
+ ```tsx
64
+ <Radio label="默认" />
65
+ <Radio label="柔和" variant="soft" />
66
+ <Radio label="实心" variant="solid" />
67
+ <Radio label="成功色" tone="success" />
68
+ <Radio label="自定义主色" color="#0EA5E9" />
69
+ <Radio colors={{ uncheckedBorder: '#CBD5E1' }} label="自定义颜色" />
70
+ ```
71
+
72
+ 像素类自定义值应在调用侧用 `wp(...)` 计算后传入:
73
+
74
+ ```tsx
75
+ <Radio
76
+ label="大尺寸"
77
+ layout={{
78
+ indicatorSize: wp(24),
79
+ indicatorDotSize: wp(11),
80
+ gap: wp(12),
81
+ }}
82
+ />
83
+ ```
84
+
85
+ ## 自定义内容
86
+
87
+ `children` 支持 render-prop。根节点本身就是可点击区域,子节点通常只读取状态做视觉变化,不需要再绑定一次点击。
88
+
89
+ ```tsx
90
+ <RadioGroup value={plan} onChange={setPlan} orientation="vertical" gap={wp(10)}>
91
+ {plans.map((item) => (
92
+ <Radio key={item.id} value={item.id} showIndicator={false}>
93
+ {({ checked }) => (
94
+ <View
95
+ style={{
96
+ paddingHorizontal: wp(12),
97
+ paddingVertical: wp(12),
98
+ borderRadius: wp(8),
99
+ borderWidth: wp(1),
100
+ borderColor: checked ? '#111827' : '#E5E7EB',
101
+ backgroundColor: checked ? '#111827' : '#FFFFFF',
102
+ }}
103
+ >
104
+ <Text tone={checked ? 'inverse' : 'default'}>{item.label}</Text>
105
+ </View>
106
+ )}
107
+ </Radio>
108
+ ))}
109
+ </RadioGroup>
110
+ ```
111
+
112
+ 需要完全替换圆点内容时使用 `indicator`,或在自定义内容中放置 `RadioIndicator`:
113
+
114
+ ```tsx
115
+ <Radio
116
+ checked={checked}
117
+ onChange={setChecked}
118
+ indicator={({ checked }) => <YourIcon name={checked ? 'dot' : 'circle'} />}
119
+ label="自定义指示器"
120
+ />
121
+ ```
122
+
123
+ ## 单个 Radio
124
+
125
+ 单个 Radio 支持 `checked/defaultChecked/onChange`,但它仍然遵循 radio 语义:点击已选中项默认不取消。协议勾选、开关设置这类布尔交互应优先使用 Checkbox 或 Switch。
126
+
127
+ ```tsx
128
+ <Radio checked={checked} onChange={setChecked} allowDeselect label="可取消的单个选项" />
129
+ ```
130
+
131
+ ## Props
132
+
133
+ Radio 透传 React Native `Pressable` 的常用属性和事件;组件自身固定使用 `accessibilityRole="radio"`,并维护 `checked/disabled` 状态。
134
+
135
+ ### 状态
136
+
137
+ - `value?: string | number`:在 `RadioGroup` 内参与单选值匹配。
138
+ - `checked?: boolean`:单个 Radio 的受控选中态。
139
+ - `defaultChecked?: boolean`:单个 Radio 的非受控初始状态,默认 `false`。
140
+ - `onChange?: (checked: boolean) => void`:单个 Radio 状态变化回调;组内 Radio 被用户点击选中或清空时也会触发。
141
+ - `allowDeselect?: boolean`:是否允许点击已选项后清空,默认跟随组配置或 `false`。
142
+ - `disabled?: boolean`:禁用,默认 `false`;组禁用会叠加到组内所有 Radio。
143
+
144
+ ### 外观
145
+
146
+ - `size?: 'sm' | 'md' | 'lg'`:尺寸,默认 `'md'`。
147
+ - `tone?: 'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'info'`:语义色,默认 `'primary'`。
148
+ - `variant?: 'outline' | 'soft' | 'solid'`:选中态视觉样式,默认 `'outline'`。
149
+ - `color?: string`:主色覆盖,支持主题 token、语义色或颜色字符串。
150
+ - `colors?: RadioColors`:结构化颜色覆盖。
151
+ - `layout?: RadioLayout`:结构化尺寸覆盖。
152
+ - `duration?: number`:状态切换动画时长,默认 `170`。
153
+
154
+ ### 内容
155
+
156
+ - `label?: ReactNode`:快捷主文案;字符串会使用组件库 `Text` 渲染,并可推导 `accessibilityLabel`。
157
+ - `description?: ReactNode`:快捷辅助文案。
158
+ - `labelPlacement?: 'start' | 'end'`:文案位于指示器的逻辑起点或终点,默认 `'end'`,RTL 下会自动调整视觉顺序。
159
+ - `children?: ReactNode | ((slot: RadioSlotProps) => ReactNode)`:自定义内容区域。
160
+ - `showIndicator?: boolean`:是否渲染内置指示器,默认 `true`。
161
+ - `indicator?: ReactNode | ((slot: RadioSlotProps) => ReactNode)`:自定义指示器内容。
162
+
163
+ ### 样式 escape hatch
164
+
165
+ - `style?: Pressable['style']`:根 Pressable 样式。
166
+ - `contentStyle?: StyleProp<ViewStyle>`:内容行样式。
167
+ - `indicatorStyle?: StyleProp<ViewStyle>`:指示器样式。
168
+ - `labelStyle?: StyleProp<TextStyle>`:快捷主文案样式。
169
+ - `descriptionStyle?: StyleProp<TextStyle>`:快捷辅助文案样式。
170
+
171
+ ### RadioGroup
172
+
173
+ - `value?: string | number | null`:受控选中值。
174
+ - `defaultValue?: string | number | null`:非受控初始选中值。
175
+ - `onChange?: (value: string | number | null) => void`:选中值变化回调。
176
+ - `disabled?: boolean`:组禁用。
177
+ - `allowDeselect?: boolean`:是否允许点击已选项后清空为 `null`,默认 `false`。
178
+ - `orientation?: 'horizontal' | 'vertical'`:布局方向,默认 `'horizontal'`。
179
+ - `align?: 'start' | 'center' | 'end' | 'stretch'`:交叉轴对齐,默认 `'start'`。
180
+ - `wrap?: boolean`:横向布局是否换行,默认 `false`。
181
+ - `gap?: number` / `rowGap?: number` / `columnGap?: number`:组内间距,默认 `wp(12)`。
182
+ - `size` / `tone` / `variant` / `color` / `colors` / `layout`:作为组内 Radio 的默认外观配置,单个 Radio 传入同名 props 时优先生效。
183
+
184
+ ## 类型
185
+
186
+ ```ts
187
+ type RadioValue = string | number;
188
+
189
+ type RadioColors = {
190
+ checkedBackground?: string;
191
+ checkedBorder?: string;
192
+ checkedIndicator?: string;
193
+ uncheckedBackground?: string;
194
+ uncheckedBorder?: string;
195
+ focusRing?: string;
196
+ };
197
+
198
+ type RadioLayout = {
199
+ indicatorSize?: number;
200
+ indicatorDotSize?: number;
201
+ indicatorBorderWidth?: number;
202
+ gap?: number;
203
+ minTouchTarget?: number;
204
+ focusRingWidth?: number;
205
+ focusRingOffset?: number;
206
+ };
207
+ ```