@greypan/web-ui 10.2.0 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.CN.md +68 -68
  2. package/README.md +43 -43
  3. package/dist/assets/glass.js.map +1 -1
  4. package/dist/assets/menu-portal.js.map +1 -1
  5. package/dist/assets/modal-surface.js.map +1 -1
  6. package/dist/assets/overlay-motion.js.map +1 -1
  7. package/dist/assets/selection-control.js.map +1 -1
  8. package/dist/components/autocomplete/index.d.ts +6 -6
  9. package/dist/components/autocomplete/index.js +1 -1
  10. package/dist/components/autocomplete/index.js.map +1 -1
  11. package/dist/components/autocomplete/style.js.map +1 -1
  12. package/dist/components/avatar/index.js.map +1 -1
  13. package/dist/components/back-top/index.js.map +1 -1
  14. package/dist/components/back-top/style.js.map +1 -1
  15. package/dist/components/badge/index.js +1 -1
  16. package/dist/components/badge/index.js.map +1 -1
  17. package/dist/components/button/index.js.map +1 -1
  18. package/dist/components/button/style.js.map +1 -1
  19. package/dist/components/checkbox/index.d.ts +1 -1
  20. package/dist/components/checkbox/index.js +1 -1
  21. package/dist/components/checkbox/index.js.map +1 -1
  22. package/dist/components/checkbox/style.js.map +1 -1
  23. package/dist/components/checkbox-group/index.d.ts +9 -0
  24. package/dist/components/checkbox-group/index.js +8 -2
  25. package/dist/components/checkbox-group/index.js.map +1 -1
  26. package/dist/components/collapse/index.d.ts +5 -5
  27. package/dist/components/collapse/index.js.map +1 -1
  28. package/dist/components/collapse/style.js.map +1 -1
  29. package/dist/components/context-menu/index.d.ts +60 -60
  30. package/dist/components/context-menu/index.js +2 -2
  31. package/dist/components/context-menu/index.js.map +1 -1
  32. package/dist/components/context-menu/scrim.js.map +1 -1
  33. package/dist/components/context-menu/style.js.map +1 -1
  34. package/dist/components/dialog/index.d.ts +4 -4
  35. package/dist/components/dialog/index.js +1 -1
  36. package/dist/components/dialog/index.js.map +1 -1
  37. package/dist/components/dialog/style.js.map +1 -1
  38. package/dist/components/drawer/index.d.ts +8 -8
  39. package/dist/components/drawer/index.js +1 -1
  40. package/dist/components/drawer/index.js.map +1 -1
  41. package/dist/components/drawer/style.js.map +1 -1
  42. package/dist/components/dropdown/index.d.ts +6 -6
  43. package/dist/components/dropdown/index.js.map +1 -1
  44. package/dist/components/dropdown-item/index.js.map +1 -1
  45. package/dist/components/editable-text/index.d.ts +2 -2
  46. package/dist/components/editable-text/index.js.map +1 -1
  47. package/dist/components/editable-text/style.js.map +1 -1
  48. package/dist/components/empty/index.d.ts +3 -3
  49. package/dist/components/empty/index.js.map +1 -1
  50. package/dist/components/image-preview/index.d.ts +5 -5
  51. package/dist/components/image-preview/index.js +7 -7
  52. package/dist/components/image-preview/index.js.map +1 -1
  53. package/dist/components/image-preview/style.js.map +1 -1
  54. package/dist/components/input/index.js +4 -1
  55. package/dist/components/input/index.js.map +1 -1
  56. package/dist/components/input/style.js.map +1 -1
  57. package/dist/components/input-number/index.d.ts +6 -0
  58. package/dist/components/input-number/index.js +27 -21
  59. package/dist/components/input-number/index.js.map +1 -1
  60. package/dist/components/layout/index.d.ts +7 -7
  61. package/dist/components/layout/index.js +4 -4
  62. package/dist/components/layout/index.js.map +1 -1
  63. package/dist/components/layout/style.js.map +1 -1
  64. package/dist/components/middle-ellipsis/index.d.ts +1 -1
  65. package/dist/components/middle-ellipsis/index.js.map +1 -1
  66. package/dist/components/middle-ellipsis/truncate.d.ts +1 -1
  67. package/dist/components/middle-ellipsis/truncate.js.map +1 -1
  68. package/dist/components/popover/index.d.ts +1 -1
  69. package/dist/components/popover/index.js.map +1 -1
  70. package/dist/components/popover/style.js.map +1 -1
  71. package/dist/components/radio/index.d.ts +1 -1
  72. package/dist/components/radio/index.js +1 -1
  73. package/dist/components/radio/index.js.map +1 -1
  74. package/dist/components/radio-group/index.d.ts +7 -0
  75. package/dist/components/radio-group/index.js +21 -3
  76. package/dist/components/radio-group/index.js.map +1 -1
  77. package/dist/components/segmented/index.js.map +1 -1
  78. package/dist/components/segmented/style.js.map +1 -1
  79. package/dist/components/segmented-trigger/style.js.map +1 -1
  80. package/dist/components/select/index.js +1 -1
  81. package/dist/components/select/index.js.map +1 -1
  82. package/dist/components/select/style.js.map +1 -1
  83. package/dist/components/slider/index.js +3 -3
  84. package/dist/components/slider/index.js.map +1 -1
  85. package/dist/components/slider/style.js +1 -1
  86. package/dist/components/slider/style.js.map +1 -1
  87. package/dist/components/spinner/index.js +1 -1
  88. package/dist/components/spinner/index.js.map +1 -1
  89. package/dist/components/switch/index.d.ts +3 -0
  90. package/dist/components/switch/index.js +26 -8
  91. package/dist/components/switch/index.js.map +1 -1
  92. package/dist/components/switch/style.js.map +1 -1
  93. package/dist/components/textarea/index.js +1 -1
  94. package/dist/components/textarea/index.js.map +1 -1
  95. package/dist/components/textarea/style.js.map +1 -1
  96. package/dist/components/theme/index.js.map +1 -1
  97. package/dist/components/theme/style.js.map +1 -1
  98. package/dist/components/toast/style.js.map +1 -1
  99. package/dist/components/toast/toast.js +1 -1
  100. package/dist/components/toast/toast.js.map +1 -1
  101. package/dist/components/tooltip/index.d.ts +1 -1
  102. package/dist/components/tooltip/index.js.map +1 -1
  103. package/dist/components/tooltip/style.js.map +1 -1
  104. package/dist/scrollbars/index.d.ts +8 -8
  105. package/dist/scrollbars/index.js.map +1 -1
  106. package/dist/scrollbars/internal.js.map +1 -1
  107. package/dist/shared/element-height/index.js.map +1 -1
  108. package/dist/shared/focus/pointer-focus.js.map +1 -1
  109. package/dist/shared/group-management/index.js +5 -1
  110. package/dist/shared/group-management/index.js.map +1 -1
  111. package/dist/shared/label-emptiness/index.js.map +1 -1
  112. package/dist/shared/menu-behavior/index.js.map +1 -1
  113. package/dist/shared/menu-portal/menu-portal.js.map +1 -1
  114. package/dist/shared/menu-portal/menu-tree.js.map +1 -1
  115. package/dist/shared/option-portal/combobox-shell.js.map +1 -1
  116. package/dist/shared/option-portal/index.js.map +1 -1
  117. package/dist/shared/overlay/anchored-panel.js.map +1 -1
  118. package/dist/shared/overlay/native-dialog-presence.js.map +1 -1
  119. package/dist/shared/overlay/nested-drawer-layers.js.map +1 -1
  120. package/dist/shared/overlay/open-overlay.js.map +1 -1
  121. package/dist/shared/overlay/overlay.js.map +1 -1
  122. package/dist/shared/overlay/portal.js.map +1 -1
  123. package/dist/shared/overlay/presence.js.map +1 -1
  124. package/dist/shared/scroll-lock/scroll-lock.js.map +1 -1
  125. package/dist/shared/theme/root-sync.js.map +1 -1
  126. package/package.json +2 -2
  127. package/dist/icons/generated/heroicons-arrow-down-16-solid.d.ts +0 -6
  128. package/dist/icons/generated/heroicons-arrow-up-16-solid.d.ts +0 -6
  129. package/dist/icons/generated/jam-volume.d.ts +0 -8
  130. package/dist/icons/generated/jam-watch-f.d.ts +0 -8
  131. package/dist/icons/generated/ooui-check.d.ts +0 -6
  132. package/dist/icons/generated/radix-icons-check.d.ts +0 -6
  133. package/dist/icons/generated/si-check-alt-fill.d.ts +0 -6
@@ -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 * 胶囊中线的唯一定义处。热区与胶囊是 dialog 的两个直接子节点,定义在 dialog 上\n * 才能被双方读到——早前它定义在 .wui-drawer-drag-zone 上,靠「胶囊是热区后代」才读得到;\n * 两者拆成兄弟后那个定义位置就断了(胶囊读不到值,定位用的 calc 整条失效)。\n * 自定义属性沿树继承,热区仍然读得到它,诊断输出不受影响。\n *\n * 胶囊中线跟随内容区 padding,consumer 覆盖 --wui-drawer-content-padding 后拖拽条自动居中;\n * max() 的第二项是下限,分两截来由:\n * - 半个胶囊厚度:padding 归零时中线不会压在面板内缘上、一半落在面板之外\n * (dialog 是 overflow: visible,这半截不被裁掉而是骑在遮罩上)。\n * - 再加 4px 呼吸间距:只兜住「不越界」时胶囊仍贴死面板边缘,读作挤在边框阴影线外侧。\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 /* 阴影属于抽屉主体,不能由同尺寸的原生 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> 壳 + 面板滚动区)集中在 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 * 释放后的收尾(弹回打开位 / 滑出到闭合位):位移与遮罩交还 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 实现遮罩跟手淡出。 */\n opacity: var(--wui-internal-drag-backdrop-opacity, 1);\n}\n\n/*\n * 热区贴在抽屉内缘,宽度由 --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 * 热区仍覆盖内缘起 --wui-drawer-drag-zone-size,保证触控命中面积不随胶囊收窄。\n * 位置随 placement 切换:right 贴在左缘、left 贴在右缘、top 贴在底边、bottom 贴在顶边。\n *\n * 上下两条还要**让开开头尾两节**:热区是 dialog 的绝对定位兄弟节点,压在 panel 的端头上,\n * 而那一端上正有 header(bottom)或 footer(top)——按钮与点击都落在热区底下。让开量由\n * index.ts 用 ResizeObserver 量出该节的 border-box 高度写成 --wui-internal-drawer-*-inset,\n * 两者都不存在时为 0,热区回到修复前的边缘位置。\n *\n * 胶囊与热区因此**不共用一套偏移**:两者都是 dialog 的直接子节点(兄弟,不是父子),\n * 胶囊恒定相对 dialog 定位、贴住可抓取边缘,热区独自承担让开量。早前胶囊是热区的后代,\n * `top` 相对热区算,于是被动继承了那 60px 让位量、被一起推离面板边缘(实测内缩 68px)。\n * 让开量是热区一方的约束,不该外溢到纯视觉的胶囊上;反过来把热区推回 0 又会让 A6 的\n * header 遮挡立刻回归。两个约束只有靠结构解耦才同时成立,调数值必然牺牲其一。\n *\n * 代价是这一让开把胶囊所在的端头**腾空了**:胶囊贴回边缘后落在让开区里,两条带恒不相交。\n * 补洞的不是本元素,而是端头那条热边 `.wui-drawer-drag-edge`(见它的规则)——\n * 两者各管一段,缺一条胶囊就成了死装饰。R1 与 R3 在几何上本就不可同满足,\n * 这里只是把「不可同满足」拆成两段各自成立,而不是假装它们能同时成立。\n *\n * 按在面板边缘之外的坐标属于遮罩:轻点走「点遮罩关闭」(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` 贴的是面板**底边**,那一侧压住的是 footer。\n * 头部在内缘的另一端,与本 placement 的热区不重叠。\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 * 端头热边:贴住可抓取边缘的一条常驻热区,只在上下 placement 渲染(见 index.ts)。\n *\n * 它补的是上面那条让开量腾出的空档。胶囊贴回边缘后不在热区里了,用户却仍然看着它,\n * 于是「handle 靠近边缘」这个诉求必须连着可拖、可点亮一起兑现,否则胶囊只是装饰。\n *\n * **足迹不得超过它所代表的胶囊**:宽度取胶囊长度、水平居中,与胶囊同一套 left。\n * 这一条是本元素唯一真正的不变量,也是关按钮与消费侧 header 控件的安全保证——\n * 早前它是满宽的,于是横扫 header 整个宽度:实测 consumer 把 `--wui-drawer-drag-bar-thickness`\n * 调到 24px 时内置关闭按钮与消费侧左右按钮各被吃 40%、40px 时各 100%。\n * 关按钮从来不是特例,**满宽才是病根**。收窄后关按钮贴右缘、胶囊居中,\n * 只要胶囊自己没伸到按钮所在的位置,两者就永不相交——对所有 thickness / space-4\n * 组合都成立(断言按 8 组组合逐一量相交面积),且完全不依赖 close-top。\n *\n * 代价明说:触摸命中面积从满宽收到胶囊长度(默认 390×12 → 56×12)。可接受——\n * **热区本身没动**(满宽 20–32px 仍在让开位上),大面积起手能力并未丢失,\n * 热边只负责兑现「你看到的那个把手按得动」。\n *\n * 收窄后仍压不死的情况只有一类:消费侧把可点控件放在胶囊那条水平跨度内。\n * 纯 CSS 无法消除(带要覆盖胶囊就必须有胶囊那么宽,而胶囊就画在那里)。\n * 但那不是新引入的视觉冲突——thickness 40px 时胶囊自身已压在 header 内容上,\n * 热边只是把「胶囊盖住的地方」一并变成可拖。是 consumer 要求了一个粗把手,\n * 而把手就画在 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 的依据:与主热区 `--wui-drawer-drag-zone-size` 的默认厚度同值,\n * 于是**胶囊的命中带恒不窄于主热区**,「按你看到的把手」不会比「按旁边的带子」更难。\n * 再往上加会开始吃掉 header 里控件的位置(端头整块可拖后,那一节是要放控件的)。\n *\n * 取 max() 而非直接用 token,是为了保住包含关系:中线的下限(`max()` 第二项)保证\n * `中线 - 半个厚度 ≥ 4 > 0`,而热边从面板边缘(top/bottom placement 下恒为 0)起算,\n * 于是**胶囊带恒被热边带包含**——consumer 把 edge-size 调到比胶囊带还小也吃不掉它。\n * 这条包含关系是 token 定义推出来的,不是靠约定维持的。\n *\n * 竖向仍比胶囊多出「面板边缘到胶囊近侧」那一条(≤ 中线 − 半个厚度,默认 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 * 嵌套下层时整套拖拽提示一起退场,热边也不例外——否则下层抽屉会留一条看不见起点、\n * 按下去却能拖的悬空热边,比悬空胶囊更费解。\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 * 胶囊是纯视觉件,命中与拖拽全部归两条热区(端头热边 + 让开量热区)。\n * pointer-events: none 是这件事得以成立的前提:\n * 胶囊叠在热区之上(z-index 更高),若参与命中测试,按在胶囊上的手势会被胶囊吃掉,\n * 热区的 @pointerdown 收不到事件,从胶囊起手的拖拽直接失效。端头热边按构造覆盖胶囊带,\n * 所以「从胶囊起手」与「从热边空白处起手」命中的是同一个元素。\n *\n * 它同时解掉了 hover / active 的归属:pointer-events 为 none 时胶囊永不成为 hover 目标,\n * 指针落在胶囊上时下面的热区/热边照常 :hover,「热区点亮胶囊」在四个 placement 上都成立,\n * 不必给胶囊另挂监听(同一手势会触发两次)。\n */\n pointer-events: none;\n\n position: absolute;\n\n /*\n * 5 高于热区与端头热边(均 4),与面板主体(无 z-index 的静态盒)也不冲突。\n * 与 close 按钮同为 5:两者之间靠的不是层序而是几何(胶囊居中、按钮贴右缘)。\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 /* 与热区同一条淡入淡出,嵌套下层退场时两者同步(见下面的 is-nested-lower)。 */\n transition: opacity var(--wui-duration-feedback, 100ms) ease;\n}\n\n/*\n * 嵌套下层时整套拖拽提示一起退场,热区与胶囊一起。胶囊曾被热区那条规则顺带盖住\n * (它是热区的后代,opacity 作用于它的渲染);拆成兄弟后必须显式列出,否则下层抽屉\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 * 通用兄弟选择器:胶囊排在这两个热区之后(index.ts 的 render 里同一个模板表达式产出\n * 这三个节点),`~` 因此在**所有 placement** 上都成立,且与「热边是否存在」无关——\n * 上下 placement 多一条热边、左右 placement 只有一条,写成 `+` 会让热边那条静默失配。\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 * 达到关闭阈值的确认态:比拖拽静息态更实的中性灰,取代原来的 accent 蓝。\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 同)。面板主体 `.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 * 不靠把热区铺上来——铺上来会把这一节里的控件吞掉,正是 A6 修掉的那条缺陷。\n * 所以这里只需要手势与光标两件样式,命中关系由那一节自己承担。\n *\n * `touch-action: none` 不可省:否则触摸落在标题文字或控件上时,浏览器会先把它\n * 判成滚动意图,越过阈值就 pointercancel,拖拽根本起不来(与热区同一条理由)。\n * 它是祖先节点,触摸落在深层 slotted 子元素上时同样生效——touch-action 沿命中链\n * 求交,不要求落在声明元素本身上。\n *\n * 按 `[draggable]` 收口:draggable 为假时这一节不接手势,也不该被夺走触摸手势。\n *\n * 再按 `dialog:not(.is-nested-lower)` 收口,与事件层闸门(index.ts 的\n * _handleSectionDragPointerDown)和热区 / 热边 / 胶囊那三条 `is-nested-lower` 退场\n * 判同一条:嵌套下层的抽屉整套拖拽提示一起退场。这里漏掉就会只剩提示面没退——\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/* ===== 关闭按钮(绝对定位)=====\n top 恰好等于 header padding 的垂直分量(16px),因此挂 --wui-space-4 与 header padding\n 同源:覆盖该级时二者一起动,注释里写死的对齐关系不会断。\n right 是按钮到 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,高于端头热边(4)。\n *\n * 端头热边收窄到胶囊长度之后,默认与常规 token 下它与本按钮的相交面积**恒为 0px²**\n * (实测 thickness 4/16/24/40 × space-4 16/8/4 共 12 组,测试按这个矩阵逐组断言),\n * 正常情形靠几何就够。\n * 这一层防的是几何失效的那一格:`--wui-drawer-drag-bar-length` 同样是公开 token,\n * 调到把手伸到按钮所在的位置时相交面积不为 0(实测 320px 时为 78px²),\n * 此时唯一还能救的就是层序。\n *\n * 为它单独抬这一层是有实测依据的**不对称**,不是泛泛的防御性声明:\n * 消费侧把控件塞进把手跨度里时,同一招不管用——Chromium 里端头热边始终压在\n * **slotted** 节点之上,给 slotted 元素抬 z-index 无效(已在测试注释里记下实测)。\n * 本按钮是组件自己画的 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 时抽屉是四周留边的圆角卡片,与 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 记录它是上下抽屉的高度,demo 的自定义高按钮\n * 直接指向它);\n * - `.wui-drawer-body` 的 `height: 100%` 解析成 auto,`.wui-drawer-content` 拿到的是\n * 内容高度而不是面板高度,`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 * 在文件更后方,必须以更高特异性显式恢复打开位,否则抽屉停在闭合位。\n *\n * Nested 层叠(对齐卡片堆栈效果):非顶层抽屉按 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,窄屏随桌面同比缩小:桌面 54→43.2,窄屏 36→28.8,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 也放得下(54px 一档的 74.86px 才会溢出)。\n * 保留窄屏档是为了不改变既定比例关系,同时给窄屏留出更宽的抽屉可视区。\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":""}
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 * 才能被双方读到——早前它定义在 .wui-drawer-drag-zone 上,靠「胶囊是 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 回到修复前的边缘位置。\n *\n * 胶囊与 drag zone 因此**不共用一套偏移**:两者都是 dialog 的直接子节点(兄弟,不是父子),\n * 胶囊恒定相对 dialog 定位、贴住可抓取边缘,drag zone 独自承担让开量。早前胶囊是 drag zone 的后代,\n * `top` 相对 drag zone 算,于是被动继承了那 60px 让位量、被一起推离 panel 边缘(实测内缩 68px)。\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 * 代价明说:触摸命中面积从满宽收到胶囊长度(默认 390×12 → 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 * 达到关闭阈值的确认态:比拖拽静息态更实的中性灰,取代原来的 accent 蓝。\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,窄屏随桌面同比缩小:桌面 54→43.2,窄屏 36→28.8,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 也放得下(54px 一档的 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":""}
@@ -19,9 +19,9 @@ export declare class WebUiDropdown extends LitElement {
19
19
  private readonly _overlay;
20
20
  private readonly _scrollLock;
21
21
  /**
22
- * 当前 Escape 候选的会话句柄(level 0)。面板要等一帧才建好,而 Escape 可能在这之前
23
- * 到达:先以宿主为 panel claim 一次,面板就绪后 release 旧会话、再以真实面板重新 claim。
24
- * 子菜单的句柄存在各自 `MenuOverlay.handle` 上(经 adopt 进本句柄子树)。
22
+ * 当前 Escape 候选的会话 handle(level 0)。panel 要等一帧才建好,而 Escape 可能在这之前
23
+ * 到达:先以宿主为 panel claim 一次,panel 就绪后 release 旧会话、再以真实 panel 重新 claim。
24
+ * submenu 的 handle 存在各自 `MenuOverlay.handle` 上(经 adopt 进本 handle 子树)。
25
25
  */
26
26
  private _handle;
27
27
  private readonly _outsideClickGuard;
