react-native-x-components 0.1.0 → 0.2.1
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.
- package/AGENTS.md +128 -0
- package/README.md +54 -0
- package/dist/XActionSheet/global.d.ts +15 -0
- package/dist/XActionSheet/global.js +32 -0
- package/dist/XActionSheet/index.js +3 -2
- package/dist/XCalendar/index.js +10 -6
- package/dist/XDropdownMenu/index.js +37 -22
- package/dist/XInput/index.d.ts +11 -0
- package/dist/XInput/index.js +84 -5
- package/dist/XLicensePlate/index.js +3 -1
- package/dist/XPopupProvider/index.js +2 -1
- package/dist/XSignature/XSignatureSkia.js +13 -5
- package/dist/XSignature/index.js +1 -2
- package/dist/XTheme/BrandColor.d.ts +20 -0
- package/dist/XTheme/BrandColor.js +33 -0
- package/dist/XTheme/ThemeControls.d.ts +22 -0
- package/dist/XTheme/ThemeControls.js +92 -0
- package/dist/XUpload/XUploadImage.js +57 -11
- package/dist/XUpload/XUploadVideo.js +29 -8
- package/dist/XUpload/helpers.d.ts +8 -0
- package/dist/XUpload/helpers.js +23 -0
- package/dist/XUpload/index.d.ts +1 -1
- package/dist/XUpload/index.js +1 -1
- package/dist/XVideoPreview/index.d.ts +17 -0
- package/dist/XVideoPreview/index.js +78 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +10 -1
- package/dist/theme.d.ts +0 -8
- package/dist/theme.js +22 -6
- package/package.json +12 -7
- package/wechat-qrcode.png +0 -0
- package/dist/XElevator/index.d.ts +0 -52
- package/dist/XElevator/index.js +0 -138
package/AGENTS.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# AGENTS.md — AI 编码工具使用指南
|
|
2
|
+
|
|
3
|
+
> 本文件供 AI 编码工具(Cursor / Copilot / Claude Code / WorkBuddy 等)在使用
|
|
4
|
+
> react-native-x-components 时参考。人也可以读,但写法面向 AI。
|
|
5
|
+
|
|
6
|
+
## 0. 安装与前置依赖
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install react-native-x-components dayjs zustand
|
|
10
|
+
# 必需 peer(Expo 项目):
|
|
11
|
+
npx expo install react-native-reanimated react-native-safe-area-context react-native-svg react-native-gesture-handler
|
|
12
|
+
# 可选 peer(用到哪个装哪个):
|
|
13
|
+
# @shopify/react-native-skia → XSignatureSkia
|
|
14
|
+
# expo-audio → XRecord
|
|
15
|
+
# expo-image-picker + expo-image-manipulator + expo-file-system → XUpload*
|
|
16
|
+
# expo-video → XVideoPreview
|
|
17
|
+
# react-native-zoom-toolkit → XImagePreview 缩放
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
原生依赖安装后需要 `npx expo prebuild --clean`(裸项目则重新构建原生包),
|
|
21
|
+
否则录音/拍照权限弹窗、Skia、视频编解码不会生效。
|
|
22
|
+
|
|
23
|
+
## 1. 必须的 App 根部接线(漏掉 = 命令式 API 静默失效)
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
27
|
+
import { XPopupProvider } from 'react-native-x-components';
|
|
28
|
+
|
|
29
|
+
export default function App() {
|
|
30
|
+
return (
|
|
31
|
+
<GestureHandlerRootView style={{flex: 1}}>
|
|
32
|
+
<XPopupProvider>
|
|
33
|
+
{/* 整个 App */}
|
|
34
|
+
</XPopupProvider>
|
|
35
|
+
</GestureHandlerRootView>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
XPopupProvider 内部挂载:XTopView(弹层宿主)、XConfirmForm、XActionSheet 全局实例、
|
|
41
|
+
XToast / XLoadingModal / XImagePreview 的 Provider。以下命令式 API 全部依赖它:
|
|
42
|
+
|
|
43
|
+
- `XToastService.show({ message, type, position })`
|
|
44
|
+
- `confirm({ title, content, danger })` → `Promise<boolean>`
|
|
45
|
+
- `showXActionSheet({ title, options })` → `Promise<value | null>`
|
|
46
|
+
- `XLoadingModalService.show({ message }) / hide()`
|
|
47
|
+
- `XImagePreviewService.show({ images, initialIndex })`
|
|
48
|
+
|
|
49
|
+
## 2. 常见错误对照表
|
|
50
|
+
|
|
51
|
+
| 症状 | 原因 | 修复 |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| Toast / confirm / ActionSheet 无反应 | XPopupProvider 未挂根 | App 根部包 `<XPopupProvider>` |
|
|
54
|
+
| 上传报 adapter 错误 | 未注入上传适配器 | `setXUploadAdapter(createMinioPresignedAdapter({...}))` |
|
|
55
|
+
| 手势 / 滚轮不响应 | 缺 gesture-handler | install + GestureHandlerRootView 包根 |
|
|
56
|
+
| Skia 签名报错 | 未装 skia | `npx expo install @shopify/react-native-skia` |
|
|
57
|
+
| 录音/拍照无权限弹窗 | prebuild 未重跑 | `npx expo prebuild --clean` |
|
|
58
|
+
| 暗黑切换全局不生效 | mode 被手动写死 | `setXThemeMode('system')` 恢复跟随 |
|
|
59
|
+
| 主题色不生效 | 组件硬编码颜色 | 颜色一律取 `useXTheme()` token |
|
|
60
|
+
|
|
61
|
+
## 3. 最小代码骨架
|
|
62
|
+
|
|
63
|
+
### 上传(MinIO 预签名直传)
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { setXUploadAdapter, createMinioPresignedAdapter, XUploadImage } from 'react-native-x-components';
|
|
67
|
+
|
|
68
|
+
setXUploadAdapter(createMinioPresignedAdapter({
|
|
69
|
+
getUploadUrl: async ({ name }) => {
|
|
70
|
+
const res = await fetch(API + '/file/getUploadUrl?fileName=' + name);
|
|
71
|
+
const { data } = await res.json();
|
|
72
|
+
return { objectKey: data.objectKey, preSignedUrl: data.preSignedUrl };
|
|
73
|
+
},
|
|
74
|
+
getPreviewUrl: async objectKey => fetchPreviewUrl(objectKey),
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
<XUploadImage value={imgs} onChange={setImgs} max={9} />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 表单
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { XForm, XInput, XButton } from 'react-native-x-components';
|
|
84
|
+
|
|
85
|
+
const [form] = XForm.useForm();
|
|
86
|
+
<XForm form={form} onFinish={console.log}>
|
|
87
|
+
<XForm.Item label="姓名" name="name" trigger="onChangeText"
|
|
88
|
+
rules={[{ required: true, message: '必填' }]}>
|
|
89
|
+
<XInput placeholder="请输入姓名" />
|
|
90
|
+
</XForm.Item>
|
|
91
|
+
<XButton type="primary" onPress={() => form.submit()}>提交</XButton>
|
|
92
|
+
</XForm>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
注意:输入类控件 `trigger="onChangeText"`,选择类 `trigger="onChange"`(默认),写错则值不同步。
|
|
96
|
+
|
|
97
|
+
### 主题 / 语言
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { useXTheme, setXThemeMode, setXBrandByName, setXLocale } from 'react-native-x-components';
|
|
101
|
+
|
|
102
|
+
setXThemeMode('dark'); // 'light' | 'dark' | 'system'(默认跟随系统)
|
|
103
|
+
setXBrandByName('薰衣紫'); // 4 套预设品牌色,深浅色各自适配
|
|
104
|
+
setXLocale('en-US'); // 组件文案即时切换
|
|
105
|
+
|
|
106
|
+
const t = useXTheme(); // 组件内取 token,禁硬编码颜色
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 4. 设计约定(生成代码前遵守)
|
|
110
|
+
|
|
111
|
+
1. **弹层一律用本库体系**:XPullView(side: bottom/top/left/right/center)+ 全局 XTopView 宿主;**不要用 RN Modal**。
|
|
112
|
+
2. **命令式服务统一 `XxxService.show()` 风格**,不再自己 setState 管 visible(除非用受控形态组件)。
|
|
113
|
+
3. **颜色/圆角一律 `useXTheme()`**:浅深两套 token 自动切换,硬编码颜色是 bug。
|
|
114
|
+
4. **XRadio / XCheckbox 的 onChange 事件对象与 antd 同构**:`e.target.value / e.target.checked`。
|
|
115
|
+
5. XForm.Item 的 trigger:输入类 `onChangeText`,其余 `onChange`。
|
|
116
|
+
6. 上传适配器协议:`upload(file, onProgress) => Promise<XUploadResult>`;`XUploadResult` 至少含 `url`(或 `objectKey`)。
|
|
117
|
+
7. XSignatureSkia 需要 Skia;XRecord 需要 expo-audio;这些可选 peer 没装时不要 import 对应组件。
|
|
118
|
+
|
|
119
|
+
## 5. 组件速查(36 个)
|
|
120
|
+
|
|
121
|
+
基础:XButton · XDivider · XInput(customKeyboard 数字键盘) · XTag · XProgress · XRadio · XCheckbox
|
|
122
|
+
表单:XForm · XFormPro · XCascadeSelect · XMultiSelect
|
|
123
|
+
弹层:XPullView · XActionSheet · XPicker · XPickerDate · XModalForm · XConfirmForm · XAnimatedView · XToast · XLoadingModal
|
|
124
|
+
交互:XTabs · XDropdownMenu · XAnimatedSearchPanel · XNumberKeyboard · XLicensePlate
|
|
125
|
+
媒体:XCalendar · XCalendarPopup · XRecord · XSignature · XSignatureSkia · XUploadImage · XUploadVideo · XVideoPreview · XImage · XImagePreview
|
|
126
|
+
数据:XChart
|
|
127
|
+
|
|
128
|
+
完整用法与 props 表:https://react-native-x-components.dev(AI 可抓取 /llms-full.txt 获取全量文档)。
|
package/README.md
CHANGED
|
@@ -1,7 +1,51 @@
|
|
|
1
1
|
# react-native-x-components
|
|
2
2
|
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/react-native-x-components)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://github.com/TimSpan/rn-x-components)
|
|
8
|
+
|
|
9
|
+
</div>
|
|
10
|
+
|
|
3
11
|
Ant Design 风格的 React Native (Expo) 组件库。对齐 antd v5 设计 token,内置**暗黑模式**与**中英双语**,弹层体系基于自研 TopView(比 RN Modal 更快),覆盖表单、选择器、日历、录音、签名(Skia 逐字签名)、上传(MinIO 预签名适配器)、轻量图表等 40+ 组件。
|
|
4
12
|
|
|
13
|
+
- 📦 GitHub:https://github.com/TimSpan/rn-x-components
|
|
14
|
+
- 📖 文档:https://react-native-x-components.dev(AI 可抓取 `/llms-full.txt` 全量文档)
|
|
15
|
+
- 💬 微信交流(问题反馈 / 技术交流 / 定制咨询):
|
|
16
|
+
|
|
17
|
+
<div align="center">
|
|
18
|
+
<img src="./wechat-qrcode.png" alt="微信添加 Otis 为好友" width="260" />
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
> 🤖 **AI 编码工具(Cursor/Copilot/Claude 等)请先读包内 [AGENTS.md](./AGENTS.md)** ——
|
|
22
|
+
> 前置接线清单、常见错误对照表、最小代码骨架,能避开 90% 的集成坑。
|
|
23
|
+
|
|
24
|
+
## 30 秒最小骨架
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
28
|
+
import { XPopupProvider, XButton, XToastService, confirm } from 'react-native-x-components';
|
|
29
|
+
|
|
30
|
+
export default function App() {
|
|
31
|
+
return (
|
|
32
|
+
<GestureHandlerRootView style={{flex: 1}}>
|
|
33
|
+
<XPopupProvider> {/* 必挂!否则命令式 API 静默失效 */}
|
|
34
|
+
<XButton
|
|
35
|
+
type="primary"
|
|
36
|
+
onPress={async () => {
|
|
37
|
+
const ok = await confirm({ title: '确认提交?', danger: true });
|
|
38
|
+
if (ok) XToastService.show({ message: '已提交', type: 'success' });
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
点我
|
|
42
|
+
</XButton>
|
|
43
|
+
</XPopupProvider>
|
|
44
|
+
</GestureHandlerRootView>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
5
49
|
## 特性
|
|
6
50
|
|
|
7
51
|
- 🌗 **暗黑模式**:`useXTheme()` 全组件自适应,`setXThemeMode('light' | 'dark' | 'system')` 一行切换
|
|
@@ -17,12 +61,22 @@ Ant Design 风格的 React Native (Expo) 组件库。对齐 antd v5 设计 token
|
|
|
17
61
|
npm install react-native-x-components dayjs zustand
|
|
18
62
|
# 必需 peer 依赖(Expo 项目推荐用 expo install 自动匹配版本)
|
|
19
63
|
npx expo install react-native-reanimated react-native-safe-area-context react-native-svg
|
|
64
|
+
# 推荐安装(XImagePreview/XInput 焦点等需要手势处理):
|
|
65
|
+
npx expo install react-native-gesture-handler
|
|
20
66
|
# 可选(按需装):
|
|
21
67
|
# 签名: npx expo install @shopify/react-native-skia
|
|
22
68
|
# 录音: npx expo install expo-audio
|
|
23
69
|
# 上传: npx expo install expo-image-picker expo-image-manipulator expo-file-system
|
|
24
70
|
```
|
|
25
71
|
|
|
72
|
+
### Android 权限说明(首次录音 / 拍照 / 选媒体时系统会自动弹)
|
|
73
|
+
|
|
74
|
+
- **录音**:`expo-audio` 在 `npx expo prebuild --clean` 后会自动往 AndroidManifest.xml 注入 `RECORD_AUDIO`、iOS Info.plist 注入 `NSMicrophoneUsageDescription`
|
|
75
|
+
- **拍照**:自动注入 `CAMERA`、iOS NSCameraUsageDescription
|
|
76
|
+
- **选媒体**:自动注入 `READ_MEDIA_IMAGES` / `READ_MEDIA_VIDEO` (Android 13+)、`READ_EXTERNAL_STORAGE` (Android 12-) 及 NSPhotoLibraryUsageDescription (iOS)
|
|
77
|
+
|
|
78
|
+
如果预编译后系统仍不弹权限框,先执行 `npx expo prebuild --clean && npx expo run:android` 让插件重新注入原生配置。
|
|
79
|
+
|
|
26
80
|
## 快速上手
|
|
27
81
|
|
|
28
82
|
```tsx
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XActionSheet 全局命令式 API:
|
|
3
|
+
* XActionSheet.show({title, options}) → Promise<value|null>
|
|
4
|
+
*
|
|
5
|
+
* 在 App 根部的 XPopupProvider 下挂载一次 XActionSheetGlobalOverlay 即可。
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { XActionSheetOption } from './index';
|
|
9
|
+
/** 命令式调用入口 */
|
|
10
|
+
export declare function showXActionSheet(opts: {
|
|
11
|
+
title?: string;
|
|
12
|
+
options: XActionSheetOption[];
|
|
13
|
+
}): Promise<any | null>;
|
|
14
|
+
/** 全局覆盖层:挂一次到 XPopupProvider 内即可 */
|
|
15
|
+
export declare function XActionSheetGlobalOverlay(): React.JSX.Element | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
import { XActionSheet } from './index';
|
|
4
|
+
const useStore = create(set => ({
|
|
5
|
+
visible: false,
|
|
6
|
+
options: [],
|
|
7
|
+
show: (opts) => {
|
|
8
|
+
return new Promise(resolve => {
|
|
9
|
+
set({ visible: true, title: opts.title, options: opts.options, resolve });
|
|
10
|
+
});
|
|
11
|
+
},
|
|
12
|
+
close: (value) => {
|
|
13
|
+
set(state => {
|
|
14
|
+
state.resolve?.(value);
|
|
15
|
+
return { visible: false, options: [], resolve: undefined };
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
/** 命令式调用入口 */
|
|
20
|
+
export async function showXActionSheet(opts) {
|
|
21
|
+
return useStore.getState().show(opts);
|
|
22
|
+
}
|
|
23
|
+
/** 全局覆盖层:挂一次到 XPopupProvider 内即可 */
|
|
24
|
+
export function XActionSheetGlobalOverlay() {
|
|
25
|
+
const visible = useStore(s => s.visible);
|
|
26
|
+
const title = useStore(s => s.title);
|
|
27
|
+
const options = useStore(s => s.options);
|
|
28
|
+
const close = useStore(s => s.close);
|
|
29
|
+
if (!visible)
|
|
30
|
+
return null;
|
|
31
|
+
return (_jsx(XActionSheet, { visible: visible, onClose: () => close(null), options: options, title: title, onSelect: opt => close(opt.value) }));
|
|
32
|
+
}
|
|
@@ -89,10 +89,11 @@ export function XActionSheet({ visible, onClose, options, title, cancelText, dur
|
|
|
89
89
|
color: t.colorTextSecondary,
|
|
90
90
|
},
|
|
91
91
|
}), [t]);
|
|
92
|
-
/**
|
|
92
|
+
/** 点选项:先通知业务方选中了谁,再关弹层(顺序关键:命令式 show() 的
|
|
93
|
+
* Promise 依赖 onSelect 先 resolve,onClose 后触发时 resolve 已消费) */
|
|
93
94
|
const handleSelect = useCallback((option) => {
|
|
94
|
-
onClose();
|
|
95
95
|
onSelect?.(option);
|
|
96
|
+
onClose();
|
|
96
97
|
}, [onClose, onSelect]);
|
|
97
98
|
return (_jsx(XPullView, { visible: visible, onClose: onClose, side: 'bottom', duration: duration, overlayOpacity: 0.4, children: _jsxs(View, { style: [styles.panel, style, { paddingBottom: Math.max(insets.bottom, 10) }], children: [!!title && (_jsx(View, { style: styles.header, children: _jsx(Text, { style: styles.title, children: title }) })), _jsx(View, { style: styles.optionList, children: options.map(option => (_jsx(Pressable, { disabled: option.disabled, onPress: () => handleSelect(option),
|
|
98
99
|
// style 可以是函数:pressed 状态时换个底色,模拟按压反馈
|
package/dist/XCalendar/index.js
CHANGED
|
@@ -147,24 +147,28 @@ export function XCalendar({ mode = 'day', value, defaultValue, multiple = false,
|
|
|
147
147
|
setInnerValue(next);
|
|
148
148
|
onChange?.(next);
|
|
149
149
|
}, [mode, multiple, currentValue, onChange, weekStartsOn, pendingStart, isDisabled]);
|
|
150
|
-
/** 单元格样式计算
|
|
150
|
+
/** 单元格样式计算
|
|
151
|
+
* 【scope 重新选择规则】已选完一个范围后再点击:pendingStart 存在时
|
|
152
|
+
* 优先渲染"新一轮起点",忽略旧范围(否则样式纹丝不动,像卡死)。
|
|
153
|
+
* 渲染优先级:pendingStart(scope 新起点)> 旧范围 > day 选中 */
|
|
151
154
|
const cellState = useCallback((d) => {
|
|
152
155
|
const key = d.format('YYYY-MM-DD');
|
|
153
156
|
const disabled = isDisabled(d);
|
|
154
157
|
const isToday = d.isSame(dayjs(), 'day');
|
|
155
158
|
let selected = false;
|
|
156
159
|
let inRange = false;
|
|
157
|
-
if (mode === '
|
|
160
|
+
if (mode === 'scope' && pendingStart) {
|
|
161
|
+
// 新一轮选择中:只高亮起点,旧范围不再显示
|
|
162
|
+
selected = key === pendingStart.format('YYYY-MM-DD');
|
|
163
|
+
}
|
|
164
|
+
else if (mode === 'day') {
|
|
158
165
|
selected = multiple ? selectedSet.has(key) : key === selStart?.format('YYYY-MM-DD');
|
|
159
166
|
}
|
|
160
167
|
else if (selStart && selEnd) {
|
|
161
|
-
// week/scope
|
|
168
|
+
// week/scope:起止实心、中间浅底
|
|
162
169
|
selected = key === selStart.format('YYYY-MM-DD') || key === selEnd.format('YYYY-MM-DD');
|
|
163
170
|
inRange = d.isAfter(selStart, 'day') && d.isBefore(selEnd, 'day');
|
|
164
171
|
}
|
|
165
|
-
else if (mode === 'scope' && pendingStart) {
|
|
166
|
-
selected = key === pendingStart.format('YYYY-MM-DD');
|
|
167
|
-
}
|
|
168
172
|
return { selected, inRange, disabled, isToday };
|
|
169
173
|
}, [mode, multiple, selectedSet, selStart, selEnd, pendingStart, isDisabled]);
|
|
170
174
|
const renderCell = (d, index) => {
|
|
@@ -46,42 +46,56 @@ function TriggerTitle({ itemProps, active, onPress, height, }) {
|
|
|
46
46
|
? itemProps.options.find(o => o.value === itemProps.value)?.name
|
|
47
47
|
: undefined;
|
|
48
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: "\
|
|
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: "\u25BC" })] }));
|
|
50
50
|
}
|
|
51
51
|
const MenuOverlay = ({ visible, trigger, round, onClose, onExitEnd, snapshot, panel }) => {
|
|
52
52
|
const t = useXTheme();
|
|
53
|
-
/** 面板内容高度(onLayout
|
|
53
|
+
/** 面板内容高度(onLayout 实测,决定遮罩起点 & 完全收起判断) */
|
|
54
54
|
const [panelHeight, setPanelHeight] = useState(0);
|
|
55
|
-
|
|
55
|
+
/** scaleY 展开动画(transformOrigin 顶部 = 触发条下沿),像"从触发条下面抽出/收回" */
|
|
56
|
+
const scaleY = useSharedValue(0);
|
|
56
57
|
const maskOpacity = useSharedValue(0);
|
|
57
|
-
/**
|
|
58
|
-
* 面板位移:隐藏态 = -(panelHeight + 20)(藏在触发条上方),visible 后
|
|
59
|
-
* 下一帧滑到 0(duxui 的 translateY(-100%) 下拉)。panelHeight 未测得前
|
|
60
|
-
* 不动画,避免从 0 位置闪现。
|
|
61
|
-
*/
|
|
58
|
+
/** 展开 scaleY 0→1(顶部锚点),遮罩淡入 */
|
|
62
59
|
useEffect(() => {
|
|
63
|
-
if (visible
|
|
60
|
+
if (visible) {
|
|
64
61
|
const raf = requestAnimationFrame(() => {
|
|
65
|
-
|
|
66
|
-
maskOpacity.value = withTiming(
|
|
62
|
+
scaleY.value = withTiming(1, { duration: 200, easing: Easing.out(Easing.cubic) });
|
|
63
|
+
maskOpacity.value = withTiming(0.4, { duration: 200 });
|
|
67
64
|
});
|
|
68
65
|
return () => cancelAnimationFrame(raf);
|
|
69
66
|
}
|
|
70
|
-
}, [visible,
|
|
71
|
-
/**
|
|
67
|
+
}, [visible, scaleY, maskOpacity]);
|
|
68
|
+
/** 收起 scaleY →0(同锚点收回,不再向上平移),动画后通知宿主移除 */
|
|
72
69
|
useEffect(() => {
|
|
73
70
|
if (!visible) {
|
|
74
|
-
|
|
75
|
-
maskOpacity.value = withTiming(0, { duration:
|
|
76
|
-
const timer = setTimeout(onExitEnd,
|
|
71
|
+
scaleY.value = withTiming(0, { duration: 180, easing: Easing.in(Easing.cubic) });
|
|
72
|
+
maskOpacity.value = withTiming(0, { duration: 180 });
|
|
73
|
+
const timer = setTimeout(onExitEnd, 220);
|
|
77
74
|
return () => clearTimeout(timer);
|
|
78
75
|
}
|
|
79
|
-
}, [visible,
|
|
76
|
+
}, [visible, scaleY, maskOpacity, onExitEnd]);
|
|
80
77
|
const panelStyle = useAnimatedStyle(() => ({
|
|
81
|
-
transform: [{
|
|
78
|
+
transform: [{ scaleY: scaleY.value }],
|
|
82
79
|
}));
|
|
83
80
|
const maskStyle = useAnimatedStyle(() => ({ opacity: maskOpacity.value }));
|
|
84
|
-
return (_jsxs(View, { style: StyleSheet.absoluteFill, pointerEvents: 'box-none', children: [_jsx(AnimatedPressable, { style: [
|
|
81
|
+
return (_jsxs(View, { style: StyleSheet.absoluteFill, pointerEvents: 'box-none', children: [_jsx(AnimatedPressable, { style: [
|
|
82
|
+
{
|
|
83
|
+
position: 'absolute',
|
|
84
|
+
left: 0,
|
|
85
|
+
right: 0,
|
|
86
|
+
top: trigger.y + trigger.height,
|
|
87
|
+
bottom: 0,
|
|
88
|
+
backgroundColor: '#000',
|
|
89
|
+
},
|
|
90
|
+
maskStyle,
|
|
91
|
+
], onPress: onClose }), _jsx(View, { pointerEvents: 'none', style: {
|
|
92
|
+
position: 'absolute',
|
|
93
|
+
left: trigger.x,
|
|
94
|
+
top: trigger.y,
|
|
95
|
+
width: trigger.width,
|
|
96
|
+
height: trigger.height,
|
|
97
|
+
backgroundColor: t.colorBgContainer,
|
|
98
|
+
}, children: snapshot }), _jsx(Animated.View, { style: [
|
|
85
99
|
styles.panel,
|
|
86
100
|
{
|
|
87
101
|
left: trigger.x,
|
|
@@ -90,6 +104,7 @@ const MenuOverlay = ({ visible, trigger, round, onClose, onExitEnd, snapshot, pa
|
|
|
90
104
|
backgroundColor: t.colorBgContainer,
|
|
91
105
|
borderBottomLeftRadius: round ? t.borderRadiusXL : 0,
|
|
92
106
|
borderBottomRightRadius: round ? t.borderRadiusXL : 0,
|
|
107
|
+
transformOrigin: '50% 0%',
|
|
93
108
|
},
|
|
94
109
|
panelStyle,
|
|
95
110
|
], pointerEvents: 'auto', children: _jsx(View, { onLayout: e => {
|
|
@@ -192,7 +207,7 @@ export function XDropdownMenu({ round = true, onOpenChange, triggerHeight = 44,
|
|
|
192
207
|
}, [remove]);
|
|
193
208
|
/** context value:open/close/activeIndex 分发给每个 Item */
|
|
194
209
|
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))) }));
|
|
210
|
+
return (_jsx(View, { ref: triggerRef, style: [styles.triggerRow, { height: triggerHeight, backgroundColor: t.colorBgContainer }, style], children: items.map((child, i) => (_jsx(MenuContext.Provider, { value: { ...ctx, index: i }, children: child }, i))) }));
|
|
196
211
|
}
|
|
197
212
|
/**
|
|
198
213
|
* 触发条单项(真实渲染):从 MenuContext 拿到自己的序号与开合动作。
|
|
@@ -202,7 +217,7 @@ export function XDropdownMenuItem(props) {
|
|
|
202
217
|
const ctx = useContext(MenuContext);
|
|
203
218
|
if (!ctx)
|
|
204
219
|
return null;
|
|
205
|
-
return (_jsx(TriggerTitle, { itemProps: props, active: ctx.activeIndex === ctx.index, onPress: () => !props.disabled && ctx.open(ctx.index), height:
|
|
220
|
+
return (_jsx(TriggerTitle, { itemProps: props, active: ctx.activeIndex === ctx.index, onPress: () => !props.disabled && ctx.open(ctx.index), height: ctx.triggerHeight }));
|
|
206
221
|
}
|
|
207
222
|
const styles = StyleSheet.create({
|
|
208
223
|
triggerRow: {
|
|
@@ -224,7 +239,7 @@ const styles = StyleSheet.create({
|
|
|
224
239
|
fontWeight: '600',
|
|
225
240
|
},
|
|
226
241
|
arrow: {
|
|
227
|
-
fontSize:
|
|
242
|
+
fontSize: 9,
|
|
228
243
|
marginLeft: 4,
|
|
229
244
|
},
|
|
230
245
|
arrowOpen: {
|
package/dist/XInput/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* <XInput value={name} onChangeText={setName} placeholder='请输入姓名' />
|
|
12
12
|
* <XInput multiline placeholder='请输入备注' />
|
|
13
13
|
* <XInput disabled value='只读' />
|
|
14
|
+
* <XInput customKeyboard='numeric' value={amount} onChangeText={setAmount} /> // 自绘数字键盘
|
|
14
15
|
* ```
|
|
15
16
|
*
|
|
16
17
|
* 与 antd Input 的对应:
|
|
@@ -18,6 +19,8 @@
|
|
|
18
19
|
* - disabled:对应 antd disabled(不可编辑 + 禁用样式)
|
|
19
20
|
* - multiline:对应 antd Input.TextArea
|
|
20
21
|
* - allowClear:对应 antd allowClear(非多行时显示清空按钮)
|
|
22
|
+
* - customKeyboard='numeric':盖住系统键盘,弹起自绘数字键盘(带"完成"按钮)
|
|
23
|
+
* 适合金额/数量等场景,外观风格统一;与 XNumberKeyboard 同源渲染
|
|
21
24
|
* - 其余 props(maxLength/keyboardType/secureTextEntry 等)原样透传给 TextInput
|
|
22
25
|
*
|
|
23
26
|
* 注意:value 是完全受控透传,不做任何 trim/transform —— 改写会打断
|
|
@@ -33,6 +36,14 @@ export interface XInputProps extends TextInputProps {
|
|
|
33
36
|
disabled?: boolean;
|
|
34
37
|
/** 有内容时显示清空按钮(antd allowClear;multiline 下不生效) */
|
|
35
38
|
allowClear?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* 自定义键盘:'numeric' 时盖住系统键盘,弹起自绘数字键盘
|
|
41
|
+
* (带"完成"工具栏,按键直接回写 value;与 XNumberKeyboard 同源)。
|
|
42
|
+
* 仅单行模式生效;与 multiline 互斥。
|
|
43
|
+
*/
|
|
44
|
+
customKeyboard?: 'numeric';
|
|
45
|
+
/** 自定义键盘的小数点(false 隐藏;true 显示,默认 true) */
|
|
46
|
+
customKeyboardPoint?: boolean;
|
|
36
47
|
style?: StyleProp<TextStyle>;
|
|
37
48
|
/** 外层容器样式(有 allowClear 按钮时想要撑满/贴边用) */
|
|
38
49
|
containerStyle?: StyleProp<ViewStyle>;
|
package/dist/XInput/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* <XInput value={name} onChangeText={setName} placeholder='请输入姓名' />
|
|
13
13
|
* <XInput multiline placeholder='请输入备注' />
|
|
14
14
|
* <XInput disabled value='只读' />
|
|
15
|
+
* <XInput customKeyboard='numeric' value={amount} onChangeText={setAmount} /> // 自绘数字键盘
|
|
15
16
|
* ```
|
|
16
17
|
*
|
|
17
18
|
* 与 antd Input 的对应:
|
|
@@ -19,17 +20,26 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
19
20
|
* - disabled:对应 antd disabled(不可编辑 + 禁用样式)
|
|
20
21
|
* - multiline:对应 antd Input.TextArea
|
|
21
22
|
* - allowClear:对应 antd allowClear(非多行时显示清空按钮)
|
|
23
|
+
* - customKeyboard='numeric':盖住系统键盘,弹起自绘数字键盘(带"完成"按钮)
|
|
24
|
+
* 适合金额/数量等场景,外观风格统一;与 XNumberKeyboard 同源渲染
|
|
22
25
|
* - 其余 props(maxLength/keyboardType/secureTextEntry 等)原样透传给 TextInput
|
|
23
26
|
*
|
|
24
27
|
* 注意:value 是完全受控透传,不做任何 trim/transform —— 改写会打断
|
|
25
28
|
* Android 中文输入法的合成过程(XForm 的同值守卫也依赖原样透传)。
|
|
26
29
|
*/
|
|
27
30
|
import { forwardRef, useEffect, useRef, useState } from 'react';
|
|
28
|
-
import { Pressable, StyleSheet, TextInput, View } from 'react-native';
|
|
31
|
+
import { Pressable, StyleSheet, TextInput, View, Text } from 'react-native';
|
|
32
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
29
33
|
import { useXTheme } from '../theme';
|
|
30
|
-
|
|
34
|
+
import { useXLocale } from '../XLocale';
|
|
35
|
+
import { XPullView } from '../XPullView';
|
|
36
|
+
import { XNumberKeyboard } from '../XNumberKeyboard';
|
|
37
|
+
export const XInput = forwardRef(function XInput({ value, onChangeText, multiline, disabled, allowClear, customKeyboard, customKeyboardPoint = true, style, containerStyle, editable, ...rest }, ref) {
|
|
31
38
|
const [focused, setFocused] = useState(false);
|
|
32
39
|
const isDisabled = disabled || editable === false;
|
|
40
|
+
const useCustomKb = customKeyboard === 'numeric' && !multiline;
|
|
41
|
+
/** 自定义键盘弹层是否展开 */
|
|
42
|
+
const [kbVisible, setKbVisible] = useState(false);
|
|
33
43
|
/**
|
|
34
44
|
* Android IME 防回抛镜像(关键):
|
|
35
45
|
* 受控模式下若把父级 store 的 value 每键原样回写给 TextInput,IME 合成会被
|
|
@@ -56,15 +66,58 @@ export const XInput = forwardRef(function XInput({ value, onChangeText, multilin
|
|
|
56
66
|
// 受控用法显示镜像;非受控(没传 value)不干预,TextInput 自己管
|
|
57
67
|
const displayValue = value !== undefined ? mirror : undefined;
|
|
58
68
|
const t = useXTheme();
|
|
69
|
+
const { t: i18n } = useXLocale();
|
|
70
|
+
const insets = useSafeAreaInsets();
|
|
59
71
|
const showClear = !!allowClear && !multiline && !isDisabled && !!displayValue;
|
|
60
72
|
/** 清空后把光标送回输入框,继续输入不用再点一次 */
|
|
61
73
|
const handleClear = () => {
|
|
62
74
|
reportedRef.current = '';
|
|
63
75
|
setMirror('');
|
|
64
76
|
onChangeText?.('');
|
|
65
|
-
|
|
77
|
+
if (!useCustomKb) {
|
|
78
|
+
ref && typeof ref === 'object' && ref.current?.focus();
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
setKbVisible(true);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/** 自定义键盘:按键 → 回写 value(带 maxLength 校验) */
|
|
85
|
+
const handleKeyPress = (key) => {
|
|
86
|
+
const prev = displayValue ?? '';
|
|
87
|
+
const maxLength = rest.maxLength;
|
|
88
|
+
if (typeof maxLength === 'number' && prev.length >= maxLength)
|
|
89
|
+
return;
|
|
90
|
+
// 小数点只允许一次、不允许开头
|
|
91
|
+
if (key === '.' && (prev.includes('.') || prev.length === 0))
|
|
92
|
+
return;
|
|
93
|
+
handleChange(prev + key);
|
|
94
|
+
};
|
|
95
|
+
/** 自定义键盘:退格 */
|
|
96
|
+
const handleBackspace = () => {
|
|
97
|
+
const prev = displayValue ?? '';
|
|
98
|
+
handleChange(prev.slice(0, -1));
|
|
66
99
|
};
|
|
67
|
-
|
|
100
|
+
// 自定义键盘模式下,"聚焦状态"由弹层是否展开驱动
|
|
101
|
+
const isFocused = useCustomKb ? kbVisible : focused;
|
|
102
|
+
return (_jsxs(View, { style: [styles.container, containerStyle], children: [useCustomKb ? (_jsx(Pressable, { onPress: () => !isDisabled && setKbVisible(true), disabled: isDisabled, children: ({ pressed }) => (_jsx(View, { style: [
|
|
103
|
+
styles.input,
|
|
104
|
+
{
|
|
105
|
+
borderColor: isFocused && !isDisabled ? t.colorPrimary : t.colorBorder,
|
|
106
|
+
backgroundColor: isDisabled
|
|
107
|
+
? t.colorBgContainerDisabled
|
|
108
|
+
: pressed
|
|
109
|
+
? t.colorBgLayout
|
|
110
|
+
: t.colorBgContainer,
|
|
111
|
+
},
|
|
112
|
+
], pointerEvents: 'none', children: _jsx(Text, { style: [
|
|
113
|
+
styles.kbText,
|
|
114
|
+
{
|
|
115
|
+
color: displayValue ? t.colorText : t.colorTextQuaternary,
|
|
116
|
+
},
|
|
117
|
+
typeof style === 'object' && !Array.isArray(style)
|
|
118
|
+
? style
|
|
119
|
+
: undefined,
|
|
120
|
+
], numberOfLines: 1, allowFontScaling: false, children: displayValue || rest.placeholder }) })) })) : (_jsx(TextInput, { ...rest, ref: ref, value: displayValue, onChangeText: handleChange, multiline: multiline, editable: !isDisabled, pointerEvents: isDisabled ? 'none' : undefined, style: [
|
|
68
121
|
styles.input,
|
|
69
122
|
{
|
|
70
123
|
borderColor: focused && !isDisabled ? t.colorPrimary : t.colorBorder,
|
|
@@ -83,7 +136,7 @@ export const XInput = forwardRef(function XInput({ value, onChangeText, multilin
|
|
|
83
136
|
}, onBlur: e => {
|
|
84
137
|
setFocused(false);
|
|
85
138
|
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 }] })] }) }))] }));
|
|
139
|
+
} })), 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 }] })] }) })), useCustomKb && (_jsx(XPullView, { visible: kbVisible, onClose: () => setKbVisible(false), side: 'bottom', overlayOpacity: 0.4, children: _jsxs(View, { style: [styles.kbPanel, { backgroundColor: t.colorBgContainer, paddingBottom: Math.max(insets.bottom, 10) }], children: [_jsxs(View, { style: [styles.kbToolbar, { borderBottomColor: t.colorSplit }], children: [_jsx(Text, { style: [styles.kbToolbarTitle, { color: t.colorTextSecondary }], numberOfLines: 1, children: rest.placeholder ?? i18n('pleaseInput') }), _jsx(Pressable, { onPress: () => setKbVisible(false), hitSlop: 8, children: _jsx(Text, { style: [styles.kbDone, { color: t.colorPrimary }], children: i18n('done') }) })] }), _jsx(XNumberKeyboard, { onKeyPress: handleKeyPress, onBackspace: handleBackspace, extraKey: customKeyboardPoint ? '.' : null })] }) }))] }));
|
|
87
140
|
});
|
|
88
141
|
/** antd Input.TextArea 的对应形态 */
|
|
89
142
|
export const XTextArea = (props) => _jsx(XInput, { multiline: true, ...props });
|
|
@@ -107,6 +160,32 @@ const styles = StyleSheet.create({
|
|
|
107
160
|
textAlignVertical: 'top',
|
|
108
161
|
paddingVertical: 10,
|
|
109
162
|
},
|
|
163
|
+
kbText: {
|
|
164
|
+
fontSize: 15,
|
|
165
|
+
paddingVertical: 2,
|
|
166
|
+
},
|
|
167
|
+
kbPanel: {
|
|
168
|
+
borderTopLeftRadius: 12,
|
|
169
|
+
borderTopRightRadius: 12,
|
|
170
|
+
overflow: 'hidden',
|
|
171
|
+
},
|
|
172
|
+
kbToolbar: {
|
|
173
|
+
flexDirection: 'row',
|
|
174
|
+
alignItems: 'center',
|
|
175
|
+
justifyContent: 'space-between',
|
|
176
|
+
paddingHorizontal: 16,
|
|
177
|
+
height: 44,
|
|
178
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
179
|
+
},
|
|
180
|
+
kbToolbarTitle: {
|
|
181
|
+
fontSize: 13,
|
|
182
|
+
flex: 1,
|
|
183
|
+
marginRight: 8,
|
|
184
|
+
},
|
|
185
|
+
kbDone: {
|
|
186
|
+
fontSize: 15,
|
|
187
|
+
fontWeight: '600',
|
|
188
|
+
},
|
|
110
189
|
clearBtn: {
|
|
111
190
|
position: 'absolute',
|
|
112
191
|
right: 8,
|
|
@@ -15,6 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
15
15
|
*/
|
|
16
16
|
import { useCallback, useMemo, useState } from 'react';
|
|
17
17
|
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
18
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
18
19
|
import { XPullView } from '../XPullView';
|
|
19
20
|
import { useXTheme } from '../theme';
|
|
20
21
|
import { useXLocale } from '../XLocale';
|
|
@@ -81,6 +82,7 @@ export function XLicensePlateKeyboard({ phase, onInput, onBackspace, allowedKeys
|
|
|
81
82
|
export function XLicensePlate({ value = '', onChange, length = 7, disabled = false, placeholder, style }) {
|
|
82
83
|
const t = useXTheme();
|
|
83
84
|
const { t: i18n } = useXLocale();
|
|
85
|
+
const insets = useSafeAreaInsets();
|
|
84
86
|
const [visible, setVisible] = useState(false);
|
|
85
87
|
/** 当前键盘阶段 */
|
|
86
88
|
const phase = value.length === 0 ? 'province' : value.length === 1 ? 'city' : 'alnum';
|
|
@@ -112,7 +114,7 @@ export function XLicensePlate({ value = '', onChange, length = 7, disabled = fal
|
|
|
112
114
|
isNewEnergyTail && { borderColor: t.colorSuccess, borderWidth: 1.5 },
|
|
113
115
|
], children: filled && (_jsx(Text, { style: [styles.cellText, { color: t.colorText }], allowFontScaling: false, children: value[i] })) }, i));
|
|
114
116
|
});
|
|
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 })] }) })] }));
|
|
117
|
+
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, paddingBottom: Math.max(insets.bottom, 10) }], 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
118
|
}
|
|
117
119
|
const styles = StyleSheet.create({
|
|
118
120
|
keyboard: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { XConfirmForm } from '../XConfirmForm';
|
|
3
3
|
import { XTopView } from '../XTopView';
|
|
4
|
+
import { XActionSheetGlobalOverlay } from '../XActionSheet/global';
|
|
4
5
|
export const XPopupProvider = ({ children }) => {
|
|
5
|
-
return (_jsxs(_Fragment, { children: [children, _jsx(XConfirmForm, {}), _jsx(XTopView, {})] }));
|
|
6
|
+
return (_jsxs(_Fragment, { children: [children, _jsx(XConfirmForm, {}), _jsx(XActionSheetGlobalOverlay, {}), _jsx(XTopView, {})] }));
|
|
6
7
|
};
|