diff --git a/packages/client/src/App.vue b/packages/client/src/App.vue index c5155eb1..2886150d 100644 --- a/packages/client/src/App.vue +++ b/packages/client/src/App.vue @@ -29,6 +29,7 @@ onRpcConnected(() => { rpc.value.emit('update-client-state', { minimizePanelInteractive: devtoolsClientState.value.minimizePanelInteractive, closeOnOutsideClick: devtoolsClientState.value.interactionCloseOnOutsideClick, + highlightComponentTracking: devtoolsClientState.value.highlightComponentTracking, showFloatingPanel: devtoolsClientState.value.showPanel, reduceMotion: devtoolsClientState.value.reduceMotion, }) diff --git a/packages/client/src/composables/state.ts b/packages/client/src/composables/state.ts index 6196b91f..64bdb7a8 100644 --- a/packages/client/src/composables/state.ts +++ b/packages/client/src/composables/state.ts @@ -17,6 +17,7 @@ interface DevtoolsClientState { scale: number interactionCloseOnOutsideClick: boolean showPanel: boolean + highlightComponentTracking: boolean minimizePanelInteractive: number reduceMotion: boolean } @@ -45,6 +46,7 @@ function clientStateFactory(): DevtoolsClientState { scale: 1, interactionCloseOnOutsideClick: false, showPanel: true, + highlightComponentTracking: false, minimizePanelInteractive: 5000, reduceMotion: false, } diff --git a/packages/client/src/pages/settings.vue b/packages/client/src/pages/settings.vue index 77cdfb34..79b252fe 100644 --- a/packages/client/src/pages/settings.vue +++ b/packages/client/src/pages/settings.vue @@ -16,7 +16,7 @@ const hostEnv = useHostEnv() */ const enableFeatureSettings = hostEnv === 'iframe' || hostEnv === 'separate-window' -const { scale, interactionCloseOnOutsideClick, showPanel, minimizePanelInteractive, expandSidebar, scrollableSidebar, reduceMotion } = toRefs(toReactive(devtoolsClientState)) +const { scale, interactionCloseOnOutsideClick, showPanel, minimizePanelInteractive, expandSidebar, scrollableSidebar, reduceMotion, highlightComponentTracking } = toRefs(toReactive(devtoolsClientState)) // #region settings const scaleOptions = [ @@ -210,6 +210,12 @@ const minimizePanelInteractiveLabel = computed(() => {
+
+ +
+ + Highlight Component Re Rendering +
diff --git a/packages/devtools-kit/src/core/component-flash/index.ts b/packages/devtools-kit/src/core/component-flash/index.ts new file mode 100644 index 00000000..57202c1f --- /dev/null +++ b/packages/devtools-kit/src/core/component-flash/index.ts @@ -0,0 +1,61 @@ +import { ComponentHighLighterOptions, VueAppInstance } from '../../../types' +import { getComponentBoundingRect } from '../component/state/bounding-rect' +import { getInstanceName } from '../component/utils' + +const CONTAINER_ELEMENT_ID = '__vue-devtools-flash__' +const REMOVE_DELAY_MS = 1000 + +const containerStyles = { + border: '2px rgba(65, 184, 131, 0.7) solid', + position: 'fixed', + zIndex: '2147483645', + pointerEvents: 'none', + borderRadius: '3px', + boxSizing: 'border-box', + transition: 'none', + opacity: '1', +} + +function getStyles(bounds: ComponentHighLighterOptions['bounds']) { + return { + left: `${Math.round(bounds.left)}px`, + top: `${Math.round(bounds.top)}px`, + width: `${Math.round(bounds.width)}px`, + height: `${Math.round(bounds.height)}px`, + } satisfies Partial +} + +function create(options: ComponentHighLighterOptions & { elementId?: string, style?: Partial }) { + const containerEl = document.createElement('div') + containerEl.id = options?.elementId ?? CONTAINER_ELEMENT_ID + + Object.assign(containerEl.style, { + ...containerStyles, + ...getStyles(options.bounds), + ...options.style, + }) + + document.body.appendChild(containerEl) + + requestAnimationFrame(() => { + containerEl.style.transition = 'opacity 1s' + containerEl.style.opacity = '0' + }) + + clearTimeout((containerEl as any)?._timer); + (containerEl as any)._timer = setTimeout(() => { + document.body.removeChild(containerEl) + }, REMOVE_DELAY_MS) + + return containerEl +} + +export function flashComponent(instance: VueAppInstance) { + const bounds = getComponentBoundingRect(instance) + + if (!bounds.width && !bounds.height) + return + + const name = getInstanceName(instance) + create({ bounds, name }) +} diff --git a/packages/devtools-kit/src/core/plugin/components.ts b/packages/devtools-kit/src/core/plugin/components.ts index d2bdbb38..99d1bcb4 100644 --- a/packages/devtools-kit/src/core/plugin/components.ts +++ b/packages/devtools-kit/src/core/plugin/components.ts @@ -6,6 +6,7 @@ import { ComponentWalker } from '../../core/component/tree/walker' import { getAppRecord, getComponentId, getComponentInstance } from '../../core/component/utils' import { activeAppRecord, devtoolsContext, devtoolsState, DevToolsV6PluginAPIHookKeys } from '../../ctx' import { hook } from '../../hook' +import { flashComponent } from '../component-flash' import { setupBuiltinTimelineLayers } from '../timeline' import { exposeInstanceToWindow } from '../vm' @@ -115,6 +116,10 @@ export function createComponentsDevToolsPlugin(app: App): [PluginDescriptor, Plu } } + if (devtoolsState.flashUpdates) { + flashComponent(component) + } + if (!appRecord) return @@ -150,6 +155,10 @@ export function createComponentsDevToolsPlugin(app: App): [PluginDescriptor, Plu } } + if (devtoolsState.flashUpdates) { + flashComponent(component) + } + if (!appRecord) return @@ -166,6 +175,10 @@ export function createComponentsDevToolsPlugin(app: App): [PluginDescriptor, Plu if (!app || (typeof uid !== 'number' && !uid) || !component) return + if (devtoolsState.flashUpdates) { + flashComponent(component) + } + const appRecord = await getAppRecord(app) if (!appRecord) diff --git a/packages/devtools-kit/src/ctx/state.ts b/packages/devtools-kit/src/ctx/state.ts index 70b48109..8b9de4fd 100644 --- a/packages/devtools-kit/src/ctx/state.ts +++ b/packages/devtools-kit/src/ctx/state.ts @@ -16,6 +16,7 @@ export interface DevToolsState { tabs: CustomTab[] commands: CustomCommand[] highPerfModeEnabled: boolean + flashUpdates: boolean devtoolsClientDetected: { [key: string]: boolean } @@ -40,6 +41,7 @@ function initStateFactory() { tabs: [], commands: [], highPerfModeEnabled: true, + flashUpdates: true, devtoolsClientDetected: {}, perfUniqueGroupId: 0, timelineLayersState: getTimelineLayersStateFromStorage(), diff --git a/packages/overlay/src/components/FrameBox.vue b/packages/overlay/src/components/FrameBox.vue index f6a082af..f7d7aabc 100644 --- a/packages/overlay/src/components/FrameBox.vue +++ b/packages/overlay/src/components/FrameBox.vue @@ -31,6 +31,7 @@ onRpcSeverReady(() => { updateState({ minimizePanelInactive: v.minimizePanelInteractive, closeOnOutsideClick: v.closeOnOutsideClick, + highlightComponentTracking: v.highlightComponentTracking, preferShowFloatingPanel: v.showFloatingPanel, reduceMotion: v.reduceMotion, }) diff --git a/packages/overlay/src/composables/state.ts b/packages/overlay/src/composables/state.ts index 58ab4ba5..49fdea5d 100644 --- a/packages/overlay/src/composables/state.ts +++ b/packages/overlay/src/composables/state.ts @@ -15,6 +15,7 @@ interface DevToolsFrameState { minimizePanelInactive: number preferShowFloatingPanel: boolean reduceMotion: boolean + highlightComponentTracking: boolean } export interface UseFrameStateReturn { @@ -35,6 +36,7 @@ const state = useLocalStorage('__vue-devtools-frame-state__' minimizePanelInactive: 5000, preferShowFloatingPanel: true, reduceMotion: false, + highlightComponentTracking: false, }) export function useFrameState(): UseFrameStateReturn {