@greypan/web-ui 12.0.0 → 12.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.CN.md +2 -0
  2. package/README.md +2 -0
  3. package/dist/assets/menu-portal.js +1 -1
  4. package/dist/assets/menu-portal.js.map +1 -1
  5. package/dist/assets/toast-containers.js +1 -1
  6. package/dist/assets/toast-containers.js.map +1 -1
  7. package/dist/components/autocomplete/index.d.ts +5 -0
  8. package/dist/components/autocomplete/index.d.ts.map +1 -1
  9. package/dist/components/autocomplete/index.js +17 -1
  10. package/dist/components/autocomplete/index.js.map +1 -1
  11. package/dist/components/autocomplete/style.js +1 -1
  12. package/dist/components/autocomplete/style.js.map +1 -1
  13. package/dist/components/back-top/style.js +1 -1
  14. package/dist/components/back-top/style.js.map +1 -1
  15. package/dist/components/button-group/style.js +1 -1
  16. package/dist/components/button-group/style.js.map +1 -1
  17. package/dist/components/collapse/index.d.ts +2 -2
  18. package/dist/components/collapse/index.js.map +1 -1
  19. package/dist/components/collapse/style.js.map +1 -1
  20. package/dist/components/dialog/index.d.ts +5 -1
  21. package/dist/components/dialog/index.d.ts.map +1 -1
  22. package/dist/components/dialog/index.js +16 -4
  23. package/dist/components/dialog/index.js.map +1 -1
  24. package/dist/components/dialog/style.js +1 -1
  25. package/dist/components/dialog/style.js.map +1 -1
  26. package/dist/components/drawer/index.js.map +1 -1
  27. package/dist/components/drawer/style.js.map +1 -1
  28. package/dist/components/dropdown/index.js +1 -1
  29. package/dist/components/icon/index.d.ts +1 -1
  30. package/dist/components/icon/index.d.ts.map +1 -1
  31. package/dist/components/index.d.ts +3 -2
  32. package/dist/components/index.d.ts.map +1 -1
  33. package/dist/components/index.js +2 -1
  34. package/dist/components/layout/index.d.ts +8 -0
  35. package/dist/components/layout/index.d.ts.map +1 -1
  36. package/dist/components/layout/index.js +23 -13
  37. package/dist/components/layout/index.js.map +1 -1
  38. package/dist/components/layout/style.js +1 -1
  39. package/dist/components/layout/style.js.map +1 -1
  40. package/dist/components/popover/index.js +1 -1
  41. package/dist/components/popover/style.js +1 -1
  42. package/dist/components/popover/style.js.map +1 -1
  43. package/dist/components/progress/index.d.ts +30 -0
  44. package/dist/components/progress/index.d.ts.map +1 -0
  45. package/dist/components/progress/index.js +103 -0
  46. package/dist/components/progress/index.js.map +1 -0
  47. package/dist/components/progress/style.js +5 -0
  48. package/dist/components/progress/style.js.map +1 -0
  49. package/dist/components/segmented-trigger/index.d.ts +2 -0
  50. package/dist/components/segmented-trigger/index.d.ts.map +1 -1
  51. package/dist/components/segmented-trigger/index.js +7 -0
  52. package/dist/components/segmented-trigger/index.js.map +1 -1
  53. package/dist/components/select/index.d.ts +13 -0
  54. package/dist/components/select/index.d.ts.map +1 -1
  55. package/dist/components/select/index.js +41 -3
  56. package/dist/components/select/index.js.map +1 -1
  57. package/dist/components/select/style.js +1 -1
  58. package/dist/components/select/style.js.map +1 -1
  59. package/dist/components/spinner/index.d.ts +0 -3
  60. package/dist/components/spinner/index.d.ts.map +1 -1
  61. package/dist/components/spinner/index.js +5 -20
  62. package/dist/components/spinner/index.js.map +1 -1
  63. package/dist/components/spinner/style.js +1 -1
  64. package/dist/components/spinner/style.js.map +1 -1
  65. package/dist/components/theme/style.js +1 -1
  66. package/dist/components/theme/style.js.map +1 -1
  67. package/dist/components/tooltip/index.js +1 -1
  68. package/dist/components/tooltip/style.js +1 -1
  69. package/dist/components/tooltip/style.js.map +1 -1
  70. package/dist/icons/generated/ant-design-sync-outlined.d.ts +7 -0
  71. package/dist/icons/generated/ant-design-sync-outlined.d.ts.map +1 -0
  72. package/dist/icons/generated/ant-design-sync-outlined.js +9 -0
  73. package/dist/icons/generated/ant-design-sync-outlined.js.map +1 -0
  74. package/dist/icons/generated/ic-baseline-phonelink.d.ts +7 -0
  75. package/dist/icons/generated/ic-baseline-phonelink.d.ts.map +1 -0
  76. package/dist/icons/generated/ic-baseline-phonelink.js +9 -0
  77. package/dist/icons/generated/ic-baseline-phonelink.js.map +1 -0
  78. package/dist/icons/generated/ic-outline-motion-photos-off.d.ts +7 -0
  79. package/dist/icons/generated/ic-outline-motion-photos-off.d.ts.map +1 -0
  80. package/dist/icons/generated/ic-outline-motion-photos-off.js +9 -0
  81. package/dist/icons/generated/ic-outline-motion-photos-off.js.map +1 -0
  82. package/dist/icons/generated/ic-outline-motion-photos-on.d.ts +7 -0
  83. package/dist/icons/generated/ic-outline-motion-photos-on.d.ts.map +1 -0
  84. package/dist/icons/generated/ic-outline-motion-photos-on.js +9 -0
  85. package/dist/icons/generated/ic-outline-motion-photos-on.js.map +1 -0
  86. package/dist/icons/generated/ic-round-menu-open.d.ts +7 -0
  87. package/dist/icons/generated/ic-round-menu-open.d.ts.map +1 -0
  88. package/dist/icons/generated/ic-round-menu-open.js +9 -0
  89. package/dist/icons/generated/ic-round-menu-open.js.map +1 -0
  90. package/dist/icons/generated/lucide-accessibility.d.ts +7 -0
  91. package/dist/icons/generated/lucide-accessibility.d.ts.map +1 -0
  92. package/dist/icons/generated/lucide-accessibility.js +9 -0
  93. package/dist/icons/generated/lucide-accessibility.js.map +1 -0
  94. package/dist/icons/generated/lucide-boxes.d.ts +7 -0
  95. package/dist/icons/generated/lucide-boxes.d.ts.map +1 -0
  96. package/dist/icons/generated/lucide-boxes.js +9 -0
  97. package/dist/icons/generated/lucide-boxes.js.map +1 -0
  98. package/dist/icons/generated/lucide-contrast.d.ts +7 -0
  99. package/dist/icons/generated/lucide-contrast.d.ts.map +1 -0
  100. package/dist/icons/generated/lucide-contrast.js +9 -0
  101. package/dist/icons/generated/lucide-contrast.js.map +1 -0
  102. package/dist/icons/generated/lucide-keyboard.d.ts +7 -0
  103. package/dist/icons/generated/lucide-keyboard.d.ts.map +1 -0
  104. package/dist/icons/generated/lucide-keyboard.js +9 -0
  105. package/dist/icons/generated/lucide-keyboard.js.map +1 -0
  106. package/dist/icons/generated/lucide-languages.d.ts +7 -0
  107. package/dist/icons/generated/lucide-languages.d.ts.map +1 -0
  108. package/dist/icons/generated/lucide-languages.js +9 -0
  109. package/dist/icons/generated/lucide-languages.js.map +1 -0
  110. package/dist/icons/generated/lucide-list.d.ts +7 -0
  111. package/dist/icons/generated/lucide-list.d.ts.map +1 -0
  112. package/dist/icons/generated/lucide-list.js +9 -0
  113. package/dist/icons/generated/lucide-list.js.map +1 -0
  114. package/dist/icons/generated/lucide-moon.d.ts +7 -0
  115. package/dist/icons/generated/lucide-moon.d.ts.map +1 -0
  116. package/dist/icons/generated/lucide-moon.js +9 -0
  117. package/dist/icons/generated/lucide-moon.js.map +1 -0
  118. package/dist/icons/generated/lucide-sparkles.d.ts +7 -0
  119. package/dist/icons/generated/lucide-sparkles.d.ts.map +1 -0
  120. package/dist/icons/generated/lucide-sparkles.js +9 -0
  121. package/dist/icons/generated/lucide-sparkles.js.map +1 -0
  122. package/dist/icons/generated/lucide-sun.d.ts +7 -0
  123. package/dist/icons/generated/lucide-sun.d.ts.map +1 -0
  124. package/dist/icons/generated/lucide-sun.js +9 -0
  125. package/dist/icons/generated/lucide-sun.js.map +1 -0
  126. package/dist/icons/generated/tabler-brand-typescript.d.ts +7 -0
  127. package/dist/icons/generated/tabler-brand-typescript.d.ts.map +1 -0
  128. package/dist/icons/generated/tabler-brand-typescript.js +9 -0
  129. package/dist/icons/generated/tabler-brand-typescript.js.map +1 -0
  130. package/dist/icons/index.d.ts +87 -72
  131. package/dist/icons/index.js +16 -1
  132. package/dist/shared/group-management/index.js.map +1 -1
  133. package/dist/shared/locale/builtin-labels.js +2 -1
  134. package/dist/shared/locale/builtin-labels.js.map +1 -1
  135. package/dist/shared/open-state/index.js.map +1 -1
  136. package/dist/shared/overlay/overlay.js.map +1 -1
  137. package/package.json +4 -2
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["glow","style"],"sources":["../../../src/components/collapse/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\n\nimport glow from '@/classes/glow.css?inline'\nimport { UserChangeController } from '@/shared/events/user-change'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { getTransitionDuration } from '@/shared/overlay/presence'\n\nimport style from './style.css?inline'\n\nconst EXIT_FALLBACK_BUFFER = 80\n\n/** peek 边缘渐隐的活动长度:关闭/收起态取由 peek 推导的长度,展开态归 0。 */\nconst PEEK_EDGE_ACTIVE = '--wui-collapse-peek-edge-active'\n\n/*\n * 注册成 `<length>` 才能被 transition 插值(未注册的自定义属性是离散值,只能瞬变)。\n *\n * 必须用 JS 的 `CSS.registerProperty` 而不是 CSS 里的 `@property`:实测后者写在\n * shadow root 的样式表里不产生注册(getComputedStyle 读打开态得到空串而非\n * initial-value),而自定义属性的注册本质上是 document 级的。\n * 注册失败(jsdom、旧浏览器、重复注册)时降级为离散过渡——渐变带长度瞬变,\n * 但位置百分比仍跟随裁剪边缘,行为依旧正确。\n *\n * `inherits` 必须是 true:晕染伪元素(style.css 的 ::after)读同一个值来调制模糊,\n * 继承为 false 时伪元素读到的是 initialValue 0px——mask 的各色标随之塌缩到 100%,\n * 模糊层静默消失且其余渲染一切照旧,只少一层效果。\n */\nfunction registerPeekEdgeActive() {\n // jsdom 与不支持的环境直接跳过;样式里的 var() fallback 保证声明仍然有效。\n if (typeof CSS === 'undefined' || typeof CSS.registerProperty !== 'function') return\n try {\n CSS.registerProperty({\n name: PEEK_EDGE_ACTIVE,\n syntax: '<length>',\n inherits: true,\n initialValue: '0px'\n })\n } catch {\n // 已注册过(HMR、多份副本)或定义冲突:保持现有注册即可。\n }\n}\n\nregisterPeekEdgeActive()\n\n/**\n * 文档流内 collapse panel:trigger 经默认 slot 提供,内容经 `slot=\"content\"` 提供。\n *\n * trigger 语义完全来自 slot 进来的交互元素(原生 button / web-ui-button 等):\n * Enter/Space 激活走该元素的原生 click,不做 role=\"button\" 补齐;组件把\n * `aria-expanded` 回写到首个 assigned element,并在 `disabled` 时把该元素置为\n * `disabled`。wrapper 仅负责点击代理,不可聚焦,不承载 ARIA。\n *\n * 不回写 `aria-controls`:目标 track 在组件 shadow root 内,IDREF 不能跨 shadow\n * root 解析(Chrome 的 AX 树里该属性会被当作悬空引用丢弃),写出去只是死属性。\n * 展开关系由 `aria-expanded` 表达,这也是 AT 实际依赖的那一条。\n *\n * 禁用也走 `disabled` 而不是 `aria-disabled`:后者在 `<web-ui-button>` 上只会落在\n * 宿主(属性不跨 shadow 转发),AT 读不到;`disabled` 是真禁用,原生 button 与\n * web-ui-button 都能生效。\n *\n * 内容区在 shadow 内以 grid `0fr ↔ 1fr` track 过渡驱动高度/宽度动画(ADR-0030\n * 选型保留)。关闭稳态三态(消费者 light DOM 永不移动,hidden 全部命令式管理,\n * 避免渲染绑定在关闭动画起点抢先 display:none):\n * - 默认:content 容器 `hidden`(display:none,脱离渲染与可访问性树);\n * - `keep-mounted`:inner `inert`(保留在 0fr track 内,滚动位置与布局可测量);\n * - `peek`:inner `inert`,track 停在固定长度的裁剪窗口,只露出内容头部;\n * - 动画进行中:inner `inert` + track pointer-events 禁用,结束后进入对应稳态。\n *\n * `peek` 与展开态之间是固定长度与内容自适应高度的过渡,CSS 无法插值;这两个\n * 方向的动画改为读出像素后以显式长度写入 track,落稳态时清除内联值交回 CSS 规则。\n */\n@customElement('web-ui-collapse')\nexport class WebUiCollapse extends LitElement {\n static override styles = [unsafeCSS(glow), unsafeCSS(style)]\n\n /** 展开状态;严格受控,唯一状态源。 */\n @property({ type: Boolean, reflect: true }) open = false\n\n /** 禁用触发:点击忽略、`disabled` 写入 trigger 元素;已展开内容保持现状。 */\n @property({ type: Boolean, reflect: true }) disabled = false\n\n /** 水平方向:true 时沿宽度展开,默认沿高度。 */\n @property({ type: Boolean, reflect: true }) horizontal = false\n\n /** 关闭稳态保持内容 `inert` 而非 `hidden`(保留滚动位置,可测量)。 */\n @property({ type: Boolean, reflect: true, attribute: 'keep-mounted' }) keepMounted = false\n\n /**\n * 关闭稳态露出的尺寸(CSS 长度,如 `120px`、`6rem`),沿动画轴生效:默认是高度,\n * `horizontal` 时为宽度;未设置时保持完全收起。\n *\n * 设置后关闭稳态语义等同 `keep-mounted`(内容留在 track 内、`inert` 阻断交互),\n * 只露出 `peek` 范围内的部分;内容本身不足 `peek` 时按内容实际尺寸收起。\n * 注意 `peek` 是固定长度与内容自适应高度之间的过渡:CSS 无法在两者之间插值,\n * 因此这两个方向的动画由本组件读出内容尺寸后以显式长度驱动(其余路径仍是\n * 零测量的 grid fr 过渡)。\n *\n * 露出区域末端自带一段边缘渐隐(提示下方还有内容),长度由 `peek` 按比例推导,\n * 无需也不接受调用方单独指定——见 style.css 中 `--wui-collapse-peek-edge-*`\n * 系列变量(比例、上限、末端颜色可用 CSS 覆盖,属于主题级调优而非组件 API)。\n */\n @property({ type: String, reflect: true }) peek: string | null = null\n\n @query('.wui-collapse-trigger-wrapper') private _triggerWrapper!: HTMLElement\n\n private readonly _userOpenChange = new UserChangeController()\n\n private _exitTimer?: ReturnType<typeof setTimeout>\n // 动画代际:每次管线启动自增,旧管线的 transitionend/兜底定时器/rAF 全部失效。\n private _generation = 0\n private _lastOpen?: boolean\n private _triggerSnapshot?: {\n trigger: HTMLElement\n expanded: string | null\n disabled: string | null\n }\n\n override connectedCallback() {\n super.connectedCallback()\n // 点击监听与生命周期同寿(宿主移除即整体销毁),无需 disconnected 清理。\n this.addEventListener('click', this._onTriggerClick)\n // 断连可能打断动画管线(瞬态残留:presence='closing'、容器未 hidden)。\n // 首次连接不必处理(初始 render 走 _lastOpen===undefined 落稳态,请求多余\n // 渲染会触发「scheduled an update after update completed」告警);重连时\n // 重置同步状态并强制一轮 update,由首次同步分支重新落稳态、丢弃在途管线。\n if (this.hasUpdated) {\n this._generation++\n this._clearExitTimer()\n this._lastOpen = undefined\n this.requestUpdate()\n }\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._clearExitTimer()\n this._restoreTriggerSnapshot()\n }\n\n protected override updated(changed: Map<string, unknown>) {\n // trigger 状态回写不依赖 open 分支,任何渲染后都保持与宿主状态同步。\n this._syncTriggerState()\n // peek 的稳态长度与动画写入共用 track 内联样式,先同步再落稳态,避免被稳态清除。\n this._syncPeek()\n\n // 首次同步(含 open 为默认值的场景,changed map 不含默认值属性):\n // 直接落到正确稳态,不播动画(popover 初始 open 同款)。\n if (this._lastOpen === undefined) {\n this._lastOpen = this.open\n if (this.open) this._applyOpenSteadyState()\n else this._applyClosedSteadyState()\n return\n }\n\n if ((changed.has('keepMounted') || changed.has('peek')) && !this.open) {\n // 运行态切换 keep-mounted / peek:关闭稳态需在 hidden / inert / 裁剪长度间重新落地。\n this._applyClosedSteadyState()\n }\n if (!changed.has('open') || this.open === this._lastOpen) return\n\n this._lastOpen = this.open\n if (this._userOpenChange.consume()) this._dispatchChange(this.open)\n if (this.open) this._beginExpand()\n else this._beginCollapse()\n }\n\n show() {\n if (this.open) return\n this.open = true\n }\n\n close() {\n if (!this.open) return\n this.open = false\n }\n\n toggle() {\n if (this.open) this.close()\n else this.show()\n }\n\n private _dispatchChange(open: boolean) {\n dispatchOpenChangeEvent(this, open)\n }\n\n /*\n * 点击代理:冒泡路径穿过 trigger wrapper 的 click 一律切换。语义由 slot 内\n * 的交互元素(原生 button / web-ui-button)提供;slot 放纯文本时无激活语义\n * (README 约定)。内容区与嵌套内层 collapse 的 click 不经过本 wrapper,天然隔离。\n */\n private _onTriggerClick = (event: MouseEvent) => {\n if (this.disabled) return\n if (!event.composedPath().includes(this._triggerWrapper)) return\n this._userOpenChange.mark()\n this.toggle()\n }\n\n /*\n * Trigger 元素取首个 assigned element(与 popover `_queryTrigger` 同款惰性查询,\n * 不依赖 slotchange——jsdom 不派发该事件,无法用状态缓存)。slot 为空时无回写目标。\n */\n private _queryTrigger(): HTMLElement | null {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('.wui-collapse-trigger-wrapper slot')\n const el = slot?.assignedElements()[0]\n return el instanceof HTMLElement ? el : null\n }\n\n /*\n * slot 内容增删不触发宿主响应式更新:显式请求一轮渲染,由 updated() 的\n * trigger 状态回写覆盖晚到的 trigger 元素(slotchange 在 jsdom 不派发,无法监听)。\n */\n private readonly _onTriggerSlotChange = () => {\n this.requestUpdate()\n }\n\n private _restoreTriggerSnapshot() {\n const snapshot = this._triggerSnapshot\n if (!snapshot) return\n\n for (const [attribute, value] of Object.entries({\n 'aria-expanded': snapshot.expanded,\n disabled: snapshot.disabled\n })) {\n if (value === null) snapshot.trigger.removeAttribute(attribute)\n else snapshot.trigger.setAttribute(attribute, value)\n }\n this._triggerSnapshot = undefined\n }\n\n /*\n * 状态回写到 trigger 元素本身(wrapper 不可聚焦,AT 读不到):展开态用\n * `aria-expanded`,禁用态用 `disabled`——后者是唯一能穿透 `<web-ui-button>`\n * shadow 边界的表达(属性会被内部原生 button 读到),且与「真禁用」语义一致。\n */\n private _syncTriggerState() {\n const trigger = this._queryTrigger()\n if (this._triggerSnapshot?.trigger !== trigger) {\n this._restoreTriggerSnapshot()\n if (trigger) {\n this._triggerSnapshot = {\n trigger,\n expanded: trigger.getAttribute('aria-expanded'),\n disabled: trigger.getAttribute('disabled')\n }\n }\n }\n if (!trigger) return\n trigger.setAttribute('aria-expanded', String(this.open))\n trigger.toggleAttribute('disabled', this.disabled)\n }\n\n // ===== 内容区动画管线(三态语义见 ADR-0030) =====\n\n /** 关闭稳态是否露出部分内容;空值与非空判断避免 `peek=\"\"` 被当作有效长度。 */\n private get _peeks(): boolean {\n return typeof this.peek === 'string' && this.peek.trim() !== ''\n }\n\n private _applyOpenSteadyState() {\n this._generation++\n this._clearExitTimer()\n const container = this._contentContainer()\n const track = this._contentTrack()\n if (container) container.hidden = false\n if (track) track.dataset.wuiPresence = 'open'\n this._clearInnerClamp()\n this._setInnerInert(false)\n }\n\n private _applyClosedSteadyState() {\n this._generation++\n this._clearExitTimer()\n const container = this._contentContainer()\n const track = this._contentTrack()\n if (track) delete track.dataset.wuiPresence\n this._clearInnerClamp()\n if (this.keepMounted || this._peeks) {\n if (container) container.hidden = false\n this._setInnerInert(true)\n return\n }\n this._setInnerInert(false)\n if (container) container.hidden = true\n }\n\n private _beginExpand() {\n this._generation++\n this._clearExitTimer()\n const container = this._contentContainer()\n const track = this._contentTrack()\n if (!container || !track) return\n\n const generation = this._generation\n // 中断关闭动画时 inner 已 inert:先恢复可交互,内容随展开重新可见。\n this._setInnerInert(false)\n // 从 display:none 进入时无先前计算样式可过渡:提交 0fr 起点后再翻转。\n container.hidden = false\n\n if (this._peeks) {\n const inner = this._contentInner()\n // 无过渡时长(reduced motion、jsdom)或无 inner 时不做测量,直接落展开稳态。\n if (!inner || getTransitionDuration(inner, 0) === 0) {\n this._applyOpenSteadyState()\n return\n }\n /*\n * peek 的裁剪长度是固定值,展开目标是内容自适应高度,两者无法由 CSS 插值,\n * 动画必须由两个显式长度驱动:起点是当前露出的长度,目标是内容完整尺寸。\n * 起点要在同任务内先写入并提交回流,否则过渡会退化成跳变(实测分帧写入\n * 反而完全不参与插值判定);目标读 inner 的 scroll 尺寸,不写探针样式。\n */\n const from = this._measureInnerSize(inner)\n const to = this._measureContentSize(inner)\n this._setInnerClamp(inner, `${from}px`)\n void this._measureInnerSize(inner)\n track.dataset.wuiPresence = 'open'\n this._setInnerClamp(inner, `${to}px`)\n this._awaitTransition(inner, generation, () => this._settle(true), [this._clampProperty()])\n return\n }\n\n void track.offsetWidth\n\n requestAnimationFrame(() => {\n if (generation !== this._generation || !this.open) return\n track.dataset.wuiPresence = 'open'\n this._awaitTransition(track, generation, () => this._settle(true))\n })\n }\n\n private _beginCollapse() {\n this._generation++\n this._clearExitTimer()\n const track = this._contentTrack()\n if (!track) return\n\n // 收起期间立即阻断交互,防止焦点落入正在消失的区域。\n this._setInnerInert(true)\n const generation = this._generation\n\n if (this._peeks) {\n const inner = this._contentInner()\n // 无过渡时长(reduced motion、jsdom)或无 inner 时不做测量,直接落裁剪稳态。\n if (!inner || getTransitionDuration(inner, 0) === 0) {\n this._applyClosedSteadyState()\n return\n }\n /*\n * 收起目标是 `peek` 长度本身:与内容尺寸的较小值由 CSS 稳态规则解析,\n * 动画只需把当前展开长度过渡到这个长度。起点同样要先写入并提交回流,\n * 顺序与 _beginExpand 一致。\n */\n const from = this._measureInnerSize(inner)\n this._setInnerClamp(inner, `${from}px`)\n void this._measureInnerSize(inner)\n track.dataset.wuiPresence = 'closing'\n this._setInnerClamp(inner, `${this.peek}`)\n this._awaitTransition(inner, generation, () => this._settle(false), [this._clampProperty()])\n return\n }\n\n track.dataset.wuiPresence = 'closing'\n this._awaitTransition(track, generation, () => this._settle(false))\n }\n\n // 关闭稳态:keep-mounted → 内容保留在 0fr track 内并 inert;peek → 保留在裁剪\n // 窗口内并 inert;默认 → 容器 hidden。\n private _settle(open: boolean) {\n this._clearExitTimer()\n const track = this._contentTrack()\n // peek 过渡以显式长度写入 inner:落稳态一律清除,交回 CSS 稳态规则(长度声明)。\n this._clearInnerClamp()\n if (open) {\n this._setInnerInert(false)\n return\n }\n // 落稳态必须清除 closing 瞬态标记:否则 track 永久保留 pointer-events:none\n // 与 exit 过渡配置(keep-mounted 分支同样清除,避免残留)。\n if (track) delete track.dataset.wuiPresence\n if (this.keepMounted || this._peeks) return\n const container = this._contentContainer()\n this._setInnerInert(false)\n if (container) container.hidden = true\n }\n\n // 等待动画属性过渡结束;时长为 0(reduced motion、jsdom)直接稳态,\n // transitionend 不触发时按时长 + 缓冲兜底。默认等待 grid track 过渡;peek 的\n // 过渡落在 inner 的裁剪长度上,由调用方传入对应属性。\n private _awaitTransition(\n animated: HTMLElement,\n generation: number,\n onDone: () => void,\n properties: readonly string[] = ['grid-template-rows', 'grid-template-columns']\n ) {\n const duration = getTransitionDuration(animated, 0)\n if (duration === 0) {\n if (generation === this._generation) onDone()\n return\n }\n\n let settled = false\n const finish = () => {\n animated.removeEventListener('transitionend', onTransitionEnd)\n if (this._exitTimer === timer) this._exitTimer = undefined\n clearTimeout(timer)\n // 过渡可能被属性匹配的后续动画触发(如中断重开):代际不符时静默退场。\n if (settled || generation !== this._generation) return\n settled = true\n onDone()\n }\n const timer = setTimeout(finish, duration + EXIT_FALLBACK_BUFFER)\n this._exitTimer = timer\n\n const onTransitionEnd = (event: TransitionEvent) => {\n if (event.target === animated && properties.includes(event.propertyName)) finish()\n }\n animated.addEventListener('transitionend', onTransitionEnd)\n }\n\n private _clearExitTimer() {\n if (this._exitTimer !== undefined) {\n clearTimeout(this._exitTimer)\n this._exitTimer = undefined\n }\n }\n\n // ===== peek 尺寸读写(裁剪长度落在 inner 上,track 只负责 1fr 与裁剪窗口) =====\n\n /*\n * peek 长度经自定义属性下发给 shadow 样式规则,而不是直接写 inner 的裁剪长度:\n * 关闭稳态由 CSS 声明(长度与内容尺寸的较小值由 CSS 原生解析),动画写入的内联\n * 长度在其上覆盖,落稳态时清除即可回到声明值。\n */\n private _syncPeek() {\n const track = this._contentTrack()\n if (!track) return\n track.toggleAttribute('data-wui-peek', this._peeks)\n if (this._peeks) {\n track.style.setProperty('--wui-collapse-peek', this.peek ?? '')\n } else {\n track.style.removeProperty('--wui-collapse-peek')\n }\n }\n\n private _clampProperty(): 'max-height' | 'max-width' {\n return this.horizontal ? 'max-width' : 'max-height'\n }\n\n private _measureInnerSize(inner: HTMLElement): number {\n return this.horizontal ? inner.offsetWidth : inner.offsetHeight\n }\n\n /*\n * 内容在动画轴上的完整尺寸(px):裁剪只作用在 inner 的 max-height/max-width 上,\n * 被测内容的布局盒子仍是完整尺寸,故读 scroll 尺寸即可,不写任何样式——写样式\n * 会被 grid 过渡机制当作过渡起点读取,必须避开。\n */\n private _measureContentSize(inner: HTMLElement): number {\n return this.horizontal ? inner.scrollWidth : inner.scrollHeight\n }\n\n private _setInnerClamp(inner: HTMLElement, size: string) {\n inner.style.setProperty(this._clampProperty(), size)\n }\n\n private _clearInnerClamp() {\n const inner = this._contentInner()\n if (!inner) return\n inner.style.removeProperty('max-height')\n inner.style.removeProperty('max-width')\n }\n\n private _setInnerInert(inert: boolean) {\n this._contentInner()?.toggleAttribute('inert', inert)\n }\n\n private _contentInner(): HTMLElement | null {\n return this.shadowRoot?.querySelector<HTMLElement>('.wui-collapse-inner') ?? null\n }\n\n private _contentContainer(): HTMLElement | null {\n return this.shadowRoot?.querySelector<HTMLElement>('.wui-collapse-content') ?? null\n }\n\n private _contentTrack(): HTMLElement | null {\n return this.shadowRoot?.querySelector<HTMLElement>('.wui-collapse-track') ?? null\n }\n\n override render() {\n // 三态由 data-wui-presence 驱动(open/closing),缺省即关闭稳态。\n const trackClass = {\n 'wui-collapse-track': true,\n 'is-horizontal': this.horizontal\n }\n\n return html`\n <div class=\"wui-collapse-trigger-wrapper\" @slotchange=${this._onTriggerSlotChange}><slot></slot></div>\n <div class=\"wui-collapse-content\">\n <div class=${classMap(trackClass)}>\n <div class=${classMap({ 'wui-collapse-inner': true, 'wui-glow': this._peeks })}>\n <slot name=\"content\"></slot>\n </div>\n </div>\n </div>\n `\n }\n\n declare readonly $events: {\n 'open-change': CustomEvent<{ open: boolean }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-collapse': WebUiCollapse\n }\n}\n"],"mappings":";;;;;;;;;;AAWA,MAAM,uBAAuB;;AAG7B,MAAM,mBAAmB;AAezB,SAAS,yBAAyB;CAEhC,IAAI,OAAO,QAAQ,eAAe,OAAO,IAAI,qBAAqB,YAAY;CAC9E,IAAI;EACF,IAAI,iBAAiB;GACnB,MAAM;GACN,QAAQ;GACR,UAAU;GACV,cAAc;EAChB,CAAC;CACH,QAAQ,CAER;AACF;AAEA,uBAAuB;AA8BhB,IAAM,gBAAN,MAAM,sBAAsB,WAAW;;;EAIO,KAAA,OAAA;EAGI,KAAA,WAAA;EAGE,KAAA,aAAA;EAG4B,KAAA,cAAA;EAgBpB,KAAA,OAAA;EAI9B,KAAA,kBAAA,IAAI,qBAAqB;EAItC,KAAA,cAAA;EAiFK,KAAA,mBAAA,UAAsB;GAC/C,IAAI,KAAK,UAAU;GACnB,IAAI,CAAC,MAAM,aAAa,CAAC,CAAC,SAAS,KAAK,eAAe,GAAG;GAC1D,KAAK,gBAAgB,KAAK;GAC1B,KAAK,OAAO;EACd;EAgB8C,KAAA,6BAAA;GAC5C,KAAK,cAAc;EACrB;;;EA5IyB,KAAA,SAAA,CAAC,UAAUA,YAAI,GAAG,UAAUC,aAAK,CAAC;;CA4C3D,oBAA6B;EAC3B,MAAM,kBAAkB;EAExB,KAAK,iBAAiB,SAAS,KAAK,eAAe;EAKnD,IAAI,KAAK,YAAY;GACnB,KAAK;GACL,KAAK,gBAAgB;GACrB,KAAK,YAAY,KAAA;GACjB,KAAK,cAAc;EACrB;CACF;CAEA,uBAAgC;EAC9B,MAAM,qBAAqB;EAC3B,KAAK,gBAAgB;EACrB,KAAK,wBAAwB;CAC/B;CAEA,QAA2B,SAA+B;EAExD,KAAK,kBAAkB;EAEvB,KAAK,UAAU;EAIf,IAAI,KAAK,cAAc,KAAA,GAAW;GAChC,KAAK,YAAY,KAAK;GACtB,IAAI,KAAK,MAAM,KAAK,sBAAsB;QACrC,KAAK,wBAAwB;GAClC;EACF;EAEA,KAAK,QAAQ,IAAI,aAAa,KAAK,QAAQ,IAAI,MAAM,MAAM,CAAC,KAAK,MAE/D,KAAK,wBAAwB;EAE/B,IAAI,CAAC,QAAQ,IAAI,MAAM,KAAK,KAAK,SAAS,KAAK,WAAW;EAE1D,KAAK,YAAY,KAAK;EACtB,IAAI,KAAK,gBAAgB,QAAQ,GAAG,KAAK,gBAAgB,KAAK,IAAI;EAClE,IAAI,KAAK,MAAM,KAAK,aAAa;OAC5B,KAAK,eAAe;CAC3B;CAEA,OAAO;EACL,IAAI,KAAK,MAAM;EACf,KAAK,OAAO;CACd;CAEA,QAAQ;EACN,IAAI,CAAC,KAAK,MAAM;EAChB,KAAK,OAAO;CACd;CAEA,SAAS;EACP,IAAI,KAAK,MAAM,KAAK,MAAM;OACrB,KAAK,KAAK;CACjB;CAEA,gBAAwB,MAAe;EACrC,wBAAwB,MAAM,IAAI;CACpC;CAkBA,gBAA4C;EAE1C,MAAM,MADO,KAAK,YAAY,cAA+B,oCAAoC,EAAA,EAChF,iBAAiB,CAAC,CAAC;EACpC,OAAO,cAAc,cAAc,KAAK;CAC1C;CAUA,0BAAkC;EAChC,MAAM,WAAW,KAAK;EACtB,IAAI,CAAC,UAAU;EAEf,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ;GAC9C,iBAAiB,SAAS;GAC1B,UAAU,SAAS;EACrB,CAAC,GACC,IAAI,UAAU,MAAM,SAAS,QAAQ,gBAAgB,SAAS;OACzD,SAAS,QAAQ,aAAa,WAAW,KAAK;EAErD,KAAK,mBAAmB,KAAA;CAC1B;CAOA,oBAA4B;EAC1B,MAAM,UAAU,KAAK,cAAc;EACnC,IAAI,KAAK,kBAAkB,YAAY,SAAS;GAC9C,KAAK,wBAAwB;GAC7B,IAAI,SACF,KAAK,mBAAmB;IACtB;IACA,UAAU,QAAQ,aAAa,eAAe;IAC9C,UAAU,QAAQ,aAAa,UAAU;GAC3C;EAEJ;EACA,IAAI,CAAC,SAAS;EACd,QAAQ,aAAa,iBAAiB,OAAO,KAAK,IAAI,CAAC;EACvD,QAAQ,gBAAgB,YAAY,KAAK,QAAQ;CACnD;;CAKA,IAAY,SAAkB;EAC5B,OAAO,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,KAAK,MAAM;CAC/D;CAEA,wBAAgC;EAC9B,KAAK;EACL,KAAK,gBAAgB;EACrB,MAAM,YAAY,KAAK,kBAAkB;EACzC,MAAM,QAAQ,KAAK,cAAc;EACjC,IAAI,WAAW,UAAU,SAAS;EAClC,IAAI,OAAO,MAAM,QAAQ,cAAc;EACvC,KAAK,iBAAiB;EACtB,KAAK,eAAe,KAAK;CAC3B;CAEA,0BAAkC;EAChC,KAAK;EACL,KAAK,gBAAgB;EACrB,MAAM,YAAY,KAAK,kBAAkB;EACzC,MAAM,QAAQ,KAAK,cAAc;EACjC,IAAI,OAAO,OAAO,MAAM,QAAQ;EAChC,KAAK,iBAAiB;EACtB,IAAI,KAAK,eAAe,KAAK,QAAQ;GACnC,IAAI,WAAW,UAAU,SAAS;GAClC,KAAK,eAAe,IAAI;GACxB;EACF;EACA,KAAK,eAAe,KAAK;EACzB,IAAI,WAAW,UAAU,SAAS;CACpC;CAEA,eAAuB;EACrB,KAAK;EACL,KAAK,gBAAgB;EACrB,MAAM,YAAY,KAAK,kBAAkB;EACzC,MAAM,QAAQ,KAAK,cAAc;EACjC,IAAI,CAAC,aAAa,CAAC,OAAO;EAE1B,MAAM,aAAa,KAAK;EAExB,KAAK,eAAe,KAAK;EAEzB,UAAU,SAAS;EAEnB,IAAI,KAAK,QAAQ;GACf,MAAM,QAAQ,KAAK,cAAc;GAEjC,IAAI,CAAC,SAAS,sBAAsB,OAAO,CAAC,MAAM,GAAG;IACnD,KAAK,sBAAsB;IAC3B;GACF;GAOA,MAAM,OAAO,KAAK,kBAAkB,KAAK;GACzC,MAAM,KAAK,KAAK,oBAAoB,KAAK;GACzC,KAAK,eAAe,OAAO,GAAG,KAAK,GAAG;GACtC,KAAU,kBAAkB,KAAK;GACjC,MAAM,QAAQ,cAAc;GAC5B,KAAK,eAAe,OAAO,GAAG,GAAG,GAAG;GACpC,KAAK,iBAAiB,OAAO,kBAAkB,KAAK,QAAQ,IAAI,GAAG,CAAC,KAAK,eAAe,CAAC,CAAC;GAC1F;EACF;EAEA,MAAW;EAEX,4BAA4B;GAC1B,IAAI,eAAe,KAAK,eAAe,CAAC,KAAK,MAAM;GACnD,MAAM,QAAQ,cAAc;GAC5B,KAAK,iBAAiB,OAAO,kBAAkB,KAAK,QAAQ,IAAI,CAAC;EACnE,CAAC;CACH;CAEA,iBAAyB;EACvB,KAAK;EACL,KAAK,gBAAgB;EACrB,MAAM,QAAQ,KAAK,cAAc;EACjC,IAAI,CAAC,OAAO;EAGZ,KAAK,eAAe,IAAI;EACxB,MAAM,aAAa,KAAK;EAExB,IAAI,KAAK,QAAQ;GACf,MAAM,QAAQ,KAAK,cAAc;GAEjC,IAAI,CAAC,SAAS,sBAAsB,OAAO,CAAC,MAAM,GAAG;IACnD,KAAK,wBAAwB;IAC7B;GACF;GAMA,MAAM,OAAO,KAAK,kBAAkB,KAAK;GACzC,KAAK,eAAe,OAAO,GAAG,KAAK,GAAG;GACtC,KAAU,kBAAkB,KAAK;GACjC,MAAM,QAAQ,cAAc;GAC5B,KAAK,eAAe,OAAO,GAAG,KAAK,MAAM;GACzC,KAAK,iBAAiB,OAAO,kBAAkB,KAAK,QAAQ,KAAK,GAAG,CAAC,KAAK,eAAe,CAAC,CAAC;GAC3F;EACF;EAEA,MAAM,QAAQ,cAAc;EAC5B,KAAK,iBAAiB,OAAO,kBAAkB,KAAK,QAAQ,KAAK,CAAC;CACpE;CAIA,QAAgB,MAAe;EAC7B,KAAK,gBAAgB;EACrB,MAAM,QAAQ,KAAK,cAAc;EAEjC,KAAK,iBAAiB;EACtB,IAAI,MAAM;GACR,KAAK,eAAe,KAAK;GACzB;EACF;EAGA,IAAI,OAAO,OAAO,MAAM,QAAQ;EAChC,IAAI,KAAK,eAAe,KAAK,QAAQ;EACrC,MAAM,YAAY,KAAK,kBAAkB;EACzC,KAAK,eAAe,KAAK;EACzB,IAAI,WAAW,UAAU,SAAS;CACpC;CAKA,iBACE,UACA,YACA,QACA,aAAgC,CAAC,sBAAsB,uBAAuB,GAC9E;EACA,MAAM,WAAW,sBAAsB,UAAU,CAAC;EAClD,IAAI,aAAa,GAAG;GAClB,IAAI,eAAe,KAAK,aAAa,OAAO;GAC5C;EACF;EAEA,IAAI,UAAU;EACd,MAAM,eAAe;GACnB,SAAS,oBAAoB,iBAAiB,eAAe;GAC7D,IAAI,KAAK,eAAe,OAAO,KAAK,aAAa,KAAA;GACjD,aAAa,KAAK;GAElB,IAAI,WAAW,eAAe,KAAK,aAAa;GAChD,UAAU;GACV,OAAO;EACT;EACA,MAAM,QAAQ,WAAW,QAAQ,WAAW,oBAAoB;EAChE,KAAK,aAAa;EAElB,MAAM,mBAAmB,UAA2B;GAClD,IAAI,MAAM,WAAW,YAAY,WAAW,SAAS,MAAM,YAAY,GAAG,OAAO;EACnF;EACA,SAAS,iBAAiB,iBAAiB,eAAe;CAC5D;CAEA,kBAA0B;EACxB,IAAI,KAAK,eAAe,KAAA,GAAW;GACjC,aAAa,KAAK,UAAU;GAC5B,KAAK,aAAa,KAAA;EACpB;CACF;CASA,YAAoB;EAClB,MAAM,QAAQ,KAAK,cAAc;EACjC,IAAI,CAAC,OAAO;EACZ,MAAM,gBAAgB,iBAAiB,KAAK,MAAM;EAClD,IAAI,KAAK,QACP,MAAM,MAAM,YAAY,uBAAuB,KAAK,QAAQ,EAAE;OAE9D,MAAM,MAAM,eAAe,qBAAqB;CAEpD;CAEA,iBAAqD;EACnD,OAAO,KAAK,aAAa,cAAc;CACzC;CAEA,kBAA0B,OAA4B;EACpD,OAAO,KAAK,aAAa,MAAM,cAAc,MAAM;CACrD;CAOA,oBAA4B,OAA4B;EACtD,OAAO,KAAK,aAAa,MAAM,cAAc,MAAM;CACrD;CAEA,eAAuB,OAAoB,MAAc;EACvD,MAAM,MAAM,YAAY,KAAK,eAAe,GAAG,IAAI;CACrD;CAEA,mBAA2B;EACzB,MAAM,QAAQ,KAAK,cAAc;EACjC,IAAI,CAAC,OAAO;EACZ,MAAM,MAAM,eAAe,YAAY;EACvC,MAAM,MAAM,eAAe,WAAW;CACxC;CAEA,eAAuB,OAAgB;EACrC,KAAK,cAAc,CAAC,EAAE,gBAAgB,SAAS,KAAK;CACtD;CAEA,gBAA4C;EAC1C,OAAO,KAAK,YAAY,cAA2B,qBAAqB,KAAK;CAC/E;CAEA,oBAAgD;EAC9C,OAAO,KAAK,YAAY,cAA2B,uBAAuB,KAAK;CACjF;CAEA,gBAA4C;EAC1C,OAAO,KAAK,YAAY,cAA2B,qBAAqB,KAAK;CAC/E;CAEA,SAAkB;EAEhB,MAAM,aAAa;GACjB,sBAAsB;GACtB,iBAAiB,KAAK;EACxB;EAEA,OAAO,IAAI;8DAC+C,KAAK,qBAAqB;;qBAEnE,SAAS,UAAU,EAAE;uBACnB,SAAS;GAAE,sBAAsB;GAAM,YAAY,KAAK;EAAO,CAAC,EAAE;;;;;;CAMvF;AAKF;AAlbG,WAAA,CAAA,SAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,cAAA,WAAA,QAAA,KAAA,CAAA;AAGzC,WAAA,CAAA,SAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,cAAA,WAAA,YAAA,KAAA,CAAA;AAGzC,WAAA,CAAA,SAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,cAAA,WAAA,cAAA,KAAA,CAAA;AAGzC,WAAA,CAAA,SAAS;CAAE,MAAM;CAAS,SAAS;CAAM,WAAW;AAAe,CAAC,CAAA,GAAA,cAAA,WAAA,eAAA,KAAA,CAAA;AAgBpE,WAAA,CAAA,SAAS;CAAE,MAAM;CAAQ,SAAS;AAAK,CAAC,CAAA,GAAA,cAAA,WAAA,QAAA,KAAA,CAAA;AAExC,WAAA,CAAA,MAAM,+BAA+B,CAAA,GAAA,cAAA,WAAA,mBAAA,KAAA,CAAA;AAhCvC,gBAAA,WAAA,CAAA,cAAc,iBAAiB,CAAA,GAAA,aAAA"}
1
+ {"version":3,"file":"index.js","names":["glow","style"],"sources":["../../../src/components/collapse/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\n\nimport glow from '@/classes/glow.css?inline'\nimport { UserChangeController } from '@/shared/events/user-change'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { getTransitionDuration } from '@/shared/overlay/presence'\n\nimport style from './style.css?inline'\n\nconst EXIT_FALLBACK_BUFFER = 80\n\n/** peek 边缘渐隐的活动长度:关闭/收起态取由 peek 推导的长度,展开态归 0。 */\nconst PEEK_EDGE_ACTIVE = '--wui-collapse-peek-edge-active'\n\n/*\n * 注册成 `<length>` 才能被 transition 插值(未注册的自定义属性是离散值,只能瞬变)。\n *\n * 必须用 JS 的 `CSS.registerProperty` 而不是 CSS 里的 `@property`:实测后者写在\n * shadow root 的样式表里不产生注册(getComputedStyle 读打开态得到空串而非\n * initial-value),而自定义属性的注册本质上是 document 级的。\n * 注册失败(jsdom、旧浏览器、重复注册)时降级为离散过渡——渐变带长度瞬变,\n * 但位置百分比仍跟随裁剪边缘,行为依旧正确。\n *\n * `inherits` 必须是 true:晕染伪元素(style.css 的 ::after)读同一个值来调制模糊,\n * 继承为 false 时伪元素读到的是 initialValue 0px——mask 的各色标随之塌缩到 100%,\n * 模糊层静默消失且其余渲染一切照旧,只少一层效果。\n */\nfunction registerPeekEdgeActive() {\n // jsdom 与不支持的环境直接跳过;样式里的 var() fallback 保证声明仍然有效。\n if (typeof CSS === 'undefined' || typeof CSS.registerProperty !== 'function') return\n try {\n CSS.registerProperty({\n name: PEEK_EDGE_ACTIVE,\n syntax: '<length>',\n inherits: true,\n initialValue: '0px'\n })\n } catch {\n // 已注册过(HMR、多份副本)或定义冲突:保持现有注册即可。\n }\n}\n\nregisterPeekEdgeActive()\n\n/**\n * 文档流内 collapse panel:trigger 经默认 slot 提供,内容经 `slot=\"content\"` 提供。\n *\n * trigger 语义完全来自 slot 进来的交互元素(原生 button / web-ui-button 等):\n * Enter/Space 激活走该元素的原生 click,不做 role=\"button\" 补齐;组件把\n * `aria-expanded` 回写到首个 assigned element,并在 `disabled` 时把该元素置为\n * `disabled`。wrapper 仅负责点击代理,不可聚焦,不承载 ARIA。\n *\n * 不回写 `aria-controls`:目标 track 在组件 shadow root 内,IDREF 不能跨 shadow\n * root 解析(Chrome 的 AX 树里该属性会被当作悬空引用丢弃),写出去只是死属性。\n * 展开关系由 `aria-expanded` 表达,这也是 AT 实际依赖的那一条。\n *\n * 禁用也走 `disabled` 而不是 `aria-disabled`:后者在 `<web-ui-button>` 上只会落在\n * 宿主(属性不跨 shadow 转发),AT 读不到;`disabled` 是真禁用,原生 button 与\n * web-ui-button 都能生效。\n *\n * 内容区在 shadow 内以 grid `0fr ↔ 1fr` track 过渡驱动高度/宽度动画。\n * 关闭稳态三态(消费者 light DOM 永不移动,hidden 全部命令式管理,\n * 避免渲染绑定在关闭动画起点抢先 display:none):\n * - 默认:content 容器 `hidden`(display:none,脱离渲染与可访问性树);\n * - `keep-mounted`:inner `inert`(保留在 0fr track 内,滚动位置与布局可测量);\n * - `peek`:inner `inert`,track 停在固定长度的裁剪窗口,只露出内容头部;\n * - 动画进行中:inner `inert` + track pointer-events 禁用,结束后进入对应稳态。\n *\n * `peek` 与展开态之间是固定长度与内容自适应高度的过渡,CSS 无法插值;这两个\n * 方向的动画改为读出像素后以显式长度写入 track,落稳态时清除内联值交回 CSS 规则。\n */\n@customElement('web-ui-collapse')\nexport class WebUiCollapse extends LitElement {\n static override styles = [unsafeCSS(glow), unsafeCSS(style)]\n\n /** 展开状态;严格受控,唯一状态源。 */\n @property({ type: Boolean, reflect: true }) open = false\n\n /** 禁用触发:点击忽略、`disabled` 写入 trigger 元素;已展开内容保持现状。 */\n @property({ type: Boolean, reflect: true }) disabled = false\n\n /** 水平方向:true 时沿宽度展开,默认沿高度。 */\n @property({ type: Boolean, reflect: true }) horizontal = false\n\n /** 关闭稳态保持内容 `inert` 而非 `hidden`(保留滚动位置,可测量)。 */\n @property({ type: Boolean, reflect: true, attribute: 'keep-mounted' }) keepMounted = false\n\n /**\n * 关闭稳态露出的尺寸(CSS 长度,如 `120px`、`6rem`),沿动画轴生效:默认是高度,\n * `horizontal` 时为宽度;未设置时保持完全收起。\n *\n * 设置后关闭稳态语义等同 `keep-mounted`(内容留在 track 内、`inert` 阻断交互),\n * 只露出 `peek` 范围内的部分;内容本身不足 `peek` 时按内容实际尺寸收起。\n * 注意 `peek` 是固定长度与内容自适应高度之间的过渡:CSS 无法在两者之间插值,\n * 因此这两个方向的动画由本组件读出内容尺寸后以显式长度驱动(其余路径仍是\n * 零测量的 grid fr 过渡)。\n *\n * 露出区域末端自带一段边缘渐隐(提示下方还有内容),长度由 `peek` 按比例推导,\n * 无需也不接受调用方单独指定——见 style.css 中 `--wui-collapse-peek-edge-*`\n * 系列变量(比例、上限、末端颜色可用 CSS 覆盖,属于主题级调优而非组件 API)。\n */\n @property({ type: String, reflect: true }) peek: string | null = null\n\n @query('.wui-collapse-trigger-wrapper') private _triggerWrapper!: HTMLElement\n\n private readonly _userOpenChange = new UserChangeController()\n\n private _exitTimer?: ReturnType<typeof setTimeout>\n // 动画代际:每次管线启动自增,旧管线的 transitionend/兜底定时器/rAF 全部失效。\n private _generation = 0\n private _lastOpen?: boolean\n private _triggerSnapshot?: {\n trigger: HTMLElement\n expanded: string | null\n disabled: string | null\n }\n\n override connectedCallback() {\n super.connectedCallback()\n // 点击监听与生命周期同寿(宿主移除即整体销毁),无需 disconnected 清理。\n this.addEventListener('click', this._onTriggerClick)\n // 断连可能打断动画管线(瞬态残留:presence='closing'、容器未 hidden)。\n // 首次连接不必处理(初始 render 走 _lastOpen===undefined 落稳态,请求多余\n // 渲染会触发「scheduled an update after update completed」告警);重连时\n // 重置同步状态并强制一轮 update,由首次同步分支重新落稳态、丢弃在途管线。\n if (this.hasUpdated) {\n this._generation++\n this._clearExitTimer()\n this._lastOpen = undefined\n this.requestUpdate()\n }\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._clearExitTimer()\n this._restoreTriggerSnapshot()\n }\n\n protected override updated(changed: Map<string, unknown>) {\n // trigger 状态回写不依赖 open 分支,任何渲染后都保持与宿主状态同步。\n this._syncTriggerState()\n // peek 的稳态长度与动画写入共用 track 内联样式,先同步再落稳态,避免被稳态清除。\n this._syncPeek()\n\n // 首次同步(含 open 为默认值的场景,changed map 不含默认值属性):\n // 直接落到正确稳态,不播动画(popover 初始 open 同款)。\n if (this._lastOpen === undefined) {\n this._lastOpen = this.open\n if (this.open) this._applyOpenSteadyState()\n else this._applyClosedSteadyState()\n return\n }\n\n if ((changed.has('keepMounted') || changed.has('peek')) && !this.open) {\n // 运行态切换 keep-mounted / peek:关闭稳态需在 hidden / inert / 裁剪长度间重新落地。\n this._applyClosedSteadyState()\n }\n if (!changed.has('open') || this.open === this._lastOpen) return\n\n this._lastOpen = this.open\n if (this._userOpenChange.consume()) this._dispatchChange(this.open)\n if (this.open) this._beginExpand()\n else this._beginCollapse()\n }\n\n show() {\n if (this.open) return\n this.open = true\n }\n\n close() {\n if (!this.open) return\n this.open = false\n }\n\n toggle() {\n if (this.open) this.close()\n else this.show()\n }\n\n private _dispatchChange(open: boolean) {\n dispatchOpenChangeEvent(this, open)\n }\n\n /*\n * 点击代理:冒泡路径穿过 trigger wrapper 的 click 一律切换。语义由 slot 内\n * 的交互元素(原生 button / web-ui-button)提供;slot 放纯文本时无激活语义\n * (README 约定)。内容区与嵌套内层 collapse 的 click 不经过本 wrapper,天然隔离。\n */\n private _onTriggerClick = (event: MouseEvent) => {\n if (this.disabled) return\n if (!event.composedPath().includes(this._triggerWrapper)) return\n this._userOpenChange.mark()\n this.toggle()\n }\n\n /*\n * Trigger 元素取首个 assigned element(与 popover `_queryTrigger` 同款惰性查询,\n * 不依赖 slotchange——jsdom 不派发该事件,无法用状态缓存)。slot 为空时无回写目标。\n */\n private _queryTrigger(): HTMLElement | null {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('.wui-collapse-trigger-wrapper slot')\n const el = slot?.assignedElements()[0]\n return el instanceof HTMLElement ? el : null\n }\n\n /*\n * slot 内容增删不触发宿主响应式更新:显式请求一轮渲染,由 updated() 的\n * trigger 状态回写覆盖晚到的 trigger 元素(slotchange 在 jsdom 不派发,无法监听)。\n */\n private readonly _onTriggerSlotChange = () => {\n this.requestUpdate()\n }\n\n private _restoreTriggerSnapshot() {\n const snapshot = this._triggerSnapshot\n if (!snapshot) return\n\n for (const [attribute, value] of Object.entries({\n 'aria-expanded': snapshot.expanded,\n disabled: snapshot.disabled\n })) {\n if (value === null) snapshot.trigger.removeAttribute(attribute)\n else snapshot.trigger.setAttribute(attribute, value)\n }\n this._triggerSnapshot = undefined\n }\n\n /*\n * 状态回写到 trigger 元素本身(wrapper 不可聚焦,AT 读不到):展开态用\n * `aria-expanded`,禁用态用 `disabled`——后者是唯一能穿透 `<web-ui-button>`\n * shadow 边界的表达(属性会被内部原生 button 读到),且与「真禁用」语义一致。\n */\n private _syncTriggerState() {\n const trigger = this._queryTrigger()\n if (this._triggerSnapshot?.trigger !== trigger) {\n this._restoreTriggerSnapshot()\n if (trigger) {\n this._triggerSnapshot = {\n trigger,\n expanded: trigger.getAttribute('aria-expanded'),\n disabled: trigger.getAttribute('disabled')\n }\n }\n }\n if (!trigger) return\n trigger.setAttribute('aria-expanded', String(this.open))\n trigger.toggleAttribute('disabled', this.disabled)\n }\n\n // ===== 内容区动画管线(三态语义见本文件顶部注释) =====\n\n /** 关闭稳态是否露出部分内容;空值与非空判断避免 `peek=\"\"` 被当作有效长度。 */\n private get _peeks(): boolean {\n return typeof this.peek === 'string' && this.peek.trim() !== ''\n }\n\n private _applyOpenSteadyState() {\n this._generation++\n this._clearExitTimer()\n const container = this._contentContainer()\n const track = this._contentTrack()\n if (container) container.hidden = false\n if (track) track.dataset.wuiPresence = 'open'\n this._clearInnerClamp()\n this._setInnerInert(false)\n }\n\n private _applyClosedSteadyState() {\n this._generation++\n this._clearExitTimer()\n const container = this._contentContainer()\n const track = this._contentTrack()\n if (track) delete track.dataset.wuiPresence\n this._clearInnerClamp()\n if (this.keepMounted || this._peeks) {\n if (container) container.hidden = false\n this._setInnerInert(true)\n return\n }\n this._setInnerInert(false)\n if (container) container.hidden = true\n }\n\n private _beginExpand() {\n this._generation++\n this._clearExitTimer()\n const container = this._contentContainer()\n const track = this._contentTrack()\n if (!container || !track) return\n\n const generation = this._generation\n // 中断关闭动画时 inner 已 inert:先恢复可交互,内容随展开重新可见。\n this._setInnerInert(false)\n // 从 display:none 进入时无先前计算样式可过渡:提交 0fr 起点后再翻转。\n container.hidden = false\n\n if (this._peeks) {\n const inner = this._contentInner()\n // 无过渡时长(reduced motion、jsdom)或无 inner 时不做测量,直接落展开稳态。\n if (!inner || getTransitionDuration(inner, 0) === 0) {\n this._applyOpenSteadyState()\n return\n }\n /*\n * peek 的裁剪长度是固定值,展开目标是内容自适应高度,两者无法由 CSS 插值,\n * 动画必须由两个显式长度驱动:起点是当前露出的长度,目标是内容完整尺寸。\n * 起点要在同任务内先写入并提交回流,否则过渡会退化成跳变(实测分帧写入\n * 反而完全不参与插值判定);目标读 inner 的 scroll 尺寸,不写探针样式。\n */\n const from = this._measureInnerSize(inner)\n const to = this._measureContentSize(inner)\n this._setInnerClamp(inner, `${from}px`)\n void this._measureInnerSize(inner)\n track.dataset.wuiPresence = 'open'\n this._setInnerClamp(inner, `${to}px`)\n this._awaitTransition(inner, generation, () => this._settle(true), [this._clampProperty()])\n return\n }\n\n void track.offsetWidth\n\n requestAnimationFrame(() => {\n if (generation !== this._generation || !this.open) return\n track.dataset.wuiPresence = 'open'\n this._awaitTransition(track, generation, () => this._settle(true))\n })\n }\n\n private _beginCollapse() {\n this._generation++\n this._clearExitTimer()\n const track = this._contentTrack()\n if (!track) return\n\n // 收起期间立即阻断交互,防止焦点落入正在消失的区域。\n this._setInnerInert(true)\n const generation = this._generation\n\n if (this._peeks) {\n const inner = this._contentInner()\n // 无过渡时长(reduced motion、jsdom)或无 inner 时不做测量,直接落裁剪稳态。\n if (!inner || getTransitionDuration(inner, 0) === 0) {\n this._applyClosedSteadyState()\n return\n }\n /*\n * 收起目标是 `peek` 长度本身:与内容尺寸的较小值由 CSS 稳态规则解析,\n * 动画只需把当前展开长度过渡到这个长度。起点同样要先写入并提交回流,\n * 顺序与 _beginExpand 一致。\n */\n const from = this._measureInnerSize(inner)\n this._setInnerClamp(inner, `${from}px`)\n void this._measureInnerSize(inner)\n track.dataset.wuiPresence = 'closing'\n this._setInnerClamp(inner, `${this.peek}`)\n this._awaitTransition(inner, generation, () => this._settle(false), [this._clampProperty()])\n return\n }\n\n track.dataset.wuiPresence = 'closing'\n this._awaitTransition(track, generation, () => this._settle(false))\n }\n\n // 关闭稳态:keep-mounted → 内容保留在 0fr track 内并 inert;peek → 保留在裁剪\n // 窗口内并 inert;默认 → 容器 hidden。\n private _settle(open: boolean) {\n this._clearExitTimer()\n const track = this._contentTrack()\n // peek 过渡以显式长度写入 inner:落稳态一律清除,交回 CSS 稳态规则(长度声明)。\n this._clearInnerClamp()\n if (open) {\n this._setInnerInert(false)\n return\n }\n // 落稳态必须清除 closing 瞬态标记:否则 track 永久保留 pointer-events:none\n // 与 exit 过渡配置(keep-mounted 分支同样清除,避免残留)。\n if (track) delete track.dataset.wuiPresence\n if (this.keepMounted || this._peeks) return\n const container = this._contentContainer()\n this._setInnerInert(false)\n if (container) container.hidden = true\n }\n\n // 等待动画属性过渡结束;时长为 0(reduced motion、jsdom)直接稳态,\n // transitionend 不触发时按时长 + 缓冲兜底。默认等待 grid track 过渡;peek 的\n // 过渡落在 inner 的裁剪长度上,由调用方传入对应属性。\n private _awaitTransition(\n animated: HTMLElement,\n generation: number,\n onDone: () => void,\n properties: readonly string[] = ['grid-template-rows', 'grid-template-columns']\n ) {\n const duration = getTransitionDuration(animated, 0)\n if (duration === 0) {\n if (generation === this._generation) onDone()\n return\n }\n\n let settled = false\n const finish = () => {\n animated.removeEventListener('transitionend', onTransitionEnd)\n if (this._exitTimer === timer) this._exitTimer = undefined\n clearTimeout(timer)\n // 过渡可能被属性匹配的后续动画触发(如中断重开):代际不符时静默退场。\n if (settled || generation !== this._generation) return\n settled = true\n onDone()\n }\n const timer = setTimeout(finish, duration + EXIT_FALLBACK_BUFFER)\n this._exitTimer = timer\n\n const onTransitionEnd = (event: TransitionEvent) => {\n if (event.target === animated && properties.includes(event.propertyName)) finish()\n }\n animated.addEventListener('transitionend', onTransitionEnd)\n }\n\n private _clearExitTimer() {\n if (this._exitTimer !== undefined) {\n clearTimeout(this._exitTimer)\n this._exitTimer = undefined\n }\n }\n\n // ===== peek 尺寸读写(裁剪长度落在 inner 上,track 只负责 1fr 与裁剪窗口) =====\n\n /*\n * peek 长度经自定义属性下发给 shadow 样式规则,而不是直接写 inner 的裁剪长度:\n * 关闭稳态由 CSS 声明(长度与内容尺寸的较小值由 CSS 原生解析),动画写入的内联\n * 长度在其上覆盖,落稳态时清除即可回到声明值。\n */\n private _syncPeek() {\n const track = this._contentTrack()\n if (!track) return\n track.toggleAttribute('data-wui-peek', this._peeks)\n if (this._peeks) {\n track.style.setProperty('--wui-collapse-peek', this.peek ?? '')\n } else {\n track.style.removeProperty('--wui-collapse-peek')\n }\n }\n\n private _clampProperty(): 'max-height' | 'max-width' {\n return this.horizontal ? 'max-width' : 'max-height'\n }\n\n private _measureInnerSize(inner: HTMLElement): number {\n return this.horizontal ? inner.offsetWidth : inner.offsetHeight\n }\n\n /*\n * 内容在动画轴上的完整尺寸(px):裁剪只作用在 inner 的 max-height/max-width 上,\n * 被测内容的布局盒子仍是完整尺寸,故读 scroll 尺寸即可,不写任何样式——写样式\n * 会被 grid 过渡机制当作过渡起点读取,必须避开。\n */\n private _measureContentSize(inner: HTMLElement): number {\n return this.horizontal ? inner.scrollWidth : inner.scrollHeight\n }\n\n private _setInnerClamp(inner: HTMLElement, size: string) {\n inner.style.setProperty(this._clampProperty(), size)\n }\n\n private _clearInnerClamp() {\n const inner = this._contentInner()\n if (!inner) return\n inner.style.removeProperty('max-height')\n inner.style.removeProperty('max-width')\n }\n\n private _setInnerInert(inert: boolean) {\n this._contentInner()?.toggleAttribute('inert', inert)\n }\n\n private _contentInner(): HTMLElement | null {\n return this.shadowRoot?.querySelector<HTMLElement>('.wui-collapse-inner') ?? null\n }\n\n private _contentContainer(): HTMLElement | null {\n return this.shadowRoot?.querySelector<HTMLElement>('.wui-collapse-content') ?? null\n }\n\n private _contentTrack(): HTMLElement | null {\n return this.shadowRoot?.querySelector<HTMLElement>('.wui-collapse-track') ?? null\n }\n\n override render() {\n // 三态由 data-wui-presence 驱动(open/closing),缺省即关闭稳态。\n const trackClass = {\n 'wui-collapse-track': true,\n 'is-horizontal': this.horizontal\n }\n\n return html`\n <div class=\"wui-collapse-trigger-wrapper\" @slotchange=${this._onTriggerSlotChange}><slot></slot></div>\n <div class=\"wui-collapse-content\">\n <div class=${classMap(trackClass)}>\n <div class=${classMap({ 'wui-collapse-inner': true, 'wui-glow': this._peeks })}>\n <slot name=\"content\"></slot>\n </div>\n </div>\n </div>\n `\n }\n\n declare readonly $events: {\n 'open-change': CustomEvent<{ open: boolean }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-collapse': WebUiCollapse\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"style.js","names":[],"sources":["../../../src/components/collapse/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n/*\n * :host 仅限 display/contain(ADR-0003)。\n * headless 内核(ADR-0039):不携带视觉样式,trigger 与内容的排版由消费者\n * 提供;这里只保留动画与三态关闭稳态必需的结构规则。\n */\n:host {\n display: block;\n}\n\n/*\n * grid 0fr↔1fr 高度动画(ADR-0038 选型):\n * track 行高在 0fr(收起)与 1fr(展开)间过渡,内容高度自适应、零 JS 测量;\n * inner 的 min-height/min-width 归零是 track 能收缩到 0 的必要条件,\n * overflow hidden 负责裁剪收起中的内容。\n */\n.wui-collapse-track {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows var(--wui-duration-collapse-enter, 200ms)\n var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n\n /*\n * 水平方向:块级 grid 默认撑满可用宽度,1fr 列会解析为剩余空间而非内容宽度;\n * width: max-content 让 1fr 列按内容 max-content 贡献解析(与行向 auto 高度同理),\n * 0fr 收缩依赖 inner 的 min-width: 0。\n */\n &.is-horizontal {\n grid-template-columns: 0fr;\n grid-template-rows: none;\n width: max-content;\n transition: grid-template-columns var(--wui-duration-collapse-enter, 200ms)\n var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n }\n}\n\n.wui-collapse-track[data-wui-presence='open'] {\n grid-template-rows: 1fr;\n\n &.is-horizontal {\n grid-template-columns: 1fr;\n grid-template-rows: none;\n }\n}\n\n.wui-collapse-track[data-wui-presence='closing'] {\n pointer-events: none;\n transition: grid-template-rows var(--wui-duration-collapse-exit, 160ms)\n var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n\n &.is-horizontal {\n transition: grid-template-columns var(--wui-duration-collapse-exit, 160ms)\n var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n }\n}\n\n/*\n * peek 模式下 track 恒为 1fr(高度始终由内容驱动),裁剪长度交给 inner 的\n * max-height/max-width:这样 `peek` 与内容尺寸的较小值由 CSS 原生解析——内容不足\n * `peek` 时不残留空白,也不会被撑到 `peek`——而动画两端都是长度,可被浏览器插值\n * (fit-content 或 minmax 的关态与内容高度之间无法插值,实测会退化成跳变)。\n *\n * 注意不能限定 `:not([data-wui-presence])`:收起动画期间 presence='closing',\n * track 会掉回 0fr 先把内容压成 0 再弹回,动画被毁。\n */\n.wui-collapse-track[data-wui-peek] {\n grid-template-rows: 1fr;\n\n &.is-horizontal {\n grid-template-columns: 1fr;\n grid-template-rows: none;\n }\n}\n\n/*\n * peek 边缘渐隐的「活动长度」(替代直接读推导长度):\n * 关闭/收起态取推导值,展开态归 0(fallback)——渐变带因此跟随开合动画\n * 平滑出现/消失,而不是在某一帧突然出现(旧实现把 mask 限定在关闭稳态,收起动画\n * 结束才显示,表现为明显的延迟)。\n *\n * 插值依赖 `CSS.registerProperty` 把它注册成 `<length>`(见 index.ts):未注册的\n * 自定义属性是离散值、无法过渡,且不注册时「未定义」会让整条 mask-image 声明\n * 在打开态失效回退到 none,因此这里必须给 var() 显式 fallback。\n */\n.wui-collapse-inner {\n overflow: hidden;\n min-height: 0;\n\n /*\n * peek 展开/收起的过渡落在裁剪长度上;track 静止在 1fr,只跟随 inner 的高度变化。\n * 活动长度与裁剪长度同步过渡,渐变带才能跟着开合一起淡入淡出。\n */\n transition:\n max-height var(--wui-duration-collapse-enter, 200ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1)),\n --wui-collapse-peek-edge-active var(--wui-duration-collapse-enter, 200ms)\n var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n}\n\n.is-horizontal .wui-collapse-inner {\n min-width: 0;\n\n /* 覆盖轴向,但保留活动长度的过渡——否则水平模式下渐变带不淡入淡出。 */\n transition-property: max-width, --wui-collapse-peek-edge-active;\n}\n\n.wui-collapse-track[data-wui-presence='closing'] .wui-collapse-inner {\n transition-duration: var(--wui-duration-collapse-exit, 160ms);\n}\n\n/*\n * peek 露出区域的边缘渐隐(mask-image):\n * 沿动画轴末端(默认底边、horizontal 时右边)做 alpha 渐变,让\"被裁掉的部分\"与\n * \"露出的部分\"之间柔和过渡,提示下方还有内容。设置 `peek` 即启用——渐隐长度由\n * `peek` 推导,不是独立入参。\n *\n * 长度推导:`peek * --wui-collapse-peek-edge-ratio`(默认 0.4),并夹在\n * `--wui-collapse-peek-edge-max`(默认 112px)以内,避免大 peek 算出过长的虚化带。\n * 纯 CSS 的 `calc(length * number)` —— 不需要 JS 解析单位,peek 用 rem/em 时\n * 渐隐长度自动跟着字号缩放。需要绝对长度时直接设 `--wui-collapse-peek-edge`\n * 覆盖(它优先于推导值),把比例设成 0 即可关掉渐隐。\n *\n * 渐变是四段「先陡后缓」而非线性:主体全黑延伸到渐变带起点后,前 30% 距离就\n * 掉到 0.6 alpha,65% 处 0.26,末端全透明。线性渐变的 alpha 曲线前 40% 几乎\n * 贴着 1.0,人眼感知到的虚化远短于声明长度——实测同样 50px 的带子,线性只让人\n * 觉得\"末尾一小截发虚\",先陡后缓从带子起点就明显发虚,感知长度接近翻倍。\n * 边缘颜色改由下面共享 .wui-glow 的渐变层承担。\n *\n * 渐变带的位置用百分比而非绝对长度:百分比相对 inner 的**当前渲染高度**,而该高度\n * 在动画期间随 max-height 逐帧变化——mask 字符串本身不需要插值,渲染结果会自动\n * 跟随裁剪边缘移动(实测:mask 字符串恒定,容器高度变化时渐变带跟着走)。\n * 渐变长度由活动长度变量控制,其过渡见上方说明。\n */\n.wui-collapse-track[data-wui-peek] .wui-collapse-inner {\n mask-image: linear-gradient(\n to bottom,\n black 0%,\n black calc(100% - var(--wui-collapse-peek-edge-active, 0px)),\n rgb(0 0 0 / 0.6) calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.7),\n rgb(0 0 0 / 0.26) calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.35),\n transparent 100%\n );\n}\n\n/*\n * peek 末端晕染走共享 .wui-glow(src/classes/glow.css):\n * ::before 铺渐变,::after 叠真实模糊。两层默认位于内容之下,这里改成 z-index: 1\n * —— peek 的末端要盖住并虚化内容,而不是垫在内容背后。\n *\n * 渐变方向从裁剪边缘指向内容:垂直 peek 是 to top,水平 peek 是 to left。\n * 带子长度复用活动长度变量,所以开合动画期间跟着裁剪边缘一起过渡;活动长度为 0\n * (展开态)时渐变的两个色标都塌缩到 0px,整层透明,不会把展开后的内容盖住。\n *\n * --wui-collapse-peek-edge-active 必须以 inherits: true 注册(见 index.ts):\n * 继承为 false 时伪元素读到 initialValue 0px,两层晕染会静默消失。\n */\n.wui-collapse-track[data-wui-peek] .wui-collapse-inner.wui-glow {\n --wui-glow-blur: var(--wui-collapse-peek-edge-blur, 4px);\n --wui-glow-overhang: 0px;\n --wui-glow-gradient: linear-gradient(\n to top,\n var(--wui-collapse-peek-edge-color, var(--wui-color-page, #fff)) 0,\n transparent var(--wui-collapse-peek-edge-active, 0px)\n );\n}\n\n.wui-collapse-track[data-wui-peek] .wui-collapse-inner.wui-glow::before,\n.wui-collapse-track[data-wui-peek] .wui-collapse-inner.wui-glow::after {\n z-index: 1;\n}\n\n.wui-collapse-track[data-wui-peek] .wui-collapse-inner.wui-glow::after {\n mask-image: linear-gradient(\n to bottom,\n transparent calc(100% - var(--wui-collapse-peek-edge-active, 0px)),\n black calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.35),\n transparent 100%\n );\n}\n\n.wui-collapse-track[data-wui-peek].is-horizontal .wui-collapse-inner {\n mask-image: linear-gradient(\n to right,\n black 0%,\n black calc(100% - var(--wui-collapse-peek-edge-active, 0px)),\n rgb(0 0 0 / 0.6) calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.7),\n rgb(0 0 0 / 0.26) calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.35),\n transparent 100%\n );\n}\n\n.wui-collapse-track[data-wui-peek].is-horizontal .wui-collapse-inner.wui-glow {\n --wui-glow-gradient: linear-gradient(\n to left,\n var(--wui-collapse-peek-edge-color, var(--wui-color-page, #fff)) 0,\n transparent var(--wui-collapse-peek-edge-active, 0px)\n );\n}\n\n.wui-collapse-track[data-wui-peek].is-horizontal .wui-collapse-inner.wui-glow::after {\n mask-image: linear-gradient(\n to right,\n transparent calc(100% - var(--wui-collapse-peek-edge-active, 0px)),\n black calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.35),\n transparent 100%\n );\n}\n\n/*\n * 关闭稳态裁剪长度由 CSS 声明;动画期间由内联长度覆盖,落稳态后回到这里。\n * `max-height` 仅适用于垂直模式;horizontal 模式由后续规则裁剪宽度。\n * 轴向裁剪规则按特异性递增排列(`no-descending-specificity`)。\n */\n.wui-collapse-track[data-wui-peek]:not([data-wui-presence], .is-horizontal) .wui-collapse-inner {\n max-height: var(--wui-collapse-peek, 0);\n}\n\n/*\n * 非展开态(关闭稳态 + 收起动画进行中)让活动长度取推导值:\n * 收起动画一开始渐变带就开始淡入,与裁剪长度同步,而不是落稳态才出现。\n * 展开态(presence='open')不命中 → 活动长度回落到 0 → mask 整段不透明,\n * 完全展开的内容不再有底部淡出。\n *\n * 推导写成 `var(--wui-collapse-peek-edge, <推导式>)`:显式设了长度就听调用方的,\n * 没设才按比例算——逃生舱仍然是纯 CSS 变量,不占组件属性名额。\n */\n.wui-collapse-track[data-wui-peek]:not([data-wui-presence='open']) .wui-collapse-inner {\n --wui-collapse-peek-edge-active: var(\n --wui-collapse-peek-edge,\n clamp(\n 0px,\n calc(var(--wui-collapse-peek, 0px) * var(--wui-collapse-peek-edge-ratio, 0.4)),\n var(--wui-collapse-peek-edge-max, 112px)\n )\n );\n}\n\n.wui-collapse-track[data-wui-peek]:not([data-wui-presence]).is-horizontal .wui-collapse-inner {\n max-width: var(--wui-collapse-peek, 0);\n}\n"],"mappings":";AAAA,IAAA,gBAAA"}
1
+ {"version":3,"file":"style.js","names":[],"sources":["../../../src/components/collapse/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n/*\n * :host 仅限 display/contain。\n * headless 内核:不携带视觉样式,trigger 与内容的排版由消费者\n * 提供;这里只保留动画与三态关闭稳态必需的结构规则。\n */\n:host {\n display: block;\n}\n\n/*\n * grid 0fr↔1fr 高度动画:\n * track 行高在 0fr(收起)与 1fr(展开)间过渡,内容高度自适应、零 JS 测量;\n * inner 的 min-height/min-width 归零是 track 能收缩到 0 的必要条件,\n * overflow hidden 负责裁剪收起中的内容。\n */\n.wui-collapse-track {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows var(--wui-duration-collapse-enter, 200ms)\n var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n\n /*\n * 水平方向:块级 grid 默认撑满可用宽度,1fr 列会解析为剩余空间而非内容宽度;\n * width: max-content 让 1fr 列按内容 max-content 贡献解析(与行向 auto 高度同理),\n * 0fr 收缩依赖 inner 的 min-width: 0。\n */\n &.is-horizontal {\n grid-template-columns: 0fr;\n grid-template-rows: none;\n width: max-content;\n transition: grid-template-columns var(--wui-duration-collapse-enter, 200ms)\n var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n }\n}\n\n.wui-collapse-track[data-wui-presence='open'] {\n grid-template-rows: 1fr;\n\n &.is-horizontal {\n grid-template-columns: 1fr;\n grid-template-rows: none;\n }\n}\n\n.wui-collapse-track[data-wui-presence='closing'] {\n pointer-events: none;\n transition: grid-template-rows var(--wui-duration-collapse-exit, 160ms)\n var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n\n &.is-horizontal {\n transition: grid-template-columns var(--wui-duration-collapse-exit, 160ms)\n var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n }\n}\n\n/*\n * peek 模式下 track 恒为 1fr(高度始终由内容驱动),裁剪长度交给 inner 的\n * max-height/max-width:这样 `peek` 与内容尺寸的较小值由 CSS 原生解析——内容不足\n * `peek` 时不残留空白,也不会被撑到 `peek`——而动画两端都是长度,可被浏览器插值\n * (fit-content 或 minmax 的关态与内容高度之间无法插值,实测会退化成跳变)。\n *\n * 注意不能限定 `:not([data-wui-presence])`:收起动画期间 presence='closing',\n * track 会掉回 0fr 先把内容压成 0 再弹回,动画被毁。\n */\n.wui-collapse-track[data-wui-peek] {\n grid-template-rows: 1fr;\n\n &.is-horizontal {\n grid-template-columns: 1fr;\n grid-template-rows: none;\n }\n}\n\n/*\n * peek 边缘渐隐的「活动长度」(替代直接读推导长度):\n * 关闭/收起态取推导值,展开态归 0(fallback)——渐变带因此跟随开合动画\n * 平滑出现/消失,而不是在某一帧突然出现(旧实现把 mask 限定在关闭稳态,收起动画\n * 结束才显示,表现为明显的延迟)。\n *\n * 插值依赖 `CSS.registerProperty` 把它注册成 `<length>`(见 index.ts):未注册的\n * 自定义属性是离散值、无法过渡,且不注册时「未定义」会让整条 mask-image 声明\n * 在打开态失效回退到 none,因此这里必须给 var() 显式 fallback。\n */\n.wui-collapse-inner {\n overflow: hidden;\n min-height: 0;\n\n /*\n * peek 展开/收起的过渡落在裁剪长度上;track 静止在 1fr,只跟随 inner 的高度变化。\n * 活动长度与裁剪长度同步过渡,渐变带才能跟着开合一起淡入淡出。\n */\n transition:\n max-height var(--wui-duration-collapse-enter, 200ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1)),\n --wui-collapse-peek-edge-active var(--wui-duration-collapse-enter, 200ms)\n var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n}\n\n.is-horizontal .wui-collapse-inner {\n min-width: 0;\n\n /* 覆盖轴向,但保留活动长度的过渡——否则水平模式下渐变带不淡入淡出。 */\n transition-property: max-width, --wui-collapse-peek-edge-active;\n}\n\n.wui-collapse-track[data-wui-presence='closing'] .wui-collapse-inner {\n transition-duration: var(--wui-duration-collapse-exit, 160ms);\n}\n\n/*\n * peek 露出区域的边缘渐隐(mask-image):\n * 沿动画轴末端(默认底边、horizontal 时右边)做 alpha 渐变,让\"被裁掉的部分\"与\n * \"露出的部分\"之间柔和过渡,提示下方还有内容。设置 `peek` 即启用——渐隐长度由\n * `peek` 推导,不是独立入参。\n *\n * 长度推导:`peek * --wui-collapse-peek-edge-ratio`(默认 0.4),并夹在\n * `--wui-collapse-peek-edge-max`(默认 112px)以内,避免大 peek 算出过长的虚化带。\n * 纯 CSS 的 `calc(length * number)` —— 不需要 JS 解析单位,peek 用 rem/em 时\n * 渐隐长度自动跟着字号缩放。需要绝对长度时直接设 `--wui-collapse-peek-edge`\n * 覆盖(它优先于推导值),把比例设成 0 即可关掉渐隐。\n *\n * 渐变是四段「先陡后缓」而非线性:主体全黑延伸到渐变带起点后,前 30% 距离就\n * 掉到 0.6 alpha,65% 处 0.26,末端全透明。线性渐变的 alpha 曲线前 40% 几乎\n * 贴着 1.0,人眼感知到的虚化远短于声明长度——实测同样 50px 的带子,线性只让人\n * 觉得\"末尾一小截发虚\",先陡后缓从带子起点就明显发虚,感知长度接近翻倍。\n * 边缘颜色改由下面共享 .wui-glow 的渐变层承担。\n *\n * 渐变带的位置用百分比而非绝对长度:百分比相对 inner 的**当前渲染高度**,而该高度\n * 在动画期间随 max-height 逐帧变化——mask 字符串本身不需要插值,渲染结果会自动\n * 跟随裁剪边缘移动(实测:mask 字符串恒定,容器高度变化时渐变带跟着走)。\n * 渐变长度由活动长度变量控制,其过渡见上方说明。\n */\n.wui-collapse-track[data-wui-peek] .wui-collapse-inner {\n mask-image: linear-gradient(\n to bottom,\n black 0%,\n black calc(100% - var(--wui-collapse-peek-edge-active, 0px)),\n rgb(0 0 0 / 0.6) calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.7),\n rgb(0 0 0 / 0.26) calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.35),\n transparent 100%\n );\n}\n\n/*\n * peek 末端晕染走共享 .wui-glow(src/classes/glow.css):\n * ::before 铺渐变,::after 叠真实模糊。两层默认位于内容之下,这里改成 z-index: 1\n * —— peek 的末端要盖住并虚化内容,而不是垫在内容背后。\n *\n * 渐变方向从裁剪边缘指向内容:垂直 peek 是 to top,水平 peek 是 to left。\n * 带子长度复用活动长度变量,所以开合动画期间跟着裁剪边缘一起过渡;活动长度为 0\n * (展开态)时渐变的两个色标都塌缩到 0px,整层透明,不会把展开后的内容盖住。\n *\n * --wui-collapse-peek-edge-active 必须以 inherits: true 注册(见 index.ts):\n * 继承为 false 时伪元素读到 initialValue 0px,两层晕染会静默消失。\n */\n.wui-collapse-track[data-wui-peek] .wui-collapse-inner.wui-glow {\n --wui-glow-blur: var(--wui-collapse-peek-edge-blur, 4px);\n --wui-glow-overhang: 0px;\n --wui-glow-gradient: linear-gradient(\n to top,\n var(--wui-collapse-peek-edge-color, var(--wui-color-page, #fff)) 0,\n transparent var(--wui-collapse-peek-edge-active, 0px)\n );\n}\n\n.wui-collapse-track[data-wui-peek] .wui-collapse-inner.wui-glow::before,\n.wui-collapse-track[data-wui-peek] .wui-collapse-inner.wui-glow::after {\n z-index: 1;\n}\n\n.wui-collapse-track[data-wui-peek] .wui-collapse-inner.wui-glow::after {\n mask-image: linear-gradient(\n to bottom,\n transparent calc(100% - var(--wui-collapse-peek-edge-active, 0px)),\n black calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.35),\n transparent 100%\n );\n}\n\n.wui-collapse-track[data-wui-peek].is-horizontal .wui-collapse-inner {\n mask-image: linear-gradient(\n to right,\n black 0%,\n black calc(100% - var(--wui-collapse-peek-edge-active, 0px)),\n rgb(0 0 0 / 0.6) calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.7),\n rgb(0 0 0 / 0.26) calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.35),\n transparent 100%\n );\n}\n\n.wui-collapse-track[data-wui-peek].is-horizontal .wui-collapse-inner.wui-glow {\n --wui-glow-gradient: linear-gradient(\n to left,\n var(--wui-collapse-peek-edge-color, var(--wui-color-page, #fff)) 0,\n transparent var(--wui-collapse-peek-edge-active, 0px)\n );\n}\n\n.wui-collapse-track[data-wui-peek].is-horizontal .wui-collapse-inner.wui-glow::after {\n mask-image: linear-gradient(\n to right,\n transparent calc(100% - var(--wui-collapse-peek-edge-active, 0px)),\n black calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.35),\n transparent 100%\n );\n}\n\n/*\n * 关闭稳态裁剪长度由 CSS 声明;动画期间由内联长度覆盖,落稳态后回到这里。\n * `max-height` 仅适用于垂直模式;horizontal 模式由后续规则裁剪宽度。\n * 轴向裁剪规则按特异性递增排列(`no-descending-specificity`)。\n */\n.wui-collapse-track[data-wui-peek]:not([data-wui-presence], .is-horizontal) .wui-collapse-inner {\n max-height: var(--wui-collapse-peek, 0);\n}\n\n/*\n * 非展开态(关闭稳态 + 收起动画进行中)让活动长度取推导值:\n * 收起动画一开始渐变带就开始淡入,与裁剪长度同步,而不是落稳态才出现。\n * 展开态(presence='open')不命中 → 活动长度回落到 0 → mask 整段不透明,\n * 完全展开的内容不再有底部淡出。\n *\n * 推导写成 `var(--wui-collapse-peek-edge, <推导式>)`:显式设了长度就听调用方的,\n * 没设才按比例算——逃生舱仍然是纯 CSS 变量,不占组件属性名额。\n */\n.wui-collapse-track[data-wui-peek]:not([data-wui-presence='open']) .wui-collapse-inner {\n --wui-collapse-peek-edge-active: var(\n --wui-collapse-peek-edge,\n clamp(\n 0px,\n calc(var(--wui-collapse-peek, 0px) * var(--wui-collapse-peek-edge-ratio, 0.4)),\n var(--wui-collapse-peek-edge-max, 112px)\n )\n );\n}\n\n.wui-collapse-track[data-wui-peek]:not([data-wui-presence]).is-horizontal .wui-collapse-inner {\n max-width: var(--wui-collapse-peek, 0);\n}\n"],"mappings":";AAAA,IAAA,gBAAA"}
@@ -1,11 +1,15 @@
1
1
  import "../icon/index.js";
2
2
  import "../button/index.js";
3
3
  import { LitElement, PropertyValues } from "lit";
4
+ export type DialogFooterOrientation = 'vertical' | 'horizontal';
4
5
  export declare class WebUiDialog extends LitElement {
5
6
  static styles: import("lit").CSSResult[];
6
7
  open: boolean;
7
8
  closable: boolean;
8
- horizontal: boolean;
9
+ /** footer 操作区的排列方向。默认纵向;横向时由子控件自己的布局能力决定宽度。 */
10
+ get footerOrientation(): DialogFooterOrientation;
11
+ set footerOrientation(value: string);
12
+ private _footerOrientation;
9
13
  noScrollLock: boolean;
10
14
  noBackdropClose: boolean;
11
15
  noEscapeClose: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/components/dialog/index.ts"],"mappings":";;;qBAqBa,oBAAoB;SACf,sBAAM;EAEsB;EACA;EACA;EAC6B;EACG;EACF;;;;;;EAO9B;;;;;;EAOhB;;;;;;;;;EAUgB;;;;;EAMgB;;MASxD;MAGA,YAAY;;EAK2C;UAE1C;mBACA;mBAOA;;UAKT;mBACS;mBACA;cAML;YAIO,QAAQ,OAAO;EAuCzB;EAWA;UAaD;UAaA;EAIR;EAKA;UAcQ;UAQA;UAMA;;mBAWS;UAUT;UAIA;UAIA;UAyBA;;;;;UAQA;cAMI;;;;;;UASJ;EAeC,wBAAM;WA6CE;IACf,eAAe;MAAc;;;;QAIzB;YACI;IACR,iBAAiB"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/components/dialog/index.ts"],"mappings":";;;YAqBY;qBAKC,oBAAoB;SACf,sBAAM;EAEsB;EACA;;MAIxC,qBAAqB;MAGrB,kBAAkB;UAKd;EAEiE;EACG;EACF;;;;;;EAO9B;;;;;;EAOhB;;;;;;;;;EAUgB;;;;;EAMgB;;MASxD;MAGA,YAAY;;EAK2C;UAE1C;mBACA;mBAOA;;UAKT;mBACS;mBACA;cAML;YAIO,QAAQ,OAAO;EAuCzB;EAWA;UAaD;UAaA;EAIR;EAKA;UAcQ;UAQA;UAMA;;mBAWS;UAUT;UAIA;UAIA;UAyBA;;;;;UAQA;cAMI;;;;;;UASJ;EAeC,wBAAM;WA6CE;IACf,eAAe;MAAc;;;;QAIzB;YACI;IACR,iBAAiB"}
@@ -2,6 +2,7 @@ import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm
2
2
  import "../icon/index.js";
3
3
  import glass_default from "../../classes/glass.js";
4
4
  import ooui_close_default from "../../icons/generated/ooui-close.js";
5
+ import { normalizeLiteral } from "../../shared/normalize/index.js";
5
6
  import "../button/index.js";
6
7
  import { resolveBuiltinLabel } from "../../shared/locale/builtin-labels.js";
7
8
  import scrollbar_default from "../../assets/scrollbar.js";
@@ -16,12 +17,13 @@ import style_default from "./style.js";
16
17
  import { LitElement, html, nothing, unsafeCSS } from "lit";
17
18
  import { customElement, property, state } from "lit/decorators.js";
18
19
  // src/components/dialog/index.ts
20
+ const ALLOWED_FOOTER_ORIENTATIONS = ["vertical", "horizontal"];
19
21
  let WebUiDialog = class WebUiDialog extends LitElement {
20
22
  constructor(..._args) {
21
23
  super(..._args);
22
24
  this.open = false;
23
25
  this.closable = false;
24
- this.horizontal = false;
26
+ this._footerOrientation = "vertical";
25
27
  this.noScrollLock = false;
26
28
  this.noBackdropClose = false;
27
29
  this.noEscapeClose = false;
@@ -77,6 +79,15 @@ let WebUiDialog = class WebUiDialog extends LitElement {
77
79
  unsafeCSS(style_default)
78
80
  ];
79
81
  }
82
+ /** footer 操作区的排列方向。默认纵向;横向时由子控件自己的布局能力决定宽度。 */
83
+ get footerOrientation() {
84
+ return this._footerOrientation;
85
+ }
86
+ set footerOrientation(value) {
87
+ const previous = this._footerOrientation;
88
+ this._footerOrientation = normalizeLiteral(value, ALLOWED_FOOTER_ORIENTATIONS, "vertical");
89
+ this.requestUpdate("footerOrientation", previous);
90
+ }
80
91
  /** @deprecated 用 `aria-dialog-label`。 */
81
92
  get dialogLabel() {
82
93
  return this.ariaDialogLabel;
@@ -238,9 +249,10 @@ __decorate([property({
238
249
  reflect: true
239
250
  })], WebUiDialog.prototype, "closable", void 0);
240
251
  __decorate([property({
241
- type: Boolean,
242
- reflect: true
243
- })], WebUiDialog.prototype, "horizontal", void 0);
252
+ type: String,
253
+ reflect: true,
254
+ attribute: "footer-orientation"
255
+ })], WebUiDialog.prototype, "footerOrientation", null);
244
256
  __decorate([property({
245
257
  type: Boolean,
246
258
  reflect: true,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["glass","modalSurface","scrollbarStyle","style","oouiClose"],"sources":["../../../src/components/dialog/index.ts"],"sourcesContent":["import '@/components/button'\nimport '@/components/icon'\n\nimport { html, LitElement, nothing, type PropertyValues, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport modalSurface from '@/assets/modal-surface.css?inline'\nimport scrollbarStyle from '@/assets/scrollbar.css?inline'\nimport glass from '@/classes/glass.css?inline'\nimport { oouiClose } from '@/icons'\nimport { UserChangeController } from '@/shared/events/user-change'\nimport { trapTabKey } from '@/shared/focus/tabbable'\nimport { resolveBuiltinLabel } from '@/shared/locale/builtin-labels'\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(scrollbarStyle), 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 }) horizontal = 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 和 backdrop 点击只派发 `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 * 用 `aria-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: 'aria-dialog-label' }) ariaDialogLabel = ''\n\n /*\n * `dialog-label` 自 11.1.0 发布,为已上线消费方保留兼容别名:attribute 仍可写,\n * 内部转发到 ariaDialogLabel。canonical 名是 aria-dialog-label;两个 attribute 同时\n * 设置时后处理者生效,文档不推荐这样做。下个 major 移除本别名。\n */\n /** @deprecated 用 `aria-dialog-label`。 */\n @property({ type: String, attribute: 'dialog-label' })\n get dialogLabel(): string {\n return this.ariaDialogLabel\n }\n set dialogLabel(value: string) {\n this.ariaDialogLabel = value\n }\n\n /** 关闭按钮的无障碍名称;未设置时回退内置英文文案(见 ADR-0016)。 */\n @property({ type: String, attribute: 'aria-close-label' }) ariaCloseLabel = ''\n\n @state() private _hasHeader = false\n private readonly _userOpenChange = new UserChangeController()\n\n /*\n * 开启态 overlay(issue #120 Block 1)。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 /** 当前开启会话的 handle;未开启时为 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 // backdrop 后面本来也已不可用。这里只清文档选区,不动 user-select,overlay 内正文\n // 仍可正常拖选。\n window.getSelection()?.removeAllRanges()\n }\n // 原生 dialog 登记为开启态 overlay:挂在它上面的 portal panel 成为本层后代,\n // Escape 仲裁据此判出最内层(issue #120 Block 1)。\n const dialog = this.dialog\n if (dialog) {\n if (this.open) {\n // 同一 panel 重新 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 * 不走关闭入口:若改成跳过候选,事件会落到下层 overlay,把外层一起关掉。该属性可在开启\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 不关闭 dialog」与 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 private handleKeydown(e: KeyboardEvent) {\n if (e.key !== 'Tab') return\n // 嵌套 drawer 的 Tab 沿 slot 宿主链冒泡到这里;边界回卷由最内层负责。\n const path = e.composedPath()\n const ownIndex = path.indexOf(e.currentTarget as Node)\n if (ownIndex > 0 && path.slice(0, ownIndex).some(node => node instanceof HTMLDialogElement)) return\n const dialog = this.dialog\n if (dialog) trapTabKey(e, dialog)\n }\n\n /** 用户发起的关闭入口(Escape、backdrop 点击、关闭 button)共用一条路径。 */\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` 模式共用同一枚关闭 button:都绝对定位到卡片右上角,偏移由\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=${resolveBuiltinLabel('close', this.ariaCloseLabel)}\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:显式 `aria-dialog-label` 优先;否则(非 headless 且带可见时)把原生 dialog\n * 的 `aria-labelledby` 指向 chrome 带——与 drawer 同形。headless 模式没有可自动关联的\n * 内置标题,名字留给 Consumer。\n */\n const dialogLabel = this.ariaDialogLabel.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 @keydown=${this.handleKeydown}\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 wui-scroll\">\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 wui-scroll\"><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
+ {"version":3,"file":"index.js","names":["glass","modalSurface","scrollbarStyle","style","oouiClose"],"sources":["../../../src/components/dialog/index.ts"],"sourcesContent":["import '@/components/button'\nimport '@/components/icon'\n\nimport { html, LitElement, nothing, type PropertyValues, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport modalSurface from '@/assets/modal-surface.css?inline'\nimport scrollbarStyle from '@/assets/scrollbar.css?inline'\nimport glass from '@/classes/glass.css?inline'\nimport { oouiClose } from '@/icons'\nimport { UserChangeController } from '@/shared/events/user-change'\nimport { trapTabKey } from '@/shared/focus/tabbable'\nimport { resolveBuiltinLabel } from '@/shared/locale/builtin-labels'\nimport { normalizeLiteral } from '@/shared/normalize'\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\nexport type DialogFooterOrientation = 'vertical' | 'horizontal'\n\nconst ALLOWED_FOOTER_ORIENTATIONS = ['vertical', 'horizontal'] as const\n\n@customElement('web-ui-dialog')\nexport class WebUiDialog extends LitElement {\n static override styles = [unsafeCSS(glass), unsafeCSS(modalSurface), unsafeCSS(scrollbarStyle), unsafeCSS(style)]\n\n @property({ type: Boolean, reflect: true }) open = false\n @property({ type: Boolean, reflect: true }) closable = false\n\n /** footer 操作区的排列方向。默认纵向;横向时由子控件自己的布局能力决定宽度。 */\n @property({ type: String, reflect: true, attribute: 'footer-orientation' })\n get footerOrientation(): DialogFooterOrientation {\n return this._footerOrientation\n }\n set footerOrientation(value: string) {\n const previous = this._footerOrientation\n this._footerOrientation = normalizeLiteral(value, ALLOWED_FOOTER_ORIENTATIONS, 'vertical')\n this.requestUpdate('footerOrientation', previous)\n }\n private _footerOrientation: DialogFooterOrientation = 'vertical'\n\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 和 backdrop 点击只派发 `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 * 用 `aria-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: 'aria-dialog-label' }) ariaDialogLabel = ''\n\n /*\n * `dialog-label` 自 11.1.0 发布,为已上线消费方保留兼容别名:attribute 仍可写,\n * 内部转发到 ariaDialogLabel。canonical 名是 aria-dialog-label;两个 attribute 同时\n * 设置时后处理者生效,文档不推荐这样做。下个 major 移除本别名。\n */\n /** @deprecated 用 `aria-dialog-label`。 */\n @property({ type: String, attribute: 'dialog-label' })\n get dialogLabel(): string {\n return this.ariaDialogLabel\n }\n set dialogLabel(value: string) {\n this.ariaDialogLabel = value\n }\n\n /** 关闭按钮的无障碍名称;未设置时回退内置英文文案(见 ADR-0016)。 */\n @property({ type: String, attribute: 'aria-close-label' }) ariaCloseLabel = ''\n\n @state() private _hasHeader = false\n private readonly _userOpenChange = new UserChangeController()\n\n /*\n * 开启态 overlay(issue #120 Block 1)。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 /** 当前开启会话的 handle;未开启时为 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 // backdrop 后面本来也已不可用。这里只清文档选区,不动 user-select,overlay 内正文\n // 仍可正常拖选。\n window.getSelection()?.removeAllRanges()\n }\n // 原生 dialog 登记为开启态 overlay:挂在它上面的 portal panel 成为本层后代,\n // Escape 仲裁据此判出最内层(issue #120 Block 1)。\n const dialog = this.dialog\n if (dialog) {\n if (this.open) {\n // 同一 panel 重新 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 * 不走关闭入口:若改成跳过候选,事件会落到下层 overlay,把外层一起关掉。该属性可在开启\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 不关闭 dialog」与 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 private handleKeydown(e: KeyboardEvent) {\n if (e.key !== 'Tab') return\n // 嵌套 drawer 的 Tab 沿 slot 宿主链冒泡到这里;边界回卷由最内层负责。\n const path = e.composedPath()\n const ownIndex = path.indexOf(e.currentTarget as Node)\n if (ownIndex > 0 && path.slice(0, ownIndex).some(node => node instanceof HTMLDialogElement)) return\n const dialog = this.dialog\n if (dialog) trapTabKey(e, dialog)\n }\n\n /** 用户发起的关闭入口(Escape、backdrop 点击、关闭 button)共用一条路径。 */\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` 模式共用同一枚关闭 button:都绝对定位到卡片右上角,偏移由\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=${resolveBuiltinLabel('close', this.ariaCloseLabel)}\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:显式 `aria-dialog-label` 优先;否则(非 headless 且带可见时)把原生 dialog\n * 的 `aria-labelledby` 指向 chrome 带——与 drawer 同形。headless 模式没有可自动关联的\n * 内置标题,名字留给 Consumer。\n */\n const dialogLabel = this.ariaDialogLabel.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 @keydown=${this.handleKeydown}\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 wui-scroll\">\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 wui-scroll\"><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
  // src/components/dialog/style.css?inline
2
- var style_default = "*,: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[open]: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(var(--wui-glass-saturation,160%)) brightness(var(--wui-glass-brightness,1.06));backdrop-filter:blur(0) saturate(var(--wui-glass-saturation,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(var(--wui-glass-blur,4px)) saturate(var(--wui-glass-saturation,160%)) brightness(var(--wui-glass-brightness,1.06));backdrop-filter:blur(var(--wui-glass-blur,4px)) saturate(var(--wui-glass-saturation,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}\n";
2
+ var style_default = "*,: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[open]: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(var(--wui-glass-saturation,160%)) brightness(var(--wui-glass-brightness,1.06));backdrop-filter:blur(0) saturate(var(--wui-glass-saturation,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(var(--wui-glass-blur,4px)) saturate(var(--wui-glass-saturation,160%)) brightness(var(--wui-glass-brightness,1.06));backdrop-filter:blur(var(--wui-glass-blur,4px)) saturate(var(--wui-glass-saturation,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);align-items:stretch;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}:host([footer-orientation=horizontal]) .wui-dialog-footer{gap:var(--wui-space-3,12px);align-items:center;justify-content:var(--wui-dialog-footer-justify,center);flex-direction:row}\n";
3
3
  export { style_default as default };
4
4
 
5
5
  //# sourceMappingURL=style.js.map
@@ -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 里的 modal dialog 变成按文档流\n 定位:渲染在页面顶部(用户看到的“残影”)且跟随页面滚动(“没有固定居中”)。\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。方向与浮动 panel 的 --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 会让 backdrop 在卡片落位后才走完大半透明度。 */\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 高度:chrome 里的 footer 一项就是 --wui-control-size——\n 触摸端 `@media (pointer: coarse)` 把它从 36px 抬到 40px,chrome 随之长高,宿主写死的\n 常数立刻腐坏。\n\n header / footer 完全由内容撑开,上限只落在 .desc:内容超高时滚动发生在内容区内部,\n 而不是把卡片顶出 dialog 盒子。宿主不必为「让它滚」而算。\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` 的整卡兜底是字面量 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 规则上的注释。 */\n\n/* 只在打开态覆盖原生 dialog 的 display,关闭后保留 UA 的 display:none。 */\ndialog[open]:has(.desc) {\n display: flex;\n flex-direction: column;\n}\n\n/* 模态表面的焦点环抑制(<dialog> 壳 + panel 滚动区)集中在 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 这个滚动容器的边因此与卡片内沿重合,scrollbar 贴边(详见 .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(var(--wui-glass-saturation, 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(阴影与关闭 button 不能由同尺寸的原生 dialog 裁),溢出的那截落在视口之下,\n 既看不到也够不着。\n\n 上限取与整卡兜底相同的字面量,而不是 `--wui-dialog-max-height`:后者的公开语义是\n `.desc` 的高度(README 有记录),headless 模式根本没有 `.desc`,套上去会凭空改掉\n 这档的既有尺寸语义。\n\n 滚动单独成层而不是让卡片自己滚:关闭 button 是卡片的绝对定位直接子元素,卡片一旦自己\n 滚动,button 会跟着内容一起滚走。padding 也跟着挪进内层,让内容与滚动容器的裁剪边\n (padding 外沿)留出余量——贴边的控件焦点环才不会被裁成残条;内层没有 scrollbar 槽的\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 模式的关闭 button 浮在卡片右上角,需要一个定位参照,所以显式重述一次\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 都不依赖 closable。\n */\n position: relative;\n}\n\ndialog[open].is-visible .wui-dialog-body {\n opacity: 1;\n backdrop-filter: blur(var(--wui-glass-blur, 4px)) saturate(var(--wui-glass-saturation, 160%))\n brightness(var(--wui-glass-brightness, 1.06));\n}\n\n/* ===== 关闭 button(绝对定位,两种内容模式同一套规则)=====\n\n 偏移由公开 token --wui-dialog-close-top / --wui-dialog-close-right 控制,默认都是字面量\n 20px。默认值不经过 --wui-space-*:这两个 token 的参照系是卡片边缘,而不是相邻元素\n 之间的间距节奏,消费方调整主题间距时不应意外移动关闭 button。dialog 的关闭偏移是 20px,\n 与 drawer 的 16/16 不同;--wui-drawer-close-* 保持不变。\n\n 两个公开 token 先在正文容器上解析成同一份内部变量,button 定位和标题避让都读这份结果,\n 避免只改一个兜底字面量后 button 与标题各自按不同默认值计算。\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/* 标题避让:关闭 button 绝对定位、不占 chrome 带的位,长标题会一直排到 button 下面。\n 预留量取「button 右偏移 + button 宽 + 呼吸间距」,而不是只减掉带自身的 padding——这样即便\n 消费者把 --wui-dialog-inline-padding 调到 0,内容右缘也不会越过 button 左缘;跟随\n --wui-dialog-close-right 变化;这里复用正文容器上已解析的内部右侧偏移,与 button 定位同源。\n 26px 对应 index.ts 里 button 的 size=\"26\",改 button 尺寸时一并改这里。\n 只在 closable 时生效:没有 button 就不必留白。 */\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,宿主内容横向溢出时就会多出一条不该出现的横向 scrollbar。\n Chromium / WebKit 实测:clip 与本规则下面的 auto 配对时计算值会降级成 hidden\n (仍可被脚本滚动)。Firefox ≥128 不做这个降级、保持 clip,但仓库的 browser-mode\n 项目只跑 chromium,这句未经本仓 CI 证实。两个引擎都不画横向 scrollbar,所以这里\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 不受这 6px 影响。\n\n inline 轴不走 focus 余量:卡片的 inline padding 归零,由本容器的 padding-inline 取\n --wui-dialog-inline-padding 承担(见 .wui-dialog-body),\n 滚动口因此与卡片内沿重合(scrollbar 贴边,与 headless 模式 / drawer 一致)。24px 的 inline\n padding 远大于 5px ring,左右焦点环不会被裁;也无需配负 margin——content box 的\n 可用宽度与卡片自带 inline padding 时相同(同一档 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),未覆写时内容区上限就是 90vh / 90dvh 这一档:\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":";AAAA,IAAA,gBAAA"}
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 里的 modal dialog 变成按文档流\n 定位:渲染在页面顶部(用户看到的“残影”)且跟随页面滚动(“没有固定居中”)。\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。方向与浮动 panel 的 --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 会让 backdrop 在卡片落位后才走完大半透明度。 */\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 高度:chrome 里的 footer 一项就是 --wui-control-size——\n 触摸端 `@media (pointer: coarse)` 把它从 36px 抬到 40px,chrome 随之长高,宿主写死的\n 常数立刻腐坏。\n\n header / footer 完全由内容撑开,上限只落在 .desc:内容超高时滚动发生在内容区内部,\n 而不是把卡片顶出 dialog 盒子。宿主不必为「让它滚」而算。\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` 的整卡兜底是字面量 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 规则上的注释。 */\n\n/* 只在打开态覆盖原生 dialog 的 display,关闭后保留 UA 的 display:none。 */\ndialog[open]:has(.desc) {\n display: flex;\n flex-direction: column;\n}\n\n/* 模态表面的焦点环抑制(<dialog> 壳 + panel 滚动区)集中在 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 这个滚动容器的边因此与卡片内沿重合,scrollbar 贴边(详见 .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(var(--wui-glass-saturation, 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(阴影与关闭 button 不能由同尺寸的原生 dialog 裁),溢出的那截落在视口之下,\n 既看不到也够不着。\n\n 上限取与整卡兜底相同的字面量,而不是 `--wui-dialog-max-height`:后者的公开语义是\n `.desc` 的高度(README 有记录),headless 模式根本没有 `.desc`,套上去会凭空改掉\n 这档的既有尺寸语义。\n\n 滚动单独成层而不是让卡片自己滚:关闭 button 是卡片的绝对定位直接子元素,卡片一旦自己\n 滚动,button 会跟着内容一起滚走。padding 也跟着挪进内层,让内容与滚动容器的裁剪边\n (padding 外沿)留出余量——贴边的控件焦点环才不会被裁成残条;内层没有 scrollbar 槽的\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 模式的关闭 button 浮在卡片右上角,需要一个定位参照,所以显式重述一次\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 都不依赖 closable。\n */\n position: relative;\n}\n\ndialog[open].is-visible .wui-dialog-body {\n opacity: 1;\n backdrop-filter: blur(var(--wui-glass-blur, 4px)) saturate(var(--wui-glass-saturation, 160%))\n brightness(var(--wui-glass-brightness, 1.06));\n}\n\n/* ===== 关闭 button(绝对定位,两种内容模式同一套规则)=====\n\n 偏移由公开 token --wui-dialog-close-top / --wui-dialog-close-right 控制,默认都是字面量\n 20px。默认值不经过 --wui-space-*:这两个 token 的参照系是卡片边缘,而不是相邻元素\n 之间的间距节奏,消费方调整主题间距时不应意外移动关闭 button。dialog 的关闭偏移是 20px,\n 与 drawer 的 16/16 不同;--wui-drawer-close-* 保持不变。\n\n 两个公开 token 先在正文容器上解析成同一份内部变量,button 定位和标题避让都读这份结果,\n 避免只改一个兜底字面量后 button 与标题各自按不同默认值计算。\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/* 标题避让:关闭 button 绝对定位、不占 chrome 带的位,长标题会一直排到 button 下面。\n 预留量取「button 右偏移 + button 宽 + 呼吸间距」,而不是只减掉带自身的 padding——这样即便\n 消费者把 --wui-dialog-inline-padding 调到 0,内容右缘也不会越过 button 左缘;跟随\n --wui-dialog-close-right 变化;这里复用正文容器上已解析的内部右侧偏移,与 button 定位同源。\n 26px 对应 index.ts 里 button 的 size=\"26\",改 button 尺寸时一并改这里。\n 只在 closable 时生效:没有 button 就不必留白。 */\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,宿主内容横向溢出时就会多出一条不该出现的横向 scrollbar。\n Chromium / WebKit 实测:clip 与本规则下面的 auto 配对时计算值会降级成 hidden\n (仍可被脚本滚动)。Firefox ≥128 不做这个降级、保持 clip,但仓库的 browser-mode\n 项目只跑 chromium,这句未经本仓 CI 证实。两个引擎都不画横向 scrollbar,所以这里\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 不受这 6px 影响。\n\n inline 轴不走 focus 余量:卡片的 inline padding 归零,由本容器的 padding-inline 取\n --wui-dialog-inline-padding 承担(见 .wui-dialog-body),\n 滚动口因此与卡片内沿重合(scrollbar 贴边,与 headless 模式 / drawer 一致)。24px 的 inline\n padding 远大于 5px ring,左右焦点环不会被裁;也无需配负 margin——content box 的\n 可用宽度与卡片自带 inline padding 时相同(同一档 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),未覆写时内容区上限就是 90vh / 90dvh 这一档:\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/* footer 是纯布局容器:只负责方向、间距、对齐和内边距。不要在这里用 ::slotted()\n 覆盖 slotted 控件的尺寸或外观;满宽等视觉状态由子组件自己的属性(例如 button 的\n `full`)表达,避免 Dialog 越过组件边界替消费者做样式决定。 */\n.wui-dialog-footer {\n display: flex;\n flex-direction: column;\n gap: var(--wui-dialog-footer-gap, 10px);\n align-items: stretch;\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\n:host([footer-orientation='horizontal']) .wui-dialog-footer {\n flex-direction: row;\n gap: var(--wui-space-3, 12px);\n align-items: center;\n justify-content: var(--wui-dialog-footer-justify, center);\n}\n"],"mappings":";AAAA,IAAA,gBAAA"}