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,88 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XSignature —— 普通签名(单画布连笔,非逐字)
5
+ * ============================================================================
6
+ *
7
+ * 【与 XSignatureSkia 的分工】
8
+ * - XSignatureSkia:逐字签名(一字一格、可单字重写、结构化 value);
9
+ * - XSignature:普通连笔签名(一块白纸写到底),适合"签个名确认"场景;
10
+ * 两者共用 ./core.tsx 的采集/贝塞尔/渲染内核。
11
+ *
12
+ * 【API】
13
+ * <XSignature height={200} onExport={base64 => upload(base64)} />
14
+ * 内置 撤销/清空/完成 三个动作按钮(XButton + theme + i18n)。
15
+ * ============================================================================
16
+ */
17
+ import { useCallback, useState } from 'react';
18
+ import { ImageFormat, useCanvasRef } from '@shopify/react-native-skia';
19
+ import { StyleSheet, Text, View } from 'react-native';
20
+ import Svg, { Line } from 'react-native-svg';
21
+ import { XButton } from '../XButton';
22
+ import { useXTheme } from '../theme';
23
+ import { useXLocale } from '../XLocale';
24
+ import { SignatureCanvas } from './core';
25
+ export function XSignature({ height = 200, strokeColor = '#111111', strokeWidth = 4, onExport, onStrokeEnd, disabled = false, style, }) {
26
+ const t = useXTheme();
27
+ const { t: i18n } = useXLocale();
28
+ const canvasRef = useCanvasRef();
29
+ const [size, setSize] = useState({ width: 0, height: 0 });
30
+ /** 已提交笔画 */
31
+ const [strokes, setStrokes] = useState([]);
32
+ /** 一笔画完 */
33
+ const handleStrokeComplete = useCallback((points, strokeSize) => {
34
+ setStrokes(prev => {
35
+ const next = [
36
+ ...prev,
37
+ {
38
+ id: `${Date.now()}-${prev.length}`,
39
+ color: strokeColor,
40
+ width: strokeWidth,
41
+ points,
42
+ size: strokeSize,
43
+ },
44
+ ];
45
+ onStrokeEnd?.(next.length);
46
+ return next;
47
+ });
48
+ }, [strokeColor, strokeWidth, onStrokeEnd]);
49
+ /** 撤销最后一笔 */
50
+ const handleUndo = useCallback(() => setStrokes(prev => prev.slice(0, -1)), []);
51
+ /** 清空 */
52
+ const handleClear = useCallback(() => setStrokes([]), []);
53
+ /** 完成:截图导出(无笔画时不上抛) */
54
+ const handleDone = useCallback(async () => {
55
+ if (!strokes.length)
56
+ return;
57
+ await new Promise(resolve => setTimeout(resolve, 50));
58
+ const image = canvasRef.current?.makeImageSnapshot();
59
+ if (image)
60
+ onExport?.(image.encodeToBase64(ImageFormat.PNG));
61
+ }, [strokes.length, canvasRef, onExport]);
62
+ return (_jsxs(View, { style: [styles.container, style], children: [_jsxs(View, { style: [styles.paper, { height, borderColor: t.colorBorder, borderRadius: t.borderRadiusXL }], children: [_jsx(SignatureCanvas, { strokes: strokes, touchEnabled: !disabled, strokeColor: strokeColor, strokeWidth: strokeWidth, onStrokeComplete: handleStrokeComplete, onLayoutSize: setSize, canvasRef: canvasRef }), size.width > 0 && (_jsxs(Svg, { pointerEvents: 'none', width: size.width, height: size.height, style: styles.grid, children: [_jsx(Line, { x1: '0%', y1: '50%', x2: '100%', y2: '50%', stroke: t.colorSplit, strokeWidth: '1', strokeDasharray: [4, 4] }), _jsx(Line, { x1: '50%', y1: '0%', x2: '50%', y2: '100%', stroke: t.colorSplit, strokeWidth: '1', strokeDasharray: [4, 4] })] })), strokes.length === 0 && (_jsx(Text, { pointerEvents: 'none', style: [styles.placeholder, { color: t.colorTextTertiary }], children: i18n('signHint') }))] }), _jsxs(View, { style: styles.actions, children: [_jsx(XButton, { size: 'small', onPress: handleUndo, disabled: disabled || !strokes.length, children: i18n('undo') }), _jsx(XButton, { size: 'small', danger: true, type: 'text', onPress: handleClear, disabled: disabled || !strokes.length, children: i18n('clear') }), _jsx(XButton, { size: 'small', type: 'primary', onPress: handleDone, disabled: disabled || !strokes.length, children: i18n('done') })] })] }));
63
+ }
64
+ const styles = StyleSheet.create({
65
+ container: {
66
+ gap: 8,
67
+ },
68
+ paper: {
69
+ overflow: 'hidden',
70
+ borderWidth: StyleSheet.hairlineWidth,
71
+ },
72
+ grid: {
73
+ position: 'absolute',
74
+ top: 0,
75
+ left: 0,
76
+ },
77
+ placeholder: {
78
+ position: 'absolute',
79
+ alignSelf: 'center',
80
+ fontSize: 13,
81
+ },
82
+ actions: {
83
+ flexDirection: 'row',
84
+ justifyContent: 'flex-end',
85
+ gap: 8,
86
+ },
87
+ });
88
+ export default XSignature;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * ============================================================================
3
+ * XTabs —— 选项卡(duxui Tab 的 RN 原生重写版)
4
+ * ============================================================================
5
+ *
6
+ * 【与 duxui Tab 的关系】
7
+ * 保留 duxui 的核心 API:type('line'|'button') / swiper(内容滑动联动)/
8
+ * lazyload / scroll / justify / TabPane 子组件声明式用法。
9
+ * 差异:duxui 的下划线与内容联动跑在 Taro Animated(CSS)上,
10
+ * 这里用 Reanimated shared value(UI 线程)+ 原生 ScrollView。
11
+ *
12
+ * 【用法】
13
+ * <XTabs onChange={...}>
14
+ * <XTabPane title="标签一" key="a">内容一</XTabPane>
15
+ * <XTabPane title="标签二" key="b" badge={3}>内容二</XTabPane>
16
+ * </XTabs>
17
+ *
18
+ * 【实现要点】
19
+ * 1. tab 头:项数 > 4 默认横向滚动(scroll 可显式覆盖),每项 onLayout
20
+ * 记录 x/width,下划线用 withTiming 平移 + 变宽(线型);
21
+ * 2. 内容区:swiper 模式 = ScrollView pagingEnabled,滑动结束反向同步
22
+ * 激活项;非 swiper = 只挂载当前激活页(天然懒加载);
23
+ * 3. lazyload 仅对 swiper 模式有意义:激活过才渲染内容,渲染后保持挂载
24
+ * (保留滚动位置),未访问的页渲染占位。
25
+ * ============================================================================
26
+ */
27
+ import React from 'react';
28
+ import { StyleProp, ViewStyle } from 'react-native';
29
+ /** 单个选项卡 */
30
+ export interface XTabPaneProps {
31
+ /** 标签文字(或任意节点) */
32
+ title?: React.ReactNode;
33
+ /** 徽标:数字/文字/true(红点) */
34
+ badge?: string | number | boolean;
35
+ /** 是否禁用 */
36
+ disabled?: boolean;
37
+ children?: React.ReactNode;
38
+ }
39
+ /** XTabPane 是声明式占位组件:内容由 XTabs 通过 element.props 提取,自身不渲染 */
40
+ export declare function XTabPane(_: XTabPaneProps): null;
41
+ export interface XTabsProps {
42
+ /** 受控激活 key */
43
+ value?: string;
44
+ /** 非受控初始 key(默认第一个 TabPane 的 key) */
45
+ defaultValue?: string;
46
+ /** 激活变化回调 */
47
+ onChange?: (key: string, index: number) => void;
48
+ /** line=下划线 / button=分段按钮,默认 line */
49
+ type?: 'line' | 'button';
50
+ /** 内容区是否可滑动(ScrollView 分页联动),默认 false */
51
+ swiper?: boolean;
52
+ /** tab 头是否可横向滚动,默认:项数 > 4 时可滚动 */
53
+ scroll?: boolean;
54
+ /** swiper 模式下:激活过才渲染内容(保留已渲染),默认 true */
55
+ lazyload?: boolean;
56
+ /** tab 头均分宽度(不可滚动时有效),默认 false */
57
+ justify?: boolean;
58
+ /** 激活项右下角文字色跟随主色之外的自定义色(少量场景用) */
59
+ activeColor?: string;
60
+ style?: StyleProp<ViewStyle>;
61
+ tabBarStyle?: StyleProp<ViewStyle>;
62
+ children?: React.ReactNode;
63
+ }
64
+ export declare function XTabs({ value, defaultValue, onChange, type, swiper, scroll, lazyload, justify, activeColor, style, tabBarStyle, children, }: XTabsProps): React.JSX.Element;
65
+ export default XTabs;
@@ -0,0 +1,210 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XTabs —— 选项卡(duxui Tab 的 RN 原生重写版)
5
+ * ============================================================================
6
+ *
7
+ * 【与 duxui Tab 的关系】
8
+ * 保留 duxui 的核心 API:type('line'|'button') / swiper(内容滑动联动)/
9
+ * lazyload / scroll / justify / TabPane 子组件声明式用法。
10
+ * 差异:duxui 的下划线与内容联动跑在 Taro Animated(CSS)上,
11
+ * 这里用 Reanimated shared value(UI 线程)+ 原生 ScrollView。
12
+ *
13
+ * 【用法】
14
+ * <XTabs onChange={...}>
15
+ * <XTabPane title="标签一" key="a">内容一</XTabPane>
16
+ * <XTabPane title="标签二" key="b" badge={3}>内容二</XTabPane>
17
+ * </XTabs>
18
+ *
19
+ * 【实现要点】
20
+ * 1. tab 头:项数 > 4 默认横向滚动(scroll 可显式覆盖),每项 onLayout
21
+ * 记录 x/width,下划线用 withTiming 平移 + 变宽(线型);
22
+ * 2. 内容区:swiper 模式 = ScrollView pagingEnabled,滑动结束反向同步
23
+ * 激活项;非 swiper = 只挂载当前激活页(天然懒加载);
24
+ * 3. lazyload 仅对 swiper 模式有意义:激活过才渲染内容,渲染后保持挂载
25
+ * (保留滚动位置),未访问的页渲染占位。
26
+ * ============================================================================
27
+ */
28
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
29
+ import { Pressable, ScrollView, StyleSheet, Text, View, } from 'react-native';
30
+ import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
31
+ import { useXTheme } from '../theme';
32
+ /** XTabPane 是声明式占位组件:内容由 XTabs 通过 element.props 提取,自身不渲染 */
33
+ export function XTabPane(_) {
34
+ return null;
35
+ }
36
+ export function XTabs({ value, defaultValue, onChange, type = 'line', swiper = false, scroll, lazyload = true, justify = false, activeColor, style, tabBarStyle, children, }) {
37
+ const t = useXTheme();
38
+ /** 解析 TabPane 子元素 → 数据化 panes(key 取 React key) */
39
+ const panes = useMemo(() => React.Children.toArray(children)
40
+ .filter(React.isValidElement)
41
+ .map(el => {
42
+ const props = el.props;
43
+ return {
44
+ key: String(el.key),
45
+ title: props.title,
46
+ badge: props.badge,
47
+ disabled: props.disabled,
48
+ content: props.children,
49
+ };
50
+ }), [children]);
51
+ const isControlled = value !== undefined;
52
+ const [inner, setInner] = useState(() => defaultValue ?? panes[0]?.key);
53
+ const activeKey = isControlled ? value : inner;
54
+ const activeIndex = Math.max(0, panes.findIndex(p => p.key === activeKey));
55
+ const setActive = useCallback((key, index) => {
56
+ if (!isControlled)
57
+ setInner(key);
58
+ onChange?.(key, index);
59
+ }, [isControlled, onChange]);
60
+ /** tab 头是否滚动:项数 > 4 默认开 */
61
+ const scrollable = scroll ?? panes.length > 4;
62
+ // ---------------------------------------------------------------------------
63
+ // 下划线(line 类型):激活项 onLayout 测量 → shared value 平移/变宽
64
+ // ---------------------------------------------------------------------------
65
+ const tabLayoutsRef = useRef({});
66
+ const lineX = useSharedValue(0);
67
+ const lineW = useSharedValue(0);
68
+ useEffect(() => {
69
+ const layout = tabLayoutsRef.current[activeIndex];
70
+ if (!layout)
71
+ return;
72
+ const duration = t.motionDurationMid;
73
+ lineX.value = withTiming(layout.x, { duration });
74
+ lineW.value = withTiming(layout.width, { duration });
75
+ }, [activeIndex, panes.length, lineX, lineW, t.motionDurationMid]);
76
+ const lineAnimatedStyle = useAnimatedStyle(() => ({
77
+ transform: [{ translateX: lineX.value }],
78
+ width: lineW.value,
79
+ }));
80
+ // ---------------------------------------------------------------------------
81
+ // swiper 内容联动:外部激活 → scrollTo;滑动结束 → 反向激活
82
+ // ---------------------------------------------------------------------------
83
+ const contentRef = useRef(null);
84
+ const [contentWidth, setContentWidth] = useState(0);
85
+ /** 记录"由滑动触发的激活",避免 scrollTo 循环 */
86
+ const swipingRef = useRef(false);
87
+ useEffect(() => {
88
+ if (!swiper || contentWidth <= 0)
89
+ return;
90
+ contentRef.current?.scrollTo({ x: activeIndex * contentWidth, animated: false });
91
+ // activeIndex 变化统一同步(滑动手势结束 setState 后也会走到这里,
92
+ // 位置已在松手处,scrollTo 同值无副作用)
93
+ }, [activeIndex, swiper, contentWidth]);
94
+ const handleContentMomentumEnd = useCallback((e) => {
95
+ const index = Math.round(e.nativeEvent.contentOffset.x / contentWidth);
96
+ const pane = panes[Math.min(Math.max(index, 0), panes.length - 1)];
97
+ if (pane && pane.key !== activeKey) {
98
+ swipingRef.current = true;
99
+ setActive(pane.key, panes.indexOf(pane));
100
+ }
101
+ }, [contentWidth, panes, activeKey, setActive]);
102
+ /** swiper 模式已访问页(lazyload) */
103
+ const [visited, setVisited] = useState(() => new Set([activeIndex]));
104
+ useEffect(() => {
105
+ setVisited(prev => (prev.has(activeIndex) ? prev : new Set(prev).add(activeIndex)));
106
+ }, [activeIndex]);
107
+ /** 渲染单个 tab 头 */
108
+ const renderTab = (pane, index) => {
109
+ const active = index === activeIndex;
110
+ const disabled = pane.disabled;
111
+ const press = () => !disabled && setActive(pane.key, index);
112
+ if (type === 'button') {
113
+ // 分段按钮:灰轨道 + 激活白底胶囊(暗黑下轨道黑、胶囊灰,由 token 自适应)
114
+ return (_jsx(Pressable, { onPress: press, onLayout: e => {
115
+ tabLayoutsRef.current[index] = { x: e.nativeEvent.layout.x, width: e.nativeEvent.layout.width };
116
+ }, style: [styles.buttonTab, justify && !scrollable && styles.flex1], children: _jsx(View, { style: [styles.buttonPill, active && { backgroundColor: t.colorBgContainer }], children: _jsx(TabTitle, { pane: pane, active: active, color: active ? t.colorPrimary : t.colorTextSecondary }) }) }, pane.key));
117
+ }
118
+ // line 类型
119
+ return (_jsx(Pressable, { onPress: press, onLayout: e => {
120
+ tabLayoutsRef.current[index] = { x: e.nativeEvent.layout.x, width: e.nativeEvent.layout.width };
121
+ }, style: [styles.lineTab, justify && !scrollable && styles.flex1, disabled && styles.disabled], children: _jsx(TabTitle, { pane: pane, active: active, color: disabled ? t.colorTextQuaternary : active ? activeColor ?? t.colorPrimary : t.colorText }) }, pane.key));
122
+ };
123
+ const tabBar = (_jsxs(View, { style: [
124
+ styles.tabBar,
125
+ type === 'button' && { backgroundColor: t.colorBgLayout, borderRadius: t.borderRadius, padding: 2 },
126
+ !scrollable && { flexDirection: 'row' },
127
+ tabBarStyle,
128
+ ], children: [scrollable ? (_jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, contentContainerStyle: styles.scrollContent, children: panes.map(renderTab) })) : (panes.map(renderTab)), type === 'line' && (_jsx(Animated.View, { style: [styles.lineIndicator, { backgroundColor: activeColor ?? t.colorPrimary }, lineAnimatedStyle], pointerEvents: 'none' }))] }));
129
+ return (_jsxs(View, { style: style, children: [tabBar, swiper ? (_jsx(View, { onLayout: e => setContentWidth(e.nativeEvent.layout.width), style: styles.content, children: contentWidth > 0 && (_jsx(ScrollView, { ref: contentRef, horizontal: true, pagingEnabled: true, showsHorizontalScrollIndicator: false, scrollEventThrottle: 16, onMomentumScrollEnd: handleContentMomentumEnd, children: panes.map((pane, index) => (_jsx(View, { style: { width: contentWidth }, children: lazyload && !visited.has(index) ? (_jsx(View, { style: styles.lazyPlaceholder })) : (pane.content) }, pane.key))) })) })) : (_jsx(View, { style: styles.content, children: panes[activeIndex]?.content }))] }));
130
+ }
131
+ /** tab 标题 + 徽标 */
132
+ function TabTitle({ pane, active, color }) {
133
+ const t = useXTheme();
134
+ return (_jsxs(View, { style: styles.titleRow, children: [typeof pane.title === 'string' ? (_jsx(Text, { style: [styles.titleText, { color }, active && styles.titleActive], numberOfLines: 1, children: pane.title })) : (pane.title), pane.badge != null && pane.badge !== false && (_jsx(View, { style: [
135
+ styles.badge,
136
+ pane.badge === true
137
+ ? { width: 6, height: 6, borderRadius: 3, backgroundColor: t.colorError }
138
+ : { backgroundColor: t.colorError },
139
+ ], children: typeof pane.badge === 'string' || typeof pane.badge === 'number' ? (_jsx(Text, { style: styles.badgeText, allowFontScaling: false, children: pane.badge })) : null }))] }));
140
+ }
141
+ const styles = StyleSheet.create({
142
+ tabBar: {
143
+ position: 'relative',
144
+ },
145
+ scrollContent: {
146
+ paddingRight: 8,
147
+ },
148
+ lineTab: {
149
+ paddingHorizontal: 16,
150
+ height: 44,
151
+ justifyContent: 'center',
152
+ alignItems: 'center',
153
+ },
154
+ buttonTab: {
155
+ paddingHorizontal: 4,
156
+ paddingVertical: 2,
157
+ },
158
+ flex1: {
159
+ flex: 1,
160
+ },
161
+ titleRow: {
162
+ flexDirection: 'row',
163
+ alignItems: 'center',
164
+ },
165
+ titleText: {
166
+ fontSize: 15,
167
+ },
168
+ titleActive: {
169
+ fontWeight: '600',
170
+ },
171
+ badge: {
172
+ marginLeft: 4,
173
+ minWidth: 16,
174
+ height: 16,
175
+ borderRadius: 8,
176
+ paddingHorizontal: 4,
177
+ alignItems: 'center',
178
+ justifyContent: 'center',
179
+ overflow: 'hidden',
180
+ },
181
+ badgeText: {
182
+ color: '#fff',
183
+ fontSize: 10,
184
+ lineHeight: 12,
185
+ },
186
+ lineIndicator: {
187
+ position: 'absolute',
188
+ bottom: 0,
189
+ left: 0,
190
+ height: 2,
191
+ borderRadius: 1,
192
+ },
193
+ buttonPill: {
194
+ height: 32,
195
+ borderRadius: 6,
196
+ paddingHorizontal: 14,
197
+ alignItems: 'center',
198
+ justifyContent: 'center',
199
+ },
200
+ content: {
201
+ overflow: 'hidden',
202
+ },
203
+ lazyPlaceholder: {
204
+ height: 100,
205
+ },
206
+ disabled: {
207
+ opacity: 0.5,
208
+ },
209
+ });
210
+ export default XTabs;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * ============================================================================
3
+ * XTag —— 对标 antd Tag(RN 自实现版),由老 components/Tag 复刻并增强
4
+ * ============================================================================
5
+ *
6
+ * 支持的 antd Tag API:
7
+ * - color: 预设色名(success/processing/error/warning/default + magenta/red/... 12 种)
8
+ * 或自定义色值('#f50' / '#0f0'),自动派生文字/背景/描边
9
+ * - variant: light(antd 默认浅底彩字)| solid(实底白字,老 Tag 视觉)| outline(描边)
10
+ * - closable + onClose: 可关闭小叉号(带 hitSlop,方便点按)
11
+ * - icon: 前置图标节点
12
+ * - bordered: 描边开关(light/outline 下生效,默认 true)
13
+ * - onPress: 整体可点(按压有透明度反馈)
14
+ * - children: 自定义内容(文本外的节点也行)
15
+ *
16
+ * 兼容老 Tag API(零成本迁移):
17
+ * - text / size(small|medium|large) / type(info|primary|success|warning|error|default)
18
+ * - 注意:老 Tag 是实底白字,迁移时加 variant='solid' 可保持视觉不变;
19
+ * XTag 默认 light 是为了对齐 antd 默认观感
20
+ *
21
+ * 与 antd 的差异(RN 环境限制或有意取舍):
22
+ * - antd 无 size,RN 下保留老 Tag 的三档尺寸(字号 12/14/16)
23
+ * - 不支持 'xxx-inverse' 色名写法,反色诉求直接用 variant='solid'
24
+ * - 无 hover 态,按压反馈统一用透明度变化
25
+ */
26
+ import React from 'react';
27
+ import { TextStyle, ViewStyle, StyleProp } from 'react-native';
28
+ export type XTagType = 'info' | 'primary' | 'success' | 'warning' | 'error' | 'default';
29
+ export type XTagSize = 'small' | 'medium' | 'large';
30
+ export type XTagVariant = 'light' | 'solid' | 'outline';
31
+ export interface XTagProps {
32
+ /** 标签文本(兼容老 Tag;与 children 二选一,children 优先级更高) */
33
+ text?: string;
34
+ /** 语义类型(兼容老 Tag),会被 color 覆盖 */
35
+ type?: XTagType;
36
+ /** 尺寸(老 Tag 三档,antd 没有) */
37
+ size?: XTagSize;
38
+ /** 预设色名或自定义色值,优先级高于 type */
39
+ color?: string;
40
+ /** 视觉变体:light=浅底彩字(默认,antd 观感)/ solid=实底白字 / outline=描边 */
41
+ variant?: XTagVariant;
42
+ /** 是否带描边(light/outline 下生效),默认 true */
43
+ bordered?: boolean;
44
+ /** 胶囊圆角(shape='round' 时圆角取高度的一半) */
45
+ shape?: 'default' | 'round';
46
+ /** 前置图标节点 */
47
+ icon?: React.ReactNode;
48
+ /** 是否显示关闭叉号 */
49
+ closable?: boolean;
50
+ /** 关闭回调(e.preventDefault 风格不适用 RN,返回 false 不阻止隐藏,组件本身受控不隐藏) */
51
+ onClose?: (e: any) => void;
52
+ /** 整体点击 */
53
+ onPress?: () => void;
54
+ /** 自定义内容(优先级高于 text) */
55
+ children?: React.ReactNode;
56
+ style?: StyleProp<ViewStyle>;
57
+ textStyle?: StyleProp<TextStyle>;
58
+ testID?: string;
59
+ }
60
+ export declare function XTag({ text, type, size, color, variant, bordered, shape, icon, closable, onClose, onPress, children, style, textStyle, testID, }: XTagProps): React.JSX.Element | null;
61
+ export default XTag;
@@ -0,0 +1,153 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
3
+ import AntDesign from '@react-native-vector-icons/ant-design';
4
+ import { useXTheme, xTheme } from '../theme';
5
+ /** antd Tag 十二预设色(文档色板,magenta ~ purple) */
6
+ const PRESET_COLORS = {
7
+ magenta: '#EB2F96',
8
+ red: '#F5222D',
9
+ volcano: '#FA541C',
10
+ orange: '#FA8C16',
11
+ gold: '#FAAD14',
12
+ lime: '#A0D911',
13
+ green: '#52C41A',
14
+ cyan: '#13C2C2',
15
+ blue: '#1677FF',
16
+ geekblue: '#2F54EB',
17
+ purple: '#722ED1',
18
+ };
19
+ /** 语义色(状态色名 + type 统一走这张表,值取当前主题 token) */
20
+ function getSemanticColors(t) {
21
+ return {
22
+ primary: { solid: t.colorPrimary, lightBg: t.colorPrimaryBg },
23
+ info: { solid: t.colorPrimary, lightBg: t.colorPrimaryBg },
24
+ success: { solid: t.colorSuccess, lightBg: t.colorSuccessBg },
25
+ warning: { solid: t.colorWarning, lightBg: t.colorWarningBg },
26
+ error: { solid: t.colorError, lightBg: t.colorErrorBg },
27
+ processing: { solid: t.colorPrimary, lightBg: t.colorPrimaryBg },
28
+ };
29
+ }
30
+ /** '#f50' / '#0f0' / '#rrggbbaa' -> rgba(alpha),用于浅底/描边派生色 */
31
+ function withAlpha(color, alpha) {
32
+ if (!color.startsWith('#')) {
33
+ return color;
34
+ }
35
+ let hex = color.slice(1);
36
+ if (hex.length === 3) {
37
+ hex = hex
38
+ .split('')
39
+ .map(c => c + c)
40
+ .join('');
41
+ }
42
+ if (hex.length === 8) {
43
+ hex = hex.slice(0, 6);
44
+ }
45
+ const r = parseInt(hex.slice(0, 2), 16);
46
+ const g = parseInt(hex.slice(2, 4), 16);
47
+ const b = parseInt(hex.slice(4, 6), 16);
48
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
49
+ }
50
+ /** color / type 解析成基准色(solid 色 + 浅底色),default 为灰色系 */
51
+ function resolvePalette(t, color, type = 'default') {
52
+ const SEMANTIC_COLORS = getSemanticColors(t);
53
+ // 1. 状态色名(antd 文档的 success/processing/...)
54
+ const semantic = color ? SEMANTIC_COLORS[color] : undefined;
55
+ if (semantic) {
56
+ return { solid: semantic.solid, lightBg: semantic.lightBg, isDefault: false };
57
+ }
58
+ // 2. 预设色名(magenta / purple / ...)
59
+ const preset = color ? PRESET_COLORS[color] : undefined;
60
+ if (preset) {
61
+ return { solid: preset, lightBg: withAlpha(preset, 0.12), isDefault: false };
62
+ }
63
+ // 3. 自定义色值(# 开头)
64
+ if (color && color.startsWith('#')) {
65
+ return { solid: color, lightBg: withAlpha(color, 0.12), isDefault: false };
66
+ }
67
+ // 4. type 语义(兼容老 Tag;default 走灰系)
68
+ const byType = SEMANTIC_COLORS[type];
69
+ if (byType) {
70
+ return { solid: byType.solid, lightBg: byType.lightBg, isDefault: false };
71
+ }
72
+ return { solid: t.colorTextSecondary, lightBg: t.colorBgContainerDisabled, isDefault: true };
73
+ }
74
+ const SIZE_STYLES = {
75
+ small: { fontSize: xTheme.fontSizeSM, paddingH: 6, paddingV: 1, radius: xTheme.borderRadiusSM, iconSize: 10 },
76
+ medium: { fontSize: xTheme.fontSize, paddingH: 8, paddingV: 2, radius: xTheme.borderRadiusSM, iconSize: 12 },
77
+ large: { fontSize: xTheme.fontSizeLG, paddingH: 10, paddingV: 4, radius: xTheme.borderRadius, iconSize: 14 },
78
+ };
79
+ export function XTag({ text, type = 'default', size = 'medium', color, variant = 'light', bordered = true, shape = 'default', icon, closable = false, onClose, onPress, children, style, textStyle, testID, }) {
80
+ const t = useXTheme();
81
+ const sizeStyle = SIZE_STYLES[size];
82
+ const palette = resolvePalette(t, color, type);
83
+ const content = children ?? text;
84
+ // 老组件行为:无文本不渲染(这里放宽为 icon/children 也算内容)
85
+ if (content === undefined || content === null || content === '') {
86
+ if (!icon) {
87
+ return null;
88
+ }
89
+ }
90
+ // --- 按 variant 派生背景 / 文字 / 描边 ---
91
+ let backgroundColor = 'transparent';
92
+ let textColor = palette.solid;
93
+ let borderColor = 'transparent';
94
+ let borderWidth = 0;
95
+ if (variant === 'solid') {
96
+ backgroundColor = palette.solid;
97
+ textColor = palette.isDefault ? t.colorText : t.colorTextLightSolid;
98
+ }
99
+ else if (variant === 'outline') {
100
+ borderColor = bordered ? palette.solid : 'transparent';
101
+ borderWidth = bordered ? 1 : 0;
102
+ }
103
+ else {
104
+ // light(默认)
105
+ backgroundColor = palette.lightBg;
106
+ borderColor = bordered ? withAlpha(palette.solid, 0.3) : 'transparent';
107
+ borderWidth = bordered ? 1 : 0;
108
+ }
109
+ const borderRadius = shape === 'round'
110
+ ? sizeStyle.fontSize / 2 + sizeStyle.paddingV + (variant === 'solid' ? 0 : borderWidth) // 近似胶囊
111
+ : sizeStyle.radius;
112
+ const containerStyle = [
113
+ styles.base,
114
+ {
115
+ backgroundColor,
116
+ borderColor,
117
+ borderWidth,
118
+ borderRadius,
119
+ paddingHorizontal: sizeStyle.paddingH,
120
+ paddingVertical: sizeStyle.paddingV,
121
+ },
122
+ style,
123
+ ];
124
+ const label = (_jsxs(View, { style: styles.content, children: [icon, _jsx(Text, { style: [{ fontSize: sizeStyle.fontSize, color: textColor, fontWeight: '500' }, textStyle], children: content })] }));
125
+ const body = closable ? (_jsxs(View, { style: styles.content, children: [label, _jsx(Pressable, { hitSlop: { top: 6, right: 6, bottom: 6, left: 6 }, onPress: e => onClose?.(e), style: ({ pressed }) => [styles.close, pressed && styles.closePressed], children: _jsx(AntDesign, { name: 'close', size: sizeStyle.iconSize, color: textColor }) })] })) : (label);
126
+ if (onPress) {
127
+ return (_jsx(Pressable, { testID: testID, onPress: onPress, style: ({ pressed }) => [containerStyle, pressed && styles.pressed], children: body }));
128
+ }
129
+ return (_jsx(View, { testID: testID, style: containerStyle, children: body }));
130
+ }
131
+ const styles = StyleSheet.create({
132
+ base: {
133
+ alignSelf: 'flex-start',
134
+ justifyContent: 'center',
135
+ alignItems: 'center',
136
+ },
137
+ content: {
138
+ flexDirection: 'row',
139
+ alignItems: 'center',
140
+ gap: 4,
141
+ },
142
+ close: {
143
+ justifyContent: 'center',
144
+ alignItems: 'center',
145
+ },
146
+ closePressed: {
147
+ opacity: 0.5,
148
+ },
149
+ pressed: {
150
+ opacity: 0.75,
151
+ },
152
+ });
153
+ export default XTag;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * ============================================================================
3
+ * XTopView —— 全局弹窗宿主渲染器
4
+ * ============================================================================
5
+ *
6
+ * 【它是谁?】
7
+ * 由 <XPopupProvider /> 挂在应用根部(App.tsx 里),读取 XTopViewStore 中的
8
+ * items,把每个弹窗渲染成一层"全屏透明的 View"。
9
+ *
10
+ * 【层级结构】
11
+ * XPopupProvider
12
+ * ├── children(整个 App:NavigationContainer 等)
13
+ * ├── <XConfirmForm /> (命令式确认框的常驻实例,它内部也是走 XPullView→XTopView)
14
+ * └── <XTopView /> ← 所有弹窗最终都画到这里,天然在所有页面之上
15
+ *
16
+ * 【关键细节】
17
+ * 1. StyleSheet.absoluteFill:撑满整个屏幕,配合 box-none 不拦截触摸,
18
+ * 触摸由弹层内部自己的遮罩/面板处理;
19
+ * 2. 每个弹窗包一层 View 是为了隔离——后挂载的弹窗盖住先挂载的;
20
+ * 3. key 用 item.key 而不是 index,保证 update 时 React 能正确复用
21
+ * 同一个组件实例(否则动画状态会被重置)。
22
+ * ============================================================================
23
+ */
24
+ import React from 'react';
25
+ export declare const XTopView: () => React.JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { StyleSheet, View } from 'react-native';
3
+ import { useXTopViewStore } from '../XTopViewStore';
4
+ export const XTopView = () => {
5
+ // 订阅 items:store 里 add/update/remove 都会触发这里重渲染
6
+ const items = useXTopViewStore(state => state.items);
7
+ return (_jsx(_Fragment, { children: items.map(item => (_jsx(View, { style: StyleSheet.absoluteFill, pointerEvents: 'box-none', children: item.element }, item.key))) }));
8
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * ============================================================================
3
+ * XTopViewStore —— 全局弹窗宿主的状态仓库(zustand)
4
+ * ============================================================================
5
+ *
6
+ * 【为什么需要它?】
7
+ * 弹窗如果渲染在页面内部(比如 ScrollView 里),会遇到两个问题:
8
+ * 1. 遮罩的 absoluteFill 只相对最近的定位父级生效,页面后面的兄弟节点
9
+ * (比如 Button)会画在遮罩上面 → 遮不住;
10
+ * 2. 弹窗会跟着页面滚动,也无法覆盖到导航头/TabBar 等页面之外的区域。
11
+ *
12
+ * 解决方案(也是 duxui XTopView 的原理):把弹窗"托管"到一个全局 store,
13
+ * 再由挂在应用根部的 <XTopView /> 渲染出来。这样弹窗永远在最顶层,
14
+ * 遮罩天然覆盖整个屏幕,与它在哪个页面里调用无关。
15
+ *
16
+ * 【三个 API 的含义】
17
+ * add(element) -> 挂载一个弹窗,返回唯一 key(之后用 key 来操作它)
18
+ * update(key, el) -> 替换指定 key 的弹窗元素(用于"关闭时切换成离场态")
19
+ * remove(key) -> 从宿主中移除(离场动画播完后再调用)
20
+ *
21
+ * 【zustand 用法速记】
22
+ * create<T>()(set => ({...})) 返回一个 hook:useXTopViewStore(s => s.xxx)
23
+ * - 选择器 s => s.add 只在 add 引用变化时才触发组件重渲染;
24
+ * - 组件内也能用 useXTopViewStore.getState().add(...) 直接调(不用 hook)。
25
+ * ============================================================================
26
+ */
27
+ import React from 'react';
28
+ /** 宿主中的一个弹窗条目:key 唯一标识,element 是要渲染的内容 */
29
+ export interface XTopViewItem {
30
+ key: number;
31
+ element: React.ReactNode;
32
+ }
33
+ /** store 的完整状态 + 操作方法定义 */
34
+ interface XTopViewState {
35
+ /** 当前所有挂载中的弹窗(按挂载顺序排列,后面的绘制在上层) */
36
+ items: XTopViewItem[];
37
+ /** 挂载一个弹窗,返回唯一 key */
38
+ add: (element: React.ReactNode) => number;
39
+ /** 替换指定 key 的弹窗内容(不改变层级顺序) */
40
+ update: (key: number, element: React.ReactNode) => void;
41
+ /** 移除指定 key 的弹窗 */
42
+ remove: (key: number) => void;
43
+ }
44
+ export declare const useXTopViewStore: import("zustand").UseBoundStore<import("zustand").StoreApi<XTopViewState>>;
45
+ export {};