diff --git a/packages/core/src/client/inject/runtime.ts b/packages/core/src/client/inject/runtime.ts index 0a5d2c52..04ee98ad 100644 --- a/packages/core/src/client/inject/runtime.ts +++ b/packages/core/src/client/inject/runtime.ts @@ -1,7 +1,7 @@ /// /// -import type { DockPanelStorage } from '@vitejs/devtools-kit/client' +import type { DevToolsDockPanelStorage } from '../webcomponents/state/docks' import { CLIENT_CONTEXT_KEY, getDevToolsRpcClient } from '@vitejs/devtools-kit/client' import { DEVTOOLS_MOUNT_PATH } from '@vitejs/devtools-kit/constants' import { useLocalStorage } from '@vueuse/core' @@ -60,7 +60,12 @@ async function mountDock(): Promise { ], }) - const state = useLocalStorage( + /** + * Defaults here are this injected client's own — deliberately different + * from `DEFAULT_DOCK_PANEL_STORE()`'s fallback used elsewhere, not a shared + * literal. See `DevToolsDockPanelStorage` for what's persisted and why. + */ + const state = useLocalStorage( 'vite-devtools-dock-state', { mode: 'float', @@ -71,6 +76,7 @@ async function mountDock(): Promise { position: 'left', open: false, inactiveTimeout: 3_000, + selectedId: null, }, { mergeDefaults: true }, ) diff --git a/packages/core/src/client/webcomponents/components/dock/DockEmbedded.vue b/packages/core/src/client/webcomponents/components/dock/DockEmbedded.vue index 14277351..14605107 100644 --- a/packages/core/src/client/webcomponents/components/dock/DockEmbedded.vue +++ b/packages/core/src/client/webcomponents/components/dock/DockEmbedded.vue @@ -2,7 +2,7 @@ import type { DocksContext } from '@vitejs/devtools-kit/client' import type { DockLayout } from './dock-layout' import { useEventListener } from '@vueuse/core' -import { onUnmounted } from 'vue' +import { onUnmounted, watch } from 'vue' import { sharedStateToRef } from '../../state/docks' import { closeDockPopup, useIsDockPopupOpen } from '../../state/popup' import { useIsRpcTrusted } from '../../utils/useIsRpcTrusted' @@ -21,12 +21,28 @@ const props = defineProps<{ layout?: Partial }>() +const context = props.context + const isDockPopupOpen = useIsDockPopupOpen() const settings = sharedStateToRef(props.context.docks.settings) // Force float mode when unauthorized, regardless of store setting const isRpcTrusted = useIsRpcTrusted(props.context) +/** + * If the panel is open but nothing valid is selected (e.g. a restored + * `selectedId` didn't resolve to a real entry), fall back to the first + * available one — mirrors `DockStandalone`'s own boot guard. + */ +watch( + () => context.docks.entries, + () => { + if (context.panel.store.open) + context.docks.selectedId ||= context.docks.entries[0]?.id ?? null + }, + { immediate: true }, +) + // Close the dock when clicking outside of it useEventListener(window, 'mousedown', (e: MouseEvent) => { if (!settings.value.closeOnOutsideClick) diff --git a/packages/core/src/client/webcomponents/components/views/ViewJsonRender.vue b/packages/core/src/client/webcomponents/components/views/ViewJsonRender.vue index 723e93d5..f9350dcf 100644 --- a/packages/core/src/client/webcomponents/components/views/ViewJsonRender.vue +++ b/packages/core/src/client/webcomponents/components/views/ViewJsonRender.vue @@ -3,7 +3,9 @@ import type { Spec } from '@json-render/core' import type { DevToolsViewJsonRender } from '@vitejs/devtools-kit' import type { DocksContext } from '@vitejs/devtools-kit/client' import { JSONUIProvider, Renderer } from '@json-render/vue' -import { computed, markRaw, onMounted, ref, shallowRef, watch } from 'vue' +import { useDebounceFn, useSessionStorage } from '@vueuse/core' +import { computed, markRaw, onMounted, provide, ref, shallowRef, useTemplateRef, watch } from 'vue' +import { DOCK_ENTRY_ID_KEY } from '../../json-render/composables/dock-entry-id' import { devtoolsRegistry, UnsupportedComponent } from '../../json-render/registry' const props = defineProps<{ @@ -11,10 +13,32 @@ const props = defineProps<{ entry: DevToolsViewJsonRender }>() +/** + * Descendants (e.g. `useUncontrolledValue`) `inject()` this to scope + * session-persisted state to "this dock" — the entry's own id, stable for + * this component instance's lifetime (`ViewEntry` re-keys on entry change). + */ +provide(DOCK_ENTRY_ID_KEY, props.entry.id) + const spec = shallowRef(null) const isLoading = ref(true) const error = ref(null) +/** + * Restores/persists the scroll position of this dock's own view, per tab, + * across a reload — keyed by the dock entry id so switching docks doesn't + * bleed one dock's scroll into another's. + */ +const scrollContainer = useTemplateRef('scrollContainer') +const scrollTop = useSessionStorage(`vite-devtools-scroll:${props.entry.id}`, 0) +onMounted(() => { + if (scrollContainer.value) + scrollContainer.value.scrollTop = scrollTop.value +}) +const persistScrollTop = useDebounceFn(() => { + scrollTop.value = scrollContainer.value?.scrollTop ?? 0 +}, 200) + // Resolve spec from entry.ui._stateKey async function loadSpec() { try { @@ -87,7 +111,7 @@ watch(() => props.entry.ui?._stateKey, loadSpec)