@greypan/web-ui 9.1.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 +38 -28
- package/README.md +38 -28
- package/dist/components/dialog/index.d.ts +29 -4
- package/dist/components/dialog/index.js +26 -13
- 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/package.json +1 -1
package/README.CN.md
CHANGED
|
@@ -799,48 +799,58 @@ web-ui-radio-group {
|
|
|
799
799
|
|
|
800
800
|
模态对话框,使用原生 `<dialog>` 的 `showModal()`。
|
|
801
801
|
|
|
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`
|
|
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` 模式下必填 |
|
|
810
813
|
|
|
811
814
|
**事件:** `open-change` (`CustomEvent<{ open: boolean }>`)。启用 `controlled` 后,Escape 与遮罩点击仅请求 `open=false`,Consumer 写入 `open=false` 前对话框保持打开。程序化 API(`showModal()`/`close()`/直接赋值 `open`)不受影响,始终直通且不派发事件。原生 dialog 关闭(如表单 `method="dialog"`)会被恢复为受控打开状态并重新发出同一请求。
|
|
812
815
|
|
|
813
|
-
**插槽:** `
|
|
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`,或自绘的可关联标题元素。
|
|
814
823
|
|
|
815
824
|
**方法:** `showModal()`, `close()`
|
|
816
825
|
|
|
817
826
|
使用原生 `<dialog>`,`@cancel` 阻止默认关闭行为。除非存在 `no-escape-close`,否则 Escape 调用 `close()`;除非存在 `no-backdrop-close`,否则点击遮罩关闭。启用 `controlled` 后,两者都只派发关闭请求而不自关闭。
|
|
818
827
|
|
|
819
|
-
启用 `closable`
|
|
828
|
+
启用 `closable` 后,默认模式与 `headless` 模式都会渲染内置关闭按钮,且与 Escape、遮罩点击走同一条关闭路径(`controlled` 对它同样生效)。两种模式的位置已统一:按钮都绝对定位到卡片右上角,距卡片两沿的偏移由 `--wui-dialog-close-top` / `--wui-dialog-close-right` 控制(默认都是 `20px`;drawer 关闭按钮仍为 `16px`)。只有 `headless` 会把整卡换掉;默认模式下 `header` 带会为按钮留出避让,长内容不会钻到按钮下面。图标与 drawer 关闭按钮同为 `ooui:close`。不启用 `closable` 时什么都不渲染——既没有按钮,也没有多余的包裹层。
|
|
820
829
|
|
|
821
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 保留到退场过渡结束。
|
|
822
831
|
|
|
823
832
|
**CSS 自定义属性:**
|
|
824
833
|
|
|
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-
|
|
835
|
-
| `--wui-dialog-
|
|
836
|
-
| `--wui-dialog-desc-
|
|
837
|
-
| `--wui-dialog-
|
|
838
|
-
| `--wui-dialog-footer-
|
|
839
|
-
| `--wui-dialog-
|
|
840
|
-
|
|
841
|
-
|
|
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`,唯一能滚的是宿主自己撑出来的内层盒子,宿主想让它滚就必须自己算这个高度。
|
|
842
852
|
>
|
|
843
|
-
> **`
|
|
853
|
+
> **`headless` 模式没有 `.desc`**,所以该 token 在这一档不生效。`headless` 下的 `default` 插槽被包进 `.wui-dialog-content`,滚动发生在这一层;卡片在内容装得下时仍贴合内容,装不下时由同一条字面量 `100vh` / `100dvh` 兜底。内置关闭按钮在内容滚动时保持 `--wui-dialog-close-top` / `--wui-dialog-close-right` 给出的位置不变。
|
|
844
854
|
|
|
845
855
|
#### `<web-ui-drawer>`
|
|
846
856
|
|
package/README.md
CHANGED
|
@@ -838,48 +838,58 @@ Set it on the group host. The token controls the long edge in both directions, s
|
|
|
838
838
|
|
|
839
839
|
Modal dialog using native `<dialog>` with `showModal()`.
|
|
840
840
|
|
|
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`
|
|
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 |
|
|
849
852
|
|
|
850
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.
|
|
851
854
|
|
|
852
|
-
**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.
|
|
853
862
|
|
|
854
863
|
**Methods:** `showModal()`, `close()`
|
|
855
864
|
|
|
856
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.
|
|
857
866
|
|
|
858
|
-
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.
|
|
859
868
|
|
|
860
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.
|
|
861
870
|
|
|
862
871
|
**CSS Custom Properties:**
|
|
863
872
|
|
|
864
|
-
| Property | Default | Description
|
|
865
|
-
| --------------------------------- | -------------------------------------------- |
|
|
866
|
-
| `--wui-dialog-width` | `360px` | Dialog width
|
|
867
|
-
| `--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
|
|
868
|
-
| `--wui-dialog-close-top` | `20px` | `closable` button offset from the card top
|
|
869
|
-
| `--wui-dialog-close-right` | `20px` | `closable` button offset from the card right edge
|
|
870
|
-
| `--wui-dialog-overlay-bg` | `var(--wui-color-backdrop)` | Backdrop background
|
|
871
|
-
| `--wui-dialog-bg` | `var(--wui-color-surface-overlay)` | Glass card background, falls back to `rgb(248 248 248 / 0.92)`
|
|
872
|
-
| `--wui-dialog-padding` | `20px 24px 24px` |
|
|
873
|
-
| `--wui-dialog-
|
|
874
|
-
| `--wui-dialog-
|
|
875
|
-
| `--wui-dialog-desc-
|
|
876
|
-
| `--wui-dialog-
|
|
877
|
-
| `--wui-dialog-footer-
|
|
878
|
-
| `--wui-dialog-
|
|
879
|
-
|
|
880
|
-
|
|
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.
|
|
881
891
|
>
|
|
882
|
-
> **`
|
|
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.
|
|
883
893
|
|
|
884
894
|
#### `<web-ui-drawer>`
|
|
885
895
|
|
|
@@ -12,7 +12,27 @@ export declare class WebUiDialog extends LitElement {
|
|
|
12
12
|
* 程序化 API(showModal/close/直接赋值 open)不受此模式影响,始终直通。
|
|
13
13
|
*/
|
|
14
14
|
controlled: boolean;
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* `header` 槽为空时的标题文案。槽优先:一旦有元素分配到 `header`,槽内容整条接管,
|
|
17
|
+
* 属性被忽略。默认空串且槽也为空时不渲染 chrome 带。`headless` 模式不渲染 chrome 带,
|
|
18
|
+
* `heading` 一并失效。
|
|
19
|
+
*/
|
|
20
|
+
heading: string;
|
|
21
|
+
/**
|
|
22
|
+
* Headless 模式:只保留 overlay 基础设施(backdrop、动画、scroll lock、`<dialog>` 语义),
|
|
23
|
+
* 移除内置 UI(chrome 带、正文区、footer),默认槽的内容渲染进卡片内的滚动层
|
|
24
|
+
* `.wui-dialog-content`,视觉交给 Consumer。形态与 `<web-ui-drawer>` 的 `headless` 一致。
|
|
25
|
+
*
|
|
26
|
+
* 该模式没有可自动关联的内置标题,内部原生 dialog 的 accessible name 必须由 Consumer
|
|
27
|
+
* 用 `dialog-label`(或自绘标题 + 自己的 `aria-labelledby`)提供。
|
|
28
|
+
*/
|
|
29
|
+
headless: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* 内部原生 dialog 的 accessible name。`headless` 模式必须由 Consumer 提供,
|
|
32
|
+
* 因为该模式不会渲染可自动关联的内置 chrome 带。
|
|
33
|
+
*/
|
|
34
|
+
dialogLabel: string;
|
|
35
|
+
private _hasHeader;
|
|
16
36
|
private readonly _userOpenChange;
|
|
17
37
|
private readonly _overlay;
|
|
18
38
|
/** 当前开启会话的句柄;未开启时为 null。查询与惰性同步走它。 */
|
|
@@ -35,11 +55,16 @@ export declare class WebUiDialog extends LitElement {
|
|
|
35
55
|
private _onTransitionEnd;
|
|
36
56
|
private _onNativeClose;
|
|
37
57
|
private _syncScrollLock;
|
|
38
|
-
private _onBodySlotChange;
|
|
39
58
|
/**
|
|
40
|
-
*
|
|
59
|
+
* 只在「有/无」翻转时写 state:slotchange 在每次分配变化时都会触发,逐次赋值会让
|
|
60
|
+
* Lit 无谓重渲染。`heading` 与槽共同决定 chrome 带的存在,见 `_showHeader`。
|
|
61
|
+
*/
|
|
62
|
+
private _onHeaderSlotChange;
|
|
63
|
+
private get _showHeader();
|
|
64
|
+
/**
|
|
65
|
+
* 默认模式与 `headless` 模式共用同一枚关闭按钮:都绝对定位到卡片右上角,偏移由
|
|
41
66
|
* `--wui-dialog-close-top` / `--wui-dialog-close-right` 控制(见 style.css)。
|
|
42
|
-
*
|
|
67
|
+
* 它在两种模式下都是卡片的直接子元素,定位参照是卡片本身(.wui-dialog-body)。
|
|
43
68
|
*/
|
|
44
69
|
private _renderCloseButton;
|
|
45
70
|
render(): import('lit').TemplateResult<1>;
|
|
@@ -15,7 +15,7 @@ import { customElement as m, property as h, state as g } from "lit/decorators.js
|
|
|
15
15
|
//#region src/components/dialog/index.ts
|
|
16
16
|
var _ = class extends u {
|
|
17
17
|
constructor(...e) {
|
|
18
|
-
super(...e), this.open = !1, this.closable = !1, this.noScrollLock = !1, this.noBackdropClose = !1, this.noEscapeClose = !1, this.controlled = !1, this.
|
|
18
|
+
super(...e), this.open = !1, this.closable = !1, this.noScrollLock = !1, this.noBackdropClose = !1, this.noEscapeClose = !1, this.controlled = !1, this.heading = "", this.headless = !1, this.dialogLabel = "", this._hasHeader = !1, this._userOpenChange = new o(), this._overlay = i().make({
|
|
19
19
|
requestClose: () => this._closeFromUser(),
|
|
20
20
|
isConnected: () => this.isConnected
|
|
21
21
|
}), this._handle = null, this._scrollLock = a().make(), this._presence = c().make({
|
|
@@ -96,8 +96,13 @@ var _ = class extends u {
|
|
|
96
96
|
_syncScrollLock(e = this.open) {
|
|
97
97
|
this._scrollLock.sync(e && !this.noScrollLock);
|
|
98
98
|
}
|
|
99
|
-
|
|
100
|
-
e.target instanceof HTMLSlotElement
|
|
99
|
+
_onHeaderSlotChange(e) {
|
|
100
|
+
if (!(e.target instanceof HTMLSlotElement)) return;
|
|
101
|
+
let t = e.target.assignedElements().length > 0;
|
|
102
|
+
t !== this._hasHeader && (this._hasHeader = t);
|
|
103
|
+
}
|
|
104
|
+
get _showHeader() {
|
|
105
|
+
return this._hasHeader || this.heading !== "";
|
|
101
106
|
}
|
|
102
107
|
_renderCloseButton() {
|
|
103
108
|
return d`
|
|
@@ -114,29 +119,31 @@ var _ = class extends u {
|
|
|
114
119
|
`;
|
|
115
120
|
}
|
|
116
121
|
render() {
|
|
122
|
+
let e = this._showHeader, t = this.dialogLabel.trim(), n = !t && !this.headless && e ? "wui-dialog-heading" : f;
|
|
117
123
|
return d`
|
|
118
124
|
<dialog
|
|
125
|
+
aria-label=${t || f}
|
|
126
|
+
aria-labelledby=${n}
|
|
119
127
|
@cancel=${this.handleCancel}
|
|
120
128
|
@close=${this._onNativeClose}
|
|
121
129
|
@click=${this.handleBackdropClick}
|
|
122
130
|
@transitionend=${this._onTransitionEnd}
|
|
123
131
|
>
|
|
124
132
|
<div class="wui-dialog-body wui-glass">
|
|
125
|
-
${this.
|
|
133
|
+
${this.headless ? d`
|
|
126
134
|
<div class="wui-dialog-content">
|
|
127
|
-
<slot
|
|
135
|
+
<slot></slot>
|
|
128
136
|
</div>
|
|
129
137
|
${this.closable ? this._renderCloseButton() : f}
|
|
130
138
|
` : d`
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
</div>
|
|
137
|
-
` : d`<div class="title"><slot name="title"></slot></div>`}
|
|
139
|
+
<div class="header" id="wui-dialog-heading" ?hidden=${!e}>
|
|
140
|
+
<slot name="header" @slotchange=${this._onHeaderSlotChange}>
|
|
141
|
+
${this.heading ? d`<span class="wui-dialog-heading">${this.heading}</span>` : f}
|
|
142
|
+
</slot>
|
|
143
|
+
</div>
|
|
138
144
|
<div class="desc"><slot></slot></div>
|
|
139
145
|
<div class="wui-dialog-footer"><slot name="footer"></slot></div>
|
|
146
|
+
${this.closable ? this._renderCloseButton() : f}
|
|
140
147
|
`}
|
|
141
148
|
</div>
|
|
142
149
|
</dialog>
|
|
@@ -164,7 +171,13 @@ e([h({
|
|
|
164
171
|
})], _.prototype, "noEscapeClose", void 0), e([h({
|
|
165
172
|
type: Boolean,
|
|
166
173
|
reflect: !0
|
|
167
|
-
})], _.prototype, "controlled", void 0), e([
|
|
174
|
+
})], _.prototype, "controlled", void 0), e([h({ type: String })], _.prototype, "heading", void 0), e([h({
|
|
175
|
+
type: Boolean,
|
|
176
|
+
reflect: !0
|
|
177
|
+
})], _.prototype, "headless", void 0), e([h({
|
|
178
|
+
type: String,
|
|
179
|
+
attribute: "dialog-label"
|
|
180
|
+
})], _.prototype, "dialogLabel", void 0), e([g()], _.prototype, "_hasHeader", void 0), _ = e([m("web-ui-dialog")], _);
|
|
168
181
|
//#endregion
|
|
169
182
|
export { _ as WebUiDialog };
|
|
170
183
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/dialog/index.ts"],"sourcesContent":["import { html, LitElement, nothing, type PropertyValues, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport '@/components/icon'\nimport '@/components/button'\nimport glass from '@/assets/glass.css?inline'\nimport modalSurface from '@/assets/modal-surface.css?inline'\nimport { oouiClose } from '@/icons'\nimport { UserChangeController } from '@/shared/events/user-change'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { defineNativeDialogPresence } from '@/shared/overlay/native-dialog-presence'\nimport { defineOpenOverlay, type OpenOverlayHandle } from '@/shared/overlay/open-overlay'\nimport { defineScrollLockLease } from '@/shared/scroll-lock/scroll-lock'\n\nimport style from './style.css?inline'\n\n@customElement('web-ui-dialog')\nexport class WebUiDialog extends LitElement {\n static override styles = [unsafeCSS(glass), unsafeCSS(modalSurface), unsafeCSS(style)]\n\n @property({ type: Boolean, reflect: true }) open = false\n @property({ type: Boolean, reflect: true }) closable = false\n @property({ type: Boolean, reflect: true, attribute: 'no-scroll-lock' }) noScrollLock = false\n @property({ type: Boolean, reflect: true, attribute: 'no-backdrop-close' }) noBackdropClose = false\n @property({ type: Boolean, reflect: true, attribute: 'no-escape-close' }) noEscapeClose = false\n\n /**\n * Controlled 模式下,Escape 和遮罩点击只派发 `open-change` 请求,\n * 不会自行修改 `open`。Consumer 回写 `open` 后才执行关闭动画。\n * 程序化 API(showModal/close/直接赋值 open)不受此模式影响,始终直通。\n */\n @property({ type: Boolean, reflect: true }) controlled = false\n\n @state() private _hasBody = false\n private readonly _userOpenChange = new UserChangeController()\n\n /*\n * 开启态浮层(issue #120 Block 1)。dialog 的 Escape 原本只走原生 cancel 事件:仲裁者\n * 会在 keydown 上 preventDefault 压掉原生关闭请求并 stopPropagation,因此这里必须\n * 自己表达关闭语义(controlled 只派发请求)。\n */\n private readonly _overlay = defineOpenOverlay().make({\n requestClose: () => this._closeFromUser(),\n isConnected: () => this.isConnected\n })\n /** 当前开启会话的句柄;未开启时为 null。查询与惰性同步走它。 */\n private _handle: OpenOverlayHandle | null = null\n private readonly _scrollLock = defineScrollLockLease().make()\n private readonly _presence = defineNativeDialogPresence().make({\n getDialog: () => this.dialog,\n isConnected: () => this.isConnected,\n isOpen: () => this.open\n })\n\n private get dialog() {\n return this.shadowRoot?.querySelector('dialog') ?? null\n }\n\n protected override updated(props: PropertyValues) {\n super.updated(props)\n if (!this.isConnected) return\n\n if (props.has('open')) {\n if (this._userOpenChange.consume()) this.emitOpenChange()\n this._presence.sync(this.open)\n if (this.open) {\n // presence.sync 内的 showModal() 把 dialog 提升进 top layer。双击手势本身\n // 留下的活选区会被浏览器拿去和提升后的新布局重新解析,结果选区落到刚挂载的\n // dialog 正文上。脆弱的是「手势来源的活选区」,不是 dblclick 的判定窗口:把\n // 打开推迟 250ms(早过双击判定)仍然复现。选区由浏览器在 showModal() 调用\n // 内部生成:脚本没有任何 Selection API 调用,在 dblclick 上 preventDefault\n // 也拦不住。\n //\n // 实测(Chromium)showModal() 不会动打开前已经存在的选区,所以这一次清理\n // 同时也覆盖「先选中页面文本、再打开」的情况——modal 打开后被选中的内容挡在\n // 遮罩后面本来也已不可用。这里只清文档选区,不动 user-select,浮层内正文\n // 仍可正常拖选。\n window.getSelection()?.removeAllRanges()\n }\n // 原生 dialog 登记为开启态浮层:挂在它上面的 portal 面板成为本层后代,\n // Escape 仲裁据此判出最内层(issue #120 Block 1)。\n const dialog = this.dialog\n if (dialog) {\n if (this.open) {\n // 同一面板重新 claim = 新的一次开启:旧会话(含其子层)整体作废。\n this._handle?.release()\n this._handle = this._overlay.claim(dialog, { ancestryFrom: this })\n } else {\n this._handle?.release()\n this._handle = null\n }\n }\n }\n if (props.has('open') || props.has('noScrollLock')) this._syncScrollLock()\n this._syncOverlayInert()\n }\n\n override connectedCallback() {\n super.connectedCallback()\n // 重挂载对账:断连时 presence、滚动锁与登记都已被撤销,而 `open` 未变化时\n // `updated()` 不会补跑任何 sync 分支。首次连接时 shadow 尚未渲染、`this.dialog`\n // 为 null,三种情况都直接跳过;打开态的首次进入仍由 updated() 的\n // `props.has('open')` 分支处理。\n this._presence.reconcile()\n this._syncScrollLock()\n this._reclaimIfOpen()\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._presence.dispose()\n this._scrollLock.release()\n // 断连即撤销登记:层不留在全局注册表里,重连后由 _reclaimIfOpen 显式重新声明。\n this._handle?.release()\n this._handle = null\n }\n\n /*\n * 「重挂载恢复」的调用方一半:模块刻意不观察 DOM 连接状态,所以断连撤销登记之后,\n * 重连时仍有开启态就必须由组件重新声明。\n */\n private _reclaimIfOpen() {\n if (!this.open || this._handle) return\n const dialog = this.dialog\n if (!dialog) return\n this._handle = this._overlay.claim(dialog, { ancestryFrom: this })\n this._syncOverlayInert()\n }\n\n /*\n * `no-escape-close` 时 dialog 仍是候选(Escape 被吞掉、原生 cancel 被压掉),只是\n * 不走关闭入口。与旧的「跳过候选」语义不同:旧语义放任事件落到下层浮层,把外层\n * 一起关掉。该属性可在开启期间改写,所以每次渲染后读它,而不是在 claim 时固定。\n */\n private _syncOverlayInert() {\n this._handle?.setInert(this.noEscapeClose)\n }\n\n showModal() {\n if (this.open) return\n this.open = true\n }\n\n close() {\n this.open = false\n }\n\n /*\n * 仲裁者在 capture 阶段 preventDefault 了 Escape 的 keydown,UA 因此不再派发原生\n * cancel,但本 handler 仍有两个仲裁者不负责的职责,**不可删**:\n * ① 任何到达 dialog 的 cancel 都要 preventDefault,把 top layer 保留到视觉退场结束\n * (否则原生关闭会跳过退出动画);\n * ② 忽略子控件(例如 file input)冒泡上来的 cancel。\n * 实测(overlay-open-owner-260918,2026-09-18)摘掉 `@cancel` 后 dialog.browser.spec.ts\n * 的「no-escape-close 存在时 Escape/cancel 不关闭对话框」与 remount-reconcile 的\n * 「打开态被移出文档再接回」两例立刻转红,断言正是 `dispatchEvent(cancel) === false`。\n */\n private handleCancel(e: Event) {\n if (e.target !== e.currentTarget) return\n\n e.preventDefault()\n if (this.noEscapeClose) return\n this._closeFromUser()\n }\n\n private handleBackdropClick(e: MouseEvent) {\n if (e.target !== (e.currentTarget as HTMLDialogElement)) return\n if (this.noBackdropClose) return\n this._closeFromUser()\n }\n\n /** 用户发起的关闭入口(Escape、遮罩点击、关闭按钮)共用一条路径。 */\n private readonly _closeFromUser = () => {\n if (this.controlled) {\n this.emitOpenChange(false)\n return\n }\n this._userOpenChange.mark()\n this.close()\n }\n\n // controlled 下派发关闭请求:detail 固定 false(用户只能请求关闭,打开永远由 Consumer 写)。\n private emitOpenChange(open = this.open) {\n dispatchOpenChangeEvent(this, open)\n }\n\n private _onTransitionEnd = (event: TransitionEvent) => {\n this._presence.handleTransitionEnd(event)\n }\n\n private _onNativeClose = () => {\n // 关闭态下的 close 事件只可能是我们自己 finishClosing 排队的异步事件(或冗余的\n // 外部关闭):交给 presence 消费 self-close 标志,避免标志泄漏到下一次真实关闭。\n if (!this.open) {\n this._presence.handleNativeClose()\n return\n }\n\n // 我们自己 dialog.close() 排队的 close 事件是异步任务:正常时序在本关闭会话内\n // 到达,快速「关闭→重开」时它在重新打开之后才到达(过期事件)。两种情况都已\n // 完成清理,由 presence 消费并返回 true,不能当成外部关闭把刚重开的 dialog 关掉。\n if (this._presence.handleNativeClose()) return\n\n // controlled 下原生关闭(如表单 method=\"dialog\")视为未经 Consumer 批准的状态丢失:\n // 恢复受控状态并派发关闭请求,由 Consumer 决定是否关闭。\n if (this.controlled) {\n this._presence.sync(true)\n this.emitOpenChange(false)\n return\n }\n\n this._userOpenChange.mark()\n this.open = false\n }\n\n private _syncScrollLock(isOpen = this.open) {\n this._scrollLock.sync(isOpen && !this.noScrollLock)\n }\n\n private _onBodySlotChange(e: Event) {\n if (!(e.target instanceof HTMLSlotElement)) return\n this._hasBody = e.target.assignedElements().length > 0\n }\n\n /**\n * 两种内容模式共用同一枚关闭按钮:都绝对定位到卡片右上角,偏移由\n * `--wui-dialog-close-top` / `--wui-dialog-close-right` 控制(见 style.css)。\n * 标题模式只是把它放在 `.title-row` 的 DOM 位置,定位参照仍是卡片本身。\n */\n private _renderCloseButton() {\n return html`\n <web-ui-button\n class=\"wui-dialog-close\"\n @click=${this._closeFromUser}\n aria-label=\"关闭\"\n variant=\"secondary\"\n icon\n size=\"26\"\n >\n <web-ui-icon size=\"14\" .icon=${oouiClose}></web-ui-icon>\n </web-ui-button>\n `\n }\n\n override render() {\n return html`\n <dialog\n @cancel=${this.handleCancel}\n @close=${this._onNativeClose}\n @click=${this.handleBackdropClick}\n @transitionend=${this._onTransitionEnd}\n >\n <div class=\"wui-dialog-body wui-glass\">\n ${\n this._hasBody\n ? html`\n <div class=\"wui-dialog-content\">\n <slot name=\"body\" @slotchange=${this._onBodySlotChange}></slot>\n </div>\n ${this.closable ? this._renderCloseButton() : nothing}\n `\n : html`\n <slot name=\"body\" @slotchange=${this._onBodySlotChange} hidden></slot>\n ${\n this.closable\n ? html`\n <div class=\"title-row\">\n <div class=\"title\"><slot name=\"title\"></slot></div>\n ${this._renderCloseButton()}\n </div>\n `\n : html`<div class=\"title\"><slot name=\"title\"></slot></div>`\n }\n <div class=\"desc\"><slot></slot></div>\n <div class=\"wui-dialog-footer\"><slot name=\"footer\"></slot></div>\n `\n }\n </div>\n </dialog>\n `\n }\n\n declare readonly $events: {\n 'open-change': CustomEvent<{ open: boolean }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-dialog': WebUiDialog\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/dialog/index.ts"],"sourcesContent":["import { html, LitElement, nothing, type PropertyValues, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport '@/components/icon'\nimport '@/components/button'\nimport glass from '@/assets/glass.css?inline'\nimport modalSurface from '@/assets/modal-surface.css?inline'\nimport { oouiClose } from '@/icons'\nimport { UserChangeController } from '@/shared/events/user-change'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { defineNativeDialogPresence } from '@/shared/overlay/native-dialog-presence'\nimport { defineOpenOverlay, type OpenOverlayHandle } from '@/shared/overlay/open-overlay'\nimport { defineScrollLockLease } from '@/shared/scroll-lock/scroll-lock'\n\nimport style from './style.css?inline'\n\n@customElement('web-ui-dialog')\nexport class WebUiDialog extends LitElement {\n static override styles = [unsafeCSS(glass), unsafeCSS(modalSurface), unsafeCSS(style)]\n\n @property({ type: Boolean, reflect: true }) open = false\n @property({ type: Boolean, reflect: true }) closable = false\n @property({ type: Boolean, reflect: true, attribute: 'no-scroll-lock' }) noScrollLock = false\n @property({ type: Boolean, reflect: true, attribute: 'no-backdrop-close' }) noBackdropClose = false\n @property({ type: Boolean, reflect: true, attribute: 'no-escape-close' }) noEscapeClose = false\n\n /**\n * Controlled 模式下,Escape 和遮罩点击只派发 `open-change` 请求,\n * 不会自行修改 `open`。Consumer 回写 `open` 后才执行关闭动画。\n * 程序化 API(showModal/close/直接赋值 open)不受此模式影响,始终直通。\n */\n @property({ type: Boolean, reflect: true }) controlled = false\n\n /**\n * `header` 槽为空时的标题文案。槽优先:一旦有元素分配到 `header`,槽内容整条接管,\n * 属性被忽略。默认空串且槽也为空时不渲染 chrome 带。`headless` 模式不渲染 chrome 带,\n * `heading` 一并失效。\n */\n @property({ type: String }) heading = ''\n\n /**\n * Headless 模式:只保留 overlay 基础设施(backdrop、动画、scroll lock、`<dialog>` 语义),\n * 移除内置 UI(chrome 带、正文区、footer),默认槽的内容渲染进卡片内的滚动层\n * `.wui-dialog-content`,视觉交给 Consumer。形态与 `<web-ui-drawer>` 的 `headless` 一致。\n *\n * 该模式没有可自动关联的内置标题,内部原生 dialog 的 accessible name 必须由 Consumer\n * 用 `dialog-label`(或自绘标题 + 自己的 `aria-labelledby`)提供。\n */\n @property({ type: Boolean, reflect: true }) headless = false\n\n /**\n * 内部原生 dialog 的 accessible name。`headless` 模式必须由 Consumer 提供,\n * 因为该模式不会渲染可自动关联的内置 chrome 带。\n */\n @property({ type: String, attribute: 'dialog-label' }) dialogLabel = ''\n\n @state() private _hasHeader = false\n private readonly _userOpenChange = new UserChangeController()\n\n /*\n * 开启态浮层(issue #120 Block 1)。dialog 的 Escape 原本只走原生 cancel 事件:仲裁者\n * 会在 keydown 上 preventDefault 压掉原生关闭请求并 stopPropagation,因此这里必须\n * 自己表达关闭语义(controlled 只派发请求)。\n */\n private readonly _overlay = defineOpenOverlay().make({\n requestClose: () => this._closeFromUser(),\n isConnected: () => this.isConnected\n })\n /** 当前开启会话的句柄;未开启时为 null。查询与惰性同步走它。 */\n private _handle: OpenOverlayHandle | null = null\n private readonly _scrollLock = defineScrollLockLease().make()\n private readonly _presence = defineNativeDialogPresence().make({\n getDialog: () => this.dialog,\n isConnected: () => this.isConnected,\n isOpen: () => this.open\n })\n\n private get dialog() {\n return this.shadowRoot?.querySelector('dialog') ?? null\n }\n\n protected override updated(props: PropertyValues) {\n super.updated(props)\n if (!this.isConnected) return\n\n if (props.has('open')) {\n if (this._userOpenChange.consume()) this.emitOpenChange()\n this._presence.sync(this.open)\n if (this.open) {\n // presence.sync 内的 showModal() 把 dialog 提升进 top layer。双击手势本身\n // 留下的活选区会被浏览器拿去和提升后的新布局重新解析,结果选区落到刚挂载的\n // dialog 正文上。脆弱的是「手势来源的活选区」,不是 dblclick 的判定窗口:把\n // 打开推迟 250ms(早过双击判定)仍然复现。选区由浏览器在 showModal() 调用\n // 内部生成:脚本没有任何 Selection API 调用,在 dblclick 上 preventDefault\n // 也拦不住。\n //\n // 实测(Chromium)showModal() 不会动打开前已经存在的选区,所以这一次清理\n // 同时也覆盖「先选中页面文本、再打开」的情况——modal 打开后被选中的内容挡在\n // 遮罩后面本来也已不可用。这里只清文档选区,不动 user-select,浮层内正文\n // 仍可正常拖选。\n window.getSelection()?.removeAllRanges()\n }\n // 原生 dialog 登记为开启态浮层:挂在它上面的 portal 面板成为本层后代,\n // Escape 仲裁据此判出最内层(issue #120 Block 1)。\n const dialog = this.dialog\n if (dialog) {\n if (this.open) {\n // 同一面板重新 claim = 新的一次开启:旧会话(含其子层)整体作废。\n this._handle?.release()\n this._handle = this._overlay.claim(dialog, { ancestryFrom: this })\n } else {\n this._handle?.release()\n this._handle = null\n }\n }\n }\n if (props.has('open') || props.has('noScrollLock')) this._syncScrollLock()\n this._syncOverlayInert()\n }\n\n override connectedCallback() {\n super.connectedCallback()\n // 重挂载对账:断连时 presence、滚动锁与登记都已被撤销,而 `open` 未变化时\n // `updated()` 不会补跑任何 sync 分支。首次连接时 shadow 尚未渲染、`this.dialog`\n // 为 null,三种情况都直接跳过;打开态的首次进入仍由 updated() 的\n // `props.has('open')` 分支处理。\n this._presence.reconcile()\n this._syncScrollLock()\n this._reclaimIfOpen()\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._presence.dispose()\n this._scrollLock.release()\n // 断连即撤销登记:层不留在全局注册表里,重连后由 _reclaimIfOpen 显式重新声明。\n this._handle?.release()\n this._handle = null\n }\n\n /*\n * 「重挂载恢复」的调用方一半:模块刻意不观察 DOM 连接状态,所以断连撤销登记之后,\n * 重连时仍有开启态就必须由组件重新声明。\n */\n private _reclaimIfOpen() {\n if (!this.open || this._handle) return\n const dialog = this.dialog\n if (!dialog) return\n this._handle = this._overlay.claim(dialog, { ancestryFrom: this })\n this._syncOverlayInert()\n }\n\n /*\n * `no-escape-close` 时 dialog 仍是候选(Escape 被吞掉、原生 cancel 被压掉),只是\n * 不走关闭入口。与旧的「跳过候选」语义不同:旧语义放任事件落到下层浮层,把外层\n * 一起关掉。该属性可在开启期间改写,所以每次渲染后读它,而不是在 claim 时固定。\n */\n private _syncOverlayInert() {\n this._handle?.setInert(this.noEscapeClose)\n }\n\n showModal() {\n if (this.open) return\n this.open = true\n }\n\n close() {\n this.open = false\n }\n\n /*\n * 仲裁者在 capture 阶段 preventDefault 了 Escape 的 keydown,UA 因此不再派发原生\n * cancel,但本 handler 仍有两个仲裁者不负责的职责,**不可删**:\n * ① 任何到达 dialog 的 cancel 都要 preventDefault,把 top layer 保留到视觉退场结束\n * (否则原生关闭会跳过退出动画);\n * ② 忽略子控件(例如 file input)冒泡上来的 cancel。\n * 实测(overlay-open-owner-260918,2026-09-18)摘掉 `@cancel` 后 dialog.browser.spec.ts\n * 的「no-escape-close 存在时 Escape/cancel 不关闭对话框」与 remount-reconcile 的\n * 「打开态被移出文档再接回」两例立刻转红,断言正是 `dispatchEvent(cancel) === false`。\n */\n private handleCancel(e: Event) {\n if (e.target !== e.currentTarget) return\n\n e.preventDefault()\n if (this.noEscapeClose) return\n this._closeFromUser()\n }\n\n private handleBackdropClick(e: MouseEvent) {\n if (e.target !== (e.currentTarget as HTMLDialogElement)) return\n if (this.noBackdropClose) return\n this._closeFromUser()\n }\n\n /** 用户发起的关闭入口(Escape、遮罩点击、关闭按钮)共用一条路径。 */\n private readonly _closeFromUser = () => {\n if (this.controlled) {\n this.emitOpenChange(false)\n return\n }\n this._userOpenChange.mark()\n this.close()\n }\n\n // controlled 下派发关闭请求:detail 固定 false(用户只能请求关闭,打开永远由 Consumer 写)。\n private emitOpenChange(open = this.open) {\n dispatchOpenChangeEvent(this, open)\n }\n\n private _onTransitionEnd = (event: TransitionEvent) => {\n this._presence.handleTransitionEnd(event)\n }\n\n private _onNativeClose = () => {\n // 关闭态下的 close 事件只可能是我们自己 finishClosing 排队的异步事件(或冗余的\n // 外部关闭):交给 presence 消费 self-close 标志,避免标志泄漏到下一次真实关闭。\n if (!this.open) {\n this._presence.handleNativeClose()\n return\n }\n\n // 我们自己 dialog.close() 排队的 close 事件是异步任务:正常时序在本关闭会话内\n // 到达,快速「关闭→重开」时它在重新打开之后才到达(过期事件)。两种情况都已\n // 完成清理,由 presence 消费并返回 true,不能当成外部关闭把刚重开的 dialog 关掉。\n if (this._presence.handleNativeClose()) return\n\n // controlled 下原生关闭(如表单 method=\"dialog\")视为未经 Consumer 批准的状态丢失:\n // 恢复受控状态并派发关闭请求,由 Consumer 决定是否关闭。\n if (this.controlled) {\n this._presence.sync(true)\n this.emitOpenChange(false)\n return\n }\n\n this._userOpenChange.mark()\n this.open = false\n }\n\n private _syncScrollLock(isOpen = this.open) {\n this._scrollLock.sync(isOpen && !this.noScrollLock)\n }\n\n /**\n * 只在「有/无」翻转时写 state:slotchange 在每次分配变化时都会触发,逐次赋值会让\n * Lit 无谓重渲染。`heading` 与槽共同决定 chrome 带的存在,见 `_showHeader`。\n */\n private _onHeaderSlotChange(e: Event) {\n if (!(e.target instanceof HTMLSlotElement)) return\n const has = e.target.assignedElements().length > 0\n if (has !== this._hasHeader) this._hasHeader = has\n }\n\n private get _showHeader(): boolean {\n return this._hasHeader || this.heading !== ''\n }\n\n /**\n * 默认模式与 `headless` 模式共用同一枚关闭按钮:都绝对定位到卡片右上角,偏移由\n * `--wui-dialog-close-top` / `--wui-dialog-close-right` 控制(见 style.css)。\n * 它在两种模式下都是卡片的直接子元素,定位参照是卡片本身(.wui-dialog-body)。\n */\n private _renderCloseButton() {\n return html`\n <web-ui-button\n class=\"wui-dialog-close\"\n @click=${this._closeFromUser}\n aria-label=\"关闭\"\n variant=\"secondary\"\n icon\n size=\"26\"\n >\n <web-ui-icon size=\"14\" .icon=${oouiClose}></web-ui-icon>\n </web-ui-button>\n `\n }\n\n override render() {\n const showHeader = this._showHeader\n /*\n * accessible name:显式 `dialog-label` 优先;否则(非 headless 且带可见时)把原生 dialog\n * 的 `aria-labelledby` 指向 chrome 带——与 drawer 同形。headless 模式没有可自动关联的\n * 内置标题,名字留给 Consumer。\n */\n const dialogLabel = this.dialogLabel.trim()\n const dialogLabelledBy = !dialogLabel && !this.headless && showHeader ? 'wui-dialog-heading' : nothing\n\n return html`\n <dialog\n aria-label=${dialogLabel || nothing}\n aria-labelledby=${dialogLabelledBy}\n @cancel=${this.handleCancel}\n @close=${this._onNativeClose}\n @click=${this.handleBackdropClick}\n @transitionend=${this._onTransitionEnd}\n >\n <div class=\"wui-dialog-body wui-glass\">\n ${\n this.headless\n ? html`\n <div class=\"wui-dialog-content\">\n <slot></slot>\n </div>\n ${this.closable ? this._renderCloseButton() : nothing}\n `\n : html`\n <div class=\"header\" id=\"wui-dialog-heading\" ?hidden=${!showHeader}>\n <slot name=\"header\" @slotchange=${this._onHeaderSlotChange}>\n ${this.heading ? html`<span class=\"wui-dialog-heading\">${this.heading}</span>` : nothing}\n </slot>\n </div>\n <div class=\"desc\"><slot></slot></div>\n <div class=\"wui-dialog-footer\"><slot name=\"footer\"></slot></div>\n ${this.closable ? this._renderCloseButton() : nothing}\n `\n }\n </div>\n </dialog>\n `\n }\n\n declare readonly $events: {\n 'open-change': CustomEvent<{ open: boolean }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-dialog': WebUiDialog\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/components/dialog/style.css?inline
|
|
2
|
-
var e = "*,:before,:after{box-sizing:border-box}:host{display:contents}dialog{transform:scale(var(--wui-dialog-scale-enter,1.1));box-sizing:border-box;width:min(90vw, var(--wui-dialog-width,360px));max-height:100vh;color:var(--wui-color-text,#1b1b1b);transition:transform var(--wui-duration-dialog-enter,.32s) var(--wui-ease-dialog,cubic-bezier(.2, 0, 0, 1));background:0 0;border:none;padding:0;font-family:inherit;position:fixed;overflow:visible}dialog::backdrop{opacity:0;background:var(--wui-dialog-overlay-bg,var(--wui-color-backdrop,#0000001f));transition:opacity var(--wui-duration-dialog-enter,.32s) var(--wui-ease-dialog,cubic-bezier(.2, 0, 0, 1))}@supports (height:100dvh){dialog{max-height:100dvh}}dialog:has(.desc){flex-direction:column;display:flex}dialog[open].is-visible{transform:scale(1)}dialog[open].is-closing{pointer-events:none;transition-duration:var(--wui-duration-dialog-exit,.26s)}dialog.is-closing::backdrop{transition-duration:var(--wui-duration-dialog-exit,.26s)}dialog[open].is-visible::backdrop{opacity:1}@media (prefers-reduced-motion:reduce){dialog,dialog[open].is-visible{transition-duration:var(--wui-duration-feedback,.1s);transform:none}}.wui-dialog-body{--wui-internal-dialog-close-top:var(--wui-dialog-close-top,20px);--wui-internal-dialog-close-right:var(--wui-dialog-close-right,20px);--wui-internal-glass-shadow:var(--wui-shadow-overlay,2px 16px 40px #0006);--wui-glass-corner-radius:var(--wui-radius-overlay,28px);padding:var(--wui-dialog-padding,var(--wui-space-5,20px) var(--wui-space-6,24px) var(--wui-space-6,24px));border-radius:var(--wui-radius-overlay,28px);opacity:0;background-color:var(--wui-dialog-bg,var(--wui-color-surface-overlay,#f8f8f8eb));-webkit-backdrop-filter:blur(0) saturate(160%) brightness(var(--wui-glass-brightness,1.06));backdrop-filter:blur(0) saturate(160%) brightness(var(--wui-glass-brightness,1.06));transition:opacity var(--wui-duration-dialog-enter,.32s) var(--wui-ease-dialog,cubic-bezier(.2, 0, 0, 1)), -webkit-backdrop-filter var(--wui-duration-dialog-enter,.32s) var(--wui-ease-dialog,cubic-bezier(.2, 0, 0, 1)), backdrop-filter var(--wui-duration-dialog-enter,.32s) var(--wui-ease-dialog,cubic-bezier(.2, 0, 0, 1))}.wui-dialog-body:has(.desc){grid-template-rows:auto minmax(0,1fr) auto;min-height:0;display:grid}.wui-dialog-body:has(>.wui-dialog-content){grid-template-rows:minmax(0,1fr);max-height:100vh;padding:0;display:grid;overflow:hidden}@supports (height:100dvh){.wui-dialog-body:has(>.wui-dialog-content){max-height:100dvh}}.wui-dialog-content{min-height:0;padding:var(--wui-dialog-padding,var(--wui-space-5,20px) var(--wui-space-6,24px) var(--wui-space-6,24px));overflow-y:auto}dialog.is-closing .wui-dialog-body{transition-duration:var(--wui-duration-dialog-exit,.26s)}:host([closable]) .wui-dialog-body{position:relative}dialog[open].is-visible .wui-dialog-body{opacity:1;-webkit-backdrop-filter:blur(4px) saturate(160%) brightness(var(--wui-glass-brightness,1.06));backdrop-filter:blur(4px) saturate(160%) brightness(var(--wui-glass-brightness,1.06))}.wui-dialog-close{z-index:3;top:var(--wui-internal-dialog-close-top);right:var(--wui-internal-dialog-close-right);position:absolute}.
|
|
2
|
+
var e = "*,:before,:after{box-sizing:border-box}:host{display:contents}dialog{transform:scale(var(--wui-dialog-scale-enter,1.1));box-sizing:border-box;width:min(90vw, var(--wui-dialog-width,360px));max-height:100vh;color:var(--wui-color-text,#1b1b1b);transition:transform var(--wui-duration-dialog-enter,.32s) var(--wui-ease-dialog,cubic-bezier(.2, 0, 0, 1));background:0 0;border:none;padding:0;font-family:inherit;position:fixed;overflow:visible}dialog::backdrop{opacity:0;background:var(--wui-dialog-overlay-bg,var(--wui-color-backdrop,#0000001f));transition:opacity var(--wui-duration-dialog-enter,.32s) var(--wui-ease-dialog,cubic-bezier(.2, 0, 0, 1))}@supports (height:100dvh){dialog{max-height:100dvh}}dialog:has(.desc){flex-direction:column;display:flex}dialog[open].is-visible{transform:scale(1)}dialog[open].is-closing{pointer-events:none;transition-duration:var(--wui-duration-dialog-exit,.26s)}dialog.is-closing::backdrop{transition-duration:var(--wui-duration-dialog-exit,.26s)}dialog[open].is-visible::backdrop{opacity:1}@media (prefers-reduced-motion:reduce){dialog,dialog[open].is-visible{transition-duration:var(--wui-duration-feedback,.1s);transform:none}}.wui-dialog-body{--wui-internal-dialog-close-top:var(--wui-dialog-close-top,20px);--wui-internal-dialog-close-right:var(--wui-dialog-close-right,20px);--wui-internal-glass-shadow:var(--wui-shadow-overlay,2px 16px 40px #0006);--wui-glass-corner-radius:var(--wui-radius-overlay,28px);padding:var(--wui-dialog-padding,var(--wui-space-5,20px) var(--wui-space-6,24px) var(--wui-space-6,24px));border-radius:var(--wui-radius-overlay,28px);opacity:0;background-color:var(--wui-dialog-bg,var(--wui-color-surface-overlay,#f8f8f8eb));-webkit-backdrop-filter:blur(0) saturate(160%) brightness(var(--wui-glass-brightness,1.06));backdrop-filter:blur(0) saturate(160%) brightness(var(--wui-glass-brightness,1.06));transition:opacity var(--wui-duration-dialog-enter,.32s) var(--wui-ease-dialog,cubic-bezier(.2, 0, 0, 1)), -webkit-backdrop-filter var(--wui-duration-dialog-enter,.32s) var(--wui-ease-dialog,cubic-bezier(.2, 0, 0, 1)), backdrop-filter var(--wui-duration-dialog-enter,.32s) var(--wui-ease-dialog,cubic-bezier(.2, 0, 0, 1));padding-inline:0}.wui-dialog-body:has(.desc){grid-template-rows:auto minmax(0,1fr) auto;min-height:0;display:grid}.wui-dialog-body:has(>.wui-dialog-content){grid-template-rows:minmax(0,1fr);max-height:100vh;padding:0;display:grid;overflow:hidden}@supports (height:100dvh){.wui-dialog-body:has(>.wui-dialog-content){max-height:100dvh}}.wui-dialog-content{min-height:0;padding:var(--wui-dialog-padding,var(--wui-space-5,20px) var(--wui-space-6,24px) var(--wui-space-6,24px));overflow-y:auto}dialog.is-closing .wui-dialog-body{transition-duration:var(--wui-duration-dialog-exit,.26s)}:host([closable]) .wui-dialog-body{position:relative}dialog[open].is-visible .wui-dialog-body{opacity:1;-webkit-backdrop-filter:blur(4px) saturate(160%) brightness(var(--wui-glass-brightness,1.06));backdrop-filter:blur(4px) saturate(160%) brightness(var(--wui-glass-brightness,1.06))}.wui-dialog-close{z-index:3;top:var(--wui-internal-dialog-close-top);right:var(--wui-internal-dialog-close-right);position:absolute}.header{gap:var(--wui-space-3,12px);margin:0 0 var(--wui-dialog-title-gap,var(--wui-space-4,16px));padding-inline:var(--wui-dialog-inline-padding,var(--wui-space-6,24px));font-size:var(--wui-font-size-title,18px);font-weight:var(--wui-font-weight-semibold,600);line-height:var(--wui-line-height-tight,1.2);color:var(--wui-color-text,#1b1b1b);justify-content:space-between;align-items:center;display:flex}.header[hidden]{display:none}@media (prefers-reduced-motion:reduce){dialog.is-closing .wui-dialog-body,dialog[open].is-visible .wui-dialog-body{transition-duration:var(--wui-duration-feedback,.1s)}}:host([closable]) .header{padding-inline-end:calc(var(--wui-internal-dialog-close-right) + 26px + var(--wui-space-3,12px))}.desc{min-height:0;max-height:calc(var(--wui-dialog-max-height,90vh) + var(--wui-dialog-desc-focus-padding,6px) * 2);margin-top:calc(-1 * var(--wui-dialog-desc-focus-padding,6px));margin-bottom:max(0px, calc(var(--wui-dialog-desc-gap,var(--wui-space-6,24px)) - var(--wui-dialog-desc-focus-padding,6px)));padding-block:var(--wui-dialog-desc-focus-padding,6px);padding-inline:var(--wui-dialog-inline-padding,var(--wui-space-6,24px));font-size:var(--wui-font-size,14px);line-height:var(--wui-line-height-normal,1.5);color:var(--wui-color-text,#1b1b1b);margin-inline:0;overflow:clip auto}@supports (height:100dvh){.desc{max-height:calc(var(--wui-dialog-max-height,min(90vh, 90dvh)) + var(--wui-dialog-desc-focus-padding,6px) * 2)}}.wui-dialog-footer{gap:var(--wui-dialog-footer-gap,10px);justify-content:var(--wui-dialog-footer-justify,flex-end);padding-inline:var(--wui-dialog-inline-padding,var(--wui-space-6,24px));flex-direction:column;display:flex}.wui-dialog-footer ::slotted(web-ui-button){width:100%;display:flex}:host([horizontal]) .wui-dialog-footer{gap:var(--wui-space-3,12px);justify-content:var(--wui-dialog-footer-justify,center);flex-direction:row}:host([horizontal]) .wui-dialog-footer ::slotted(web-ui-button:not([full])){width:auto}:host([horizontal]) .wui-dialog-footer ::slotted(web-ui-button[full]){flex:1;min-width:0}";
|
|
3
3
|
//#endregion
|
|
4
4
|
export { e as default };
|
|
5
5
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.js","names":[],"sources":["../../../src/components/dialog/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n display: contents;\n}\n\ndialog {\n /* 必须显式 position: fixed:UA 的 dialog:modal 默认就是 fixed,但作者样式若出现\n position: relative(本轮单层重构曾引入)会把它覆盖回文档流——top layer 里的\n modal dialog 变成按文档流定位:渲染在页面顶部(用户看到的“残影”)且跟随页面\n 滚动(“没有固定居中”)。显式写死 fixed 防御未来任何作者类再覆盖。 */\n position: fixed;\n\n /* 本元素不参与 opacity 过渡:opacity < 1 会让 dialog 成为 backdrop root,后代\n backdrop-filter 在该过渡期间被整体禁用。玻璃卡片(.wui-dialog-body)自身\n opacity + backdrop-filter 插值过渡(own opacity 不禁用自身 blur,见下)。 */\n\n /* 进场由大到小收缩(1.1 → 1),退场反向。取消 .is-visible 后回落到本条起点值,\n enter/exit 共用同一 token。方向与浮动面板的 --wui-scale-enter 相反,故单独成 token。 */\n transform: scale(var(--wui-dialog-scale-enter, 1.1));\n\n overflow: visible;\n\n box-sizing: border-box;\n width: min(90vw, var(--wui-dialog-width, 360px));\n\n /* 整卡兜底:防卡片高过视口。真正的内容区上限在 .desc(见下),这里只接住\n 「视口本身很矮、chrome + 内容区上限也放不下」的情况。字面量而非 token,\n 免得与 .desc 抢同一个数值——共用时外层必然先触发(它是整卡约束),\n .desc 的 max-height 就成了永远轮不到的死约束。dvh 增强见文件末尾。 */\n max-height: 100vh;\n padding: 0;\n border: none;\n\n font-family: inherit;\n color: var(--wui-color-text, #1b1b1b);\n\n background: transparent;\n\n transition: transform var(--wui-duration-dialog-enter, 320ms) var(--wui-ease-dialog, cubic-bezier(0.2, 0, 0, 1));\n\n &::backdrop {\n opacity: 0;\n background: var(--wui-dialog-overlay-bg, var(--wui-color-backdrop, rgb(0 0 0 / 0.12)));\n\n /* 与卡片共用同一条 dialog 缓动:用内置 ease 会让遮罩在卡片落位后才走完大半透明度。 */\n transition: opacity var(--wui-duration-dialog-enter, 320ms) var(--wui-ease-dialog, cubic-bezier(0.2, 0, 0, 1));\n }\n}\n\n/* 动态视口高度兜底:移动端工具栏收放时不让 dialog 超出可见区域。\n 放在 @supports 内而非与上一条并列,是因为 var() 的回退值要到计算值阶段才替换;\n 不支持 dvh 的引擎会在该阶段拿到非法值,使整条声明变成 invalid-at-computed-value-time\n 而丢失 max-height。并列两条普通声明则由解析期丢弃,行为不同。dialog 这条是字面量,\n 并列也不会丢,但统一放在 @supports 内,免得两条规则看起来像两种不同的兜底策略。\n\n `.desc` 的 dvh 增强不在这里:它必须排在文件后面那条 `.desc` 规则之后才生效,\n 同特异度下靠后者胜。放在这里会被后面那条的 `90vh` 回退静默覆盖。 */\n@supports (height: 100dvh) {\n dialog {\n max-height: 100dvh;\n }\n}\n\n/* ===== 高度约束下移到内容区(chrome 由内容撑开) =====\n\n 宿主过去必须复述一个会变的 chrome 高度:AddDialog / RestoreDialog 都写\n `height: min(calc(82vh - 108px), calc(var(--wui-dialog-max-height, 560px) - 108px))`\n (改动前的写法,见 base b6b7eb34;这两处宿主已按新语义转换),\n 而那 108 里的 footer 一项就是 --wui-control-size——触摸端\n `@media (pointer: coarse)` 把它从 36px 抬到 40px,chrome 随之长高,宿主的常数立刻腐坏。\n\n 现在 header / footer 完全由内容撑开,上限只落在 .desc。修掉的是「宿主必须知道 chrome\n 才能让内容区滚起来」:改动前 .desc 既没有上限也没有 overflow,内容超高时直接把卡片\n 顶出 dialog 盒子,唯一能滚的地方是宿主自己用 `min(calc(82vh - 108px), …)` 之类的\n 内层 height 撑出来的那一块。现在上限与滚动都归 .desc,宿主不必再为「让它滚」而算。\n\n 但「宿主不再需要知道 chrome 多高」并不成立,改成这样写会误导后人。--wui-dialog-max-height\n 的语义是**内容区**上限:宿主若要给整卡定上限,仍然得自己量出 chrome 再从 token 里减掉\n (AddDialog / RestoreDialog 减 142,SettingsDialog 减 106,三处不能互相照抄)。把整卡\n 上限原样写进 token,卡片会比预期高出一个 chrome。\n\n 实测(Chromium + WebKit,见 __tests__/dialog-content-height.browser.spec.ts):本组 CSS\n 只对 **title 模式**生效——body 模式(走 slot=\"body\"、没有 .desc)的**卡片尺寸**不变。\n 但位置不保证:`dialog` 的整卡兜底从 min(90vh, 90dvh) 收紧到字面量 100vh,modal dialog\n 按自身盒子在视口里居中、卡片从盒子顶部起画,盒子更高 → 卡片在视口内上移。内容接近视口\n 高度时位移可观测(reviewer 实测 52px / 102px / 25.16px),只有内容远矮于视口时位移为 0\n (两个 demo 正是落在这一档,巧合而非保证)。 */\n\n/* dialog 不是 flex 容器时 .desc 的 height:100% 拿不到确定高度(:host 是 display:contents,\n dialog 自身默认 block),宿主的内层 grid 因此撑不满、两栏不等高。 */\ndialog:has(.desc) {\n display: flex;\n flex-direction: column;\n}\n\n/* 模态表面的焦点环抑制(<dialog> 壳 + 面板滚动区)集中在 assets/modal-surface.css,见组件 index.ts 的 static styles。 */\n\ndialog[open].is-visible {\n transform: scale(1);\n}\n\ndialog[open].is-closing {\n pointer-events: none;\n transition-duration: var(--wui-duration-dialog-exit, 260ms);\n}\n\ndialog.is-closing::backdrop {\n transition-duration: var(--wui-duration-dialog-exit, 260ms);\n}\n\ndialog[open].is-visible::backdrop {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n dialog,\n dialog[open].is-visible {\n transform: none;\n transition-duration: var(--wui-duration-feedback, 100ms);\n }\n}\n\n/* ===== 玻璃卡片主体(样式由 glass.css 提供) ===== */\n\n/* 单层玻璃:卡片自身承担背景/阴影/backdrop-filter。blur(0px) ↔ blur(4px) 平滑插值\n (none↔blur 不可插值才是当年生硬跳变的根因),own opacity 过渡不禁用自身 blur,\n 因此卡片直接 opacity + backdrop-filter 同步过渡即可,无需双层。 */\n.wui-dialog-body {\n --wui-internal-dialog-close-top: var(--wui-dialog-close-top, 20px);\n --wui-internal-dialog-close-right: var(--wui-dialog-close-right, 20px);\n --wui-internal-glass-shadow: var(--wui-shadow-overlay, 2px 16px 40px rgb(0 0 0 / 0.4));\n --wui-glass-corner-radius: var(--wui-radius-overlay, 28px);\n padding: var(--wui-dialog-padding, var(--wui-space-5, 20px) var(--wui-space-6, 24px) var(--wui-space-6, 24px));\n border-radius: var(--wui-radius-overlay, 28px);\n\n opacity: 0;\n background-color: var(--wui-dialog-bg, var(--wui-color-surface-overlay, rgb(248 248 248 / 0.92)));\n backdrop-filter: blur(0) saturate(160%) brightness(var(--wui-glass-brightness, 1.06));\n\n /* transition 列表需手写 -webkit-backdrop-filter:lightningcss 不会在 transition\n 的 value 内追加 -webkit 项,而 iOS 16.4 上 blur 过渡依赖它(属性前缀会自动生成)。 */\n transition:\n opacity var(--wui-duration-dialog-enter, 320ms) var(--wui-ease-dialog, cubic-bezier(0.2, 0, 0, 1)),\n -webkit-backdrop-filter var(--wui-duration-dialog-enter, 320ms) var(--wui-ease-dialog, cubic-bezier(0.2, 0, 0, 1)),\n backdrop-filter var(--wui-duration-dialog-enter, 320ms) var(--wui-ease-dialog, cubic-bezier(0.2, 0, 0, 1));\n}\n\n/* 卡片改 grid 容器:title 与 footer 走 auto(内容撑开),.desc 走 minmax(0,1fr)。\n minmax 的 0 是 .desc 能收缩的前提;配合 .desc 的 min-height:0 与 overflow-y:auto,\n 内容超高时滚动发生在内容区内部,而不是把卡片顶出 dialog 盒子。\n\n 单列而非与基础 .wui-dialog-body 合并:`:has()` 的特异度高于类选择器,合并会让\n 「仅 title 模式变 grid」变成「所有模式都变 grid」,那会改掉 body 模式的几何\n (实测高内容 body 的卡片会从 3044 被夹到 896)。位置也受 stylelint\n no-descending-specificity 约束:须排在基础规则之后、dialog.is-closing 之前。 */\n.wui-dialog-body:has(.desc) {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr) auto;\n\n /* 这条 min-height: 0 是承重的(`.desc` 那条不是):卡片是 `dialog` 这个 column flex\n 容器的 flex 项,`min-height: auto` 会把它垫到内容高度,视口的 100dvh 兜底就再也\n 夹不住。删掉后 token=5000px 时卡片实测 3154、dialog 896。 */\n min-height: 0;\n}\n\n/* ===== body 模式:内容区是卡片内部的滚动层 =====\n\n body 模式没有 title / footer,卡片就是内容区,所以这里要同时守住两件事:\n 内容不高时卡片贴合内容(与基础规则一致),内容高过视口时把上限交给视口、把溢出交给\n 滚动。少了后半件,卡片会一路长到内容高度并溢出 dialog 盒子;dialog 是 overflow:\n visible(阴影与关闭按钮不能由同尺寸的原生 dialog 裁),溢出的那截落在视口之下,\n 既看不到也够不着。\n\n 上限取与整卡兜底相同的字面量,而不是 `--wui-dialog-max-height`:后者的公开语义是\n `.desc` 的高度(README 有记录),body 模式根本没有 `.desc`,套上去会凭空改掉\n 这档的既有尺寸语义。\n\n 滚动单独成层而不是让卡片自己滚:关闭按钮是卡片的绝对定位直接子元素,卡片一旦自己\n 滚动,按钮会跟着内容一起滚走。padding 也跟着挪进内层,让内容与滚动容器的裁剪边\n (padding 外沿)留出余量——贴边的控件焦点环才不会被裁成残条;内层没有滚动条槽的\n 引擎里,这圈余量与卡片直接持 padding 时逐像素相同。 */\n.wui-dialog-body:has(> .wui-dialog-content) {\n overflow: hidden;\n display: grid;\n grid-template-rows: minmax(0, 1fr);\n\n max-height: 100vh;\n padding: 0;\n}\n\n@supports (height: 100dvh) {\n .wui-dialog-body:has(> .wui-dialog-content) {\n max-height: 100dvh;\n }\n}\n\n.wui-dialog-content {\n overflow-y: auto;\n min-height: 0;\n padding: var(--wui-dialog-padding, var(--wui-space-5, 20px) var(--wui-space-6, 24px) var(--wui-space-6, 24px));\n}\n\ndialog.is-closing .wui-dialog-body {\n transition-duration: var(--wui-duration-dialog-exit, 260ms);\n}\n\n:host([closable]) .wui-dialog-body {\n /*\n * body 模式的关闭按钮浮在卡片右上角,需要一个定位参照,所以显式重述一次\n * position: relative。\n *\n * 实际提供这个上下文的是卡片自带的 .wui-glass(glass.css 的 .wui-glass 已有\n * position: relative),所以这条声明目前是冗余的,写在这里是为了不依赖\n * glass.css 的内部实现——若哪天 glass 改成非定位元素,这条仍然兜得住。\n * 它不是本轮为 closable 建立的包含块边界:body 上的 wui-glass class 与\n * static styles 里的 glass 都早于本轮存在。\n */\n position: relative;\n}\n\ndialog[open].is-visible .wui-dialog-body {\n opacity: 1;\n backdrop-filter: blur(4px) saturate(160%) brightness(var(--wui-glass-brightness, 1.06));\n}\n\n/* ===== 关闭按钮(绝对定位,两种内容模式同一套规则)=====\n\n 偏移由公开 token --wui-dialog-close-top / --wui-dialog-close-right 控制,默认都是字面量\n 20px。默认值不再经过 --wui-space-*:这两个 token 的参照系是卡片边缘,而不是相邻元素\n 之间的间距节奏,消费方调整主题间距时不应意外移动关闭按钮。dialog 本轮也不再与 drawer\n 的 16/16 对齐;--wui-drawer-close-* 保持不变。\n\n 两个公开 token 先在正文容器上解析成同一份内部变量,按钮定位和标题避让都读这份结果,\n 避免只改一个兜底字面量后按钮与标题各自按不同默认值计算。\n\n 标题模式此前是 .title-row 里的 flex 子项(flex: none),偏移由卡片 padding 决定——默认\n 上 20 / 右 24,二者不等,正是「设置」对话框里按钮离上沿近、离右沿远的来源。改成绝对\n 定位后两种模式同源,几何只由本条规则决定。\n\n 绝对定位的包含块是 .wui-dialog-body(:host([closable]) 给了它 position: relative),\n 不是 .title-row:后者是 static,按钮只是它名义上的孩子;top/right 因此量到卡片 padding\n box 的右上角。 */\n.wui-dialog-close {\n position: absolute;\n z-index: 3;\n top: var(--wui-internal-dialog-close-top);\n right: var(--wui-internal-dialog-close-right);\n}\n\n/*\n * 标题行只在 closable 时出现,因此 .title 自身的 margin-bottom 语义不变:\n * 它仍由 .title-row 的行高承担,行高 = 标题外高(含 16px 下边距)。\n * 关闭按钮已是绝对定位、不再是这一行的占位项(见上),所以这里的 flex/gap 只剩排版作用。\n */\n.title-row {\n display: flex;\n gap: var(--wui-space-3, 12px);\n align-items: flex-start;\n justify-content: space-between;\n}\n\n@media (prefers-reduced-motion: reduce) {\n dialog.is-closing .wui-dialog-body,\n dialog[open].is-visible .wui-dialog-body {\n transition-duration: var(--wui-duration-feedback, 100ms);\n }\n}\n\n.title {\n margin: 0 0 var(--wui-dialog-title-gap, var(--wui-space-4, 16px));\n padding: 0;\n\n font-size: var(--wui-font-size-title, 18px);\n font-weight: var(--wui-font-weight-semibold, 600);\n line-height: var(--wui-line-height-tight, 1.2);\n color: var(--wui-color-text, #1b1b1b);\n}\n\n/* 标题避让:关闭按钮绝对定位后不再占 .title-row 的位,长标题会一直排到按钮下面。\n 预留量取「按钮右偏移 + 按钮宽 + 呼吸间距」,而不是只减掉卡片 padding——这样即便消费者把\n --wui-dialog-padding 的 inline 分量调到 0,标题文字右缘也不会越过按钮左缘;跟随\n --wui-dialog-close-right 变化;这里复用正文容器上已解析的内部右侧偏移,与按钮定位同源。\n 26px 对应 index.ts 里按钮的 size=\"26\",改按钮尺寸时一并改这里。\n 只在 closable 时生效:没有按钮就不必留白。 */\n:host([closable]) .title {\n padding-inline-end: calc(var(--wui-internal-dialog-close-right) + 26px + var(--wui-space-3, 12px));\n}\n\n.desc {\n /* 横向显式 clip 而非留默认的 visible:一旦本轴成为滚动容器,另一轴的 visible 会被\n 计算成 auto,宿主内容横向溢出时就会多出一条不该出现的横向滚动条。\n Chromium / WebKit 实测:clip 与本规则下面的 auto 配对时计算值会降级成 hidden\n (仍可被脚本滚动)。Firefox ≥128 不做这个降级、保持 clip,但仓库的 browser-mode\n 项目只跑 chromium,这句未经本仓 CI 证实。两个引擎都不画横向滚动条,所以这里\n 只需要「不是 auto」;断言写成 not.toBe('auto') 而非 toBe('clip'),否则会在\n 降级的引擎上误报。 */\n overflow-x: clip;\n overflow-y: auto;\n\n /* 承重的是上面那条 `overflow-y: auto`:按 CSS Sizing 3,溢出轴不为 visible 的\n grid 项其自动最小尺寸就是 0,`.desc` 无需再显式归零(实测删掉本条测试仍全绿,\n 改成 `auto auto auto` 也一样)。这里保留是防御性的:哪天 `overflow-y` 被改回\n visible,显式归零仍在,minmax(0,1fr) 才不会被顶破。 */\n min-height: 0;\n\n /* 内容区上限 = token + ring 余量:token 语义是内容区高度,ring 不该从它里面扣。\n token 只挂在这里,不与 dialog 的 max-height 共用——共用时外层(整卡)必然先触发。 */\n max-height: calc(var(--wui-dialog-max-height, 90vh) + var(--wui-dialog-desc-focus-padding, 6px) * 2);\n\n /* ===== focus-ring 余量:四个轴一视同仁,成对负 margin 原样扣回 =====\n\n .desc 既是滚动容器又是裁剪盒(clip),贴在内容区任一沿的控件,其 ring\n (offset 2 + width 3 = 5px)会被 padding box 裁掉;留 6px 余 1px。少补任何一条轴,\n 那一侧的 ring 就会退化成 1px 残条——只补 `padding-block` 的那版,全宽控件的左右环\n 就是这样被裁掉的(改动前 `.desc` 没有 overflow,最近裁剪祖先是 NONE,所以这是本轮\n 新引入的裁剪,不是既有限制)。\n\n 四条 padding 各配一条等量负 margin,把这 12px×2 从外部间距里扣回去,border box\n 回到改动前的样子,content box 的宽/高也不变(负 margin 让盒子外扩,padding 原样\n 吃回去)。所以 title→内容、内容→footer、内容与卡片左右沿的视觉间距与改动前逐像素\n 相同,宿主内层 grid 的可用尺寸也不变——后者是横向余量最该守住的不变量,否则\n AddDialog 那种两栏 grid 的 drop zone 会换行(实测见\n __tests__/dialog-content-height.browser.spec.ts 的「focus-ring 余量覆盖四条轴」)。\n\n 取舍,是取舍不是疏漏:负 margin 让 `.desc` 的 border box 溢出滚动内容区,所以\n 底部那圈 ring 余量不随内容滚动——滚到最底时 footer 那一侧仍留得住 padding,而\n 内容末端到 `.desc` 下沿的 desc-gap 恒定,不跟着内容走。 */\n margin-top: calc(-1 * var(--wui-dialog-desc-focus-padding, 6px));\n\n /* margin-bottom 是「desc-gap 减去那对 padding-block」,两个都是公开 token:宿主把\n desc-gap 调到 focus-padding 以下就会算出负 margin(24/8 安全,4/2/0 会算出\n -2/-4/-6)。max() 兜住下界 0——那条路径下内容末端与 footer 会贴在一起,但盒模型\n 与 grid 行分配不进负值区间。\n\n 横向不需要同样的兜底:margin-inline 恒为 -focus-padding,没有第二个公开 token 参与\n 减法。它与卡片 inline padding(默认 24px)的关系是「focus-padding 不应超过该\n padding,否则 .desc 会横向溢出卡片」,由上面那段注释交代。 */\n margin-bottom: max(\n 0px,\n calc(var(--wui-dialog-desc-gap, var(--wui-space-6, 24px)) - var(--wui-dialog-desc-focus-padding, 6px))\n );\n margin-inline: calc(-1 * var(--wui-dialog-desc-focus-padding, 6px));\n padding-block: var(--wui-dialog-desc-focus-padding, 6px);\n padding-inline: var(--wui-dialog-desc-focus-padding, 6px);\n\n font-size: var(--wui-font-size, 14px);\n line-height: var(--wui-line-height-normal, 1.5);\n color: var(--wui-color-text, #1b1b1b);\n}\n\n/* `--wui-dialog-max-height` 是公开 token(README 有记录),语义是**内容区**高度。\n 默认值与整卡兜底同族(90vh / dvh),使未覆写的 dialog 视觉高度与改动前一致:\n 移动端 90vh 可能大于 90dvh(vh 取大视口高),少了 min() 内容区会探到工具栏下面。\n\n 必须紧跟在 `.desc` 之后而非文件开头的 @supports 块里:两者特异度相同(0,1,0),\n 靠后者胜。实测把本条挪到前面后,`.desc` 的计算值会退回 90vh。 */\n@supports (height: 100dvh) {\n .desc {\n max-height: calc(var(--wui-dialog-max-height, min(90vh, 90dvh)) + var(--wui-dialog-desc-focus-padding, 6px) * 2);\n }\n}\n\n.wui-dialog-footer {\n display: flex;\n flex-direction: column;\n gap: var(--wui-dialog-footer-gap, 10px);\n justify-content: var(--wui-dialog-footer-justify, flex-end);\n\n & ::slotted(web-ui-button) {\n display: flex;\n width: 100%;\n }\n}\n\n:host([horizontal]) .wui-dialog-footer {\n flex-direction: row;\n gap: var(--wui-space-3, 12px);\n justify-content: var(--wui-dialog-footer-justify, center);\n\n & ::slotted(web-ui-button:not([full])) {\n width: auto;\n }\n\n & ::slotted(web-ui-button[full]) {\n flex: 1;\n min-width: 0;\n }\n}\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"style.js","names":[],"sources":["../../../src/components/dialog/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n display: contents;\n}\n\ndialog {\n /* 必须显式 position: fixed:UA 的 dialog:modal 默认就是 fixed,但作者样式若出现\n position: relative(本轮单层重构曾引入)会把它覆盖回文档流——top layer 里的\n modal dialog 变成按文档流定位:渲染在页面顶部(用户看到的“残影”)且跟随页面\n 滚动(“没有固定居中”)。显式写死 fixed 防御未来任何作者类再覆盖。 */\n position: fixed;\n\n /* 本元素不参与 opacity 过渡:opacity < 1 会让 dialog 成为 backdrop root,后代\n backdrop-filter 在该过渡期间被整体禁用。玻璃卡片(.wui-dialog-body)自身\n opacity + backdrop-filter 插值过渡(own opacity 不禁用自身 blur,见下)。 */\n\n /* 进场由大到小收缩(1.1 → 1),退场反向。取消 .is-visible 后回落到本条起点值,\n enter/exit 共用同一 token。方向与浮动面板的 --wui-scale-enter 相反,故单独成 token。 */\n transform: scale(var(--wui-dialog-scale-enter, 1.1));\n\n overflow: visible;\n\n box-sizing: border-box;\n width: min(90vw, var(--wui-dialog-width, 360px));\n\n /* 整卡兜底:防卡片高过视口。真正的内容区上限在 .desc(见下),这里只接住\n 「视口本身很矮、chrome + 内容区上限也放不下」的情况。字面量而非 token,\n 免得与 .desc 抢同一个数值——共用时外层必然先触发(它是整卡约束),\n .desc 的 max-height 就成了永远轮不到的死约束。dvh 增强见文件末尾。 */\n max-height: 100vh;\n padding: 0;\n border: none;\n\n font-family: inherit;\n color: var(--wui-color-text, #1b1b1b);\n\n background: transparent;\n\n transition: transform var(--wui-duration-dialog-enter, 320ms) var(--wui-ease-dialog, cubic-bezier(0.2, 0, 0, 1));\n\n &::backdrop {\n opacity: 0;\n background: var(--wui-dialog-overlay-bg, var(--wui-color-backdrop, rgb(0 0 0 / 0.12)));\n\n /* 与卡片共用同一条 dialog 缓动:用内置 ease 会让遮罩在卡片落位后才走完大半透明度。 */\n transition: opacity var(--wui-duration-dialog-enter, 320ms) var(--wui-ease-dialog, cubic-bezier(0.2, 0, 0, 1));\n }\n}\n\n/* 动态视口高度兜底:移动端工具栏收放时不让 dialog 超出可见区域。\n 放在 @supports 内而非与上一条并列,是因为 var() 的回退值要到计算值阶段才替换;\n 不支持 dvh 的引擎会在该阶段拿到非法值,使整条声明变成 invalid-at-computed-value-time\n 而丢失 max-height。并列两条普通声明则由解析期丢弃,行为不同。dialog 这条是字面量,\n 并列也不会丢,但统一放在 @supports 内,免得两条规则看起来像两种不同的兜底策略。\n\n `.desc` 的 dvh 增强不在这里:它必须排在文件后面那条 `.desc` 规则之后才生效,\n 同特异度下靠后者胜。放在这里会被后面那条的 `90vh` 回退静默覆盖。 */\n@supports (height: 100dvh) {\n dialog {\n max-height: 100dvh;\n }\n}\n\n/* ===== 高度约束下移到内容区(chrome 由内容撑开) =====\n\n 宿主过去必须复述一个会变的 chrome 高度:AddDialog / RestoreDialog 都写\n `height: min(calc(82vh - 108px), calc(var(--wui-dialog-max-height, 560px) - 108px))`\n (改动前的写法,见 base b6b7eb34;这两处宿主已按新语义转换),\n 而那 108 里的 footer 一项就是 --wui-control-size——触摸端\n `@media (pointer: coarse)` 把它从 36px 抬到 40px,chrome 随之长高,宿主的常数立刻腐坏。\n\n 现在 header / footer 完全由内容撑开,上限只落在 .desc。修掉的是「宿主必须知道 chrome\n 才能让内容区滚起来」:改动前 .desc 既没有上限也没有 overflow,内容超高时直接把卡片\n 顶出 dialog 盒子,唯一能滚的地方是宿主自己用 `min(calc(82vh - 108px), …)` 之类的\n 内层 height 撑出来的那一块。现在上限与滚动都归 .desc,宿主不必再为「让它滚」而算。\n\n 但「宿主不再需要知道 chrome 多高」并不成立,改成这样写会误导后人。--wui-dialog-max-height\n 的语义是**内容区**上限:宿主若要给整卡定上限,仍然得自己量出 chrome 再从 token 里减掉\n (AddDialog / RestoreDialog 减 142,SettingsDialog 减 106,三处不能互相照抄)。把整卡\n 上限原样写进 token,卡片会比预期高出一个 chrome。\n\n 实测(Chromium + WebKit,见 __tests__/dialog-content-height.browser.spec.ts):本组 CSS\n 只对 **默认模式**生效——headless 模式(默认槽渲染进 .wui-dialog-content、没有 .desc)的**卡片尺寸**不变。\n 但位置不保证:`dialog` 的整卡兜底从 min(90vh, 90dvh) 收紧到字面量 100vh,modal dialog\n 按自身盒子在视口里居中、卡片从盒子顶部起画,盒子更高 → 卡片在视口内上移。内容接近视口\n 高度时位移可观测(reviewer 实测 52px / 102px / 25.16px),只有内容远矮于视口时位移为 0\n (两个 demo 正是落在这一档,巧合而非保证)。 */\n\n/* dialog 不是 flex 容器时 .desc 的 height:100% 拿不到确定高度(:host 是 display:contents,\n dialog 自身默认 block),宿主的内层 grid 因此撑不满、两栏不等高。模式判定按 DOM 形状\n 而不是 attribute,理由见下方 grid 规则上的注释。 */\ndialog:has(.desc) {\n display: flex;\n flex-direction: column;\n}\n\n/* 模态表面的焦点环抑制(<dialog> 壳 + 面板滚动区)集中在 assets/modal-surface.css,见组件 index.ts 的 static styles。 */\n\ndialog[open].is-visible {\n transform: scale(1);\n}\n\ndialog[open].is-closing {\n pointer-events: none;\n transition-duration: var(--wui-duration-dialog-exit, 260ms);\n}\n\ndialog.is-closing::backdrop {\n transition-duration: var(--wui-duration-dialog-exit, 260ms);\n}\n\ndialog[open].is-visible::backdrop {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n dialog,\n dialog[open].is-visible {\n transform: none;\n transition-duration: var(--wui-duration-feedback, 100ms);\n }\n}\n\n/* ===== 玻璃卡片主体(样式由 glass.css 提供) ===== */\n\n/* 单层玻璃:卡片自身承担背景/阴影/backdrop-filter。blur(0px) ↔ blur(4px) 平滑插值\n (none↔blur 不可插值才是当年生硬跳变的根因),own opacity 过渡不禁用自身 blur,\n 因此卡片直接 opacity + backdrop-filter 同步过渡即可,无需双层。 */\n.wui-dialog-body {\n --wui-internal-dialog-close-top: var(--wui-dialog-close-top, 20px);\n --wui-internal-dialog-close-right: var(--wui-dialog-close-right, 20px);\n --wui-internal-glass-shadow: var(--wui-shadow-overlay, 2px 16px 40px rgb(0 0 0 / 0.4));\n --wui-glass-corner-radius: var(--wui-radius-overlay, 28px);\n\n /* 卡片只吃 --wui-dialog-padding 的 block 分量;inline 分量让给各分区\n (.header / .desc / .wui-dialog-footer,由 --wui-dialog-inline-padding 控制),\n .desc 这个滚动容器的边因此与卡片内沿重合,滚动条贴边(详见 .desc)。 */\n padding: var(--wui-dialog-padding, var(--wui-space-5, 20px) var(--wui-space-6, 24px) var(--wui-space-6, 24px));\n padding-inline: 0;\n border-radius: var(--wui-radius-overlay, 28px);\n\n opacity: 0;\n background-color: var(--wui-dialog-bg, var(--wui-color-surface-overlay, rgb(248 248 248 / 0.92)));\n backdrop-filter: blur(0) saturate(160%) brightness(var(--wui-glass-brightness, 1.06));\n\n /* transition 列表需手写 -webkit-backdrop-filter:lightningcss 不会在 transition\n 的 value 内追加 -webkit 项,而 iOS 16.4 上 blur 过渡依赖它(属性前缀会自动生成)。 */\n transition:\n opacity var(--wui-duration-dialog-enter, 320ms) var(--wui-ease-dialog, cubic-bezier(0.2, 0, 0, 1)),\n -webkit-backdrop-filter var(--wui-duration-dialog-enter, 320ms) var(--wui-ease-dialog, cubic-bezier(0.2, 0, 0, 1)),\n backdrop-filter var(--wui-duration-dialog-enter, 320ms) var(--wui-ease-dialog, cubic-bezier(0.2, 0, 0, 1));\n}\n\n/* 卡片改 grid 容器:header 与 footer 走 auto(内容撑开),.desc 走 minmax(0,1fr)。\n minmax 的 0 是 .desc 能收缩的前提;配合 .desc 的 min-height:0 与 overflow-y:auto,\n 内容超高时滚动发生在内容区内部,而不是把卡片顶出 dialog 盒子。\n\n 单列而非与基础 .wui-dialog-body 合并:`:has(.desc)` 与类选择器叠加的特异度高于单个类\n 选择器,合并会让「仅默认模式变 grid」变成「所有模式都变 grid」,那会改掉 headless\n 模式的几何(实测高内容 headless 的卡片会从 3044 被夹到 896)。位置也受 stylelint\n no-descending-specificity 约束:须排在基础规则之后、dialog.is-closing 之前。\n\n 模式判定按 **DOM 形状**(`:has(.desc)`)而不是 attribute(`:host(:not([headless]))`):\n 外部直接 `setAttribute('headless', '')` 时 attribute 立刻翻转,Lit 要等下一次 update 才\n 换模板——那个窗口里 DOM 仍是默认形状。`:has(.desc)` 描述的就是当下的 DOM,与它自洽;\n `:host([headless])` 会按 attribute 提前把卡片按 headless 几何渲染,而 DOM 里还带着\n `.desc`。改成 drawer 那种 attribute 形式还要把承重规则上移(`dialog.is-closing\n .wui-dialog-body`;焦点环抑制那条已挪去 `assets/modal-surface.css`)来满足\n no-descending-specificity,收益不抵风险。 */\n.wui-dialog-body:has(.desc) {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr) auto;\n\n /* 这条 min-height: 0 是承重的(`.desc` 那条不是):卡片是 `dialog` 这个 column flex\n 容器的 flex 项,`min-height: auto` 会把它垫到内容高度,视口的 100dvh 兜底就再也\n 夹不住。删掉后 token=5000px 时卡片实测 3154、dialog 896。 */\n min-height: 0;\n}\n\n/* ===== headless 模式:内容区是卡片内部的滚动层 =====\n\n headless 模式没有 header / .desc / footer,卡片就是那个滚动层,所以这里要同时守住两件事:\n 内容不高时卡片贴合内容(与基础规则一致),内容高过视口时把上限交给视口、把溢出交给\n 滚动。少了后半件,卡片会一路长到内容高度并溢出 dialog 盒子;dialog 是 overflow:\n visible(阴影与关闭按钮不能由同尺寸的原生 dialog 裁),溢出的那截落在视口之下,\n 既看不到也够不着。\n\n 上限取与整卡兜底相同的字面量,而不是 `--wui-dialog-max-height`:后者的公开语义是\n `.desc` 的高度(README 有记录),headless 模式根本没有 `.desc`,套上去会凭空改掉\n 这档的既有尺寸语义。\n\n 滚动单独成层而不是让卡片自己滚:关闭按钮是卡片的绝对定位直接子元素,卡片一旦自己\n 滚动,按钮会跟着内容一起滚走。padding 也跟着挪进内层,让内容与滚动容器的裁剪边\n (padding 外沿)留出余量——贴边的控件焦点环才不会被裁成残条;内层没有滚动条槽的\n 引擎里,这圈余量与卡片直接持 padding 时逐像素相同。\n\n headless 的收窄也取结构(`:has(> .wui-dialog-content)`)而不是 `:host([headless])`,与上方\n grid 规则同一条理由:判定跟 DOM 形状走。attribute 与 DOM 在直接 setAttribute 的那个\n 窗口里会短暂分叉,结构选择器始终与 DOM 自洽。 */\n.wui-dialog-body:has(> .wui-dialog-content) {\n overflow: hidden;\n display: grid;\n grid-template-rows: minmax(0, 1fr);\n\n max-height: 100vh;\n padding: 0;\n}\n\n@supports (height: 100dvh) {\n .wui-dialog-body:has(> .wui-dialog-content) {\n max-height: 100dvh;\n }\n}\n\n.wui-dialog-content {\n overflow-y: auto;\n min-height: 0;\n padding: var(--wui-dialog-padding, var(--wui-space-5, 20px) var(--wui-space-6, 24px) var(--wui-space-6, 24px));\n}\n\ndialog.is-closing .wui-dialog-body {\n transition-duration: var(--wui-duration-dialog-exit, 260ms);\n}\n\n:host([closable]) .wui-dialog-body {\n /*\n * headless 模式的关闭按钮浮在卡片右上角,需要一个定位参照,所以显式重述一次\n * position: relative。\n *\n * 实际提供这个上下文的是卡片自带的 .wui-glass(glass.css 的 .wui-glass 已有\n * position: relative),所以这条声明目前是冗余的,写在这里是为了不依赖\n * glass.css 的内部实现——若哪天 glass 改成非定位元素,这条仍然兜得住。\n * 它不是本轮为 closable 建立的包含块边界:body 上的 wui-glass class 与\n * static styles 里的 glass 都早于本轮存在。\n */\n position: relative;\n}\n\ndialog[open].is-visible .wui-dialog-body {\n opacity: 1;\n backdrop-filter: blur(4px) saturate(160%) brightness(var(--wui-glass-brightness, 1.06));\n}\n\n/* ===== 关闭按钮(绝对定位,两种内容模式同一套规则)=====\n\n 偏移由公开 token --wui-dialog-close-top / --wui-dialog-close-right 控制,默认都是字面量\n 20px。默认值不再经过 --wui-space-*:这两个 token 的参照系是卡片边缘,而不是相邻元素\n 之间的间距节奏,消费方调整主题间距时不应意外移动关闭按钮。dialog 本轮也不再与 drawer\n 的 16/16 对齐;--wui-drawer-close-* 保持不变。\n\n 两个公开 token 先在正文容器上解析成同一份内部变量,按钮定位和标题避让都读这份结果,\n 避免只改一个兜底字面量后按钮与标题各自按不同默认值计算。\n\n 两种模式下它都是卡片的直接子元素:绝对定位的包含块是 .wui-dialog-body\n (:host([closable]) 给了它 position: relative),top/right 因此量到卡片 padding box\n 的右上角,与 chrome 带的 padding 无关。 */\n.wui-dialog-close {\n position: absolute;\n z-index: 3;\n top: var(--wui-internal-dialog-close-top);\n right: var(--wui-internal-dialog-close-right);\n}\n\n/*\n * chrome 带:`header` 槽 + `heading` 属性 fallback,与 drawer 的 header 同形。\n * 卡片把 inline padding 让给各分区后(见 .wui-dialog-body),这条带自带水平 padding,\n * 否则内容会贴到卡片内沿。带里装的不一定是标题——消费方可以放整条工具条\n * (SettingsDialog 就是),所以用 flex / space-between 让多个子项排布;fallback 的\n * .wui-dialog-heading 与带共用 title 级排版,与 drawer 的 .wui-drawer-heading 对齐。\n * 带为空(槽无分配元素且 heading 为空)时隐藏,由 index.ts 的 _showHeader 决定。\n */\n.header {\n display: flex;\n gap: var(--wui-space-3, 12px);\n align-items: center;\n justify-content: space-between;\n\n margin: 0 0 var(--wui-dialog-title-gap, var(--wui-space-4, 16px));\n padding-inline: var(--wui-dialog-inline-padding, var(--wui-space-6, 24px));\n\n font-size: var(--wui-font-size-title, 18px);\n font-weight: var(--wui-font-weight-semibold, 600);\n line-height: var(--wui-line-height-tight, 1.2);\n color: var(--wui-color-text, #1b1b1b);\n}\n\n.header[hidden] {\n display: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n dialog.is-closing .wui-dialog-body,\n dialog[open].is-visible .wui-dialog-body {\n transition-duration: var(--wui-duration-feedback, 100ms);\n }\n}\n\n/* 标题避让:关闭按钮绝对定位、不占 chrome 带的位,长标题会一直排到按钮下面。\n 预留量取「按钮右偏移 + 按钮宽 + 呼吸间距」,而不是只减掉带自身的 padding——这样即便\n 消费者把 --wui-dialog-inline-padding 调到 0,内容右缘也不会越过按钮左缘;跟随\n --wui-dialog-close-right 变化;这里复用正文容器上已解析的内部右侧偏移,与按钮定位同源。\n 26px 对应 index.ts 里按钮的 size=\"26\",改按钮尺寸时一并改这里。\n 只在 closable 时生效:没有按钮就不必留白。 */\n:host([closable]) .header {\n padding-inline-end: calc(var(--wui-internal-dialog-close-right) + 26px + var(--wui-space-3, 12px));\n}\n\n.desc {\n /* 横向显式 clip 而非留默认的 visible:一旦本轴成为滚动容器,另一轴的 visible 会被\n 计算成 auto,宿主内容横向溢出时就会多出一条不该出现的横向滚动条。\n Chromium / WebKit 实测:clip 与本规则下面的 auto 配对时计算值会降级成 hidden\n (仍可被脚本滚动)。Firefox ≥128 不做这个降级、保持 clip,但仓库的 browser-mode\n 项目只跑 chromium,这句未经本仓 CI 证实。两个引擎都不画横向滚动条,所以这里\n 只需要「不是 auto」;断言写成 not.toBe('auto') 而非 toBe('clip'),否则会在\n 降级的引擎上误报。 */\n overflow-x: clip;\n overflow-y: auto;\n\n /* 承重的是上面那条 `overflow-y: auto`:按 CSS Sizing 3,溢出轴不为 visible 的\n grid 项其自动最小尺寸就是 0,`.desc` 无需再显式归零(实测删掉本条测试仍全绿,\n 改成 `auto auto auto` 也一样)。这里保留是防御性的:哪天 `overflow-y` 被改回\n visible,显式归零仍在,minmax(0,1fr) 才不会被顶破。 */\n min-height: 0;\n\n /* 内容区上限 = token + ring 余量:token 语义是内容区高度,ring 不该从它里面扣。\n token 只挂在这里,不与 dialog 的 max-height 共用——共用时外层(整卡)必然先触发。 */\n max-height: calc(var(--wui-dialog-max-height, 90vh) + var(--wui-dialog-desc-focus-padding, 6px) * 2);\n\n /* ===== focus-ring 余量(block 轴)=====\n\n .desc 既是滚动容器又是裁剪盒(clip),贴在内容区任一沿的控件,其 ring\n (offset 2 + width 3 = 5px)会被 padding box 裁掉;block 轴留 6px 余 1px,配一条\n 等量负 margin 把这 6px 从外部间距里扣回去——标题带→内容、内容→footer 的视觉间距\n 与改动前逐像素相同。\n\n inline 轴不再走 focus 余量:卡片的 inline padding 整段移进了本容器(padding-inline\n 取 --wui-dialog-inline-padding,卡片的 inline 分量归零,见 .wui-dialog-body),\n 滚动口因此与卡片内沿重合(滚动条贴边,与 headless 模式 / drawer 一致)。24px 的 inline\n padding 远大于 5px ring,左右焦点环不再被裁;也无需再配负 margin——content box 的\n 可用宽度与改动前相同(卡片原本也是同一档 inline padding),否则 AddDialog 那种两栏\n grid 的 drop zone 会换行(实测见 __tests__/dialog-content-height.browser.spec.ts 的\n 「全宽控件的 focus ring 不被内容区裁掉,且宿主拿到的可用宽度不被改窄」)。\n\n 取舍,是取舍不是疏漏:负 margin 让 `.desc` 的 border box 在 block 轴溢出滚动内容区,\n 所以底部那圈 ring 余量不随内容滚动——滚到最底时 footer 那一侧仍留得住 padding,而\n 内容末端到 `.desc` 下沿的 desc-gap 恒定,不跟着内容走。 */\n margin-top: calc(-1 * var(--wui-dialog-desc-focus-padding, 6px));\n\n /* margin-bottom 是「desc-gap 减去那对 padding-block」,两个都是公开 token:宿主把\n desc-gap 调到 focus-padding 以下就会算出负 margin(24/8 安全,4/2/0 会算出\n -2/-4/-6)。max() 兜住下界 0——那条路径下内容末端与 footer 会贴在一起,但盒模型\n 与 grid 行分配不进负值区间。 */\n margin-bottom: max(\n 0px,\n calc(var(--wui-dialog-desc-gap, var(--wui-space-6, 24px)) - var(--wui-dialog-desc-focus-padding, 6px))\n );\n margin-inline: 0;\n padding-block: var(--wui-dialog-desc-focus-padding, 6px);\n padding-inline: var(--wui-dialog-inline-padding, var(--wui-space-6, 24px));\n\n font-size: var(--wui-font-size, 14px);\n line-height: var(--wui-line-height-normal, 1.5);\n color: var(--wui-color-text, #1b1b1b);\n}\n\n/* `--wui-dialog-max-height` 是公开 token(README 有记录),语义是**内容区**高度。\n 默认值与整卡兜底同族(90vh / dvh),使未覆写的 dialog 视觉高度与改动前一致:\n 移动端 90vh 可能大于 90dvh(vh 取大视口高),少了 min() 内容区会探到工具栏下面。\n\n 必须紧跟在 `.desc` 之后而非文件开头的 @supports 块里:两者特异度相同(0,1,0),\n 靠后者胜。实测把本条挪到前面后,`.desc` 的计算值会退回 90vh。 */\n@supports (height: 100dvh) {\n .desc {\n max-height: calc(var(--wui-dialog-max-height, min(90vh, 90dvh)) + var(--wui-dialog-desc-focus-padding, 6px) * 2);\n }\n}\n\n.wui-dialog-footer {\n display: flex;\n flex-direction: column;\n gap: var(--wui-dialog-footer-gap, 10px);\n justify-content: var(--wui-dialog-footer-justify, flex-end);\n\n /* 卡片 inline padding 归零后由本区自带水平 padding,与 .header / .desc 同源。 */\n padding-inline: var(--wui-dialog-inline-padding, var(--wui-space-6, 24px));\n\n & ::slotted(web-ui-button) {\n display: flex;\n width: 100%;\n }\n}\n\n:host([horizontal]) .wui-dialog-footer {\n flex-direction: row;\n gap: var(--wui-space-3, 12px);\n justify-content: var(--wui-dialog-footer-justify, center);\n\n & ::slotted(web-ui-button:not([full])) {\n width: auto;\n }\n\n & ::slotted(web-ui-button[full]) {\n flex: 1;\n min-width: 0;\n }\n}\n"],"mappings":""}
|