@q1travel/design-system 0.7.1 → 0.7.3
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/bin/q1-design.mjs +108 -15
- package/contracts/button-api-compatibility.md +29 -3
- package/contracts/overlay-foundation.md +219 -0
- package/contracts/status-chip-badge-migration.md +69 -0
- package/dist-pkg/design-system.d.ts +17 -7
- package/dist-pkg/design-system.js +3402 -3050
- package/dist-pkg/style.css +256 -47
- package/dist-pkg/tokens.css +4 -0
- package/package.json +2 -2
package/bin/q1-design.mjs
CHANGED
|
@@ -12,13 +12,13 @@ import {
|
|
|
12
12
|
mkdirSync,
|
|
13
13
|
readFileSync,
|
|
14
14
|
readdirSync,
|
|
15
|
+
realpathSync,
|
|
15
16
|
renameSync,
|
|
16
17
|
rmSync,
|
|
17
|
-
statSync,
|
|
18
18
|
writeFileSync,
|
|
19
19
|
} from 'node:fs';
|
|
20
20
|
import { createHash } from 'node:crypto';
|
|
21
|
-
import { dirname, extname, join, relative, resolve, sep } from 'node:path';
|
|
21
|
+
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
23
|
|
|
24
24
|
const PACKAGE_NAME = '@q1travel/design-system';
|
|
@@ -55,15 +55,47 @@ function toPosixPath(pathValue) {
|
|
|
55
55
|
return pathValue.split(sep).join('/');
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function safeRealpath(pathValue) {
|
|
59
|
+
try {
|
|
60
|
+
return realpathSync(pathValue);
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isPathInside(rootDir, pathValue) {
|
|
67
|
+
const relativePath = relative(rootDir, pathValue);
|
|
68
|
+
return relativePath === '' ||
|
|
69
|
+
(relativePath !== '..' &&
|
|
70
|
+
!relativePath.startsWith(`..${sep}`) &&
|
|
71
|
+
!isAbsolute(relativePath));
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
function listFilesRecursively(rootDir) {
|
|
59
75
|
const relativeFilePaths = [];
|
|
76
|
+
const rootRealPath = safeRealpath(rootDir);
|
|
77
|
+
const visitedDirRealPaths = new Set();
|
|
60
78
|
|
|
61
79
|
function walk(currentDir) {
|
|
80
|
+
const currentDirRealPath = safeRealpath(currentDir);
|
|
81
|
+
if (
|
|
82
|
+
!rootRealPath ||
|
|
83
|
+
!currentDirRealPath ||
|
|
84
|
+
!isPathInside(rootRealPath, currentDirRealPath) ||
|
|
85
|
+
visitedDirRealPaths.has(currentDirRealPath)
|
|
86
|
+
) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
visitedDirRealPaths.add(currentDirRealPath);
|
|
90
|
+
|
|
62
91
|
for (const entryName of readdirSync(currentDir).sort()) {
|
|
63
92
|
const entryPath = join(currentDir, entryName);
|
|
64
|
-
|
|
93
|
+
const entryStats = lstatSync(entryPath);
|
|
94
|
+
if (entryStats.isSymbolicLink()) {
|
|
95
|
+
relativeFilePaths.push(toPosixPath(relative(rootDir, entryPath)));
|
|
96
|
+
} else if (entryStats.isDirectory()) {
|
|
65
97
|
walk(entryPath);
|
|
66
|
-
} else {
|
|
98
|
+
} else if (entryStats.isFile()) {
|
|
67
99
|
relativeFilePaths.push(toPosixPath(relative(rootDir, entryPath)));
|
|
68
100
|
}
|
|
69
101
|
}
|
|
@@ -75,8 +107,25 @@ function listFilesRecursively(rootDir) {
|
|
|
75
107
|
return relativeFilePaths;
|
|
76
108
|
}
|
|
77
109
|
|
|
78
|
-
function computeFileHash(filePath) {
|
|
79
|
-
|
|
110
|
+
function computeFileHash(filePath, rootDir) {
|
|
111
|
+
const stats = lstatSync(filePath);
|
|
112
|
+
const relativeFilePath = rootDir
|
|
113
|
+
? toPosixPath(relative(rootDir, filePath))
|
|
114
|
+
: filePath;
|
|
115
|
+
if (stats.isSymbolicLink()) {
|
|
116
|
+
throw new Error(`拒绝哈希符号链接: ${relativeFilePath}`);
|
|
117
|
+
}
|
|
118
|
+
if (!stats.isFile()) {
|
|
119
|
+
throw new Error(`拒绝哈希非文件路径: ${relativeFilePath}`);
|
|
120
|
+
}
|
|
121
|
+
const realFilePath = realpathSync(filePath);
|
|
122
|
+
if (rootDir) {
|
|
123
|
+
const rootRealPath = realpathSync(rootDir);
|
|
124
|
+
if (!isPathInside(rootRealPath, realFilePath)) {
|
|
125
|
+
throw new Error(`拒绝哈希项目根外文件: ${relativeFilePath}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return createHash('sha256').update(readFileSync(realFilePath)).digest('hex');
|
|
80
129
|
}
|
|
81
130
|
|
|
82
131
|
function assertNotSymlink(pathValue) {
|
|
@@ -157,6 +206,7 @@ function runSync(consumerRootDir) {
|
|
|
157
206
|
for (const relativeFilePath of listFilesRecursively(tempDir)) {
|
|
158
207
|
fileHashes[relativeFilePath] = computeFileHash(
|
|
159
208
|
join(tempDir, relativeFilePath),
|
|
209
|
+
tempDir,
|
|
160
210
|
);
|
|
161
211
|
}
|
|
162
212
|
|
|
@@ -196,12 +246,14 @@ function computeExpectedFileHashes(packageRootDir) {
|
|
|
196
246
|
for (const relativeFilePath of listFilesRecursively(skillSourceDir)) {
|
|
197
247
|
expectedFileHashes[relativeFilePath] = computeFileHash(
|
|
198
248
|
join(skillSourceDir, relativeFilePath),
|
|
249
|
+
skillSourceDir,
|
|
199
250
|
);
|
|
200
251
|
}
|
|
201
252
|
const contractsSourceDir = join(packageRootDir, CONTRACTS_SOURCE_SUBPATH);
|
|
202
253
|
for (const relativeFilePath of listFilesRecursively(contractsSourceDir)) {
|
|
203
254
|
expectedFileHashes[`contracts/${relativeFilePath}`] = computeFileHash(
|
|
204
255
|
join(contractsSourceDir, relativeFilePath),
|
|
256
|
+
contractsSourceDir,
|
|
205
257
|
);
|
|
206
258
|
}
|
|
207
259
|
return expectedFileHashes;
|
|
@@ -233,7 +285,14 @@ function runVerify(consumerRootDir) {
|
|
|
233
285
|
process.exit(1);
|
|
234
286
|
}
|
|
235
287
|
|
|
236
|
-
|
|
288
|
+
let expectedFileHashes;
|
|
289
|
+
try {
|
|
290
|
+
expectedFileHashes = computeExpectedFileHashes(packageRootDir);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error('[q1-design] verify ✗');
|
|
293
|
+
console.error(` - 安装包文件不可校验: ${error.message}`);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
237
296
|
for (const [relativeFilePath, expectedHash] of Object.entries(
|
|
238
297
|
expectedFileHashes,
|
|
239
298
|
)) {
|
|
@@ -242,7 +301,13 @@ function runVerify(consumerRootDir) {
|
|
|
242
301
|
problems.push(`缺文件: ${relativeFilePath}`);
|
|
243
302
|
continue;
|
|
244
303
|
}
|
|
245
|
-
|
|
304
|
+
let actualHash;
|
|
305
|
+
try {
|
|
306
|
+
actualHash = computeFileHash(filePath, skillCacheDir);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
problems.push(`${relativeFilePath}: ${error.message}`);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
246
311
|
if (actualHash !== expectedHash) {
|
|
247
312
|
problems.push(`与安装包不一致(疑似手改): ${relativeFilePath}`);
|
|
248
313
|
}
|
|
@@ -276,12 +341,30 @@ function runVerify(consumerRootDir) {
|
|
|
276
341
|
console.log('[q1-design] verify ✓');
|
|
277
342
|
}
|
|
278
343
|
|
|
279
|
-
function walkForLint(dirPath, targetFiles) {
|
|
344
|
+
function walkForLint(dirPath, targetFiles, baseRealPath, visitedDirRealPaths) {
|
|
345
|
+
const dirRealPath = safeRealpath(dirPath);
|
|
346
|
+
if (
|
|
347
|
+
!dirRealPath ||
|
|
348
|
+
!isPathInside(baseRealPath, dirRealPath) ||
|
|
349
|
+
visitedDirRealPaths.has(dirRealPath)
|
|
350
|
+
) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
visitedDirRealPaths.add(dirRealPath);
|
|
354
|
+
|
|
280
355
|
for (const entryName of readdirSync(dirPath).sort()) {
|
|
281
356
|
const entryPath = join(dirPath, entryName);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
357
|
+
const entryStats = lstatSync(entryPath);
|
|
358
|
+
if (entryStats.isSymbolicLink()) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const entryRealPath = safeRealpath(entryPath);
|
|
362
|
+
if (!entryRealPath || !isPathInside(baseRealPath, entryRealPath)) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (entryStats.isDirectory()) {
|
|
366
|
+
walkForLint(entryPath, targetFiles, baseRealPath, visitedDirRealPaths);
|
|
367
|
+
} else if (entryStats.isFile() && LINT_EXTENSIONS.has(extname(entryPath))) {
|
|
285
368
|
targetFiles.push(entryPath);
|
|
286
369
|
}
|
|
287
370
|
}
|
|
@@ -289,15 +372,25 @@ function walkForLint(dirPath, targetFiles) {
|
|
|
289
372
|
|
|
290
373
|
function collectLintTargetFiles(inputPaths, baseDir) {
|
|
291
374
|
const targetFiles = [];
|
|
375
|
+
const baseRealPath = realpathSync(baseDir);
|
|
376
|
+
const visitedDirRealPaths = new Set();
|
|
292
377
|
for (const inputPath of inputPaths) {
|
|
293
378
|
const absolutePath = resolve(baseDir, inputPath);
|
|
294
379
|
if (!existsSync(absolutePath)) {
|
|
295
380
|
console.error(`[q1-design] 路径不存在: ${inputPath}`);
|
|
296
381
|
process.exit(1);
|
|
297
382
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
383
|
+
const targetStats = lstatSync(absolutePath);
|
|
384
|
+
if (targetStats.isSymbolicLink()) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const targetRealPath = safeRealpath(absolutePath);
|
|
388
|
+
if (!targetRealPath || !isPathInside(baseRealPath, targetRealPath)) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (targetStats.isDirectory()) {
|
|
392
|
+
walkForLint(absolutePath, targetFiles, baseRealPath, visitedDirRealPaths);
|
|
393
|
+
} else if (targetStats.isFile() && LINT_EXTENSIONS.has(extname(absolutePath))) {
|
|
301
394
|
targetFiles.push(absolutePath);
|
|
302
395
|
}
|
|
303
396
|
}
|
|
@@ -26,12 +26,37 @@ IconButton requires either `aria-label` or the deprecated `ariaLabel` at the Typ
|
|
|
26
26
|
|
|
27
27
|
Button and IconButton accept native button props, ARIA attributes, `data-*` attributes, and DOM event handlers. `data-*` values accept React-renderable scalar metadata (`string`, `number`, `boolean`, `null`, or `undefined`); `null` and `undefined` omit the attribute. They forward those props to the real `<button>` instead of a wrapper so menu semantics, roving focus, hover, and pointer-down behavior remain attached to the interactive element.
|
|
28
28
|
|
|
29
|
-
Package-owned semantics are applied after native props. `type`, `className`, `disabled`, loading `aria-disabled`/`tabIndex`, the resolved accessible name, `ref`, and the loading click
|
|
29
|
+
Package-owned semantics are applied after native props. `type`, `className`, `disabled`, busy/loading `aria-disabled`/`aria-busy`/`tabIndex`, the resolved accessible name, `ref`, and the busy/loading click guards therefore keep their documented behavior. Outside busy/loading, a consumer-provided `tabIndex` or `aria-disabled` remains intact. The canonical native `aria-keyshortcuts` value wins over the compatibility `ariaKeyshortcuts` field by nullish precedence.
|
|
30
|
+
|
|
31
|
+
## Loading and Busy States
|
|
32
|
+
|
|
33
|
+
`loading` and `busy` are intentionally different public states:
|
|
34
|
+
|
|
35
|
+
| State | Button | ButtonLink | Label and status |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| `loading` | Keeps the existing `aria-disabled="true"` and `tabIndex=0` focusable contract; does not add `aria-busy` or native `disabled` unless `disabled` was also passed. | Removes `href`, keeps `aria-disabled="true"` and `tabIndex=0` so the loading state remains discoverable. | Keeps the original visible children and announces `loadingLabel` through the existing internal `role="status"` spinner. |
|
|
38
|
+
| `busy` | Uses native `disabled`, sets `aria-busy="true"`, and blocks clicks. | Removes `href`, sets `aria-disabled="true"`, `aria-busy="true"`, `tabIndex=-1`, and blocks clicks. | Shows one spinner. A non-empty `busyLabel` replaces the visible label without replacing the stable accessible name; a separate polite `role="status"` announces `busyLabel`, then `loadingLabel`, then `处理中…`. |
|
|
39
|
+
| `busy + loading` | Busy wins; one spinner and the busy interaction contract apply. | Busy wins; the link is not navigable or focusable. | `busyLabel` wins for visible/status text; `loadingLabel` is only a fallback when busyLabel is empty. |
|
|
40
|
+
|
|
41
|
+
`busyLabel` is an optional string. An empty string behaves as absent and never disables repeat-submission prevention. The original children remain the accessible-name source (and an explicit `aria-label` remains unchanged); the visible busy label is a separate decorative overlay, while the live status node is outside the button/anchor name calculation. The label slot reserves geometry for both values. Under `prefers-reduced-motion: reduce`, the spinner remains present but static; no rotation, pulse, scale, or spring animation is required to understand the state.
|
|
42
|
+
|
|
43
|
+
## Web Adapter Seam (`q1travel-web` #702)
|
|
44
|
+
|
|
45
|
+
The package deliberately does not add product-specific `quiet`, `page`, or React Router props. The q1travel-web adapter tracked by #702 owns these measurable consumer rules:
|
|
46
|
+
|
|
47
|
+
| Web consumer contract | Package base | Adapter acceptance rule |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `variant="quiet"` | `variant="ghost"` for structure and behavior only | Preserve the existing subtle fill, border, hover/focus, disabled states, and dark-theme tokens; do not present transparent package ghost as an equivalent visual migration. |
|
|
50
|
+
| `size="page"` | `size="md"` for structure and behavior only | Preserve `36px` min-height, existing padding, font size, and minimum width; never map it to package `lg` (`48px`). |
|
|
51
|
+
| `Button as={Link} to={...}` | `ButtonLink href={...}` | Generate `href` with Router `useHref(to)`, keep Router navigation handlers for SPA, modified-click, keyboard, and disabled behavior, and forward the ref to the real anchor. |
|
|
52
|
+
| `leadingIcon` / `trailingIcon` | `prefix` / `suffix` | Mechanical prop rename only; preserve the decorative slot boundary. |
|
|
53
|
+
|
|
54
|
+
The adapter must keep the package's real native `<a>` as the interactive owner; it must not move navigation roles, focus, keyboard handling, disabled semantics, or the ref onto a wrapper. Adapter implementation, SPA behavior tests, and all-site consumer evidence remain q1travel-web #702 work and are not proven by this package gate.
|
|
30
55
|
|
|
31
56
|
## Component-Owned Semantics
|
|
32
57
|
|
|
33
58
|
- `prefix` and `suffix` are decorative slots. Button and ButtonLink apply `aria-hidden="true"` to the slot boundary; callers do not repeat that attribute on the supplied icon.
|
|
34
|
-
- `
|
|
59
|
+
- `aria-keyshortcuts` is the canonical native attribute on Button and ButtonLink; the compatibility `ariaKeyshortcuts` field is also accepted, and a defined canonical value wins by nullish precedence. The visible `shortcut` badge remains decorative and must correspond to a real registered shortcut.
|
|
35
60
|
- ButtonLink renders an anchor. Its inherited button-only `type` field remains a deprecated compatibility no-op and is never forwarded to the anchor.
|
|
36
61
|
|
|
37
62
|
## Known Consumer Scan
|
|
@@ -69,5 +94,6 @@ Until then, the aliases remain tested public compatibility contracts.
|
|
|
69
94
|
|
|
70
95
|
- This contract does not migrate `q1travel-web`.
|
|
71
96
|
- This contract does not choose a package version, publish a package, or remove an alias.
|
|
72
|
-
- This contract does not
|
|
97
|
+
- This contract does not migrate `quiet`, `page`, or React Router adapter behavior into the package; those remain q1travel-web #702 consumer seams with the measurable rules above.
|
|
98
|
+
- This contract does not change Button size, variant, route, or existing loading compatibility beyond the state contract described above.
|
|
73
99
|
- This contract does not move button semantics or event handlers to a wrapper.
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Overlay Foundation Contract
|
|
2
|
+
|
|
3
|
+
> 状态:Accepted(O1–O7 已确认;仅允许进入 Foundation RED 阶段,未授权 GREEN 或生产实现)。
|
|
4
|
+
> Issue:[#27](https://github.com/q1travel/q1travel-design-system/issues/27)
|
|
5
|
+
> 日期:2026-08-06
|
|
6
|
+
> 所有者:`@q1travel/design-system`
|
|
7
|
+
|
|
8
|
+
## 1. 目的
|
|
9
|
+
|
|
10
|
+
为 `Modal`、`DestructiveActionModal`、`Sheet`、`Drawer` 提供一个共同的 overlay foundation seam,统一以下跨组件行为:
|
|
11
|
+
|
|
12
|
+
- portal 挂载与 SSR 安全边界;
|
|
13
|
+
- overlay 注册、嵌套顺序与 top-most 事件所有权;
|
|
14
|
+
- Escape、outside pointer、focus trap 与 focus return 的协作;
|
|
15
|
+
- blocking overlay 的 ref-counted scroll lock;
|
|
16
|
+
- 层级的语义顺序,而不是由每个组件各自猜测全局 z-index。
|
|
17
|
+
|
|
18
|
+
本文件先定义可验证的行为合同,再由拆分后的实现票逐个接入组件。
|
|
19
|
+
|
|
20
|
+
## 2. 决策边界
|
|
21
|
+
|
|
22
|
+
### 2.1 本稿确认
|
|
23
|
+
|
|
24
|
+
本稿已按 O1–O7 完成 ADR review,可作为 Foundation RED 阶段的行为约束。以下边界仍然有效:
|
|
25
|
+
|
|
26
|
+
- 不修改现有 overlay 组件实现;
|
|
27
|
+
- 不新增公开 `OverlayHost`、stack 或 hook API;
|
|
28
|
+
- 不修改 `public/design.md`、`public/design.dark.md` 等生成镜像;
|
|
29
|
+
- 不修改 `q1travel-web`,也不把 Web 的类名、DOM 结构或实现复制到本包;
|
|
30
|
+
- 不把 foundation、四个组件接入和 Web migration 合并为一个 PR。
|
|
31
|
+
|
|
32
|
+
### 2.2 第一批纳入 foundation 的 surface
|
|
33
|
+
|
|
34
|
+
第一批目标是 `Modal`、`DestructiveActionModal`、`Sheet`、`Drawer`。它们当前都拥有不同版本的 focus、Escape、body overflow 或 stack-adjacent 行为,因此需要同一 seam。
|
|
35
|
+
|
|
36
|
+
以下 surface 不在第一批组件接入范围内,必须先完成单独分类后才能宣称复用本合同:
|
|
37
|
+
|
|
38
|
+
| Surface | 分类 | 原因 |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| `CommandMenu`、`SearchDialog` | 同一 foundation 的后续 consumer | 单独 integration ticket;保留各自的菜单/搜索键盘模型,只复用已接受的 shared seam |
|
|
41
|
+
| `Calendar` | 独立 composite | popup dialog 语义与日期键盘模型需保持独立 |
|
|
42
|
+
| `ContextSwitcher` | 独立 composite | 现有 portal 与菜单/对话框模式有自己的契约 |
|
|
43
|
+
| `Toast` | 非 blocking surface,独立 live layer | 保持可见/可播报;blocking overlay 打开时 action 不可 focus/click |
|
|
44
|
+
| anchored menu、popover、tooltip | 非 blocking surface | 不应自动获得 modal 的 focus trap 或 body scroll lock |
|
|
45
|
+
|
|
46
|
+
### 2.3 行为证据的使用方式
|
|
47
|
+
|
|
48
|
+
`q1travel-web` 的 OverlayHost/GlassSurface 行为、测试和 Gate 0 文档只能作为行为证据。它们不改变本包的设计权威,也不授权 Web migration。package contract、component API 和本仓库的 token 规则优先。
|
|
49
|
+
|
|
50
|
+
## 3. 术语与记录
|
|
51
|
+
|
|
52
|
+
Foundation 内部可以使用以下记录,不代表现在要导出同名 public API:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
type OverlayRecord = {
|
|
56
|
+
id: string;
|
|
57
|
+
kind: string;
|
|
58
|
+
blocking: boolean;
|
|
59
|
+
dismiss: {
|
|
60
|
+
escape: boolean;
|
|
61
|
+
outsidePointer: boolean;
|
|
62
|
+
};
|
|
63
|
+
opener: HTMLElement | null;
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
记录必须有稳定的实例身份。相同实例重复 register/unregister 必须幂等;不能用组件名称、DOM 文本或全局单例状态代替实例身份。
|
|
68
|
+
|
|
69
|
+
## 4. Normative contract
|
|
70
|
+
|
|
71
|
+
本节中的 `MUST` / `MUST NOT` 是拟议的实现约束;`SHOULD` 是默认要求,若不遵守必须在组件 integration contract 中说明原因。
|
|
72
|
+
|
|
73
|
+
### 4.1 Portal 与 SSR
|
|
74
|
+
|
|
75
|
+
- MUST 在浏览器中把 foundation overlay 挂到 package-owned 的稳定 overlay root,而不是把 panel 直接挂在任意业务容器或组件局部节点上。
|
|
76
|
+
- MUST 让同一 `Document` 复用一个 root;建议使用独立的 `data-q1-portal="overlay"` 标识,不复用 `ContextSwitcher` 的 root。
|
|
77
|
+
- MUST 延迟 root 的 DOM 创建到浏览器生命周期;SSR/render 阶段不得读取 `document`、`window` 或修改 `body`。
|
|
78
|
+
- MUST 在 hydration 后保持 root 的身份稳定,避免打开/关闭 overlay 时反复创建不同 root。
|
|
79
|
+
- MUST 只清理由 foundation 自己创建且已经没有注册实例的 root;不得删除宿主显式提供的节点或其中的无关子节点。
|
|
80
|
+
- MUST 在 `document.body` 尚未存在时安全等待挂载;不得为了完成 render 强行创建 body。
|
|
81
|
+
- MUST 为 root 提供独立 stacking context,避免业务页面的局部 `z-index` 影响 overlay 顺序。
|
|
82
|
+
|
|
83
|
+
### 4.2 Registry、stack 与 top-most
|
|
84
|
+
|
|
85
|
+
- MUST 以注册顺序形成 LIFO stack;最后打开的实例是当前 top-most。
|
|
86
|
+
- MUST 只让 top-most 实例消费 foundation-owned 的 Escape、outside pointer 和 focus-trap 事件。
|
|
87
|
+
- MUST 在 top-most 关闭或卸载后,让下一个仍打开的实例立即恢复为事件 owner。
|
|
88
|
+
- MUST 对卸载、重复关闭和重复 unregister 做幂等处理;不得留下失效 record、重复 scroll lock 或悬挂事件监听器。
|
|
89
|
+
- MUST 使 `blocking` 成为显式 record 属性;不得根据 `kind` 名称、DOM 层级或 z-index 猜测是否阻断。
|
|
90
|
+
- MUST 让非 blocking foundation consumer 可以参与排序而不增加 blocking lock 计数;它仍不能未经授权获得 modal 的 `aria-modal` 或 focus trap。
|
|
91
|
+
- MUST 保证一个组件实例的多个 React render 不会产生多个 stack record。
|
|
92
|
+
|
|
93
|
+
### 4.3 Dismissal ownership
|
|
94
|
+
|
|
95
|
+
- MUST 把 Escape 和 outside pointer 的监听归 foundation 统一调度,组件只提供当前 policy 和 close callback。
|
|
96
|
+
- MUST 只对 top-most 且 policy 允许 dismiss 的实例触发 close。
|
|
97
|
+
- MUST 处理 pointer down/up 的完整 outside gesture;拖动从 overlay 内开始、在外部结束时不得误关闭。
|
|
98
|
+
- MUST 不把 backdrop 当作可获得键盘焦点的额外控件;可关闭 overlay 必须提供可见且可命名的 close control 或等价取消路径。
|
|
99
|
+
- MUST 允许组件在 loading、提交中或其他不可中断状态下关闭 dismissal policy;foundation 不得绕过该 policy。
|
|
100
|
+
- MUST 保留每个组件既有的公开关闭 API,具体默认值在对应 integration contract 中冻结,不在 foundation 实现中隐式改写。
|
|
101
|
+
|
|
102
|
+
### 4.4 Focus entry、trap 与 return
|
|
103
|
+
|
|
104
|
+
- MUST 在 blocking dialog 打开后把 focus 移到 overlay 内:优先使用有效的显式 initial-focus target,其次使用第一个可聚焦元素,再其次将 dialog container 临时设为 `tabIndex=-1` 并 focus。
|
|
105
|
+
- MUST 对空内容、异步内容和所有 focusable 元素都提供稳定 fallback;不得因没有按钮而把 focus 留在页面背景。
|
|
106
|
+
- MUST 仅由 top-most blocking overlay 捕获 `Tab` / `Shift+Tab`,并在其 focusable 集合内循环。
|
|
107
|
+
- MUST 排除 disabled、hidden、不可见或不可交互节点;focusable selector 必须由 foundation 统一定义,不能由每个组件复制一份近似列表。
|
|
108
|
+
- MUST 记录打开时的 opener,并在正常关闭后把 focus 返回仍连接且可聚焦的 opener。
|
|
109
|
+
- MUST 在 opener 已卸载、被 disabled 或不再可聚焦时使用组件提供的明确 fallback;不得 focus 到 `body` 或随机页面节点。
|
|
110
|
+
- MUST 在嵌套 overlay 中只恢复最内层关闭后的上一个合法 focus 目标;外层 overlay 不能被 inner close 错误地重置。
|
|
111
|
+
- SHOULD 对长内容允许把 dialog container 或首个静态标题作为 initial focus,避免用户从很远的末尾内容开始阅读。
|
|
112
|
+
|
|
113
|
+
### 4.5 Blocking、inert 与 aria
|
|
114
|
+
|
|
115
|
+
- MUST 只有真正阻断 overlay 外部交互时才使用 `aria-modal="true"`。
|
|
116
|
+
- MUST 让 blocking overlay 打开时,overlay 外部的页面内容不可点击、不可通过 Tab 进入,并在视觉上保持被阻断的关系。
|
|
117
|
+
- MUST 让非 blocking surface 不声明 `aria-modal="true"`,也不借用 blocking overlay 的 focus trap。
|
|
118
|
+
- MUST 为 dialog 提供 `aria-labelledby` 或 `aria-label`;有可用的补充说明时再使用 `aria-describedby`。
|
|
119
|
+
- MUST 按实际交互选择 `dialog`、`alertdialog` 或其他匹配的 APG pattern,不得仅因为视觉上是弹层就统一提升为 `alertdialog`。
|
|
120
|
+
- MUST 使可见 close/cancel 路径处于正常键盘顺序;Escape 不能成为唯一退出方式。
|
|
121
|
+
- MUST 让 dialog 内的 loading、error、disabled 状态保留可理解的名称、状态和恢复路径。
|
|
122
|
+
|
|
123
|
+
### 4.6 Scroll lock
|
|
124
|
+
|
|
125
|
+
- MUST 按 `Document` 对 blocking overlay 做 ref-counted lock;第一个 lock 保存原始 overflow、scrollbar compensation 以及 foundation 改写的其他 inline style。
|
|
126
|
+
- MUST 在嵌套 blocking overlay 打开时只增加计数,不覆盖第一次保存的原始值,也不重复累加 compensation。
|
|
127
|
+
- MUST 在最后一个 blocking overlay 关闭或卸载时精确恢复保存的 inline style;预先存在的 `overflow`、padding 和空值语义必须保留。
|
|
128
|
+
- MUST 防止计数低于零,并在重复 close/unregister、异常卸载和打开失败后清理正确。
|
|
129
|
+
- MUST 在 SSR 或没有 `document` 时 no-op,且不得输出浏览器-only warning 作为正常路径。
|
|
130
|
+
- SHOULD 在 overlay 内部滚动区域设置 `overscroll-behavior: contain`,避免滚动链穿透到背景页面。
|
|
131
|
+
|
|
132
|
+
### 4.7 Layer 与 token
|
|
133
|
+
|
|
134
|
+
- MUST 由 foundation 决定 overlay 的语义顺序;组件 CSS 不得继续各自硬编码一组互不关联的全局 z-index 作为 stack 协议。
|
|
135
|
+
- MUST 让同一 root 内的 DOM/stack 顺序表达 top-most 关系;数值只服务于明确的 stacking context,不得成为事件所有权的来源。
|
|
136
|
+
- 新增公开 token 时 MUST 使用 `--q1-*` 命名;不得新增 Web 的 `--ds-*` 名称,也不得在本票中做全仓库重命名。
|
|
137
|
+
- Foundation root 使用语义 token `--q1-z-overlay-root`,接受的初始层级值为 `1300`,高于现有 `--q1-z-popover: 1200`;实现不得继续使用 Modal/Drawer/Sheet 当前的 `69/70/80` 作为跨组件协议。
|
|
138
|
+
- Toast 使用独立语义 token `--q1-z-toast`,接受的初始层级值为 `1400`;它不加入 foundation blocking registry,但 blocking overlay 打开时 Toast action 必须 pointer-inert 且不可 focus。
|
|
139
|
+
- `--q1-z-popover` 保持现有名称和值;tooltip、anchored menu、popover 的后续分类不能反向改变本票已确认的 overlay root 顺序。
|
|
140
|
+
|
|
141
|
+
## 5. 初始组件适配边界
|
|
142
|
+
|
|
143
|
+
每个组件 integration ticket 必须保留自己的产品语义,只把重复的基础行为接入 foundation:
|
|
144
|
+
|
|
145
|
+
| Integration | 必须保留 | 必须由 foundation 接管 | 依赖 |
|
|
146
|
+
| --- | --- | --- | --- |
|
|
147
|
+
| Modal | 公开 props、尺寸/布局、关闭策略、文档示例 | portal、stack、focus、blocking lock、统一 Escape/outside ownership | Foundation |
|
|
148
|
+
| DestructiveActionModal | destructive/loading/confirm 语义与提交状态 | 同上;loading 时由 policy 禁止关闭 | Foundation |
|
|
149
|
+
| Sheet | 现有 side、persistent-context 语义;blocking=true、outside pointer=false | 同上;不能让 CSS 注释与 ARIA 语义继续冲突 | Foundation |
|
|
150
|
+
| Drawer | mobile bottom、gesture、outside/Escape 语义 | 同上;gesture 关闭仍必须服从 top-most 与 loading policy | Foundation |
|
|
151
|
+
|
|
152
|
+
组件接入不得一次性重写四个组件,也不得在未有单独 contract 的情况下改变公开 DOM、默认 dismissal 或 accessibility tier。
|
|
153
|
+
|
|
154
|
+
## 6. RED 验收设计
|
|
155
|
+
|
|
156
|
+
Foundation RED 阶段先提交失败的 runtime/browser assertions,不写生产实现。RED 至少覆盖:
|
|
157
|
+
|
|
158
|
+
1. SSR:无 `document` 时不抛错、不创建 DOM;hydration 后只产生一个可复用 root。
|
|
159
|
+
2. Root lifecycle:两个实例共用 root;最后一个实例关闭后才清理 package-owned root;宿主节点不被误删。
|
|
160
|
+
3. Stack:A → B 打开时 B 是 top-most;B 关闭后 A 恢复;重复注册/卸载不会重复响应。
|
|
161
|
+
4. Escape:只关闭 top-most 且允许 Escape 的实例;外层不能越级关闭。
|
|
162
|
+
5. Outside pointer:只处理 top-most;完整 down/up gesture 不误判拖动;禁止 outside 时不关闭。
|
|
163
|
+
6. Focus entry:显式 target、首个 tabbable、container fallback 三条路径都把 focus 放入 overlay。
|
|
164
|
+
7. Focus loop:Tab/Shift+Tab 在当前 top-most 集合内循环;disabled/hidden 节点不会进入集合。
|
|
165
|
+
8. Focus return:正常 close 返回 opener;opener 卸载或不可聚焦时使用确定 fallback;嵌套 close 不污染外层。
|
|
166
|
+
9. Scroll lock:预存 inline style、嵌套计数、scrollbar compensation、重复 close 和异常卸载均精确恢复。
|
|
167
|
+
10. Accessibility:blocking/non-blocking 的 ARIA 差异、accessible name、可见 close control 和键盘路径均有 browser assertion。
|
|
168
|
+
|
|
169
|
+
RED 通过并经过单独 review 后,才可提出 Foundation GREEN 授权;随后按第 7 节拆分的组件 ticket 逐个运行现有 `npm run check` 与相关 browser checks。部分测试、静态快照或“页面看起来打开了”不构成验收通过。
|
|
170
|
+
|
|
171
|
+
## 7. 交付拆分与阻塞关系
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
D0 #27 ADR/contract acceptance
|
|
175
|
+
↓ separate RED authorization
|
|
176
|
+
D1 Overlay Foundation RED tests/assertions
|
|
177
|
+
↓ RED review + separate GREEN authorization
|
|
178
|
+
D2 Overlay Foundation GREEN runtime
|
|
179
|
+
├──→ D3 Modal + DestructiveActionModal integration
|
|
180
|
+
├──→ D4 Sheet integration(blocking=true,outside pointer=false)
|
|
181
|
+
└──→ D5 Drawer integration + gesture verification
|
|
182
|
+
↓ all relevant checks green
|
|
183
|
+
D6 docs/browser contract completion and package PR
|
|
184
|
+
↓ separate release/consumer gate
|
|
185
|
+
D7 q1travel-web adoption/migration
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- D1 不得修改四个生产组件;D2 不得在 RED review 前开始;D3–D5 分开审查和验证。
|
|
189
|
+
- D6 不等于 npm publish;source、PR、CI/review、merge、registry/release、consumer adoption 和 production/smoke 是独立 gates。
|
|
190
|
+
- D7 不属于本仓库 #27 的实现 PR;Web 只能在 package contract、发布和 consumer evidence 都满足后另行授权。
|
|
191
|
+
|
|
192
|
+
## 8. 已确认的 ADR decisions
|
|
193
|
+
|
|
194
|
+
以下结论已在本次 #27 review 中确认。它们定义 RED 阶段的边界,不授权生产实现:
|
|
195
|
+
|
|
196
|
+
| ID | 已确认决策 | 依据与实现边界 |
|
|
197
|
+
| --- | --- | --- |
|
|
198
|
+
| O1 | `CommandMenu`、`SearchDialog` 是同一 foundation 的后续 consumer | foundation core 先服务四个首批组件;后续 ticket 只接入 shared seam,不重写其菜单/搜索键盘模型 |
|
|
199
|
+
| O2 | `Sheet` 是 blocking overlay,不是 non-modal layer | 当前 body lock、focus trap、`aria-modal` 与遮罩行为一致;外部点击关闭策略保持 false,不能把“不外点关闭”误称为 non-modal |
|
|
200
|
+
| O3 | overlay root 只由 package-owned foundation 管理 | v1 不开放 consumer 注入;公共注入能力另开 public API contract |
|
|
201
|
+
| O4 | Toast action 在 blocking overlay 期间不可交互 | Toast 保持可见/可播报;action 移除 focus/click 路径,需可交互时改为 dialog 内 action 或显式 non-modal 流程 |
|
|
202
|
+
| O5 | 不新增公开 focus fallback 配置 | foundation 使用稳定的内部 fallback;保留现有组件 public API,避免为未证实 consumer 需求扩展 contract |
|
|
203
|
+
| O6 | 单 root semantic layer,不按组件分配全局 z-index | `--q1-z-overlay-root: 1300`、`--q1-z-toast: 1400`;root 内由 DOM/registry 顺序表达 top-most,现有 `--q1-z-popover: 1200` 不改 |
|
|
204
|
+
| O7 | 四个首批组件共用同一 focus/dismiss/scroll-lock core | gesture、loading 和产品特有 dismissal 只保留在组件 policy 层;foundation 不绕过 policy |
|
|
205
|
+
|
|
206
|
+
## 9. 非目标
|
|
207
|
+
|
|
208
|
+
- 不在本票中修复 Modal、Sheet、Drawer、DestructiveActionModal 的现有实现差异。
|
|
209
|
+
- 不在本票中新增动画、视觉 redesign、尺寸/radius/color token 或组件 API。
|
|
210
|
+
- 不在本票中迁移 Web,也不以 Web PR、Web 测试通过或本地浏览器结果替代 package contract acceptance。
|
|
211
|
+
- 不以关闭 #27、创建 PR、merge、publish 或 release 作为本 contract 的隐含后续动作。
|
|
212
|
+
|
|
213
|
+
## 10. References
|
|
214
|
+
|
|
215
|
+
- [q1travel Interface Guidelines](./interface-guidelines.md)
|
|
216
|
+
- [q1travel Implementation Map](./implementation-map.md)
|
|
217
|
+
- [q1travel Token Naming](./token-naming.md)
|
|
218
|
+
- [WAI-ARIA APG: Dialog (Modal) Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)
|
|
219
|
+
- 历史范围证据:`docs/superpowers/specs/w2-overlays-familyization-scoping.md`(非当前决策源)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# StatusChip → Badge 迁移合同
|
|
2
|
+
|
|
3
|
+
Status: accepted
|
|
4
|
+
|
|
5
|
+
关联:q1travel-design-system#71、q1travel-web StatusChip consumer migration
|
|
6
|
+
|
|
7
|
+
## 目标与边界
|
|
8
|
+
|
|
9
|
+
这份合同把 q1travel-web 的展示型 `StatusChip` 映射到已发布的 `@q1travel/design-system` `Badge`。它只冻结 package props、语义映射、视觉取舍和回滚单位;不修改 q1travel-web,不把 `StatusChip` 兼容 alias 加入 package,也不批量迁移消费者。
|
|
10
|
+
|
|
11
|
+
`Badge` 已经是胶囊形(`border-radius: 999px`),因此迁移不需要新增 pill API。#24/#58 已裁定:状态 tone 使用现有 `subtle`,`quiet` 保留为低强调元数据档,StatusChip 原有 1px 描边并入 subtle,不新增 `outline`。
|
|
12
|
+
|
|
13
|
+
## Variant 映射
|
|
14
|
+
|
|
15
|
+
| StatusChip 输入 | Badge props | 处理与语义 |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `neutral` / `is-neutral` | `<Badge tone="neutral" subtle>` | 中性状态;保留可扫描文字,不使用语义色。 |
|
|
18
|
+
| `info` / `is-info` | `<Badge tone="info" subtle>` | 信息状态;使用 package 的 info semantic token。 |
|
|
19
|
+
| `success` / `is-success` | `<Badge tone="success" subtle>` | 成功状态;使用 package `#28a948` 对应的 status-success token。 |
|
|
20
|
+
| `warning` / `is-warning` | `<Badge tone="warning" subtle>` | 警告状态;保留文字与 warning 语义。 |
|
|
21
|
+
| `danger` / `is-danger` | `<Badge tone="danger" subtle>` | 危险状态;保留文字与 danger 语义。 |
|
|
22
|
+
| `quiet` / `is-quiet` | `<Badge quiet>` | 低强调元数据;不携带 tone 语义色。 |
|
|
23
|
+
| 空值 / unknown | `<Badge tone="neutral" subtle>` | 与现有 StatusChip fallback 一致;不得把未知输入推断为 success 或 quiet。 |
|
|
24
|
+
|
|
25
|
+
### Props 保留规则
|
|
26
|
+
|
|
27
|
+
- `children` 原样保留。
|
|
28
|
+
- `className`、`title`、`data-*`、ARIA 和其他原生 span 属性原样透传到 `Badge`。
|
|
29
|
+
- StatusChip 的 `variant` 归一化在 Web adapter 或调用点完成;package `Badge` 不接受 `is-*` alias。
|
|
30
|
+
- StatusChip 没有独立尺寸合同;直接迁移使用 `Badge` 默认 `size="md"`(24px)。只有经过单独密度验收的调用点才能显式选择 `size="sm"`,不能从“处于表格中”自动推断尺寸。
|
|
31
|
+
|
|
32
|
+
## 可接受变化与停止条件
|
|
33
|
+
|
|
34
|
+
以下变化是已接受的 package 迁移结果:
|
|
35
|
+
|
|
36
|
+
- StatusChip 的 1px hairline 描边不单独保留;其余 tone 使用 Badge `subtle` 的无描边淡染形态。
|
|
37
|
+
- 成功色统一到 package `--q1-color-status-success`(`#28a948` 来源),不继续维护 Web 的独立 success source token。
|
|
38
|
+
- Badge 的 light/dark subtle 颜色由 package token 派生;不能把 Web 的 page-local 颜色复制进 package。
|
|
39
|
+
|
|
40
|
+
出现以下任一条件时,停止该调用点迁移并保留本地 StatusChip:
|
|
41
|
+
|
|
42
|
+
- 调用点依赖 `.ui2-status-chip` 的自定义 border、尺寸或布局几何,且不能由 Badge props 与局部 adapter 明确复现。
|
|
43
|
+
- `children` 不是展示型状态内容,或状态含义需要交互/点击行为;改用对应的交互组件或另立设计票。
|
|
44
|
+
- 迁移后状态文字、语义色、密集行高或 dark theme 对比度不满足当前消费者验收。
|
|
45
|
+
- 调用点需要新的 tone、outline、尺寸或状态语义;不得在本迁移中偷偷扩大 Badge 公共 API。
|
|
46
|
+
|
|
47
|
+
## Props 迁移与回滚单位
|
|
48
|
+
|
|
49
|
+
一次迁移的最小单位是一个消费者文件中的一组 StatusChip 使用点:
|
|
50
|
+
|
|
51
|
+
1. 将 import 从 `StatusChip` 改为 `Badge`。
|
|
52
|
+
2. 将 `variant` 通过上述映射转换为 `tone`/`subtle`/`quiet`。
|
|
53
|
+
3. 保留 children、className、title、data-*、ARIA 和周围表格/详情结构。
|
|
54
|
+
4. 在同一文件运行该消费者的 focused contract、light/dark 浏览器检查和密集表面验收。
|
|
55
|
+
|
|
56
|
+
回滚只恢复这个消费者文件的 import 与 JSX/props 映射,不回滚 package Badge,也不修改其他消费者。一个文件的回滚不应影响同一批次的其他状态面。
|
|
57
|
+
|
|
58
|
+
## 对比度证据
|
|
59
|
+
|
|
60
|
+
package `Badge` 的 subtle 派生色已在 #24 的双主题实测中验证:success subtle 在 light theme 约为 5.5:1,在 dark theme 约为 7.1:1,均达到正文 AA 4.5:1。迁移使用 package 派生文字色,不能沿用 Web StatusChip 的低对比 success text token。
|
|
61
|
+
|
|
62
|
+
状态信息仍由文字和语义 tone 双重表达;不能只依赖颜色。Badge 的密集表格/详情 specimen 在 Badge 页面同时覆盖 neutral、info、success、warning、danger 和 quiet,并随文档站 light/dark theme 重新计算 token。
|
|
63
|
+
|
|
64
|
+
## 非目标
|
|
65
|
+
|
|
66
|
+
- 不修改 q1travel-web。
|
|
67
|
+
- 不把 StatusChip 导出到 `@q1travel/design-system`。
|
|
68
|
+
- 不在本票新增 `outline`、`is-*`、page-local color token 或第三方路由依赖。
|
|
69
|
+
- 不因 package specimen 通过就宣称 61 个 Web 消费文件已迁移。
|
|
@@ -204,6 +204,10 @@ declare type ButtonBaseProps = {
|
|
|
204
204
|
loading?: boolean;
|
|
205
205
|
/** loading 时 role=status 播报的文案,默认"加载中…";仅供屏幕阅读器,不影响可见文案。 */
|
|
206
206
|
loadingLabel?: string;
|
|
207
|
+
/** 已提交忙态:原生禁用 + aria-busy,阻止重复提交。 */
|
|
208
|
+
busy?: boolean;
|
|
209
|
+
/** busy 时的可见状态文案;为空时保留原始 children,并用 loadingLabel 或固定 fallback 播报。 */
|
|
210
|
+
busyLabel?: string;
|
|
207
211
|
disabled?: boolean;
|
|
208
212
|
type?: 'button' | 'submit' | 'reset';
|
|
209
213
|
shortcut?: string;
|
|
@@ -238,6 +242,7 @@ export declare type ButtonLinkProps = Omit<ButtonBaseProps, 'type'> & {
|
|
|
238
242
|
onFocus?: FocusEventHandler<HTMLAnchorElement>;
|
|
239
243
|
onBlur?: FocusEventHandler<HTMLAnchorElement>;
|
|
240
244
|
onKeyDown?: KeyboardEventHandler<HTMLAnchorElement>;
|
|
245
|
+
'aria-keyshortcuts'?: string;
|
|
241
246
|
ref?: Ref<HTMLAnchorElement>;
|
|
242
247
|
};
|
|
243
248
|
|
|
@@ -630,7 +635,7 @@ export declare function DestructiveActionModal({ open, onClose, title, descripti
|
|
|
630
635
|
/** 提交进行中:双按钮禁用、主操作显示加载指示;open 由调用方控制,加载中模态保持打开。 */
|
|
631
636
|
loading?: boolean;
|
|
632
637
|
onConfirm: () => string | null;
|
|
633
|
-
}):
|
|
638
|
+
}): ReactNode;
|
|
634
639
|
|
|
635
640
|
export declare function DotsMenu({ entries, size, disabled, align, }: {
|
|
636
641
|
entries: DotsMenuEntry[];
|
|
@@ -653,7 +658,7 @@ export declare function Drawer({ open, onClose, title, children, footer, height,
|
|
|
653
658
|
footer: ReactNode;
|
|
654
659
|
/** 面板最大高度(px),覆盖默认 60vh;仅当默认高度会裁切主操作时使用。 */
|
|
655
660
|
height?: number;
|
|
656
|
-
}):
|
|
661
|
+
}): ReactNode;
|
|
657
662
|
|
|
658
663
|
export declare function EmptyState({ icon, title, description, actions, headingLevel, }: {
|
|
659
664
|
icon?: ReactNode;
|
|
@@ -892,7 +897,7 @@ export declare function Modal({ open, onClose, title, children, footer, initialF
|
|
|
892
897
|
dismissable?: boolean;
|
|
893
898
|
/** 头尾固定、只有正文区域滚动;用于正文可能超出视口高度的场景(长列表、长条款)。 */
|
|
894
899
|
sticky?: boolean;
|
|
895
|
-
}):
|
|
900
|
+
}): ReactNode;
|
|
896
901
|
|
|
897
902
|
export declare function ModalInset({ children }: {
|
|
898
903
|
children: ReactNode;
|
|
@@ -1179,7 +1184,7 @@ export declare function Sheet({ open, onClose, title, side, children, footer }:
|
|
|
1179
1184
|
side?: SheetSide;
|
|
1180
1185
|
children: ReactNode;
|
|
1181
1186
|
footer?: ReactNode;
|
|
1182
|
-
}):
|
|
1187
|
+
}): ReactNode;
|
|
1183
1188
|
|
|
1184
1189
|
export declare type SheetSide = 'top' | 'right' | 'bottom' | 'left';
|
|
1185
1190
|
|
|
@@ -1280,11 +1285,12 @@ export declare function StatusDot({ tone, label, animated, showLabel, titlePrefi
|
|
|
1280
1285
|
|
|
1281
1286
|
export declare type StatusDotTone = 'neutral' | 'warning' | 'success' | 'danger';
|
|
1282
1287
|
|
|
1283
|
-
export declare function Switch({ options, ariaLabel, name, defaultValue, onChange, size, fullWidth, iconOnly, }: {
|
|
1288
|
+
export declare function Switch({ options, ariaLabel, name, defaultValue, value, onChange, size, fullWidth, iconOnly, }: {
|
|
1284
1289
|
options: SwitchOption[];
|
|
1285
1290
|
ariaLabel: string;
|
|
1286
1291
|
name?: string;
|
|
1287
1292
|
defaultValue?: string;
|
|
1293
|
+
value?: string;
|
|
1288
1294
|
onChange?: (value: string) => void;
|
|
1289
1295
|
size?: 'sm' | 'md' | 'lg';
|
|
1290
1296
|
fullWidth?: boolean;
|
|
@@ -1296,8 +1302,9 @@ export declare type SwitchOption = {
|
|
|
1296
1302
|
value: string;
|
|
1297
1303
|
label: string;
|
|
1298
1304
|
icon?: ReactNode;
|
|
1305
|
+
meta?: ReactNode;
|
|
1299
1306
|
disabled?: boolean;
|
|
1300
|
-
/**
|
|
1307
|
+
/** 补充说明;用于在标签旁提供低强调度的上下文。 */
|
|
1301
1308
|
tooltip?: string;
|
|
1302
1309
|
};
|
|
1303
1310
|
|
|
@@ -1365,7 +1372,7 @@ export declare function ToastProvider({ children }: {
|
|
|
1365
1372
|
|
|
1366
1373
|
export declare type ToastTone = 'default' | 'success' | 'warning' | 'error';
|
|
1367
1374
|
|
|
1368
|
-
export declare function Toggle({ checked, defaultOn, onChange, disabled, size, onColor, label, description, labelSide, ariaLabel, }: {
|
|
1375
|
+
export declare function Toggle({ checked, defaultOn, onChange, disabled, size, onColor, label, description, labelSide, 'aria-label': canonicalAriaLabel, 'aria-labelledby': ariaLabelledBy, ariaLabel, }: {
|
|
1369
1376
|
/** 受控值;提供后内部状态失效,显示值以外部回写的 checked 为准。 */
|
|
1370
1377
|
checked?: boolean;
|
|
1371
1378
|
/** 非受控模式的初始值;与 checked 同传时以 checked 为准。 */
|
|
@@ -1378,6 +1385,9 @@ export declare function Toggle({ checked, defaultOn, onChange, disabled, size, o
|
|
|
1378
1385
|
label?: string;
|
|
1379
1386
|
description?: string;
|
|
1380
1387
|
labelSide?: 'left' | 'right';
|
|
1388
|
+
'aria-label'?: string;
|
|
1389
|
+
'aria-labelledby'?: string;
|
|
1390
|
+
/** @deprecated 使用 aria-label。 */
|
|
1381
1391
|
ariaLabel?: string;
|
|
1382
1392
|
}): JSX.Element;
|
|
1383
1393
|
|