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,113 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XConfirmForm —— 命令式确认框(duxui XConfirmForm 移植)
5
+ * ============================================================================
6
+ *
7
+ * 【声明式 vs 命令式】
8
+ * XActionSheet/XPicker 是"声明式":页面里写 <xxx visible={...} />。
9
+ * XConfirmForm 是"命令式":不用管 visible,直接调
10
+ * const ok = await XConfirmForm.show({...})
11
+ * const ok = await confirm({...}) // 别名,等价
12
+ * 返回 Promise<boolean>:true=点了确定,false=取消/点遮罩。
13
+ *
14
+ * 【如何做到"不用挂载就能调"?】
15
+ * 1. XPopupProvider 在应用根部常驻渲染 <XConfirmForm />(一个实例);
16
+ * 2. 这个实例挂载时通过 XConfirmFormService._setHost({show, hide})
17
+ * 把自己"注册"到模块级变量 host;
18
+ * 3. XConfirmForm.show 内部实际调用 host.show —— 无论从哪个页面调用,
19
+ * 最终都落到根部那个常驻实例上。
20
+ * (LoadingModalProvider 用的就是同款套路,RN 没有原生 Portal,
21
+ * 这就是最常用的"伪 Portal"方案。)
22
+ *
23
+ * 【Promise 如何落地?】
24
+ * resolver 保存 Promise 的 resolve 函数:
25
+ * - show() 里 new Promise(resolve => resolver.current = resolve);
26
+ * - 用户点确定 → hide(true) → resolver(true) → Promise 兑现为 true;
27
+ * - 点取消/遮罩 → hide(false) → Promise 兑现为 false。
28
+ * 注意:同一时间只允许一个确认框(后开的会覆盖前一个)。
29
+ * ============================================================================
30
+ */
31
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
32
+ import { StyleSheet, View, Text } from 'react-native';
33
+ import { XModalForm } from '../XModalForm';
34
+ import { useXTheme } from '../theme';
35
+ import { useXLocale } from '../XLocale';
36
+ /**
37
+ * XConfirmFormComponent —— 常驻的确认框实例
38
+ * 本身也是一个"受控的 XModalForm":visible 由内部状态控制。
39
+ */
40
+ export const XConfirmFormComponent = () => {
41
+ const t = useXTheme();
42
+ const { t: i18n } = useXLocale();
43
+ /** 是否显示(由 show/hide 控制,页面无需感知) */
44
+ const [visible, setVisible] = useState(false);
45
+ /** 本次显示的配置 */
46
+ const [options, setOptions] = useState({});
47
+ /** 保存当前 Promise 的 resolve,点按钮时兑现它 */
48
+ const resolver = useRef(null);
49
+ /** 显示确认框,返回 Promise:确定→true,取消/遮罩→false */
50
+ const show = useCallback((opts) => {
51
+ setOptions(opts);
52
+ setVisible(true);
53
+ return new Promise(resolve => {
54
+ resolver.current = resolve;
55
+ });
56
+ }, []);
57
+ /** 关闭并按结果兑现 Promise(默认 false) */
58
+ const hide = useCallback((result) => {
59
+ resolver.current?.(result ?? false);
60
+ resolver.current = null;
61
+ setVisible(false);
62
+ }, []);
63
+ /** 挂载时把自己注册进 Service,卸载时反注册 */
64
+ useEffect(() => {
65
+ XConfirmFormService._setHost({ show, hide });
66
+ return () => XConfirmFormService._setHost(null);
67
+ }, [show, hide]);
68
+ const styles = useMemo(() => StyleSheet.create({
69
+ content: {
70
+ fontSize: 14,
71
+ lineHeight: 22,
72
+ textAlign: 'center',
73
+ },
74
+ dangerWrap: {
75
+ marginTop: 12,
76
+ },
77
+ dangerHint: {
78
+ fontSize: 12,
79
+ textAlign: 'center',
80
+ },
81
+ }), []);
82
+ return (_jsxs(XModalForm, { visible: visible, onClose: () => hide(false), title: options.title, confirmText: options.confirmText, cancelText: options.cancelText, onSubmit: () => {
83
+ // 点确定:兑现 true 并关闭
84
+ hide(true);
85
+ return true;
86
+ }, children: [typeof options.content === 'string' ? _jsx(Text, { style: [styles.content, { color: t.colorTextSecondary }], children: options.content }) : options.content, options.danger && (_jsx(View, { style: styles.dangerWrap, children: _jsx(Text, { style: [styles.dangerHint, { color: t.colorError }], children: i18n('operationIrreversible') }) }))] }));
87
+ };
88
+ /**
89
+ * XConfirmFormService —— 模块级全局服务
90
+ * host 是常驻实例注册进来的引用;模块加载时 host 为 null,
91
+ * 此时调用 show 会直接 resolve(false)(兜底,不会报错)。
92
+ */
93
+ let host = null;
94
+ export const XConfirmFormService = {
95
+ /** 由 XConfirmFormComponent 挂载/卸载时调用 */
96
+ _setHost: (h) => {
97
+ host = h;
98
+ },
99
+ show: (options) => host?.show(options) ?? Promise.resolve(false),
100
+ hide: () => host?.hide(false),
101
+ };
102
+ /**
103
+ * 对外导出的"组件 + 静态方法":
104
+ * - <XConfirmForm />(XPopupProvider 里用的就是它)
105
+ * - XConfirmForm.show({...}) / XConfirmForm.hide()
106
+ * Object.assign 把静态方法挂到函数组件上。
107
+ */
108
+ export const XConfirmForm = Object.assign(XConfirmFormComponent, {
109
+ show: XConfirmFormService.show,
110
+ hide: XConfirmFormService.hide,
111
+ });
112
+ /** 保留 duxui 的 confirm 命名风格:confirm({...}) 等价 XConfirmForm.show({...}) */
113
+ export const confirm = XConfirmFormService.show;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ============================================================================
3
+ * XDivider —— 对标 antd Divider
4
+ * ============================================================================
5
+ *
6
+ * 支持的 antd API:
7
+ * - type: 'horizontal' | 'vertical'
8
+ * - dashed: 虚线(RN 实现方式:0 高度 + dashed 上边框,Android 上虚线密度固定,视觉近似)
9
+ * - orientation: 'left' | 'center' | 'right'(带 children 标题时分隔线的断开位置)
10
+ * - orientationMargin: 标题距屏幕边缘的距离(number,RN 无 '%' margin 字符串场景,仅收 number)
11
+ * - plain: 标题弱化样式(更浅的文字色)
12
+ * - children: 标题内容(如 <XDivider>标题</XDivider>)
13
+ *
14
+ * 与 antd 的差异:
15
+ * - orientationMargin 的 'xx%' 字符串不支持,只收 number
16
+ */
17
+ import React from 'react';
18
+ import { StyleProp, ViewStyle } from 'react-native';
19
+ export interface XDividerProps {
20
+ type?: 'horizontal' | 'vertical';
21
+ dashed?: boolean;
22
+ orientation?: 'left' | 'center' | 'right';
23
+ orientationMargin?: number;
24
+ plain?: boolean;
25
+ children?: React.ReactNode;
26
+ style?: StyleProp<ViewStyle>;
27
+ /** 分隔线颜色,默认 colorSplit */
28
+ color?: string;
29
+ /** vertical 模式的高度,默认撑满(alignSelf: 'stretch') */
30
+ height?: number;
31
+ }
32
+ export declare function XDivider({ type, dashed, orientation, orientationMargin, plain, children, style, color, height, }: XDividerProps): React.JSX.Element;
33
+ export default XDivider;
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StyleSheet, View, Text } from 'react-native';
3
+ import { useXTheme, xTheme } from '../theme';
4
+ export function XDivider({ type = 'horizontal', dashed = false, orientation = 'center', orientationMargin, plain = false, children, style, color, height, }) {
5
+ const t = useXTheme();
6
+ const lineColor = color ?? t.colorSplit;
7
+ // ---- 垂直分隔线:一条竖线,宽度 hairline,高度默认跟随父容器拉伸 ----
8
+ if (type === 'vertical') {
9
+ return (_jsx(View, { style: [
10
+ {
11
+ width: StyleSheet.hairlineWidth,
12
+ height: height,
13
+ alignSelf: height ? 'center' : 'stretch',
14
+ backgroundColor: lineColor,
15
+ marginHorizontal: 8,
16
+ },
17
+ style,
18
+ ] }));
19
+ }
20
+ // ---- 水平分隔线(无标题):一条横线 ----
21
+ if (children == null) {
22
+ return (_jsx(View, { style: [
23
+ styles.line,
24
+ { backgroundColor: dashed ? 'transparent' : lineColor },
25
+ dashed && { borderTopWidth: StyleSheet.hairlineWidth * 2, borderTopColor: lineColor, borderStyle: 'dashed' },
26
+ style,
27
+ ] }));
28
+ }
29
+ // ---- 水平带标题:线 — 标题 — 线,orientation 决定标题位置 ----
30
+ const hasLeftLine = orientation !== 'left';
31
+ const hasRightLine = orientation !== 'right';
32
+ // orientationMargin 生效规则(同 antd):left 时约束左侧留白,right 时约束右侧留白
33
+ const leftMargin = orientation === 'left' ? orientationMargin ?? 16 : undefined;
34
+ const rightMargin = orientation === 'right' ? orientationMargin ?? 16 : undefined;
35
+ const lineStyle = [
36
+ styles.line,
37
+ styles.titleLine,
38
+ { backgroundColor: dashed ? 'transparent' : lineColor },
39
+ dashed && { borderTopWidth: StyleSheet.hairlineWidth * 2, borderTopColor: lineColor, borderStyle: 'dashed' },
40
+ ];
41
+ return (_jsxs(View, { style: [styles.titleContainer, { marginLeft: leftMargin, marginRight: rightMargin }, style], children: [hasLeftLine && _jsx(View, { style: lineStyle }), _jsx(Text, { style: [styles.title, { color: plain ? t.colorTextTertiary : t.colorText }, plain && { fontSize: xTheme.fontSizeSM }], allowFontScaling: false, children: children }), hasRightLine && _jsx(View, { style: lineStyle })] }));
42
+ }
43
+ const styles = StyleSheet.create({
44
+ line: {
45
+ height: StyleSheet.hairlineWidth,
46
+ width: '100%',
47
+ },
48
+ titleContainer: {
49
+ flexDirection: 'row',
50
+ alignItems: 'center',
51
+ marginVertical: 12,
52
+ },
53
+ titleLine: {
54
+ flex: 1,
55
+ },
56
+ title: {
57
+ fontSize: xTheme.fontSize,
58
+ marginHorizontal: 12,
59
+ },
60
+ });
61
+ export default XDivider;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * ============================================================================
3
+ * XDropdownMenu —— 下拉菜单(duxui Menu 的 XTopView 版重写)
4
+ * ============================================================================
5
+ *
6
+ * 【回答"为什么用 XTopView 而不是 duxui 的方式"】
7
+ * duxui 的 Menu 用"页内 Absolute"(TopView.add 挂到页面层)实现,
8
+ * 需要每页包 TopView.page 宿主。本项目已有全局 XTopView 宿主
9
+ * (XPopupProvider 挂根),弹层直接 add 进宿主即可:
10
+ * 1. 不依赖页面容器 —— 任何组件树层级都能弹出;
11
+ * 2. 复用 XPullView 已验证的"挂载→入场→离场→移除"生命周期;
12
+ * 3. 动画走 Reanimated UI 线程,与项目弹窗族手感一致。
13
+ *
14
+ * 【结构与 duxui 对齐】
15
+ * <XDropdownMenu>
16
+ * <XDropdownMenu.Item title="全部" options={[{name,value}]} value onChange />
17
+ * <XDropdownMenu.Item title="排序" options={...} value onChange />
18
+ * </XDropdownMenu>
19
+ * 打开时:遮罩(点击关闭)+ 触发条"快照"(显示激活态标题,duxui 同思路)
20
+ * + 面板从触发条下方 translateY 下拉滑入;选中即回调 onChange 并关闭。
21
+ * Item 支持自定义 children 面板(替代 options 网格)。
22
+ *
23
+ * 【差异】v1 仅向下展开(绝大多数筛选场景);多列宽度均分(duxui 支持自定义 column)。
24
+ * ============================================================================
25
+ */
26
+ import React from 'react';
27
+ import { StyleProp, ViewStyle } from 'react-native';
28
+ export interface XMenuOption {
29
+ name: string;
30
+ value: any;
31
+ }
32
+ export interface XDropdownMenuItemProps {
33
+ /** 无 options 时的触发文案 */
34
+ title?: string;
35
+ /** 选项列表(有 options 才渲染默认面板) */
36
+ options?: XMenuOption[];
37
+ /** 受控选中值 */
38
+ value?: any;
39
+ /** 选中回调(选中后面板自动关闭) */
40
+ onChange?: (value: any) => void;
41
+ /** 禁用 */
42
+ disabled?: boolean;
43
+ /** 自定义面板内容(替代 options) */
44
+ children?: React.ReactNode;
45
+ }
46
+ export interface XDropdownMenuProps {
47
+ /** 面板底部是否圆角,默认 true */
48
+ round?: boolean;
49
+ /** 打开/关闭回调 */
50
+ onOpenChange?: (open: boolean) => void;
51
+ /** 触发条高度,默认 44 */
52
+ triggerHeight?: number;
53
+ style?: StyleProp<ViewStyle>;
54
+ children?: React.ReactNode;
55
+ }
56
+ export declare function XDropdownMenu({ round, onOpenChange, triggerHeight, style, children }: XDropdownMenuProps): React.JSX.Element;
57
+ /**
58
+ * 触发条单项(真实渲染):从 MenuContext 拿到自己的序号与开合动作。
59
+ * 面板内容由 XDropdownMenu 直接读取 element.props 渲染,本组件只负责触发条 UI。
60
+ */
61
+ export declare function XDropdownMenuItem(props: XDropdownMenuItemProps): React.JSX.Element | null;
62
+ /** 挂载到 XDropdownMenu 上便于声明式使用:<XDropdownMenu.Item .../> */
63
+ declare const MenuWithItem: typeof XDropdownMenu & {
64
+ Item: typeof XDropdownMenuItem;
65
+ };
66
+ export default MenuWithItem;
@@ -0,0 +1,258 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XDropdownMenu —— 下拉菜单(duxui Menu 的 XTopView 版重写)
5
+ * ============================================================================
6
+ *
7
+ * 【回答"为什么用 XTopView 而不是 duxui 的方式"】
8
+ * duxui 的 Menu 用"页内 Absolute"(TopView.add 挂到页面层)实现,
9
+ * 需要每页包 TopView.page 宿主。本项目已有全局 XTopView 宿主
10
+ * (XPopupProvider 挂根),弹层直接 add 进宿主即可:
11
+ * 1. 不依赖页面容器 —— 任何组件树层级都能弹出;
12
+ * 2. 复用 XPullView 已验证的"挂载→入场→离场→移除"生命周期;
13
+ * 3. 动画走 Reanimated UI 线程,与项目弹窗族手感一致。
14
+ *
15
+ * 【结构与 duxui 对齐】
16
+ * <XDropdownMenu>
17
+ * <XDropdownMenu.Item title="全部" options={[{name,value}]} value onChange />
18
+ * <XDropdownMenu.Item title="排序" options={...} value onChange />
19
+ * </XDropdownMenu>
20
+ * 打开时:遮罩(点击关闭)+ 触发条"快照"(显示激活态标题,duxui 同思路)
21
+ * + 面板从触发条下方 translateY 下拉滑入;选中即回调 onChange 并关闭。
22
+ * Item 支持自定义 children 面板(替代 options 网格)。
23
+ *
24
+ * 【差异】v1 仅向下展开(绝大多数筛选场景);多列宽度均分(duxui 支持自定义 column)。
25
+ * ============================================================================
26
+ */
27
+ import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
28
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
29
+ import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
30
+ import { useXTopViewStore } from '../XTopViewStore';
31
+ import { useXTheme } from '../theme';
32
+ const MenuContext = createContext(null);
33
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
35
+ // ---------------------------------------------------------------------------
36
+ // 触发条单项标题(真实触发与"快照"共用一套渲染)
37
+ // ---------------------------------------------------------------------------
38
+ function TriggerTitle({ itemProps, active, onPress, height, }) {
39
+ const t = useXTheme();
40
+ const color = itemProps.disabled
41
+ ? t.colorTextQuaternary
42
+ : active
43
+ ? t.colorPrimary
44
+ : t.colorText;
45
+ const selectedName = itemProps.options && itemProps.value !== undefined
46
+ ? itemProps.options.find(o => o.value === itemProps.value)?.name
47
+ : undefined;
48
+ const label = selectedName ?? itemProps.title;
49
+ return (_jsxs(Pressable, { onPress: onPress, style: [styles.triggerItem, { height }], disabled: itemProps.disabled || !onPress, children: [_jsx(Text, { style: [styles.triggerText, { color }, active && styles.triggerTextActive], numberOfLines: 1, allowFontScaling: false, children: label }), _jsx(Text, { style: [styles.arrow, { color }, active && styles.arrowOpen], allowFontScaling: false, children: "\u25BE" })] }));
50
+ }
51
+ const MenuOverlay = ({ visible, trigger, round, onClose, onExitEnd, snapshot, panel }) => {
52
+ const t = useXTheme();
53
+ /** 面板内容高度(onLayout 实测 → 决定位移初值) */
54
+ const [panelHeight, setPanelHeight] = useState(0);
55
+ const translateY = useSharedValue(0);
56
+ const maskOpacity = useSharedValue(0);
57
+ /**
58
+ * 面板位移:隐藏态 = -(panelHeight + 20)(藏在触发条上方),visible 后
59
+ * 下一帧滑到 0(duxui 的 translateY(-100%) 下拉)。panelHeight 未测得前
60
+ * 不动画,避免从 0 位置闪现。
61
+ */
62
+ useEffect(() => {
63
+ if (visible && panelHeight > 0) {
64
+ const raf = requestAnimationFrame(() => {
65
+ translateY.value = withTiming(0, { duration: 200, easing: Easing.out(Easing.ease) });
66
+ maskOpacity.value = withTiming(1, { duration: 200 });
67
+ });
68
+ return () => cancelAnimationFrame(raf);
69
+ }
70
+ }, [visible, panelHeight, translateY, maskOpacity]);
71
+ /** 离场:反向动画后通知宿主移除 */
72
+ useEffect(() => {
73
+ if (!visible) {
74
+ translateY.value = withTiming(-(panelHeight + 20), { duration: 200, easing: Easing.inOut(Easing.ease) });
75
+ maskOpacity.value = withTiming(0, { duration: 200 });
76
+ const timer = setTimeout(onExitEnd, 250);
77
+ return () => clearTimeout(timer);
78
+ }
79
+ }, [visible, panelHeight, translateY, maskOpacity, onExitEnd]);
80
+ const panelStyle = useAnimatedStyle(() => ({
81
+ transform: [{ translateY: translateY.value }],
82
+ }));
83
+ const maskStyle = useAnimatedStyle(() => ({ opacity: maskOpacity.value }));
84
+ return (_jsxs(View, { style: StyleSheet.absoluteFill, pointerEvents: 'box-none', children: [_jsx(AnimatedPressable, { style: [StyleSheet.absoluteFill, { backgroundColor: '#000' }, maskStyle], onPress: onClose }), _jsx(View, { pointerEvents: 'none', style: { position: 'absolute', left: trigger.x, top: trigger.y, width: trigger.width, height: trigger.height }, children: snapshot }), _jsx(Animated.View, { style: [
85
+ styles.panel,
86
+ {
87
+ left: trigger.x,
88
+ top: trigger.y + trigger.height,
89
+ width: trigger.width,
90
+ backgroundColor: t.colorBgContainer,
91
+ borderBottomLeftRadius: round ? t.borderRadiusXL : 0,
92
+ borderBottomRightRadius: round ? t.borderRadiusXL : 0,
93
+ },
94
+ panelStyle,
95
+ ], pointerEvents: 'auto', children: _jsx(View, { onLayout: e => {
96
+ const h = e.nativeEvent.layout.height;
97
+ setPanelHeight(prev => (prev === h ? prev : h));
98
+ }, children: panel }) })] }));
99
+ };
100
+ // ---------------------------------------------------------------------------
101
+ // 对外组件
102
+ // ---------------------------------------------------------------------------
103
+ export function XDropdownMenu({ round = true, onOpenChange, triggerHeight = 44, style, children }) {
104
+ const t = useXTheme();
105
+ const add = useXTopViewStore(s => s.add);
106
+ const update = useXTopViewStore(s => s.update);
107
+ const remove = useXTopViewStore(s => s.remove);
108
+ /** 当前打开的 Item 下标;null = 关闭 */
109
+ const [activeIndex, setActiveIndex] = useState(null);
110
+ /** 触发条窗口坐标(打开瞬间 measureInWindow 实测) */
111
+ const [trigger, setTrigger] = useState({ x: 0, y: 0, width: 0, height: triggerHeight });
112
+ /** 面板宿主 key(XPullView 同款生命周期) */
113
+ const keyRef = useRef(null);
114
+ /** 最后一次打开的下标(离场动画需要保留面板内容) */
115
+ const lastOpenIndexRef = useRef(-1);
116
+ /** 触发条容器(measureInWindow 用) */
117
+ const triggerRef = useRef(null);
118
+ /** 子 Item 列表(保持声明式 API:通过 element.props 取 options/onChange/children) */
119
+ const items = useMemo(() => React.Children.toArray(children).filter(React.isValidElement), [children]);
120
+ const close = useCallback(() => setActiveIndex(null), []);
121
+ /** 打开某一项:实测触发条窗口坐标 → set 状态 → effect 挂载面板;同项再点 = 关闭 */
122
+ const open = useCallback((index) => {
123
+ if (index === activeIndex) {
124
+ close();
125
+ return;
126
+ }
127
+ triggerRef.current?.measureInWindow((x, y, width, height) => {
128
+ setTrigger({ x, y, width, height: height || triggerHeight });
129
+ setActiveIndex(prev => (prev === index ? prev : index));
130
+ });
131
+ }, [activeIndex, close, triggerHeight]);
132
+ /** onOpenChange 通知 */
133
+ useEffect(() => {
134
+ onOpenChange?.(activeIndex !== null);
135
+ }, [activeIndex, onOpenChange]);
136
+ /**
137
+ * 面板挂载/更新/移除:
138
+ * activeIndex 变化 → add/update 覆盖层(visible=true);
139
+ * 变为 null → update 成 visible=false 播离场 → onExitEnd 移除。
140
+ */
141
+ const buildElement = useCallback((openIndex, visible) => {
142
+ const item = items[openIndex];
143
+ const itemProps = item?.props ?? {};
144
+ const handleClose = () => setActiveIndex(null);
145
+ /** 选项点击:回调 + 关闭 */
146
+ const handleOptionPress = (option) => {
147
+ itemProps.onChange?.(option.value);
148
+ handleClose();
149
+ };
150
+ return (_jsx(MenuOverlay, { visible: visible, trigger: trigger, round: round, onClose: handleClose, onExitEnd: () => {
151
+ if (keyRef.current) {
152
+ remove(keyRef.current);
153
+ keyRef.current = null;
154
+ }
155
+ }, snapshot: _jsx(View, { style: [styles.triggerRow, { height: trigger.height }], children: items.map((child, i) => (_jsx(TriggerTitle, { itemProps: child.props, active: i === openIndex, height: trigger.height }, i))) }), panel: itemProps.children != null ? (itemProps.children) : (_jsx(View, { style: styles.optionsWrap, children: (itemProps.options ?? []).map(option => {
156
+ const selected = option.value === itemProps.value;
157
+ return (_jsxs(Pressable, { onPress: () => handleOptionPress(option), style: ({ pressed }) => [
158
+ styles.optionRow,
159
+ { borderBottomColor: t.colorSplit },
160
+ pressed && { backgroundColor: t.colorBgLayout },
161
+ ], children: [_jsx(Text, { style: [
162
+ styles.optionText,
163
+ { color: selected ? t.colorPrimary : t.colorText },
164
+ selected && styles.optionTextActive,
165
+ ], allowFontScaling: false, children: option.name }), selected && (_jsx(Text, { style: { color: t.colorPrimary, fontSize: 14 }, allowFontScaling: false, children: "\u2713" }))] }, String(option.value)));
166
+ }) })) }));
167
+ }, [items, trigger, round, remove, t]);
168
+ useEffect(() => {
169
+ if (activeIndex !== null) {
170
+ const el = buildElement(activeIndex, true);
171
+ if (!keyRef.current) {
172
+ keyRef.current = add(el);
173
+ }
174
+ else {
175
+ update(keyRef.current, el);
176
+ }
177
+ lastOpenIndexRef.current = activeIndex;
178
+ }
179
+ else if (keyRef.current && lastOpenIndexRef.current >= 0) {
180
+ // 关闭:更新为离场态(保留最后内容),动画后由 onExitEnd 移除
181
+ update(keyRef.current, buildElement(lastOpenIndexRef.current, false));
182
+ }
183
+ }, [activeIndex, buildElement, add, update]);
184
+ /** 卸载兜底清理 */
185
+ useEffect(() => {
186
+ return () => {
187
+ if (keyRef.current) {
188
+ remove(keyRef.current);
189
+ keyRef.current = null;
190
+ }
191
+ };
192
+ }, [remove]);
193
+ /** context value:open/close/activeIndex 分发给每个 Item */
194
+ const ctx = useMemo(() => ({ index: -1, activeIndex: activeIndex ?? -1, open, close, triggerHeight }), [activeIndex, open, close, triggerHeight]);
195
+ return (_jsx(View, { ref: triggerRef, style: [styles.triggerRow, { height: triggerHeight }, style], children: items.map((child, i) => (_jsx(MenuContext.Provider, { value: { ...ctx, index: i }, children: child }, i))) }));
196
+ }
197
+ /**
198
+ * 触发条单项(真实渲染):从 MenuContext 拿到自己的序号与开合动作。
199
+ * 面板内容由 XDropdownMenu 直接读取 element.props 渲染,本组件只负责触发条 UI。
200
+ */
201
+ export function XDropdownMenuItem(props) {
202
+ const ctx = useContext(MenuContext);
203
+ if (!ctx)
204
+ return null;
205
+ return (_jsx(TriggerTitle, { itemProps: props, active: ctx.activeIndex === ctx.index, onPress: () => !props.disabled && ctx.open(ctx.index), height: 0 }));
206
+ }
207
+ const styles = StyleSheet.create({
208
+ triggerRow: {
209
+ flexDirection: 'row',
210
+ alignItems: 'center',
211
+ backgroundColor: 'transparent',
212
+ },
213
+ triggerItem: {
214
+ flex: 1,
215
+ flexDirection: 'row',
216
+ alignItems: 'center',
217
+ justifyContent: 'center',
218
+ paddingHorizontal: 8,
219
+ },
220
+ triggerText: {
221
+ fontSize: 14,
222
+ },
223
+ triggerTextActive: {
224
+ fontWeight: '600',
225
+ },
226
+ arrow: {
227
+ fontSize: 10,
228
+ marginLeft: 4,
229
+ },
230
+ arrowOpen: {
231
+ transform: [{ rotate: '180deg' }],
232
+ },
233
+ panel: {
234
+ position: 'absolute',
235
+ overflow: 'hidden',
236
+ },
237
+ optionsWrap: {
238
+ paddingVertical: 4,
239
+ },
240
+ optionRow: {
241
+ height: 46,
242
+ paddingHorizontal: 16,
243
+ flexDirection: 'row',
244
+ alignItems: 'center',
245
+ justifyContent: 'space-between',
246
+ borderBottomWidth: StyleSheet.hairlineWidth,
247
+ },
248
+ optionText: {
249
+ fontSize: 14,
250
+ },
251
+ optionTextActive: {
252
+ fontWeight: '600',
253
+ },
254
+ });
255
+ /** 挂载到 XDropdownMenu 上便于声明式使用:<XDropdownMenu.Item .../> */
256
+ const MenuWithItem = XDropdownMenu;
257
+ MenuWithItem.Item = XDropdownMenuItem;
258
+ export default MenuWithItem;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * ============================================================================
3
+ * XElevator —— 电梯楼层/字母导航列表(duxui Elevator 的 RN 原生重写版)
4
+ * ============================================================================
5
+ *
6
+ * 【与 duxui Elevator 的关系】
7
+ * duxui 用 ScrollView + Layout 手动累加每组高度算 scrollTop;
8
+ * RN 端 SectionList 原生支持分组吸顶 + scrollToLocation,
9
+ * 这里直接用 SectionList 重写:代码量减半、吸顶/定位更稳。
10
+ *
11
+ * 【右侧导航条】
12
+ * - 触摸(按下/滑动)定位:容器拦截 responder 事件,用 locationY 与
13
+ * 各标签 onLayout 记录的 y 区间比对,命中即 scrollToLocation;
14
+ * - 滚动联动高亮:onViewableItemsChanged 取第一个可见项所属 section。
15
+ *
16
+ * 【API 对标 duxui】list -> sections({title, data}[])、onItemClick、
17
+ * showNav、renderTop/Header/Footer/Empty;扩展 navLabel(导航条短标签,
18
+ * 默认取 title 首字符)、keyExtractor。
19
+ * ============================================================================
20
+ */
21
+ import React from 'react';
22
+ import { StyleProp, ViewStyle } from 'react-native';
23
+ export interface XElevatorSection<T = {
24
+ name: string;
25
+ }> {
26
+ /** 分组标题(吸顶显示) */
27
+ title: string;
28
+ /** 右侧导航条短标签,默认取 title 首字符 */
29
+ navLabel?: string;
30
+ /** 数据 */
31
+ data: T[];
32
+ }
33
+ export interface XElevatorProps<T> {
34
+ sections: XElevatorSection<T>[];
35
+ /** 自定义行渲染,默认显示 item.name */
36
+ renderItem?: (item: T, section: XElevatorSection<T>, index: number) => React.ReactNode;
37
+ keyExtractor?: (item: T, index: number) => string;
38
+ onItemClick?: (item: T, section: XElevatorSection<T>) => void;
39
+ /** 是否显示右侧导航条,默认 true */
40
+ showNav?: boolean;
41
+ /** 列表顶部(搜索框等) */
42
+ renderTop?: () => React.ReactNode;
43
+ /** 列表底部 */
44
+ renderFooter?: () => React.ReactNode;
45
+ /** 空态 */
46
+ renderEmpty?: () => React.ReactNode;
47
+ style?: StyleProp<ViewStyle>;
48
+ }
49
+ export declare function XElevator<T = {
50
+ name: string;
51
+ }>({ sections, renderItem, keyExtractor, onItemClick, showNav, renderTop, renderFooter, renderEmpty, style, }: XElevatorProps<T>): React.JSX.Element;
52
+ export default XElevator;