@autional/ui 0.1.0-rc.47 → 0.1.0-rc.49
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/package.json +3 -3
- package/src/i18n/resources.ts +15 -0
- package/src/molecules/Alert.tsx +9 -2
- package/src/molecules/AppShell.tsx +1 -1
- package/src/molecules/ConfirmDialog.tsx +25 -7
- package/src/molecules/EmptyState.tsx +13 -4
- package/src/molecules/LoadingScreen.tsx +14 -2
- package/src/molecules/Modal.tsx +13 -2
- package/src/molecules/Toast.tsx +14 -6
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autional/ui",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
4
|
-
"description": "Autional shared React component library (atoms + molecules)
|
|
3
|
+
"version": "0.1.0-rc.49",
|
|
4
|
+
"description": "Autional shared React component library (atoms + molecules) …the canonical home of the component layer",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"module": "./src/index.ts",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"react-i18next": "17.0.7",
|
|
83
83
|
"typescript": "5.8.3",
|
|
84
84
|
"vitest": "3.2.0",
|
|
85
|
-
"@autional/tokens": "0.1.0-rc.
|
|
85
|
+
"@autional/tokens": "0.1.0-rc.17"
|
|
86
86
|
},
|
|
87
87
|
"scripts": {
|
|
88
88
|
"typecheck": "tsc --noEmit",
|
package/src/i18n/resources.ts
CHANGED
|
@@ -32,6 +32,14 @@ export const uiI18nResources: Record<'zh-CN' | 'en-US', Record<string, string>>
|
|
|
32
32
|
// ThemeToggle 默认文案(UP-04:原为组件内硬编码中文,EN 站点顶栏出现中文标签)
|
|
33
33
|
'theme.toggleDark': '切换到深色模式',
|
|
34
34
|
'theme.toggleLight': '切换到浅色模式',
|
|
35
|
+
// 组件默认文案(语言问题审计 2026-10-13):原为组件内硬编码中文默认 ——
|
|
36
|
+
// EN 站裸露中文(trust 加载态「加载中...」线上实测)。文案逐字保留原硬编码值。
|
|
37
|
+
'loadingScreen.message': '加载中...',
|
|
38
|
+
'emptyState.title': '暂无数据',
|
|
39
|
+
'emptyState.description': '当前没有可显示的内容。',
|
|
40
|
+
'confirmDialog.confirm': '确认',
|
|
41
|
+
'confirmDialog.cancel': '取消',
|
|
42
|
+
'alert.close': '关闭',
|
|
35
43
|
},
|
|
36
44
|
'en-US': {
|
|
37
45
|
'portal.switcher': 'Switch portal',
|
|
@@ -52,5 +60,12 @@ export const uiI18nResources: Record<'zh-CN' | 'en-US', Record<string, string>>
|
|
|
52
60
|
'pageStatus.retry': 'Retry',
|
|
53
61
|
'theme.toggleDark': 'Switch to dark mode',
|
|
54
62
|
'theme.toggleLight': 'Switch to light mode',
|
|
63
|
+
// EN 词汇对齐舰队既有口径(Loading... / No data / Confirm / Cancel / Close)
|
|
64
|
+
'loadingScreen.message': 'Loading...',
|
|
65
|
+
'emptyState.title': 'No data',
|
|
66
|
+
'emptyState.description': 'Nothing to display yet.',
|
|
67
|
+
'confirmDialog.confirm': 'Confirm',
|
|
68
|
+
'confirmDialog.cancel': 'Cancel',
|
|
69
|
+
'alert.close': 'Close',
|
|
55
70
|
},
|
|
56
71
|
};
|
package/src/molecules/Alert.tsx
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { AlertTriangle, CheckCircle2, Info, X, XCircle } from 'lucide-react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
5
6
|
import { SOFT_BORDER, SOFT_TEXT, type SemanticVariant } from '../internal/semantic-styles';
|
|
7
|
+
import { UI_I18N_NS, uiText } from '../i18n';
|
|
6
8
|
|
|
7
9
|
// Alert —— 区块级提示(着色块)。
|
|
8
10
|
//
|
|
@@ -67,9 +69,14 @@ export const Alert = React.memo(function Alert({
|
|
|
67
69
|
action,
|
|
68
70
|
closable = false,
|
|
69
71
|
onClose,
|
|
70
|
-
closeLabel
|
|
72
|
+
closeLabel,
|
|
71
73
|
className = ''
|
|
72
74
|
}: AlertProps) {
|
|
75
|
+
const { t, i18n } = useTranslation();
|
|
76
|
+
// 文案解析链与 Modal / ThemeToggle 同构(语言问题审计 2026-10-13):原为硬编码中文默认。
|
|
77
|
+
const closeLabelText =
|
|
78
|
+
closeLabel ??
|
|
79
|
+
t('alert.close', { ns: UI_I18N_NS, defaultValue: uiText(i18n.language, 'alert.close') ?? '关闭' });
|
|
73
80
|
const glyph = icon === null ? null : (icon ?? DEFAULT_ICONS[variant]);
|
|
74
81
|
return (
|
|
75
82
|
// role="alert" 与 antd 的 Alert 一致:这些块绝大多数是**动作之后**才出现的(结果/失败原因),
|
|
@@ -89,7 +96,7 @@ export const Alert = React.memo(function Alert({
|
|
|
89
96
|
<button
|
|
90
97
|
type="button"
|
|
91
98
|
onClick={onClose}
|
|
92
|
-
aria-label={
|
|
99
|
+
aria-label={closeLabelText}
|
|
93
100
|
className="-mr-1 -mt-1 shrink-0 rounded-md p-1 opacity-70 transition-opacity hover:opacity-100 focus-visible:outline focus-visible:outline-2"
|
|
94
101
|
>
|
|
95
102
|
<X className="h-4 w-4" />
|
|
@@ -69,7 +69,7 @@ export const AppShell = React.memo(function AppShell({
|
|
|
69
69
|
{/* 移动端遮罩:点它关闭抽屉。桌面端不渲染(lg:hidden)。 */}
|
|
70
70
|
{mobileOpen && (
|
|
71
71
|
<div
|
|
72
|
-
className="fixed inset-0 z-40 bg-
|
|
72
|
+
className="fixed inset-0 z-40 bg-scrim/30 lg:hidden"
|
|
73
73
|
onClick={onMobileClose}
|
|
74
74
|
aria-hidden="true"
|
|
75
75
|
/>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import React, { useCallback, useEffect } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
2
3
|
import { AlertTriangle, X } from 'lucide-react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { UI_I18N_NS, uiText } from '../i18n';
|
|
3
6
|
|
|
4
7
|
interface ConfirmDialogProps {
|
|
5
8
|
open: boolean;
|
|
@@ -31,12 +34,22 @@ export const ConfirmDialog = React.memo(function ConfirmDialog({
|
|
|
31
34
|
open,
|
|
32
35
|
title,
|
|
33
36
|
description,
|
|
34
|
-
confirmText
|
|
35
|
-
cancelText
|
|
37
|
+
confirmText,
|
|
38
|
+
cancelText,
|
|
36
39
|
variant = 'neutral',
|
|
37
40
|
onConfirm,
|
|
38
41
|
onCancel,
|
|
39
42
|
}: ConfirmDialogProps) {
|
|
43
|
+
const { t, i18n } = useTranslation();
|
|
44
|
+
// 文案解析链与 Modal / ThemeToggle 同构(语言问题审计 2026-10-13):原为硬编码
|
|
45
|
+
// 中文默认('确认' / '取消' / X 按钮 '关闭')—— user 门户 5 处裸调用 EN 站裸露中文。
|
|
46
|
+
const resolve = (key: string, fallback: string): string =>
|
|
47
|
+
t(key, { ns: UI_I18N_NS, defaultValue: uiText(i18n.language, key) ?? fallback });
|
|
48
|
+
const confirmLabel = confirmText ?? resolve('confirmDialog.confirm', '确认');
|
|
49
|
+
const cancelLabel = cancelText ?? resolve('confirmDialog.cancel', '取消');
|
|
50
|
+
// X 按钮与 Modal 同键(同为对话框关闭,文案一致)
|
|
51
|
+
const closeLabel = resolve('modal.close', '关闭');
|
|
52
|
+
|
|
40
53
|
const handleKeyDown = useCallback(
|
|
41
54
|
(e: KeyboardEvent) => {
|
|
42
55
|
if (e.key === 'Escape') onCancel();
|
|
@@ -59,14 +72,16 @@ export const ConfirmDialog = React.memo(function ConfirmDialog({
|
|
|
59
72
|
|
|
60
73
|
const style = variantStyles[variant];
|
|
61
74
|
|
|
62
|
-
|
|
75
|
+
// 与 Modal 同因同解(第 63 轮补·四):portal 到 body,否则「有 backdrop-filter 的祖先」
|
|
76
|
+
// 会成为 fixed 后代的包含块,遮罩只覆盖那一小块。
|
|
77
|
+
const content = (
|
|
63
78
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
64
|
-
<div className="absolute inset-0 bg-
|
|
79
|
+
<div className="absolute inset-0 bg-scrim/50 backdrop-blur-sm" onClick={onCancel} />
|
|
65
80
|
<div className="relative w-full max-w-md rounded-xl border border-[var(--color-border-subtle)] bg-[var(--color-bg-surface)] p-6 shadow-brand">
|
|
66
81
|
<button
|
|
67
82
|
onClick={onCancel}
|
|
68
83
|
className="absolute right-4 top-4 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
|
69
|
-
aria-label=
|
|
84
|
+
aria-label={closeLabel}
|
|
70
85
|
>
|
|
71
86
|
<X className="h-4 w-4" />
|
|
72
87
|
</button>
|
|
@@ -86,16 +101,19 @@ export const ConfirmDialog = React.memo(function ConfirmDialog({
|
|
|
86
101
|
onClick={onCancel}
|
|
87
102
|
className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-bg-surface)] px-4 py-2 text-sm font-medium text-[var(--color-text-primary)] transition-colors hover:bg-[var(--color-bg-muted)]"
|
|
88
103
|
>
|
|
89
|
-
{
|
|
104
|
+
{cancelLabel}
|
|
90
105
|
</button>
|
|
91
106
|
<button
|
|
92
107
|
onClick={onConfirm}
|
|
93
108
|
className={`rounded-md px-4 py-2 text-sm font-medium transition-colors ${style.confirm}`}
|
|
94
109
|
>
|
|
95
|
-
{
|
|
110
|
+
{confirmLabel}
|
|
96
111
|
</button>
|
|
97
112
|
</div>
|
|
98
113
|
</div>
|
|
99
114
|
</div>
|
|
100
115
|
);
|
|
116
|
+
|
|
117
|
+
if (typeof document === 'undefined') return content;
|
|
118
|
+
return createPortal(content, document.body);
|
|
101
119
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Inbox } from 'lucide-react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { UI_I18N_NS, uiText } from '../i18n';
|
|
3
5
|
|
|
4
6
|
interface EmptyStateProps {
|
|
5
7
|
title?: string;
|
|
@@ -8,17 +10,24 @@ interface EmptyStateProps {
|
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export const EmptyState = React.memo(function EmptyState({
|
|
11
|
-
title
|
|
12
|
-
description
|
|
13
|
+
title,
|
|
14
|
+
description,
|
|
13
15
|
icon,
|
|
14
16
|
}: EmptyStateProps) {
|
|
17
|
+
const { t, i18n } = useTranslation();
|
|
18
|
+
// 文案解析链与 Modal / ThemeToggle 同构(语言问题审计 2026-10-13):原为硬编码中文默认。
|
|
19
|
+
const resolve = (key: string, fallback: string): string =>
|
|
20
|
+
t(key, { ns: UI_I18N_NS, defaultValue: uiText(i18n.language, key) ?? fallback });
|
|
21
|
+
const titleText = title ?? resolve('emptyState.title', '暂无数据');
|
|
22
|
+
const descriptionText = description ?? resolve('emptyState.description', '当前没有可显示的内容。');
|
|
23
|
+
|
|
15
24
|
return (
|
|
16
25
|
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-[var(--color-border-subtle)] bg-[var(--color-bg-muted)] px-4 py-12 text-center">
|
|
17
26
|
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-bg-muted)] text-[var(--color-text-muted)]">
|
|
18
27
|
{icon ?? <Inbox className="h-6 w-6" />}
|
|
19
28
|
</div>
|
|
20
|
-
<h3 className="mt-4 text-base font-semibold text-[var(--color-text-primary)]">{
|
|
21
|
-
<p className="mt-1 text-sm text-[var(--color-text-secondary)]">{
|
|
29
|
+
<h3 className="mt-4 text-base font-semibold text-[var(--color-text-primary)]">{titleText}</h3>
|
|
30
|
+
<p className="mt-1 text-sm text-[var(--color-text-secondary)]">{descriptionText}</p>
|
|
22
31
|
</div>
|
|
23
32
|
);
|
|
24
33
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Loader2 } from 'lucide-react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { UI_I18N_NS, uiText } from '../i18n';
|
|
2
4
|
|
|
3
5
|
export interface LoadingScreenProps {
|
|
4
6
|
message?: string;
|
|
@@ -7,14 +9,24 @@ export interface LoadingScreenProps {
|
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export function LoadingScreen({
|
|
10
|
-
message
|
|
12
|
+
message,
|
|
11
13
|
fullScreen = false,
|
|
12
14
|
className = '',
|
|
13
15
|
}: LoadingScreenProps) {
|
|
16
|
+
const { t, i18n } = useTranslation();
|
|
17
|
+
// 文案解析链与 Modal / ThemeToggle 同构(语言问题审计 2026-10-13):
|
|
18
|
+
// 原为硬编码中文默认,EN 站加载态裸露「加载中...」(trust 线上实测)。
|
|
19
|
+
const text =
|
|
20
|
+
message ??
|
|
21
|
+
t('loadingScreen.message', {
|
|
22
|
+
ns: UI_I18N_NS,
|
|
23
|
+
defaultValue: uiText(i18n.language, 'loadingScreen.message') ?? '加载中...',
|
|
24
|
+
});
|
|
25
|
+
|
|
14
26
|
const content = (
|
|
15
27
|
<div className="flex flex-col items-center justify-center gap-3">
|
|
16
28
|
<Loader2 size={fullScreen ? 32 : 24} className="animate-spin text-[var(--color-brand)]" />
|
|
17
|
-
<p className="text-sm text-[var(--color-text-secondary)]">{
|
|
29
|
+
<p className="text-sm text-[var(--color-text-secondary)]">{text}</p>
|
|
18
30
|
</div>
|
|
19
31
|
);
|
|
20
32
|
|
package/src/molecules/Modal.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useId, useRef } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
2
3
|
import { X } from 'lucide-react';
|
|
3
4
|
import { useTranslation } from 'react-i18next';
|
|
4
5
|
import { UI_I18N_NS, uiText } from '../i18n';
|
|
@@ -128,8 +129,14 @@ export const Modal = React.memo(function Modal({
|
|
|
128
129
|
|
|
129
130
|
if (!open) return null;
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
// ⚠ 必须 portal 到 document.body(第 63 轮补·四实测):
|
|
133
|
+
// `position: fixed` 的包含块**不是视口**,而是最近的「有 transform / filter / backdrop-filter /
|
|
134
|
+
// will-change / contain」的祖先。站点的顶栏内层写了 `backdrop-blur-xl`,于是**在顶栏里打开的浮层**
|
|
135
|
+
// 的 inset-0 只覆盖顶栏那一小块 —— 实测 www.autional.cn 的搜索面板:遮罩是 976x135 的一条,
|
|
136
|
+
// 而不是整屏(用户的原话是「弹出一个黑色遮罩,怪怪的」)。
|
|
137
|
+
// 浮层属于顶层,不属于触发它的那棵子树;portal 是这条规则的结构性实现。
|
|
138
|
+
const content = (
|
|
139
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-scrim/50 p-4">
|
|
133
140
|
<div className="absolute inset-0" onClick={onClose} />
|
|
134
141
|
<div
|
|
135
142
|
ref={panelRef}
|
|
@@ -175,4 +182,8 @@ export const Modal = React.memo(function Modal({
|
|
|
175
182
|
</div>
|
|
176
183
|
</div>
|
|
177
184
|
);
|
|
185
|
+
|
|
186
|
+
// SSR / 测试环境没有 document 时退回就地渲染(portal 的目标不存在)
|
|
187
|
+
if (typeof document === 'undefined') return content;
|
|
188
|
+
return createPortal(content, document.body);
|
|
178
189
|
});
|
package/src/molecules/Toast.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type ReactNode,
|
|
9
9
|
} from 'react';
|
|
10
10
|
import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react';
|
|
11
|
+
import { SOFT_BORDER, SOFT_TEXT } from '../internal/semantic-styles';
|
|
11
12
|
|
|
12
13
|
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
|
13
14
|
|
|
@@ -42,11 +43,18 @@ const ICONS: Record<ToastType, typeof CheckCircle> = {
|
|
|
42
43
|
info: Info,
|
|
43
44
|
};
|
|
44
45
|
|
|
46
|
+
// 第 63 轮补·四:改用与 Alert / StatusBadge **同一份**语义配对(internal/semantic-styles.ts 的 SOFT_TEXT)。
|
|
47
|
+
// 此前是「实底 + 白字」,实测对白字的对比度 ≈ success 2.6 / error 3.4 / warning 2.2 : 1 —— 三档都不达 AA 4.5:1,
|
|
48
|
+
// 而 Toast 不在任何对比度目标页里,所以这条判据从未扫到它(「不是判据错了,是判据没扫到」)。
|
|
49
|
+
// 换成 -soft / -text 配对后,对比度是设计系统建这一份配对时就验过的:
|
|
50
|
+
// success 5.51 / warning 4.85 / danger 4.65 / info 6.70 —— 与 Alert 同一个来源,不再有第二套。
|
|
51
|
+
// 底 + 字 + 边框三件都取自那一份配对(Alert / StatusBadge 同源):软底若不带边框,
|
|
52
|
+
// 贴在白卡上分不出边界 —— Alert 早就是这么做的,Toast 直接跟它对齐。
|
|
45
53
|
const STYLES: Record<ToastType, string> = {
|
|
46
|
-
success:
|
|
47
|
-
error:
|
|
48
|
-
warning:
|
|
49
|
-
info: '
|
|
54
|
+
success: SOFT_TEXT.success + ' ' + SOFT_BORDER.success,
|
|
55
|
+
error: SOFT_TEXT.danger + ' ' + SOFT_BORDER.danger,
|
|
56
|
+
warning: SOFT_TEXT.warning + ' ' + SOFT_BORDER.warning,
|
|
57
|
+
info: SOFT_TEXT.info + ' ' + SOFT_BORDER.info,
|
|
50
58
|
};
|
|
51
59
|
|
|
52
60
|
export function ToastProvider({ children }: { children: ReactNode }) {
|
|
@@ -87,13 +95,13 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
|
|
87
95
|
return (
|
|
88
96
|
<div
|
|
89
97
|
key={toast.id}
|
|
90
|
-
className={`pointer-events-auto flex items-center gap-2 rounded-xl px-4 py-2.5 text-sm font-medium shadow-
|
|
98
|
+
className={`pointer-events-auto flex items-center gap-2 rounded-xl border px-4 py-2.5 text-sm font-medium shadow-card ${STYLES[toast.type]}`}
|
|
91
99
|
>
|
|
92
100
|
<Icon className="h-4 w-4 shrink-0" />
|
|
93
101
|
<span className="flex-1">{toast.message}</span>
|
|
94
102
|
<button
|
|
95
103
|
onClick={() => removeToast(toast.id)}
|
|
96
|
-
className="ml-1 shrink-0 rounded-xs p-0.5 opacity-70 hover:
|
|
104
|
+
className="ml-1 shrink-0 rounded-xs p-0.5 opacity-70 hover:opacity-100"
|
|
97
105
|
>
|
|
98
106
|
<X className="h-3.5 w-3.5" />
|
|
99
107
|
</button>
|