diff --git a/.changeset/fair-crabs-sing.md b/.changeset/fair-crabs-sing.md new file mode 100644 index 0000000..b359cc8 --- /dev/null +++ b/.changeset/fair-crabs-sing.md @@ -0,0 +1,6 @@ +--- +'@tanstack/react-devtools': patch +'@tanstack/devtools': patch +--- + +Adds split panel functionality to the devtools panel, allowing multiple instances of devtools to be shown. diff --git a/packages/devtools/src/context/devtools-store.ts b/packages/devtools/src/context/devtools-store.ts index ab69d08..ddc5ebb 100644 --- a/packages/devtools/src/context/devtools-store.ts +++ b/packages/devtools/src/context/devtools-store.ts @@ -65,7 +65,7 @@ export type DevtoolsStore = { state: { activeTab: TabName height: number - activePlugin?: string | undefined + activePlugins: Array persistOpen: boolean } plugins?: Array @@ -89,7 +89,7 @@ export const initialState: DevtoolsStore = { state: { activeTab: 'plugins', height: 400, - activePlugin: undefined, + activePlugins: [], persistOpen: false, }, } diff --git a/packages/devtools/src/context/use-devtools-context.ts b/packages/devtools/src/context/use-devtools-context.ts index 51777bb..d61115d 100644 --- a/packages/devtools/src/context/use-devtools-context.ts +++ b/packages/devtools/src/context/use-devtools-context.ts @@ -33,27 +33,35 @@ export const usePlugins = () => { const { setForceExpand } = useDrawContext() const plugins = createMemo(() => store.plugins) - const activePlugin = createMemo(() => store.state.activePlugin) + const activePlugins = createMemo(() => store.state.activePlugins) createEffect(() => { - if (activePlugin() == null) { + if (activePlugins().length === 0) { setForceExpand(true) } else { setForceExpand(false) } }) - const setActivePlugin = (pluginId: string) => { - setStore((prev) => ({ - ...prev, - state: { - ...prev.state, - activePlugin: pluginId, - }, - })) + const toggleActivePlugins = (pluginId: string) => { + setStore((prev) => { + const isActive = prev.state.activePlugins.includes(pluginId) + + const updatedPlugins = isActive + ? prev.state.activePlugins.filter((id) => id !== pluginId) + : [...prev.state.activePlugins, pluginId] + if (updatedPlugins.length > 3) return prev + return { + ...prev, + state: { + ...prev.state, + activePlugins: updatedPlugins, + }, + } + }) } - return { plugins, setActivePlugin, activePlugin } + return { plugins, toggleActivePlugins, activePlugins } } export const useDevtoolsState = () => { diff --git a/packages/devtools/src/tabs/plugins-tab.tsx b/packages/devtools/src/tabs/plugins-tab.tsx index adb238e..d88f2fe 100644 --- a/packages/devtools/src/tabs/plugins-tab.tsx +++ b/packages/devtools/src/tabs/plugins-tab.tsx @@ -1,4 +1,4 @@ -import { For, createEffect } from 'solid-js' +import { For, createEffect, createMemo, createSignal } from 'solid-js' import clsx from 'clsx' import { useDrawContext } from '../context/draw-context' import { usePlugins, useTheme } from '../context/use-devtools-context' @@ -6,20 +6,30 @@ import { useStyles } from '../styles/use-styles' import { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID } from '../constants' export const PluginsTab = () => { - const { plugins, activePlugin, setActivePlugin } = usePlugins() + const { plugins, activePlugins, toggleActivePlugins } = usePlugins() const { expanded, hoverUtils, animationMs } = useDrawContext() - let activePluginRef: HTMLDivElement | undefined + + const [pluginRefs, setPluginRefs] = createSignal( + new Map(), + ) + + const styles = useStyles() const { theme } = useTheme() createEffect(() => { - const currentActivePlugin = plugins()?.find( - (plugin) => plugin.id === activePlugin(), + const currentActivePlugins = plugins()?.filter((plugin) => + activePlugins().includes(plugin.id!), ) - if (activePluginRef && currentActivePlugin) { - currentActivePlugin.render(activePluginRef, theme()) - } + + currentActivePlugins?.forEach((plugin) => { + const ref = pluginRefs().get(plugin.id!) + + if (ref) { + plugin.render(ref, theme()) + } + }) }) - const styles = useStyles() + return (
{ }, styles().pluginsTabDrawTransition(animationMs), )} - onMouseEnter={() => { - hoverUtils.enter() - }} - onMouseLeave={() => { - hoverUtils.leave() - }} + onMouseEnter={() => hoverUtils.enter()} + onMouseLeave={() => hoverUtils.leave()} >
{ {(plugin) => { let pluginHeading: HTMLHeadingElement | undefined + createEffect(() => { if (pluginHeading) { typeof plugin.name === 'string' @@ -53,14 +60,24 @@ export const PluginsTab = () => { : plugin.name(pluginHeading, theme()) } }) + + const isActive = createMemo(() => + activePlugins().includes(plugin.id!), + ) + return (
setActivePlugin(plugin.id!)} + onClick={() => { + toggleActivePlugins(plugin.id!) + }} class={clsx(styles().pluginName, { - active: activePlugin() === plugin.id, + active: isActive(), })} > -

+

) }} @@ -68,11 +85,21 @@ export const PluginsTab = () => {
-
+ + {(pluginId) => ( +
{ + setPluginRefs((prev) => { + const updated = new Map(prev) + updated.set(pluginId, el) + return updated + }) + }} + class={styles().pluginsTabContent} + /> + )} +
) } diff --git a/packages/react-devtools/src/devtools.tsx b/packages/react-devtools/src/devtools.tsx index 4e789d7..f32a40c 100644 --- a/packages/react-devtools/src/devtools.tsx +++ b/packages/react-devtools/src/devtools.tsx @@ -1,9 +1,5 @@ import React, { useEffect, useRef, useState } from 'react' -import { - PLUGIN_CONTAINER_ID, - PLUGIN_TITLE_CONTAINER_ID, - TanStackDevtoolsCore, -} from '@tanstack/devtools' +import { TanStackDevtoolsCore } from '@tanstack/devtools' import { createPortal } from 'react-dom' import type { JSX, ReactElement } from 'react' import type { @@ -94,13 +90,19 @@ export interface TanStackDevtoolsReactInit { const convertRender = ( Component: PluginRender, - setComponent: React.Dispatch>, + setComponents: React.Dispatch< + React.SetStateAction> + >, e: HTMLElement, theme: 'dark' | 'light', ) => { - setComponent( - typeof Component === 'function' ? Component(e, theme) : Component, - ) + const element = + typeof Component === 'function' ? Component(e, theme) : Component + + setComponents((prev) => ({ + ...prev, + [e.getAttribute('id') as string]: element, + })) } export const TanStackDevtools = ({ @@ -109,14 +111,21 @@ export const TanStackDevtools = ({ eventBusConfig, }: TanStackDevtoolsReactInit): ReactElement | null => { const devToolRef = useRef(null) - const [pluginContainer, setPluginContainer] = useState( - null, - ) - const [titleContainer, setTitleContainer] = useState(null) - const [PluginComponent, setPluginComponent] = useState( - null, - ) - const [TitleComponent, setTitleComponent] = useState(null) + + const [pluginContainers, setPluginContainers] = useState< + Record + >({}) + const [titleContainers, setTitleContainers] = useState< + Record + >({}) + + const [PluginComponents, setPluginComponents] = useState< + Record + >({}) + const [TitleComponents, setTitleComponents] = useState< + Record + >({}) + const [devtools] = useState( () => new TanStackDevtoolsCore({ @@ -128,30 +137,42 @@ export const TanStackDevtools = ({ name: typeof plugin.name === 'string' ? plugin.name - : // The check above confirms that `plugin.name` is of Render type - (e, theme) => { - setTitleContainer( - e.ownerDocument.getElementById( - PLUGIN_TITLE_CONTAINER_ID, - ) || null, - ) + : (e, theme) => { + const id = e.getAttribute('id')! + const target = e.ownerDocument.getElementById(id) + + if (target) { + setTitleContainers((prev) => ({ + ...prev, + [id]: e, + })) + } + convertRender( plugin.name as PluginRender, - setTitleComponent, + setTitleComponents, e, theme, ) }, render: (e, theme) => { - setPluginContainer( - e.ownerDocument.getElementById(PLUGIN_CONTAINER_ID) || null, - ) - convertRender(plugin.render, setPluginComponent, e, theme) + const id = e.getAttribute('id')! + const target = e.ownerDocument.getElementById(id) + + if (target) { + setPluginContainers((prev) => ({ + ...prev, + [id]: e, + })) + } + + convertRender(plugin.render, setPluginComponents, e, theme) }, } }), }), ) + useEffect(() => { if (devToolRef.current) { devtools.mount(devToolRef.current) @@ -160,14 +181,27 @@ export const TanStackDevtools = ({ return () => devtools.unmount() }, [devtools]) + const hasPlugins = + Object.values(pluginContainers).length > 0 && + Object.values(PluginComponents).length > 0 + const hasTitles = + Object.values(titleContainers).length > 0 && + Object.values(TitleComponents).length > 0 + return ( <>
- {pluginContainer && PluginComponent - ? createPortal(<>{PluginComponent}, pluginContainer) + + {hasPlugins + ? Object.entries(pluginContainers).map(([key, pluginContainer]) => + createPortal(<>{PluginComponents[key]}, pluginContainer), + ) : null} - {titleContainer && TitleComponent - ? createPortal(<>{TitleComponent}, titleContainer) + + {hasTitles + ? Object.entries(titleContainers).map(([key, titleContainer]) => + createPortal(<>{TitleComponents[key]}, titleContainer), + ) : null} )