From 7b7cbd9f0f55189d1fbfe2b866a69beeaacbbbe9 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Sun, 31 Aug 2025 11:49:39 +0200 Subject: [PATCH 01/11] feat(core): plugin split pane functionality --- packages/devtools/package.json | 1 + .../devtools/src/context/devtools-context.tsx | 5 +- .../devtools/src/context/devtools-store.ts | 4 +- .../devtools/src/context/dropzone-context.tsx | 60 +++++++++++++++ .../src/context/use-devtools-context.ts | 34 +++++---- packages/devtools/src/index.ts | 1 + packages/devtools/src/tabs/plugins-tab.tsx | 73 +++++++++++++------ packages/react-devtools/src/devtools.tsx | 63 +++++++++++----- pnpm-lock.yaml | 19 +++++ 9 files changed, 202 insertions(+), 58 deletions(-) create mode 100644 packages/devtools/src/context/dropzone-context.tsx diff --git a/packages/devtools/package.json b/packages/devtools/package.json index 652d3742..86fcb58c 100644 --- a/packages/devtools/package.json +++ b/packages/devtools/package.json @@ -55,6 +55,7 @@ "build": "tsup" }, "dependencies": { + "@neodrag/solid": "3.0.0-next.8", "@solid-primitives/keyboard": "^1.2.8", "@tanstack/devtools-event-bus": "workspace:*", "@tanstack/devtools-ui": "workspace:*", diff --git a/packages/devtools/src/context/devtools-context.tsx b/packages/devtools/src/context/devtools-context.tsx index b3da5a53..f18af05e 100644 --- a/packages/devtools/src/context/devtools-context.tsx +++ b/packages/devtools/src/context/devtools-context.tsx @@ -86,7 +86,10 @@ const getSettings = () => { } } -const generatePluginId = (plugin: TanStackDevtoolsPlugin, index: number) => { +export const generatePluginId = ( + plugin: TanStackDevtoolsPlugin, + index: number, +) => { // if set by user, return the plugin id if (plugin.id) { return plugin.id diff --git a/packages/devtools/src/context/devtools-store.ts b/packages/devtools/src/context/devtools-store.ts index ab69d081..ddc5ebb2 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/dropzone-context.tsx b/packages/devtools/src/context/dropzone-context.tsx new file mode 100644 index 00000000..ce92ce3b --- /dev/null +++ b/packages/devtools/src/context/dropzone-context.tsx @@ -0,0 +1,60 @@ +import { createContext, createSignal, useContext } from 'solid-js' + +import type { ParentComponent } from 'solid-js' + +type DropZone = { + id: string + name: string + ref: HTMLElement +} + +const useDropzone = () => { + const [isDragging, setDragging] = createSignal(false) + const dropZones: Array = [] + + const registerDropZone = (zone: DropZone) => { + dropZones.push(zone) + } + + const checkDrop = (dragEl: HTMLElement): string | null => { + const dragRect = dragEl.getBoundingClientRect() + for (const { ref, name } of dropZones) { + const dropRect = ref.getBoundingClientRect() + const isInside = + dragRect.left >= dropRect.left && + dragRect.right <= dropRect.right && + dragRect.top >= dropRect.top && + dragRect.bottom <= dropRect.bottom + if (isInside) return name + } + return null + } + + return { isDragging, setDragging, checkDrop, registerDropZone } +} + +type ContextType = ReturnType + +const DropzoneContext = createContext(undefined) + +export const DropzoneProvider: ParentComponent = (props) => { + const value = useDropzone() + + return ( + + {props.children} + + ) +} + +export function useDropzoneContext() { + const context = useContext(DropzoneContext) + + if (context === undefined) { + throw new Error( + `useDropzoneContext must be used within a DropzoneClientProvider`, + ) + } + + return context +} diff --git a/packages/devtools/src/context/use-devtools-context.ts b/packages/devtools/src/context/use-devtools-context.ts index 51777bb5..157c850f 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) { - setForceExpand(true) - } else { + if (activePlugins().length > 0) { setForceExpand(false) + } else { + setForceExpand(true) } }) - 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] + + return { + ...prev, + state: { + ...prev.state, + activePlugins: updatedPlugins, + }, + } + }) } - return { plugins, setActivePlugin, activePlugin } + return { plugins, toggleActivePlugins, activePlugins } } export const useDevtoolsState = () => { diff --git a/packages/devtools/src/index.ts b/packages/devtools/src/index.ts index 058273ba..e3d8c719 100644 --- a/packages/devtools/src/index.ts +++ b/packages/devtools/src/index.ts @@ -5,3 +5,4 @@ export type { TanStackDevtoolsPlugin, TanStackDevtoolsConfig, } from './context/devtools-context' +export { generatePluginId } from './context/devtools-context' diff --git a/packages/devtools/src/tabs/plugins-tab.tsx b/packages/devtools/src/tabs/plugins-tab.tsx index adb238ea..d88f2fec 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 4e789d78..24cba372 100644 --- a/packages/react-devtools/src/devtools.tsx +++ b/packages/react-devtools/src/devtools.tsx @@ -3,6 +3,7 @@ import { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID, TanStackDevtoolsCore, + generatePluginId, } from '@tanstack/devtools' import { createPortal } from 'react-dom' import type { JSX, ReactElement } from 'react' @@ -109,20 +110,21 @@ export const TanStackDevtools = ({ eventBusConfig, }: TanStackDevtoolsReactInit): ReactElement | null => { const devToolRef = useRef(null) - const [pluginContainer, setPluginContainer] = useState( - null, + const [pluginContainers, setPluginContainers] = useState>( + [], ) - const [titleContainer, setTitleContainer] = useState(null) - const [PluginComponent, setPluginComponent] = useState( - null, + const [titleContainers, setTitleContainers] = useState>([]) + const [PluginComponents, setPluginComponents] = useState>( + [], ) - const [TitleComponent, setTitleComponent] = useState(null) + const [TitleComponents, setTitleComponents] = useState>([]) + const [devtools] = useState( () => new TanStackDevtoolsCore({ config, eventBusConfig, - plugins: plugins?.map((plugin) => { + plugins: plugins?.map((plugin, index) => { return { ...plugin, name: @@ -130,28 +132,45 @@ export const TanStackDevtools = ({ ? plugin.name : // The check above confirms that `plugin.name` is of Render type (e, theme) => { - setTitleContainer( - e.ownerDocument.getElementById( - PLUGIN_TITLE_CONTAINER_ID, - ) || null, + const target = e.ownerDocument.getElementById( + // @ts-ignore just testing + `${PLUGIN_TITLE_CONTAINER_ID}-${generatePluginId(plugin, index)}`, ) + if (target) { + setTitleContainers((prev) => [...prev, target]) + } convertRender( plugin.name as PluginRender, - setTitleComponent, + (newVal) => + // @ts-ignore just testing + setTitleComponents((prev) => [...prev, newVal]), e, theme, ) }, render: (e, theme) => { - setPluginContainer( - e.ownerDocument.getElementById(PLUGIN_CONTAINER_ID) || null, + const target = e.ownerDocument.getElementById( + // @ts-ignore just testing + `${PLUGIN_CONTAINER_ID}-${generatePluginId(plugin, index)}`, + ) + if (target) { + setPluginContainers((prev) => [...prev, target]) + } + + convertRender( + plugin.render, + (newVal) => + // @ts-ignore just testing + setPluginComponents((prev) => [...prev, newVal]), + e, + theme, ) - convertRender(plugin.render, setPluginComponent, e, theme) }, } }), }), ) + useEffect(() => { if (devToolRef.current) { devtools.mount(devToolRef.current) @@ -163,11 +182,17 @@ export const TanStackDevtools = ({ return ( <>
- {pluginContainer && PluginComponent - ? createPortal(<>{PluginComponent}, pluginContainer) + + {pluginContainers.length > 0 && PluginComponents.length > 0 + ? pluginContainers.map((pluginContainer, index) => + createPortal(<>{PluginComponents[index]}, pluginContainer), + ) : null} - {titleContainer && TitleComponent - ? createPortal(<>{TitleComponent}, titleContainer) + + {titleContainers.length > 0 && TitleComponents.length > 0 + ? titleContainers.map((titleContainer, index) => + createPortal(<>{TitleComponents[index]}, titleContainer), + ) : null} ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9f11877..b27158c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -456,6 +456,9 @@ importers: packages/devtools: dependencies: + '@neodrag/solid': + specifier: 3.0.0-next.8 + version: 3.0.0-next.8(@neodrag/core@3.0.0-next.8)(solid-js@1.9.7) '@solid-primitives/keyboard': specifier: ^1.2.8 version: 1.2.8(solid-js@1.9.7) @@ -1754,6 +1757,15 @@ packages: '@napi-rs/wasm-runtime@0.2.4': resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} + '@neodrag/core@3.0.0-next.8': + resolution: {integrity: sha512-RNFwbo2w6M7BEzCh7pls68X2h3n+ofFDW1XnGyIzhtJ2bb8J/2VqixOpOlw9NfSJ61xgUC5ymGQbQFg5BKsWBA==} + + '@neodrag/solid@3.0.0-next.8': + resolution: {integrity: sha512-7NCXB/fZtImSneXSEJ4sK0O9Ob/1SvIjzqTEshE9TATegXFD+jdqDwFhF8W0y6xNpjQlI2cjMELfHARH7HH65g==} + peerDependencies: + '@neodrag/core': 3.0.0-next.8 + solid-js: ^1.0.0 + '@netlify/binary-info@1.0.0': resolution: {integrity: sha512-4wMPu9iN3/HL97QblBsBay3E1etIciR84izI3U+4iALY+JHCrI+a2jO0qbAZ/nxKoegypYEaiiqWXylm+/zfrw==} @@ -8766,6 +8778,13 @@ snapshots: '@emnapi/runtime': 1.4.3 '@tybys/wasm-util': 0.9.0 + '@neodrag/core@3.0.0-next.8': {} + + '@neodrag/solid@3.0.0-next.8(@neodrag/core@3.0.0-next.8)(solid-js@1.9.7)': + dependencies: + '@neodrag/core': 3.0.0-next.8 + solid-js: 1.9.7 + '@netlify/binary-info@1.0.0': {} '@netlify/blobs@9.1.2': From a1b2ece21322e3f763c4e883c51022a01532cad6 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Tue, 9 Sep 2025 14:29:30 +0200 Subject: [PATCH 02/11] fix: cleanup --- .../devtools/src/context/dropzone-context.tsx | 60 ------------------- packages/react-devtools/src/devtools.tsx | 30 +++------- 2 files changed, 9 insertions(+), 81 deletions(-) delete mode 100644 packages/devtools/src/context/dropzone-context.tsx diff --git a/packages/devtools/src/context/dropzone-context.tsx b/packages/devtools/src/context/dropzone-context.tsx deleted file mode 100644 index ce92ce3b..00000000 --- a/packages/devtools/src/context/dropzone-context.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { createContext, createSignal, useContext } from 'solid-js' - -import type { ParentComponent } from 'solid-js' - -type DropZone = { - id: string - name: string - ref: HTMLElement -} - -const useDropzone = () => { - const [isDragging, setDragging] = createSignal(false) - const dropZones: Array = [] - - const registerDropZone = (zone: DropZone) => { - dropZones.push(zone) - } - - const checkDrop = (dragEl: HTMLElement): string | null => { - const dragRect = dragEl.getBoundingClientRect() - for (const { ref, name } of dropZones) { - const dropRect = ref.getBoundingClientRect() - const isInside = - dragRect.left >= dropRect.left && - dragRect.right <= dropRect.right && - dragRect.top >= dropRect.top && - dragRect.bottom <= dropRect.bottom - if (isInside) return name - } - return null - } - - return { isDragging, setDragging, checkDrop, registerDropZone } -} - -type ContextType = ReturnType - -const DropzoneContext = createContext(undefined) - -export const DropzoneProvider: ParentComponent = (props) => { - const value = useDropzone() - - return ( - - {props.children} - - ) -} - -export function useDropzoneContext() { - const context = useContext(DropzoneContext) - - if (context === undefined) { - throw new Error( - `useDropzoneContext must be used within a DropzoneClientProvider`, - ) - } - - return context -} diff --git a/packages/react-devtools/src/devtools.tsx b/packages/react-devtools/src/devtools.tsx index 24cba372..49f56e2f 100644 --- a/packages/react-devtools/src/devtools.tsx +++ b/packages/react-devtools/src/devtools.tsx @@ -95,13 +95,13 @@ export interface TanStackDevtoolsReactInit { const convertRender = ( Component: PluginRender, - setComponent: React.Dispatch>, + setComponents: React.Dispatch>>, 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, element]) } export const TanStackDevtools = ({ @@ -130,41 +130,29 @@ export const TanStackDevtools = ({ name: typeof plugin.name === 'string' ? plugin.name - : // The check above confirms that `plugin.name` is of Render type - (e, theme) => { + : (e, theme) => { const target = e.ownerDocument.getElementById( - // @ts-ignore just testing - `${PLUGIN_TITLE_CONTAINER_ID}-${generatePluginId(plugin, index)}`, + `${PLUGIN_TITLE_CONTAINER_ID}-${generatePluginId(plugin as TanStackDevtoolsPlugin, index)}`, ) if (target) { setTitleContainers((prev) => [...prev, target]) } convertRender( plugin.name as PluginRender, - (newVal) => - // @ts-ignore just testing - setTitleComponents((prev) => [...prev, newVal]), + setTitleComponents, e, theme, ) }, render: (e, theme) => { const target = e.ownerDocument.getElementById( - // @ts-ignore just testing - `${PLUGIN_CONTAINER_ID}-${generatePluginId(plugin, index)}`, + `${PLUGIN_CONTAINER_ID}-${generatePluginId(plugin as TanStackDevtoolsPlugin, index)}`, ) if (target) { setPluginContainers((prev) => [...prev, target]) } - convertRender( - plugin.render, - (newVal) => - // @ts-ignore just testing - setPluginComponents((prev) => [...prev, newVal]), - e, - theme, - ) + convertRender(plugin.render, setPluginComponents, e, theme) }, } }), From fa3bcb8ad2eb2a334f3c0e77d2bbf586b63e80d5 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Tue, 9 Sep 2025 14:38:26 +0200 Subject: [PATCH 03/11] fix: remove neodrag until package is ready --- packages/devtools/package.json | 1 - pnpm-lock.yaml | 19 ------------------- 2 files changed, 20 deletions(-) diff --git a/packages/devtools/package.json b/packages/devtools/package.json index 86fcb58c..652d3742 100644 --- a/packages/devtools/package.json +++ b/packages/devtools/package.json @@ -55,7 +55,6 @@ "build": "tsup" }, "dependencies": { - "@neodrag/solid": "3.0.0-next.8", "@solid-primitives/keyboard": "^1.2.8", "@tanstack/devtools-event-bus": "workspace:*", "@tanstack/devtools-ui": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b27158c9..b9f11877 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -456,9 +456,6 @@ importers: packages/devtools: dependencies: - '@neodrag/solid': - specifier: 3.0.0-next.8 - version: 3.0.0-next.8(@neodrag/core@3.0.0-next.8)(solid-js@1.9.7) '@solid-primitives/keyboard': specifier: ^1.2.8 version: 1.2.8(solid-js@1.9.7) @@ -1757,15 +1754,6 @@ packages: '@napi-rs/wasm-runtime@0.2.4': resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} - '@neodrag/core@3.0.0-next.8': - resolution: {integrity: sha512-RNFwbo2w6M7BEzCh7pls68X2h3n+ofFDW1XnGyIzhtJ2bb8J/2VqixOpOlw9NfSJ61xgUC5ymGQbQFg5BKsWBA==} - - '@neodrag/solid@3.0.0-next.8': - resolution: {integrity: sha512-7NCXB/fZtImSneXSEJ4sK0O9Ob/1SvIjzqTEshE9TATegXFD+jdqDwFhF8W0y6xNpjQlI2cjMELfHARH7HH65g==} - peerDependencies: - '@neodrag/core': 3.0.0-next.8 - solid-js: ^1.0.0 - '@netlify/binary-info@1.0.0': resolution: {integrity: sha512-4wMPu9iN3/HL97QblBsBay3E1etIciR84izI3U+4iALY+JHCrI+a2jO0qbAZ/nxKoegypYEaiiqWXylm+/zfrw==} @@ -8778,13 +8766,6 @@ snapshots: '@emnapi/runtime': 1.4.3 '@tybys/wasm-util': 0.9.0 - '@neodrag/core@3.0.0-next.8': {} - - '@neodrag/solid@3.0.0-next.8(@neodrag/core@3.0.0-next.8)(solid-js@1.9.7)': - dependencies: - '@neodrag/core': 3.0.0-next.8 - solid-js: 1.9.7 - '@netlify/binary-info@1.0.0': {} '@netlify/blobs@9.1.2': From 26ba2b7c66688d28eeb6d60e01b0778abd0e555c Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Tue, 9 Sep 2025 15:29:46 +0200 Subject: [PATCH 04/11] feat: update id getter --- .../devtools/src/context/devtools-context.tsx | 5 +---- packages/devtools/src/index.ts | 1 - packages/react-devtools/src/devtools.tsx | 16 +++++++--------- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/devtools/src/context/devtools-context.tsx b/packages/devtools/src/context/devtools-context.tsx index f18af05e..b3da5a53 100644 --- a/packages/devtools/src/context/devtools-context.tsx +++ b/packages/devtools/src/context/devtools-context.tsx @@ -86,10 +86,7 @@ const getSettings = () => { } } -export const generatePluginId = ( - plugin: TanStackDevtoolsPlugin, - index: number, -) => { +const generatePluginId = (plugin: TanStackDevtoolsPlugin, index: number) => { // if set by user, return the plugin id if (plugin.id) { return plugin.id diff --git a/packages/devtools/src/index.ts b/packages/devtools/src/index.ts index e3d8c719..058273ba 100644 --- a/packages/devtools/src/index.ts +++ b/packages/devtools/src/index.ts @@ -5,4 +5,3 @@ export type { TanStackDevtoolsPlugin, TanStackDevtoolsConfig, } from './context/devtools-context' -export { generatePluginId } from './context/devtools-context' diff --git a/packages/react-devtools/src/devtools.tsx b/packages/react-devtools/src/devtools.tsx index 49f56e2f..1fa3d9f3 100644 --- a/packages/react-devtools/src/devtools.tsx +++ b/packages/react-devtools/src/devtools.tsx @@ -1,10 +1,5 @@ import React, { useEffect, useRef, useState } from 'react' -import { - PLUGIN_CONTAINER_ID, - PLUGIN_TITLE_CONTAINER_ID, - TanStackDevtoolsCore, - generatePluginId, -} from '@tanstack/devtools' +import { TanStackDevtoolsCore } from '@tanstack/devtools' import { createPortal } from 'react-dom' import type { JSX, ReactElement } from 'react' import type { @@ -124,7 +119,7 @@ export const TanStackDevtools = ({ new TanStackDevtoolsCore({ config, eventBusConfig, - plugins: plugins?.map((plugin, index) => { + plugins: plugins?.map((plugin) => { return { ...plugin, name: @@ -132,11 +127,13 @@ export const TanStackDevtools = ({ ? plugin.name : (e, theme) => { const target = e.ownerDocument.getElementById( - `${PLUGIN_TITLE_CONTAINER_ID}-${generatePluginId(plugin as TanStackDevtoolsPlugin, index)}`, + e.getAttribute('id')!, ) + if (target) { setTitleContainers((prev) => [...prev, target]) } + convertRender( plugin.name as PluginRender, setTitleComponents, @@ -146,8 +143,9 @@ export const TanStackDevtools = ({ }, render: (e, theme) => { const target = e.ownerDocument.getElementById( - `${PLUGIN_CONTAINER_ID}-${generatePluginId(plugin as TanStackDevtoolsPlugin, index)}`, + e.getAttribute('id')!, ) + if (target) { setPluginContainers((prev) => [...prev, target]) } From 76dd702862185b2d88668cf2cebc681a4dfe6a5f Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Tue, 9 Sep 2025 16:10:04 +0200 Subject: [PATCH 05/11] fix: dedupe plugins --- .../src/context/use-devtools-context.ts | 4 +- packages/react-devtools/src/devtools.tsx | 55 +++++++++++++------ 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/packages/devtools/src/context/use-devtools-context.ts b/packages/devtools/src/context/use-devtools-context.ts index 157c850f..2d9f8472 100644 --- a/packages/devtools/src/context/use-devtools-context.ts +++ b/packages/devtools/src/context/use-devtools-context.ts @@ -37,9 +37,9 @@ export const usePlugins = () => { createEffect(() => { if (activePlugins().length > 0) { - setForceExpand(false) - } else { setForceExpand(true) + } else { + setForceExpand(false) } }) diff --git a/packages/react-devtools/src/devtools.tsx b/packages/react-devtools/src/devtools.tsx index 1fa3d9f3..db2e8240 100644 --- a/packages/react-devtools/src/devtools.tsx +++ b/packages/react-devtools/src/devtools.tsx @@ -90,13 +90,19 @@ export interface TanStackDevtoolsReactInit { const convertRender = ( Component: PluginRender, - setComponents: React.Dispatch>>, + setComponents: React.Dispatch< + React.SetStateAction> + >, e: HTMLElement, theme: 'dark' | 'light', ) => { const element = typeof Component === 'function' ? Component(e, theme) : Component - setComponents((prev) => [...prev, element]) + + setComponents((prev) => ({ + ...prev, + [e.getAttribute('id') as string]: element, + })) } export const TanStackDevtools = ({ @@ -105,14 +111,19 @@ export const TanStackDevtools = ({ eventBusConfig, }: TanStackDevtoolsReactInit): ReactElement | null => { const devToolRef = useRef(null) - const [pluginContainers, setPluginContainers] = useState>( - [], - ) - const [titleContainers, setTitleContainers] = useState>([]) - const [PluginComponents, setPluginComponents] = useState>( - [], - ) - const [TitleComponents, setTitleComponents] = useState>([]) + const [pluginContainers, setPluginContainers] = useState< + Record + >({}) + const [titleContainers, setTitleContainers] = useState< + Record + >({}) + + const [PluginComponents, setPluginComponents] = useState< + Record + >({}) + const [TitleComponents, setTitleComponents] = useState< + Record + >({}) const [devtools] = useState( () => @@ -131,7 +142,10 @@ export const TanStackDevtools = ({ ) if (target) { - setTitleContainers((prev) => [...prev, target]) + setTitleContainers((prev) => ({ + ...prev, + [e.getAttribute('id') as string]: e, + })) } convertRender( @@ -147,7 +161,10 @@ export const TanStackDevtools = ({ ) if (target) { - setPluginContainers((prev) => [...prev, target]) + setPluginContainers((prev) => ({ + ...prev, + [e.getAttribute('id') as string]: e, + })) } convertRender(plugin.render, setPluginComponents, e, theme) @@ -169,15 +186,17 @@ export const TanStackDevtools = ({ <>
- {pluginContainers.length > 0 && PluginComponents.length > 0 - ? pluginContainers.map((pluginContainer, index) => - createPortal(<>{PluginComponents[index]}, pluginContainer), + {Object.values(pluginContainers).length > 0 && + Object.values(PluginComponents).length > 0 + ? Object.entries(pluginContainers).map(([key, pluginContainer]) => + createPortal(<>{PluginComponents[key]}, pluginContainer), ) : null} - {titleContainers.length > 0 && TitleComponents.length > 0 - ? titleContainers.map((titleContainer, index) => - createPortal(<>{TitleComponents[index]}, titleContainer), + {Object.values(titleContainers).length > 0 && + Object.values(TitleComponents).length > 0 + ? Object.entries(titleContainers).map(([key, titleContainer]) => + createPortal(<>{TitleComponents[key]}, titleContainer), ) : null} From fef5f52123bb3a418da2cbc92e4e0eaf855a5591 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Tue, 9 Sep 2025 16:21:44 +0200 Subject: [PATCH 06/11] feat: limit devtools pannel to 3 plugins --- packages/devtools/src/context/use-devtools-context.ts | 2 ++ packages/react-devtools/src/devtools.tsx | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/devtools/src/context/use-devtools-context.ts b/packages/devtools/src/context/use-devtools-context.ts index 2d9f8472..cf5f4027 100644 --- a/packages/devtools/src/context/use-devtools-context.ts +++ b/packages/devtools/src/context/use-devtools-context.ts @@ -44,6 +44,8 @@ export const usePlugins = () => { }) const toggleActivePlugins = (pluginId: string) => { + if (store.state.activePlugins.length === 3) return + setStore((prev) => { const isActive = prev.state.activePlugins.includes(pluginId) diff --git a/packages/react-devtools/src/devtools.tsx b/packages/react-devtools/src/devtools.tsx index db2e8240..eb38b2a0 100644 --- a/packages/react-devtools/src/devtools.tsx +++ b/packages/react-devtools/src/devtools.tsx @@ -111,6 +111,7 @@ export const TanStackDevtools = ({ eventBusConfig, }: TanStackDevtoolsReactInit): ReactElement | null => { const devToolRef = useRef(null) + const [pluginContainers, setPluginContainers] = useState< Record >({}) From a3da722a9dbc5b4c2cb8167c48278b3f37f41f69 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Tue, 9 Sep 2025 19:45:25 +0200 Subject: [PATCH 07/11] chore: reuse identifiers --- packages/react-devtools/src/devtools.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/react-devtools/src/devtools.tsx b/packages/react-devtools/src/devtools.tsx index eb38b2a0..98ab2b07 100644 --- a/packages/react-devtools/src/devtools.tsx +++ b/packages/react-devtools/src/devtools.tsx @@ -138,14 +138,13 @@ export const TanStackDevtools = ({ typeof plugin.name === 'string' ? plugin.name : (e, theme) => { - const target = e.ownerDocument.getElementById( - e.getAttribute('id')!, - ) + const id = e.getAttribute('id')! + const target = e.ownerDocument.getElementById(id) if (target) { setTitleContainers((prev) => ({ ...prev, - [e.getAttribute('id') as string]: e, + [id]: e, })) } @@ -157,14 +156,13 @@ export const TanStackDevtools = ({ ) }, render: (e, theme) => { - const target = e.ownerDocument.getElementById( - e.getAttribute('id')!, - ) + const id = e.getAttribute('id')! + const target = e.ownerDocument.getElementById(id) if (target) { setPluginContainers((prev) => ({ ...prev, - [e.getAttribute('id') as string]: e, + [id]: e, })) } From b5b70faee1e7d832760b7d40e24f6644f4aebfba Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Wed, 10 Sep 2025 20:08:57 +0200 Subject: [PATCH 08/11] fix: max pannels --- packages/devtools/src/context/use-devtools-context.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/devtools/src/context/use-devtools-context.ts b/packages/devtools/src/context/use-devtools-context.ts index cf5f4027..4375a0dd 100644 --- a/packages/devtools/src/context/use-devtools-context.ts +++ b/packages/devtools/src/context/use-devtools-context.ts @@ -44,15 +44,13 @@ export const usePlugins = () => { }) const toggleActivePlugins = (pluginId: string) => { - if (store.state.activePlugins.length === 3) return - 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: { From cf8428b5e529bcae6c5da33836ddc78b30c835ba Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Wed, 10 Sep 2025 21:20:45 +0200 Subject: [PATCH 09/11] chore: alias plugin and title state --- packages/react-devtools/src/devtools.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/react-devtools/src/devtools.tsx b/packages/react-devtools/src/devtools.tsx index 98ab2b07..f32a40c3 100644 --- a/packages/react-devtools/src/devtools.tsx +++ b/packages/react-devtools/src/devtools.tsx @@ -181,19 +181,24 @@ 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 ( <>
- {Object.values(pluginContainers).length > 0 && - Object.values(PluginComponents).length > 0 + {hasPlugins ? Object.entries(pluginContainers).map(([key, pluginContainer]) => createPortal(<>{PluginComponents[key]}, pluginContainer), ) : null} - {Object.values(titleContainers).length > 0 && - Object.values(TitleComponents).length > 0 + {hasTitles ? Object.entries(titleContainers).map(([key, titleContainer]) => createPortal(<>{TitleComponents[key]}, titleContainer), ) From 59025a725337ebe42ff38867e7958abfd9ee06bb Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Wed, 10 Sep 2025 22:04:34 +0200 Subject: [PATCH 10/11] chore: change set --- .changeset/fair-crabs-sing.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/fair-crabs-sing.md diff --git a/.changeset/fair-crabs-sing.md b/.changeset/fair-crabs-sing.md new file mode 100644 index 00000000..b359cc8a --- /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. From 7e7744953e77509c4a8377c704156b589dab81d4 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Wed, 10 Sep 2025 22:38:28 +0200 Subject: [PATCH 11/11] fix: draw force expand --- packages/devtools/src/context/use-devtools-context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools/src/context/use-devtools-context.ts b/packages/devtools/src/context/use-devtools-context.ts index 4375a0dd..d61115d0 100644 --- a/packages/devtools/src/context/use-devtools-context.ts +++ b/packages/devtools/src/context/use-devtools-context.ts @@ -36,7 +36,7 @@ export const usePlugins = () => { const activePlugins = createMemo(() => store.state.activePlugins) createEffect(() => { - if (activePlugins().length > 0) { + if (activePlugins().length === 0) { setForceExpand(true) } else { setForceExpand(false)