From 5eb3630541d0ab6d166be2bed4c3b382de1cf72c Mon Sep 17 00:00:00 2001 From: akshat Date: Mon, 13 Oct 2025 19:13:09 +0530 Subject: [PATCH 1/4] feat: custom trigger component - changes in config to accept an optional custom trigger component - replaces default trigger and allows for rendering a custom trigger --- packages/devtools/src/components/trigger.tsx | 40 ++++- .../devtools/src/context/devtools-store.ts | 12 ++ packages/react-devtools/src/devtools.tsx | 150 ++++++++++++------ 3 files changed, 148 insertions(+), 54 deletions(-) diff --git a/packages/devtools/src/components/trigger.tsx b/packages/devtools/src/components/trigger.tsx index a2944e79..962d1ab7 100644 --- a/packages/devtools/src/components/trigger.tsx +++ b/packages/devtools/src/components/trigger.tsx @@ -1,4 +1,4 @@ -import { Show, createMemo } from 'solid-js' +import { Show, createEffect, createMemo, createSignal } from 'solid-js' import clsx from 'clsx' import { useDevtoolsSettings } from '../context/use-devtools-context' import { useStyles } from '../styles/use-styles' @@ -15,6 +15,7 @@ export const Trigger = ({ image: string }) => { const { settings } = useDevtoolsSettings() + const [containerRef, setContainerRef] = createSignal() const styles = useStyles() const buttonStyle = createMemo(() => { return clsx( @@ -23,16 +24,39 @@ export const Trigger = ({ styles().mainCloseBtnAnimation(isOpen(), settings().hideUntilHover), ) }) + + createEffect(() => { + const triggerComponent = settings().triggerComponent + const el = containerRef() + if (triggerComponent && el) { + triggerComponent(el, { + theme: settings().theme, + image: image || TanStackLogo, + isOpen, + setIsOpen, + hideUntilHover: settings().hideUntilHover, + position: settings().position, + }) + } + }) + return ( - + } > - TanStack Devtools - +
+ ) } diff --git a/packages/devtools/src/context/devtools-store.ts b/packages/devtools/src/context/devtools-store.ts index 8ff46b39..172e5d28 100644 --- a/packages/devtools/src/context/devtools-store.ts +++ b/packages/devtools/src/context/devtools-store.ts @@ -1,3 +1,4 @@ +import type { Accessor } from 'solid-js' import type { TabName } from '../tabs' import type { TanStackDevtoolsPlugin } from './devtools-context' @@ -18,6 +19,15 @@ type TriggerPosition = | 'middle-left' | 'middle-right' +type TriggerProps = { + isOpen: Accessor + setIsOpen: (isOpen: boolean) => void + theme: 'light' | 'dark' + image: string + position: TriggerPosition + hideUntilHover: boolean +} + export type DevtoolsStore = { settings: { /** @@ -70,6 +80,7 @@ export type DevtoolsStore = { * Whether the trigger should be completely hidden or not (you can still open with the hotkey) */ triggerHidden?: boolean + triggerComponent?: (el: HTMLElement, props: TriggerProps) => void } state: { activeTab: TabName @@ -97,6 +108,7 @@ export const initialState: DevtoolsStore = { : 'light', triggerImage: '', triggerHidden: false, + triggerComponent: undefined, }, state: { activeTab: 'plugins', diff --git a/packages/react-devtools/src/devtools.tsx b/packages/react-devtools/src/devtools.tsx index f32a40c3..dc973896 100644 --- a/packages/react-devtools/src/devtools.tsx +++ b/packages/react-devtools/src/devtools.tsx @@ -12,6 +12,25 @@ type PluginRender = | JSX.Element | ((el: HTMLElement, theme: 'dark' | 'light') => JSX.Element) +type TriggerProps = { + theme: 'dark' | 'light' + image: string + position: + | 'top-left' + | 'top-right' + | 'bottom-left' + | 'bottom-right' + | 'middle-left' + | 'middle-right' + isOpen: boolean + setIsOpen: (isOpen: boolean) => void + hideUntilHover: boolean +} + +type TriggerRender = + | JSX.Element + | ((el: HTMLElement, props: TriggerProps) => JSX.Element) + export type TanStackDevtoolsReactPlugin = Omit< TanStackDevtoolsPlugin, 'render' | 'name' @@ -57,6 +76,13 @@ export type TanStackDevtoolsReactPlugin = Omit< name: string | PluginRender } +export type TanStackDevtoolsReactConfig = Omit< + Partial, + 'triggerComponent' | 'triggerContainerClass' +> & { + triggerComponent?: TriggerRender +} + export interface TanStackDevtoolsReactInit { /** * Array of plugins to be used in the devtools. @@ -81,7 +107,7 @@ export interface TanStackDevtoolsReactInit { * initial state of the devtools when it is started for the first time. Afterwards, * the settings are persisted in local storage and changed through the settings panel. */ - config?: Partial + config?: TanStackDevtoolsReactConfig /** * Configuration for the TanStack Devtools client event bus. */ @@ -105,6 +131,17 @@ const convertRender = ( })) } +const convertTrigger = ( + Component: TriggerRender, + setComponent: React.Dispatch>, + e: HTMLElement, + props: TriggerProps, +) => { + const element = + typeof Component === 'function' ? Component(e, props) : Component + setComponent(element) +} + export const TanStackDevtools = ({ plugins, config, @@ -118,6 +155,9 @@ export const TanStackDevtools = ({ const [titleContainers, setTitleContainers] = useState< Record >({}) + const [triggerContainer, setTriggerContainer] = useState( + null, + ) const [PluginComponents, setPluginComponents] = useState< Record @@ -125,53 +165,67 @@ export const TanStackDevtools = ({ const [TitleComponents, setTitleComponents] = useState< Record >({}) + const [TriggerComponent, setTriggerComponent] = useState( + null, + ) + + const [devtools] = useState(() => { + const { triggerComponent, ...coreConfig } = config || {} + return new TanStackDevtoolsCore({ + config: { + ...coreConfig, + triggerComponent: triggerComponent + ? (el, props) => { + setTriggerContainer(el) + convertTrigger(triggerComponent, setTriggerComponent, el, { + ...props, + isOpen: props.isOpen(), + }) + } + : undefined, + }, + eventBusConfig, + plugins: plugins?.map((plugin) => { + return { + ...plugin, + name: + typeof plugin.name === 'string' + ? plugin.name + : (e, theme) => { + const id = e.getAttribute('id')! + const target = e.ownerDocument.getElementById(id) + + if (target) { + setTitleContainers((prev) => ({ + ...prev, + [id]: e, + })) + } - const [devtools] = useState( - () => - new TanStackDevtoolsCore({ - config, - eventBusConfig, - plugins: plugins?.map((plugin) => { - return { - ...plugin, - name: - typeof plugin.name === 'string' - ? plugin.name - : (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, - setTitleComponents, - e, - theme, - ) - }, - render: (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) - }, - } - }), + convertRender( + plugin.name as PluginRender, + setTitleComponents, + e, + theme, + ) + }, + render: (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) { @@ -203,6 +257,10 @@ export const TanStackDevtools = ({ createPortal(<>{TitleComponents[key]}, titleContainer), ) : null} + + {triggerContainer && TriggerComponent + ? createPortal(<>{TriggerComponent}, triggerContainer) + : null} ) } From 77145fc834bf6a2f0ca10a6391ed54ac6a73f90e Mon Sep 17 00:00:00 2001 From: akshat Date: Wed, 22 Oct 2025 09:13:08 +0530 Subject: [PATCH 2/4] docs: add docstring for triggerComponent --- packages/devtools/src/context/devtools-store.ts | 5 +++++ packages/react-devtools/src/devtools.tsx | 13 ++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/devtools/src/context/devtools-store.ts b/packages/devtools/src/context/devtools-store.ts index 172e5d28..c3e50802 100644 --- a/packages/devtools/src/context/devtools-store.ts +++ b/packages/devtools/src/context/devtools-store.ts @@ -80,6 +80,11 @@ export type DevtoolsStore = { * Whether the trigger should be completely hidden or not (you can still open with the hotkey) */ triggerHidden?: boolean + /** + * An optional custom function to render the dev tools trigger component. + * If provided, it replaces the default trigger button. + * @default undefined + */ triggerComponent?: (el: HTMLElement, props: TriggerProps) => void } state: { diff --git a/packages/react-devtools/src/devtools.tsx b/packages/react-devtools/src/devtools.tsx index dc973896..3d0e618b 100644 --- a/packages/react-devtools/src/devtools.tsx +++ b/packages/react-devtools/src/devtools.tsx @@ -78,8 +78,19 @@ export type TanStackDevtoolsReactPlugin = Omit< export type TanStackDevtoolsReactConfig = Omit< Partial, - 'triggerComponent' | 'triggerContainerClass' + 'triggerComponent' > & { + /** + * Optional custom trigger component for the devtools. + * It can be a React element or a function that renders one. + * + * Example: + * ```jsx + * { + * triggerComponent: , + * } + * ``` + */ triggerComponent?: TriggerRender } From a8b6a43437cbfce452e53db1b9f92d6019aad127 Mon Sep 17 00:00:00 2001 From: akshat Date: Wed, 22 Oct 2025 09:54:09 +0530 Subject: [PATCH 3/4] fix: remove export from unused exported type --- packages/react-devtools/src/devtools.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools/src/devtools.tsx b/packages/react-devtools/src/devtools.tsx index bd295d9a..a37b7585 100644 --- a/packages/react-devtools/src/devtools.tsx +++ b/packages/react-devtools/src/devtools.tsx @@ -76,7 +76,7 @@ export type TanStackDevtoolsReactPlugin = Omit< name: string | PluginRender } -export type TanStackDevtoolsReactConfig = Omit< +type TanStackDevtoolsReactConfig = Omit< Partial, 'triggerComponent' > & { From 9a81bbaaac1deee488842dec85fa5b4977bfc817 Mon Sep 17 00:00:00 2001 From: akshat Date: Wed, 22 Oct 2025 09:57:29 +0530 Subject: [PATCH 4/4] chore: changeset --- .changeset/rare-moles-rest.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/rare-moles-rest.md diff --git a/.changeset/rare-moles-rest.md b/.changeset/rare-moles-rest.md new file mode 100644 index 00000000..114152cd --- /dev/null +++ b/.changeset/rare-moles-rest.md @@ -0,0 +1,6 @@ +--- +'@tanstack/react-devtools': minor +'@tanstack/devtools': minor +--- + +added optional trigger component in config