@greypan/web-ui 6.1.0 → 6.2.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 (129) hide show
  1. package/README.CN.md +209 -80
  2. package/README.md +189 -60
  3. package/dist/assets/menu-portal.js +1 -1
  4. package/dist/assets/menu-portal.js.map +1 -1
  5. package/dist/assets/overlay-motion.js +1 -1
  6. package/dist/assets/overlay-motion.js.map +1 -1
  7. package/dist/components/autocomplete/index.js +54 -53
  8. package/dist/components/autocomplete/index.js.map +1 -1
  9. package/dist/components/autocomplete/style.js +1 -1
  10. package/dist/components/autocomplete/style.js.map +1 -1
  11. package/dist/components/button/index.js +36 -32
  12. package/dist/components/button/index.js.map +1 -1
  13. package/dist/components/button/style.js +1 -1
  14. package/dist/components/button/style.js.map +1 -1
  15. package/dist/components/collapse/index.d.ts +29 -1
  16. package/dist/components/collapse/index.js +92 -30
  17. package/dist/components/collapse/index.js.map +1 -1
  18. package/dist/components/collapse/style.js +1 -1
  19. package/dist/components/collapse/style.js.map +1 -1
  20. package/dist/components/context-menu/index.d.ts +1 -0
  21. package/dist/components/context-menu/index.js +80 -76
  22. package/dist/components/context-menu/index.js.map +1 -1
  23. package/dist/components/dialog/index.js +6 -2
  24. package/dist/components/dialog/index.js.map +1 -1
  25. package/dist/components/dialog/style.js +1 -1
  26. package/dist/components/dialog/style.js.map +1 -1
  27. package/dist/components/drawer/index.js +24 -14
  28. package/dist/components/drawer/index.js.map +1 -1
  29. package/dist/components/drawer/style.js +1 -1
  30. package/dist/components/drawer/style.js.map +1 -1
  31. package/dist/components/dropdown/index.d.ts +1 -0
  32. package/dist/components/dropdown/index.js +65 -60
  33. package/dist/components/dropdown/index.js.map +1 -1
  34. package/dist/components/image-preview/index.d.ts +58 -0
  35. package/dist/components/image-preview/index.js +481 -0
  36. package/dist/components/image-preview/index.js.map +1 -0
  37. package/dist/components/image-preview/style.js +6 -0
  38. package/dist/components/image-preview/style.js.map +1 -0
  39. package/dist/components/index.d.ts +1 -0
  40. package/dist/components/index.js +20 -19
  41. package/dist/components/input/style.js +1 -1
  42. package/dist/components/input/style.js.map +1 -1
  43. package/dist/components/input-number/style.js +1 -1
  44. package/dist/components/input-number/style.js.map +1 -1
  45. package/dist/components/layout/index.d.ts +1 -1
  46. package/dist/components/layout/index.js +5 -3
  47. package/dist/components/layout/index.js.map +1 -1
  48. package/dist/components/layout/style.js +1 -1
  49. package/dist/components/layout/style.js.map +1 -1
  50. package/dist/components/popover/index.d.ts +1 -0
  51. package/dist/components/popover/index.js +53 -39
  52. package/dist/components/popover/index.js.map +1 -1
  53. package/dist/components/popover/style.js.map +1 -1
  54. package/dist/components/radio/index.js +18 -16
  55. package/dist/components/radio/index.js.map +1 -1
  56. package/dist/components/radio/style.js +1 -1
  57. package/dist/components/radio/style.js.map +1 -1
  58. package/dist/components/segmented/index.js +24 -22
  59. package/dist/components/segmented/index.js.map +1 -1
  60. package/dist/components/segmented/style.js +1 -1
  61. package/dist/components/segmented/style.js.map +1 -1
  62. package/dist/components/segmented-trigger/index.js +17 -15
  63. package/dist/components/segmented-trigger/index.js.map +1 -1
  64. package/dist/components/segmented-trigger/style.js +1 -1
  65. package/dist/components/segmented-trigger/style.js.map +1 -1
  66. package/dist/components/select/index.js +43 -38
  67. package/dist/components/select/index.js.map +1 -1
  68. package/dist/components/select/style.js +1 -1
  69. package/dist/components/select/style.js.map +1 -1
  70. package/dist/components/slider/index.js +32 -30
  71. package/dist/components/slider/index.js.map +1 -1
  72. package/dist/components/slider/style.js +1 -1
  73. package/dist/components/slider/style.js.map +1 -1
  74. package/dist/components/svg-draw-lines/index.js +1 -1
  75. package/dist/components/svg-draw-lines/index.js.map +1 -1
  76. package/dist/components/switch/index.js +25 -23
  77. package/dist/components/switch/index.js.map +1 -1
  78. package/dist/components/switch/style.js +1 -1
  79. package/dist/components/switch/style.js.map +1 -1
  80. package/dist/components/textarea/style.js +1 -1
  81. package/dist/components/textarea/style.js.map +1 -1
  82. package/dist/components/theme/index.js +1 -1
  83. package/dist/components/theme/index.js.map +1 -1
  84. package/dist/components/theme/style.js +1 -1
  85. package/dist/components/theme/style.js.map +1 -1
  86. package/dist/components/toast/manager.js +2 -2
  87. package/dist/components/toast/manager.js.map +1 -1
  88. package/dist/components/toast/style.js +1 -1
  89. package/dist/components/toast/style.js.map +1 -1
  90. package/dist/components/tooltip/index.d.ts +1 -0
  91. package/dist/components/tooltip/index.js +37 -33
  92. package/dist/components/tooltip/index.js.map +1 -1
  93. package/dist/components/tooltip/style.js.map +1 -1
  94. package/dist/icons/generated/radix-icons-reset.d.ts +6 -0
  95. package/dist/icons/generated/radix-icons-reset.js +10 -0
  96. package/dist/icons/generated/radix-icons-reset.js.map +1 -0
  97. package/dist/icons/index.d.ts +1 -0
  98. package/dist/icons/index.js +2 -1
  99. package/dist/shared/focus/pointer-focus.js +37 -0
  100. package/dist/shared/focus/pointer-focus.js.map +1 -0
  101. package/dist/shared/gesture/drag-gesture.js +53 -30
  102. package/dist/shared/gesture/drag-gesture.js.map +1 -1
  103. package/dist/shared/gesture/pinch-gesture.js +84 -0
  104. package/dist/shared/gesture/pinch-gesture.js.map +1 -0
  105. package/dist/shared/group-management/index.js.map +1 -1
  106. package/dist/shared/menu-portal/menu-portal.js.map +1 -1
  107. package/dist/shared/option-portal/combobox-shell.js +8 -6
  108. package/dist/shared/option-portal/combobox-shell.js.map +1 -1
  109. package/dist/shared/overlay/anchored-panel.js +33 -30
  110. package/dist/shared/overlay/anchored-panel.js.map +1 -1
  111. package/dist/shared/overlay/composition.js +57 -0
  112. package/dist/shared/overlay/composition.js.map +1 -0
  113. package/dist/shared/overlay/lifecycle.js +41 -0
  114. package/dist/shared/overlay/lifecycle.js.map +1 -0
  115. package/dist/shared/overlay/native-dialog-presence.js +15 -15
  116. package/dist/shared/overlay/native-dialog-presence.js.map +1 -1
  117. package/dist/shared/{theme → overlay}/overlay-root.js +1 -1
  118. package/dist/shared/overlay/overlay-root.js.map +1 -0
  119. package/dist/shared/overlay/overlay.js +43 -41
  120. package/dist/shared/overlay/overlay.js.map +1 -1
  121. package/dist/shared/overlay/portal.js +2 -2
  122. package/dist/shared/overlay/portal.js.map +1 -1
  123. package/dist/shared/overlay/positioning-generation.js +20 -0
  124. package/dist/shared/overlay/positioning-generation.js.map +1 -0
  125. package/dist/shared/{theme/theme-scope.js → overlay/theme-overlay-scope.js} +2 -2
  126. package/dist/shared/overlay/theme-overlay-scope.js.map +1 -0
  127. package/package.json +5 -5
  128. package/dist/shared/theme/overlay-root.js.map +0 -1
  129. package/dist/shared/theme/theme-scope.js.map +0 -1
