@greypan/web-ui 8.1.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.CN.md +309 -135
  2. package/README.md +311 -135
  3. package/dist/assets/glass.js +1 -1
  4. package/dist/assets/glass.js.map +1 -1
  5. package/dist/assets/menu-portal.js +1 -1
  6. package/dist/assets/menu-portal.js.map +1 -1
  7. package/dist/assets/modal-surface.js +6 -0
  8. package/dist/assets/modal-surface.js.map +1 -0
  9. package/dist/assets/selection-control.js +1 -1
  10. package/dist/assets/selection-control.js.map +1 -1
  11. package/dist/assets/toast-containers.js +1 -1
  12. package/dist/assets/toast-containers.js.map +1 -1
  13. package/dist/components/autocomplete/index.d.ts +3 -2
  14. package/dist/components/autocomplete/index.js +40 -44
  15. package/dist/components/autocomplete/index.js.map +1 -1
  16. package/dist/components/autocomplete/style.js +1 -1
  17. package/dist/components/autocomplete/style.js.map +1 -1
  18. package/dist/components/avatar/index.js.map +1 -1
  19. package/dist/components/avatar/style.js +1 -1
  20. package/dist/components/avatar/style.js.map +1 -1
  21. package/dist/components/back-top/style.js +1 -1
  22. package/dist/components/back-top/style.js.map +1 -1
  23. package/dist/components/badge/style.js +1 -1
  24. package/dist/components/badge/style.js.map +1 -1
  25. package/dist/components/button/style.js +1 -1
  26. package/dist/components/button/style.js.map +1 -1
  27. package/dist/components/button-group/index.js +0 -1
  28. package/dist/components/button-group/index.js.map +1 -1
  29. package/dist/components/button-group/style.js +1 -1
  30. package/dist/components/button-group/style.js.map +1 -1
  31. package/dist/components/checkbox/index.js +4 -4
  32. package/dist/components/checkbox/index.js.map +1 -1
  33. package/dist/components/checkbox-group/style.js +1 -1
  34. package/dist/components/checkbox-group/style.js.map +1 -1
  35. package/dist/components/context-menu/index.d.ts +182 -3
  36. package/dist/components/context-menu/index.js +166 -91
  37. package/dist/components/context-menu/index.js.map +1 -1
  38. package/dist/components/context-menu/scrim.js +6 -0
  39. package/dist/components/context-menu/scrim.js.map +1 -0
  40. package/dist/components/context-menu/style.js +1 -1
  41. package/dist/components/context-menu/style.js.map +1 -1
  42. package/dist/components/dialog/index.d.ts +9 -0
  43. package/dist/components/dialog/index.js +68 -46
  44. package/dist/components/dialog/index.js.map +1 -1
  45. package/dist/components/dialog/style.js +1 -1
  46. package/dist/components/dialog/style.js.map +1 -1
  47. package/dist/components/drawer/index.d.ts +45 -0
  48. package/dist/components/drawer/index.js +122 -53
  49. package/dist/components/drawer/index.js.map +1 -1
  50. package/dist/components/drawer/style.js +1 -1
  51. package/dist/components/drawer/style.js.map +1 -1
  52. package/dist/components/dropdown/index.d.ts +12 -0
  53. package/dist/components/dropdown/index.js +13 -10
  54. package/dist/components/dropdown/index.js.map +1 -1
  55. package/dist/components/dropdown-divider/style.js +1 -1
  56. package/dist/components/dropdown-divider/style.js.map +1 -1
  57. package/dist/components/dropdown-header/style.js +1 -1
  58. package/dist/components/dropdown-header/style.js.map +1 -1
  59. package/dist/components/dropdown-item/style.js +1 -1
  60. package/dist/components/dropdown-item/style.js.map +1 -1
  61. package/dist/components/editable-text/index.d.ts +39 -0
  62. package/dist/components/editable-text/index.js +6 -2
  63. package/dist/components/editable-text/index.js.map +1 -1
  64. package/dist/components/empty/index.d.ts +21 -4
  65. package/dist/components/empty/index.js +37 -32
  66. package/dist/components/empty/index.js.map +1 -1
  67. package/dist/components/empty/style.js +1 -1
  68. package/dist/components/empty/style.js.map +1 -1
  69. package/dist/components/image-preview/index.js +75 -63
  70. package/dist/components/image-preview/index.js.map +1 -1
  71. package/dist/components/image-preview/style.js +1 -1
  72. package/dist/components/image-preview/style.js.map +1 -1
  73. package/dist/components/index.d.ts +1 -0
  74. package/dist/components/index.js +38 -37
  75. package/dist/components/input/style.js +1 -1
  76. package/dist/components/input/style.js.map +1 -1
  77. package/dist/components/input-number/style.js +1 -1
  78. package/dist/components/input-number/style.js.map +1 -1
  79. package/dist/components/layout/index.d.ts +21 -6
  80. package/dist/components/layout/index.js +47 -27
  81. package/dist/components/layout/index.js.map +1 -1
  82. package/dist/components/layout/style.js +1 -1
  83. package/dist/components/layout/style.js.map +1 -1
  84. package/dist/components/middle-ellipsis/index.d.ts +100 -0
  85. package/dist/components/middle-ellipsis/index.js +112 -0
  86. package/dist/components/middle-ellipsis/index.js.map +1 -0
  87. package/dist/components/middle-ellipsis/style.js +6 -0
  88. package/dist/components/middle-ellipsis/style.js.map +1 -0
  89. package/dist/components/middle-ellipsis/truncate.d.ts +62 -0
  90. package/dist/components/middle-ellipsis/truncate.js +45 -0
  91. package/dist/components/middle-ellipsis/truncate.js.map +1 -0
  92. package/dist/components/option/style.js +1 -1
  93. package/dist/components/option/style.js.map +1 -1
  94. package/dist/components/popover/index.js.map +1 -1
  95. package/dist/components/popover/style.js +1 -1
  96. package/dist/components/popover/style.js.map +1 -1
  97. package/dist/components/radio/style.js +1 -1
  98. package/dist/components/radio/style.js.map +1 -1
  99. package/dist/components/radio-group/style.js +1 -1
  100. package/dist/components/radio-group/style.js.map +1 -1
  101. package/dist/components/segmented/index.d.ts +5 -0
  102. package/dist/components/segmented/index.js +45 -28
  103. package/dist/components/segmented/index.js.map +1 -1
  104. package/dist/components/segmented/style.js +1 -1
  105. package/dist/components/segmented/style.js.map +1 -1
  106. package/dist/components/segmented-trigger/style.js +1 -1
  107. package/dist/components/segmented-trigger/style.js.map +1 -1
  108. package/dist/components/select/index.js +0 -1
  109. package/dist/components/select/index.js.map +1 -1
  110. package/dist/components/select/style.js +1 -1
  111. package/dist/components/select/style.js.map +1 -1
  112. package/dist/components/slider/style.js +1 -1
  113. package/dist/components/slider/style.js.map +1 -1
  114. package/dist/components/spinner/style.js +1 -1
  115. package/dist/components/spinner/style.js.map +1 -1
  116. package/dist/components/svg-draw-lines/index.js.map +1 -1
  117. package/dist/components/switch/style.js +1 -1
  118. package/dist/components/switch/style.js.map +1 -1
  119. package/dist/components/textarea/index.js +2 -2
  120. package/dist/components/textarea/index.js.map +1 -1
  121. package/dist/components/textarea/style.js +1 -1
  122. package/dist/components/textarea/style.js.map +1 -1
  123. package/dist/components/theme/style.js +1 -1
  124. package/dist/components/theme/style.js.map +1 -1
  125. package/dist/components/toast/style.js +1 -1
  126. package/dist/components/toast/style.js.map +1 -1
  127. package/dist/components/tooltip/index.js.map +1 -1
  128. package/dist/components/tooltip/style.js +1 -1
  129. package/dist/components/tooltip/style.js.map +1 -1
  130. package/dist/icons/generated/akar-icons-circle-check.d.ts +6 -0
  131. package/dist/icons/generated/akar-icons-circle-check.js +10 -0
  132. package/dist/icons/generated/akar-icons-circle-check.js.map +1 -0
  133. package/dist/icons/generated/bi-check-lg.d.ts +6 -0
  134. package/dist/icons/generated/bi-check-lg.js +10 -0
  135. package/dist/icons/generated/bi-check-lg.js.map +1 -0
  136. package/dist/icons/generated/fluent-tag-search-24-regular.d.ts +6 -0
  137. package/dist/icons/generated/fluent-tag-search-24-regular.js +10 -0
  138. package/dist/icons/generated/fluent-tag-search-24-regular.js.map +1 -0
  139. package/dist/icons/generated/mdi-tag-search-outline.d.ts +6 -0
  140. package/dist/icons/generated/mdi-tag-search-outline.js +10 -0
  141. package/dist/icons/generated/mdi-tag-search-outline.js.map +1 -0
  142. package/dist/icons/index.d.ts +4 -0
  143. package/dist/icons/index.js +5 -1
  144. package/dist/scrollbars/index.d.ts +33 -0
  145. package/dist/scrollbars/index.js +18 -0
  146. package/dist/scrollbars/index.js.map +1 -0
  147. package/dist/scrollbars/style.css +10 -0
  148. package/dist/shared/element-height/index.js +47 -0
  149. package/dist/shared/element-height/index.js.map +1 -0
  150. package/dist/shared/element-width/index.js +37 -0
  151. package/dist/shared/element-width/index.js.map +1 -0
  152. package/dist/shared/menu-portal/menu-portal.js +11 -11
  153. package/dist/shared/menu-portal/menu-portal.js.map +1 -1
  154. package/dist/shared/menu-portal/menu-tree.js +2 -2
  155. package/dist/shared/menu-portal/menu-tree.js.map +1 -1
  156. package/dist/shared/overlay/anchored-panel.js.map +1 -1
  157. package/dist/shared/overlay/native-dialog-presence.js +23 -11
  158. package/dist/shared/overlay/native-dialog-presence.js.map +1 -1
  159. package/dist/shared/overlay/nested-drawer-layers.js +30 -23
  160. package/dist/shared/overlay/nested-drawer-layers.js.map +1 -1
  161. package/dist/shared/scroll-lock/scroll-lock.js +3 -3
  162. package/dist/shared/scroll-lock/scroll-lock.js.map +1 -1
  163. package/package.json +19 -5
package/README.CN.md CHANGED
@@ -25,10 +25,35 @@ import '@greypan/web-ui'
25
25
  // import '@greypan/web-ui/components/button'
26
26
  ```
27
27
 
28
+ subpath 导入只注册它点名的那个组件,页面用到的每个标签都要单独导入。
29
+
28
30
  ```html
29
31
  <web-ui-button variant="primary">点击我</web-ui-button> <web-ui-icon .icon="${lucidePlus}"></web-ui-icon>
