react-native-x-components 0.1.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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/XActionSheet/index.d.ts +47 -0
  4. package/dist/XActionSheet/index.js +100 -0
  5. package/dist/XAnimatedSearchPanel/index.d.ts +33 -0
  6. package/dist/XAnimatedSearchPanel/index.js +295 -0
  7. package/dist/XAnimatedView/index.d.ts +77 -0
  8. package/dist/XAnimatedView/index.js +227 -0
  9. package/dist/XButton/index.d.ts +44 -0
  10. package/dist/XButton/index.js +126 -0
  11. package/dist/XCalendar/XCalendarPopup.d.ts +38 -0
  12. package/dist/XCalendar/XCalendarPopup.js +76 -0
  13. package/dist/XCalendar/index.d.ts +50 -0
  14. package/dist/XCalendar/index.js +241 -0
  15. package/dist/XCarousel/index.d.ts +72 -0
  16. package/dist/XCarousel/index.js +183 -0
  17. package/dist/XCascadeSelect/index.d.ts +39 -0
  18. package/dist/XCascadeSelect/index.js +147 -0
  19. package/dist/XChart/index.d.ts +78 -0
  20. package/dist/XChart/index.js +162 -0
  21. package/dist/XCheckbox/index.d.ts +61 -0
  22. package/dist/XCheckbox/index.js +128 -0
  23. package/dist/XConfirmForm/index.d.ts +69 -0
  24. package/dist/XConfirmForm/index.js +113 -0
  25. package/dist/XDivider/index.d.ts +33 -0
  26. package/dist/XDivider/index.js +61 -0
  27. package/dist/XDropdownMenu/index.d.ts +66 -0
  28. package/dist/XDropdownMenu/index.js +258 -0
  29. package/dist/XElevator/index.d.ts +52 -0
  30. package/dist/XElevator/index.js +138 -0
  31. package/dist/XForm/FormStore.d.ts +82 -0
  32. package/dist/XForm/FormStore.js +279 -0
  33. package/dist/XForm/index.d.ts +116 -0
  34. package/dist/XForm/index.js +212 -0
  35. package/dist/XForm/types.d.ts +82 -0
  36. package/dist/XForm/types.js +1 -0
  37. package/dist/XForm/useForm.d.ts +2 -0
  38. package/dist/XForm/useForm.js +19 -0
  39. package/dist/XForm/utils.d.ts +42 -0
  40. package/dist/XForm/utils.js +131 -0
  41. package/dist/XForm/validate.d.ts +20 -0
  42. package/dist/XForm/validate.js +118 -0
  43. package/dist/XFormPro/components/FieldTrigger.d.ts +14 -0
  44. package/dist/XFormPro/components/FieldTrigger.js +38 -0
  45. package/dist/XFormPro/components/XDateField.d.ts +22 -0
  46. package/dist/XFormPro/components/XDateField.js +22 -0
  47. package/dist/XFormPro/components/XSelectField.d.ts +16 -0
  48. package/dist/XFormPro/components/XSelectField.js +19 -0
  49. package/dist/XFormPro/index.d.ts +44 -0
  50. package/dist/XFormPro/index.js +111 -0
  51. package/dist/XFormPro/renderers.d.ts +24 -0
  52. package/dist/XFormPro/renderers.js +53 -0
  53. package/dist/XFormPro/types.d.ts +142 -0
  54. package/dist/XFormPro/types.js +1 -0
  55. package/dist/XImage/index.d.ts +13 -0
  56. package/dist/XImage/index.js +32 -0
  57. package/dist/XInput/index.d.ts +43 -0
  58. package/dist/XInput/index.js +136 -0
  59. package/dist/XLicensePlate/index.d.ts +40 -0
  60. package/dist/XLicensePlate/index.js +172 -0
  61. package/dist/XLocale/index.d.ts +23 -0
  62. package/dist/XLocale/index.js +174 -0
  63. package/dist/XModalForm/index.d.ts +32 -0
  64. package/dist/XModalForm/index.js +85 -0
  65. package/dist/XMultiSelect/index.d.ts +31 -0
  66. package/dist/XMultiSelect/index.js +131 -0
  67. package/dist/XNumberKeyboard/index.d.ts +57 -0
  68. package/dist/XNumberKeyboard/index.js +142 -0
  69. package/dist/XPicker/index.d.ts +35 -0
  70. package/dist/XPicker/index.js +96 -0
  71. package/dist/XPickerDate/index.d.ts +58 -0
  72. package/dist/XPickerDate/index.js +234 -0
  73. package/dist/XPopupProvider/index.d.ts +23 -0
  74. package/dist/XPopupProvider/index.js +6 -0
  75. package/dist/XProgress/index.d.ts +49 -0
  76. package/dist/XProgress/index.js +196 -0
  77. package/dist/XProviders/XImagePreview/index.d.ts +44 -0
  78. package/dist/XProviders/XImagePreview/index.js +164 -0
  79. package/dist/XProviders/XLoadingModal/index.d.ts +46 -0
  80. package/dist/XProviders/XLoadingModal/index.js +119 -0
  81. package/dist/XProviders/XToast/index.d.ts +50 -0
  82. package/dist/XProviders/XToast/index.js +183 -0
  83. package/dist/XProviders/index.d.ts +31 -0
  84. package/dist/XProviders/index.js +28 -0
  85. package/dist/XProviders/usageExample.d.ts +19 -0
  86. package/dist/XProviders/usageExample.js +83 -0
  87. package/dist/XPullView/index.d.ts +76 -0
  88. package/dist/XPullView/index.js +220 -0
  89. package/dist/XRadio/index.d.ts +83 -0
  90. package/dist/XRadio/index.js +186 -0
  91. package/dist/XRecord/index.d.ts +37 -0
  92. package/dist/XRecord/index.js +160 -0
  93. package/dist/XSignature/XSignatureSkia.d.ts +39 -0
  94. package/dist/XSignature/XSignatureSkia.js +184 -0
  95. package/dist/XSignature/core.d.ts +69 -0
  96. package/dist/XSignature/core.js +108 -0
  97. package/dist/XSignature/index.d.ts +33 -0
  98. package/dist/XSignature/index.js +88 -0
  99. package/dist/XTabs/index.d.ts +65 -0
  100. package/dist/XTabs/index.js +210 -0
  101. package/dist/XTag/index.d.ts +61 -0
  102. package/dist/XTag/index.js +153 -0
  103. package/dist/XTopView/index.d.ts +25 -0
  104. package/dist/XTopView/index.js +8 -0
  105. package/dist/XTopViewStore/index.d.ts +45 -0
  106. package/dist/XTopViewStore/index.js +49 -0
  107. package/dist/XUpload/XUploadImage.d.ts +37 -0
  108. package/dist/XUpload/XUploadImage.js +142 -0
  109. package/dist/XUpload/XUploadVideo.d.ts +24 -0
  110. package/dist/XUpload/XUploadVideo.js +83 -0
  111. package/dist/XUpload/adapters.d.ts +50 -0
  112. package/dist/XUpload/adapters.js +110 -0
  113. package/dist/XUpload/helpers.d.ts +35 -0
  114. package/dist/XUpload/helpers.js +85 -0
  115. package/dist/XUpload/index.d.ts +12 -0
  116. package/dist/XUpload/index.js +5 -0
  117. package/dist/XUpload/provider.d.ts +26 -0
  118. package/dist/XUpload/provider.js +43 -0
  119. package/dist/XUpload/types.d.ts +31 -0
  120. package/dist/XUpload/types.js +4 -0
  121. package/dist/XWheel/index.d.ts +49 -0
  122. package/dist/XWheel/index.js +263 -0
  123. package/dist/index.d.ts +113 -0
  124. package/dist/index.js +127 -0
  125. package/dist/theme.d.ts +164 -0
  126. package/dist/theme.js +161 -0
  127. package/package.json +78 -0
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ScrollView, StyleSheet, Text, View } from 'react-native';
3
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
4
+ import { XButton } from '../XButton';
5
+ import { useXTheme } from '../theme';
6
+ import { XImagePreviewProvider, XImagePreviewService, XLoadingModalProvider, XLoadingModalService, XToastProvider, XToastService, } from './';
7
+ /** 在线演示图片(picsum 随机图床) */
8
+ const ONLINE_IMAGES = [
9
+ 'https://picsum.photos/id/1015/1200/800',
10
+ 'https://picsum.photos/id/1016/1200/800',
11
+ 'https://picsum.photos/id/1018/1200/800',
12
+ 'https://picsum.photos/id/1024/1200/800',
13
+ ];
14
+ /** 演示区块 */
15
+ const Section = ({ title, description, children }) => {
16
+ const theme = useXTheme();
17
+ return (_jsxs(View, { style: [styles.section, { backgroundColor: theme.colorBgContainer, borderRadius: theme.borderRadiusXL }], children: [_jsx(Text, { style: [styles.sectionTitle, { color: theme.colorText }], children: title }), !!description && _jsx(Text, { style: [styles.sectionDesc, { color: theme.colorTextSecondary }], children: description }), _jsx(View, { style: styles.sectionBody, children: children })] }));
18
+ };
19
+ const XProvidersDemo = () => {
20
+ const theme = useXTheme();
21
+ // ---- XToast ----
22
+ const showToast = (type) => {
23
+ const map = {
24
+ success: '保存成功',
25
+ error: '网络错误,请重试',
26
+ info: '当前版本 v2.3.1',
27
+ warning: '存储空间不足',
28
+ loading: '正在加载...',
29
+ text: '这是一条纯文本 Toast',
30
+ };
31
+ XToastService.show({ message: map[type], type });
32
+ };
33
+ // ---- XLoadingModal ----
34
+ const showLoadingOnce = () => {
35
+ XLoadingModalService.show({ title: '加载中...' });
36
+ setTimeout(() => XLoadingModalService.hide(), 2000);
37
+ };
38
+ /** 并发场景:两个 show + 两个 hide,验证引用计数(弹窗不会提前消失) */
39
+ const showLoadingConcurrent = () => {
40
+ XLoadingModalService.show({ title: '第一步...' });
41
+ setTimeout(() => XLoadingModalService.show({ title: '第二步...' }), 600);
42
+ setTimeout(() => XLoadingModalService.hide(), 1500);
43
+ // 第二个 hide(2.6s)之前,第一步的 hide 不会让弹窗提前关闭
44
+ setTimeout(() => XLoadingModalService.hide(), 2600);
45
+ };
46
+ // ---- XImagePreview ----
47
+ const previewImages = (initialIndex) => {
48
+ XImagePreviewService.show({ images: ONLINE_IMAGES, initialIndex });
49
+ };
50
+ return (_jsx(SafeAreaProvider, { children: _jsx(XLoadingModalProvider, { children: _jsx(XImagePreviewProvider, { children: _jsx(XToastProvider, { children: _jsxs(ScrollView, { style: [styles.screen, { backgroundColor: theme.colorBgLayout }], contentContainerStyle: styles.content, children: [_jsxs(Section, { title: 'XToast \u8F7B\u63D0\u793A', description: '\u6DF1\u8272\u80F6\u56CA + \u8BED\u4E49\u56FE\u6807\uFF0C\u652F\u6301 top / center / bottom \u4E09\u4E2A\u4F4D\u7F6E', children: [_jsxs(View, { style: styles.row, children: [_jsx(XButton, { type: 'primary', size: 'small', onPress: () => showToast('success'), children: "\u6210\u529F" }), _jsx(XButton, { type: 'primary', danger: true, size: 'small', onPress: () => showToast('error'), children: "\u5931\u8D25" }), _jsx(XButton, { type: 'primary', size: 'small', onPress: () => showToast('info'), children: "\u4FE1\u606F" }), _jsx(XButton, { type: 'primary', size: 'small', onPress: () => showToast('warning'), children: "\u8B66\u544A" })] }), _jsxs(View, { style: styles.row, children: [_jsx(XButton, { size: 'small', onPress: () => showToast('loading'), children: "\u52A0\u8F7D\u4E2D" }), _jsx(XButton, { size: 'small', onPress: () => showToast('text'), children: "\u7EAF\u6587\u672C" }), _jsx(XButton, { size: 'small', onPress: () => XToastService.show({ message: '我是顶部 Toast', type: 'info', position: 'top' }), children: "\u9876\u90E8" }), _jsx(XButton, { size: 'small', onPress: () => XToastService.show({ message: '我是居中 Toast', type: 'info', position: 'center' }), children: "\u5C45\u4E2D" })] })] }), _jsx(Section, { title: 'XLoadingModal \u5168\u5C40\u52A0\u8F7D', description: 'show/hide \u5E26\u5F15\u7528\u8BA1\u6570\uFF0C\u5E76\u53D1\u8BF7\u6C42\u4E0D\u4F1A\u4E92\u76F8\u6253\u65AD', children: _jsxs(View, { style: styles.row, children: [_jsx(XButton, { type: 'primary', size: 'small', onPress: showLoadingOnce, children: "\u5355\u6B21\u52A0\u8F7D 2s" }), _jsx(XButton, { type: 'primary', size: 'small', onPress: showLoadingConcurrent, children: "\u5E76\u53D1\u52A0\u8F7D\u6F14\u793A" })] }) }), _jsx(Section, { title: 'XImagePreview \u56FE\u7247\u9884\u89C8', description: '\u5728\u7EBF\u56FE\u7247\u6F14\u793A\uFF0C\u53CC\u6307\u7F29\u653E / \u5DE6\u53F3\u7FFB\u9875 / \u70B9\u51FB\u5173\u95ED', children: _jsxs(View, { style: styles.row, children: [_jsx(XButton, { type: 'primary', size: 'small', onPress: () => previewImages(0), children: "\u9884\u89C8\uFF08\u7B2C\u4E00\u5F20\uFF09" }), _jsx(XButton, { type: 'primary', size: 'small', onPress: () => previewImages(3), children: "\u9884\u89C8\uFF08\u7B2C\u56DB\u5F20\uFF09" })] }) })] }) }) }) }) }));
51
+ };
52
+ const styles = StyleSheet.create({
53
+ screen: {
54
+ flex: 1,
55
+ },
56
+ content: {
57
+ padding: 16,
58
+ paddingBottom: 40,
59
+ },
60
+ section: {
61
+ marginBottom: 16,
62
+ padding: 16,
63
+ },
64
+ sectionTitle: {
65
+ fontSize: 16,
66
+ fontWeight: '600',
67
+ },
68
+ sectionDesc: {
69
+ marginTop: 4,
70
+ fontSize: 12,
71
+ lineHeight: 18,
72
+ },
73
+ sectionBody: {
74
+ marginTop: 12,
75
+ },
76
+ row: {
77
+ flexDirection: 'row',
78
+ flexWrap: 'wrap',
79
+ gap: 10,
80
+ marginBottom: 10,
81
+ },
82
+ });
83
+ export default XProvidersDemo;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * ============================================================================
3
+ * XPullView —— 通用弹出层(duxapp/duxui XPullView 的 RN 移植版)
4
+ * ============================================================================
5
+ *
6
+ * 【一句话理解】
7
+ * 它不渲染任何东西到页面里(外层组件 return null),而是把弹层
8
+ * "托管"给全局 XTopView 宿主,由宿主在应用根部绘制。
9
+ *
10
+ * 【为什么这比 RN 原生 Modal 丝滑?】
11
+ * 1. 不是原生 Modal 窗口:没有原生窗口创建/销毁的开销;
12
+ * 2. Reanimated 在 UI 线程驱动动画(worklet):JS 线程不参与逐帧运算,
13
+ * 动画期间 React 零重渲染;
14
+ * 3. 只动画 transform + opacity(GPU 合成属性),不触发布局;
15
+ * 4. 短时长 + ease-out 入场(默认 200ms,原生 Modal 约 300ms),起步快收尾利落;
16
+ * 5. 正确的挂载时机:第一帧就是隐藏态(面板在屏幕外/透明),下一帧才开始
17
+ * 入场动画,不存在"内容先闪现再移动"。
18
+ *
19
+ * 【组件分工】
20
+ * - XPullView(对外壳):监听 visible 变化,用 XTopViewStore 的 add/update
21
+ * 把 PullOverlay 挂载/更新/移除;
22
+ * - PullOverlay(核心层):真正渲染遮罩+面板,负责入场/离场动画。
23
+ *
24
+ * 【生命周期(关键,多看几遍)】
25
+ * 打开:visible=true
26
+ * → add(<PullOverlay visible />) 组件挂载,第一帧是隐藏态
27
+ * → 下一帧 rAF 触发入场动画(translateY: 屏高→0, ease-out)
28
+ *
29
+ * 关闭:visible=false
30
+ * → update(同一个 key, <PullOverlay visible={false} />)
31
+ * → React 按 key 复用同一个组件实例,只更新 props → 触发离场动画
32
+ * → 动画播完(duration+50ms)才 remove(),从宿主中摘掉
33
+ *
34
+ * 再次打开:visible=true
35
+ * → update 回 visible=true → 同一个实例重新播放入场动画
36
+ * (此时 shared value 正处于隐藏态,正好无缝衔接)
37
+ * ============================================================================
38
+ */
39
+ import React from 'react';
40
+ import { StyleProp, ViewStyle } from 'react-native';
41
+ /** 弹出方向:bottom 从底部滑入 / top 从顶部 / left、right 从两侧 / center 居中缩放 */
42
+ export type XPullSide = 'bottom' | 'top' | 'left' | 'right' | 'center';
43
+ /** XPullView 对外 API */
44
+ interface XPullViewProps {
45
+ /** 是否显示(受控组件,与原生 Modal 的 visible 用法一致) */
46
+ visible: boolean;
47
+ /** 关闭回调:点遮罩(maskClosable)或需要关闭时触发 */
48
+ onClose?: () => void;
49
+ /** 弹出方向,默认 bottom */
50
+ side?: XPullSide;
51
+ /** 动画时长,默认 200ms(duxui 同款) */
52
+ duration?: number;
53
+ /** 遮罩最终透明度,默认 0.5(越接近 1 越黑) */
54
+ overlayOpacity?: number;
55
+ /** 是否显示黑色遮罩,默认 true */
56
+ mask?: boolean;
57
+ /** 点击遮罩是否关闭,默认 true */
58
+ maskClosable?: boolean;
59
+ /** 面板额外样式 */
60
+ style?: StyleProp<ViewStyle>;
61
+ children?: React.ReactNode;
62
+ }
63
+ /**
64
+ * XPullView —— 对外组件(薄壳)
65
+ *
66
+ * 【为什么壳要这么设计?】
67
+ * 弹层要一直"活着"到离场动画播完,所以不能 visible=false 就直接卸载。
68
+ * 这里用一个 keyRef 记录宿主中的 key:
69
+ * - visible=true && 无 key → add 挂载
70
+ * - visible=true && 有 key → update 更新(比如 children 变了)
71
+ * - visible=false && 有 key → update 成离场态,由 PullOverlay 播完动画后
72
+ * 调用 onExitEnd → remove
73
+ * - 组件自身卸载(页面销毁)→ 兜底 remove
74
+ */
75
+ export declare const XPullView: ({ visible, onClose, side, duration, overlayOpacity, mask, maskClosable, style, children, }: XPullViewProps) => null;
76
+ export {};
@@ -0,0 +1,220 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XPullView —— 通用弹出层(duxapp/duxui XPullView 的 RN 移植版)
5
+ * ============================================================================
6
+ *
7
+ * 【一句话理解】
8
+ * 它不渲染任何东西到页面里(外层组件 return null),而是把弹层
9
+ * "托管"给全局 XTopView 宿主,由宿主在应用根部绘制。
10
+ *
11
+ * 【为什么这比 RN 原生 Modal 丝滑?】
12
+ * 1. 不是原生 Modal 窗口:没有原生窗口创建/销毁的开销;
13
+ * 2. Reanimated 在 UI 线程驱动动画(worklet):JS 线程不参与逐帧运算,
14
+ * 动画期间 React 零重渲染;
15
+ * 3. 只动画 transform + opacity(GPU 合成属性),不触发布局;
16
+ * 4. 短时长 + ease-out 入场(默认 200ms,原生 Modal 约 300ms),起步快收尾利落;
17
+ * 5. 正确的挂载时机:第一帧就是隐藏态(面板在屏幕外/透明),下一帧才开始
18
+ * 入场动画,不存在"内容先闪现再移动"。
19
+ *
20
+ * 【组件分工】
21
+ * - XPullView(对外壳):监听 visible 变化,用 XTopViewStore 的 add/update
22
+ * 把 PullOverlay 挂载/更新/移除;
23
+ * - PullOverlay(核心层):真正渲染遮罩+面板,负责入场/离场动画。
24
+ *
25
+ * 【生命周期(关键,多看几遍)】
26
+ * 打开:visible=true
27
+ * → add(<PullOverlay visible />) 组件挂载,第一帧是隐藏态
28
+ * → 下一帧 rAF 触发入场动画(translateY: 屏高→0, ease-out)
29
+ *
30
+ * 关闭:visible=false
31
+ * → update(同一个 key, <PullOverlay visible={false} />)
32
+ * → React 按 key 复用同一个组件实例,只更新 props → 触发离场动画
33
+ * → 动画播完(duration+50ms)才 remove(),从宿主中摘掉
34
+ *
35
+ * 再次打开:visible=true
36
+ * → update 回 visible=true → 同一个实例重新播放入场动画
37
+ * (此时 shared value 正处于隐藏态,正好无缝衔接)
38
+ * ============================================================================
39
+ */
40
+ import { useCallback, useEffect, useRef } from 'react';
41
+ import { Dimensions, Pressable, StyleSheet, View } from 'react-native';
42
+ import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated';
43
+ import { useXTopViewStore } from '../XTopViewStore';
44
+ /** 取一次屏幕宽高即可(旋转屏幕场景可改为监听 Dimensions 变化) */
45
+ const { width: windowWidth, height: windowHeight } = Dimensions.get('window');
46
+ /** 每种方向的"面板停靠位置":absolute 定位,配合位移动画实现滑入滑出 */
47
+ const SIDE_STYLES = {
48
+ bottom: { position: 'absolute', bottom: 0, left: 0, right: 0 }, // 贴底,宽度撑满
49
+ top: { position: 'absolute', top: 0, left: 0, right: 0 }, // 贴顶
50
+ left: { position: 'absolute', left: 0, top: 0, bottom: 0 }, // 贴左,高度撑满
51
+ right: { position: 'absolute', right: 0, top: 0, bottom: 0 }, // 贴右
52
+ center: {
53
+ // 居中:全屏 + flex 居中,面板在正中间
54
+ position: 'absolute',
55
+ left: 0,
56
+ right: 0,
57
+ top: 0,
58
+ bottom: 0,
59
+ justifyContent: 'center',
60
+ alignItems: 'center',
61
+ },
62
+ };
63
+ /**
64
+ * 重要!遮罩要用 Animated.createAnimatedComponent 包装成"动画组件"。
65
+ *
66
+ * 【踩坑记录】useAnimatedStyle 返回的动画样式,只有 Reanimated 的
67
+ * Animated.* 组件(Animated.View / Animated.Text / createAnimatedComponent
68
+ * 包装的组件)才会生效。之前直接传给普通 <Pressable>,opacity 动画根本没跑,
69
+ * 遮罩变成了纯黑(#000 没有透明度)。这一行就是修复。
70
+ */
71
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
72
+ /**
73
+ * PullOverlay —— 真正渲染在 XTopView 宿主中的弹层(遮罩 + 面板)
74
+ *
75
+ * 【动画原理】
76
+ * 4 个 shared value(UI 线程上的"共享变量")驱动一切:
77
+ * translate = 面板位移(隐藏时 = 屏高/屏宽,展示时 = 0)
78
+ * scale = 面板缩放(仅 center 方向用:0.85 → 1)
79
+ * opacity = 面板透明度(0 → 1)
80
+ * maskOpacity = 遮罩透明度(0 → overlayOpacity)
81
+ *
82
+ * withTiming 是"动画指令":告诉 Reanimated"用多少时长、什么缓动
83
+ * 把值动画到目标"。动画全程在 UI 线程跑,不经过 JS 线程和 React 渲染。
84
+ */
85
+ const PullOverlay = ({ visible, onClose, side = 'bottom', duration = 200, overlayOpacity = 0.5, mask = true, maskClosable = true, style, children, onExitEnd, }) => {
86
+ const isCenter = side === 'center';
87
+ const horizontal = side === 'left' || side === 'right';
88
+ // 隐藏位移取整屏尺寸:保证不管面板多高/多宽,都能完全移出屏幕外
89
+ const hiddenOffset = horizontal ? windowWidth : windowHeight;
90
+ // center 方向不位移,改用缩放(0.85 → 1),质感更柔和
91
+ const hiddenScale = isCenter ? 0.85 : 1;
92
+ // 初始值 = 隐藏态(面板在屏幕外、透明)→ 这就是"第一帧不闪现"的关键
93
+ const translate = useSharedValue(hiddenOffset);
94
+ const scale = useSharedValue(hiddenScale);
95
+ const opacity = useSharedValue(0);
96
+ const maskOpacity = useSharedValue(0);
97
+ /**
98
+ * 入场动画:visible 为 true 时执行。
99
+ * 用 requestAnimationFrame 包一层 → 保证组件先以隐藏态渲染完一帧,
100
+ * 下一帧才开启动画。如果直接在 useEffect 里改值,React 批处理可能
101
+ * 导致第一帧就直接以目标值渲染(看不到动画)。
102
+ */
103
+ useEffect(() => {
104
+ if (visible) {
105
+ const raf = requestAnimationFrame(() => {
106
+ // 入场用 ease-out:起步快、末尾减速,给人"轻快"的感觉
107
+ translate.value = withTiming(0, { duration, easing: Easing.out(Easing.ease) });
108
+ scale.value = withTiming(1, { duration, easing: Easing.out(Easing.ease) });
109
+ opacity.value = withTiming(1, { duration });
110
+ maskOpacity.value = withTiming(overlayOpacity, { duration });
111
+ });
112
+ // 组件卸载或 visible 变化时取消未执行的 rAF,防止内存泄漏/误触发
113
+ return () => cancelAnimationFrame(raf);
114
+ }
115
+ }, [visible, duration, overlayOpacity]);
116
+ /**
117
+ * 离场动画:visible 变为 false 时执行。
118
+ * 四个值反向动画回隐藏态,duration+50ms 后调用 onExitEnd 让宿主移除本层
119
+ * (+50ms 是安全余量,确保最后一帧已经渲染完成)。
120
+ * 注意清除 setTimeout:如果动画中途被重新打开,不能让旧的定时器把层移除。
121
+ */
122
+ useEffect(() => {
123
+ if (!visible) {
124
+ // 离场用 ease-in-out:中间快、两端慢,收尾自然
125
+ translate.value = withTiming(hiddenOffset, { duration, easing: Easing.inOut(Easing.ease) });
126
+ scale.value = withTiming(hiddenScale, { duration });
127
+ opacity.value = withTiming(0, { duration });
128
+ maskOpacity.value = withTiming(0, { duration });
129
+ const timer = setTimeout(onExitEnd, duration + 50);
130
+ return () => clearTimeout(timer);
131
+ }
132
+ }, [visible, duration, onExitEnd]);
133
+ /**
134
+ * useAnimatedStyle:把 shared value 映射成"动画样式"。
135
+ * 这个 worklet 函数在 UI 线程上每次动画帧都会执行,
136
+ * 返回值直接合成到视图样式上 —— 这就是动画期间 React 零重渲染的原因。
137
+ */
138
+ const panelAnimatedStyle = useAnimatedStyle(() => ({
139
+ opacity: opacity.value,
140
+ transform: isCenter
141
+ ? [{ scale: scale.value }] // 居中:只缩放
142
+ : horizontal
143
+ ? [{ translateX: translate.value }] // 左右:横向位移
144
+ : [{ translateY: translate.value }], // 上下:纵向位移
145
+ }));
146
+ /** 遮罩样式:纯黑底 + 动画透明度(0.5 就是常见的半透明黑) */
147
+ const maskAnimatedStyle = useAnimatedStyle(() => ({ opacity: maskOpacity.value }));
148
+ /** 点遮罩关闭(maskClosable 为 true 时) */
149
+ const handleMaskPress = useCallback(() => {
150
+ if (maskClosable) {
151
+ onClose?.();
152
+ }
153
+ }, [maskClosable, onClose]);
154
+ return (_jsxs(View, { style: StyleSheet.absoluteFill, pointerEvents: 'box-none', children: [mask && (_jsx(AnimatedPressable, { onPress: handleMaskPress, style: [StyleSheet.absoluteFill, { backgroundColor: '#000' }, maskAnimatedStyle] })), _jsx(Animated.View, { style: [SIDE_STYLES[side], panelAnimatedStyle, style], pointerEvents: 'box-none', children: _jsx(View, { pointerEvents: 'auto', style: isCenter ? undefined : styles.panelWrapper, children: children }) })] }));
155
+ };
156
+ /**
157
+ * XPullView —— 对外组件(薄壳)
158
+ *
159
+ * 【为什么壳要这么设计?】
160
+ * 弹层要一直"活着"到离场动画播完,所以不能 visible=false 就直接卸载。
161
+ * 这里用一个 keyRef 记录宿主中的 key:
162
+ * - visible=true && 无 key → add 挂载
163
+ * - visible=true && 有 key → update 更新(比如 children 变了)
164
+ * - visible=false && 有 key → update 成离场态,由 PullOverlay 播完动画后
165
+ * 调用 onExitEnd → remove
166
+ * - 组件自身卸载(页面销毁)→ 兜底 remove
167
+ */
168
+ export const XPullView = ({ visible, onClose, side = 'bottom', duration = 200, overlayOpacity = 0.5, mask = true, maskClosable = true, style, children, }) => {
169
+ // 从全局 store 取三个操作(选择器写法:只订阅这几个函数的引用)
170
+ const add = useXTopViewStore(s => s.add);
171
+ const update = useXTopViewStore(s => s.update);
172
+ const remove = useXTopViewStore(s => s.remove);
173
+ /** 记录当前弹层在宿主中的 key;null 表示没有挂载 */
174
+ const keyRef = useRef(null);
175
+ /**
176
+ * 根据 show 构建要挂载的弹层元素。
177
+ * useCallback 缓存:children/参数不变时复用同一引用,
178
+ * 避免 useEffect 因依赖变化反复执行。
179
+ */
180
+ const buildElement = useCallback((show) => (_jsx(PullOverlay, { visible: show, side: side, duration: duration, overlayOpacity: overlayOpacity, mask: mask, maskClosable: maskClosable, style: style, onClose: onClose, onExitEnd: () => {
181
+ // 离场播完:从宿主移除,并把 key 置空(下次打开重新 add)
182
+ if (keyRef.current) {
183
+ remove(keyRef.current);
184
+ keyRef.current = null;
185
+ }
186
+ }, children: children })), [children, duration, mask, maskClosable, onClose, overlayOpacity, remove, side, style]);
187
+ /** 核心逻辑:visible 变化 → 挂载/更新弹层 */
188
+ useEffect(() => {
189
+ if (visible) {
190
+ // 打开:没挂载就挂载(入场),已挂载就更新(比如重新打开或内容变化)
191
+ if (!keyRef.current) {
192
+ keyRef.current = add(buildElement(true));
193
+ }
194
+ else {
195
+ update(keyRef.current, buildElement(true));
196
+ }
197
+ }
198
+ else if (keyRef.current) {
199
+ // 关闭:更新成离场态,动画播完后 PullOverlay 会调用 onExitEnd 移除
200
+ update(keyRef.current, buildElement(false));
201
+ }
202
+ }, [visible, add, update, buildElement]);
203
+ /** 兜底:XPullView 自身被卸载时(比如页面 navigate 走了),清理宿主里的弹层 */
204
+ useEffect(() => {
205
+ return () => {
206
+ if (keyRef.current) {
207
+ remove(keyRef.current);
208
+ keyRef.current = null;
209
+ }
210
+ };
211
+ }, [remove]);
212
+ // 壳本身不渲染任何 UI,弹层都在 XTopView 宿主里
213
+ return null;
214
+ };
215
+ const styles = StyleSheet.create({
216
+ /** 非 center 方向:让面板宽度/高度撑满停靠边 */
217
+ panelWrapper: {
218
+ alignSelf: 'stretch',
219
+ },
220
+ });
@@ -0,0 +1,83 @@
1
+ /**
2
+ * ============================================================================
3
+ * XRadio —— 对标 antd Radio(RN 自实现版)
4
+ * ============================================================================
5
+ *
6
+ * 支持的 antd API:
7
+ * - <XRadio value onChange disabled checked defaultChecked>label</XRadio>
8
+ * - <XRadio.Group value defaultValue options onChange optionType buttonStyle disabled>
9
+ * - options: {label, value, disabled?}[],传了就不用手写子 Radio
10
+ * - optionType: 'default'(圆点单选) | 'button'(按钮单选,配合 options 使用)
11
+ * - buttonStyle: 'outline' | 'solid'(按钮单选的选中样式)
12
+ * - <XRadio.Button value>label</XRadio.Button>(手动组合按钮单选,各自独立胶囊)
13
+ *
14
+ * 与 antd 的差异:
15
+ * - onChange 回调参数是轻量合成事件 {value, checked, target}(antd 是 DOM 事件包装)
16
+ * - Radio.Button 手动组合时不做 antd 那种连体分段样式(连体只出现在
17
+ * Group options + optionType='button' 的自动渲染里,因为只有那一刻才拿得到索引)
18
+ */
19
+ import React from 'react';
20
+ import { StyleProp, ViewStyle, TextStyle } from 'react-native';
21
+ /** antd Radio 的 onChange 事件对象的 RN 等价物 */
22
+ export interface XRadioChangeEvent {
23
+ value: any;
24
+ checked: boolean;
25
+ target: {
26
+ value: any;
27
+ checked: boolean;
28
+ };
29
+ stopPropagation: () => void;
30
+ }
31
+ export interface XRadioGroupContextValue {
32
+ value?: any;
33
+ disabled?: boolean;
34
+ /** 圆点单选:走统一 onChange;按钮单选:Group 自渲染,同样走这里 */
35
+ onRadioChange: (value: any, checked: boolean) => void;
36
+ /** 是否是按钮形态(Group 内的 Radio.Button 交给 Group 统一渲染时使用) */
37
+ optionType?: 'default' | 'button';
38
+ buttonStyle?: 'outline' | 'solid';
39
+ }
40
+ export interface XRadioProps {
41
+ value?: any;
42
+ checked?: boolean;
43
+ defaultChecked?: boolean;
44
+ disabled?: boolean;
45
+ onChange?: (e: XRadioChangeEvent) => void;
46
+ children?: React.ReactNode;
47
+ style?: StyleProp<ViewStyle>;
48
+ textStyle?: StyleProp<TextStyle>;
49
+ testID?: string;
50
+ }
51
+ export declare function XRadio({ value, checked, defaultChecked, disabled, onChange, children, style, textStyle, testID }: XRadioProps): React.JSX.Element;
52
+ export declare namespace XRadio {
53
+ var Group: typeof XRadioGroup;
54
+ var Button: typeof XRadioButton;
55
+ }
56
+ export interface XRadioButtonProps {
57
+ value?: any;
58
+ disabled?: boolean;
59
+ checked?: boolean;
60
+ onChange?: (e: XRadioChangeEvent) => void;
61
+ children?: React.ReactNode;
62
+ style?: StyleProp<ViewStyle>;
63
+ testID?: string;
64
+ }
65
+ export declare function XRadioButton({ value, disabled, checked, onChange, children, style, testID }: XRadioButtonProps): React.JSX.Element;
66
+ export interface XRadioGroupProps {
67
+ value?: any;
68
+ defaultValue?: any;
69
+ options?: Array<{
70
+ label: React.ReactNode;
71
+ value: any;
72
+ disabled?: boolean;
73
+ }>;
74
+ onChange?: (e: XRadioChangeEvent) => void;
75
+ optionType?: 'default' | 'button';
76
+ buttonStyle?: 'outline' | 'solid';
77
+ disabled?: boolean;
78
+ children?: React.ReactNode;
79
+ style?: StyleProp<ViewStyle>;
80
+ testID?: string;
81
+ }
82
+ declare function XRadioGroup(props: XRadioGroupProps): React.JSX.Element;
83
+ export default XRadio;
@@ -0,0 +1,186 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XRadio —— 对标 antd Radio(RN 自实现版)
5
+ * ============================================================================
6
+ *
7
+ * 支持的 antd API:
8
+ * - <XRadio value onChange disabled checked defaultChecked>label</XRadio>
9
+ * - <XRadio.Group value defaultValue options onChange optionType buttonStyle disabled>
10
+ * - options: {label, value, disabled?}[],传了就不用手写子 Radio
11
+ * - optionType: 'default'(圆点单选) | 'button'(按钮单选,配合 options 使用)
12
+ * - buttonStyle: 'outline' | 'solid'(按钮单选的选中样式)
13
+ * - <XRadio.Button value>label</XRadio.Button>(手动组合按钮单选,各自独立胶囊)
14
+ *
15
+ * 与 antd 的差异:
16
+ * - onChange 回调参数是轻量合成事件 {value, checked, target}(antd 是 DOM 事件包装)
17
+ * - Radio.Button 手动组合时不做 antd 那种连体分段样式(连体只出现在
18
+ * Group options + optionType='button' 的自动渲染里,因为只有那一刻才拿得到索引)
19
+ */
20
+ import { createContext, useContext, useMemo, useState } from 'react';
21
+ import { Pressable, StyleSheet, View, Text } from 'react-native';
22
+ import { useXTheme, xTheme } from '../theme';
23
+ const RadioGroupContext = createContext(null);
24
+ function makeEvent(value, checked) {
25
+ const event = {
26
+ value,
27
+ checked,
28
+ target: { value, checked },
29
+ stopPropagation: () => { },
30
+ };
31
+ return event;
32
+ }
33
+ export function XRadio({ value, checked, defaultChecked, disabled, onChange, children, style, textStyle, testID }) {
34
+ const t = useXTheme();
35
+ const group = useContext(RadioGroupContext);
36
+ const [innerChecked, setInnerChecked] = useState(!!defaultChecked);
37
+ // Group 里的选中态由 Group value 决定;独立使用时受控/非受控自管理
38
+ const isChecked = group ? group.value !== undefined && group.value === value : checked ?? innerChecked;
39
+ const isDisabled = disabled || (group?.disabled ?? false);
40
+ const handlePress = () => {
41
+ if (isDisabled || isChecked)
42
+ return; // 单选已选中再点不触发(antd 同款)
43
+ if (!group) {
44
+ setInnerChecked(true);
45
+ onChange?.(makeEvent(value, true));
46
+ }
47
+ else {
48
+ group.onRadioChange(value, true);
49
+ }
50
+ };
51
+ return (_jsxs(Pressable, { testID: testID, onPress: handlePress, disabled: isDisabled, style: ({ pressed }) => [styles.radioRow, pressed && !isDisabled && styles.pressed, style], children: [_jsx(XRadioIndicator, { checked: isChecked, disabled: isDisabled }), children != null && (_jsx(Text, { style: [styles.radioLabel, { color: isDisabled ? t.colorTextQuaternary : t.colorText }, textStyle], allowFontScaling: false, children: children }))] }));
52
+ }
53
+ /** 圆点指示器:外圈 20 + 选中内点 10,颜色随状态 */
54
+ function XRadioIndicator({ checked, disabled }) {
55
+ const t = useXTheme();
56
+ const ringColor = disabled ? t.colorBorder : checked ? t.colorPrimary : t.colorBorder;
57
+ return (_jsx(View, { style: [styles.radioRing, { borderColor: ringColor }], children: checked && _jsx(View, { style: [styles.radioDot, { backgroundColor: disabled ? t.colorTextQuaternary : t.colorPrimary }] }) }));
58
+ }
59
+ export function XRadioButton({ value, disabled, checked, onChange, children, style, testID }) {
60
+ const t = useXTheme();
61
+ const group = useContext(RadioGroupContext);
62
+ const isChecked = group ? group.value === value : !!checked;
63
+ const isDisabled = disabled || (group?.disabled ?? false);
64
+ const buttonStyle = group?.buttonStyle ?? 'outline';
65
+ const handlePress = () => {
66
+ if (isDisabled || isChecked)
67
+ return;
68
+ if (group) {
69
+ group.onRadioChange(value, true);
70
+ }
71
+ else {
72
+ onChange?.(makeEvent(value, true));
73
+ }
74
+ };
75
+ // outline:选中=主色描边+主色字;solid:选中=主色实底+白字
76
+ const selected = buttonStyle === 'solid';
77
+ const bg = isDisabled ? t.colorBgContainerDisabled : isChecked && selected ? t.colorPrimary : t.colorBgContainer;
78
+ const textColor = isDisabled ? t.colorTextQuaternary : isChecked ? (selected ? t.colorTextLightSolid : t.colorPrimary) : t.colorTextSecondary;
79
+ const borderColor = isDisabled ? t.colorBorder : isChecked ? t.colorPrimary : t.colorBorder;
80
+ return (_jsx(Pressable, { testID: testID, onPress: handlePress, disabled: isDisabled, style: ({ pressed }) => [styles.buttonBase, { backgroundColor: bg, borderColor }, pressed && !isDisabled && styles.pressed, style], children: _jsx(Text, { style: [styles.buttonText, { color: textColor }], allowFontScaling: false, children: children }) }));
81
+ }
82
+ function XRadioGroup(props) {
83
+ const { value, defaultValue, options, onChange, optionType = 'default', buttonStyle = 'outline', disabled, children, style, testID } = props;
84
+ const [innerValue, setInnerValue] = useState(defaultValue);
85
+ // 受控模式:外部传了 value 就以外部为准;否则用内部态
86
+ const currentValue = value !== undefined ? value : innerValue;
87
+ const handleRadioChange = (v, checked) => {
88
+ if (value === undefined) {
89
+ setInnerValue(v);
90
+ }
91
+ onChange?.(makeEvent(v, checked));
92
+ };
93
+ const ctx = useMemo(() => ({ value: currentValue, disabled, onRadioChange: handleRadioChange, optionType, buttonStyle }), [currentValue, disabled, optionType, buttonStyle, value, onChange]);
94
+ return (_jsx(RadioGroupContext.Provider, { value: ctx, children: _jsx(View, { testID: testID, style: [styles.group, style], children: options ? (optionType === 'button' ? (_jsx(View, { style: styles.buttonRow, children: options.map((opt, idx) => (_jsx(GroupButton, { option: opt, idx: idx, total: options.length, checked: currentValue === opt.value, disabled: disabled ?? opt.disabled ?? false, solid: buttonStyle === 'solid', onPress: () => handleRadioChange(opt.value, true) }, String(opt.value)))) })) : (
95
+ // 圆点形态:横排换行
96
+ options.map(opt => (_jsx(XRadio, { value: opt.value, disabled: opt.disabled, children: opt.label }, String(opt.value)))))) : (children) }) }));
97
+ }
98
+ /** Group options + button 形态的连体按钮(需要知道 idx 才能算首尾圆角) */
99
+ function GroupButton({ option, idx, total, checked, disabled, solid, onPress, }) {
100
+ const t = useXTheme();
101
+ const isFirst = idx === 0;
102
+ const isLast = idx === total - 1;
103
+ const radius = xTheme.borderRadius;
104
+ const bg = disabled ? t.colorBgContainerDisabled : checked && solid ? t.colorPrimary : t.colorBgContainer;
105
+ const textColor = disabled ? t.colorTextQuaternary : checked ? (solid ? t.colorTextLightSolid : t.colorPrimary) : t.colorTextSecondary;
106
+ const borderColor = disabled ? t.colorBorder : checked ? t.colorPrimary : t.colorBorder;
107
+ return (_jsx(Pressable, { onPress: onPress, disabled: disabled, style: ({ pressed }) => [
108
+ styles.groupButton,
109
+ {
110
+ backgroundColor: bg,
111
+ borderColor,
112
+ borderTopLeftRadius: isFirst ? radius : 0,
113
+ borderBottomLeftRadius: isFirst ? radius : 0,
114
+ borderTopRightRadius: isLast ? radius : 0,
115
+ borderBottomRightRadius: isLast ? radius : 0,
116
+ // 连体:非首元素左移一个边框宽度,实现边框重叠(antd 的 -1px margin 同款思路)
117
+ marginLeft: isFirst ? 0 : -1,
118
+ // 后画的兄弟会盖住前一个的右边框:选中的按钮抬高层级,
119
+ // 保证自己的蓝色右边框压在后一个按钮的灰色左边框之上(antd 的 z-index:1 同款)
120
+ zIndex: checked ? 2 : 1,
121
+ },
122
+ pressed && !disabled && styles.pressed,
123
+ ], children: _jsx(Text, { style: [styles.buttonText, { color: textColor }], allowFontScaling: false, children: option.label }) }));
124
+ }
125
+ XRadio.Group = XRadioGroup;
126
+ XRadio.Button = XRadioButton;
127
+ const styles = StyleSheet.create({
128
+ radioRow: {
129
+ flexDirection: 'row',
130
+ alignItems: 'center',
131
+ paddingVertical: 8, // 扩大点击热区(对标 antd RN 的 Radio 触摸面积)
132
+ alignSelf: 'flex-start',
133
+ },
134
+ pressed: {
135
+ opacity: 0.7,
136
+ },
137
+ radioRing: {
138
+ width: 20,
139
+ height: 20,
140
+ borderRadius: 10,
141
+ borderWidth: 2,
142
+ alignItems: 'center',
143
+ justifyContent: 'center',
144
+ },
145
+ radioDot: {
146
+ width: 10,
147
+ height: 10,
148
+ borderRadius: 5,
149
+ },
150
+ radioLabel: {
151
+ fontSize: xTheme.fontSize,
152
+ marginLeft: 8,
153
+ },
154
+ buttonBase: {
155
+ height: xTheme.controlHeight,
156
+ paddingHorizontal: 14,
157
+ borderRadius: xTheme.borderRadius,
158
+ borderWidth: StyleSheet.hairlineWidth * 2,
159
+ alignItems: 'center',
160
+ justifyContent: 'center',
161
+ alignSelf: 'flex-start',
162
+ },
163
+ groupButton: {
164
+ height: xTheme.controlHeight,
165
+ paddingHorizontal: 14,
166
+ borderWidth: 1,
167
+ alignItems: 'center',
168
+ justifyContent: 'center',
169
+ },
170
+ buttonRow: {
171
+ flexDirection: 'row',
172
+ alignItems: 'center',
173
+ alignSelf: 'flex-start',
174
+ },
175
+ buttonText: {
176
+ fontSize: xTheme.fontSize,
177
+ },
178
+ group: {
179
+ flexDirection: 'row',
180
+ flexWrap: 'wrap',
181
+ alignItems: 'center',
182
+ rowGap: 8,
183
+ columnGap: 16,
184
+ },
185
+ });
186
+ export default XRadio;