diff --git a/packages/core/package.json b/packages/core/package.json
index 9eb91442..82712a10 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -79,6 +79,7 @@
"dompurify": "catalog:frontend",
"fuse.js": "catalog:frontend",
"human-id": "catalog:inlined",
+ "iframe-pane": "catalog:frontend",
"tsdown": "catalog:build",
"typescript": "catalog:devtools",
"unplugin-vue": "catalog:build",
@@ -103,6 +104,7 @@
"fast-wrap-ansi": "0.2.0",
"fuse.js": "7.3.0",
"get-port-please": "3.2.0",
+ "iframe-pane": "1.1.0",
"sisteransi": "1.0.5",
"zod": "4.3.6"
}
diff --git a/packages/core/src/client/webcomponents/components/dock/DockEdge.vue b/packages/core/src/client/webcomponents/components/dock/DockEdge.vue
index 88bcdbbd..7ffacb80 100644
--- a/packages/core/src/client/webcomponents/components/dock/DockEdge.vue
+++ b/packages/core/src/client/webcomponents/components/dock/DockEdge.vue
@@ -1,10 +1,10 @@
diff --git a/packages/core/src/client/webcomponents/components/views/ViewEntry.vue b/packages/core/src/client/webcomponents/components/views/ViewEntry.vue
index a85d6e5b..f732ba4e 100644
--- a/packages/core/src/client/webcomponents/components/views/ViewEntry.vue
+++ b/packages/core/src/client/webcomponents/components/views/ViewEntry.vue
@@ -1,8 +1,8 @@
diff --git a/packages/core/src/client/webcomponents/index.ts b/packages/core/src/client/webcomponents/index.ts
index e9e765d0..da4da964 100644
--- a/packages/core/src/client/webcomponents/index.ts
+++ b/packages/core/src/client/webcomponents/index.ts
@@ -1,4 +1,3 @@
export * from './components/DockEmbedded'
export * from './components/DockStandalone'
export * from './state/docks'
-export * from './utils/PersistedDomViewsManager'
diff --git a/packages/core/src/client/webcomponents/utils/PersistedDomViewsManager.ts b/packages/core/src/client/webcomponents/utils/PersistedDomViewsManager.ts
deleted file mode 100644
index 7e5b4154..00000000
--- a/packages/core/src/client/webcomponents/utils/PersistedDomViewsManager.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import type { ShallowRef } from 'vue'
-
-export interface TagNameToElementMap {
- iframe: HTMLIFrameElement
- div: HTMLDivElement
-}
-
-export class PersistedDomViewsManager {
- readonly holders: Record> = {}
-
- constructor(
- public container: Readonly>,
- ) {
- }
-
- getHolder(id: string, _type: T): PersistedDomHolder | undefined {
- return this.holders[id] as PersistedDomHolder
- }
-
- getOrCreateHolder(id: string, type: T): PersistedDomHolder {
- if (!this.container.value) {
- throw new Error('[VITE DEVTOOLS] PersistedDomViewsManager: container is not set')
- }
- let holder: PersistedDomHolder
- if (!this.holders[id]) {
- const el = document.createElement(type) as TagNameToElementMap[T]
- this.holders[id] = new PersistedDomHolder(id, el)
- this.container.value.appendChild(el)
- holder = this.holders[id]
- }
- else {
- holder = this.holders[id]
- }
- return holder as PersistedDomHolder
- }
-
- removeHolder(id: string) {
- const holder = this.holders[id]
- if (!holder)
- return false
- holder.unmount()
- holder.element.remove()
- delete this.holders[id]
- return true
- }
-}
-
-export class PersistedDomHolder {
- readonly element: ElementType
- readonly id: string
- anchor?: Element
-
- _cleanups: (() => void)[] = []
-
- constructor(id: string, iframe: ElementType) {
- this.id = id
- this.element = iframe
- }
-
- cleanup() {
- this._cleanups.forEach(cleanup => cleanup())
- this._cleanups = []
- }
-
- mount(parent: Element) {
- if (this.anchor === parent) {
- this.show()
- return
- }
-
- this.cleanup()
- this.anchor = parent
-
- const update = () => this.update()
- window.addEventListener('resize', update)
- this._cleanups.push(() => window.removeEventListener('resize', update))
- this.show()
- }
-
- hide() {
- this.element.style.display = 'none'
- }
-
- show() {
- this.element.style.display = 'block'
- this.update()
- }
-
- update() {
- if (!this.anchor)
- return
- const rect = this.anchor.getBoundingClientRect()
- this.element.style.position = 'absolute'
- this.element.style.width = `${rect.width}px`
- this.element.style.height = `${rect.height}px`
- }
-
- unmount() {
- this.cleanup()
- this.hide()
- this.anchor = undefined
- }
-}
diff --git a/packages/core/src/client/webcomponents/utils/useIframePanes.ts b/packages/core/src/client/webcomponents/utils/useIframePanes.ts
new file mode 100644
index 00000000..5e861cce
--- /dev/null
+++ b/packages/core/src/client/webcomponents/utils/useIframePanes.ts
@@ -0,0 +1,62 @@
+import type { DocksPanelContext } from '@vitejs/devtools-kit/client'
+import type { IframePanes } from 'iframe-pane'
+import type { ShallowRef } from 'vue'
+import { createIframePanes } from 'iframe-pane'
+import { markRaw, onScopeDispose, shallowRef, watch } from 'vue'
+
+/**
+ * Own an {@link IframePanes} manager for a dock shell, parking its persistent
+ * iframe/element panes in the given overlay container.
+ *
+ * The manager is created lazily once the container element exists (it is a
+ * template ref that starts empty), and torn down when the owning scope is
+ * disposed. When a `panel` context is supplied, its geometry is watched so
+ * panes stay glued to their targets during moves the shared `ResizeObserver`
+ * can't see, and pointer events on every pane are locked while the panel is
+ * dragged or resized so iframes don't swallow the interaction.
+ */
+export function useIframePanes(
+ container: Readonly>,
+ panel?: DocksPanelContext,
+): Readonly> {
+ const panes = shallowRef()
+
+ const stop = watch(
+ container,
+ (el) => {
+ if (el && !panes.value) {
+ panes.value = markRaw(createIframePanes({ container: el }))
+ stop()
+ }
+ },
+ { immediate: true, flush: 'post' },
+ )
+
+ if (panel) {
+ // A panel move changes its box position without resizing the target, so the
+ // shared ResizeObserver/scroll listeners never fire — re-sync explicitly.
+ watch(() => panel, () => panes.value?.updateAll(), { deep: true })
+
+ // Iframes capture pointer events, which breaks dragging/resizing happening
+ // above them — lock them for the duration of the interaction.
+ let release: (() => void) | undefined
+ watch(
+ () => panel.isDragging || panel.isResizing,
+ (locked) => {
+ if (locked) {
+ release ??= panes.value?.lockPointerEvents()
+ }
+ else {
+ release?.()
+ release = undefined
+ }
+ },
+ { flush: 'sync' },
+ )
+ onScopeDispose(() => release?.())
+ }
+
+ onScopeDispose(() => panes.value?.dispose())
+
+ return panes
+}
diff --git a/packages/core/tsdown.config.ts b/packages/core/tsdown.config.ts
index 833fe36c..32a81cb4 100644
--- a/packages/core/tsdown.config.ts
+++ b/packages/core/tsdown.config.ts
@@ -39,6 +39,7 @@ const deps = {
'fuse.js',
'get-port-please',
'human-id',
+ 'iframe-pane',
'sisteransi',
'vue',
'zod',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 318b0772..82f4928a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -229,6 +229,9 @@ catalogs:
idb-keyval:
specifier: ^6.2.6
version: 6.2.6
+ iframe-pane:
+ specifier: ^1.1.0
+ version: 1.1.0
modern-monaco:
specifier: ^0.4.2
version: 0.4.2
@@ -773,6 +776,9 @@ importers:
human-id:
specifier: catalog:inlined
version: 4.2.0
+ iframe-pane:
+ specifier: catalog:frontend
+ version: 1.1.0
tsdown:
specifier: catalog:build
version: 0.22.3(@vitejs/devtools@0.3.4)(publint@0.3.21)(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))(vue-tsc@3.3.6(typescript@6.0.3))
@@ -5757,6 +5763,9 @@ packages:
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+ iframe-pane@1.1.0:
+ resolution: {integrity: sha512-UV+PuLDiXZ05vMSNdssR/MRb6qIL4YM7Fxbx1OjZdvW2cfKyukKTx9Xn0YgfjlrFocOuFhk+m+ZPH3ZknmIE/w==}
+
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -13728,6 +13737,8 @@ snapshots:
ieee754@1.2.1: {}
+ iframe-pane@1.1.0: {}
+
ignore@5.3.2: {}
ignore@7.0.5: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 94e66e0e..ce40dd2c 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -126,6 +126,7 @@ catalogs:
floating-vue: ^5.2.2
fuse.js: ^7.4.2
idb-keyval: ^6.2.6
+ iframe-pane: ^1.1.0
modern-monaco: ^0.4.2
nanovis: ^1.0.0
react: ^19.2.7
diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts
index c760938f..8e82716b 100644
--- a/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts
+++ b/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts
@@ -1,37 +1,6 @@
/**
* Generated by tsnapi — public API snapshot of `@vitejs/devtools/client/webcomponents`
*/
-// #region Interfaces
-export interface TagNameToElementMap {
- iframe: HTMLIFrameElement;
- div: HTMLDivElement;
-}
-// #endregion
-
-// #region Classes
-export declare class PersistedDomHolder {
- readonly element: ElementType;
- readonly id: string;
- anchor?: Element;
- _cleanups: (() => void)[];
- constructor(_: string, _: ElementType);
- cleanup(): void;
- mount(_: Element): void;
- hide(): void;
- show(): void;
- update(): void;
- unmount(): void;
-}
-export declare class PersistedDomViewsManager {
- container: Readonly>;
- readonly holders: Record>;
- constructor(_: Readonly>);
- getHolder(_: string, _: T): PersistedDomHolder | undefined;
- getOrCreateHolder(_: string, _: T): PersistedDomHolder;
- removeHolder(_: string): boolean;
-}
-// #endregion
-
// #region Functions
export declare function createDockEntryState(_: DevToolsDockEntry, _: Ref): DockEntryState;
export declare function DEFAULT_DOCK_PANEL_STORE(): DockPanelStorage;
diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.js b/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.js
index 09085266..f07f79d5 100644
--- a/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.js
+++ b/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.js
@@ -9,8 +9,6 @@ export var DockEmbedded /* const */
export { createDockEntryState }
export { DEFAULT_DOCK_PANEL_STORE }
export { DockStandalone }
-export { PersistedDomHolder }
-export { PersistedDomViewsManager }
export { sharedStateToRef }
export { useDocksEntries }
// #endregion
\ No newline at end of file