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,157 @@
1
+ # Text
2
+
3
+ `Text` 是 zkit 的统一文字原语。它集中处理排版 token、主题色、字体缩放、截断和跨端字重差异,让业务和公共组件都走同一套文字规则。
4
+
5
+ ## 设计取舍
6
+
7
+ - `Text` 是无状态组件,没有受控/非受控语义。
8
+ - `variant` 表达排版语义,`tone` 表达颜色意图,`style` 只作为最终 escape hatch。
9
+ - 内置字号和行高按设计像素声明,组件内部统一通过 `wp(...)` 计算。
10
+ - 默认使用 `getMaxFontSizeMultiplier()` 限制字体缩放上限,单次可用 `maxFontSizeMultiplier` 覆盖。
11
+ - Android 默认关闭 `includeFontPadding`,用明确 lineHeight 保证 iOS / Android / Web 视觉高度更一致。
12
+ - `color` 支持主题 token、语义色 token 和原生可解析颜色;无效颜色会回落到 `tone`。
13
+ - 屏幕宽度变化时会重新计算排版尺寸,横竖屏和分屏场景不会卡在初始化尺寸。
14
+
15
+ ## 基础用法
16
+
17
+ ```tsx
18
+ import { Text } from 'zkit-ui';
19
+
20
+ export function Demo() {
21
+ return <Text>默认正文</Text>;
22
+ }
23
+ ```
24
+
25
+ 默认等价于:
26
+
27
+ - `variant="body"`
28
+ - `tone="default"`
29
+ - `size="md"`
30
+ - `weight="regular"`
31
+ - `maxFontSizeMultiplier={getMaxFontSizeMultiplier()}`
32
+
33
+ ## 排版语义
34
+
35
+ ```tsx
36
+ import { Text } from 'zkit-ui';
37
+
38
+ export function Demo() {
39
+ return (
40
+ <>
41
+ <Text variant="display">页面主标题</Text>
42
+ <Text variant="heading">分区标题</Text>
43
+ <Text variant="title">列表标题</Text>
44
+ <Text variant="body">正文内容</Text>
45
+ <Text variant="label">表单标签</Text>
46
+ <Text variant="caption" tone="muted">
47
+ 辅助说明
48
+ </Text>
49
+ <Text variant="code">const ready = true;</Text>
50
+ </>
51
+ );
52
+ }
53
+ ```
54
+
55
+ `variant` 只决定默认 `size / weight / fontFamily`,显式传入 `size`、`weight` 或 `style` 时会覆盖对应值。
56
+
57
+ ## 尺寸
58
+
59
+ ```tsx
60
+ <>
61
+ <Text size="2xs">2xs</Text>
62
+ <Text size="xs">xs</Text>
63
+ <Text size="sm">sm</Text>
64
+ <Text size="md">md</Text>
65
+ <Text size="lg">lg</Text>
66
+ <Text size="xl">xl</Text>
67
+ <Text size="2xl">2xl</Text>
68
+ <Text size="3xl">3xl</Text>
69
+ <Text size="4xl">4xl</Text>
70
+ <Text size={18} lineHeight={26}>
71
+ 自定义设计尺寸
72
+ </Text>
73
+ </>
74
+ ```
75
+
76
+ 内置尺寸:
77
+
78
+ | size | fontSize | lineHeight |
79
+ | --- | ---: | ---: |
80
+ | `2xs` | `wp(11)` | `wp(14)` |
81
+ | `xs` | `wp(12)` | `wp(16)` |
82
+ | `sm` | `wp(13)` | `wp(18)` |
83
+ | `md` | `wp(15)` | `wp(21)` |
84
+ | `lg` | `wp(17)` | `wp(24)` |
85
+ | `xl` | `wp(20)` | `wp(28)` |
86
+ | `2xl` | `wp(24)` | `wp(32)` |
87
+ | `3xl` | `wp(30)` | `wp(38)` |
88
+ | `4xl` | `wp(36)` | `wp(44)` |
89
+
90
+ 传入数字 `size` 或 `lineHeight` 时,数字代表未缩放的设计像素,组件内部会统一转换。
91
+
92
+ ## 颜色
93
+
94
+ ```tsx
95
+ <>
96
+ <Text tone="default">默认文字</Text>
97
+ <Text tone="muted">弱化文字</Text>
98
+ <Text tone="primary">强调文字</Text>
99
+ <Text tone="success">成功状态</Text>
100
+ <Text tone="warning">警示状态</Text>
101
+ <Text tone="danger">危险状态</Text>
102
+ <Text tone="disabled">禁用说明</Text>
103
+ <Text color="onPrimary">主题 token</Text>
104
+ <Text color="#DC2626">一次性颜色</Text>
105
+ </>
106
+ ```
107
+
108
+ 常用 `tone`:
109
+
110
+ - `default` / `neutral`:`theme.colors.onSurface`
111
+ - `muted`:`theme.colors.muted`
112
+ - `subtle` / `disabled`:`theme.colors.disabled`
113
+ - `primary`:`theme.colors.primary`
114
+ - `success` / `warning` / `danger` / `info`:内置语义状态色
115
+ - `inverse` / `onPrimary`:`theme.colors.onPrimary`
116
+ - `onSecondary`:`theme.colors.onSecondary`
117
+ - `inherit`:不设置颜色,用于嵌套文字继承父级颜色
118
+
119
+ ```tsx
120
+ <Text tone="primary">
121
+ 已选择 <Text tone="inherit" weight="bold">3</Text> 项
122
+ </Text>
123
+ ```
124
+
125
+ ## 截断、字重和数字
126
+
127
+ ```tsx
128
+ <>
129
+ <Text truncate>单行省略</Text>
130
+ <Text truncate={2}>最多两行</Text>
131
+ <Text weight="semibold">语义字重</Text>
132
+ <Text weight="850">自定义字重会归一化到 100-900</Text>
133
+ <Text tabularNumbers>12:08 / 98%</Text>
134
+ </>
135
+ ```
136
+
137
+ - `truncate` 是 `numberOfLines` 和默认 `ellipsizeMode="tail"` 的语义化快捷方式。
138
+ - `weight` 支持 `regular / medium / semibold / bold / heavy / black`,也支持 React Native 原生 `fontWeight`。
139
+ - Web 常见字重名会被映射到稳定数字权重;Android API 28 以下会降级到 `normal / bold`。
140
+ - `tabularNumbers` 用于计时器、金额、百分比等需要数字等宽的场景。
141
+
142
+ ## 原生能力
143
+
144
+ `Text` 继承 React Native `Text` 的原生能力,包括:
145
+
146
+ - `numberOfLines`
147
+ - `ellipsizeMode`
148
+ - `allowFontScaling`
149
+ - `maxFontSizeMultiplier`
150
+ - `selectable`
151
+ - `onPress`
152
+ - `onLongPress`
153
+ - `accessibilityRole`
154
+ - `accessibilityLabel`
155
+ - `testID`
156
+
157
+ 公共组件内部应优先使用 `variant / tone / size / weight / truncate`。只有确实需要局部覆盖布局、颜色或字体细节时再使用 `style`。
@@ -0,0 +1,450 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Platform,
4
+ StyleSheet,
5
+ Text as RNText,
6
+ type AccessibilityState,
7
+ type StyleProp,
8
+ type TextProps as RNTextProps,
9
+ type TextStyle,
10
+ processColor,
11
+ useWindowDimensions,
12
+ } from 'react-native';
13
+ import { getMaxFontSizeMultiplier, wp } from 'zkit-tools';
14
+ import type { Theme } from '../../theme/types';
15
+ import { useTheme } from '../../theme/useTheme';
16
+
17
+ export type TextVariant = 'body' | 'label' | 'caption' | 'title' | 'heading' | 'display' | 'code';
18
+ export type TextSize = '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
19
+ export type TextSizeValue = TextSize | number;
20
+ export type TextTone =
21
+ | 'default'
22
+ | 'neutral'
23
+ | 'muted'
24
+ | 'subtle'
25
+ | 'primary'
26
+ | 'success'
27
+ | 'warning'
28
+ | 'danger'
29
+ | 'info'
30
+ | 'disabled'
31
+ | 'inverse'
32
+ | 'onPrimary'
33
+ | 'onSecondary'
34
+ | 'inherit';
35
+ export type TextWeight =
36
+ | 'regular'
37
+ | 'medium'
38
+ | 'semibold'
39
+ | 'bold'
40
+ | 'heavy'
41
+ | 'black'
42
+ | NonNullable<TextStyle['fontWeight']>;
43
+ export type TextTruncate = boolean | number;
44
+
45
+ type NativeTextProps = Omit<RNTextProps, 'style' | 'disabled' | 'numberOfLines' | 'ellipsizeMode'>;
46
+
47
+ export type TextRef = React.ComponentRef<typeof RNText>;
48
+
49
+ export interface TextProps extends NativeTextProps {
50
+ /**
51
+ * Semantic typography preset. Prefer this over raw font styles.
52
+ */
53
+ variant?: TextVariant;
54
+ /**
55
+ * Named type scale token, or an unscaled design pixel size that will be resolved through wp(...).
56
+ */
57
+ size?: TextSizeValue;
58
+ /**
59
+ * Optional line height in design pixels. Style still wins as the final escape hatch.
60
+ */
61
+ lineHeight?: number;
62
+ weight?: TextWeight;
63
+ tone?: TextTone;
64
+ /**
65
+ * Theme color token, semantic token, or processable native color.
66
+ */
67
+ color?: string;
68
+ align?: TextStyle['textAlign'];
69
+ transform?: TextStyle['textTransform'];
70
+ truncate?: TextTruncate;
71
+ tabularNumbers?: boolean;
72
+ disabled?: boolean;
73
+ numberOfLines?: RNTextProps['numberOfLines'];
74
+ ellipsizeMode?: RNTextProps['ellipsizeMode'];
75
+ style?: StyleProp<TextStyle>;
76
+ }
77
+
78
+ type TypographyToken = {
79
+ fontSize: number;
80
+ lineHeight: number;
81
+ };
82
+
83
+ type VariantToken = {
84
+ size: TextSize;
85
+ weight: TextWeight;
86
+ fontFamily?: TextStyle['fontFamily'];
87
+ };
88
+
89
+ const SEMANTIC_COLORS = {
90
+ danger: '#DC2626',
91
+ error: '#DC2626',
92
+ info: '#2563EB',
93
+ success: '#16A34A',
94
+ warn: '#D97706',
95
+ warning: '#D97706',
96
+ } as const;
97
+
98
+ const SIZE_TOKENS = {
99
+ '2xs': { fontSize: 11, lineHeight: 14 },
100
+ xs: { fontSize: 12, lineHeight: 16 },
101
+ sm: { fontSize: 13, lineHeight: 18 },
102
+ md: { fontSize: 15, lineHeight: 21 },
103
+ lg: { fontSize: 17, lineHeight: 24 },
104
+ xl: { fontSize: 20, lineHeight: 28 },
105
+ '2xl': { fontSize: 24, lineHeight: 32 },
106
+ '3xl': { fontSize: 30, lineHeight: 38 },
107
+ '4xl': { fontSize: 36, lineHeight: 44 },
108
+ } as const satisfies Record<TextSize, TypographyToken>;
109
+
110
+ const MONOSPACE_FONT_FAMILY = Platform.select({
111
+ android: 'monospace',
112
+ ios: 'Menlo',
113
+ web: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
114
+ default: 'monospace',
115
+ });
116
+
117
+ const VARIANT_TOKENS = {
118
+ body: {
119
+ size: 'md',
120
+ weight: 'regular',
121
+ },
122
+ label: {
123
+ size: 'sm',
124
+ weight: 'medium',
125
+ },
126
+ caption: {
127
+ size: 'xs',
128
+ weight: 'regular',
129
+ },
130
+ title: {
131
+ size: 'lg',
132
+ weight: 'semibold',
133
+ },
134
+ heading: {
135
+ size: '2xl',
136
+ weight: 'bold',
137
+ },
138
+ display: {
139
+ size: '3xl',
140
+ weight: 'bold',
141
+ },
142
+ code: {
143
+ size: 'sm',
144
+ weight: 'regular',
145
+ fontFamily: MONOSPACE_FONT_FAMILY,
146
+ },
147
+ } as const satisfies Record<TextVariant, VariantToken>;
148
+
149
+ const WEB_NAMED_WEIGHT_MAP: Record<string, number> = {
150
+ black: 900,
151
+ bold: 700,
152
+ book: 400,
153
+ demibold: 600,
154
+ extrabold: 800,
155
+ extralight: 200,
156
+ hairline: 100,
157
+ heavy: 900,
158
+ light: 300,
159
+ medium: 500,
160
+ normal: 400,
161
+ regular: 400,
162
+ semibold: 600,
163
+ thin: 100,
164
+ ultrabold: 800,
165
+ ultralight: 200,
166
+ };
167
+
168
+ const baseStyles = StyleSheet.create({
169
+ root: {
170
+ includeFontPadding: false,
171
+ margin: 0,
172
+ padding: 0,
173
+ },
174
+ });
175
+
176
+ function isFiniteNumber(value: unknown): value is number {
177
+ return typeof value === 'number' && Number.isFinite(value);
178
+ }
179
+
180
+ function isProcessableColor(color: string) {
181
+ return processColor(color) != null;
182
+ }
183
+
184
+ function normalizeFontWeight(weight: TextStyle['fontWeight']): TextStyle['fontWeight'] {
185
+ if (weight == null) return undefined;
186
+
187
+ const weightStr = String(weight).trim().toLowerCase();
188
+ const compactWeight = weightStr.replace(/[\s_-]/g, '');
189
+
190
+ let normalizedNumericWeight: number | undefined;
191
+
192
+ if (compactWeight === 'lighter') normalizedNumericWeight = 300;
193
+ if (compactWeight === 'bolder') normalizedNumericWeight = 700;
194
+
195
+ if (normalizedNumericWeight == null && WEB_NAMED_WEIGHT_MAP[compactWeight] != null) {
196
+ normalizedNumericWeight = WEB_NAMED_WEIGHT_MAP[compactWeight];
197
+ }
198
+
199
+ if (normalizedNumericWeight == null) {
200
+ const parsed = Number.parseInt(weightStr, 10);
201
+ if (Number.isNaN(parsed)) return 'normal';
202
+ normalizedNumericWeight = Math.round(parsed / 100) * 100;
203
+ }
204
+
205
+ const clamped = Math.max(100, Math.min(900, normalizedNumericWeight));
206
+ const androidApiLevel =
207
+ typeof Platform.Version === 'number' ? Platform.Version : Number.parseInt(String(Platform.Version), 10);
208
+
209
+ if (Platform.OS === 'android' && Number.isFinite(androidApiLevel) && androidApiLevel < 28) {
210
+ return clamped >= 600 ? 'bold' : 'normal';
211
+ }
212
+
213
+ if (clamped === 400) return 'normal';
214
+ if (clamped === 700) return 'bold';
215
+ return String(clamped) as TextStyle['fontWeight'];
216
+ }
217
+
218
+ function resolveTextWeight(weight: TextWeight): TextStyle['fontWeight'] {
219
+ switch (weight) {
220
+ case 'regular':
221
+ return '400';
222
+ case 'medium':
223
+ return '500';
224
+ case 'semibold':
225
+ return '600';
226
+ case 'bold':
227
+ return '700';
228
+ case 'heavy':
229
+ return '800';
230
+ case 'black':
231
+ return '900';
232
+ default:
233
+ return weight;
234
+ }
235
+ }
236
+
237
+ function resolveToneColor(colors: Theme['colors'], tone: TextTone): string | undefined {
238
+ switch (tone) {
239
+ case 'inherit':
240
+ return undefined;
241
+ case 'primary':
242
+ return colors.primary;
243
+ case 'success':
244
+ return SEMANTIC_COLORS.success;
245
+ case 'warning':
246
+ return SEMANTIC_COLORS.warning;
247
+ case 'danger':
248
+ return SEMANTIC_COLORS.danger;
249
+ case 'info':
250
+ return SEMANTIC_COLORS.info;
251
+ case 'muted':
252
+ return colors.muted;
253
+ case 'subtle':
254
+ return colors.disabled;
255
+ case 'disabled':
256
+ return colors.disabled;
257
+ case 'inverse':
258
+ case 'onPrimary':
259
+ return colors.onPrimary;
260
+ case 'onSecondary':
261
+ return colors.onSecondary;
262
+ case 'default':
263
+ case 'neutral':
264
+ default:
265
+ return colors.onSurface;
266
+ }
267
+ }
268
+
269
+ function resolveColorToken(input: string | undefined, fallback: string | undefined, theme: Theme) {
270
+ if (input == null) return fallback;
271
+
272
+ const key = input.trim();
273
+ if (!key) return fallback;
274
+
275
+ const resolved =
276
+ key === 'default' || key === 'neutral' || key === 'onSurface'
277
+ ? theme.colors.onSurface
278
+ : key === 'primary'
279
+ ? theme.colors.primary
280
+ : key === 'onPrimary' || key === 'inverse'
281
+ ? theme.colors.onPrimary
282
+ : key === 'secondary'
283
+ ? theme.colors.secondary
284
+ : key === 'onSecondary'
285
+ ? theme.colors.onSecondary
286
+ : key === 'surface'
287
+ ? theme.colors.surface
288
+ : key === 'border'
289
+ ? theme.colors.border
290
+ : key === 'muted'
291
+ ? theme.colors.muted
292
+ : key === 'subtle' || key === 'disabled'
293
+ ? theme.colors.disabled
294
+ : SEMANTIC_COLORS[key as keyof typeof SEMANTIC_COLORS] ?? key;
295
+
296
+ return isProcessableColor(resolved) ? resolved : fallback;
297
+ }
298
+
299
+ function resolveTypographyToken(size: TextSizeValue): TypographyToken {
300
+ if (typeof size === 'string') {
301
+ return SIZE_TOKENS[size] ?? SIZE_TOKENS.md;
302
+ }
303
+
304
+ if (!isFiniteNumber(size)) return SIZE_TOKENS.md;
305
+
306
+ const fontSize = Math.max(0, size);
307
+ return {
308
+ fontSize,
309
+ lineHeight: Math.ceil(fontSize * 1.4),
310
+ };
311
+ }
312
+
313
+ function resolveTypographyStyle(size: TextSizeValue, explicitLineHeight: number | undefined): TextStyle {
314
+ const token = resolveTypographyToken(size);
315
+ return {
316
+ fontSize: wp(token.fontSize),
317
+ lineHeight: wp(isFiniteNumber(explicitLineHeight) ? Math.max(0, explicitLineHeight) : token.lineHeight),
318
+ };
319
+ }
320
+
321
+ function normalizeStyleFontWeight(style: StyleProp<TextStyle>): StyleProp<TextStyle> {
322
+ if (!style) return style;
323
+
324
+ const flattened = StyleSheet.flatten(style);
325
+ const fontWeight = flattened?.fontWeight;
326
+ if (fontWeight == null) return style;
327
+
328
+ const normalizedFontWeight = normalizeFontWeight(fontWeight);
329
+ if (normalizedFontWeight === fontWeight) return style;
330
+
331
+ return [style, { fontWeight: normalizedFontWeight }];
332
+ }
333
+
334
+ function resolveNumberOfLines(truncate: TextTruncate | undefined, numberOfLines: RNTextProps['numberOfLines']) {
335
+ if (truncate === true) return 1;
336
+ if (typeof truncate === 'number' && Number.isFinite(truncate)) {
337
+ return Math.max(1, Math.floor(truncate));
338
+ }
339
+ return numberOfLines;
340
+ }
341
+
342
+ function resolveAccessibilityState(
343
+ disabled: boolean,
344
+ accessibilityState: AccessibilityState | undefined
345
+ ): AccessibilityState | undefined {
346
+ if (!disabled) return accessibilityState;
347
+ return {
348
+ ...accessibilityState,
349
+ disabled: true,
350
+ };
351
+ }
352
+
353
+ const TextBase = React.forwardRef<TextRef, TextProps>(function Text(
354
+ {
355
+ variant = 'body',
356
+ size,
357
+ lineHeight,
358
+ weight,
359
+ tone = 'default',
360
+ color,
361
+ align,
362
+ transform,
363
+ truncate,
364
+ tabularNumbers = false,
365
+ disabled = false,
366
+ style,
367
+ maxFontSizeMultiplier,
368
+ allowFontScaling,
369
+ numberOfLines,
370
+ ellipsizeMode,
371
+ accessibilityState,
372
+ children,
373
+ ...rest
374
+ },
375
+ ref
376
+ ) {
377
+ const theme = useTheme();
378
+ const { width: viewportWidth } = useWindowDimensions();
379
+ const variantToken: VariantToken = VARIANT_TOKENS[variant] ?? VARIANT_TOKENS.body;
380
+ const resolvedSize = size ?? variantToken.size;
381
+ const resolvedWeight = weight ?? variantToken.weight;
382
+ const toneColor = resolveToneColor(theme.colors, disabled ? 'disabled' : tone);
383
+ const resolvedColor = resolveColorToken(color, toneColor, theme);
384
+ const resolvedNumberOfLines = resolveNumberOfLines(truncate, numberOfLines);
385
+ const resolvedEllipsizeMode = truncate ? (ellipsizeMode ?? 'tail') : ellipsizeMode;
386
+ const resolvedAccessibilityState = React.useMemo(
387
+ () => resolveAccessibilityState(disabled, accessibilityState),
388
+ [accessibilityState, disabled]
389
+ );
390
+
391
+ const typographyStyle = React.useMemo(
392
+ () => resolveTypographyStyle(resolvedSize, lineHeight),
393
+ [lineHeight, resolvedSize, viewportWidth]
394
+ );
395
+
396
+ const semanticStyle = React.useMemo<TextStyle>(() => {
397
+ const nextStyle: TextStyle = {
398
+ fontFamily: variantToken.fontFamily,
399
+ fontVariant: tabularNumbers ? ['tabular-nums'] : undefined,
400
+ fontWeight: normalizeFontWeight(resolveTextWeight(resolvedWeight)),
401
+ textAlign: align,
402
+ textTransform: transform,
403
+ };
404
+
405
+ if (resolvedColor !== undefined) {
406
+ nextStyle.color = resolvedColor;
407
+ }
408
+
409
+ return nextStyle;
410
+ }, [
411
+ align,
412
+ resolvedColor,
413
+ resolvedWeight,
414
+ tabularNumbers,
415
+ transform,
416
+ variantToken.fontFamily,
417
+ ]);
418
+
419
+ const normalizedStyle = React.useMemo(() => normalizeStyleFontWeight(style), [style]);
420
+ const finalStyle = React.useMemo<StyleProp<TextStyle>>(
421
+ () =>
422
+ normalizedStyle
423
+ ? [baseStyles.root, typographyStyle, semanticStyle, normalizedStyle]
424
+ : [baseStyles.root, typographyStyle, semanticStyle],
425
+ [normalizedStyle, semanticStyle, typographyStyle]
426
+ );
427
+
428
+ return (
429
+ <RNText
430
+ ref={ref}
431
+ {...rest}
432
+ accessibilityState={resolvedAccessibilityState}
433
+ allowFontScaling={allowFontScaling}
434
+ disabled={disabled}
435
+ ellipsizeMode={resolvedEllipsizeMode}
436
+ maxFontSizeMultiplier={
437
+ allowFontScaling === false ? maxFontSizeMultiplier : (maxFontSizeMultiplier ?? getMaxFontSizeMultiplier())
438
+ }
439
+ numberOfLines={resolvedNumberOfLines}
440
+ style={finalStyle}
441
+ >
442
+ {children}
443
+ </RNText>
444
+ );
445
+ });
446
+
447
+ TextBase.displayName = 'Text';
448
+
449
+ export const Text = React.memo(TextBase);
450
+ Text.displayName = 'Text';