@greypan/web-ui 12.0.0 → 12.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/collapse/index.d.ts +2 -2
- package/dist/components/collapse/index.js.map +1 -1
- package/dist/components/collapse/style.js.map +1 -1
- package/dist/components/drawer/index.js.map +1 -1
- package/dist/components/drawer/style.js.map +1 -1
- package/dist/shared/group-management/index.js.map +1 -1
- package/dist/shared/open-state/index.js.map +1 -1
- package/dist/shared/overlay/overlay.js.map +1 -1
- package/package.json +1 -1
|
@@ -15,8 +15,8 @@ import { LitElement } from "lit";
|
|
|
15
15
|
* 宿主(属性不跨 shadow 转发),AT 读不到;`disabled` 是真禁用,原生 button 与
|
|
16
16
|
* web-ui-button 都能生效。
|
|
17
17
|
*
|
|
18
|
-
* 内容区在 shadow 内以 grid `0fr ↔ 1fr` track
|
|
19
|
-
*
|
|
18
|
+
* 内容区在 shadow 内以 grid `0fr ↔ 1fr` track 过渡驱动高度/宽度动画。
|
|
19
|
+
* 关闭稳态三态(消费者 light DOM 永不移动,hidden 全部命令式管理,
|
|
20
20
|
* 避免渲染绑定在关闭动画起点抢先 display:none):
|
|
21
21
|
* - 默认:content 容器 `hidden`(display:none,脱离渲染与可访问性树);
|
|
22
22
|
* - `keep-mounted`:inner `inert`(保留在 0fr track 内,滚动位置与布局可测量);
|
|
@@ -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":"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"}
|
|
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
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["glass","modalSurface","scrollbarStyle","style","oouiClose"],"sources":["../../../src/components/drawer/index.ts"],"sourcesContent":["import '@/components/button'\nimport '@/components/icon'\n\nimport { html, LitElement, nothing, type PropertyValues, unsafeCSS } from 'lit'\nimport { customElement, property } 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 { ElementHeightController } from '@/shared/element-height'\nimport { UserChangeController } from '@/shared/events/user-change'\nimport { trapTabKey } from '@/shared/focus/tabbable'\nimport { attachDragGesture, dampOverscroll, type DragGestureHandle } from '@/shared/gesture'\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 { defineNestedDrawerLayers } from '@/shared/overlay/nested-drawer-layers'\nimport { defineOpenOverlay, type OpenOverlayHandle } from '@/shared/overlay/open-overlay'\nimport { defineScrollLockLease } from '@/shared/scroll-lock/scroll-lock'\nimport { prefersReducedMotion } from '@/shared/theme/reduced-motion'\n\nimport style from './style.css?inline'\n\nconst ALLOWED_PLACEMENTS = ['right', 'left', 'top', 'bottom'] as const\n\nexport type DrawerPlacement = (typeof ALLOWED_PLACEMENTS)[number]\n\n/*\n * 释放后的拖拽关闭判定:\n *\n * - 基准校准:位移零点与判定时钟都在**首个 pointermove** 处重置(手势层\n * `calibrateOnFirstMove`,drawer 显式开启),吸收「按下 → 首个 move」之间的空隙——\n * 触摸输入的首个 move 常已相对按下点偏移,不重置会让元素在首个 move 一次性兑现\n * 而跳动。代价是这段位移被丢弃,一次手势至少要累积两个 move 才可能产生位移;\n * `DragMoveInfo/DragEndInfo` 的 `delta*` 与 `duration` 均以该点为零点与起点。\n * - 距离:**自抓取瞬间起的净位移**超过 drawer 尺寸的一半(下限 10px)。用净位移而不是\n * 绝对位置:从弹回/收尾途中重新抓取时,抓取瞬间停在哪里都不算「已经拖过一半」。\n * 尺寸量不到时(0)由下限兜底,保证退化场景不会因为阈值 0 而「动一下就关」。\n * - 甩动:整段手势的平均速度**达到** 500px/s(含等号)。用「整段手势」而不是释放\n * 瞬间的滑窗速度:滑窗只描述最后一小段轨迹,反向回扫(先往打开方向拖进量程,\n * 再快速扫回)在释放瞬间同样能凑出很高的滑窗速度,但它不是朝关闭方向的甩动。\n * 净速度 = 净位移 / 整段时长(自校准点起算),分母钳到 50ms;时长为 0 时速度取 0,\n * 而不是按 50ms 兜底放大——直接放大会把「测不到时长」变成一次甩动。\n * - 方向:净位移未朝闭合方向(`<= 0`)时一律弹回。\n * - 改主意:拖拽期内追踪「自折返点起」的位移,一旦自方向确认点回撤满 10px 即标记\n * 取消,甩动分支此后拒绝关闭;位移重新越过关闭距离阈值时清除标记,即\n * 「已经拖过一半」不受回撤影响。见 `_trackSwipeCancel`。\n * - 过冲阻尼:闭合方向全额 1:1 跟手,反向按 `dampOverscroll` 平方根压缩。阻尼作用于\n * **本次手势的增量**,再叠加抓取瞬间的基准位移;对总位移做阻尼会连基准一起压缩,\n * 元素不再接着视觉位置走。\n */\nconst DRAG_CLOSE_RATIO = 0.5\nconst DRAG_MIN_CLOSE_DISTANCE = 10\nconst DRAG_FLICK_VELOCITY = 500\nconst DRAG_MIN_VELOCITY_SPAN_MS = 50\nconst DRAG_REVERSE_CANCEL_THRESHOLD = 10\nconst DRAG_REQUEST_WINDOW_MS = 120\n// backdrop 点击(click 链路)的轻点判定距离:按下起点到 click 坐标的欧氏位移未越过该值\n// 视为轻点,沿用「点 backdrop 关闭」语义(对齐 _dragCloseThreshold 的 10px 下限量级)。\n// 必须用欧氏长度(而非仅主轴分量):只看主轴会让「主轴不动、垂直大幅滑动」的\n// press–drag–release 链路仍落在窗口内被误读成轻点。\nconst BACKDROP_TAP_DISTANCE = 10\n\n/*\n * header / footer 让开给「可点控件」的选择器(上下 placement 专用)。\n *\n * 上下 placement 的端头整块可拖后,让开靠**逐个控件**在事件层判断,不靠几何切割:\n * 切割要测量每个控件的位置,控件一改位置就失效;逐个控件让开是结构性的。\n *\n * 判据是「这个元素自己就代表一次用户操作」,分三类:\n * - 原生可交互元素与原生可点容器(button / input / label / a[href] / summary …)\n * - 显式可编辑(contenteditable)\n * - 交互语义 role(组件库自绘的 checkbox、tab、menuitem 等没有原生 tag)\n * 另加「作者显式给了可聚焦 tabindex」:自定义控件常只靠 tabindex 暴露可操作性。\n *\n * 方向是**保守让开**:判不出来就让开。让开的代价是那一小块起不了拖(用户多点一下),\n * 判错的代价是控件失效(A6 那条回归),两者不对等。\n */\nconst HEADER_FOOTER_INTERACTIVE_SELECTOR = [\n 'button',\n 'input',\n 'select',\n 'textarea',\n 'label',\n 'summary',\n 'a[href]',\n 'audio[controls]',\n 'video[controls]',\n '[contenteditable]:not([contenteditable=\"false\"])',\n '[tabindex]:not([tabindex=\"-1\"])',\n '[role=\"button\"]',\n '[role=\"link\"]',\n '[role=\"checkbox\"]',\n '[role=\"switch\"]',\n '[role=\"radio\"]',\n '[role=\"tab\"]',\n '[role=\"menuitem\"]',\n '[role=\"menuitemcheckbox\"]',\n '[role=\"menuitemradio\"]',\n '[role=\"option\"]',\n '[role=\"textbox\"]',\n '[role=\"combobox\"]',\n '[role=\"slider\"]',\n '[role=\"spinbutton\"]'\n].join(',')\n\n/*\n * 释放后的收尾(弹回打开位 / 滑出到闭合位)由 CSS transition 接管(issue #123):\n * 拖拽期间内联 transform + `transition: none`,松手时把终值与过渡参数写入内联并在\n * 同一次样式重算里解除抑制,回弹即唯一的一次 CSS 过渡。全程无 `element.animate()`、\n * 无 fill、无 JS→CSS 动画交接边界,因此不存在 Safari 采不到 before-change style 的\n * 窗口。\n *\n * 代价是没有弹簧的速度感知轨迹:释放速度只用于估算过渡时长,过冲手感由缓动曲线\n * 近似。两条曲线对应弹簧预设的阻尼比——close(ζ≈1.05)无过冲,\n * rebound(ζ≈0.74)保留约 3% 过冲。\n */\nconst SETTLE_MIN_MS = 180\nconst SETTLE_MAX_MS = 420\nconst SETTLE_EASE_CLOSE = 'cubic-bezier(0.32, 0.72, 0, 1)'\n// 过冲量由 y1 决定:1.3 时峰值 ≈2.99%,与 ζ≈0.74 的理论过冲(3.15%)对齐;1.12 只剩 0.37%。\nconst SETTLE_EASE_REBOUND = 'cubic-bezier(0.22, 1.3, 0.36, 1)'\n// 估算时长的速度下限(px/s):静止释放时退化为最大时长,避免除零与无限时长。\nconst SETTLE_MIN_VELOCITY = 1\n// transitionend 兜底定时器的宽限:覆盖一帧的调度抖动。\nconst SETTLE_GRACE_MS = 80\n\n/*\n * 拖拽期间由 JS 写入的 backdrop 透明度变量参与 WAAPI 关键帧。未注册的自定义属性在\n * 关键帧之间是离散插值(半帧跳变),注册为 <number> 后获得线性插值。\n * inherits 必须为 true:::backdrop 只从 originating element 继承「可继承属性」,\n * 注册为不可继承会让变量到不了 backdrop,跟手淡出整体失效(Chrome 151 实证)。\n * 注册表是全局的,模块重复执行(HMR)时捕获已注册错误并忽略。\n */\nif (typeof CSS !== 'undefined' && 'registerProperty' in CSS) {\n try {\n CSS.registerProperty({\n name: '--wui-internal-drag-backdrop-opacity',\n syntax: '<number>',\n inherits: true,\n initialValue: '1'\n })\n } catch {\n // 已注册(如 HMR 重复执行)时忽略\n }\n /*\n * 公开 token 注册为 <length>:Consumer 传 unitless 0 时计算值被归一为 0px,\n * 避免 calc(100% + 0) 因 number/percentage 不兼容而使闭合位移失效。\n */\n try {\n CSS.registerProperty({\n name: '--wui-drawer-inset',\n syntax: '<length>',\n inherits: true,\n initialValue: '8px'\n })\n } catch {\n // 已注册(如 HMR 重复执行)时忽略\n }\n /*\n * 嵌套层叠对数曲线的基准 A(shift = A · ln(depth + 1)):注册为 <length> 后\n * 桌面基准只有这一个真相源,JS 从 computed style 读到的永远是确定值;\n * 窄视口基准由 style.css 的媒体查询覆盖同一 token。消费方写 unitless 的\n * `0` 会在计算值阶段归一成 `0px`(层叠露边归零,而不是让 ln 项算出 NaN)。\n *\n * A = 43.2px 来自视觉 pass:四层等宽 320px 时逐层露边\n * 29.94 / 17.52 / 12.43px,总堆叠宽 59.89px。\n *\n * 取值由**最里层**那一跳决定:对数曲线压得最狠的正是第四层,它只有\n * A·ln(4/3) = 0.288A。12.43px 略高于 12px 的线性步进,但余量已经收窄到\n * 3.5%——继续下调会让最里层那一条边与上一条边难以分辨。若还要更小的基准,\n * 应先改 ln 的压缩比而不是只降 A。\n */\n try {\n CSS.registerProperty({\n name: '--wui-drawer-nested-peek-base',\n syntax: '<length>',\n inherits: true,\n initialValue: '43.2px'\n })\n } catch {\n // 已注册(如 HMR 重复执行)时忽略\n }\n /*\n * 收尾过渡的时长:注册为 <time> 后 ::backdrop 也能继承到同一值。不注册时变量在\n * transition 简写里只是 token 替换,值一旦无效整条声明在计算值阶段失效\n * (退化为 transition: none),回弹会变成瞬移。\n * 缓动不注册:CSS 属性语法没有 <easing-function>,且它不需要插值,靠 var() 的\n * 兜底值即可。\n */\n try {\n CSS.registerProperty({\n name: '--wui-internal-settle-duration',\n syntax: '<time>',\n inherits: true,\n initialValue: '280ms'\n })\n } catch {\n // 已注册(如 HMR 重复执行)时忽略\n }\n}\n\n@customElement('web-ui-drawer')\nexport class WebUiDrawer 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, attribute: 'no-scroll-lock' }) noScrollLock = false\n @property({ type: Boolean, reflect: true, attribute: 'no-backdrop-close' }) noBackdropClose = false\n\n /**\n * Controlled 模式下,Escape、backdrop、关闭 button 和拖拽关闭只派发 `open-change` 请求,\n * 不会自行修改 `open`。Consumer 回写 `open` 后才执行关闭动画。\n * 程序化 API(show/close/直接赋值 open)不受此模式影响,始终直通。\n */\n @property({ type: Boolean, reflect: true }) controlled = false\n\n @property({ type: String, reflect: true })\n get placement(): DrawerPlacement {\n return this._placement\n }\n set placement(v: string) {\n const old = this._placement\n this._placement = normalizeLiteral(v, ALLOWED_PLACEMENTS, 'right')\n this.requestUpdate('placement', old)\n }\n private _placement: DrawerPlacement = 'right'\n\n // 标题文字(未传 header slot 时显示默认 header)\n @property({ type: String }) heading = ''\n\n @property({ type: Boolean, reflect: true }) closable = false\n\n /**\n * Headless 模式:只保留 overlay 基础设施(backdrop、动画、scroll lock、dialog 语义),\n * 移除内置 UI(glass 样式、header、close button、footer)。Consumer 自定义内容样式。\n */\n @property({ type: Boolean, reflect: true }) headless = false\n\n /**\n * 内部原生 dialog 的 accessible name。headless 模式必须由 Consumer 提供,\n * 因为该模式不会渲染可自动关联的内置 header。\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 /**\n * 启用拖拽关闭:打开态在 drawer 内缘渲染 drag bar(灰色胶囊 + 加宽 drag zone),\n * 指针拖拽实时跟手,松手按位移与甩动速度判定关闭或弹回。\n * 关闭态基于原生 dialog 无可见渲染物,因此不支持从关闭态拖拽打开。\n */\n @property({ type: Boolean, reflect: true }) override draggable = false\n\n private get dialog() {\n return this.shadowRoot?.querySelector('dialog') ?? null\n }\n\n private _hasHeaderSlot = false\n private _hasFooterSlot = false\n private readonly _userOpenChange = new UserChangeController()\n /*\n * 上下 placement 的拖拽 drag zone 要让开头尾两节(见 style.css 的 --wui-internal-drawer-* 说明):\n * `bottom` 贴顶边、drag zone 让开 header,`top` 贴底边、drag zone 让开 footer。让开的量就是那一节的\n * border-box 高度,写成 host 上的自定义属性交给 CSS 去算偏移——样式侧因此不必知道\n * header / footer 的存在与高度,两条 placement 规则各只读自己的那个属性。\n *\n * 用 offsetHeight 而非 CSS 侧推导:`[hidden]` 的节天然是 0(让位量归零,drag zone 回到 panel 边缘),\n * 而「header 存在但高度为 0」与「没有 header」在布局上是同一件事,不需要额外的存在性 toggle。\n * 高度是动态的(consumer 的 padding、窄屏断点改写、slotted 内容换行),所以观察而非读取一次。\n */\n private readonly _headerHeight = new ElementHeightController(this, '.wui-drawer-header', height =>\n this._setAvoidance('--wui-internal-drawer-header-inset', height)\n )\n private readonly _footerHeight = new ElementHeightController(this, '.wui-drawer-footer', height =>\n this._setAvoidance('--wui-internal-drawer-footer-inset', height)\n )\n\n /*\n * 让位量 = 该节高度 + 一点呼吸间距,且**只在真有那一节时**才加间距。\n *\n * 间距写成属性值里的 calc 而不是让 CSS 无条件相加,是因为「没有 header」这一态必须精确等于\n * 让位量 0 的几何(drag zone 顶边贴 panel 顶边)。若让间距无条件生效,无 header 的 drawer 会\n * 平移 4px,drag zone 端边与 panel 端边的贴合关系跟着变——那是\n * `header-drag-surface.browser.spec.ts` 的 R2 钉住的关系,这条护栏不该让步。\n * 间距本身仍以 token 暴露,真机调参不必改代码。\n */\n private _setAvoidance(property: string, height: number) {\n this.style.setProperty(\n property,\n height > 0 ? `calc(${height}px + var(--wui-drawer-drag-zone-inset, var(--wui-space-1, 4px)))` : '0px'\n )\n }\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 // nested 层序:打开后纳入全局栈,上层打开/关闭时本层缩放平移。\n private readonly _nestedLayers = defineNestedDrawerLayers().make({\n getDialog: () => this.dialog,\n getPlacement: () => this._placement\n })\n /*\n * 开启态 overlay(issue #120 Block 1)。drawer 把自己的原生 <dialog> 登记为 panel,使挂在\n * dialog 上的 portal panel(select / dropdown / tooltip)在逻辑组合树上成为它的后代,\n * 仲裁者据此选出最内层。\n *\n * 拖拽进行中不响应 Escape,但**仍是候选**:仲裁者照旧吞掉按键并压掉原生 cancel,\n * 只是不走关闭入口。关闭入口只有这一个执行者。\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\n private _dragGesture: DragGestureHandle | null = null\n // pointerdown 时刻已存在的闭合方向位移(从弹回动画中抓取时非 0)。\n private _dragInitialOffset = 0\n private _dragOffset = 0\n // 位移零点与判定时钟由手势层的 `calibrateOnFirstMove` 校准到首个 pointermove,\n // 这里只保留「改主意」相关的追踪状态。\n // 折返点(轴坐标):随反向移动拉到当前坐标。\n private _cancelBaseline = 0\n // 上一次 move 的轴坐标,用于求移动量。\n private _lastAxisPos = 0\n // 方向确认时的位移;负值表示方向尚未确认。\n private _swipeIntentDisp = -1\n // 已判定为「改主意」,甩动分支据此拒绝关闭。\n private _swipeCancelled = false\n // 释放后的 CSS transition 收尾:完成回调,以及终值与当前值相同时\n // 不会派发 transitionend 的兜底定时器。\n private _settleFinish: (() => void) | null = null\n private _settleTimer: ReturnType<typeof setTimeout> | undefined\n // controlled:弹簧到闭合位后等待 Consumer 回写 open;超时未回写则弹回。\n private _dragAwaitWriteback = false\n private _dragRequestTimer: ReturnType<typeof setTimeout> | undefined\n\n private get _dragAxis(): 'x' | 'y' {\n return this._placement === 'left' || this._placement === 'right' ? 'x' : 'y'\n }\n\n // 位移正方向 = drawer 的闭合方向(拖出屏幕为正)。\n private get _dragCloseSign(): number {\n switch (this._placement) {\n case 'right':\n return 1\n case 'left':\n return -1\n case 'top':\n return -1\n case 'bottom':\n return 1\n }\n }\n\n private _measureDragSize(): number {\n const dialog = this.dialog\n if (!dialog) return 0\n return this._dragAxis === 'x' ? dialog.offsetWidth : dialog.offsetHeight\n }\n\n // 关闭距离阈值:尺寸的一半,下限 10px。\n // 胶囊的 accent 视觉确认与实际判定共用同一阈值,两者不会漂移。\n private _dragCloseThreshold(size: number): number {\n return Math.max(size * DRAG_CLOSE_RATIO, DRAG_MIN_CLOSE_DISTANCE)\n }\n\n // 浮动卡片(非 headless)的四周留边;闭合位移需越过它才能完全滑出视口。\n // headless 在 :host([headless]) dialog 上显式归零(防嵌套继承),解析失败回退 0。\n private _readDrawerInset(dialog: HTMLDialogElement): number {\n const raw = getComputedStyle(dialog).getPropertyValue('--wui-internal-drawer-inset')\n const parsed = Number.parseFloat(raw)\n return Number.isFinite(parsed) ? parsed : 0\n }\n\n // 闭合方向上的完全出屏距离:drawer 尺寸 + 浮动留边(headless 下即尺寸本身)。\n // CSS 闭合态 transform、controlled 悬停终态与弹簧终点共用同一数学,避免衔接跳变。\n private _dragCloseDistance(dialog: HTMLDialogElement): number {\n return this._measureDragSize() + this._readDrawerInset(dialog)\n }\n\n private _isDragging(): boolean {\n return this._dragGesture?.isDragging() ?? false\n }\n\n /*\n * 拖拽期间本层仍是候选:仲裁者照旧 preventDefault(压掉原生 <dialog> 的 cancel)与\n * stopPropagation,只是不调用 requestClose。`isDragging()` 的翻转由手势层内部决定,\n * 所以随 move 与各终结路径同步(幂等)。除拖拽外本层没有别的惰性来源。\n */\n private _syncDragInert() {\n this._handle?.setInert(this._isDragging())\n }\n\n private _cancelDragAwait() {\n if (this._dragRequestTimer !== undefined) {\n clearTimeout(this._dragRequestTimer)\n this._dragRequestTimer = undefined\n }\n this._dragAwaitWriteback = false\n }\n\n /**\n * 上下 placement 的 header / footer 自己接拖拽手势。\n *\n * 为什么不是「把 drag zone 铺到 header 上」:那样 header 里的控件会被透明命中层吞掉,\n * 正是 A6 修掉的那条缺陷。这里在事件层让开——控件照常收自己的事件,拖拽只在\n * 「落点不是可点控件」时起手。\n *\n * 三道闸门任一不成立就不接管:\n * 1. placement 必须是上下。左右 placement 的拖拽轴是 x,端头在竖边上,header\n * 既不在可抓边缘上也不该起手(R4 的行为侧)。\n * 2. 下层嵌套 drawer 整套拖拽提示一起退场。drag zone 与 drag edge 靠 CSS 的 pointer-events 退场,\n * 事件层必须自己判同一条,否则 header 会成为唯一一条漏网的入口。\n * 3. 落点路径里有可点控件就让开——控件优先于「这一块可拖」。\n */\n private _handleSectionDragPointerDown(e: PointerEvent) {\n if (this._placement !== 'top' && this._placement !== 'bottom') return\n const section = (e.currentTarget as HTMLElement | null)?.closest('.wui-drawer-header, .wui-drawer-footer')\n const isHeader = section?.classList.contains('wui-drawer-header') ?? false\n // 只有**贴着可抓边缘**的那一节接手势:bottom 的 header、top 的 footer。\n // 另一节在 panel 远端,在那里起手却朝闭合方向拖是反直觉的,也不属于「端头」——\n // 与 drag zone 只让开同一侧那节是同一条边界。\n if (this._placement === 'bottom' && !isHeader) return\n if (this._placement === 'top' && isHeader) return\n // 下层嵌套 drawer 整套拖拽提示一起退场:drag zone 与 drag edge 靠 CSS 的 pointer-events 退场,\n // 事件层必须判同一条,否则这里是唯一一条漏网的入口。\n if (this.dialog?.classList.contains('is-nested-lower')) return\n if (this._isInteractivePath(e)) return\n this._handleDragPointerDown(e)\n }\n\n /**\n * 按下路径上是否出现可点控件。\n *\n * 走 composedPath 而不是 event.target:消费侧 slotted 控件与组件库自绘控件都各有\n * shadow root,target 只给出最内层那个节点,沿 parentElement 上行会在 shadow 边界\n * 断掉,控件自己那层反而看不见。\n */\n private _isInteractivePath(e: PointerEvent): boolean {\n const path = typeof e.composedPath === 'function' ? e.composedPath() : [e.target]\n for (const node of path) {\n if (node instanceof Element && node.matches(HEADER_FOOTER_INTERACTIVE_SELECTOR)) return true\n }\n return false\n }\n\n private _handleDragPointerDown(e: PointerEvent) {\n if (!this.open || !this.draggable || this._isDragging()) return\n // 多点触控的副指针(isPrimary 明确为 false)不参与手势。\n if (e.isPrimary === false) return\n\n const dialog = this.dialog\n if (!dialog) return\n // is-visible 由 presence 在 showModal 后一帧加上;就位前(打开极早期)忽略。\n // 就位后即使 enter 过渡仍在进行也允许抓取,起始位移从当前计算值续接。\n if (!dialog.classList.contains('is-visible')) return\n\n // 先读取动画中的当前位移再取消弹回动画,避免取消后回跳到内联样式值。\n const currentTransform = getComputedStyle(dialog).transform\n const axisValue =\n currentTransform && currentTransform !== 'none' && typeof DOMMatrixReadOnly === 'function'\n ? new DOMMatrixReadOnly(currentTransform)[this._dragAxis === 'x' ? 'm41' : 'm42']\n : 0\n // 收尾过渡进行中被重新抓取:丢弃收尾状态(不执行其回调,避免它回头写内联),\n // 位移已在下面按当前计算值续接。\n this._abortSettle()\n this._cancelDragAwait()\n\n this._dragInitialOffset = axisValue * this._dragCloseSign\n this._dragOffset = this._dragInitialOffset\n // 判定基准复位:折返点从按下位置起算。\n const axisPos = this._dragAxis === 'x' ? e.clientX : e.clientY\n this._cancelBaseline = axisPos\n this._lastAxisPos = axisPos\n this._swipeIntentDisp = -1\n this._swipeCancelled = false\n dialog.classList.add('is-dragging')\n // 收尾过渡进行中重新抓取时内联还停在收尾终值(打开位/闭合位),不写回当前位移\n // 会在抑制生效的瞬间跳到终值。写回后计算值 == 抓取瞬间的视觉位置,无跳变。\n this._applyDragOffset(dialog, this._dragOffset)\n\n this._dragGesture = attachDragGesture(e, {\n axis: this._dragAxis,\n // 拖拽零点与判定时钟一起重置到首个 move。\n calibrateOnFirstMove: true,\n onMove: info => {\n // info 的 delta/duration 已以校准点为零点。\n const pointerDelta = this._dragAxis === 'x' ? info.deltaX : info.deltaY\n // 本次手势自校准点起的**增量**(朝闭合方向为正,未阻尼)。\n const dragDelta = pointerDelta * this._dragCloseSign\n // 阻尼只作用于增量,再叠加抓取瞬间的基准位移。对总位移做阻尼会在「从弹回中\n // 重新抓取」时差出可见位移——基准位移本身会被一起压缩,元素不再接着视觉位置走。\n this._dragOffset = this._dragInitialOffset + dampOverscroll(dragDelta)\n this._trackSwipeCancel(this._dragAxis === 'x' ? info.clientX : info.clientY, dragDelta)\n\n // 达到关闭阈值时胶囊换成更实的中性灰作视觉确认(ADR-0027)。与距离分支共用同一量\n //(自抓取瞬间起的净位移)与同一阈值,视觉确认与实际判定不会漂移。\n const dragSize = this._measureDragSize()\n const dragDisplacement = this._dragOffset - this._dragInitialOffset\n dialog.classList.toggle('is-drag-close', dragDisplacement > this._dragCloseThreshold(dragSize))\n this._applyDragOffset(dialog, this._dragOffset)\n },\n onEnd: info => {\n this._dragGesture = null\n this._syncDragInert()\n dialog.classList.remove('is-dragging', 'is-drag-close')\n const size = this._measureDragSize()\n const velocity = (this._dragAxis === 'x' ? info.velocityX : info.velocityY) * this._dragCloseSign\n // 净位移从抓取瞬间的位移起算(弹回/收尾过渡中被重新抓取时起点非 0)。\n const displacement = this._dragOffset - this._dragInitialOffset\n // 时长同样自校准点(首个 move)起算,与位移零点同源:按下后的停顿不参与分母。\n // 分母下限 50ms;时长量为 0(首尾时间戳相同,或事件时间戳不可用/倒退)时速度取\n // 0——直接按 50ms 兜底会把「测不到时长」放大成一次甩动。\n const netVelocity =\n info.duration > 0 ? (displacement / Math.max(info.duration, DRAG_MIN_VELOCITY_SPAN_MS)) * 1000 : 0\n // 甩动判据:净位移朝闭合方向(`> 0`)、平均速度达到阈值(`>=`)、\n // 且本次手势未被判定为「改主意」。\n const flicked = displacement > 0 && netVelocity >= DRAG_FLICK_VELOCITY && !this._swipeCancelled\n // 距离分支同样用净位移,不是绝对位置:抓取瞬间停在哪里都不算「已经拖过一半」。\n // `_dragOffset` 只留给收尾的起点使用。\n const shouldClose = displacement > this._dragCloseThreshold(size) || flicked\n\n if (shouldClose) this._settleToClose(dialog, velocity)\n else this._settleRebound(dialog, velocity, this._dragOffset)\n },\n onCancel: () => {\n this._dragGesture = null\n this._syncDragInert()\n dialog.classList.remove('is-dragging', 'is-drag-close')\n this._settleRebound(dialog, 0, this._dragOffset)\n }\n })\n /*\n * 惰性必须与 attach 同拍。漏掉这里,按下到手势终结之间那一整段里 Escape 仍会关闭\n * drawer——push 通道没有自动同步,每个翻转点自己推一次。\n *\n * `isDragging()` 的翻转点不止 attach 与 onEnd:手势层的 `handlePointerMove` 在越过死区后\n * 若判定轴向冲突(限定了 x 轴却明显纵向移动,或反之)会直接 `cancel()`,\n * `lostpointercapture` 也会终结手势。这些路径都走 `onCancel`,所以 `onCancel` 里那次\n * `_syncDragInert()` **不是**冗余——删掉它,「移动中取消拖拽」会让 drawer 重新变回可被\n * Escape 关闭。\n */\n this._syncDragInert()\n }\n\n /*\n * 「改主意」守卫。\n *\n * 折返点随「反向移动」被拉到当前坐标,因此「自折返点起的位移」度量的是**自最近一次\n * 折返之后**回撤了多少:一旦它比方向确认时的位移少满 10px,本次手势即被标记为\n * 「改主意」,甩动分支据此拒绝关闭;位移重新越过关闭距离阈值即清除标记,\n * 即「已经拖过一半」不受回撤影响。\n *\n * @param axisPos 本次 move 在主轴上(`_dragAxis`)的指针坐标\n * @param displacement 自校准点起、朝闭合方向的**未阻尼**位移\n */\n private _trackSwipeCancel(axisPos: number, displacement: number) {\n const movement = axisPos - this._lastAxisPos\n this._lastAxisPos = axisPos\n if ((movement < 0 && axisPos > this._cancelBaseline) || (movement > 0 && axisPos < this._cancelBaseline)) {\n this._cancelBaseline = axisPos\n }\n\n if (this._swipeIntentDisp < 0) {\n // 方向确认:首次朝闭合方向累积出位移。\n if (displacement > 0) this._swipeIntentDisp = displacement\n return\n }\n\n const cancelDisplacement = (axisPos - this._cancelBaseline) * this._dragCloseSign\n if (cancelDisplacement > this._dragCloseThreshold(this._measureDragSize())) {\n this._swipeCancelled = false\n } else if (this._swipeIntentDisp - cancelDisplacement >= DRAG_REVERSE_CANCEL_THRESHOLD) {\n this._swipeCancelled = true\n }\n }\n\n // 受控状态写入等外部原因强制终结拖拽:清手势状态与拖拽样式,不弹回,\n // 后续管线(关闭/打开)由调用方继续执行。\n private _cancelActiveDrag() {\n if (!this._isDragging()) return\n this._dragGesture?.destroy()\n this._dragGesture = null\n this._syncDragInert()\n const dialog = this.dialog\n if (!dialog) return\n dialog.classList.remove('is-dragging', 'is-drag-close')\n this._clearDragStyles(dialog)\n }\n\n // 将闭合方向位移映射为 dialog transform,并同步 backdrop 透明度保持跟手反馈。\n private _applyDragOffset(dialog: HTMLDialogElement, offset: number) {\n const sign = this._dragCloseSign\n const value = offset * sign\n dialog.style.transform = this._dragAxis === 'x' ? `translateX(${value}px)` : `translateY(${value}px)`\n const size = this._measureDragSize()\n const progress = size > 0 ? Math.min(1, Math.max(0, offset / size)) : 0\n dialog.style.setProperty('--wui-internal-drag-backdrop-opacity', String(1 - progress))\n }\n\n private _clearDragStyles(dialog: HTMLDialogElement) {\n dialog.style.removeProperty('transform')\n dialog.style.removeProperty('--wui-internal-drag-backdrop-opacity')\n }\n\n // 由 CSS transition 收尾到完全闭合;controlled 下保持闭合位等待回写,其余走常规关闭管线。\n private _settleToClose(dialog: HTMLDialogElement, velocity: number) {\n const from = this._dragOffset\n this._dragOffset = 0\n // 终点 = 完全出屏距离(含浮动留边),与 CSS 闭合态/悬停终态一致,\n // 否则收尾结束后会有一个留边宽度的瞬移。\n const to = this._dragCloseDistance(dialog)\n\n const finishClose = () => {\n if (this.controlled) {\n // 保持在闭合位(is-visible 未移除,状态仍 open),等待 Consumer 回写或超时弹回。\n this._applyDragOffset(dialog, to)\n } else {\n // 移除 is-visible 后基础 transform 即闭合位,清内联样式不产生跳变;\n // presence.sync(false) 检测不到 is-visible 会立即完成关闭,不重播退出动画。\n dialog.classList.remove('is-visible')\n this._clearDragStyles(dialog)\n }\n this._closeFromDrag()\n }\n\n if (prefersReducedMotion(this) || Math.abs(to - from) < 1) {\n // 与 _settleRebound 的无动画路径对称:自己解除抑制,不依赖调用方清过类。\n dialog.classList.remove('is-dragging')\n finishClose()\n return\n }\n\n this._startSettle(\n dialog,\n to,\n 0,\n this._settleDuration(Math.abs(to - from), velocity),\n SETTLE_EASE_CLOSE,\n finishClose\n )\n }\n\n // 弹回打开位:从 from 位移回到 0;结束后把内联样式交还 CSS 打开态。\n private _settleRebound(dialog: HTMLDialogElement, velocity: number, from: number) {\n this._dragOffset = 0\n\n // 收尾结束必须清掉内联 transform:translateX(0px) 内联会盖住闭合态 CSS transform,\n // 之后用 button/backdrop/Esc 关闭时计算值恒为 0、开与关都不再触发过渡。\n const finishRebound = () => {\n this._clearDragStyles(dialog)\n }\n\n if (prefersReducedMotion(this) || Math.abs(from) < 1) {\n // 无动画路径:直接移除内联回到 CSS 打开位。计算值就是可信的打开位 0,\n // 不存在 WAAPI fill 覆盖,无需强制 reflow「烘焙」。\n dialog.classList.remove('is-dragging')\n finishRebound()\n return\n }\n\n this._startSettle(dialog, 0, 1, this._settleDuration(Math.abs(from), velocity), SETTLE_EASE_REBOUND, finishRebound)\n }\n\n // 释放速度只影响收尾时长:以它走完剩余距离的时间作为估计,慢放长、甩动短。\n private _settleDuration(distance: number, velocity: number): number {\n const ms = (distance / Math.max(Math.abs(velocity), SETTLE_MIN_VELOCITY)) * 1000\n return Math.min(SETTLE_MAX_MS, Math.max(SETTLE_MIN_MS, ms))\n }\n\n /*\n * 把释放后的收尾交给 CSS transition:写入终值与「时长/缓动」两个内部变量,并在\n * 同一次样式重算里解除抑制(is-dragging → is-settling)。过渡的 before-change\n * style 就是拖拽期间写入的内联值——没有 WAAPI 覆盖层,各引擎采样一致,这正是\n * 对 Safari「不采信 fill 覆盖」怪癖免疫的原因。\n */\n private _startSettle(\n dialog: HTMLDialogElement,\n offset: number,\n backdropOpacity: number,\n duration: number,\n easing: string,\n onDone: () => void\n ) {\n // 覆盖前先丢弃上一次收尾:否则它的兜底定时器会提前结束这一次的过渡。\n this._abortSettle()\n const sign = this._dragCloseSign\n dialog.style.setProperty('--wui-internal-settle-duration', `${duration}ms`)\n dialog.style.setProperty('--wui-internal-settle-easing', easing)\n dialog.style.transform =\n this._dragAxis === 'x' ? `translateX(${offset * sign}px)` : `translateY(${offset * sign}px)`\n dialog.style.setProperty('--wui-internal-drag-backdrop-opacity', String(backdropOpacity))\n dialog.classList.add('is-settling')\n dialog.classList.remove('is-dragging')\n\n this._settleFinish = onDone\n // 终值与当前值相同时不会派发 transitionend(也没有可见变化),用定时器兜底。\n this._settleTimer = setTimeout(() => this._finishSettle(), duration + SETTLE_GRACE_MS)\n }\n\n // 收尾过渡结束(transitionend 或兜底超时):先撤 settle 状态再执行收尾。\n private _finishSettle() {\n const finish = this._settleFinish\n if (!finish) return\n this._abortSettle()\n finish()\n }\n\n // 丢弃进行中的收尾(重新抓取、强制关闭、断连):只撤状态,不执行收尾回调。\n private _abortSettle() {\n this._settleFinish = null\n if (this._settleTimer !== undefined) {\n clearTimeout(this._settleTimer)\n this._settleTimer = undefined\n }\n const dialog = this.dialog\n if (!dialog) return\n dialog.classList.remove('is-settling')\n dialog.style.removeProperty('--wui-internal-settle-duration')\n dialog.style.removeProperty('--wui-internal-settle-easing')\n }\n\n private _closeFromDrag() {\n // 先派发关闭请求(_closeFromUser),再进入悬停等待:等待态中 _closeFromUser\n // 会被 L2 去重守卫短路,顺序颠倒会吞掉首次请求。\n this._closeFromUser()\n if (this.controlled) {\n this._dragAwaitWriteback = true\n this._dragRequestTimer = setTimeout(this._handleDragWritebackTimeout, DRAG_REQUEST_WINDOW_MS)\n }\n }\n\n // controlled 回写窗口超时:Consumer 拒绝关闭,从闭合位弹回打开位。\n private readonly _handleDragWritebackTimeout = () => {\n this._dragRequestTimer = undefined\n if (!this._dragAwaitWriteback || !this.open || this._isDragging()) {\n this._dragAwaitWriteback = false\n return\n }\n this._dragAwaitWriteback = false\n\n const dialog = this.dialog\n if (!dialog) return\n dialog.classList.add('is-visible')\n // 悬停终态位于完全出屏位(含留边),弹回也从该真实位置起步,避免首帧内跳。\n this._settleRebound(dialog, 0, this._dragCloseDistance(dialog))\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this._resyncSlotPresence()\n // 重挂载对账:断连时 presence、滚动锁、nested 层序与登记都已被撤销,而\n // `open` 未变化时 `updated()` 不会补跑任何 sync 分支。首次连接时 shadow 尚未\n // 渲染、`this.dialog` 为 null,三种情况都直接跳过;打开态的首次进入仍由\n // updated() 的 `props.has('open')` 分支处理。\n this._presence.reconcile()\n if (this.open) {\n // presence.reconcile 已让打开态的 dialog 同步回到 top layer(此刻 dialog.open\n // 为真),层序 depth 可正确计数,与 updated() 的 register 时机一致。\n this._nestedLayers.register()\n }\n this._syncScrollLock()\n this._reclaimIfOpen()\n }\n\n override firstUpdated() {\n this._checkSlotContent('footer')\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._nestedLayers.dispose()\n this._dragGesture?.destroy()\n this._dragGesture = null\n this._presence.dispose()\n this._scrollLock.release()\n // 断连即撤销登记,重连后由 _reclaimIfOpen 显式重新声明。\n this._handle?.release()\n this._handle = null\n this._abortSettle()\n this._cancelDragAwait()\n }\n\n /** 「重挂载恢复」的调用方一半:模块刻意不观察 DOM 连接状态。 */\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._syncDragInert()\n }\n\n /**\n * 对账 header / footer slot 的有无,**只在真的变了时排一次更新**。\n *\n * 排更新是这条对账不可省的一半:`showHeader` 与 footer 的 `?hidden` 只在 render 里求值,\n * 而 Lit 的 `connectedCallback` 只做 `enableUpdating(true)` 与 `setConnected(true)`,本身不排\n * 更新。两条 slotchange 处理器也接不住这条路径——slotchange 只在**分配集合**变化时派发,\n * 断连期间清空槽位内容时宿主节点本身没动,事件不触发(Chromium 实测)。\n *\n * 漏掉这一半的后果:断连前有 footer、重连时已清空的 drawer 留下一节空的 footer。它在 style.css\n * 的 padding 下仍有真实高度(实测 32px),于是底部一条点不动的死带,拖拽 drag zone 的让位量也照\n * 它算而不归零。\n *\n * 判据取 light DOM 子节点而非 `slot.assignedNodes()`:首次连接时 shadow 尚未渲染,slot 元素\n * 还不存在,那一侧会直接跳过(首次连接的 presence 由 `firstUpdated()` 与 slotchange 兜底);\n * 这里要的正是重连对账——shadow root 跨断连保留,两侧都可用,但只有子节点扫描不依赖渲染\n * 时序。「只在真变了才排」与两条 slotchange 处理器同一纪律,避免每次重连都多一次空渲染。\n */\n private _resyncSlotPresence() {\n const hasHeader = this._hasSlottedChild('header')\n const hasFooter = this._hasSlottedChild('footer')\n if (hasHeader === this._hasHeaderSlot && hasFooter === this._hasFooterSlot) return\n this._hasHeaderSlot = hasHeader\n this._hasFooterSlot = hasFooter\n this.requestUpdate()\n }\n\n private _hasSlottedChild(name: 'header' | 'footer') {\n return Array.from(this.children).some(child => child.getAttribute('slot') === name)\n }\n\n private _checkSlotContent(name: string) {\n const slot = this.shadowRoot?.querySelector(`slot[name=\"${name}\"]`) as HTMLSlotElement | null\n if (!slot) return\n const has = slot.assignedNodes().length > 0\n if (name === 'footer' && has !== this._hasFooterSlot) {\n this._hasFooterSlot = has\n this.requestUpdate()\n }\n }\n\n private handleHeaderSlotChange(e: Event) {\n const has = (e.target as HTMLSlotElement).assignedNodes().length > 0\n if (has !== this._hasHeaderSlot) {\n this._hasHeaderSlot = has\n this.requestUpdate()\n }\n }\n\n private handleFooterSlotChange(e: Event) {\n const has = (e.target as HTMLSlotElement).assignedNodes().length > 0\n if (has !== this._hasFooterSlot) {\n this._hasFooterSlot = has\n this.requestUpdate()\n }\n }\n\n protected override updated(props: PropertyValues) {\n super.updated(props)\n if (!this.isConnected) return\n\n if (props.has('open')) {\n // Consumer 在拖拽进行中写入受控 open:受控状态优先,立即终结手势(等同\n // pointercancel 的清理但不弹回),交由下方 presence 走标准关闭/打开管线。\n if (this._isDragging() && !this.open) this._cancelActiveDrag()\n if (this._userOpenChange.consume()) this.emitOpenChange()\n // controlled 拖拽关闭的回写结果:确认关闭则清除闭合位悬停状态走正常关闭;\n // 拒绝关闭(回写 open=true)则从闭合位(含留边)弹回。\n if (this._dragAwaitWriteback) {\n this._cancelDragAwait()\n const dialog = this.dialog\n if (dialog && !this.open) {\n dialog.classList.remove('is-visible')\n this._clearDragStyles(dialog)\n } else if (dialog && this.open) {\n this._settleRebound(dialog, 0, this._dragCloseDistance(dialog))\n }\n }\n // 关闭且无进行中的拖拽/收尾时,清理可能残留的内联拖拽样式(如收尾路径写在\n // 打开位的 translateX(0px))。若不清理,0px 内联会盖住闭合态 CSS transform,\n // 后续开/关都不再触发过渡。收尾进行中由 _finishSettle 自行收尾,这里丢弃它\n // 以免回调回头写内联与关闭管线竞争。\n if (!this.open && !this._isDragging()) {\n this._abortSettle()\n const dialog = this.dialog\n if (dialog) this._clearDragStyles(dialog)\n }\n this._presence.sync(this.open)\n if (this.open) {\n // presence.sync 内的 showModal() 把 dialog 提升进 top layer。双击手势本身\n // 留下的活选区会被浏览器拿去和提升后的新布局重新解析,结果选区落到刚挂载的\n // drawer 正文上。脆弱的是「手势来源的活选区」,不是 dblclick 的判定窗口:把\n // 打开推迟 250ms(早过双击判定)仍然复现。选区由浏览器在 showModal() 调用\n // 内部生成:脚本没有任何 Selection API 调用,在 dblclick 上 preventDefault\n // 也拦不住。\n //\n // 实测(Chromium)showModal() 不会动打开前已经存在的选区,所以这一次清理\n // 同时也覆盖「先选中页面文本、再打开」的情况——modal 打开后被选中的内容挡在\n // backdrop 后面本来也已不可用。这里只清文档选区,不动 user-select,drawer 内正文\n // 仍可正常拖选;拖拽 handle 与 toggle 时序不受影响。\n window.getSelection()?.removeAllRanges()\n // presence.sync 已同步发起 showModal:此刻 dialog.open 为真,计数出正确\n // 的层序 depth 并驱动下层缩放。直接同步 register,不等待 is-visible\n //(那要再等一帧,且打开过渡期间上层关系已应确立)。\n this._nestedLayers.register()\n // 原生 dialog 登记为开启态 overlay:挂在它上面的 portal panel 在逻辑组合树上\n // 成为本层后代,Escape 仲裁据此判出最内层(issue #120 Block 1)。\n const dialog = this.dialog\n if (dialog) {\n // 同一 panel 重新 claim = 新的一次开启:旧会话(含其子层)整体作废。\n this._handle?.release()\n this._handle = this._overlay.claim(dialog, { ancestryFrom: this })\n this._syncDragInert()\n }\n } else {\n this._nestedLayers.unregister()\n this._handle?.release()\n this._handle = null\n }\n }\n if (props.has('open') || props.has('noScrollLock')) this._syncScrollLock()\n }\n\n private handleTransitionEnd(e: TransitionEvent) {\n // transitionend 会从后代元素冒泡上来(slotted 消费者内容、嵌套子 drawer)。\n // 只处理 dialog 自身的事件:presence 与收尾都只认本层 dialog 的过渡,\n // 后代一条 transform 过渡结束不能提前终结本层收尾(与 presence 内部守卫同规则)。\n if (e.target !== e.currentTarget) return\n this._presence.handleTransitionEnd(e)\n\n // 收尾过渡结束:::backdrop 的 opacity 过渡 target 也是 dialog(带 pseudoElement),\n // 因此再按 propertyName 只认 transform。\n if (e.propertyName === 'transform') this._finishSettle()\n }\n\n private _isNestedDialogEvent(e: KeyboardEvent): boolean {\n const path = e.composedPath()\n const ownDialogIndex = path.indexOf(e.currentTarget as Node)\n return ownDialogIndex > 0 && path.slice(0, ownDialogIndex).some(node => node instanceof HTMLDialogElement)\n }\n\n private handleKeydown(e: KeyboardEvent) {\n if (e.key === 'Tab') {\n if (e.defaultPrevented || this._isNestedDialogEvent(e)) return\n const dialog = this.dialog\n if (dialog) trapTabKey(e, dialog)\n return\n }\n if (e.key !== 'Escape') return\n /*\n * Escape 的归属由共享仲裁者在 document 捕获阶段判定(issue #120 Block 1);命中后\n * 它会 stopPropagation,本 handler 不再执行。此处保留两条兜底:\n * ① defaultPrevented 说明仲裁者已介入,不再重复关闭;\n * ② 兜底路径仍按原逻辑处理,避免「panel 未登记」时 Escape 失灵。\n *\n * 本 handler **不是**可随手删掉的:仲裁者挂在 document 上,只对能跨 shadow\n * 边界的事件生效。实测(overlay-open-owner-260918,2026-09-18)摘掉 `@keydown` 后\n * layout.browser.spec.ts 的两例 Escape 用例立刻转红——它们派发的 keydown 没有\n * `composed: true`,事件止步于 shadow root,到不了 document。\n */\n if (e.defaultPrevented) return\n /*\n * nested 场景防连锁:子 drawer 的 dialog 经 slot 投影在本层 shadow 内,其\n * keydown composed 冒泡路径会再次经过本层 dialog(事件路径含 slot 宿主链)。\n * 若事件传播路径在本层 dialog 之前已存在其他 <dialog>,说明该事件来自子层,本层忽略。\n */\n if (this._isNestedDialogEvent(e)) return\n // 拖拽进行中忽略 ESC,避免手势与关闭管线竞争。\n if (this._isDragging()) {\n e.preventDefault()\n return\n }\n e.preventDefault()\n this._closeFromUser()\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 private readonly _closeFromUser = () => {\n if (!this.open) return\n // controlled 悬停等待回写期间,关闭意图已在途(open-change 已派发):\n // Escape/backdrop/关闭 button 的重复触发不再派发第二次请求,也不与超时弹回竞争。\n if (this._dragAwaitWriteback) return\n if (this.controlled) {\n this.emitOpenChange(false)\n return\n }\n\n this._userOpenChange.mark()\n this.close()\n }\n\n /*\n * 仲裁者在 capture 阶段 preventDefault 了 Escape 的 keydown,UA 因此不再派发原生\n * cancel,但本 handler 仍是「原生 cancel 到达时也要走完整关闭管线」的契约入口,\n * **不可删**。实测(overlay-open-owner-260918,2026-09-18)摘掉 `@cancel` 后\n * drawer.spec.ts 的「no-backdrop-close 存在时 cancel 仍通过关闭过渡退出」与\n * remount-reconcile 的「打开态被移出文档再接回」两例立刻转红。\n */\n private handleCancel(e: Event) {\n // 保留 top layer 直到 CSS 过渡结束,避免原生关闭跳过退出动画。\n e.preventDefault()\n this._closeFromUser()\n }\n\n private handleNativeClose() {\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 // finishClosing 完成全部清理,由 presence 消费并返回 true,不能当成外部关闭把\n // 刚重开的 dialog 关掉——否则重开即被误关,表现为连续 toggle 丢失过渡动画。\n if (this._presence.handleNativeClose()) return\n\n // 悬停等待期间的原生关闭(如表单 method=\"dialog\"):视为回写窗口内的重复\n // 关闭意图,取消等待不补发请求;已原生关闭的 dialog 不再被超时弹回拉起。\n if (this._dragAwaitWriteback) {\n this._cancelDragAwait()\n return\n }\n\n // 原生关闭可绕过 cancel;controlled 时恢复受控状态,由 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 /*\n * backdrop 点击(backdrop click)的按-放链路回溯。\n *\n * 浏览器对「按下 → 拖动 → 松手」生成的 click 落在起点与松手点 DOM 的**共同祖先**\n * 上:从 panel 内容、拖拽 drag zone 甚至 backdrop 本处开始拖拽、松手落在 backdrop 上时,click 的\n * target 都是 dialog,与真正的轻点 backdrop 从 click 自身无法区分(真实鼠标/触摸下\n * 必现的误关路径,合成事件不派发 click 故自动化易漏测)。因此在 dialog 的\n * pointerdown 记录按下起点与是否落在 backdrop 上,click 时回溯校验:\n * - 按下起点不在 backdrop 上(从 panel 内容/drag zone 开始的链路)→ 不是点 backdrop,忽略;\n * - 按下到松手位移超过轻点量级 → 是拖拽松手而非点击,忽略;\n * 两者都通过才沿用「点 backdrop 关闭」语义。\n */\n private _backdropPress: { onMask: boolean; x: number; y: number } | null = null\n\n private handleDialogPointerDown(e: PointerEvent) {\n this._backdropPress = { onMask: e.target === e.currentTarget, x: e.clientX, y: e.clientY }\n }\n\n private handleBackdropClick(e: MouseEvent) {\n if (e.target !== (e.currentTarget as HTMLDialogElement)) return\n if (this.noBackdropClose) return\n // 拖拽进行中 pointer capture 使 click 落在 dialog 上,忽略以避免与手势竞争。\n if (this._isDragging()) return\n // detail 为 0 表示这次 click 不来自指针(键盘激活、程序化 .click()):\n // pointerdown 记录可能是上一次指针交互的残留,不能据此判定 backdrop 点击\n //(对齐 image-preview 的既有守卫)。\n if (e.detail === 0) return\n const press = this._backdropPress\n this._backdropPress = null\n if (!press || !press.onMask) return\n if (Math.hypot(e.clientX - press.x, e.clientY - press.y) > BACKDROP_TAP_DISTANCE) return\n this._closeFromUser()\n }\n\n private emitOpenChange(open = this.open) {\n dispatchOpenChangeEvent(this, open)\n }\n\n private _syncScrollLock(isOpen = this.open) {\n this._scrollLock.sync(isOpen && !this.noScrollLock)\n }\n\n override render() {\n const showHeader = this._hasHeaderSlot || !!this.heading\n const dialogLabel = this.ariaDialogLabel.trim()\n const dialogLabelledBy = !dialogLabel && !this.headless && showHeader ? 'wui-drawer-heading' : nothing\n\n // 拖拽 drag zone:仅在打开且 draggable 时渲染;胶囊 + 加宽命中条贴在 drawer 内缘。\n // 胶囊是 drag zone 的**兄弟节点**而非后代:drag zone 要让开 header / footer(见 style.css),\n // 胶囊作为后代会连带继承那份让位量,被一起推离 panel 边缘。两者都是 dialog 的直接子节点,\n // 胶囊因此相对 dialog 定位、恒贴可抓取边缘,drag zone 则继续为端头上的 button 让位。\n //\n // 端头 drag edge 补上解耦留下的空档:胶囊贴回边缘后就不再落在 drag zone 的让开区里\n // (bottom + header 实测胶囊带 [8,12]、drag zone 带 [60,80]),那一段若没有命中层,\n // 胶囊就成了一条按不下去、悬停不亮的死装饰——R1 与 R3 在几何上本就不可同满足,\n // 结构解耦只让两者不再互相位移,命中层必须另给。drag edge 只在上下 placement 渲染:\n // 左右两条的胶囊本就落在 drag zone 内,多一个元素只会让 R5「左右几何不变」需要额外论证。\n //\n // 顺序要紧:胶囊排最后,两条 drag zone(边、区)都在它前面,hover / active / 确认态三条规则\n // 因此靠通用兄弟选择器 `~` 挂上去。写成相邻兄弟 `+` 的话 drag edge 那条会静默失效。\n const needsEdgeBand = this.placement === 'top' || this.placement === 'bottom'\n const dragBar = this.draggable\n ? html`\n ${\n needsEdgeBand\n ? html`<div class=\"wui-drawer-drag-edge\" @pointerdown=${this._handleDragPointerDown}></div>`\n : nothing\n }\n <div class=\"wui-drawer-drag-zone\" @pointerdown=${this._handleDragPointerDown}></div>\n <div class=\"wui-drawer-drag-bar\"></div>\n `\n : nothing\n\n // 保持同一个 dialog 实例,避免打开期间切换 headless 时脱离 top layer。\n return html`\n <dialog\n aria-label=${dialogLabel || nothing}\n aria-labelledby=${dialogLabelledBy}\n @cancel=${this.handleCancel}\n @close=${this.handleNativeClose}\n @click=${this.handleBackdropClick}\n @pointerdown=${this.handleDialogPointerDown}\n @keydown=${this.handleKeydown}\n @transitionend=${this.handleTransitionEnd}\n >\n ${\n this.headless\n ? html`<slot></slot>${dragBar}`\n : html`\n <div class=\"wui-drawer-body wui-glass\">\n <div\n class=\"wui-drawer-header\"\n id=\"wui-drawer-heading\"\n ?hidden=${!showHeader}\n @pointerdown=${this._handleSectionDragPointerDown}\n >\n <slot name=\"header\" @slotchange=${this.handleHeaderSlotChange}>\n ${this.heading ? html`<span class=\"wui-drawer-heading\">${this.heading}</span>` : nothing}\n </slot>\n </div>\n <div class=\"wui-drawer-content wui-scroll\">\n <slot></slot>\n </div>\n <div\n class=\"wui-drawer-footer\"\n ?hidden=${!this._hasFooterSlot}\n @pointerdown=${this._handleSectionDragPointerDown}\n >\n <slot name=\"footer\" @slotchange=${this.handleFooterSlotChange}></slot>\n </div>\n </div>\n ${\n this.closable\n ? html`\n <web-ui-button\n class=\"wui-drawer-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 : nothing\n }\n ${dragBar}\n `\n }\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-drawer': WebUiDrawer\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["glass","modalSurface","scrollbarStyle","style","oouiClose"],"sources":["../../../src/components/drawer/index.ts"],"sourcesContent":["import '@/components/button'\nimport '@/components/icon'\n\nimport { html, LitElement, nothing, type PropertyValues, unsafeCSS } from 'lit'\nimport { customElement, property } 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 { ElementHeightController } from '@/shared/element-height'\nimport { UserChangeController } from '@/shared/events/user-change'\nimport { trapTabKey } from '@/shared/focus/tabbable'\nimport { attachDragGesture, dampOverscroll, type DragGestureHandle } from '@/shared/gesture'\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 { defineNestedDrawerLayers } from '@/shared/overlay/nested-drawer-layers'\nimport { defineOpenOverlay, type OpenOverlayHandle } from '@/shared/overlay/open-overlay'\nimport { defineScrollLockLease } from '@/shared/scroll-lock/scroll-lock'\nimport { prefersReducedMotion } from '@/shared/theme/reduced-motion'\n\nimport style from './style.css?inline'\n\nconst ALLOWED_PLACEMENTS = ['right', 'left', 'top', 'bottom'] as const\n\nexport type DrawerPlacement = (typeof ALLOWED_PLACEMENTS)[number]\n\n/*\n * 释放后的拖拽关闭判定:\n *\n * - 基准校准:位移零点与判定时钟都在**首个 pointermove** 处重置(手势层\n * `calibrateOnFirstMove`,drawer 显式开启),吸收「按下 → 首个 move」之间的空隙——\n * 触摸输入的首个 move 常已相对按下点偏移,不重置会让元素在首个 move 一次性兑现\n * 而跳动。代价是这段位移被丢弃,一次手势至少要累积两个 move 才可能产生位移;\n * `DragMoveInfo/DragEndInfo` 的 `delta*` 与 `duration` 均以该点为零点与起点。\n * - 距离:**自抓取瞬间起的净位移**超过 drawer 尺寸的一半(下限 10px)。用净位移而不是\n * 绝对位置:从弹回/收尾途中重新抓取时,抓取瞬间停在哪里都不算「已经拖过一半」。\n * 尺寸量不到时(0)由下限兜底,保证退化场景不会因为阈值 0 而「动一下就关」。\n * - 甩动:整段手势的平均速度**达到** 500px/s(含等号)。用「整段手势」而不是释放\n * 瞬间的滑窗速度:滑窗只描述最后一小段轨迹,反向回扫(先往打开方向拖进量程,\n * 再快速扫回)在释放瞬间同样能凑出很高的滑窗速度,但它不是朝关闭方向的甩动。\n * 净速度 = 净位移 / 整段时长(自校准点起算),分母钳到 50ms;时长为 0 时速度取 0,\n * 而不是按 50ms 兜底放大——直接放大会把「测不到时长」变成一次甩动。\n * - 方向:净位移未朝闭合方向(`<= 0`)时一律弹回。\n * - 改主意:拖拽期内追踪「自折返点起」的位移,一旦自方向确认点回撤满 10px 即标记\n * 取消,甩动分支此后拒绝关闭;位移重新越过关闭距离阈值时清除标记,即\n * 「已经拖过一半」不受回撤影响。见 `_trackSwipeCancel`。\n * - 过冲阻尼:闭合方向全额 1:1 跟手,反向按 `dampOverscroll` 平方根压缩。阻尼作用于\n * **本次手势的增量**,再叠加抓取瞬间的基准位移;对总位移做阻尼会连基准一起压缩,\n * 元素不再接着视觉位置走。\n */\nconst DRAG_CLOSE_RATIO = 0.5\nconst DRAG_MIN_CLOSE_DISTANCE = 10\nconst DRAG_FLICK_VELOCITY = 500\nconst DRAG_MIN_VELOCITY_SPAN_MS = 50\nconst DRAG_REVERSE_CANCEL_THRESHOLD = 10\nconst DRAG_REQUEST_WINDOW_MS = 120\n// backdrop 点击(click 链路)的轻点判定距离:按下起点到 click 坐标的欧氏位移未越过该值\n// 视为轻点,沿用「点 backdrop 关闭」语义(对齐 _dragCloseThreshold 的 10px 下限量级)。\n// 必须用欧氏长度(而非仅主轴分量):只看主轴会让「主轴不动、垂直大幅滑动」的\n// press–drag–release 链路仍落在窗口内被误读成轻点。\nconst BACKDROP_TAP_DISTANCE = 10\n\n/*\n * header / footer 让开给「可点控件」的选择器(上下 placement 专用)。\n *\n * 上下 placement 的端头整块可拖后,让开靠**逐个控件**在事件层判断,不靠几何切割:\n * 切割要测量每个控件的位置,控件一改位置就失效;逐个控件让开是结构性的。\n *\n * 判据是「这个元素自己就代表一次用户操作」,分三类:\n * - 原生可交互元素与原生可点容器(button / input / label / a[href] / summary …)\n * - 显式可编辑(contenteditable)\n * - 交互语义 role(组件库自绘的 checkbox、tab、menuitem 等没有原生 tag)\n * 另加「作者显式给了可聚焦 tabindex」:自定义控件常只靠 tabindex 暴露可操作性。\n *\n * 方向是**保守让开**:判不出来就让开。让开的代价是那一小块起不了拖(用户多点一下),\n * 判错的代价是控件失效(A6 那条回归),两者不对等。\n */\nconst HEADER_FOOTER_INTERACTIVE_SELECTOR = [\n 'button',\n 'input',\n 'select',\n 'textarea',\n 'label',\n 'summary',\n 'a[href]',\n 'audio[controls]',\n 'video[controls]',\n '[contenteditable]:not([contenteditable=\"false\"])',\n '[tabindex]:not([tabindex=\"-1\"])',\n '[role=\"button\"]',\n '[role=\"link\"]',\n '[role=\"checkbox\"]',\n '[role=\"switch\"]',\n '[role=\"radio\"]',\n '[role=\"tab\"]',\n '[role=\"menuitem\"]',\n '[role=\"menuitemcheckbox\"]',\n '[role=\"menuitemradio\"]',\n '[role=\"option\"]',\n '[role=\"textbox\"]',\n '[role=\"combobox\"]',\n '[role=\"slider\"]',\n '[role=\"spinbutton\"]'\n].join(',')\n\n/*\n * 释放后的收尾(弹回打开位 / 滑出到闭合位)由 CSS transition 接管(issue #123):\n * 拖拽期间内联 transform + `transition: none`,松手时把终值与过渡参数写入内联并在\n * 同一次样式重算里解除抑制,回弹即唯一的一次 CSS 过渡。全程无 `element.animate()`、\n * 无 fill、无 JS→CSS 动画交接边界,因此不存在 Safari 采不到 before-change style 的\n * 窗口。\n *\n * 代价是没有弹簧的速度感知轨迹:释放速度只用于估算过渡时长,过冲手感由缓动曲线\n * 近似。两条曲线对应弹簧预设的阻尼比——close(ζ≈1.05)无过冲,\n * rebound(ζ≈0.74)保留约 3% 过冲。\n */\nconst SETTLE_MIN_MS = 180\nconst SETTLE_MAX_MS = 420\nconst SETTLE_EASE_CLOSE = 'cubic-bezier(0.32, 0.72, 0, 1)'\n// 过冲量由 y1 决定:1.3 时峰值 ≈2.99%,与 ζ≈0.74 的理论过冲(3.15%)对齐;1.12 只剩 0.37%。\nconst SETTLE_EASE_REBOUND = 'cubic-bezier(0.22, 1.3, 0.36, 1)'\n// 估算时长的速度下限(px/s):静止释放时退化为最大时长,避免除零与无限时长。\nconst SETTLE_MIN_VELOCITY = 1\n// transitionend 兜底定时器的宽限:覆盖一帧的调度抖动。\nconst SETTLE_GRACE_MS = 80\n\n/*\n * 拖拽期间由 JS 写入的 backdrop 透明度变量参与 WAAPI 关键帧。未注册的自定义属性在\n * 关键帧之间是离散插值(半帧跳变),注册为 <number> 后获得线性插值。\n * inherits 必须为 true:::backdrop 只从 originating element 继承「可继承属性」,\n * 注册为不可继承会让变量到不了 backdrop,跟手淡出整体失效(Chrome 151 实证)。\n * 注册表是全局的,模块重复执行(HMR)时捕获已注册错误并忽略。\n */\nif (typeof CSS !== 'undefined' && 'registerProperty' in CSS) {\n try {\n CSS.registerProperty({\n name: '--wui-internal-drag-backdrop-opacity',\n syntax: '<number>',\n inherits: true,\n initialValue: '1'\n })\n } catch {\n // 已注册(如 HMR 重复执行)时忽略\n }\n /*\n * 公开 token 注册为 <length>:Consumer 传 unitless 0 时计算值被归一为 0px,\n * 避免 calc(100% + 0) 因 number/percentage 不兼容而使闭合位移失效。\n */\n try {\n CSS.registerProperty({\n name: '--wui-drawer-inset',\n syntax: '<length>',\n inherits: true,\n initialValue: '8px'\n })\n } catch {\n // 已注册(如 HMR 重复执行)时忽略\n }\n /*\n * 嵌套层叠对数曲线的基准 A(shift = A · ln(depth + 1)):注册为 <length> 后\n * 桌面基准只有这一个真相源,JS 从 computed style 读到的永远是确定值;\n * 窄视口基准由 style.css 的媒体查询覆盖同一 token。消费方写 unitless 的\n * `0` 会在计算值阶段归一成 `0px`(层叠露边归零,而不是让 ln 项算出 NaN)。\n *\n * A = 43.2px 来自视觉 pass:四层等宽 320px 时逐层露边\n * 29.94 / 17.52 / 12.43px,总堆叠宽 59.89px。\n *\n * 取值由**最里层**那一跳决定:对数曲线压得最狠的正是第四层,它只有\n * A·ln(4/3) = 0.288A。12.43px 略高于 12px 的线性步进,但余量已经收窄到\n * 3.5%——继续下调会让最里层那一条边与上一条边难以分辨。若还要更小的基准,\n * 应先改 ln 的压缩比而不是只降 A。\n */\n try {\n CSS.registerProperty({\n name: '--wui-drawer-nested-peek-base',\n syntax: '<length>',\n inherits: true,\n initialValue: '43.2px'\n })\n } catch {\n // 已注册(如 HMR 重复执行)时忽略\n }\n /*\n * 收尾过渡的时长:注册为 <time> 后 ::backdrop 也能继承到同一值。不注册时变量在\n * transition 简写里只是 token 替换,值一旦无效整条声明在计算值阶段失效\n * (退化为 transition: none),回弹会变成瞬移。\n * 缓动不注册:CSS 属性语法没有 <easing-function>,且它不需要插值,靠 var() 的\n * 兜底值即可。\n */\n try {\n CSS.registerProperty({\n name: '--wui-internal-settle-duration',\n syntax: '<time>',\n inherits: true,\n initialValue: '280ms'\n })\n } catch {\n // 已注册(如 HMR 重复执行)时忽略\n }\n}\n\n@customElement('web-ui-drawer')\nexport class WebUiDrawer 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, attribute: 'no-scroll-lock' }) noScrollLock = false\n @property({ type: Boolean, reflect: true, attribute: 'no-backdrop-close' }) noBackdropClose = false\n\n /**\n * Controlled 模式下,Escape、backdrop、关闭 button 和拖拽关闭只派发 `open-change` 请求,\n * 不会自行修改 `open`。Consumer 回写 `open` 后才执行关闭动画。\n * 程序化 API(show/close/直接赋值 open)不受此模式影响,始终直通。\n */\n @property({ type: Boolean, reflect: true }) controlled = false\n\n @property({ type: String, reflect: true })\n get placement(): DrawerPlacement {\n return this._placement\n }\n set placement(v: string) {\n const old = this._placement\n this._placement = normalizeLiteral(v, ALLOWED_PLACEMENTS, 'right')\n this.requestUpdate('placement', old)\n }\n private _placement: DrawerPlacement = 'right'\n\n // 标题文字(未传 header slot 时显示默认 header)\n @property({ type: String }) heading = ''\n\n @property({ type: Boolean, reflect: true }) closable = false\n\n /**\n * Headless 模式:只保留 overlay 基础设施(backdrop、动画、scroll lock、dialog 语义),\n * 移除内置 UI(glass 样式、header、close button、footer)。Consumer 自定义内容样式。\n */\n @property({ type: Boolean, reflect: true }) headless = false\n\n /**\n * 内部原生 dialog 的 accessible name。headless 模式必须由 Consumer 提供,\n * 因为该模式不会渲染可自动关联的内置 header。\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 /**\n * 启用拖拽关闭:打开态在 drawer 内缘渲染 drag bar(灰色胶囊 + 加宽 drag zone),\n * 指针拖拽实时跟手,松手按位移与甩动速度判定关闭或弹回。\n * 关闭态基于原生 dialog 无可见渲染物,因此不支持从关闭态拖拽打开。\n */\n @property({ type: Boolean, reflect: true }) override draggable = false\n\n private get dialog() {\n return this.shadowRoot?.querySelector('dialog') ?? null\n }\n\n private _hasHeaderSlot = false\n private _hasFooterSlot = false\n private readonly _userOpenChange = new UserChangeController()\n /*\n * 上下 placement 的拖拽 drag zone 要让开头尾两节(见 style.css 的 --wui-internal-drawer-* 说明):\n * `bottom` 贴顶边、drag zone 让开 header,`top` 贴底边、drag zone 让开 footer。让开的量就是那一节的\n * border-box 高度,写成 host 上的自定义属性交给 CSS 去算偏移——样式侧因此不必知道\n * header / footer 的存在与高度,两条 placement 规则各只读自己的那个属性。\n *\n * 用 offsetHeight 而非 CSS 侧推导:`[hidden]` 的节天然是 0(让位量归零,drag zone 回到 panel 边缘),\n * 而「header 存在但高度为 0」与「没有 header」在布局上是同一件事,不需要额外的存在性 toggle。\n * 高度是动态的(consumer 的 padding、窄屏断点改写、slotted 内容换行),所以观察而非读取一次。\n */\n private readonly _headerHeight = new ElementHeightController(this, '.wui-drawer-header', height =>\n this._setAvoidance('--wui-internal-drawer-header-inset', height)\n )\n private readonly _footerHeight = new ElementHeightController(this, '.wui-drawer-footer', height =>\n this._setAvoidance('--wui-internal-drawer-footer-inset', height)\n )\n\n /*\n * 让位量 = 该节高度 + 一点呼吸间距,且**只在真有那一节时**才加间距。\n *\n * 间距写成属性值里的 calc 而不是让 CSS 无条件相加,是因为「没有 header」这一态必须精确等于\n * 让位量 0 的几何(drag zone 顶边贴 panel 顶边)。若让间距无条件生效,无 header 的 drawer 会\n * 平移 4px,drag zone 端边与 panel 端边的贴合关系跟着变——那是\n * `header-drag-surface.browser.spec.ts` 的 R2 钉住的关系,这条护栏不该让步。\n * 间距本身仍以 token 暴露,真机调参不必改代码。\n */\n private _setAvoidance(property: string, height: number) {\n this.style.setProperty(\n property,\n height > 0 ? `calc(${height}px + var(--wui-drawer-drag-zone-inset, var(--wui-space-1, 4px)))` : '0px'\n )\n }\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 // nested 层序:打开后纳入全局栈,上层打开/关闭时本层缩放平移。\n private readonly _nestedLayers = defineNestedDrawerLayers().make({\n getDialog: () => this.dialog,\n getPlacement: () => this._placement\n })\n /*\n * 开启态 overlay(issue #120 Block 1)。drawer 把自己的原生 <dialog> 登记为 panel,使挂在\n * dialog 上的 portal panel(select / dropdown / tooltip)在逻辑组合树上成为它的后代,\n * 仲裁者据此选出最内层。\n *\n * 拖拽进行中不响应 Escape,但**仍是候选**:仲裁者照旧吞掉按键并压掉原生 cancel,\n * 只是不走关闭入口。关闭入口只有这一个执行者。\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\n private _dragGesture: DragGestureHandle | null = null\n // pointerdown 时刻已存在的闭合方向位移(从弹回动画中抓取时非 0)。\n private _dragInitialOffset = 0\n private _dragOffset = 0\n // 位移零点与判定时钟由手势层的 `calibrateOnFirstMove` 校准到首个 pointermove,\n // 这里只保留「改主意」相关的追踪状态。\n // 折返点(轴坐标):随反向移动拉到当前坐标。\n private _cancelBaseline = 0\n // 上一次 move 的轴坐标,用于求移动量。\n private _lastAxisPos = 0\n // 方向确认时的位移;负值表示方向尚未确认。\n private _swipeIntentDisp = -1\n // 已判定为「改主意」,甩动分支据此拒绝关闭。\n private _swipeCancelled = false\n // 释放后的 CSS transition 收尾:完成回调,以及终值与当前值相同时\n // 不会派发 transitionend 的兜底定时器。\n private _settleFinish: (() => void) | null = null\n private _settleTimer: ReturnType<typeof setTimeout> | undefined\n // controlled:弹簧到闭合位后等待 Consumer 回写 open;超时未回写则弹回。\n private _dragAwaitWriteback = false\n private _dragRequestTimer: ReturnType<typeof setTimeout> | undefined\n\n private get _dragAxis(): 'x' | 'y' {\n return this._placement === 'left' || this._placement === 'right' ? 'x' : 'y'\n }\n\n // 位移正方向 = drawer 的闭合方向(拖出屏幕为正)。\n private get _dragCloseSign(): number {\n switch (this._placement) {\n case 'right':\n return 1\n case 'left':\n return -1\n case 'top':\n return -1\n case 'bottom':\n return 1\n }\n }\n\n private _measureDragSize(): number {\n const dialog = this.dialog\n if (!dialog) return 0\n return this._dragAxis === 'x' ? dialog.offsetWidth : dialog.offsetHeight\n }\n\n // 关闭距离阈值:尺寸的一半,下限 10px。\n // 胶囊的 accent 视觉确认与实际判定共用同一阈值,两者不会漂移。\n private _dragCloseThreshold(size: number): number {\n return Math.max(size * DRAG_CLOSE_RATIO, DRAG_MIN_CLOSE_DISTANCE)\n }\n\n // 浮动卡片(非 headless)的四周留边;闭合位移需越过它才能完全滑出视口。\n // headless 在 :host([headless]) dialog 上显式归零(防嵌套继承),解析失败回退 0。\n private _readDrawerInset(dialog: HTMLDialogElement): number {\n const raw = getComputedStyle(dialog).getPropertyValue('--wui-internal-drawer-inset')\n const parsed = Number.parseFloat(raw)\n return Number.isFinite(parsed) ? parsed : 0\n }\n\n // 闭合方向上的完全出屏距离:drawer 尺寸 + 浮动留边(headless 下即尺寸本身)。\n // CSS 闭合态 transform、controlled 悬停终态与弹簧终点共用同一数学,避免衔接跳变。\n private _dragCloseDistance(dialog: HTMLDialogElement): number {\n return this._measureDragSize() + this._readDrawerInset(dialog)\n }\n\n private _isDragging(): boolean {\n return this._dragGesture?.isDragging() ?? false\n }\n\n /*\n * 拖拽期间本层仍是候选:仲裁者照旧 preventDefault(压掉原生 <dialog> 的 cancel)与\n * stopPropagation,只是不调用 requestClose。`isDragging()` 的翻转由手势层内部决定,\n * 所以随 move 与各终结路径同步(幂等)。除拖拽外本层没有别的惰性来源。\n */\n private _syncDragInert() {\n this._handle?.setInert(this._isDragging())\n }\n\n private _cancelDragAwait() {\n if (this._dragRequestTimer !== undefined) {\n clearTimeout(this._dragRequestTimer)\n this._dragRequestTimer = undefined\n }\n this._dragAwaitWriteback = false\n }\n\n /**\n * 上下 placement 的 header / footer 自己接拖拽手势。\n *\n * 为什么不是「把 drag zone 铺到 header 上」:那样 header 里的控件会被透明命中层吞掉,\n * 正是 A6 修掉的那条缺陷。这里在事件层让开——控件照常收自己的事件,拖拽只在\n * 「落点不是可点控件」时起手。\n *\n * 三道闸门任一不成立就不接管:\n * 1. placement 必须是上下。左右 placement 的拖拽轴是 x,端头在竖边上,header\n * 既不在可抓边缘上也不该起手(R4 的行为侧)。\n * 2. 下层嵌套 drawer 整套拖拽提示一起退场。drag zone 与 drag edge 靠 CSS 的 pointer-events 退场,\n * 事件层必须自己判同一条,否则 header 会成为唯一一条漏网的入口。\n * 3. 落点路径里有可点控件就让开——控件优先于「这一块可拖」。\n */\n private _handleSectionDragPointerDown(e: PointerEvent) {\n if (this._placement !== 'top' && this._placement !== 'bottom') return\n const section = (e.currentTarget as HTMLElement | null)?.closest('.wui-drawer-header, .wui-drawer-footer')\n const isHeader = section?.classList.contains('wui-drawer-header') ?? false\n // 只有**贴着可抓边缘**的那一节接手势:bottom 的 header、top 的 footer。\n // 另一节在 panel 远端,在那里起手却朝闭合方向拖是反直觉的,也不属于「端头」——\n // 与 drag zone 只让开同一侧那节是同一条边界。\n if (this._placement === 'bottom' && !isHeader) return\n if (this._placement === 'top' && isHeader) return\n // 下层嵌套 drawer 整套拖拽提示一起退场:drag zone 与 drag edge 靠 CSS 的 pointer-events 退场,\n // 事件层必须判同一条,否则这里是唯一一条漏网的入口。\n if (this.dialog?.classList.contains('is-nested-lower')) return\n if (this._isInteractivePath(e)) return\n this._handleDragPointerDown(e)\n }\n\n /**\n * 按下路径上是否出现可点控件。\n *\n * 走 composedPath 而不是 event.target:消费侧 slotted 控件与组件库自绘控件都各有\n * shadow root,target 只给出最内层那个节点,沿 parentElement 上行会在 shadow 边界\n * 断掉,控件自己那层反而看不见。\n */\n private _isInteractivePath(e: PointerEvent): boolean {\n const path = typeof e.composedPath === 'function' ? e.composedPath() : [e.target]\n for (const node of path) {\n if (node instanceof Element && node.matches(HEADER_FOOTER_INTERACTIVE_SELECTOR)) return true\n }\n return false\n }\n\n private _handleDragPointerDown(e: PointerEvent) {\n if (!this.open || !this.draggable || this._isDragging()) return\n // 多点触控的副指针(isPrimary 明确为 false)不参与手势。\n if (e.isPrimary === false) return\n\n const dialog = this.dialog\n if (!dialog) return\n // is-visible 由 presence 在 showModal 后一帧加上;就位前(打开极早期)忽略。\n // 就位后即使 enter 过渡仍在进行也允许抓取,起始位移从当前计算值续接。\n if (!dialog.classList.contains('is-visible')) return\n\n // 先读取动画中的当前位移再取消弹回动画,避免取消后回跳到内联样式值。\n const currentTransform = getComputedStyle(dialog).transform\n const axisValue =\n currentTransform && currentTransform !== 'none' && typeof DOMMatrixReadOnly === 'function'\n ? new DOMMatrixReadOnly(currentTransform)[this._dragAxis === 'x' ? 'm41' : 'm42']\n : 0\n // 收尾过渡进行中被重新抓取:丢弃收尾状态(不执行其回调,避免它回头写内联),\n // 位移已在下面按当前计算值续接。\n this._abortSettle()\n this._cancelDragAwait()\n\n this._dragInitialOffset = axisValue * this._dragCloseSign\n this._dragOffset = this._dragInitialOffset\n // 判定基准复位:折返点从按下位置起算。\n const axisPos = this._dragAxis === 'x' ? e.clientX : e.clientY\n this._cancelBaseline = axisPos\n this._lastAxisPos = axisPos\n this._swipeIntentDisp = -1\n this._swipeCancelled = false\n dialog.classList.add('is-dragging')\n // 收尾过渡进行中重新抓取时内联还停在收尾终值(打开位/闭合位),不写回当前位移\n // 会在抑制生效的瞬间跳到终值。写回后计算值 == 抓取瞬间的视觉位置,无跳变。\n this._applyDragOffset(dialog, this._dragOffset)\n\n this._dragGesture = attachDragGesture(e, {\n axis: this._dragAxis,\n // 拖拽零点与判定时钟一起重置到首个 move。\n calibrateOnFirstMove: true,\n onMove: info => {\n // info 的 delta/duration 已以校准点为零点。\n const pointerDelta = this._dragAxis === 'x' ? info.deltaX : info.deltaY\n // 本次手势自校准点起的**增量**(朝闭合方向为正,未阻尼)。\n const dragDelta = pointerDelta * this._dragCloseSign\n // 阻尼只作用于增量,再叠加抓取瞬间的基准位移。对总位移做阻尼会在「从弹回中\n // 重新抓取」时差出可见位移——基准位移本身会被一起压缩,元素不再接着视觉位置走。\n this._dragOffset = this._dragInitialOffset + dampOverscroll(dragDelta)\n this._trackSwipeCancel(this._dragAxis === 'x' ? info.clientX : info.clientY, dragDelta)\n\n // 达到关闭阈值时胶囊换成更实的中性灰作视觉确认。与距离分支共用同一量\n //(自抓取瞬间起的净位移)与同一阈值,视觉确认与实际判定不会漂移。\n const dragSize = this._measureDragSize()\n const dragDisplacement = this._dragOffset - this._dragInitialOffset\n dialog.classList.toggle('is-drag-close', dragDisplacement > this._dragCloseThreshold(dragSize))\n this._applyDragOffset(dialog, this._dragOffset)\n },\n onEnd: info => {\n this._dragGesture = null\n this._syncDragInert()\n dialog.classList.remove('is-dragging', 'is-drag-close')\n const size = this._measureDragSize()\n const velocity = (this._dragAxis === 'x' ? info.velocityX : info.velocityY) * this._dragCloseSign\n // 净位移从抓取瞬间的位移起算(弹回/收尾过渡中被重新抓取时起点非 0)。\n const displacement = this._dragOffset - this._dragInitialOffset\n // 时长同样自校准点(首个 move)起算,与位移零点同源:按下后的停顿不参与分母。\n // 分母下限 50ms;时长量为 0(首尾时间戳相同,或事件时间戳不可用/倒退)时速度取\n // 0——直接按 50ms 兜底会把「测不到时长」放大成一次甩动。\n const netVelocity =\n info.duration > 0 ? (displacement / Math.max(info.duration, DRAG_MIN_VELOCITY_SPAN_MS)) * 1000 : 0\n // 甩动判据:净位移朝闭合方向(`> 0`)、平均速度达到阈值(`>=`)、\n // 且本次手势未被判定为「改主意」。\n const flicked = displacement > 0 && netVelocity >= DRAG_FLICK_VELOCITY && !this._swipeCancelled\n // 距离分支同样用净位移,不是绝对位置:抓取瞬间停在哪里都不算「已经拖过一半」。\n // `_dragOffset` 只留给收尾的起点使用。\n const shouldClose = displacement > this._dragCloseThreshold(size) || flicked\n\n if (shouldClose) this._settleToClose(dialog, velocity)\n else this._settleRebound(dialog, velocity, this._dragOffset)\n },\n onCancel: () => {\n this._dragGesture = null\n this._syncDragInert()\n dialog.classList.remove('is-dragging', 'is-drag-close')\n this._settleRebound(dialog, 0, this._dragOffset)\n }\n })\n /*\n * 惰性必须与 attach 同拍。漏掉这里,按下到手势终结之间那一整段里 Escape 仍会关闭\n * drawer——push 通道没有自动同步,每个翻转点自己推一次。\n *\n * `isDragging()` 的翻转点不止 attach 与 onEnd:手势层的 `handlePointerMove` 在越过死区后\n * 若判定轴向冲突(限定了 x 轴却明显纵向移动,或反之)会直接 `cancel()`,\n * `lostpointercapture` 也会终结手势。这些路径都走 `onCancel`,所以 `onCancel` 里那次\n * `_syncDragInert()` **不是**冗余——删掉它,「移动中取消拖拽」会让 drawer 重新变回可被\n * Escape 关闭。\n */\n this._syncDragInert()\n }\n\n /*\n * 「改主意」守卫。\n *\n * 折返点随「反向移动」被拉到当前坐标,因此「自折返点起的位移」度量的是**自最近一次\n * 折返之后**回撤了多少:一旦它比方向确认时的位移少满 10px,本次手势即被标记为\n * 「改主意」,甩动分支据此拒绝关闭;位移重新越过关闭距离阈值即清除标记,\n * 即「已经拖过一半」不受回撤影响。\n *\n * @param axisPos 本次 move 在主轴上(`_dragAxis`)的指针坐标\n * @param displacement 自校准点起、朝闭合方向的**未阻尼**位移\n */\n private _trackSwipeCancel(axisPos: number, displacement: number) {\n const movement = axisPos - this._lastAxisPos\n this._lastAxisPos = axisPos\n if ((movement < 0 && axisPos > this._cancelBaseline) || (movement > 0 && axisPos < this._cancelBaseline)) {\n this._cancelBaseline = axisPos\n }\n\n if (this._swipeIntentDisp < 0) {\n // 方向确认:首次朝闭合方向累积出位移。\n if (displacement > 0) this._swipeIntentDisp = displacement\n return\n }\n\n const cancelDisplacement = (axisPos - this._cancelBaseline) * this._dragCloseSign\n if (cancelDisplacement > this._dragCloseThreshold(this._measureDragSize())) {\n this._swipeCancelled = false\n } else if (this._swipeIntentDisp - cancelDisplacement >= DRAG_REVERSE_CANCEL_THRESHOLD) {\n this._swipeCancelled = true\n }\n }\n\n // 受控状态写入等外部原因强制终结拖拽:清手势状态与拖拽样式,不弹回,\n // 后续管线(关闭/打开)由调用方继续执行。\n private _cancelActiveDrag() {\n if (!this._isDragging()) return\n this._dragGesture?.destroy()\n this._dragGesture = null\n this._syncDragInert()\n const dialog = this.dialog\n if (!dialog) return\n dialog.classList.remove('is-dragging', 'is-drag-close')\n this._clearDragStyles(dialog)\n }\n\n // 将闭合方向位移映射为 dialog transform,并同步 backdrop 透明度保持跟手反馈。\n private _applyDragOffset(dialog: HTMLDialogElement, offset: number) {\n const sign = this._dragCloseSign\n const value = offset * sign\n dialog.style.transform = this._dragAxis === 'x' ? `translateX(${value}px)` : `translateY(${value}px)`\n const size = this._measureDragSize()\n const progress = size > 0 ? Math.min(1, Math.max(0, offset / size)) : 0\n dialog.style.setProperty('--wui-internal-drag-backdrop-opacity', String(1 - progress))\n }\n\n private _clearDragStyles(dialog: HTMLDialogElement) {\n dialog.style.removeProperty('transform')\n dialog.style.removeProperty('--wui-internal-drag-backdrop-opacity')\n }\n\n // 由 CSS transition 收尾到完全闭合;controlled 下保持闭合位等待回写,其余走常规关闭管线。\n private _settleToClose(dialog: HTMLDialogElement, velocity: number) {\n const from = this._dragOffset\n this._dragOffset = 0\n // 终点 = 完全出屏距离(含浮动留边),与 CSS 闭合态/悬停终态一致,\n // 否则收尾结束后会有一个留边宽度的瞬移。\n const to = this._dragCloseDistance(dialog)\n\n const finishClose = () => {\n if (this.controlled) {\n // 保持在闭合位(is-visible 未移除,状态仍 open),等待 Consumer 回写或超时弹回。\n this._applyDragOffset(dialog, to)\n } else {\n // 移除 is-visible 后基础 transform 即闭合位,清内联样式不产生跳变;\n // presence.sync(false) 检测不到 is-visible 会立即完成关闭,不重播退出动画。\n dialog.classList.remove('is-visible')\n this._clearDragStyles(dialog)\n }\n this._closeFromDrag()\n }\n\n if (prefersReducedMotion(this) || Math.abs(to - from) < 1) {\n // 与 _settleRebound 的无动画路径对称:自己解除抑制,不依赖调用方清过类。\n dialog.classList.remove('is-dragging')\n finishClose()\n return\n }\n\n this._startSettle(\n dialog,\n to,\n 0,\n this._settleDuration(Math.abs(to - from), velocity),\n SETTLE_EASE_CLOSE,\n finishClose\n )\n }\n\n // 弹回打开位:从 from 位移回到 0;结束后把内联样式交还 CSS 打开态。\n private _settleRebound(dialog: HTMLDialogElement, velocity: number, from: number) {\n this._dragOffset = 0\n\n // 收尾结束必须清掉内联 transform:translateX(0px) 内联会盖住闭合态 CSS transform,\n // 之后用 button/backdrop/Esc 关闭时计算值恒为 0、开与关都不再触发过渡。\n const finishRebound = () => {\n this._clearDragStyles(dialog)\n }\n\n if (prefersReducedMotion(this) || Math.abs(from) < 1) {\n // 无动画路径:直接移除内联回到 CSS 打开位。计算值就是可信的打开位 0,\n // 不存在 WAAPI fill 覆盖,无需强制 reflow「烘焙」。\n dialog.classList.remove('is-dragging')\n finishRebound()\n return\n }\n\n this._startSettle(dialog, 0, 1, this._settleDuration(Math.abs(from), velocity), SETTLE_EASE_REBOUND, finishRebound)\n }\n\n // 释放速度只影响收尾时长:以它走完剩余距离的时间作为估计,慢放长、甩动短。\n private _settleDuration(distance: number, velocity: number): number {\n const ms = (distance / Math.max(Math.abs(velocity), SETTLE_MIN_VELOCITY)) * 1000\n return Math.min(SETTLE_MAX_MS, Math.max(SETTLE_MIN_MS, ms))\n }\n\n /*\n * 把释放后的收尾交给 CSS transition:写入终值与「时长/缓动」两个内部变量,并在\n * 同一次样式重算里解除抑制(is-dragging → is-settling)。过渡的 before-change\n * style 就是拖拽期间写入的内联值——没有 WAAPI 覆盖层,各引擎采样一致,这正是\n * 对 Safari「不采信 fill 覆盖」怪癖免疫的原因。\n */\n private _startSettle(\n dialog: HTMLDialogElement,\n offset: number,\n backdropOpacity: number,\n duration: number,\n easing: string,\n onDone: () => void\n ) {\n // 覆盖前先丢弃上一次收尾:否则它的兜底定时器会提前结束这一次的过渡。\n this._abortSettle()\n const sign = this._dragCloseSign\n dialog.style.setProperty('--wui-internal-settle-duration', `${duration}ms`)\n dialog.style.setProperty('--wui-internal-settle-easing', easing)\n dialog.style.transform =\n this._dragAxis === 'x' ? `translateX(${offset * sign}px)` : `translateY(${offset * sign}px)`\n dialog.style.setProperty('--wui-internal-drag-backdrop-opacity', String(backdropOpacity))\n dialog.classList.add('is-settling')\n dialog.classList.remove('is-dragging')\n\n this._settleFinish = onDone\n // 终值与当前值相同时不会派发 transitionend(也没有可见变化),用定时器兜底。\n this._settleTimer = setTimeout(() => this._finishSettle(), duration + SETTLE_GRACE_MS)\n }\n\n // 收尾过渡结束(transitionend 或兜底超时):先撤 settle 状态再执行收尾。\n private _finishSettle() {\n const finish = this._settleFinish\n if (!finish) return\n this._abortSettle()\n finish()\n }\n\n // 丢弃进行中的收尾(重新抓取、强制关闭、断连):只撤状态,不执行收尾回调。\n private _abortSettle() {\n this._settleFinish = null\n if (this._settleTimer !== undefined) {\n clearTimeout(this._settleTimer)\n this._settleTimer = undefined\n }\n const dialog = this.dialog\n if (!dialog) return\n dialog.classList.remove('is-settling')\n dialog.style.removeProperty('--wui-internal-settle-duration')\n dialog.style.removeProperty('--wui-internal-settle-easing')\n }\n\n private _closeFromDrag() {\n // 先派发关闭请求(_closeFromUser),再进入悬停等待:等待态中 _closeFromUser\n // 会被 L2 去重守卫短路,顺序颠倒会吞掉首次请求。\n this._closeFromUser()\n if (this.controlled) {\n this._dragAwaitWriteback = true\n this._dragRequestTimer = setTimeout(this._handleDragWritebackTimeout, DRAG_REQUEST_WINDOW_MS)\n }\n }\n\n // controlled 回写窗口超时:Consumer 拒绝关闭,从闭合位弹回打开位。\n private readonly _handleDragWritebackTimeout = () => {\n this._dragRequestTimer = undefined\n if (!this._dragAwaitWriteback || !this.open || this._isDragging()) {\n this._dragAwaitWriteback = false\n return\n }\n this._dragAwaitWriteback = false\n\n const dialog = this.dialog\n if (!dialog) return\n dialog.classList.add('is-visible')\n // 悬停终态位于完全出屏位(含留边),弹回也从该真实位置起步,避免首帧内跳。\n this._settleRebound(dialog, 0, this._dragCloseDistance(dialog))\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this._resyncSlotPresence()\n // 重挂载对账:断连时 presence、滚动锁、nested 层序与登记都已被撤销,而\n // `open` 未变化时 `updated()` 不会补跑任何 sync 分支。首次连接时 shadow 尚未\n // 渲染、`this.dialog` 为 null,三种情况都直接跳过;打开态的首次进入仍由\n // updated() 的 `props.has('open')` 分支处理。\n this._presence.reconcile()\n if (this.open) {\n // presence.reconcile 已让打开态的 dialog 同步回到 top layer(此刻 dialog.open\n // 为真),层序 depth 可正确计数,与 updated() 的 register 时机一致。\n this._nestedLayers.register()\n }\n this._syncScrollLock()\n this._reclaimIfOpen()\n }\n\n override firstUpdated() {\n this._checkSlotContent('footer')\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._nestedLayers.dispose()\n this._dragGesture?.destroy()\n this._dragGesture = null\n this._presence.dispose()\n this._scrollLock.release()\n // 断连即撤销登记,重连后由 _reclaimIfOpen 显式重新声明。\n this._handle?.release()\n this._handle = null\n this._abortSettle()\n this._cancelDragAwait()\n }\n\n /** 「重挂载恢复」的调用方一半:模块刻意不观察 DOM 连接状态。 */\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._syncDragInert()\n }\n\n /**\n * 对账 header / footer slot 的有无,**只在真的变了时排一次更新**。\n *\n * 排更新是这条对账不可省的一半:`showHeader` 与 footer 的 `?hidden` 只在 render 里求值,\n * 而 Lit 的 `connectedCallback` 只做 `enableUpdating(true)` 与 `setConnected(true)`,本身不排\n * 更新。两条 slotchange 处理器也接不住这条路径——slotchange 只在**分配集合**变化时派发,\n * 断连期间清空槽位内容时宿主节点本身没动,事件不触发(Chromium 实测)。\n *\n * 漏掉这一半的后果:断连前有 footer、重连时已清空的 drawer 留下一节空的 footer。它在 style.css\n * 的 padding 下仍有真实高度(实测 32px),于是底部一条点不动的死带,拖拽 drag zone 的让位量也照\n * 它算而不归零。\n *\n * 判据取 light DOM 子节点而非 `slot.assignedNodes()`:首次连接时 shadow 尚未渲染,slot 元素\n * 还不存在,那一侧会直接跳过(首次连接的 presence 由 `firstUpdated()` 与 slotchange 兜底);\n * 这里要的正是重连对账——shadow root 跨断连保留,两侧都可用,但只有子节点扫描不依赖渲染\n * 时序。「只在真变了才排」与两条 slotchange 处理器同一纪律,避免每次重连都多一次空渲染。\n */\n private _resyncSlotPresence() {\n const hasHeader = this._hasSlottedChild('header')\n const hasFooter = this._hasSlottedChild('footer')\n if (hasHeader === this._hasHeaderSlot && hasFooter === this._hasFooterSlot) return\n this._hasHeaderSlot = hasHeader\n this._hasFooterSlot = hasFooter\n this.requestUpdate()\n }\n\n private _hasSlottedChild(name: 'header' | 'footer') {\n return Array.from(this.children).some(child => child.getAttribute('slot') === name)\n }\n\n private _checkSlotContent(name: string) {\n const slot = this.shadowRoot?.querySelector(`slot[name=\"${name}\"]`) as HTMLSlotElement | null\n if (!slot) return\n const has = slot.assignedNodes().length > 0\n if (name === 'footer' && has !== this._hasFooterSlot) {\n this._hasFooterSlot = has\n this.requestUpdate()\n }\n }\n\n private handleHeaderSlotChange(e: Event) {\n const has = (e.target as HTMLSlotElement).assignedNodes().length > 0\n if (has !== this._hasHeaderSlot) {\n this._hasHeaderSlot = has\n this.requestUpdate()\n }\n }\n\n private handleFooterSlotChange(e: Event) {\n const has = (e.target as HTMLSlotElement).assignedNodes().length > 0\n if (has !== this._hasFooterSlot) {\n this._hasFooterSlot = has\n this.requestUpdate()\n }\n }\n\n protected override updated(props: PropertyValues) {\n super.updated(props)\n if (!this.isConnected) return\n\n if (props.has('open')) {\n // Consumer 在拖拽进行中写入受控 open:受控状态优先,立即终结手势(等同\n // pointercancel 的清理但不弹回),交由下方 presence 走标准关闭/打开管线。\n if (this._isDragging() && !this.open) this._cancelActiveDrag()\n if (this._userOpenChange.consume()) this.emitOpenChange()\n // controlled 拖拽关闭的回写结果:确认关闭则清除闭合位悬停状态走正常关闭;\n // 拒绝关闭(回写 open=true)则从闭合位(含留边)弹回。\n if (this._dragAwaitWriteback) {\n this._cancelDragAwait()\n const dialog = this.dialog\n if (dialog && !this.open) {\n dialog.classList.remove('is-visible')\n this._clearDragStyles(dialog)\n } else if (dialog && this.open) {\n this._settleRebound(dialog, 0, this._dragCloseDistance(dialog))\n }\n }\n // 关闭且无进行中的拖拽/收尾时,清理可能残留的内联拖拽样式(如收尾路径写在\n // 打开位的 translateX(0px))。若不清理,0px 内联会盖住闭合态 CSS transform,\n // 后续开/关都不再触发过渡。收尾进行中由 _finishSettle 自行收尾,这里丢弃它\n // 以免回调回头写内联与关闭管线竞争。\n if (!this.open && !this._isDragging()) {\n this._abortSettle()\n const dialog = this.dialog\n if (dialog) this._clearDragStyles(dialog)\n }\n this._presence.sync(this.open)\n if (this.open) {\n // presence.sync 内的 showModal() 把 dialog 提升进 top layer。双击手势本身\n // 留下的活选区会被浏览器拿去和提升后的新布局重新解析,结果选区落到刚挂载的\n // drawer 正文上。脆弱的是「手势来源的活选区」,不是 dblclick 的判定窗口:把\n // 打开推迟 250ms(早过双击判定)仍然复现。选区由浏览器在 showModal() 调用\n // 内部生成:脚本没有任何 Selection API 调用,在 dblclick 上 preventDefault\n // 也拦不住。\n //\n // 实测(Chromium)showModal() 不会动打开前已经存在的选区,所以这一次清理\n // 同时也覆盖「先选中页面文本、再打开」的情况——modal 打开后被选中的内容挡在\n // backdrop 后面本来也已不可用。这里只清文档选区,不动 user-select,drawer 内正文\n // 仍可正常拖选;拖拽 handle 与 toggle 时序不受影响。\n window.getSelection()?.removeAllRanges()\n // presence.sync 已同步发起 showModal:此刻 dialog.open 为真,计数出正确\n // 的层序 depth 并驱动下层缩放。直接同步 register,不等待 is-visible\n //(那要再等一帧,且打开过渡期间上层关系已应确立)。\n this._nestedLayers.register()\n // 原生 dialog 登记为开启态 overlay:挂在它上面的 portal panel 在逻辑组合树上\n // 成为本层后代,Escape 仲裁据此判出最内层(issue #120 Block 1)。\n const dialog = this.dialog\n if (dialog) {\n // 同一 panel 重新 claim = 新的一次开启:旧会话(含其子层)整体作废。\n this._handle?.release()\n this._handle = this._overlay.claim(dialog, { ancestryFrom: this })\n this._syncDragInert()\n }\n } else {\n this._nestedLayers.unregister()\n this._handle?.release()\n this._handle = null\n }\n }\n if (props.has('open') || props.has('noScrollLock')) this._syncScrollLock()\n }\n\n private handleTransitionEnd(e: TransitionEvent) {\n // transitionend 会从后代元素冒泡上来(slotted 消费者内容、嵌套子 drawer)。\n // 只处理 dialog 自身的事件:presence 与收尾都只认本层 dialog 的过渡,\n // 后代一条 transform 过渡结束不能提前终结本层收尾(与 presence 内部守卫同规则)。\n if (e.target !== e.currentTarget) return\n this._presence.handleTransitionEnd(e)\n\n // 收尾过渡结束:::backdrop 的 opacity 过渡 target 也是 dialog(带 pseudoElement),\n // 因此再按 propertyName 只认 transform。\n if (e.propertyName === 'transform') this._finishSettle()\n }\n\n private _isNestedDialogEvent(e: KeyboardEvent): boolean {\n const path = e.composedPath()\n const ownDialogIndex = path.indexOf(e.currentTarget as Node)\n return ownDialogIndex > 0 && path.slice(0, ownDialogIndex).some(node => node instanceof HTMLDialogElement)\n }\n\n private handleKeydown(e: KeyboardEvent) {\n if (e.key === 'Tab') {\n if (e.defaultPrevented || this._isNestedDialogEvent(e)) return\n const dialog = this.dialog\n if (dialog) trapTabKey(e, dialog)\n return\n }\n if (e.key !== 'Escape') return\n /*\n * Escape 的归属由共享仲裁者在 document 捕获阶段判定(issue #120 Block 1);命中后\n * 它会 stopPropagation,本 handler 不再执行。此处保留两条兜底:\n * ① defaultPrevented 说明仲裁者已介入,不再重复关闭;\n * ② 兜底路径仍按原逻辑处理,避免「panel 未登记」时 Escape 失灵。\n *\n * 本 handler **不是**可随手删掉的:仲裁者挂在 document 上,只对能跨 shadow\n * 边界的事件生效。实测(overlay-open-owner-260918,2026-09-18)摘掉 `@keydown` 后\n * layout.browser.spec.ts 的两例 Escape 用例立刻转红——它们派发的 keydown 没有\n * `composed: true`,事件止步于 shadow root,到不了 document。\n */\n if (e.defaultPrevented) return\n /*\n * nested 场景防连锁:子 drawer 的 dialog 经 slot 投影在本层 shadow 内,其\n * keydown composed 冒泡路径会再次经过本层 dialog(事件路径含 slot 宿主链)。\n * 若事件传播路径在本层 dialog 之前已存在其他 <dialog>,说明该事件来自子层,本层忽略。\n */\n if (this._isNestedDialogEvent(e)) return\n // 拖拽进行中忽略 ESC,避免手势与关闭管线竞争。\n if (this._isDragging()) {\n e.preventDefault()\n return\n }\n e.preventDefault()\n this._closeFromUser()\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 private readonly _closeFromUser = () => {\n if (!this.open) return\n // controlled 悬停等待回写期间,关闭意图已在途(open-change 已派发):\n // Escape/backdrop/关闭 button 的重复触发不再派发第二次请求,也不与超时弹回竞争。\n if (this._dragAwaitWriteback) return\n if (this.controlled) {\n this.emitOpenChange(false)\n return\n }\n\n this._userOpenChange.mark()\n this.close()\n }\n\n /*\n * 仲裁者在 capture 阶段 preventDefault 了 Escape 的 keydown,UA 因此不再派发原生\n * cancel,但本 handler 仍是「原生 cancel 到达时也要走完整关闭管线」的契约入口,\n * **不可删**。实测(overlay-open-owner-260918,2026-09-18)摘掉 `@cancel` 后\n * drawer.spec.ts 的「no-backdrop-close 存在时 cancel 仍通过关闭过渡退出」与\n * remount-reconcile 的「打开态被移出文档再接回」两例立刻转红。\n */\n private handleCancel(e: Event) {\n // 保留 top layer 直到 CSS 过渡结束,避免原生关闭跳过退出动画。\n e.preventDefault()\n this._closeFromUser()\n }\n\n private handleNativeClose() {\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 // finishClosing 完成全部清理,由 presence 消费并返回 true,不能当成外部关闭把\n // 刚重开的 dialog 关掉——否则重开即被误关,表现为连续 toggle 丢失过渡动画。\n if (this._presence.handleNativeClose()) return\n\n // 悬停等待期间的原生关闭(如表单 method=\"dialog\"):视为回写窗口内的重复\n // 关闭意图,取消等待不补发请求;已原生关闭的 dialog 不再被超时弹回拉起。\n if (this._dragAwaitWriteback) {\n this._cancelDragAwait()\n return\n }\n\n // 原生关闭可绕过 cancel;controlled 时恢复受控状态,由 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 /*\n * backdrop 点击(backdrop click)的按-放链路回溯。\n *\n * 浏览器对「按下 → 拖动 → 松手」生成的 click 落在起点与松手点 DOM 的**共同祖先**\n * 上:从 panel 内容、拖拽 drag zone 甚至 backdrop 本处开始拖拽、松手落在 backdrop 上时,click 的\n * target 都是 dialog,与真正的轻点 backdrop 从 click 自身无法区分(真实鼠标/触摸下\n * 必现的误关路径,合成事件不派发 click 故自动化易漏测)。因此在 dialog 的\n * pointerdown 记录按下起点与是否落在 backdrop 上,click 时回溯校验:\n * - 按下起点不在 backdrop 上(从 panel 内容/drag zone 开始的链路)→ 不是点 backdrop,忽略;\n * - 按下到松手位移超过轻点量级 → 是拖拽松手而非点击,忽略;\n * 两者都通过才沿用「点 backdrop 关闭」语义。\n */\n private _backdropPress: { onMask: boolean; x: number; y: number } | null = null\n\n private handleDialogPointerDown(e: PointerEvent) {\n this._backdropPress = { onMask: e.target === e.currentTarget, x: e.clientX, y: e.clientY }\n }\n\n private handleBackdropClick(e: MouseEvent) {\n if (e.target !== (e.currentTarget as HTMLDialogElement)) return\n if (this.noBackdropClose) return\n // 拖拽进行中 pointer capture 使 click 落在 dialog 上,忽略以避免与手势竞争。\n if (this._isDragging()) return\n // detail 为 0 表示这次 click 不来自指针(键盘激活、程序化 .click()):\n // pointerdown 记录可能是上一次指针交互的残留,不能据此判定 backdrop 点击\n //(对齐 image-preview 的既有守卫)。\n if (e.detail === 0) return\n const press = this._backdropPress\n this._backdropPress = null\n if (!press || !press.onMask) return\n if (Math.hypot(e.clientX - press.x, e.clientY - press.y) > BACKDROP_TAP_DISTANCE) return\n this._closeFromUser()\n }\n\n private emitOpenChange(open = this.open) {\n dispatchOpenChangeEvent(this, open)\n }\n\n private _syncScrollLock(isOpen = this.open) {\n this._scrollLock.sync(isOpen && !this.noScrollLock)\n }\n\n override render() {\n const showHeader = this._hasHeaderSlot || !!this.heading\n const dialogLabel = this.ariaDialogLabel.trim()\n const dialogLabelledBy = !dialogLabel && !this.headless && showHeader ? 'wui-drawer-heading' : nothing\n\n // 拖拽 drag zone:仅在打开且 draggable 时渲染;胶囊 + 加宽命中条贴在 drawer 内缘。\n // 胶囊是 drag zone 的**兄弟节点**而非后代:drag zone 要让开 header / footer(见 style.css),\n // 胶囊作为后代会连带继承那份让位量,被一起推离 panel 边缘。两者都是 dialog 的直接子节点,\n // 胶囊因此相对 dialog 定位、恒贴可抓取边缘,drag zone 则继续为端头上的 button 让位。\n //\n // 端头 drag edge 补上解耦留下的空档:胶囊贴回边缘后就不再落在 drag zone 的让开区里\n // (bottom + header 实测胶囊带 [8,12]、drag zone 带 [60,80]),那一段若没有命中层,\n // 胶囊就成了一条按不下去、悬停不亮的死装饰——R1 与 R3 在几何上本就不可同满足,\n // 结构解耦只让两者不再互相位移,命中层必须另给。drag edge 只在上下 placement 渲染:\n // 左右两条的胶囊本就落在 drag zone 内,多一个元素只会让 R5「左右几何不变」需要额外论证。\n //\n // 顺序要紧:胶囊排最后,两条 drag zone(边、区)都在它前面,hover / active / 确认态三条规则\n // 因此靠通用兄弟选择器 `~` 挂上去。写成相邻兄弟 `+` 的话 drag edge 那条会静默失效。\n const needsEdgeBand = this.placement === 'top' || this.placement === 'bottom'\n const dragBar = this.draggable\n ? html`\n ${\n needsEdgeBand\n ? html`<div class=\"wui-drawer-drag-edge\" @pointerdown=${this._handleDragPointerDown}></div>`\n : nothing\n }\n <div class=\"wui-drawer-drag-zone\" @pointerdown=${this._handleDragPointerDown}></div>\n <div class=\"wui-drawer-drag-bar\"></div>\n `\n : nothing\n\n // 保持同一个 dialog 实例,避免打开期间切换 headless 时脱离 top layer。\n return html`\n <dialog\n aria-label=${dialogLabel || nothing}\n aria-labelledby=${dialogLabelledBy}\n @cancel=${this.handleCancel}\n @close=${this.handleNativeClose}\n @click=${this.handleBackdropClick}\n @pointerdown=${this.handleDialogPointerDown}\n @keydown=${this.handleKeydown}\n @transitionend=${this.handleTransitionEnd}\n >\n ${\n this.headless\n ? html`<slot></slot>${dragBar}`\n : html`\n <div class=\"wui-drawer-body wui-glass\">\n <div\n class=\"wui-drawer-header\"\n id=\"wui-drawer-heading\"\n ?hidden=${!showHeader}\n @pointerdown=${this._handleSectionDragPointerDown}\n >\n <slot name=\"header\" @slotchange=${this.handleHeaderSlotChange}>\n ${this.heading ? html`<span class=\"wui-drawer-heading\">${this.heading}</span>` : nothing}\n </slot>\n </div>\n <div class=\"wui-drawer-content wui-scroll\">\n <slot></slot>\n </div>\n <div\n class=\"wui-drawer-footer\"\n ?hidden=${!this._hasFooterSlot}\n @pointerdown=${this._handleSectionDragPointerDown}\n >\n <slot name=\"footer\" @slotchange=${this.handleFooterSlotChange}></slot>\n </div>\n </div>\n ${\n this.closable\n ? html`\n <web-ui-button\n class=\"wui-drawer-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 : nothing\n }\n ${dragBar}\n `\n }\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-drawer': WebUiDrawer\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.js","names":[],"sources":["../../../src/components/drawer/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n display: contents;\n}\n\ndialog {\n /*\n * 胶囊中线的唯一定义处。drag zone 与胶囊是 dialog 的两个直接子节点,定义在 dialog 上\n * 才能被双方读到——胶囊不是 drag zone 的后代,定义在 .wui-drawer-drag-zone 上读不到值,\n * 定位用的 calc 会整条失效。\n * 自定义属性沿树继承,drag zone 仍然读得到它,诊断输出不受影响。\n *\n * 胶囊中线跟随内容区 padding,consumer 覆盖 --wui-drawer-content-padding 后拖拽条自动居中;\n * max() 的第二项是下限,分两截来由:\n * - 半个胶囊厚度:padding 归零时中线不会压在 panel 内缘上、一半落在 panel 之外\n * (dialog 是 overflow: visible,这半截不被裁掉而是骑在 backdrop 上)。\n * - 再加 4px 呼吸间距:只兜住「不越界」时胶囊仍贴死 panel 边缘,读作挤在边框阴影线外侧。\n * 4px 是视觉调参结论(真机反馈),不开放成 consumer token。\n */\n --wui-internal-drag-bar-center: max(\n calc(var(--wui-drawer-content-padding, 20px) / 2),\n calc(var(--wui-drawer-drag-bar-thickness, 4px) / 2 + 4px)\n );\n\n /* 显式 position: fixed(防御性):UA 的 dialog:modal 默认 fixed,但作者样式若覆盖\n position 会把它退回文档流——top layer 里的 modal dialog 变成按文档流定位,出现在\n 页面顶部且跟随页面滚动。 */\n position: fixed;\n\n /* 默认:右侧滑出 — 需显式 left:auto 覆盖 UA dialog:modal 的 left:0 */\n top: 0;\n right: 0;\n bottom: 0;\n left: auto;\n transform: translateX(100%);\n\n /* 阴影属于 drawer 主体,不能由同尺寸的原生 dialog 裁剪。内容滚动由 .wui-drawer-content 管理。 */\n overflow: visible;\n\n box-sizing: border-box;\n width: var(--wui-drawer-width, 320px);\n max-width: 90vw;\n height: 100vh;\n height: 100dvh;\n max-height: 100vh;\n max-height: 100dvh;\n margin: 0;\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-drawer-enter, 280ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n\n &::backdrop {\n opacity: 0;\n background: var(--wui-drawer-overlay-bg, var(--wui-color-backdrop, rgb(0 0 0 / 0.12)));\n transition: opacity var(--wui-duration-drawer-enter, 280ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n }\n}\n\n/* 模态表面的焦点环抑制(<dialog> 壳 + panel 滚动区)集中在 assets/modal-surface.css,见组件 index.ts 的 static styles。 */\n\ndialog[open].is-visible {\n transform: translate(0, 0);\n}\n\n/*\n * 拖拽跟手期间,transform/backdrop 由 JS 内联样式管辖,CSS transition 必须让位。\n * 特异性必须压过下方开放守卫块(:host(:not()) dialog[open].is-visible = (0,4,1)\n * 的 nested transition 声明),否则拖拽写入的 transform 会被平滑掉、跟手失效。\n * 复合选择器达 (0,6,1),恒高于守卫。\n */\n:host(:not([headless])) dialog.is-dragging[open].is-visible,\ndialog.is-dragging {\n transition: none;\n\n &::backdrop {\n transition: none;\n }\n}\n\n/*\n * 释放后的收尾(弹回打开位 / 滑出到闭合位):位移与 backdrop 交还 CSS transition,\n * JS 只在松手瞬间写入终值与 --wui-internal-settle-duration / --wui-internal-settle-easing\n *(issue #123)。全程无 WAAPI,before-change style 就是拖拽期间写入的内联值,\n * 不存在 JS→CSS 动画交接边界,因此不需要 Safari 的 reflow 烘焙补丁。\n * 特异性对齐 .is-dragging:同样要压过下方开放守卫块。\n */\n:host(:not([headless])) dialog.is-settling[open].is-visible,\ndialog.is-settling {\n transition: transform var(--wui-internal-settle-duration, 280ms)\n var(--wui-internal-settle-easing, cubic-bezier(0.32, 0.72, 0, 1));\n\n &::backdrop {\n transition: opacity var(--wui-internal-settle-duration, 280ms)\n var(--wui-internal-settle-easing, cubic-bezier(0.32, 0.72, 0, 1));\n }\n}\n\n:host([placement='left']) dialog[open].is-visible,\n:host([placement='top']) dialog[open].is-visible,\n:host([placement='bottom']) dialog[open].is-visible {\n transform: translate(0, 0);\n}\n\ndialog.is-closing {\n pointer-events: none;\n transition: transform var(--wui-duration-drawer-exit, 240ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n}\n\ndialog.is-closing::backdrop {\n transition: opacity var(--wui-duration-drawer-exit, 240ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n}\n\ndialog[open].is-visible::backdrop {\n /* 拖拽期间由 JS 写入 --wui-internal-drag-backdrop-opacity 实现 backdrop 跟手淡出。 */\n opacity: var(--wui-internal-drag-backdrop-opacity, 1);\n}\n\n/*\n * drag zone 贴在 drawer 内缘,宽度由 --wui-drawer-drag-zone-size(默认 20px)控制便于触控命中;\n * 胶囊本身由 --wui-drawer-drag-bar-thickness × --wui-drawer-drag-bar-length(默认 4×56px)控制。\n * 胶囊绝对定位在 --wui-drawer-content-padding 的视觉中线(默认 20px → 中线 10px),\n * 并以半个胶囊厚度加 4px 呼吸间距兜底,使 padding 归零的 consumer 也留有可见内缩(见下方 max());\n * drag zone 仍覆盖内缘起 --wui-drawer-drag-zone-size,保证触控命中面积不随胶囊收窄。\n * 位置随 placement 切换:right 贴在左缘、left 贴在右缘、top 贴在底边、bottom 贴在顶边。\n *\n * 上下两条还要**让开开头尾两节**:drag zone 是 dialog 的绝对定位兄弟节点,压在 panel 的端头上,\n * 而那一端上正有 header(bottom)或 footer(top)——button 与点击都落在 drag zone 底下。让开量由\n * index.ts 用 ResizeObserver 量出该节的 border-box 高度写成 --wui-internal-drawer-*-inset,\n * 两者都不存在时为 0,drag zone 回到 panel 边缘。\n *\n * 胶囊与 drag zone 因此**不共用一套偏移**:两者都是 dialog 的直接子节点(兄弟,不是父子),\n * 胶囊恒定相对 dialog 定位、贴住可抓取边缘,drag zone 独自承担让开量。胶囊不是 drag zone 的后代,\n * `top` 相对 dialog 算,不会被那 60px 让位量一起推离 panel 边缘。\n * 让开量是 drag zone 一方的约束,不该外溢到纯视觉的胶囊上;反过来把 drag zone 推回 0 又会让 A6 的\n * header 遮挡立刻回归。两个约束只有靠结构解耦才同时成立,调数值必然牺牲其一。\n *\n * 代价是这一让开把胶囊所在的端头**腾空了**:胶囊贴回边缘后落在让开区里,两条带恒不相交。\n * 补洞的不是本元素,而是端头那条 drag edge `.wui-drawer-drag-edge`(见它的规则)——\n * 两者各管一段,缺一条胶囊就成了死装饰。R1 与 R3 在几何上本就不可同满足,\n * 这里只是把「不可同满足」拆成两段各自成立,而不是假装它们能同时成立。\n *\n * 按在 panel 边缘之外的坐标属于 backdrop:轻点走「点 backdrop 关闭」(click 链路回溯校验,\n * 见 index.ts 的 handleBackdropClick),拖拽松手不关闭。\n */\n.wui-drawer-drag-zone {\n touch-action: none;\n cursor: grab;\n\n position: absolute;\n z-index: 4;\n\n transition: opacity var(--wui-duration-feedback, 100ms) ease;\n}\n\ndialog.is-nested-lower .wui-drawer-drag-zone {\n pointer-events: none;\n opacity: 0;\n}\n\n:host([placement='right']) .wui-drawer-drag-zone {\n top: 0;\n left: 0;\n width: var(--wui-drawer-drag-zone-size, 20px);\n height: 100%;\n}\n\n:host([placement='left']) .wui-drawer-drag-zone {\n top: 0;\n right: 0;\n width: var(--wui-drawer-drag-zone-size, 20px);\n height: 100%;\n}\n\n:host([placement='top']) .wui-drawer-drag-zone,\n:host([placement='bottom']) .wui-drawer-drag-zone {\n right: 0;\n left: 0;\n width: 100%;\n height: var(--wui-drawer-drag-zone-size, 20px);\n}\n\n:host([placement='top']) .wui-drawer-drag-zone {\n /*\n * 让开 footer(不是 header):`top` 贴的是 panel**底边**,那一侧压住的是 footer。\n * 头部在内缘的另一端,与本 placement 的 drag zone 不重叠。\n */\n bottom: var(--wui-internal-drawer-footer-inset, 0);\n}\n\n:host([placement='bottom']) .wui-drawer-drag-zone {\n /* 对称:让开 header。左右两条不受影响(贴的是竖边,header / footer 都不在那里)。 */\n top: var(--wui-internal-drawer-header-inset, 0);\n}\n\n/*\n * 端头 drag edge:贴住可抓取边缘的一条常驻 drag zone,只在上下 placement 渲染(见 index.ts)。\n *\n * 它补的是上面那条让开量腾出的空档。胶囊贴回边缘后不在 drag zone 里了,用户却仍然看着它,\n * 于是「handle 靠近边缘」这个诉求必须连着可拖、可点亮一起兑现,否则胶囊只是装饰。\n *\n * **足迹不得超过它所代表的胶囊**:宽度取胶囊长度、水平居中,与胶囊同一套 left。\n * 这一条是本元素唯一真正的不变量,也是关 button 与消费侧 header 控件的安全保证——\n * 满宽会横扫 header 整个宽度:实测 consumer 把 `--wui-drawer-drag-bar-thickness`\n * 调到 24px 时内置关闭 button 与消费侧左右 button 各被吃 40%、40px 时各 100%。\n * 关 button 从来不是特例,**满宽才是病根**。收窄后关 button 贴右缘、胶囊居中,\n * 只要胶囊自己没伸到 button 所在的位置,两者就永不相交——对所有 thickness / space-4\n * 组合都成立(断言按 8 组组合逐一量相交面积),且完全不依赖 close-top。\n *\n * 代价明说:触摸命中面积只有胶囊长度(默认 56×12)。可接受——\n * **drag zone 本身没动**(满宽 20–32px 仍在让开位上),大面积起手能力并未丢失,\n * drag edge 只负责兑现「你看到的那个 drag bar 按得动」。\n *\n * 收窄后仍压不死的情况只有一类:消费侧把可点控件放在胶囊那条水平跨度内。\n * 纯 CSS 无法消除(带要覆盖胶囊就必须有胶囊那么宽,而胶囊就画在那里)。\n * 但那不是新引入的视觉冲突——thickness 40px 时胶囊自身已压在 header 内容上,\n * drag edge 只是把「胶囊盖住的地方」一并变成可拖。是 consumer 要求了一个粗 drag bar,\n * 而 drag bar 就画在 header 上。\n *\n * 实测:给该控件抬 z-index ≥ 5 **救不回来**——Chromium 里本元素始终压在 slotted\n * 节点之上,抬 z-index 无效。唯一量得着的杠杆是 `--wui-drawer-drag-bar-length`:\n * 它同时决定胶囊长度与本元素宽度,缩短它就同比例缩短本元素(README 已写明)。\n *\n * 高度取两条来源的 **max()**:\n * - `中线 + 半个厚度` 恰是胶囊的外沿;\n * - `--wui-drawer-drag-edge-size`(默认 20px)是给手指的命中厚度。\n * 加这条是因为端头只有 12px(=中线 10 + 半个厚度 2)时真机反馈「很难拖动到」。\n * 默认值取 20px 的依据:与主 drag zone `--wui-drawer-drag-zone-size` 的默认厚度同值,\n * 于是**胶囊的命中带恒不窄于主 drag zone**,「按你看到的 drag bar」不会比「按旁边的带子」更难。\n * 再往上加会开始吃掉 header 里控件的位置(端头整块可拖后,那一节是要放控件的)。\n *\n * 取 max() 而非直接用 token,是为了保住包含关系:中线的下限(`max()` 第二项)保证\n * `中线 - 半个厚度 ≥ 4 > 0`,而 drag edge 从 panel 边缘(top/bottom placement 下恒为 0)起算,\n * 于是**胶囊带恒被 drag edge 带包含**——consumer 把 edge-size 调到比胶囊带还小也吃不掉它。\n * 这条包含关系是 token 定义推出来的,不是靠约定维持的。\n *\n * 竖向仍比胶囊多出「panel 边缘到胶囊近侧」那一条(≤ 中线 − 半个厚度,默认 8px),\n * 用来让指针不必精确压在 4px 粗的胶囊上。\n */\n.wui-drawer-drag-edge {\n touch-action: none;\n cursor: grab;\n\n position: absolute;\n z-index: 4;\n\n /* 只在上下 placement 渲染(见 index.ts),故居中方式与胶囊的那条规则逐字相同。 */\n left: calc(50% - var(--wui-drawer-drag-bar-length, 56px) / 2);\n\n width: var(--wui-drawer-drag-bar-length, 56px);\n height: max(\n calc(var(--wui-internal-drag-bar-center) + var(--wui-drawer-drag-bar-thickness, 4px) / 2),\n var(--wui-drawer-drag-edge-size, 20px)\n );\n\n transition: opacity var(--wui-duration-feedback, 100ms) ease;\n}\n\n/*\n * 嵌套下层时整套拖拽提示一起退场,drag edge 也不例外——否则下层 drawer 会留一条看不见起点、\n * 按下去却能拖的悬空 drag edge,比悬空胶囊更费解。\n */\ndialog.is-nested-lower .wui-drawer-drag-edge {\n pointer-events: none;\n opacity: 0;\n}\n\n:host([placement='bottom']) .wui-drawer-drag-edge {\n top: 0;\n}\n\n:host([placement='top']) .wui-drawer-drag-edge {\n bottom: 0;\n}\n\n.wui-drawer-drag-bar {\n /*\n * 胶囊是纯视觉件,命中与拖拽全部归两条 drag zone(端头 drag edge + 让开量 drag zone)。\n * pointer-events: none 是这件事得以成立的前提:\n * 胶囊叠在 drag zone 之上(z-index 更高),若参与命中测试,按在胶囊上的手势会被胶囊吃掉,\n * drag zone 的 @pointerdown 收不到事件,从胶囊起手的拖拽直接失效。端头 drag edge 按构造覆盖胶囊带,\n * 所以「从胶囊起手」与「从 drag edge 空白处起手」命中的是同一个元素。\n *\n * 它同时解掉了 hover / active 的归属:pointer-events 为 none 时胶囊永不成为 hover 目标,\n * 指针落在胶囊上时下面的 drag zone/drag edge 照常 :hover,「drag zone 点亮胶囊」在四个 placement 上都成立,\n * 不必给胶囊另挂监听(同一手势会触发两次)。\n */\n pointer-events: none;\n\n position: absolute;\n\n /*\n * 5 高于 drag zone 与端头 drag edge(均 4),与 panel 主体(无 z-index 的静态盒)也不冲突。\n * 与 close button 同为 5:两者之间靠的不是层序而是几何(胶囊居中、button 贴右缘)。\n * 而胶囊 `pointer-events: none`,从不参与命中测试,层序高低对它没有命中含义。\n */\n z-index: 5;\n\n width: var(--wui-drawer-drag-bar-thickness, 4px);\n height: var(--wui-drawer-drag-bar-length, 56px);\n border-radius: var(--wui-radius-control, calc(infinity * 1px));\n\n background: color-mix(in srgb, var(--wui-color-text, #1b1b1b) 24%, transparent);\n\n /* 与 drag zone 同一条淡入淡出,嵌套下层退场时两者同步(见下面的 is-nested-lower)。 */\n transition: opacity var(--wui-duration-feedback, 100ms) ease;\n}\n\n/*\n * 嵌套下层时整套拖拽提示一起退场,drag zone 与胶囊一起。胶囊是 drag zone 的兄弟而非后代,\n * drag zone 那条规则的 opacity 作用不到它,必须显式列出,否则下层 drawer\n * 会剩一条没人操作的悬空胶囊。放在基础规则之后,保住 no-descending-specificity 的升序。\n */\ndialog.is-nested-lower .wui-drawer-drag-bar {\n pointer-events: none;\n opacity: 0;\n}\n\n:host([placement='right']) .wui-drawer-drag-bar {\n top: calc(50% - var(--wui-drawer-drag-bar-length, 56px) / 2);\n left: calc(var(--wui-internal-drag-bar-center) - var(--wui-drawer-drag-bar-thickness, 4px) / 2);\n}\n\n:host([placement='left']) .wui-drawer-drag-bar {\n top: calc(50% - var(--wui-drawer-drag-bar-length, 56px) / 2);\n right: calc(var(--wui-internal-drag-bar-center) - var(--wui-drawer-drag-bar-thickness, 4px) / 2);\n}\n\n:host([placement='top']) .wui-drawer-drag-bar,\n:host([placement='bottom']) .wui-drawer-drag-bar {\n width: var(--wui-drawer-drag-bar-length, 56px);\n height: var(--wui-drawer-drag-bar-thickness, 4px);\n}\n\n:host([placement='top']) .wui-drawer-drag-bar {\n bottom: calc(var(--wui-internal-drag-bar-center) - var(--wui-drawer-drag-bar-thickness, 4px) / 2);\n left: calc(50% - var(--wui-drawer-drag-bar-length, 56px) / 2);\n}\n\n:host([placement='bottom']) .wui-drawer-drag-bar {\n top: calc(var(--wui-internal-drag-bar-center) - var(--wui-drawer-drag-bar-thickness, 4px) / 2);\n left: calc(50% - var(--wui-drawer-drag-bar-length, 56px) / 2);\n}\n\n/*\n * 通用兄弟选择器:胶囊排在这两个 drag zone 之后(index.ts 的 render 里同一个模板表达式产出\n * 这三个节点),`~` 因此在**所有 placement** 上都成立,且与「drag edge 是否存在」无关——\n * 上下 placement 多一条 drag edge、左右 placement 只有一条,写成 `+` 会让 drag edge 那条静默失配。\n * 这两条规则写成后代选择器(.zone:hover .bar)就不满足当前结构——不报错、\n * 只是静默不变色,是本结构最隐蔽的一处回归。\n */\n.wui-drawer-drag-edge:hover ~ .wui-drawer-drag-bar,\n.wui-drawer-drag-edge:active ~ .wui-drawer-drag-bar,\n.wui-drawer-drag-zone:hover ~ .wui-drawer-drag-bar,\n.wui-drawer-drag-zone:active ~ .wui-drawer-drag-bar {\n background: color-mix(in srgb, var(--wui-color-text, #1b1b1b) 40%, transparent);\n}\n\n/*\n * 达到关闭阈值的确认态:比拖拽静息态更实的中性灰。\n * 55% 这一档是看着真浏览器定的:拖拽全程 capture 使 hover/active 恒成立,40% 那档\n * 在拖拽中始终可见,确认态必须明显压过它——试过的 25%(比静息态还淡)越过阈值\n * 读成「变弱」,24% 的静息灰与它几乎无法区分。55% 相对静息 40% 有可辨的一档\n * 落差,又不至于重到读成实心深色条;再往上(60%)与 55% 的观感差别小于它与 40%\n * 的差别,收益不抵「更重」。\n * 这里的选择器特异性 (0,3,1) 高于 hover/active 的 (0,3,0),靠特异性而非顺序压盖。\n * 拆成兄弟后仍**保留 .wui-drawer-drag-zone 这一段**(用 `~` 接胶囊):若缩写成\n * `dialog.is-drag-close .wui-drawer-drag-bar` 会掉到 (0,2,1),反过来被 (0,3,0) 的\n * hover/active 压过,确认态在拖拽全程(capture 使 hover 恒成立)就不再显形。\n */\ndialog.is-drag-close .wui-drawer-drag-zone ~ .wui-drawer-drag-bar {\n background: color-mix(in srgb, var(--wui-color-text, #1b1b1b) 55%, transparent);\n}\n\n:host([placement='left']) dialog {\n right: auto;\n left: 0;\n transform: translateX(-100%);\n}\n\n:host([placement='top']) dialog,\n:host([placement='bottom']) dialog {\n width: 100vw;\n max-width: 100vw;\n height: var(--wui-drawer-height, 300px);\n max-height: 90dvh;\n}\n\n:host([placement='top']) dialog {\n top: 0;\n right: 0;\n bottom: auto;\n left: 0;\n transform: translateY(-100%);\n}\n\n:host([placement='bottom']) dialog {\n top: auto;\n right: 0;\n bottom: 0;\n left: 0;\n transform: translateY(100%);\n}\n\n/*\n * 拖拽中的 grabbing 光标。dialog 复合前缀使其特异性恒高于上方 placement 变体,\n * stylelint 的顺序比较对这种结构必然告警;cascade 上此规则不依赖 placement,\n * 置于其后仅为可读性,故显式豁免。\n */\n/* stylelint-disable-next-line no-descending-specificity */\ndialog.is-dragging .wui-drawer-drag-zone {\n cursor: grabbing;\n}\n\n.wui-drawer-body {\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-drawer-radius, var(--wui-radius-overlay, 28px));\n display: flex;\n flex-direction: column;\n height: 100%;\n background-color: var(--wui-drawer-bg, var(--wui-color-surface-overlay, rgb(248 248 248 / 0.92)));\n}\n\n.wui-drawer-header {\n /*\n * 刻意没有分割线(footer 同)。panel 主体 `.wui-drawer-body` 自带 `.wui-glass` 的描边环与\n * 投影,分区靠 padding 与排版层级读出来;再叠一条写死的 6% 黑线,在半透明玻璃面上会读成\n * 一道硬接缝,且它不跟随任何 token——换外观时是块跟不上的残留。\n */\n display: flex;\n align-items: center;\n justify-content: space-between;\n\n padding: var(--wui-drawer-header-padding, var(--wui-space-4, 16px) var(--wui-space-5, 20px));\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-snug, 1.4);\n}\n\n.wui-drawer-header[hidden] {\n display: none;\n}\n\n.wui-drawer-content {\n overflow-y: auto;\n overscroll-behavior: contain;\n flex: 1;\n\n padding: var(--wui-drawer-content-padding, var(--wui-space-5, 20px));\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-drawer-footer {\n flex-shrink: 0;\n padding: var(--wui-drawer-footer-padding, var(--wui-space-4, 16px) var(--wui-space-5, 20px));\n}\n\n.wui-drawer-footer[hidden] {\n display: none;\n}\n\n/*\n * 上下 placement:贴着可抓边缘的那一节(bottom 的 header、top 的 footer)整块可拖。\n *\n * 让开靠**逐个控件**在事件层判断(见 index.ts 的 _handleSectionDragPointerDown),\n * 不靠把 drag zone 铺上来——铺上来会把这一节里的控件吞掉,正是 A6 修掉的那条缺陷。\n * 所以这里只需要手势与光标两件样式,命中关系由那一节自己承担。\n *\n * `touch-action: none` 不可省:否则触摸落在标题文字或控件上时,浏览器会先把它\n * 判成滚动意图,越过阈值就 pointercancel,拖拽根本起不来(与 drag zone 同一条理由)。\n * 它是祖先节点,触摸落在深层 slotted 子元素上时同样生效——touch-action 沿命中链\n * 求交,不要求落在声明元素本身上。\n *\n * 按 `[draggable]` 收口:draggable 为假时这一节不接手势,也不该被夺走触摸手势。\n *\n * 再按 `dialog:not(.is-nested-lower)` 收口,与事件层闸门(index.ts 的\n * _handleSectionDragPointerDown)和 drag zone / drag edge / 胶囊那三条 `is-nested-lower` 退场\n * 判同一条:嵌套下层的 drawer 整套拖拽提示一起退场。这里漏掉就会只剩提示面没退——\n * 而 `touch-action` 沿命中链求交,下层 header 区域的触摸滚动是被真的夺走的,\n * 那里却拖不动:既骗光标又吞手势,比留着可拖的提示面更糟。\n */\n:host([placement='bottom'][draggable]) dialog:not(.is-nested-lower) .wui-drawer-header,\n:host([placement='top'][draggable]) dialog:not(.is-nested-lower) .wui-drawer-footer {\n touch-action: none;\n cursor: grab;\n}\n\n:host([placement='bottom'][draggable]) dialog:not(.is-nested-lower).is-dragging .wui-drawer-header,\n:host([placement='top'][draggable]) dialog:not(.is-nested-lower).is-dragging .wui-drawer-footer {\n cursor: grabbing;\n}\n\n/* ===== 关闭 button(绝对定位)=====\n top 恰好等于 header padding 的垂直分量(16px),因此挂 --wui-space-4 与 header padding\n 同源:覆盖该级时二者一起动,注释里写死的对齐关系不会断。\n right 是 button 到 drawer 右边沿的独立偏移(16px),不对齐相邻元素的节奏值(header\n 的水平分量是 20px,与它并不相等),参照系是容器边缘而非相邻元素之间的节奏,故与\n --wui-image-preview-edge-gap 等边缘对齐值同属一族,保持字面量。挂到 --wui-space-4\n 本身并不会造成视觉变化(该级默认仍是 16px)——不挂阶的理由是语义归属,不是视觉风险。 */\n\n.wui-drawer-close {\n position: absolute;\n\n /*\n * 5,高于端头 drag edge(4)。\n *\n * 端头 drag edge 收窄到胶囊长度之后,默认与常规 token 下它与本 button 的相交面积**恒为 0px²**\n * (实测 thickness 4/16/24/40 × space-4 16/8/4 共 12 组,测试按这个矩阵逐组断言),\n * 正常情形靠几何就够。\n * 这一层防的是几何失效的那一格:`--wui-drawer-drag-bar-length` 同样是公开 token,\n * 调到 drag bar 伸到 button 所在的位置时相交面积不为 0(实测 320px 时为 78px²),\n * 此时唯一还能救的就是层序。\n *\n * 为它单独抬这一层是有实测依据的**不对称**,不是泛泛的防御性声明:\n * 消费侧把控件塞进 drag bar 跨度里时,同一招不管用——Chromium 里端头 drag edge 始终压在\n * **slotted** 节点之上,给 slotted 元素抬 z-index 无效(已在测试注释里记下实测)。\n * 本 button 是组件自己画的 shadow 节点,层序对它有效,所以它是这条链上唯一能自救的节点。\n */\n z-index: 5;\n top: var(--wui-drawer-close-top, var(--wui-space-4, 16px));\n right: var(--wui-drawer-close-right, 16px);\n bottom: auto;\n left: auto;\n}\n\n/* Headless 模式下,dialog 只作为 overlay 容器,不渲染内置 UI */\n:host([headless]) dialog {\n /*\n * inset 只属于浮动卡片几何;headless 嵌套在非 headless 外层内时会沿\n * flattened tree 继承到 8px,破坏 JS 读取该变量得到的贴边几何假设\n * (parseFloat('0px') 与回退 0 语义一致),故显式归零。\n */\n --wui-internal-drawer-inset: 0px;\n\n width: auto;\n max-width: none;\n}\n\n/* ===== 浮动卡片视觉语言(ADR-0036) ===== */\n\n/*\n * 非 headless 时 drawer 是四周留边的圆角卡片,与 layout sidebar 的设计语言一致;\n * 弹性拖拽使边距变化(8px → 更大)是合法状态而非渲染缺口。\n * headless 保持贴边几何,视觉由 Consumer 自绘。\n *\n * 留边经公开 token --wui-drawer-inset 暴露(默认 8px,ADR-0036 修订);\n * 置 0 即贴边几何(与 headless 相同),通常与 --wui-drawer-radius: 0 搭配。\n *\n * 闭合位移需要越过留边才能完全滑出视口,因此闭合态 transform 以\n * --wui-internal-drawer-inset 补偿;JS 在 controlled 悬停终态读取同一变量,\n * 保证两条关闭路径的行为一致。headless 将该变量显式归零(见上方 Headless 模式)。\n */\n\n/* 默认 right:四周留边、高度随上下边距拉伸,闭合位移越过右侧留边 */\n:host(:not([headless])) dialog {\n --wui-internal-drawer-inset: var(--wui-drawer-inset, 8px);\n --wui-internal-drawer-nested-depth: 0;\n --wui-internal-drawer-nested-scale: 1;\n --wui-internal-drawer-nested-shift: 0px;\n\n top: var(--wui-internal-drawer-inset);\n right: var(--wui-internal-drawer-inset);\n bottom: var(--wui-internal-drawer-inset);\n left: auto;\n transform: translateX(calc(100% + var(--wui-internal-drawer-inset)));\n\n height: auto;\n border-radius: var(--wui-drawer-radius, var(--wui-radius-overlay, 28px));\n}\n\n:host(:not([headless])) .wui-drawer-body {\n border-radius: inherit;\n}\n\n:host(:not([headless])[placement='left']) dialog {\n top: var(--wui-internal-drawer-inset);\n right: auto;\n bottom: var(--wui-internal-drawer-inset);\n left: var(--wui-internal-drawer-inset);\n transform: translateX(calc(-100% - var(--wui-internal-drawer-inset)));\n\n height: auto;\n}\n\n/*\n * 上下 placement 的高度在这里再声明一次,且不能省。\n *\n * 上面 `:host(:not([headless])) dialog` 的 `height: auto` 与基础 placement 块里那条挂\n * `--wui-drawer-height`(默认 300px)的 height 声明特异性相同(都是 (0,2,1))、位置更靠后,\n * 会把那条整条压掉。左右不受影响——它们的 auto 有 top / bottom 双边同时约束,仍算得出\n * 确定高度;上下只有单边贴边,dialog 于是变成内容高度。后果有两层:\n *\n * - `--wui-drawer-height` 失效(README 记录它是上下 drawer 的高度,demo 的自定义高 button\n * 直接指向它);\n * - `.wui-drawer-body` 的 `height: 100%` 解析成 auto,`.wui-drawer-content` 拿到的是\n * 内容高度而不是 panel 高度,`flex: 1` 收缩不了自己,长内容直接溢出 dialog 盒子\n * (overflow: visible)落到视口之下——够不着,也滚不动。\n */\n:host(:not([headless])[placement='top']) dialog {\n top: var(--wui-internal-drawer-inset);\n right: var(--wui-internal-drawer-inset);\n bottom: auto;\n left: var(--wui-internal-drawer-inset);\n transform: translateY(calc(-100% - var(--wui-internal-drawer-inset)));\n\n width: auto;\n max-width: none;\n height: var(--wui-drawer-height, 300px);\n}\n\n:host(:not([headless])[placement='bottom']) dialog {\n top: auto;\n right: var(--wui-internal-drawer-inset);\n bottom: var(--wui-internal-drawer-inset);\n left: var(--wui-internal-drawer-inset);\n transform: translateY(calc(100% + var(--wui-internal-drawer-inset)));\n\n width: auto;\n max-width: none;\n height: var(--wui-drawer-height, 300px);\n}\n\n/*\n * 开放态守卫:上方闭合 transform 与基础 `dialog[open].is-visible` 特异性相同且\n * 在文件更后方,必须以更高特异性显式恢复打开位,否则 drawer 停在闭合位。\n *\n * Nested 层叠(对齐卡片堆栈效果):非顶层 drawer 按 0.95^depth 缩放并向屏幕内侧\n * 平移(shift 为正,朝屏幕中心方向),使缩小的卡片在顶层后方露出阶梯式卡片边缘。\n * 用独立 translate/scale 属性(而非并入 transform):\n * 它们是标准可过渡属性,变量变化即触发平滑过渡;且与 transform 轴上的\n * 闭合/拖拽/弹簧位移互不覆写(独立属性先于 transform 应用后合并)。\n * JS 层序管理写入 depth/scale/shift 三个内部变量,单层使用时全为中性值。\n *\n * 层叠变化的平滑过渡(450ms easeOutQuint 类曲线)。\n * transform 保持既有 enter/exit 时长;translate/scale 只在层叠数变化时更新,\n * 用更长的 nested 时长。\n */\n:host(:not([headless])) dialog[open].is-visible {\n transform: translate(0, 0);\n translate: calc(var(--wui-internal-drawer-nested-shift) * -1) 0;\n scale: var(--wui-internal-drawer-nested-scale);\n transition:\n transform var(--wui-duration-drawer-enter, 280ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1)),\n translate var(--wui-duration-drawer-nested, 450ms) cubic-bezier(0.22, 1, 0.36, 1),\n scale var(--wui-duration-drawer-nested, 450ms) cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n:host(:not([headless])[placement='left']) dialog[open].is-visible {\n transform: translate(0, 0);\n translate: var(--wui-internal-drawer-nested-shift) 0;\n}\n\n:host(:not([headless])[placement='top']) dialog[open].is-visible {\n transform: translate(0, 0);\n translate: 0 var(--wui-internal-drawer-nested-shift);\n}\n\n:host(:not([headless])[placement='bottom']) dialog[open].is-visible {\n transform: translate(0, 0);\n translate: 0 calc(var(--wui-internal-drawer-nested-shift) * -1);\n}\n\n/*\n * 窄视口的层叠基准:同一个 `--wui-drawer-nested-peek-base` 用更小的 A。\n * 桌面基准(43.2px)来自 index.ts 的 `CSS.registerProperty` initialValue,这里只覆盖窄视口,\n * 因此 A 的桌面真相源仍只有注册项一处。\n *\n * 28.8px 沿用桌面:窄屏 3:2,窄屏随桌面同比缩小:桌面 43.2px、窄屏 28.8px,3:2 不变。\n * 第一跳仍是 28.8·ln2 = 19.96px,高于 12px 的线性步进;四层总宽 39.93px。\n *\n * 屏内余量不是定 3:2 的理由:390px 视口上单层左缘有 390 − 8 − 320 = 62px,\n * 桌面基准的四层总宽 43.2·ln4 = 59.89px 也放得下(总宽 74.86px 才会溢出)。\n * 保留窄屏档是为了不改变既定比例关系,同时给窄屏留出更宽的 drawer 可视区。\n *\n * 断点字面量与 shared/overlay/nested-drawer-layers.ts 的 matchMedia 查询各有一份:\n * 跨断点时 JS 侧靠它重算已打开的堆叠,两处必须同步(见该文件的同名常量)。\n *\n * 消费方在页面里给宿主写的值仍然优先——外层树的声明压过 shadow 内的 :host 规则。\n */\n@media (width <= 640px) {\n :host(:not([headless])) {\n --wui-drawer-nested-peek-base: 28.8px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n /*\n * reduce 下:位移一律归零、transform 不参与过渡(仅 opacity 反馈)。\n * 全部 !important:压过开放守卫(嵌套层叠变量的特异性猫鼠不可维护),\n * 排除 .is-dragging —— reduce 下拖拽仍需实时跟手(ADR-0035)。\n * JS 收尾移除内联 transform 后立即为终态,无 280ms 过渡尾巴。\n */\n dialog:not(.is-dragging) {\n transition: opacity var(--wui-duration-feedback, 100ms) ease !important;\n\n &::backdrop {\n transition-duration: var(--wui-duration-feedback, 100ms) !important;\n }\n }\n\n dialog[open].is-visible:not(.is-dragging) {\n transform: none !important;\n translate: none !important;\n scale: none !important;\n }\n}\n"],"mappings":";AAAA,IAAA,gBAAA"}
|
|
1
|
+
{"version":3,"file":"style.js","names":[],"sources":["../../../src/components/drawer/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n display: contents;\n}\n\ndialog {\n /*\n * 胶囊中线的唯一定义处。drag zone 与胶囊是 dialog 的两个直接子节点,定义在 dialog 上\n * 才能被双方读到——胶囊不是 drag zone 的后代,定义在 .wui-drawer-drag-zone 上读不到值,\n * 定位用的 calc 会整条失效。\n * 自定义属性沿树继承,drag zone 仍然读得到它,诊断输出不受影响。\n *\n * 胶囊中线跟随内容区 padding,consumer 覆盖 --wui-drawer-content-padding 后拖拽条自动居中;\n * max() 的第二项是下限,分两截来由:\n * - 半个胶囊厚度:padding 归零时中线不会压在 panel 内缘上、一半落在 panel 之外\n * (dialog 是 overflow: visible,这半截不被裁掉而是骑在 backdrop 上)。\n * - 再加 4px 呼吸间距:只兜住「不越界」时胶囊仍贴死 panel 边缘,读作挤在边框阴影线外侧。\n * 4px 是视觉调参结论(真机反馈),不开放成 consumer token。\n */\n --wui-internal-drag-bar-center: max(\n calc(var(--wui-drawer-content-padding, 20px) / 2),\n calc(var(--wui-drawer-drag-bar-thickness, 4px) / 2 + 4px)\n );\n\n /* 显式 position: fixed(防御性):UA 的 dialog:modal 默认 fixed,但作者样式若覆盖\n position 会把它退回文档流——top layer 里的 modal dialog 变成按文档流定位,出现在\n 页面顶部且跟随页面滚动。 */\n position: fixed;\n\n /* 默认:右侧滑出 — 需显式 left:auto 覆盖 UA dialog:modal 的 left:0 */\n top: 0;\n right: 0;\n bottom: 0;\n left: auto;\n transform: translateX(100%);\n\n /* 阴影属于 drawer 主体,不能由同尺寸的原生 dialog 裁剪。内容滚动由 .wui-drawer-content 管理。 */\n overflow: visible;\n\n box-sizing: border-box;\n width: var(--wui-drawer-width, 320px);\n max-width: 90vw;\n height: 100vh;\n height: 100dvh;\n max-height: 100vh;\n max-height: 100dvh;\n margin: 0;\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-drawer-enter, 280ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n\n &::backdrop {\n opacity: 0;\n background: var(--wui-drawer-overlay-bg, var(--wui-color-backdrop, rgb(0 0 0 / 0.12)));\n transition: opacity var(--wui-duration-drawer-enter, 280ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n }\n}\n\n/* 模态表面的焦点环抑制(<dialog> 壳 + panel 滚动区)集中在 assets/modal-surface.css,见组件 index.ts 的 static styles。 */\n\ndialog[open].is-visible {\n transform: translate(0, 0);\n}\n\n/*\n * 拖拽跟手期间,transform/backdrop 由 JS 内联样式管辖,CSS transition 必须让位。\n * 特异性必须压过下方开放守卫块(:host(:not()) dialog[open].is-visible = (0,4,1)\n * 的 nested transition 声明),否则拖拽写入的 transform 会被平滑掉、跟手失效。\n * 复合选择器达 (0,6,1),恒高于守卫。\n */\n:host(:not([headless])) dialog.is-dragging[open].is-visible,\ndialog.is-dragging {\n transition: none;\n\n &::backdrop {\n transition: none;\n }\n}\n\n/*\n * 释放后的收尾(弹回打开位 / 滑出到闭合位):位移与 backdrop 交还 CSS transition,\n * JS 只在松手瞬间写入终值与 --wui-internal-settle-duration / --wui-internal-settle-easing\n *(issue #123)。全程无 WAAPI,before-change style 就是拖拽期间写入的内联值,\n * 不存在 JS→CSS 动画交接边界,因此不需要 Safari 的 reflow 烘焙补丁。\n * 特异性对齐 .is-dragging:同样要压过下方开放守卫块。\n */\n:host(:not([headless])) dialog.is-settling[open].is-visible,\ndialog.is-settling {\n transition: transform var(--wui-internal-settle-duration, 280ms)\n var(--wui-internal-settle-easing, cubic-bezier(0.32, 0.72, 0, 1));\n\n &::backdrop {\n transition: opacity var(--wui-internal-settle-duration, 280ms)\n var(--wui-internal-settle-easing, cubic-bezier(0.32, 0.72, 0, 1));\n }\n}\n\n:host([placement='left']) dialog[open].is-visible,\n:host([placement='top']) dialog[open].is-visible,\n:host([placement='bottom']) dialog[open].is-visible {\n transform: translate(0, 0);\n}\n\ndialog.is-closing {\n pointer-events: none;\n transition: transform var(--wui-duration-drawer-exit, 240ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n}\n\ndialog.is-closing::backdrop {\n transition: opacity var(--wui-duration-drawer-exit, 240ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n}\n\ndialog[open].is-visible::backdrop {\n /* 拖拽期间由 JS 写入 --wui-internal-drag-backdrop-opacity 实现 backdrop 跟手淡出。 */\n opacity: var(--wui-internal-drag-backdrop-opacity, 1);\n}\n\n/*\n * drag zone 贴在 drawer 内缘,宽度由 --wui-drawer-drag-zone-size(默认 20px)控制便于触控命中;\n * 胶囊本身由 --wui-drawer-drag-bar-thickness × --wui-drawer-drag-bar-length(默认 4×56px)控制。\n * 胶囊绝对定位在 --wui-drawer-content-padding 的视觉中线(默认 20px → 中线 10px),\n * 并以半个胶囊厚度加 4px 呼吸间距兜底,使 padding 归零的 consumer 也留有可见内缩(见下方 max());\n * drag zone 仍覆盖内缘起 --wui-drawer-drag-zone-size,保证触控命中面积不随胶囊收窄。\n * 位置随 placement 切换:right 贴在左缘、left 贴在右缘、top 贴在底边、bottom 贴在顶边。\n *\n * 上下两条还要**让开开头尾两节**:drag zone 是 dialog 的绝对定位兄弟节点,压在 panel 的端头上,\n * 而那一端上正有 header(bottom)或 footer(top)——button 与点击都落在 drag zone 底下。让开量由\n * index.ts 用 ResizeObserver 量出该节的 border-box 高度写成 --wui-internal-drawer-*-inset,\n * 两者都不存在时为 0,drag zone 回到 panel 边缘。\n *\n * 胶囊与 drag zone 因此**不共用一套偏移**:两者都是 dialog 的直接子节点(兄弟,不是父子),\n * 胶囊恒定相对 dialog 定位、贴住可抓取边缘,drag zone 独自承担让开量。胶囊不是 drag zone 的后代,\n * `top` 相对 dialog 算,不会被那 60px 让位量一起推离 panel 边缘。\n * 让开量是 drag zone 一方的约束,不该外溢到纯视觉的胶囊上;反过来把 drag zone 推回 0 又会让 A6 的\n * header 遮挡立刻回归。两个约束只有靠结构解耦才同时成立,调数值必然牺牲其一。\n *\n * 代价是这一让开把胶囊所在的端头**腾空了**:胶囊贴回边缘后落在让开区里,两条带恒不相交。\n * 补洞的不是本元素,而是端头那条 drag edge `.wui-drawer-drag-edge`(见它的规则)——\n * 两者各管一段,缺一条胶囊就成了死装饰。R1 与 R3 在几何上本就不可同满足,\n * 这里只是把「不可同满足」拆成两段各自成立,而不是假装它们能同时成立。\n *\n * 按在 panel 边缘之外的坐标属于 backdrop:轻点走「点 backdrop 关闭」(click 链路回溯校验,\n * 见 index.ts 的 handleBackdropClick),拖拽松手不关闭。\n */\n.wui-drawer-drag-zone {\n touch-action: none;\n cursor: grab;\n\n position: absolute;\n z-index: 4;\n\n transition: opacity var(--wui-duration-feedback, 100ms) ease;\n}\n\ndialog.is-nested-lower .wui-drawer-drag-zone {\n pointer-events: none;\n opacity: 0;\n}\n\n:host([placement='right']) .wui-drawer-drag-zone {\n top: 0;\n left: 0;\n width: var(--wui-drawer-drag-zone-size, 20px);\n height: 100%;\n}\n\n:host([placement='left']) .wui-drawer-drag-zone {\n top: 0;\n right: 0;\n width: var(--wui-drawer-drag-zone-size, 20px);\n height: 100%;\n}\n\n:host([placement='top']) .wui-drawer-drag-zone,\n:host([placement='bottom']) .wui-drawer-drag-zone {\n right: 0;\n left: 0;\n width: 100%;\n height: var(--wui-drawer-drag-zone-size, 20px);\n}\n\n:host([placement='top']) .wui-drawer-drag-zone {\n /*\n * 让开 footer(不是 header):`top` 贴的是 panel**底边**,那一侧压住的是 footer。\n * 头部在内缘的另一端,与本 placement 的 drag zone 不重叠。\n */\n bottom: var(--wui-internal-drawer-footer-inset, 0);\n}\n\n:host([placement='bottom']) .wui-drawer-drag-zone {\n /* 对称:让开 header。左右两条不受影响(贴的是竖边,header / footer 都不在那里)。 */\n top: var(--wui-internal-drawer-header-inset, 0);\n}\n\n/*\n * 端头 drag edge:贴住可抓取边缘的一条常驻 drag zone,只在上下 placement 渲染(见 index.ts)。\n *\n * 它补的是上面那条让开量腾出的空档。胶囊贴回边缘后不在 drag zone 里了,用户却仍然看着它,\n * 于是「handle 靠近边缘」这个诉求必须连着可拖、可点亮一起兑现,否则胶囊只是装饰。\n *\n * **足迹不得超过它所代表的胶囊**:宽度取胶囊长度、水平居中,与胶囊同一套 left。\n * 这一条是本元素唯一真正的不变量,也是关 button 与消费侧 header 控件的安全保证——\n * 满宽会横扫 header 整个宽度:实测 consumer 把 `--wui-drawer-drag-bar-thickness`\n * 调到 24px 时内置关闭 button 与消费侧左右 button 各被吃 40%、40px 时各 100%。\n * 关 button 从来不是特例,**满宽才是病根**。收窄后关 button 贴右缘、胶囊居中,\n * 只要胶囊自己没伸到 button 所在的位置,两者就永不相交——对所有 thickness / space-4\n * 组合都成立(断言按 8 组组合逐一量相交面积),且完全不依赖 close-top。\n *\n * 代价明说:触摸命中面积只有胶囊长度(默认 56×12)。可接受——\n * **drag zone 本身没动**(满宽 20–32px 仍在让开位上),大面积起手能力并未丢失,\n * drag edge 只负责兑现「你看到的那个 drag bar 按得动」。\n *\n * 收窄后仍压不死的情况只有一类:消费侧把可点控件放在胶囊那条水平跨度内。\n * 纯 CSS 无法消除(带要覆盖胶囊就必须有胶囊那么宽,而胶囊就画在那里)。\n * 但那不是新引入的视觉冲突——thickness 40px 时胶囊自身已压在 header 内容上,\n * drag edge 只是把「胶囊盖住的地方」一并变成可拖。是 consumer 要求了一个粗 drag bar,\n * 而 drag bar 就画在 header 上。\n *\n * 实测:给该控件抬 z-index ≥ 5 **救不回来**——Chromium 里本元素始终压在 slotted\n * 节点之上,抬 z-index 无效。唯一量得着的杠杆是 `--wui-drawer-drag-bar-length`:\n * 它同时决定胶囊长度与本元素宽度,缩短它就同比例缩短本元素(README 已写明)。\n *\n * 高度取两条来源的 **max()**:\n * - `中线 + 半个厚度` 恰是胶囊的外沿;\n * - `--wui-drawer-drag-edge-size`(默认 20px)是给手指的命中厚度。\n * 加这条是因为端头只有 12px(=中线 10 + 半个厚度 2)时真机反馈「很难拖动到」。\n * 默认值取 20px 的依据:与主 drag zone `--wui-drawer-drag-zone-size` 的默认厚度同值,\n * 于是**胶囊的命中带恒不窄于主 drag zone**,「按你看到的 drag bar」不会比「按旁边的带子」更难。\n * 再往上加会开始吃掉 header 里控件的位置(端头整块可拖后,那一节是要放控件的)。\n *\n * 取 max() 而非直接用 token,是为了保住包含关系:中线的下限(`max()` 第二项)保证\n * `中线 - 半个厚度 ≥ 4 > 0`,而 drag edge 从 panel 边缘(top/bottom placement 下恒为 0)起算,\n * 于是**胶囊带恒被 drag edge 带包含**——consumer 把 edge-size 调到比胶囊带还小也吃不掉它。\n * 这条包含关系是 token 定义推出来的,不是靠约定维持的。\n *\n * 竖向仍比胶囊多出「panel 边缘到胶囊近侧」那一条(≤ 中线 − 半个厚度,默认 8px),\n * 用来让指针不必精确压在 4px 粗的胶囊上。\n */\n.wui-drawer-drag-edge {\n touch-action: none;\n cursor: grab;\n\n position: absolute;\n z-index: 4;\n\n /* 只在上下 placement 渲染(见 index.ts),故居中方式与胶囊的那条规则逐字相同。 */\n left: calc(50% - var(--wui-drawer-drag-bar-length, 56px) / 2);\n\n width: var(--wui-drawer-drag-bar-length, 56px);\n height: max(\n calc(var(--wui-internal-drag-bar-center) + var(--wui-drawer-drag-bar-thickness, 4px) / 2),\n var(--wui-drawer-drag-edge-size, 20px)\n );\n\n transition: opacity var(--wui-duration-feedback, 100ms) ease;\n}\n\n/*\n * 嵌套下层时整套拖拽提示一起退场,drag edge 也不例外——否则下层 drawer 会留一条看不见起点、\n * 按下去却能拖的悬空 drag edge,比悬空胶囊更费解。\n */\ndialog.is-nested-lower .wui-drawer-drag-edge {\n pointer-events: none;\n opacity: 0;\n}\n\n:host([placement='bottom']) .wui-drawer-drag-edge {\n top: 0;\n}\n\n:host([placement='top']) .wui-drawer-drag-edge {\n bottom: 0;\n}\n\n.wui-drawer-drag-bar {\n /*\n * 胶囊是纯视觉件,命中与拖拽全部归两条 drag zone(端头 drag edge + 让开量 drag zone)。\n * pointer-events: none 是这件事得以成立的前提:\n * 胶囊叠在 drag zone 之上(z-index 更高),若参与命中测试,按在胶囊上的手势会被胶囊吃掉,\n * drag zone 的 @pointerdown 收不到事件,从胶囊起手的拖拽直接失效。端头 drag edge 按构造覆盖胶囊带,\n * 所以「从胶囊起手」与「从 drag edge 空白处起手」命中的是同一个元素。\n *\n * 它同时解掉了 hover / active 的归属:pointer-events 为 none 时胶囊永不成为 hover 目标,\n * 指针落在胶囊上时下面的 drag zone/drag edge 照常 :hover,「drag zone 点亮胶囊」在四个 placement 上都成立,\n * 不必给胶囊另挂监听(同一手势会触发两次)。\n */\n pointer-events: none;\n\n position: absolute;\n\n /*\n * 5 高于 drag zone 与端头 drag edge(均 4),与 panel 主体(无 z-index 的静态盒)也不冲突。\n * 与 close button 同为 5:两者之间靠的不是层序而是几何(胶囊居中、button 贴右缘)。\n * 而胶囊 `pointer-events: none`,从不参与命中测试,层序高低对它没有命中含义。\n */\n z-index: 5;\n\n width: var(--wui-drawer-drag-bar-thickness, 4px);\n height: var(--wui-drawer-drag-bar-length, 56px);\n border-radius: var(--wui-radius-control, calc(infinity * 1px));\n\n background: color-mix(in srgb, var(--wui-color-text, #1b1b1b) 24%, transparent);\n\n /* 与 drag zone 同一条淡入淡出,嵌套下层退场时两者同步(见下面的 is-nested-lower)。 */\n transition: opacity var(--wui-duration-feedback, 100ms) ease;\n}\n\n/*\n * 嵌套下层时整套拖拽提示一起退场,drag zone 与胶囊一起。胶囊是 drag zone 的兄弟而非后代,\n * drag zone 那条规则的 opacity 作用不到它,必须显式列出,否则下层 drawer\n * 会剩一条没人操作的悬空胶囊。放在基础规则之后,保住 no-descending-specificity 的升序。\n */\ndialog.is-nested-lower .wui-drawer-drag-bar {\n pointer-events: none;\n opacity: 0;\n}\n\n:host([placement='right']) .wui-drawer-drag-bar {\n top: calc(50% - var(--wui-drawer-drag-bar-length, 56px) / 2);\n left: calc(var(--wui-internal-drag-bar-center) - var(--wui-drawer-drag-bar-thickness, 4px) / 2);\n}\n\n:host([placement='left']) .wui-drawer-drag-bar {\n top: calc(50% - var(--wui-drawer-drag-bar-length, 56px) / 2);\n right: calc(var(--wui-internal-drag-bar-center) - var(--wui-drawer-drag-bar-thickness, 4px) / 2);\n}\n\n:host([placement='top']) .wui-drawer-drag-bar,\n:host([placement='bottom']) .wui-drawer-drag-bar {\n width: var(--wui-drawer-drag-bar-length, 56px);\n height: var(--wui-drawer-drag-bar-thickness, 4px);\n}\n\n:host([placement='top']) .wui-drawer-drag-bar {\n bottom: calc(var(--wui-internal-drag-bar-center) - var(--wui-drawer-drag-bar-thickness, 4px) / 2);\n left: calc(50% - var(--wui-drawer-drag-bar-length, 56px) / 2);\n}\n\n:host([placement='bottom']) .wui-drawer-drag-bar {\n top: calc(var(--wui-internal-drag-bar-center) - var(--wui-drawer-drag-bar-thickness, 4px) / 2);\n left: calc(50% - var(--wui-drawer-drag-bar-length, 56px) / 2);\n}\n\n/*\n * 通用兄弟选择器:胶囊排在这两个 drag zone 之后(index.ts 的 render 里同一个模板表达式产出\n * 这三个节点),`~` 因此在**所有 placement** 上都成立,且与「drag edge 是否存在」无关——\n * 上下 placement 多一条 drag edge、左右 placement 只有一条,写成 `+` 会让 drag edge 那条静默失配。\n * 这两条规则写成后代选择器(.zone:hover .bar)就不满足当前结构——不报错、\n * 只是静默不变色,是本结构最隐蔽的一处回归。\n */\n.wui-drawer-drag-edge:hover ~ .wui-drawer-drag-bar,\n.wui-drawer-drag-edge:active ~ .wui-drawer-drag-bar,\n.wui-drawer-drag-zone:hover ~ .wui-drawer-drag-bar,\n.wui-drawer-drag-zone:active ~ .wui-drawer-drag-bar {\n background: color-mix(in srgb, var(--wui-color-text, #1b1b1b) 40%, transparent);\n}\n\n/*\n * 达到关闭阈值的确认态:比拖拽静息态更实的中性灰。\n * 55% 这一档是看着真浏览器定的:拖拽全程 capture 使 hover/active 恒成立,40% 那档\n * 在拖拽中始终可见,确认态必须明显压过它——试过的 25%(比静息态还淡)越过阈值\n * 读成「变弱」,24% 的静息灰与它几乎无法区分。55% 相对静息 40% 有可辨的一档\n * 落差,又不至于重到读成实心深色条;再往上(60%)与 55% 的观感差别小于它与 40%\n * 的差别,收益不抵「更重」。\n * 这里的选择器特异性 (0,3,1) 高于 hover/active 的 (0,3,0),靠特异性而非顺序压盖。\n * 拆成兄弟后仍**保留 .wui-drawer-drag-zone 这一段**(用 `~` 接胶囊):若缩写成\n * `dialog.is-drag-close .wui-drawer-drag-bar` 会掉到 (0,2,1),反过来被 (0,3,0) 的\n * hover/active 压过,确认态在拖拽全程(capture 使 hover 恒成立)就不再显形。\n */\ndialog.is-drag-close .wui-drawer-drag-zone ~ .wui-drawer-drag-bar {\n background: color-mix(in srgb, var(--wui-color-text, #1b1b1b) 55%, transparent);\n}\n\n:host([placement='left']) dialog {\n right: auto;\n left: 0;\n transform: translateX(-100%);\n}\n\n:host([placement='top']) dialog,\n:host([placement='bottom']) dialog {\n width: 100vw;\n max-width: 100vw;\n height: var(--wui-drawer-height, 300px);\n max-height: 90dvh;\n}\n\n:host([placement='top']) dialog {\n top: 0;\n right: 0;\n bottom: auto;\n left: 0;\n transform: translateY(-100%);\n}\n\n:host([placement='bottom']) dialog {\n top: auto;\n right: 0;\n bottom: 0;\n left: 0;\n transform: translateY(100%);\n}\n\n/*\n * 拖拽中的 grabbing 光标。dialog 复合前缀使其特异性恒高于上方 placement 变体,\n * stylelint 的顺序比较对这种结构必然告警;cascade 上此规则不依赖 placement,\n * 置于其后仅为可读性,故显式豁免。\n */\n/* stylelint-disable-next-line no-descending-specificity */\ndialog.is-dragging .wui-drawer-drag-zone {\n cursor: grabbing;\n}\n\n.wui-drawer-body {\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-drawer-radius, var(--wui-radius-overlay, 28px));\n display: flex;\n flex-direction: column;\n height: 100%;\n background-color: var(--wui-drawer-bg, var(--wui-color-surface-overlay, rgb(248 248 248 / 0.92)));\n}\n\n.wui-drawer-header {\n /*\n * 刻意没有分割线(footer 同)。panel 主体 `.wui-drawer-body` 自带 `.wui-glass` 的描边环与\n * 投影,分区靠 padding 与排版层级读出来;再叠一条写死的 6% 黑线,在半透明玻璃面上会读成\n * 一道硬接缝,且它不跟随任何 token——换外观时是块跟不上的残留。\n */\n display: flex;\n align-items: center;\n justify-content: space-between;\n\n padding: var(--wui-drawer-header-padding, var(--wui-space-4, 16px) var(--wui-space-5, 20px));\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-snug, 1.4);\n}\n\n.wui-drawer-header[hidden] {\n display: none;\n}\n\n.wui-drawer-content {\n overflow-y: auto;\n overscroll-behavior: contain;\n flex: 1;\n\n padding: var(--wui-drawer-content-padding, var(--wui-space-5, 20px));\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-drawer-footer {\n flex-shrink: 0;\n padding: var(--wui-drawer-footer-padding, var(--wui-space-4, 16px) var(--wui-space-5, 20px));\n}\n\n.wui-drawer-footer[hidden] {\n display: none;\n}\n\n/*\n * 上下 placement:贴着可抓边缘的那一节(bottom 的 header、top 的 footer)整块可拖。\n *\n * 让开靠**逐个控件**在事件层判断(见 index.ts 的 _handleSectionDragPointerDown),\n * 不靠把 drag zone 铺上来——铺上来会把这一节里的控件吞掉,正是 A6 修掉的那条缺陷。\n * 所以这里只需要手势与光标两件样式,命中关系由那一节自己承担。\n *\n * `touch-action: none` 不可省:否则触摸落在标题文字或控件上时,浏览器会先把它\n * 判成滚动意图,越过阈值就 pointercancel,拖拽根本起不来(与 drag zone 同一条理由)。\n * 它是祖先节点,触摸落在深层 slotted 子元素上时同样生效——touch-action 沿命中链\n * 求交,不要求落在声明元素本身上。\n *\n * 按 `[draggable]` 收口:draggable 为假时这一节不接手势,也不该被夺走触摸手势。\n *\n * 再按 `dialog:not(.is-nested-lower)` 收口,与事件层闸门(index.ts 的\n * _handleSectionDragPointerDown)和 drag zone / drag edge / 胶囊那三条 `is-nested-lower` 退场\n * 判同一条:嵌套下层的 drawer 整套拖拽提示一起退场。这里漏掉就会只剩提示面没退——\n * 而 `touch-action` 沿命中链求交,下层 header 区域的触摸滚动是被真的夺走的,\n * 那里却拖不动:既骗光标又吞手势,比留着可拖的提示面更糟。\n */\n:host([placement='bottom'][draggable]) dialog:not(.is-nested-lower) .wui-drawer-header,\n:host([placement='top'][draggable]) dialog:not(.is-nested-lower) .wui-drawer-footer {\n touch-action: none;\n cursor: grab;\n}\n\n:host([placement='bottom'][draggable]) dialog:not(.is-nested-lower).is-dragging .wui-drawer-header,\n:host([placement='top'][draggable]) dialog:not(.is-nested-lower).is-dragging .wui-drawer-footer {\n cursor: grabbing;\n}\n\n/* ===== 关闭 button(绝对定位)=====\n top 恰好等于 header padding 的垂直分量(16px),因此挂 --wui-space-4 与 header padding\n 同源:覆盖该级时二者一起动,注释里写死的对齐关系不会断。\n right 是 button 到 drawer 右边沿的独立偏移(16px),不对齐相邻元素的节奏值(header\n 的水平分量是 20px,与它并不相等),参照系是容器边缘而非相邻元素之间的节奏,故与\n --wui-image-preview-edge-gap 等边缘对齐值同属一族,保持字面量。挂到 --wui-space-4\n 本身并不会造成视觉变化(该级默认仍是 16px)——不挂阶的理由是语义归属,不是视觉风险。 */\n\n.wui-drawer-close {\n position: absolute;\n\n /*\n * 5,高于端头 drag edge(4)。\n *\n * 端头 drag edge 收窄到胶囊长度之后,默认与常规 token 下它与本 button 的相交面积**恒为 0px²**\n * (实测 thickness 4/16/24/40 × space-4 16/8/4 共 12 组,测试按这个矩阵逐组断言),\n * 正常情形靠几何就够。\n * 这一层防的是几何失效的那一格:`--wui-drawer-drag-bar-length` 同样是公开 token,\n * 调到 drag bar 伸到 button 所在的位置时相交面积不为 0(实测 320px 时为 78px²),\n * 此时唯一还能救的就是层序。\n *\n * 为它单独抬这一层是有实测依据的**不对称**,不是泛泛的防御性声明:\n * 消费侧把控件塞进 drag bar 跨度里时,同一招不管用——Chromium 里端头 drag edge 始终压在\n * **slotted** 节点之上,给 slotted 元素抬 z-index 无效(已在测试注释里记下实测)。\n * 本 button 是组件自己画的 shadow 节点,层序对它有效,所以它是这条链上唯一能自救的节点。\n */\n z-index: 5;\n top: var(--wui-drawer-close-top, var(--wui-space-4, 16px));\n right: var(--wui-drawer-close-right, 16px);\n bottom: auto;\n left: auto;\n}\n\n/* Headless 模式下,dialog 只作为 overlay 容器,不渲染内置 UI */\n:host([headless]) dialog {\n /*\n * inset 只属于浮动卡片几何;headless 嵌套在非 headless 外层内时会沿\n * flattened tree 继承到 8px,破坏 JS 读取该变量得到的贴边几何假设\n * (parseFloat('0px') 与回退 0 语义一致),故显式归零。\n */\n --wui-internal-drawer-inset: 0px;\n\n width: auto;\n max-width: none;\n}\n\n/* ===== 浮动卡片视觉语言 ===== */\n\n/*\n * 非 headless 时 drawer 是四周留边的圆角卡片,与 layout sidebar 的设计语言一致;\n * 弹性拖拽使边距变化(8px → 更大)是合法状态而非渲染缺口。\n * headless 保持贴边几何,视觉由 Consumer 自绘。\n *\n * 留边经公开 token --wui-drawer-inset 暴露(默认 8px);\n * 置 0 即贴边几何(与 headless 相同),通常与 --wui-drawer-radius: 0 搭配。\n *\n * 闭合位移需要越过留边才能完全滑出视口,因此闭合态 transform 以\n * --wui-internal-drawer-inset 补偿;JS 在 controlled 悬停终态读取同一变量,\n * 保证两条关闭路径的行为一致。headless 将该变量显式归零(见上方 Headless 模式)。\n */\n\n/* 默认 right:四周留边、高度随上下边距拉伸,闭合位移越过右侧留边 */\n:host(:not([headless])) dialog {\n --wui-internal-drawer-inset: var(--wui-drawer-inset, 8px);\n --wui-internal-drawer-nested-depth: 0;\n --wui-internal-drawer-nested-scale: 1;\n --wui-internal-drawer-nested-shift: 0px;\n\n top: var(--wui-internal-drawer-inset);\n right: var(--wui-internal-drawer-inset);\n bottom: var(--wui-internal-drawer-inset);\n left: auto;\n transform: translateX(calc(100% + var(--wui-internal-drawer-inset)));\n\n height: auto;\n border-radius: var(--wui-drawer-radius, var(--wui-radius-overlay, 28px));\n}\n\n:host(:not([headless])) .wui-drawer-body {\n border-radius: inherit;\n}\n\n:host(:not([headless])[placement='left']) dialog {\n top: var(--wui-internal-drawer-inset);\n right: auto;\n bottom: var(--wui-internal-drawer-inset);\n left: var(--wui-internal-drawer-inset);\n transform: translateX(calc(-100% - var(--wui-internal-drawer-inset)));\n\n height: auto;\n}\n\n/*\n * 上下 placement 的高度在这里再声明一次,且不能省。\n *\n * 上面 `:host(:not([headless])) dialog` 的 `height: auto` 与基础 placement 块里那条挂\n * `--wui-drawer-height`(默认 300px)的 height 声明特异性相同(都是 (0,2,1))、位置更靠后,\n * 会把那条整条压掉。左右不受影响——它们的 auto 有 top / bottom 双边同时约束,仍算得出\n * 确定高度;上下只有单边贴边,dialog 于是变成内容高度。后果有两层:\n *\n * - `--wui-drawer-height` 失效(README 记录它是上下 drawer 的高度,demo 的自定义高 button\n * 直接指向它);\n * - `.wui-drawer-body` 的 `height: 100%` 解析成 auto,`.wui-drawer-content` 拿到的是\n * 内容高度而不是 panel 高度,`flex: 1` 收缩不了自己,长内容直接溢出 dialog 盒子\n * (overflow: visible)落到视口之下——够不着,也滚不动。\n */\n:host(:not([headless])[placement='top']) dialog {\n top: var(--wui-internal-drawer-inset);\n right: var(--wui-internal-drawer-inset);\n bottom: auto;\n left: var(--wui-internal-drawer-inset);\n transform: translateY(calc(-100% - var(--wui-internal-drawer-inset)));\n\n width: auto;\n max-width: none;\n height: var(--wui-drawer-height, 300px);\n}\n\n:host(:not([headless])[placement='bottom']) dialog {\n top: auto;\n right: var(--wui-internal-drawer-inset);\n bottom: var(--wui-internal-drawer-inset);\n left: var(--wui-internal-drawer-inset);\n transform: translateY(calc(100% + var(--wui-internal-drawer-inset)));\n\n width: auto;\n max-width: none;\n height: var(--wui-drawer-height, 300px);\n}\n\n/*\n * 开放态守卫:上方闭合 transform 与基础 `dialog[open].is-visible` 特异性相同且\n * 在文件更后方,必须以更高特异性显式恢复打开位,否则 drawer 停在闭合位。\n *\n * Nested 层叠(对齐卡片堆栈效果):非顶层 drawer 按 0.95^depth 缩放并向屏幕内侧\n * 平移(shift 为正,朝屏幕中心方向),使缩小的卡片在顶层后方露出阶梯式卡片边缘。\n * 用独立 translate/scale 属性(而非并入 transform):\n * 它们是标准可过渡属性,变量变化即触发平滑过渡;且与 transform 轴上的\n * 闭合/拖拽/弹簧位移互不覆写(独立属性先于 transform 应用后合并)。\n * JS 层序管理写入 depth/scale/shift 三个内部变量,单层使用时全为中性值。\n *\n * 层叠变化的平滑过渡(450ms easeOutQuint 类曲线)。\n * transform 保持既有 enter/exit 时长;translate/scale 只在层叠数变化时更新,\n * 用更长的 nested 时长。\n */\n:host(:not([headless])) dialog[open].is-visible {\n transform: translate(0, 0);\n translate: calc(var(--wui-internal-drawer-nested-shift) * -1) 0;\n scale: var(--wui-internal-drawer-nested-scale);\n transition:\n transform var(--wui-duration-drawer-enter, 280ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1)),\n translate var(--wui-duration-drawer-nested, 450ms) cubic-bezier(0.22, 1, 0.36, 1),\n scale var(--wui-duration-drawer-nested, 450ms) cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n:host(:not([headless])[placement='left']) dialog[open].is-visible {\n transform: translate(0, 0);\n translate: var(--wui-internal-drawer-nested-shift) 0;\n}\n\n:host(:not([headless])[placement='top']) dialog[open].is-visible {\n transform: translate(0, 0);\n translate: 0 var(--wui-internal-drawer-nested-shift);\n}\n\n:host(:not([headless])[placement='bottom']) dialog[open].is-visible {\n transform: translate(0, 0);\n translate: 0 calc(var(--wui-internal-drawer-nested-shift) * -1);\n}\n\n/*\n * 窄视口的层叠基准:同一个 `--wui-drawer-nested-peek-base` 用更小的 A。\n * 桌面基准(43.2px)来自 index.ts 的 `CSS.registerProperty` initialValue,这里只覆盖窄视口,\n * 因此 A 的桌面真相源仍只有注册项一处。\n *\n * 28.8px 沿用桌面:窄屏 3:2,窄屏随桌面同比缩小:桌面 43.2px、窄屏 28.8px,3:2 不变。\n * 第一跳仍是 28.8·ln2 = 19.96px,高于 12px 的线性步进;四层总宽 39.93px。\n *\n * 屏内余量不是定 3:2 的理由:390px 视口上单层左缘有 390 − 8 − 320 = 62px,\n * 桌面基准的四层总宽 43.2·ln4 = 59.89px 也放得下(总宽 74.86px 才会溢出)。\n * 保留窄屏档是为了不改变既定比例关系,同时给窄屏留出更宽的 drawer 可视区。\n *\n * 断点字面量与 shared/overlay/nested-drawer-layers.ts 的 matchMedia 查询各有一份:\n * 跨断点时 JS 侧靠它重算已打开的堆叠,两处必须同步(见该文件的同名常量)。\n *\n * 消费方在页面里给宿主写的值仍然优先——外层树的声明压过 shadow 内的 :host 规则。\n */\n@media (width <= 640px) {\n :host(:not([headless])) {\n --wui-drawer-nested-peek-base: 28.8px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n /*\n * reduce 下:位移一律归零、transform 不参与过渡(仅 opacity 反馈)。\n * 全部 !important:压过开放守卫(嵌套层叠变量的特异性猫鼠不可维护),\n * 排除 .is-dragging —— reduce 下拖拽仍需实时跟手。\n * JS 收尾移除内联 transform 后立即为终态,无 280ms 过渡尾巴。\n */\n dialog:not(.is-dragging) {\n transition: opacity var(--wui-duration-feedback, 100ms) ease !important;\n\n &::backdrop {\n transition-duration: var(--wui-duration-feedback, 100ms) !important;\n }\n }\n\n dialog[open].is-visible:not(.is-dragging) {\n transform: none !important;\n translate: none !important;\n scale: none !important;\n }\n}\n"],"mappings":";AAAA,IAAA,gBAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/shared/group-management/index.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\nimport { ContextConsumer, ContextProvider, createContext, type Context } from '@lit/context'\nimport type { ReactiveController, ReactiveControllerHost } from 'lit'\n\n/*\n * 下行通道基于 @lit/context(ADR-0032):root 上的 ContextProvider 以\n * `ReadonlyMap<HTMLElement, Context>` 为载荷向全子树广播,子项侧\n * ContextConsumer 订阅并按自身元素取条目。context-request 事件 composed,\n * 可穿过中间 shadow 边界;custom element 升级顺序保证祖先先于后代升级,\n * provider 总是先于子项请求就绪。成员追踪(querySelectorAll +\n * MutationObserver/slotchange)、点击归因与 setItemSelected 上行写回\n * 不在下行通道内,仍由 GroupController 驱动。\n */\n\nexport interface SelectionGroupContext {\n readonly disabled: boolean\n readonly rovingTabStop: boolean\n readonly tabStop?: boolean\n}\n\nexport interface ButtonGroupContext {\n readonly direction: 'horizontal' | 'vertical'\n readonly isLast: boolean\n}\n\n// 载荷类型仅编译期品牌化;按载荷形状分两个 key,避免异构树串扰。\n// createContext 泛型顺序为 <ValueType, K>。\nconst SELECTION_GROUP_CONTEXT = createContext<ReadonlyMap<HTMLElement, SelectionGroupContext>, symbol>(\n Symbol('web-ui.selection-group-context')\n)\n\nconst BUTTON_GROUP_CONTEXT = createContext<ReadonlyMap<HTMLElement, ButtonGroupContext>, symbol>(\n Symbol('web-ui.button-group-context')\n)\n\n// 两个 key 的运行时形状一致(Map 载荷),泛型插件内部统一按宽松类型驱动。\ntype AnyGroupContext = Context<symbol, ReadonlyMap<HTMLElement, unknown>>\n\nconst SELECTION_GROUP_CONTEXT_LOOSE = SELECTION_GROUP_CONTEXT as unknown as AnyGroupContext\nconst BUTTON_GROUP_CONTEXT_LOOSE = BUTTON_GROUP_CONTEXT as unknown as AnyGroupContext\n\nexport const selectionGroupContextKey: AnyGroupContext = SELECTION_GROUP_CONTEXT_LOOSE\nexport const buttonGroupContextKey: AnyGroupContext = BUTTON_GROUP_CONTEXT_LOOSE\nexport const GROUP_MEMBER_DISABLED_CHANGE_EVENT = 'wui-group-member-disabled-change'\n\ninterface GroupControllerHost extends HTMLElement, ReactiveControllerHost {\n readonly renderRoot: Element | DocumentFragment\n}\n\n/**\n * 创建受管理 group 的子项侧能力:订阅最近 provider 的 context 载荷并提取\n * 自身条目。在 `make()` 时完成注册,组件无需再编写 constructor 样板。\n */\nexport function defineGroupManaged<Context>(\n item: HTMLElement & ReactiveControllerHost,\n options: {\n /** 与所属根组件使用的 context key 一致。 */\n context: AnyGroupContext\n requestUpdate: () => void\n equals?: (a: Context | undefined, b: Context | undefined) => boolean\n }\n) {\n return definePlugin(() => {\n let context: Context | undefined\n const equals = options.equals ?? Object.is\n\n const consumer = new ContextConsumer(item, {\n context: options.context,\n subscribe: true,\n callback: payload => {\n const next = (payload as ReadonlyMap<HTMLElement, Context> | undefined)?.get(item)\n if (equals(context, next)) return\n context = next\n options.requestUpdate()\n }\n })\n\n /*\n * 离开 group(被移出 provider 子树)时 ContextConsumer 只是静默退订,\n * 不会收到「条目已移除」的通知:显式清空上下文,恢复独立控件语义\n * (如 checkbox 恢复自身表单提交)。跨组移动先断连清空、再重连订阅新组。\n */\n item.addController({\n hostDisconnected() {\n if (equals(context, undefined)) return\n context = undefined\n options.requestUpdate()\n }\n })\n\n return {\n getContext(): Context | undefined {\n return context\n },\n // 供测试或特殊场景确认订阅状态。\n isSubscribed(): boolean {\n return consumer.value !== undefined\n }\n }\n })\n}\n\nexport interface GroupPresentationOptions<Item extends HTMLElement, Context> {\n readonly host: HTMLElement\n /** 与子项 defineGroupManaged 使用的 context key 一致。 */\n readonly context: AnyGroupContext\n readonly getItems: () => readonly Item[]\n readonly getContext: (item: Item, index: number, items: readonly Item[]) => Context\n}\n\n/**\n * 为分组子项注册可逆的展示上下文(@lit/context 下行通道)。不持有公开 DOM\n * 属性;sync() 以新 Map 载荷整体广播,成员变化体现为子项条目消失。\n */\nexport function defineGroupPresentation<Item extends HTMLElement, Context>(\n options: GroupPresentationOptions<Item, Context>\n) {\n return definePlugin(() => {\n const provider = new ContextProvider(options.host, {\n context: options.context,\n initialValue: new Map<HTMLElement, unknown>()\n })\n\n return {\n sync(): boolean {\n const nextItems = options.getItems()\n const payload = new Map<HTMLElement, unknown>()\n nextItems.forEach((item, index) => payload.set(item, options.getContext(item, index, nextItems)))\n // 新 Map 实例保证 Object.is 判定为变更,全量通知订阅者。\n // 每次 hostUpdated 都重播(即使成员/方向未变)是有意形态:每次新建\n // context 对象,但子项 defineGroupManaged 的 equals 门控 requestUpdate\n // (标量字段比较),重播不产生多余渲染;代价是比「仅成员变化才通知」\n // 更频繁的广播事件,换取零状态追踪的简单性(与 GroupController 的\n // 全量 sync 对齐)。\n provider.setValue(payload)\n return true\n },\n disconnect() {\n provider.setValue(new Map())\n }\n }\n })\n}\n\nexport interface GroupLifecycle {\n sync(): boolean\n disconnect(): void\n handleChange?(event: Event): void\n handleKeyDown?(event: KeyboardEvent): void\n}\n\ninterface GroupControllerOptions {\n afterSync?: () => void\n observeAttributes?: readonly string[]\n}\n\n/**\n * 把组生命周期插件接入 Lit,协调规则不落在组件生命周期方法里。\n */\nexport class GroupController implements ReactiveController {\n private connected = false\n private observer: MutationObserver | null = null\n private observedSlots = new Set<HTMLSlotElement>()\n constructor(\n private readonly host: GroupControllerHost,\n private readonly lifecycle: GroupLifecycle,\n private readonly options: GroupControllerOptions = {}\n ) {\n host.addController(this)\n }\n\n hostConnected() {\n this.connected = true\n this.host.addEventListener('change', this.handleChange, true)\n this.host.addEventListener('keydown', this.handleKeyDown, true)\n this.host.addEventListener(GROUP_MEMBER_DISABLED_CHANGE_EVENT, this.handleMemberDisabledChange, true)\n this.observeHost()\n this.observeSlots()\n this.sync()\n }\n\n hostUpdated() {\n this.observeSlots()\n this.sync()\n }\n\n hostDisconnected() {\n this.connected = false\n this.host.removeEventListener('change', this.handleChange, true)\n this.host.removeEventListener('keydown', this.handleKeyDown, true)\n this.host.removeEventListener(GROUP_MEMBER_DISABLED_CHANGE_EVENT, this.handleMemberDisabledChange, true)\n this.disconnectSlots()\n this.disconnectHostObserver()\n this.lifecycle.disconnect()\n }\n\n private readonly handleChange = (event: Event) => {\n this.lifecycle.handleChange?.(event)\n }\n\n private readonly handleKeyDown = (event: KeyboardEvent) => {\n this.lifecycle.handleKeyDown?.(event)\n }\n\n private readonly handleMemberDisabledChange = () => {\n this.sync()\n }\n\n private readonly handleSlotChange = () => {\n if (!this.connected) return\n this.sync()\n }\n\n private readonly handleMutations = () => {\n if (!this.connected) return\n this.sync()\n }\n\n sync() {\n if (!this.lifecycle.sync()) return\n this.options.afterSync?.()\n }\n\n private observeHost() {\n this.disconnectHostObserver()\n // 监听 light DOM 的增删,覆盖 Vue v-if / React && 的 comment 锚点替换与深层包裹\n this.observer = new MutationObserver(this.handleMutations)\n const observerOptions: MutationObserverInit = { childList: true, subtree: true }\n if (this.options.observeAttributes?.length) {\n observerOptions.attributes = true\n observerOptions.attributeFilter = [...this.options.observeAttributes]\n }\n this.observer.observe(this.host, observerOptions)\n }\n\n private disconnectHostObserver() {\n if (this.observer) {\n this.observer.disconnect()\n this.observer = null\n }\n }\n\n private observeSlots() {\n const root = this.host.renderRoot as Element\n const slots = root.querySelectorAll<HTMLSlotElement>('slot')\n for (const slot of slots) {\n if (!this.observedSlots.has(slot)) {\n slot.addEventListener('slotchange', this.handleSlotChange)\n this.observedSlots.add(slot)\n }\n }\n // 清理已移除的 slot(热更新等场景)\n for (const slot of this.observedSlots) {\n if (!root.contains(slot)) {\n slot.removeEventListener('slotchange', this.handleSlotChange)\n this.observedSlots.delete(slot)\n }\n }\n }\n\n private disconnectSlots() {\n for (const slot of this.observedSlots) {\n slot.removeEventListener('slotchange', this.handleSlotChange)\n }\n this.observedSlots.clear()\n }\n}\n\nexport interface GroupCoordinatorOptions<Item extends HTMLElement, Value> {\n readonly host: HTMLElement\n readonly getItems: () => readonly Item[]\n readonly getValue: () => Value\n readonly setValue: (value: Value) => void\n readonly getDisabled: () => boolean\n readonly isItem: (target: EventTarget | null) => target is Item\n readonly isItemSelected: (item: Item, value: Value) => boolean\n readonly isItemTabStop?: (item: Item, items: readonly Item[]) => boolean\n readonly getItemSelected?: (item: Item) => boolean\n readonly getNextValue: (item: Item, value: Value) => Value\n readonly valuesEqual: (a: Value, b: Value) => boolean\n readonly copyValue: (value: Value) => Value\n readonly setItemSelected: (item: Item, selected: boolean) => void\n readonly dispatchValueChange: () => void\n readonly getRovingTabStop?: (item: Item, index: number, items: readonly Item[], value: Value) => boolean\n readonly handleKeyDown?: (event: KeyboardEvent) => void\n /**\n * 交互已发生但值没有变化时的回调(例如单选组成员 value 重复:点击同值成员\n * 不会改变 value,却必须把选中态收到被点击的那一项)。缺省视为无事可做。\n */\n readonly resyncOnUnchangedValue?: (item: Item) => void\n}\n\nconst ENABLED_SELECTION_CONTEXT: SelectionGroupContext = { disabled: false, rovingTabStop: false }\nconst ENABLED_ROVING_SELECTION_CONTEXT: SelectionGroupContext = { disabled: false, rovingTabStop: true }\nconst DISABLED_SELECTION_CONTEXT: SelectionGroupContext = { disabled: true, rovingTabStop: false }\n\nfunction getSelectionContext(disabled: boolean, rovingTabStop: boolean, tabStop?: boolean): SelectionGroupContext {\n const context = disabled\n ? DISABLED_SELECTION_CONTEXT\n : rovingTabStop\n ? ENABLED_ROVING_SELECTION_CONTEXT\n : ENABLED_SELECTION_CONTEXT\n return tabStop === undefined ? context : { ...context, tabStop }\n}\n\n/**\n * 在一个内部插件接口后收敛选中态、继承禁用与受管子项成员关系。\n * 禁用态经 @lit/context 下行广播;选中态上行写回保持直写。\n */\nexport function defineGroupCoordinator<Item extends HTMLElement, Value>(options: GroupCoordinatorOptions<Item, Value>) {\n return definePlugin(() => {\n const provider = new ContextProvider(options.host, {\n context: SELECTION_GROUP_CONTEXT_LOOSE,\n initialValue: new Map<HTMLElement, SelectionGroupContext>()\n })\n\n let lastValue: Value | undefined\n let lastDisabled: boolean | undefined\n let lastItems = new Set<Item>()\n let initialized = false\n\n const sync = (): boolean => {\n const nextItems = options.getItems()\n const value = options.getValue()\n const disabled = options.getDisabled()\n // 成员变化也要重放选中写回:新成员需继承当前选中态。\n const membershipChanged = nextItems.length !== lastItems.size || [...nextItems].some(item => !lastItems.has(item))\n // 子项可能在宿主首次连接后才完成 custom-element upgrade;值变得可读时,\n // 即使组值和成员集合没变,也要修复之前写入的过期选中态。\n const itemSelectionChanged =\n options.getItemSelected !== undefined &&\n nextItems.some(item => options.getItemSelected?.(item) !== options.isItemSelected(item, value))\n const selectionChanged =\n !initialized ||\n !options.valuesEqual(value, lastValue as Value) ||\n disabled !== lastDisabled ||\n membershipChanged ||\n itemSelectionChanged\n\n // 上行选中写回:根写入子项(context 是纯下行,不承担该方向)。\n if (selectionChanged) {\n for (const item of nextItems) {\n options.setItemSelected(item, options.isItemSelected(item, value))\n }\n }\n\n // 禁用下行:每次 sync 以新 Map 载荷整体广播(成员增删与禁用变化一并覆盖,\n // 新 Map 实例保证 Object.is 判定为变更)。\n provider.setValue(\n new Map(\n nextItems.map((item, index) => [\n item as HTMLElement,\n getSelectionContext(\n disabled,\n options.getRovingTabStop?.(item, index, nextItems, value) === true,\n options.isItemTabStop?.(item, nextItems)\n )\n ])\n )\n )\n\n lastItems = new Set(nextItems)\n lastValue = options.copyValue(value)\n lastDisabled = disabled\n initialized = true\n return selectionChanged\n }\n\n return {\n sync,\n disconnect() {\n provider.setValue(new Map())\n lastItems.clear()\n lastValue = undefined\n lastDisabled = undefined\n initialized = false\n },\n handleChange(event: Event) {\n if (options.getDisabled() || !options.isItem(event.target) || !options.host.contains(event.target)) {\n return\n }\n\n const current = options.getValue()\n const next = options.getNextValue(event.target, current)\n if (options.valuesEqual(current, next)) {\n options.resyncOnUnchangedValue?.(event.target)\n if (options.resyncOnUnchangedValue) sync()\n return\n }\n\n options.setValue(next)\n options.dispatchValueChange()\n },\n handleKeyDown(event: KeyboardEvent) {\n options.handleKeyDown?.(event)\n }\n }\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/shared/group-management/index.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\nimport { ContextConsumer, ContextProvider, createContext, type Context } from '@lit/context'\nimport type { ReactiveController, ReactiveControllerHost } from 'lit'\n\n/*\n * 下行通道基于 @lit/context(ADR-0006 §5):root 上的 ContextProvider 以\n * `ReadonlyMap<HTMLElement, Context>` 为载荷向全子树广播,子项侧\n * ContextConsumer 订阅并按自身元素取条目。context-request 事件 composed,\n * 可穿过中间 shadow 边界;custom element 升级顺序保证祖先先于后代升级,\n * provider 总是先于子项请求就绪。成员追踪(querySelectorAll +\n * MutationObserver/slotchange)、点击归因与 setItemSelected 上行写回\n * 不在下行通道内,仍由 GroupController 驱动。\n */\n\nexport interface SelectionGroupContext {\n readonly disabled: boolean\n readonly rovingTabStop: boolean\n readonly tabStop?: boolean\n}\n\nexport interface ButtonGroupContext {\n readonly direction: 'horizontal' | 'vertical'\n readonly isLast: boolean\n}\n\n// 载荷类型仅编译期品牌化;按载荷形状分两个 key,避免异构树串扰。\n// createContext 泛型顺序为 <ValueType, K>。\nconst SELECTION_GROUP_CONTEXT = createContext<ReadonlyMap<HTMLElement, SelectionGroupContext>, symbol>(\n Symbol('web-ui.selection-group-context')\n)\n\nconst BUTTON_GROUP_CONTEXT = createContext<ReadonlyMap<HTMLElement, ButtonGroupContext>, symbol>(\n Symbol('web-ui.button-group-context')\n)\n\n// 两个 key 的运行时形状一致(Map 载荷),泛型插件内部统一按宽松类型驱动。\ntype AnyGroupContext = Context<symbol, ReadonlyMap<HTMLElement, unknown>>\n\nconst SELECTION_GROUP_CONTEXT_LOOSE = SELECTION_GROUP_CONTEXT as unknown as AnyGroupContext\nconst BUTTON_GROUP_CONTEXT_LOOSE = BUTTON_GROUP_CONTEXT as unknown as AnyGroupContext\n\nexport const selectionGroupContextKey: AnyGroupContext = SELECTION_GROUP_CONTEXT_LOOSE\nexport const buttonGroupContextKey: AnyGroupContext = BUTTON_GROUP_CONTEXT_LOOSE\nexport const GROUP_MEMBER_DISABLED_CHANGE_EVENT = 'wui-group-member-disabled-change'\n\ninterface GroupControllerHost extends HTMLElement, ReactiveControllerHost {\n readonly renderRoot: Element | DocumentFragment\n}\n\n/**\n * 创建受管理 group 的子项侧能力:订阅最近 provider 的 context 载荷并提取\n * 自身条目。在 `make()` 时完成注册,组件无需再编写 constructor 样板。\n */\nexport function defineGroupManaged<Context>(\n item: HTMLElement & ReactiveControllerHost,\n options: {\n /** 与所属根组件使用的 context key 一致。 */\n context: AnyGroupContext\n requestUpdate: () => void\n equals?: (a: Context | undefined, b: Context | undefined) => boolean\n }\n) {\n return definePlugin(() => {\n let context: Context | undefined\n const equals = options.equals ?? Object.is\n\n const consumer = new ContextConsumer(item, {\n context: options.context,\n subscribe: true,\n callback: payload => {\n const next = (payload as ReadonlyMap<HTMLElement, Context> | undefined)?.get(item)\n if (equals(context, next)) return\n context = next\n options.requestUpdate()\n }\n })\n\n /*\n * 离开 group(被移出 provider 子树)时 ContextConsumer 只是静默退订,\n * 不会收到「条目已移除」的通知:显式清空上下文,恢复独立控件语义\n * (如 checkbox 恢复自身表单提交)。跨组移动先断连清空、再重连订阅新组。\n */\n item.addController({\n hostDisconnected() {\n if (equals(context, undefined)) return\n context = undefined\n options.requestUpdate()\n }\n })\n\n return {\n getContext(): Context | undefined {\n return context\n },\n // 供测试或特殊场景确认订阅状态。\n isSubscribed(): boolean {\n return consumer.value !== undefined\n }\n }\n })\n}\n\nexport interface GroupPresentationOptions<Item extends HTMLElement, Context> {\n readonly host: HTMLElement\n /** 与子项 defineGroupManaged 使用的 context key 一致。 */\n readonly context: AnyGroupContext\n readonly getItems: () => readonly Item[]\n readonly getContext: (item: Item, index: number, items: readonly Item[]) => Context\n}\n\n/**\n * 为分组子项注册可逆的展示上下文(@lit/context 下行通道)。不持有公开 DOM\n * 属性;sync() 以新 Map 载荷整体广播,成员变化体现为子项条目消失。\n */\nexport function defineGroupPresentation<Item extends HTMLElement, Context>(\n options: GroupPresentationOptions<Item, Context>\n) {\n return definePlugin(() => {\n const provider = new ContextProvider(options.host, {\n context: options.context,\n initialValue: new Map<HTMLElement, unknown>()\n })\n\n return {\n sync(): boolean {\n const nextItems = options.getItems()\n const payload = new Map<HTMLElement, unknown>()\n nextItems.forEach((item, index) => payload.set(item, options.getContext(item, index, nextItems)))\n // 新 Map 实例保证 Object.is 判定为变更,全量通知订阅者。\n // 每次 hostUpdated 都重播(即使成员/方向未变)是有意形态:每次新建\n // context 对象,但子项 defineGroupManaged 的 equals 门控 requestUpdate\n // (标量字段比较),重播不产生多余渲染;代价是比「仅成员变化才通知」\n // 更频繁的广播事件,换取零状态追踪的简单性(与 GroupController 的\n // 全量 sync 对齐)。\n provider.setValue(payload)\n return true\n },\n disconnect() {\n provider.setValue(new Map())\n }\n }\n })\n}\n\nexport interface GroupLifecycle {\n sync(): boolean\n disconnect(): void\n handleChange?(event: Event): void\n handleKeyDown?(event: KeyboardEvent): void\n}\n\ninterface GroupControllerOptions {\n afterSync?: () => void\n observeAttributes?: readonly string[]\n}\n\n/**\n * 把组生命周期插件接入 Lit,协调规则不落在组件生命周期方法里。\n */\nexport class GroupController implements ReactiveController {\n private connected = false\n private observer: MutationObserver | null = null\n private observedSlots = new Set<HTMLSlotElement>()\n constructor(\n private readonly host: GroupControllerHost,\n private readonly lifecycle: GroupLifecycle,\n private readonly options: GroupControllerOptions = {}\n ) {\n host.addController(this)\n }\n\n hostConnected() {\n this.connected = true\n this.host.addEventListener('change', this.handleChange, true)\n this.host.addEventListener('keydown', this.handleKeyDown, true)\n this.host.addEventListener(GROUP_MEMBER_DISABLED_CHANGE_EVENT, this.handleMemberDisabledChange, true)\n this.observeHost()\n this.observeSlots()\n this.sync()\n }\n\n hostUpdated() {\n this.observeSlots()\n this.sync()\n }\n\n hostDisconnected() {\n this.connected = false\n this.host.removeEventListener('change', this.handleChange, true)\n this.host.removeEventListener('keydown', this.handleKeyDown, true)\n this.host.removeEventListener(GROUP_MEMBER_DISABLED_CHANGE_EVENT, this.handleMemberDisabledChange, true)\n this.disconnectSlots()\n this.disconnectHostObserver()\n this.lifecycle.disconnect()\n }\n\n private readonly handleChange = (event: Event) => {\n this.lifecycle.handleChange?.(event)\n }\n\n private readonly handleKeyDown = (event: KeyboardEvent) => {\n this.lifecycle.handleKeyDown?.(event)\n }\n\n private readonly handleMemberDisabledChange = () => {\n this.sync()\n }\n\n private readonly handleSlotChange = () => {\n if (!this.connected) return\n this.sync()\n }\n\n private readonly handleMutations = () => {\n if (!this.connected) return\n this.sync()\n }\n\n sync() {\n if (!this.lifecycle.sync()) return\n this.options.afterSync?.()\n }\n\n private observeHost() {\n this.disconnectHostObserver()\n // 监听 light DOM 的增删,覆盖 Vue v-if / React && 的 comment 锚点替换与深层包裹\n this.observer = new MutationObserver(this.handleMutations)\n const observerOptions: MutationObserverInit = { childList: true, subtree: true }\n if (this.options.observeAttributes?.length) {\n observerOptions.attributes = true\n observerOptions.attributeFilter = [...this.options.observeAttributes]\n }\n this.observer.observe(this.host, observerOptions)\n }\n\n private disconnectHostObserver() {\n if (this.observer) {\n this.observer.disconnect()\n this.observer = null\n }\n }\n\n private observeSlots() {\n const root = this.host.renderRoot as Element\n const slots = root.querySelectorAll<HTMLSlotElement>('slot')\n for (const slot of slots) {\n if (!this.observedSlots.has(slot)) {\n slot.addEventListener('slotchange', this.handleSlotChange)\n this.observedSlots.add(slot)\n }\n }\n // 清理已移除的 slot(热更新等场景)\n for (const slot of this.observedSlots) {\n if (!root.contains(slot)) {\n slot.removeEventListener('slotchange', this.handleSlotChange)\n this.observedSlots.delete(slot)\n }\n }\n }\n\n private disconnectSlots() {\n for (const slot of this.observedSlots) {\n slot.removeEventListener('slotchange', this.handleSlotChange)\n }\n this.observedSlots.clear()\n }\n}\n\nexport interface GroupCoordinatorOptions<Item extends HTMLElement, Value> {\n readonly host: HTMLElement\n readonly getItems: () => readonly Item[]\n readonly getValue: () => Value\n readonly setValue: (value: Value) => void\n readonly getDisabled: () => boolean\n readonly isItem: (target: EventTarget | null) => target is Item\n readonly isItemSelected: (item: Item, value: Value) => boolean\n readonly isItemTabStop?: (item: Item, items: readonly Item[]) => boolean\n readonly getItemSelected?: (item: Item) => boolean\n readonly getNextValue: (item: Item, value: Value) => Value\n readonly valuesEqual: (a: Value, b: Value) => boolean\n readonly copyValue: (value: Value) => Value\n readonly setItemSelected: (item: Item, selected: boolean) => void\n readonly dispatchValueChange: () => void\n readonly getRovingTabStop?: (item: Item, index: number, items: readonly Item[], value: Value) => boolean\n readonly handleKeyDown?: (event: KeyboardEvent) => void\n /**\n * 交互已发生但值没有变化时的回调(例如单选组成员 value 重复:点击同值成员\n * 不会改变 value,却必须把选中态收到被点击的那一项)。缺省视为无事可做。\n */\n readonly resyncOnUnchangedValue?: (item: Item) => void\n}\n\nconst ENABLED_SELECTION_CONTEXT: SelectionGroupContext = { disabled: false, rovingTabStop: false }\nconst ENABLED_ROVING_SELECTION_CONTEXT: SelectionGroupContext = { disabled: false, rovingTabStop: true }\nconst DISABLED_SELECTION_CONTEXT: SelectionGroupContext = { disabled: true, rovingTabStop: false }\n\nfunction getSelectionContext(disabled: boolean, rovingTabStop: boolean, tabStop?: boolean): SelectionGroupContext {\n const context = disabled\n ? DISABLED_SELECTION_CONTEXT\n : rovingTabStop\n ? ENABLED_ROVING_SELECTION_CONTEXT\n : ENABLED_SELECTION_CONTEXT\n return tabStop === undefined ? context : { ...context, tabStop }\n}\n\n/**\n * 在一个内部插件接口后收敛选中态、继承禁用与受管子项成员关系。\n * 禁用态经 @lit/context 下行广播;选中态上行写回保持直写。\n */\nexport function defineGroupCoordinator<Item extends HTMLElement, Value>(options: GroupCoordinatorOptions<Item, Value>) {\n return definePlugin(() => {\n const provider = new ContextProvider(options.host, {\n context: SELECTION_GROUP_CONTEXT_LOOSE,\n initialValue: new Map<HTMLElement, SelectionGroupContext>()\n })\n\n let lastValue: Value | undefined\n let lastDisabled: boolean | undefined\n let lastItems = new Set<Item>()\n let initialized = false\n\n const sync = (): boolean => {\n const nextItems = options.getItems()\n const value = options.getValue()\n const disabled = options.getDisabled()\n // 成员变化也要重放选中写回:新成员需继承当前选中态。\n const membershipChanged = nextItems.length !== lastItems.size || [...nextItems].some(item => !lastItems.has(item))\n // 子项可能在宿主首次连接后才完成 custom-element upgrade;值变得可读时,\n // 即使组值和成员集合没变,也要修复之前写入的过期选中态。\n const itemSelectionChanged =\n options.getItemSelected !== undefined &&\n nextItems.some(item => options.getItemSelected?.(item) !== options.isItemSelected(item, value))\n const selectionChanged =\n !initialized ||\n !options.valuesEqual(value, lastValue as Value) ||\n disabled !== lastDisabled ||\n membershipChanged ||\n itemSelectionChanged\n\n // 上行选中写回:根写入子项(context 是纯下行,不承担该方向)。\n if (selectionChanged) {\n for (const item of nextItems) {\n options.setItemSelected(item, options.isItemSelected(item, value))\n }\n }\n\n // 禁用下行:每次 sync 以新 Map 载荷整体广播(成员增删与禁用变化一并覆盖,\n // 新 Map 实例保证 Object.is 判定为变更)。\n provider.setValue(\n new Map(\n nextItems.map((item, index) => [\n item as HTMLElement,\n getSelectionContext(\n disabled,\n options.getRovingTabStop?.(item, index, nextItems, value) === true,\n options.isItemTabStop?.(item, nextItems)\n )\n ])\n )\n )\n\n lastItems = new Set(nextItems)\n lastValue = options.copyValue(value)\n lastDisabled = disabled\n initialized = true\n return selectionChanged\n }\n\n return {\n sync,\n disconnect() {\n provider.setValue(new Map())\n lastItems.clear()\n lastValue = undefined\n lastDisabled = undefined\n initialized = false\n },\n handleChange(event: Event) {\n if (options.getDisabled() || !options.isItem(event.target) || !options.host.contains(event.target)) {\n return\n }\n\n const current = options.getValue()\n const next = options.getNextValue(event.target, current)\n if (options.valuesEqual(current, next)) {\n options.resyncOnUnchangedValue?.(event.target)\n if (options.resyncOnUnchangedValue) sync()\n return\n }\n\n options.setValue(next)\n options.dispatchValueChange()\n },\n handleKeyDown(event: KeyboardEvent) {\n options.handleKeyDown?.(event)\n }\n }\n })\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/shared/open-state/index.ts"],"sourcesContent":["import type { LitElement } from 'lit'\n\n/**\n * overlay 族 `open-change` 事件的单一发出点与契约说明。\n *\n * 同名事件存在两种 flavor
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/shared/open-state/index.ts"],"sourcesContent":["import type { LitElement } from 'lit'\n\n/**\n * overlay 族 `open-change` 事件的单一发出点与契约说明。\n *\n * 同名事件存在两种 flavor(事件模型见 ADR-0005 §2.4,勿混淆):\n * - **request 语义**(dialog / drawer / layout):controlled 属性开启时,\n * `open-change` 是用户的关闭/打开\"请求\",组件不自行修改 `open`,等待回写;\n * - **notification 语义**(popover / tooltip / dropdown / context-menu /\n * select / autocomplete / collapse):组件总是自行变更 `open`,事件只作通知。\n *\n * 两种 flavor 的机械发出在此收敛;flavor 决策(是否 controlled 门控)留在各组件。\n */\nexport interface OpenChangeEventDetail {\n open: boolean\n}\n\nexport function dispatchOpenChangeEvent(host: LitElement, open: boolean): void {\n host.dispatchEvent(\n new CustomEvent<OpenChangeEventDetail>('open-change', {\n detail: { open },\n bubbles: true,\n composed: true\n })\n )\n}\n"],"mappings":";AAiBA,SAAgB,wBAAwB,MAAkB,MAAqB;CAC7E,KAAK,cACH,IAAI,YAAmC,eAAe;EACpD,QAAQ,EAAE,KAAK;EACf,SAAS;EACT,UAAU;CACZ,CAAC,CACH;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overlay.js","names":[],"sources":["../../../src/shared/overlay/overlay.ts"],"sourcesContent":["import {\n autoUpdate,\n computePosition,\n flip,\n offset,\n shift,\n size,\n type Middleware,\n type Placement,\n type Strategy\n} from '@floating-ui/dom'\nimport { definePlugin } from '@greypan/js-kit'\n\nimport { defineOverlayPositioningGeneration, type OverlayPositioningGeneration } from './positioning-generation'\n\nexport interface OverlayOptions {\n placement?: Placement\n offset?: number\n flip?: boolean\n shift?: boolean\n /** panel 宽度与锚点宽度保持一致。 */\n matchWidth?: boolean\n /** panel 至少与锚点同宽,同时允许内容将 panel 撑宽。 */\n minAnchorWidth?: boolean\n strategy?: Strategy\n}\n\n/** 虚拟锚点:无对应 DOM 元素的定位基准。context-menu 未走 defineOverlay(定位语义分歧见 ADR-0038)。 */\nexport interface OverlayVirtualAnchor {\n getBoundingClientRect(): DOMRect\n}\n\nexport type OverlayAnchor = HTMLElement | OverlayVirtualAnchor\n\nexport interface OverlayApi {\n isOpen(): boolean\n readonly options: Required<OverlayOptions>\n open(): void\n close(): void\n toggle(): void\n update(options: Partial<OverlayOptions>): void\n updateAnchor(anchor: OverlayAnchor): void\n dispose(): void\n}\n\nconst DEFAULT_OVERLAY_MIN_WIDTH = 120\nconst OVERLAY_MIN_WIDTH_VARIABLE = '--wui-overlay-min-width'\n\nconst DEFAULT_OPTIONS: Required<OverlayOptions> = {\n placement: 'bottom-start',\n offset: 4,\n flip: true,\n shift: true,\n matchWidth: false,\n minAnchorWidth: false,\n strategy: 'absolute'\n}\n\ninterface WidthStyleState {\n hasManagedStyles: boolean\n}\n\nfunction readOverlayMinWidth(overlay: HTMLElement): number {\n if (typeof getComputedStyle !== 'function') return DEFAULT_OVERLAY_MIN_WIDTH\n\n const minWidth = Number.parseFloat(getComputedStyle(overlay).getPropertyValue(OVERLAY_MIN_WIDTH_VARIABLE))\n return Number.isFinite(minWidth) ? minWidth : DEFAULT_OVERLAY_MIN_WIDTH\n}\n\nfunction clearManagedWidthStyles(overlay: HTMLElement) {\n overlay.style.removeProperty('width')\n overlay.style.removeProperty('min-width')\n}\n\nfunction createWidthMiddleware(\n overlay: HTMLElement,\n options: Required<OverlayOptions>,\n widthStyles: WidthStyleState,\n positioningGeneration: OverlayPositioningGeneration,\n generation: number\n): Middleware | undefined {\n const isCurrentGeneration = () => positioningGeneration.isCurrent(generation)\n if (options.matchWidth) {\n // matchWidth 必须移除之前 minAnchorWidth 写入的 min-width,才能精确跟随 trigger。\n overlay.style.removeProperty('min-width')\n widthStyles.hasManagedStyles = true\n return size({\n apply({ rects }) {\n if (!isCurrentGeneration()) return\n overlay.style.width = `${rects.reference.width}px`\n }\n })\n }\n\n if (options.minAnchorWidth) {\n const minWidth = readOverlayMinWidth(overlay)\n widthStyles.hasManagedStyles = true\n return size({\n apply({ rects }) {\n if (!isCurrentGeneration()) return\n // max-content 让内容决定宽度,min-width 再提供 trigger 与 floor 两个下限。\n overlay.style.width = 'max-content'\n overlay.style.minWidth = `${Math.max(rects.reference.width, minWidth)}px`\n }\n })\n }\n\n if (widthStyles.hasManagedStyles) {\n clearManagedWidthStyles(overlay)\n widthStyles.hasManagedStyles = false\n }\n\n return undefined\n}\n\nfunction createPositioningMiddleware(\n overlay: HTMLElement,\n options: Required<OverlayOptions>,\n widthStyles: WidthStyleState,\n positioningGeneration: OverlayPositioningGeneration,\n generation: number\n): Middleware[] {\n const middleware: Middleware[] = [offset(options.offset)]\n if (options.flip) middleware.push(flip())\n if (options.shift) {\n const side = options.placement.split('-')[0]\n middleware.push(shift({ padding: 8, crossAxis: side === 'left' || side === 'right' }))\n }\n\n const widthMiddleware = createWidthMiddleware(overlay, options, widthStyles, positioningGeneration, generation)\n if (widthMiddleware) middleware.push(widthMiddleware)\n\n return middleware\n}\n\nfunction getTransformOrigin(placement: Placement): string {\n const [side, alignment] = placement.split('-')\n const horizontalOrigin = alignment === 'start' ? 'left' : alignment === 'end' ? 'right' : 'center'\n const verticalOrigin = alignment === 'start' ? 'top' : alignment === 'end' ? 'bottom' : 'center'\n\n if (side === 'top') return `bottom ${horizontalOrigin}`\n if (side === 'bottom') return `top ${horizontalOrigin}`\n if (side === 'left') return `right ${verticalOrigin}`\n return `left ${verticalOrigin}`\n}\n\nexport const defineOverlay = () =>\n definePlugin<OverlayApi, { anchor: OverlayAnchor; overlay: HTMLElement } & OverlayOptions>(ctx => {\n const options: Required<OverlayOptions> = {\n placement: ctx.placement ?? DEFAULT_OPTIONS.placement,\n offset: ctx.offset ?? DEFAULT_OPTIONS.offset,\n flip: ctx.flip ?? DEFAULT_OPTIONS.flip,\n shift: ctx.shift ?? DEFAULT_OPTIONS.shift,\n matchWidth: ctx.matchWidth ?? DEFAULT_OPTIONS.matchWidth,\n minAnchorWidth: ctx.minAnchorWidth ?? DEFAULT_OPTIONS.minAnchorWidth,\n strategy: ctx.strategy ?? DEFAULT_OPTIONS.strategy\n }\n\n const overlay = ctx.overlay\n const widthStyles: WidthStyleState = { hasManagedStyles: false }\n let isOpen = false\n let cleanupAutoUpdate: (() => void) | null = null\n let currentAnchor = ctx.anchor\n // 每次刷新都是新定位代;open 期间 autoUpdate 的多次回调属于同一代。\n // reposition/close 会让上一代 promise 失效,避免乱序完成覆盖最新坐标。\n const positioningGeneration = defineOverlayPositioningGeneration().make()\n\n function updatePosition() {\n const generation = positioningGeneration.next()\n const middleware = createPositioningMiddleware(overlay, options, widthStyles, positioningGeneration, generation)\n\n const applyPosition = () => {\n void computePosition(currentAnchor, overlay, {\n placement: options.placement,\n strategy: options.strategy,\n middleware\n }).then(({ x, y, placement }) => {\n if (!positioningGeneration.isCurrent(generation) || !isOpen) return\n overlay.style.left = `${x}px`\n overlay.style.top = `${y}px`\n overlay.style.setProperty('--wui-internal-overlay-transform-origin', getTransformOrigin(placement))\n })\n }\n\n // 立即定位一次,避免 overlay 首帧出现在左上角;autoUpdate 负责后续滚动/尺寸变化。\n applyPosition()\n cleanupAutoUpdate?.()\n cleanupAutoUpdate = autoUpdate(currentAnchor, overlay, applyPosition)\n }\n\n return {\n isOpen() {\n return isOpen\n },\n options,\n\n open() {\n if (isOpen) return\n isOpen = true\n overlay.style.display = ''\n updatePosition()\n },\n\n close() {\n if (!isOpen) return\n isOpen = false\n positioningGeneration.invalidate()\n cleanupAutoUpdate?.()\n cleanupAutoUpdate = null\n },\n\n toggle() {\n if (isOpen) this.close()\n else this.open()\n },\n\n update(nextOptions) {\n Object.assign(options, nextOptions)\n if (isOpen) updatePosition()\n },\n\n updateAnchor(newAnchor: OverlayAnchor) {\n currentAnchor = newAnchor\n if (isOpen) updatePosition()\n },\n\n dispose() {\n this.close()\n }\n }\n })\n"],"mappings":";;;;AA6CA,MAAM,4BAA4B;AAClC,MAAM,6BAA6B;AAEnC,MAAM,kBAA4C;CAChD,WAAW;CACX,QAAQ;CACR,MAAM;CACN,OAAO;CACP,YAAY;CACZ,gBAAgB;CAChB,UAAU;AACZ;AAMA,SAAS,oBAAoB,SAA8B;CACzD,IAAI,OAAO,qBAAqB,YAAY,OAAO;CAEnD,MAAM,WAAW,OAAO,WAAW,iBAAiB,OAAO,CAAC,CAAC,iBAAiB,0BAA0B,CAAC;CACzG,OAAO,OAAO,SAAS,QAAQ,IAAI,WAAW;AAChD;AAEA,SAAS,wBAAwB,SAAsB;CACrD,QAAQ,MAAM,eAAe,OAAO;CACpC,QAAQ,MAAM,eAAe,WAAW;AAC1C;AAEA,SAAS,sBACP,SACA,SACA,aACA,uBACA,YACwB;CACxB,MAAM,4BAA4B,sBAAsB,UAAU,UAAU;CAC5E,IAAI,QAAQ,YAAY;EAEtB,QAAQ,MAAM,eAAe,WAAW;EACxC,YAAY,mBAAmB;EAC/B,OAAO,KAAK,EACV,MAAM,EAAE,SAAS;GACf,IAAI,CAAC,oBAAoB,GAAG;GAC5B,QAAQ,MAAM,QAAQ,GAAG,MAAM,UAAU,MAAM;EACjD,EACF,CAAC;CACH;CAEA,IAAI,QAAQ,gBAAgB;EAC1B,MAAM,WAAW,oBAAoB,OAAO;EAC5C,YAAY,mBAAmB;EAC/B,OAAO,KAAK,EACV,MAAM,EAAE,SAAS;GACf,IAAI,CAAC,oBAAoB,GAAG;GAE5B,QAAQ,MAAM,QAAQ;GACtB,QAAQ,MAAM,WAAW,GAAG,KAAK,IAAI,MAAM,UAAU,OAAO,QAAQ,EAAE;EACxE,EACF,CAAC;CACH;CAEA,IAAI,YAAY,kBAAkB;EAChC,wBAAwB,OAAO;EAC/B,YAAY,mBAAmB;CACjC;AAGF;AAEA,SAAS,4BACP,SACA,SACA,aACA,uBACA,YACc;CACd,MAAM,aAA2B,CAAC,OAAO,QAAQ,MAAM,CAAC;CACxD,IAAI,QAAQ,MAAM,WAAW,KAAK,KAAK,CAAC;CACxC,IAAI,QAAQ,OAAO;EACjB,MAAM,OAAO,QAAQ,UAAU,MAAM,GAAG,CAAC,CAAC;EAC1C,WAAW,KAAK,MAAM;GAAE,SAAS;GAAG,WAAW,SAAS,UAAU,SAAS;EAAQ,CAAC,CAAC;CACvF;CAEA,MAAM,kBAAkB,sBAAsB,SAAS,SAAS,aAAa,uBAAuB,UAAU;CAC9G,IAAI,iBAAiB,WAAW,KAAK,eAAe;CAEpD,OAAO;AACT;AAEA,SAAS,mBAAmB,WAA8B;CACxD,MAAM,CAAC,MAAM,aAAa,UAAU,MAAM,GAAG;CAC7C,MAAM,mBAAmB,cAAc,UAAU,SAAS,cAAc,QAAQ,UAAU;CAC1F,MAAM,iBAAiB,cAAc,UAAU,QAAQ,cAAc,QAAQ,WAAW;CAExF,IAAI,SAAS,OAAO,OAAO,UAAU;CACrC,IAAI,SAAS,UAAU,OAAO,OAAO;CACrC,IAAI,SAAS,QAAQ,OAAO,SAAS;CACrC,OAAO,QAAQ;AACjB;AAEA,MAAa,sBACX,cAA2F,QAAO;CAChG,MAAM,UAAoC;EACxC,WAAW,IAAI,aAAa,gBAAgB;EAC5C,QAAQ,IAAI,UAAU,gBAAgB;EACtC,MAAM,IAAI,QAAQ,gBAAgB;EAClC,OAAO,IAAI,SAAS,gBAAgB;EACpC,YAAY,IAAI,cAAc,gBAAgB;EAC9C,gBAAgB,IAAI,kBAAkB,gBAAgB;EACtD,UAAU,IAAI,YAAY,gBAAgB;CAC5C;CAEA,MAAM,UAAU,IAAI;CACpB,MAAM,cAA+B,EAAE,kBAAkB,MAAM;CAC/D,IAAI,SAAS;CACb,IAAI,oBAAyC;CAC7C,IAAI,gBAAgB,IAAI;CAGxB,MAAM,wBAAwB,mCAAmC,CAAC,CAAC,KAAK;CAExE,SAAS,iBAAiB;EACxB,MAAM,aAAa,sBAAsB,KAAK;EAC9C,MAAM,aAAa,4BAA4B,SAAS,SAAS,aAAa,uBAAuB,UAAU;EAE/G,MAAM,sBAAsB;GAC1B,gBAAqB,eAAe,SAAS;IAC3C,WAAW,QAAQ;IACnB,UAAU,QAAQ;IAClB;GACF,CAAC,CAAC,CAAC,MAAM,EAAE,GAAG,GAAG,gBAAgB;IAC/B,IAAI,CAAC,sBAAsB,UAAU,UAAU,KAAK,CAAC,QAAQ;IAC7D,QAAQ,MAAM,OAAO,GAAG,EAAE;IAC1B,QAAQ,MAAM,MAAM,GAAG,EAAE;IACzB,QAAQ,MAAM,YAAY,2CAA2C,mBAAmB,SAAS,CAAC;GACpG,CAAC;EACH;EAGA,cAAc;EACd,oBAAoB;EACpB,oBAAoB,WAAW,eAAe,SAAS,aAAa;CACtE;CAEA,OAAO;EACL,SAAS;GACP,OAAO;EACT;EACA;EAEA,OAAO;GACL,IAAI,QAAQ;GACZ,SAAS;GACT,QAAQ,MAAM,UAAU;GACxB,eAAe;EACjB;EAEA,QAAQ;GACN,IAAI,CAAC,QAAQ;GACb,SAAS;GACT,sBAAsB,WAAW;GACjC,oBAAoB;GACpB,oBAAoB;EACtB;EAEA,SAAS;GACP,IAAI,QAAQ,KAAK,MAAM;QAClB,KAAK,KAAK;EACjB;EAEA,OAAO,aAAa;GAClB,OAAO,OAAO,SAAS,WAAW;GAClC,IAAI,QAAQ,eAAe;EAC7B;EAEA,aAAa,WAA0B;GACrC,gBAAgB;GAChB,IAAI,QAAQ,eAAe;EAC7B;EAEA,UAAU;GACR,KAAK,MAAM;EACb;CACF;AACF,CAAC"}
|
|
1
|
+
{"version":3,"file":"overlay.js","names":[],"sources":["../../../src/shared/overlay/overlay.ts"],"sourcesContent":["import {\n autoUpdate,\n computePosition,\n flip,\n offset,\n shift,\n size,\n type Middleware,\n type Placement,\n type Strategy\n} from '@floating-ui/dom'\nimport { definePlugin } from '@greypan/js-kit'\n\nimport { defineOverlayPositioningGeneration, type OverlayPositioningGeneration } from './positioning-generation'\n\nexport interface OverlayOptions {\n placement?: Placement\n offset?: number\n flip?: boolean\n shift?: boolean\n /** panel 宽度与锚点宽度保持一致。 */\n matchWidth?: boolean\n /** panel 至少与锚点同宽,同时允许内容将 panel 撑宽。 */\n minAnchorWidth?: boolean\n strategy?: Strategy\n}\n\n/** 虚拟锚点:无对应 DOM 元素的定位基准。context-menu 未走 defineOverlay(私有定位路径的准入见 ADR-0006 §2)。 */\nexport interface OverlayVirtualAnchor {\n getBoundingClientRect(): DOMRect\n}\n\nexport type OverlayAnchor = HTMLElement | OverlayVirtualAnchor\n\nexport interface OverlayApi {\n isOpen(): boolean\n readonly options: Required<OverlayOptions>\n open(): void\n close(): void\n toggle(): void\n update(options: Partial<OverlayOptions>): void\n updateAnchor(anchor: OverlayAnchor): void\n dispose(): void\n}\n\nconst DEFAULT_OVERLAY_MIN_WIDTH = 120\nconst OVERLAY_MIN_WIDTH_VARIABLE = '--wui-overlay-min-width'\n\nconst DEFAULT_OPTIONS: Required<OverlayOptions> = {\n placement: 'bottom-start',\n offset: 4,\n flip: true,\n shift: true,\n matchWidth: false,\n minAnchorWidth: false,\n strategy: 'absolute'\n}\n\ninterface WidthStyleState {\n hasManagedStyles: boolean\n}\n\nfunction readOverlayMinWidth(overlay: HTMLElement): number {\n if (typeof getComputedStyle !== 'function') return DEFAULT_OVERLAY_MIN_WIDTH\n\n const minWidth = Number.parseFloat(getComputedStyle(overlay).getPropertyValue(OVERLAY_MIN_WIDTH_VARIABLE))\n return Number.isFinite(minWidth) ? minWidth : DEFAULT_OVERLAY_MIN_WIDTH\n}\n\nfunction clearManagedWidthStyles(overlay: HTMLElement) {\n overlay.style.removeProperty('width')\n overlay.style.removeProperty('min-width')\n}\n\nfunction createWidthMiddleware(\n overlay: HTMLElement,\n options: Required<OverlayOptions>,\n widthStyles: WidthStyleState,\n positioningGeneration: OverlayPositioningGeneration,\n generation: number\n): Middleware | undefined {\n const isCurrentGeneration = () => positioningGeneration.isCurrent(generation)\n if (options.matchWidth) {\n // matchWidth 必须移除之前 minAnchorWidth 写入的 min-width,才能精确跟随 trigger。\n overlay.style.removeProperty('min-width')\n widthStyles.hasManagedStyles = true\n return size({\n apply({ rects }) {\n if (!isCurrentGeneration()) return\n overlay.style.width = `${rects.reference.width}px`\n }\n })\n }\n\n if (options.minAnchorWidth) {\n const minWidth = readOverlayMinWidth(overlay)\n widthStyles.hasManagedStyles = true\n return size({\n apply({ rects }) {\n if (!isCurrentGeneration()) return\n // max-content 让内容决定宽度,min-width 再提供 trigger 与 floor 两个下限。\n overlay.style.width = 'max-content'\n overlay.style.minWidth = `${Math.max(rects.reference.width, minWidth)}px`\n }\n })\n }\n\n if (widthStyles.hasManagedStyles) {\n clearManagedWidthStyles(overlay)\n widthStyles.hasManagedStyles = false\n }\n\n return undefined\n}\n\nfunction createPositioningMiddleware(\n overlay: HTMLElement,\n options: Required<OverlayOptions>,\n widthStyles: WidthStyleState,\n positioningGeneration: OverlayPositioningGeneration,\n generation: number\n): Middleware[] {\n const middleware: Middleware[] = [offset(options.offset)]\n if (options.flip) middleware.push(flip())\n if (options.shift) {\n const side = options.placement.split('-')[0]\n middleware.push(shift({ padding: 8, crossAxis: side === 'left' || side === 'right' }))\n }\n\n const widthMiddleware = createWidthMiddleware(overlay, options, widthStyles, positioningGeneration, generation)\n if (widthMiddleware) middleware.push(widthMiddleware)\n\n return middleware\n}\n\nfunction getTransformOrigin(placement: Placement): string {\n const [side, alignment] = placement.split('-')\n const horizontalOrigin = alignment === 'start' ? 'left' : alignment === 'end' ? 'right' : 'center'\n const verticalOrigin = alignment === 'start' ? 'top' : alignment === 'end' ? 'bottom' : 'center'\n\n if (side === 'top') return `bottom ${horizontalOrigin}`\n if (side === 'bottom') return `top ${horizontalOrigin}`\n if (side === 'left') return `right ${verticalOrigin}`\n return `left ${verticalOrigin}`\n}\n\nexport const defineOverlay = () =>\n definePlugin<OverlayApi, { anchor: OverlayAnchor; overlay: HTMLElement } & OverlayOptions>(ctx => {\n const options: Required<OverlayOptions> = {\n placement: ctx.placement ?? DEFAULT_OPTIONS.placement,\n offset: ctx.offset ?? DEFAULT_OPTIONS.offset,\n flip: ctx.flip ?? DEFAULT_OPTIONS.flip,\n shift: ctx.shift ?? DEFAULT_OPTIONS.shift,\n matchWidth: ctx.matchWidth ?? DEFAULT_OPTIONS.matchWidth,\n minAnchorWidth: ctx.minAnchorWidth ?? DEFAULT_OPTIONS.minAnchorWidth,\n strategy: ctx.strategy ?? DEFAULT_OPTIONS.strategy\n }\n\n const overlay = ctx.overlay\n const widthStyles: WidthStyleState = { hasManagedStyles: false }\n let isOpen = false\n let cleanupAutoUpdate: (() => void) | null = null\n let currentAnchor = ctx.anchor\n // 每次刷新都是新定位代;open 期间 autoUpdate 的多次回调属于同一代。\n // reposition/close 会让上一代 promise 失效,避免乱序完成覆盖最新坐标。\n const positioningGeneration = defineOverlayPositioningGeneration().make()\n\n function updatePosition() {\n const generation = positioningGeneration.next()\n const middleware = createPositioningMiddleware(overlay, options, widthStyles, positioningGeneration, generation)\n\n const applyPosition = () => {\n void computePosition(currentAnchor, overlay, {\n placement: options.placement,\n strategy: options.strategy,\n middleware\n }).then(({ x, y, placement }) => {\n if (!positioningGeneration.isCurrent(generation) || !isOpen) return\n overlay.style.left = `${x}px`\n overlay.style.top = `${y}px`\n overlay.style.setProperty('--wui-internal-overlay-transform-origin', getTransformOrigin(placement))\n })\n }\n\n // 立即定位一次,避免 overlay 首帧出现在左上角;autoUpdate 负责后续滚动/尺寸变化。\n applyPosition()\n cleanupAutoUpdate?.()\n cleanupAutoUpdate = autoUpdate(currentAnchor, overlay, applyPosition)\n }\n\n return {\n isOpen() {\n return isOpen\n },\n options,\n\n open() {\n if (isOpen) return\n isOpen = true\n overlay.style.display = ''\n updatePosition()\n },\n\n close() {\n if (!isOpen) return\n isOpen = false\n positioningGeneration.invalidate()\n cleanupAutoUpdate?.()\n cleanupAutoUpdate = null\n },\n\n toggle() {\n if (isOpen) this.close()\n else this.open()\n },\n\n update(nextOptions) {\n Object.assign(options, nextOptions)\n if (isOpen) updatePosition()\n },\n\n updateAnchor(newAnchor: OverlayAnchor) {\n currentAnchor = newAnchor\n if (isOpen) updatePosition()\n },\n\n dispose() {\n this.close()\n }\n }\n })\n"],"mappings":"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"}
|