@greypan/web-ui 6.4.0 → 8.0.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 (148) hide show
  1. package/README.CN.md +211 -50
  2. package/README.md +222 -61
  3. package/dist/assets/glass.js +1 -1
  4. package/dist/assets/glass.js.map +1 -1
  5. package/dist/assets/selection-control.js +6 -0
  6. package/dist/assets/selection-control.js.map +1 -0
  7. package/dist/assets/toast-containers.js.map +1 -1
  8. package/dist/components/autocomplete/index.d.ts +19 -5
  9. package/dist/components/autocomplete/index.js +142 -108
  10. package/dist/components/autocomplete/index.js.map +1 -1
  11. package/dist/components/autocomplete/style.js +1 -1
  12. package/dist/components/autocomplete/style.js.map +1 -1
  13. package/dist/components/avatar/index.js +4 -4
  14. package/dist/components/badge/style.js.map +1 -1
  15. package/dist/components/button/index.js +15 -15
  16. package/dist/components/button/style.js.map +1 -1
  17. package/dist/components/button-group/index.js +5 -5
  18. package/dist/components/checkbox/index.d.ts +9 -2
  19. package/dist/components/checkbox/index.js +50 -26
  20. package/dist/components/checkbox/index.js.map +1 -1
  21. package/dist/components/checkbox/style.js +1 -1
  22. package/dist/components/checkbox/style.js.map +1 -1
  23. package/dist/components/checkbox-group/index.d.ts +3 -0
  24. package/dist/components/checkbox-group/index.js +27 -16
  25. package/dist/components/checkbox-group/index.js.map +1 -1
  26. package/dist/components/checkbox-group/style.js +1 -1
  27. package/dist/components/checkbox-group/style.js.map +1 -1
  28. package/dist/components/collapse/index.js.map +1 -1
  29. package/dist/components/context-menu/index.d.ts +10 -0
  30. package/dist/components/context-menu/index.js +51 -34
  31. package/dist/components/context-menu/index.js.map +1 -1
  32. package/dist/components/dialog/index.d.ts +6 -0
  33. package/dist/components/dialog/index.js +49 -21
  34. package/dist/components/dialog/index.js.map +1 -1
  35. package/dist/components/dialog/style.js +1 -1
  36. package/dist/components/dialog/style.js.map +1 -1
  37. package/dist/components/drawer/index.d.ts +22 -4
  38. package/dist/components/drawer/index.js +141 -110
  39. package/dist/components/drawer/index.js.map +1 -1
  40. package/dist/components/drawer/style.js +1 -1
  41. package/dist/components/drawer/style.js.map +1 -1
  42. package/dist/components/dropdown/index.d.ts +21 -1
  43. package/dist/components/dropdown/index.js +77 -51
  44. package/dist/components/dropdown/index.js.map +1 -1
  45. package/dist/components/editable-text/index.d.ts +471 -0
  46. package/dist/components/editable-text/index.js +206 -0
  47. package/dist/components/editable-text/index.js.map +1 -0
  48. package/dist/components/editable-text/style.js +6 -0
  49. package/dist/components/editable-text/style.js.map +1 -0
  50. package/dist/components/image-preview/index.js +103 -80
  51. package/dist/components/image-preview/index.js.map +1 -1
  52. package/dist/components/image-preview/style.js +1 -1
  53. package/dist/components/image-preview/style.js.map +1 -1
  54. package/dist/components/index.d.ts +1 -0
  55. package/dist/components/index.js +39 -38
  56. package/dist/components/input/index.d.ts +2 -0
  57. package/dist/components/input/index.js +12 -6
  58. package/dist/components/input/index.js.map +1 -1
  59. package/dist/components/input/style.js.map +1 -1
  60. package/dist/components/input-number/index.js +15 -15
  61. package/dist/components/layout/index.js +9 -9
  62. package/dist/components/layout/index.js.map +1 -1
  63. package/dist/components/layout/style.js +1 -1
  64. package/dist/components/layout/style.js.map +1 -1
  65. package/dist/components/popover/index.d.ts +8 -2
  66. package/dist/components/popover/index.js +53 -55
  67. package/dist/components/popover/index.js.map +1 -1
  68. package/dist/components/radio/index.d.ts +1 -1
  69. package/dist/components/radio/index.js +22 -21
  70. package/dist/components/radio/index.js.map +1 -1
  71. package/dist/components/radio/style.js +1 -1
  72. package/dist/components/radio/style.js.map +1 -1
  73. package/dist/components/radio-group/index.d.ts +3 -0
  74. package/dist/components/radio-group/index.js +27 -16
  75. package/dist/components/radio-group/index.js.map +1 -1
  76. package/dist/components/radio-group/style.js +1 -1
  77. package/dist/components/radio-group/style.js.map +1 -1
  78. package/dist/components/segmented/index.d.ts +4 -1
  79. package/dist/components/segmented/index.js +42 -29
  80. package/dist/components/segmented/index.js.map +1 -1
  81. package/dist/components/segmented/style.js +1 -1
  82. package/dist/components/segmented/style.js.map +1 -1
  83. package/dist/components/segmented-trigger/style.js +1 -1
  84. package/dist/components/segmented-trigger/style.js.map +1 -1
  85. package/dist/components/select/index.d.ts +2 -0
  86. package/dist/components/select/index.js +38 -34
  87. package/dist/components/select/index.js.map +1 -1
  88. package/dist/components/slider/index.js +11 -11
  89. package/dist/components/slider/index.js.map +1 -1
  90. package/dist/components/slider/style.js +1 -1
  91. package/dist/components/slider/style.js.map +1 -1
  92. package/dist/components/svg-draw-lines/index.d.ts +13 -2
  93. package/dist/components/svg-draw-lines/index.js +44 -31
  94. package/dist/components/svg-draw-lines/index.js.map +1 -1
  95. package/dist/components/switch/index.js +12 -12
  96. package/dist/components/switch/index.js.map +1 -1
  97. package/dist/components/switch/style.js.map +1 -1
  98. package/dist/components/textarea/index.js +6 -6
  99. package/dist/components/textarea/index.js.map +1 -1
  100. package/dist/components/textarea/style.js.map +1 -1
  101. package/dist/components/theme/index.d.ts +15 -0
  102. package/dist/components/theme/index.js +178 -21
  103. package/dist/components/theme/index.js.map +1 -1
  104. package/dist/components/theme/style.js +1 -1
  105. package/dist/components/theme/style.js.map +1 -1
  106. package/dist/components/toast/index.d.ts +1 -1
  107. package/dist/components/toast/manager.d.ts +1 -3
  108. package/dist/components/toast/manager.js +92 -49
  109. package/dist/components/toast/manager.js.map +1 -1
  110. package/dist/components/toast/style.js.map +1 -1
  111. package/dist/components/toast/toast.d.ts +37 -0
  112. package/dist/components/toast/toast.js +74 -21
  113. package/dist/components/toast/toast.js.map +1 -1
  114. package/dist/components/toast/types.d.ts +0 -4
  115. package/dist/components/tooltip/index.d.ts +6 -1
  116. package/dist/components/tooltip/index.js +21 -19
  117. package/dist/components/tooltip/index.js.map +1 -1
  118. package/dist/shared/gesture/drag-gesture.js +66 -64
  119. package/dist/shared/gesture/drag-gesture.js.map +1 -1
  120. package/dist/shared/gesture/physics.js +7 -29
  121. package/dist/shared/gesture/physics.js.map +1 -1
  122. package/dist/shared/menu-behavior/index.js +3 -4
  123. package/dist/shared/menu-behavior/index.js.map +1 -1
  124. package/dist/shared/option-portal/combobox-shell.js +48 -7
  125. package/dist/shared/option-portal/combobox-shell.js.map +1 -1
  126. package/dist/shared/option-portal/index.js +9 -9
  127. package/dist/shared/overlay/anchored-panel.js +36 -31
  128. package/dist/shared/overlay/anchored-panel.js.map +1 -1
  129. package/dist/shared/overlay/native-dialog-presence.js +16 -0
  130. package/dist/shared/overlay/native-dialog-presence.js.map +1 -1
  131. package/dist/shared/overlay/nested-drawer-layers.js +7 -4
  132. package/dist/shared/overlay/nested-drawer-layers.js.map +1 -1
  133. package/dist/shared/overlay/open-overlay.js +198 -0
  134. package/dist/shared/overlay/open-overlay.js.map +1 -0
  135. package/dist/shared/overlay/presence.js +19 -22
  136. package/dist/shared/overlay/presence.js.map +1 -1
  137. package/dist/shared/theme/duration.js +9 -0
  138. package/dist/shared/theme/duration.js.map +1 -0
  139. package/dist/shared/theme/reduced-motion.js +15 -0
  140. package/dist/shared/theme/reduced-motion.js.map +1 -0
  141. package/dist/shared/theme/root-sync.js +42 -0
  142. package/dist/shared/theme/root-sync.js.map +1 -0
  143. package/package.json +1 -1
  144. package/dist/shared/overlay/composition.js +0 -57
  145. package/dist/shared/overlay/composition.js.map +0 -1
  146. package/dist/shared/overlay/lifecycle.js +0 -41
  147. package/dist/shared/overlay/lifecycle.js.map +0 -1
  148. package/dist/vite.svg +0 -1