@@ -54,11 +54,11 @@ export declare class WebUiDropdown extends LitElement {
54
54
  protected updated(changed: Map<string, unknown>): void;
55
55
  /**
56
56
  * 焦点是否已无归属:被 detach 打掉的焦点会落回 `body`,用户主动点到别处时不会。
57
- * 重挂载对账只在前者才把焦点夺回菜单,否则等于从消费者手里抢焦点。
57
+ * 重挂载对账只在前者才把焦点夺回 menu,否则等于从消费者手里抢焦点。
58
58
  */
59
59
  private _isFocusOrphaned;
60
60
  /**
61
- * 打开态的唯一对账入口:面板、滚动锁、outside click、hover 与菜单焦点都从这里收敛,
61
+ * 打开态的唯一对账入口:panel、滚动锁、outside click、hover 与 menu 焦点都从这里收敛,
62
62
  * `updated()` 的打开分支与重连对账共用它,两条路径必须落到同一个状态。
63
63
  */
64
64
  private _syncOpenOverlay;
@@ -81,7 +81,7 @@ export declare class WebUiDropdown extends LitElement {
81
81
  private _populateOverlay;
82
82
  private _depopulateOverlay;
83
83
  private _claimRoot;
84
- /** 句柄缺失时补登记;面板还没建好时以宿主代填,与 `_openMenu` 的占位会话同一口径。 */
84
+ /** handle 缺失时补登记;panel 还没建好时以宿主代填,与 `_openMenu` 的占位会话同一口径。 */
85
85
  private _ensureRootHandle;
86
86
  private _ensureOverlay;
87
87
  private _buildOverlay;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/dropdown/index.ts"],"sourcesContent":["import type { Placement } from '@floating-ui/dom'\nimport { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { UserChangeController } from '@/shared/events/user-change'\nimport {\n createClosingSubmenuStack,\n createMenuHoverBinder,\n createMenuOutsideClickGuard,\n getEnabledMenuLevelItems,\n getFocusedMenuItemFromPanels,\n handleMenuKeyboard,\n type MenuHoverDelegate,\n type MenuKeyboardDelegate\n} from '@/shared/menu-behavior'\nimport { createMenuPortalOverlay } from '@/shared/menu-portal/menu-portal'\nimport {\n captureFrameworkAnchors,\n focusMenuItem,\n getMenuChildren,\n getMenuItemFromEvent,\n hideNestedMenuChildren,\n moveMenuChildren,\n orderManagedMenuItems,\n reconcileManagedMenuItems,\n restoreFrameworkAnchors,\n returnManagedMenuItemsToSlot,\n type MenuItemAnchors\n} from '@/shared/menu-portal/menu-tree'\nimport { normalizeLiteral, normalizeNumber } from '@/shared/normalize'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { defineOpenOverlay, type OpenOverlayHandle } from '@/shared/overlay/open-overlay'\nimport { defineOverlay, type OverlayApi } from '@/shared/overlay/overlay'\nimport { FLOATING_PLACEMENTS } from '@/shared/overlay/placement-props'\nimport { hideOverlayPresence, showOverlayPresence } from '@/shared/overlay/presence'\nimport { defineScrollLockLease } from '@/shared/scroll-lock/scroll-lock'\n\nimport style from './style.css?inline'\n\nexport type { Placement }\n\nconst SLOT_PREFIX = 'web-ui-menu-level-'\n// 一级菜单项的模板位 marker(机制与 context-menu 共用,见 menu-tree 的托管项锚说明)。\nconst MENU_ITEM_MARKER = 'wui-dropdown-menu-item'\n\nlet dropdownIdCounter = 0\n\ninterface MenuOverlay {\n api: OverlayApi\n overlay: HTMLElement\n content: HTMLElement\n /** 该层为子菜单时的会话句柄;根层句柄由组件单独持有(见 `_handle`)。 */\n handle?: OpenOverlayHandle\n}\n\n@customElement('web-ui-dropdown')\nexport class WebUiDropdown extends LitElement {\n static override styles = unsafeCSS(style)\n\n @property({ type: Boolean, reflect: true }) open = false\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true, attribute: 'match-width' }) matchWidth = false\n @property({ type: Boolean, reflect: true, attribute: 'no-scroll-lock' }) noScrollLock = false\n\n private _placement: Placement = 'bottom-start'\n\n @property({ type: String, reflect: true })\n get placement(): Placement {\n return this._placement\n }\n set placement(v: string) {\n this._placement = normalizeLiteral(v, FLOATING_PLACEMENTS, 'bottom-start')\n }\n\n private _offset = 4\n\n @property({ type: Number })\n get offset(): number {\n return this._offset\n }\n set offset(v: number) {\n this._offset = normalizeNumber(v, 0, 100, 4)\n }\n\n @state() private _activePath: number[] = []\n\n private readonly _overlays = new Map<number, MenuOverlay>()\n /** 实例作用域的帧事务入口(合并了原 lifecycle)。 */\n private readonly _overlay = defineOpenOverlay().make({\n requestClose: () => this._keyboardDelegate.closeDeepestOrAll(),\n isConnected: () => this.isConnected\n })\n private readonly _scrollLock = defineScrollLockLease().make()\n /**\n * 当前 Escape 候选的会话句柄(level 0)。面板要等一帧才建好,而 Escape 可能在这之前\n * 到达:先以宿主为 panel claim 一次,面板就绪后 release 旧会话、再以真实面板重新 claim。\n * 子菜单的句柄存在各自 `MenuOverlay.handle` 上(经 adopt 进本句柄子树)。\n */\n private _handle: OpenOverlayHandle | null = null\n // 行为层(hover / outside-click / 键盘 / submenu 收尾)由 shared/menu-behavior 驱动\n private readonly _outsideClickGuard = createMenuOutsideClickGuard(\n this,\n // 根句柄的 contains 递归覆盖全部已 adopt 的子层,一次查询替代逐层判定。\n node => this._handle?.contains(node) ?? false\n )\n private readonly _closingSubmenus = createClosingSubmenuStack<MenuOverlay>({\n getPanel: container => container.overlay,\n restoreItems: (container, parentItem) => moveMenuChildren(container.content, parentItem),\n dispose: container => {\n container.handle?.release()\n container.overlay.remove()\n container.api.dispose()\n }\n })\n private readonly _hoverDelegate: MenuHoverDelegate = {\n getOpenDepth: () => this._activePath.length,\n getLevelItems: level => this._getLevelItems(level),\n getActiveItem: level => this._getLevelItems(level)[this._activePath[level]],\n isSubmenuItem: item => item.hasAttribute('submenu'),\n openSubmenu: item => {\n const level = this._getFocusedLevel(item)\n if (level === undefined) return\n const itemIndex = this._getLevelItems(level).indexOf(item)\n if (itemIndex >= 0) this._toggleSubmenu(level, itemIndex)\n },\n closeFrom: level => this._closeSubmenuFrom(level + 1)\n }\n private readonly _hoverBinder = createMenuHoverBinder(this._hoverDelegate, 'web-ui-dropdown-item')\n private readonly _keyboardDelegate: MenuKeyboardDelegate = {\n getFocusedItem: event =>\n getFocusedMenuItemFromPanels(\n event,\n [...this._overlays.values()].map(({ overlay }) => overlay),\n 'web-ui-dropdown-item'\n ),\n getLevelOf: item => this._getFocusedLevel(item),\n getEnabledItems: level => getEnabledMenuLevelItems(this._overlays.get(level)?.content),\n isSubmenuItem: item => item.hasAttribute('submenu'),\n openSubmenuInstant: item => {\n const level = this._getFocusedLevel(item)\n if (level === undefined) return\n const itemIndex = this._getLevelItems(level).indexOf(item)\n if (itemIndex >= 0) this._toggleSubmenu(level, itemIndex, true)\n },\n closeToParent: level => {\n this._closeSubmenuFrom(level)\n return this._getLevelItems(level - 1)[this._activePath[level - 1]]\n },\n closeDeepestOrAll: () => {\n if (this._activePath.length > 0) this._closeSubmenuFrom(this._activePath.length)\n else this._closeAll(true)\n }\n }\n private readonly _userOpenChange = new UserChangeController()\n private _restoreFocusTarget?: HTMLElement\n private _shouldOpenInstantly = true\n /**\n * 本次打开是否抑制首项的 `:focus-visible` accent 视觉(口径与 context-menu 一致)。\n *\n * 默认 `true`(抑制),只有键盘激活触发的打开才置 `false`:首项的焦点是组件程序化\n * 落的,而各引擎对「程序化 focus 是否算 focus-visible」判定不同 —— Chromium 走启发式\n * 通常不匹配,WKWebView 则会匹配(用户在 Interweave 桌面 app 实测到蓝底白字),\n * 于是同一段代码只在 WebKit 侧暴露出缺陷。取默认抑制而非默认保留,是因为冷启动的\n * `open` 属性路径拿不到任何手势模态信息,抑制是唯一确定安全的一侧;误伤键盘用户的\n * 代价有界:`handleMenuKeyboard` 首次方向键即以无参 `focusMenuItem` 重新落焦,\n * `menu-tree` 的 `toggleAttribute` 无条件清掉本属性,accent 当场回来。\n */\n private _suppressInitialFocusVisible = true\n private _menuSyncScheduled = false\n private readonly _level0ItemAnchors: MenuItemAnchors = new Map()\n // 打开期实时渲染:框架(Vue/React 条件渲染)可能在菜单打开期间持续插入菜单项。\n // 观察宿主子树(新增项可能被框架 wrapper 包裹)与各层级面板 content 的子树\n // (打开中的 submenu 父项已被迁入 content,其新增子项只在这里可见),下一帧统一\n // 把新子树迁入对应层级面板;面板尺寸变化由 defineOverlay 的 autoUpdate 跟进重定位。\n private readonly _menuContentObserver = new MutationObserver(() => {\n if (this.open) this._scheduleMenuSync()\n })\n\n get isOpen(): boolean {\n return this.open || this._activePath.length > 0\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this._overlay.resume()\n this.addEventListener('keydown', this._onKeydown)\n document.addEventListener('click', this._onClickOutside)\n this._menuContentObserver.observe(this, { childList: true, subtree: true })\n /*\n * 重挂载对账:`disconnectedCallback` 会拆掉面板、把菜单项迁回宿主,但 `open` 是公开\n * prop,不因卸载而改写。Lit 在 detach 期间不记 `changedProperties`,重连后 `updated()`\n * 不再命中 `changed.has('open')`,面板于是永久缺席:宿主仍反射 `open` 与\n * `aria-expanded`,Escape 和 outside click 却无层可关。首连时 `open` 在\n * `changedProperties` 里、由 `updated()` 负责,`hasUpdated` 就是用来只认重连的。\n */\n if (this.open && this.hasUpdated) this._syncOpenOverlay(this._isFocusOrphaned())\n }\n\n override firstUpdated() {\n // trigger slot 内容增删不触发宿主响应式更新:显式请求一轮渲染,\n // 由 updated() 的 ARIA 回写覆盖晚到的 trigger 元素。\n this.shadowRoot\n ?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]')\n ?.addEventListener('slotchange', () => this.requestUpdate())\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._menuContentObserver.disconnect()\n this.removeEventListener('keydown', this._onKeydown)\n document.removeEventListener('click', this._onClickOutside)\n this._outsideClickGuard.dispose()\n this._hoverBinder.dispose()\n // 停止帧调度;登记由 _cleanupClosedMenu → _disposeAll 撤销。\n this._overlay.suspend()\n this._scrollLock.release()\n this._cleanupClosedMenu()\n }\n\n protected override updated(changed: Map<string, unknown>) {\n // ARIA 回写不依赖 open 分支,任何渲染后都保持与宿主状态同步。\n this._syncTriggerAria()\n\n if (changed.has('open')) {\n if (this.open) {\n this._syncOpenOverlay(true)\n } else {\n this._overlay.invalidate()\n this._syncScrollLock()\n /*\n * 复位打开模态:`open` 真正翻回 false 才是「上一次打开会话结束」的唯一信号,\n * 下一会话若不经 `_openMenu()`(宿主直接翻 `open` 属性)就从默认口径重新起算,\n * 不会把上一次键盘打开留下的 `false` 带进这次鼠标驱动的声明式打开。\n * 刻意不复位在 `disconnectedCallback`/`_cleanupClosedMenu`:重挂载对账(:188)\n * 期间 `open` 始终为 true,那次重取焦点属于同一会话,应保留原模态。\n */\n this._suppressInitialFocusVisible = true\n /*\n * release ⟺ 关闭:新模块里「已登记」就等于「参与仲裁」,不再由 host.isOpen()\n * 兜底过滤,所以必须在这里撤销,不能等到退场动画结束。\n */\n this._handle?.release()\n this._handle = null\n void this._closeRootAfterPresence()\n }\n if (this._userOpenChange.consume()) {\n dispatchOpenChangeEvent(this, this.open)\n }\n }\n if (changed.has('noScrollLock')) this._syncScrollLock()\n if (changed.has('placement') || changed.has('offset') || changed.has('matchWidth')) {\n this._overlays.get(0)?.api.update({\n placement: this.placement,\n offset: this.offset,\n matchWidth: this.matchWidth\n })\n }\n this._hoverBinder.bind()\n }\n\n /**\n * 焦点是否已无归属:被 detach 打掉的焦点会落回 `body`,用户主动点到别处时不会。\n * 重挂载对账只在前者才把焦点夺回菜单,否则等于从消费者手里抢焦点。\n */\n private _isFocusOrphaned(): boolean {\n const active = document.activeElement\n return active === null || active === document.body || active === document.documentElement\n }\n\n /**\n * 打开态的唯一对账入口:面板、滚动锁、outside click、hover 与菜单焦点都从这里收敛,\n * `updated()` 的打开分支与重连对账共用它,两条路径必须落到同一个状态。\n */\n private _syncOpenOverlay(takeFocus: boolean) {\n this._restoreFocusTarget ??= document.activeElement instanceof HTMLElement ? document.activeElement : undefined\n this._outsideClickGuard.arm()\n this._syncScrollLock()\n this._hideAllSubmenuChildren()\n this._overlay.invalidate()\n this._overlay.scheduleFrame(() => {\n this._ensureOverlay(0, this._shouldOpenInstantly)\n this._shouldOpenInstantly = true\n if (takeFocus) {\n focusMenuItem(getEnabledMenuLevelItems(this._overlays.get(0)?.content)[0], {\n suppressFocusVisible: this._suppressInitialFocusVisible\n })\n }\n // 面板 id 在 overlay 构建后才有:回写 aria-controls 指向。\n this._syncTriggerAria()\n })\n this._bindHoversAfterUpdate()\n }\n\n /*\n * 命令式打开,不派发 open-change。命令式入口没有手势模态信息,按与 context-menu 的\n * `openAt()` 同一口径抑制首项 accent。\n */\n openMenu() {\n this._openMenu(true, false, true)\n }\n\n private _openMenu(isInstant: boolean, fromUser: boolean, suppressFocusVisible: boolean) {\n if (this.disabled || this.open) return\n this._outsideClickGuard.arm()\n this._shouldOpenInstantly = isInstant\n this._suppressInitialFocusVisible = suppressFocusVisible\n if (fromUser) this._userOpenChange.mark()\n // 面板要等一帧才建好,而 Escape 可能在这之前到达:先用宿主当 panel claim 一次;\n // 面板就绪后 _buildOverlay 会 release 这个占位会话,再以真实面板重新 claim。\n this._claimRoot(this._overlays.get(0)?.overlay ?? this)\n this.open = true\n }\n\n // 命令式关闭全部层级,不派发 open-change。\n closeAll() {\n this._closeAll(false)\n }\n\n private _closeAll(fromUser: boolean) {\n if (!this.open) return\n if (fromUser) this._userOpenChange.mark()\n this.open = false\n }\n\n private _closeAllSubmenus() {\n this._closeSubmenuFrom(1, true)\n this._closingSubmenus.restoreAll()\n this._syncActiveAttrs()\n }\n\n private _cleanupClosedMenu() {\n this._closeAllSubmenus()\n this._returnLevel0Items()\n this._disposeAll()\n }\n\n private async _closeRootAfterPresence() {\n const overlay = this._overlays.get(0)?.overlay\n if (overlay && !(await hideOverlayPresence(overlay))) return\n if (this.open || !this.isConnected) return\n\n this._cleanupClosedMenu()\n this._restoreFocusTarget?.focus()\n this._restoreFocusTarget = undefined\n }\n\n private _hideAllSubmenuChildren() {\n hideNestedMenuChildren(this, `${SLOT_PREFIX}-hidden`)\n }\n\n private _toggleSubmenu(level: number, itemIndex: number, isInstant = false) {\n if (this._activePath[level] === itemIndex) {\n return\n }\n\n this._closeSubmenuFrom(level + 1)\n\n this._activePath = [...this._activePath.slice(0, level), itemIndex]\n this._syncActiveAttrs()\n\n const item = this._getLevelItems(level)[itemIndex]\n if (item?.hasAttribute('submenu')) {\n this._overlay.scheduleFrame(() => {\n if (!this.open || this._activePath[level] !== itemIndex) return\n const restored = this._ensureOverlay(level + 1, isInstant, item)\n if (!restored) this._populateOverlay(level + 1, item)\n })\n }\n\n this._bindHoversAfterUpdate()\n }\n\n private _closeSubmenuFrom(level: number, isInstant = false) {\n for (let lv = this._activePath.length; lv >= level; lv--) {\n const overlay = this._overlays.get(lv)\n const parentItem = this._getLevelItems(lv - 1)[this._activePath[lv - 1]]\n if (!overlay) continue\n\n if (!parentItem || isInstant) {\n this._depopulateOverlay(lv, parentItem)\n this._disposeOverlay(lv)\n } else {\n this._overlays.delete(lv)\n this._closingSubmenus.closeAsync(parentItem, overlay)\n }\n }\n const nextActivePath = this._activePath.slice(0, level - 1)\n if (nextActivePath.length !== this._activePath.length) {\n this._activePath = nextActivePath\n }\n this._syncActiveAttrs()\n }\n\n private _syncActiveAttrs() {\n this.querySelectorAll('web-ui-dropdown-item').forEach(item => {\n item.removeAttribute('active')\n })\n this._overlays.forEach((_, level) => {\n this._getLevelItems(level).forEach(item => item.removeAttribute('active'))\n })\n this._activePath.forEach((itemIndex, level) => {\n const item = this._getLevelItems(level)[itemIndex]\n if (item?.matches('web-ui-dropdown-item')) {\n item.setAttribute('active', '')\n }\n })\n }\n\n private _getLevelItems(level: number): HTMLElement[] {\n const content = this._overlays.get(level)?.content\n return content ? getMenuChildren(content) : []\n }\n\n private _populateLevel0() {\n const content = this._overlays.get(0)?.content\n if (!content) return\n // 初次打开即建立 marker 锚:宿主此后始终持有模板序骨架,打开期框架插入与\n // 关闭归还都以它为基准(机制与 context-menu 共用,见 menu-tree)。\n reconcileManagedMenuItems(this, content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n orderManagedMenuItems(this, content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n }\n\n private _scheduleMenuSync() {\n if (this._menuSyncScheduled) return\n this._menuSyncScheduled = true\n requestAnimationFrame(() => {\n this._menuSyncScheduled = false\n if (!this.open) return\n this._syncOpenMenuContent()\n })\n }\n\n // 打开期的统一内容同步:把打开期间框架新增的菜单子树迁入对应层级面板。\n // 一级项走 marker 锚 reconcile(保序 + 关闭按模板位归还);面板尺寸变化由\n // defineOverlay 的 autoUpdate 跟进重定位。\n private _syncOpenMenuContent() {\n const level0Content = this._overlays.get(0)?.content\n // 迁移前隐藏宿主中新项的嵌套子项(hidden slot 属性随元素一起迁移)。\n this._hideAllSubmenuChildren()\n if (level0Content) {\n reconcileManagedMenuItems(this, level0Content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n // 新迁入项的嵌套子项未打 hidden slot,会经 dropdown-item 默认 slot 渲染进\n // 父项 label 叠到菜单上;面板项在面板内渲染,宿主查询够不到,须对 content 补隐藏。\n hideNestedMenuChildren(level0Content, `${SLOT_PREFIX}-hidden`)\n // 框架注释锚点复位独立于元素重排:锚点漂移不收敛会破坏后续翻转的插入点(同 context-menu)。\n const frameworkAnchors = captureFrameworkAnchors(level0Content, MENU_ITEM_MARKER)\n orderManagedMenuItems(this, level0Content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n restoreFrameworkAnchors(level0Content, frameworkAnchors)\n }\n for (const [level, overlay] of this._overlays) {\n if (level === 0) continue\n const parentItem = this._getLevelItems(level - 1)[this._activePath[level - 1]]\n if (parentItem) moveMenuChildren(parentItem, overlay.content)\n hideNestedMenuChildren(overlay.content, `${SLOT_PREFIX}-hidden`)\n }\n this._hoverBinder.bind()\n }\n\n private _returnLevel0Items() {\n const content = this._overlays.get(0)?.content\n if (content) returnManagedMenuItemsToSlot(this, content, this._level0ItemAnchors)\n }\n\n private _populateOverlay(level: number, submenuItem: HTMLElement) {\n const content = this._overlays.get(level)?.content\n if (content) moveMenuChildren(submenuItem, content)\n }\n\n private _depopulateOverlay(level: number, parentItem?: HTMLElement) {\n const content = this._overlays.get(level)?.content\n if (!content) return\n const targetLevel = level - 1\n const submenuItem = parentItem ?? this._getLevelItems(targetLevel)[this._activePath[targetLevel]]\n if (submenuItem) moveMenuChildren(content, submenuItem)\n }\n\n /*\n * 登记与「面板是否本帧新建」解耦,与 context-menu 的根层修复同构。\n *\n * 退场被打断时(`_closeRootAfterPresence` 在 `hideOverlayPresence` 返回 false 后提前\n * return)面板仍在 DOM 里,而关闭分支已经把句柄撤了。此时重开走的是 `_ensureOverlay`\n * 的两条复用分支,它们不会重跑 `_buildOverlay`——原先只有 `_buildOverlay` 会 claim/adopt,\n * 于是出现「面板可见 + `open === true` 却没有任何登记」:Escape 关不掉它(仲裁看不到它),\n * 守卫里 `contains` 恒 false,点面板内部反被当成外部点击关掉整张菜单。\n *\n * `open` 属性路径(`updated()` 里 scheduleFrame 到 `_ensureOverlay(0)`)与 `openMenu()`\n * 都落在复用分支上,因此**所有**根 claim 都必须走这里 —— 只在 `_ensureOverlay` 里补 claim\n * 是不够的:`openMenu()` 会先把句柄建好,使那次补 claim 变成 no-op,子层照样被漏掉。\n *\n * 新 claim 即新会话,旧会话连同其全部 adopt 子树一起作废,因此**仍在场的**子层必须\n * 在这里统一重挂。来源有两个:`_overlays` 里登记的层级,以及本轮已退场、被移出\n * `_overlays` 却仍在 DOM 里的收尾栈面板 —— 只看前者会漏掉后者(见下)。\n */\n private _claimRoot(panel: HTMLElement): OpenOverlayHandle {\n this._handle?.release()\n this._handle = this._overlay.claim(panel)\n for (const [level, sub] of this._overlays) {\n if (level === 0) continue\n sub.handle = this._handle.adopt(sub.overlay)\n }\n /*\n * 收尾栈里的子菜单面板同样「可见但已脱离快照」:`_closeSubmenuFrom` 只把它们移出\n * `_overlays`,层仍挂在旧句柄下、面板仍在 DOM 里退场。不重挂的话它们会被判成面板外,\n * 点它内部就关掉整张菜单 —— base 的 `composition.contains` 走登记树、且退场期间不注销,\n * 所以这条是相对 base 的行为回归,不是 parity。\n * 新句柄要写回容器:收尾结束时 adapter.dispose 释放的是 `container.handle`。\n */\n for (const container of this._closingSubmenus.closing()) {\n container.handle = this._handle.adopt(container.overlay)\n }\n return this._handle\n }\n\n /** 句柄缺失时补登记;面板还没建好时以宿主代填,与 `_openMenu` 的占位会话同一口径。 */\n private _ensureRootHandle(): OpenOverlayHandle {\n return this._handle ?? this._claimRoot(this._overlays.get(0)?.overlay ?? this)\n }\n\n private _ensureOverlay(level: number, isInstant = false, submenuItem?: HTMLElement): boolean {\n const closingOverlay = submenuItem ? this._closingSubmenus.take(submenuItem) : undefined\n if (closingOverlay) {\n // 从 closing 栈取回的面板:登记已在关闭时撤销,这里必须重新挂回根句柄子树。\n closingOverlay.handle = this._ensureRootHandle().adopt(closingOverlay.overlay)\n this._overlays.set(level, closingOverlay)\n closingOverlay.api.open()\n showOverlayPresence(closingOverlay.overlay, { isInstant })\n return true\n }\n const existing = this._overlays.get(level)\n if (existing) {\n this._ensureRootHandle()\n existing.api.open()\n showOverlayPresence(existing.overlay, { isInstant })\n return false\n }\n this._buildOverlay(level, isInstant)\n return false\n }\n\n private _buildOverlay(level: number, isInstant = false) {\n const { panel: overlay, content } = createMenuPortalOverlay('dropdown-overlay', this)\n overlay.setAttribute('role', 'menu')\n // 根层级面板带 id:trigger 的 aria-controls 指向它(Q9a)。\n if (level === 0) overlay.id = `wui-dropdown-menu-${++dropdownIdCounter}`\n overlay.dataset.level = String(level)\n overlay.addEventListener('click', this._onMenuClick)\n overlay.addEventListener('keydown', this._onKeydown)\n\n const anchor = level === 0 ? this._queryTriggerAnchor() : this._getSubmenuTriggerAnchor(level - 1)\n\n if (anchor) {\n const ctrl = defineOverlay().make({\n anchor,\n overlay,\n placement: level === 0 ? this.placement : 'right-start',\n offset: level === 0 ? this.offset : 0,\n // 菜单至少与 trigger/父项同宽,内容可更宽(floor 来自 --wui-overlay-min-width)\n minAnchorWidth: true,\n matchWidth: level === 0 ? this.matchWidth : false,\n strategy: 'fixed'\n })\n if (level === 0) {\n // 面板就绪:释放「以宿主代填」的占位会话,换成真实面板。\n this._claimRoot(overlay)\n this._overlays.set(level, { api: ctrl, overlay, content })\n this._populateLevel0()\n } else {\n /*\n * 子层进根句柄子树,默认不是独立候选 —— 「在树里但不是这一层」由此表达;\n * 原先按 level 找父面板的判断随之消失。用 `_ensureRootHandle()` 而不是 `_handle?.`:\n * 后者在句柄意外缺失时会静默跳过 adopt,留下一棵无人认领的子树。\n */\n const handle = this._ensureRootHandle().adopt(overlay)\n this._overlays.set(level, { api: ctrl, overlay, content, handle })\n }\n // submenu 父项在面板 content 内,其新增子项只有观察 content 子树才能看到\n this._menuContentObserver.observe(content, { childList: true, subtree: true })\n ctrl.open()\n showOverlayPresence(overlay, { isInstant })\n }\n }\n\n private _disposeOverlay(level: number) {\n const overlay = this._overlays.get(level)\n if (level === 0) {\n // 根句柄撤销会递归撤销全部已 adopt 的子层。\n this._handle?.release()\n this._handle = null\n }\n // 子层句柄随所属 MenuOverlay 释放;已被根层递归撤销时幂等无操作。\n overlay?.handle?.release()\n overlay?.overlay.removeEventListener('click', this._onMenuClick)\n overlay?.overlay.removeEventListener('keydown', this._onKeydown)\n overlay?.overlay.remove()\n overlay?.api.dispose()\n this._overlays.delete(level)\n this._rebindMenuContentObservers()\n }\n\n // jsdom 的 MutationObserver 不实现 unobserve;统一 disconnect 后重绑剩余目标。\n private _rebindMenuContentObservers() {\n this._menuContentObserver.disconnect()\n this._menuContentObserver.observe(this, { childList: true, subtree: true })\n for (const overlay of this._overlays.values()) {\n this._menuContentObserver.observe(overlay.content, { childList: true, subtree: true })\n }\n }\n\n private _disposeAll() {\n /*\n * 先无条件撤销根句柄。`_openMenu` 会先用宿主当 panel claim 一次占位会话(真实面板要等\n * 一帧才建好),若这段时间内宿主被卸载、`_buildOverlay` 没机会跑,`_overlays` 仍是空的\n * —— 只遍历 `_overlays` 就会漏掉它:该层永久留在模块注册表里强引用整个组件,且宿主\n * 重挂载后 `panel.isConnected` 又为真,占位层重新成为候选、吞掉 Escape。\n */\n this._handle?.release()\n this._handle = null\n for (const level of this._overlays.keys()) {\n this._disposeOverlay(level)\n }\n }\n\n private _syncScrollLock(isOpen = this.open) {\n this._scrollLock.sync(isOpen && !this.noScrollLock)\n }\n\n private _queryTriggerAnchor(): HTMLElement | null {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]')\n const el = slot?.assignedElements()[0]\n return el instanceof HTMLElement ? el : null\n }\n\n /*\n * trigger 语义回写(Q9a):slot 进来的元素是真正的可聚焦交互元素,\n * aria-haspopup / aria-expanded / aria-controls 必须设在它身上;\n * 组件没有 trigger 包装 div,此前完全没有 ARIA 接线。\n */\n private _syncTriggerAria() {\n const trigger = this._queryTriggerAnchor()\n if (!trigger) return\n trigger.setAttribute('aria-haspopup', 'menu')\n trigger.setAttribute('aria-expanded', String(this.open))\n const rootOverlay = this._overlays.get(0)?.overlay\n if (rootOverlay?.id) trigger.setAttribute('aria-controls', rootOverlay.id)\n else trigger.removeAttribute('aria-controls')\n }\n\n private _getSubmenuTriggerAnchor(level: number): HTMLElement | null {\n const idx = this._activePath[level]\n if (idx === undefined) return null\n const items = this._getLevelItems(level)\n const item = items[idx]\n if (!item) return null\n return item.shadowRoot?.querySelector('.item-inner') ?? item\n }\n\n private _onTriggerClick = (event: MouseEvent) => {\n if (this.disabled) return\n if (this.open) {\n this._closeAll(true)\n } else {\n /*\n * `detail === 0` ⇔ 键盘激活(Enter/Space 触发的 click),`detail > 0` ⇔ 指针点击。\n * 同一个信号既决定是否免入场过渡,也决定首项要不要留 `:focus-visible`:\n * 键盘打开首项即应有 accent 焦点视觉,指针打开则抑制(见 `_suppressInitialFocusVisible`)。\n */\n const fromKeyboard = event.detail === 0\n this._openMenu(fromKeyboard, true, !fromKeyboard)\n }\n }\n\n private _onClickOutside = (e: MouseEvent) => {\n if (!this.isOpen || this._outsideClickGuard.isArmed()) return\n if (this._outsideClickGuard.isInside(e)) return\n this._closeAll(true)\n }\n\n private _onMenuClick = (e: MouseEvent) => {\n const item = getMenuItemFromEvent(e)\n if (!item || item.hasAttribute('disabled') || !item.hasAttribute('submenu')) return\n\n const level = this._getFocusedLevel(item)\n if (level === undefined) return\n const itemIndex = this._getLevelItems(level).indexOf(item)\n if (itemIndex >= 0) this._toggleSubmenu(level, itemIndex, e.detail === 0)\n }\n\n private _onKeydown = (e: KeyboardEvent) => {\n if (this.disabled || !this.open) return\n\n // overlay 上键盘事件 target 是菜单项而非组件自身,不能限定 e.target === this,\n // 否则鼠标打开、焦点在菜单项时 Escape 无法关闭\n handleMenuKeyboard(this._keyboardDelegate, e)\n }\n\n private _getFocusedLevel(item?: HTMLElement): number | undefined {\n if (!item) return undefined\n const overlay = [...this._overlays.entries()].find(([, value]) => value.overlay.contains(item))\n return overlay?.[0]\n }\n\n private _bindHoversAfterUpdate() {\n requestAnimationFrame(() => this._hoverBinder.bind())\n }\n\n override render() {\n return html`\n <div class=\"dropdown-trigger\" @click=${this._onTriggerClick}>\n <slot name=\"trigger\"></slot>\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-dropdown': WebUiDropdown\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAyCA,IAAM,IAAc,sBAEd,IAAmB,0BAErB,IAAoB,GAWX,IAAN,cAA4B,EAAW;;EAunBtB,aApnB6B,KAAA,OAAA,IACI,KAAA,WAAA,IAC4B,KAAA,aAAA,IACK,KAAA,eAAA,IAExD,KAAA,aAAA,gBAUd,KAAA,UAAA,GAUuB,KAAA,cAAA,CAAC,GAEb,KAAA,4BAAA,IAAI,IAAyB,GAE9B,KAAA,WAAA,EAAkB,CAAC,CAAC,KAAK;GACnD,oBAAoB,KAAK,kBAAkB,kBAAkB;GAC7D,mBAAmB,KAAK;EAC1B,CAAC,GAC8B,KAAA,cAAA,EAAsB,CAAC,CAAC,KAAK,GAMhB,KAAA,UAAA,MAEN,KAAA,qBAAA,EACpC,OAEA,MAAQ,KAAK,SAAS,SAAS,CAAI,KAAK,EAC1C,GACoC,KAAA,mBAAA,EAAuC;GACzE,WAAU,MAAa,EAAU;GACjC,eAAe,GAAW,MAAe,EAAiB,EAAU,SAAS,CAAU;GACvF,UAAS,MAAa;IAGpB,AAFA,EAAU,QAAQ,QAAQ,GAC1B,EAAU,QAAQ,OAAO,GACzB,EAAU,IAAI,QAAQ;GACxB;EACF,CAAC,GACoD,KAAA,iBAAA;GACnD,oBAAoB,KAAK,YAAY;GACrC,gBAAe,MAAS,KAAK,eAAe,CAAK;GACjD,gBAAe,MAAS,KAAK,eAAe,CAAK,CAAC,CAAC,KAAK,YAAY;GACpE,gBAAe,MAAQ,EAAK,aAAa,SAAS;GAClD,cAAa,MAAQ;IACnB,IAAM,IAAQ,KAAK,iBAAiB,CAAI;IACxC,IAAI,MAAU,KAAA,GAAW;IACzB,IAAM,IAAY,KAAK,eAAe,CAAK,CAAC,CAAC,QAAQ,CAAI;IACzD,AAAI,KAAa,KAAG,KAAK,eAAe,GAAO,CAAS;GAC1D;GACA,YAAW,MAAS,KAAK,kBAAkB,IAAQ,CAAC;EACtD,GACgC,KAAA,eAAA,EAAsB,KAAK,gBAAgB,sBAAsB,GACtC,KAAA,oBAAA;GACzD,iBAAgB,MACd,EACE,GACA,CAAC,GAAG,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,iBAAc,CAAO,GACzD,sBACF;GACF,aAAY,MAAQ,KAAK,iBAAiB,CAAI;GAC9C,kBAAiB,MAAS,EAAyB,KAAK,UAAU,IAAI,CAAK,CAAC,EAAE,OAAO;GACrF,gBAAe,MAAQ,EAAK,aAAa,SAAS;GAClD,qBAAoB,MAAQ;IAC1B,IAAM,IAAQ,KAAK,iBAAiB,CAAI;IACxC,IAAI,MAAU,KAAA,GAAW;IACzB,IAAM,IAAY,KAAK,eAAe,CAAK,CAAC,CAAC,QAAQ,CAAI;IACzD,AAAI,KAAa,KAAG,KAAK,eAAe,GAAO,GAAW,EAAI;GAChE;GACA,gBAAe,OACb,KAAK,kBAAkB,CAAK,GACrB,KAAK,eAAe,IAAQ,CAAC,CAAC,CAAC,KAAK,YAAY,IAAQ;GAEjE,yBAAyB;IACvB,AAAI,KAAK,YAAY,SAAS,IAAG,KAAK,kBAAkB,KAAK,YAAY,MAAM,IAC1E,KAAK,UAAU,EAAI;GAC1B;EACF,GACmC,KAAA,kBAAA,IAAI,EAAqB,GAE7B,KAAA,uBAAA,IAYQ,KAAA,+BAAA,IACV,KAAA,qBAAA,IAC0B,KAAA,qCAAA,IAAI,IAAI,GAKvB,KAAA,uBAAA,IAAI,uBAAuB;GACjE,AAAI,KAAK,QAAM,KAAK,kBAAkB;EACxC,CAAC,GAge0B,KAAA,mBAAA,MAAsB;GAC3C,UAAK,UACT;QAAI,KAAK,MACP,KAAK,UAAU,EAAI;SACd;KAML,IAAM,IAAe,EAAM,WAAW;KACtC,KAAK,UAAU,GAAc,IAAM,CAAC,CAAY;IAClD;;EACF,GAE2B,KAAA,mBAAA,MAAkB;GACvC,AAAC,KAAK,UAAU,MAAK,mBAAmB,QAAQ,MAChD,KAAK,mBAAmB,SAAS,CAAC,KACtC,KAAK,UAAU,EAAI;EACrB,GAEwB,KAAA,gBAAA,MAAkB;GACxC,IAAM,IAAO,EAAqB,CAAC;GACnC,IAAI,CAAC,KAAQ,EAAK,aAAa,UAAU,KAAK,CAAC,EAAK,aAAa,SAAS,GAAG;GAE7E,IAAM,IAAQ,KAAK,iBAAiB,CAAI;GACxC,IAAI,MAAU,KAAA,GAAW;GACzB,IAAM,IAAY,KAAK,eAAe,CAAK,CAAC,CAAC,QAAQ,CAAI;GACzD,AAAI,KAAa,KAAG,KAAK,eAAe,GAAO,GAAW,EAAE,WAAW,CAAC;EAC1E,GAEsB,KAAA,cAAA,MAAqB;GACrC,MAAK,YAAa,KAAK,QAI3B,EAAmB,KAAK,mBAAmB,CAAC;EAC9C;;;EA5nByB,KAAA,SAAA,EAAU,CAAK;;CASxC,IACI,YAAuB;EACzB,OAAO,KAAK;CACd;CACA,IAAI,UAAU,GAAW;EACvB,KAAK,aAAa,EAAiB,GAAG,GAAqB,cAAc;CAC3E;CAIA,IACI,SAAiB;EACnB,OAAO,KAAK;CACd;CACA,IAAI,OAAO,GAAW;EACpB,KAAK,UAAU,EAAgB,GAAG,GAAG,KAAK,CAAC;CAC7C;CAgGA,IAAI,SAAkB;EACpB,OAAO,KAAK,QAAQ,KAAK,YAAY,SAAS;CAChD;CAEA,oBAA6B;EAa3B,AAZA,MAAM,kBAAkB,GACxB,KAAK,SAAS,OAAO,GACrB,KAAK,iBAAiB,WAAW,KAAK,UAAU,GAChD,SAAS,iBAAiB,SAAS,KAAK,eAAe,GACvD,KAAK,qBAAqB,QAAQ,MAAM;GAAE,WAAW;GAAM,SAAS;EAAK,CAAC,GAQtE,KAAK,QAAQ,KAAK,cAAY,KAAK,iBAAiB,KAAK,iBAAiB,CAAC;CACjF;CAEA,eAAwB;EAGtB,KAAK,YACD,cAA+B,wBAAsB,CAAC,EACtD,iBAAiB,oBAAoB,KAAK,cAAc,CAAC;CAC/D;CAEA,uBAAgC;EAU9B,AATA,MAAM,qBAAqB,GAC3B,KAAK,qBAAqB,WAAW,GACrC,KAAK,oBAAoB,WAAW,KAAK,UAAU,GACnD,SAAS,oBAAoB,SAAS,KAAK,eAAe,GAC1D,KAAK,mBAAmB,QAAQ,GAChC,KAAK,aAAa,QAAQ,GAE1B,KAAK,SAAS,QAAQ,GACtB,KAAK,YAAY,QAAQ,GACzB,KAAK,mBAAmB;CAC1B;CAEA,QAA2B,GAA+B;EAsCxD,AApCA,KAAK,iBAAiB,GAElB,EAAQ,IAAI,MAAM,MAChB,KAAK,OACP,KAAK,iBAAiB,EAAI,KAE1B,KAAK,SAAS,WAAW,GACzB,KAAK,gBAAgB,GAQrB,KAAK,+BAA+B,IAKpC,KAAK,SAAS,QAAQ,GACtB,KAAK,UAAU,MACf,KAAU,wBAAwB,IAEhC,KAAK,gBAAgB,QAAQ,KAC/B,EAAwB,MAAM,KAAK,IAAI,IAGvC,EAAQ,IAAI,cAAc,KAAG,KAAK,gBAAgB,IAClD,EAAQ,IAAI,WAAW,KAAK,EAAQ,IAAI,QAAQ,KAAK,EAAQ,IAAI,YAAY,MAC/E,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE,IAAI,OAAO;GAChC,WAAW,KAAK;GAChB,QAAQ,KAAK;GACb,YAAY,KAAK;EACnB,CAAC,GAEH,KAAK,aAAa,KAAK;CACzB;CAMA,mBAAoC;EAClC,IAAM,IAAS,SAAS;EACxB,OAAO,MAAW,QAAQ,MAAW,SAAS,QAAQ,MAAW,SAAS;CAC5E;CAMA,iBAAyB,GAAoB;EAiB3C,AAhBA,KAAK,wBAAwB,SAAS,yBAAyB,cAAc,SAAS,gBAAgB,KAAA,GACtG,KAAK,mBAAmB,IAAI,GAC5B,KAAK,gBAAgB,GACrB,KAAK,wBAAwB,GAC7B,KAAK,SAAS,WAAW,GACzB,KAAK,SAAS,oBAAoB;GAShC,AARA,KAAK,eAAe,GAAG,KAAK,oBAAoB,GAChD,KAAK,uBAAuB,IACxB,KACF,EAAc,EAAyB,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,EACzE,sBAAsB,KAAK,6BAC7B,CAAC,GAGH,KAAK,iBAAiB;EACxB,CAAC,GACD,KAAK,uBAAuB;CAC9B;CAMA,WAAW;EACT,KAAK,UAAU,IAAM,IAAO,EAAI;CAClC;CAEA,UAAkB,GAAoB,GAAmB,GAA+B;EAClF,KAAK,YAAY,KAAK,SAC1B,KAAK,mBAAmB,IAAI,GAC5B,KAAK,uBAAuB,GAC5B,KAAK,+BAA+B,GAChC,KAAU,KAAK,gBAAgB,KAAK,GAGxC,KAAK,WAAW,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE,WAAW,IAAI,GACtD,KAAK,OAAO;CACd;CAGA,WAAW;EACT,KAAK,UAAU,EAAK;CACtB;CAEA,UAAkB,GAAmB;EAC9B,AAEL,KAAK,UADD,KAAU,KAAK,gBAAgB,KAAK,GAC5B;CACd;CAEA,oBAA4B;EAG1B,AAFA,KAAK,kBAAkB,GAAG,EAAI,GAC9B,KAAK,iBAAiB,WAAW,GACjC,KAAK,iBAAiB;CACxB;CAEA,qBAA6B;EAG3B,AAFA,KAAK,kBAAkB,GACvB,KAAK,mBAAmB,GACxB,KAAK,YAAY;CACnB;CAEA,MAAc,0BAA0B;EACtC,IAAM,IAAU,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE;EACnC,OAAa,MAAM,EAAoB,CAAO,MAC9C,MAAK,QAAS,KAAK,gBAEvB,KAAK,mBAAmB,GACxB,KAAK,qBAAqB,MAAM,GAChC,KAAK,sBAAsB,KAAA;CAC7B;CAEA,0BAAkC;EAChC,EAAuB,MAAM,GAAG,EAAY,QAAQ;CACtD;CAEA,eAAuB,GAAe,GAAmB,IAAY,IAAO;EAC1E,IAAI,KAAK,YAAY,OAAW,GAC9B;EAMF,AAHA,KAAK,kBAAkB,IAAQ,CAAC,GAEhC,KAAK,cAAc,CAAC,GAAG,KAAK,YAAY,MAAM,GAAG,CAAK,GAAG,CAAS,GAClE,KAAK,iBAAiB;EAEtB,IAAM,IAAO,KAAK,eAAe,CAAK,CAAC,CAAC;EASxC,AARI,GAAM,aAAa,SAAS,KAC9B,KAAK,SAAS,oBAAoB;GAC5B,AAAC,KAAK,QAAQ,KAAK,YAAY,OAAW,MAC7B,KAAK,eAAe,IAAQ,GAAG,GAAW,CACtD,KAAU,KAAK,iBAAiB,IAAQ,GAAG,CAAI;EACtD,CAAC,GAGH,KAAK,uBAAuB;CAC9B;CAEA,kBAA0B,GAAe,IAAY,IAAO;EAC1D,KAAK,IAAI,IAAK,KAAK,YAAY,QAAQ,KAAM,GAAO,KAAM;GACxD,IAAM,IAAU,KAAK,UAAU,IAAI,CAAE,GAC/B,IAAa,KAAK,eAAe,IAAK,CAAC,CAAC,CAAC,KAAK,YAAY,IAAK;GAChE,MAED,CAAC,KAAc,KACjB,KAAK,mBAAmB,GAAI,CAAU,GACtC,KAAK,gBAAgB,CAAE,MAEvB,KAAK,UAAU,OAAO,CAAE,GACxB,KAAK,iBAAiB,WAAW,GAAY,CAAO;EAExD;EACA,IAAM,IAAiB,KAAK,YAAY,MAAM,GAAG,IAAQ,CAAC;EAI1D,AAHI,EAAe,WAAW,KAAK,YAAY,WAC7C,KAAK,cAAc,IAErB,KAAK,iBAAiB;CACxB;CAEA,mBAA2B;EAOzB,AANA,KAAK,iBAAiB,sBAAsB,CAAC,CAAC,SAAQ,MAAQ;GAC5D,EAAK,gBAAgB,QAAQ;EAC/B,CAAC,GACD,KAAK,UAAU,SAAS,GAAG,MAAU;GACnC,KAAK,eAAe,CAAK,CAAC,CAAC,SAAQ,MAAQ,EAAK,gBAAgB,QAAQ,CAAC;EAC3E,CAAC,GACD,KAAK,YAAY,SAAS,GAAW,MAAU;GAC7C,IAAM,IAAO,KAAK,eAAe,CAAK,CAAC,CAAC;GACxC,AAAI,GAAM,QAAQ,sBAAsB,KACtC,EAAK,aAAa,UAAU,EAAE;EAElC,CAAC;CACH;CAEA,eAAuB,GAA8B;EACnD,IAAM,IAAU,KAAK,UAAU,IAAI,CAAK,CAAC,EAAE;EAC3C,OAAO,IAAU,EAAgB,CAAO,IAAI,CAAC;CAC/C;CAEA,kBAA0B;EACxB,IAAM,IAAU,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE;EAClC,MAGL,EAA0B,MAAM,GAAS,KAAK,oBAAoB,CAAgB,GAClF,EAAsB,MAAM,GAAS,KAAK,oBAAoB,CAAgB;CAChF;CAEA,oBAA4B;EACtB,KAAK,uBACT,KAAK,qBAAqB,IAC1B,4BAA4B;GAC1B,KAAK,qBAAqB,IACrB,KAAK,QACV,KAAK,qBAAqB;EAC5B,CAAC;CACH;CAKA,uBAA+B;EAC7B,IAAM,IAAgB,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE;EAG7C,IADA,KAAK,wBAAwB,GACzB,GAAe;GAIjB,AAHA,EAA0B,MAAM,GAAe,KAAK,oBAAoB,CAAgB,GAGxF,EAAuB,GAAe,GAAG,EAAY,QAAQ;GAE7D,IAAM,IAAmB,EAAwB,GAAe,CAAgB;GAEhF,AADA,EAAsB,MAAM,GAAe,KAAK,oBAAoB,CAAgB,GACpF,EAAwB,GAAe,CAAgB;EACzD;EACA,KAAK,IAAM,CAAC,GAAO,MAAY,KAAK,WAAW;GAC7C,IAAI,MAAU,GAAG;GACjB,IAAM,IAAa,KAAK,eAAe,IAAQ,CAAC,CAAC,CAAC,KAAK,YAAY,IAAQ;GAE3E,AADI,KAAY,EAAiB,GAAY,EAAQ,OAAO,GAC5D,EAAuB,EAAQ,SAAS,GAAG,EAAY,QAAQ;EACjE;EACA,KAAK,aAAa,KAAK;CACzB;CAEA,qBAA6B;EAC3B,IAAM,IAAU,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE;EACvC,AAAI,KAAS,EAA6B,MAAM,GAAS,KAAK,kBAAkB;CAClF;CAEA,iBAAyB,GAAe,GAA0B;EAChE,IAAM,IAAU,KAAK,UAAU,IAAI,CAAK,CAAC,EAAE;EAC3C,AAAI,KAAS,EAAiB,GAAa,CAAO;CACpD;CAEA,mBAA2B,GAAe,GAA0B;EAClE,IAAM,IAAU,KAAK,UAAU,IAAI,CAAK,CAAC,EAAE;EAC3C,IAAI,CAAC,GAAS;EACd,IAAM,IAAc,IAAQ,GACtB,IAAc,KAAc,KAAK,eAAe,CAAW,CAAC,CAAC,KAAK,YAAY;EACpF,AAAI,KAAa,EAAiB,GAAS,CAAW;CACxD;CAmBA,WAAmB,GAAuC;EAExD,AADA,KAAK,SAAS,QAAQ,GACtB,KAAK,UAAU,KAAK,SAAS,MAAM,CAAK;EACxC,KAAK,IAAM,CAAC,GAAO,MAAQ,KAAK,WAC1B,MAAU,MACd,EAAI,SAAS,KAAK,QAAQ,MAAM,EAAI,OAAO;EAS7C,KAAK,IAAM,KAAa,KAAK,iBAAiB,QAAQ,GACpD,EAAU,SAAS,KAAK,QAAQ,MAAM,EAAU,OAAO;EAEzD,OAAO,KAAK;CACd;CAGA,oBAA+C;EAC7C,OAAO,KAAK,WAAW,KAAK,WAAW,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE,WAAW,IAAI;CAC/E;CAEA,eAAuB,GAAe,IAAY,IAAO,GAAoC;EAC3F,IAAM,IAAiB,IAAc,KAAK,iBAAiB,KAAK,CAAW,IAAI,KAAA;EAC/E,IAAI,GAMF,OAJA,EAAe,SAAS,KAAK,kBAAkB,CAAC,CAAC,MAAM,EAAe,OAAO,GAC7E,KAAK,UAAU,IAAI,GAAO,CAAc,GACxC,EAAe,IAAI,KAAK,GACxB,EAAoB,EAAe,SAAS,EAAE,aAAU,CAAC,GAClD;EAET,IAAM,IAAW,KAAK,UAAU,IAAI,CAAK;EAQzC,OAPI,KACF,KAAK,kBAAkB,GACvB,EAAS,IAAI,KAAK,GAClB,EAAoB,EAAS,SAAS,EAAE,aAAU,CAAC,GAC5C,OAET,KAAK,cAAc,GAAO,CAAS,GAC5B;CACT;CAEA,cAAsB,GAAe,IAAY,IAAO;EACtD,IAAM,EAAE,OAAO,GAAS,eAAY,EAAwB,oBAAoB,IAAI;EAMpF,AALA,EAAQ,aAAa,QAAQ,MAAM,GAE/B,MAAU,MAAG,EAAQ,KAAK,qBAAqB,EAAE,MACrD,EAAQ,QAAQ,QAAQ,OAAO,CAAK,GACpC,EAAQ,iBAAiB,SAAS,KAAK,YAAY,GACnD,EAAQ,iBAAiB,WAAW,KAAK,UAAU;EAEnD,IAAM,IAAS,MAAU,IAAI,KAAK,oBAAoB,IAAI,KAAK,yBAAyB,IAAQ,CAAC;EAEjG,IAAI,GAAQ;GACV,IAAM,IAAO,EAAc,CAAC,CAAC,KAAK;IAChC;IACA;IACA,WAAW,MAAU,IAAI,KAAK,YAAY;IAC1C,QAAQ,MAAU,IAAI,KAAK,SAAS;IAEpC,gBAAgB;IAChB,YAAY,MAAU,KAAI,KAAK;IAC/B,UAAU;GACZ,CAAC;GACD,IAAI,MAAU,GAIZ,AAFA,KAAK,WAAW,CAAO,GACvB,KAAK,UAAU,IAAI,GAAO;IAAE,KAAK;IAAM;IAAS;GAAQ,CAAC,GACzD,KAAK,gBAAgB;QAChB;IAML,IAAM,IAAS,KAAK,kBAAkB,CAAC,CAAC,MAAM,CAAO;IACrD,KAAK,UAAU,IAAI,GAAO;KAAE,KAAK;KAAM;KAAS;KAAS;IAAO,CAAC;GACnE;GAIA,AAFA,KAAK,qBAAqB,QAAQ,GAAS;IAAE,WAAW;IAAM,SAAS;GAAK,CAAC,GAC7E,EAAK,KAAK,GACV,EAAoB,GAAS,EAAE,aAAU,CAAC;EAC5C;CACF;CAEA,gBAAwB,GAAe;EACrC,IAAM,IAAU,KAAK,UAAU,IAAI,CAAK;EAaxC,AAZI,MAAU,MAEZ,KAAK,SAAS,QAAQ,GACtB,KAAK,UAAU,OAGjB,GAAS,QAAQ,QAAQ,GACzB,GAAS,QAAQ,oBAAoB,SAAS,KAAK,YAAY,GAC/D,GAAS,QAAQ,oBAAoB,WAAW,KAAK,UAAU,GAC/D,GAAS,QAAQ,OAAO,GACxB,GAAS,IAAI,QAAQ,GACrB,KAAK,UAAU,OAAO,CAAK,GAC3B,KAAK,4BAA4B;CACnC;CAGA,8BAAsC;EAEpC,AADA,KAAK,qBAAqB,WAAW,GACrC,KAAK,qBAAqB,QAAQ,MAAM;GAAE,WAAW;GAAM,SAAS;EAAK,CAAC;EAC1E,KAAK,IAAM,KAAW,KAAK,UAAU,OAAO,GAC1C,KAAK,qBAAqB,QAAQ,EAAQ,SAAS;GAAE,WAAW;GAAM,SAAS;EAAK,CAAC;CAEzF;CAEA,cAAsB;EAQpB,AADA,KAAK,SAAS,QAAQ,GACtB,KAAK,UAAU;EACf,KAAK,IAAM,KAAS,KAAK,UAAU,KAAK,GACtC,KAAK,gBAAgB,CAAK;CAE9B;CAEA,gBAAwB,IAAS,KAAK,MAAM;EAC1C,KAAK,YAAY,KAAK,KAAU,CAAC,KAAK,YAAY;CACpD;CAEA,sBAAkD;EAEhD,IAAM,KADO,KAAK,YAAY,cAA+B,wBAAsB,EAAA,EAClE,iBAAiB,CAAC,CAAC;EACpC,OAAO,aAAc,cAAc,IAAK;CAC1C;CAOA,mBAA2B;EACzB,IAAM,IAAU,KAAK,oBAAoB;EACzC,IAAI,CAAC,GAAS;EAEd,AADA,EAAQ,aAAa,iBAAiB,MAAM,GAC5C,EAAQ,aAAa,iBAAiB,OAAO,KAAK,IAAI,CAAC;EACvD,IAAM,IAAc,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE;EAC3C,AAAI,GAAa,KAAI,EAAQ,aAAa,iBAAiB,EAAY,EAAE,IACpE,EAAQ,gBAAgB,eAAe;CAC9C;CAEA,yBAAiC,GAAmC;EAClE,IAAM,IAAM,KAAK,YAAY;EAC7B,IAAI,MAAQ,KAAA,GAAW,OAAO;EAE9B,IAAM,IADQ,KAAK,eAAe,CACrB,CAAA,CAAM;EAEnB,OADK,IACE,EAAK,YAAY,cAAc,aAAa,KAAK,IADtC;CAEpB;CAyCA,iBAAyB,GAAwC;EAC1D,OAEL,OADgB,CAAC,GAAG,KAAK,UAAU,QAAQ,CAAC,CAAC,CAAC,MAAM,GAAG,OAAW,EAAM,QAAQ,SAAS,CAAI,CACtF,CAAA,GAAU;CACnB;CAEA,yBAAiC;EAC/B,4BAA4B,KAAK,aAAa,KAAK,CAAC;CACtD;CAEA,SAAkB;EAChB,OAAO,CAAI;6CAC8B,KAAK,gBAAgB;;;;CAIhE;AAKF;AAjpBG,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;CAAM,WAAW;AAAc,CAAC,CAAA,GAAA,EAAA,WAAA,cAAA,KAAA,CAAA,GACnE,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;CAAM,WAAW;AAAiB,CAAC,CAAA,GAAA,EAAA,WAAA,gBAAA,KAAA,CAAA,GAItE,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,aAAA,IAAA,GAUxC,EAAA,CAAA,EAAS,EAAE,MAAM,OAAO,CAAC,CAAA,GAAA,EAAA,WAAA,UAAA,IAAA,GAQzB,EAAA,CAAA,EAAM,CAAA,GAAA,EAAA,WAAA,eAAA,KAAA,CAAA,GA7BR,IAAA,EAAA,CAAA,EAAc,iBAAiB,CAAA,GAAA,CAAA"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/dropdown/index.ts"],"sourcesContent":["import type { Placement } from '@floating-ui/dom'\nimport { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { UserChangeController } from '@/shared/events/user-change'\nimport {\n createClosingSubmenuStack,\n createMenuHoverBinder,\n createMenuOutsideClickGuard,\n getEnabledMenuLevelItems,\n getFocusedMenuItemFromPanels,\n handleMenuKeyboard,\n type MenuHoverDelegate,\n type MenuKeyboardDelegate\n} from '@/shared/menu-behavior'\nimport { createMenuPortalOverlay } from '@/shared/menu-portal/menu-portal'\nimport {\n captureFrameworkAnchors,\n focusMenuItem,\n getMenuChildren,\n getMenuItemFromEvent,\n hideNestedMenuChildren,\n moveMenuChildren,\n orderManagedMenuItems,\n reconcileManagedMenuItems,\n restoreFrameworkAnchors,\n returnManagedMenuItemsToSlot,\n type MenuItemAnchors\n} from '@/shared/menu-portal/menu-tree'\nimport { normalizeLiteral, normalizeNumber } from '@/shared/normalize'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { defineOpenOverlay, type OpenOverlayHandle } from '@/shared/overlay/open-overlay'\nimport { defineOverlay, type OverlayApi } from '@/shared/overlay/overlay'\nimport { FLOATING_PLACEMENTS } from '@/shared/overlay/placement-props'\nimport { hideOverlayPresence, showOverlayPresence } from '@/shared/overlay/presence'\nimport { defineScrollLockLease } from '@/shared/scroll-lock/scroll-lock'\n\nimport style from './style.css?inline'\n\nexport type { Placement }\n\nconst SLOT_PREFIX = 'web-ui-menu-level-'\n// 一级 menu item 的模板位 marker(机制与 context-menu 共用,见 menu-tree 的托管项锚说明)。\nconst MENU_ITEM_MARKER = 'wui-dropdown-menu-item'\n\nlet dropdownIdCounter = 0\n\ninterface MenuOverlay {\n api: OverlayApi\n overlay: HTMLElement\n content: HTMLElement\n /** 该层为 submenu 时的会话 handle;根层 handle 由组件单独持有(见 `_handle`)。 */\n handle?: OpenOverlayHandle\n}\n\n@customElement('web-ui-dropdown')\nexport class WebUiDropdown extends LitElement {\n static override styles = unsafeCSS(style)\n\n @property({ type: Boolean, reflect: true }) open = false\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true, attribute: 'match-width' }) matchWidth = false\n @property({ type: Boolean, reflect: true, attribute: 'no-scroll-lock' }) noScrollLock = false\n\n private _placement: Placement = 'bottom-start'\n\n @property({ type: String, reflect: true })\n get placement(): Placement {\n return this._placement\n }\n set placement(v: string) {\n this._placement = normalizeLiteral(v, FLOATING_PLACEMENTS, 'bottom-start')\n }\n\n private _offset = 4\n\n @property({ type: Number })\n get offset(): number {\n return this._offset\n }\n set offset(v: number) {\n this._offset = normalizeNumber(v, 0, 100, 4)\n }\n\n @state() private _activePath: number[] = []\n\n private readonly _overlays = new Map<number, MenuOverlay>()\n /** 实例作用域的帧事务入口(合并了原 lifecycle)。 */\n private readonly _overlay = defineOpenOverlay().make({\n requestClose: () => this._keyboardDelegate.closeDeepestOrAll(),\n isConnected: () => this.isConnected\n })\n private readonly _scrollLock = defineScrollLockLease().make()\n /**\n * 当前 Escape 候选的会话 handle(level 0)。panel 要等一帧才建好,而 Escape 可能在这之前\n * 到达:先以宿主为 panel claim 一次,panel 就绪后 release 旧会话、再以真实 panel 重新 claim。\n * submenu 的 handle 存在各自 `MenuOverlay.handle` 上(经 adopt 进本 handle 子树)。\n */\n private _handle: OpenOverlayHandle | null = null\n // 行为层(hover / outside-click / 键盘 / submenu 收尾)由 shared/menu-behavior 驱动\n private readonly _outsideClickGuard = createMenuOutsideClickGuard(\n this,\n // 根 handle 的 contains 递归覆盖全部已 adopt 的子层,一次查询替代逐层判定。\n node => this._handle?.contains(node) ?? false\n )\n private readonly _closingSubmenus = createClosingSubmenuStack<MenuOverlay>({\n getPanel: container => container.overlay,\n restoreItems: (container, parentItem) => moveMenuChildren(container.content, parentItem),\n dispose: container => {\n container.handle?.release()\n container.overlay.remove()\n container.api.dispose()\n }\n })\n private readonly _hoverDelegate: MenuHoverDelegate = {\n getOpenDepth: () => this._activePath.length,\n getLevelItems: level => this._getLevelItems(level),\n getActiveItem: level => this._getLevelItems(level)[this._activePath[level]],\n isSubmenuItem: item => item.hasAttribute('submenu'),\n openSubmenu: item => {\n const level = this._getFocusedLevel(item)\n if (level === undefined) return\n const itemIndex = this._getLevelItems(level).indexOf(item)\n if (itemIndex >= 0) this._toggleSubmenu(level, itemIndex)\n },\n closeFrom: level => this._closeSubmenuFrom(level + 1)\n }\n private readonly _hoverBinder = createMenuHoverBinder(this._hoverDelegate, 'web-ui-dropdown-item')\n private readonly _keyboardDelegate: MenuKeyboardDelegate = {\n getFocusedItem: event =>\n getFocusedMenuItemFromPanels(\n event,\n [...this._overlays.values()].map(({ overlay }) => overlay),\n 'web-ui-dropdown-item'\n ),\n getLevelOf: item => this._getFocusedLevel(item),\n getEnabledItems: level => getEnabledMenuLevelItems(this._overlays.get(level)?.content),\n isSubmenuItem: item => item.hasAttribute('submenu'),\n openSubmenuInstant: item => {\n const level = this._getFocusedLevel(item)\n if (level === undefined) return\n const itemIndex = this._getLevelItems(level).indexOf(item)\n if (itemIndex >= 0) this._toggleSubmenu(level, itemIndex, true)\n },\n closeToParent: level => {\n this._closeSubmenuFrom(level)\n return this._getLevelItems(level - 1)[this._activePath[level - 1]]\n },\n closeDeepestOrAll: () => {\n if (this._activePath.length > 0) this._closeSubmenuFrom(this._activePath.length)\n else this._closeAll(true)\n }\n }\n private readonly _userOpenChange = new UserChangeController()\n private _restoreFocusTarget?: HTMLElement\n private _shouldOpenInstantly = true\n /**\n * 本次打开是否抑制首项的 `:focus-visible` accent 视觉(口径与 context-menu 一致)。\n *\n * 默认 `true`(抑制),只有键盘激活触发的打开才置 `false`:首项的焦点是组件程序化\n * 落的,而各引擎对「程序化 focus 是否算 focus-visible」判定不同 —— Chromium 走启发式\n * 通常不匹配,WKWebView 则会匹配(用户在 Interweave 桌面 app 实测到蓝底白字),\n * 于是同一段代码只在 WebKit 侧暴露出缺陷。取默认抑制而非默认保留,是因为冷启动的\n * `open` 属性路径拿不到任何手势模态信息,抑制是唯一确定安全的一侧;误伤键盘用户的\n * 代价有界:`handleMenuKeyboard` 首次方向键即以无参 `focusMenuItem` 重新落焦,\n * `menu-tree` 的 `toggleAttribute` 无条件清掉本属性,accent 当场回来。\n */\n private _suppressInitialFocusVisible = true\n private _menuSyncScheduled = false\n private readonly _level0ItemAnchors: MenuItemAnchors = new Map()\n // 打开期实时渲染:框架(Vue/React 条件渲染)可能在 menu 打开期间持续插入 menu item。\n // 观察宿主子树(新增项可能被框架 wrapper 包裹)与各层级 panel content 的子树\n // (打开中的 submenu 父项已被迁入 content,其新增子项只在这里可见),下一帧统一\n // 把新子树迁入对应层级 panel;panel 尺寸变化由 defineOverlay 的 autoUpdate 跟进重定位。\n private readonly _menuContentObserver = new MutationObserver(() => {\n if (this.open) this._scheduleMenuSync()\n })\n\n get isOpen(): boolean {\n return this.open || this._activePath.length > 0\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this._overlay.resume()\n this.addEventListener('keydown', this._onKeydown)\n document.addEventListener('click', this._onClickOutside)\n this._menuContentObserver.observe(this, { childList: true, subtree: true })\n /*\n * 重挂载对账:`disconnectedCallback` 会拆掉 panel、把 menu item 迁回宿主,但 `open` 是公开\n * prop,不因卸载而改写。Lit 在 detach 期间不记 `changedProperties`,重连后 `updated()`\n * 不再命中 `changed.has('open')`,panel 于是永久缺席:宿主仍反射 `open` 与\n * `aria-expanded`,Escape 和 outside click 却无层可关。首连时 `open` 在\n * `changedProperties` 里、由 `updated()` 负责,`hasUpdated` 就是用来只认重连的。\n */\n if (this.open && this.hasUpdated) this._syncOpenOverlay(this._isFocusOrphaned())\n }\n\n override firstUpdated() {\n // trigger slot 内容增删不触发宿主响应式更新:显式请求一轮渲染,\n // 由 updated() 的 ARIA 回写覆盖晚到的 trigger 元素。\n this.shadowRoot\n ?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]')\n ?.addEventListener('slotchange', () => this.requestUpdate())\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._menuContentObserver.disconnect()\n this.removeEventListener('keydown', this._onKeydown)\n document.removeEventListener('click', this._onClickOutside)\n this._outsideClickGuard.dispose()\n this._hoverBinder.dispose()\n // 停止帧调度;登记由 _cleanupClosedMenu → _disposeAll 撤销。\n this._overlay.suspend()\n this._scrollLock.release()\n this._cleanupClosedMenu()\n }\n\n protected override updated(changed: Map<string, unknown>) {\n // ARIA 回写不依赖 open 分支,任何渲染后都保持与宿主状态同步。\n this._syncTriggerAria()\n\n if (changed.has('open')) {\n if (this.open) {\n this._syncOpenOverlay(true)\n } else {\n this._overlay.invalidate()\n this._syncScrollLock()\n /*\n * 复位打开模态:`open` 真正翻回 false 才是「上一次打开会话结束」的唯一信号,\n * 下一会话若不经 `_openMenu()`(宿主直接翻 `open` 属性)就从默认口径重新起算,\n * 不会把上一次键盘打开留下的 `false` 带进这次鼠标驱动的声明式打开。\n * 刻意不复位在 `disconnectedCallback`/`_cleanupClosedMenu`:重挂载对账(:188)\n * 期间 `open` 始终为 true,那次重取焦点属于同一会话,应保留原模态。\n */\n this._suppressInitialFocusVisible = true\n /*\n * release ⟺ 关闭:新模块里「已登记」就等于「参与仲裁」,不再由 host.isOpen()\n * 兜底过滤,所以必须在这里撤销,不能等到退场动画结束。\n */\n this._handle?.release()\n this._handle = null\n void this._closeRootAfterPresence()\n }\n if (this._userOpenChange.consume()) {\n dispatchOpenChangeEvent(this, this.open)\n }\n }\n if (changed.has('noScrollLock')) this._syncScrollLock()\n if (changed.has('placement') || changed.has('offset') || changed.has('matchWidth')) {\n this._overlays.get(0)?.api.update({\n placement: this.placement,\n offset: this.offset,\n matchWidth: this.matchWidth\n })\n }\n this._hoverBinder.bind()\n }\n\n /**\n * 焦点是否已无归属:被 detach 打掉的焦点会落回 `body`,用户主动点到别处时不会。\n * 重挂载对账只在前者才把焦点夺回 menu,否则等于从消费者手里抢焦点。\n */\n private _isFocusOrphaned(): boolean {\n const active = document.activeElement\n return active === null || active === document.body || active === document.documentElement\n }\n\n /**\n * 打开态的唯一对账入口:panel、滚动锁、outside click、hover 与 menu 焦点都从这里收敛,\n * `updated()` 的打开分支与重连对账共用它,两条路径必须落到同一个状态。\n */\n private _syncOpenOverlay(takeFocus: boolean) {\n this._restoreFocusTarget ??= document.activeElement instanceof HTMLElement ? document.activeElement : undefined\n this._outsideClickGuard.arm()\n this._syncScrollLock()\n this._hideAllSubmenuChildren()\n this._overlay.invalidate()\n this._overlay.scheduleFrame(() => {\n this._ensureOverlay(0, this._shouldOpenInstantly)\n this._shouldOpenInstantly = true\n if (takeFocus) {\n focusMenuItem(getEnabledMenuLevelItems(this._overlays.get(0)?.content)[0], {\n suppressFocusVisible: this._suppressInitialFocusVisible\n })\n }\n // panel id 在 overlay 构建后才有:回写 aria-controls 指向。\n this._syncTriggerAria()\n })\n this._bindHoversAfterUpdate()\n }\n\n /*\n * 命令式打开,不派发 open-change。命令式入口没有手势模态信息,按与 context-menu 的\n * `openAt()` 同一口径抑制首项 accent。\n */\n openMenu() {\n this._openMenu(true, false, true)\n }\n\n private _openMenu(isInstant: boolean, fromUser: boolean, suppressFocusVisible: boolean) {\n if (this.disabled || this.open) return\n this._outsideClickGuard.arm()\n this._shouldOpenInstantly = isInstant\n this._suppressInitialFocusVisible = suppressFocusVisible\n if (fromUser) this._userOpenChange.mark()\n // panel 要等一帧才建好,而 Escape 可能在这之前到达:先用宿主当 panel claim 一次;\n // panel 就绪后 _buildOverlay 会 release 这个占位会话,再以真实 panel 重新 claim。\n this._claimRoot(this._overlays.get(0)?.overlay ?? this)\n this.open = true\n }\n\n // 命令式关闭全部层级,不派发 open-change。\n closeAll() {\n this._closeAll(false)\n }\n\n private _closeAll(fromUser: boolean) {\n if (!this.open) return\n if (fromUser) this._userOpenChange.mark()\n this.open = false\n }\n\n private _closeAllSubmenus() {\n this._closeSubmenuFrom(1, true)\n this._closingSubmenus.restoreAll()\n this._syncActiveAttrs()\n }\n\n private _cleanupClosedMenu() {\n this._closeAllSubmenus()\n this._returnLevel0Items()\n this._disposeAll()\n }\n\n private async _closeRootAfterPresence() {\n const overlay = this._overlays.get(0)?.overlay\n if (overlay && !(await hideOverlayPresence(overlay))) return\n if (this.open || !this.isConnected) return\n\n this._cleanupClosedMenu()\n this._restoreFocusTarget?.focus()\n this._restoreFocusTarget = undefined\n }\n\n private _hideAllSubmenuChildren() {\n hideNestedMenuChildren(this, `${SLOT_PREFIX}-hidden`)\n }\n\n private _toggleSubmenu(level: number, itemIndex: number, isInstant = false) {\n if (this._activePath[level] === itemIndex) {\n return\n }\n\n this._closeSubmenuFrom(level + 1)\n\n this._activePath = [...this._activePath.slice(0, level), itemIndex]\n this._syncActiveAttrs()\n\n const item = this._getLevelItems(level)[itemIndex]\n if (item?.hasAttribute('submenu')) {\n this._overlay.scheduleFrame(() => {\n if (!this.open || this._activePath[level] !== itemIndex) return\n const restored = this._ensureOverlay(level + 1, isInstant, item)\n if (!restored) this._populateOverlay(level + 1, item)\n })\n }\n\n this._bindHoversAfterUpdate()\n }\n\n private _closeSubmenuFrom(level: number, isInstant = false) {\n for (let lv = this._activePath.length; lv >= level; lv--) {\n const overlay = this._overlays.get(lv)\n const parentItem = this._getLevelItems(lv - 1)[this._activePath[lv - 1]]\n if (!overlay) continue\n\n if (!parentItem || isInstant) {\n this._depopulateOverlay(lv, parentItem)\n this._disposeOverlay(lv)\n } else {\n this._overlays.delete(lv)\n this._closingSubmenus.closeAsync(parentItem, overlay)\n }\n }\n const nextActivePath = this._activePath.slice(0, level - 1)\n if (nextActivePath.length !== this._activePath.length) {\n this._activePath = nextActivePath\n }\n this._syncActiveAttrs()\n }\n\n private _syncActiveAttrs() {\n this.querySelectorAll('web-ui-dropdown-item').forEach(item => {\n item.removeAttribute('active')\n })\n this._overlays.forEach((_, level) => {\n this._getLevelItems(level).forEach(item => item.removeAttribute('active'))\n })\n this._activePath.forEach((itemIndex, level) => {\n const item = this._getLevelItems(level)[itemIndex]\n if (item?.matches('web-ui-dropdown-item')) {\n item.setAttribute('active', '')\n }\n })\n }\n\n private _getLevelItems(level: number): HTMLElement[] {\n const content = this._overlays.get(level)?.content\n return content ? getMenuChildren(content) : []\n }\n\n private _populateLevel0() {\n const content = this._overlays.get(0)?.content\n if (!content) return\n // 初次打开即建立 marker 锚:宿主此后始终持有模板序骨架,打开期框架插入与\n // 关闭归还都以它为基准(机制与 context-menu 共用,见 menu-tree)。\n reconcileManagedMenuItems(this, content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n orderManagedMenuItems(this, content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n }\n\n private _scheduleMenuSync() {\n if (this._menuSyncScheduled) return\n this._menuSyncScheduled = true\n requestAnimationFrame(() => {\n this._menuSyncScheduled = false\n if (!this.open) return\n this._syncOpenMenuContent()\n })\n }\n\n // 打开期的统一内容同步:把打开期间框架新增的 menu 子树迁入对应层级 panel。\n // 一级项走 marker 锚 reconcile(保序 + 关闭按模板位归还);panel 尺寸变化由\n // defineOverlay 的 autoUpdate 跟进重定位。\n private _syncOpenMenuContent() {\n const level0Content = this._overlays.get(0)?.content\n // 迁移前隐藏宿主中新项的嵌套子项(hidden slot 属性随元素一起迁移)。\n this._hideAllSubmenuChildren()\n if (level0Content) {\n reconcileManagedMenuItems(this, level0Content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n // 新迁入项的嵌套子项未打 hidden slot,会经 dropdown-item 默认 slot 渲染进\n // 父项 label 叠到 menu 上;panel 项在 panel 内渲染,宿主查询够不到,须对 content 补隐藏。\n hideNestedMenuChildren(level0Content, `${SLOT_PREFIX}-hidden`)\n // 框架注释锚点复位独立于元素重排:锚点漂移不收敛会破坏后续翻转的插入点(同 context-menu)。\n const frameworkAnchors = captureFrameworkAnchors(level0Content, MENU_ITEM_MARKER)\n orderManagedMenuItems(this, level0Content, this._level0ItemAnchors, MENU_ITEM_MARKER)\n restoreFrameworkAnchors(level0Content, frameworkAnchors)\n }\n for (const [level, overlay] of this._overlays) {\n if (level === 0) continue\n const parentItem = this._getLevelItems(level - 1)[this._activePath[level - 1]]\n if (parentItem) moveMenuChildren(parentItem, overlay.content)\n hideNestedMenuChildren(overlay.content, `${SLOT_PREFIX}-hidden`)\n }\n this._hoverBinder.bind()\n }\n\n private _returnLevel0Items() {\n const content = this._overlays.get(0)?.content\n if (content) returnManagedMenuItemsToSlot(this, content, this._level0ItemAnchors)\n }\n\n private _populateOverlay(level: number, submenuItem: HTMLElement) {\n const content = this._overlays.get(level)?.content\n if (content) moveMenuChildren(submenuItem, content)\n }\n\n private _depopulateOverlay(level: number, parentItem?: HTMLElement) {\n const content = this._overlays.get(level)?.content\n if (!content) return\n const targetLevel = level - 1\n const submenuItem = parentItem ?? this._getLevelItems(targetLevel)[this._activePath[targetLevel]]\n if (submenuItem) moveMenuChildren(content, submenuItem)\n }\n\n /*\n * 登记与「panel 是否本帧新建」解耦,与 context-menu 的根层修复同构。\n *\n * 退场被打断时(`_closeRootAfterPresence` 在 `hideOverlayPresence` 返回 false 后提前\n * return)panel 仍在 DOM 里,而关闭分支已经把 handle 撤了。此时重开走的是 `_ensureOverlay`\n * 的两条复用分支,它们不会重跑 `_buildOverlay`——原先只有 `_buildOverlay` 会 claim/adopt,\n * 于是出现「panel 可见 + `open === true` 却没有任何登记」:Escape 关不掉它(仲裁看不到它),\n * 守卫里 `contains` 恒 false,点 panel 内部反被当成外部点击关掉整张 menu。\n *\n * `open` 属性路径(`updated()` 里 scheduleFrame 到 `_ensureOverlay(0)`)与 `openMenu()`\n * 都落在复用分支上,因此**所有**根 claim 都必须走这里 —— 只在 `_ensureOverlay` 里补 claim\n * 是不够的:`openMenu()` 会先把 handle 建好,使那次补 claim 变成 no-op,子层照样被漏掉。\n *\n * 新 claim 即新会话,旧会话连同其全部 adopt 子树一起作废,因此**仍在场的**子层必须\n * 在这里统一重挂。来源有两个:`_overlays` 里登记的层级,以及本轮已退场、被移出\n * `_overlays` 却仍在 DOM 里的收尾栈 panel —— 只看前者会漏掉后者(见下)。\n */\n private _claimRoot(panel: HTMLElement): OpenOverlayHandle {\n this._handle?.release()\n this._handle = this._overlay.claim(panel)\n for (const [level, sub] of this._overlays) {\n if (level === 0) continue\n sub.handle = this._handle.adopt(sub.overlay)\n }\n /*\n * 收尾栈里的 submenu panel 同样「可见但已脱离快照」:`_closeSubmenuFrom` 只把它们移出\n * `_overlays`,层仍挂在旧 handle 下、panel 仍在 DOM 里退场。不重挂的话它们会被判成 panel 外,\n * 点它内部就关掉整张 menu —— base 的 `composition.contains` 走登记树、且退场期间不注销,\n * 所以这条是相对 base 的行为回归,不是 parity。\n * 新 handle 要写回容器:收尾结束时 adapter.dispose 释放的是 `container.handle`。\n */\n for (const container of this._closingSubmenus.closing()) {\n container.handle = this._handle.adopt(container.overlay)\n }\n return this._handle\n }\n\n /** handle 缺失时补登记;panel 还没建好时以宿主代填,与 `_openMenu` 的占位会话同一口径。 */\n private _ensureRootHandle(): OpenOverlayHandle {\n return this._handle ?? this._claimRoot(this._overlays.get(0)?.overlay ?? this)\n }\n\n private _ensureOverlay(level: number, isInstant = false, submenuItem?: HTMLElement): boolean {\n const closingOverlay = submenuItem ? this._closingSubmenus.take(submenuItem) : undefined\n if (closingOverlay) {\n // 从 closing 栈取回的 panel:登记已在关闭时撤销,这里必须重新挂回根 handle 子树。\n closingOverlay.handle = this._ensureRootHandle().adopt(closingOverlay.overlay)\n this._overlays.set(level, closingOverlay)\n closingOverlay.api.open()\n showOverlayPresence(closingOverlay.overlay, { isInstant })\n return true\n }\n const existing = this._overlays.get(level)\n if (existing) {\n this._ensureRootHandle()\n existing.api.open()\n showOverlayPresence(existing.overlay, { isInstant })\n return false\n }\n this._buildOverlay(level, isInstant)\n return false\n }\n\n private _buildOverlay(level: number, isInstant = false) {\n const { panel: overlay, content } = createMenuPortalOverlay('dropdown-overlay', this)\n overlay.setAttribute('role', 'menu')\n // 根层级 panel 带 id:trigger 的 aria-controls 指向它(Q9a)。\n if (level === 0) overlay.id = `wui-dropdown-menu-${++dropdownIdCounter}`\n overlay.dataset.level = String(level)\n overlay.addEventListener('click', this._onMenuClick)\n overlay.addEventListener('keydown', this._onKeydown)\n\n const anchor = level === 0 ? this._queryTriggerAnchor() : this._getSubmenuTriggerAnchor(level - 1)\n\n if (anchor) {\n const ctrl = defineOverlay().make({\n anchor,\n overlay,\n placement: level === 0 ? this.placement : 'right-start',\n offset: level === 0 ? this.offset : 0,\n // menu 至少与 trigger/父项同宽,内容可更宽(floor 来自 --wui-overlay-min-width)\n minAnchorWidth: true,\n matchWidth: level === 0 ? this.matchWidth : false,\n strategy: 'fixed'\n })\n if (level === 0) {\n // panel 就绪:释放「以宿主代填」的占位会话,换成真实 panel。\n this._claimRoot(overlay)\n this._overlays.set(level, { api: ctrl, overlay, content })\n this._populateLevel0()\n } else {\n /*\n * 子层进根 handle 子树,默认不是独立候选 —— 「在树里但不是这一层」由此表达;\n * 原先按 level 找父 panel 的判断随之消失。用 `_ensureRootHandle()` 而不是 `_handle?.`:\n * 后者在 handle 意外缺失时会静默跳过 adopt,留下一棵无人认领的子树。\n */\n const handle = this._ensureRootHandle().adopt(overlay)\n this._overlays.set(level, { api: ctrl, overlay, content, handle })\n }\n // submenu 父项在 panel content 内,其新增子项只有观察 content 子树才能看到\n this._menuContentObserver.observe(content, { childList: true, subtree: true })\n ctrl.open()\n showOverlayPresence(overlay, { isInstant })\n }\n }\n\n private _disposeOverlay(level: number) {\n const overlay = this._overlays.get(level)\n if (level === 0) {\n // 根 handle 撤销会递归撤销全部已 adopt 的子层。\n this._handle?.release()\n this._handle = null\n }\n // 子层 handle 随所属 MenuOverlay 释放;已被根层递归撤销时幂等无操作。\n overlay?.handle?.release()\n overlay?.overlay.removeEventListener('click', this._onMenuClick)\n overlay?.overlay.removeEventListener('keydown', this._onKeydown)\n overlay?.overlay.remove()\n overlay?.api.dispose()\n this._overlays.delete(level)\n this._rebindMenuContentObservers()\n }\n\n // jsdom 的 MutationObserver 不实现 unobserve;统一 disconnect 后重绑剩余目标。\n private _rebindMenuContentObservers() {\n this._menuContentObserver.disconnect()\n this._menuContentObserver.observe(this, { childList: true, subtree: true })\n for (const overlay of this._overlays.values()) {\n this._menuContentObserver.observe(overlay.content, { childList: true, subtree: true })\n }\n }\n\n private _disposeAll() {\n /*\n * 先无条件撤销根 handle。`_openMenu` 会先用宿主当 panel claim 一次占位会话(真实 panel 要等\n * 一帧才建好),若这段时间内宿主被卸载、`_buildOverlay` 没机会跑,`_overlays` 仍是空的\n * —— 只遍历 `_overlays` 就会漏掉它:该层永久留在模块注册表里强引用整个组件,且宿主\n * 重挂载后 `panel.isConnected` 又为真,占位层重新成为候选、吞掉 Escape。\n */\n this._handle?.release()\n this._handle = null\n for (const level of this._overlays.keys()) {\n this._disposeOverlay(level)\n }\n }\n\n private _syncScrollLock(isOpen = this.open) {\n this._scrollLock.sync(isOpen && !this.noScrollLock)\n }\n\n private _queryTriggerAnchor(): HTMLElement | null {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]')\n const el = slot?.assignedElements()[0]\n return el instanceof HTMLElement ? el : null\n }\n\n /*\n * trigger 语义回写(Q9a):slot 进来的元素是真正的可聚焦交互元素,\n * aria-haspopup / aria-expanded / aria-controls 必须设在它身上;\n * 组件没有 trigger 包装 div,此前完全没有 ARIA 接线。\n */\n private _syncTriggerAria() {\n const trigger = this._queryTriggerAnchor()\n if (!trigger) return\n trigger.setAttribute('aria-haspopup', 'menu')\n trigger.setAttribute('aria-expanded', String(this.open))\n const rootOverlay = this._overlays.get(0)?.overlay\n if (rootOverlay?.id) trigger.setAttribute('aria-controls', rootOverlay.id)\n else trigger.removeAttribute('aria-controls')\n }\n\n private _getSubmenuTriggerAnchor(level: number): HTMLElement | null {\n const idx = this._activePath[level]\n if (idx === undefined) return null\n const items = this._getLevelItems(level)\n const item = items[idx]\n if (!item) return null\n return item.shadowRoot?.querySelector('.item-inner') ?? item\n }\n\n private _onTriggerClick = (event: MouseEvent) => {\n if (this.disabled) return\n if (this.open) {\n this._closeAll(true)\n } else {\n /*\n * `detail === 0` ⇔ 键盘激活(Enter/Space 触发的 click),`detail > 0` ⇔ 指针点击。\n * 同一个信号既决定是否免入场过渡,也决定首项要不要留 `:focus-visible`:\n * 键盘打开首项即应有 accent 焦点视觉,指针打开则抑制(见 `_suppressInitialFocusVisible`)。\n */\n const fromKeyboard = event.detail === 0\n this._openMenu(fromKeyboard, true, !fromKeyboard)\n }\n }\n\n private _onClickOutside = (e: MouseEvent) => {\n if (!this.isOpen || this._outsideClickGuard.isArmed()) return\n if (this._outsideClickGuard.isInside(e)) return\n this._closeAll(true)\n }\n\n private _onMenuClick = (e: MouseEvent) => {\n const item = getMenuItemFromEvent(e)\n if (!item || item.hasAttribute('disabled') || !item.hasAttribute('submenu')) return\n\n const level = this._getFocusedLevel(item)\n if (level === undefined) return\n const itemIndex = this._getLevelItems(level).indexOf(item)\n if (itemIndex >= 0) this._toggleSubmenu(level, itemIndex, e.detail === 0)\n }\n\n private _onKeydown = (e: KeyboardEvent) => {\n if (this.disabled || !this.open) return\n\n // overlay 上键盘事件 target 是 menu item 而非组件自身,不能限定 e.target === this,\n // 否则鼠标打开、焦点在 menu item 时 Escape 无法关闭\n handleMenuKeyboard(this._keyboardDelegate, e)\n }\n\n private _getFocusedLevel(item?: HTMLElement): number | undefined {\n if (!item) return undefined\n const overlay = [...this._overlays.entries()].find(([, value]) => value.overlay.contains(item))\n return overlay?.[0]\n }\n\n private _bindHoversAfterUpdate() {\n requestAnimationFrame(() => this._hoverBinder.bind())\n }\n\n override render() {\n return html`\n <div class=\"dropdown-trigger\" @click=${this._onTriggerClick}>\n <slot name=\"trigger\"></slot>\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-dropdown': WebUiDropdown\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/dropdown-item/index.ts"],"sourcesContent":["import '@/components/icon'\n\nimport { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { lucideChevronRight } from '@/icons'\n\nimport style from './style.css?inline'\n\n@customElement('web-ui-dropdown-item')\nexport class WebUiDropdownItem extends LitElement {\n static override styles = unsafeCSS(style)\n\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: String, reflect: true }) pl = ''\n @property({ type: String, reflect: true }) value = ''\n @property({ type: Boolean, reflect: true }) submenu = false\n\n // 通过可见元素聚焦,避免 portal 菜单跨 Shadow DOM 时丢失焦点。\n focusItem() {\n this.shadowRoot?.querySelector<HTMLElement>('.item-inner')?.focus()\n }\n\n override render() {\n const plStyle = this.pl ? `padding-left: ${this.pl}` : ''\n return html`\n <div class=\"item-inner\" style=${plStyle} role=\"menuitem\" tabindex=${this.disabled ? '-1' : '0'}>\n <slot name=\"prefix\"></slot>\n <span class=\"item-label\"><slot></slot></span>\n ${\n this.submenu\n ? html`<web-ui-icon .icon=${lucideChevronRight}></web-ui-icon>`\n : html`<span class=\"item-suffix\"><slot name=\"suffix\"></slot></span>`\n }\n </div>\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-dropdown-item': WebUiDropdownItem\n }\n}\n"],"mappings":";;;;;;;AAUO,IAAM,IAAN,cAAgC,EAAW;;EAMM,aAHC,KAAA,WAAA,IACP,KAAA,KAAA,IACG,KAAA,QAAA,IACG,KAAA,UAAA;;;EAL7B,KAAA,SAAA,EAAU,CAAK;;CAQxC,YAAY;EACV,KAAK,YAAY,cAA2B,aAAa,CAAC,EAAE,MAAM;CACpE;CAEA,SAAkB;EAChB,IAAM,IAAU,KAAK,KAAK,iBAAiB,KAAK,OAAO;EACvD,OAAO,CAAI;sCACuB,EAAQ,4BAA4B,KAAK,WAAW,OAAO,IAAI;;;UAI3F,KAAK,UACD,CAAI,sBAAsB,EAAmB,mBAC7C,CAAI,+DACT;;;CAGP;AACF;AAxBG,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,MAAA,KAAA,CAAA,GACxC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,SAAA,KAAA,CAAA,GACxC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GAP3C,IAAA,EAAA,CAAA,EAAc,sBAAsB,CAAA,GAAA,CAAA"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/dropdown-item/index.ts"],"sourcesContent":["import '@/components/icon'\n\nimport { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { lucideChevronRight } from '@/icons'\n\nimport style from './style.css?inline'\n\n@customElement('web-ui-dropdown-item')\nexport class WebUiDropdownItem extends LitElement {\n static override styles = unsafeCSS(style)\n\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: String, reflect: true }) pl = ''\n @property({ type: String, reflect: true }) value = ''\n @property({ type: Boolean, reflect: true }) submenu = false\n\n // 通过可见元素聚焦,避免 portal menu 跨 Shadow DOM 时丢失焦点。\n focusItem() {\n this.shadowRoot?.querySelector<HTMLElement>('.item-inner')?.focus()\n }\n\n override render() {\n const plStyle = this.pl ? `padding-left: ${this.pl}` : ''\n return html`\n <div class=\"item-inner\" style=${plStyle} role=\"menuitem\" tabindex=${this.disabled ? '-1' : '0'}>\n <slot name=\"prefix\"></slot>\n <span class=\"item-label\"><slot></slot></span>\n ${\n this.submenu\n ? html`<web-ui-icon .icon=${lucideChevronRight}></web-ui-icon>`\n : html`<span class=\"item-suffix\"><slot name=\"suffix\"></slot></span>`\n }\n </div>\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-dropdown-item': WebUiDropdownItem\n }\n}\n"],"mappings":";;;;;;;AAUO,IAAM,IAAN,cAAgC,EAAW;;EAMM,aAHC,KAAA,WAAA,IACP,KAAA,KAAA,IACG,KAAA,QAAA,IACG,KAAA,UAAA;;;EAL7B,KAAA,SAAA,EAAU,CAAK;;CAQxC,YAAY;EACV,KAAK,YAAY,cAA2B,aAAa,CAAC,EAAE,MAAM;CACpE;CAEA,SAAkB;EAChB,IAAM,IAAU,KAAK,KAAK,iBAAiB,KAAK,OAAO;EACvD,OAAO,CAAI;sCACuB,EAAQ,4BAA4B,KAAK,WAAW,OAAO,IAAI;;;UAI3F,KAAK,UACD,CAAI,sBAAsB,EAAmB,mBAC7C,CAAI,+DACT;;;CAGP;AACF;AAxBG,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,MAAA,KAAA,CAAA,GACxC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,SAAA,KAAA,CAAA,GACxC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GAP3C,IAAA,EAAA,CAAA,EAAc,sBAAsB,CAAA,GAAA,CAAA"}
@@ -478,9 +478,9 @@ export declare class WebUiEditableText extends WebUiEditableText_base {
478
478
  /**
479
479
  * 编辑态按键归属:Enter 提交、Escape 取消。
480
480
  *
481
- * 监听挂在 window 捕获阶段而不是 shadow 内的 textarea 上:消费方(浮层仲裁者)在
481
+ * 监听挂在 window 捕获阶段而不是 shadow 内的 textarea 上:消费方(overlay 仲裁者)在
482
482
  * document 捕获阶段收 Escape,组件若只在 textarea 上处理,按键先被仲裁者收走,
483
- * 于是一次 Escape 会同时取消编辑和关闭外层浮层。window 在 document 上游,
483
+ * 于是一次 Escape 会同时取消编辑和关闭外层 overlay。window 在 document 上游,
484
484
  * stopPropagation 只有挂在这里才拦得住仲裁者。
485
485
  */
486
486
  private _claimEditingKeys;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/editable-text/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\nimport { ifDefined } from 'lit/directives/if-defined.js'\n\nimport { FormAssociated, defineFormAssociation, FormAssociationController } from '@/shared/form-association'\nimport { createFieldId } from '@/shared/form-association/field-id'\n\nimport style from './style.css?inline'\n\n/** 空值且无 placeholder 时占位,保留一个行盒,避免宿主退化成 0 高度、无法点击进入编辑。 */\nconst BLANK_GLYPH = '\\u200B'\n\n/** 行/字符落点二分的容差(px),吸收次像素与舍入差异。 */\nconst CARET_TOLERANCE = 1\n\n/**\n * 取文本层承载文案的 Text 节点。\n *\n * 绑定表达式位于元素首个位置时,Lit 会在它前面留一个注释标记节点,因此不能用\n * `firstChild`:那会拿到没有布局的注释,Range 量不到任何矩形。\n */\nfunction textNodeOf(layer: HTMLElement): Text | null {\n for (const child of layer.childNodes) {\n if (child instanceof Text) return child\n }\n return null\n}\n\n/**\n * 把视口坐标映射为文本层的字符偏移。\n *\n * `document.caretPositionFromPoint` 不进入 shadow DOM(对 shadow 内容返回 shadow host\n * 之外的节点),因此在文本层上用 Range 空矩形二分:先按 y 选行,再在行内按 x 选字符。\n * 文本层与编辑层同盒同排版,偏移与 textarea 的 selectionStart 一一对应。\n */\nfunction caretOffsetFromPoint(textLayer: HTMLElement, x: number, y: number): number {\n const node = textNodeOf(textLayer)\n if (!node) return 0\n const length = node.data.length\n const range = document.createRange()\n const rectAt = (offset: number): DOMRect => {\n range.setStart(node, offset)\n range.setEnd(node, offset)\n return range.getBoundingClientRect()\n }\n\n // 同一行的盒可能被拆成多个 client rect;按 top 去重得到行列表,取与 y 最近的一行,\n // 距离相等时取更靠下的一行(与浏览器在行间的落点行为一致)。\n const lines: DOMRect[] = []\n for (const rect of textLayer.getClientRects()) {\n if (!lines.some(line => Math.abs(line.top - rect.top) <= CARET_TOLERANCE)) lines.push(rect)\n }\n if (lines.length === 0) return length\n let line = lines[0]\n let nearest = Infinity\n for (const candidate of lines) {\n const distance = Math.abs((candidate.top + candidate.bottom) / 2 - y)\n if (distance <= nearest) {\n nearest = distance\n line = candidate\n }\n }\n\n // 行首:caret top 首次落入本行的最小偏移\n let lo = 0\n let hi = length\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if (rectAt(mid).top >= line.top - CARET_TOLERANCE) hi = mid\n else lo = mid + 1\n }\n const lineStart = lo\n // 行尾:caret top 仍落在本行的最大偏移\n lo = lineStart\n hi = length\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1\n if (rectAt(mid).top <= line.top + CARET_TOLERANCE) lo = mid\n else hi = mid - 1\n }\n const lineEnd = Math.max(lo, lineStart)\n // 行内:caret left 不超过 x 的最大偏移\n lo = lineStart\n hi = lineEnd\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1\n if (rectAt(mid).left <= x + CARET_TOLERANCE) lo = mid\n else hi = mid - 1\n }\n return lo\n}\n\n@customElement('web-ui-editable-text')\nexport class WebUiEditableText extends FormAssociated(LitElement) {\n static override styles = [unsafeCSS(style)]\n\n @property({ type: String, reflect: true }) name = ''\n @property({ type: String, reflect: true }) placeholder = ''\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true }) readonly = false\n\n /**\n * 卸载时按提交语义结束编辑,默认 false。\n *\n * 跨引擎实测(Chromium 与 WebKit 各一轮)结论**不一致**,这是本属性的主要价值来源:\n *\n * - Chromium:移除正在编辑的组件会向编辑层派发 `blur`,默认行为下 `change` 已照常派发\n * 一次,草稿不丢。blur 与 change 同步排在 `disconnectedCallback` 之后(后者在\n * disconnected 步骤内跑完),卸载因此没拿到一条「排在该消费方状态更新之前」的提交来源。\n * - WebKit(Safari / iOS):移除**不派发 blur**,默认路径什么都不提交,草稿随组件一起丢弃。\n * 这里 blur 兜底不存在,只有显式开启本属性才能保住草稿。\n * - Gecko(Firefox)**未验证**:实测所用机器上没有任何 Firefox 能启动,这里不做断言。把它当作\n * 未知,而不是默认与上面两者之一相同。\n *\n * 所以在 Safari 上这不是可选优化而是必需的:默认值 `false` 对 Safari 用户等于没有兜底,\n * 消费方若依赖「行被回收时草稿不丢」,在 Safari 上必须显式开启本属性。\n *\n * 开启后由 `disconnectedCallback` 复用 `_commitEditing`,与 `Enter`/`blur` 是同一份实现,\n * 卸载不是第三条独立的提交语义。副作用与 `blur` 提交一致:草稿成为新值、退出编辑、\n * 派发一次 `change`(冒泡且组合)。两处与 `blur` 刻意不同:\n *\n * 1. 提交判据——`Enter` 与 `blur` 由用户动作触发,一律算提交意图;卸载没有这种意图,\n * 因此只在草稿与进入编辑时的值确实不同时才提交,不产生空提交。\n * 2. 禁用与只读态——`disabled` / `readonly` 时与 `_onBlur` 一致:不派发提交事件。两者对「已有\n * 草稿」的后果不同,必须分开说:`disabled` 下草稿保留在组件上;`readonly` 下草稿是真的\n * 丢掉——`readonly` 阻止不了先前的输入,而卸载时不会再有 blur 兜底(WebKit 上连 blur\n * 都没有)。可达路径是「可编辑态进入 → 起草稿 → 期间翻 readonly → 卸载」。这里不按取消\n * 处理:cancel 的契约是恢复 `_editBase`,而卸载既没有焦点要交还也没有宿主接收结果,\n * 改成 cancel 还会与「与 `_onBlur` 一致」这条公开承诺相悖。用户看到的是「我打的字没了」,\n * 既没有 change 也没有 cancel。\n *\n * 派发发生在卸载回调内,`change` 沿组合路径向上派发,因此挂在组件自身或任一祖先上的监听\n * 都会执行,**包括已经随宿主一起被卸载的祖先**——派发不要求祖先仍在文档中。这正是虚拟化\n * 场景需要的:消费方的监听器往往就挂在被回收的那一行上。因此监听器不能假定组件此刻仍在\n * 文档里;需要把结果交给宿主树之外的状态时,用 `queueMicrotask` 之类的显式转交。\n *\n * 祖先上的 `display: none` 不是卸载:组件仍在文档中、不派发任何事件(两引擎一致),那种\n * 场合需要消费方主动结束编辑。\n */\n @property({ type: Boolean, reflect: true, attribute: 'commit-on-unmount' }) commitOnUnmount = false\n\n @property({ type: String, attribute: 'aria-label' }) override ariaLabel: string | null = null\n\n @state() private _value = ''\n\n /**\n * `value` attribute 承载 form reset 的初值,不随当前值反射;property 暴露当前值,\n * 与原生 input 的 value / defaultValue 关系一致。\n */\n @property({ type: String })\n get value(): string {\n return this._value\n }\n\n set value(v: string) {\n const old = this._value\n this._value = v\n this._formAssociation.sync()\n this.requestUpdate('value', old)\n }\n\n @query('.editor') private _editor?: HTMLTextAreaElement\n @query('.text') private _textLayer?: HTMLElement\n\n private _editing = false\n private _editBase = ''\n private _pendingCaret: number | null = null\n private _refocusing = false\n private _resizeObserver: ResizeObserver | null = null\n private readonly _fieldId = createFieldId('web-ui-editable-text')\n\n private get _isDisabled(): boolean {\n return this.disabled || this._formAssociation.isFormDisabled()\n }\n\n private readonly _formAssociation = defineFormAssociation<string>({\n host: this,\n getState: () => this._value,\n setState: value => {\n this.value = value\n },\n getFormValue: () => this._value,\n getFormState: () => this._value,\n restoreState: state => {\n if (typeof state === 'string') this.value = state\n }\n }).make()\n\n private readonly _formAssociationController = new FormAssociationController(this, this._formAssociation)\n\n constructor() {\n super()\n // focus 不冒泡:宿主监听只在焦点从组件外进入时触发,shadow 内聚焦不会误触\n this.addEventListener('focus', this._onHostFocus)\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n /*\n * 卸载没有用户动作作为提交意图(`Enter` / `blur` 都有),所以判据是草稿与进入编辑时的\n * 值是否不同,而不是「是否提交过」——Escape 会把值恢复成 `_editBase`,但那是取消,与\n * 本次卸载无关;一个只聚焦了没打字的会话也不该在消失时凭空产生一次 change。\n *\n * `_isDisabled` 与 `readonly` 两道守卫都与 `_onBlur` 对称:两者都不派发提交事件。`disabled`\n * 必须显式判——`disabled = true` 与 `remove()` 在同一任务里执行时 Lit 更新是微任务,\n * `updated()` 还没跑到(禁用态退出编辑那条分支尚未执行),此时组件仍在编辑态,不判就会\n * 派发一条 blur 路径明确拒绝的 change。`readonly` 不能靠「草稿恒等于基线」兜住:可编辑态\n * 进入并起草稿之后属性才翻成 readonly 的会话,基线仍是进入编辑时的旧值,两条判据都成立\n * 却不该提交(草稿在这次卸载中丢失,理由见 property 的 JSDoc)。\n */\n if (\n this.commitOnUnmount &&\n this._editing &&\n !this._isDisabled &&\n !this.readonly &&\n this._value !== this._editBase\n ) {\n this._commitEditing(false)\n }\n this._releaseEditingKeys()\n this._teardownAutosize()\n }\n\n override connectedCallback() {\n super.connectedCallback()\n /*\n * autosize 的 ResizeObserver 在 disconnectedCallback 被拆掉,搭建若只放在\n * firstUpdated(一生一次),元素断开再重连后 RO 永久丢失、autosize 静默失效。\n * connectedCallback 每次连接都跑:_setupAutosize 自带 teardown,重复进入无副作用。\n * 首连接时渲染尚未发生(_editor 为 null),_autosizeEditor 与 RO 回调都有判空守卫。\n */\n this._setupAutosize()\n // 同类缺口:编辑态被断开时 _releaseEditingKeys 撤了 window 监听,重连后 _editing\n // 仍为 true(blur 取消未必先于断开到达),此时补领,避免重连后按键失灵。\n if (this._editing) this._claimEditingKeys()\n }\n\n override updated() {\n // disabled 时移出 tab 序列;编辑态由 editing attribute 表达,二者互斥\n if (this._isDisabled) this.removeAttribute('tabindex')\n else this.setAttribute('tabindex', '0')\n // 禁用态生效时(属性或 fieldset)退出编辑:值保持当前草稿,不派发提交事件\n if (this._editing && this._isDisabled) this._exitEditing()\n this._autosizeEditor()\n }\n\n private _onHostFocus() {\n if (this._refocusing) {\n this._refocusing = false\n return\n }\n this._enterEditing()\n }\n\n /**\n * pointerdown 时文本层仍是命中目标,此时记录落点;浏览器随后的默认聚焦会触发\n * host focus 进入编辑,偏移在那里应用到 textarea。\n */\n private _onPointerDown(e: PointerEvent) {\n if (this._isDisabled || this._editing || !this._textLayer) return\n this._pendingCaret = caretOffsetFromPoint(this._textLayer, e.clientX, e.clientY)\n }\n\n /** mousedown 默认聚焦被拦截等场景由 click 兜底进入编辑。 */\n private _onClick() {\n if (this._isDisabled || this._editing) return\n this._enterEditing()\n }\n\n /**\n * 公共 API:进入编辑态并全选内容;已在编辑态时只重新全选。\n *\n * `disabled` 时不进入编辑,与 `focus()` 一致。\n */\n select() {\n if (this._isDisabled) return\n if (!this._editing) this._enterEditing()\n const editor = this._editor\n // 全选:末端显式取当前文本长度,覆盖 _enterEditing 落下的光标位置\n editor?.setSelectionRange(0, editor.value.length)\n }\n\n private _enterEditing() {\n if (this._isDisabled || this._editing) return\n this._editing = true\n this._editBase = this._value\n this.toggleAttribute('editing', true)\n this._claimEditingKeys()\n const editor = this._editor\n if (!editor) return\n editor.focus()\n // Tab / 编程式聚焦光标到末尾;指针落点用 pointerdown 记下的偏移\n const offset = this._pendingCaret ?? this._value.length\n this._pendingCaret = null\n const clamped = Math.max(0, Math.min(offset, editor.value.length))\n editor.setSelectionRange(clamped, clamped)\n }\n\n private _exitEditing() {\n if (!this._editing) return\n this._editing = false\n this._releaseEditingKeys()\n this.toggleAttribute('editing', false)\n }\n\n private _onInput() {\n // 原生 input 已 composed 冒泡出 shadow root 且 target 重定向到宿主,无需补发\n const editor = this._editor\n if (this._isDisabled || this.readonly) {\n if (editor && editor.value !== this._value) editor.value = this._value\n return\n }\n this._value = editor?.value ?? ''\n this._formAssociation.sync()\n this._autosizeEditor()\n }\n\n /**\n * blur 提交:与 Enter 同一条提交路径,草稿成为新值并派发 change。\n *\n * 不交还焦点:焦点已被用户移走,抢回宿主会吞掉消费者的目标焦点。\n */\n private _onBlur() {\n if (!this._editing || this._isDisabled) return\n if (this.readonly) {\n this._exitEditing()\n return\n }\n this._commitEditing(false)\n }\n\n /**\n * 提交:草稿成为新值、退出编辑,并恰好派发一次 change。Enter 与 blur 同走这一条路。\n *\n * `returnFocus` 区分两种来路:Enter 主动结束时把焦点交还宿主(标记防重入编辑);\n * blur 是被动失焦,焦点已被用户移走,再抢回宿主会吞掉消费者的目标焦点。\n * 先退出编辑再动焦点:退出时 textarea 隐藏触发的 blur 看到 _editing 已为 false,\n * 不会把这次提交误判成又一次 blur 提交。\n */\n private _commitEditing(returnFocus: boolean) {\n if (!this._editing) return\n this._exitEditing()\n if (returnFocus) this._returnFocusToHost()\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n }\n\n /**\n * 取消:恢复进入编辑时的值并派发 cancel。仅 Escape 走这一条路径。\n *\n * 调用方(Escape 分支)随后才交还焦点:本方法先摘除编辑态,焦点迁移触发的 blur\n * 到达时 `_editing` 已为 false 而早退,不会把取消误判成 blur 提交。\n */\n private _cancelEditing() {\n if (!this._editing) return\n this._value = this._editBase\n this._formAssociation.sync()\n this._exitEditing()\n /*\n * 重入守卫先于 dispatch 置位:cancel 同步派发,消费者可能在监听器里调\n * el.focus()——那是一次真实的宿主 focus,没有守卫就会重新进入编辑,随后一次\n * blur 便把恢复后的原值当成新草稿提交,用户什么都没改却收到 change。守卫在此\n * 置位后即被该次 focus 消费,与取消后由组件自己交还焦点走同一条早退路径。\n */\n this._refocusing = true\n /*\n * cancel 与原生 <dialog> 的 cancel 同名,而 dialog 的关闭管线就监听这个事件名。\n * 组件被消费方投映进浮层 shadow(drawer/dialog 标题)时,任何冒泡的 cancel——\n * composed 与否都一样,slot 都会把事件带进 shadow 树——都会被那个 dialog 当成\n * 一次关闭请求:issue #159 的实测症状是 drawer 标题聚焦编辑时按 Escape,或点到\n * 抽屉别处触发 blur,抽屉跟着编辑一起关。因此只在宿主上派发,不冒泡不组合;\n * 监听挂在组件本身即可(vue @cancel、React addEventListener 均不受影响)。\n * 手法同 checkbox/radio 受管时的事件策略(见各自 handleClick 注释)。\n */\n this.dispatchEvent(new Event('cancel', { bubbles: false, composed: false }))\n }\n\n /**\n * 焦点交还宿主:先标记再聚焦,宿主 focus 到达时标记已消费,不会再次进入编辑;\n * 标记在微任务里清掉,此后的聚焦照常进入编辑。\n */\n private _returnFocusToHost() {\n this._refocusing = true\n this.focus()\n queueMicrotask(() => {\n this._refocusing = false\n })\n }\n\n /**\n * 编辑态按键归属:Enter 提交、Escape 取消。\n *\n * 监听挂在 window 捕获阶段而不是 shadow 内的 textarea 上:消费方(浮层仲裁者)在\n * document 捕获阶段收 Escape,组件若只在 textarea 上处理,按键先被仲裁者收走,\n * 于是一次 Escape 会同时取消编辑和关闭外层浮层。window 在 document 上游,\n * stopPropagation 只有挂在这里才拦得住仲裁者。\n */\n private _claimEditingKeys() {\n window.addEventListener('keydown', this._onEditingKeydown, true)\n }\n\n private _releaseEditingKeys() {\n window.removeEventListener('keydown', this._onEditingKeydown, true)\n }\n\n private readonly _onEditingKeydown = (e: KeyboardEvent) => {\n // 输入法组合期间的 Enter/Escape 属于组合会话,交给 IME\n if (e.isComposing) return\n if (!e.composedPath().includes(this)) return\n if (e.key === 'Escape') {\n // 按键本身也被编辑层消费:外层浮层不应收到同一次 Escape\n e.preventDefault()\n e.stopPropagation()\n // 先取消(内部摘除编辑态)再交还焦点:focus 触发的 blur 看到 _editing 已为\n // false 而早退,不会把这次取消误判成 blur 提交\n this._cancelEditing()\n this._returnFocusToHost()\n return\n }\n if (e.key === 'Enter') {\n /*\n * Enter 无论是否只读都属于编辑层,消费策略与 Escape 对称:preventDefault\n * 压掉默认行为,stopPropagation 阻止外层表单隐式提交或浮层监听收到按键。\n * 只读态只是没有草稿可提交,因此消费后直接返回,不退出编辑、不派发 change。\n */\n e.preventDefault()\n e.stopPropagation()\n if (this.readonly) return\n /*\n * 可编辑态的 Enter 随后提交草稿;外层表单的隐式提交与浮层监听不应收到\n * 同一次按键——正如 Escape 属于取消。\n */\n this._commitEditing(true)\n }\n }\n\n private _setupAutosize() {\n this._teardownAutosize()\n // jsdom 等无 ResizeObserver 的环境退化为按渲染 autosize,够用且不抛错\n if (typeof ResizeObserver === 'undefined') {\n this._autosizeEditor()\n return\n }\n this._resizeObserver = new ResizeObserver(() => this._autosizeEditor())\n this._resizeObserver.observe(this)\n this._autosizeEditor()\n }\n\n private _teardownAutosize() {\n this._resizeObserver?.disconnect()\n this._resizeObserver = null\n this._editor?.style.removeProperty('height')\n }\n\n /**\n * 编辑层高度始终跟随自身内容,不依赖文本层折出的行盒。\n *\n * 编辑层绝对定位并用 top/bottom 撑高,`height: auto` 量到的是包含块高度而不是内容高;\n * 先归零再读 scrollHeight,拿到的才是自身内容的高度(含 1px 内边距)。\n * 观察宿主而非编辑层:编辑层在文档流外,改它的高度不会反过来改动宿主尺寸。\n */\n private _autosizeEditor() {\n const editor = this._editor\n if (!editor) return\n const previous = editor.style.height\n editor.style.height = '0px'\n const height = editor.scrollHeight\n editor.style.height = height > 0 ? `${height}px` : previous\n }\n\n private get _displayText(): string {\n if (this._value !== '') return this._value\n if (this.placeholder !== '') return this.placeholder\n return BLANK_GLYPH\n }\n\n override render() {\n return html`\n <div class=\"layers\" @pointerdown=${this._onPointerDown} @click=${this._onClick}>\n <span class=${classMap({ text: true, placeholder: this._value === '' })}>${this._displayText}</span>\n <textarea\n id=${this._fieldId}\n class=\"editor\"\n .value=${this._value}\n .placeholder=${this.placeholder}\n ?disabled=${this._isDisabled}\n ?readonly=${this.readonly}\n aria-label=${ifDefined(this.ariaLabel)}\n @input=${this._onInput}\n @blur=${this._onBlur}\n ></textarea>\n </div>\n `\n }\n\n declare readonly $events: {\n input: Event\n change: Event\n cancel: Event\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-editable-text': WebUiEditableText\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/editable-text/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\nimport { ifDefined } from 'lit/directives/if-defined.js'\n\nimport { FormAssociated, defineFormAssociation, FormAssociationController } from '@/shared/form-association'\nimport { createFieldId } from '@/shared/form-association/field-id'\n\nimport style from './style.css?inline'\n\n/** 空值且无 placeholder 时占位,保留一个行盒,避免宿主退化成 0 高度、无法点击进入编辑。 */\nconst BLANK_GLYPH = '\\u200B'\n\n/** 行/字符落点二分的容差(px),吸收次像素与舍入差异。 */\nconst CARET_TOLERANCE = 1\n\n/**\n * 取文本层承载文案的 Text 节点。\n *\n * 绑定表达式位于元素首个位置时,Lit 会在它前面留一个注释标记节点,因此不能用\n * `firstChild`:那会拿到没有布局的注释,Range 量不到任何矩形。\n */\nfunction textNodeOf(layer: HTMLElement): Text | null {\n for (const child of layer.childNodes) {\n if (child instanceof Text) return child\n }\n return null\n}\n\n/**\n * 把视口坐标映射为文本层的字符偏移。\n *\n * `document.caretPositionFromPoint` 不进入 shadow DOM(对 shadow 内容返回 shadow host\n * 之外的节点),因此在文本层上用 Range 空矩形二分:先按 y 选行,再在行内按 x 选字符。\n * 文本层与编辑层同盒同排版,偏移与 textarea 的 selectionStart 一一对应。\n */\nfunction caretOffsetFromPoint(textLayer: HTMLElement, x: number, y: number): number {\n const node = textNodeOf(textLayer)\n if (!node) return 0\n const length = node.data.length\n const range = document.createRange()\n const rectAt = (offset: number): DOMRect => {\n range.setStart(node, offset)\n range.setEnd(node, offset)\n return range.getBoundingClientRect()\n }\n\n // 同一行的盒可能被拆成多个 client rect;按 top 去重得到行列表,取与 y 最近的一行,\n // 距离相等时取更靠下的一行(与浏览器在行间的落点行为一致)。\n const lines: DOMRect[] = []\n for (const rect of textLayer.getClientRects()) {\n if (!lines.some(line => Math.abs(line.top - rect.top) <= CARET_TOLERANCE)) lines.push(rect)\n }\n if (lines.length === 0) return length\n let line = lines[0]\n let nearest = Infinity\n for (const candidate of lines) {\n const distance = Math.abs((candidate.top + candidate.bottom) / 2 - y)\n if (distance <= nearest) {\n nearest = distance\n line = candidate\n }\n }\n\n // 行首:caret top 首次落入本行的最小偏移\n let lo = 0\n let hi = length\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if (rectAt(mid).top >= line.top - CARET_TOLERANCE) hi = mid\n else lo = mid + 1\n }\n const lineStart = lo\n // 行尾:caret top 仍落在本行的最大偏移\n lo = lineStart\n hi = length\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1\n if (rectAt(mid).top <= line.top + CARET_TOLERANCE) lo = mid\n else hi = mid - 1\n }\n const lineEnd = Math.max(lo, lineStart)\n // 行内:caret left 不超过 x 的最大偏移\n lo = lineStart\n hi = lineEnd\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1\n if (rectAt(mid).left <= x + CARET_TOLERANCE) lo = mid\n else hi = mid - 1\n }\n return lo\n}\n\n@customElement('web-ui-editable-text')\nexport class WebUiEditableText extends FormAssociated(LitElement) {\n static override styles = [unsafeCSS(style)]\n\n @property({ type: String, reflect: true }) name = ''\n @property({ type: String, reflect: true }) placeholder = ''\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true }) readonly = false\n\n /**\n * 卸载时按提交语义结束编辑,默认 false。\n *\n * 跨引擎实测(Chromium 与 WebKit 各一轮)结论**不一致**,这是本属性的主要价值来源:\n *\n * - Chromium:移除正在编辑的组件会向编辑层派发 `blur`,默认行为下 `change` 已照常派发\n * 一次,草稿不丢。blur 与 change 同步排在 `disconnectedCallback` 之后(后者在\n * disconnected 步骤内跑完),卸载因此没拿到一条「排在该消费方状态更新之前」的提交来源。\n * - WebKit(Safari / iOS):移除**不派发 blur**,默认路径什么都不提交,草稿随组件一起丢弃。\n * 这里 blur 兜底不存在,只有显式开启本属性才能保住草稿。\n * - Gecko(Firefox)**未验证**:实测所用机器上没有任何 Firefox 能启动,这里不做断言。把它当作\n * 未知,而不是默认与上面两者之一相同。\n *\n * 所以在 Safari 上这不是可选优化而是必需的:默认值 `false` 对 Safari 用户等于没有兜底,\n * 消费方若依赖「行被回收时草稿不丢」,在 Safari 上必须显式开启本属性。\n *\n * 开启后由 `disconnectedCallback` 复用 `_commitEditing`,与 `Enter`/`blur` 是同一份实现,\n * 卸载不是第三条独立的提交语义。副作用与 `blur` 提交一致:草稿成为新值、退出编辑、\n * 派发一次 `change`(冒泡且组合)。两处与 `blur` 刻意不同:\n *\n * 1. 提交判据——`Enter` 与 `blur` 由用户动作触发,一律算提交意图;卸载没有这种意图,\n * 因此只在草稿与进入编辑时的值确实不同时才提交,不产生空提交。\n * 2. 禁用与只读态——`disabled` / `readonly` 时与 `_onBlur` 一致:不派发提交事件。两者对「已有\n * 草稿」的后果不同,必须分开说:`disabled` 下草稿保留在组件上;`readonly` 下草稿是真的\n * 丢掉——`readonly` 阻止不了先前的输入,而卸载时不会再有 blur 兜底(WebKit 上连 blur\n * 都没有)。可达路径是「可编辑态进入 → 起草稿 → 期间翻 readonly → 卸载」。这里不按取消\n * 处理:cancel 的契约是恢复 `_editBase`,而卸载既没有焦点要交还也没有宿主接收结果,\n * 改成 cancel 还会与「与 `_onBlur` 一致」这条公开承诺相悖。用户看到的是「我打的字没了」,\n * 既没有 change 也没有 cancel。\n *\n * 派发发生在卸载回调内,`change` 沿组合路径向上派发,因此挂在组件自身或任一祖先上的监听\n * 都会执行,**包括已经随宿主一起被卸载的祖先**——派发不要求祖先仍在文档中。这正是虚拟化\n * 场景需要的:消费方的监听器往往就挂在被回收的那一行上。因此监听器不能假定组件此刻仍在\n * 文档里;需要把结果交给宿主树之外的状态时,用 `queueMicrotask` 之类的显式转交。\n *\n * 祖先上的 `display: none` 不是卸载:组件仍在文档中、不派发任何事件(两引擎一致),那种\n * 场合需要消费方主动结束编辑。\n */\n @property({ type: Boolean, reflect: true, attribute: 'commit-on-unmount' }) commitOnUnmount = false\n\n @property({ type: String, attribute: 'aria-label' }) override ariaLabel: string | null = null\n\n @state() private _value = ''\n\n /**\n * `value` attribute 承载 form reset 的初值,不随当前值反射;property 暴露当前值,\n * 与原生 input 的 value / defaultValue 关系一致。\n */\n @property({ type: String })\n get value(): string {\n return this._value\n }\n\n set value(v: string) {\n const old = this._value\n this._value = v\n this._formAssociation.sync()\n this.requestUpdate('value', old)\n }\n\n @query('.editor') private _editor?: HTMLTextAreaElement\n @query('.text') private _textLayer?: HTMLElement\n\n private _editing = false\n private _editBase = ''\n private _pendingCaret: number | null = null\n private _refocusing = false\n private _resizeObserver: ResizeObserver | null = null\n private readonly _fieldId = createFieldId('web-ui-editable-text')\n\n private get _isDisabled(): boolean {\n return this.disabled || this._formAssociation.isFormDisabled()\n }\n\n private readonly _formAssociation = defineFormAssociation<string>({\n host: this,\n getState: () => this._value,\n setState: value => {\n this.value = value\n },\n getFormValue: () => this._value,\n getFormState: () => this._value,\n restoreState: state => {\n if (typeof state === 'string') this.value = state\n }\n }).make()\n\n private readonly _formAssociationController = new FormAssociationController(this, this._formAssociation)\n\n constructor() {\n super()\n // focus 不冒泡:宿主监听只在焦点从组件外进入时触发,shadow 内聚焦不会误触\n this.addEventListener('focus', this._onHostFocus)\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n /*\n * 卸载没有用户动作作为提交意图(`Enter` / `blur` 都有),所以判据是草稿与进入编辑时的\n * 值是否不同,而不是「是否提交过」——Escape 会把值恢复成 `_editBase`,但那是取消,与\n * 本次卸载无关;一个只聚焦了没打字的会话也不该在消失时凭空产生一次 change。\n *\n * `_isDisabled` 与 `readonly` 两道守卫都与 `_onBlur` 对称:两者都不派发提交事件。`disabled`\n * 必须显式判——`disabled = true` 与 `remove()` 在同一任务里执行时 Lit 更新是微任务,\n * `updated()` 还没跑到(禁用态退出编辑那条分支尚未执行),此时组件仍在编辑态,不判就会\n * 派发一条 blur 路径明确拒绝的 change。`readonly` 不能靠「草稿恒等于基线」兜住:可编辑态\n * 进入并起草稿之后属性才翻成 readonly 的会话,基线仍是进入编辑时的旧值,两条判据都成立\n * 却不该提交(草稿在这次卸载中丢失,理由见 property 的 JSDoc)。\n */\n if (\n this.commitOnUnmount &&\n this._editing &&\n !this._isDisabled &&\n !this.readonly &&\n this._value !== this._editBase\n ) {\n this._commitEditing(false)\n }\n this._releaseEditingKeys()\n this._teardownAutosize()\n }\n\n override connectedCallback() {\n super.connectedCallback()\n /*\n * autosize 的 ResizeObserver 在 disconnectedCallback 被拆掉,搭建若只放在\n * firstUpdated(一生一次),元素断开再重连后 RO 永久丢失、autosize 静默失效。\n * connectedCallback 每次连接都跑:_setupAutosize 自带 teardown,重复进入无副作用。\n * 首连接时渲染尚未发生(_editor 为 null),_autosizeEditor 与 RO 回调都有判空守卫。\n */\n this._setupAutosize()\n // 同类缺口:编辑态被断开时 _releaseEditingKeys 撤了 window 监听,重连后 _editing\n // 仍为 true(blur 取消未必先于断开到达),此时补领,避免重连后按键失灵。\n if (this._editing) this._claimEditingKeys()\n }\n\n override updated() {\n // disabled 时移出 tab 序列;编辑态由 editing attribute 表达,二者互斥\n if (this._isDisabled) this.removeAttribute('tabindex')\n else this.setAttribute('tabindex', '0')\n // 禁用态生效时(属性或 fieldset)退出编辑:值保持当前草稿,不派发提交事件\n if (this._editing && this._isDisabled) this._exitEditing()\n this._autosizeEditor()\n }\n\n private _onHostFocus() {\n if (this._refocusing) {\n this._refocusing = false\n return\n }\n this._enterEditing()\n }\n\n /**\n * pointerdown 时文本层仍是命中目标,此时记录落点;浏览器随后的默认聚焦会触发\n * host focus 进入编辑,偏移在那里应用到 textarea。\n */\n private _onPointerDown(e: PointerEvent) {\n if (this._isDisabled || this._editing || !this._textLayer) return\n this._pendingCaret = caretOffsetFromPoint(this._textLayer, e.clientX, e.clientY)\n }\n\n /** mousedown 默认聚焦被拦截等场景由 click 兜底进入编辑。 */\n private _onClick() {\n if (this._isDisabled || this._editing) return\n this._enterEditing()\n }\n\n /**\n * 公共 API:进入编辑态并全选内容;已在编辑态时只重新全选。\n *\n * `disabled` 时不进入编辑,与 `focus()` 一致。\n */\n select() {\n if (this._isDisabled) return\n if (!this._editing) this._enterEditing()\n const editor = this._editor\n // 全选:末端显式取当前文本长度,覆盖 _enterEditing 落下的光标位置\n editor?.setSelectionRange(0, editor.value.length)\n }\n\n private _enterEditing() {\n if (this._isDisabled || this._editing) return\n this._editing = true\n this._editBase = this._value\n this.toggleAttribute('editing', true)\n this._claimEditingKeys()\n const editor = this._editor\n if (!editor) return\n editor.focus()\n // Tab / 编程式聚焦光标到末尾;指针落点用 pointerdown 记下的偏移\n const offset = this._pendingCaret ?? this._value.length\n this._pendingCaret = null\n const clamped = Math.max(0, Math.min(offset, editor.value.length))\n editor.setSelectionRange(clamped, clamped)\n }\n\n private _exitEditing() {\n if (!this._editing) return\n this._editing = false\n this._releaseEditingKeys()\n this.toggleAttribute('editing', false)\n }\n\n private _onInput() {\n // 原生 input 已 composed 冒泡出 shadow root 且 target 重定向到宿主,无需补发\n const editor = this._editor\n if (this._isDisabled || this.readonly) {\n if (editor && editor.value !== this._value) editor.value = this._value\n return\n }\n this._value = editor?.value ?? ''\n this._formAssociation.sync()\n this._autosizeEditor()\n }\n\n /**\n * blur 提交:与 Enter 同一条提交路径,草稿成为新值并派发 change。\n *\n * 不交还焦点:焦点已被用户移走,抢回宿主会吞掉消费者的目标焦点。\n */\n private _onBlur() {\n if (!this._editing || this._isDisabled) return\n if (this.readonly) {\n this._exitEditing()\n return\n }\n this._commitEditing(false)\n }\n\n /**\n * 提交:草稿成为新值、退出编辑,并恰好派发一次 change。Enter 与 blur 同走这一条路。\n *\n * `returnFocus` 区分两种来路:Enter 主动结束时把焦点交还宿主(标记防重入编辑);\n * blur 是被动失焦,焦点已被用户移走,再抢回宿主会吞掉消费者的目标焦点。\n * 先退出编辑再动焦点:退出时 textarea 隐藏触发的 blur 看到 _editing 已为 false,\n * 不会把这次提交误判成又一次 blur 提交。\n */\n private _commitEditing(returnFocus: boolean) {\n if (!this._editing) return\n this._exitEditing()\n if (returnFocus) this._returnFocusToHost()\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n }\n\n /**\n * 取消:恢复进入编辑时的值并派发 cancel。仅 Escape 走这一条路径。\n *\n * 调用方(Escape 分支)随后才交还焦点:本方法先摘除编辑态,焦点迁移触发的 blur\n * 到达时 `_editing` 已为 false 而早退,不会把取消误判成 blur 提交。\n */\n private _cancelEditing() {\n if (!this._editing) return\n this._value = this._editBase\n this._formAssociation.sync()\n this._exitEditing()\n /*\n * 重入守卫先于 dispatch 置位:cancel 同步派发,消费者可能在监听器里调\n * el.focus()——那是一次真实的宿主 focus,没有守卫就会重新进入编辑,随后一次\n * blur 便把恢复后的原值当成新草稿提交,用户什么都没改却收到 change。守卫在此\n * 置位后即被该次 focus 消费,与取消后由组件自己交还焦点走同一条早退路径。\n */\n this._refocusing = true\n /*\n * cancel 与原生 <dialog> 的 cancel 同名,而 dialog 的关闭管线就监听这个事件名。\n * 组件被消费方投映进 overlay shadow(drawer/dialog 标题)时,任何冒泡的 cancel——\n * composed 与否都一样,slot 都会把事件带进 shadow 树——都会被那个 dialog 当成\n * 一次关闭请求:issue #159 的实测症状是 drawer 标题聚焦编辑时按 Escape,或点到\n * drawer 别处触发 blur,drawer 跟着编辑一起关。因此只在宿主上派发,不冒泡不组合;\n * 监听挂在组件本身即可(vue @cancel、React addEventListener 均不受影响)。\n * 手法同 checkbox/radio 受管时的事件策略(见各自 handleClick 注释)。\n */\n this.dispatchEvent(new Event('cancel', { bubbles: false, composed: false }))\n }\n\n /**\n * 焦点交还宿主:先标记再聚焦,宿主 focus 到达时标记已消费,不会再次进入编辑;\n * 标记在微任务里清掉,此后的聚焦照常进入编辑。\n */\n private _returnFocusToHost() {\n this._refocusing = true\n this.focus()\n queueMicrotask(() => {\n this._refocusing = false\n })\n }\n\n /**\n * 编辑态按键归属:Enter 提交、Escape 取消。\n *\n * 监听挂在 window 捕获阶段而不是 shadow 内的 textarea 上:消费方(overlay 仲裁者)在\n * document 捕获阶段收 Escape,组件若只在 textarea 上处理,按键先被仲裁者收走,\n * 于是一次 Escape 会同时取消编辑和关闭外层 overlay。window 在 document 上游,\n * stopPropagation 只有挂在这里才拦得住仲裁者。\n */\n private _claimEditingKeys() {\n window.addEventListener('keydown', this._onEditingKeydown, true)\n }\n\n private _releaseEditingKeys() {\n window.removeEventListener('keydown', this._onEditingKeydown, true)\n }\n\n private readonly _onEditingKeydown = (e: KeyboardEvent) => {\n // 输入法组合期间的 Enter/Escape 属于组合会话,交给 IME\n if (e.isComposing) return\n if (!e.composedPath().includes(this)) return\n if (e.key === 'Escape') {\n // 按键本身也被编辑层消费:外层 overlay 不应收到同一次 Escape\n e.preventDefault()\n e.stopPropagation()\n // 先取消(内部摘除编辑态)再交还焦点:focus 触发的 blur 看到 _editing 已为\n // false 而早退,不会把这次取消误判成 blur 提交\n this._cancelEditing()\n this._returnFocusToHost()\n return\n }\n if (e.key === 'Enter') {\n /*\n * Enter 无论是否只读都属于编辑层,消费策略与 Escape 对称:preventDefault\n * 压掉默认行为,stopPropagation 阻止外层表单隐式提交或 overlay 监听收到按键。\n * 只读态只是没有草稿可提交,因此消费后直接返回,不退出编辑、不派发 change。\n */\n e.preventDefault()\n e.stopPropagation()\n if (this.readonly) return\n /*\n * 可编辑态的 Enter 随后提交草稿;外层表单的隐式提交与 overlay 监听不应收到\n * 同一次按键——正如 Escape 属于取消。\n */\n this._commitEditing(true)\n }\n }\n\n private _setupAutosize() {\n this._teardownAutosize()\n // jsdom 等无 ResizeObserver 的环境退化为按渲染 autosize,够用且不抛错\n if (typeof ResizeObserver === 'undefined') {\n this._autosizeEditor()\n return\n }\n this._resizeObserver = new ResizeObserver(() => this._autosizeEditor())\n this._resizeObserver.observe(this)\n this._autosizeEditor()\n }\n\n private _teardownAutosize() {\n this._resizeObserver?.disconnect()\n this._resizeObserver = null\n this._editor?.style.removeProperty('height')\n }\n\n /**\n * 编辑层高度始终跟随自身内容,不依赖文本层折出的行盒。\n *\n * 编辑层绝对定位并用 top/bottom 撑高,`height: auto` 量到的是包含块高度而不是内容高;\n * 先归零再读 scrollHeight,拿到的才是自身内容的高度(含 1px 内边距)。\n * 观察宿主而非编辑层:编辑层在文档流外,改它的高度不会反过来改动宿主尺寸。\n */\n private _autosizeEditor() {\n const editor = this._editor\n if (!editor) return\n const previous = editor.style.height\n editor.style.height = '0px'\n const height = editor.scrollHeight\n editor.style.height = height > 0 ? `${height}px` : previous\n }\n\n private get _displayText(): string {\n if (this._value !== '') return this._value\n if (this.placeholder !== '') return this.placeholder\n return BLANK_GLYPH\n }\n\n override render() {\n return html`\n <div class=\"layers\" @pointerdown=${this._onPointerDown} @click=${this._onClick}>\n <span class=${classMap({ text: true, placeholder: this._value === '' })}>${this._displayText}</span>\n <textarea\n id=${this._fieldId}\n class=\"editor\"\n .value=${this._value}\n .placeholder=${this.placeholder}\n ?disabled=${this._isDisabled}\n ?readonly=${this.readonly}\n aria-label=${ifDefined(this.ariaLabel)}\n @input=${this._onInput}\n @blur=${this._onBlur}\n ></textarea>\n </div>\n `\n }\n\n declare readonly $events: {\n input: Event\n change: Event\n cancel: Event\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-editable-text': WebUiEditableText\n }\n}\n"],"mappings":";;;;;;;;;AAWA,IAAM,IAAc,KAGd,IAAkB;AAQxB,SAAS,EAAW,GAAiC;CACnD,KAAK,IAAM,KAAS,EAAM,YACxB,IAAI,aAAiB,MAAM,OAAO;CAEpC,OAAO;AACT;AASA,SAAS,EAAqB,GAAwB,GAAW,GAAmB;CAClF,IAAM,IAAO,EAAW,CAAS;CACjC,IAAI,CAAC,GAAM,OAAO;CAClB,IAAM,IAAS,EAAK,KAAK,QACnB,IAAQ,SAAS,YAAY,GAC7B,KAAU,OACd,EAAM,SAAS,GAAM,CAAM,GAC3B,EAAM,OAAO,GAAM,CAAM,GAClB,EAAM,sBAAsB,IAK/B,IAAmB,CAAC;CAC1B,KAAK,IAAM,KAAQ,EAAU,eAAe,GAC1C,AAAK,EAAM,MAAK,MAAQ,KAAK,IAAI,EAAK,MAAM,EAAK,GAAG,KAAK,CAAe,KAAG,EAAM,KAAK,CAAI;CAE5F,IAAI,EAAM,WAAW,GAAG,OAAO;CAC/B,IAAI,IAAO,EAAM,IACb,IAAU;CACd,KAAK,IAAM,KAAa,GAAO;EAC7B,IAAM,IAAW,KAAK,KAAK,EAAU,MAAM,EAAU,UAAU,IAAI,CAAC;EACpE,AAAI,KAAY,MACd,IAAU,GACV,IAAO;CAEX;CAGA,IAAI,IAAK,GACL,IAAK;CACT,OAAO,IAAK,IAAI;EACd,IAAM,IAAO,IAAK,KAAO;EACzB,AAAI,EAAO,CAAG,CAAC,CAAC,OAAO,EAAK,MAAM,IAAiB,IAAK,IACnD,IAAK,IAAM;CAClB;CACA,IAAM,IAAY;CAIlB,KAFA,IAAK,GACL,IAAK,GACE,IAAK,IAAI;EACd,IAAM,IAAO,IAAK,IAAK,KAAM;EAC7B,AAAI,EAAO,CAAG,CAAC,CAAC,OAAO,EAAK,MAAM,IAAiB,IAAK,IACnD,IAAK,IAAM;CAClB;CACA,IAAM,IAAU,KAAK,IAAI,GAAI,CAAS;CAItC,KAFA,IAAK,GACL,IAAK,GACE,IAAK,IAAI;EACd,IAAM,IAAO,IAAK,IAAK,KAAM;EAC7B,AAAI,EAAO,CAAG,CAAC,CAAC,QAAQ,IAAI,IAAiB,IAAK,IAC7C,IAAK,IAAM;CAClB;CACA,OAAO;AACT;AAGO,IAAM,IAAN,cAAgC,EAAe,CAAU,CAAC,CAAC;;EACvC,KAAA,SAAA,CAAC,EAAU,CAAK,CAAC;;CAuD1C,IACI,QAAgB;EAClB,OAAO,KAAK;CACd;CAEA,IAAI,MAAM,GAAW;EACnB,IAAM,IAAM,KAAK;EAGjB,AAFA,KAAK,SAAS,GACd,KAAK,iBAAiB,KAAK,GAC3B,KAAK,cAAc,SAAS,CAAG;CACjC;CAYA,IAAY,cAAuB;EACjC,OAAO,KAAK,YAAY,KAAK,iBAAiB,eAAe;CAC/D;CAiBA,cAAc;EAGZ,AAFA,MAAM,GA/F0C,KAAA,OAAA,IACO,KAAA,cAAA,IACF,KAAA,WAAA,IACA,KAAA,WAAA,IAwCuC,KAAA,kBAAA,IAEL,KAAA,YAAA,MAE/D,KAAA,SAAA,IAqBP,KAAA,WAAA,IACC,KAAA,YAAA,IACmB,KAAA,gBAAA,MACjB,KAAA,cAAA,IAC2B,KAAA,kBAAA,MACrB,KAAA,WAAA,EAAc,sBAAsB,GAM5B,KAAA,mBAAA,EAA8B;GAChE,MAAM;GACN,gBAAgB,KAAK;GACrB,WAAU,MAAS;IACjB,KAAK,QAAQ;GACf;GACA,oBAAoB,KAAK;GACzB,oBAAoB,KAAK;GACzB,eAAc,MAAS;IACrB,AAAI,OAAO,KAAU,aAAU,KAAK,QAAQ;GAC9C;EACF,CAAC,CAAC,CAAC,KAAK,GAEsC,KAAA,6BAAA,IAAI,EAA0B,MAAM,KAAK,gBAAgB,GAwNjE,KAAA,qBAAA,MAAqB;GAErD,OAAE,eACD,EAAE,aAAa,CAAC,CAAC,SAAS,IAAI,GACnC;QAAI,EAAE,QAAQ,UAAU;KAOtB,AALA,EAAE,eAAe,GACjB,EAAE,gBAAgB,GAGlB,KAAK,eAAe,GACpB,KAAK,mBAAmB;KACxB;IACF;IACA,IAAI,EAAE,QAAQ,SAAS;KAQrB,IAFA,EAAE,eAAe,GACjB,EAAE,gBAAgB,GACd,KAAK,UAAU;KAKnB,KAAK,eAAe,EAAI;IAC1B;GAfA;EAgBF,GAhPE,KAAK,iBAAiB,SAAS,KAAK,YAAY;CAClD;CAEA,uBAAgC;EAwB9B,AAvBA,MAAM,qBAAqB,GAczB,KAAK,mBACL,KAAK,YACL,CAAC,KAAK,eACN,CAAC,KAAK,YACN,KAAK,WAAW,KAAK,aAErB,KAAK,eAAe,EAAK,GAE3B,KAAK,oBAAoB,GACzB,KAAK,kBAAkB;CACzB;CAEA,oBAA6B;EAW3B,AAVA,MAAM,kBAAkB,GAOxB,KAAK,eAAe,GAGhB,KAAK,YAAU,KAAK,kBAAkB;CAC5C;CAEA,UAAmB;EAMjB,AAJI,KAAK,cAAa,KAAK,gBAAgB,UAAU,IAChD,KAAK,aAAa,YAAY,GAAG,GAElC,KAAK,YAAY,KAAK,eAAa,KAAK,aAAa,GACzD,KAAK,gBAAgB;CACvB;CAEA,eAAuB;EACrB,IAAI,KAAK,aAAa;GACpB,KAAK,cAAc;GACnB;EACF;EACA,KAAK,cAAc;CACrB;CAMA,eAAuB,GAAiB;EAClC,KAAK,eAAe,KAAK,YAAY,CAAC,KAAK,eAC/C,KAAK,gBAAgB,EAAqB,KAAK,YAAY,EAAE,SAAS,EAAE,OAAO;CACjF;CAGA,WAAmB;EACb,KAAK,eAAe,KAAK,YAC7B,KAAK,cAAc;CACrB;CAOA,SAAS;EACP,IAAI,KAAK,aAAa;EACtB,AAAK,KAAK,YAAU,KAAK,cAAc;EACvC,IAAM,IAAS,KAAK;EAEpB,GAAQ,kBAAkB,GAAG,EAAO,MAAM,MAAM;CAClD;CAEA,gBAAwB;EACtB,IAAI,KAAK,eAAe,KAAK,UAAU;EAIvC,AAHA,KAAK,WAAW,IAChB,KAAK,YAAY,KAAK,QACtB,KAAK,gBAAgB,WAAW,EAAI,GACpC,KAAK,kBAAkB;EACvB,IAAM,IAAS,KAAK;EACpB,IAAI,CAAC,GAAQ;EACb,EAAO,MAAM;EAEb,IAAM,IAAS,KAAK,iBAAiB,KAAK,OAAO;EACjD,KAAK,gBAAgB;EACrB,IAAM,IAAU,KAAK,IAAI,GAAG,KAAK,IAAI,GAAQ,EAAO,MAAM,MAAM,CAAC;EACjE,EAAO,kBAAkB,GAAS,CAAO;CAC3C;CAEA,eAAuB;EAChB,KAAK,aACV,KAAK,WAAW,IAChB,KAAK,oBAAoB,GACzB,KAAK,gBAAgB,WAAW,EAAK;CACvC;CAEA,WAAmB;EAEjB,IAAM,IAAS,KAAK;EACpB,IAAI,KAAK,eAAe,KAAK,UAAU;GACrC,AAAI,KAAU,EAAO,UAAU,KAAK,WAAQ,EAAO,QAAQ,KAAK;GAChE;EACF;EAGA,AAFA,KAAK,SAAS,GAAQ,SAAS,IAC/B,KAAK,iBAAiB,KAAK,GAC3B,KAAK,gBAAgB;CACvB;CAOA,UAAkB;EACZ,IAAC,KAAK,YAAY,MAAK,aAC3B;OAAI,KAAK,UAAU;IACjB,KAAK,aAAa;IAClB;GACF;GACA,KAAK,eAAe,EAAK;EADzB;CAEF;CAUA,eAAuB,GAAsB;EACtC,KAAK,aACV,KAAK,aAAa,GACd,KAAa,KAAK,mBAAmB,GACzC,KAAK,cAAc,IAAI,MAAM,UAAU;GAAE,SAAS;GAAM,UAAU;EAAK,CAAC,CAAC;CAC3E;CAQA,iBAAyB;EAClB,KAAK,aACV,KAAK,SAAS,KAAK,WACnB,KAAK,iBAAiB,KAAK,GAC3B,KAAK,aAAa,GAOlB,KAAK,cAAc,IAUnB,KAAK,cAAc,IAAI,MAAM,UAAU;GAAE,SAAS;GAAO,UAAU;EAAM,CAAC,CAAC;CAC7E;CAMA,qBAA6B;EAG3B,AAFA,KAAK,cAAc,IACnB,KAAK,MAAM,GACX,qBAAqB;GACnB,KAAK,cAAc;EACrB,CAAC;CACH;CAUA,oBAA4B;EAC1B,OAAO,iBAAiB,WAAW,KAAK,mBAAmB,EAAI;CACjE;CAEA,sBAA8B;EAC5B,OAAO,oBAAoB,WAAW,KAAK,mBAAmB,EAAI;CACpE;CAiCA,iBAAyB;EAGvB,IAFA,KAAK,kBAAkB,GAEnB,OAAO,iBAAmB,KAAa;GACzC,KAAK,gBAAgB;GACrB;EACF;EAGA,AAFA,KAAK,kBAAkB,IAAI,qBAAqB,KAAK,gBAAgB,CAAC,GACtE,KAAK,gBAAgB,QAAQ,IAAI,GACjC,KAAK,gBAAgB;CACvB;CAEA,oBAA4B;EAG1B,AAFA,KAAK,iBAAiB,WAAW,GACjC,KAAK,kBAAkB,MACvB,KAAK,SAAS,MAAM,eAAe,QAAQ;CAC7C;CASA,kBAA0B;EACxB,IAAM,IAAS,KAAK;EACpB,IAAI,CAAC,GAAQ;EACb,IAAM,IAAW,EAAO,MAAM;EAC9B,EAAO,MAAM,SAAS;EACtB,IAAM,IAAS,EAAO;EACtB,EAAO,MAAM,SAAS,IAAS,IAAI,GAAG,EAAO,MAAM;CACrD;CAEA,IAAY,eAAuB;EAGjC,OAFI,KAAK,WAAW,KAChB,KAAK,gBAAgB,KAClB,IAD6B,KAAK,cADV,KAAK;CAGtC;CAEA,SAAkB;EAChB,OAAO,CAAI;yCAC0B,KAAK,eAAe,UAAU,KAAK,SAAS;sBAC/D,EAAS;GAAE,MAAM;GAAM,aAAa,KAAK,WAAW;EAAG,CAAC,EAAE,GAAG,KAAK,aAAa;;eAEtF,KAAK,SAAS;;mBAEV,KAAK,OAAO;yBACN,KAAK,YAAY;sBACpB,KAAK,YAAY;sBACjB,KAAK,SAAS;uBACb,EAAU,KAAK,SAAS,EAAE;mBAC9B,KAAK,SAAS;kBACf,KAAK,QAAQ;;;;CAI7B;AAOF;AAnZG,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GACxC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,eAAA,KAAA,CAAA,GACxC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GACzC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GAwCzC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAS,SAAS;CAAM,WAAW;AAAoB,CAAC,CAAA,GAAA,EAAA,WAAA,mBAAA,KAAA,CAAA,GAEzE,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,WAAW;AAAa,CAAC,CAAA,GAAA,EAAA,WAAA,aAAA,KAAA,CAAA,GAElD,EAAA,CAAA,EAAM,CAAA,GAAA,EAAA,WAAA,UAAA,KAAA,CAAA,GAMN,EAAA,CAAA,EAAS,EAAE,MAAM,OAAO,CAAC,CAAA,GAAA,EAAA,WAAA,SAAA,IAAA,GAYzB,EAAA,CAAA,EAAM,SAAS,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GACf,EAAA,CAAA,EAAM,OAAO,CAAA,GAAA,EAAA,WAAA,cAAA,KAAA,CAAA,GAtEf,IAAA,EAAA,CAAA,EAAc,sBAAsB,CAAA,GAAA,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"style.js","names":[],"sources":["../../../src/components/editable-text/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n/* 组件对外表现为同级 span:行内级盒子,排版与颜色全部继承外部上下文。\n :host 只设 display(ADR-0005),其余样式在 shadow root 内。 */\n:host {\n display: inline-block;\n}\n\n/* 文本层与编辑层共用的排版盒:编辑层 absolute 定位覆盖它,切换只改可见性,\n 因此文字态与编辑态逐像素同盒。 */\n.layers {\n position: relative;\n display: block;\n}\n\n.text,\n.editor {\n /* 长无空格串两层都必须能断:文本层若只继承宿主默认的 normal,长字母数字串不\n 截断、直接溢出宿主盒(文字态),而编辑层在盒内裁剪(编辑态),两层断行\n 不一致。默认 anywhere:仅在单词放不下时断,且参与 min-content 计算,收缩\n 上下文(flex 项 min-width:0)里长串也不会撑爆宿主;两层取同一变量,外部\n 覆盖时仍保持一致。 */\n overflow-wrap: var(--wui-editable-text-overflow-wrap, anywhere);\n\n /* 多行编辑要求换行符成行,但 pre-wrap 只能落在文本层上:模板里元素之间的空白\n 文本节点若被 pre-wrap 保留,会各自生成幻影行盒,把文本层顶偏、盒高翻倍,\n 文字态与编辑态随之不再同盒。这里保持 .layers 为默认 normal 让空白折叠,\n 两层取同一变量,外部覆盖(如 nowrap)时仍保持一致。 */\n white-space: var(--wui-editable-text-white-space, pre-wrap);\n}\n\n.text {\n display: inline;\n}\n\n.placeholder {\n color: var(--wui-color-text-secondary, #6a6a6a);\n}\n\n/* 原生控件 reset:不贡献尺寸、无边框背景描边、不可手拖改变大小 */\n.editor {\n resize: none;\n\n position: absolute;\n\n /* 四边各外扩 1px,再用 1px 内边距把文本起点与换行宽度拉回内容盒。\n 两层必须同盒,但盒的边界同时也是原生控件的裁剪边界:文本层的字形会按\n 边距溢出内容盒半个像素左右,紧凑行高下 textarea 内部内容也会比内容盒高\n 出 1px。裁剪盒若与内容盒重合,这些墨色只存在于文字态,编辑态被裁掉或顶偏,\n 两态逐像素比对就对不上;外扩 1px 后编辑层的裁剪盒覆盖文本层的墨色范围。 */\n inset: -1px;\n\n overflow: hidden;\n\n margin: 0;\n padding: 1px;\n border: 0;\n\n /* 原生控件不继承排版:显式继承让编辑层与文本层共用同一套字体度量 */\n font: inherit;\n line-height: inherit;\n color: inherit;\n text-align: inherit;\n text-transform: inherit;\n letter-spacing: inherit;\n word-break: inherit;\n\n appearance: none;\n visibility: hidden;\n background: transparent;\n outline: none;\n\n /* 光标颜色组件级默认:跟随共享语义 token,消费方在宿主或任意祖先上设\n --wui-color-accent 即可覆盖(自定义属性穿透 shadow 边界继承);\n 此前只有 interweave 页面级设置,demo 等消费方拿到的是浏览器默认黑。 */\n caret-color: var(--wui-color-accent, #08f);\n\n &::placeholder {\n color: var(--wui-color-text-secondary, #6a6a6a);\n }\n}\n\n/* 可见性切换:文字态隐藏编辑层、编辑态显露;盒模型、滚动与换行不随之变化 */\n:host([editing]) .editor {\n /* 空内容时宿主可能被收缩到 0 宽:行内级盒子配上零宽占位符,收缩上下文(flex 项\n min-width:0、表格单元格等)里就量不到宽度,编辑层随之没有可绘制光标的地盘,\n 清空全部内容后光标消失。留一个字符宽,保证空草稿下光标仍然可见。\n 规则落在 shadow 内:宿主上的 min-width 属于外层文档样式,会盖过 :host 声明。 */\n min-width: 1ch;\n visibility: visible;\n}\n\n/* 编辑态隐藏文本层:两层同盒同排版,若同时可见会把同一份文字画两遍,\n 抗锯齿边缘的墨色会加重,破坏文字态/编辑态的逐像素一致。 */\n:host([editing]) .text {\n visibility: hidden;\n}\n\n:host([disabled]) {\n cursor: not-allowed;\n}\n"],"mappings":""}
1
+ {"version":3,"file":"style.js","names":[],"sources":["../../../src/components/editable-text/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n/* 组件对外表现为同级 span:行内级盒子,排版与颜色全部继承外部上下文。\n :host 只设 display(ADR-0005),其余样式在 shadow root 内。 */\n:host {\n display: inline-block;\n}\n\n/* 文本层与编辑层共用的排版盒:编辑层 absolute 定位覆盖它,切换只改可见性,\n 因此文字态与编辑态逐像素同盒。 */\n.layers {\n position: relative;\n display: block;\n}\n\n.text,\n.editor {\n /* 长无空格串两层都必须能断:文本层若只继承宿主默认的 normal,长字母数字串不\n 截断、直接溢出宿主盒(文字态),而编辑层在盒内裁剪(编辑态),两层断行\n 不一致。默认 anywhere:仅在单词放不下时断,且参与 min-content 计算,收缩\n 上下文(flex 项 min-width:0)里长串也不会撑爆宿主;两层取同一变量,外部\n 覆盖时仍保持一致。 */\n overflow-wrap: var(--wui-editable-text-overflow-wrap, anywhere);\n\n /* 多行编辑要求换行符成行,但 pre-wrap 只能落在文本层上:模板里元素之间的空白\n 文本节点若被 pre-wrap 保留,会各自生成幻影行盒,把文本层顶偏、盒高翻倍,\n 文字态与编辑态随之不再同盒。这里保持 .layers 为默认 normal 让空白 collapse,\n 两层取同一变量,外部覆盖(如 nowrap)时仍保持一致。 */\n white-space: var(--wui-editable-text-white-space, pre-wrap);\n}\n\n.text {\n display: inline;\n}\n\n.placeholder {\n color: var(--wui-color-text-secondary, #6a6a6a);\n}\n\n/* 原生控件 reset:不贡献尺寸、无边框背景描边、不可手拖改变大小 */\n.editor {\n resize: none;\n\n position: absolute;\n\n /* 四边各外扩 1px,再用 1px 内边距把文本起点与换行宽度拉回内容盒。\n 两层必须同盒,但盒的边界同时也是原生控件的裁剪边界:文本层的字形会按\n 边距溢出内容盒半个像素左右,紧凑行高下 textarea 内部内容也会比内容盒高\n 出 1px。裁剪盒若与内容盒重合,这些墨色只存在于文字态,编辑态被裁掉或顶偏,\n 两态逐像素比对就对不上;外扩 1px 后编辑层的裁剪盒覆盖文本层的墨色范围。 */\n inset: -1px;\n\n overflow: hidden;\n\n margin: 0;\n padding: 1px;\n border: 0;\n\n /* 原生控件不继承排版:显式继承让编辑层与文本层共用同一套字体度量 */\n font: inherit;\n line-height: inherit;\n color: inherit;\n text-align: inherit;\n text-transform: inherit;\n letter-spacing: inherit;\n word-break: inherit;\n\n appearance: none;\n visibility: hidden;\n background: transparent;\n outline: none;\n\n /* 光标颜色组件级默认:跟随共享语义 token,消费方在宿主或任意祖先上设\n --wui-color-accent 即可覆盖(自定义属性穿透 shadow 边界继承);\n 此前只有 interweave 页面级设置,demo 等消费方拿到的是浏览器默认黑。 */\n caret-color: var(--wui-color-accent, #08f);\n\n &::placeholder {\n color: var(--wui-color-text-secondary, #6a6a6a);\n }\n}\n\n/* 可见性切换:文字态隐藏编辑层、编辑态显露;盒模型、滚动与换行不随之变化 */\n:host([editing]) .editor {\n /* 空内容时宿主可能被收缩到 0 宽:行内级盒子配上零宽 placeholder,收缩上下文(flex 项\n min-width:0、表格单元格等)里就量不到宽度,编辑层随之没有可绘制光标的地盘,\n 清空全部内容后光标消失。留一个字符宽,保证空草稿下光标仍然可见。\n 规则落在 shadow 内:宿主上的 min-width 属于外层文档样式,会盖过 :host 声明。 */\n min-width: 1ch;\n visibility: visible;\n}\n\n/* 编辑态隐藏文本层:两层同盒同排版,若同时可见会把同一份文字画两遍,\n 抗锯齿边缘的墨色会加重,破坏文字态/编辑态的逐像素一致。 */\n:host([editing]) .text {\n visibility: hidden;\n}\n\n:host([disabled]) {\n cursor: not-allowed;\n}\n"],"mappings":""}