From bc70ce7be5989448a44ac0a503ec6d39a3bcd6fd Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Sun, 27 Sep 2026 13:44:38 -0500 Subject: [PATCH 1/2] fix: default standalone devtools panel props --- .changeset/dirty-berries-mate.md | 7 ++ docs/devtools.md | 14 ++++ .../src/PreactHotkeysDevtools.tsx | 18 ++++- .../tests/panel.test.ts | 55 +++++++++++++++ .../src/ReactHotkeysDevtools.tsx | 18 ++++- .../tests/panel.test.ts | 61 ++++++++++++++++ .../src/SolidHotkeysDevtools.tsx | 17 ++++- .../tests/panel.test.ts | 69 +++++++++++++++++++ 8 files changed, 250 insertions(+), 9 deletions(-) create mode 100644 .changeset/dirty-berries-mate.md create mode 100644 packages/preact-hotkeys-devtools/tests/panel.test.ts create mode 100644 packages/react-hotkeys-devtools/tests/panel.test.ts create mode 100644 packages/solid-hotkeys-devtools/tests/panel.test.ts diff --git a/.changeset/dirty-berries-mate.md b/.changeset/dirty-berries-mate.md new file mode 100644 index 00000000..9d857060 --- /dev/null +++ b/.changeset/dirty-berries-mate.md @@ -0,0 +1,7 @@ +--- +'@tanstack/react-hotkeys-devtools': patch +'@tanstack/preact-hotkeys-devtools': patch +'@tanstack/solid-hotkeys-devtools': patch +--- + +Allow standalone HotkeysDevtoolsPanel components without props, supplying dark theme and open defaults while preserving props from the Devtools dock. diff --git a/docs/devtools.md b/docs/devtools.md index 2a3aabb3..2e66e21f 100644 --- a/docs/devtools.md +++ b/docs/devtools.md @@ -97,6 +97,20 @@ import { HotkeysDevtoolsPanel } from '@tanstack/vue-hotkeys-devtools' For React, Preact, and Solid, the Hotkeys panel appears alongside any other TanStack devtools plugins you have installed. +### Standalone panel + +React, Preact, and Solid also export `HotkeysDevtoolsPanel` for rendering without the TanStack Devtools dock: + +```tsx +import { HotkeysDevtoolsPanel } from '@tanstack/react-hotkeys-devtools' + +function DebugPanel() { + return +} +``` + +Use the corresponding `@tanstack/preact-hotkeys-devtools` or `@tanstack/solid-hotkeys-devtools` import for those frameworks. Both props are optional: `theme` defaults to `'dark'` and `devtoolsOpen` defaults to `true`. Pass `theme="light"` to select the light theme. When using `hotkeysDevtoolsPlugin()`, the dock supplies these props and its values take precedence over the standalone defaults. + ## Production builds In production builds, the framework devtools adapters return no-op implementations, so they don't affect your bundle's behavior. diff --git a/packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx b/packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx index 39708a03..861fdf3f 100644 --- a/packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx +++ b/packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx @@ -1,9 +1,10 @@ +import { h } from 'preact' import { createPreactPanel } from '@tanstack/devtools-utils/preact' import { HotkeysDevtoolsCore } from '@tanstack/hotkeys-devtools' import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/preact' import type { JSX } from 'preact' -export interface HotkeysDevtoolsPreactInit extends DevtoolsPanelProps {} +export interface HotkeysDevtoolsPreactInit extends Partial {} type HotkeysDevtoolsPanelComponent = ( props: HotkeysDevtoolsPreactInit, @@ -11,5 +12,16 @@ type HotkeysDevtoolsPanelComponent = ( const panels = createPreactPanel(HotkeysDevtoolsCore) -export const HotkeysDevtoolsPanel: HotkeysDevtoolsPanelComponent = panels[0] -export const HotkeysDevtoolsPanelNoOp: HotkeysDevtoolsPanelComponent = panels[1] +function withDefaults( + Panel: (typeof panels)[number], +): HotkeysDevtoolsPanelComponent { + return (props) => + h(Panel, { + ...props, + theme: props.theme ?? 'dark', + devtoolsOpen: props.devtoolsOpen ?? true, + }) +} + +export const HotkeysDevtoolsPanel = withDefaults(panels[0]) +export const HotkeysDevtoolsPanelNoOp = withDefaults(panels[1]) diff --git a/packages/preact-hotkeys-devtools/tests/panel.test.ts b/packages/preact-hotkeys-devtools/tests/panel.test.ts new file mode 100644 index 00000000..5179503e --- /dev/null +++ b/packages/preact-hotkeys-devtools/tests/panel.test.ts @@ -0,0 +1,55 @@ +import { h, render } from 'preact' +import { act } from 'preact/test-utils' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { + HotkeysDevtoolsPanel, + HotkeysDevtoolsPanelNoOp, +} from '../src/PreactHotkeysDevtools' +import { hotkeysDevtoolsPlugin } from '../src/plugin' + +const core = vi.hoisted(() => ({ mount: vi.fn(), unmount: vi.fn() })) +vi.mock('@tanstack/hotkeys-devtools', () => ({ + HotkeysDevtoolsCore: class { + mount = core.mount + unmount = core.unmount + }, +})) + +const emptyProps: Parameters[0] = {} +let host: HTMLDivElement +beforeEach(() => { + vi.clearAllMocks() + host = document.createElement('div') + document.body.append(host) +}) +afterEach(() => { + act(() => render(null, host)) + host.remove() +}) + +it('mounts a standalone panel with default props', () => { + act(() => render(h(HotkeysDevtoolsPanel, emptyProps), host)) + expect(core.mount).toHaveBeenCalledOnce() + expect(core.mount.mock.calls[0]![1]).toMatchObject({ + theme: 'dark', + devtoolsOpen: true, + }) + act(() => render(null, host)) + expect(core.unmount).toHaveBeenCalledOnce() +}) + +it('preserves props supplied by the devtools dock', () => { + const plugin = hotkeysDevtoolsPlugin() + act(() => + render(plugin.render(host, { theme: 'light', devtoolsOpen: false }), host), + ) + expect(core.mount.mock.calls[0]![1]).toMatchObject({ + theme: 'light', + devtoolsOpen: false, + }) +}) + +it('keeps the no-op panel inert with empty props', () => { + act(() => render(h(HotkeysDevtoolsPanelNoOp, emptyProps), host)) + expect(core.mount).not.toHaveBeenCalled() +}) diff --git a/packages/react-hotkeys-devtools/src/ReactHotkeysDevtools.tsx b/packages/react-hotkeys-devtools/src/ReactHotkeysDevtools.tsx index 33c7fb53..43904593 100644 --- a/packages/react-hotkeys-devtools/src/ReactHotkeysDevtools.tsx +++ b/packages/react-hotkeys-devtools/src/ReactHotkeysDevtools.tsx @@ -1,9 +1,10 @@ +import { createElement } from 'react' import { createReactPanel } from '@tanstack/devtools-utils/react' import { HotkeysDevtoolsCore } from '@tanstack/hotkeys-devtools' import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/react' import type { JSX } from 'react' -export interface HotkeysDevtoolsReactInit extends DevtoolsPanelProps {} +export interface HotkeysDevtoolsReactInit extends Partial {} type HotkeysDevtoolsPanelComponent = ( props: HotkeysDevtoolsReactInit, @@ -11,5 +12,16 @@ type HotkeysDevtoolsPanelComponent = ( const panels = createReactPanel(HotkeysDevtoolsCore) -export const HotkeysDevtoolsPanel: HotkeysDevtoolsPanelComponent = panels[0] -export const HotkeysDevtoolsPanelNoOp: HotkeysDevtoolsPanelComponent = panels[1] +function withDefaults( + Panel: (typeof panels)[number], +): HotkeysDevtoolsPanelComponent { + return (props) => + createElement(Panel, { + ...props, + theme: props.theme ?? 'dark', + devtoolsOpen: props.devtoolsOpen ?? true, + }) +} + +export const HotkeysDevtoolsPanel = withDefaults(panels[0]) +export const HotkeysDevtoolsPanelNoOp = withDefaults(panels[1]) diff --git a/packages/react-hotkeys-devtools/tests/panel.test.ts b/packages/react-hotkeys-devtools/tests/panel.test.ts new file mode 100644 index 00000000..b194663f --- /dev/null +++ b/packages/react-hotkeys-devtools/tests/panel.test.ts @@ -0,0 +1,61 @@ +import { act, createElement } from 'react' +import { createRoot } from 'react-dom/client' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { + HotkeysDevtoolsPanel, + HotkeysDevtoolsPanelNoOp, +} from '../src/ReactHotkeysDevtools' +import { hotkeysDevtoolsPlugin } from '../src/plugin' +import type { Root } from 'react-dom/client' + +const core = vi.hoisted(() => ({ mount: vi.fn(), unmount: vi.fn() })) +vi.mock('@tanstack/hotkeys-devtools', () => ({ + HotkeysDevtoolsCore: class { + mount = core.mount + unmount = core.unmount + }, +})) + +const emptyProps: Parameters[0] = {} +let host: HTMLDivElement +let root: Root +beforeEach(() => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) + vi.clearAllMocks() + host = document.createElement('div') + document.body.append(host) + root = createRoot(host) +}) +afterEach(async () => { + await act(() => root.unmount()) + host.remove() + vi.unstubAllGlobals() +}) + +it('mounts a standalone panel with default props', async () => { + await act(() => root.render(createElement(HotkeysDevtoolsPanel, emptyProps))) + expect(core.mount).toHaveBeenCalledOnce() + expect(core.mount.mock.calls[0]![1]).toMatchObject({ + theme: 'dark', + devtoolsOpen: true, + }) + await act(() => root.render(null)) +}) + +it('preserves props supplied by the devtools dock', async () => { + const plugin = hotkeysDevtoolsPlugin() + await act(() => + root.render(plugin.render(host, { theme: 'light', devtoolsOpen: false })), + ) + expect(core.mount.mock.calls[0]![1]).toMatchObject({ + theme: 'light', + devtoolsOpen: false, + }) +}) + +it('keeps the no-op panel inert with empty props', async () => { + await act(() => + root.render(createElement(HotkeysDevtoolsPanelNoOp, emptyProps)), + ) + expect(core.mount).not.toHaveBeenCalled() +}) diff --git a/packages/solid-hotkeys-devtools/src/SolidHotkeysDevtools.tsx b/packages/solid-hotkeys-devtools/src/SolidHotkeysDevtools.tsx index 60d152e1..71d2a3dc 100644 --- a/packages/solid-hotkeys-devtools/src/SolidHotkeysDevtools.tsx +++ b/packages/solid-hotkeys-devtools/src/SolidHotkeysDevtools.tsx @@ -1,9 +1,10 @@ +import { createComponent, mergeProps } from 'solid-js' import { createSolidPanel } from '@tanstack/devtools-utils/solid' import { HotkeysDevtoolsCore } from '@tanstack/hotkeys-devtools' import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/solid' import type { JSX } from 'solid-js' -export interface HotkeysDevtoolsSolidInit extends DevtoolsPanelProps {} +export interface HotkeysDevtoolsSolidInit extends Partial {} type HotkeysDevtoolsPanelComponent = ( props: HotkeysDevtoolsSolidInit, @@ -11,5 +12,15 @@ type HotkeysDevtoolsPanelComponent = ( const panels = createSolidPanel(HotkeysDevtoolsCore) -export const HotkeysDevtoolsPanel: HotkeysDevtoolsPanelComponent = panels[0] -export const HotkeysDevtoolsPanelNoOp: HotkeysDevtoolsPanelComponent = panels[1] +function withDefaults( + Panel: (typeof panels)[number], +): HotkeysDevtoolsPanelComponent { + return (props) => + createComponent( + Panel, + mergeProps({ theme: 'dark' as const, devtoolsOpen: true }, props), + ) +} + +export const HotkeysDevtoolsPanel = withDefaults(panels[0]) +export const HotkeysDevtoolsPanelNoOp = withDefaults(panels[1]) diff --git a/packages/solid-hotkeys-devtools/tests/panel.test.ts b/packages/solid-hotkeys-devtools/tests/panel.test.ts new file mode 100644 index 00000000..fb694253 --- /dev/null +++ b/packages/solid-hotkeys-devtools/tests/panel.test.ts @@ -0,0 +1,69 @@ +import { createComponent, createSignal } from 'solid-js' +import { render } from 'solid-js/web' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { + HotkeysDevtoolsPanel, + HotkeysDevtoolsPanelNoOp, +} from '../src/SolidHotkeysDevtools' +import { hotkeysDevtoolsPlugin } from '../src/plugin' + +const core = vi.hoisted(() => ({ mount: vi.fn(), unmount: vi.fn() })) +vi.mock('@tanstack/hotkeys-devtools', () => ({ + HotkeysDevtoolsCore: class { + mount = core.mount + unmount = core.unmount + }, +})) + +const emptyProps: Parameters[0] = {} +let host: HTMLDivElement +let dispose: () => void +beforeEach(() => { + vi.clearAllMocks() + host = document.createElement('div') + document.body.append(host) + dispose = () => {} +}) +afterEach(() => { + dispose() + host.remove() +}) + +it('mounts a standalone panel with default props', () => { + dispose = render( + () => createComponent(HotkeysDevtoolsPanel, emptyProps), + host, + ) + expect(core.mount).toHaveBeenCalledOnce() + expect(core.mount.mock.calls[0]![1]).toMatchObject({ + theme: 'dark', + devtoolsOpen: true, + }) +}) + +it('preserves reactive props supplied by the devtools dock', () => { + const [theme, setTheme] = createSignal<'light' | 'dark'>('light') + const plugin = hotkeysDevtoolsPlugin() + dispose = render( + () => + plugin.render(host, { + get theme() { + return theme() + }, + devtoolsOpen: false, + }), + host, + ) + const props = core.mount.mock.calls[0]![1] + expect(props).toMatchObject({ theme: 'light', devtoolsOpen: false }) + setTheme('dark') + expect(props.theme).toBe('dark') +}) + +it('keeps the no-op panel inert with empty props', () => { + dispose = render( + () => createComponent(HotkeysDevtoolsPanelNoOp, emptyProps), + host, + ) + expect(core.mount).not.toHaveBeenCalled() +}) From a880356c874c92631274dc9c5361a7a2742e7ac4 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Sun, 27 Sep 2026 14:18:53 -0500 Subject: [PATCH 2/2] fix: accept omitted devtools panel props --- docs/devtools.md | 2 +- .../src/PreactHotkeysDevtools.tsx | 4 ++-- .../preact-hotkeys-devtools/tests/panel.test.ts | 14 +++++++++++++- .../src/ReactHotkeysDevtools.tsx | 4 ++-- .../react-hotkeys-devtools/tests/panel.test.ts | 16 +++++++++++++++- .../src/SolidHotkeysDevtools.tsx | 4 ++-- .../solid-hotkeys-devtools/tests/panel.test.ts | 15 ++++++++++++++- 7 files changed, 49 insertions(+), 10 deletions(-) diff --git a/docs/devtools.md b/docs/devtools.md index 2e66e21f..ece7427e 100644 --- a/docs/devtools.md +++ b/docs/devtools.md @@ -109,7 +109,7 @@ function DebugPanel() { } ``` -Use the corresponding `@tanstack/preact-hotkeys-devtools` or `@tanstack/solid-hotkeys-devtools` import for those frameworks. Both props are optional: `theme` defaults to `'dark'` and `devtoolsOpen` defaults to `true`. Pass `theme="light"` to select the light theme. When using `hotkeysDevtoolsPlugin()`, the dock supplies these props and its values take precedence over the standalone defaults. +Use the corresponding `@tanstack/preact-hotkeys-devtools` or `@tanstack/solid-hotkeys-devtools` import for those frameworks. The panel and no-op component also accept an omitted props argument during rendering. Both props are optional: `theme` defaults to `'dark'` and `devtoolsOpen` defaults to `true`. Pass `theme="light"` to select the light theme. When using `hotkeysDevtoolsPlugin()`, the dock supplies these props and its values take precedence over the standalone defaults. ## Production builds diff --git a/packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx b/packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx index 861fdf3f..935babad 100644 --- a/packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx +++ b/packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx @@ -7,7 +7,7 @@ import type { JSX } from 'preact' export interface HotkeysDevtoolsPreactInit extends Partial {} type HotkeysDevtoolsPanelComponent = ( - props: HotkeysDevtoolsPreactInit, + props?: HotkeysDevtoolsPreactInit, ) => JSX.Element const panels = createPreactPanel(HotkeysDevtoolsCore) @@ -15,7 +15,7 @@ const panels = createPreactPanel(HotkeysDevtoolsCore) function withDefaults( Panel: (typeof panels)[number], ): HotkeysDevtoolsPanelComponent { - return (props) => + return (props = {}) => h(Panel, { ...props, theme: props.theme ?? 'dark', diff --git a/packages/preact-hotkeys-devtools/tests/panel.test.ts b/packages/preact-hotkeys-devtools/tests/panel.test.ts index 5179503e..977e5cbd 100644 --- a/packages/preact-hotkeys-devtools/tests/panel.test.ts +++ b/packages/preact-hotkeys-devtools/tests/panel.test.ts @@ -15,7 +15,7 @@ vi.mock('@tanstack/hotkeys-devtools', () => ({ }, })) -const emptyProps: Parameters[0] = {} +const emptyProps: NonNullable[0]> = {} let host: HTMLDivElement beforeEach(() => { vi.clearAllMocks() @@ -53,3 +53,15 @@ it('keeps the no-op panel inert with empty props', () => { act(() => render(h(HotkeysDevtoolsPanelNoOp, emptyProps), host)) expect(core.mount).not.toHaveBeenCalled() }) + +it('accepts omitted props for real and no-op panels during render', () => { + const Standalone = () => HotkeysDevtoolsPanel() + const NoOp = () => HotkeysDevtoolsPanelNoOp() + act(() => render(h(Standalone, {}), host)) + expect(core.mount.mock.calls[0]![1]).toMatchObject({ + theme: 'dark', + devtoolsOpen: true, + }) + act(() => render(h(NoOp, {}), host)) + expect(core.mount).toHaveBeenCalledOnce() +}) diff --git a/packages/react-hotkeys-devtools/src/ReactHotkeysDevtools.tsx b/packages/react-hotkeys-devtools/src/ReactHotkeysDevtools.tsx index 43904593..0f87279f 100644 --- a/packages/react-hotkeys-devtools/src/ReactHotkeysDevtools.tsx +++ b/packages/react-hotkeys-devtools/src/ReactHotkeysDevtools.tsx @@ -7,7 +7,7 @@ import type { JSX } from 'react' export interface HotkeysDevtoolsReactInit extends Partial {} type HotkeysDevtoolsPanelComponent = ( - props: HotkeysDevtoolsReactInit, + props?: HotkeysDevtoolsReactInit, ) => JSX.Element const panels = createReactPanel(HotkeysDevtoolsCore) @@ -15,7 +15,7 @@ const panels = createReactPanel(HotkeysDevtoolsCore) function withDefaults( Panel: (typeof panels)[number], ): HotkeysDevtoolsPanelComponent { - return (props) => + return (props = {}) => createElement(Panel, { ...props, theme: props.theme ?? 'dark', diff --git a/packages/react-hotkeys-devtools/tests/panel.test.ts b/packages/react-hotkeys-devtools/tests/panel.test.ts index b194663f..ae3176a9 100644 --- a/packages/react-hotkeys-devtools/tests/panel.test.ts +++ b/packages/react-hotkeys-devtools/tests/panel.test.ts @@ -16,7 +16,9 @@ vi.mock('@tanstack/hotkeys-devtools', () => ({ }, })) -const emptyProps: Parameters[0] = {} +const emptyProps = {} satisfies NonNullable< + Parameters[0] +> let host: HTMLDivElement let root: Root beforeEach(() => { @@ -59,3 +61,15 @@ it('keeps the no-op panel inert with empty props', async () => { ) expect(core.mount).not.toHaveBeenCalled() }) + +it('accepts omitted props for real and no-op panels during render', async () => { + const Standalone = () => HotkeysDevtoolsPanel() + const NoOp = () => HotkeysDevtoolsPanelNoOp() + await act(() => root.render(createElement(Standalone))) + expect(core.mount.mock.calls[0]![1]).toMatchObject({ + theme: 'dark', + devtoolsOpen: true, + }) + await act(() => root.render(createElement(NoOp))) + expect(core.mount).toHaveBeenCalledOnce() +}) diff --git a/packages/solid-hotkeys-devtools/src/SolidHotkeysDevtools.tsx b/packages/solid-hotkeys-devtools/src/SolidHotkeysDevtools.tsx index 71d2a3dc..4c6972fb 100644 --- a/packages/solid-hotkeys-devtools/src/SolidHotkeysDevtools.tsx +++ b/packages/solid-hotkeys-devtools/src/SolidHotkeysDevtools.tsx @@ -7,7 +7,7 @@ import type { JSX } from 'solid-js' export interface HotkeysDevtoolsSolidInit extends Partial {} type HotkeysDevtoolsPanelComponent = ( - props: HotkeysDevtoolsSolidInit, + props?: HotkeysDevtoolsSolidInit, ) => JSX.Element const panels = createSolidPanel(HotkeysDevtoolsCore) @@ -15,7 +15,7 @@ const panels = createSolidPanel(HotkeysDevtoolsCore) function withDefaults( Panel: (typeof panels)[number], ): HotkeysDevtoolsPanelComponent { - return (props) => + return (props = {}) => createComponent( Panel, mergeProps({ theme: 'dark' as const, devtoolsOpen: true }, props), diff --git a/packages/solid-hotkeys-devtools/tests/panel.test.ts b/packages/solid-hotkeys-devtools/tests/panel.test.ts index fb694253..94713487 100644 --- a/packages/solid-hotkeys-devtools/tests/panel.test.ts +++ b/packages/solid-hotkeys-devtools/tests/panel.test.ts @@ -15,7 +15,9 @@ vi.mock('@tanstack/hotkeys-devtools', () => ({ }, })) -const emptyProps: Parameters[0] = {} +const emptyProps = {} satisfies NonNullable< + Parameters[0] +> let host: HTMLDivElement let dispose: () => void beforeEach(() => { @@ -67,3 +69,14 @@ it('keeps the no-op panel inert with empty props', () => { ) expect(core.mount).not.toHaveBeenCalled() }) + +it('accepts omitted props for real and no-op panels during render', () => { + dispose = render(() => HotkeysDevtoolsPanel(), host) + expect(core.mount.mock.calls[0]![1]).toMatchObject({ + theme: 'dark', + devtoolsOpen: true, + }) + dispose() + dispose = render(() => HotkeysDevtoolsPanelNoOp(), host) + expect(core.mount).toHaveBeenCalledOnce() +})