@greypan/web-ui 9.0.0 → 10.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.
- package/README.CN.md +102 -31
- package/README.md +115 -42
- package/dist/assets/modal-surface.js +6 -0
- package/dist/assets/modal-surface.js.map +1 -0
- package/dist/components/context-menu/index.d.ts +15 -0
- package/dist/components/context-menu/index.js +50 -46
- package/dist/components/context-menu/index.js.map +1 -1
- package/dist/components/context-menu/scrim.js +1 -1
- package/dist/components/context-menu/scrim.js.map +1 -1
- package/dist/components/dialog/index.d.ts +30 -5
- package/dist/components/dialog/index.js +53 -33
- package/dist/components/dialog/index.js.map +1 -1
- package/dist/components/dialog/style.js +1 -1
- package/dist/components/dialog/style.js.map +1 -1
- package/dist/components/drawer/index.js +54 -49
- package/dist/components/drawer/index.js.map +1 -1
- package/dist/components/drawer/style.js +1 -1
- package/dist/components/drawer/style.js.map +1 -1
- package/dist/components/image-preview/index.js +66 -61
- package/dist/components/image-preview/index.js.map +1 -1
- package/dist/components/image-preview/style.js +1 -1
- package/dist/components/image-preview/style.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +17 -16
- package/dist/components/input/style.js +1 -1
- package/dist/components/input/style.js.map +1 -1
- package/dist/components/input-number/style.js +1 -1
- package/dist/components/input-number/style.js.map +1 -1
- package/dist/components/layout/index.d.ts +22 -4
- package/dist/components/layout/index.js +46 -26
- package/dist/components/layout/index.js.map +1 -1
- package/dist/components/layout/style.js +1 -1
- package/dist/components/layout/style.js.map +1 -1
- package/dist/components/middle-ellipsis/index.d.ts +100 -0
- package/dist/components/middle-ellipsis/index.js +112 -0
- package/dist/components/middle-ellipsis/index.js.map +1 -0
- package/dist/components/middle-ellipsis/style.js +6 -0
- package/dist/components/middle-ellipsis/style.js.map +1 -0
- package/dist/components/middle-ellipsis/truncate.d.ts +62 -0
- package/dist/components/middle-ellipsis/truncate.js +45 -0
- package/dist/components/middle-ellipsis/truncate.js.map +1 -0
- package/dist/components/textarea/style.js +1 -1
- package/dist/components/textarea/style.js.map +1 -1
- package/dist/scrollbars/index.d.ts +33 -0
- package/dist/scrollbars/index.js +18 -0
- package/dist/scrollbars/index.js.map +1 -0
- package/dist/scrollbars/style.css +10 -0
- package/dist/shared/element-width/index.js +37 -0
- package/dist/shared/element-width/index.js.map +1 -0
- package/package.json +14 -2
package/README.CN.md
CHANGED
|
@@ -31,6 +31,29 @@ subpath 导入只注册它点名的那个组件,页面用到的每个标签都
|
|
|
31
31
|
<web-ui-button variant="primary">点击我</web-ui-button> <web-ui-icon .icon="${lucidePlus}"></web-ui-icon>
|
|
32
32
|
```
|
|
33
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
|
+
|
|
34
57
|
## 框架集成
|
|
35
58
|
|
|
36
59
|
### 跨框架 API 约定
|
|
@@ -276,6 +299,7 @@ dropdown、tooltip)不需要它。
|
|
|
276
299
|
| **数据展示** | [`<web-ui-avatar>`](#web-ui-avatar) |
|
|
277
300
|
| | [`<web-ui-badge>`](#web-ui-badge) |
|
|
278
301
|
| | [`<web-ui-empty>`](#web-ui-empty) |
|
|
302
|
+
| | [`<web-ui-middle-ellipsis>`](#web-ui-middle-ellipsis) |
|
|
279
303
|
| | [`<web-ui-icon>`](#web-ui-icon) |
|
|
280
304
|
| | [`<web-ui-spinner>`](#web-ui-spinner) |
|
|
281
305
|
| **布局与工具** | [`<web-ui-layout>`](#web-ui-layout) |
|
|
@@ -775,46 +799,58 @@ web-ui-radio-group {
|
|
|
775
799
|
|
|
776
800
|
模态对话框,使用原生 `<dialog>` 的 `showModal()`。
|
|
777
801
|
|
|
778
|
-
| 属性 | 类型 | 默认值 | 说明
|
|
779
|
-
| ------------------- | --------- | ------- |
|
|
780
|
-
| `open` | `boolean` | `false` | 对话框可见性
|
|
781
|
-
| `closable` | `boolean` | `false` | 显示内置关闭按钮
|
|
782
|
-
| `no-scroll-lock` | `boolean` | `false` | 打开时不锁定页面滚动
|
|
783
|
-
| `no-backdrop-close` | `boolean` | `false` | 禁止点击遮罩关闭
|
|
784
|
-
| `no-escape-close` | `boolean` | `false` | 禁止按 Escape 关闭
|
|
785
|
-
| `controlled` | `boolean` | `false` | Escape 与遮罩仅请求 `open=false`,由 Consumer 回写 `open`
|
|
802
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
803
|
+
| ------------------- | --------- | ------- | ----------------------------------------------------------------------------------------------- |
|
|
804
|
+
| `open` | `boolean` | `false` | 对话框可见性 |
|
|
805
|
+
| `closable` | `boolean` | `false` | 显示内置关闭按钮 |
|
|
806
|
+
| `no-scroll-lock` | `boolean` | `false` | 打开时不锁定页面滚动 |
|
|
807
|
+
| `no-backdrop-close` | `boolean` | `false` | 禁止点击遮罩关闭 |
|
|
808
|
+
| `no-escape-close` | `boolean` | `false` | 禁止按 Escape 关闭 |
|
|
809
|
+
| `controlled` | `boolean` | `false` | Escape 与遮罩仅请求 `open=false`,由 Consumer 回写 `open` |
|
|
810
|
+
| `heading` | `string` | `''` | 标题文字;`header` 插槽为空时的 fallback(`headless` 模式下忽略) |
|
|
811
|
+
| `headless` | `boolean` | `false` | 只保留浮层行为;`default` 插槽渲染为卡片内唯一的滚动层,不再渲染内置 header 带、内容区与 footer |
|
|
812
|
+
| `dialog-label` | `string` | `''` | 内部原生 dialog 的 accessible name;`headless` 模式下必填 |
|
|
786
813
|
|
|
787
814
|
**事件:** `open-change` (`CustomEvent<{ open: boolean }>`)。启用 `controlled` 后,Escape 与遮罩点击仅请求 `open=false`,Consumer 写入 `open=false` 前对话框保持打开。程序化 API(`showModal()`/`close()`/直接赋值 `open`)不受影响,始终直通且不派发事件。原生 dialog 关闭(如表单 `method="dialog"`)会被恢复为受控打开状态并重新发出同一请求。
|
|
788
815
|
|
|
789
|
-
**插槽:** `
|
|
816
|
+
**插槽:** `header`, `default`, `footer`。`headless` 下只渲染 `default` 插槽。
|
|
817
|
+
|
|
818
|
+
`header` 槽是卡片的 chrome 带:不参与滚动,自带水平 padding(`--wui-dialog-inline-padding`),内容不会贴到卡片内沿。`heading` 属性是它的 fallback——槽一旦有分配元素就整条接管,否则非空的 `heading` 渲染出 `.wui-dialog-heading`;两者都为空时整条带隐藏。
|
|
819
|
+
|
|
820
|
+
`default` 槽在两种模式下都承载正文。默认模式下它渲染进 `.desc`(卡片的内容区),与 `header` 带、`footer` 并列。`headless` 下整张卡片变成一个滚动层:`default` 槽渲染进 `.wui-dialog-content`,`header` 带、`.desc` 与 `footer` 一概不渲染,`heading` 一并失效。`headless` 与 `<web-ui-drawer>` 是同一套机制——显式布尔属性,不是「往某个插槽里塞东西」的开关。
|
|
821
|
+
|
|
822
|
+
**accessible name:** 设置了 `dialog-label` 时,内部原生 `<dialog>` 直接取它的 `aria-label`;否则在默认模式且 `header` 带可见时,取指向该带的 `aria-labelledby="wui-dialog-heading"`。`headless` 没有可指向的内置标题,所以**名字必须由 Consumer 提供**——`dialog-label`,或自绘的可关联标题元素。
|
|
790
823
|
|
|
791
824
|
**方法:** `showModal()`, `close()`
|
|
792
825
|
|
|
793
826
|
使用原生 `<dialog>`,`@cancel` 阻止默认关闭行为。除非存在 `no-escape-close`,否则 Escape 调用 `close()`;除非存在 `no-backdrop-close`,否则点击遮罩关闭。启用 `controlled` 后,两者都只派发关闭请求而不自关闭。
|
|
794
827
|
|
|
795
|
-
启用 `closable`
|
|
828
|
+
启用 `closable` 后,默认模式与 `headless` 模式都会渲染内置关闭按钮,且与 Escape、遮罩点击走同一条关闭路径(`controlled` 对它同样生效)。两种模式的位置已统一:按钮都绝对定位到卡片右上角,距卡片两沿的偏移由 `--wui-dialog-close-top` / `--wui-dialog-close-right` 控制(默认都是 `20px`;drawer 关闭按钮仍为 `16px`)。只有 `headless` 会把整卡换掉;默认模式下 `header` 带会为按钮留出避让,长内容不会钻到按钮下面。图标与 drawer 关闭按钮同为 `ooui:close`。不启用 `closable` 时什么都不渲染——既没有按钮,也没有多余的包裹层。
|
|
796
829
|
|
|
797
830
|
> **Escape 归属**:Escape 由共享仲裁者统一判定,一次按键只关闭**最内层**的已打开浮层(popover、select、autocomplete、dropdown、context-menu、drawer、dialog 都参与)。例如在 drawer 内打开 select,第一次 Escape 只关 select,第二次才关 drawer。互不嵌套的并列浮层按打开顺序关闭最上层。正在播退场过渡的面板仍在场上,也仍由它接住 Escape——但只要还有别的浮层开着,那一次按键仍然归该层,「一次按键关一层」不因此改变。`image-preview` 同样参与:它的原生 `<dialog>` 会登记进同一个仲裁者,Escape 按层级判定;组件的 `cancel` handler 只是拦掉原生的瞬时关闭,把 top layer 保留到退场过渡结束。
|
|
798
831
|
|
|
799
832
|
**CSS 自定义属性:**
|
|
800
833
|
|
|
801
|
-
| 属性 | 默认值 | 说明
|
|
802
|
-
| --------------------------------- | ------------------------------------------ |
|
|
803
|
-
| `--wui-dialog-width` | `360px` | 对话框宽度
|
|
804
|
-
| `--wui-dialog-max-height` | `90vh` / `min(90vh, 90dvh)`(dvh 引擎) | 内容区(`.desc`)的最大高度,不是整卡高度。两套 fallback:基础规则 `90vh`,`@supports (height: 100dvh)` 内 `min(90vh, 90dvh)`——不支持 dvh 的引擎只拿到 `90vh`。整卡换算见下方说明
|
|
805
|
-
| `--wui-dialog-close-top` | `
|
|
806
|
-
| `--wui-dialog-close-right` | `
|
|
807
|
-
| `--wui-dialog-overlay-bg` | `var(--wui-color-backdrop)` | 遮罩背景色
|
|
808
|
-
| `--wui-dialog-bg` | `var(--wui-color-surface-overlay)` | 玻璃卡片背景色,回退到 `rgb(248 248 248 / 0.92)`
|
|
809
|
-
| `--wui-dialog-padding` | `20px 24px 24px` |
|
|
810
|
-
| `--wui-dialog-
|
|
811
|
-
| `--wui-dialog-
|
|
812
|
-
| `--wui-dialog-desc-
|
|
813
|
-
| `--wui-dialog-
|
|
814
|
-
| `--wui-dialog-footer-
|
|
815
|
-
| `--wui-dialog-
|
|
816
|
-
|
|
817
|
-
|
|
834
|
+
| 属性 | 默认值 | 说明 |
|
|
835
|
+
| --------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
836
|
+
| `--wui-dialog-width` | `360px` | 对话框宽度 |
|
|
837
|
+
| `--wui-dialog-max-height` | `90vh` / `min(90vh, 90dvh)`(dvh 引擎) | 内容区(`.desc`)的最大高度,不是整卡高度。两套 fallback:基础规则 `90vh`,`@supports (height: 100dvh)` 内 `min(90vh, 90dvh)`——不支持 dvh 的引擎只拿到 `90vh`。整卡换算见下方说明 |
|
|
838
|
+
| `--wui-dialog-close-top` | `20px` | `closable` 按钮距卡片顶部的偏移 |
|
|
839
|
+
| `--wui-dialog-close-right` | `20px` | `closable` 按钮距卡片右沿的偏移 |
|
|
840
|
+
| `--wui-dialog-overlay-bg` | `var(--wui-color-backdrop)` | 遮罩背景色 |
|
|
841
|
+
| `--wui-dialog-bg` | `var(--wui-color-surface-overlay)` | 玻璃卡片背景色,回退到 `rgb(248 248 248 / 0.92)` |
|
|
842
|
+
| `--wui-dialog-padding` | `20px 24px 24px` | 卡片的 block 内边距。inline 分量不再作用于卡片:水平内边距移到了各分区(`.header` / `.desc` / `.wui-dialog-footer`),见 `--wui-dialog-inline-padding` |
|
|
843
|
+
| `--wui-dialog-inline-padding` | `24px` | `header` 带、内容区(`.desc`)与 footer 的水平内边距。卡片自身没有 inline padding,内容区的滚动口(其 border box)因此与卡片内沿重合——滚动条贴边——而内容仍保留这个内缩。默认取自 `--wui-space-6` |
|
|
844
|
+
| `--wui-dialog-title-gap` | `16px` | 标题下方间距 |
|
|
845
|
+
| `--wui-dialog-desc-gap` | `24px` | 正文内容下方间距 |
|
|
846
|
+
| `--wui-dialog-desc-focus-padding` | `6px` | 内容区**block 轴**预留的 focus-ring 余量。`.desc` 既是滚动容器,又因为有一条轴必须非 `visible` 而成了裁剪盒,所以紧贴其上下沿的控件,ring 会被 padding box 裁掉;这个余量覆盖 `5px` 的 ring(`offset: 2` + `width: 3`)。横向的 ring 余量改由 `--wui-dialog-inline-padding` 提供(24px ≫ 5px),所以本 token 只作用于 block 轴——原来的 inline 分量不再生效。成对负 margin 使 chrome 高度保持不变 |
|
|
847
|
+
| `--wui-dialog-footer-gap` | `10px` / horizontal `12px` | Footer 按钮间距 |
|
|
848
|
+
| `--wui-dialog-footer-justify` | `flex-end`(默认)/ `center`(horizontal) | Footer `justify-content`;horizontal 模式下覆盖为 `flex-end` 可右对齐 |
|
|
849
|
+
| `--wui-dialog-scale-enter` | `1.1` | 进场缩放起点:由 `1.1` 收缩到 `1`,退场反向 |
|
|
850
|
+
|
|
851
|
+
> **高度语义**:本 token 限制的是内容区,不是整卡。`header` 带与 footer 仍由内容撑开,超出上限的部分在 `.desc` 内部滚动;整卡本身只由字面量 `100vh` / `100dvh` 兜底,接住「视口太矮、chrome 加内容区上限也放不下」的情况。要给**整卡**定上限的宿主仍需自行量出 chrome(卡片 block 内边距 + header 带 + 间距 + footer)并从 token 里减掉——Interweave 前端三个 dialog 分别减 142、142、158。不再需要这个数的是「让内容区滚起来」:改动前 `.desc` 既没有上限也没有 `overflow`,唯一能滚的是宿主自己撑出来的内层盒子,宿主想让它滚就必须自己算这个高度。
|
|
852
|
+
>
|
|
853
|
+
> **`headless` 模式没有 `.desc`**,所以该 token 在这一档不生效。`headless` 下的 `default` 插槽被包进 `.wui-dialog-content`,滚动发生在这一层;卡片在内容装得下时仍贴合内容,装不下时由同一条字面量 `100vh` / `100dvh` 兜底。内置关闭按钮在内容滚动时保持 `--wui-dialog-close-top` / `--wui-dialog-close-right` 给出的位置不变。
|
|
818
854
|
|
|
819
855
|
#### `<web-ui-drawer>`
|
|
820
856
|
|
|
@@ -1265,6 +1301,30 @@ Hover 模式使用 `pointerenter`/`pointerleave` 加延迟控制。Click 模式
|
|
|
1265
1301
|
| `--wui-empty-description-font-size` | `14px` | 描述字号 |
|
|
1266
1302
|
| `--wui-empty-action-margin-top` | `20px` | 操作区上方的间距 |
|
|
1267
1303
|
|
|
1304
|
+
#### `<web-ui-middle-ellipsis>`
|
|
1305
|
+
|
|
1306
|
+
单行中间省略:保留首尾、丢掉中间——`very-long-file-…-abcdefghij.txt`——而 `text-overflow: ellipsis` 只能保住头部。
|
|
1307
|
+
|
|
1308
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
1309
|
+
| ----------------- | -------- | ------ | -------------------------------------------------------------------------------------------------------- |
|
|
1310
|
+
| `text` | `string` | `''` | 完整文本。组件只读它,不回写。 |
|
|
1311
|
+
| `marker` | `string` | `'…'` | 插在两端之间的标记。空串即截断但不给任何可见信号。 |
|
|
1312
|
+
| `marker-position` | `number` | `50` | 标记所在的横向比例,0–100。`0` 贴行末(只剩头部),`100` 贴行首(只剩尾部)。越界夹紧,`NaN` 回退 `50`。 |
|
|
1313
|
+
|
|
1314
|
+
使用条件与 `text-overflow: ellipsis` 相同:**单行、且有确定行内尺寸**。块级盒子、flex 行里的 `flex: 1; min-width: 0`、grid 轨道都可以。宿主按自身内容撑宽时不存在可用空间,组件按量到的宽度切一次就停止继续收缩,而不是一路塌到只剩标记。
|
|
1315
|
+
|
|
1316
|
+
文件名场景靠 `marker-position` 调节,头部是路径噪声、尾部才是扩展名时取大值。下表是示意而非可复现的取值——确切切点取决于字体——取自 16px `system-ui`、240px 盒子:
|
|
1317
|
+
|
|
1318
|
+
| `marker-position` | 240px 盒子里 |
|
|
1319
|
+
| ----------------- | --------------------------------- |
|
|
1320
|
+
| `0` | `very-long-file-name-abcdefghi…` |
|
|
1321
|
+
| `50`(默认) | `very-long-file-…-abcdefghij.txt` |
|
|
1322
|
+
| `80` | `very-l…file-name-abcdefghij.txt` |
|
|
1323
|
+
|
|
1324
|
+
切点一律落在**字素簇**边界上:代理对、组合符序列、ZWJ emoji 都不会被从中间劈开。截断期间完整原文写在文本元素的 `title` 上,放得下之后移除。宿主需为 `direction: ltr`;`direction: rtl` 下合串会被双向算法重排。
|
|
1325
|
+
|
|
1326
|
+
**已知限制——复制与辅助技术。** 元素里装的是屏幕上那串,因此选中复制拿到的是截断后的文本,读屏与页内查找命中的也是截断后的串。CSS 的 `text-overflow` 没有这两个问题(它从不改动文本节点),这是用 JS 算切点绕不开的代价;原文经 `title` 提供。保持全文留在 DOM 里的替代方案及其代价见 `docs/research/web-ui-middle-ellipsis-261007.md`。
|
|
1327
|
+
|
|
1268
1328
|
#### `<web-ui-icon>`
|
|
1269
1329
|
|
|
1270
1330
|
图标渲染组件。接受 Iconify 数据对象。
|
|
@@ -1338,13 +1398,24 @@ WebUiSpinner.hide() // 隐藏
|
|
|
1338
1398
|
| `sidebar-collapsed` | `boolean` | `false` | 桌面端侧边栏受控折叠状态 |
|
|
1339
1399
|
| `sidebar-open` | `boolean` | `false` | 移动端侧边栏 Drawer 受控打开状态 |
|
|
1340
1400
|
| `header-glow` | `boolean` | `false` | 在 header 插槽内容背后显示装饰性晕染 |
|
|
1401
|
+
| `mobile` | `boolean` | `false` | 只读派生值:当前视口是否处于移动端断点及以下 |
|
|
1341
1402
|
| `sidebar-width` | `string` | `'240px'` | 桌面端和移动端展开时的侧边栏宽度 |
|
|
1342
1403
|
| `collapsed-width` | `string` | `'72px'` | 桌面端折叠时的侧边栏宽度 |
|
|
1343
1404
|
| `sidebar-resizable` | `boolean` | `false` | 启用桌面端侧边栏右边缘拖拽调整宽度 |
|
|
1344
1405
|
| `sidebar-min-width` | `string` | — | 拖拽调整的下限(px);默认回退到 `collapsed-width` |
|
|
1345
1406
|
| `sidebar-max-width` | `string` | — | 拖拽调整的上限(px);钳制在视口一半以内,内置上限优先于配置值 |
|
|
1346
1407
|
|
|
1347
|
-
**事件:** `sidebar-collapsed-change`(`CustomEvent<{ collapsed: boolean }>`)用于请求更新桌面端折叠状态;`sidebar-open-change`(`CustomEvent<{ open: boolean }>`)用于请求更新移动端 Drawer 打开状态;`sidebar-width-change`(`CustomEvent<{ width: string }>`)用于请求在拖拽调整结束后更新侧边栏宽度。Consumer
|
|
1408
|
+
**事件:** `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 自己的派生视口状态。
|
|
1409
|
+
|
|
1410
|
+
**移动端视口状态:** `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]`。
|
|
1411
|
+
|
|
1412
|
+
**框架侧的消费方式:** layout 自身在 microtask 里重渲染,因此**在宏任务上响应的消费者会读到旧值,而新树已经在屏幕上了**:
|
|
1413
|
+
|
|
1414
|
+
- **Vue**:模板里绑 `@mobile-change`,用 `ref` 存值。Vue 在元素插入前就挂上模板监听(因此也能收到连接期那一次),且它的 flush 在 microtask,早于 layout 自己的更新。
|
|
1415
|
+
- **React**:用 ref 拿到元素,在 `useLayoutEffect` 里**先读 `layout.mobile` 再订阅**,并用 `flushSync` 回写。先读是为了拿到「挂在插入之后的监听收不到」的挂载初值;`flushSync` 是为了让运行时跨断点抢在 layout 重渲染之前——React 对 DOM 监听里的 setState 走宏任务 flush,否则会出现一帧「drawer 已按新状态渲染、consumer 的值还是旧的」(本仓 React demo 修复前实测 2/221 帧)。
|
|
1416
|
+
- **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:`。
|
|
1417
|
+
|
|
1418
|
+
**折叠只作用于桌面端:** `sidebar-collapsed` 只收窄桌面端侧边栏,布局不对移动端 Drawer 施加任何折叠几何。两条分支承载的是同一份 `sidebar` 插槽内容,所以侧边栏内容密度跟着该属性变的 Consumer 必须按 `mobile` 收窄这个条件(例如 `collapsed && !layout.mobile`,或 CSS 的 `web-ui-layout[mobile]`),否则桌面的折叠偏好会一路渲染进 Drawer。见 issue #195。
|
|
1348
1419
|
|
|
1349
1420
|
**侧边栏调整宽度:** 启用 `sidebar-resizable` 后,桌面端侧边栏右边缘会出现调整手柄(折叠状态下隐藏);悬停或拖拽时显示 3px 宽的强调色垂直线和 `col-resize` 光标。
|
|
1350
1421
|
|
|
@@ -1389,9 +1460,9 @@ WebUiSpinner.hide() // 隐藏
|
|
|
1389
1460
|
}
|
|
1390
1461
|
```
|
|
1391
1462
|
|
|
1392
|
-
在 `640px`
|
|
1463
|
+
在 `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 内边距对齐。
|
|
1393
1464
|
|
|
1394
|
-
`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`
|
|
1465
|
+
`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`)。
|
|
1395
1466
|
|
|
1396
1467
|
侧边栏卡片表面使用 `--wui-color-surface-sidebar`:浅色保持半透明中性分层,深色比 `--wui-color-page` 浅一档、与 `--wui-color-surface` 同级。它独立成 token,是因为 dialog、drawer 和 toast 共用 `--wui-color-surface-overlay`,可以采用不同表面。
|
|
1397
1468
|
|
|
@@ -1498,7 +1569,7 @@ SVG 线条绘制动画,基于 `stroke-dashoffset`。直接在原元素上动
|
|
|
1498
1569
|
| ---------------------------- | ------ | -------------------------------------------------- |
|
|
1499
1570
|
| `--wui-font-size-caption` | `12px` | 密集 chrome 标签(徽标、toast 时间戳、菜单分组头) |
|
|
1500
1571
|
| `--wui-font-size-readout` | `13px` | 数字读数(图片预览的计数与缩放比) |
|
|
1501
|
-
| `--wui-font-size` | `14px` |
|
|
1572
|
+
| `--wui-font-size` | `14px` | 正文与控件的基础字号(含控件的占位文本) |
|
|
1502
1573
|
| `--wui-font-size-title` | `18px` | 有界卡片标题(dialog、drawer) |
|
|
1503
1574
|
| `--wui-font-weight-medium` | `500` | 强调行内文字的中等字重 |
|
|
1504
1575
|
| `--wui-font-weight-semibold` | `600` | 标题的半粗字重 |
|
package/README.md
CHANGED
|
@@ -31,6 +31,31 @@ A subpath import registers only the component it names, so import every tag the
|
|
|
31
31
|
<web-ui-button variant="primary">Click me</web-ui-button> <web-ui-icon .icon="${lucidePlus}"></web-ui-icon>
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
## Scrollbars
|
|
35
|
+
|
|
36
|
+
`@greypan/web-ui/scrollbars` carries the repo-wide scrollbar behavior for [OverlayScrollbars](https://github.com/KingSora/OverlayScrollbars)
|
|
37
|
+
hosts: a macOS-aligned theme, auto-hide that reveals the bar while scrolling, and clicking the track scrolling to that spot.
|
|
38
|
+
Import the stylesheet once per document and pass the options to whichever adapter you initialize with.
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import '@greypan/web-ui/scrollbars.css'
|
|
42
|
+
import { webUiScrollbarsOptions } from '@greypan/web-ui/scrollbars'
|
|
43
|
+
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react' // or overlayscrollbars-vue
|
|
44
|
+
|
|
45
|
+
;<OverlayScrollbarsComponent options={webUiScrollbarsOptions}>{children}</OverlayScrollbarsComponent>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- `overlayscrollbars` is an optional peer dependency and is only needed by this subpath. The stylesheet already
|
|
49
|
+
contains the library's structural styles, so it replaces `import 'overlayscrollbars/overlayscrollbars.css'`.
|
|
50
|
+
- Keep the container's own `overflow` declaration (`overflow-y-auto`, …). OverlayScrollbars overrides it once it
|
|
51
|
+
takes over, but it is what keeps the container scrollable until then.
|
|
52
|
+
- The target must be a plain scroll container. OverlayScrollbars turns the host into a flex row and moves its
|
|
53
|
+
children into a generated viewport, so a host whose own layout carries meaning — a grid, a centred box, a `ul`/`ol`
|
|
54
|
+
— loses it.
|
|
55
|
+
- OverlayScrollbars only changes the element it is initialized on; children keep their native scrollbars. To cover
|
|
56
|
+
the document scroller, initialize `document.body` — `window.scrollY` / `window.scrollTo` keep their native
|
|
57
|
+
semantics there.
|
|
58
|
+
|
|
34
59
|
## Framework Setup
|
|
35
60
|
|
|
36
61
|
### Cross-framework API conventions
|
|
@@ -306,6 +331,7 @@ All form controls participate in native `FormData`, constraint validation, `form
|
|
|
306
331
|
| **Data Display** | [`<web-ui-avatar>`](#web-ui-avatar) |
|
|
307
332
|
| | [`<web-ui-badge>`](#web-ui-badge) |
|
|
308
333
|
| | [`<web-ui-empty>`](#web-ui-empty) |
|
|
334
|
+
| | [`<web-ui-middle-ellipsis>`](#web-ui-middle-ellipsis) |
|
|
309
335
|
| | [`<web-ui-icon>`](#web-ui-icon) |
|
|
310
336
|
| | [`<web-ui-spinner>`](#web-ui-spinner) |
|
|
311
337
|
| **Layout & Utility** | [`<web-ui-layout>`](#web-ui-layout) |
|
|
@@ -812,46 +838,58 @@ Set it on the group host. The token controls the long edge in both directions, s
|
|
|
812
838
|
|
|
813
839
|
Modal dialog using native `<dialog>` with `showModal()`.
|
|
814
840
|
|
|
815
|
-
| Attribute | Type | Default | Description
|
|
816
|
-
| ------------------- | --------- | ------- |
|
|
817
|
-
| `open` | `boolean` | `false` | Dialog visibility
|
|
818
|
-
| `closable` | `boolean` | `false` | Show the built-in close button
|
|
819
|
-
| `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open
|
|
820
|
-
| `no-backdrop-close` | `boolean` | `false` | Do not close on backdrop click
|
|
821
|
-
| `no-escape-close` | `boolean` | `false` | Do not close when Escape is pressed
|
|
822
|
-
| `controlled` | `boolean` | `false` | Escape and backdrop only request `open=false`; the consumer must update `open`
|
|
841
|
+
| Attribute | Type | Default | Description |
|
|
842
|
+
| ------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
843
|
+
| `open` | `boolean` | `false` | Dialog visibility |
|
|
844
|
+
| `closable` | `boolean` | `false` | Show the built-in close button |
|
|
845
|
+
| `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open |
|
|
846
|
+
| `no-backdrop-close` | `boolean` | `false` | Do not close on backdrop click |
|
|
847
|
+
| `no-escape-close` | `boolean` | `false` | Do not close when Escape is pressed |
|
|
848
|
+
| `controlled` | `boolean` | `false` | Escape and backdrop only request `open=false`; the consumer must update `open` |
|
|
849
|
+
| `heading` | `string` | `''` | Header text; fallback when the `header` slot is empty (ignored in `headless` mode) |
|
|
850
|
+
| `headless` | `boolean` | `false` | Keep only overlay behavior; render the `default` slot as the card's single scroll layer, without the built-in header band, content area or footer |
|
|
851
|
+
| `dialog-label` | `string` | `''` | Accessible name for the internal native dialog; required in `headless` mode |
|
|
823
852
|
|
|
824
853
|
**Events:** `open-change` (`CustomEvent<{ open: boolean }>`). With `controlled`, Escape and backdrop clicks only request `open=false` and the dialog stays open until the consumer writes `open=false`. Programmatic APIs (`showModal()`, `close()`, assigning `open`) stay direct and never emit. Native dialog closure (e.g. form `method="dialog"`) is restored to the controlled open state and re-emits the request.
|
|
825
854
|
|
|
826
|
-
**Slots:** `
|
|
855
|
+
**Slots:** `header`, `default`, `footer`. With `headless`, only the `default` slot is rendered.
|
|
856
|
+
|
|
857
|
+
The `header` slot is the card's chrome band: it does not scroll and it carries its own horizontal padding (`--wui-dialog-inline-padding`), so its content never touches the card edge. The `heading` attribute is its fallback — the slot wins whenever it has assigned elements, otherwise a non-empty `heading` renders a `.wui-dialog-heading`; when neither is present the band is hidden.
|
|
858
|
+
|
|
859
|
+
The `default` slot carries the body in both modes. By default it renders inside `.desc` — the card's content area — alongside the `header` band and the `footer`. With `headless` the whole card becomes one scroll layer: the `default` slot renders into `.wui-dialog-content`, and the `header` band, `.desc` and `footer` are not rendered at all, so `heading` is ignored. `headless` is the same mechanism as `<web-ui-drawer>`: an explicit boolean attribute, not a slot you have to fill to switch modes.
|
|
860
|
+
|
|
861
|
+
**Accessible name:** the internal native `<dialog>` takes `aria-label` from `dialog-label` when that is set; otherwise, in the default mode with a visible `header` band, it takes `aria-labelledby="wui-dialog-heading"` pointing at that band. `headless` renders no built-in title to point at, so **the consumer must supply the name** — `dialog-label`, or their own labelled element.
|
|
827
862
|
|
|
828
863
|
**Methods:** `showModal()`, `close()`
|
|
829
864
|
|
|
830
865
|
Uses native `<dialog>` with `@cancel` prevention. Escape calls `close()` unless `no-escape-close` is present. Click on backdrop closes dialog unless `no-backdrop-close` is present. With `controlled`, both only emit the close request instead.
|
|
831
866
|
|
|
832
|
-
With `closable`, a built-in close button is rendered in both
|
|
867
|
+
With `closable`, a built-in close button is rendered in both the default and `headless` modes, and it follows the same close path as Escape and backdrop clicks (so `controlled` applies to it identically). Both modes place it the same way: it is absolutely positioned at the card's top-right corner, offset from the card edges by `--wui-dialog-close-top` / `--wui-dialog-close-right` (both default to `20px`; the drawer close button remains at `16px`). Only `headless` replaces the whole card; in the default mode the `header` band reserves room for the button so long header content never runs under it. It uses the same `ooui:close` icon as the drawer close button. Omitting `closable` renders nothing at all — no button and no extra wrapper.
|
|
833
868
|
|
|
834
869
|
> **Escape ownership:** Escape is arbitrated by a single shared owner, so one keypress closes only the **innermost** open overlay (popover, select, autocomplete, dropdown, context-menu, drawer and dialog all take part). With a select open inside a drawer, the first Escape closes the select and only the second closes the drawer. Sibling overlays that do not nest fall back to open order, closing the most recently opened one. A panel that is still playing its exit transition stays on screen and still catches the Escape, but any overlay that is still open outranks it, so one keypress still closes exactly one layer. `image-preview` takes part as well: its native `<dialog>` is registered with the same arbiter, so Escape is decided by layer order; the component's `cancel` handler only vetoes the native instant close, keeping the top layer until the exit transition finishes.
|
|
835
870
|
|
|
836
871
|
**CSS Custom Properties:**
|
|
837
872
|
|
|
838
|
-
| Property | Default | Description
|
|
839
|
-
| --------------------------------- | -------------------------------------------- |
|
|
840
|
-
| `--wui-dialog-width` | `360px` | Dialog width
|
|
841
|
-
| `--wui-dialog-max-height` | `90vh` / `min(90vh, 90dvh)` (dvh engines) | Max height of the content area (`.desc`), not of the card. Two fallback tiers: `90vh` on the base rule, `min(90vh, 90dvh)` inside `@supports (height: 100dvh)` — an engine without `dvh` support only ever gets `90vh`. See the note below for the chrome conversion
|
|
842
|
-
| `--wui-dialog-close-top` | `
|
|
843
|
-
| `--wui-dialog-close-right` | `
|
|
844
|
-
| `--wui-dialog-overlay-bg` | `var(--wui-color-backdrop)` | Backdrop background
|
|
845
|
-
| `--wui-dialog-bg` | `var(--wui-color-surface-overlay)` | Glass card background, falls back to `rgb(248 248 248 / 0.92)`
|
|
846
|
-
| `--wui-dialog-padding` | `20px 24px 24px` |
|
|
847
|
-
| `--wui-dialog-
|
|
848
|
-
| `--wui-dialog-
|
|
849
|
-
| `--wui-dialog-desc-
|
|
850
|
-
| `--wui-dialog-
|
|
851
|
-
| `--wui-dialog-footer-
|
|
852
|
-
| `--wui-dialog-
|
|
853
|
-
|
|
854
|
-
|
|
873
|
+
| Property | Default | Description |
|
|
874
|
+
| --------------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
875
|
+
| `--wui-dialog-width` | `360px` | Dialog width |
|
|
876
|
+
| `--wui-dialog-max-height` | `90vh` / `min(90vh, 90dvh)` (dvh engines) | Max height of the content area (`.desc`), not of the card. Two fallback tiers: `90vh` on the base rule, `min(90vh, 90dvh)` inside `@supports (height: 100dvh)` — an engine without `dvh` support only ever gets `90vh`. See the note below for the chrome conversion |
|
|
877
|
+
| `--wui-dialog-close-top` | `20px` | `closable` button offset from the card top |
|
|
878
|
+
| `--wui-dialog-close-right` | `20px` | `closable` button offset from the card right edge |
|
|
879
|
+
| `--wui-dialog-overlay-bg` | `var(--wui-color-backdrop)` | Backdrop background |
|
|
880
|
+
| `--wui-dialog-bg` | `var(--wui-color-surface-overlay)` | Glass card background, falls back to `rgb(248 248 248 / 0.92)` |
|
|
881
|
+
| `--wui-dialog-padding` | `20px 24px 24px` | Block padding of the card. Its inline component no longer applies to the card: the horizontal padding moved onto the regions, see `--wui-dialog-inline-padding` |
|
|
882
|
+
| `--wui-dialog-inline-padding` | `24px` | Horizontal padding of the `header` band, the content area (`.desc`) and the footer. The card itself has no inline padding, so the content area's scroll port (its border box) coincides with the card edge — the scrollbar sits flush — while the content keeps this inset. The default comes from `--wui-space-6` |
|
|
883
|
+
| `--wui-dialog-title-gap` | `16px` | Spacing below the title |
|
|
884
|
+
| `--wui-dialog-desc-gap` | `24px` | Spacing below the body content |
|
|
885
|
+
| `--wui-dialog-desc-focus-padding` | `6px` | Focus-ring margin reserved on the **block axis** of the content area. `.desc` is a scroll container and, because one axis must be non-`visible`, a clip box as well, so a control flush against its top or bottom edge would have its ring clipped by the padding box; the margin covers the `5px` ring (`offset: 2` + `width: 3`). The horizontal ring room now comes from `--wui-dialog-inline-padding` (24px ≫ 5px), so this token only affects the block axis — its former inline component no longer applies. The paired negative top margin keeps the chrome height unchanged |
|
|
886
|
+
| `--wui-dialog-footer-gap` | `10px` / `12px` (horizontal) | Spacing between footer buttons |
|
|
887
|
+
| `--wui-dialog-footer-justify` | `flex-end` (default) / `center` (horizontal) | Footer `justify-content`; override to `flex-end` for right-aligned horizontal buttons |
|
|
888
|
+
| `--wui-dialog-scale-enter` | `1.1` | Enter scale start: the panel shrinks in from `1.1` to `1`, and exit reverses it |
|
|
889
|
+
|
|
890
|
+
> **Height semantics:** the token caps the content area, not the card. The `header` band and the footer stay sized by their content, and anything past the cap scrolls inside `.desc`; the card itself is bounded only by a literal `100vh` / `100dvh` backstop, which catches the case where the viewport is too short to fit chrome plus the cap. A host that wants to bound the **whole card** must still measure its own chrome (card padding + title row + gaps + footer) and subtract it from the token — the three dialogs in the Interweave frontend subtract 142, 142 and 158 respectively. What no longer needs that number is making the content area scroll: before, `.desc` had neither a cap nor `overflow`, so the only element that could scroll was a host-sized inner box, and a host that wanted overflow to scroll had to compute that height itself.
|
|
891
|
+
>
|
|
892
|
+
> **`headless` mode has no `.desc`**, so the token does not apply there. The `headless` default slot is wrapped in `.wui-dialog-content`, which is the scroll area, and the card stays content-sized until the same literal `100vh` / `100dvh` backstop kicks in; the built-in close button keeps its `--wui-dialog-close-top` / `--wui-dialog-close-right` offset while that content scrolls.
|
|
855
893
|
|
|
856
894
|
#### `<web-ui-drawer>`
|
|
857
895
|
|
|
@@ -1302,6 +1340,30 @@ Each one overrides the value `size` derives, so all eight stay authoritative —
|
|
|
1302
1340
|
| `--wui-empty-description-font-size` | `14px` | Description font size |
|
|
1303
1341
|
| `--wui-empty-action-margin-top` | `20px` | Space above the action slot |
|
|
1304
1342
|
|
|
1343
|
+
#### `<web-ui-middle-ellipsis>`
|
|
1344
|
+
|
|
1345
|
+
Single-line middle truncation: keeps both ends and drops the middle — `very-long-file-…-abcdefghij.txt` — where `text-overflow: ellipsis` can only keep the head.
|
|
1346
|
+
|
|
1347
|
+
| Attribute | Type | Default | Description |
|
|
1348
|
+
| ----------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1349
|
+
| `text` | `string` | `''` | Full text. Read-only from the component's side. |
|
|
1350
|
+
| `marker` | `string` | `'…'` | Marker inserted between the two ends. An empty string truncates without any visible signal. |
|
|
1351
|
+
| `marker-position` | `number` | `50` | Where the marker sits, 0–100. `0` puts it at the line-end (head only), `100` at the line-start (tail only). Out-of-range clamps, `NaN` falls back to `50`. |
|
|
1352
|
+
|
|
1353
|
+
It needs what `text-overflow: ellipsis` needs: **one line with a definite inline size**. A block-level box, `flex: 1; min-width: 0` in a flex row, or a grid track all work. A host that sizes itself to its own content has no available space to truncate against, so the component settles on the width it measured and stops shrinking instead of collapsing to the marker.
|
|
1354
|
+
|
|
1355
|
+
`marker-position` is the knob for file names, where the head is path noise and the tail carries the extension. The rows below are illustrative, not reproducible — exact cut points depend on the font; they were taken at 16px `system-ui` in a 240px box:
|
|
1356
|
+
|
|
1357
|
+
| `marker-position` | In a 240px box |
|
|
1358
|
+
| ----------------- | --------------------------------- |
|
|
1359
|
+
| `0` | `very-long-file-name-abcdefghi…` |
|
|
1360
|
+
| `50` _(default)_ | `very-long-file-…-abcdefghij.txt` |
|
|
1361
|
+
| `80` | `very-l…file-name-abcdefghij.txt` |
|
|
1362
|
+
|
|
1363
|
+
Cut points always fall on **grapheme cluster** boundaries: surrogate pairs, combining marks and ZWJ emoji sequences are never split. While truncated, the full string is exposed as the `title` of the text element and removed again once everything fits. The host must be `direction: ltr`; under `direction: rtl` the composed string is reordered by the bidi algorithm.
|
|
1364
|
+
|
|
1365
|
+
**Known limitation — copy and assistive tech.** The element holds the string you can see, so selecting and copying yields the truncated text rather than the original, and screen readers and in-page find match the truncated string too. CSS `text-overflow` has neither problem because it never touches the text node; this is the structural price of computing the split in JavaScript. `title` carries the original for hover. See `docs/research/web-ui-middle-ellipsis-261007.md` for the alternatives that keep the full string in the DOM and what they cost.
|
|
1366
|
+
|
|
1305
1367
|
#### `<web-ui-icon>`
|
|
1306
1368
|
|
|
1307
1369
|
Icon renderer using Iconify data objects.
|
|
@@ -1375,13 +1437,24 @@ Responsive page layout with an optional full-width banner, a collapsible desktop
|
|
|
1375
1437
|
| `sidebar-collapsed` | `boolean` | `false` | Controlled desktop sidebar collapsed state |
|
|
1376
1438
|
| `sidebar-open` | `boolean` | `false` | Controlled mobile sidebar drawer open state |
|
|
1377
1439
|
| `header-glow` | `boolean` | `false` | Decorative Header background glow behind slot content |
|
|
1440
|
+
| `mobile` | `boolean` | `false` | Read-only, derived: whether the viewport is at or below the mobile breakpoint |
|
|
1378
1441
|
| `sidebar-width` | `string` | `'240px'` | Expanded desktop and mobile sidebar width |
|
|
1379
1442
|
| `collapsed-width` | `string` | `'72px'` | Collapsed desktop sidebar width |
|
|
1380
1443
|
| `sidebar-resizable` | `boolean` | `false` | Enable drag-to-resize on the desktop sidebar edge |
|
|
1381
1444
|
| `sidebar-min-width` | `string` | — | Resize lower bound (px); falls back to `collapsed-width` |
|
|
1382
1445
|
| `sidebar-max-width` | `string` | — | Resize upper bound (px); clamped to half the viewport, which always wins over the configured value |
|
|
1383
1446
|
|
|
1384
|
-
**Events:** `sidebar-collapsed-change` (`CustomEvent<{ collapsed: boolean }>`) requests a desktop collapse-state update. `sidebar-open-change` (`CustomEvent<{ open: boolean }>`) requests a mobile drawer open-state update. `sidebar-width-change` (`CustomEvent<{ width: string }>`) requests a sidebar width update after a resize drag ends. Consumers must write the requested value back to the corresponding controlled property.
|
|
1447
|
+
**Events:** `sidebar-collapsed-change` (`CustomEvent<{ collapsed: boolean }>`) requests a desktop collapse-state update. `sidebar-open-change` (`CustomEvent<{ open: boolean }>`) requests a mobile drawer open-state update. `sidebar-width-change` (`CustomEvent<{ width: string }>`) requests a sidebar width update after a resize drag ends. Consumers must write the requested value back to the corresponding controlled property. `mobile-change` (`CustomEvent<{ mobile: boolean }>`) is not a request: it reports the layout's own derived viewport state.
|
|
1448
|
+
|
|
1449
|
+
**Mobile viewport state:** `mobile` is derived, not a second input — the layout writes it, the property is read-only (assignments take no effect and throw for strict-mode callers), and setting the attribute is reverted in the same reaction, value included, with no event for either. It flips with the media query the layout renders by (`@media (width <= 640px)`) and the same condition the layout CSS uses to hide the desktop sidebar, so the tree, the layout's own CSS and what consumers read can never disagree; there is no resize debounce, the flip lands on the media-query change itself. `mobile-change` fires once per crossing, including the connect-time evaluation when the element mounts on a narrow viewport. Read `layout.mobile` for the current value and subscribe to `mobile-change` for updates; CSS can target `web-ui-layout[mobile]`.
|
|
1450
|
+
|
|
1451
|
+
**Consuming it in a framework.** The layout re-renders in a microtask, so a consumer that reacts on a scheduler macrotask still reads the old value while the new tree is already on screen:
|
|
1452
|
+
|
|
1453
|
+
- **Vue** — bind `@mobile-change` in the template and hold the value in a `ref`. Vue attaches template listeners before the element is inserted (so they receive the connect-time event too) and flushes in a microtask, which lands ahead of the layout's own update.
|
|
1454
|
+
- **React** — take the element through a ref, then read `layout.mobile` and subscribe inside `useLayoutEffect`, and write the value back with `flushSync`. Reading gives you the mount-time value that a later-attached listener misses, and `flushSync` keeps a runtime crossing ahead of the layout's re-render: React flushes updates from DOM listeners on a macrotask, which otherwise leaves the drawer rendered with a stale consumer value for a frame (measured at 2 of 221 frames in this repository's React demo before the fix).
|
|
1455
|
+
- **CSS** — style off `web-ui-layout[mobile]`, either directly or through a variant. A Tailwind v4 app can write the mobile side of the boundary once instead of per utility: `@custom-variant mobile (&:where(web-ui-layout[mobile], web-ui-layout[mobile] *));`. Two limits: the attribute only reaches nodes inside the layout subtree, so content an overlay portals to `document.body` still needs a media query; and do not use a custom variant for the min-width side — Tailwind emits `@custom-variant` rules after the theme breakpoints, so a `desktop:` variant on the same property silently overrides `md:`/`lg:` at every width above them.
|
|
1456
|
+
|
|
1457
|
+
**Desktop-only collapse:** `sidebar-collapsed` only narrows the desktop sidebar; the layout applies no collapsed geometry to the mobile drawer. Both branches host the same `sidebar` slot content, so a consumer whose sidebar content changes density with that property must gate it on `mobile` (for example `collapsed && !layout.mobile`, or CSS under `web-ui-layout[mobile]`) — otherwise a collapsed desktop preference renders inside the drawer. See issue #195.
|
|
1385
1458
|
|
|
1386
1459
|
**Sidebar resize:** With `sidebar-resizable`, a resize handle appears on the desktop sidebar's right edge (hidden while collapsed); hovering or dragging shows a 3px accent vertical line with a `col-resize` cursor.
|
|
1387
1460
|
|
|
@@ -1426,9 +1499,9 @@ Responsive page layout with an optional full-width banner, a collapsible desktop
|
|
|
1426
1499
|
}
|
|
1427
1500
|
```
|
|
1428
1501
|
|
|
1429
|
-
At `640px` and below
|
|
1502
|
+
At `640px` and below — the same condition `mobile` reports — the sidebar becomes a `web-ui-drawer` with its built-in glass body, scrollable content, and drag zone. The layout maps `sidebar-width` to `--wui-drawer-width` and `--wui-layout-sidebar-radius` to `--wui-drawer-radius`. The mobile toggle appears in the header row as a glass button. Its left inset defaults to `8px`; align it with your own header padding via `--wui-layout-mobile-toggle-inset`.
|
|
1430
1503
|
|
|
1431
|
-
`header-glow` adds a pointer-transparent decorative glow behind header-slot content and the mobile toggle. It is a Header background rather than a foreground layer, so slotted content remains above it. The glow takes its colour from `--wui-color-page`, so it follows light and dark appearance on its own; override that property on `web-ui-layout`, on the theme, or anywhere above it to change it. Two pseudo-elements build it. `::before` paints the colour as a `linear-gradient` inside the header box, fading to fully transparent, and `::after` adds a real `backdrop-filter: blur(4px)` layer whose `mask` fades that blur toward zero alpha at the bottom edge. Keeping the two apart means blur strength and colour strength do not pull on each other. Both expand with a negative `margin` rather than a `transform
|
|
1504
|
+
`header-glow` adds a pointer-transparent decorative glow behind header-slot content and the mobile toggle. It is a Header background rather than a foreground layer, so slotted content remains above it. The glow takes its colour from `--wui-color-page`, so it follows light and dark appearance on its own; override that property on `web-ui-layout`, on the theme, or anywhere above it to change it. Two pseudo-elements build it. `::before` paints the colour as a `linear-gradient` inside the header box, fading to fully transparent, and `::after` adds a real `backdrop-filter: blur(4px)` layer whose `mask` fades that blur toward zero alpha at the bottom edge. Keeping the two apart means blur strength and colour strength do not pull on each other. Both expand with a negative `margin` rather than a `transform`, and the blur layer stops at the header's top edge — it only extends downward and to the sides. Horizontal clipping is owned by `.layout-content`, so the glow never produces a horizontal scrollbar. Layout layers are ordered as Header (`10`) < Auxiliary (`20`) < Banner (`30`) < Tabbar (`40`) < Sidebar (`50`).
|
|
1432
1505
|
|
|
1433
1506
|
The sidebar card surface is `--wui-color-surface-sidebar`. In light mode it keeps its translucent neutral layer; in dark mode it sits one notch above `--wui-color-page`, level with `--wui-color-surface`. It is a separate token because dialog, drawer and toast share `--wui-color-surface-overlay` and may use a different surface.
|
|
1434
1507
|
|
|
@@ -1531,18 +1604,18 @@ The host uses `display: contents` and does not paint any background: the library
|
|
|
1531
1604
|
|
|
1532
1605
|
**Typography tokens:** font sizes are named by the role the text plays, not by scale position — `caption` for dense chrome labels, `readout` for numeric readouts, the unsuffixed base for body copy, and `title` for bounded card headings. Weights are named by weight, and line heights by how much vertical air the text needs. These are appearance-independent: they stay the same under both light and dark.
|
|
1533
1606
|
|
|
1534
|
-
| Property | Default | Description
|
|
1535
|
-
| ---------------------------- | ------- |
|
|
1536
|
-
| `--wui-font-size-caption` | `12px` | Dense chrome labels (badge, toast timestamp, menu section head)
|
|
1537
|
-
| `--wui-font-size-readout` | `13px` | Numeric readouts (image preview count and zoom ratio)
|
|
1538
|
-
| `--wui-font-size` | `14px` | Base font size for body copy and controls
|
|
1539
|
-
| `--wui-font-size-title` | `18px` | Bounded card headings (dialog, drawer)
|
|
1540
|
-
| `--wui-font-weight-medium` | `500` | Medium weight for emphasized inline text
|
|
1541
|
-
| `--wui-font-weight-semibold` | `600` | Semibold weight for headings
|
|
1542
|
-
| `--wui-line-height-tight` | `1.2` | Tight leading for large headings
|
|
1543
|
-
| `--wui-line-height-snug` | `1.4` | Snug leading for mid-size headings
|
|
1544
|
-
| `--wui-line-height-normal` | `1.5` | Normal leading for body copy
|
|
1545
|
-
| `--wui-line-height-relaxed` | `1.6` | Relaxed leading for long-form reading
|
|
1607
|
+
| Property | Default | Description |
|
|
1608
|
+
| ---------------------------- | ------- | --------------------------------------------------------------------------- |
|
|
1609
|
+
| `--wui-font-size-caption` | `12px` | Dense chrome labels (badge, toast timestamp, menu section head) |
|
|
1610
|
+
| `--wui-font-size-readout` | `13px` | Numeric readouts (image preview count and zoom ratio) |
|
|
1611
|
+
| `--wui-font-size` | `14px` | Base font size for body copy and controls, including their placeholder text |
|
|
1612
|
+
| `--wui-font-size-title` | `18px` | Bounded card headings (dialog, drawer) |
|
|
1613
|
+
| `--wui-font-weight-medium` | `500` | Medium weight for emphasized inline text |
|
|
1614
|
+
| `--wui-font-weight-semibold` | `600` | Semibold weight for headings |
|
|
1615
|
+
| `--wui-line-height-tight` | `1.2` | Tight leading for large headings |
|
|
1616
|
+
| `--wui-line-height-snug` | `1.4` | Snug leading for mid-size headings |
|
|
1617
|
+
| `--wui-line-height-normal` | `1.5` | Normal leading for body copy |
|
|
1618
|
+
| `--wui-line-height-relaxed` | `1.6` | Relaxed leading for long-form reading |
|
|
1546
1619
|
|
|
1547
1620
|
`line-height: 1` and `line-height: 0` inside components are intentionally not tokenized: the former vertically centers a single-line control label, the latter collapses a wrapper box onto its content. Neither is typographic leading, and tokenizing them would invite callers to retune them as if they were.
|
|
1548
1621
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modal-surface.js","names":[],"sources":["../../src/assets/modal-surface.css?inline"],"sourcesContent":["/*\n * 模态表面(dialog / drawer / image-preview 的 <dialog> 面板)的焦点环策略,集中一处。\n *\n * 1. `<dialog>` 壳不是可聚焦控件:面板里没有更优候选时,`showModal()` 会把焦点交给\n * dialog 本身,UA 会为它画一枚默认 ring。它是模态表面,内部控件各自保留 focus ring。\n *\n * 2. 面板的滚动区(`.desc` / `.wui-dialog-content` / `.wui-drawer-content`)同样不是\n * 可聚焦控件。Chromium 会把「可滚动的滚动容器」变成键盘可聚焦项\n * (keyboard-focusable scrollers),于是 dialog 的 focusing steps 会选中面板滚动区,\n * UA 为它画一枚默认蓝环(`outline: auto` + `-webkit-focus-ring-color`)。这一枚环\n * 与库的统一 ring(`outline: var(--wui-focus-ring-width) solid var(--wui-color-focus-ring)`)\n * 形态/颜色都不一致,是「dialog / drawer 打开后卡片上多出一圈蓝框」的来源:滚动区若是\n * 铺满卡片的(body 模式)且卡片自带 `overflow: hidden` + 圆角,这枚直角的 UA 环会被\n * 卡片裁成圆角矩形,看起来正贴着卡片外缘。\n *\n * 逐组件各抄一份正是「新增一个基于 `<dialog>` 的组件就漏掉抑制规则」的成因;新组件把本\n * 文件加进 `static styles` 即可。文档级 scrim(context-menu)不在这条链上——它自己注入\n * 样式表,按同样策略在 `scrim.css` 里就地收口。\n */\ndialog:focus,\ndialog:focus-visible,\ndialog :where(.desc, .wui-dialog-content, .wui-drawer-content):focus-visible {\n outline: none;\n}\n"],"mappings":""}
|
|
@@ -176,6 +176,21 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
176
176
|
* 会跨会话残留,而这些都不是本组件能重置的。
|
|
177
177
|
*/
|
|
178
178
|
private _openScrim;
|
|
179
|
+
/**
|
|
180
|
+
* scrim 的挂载点:**与面板在没有 scrim 时会落到的容器同源** —— 最近主题的 theme-owned
|
|
181
|
+
* overlay root。scrim 就是那个容器的替身(面板必须挂进 scrim,见 `updated()` 的顺序注释),
|
|
182
|
+
* 所以它得站在容器该站的位置上。
|
|
183
|
+
*
|
|
184
|
+
* 为什么不能在整棵 DOM 里随便挑位置:`--wui-color-*` 由 `<web-ui-theme>` 写在自己的 `:host`
|
|
185
|
+
* 上,只沿 DOM 树向下继承。scrim 若挂到 `document.body`,而 `<web-ui-theme>` 是 body 的**子节点**,
|
|
186
|
+
* 这条继承链就断了 —— 面板与 scrim 双双落回 `var(--wui-color-*, <浅色字面量>)` 的 fallback,
|
|
187
|
+
* 深色外观下菜单是一块浅色玻璃(app 侧的 `dark:` 变体也够不到它)。挂进 theme overlay root
|
|
188
|
+
* 之后,面板与 scrim 跟 dropdown / select 一样吃同一份 token。
|
|
189
|
+
*
|
|
190
|
+
* 无主题时保持历史行为挂在 `document.body`:那条路径本来就没有 token 作用域可继承,改用
|
|
191
|
+
* fallback overlay root 只会改变既有测试与消费者的观察面,不换来任何东西。
|
|
192
|
+
*/
|
|
193
|
+
private _scrimParent;
|
|
179
194
|
/**
|
|
180
195
|
* 关掉并摘除 scrim。幂等:关闭分支与 `disconnectedCallback` 都会调。
|
|
181
196
|
*
|