30
32
  ```
31
33
 
34
+ ## 滚动条
35
+
36
+ `@greypan/web-ui/scrollbars` 承载全仓统一的 [OverlayScrollbars](https://github.com/KingSora/OverlayScrollbars)
37
+ 滚动条行为:对齐 macOS 的主题、滚动时出现并自动隐去、点击轨道滚动到该位置。样式表每份文档导入一次,
38
+ 选项传给所用的适配包。
39
+
40
+ ```js
41
+ import '@greypan/web-ui/scrollbars.css'
42
+ import { webUiScrollbarsOptions } from '@greypan/web-ui/scrollbars'
43
+ import { OverlayScrollbarsComponent } from 'overlayscrollbars-vue' // 或 overlayscrollbars-react
44
+
45
+ <OverlayScrollbarsComponent :options="webUiScrollbarsOptions">{children}</OverlayScrollbarsComponent>
46
+ ```
47
+
48
+ - `overlayscrollbars` 是可选 peer dependency,只有这个 subpath 需要它。样式表里已经包含库自己的结构样式,
49
+ 它替代 `import 'overlayscrollbars/overlayscrollbars.css'`。
50
+ - 容器自己的 `overflow` 声明(`overflow-y-auto` 等)要保留:OverlayScrollbars 接管后会覆盖它,但接管之前
51
+ 靠它维持可滚动。
52
+ - 目标必须是「纯粹的滚动容器」。OverlayScrollbars 会把宿主变成 flex 行、把子节点搬进它生成的 viewport,
53
+ 所以宿主自身的布局一旦承重(grid、居中盒子、`ul`/`ol`)就会坏。
54
+ - OverlayScrollbars 只改变它被初始化到的那个元素,子元素保持原生滚动条。要覆盖文档滚动条就初始化
55
+ `document.body`——在那里 `window.scrollY` / `window.scrollTo` 保持原生语义。
56
+
32
57
  ## 框架集成
33
58
 
34
59
  ### 跨框架 API 约定
@@ -274,6 +299,7 @@ dropdown、tooltip)不需要它。
274
299
  | **数据展示** | [`<web-ui-avatar>`](#web-ui-avatar) |
275
300
  | | [`<web-ui-badge>`](#web-ui-badge) |
276
301
  | | [`<web-ui-empty>`](#web-ui-empty) |
302
+ | | [`<web-ui-middle-ellipsis>`](#web-ui-middle-ellipsis) |
277
303
  | | [`<web-ui-icon>`](#web-ui-icon) |
278
304
  | | [`<web-ui-spinner>`](#web-ui-spinner) |
279
305
  | **布局与工具** | [`<web-ui-layout>`](#web-ui-layout) |
@@ -324,24 +350,24 @@ dropdown、tooltip)不需要它。
324
350
 
325
351
  多行文本输入框,支持自动调整高度。
326
352
 
327
- | 属性 | 类型 | 默认值 | 说明 |
328
- | ----------------- | --------- | ------- | --------------------------------------------------------------------- |
329
- | `value` | `string` | `''` | 输入值 |
330
- | `placeholder` | `string` | `''` | 占位文本 |
331
- | `rows` | `number` | `3` | 显示行数 |
332
- | `name` | `string` | `''` | 表单字段名 |
333
- | `disabled` | `boolean` | `false` | 禁用状态 |
334
- | `readonly` | `boolean` | `false` | 只读状态 |
335
- | `required` | `boolean` | `false` | 必填校验 |
336
- | `clearable` | `boolean` | `false` | 显示清除按钮 |
337
- | `full` | `boolean` | `false` | 全宽 |
338
- | `borderless` | `boolean` | `false` | ghost 形态:移除边框、背景与阴影;保留 padding、高度度量与 focus ring |
339
- | `autosize` | `boolean` | `false` | 自动调整高度 |
340
- | `max-height` | `number` | `0` | 自动高度上限(px),`0` 表示不限制 |
341
- | `minlength` | `number` | — | 最小长度校验 |
342
- | `maxlength` | `number` | — | 最大长度校验 |
343
- | `aria-label` | `string` | — | 无障碍标签 |
344
- | `aria-labelledby` | `string` | — | 无障碍标签引用 |
353
+ | 属性 | 类型 | 默认值 | 说明 |
354
+ | ----------------- | --------- | ------- | ----------------------------------------------------------------------- |
355
+ | `value` | `string` | `''` | 输入值 |
356
+ | `placeholder` | `string` | `''` | 占位文本 |
357
+ | `rows` | `number` | `3` | 显示行数 |
358
+ | `name` | `string` | `''` | 表单字段名 |
359
+ | `disabled` | `boolean` | `false` | 禁用状态 |
360
+ | `readonly` | `boolean` | `false` | 只读状态 |
361
+ | `required` | `boolean` | `false` | 必填校验 |
362
+ | `clearable` | `boolean` | `false` | 显示清除按钮 |
363
+ | `full` | `boolean` | `false` | 全宽 |
364
+ | `borderless` | `boolean` | `false` | ghost 形态:移除边框、背景与阴影;保留 padding、高度度量与 focus ring |
365
+ | `autosize` | `boolean` | `false` | 自动调整高度;同时强制 `resize: none`,原生拖拽柄会与脚本接管的高度冲突 |
366
+ | `max-height` | `number` | `0` | 自动高度上限(px),`0` 表示不限制 |
367
+ | `minlength` | `number` | — | 最小长度校验 |
368
+ | `maxlength` | `number` | — | 最大长度校验 |
369
+ | `aria-label` | `string` | — | 无障碍标签 |
370
+ | `aria-labelledby` | `string` | — | 无障碍标签引用 |
345
371
 
346
372
  **事件:** `input`, `change`, `focus`, `blur`
347
373
 
@@ -360,14 +386,15 @@ dropdown、tooltip)不需要它。
360
386
 
361
387
  行内纯文本编辑器:点击文字就地编辑,`Enter` 与 `blur` 提交,`Escape` 取消。文本层与编辑层共用一个盒,进入编辑态不会移动任何一个像素。
362
388
 
363
- | 属性 | 类型 | 默认值 | 说明 |
364
- | ------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------- |
365
- | `value` | `string` | `''` | 当前值;首次连接时捕获声明式初值作为 `form.reset()` 默认,连接后修改 attribute 或 property 均不更新该初值 |
366
- | `placeholder` | `string` | `''` | 值为空时显示的占位文本 |
367
- | `name` | `string` | `''` | 表单字段名 |
368
- | `disabled` | `boolean` | `false` | 禁用状态;只影响行为,不做视觉置灰 |
369
- | `readonly` | `boolean` | `false` | 只读状态;仍可聚焦、全选和复制,但不能编辑或提交 `change` |
370
- | `aria-label` | `string` | — | 无障碍标签 |
389
+ | 属性 | 类型 | 默认值 | 说明 |
390
+ | ------------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------- |
391
+ | `value` | `string` | `''` | 当前值;首次连接时捕获声明式初值作为 `form.reset()` 默认,连接后修改 attribute 或 property 均不更新该初值 |
392
+ | `placeholder` | `string` | `''` | 值为空时显示的占位文本 |
393
+ | `name` | `string` | `''` | 表单字段名 |
394
+ | `disabled` | `boolean` | `false` | 禁用状态;只影响行为,不做视觉置灰 |
395
+ | `readonly` | `boolean` | `false` | 只读状态;仍可聚焦、全选和复制,但不能编辑或提交 `change` |
396
+ | `commit-on-unmount` | `boolean` | `false` | 卸载时按提交语义结束编辑态;见下文 |
397
+ | `aria-label` | `string` | — | 无障碍标签 |
371
398
 
372
399
  **事件:** `input`(每次输入)、`change`(提交)、`cancel`(取消;与原生 `<dialog>` 的 `cancel` 同名,不冒泡、不组合,只在组件本身派发)。React 没有覆盖 `cancel` 的合成事件,须用 `addEventListener('cancel', ...)` 监听
373
400
 
@@ -377,6 +404,18 @@ dropdown、tooltip)不需要它。
377
404
 
378
405
  `select()` 进入编辑态并全选内容;已在编辑态时只重新全选。`disabled` 时与 `focus()` 一样不产生效果。`readonly` 时仍可聚焦和全选,但输入被拒绝,退出编辑态也不派发 `change`。
379
406
 
407
+ **编辑到一半被卸载。** 正在编辑的元素被从 DOM 移除时会发生什么,取决于引擎,这个差异正是本属性存在的理由:
408
+
409
+ - **Chromium** 会在编辑层上派发 `blur`,因此纯粹的 `value` 提交今天就已经发生,默认 `false` 不改变这一点。`blur` 与它触发的 `change` 都在卸载回调跑完**之后**同步派发,也就是说提交落在「消费方针对这次移除已经更新完状态」之后。
410
+ - **WebKit(Safari、iOS)** 完全不派发 `blur`。默认 `false` 下什么都不提交,草稿随元素一起丢弃。
411
+ - **Gecko(Firefox)未验证**:实测所用机器上没有任何 Firefox 能启动,因此这里不做任何断言。把 Gecko 当作未知,而不是默认它与上面两列之一相同;需要靠卸载提交来保住草稿时,请开启本属性。
412
+
413
+ 如果你的代码依赖「行被回收时草稿不丢」,请**开启 `commit-on-unmount`**:在 Safari 与 iOS 上它是唯一能保住草稿的手段,在 Chromium 上它把提交提前到移除自带的 `blur` 之前。两个引擎下,卸载都按提交语义收尾一段开着的编辑会话——草稿成为新值、退出编辑、恰好派发一次 `change`,走的是与 `Enter`、`blur` 同一份实现。结论在 Chromium 与 WebKit 上实测过;浏览器测试套件只跑 Chromium,WebKit 的结果来自一次一次性跨引擎取证、不在 CI 里,Gecko 则完全没有测。
414
+
415
+ `Enter` 与 `blur` 由用户动作触发,一律算提交意图;卸载没有这种意图。因此只有草稿与进入编辑时的值确实不同时才提交——什么都没改的会话不会产生空提交。不在编辑态时没有可提交的内容;`disabled` 或 `readonly` 时与 `blur` 路径一致,都不派发提交事件。但两者对「已经有草稿」的处置不同:`disabled` 下草稿留在组件上,`readonly` 下草稿是真的丢掉。`readonly` 阻止不了先前输入的字,而卸载时已没有 blur 可以兜底提交——WebKit 上连 blur 都没有。可达路径是:可编辑态进入 → 起草稿 → 期间翻 readonly → 卸载。这里不按取消处理——取消的契约是恢复进入编辑时的值,而卸载既没有焦点要交也没有宿主接收结果;用户看到的是「我打的字没了」,既没有 `change` 也没有 `cancel`。
416
+
417
+ 提交发生在卸载回调里,`change` 仍沿组合路径向上派发,因此挂在组件自身或任一祖先上的监听都会执行,**包括自身也正在被卸载的祖先**:派发不要求祖先此刻还在文档里。因此监听器不能假定组件仍在文档中;结果需要活过这次移除时,用 `queueMicrotask` 之类的方式显式转交给宿主树之外的状态。注意这条可达性对「由 React 自身 render 驱动的卸载」并不成立:那种移除走不到 React 的事件委托根,`onChange` 不会触发,只能改为从元素上读值——与上文 `cancel` 的告示同源。在祖先上设 `display: none` 不算移除——两个引擎下组件都仍在文档中、不派发任何事件,那种场合需要消费方主动结束编辑。
418
+
380
419
  宿主是行内级盒子:未设宽度时随内容伸缩,折行后高度按行数增长。字体、颜色、文本对齐与空白处理全部继承外部上下文,因此编辑前它就是一段普通文字。
381
420
 
382
421
  光标颜色默认跟随共享语义 token `--wui-color-accent`;在宿主或任意祖先上设置该 token 即可整体改色。两层对长无空格串(连续字母数字)按同一组断行点折行:超出盒宽的长串在盒内断开,文字态不再溢出宿主盒。
@@ -472,9 +511,9 @@ Portal 面板创建时会镜像 host 上解析后的这些变量;更新 host
472
511
 
473
512
  **方法:** `focus()`, `blur()`
474
513
 
475
- **插槽:** `default`(投影 `<web-ui-option>` 元素)、`trigger`(自定义触发器内容——替换默认输入框)、`empty`(替换无匹配空态;默认回退为「无匹配选项」)
514
+ **插槽:** `default`(投影 `<web-ui-option>` 元素)、`trigger`(自定义触发器内容——替换默认输入框)
476
515
 
477
- 键入时按 label 过滤候选(`contains` 或 `prefix`,`none` 关闭过滤)。选择 option 时文本回填为该项 label,`selected-value` 暴露该项的 value;`change` 在选择提交时触发。支持 ArrowDown/ArrowUp/Enter/Escape 键盘导航。
516
+ 键入时按 label 过滤候选(`contains` 或 `prefix`,`none` 关闭过滤)。没有匹配项时下拉面板保持关闭;删除字符后重新出现匹配项时,面板会再次打开。选择 option 时文本回填为该项 label,`selected-value` 暴露该项的 value;`change` 在选择提交时触发。支持 ArrowDown/ArrowUp/Enter/Escape 键盘导航。
478
517
 
479
518
  **触发器:** 默认触发器是 shadow 内的 `web-ui-input`。把任意可编辑组件放进 `trigger` slot 即可替换它——包装 div 继续承载 combobox ARIA,并以 `data-custom-trigger` 标记当前使用自定义触发器,浮层始终以触发器元素为锚点。组件的 `focus()` / `blur()` 委托到当前生效的触发器:`web-ui-input` 与 `web-ui-textarea` 会把焦点落到内部原生控件;自定义触发器没有自己的 focus 重定向时,按宿主自身聚焦。
480
519
 
@@ -490,9 +529,7 @@ Portal 面板创建时会镜像 host 上解析后的这些变量;更新 host
490
529
 
491
530
  多行触发器(可编辑元素为 `<textarea>`)保留 Enter 换行语义:Enter 不会选中高亮项,也不会提交 custom value,关闭面板用 Escape 或 blur。选择 option 仍会把该项 label 回写到触发器。单行自定义触发器保持默认的 Enter 语义。面板打开时 ArrowUp/ArrowDown 适用同一例外:方向键移动文本光标而不导航候选,该状态下键盘无法导航 option——用指针点击选择。面板关闭时 ArrowDown/ArrowUp 仍可打开面板。
492
531
 
493
- 启用 `allow-custom-value` 后,无匹配且无活动 option 时,Enter 会把当前输入原文作为 custom value 提交并关闭面板;`change` 会触发,`selected-value` 保持为空。组件不会自动创建 option,也不会 trim 原文。命中禁用 option 的文本不会绕过禁用语义,也不会派生为已选 option。
494
-
495
- 通过 `<div slot="empty">…</div>` 自定义静态、非交互的空态内容。Portal 渲染时该节点会迁入浮层,关闭后恢复到宿主。
532
+ 启用 `allow-custom-value` 后,无匹配且无活动 option 时,Enter 会把当前输入原文作为 custom value 提交并保持面板关闭;`change` 会触发,`selected-value` 保持为空。组件不会自动创建 option,也不会 trim 原文。命中禁用 option 的文本不会绕过禁用语义,也不会派生为已选 option。
496
533
 
497
534
  **CSS 自定义属性:**
498
535
 
@@ -647,11 +684,11 @@ Portal 面板创建时会镜像 host 上解析后的这些变量;更新 host
647
684
 
648
685
  ##### Token
649
686
 
650
- | Token | 默认值 | 说明 |
651
- | -------------------------- | ------ | ------------------------ |
652
- | `--wui-checkbox-group-gap` | `8px` | 成员 checkbox 之间的间距 |
687
+ | Token | 默认值 | 说明 |
688
+ | -------------------------- | -------------------- | ------------------------ |
689
+ | `--wui-checkbox-group-gap` | `var(--wui-space-2)` | 成员 checkbox 之间的间距 |
653
690
 
654
- 在 group 元素本身,或同一个 `<web-ui-theme>` 作用域内的祖先元素上设置。theme 宿主会为自己的子树声明 `8px`,写在 `<web-ui-theme>` 元素之外的覆盖到不了 group。
691
+ 在 group 元素本身,或同一个 `<web-ui-theme>` 作用域内的祖先元素上设置。theme 宿主会为自己的子树声明该间距阶,写在 `<web-ui-theme>` 元素之外的覆盖到不了 group。它的默认值现在经 `--wui-space-2` 解析,所以在 theme 宿主上覆盖该级会连带改变成员间距;显式设置这个 token 仍然优先于两者。
655
692
 
656
693
  ```css
