@greypan/web-ui 6.0.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 (187) hide show
  1. package/README.CN.md +285 -93
  2. package/README.md +266 -87
  3. package/dist/assets/glass.js +1 -1
  4. package/dist/assets/glass.js.map +1 -1
  5. package/dist/assets/menu-portal.js +1 -1
  6. package/dist/assets/menu-portal.js.map +1 -1
  7. package/dist/assets/overlay-motion.js +1 -1
  8. package/dist/assets/overlay-motion.js.map +1 -1
  9. package/dist/components/autocomplete/index.d.ts +367 -4
  10. package/dist/components/autocomplete/index.js +87 -80
  11. package/dist/components/autocomplete/index.js.map +1 -1
  12. package/dist/components/autocomplete/style.js +1 -1
  13. package/dist/components/autocomplete/style.js.map +1 -1
  14. package/dist/components/avatar/style.js +1 -1
  15. package/dist/components/avatar/style.js.map +1 -1
  16. package/dist/components/back-top/style.js +1 -1
  17. package/dist/components/back-top/style.js.map +1 -1
  18. package/dist/components/badge/style.js +1 -1
  19. package/dist/components/badge/style.js.map +1 -1
  20. package/dist/components/button/index.js +36 -32
  21. package/dist/components/button/index.js.map +1 -1
  22. package/dist/components/button/style.js +1 -1
  23. package/dist/components/button/style.js.map +1 -1
  24. package/dist/components/button-group/style.js +1 -1
  25. package/dist/components/button-group/style.js.map +1 -1
  26. package/dist/components/checkbox/index.d.ts +366 -5
  27. package/dist/components/checkbox/index.js +19 -31
  28. package/dist/components/checkbox/index.js.map +1 -1
  29. package/dist/components/checkbox/style.js +1 -1
  30. package/dist/components/checkbox/style.js.map +1 -1
  31. package/dist/components/checkbox-group/index.d.ts +366 -4
  32. package/dist/components/checkbox-group/index.js +16 -25
  33. package/dist/components/checkbox-group/index.js.map +1 -1
  34. package/dist/components/collapse/index.d.ts +29 -1
  35. package/dist/components/collapse/index.js +105 -46
  36. package/dist/components/collapse/index.js.map +1 -1
  37. package/dist/components/collapse/style.js +1 -1
  38. package/dist/components/collapse/style.js.map +1 -1
  39. package/dist/components/context-menu/index.d.ts +7 -14
  40. package/dist/components/context-menu/index.js +113 -164
  41. package/dist/components/context-menu/index.js.map +1 -1
  42. package/dist/components/dialog/index.js +26 -25
  43. package/dist/components/dialog/index.js.map +1 -1
  44. package/dist/components/dialog/style.js +1 -1
  45. package/dist/components/dialog/style.js.map +1 -1
  46. package/dist/components/drawer/index.js +71 -64
  47. package/dist/components/drawer/index.js.map +1 -1
  48. package/dist/components/drawer/style.js +1 -1
  49. package/dist/components/drawer/style.js.map +1 -1
  50. package/dist/components/dropdown/index.d.ts +6 -14
  51. package/dist/components/dropdown/index.js +100 -156
  52. package/dist/components/dropdown/index.js.map +1 -1
  53. package/dist/components/dropdown-header/style.js +1 -1
  54. package/dist/components/dropdown-header/style.js.map +1 -1
  55. package/dist/components/dropdown-item/style.js +1 -1
  56. package/dist/components/dropdown-item/style.js.map +1 -1
  57. package/dist/components/empty/style.js +1 -1
  58. package/dist/components/empty/style.js.map +1 -1
  59. package/dist/components/image-preview/index.d.ts +58 -0
  60. package/dist/components/image-preview/index.js +481 -0
  61. package/dist/components/image-preview/index.js.map +1 -0
  62. package/dist/components/image-preview/style.js +6 -0
  63. package/dist/components/image-preview/style.js.map +1 -0
  64. package/dist/components/index.d.ts +1 -0
  65. package/dist/components/index.js +20 -19
  66. package/dist/components/input/index.d.ts +366 -5
  67. package/dist/components/input/index.js +35 -47
  68. package/dist/components/input/index.js.map +1 -1
  69. package/dist/components/input/style.js +1 -1
  70. package/dist/components/input/style.js.map +1 -1
  71. package/dist/components/input-number/index.d.ts +366 -5
  72. package/dist/components/input-number/index.js +31 -44
  73. package/dist/components/input-number/index.js.map +1 -1
  74. package/dist/components/input-number/style.js +1 -1
  75. package/dist/components/input-number/style.js.map +1 -1
  76. package/dist/components/layout/index.d.ts +1 -1
  77. package/dist/components/layout/index.js +5 -3
  78. package/dist/components/layout/index.js.map +1 -1
  79. package/dist/components/layout/style.js +1 -1
  80. package/dist/components/layout/style.js.map +1 -1
  81. package/dist/components/option/style.js +1 -1
  82. package/dist/components/option/style.js.map +1 -1
  83. package/dist/components/popover/index.d.ts +1 -0
  84. package/dist/components/popover/index.js +55 -60
  85. package/dist/components/popover/index.js.map +1 -1
  86. package/dist/components/popover/style.js +1 -1
  87. package/dist/components/popover/style.js.map +1 -1
  88. package/dist/components/radio/index.d.ts +366 -5
  89. package/dist/components/radio/index.js +21 -31
  90. package/dist/components/radio/index.js.map +1 -1
  91. package/dist/components/radio/style.js +1 -1
  92. package/dist/components/radio/style.js.map +1 -1
  93. package/dist/components/radio-group/index.d.ts +366 -4
  94. package/dist/components/radio-group/index.js +16 -25
  95. package/dist/components/radio-group/index.js.map +1 -1
  96. package/dist/components/segmented/index.d.ts +366 -4
  97. package/dist/components/segmented/index.js +28 -35
  98. package/dist/components/segmented/index.js.map +1 -1
  99. package/dist/components/segmented/style.js +1 -1
  100. package/dist/components/segmented/style.js.map +1 -1
  101. package/dist/components/segmented-trigger/index.js +17 -15
  102. package/dist/components/segmented-trigger/index.js.map +1 -1
  103. package/dist/components/segmented-trigger/style.js +1 -1
  104. package/dist/components/segmented-trigger/style.js.map +1 -1
  105. package/dist/components/select/index.d.ts +368 -5
  106. package/dist/components/select/index.js +71 -68
  107. package/dist/components/select/index.js.map +1 -1
  108. package/dist/components/select/style.js +1 -1
  109. package/dist/components/select/style.js.map +1 -1
  110. package/dist/components/slider/index.d.ts +366 -5
  111. package/dist/components/slider/index.js +40 -50
  112. package/dist/components/slider/index.js.map +1 -1
  113. package/dist/components/slider/style.js +1 -1
  114. package/dist/components/slider/style.js.map +1 -1
  115. package/dist/components/spinner/style.js +1 -1
  116. package/dist/components/spinner/style.js.map +1 -1
  117. package/dist/components/svg-draw-lines/index.js +1 -1
  118. package/dist/components/svg-draw-lines/index.js.map +1 -1
  119. package/dist/components/switch/index.d.ts +366 -5
  120. package/dist/components/switch/index.js +28 -38
  121. package/dist/components/switch/index.js.map +1 -1
  122. package/dist/components/switch/style.js +1 -1
  123. package/dist/components/switch/style.js.map +1 -1
  124. package/dist/components/textarea/index.d.ts +366 -5
  125. package/dist/components/textarea/index.js +43 -57
  126. package/dist/components/textarea/index.js.map +1 -1
  127. package/dist/components/textarea/style.js +1 -1
  128. package/dist/components/textarea/style.js.map +1 -1
  129. package/dist/components/theme/index.js +1 -1
  130. package/dist/components/theme/index.js.map +1 -1
  131. package/dist/components/theme/style.js +1 -1
  132. package/dist/components/theme/style.js.map +1 -1
  133. package/dist/components/toast/manager.js +2 -2
  134. package/dist/components/toast/manager.js.map +1 -1
  135. package/dist/components/toast/style.js +1 -1
  136. package/dist/components/toast/style.js.map +1 -1
  137. package/dist/components/tooltip/index.d.ts +1 -0
  138. package/dist/components/tooltip/index.js +43 -58
  139. package/dist/components/tooltip/index.js.map +1 -1
  140. package/dist/components/tooltip/style.js +1 -1
  141. package/dist/components/tooltip/style.js.map +1 -1
  142. package/dist/icons/generated/radix-icons-reset.d.ts +6 -0
  143. package/dist/icons/generated/radix-icons-reset.js +10 -0
  144. package/dist/icons/generated/radix-icons-reset.js.map +1 -0
  145. package/dist/icons/index.d.ts +1 -0
  146. package/dist/icons/index.js +2 -1
  147. package/dist/shared/focus/pointer-focus.js +37 -0
  148. package/dist/shared/focus/pointer-focus.js.map +1 -0
  149. package/dist/shared/form-association/index.js +31 -4
  150. package/dist/shared/form-association/index.js.map +1 -1
  151. package/dist/shared/gesture/drag-gesture.js +53 -30
  152. package/dist/shared/gesture/drag-gesture.js.map +1 -1
  153. package/dist/shared/gesture/pinch-gesture.js +84 -0
  154. package/dist/shared/gesture/pinch-gesture.js.map +1 -0
  155. package/dist/shared/group-management/index.js.map +1 -1
  156. package/dist/shared/menu-behavior/index.js +112 -0
  157. package/dist/shared/menu-behavior/index.js.map +1 -0
  158. package/dist/shared/menu-portal/menu-portal.js.map +1 -1
  159. package/dist/shared/open-state/index.js +12 -0
  160. package/dist/shared/open-state/index.js.map +1 -0
  161. package/dist/shared/option-portal/combobox-shell.js +43 -0
  162. package/dist/shared/option-portal/combobox-shell.js.map +1 -0
  163. package/dist/shared/option-portal/index.js +22 -21
  164. package/dist/shared/option-portal/index.js.map +1 -1
  165. package/dist/shared/overlay/anchored-panel.js +33 -30
  166. package/dist/shared/overlay/anchored-panel.js.map +1 -1
  167. package/dist/shared/overlay/composition.js +57 -0
  168. package/dist/shared/overlay/composition.js.map +1 -0
  169. package/dist/shared/overlay/lifecycle.js +41 -0
  170. package/dist/shared/overlay/lifecycle.js.map +1 -0
  171. package/dist/shared/overlay/native-dialog-presence.js +15 -15
  172. package/dist/shared/overlay/native-dialog-presence.js.map +1 -1
  173. package/dist/shared/{theme → overlay}/overlay-root.js +1 -1
  174. package/dist/shared/overlay/overlay-root.js.map +1 -0
  175. package/dist/shared/overlay/overlay.js +43 -41
  176. package/dist/shared/overlay/overlay.js.map +1 -1
  177. package/dist/shared/overlay/placement-props.js +19 -0
  178. package/dist/shared/overlay/placement-props.js.map +1 -0
  179. package/dist/shared/overlay/portal.js +6 -4
  180. package/dist/shared/overlay/portal.js.map +1 -1
  181. package/dist/shared/overlay/positioning-generation.js +20 -0
  182. package/dist/shared/overlay/positioning-generation.js.map +1 -0
  183. package/dist/shared/{theme/theme-scope.js → overlay/theme-overlay-scope.js} +2 -2
  184. package/dist/shared/overlay/theme-overlay-scope.js.map +1 -0
  185. package/package.json +6 -10
  186. package/dist/shared/theme/overlay-root.js.map +0 -1
  187. package/dist/shared/theme/theme-scope.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"drag-gesture.js","names":[],"sources":["../../../src/shared/gesture/drag-gesture.ts"],"sourcesContent":["/**\n * 跨平台/跨浏览器 Pointer 拖拽手势生命周期管理器\n *\n * 封装:\n * 1. setPointerCapture 安全捕获与 window 兜底监听(防 pointer 丢失/离开视口悬挂)\n * 2. 多点触控副指针与未聚焦指针过滤\n * 3. 意图死区(threshold)与滑动轴向判定\n * 4. 100ms 滑动窗口平滑速度估算(Velocity px/s)\n * 5. Window / Handle 跨层事件去重(防重复采样)\n */\n\nconst VELOCITY_WINDOW_MS = 100\n\nexport interface DragSample {\n t: number\n x: number\n y: number\n}\n\nexport interface DragStartInfo {\n pointerId: number\n clientX: number\n clientY: number\n timeStamp: number\n}\n\nexport interface DragMoveInfo {\n pointerId: number\n clientX: number\n clientY: number\n deltaX: number\n deltaY: number\n velocityX: number\n velocityY: number\n velocity: number // 沿配置主轴(若为 both 则为欧氏速度)\n timeStamp: number\n}\n\nexport interface DragEndInfo {\n pointerId: number\n clientX: number\n clientY: number\n deltaX: number\n deltaY: number\n velocityX: number\n velocityY: number\n velocity: number\n timeStamp: number\n}\n\nexport interface DragGestureOptions {\n /** 允许拖拽的轴向,默认 'both' */\n axis?: 'x' | 'y' | 'both'\n /** 拖拽启动的意图死区阈值(像素)。默认 0(即时跟手) */\n threshold?: number\n /** 手势启动回调。返回 false 可取消本次拖拽 */\n onStart?: (info: DragStartInfo, event: PointerEvent) => boolean | void\n /** 拖拽移动回调 */\n onMove?: (info: DragMoveInfo, event: PointerEvent) => void\n /** 正常松手回调 */\n onEnd?: (info: DragEndInfo, event: PointerEvent) => void\n /** 未通过意图阈值的 pointerup 回调,供点击定位类控件提交一次 tap */\n onTap?: (info: DragEndInfo, event: PointerEvent) => void\n /** 手势取消/被打断回调(如 pointercancel、外部强行 cancel) */\n onCancel?: (event?: PointerEvent) => void\n}\n\nexport interface DragGestureHandle {\n /** 当前是否有正在进行的拖拽手势 */\n isDragging: () => boolean\n /** 外部强制取消当前拖拽(恢复初态并触发 onCancel) */\n cancel: () => void\n /** 彻底销毁手势监听器与解绑 DOM */\n destroy: () => void\n}\n\n/**\n * 为指定元素挂载统一的 Pointer 拖拽手势,或在已有的 pointerdown 事件中启动手势\n */\nexport function attachDragGesture(\n targetOrEvent: HTMLElement | PointerEvent,\n options: DragGestureOptions\n): DragGestureHandle {\n const isEvent = typeof PointerEvent !== 'undefined' && targetOrEvent instanceof PointerEvent\n const target: HTMLElement | null = isEvent\n ? (targetOrEvent.currentTarget as HTMLElement | null) || (targetOrEvent.target as HTMLElement | null)\n : (targetOrEvent as HTMLElement)\n\n const { axis = 'both', threshold = 0, onStart, onMove, onEnd, onTap, onCancel } = options\n\n let activePointerId: number | null = null\n let startX = 0\n let startY = 0\n let isThresholdPassed = threshold <= 0\n let samples: DragSample[] = []\n let handledMoveEvent: PointerEvent | null = null\n\n function estimateVelocity(): { vx: number; vy: number; v: number } {\n if (samples.length < 2) return { vx: 0, vy: 0, v: 0 }\n const first = samples[0]\n const last = samples[samples.length - 1]\n const dt = last.t - first.t\n if (dt < 8) return { vx: 0, vy: 0, v: 0 } // 采样跨度不足 8ms 时不可信\n const vx = ((last.x - first.x) / dt) * 1000\n const vy = ((last.y - first.y) / dt) * 1000\n let v = 0\n if (axis === 'x') v = vx\n else if (axis === 'y') v = vy\n else v = Math.hypot(vx, vy)\n return { vx, vy, v }\n }\n\n function detachWindowListeners() {\n window.removeEventListener('pointermove', handlePointerMove, true)\n window.removeEventListener('pointerup', handlePointerUp, true)\n window.removeEventListener('pointercancel', handlePointerCancel, true)\n window.removeEventListener('lostpointercapture', handlePointerCancel, true)\n }\n\n function attachWindowListeners() {\n window.addEventListener('pointermove', handlePointerMove, true)\n window.addEventListener('pointerup', handlePointerUp, true)\n window.addEventListener('pointercancel', handlePointerCancel, true)\n window.addEventListener('lostpointercapture', handlePointerCancel, true)\n }\n\n function detachTargetListeners() {\n if (!target) return\n target.removeEventListener('pointermove', handlePointerMove)\n target.removeEventListener('pointerup', handlePointerUp)\n target.removeEventListener('pointercancel', handlePointerCancel)\n target.removeEventListener('lostpointercapture', handlePointerCancel)\n }\n\n function attachTargetListeners() {\n if (!target) return\n target.addEventListener('pointermove', handlePointerMove)\n target.addEventListener('pointerup', handlePointerUp)\n target.addEventListener('pointercancel', handlePointerCancel)\n target.addEventListener('lostpointercapture', handlePointerCancel)\n }\n\n function resetState() {\n if (activePointerId !== null && target && typeof target.hasPointerCapture === 'function') {\n try {\n if (target.hasPointerCapture(activePointerId)) {\n target.releasePointerCapture(activePointerId)\n }\n } catch {\n // ignore\n }\n }\n activePointerId = null\n isThresholdPassed = threshold <= 0\n samples = []\n handledMoveEvent = null\n detachWindowListeners()\n detachTargetListeners()\n }\n\n function handlePointerDown(e: PointerEvent) {\n if (activePointerId !== null) return\n // 真实触控环境下的多点触控副指针不启动手势(合成事件或鼠标环境缺省时不拦截)\n if (e.pointerType === 'touch' && e.isPrimary === false) return\n\n const startInfo: DragStartInfo = {\n pointerId: e.pointerId,\n clientX: e.clientX,\n clientY: e.clientY,\n timeStamp: e.timeStamp\n }\n\n if (onStart?.(startInfo, e) === false) return\n\n activePointerId = e.pointerId\n startX = e.clientX\n startY = e.clientY\n isThresholdPassed = threshold <= 0\n samples = [{ t: e.timeStamp, x: e.clientX, y: e.clientY }]\n handledMoveEvent = null\n\n attachWindowListeners()\n attachTargetListeners()\n\n // 仅在无意图死区时立即捕获指针;若存在死区,延迟至确认拖拽后捕获,避免干扰子元素原生点击\n if (isThresholdPassed) {\n try {\n target?.setPointerCapture?.(e.pointerId)\n } catch {\n // 忽略合成指针或不可捕获上下文(如测试环境)\n }\n }\n }\n\n function handlePointerMove(e: PointerEvent) {\n if (activePointerId !== e.pointerId) return\n if (handledMoveEvent === e) return\n handledMoveEvent = e\n\n const deltaX = e.clientX - startX\n const deltaY = e.clientY - startY\n\n if (!isThresholdPassed) {\n const distance = axis === 'x' ? Math.abs(deltaX) : axis === 'y' ? Math.abs(deltaY) : Math.hypot(deltaX, deltaY)\n if (distance < threshold) return\n\n // 若限定了 X 轴且 Y 轴位移明显更大,视为滚动意图而非拖拽\n if (axis === 'x' && Math.abs(deltaY) > Math.abs(deltaX)) {\n cancel()\n return\n }\n // 若限定了 Y 轴且 X 轴位移明显更大,放弃拖拽\n if (axis === 'y' && Math.abs(deltaX) > Math.abs(deltaY)) {\n cancel()\n return\n }\n isThresholdPassed = true\n try {\n target?.setPointerCapture?.(e.pointerId)\n } catch {\n // 忽略合成指针\n }\n }\n\n // 确认拖拽后阻止默认滚动/文字选中\n if (e.cancelable) e.preventDefault()\n\n const now = e.timeStamp\n samples.push({ t: now, x: e.clientX, y: e.clientY })\n while (samples.length > 2 && now - samples[0].t > VELOCITY_WINDOW_MS) {\n samples.shift()\n }\n\n const { vx, vy, v } = estimateVelocity()\n\n const moveInfo: DragMoveInfo = {\n pointerId: e.pointerId,\n clientX: e.clientX,\n clientY: e.clientY,\n deltaX,\n deltaY,\n velocityX: vx,\n velocityY: vy,\n velocity: v,\n timeStamp: now\n }\n\n onMove?.(moveInfo, e)\n }\n\n function handlePointerUp(e: PointerEvent) {\n if (activePointerId !== e.pointerId) return\n\n const now = e.timeStamp\n samples.push({ t: now, x: e.clientX, y: e.clientY })\n while (samples.length > 2 && now - samples[0].t > VELOCITY_WINDOW_MS) {\n samples.shift()\n }\n\n const deltaX = e.clientX - startX\n const deltaY = e.clientY - startY\n const { vx, vy, v } = estimateVelocity()\n\n const endInfo: DragEndInfo = {\n pointerId: e.pointerId,\n clientX: e.clientX,\n clientY: e.clientY,\n deltaX,\n deltaY,\n velocityX: vx,\n velocityY: vy,\n velocity: v,\n timeStamp: e.timeStamp\n }\n\n const hadPassedThreshold = isThresholdPassed\n resetState()\n\n if (hadPassedThreshold) {\n onEnd?.(endInfo, e)\n } else {\n onTap?.(endInfo, e)\n onCancel?.(e)\n }\n }\n\n function handlePointerCancel(e: PointerEvent) {\n if (activePointerId !== e.pointerId) return\n resetState()\n onCancel?.(e)\n }\n\n function cancel() {\n if (activePointerId === null) return\n resetState()\n onCancel?.()\n }\n\n if (isEvent) {\n handlePointerDown(targetOrEvent)\n } else if (target) {\n target.addEventListener('pointerdown', handlePointerDown)\n target.addEventListener('pointermove', handlePointerMove)\n target.addEventListener('pointerup', handlePointerUp)\n target.addEventListener('pointercancel', handlePointerCancel)\n }\n\n return {\n isDragging: () => activePointerId !== null,\n cancel,\n destroy: () => {\n cancel()\n if (!isEvent && target) {\n target.removeEventListener('pointerdown', handlePointerDown)\n target.removeEventListener('pointermove', handlePointerMove)\n target.removeEventListener('pointerup', handlePointerUp)\n target.removeEventListener('pointercancel', handlePointerCancel)\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"drag-gesture.js","names":[],"sources":["../../../src/shared/gesture/drag-gesture.ts"],"sourcesContent":["/**\n * 跨平台/跨浏览器 Pointer 拖拽手势生命周期管理器\n *\n * 封装:\n * 1. setPointerCapture 安全捕获与 window 兜底监听(防 pointer 丢失/离开视口悬挂)\n * 2. 多点触控副指针与未聚焦指针过滤\n * 3. 意图死区(threshold)与滑动轴向判定\n * 4. 100ms 滑动窗口平滑速度估算(Velocity px/s)\n * 5. Window / Handle 跨层事件去重(防重复采样)\n */\n\nconst VELOCITY_WINDOW_MS = 100\n\nexport interface DragSample {\n t: number\n x: number\n y: number\n}\n\nexport interface DragStartInfo {\n pointerId: number\n clientX: number\n clientY: number\n timeStamp: number\n}\n\nexport interface DragMoveInfo {\n pointerId: number\n clientX: number\n clientY: number\n deltaX: number\n deltaY: number\n velocityX: number\n velocityY: number\n velocity: number // 沿配置主轴(若为 both 则为欧氏速度)\n timeStamp: number\n}\n\nexport interface DragEndInfo {\n pointerId: number\n clientX: number\n clientY: number\n deltaX: number\n deltaY: number\n velocityX: number\n velocityY: number\n velocity: number\n timeStamp: number\n}\n\nexport interface DragGestureOptions {\n /** 允许拖拽的轴向,默认 'both' */\n axis?: 'x' | 'y' | 'both'\n /** 拖拽启动的意图死区阈值(像素)。默认 0(即时跟手) */\n threshold?: number\n /** 手势启动回调。返回 false 可取消本次拖拽 */\n onStart?: (info: DragStartInfo, event: PointerEvent) => boolean | void\n /** 拖拽移动回调 */\n onMove?: (info: DragMoveInfo, event: PointerEvent) => void\n /** 正常松手回调 */\n onEnd?: (info: DragEndInfo, event: PointerEvent) => void\n /** 未通过意图阈值的 pointerup 回调,供点击定位类控件提交一次 tap */\n onTap?: (info: DragEndInfo, event: PointerEvent) => void\n /** 手势取消/被打断回调(如 pointercancel、外部强行 cancel) */\n onCancel?: (event?: PointerEvent) => void\n}\n\nexport interface DragGestureHandle {\n /** 当前是否有正在进行的拖拽手势 */\n isDragging: () => boolean\n /** 外部强制取消当前拖拽(恢复初态并触发 onCancel) */\n cancel: () => void\n /** 彻底销毁手势监听器与解绑 DOM */\n destroy: () => void\n}\n\n/**\n * 为指定元素挂载统一的 Pointer 拖拽手势,或在已有的 pointerdown 事件中启动手势\n */\nexport function attachDragGesture(\n targetOrEvent: HTMLElement | PointerEvent,\n options: DragGestureOptions\n): DragGestureHandle {\n const isEvent = typeof PointerEvent !== 'undefined' && targetOrEvent instanceof PointerEvent\n const target: HTMLElement | null = isEvent\n ? (targetOrEvent.currentTarget as HTMLElement | null) || (targetOrEvent.target as HTMLElement | null)\n : (targetOrEvent as HTMLElement)\n\n const { axis = 'both', threshold = 0, onStart, onMove, onEnd, onTap, onCancel } = options\n\n let activePointerId: number | null = null\n let startX = 0\n let startY = 0\n let isThresholdPassed = threshold <= 0\n let samples: DragSample[] = []\n let handledMoveEvent: PointerEvent | null = null\n let touchGuardTargets: Set<EventTarget> | null = null\n\n /**\n * iOS Safari 滚动接管兜底:\n * WebKit 对 shadow DOM 内 touch-action 的交集计算存在怪癖,触摸落在深层子元素\n * (如 slider thumb、segmented-trigger 的 shadow 内容)时,声明在手势元素上的\n * touch-action: none 可能不生效,iOS 会把触摸判为滚动意图并在越过阈值前触发\n * pointercancel,拖拽因此中断。非被动 touchmove + preventDefault 是 WebKit 也\n * 尊重的最后手段:页面显式处理触摸移动后,滚动识别器不会获胜,也就不会接管。\n */\n function handleTouchMove(e: Event) {\n if (e.cancelable) e.preventDefault()\n }\n\n /**\n * 沿 pointerdown 的 composedPath 在每个 Element/ShadowRoot 挂非被动 touchmove\n * 监听。TouchEvent 不像 PointerEvent 那样可组合:非 composed 的 touchmove 只在\n * 落点所在的 shadow tree 内传播(连 host 都不经过),所以单一手势元素上的监听\n * 对 segmented-trigger 这类深层 shadow 内容完全失效。composedPath 覆盖落点的\n * 全部 shadow tree 链,无论 touchmove 在哪一层传播,路径上必有我们挂载的节点\n * 能收到事件并 preventDefault。document/window 不在挂载范围:touchmove 到不了\n * 它们,挂上也只是死代码。\n */\n function attachTouchGuards(e: PointerEvent) {\n const targets = new Set<EventTarget>()\n const path = typeof e.composedPath === 'function' ? e.composedPath() : []\n for (const node of path) {\n const type = (node as Node).nodeType\n if (type === Node.ELEMENT_NODE || type === Node.DOCUMENT_FRAGMENT_NODE) {\n node.addEventListener('touchmove', handleTouchMove, { capture: true, passive: false })\n targets.add(node)\n }\n }\n touchGuardTargets = targets\n }\n\n function detachTouchGuards() {\n if (!touchGuardTargets) return\n for (const node of touchGuardTargets) {\n node.removeEventListener('touchmove', handleTouchMove, true)\n }\n touchGuardTargets = null\n }\n\n function estimateVelocity(): { vx: number; vy: number; v: number } {\n if (samples.length < 2) return { vx: 0, vy: 0, v: 0 }\n const first = samples[0]\n const last = samples[samples.length - 1]\n const dt = last.t - first.t\n // 采样跨度不足 2ms(同帧/同毫秒的合成或极端快速事件)时不可信,置 0;\n // 2-8ms 的超短轻扫仍按真实跨度上报速度,避免「快速轻扫」被误判为无速度\n // 而回弹(旧守卫 <8ms 会把真实超短轻扫的速度清零)。\n if (dt < 2) return { vx: 0, vy: 0, v: 0 }\n const vx = ((last.x - first.x) / dt) * 1000\n const vy = ((last.y - first.y) / dt) * 1000\n let v = 0\n if (axis === 'x') v = vx\n else if (axis === 'y') v = vy\n else v = Math.hypot(vx, vy)\n return { vx, vy, v }\n }\n\n function detachWindowListeners() {\n window.removeEventListener('pointermove', handlePointerMove, true)\n window.removeEventListener('pointerup', handlePointerUp, true)\n window.removeEventListener('pointercancel', handlePointerCancel, true)\n window.removeEventListener('lostpointercapture', handleLostPointerCapture, true)\n }\n\n function attachWindowListeners() {\n window.addEventListener('pointermove', handlePointerMove, true)\n window.addEventListener('pointerup', handlePointerUp, true)\n window.addEventListener('pointercancel', handlePointerCancel, true)\n window.addEventListener('lostpointercapture', handleLostPointerCapture, true)\n }\n\n function detachTargetListeners() {\n if (!target) return\n target.removeEventListener('pointermove', handlePointerMove)\n target.removeEventListener('pointerup', handlePointerUp)\n target.removeEventListener('pointercancel', handlePointerCancel)\n target.removeEventListener('lostpointercapture', handleLostPointerCapture)\n }\n\n function attachTargetListeners() {\n if (!target) return\n target.addEventListener('pointermove', handlePointerMove)\n target.addEventListener('pointerup', handlePointerUp)\n target.addEventListener('pointercancel', handlePointerCancel)\n target.addEventListener('lostpointercapture', handleLostPointerCapture)\n }\n\n function resetState() {\n if (activePointerId !== null && target && typeof target.hasPointerCapture === 'function') {\n try {\n if (target.hasPointerCapture(activePointerId)) {\n target.releasePointerCapture(activePointerId)\n }\n } catch {\n // ignore\n }\n }\n activePointerId = null\n isThresholdPassed = threshold <= 0\n samples = []\n handledMoveEvent = null\n detachWindowListeners()\n detachTargetListeners()\n detachTouchGuards()\n }\n\n function handlePointerDown(e: PointerEvent) {\n if (activePointerId !== null) return\n // 真实触控环境下的多点触控副指针不启动手势(合成事件或鼠标环境缺省时不拦截)\n if (e.pointerType === 'touch' && e.isPrimary === false) return\n\n const startInfo: DragStartInfo = {\n pointerId: e.pointerId,\n clientX: e.clientX,\n clientY: e.clientY,\n timeStamp: e.timeStamp\n }\n\n if (onStart?.(startInfo, e) === false) return\n\n activePointerId = e.pointerId\n startX = e.clientX\n startY = e.clientY\n isThresholdPassed = threshold <= 0\n samples = [{ t: e.timeStamp, x: e.clientX, y: e.clientY }]\n handledMoveEvent = null\n\n attachWindowListeners()\n attachTargetListeners()\n attachTouchGuards(e)\n\n // 仅在无意图死区时立即捕获指针;若存在死区,延迟至确认拖拽后捕获,避免干扰子元素原生点击\n if (isThresholdPassed) {\n try {\n target?.setPointerCapture?.(e.pointerId)\n } catch {\n // 忽略合成指针或不可捕获上下文(如测试环境)\n }\n }\n }\n\n function handlePointerMove(e: PointerEvent) {\n if (activePointerId !== e.pointerId) return\n if (handledMoveEvent === e) return\n handledMoveEvent = e\n\n const deltaX = e.clientX - startX\n const deltaY = e.clientY - startY\n\n if (!isThresholdPassed) {\n const distance = axis === 'x' ? Math.abs(deltaX) : axis === 'y' ? Math.abs(deltaY) : Math.hypot(deltaX, deltaY)\n if (distance < threshold) return\n\n // 若限定了 X 轴且 Y 轴位移明显更大,视为滚动意图而非拖拽\n if (axis === 'x' && Math.abs(deltaY) > Math.abs(deltaX)) {\n cancel()\n return\n }\n // 若限定了 Y 轴且 X 轴位移明显更大,放弃拖拽\n if (axis === 'y' && Math.abs(deltaX) > Math.abs(deltaY)) {\n cancel()\n return\n }\n isThresholdPassed = true\n try {\n target?.setPointerCapture?.(e.pointerId)\n } catch {\n // 忽略合成指针\n }\n }\n\n // 确认拖拽后阻止默认滚动/文字选中\n if (e.cancelable) e.preventDefault()\n\n const now = e.timeStamp\n samples.push({ t: now, x: e.clientX, y: e.clientY })\n while (samples.length > 2 && now - samples[0].t > VELOCITY_WINDOW_MS) {\n samples.shift()\n }\n\n const { vx, vy, v } = estimateVelocity()\n\n const moveInfo: DragMoveInfo = {\n pointerId: e.pointerId,\n clientX: e.clientX,\n clientY: e.clientY,\n deltaX,\n deltaY,\n velocityX: vx,\n velocityY: vy,\n velocity: v,\n timeStamp: now\n }\n\n onMove?.(moveInfo, e)\n }\n\n function handlePointerUp(e: PointerEvent) {\n if (activePointerId !== e.pointerId) return\n\n const now = e.timeStamp\n samples.push({ t: now, x: e.clientX, y: e.clientY })\n while (samples.length > 2 && now - samples[0].t > VELOCITY_WINDOW_MS) {\n samples.shift()\n }\n\n const deltaX = e.clientX - startX\n const deltaY = e.clientY - startY\n const { vx, vy, v } = estimateVelocity()\n\n const endInfo: DragEndInfo = {\n pointerId: e.pointerId,\n clientX: e.clientX,\n clientY: e.clientY,\n deltaX,\n deltaY,\n velocityX: vx,\n velocityY: vy,\n velocity: v,\n timeStamp: e.timeStamp\n }\n\n const hadPassedThreshold = isThresholdPassed\n resetState()\n\n if (hadPassedThreshold) {\n onEnd?.(endInfo, e)\n } else {\n onTap?.(endInfo, e)\n onCancel?.(e)\n }\n }\n\n function handlePointerCancel(e: PointerEvent) {\n if (activePointerId !== e.pointerId) return\n resetState()\n onCancel?.(e)\n }\n\n /**\n * lostpointercapture 不能当作 pointercancel:touch 指针在 pointerdown 时会被浏览器\n * 隐式捕获到命中元素(如 slider thumb、segmented-trigger 内容),组件随后\n * setPointerCapture(target) 把捕获转手到手势元素,命中元素先收到 lostpointercapture——\n * 这是正常转手而非取消,若直接走 cancel 会把刚启动的拖拽杀死(值只走一步即冻结)。\n * 只有手势元素自身意外丢失捕获(如元素被移出文档)才应取消。\n *\n * resetState 内 releasePointerCapture 产生的 lostpointercapture 会在 activePointerId\n * 置空之后异步到达,被 pointerId 守卫挡住,不会误伤。\n */\n function handleLostPointerCapture(e: PointerEvent) {\n if (activePointerId !== e.pointerId) return\n if (e.target !== target) return\n resetState()\n onCancel?.(e)\n }\n\n function cancel() {\n if (activePointerId === null) return\n resetState()\n onCancel?.()\n }\n\n if (isEvent) {\n handlePointerDown(targetOrEvent)\n } else if (target) {\n target.addEventListener('pointerdown', handlePointerDown)\n target.addEventListener('pointermove', handlePointerMove)\n target.addEventListener('pointerup', handlePointerUp)\n target.addEventListener('pointercancel', handlePointerCancel)\n }\n\n return {\n isDragging: () => activePointerId !== null,\n cancel,\n destroy: () => {\n cancel()\n if (!isEvent && target) {\n target.removeEventListener('pointerdown', handlePointerDown)\n target.removeEventListener('pointermove', handlePointerMove)\n target.removeEventListener('pointerup', handlePointerUp)\n target.removeEventListener('pointercancel', handlePointerCancel)\n }\n }\n }\n}\n"],"mappings":"AA+EA,SAAgB,EACd,GACA,GACmB;CACnB,IAAM,IAAU,OAAO,eAAiB,OAAe,aAAyB,cAC1E,IAA6B,IAC9B,EAAc,iBAAyC,EAAc,SACrE,GAEC,EAAE,UAAO,QAAQ,eAAY,GAAG,YAAS,WAAQ,UAAO,UAAO,gBAAa,GAE9E,IAAiC,MACjC,IAAS,GACT,IAAS,GACT,IAAoB,KAAa,GACjC,IAAwB,CAAC,GACzB,IAAwC,MACxC,IAA6C;CAUjD,SAAS,EAAgB,GAAU;EACjC,AAAI,EAAE,cAAY,EAAE,eAAe;CACrC;CAWA,SAAS,EAAkB,GAAiB;EAC1C,IAAM,oBAAU,IAAI,IAAiB,GAC/B,IAAO,OAAO,EAAE,gBAAiB,aAAa,EAAE,aAAa,IAAI,CAAC;EACxE,KAAK,IAAM,KAAQ,GAAM;GACvB,IAAM,IAAQ,EAAc;GAC5B,CAAI,MAAS,KAAK,gBAAgB,MAAS,KAAK,4BAC9C,EAAK,iBAAiB,aAAa,GAAiB;IAAE,SAAS;IAAM,SAAS;GAAM,CAAC,GACrF,EAAQ,IAAI,CAAI;EAEpB;EACA,IAAoB;CACtB;CAEA,SAAS,IAAoB;EACtB,OACL;QAAK,IAAM,KAAQ,GACjB,EAAK,oBAAoB,aAAa,GAAiB,EAAI;GAE7D,IAAoB;EAFyC;CAG/D;CAEA,SAAS,IAA0D;EACjE,IAAI,EAAQ,SAAS,GAAG,OAAO;GAAE,IAAI;GAAG,IAAI;GAAG,GAAG;EAAE;EACpD,IAAM,IAAQ,EAAQ,IAChB,IAAO,EAAQ,EAAQ,SAAS,IAChC,IAAK,EAAK,IAAI,EAAM;EAI1B,IAAI,IAAK,GAAG,OAAO;GAAE,IAAI;GAAG,IAAI;GAAG,GAAG;EAAE;EACxC,IAAM,KAAO,EAAK,IAAI,EAAM,KAAK,IAAM,KACjC,KAAO,EAAK,IAAI,EAAM,KAAK,IAAM,KACnC,IAAI;EAIR,OAHA,AAEK,IAFD,MAAS,MAAS,IACb,MAAS,MAAS,IAClB,KAAK,MAAM,GAAI,CAAE,GACnB;GAAE;GAAI;GAAI;EAAE;CACrB;CAEA,SAAS,IAAwB;EAI/B,AAHA,OAAO,oBAAoB,eAAe,GAAmB,EAAI,GACjE,OAAO,oBAAoB,aAAa,GAAiB,EAAI,GAC7D,OAAO,oBAAoB,iBAAiB,GAAqB,EAAI,GACrE,OAAO,oBAAoB,sBAAsB,GAA0B,EAAI;CACjF;CAEA,SAAS,IAAwB;EAI/B,AAHA,OAAO,iBAAiB,eAAe,GAAmB,EAAI,GAC9D,OAAO,iBAAiB,aAAa,GAAiB,EAAI,GAC1D,OAAO,iBAAiB,iBAAiB,GAAqB,EAAI,GAClE,OAAO,iBAAiB,sBAAsB,GAA0B,EAAI;CAC9E;CAEA,SAAS,IAAwB;EAC1B,MACL,EAAO,oBAAoB,eAAe,CAAiB,GAC3D,EAAO,oBAAoB,aAAa,CAAe,GACvD,EAAO,oBAAoB,iBAAiB,CAAmB,GAC/D,EAAO,oBAAoB,sBAAsB,CAAwB;CAC3E;CAEA,SAAS,IAAwB;EAC1B,MACL,EAAO,iBAAiB,eAAe,CAAiB,GACxD,EAAO,iBAAiB,aAAa,CAAe,GACpD,EAAO,iBAAiB,iBAAiB,CAAmB,GAC5D,EAAO,iBAAiB,sBAAsB,CAAwB;CACxE;CAEA,SAAS,IAAa;EACpB,IAAI,MAAoB,QAAQ,KAAU,OAAO,EAAO,qBAAsB,YAC5E,IAAI;GACF,AAAI,EAAO,kBAAkB,CAAe,KAC1C,EAAO,sBAAsB,CAAe;EAEhD,QAAQ,CAER;EAQF,AANA,IAAkB,MAClB,IAAoB,KAAa,GACjC,IAAU,CAAC,GACX,IAAmB,MACnB,EAAsB,GACtB,EAAsB,GACtB,EAAkB;CACpB;CAEA,SAAS,EAAkB,GAAiB;EAG1C,IAFI,MAAoB,QAEpB,EAAE,gBAAgB,WAAW,EAAE,cAAc,IAAO;EAExD,IAAM,IAA2B;GAC/B,WAAW,EAAE;GACb,SAAS,EAAE;GACX,SAAS,EAAE;GACX,WAAW,EAAE;EACf;EAEI,QAAU,GAAW,CAAC,MAAM,OAEhC,IAAkB,EAAE,WACpB,IAAS,EAAE,SACX,IAAS,EAAE,SACX,IAAoB,KAAa,GACjC,IAAU,CAAC;GAAE,GAAG,EAAE;GAAW,GAAG,EAAE;GAAS,GAAG,EAAE;EAAQ,CAAC,GACzD,IAAmB,MAEnB,EAAsB,GACtB,EAAsB,GACtB,EAAkB,CAAC,GAGf,IACF,IAAI;GACF,GAAQ,oBAAoB,EAAE,SAAS;EACzC,QAAQ,CAER;CAEJ;CAEA,SAAS,EAAkB,GAAiB;EAE1C,IADI,MAAoB,EAAE,aACtB,MAAqB,GAAG;EAC5B,IAAmB;EAEnB,IAAM,IAAS,EAAE,UAAU,GACrB,IAAS,EAAE,UAAU;EAE3B,IAAI,CAAC,GAAmB;GAEtB,KADiB,MAAS,MAAM,KAAK,IAAI,CAAM,IAAI,MAAS,MAAM,KAAK,IAAI,CAAM,IAAI,KAAK,MAAM,GAAQ,CAAM,KAC/F,GAAW;GAG1B,IAAI,MAAS,OAAO,KAAK,IAAI,CAAM,IAAI,KAAK,IAAI,CAAM,GAAG;IACvD,EAAO;IACP;GACF;GAEA,IAAI,MAAS,OAAO,KAAK,IAAI,CAAM,IAAI,KAAK,IAAI,CAAM,GAAG;IACvD,EAAO;IACP;GACF;GACA,IAAoB;GACpB,IAAI;IACF,GAAQ,oBAAoB,EAAE,SAAS;GACzC,QAAQ,CAER;EACF;EAGA,AAAI,EAAE,cAAY,EAAE,eAAe;EAEnC,IAAM,IAAM,EAAE;EAEd,KADA,EAAQ,KAAK;GAAE,GAAG;GAAK,GAAG,EAAE;GAAS,GAAG,EAAE;EAAQ,CAAC,GAC5C,EAAQ,SAAS,KAAK,IAAM,EAAQ,EAAE,CAAC,IAAI,MAChD,EAAQ,MAAM;EAGhB,IAAM,EAAE,OAAI,OAAI,SAAM,EAAiB,GAEjC,IAAyB;GAC7B,WAAW,EAAE;GACb,SAAS,EAAE;GACX,SAAS,EAAE;GACX;GACA;GACA,WAAW;GACX,WAAW;GACX,UAAU;GACV,WAAW;EACb;EAEA,IAAS,GAAU,CAAC;CACtB;CAEA,SAAS,EAAgB,GAAiB;EACxC,IAAI,MAAoB,EAAE,WAAW;EAErC,IAAM,IAAM,EAAE;EAEd,KADA,EAAQ,KAAK;GAAK;GAAK,GAAG,EAAE;GAAS,GAAG,EAAE;EAAQ,CAAC,GAC5C,EAAQ,SAAS,KAAK,IAAM,EAAQ,EAAE,CAAC,IAAI,MAChD,EAAQ,MAAM;EAGhB,IAAM,IAAS,EAAE,UAAU,GACrB,IAAS,EAAE,UAAU,GACrB,EAAE,OAAI,OAAI,SAAM,EAAiB,GAEjC,IAAuB;GAC3B,WAAW,EAAE;GACb,SAAS,EAAE;GACX,SAAS,EAAE;GACX;GACA;GACA,WAAW;GACX,WAAW;GACX,UAAU;GACV,WAAW,EAAE;EACf,GAEM,IAAqB;EAG3B,AAFA,EAAW,GAEP,IACF,IAAQ,GAAS,CAAC,KAElB,IAAQ,GAAS,CAAC,GAClB,IAAW,CAAC;CAEhB;CAEA,SAAS,EAAoB,GAAiB;EACxC,MAAoB,EAAE,cAC1B,EAAW,GACX,IAAW,CAAC;CACd;CAYA,SAAS,EAAyB,GAAiB;EAC7C,MAAoB,EAAE,aACtB,EAAE,WAAW,MACjB,EAAW,GACX,IAAW,CAAC;CACd;CAEA,SAAS,IAAS;EACZ,MAAoB,SACxB,EAAW,GACX,IAAW;CACb;CAWA,OATI,IACF,EAAkB,CAAa,IACtB,MACT,EAAO,iBAAiB,eAAe,CAAiB,GACxD,EAAO,iBAAiB,eAAe,CAAiB,GACxD,EAAO,iBAAiB,aAAa,CAAe,GACpD,EAAO,iBAAiB,iBAAiB,CAAmB,IAGvD;EACL,kBAAkB,MAAoB;EACtC;EACA,eAAe;GAEb,AADA,EAAO,GACH,CAAC,KAAW,MACd,EAAO,oBAAoB,eAAe,CAAiB,GAC3D,EAAO,oBAAoB,eAAe,CAAiB,GAC3D,EAAO,oBAAoB,aAAa,CAAe,GACvD,EAAO,oBAAoB,iBAAiB,CAAmB;EAEnE;CACF;AACF"}
@@ -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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"}
@@ -0,0 +1,112 @@
1
+ import { hideOverlayPresence as e } from "../overlay/presence.js";
2
+ import { findFocusedMenuItem as t, focusMenuItem as n, getEnabledMenuItems as r } from "../menu-portal/menu-tree.js";
3
+ //#region src/shared/menu-behavior/index.ts
4
+ function i(e, t) {
5
+ let n = !1, r;
6
+ return {
7
+ arm() {
8
+ n = !0, clearTimeout(r), r = setTimeout(() => {
9
+ n = !1;
10
+ });
11
+ },
12
+ isArmed: () => n,
13
+ isInside: (n) => {
14
+ for (let r of n.composedPath()) if (r === e || r === e.shadowRoot || r instanceof Node && r.getRootNode() === e.shadowRoot || r instanceof Node && t(r)) return !0;
15
+ return !1;
16
+ },
17
+ dispose() {
18
+ clearTimeout(r), n = !1;
19
+ }
20
+ };
21
+ }
22
+ var a = 200;
23
+ function o(e, t) {
24
+ let n, r = [];
25
+ function i() {
26
+ r.forEach((e) => e()), r = [];
27
+ let i = e.getOpenDepth();
28
+ for (let o = 0; o <= i + 1; o++) {
29
+ let i = e.getLevelItems(o);
30
+ i.length && i.forEach((i) => {
31
+ if (!i.matches(t) || i.hasAttribute("disabled")) return;
32
+ let s = (t) => {
33
+ t.pointerType !== "touch" && (clearTimeout(n), e.isSubmenuItem(i) ? e.getActiveItem(o) !== i && (n = setTimeout(() => e.openSubmenu(i), a)) : e.getOpenDepth() > o && e.closeFrom(o));
34
+ };
35
+ i.addEventListener("pointerenter", s, { passive: !0 }), r.push(() => i.removeEventListener("pointerenter", s));
36
+ });
37
+ }
38
+ }
39
+ return {
40
+ bind: i,
41
+ dispose() {
42
+ clearTimeout(n), r.forEach((e) => e()), r = [];
43
+ }
44
+ };
45
+ }
46
+ function s(e, t) {
47
+ if (t.key === "Escape") {
48
+ e.closeDeepestOrAll(), t.preventDefault();
49
+ return;
50
+ }
51
+ let r = e.getFocusedItem(t), i = r ? e.getLevelOf(r) : void 0;
52
+ if (i === void 0) return;
53
+ let a = e.getEnabledItems(i), o = r ? a.indexOf(r) : -1;
54
+ switch (t.key) {
55
+ case "ArrowDown":
56
+ n(a[(o + 1 + a.length) % a.length]);
57
+ break;
58
+ case "ArrowUp":
59
+ n(a[(o - 1 + a.length) % a.length]);
60
+ break;
61
+ case "Home":
62
+ n(a[0]);
63
+ break;
64
+ case "End":
65
+ n(a.at(-1));
66
+ break;
67
+ case "ArrowRight":
68
+ r?.hasAttribute("submenu") && (e.openSubmenuInstant(r), requestAnimationFrame(() => requestAnimationFrame(() => n(e.getEnabledItems(i + 1)[0]))));
69
+ break;
70
+ case "ArrowLeft":
71
+ i > 0 && n(e.closeToParent(i));
72
+ break;
73
+ case "Enter":
74
+ case " ":
75
+ r?.click();
76
+ break;
77
+ default: return;
78
+ }
79
+ t.preventDefault(), t.stopPropagation();
80
+ }
81
+ function c(e, n, r) {
82
+ return e?.composedPath().find((e) => e instanceof HTMLElement && e.matches(r)) || t(n);
83
+ }
84
+ function l(e) {
85
+ return e ? r(e) : [];
86
+ }
87
+ function u(t) {
88
+ let n = /* @__PURE__ */ new Map();
89
+ return {
90
+ take(e) {
91
+ let t = n.get(e);
92
+ return t && n.delete(e), t;
93
+ },
94
+ closeAsync(r, i) {
95
+ n.set(r, i), (async () => {
96
+ await e(t.getPanel(i)) && n.get(r) === i && (n.delete(r), t.restoreItems(i, r), t.dispose(i));
97
+ })();
98
+ },
99
+ restoreAll() {
100
+ n.forEach((e, n) => {
101
+ t.restoreItems(e, n), t.dispose(e);
102
+ }), n.clear();
103
+ },
104
+ disposeAll() {
105
+ n.forEach((e) => t.dispose(e)), n.clear();
106
+ }
107
+ };
108
+ }
109
+ //#endregion
110
+ export { u as createClosingSubmenuStack, o as createMenuHoverBinder, i as createMenuOutsideClickGuard, l as getEnabledMenuLevelItems, c as getFocusedMenuItemFromPanels, s as handleMenuKeyboard };
111
+
112
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/shared/menu-behavior/index.ts"],"sourcesContent":["/**\n * 菜单行为层:dropdown 与 context-menu 共用的交互行为。\n *\n * 结构层(面板构建、迁移、定位)由 menu-portal/menu-tree 与各组件自己承载;\n * 这里只收口四种在两个组件中逐行复制的行为:\n * 1. outside-click 守卫(同帧吞掉打开点击 + composedPath 内含判定)\n * 2. 层级 hover 绑定(200ms 打开计时、touch 守卫、hover 收起更深层级)\n * 3. roving 键盘导航(Arrow/Home/End/Enter/Space/Escape)\n * 4. 关闭中的 submenu presence 收尾栈(动画结束后归还子项并销毁面板)\n *\n * 组件是 adapter:提供层级容器/项的查询与打开/关闭原语,行为由此驱动。\n */\nimport { findFocusedMenuItem, focusMenuItem, getEnabledMenuItems } from '@/shared/menu-portal/menu-tree'\nimport { hideOverlayPresence as hidePanelPresence } from '@/shared/overlay/presence'\n\n/** 菜单层级面板的最小视图:行为层只需要面板(presence/contains)与内容容器(项查询)。 */\nexport interface MenuLevelSurface {\n panel: HTMLElement\n content: HTMLElement\n}\n\n/* ------------------------------------------------------------------ *\n * Outside-click 守卫\n * ------------------------------------------------------------------ */\n\nexport interface MenuOutsideClickGuard {\n /** 打开动作后调用:吞掉同一帧内随后冒泡到 document 的本次 click。 */\n arm(): void\n isArmed(): boolean\n /** 事件是否发生在宿主 shadow root 或任何已打开面板内部。 */\n isInside(event: MouseEvent): boolean\n dispose(): void\n}\n\nexport function createMenuOutsideClickGuard(\n host: HTMLElement,\n isInsideOverlayPanel: (node: Node) => boolean\n): MenuOutsideClickGuard {\n let armed = false\n let disarmTimer: ReturnType<typeof setTimeout> | undefined\n\n const isInside = (event: MouseEvent): boolean => {\n for (const node of event.composedPath()) {\n if (node === host || node === host.shadowRoot) return true\n if (node instanceof Node && node.getRootNode() === host.shadowRoot) return true\n if (node instanceof Node && isInsideOverlayPanel(node)) return true\n }\n return false\n }\n\n return {\n arm() {\n armed = true\n clearTimeout(disarmTimer)\n disarmTimer = setTimeout(() => {\n armed = false\n })\n },\n isArmed: () => armed,\n isInside,\n dispose() {\n clearTimeout(disarmTimer)\n armed = false\n }\n }\n}\n\n/* ------------------------------------------------------------------ *\n * 层级 hover 绑定\n * ------------------------------------------------------------------ */\n\nexport interface MenuHoverDelegate {\n /** 当前展开的 submenu 深度;行为层会绑定 0..depth+1 每一层。 */\n getOpenDepth(): number\n getLevelItems(level: number): HTMLElement[]\n /** 该层当前激活(子面板已展开)的项;用于避免重复打开。 */\n getActiveItem(level: number): HTMLElement | undefined\n isSubmenuItem(item: HTMLElement): boolean\n /** hover 到 submenu 项且它不是当前激活项。 */\n openSubmenu(item: HTMLElement): void\n /** hover 到叶子项且存在更深层级:关闭比它深的子面板(内部自行重新 bind)。 */\n closeFrom(level: number): void\n}\n\nexport interface MenuHoverBinder {\n bind(): void\n dispose(): void\n}\n\nconst SUBMENU_HOVER_DELAY = 200\n\nexport function createMenuHoverBinder(delegate: MenuHoverDelegate, itemSelector: string): MenuHoverBinder {\n let openTimer: ReturnType<typeof setTimeout> | undefined\n let cleanupFns: (() => void)[] = []\n\n function bind() {\n cleanupFns.forEach(fn => fn())\n cleanupFns = []\n\n const depth = delegate.getOpenDepth()\n for (let level = 0; level <= depth + 1; level++) {\n const items = delegate.getLevelItems(level)\n if (!items.length) continue\n items.forEach(item => {\n if (!item.matches(itemSelector) || item.hasAttribute('disabled')) return\n const handler = (event: PointerEvent) => {\n if (event.pointerType === 'touch') return\n clearTimeout(openTimer)\n if (delegate.isSubmenuItem(item)) {\n if (delegate.getActiveItem(level) !== item) {\n openTimer = setTimeout(() => delegate.openSubmenu(item), SUBMENU_HOVER_DELAY)\n }\n } else if (delegate.getOpenDepth() > level) {\n delegate.closeFrom(level)\n }\n }\n item.addEventListener('pointerenter', handler, { passive: true })\n cleanupFns.push(() => item.removeEventListener('pointerenter', handler))\n })\n }\n }\n\n return {\n bind,\n dispose() {\n clearTimeout(openTimer)\n cleanupFns.forEach(fn => fn())\n cleanupFns = []\n }\n }\n}\n\n/* ------------------------------------------------------------------ *\n * roving 键盘导航\n * ------------------------------------------------------------------ */\n\nexport interface MenuKeyboardDelegate {\n getFocusedItem(event?: KeyboardEvent): HTMLElement | undefined\n /** 焦点项所在层级;不在任何面板内时返回 undefined。 */\n getLevelOf(item: HTMLElement): number | undefined\n getEnabledItems(level: number): HTMLElement[]\n isSubmenuItem(item: HTMLElement): boolean\n /** 键盘 ArrowRight:以 instant 语义打开该项的子面板。 */\n openSubmenuInstant(item: HTMLElement): void\n /** 键盘 ArrowLeft:关闭 level 层子面板并返回其父项。 */\n closeToParent(level: number): HTMLElement | undefined\n /** Escape:关闭最深一层子面板;没有子面板时关闭整个菜单。 */\n closeDeepestOrAll(): void\n}\n\n/**\n * 处理菜单键盘事件;未命中的键原样返回,调用方不做 preventDefault。\n * 事件监听的挂载位置(宿主 / 面板 / document)由组件自己决定。\n */\nexport function handleMenuKeyboard(delegate: MenuKeyboardDelegate, e: KeyboardEvent): void {\n if (e.key === 'Escape') {\n delegate.closeDeepestOrAll()\n e.preventDefault()\n return\n }\n\n const focused = delegate.getFocusedItem(e)\n const level = focused ? delegate.getLevelOf(focused) : undefined\n if (level === undefined) return\n const items = delegate.getEnabledItems(level)\n const currentIndex = focused ? items.indexOf(focused) : -1\n\n switch (e.key) {\n case 'ArrowDown':\n focusMenuItem(items[(currentIndex + 1 + items.length) % items.length])\n break\n case 'ArrowUp':\n focusMenuItem(items[(currentIndex - 1 + items.length) % items.length])\n break\n case 'Home':\n focusMenuItem(items[0])\n break\n case 'End':\n focusMenuItem(items.at(-1))\n break\n case 'ArrowRight':\n if (focused?.hasAttribute('submenu')) {\n delegate.openSubmenuInstant(focused)\n // 双 rAF:等子面板构建/迁移完成后再聚焦其第一个可用项\n requestAnimationFrame(() => requestAnimationFrame(() => focusMenuItem(delegate.getEnabledItems(level + 1)[0])))\n }\n break\n case 'ArrowLeft':\n if (level > 0) {\n focusMenuItem(delegate.closeToParent(level))\n }\n break\n case 'Enter':\n case ' ':\n focused?.click()\n break\n default:\n return\n }\n e.preventDefault()\n e.stopPropagation()\n}\n\n/** 供 getFocusedItem 实现:优先取事件 composedPath 上的菜单项,其次查找面板内已聚焦项。 */\nexport function getFocusedMenuItemFromPanels(\n event: KeyboardEvent | undefined,\n panels: Iterable<HTMLElement | undefined>,\n itemSelector: string\n): HTMLElement | undefined {\n const eventItem = event\n ?.composedPath()\n .find((node): node is HTMLElement => node instanceof HTMLElement && node.matches(itemSelector))\n if (eventItem) return eventItem\n\n return findFocusedMenuItem(panels)\n}\n\n/** 供 getEnabledItems 实现:从层级 content 查询可用项。 */\nexport function getEnabledMenuLevelItems(content: HTMLElement | undefined): HTMLElement[] {\n return content ? getEnabledMenuItems(content) : []\n}\n\n/* ------------------------------------------------------------------ *\n * 关闭中的 submenu presence 收尾栈\n * ------------------------------------------------------------------ */\n\nexport interface ClosingSubmenuStack<C> {\n /** 取出正在关闭的容器以复用(同帧关闭→重开同一父项)。 */\n take(parentItem: HTMLElement): C | undefined\n /** 挂起关闭:等出场动画结束后归还子项并销毁面板。 */\n closeAsync(parentItem: HTMLElement, container: C): void\n /** 立即归还并销毁全部关闭中的容器(整菜单关闭 / 重开路径)。 */\n restoreAll(): void\n /** 全部强制销毁(断开连接时),不再归还子项。 */\n disposeAll(): void\n}\n\nexport interface ClosingSubmenuStackAdapter<C> {\n getPanel(container: C): HTMLElement\n /** 归子项回父项(moveMenuChildren 与隐藏补丁由 adapter 决定)。 */\n restoreItems(container: C, parentItem: HTMLElement): void\n /** 销毁面板(remove + overlay api.dispose 等)。 */\n dispose(container: C): void\n}\n\nexport function createClosingSubmenuStack<C>(adapter: ClosingSubmenuStackAdapter<C>): ClosingSubmenuStack<C> {\n const closing = new Map<HTMLElement, C>()\n\n return {\n take(parentItem) {\n const container = closing.get(parentItem)\n if (container) closing.delete(parentItem)\n return container\n },\n closeAsync(parentItem, container) {\n // 先登记再等出场动画:同帧关闭→重开时 take() 会从这里取走复用\n closing.set(parentItem, container)\n void (async () => {\n if (!(await hidePanelPresence(adapter.getPanel(container)))) return\n if (closing.get(parentItem) !== container) return\n\n closing.delete(parentItem)\n adapter.restoreItems(container, parentItem)\n adapter.dispose(container)\n })()\n },\n restoreAll() {\n closing.forEach((container, parentItem) => {\n adapter.restoreItems(container, parentItem)\n adapter.dispose(container)\n })\n closing.clear()\n },\n disposeAll() {\n closing.forEach(container => adapter.dispose(container))\n closing.clear()\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"}
@@ -0,0 +1,12 @@
1
+ //#region src/shared/open-state/index.ts
2
+ function e(e, t) {
3
+ e.dispatchEvent(new CustomEvent("open-change", {
4
+ detail: { open: t },
5
+ bubbles: !0,
6
+ composed: !0
7
+ }));
8
+ }
9
+ //#endregion
10
+ export { e as dispatchOpenChangeEvent };
11
+
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/shared/open-state/index.ts"],"sourcesContent":["import type { LitElement } from 'lit'\n\n/**\n * overlay 族 `open-change` 事件的单一发出点与契约说明。\n *\n * 同名事件存在两种 flavor(决策见 ADR-0005 / ADR-0007,勿混淆):\n * - **request 语义**(dialog / drawer / layout):controlled 属性开启时,\n * `open-change` 是用户的关闭/打开\"请求\",组件不自行修改 `open`,等待回写;\n * - **notification 语义**(popover / tooltip / dropdown / context-menu /\n * select / autocomplete / collapse):组件总是自行变更 `open`,事件只作通知。\n *\n * 两种 flavor 的机械发出在此收敛;flavor 决策(是否 controlled 门控)留在各组件。\n */\nexport interface OpenChangeEventDetail {\n open: boolean\n}\n\nexport function dispatchOpenChangeEvent(host: LitElement, open: boolean): void {\n host.dispatchEvent(\n new CustomEvent<OpenChangeEventDetail>('open-change', {\n detail: { open },\n bubbles: true,\n composed: true\n })\n )\n}\n"],"mappings":";AAiBA,SAAgB,EAAwB,GAAkB,GAAqB;CAC7E,EAAK,cACH,IAAI,YAAmC,eAAe;EACpD,QAAQ,EAAE,QAAK;EACf,SAAS;EACT,UAAU;CACZ,CAAC,CACH;AACF"}
@@ -0,0 +1,43 @@
1
+ import { overlayComposition as e } from "../overlay/composition.js";
2
+ //#region src/shared/option-portal/combobox-shell.ts
3
+ function t(e) {
4
+ function t(t = !1) {
5
+ e.canOpen() === !1 || e.getIsOpen() || (e.setIsOpen(!0), e.dispatchOpenChange(), e.onOpen?.(t), e.syncScrollLock(!0), e.isPortal() ? requestAnimationFrame(() => {
6
+ e.getIsOpen() && e.openOverlay(t);
7
+ }) : e.openOverlay(t));
8
+ }
9
+ function n() {
10
+ e.getIsOpen() && (e.onBeforeClose?.(), e.setIsOpen(!1), e.dispatchOpenChange(), e.onAfterClose?.(), e.syncScrollLock(!1), e.closeOverlay());
11
+ }
12
+ return {
13
+ open: t,
14
+ close: n
15
+ };
16
+ }
17
+ function n(e) {
18
+ function t(t) {
19
+ t.addEventListener("click", e.onClick), t.addEventListener("pointerover", e.onPointerOver), t.addEventListener("pointerdown", e.onPointerDown), t.addEventListener("option-update", e.onUpdate);
20
+ }
21
+ function n(t) {
22
+ t.removeEventListener("click", e.onClick), t.removeEventListener("pointerover", e.onPointerOver), t.removeEventListener("pointerdown", e.onPointerDown), t.removeEventListener("option-update", e.onUpdate);
23
+ }
24
+ return {
25
+ bind: t,
26
+ unbind: n
27
+ };
28
+ }
29
+ function r(t, n, r, i) {
30
+ requestAnimationFrame(() => {
31
+ let a = n();
32
+ r() && !t.matches(":focus-within") && !(a && e.hasFocusWithin(a)) && i();
33
+ });
34
+ }
35
+ function i(e, t, n) {
36
+ if (e === 0) return -1;
37
+ let r = t + n;
38
+ return r < 0 && (r = e - 1), r >= e && (r = 0), r;
39
+ }
40
+ //#endregion
41
+ export { t as createComboboxOpenController, n as createOptionListenerBinding, r as handleComboboxFocusOut, i as nextWrappingIndex };
42
+
43
+ //# sourceMappingURL=combobox-shell.js.map
@@ -0,0 +1 @@
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,48 +1,49 @@
1
- import { definePlugin as e } from "@greypan/js-kit";
1
+ import { createComboboxOpenController as e, createOptionListenerBinding as t, handleComboboxFocusOut as n, nextWrappingIndex as r } from "./combobox-shell.js";
2
+ import { definePlugin as i } from "@greypan/js-kit";
2
3
  //#region src/shared/option-portal/index.ts
3
- var t = "web-ui-option", n = () => e((e) => {
4
- let n = /* @__PURE__ */ new Set(), r = /* @__PURE__ */ new WeakMap(), i = 0, a = (t) => {
4
+ var a = "web-ui-option", o = () => i((e) => {
5
+ let t = /* @__PURE__ */ new Set(), n = /* @__PURE__ */ new WeakMap(), r = 0, i = (t) => {
5
6
  e.bindOption(t), t.addEventListener("option-unregister", s);
6
7
  }, o = (t) => {
7
8
  e.unbindOption(t), t.removeEventListener("option-unregister", s);
8
9
  }, s = (e) => {
9
- e.target instanceof HTMLElement && e.target.localName === t && n.add(e.target), l.scheduleRefresh();
10
+ e.target instanceof HTMLElement && e.target.localName === a && t.add(e.target), l.scheduleRefresh();
10
11
  }, c = () => {
11
12
  l.scheduleRefresh();
12
13
  }, l = {
13
14
  queryOptions() {
14
- let n = e.getPortal()?.panel ?? e.getPortalContent(), r = n ? [...n.querySelectorAll(t)] : [];
15
- return r.length > 0 ? r : [...e.element.querySelectorAll(t)];
15
+ let t = e.getPortal()?.panel ?? e.getPortalContent(), n = t ? [...t.querySelectorAll(a)] : [];
16
+ return n.length > 0 ? n : [...e.element.querySelectorAll(a)];
16
17
  },
17
18
  refresh(e, t) {
18
19
  let n = l.queryOptions();
19
20
  for (let t of e) n.includes(t) || o(t);
20
- return n.forEach(a), {
21
+ return n.forEach(i), {
21
22
  options: n,
22
23
  activeIndex: t && n.includes(t) ? n.indexOf(t) : -1
23
24
  };
24
25
  },
25
26
  ensureOptionIds(t) {
26
- let n = new Set(t.map((e) => e.id).filter((e) => !!e));
27
+ let i = new Set(t.map((e) => e.id).filter((e) => !!e));
27
28
  t.forEach((t) => {
28
29
  if (t.id) return;
29
- let a = r.get(t);
30
- if (!a || n.has(a)) {
30
+ let a = n.get(t);
31
+ if (!a || i.has(a)) {
31
32
  do
32
- a = `${e.idPrefix}-option-${++i}`;
33
- while (n.has(a));
34
- r.set(t, a);
33
+ a = `${e.idPrefix}-option-${++r}`;
34
+ while (i.has(a));
35
+ n.set(t, a);
35
36
  }
36
- t.id = a, n.add(a);
37
+ t.id = a, i.add(a);
37
38
  });
38
39
  },
39
40
  syncPortalContent() {
40
- let t = e.getPortal(), r = e.getPortalContent();
41
- if (!t || !r) return;
42
- for (let i of n) !r.contains(i) && !e.element.contains(i) && t.removeContent([i]);
43
- if (n.clear(), !e.isOpen()) return;
41
+ let n = e.getPortal(), r = e.getPortalContent();
42
+ if (!n || !r) return;
43
+ for (let i of t) !r.contains(i) && !e.element.contains(i) && n.removeContent([i]);
44
+ if (t.clear(), !e.isOpen()) return;
44
45
  let i = e.getMigratableNodes().filter((e) => !(e instanceof Comment));
45
- i.length && t.appendContent(i, r);
46
+ i.length && n.appendContent(i, r);
46
47
  },
47
48
  scheduleRefresh() {
48
49
  queueMicrotask(() => {
@@ -54,12 +55,12 @@ var t = "web-ui-option", n = () => e((e) => {
54
55
  e.element.addEventListener("option-register", c), e.element.addEventListener("option-unregister", s);
55
56
  },
56
57
  dispose() {
57
- e.element.removeEventListener("option-register", c), e.element.removeEventListener("option-unregister", s), n.clear();
58
+ e.element.removeEventListener("option-register", c), e.element.removeEventListener("option-unregister", s), t.clear();
58
59
  }
59
60
  };
60
61
  return l;
61
62
  });
62
63
  //#endregion
63
- export { n as defineOptionPortal };
64
+ export { e as createComboboxOpenController, t as createOptionListenerBinding, o as defineOptionPortal, n as handleComboboxFocusOut, r as nextWrappingIndex };
64
65
 
65
66
  //# sourceMappingURL=index.js.map