@@ -0,0 +1,84 @@
1
+ function e(e, t) {
2
+ let { onStart: n, onMove: r, onEnd: i, onCancel: a } = t, o = /* @__PURE__ */ new Map(), s = 0, c = 0, l = 0, u = !1;
3
+ function d() {
4
+ window.addEventListener("pointermove", _, !0), window.addEventListener("pointerup", v, !0), window.addEventListener("pointercancel", v, !0);
5
+ }
6
+ function f() {
7
+ window.removeEventListener("pointermove", _, !0), window.removeEventListener("pointerup", v, !0), window.removeEventListener("pointercancel", v, !0);
8
+ }
9
+ function p() {
10
+ o.clear(), s = 0, c = 0, l = 0, u = !1, f();
11
+ }
12
+ function m() {
13
+ if (o.size !== 2) return null;
14
+ let e = o.values(), t = e.next().value, n = e.next().value;
15
+ return {
16
+ distance: Math.hypot(t.x - n.x, t.y - n.y),
17
+ centerX: (t.x + n.x) / 2,
18
+ centerY: (t.y + n.y) / 2
19
+ };
20
+ }
21
+ function h(e) {
22
+ let t = m();
23
+ if (!t) {
24
+ s = 0;
25
+ return;
26
+ }
27
+ s = t.distance, c = t.centerX, l = t.centerY, s >= 16 && n?.(t, e);
28
+ }
29
+ function g(e) {
30
+ if (o.set(e.pointerId, {
31
+ x: e.clientX,
32
+ y: e.clientY
33
+ }), d(), !(o.size < 2)) {
34
+ if (o.size > 2) {
35
+ s = 0;
36
+ return;
37
+ }
38
+ u = !0, h(e);
39
+ }
40
+ }
41
+ function _(e) {
42
+ if (!o.has(e.pointerId) || (o.set(e.pointerId, {
43
+ x: e.clientX,
44
+ y: e.clientY
45
+ }), !u)) return;
46
+ if (s < 16) {
47
+ h(e);
48
+ return;
49
+ }
50
+ let t = m();
51
+ t && r?.({
52
+ ratio: t.distance / s,
53
+ distance: t.distance,
54
+ centerX: t.centerX,
55
+ centerY: t.centerY,
56
+ deltaX: t.centerX - c,
57
+ deltaY: t.centerY - l
58
+ }, e);
59
+ }
60
+ function v(e) {
61
+ if (!o.delete(e.pointerId)) return;
62
+ if (o.size === 0 && f(), o.size >= 2) {
63
+ u && h(e);
64
+ return;
65
+ }
66
+ let t = u;
67
+ u = !1, s = 0, t && (e.type === "pointercancel" ? a?.(e) : i?.(e));
68
+ }
69
+ function y() {
70
+ let e = u;
71
+ p(), e && a?.();
72
+ }
73
+ return e.addEventListener("pointerdown", g), {
74
+ isPinching: () => u,
75
+ cancel: y,
76
+ destroy: () => {
77
+ e.removeEventListener("pointerdown", g), y();
78
+ }
79
+ };
80
+ }
81
+ //#endregion
82
+ export { e as attachPinchGesture };
83
+
84
+ //# sourceMappingURL=pinch-gesture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinch-gesture.js","names":[],"sources":["../../../src/shared/gesture/pinch-gesture.ts"],"sourcesContent":["/**\n * 跨平台/跨浏览器 Pointer 双指捏合手势生命周期管理器\n *\n * 封装:\n * 1. 活动指针表与「第二指就位即进入手势」的判定(首指仍留给并行的单指手势)\n * 2. window 兜底监听(防指针离开元素或移出视口导致手势悬挂)\n * 3. 两指距离比例与中点位移计算\n * 4. 起始距离过近时的基准重置(两指贴着落下不会产生比例跳变)\n *\n * 本模块只产出几何信息;比例与中点如何映射为缩放和平移由消费方决定。\n */\n\n/** 基准距离下限(px):低于此值不产出比例,等两指拉开后以新基准重新开始。 */\nconst MIN_BASE_DISTANCE = 16\n\nexport interface PinchStartInfo {\n /** 基准两指距离(px),恒 >= MIN_BASE_DISTANCE */\n distance: number\n /** 两指中点(client 坐标) */\n centerX: number\n centerY: number\n}\n\nexport interface PinchMoveInfo {\n /** 当前距离与基准距离之比;> 1 为放大,< 1 为缩小 */\n ratio: number\n distance: number\n centerX: number\n centerY: number\n /** 中点相对基准中点的位移(px) */\n deltaX: number\n deltaY: number\n}\n\nexport interface PinchGestureOptions {\n /** 两指就位且距离达到基准时触发;指针增删导致端点变化时会以新基准再次触发 */\n onStart?: (info: PinchStartInfo, event: PointerEvent) => void\n onMove?: (info: PinchMoveInfo, event: PointerEvent) => void\n /** 指针少于两个导致手势结束时触发 */\n onEnd?: (event: PointerEvent) => void\n /** 手势被 pointercancel 或 destroy 打断时触发 */\n onCancel?: (event?: PointerEvent) => void\n}\n\nexport interface PinchGestureHandle {\n isPinching: () => boolean\n /** 主动清空指针表并结束手势(打断时触发 onCancel) */\n cancel: () => void\n /** 移除监听并结束手势 */\n destroy: () => void\n}\n\ninterface TrackedPointer {\n x: number\n y: number\n}\n\n/**\n * 为元素挂载双指捏合手势。第二指在该元素上按下即进入手势,\n * 首指的按下事件不受影响,可由单指手势管理器并行接管。\n */\nexport function attachPinchGesture(target: HTMLElement, options: PinchGestureOptions): PinchGestureHandle {\n const { onStart, onMove, onEnd, onCancel } = options\n\n const pointers = new Map<number, TrackedPointer>()\n let baseDistance = 0\n let baseCenterX = 0\n let baseCenterY = 0\n let pinching = false\n\n function attachWindowListeners() {\n window.addEventListener('pointermove', handlePointerMove, true)\n window.addEventListener('pointerup', handlePointerEnd, true)\n window.addEventListener('pointercancel', handlePointerEnd, true)\n }\n\n function detachWindowListeners() {\n window.removeEventListener('pointermove', handlePointerMove, true)\n window.removeEventListener('pointerup', handlePointerEnd, true)\n window.removeEventListener('pointercancel', handlePointerEnd, true)\n }\n\n function reset() {\n pointers.clear()\n baseDistance = 0\n baseCenterX = 0\n baseCenterY = 0\n pinching = false\n detachWindowListeners()\n }\n\n /** 恰好两个指针时返回捏合两端的中点与距离;其余数量不产出几何。 */\n function geometry(): { distance: number; centerX: number; centerY: number } | null {\n if (pointers.size !== 2) return null\n const iterator = pointers.values()\n const first = iterator.next().value as TrackedPointer\n const second = iterator.next().value as TrackedPointer\n return {\n distance: Math.hypot(first.x - second.x, first.y - second.y),\n centerX: (first.x + second.x) / 2,\n centerY: (first.y + second.y) / 2\n }\n }\n\n /** 以当前几何重建基准;距离达到下限才派发 onStart,否则留待拉开后重建。 */\n function rebase(event: PointerEvent) {\n const current = geometry()\n if (!current) {\n baseDistance = 0\n return\n }\n baseDistance = current.distance\n baseCenterX = current.centerX\n baseCenterY = current.centerY\n if (baseDistance >= MIN_BASE_DISTANCE) onStart?.(current, event)\n }\n\n function handlePointerDown(event: PointerEvent) {\n pointers.set(event.pointerId, { x: event.clientX, y: event.clientY })\n attachWindowListeners()\n\n if (pointers.size < 2) return\n if (pointers.size > 2) {\n baseDistance = 0\n return\n }\n\n pinching = true\n rebase(event)\n }\n\n function handlePointerMove(event: PointerEvent) {\n if (!pointers.has(event.pointerId)) return\n pointers.set(event.pointerId, { x: event.clientX, y: event.clientY })\n if (!pinching) return\n\n if (baseDistance < MIN_BASE_DISTANCE) {\n rebase(event)\n return\n }\n\n const current = geometry()\n if (!current) return\n\n onMove?.(\n {\n ratio: current.distance / baseDistance,\n distance: current.distance,\n centerX: current.centerX,\n centerY: current.centerY,\n deltaX: current.centerX - baseCenterX,\n deltaY: current.centerY - baseCenterY\n },\n event\n )\n }\n\n function handlePointerEnd(event: PointerEvent) {\n if (!pointers.delete(event.pointerId)) return\n if (pointers.size === 0) detachWindowListeners()\n\n if (pointers.size >= 2) {\n // 指针增删换了端点,旧基准会让比例跳变;二指仍在则重建。\n if (pinching) rebase(event)\n return\n }\n\n const wasPinching = pinching\n pinching = false\n baseDistance = 0\n if (!wasPinching) return\n\n if (event.type === 'pointercancel') onCancel?.(event)\n else onEnd?.(event)\n }\n\n function cancel() {\n const wasPinching = pinching\n reset()\n if (wasPinching) onCancel?.()\n }\n\n target.addEventListener('pointerdown', handlePointerDown)\n\n return {\n isPinching: () => pinching,\n cancel,\n destroy: () => {\n target.removeEventListener('pointerdown', handlePointerDown)\n cancel()\n }\n }\n}\n"],"mappings":"AA6DA,SAAgB,EAAmB,GAAqB,GAAkD;CACxG,IAAM,EAAE,YAAS,WAAQ,UAAO,gBAAa,GAEvC,oBAAW,IAAI,IAA4B,GAC7C,IAAe,GACf,IAAc,GACd,IAAc,GACd,IAAW;CAEf,SAAS,IAAwB;EAG/B,AAFA,OAAO,iBAAiB,eAAe,GAAmB,EAAI,GAC9D,OAAO,iBAAiB,aAAa,GAAkB,EAAI,GAC3D,OAAO,iBAAiB,iBAAiB,GAAkB,EAAI;CACjE;CAEA,SAAS,IAAwB;EAG/B,AAFA,OAAO,oBAAoB,eAAe,GAAmB,EAAI,GACjE,OAAO,oBAAoB,aAAa,GAAkB,EAAI,GAC9D,OAAO,oBAAoB,iBAAiB,GAAkB,EAAI;CACpE;CAEA,SAAS,IAAQ;EAMf,AALA,EAAS,MAAM,GACf,IAAe,GACf,IAAc,GACd,IAAc,GACd,IAAW,IACX,EAAsB;CACxB;CAGA,SAAS,IAA0E;EACjF,IAAI,EAAS,SAAS,GAAG,OAAO;EAChC,IAAM,IAAW,EAAS,OAAO,GAC3B,IAAQ,EAAS,KAAK,CAAC,CAAC,OACxB,IAAS,EAAS,KAAK,CAAC,CAAC;EAC/B,OAAO;GACL,UAAU,KAAK,MAAM,EAAM,IAAI,EAAO,GAAG,EAAM,IAAI,EAAO,CAAC;GAC3D,UAAU,EAAM,IAAI,EAAO,KAAK;GAChC,UAAU,EAAM,IAAI,EAAO,KAAK;EAClC;CACF;CAGA,SAAS,EAAO,GAAqB;EACnC,IAAM,IAAU,EAAS;EACzB,IAAI,CAAC,GAAS;GACZ,IAAe;GACf;EACF;EAIA,AAHA,IAAe,EAAQ,UACvB,IAAc,EAAQ,SACtB,IAAc,EAAQ,SAClB,KAAgB,MAAmB,IAAU,GAAS,CAAK;CACjE;CAEA,SAAS,EAAkB,GAAqB;EAC9C,MAAS,IAAI,EAAM,WAAW;GAAE,GAAG,EAAM;GAAS,GAAG,EAAM;EAAQ,CAAC,GACpE,EAAsB,GAElB,IAAS,OAAO,IACpB;OAAI,EAAS,OAAO,GAAG;IACrB,IAAe;IACf;GACF;GAGA,AADA,IAAW,IACX,EAAO,CAAK;EAHZ;CAIF;CAEA,SAAS,EAAkB,GAAqB;EAG9C,IAFI,CAAC,EAAS,IAAI,EAAM,SAAS,MACjC,EAAS,IAAI,EAAM,WAAW;GAAE,GAAG,EAAM;GAAS,GAAG,EAAM;EAAQ,CAAC,GAChE,CAAC,IAAU;EAEf,IAAI,IAAe,IAAmB;GACpC,EAAO,CAAK;GACZ;EACF;EAEA,IAAM,IAAU,EAAS;EACpB,KAEL,IACE;GACE,OAAO,EAAQ,WAAW;GAC1B,UAAU,EAAQ;GAClB,SAAS,EAAQ;GACjB,SAAS,EAAQ;GACjB,QAAQ,EAAQ,UAAU;GAC1B,QAAQ,EAAQ,UAAU;EAC5B,GACA,CACF;CACF;CAEA,SAAS,EAAiB,GAAqB;EAC7C,IAAI,CAAC,EAAS,OAAO,EAAM,SAAS,GAAG;EAGvC,IAFI,EAAS,SAAS,KAAG,EAAsB,GAE3C,EAAS,QAAQ,GAAG;GAEtB,AAAI,KAAU,EAAO,CAAK;GAC1B;EACF;EAEA,IAAM,IAAc;EACpB,IAAW,IACX,IAAe,GACV,MAED,EAAM,SAAS,kBAAiB,IAAW,CAAK,IAC/C,IAAQ,CAAK;CACpB;CAEA,SAAS,IAAS;EAChB,IAAM,IAAc;EAEpB,AADA,EAAM,GACF,KAAa,IAAW;CAC9B;CAIA,OAFA,EAAO,iBAAiB,eAAe,CAAiB,GAEjD;EACL,kBAAkB;EAClB;EACA,eAAe;GAEb,AADA,EAAO,oBAAoB,eAAe,CAAiB,GAC3D,EAAO;EACT;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/shared/group-management/index.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\nimport { ContextConsumer, ContextProvider, createContext, type Context } from '@lit/context'\nimport type { ReactiveController, ReactiveControllerHost } from 'lit'\n\n/*\n * 下行通道基于 @lit/context(ADR-0040):root 上的 ContextProvider 以\n * `ReadonlyMap<HTMLElement, Context>` 为载荷向全子树广播,子项侧\n * ContextConsumer 订阅并按自身元素取条目。context-request 事件 composed,\n * 可穿过中间 shadow 边界;custom element 升级顺序保证祖先先于后代升级,\n * provider 总是先于子项请求就绪。成员追踪(querySelectorAll +\n * MutationObserver/slotchange)、点击归因与 setItemSelected 上行写回\n * 不在下行通道内,仍由 GroupController 驱动。\n */\n\nexport interface SelectionGroupContext {\n readonly disabled: boolean\n}\n\nexport interface ButtonGroupContext {\n readonly direction: 'horizontal' | 'vertical'\n readonly isLast: boolean\n}\n\n// 载荷类型仅编译期品牌化;按载荷形状分两个 key,避免异构树串扰。\n// createContext 泛型顺序为 <ValueType, K>。\nconst SELECTION_GROUP_CONTEXT = createContext<ReadonlyMap<HTMLElement, SelectionGroupContext>, symbol>(\n Symbol('web-ui.selection-group-context')\n)\n\nconst BUTTON_GROUP_CONTEXT = createContext<ReadonlyMap<HTMLElement, ButtonGroupContext>, symbol>(\n Symbol('web-ui.button-group-context')\n)\n\n// 两个 key 的运行时形状一致(Map 载荷),泛型插件内部统一按宽松类型驱动。\ntype AnyGroupContext = Context<symbol, ReadonlyMap<HTMLElement, unknown>>\n\nconst SELECTION_GROUP_CONTEXT_LOOSE = SELECTION_GROUP_CONTEXT as unknown as AnyGroupContext\nconst BUTTON_GROUP_CONTEXT_LOOSE = BUTTON_GROUP_CONTEXT as unknown as AnyGroupContext\n\nexport const selectionGroupContextKey: AnyGroupContext = SELECTION_GROUP_CONTEXT_LOOSE\nexport const buttonGroupContextKey: AnyGroupContext = BUTTON_GROUP_CONTEXT_LOOSE\n\ninterface GroupControllerHost extends HTMLElement, ReactiveControllerHost {\n readonly renderRoot: Element | DocumentFragment\n}\n\n/**\n * 创建受管理 group 的子项侧能力:订阅最近 provider 的 context 载荷并提取\n * 自身条目。在 `make()` 时完成注册,组件无需再编写 constructor 样板。\n */\nexport function defineGroupManaged<Context>(\n item: HTMLElement & ReactiveControllerHost,\n options: {\n /** 与所属根组件使用的 context key 一致。 */\n context: AnyGroupContext\n requestUpdate: () => void\n equals?: (a: Context | undefined, b: Context | undefined) => boolean\n }\n) {\n return definePlugin(() => {\n let context: Context | undefined\n const equals = options.equals ?? Object.is\n\n const consumer = new ContextConsumer(item, {\n context: options.context,\n subscribe: true,\n callback: payload => {\n const next = (payload as ReadonlyMap<HTMLElement, Context> | undefined)?.get(item)\n if (equals(context, next)) return\n context = next\n options.requestUpdate()\n }\n })\n\n /*\n * 离开 group(被移出 provider 子树)时 ContextConsumer 只是静默退订,\n * 不会收到「条目已移除」的通知:显式清空上下文,恢复独立控件语义\n * (如 checkbox 恢复自身表单提交)。跨组移动先断连清空、再重连订阅新组。\n */\n item.addController({\n hostDisconnected() {\n if (equals(context, undefined)) return\n context = undefined\n options.requestUpdate()\n }\n })\n\n return {\n getContext(): Context | undefined {\n return context\n },\n // 供测试或特殊场景确认订阅状态。\n isSubscribed(): boolean {\n return consumer.value !== undefined\n }\n }\n })\n}\n\nexport interface GroupPresentationOptions<Item extends HTMLElement, Context> {\n readonly host: HTMLElement\n /** 与子项 defineGroupManaged 使用的 context key 一致。 */\n readonly context: AnyGroupContext\n readonly getItems: () => readonly Item[]\n readonly getContext: (item: Item, index: number, items: readonly Item[]) => Context\n}\n\n/**\n * 为分组子项注册可逆的展示上下文(@lit/context 下行通道)。不持有公开 DOM\n * 属性;sync() 以新 Map 载荷整体广播,成员变化体现为子项条目消失。\n */\nexport function defineGroupPresentation<Item extends HTMLElement, Context>(\n options: GroupPresentationOptions<Item, Context>\n) {\n return definePlugin(() => {\n const provider = new ContextProvider(options.host, {\n context: options.context,\n initialValue: new Map<HTMLElement, unknown>()\n })\n\n return {\n sync(): boolean {\n const nextItems = options.getItems()\n const payload = new Map<HTMLElement, unknown>()\n nextItems.forEach((item, index) => payload.set(item, options.getContext(item, index, nextItems)))\n // 新 Map 实例保证 Object.is 判定为变更,全量通知订阅者。\n // 每次 hostUpdated 都重播(即使成员/方向未变)是有意形态:每次新建\n // context 对象,但子项 defineGroupManaged 的 equals 门控 requestUpdate\n // (标量字段比较),重播不产生多余渲染;代价是比「仅成员变化才通知」\n // 更频繁的广播事件,换取零状态追踪的简单性(与 GroupController 的\n // 全量 sync 对齐)。\n provider.setValue(payload)\n return true\n },\n disconnect() {\n provider.setValue(new Map())\n }\n }\n })\n}\n\nexport interface GroupLifecycle {\n sync(): boolean\n disconnect(): void\n handleChange?(event: Event): void\n}\n\ninterface GroupControllerOptions {\n afterSync?: () => void\n}\n\n/**\n * 把组生命周期插件接入 Lit,协调规则不落在组件生命周期方法里。\n */\nexport class GroupController implements ReactiveController {\n private connected = false\n private observer: MutationObserver | null = null\n private observedSlots = new Set<HTMLSlotElement>()\n constructor(\n private readonly host: GroupControllerHost,\n private readonly lifecycle: GroupLifecycle,\n private readonly options: GroupControllerOptions = {}\n ) {\n host.addController(this)\n }\n\n hostConnected() {\n this.connected = true\n this.host.addEventListener('change', this.handleChange, true)\n this.observeHost()\n this.observeSlots()\n this.sync()\n }\n\n hostUpdated() {\n this.observeSlots()\n this.sync()\n }\n\n hostDisconnected() {\n this.connected = false\n this.host.removeEventListener('change', this.handleChange, true)\n this.disconnectSlots()\n this.disconnectHostObserver()\n this.lifecycle.disconnect()\n }\n\n private readonly handleChange = (event: Event) => {\n this.lifecycle.handleChange?.(event)\n }\n\n private readonly handleSlotChange = () => {\n if (!this.connected) return\n this.sync()\n }\n\n private readonly handleMutations = () => {\n if (!this.connected) return\n this.sync()\n }\n\n sync() {\n if (!this.lifecycle.sync()) return\n this.options.afterSync?.()\n }\n\n private observeHost() {\n this.disconnectHostObserver()\n // 监听 light DOM 的增删,覆盖 Vue v-if / React && 的 comment 锚点替换与深层包裹\n this.observer = new MutationObserver(this.handleMutations)\n this.observer.observe(this.host, { childList: true, subtree: true })\n }\n\n private disconnectHostObserver() {\n if (this.observer) {\n this.observer.disconnect()\n this.observer = null\n }\n }\n\n private observeSlots() {\n const root = this.host.renderRoot as Element\n const slots = root.querySelectorAll<HTMLSlotElement>('slot')\n for (const slot of slots) {\n if (!this.observedSlots.has(slot)) {\n slot.addEventListener('slotchange', this.handleSlotChange)\n this.observedSlots.add(slot)\n }\n }\n // 清理已移除的 slot(热更新等场景)\n for (const slot of this.observedSlots) {\n if (!root.contains(slot)) {\n slot.removeEventListener('slotchange', this.handleSlotChange)\n this.observedSlots.delete(slot)\n }\n }\n }\n\n private disconnectSlots() {\n for (const slot of this.observedSlots) {\n slot.removeEventListener('slotchange', this.handleSlotChange)\n }\n this.observedSlots.clear()\n }\n}\n\nexport interface GroupCoordinatorOptions<Item extends HTMLElement, Value> {\n readonly host: HTMLElement\n readonly getItems: () => readonly Item[]\n readonly getValue: () => Value\n readonly setValue: (value: Value) => void\n readonly getDisabled: () => boolean\n readonly isItem: (target: EventTarget | null) => target is Item\n readonly isItemSelected: (item: Item, value: Value) => boolean\n readonly getNextValue: (item: Item, value: Value) => Value\n readonly valuesEqual: (a: Value, b: Value) => boolean\n readonly copyValue: (value: Value) => Value\n readonly setItemSelected: (item: Item, selected: boolean) => void\n readonly dispatchValueChange: () => void\n}\n\nconst ENABLED_SELECTION_CONTEXT: SelectionGroupContext = { disabled: false }\nconst DISABLED_SELECTION_CONTEXT: SelectionGroupContext = { disabled: true }\n\n/**\n * 在一个内部插件接口后收敛选中态、继承禁用与受管子项成员关系。\n * 禁用态经 @lit/context 下行广播;选中态上行写回保持直写。\n */\nexport function defineGroupCoordinator<Item extends HTMLElement, Value>(options: GroupCoordinatorOptions<Item, Value>) {\n return definePlugin(() => {\n const provider = new ContextProvider(options.host, {\n context: SELECTION_GROUP_CONTEXT_LOOSE,\n initialValue: new Map<HTMLElement, SelectionGroupContext>()\n })\n\n let lastValue: Value | undefined\n let lastDisabled: boolean | undefined\n let lastItems = new Set<Item>()\n let initialized = false\n\n const sync = (): boolean => {\n const nextItems = options.getItems()\n const value = options.getValue()\n const disabled = options.getDisabled()\n // 成员变化也要重放选中写回:新成员需继承当前选中态。\n const membershipChanged = nextItems.length !== lastItems.size || [...nextItems].some(item => !lastItems.has(item))\n const selectionChanged =\n !initialized ||\n !options.valuesEqual(value, lastValue as Value) ||\n disabled !== lastDisabled ||\n membershipChanged\n\n // 上行选中写回:根写入子项(context 是纯下行,不承担该方向)。\n if (selectionChanged) {\n for (const item of nextItems) {\n options.setItemSelected(item, options.isItemSelected(item, value))\n }\n }\n\n // 禁用下行:每次 sync 以新 Map 载荷整体广播(成员增删与禁用变化一并覆盖,\n // 新 Map 实例保证 Object.is 判定为变更)。\n provider.setValue(\n new Map(\n nextItems.map(item => [\n item as HTMLElement,\n disabled ? DISABLED_SELECTION_CONTEXT : ENABLED_SELECTION_CONTEXT\n ])\n )\n )\n\n lastItems = new Set(nextItems)\n lastValue = options.copyValue(value)\n lastDisabled = disabled\n initialized = true\n return selectionChanged\n }\n\n return {\n sync,\n disconnect() {\n provider.setValue(new Map())\n lastItems.clear()\n lastValue = undefined\n lastDisabled = undefined\n initialized = false\n },\n handleChange(event: Event) {\n if (options.getDisabled() || !options.isItem(event.target) || !options.host.contains(event.target)) {\n return\n }\n\n const current = options.getValue()\n const next = options.getNextValue(event.target, current)\n if (options.valuesEqual(current, next)) return\n\n options.setValue(next)\n options.dispatchValueChange()\n }\n }\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/shared/group-management/index.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\nimport { ContextConsumer, ContextProvider, createContext, type Context } from '@lit/context'\nimport type { ReactiveController, ReactiveControllerHost } from 'lit'\n\n/*\n * 下行通道基于 @lit/context(ADR-0032):root 上的 ContextProvider 以\n * `ReadonlyMap<HTMLElement, Context>` 为载荷向全子树广播,子项侧\n * ContextConsumer 订阅并按自身元素取条目。context-request 事件 composed,\n * 可穿过中间 shadow 边界;custom element 升级顺序保证祖先先于后代升级,\n * provider 总是先于子项请求就绪。成员追踪(querySelectorAll +\n * MutationObserver/slotchange)、点击归因与 setItemSelected 上行写回\n * 不在下行通道内,仍由 GroupController 驱动。\n */\n\nexport interface SelectionGroupContext {\n readonly disabled: boolean\n}\n\nexport interface ButtonGroupContext {\n readonly direction: 'horizontal' | 'vertical'\n readonly isLast: boolean\n}\n\n// 载荷类型仅编译期品牌化;按载荷形状分两个 key,避免异构树串扰。\n// createContext 泛型顺序为 <ValueType, K>。\nconst SELECTION_GROUP_CONTEXT = createContext<ReadonlyMap<HTMLElement, SelectionGroupContext>, symbol>(\n Symbol('web-ui.selection-group-context')\n)\n\nconst BUTTON_GROUP_CONTEXT = createContext<ReadonlyMap<HTMLElement, ButtonGroupContext>, symbol>(\n Symbol('web-ui.button-group-context')\n)\n\n// 两个 key 的运行时形状一致(Map 载荷),泛型插件内部统一按宽松类型驱动。\ntype AnyGroupContext = Context<symbol, ReadonlyMap<HTMLElement, unknown>>\n\nconst SELECTION_GROUP_CONTEXT_LOOSE = SELECTION_GROUP_CONTEXT as unknown as AnyGroupContext\nconst BUTTON_GROUP_CONTEXT_LOOSE = BUTTON_GROUP_CONTEXT as unknown as AnyGroupContext\n\nexport const selectionGroupContextKey: AnyGroupContext = SELECTION_GROUP_CONTEXT_LOOSE\nexport const buttonGroupContextKey: AnyGroupContext = BUTTON_GROUP_CONTEXT_LOOSE\n\ninterface GroupControllerHost extends HTMLElement, ReactiveControllerHost {\n readonly renderRoot: Element | DocumentFragment\n}\n\n/**\n * 创建受管理 group 的子项侧能力:订阅最近 provider 的 context 载荷并提取\n * 自身条目。在 `make()` 时完成注册,组件无需再编写 constructor 样板。\n */\nexport function defineGroupManaged<Context>(\n item: HTMLElement & ReactiveControllerHost,\n options: {\n /** 与所属根组件使用的 context key 一致。 */\n context: AnyGroupContext\n requestUpdate: () => void\n equals?: (a: Context | undefined, b: Context | undefined) => boolean\n }\n) {\n return definePlugin(() => {\n let context: Context | undefined\n const equals = options.equals ?? Object.is\n\n const consumer = new ContextConsumer(item, {\n context: options.context,\n subscribe: true,\n callback: payload => {\n const next = (payload as ReadonlyMap<HTMLElement, Context> | undefined)?.get(item)\n if (equals(context, next)) return\n context = next\n options.requestUpdate()\n }\n })\n\n /*\n * 离开 group(被移出 provider 子树)时 ContextConsumer 只是静默退订,\n * 不会收到「条目已移除」的通知:显式清空上下文,恢复独立控件语义\n * (如 checkbox 恢复自身表单提交)。跨组移动先断连清空、再重连订阅新组。\n */\n item.addController({\n hostDisconnected() {\n if (equals(context, undefined)) return\n context = undefined\n options.requestUpdate()\n }\n })\n\n return {\n getContext(): Context | undefined {\n return context\n },\n // 供测试或特殊场景确认订阅状态。\n isSubscribed(): boolean {\n return consumer.value !== undefined\n }\n }\n })\n}\n\nexport interface GroupPresentationOptions<Item extends HTMLElement, Context> {\n readonly host: HTMLElement\n /** 与子项 defineGroupManaged 使用的 context key 一致。 */\n readonly context: AnyGroupContext\n readonly getItems: () => readonly Item[]\n readonly getContext: (item: Item, index: number, items: readonly Item[]) => Context\n}\n\n/**\n * 为分组子项注册可逆的展示上下文(@lit/context 下行通道)。不持有公开 DOM\n * 属性;sync() 以新 Map 载荷整体广播,成员变化体现为子项条目消失。\n */\nexport function defineGroupPresentation<Item extends HTMLElement, Context>(\n options: GroupPresentationOptions<Item, Context>\n) {\n return definePlugin(() => {\n const provider = new ContextProvider(options.host, {\n context: options.context,\n initialValue: new Map<HTMLElement, unknown>()\n })\n\n return {\n sync(): boolean {\n const nextItems = options.getItems()\n const payload = new Map<HTMLElement, unknown>()\n nextItems.forEach((item, index) => payload.set(item, options.getContext(item, index, nextItems)))\n // 新 Map 实例保证 Object.is 判定为变更,全量通知订阅者。\n // 每次 hostUpdated 都重播(即使成员/方向未变)是有意形态:每次新建\n // context 对象,但子项 defineGroupManaged 的 equals 门控 requestUpdate\n // (标量字段比较),重播不产生多余渲染;代价是比「仅成员变化才通知」\n // 更频繁的广播事件,换取零状态追踪的简单性(与 GroupController 的\n // 全量 sync 对齐)。\n provider.setValue(payload)\n return true\n },\n disconnect() {\n provider.setValue(new Map())\n }\n }\n })\n}\n\nexport interface GroupLifecycle {\n sync(): boolean\n disconnect(): void\n handleChange?(event: Event): void\n}\n\ninterface GroupControllerOptions {\n afterSync?: () => void\n}\n\n/**\n * 把组生命周期插件接入 Lit,协调规则不落在组件生命周期方法里。\n */\nexport class GroupController implements ReactiveController {\n private connected = false\n private observer: MutationObserver | null = null\n private observedSlots = new Set<HTMLSlotElement>()\n constructor(\n private readonly host: GroupControllerHost,\n private readonly lifecycle: GroupLifecycle,\n private readonly options: GroupControllerOptions = {}\n ) {\n host.addController(this)\n }\n\n hostConnected() {\n this.connected = true\n this.host.addEventListener('change', this.handleChange, true)\n this.observeHost()\n this.observeSlots()\n this.sync()\n }\n\n hostUpdated() {\n this.observeSlots()\n this.sync()\n }\n\n hostDisconnected() {\n this.connected = false\n this.host.removeEventListener('change', this.handleChange, true)\n this.disconnectSlots()\n this.disconnectHostObserver()\n this.lifecycle.disconnect()\n }\n\n private readonly handleChange = (event: Event) => {\n this.lifecycle.handleChange?.(event)\n }\n\n private readonly handleSlotChange = () => {\n if (!this.connected) return\n this.sync()\n }\n\n private readonly handleMutations = () => {\n if (!this.connected) return\n this.sync()\n }\n\n sync() {\n if (!this.lifecycle.sync()) return\n this.options.afterSync?.()\n }\n\n private observeHost() {\n this.disconnectHostObserver()\n // 监听 light DOM 的增删,覆盖 Vue v-if / React && 的 comment 锚点替换与深层包裹\n this.observer = new MutationObserver(this.handleMutations)\n this.observer.observe(this.host, { childList: true, subtree: true })\n }\n\n private disconnectHostObserver() {\n if (this.observer) {\n this.observer.disconnect()\n this.observer = null\n }\n }\n\n private observeSlots() {\n const root = this.host.renderRoot as Element\n const slots = root.querySelectorAll<HTMLSlotElement>('slot')\n for (const slot of slots) {\n if (!this.observedSlots.has(slot)) {\n slot.addEventListener('slotchange', this.handleSlotChange)\n this.observedSlots.add(slot)\n }\n }\n // 清理已移除的 slot(热更新等场景)\n for (const slot of this.observedSlots) {\n if (!root.contains(slot)) {\n slot.removeEventListener('slotchange', this.handleSlotChange)\n this.observedSlots.delete(slot)\n }\n }\n }\n\n private disconnectSlots() {\n for (const slot of this.observedSlots) {\n slot.removeEventListener('slotchange', this.handleSlotChange)\n }\n this.observedSlots.clear()\n }\n}\n\nexport interface GroupCoordinatorOptions<Item extends HTMLElement, Value> {\n readonly host: HTMLElement\n readonly getItems: () => readonly Item[]\n readonly getValue: () => Value\n readonly setValue: (value: Value) => void\n readonly getDisabled: () => boolean\n readonly isItem: (target: EventTarget | null) => target is Item\n readonly isItemSelected: (item: Item, value: Value) => boolean\n readonly getNextValue: (item: Item, value: Value) => Value\n readonly valuesEqual: (a: Value, b: Value) => boolean\n readonly copyValue: (value: Value) => Value\n readonly setItemSelected: (item: Item, selected: boolean) => void\n readonly dispatchValueChange: () => void\n}\n\nconst ENABLED_SELECTION_CONTEXT: SelectionGroupContext = { disabled: false }\nconst DISABLED_SELECTION_CONTEXT: SelectionGroupContext = { disabled: true }\n\n/**\n * 在一个内部插件接口后收敛选中态、继承禁用与受管子项成员关系。\n * 禁用态经 @lit/context 下行广播;选中态上行写回保持直写。\n */\nexport function defineGroupCoordinator<Item extends HTMLElement, Value>(options: GroupCoordinatorOptions<Item, Value>) {\n return definePlugin(() => {\n const provider = new ContextProvider(options.host, {\n context: SELECTION_GROUP_CONTEXT_LOOSE,\n initialValue: new Map<HTMLElement, SelectionGroupContext>()\n })\n\n let lastValue: Value | undefined\n let lastDisabled: boolean | undefined\n let lastItems = new Set<Item>()\n let initialized = false\n\n const sync = (): boolean => {\n const nextItems = options.getItems()\n const value = options.getValue()\n const disabled = options.getDisabled()\n // 成员变化也要重放选中写回:新成员需继承当前选中态。\n const membershipChanged = nextItems.length !== lastItems.size || [...nextItems].some(item => !lastItems.has(item))\n const selectionChanged =\n !initialized ||\n !options.valuesEqual(value, lastValue as Value) ||\n disabled !== lastDisabled ||\n membershipChanged\n\n // 上行选中写回:根写入子项(context 是纯下行,不承担该方向)。\n if (selectionChanged) {\n for (const item of nextItems) {\n options.setItemSelected(item, options.isItemSelected(item, value))\n }\n }\n\n // 禁用下行:每次 sync 以新 Map 载荷整体广播(成员增删与禁用变化一并覆盖,\n // 新 Map 实例保证 Object.is 判定为变更)。\n provider.setValue(\n new Map(\n nextItems.map(item => [\n item as HTMLElement,\n disabled ? DISABLED_SELECTION_CONTEXT : ENABLED_SELECTION_CONTEXT\n ])\n )\n )\n\n lastItems = new Set(nextItems)\n lastValue = options.copyValue(value)\n lastDisabled = disabled\n initialized = true\n return selectionChanged\n }\n\n return {\n sync,\n disconnect() {\n provider.setValue(new Map())\n lastItems.clear()\n lastValue = undefined\n lastDisabled = undefined\n initialized = false\n },\n handleChange(event: Event) {\n if (options.getDisabled() || !options.isItem(event.target) || !options.host.contains(event.target)) {\n return\n }\n\n const current = options.getValue()\n const next = options.getNextValue(event.target, current)\n if (options.valuesEqual(current, next)) return\n\n options.setValue(next)\n options.dispatchValueChange()\n }\n }\n })\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"menu-portal.js","names":[],"sources":["../../../src/shared/menu-portal/menu-portal.ts"],"sourcesContent":["import glass from '@/assets/glass.css?inline'\nimport menuPortalStyle from '@/assets/menu-portal.css?inline'\nimport overlayMotion from '@/assets/overlay-motion.css?inline'\nimport { resolveOverlayContainer } from '@/shared/overlay/portal'\n\nexport interface MenuPortalOverlay {\n readonly panel: HTMLElement\n readonly content: HTMLElement\n}\n\n// 菜单面板所需的共享样式。常规 overlay 容器已预注入这些样式;但当 overlay 因\n// target 位于已打开原生 dialog 内而被挂到该 dialog(top layer)时,dialog 的\n// shadow root 并不包含这些样式。故面板自携带一份,保证在任何容器下都能正确渲染。\nconst MENU_PANEL_STYLES = `${glass}\n${menuPortalStyle}\n${overlayMotion}`\n\n/**\n * 菜单浮层面板:面板直接挂到 overlay 容器(与普通 Overlay Portal 共用容器解析),\n * 样式复用容器内已注入的共享菜单样式(glass/menu-portal/overlay-motion),\n * 因此无需自建 shadow。菜单内容由 dropdown/context-menu 自行构建(menu-tree),\n * 这里只提供 panel 与 content 挂载点。\n */\nexport function createMenuPortalOverlay(className: string, target?: Element): MenuPortalOverlay {\n const panel = document.createElement('div')\n panel.className = `wui-menu-portal-overlay wui-floating-panel wui-glass ${className}`\n panel.dataset.wuiPresence = 'entering'\n const scroll = document.createElement('div')\n scroll.className = 'wui-menu-scroll'\n const content = document.createElement('div')\n content.className = 'wui-menu-content'\n scroll.append(content)\n\n const container = resolveOverlayContainer(undefined, target ?? document.body, {\n preferRootTheme: target === undefined\n })\n // 常规 overlay root 已预注入共享样式;只有 dialog 没有,因此按容器条件注入。\n if (container instanceof HTMLDialogElement) {\n const style = document.createElement('style')\n style.textContent = MENU_PANEL_STYLES\n panel.append(style)\n }\n panel.append(scroll)\n container.appendChild(panel)\n\n return { panel, content }\n}\n"],"mappings":";;;;;AAaA,IAAM,IAAoB,GAAG,EAAM;EACjC,EAAgB;EAChB;AAQF,SAAgB,EAAwB,GAAmB,GAAqC;CAC9F,IAAM,IAAQ,SAAS,cAAc,KAAK;CAE1C,AADA,EAAM,YAAY,wDAAwD,KAC1E,EAAM,QAAQ,cAAc;CAC5B,IAAM,IAAS,SAAS,cAAc,KAAK;CAC3C,EAAO,YAAY;CACnB,IAAM,IAAU,SAAS,cAAc,KAAK;CAE5C,AADA,EAAQ,YAAY,oBACpB,EAAO,OAAO,CAAO;CAErB,IAAM,IAAY,EAAwB,KAAA,GAAW,KAAU,SAAS,MAAM,EAC5E,iBAAiB,MAAW,KAAA,EAC9B,CAAC;CAED,IAAI,aAAqB,mBAAmB;EAC1C,IAAM,IAAQ,SAAS,cAAc,OAAO;EAE5C,AADA,EAAM,cAAc,GACpB,EAAM,OAAO,CAAK;CACpB;CAIA,OAHA,EAAM,OAAO,CAAM,GACnB,EAAU,YAAY,CAAK,GAEpB;EAAE;EAAO;CAAQ;AAC1B"}
1
+ {"version":3,"file":"menu-portal.js","names":[],"sources":["../../../src/shared/menu-portal/menu-portal.ts"],"sourcesContent":["import glass from '@/assets/glass.css?inline'\nimport menuPortalStyle from '@/assets/menu-portal.css?inline'\nimport overlayMotion from '@/assets/overlay-motion.css?inline'\nimport { resolveOverlayContainer } from '@/shared/overlay/portal'\n\nexport interface MenuPortalOverlay {\n readonly panel: HTMLElement\n readonly content: HTMLElement\n}\n\n// 菜单面板所需的共享样式。theme-owned/fallback overlay root 已预注入这些样式;但当 overlay 因\n// target 位于已打开原生 dialog 内而被挂到该 dialog(top layer)时,dialog 的\n// shadow root 并不包含这些样式。故面板自携带一份,保证在任何容器下都能正确渲染。\nconst MENU_PANEL_STYLES = `${glass}\n${menuPortalStyle}\n${overlayMotion}`\n\n/**\n * 菜单浮层面板:面板直接挂到 overlay 容器(与普通 Overlay Portal 共用容器解析),\n * 样式复用容器内已注入的共享菜单样式(glass/menu-portal/overlay-motion),\n * 因此无需自建 shadow。菜单内容由 dropdown/context-menu 自行构建(menu-tree),\n * 这里只提供 panel 与 content 挂载点。\n */\nexport function createMenuPortalOverlay(className: string, target?: Element): MenuPortalOverlay {\n const panel = document.createElement('div')\n // 单层玻璃:wui-glass 留在面板自身,背景/阴影/backdrop-filter 随面板 opacity\n // 与 blur 插值一起过渡(见 overlay-motion.css),无需 blur/surface 双层。\n panel.className = `wui-menu-portal-overlay wui-floating-panel wui-glass ${className}`\n panel.dataset.wuiPresence = 'entering'\n const scroll = document.createElement('div')\n scroll.className = 'wui-menu-scroll'\n const content = document.createElement('div')\n content.className = 'wui-menu-content'\n scroll.append(content)\n\n const container = resolveOverlayContainer(undefined, target ?? document.body, {\n preferRootTheme: target === undefined\n })\n // theme-owned/fallback overlay root 已预注入共享样式;只有 dialog 没有,因此按容器条件注入。\n if (container instanceof HTMLDialogElement) {\n const style = document.createElement('style')\n style.textContent = MENU_PANEL_STYLES\n panel.append(style)\n }\n panel.append(scroll)\n container.appendChild(panel)\n\n return { panel, content }\n}\n"],"mappings":";;;;;AAaA,IAAM,IAAoB,GAAG,EAAM;EACjC,EAAgB;EAChB;AAQF,SAAgB,EAAwB,GAAmB,GAAqC;CAC9F,IAAM,IAAQ,SAAS,cAAc,KAAK;CAI1C,AADA,EAAM,YAAY,wDAAwD,KAC1E,EAAM,QAAQ,cAAc;CAC5B,IAAM,IAAS,SAAS,cAAc,KAAK;CAC3C,EAAO,YAAY;CACnB,IAAM,IAAU,SAAS,cAAc,KAAK;CAE5C,AADA,EAAQ,YAAY,oBACpB,EAAO,OAAO,CAAO;CAErB,IAAM,IAAY,EAAwB,KAAA,GAAW,KAAU,SAAS,MAAM,EAC5E,iBAAiB,MAAW,KAAA,EAC9B,CAAC;CAED,IAAI,aAAqB,mBAAmB;EAC1C,IAAM,IAAQ,SAAS,cAAc,OAAO;EAE5C,AADA,EAAM,cAAc,GACpB,EAAM,OAAO,CAAK;CACpB;CAIA,OAHA,EAAM,OAAO,CAAM,GACnB,EAAU,YAAY,CAAK,GAEpB;EAAE;EAAO;CAAQ;AAC1B"}
@@ -1,5 +1,6 @@
1
+ import { overlayComposition as e } from "../overlay/composition.js";
1
2
  //#region src/shared/option-portal/combobox-shell.ts
