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,136 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XInput —— 项目通用输入框(样式取自 componentsTest 的 styles.input)
5
+ * ============================================================================
6
+ *
7
+ * 把项目里反复手写的输入框样式沉淀成组件:
8
+ * 发丝边框 + 8 圆角 + 浅灰底,多行模式自动变成"备注"样式(minHeight + 顶部对齐)。
9
+ *
10
+ * 用法:
11
+ * ```tsx
12
+ * <XInput value={name} onChangeText={setName} placeholder='请输入姓名' />
13
+ * <XInput multiline placeholder='请输入备注' />
14
+ * <XInput disabled value='只读' />
15
+ * ```
16
+ *
17
+ * 与 antd Input 的对应:
18
+ * - value / onChangeText:RN 原生命名(方便 XForm.Item 直接注入,trigger='onChangeText')
19
+ * - disabled:对应 antd disabled(不可编辑 + 禁用样式)
20
+ * - multiline:对应 antd Input.TextArea
21
+ * - allowClear:对应 antd allowClear(非多行时显示清空按钮)
22
+ * - 其余 props(maxLength/keyboardType/secureTextEntry 等)原样透传给 TextInput
23
+ *
24
+ * 注意:value 是完全受控透传,不做任何 trim/transform —— 改写会打断
25
+ * Android 中文输入法的合成过程(XForm 的同值守卫也依赖原样透传)。
26
+ */
27
+ import { forwardRef, useEffect, useRef, useState } from 'react';
28
+ import { Pressable, StyleSheet, TextInput, View } from 'react-native';
29
+ import { useXTheme } from '../theme';
30
+ export const XInput = forwardRef(function XInput({ value, onChangeText, multiline, disabled, allowClear, style, containerStyle, editable, ...rest }, ref) {
31
+ const [focused, setFocused] = useState(false);
32
+ const isDisabled = disabled || editable === false;
33
+ /**
34
+ * Android IME 防回抛镜像(关键):
35
+ * 受控模式下若把父级 store 的 value 每键原样回写给 TextInput,IME 合成会被
36
+ * 打断并触发原生层再次回抛 onChangeText,形成"回写 -> 回抛 -> 回写"的同步
37
+ * 递归(Maximum call stack size exceeded)。
38
+ * 做法:输入先写本地镜像再上报,传回 TextInput 的 value 恒等于原生当前文本,
39
+ * RN 的事件计数对得上就不会再 setText,循环从源头消失。
40
+ * 外部改值(回填/重置)通过 reportedRef 区分:value 不等于我们最近上报的
41
+ * 文本,说明是外部注入,才采纳进镜像。
42
+ */
43
+ const [mirror, setMirror] = useState(value);
44
+ const reportedRef = useRef(value);
45
+ useEffect(() => {
46
+ if (value !== reportedRef.current) {
47
+ reportedRef.current = value;
48
+ setMirror(value);
49
+ }
50
+ }, [value]);
51
+ const handleChange = (text) => {
52
+ reportedRef.current = text;
53
+ setMirror(text);
54
+ onChangeText?.(text);
55
+ };
56
+ // 受控用法显示镜像;非受控(没传 value)不干预,TextInput 自己管
57
+ const displayValue = value !== undefined ? mirror : undefined;
58
+ const t = useXTheme();
59
+ const showClear = !!allowClear && !multiline && !isDisabled && !!displayValue;
60
+ /** 清空后把光标送回输入框,继续输入不用再点一次 */
61
+ const handleClear = () => {
62
+ reportedRef.current = '';
63
+ setMirror('');
64
+ onChangeText?.('');
65
+ ref && typeof ref === 'object' && ref.current?.focus();
66
+ };
67
+ return (_jsxs(View, { style: [styles.container, containerStyle], children: [_jsx(TextInput, { ...rest, ref: ref, value: displayValue, onChangeText: handleChange, multiline: multiline, editable: !isDisabled, pointerEvents: isDisabled ? 'none' : undefined, style: [
68
+ styles.input,
69
+ {
70
+ borderColor: focused && !isDisabled ? t.colorPrimary : t.colorBorder,
71
+ color: isDisabled ? t.colorTextQuaternary : t.colorText,
72
+ backgroundColor: isDisabled
73
+ ? t.colorBgContainerDisabled
74
+ : focused
75
+ ? t.colorBgContainer
76
+ : t.colorBgLayout,
77
+ },
78
+ multiline && styles.textarea,
79
+ style,
80
+ ], placeholderTextColor: rest.placeholderTextColor ?? t.colorTextQuaternary, allowFontScaling: false, onFocus: e => {
81
+ setFocused(true);
82
+ rest.onFocus?.(e);
83
+ }, onBlur: e => {
84
+ setFocused(false);
85
+ rest.onBlur?.(e);
86
+ } }), showClear && (_jsx(Pressable, { onPress: handleClear, style: styles.clearBtn, hitSlop: 8, children: _jsxs(View, { style: [styles.clearCircle, { backgroundColor: t.colorTextQuaternary }], children: [_jsx(View, { style: [styles.clearX1, { backgroundColor: t.colorTextLightSolid }] }), _jsx(View, { style: [styles.clearX2, { backgroundColor: t.colorTextLightSolid }] })] }) }))] }));
87
+ });
88
+ /** antd Input.TextArea 的对应形态 */
89
+ export const XTextArea = (props) => _jsx(XInput, { multiline: true, ...props });
90
+ XInput.TextArea = XTextArea;
91
+ const styles = StyleSheet.create({
92
+ container: {
93
+ flexDirection: 'row',
94
+ alignItems: 'center',
95
+ alignSelf: 'stretch',
96
+ },
97
+ input: {
98
+ borderWidth: StyleSheet.hairlineWidth,
99
+ borderRadius: 8,
100
+ paddingHorizontal: 12,
101
+ paddingVertical: 8,
102
+ fontSize: 15,
103
+ flex: 1,
104
+ },
105
+ textarea: {
106
+ minHeight: 72,
107
+ textAlignVertical: 'top',
108
+ paddingVertical: 10,
109
+ },
110
+ clearBtn: {
111
+ position: 'absolute',
112
+ right: 8,
113
+ justifyContent: 'center',
114
+ alignItems: 'center',
115
+ },
116
+ clearCircle: {
117
+ width: 16,
118
+ height: 16,
119
+ borderRadius: 8,
120
+ alignItems: 'center',
121
+ justifyContent: 'center',
122
+ },
123
+ clearX1: {
124
+ position: 'absolute',
125
+ width: 8,
126
+ height: 1.5,
127
+ transform: [{ rotate: '45deg' }],
128
+ },
129
+ clearX2: {
130
+ position: 'absolute',
131
+ width: 8,
132
+ height: 1.5,
133
+ transform: [{ rotate: '-45deg' }],
134
+ },
135
+ });
136
+ export default XInput;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * ============================================================================
3
+ * XLicensePlate —— 车牌号输入(duxui LicensePlate 的 XTheme 重写版)
4
+ * ============================================================================
5
+ *
6
+ * 【与 duxui LicensePlate 的关系】
7
+ * 保留 duxui 的核心交互:按输入进度切换三套键盘布局
8
+ * 省份(第 1 位)→ 城市字母(第 2 位)→ 字母+数字(第 3 位起);
9
+ * 末位可输入特殊字符(港澳学领警挂);新能源(8 位)末位仅 D/F。
10
+ * 差异:duxui 用 PullView + Context 三组件分发,这里整合为两组件:
11
+ * - XLicensePlateKeyboard:纯键盘(phase/onInput/onBackspace 受控);
12
+ * - XLicensePlate:输入格 + XPullView 弹出整合体(开箱即用)。
13
+ * ============================================================================
14
+ */
15
+ import React from 'react';
16
+ import { StyleProp, ViewStyle } from 'react-native';
17
+ /** 键盘阶段:省份 → 城市字母 → 字母数字 */
18
+ export type XLicensePlatePhase = 'province' | 'city' | 'alnum';
19
+ export interface XLicensePlateKeyboardProps {
20
+ phase: XLicensePlatePhase;
21
+ onInput: (key: string) => void;
22
+ onBackspace: () => void;
23
+ /** 限制可选键(新能源末位仅 D/F);null = 不限制 */
24
+ allowedKeys?: string[] | null;
25
+ /** 是否处于末位(普通牌显示特殊字符行) */
26
+ isLast?: boolean;
27
+ }
28
+ export declare function XLicensePlateKeyboard({ phase, onInput, onBackspace, allowedKeys, isLast }: XLicensePlateKeyboardProps): React.JSX.Element;
29
+ export interface XLicensePlateProps {
30
+ /** 受控值(如 '京A12345') */
31
+ value?: string;
32
+ onChange?: (value: string) => void;
33
+ /** 车牌位数:7 普通 / 8 新能源,默认 7 */
34
+ length?: 7 | 8;
35
+ disabled?: boolean;
36
+ placeholder?: string;
37
+ style?: StyleProp<ViewStyle>;
38
+ }
39
+ export declare function XLicensePlate({ value, onChange, length, disabled, placeholder, style }: XLicensePlateProps): React.JSX.Element;
40
+ export default XLicensePlate;
@@ -0,0 +1,172 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XLicensePlate —— 车牌号输入(duxui LicensePlate 的 XTheme 重写版)
5
+ * ============================================================================
6
+ *
7
+ * 【与 duxui LicensePlate 的关系】
8
+ * 保留 duxui 的核心交互:按输入进度切换三套键盘布局
9
+ * 省份(第 1 位)→ 城市字母(第 2 位)→ 字母+数字(第 3 位起);
10
+ * 末位可输入特殊字符(港澳学领警挂);新能源(8 位)末位仅 D/F。
11
+ * 差异:duxui 用 PullView + Context 三组件分发,这里整合为两组件:
12
+ * - XLicensePlateKeyboard:纯键盘(phase/onInput/onBackspace 受控);
13
+ * - XLicensePlate:输入格 + XPullView 弹出整合体(开箱即用)。
14
+ * ============================================================================
15
+ */
16
+ import { useCallback, useMemo, useState } from 'react';
17
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
18
+ import { XPullView } from '../XPullView';
19
+ import { useXTheme } from '../theme';
20
+ import { useXLocale } from '../XLocale';
21
+ /** 省份简称 + 使领(duxui carCity.json 同款数据) */
22
+ const PROVINCE_ROWS = [
23
+ ['京', '津', '冀', '晋', '蒙', '辽', '吉'],
24
+ ['黑', '沪', '苏', '浙', '皖', '闽', '赣'],
25
+ ['鲁', '豫', '鄂', '湘', '粤', '桂', '琼'],
26
+ ['川', '贵', '云', '藏', '陕', '甘', '青'],
27
+ ['宁', '新', '渝', '使', '领'],
28
+ ];
29
+ /** 字母表(车牌不含 I / O) */
30
+ const LETTERS = 'ABCDEFGHJKLMNPQRSTUVWXYZ'.split('');
31
+ /** 末位特殊字符(普通牌) */
32
+ const TAIL_SPECIAL = ['港', '澳', '学', '领', '警', '挂'];
33
+ export function XLicensePlateKeyboard({ phase, onInput, onBackspace, allowedKeys, isLast }) {
34
+ const t = useXTheme();
35
+ /** 单键 */
36
+ const renderKey = useCallback((label, flex = 1) => {
37
+ const disabled = allowedKeys != null && !allowedKeys.includes(label);
38
+ return (_jsx(Pressable, { disabled: disabled, onPress: () => !disabled && onInput(label), style: ({ pressed }) => [
39
+ styles.key,
40
+ { flex, borderRadius: t.borderRadius },
41
+ { backgroundColor: disabled ? t.colorBgContainerDisabled : pressed ? t.colorBgLayout : t.colorBgContainer },
42
+ ], children: _jsx(Text, { style: [styles.keyText, { color: disabled ? t.colorTextQuaternary : t.colorText }], allowFontScaling: false, children: label }) }, label));
43
+ }, [allowedKeys, onInput, t]);
44
+ /** 退格键 */
45
+ const backspaceKey = useCallback((flex = 1) => (_jsx(Pressable, { onPress: onBackspace, style: ({ pressed }) => [
46
+ styles.key,
47
+ { flex, backgroundColor: pressed ? t.colorBgLayout : t.colorBgContainerDisabled, borderRadius: t.borderRadius },
48
+ ], children: _jsx(Text, { style: [styles.keyText, { color: t.colorText }], allowFontScaling: false, children: "\u232B" }) }, '__backspace')), [onBackspace, t]);
49
+ /** 布局:行数组(每行 = [label, flex][]) */
50
+ const rows = useMemo(() => {
51
+ if (allowedKeys != null) {
52
+ // 受限模式(新能源末位 D/F):单行受限键 + 退格
53
+ return [allowedKeys.map(k => [k, 1]).concat([['__bs', 1]])];
54
+ }
55
+ if (phase === 'province') {
56
+ return PROVINCE_ROWS.map(row => row.map(k => [k, 1]));
57
+ }
58
+ if (phase === 'city') {
59
+ return [
60
+ LETTERS.slice(0, 7),
61
+ LETTERS.slice(7, 14),
62
+ LETTERS.slice(14, 21),
63
+ LETTERS.slice(21),
64
+ ].map(row => row.map(k => [k, 1]));
65
+ }
66
+ // alnum:字母 3 行 + XYZ0123 + 456789 + 退格;(普通牌末位追加特殊字符行)
67
+ const rows = [
68
+ LETTERS.slice(0, 7).map(k => [k, 1]),
69
+ LETTERS.slice(7, 14).map(k => [k, 1]),
70
+ LETTERS.slice(14, 21).map(k => [k, 1]),
71
+ [...LETTERS.slice(21), '0', '1', '2', '3'].map(k => [k, 1]),
72
+ [...'456789'.split(''), '__bs'].map(k => [k, 1]),
73
+ ];
74
+ if (isLast) {
75
+ rows.unshift([...TAIL_SPECIAL, '__bs'].map(k => [k, 1]));
76
+ }
77
+ return rows;
78
+ }, [phase, allowedKeys, isLast]);
79
+ return (_jsx(View, { style: [styles.keyboard, { backgroundColor: t.colorBgLayout }], children: rows.map((row, ri) => (_jsx(View, { style: styles.row, children: row.map(([label, flex]) => label === '__bs' ? backspaceKey(flex) : renderKey(label, flex)) }, ri))) }));
80
+ }
81
+ export function XLicensePlate({ value = '', onChange, length = 7, disabled = false, placeholder, style }) {
82
+ const t = useXTheme();
83
+ const { t: i18n } = useXLocale();
84
+ const [visible, setVisible] = useState(false);
85
+ /** 当前键盘阶段 */
86
+ const phase = value.length === 0 ? 'province' : value.length === 1 ? 'city' : 'alnum';
87
+ /** 是否在末位 */
88
+ const isLast = value.length === length - 1;
89
+ /** 新能源末位仅 D/F */
90
+ const allowedKeys = isLast && length === 8 ? ['D', 'F'] : null;
91
+ const handleInput = useCallback((key) => {
92
+ if (value.length >= length)
93
+ return;
94
+ const next = value + key;
95
+ onChange?.(next);
96
+ // 输满自动收起
97
+ if (next.length >= length)
98
+ setVisible(false);
99
+ }, [value, length, onChange]);
100
+ const handleBackspace = useCallback(() => {
101
+ onChange?.(value.slice(0, -1));
102
+ }, [value, onChange]);
103
+ /** 输入格渲染 */
104
+ const cells = Array.from({ length }, (_, i) => {
105
+ const filled = i < value.length;
106
+ const active = i === value.length;
107
+ const isNewEnergyTail = length === 8 && i === 7;
108
+ return (_jsx(View, { style: [
109
+ styles.cell,
110
+ { backgroundColor: t.colorBgContainer, borderColor: t.colorBorder },
111
+ active && { borderColor: t.colorPrimary, borderWidth: 1.5 },
112
+ isNewEnergyTail && { borderColor: t.colorSuccess, borderWidth: 1.5 },
113
+ ], children: filled && (_jsx(Text, { style: [styles.cellText, { color: t.colorText }], allowFontScaling: false, children: value[i] })) }, i));
114
+ });
115
+ return (_jsxs(_Fragment, { children: [_jsx(Pressable, { onPress: () => !disabled && setVisible(true), style: [styles.cellRow, style], disabled: disabled, children: cells }), _jsx(XPullView, { visible: visible, onClose: () => setVisible(false), side: 'bottom', overlayOpacity: 0.4, children: _jsxs(View, { style: [styles.panel, { backgroundColor: t.colorBgContainer }], children: [_jsxs(View, { style: [styles.header, { borderBottomColor: t.colorSplit }], children: [_jsx(Text, { style: [styles.headerTitle, { color: t.colorTextSecondary }], allowFontScaling: false, children: length === 8 ? i18n('newEnergyTip') : i18n('platePlaceholder') }), _jsx(Pressable, { onPress: () => setVisible(false), hitSlop: 8, children: _jsx(Text, { style: [styles.doneBtn, { color: t.colorPrimary }], children: i18n('done') }) })] }), _jsx(XLicensePlateKeyboard, { phase: phase, onInput: handleInput, onBackspace: handleBackspace, allowedKeys: allowedKeys, isLast: isLast && length === 7 })] }) })] }));
116
+ }
117
+ const styles = StyleSheet.create({
118
+ keyboard: {
119
+ padding: 6,
120
+ gap: 6,
121
+ },
122
+ row: {
123
+ flexDirection: 'row',
124
+ gap: 6,
125
+ },
126
+ key: {
127
+ height: 42,
128
+ alignItems: 'center',
129
+ justifyContent: 'center',
130
+ },
131
+ keyText: {
132
+ fontSize: 16,
133
+ fontWeight: '500',
134
+ },
135
+ cellRow: {
136
+ flexDirection: 'row',
137
+ gap: 6,
138
+ },
139
+ cell: {
140
+ flex: 1,
141
+ height: 52,
142
+ borderWidth: StyleSheet.hairlineWidth,
143
+ borderRadius: 6,
144
+ alignItems: 'center',
145
+ justifyContent: 'center',
146
+ },
147
+ cellText: {
148
+ fontSize: 18,
149
+ fontWeight: '600',
150
+ },
151
+ panel: {
152
+ borderTopLeftRadius: 12,
153
+ borderTopRightRadius: 12,
154
+ overflow: 'hidden',
155
+ },
156
+ header: {
157
+ flexDirection: 'row',
158
+ alignItems: 'center',
159
+ justifyContent: 'space-between',
160
+ paddingHorizontal: 16,
161
+ height: 44,
162
+ borderBottomWidth: StyleSheet.hairlineWidth,
163
+ },
164
+ headerTitle: {
165
+ fontSize: 13,
166
+ },
167
+ doneBtn: {
168
+ fontSize: 15,
169
+ fontWeight: '600',
170
+ },
171
+ });
172
+ export default XLicensePlate;
@@ -0,0 +1,23 @@
1
+ /** 支持的语言 */
2
+ export type XLocale = 'zh-CN' | 'en-US';
3
+ /** 文案 key(字典的并集,TS 会约束组件里不许写错 key) */
4
+ export type XLocaleKey = 'cancel' | 'confirm' | 'done' | 'close' | 'clear' | 'reset' | 'operationIrreversible' | 'expandSearch' | 'collapseSearch' | 'delete' | 'retry' | 'loading' | 'noData' | 'search' | 'pleaseInput' | 'pleaseInputLabel' | 'pleaseSelect' | 'pleaseSelectLabel' | 'cannotBeEmpty' | 'selectDate' | 'selectTime' | 'selectDateTime' | 'today' | 'weekScopeTip' | 'rewrite' | 'saveChar' | 'savedChars' | 'signHint' | 'undo' | 'tapToRecord' | 'recording' | 'stopRecord' | 'play' | 'pause' | 'rerecord' | 'useRecording' | 'maxDurationTip' | 'micPermissionTip' | 'uploadingPercent' | 'uploadFailed' | 'maxCountTip' | 'platePlaceholder' | 'newEnergyTip' | 'themeMode' | 'language' | 'light' | 'dark' | 'followSystem';
5
+ /** 星期短标签(周一开始,与 XCalendar 默认 weekStartsOn=1 对齐) */
6
+ export declare const X_WEEKDAYS: Record<XLocale, string[]>;
7
+ /** "YYYY年M月" / "September 2026" 的本地化月标题 */
8
+ export declare function xMonthTitle(locale: XLocale, year: number, month1based: number): string;
9
+ /** 全局切换语言(命令式) */
10
+ export declare function setXLocale(locale: XLocale): void;
11
+ /** 读取当前语言(非组件上下文) */
12
+ export declare function getXLocale(): XLocale;
13
+ /**
14
+ * 组件内使用的翻译 Hook。
15
+ * t 引用随 locale 变化而更新(useCallback 依赖 locale),组件自然重渲染。
16
+ */
17
+ export declare function useXLocale(): {
18
+ locale: XLocale;
19
+ setLocale: (locale: XLocale) => void;
20
+ t: (key: XLocaleKey, params?: Record<string, string | number>) => string;
21
+ };
22
+ /** 命令式翻译:Toast / 事件回调等非组件上下文里用(取当前语言一次性求值) */
23
+ export declare function xT(key: XLocaleKey, params?: Record<string, string | number>): string;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * ============================================================================
3
+ * XLocale —— X-Components 内置轻量国际化(zh-CN / en-US)
4
+ * ============================================================================
5
+ *
6
+ * 【设计取向】
7
+ * 组件库只需要"组件内文案"的多语言(取消/确定/上传中…),为此引 i18next 这类
8
+ * 大依赖不值当。这里用 zustand 全局单例 + 双语字典 + 模板插值,几十行搞定:
9
+ *
10
+ * - useXLocale():React 组件内用,返回 {locale, setLocale, t};
11
+ * - xT():命令式场景用(Toast、事件回调等非组件上下文);
12
+ * - setXLocale('en-US'):全局切换,所有组件即时更新;
13
+ * - t('uploadingPercent', {n: 66}) → '上传中 66%' / 'Uploading 66%'。
14
+ *
15
+ * 【接入业务层 i18n】
16
+ * 业务若已有自己的 i18n 体系,可通过 setXLocale 同步业务语言;本模块只负责
17
+ * X 组件内部文案,不与应用页面文案耦合。
18
+ * ============================================================================
19
+ */
20
+ import { useCallback } from 'react';
21
+ import { create } from 'zustand';
22
+ /** zh-CN 字典 */
23
+ const zhCN = {
24
+ cancel: '取消',
25
+ confirm: '确定',
26
+ done: '完成',
27
+ close: '关闭',
28
+ clear: '清空',
29
+ reset: '重置',
30
+ operationIrreversible: '此操作不可撤销',
31
+ expandSearch: '展开搜索',
32
+ collapseSearch: '收起搜索',
33
+ delete: '删除',
34
+ retry: '重试',
35
+ loading: '加载中…',
36
+ noData: '暂无数据',
37
+ search: '搜索',
38
+ pleaseInput: '请输入',
39
+ pleaseInputLabel: '请输入{n}',
40
+ pleaseSelect: '请选择',
41
+ pleaseSelectLabel: '请选择{n}',
42
+ cannotBeEmpty: '不能为空',
43
+ selectDate: '请选择日期',
44
+ selectTime: '请选择时间',
45
+ selectDateTime: '请选择日期时间',
46
+ today: '今天',
47
+ weekScopeTip: '本周',
48
+ rewrite: '重写',
49
+ saveChar: '存字',
50
+ savedChars: '已存 {n} 字',
51
+ signHint: '请在格内签名',
52
+ undo: '撤销',
53
+ tapToRecord: '点击开始录音',
54
+ recording: '录音中',
55
+ stopRecord: '停止',
56
+ play: '播放',
57
+ pause: '暂停',
58
+ rerecord: '重新录制',
59
+ useRecording: '使用录音',
60
+ maxDurationTip: '最长录制 {n} 秒',
61
+ micPermissionTip: '需要麦克风权限才能录音',
62
+ uploadingPercent: '上传中 {n}%',
63
+ uploadFailed: '上传失败',
64
+ maxCountTip: '最多选择 {n} 张',
65
+ platePlaceholder: '点击输入车牌号',
66
+ newEnergyTip: '新能源',
67
+ themeMode: '主题模式',
68
+ language: '语言',
69
+ light: '浅色',
70
+ dark: '深色',
71
+ followSystem: '跟随系统',
72
+ };
73
+ /** en-US 字典 */
74
+ const enUS = {
75
+ cancel: 'Cancel',
76
+ confirm: 'OK',
77
+ done: 'Done',
78
+ close: 'Close',
79
+ clear: 'Clear',
80
+ reset: 'Reset',
81
+ operationIrreversible: 'This action cannot be undone',
82
+ expandSearch: 'Expand',
83
+ collapseSearch: 'Collapse',
84
+ delete: 'Delete',
85
+ retry: 'Retry',
86
+ loading: 'Loading…',
87
+ noData: 'No data',
88
+ search: 'Search',
89
+ pleaseInput: 'Please input',
90
+ pleaseInputLabel: 'Please input {n}',
91
+ pleaseSelect: 'Please select',
92
+ pleaseSelectLabel: 'Please select {n}',
93
+ cannotBeEmpty: 'cannot be empty',
94
+ selectDate: 'Select date',
95
+ selectTime: 'Select time',
96
+ selectDateTime: 'Select date & time',
97
+ today: 'Today',
98
+ weekScopeTip: 'This week',
99
+ rewrite: 'Redo',
100
+ saveChar: 'Save',
101
+ savedChars: '{n} chars saved',
102
+ signHint: 'Sign inside the box',
103
+ undo: 'Undo',
104
+ tapToRecord: 'Tap to record',
105
+ recording: 'Recording',
106
+ stopRecord: 'Stop',
107
+ play: 'Play',
108
+ pause: 'Pause',
109
+ rerecord: 'Re-record',
110
+ useRecording: 'Use',
111
+ maxDurationTip: 'Max {n}s',
112
+ micPermissionTip: 'Microphone permission is required',
113
+ uploadingPercent: 'Uploading {n}%',
114
+ uploadFailed: 'Upload failed',
115
+ maxCountTip: 'Up to {n} items',
116
+ platePlaceholder: 'Tap to enter plate no.',
117
+ newEnergyTip: 'New Energy',
118
+ themeMode: 'Theme mode',
119
+ language: 'Language',
120
+ light: 'Light',
121
+ dark: 'Dark',
122
+ followSystem: 'System',
123
+ };
124
+ const DICTIONARIES = {
125
+ 'zh-CN': zhCN,
126
+ 'en-US': enUS,
127
+ };
128
+ /** 星期短标签(周一开始,与 XCalendar 默认 weekStartsOn=1 对齐) */
129
+ export const X_WEEKDAYS = {
130
+ 'zh-CN': ['一', '二', '三', '四', '五', '六', '日'],
131
+ 'en-US': ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
132
+ };
133
+ /** 英文月份全名(en 标题用;zh 用数字月份拼接) */
134
+ const EN_MONTHS = [
135
+ 'January', 'February', 'March', 'April', 'May', 'June',
136
+ 'July', 'August', 'September', 'October', 'November', 'December',
137
+ ];
138
+ /** "YYYY年M月" / "September 2026" 的本地化月标题 */
139
+ export function xMonthTitle(locale, year, month1based) {
140
+ return locale === 'zh-CN' ? `${year}年${month1based}月` : `${EN_MONTHS[month1based - 1]} ${year}`;
141
+ }
142
+ const useXLocaleStore = create(set => ({
143
+ locale: 'zh-CN',
144
+ setLocale: locale => set({ locale }),
145
+ }));
146
+ /** 全局切换语言(命令式) */
147
+ export function setXLocale(locale) {
148
+ useXLocaleStore.getState().setLocale(locale);
149
+ }
150
+ /** 读取当前语言(非组件上下文) */
151
+ export function getXLocale() {
152
+ return useXLocaleStore.getState().locale;
153
+ }
154
+ /** 插值:'{n} 张' + {n:3} → '3 张' */
155
+ function interpolate(template, params) {
156
+ if (!params)
157
+ return template;
158
+ return template.replace(/\{(\w+)\}/g, (_, name) => params[name] !== undefined ? String(params[name]) : `{${name}}`);
159
+ }
160
+ /**
161
+ * 组件内使用的翻译 Hook。
162
+ * t 引用随 locale 变化而更新(useCallback 依赖 locale),组件自然重渲染。
163
+ */
164
+ export function useXLocale() {
165
+ const locale = useXLocaleStore(s => s.locale);
166
+ const setLocale = useXLocaleStore(s => s.setLocale);
167
+ const t = useCallback((key, params) => interpolate(DICTIONARIES[locale][key], params), [locale]);
168
+ return { locale, setLocale, t };
169
+ }
170
+ /** 命令式翻译:Toast / 事件回调等非组件上下文里用(取当前语言一次性求值) */
171
+ export function xT(key, params) {
172
+ const locale = useXLocaleStore.getState().locale;
173
+ return interpolate(DICTIONARIES[locale][key], params);
174
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * ============================================================================
3
+ * XModalForm —— 居中弹出表单(duxui XModalForm 移植)
4
+ * ============================================================================
5
+ *
6
+ * 【与底部弹窗的区别】
7
+ * side='center' 的 XPullView:面板从屏幕中间"放大"出现(scale 0.85→1)
8
+ * 同时透明度淡入,默认 150ms —— 比底部滑入更轻快,适合确认框/表单这类
9
+ * 小面积内容(duxui 同款设计)。
10
+ *
11
+ * 【表单校验约定】
12
+ * onSubmit 返回 false(或 Promise<false>)时弹窗不关闭,
13
+ * 可用来做"校验不通过就不让关"的逻辑。
14
+ * ============================================================================
15
+ */
16
+ import React from 'react';
17
+ import { ViewStyle, StyleProp } from 'react-native';
18
+ interface XModalFormProps {
19
+ visible: boolean;
20
+ onClose: () => void;
21
+ title?: string;
22
+ /** 表单内容(TextInput 等,由调用方自由组合) */
23
+ children?: React.ReactNode;
24
+ /** 确认回调,返回 false 时不关闭(可做表单校验) */
25
+ onSubmit?: () => boolean | void | Promise<boolean | void>;
26
+ confirmText?: string;
27
+ cancelText?: string;
28
+ duration?: number;
29
+ style?: StyleProp<ViewStyle>;
30
+ }
31
+ export declare function XModalForm({ visible, onClose, title, children, onSubmit, confirmText, cancelText, duration, style, }: XModalFormProps): React.JSX.Element;
32
+ export {};
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XModalForm —— 居中弹出表单(duxui XModalForm 移植)
5
+ * ============================================================================
6
+ *
7
+ * 【与底部弹窗的区别】
8
+ * side='center' 的 XPullView:面板从屏幕中间"放大"出现(scale 0.85→1)
9
+ * 同时透明度淡入,默认 150ms —— 比底部滑入更轻快,适合确认框/表单这类
10
+ * 小面积内容(duxui 同款设计)。
11
+ *
12
+ * 【表单校验约定】
13
+ * onSubmit 返回 false(或 Promise<false>)时弹窗不关闭,
14
+ * 可用来做"校验不通过就不让关"的逻辑。
15
+ * ============================================================================
16
+ */
17
+ import { useCallback, useMemo } from 'react';
18
+ import { Pressable, StyleSheet, View, ScrollView, Text } from 'react-native';
19
+ import { XPullView } from '../XPullView';
20
+ import { useXTheme } from '../theme';
21
+ import { useXLocale } from '../XLocale';
22
+ export function XModalForm({ visible, onClose, title, children, onSubmit, confirmText, cancelText, duration = 150, style, }) {
23
+ const t = useXTheme();
24
+ const { t: i18n } = useXLocale();
25
+ const resolvedConfirm = confirmText ?? i18n('confirm');
26
+ const resolvedCancel = cancelText ?? i18n('cancel');
27
+ /** 点确定:await 校验回调,只有没返回 false 才关闭 */
28
+ const handleSubmit = useCallback(async () => {
29
+ const result = await onSubmit?.();
30
+ if (result !== false) {
31
+ onClose();
32
+ }
33
+ }, [onSubmit, onClose]);
34
+ const styles = useMemo(() => StyleSheet.create({
35
+ /** 卡片本体:固定宽度 300、圆角、白底 */
36
+ card: {
37
+ width: 300,
38
+ backgroundColor: t.colorBgContainer,
39
+ borderRadius: 12,
40
+ overflow: 'hidden',
41
+ },
42
+ header: {
43
+ paddingHorizontal: 20,
44
+ paddingTop: 18,
45
+ paddingBottom: 12,
46
+ },
47
+ title: {
48
+ fontSize: 17,
49
+ fontWeight: '600',
50
+ textAlign: 'center',
51
+ },
52
+ body: {
53
+ maxHeight: 320,
54
+ paddingHorizontal: 20,
55
+ paddingVertical: 8,
56
+ },
57
+ footer: {
58
+ flexDirection: 'row',
59
+ borderTopWidth: StyleSheet.hairlineWidth,
60
+ borderTopColor: t.colorSplit,
61
+ },
62
+ footerBtn: {
63
+ flex: 1,
64
+ height: 48,
65
+ justifyContent: 'center',
66
+ alignItems: 'center',
67
+ },
68
+ cancelBtn: {},
69
+ submitBtn: {
70
+ borderLeftWidth: StyleSheet.hairlineWidth,
71
+ borderLeftColor: t.colorSplit,
72
+ },
73
+ pressed: {
74
+ backgroundColor: t.colorBgLayout,
75
+ },
76
+ cancelText: {
77
+ fontSize: 16,
78
+ },
79
+ submitText: {
80
+ fontSize: 16,
81
+ fontWeight: '600',
82
+ },
83
+ }), [t]);
84
+ return (_jsx(XPullView, { visible: visible, onClose: onClose, side: 'center', duration: duration, overlayOpacity: 0.25, children: _jsxs(View, { style: [styles.card, style], children: [!!title && (_jsx(View, { style: styles.header, children: _jsx(Text, { style: [styles.title, { color: t.colorText }], children: title }) })), _jsx(ScrollView, { style: styles.body, keyboardShouldPersistTaps: 'handled', children: children }), _jsxs(View, { style: styles.footer, children: [_jsx(Pressable, { onPress: onClose, style: ({ pressed }) => [styles.footerBtn, styles.cancelBtn, pressed && styles.pressed], children: _jsx(Text, { style: [styles.cancelText, { color: t.colorTextSecondary }], children: resolvedCancel }) }), _jsx(Pressable, { onPress: handleSubmit, style: ({ pressed }) => [styles.footerBtn, styles.submitBtn, pressed && styles.pressed], children: _jsx(Text, { style: [styles.submitText, { color: t.colorPrimary }], children: resolvedConfirm }) })] })] }) }));
85
+ }