package/README.CN.md CHANGED
@@ -245,6 +245,7 @@ dropdown、tooltip)不需要它。
245
245
  | --------------------- | --------------------------------------------------------- |
246
246
  | **表单控件** | [`<web-ui-input>`](#web-ui-input) |
247
247
  | | [`<web-ui-textarea>`](#web-ui-textarea) |
248
+ | | [`<web-ui-editable-text>`](#web-ui-editable-text) |
248
249
  | | [`<web-ui-input-number>`](#web-ui-input-number) |
249
250
  | | [`<web-ui-select>`](#web-ui-select) |
250
251
  | | [`<web-ui-autocomplete>`](#web-ui-autocomplete) |
@@ -307,6 +308,8 @@ dropdown、tooltip)不需要它。
307
308
 
308
309
  **事件:** `input`, `change`, `focus`, `blur`
309
310
 
311
+ **方法:** `focus()`, `blur()` —— 委托到内部原生 input(宿主自身不可聚焦)
312
+
310
313
  **插槽:** `prefix`, `default`, `suffix`
311
314
 
312
315
  **CSS 自定义属性:**
@@ -351,6 +354,41 @@ dropdown、tooltip)不需要它。
351
354
  | `--wui-textarea-width` | `200px` | 文本域宽度 |
352
355
  | `--wui-textarea-clear-color` | `var(--wui-color-text-tertiary)` | 清除按钮颜色 |
353
356
 
357
+ #### `<web-ui-editable-text>`
358
+
359
+ 行内纯文本编辑器:点击文字就地编辑,`Enter` 与 `blur` 提交,`Escape` 取消。文本层与编辑层共用一个盒,进入编辑态不会移动任何一个像素。
360
+
361
+ | 属性 | 类型 | 默认值 | 说明 |
362
+ | ------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------- |
363
+ | `value` | `string` | `''` | 当前值;首次连接时捕获声明式初值作为 `form.reset()` 默认,连接后修改 attribute 或 property 均不更新该初值 |
364
+ | `placeholder` | `string` | `''` | 值为空时显示的占位文本 |
365
+ | `name` | `string` | `''` | 表单字段名 |
366
+ | `disabled` | `boolean` | `false` | 禁用状态;只影响行为,不做视觉置灰 |
367
+ | `aria-label` | `string` | — | 无障碍标签 |
368
+
369
+ **事件:** `input`(每次输入)、`change`(提交)、`cancel`(取消;与原生 `<dialog>` 的 `cancel` 同名,不冒泡、不组合,只在组件本身派发)。React 没有覆盖 `cancel` 的合成事件,须用 `addEventListener('cancel', ...)` 监听
370
+
371
+ **方法:** `focus()`, `blur()`, `select()`
372
+
373
+ 点击时光标落在点击处;键盘聚焦时落在文本末尾。`Enter` 与 `blur` 均提交草稿并恰好派发一次 `change`:`Enter` 不插入换行,并把焦点交还宿主;`blur` 不干预焦点,焦点留在用户移往的位置。只有 `Escape` 取消:值回到进入编辑时的状态,派发 `cancel` 而不派发 `change`,焦点交还宿主,且按键由编辑层消费,外层浮层(抽屉、菜单)不会因同一次按键关闭。`cancel` 不冒泡也不组合,只在组件本身派发:组件被投映在浮层 shadow 内时(如 drawer 标题),它不会触达浮层的原生 `cancel` 关闭管线,监听一律挂在组件本身。值里已有的换行仍按多行渲染,只是不能再靠输入 `Enter` 增加换行。空值继续显示 placeholder;编辑层始终按自身内容撑高,因此空草稿或纯空格草稿在宿主自身塌缩的场合(flex 项 `min-width: 0`、表格单元格)也仍有承接光标的位置。
374
+
375
+ `select()` 进入编辑态并全选内容;已在编辑态时只重新全选。`disabled` 时与 `focus()` 一样不产生效果。
376
+
377
+ 宿主是行内级盒子:未设宽度时随内容伸缩,折行后高度按行数增长。字体、颜色、文本对齐与空白处理全部继承外部上下文,因此编辑前它就是一段普通文字。
378
+
379
+ 光标颜色默认跟随共享语义 token `--wui-color-accent`;在宿主或任意祖先上设置该 token 即可整体改色。两层对长无空格串(连续字母数字)按同一组断行点折行:超出盒宽的长串在盒内断开,文字态不再溢出宿主盒。
380
+
381
+ **CSS 自定义属性:**
382
+
383
+ | 属性 | 默认值 | 说明 |
384
+ | ----------------------------------- | ---------- | ---------------------------------------------------------------- |
385
+ | `--wui-editable-text-white-space` | `pre-wrap` | 两层的空白处理方式 |
386
+ | `--wui-editable-text-overflow-wrap` | `anywhere` | 两层的断词处理方式;默认让长无空格串在盒内断开,与原生编辑层一致 |
387
+
388
+ 共用盒子带来两个约束:`line-height` 需不小于 `1`,更紧凑时原生编辑层内容会高出自身盒子,文字被顶偏 1px;`nowrap` 加固定宽度时,超出盒宽的文案在文字态溢出显示、在编辑态于盒内滚动。
389
+
390
+ **交互变更:** `blur` 恢复提交:`Enter` 与 `blur` 均提交草稿,只有 `Escape` 取消,值回到进入编辑时的状态。`Enter` 仍不插入换行。`cancel` 事件不再冒泡:此前冒泡的自定义 `cancel` 会经 slot 进入外层浮层 shadow 内的 `<dialog>`(`composed` 与否都会),被当成关闭请求一并关掉(issue #159);监听请挂在组件本身。
391
+
354
392
  #### `<web-ui-input-number>`
355
393
 
356
394
  数字输入框,支持步进按钮和键盘操作。
@@ -429,10 +467,26 @@ Portal 面板创建时会镜像 host 上解析后的这些变量;更新 host
429
467
 
430
468
  **事件:** `input`, `change`, `focus`, `blur`, `open-change` (`CustomEvent<{ open: boolean }>`)
431
469
 
432
- **插槽:** `default`(投影 `<web-ui-option>` 元素)、`empty`(替换无匹配空态;默认回退为“无匹配选项”)
470
+ **方法:** `focus()`, `blur()`
471
+
472
+ **插槽:** `default`(投影 `<web-ui-option>` 元素)、`trigger`(自定义触发器内容——替换默认输入框)、`empty`(替换无匹配空态;默认回退为“无匹配选项”)
433
473
 
434
474
  键入时按 label 过滤候选(`contains` 或 `prefix`,`none` 关闭过滤)。选择 option 时文本回填为该项 label,`selected-value` 暴露该项的 value;`change` 在选择提交时触发。支持 ArrowDown/ArrowUp/Enter/Escape 键盘导航。
435
475
 
476
+ **触发器:** 默认触发器是 shadow 内的 `web-ui-input`。把任意可编辑组件放进 `trigger` slot 即可替换它——包装 div 继续承载 combobox ARIA,并以 `data-custom-trigger` 标记当前使用自定义触发器,浮层始终以触发器元素为锚点。组件的 `focus()` / `blur()` 委托到当前生效的触发器:`web-ui-input` 与 `web-ui-textarea` 会把焦点落到内部原生控件;自定义触发器没有自己的 focus 重定向时,按宿主自身聚焦。
477
+
478
+ ```html
479
+ <web-ui-autocomplete placeholder="描述问题">
480
+ <web-ui-textarea slot="trigger" rows="3"></web-ui-textarea>
481
+ <web-ui-option value="bug" label="缺陷"></web-ui-option>
482
+ <web-ui-option value="feature" label="需求"></web-ui-option>
483
+ </web-ui-autocomplete>
484
+ ```
485
+
486
+ 自定义触发器与默认触发器共用同一份契约:字符串 `value` 承载文本、可聚焦,并派发组件委托监听的事件(`input`、`click`、`focus`、`blur`)。`web-ui-input` 与 `web-ui-textarea` 开箱即用;自定义元素只要暴露字符串 `value` property 即可接入。包装 div 不占 tab 位(`tabindex="-1"`):顺序焦点归触发器自身,自定义触发器必须可聚焦,键盘用户才能到达 combobox。
487
+
488
+ 多行触发器(可编辑元素为 `<textarea>`)保留 Enter 换行语义:Enter 不会选中高亮项,也不会提交 custom value,关闭面板用 Escape 或 blur。选择 option 仍会把该项 label 回写到触发器。单行自定义触发器保持默认的 Enter 语义。面板打开时 ArrowUp/ArrowDown 适用同一例外:方向键移动文本光标而不导航候选,该状态下键盘无法导航 option——用指针点击选择。面板关闭时 ArrowDown/ArrowUp 仍可打开面板。
489
+
436
490
  启用 `allow-custom-value` 后,无匹配且无活动 option 时,Enter 会把当前输入原文作为 custom value 提交并关闭面板;`change` 会触发,`selected-value` 保持为空。组件不会自动创建 option,也不会 trim 原文。命中禁用 option 的文本不会绕过禁用语义,也不会派生为已选 option。
437
491
 
438
492
  通过 `<div slot="empty">…</div>` 自定义静态、非交互的空态内容。Portal 渲染时该节点会迁入浮层,关闭后恢复到宿主。
@@ -478,8 +532,7 @@ Portal 面板创建时会镜像 host 上解析后的这些变量;更新 host
478
532
  | `--wui-slider-height` | `var(--wui-slider-track-size, 6px)` | 轨道厚度 |
479
533
  | `--wui-slider-track-size` | `6px` | 轨道尺寸 |
480
534
  | `--wui-slider-thumb-width` | `24px` | 滑块宽度 |
481
- | `--wui-slider-thumb-height` | `18px` | 滑块高度 |
482
- | `--wui-slider-thumb-radius` | `8px` | 滑块圆角 |
535
+ | `--wui-slider-thumb-height` | `20px` | 滑块高度 |
483
536
  | `--wui-slider-marks-inset` | `0` | 刻度内缩 |
484
537
 
485
538
  #### `<web-ui-checkbox>`
@@ -500,6 +553,12 @@ Portal 面板创建时会镜像 host 上解析后的这些变量;更新 host
500
553
 
501
554
  使用 `role="checkbox"` 和 `aria-checked`。Enter/Space 键盘切换。
502
555
 
556
+ **布局:** 宿主是 inline-flex 盒,高度由内容撑开、不继承页面行高,因此不会在指示器上下留出多余缝隙;`--wui-selection-control-size`(`18px`)决定指示器宽高,宿主与相邻文字的对齐固定为 `vertical-align: middle`。`<web-ui-radio>` 共用同一套契约。
557
+
558
+ **选中动画:** 对勾是控件自持的描边路径(不再走 `<web-ui-icon>` 图标资产),外层包 `<web-ui-svg-draw-lines>` 并带 `no-autoplay`,所以挂载时就已勾选的控件显示静态勾。勾选时线条自左向右按恒定笔速画出,时长取 `--wui-duration-trigger`(默认 160ms),在切换当下从已生效的主题解析,和指示器底色那条 transition 落在同一拍;取消时沿同一条路径收回到空白,而不是只淡出;主题范围为 `motion="reduced"` 时两者都跳过,两个状态直接切换。
559
+
560
+ **未激活态:** 未选中指示器的底色取 `--wui-color-surface-control`(与中性按钮同一档控件底),深色模式下也能和 `--wui-color-page` 分辨开。触发区内任意位置(指示器、间距或右侧 slot 标签)被 hover 时,该底色再叠 6% 状态层。hover 只在 `(hover: hover) and (pointer: fine)` 设备上生效;没有按下态,已选中和禁用态保持各自底色。`<web-ui-radio>` 共用同一套状态。
561
+
503
562
  #### `<web-ui-radio>`
504
563
 
505
564
  单个单选按钮。
@@ -516,6 +575,8 @@ Portal 面板创建时会镜像 host 上解析后的这些变量;更新 host
516
575
 
517
576
  **插槽:** `default`(标签文本)
518
577
 
578
+ **布局:** 与 `<web-ui-checkbox>` 共用同一套选择控件盒契约——宿主高度由内容撑开、不继承页面行高,指示器宽高走 `--wui-selection-control-size`,与相邻文字按 `vertical-align: middle` 对齐。
579
+
519
580
  #### `<web-ui-switch>`
520
581
 
521
582
  开关切换。
@@ -537,56 +598,97 @@ Portal 面板创建时会镜像 host 上解析后的这些变量;更新 host
537
598
 
538
599
  分段控制——单选按钮组。
539
600
 
540
- | 属性 | 类型 | 默认值 | 说明 |
541
- | ---------- | --------- | ------- | ---------------- |
542
- | `value` | `string` | `''` | 当前选中值 |
543
- | `name` | `string` | `''` | 表单字段名 |
544
- | `disabled` | `boolean` | `false` | 禁用全部 trigger |
545
- | `required` | `boolean` | `false` | 必填校验 |
601
+ | 属性 | 类型 | 默认值 | 说明 |
602
+ | ---------- | ------------------- | ------- | ---------------------- |
603
+ | `value` | `string` | `''` | 当前选中值 |
604
+ | `name` | `string` | `''` | 表单字段名 |
605
+ | `disabled` | `boolean` | `false` | 禁用全部 trigger |
606
+ | `required` | `boolean` | `false` | 必填校验 |
607
+ | `variant` | `inset` \| `raised` | `inset` | 轨道与静止指示器的材质 |
546
608
 
547
609
  **事件:** `input`, `change`
548
610
 
549
611
  **插槽:** `default`(投影 `<web-ui-segmented-trigger>` 元素)
550
612
 
613
+ **变体:** `inset`(默认,「凹」)是不透明实体轨道(`--wui-color-surface`)——1px 描边环与投影全程常驻,静止指示器是 `--wui-color-surface-segmented` 纯灰底,视觉上嵌入轨道。浅色下该 token 与 `--wui-color-page` 同值,轨道与页面齐平,凹感只由常驻环与投影承担;深色下它是「比页面高一档」的 elevation,轨道依旧浮离页面。`raised`(「凸」)是经典形态:flat 灰轨道(`--wui-color-surface-segmented`,无环无投影),静止指示器是 `--wui-color-surface-selected` 实体底加柔投影,浮起于轨道之上。两个变体的按压/拖拽态一致——指示器转透明玻璃(backdrop blur、80ms 淡入的描边环、1.5x 放大),松开淡回各自静止面。非法值回退 `inset`。
614
+
551
615
  与原生 `<form>` 集成(通过 `ElementInternals`)。
552
616
 
553
617
  根据 `value` 同步子 trigger 的 `checked` 状态。`disabled` 提供继承的有效禁用状态,不改写 trigger 自身的 `disabled` 属性。直接设 `value` 不派发事件。
554
618
 
555
619
  按住当前选中项横向拖拽可滑动指示器,松手吸附到最近选项(快速抛掷按速度切换)。触摸面声明 `touch-action: none`,拖拽激活期间阻止 `touchmove` 默认滚动,避免 iOS Safari 中断手势。
556
620
 
621
+ **按压态:** 轨道在所有状态下都是不透明实体面(`--wui-color-surface`,浅色即 page 同色、无 backdrop filter),1px 描边环与投影全程不变;转为玻璃的是指示器这一层。静止态指示器是 `--wui-color-surface-segmented` 实色底、玻璃输出全部关闭;按压/拖拽态获得 backdrop blur、随 80ms 过渡淡入的玻璃描边环与 1.5x 放大,底色全透明:指示器的 blur 采样面整个落在轨道之内,此前的玻璃轨道上全透明指示器在均匀深底与相邻轨道仅差 +0.5 阶,条纹底实测指示器内部灰度 std 1.89(背景渗透);轨道实体化后同一采样 std 归零,按压态本体的可感知性由实色轨道与放大/环/投影共同承担,不再需要 tint 打底或压制轨道边缘装饰。底色不淡入——按下第一帧即落定;松开时指示器经 `--wui-duration-press` 淡回实色灰底。
622
+
623
+ **文字色:** 选中与未选中项文字一律保持 `--wui-color-text-secondary`——`inset`/`raised` 两个变体一致,静止态与按压/拖拽态一致。选中态只由指示器承担,任何标签都不着 `--wui-color-accent`。
624
+
557
625
  #### `<web-ui-checkbox-group>`
558
626
 
559
627
  多选复选框组。
560
628
 
561
- | 属性 | 类型 | 默认值 | 说明 |
562
- | ---------- | ---------- | ------- | -------------- |
563
- | `value` | `string[]` | `[]` | 已选项的值数组 |
564
- | `name` | `string` | `''` | 表单字段名 |
565
- | `disabled` | `boolean` | `false` | 禁用全部子项 |
566
- | `required` | `boolean` | `false` | 必填校验 |
629
+ ##### 属性
630
+
631
+ | 属性 | 类型 | 默认值 | 说明 |
632
+ | ----------- | ---------------------------- | ------------ | -------------- |
633
+ | `value` | `string[]` | `[]` | 已选项的值数组 |
634
+ | `name` | `string` | `''` | 表单字段名 |
635
+ | `disabled` | `boolean` | `false` | 禁用全部子项 |
636
+ | `required` | `boolean` | `false` | 必填校验 |
637
+ | `direction` | `'horizontal' \| 'vertical'` | `'vertical'` | 布局方向 |
567
638
 
568
639
  **事件:** `input`, `change`
569
640
 
570
641
  **插槽:** `default`(投影 `<web-ui-checkbox>` 元素)
571
642
 
572
- 同步子 checkbox 的 `checked` 状态。`disabled` 提供继承的有效禁用状态,不改写子项自身的 `disabled` 属性。监听子项 `change` 事件。
643
+ 同步子 checkbox 的 `checked` 状态。`disabled` 提供继承的有效禁用状态,不改写子项自身的 `disabled` 属性。监听子项 `change` 事件。`direction` 传入非法值时回退为 `vertical`。
644
+
645
+ ##### Token
646
+
647
+ | Token | 默认值 | 说明 |
648
+ | -------------------------- | ------ | ------------------------ |
649
+ | `--wui-checkbox-group-gap` | `8px` | 成员 checkbox 之间的间距 |
650
+
651
+ 在 group 元素本身,或同一个 `<web-ui-theme>` 作用域内的祖先元素上设置。theme 宿主会为自己的子树声明 `8px`,写在 `<web-ui-theme>` 元素之外的覆盖到不了 group。
652
+
653
+ ```css
654
+ web-ui-checkbox-group {
655
+ --wui-checkbox-group-gap: 16px;
656
+ }
657
+ ```
573
658
 
574
659
  #### `<web-ui-radio-group>`
575
660
 
576
661
  单选组。
577
662
 
578
- | 属性 | 类型 | 默认值 | 说明 |
579
- | ---------- | --------- | ------- | ------------------------ |
580
- | `value` | `string` | `''` | 当前选中值 |
581
- | `name` | `string` | `''` | 表单字段名(传递到子项) |
582
- | `disabled` | `boolean` | `false` | 禁用全部子项 |
583
- | `required` | `boolean` | `false` | 必填校验 |
663
+ ##### 属性
664
+
665
+ | 属性 | 类型 | 默认值 | 说明 |
666
+ | ----------- | ---------------------------- | ------------ | ------------------------ |
667
+ | `value` | `string` | `''` | 当前选中值 |
668
+ | `name` | `string` | `''` | 表单字段名(传递到子项) |
669
+ | `disabled` | `boolean` | `false` | 禁用全部子项 |
670
+ | `required` | `boolean` | `false` | 必填校验 |
671
+ | `direction` | `'horizontal' \| 'vertical'` | `'vertical'` | 布局方向 |
584
672
 
585
673
  **事件:** `input`, `change`
586
674
 
587
675
  **插槽:** `default`(投影 `<web-ui-radio>` 元素)
588
676
 
589
- `disabled` 提供继承的有效禁用状态,不改写子项自身的 `disabled` 属性。
677
+ `disabled` 提供继承的有效禁用状态,不改写子项自身的 `disabled` 属性。`direction` 传入非法值时回退为 `vertical`。
678
+
679
+ ##### Token
680
+
681
+ | Token | 默认值 | 说明 |
682
+ | ----------------------- | ------ | --------------------- |
683
+ | `--wui-radio-group-gap` | `8px` | 成员 radio 之间的间距 |
684
+
685
+ 在 group 元素本身,或同一个 `<web-ui-theme>` 作用域内的祖先元素上设置。theme 宿主会为自己的子树声明 `8px`,写在 `<web-ui-theme>` 元素之外的覆盖到不了 group。
686
+
687
+ ```css
688
+ web-ui-radio-group {
689
+ --wui-radio-group-gap: 16px;
690
+ }
691
+ ```
590
692
 
591
693
  ---
592
694
 
@@ -665,6 +767,8 @@ Portal 面板创建时会镜像 host 上解析后的这些变量;更新 host
665
767
 
666
768
  使用原生 `<dialog>`,`@cancel` 阻止默认关闭行为。除非存在 `no-escape-close`,否则 Escape 调用 `close()`;除非存在 `no-backdrop-close`,否则点击遮罩关闭。启用 `controlled` 后,两者都只派发关闭请求而不自关闭。
667
769
 
770
+ > **Escape 归属**:Escape 由共享仲裁者统一判定,一次按键只关闭**最内层**的已打开浮层(popover、select、autocomplete、dropdown、context-menu、drawer、dialog 都参与)。例如在 drawer 内打开 select,第一次 Escape 只关 select,第二次才关 drawer。互不嵌套的并列浮层按打开顺序关闭最上层。正在播退场过渡的面板仍在场上,也仍由它接住 Escape——但只要还有别的浮层开着,那一次按键仍然归该层,「一次按键关一层」不因此改变。`image-preview` 同样参与:它的原生 `<dialog>` 会登记进同一个仲裁者,Escape 按层级判定;组件的 `cancel` handler 只是拦掉原生的瞬时关闭,把 top layer 保留到退场过渡结束。
771
+
668
772
  **CSS 自定义属性:**
669
773
 
670
774
  | 属性 | 默认值 | 说明 |
@@ -678,7 +782,7 @@ Portal 面板创建时会镜像 host 上解析后的这些变量;更新 host
678
782
  | `--wui-dialog-desc-gap` | `24px` | 正文内容下方间距 |
679
783
  | `--wui-dialog-footer-gap` | `10px` / horizontal `12px` | Footer 按钮间距 |
680
784
  | `--wui-dialog-footer-justify` | `flex-end`(默认)/ `center`(horizontal) | Footer `justify-content`;horizontal 模式下覆盖为 `flex-end` 可右对齐 |
681
- | `--wui-dialog-scale-enter` | `1.2` | 进场缩放起点:由 `1.2` 收缩到 `1`,退场反向 |
785
+ | `--wui-dialog-scale-enter` | `1.1` | 进场缩放起点:由 `1.1` 收缩到 `1`,退场反向 |
682
786
 
683
787
  #### `<web-ui-drawer>`
684
788
 
@@ -712,10 +816,18 @@ Portal 面板创建时会镜像 host 上解析后的这些变量;更新 host
712
816
  **拖拽关闭:** 启用 `draggable` 后,打开的抽屉在内缘显示灰色胶囊 drag bar(默认 4×56px,视觉中线距内缘 10px,位于 20px 厚的命中热区内;`right` 在左缘、`left` 在右缘、`top` 在下缘、`bottom` 在上缘):
713
817
 
714
818
  - 拖拽实时跟手,遮罩透明度按比例淡出。
715
- - 松手时位移超过抽屉尺寸约 1/3 或快速甩动即弹簧关闭,否则弹回打开位;方向随 placement 适配。
819
+ - 遮罩点击关闭只认**轻点链路**:按下起点在遮罩上、且按-放位移在轻点量级内的 click 才关闭。浏览器对「按下 → 拖动 → 松手」生成的 click 落在起点与松手点的共同祖先(dialog)上——从面板内容或遮罩上开始拖拽、松手落在遮罩时,click 的 target 同样是 dialog;组件在 `pointerdown` 记录起点与坐标做回溯校验,这类拖拽松手一律弹回。`detail` 为 0 的 click(键盘/程序化来源)不消费指针记录。
820
+ - 松手时自抓取瞬间起的**净位移**超过抽屉尺寸的一半(下限 10px)或快速甩动即关闭,否则弹回打开位;方向随 placement 适配。判定的每一环都与 Base UI `useSwipeDismiss` 对齐:
821
+ - 位移零点与判定时钟都在**首个 `pointermove`** 处重置,用来吸收「按下 → 首个 move」之间的空隙;该次 move 的整程位移被整体丢弃,因此一次手势至少要两个 move 才可能累积出位移。
822
+ - 甩动看的是**整段手势**的平均速度(净位移 ÷ 手势时长,分母下限 50ms)**达到** 500px/s,不是释放瞬间的瞬时速度。滑窗只描述最后一小段轨迹;在整段平均速度下,「先往边缘拖出、再快速扫回」不再能凑成甩动。时长为 0 时速度取 0 而不是按 50ms 兜底,因此测不到时长的手势永远不会被读成甩动。
823
+ - 净位移没有朝闭合方向的松手一律弹回打开位。
824
+ - 距离判据同样用净位移:从弹回途中抓取后不再拖动就松手,不会被当成「已经拖过一半」。
825
+ - 自「方向确认点」起反向回撤满 10px 即取消本次甩动,因此拖出后回扫不会关闭。位移重新越过距离阈值会清除该标记,所以「已经拖过一半」不受小幅回撤影响。上游有同一个守卫(`cancelledSwipe`),但在 drawer 这条路径上永远走不到——`DrawerViewport.onRelease` 必然返回一个决定。
826
+ - 朝打开方向的拖拽按平方根压缩(`sign(d) · |d| ** 0.5`),对齐 `applyDirectionalDamping`。阻尼作用于本次手势的**增量**、叠加在抓取瞬间的位移之上,因此自限幅而不需要固定的位移上限。
827
+ - 释放速度只决定收尾过渡的时长(180–420ms);收尾本身是一次交还 CSS 的 `transform` 过渡,不再创建 WAAPI 动画。
716
828
  - 启用 `controlled` 后,超过阈值松手仅派发 `open-change(false)`;抽屉在闭合位短暂等待(120ms 回写窗口),Consumer 拒绝或超时未回写时弹回打开位。
717
829
  - 不支持拖拽打开——关闭态的抽屉在原生 dialog 之外没有任何渲染物。
718
- - `prefers-reduced-motion` 下松手即时到位,不播放弹簧动画。
830
+ - `prefers-reduced-motion` 下松手即时到位,不播放收尾过渡。
719
831
 
720
832
  **CSS 自定义属性:**
721
833
 
@@ -732,7 +844,7 @@ Portal 面板创建时会镜像 host 上解析后的这些变量;更新 host
732
844
  | `--wui-drawer-drag-bar-length` | `56px` | Drag bar 胶囊长度(沿抽屉边缘) |
733
845
  | `--wui-drawer-header-padding` | `16px 20px` | Header 区域 padding |
734
846
  | `--wui-drawer-close-top` | `16px` | 内置关闭按钮相对 header 顶部的偏移 |
735
- | `--wui-drawer-close-right` | `20px` | 内置关闭按钮相对抽屉右缘的偏移 |
847
+ | `--wui-drawer-close-right` | `16px` | 内置关闭按钮相对抽屉右缘的偏移 |
736
848
  | `--wui-drawer-content-padding` | `20px` | 内容区 padding;同时驱动 drag bar 视觉中线(取半值) |
737
849
  | `--wui-drawer-footer-padding` | `16px 20px` | Footer 区域 padding |
738
850
 
@@ -1190,12 +1302,14 @@ WebUiSpinner.hide() // 隐藏
1190
1302
 
1191
1303
  `header-glow` 会在 header 插槽内容和移动端 Toggle 的背后添加 `pointer-events: none` 的装饰性晕染。它属于 Header 背景而非前景层,因此插槽内容始终位于其上方;可通过 `--wui-layout-header-glow-color` 覆盖颜色,默认值为 `--wui-color-page`。晕染浓度和范围由内部变量 `--wui-layout-header-glow-height`(默认 `150%`)控制;增大可加强覆盖,减小则更柔和。布局层级顺序为 Header(`10`)< Auxiliary(`20`)< Banner(`30`)< Tabbar(`40`)< Sidebar(`50`)。
1192
1304
 
1305
+ 侧边栏卡片表面使用 `--wui-color-surface-sidebar`:浅色与共享的 `--wui-color-surface-overlay` 同值,深色比 `--wui-color-page` 浅一档、与 `--wui-color-surface` 同级。它独立成 token,是因为 dialog、drawer 和 toast 共用 `--wui-color-surface-overlay`,不随侧边栏一起抬升。
1306
+
1193
1307
  **CSS 自定义属性:**
1194
1308
 
1195
- | 属性 | 默认值 | 说明 |
1196
- | ---------------------------------- | --------------------------------- | ------------------------------------ |
1197
- | `--wui-layout-sidebar-radius` | `var(--wui-radius-overlay, 28px)` | 侧边栏卡片圆角(桌面端和移动端共用) |
1198
- | `--wui-layout-mobile-toggle-inset` | `8px` | 移动端 header Toggle 的左缩进 |
1309
+ | 属性 | 默认值 | 说明 |
1310
+ | ---------------------------------- | ------ | ------------------------------------ |
1311
+ | `--wui-layout-sidebar-radius` | `24px` | 侧边栏卡片圆角(桌面端和移动端共用) |
1312
+ | `--wui-layout-mobile-toggle-inset` | `8px` | 移动端 header Toggle 的左缩进 |
1199
1313
 
1200
1314
  #### `<web-ui-back-top>`
1201
1315
 
@@ -1229,20 +1343,25 @@ WebUiSpinner.hide() // 隐藏
1229
1343
 
1230
1344
  #### `<web-ui-svg-draw-lines>`
1231
1345
 
1232
- SVG 线条绘制动画,基于 `stroke-dashoffset`。直接在原元素上动画 —— 不克隆、不操作 DOM。
1346
+ SVG 线条绘制动画,基于 `stroke-dashoffset`。直接在原元素上动画 —— 不克隆、不操作 DOM。两个播放方向:默认按从无到有描出几何形状,`replay({ reverse: true })` 沿原路收回,从有到无。
1347
+
1348
+ | 属性 | 类型 | 默认值 | 说明 |
1349
+ | ------------- | --------- | ---------- | ---------------------------------------- |
1350
+ | `duration` | `number` | `1000` | 动画时长(毫秒),限制在 `[0, 30000]` |
1351
+ | `easing` | `string` | `'linear'` | CSS 缓动函数,传递给 `element.animate()` |
1352
+ | `no-autoplay` | `boolean` | `false` | 关闭内容首次出现时的自动播放 |
1233
1353
 
1234
- | 属性 | 类型 | 默认值 | 说明 |
1235
- | ---------- | -------- | ---------- | -------------------------------------------- |
1236
- | `duration` | `number` | `1000` | 动画时长(毫秒),限制在 `[0, 30000]` 范围内 |
1237
- | `easing` | `string` | `'linear'` | CSS 缓动函数,传递给 `element.animate()` |
1354
+ 三个属性均会反射(reflected)。
1238
1355
 
1239
- 两个属性均会反射(reflected)。
1356
+ **自动播放:** slot 内容第一次稳定时自动播放一次画入。设置 `no-autoplay` 后保持资产原样、由调用方决定何时播放 —— `<web-ui-checkbox>` 正是靠它避免挂载时就已勾选的勾在页面首帧画出来。
1240
1357
 
1241
- **方法:** `replay(): Promise<void>` — 取消当前动画,重新从 DOM 收集几何元素并开始新动画。所有目标以相同的 duration/easing 并行播放。动画全部完成后 resolve。最近主题范围为 `motion="reduced"` 时立即返回,不播放动画;`motion="system"` 则在匹配 `prefers-reduced-motion: reduce` 时执行相同行为。
1358
+ **方法:** `replay(options?: { reverse?: boolean }): Promise<void>` — 取消当前动画,重新从 DOM 收集几何元素并开始新动画。所有目标以相同的 duration/easing 并行播放。动画全部完成后 resolve。收回段以「空白」收尾,并把这一末态留在 DOM 上:动画本身在收尾时撤销,但让描边隐去的 dash 值作为内联样式保留,直到下一次 `replay()` 把它替换或撤掉。画入段则恢复消费者自己写的内联样式。收回总是从完整描边起收,所以在画入途中打断也会把整条路径描回去,而不是只收掉看得见的那一段。最近主题范围为 `motion="reduced"` 时不启动任何动画 —— 但上一次收回留下的空白仍会被撤掉,几何回到资产原样,可见性完全交给消费者自己的样式;`motion="system"` 在匹配 `prefers-reduced-motion: reduce` 时行为相同。
1359
+
1360
+ **收回末态:** 收回留下的空白是写在被动画的几何元素上的内联样式,包括嵌套开放 Shadow Root(如 `<web-ui-icon>`)里的节点。克隆这些节点(`cloneNode`、`innerHTML` 往返)会把空白一起带走;把节点摘下来再挂回去,描边也仍然是隐去的 —— 收回结束是一个 DOM 状态而不是一条还活着的动画,没有留下任何东西去复原它。要重新显示就调用 `replay()`。由于组件会递归进入嵌套的开放 Shadow Root,两层 `<web-ui-svg-draw-lines>` 不得覆盖同一批几何元素:每个实例只在第一次动画某个元素时记录它的内联 dash 值,内层留下的空白会被外层当成「消费者自己写的值」。
1242
1361
 
1243
1362
  **插槽:** `default` — 需要动画的 SVG 内容。接受内联 `<svg>` 元素(light DOM)以及将 SVG 渲染在开放 Shadow DOM 中的组件(如 `<web-ui-icon>`)。closed Shadow Root 被跳过。
1244
1363
 
1245
- 递归遍历 light DOM 和所有开放 Shadow Root,查找 `path`、`rect`、`circle`、`line`、`polyline`、`polygon`、`ellipse` 元素。以 `Z`/`z` 结尾的 `<path>` 会临时应用缺口修复逻辑确保闭合段正确渲染。动画完成或取消后恢复所有内联样式。
1364
+ 递归遍历 light DOM 和所有开放 Shadow Root,查找 `path`、`rect`、`circle`、`line`、`polyline`、`polygon`、`ellipse` 元素。以 `Z`/`z` 结尾的 `<path>` 会临时应用缺口修复逻辑确保闭合段正确渲染。画入段完成或取消后恢复所有内联样式。
1246
1365
 
1247
1366
  #### `<web-ui-theme>`
1248
1367
 
@@ -1259,8 +1378,14 @@ SVG 线条绘制动画,基于 `stroke-dashoffset`。直接在原元素上动
1259
1378
 
1260
1379
  在其子树中定义基础、颜色、层级、阴影和动效 token。`motion="system"` 跟随 `prefers-reduced-motion`;使用 `motion="reduced"` 降低当前作用域动效,或在嵌套主题中使用 `motion="full"` 恢复默认 token。System 配色模式跟随 `prefers-color-scheme`。
1261
1380
 
1381
+ 配色变化是否使用 View Transitions API 播放动画,由当前主题范围的 `motion` 档位决定:`full` 始终播放揭示,`reduced` 直接落地新 appearance、不播放揭示,`system` 跟随 `prefers-reduced-motion`。根主题揭示整页;嵌套主题只揭示自己的 capture box。圆心优先取最近一次 pointerdown 坐标,否则回退主题盒或视口中心;深浅两个方向反向播放。不支持的浏览器、reduced-motion 作用域、时长为 0 以及同一 flight 内已有未完成请求都会立即落地新 appearance;当前版本对 `appearance="system"` 的 OS 深浅翻转不做动画。
1382
+
1383
+ **从已移除的 `transition` 属性迁移:** 删掉该属性,改用 `motion` 表达意图。原先带 `transition` 的主题在默认 `motion="system"` 下揭示行为不变;需要在系统偏好 reduce 时仍播放揭示,则加 `motion="full"`。原先不带 `transition` 的主题现在默认就会揭示,若要保持无动画直接切换,请显式设置 `motion="reduced"`。
1384
+
1262
1385
  主题宿主使用 `display: contents` 且不绘制任何背景:组件库不在宿主页面画背景,嵌入方对主题子树背后的表面保留完全控制权。自定义属性仍可靠继承到 slotted 内容。
1263
1386
 
1387
+ **根节点 page 色同步:** 最外层 active theme 会把自己计算出的 `--wui-color-page` 以行内自定义属性的形式镜像到 `document.documentElement` 上。它补上的正是该 token 自己到不了的那一处——自定义属性的继承链到 documentElement 就断了,所以消费方写 `body { background: var(--wui-color-page) }` 只能给滚动区上色,橡皮筋回弹(macOS rubber-band)露出的画布区仍是 UA 背景色;有了这个镜像属性,回弹区也跟随主题。写入时机是主题连接时与 appearance 变化时(含 `appearance="system"` 跟随系统深浅翻转);写入的是该主题的计算值,因此消费者在 host 上覆盖 `--wui-color-page` 会被按当前值原样镜像。**非实时:** 组件不观察 host 自身的样式变化,因此运行中改该属性(改行内样式、切 class、host 级媒体查询)不会实时镜像到 root,要等下一个写入触发点——连接、`appearance` 变成另一个值、或系统深浅翻转;把 `appearance` 重新赋成当前值不算触发点,因为值没变时更新会被跳过。页面色需要独立于主题 appearance 变化时,消费者自己在该变量上持有它:在 `:root` 或 `body` 上用 `!important` 定义 `--wui-color-page`,重要声明在层级上压过本模块写在 documentElement 上的行内值;或者让页面色跟随 `appearance` 走。只有最外层主题会写入:嵌套主题保留自己的作用域,不改写 root 值。同步权按 connect 顺序归属,最外层断开时顺延给下一个已连接主题;最后一个主题断开时刻意保留最后值而不是移除,避免主题切换间隙闪回 UA 背景。组件库在 documentElement 上只写这一个自定义属性,不写其它内容。
1388
+
1264
1389
  **基础 token:**
1265
1390
 
1266
1391
  | 属性 | 默认值 | 说明 |
@@ -1271,6 +1396,14 @@ SVG 线条绘制动画,基于 `stroke-dashoffset`。直接在原元素上动
1271
1396
  | `--wui-overlay-min-width` | `200px` | 锚定浮层最小宽度 |
1272
1397
  | `--wui-focus-ring-width` | `3px` | Focus 指示器宽度 |
1273
1398
 
1399
+ **选择控件 token(radio、checkbox):**
1400
+
1401
+ | 属性 | 默认值 | 说明 |
1402
+ | ------------------------------ | ------ | ------------------------------------ |
1403
+ | `--wui-selection-control-size` | `18px` | 指示器(圆点 / 方框)宽高 |
1404
+ | `--wui-radio-group-gap` | `8px` | `<web-ui-radio-group>` 的成员间距 |
1405
+ | `--wui-checkbox-group-gap` | `8px` | `<web-ui-checkbox-group>` 的成员间距 |
1406
+
1274
1407
  **圆角 token:**
1275
1408
 
1276
1409
  | 属性 | 默认值 | 说明 |
@@ -1295,7 +1428,7 @@ SVG 线条绘制动画,基于 `stroke-dashoffset`。直接在原元素上动
1295
1428
  | `--wui-layer-toast` | `200` | Toast |
1296
1429
  | `--wui-layer-loading` | `300` | 阻塞式 Loading |
1297
1430
 
1298
- **动效 token:** duration 默认值为 `--wui-duration-press: 80ms`、`--wui-duration-feedback: 100ms`、`--wui-duration-trigger: 160ms`、`--wui-duration-focus: 200ms`、`--wui-duration-float-enter: 240ms`、`--wui-duration-float-exit: 160ms`、`--wui-duration-dialog-enter: 320ms`、`--wui-duration-dialog-exit: 260ms`、`--wui-duration-drawer-enter: 280ms`、`--wui-duration-drawer-exit: 240ms`、`--wui-duration-drawer-nested: 450ms`、`--wui-duration-toast-enter: 280ms`、`--wui-duration-toast-exit: 200ms`、`--wui-duration-collapse-enter: 200ms`、`--wui-duration-collapse-exit: 160ms`、`--wui-duration-layout: 200ms`、`--wui-duration-swipe-settle: 220ms`(image-preview 轮播回位)、`--wui-duration-spin: 600ms`(icon 旋转)与 `--wui-duration-spinner: 800ms`(spinner 叶片追光)。Easing token 是 `--wui-ease-enter`、`--wui-ease-dialog`(`cubic-bezier(0.2, 0, 0, 1)`)、`--wui-ease-slide`、`--wui-ease-float`(`cubic-bezier(0.4, 0.38, 0.2, 1)`,锚定浮动面板专用的无回弹弹簧拟合曲线)以及 `--wui-ease-swipe`(`cubic-bezier(0.32, 0.72, 0, 1)`,轮播回位的减速收尾);进入缩放是 `--wui-scale-enter: 0.95`(浮动面板与 image-preview 的图片舞台层由小到大展开),dialog 使用 `--wui-dialog-scale-enter: 1.2`(由大到小收缩入场,退场反向)。`motion="reduced"` 下所有转场时长归零(轮播回位一并归零),两个无限加载循环只放慢、不停(`--wui-duration-spin` / `--wui-duration-spinner`:`600ms` / `800ms` → `1600ms`)。加载指示冻结会被读成界面卡死。hover/active 背景反馈即时切换、无过渡动画;选中态、按压与 focus 走带时长的过渡,reduce 下同样归零。
1431
+ **动效 token:** duration 默认值为 `--wui-duration-press: 80ms`、`--wui-duration-feedback: 100ms`、`--wui-duration-trigger: 160ms`、`--wui-duration-focus: 200ms`、`--wui-duration-float-enter: 240ms`、`--wui-duration-float-exit: 160ms`、`--wui-duration-dialog-enter: 320ms`、`--wui-duration-dialog-exit: 260ms`、`--wui-duration-drawer-enter: 280ms`、`--wui-duration-drawer-exit: 240ms`、`--wui-duration-drawer-nested: 450ms`、`--wui-duration-toast-enter: 280ms`、`--wui-duration-toast-exit: 200ms`、`--wui-duration-collapse-enter: 200ms`、`--wui-duration-collapse-exit: 160ms`、`--wui-duration-layout: 200ms`、`--wui-duration-swipe-settle: 220ms`(image-preview 轮播回位)、`--wui-duration-spin: 600ms`(icon 旋转)、`--wui-duration-spinner: 800ms`(spinner 叶片追光)与 `--wui-theme-transition-duration: 500ms`(主题配色揭示)。Easing token 是 `--wui-ease-enter`、`--wui-ease-dialog`(`cubic-bezier(0.2, 0, 0, 1)`)、`--wui-ease-slide`、`--wui-ease-float`(`cubic-bezier(0.4, 0.38, 0.2, 1)`,锚定浮动面板专用的无回弹弹簧拟合曲线)、`--wui-ease-swipe`(`cubic-bezier(0.32, 0.72, 0, 1)`,轮播回位的减速收尾)与 `--wui-theme-transition-easing: ease-in`;进入缩放是 `--wui-scale-enter: 0.95`(浮动面板与 image-preview 的图片舞台层由小到大展开),dialog 使用 `--wui-dialog-scale-enter: 1.1`(由大到小收缩入场,退场反向)。`motion="reduced"` 下所有转场时长归零(轮播回位一并归零),两个无限加载循环只放慢、不停(`--wui-duration-spin` / `--wui-duration-spinner`:`600ms` / `800ms` → `1600ms`)。加载指示冻结会被读成界面卡死。hover/active 背景反馈即时切换、无过渡动画;选中态、按压与 focus 走带时长的过渡,reduce 下同样归零。
1299
1432
 
1300
1433
  **颜色 token:**
1301
1434
 
@@ -1306,13 +1439,14 @@ SVG 线条绘制动画,基于 `stroke-dashoffset`。直接在原元素上动
1306
1439
  | `--wui-color-surface-raised` | `#f2f2f7` | `#2c2c2e` | 抬升表面 |
1307
1440
  | `--wui-color-surface-control` | `#dfdfdf` | `#3a3a3c` | 中性可交互控件表面 |
1308
1441
  | `--wui-color-surface-track` | `#e5e5ea` | `#444446` | Slider/Switch 轨道表面 |
1309
- | `--wui-color-surface-menu` | `rgb(246 246 246 / 0.82)` | `rgb(30 30 32 / 0.92)` | Menu 和浮动面板表面 |
1442
+ | `--wui-color-surface-menu` | `rgb(246 246 246 / 0.82)` | `rgb(44 44 46 / 0.78)` | Menu 和浮动面板表面 |
1310
1443
  | `--wui-color-surface-glass` | `rgb(250 250 250 / 0.34)` | `rgb(44 44 46 / 0.42)` | 液态玻璃表面 |
1311
1444
  | `--wui-color-surface-glass-hover` | `color-mix(... text 6%, surface-glass)` | `color-mix(... text 6%, surface-glass)` | Glass 完整悬停背景 |
1312
1445
  | `--wui-color-surface-glass-active` | `color-mix(... text 15%, surface-glass)` | `color-mix(... text 15%, surface-glass)` | Glass 完整按下背景 |
1313
1446
  | `--wui-color-surface-overlay` | `rgb(246 246 246 / 0.82)` | `rgb(32 34 34 / 0.9)` | 半透明浮层表面 |
1314
- | `--wui-color-surface-segmented` | `#e5e5ea` | `#3a3a3c` | Segmented 轨道表面 |
1315
- | `--wui-color-surface-selected` | `#fff` | `#5c5c5e` | Segmented 选中滑块表面 |
1447
+ | `--wui-color-surface-sidebar` | `rgb(246 246 246 / 0.82)` | `rgb(44 44 46 / 0.8)` | 侧边栏面板表面 |
1448
+ | `--wui-color-surface-segmented` | `#e5e5ea` | `#3a3a3c` | Segmented 指示器表面 |
1449
+ | `--wui-color-surface-selected` | `#fff` | `#5c5c5e` | 选中表面 |
1316
1450
  | `--wui-color-text` | `#1b1b1b` | `#e9eaea` | 主要文本 |
1317
1451
  | `--wui-color-text-secondary` | `#6a6a6a` | `#a1a1a6` | 次要文本 |
1318
1452
  | `--wui-color-text-tertiary` | `color-mix(in srgb, var(--wui-color-text) 35%, transparent)` | `color-mix(in srgb, var(--wui-color-text) 42%, transparent)` | 三级文本和弱意图图标 |
@@ -1322,8 +1456,8 @@ SVG 线条绘制动画,基于 `stroke-dashoffset`。直接在原元素上动
1322
1456
  | `--wui-color-border` | `rgb(0 0 0 / 0.1)` | `rgb(255 255 255 / 0.14)` | 常规边框和分隔线 |
1323
1457
  | `--wui-color-glass-border` | `transparent` | `rgb(255 255 255 / 0.05)` | Glass 边框色调 |
1324
1458
  | `--wui-color-glass-highlight` | `rgb(255 255 255 / 0.9)` | `rgb(255 255 255 / 0.1)` | Glass 高光边缘 |
1325
- | `--wui-color-glass-corner` | `rgb(255 255 255 / 0.08)` | `rgb(255 255 255 / 0.2)` | Glass 边框角落光泽 |
1326
- | `--wui-color-glass-shade` | `rgb(0 0 0 / 0.06)` | `rgb(0 0 0 / 0.5)` | Glass 边框背光角压暗 |
1459
+ | `--wui-color-glass-corner` | `rgb(255 255 255 / 0.5)` | `rgb(255 255 255 / 0.2)` | Glass 边框角落光泽 |
1460
+ | `--wui-color-glass-shade` | `rgb(0 0 0 / 0.03)` | `rgb(0 0 0 / 0.5)` | Glass 边框背光角压暗 |
1327
1461
  | `--wui-color-accent` | `#08f` | `#0a84ff` | Accent 和输入焦点边框 |
1328
1462
  | `--wui-color-on-accent` | `#fff` | `#fff` | Accent 上的前景色 |
1329
1463
  | `--wui-color-on-control` | `#fff` | `#f2f2f7` | 控件内芯前景色 |
@@ -1374,10 +1508,18 @@ const id = toast({ message: '自定义', type: 'info', position: 'bottom-right',
1374
1508
  toast.close(id)
1375
1509
  toast.clear()
1376
1510
 
1377
- // 更新可见内容,不重置自动关闭计时
1378
- toast.updateMessage(id, { message: '上传已完成 60%', heading: '正在上传' })
1511
+ // upsert:同一 id 再次调用是更新那一条,而不是再建一条
1512
+ toast.error('网络连接中断,正在重试…', { id: 'network' })
1513
+ toast.error('网络连接中断(第 2 次重试)', { id: 'network' })
1514
+
1515
+ // 一条 toast 走完整个生命周期
1516
+ toast.info('上传中 0%', { id: 'upload', duration: 0 })
1517
+ toast.info('上传中 60%', { id: 'upload' }) // 省略 duration,计时不动
1518
+ toast.success('上传完成', { id: 'upload', duration: 3000 }) // 换 type,重新开始倒计时
1379
1519
  ```
1380
1520
 
1521
+ 每次调用都返回这条 toast 的最终 id,调用方无需区分新建还是更新。
1522
+
1381
1523
  **ToastOptions:**
1382
1524
 
1383
1525
  | 选项 | 类型 | 默认值 | 说明 |
@@ -1386,17 +1528,36 @@ toast.updateMessage(id, { message: '上传已完成 60%', heading: '正在上传
1386
1528
  | `type` | `'success' \| 'info' \| 'warning' \| 'error'` | `'info'` | 类型 |
1387
1529
  | `duration` | `number` | `3000`(error 为 `5000`) | 自动关闭时间(0=不自动关闭) |
1388
1530
  | `closable` | `boolean` | `true` | 显示关闭按钮 |
1389
- | `id` | `string` | auto | 去重标识符 |
1531
+ | `id` | `string` | auto | 合并键:同一 id 的调用更新同一条 |
1390
1532
  | `heading` | `string` | `''` | 粗体标题 |
1391
1533
  | `position` | 6 种位置 | `'top-right'` | 屏幕位置 |
1392
1534
  | `target` | `Element` | — | 用于查找最近 theme-owned overlay root |
1393
1535
  | `container` | `HTMLElement` | — | 显式挂载容器(最高优先级) |
1394
1536
 
1395
- **`toast.updateMessage(id, options)`** 更新可见 Toast 的 `message`,并在传入时更新 `heading`;不会重置自动关闭计时。`options` 类型为 `ToastMessageUpdateOptions`:`{ message: string; heading?: string }`。
1537
+ **upsert 语义** —— 同一 `id` 再次调用的结果:
1538
+
1539
+ | 目标状态 | 结果 |
1540
+ | -------------------- | ------------------------------------------------ |
1541
+ | 已挂载 | 返回同一个 id;给出的字段覆盖,未给出的保持原值 |
1542
+ | 同 tick 内仍在待挂载 | patch 待挂载的 options,挂载后生效,不产生第二条 |
1543
+ | 已关闭或正在退场 | 新建一条;正在离场的那条自行走完退场动画 |
1544
+
1545
+ `duration` 只有显式传入才重启倒计时(悬停暂停期间是例外:只记新值不点火,指针离开后按新时长计满);`message`、`heading`、`type` 是普通属性,不碰计时。`toast.error` 的 5000 默认值在**创建时**兜底,不算显式传入,因此重复调用 `toast.error(msg, { id })` 不会重置倒计时。`position` 变化会把元素搬到新容器并保住剩余计时(支持 `moveBefore` 的引擎直接搬,其余走暂停/续跑降级);若倒计时在主线程被占用期间已经到期,降级路径会在续跑时直接退场,而不是让它一直挂着。`container` 与 `target` 以首次调用为准,不支持把已存在的 toast 换到另一个 overlay root。
1546
+
1547
+ **关闭语义** —— `toast.close(id)` 覆盖 toast 出现的每个阶段,包括「还没开始显示」的两种:
1548
+
1549
+ | 目标状态 | 结果 |
1550
+ | --------------------- | ---------------------------------------- |
1551
+ | 已显示 | 播放退场动画,结束后派发 `toast-close` |
1552
+ | 已挂载、`show()` 未跑 | 没有退场动画可播,立即派发 `toast-close` |
1553
+ | 同 tick 仍在待挂载 | 挂载前出队:不会出现,也不派发事件 |
1554
+ | 已在退场 | 空操作,由那条 toast 自己走完退场 |
1555
+
1556
+ `toast.clear()` 的口径相同:队列条目一并取消,已挂载的全部关闭。
1396
1557
 
1397
1558
  **事件:** `toast-close` (`CustomEvent<{ id: string; reason: 'auto' | 'manual' | 'programmatic' | 'clear' }>`)
1398
1559
 
1399
- 悬停暂停自动关闭计时器(使用 `pointerenter`/`pointerleave`)。同一微任务中批量挂载 Toast。
1560
+ 悬停暂停自动关闭计时器(使用 `pointerenter`/`pointerleave`),指针离开后**续跑剩余时间**,而不是重新计满。暂停期间漏掉 `pointerleave`(指针拖出窗口、元素在悬停中被搬迁或摘出)时,由 document 级 `pointerover`/`pointerout`/`pointerleave` 兜底恢复,不会永久停在屏幕上。同一微任务中批量挂载 Toast。
1400
1561
 
1401
1562
  **CSS 自定义属性:**
1402
1563