@greypan/web-ui 6.3.1 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.CN.md +145 -42
- package/README.md +156 -53
- package/dist/_virtual/{_@oxc-project_runtime@0.146.0 → _@oxc-project_runtime@0.149.0}/helpers/esm/decorate.js +1 -1
- package/dist/assets/glass.js +1 -1
- package/dist/assets/glass.js.map +1 -1
- package/dist/assets/menu-portal.js +1 -1
- package/dist/assets/menu-portal.js.map +1 -1
- package/dist/assets/overlay-motion.js +1 -1
- package/dist/assets/overlay-motion.js.map +1 -1
- package/dist/assets/selection-control.js +6 -0
- package/dist/assets/selection-control.js.map +1 -0
- package/dist/assets/toast-containers.js.map +1 -1
- package/dist/components/autocomplete/index.d.ts +2 -0
- package/dist/components/autocomplete/index.js +22 -23
- package/dist/components/autocomplete/index.js.map +1 -1
- package/dist/components/autocomplete/style.js +1 -1
- package/dist/components/autocomplete/style.js.map +1 -1
- package/dist/components/avatar/index.js +1 -1
- package/dist/components/back-top/index.js +1 -1
- package/dist/components/badge/index.js +1 -1
- package/dist/components/badge/style.js.map +1 -1
- package/dist/components/button/index.js +1 -1
- package/dist/components/button/style.js.map +1 -1
- package/dist/components/button-group/index.js +1 -1
- package/dist/components/checkbox/index.d.ts +9 -2
- package/dist/components/checkbox/index.js +52 -28
- package/dist/components/checkbox/index.js.map +1 -1
- package/dist/components/checkbox/style.js +1 -1
- package/dist/components/checkbox/style.js.map +1 -1
- package/dist/components/checkbox-group/index.d.ts +3 -0
- package/dist/components/checkbox-group/index.js +26 -15
- package/dist/components/checkbox-group/index.js.map +1 -1
- package/dist/components/checkbox-group/style.js +1 -1
- package/dist/components/checkbox-group/style.js.map +1 -1
- package/dist/components/collapse/index.js +3 -3
- package/dist/components/collapse/index.js.map +1 -1
- package/dist/components/context-menu/index.d.ts +10 -0
- package/dist/components/context-menu/index.js +53 -39
- package/dist/components/context-menu/index.js.map +1 -1
- package/dist/components/dialog/index.d.ts +6 -0
- package/dist/components/dialog/index.js +46 -21
- package/dist/components/dialog/index.js.map +1 -1
- package/dist/components/dialog/style.js +1 -1
- package/dist/components/dialog/style.js.map +1 -1
- package/dist/components/drawer/index.d.ts +22 -4
- package/dist/components/drawer/index.js +135 -107
- package/dist/components/drawer/index.js.map +1 -1
- package/dist/components/drawer/style.js +1 -1
- package/dist/components/drawer/style.js.map +1 -1
- package/dist/components/dropdown/index.d.ts +21 -1
- package/dist/components/dropdown/index.js +78 -55
- package/dist/components/dropdown/index.js.map +1 -1
- package/dist/components/dropdown-divider/index.js +1 -1
- package/dist/components/dropdown-header/index.js +1 -1
- package/dist/components/dropdown-item/index.js +1 -1
- package/dist/components/empty/index.js +1 -1
- package/dist/components/icon/index.js +1 -1
- package/dist/components/icon/style.js +1 -1
- package/dist/components/icon/style.js.map +1 -1
- package/dist/components/image-preview/index.js +89 -69
- package/dist/components/image-preview/index.js.map +1 -1
- package/dist/components/image-preview/style.js +1 -1
- package/dist/components/image-preview/style.js.map +1 -1
- package/dist/components/index.js +29 -29
- package/dist/components/input/index.js +4 -4
- package/dist/components/input/index.js.map +1 -1
- package/dist/components/input/style.js.map +1 -1
- package/dist/components/input-number/index.js +2 -2
- package/dist/components/input-number/index.js.map +1 -1
- package/dist/components/layout/index.js +3 -3
- package/dist/components/layout/index.js.map +1 -1
- package/dist/components/layout/style.js +1 -1
- package/dist/components/layout/style.js.map +1 -1
- package/dist/components/option/index.js +1 -1
- package/dist/components/popover/index.d.ts +8 -2
- package/dist/components/popover/index.js +48 -53
- package/dist/components/popover/index.js.map +1 -1
- package/dist/components/radio/index.d.ts +1 -1
- package/dist/components/radio/index.js +16 -15
- package/dist/components/radio/index.js.map +1 -1
- package/dist/components/radio/style.js +1 -1
- package/dist/components/radio/style.js.map +1 -1
- package/dist/components/radio-group/index.d.ts +3 -0
- package/dist/components/radio-group/index.js +26 -15
- package/dist/components/radio-group/index.js.map +1 -1
- package/dist/components/radio-group/style.js +1 -1
- package/dist/components/radio-group/style.js.map +1 -1
- package/dist/components/segmented/index.js +1 -1
- package/dist/components/segmented/style.js +1 -1
- package/dist/components/segmented/style.js.map +1 -1
- package/dist/components/segmented-trigger/index.js +1 -1
- package/dist/components/select/index.d.ts +2 -0
- package/dist/components/select/index.js +23 -22
- package/dist/components/select/index.js.map +1 -1
- package/dist/components/select/style.js +1 -1
- package/dist/components/select/style.js.map +1 -1
- package/dist/components/slider/index.js +1 -1
- package/dist/components/slider/index.js.map +1 -1
- package/dist/components/slider/style.js +1 -1
- package/dist/components/slider/style.js.map +1 -1
- package/dist/components/spinner/index.js +2 -2
- package/dist/components/spinner/index.js.map +1 -1
- package/dist/components/spinner/style.js +1 -1
- package/dist/components/spinner/style.js.map +1 -1
- package/dist/components/svg-draw-lines/index.d.ts +13 -2
- package/dist/components/svg-draw-lines/index.js +45 -32
- package/dist/components/svg-draw-lines/index.js.map +1 -1
- package/dist/components/switch/index.js +2 -2
- package/dist/components/switch/index.js.map +1 -1
- package/dist/components/switch/style.js +1 -1
- package/dist/components/switch/style.js.map +1 -1
- package/dist/components/textarea/index.js +5 -5
- package/dist/components/textarea/index.js.map +1 -1
- package/dist/components/textarea/style.js.map +1 -1
- package/dist/components/theme/index.d.ts +17 -0
- package/dist/components/theme/index.js +181 -21
- package/dist/components/theme/index.js.map +1 -1
- package/dist/components/theme/style.js +1 -1
- package/dist/components/theme/style.js.map +1 -1
- package/dist/components/toast/index.d.ts +1 -1
- package/dist/components/toast/manager.d.ts +1 -3
- package/dist/components/toast/manager.js +92 -49
- package/dist/components/toast/manager.js.map +1 -1
- package/dist/components/toast/style.js +1 -1
- package/dist/components/toast/style.js.map +1 -1
- package/dist/components/toast/toast.d.ts +37 -0
- package/dist/components/toast/toast.js +62 -9
- package/dist/components/toast/toast.js.map +1 -1
- package/dist/components/toast/types.d.ts +0 -4
- package/dist/components/tooltip/index.d.ts +6 -1
- package/dist/components/tooltip/index.js +17 -15
- package/dist/components/tooltip/index.js.map +1 -1
- package/dist/shared/form-association/index.js +1 -1
- package/dist/shared/form-association/index.js.map +1 -1
- package/dist/shared/gesture/drag-gesture.js +66 -64
- package/dist/shared/gesture/drag-gesture.js.map +1 -1
- package/dist/shared/gesture/physics.js +7 -29
- package/dist/shared/gesture/physics.js.map +1 -1
- package/dist/shared/menu-behavior/index.js +3 -4
- package/dist/shared/menu-behavior/index.js.map +1 -1
- package/dist/shared/menu-portal/menu-tree.js +1 -1
- package/dist/shared/menu-portal/menu-tree.js.map +1 -1
- package/dist/shared/option-portal/combobox-shell.js +7 -8
- package/dist/shared/option-portal/combobox-shell.js.map +1 -1
- package/dist/shared/overlay/anchored-panel.js +36 -31
- package/dist/shared/overlay/anchored-panel.js.map +1 -1
- package/dist/shared/overlay/native-dialog-presence.js +18 -2
- package/dist/shared/overlay/native-dialog-presence.js.map +1 -1
- package/dist/shared/overlay/nested-drawer-layers.js +7 -4
- package/dist/shared/overlay/nested-drawer-layers.js.map +1 -1
- package/dist/shared/overlay/open-overlay.js +182 -0
- package/dist/shared/overlay/open-overlay.js.map +1 -0
- package/dist/shared/overlay/overlay.js +1 -1
- package/dist/shared/overlay/overlay.js.map +1 -1
- package/dist/shared/overlay/portal.js +1 -1
- package/dist/shared/overlay/portal.js.map +1 -1
- package/dist/shared/overlay/presence.js +19 -22
- package/dist/shared/overlay/presence.js.map +1 -1
- package/dist/shared/theme/duration.js +9 -0
- package/dist/shared/theme/duration.js.map +1 -0
- package/dist/shared/theme/reduced-motion.js +15 -0
- package/dist/shared/theme/reduced-motion.js.map +1 -0
- package/dist/shared/visible-area/index.js +1 -1
- package/dist/shared/visible-area/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/shared/overlay/composition.js +0 -57
- package/dist/shared/overlay/composition.js.map +0 -1
- package/dist/shared/overlay/lifecycle.js +0 -41
- package/dist/shared/overlay/lifecycle.js.map +0 -1
- package/dist/vite.svg +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"portal.js","names":[],"sources":["../../../src/shared/overlay/portal.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\n\nimport { getFallbackOverlayRoot } from '@/shared/overlay/overlay-root'\nimport { findNearestTheme, findRootTheme } from '@/shared/overlay/theme-overlay-scope'\n\nexport type OverlayContainer = HTMLElement | (() => HTMLElement | undefined)\n\nexport interface OverlayContainerResolutionOptions {\n /** 无 target 时优先使用文档中 root theme 的 theme-owned overlay root。 */\n preferRootTheme?: boolean\n}\n\nexport interface OverlayPortalOptions {\n container?: OverlayContainer\n target: Element\n style: string\n className: string\n /**\n * 额外的面板内容变化反应。注意:已迁移节点被框架物理删除后的解除追踪\n * (removeContent)是 portal 的内建默认行为,不经过该回调;这里只承载\n * 组件自己的附加逻辑(如 option 域的刷新、空态占位改写)。\n */\n onContentChange?: (mutations: MutationRecord[]) => void\n /**\n * 打开期实时渲染契约:框架(Vue/React 条件渲染)可能在浮层打开期间向宿主\n * light DOM 持续插入节点。提供该回调时,portal 监听宿主直接子节点的 childList,\n * 把回调认定的可迁移节点增量迁入面板;关闭恢复时与初始迁移节点一样按原位回插。\n * 回调应只返回渲染性内容(元素/文本),框架注释锚点必须留在宿主,\n * 否则框架后续 patch 会以面板内节点为插入基准。\n * 返回空数组表示本次新增无需迁移。\n */\n migrateAddedNodes?: (addedNodes: Node[]) => Node[]\n}\n\nexport interface OverlayPortal {\n readonly panel: HTMLElement\n restoreContent(): void\n moveContent(nodes: Node[], target?: HTMLElement): void\n appendContent(nodes: Node[], target?: HTMLElement): void\n removeContent(nodes: Node[]): void\n remove(): void\n}\n\n/**\n * Portal 面板位于独立 shadow root,宿主上的 CSS 自定义属性不会沿渲染树继承到 panel。\n * 这里在创建时镜像锚点解析后的变量,让组件 host 上的尺寸配置对 portal 仍然生效。\n */\nexport function applyOverlayVariables(panel: HTMLElement, source: Element, variables: readonly string[]): void {\n const computedStyle = getComputedStyle(source)\n for (const variable of variables) {\n const value = computedStyle.getPropertyValue(variable).trim()\n if (value) panel.style.setProperty(variable, value)\n }\n}\n\n/**\n * 迁移节点的追踪条目。每个节点迁入面板时在宿主 light DOM 原位留一个 marker 注释:\n * 宿主因此始终持有完整有序的骨架(marker + 框架注释锚点 + 未迁移节点),\n * 框架(Vue/React 条件渲染)打开期的插入以宿主为容器、以真实锚点为基准;\n * 面板内节点按宿主骨架序重排(中段插入不落到面板末尾);关闭时按 marker 位\n * 双向归还、marker 摘除。机制与 menu-tree 的托管菜单项锚一致(context-menu/dropdown 共用)。\n */\ninterface TrackedNodeEntry {\n node: Node\n marker: Comment\n}\n\nconst PORTAL_CONTENT_MARKER = 'wui-portal-content-anchor'\n\n/**\n * 构建带 Shadow DOM 样式边界的 Portal 面板,并追踪被迁移的内容节点以便恢复。\n */\nexport const defineOverlayPortal = () =>\n definePlugin<OverlayPortal, OverlayPortalOptions>(ctx => {\n const host = document.createElement('div')\n // :host 规则可能让宿主在 overlay 容器中生成行盒;display: contents 保留面板定位而不参与布局。\n host.style.display = 'contents'\n\n const root = host.attachShadow({ mode: 'open' })\n const style = document.createElement('style')\n style.textContent = ctx.style\n const panel = document.createElement('div')\n // 单层玻璃:wui-glass 原样留在面板自身,背景/阴影/backdrop-filter 随面板\n // opacity 与 blur 插值一起过渡(见 overlay-motion.css),无 blur/surface 双层。\n panel.className = ctx.className\n panel.dataset.wuiPresence = 'entering'\n root.append(style, panel)\n\n const trackedNodes: TrackedNodeEntry[] = []\n const contentObserver = new MutationObserver(mutations => {\n // 内部不变量先于消费方回调执行:配对依赖 trackedNodes 尚未被消费方 untrack 的状态。\n returnStrayFrameworkComments(mutations)\n // 框架在打开期物理删除已迁移节点:即时解除追踪,否则关闭恢复时会把已删除\n // 节点复活回宿主 light DOM。面板内部移动(重排 insertBefore)在同一 mutation\n // record 中同时出现在 removed/added,不属于删除,须排除。\n const addedInBatch = new Set(mutations.flatMap(mutation => [...mutation.addedNodes]))\n const removedNodes = mutations\n .flatMap(mutation => [...mutation.removedNodes])\n .filter(node => !addedInBatch.has(node))\n if (removedNodes.length) untrackNodes(removedNodes)\n ctx.onContentChange?.(mutations)\n })\n // 同一 observer 批次内被插入又移除的游离节点没有宿主位可锚,不追踪不迁移\n // (insertBefore 要求 node 是 parent 的子节点);其归宿由框架的删除语义决定。\n const recordEntry = (node: Node): TrackedNodeEntry | null => {\n const parent = node.parentNode\n if (!parent) return null\n const marker = document.createComment(PORTAL_CONTENT_MARKER)\n parent.insertBefore(marker, node)\n const entry: TrackedNodeEntry = { node, marker }\n trackedNodes.push(entry)\n return entry\n }\n const untrackNodes = (nodes: Node[]) => {\n for (const node of nodes) {\n const index = trackedNodes.findIndex(entry => entry.node === node)\n if (index >= 0) {\n trackedNodes[index].marker.remove()\n trackedNodes.splice(index, 1)\n }\n }\n }\n // 面板内节点按宿主骨架序重排:骨架 = 宿主 childNodes 中本 portal 的 marker 序。\n // 只在已跟踪节点之间移动(insertBefore 相对定位),不触碰面板内的组件自有节点\n // (如 autocomplete 的空态占位);末位节点 append,越过面板内注释锚点的情形由\n // restoreContent 的相邻注释随迁兜底。\n const orderTargetBySkeleton = (target: HTMLElement) => {\n const skeleton: Node[] = []\n for (const node of Array.from(ctx.target.childNodes)) {\n if (node instanceof Comment && node.textContent === PORTAL_CONTENT_MARKER) {\n const entry = trackedNodes.find(e => e.marker === node)\n if (entry && entry.node.parentNode === target) skeleton.push(entry.node)\n }\n }\n if (skeleton.length === 0) return\n const current = Array.from(target.childNodes).filter(\n node => skeleton.includes(node) || !(node instanceof Comment)\n )\n if (current.length === skeleton.length && current.every((node, index) => node === skeleton[index])) return\n for (let index = skeleton.length - 1; index >= 0; index--) {\n const node = skeleton[index]\n const next = skeleton[index + 1]\n if (next) {\n if ((node as Element).nextElementSibling !== next) target.insertBefore(node, next)\n } else {\n target.appendChild(node)\n }\n }\n }\n\n /**\n * 打开期不变量:框架删除已迁移节点时,其条件渲染占位注释(v-if 等)会被插进\n * 面板内容区而不是宿主——block 子树 patch 以 hostParentNode(oldVNode.el) 重定\n * container,而旧 el 已在面板(可能是 panel 自身或组件的内容子容器)。\n * 注释是框架后续翻转依赖的 vnode.el:留在面板会随面板销毁,下次翻转把内容插进\n * 已脱离文档的旧面板(内容\"既不在宿主也不在面板\"且无法恢复)。\n * 每个面板 mutation 批次内即时归还:与同批次被框架删除的 tracked 节点按宿主\n * marker 序配对,插到其 marker 位(多注释对多删除为近似配对;无配对时落宿主\n * 骨架末尾兜底)。tracked 子树内的注释是迁移内容自身的嵌套条件渲染,不属游离\n * 锚点,必须留在原位;随整个子树被删的注释已不在面板内,无需救援。\n */\n const returnStrayFrameworkComments = (mutations: MutationRecord[]) => {\n const isInsideTrackedSubtree = (start: Node): boolean => {\n let current: Node | null = start\n while (current && current !== panel) {\n if (trackedNodes.some(entry => entry.node === current)) return true\n current = current.parentNode\n }\n return false\n }\n const addedNodes = new Set(mutations.flatMap(mutation => [...mutation.addedNodes]))\n const strays: Comment[] = []\n for (const mutation of mutations) {\n for (const node of mutation.addedNodes) {\n if (!(node instanceof Comment) || node.textContent === PORTAL_CONTENT_MARKER) continue\n // 同批次移入又移除的游离注释、随删除子树一起消失的注释都不在面板内,跳过\n if (!panel.contains(node)) continue\n if (isInsideTrackedSubtree(mutation.target)) continue\n strays.push(node)\n }\n }\n if (!strays.length) return\n const hostChildren = Array.from(ctx.target.childNodes)\n const removedEntries = trackedNodes\n .filter(\n entry =>\n !addedNodes.has(entry.node) && mutations.some(mutation => [...mutation.removedNodes].includes(entry.node))\n )\n .sort((a, b) => hostChildren.indexOf(a.marker) - hostChildren.indexOf(b.marker))\n for (const [index, comment] of strays.entries()) {\n const marker = removedEntries[Math.min(index, removedEntries.length - 1)]?.marker\n if (marker && marker.parentNode === ctx.target) ctx.target.insertBefore(comment, marker)\n else ctx.target.appendChild(comment)\n }\n }\n\n resolveOverlayContainer(ctx.container, ctx.target).appendChild(host)\n // 面板内容观察是 portal 自身的不变量(游离注释救援 + 删除节点解除追踪),\n // 不依赖消费方是否注册 onContentChange。\n contentObserver.observe(panel, { childList: true, subtree: true })\n\n // 打开期宿主 childList 观察(不含子树,框架 patch 嵌套组件的 light DOM 不属于\n // 本面板内容)。portal 实例的生命周期即打开期,随恢复/移除断开。\n const migrateAddedNodes = ctx.migrateAddedNodes\n const hostObserver = new MutationObserver(mutations => {\n // 框架清除逻辑(如 lit 的 _$clear)按宿主 marker 邻位遍历已迁移内容:元素\n // 物理上在面板,遍历找不到本体,会把 portal marker 当作内容本体移除。\n // marker 被框架摘除即框架删除语义——解除追踪并清掉滞留面板的游离元素;\n // 同批次移入又移回的 marker(框架重排)不属于删除。实时迁移仍以\n // migrateAddedNodes 的回调为准,无该回调的 portal(如 option 域)只做删除清理。\n const addedNodes = new Set(mutations.flatMap(mutation => [...mutation.addedNodes]))\n const strandedNodes = mutations\n .flatMap(mutation => [...mutation.removedNodes])\n .filter(node => node instanceof Comment && node.textContent === PORTAL_CONTENT_MARKER && !addedNodes.has(node))\n .map(marker => trackedNodes.find(entry => entry.marker === marker)?.node)\n .filter((node): node is Node => node !== undefined)\n if (strandedNodes.length) {\n api.removeContent(strandedNodes)\n // remove() 的规范语义即 parentNode.removeChild:宿主/类型层面都以 Node 表达\n for (const node of strandedNodes) node.parentNode?.removeChild(node)\n }\n if (!migrateAddedNodes) return\n const migratable = migrateAddedNodes(mutations.flatMap(mutation => [...mutation.addedNodes]))\n if (migratable.length) api.appendContent(migratable)\n })\n hostObserver.observe(ctx.target, { childList: true })\n\n const api: OverlayPortal = {\n panel,\n\n moveContent(nodes, target = panel) {\n trackedNodes.forEach(entry => entry.marker.remove())\n const entries = nodes.map(recordEntry).filter((entry): entry is TrackedNodeEntry => entry !== null)\n trackedNodes.length = 0\n trackedNodes.push(...entries)\n target.append(...nodes)\n orderTargetBySkeleton(target)\n },\n\n appendContent(nodes, target = panel) {\n for (const node of nodes) {\n untrackNodes([node])\n const entry = recordEntry(node)\n if (!entry) continue\n target.appendChild(entry.node)\n }\n orderTargetBySkeleton(target)\n },\n\n removeContent(nodes) {\n // 框架删除节点时插进面板的占位注释由 contentObserver 的内部不变量\n // (returnStrayFrameworkComments)即时归还宿主,这里只负责解除追踪。\n untrackNodes(nodes)\n },\n\n restoreContent() {\n // 先停掉实时迁移:恢复动作本身会改动宿主 childList,\n // 否则回插的节点会被当成新增内容再次迁入面板形成回环。\n hostObserver.disconnect()\n\n // 节点回到宿主中自己的 marker 位;面板内紧邻其前的框架注释锚点(v-if 占位)\n // 随元素一起迁回——Vue 仍持有这些锚点的 vnode.el 引用,留在面板会随面板销毁,\n // 下次翻转将因 parentNode === null 崩溃。\n for (const entry of trackedNodes) {\n const pending: Comment[] = []\n let previous = entry.node.previousSibling\n while (previous instanceof Comment && previous.textContent !== PORTAL_CONTENT_MARKER) {\n pending.unshift(previous)\n previous = previous.previousSibling\n }\n for (const comment of pending) {\n if (entry.marker.parentNode === ctx.target) ctx.target.insertBefore(comment, entry.marker)\n else ctx.target.appendChild(comment)\n }\n if (entry.marker.parentNode === ctx.target) ctx.target.insertBefore(entry.node, entry.marker)\n else ctx.target.appendChild(entry.node)\n entry.marker.remove()\n }\n trackedNodes.length = 0\n },\n\n remove() {\n hostObserver.disconnect()\n contentObserver.disconnect()\n // 防御:正常 dispose 先 restore(marker 已清),异常路径下避免 marker 泄漏在宿主。\n trackedNodes.forEach(entry => entry.marker.remove())\n trackedNodes.length = 0\n host.remove()\n }\n }\n return api\n })\n\n/**\n * 判断 node 是否是 slot 已分配节点(或其 light DOM 后代),即 node 实际渲染在 shadowRoot 内。\n * slotted 内容的 parentNode 仍停留在 light DOM,因此不能用普通祖先遍历跨过 shadow 边界,\n * 只能通过 slot.assignedNodes() 判断内容被渲染到哪个 slot。\n */\nfunction isAssignedIntoTarget(node: Node, assignedNode: Node, boundary: Node): boolean {\n let current: Node | null = node\n while (current && current !== boundary) {\n if (current === assignedNode) return true\n current = current.parentNode\n }\n return false\n}\n\n/** parentNode 为 null 时跨过 shadow 边界上行到 host(portal 面板等 shadow 子树的 parentNode 链会断裂)。 */\nfunction getParentAcrossShadow(node: Node): Node | null {\n if (node.parentNode) return node.parentNode\n const root = node.getRootNode()\n return root instanceof ShadowRoot ? root.host : null\n}\n\n/**\n * 查找包含 target 且处于打开态的最近原生 <dialog>。\n * 原生 showModal() 会把 dialog 提升到浏览器 top layer,而常规 overlay 容器在普通文档流,\n * 二者无法靠 z-index 叠序——overlay 会被 top layer 的 dialog 遮住。\n * 因此 target 被渲染到某个已打开的原生 dialog 内时,应把 overlay 挂到该 dialog 上,\n * 使其一并进入 top layer(抽屉 / 对话框内的 dropdown、tooltip、context-menu 均适用)。\n * 上行路径必须跨 shadow 边界:target 可能位于 portal 面板(shadow 子树)内,\n * 纯 parentNode 链在该处断裂会误判为 dialog 外,浮层落进 fallback root 被遮挡。\n */\nfunction findEnclosingOpenDialog(target: Element): HTMLDialogElement | null {\n let current: Node | null = target\n while (current && current !== document.documentElement) {\n if (current instanceof HTMLDialogElement && current.open) return current\n\n // 跨过 shadow host:检查其 shadowRoot 内已打开的 dialog 是否渲染了 target。\n if (current instanceof Element && current.shadowRoot) {\n const dialogs = current.shadowRoot.querySelectorAll('dialog')\n for (const dialog of dialogs) {\n if (!(dialog instanceof HTMLDialogElement) || !dialog.open) continue\n const slots = dialog.querySelectorAll('slot')\n for (const slot of slots) {\n for (const assigned of slot.assignedNodes()) {\n if (!(assigned instanceof Node)) continue\n if (isAssignedIntoTarget(target, assigned, current)) return dialog\n }\n }\n }\n }\n\n current = getParentAcrossShadow(current)\n }\n return null\n}\n\nexport function resolveOverlayContainer(\n container: OverlayContainer | undefined,\n target: Element,\n options: OverlayContainerResolutionOptions = {}\n): HTMLElement {\n const explicit = typeof container === 'function' ? container() : container\n if (explicit) return explicit\n\n // target 在已打开的原生 dialog / drawer 内:挂到该 dialog,加入 top layer。\n const enclosingDialog = findEnclosingOpenDialog(target)\n if (enclosingDialog) return enclosingDialog\n\n // 无 target 的调用(如菜单)优先使用 root theme 的 theme-owned overlay root;\n // 有 target 时使用最近 theme 的 theme-owned overlay root。\n const theme = options.preferRootTheme ? findRootTheme() : findNearestTheme(target)\n return theme?.getOverlayRoot() ?? getFallbackOverlayRoot()\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"portal.js","names":[],"sources":["../../../src/shared/overlay/portal.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\n\nimport { getFallbackOverlayRoot } from '@/shared/overlay/overlay-root'\nimport { findNearestTheme, findRootTheme } from '@/shared/overlay/theme-overlay-scope'\n\nexport type OverlayContainer = HTMLElement | (() => HTMLElement | undefined)\n\nexport interface OverlayContainerResolutionOptions {\n /** 无 target 时优先使用文档中 root theme 的 theme-owned overlay root。 */\n preferRootTheme?: boolean\n}\n\nexport interface OverlayPortalOptions {\n container?: OverlayContainer\n target: Element\n style: string\n className: string\n /**\n * 额外的面板内容变化反应。注意:已迁移节点被框架物理删除后的解除追踪\n * (removeContent)是 portal 的内建默认行为,不经过该回调;这里只承载\n * 组件自己的附加逻辑(如 option 域的刷新、空态占位改写)。\n */\n onContentChange?: (mutations: MutationRecord[]) => void\n /**\n * 打开期实时渲染契约:框架(Vue/React 条件渲染)可能在浮层打开期间向宿主\n * light DOM 持续插入节点。提供该回调时,portal 监听宿主直接子节点的 childList,\n * 把回调认定的可迁移节点增量迁入面板;关闭恢复时与初始迁移节点一样按原位回插。\n * 回调应只返回渲染性内容(元素/文本),框架注释锚点必须留在宿主,\n * 否则框架后续 patch 会以面板内节点为插入基准。\n * 返回空数组表示本次新增无需迁移。\n */\n migrateAddedNodes?: (addedNodes: Node[]) => Node[]\n}\n\nexport interface OverlayPortal {\n readonly panel: HTMLElement\n restoreContent(): void\n moveContent(nodes: Node[], target?: HTMLElement): void\n appendContent(nodes: Node[], target?: HTMLElement): void\n removeContent(nodes: Node[]): void\n remove(): void\n}\n\n/**\n * Portal 面板位于独立 shadow root,宿主上的 CSS 自定义属性不会沿渲染树继承到 panel。\n * 这里在创建时镜像锚点解析后的变量,让组件 host 上的尺寸配置对 portal 仍然生效。\n */\nexport function applyOverlayVariables(panel: HTMLElement, source: Element, variables: readonly string[]): void {\n const computedStyle = getComputedStyle(source)\n for (const variable of variables) {\n const value = computedStyle.getPropertyValue(variable).trim()\n if (value) panel.style.setProperty(variable, value)\n }\n}\n\n/**\n * 迁移节点的追踪条目。每个节点迁入面板时在宿主 light DOM 原位留一个 marker 注释:\n * 宿主因此始终持有完整有序的骨架(marker + 框架注释锚点 + 未迁移节点),\n * 框架(Vue/React 条件渲染)打开期的插入以宿主为容器、以真实锚点为基准;\n * 面板内节点按宿主骨架序重排(中段插入不落到面板末尾);关闭时按 marker 位\n * 双向归还、marker 摘除。机制与 menu-tree 的托管菜单项锚一致(context-menu/dropdown 共用)。\n */\ninterface TrackedNodeEntry {\n node: Node\n marker: Comment\n}\n\nconst PORTAL_CONTENT_MARKER = 'wui-portal-content-anchor'\n\n/**\n * 构建带 Shadow DOM 样式边界的 Portal 面板,并追踪被迁移的内容节点以便恢复。\n */\nexport const defineOverlayPortal = () =>\n definePlugin<OverlayPortal, OverlayPortalOptions>(ctx => {\n const host = document.createElement('div')\n // :host 规则可能让宿主在 overlay 容器中生成行盒;display: contents 保留面板定位而不参与布局。\n host.style.display = 'contents'\n\n const root = host.attachShadow({ mode: 'open' })\n const style = document.createElement('style')\n style.textContent = ctx.style\n const panel = document.createElement('div')\n // 单层玻璃:wui-glass 原样留在面板自身,背景/阴影/backdrop-filter 随面板\n // opacity 与 blur 插值一起过渡(见 overlay-motion.css),无 blur/surface 双层。\n panel.className = ctx.className\n panel.dataset.wuiPresence = 'entering'\n root.append(style, panel)\n\n const trackedNodes: TrackedNodeEntry[] = []\n const contentObserver = new MutationObserver(mutations => {\n // 内部不变量先于消费方回调执行:配对依赖 trackedNodes 尚未被消费方 untrack 的状态。\n returnStrayFrameworkComments(mutations)\n // 框架在打开期物理删除已迁移节点:即时解除追踪,否则关闭恢复时会把已删除\n // 节点复活回宿主 light DOM。面板内部移动(重排 insertBefore)在同一 mutation\n // record 中同时出现在 removed/added,不属于删除,须排除。\n const addedInBatch = new Set(mutations.flatMap(mutation => [...mutation.addedNodes]))\n const removedNodes = mutations\n .flatMap(mutation => [...mutation.removedNodes])\n .filter(node => !addedInBatch.has(node))\n if (removedNodes.length) untrackNodes(removedNodes)\n ctx.onContentChange?.(mutations)\n })\n // 同一 observer 批次内被插入又移除的游离节点没有宿主位可锚,不追踪不迁移\n // (insertBefore 要求 node 是 parent 的子节点);其归宿由框架的删除语义决定。\n const recordEntry = (node: Node): TrackedNodeEntry | null => {\n const parent = node.parentNode\n if (!parent) return null\n const marker = document.createComment(PORTAL_CONTENT_MARKER)\n parent.insertBefore(marker, node)\n const entry: TrackedNodeEntry = { node, marker }\n trackedNodes.push(entry)\n return entry\n }\n const untrackNodes = (nodes: Node[]) => {\n for (const node of nodes) {\n const index = trackedNodes.findIndex(entry => entry.node === node)\n if (index >= 0) {\n trackedNodes[index].marker.remove()\n trackedNodes.splice(index, 1)\n }\n }\n }\n // 面板内节点按宿主骨架序重排:骨架 = 宿主 childNodes 中本 portal 的 marker 序。\n // 只在已跟踪节点之间移动(insertBefore 相对定位),不触碰面板内的组件自有节点\n // (如 autocomplete 的空态占位);末位节点 append,越过面板内注释锚点的情形由\n // restoreContent 的相邻注释随迁兜底。\n const orderTargetBySkeleton = (target: HTMLElement) => {\n const skeleton: Node[] = []\n for (const node of Array.from(ctx.target.childNodes)) {\n if (node instanceof Comment && node.textContent === PORTAL_CONTENT_MARKER) {\n const entry = trackedNodes.find(e => e.marker === node)\n if (entry && entry.node.parentNode === target) skeleton.push(entry.node)\n }\n }\n if (skeleton.length === 0) return\n const current = Array.from(target.childNodes).filter(\n node => skeleton.includes(node) || !(node instanceof Comment)\n )\n if (current.length === skeleton.length && current.every((node, index) => node === skeleton[index])) return\n for (let index = skeleton.length - 1; index >= 0; index--) {\n const node = skeleton[index]\n const next = skeleton[index + 1]\n if (next) {\n if ((node as Element).nextElementSibling !== next) target.insertBefore(node, next)\n } else {\n target.appendChild(node)\n }\n }\n }\n\n /**\n * 打开期不变量:框架删除已迁移节点时,其条件渲染占位注释(v-if 等)会被插进\n * 面板内容区而不是宿主——block 子树 patch 以 hostParentNode(oldVNode.el) 重定\n * container,而旧 el 已在面板(可能是 panel 自身或组件的内容子容器)。\n * 注释是框架后续翻转依赖的 vnode.el:留在面板会随面板销毁,下次翻转把内容插进\n * 已脱离文档的旧面板(内容\"既不在宿主也不在面板\"且无法恢复)。\n * 每个面板 mutation 批次内即时归还:与同批次被框架删除的 tracked 节点按宿主\n * marker 序配对,插到其 marker 位(多注释对多删除为近似配对;无配对时落宿主\n * 骨架末尾兜底)。tracked 子树内的注释是迁移内容自身的嵌套条件渲染,不属游离\n * 锚点,必须留在原位;随整个子树被删的注释已不在面板内,无需救援。\n */\n const returnStrayFrameworkComments = (mutations: MutationRecord[]) => {\n const isInsideTrackedSubtree = (start: Node): boolean => {\n let current: Node | null = start\n while (current && current !== panel) {\n if (trackedNodes.some(entry => entry.node === current)) return true\n current = current.parentNode\n }\n return false\n }\n const addedNodes = new Set(mutations.flatMap(mutation => [...mutation.addedNodes]))\n const strays: Comment[] = []\n for (const mutation of mutations) {\n for (const node of mutation.addedNodes) {\n if (!(node instanceof Comment) || node.textContent === PORTAL_CONTENT_MARKER) continue\n // 同批次移入又移除的游离注释、随删除子树一起消失的注释都不在面板内,跳过\n if (!panel.contains(node)) continue\n if (isInsideTrackedSubtree(mutation.target)) continue\n strays.push(node)\n }\n }\n if (!strays.length) return\n const hostChildren = Array.from(ctx.target.childNodes)\n const removedEntries = trackedNodes\n .filter(\n entry =>\n !addedNodes.has(entry.node) && mutations.some(mutation => [...mutation.removedNodes].includes(entry.node))\n )\n .sort((a, b) => hostChildren.indexOf(a.marker) - hostChildren.indexOf(b.marker))\n for (const [index, comment] of strays.entries()) {\n const marker = removedEntries[Math.min(index, removedEntries.length - 1)]?.marker\n if (marker && marker.parentNode === ctx.target) ctx.target.insertBefore(comment, marker)\n else ctx.target.appendChild(comment)\n }\n }\n\n resolveOverlayContainer(ctx.container, ctx.target).appendChild(host)\n // 面板内容观察是 portal 自身的不变量(游离注释救援 + 删除节点解除追踪),\n // 不依赖消费方是否注册 onContentChange。\n contentObserver.observe(panel, { childList: true, subtree: true })\n\n // 打开期宿主 childList 观察(不含子树,框架 patch 嵌套组件的 light DOM 不属于\n // 本面板内容)。portal 实例的生命周期即打开期,随恢复/移除断开。\n const migrateAddedNodes = ctx.migrateAddedNodes\n const hostObserver = new MutationObserver(mutations => {\n // 框架清除逻辑(如 lit 的 _$clear)按宿主 marker 邻位遍历已迁移内容:元素\n // 物理上在面板,遍历找不到本体,会把 portal marker 当作内容本体移除。\n // marker 被框架摘除即框架删除语义——解除追踪并清掉滞留面板的游离元素;\n // 同批次移入又移回的 marker(框架重排)不属于删除。实时迁移仍以\n // migrateAddedNodes 的回调为准,无该回调的 portal(如 option 域)只做删除清理。\n const addedNodes = new Set(mutations.flatMap(mutation => [...mutation.addedNodes]))\n const strandedNodes = mutations\n .flatMap(mutation => [...mutation.removedNodes])\n .filter(node => node instanceof Comment && node.textContent === PORTAL_CONTENT_MARKER && !addedNodes.has(node))\n .map(marker => trackedNodes.find(entry => entry.marker === marker)?.node)\n .filter((node): node is Node => node !== undefined)\n if (strandedNodes.length) {\n api.removeContent(strandedNodes)\n // remove() 的规范语义即 parentNode.removeChild:宿主/类型层面都以 Node 表达\n for (const node of strandedNodes) node.parentNode?.removeChild(node)\n }\n if (!migrateAddedNodes) return\n const migratable = migrateAddedNodes(mutations.flatMap(mutation => [...mutation.addedNodes]))\n if (migratable.length) api.appendContent(migratable)\n })\n hostObserver.observe(ctx.target, { childList: true })\n\n const api: OverlayPortal = {\n panel,\n\n moveContent(nodes, target = panel) {\n trackedNodes.forEach(entry => entry.marker.remove())\n const entries = nodes.map(recordEntry).filter((entry): entry is TrackedNodeEntry => entry !== null)\n trackedNodes.length = 0\n trackedNodes.push(...entries)\n target.append(...nodes)\n orderTargetBySkeleton(target)\n },\n\n appendContent(nodes, target = panel) {\n for (const node of nodes) {\n untrackNodes([node])\n const entry = recordEntry(node)\n if (!entry) continue\n target.appendChild(entry.node)\n }\n orderTargetBySkeleton(target)\n },\n\n removeContent(nodes) {\n // 框架删除节点时插进面板的占位注释由 contentObserver 的内部不变量\n // (returnStrayFrameworkComments)即时归还宿主,这里只负责解除追踪。\n untrackNodes(nodes)\n },\n\n restoreContent() {\n // 先停掉实时迁移:恢复动作本身会改动宿主 childList,\n // 否则回插的节点会被当成新增内容再次迁入面板形成回环。\n hostObserver.disconnect()\n\n // 节点回到宿主中自己的 marker 位;面板内紧邻其前的框架注释锚点(v-if 占位)\n // 随元素一起迁回——Vue 仍持有这些锚点的 vnode.el 引用,留在面板会随面板销毁,\n // 下次翻转将因 parentNode === null 崩溃。\n for (const entry of trackedNodes) {\n const pending: Comment[] = []\n let previous = entry.node.previousSibling\n while (previous instanceof Comment && previous.textContent !== PORTAL_CONTENT_MARKER) {\n pending.unshift(previous)\n previous = previous.previousSibling\n }\n for (const comment of pending) {\n if (entry.marker.parentNode === ctx.target) ctx.target.insertBefore(comment, entry.marker)\n else ctx.target.appendChild(comment)\n }\n if (entry.marker.parentNode === ctx.target) ctx.target.insertBefore(entry.node, entry.marker)\n else ctx.target.appendChild(entry.node)\n entry.marker.remove()\n }\n trackedNodes.length = 0\n },\n\n remove() {\n hostObserver.disconnect()\n contentObserver.disconnect()\n // 防御:正常 dispose 先 restore(marker 已清),异常路径下避免 marker 泄漏在宿主。\n trackedNodes.forEach(entry => entry.marker.remove())\n trackedNodes.length = 0\n host.remove()\n }\n }\n return api\n })\n\n/**\n * 判断 node 是否是 slot 已分配节点(或其 light DOM 后代),即 node 实际渲染在 shadowRoot 内。\n * slotted 内容的 parentNode 仍停留在 light DOM,因此不能用普通祖先遍历跨过 shadow 边界,\n * 只能通过 slot.assignedNodes() 判断内容被渲染到哪个 slot。\n */\nfunction isAssignedIntoTarget(node: Node, assignedNode: Node, boundary: Node): boolean {\n let current: Node | null = node\n while (current && current !== boundary) {\n if (current === assignedNode) return true\n current = current.parentNode\n }\n return false\n}\n\n/** parentNode 为 null 时跨过 shadow 边界上行到 host(portal 面板等 shadow 子树的 parentNode 链会断裂)。 */\nfunction getParentAcrossShadow(node: Node): Node | null {\n if (node.parentNode) return node.parentNode\n const root = node.getRootNode()\n return root instanceof ShadowRoot ? root.host : null\n}\n\n/**\n * 查找包含 target 且处于打开态的最近原生 <dialog>。\n * 原生 showModal() 会把 dialog 提升到浏览器 top layer,而常规 overlay 容器在普通文档流,\n * 二者无法靠 z-index 叠序——overlay 会被 top layer 的 dialog 遮住。\n * 因此 target 被渲染到某个已打开的原生 dialog 内时,应把 overlay 挂到该 dialog 上,\n * 使其一并进入 top layer(抽屉 / 对话框内的 dropdown、tooltip、context-menu 均适用)。\n * 上行路径必须跨 shadow 边界:target 可能位于 portal 面板(shadow 子树)内,\n * 纯 parentNode 链在该处断裂会误判为 dialog 外,浮层落进 fallback root 被遮挡。\n */\nfunction findEnclosingOpenDialog(target: Element): HTMLDialogElement | null {\n let current: Node | null = target\n while (current && current !== document.documentElement) {\n if (current instanceof HTMLDialogElement && current.open) return current\n\n // 跨过 shadow host:检查其 shadowRoot 内已打开的 dialog 是否渲染了 target。\n if (current instanceof Element && current.shadowRoot) {\n const dialogs = current.shadowRoot.querySelectorAll('dialog')\n for (const dialog of dialogs) {\n if (!(dialog instanceof HTMLDialogElement) || !dialog.open) continue\n const slots = dialog.querySelectorAll('slot')\n for (const slot of slots) {\n for (const assigned of slot.assignedNodes()) {\n if (!(assigned instanceof Node)) continue\n if (isAssignedIntoTarget(target, assigned, current)) return dialog\n }\n }\n }\n }\n\n current = getParentAcrossShadow(current)\n }\n return null\n}\n\nexport function resolveOverlayContainer(\n container: OverlayContainer | undefined,\n target: Element,\n options: OverlayContainerResolutionOptions = {}\n): HTMLElement {\n const explicit = typeof container === 'function' ? container() : container\n if (explicit) return explicit\n\n // target 在已打开的原生 dialog / drawer 内:挂到该 dialog,加入 top layer。\n const enclosingDialog = findEnclosingOpenDialog(target)\n if (enclosingDialog) return enclosingDialog\n\n // 无 target 的调用(如菜单)优先使用 root theme 的 theme-owned overlay root;\n // 有 target 时使用最近 theme 的 theme-owned overlay root。\n const theme = options.preferRootTheme ? findRootTheme() : findNearestTheme(target)\n return theme?.getOverlayRoot() ?? getFallbackOverlayRoot()\n}\n"],"mappings":"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"}
|
|
@@ -1,34 +1,31 @@
|
|
|
1
|
+
import { parseDuration as e } from "../theme/duration.js";
|
|
1
2
|
//#region src/shared/overlay/presence.ts
|
|
2
|
-
var
|
|
3
|
-
function t
|
|
4
|
-
let t =
|
|
5
|
-
return
|
|
3
|
+
var t = 160, n = 80, r = /* @__PURE__ */ new WeakMap();
|
|
4
|
+
function i(n, r = t) {
|
|
5
|
+
let i = getComputedStyle(n), a = i.transitionDuration.split(",").map((t) => e(t) ?? 0), o = i.transitionDelay.split(",").map((t) => e(t) ?? 0);
|
|
6
|
+
return a.reduce((e, t, n) => Math.max(e, t + (o[n] ?? o[0] ?? 0)), 0) || r;
|
|
6
7
|
}
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
function r(t, n = {}) {
|
|
12
|
-
if (e.get(t)?.(), t.hidden = !1, n.isInstant) {
|
|
13
|
-
t.dataset.wuiPresence = "open";
|
|
8
|
+
function a(e, t = {}) {
|
|
9
|
+
if (r.get(e)?.(), e.hidden = !1, t.isInstant) {
|
|
10
|
+
e.dataset.wuiPresence = "open";
|
|
14
11
|
return;
|
|
15
12
|
}
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
e.dataset.wuiPresence = "entering", e.offsetWidth, requestAnimationFrame(() => {
|
|
14
|
+
e.dataset.wuiPresence === "entering" && (e.dataset.wuiPresence = "open");
|
|
18
15
|
});
|
|
19
16
|
}
|
|
20
|
-
function
|
|
21
|
-
return
|
|
22
|
-
let
|
|
23
|
-
|
|
24
|
-
},
|
|
25
|
-
|
|
17
|
+
function o(e) {
|
|
18
|
+
return r.get(e)?.(), e.dataset.wuiPresence = "closing", i(e, 0) === 0 ? (e.hidden = !0, Promise.resolve(!0)) : new Promise((t) => {
|
|
19
|
+
let a = !1, o = (n) => {
|
|
20
|
+
a || (a = !0, e.removeEventListener("transitionend", s), clearTimeout(c), r.delete(e), n && e.dataset.wuiPresence === "closing" && (e.hidden = !0), t(n));
|
|
21
|
+
}, s = (t) => {
|
|
22
|
+
t.target === e && (t.propertyName === "opacity" || t.propertyName === "transform") && o(!0);
|
|
26
23
|
};
|
|
27
|
-
|
|
28
|
-
let
|
|
24
|
+
r.set(e, () => o(!1)), e.addEventListener("transitionend", s);
|
|
25
|
+
let c = setTimeout(() => o(!0), i(e) + n);
|
|
29
26
|
});
|
|
30
27
|
}
|
|
31
28
|
//#endregion
|
|
32
|
-
export {
|
|
29
|
+
export { i as getTransitionDuration, o as hideOverlayPresence, a as showOverlayPresence };
|
|
33
30
|
|
|
34
31
|
//# sourceMappingURL=presence.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presence.js","names":[],"sources":["../../../src/shared/overlay/presence.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"presence.js","names":[],"sources":["../../../src/shared/overlay/presence.ts"],"sourcesContent":["import { parseDuration } from '@/shared/theme/duration'\n\nconst DEFAULT_EXIT_DURATION = 160\nconst EXIT_FALLBACK_BUFFER = 80\n\nconst pendingExits = new WeakMap<HTMLElement, () => void>()\n\ninterface OverlayPresenceOptions {\n isInstant?: boolean\n}\n\n// 读取元素实际 CSS transition 的最长时长,供可覆盖主题 token 的 JS 生命周期使用。\nexport function getTransitionDuration(panel: HTMLElement, fallback = DEFAULT_EXIT_DURATION): number {\n const style = getComputedStyle(panel)\n const durations = style.transitionDuration.split(',').map(value => parseDuration(value) ?? 0)\n const delays = style.transitionDelay.split(',').map(value => parseDuration(value) ?? 0)\n const longest = durations.reduce(\n (max, duration, index) => Math.max(max, duration + (delays[index] ?? delays[0] ?? 0)),\n 0\n )\n\n return longest || fallback\n}\n\n// 以可中断的方式显示浮层,避免快速切换时重启动画。\nexport function showOverlayPresence(panel: HTMLElement, options: OverlayPresenceOptions = {}) {\n pendingExits.get(panel)?.()\n panel.hidden = false\n if (options.isInstant) {\n panel.dataset.wuiPresence = 'open'\n return\n }\n panel.dataset.wuiPresence = 'entering'\n\n // 提交初始状态,让新挂载的面板拥有过渡起点。\n void panel.offsetWidth\n\n requestAnimationFrame(() => {\n if (panel.dataset.wuiPresence === 'entering') panel.dataset.wuiPresence = 'open'\n })\n}\n\n// 播放退出过渡;若在结束前重新显示则返回 false。\nexport function hideOverlayPresence(panel: HTMLElement): Promise<boolean> {\n pendingExits.get(panel)?.()\n panel.dataset.wuiPresence = 'closing'\n\n // jsdom 等没有计算 CSS transition 的环境无需伪造退场等待。\n if (getTransitionDuration(panel, 0) === 0) {\n panel.hidden = true\n return Promise.resolve(true)\n }\n\n return new Promise(resolve => {\n let settled = false\n const finish = (didClose: boolean) => {\n if (settled) return\n settled = true\n panel.removeEventListener('transitionend', onTransitionEnd)\n clearTimeout(timeout)\n pendingExits.delete(panel)\n if (didClose && panel.dataset.wuiPresence === 'closing') panel.hidden = true\n resolve(didClose)\n }\n\n const onTransitionEnd = (event: TransitionEvent) => {\n if (event.target === panel && (event.propertyName === 'opacity' || event.propertyName === 'transform'))\n finish(true)\n }\n\n pendingExits.set(panel, () => finish(false))\n panel.addEventListener('transitionend', onTransitionEnd)\n const timeout = setTimeout(() => finish(true), getTransitionDuration(panel) + EXIT_FALLBACK_BUFFER)\n })\n}\n"],"mappings":";;AAEA,IAAM,IAAwB,KACxB,IAAuB,IAEvB,oBAAe,IAAI,QAAiC;AAO1D,SAAgB,EAAsB,GAAoB,IAAW,GAA+B;CAClG,IAAM,IAAQ,iBAAiB,CAAK,GAC9B,IAAY,EAAM,mBAAmB,MAAM,GAAG,CAAC,CAAC,KAAI,MAAS,EAAc,CAAK,KAAK,CAAC,GACtF,IAAS,EAAM,gBAAgB,MAAM,GAAG,CAAC,CAAC,KAAI,MAAS,EAAc,CAAK,KAAK,CAAC;CAMtF,OALgB,EAAU,QACvB,GAAK,GAAU,MAAU,KAAK,IAAI,GAAK,KAAY,EAAO,MAAU,EAAO,MAAM,EAAE,GACpF,CAGK,KAAW;AACpB;AAGA,SAAgB,EAAoB,GAAoB,IAAkC,CAAC,GAAG;CAG5F,IAFA,EAAa,IAAI,CAAK,CAAC,GAAG,GAC1B,EAAM,SAAS,IACX,EAAQ,WAAW;EACrB,EAAM,QAAQ,cAAc;EAC5B;CACF;CAMA,AALA,EAAM,QAAQ,cAAc,YAG5B,EAAW,aAEX,4BAA4B;EAC1B,AAAI,EAAM,QAAQ,gBAAgB,eAAY,EAAM,QAAQ,cAAc;CAC5E,CAAC;AACH;AAGA,SAAgB,EAAoB,GAAsC;CAUxE,OATA,EAAa,IAAI,CAAK,CAAC,GAAG,GAC1B,EAAM,QAAQ,cAAc,WAGxB,EAAsB,GAAO,CAAC,MAAM,KACtC,EAAM,SAAS,IACR,QAAQ,QAAQ,EAAI,KAGtB,IAAI,SAAQ,MAAW;EAC5B,IAAI,IAAU,IACR,KAAU,MAAsB;GAChC,MACJ,IAAU,IACV,EAAM,oBAAoB,iBAAiB,CAAe,GAC1D,aAAa,CAAO,GACpB,EAAa,OAAO,CAAK,GACrB,KAAY,EAAM,QAAQ,gBAAgB,cAAW,EAAM,SAAS,KACxE,EAAQ,CAAQ;EAClB,GAEM,KAAmB,MAA2B;GAClD,AAAI,EAAM,WAAW,MAAU,EAAM,iBAAiB,aAAa,EAAM,iBAAiB,gBACxF,EAAO,EAAI;EACf;EAGA,AADA,EAAa,IAAI,SAAa,EAAO,EAAK,CAAC,GAC3C,EAAM,iBAAiB,iBAAiB,CAAe;EACvD,IAAM,IAAU,iBAAiB,EAAO,EAAI,GAAG,EAAsB,CAAK,IAAI,CAAoB;CACpG,CAAC;AACH"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/shared/theme/duration.ts
|
|
2
|
+
function e(e) {
|
|
3
|
+
let t = /^\s*([+-]?(?:\d+\.?\d*|\.\d+))(ms|s)\s*$/.exec(e);
|
|
4
|
+
return t ? Number(t[1]) * (t[2] === "s" ? 1e3 : 1) : null;
|
|
5
|
+
}
|
|
6
|
+
//#endregion
|
|
7
|
+
export { e as parseDuration };
|
|
8
|
+
|
|
9
|
+
//# sourceMappingURL=duration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.js","names":[],"sources":["../../../src/shared/theme/duration.ts"],"sourcesContent":["/**\n * 解析单个 CSS `<time>` 字面量(`160ms` / `.16s`)为毫秒;格式不符时返回 `null`。\n * 只认规范写法,不做 `parseFloat` 式的宽容解析——计算样式里的时长/延迟都带单位。\n *\n * 兜底与钳制留各调用方决定:theme 过渡兜底 500、checkbox 描边兜底 160 且负值钳到 0、\n * overlay 生命周期把逗号列表里的解析失败项当 0。\n */\nexport function parseDuration(value: string): number | null {\n const match = /^\\s*([+-]?(?:\\d+\\.?\\d*|\\.\\d+))(ms|s)\\s*$/.exec(value)\n if (!match) return null\n return Number(match[1]) * (match[2] === 's' ? 1000 : 1)\n}\n"],"mappings":";AAOA,SAAgB,EAAc,GAA8B;CAC1D,IAAM,IAAQ,2CAA2C,KAAK,CAAK;CAEnE,OADK,IACE,OAAO,EAAM,EAAE,KAAK,EAAM,OAAO,MAAM,MAAO,KADlC;AAErB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { findNearestTheme as e } from "../overlay/theme-overlay-scope.js";
|
|
2
|
+
//#region src/shared/theme/reduced-motion.ts
|
|
3
|
+
function t(t) {
|
|
4
|
+
let n = e(t);
|
|
5
|
+
if (n) return n.isReducedMotion();
|
|
6
|
+
try {
|
|
7
|
+
return typeof window.matchMedia == "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
8
|
+
} catch {
|
|
9
|
+
return !1;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { t as prefersReducedMotion };
|
|
14
|
+
|
|
15
|
+
//# sourceMappingURL=reduced-motion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reduced-motion.js","names":[],"sources":["../../../src/shared/theme/reduced-motion.ts"],"sourcesContent":["import { findNearestTheme } from '@/shared/overlay/theme-overlay-scope'\n\n/**\n * 元素所在主题范围是否要求减少动效:优先 `web-ui-theme` 的 motion 档位,\n * 无主题范围时回退系统 `prefers-reduced-motion`。jsdom 等无 matchMedia 的环境按完整动效处理。\n *\n * 供「CSS 时长会被主题归零、但 JS 动画要自己决定跑不跑」的调用点共用(如 WAAPI 前置判断)。\n */\nexport function prefersReducedMotion(el: Element): boolean {\n const theme = findNearestTheme(el)\n if (theme) return theme.isReducedMotion()\n try {\n return typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n } catch {\n return false\n }\n}\n"],"mappings":";;AAQA,SAAgB,EAAqB,GAAsB;CACzD,IAAM,IAAQ,EAAiB,CAAE;CACjC,IAAI,GAAO,OAAO,EAAM,gBAAgB;CACxC,IAAI;EACF,OAAO,OAAO,OAAO,cAAe,cAAc,OAAO,WAAW,kCAAkC,CAAC,CAAC;CAC1G,QAAQ;EACN,OAAO;CACT;AACF"}
|
|
@@ -30,7 +30,7 @@ function i(t) {
|
|
|
30
30
|
let i = n(e);
|
|
31
31
|
r(s, i) || (s = i, t.onVisibleAreaChange(i));
|
|
32
32
|
}, l = () => {
|
|
33
|
-
|
|
33
|
+
o && a === void 0 && (a = requestAnimationFrame(c));
|
|
34
34
|
}, u = () => {
|
|
35
35
|
i?.disconnect(), i = void 0, !(!e || typeof ResizeObserver > "u") && (i = new ResizeObserver(l), i.observe(e));
|
|
36
36
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/shared/visible-area/index.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\nimport type { ReactiveController, ReactiveControllerHost } from 'lit'\n\nexport interface VisibleArea {\n readonly top: number\n readonly right: number\n readonly bottom: number\n readonly left: number\n readonly width: number\n readonly height: number\n}\n\nexport interface VisibleAreaTracker {\n setTarget(target: Element | null): void\n refresh(): void\n connect(): void\n disconnect(): void\n}\n\nconst emptyVisibleArea: VisibleArea = {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n width: 0,\n height: 0\n}\n\nfunction getVisibleArea(target: Element | null): VisibleArea {\n if (!target) return emptyVisibleArea\n\n const rect = target.getBoundingClientRect()\n const left = Math.max(0, rect.left)\n const top = Math.max(0, rect.top)\n const right = Math.min(window.innerWidth, rect.right)\n const bottom = Math.min(window.innerHeight, rect.bottom)\n\n if (right <= left || bottom <= top) return emptyVisibleArea\n\n return {\n top,\n right,\n bottom,\n left,\n width: right - left,\n height: bottom - top\n }\n}\n\nfunction visibleAreasEqual(a: VisibleArea | undefined, b: VisibleArea): boolean {\n return (\n a?.top === b.top &&\n a.right === b.right &&\n a.bottom === b.bottom &&\n a.left === b.left &&\n a.width === b.width &&\n a.height === b.height\n )\n}\n\n/**\n * 追踪单个元素与浏览器 viewport 的交集区域。\n *\n * 模块统一持有 ResizeObserver、viewport 监听器和 rAF 合帧;\n * 使用方只需管理 target 并响应可见区域变化。\n */\nexport function defineVisibleAreaTracker(options: { readonly onVisibleAreaChange: (area: VisibleArea) => void }) {\n return definePlugin<VisibleAreaTracker, Record<never, never>>(() => {\n let target: Element | null = null\n let observer: ResizeObserver | undefined\n let frame: number | undefined\n let connected = false\n let previousArea: VisibleArea | undefined\n\n const report = () => {\n frame = undefined\n if (!connected) return\n\n const nextArea = getVisibleArea(target)\n if (visibleAreasEqual(previousArea, nextArea)) return\n\n previousArea = nextArea\n options.onVisibleAreaChange(nextArea)\n }\n\n const refresh = () => {\n if (!connected || frame !== undefined) return\n frame = requestAnimationFrame(report)\n }\n\n const observeTarget = () => {\n observer?.disconnect()\n observer = undefined\n\n if (!target || typeof ResizeObserver === 'undefined') return\n\n observer = new ResizeObserver(refresh)\n observer.observe(target)\n }\n\n return {\n setTarget(nextTarget) {\n if (target === nextTarget) {\n refresh()\n return\n }\n\n target = nextTarget\n previousArea = undefined\n if (!connected) return\n\n observeTarget()\n refresh()\n },\n\n refresh,\n\n connect() {\n if (connected) return\n\n connected = true\n window.addEventListener('scroll', refresh, { passive: true })\n window.addEventListener('resize', refresh, { passive: true })\n observeTarget()\n refresh()\n },\n\n disconnect() {\n if (!connected) return\n\n connected = false\n window.removeEventListener('scroll', refresh)\n window.removeEventListener('resize', refresh)\n observer?.disconnect()\n observer = undefined\n previousArea = undefined\n\n if (frame !== undefined) cancelAnimationFrame(frame)\n frame = undefined\n }\n }\n })\n}\n\n/** 将内部可见区域追踪器接入 Lit host 生命周期。 */\nexport class VisibleAreaController implements ReactiveController {\n constructor(\n host: ReactiveControllerHost,\n private readonly tracker: VisibleAreaTracker\n ) {\n host.addController(this)\n }\n\n hostConnected() {\n this.tracker.connect()\n }\n\n hostDisconnected() {\n this.tracker.disconnect()\n }\n}\n"],"mappings":";;AAmBA,IAAM,IAAgC;CACpC,KAAK;CACL,OAAO;CACP,QAAQ;CACR,MAAM;CACN,OAAO;CACP,QAAQ;AACV;AAEA,SAAS,EAAe,GAAqC;CAC3D,IAAI,CAAC,GAAQ,OAAO;CAEpB,IAAM,IAAO,EAAO,sBAAsB,GACpC,IAAO,KAAK,IAAI,GAAG,EAAK,IAAI,GAC5B,IAAM,KAAK,IAAI,GAAG,EAAK,GAAG,GAC1B,IAAQ,KAAK,IAAI,OAAO,YAAY,EAAK,KAAK,GAC9C,IAAS,KAAK,IAAI,OAAO,aAAa,EAAK,MAAM;CAIvD,OAFI,KAAS,KAAQ,KAAU,IAAY,IAEpC;EACL;EACA;EACA;EACA;EACA,OAAO,IAAQ;EACf,QAAQ,IAAS;CACnB;AACF;AAEA,SAAS,EAAkB,GAA4B,GAAyB;CAC9E,OACE,GAAG,QAAQ,EAAE,OACb,EAAE,UAAU,EAAE,SACd,EAAE,WAAW,EAAE,UACf,EAAE,SAAS,EAAE,QACb,EAAE,UAAU,EAAE,SACd,EAAE,WAAW,EAAE;AAEnB;AAQA,SAAgB,EAAyB,GAAwE;CAC/G,OAAO,QAA6D;EAClE,IAAI,IAAyB,MACzB,GACA,GACA,IAAY,IACZ,GAEE,UAAe;GAEnB,IADA,IAAQ,KAAA,GACJ,CAAC,GAAW;GAEhB,IAAM,IAAW,EAAe,CAAM;GAClC,EAAkB,GAAc,CAAQ,MAE5C,IAAe,GACf,EAAQ,oBAAoB,CAAQ;EACtC,GAEM,UAAgB;GAChB,
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/shared/visible-area/index.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\nimport type { ReactiveController, ReactiveControllerHost } from 'lit'\n\nexport interface VisibleArea {\n readonly top: number\n readonly right: number\n readonly bottom: number\n readonly left: number\n readonly width: number\n readonly height: number\n}\n\nexport interface VisibleAreaTracker {\n setTarget(target: Element | null): void\n refresh(): void\n connect(): void\n disconnect(): void\n}\n\nconst emptyVisibleArea: VisibleArea = {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n width: 0,\n height: 0\n}\n\nfunction getVisibleArea(target: Element | null): VisibleArea {\n if (!target) return emptyVisibleArea\n\n const rect = target.getBoundingClientRect()\n const left = Math.max(0, rect.left)\n const top = Math.max(0, rect.top)\n const right = Math.min(window.innerWidth, rect.right)\n const bottom = Math.min(window.innerHeight, rect.bottom)\n\n if (right <= left || bottom <= top) return emptyVisibleArea\n\n return {\n top,\n right,\n bottom,\n left,\n width: right - left,\n height: bottom - top\n }\n}\n\nfunction visibleAreasEqual(a: VisibleArea | undefined, b: VisibleArea): boolean {\n return (\n a?.top === b.top &&\n a.right === b.right &&\n a.bottom === b.bottom &&\n a.left === b.left &&\n a.width === b.width &&\n a.height === b.height\n )\n}\n\n/**\n * 追踪单个元素与浏览器 viewport 的交集区域。\n *\n * 模块统一持有 ResizeObserver、viewport 监听器和 rAF 合帧;\n * 使用方只需管理 target 并响应可见区域变化。\n */\nexport function defineVisibleAreaTracker(options: { readonly onVisibleAreaChange: (area: VisibleArea) => void }) {\n return definePlugin<VisibleAreaTracker, Record<never, never>>(() => {\n let target: Element | null = null\n let observer: ResizeObserver | undefined\n let frame: number | undefined\n let connected = false\n let previousArea: VisibleArea | undefined\n\n const report = () => {\n frame = undefined\n if (!connected) return\n\n const nextArea = getVisibleArea(target)\n if (visibleAreasEqual(previousArea, nextArea)) return\n\n previousArea = nextArea\n options.onVisibleAreaChange(nextArea)\n }\n\n const refresh = () => {\n if (!connected || frame !== undefined) return\n frame = requestAnimationFrame(report)\n }\n\n const observeTarget = () => {\n observer?.disconnect()\n observer = undefined\n\n if (!target || typeof ResizeObserver === 'undefined') return\n\n observer = new ResizeObserver(refresh)\n observer.observe(target)\n }\n\n return {\n setTarget(nextTarget) {\n if (target === nextTarget) {\n refresh()\n return\n }\n\n target = nextTarget\n previousArea = undefined\n if (!connected) return\n\n observeTarget()\n refresh()\n },\n\n refresh,\n\n connect() {\n if (connected) return\n\n connected = true\n window.addEventListener('scroll', refresh, { passive: true })\n window.addEventListener('resize', refresh, { passive: true })\n observeTarget()\n refresh()\n },\n\n disconnect() {\n if (!connected) return\n\n connected = false\n window.removeEventListener('scroll', refresh)\n window.removeEventListener('resize', refresh)\n observer?.disconnect()\n observer = undefined\n previousArea = undefined\n\n if (frame !== undefined) cancelAnimationFrame(frame)\n frame = undefined\n }\n }\n })\n}\n\n/** 将内部可见区域追踪器接入 Lit host 生命周期。 */\nexport class VisibleAreaController implements ReactiveController {\n constructor(\n host: ReactiveControllerHost,\n private readonly tracker: VisibleAreaTracker\n ) {\n host.addController(this)\n }\n\n hostConnected() {\n this.tracker.connect()\n }\n\n hostDisconnected() {\n this.tracker.disconnect()\n }\n}\n"],"mappings":";;AAmBA,IAAM,IAAgC;CACpC,KAAK;CACL,OAAO;CACP,QAAQ;CACR,MAAM;CACN,OAAO;CACP,QAAQ;AACV;AAEA,SAAS,EAAe,GAAqC;CAC3D,IAAI,CAAC,GAAQ,OAAO;CAEpB,IAAM,IAAO,EAAO,sBAAsB,GACpC,IAAO,KAAK,IAAI,GAAG,EAAK,IAAI,GAC5B,IAAM,KAAK,IAAI,GAAG,EAAK,GAAG,GAC1B,IAAQ,KAAK,IAAI,OAAO,YAAY,EAAK,KAAK,GAC9C,IAAS,KAAK,IAAI,OAAO,aAAa,EAAK,MAAM;CAIvD,OAFI,KAAS,KAAQ,KAAU,IAAY,IAEpC;EACL;EACA;EACA;EACA;EACA,OAAO,IAAQ;EACf,QAAQ,IAAS;CACnB;AACF;AAEA,SAAS,EAAkB,GAA4B,GAAyB;CAC9E,OACE,GAAG,QAAQ,EAAE,OACb,EAAE,UAAU,EAAE,SACd,EAAE,WAAW,EAAE,UACf,EAAE,SAAS,EAAE,QACb,EAAE,UAAU,EAAE,SACd,EAAE,WAAW,EAAE;AAEnB;AAQA,SAAgB,EAAyB,GAAwE;CAC/G,OAAO,QAA6D;EAClE,IAAI,IAAyB,MACzB,GACA,GACA,IAAY,IACZ,GAEE,UAAe;GAEnB,IADA,IAAQ,KAAA,GACJ,CAAC,GAAW;GAEhB,IAAM,IAAW,EAAe,CAAM;GAClC,EAAkB,GAAc,CAAQ,MAE5C,IAAe,GACf,EAAQ,oBAAoB,CAAQ;EACtC,GAEM,UAAgB;GAChB,AAAC,KAAa,MAAU,KAAA,MAC5B,IAAQ,sBAAsB,CAAM;EACtC,GAEM,UAAsB;GAC1B,GAAU,WAAW,GACrB,IAAW,KAAA,GAEP,GAAC,KAAU,OAAO,iBAAmB,SAEzC,IAAW,IAAI,eAAe,CAAO,GACrC,EAAS,QAAQ,CAAM;EACzB;EAEA,OAAO;GACL,UAAU,GAAY;IACpB,IAAI,MAAW,GAAY;KACzB,EAAQ;KACR;IACF;IAEA,IAAS,GACT,IAAe,KAAA,GACV,MAEL,EAAc,GACd,EAAQ;GACV;GAEA;GAEA,UAAU;IACJ,MAEJ,IAAY,IACZ,OAAO,iBAAiB,UAAU,GAAS,EAAE,SAAS,GAAK,CAAC,GAC5D,OAAO,iBAAiB,UAAU,GAAS,EAAE,SAAS,GAAK,CAAC,GAC5D,EAAc,GACd,EAAQ;GACV;GAEA,aAAa;IACN,MAEL,IAAY,IACZ,OAAO,oBAAoB,UAAU,CAAO,GAC5C,OAAO,oBAAoB,UAAU,CAAO,GAC5C,GAAU,WAAW,GACrB,IAAW,KAAA,GACX,IAAe,KAAA,GAEX,MAAU,KAAA,KAAW,qBAAqB,CAAK,GACnD,IAAQ,KAAA;GACV;EACF;CACF,CAAC;AACH;AAGA,IAAa,IAAb,MAAiE;CAC/D,YACE,GACA,GACA;EACA,AAFiB,KAAA,UAAA,GAEjB,EAAK,cAAc,IAAI;CACzB;CAEA,gBAAgB;EACd,KAAK,QAAQ,QAAQ;CACvB;CAEA,mBAAmB;EACjB,KAAK,QAAQ,WAAW;CAC1B;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@greypan/web-ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Lit-based web components for React, Vue, and vanilla JS",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"react-dom": "^19.2.7",
|
|
59
59
|
"vite-plugin-dts": "^5.0.3",
|
|
60
60
|
"vue": "^3.5.39",
|
|
61
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@0.3.
|
|
62
|
-
"vite-plus": "0.3.
|
|
61
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.3.2",
|
|
62
|
+
"vite-plus": "0.3.2"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
65
|
"@types/react": ">=19",
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
//#region src/shared/overlay/composition.ts
|
|
2
|
-
var e = /* @__PURE__ */ new WeakMap(), t = /* @__PURE__ */ new WeakMap(), n = /* @__PURE__ */ new WeakMap();
|
|
3
|
-
function r(t) {
|
|
4
|
-
let n = e.get(t);
|
|
5
|
-
return n || (n = { children: /* @__PURE__ */ new Set() }, e.set(t, n)), n;
|
|
6
|
-
}
|
|
7
|
-
function i(e) {
|
|
8
|
-
return e instanceof Element && e.assignedSlot ? e.assignedSlot : e.parentNode ? e.parentNode : e instanceof ShadowRoot ? e.host : null;
|
|
9
|
-
}
|
|
10
|
-
function a(e, t) {
|
|
11
|
-
let n = t;
|
|
12
|
-
for (; n;) {
|
|
13
|
-
if (n === e || e.contains(n)) return !0;
|
|
14
|
-
n = i(n);
|
|
15
|
-
}
|
|
16
|
-
return !1;
|
|
17
|
-
}
|
|
18
|
-
function o(e) {
|
|
19
|
-
let r = t.get(e);
|
|
20
|
-
r && (n.get(r)?.delete(e), n.get(r)?.size === 0 && n.delete(r), t.delete(e));
|
|
21
|
-
}
|
|
22
|
-
var s = {
|
|
23
|
-
registerPanel(e, i) {
|
|
24
|
-
if (e === i || (o(e), r(e), !i)) return;
|
|
25
|
-
r(i);
|
|
26
|
-
let a = n.get(i) ?? /* @__PURE__ */ new Set();
|
|
27
|
-
a.add(e), n.set(i, a), t.set(e, i);
|
|
28
|
-
},
|
|
29
|
-
registerPanelFromAncestry(t, n) {
|
|
30
|
-
let r = n === t ? i(n) : n;
|
|
31
|
-
for (; r;) {
|
|
32
|
-
if (r instanceof HTMLElement && r !== t && e.has(r)) return this.registerPanel(t, r), r;
|
|
33
|
-
r = i(r);
|
|
34
|
-
}
|
|
35
|
-
this.registerPanel(t), o(t);
|
|
36
|
-
},
|
|
37
|
-
unregisterPanel(t) {
|
|
38
|
-
Array.from(n.get(t) ?? []).forEach((e) => this.unregisterPanel(e)), o(t), n.delete(t), e.delete(t);
|
|
39
|
-
},
|
|
40
|
-
contains(e, t) {
|
|
41
|
-
if (a(e, t)) return !0;
|
|
42
|
-
for (let r of n.get(e) ?? []) if (this.contains(r, t)) return !0;
|
|
43
|
-
return !1;
|
|
44
|
-
},
|
|
45
|
-
containsEvent(e, t) {
|
|
46
|
-
return t.composedPath().some((t) => t instanceof Node && this.contains(e, t));
|
|
47
|
-
},
|
|
48
|
-
hasFocusWithin(e) {
|
|
49
|
-
if (e.matches(":focus-within")) return !0;
|
|
50
|
-
for (let t of n.get(e) ?? []) if (this.hasFocusWithin(t)) return !0;
|
|
51
|
-
return !1;
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
//#endregion
|
|
55
|
-
export { s as overlayComposition };
|
|
56
|
-
|
|
57
|
-
//# sourceMappingURL=composition.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"composition.js","names":[],"sources":["../../../src/shared/overlay/composition.ts"],"sourcesContent":["/**\n * Overlay 逻辑祖先关系注册表。\n *\n * Portal 会让 DOM 祖先链与用户感知的组合关系分离:submenu、嵌套 popover 或\n * panel 内的子 overlay 通常被迁到独立 overlay root。交互所有权必须以这里登记\n * 的逻辑树判定,而不是继续在各组件内复制 contains()。\n */\n\ninterface OverlayPanelEntry {\n children: Set<HTMLElement>\n}\n\nconst entries = new WeakMap<HTMLElement, OverlayPanelEntry>()\nconst parents = new WeakMap<HTMLElement, HTMLElement>()\nconst children = new WeakMap<HTMLElement, Set<HTMLElement>>()\n\nfunction entryFor(panel: HTMLElement): OverlayPanelEntry {\n let entry = entries.get(panel)\n if (!entry) {\n entry = { children: new Set() }\n entries.set(panel, entry)\n }\n return entry\n}\n\n/** 判断 node 是否在 root 的物理或 shadow DOM 子树内。 */\nfunction nextCompositionNode(node: Node): Node | null {\n if (node instanceof Element && node.assignedSlot) return node.assignedSlot\n if (node.parentNode) return node.parentNode\n if (node instanceof ShadowRoot) return node.host\n return null\n}\n\nfunction nodeIsInside(root: HTMLElement, node: Node): boolean {\n let current: Node | null = node\n while (current) {\n if (current === root || root.contains(current)) return true\n current = nextCompositionNode(current)\n }\n return false\n}\n\nfunction removeFromParent(panel: HTMLElement): void {\n const parent = parents.get(panel)\n if (!parent) return\n children.get(parent)?.delete(panel)\n if (children.get(parent)?.size === 0) children.delete(parent)\n parents.delete(panel)\n}\n\nexport const overlayComposition = {\n /**\n * 登记 panel;parentPanel 存在时建立逻辑父子关系。重复登记会迁移到新父级,\n * 但不影响 panel 已有后代,供同 panel 重定位/恢复场景复用。\n */\n registerPanel(panel: HTMLElement, parentPanel?: HTMLElement): void {\n if (panel === parentPanel) return\n removeFromParent(panel)\n entryFor(panel)\n if (!parentPanel) return\n\n entryFor(parentPanel)\n const siblings = children.get(parentPanel) ?? new Set<HTMLElement>()\n siblings.add(panel)\n children.set(parentPanel, siblings)\n parents.set(panel, parentPanel)\n },\n\n /**\n * 通过 owner 的物理祖先链寻找已登记 overlay panel。panel 可以是本地 shadow\n * panel,也可以是 portal panel;portal 场景传入 portal target 即可找到原组合父级。\n */\n registerPanelFromAncestry(panel: HTMLElement, owner: Node): HTMLElement | undefined {\n let current: Node | null = owner === panel ? nextCompositionNode(owner) : owner\n while (current) {\n if (current instanceof HTMLElement && current !== panel && entries.has(current)) {\n this.registerPanel(panel, current)\n return current\n }\n current = nextCompositionNode(current)\n }\n this.registerPanel(panel)\n removeFromParent(panel)\n return undefined\n },\n\n /** 注销 panel 及其全部逻辑后代,避免关闭后留下 stale ancestry。 */\n unregisterPanel(panel: HTMLElement): void {\n const descendants = Array.from(children.get(panel) ?? [])\n descendants.forEach(descendant => this.unregisterPanel(descendant))\n removeFromParent(panel)\n children.delete(panel)\n entries.delete(panel)\n },\n\n /** target 是否属于 panel 自身或任何已登记逻辑后代 panel。 */\n contains(panel: HTMLElement, target: Node): boolean {\n if (nodeIsInside(panel, target)) return true\n for (const child of children.get(panel) ?? []) {\n if (this.contains(child, target)) return true\n }\n return false\n },\n\n /** event 的完整 composed path 是否落入 panel 的逻辑组合子树。 */\n containsEvent(panel: HTMLElement, event: Event): boolean {\n return event.composedPath().some(node => node instanceof Node && this.contains(panel, node))\n },\n\n /** 焦点是否在 panel 或其逻辑后代 panel 内。 */\n hasFocusWithin(panel: HTMLElement): boolean {\n if (panel.matches(':focus-within')) return true\n for (const child of children.get(panel) ?? []) {\n if (this.hasFocusWithin(child)) return true\n }\n return false\n }\n}\n"],"mappings":";AAYA,IAAM,oBAAU,IAAI,QAAwC,GACtD,oBAAU,IAAI,QAAkC,GAChD,oBAAW,IAAI,QAAuC;AAE5D,SAAS,EAAS,GAAuC;CACvD,IAAI,IAAQ,EAAQ,IAAI,CAAK;CAK7B,OAJK,MACH,IAAQ,EAAE,0BAAU,IAAI,IAAI,EAAE,GAC9B,EAAQ,IAAI,GAAO,CAAK,IAEnB;AACT;AAGA,SAAS,EAAoB,GAAyB;CAIpD,OAHI,aAAgB,WAAW,EAAK,eAAqB,EAAK,eAC1D,EAAK,aAAmB,EAAK,aAC7B,aAAgB,aAAmB,EAAK,OACrC;AACT;AAEA,SAAS,EAAa,GAAmB,GAAqB;CAC5D,IAAI,IAAuB;CAC3B,OAAO,IAAS;EACd,IAAI,MAAY,KAAQ,EAAK,SAAS,CAAO,GAAG,OAAO;EACvD,IAAU,EAAoB,CAAO;CACvC;CACA,OAAO;AACT;AAEA,SAAS,EAAiB,GAA0B;CAClD,IAAM,IAAS,EAAQ,IAAI,CAAK;CAC3B,MACL,EAAS,IAAI,CAAM,CAAC,EAAE,OAAO,CAAK,GAC9B,EAAS,IAAI,CAAM,CAAC,EAAE,SAAS,KAAG,EAAS,OAAO,CAAM,GAC5D,EAAQ,OAAO,CAAK;AACtB;AAEA,IAAa,IAAqB;CAKhC,cAAc,GAAoB,GAAiC;EAIjE,IAHI,MAAU,MACd,EAAiB,CAAK,GACtB,EAAS,CAAK,GACV,CAAC,IAAa;EAElB,EAAS,CAAW;EACpB,IAAM,IAAW,EAAS,IAAI,CAAW,qBAAK,IAAI,IAAiB;EAGnE,AAFA,EAAS,IAAI,CAAK,GAClB,EAAS,IAAI,GAAa,CAAQ,GAClC,EAAQ,IAAI,GAAO,CAAW;CAChC;CAMA,0BAA0B,GAAoB,GAAsC;EAClF,IAAI,IAAuB,MAAU,IAAQ,EAAoB,CAAK,IAAI;EAC1E,OAAO,IAAS;GACd,IAAI,aAAmB,eAAe,MAAY,KAAS,EAAQ,IAAI,CAAO,GAE5E,OADA,KAAK,cAAc,GAAO,CAAO,GAC1B;GAET,IAAU,EAAoB,CAAO;EACvC;EAEA,AADA,KAAK,cAAc,CAAK,GACxB,EAAiB,CAAK;CAExB;CAGA,gBAAgB,GAA0B;EAKxC,AAHA,MAD0B,KAAK,EAAS,IAAI,CAAK,KAAK,CAAC,CACvD,CAAA,CAAY,SAAQ,MAAc,KAAK,gBAAgB,CAAU,CAAC,GAClE,EAAiB,CAAK,GACtB,EAAS,OAAO,CAAK,GACrB,EAAQ,OAAO,CAAK;CACtB;CAGA,SAAS,GAAoB,GAAuB;EAClD,IAAI,EAAa,GAAO,CAAM,GAAG,OAAO;EACxC,KAAK,IAAM,KAAS,EAAS,IAAI,CAAK,KAAK,CAAC,GAC1C,IAAI,KAAK,SAAS,GAAO,CAAM,GAAG,OAAO;EAE3C,OAAO;CACT;CAGA,cAAc,GAAoB,GAAuB;EACvD,OAAO,EAAM,aAAa,CAAC,CAAC,MAAK,MAAQ,aAAgB,QAAQ,KAAK,SAAS,GAAO,CAAI,CAAC;CAC7F;CAGA,eAAe,GAA6B;EAC1C,IAAI,EAAM,QAAQ,eAAe,GAAG,OAAO;EAC3C,KAAK,IAAM,KAAS,EAAS,IAAI,CAAK,KAAK,CAAC,GAC1C,IAAI,KAAK,eAAe,CAAK,GAAG,OAAO;EAEzC,OAAO;CACT;AACF"}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { definePlugin as e } from "@greypan/js-kit";
|
|
2
|
-
//#region src/shared/overlay/lifecycle.ts
|
|
3
|
-
var t = () => e((e) => {
|
|
4
|
-
let t = 0, n = !1, r = /* @__PURE__ */ new Set(), i = (e) => {
|
|
5
|
-
cancelAnimationFrame(e.id), r.delete(e);
|
|
6
|
-
}, a = () => {
|
|
7
|
-
if (!n) {
|
|
8
|
-
t += 1;
|
|
9
|
-
for (let e of Array.from(r)) i(e);
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
return {
|
|
13
|
-
getGeneration() {
|
|
14
|
-
return t;
|
|
15
|
-
},
|
|
16
|
-
invalidate: a,
|
|
17
|
-
scheduleFrame(a, o = {}) {
|
|
18
|
-
if (n) return { cancel() {} };
|
|
19
|
-
let s = t, c = {
|
|
20
|
-
generation: s,
|
|
21
|
-
id: requestAnimationFrame(() => {
|
|
22
|
-
r.delete(c), !(n || s !== t) && e.isConnected() && (o.expectedOpen === void 0 || e.isOpen() === o.expectedOpen) && a(s);
|
|
23
|
-
}),
|
|
24
|
-
cancel() {
|
|
25
|
-
i(c);
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
return r.add(c), c;
|
|
29
|
-
},
|
|
30
|
-
dispose() {
|
|
31
|
-
n || (n = !0, a());
|
|
32
|
-
},
|
|
33
|
-
resume() {
|
|
34
|
-
n && (n = !1, t += 1);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
});
|
|
38
|
-
//#endregion
|
|
39
|
-
export { t as defineOverlayLifecycle };
|
|
40
|
-
|
|
41
|
-
//# sourceMappingURL=lifecycle.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.js","names":[],"sources":["../../../src/shared/overlay/lifecycle.ts"],"sourcesContent":["import { definePlugin } from '@greypan/js-kit'\n\nexport interface OverlayLifecycleHost {\n isConnected(): boolean\n isOpen(): boolean\n}\n\nexport interface OverlayFrameOptions {\n /**\n * 显式要求回调执行时 open 状态必须等于该值;未传入时只校验连接状态。\n */\n expectedOpen?: boolean\n}\n\nexport interface OverlayFrameHandle {\n cancel(): void\n}\n\nexport interface OverlayLifecycle {\n /** generation 只能通过方法读取:definePlugin.make 展开对象时会立即消费 getter。 */\n getGeneration(): number\n /** 结束当前事务:取消该事务内尚未执行的 frame,并让旧 generation 回调失效。 */\n invalidate(): void\n scheduleFrame(callback: (generation: number) => void, options?: OverlayFrameOptions): OverlayFrameHandle\n dispose(): void\n /** temporary disconnect 后恢复调度;generation 继续单调递增,旧事务保持失效。 */\n resume(): void\n}\n\n/**\n * Overlay 组件共享的帧级事务调度。portal / presence / positioning 可能跨多帧,\n * 同帧 controlled open/close、disconnect 或 reconfigure 都必须使旧事务失效,\n * 而不是让每个组件各自散落 open / isConnected 复查。\n */\nexport const defineOverlayLifecycle = () =>\n definePlugin<OverlayLifecycle, OverlayLifecycleHost>(host => {\n let generation = 0\n let disposed = false\n const frames = new Set<OverlayFrameHandle & { id: number; generation: number }>()\n\n const cancelFrame = (frame: OverlayFrameHandle & { id: number; generation: number }) => {\n cancelAnimationFrame(frame.id)\n frames.delete(frame)\n }\n\n const invalidateCurrent = () => {\n if (disposed) return\n generation += 1\n for (const frame of Array.from(frames)) cancelFrame(frame)\n }\n\n return {\n getGeneration() {\n return generation\n },\n\n invalidate: invalidateCurrent,\n\n scheduleFrame(callback, options = {}) {\n if (disposed) return { cancel() {} }\n\n const scheduledGeneration = generation\n const frame = {\n generation: scheduledGeneration,\n id: requestAnimationFrame(() => {\n frames.delete(frame)\n if (disposed || scheduledGeneration !== generation) return\n if (!host.isConnected()) return\n if (options.expectedOpen !== undefined && host.isOpen() !== options.expectedOpen) return\n callback(scheduledGeneration)\n }),\n cancel() {\n cancelFrame(frame)\n }\n }\n frames.add(frame)\n return frame\n },\n\n dispose() {\n if (disposed) return\n disposed = true\n invalidateCurrent()\n },\n\n resume() {\n if (!disposed) return\n disposed = false\n generation += 1\n }\n }\n })\n"],"mappings":";;AAkCA,IAAa,UACX,GAAqD,MAAQ;CAC3D,IAAI,IAAa,GACb,IAAW,IACT,oBAAS,IAAI,IAA6D,GAE1E,KAAe,MAAmE;EAEtF,AADA,qBAAqB,EAAM,EAAE,GAC7B,EAAO,OAAO,CAAK;CACrB,GAEM,UAA0B;EAC1B,QACJ;QAAc;GACd,KAAK,IAAM,KAAS,MAAM,KAAK,CAAM,GAAG,EAAY,CAAK;EAD3C;CAEhB;CAEA,OAAO;EACL,gBAAgB;GACd,OAAO;EACT;EAEA,YAAY;EAEZ,cAAc,GAAU,IAAU,CAAC,GAAG;GACpC,IAAI,GAAU,OAAO,EAAE,SAAS,CAAC,EAAE;GAEnC,IAAM,IAAsB,GACtB,IAAQ;IACZ,YAAY;IACZ,IAAI,4BAA4B;KAC9B,EAAO,OAAO,CAAK,GACf,OAAY,MAAwB,MACnC,EAAK,YAAY,MAClB,EAAQ,iBAAiB,KAAA,KAAa,EAAK,OAAO,MAAM,EAAQ,iBACpE,EAAS,CAAmB;IAC9B,CAAC;IACD,SAAS;KACP,EAAY,CAAK;IACnB;GACF;GAEA,OADA,EAAO,IAAI,CAAK,GACT;EACT;EAEA,UAAU;GACJ,MACJ,IAAW,IACX,EAAkB;EACpB;EAEA,SAAS;GACF,MACL,IAAW,IACX,KAAc;EAChB;CACF;AACF,CAAC"}
|
package/dist/vite.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|