657
694
  web-ui-checkbox-group {
@@ -681,11 +718,11 @@ web-ui-checkbox-group {
681
718
 
682
719
  ##### Token
683
720
 
684
- | Token | 默认值 | 说明 |
685
- | ----------------------- | ------ | --------------------- |
686
- | `--wui-radio-group-gap` | `8px` | 成员 radio 之间的间距 |
721
+ | Token | 默认值 | 说明 |
722
+ | ----------------------- | -------------------- | --------------------- |
723
+ | `--wui-radio-group-gap` | `var(--wui-space-2)` | 成员 radio 之间的间距 |
687
724
 
688
- 在 group 元素本身,或同一个 `<web-ui-theme>` 作用域内的祖先元素上设置。theme 宿主会为自己的子树声明 `8px`,写在 `<web-ui-theme>` 元素之外的覆盖到不了 group。
725
+ 在 group 元素本身,或同一个 `<web-ui-theme>` 作用域内的祖先元素上设置。theme 宿主会为自己的子树声明该间距阶,写在 `<web-ui-theme>` 元素之外的覆盖到不了 group。它的默认值现在经 `--wui-space-2` 解析,所以在 theme 宿主上覆盖该级会连带改变成员间距;显式设置这个 token 仍然优先于两者。
689
726
 
690
727
  ```css
691
728
  web-ui-radio-group {
@@ -765,6 +802,7 @@ web-ui-radio-group {
765
802
  | 属性 | 类型 | 默认值 | 说明 |
766
803
  | ------------------- | --------- | ------- | --------------------------------------------------------- |
767
804
  | `open` | `boolean` | `false` | 对话框可见性 |
805
+ | `closable` | `boolean` | `false` | 显示内置关闭按钮 |
768
806
  | `no-scroll-lock` | `boolean` | `false` | 打开时不锁定页面滚动 |
769
807
  | `no-backdrop-close` | `boolean` | `false` | 禁止点击遮罩关闭 |
770
808
  | `no-escape-close` | `boolean` | `false` | 禁止按 Escape 关闭 |
@@ -778,22 +816,31 @@ web-ui-radio-group {
778
816
 
779
817
  使用原生 `<dialog>`,`@cancel` 阻止默认关闭行为。除非存在 `no-escape-close`,否则 Escape 调用 `close()`;除非存在 `no-backdrop-close`,否则点击遮罩关闭。启用 `controlled` 后,两者都只派发关闭请求而不自关闭。
780
818
 
819
+ 启用 `closable` 后,两种内容模式都会渲染内置关闭按钮,且与 Escape、遮罩点击走同一条关闭路径(`controlled` 对它同样生效)。两种模式的位置已统一:按钮都绝对定位到卡片右上角,距卡片两沿的偏移由 `--wui-dialog-close-top` / `--wui-dialog-close-right` 控制(默认都是 `20px`;drawer 关闭按钮仍为 `16px`)。只有 `body` 插槽会让标题行消失;标题模式下标题会为按钮留出避让,长标题不会钻到按钮下面。图标与 drawer 关闭按钮同为 `ooui:close`。不启用 `closable` 时什么都不渲染——既没有按钮,也没有多余的包裹层。
820
+
781
821
  > **Escape 归属**:Escape 由共享仲裁者统一判定,一次按键只关闭**最内层**的已打开浮层(popover、select、autocomplete、dropdown、context-menu、drawer、dialog 都参与)。例如在 drawer 内打开 select,第一次 Escape 只关 select,第二次才关 drawer。互不嵌套的并列浮层按打开顺序关闭最上层。正在播退场过渡的面板仍在场上,也仍由它接住 Escape——但只要还有别的浮层开着,那一次按键仍然归该层,「一次按键关一层」不因此改变。`image-preview` 同样参与:它的原生 `<dialog>` 会登记进同一个仲裁者,Escape 按层级判定;组件的 `cancel` handler 只是拦掉原生的瞬时关闭,把 top layer 保留到退场过渡结束。
782
822
 
783
823
  **CSS 自定义属性:**
784
824
 
785
- | 属性 | 默认值 | 说明 |
786
- | ----------------------------- | ------------------------------------------ | --------------------------------------------------------------------- |
787
- | `--wui-dialog-width` | `360px` | 对话框宽度 |
788
- | `--wui-dialog-max-height` | `90vh` | 对话框最大高度 |
789
- | `--wui-dialog-overlay-bg` | `var(--wui-color-backdrop)` | 遮罩背景色 |
790
- | `--wui-dialog-bg` | `var(--wui-color-surface-overlay)` | 玻璃卡片背景色,回退到 `rgb(246 246 246 / 0.82)` |
791
- | `--wui-dialog-padding` | `20px 24px 24px` | 对话框表面内边距 |
792
- | `--wui-dialog-title-gap` | `16px` | 标题下方间距 |
793
- | `--wui-dialog-desc-gap` | `24px` | 正文内容下方间距 |
794
- | `--wui-dialog-footer-gap` | `10px` / horizontal `12px` | Footer 按钮间距 |
795
- | `--wui-dialog-footer-justify` | `flex-end`(默认)/ `center`(horizontal) | Footer `justify-content`;horizontal 模式下覆盖为 `flex-end` 可右对齐 |
796
- | `--wui-dialog-scale-enter` | `1.1` | 进场缩放起点:由 `1.1` 收缩到 `1`,退场反向 |
825
+ | 属性 | 默认值 | 说明 |
826
+ | --------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
827
+ | `--wui-dialog-width` | `360px` | 对话框宽度 |
828
+ | `--wui-dialog-max-height` | `90vh` / `min(90vh, 90dvh)`(dvh 引擎) | 内容区(`.desc`)的最大高度,不是整卡高度。两套 fallback:基础规则 `90vh`,`@supports (height: 100dvh)` 内 `min(90vh, 90dvh)`——不支持 dvh 的引擎只拿到 `90vh`。整卡换算见下方说明 |
829
+ | `--wui-dialog-close-top` | `20px` | `closable` 按钮距卡片顶部的偏移 |
830
+ | `--wui-dialog-close-right` | `20px` | `closable` 按钮距卡片右沿的偏移 |
831
+ | `--wui-dialog-overlay-bg` | `var(--wui-color-backdrop)` | 遮罩背景色 |
832
+ | `--wui-dialog-bg` | `var(--wui-color-surface-overlay)` | 玻璃卡片背景色,回退到 `rgb(248 248 248 / 0.92)` |
833
+ | `--wui-dialog-padding` | `20px 24px 24px` | 对话框表面内边距 |
834
+ | `--wui-dialog-title-gap` | `16px` | 标题下方间距 |
835
+ | `--wui-dialog-desc-gap` | `24px` | 正文内容下方间距 |
836
+ | `--wui-dialog-desc-focus-padding` | `6px` | 内容区**四边**预留的 focus-ring 余量。`.desc` 既是滚动容器,又因为有一条轴必须非 `visible` 而成了裁剪盒,所以紧贴其任一沿的控件,ring 都会被 padding box 裁掉;这个余量在四个轴上都覆盖 `5px` 的 ring(`offset: 2` + `width: 3`)。横向余量同样由组件提供,宿主不需要自己留。成对负 margin 使 chrome 高度保持不变,因此可按各平台字体度量重新调这个值而不改变整卡高度 |
837
+ | `--wui-dialog-footer-gap` | `10px` / horizontal `12px` | Footer 按钮间距 |
838
+ | `--wui-dialog-footer-justify` | `flex-end`(默认)/ `center`(horizontal) | Footer `justify-content`;horizontal 模式下覆盖为 `flex-end` 可右对齐 |
839
+ | `--wui-dialog-scale-enter` | `1.1` | 进场缩放起点:由 `1.1` 收缩到 `1`,退场反向 |
840
+
841
+ > **高度语义**:本 token 限制的是内容区,不是整卡。标题行与 footer 仍由内容撑开,超出上限的部分在 `.desc` 内部滚动;整卡本身只由字面量 `100vh` / `100dvh` 兜底,接住「视口太矮、chrome 加内容区上限也放不下」的情况。要给**整卡**定上限的宿主仍需自行量出 chrome(卡片内边距 + 标题行 + 间距 + footer)并从 token 里减掉——Interweave 前端三个 dialog 分别减 142、142、106。不再需要这个数的是「让内容区滚起来」:改动前 `.desc` 既没有上限也没有 `overflow`,唯一能滚的是宿主自己撑出来的内层盒子,宿主想让它滚就必须自己算这个高度。
842
+ >
843
+ > **`body` 模式没有 `.desc`**,所以该 token 在这一档不生效。`body` 插槽被包进 `.wui-dialog-content`,滚动发生在这一层;卡片在内容装得下时仍贴合内容,装不下时由同一条字面量 `100vh` / `100dvh` 兜底。内置关闭按钮在内容滚动时保持 `--wui-dialog-close-top` / `--wui-dialog-close-right` 给出的位置不变。
797
844
 
798
845
  #### `<web-ui-drawer>`
799
846
 
@@ -822,9 +869,17 @@ web-ui-radio-group {
822
869
 
823
870
  关闭时保留原生 dialog 的 top layer,待退出过渡完成后调用 `dialog.close()`。Escape 始终走此关闭路径;`no-backdrop-close` 仅控制遮罩点击。
824
871
 
825
- 启用 `closable` 时,内置关闭按钮固定在 header 右上角(默认顶部 `16px`、右侧 `20px`),不会再拉伸到抽屉中间。
872
+ 启用 `closable` 时,内置关闭按钮固定在 header 右上角(默认顶部 `16px`、右侧 `16px`),不会再拉伸到抽屉中间。顶部 `16px` 经 `--wui-space-4` 解析,因此与 drawer 的 header padding 同源;右侧 `16px` 是按钮自身到面板右边沿的偏移,它并不等于 header 的水平内边距(`20px`),因此保持字面量。
873
+
874
+ **拖拽关闭:** 启用 `draggable` 后,打开的抽屉在内缘显示灰色胶囊 drag bar(默认 4×56px,视觉中线距内缘 10px;`right` 在左缘、`left` 在右缘、`top` 在下缘、`bottom` 在上缘):
875
+
876
+ 中线跟随 `--wui-drawer-content-padding` 的半值,并以「半个胶囊厚度 + 4px 呼吸间距」兜底:把该 padding 归零、让内容贴边铺满时,胶囊仍留可见内缩,既不会越出面板内缘,也不会贴死面板边缘。
877
+
878
+ 胶囊是纯视觉件:`pointer-events: none`,本身不吃任何一次按下。真正被按下的是命中层。左右 placement 只有热区;上下 placement 有三层——拖拽热区、沿可抓取边缘的一条端头热边(宽度与胶囊等宽,厚度不小于 `--wui-drawer-drag-edge-size`),以及整块的 header(`placement=bottom`)或 footer(`placement=top`)。热边的高度取「胶囊外沿」与该 token 的较大者,按构造覆盖胶囊所在的带,所以在四个 placement 上,按住胶囊能开始拖拽、悬停胶囊会点亮它。
826
879
 
827
- **拖拽关闭:** 启用 `draggable` 后,打开的抽屉在内缘显示灰色胶囊 drag bar(默认 4×56px,视觉中线距内缘 10px,位于 20px 厚的命中热区内;`right` 在左缘、`left` 在右缘、`top` 在下缘、`bottom` 在上缘):
880
+ 让开 header(`placement=bottom`)或 footer(`placement=top`)的是热区本身,因此那一节不会把胶囊推离面板边缘;而那一节改由**自己**接拖拽,不再整块按不动:在它里面任意一点起手都能开始拖拽,标题文字同样在内。控件的让开是**逐个控件、在事件层**做的,不是把命中面几何切开——按下路径上只要出现可点元素(原生 `button` / `input` / `select` / `textarea` / `label` / `a[href]` / `summary`,任何 `contenteditable`,交互语义 `role`(`button`、`checkbox`、`switch`、`tab`、`menuitem`、`option`、`slider` 等),以及任何显式 `tabindex` 且不为 `-1`),这一次按下就交给控件、不起拖拽。自绘控件若本身不是原生可交互元素,需要带上其中之一才会被认出来;补 `role` 或 `tabindex` 就是入口,而它本身也是无障碍上该做的事。几何切割是被有意否掉的:它必须测量每个控件的位置,控件一动就失效,而且只是把 A6 那条缺陷换了个位置。代价是明说的:内置关闭按钮贴着面板右缘、在胶囊跨度之外,仍可点击;落在胶囊水平跨度内的控件,其顶部到热边高度之间仍被拖拽面盖住。
881
+
882
+ 「把手占位」自带一条推论:落在这条跨度内、顶边又落在热边高度(`--wui-drawer-drag-edge-size`,默认 20px;胶囊外沿更厚时以胶囊为准)之内的控件,上部会被拖拽面盖住——把手就画在那里,起手必须从把手所在的位置开始。抬 `z-index` 没用(Chromium 里热边始终压在 slotted 内容之上),量得着的杠杆是 `--wui-drawer-drag-bar-length`(同时决定胶囊长度与热边宽度)与 `--wui-drawer-drag-edge-size`(热边厚度)。
828
883
 
829
884
  - 拖拽实时跟手,遮罩透明度按比例淡出。
830
885
  - 遮罩点击关闭只认**轻点链路**:按下起点在遮罩上、且按-放位移在轻点量级内的 click 才关闭。浏览器对「按下 → 拖动 → 松手」生成的 click 落在起点与松手点的共同祖先(dialog)上——从面板内容或遮罩上开始拖拽、松手落在遮罩时,click 的 target 同样是 dialog;组件在 `pointerdown` 记录起点与坐标做回溯校验,这类拖拽松手一律弹回。`detail` 为 0 的 click(键盘/程序化来源)不消费指针记录。
@@ -842,22 +897,29 @@ web-ui-radio-group {
842
897
 
843
898
  **CSS 自定义属性:**
844
899
 
845
- | 属性 | 默认值 | 说明 |
846
- | --------------------------------- | ---------------------------------- | ---------------------------------------------------- |
847
- | `--wui-drawer-width` | `320px` | 抽屉宽度 |
848
- | `--wui-drawer-height` | `300px` | 抽屉高度(上/下) |
849
- | `--wui-drawer-bg` | `var(--wui-color-surface-overlay)` | 抽屉背景色 |
850
- | `--wui-drawer-radius` | `var(--wui-radius-overlay, 28px)` | 浮动卡片圆角(非 headless) |
851
- | `--wui-drawer-inset` | `8px` | 浮动卡片视口留边(非 headless);置 `0` 为贴边几何 |
852
- | `--wui-drawer-overlay-bg` | `rgb(0 0 0 / 0.12)` | 遮罩背景色 |
853
- | `--wui-drawer-drag-zone-size` | `20px` | Drag-to-close 命中热区厚度(draggable) |
854
- | `--wui-drawer-drag-bar-thickness` | `4px` | Drag bar 胶囊厚度(短轴) |
855
- | `--wui-drawer-drag-bar-length` | `56px` | Drag bar 胶囊长度(沿抽屉边缘) |
856
- | `--wui-drawer-header-padding` | `16px 20px` | Header 区域 padding |
857
- | `--wui-drawer-close-top` | `16px` | 内置关闭按钮相对 header 顶部的偏移 |
858
- | `--wui-drawer-close-right` | `16px` | 内置关闭按钮相对抽屉右缘的偏移 |
859
- | `--wui-drawer-content-padding` | `20px` | 内容区 padding;同时驱动 drag bar 视觉中线(取半值) |
860
- | `--wui-drawer-footer-padding` | `16px 20px` | Footer 区域 padding |
900
+ | 属性 | 默认值 | 说明 |
901
+ | --------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
902
+ | `--wui-drawer-width` | `320px` | 抽屉宽度 |
903
+ | `--wui-drawer-height` | `300px` | 抽屉高度(上/下) |
904
+ | `--wui-drawer-bg` | `var(--wui-color-surface-overlay)` | 抽屉背景色 |
905
+ | `--wui-drawer-radius` | `var(--wui-radius-overlay, 28px)` | 浮动卡片圆角(非 headless) |
906
+ | `--wui-drawer-inset` | `8px` | 浮动卡片视口留边(非 headless);置 `0` 为贴边几何 |
907
+ | `--wui-drawer-nested-peek-base` | `43.2px` | 嵌套堆叠露边基准 `A`(`width <= 640px` 时为 `28.8px`);堆叠总宽按 `A · ln(n)` 增长,单层因此完全不动,每多一层新增的露边递减。置 `0` 关闭露边。详见下方说明 |
908
+ | `--wui-drawer-overlay-bg` | `rgb(0 0 0 / 0.12)` | 遮罩背景色 |
909
+ | `--wui-drawer-drag-zone-size` | `20px` | Drag-to-close 命中热区厚度(draggable) |
910
+ | `--wui-drawer-drag-zone-inset` | `var(--wui-space-1, 4px)` | 拖拽热区与它避让的那一节(`placement=bottom` 的 header / `placement=top` 的 footer)之间的呼吸间距;该节不存在时为 0 |
911
+ | `--wui-drawer-drag-bar-thickness` | `4px` | Drag bar 胶囊厚度(短轴) |
912
+ | `--wui-drawer-drag-bar-length` | `56px` | Drag bar 胶囊长度(沿抽屉边缘) |
913
+ | `--wui-drawer-drag-edge-size` | `20px` | 胶囊命中带沿可抓取边缘的厚度(仅上下 placement)。以胶囊外沿兜底,胶囊所在的带恒被覆盖;默认值与 `--wui-drawer-drag-zone-size` 同值,于是把手不会比旁边的带子更难按 |
914
+ | `--wui-drawer-header-padding` | `16px 20px` | Header 区域 padding |
915
+ | `--wui-drawer-close-top` | `16px` | 内置关闭按钮相对 header 顶部的偏移 |
916
+ | `--wui-drawer-close-right` | `16px` | 内置关闭按钮相对抽屉右缘的偏移 |
917
+ | `--wui-drawer-content-padding` | `20px` | 内容区 padding;同时驱动 drag bar 视觉中线(取半值,不低于半个胶囊厚度 + 4px) |
918
+ | `--wui-drawer-footer-padding` | `16px 20px` | Footer 区域 padding |
919
+
920
+ `--wui-drawer-nested-peek-base` 不需要消费方声明:默认值 `43.2px` 是组件用 `CSS.registerProperty` 注册该属性时的 `initialValue`(`components/drawer/index.ts`),窄视口的 `28.8px` 基准来自 `components/drawer/style.css` 的 `@media (width <= 640px)` 规则——这两处就是内置基准的所在地。在宿主或任意祖先上设置该 token 可同时覆盖两者,在任何视口宽度下都生效。
921
+
922
+ 按内置基准,四层 `320px` 等宽堆叠逐层露边在桌面端为 `29.94px` / `17.52px` / `12.43px`、窄视口为 `19.96px` / `11.68px` / `8.29px`,对应总堆叠宽度 `59.89px` 与 `39.93px`。单层始终不动,因为 `ln(1)` 为 `0`。
861
923
 
862
924
  #### `imagePreview()`
863
925
 
@@ -1064,10 +1126,12 @@ Hover 模式使用 `pointerenter`/`pointerleave` 加延迟控制。Click 模式
1064
1126
 
1065
1127
  右键上下文菜单。
1066
1128
 
1067
- | 属性 | 类型 | 默认值 | 说明 |
1068
- | ---------------- | --------- | ------- | ------------ |
1069
- | `disabled` | `boolean` | `false` | 禁用状态 |
1070
- | `no-scroll-lock` | `boolean` | `false` | 允许背景滚动 |
1129
+ | 属性 | 类型 | 默认值 | 说明 |
1130
+ | ------------------ | --------- | ------- | ------------------------------------------------ |
1131
+ | `disabled` | `boolean` | `false` | 右键不打开菜单,不做视觉置灰 |
1132
+ | `no-scroll-lock` | `boolean` | `false` | 允许背景滚动 |
1133
+ | `long-press` | `boolean` | `false` | 触屏长按打开菜单(opt-in;鼠标走 `contextmenu`) |
1134
+ | `long-press-delay` | `number` | `500` | `long-press` 触发前需按住的时长(ms) |
1071
1135
 
1072
1136
  **事件:** `open-change` (`CustomEvent<{ open: boolean }>`)
1073
1137
 
@@ -1075,10 +1139,26 @@ Hover 模式使用 `pointerenter`/`pointerleave` 加延迟控制。Click 模式
1075
1139
 
1076
1140
  **方法:** `openAt(x: number, y: number)`, `close()`
1077
1141
 
1142
+ **CSS 自定义属性:**
1143
+
1144
+ | 属性 | 默认值 | 说明 |
1145
+ | ------------------------------------- | ---------------------------------- | ---------------------------- |
1146
+ | `--wui-context-menu-safe-area-bottom` | `env(safe-area-inset-bottom, 0px)` | 触屏菜单需要让开的底部 inset |
1147
+
1078
1148
  通过 `contextmenu` 事件打开。菜单项:`web-ui-dropdown-item`、`web-ui-dropdown-divider`、`web-ui-dropdown-header`。支持键盘导航和子菜单 hover。
1079
1149
 
1150
+ 菜单打开期间是模态的:它渲染进浏览器 top layer 里一个透明的 `<dialog>`,下层页面拿不到任何命中测试。因此「点外面」只会关掉菜单,不会顺带把下面那一行、那个 checkbox、那个链接一起激活。scrim 刻意保持完全透明——它的职责是吃掉这次命中,不是给页面加一层遮罩。另有一处需要知道的代价:菜单项激活后约 160ms 的退场过渡里,scrim 已经提前出了 top layer,下层页面在这段时间重新可点,菜单的层级也退回普通流。
1151
+
1152
+ `long-press` 是 opt-in 且只针对触屏:指针按住 `long-press-delay`(默认 `500` ms,与平台原生长按一致)后打开菜单,走与右键完全相同的路径。它只响应 `pointerType === 'touch'`;按住期间位移超过 10px 即取消,因此滚动不会误开菜单。由于浏览器会把同一次按住也识别为原生手势,长按打开后引擎补发的 `contextmenu` 与 `click` 会被吸收,既不会重复打开,也不会把刚打开的菜单立刻 light-dismiss 掉。
1153
+
1154
+ `long-press` 打开的菜单从按点**向下**展开:上缘对齐按点 `y`、水平居中于按点 `x`,与 iOS / Android 原生长按菜单的形态一致。下方空间不够时面板翻转到按点上方而不是跑出屏幕,两轴都会被夹回视口内,并让开 `--wui-context-menu-safe-area-bottom`,避免最下面一项落进 home indicator。分流依据是**输入方式**而非视口宽度:窄窗口里的鼠标右键仍然贴着光标左上角展开。两者只有水平对齐量不同 —— 定位走的是同一趟 `computePosition`,因此不会各自漂移。
1155
+
1156
+ `disabled` 只抑制菜单行为:右键与 `openAt()` 都不打开菜单。它不会给 default slot 里的内容置灰或改色——组件自身根本不渲染菜单面,没有可置灰的对象;禁用右键菜单也不等于禁用触发区。需要让禁用态在视觉上可读时,请在自己的内容上表达。
1157
+
1080
1158
  菜单打开期间,Consumer可以安全地使用条件渲染(如 Vue `v-if`)切换、移动或删除菜单项,无需重新插入到宿主元素;portal 内的变更会自动 reconcile,关闭时框架锚点随元素迁回宿主,保证后续框架更新正常。
1081
1159
 
1160
+ 关闭时,焦点会归还给菜单打开前持焦的元素。这条是浏览器模态 dialog 自带的行为,而不是组件自己实现的——也正因如此,它只在菜单还在 top layer 时才生效。菜单项激活是唯一会提前交还焦点的路径:在点击那一刻就还,退场过渡之后不再过问,于是菜单项回调若把焦点移到别处(例如就地进入编辑态),焦点就留在那里了。关闭过程不会再把焦点从新的焦点目标上拽走,因此按 blur 提交的那类组件也不会因此提交一次没改过的编辑。
1161
+
1082
1162
  ---
1083
1163
 
1084
1164
  ### 菜单
@@ -1168,24 +1248,72 @@ Hover 模式使用 `pointerenter`/`pointerleave` 加延迟控制。Click 模式
1168
1248
 
1169
1249
  空状态占位。
1170
1250
 
1171
- | 属性 | 类型 | 默认值 | 说明 |
1172
- | ------------- | -------------------------------- | ---------- | -------- |
1173
- | `title` | `string` | `''` | 标题 |
1174
- | `description` | `string` | `''` | 描述文本 |
1175
- | `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | 尺寸 |
1251
+ | 属性 | 类型 | 默认值 | 说明 |
1252
+ | ------------- | -------- | ------ | --------------------------------------------- |
1253
+ | `title` | `string` | `''` | 标题 |
1254
+ | `description` | `string` | `''` | 描述文本 |
1255
+ | `size` | `number` | `56` | 版面缩放基准(px);非法值或非正数回退为 `56` |
1256
+
1257
+ `size` 是整个空态的单一旋钮:它驱动图标盒、`min-height`、内边距与标题/描述字号,因此大图标不再配 medium 留白:
1258
+
1259
+ | `size` | 图标盒 | 字形 | `min-height` | 内边距 块/行 | 标题 | 描述 |
1260
+ | ------ | ------ | ------ | ------------ | ------------ | ------ | ------ |
1261
+ | `40` | `40px` | `17px` | `171px` | `23px/17px` | `14px` | `13px` |
1262
+ | `56` | `56px` | `24px` | `240px` | `32px/24px` | `16px` | `14px` |
1263
+ | `72` | `72px` | `31px` | `309px` | `41px/31px` | `16px` | `14px` |
1264
+
1265
+ 派生规则是 `min-height: round(size * 30 / 7)`、`padding-block: round(size * 4 / 7)`、`padding-inline: round(size * 3 / 7)`,默认字形是 `round(size * 3 / 7)`。
1266
+
1267
+ 字号只有一档台阶而不是连续缩放:低于 `56` 时标题 `14px`、描述 `13px`,`56` 及以上为 `16px` 与 `14px`。更大的空态靠留白撑开,而不是靠字变大。
1268
+
1269
+ **从 `'small' \| 'medium' \| 'large'` 枚举迁移:** `size` 现在是数字。被删除的档位是手调值,这套比例只能逼近而复现不了——`small` 原本是 `160px` 高、`20px 16px` 内边距,`large` 原本是 `320px` 与 `48px 32px`。段间距与内容宽度从未跟随档位。
1270
+
1271
+ | 旧值 | 新值 | 默认字形 |
1272
+ | -------- | ---- | -------- |
1273
+ | `small` | `40` | `17` |
1274
+ | `medium` | `56` | `24` |
1275
+ | `large` | `72` | `31` |
1176
1276
 
1177
1277
  **插槽:** `default`(标题,覆盖 `title` 属性)、`icon`、`description`、`action`
1178
1278
 
1179
1279
  **CSS 自定义属性:**
1180
1280
 
1181
- | 属性 | 默认值 | 说明 |
1182
- | ----------------------------------- | ----------- | ------------------ |
1183
- | `--wui-empty-min-height` | `240px` | 最小高度(medium) |
1184
- | `--wui-empty-padding` | `32px 24px` | 内边距(medium) |
1185
- | `--wui-empty-icon-size` | `56px` | 图标容器尺寸 |
1186
- | `--wui-empty-content-width` | `480px` | 标题/描述最大宽度 |
1187
- | `--wui-empty-title-font-size` | `16px` | 标题字号(medium) |
1188
- | `--wui-empty-description-font-size` | `14px` | 描述字号(medium) |
1281
+ 每一项都覆盖 `size` 派生的值,因此八个属性始终优先——包括两个字号。默认值取默认 `size` 为 `56` 时的取值。
1282
+
1283
+ | 属性 | 默认值 | 说明 |
1284
+ | ----------------------------------- | ----------- | --------------------------- |
1285
+ | `--wui-empty-min-height` | `240px` | 最小高度 |
1286
+ | `--wui-empty-padding` | `32px 24px` | 内边距 |
1287
+ | `--wui-empty-icon-size` | `56px` | 图标容器尺寸,优先于 `size` |
1288
+ | `--wui-empty-content-width` | `480px` | 标题/描述/操作区最大宽度 |
1289
+ | `--wui-empty-title-margin-top` | `16px` | 图标与标题之间的间距 |
1290
+ | `--wui-empty-title-font-size` | `16px` | 标题字号 |
1291
+ | `--wui-empty-description-font-size` | `14px` | 描述字号 |
1292
+ | `--wui-empty-action-margin-top` | `20px` | 操作区上方的间距 |
1293
+
1294
+ #### `<web-ui-middle-ellipsis>`
1295
+
1296
+ 单行中间省略:保留首尾、丢掉中间——`very-long-file-…-abcdefghij.txt`——而 `text-overflow: ellipsis` 只能保住头部。
1297
+
1298
+ | 属性 | 类型 | 默认值 | 说明 |
1299
+ | ----------------- | -------- | ------ | -------------------------------------------------------------------------------------------------------- |
1300
+ | `text` | `string` | `''` | 完整文本。组件只读它,不回写。 |
1301
+ | `marker` | `string` | `'…'` | 插在两端之间的标记。空串即截断但不给任何可见信号。 |
1302
+ | `marker-position` | `number` | `50` | 标记所在的横向比例,0–100。`0` 贴行末(只剩头部),`100` 贴行首(只剩尾部)。越界夹紧,`NaN` 回退 `50`。 |
1303
+
1304
+ 使用条件与 `text-overflow: ellipsis` 相同:**单行、且有确定行内尺寸**。块级盒子、flex 行里的 `flex: 1; min-width: 0`、grid 轨道都可以。宿主按自身内容撑宽时不存在可用空间,组件按量到的宽度切一次就停止继续收缩,而不是一路塌到只剩标记。
1305
+
1306
+ 文件名场景靠 `marker-position` 调节,头部是路径噪声、尾部才是扩展名时取大值。下表是示意而非可复现的取值——确切切点取决于字体——取自 16px `system-ui`、240px 盒子:
1307
+
1308
+ | `marker-position` | 240px 盒子里 |
1309
+ | ----------------- | --------------------------------- |
1310
+ | `0` | `very-long-file-name-abcdefghi…` |
1311
+ | `50`(默认) | `very-long-file-…-abcdefghij.txt` |
1312
+ | `80` | `very-l…file-name-abcdefghij.txt` |
1313
+
1314
+ 切点一律落在**字素簇**边界上:代理对、组合符序列、ZWJ emoji 都不会被从中间劈开。截断期间完整原文写在文本元素的 `title` 上,放得下之后移除。宿主需为 `direction: ltr`;`direction: rtl` 下合串会被双向算法重排。
1315
+
1316
+ **已知限制——复制与辅助技术。** 元素里装的是屏幕上那串,因此选中复制拿到的是截断后的文本,读屏与页内查找命中的也是截断后的串。CSS 的 `text-overflow` 没有这两个问题(它从不改动文本节点),这是用 JS 算切点绕不开的代价;原文经 `title` 提供。保持全文留在 DOM 里的替代方案及其代价见 `docs/research/web-ui-middle-ellipsis-261007.md`。
1189
1317
 
1190
1318
  #### `<web-ui-icon>`
1191
1319
 
@@ -1260,13 +1388,24 @@ WebUiSpinner.hide() // 隐藏
1260
1388
  | `sidebar-collapsed` | `boolean` | `false` | 桌面端侧边栏受控折叠状态 |
1261
1389
  | `sidebar-open` | `boolean` | `false` | 移动端侧边栏 Drawer 受控打开状态 |
1262
1390
  | `header-glow` | `boolean` | `false` | 在 header 插槽内容背后显示装饰性晕染 |
1391
+ | `mobile` | `boolean` | `false` | 只读派生值:当前视口是否处于移动端断点及以下 |
1263
1392
  | `sidebar-width` | `string` | `'240px'` | 桌面端和移动端展开时的侧边栏宽度 |
1264
1393
  | `collapsed-width` | `string` | `'72px'` | 桌面端折叠时的侧边栏宽度 |
1265
1394
  | `sidebar-resizable` | `boolean` | `false` | 启用桌面端侧边栏右边缘拖拽调整宽度 |
1266
1395
  | `sidebar-min-width` | `string` | — | 拖拽调整的下限(px);默认回退到 `collapsed-width` |
1267
1396
  | `sidebar-max-width` | `string` | — | 拖拽调整的上限(px);钳制在视口一半以内,内置上限优先于配置值 |
1268
1397
 
1269
- **事件:** `sidebar-collapsed-change`(`CustomEvent<{ collapsed: boolean }>`)用于请求更新桌面端折叠状态;`sidebar-open-change`(`CustomEvent<{ open: boolean }>`)用于请求更新移动端 Drawer 打开状态;`sidebar-width-change`(`CustomEvent<{ width: string }>`)用于请求在拖拽调整结束后更新侧边栏宽度。Consumer 必须将请求值回写到对应的受控属性。
1398
+ **事件:** `sidebar-collapsed-change`(`CustomEvent<{ collapsed: boolean }>`)用于请求更新桌面端折叠状态;`sidebar-open-change`(`CustomEvent<{ open: boolean }>`)用于请求更新移动端 Drawer 打开状态;`sidebar-width-change`(`CustomEvent<{ width: string }>`)用于请求在拖拽调整结束后更新侧边栏宽度。Consumer 必须将请求值回写到对应的受控属性。`mobile-change`(`CustomEvent<{ mobile: boolean }>`)不是请求,它报告的是 layout 自己的派生视口状态。
1399
+
1400
+ **移动端视口状态:** `mobile` 是派生输出而不是第二个输入——由 layout 独占写入;property 只读(赋值不生效,严格模式下抛错),写 attribute 会在同一次 reaction 内被恢复、连值一起归位,两种写法都不派发事件。它与 layout 的渲染分支、以及 layout CSS 用来隐藏桌面 aside 的媒体查询(`@media (width <= 640px)`)是同一条条件,三者不可能互相错开;翻转不再经 resize 去抖,就发生在媒体查询变化的那一刻。`mobile-change` 每次跨断点只派发一次,元素在窄视口挂载时连接那一刻的那次求值也算一次。Consumer 用 `layout.mobile` 读当前值、用 `mobile-change` 收更新即可,CSS 侧可以写 `web-ui-layout[mobile]`。
1401
+
1402
+ **框架侧的消费方式:** layout 自身在 microtask 里重渲染,因此**在宏任务上响应的消费者会读到旧值,而新树已经在屏幕上了**:
1403
+
1404
+ - **Vue**:模板里绑 `@mobile-change`,用 `ref` 存值。Vue 在元素插入前就挂上模板监听(因此也能收到连接期那一次),且它的 flush 在 microtask,早于 layout 自己的更新。
1405
+ - **React**:用 ref 拿到元素,在 `useLayoutEffect` 里**先读 `layout.mobile` 再订阅**,并用 `flushSync` 回写。先读是为了拿到「挂在插入之后的监听收不到」的挂载初值;`flushSync` 是为了让运行时跨断点抢在 layout 重渲染之前——React 对 DOM 监听里的 setState 走宏任务 flush,否则会出现一帧「drawer 已按新状态渲染、consumer 的值还是旧的」(本仓 React demo 修复前实测 2/221 帧)。
1406
+ - **CSS**:直接按 `web-ui-layout[mobile]` 写样式,或把它包成一个变体。Tailwind v4 的 app 可以只写一次这条边界的**移动端那一侧**、而不是每个工具类各写一遍:`@custom-variant mobile (&:where(web-ui-layout[mobile], web-ui-layout[mobile] *));`。两条限制:属性选择器只够得到 layout 子树内的节点,被 overlay portal 到 `document.body` 的内容仍需媒体查询;**不要拿自定义变体做 min-width 那一侧**——Tailwind 把 `@custom-variant` 规则排在主题断点之后,同属性的 `desktop:` 变体会在它之上的每个宽度静默压掉 `md:`/`lg:`。
1407
+
1408
+ **折叠只作用于桌面端:** `sidebar-collapsed` 只收窄桌面端侧边栏,布局不对移动端 Drawer 施加任何折叠几何。两条分支承载的是同一份 `sidebar` 插槽内容,所以侧边栏内容密度跟着该属性变的 Consumer 必须按 `mobile` 收窄这个条件(例如 `collapsed && !layout.mobile`,或 CSS 的 `web-ui-layout[mobile]`),否则桌面的折叠偏好会一路渲染进 Drawer。见 issue #195。
1270
1409
 
1271
1410
  **侧边栏调整宽度:** 启用 `sidebar-resizable` 后,桌面端侧边栏右边缘会出现调整手柄(折叠状态下隐藏);悬停或拖拽时显示 3px 宽的强调色垂直线和 `col-resize` 光标。
1272
1411
 
@@ -1311,18 +1450,19 @@ WebUiSpinner.hide() // 隐藏
1311
1450
  }
1312
1451
  ```
1313
1452
 
1314
- 在 `640px` 及以下,侧边栏会切换为使用内置 glass body、可滚动 content 和 drag zone 的 `web-ui-drawer`。Layout 会将 `sidebar-width` 映射为 `--wui-drawer-width`,将 `--wui-layout-sidebar-radius` 映射为 `--wui-drawer-radius`。移动端 Toggle 以 glass 变体位于 header 行内。其左缩进默认 `8px`,可通过 `--wui-layout-mobile-toggle-inset` 与 Consumer 自身的 header 内边距对齐。
1453
+ 在 `640px` 及以下——与 `mobile` 报告的正是同一条条件——侧边栏会切换为使用内置 glass body、可滚动 content 和 drag zone 的 `web-ui-drawer`。Layout 会将 `sidebar-width` 映射为 `--wui-drawer-width`,将 `--wui-layout-sidebar-radius` 映射为 `--wui-drawer-radius`。移动端 Toggle 以 glass 变体位于 header 行内。其左缩进默认 `8px`,可通过 `--wui-layout-mobile-toggle-inset` 与 Consumer 自身的 header 内边距对齐。
1315
1454
 
1316
- `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`)。
1455
+ `header-glow` 会在 header 插槽内容和移动端 Toggle 的背后添加 `pointer-events: none` 的装饰性晕染。它属于 Header 背景而非前景层,因此插槽内容始终位于其上方。晕染颜色取自 `--wui-color-page`,因此浅色/暗色模式自动跟随;要改颜色,在 `web-ui-layout`、theme 或其上层任意位置覆盖该属性即可。晕染由两层伪元素合成:`::before` 在 header 盒内用 `linear-gradient` 绘制底色并渐变到全透明,`::after` 再叠一层真实的 `backdrop-filter: blur(4px)`,并用 `mask` 让这层模糊的 alpha 沿高度衰减、到底缘附近归零。两层职责分开后,模糊强度与颜色浓度互不牵制。两层都用负 `margin` 而不是 `transform` 撑开;模糊层止于 header 上缘,只向下与向两侧延伸。横向裁剪归属 `.layout-content`,glow 因此不会撑出横向滚动条。布局层级顺序为 Header(`10`)< Auxiliary(`20`)< Banner(`30`)< Tabbar(`40`)< Sidebar(`50`)。
1317
1456
 
1318
1457
  侧边栏卡片表面使用 `--wui-color-surface-sidebar`:浅色保持半透明中性分层,深色比 `--wui-color-page` 浅一档、与 `--wui-color-surface` 同级。它独立成 token,是因为 dialog、drawer 和 toast 共用 `--wui-color-surface-overlay`,可以采用不同表面。
1319
1458
 
1320
1459
  **CSS 自定义属性:**
1321
1460
 
1322
- | 属性 | 默认值 | 说明 |
1323
- | ---------------------------------- | ------ | ------------------------------------ |
1324
- | `--wui-layout-sidebar-radius` | `24px` | 侧边栏卡片圆角(桌面端和移动端共用) |
1325
- | `--wui-layout-mobile-toggle-inset` | `8px` | 移动端 header Toggle 的左缩进 |
1461
+ | 属性 | 默认值 | 说明 |
1462
+ | ----------------------------------- | ------ | ------------------------------------------------------------------- |
1463
+ | `--wui-layout-sidebar-radius` | `24px` | 侧边栏卡片圆角(桌面端和移动端共用) |
1464
+ | `--wui-layout-mobile-toggle-inset` | `8px` | 移动端 header Toggle 的左缩进 |
1465
+ | `--wui-layout-sidebar-toggle-width` | `44px` | 桌面端侧边栏折叠 Toggle 的宽度,下限为 `--wui-control-size`(36px) |
1326
1466
 
1327
1467
  #### `<web-ui-back-top>`
1328
1468
 
@@ -1408,19 +1548,54 @@ SVG 线条绘制动画,基于 `stroke-dashoffset`。直接在原元素上动
1408
1548
 
1409
1549
  | 属性 | 默认值 | 说明 |
1410
1550
  | ------------------------- | ------- | ------------------------ |
1411
- | `--wui-font-size` | `14px` | 控件基础字号 |
1412
1551
  | `--wui-input-width` | `200px` | 紧凑表单控件默认宽度 |
1413
1552
  | `--wui-control-size` | `36px` | 控件默认高度和方形最小宽 |
1414
1553
  | `--wui-overlay-min-width` | `200px` | 锚定浮层最小宽度 |
1415
1554
  | `--wui-focus-ring-width` | `3px` | Focus 指示器宽度 |
1416
1555
 
1556
+ **排版 token:** 字号按文字扮演的角色命名,而不是按它在尺度里的位置——`caption` 是密集 chrome 上的小标签,`readout` 是数字读数,无后缀的基准字号是正文,`title` 是有界卡片标题。字重按重量命名,行高按这段文字需要的垂直空气命名。这一族与外观无关:light 和 dark 下取值相同。
1557
+
1558
+ | 属性 | 默认值 | 说明 |
1559
+ | ---------------------------- | ------ | -------------------------------------------------- |
1560
+ | `--wui-font-size-caption` | `12px` | 密集 chrome 标签(徽标、toast 时间戳、菜单分组头) |
1561
+ | `--wui-font-size-readout` | `13px` | 数字读数(图片预览的计数与缩放比) |
1562
+ | `--wui-font-size` | `14px` | 正文与控件的基础字号(含控件的占位文本) |
1563
+ | `--wui-font-size-title` | `18px` | 有界卡片标题(dialog、drawer) |
1564
+ | `--wui-font-weight-medium` | `500` | 强调行内文字的中等字重 |
1565
+ | `--wui-font-weight-semibold` | `600` | 标题的半粗字重 |
1566
+ | `--wui-line-height-tight` | `1.2` | 大号标题的紧行高 |
1567
+ | `--wui-line-height-snug` | `1.4` | 中号标题的适中行高 |
1568
+ | `--wui-line-height-normal` | `1.5` | 正文的常规行高 |
1569
+ | `--wui-line-height-relaxed` | `1.6` | 长文阅读的宽松行高 |
1570
+
1571
+ 组件里的 `line-height: 1` 与 `line-height: 0` 刻意不做 token:前者是控件内单行标签的垂直居中手段,后者是把包裹盒高收缩到内容——两者都不是排版行高,token 化会诱使调用方把它们当成行高去整体调节。
1572
+
1573
+ **间距 token:** 4px 基准的六级阶,止于 24px——组件层以生效的静态节奏字面量写入的最大间距值。(唯一更宽的是 `<web-ui-empty>` 的 `32px 24px`,它由 `--wui-empty-size` 在 JS 侧派生,属尺寸那根轴而非节奏,理由见下面的排除项。)它不替代组件各自的覆盖 token(`--wui-button-px`、`--wui-dialog-padding` 等),而是充当那些 token 的 fallback 默认值——组件仍然决定自己的 padding,间距阶只说明这个值在整体节奏里的位置。因此覆盖某一级,就等于给嵌入应用一根作用于所有已挂阶调用点的密度杠杆——它调的是节奏,不是结构,理由见下面的排除项。
1574
+
1575
+ | 属性 | 默认值 | 说明 |
1576
+ | --------------- | ------ | -------------------------- |
1577
+ | `--wui-space-1` | `4px` | 紧凑内边距(徽标、菜单) |
1578
+ | `--wui-space-2` | `8px` | 控件间距、组间距、行内间距 |
1579
+ | `--wui-space-3` | `12px` | 控件水平内边距 |
1580
+ | `--wui-space-4` | `16px` | 标准区块间距 |
1581
+ | `--wui-space-5` | `20px` | 浮层内容内边距 |
1582
+ | `--wui-space-6` | `24px` | 宽浮层内边距 |
1583
+
1584
+ 这一阶刻意不含 `1px`、`2px`、`6px`、`7.5px`、`10px` 和负值。`1px` 是发丝线和描边环的结构宽度;`2px`、`6px`、`7.5px` 是光学修正;`10px` 是 4px 阶之外的半档耦合值;负 margin 用来抵消 flex gap 或按钮的 padding。它们的「为什么是这个数」各自独立,没有共享间距语义,收进同一把尺子只会让覆盖其中之一时误伤其余。
1585
+
1586
+ 第二类排除按语义而非按取值:对齐视口边缘或宿主自身内容边缘的偏移量,即使正好落在 4px 阶上也不挂阶——`--wui-image-preview-edge-gap`、`--wui-toast-viewport-gap`、`--wui-layout-mobile-toggle-inset`、`--wui-back-top-right` / `--wui-back-top-left` / `--wui-back-top-bottom`、`--wui-drawer-inset` 与 `--wui-drawer-close-right`。它们的参照系是屏幕或宿主的内容边缘,不是相邻元素之间的节奏;并进间距阶会让「收紧 `--wui-space-2`」意外把组件推离屏幕边缘。`--wui-drawer-close-top` 是反例:它经 `--wui-space-4` 解析,因为它要跟随 drawer 的 header padding,两者必须一起动。
1587
+
1588
+ 第三类排除是由组件**尺寸** prop 派生、而非写成节奏值的间距:`<web-ui-empty>` 让 `padding`、`min-block-size` 与图形尺寸跟随 `--wui-empty-size` 缩放,`40` / `56` / `72` 三档实测 padding 分别是 `23px 17px` / `32px 24px` / `41px 31px`。这六个值里有四个是奇数,4px 阶根本无法表示;`56` 档的 `32px 24px` 落在基准上纯属尺寸选值的巧合——`56` 是三档里唯一被 7 整除的,商恰为 8,而 8 本身是 4 的倍数,取整在这一档根本没有发生。一根在三个尺寸里有两个静默失效、只在第三个上碰巧生效的密度杠杆,比没有杠杆更糟;何况整组值本来就随 `size` 走,因此不挂阶。这里直接写 px 即可。
1589
+
1590
+ 覆盖必须落在 `<web-ui-theme>` 作用域内,因为 theme 宿主只为自己的子树声明这些级。已知边界:`assets/*.css` 的浮层样式有两条注入路径——经 theme 自带的 overlay root 注入时间距阶生效,退到 document 级 fallback overlay root(一个看不到 theme token 的普通 `div`)时,浮层间距恒取字面量 fallback。
1591
+
1417
1592
  **选择控件 token(radio、checkbox):**
1418
1593
 
1419
- | 属性 | 默认值 | 说明 |
1420
- | ------------------------------ | ------ | ------------------------------------ |
1421
- | `--wui-selection-control-size` | `18px` | 指示器(圆点 / 方框)宽高 |
1422
- | `--wui-radio-group-gap` | `8px` | `<web-ui-radio-group>` 的成员间距 |
1423
- | `--wui-checkbox-group-gap` | `8px` | `<web-ui-checkbox-group>` 的成员间距 |
1594
+ | 属性 | 默认值 | 说明 |
1595
+ | ------------------------------ | -------------------- | ------------------------------------ |
1596
+ | `--wui-selection-control-size` | `18px` | 指示器(圆点 / 方框)宽高 |
1597
+ | `--wui-radio-group-gap` | `var(--wui-space-2)` | `<web-ui-radio-group>` 的成员间距 |
1598
+ | `--wui-checkbox-group-gap` | `var(--wui-space-2)` | `<web-ui-checkbox-group>` 的成员间距 |
1424
1599
 
1425
1600
  **圆角 token:**
1426
1601
 
@@ -1434,7 +1609,6 @@ SVG 线条绘制动画,基于 `stroke-dashoffset`。直接在原元素上动
1434
1609
 
1435
1610
  | 属性 | 默认值 | 说明 |
1436
1611
  | ---------------------------- | ------ | -------------- |
1437
- | `--wui-layer-base` | `0` | 基础内容 |
1438
1612
  | `--wui-layer-inline-overlay` | `1` | 组件内部浮层 |
1439
1613
  | `--wui-layer-header` | `10` | 页面 Header |
1440
1614
  | `--wui-layer-auxiliary` | `20` | 悬浮工具控件 |
@@ -1450,41 +1624,41 @@ SVG 线条绘制动画,基于 `stroke-dashoffset`。直接在原元素上动
1450
1624
 
1451
1625
  **颜色 token:**
1452
1626
 
1453
- | 属性 | 浅色默认值 | 深色默认值 | 说明 |
1454
- | ---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ---------------------- |
1455
- | `--wui-color-page` | `#fff` | `#242628` | 页面背景 |
1456
- | `--wui-color-surface` | `#fff` | `#2c2c2e` | 普通 表面 |
1457
- | `--wui-color-surface-raised` | `#f2f2f7` | `#2c2c2e` | 抬升表面 |
1458
- | `--wui-color-surface-control` | `#dfdfdf` | `#3a3a3c` | 中性可交互控件表面 |
1459
- | `--wui-color-surface-track` | `#e5e5ea` | `#444446` | Slider/Switch 轨道表面 |
1460
- | `--wui-color-surface-menu` | `rgb(254 254 254 / 0.76)` | `rgb(49 48 50 / 0.74)` | Menu 和浮动面板表面 |
1461
- | `--wui-color-surface-glass` | `rgb(250 250 250 / 0.34)` | `rgb(44 44 46 / 0.42)` | 液态玻璃表面 |
1462
- | `--wui-color-surface-glass-hover` | `color-mix(... text 6%, surface-glass)` | `color-mix(... text 6%, surface-glass)` | Glass 完整悬停背景 |
1463
- | `--wui-color-surface-glass-active` | `color-mix(... text 15%, surface-glass)` | `color-mix(... text 15%, surface-glass)` | Glass 完整按下背景 |
1464
- | `--wui-color-surface-overlay` | `rgb(246 246 246 / 0.82)` | `rgb(32 34 34 / 0.9)` | 半透明浮层表面 |
1465
- | `--wui-color-surface-sidebar` | `rgb(233 233 233 / 0.82)` | `rgb(44 44 46 / 0.8)` | 侧边栏面板表面 |
1466
- | `--wui-color-surface-segmented` | `#e5e5ea` | `#3a3a3c` | Segmented 指示器表面 |
1467
- | `--wui-color-surface-selected` | `#fff` | `#5c5c5e` | 选中表面 |
1468
- | `--wui-color-text` | `#1b1b1b` | `#e9eaea` | 主要文本 |
1469
- | `--wui-color-text-secondary` | `#6a6a6a` | `#a1a1a6` | 次要文本 |
1470
- | `--wui-color-text-tertiary` | `color-mix(in srgb, var(--wui-color-text) 35%, transparent)` | `color-mix(in srgb, var(--wui-color-text) 42%, transparent)` | 三级文本和弱意图图标 |
1471
- | `--wui-color-text-disabled` | `color-mix(in srgb, var(--wui-color-text) 32%, transparent)` | `color-mix(in srgb, var(--wui-color-text) 38%, transparent)` | 禁用态前景文本 |
1472
- | `--wui-color-state-layer-hover` | `color-mix(in srgb, var(--wui-color-text) 6%, transparent)` | `color-mix(in srgb, var(--wui-color-text) 6%, transparent)` | 透明悬停层 |
1473
- | `--wui-color-state-layer-active` | `color-mix(in srgb, var(--wui-color-text) 15%, transparent)` | `color-mix(in srgb, var(--wui-color-text) 15%, transparent)` | 透明按下层 |
1474
- | `--wui-color-border` | `rgb(0 0 0 / 0.1)` | `rgb(255 255 255 / 0.14)` | 常规边框和分隔线 |
1475
- | `--wui-color-glass-border` | `transparent` | `rgb(255 255 255 / 0.05)` | Glass 边框色调 |
1476
- | `--wui-color-glass-highlight` | `rgb(255 255 255 / 0.9)` | `rgb(255 255 255 / 0.1)` | Glass 高光边缘 |
1477
- | `--wui-color-glass-corner` | `rgb(255 255 255 / 0.5)` | `rgb(255 255 255 / 0.2)` | Glass 边框角落光泽 |
1478
- | `--wui-color-glass-shade` | `rgb(0 0 0 / 0.03)` | `rgb(0 0 0 / 0.5)` | Glass 边框背光角压暗 |
1479
- | `--wui-color-accent` | `#08f` | `#0a84ff` | Accent 和输入焦点边框 |
1480
- | `--wui-color-on-accent` | `#fff` | `#fff` | Accent 上的前景色 |
1481
- | `--wui-color-on-control` | `#fff` | `#f2f2f7` | 控件内芯前景色 |
1482
- | `--wui-color-success` | `#16a34a` | `#30d158` | 成功 |
1483
- | `--wui-color-warning` | `#d97706` | `#ff9f0a` | 警告 |
1484
- | `--wui-color-danger` | `#dc2626` | `#ff453a` | 危险 |
1485
- | `--wui-color-info` | `#2563eb` | `#64d2ff` | 信息 |
1486
- | `--wui-color-backdrop` | `rgb(0 0 0 / 0.12)` | `rgb(0 0 0 / 0.48)` | Modal 遮罩 |
1487
- | `--wui-color-focus-ring` | `rgb(0 136 255 / 0.4)` | `rgb(10 132 255 / 0.62)` | Focus 指示器颜色 |
1627
+ | 属性 | 浅色默认值 | 深色默认值 | 说明 |
1628
+ | ---------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------- |
1629
+ | `--wui-color-page` | `#fff` | `#242628` | 页面背景 |
1630
+ | `--wui-color-surface` | `#fff` | `#2c2c2e` | 普通 表面 |
1631
+ | `--wui-color-surface-raised` | `#f2f2f7` | `#2c2c2e` | 抬升表面 |
1632
+ | `--wui-color-surface-control` | `#dfdfdf` | `#3a3a3c` | 中性可交互控件表面 |
1633
+ | `--wui-color-surface-track` | `#e5e5ea` | `#444446` | Slider/Switch 轨道表面 |
1634
+ | `--wui-color-surface-menu` | `rgb(250 250 250 / 0.76)` | `rgb(49 48 50 / 0.74)` | Menu 和浮动面板表面 |
1635
+ | `--wui-color-surface-glass` | `rgb(250 250 250 / 0.34)` | `rgb(44 44 46 / 0.42)` | 液态玻璃表面 |
1636
+ | `--wui-color-surface-glass-hover` | `color-mix(... text 6%, surface-glass)` | `color-mix(... text 6%, surface-glass)` | Glass 完整悬停背景 |
1637
+ | `--wui-color-surface-glass-active` | `color-mix(... text 15%, surface-glass)` | `color-mix(... text 15%, surface-glass)` | Glass 完整按下背景 |
1638
+ | `--wui-color-surface-overlay` | `rgb(248 248 248 / 0.92)` | `rgb(32 34 34 / 0.92)` | 半透明浮层表面 |
1639
+ | `--wui-color-surface-sidebar` | `rgb(233 233 233 / 0.82)` | `rgb(44 44 46 / 0.8)` | 侧边栏面板表面 |
1640
+ | `--wui-color-surface-segmented` | `#e5e5ea` | `#3a3a3c` | Segmented 指示器表面 |
1641
+ | `--wui-color-surface-selected` | `#fff` | `#5c5c5e` | 选中表面 |
1642
+ | `--wui-color-text` | `#1b1b1b` | `#e9eaea` | 主要文本 |
1643
+ | `--wui-color-text-secondary` | `#6a6a6a` | `#a1a1a6` | 次要文本 |
1644
+ | `--wui-color-text-tertiary` | `color-mix(in srgb, var(--wui-color-text) 35%, transparent)` | `color-mix(in srgb, var(--wui-color-text) 42%, transparent)` | 三级文本和弱意图图标 |
1645
+ | `--wui-color-text-disabled` | `color-mix(in srgb, var(--wui-color-text) 32%, transparent)` | `color-mix(in srgb, var(--wui-color-text) 38%, transparent)` | 禁用态前景文本 |
1646
+ | `--wui-color-state-layer-hover` | `color-mix(in srgb, var(--wui-color-text) 6%, transparent)` | `color-mix(in srgb, var(--wui-color-text) 6%, transparent)` | 透明悬停层 |
1647
+ | `--wui-color-state-layer-active` | `color-mix(in srgb, var(--wui-color-text) 15%, transparent)` | `color-mix(in srgb, var(--wui-color-text) 15%, transparent)` | 透明按下层 |
1648
+ | `--wui-color-border` | `rgb(0 0 0 / 0.1)` | `rgb(255 255 255 / 0.14)` | 常规边框和分隔线 |
1649
+ | `--wui-color-glass-highlight` | `rgb(255 255 255 / 0.9)` | `rgb(255 255 255 / 0.1)` | 表面 inset 高光 |
1650
+ | `--wui-color-glass-ring` | `rgb(0 0 0 / 0.05)` | `rgb(255 255 255 / 0.05)` | 1px 描边环底色 |
1651
+ | `--wui-color-glass-ring-sheen` | `rgb(255 255 255 / 0.5)` | `rgb(255 255 255 / 0.2)` | 描边环角落受光光泽 |
1652
+ | `--wui-color-glass-ring-shade` | `rgb(0 0 0 / 0.03)` | `rgb(0 0 0 / 0.5)` | 描边环角落背光压暗 |
1653
+ | `--wui-color-accent` | `#08f` | `#0a84ff` | Accent 和输入焦点边框 |
1654
+ | `--wui-color-on-accent` | `#fff` | `#fff` | Accent 上的前景色 |
1655
+ | `--wui-color-on-control` | `#fff` | `#f2f2f7` | 控件内芯前景色 |
1656
+ | `--wui-color-success` | `#16a34a` | `#30d158` | 成功 |
1657
+ | `--wui-color-warning` | `#d97706` | `#ff9f0a` | 警告 |
1658
+ | `--wui-color-danger` | `#dc2626` | `#ff453a` | 危险 |
1659
+ | `--wui-color-info` | `#2563eb` | `#64d2ff` | 信息 |
1660
+ | `--wui-color-backdrop` | `rgb(0 0 0 / 0.12)` | `rgb(0 0 0 / 0.48)` | Modal 遮罩 |
1661
+ | `--wui-color-focus-ring` | `color-mix(in srgb, var(--wui-color-accent, #08f) 40%, transparent)` | `color-mix(in srgb, var(--wui-color-accent, #0a84ff) 62%, transparent)` | Focus 指示器颜色,由 accent 派生 |
1488
1662
 
1489
1663
  **阴影 token:**
1490
1664