2
- function e(e) {
3
+ function t(e) {
3
4
  function t(t = !1) {
4
5
  e.canOpen() === !1 || e.getIsOpen() || (e.setIsOpen(!0), e.dispatchOpenChange(), e.onOpen?.(t), e.syncScrollLock(!0), e.isPortal() ? requestAnimationFrame(() => {
5
6
  e.getIsOpen() && e.openOverlay(t);
@@ -13,7 +14,7 @@ function e(e) {
13
14
  close: n
14
15
  };
15
16
  }
16
- function t(e) {
17
+ function n(e) {
17
18
  function t(t) {
18
19
  t.addEventListener("click", e.onClick), t.addEventListener("pointerover", e.onPointerOver), t.addEventListener("pointerdown", e.onPointerDown), t.addEventListener("option-update", e.onUpdate);
19
20
  }
@@ -25,17 +26,18 @@ function t(e) {
25
26
  unbind: n
26
27
  };
27
28
  }
28
- function n(e, t, n, r) {
29
+ function r(t, n, r, i) {
29
30
  requestAnimationFrame(() => {
30
- n() && !e.matches(":focus-within") && !t()?.matches(":focus-within") && r();
31
+ let a = n();
32
+ r() && !t.matches(":focus-within") && !(a && e.hasFocusWithin(a)) && i();
31
33
  });
32
34
  }
33
- function r(e, t, n) {
35
+ function i(e, t, n) {
34
36
  if (e === 0) return -1;
35
37
  let r = t + n;
36
38
  return r < 0 && (r = e - 1), r >= e && (r = 0), r;
37
39
  }
38
40
  //#endregion
39
- export { e as createComboboxOpenController, t as createOptionListenerBinding, n as handleComboboxFocusOut, r as nextWrappingIndex };
41
+ export { t as createComboboxOpenController, n as createOptionListenerBinding, r as handleComboboxFocusOut, i as nextWrappingIndex };
40
42
 
41
43
  //# sourceMappingURL=combobox-shell.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"combobox-shell.js","names":[],"sources":["../../../src/shared/option-portal/combobox-shell.ts"],"sourcesContent":["import type { WebUiOption } from '@/components/option'\n\n/**\n * combobox 开合与 option 交互的共享骨架(select / autocomplete)。\n *\n * 与 option-portal 的边界约定一致:交互语义的\"差异点\"经回调注入,\n * 这里收口两组件逐行复制的生命周期不变量——\n * - open/close 状态迁移顺序:guard → 状态翻转 → open-change 派发 →\n * 激活钩子 → scroll-lock 配对 → portal 模式的 rAF 延迟 openOverlay\n * - close 路径:before-close 钩子(portal 内容先收敛)→ 激活清理 →\n * scroll-lock 释放 → closeOverlay(关闭后使 portal 引用失效)\n * - option 四事件监听对的绑定/解绑\n * - focusout 的 rAF :focus-within 复核\n * - 键盘导航的环绕游标算法\n */\n\nexport interface ComboboxOpenHooks {\n /** 是否允许打开(disabled / readonly 等由组件判断)。 */\n canOpen(): boolean\n getIsOpen(): boolean\n setIsOpen(open: boolean): void\n dispatchOpenChange(): void\n syncScrollLock(open: boolean): void\n /** 是否 portal 模式(决定 openOverlay 是否延迟到下一帧)。 */\n isPortal(): boolean\n openOverlay(isKeyboardNavigation: boolean): void\n closeOverlay(): void\n /** 打开时的激活项初始化(键盘导航取选中/首项,指针打开保留现状)。 */\n onOpen?(isKeyboardNavigation: boolean): void\n /** 关闭前钩子:autocomplete 需先把 light DOM 新增节点收敛进当前 portal content。 */\n onBeforeClose?(): void\n /** 关闭时清理激活状态(清 _activeIndex 与 active 属性)。 */\n onAfterClose?(): void\n}\n\nexport interface ComboboxOpenController {\n open(isKeyboardNavigation?: boolean): void\n close(): void\n}\n\nexport function createComboboxOpenController(hooks: ComboboxOpenHooks): ComboboxOpenController {\n function open(isKeyboardNavigation = false) {\n if (hooks.canOpen() === false || hooks.getIsOpen()) return\n hooks.setIsOpen(true)\n hooks.dispatchOpenChange()\n hooks.onOpen?.(isKeyboardNavigation)\n hooks.syncScrollLock(true)\n if (hooks.isPortal()) {\n requestAnimationFrame(() => {\n if (hooks.getIsOpen()) hooks.openOverlay(isKeyboardNavigation)\n })\n } else {\n hooks.openOverlay(isKeyboardNavigation)\n }\n }\n\n function close() {\n if (!hooks.getIsOpen()) return\n hooks.onBeforeClose?.()\n hooks.setIsOpen(false)\n hooks.dispatchOpenChange()\n hooks.onAfterClose?.()\n hooks.syncScrollLock(false)\n hooks.closeOverlay()\n }\n\n return { open, close }\n}\n\nexport interface OptionListenerHandlers {\n onClick: (event: Event) => void\n onPointerOver: (event: PointerEvent) => void\n onPointerDown: (event: PointerEvent) => void\n onUpdate: () => void\n}\n\n/** option 四事件监听对的绑定/解绑;两组件的 handler 签名一致,仅实现不同。 */\nexport function createOptionListenerBinding(handlers: OptionListenerHandlers) {\n function bind(option: WebUiOption) {\n option.addEventListener('click', handlers.onClick)\n option.addEventListener('pointerover', handlers.onPointerOver)\n option.addEventListener('pointerdown', handlers.onPointerDown)\n option.addEventListener('option-update', handlers.onUpdate)\n }\n function unbind(option: WebUiOption) {\n option.removeEventListener('click', handlers.onClick)\n option.removeEventListener('pointerover', handlers.onPointerOver)\n option.removeEventListener('pointerdown', handlers.onPointerDown)\n option.removeEventListener('option-update', handlers.onUpdate)\n }\n return { bind, unbind }\n}\n\n/**\n * focusout 关闭复核:焦点是否真的离开了宿主与面板。\n * rAF 等待 click/pointerdown 相关的焦点迁移完成后再判定。\n */\nexport function handleComboboxFocusOut(\n host: HTMLElement,\n getPanel: () => HTMLElement | null | undefined,\n getIsOpen: () => boolean,\n close: () => void\n): void {\n requestAnimationFrame(() => {\n if (getIsOpen() && !host.matches(':focus-within') && !getPanel()?.matches(':focus-within')) {\n close()\n }\n })\n}\n\n/**\n * 环绕游标:在 enabled 列表内按 delta 移动;从 -1(无激活)向下取首项、向上取末项。\n * 返回在 enabled 中的下一索引;调用方负责换算回全量 options 索引。\n */\nexport function nextWrappingIndex(enabledCount: number, currentIdx: number, delta: number): number {\n if (enabledCount === 0) return -1\n let nextIdx = currentIdx + delta\n if (nextIdx < 0) nextIdx = enabledCount - 1\n if (nextIdx >= enabledCount) nextIdx = 0\n return nextIdx\n}\n"],"mappings":";AAwCA,SAAgB,EAA6B,GAAkD;CAC7F,SAAS,EAAK,IAAuB,IAAO;EACtC,EAAM,QAAQ,MAAM,MAAS,EAAM,UAAU,MACjD,EAAM,UAAU,EAAI,GACpB,EAAM,mBAAmB,GACzB,EAAM,SAAS,CAAoB,GACnC,EAAM,eAAe,EAAI,GACrB,EAAM,SAAS,IACjB,4BAA4B;GAC1B,AAAI,EAAM,UAAU,KAAG,EAAM,YAAY,CAAoB;EAC/D,CAAC,IAED,EAAM,YAAY,CAAoB;CAE1C;CAEA,SAAS,IAAQ;EACV,EAAM,UAAU,MACrB,EAAM,gBAAgB,GACtB,EAAM,UAAU,EAAK,GACrB,EAAM,mBAAmB,GACzB,EAAM,eAAe,GACrB,EAAM,eAAe,EAAK,GAC1B,EAAM,aAAa;CACrB;CAEA,OAAO;EAAE;EAAM;CAAM;AACvB;AAUA,SAAgB,EAA4B,GAAkC;CAC5E,SAAS,EAAK,GAAqB;EAIjC,AAHA,EAAO,iBAAiB,SAAS,EAAS,OAAO,GACjD,EAAO,iBAAiB,eAAe,EAAS,aAAa,GAC7D,EAAO,iBAAiB,eAAe,EAAS,aAAa,GAC7D,EAAO,iBAAiB,iBAAiB,EAAS,QAAQ;CAC5D;CACA,SAAS,EAAO,GAAqB;EAInC,AAHA,EAAO,oBAAoB,SAAS,EAAS,OAAO,GACpD,EAAO,oBAAoB,eAAe,EAAS,aAAa,GAChE,EAAO,oBAAoB,eAAe,EAAS,aAAa,GAChE,EAAO,oBAAoB,iBAAiB,EAAS,QAAQ;CAC/D;CACA,OAAO;EAAE;EAAM;CAAO;AACxB;AAMA,SAAgB,EACd,GACA,GACA,GACA,GACM;CACN,4BAA4B;EAC1B,AAAI,EAAU,KAAK,CAAC,EAAK,QAAQ,eAAe,KAAK,CAAC,EAAS,CAAC,EAAE,QAAQ,eAAe,KACvF,EAAM;CAEV,CAAC;AACH;AAMA,SAAgB,EAAkB,GAAsB,GAAoB,GAAuB;CACjG,IAAI,MAAiB,GAAG,OAAO;CAC/B,IAAI,IAAU,IAAa;CAG3B,OAFI,IAAU,MAAG,IAAU,IAAe,IACtC,KAAW,MAAc,IAAU,IAChC;AACT"}
1
+ {"version":3,"file":"combobox-shell.js","names":[],"sources":["../../../src/shared/option-portal/combobox-shell.ts"],"sourcesContent":["import type { WebUiOption } from '@/components/option'\nimport { overlayComposition } from '@/shared/overlay/composition'\n\n/**\n * combobox 开合与 option 交互的共享骨架(select / autocomplete)。\n *\n * 与 option-portal 的边界约定一致:交互语义的\"差异点\"经回调注入,\n * 这里收口两组件逐行复制的生命周期不变量——\n * - open/close 状态迁移顺序:guard → 状态翻转 → open-change 派发 →\n * 激活钩子 → scroll-lock 配对 → portal 模式的 rAF 延迟 openOverlay\n * - close 路径:before-close 钩子(portal 内容先收敛)→ 激活清理 →\n * scroll-lock 释放 → closeOverlay(关闭后使 portal 引用失效)\n * - option 四事件监听对的绑定/解绑\n * - focusout 的 rAF :focus-within 复核\n * - 键盘导航的环绕游标算法\n */\n\nexport interface ComboboxOpenHooks {\n /** 是否允许打开(disabled / readonly 等由组件判断)。 */\n canOpen(): boolean\n getIsOpen(): boolean\n setIsOpen(open: boolean): void\n dispatchOpenChange(): void\n syncScrollLock(open: boolean): void\n /** 是否 portal 模式(决定 openOverlay 是否延迟到下一帧)。 */\n isPortal(): boolean\n openOverlay(isKeyboardNavigation: boolean): void\n closeOverlay(): void\n /** 打开时的激活项初始化(键盘导航取选中/首项,指针打开保留现状)。 */\n onOpen?(isKeyboardNavigation: boolean): void\n /** 关闭前钩子:autocomplete 需先把 light DOM 新增节点收敛进当前 portal content。 */\n onBeforeClose?(): void\n /** 关闭时清理激活状态(清 _activeIndex 与 active 属性)。 */\n onAfterClose?(): void\n}\n\nexport interface ComboboxOpenController {\n open(isKeyboardNavigation?: boolean): void\n close(): void\n}\n\nexport function createComboboxOpenController(hooks: ComboboxOpenHooks): ComboboxOpenController {\n function open(isKeyboardNavigation = false) {\n if (hooks.canOpen() === false || hooks.getIsOpen()) return\n hooks.setIsOpen(true)\n hooks.dispatchOpenChange()\n hooks.onOpen?.(isKeyboardNavigation)\n hooks.syncScrollLock(true)\n if (hooks.isPortal()) {\n requestAnimationFrame(() => {\n if (hooks.getIsOpen()) hooks.openOverlay(isKeyboardNavigation)\n })\n } else {\n hooks.openOverlay(isKeyboardNavigation)\n }\n }\n\n function close() {\n if (!hooks.getIsOpen()) return\n hooks.onBeforeClose?.()\n hooks.setIsOpen(false)\n hooks.dispatchOpenChange()\n hooks.onAfterClose?.()\n hooks.syncScrollLock(false)\n hooks.closeOverlay()\n }\n\n return { open, close }\n}\n\nexport interface OptionListenerHandlers {\n onClick: (event: Event) => void\n onPointerOver: (event: PointerEvent) => void\n onPointerDown: (event: PointerEvent) => void\n onUpdate: () => void\n}\n\n/** option 四事件监听对的绑定/解绑;两组件的 handler 签名一致,仅实现不同。 */\nexport function createOptionListenerBinding(handlers: OptionListenerHandlers) {\n function bind(option: WebUiOption) {\n option.addEventListener('click', handlers.onClick)\n option.addEventListener('pointerover', handlers.onPointerOver)\n option.addEventListener('pointerdown', handlers.onPointerDown)\n option.addEventListener('option-update', handlers.onUpdate)\n }\n function unbind(option: WebUiOption) {\n option.removeEventListener('click', handlers.onClick)\n option.removeEventListener('pointerover', handlers.onPointerOver)\n option.removeEventListener('pointerdown', handlers.onPointerDown)\n option.removeEventListener('option-update', handlers.onUpdate)\n }\n return { bind, unbind }\n}\n\n/**\n * focusout 关闭复核:焦点是否真的离开了宿主与面板。\n * rAF 等待 click/pointerdown 相关的焦点迁移完成后再判定。\n */\nexport function handleComboboxFocusOut(\n host: HTMLElement,\n getPanel: () => HTMLElement | null | undefined,\n getIsOpen: () => boolean,\n close: () => void\n): void {\n requestAnimationFrame(() => {\n const panel = getPanel()\n if (getIsOpen() && !host.matches(':focus-within') && !(panel && overlayComposition.hasFocusWithin(panel))) {\n close()\n }\n })\n}\n\n/**\n * 环绕游标:在 enabled 列表内按 delta 移动;从 -1(无激活)向下取首项、向上取末项。\n * 返回在 enabled 中的下一索引;调用方负责换算回全量 options 索引。\n */\nexport function nextWrappingIndex(enabledCount: number, currentIdx: number, delta: number): number {\n if (enabledCount === 0) return -1\n let nextIdx = currentIdx + delta\n if (nextIdx < 0) nextIdx = enabledCount - 1\n if (nextIdx >= enabledCount) nextIdx = 0\n return nextIdx\n}\n"],"mappings":";;AAyCA,SAAgB,EAA6B,GAAkD;CAC7F,SAAS,EAAK,IAAuB,IAAO;EACtC,EAAM,QAAQ,MAAM,MAAS,EAAM,UAAU,MACjD,EAAM,UAAU,EAAI,GACpB,EAAM,mBAAmB,GACzB,EAAM,SAAS,CAAoB,GACnC,EAAM,eAAe,EAAI,GACrB,EAAM,SAAS,IACjB,4BAA4B;GAC1B,AAAI,EAAM,UAAU,KAAG,EAAM,YAAY,CAAoB;EAC/D,CAAC,IAED,EAAM,YAAY,CAAoB;CAE1C;CAEA,SAAS,IAAQ;EACV,EAAM,UAAU,MACrB,EAAM,gBAAgB,GACtB,EAAM,UAAU,EAAK,GACrB,EAAM,mBAAmB,GACzB,EAAM,eAAe,GACrB,EAAM,eAAe,EAAK,GAC1B,EAAM,aAAa;CACrB;CAEA,OAAO;EAAE;EAAM;CAAM;AACvB;AAUA,SAAgB,EAA4B,GAAkC;CAC5E,SAAS,EAAK,GAAqB;EAIjC,AAHA,EAAO,iBAAiB,SAAS,EAAS,OAAO,GACjD,EAAO,iBAAiB,eAAe,EAAS,aAAa,GAC7D,EAAO,iBAAiB,eAAe,EAAS,aAAa,GAC7D,EAAO,iBAAiB,iBAAiB,EAAS,QAAQ;CAC5D;CACA,SAAS,EAAO,GAAqB;EAInC,AAHA,EAAO,oBAAoB,SAAS,EAAS,OAAO,GACpD,EAAO,oBAAoB,eAAe,EAAS,aAAa,GAChE,EAAO,oBAAoB,eAAe,EAAS,aAAa,GAChE,EAAO,oBAAoB,iBAAiB,EAAS,QAAQ;CAC/D;CACA,OAAO;EAAE;EAAM;CAAO;AACxB;AAMA,SAAgB,EACd,GACA,GACA,GACA,GACM;CACN,4BAA4B;EAC1B,IAAM,IAAQ,EAAS;EACvB,AAAI,EAAU,KAAK,CAAC,EAAK,QAAQ,eAAe,KAAK,EAAE,KAAS,EAAmB,eAAe,CAAK,MACrG,EAAM;CAEV,CAAC;AACH;AAMA,SAAgB,EAAkB,GAAsB,GAAoB,GAAuB;CACjG,IAAI,MAAiB,GAAG,OAAO;CAC/B,IAAI,IAAU,IAAa;CAG3B,OAFI,IAAU,MAAG,IAAU,IAAe,IACtC,KAAW,MAAc,IAAU,IAChC;AACT"}
@@ -1,49 +1,52 @@
1
- import { defineOverlay as e } from "./overlay.js";
2
- import { hideOverlayPresence as t, showOverlayPresence as n } from "./presence.js";
3
- import { definePlugin as r } from "@greypan/js-kit";
1
+ import { overlayComposition as e } from "./composition.js";
2
+ import { defineOverlay as t } from "./overlay.js";
3
+ import { hideOverlayPresence as n, showOverlayPresence as r } from "./presence.js";
4
+ import { definePlugin as i } from "@greypan/js-kit";
4
5
  //#region src/shared/overlay/anchored-panel.ts
5
- var i = () => r((r) => {
6
- let i, a, o = () => a?.panel ?? r.getLocalPanel(), s = () => {
7
- a && (a.restoreContent(), a.remove(), a = void 0, i = void 0);
8
- }, c = () => r.isPortal() ? (a ??= r.createPortal(), a.panel) : r.getLocalPanel(), l = (t, n) => {
9
- if (i) {
10
- i.updateAnchor(t), i.update(r.getPositioning());
6
+ var a = () => i((i) => {
7
+ let a, o, s = () => o?.panel ?? i.getLocalPanel(), c = () => {
8
+ o && (e.unregisterPanel(o.panel), o.restoreContent(), o.remove(), o = void 0, a = void 0);
9
+ }, l = () => i.isPortal() ? (o ??= i.createPortal(), o.panel) : i.getLocalPanel(), u = (e, n) => {
10
+ if (a) {
11
+ a.updateAnchor(e), a.update(i.getPositioning());
11
12
  return;
12
13
  }
13
- i = e().make({
14
- anchor: t,
14
+ a = t().make({
15
+ anchor: e,
15
16
  overlay: n,
16
- ...r.getPositioning()
17
+ ...i.getPositioning()
17
18
  });
18
- }, u = (e = !1) => {
19
- let t = r.getAnchor();
20
- if (!t) return;
21
- let a = c();
22
- a && (l(t, a), i?.open(), n(a, { isInstant: e }));
23
- }, d = () => {
24
- i?.dispose(), i = void 0, s();
19
+ }, d = (t = !1) => {
20
+ let n = i.getAnchor();
21
+ if (!n) return;
22
+ let o = l();
23
+ o && (e.registerPanelFromAncestry(o, n), u(n, o), a?.open(), r(o, { isInstant: t }));
24
+ }, f = () => {
25
+ a?.dispose(), a = void 0;
26
+ let t = s();
27
+ t && e.unregisterPanel(t), c();
25
28
  };
26
29
  return {
27
30
  getPanel() {
28
- return o();
31
+ return s();
29
32
  },
30
- open: u,
31
- async close(e) {
32
- i?.close();
33
- let n = o();
34
- return n && !await t(n) || e() ? !1 : (s(), !0);
33
+ open: d,
34
+ async close(t) {
35
+ a?.close();
36
+ let r = s();
37
+ return r && !await n(r) || t() ? !1 : (r && e.unregisterPanel(r), c(), !0);
35
38
  },
36
39
  updatePosition() {
37
- i?.update(r.getPositioning());
40
+ a?.update(i.getPositioning());
38
41
  },
39
42
  reconfigure(e) {
40
- let t = o()?.dataset.wuiPresence === "entering";
41
- d(), e && u(!t);
43
+ let t = s()?.dataset.wuiPresence === "entering";
44
+ f(), e && d(!t);
42
45
  },
43
- dispose: d
46
+ dispose: f
44
47
  };
45
48
  });
46
49
  //#endregion
47
- export { i as defineAnchoredPanel };
50
+ export { a as defineAnchoredPanel };
48
51
 
49
52
  //# sourceMappingURL=anchored-panel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"anchored-panel.js","names":[],"sources":["../../../src/shared/overlay/anchored-panel.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\n\nimport type { OverlayOptions } from './overlay'\nimport { defineOverlay } from './overlay'\nimport type { OverlayApi } from './overlay'\nimport type { OverlayPortal } from './portal'\nimport { hideOverlayPresence, showOverlayPresence } from './presence'\n\nexport interface AnchoredPanelOptions {\n getAnchor(): HTMLElement | null\n getLocalPanel(): HTMLElement | null\n getPositioning(): OverlayOptions\n isPortal(): boolean\n createPortal(): OverlayPortal\n}\n\n/**\n * 管理锚定面板的本地/Portal 容器、定位器和可中断 presence。\n * 内容迁移与交互语义由各组件保留,避免把不同组件的 public contract 混入此模块。\n */\nexport interface AnchoredPanelApi {\n getPanel(): HTMLElement | null\n open(isInstant?: boolean): void\n close(isStillOpen: () => boolean): Promise<boolean>\n updatePosition(): void\n reconfigure(isOpen: boolean): void\n dispose(): void\n}\n\nexport const defineAnchoredPanel = () =>\n definePlugin<AnchoredPanelApi, AnchoredPanelOptions>(ctx => {\n let overlay: OverlayApi | undefined\n let portal: OverlayPortal | undefined\n\n const getPanel = () => portal?.panel ?? ctx.getLocalPanel()\n\n const disposePortal = () => {\n if (!portal) return\n portal.restoreContent()\n portal.remove()\n portal = undefined\n overlay = undefined\n }\n\n const getOrCreatePanel = (): HTMLElement | null => {\n if (!ctx.isPortal()) return ctx.getLocalPanel()\n portal ??= ctx.createPortal()\n return portal.panel\n }\n\n const ensureOverlay = (anchor: HTMLElement, panel: HTMLElement) => {\n if (overlay) {\n overlay.updateAnchor(anchor)\n overlay.update(ctx.getPositioning())\n return\n }\n overlay = defineOverlay().make({ anchor, overlay: panel, ...ctx.getPositioning() })\n }\n\n const open = (isInstant = false) => {\n const anchor = ctx.getAnchor()\n if (!anchor) return\n const panel = getOrCreatePanel()\n if (!panel) return\n ensureOverlay(anchor, panel)\n overlay?.open()\n showOverlayPresence(panel, { isInstant })\n }\n\n const dispose = () => {\n overlay?.dispose()\n overlay = undefined\n disposePortal()\n }\n\n return {\n getPanel() {\n return getPanel()\n },\n\n open,\n\n async close(isStillOpen) {\n overlay?.close()\n const panel = getPanel()\n if (panel && !(await hideOverlayPresence(panel))) return false\n if (isStillOpen()) return false\n disposePortal()\n return true\n },\n\n updatePosition() {\n overlay?.update(ctx.getPositioning())\n },\n\n reconfigure(isOpen) {\n const shouldAnimate = getPanel()?.dataset.wuiPresence === 'entering'\n dispose()\n if (isOpen) open(!shouldAnimate)\n },\n\n dispose\n }\n })\n"],"mappings":";;;;AA6BA,IAAa,UACX,GAAqD,MAAO;CAC1D,IAAI,GACA,GAEE,UAAiB,GAAQ,SAAS,EAAI,cAAc,GAEpD,UAAsB;EACrB,MACL,EAAO,eAAe,GACtB,EAAO,OAAO,GACd,IAAS,KAAA,GACT,IAAU,KAAA;CACZ,GAEM,UACC,EAAI,SAAS,KAClB,MAAW,EAAI,aAAa,GACrB,EAAO,SAFc,EAAI,cAAc,GAK1C,KAAiB,GAAqB,MAAuB;EACjE,IAAI,GAAS;GAEX,AADA,EAAQ,aAAa,CAAM,GAC3B,EAAQ,OAAO,EAAI,eAAe,CAAC;GACnC;EACF;EACA,IAAU,EAAc,CAAC,CAAC,KAAK;GAAE;GAAQ,SAAS;GAAO,GAAG,EAAI,eAAe;EAAE,CAAC;CACpF,GAEM,KAAQ,IAAY,OAAU;EAClC,IAAM,IAAS,EAAI,UAAU;EAC7B,IAAI,CAAC,GAAQ;EACb,IAAM,IAAQ,EAAiB;EAC1B,MACL,EAAc,GAAQ,CAAK,GAC3B,GAAS,KAAK,GACd,EAAoB,GAAO,EAAE,aAAU,CAAC;CAC1C,GAEM,UAAgB;EAGpB,AAFA,GAAS,QAAQ,GACjB,IAAU,KAAA,GACV,EAAc;CAChB;CAEA,OAAO;EACL,WAAW;GACT,OAAO,EAAS;EAClB;EAEA;EAEA,MAAM,MAAM,GAAa;GACvB,GAAS,MAAM;GACf,IAAM,IAAQ,EAAS;GAIvB,OAHI,KAAS,CAAE,MAAM,EAAoB,CAAK,KAC1C,EAAY,IAAU,MAC1B,EAAc,GACP;EACT;EAEA,iBAAiB;GACf,GAAS,OAAO,EAAI,eAAe,CAAC;EACtC;EAEA,YAAY,GAAQ;GAClB,IAAM,IAAgB,EAAS,CAAC,EAAE,QAAQ,gBAAgB;GAE1D,AADA,EAAQ,GACJ,KAAQ,EAAK,CAAC,CAAa;EACjC;EAEA;CACF;AACF,CAAC"}
1
+ {"version":3,"file":"anchored-panel.js","names":[],"sources":["../../../src/shared/overlay/anchored-panel.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\n\nimport { overlayComposition } from './composition'\nimport type { OverlayOptions } from './overlay'\nimport { defineOverlay } from './overlay'\nimport type { OverlayApi } from './overlay'\nimport type { OverlayPortal } from './portal'\nimport { hideOverlayPresence, showOverlayPresence } from './presence'\n\nexport interface AnchoredPanelOptions {\n getAnchor(): HTMLElement | null\n getLocalPanel(): HTMLElement | null\n getPositioning(): OverlayOptions\n isPortal(): boolean\n createPortal(): OverlayPortal\n}\n\n/**\n * 管理锚定面板的本地/Portal 容器、定位器和可中断 presence。\n * 内容迁移与交互语义由各组件保留,避免把不同组件的 public contract 混入此模块。\n */\nexport interface AnchoredPanelApi {\n getPanel(): HTMLElement | null\n open(isInstant?: boolean): void\n close(isStillOpen: () => boolean): Promise<boolean>\n updatePosition(): void\n reconfigure(isOpen: boolean): void\n dispose(): void\n}\n\nexport const defineAnchoredPanel = () =>\n definePlugin<AnchoredPanelApi, AnchoredPanelOptions>(ctx => {\n let overlay: OverlayApi | undefined\n let portal: OverlayPortal | undefined\n\n const getPanel = () => portal?.panel ?? ctx.getLocalPanel()\n\n const disposePortal = () => {\n if (!portal) return\n overlayComposition.unregisterPanel(portal.panel)\n portal.restoreContent()\n portal.remove()\n portal = undefined\n overlay = undefined\n }\n\n const getOrCreatePanel = (): HTMLElement | null => {\n if (!ctx.isPortal()) return ctx.getLocalPanel()\n portal ??= ctx.createPortal()\n return portal.panel\n }\n\n const ensureOverlay = (anchor: HTMLElement, panel: HTMLElement) => {\n if (overlay) {\n overlay.updateAnchor(anchor)\n overlay.update(ctx.getPositioning())\n return\n }\n overlay = defineOverlay().make({ anchor, overlay: panel, ...ctx.getPositioning() })\n }\n\n const open = (isInstant = false) => {\n const anchor = ctx.getAnchor()\n if (!anchor) return\n const panel = getOrCreatePanel()\n if (!panel) return\n // portal panel 与宿主物理分离;本地 panel 也可能隔着 shadow 边界。\n // 以 anchor 祖先链寻找最近的已登记 overlay,建立统一逻辑父子关系。\n overlayComposition.registerPanelFromAncestry(panel, anchor)\n ensureOverlay(anchor, panel)\n overlay?.open()\n showOverlayPresence(panel, { isInstant })\n }\n\n const dispose = () => {\n overlay?.dispose()\n overlay = undefined\n const panel = getPanel()\n if (panel) overlayComposition.unregisterPanel(panel)\n disposePortal()\n }\n\n return {\n getPanel() {\n return getPanel()\n },\n\n open,\n\n async close(isStillOpen) {\n overlay?.close()\n const panel = getPanel()\n if (panel && !(await hideOverlayPresence(panel))) return false\n if (isStillOpen()) return false\n if (panel) overlayComposition.unregisterPanel(panel)\n disposePortal()\n return true\n },\n\n updatePosition() {\n overlay?.update(ctx.getPositioning())\n },\n\n reconfigure(isOpen) {\n const shouldAnimate = getPanel()?.dataset.wuiPresence === 'entering'\n dispose()\n if (isOpen) open(!shouldAnimate)\n },\n\n dispose\n }\n })\n"],"mappings":";;;;;AA8BA,IAAa,UACX,GAAqD,MAAO;CAC1D,IAAI,GACA,GAEE,UAAiB,GAAQ,SAAS,EAAI,cAAc,GAEpD,UAAsB;EACrB,MACL,EAAmB,gBAAgB,EAAO,KAAK,GAC/C,EAAO,eAAe,GACtB,EAAO,OAAO,GACd,IAAS,KAAA,GACT,IAAU,KAAA;CACZ,GAEM,UACC,EAAI,SAAS,KAClB,MAAW,EAAI,aAAa,GACrB,EAAO,SAFc,EAAI,cAAc,GAK1C,KAAiB,GAAqB,MAAuB;EACjE,IAAI,GAAS;GAEX,AADA,EAAQ,aAAa,CAAM,GAC3B,EAAQ,OAAO,EAAI,eAAe,CAAC;GACnC;EACF;EACA,IAAU,EAAc,CAAC,CAAC,KAAK;GAAE;GAAQ,SAAS;GAAO,GAAG,EAAI,eAAe;EAAE,CAAC;CACpF,GAEM,KAAQ,IAAY,OAAU;EAClC,IAAM,IAAS,EAAI,UAAU;EAC7B,IAAI,CAAC,GAAQ;EACb,IAAM,IAAQ,EAAiB;EAC1B,MAGL,EAAmB,0BAA0B,GAAO,CAAM,GAC1D,EAAc,GAAQ,CAAK,GAC3B,GAAS,KAAK,GACd,EAAoB,GAAO,EAAE,aAAU,CAAC;CAC1C,GAEM,UAAgB;EAEpB,AADA,GAAS,QAAQ,GACjB,IAAU,KAAA;EACV,IAAM,IAAQ,EAAS;EAEvB,AADI,KAAO,EAAmB,gBAAgB,CAAK,GACnD,EAAc;CAChB;CAEA,OAAO;EACL,WAAW;GACT,OAAO,EAAS;EAClB;EAEA;EAEA,MAAM,MAAM,GAAa;GACvB,GAAS,MAAM;GACf,IAAM,IAAQ,EAAS;GAKvB,OAJI,KAAS,CAAE,MAAM,EAAoB,CAAK,KAC1C,EAAY,IAAU,MACtB,KAAO,EAAmB,gBAAgB,CAAK,GACnD,EAAc,GACP;EACT;EAEA,iBAAiB;GACf,GAAS,OAAO,EAAI,eAAe,CAAC;EACtC;EAEA,YAAY,GAAQ;GAClB,IAAM,IAAgB,EAAS,CAAC,EAAE,QAAQ,gBAAgB;GAE1D,AADA,EAAQ,GACJ,KAAQ,EAAK,CAAC,CAAa;EACjC;EAEA;CACF;AACF,CAAC"}
@@ -0,0 +1,57 @@
1
+ //#region src/shared/overlay/composition.ts
2
+ var e = /* @__PURE__ */ new WeakMap(), t = /* @__PURE__ */ new WeakMap(), n = /* @__PURE__ */ new WeakMap();
3
+ function r(t) {
4
+ let n = e.get(t);
5
+ return n || (n = { children: /* @__PURE__ */ new Set() }, e.set(t, n)), n;
6
+ }
7
+ function i(e) {
8
+ return e instanceof Element && e.assignedSlot ? e.assignedSlot : e.parentNode ? e.parentNode : e instanceof ShadowRoot ? e.host : null;
9
+ }
10
+ function a(e, t) {
11
+ let n = t;
12
+ for (; n;) {
13
+ if (n === e || e.contains(n)) return !0;
14
+ n = i(n);
15
+ }
16
+ return !1;
17
+ }
18
+ function o(e) {
19
+ let r = t.get(e);
20
+ r && (n.get(r)?.delete(e), n.get(r)?.size === 0 && n.delete(r), t.delete(e));
21
+ }
22
+ var s = {
23
+ registerPanel(e, i) {
24
+ if (e === i || (o(e), r(e), !i)) return;
25
+ r(i);
26
+ let a = n.get(i) ?? /* @__PURE__ */ new Set();
27
+ a.add(e), n.set(i, a), t.set(e, i);
28
+ },
29
+ registerPanelFromAncestry(t, n) {
30
+ let r = n === t ? i(n) : n;
31
+ for (; r;) {
32
+ if (r instanceof HTMLElement && r !== t && e.has(r)) return this.registerPanel(t, r), r;
33
+ r = i(r);
34
+ }
35
+ this.registerPanel(t), o(t);
36
+ },
37
+ unregisterPanel(t) {
38
+ Array.from(n.get(t) ?? []).forEach((e) => this.unregisterPanel(e)), o(t), n.delete(t), e.delete(t);
39
+ },
40
+ contains(e, t) {
41
+ if (a(e, t)) return !0;
42
+ for (let r of n.get(e) ?? []) if (this.contains(r, t)) return !0;
43
+ return !1;
44
+ },
45
+ containsEvent(e, t) {
46
+ return t.composedPath().some((t) => t instanceof Node && this.contains(e, t));
47
+ },
48
+ hasFocusWithin(e) {
49
+ if (e.matches(":focus-within")) return !0;
50
+ for (let t of n.get(e) ?? []) if (this.hasFocusWithin(t)) return !0;
51
+ return !1;
52
+ }
53
+ };
54
+ //#endregion
55
+ export { s as overlayComposition };
56
+
57
+ //# sourceMappingURL=composition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composition.js","names":[],"sources":["../../../src/shared/overlay/composition.ts"],"sourcesContent":["/**\n * Overlay 逻辑祖先关系注册表。\n *\n * Portal 会让 DOM 祖先链与用户感知的组合关系分离:submenu、嵌套 popover 或\n * panel 内的子 overlay 通常被迁到独立 overlay root。交互所有权必须以这里登记\n * 的逻辑树判定,而不是继续在各组件内复制 contains()。\n */\n\ninterface OverlayPanelEntry {\n children: Set<HTMLElement>\n}\n\nconst entries = new WeakMap<HTMLElement, OverlayPanelEntry>()\nconst parents = new WeakMap<HTMLElement, HTMLElement>()\nconst children = new WeakMap<HTMLElement, Set<HTMLElement>>()\n\nfunction entryFor(panel: HTMLElement): OverlayPanelEntry {\n let entry = entries.get(panel)\n if (!entry) {\n entry = { children: new Set() }\n entries.set(panel, entry)\n }\n return entry\n}\n\n/** 判断 node 是否在 root 的物理或 shadow DOM 子树内。 */\nfunction nextCompositionNode(node: Node): Node | null {\n if (node instanceof Element && node.assignedSlot) return node.assignedSlot\n if (node.parentNode) return node.parentNode\n if (node instanceof ShadowRoot) return node.host\n return null\n}\n\nfunction nodeIsInside(root: HTMLElement, node: Node): boolean {\n let current: Node | null = node\n while (current) {\n if (current === root || root.contains(current)) return true\n current = nextCompositionNode(current)\n }\n return false\n}\n\nfunction removeFromParent(panel: HTMLElement): void {\n const parent = parents.get(panel)\n if (!parent) return\n children.get(parent)?.delete(panel)\n if (children.get(parent)?.size === 0) children.delete(parent)\n parents.delete(panel)\n}\n\nexport const overlayComposition = {\n /**\n * 登记 panel;parentPanel 存在时建立逻辑父子关系。重复登记会迁移到新父级,\n * 但不影响 panel 已有后代,供同 panel 重定位/恢复场景复用。\n */\n registerPanel(panel: HTMLElement, parentPanel?: HTMLElement): void {\n if (panel === parentPanel) return\n removeFromParent(panel)\n entryFor(panel)\n if (!parentPanel) return\n\n entryFor(parentPanel)\n const siblings = children.get(parentPanel) ?? new Set<HTMLElement>()\n siblings.add(panel)\n children.set(parentPanel, siblings)\n parents.set(panel, parentPanel)\n },\n\n /**\n * 通过 owner 的物理祖先链寻找已登记 overlay panel。panel 可以是本地 shadow\n * panel,也可以是 portal panel;portal 场景传入 portal target 即可找到原组合父级。\n */\n registerPanelFromAncestry(panel: HTMLElement, owner: Node): HTMLElement | undefined {\n let current: Node | null = owner === panel ? nextCompositionNode(owner) : owner\n while (current) {\n if (current instanceof HTMLElement && current !== panel && entries.has(current)) {\n this.registerPanel(panel, current)\n return current\n }\n current = nextCompositionNode(current)\n }\n this.registerPanel(panel)\n removeFromParent(panel)\n return undefined\n },\n\n /** 注销 panel 及其全部逻辑后代,避免关闭后留下 stale ancestry。 */\n unregisterPanel(panel: HTMLElement): void {\n const descendants = Array.from(children.get(panel) ?? [])\n descendants.forEach(descendant => this.unregisterPanel(descendant))\n removeFromParent(panel)\n children.delete(panel)\n entries.delete(panel)\n },\n\n /** target 是否属于 panel 自身或任何已登记逻辑后代 panel。 */\n contains(panel: HTMLElement, target: Node): boolean {\n if (nodeIsInside(panel, target)) return true\n for (const child of children.get(panel) ?? []) {\n if (this.contains(child, target)) return true\n }\n return false\n },\n\n /** event 的完整 composed path 是否落入 panel 的逻辑组合子树。 */\n containsEvent(panel: HTMLElement, event: Event): boolean {\n return event.composedPath().some(node => node instanceof Node && this.contains(panel, node))\n },\n\n /** 焦点是否在 panel 或其逻辑后代 panel 内。 */\n hasFocusWithin(panel: HTMLElement): boolean {\n if (panel.matches(':focus-within')) return true\n for (const child of children.get(panel) ?? []) {\n if (this.hasFocusWithin(child)) return true\n }\n return false\n }\n}\n"],"mappings":";AAYA,IAAM,oBAAU,IAAI,QAAwC,GACtD,oBAAU,IAAI,QAAkC,GAChD,oBAAW,IAAI,QAAuC;AAE5D,SAAS,EAAS,GAAuC;CACvD,IAAI,IAAQ,EAAQ,IAAI,CAAK;CAK7B,OAJK,MACH,IAAQ,EAAE,0BAAU,IAAI,IAAI,EAAE,GAC9B,EAAQ,IAAI,GAAO,CAAK,IAEnB;AACT;AAGA,SAAS,EAAoB,GAAyB;CAIpD,OAHI,aAAgB,WAAW,EAAK,eAAqB,EAAK,eAC1D,EAAK,aAAmB,EAAK,aAC7B,aAAgB,aAAmB,EAAK,OACrC;AACT;AAEA,SAAS,EAAa,GAAmB,GAAqB;CAC5D,IAAI,IAAuB;CAC3B,OAAO,IAAS;EACd,IAAI,MAAY,KAAQ,EAAK,SAAS,CAAO,GAAG,OAAO;EACvD,IAAU,EAAoB,CAAO;CACvC;CACA,OAAO;AACT;AAEA,SAAS,EAAiB,GAA0B;CAClD,IAAM,IAAS,EAAQ,IAAI,CAAK;CAC3B,MACL,EAAS,IAAI,CAAM,CAAC,EAAE,OAAO,CAAK,GAC9B,EAAS,IAAI,CAAM,CAAC,EAAE,SAAS,KAAG,EAAS,OAAO,CAAM,GAC5D,EAAQ,OAAO,CAAK;AACtB;AAEA,IAAa,IAAqB;CAKhC,cAAc,GAAoB,GAAiC;EAIjE,IAHI,MAAU,MACd,EAAiB,CAAK,GACtB,EAAS,CAAK,GACV,CAAC,IAAa;EAElB,EAAS,CAAW;EACpB,IAAM,IAAW,EAAS,IAAI,CAAW,qBAAK,IAAI,IAAiB;EAGnE,AAFA,EAAS,IAAI,CAAK,GAClB,EAAS,IAAI,GAAa,CAAQ,GAClC,EAAQ,IAAI,GAAO,CAAW;CAChC;CAMA,0BAA0B,GAAoB,GAAsC;EAClF,IAAI,IAAuB,MAAU,IAAQ,EAAoB,CAAK,IAAI;EAC1E,OAAO,IAAS;GACd,IAAI,aAAmB,eAAe,MAAY,KAAS,EAAQ,IAAI,CAAO,GAE5E,OADA,KAAK,cAAc,GAAO,CAAO,GAC1B;GAET,IAAU,EAAoB,CAAO;EACvC;EAEA,AADA,KAAK,cAAc,CAAK,GACxB,EAAiB,CAAK;CAExB;CAGA,gBAAgB,GAA0B;EAKxC,AAHA,MAD0B,KAAK,EAAS,IAAI,CAAK,KAAK,CAAC,CACvD,CAAA,CAAY,SAAQ,MAAc,KAAK,gBAAgB,CAAU,CAAC,GAClE,EAAiB,CAAK,GACtB,EAAS,OAAO,CAAK,GACrB,EAAQ,OAAO,CAAK;CACtB;CAGA,SAAS,GAAoB,GAAuB;EAClD,IAAI,EAAa,GAAO,CAAM,GAAG,OAAO;EACxC,KAAK,IAAM,KAAS,EAAS,IAAI,CAAK,KAAK,CAAC,GAC1C,IAAI,KAAK,SAAS,GAAO,CAAM,GAAG,OAAO;EAE3C,OAAO;CACT;CAGA,cAAc,GAAoB,GAAuB;EACvD,OAAO,EAAM,aAAa,CAAC,CAAC,MAAK,MAAQ,aAAgB,QAAQ,KAAK,SAAS,GAAO,CAAI,CAAC;CAC7F;CAGA,eAAe,GAA6B;EAC1C,IAAI,EAAM,QAAQ,eAAe,GAAG,OAAO;EAC3C,KAAK,IAAM,KAAS,EAAS,IAAI,CAAK,KAAK,CAAC,GAC1C,IAAI,KAAK,eAAe,CAAK,GAAG,OAAO;EAEzC,OAAO;CACT;AACF"}
@@ -0,0 +1,41 @@
1
+ import { definePlugin as e } from "@greypan/js-kit";
2
+ //#region src/shared/overlay/lifecycle.ts
3
+ var t = () => e((e) => {
4
+ let t = 0, n = !1, r = /* @__PURE__ */ new Set(), i = (e) => {
5
+ cancelAnimationFrame(e.id), r.delete(e);
6
+ }, a = () => {
7
+ if (!n) {
8
+ t += 1;
9
+ for (let e of Array.from(r)) i(e);
10
+ }
11
+ };
12
+ return {
13
+ getGeneration() {
14
+ return t;
15
+ },
16
+ invalidate: a,
17
+ scheduleFrame(a, o = {}) {
18
+ if (n) return { cancel() {} };
19
+ let s = t, c = {
20
+ generation: s,
21
+ id: requestAnimationFrame(() => {
22
+ r.delete(c), !(n || s !== t) && e.isConnected() && (o.expectedOpen === void 0 || e.isOpen() === o.expectedOpen) && a(s);
23
+ }),
24
+ cancel() {
25
+ i(c);
26
+ }
27
+ };
28
+ return r.add(c), c;
29
+ },
30
+ dispose() {
31
+ n || (n = !0, a());
32
+ },
33
+ resume() {
34
+ n && (n = !1, t += 1);
35
+ }
36
+ };
37
+ });
38
+ //#endregion
39
+ export { t as defineOverlayLifecycle };
40
+
41
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.js","names":[],"sources":["../../../src/shared/overlay/lifecycle.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\n\nexport interface OverlayLifecycleHost {\n isConnected(): boolean\n isOpen(): boolean\n}\n\nexport interface OverlayFrameOptions {\n /**\n * 显式要求回调执行时 open 状态必须等于该值;未传入时只校验连接状态。\n */\n expectedOpen?: boolean\n}\n\nexport interface OverlayFrameHandle {\n cancel(): void\n}\n\nexport interface OverlayLifecycle {\n /** generation 只能通过方法读取:definePlugin.make 展开对象时会立即消费 getter。 */\n getGeneration(): number\n /** 结束当前事务:取消该事务内尚未执行的 frame,并让旧 generation 回调失效。 */\n invalidate(): void\n scheduleFrame(callback: (generation: number) => void, options?: OverlayFrameOptions): OverlayFrameHandle\n dispose(): void\n /** temporary disconnect 后恢复调度;generation 继续单调递增,旧事务保持失效。 */\n resume(): void\n}\n\n/**\n * Overlay 组件共享的帧级事务调度。portal / presence / positioning 可能跨多帧,\n * 同帧 controlled open/close、disconnect 或 reconfigure 都必须使旧事务失效,\n * 而不是让每个组件各自散落 open / isConnected 复查。\n */\nexport const defineOverlayLifecycle = () =>\n definePlugin<OverlayLifecycle, OverlayLifecycleHost>(host => {\n let generation = 0\n let disposed = false\n const frames = new Set<OverlayFrameHandle & { id: number; generation: number }>()\n\n const cancelFrame = (frame: OverlayFrameHandle & { id: number; generation: number }) => {\n cancelAnimationFrame(frame.id)\n frames.delete(frame)\n }\n\n const invalidateCurrent = () => {\n if (disposed) return\n generation += 1\n for (const frame of Array.from(frames)) cancelFrame(frame)\n }\n\n return {\n getGeneration() {\n return generation\n },\n\n invalidate: invalidateCurrent,\n\n scheduleFrame(callback, options = {}) {\n if (disposed) return { cancel() {} }\n\n const scheduledGeneration = generation\n const frame = {\n generation: scheduledGeneration,\n id: requestAnimationFrame(() => {\n frames.delete(frame)\n if (disposed || scheduledGeneration !== generation) return\n if (!host.isConnected()) return\n if (options.expectedOpen !== undefined && host.isOpen() !== options.expectedOpen) return\n callback(scheduledGeneration)\n }),\n cancel() {\n cancelFrame(frame)\n }\n }\n frames.add(frame)\n return frame\n },\n\n dispose() {\n if (disposed) return\n disposed = true\n invalidateCurrent()\n },\n\n resume() {\n if (!disposed) return\n disposed = false\n generation += 1\n }\n }\n })\n"],"mappings":";;AAkCA,IAAa,UACX,GAAqD,MAAQ;CAC3D,IAAI,IAAa,GACb,IAAW,IACT,oBAAS,IAAI,IAA6D,GAE1E,KAAe,MAAmE;EAEtF,AADA,qBAAqB,EAAM,EAAE,GAC7B,EAAO,OAAO,CAAK;CACrB,GAEM,UAA0B;EAC1B,QACJ;QAAc;GACd,KAAK,IAAM,KAAS,MAAM,KAAK,CAAM,GAAG,EAAY,CAAK;EAD3C;CAEhB;CAEA,OAAO;EACL,gBAAgB;GACd,OAAO;EACT;EAEA,YAAY;EAEZ,cAAc,GAAU,IAAU,CAAC,GAAG;GACpC,IAAI,GAAU,OAAO,EAAE,SAAS,CAAC,EAAE;GAEnC,IAAM,IAAsB,GACtB,IAAQ;IACZ,YAAY;IACZ,IAAI,4BAA4B;KAC9B,EAAO,OAAO,CAAK,GACf,OAAY,MAAwB,MACnC,EAAK,YAAY,MAClB,EAAQ,iBAAiB,KAAA,KAAa,EAAK,OAAO,MAAM,EAAQ,iBACpE,EAAS,CAAmB;IAC9B,CAAC;IACD,SAAS;KACP,EAAY,CAAK;IACnB;GACF;GAEA,OADA,EAAO,IAAI,CAAK,GACT;EACT;EAEA,UAAU;GACJ,MACJ,IAAW,IACX,EAAkB;EACpB;EAEA,SAAS;GACF,MACL,IAAW,IACX,KAAc;EAChB;CACF;AACF,CAAC"}
@@ -2,45 +2,45 @@ import { getTransitionDuration as e } from "./presence.js";
2
2
  import { definePlugin as t } from "@greypan/js-kit";
3
3
  //#region src/shared/overlay/native-dialog-presence.ts
4
4
  var n = () => t((t) => {
5
- let n, r, i = !1, a = () => {
5
+ let n, r, i = !1, a = !1, o = () => {
6
6
  n !== void 0 && (clearTimeout(n), n = void 0);
7
- }, o = () => {
8
- r !== void 0 && (cancelAnimationFrame(r), r = void 0);
9
7
  }, s = () => {
10
- let e = t.getDialog();
11
- !e || !i || t.isOpen() || (a(), i = !1, e.classList.remove("is-closing", "is-visible"), e.open && e.close?.());
8
+ r !== void 0 && (cancelAnimationFrame(r), r = void 0);
12
9
  }, c = () => {
10
+ let e = t.getDialog();
11
+ !e || !i || t.isOpen() || (o(), i = !1, e.classList.remove("is-closing", "is-visible"), e.open && (a = !0, e.close?.()));
12
+ }, l = () => {
13
13
  let e = t.getDialog();
14
14
  if (e) {
15
- if (i = !1, a(), e.classList.remove("is-closing"), !e.open) try {
15
+ if (i = !1, o(), e.classList.remove("is-closing"), !e.open) try {
16
16
  e.showModal?.();
17
17
  } catch {}
18
- o(), r = requestAnimationFrame(() => {
18
+ e.offsetWidth, s(), r = requestAnimationFrame(() => {
19
19
  r = void 0, !(!t.isConnected() || !t.isOpen() || !e.open) && e.classList.add("is-visible");
20
20
  });
21
21
  }
22
- }, l = () => {
22
+ }, u = () => {
23
23
  let r = t.getDialog();
24
24
  if (r?.open) {
25
- if (o(), i = !0, !r.classList.contains("is-visible")) {
26
- s();
25
+ if (s(), i = !0, !r.classList.contains("is-visible")) {
26
+ c();
27
27
  return;
28
28
  }
29
- r.classList.add("is-closing"), r.classList.remove("is-visible"), a(), n = setTimeout(() => s(), e(r) + 80);
29
+ r.classList.add("is-closing"), r.classList.remove("is-visible"), o(), n = setTimeout(() => c(), e(r) + 80);
30
30
  }
31
31
  };
32
32
  return {
33
33
  sync(e) {
34
- e ? c() : l();
34
+ e ? l() : u();
35
35
  },
36
36
  handleTransitionEnd(e) {
37
- e.target === t.getDialog() && e.propertyName === "transform" && s();
37
+ e.target === t.getDialog() && e.propertyName === "transform" && c();
38
38
  },
39
39
  handleNativeClose() {
40
- o(), a(), i = !1, t.getDialog()?.classList.remove("is-closing", "is-visible");
40
+ return a ? (a = !1, !0) : (s(), o(), i = !1, t.getDialog()?.classList.remove("is-closing", "is-visible"), !1);
41
41
  },
42
42
  dispose() {
43
- o(), a(), i = !1;
43
+ s(), o(), i = !1, a = !1;
44
44
  }
45
45
  };
46
46
  });
@@ -1 +1 @@
1
- {"version":3,"file":"native-dialog-presence.js","names":[],"sources":["../../../src/shared/overlay/native-dialog-presence.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\n\nimport { getTransitionDuration } from './presence'\n\nexport interface NativeDialogPresenceOptions {\n getDialog(): HTMLDialogElement | null\n isConnected(): boolean\n isOpen(): boolean\n}\n\nexport interface NativeDialogPresenceApi {\n sync(open: boolean): void\n handleTransitionEnd(event: TransitionEvent): void\n handleNativeClose(): void\n dispose(): void\n}\n\n// 管理原生 dialog 保持在 top layer 直至退出动画完成的生命周期。\nexport const defineNativeDialogPresence = () =>\n definePlugin<NativeDialogPresenceApi, NativeDialogPresenceOptions>(ctx => {\n let closeFallbackTimer: ReturnType<typeof setTimeout> | undefined\n let openFrame: number | undefined\n let isClosing = false\n\n const clearCloseFallback = () => {\n if (closeFallbackTimer === undefined) return\n clearTimeout(closeFallbackTimer)\n closeFallbackTimer = undefined\n }\n\n const cancelOpenFrame = () => {\n if (openFrame === undefined) return\n cancelAnimationFrame(openFrame)\n openFrame = undefined\n }\n\n const finishClosing = () => {\n const dialog = ctx.getDialog()\n if (!dialog || !isClosing || ctx.isOpen()) return\n clearCloseFallback()\n isClosing = false\n dialog.classList.remove('is-closing', 'is-visible')\n if (dialog.open) dialog.close?.()\n }\n\n const startOpening = () => {\n const dialog = ctx.getDialog()\n if (!dialog) return\n isClosing = false\n clearCloseFallback()\n dialog.classList.remove('is-closing')\n if (!dialog.open) {\n try {\n // 并发多个 modal dialog(nested drawer 场景)在规范中合法;\n // 此 try/catch 为防御性兜底(如对已在 top layer 的 dialog 重复 showModal\n // 会抛 InvalidStateError),失败时由下方 rAF 回调的 dialog.open 判断中止动画。\n dialog.showModal?.()\n } catch {\n // 保留未打开状态\n }\n }\n\n cancelOpenFrame()\n openFrame = requestAnimationFrame(() => {\n openFrame = undefined\n if (!ctx.isConnected() || !ctx.isOpen() || !dialog.open) return\n dialog.classList.add('is-visible')\n })\n }\n\n const startClosing = () => {\n const dialog = ctx.getDialog()\n if (!dialog?.open) return\n cancelOpenFrame()\n isClosing = true\n if (!dialog.classList.contains('is-visible')) {\n finishClosing()\n return\n }\n\n dialog.classList.add('is-closing')\n dialog.classList.remove('is-visible')\n clearCloseFallback()\n closeFallbackTimer = setTimeout(() => finishClosing(), getTransitionDuration(dialog) + 80)\n }\n\n return {\n sync(open) {\n if (open) startOpening()\n else startClosing()\n },\n\n handleTransitionEnd(event) {\n if (event.target !== ctx.getDialog() || event.propertyName !== 'transform') return\n finishClosing()\n },\n\n handleNativeClose() {\n cancelOpenFrame()\n clearCloseFallback()\n isClosing = false\n ctx.getDialog()?.classList.remove('is-closing', 'is-visible')\n },\n\n dispose() {\n cancelOpenFrame()\n clearCloseFallback()\n isClosing = false\n }\n }\n })\n"],"mappings":";;;AAkBA,IAAa,UACX,GAAmE,MAAO;CACxE,IAAI,GACA,GACA,IAAY,IAEV,UAA2B;EAC3B,MAAuB,KAAA,MAC3B,aAAa,CAAkB,GAC/B,IAAqB,KAAA;CACvB,GAEM,UAAwB;EACxB,MAAc,KAAA,MAClB,qBAAqB,CAAS,GAC9B,IAAY,KAAA;CACd,GAEM,UAAsB;EAC1B,IAAM,IAAS,EAAI,UAAU;EACzB,CAAC,KAAU,CAAC,KAAa,EAAI,OAAO,MACxC,EAAmB,GACnB,IAAY,IACZ,EAAO,UAAU,OAAO,cAAc,YAAY,GAC9C,EAAO,QAAM,EAAO,QAAQ;CAClC,GAEM,UAAqB;EACzB,IAAM,IAAS,EAAI,UAAU;EACxB,OAIL;OAHA,IAAY,IACZ,EAAmB,GACnB,EAAO,UAAU,OAAO,YAAY,GAChC,CAAC,EAAO,MACV,IAAI;IAIF,EAAO,YAAY;GACrB,QAAQ,CAER;GAIF,AADA,EAAgB,GAChB,IAAY,4BAA4B;IACtC,IAAY,KAAA,GACR,GAAC,EAAI,YAAY,KAAK,CAAC,EAAI,OAAO,KAAK,CAAC,EAAO,SACnD,EAAO,UAAU,IAAI,YAAY;GACnC,CAAC;EARC;CASJ,GAEM,UAAqB;EACzB,IAAM,IAAS,EAAI,UAAU;EACxB,OAAQ,MAGb;OAFA,EAAgB,GAChB,IAAY,IACR,CAAC,EAAO,UAAU,SAAS,YAAY,GAAG;IAC5C,EAAc;IACd;GACF;GAKA,AAHA,EAAO,UAAU,IAAI,YAAY,GACjC,EAAO,UAAU,OAAO,YAAY,GACpC,EAAmB,GACnB,IAAqB,iBAAiB,EAAc,GAAG,EAAsB,CAAM,IAAI,EAAE;EALzF;CAMF;CAEA,OAAO;EACL,KAAK,GAAM;GACT,AAAI,IAAM,EAAa,IAClB,EAAa;EACpB;EAEA,oBAAoB,GAAO;GACrB,EAAM,WAAW,EAAI,UAAU,KAAK,EAAM,iBAAiB,eAC/D,EAAc;EAChB;EAEA,oBAAoB;GAIlB,AAHA,EAAgB,GAChB,EAAmB,GACnB,IAAY,IACZ,EAAI,UAAU,CAAC,EAAE,UAAU,OAAO,cAAc,YAAY;EAC9D;EAEA,UAAU;GAGR,AAFA,EAAgB,GAChB,EAAmB,GACnB,IAAY;EACd;CACF;AACF,CAAC"}
1
+ {"version":3,"file":"native-dialog-presence.js","names":[],"sources":["../../../src/shared/overlay/native-dialog-presence.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\n\nimport { getTransitionDuration } from './presence'\n\nexport interface NativeDialogPresenceOptions {\n getDialog(): HTMLDialogElement | null\n isConnected(): boolean\n isOpen(): boolean\n}\n\nexport interface NativeDialogPresenceApi {\n sync(open: boolean): void\n handleTransitionEnd(event: TransitionEvent): void\n /**\n * 处理原生 dialog 的 `close` 事件。\n *\n * 返回 `true` 表示该事件是本模块自己 `finishClosing → dialog.close()` 排队的\n * 异步事件(正常时序在本关闭会话内到达,快速重开时在重开后到达的过期事件),\n * 消费方不应把它当成外部关闭;返回 `false` 表示真实的外部关闭\n * (如表单 method=\"dialog\"、宿主程序化 dialog.close()),消费方需自行恢复状态。\n */\n handleNativeClose(): boolean\n dispose(): void\n}\n\n// 管理原生 dialog 保持在 top layer 直至退出动画完成的生命周期。\nexport const defineNativeDialogPresence = () =>\n definePlugin<NativeDialogPresenceApi, NativeDialogPresenceOptions>(ctx => {\n let closeFallbackTimer: ReturnType<typeof setTimeout> | undefined\n let openFrame: number | undefined\n let isClosing = false\n // 由本模块 finishClosing 主动 dialog.close() 时置 true。原生 dialog 的 close 事件\n // 是异步任务(queue a task),可能在之后任意时刻才到达:正常时序在本关闭会话内\n // 到达(消费方 open 已为 false,本就该忽略);快速「关闭→重开」时它在重新打开\n // 之后才到达,若消费方把它当成外部关闭,会把刚重开的 dialog 关掉——这是 drawer\n // 连续开关后丢失过渡动画(重开即被误关)的根因。置位后由消费方的\n // handleNativeClose 经 handleNativeClose() 消费;消费方 open 为 false 提前返回时\n // 也必须调用一次以消费标志,避免泄漏到下一次真实关闭。\n let selfClosePending = false\n\n const clearCloseFallback = () => {\n if (closeFallbackTimer === undefined) return\n clearTimeout(closeFallbackTimer)\n closeFallbackTimer = undefined\n }\n\n const cancelOpenFrame = () => {\n if (openFrame === undefined) return\n cancelAnimationFrame(openFrame)\n openFrame = undefined\n }\n\n const finishClosing = () => {\n const dialog = ctx.getDialog()\n if (!dialog || !isClosing || ctx.isOpen()) return\n clearCloseFallback()\n isClosing = false\n dialog.classList.remove('is-closing', 'is-visible')\n if (dialog.open) {\n selfClosePending = true\n dialog.close?.()\n }\n }\n\n const startOpening = () => {\n const dialog = ctx.getDialog()\n if (!dialog) return\n isClosing = false\n clearCloseFallback()\n dialog.classList.remove('is-closing')\n if (!dialog.open) {\n try {\n // 并发多个 modal dialog(nested drawer 场景)在规范中合法;\n // 此 try/catch 为防御性兜底(如对已在 top layer 的 dialog 重复 showModal\n // 会抛 InvalidStateError),失败时由下方 rAF 回调的 dialog.open 判断中止动画。\n dialog.showModal?.()\n } catch {\n // 保留未打开状态\n }\n }\n // 强制计算初始 top-layer 布局,确保 Safari 等浏览器在下一帧切换 class 时\n // 有稳定的 transition 起点,而不是偶发跳过进场过渡。\n void dialog.offsetWidth\n\n cancelOpenFrame()\n openFrame = requestAnimationFrame(() => {\n openFrame = undefined\n if (!ctx.isConnected() || !ctx.isOpen() || !dialog.open) return\n dialog.classList.add('is-visible')\n })\n }\n\n const startClosing = () => {\n const dialog = ctx.getDialog()\n if (!dialog?.open) return\n cancelOpenFrame()\n isClosing = true\n if (!dialog.classList.contains('is-visible')) {\n finishClosing()\n return\n }\n\n dialog.classList.add('is-closing')\n dialog.classList.remove('is-visible')\n clearCloseFallback()\n closeFallbackTimer = setTimeout(() => finishClosing(), getTransitionDuration(dialog) + 80)\n }\n\n return {\n sync(open) {\n if (open) startOpening()\n else startClosing()\n },\n\n handleTransitionEnd(event) {\n if (event.target !== ctx.getDialog() || event.propertyName !== 'transform') return\n finishClosing()\n },\n\n handleNativeClose() {\n if (selfClosePending) {\n // 我们自己的 dialog.close() 排队的事件:正常时序在本会话内到达、快速重开\n // 时是上一会话的过期事件——两种情况本模块都已在 finishClosing 完成全部清理,\n // 消费标志并让调用方跳过外部关闭处理即可。\n selfClosePending = false\n return true\n }\n // 真实的外部关闭:重置打开/关闭状态,等待消费方恢复(如 open=false)。\n cancelOpenFrame()\n clearCloseFallback()\n isClosing = false\n ctx.getDialog()?.classList.remove('is-closing', 'is-visible')\n return false\n },\n\n dispose() {\n cancelOpenFrame()\n clearCloseFallback()\n isClosing = false\n selfClosePending = false\n }\n }\n })\n"],"mappings":";;;AA0BA,IAAa,UACX,GAAmE,MAAO;CACxE,IAAI,GACA,GACA,IAAY,IAQZ,IAAmB,IAEjB,UAA2B;EAC3B,MAAuB,KAAA,MAC3B,aAAa,CAAkB,GAC/B,IAAqB,KAAA;CACvB,GAEM,UAAwB;EACxB,MAAc,KAAA,MAClB,qBAAqB,CAAS,GAC9B,IAAY,KAAA;CACd,GAEM,UAAsB;EAC1B,IAAM,IAAS,EAAI,UAAU;EACzB,CAAC,KAAU,CAAC,KAAa,EAAI,OAAO,MACxC,EAAmB,GACnB,IAAY,IACZ,EAAO,UAAU,OAAO,cAAc,YAAY,GAC9C,EAAO,SACT,IAAmB,IACnB,EAAO,QAAQ;CAEnB,GAEM,UAAqB;EACzB,IAAM,IAAS,EAAI,UAAU;EACxB,OAIL;OAHA,IAAY,IACZ,EAAmB,GACnB,EAAO,UAAU,OAAO,YAAY,GAChC,CAAC,EAAO,MACV,IAAI;IAIF,EAAO,YAAY;GACrB,QAAQ,CAER;GAOF,AAHA,EAAY,aAEZ,EAAgB,GAChB,IAAY,4BAA4B;IACtC,IAAY,KAAA,GACR,GAAC,EAAI,YAAY,KAAK,CAAC,EAAI,OAAO,KAAK,CAAC,EAAO,SACnD,EAAO,UAAU,IAAI,YAAY;GACnC,CAAC;EAXC;CAYJ,GAEM,UAAqB;EACzB,IAAM,IAAS,EAAI,UAAU;EACxB,OAAQ,MAGb;OAFA,EAAgB,GAChB,IAAY,IACR,CAAC,EAAO,UAAU,SAAS,YAAY,GAAG;IAC5C,EAAc;IACd;GACF;GAKA,AAHA,EAAO,UAAU,IAAI,YAAY,GACjC,EAAO,UAAU,OAAO,YAAY,GACpC,EAAmB,GACnB,IAAqB,iBAAiB,EAAc,GAAG,EAAsB,CAAM,IAAI,EAAE;EALzF;CAMF;CAEA,OAAO;EACL,KAAK,GAAM;GACT,AAAI,IAAM,EAAa,IAClB,EAAa;EACpB;EAEA,oBAAoB,GAAO;GACrB,EAAM,WAAW,EAAI,UAAU,KAAK,EAAM,iBAAiB,eAC/D,EAAc;EAChB;EAEA,oBAAoB;GAalB,OAZI,KAIF,IAAmB,IACZ,OAGT,EAAgB,GAChB,EAAmB,GACnB,IAAY,IACZ,EAAI,UAAU,CAAC,EAAE,UAAU,OAAO,cAAc,YAAY,GACrD;EACT;EAEA,UAAU;GAIR,AAHA,EAAgB,GAChB,EAAmB,GACnB,IAAY,IACZ,IAAmB;EACrB;CACF;AACF,CAAC"}
@@ -2,7 +2,7 @@ import e from "../../assets/glass.js";
2
2
  import t from "../../assets/overlay-motion.js";
3
3
  import n from "../../assets/menu-portal.js";
4
4
  import r from "../../assets/toast-containers.js";
5
- //#region src/shared/theme/overlay-root.ts
5
+ //#region src/shared/overlay/overlay-root.ts
6
6
  var i = "data-wui-overlay-root", a = `${e}\n${r}\n${n}\n${t}`;
7
7
  function o(e) {
8
8
  let t = document.createElement("style");
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay-root.js","names":[],"sources":["../../../src/shared/overlay/overlay-root.ts"],"sourcesContent":["import glass from '@/assets/glass.css?inline'\nimport menuPortalStyle from '@/assets/menu-portal.css?inline'\nimport overlayMotion from '@/assets/overlay-motion.css?inline'\nimport toastContainerStyle from '@/assets/toast-containers.css?inline'\n\n/**\n * Theme-owned overlay root plumbing: applies shared overlay styles to a theme\n * root and lazily creates the document-level fallback overlay root.\n */\nconst FALLBACK_ROOT_ATTRIBUTE = 'data-wui-overlay-root'\n\nconst overlayStyle = `${glass}\\n${toastContainerStyle}\\n${menuPortalStyle}\\n${overlayMotion}`\n\nexport function applyOverlayRootStyles(root: ShadowRoot) {\n const style = document.createElement('style')\n style.textContent = overlayStyle\n root.prepend(style)\n}\n\nexport function getFallbackOverlayRoot(): HTMLElement {\n const existing = document.querySelector<HTMLElement>(`[${FALLBACK_ROOT_ATTRIBUTE}]`)\n if (existing?.shadowRoot) return getOverlayContainer(existing.shadowRoot)\n\n const host = document.createElement('div')\n host.setAttribute(FALLBACK_ROOT_ATTRIBUTE, '')\n const root = host.attachShadow({ mode: 'open' })\n applyOverlayRootStyles(root)\n document.body.appendChild(host)\n return getOverlayContainer(root)\n}\n\nexport function getOverlayContainer(root: ShadowRoot): HTMLElement {\n const existing = root.querySelector<HTMLElement>('[data-wui-overlay-container]')\n if (existing) return existing\n\n const container = document.createElement('div')\n container.setAttribute('data-wui-overlay-container', '')\n root.appendChild(container)\n return container\n}\n"],"mappings":";;;;;AASA,IAAM,IAA0B,yBAE1B,IAAe,GAAG,EAAM,IAAI,EAAoB,IAAI,EAAgB,IAAI;AAE9E,SAAgB,EAAuB,GAAkB;CACvD,IAAM,IAAQ,SAAS,cAAc,OAAO;CAE5C,AADA,EAAM,cAAc,GACpB,EAAK,QAAQ,CAAK;AACpB;AAEA,SAAgB,IAAsC;CACpD,IAAM,IAAW,SAAS,cAA2B,IAAI,EAAwB,EAAE;CACnF,IAAI,GAAU,YAAY,OAAO,EAAoB,EAAS,UAAU;CAExE,IAAM,IAAO,SAAS,cAAc,KAAK;CACzC,EAAK,aAAa,GAAyB,EAAE;CAC7C,IAAM,IAAO,EAAK,aAAa,EAAE,MAAM,OAAO,CAAC;CAG/C,OAFA,EAAuB,CAAI,GAC3B,SAAS,KAAK,YAAY,CAAI,GACvB,EAAoB,CAAI;AACjC;AAEA,SAAgB,EAAoB,GAA+B;CACjE,IAAM,IAAW,EAAK,cAA2B,8BAA8B;CAC/E,IAAI,GAAU,OAAO;CAErB,IAAM,IAAY,SAAS,cAAc,KAAK;CAG9C,OAFA,EAAU,aAAa,8BAA8B,EAAE,GACvD,EAAK,YAAY,CAAS,GACnB;AACT"}