From 87581785b0313914d26c4d010c3b98fc03cc8ebf Mon Sep 17 00:00:00 2001 From: Zaidhaan Hussain Date: Sat, 29 Nov 2025 11:28:04 +0800 Subject: [PATCH 1/8] feat: inspector hotkey configuration --- .../src/components/source-inspector.tsx | 33 ++++++++++++++++--- .../devtools/src/context/devtools-store.ts | 5 +++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/devtools/src/components/source-inspector.tsx b/packages/devtools/src/components/source-inspector.tsx index b5f51ecb..9f4b4026 100644 --- a/packages/devtools/src/components/source-inspector.tsx +++ b/packages/devtools/src/components/source-inspector.tsx @@ -3,8 +3,12 @@ import { createStore } from 'solid-js/store' import { createElementSize } from '@solid-primitives/resize-observer' import { useKeyDownList } from '@solid-primitives/keyboard' import { createEventListener } from '@solid-primitives/event-listener' +import { useDevtoolsSettings } from '../context/use-devtools-context' +import { keyboardModifiers } from '../context/devtools-store' +import { getAllPermutations } from '../utils/sanitize' export const SourceInspector = () => { + const { settings } = useDevtoolsSettings() const highlightStateInit = () => ({ element: null as HTMLElement | null, bounding: { width: 0, height: 0, left: 0, top: 0 }, @@ -25,12 +29,31 @@ export const SourceInspector = () => { }) const downList = useKeyDownList() + + const hotKeyPermutations = createMemo(() => { + const modifiers = settings().inspectHotkey.filter((key) => + keyboardModifiers.includes(key as any), + ).map((key) => key.toUpperCase()) + const nonModifiers = settings().inspectHotkey.filter( + (key) => !keyboardModifiers.includes(key as any), + ).map((key) => key.toUpperCase()) + + const allModifierCombinations = getAllPermutations(modifiers) + + const permutations = allModifierCombinations.map(c => [...c, ...nonModifiers]); + + return permutations + }) + const isHighlightingKeysHeld = createMemo(() => { - const keys = downList() - const isShiftHeld = keys.includes('SHIFT') - const isCtrlHeld = keys.includes('CONTROL') - const isMetaHeld = keys.includes('META') - return isShiftHeld && (isCtrlHeld || isMetaHeld) + const downKeys = downList() + return hotKeyPermutations().some( + (combo) => + // every key in the combo must be pressed + combo.every(key => downKeys.includes(key)) && + // and no extra keys beyond the combo + downKeys.every(key => combo.includes(key)) + ); }) createEffect(() => { diff --git a/packages/devtools/src/context/devtools-store.ts b/packages/devtools/src/context/devtools-store.ts index da30780a..df3280ca 100644 --- a/packages/devtools/src/context/devtools-store.ts +++ b/packages/devtools/src/context/devtools-store.ts @@ -50,6 +50,10 @@ export type DevtoolsStore = { * @default "shift+a" */ openHotkey: Array + /** + * The hotkey to open the source inspector + */ + inspectHotkey: Array /** * Whether to require the URL flag to open the dev tools * @default false @@ -93,6 +97,7 @@ export const initialState: DevtoolsStore = { position: 'bottom-right', panelLocation: 'bottom', openHotkey: ['Shift', 'A'], + inspectHotkey: ['Shift', 'Meta'], requireUrlFlag: false, urlFlag: 'tanstack-devtools', theme: From 9b9f2ef537a1f124380ed88f6f5a7cc654afe2a0 Mon Sep 17 00:00:00 2001 From: Zaidhaan Hussain Date: Sat, 29 Nov 2025 11:45:11 +0800 Subject: [PATCH 2/8] feat: refactor hotkey handling --- .../src/components/source-inspector.tsx | 28 +-- .../devtools/src/context/devtools-store.ts | 7 +- packages/devtools/src/devtools.tsx | 19 +- packages/devtools/src/tabs/settings-tab.tsx | 193 +++++++++++++----- packages/devtools/src/utils/hotkey.ts | 77 +++++++ 5 files changed, 230 insertions(+), 94 deletions(-) create mode 100644 packages/devtools/src/utils/hotkey.ts diff --git a/packages/devtools/src/components/source-inspector.tsx b/packages/devtools/src/components/source-inspector.tsx index 9f4b4026..519bcb51 100644 --- a/packages/devtools/src/components/source-inspector.tsx +++ b/packages/devtools/src/components/source-inspector.tsx @@ -3,9 +3,9 @@ import { createStore } from 'solid-js/store' import { createElementSize } from '@solid-primitives/resize-observer' import { useKeyDownList } from '@solid-primitives/keyboard' import { createEventListener } from '@solid-primitives/event-listener' + import { useDevtoolsSettings } from '../context/use-devtools-context' -import { keyboardModifiers } from '../context/devtools-store' -import { getAllPermutations } from '../utils/sanitize' +import { isHotkeyCombinationPressed } from '../utils/hotkey' export const SourceInspector = () => { const { settings } = useDevtoolsSettings() @@ -30,30 +30,8 @@ export const SourceInspector = () => { const downList = useKeyDownList() - const hotKeyPermutations = createMemo(() => { - const modifiers = settings().inspectHotkey.filter((key) => - keyboardModifiers.includes(key as any), - ).map((key) => key.toUpperCase()) - const nonModifiers = settings().inspectHotkey.filter( - (key) => !keyboardModifiers.includes(key as any), - ).map((key) => key.toUpperCase()) - - const allModifierCombinations = getAllPermutations(modifiers) - - const permutations = allModifierCombinations.map(c => [...c, ...nonModifiers]); - - return permutations - }) - const isHighlightingKeysHeld = createMemo(() => { - const downKeys = downList() - return hotKeyPermutations().some( - (combo) => - // every key in the combo must be pressed - combo.every(key => downKeys.includes(key)) && - // and no extra keys beyond the combo - downKeys.every(key => combo.includes(key)) - ); + return isHotkeyCombinationPressed(downList(), settings().inspectHotkey) }) createEffect(() => { diff --git a/packages/devtools/src/context/devtools-store.ts b/packages/devtools/src/context/devtools-store.ts index df3280ca..d1d231c9 100644 --- a/packages/devtools/src/context/devtools-store.ts +++ b/packages/devtools/src/context/devtools-store.ts @@ -1,13 +1,15 @@ import type { TabName } from '../tabs' import type { TanStackDevtoolsPlugin } from './devtools-context' -type ModifierKey = 'Alt' | 'Control' | 'Meta' | 'Shift' +type ModifierKey = 'Alt' | 'Control' | 'Meta' | 'Shift' | 'CtrlOrMeta' type KeyboardKey = ModifierKey | (string & {}) +export type { ModifierKey, KeyboardKey } export const keyboardModifiers: Array = [ 'Alt', 'Control', 'Meta', 'Shift', + 'CtrlOrMeta', ] type TriggerPosition = @@ -52,6 +54,7 @@ export type DevtoolsStore = { openHotkey: Array /** * The hotkey to open the source inspector + * @default ["Shift", "CtrlOrMeta"] */ inspectHotkey: Array /** @@ -97,7 +100,7 @@ export const initialState: DevtoolsStore = { position: 'bottom-right', panelLocation: 'bottom', openHotkey: ['Shift', 'A'], - inspectHotkey: ['Shift', 'Meta'], + inspectHotkey: ['Shift', 'CtrlOrMeta'], requireUrlFlag: false, urlFlag: 'tanstack-devtools', theme: diff --git a/packages/devtools/src/devtools.tsx b/packages/devtools/src/devtools.tsx index 7276854a..5a68a04d 100644 --- a/packages/devtools/src/devtools.tsx +++ b/packages/devtools/src/devtools.tsx @@ -3,6 +3,7 @@ import { createShortcut } from '@solid-primitives/keyboard' import { Portal } from 'solid-js/web' import { ThemeContextProvider } from '@tanstack/devtools-ui' import { devtoolsEventClient } from '@tanstack/devtools-client' + import { useDevtoolsSettings, useHeight, @@ -11,13 +12,12 @@ import { } from './context/use-devtools-context' import { useDisableTabbing } from './hooks/use-disable-tabbing' import { TANSTACK_DEVTOOLS } from './utils/storage' +import { getHotkeyPermutations } from './utils/hotkey' import { Trigger } from './components/trigger' import { MainPanel } from './components/main-panel' import { ContentPanel } from './components/content-panel' import { Tabs } from './components/tabs' import { TabContent } from './components/tab-content' -import { keyboardModifiers } from './context/devtools-store' -import { getAllPermutations } from './utils/sanitize' import { usePiPWindow } from './context/pip-context' import { SourceInspector } from './components/source-inspector' @@ -165,18 +165,9 @@ export default function DevTools() { } }) createEffect(() => { - // we create all combinations of modifiers - const modifiers = settings().openHotkey.filter((key) => - keyboardModifiers.includes(key as any), - ) - const nonModifiers = settings().openHotkey.filter( - (key) => !keyboardModifiers.includes(key as any), - ) - - const allModifierCombinations = getAllPermutations(modifiers) - - for (const combination of allModifierCombinations) { - const permutation = [...combination, ...nonModifiers] + const permutations = getHotkeyPermutations(settings().openHotkey) + + for (const permutation of permutations) { createShortcut(permutation, () => { toggleOpen() }) diff --git a/packages/devtools/src/tabs/settings-tab.tsx b/packages/devtools/src/tabs/settings-tab.tsx index 147e8f7d..44b02e8c 100644 --- a/packages/devtools/src/tabs/settings-tab.tsx +++ b/packages/devtools/src/tabs/settings-tab.tsx @@ -1,4 +1,4 @@ -import { Show, createMemo } from 'solid-js' +import { Show } from 'solid-js' import { Button, Checkbox, @@ -19,29 +19,78 @@ import { import { useDevtoolsSettings } from '../context/use-devtools-context' import { uppercaseFirstLetter } from '../utils/sanitize' import { useStyles } from '../styles/use-styles' -import type { ModifierKey } from '@solid-primitives/keyboard' +import type { KeyboardKey } from '../context/devtools-store' export const SettingsTab = () => { const { setSettings, settings } = useDevtoolsSettings() const styles = useStyles() - const hotkey = createMemo(() => settings().openHotkey) - const modifiers: Array = ['Control', 'Alt', 'Meta', 'Shift'] - const changeHotkey = (newHotkey: ModifierKey) => () => { - if (hotkey().includes(newHotkey)) { + + const modifiers: Array = [ + 'Control', + 'Alt', + 'Meta', + 'Shift', + 'CtrlOrMeta', + ] + + const createHotkeyHandler = + (hotkeyKey: 'openHotkey' | 'inspectHotkey') => + (newHotkey: KeyboardKey) => + () => { + const hotkey = settings()[hotkeyKey] + if (hotkey.includes(newHotkey)) { + return setSettings({ + [hotkeyKey]: hotkey.filter((key) => key !== newHotkey), + }) + } + const existingModifiers = hotkey.filter((key) => + modifiers.includes(key as any), + ) + const otherKeys = hotkey.filter( + (key) => !modifiers.includes(key as any), + ) + setSettings({ + [hotkeyKey]: [...existingModifiers, newHotkey, ...otherKeys], + }) + } + + const createHotkeyDisplay = + (hotkeyKey: 'openHotkey' | 'inspectHotkey') => () => { + return settings()[hotkeyKey].join(' + ') + } + + const createHotkeyNonModifierValue = + (hotkeyKey: 'openHotkey' | 'inspectHotkey') => () => { + return settings()[hotkeyKey] + .filter((key) => !modifiers.includes(key as any)) + .join('+') + } + + const createHotkeyInputHandler = + (hotkeyKey: 'openHotkey' | 'inspectHotkey') => (e: string) => { + const makeModifierArray = (key: string) => { + if (key.length === 1) return [uppercaseFirstLetter(key)] + const modifiersArray: Array = [] + for (const character of key) { + const newLetter = uppercaseFirstLetter(character) + if (!modifiersArray.includes(newLetter)) + modifiersArray.push(newLetter) + } + return modifiersArray + } + const hotkey = settings()[hotkeyKey] + const hotkeyModifiers = hotkey.filter((key) => + modifiers.includes(key as any), + ) + const newKeys = e + .split('+') + .flatMap((key) => makeModifierArray(key)) + .filter(Boolean) return setSettings({ - openHotkey: hotkey().filter((key) => key !== newHotkey), + [hotkeyKey]: [...hotkeyModifiers, ...newKeys], }) } - const existingModifiers = hotkey().filter((key) => - modifiers.includes(key as any), - ) - const otherModifiers = hotkey().filter( - (key) => !modifiers.includes(key as any), - ) - setSettings({ - openHotkey: [...existingModifiers, newHotkey, ...otherModifiers], - }) - } + return (
@@ -144,71 +193,109 @@ export const SettingsTab = () => { Customize keyboard shortcuts for quick access. + + {/* Open/Close Devtools Hotkey */}
+

Open/Close Devtools

- + +
!['Shift', 'Meta', 'Alt', 'Ctrl'].includes(key)) - .join('+')} - onChange={(e) => { - const makeModifierArray = (key: string) => { - if (key.length === 1) return [uppercaseFirstLetter(key)] - const modifiers: Array = [] - for (const character of key) { - const newLetter = uppercaseFirstLetter(character) - if (!modifiers.includes(newLetter)) modifiers.push(newLetter) - } - return modifiers - } - const modifiers = e - .split('+') - .flatMap((key) => makeModifierArray(key)) - .filter(Boolean) - return setSettings({ - openHotkey: [ - ...hotkey().filter((key) => - ['Shift', 'Meta', 'Alt', 'Ctrl'].includes(key), - ), - ...modifiers, - ], - }) - }} + value={createHotkeyNonModifierValue('openHotkey')()} + onChange={createHotkeyInputHandler('openHotkey')} + /> + Final shortcut is: {createHotkeyDisplay('openHotkey')()} +
+ + {/* Inspector Hotkey */} +
+

Source Inspector

+
+ + + + + + + +
+ - Final shortcut is: {hotkey().join(' + ')} + Final shortcut is: {createHotkeyDisplay('inspectHotkey')()}
diff --git a/packages/devtools/src/utils/hotkey.ts b/packages/devtools/src/utils/hotkey.ts new file mode 100644 index 00000000..4e8c5316 --- /dev/null +++ b/packages/devtools/src/utils/hotkey.ts @@ -0,0 +1,77 @@ +import { keyboardModifiers } from '../context/devtools-store' +import { getAllPermutations } from './sanitize' + +import type { KeyboardKey, ModifierKey } from '../context/devtools-store' + +/** + * Normalizes a keyboard key array by expanding CtrlOrMeta to platform-specific modifiers + * @param keys - The keyboard keys to normalize + * @returns An array of normalized keyboard key arrays with CtrlOrMeta expanded + */ +export const normalizeHotkey = (keys: Array): Array> => { + // Check if CtrlOrMeta is present + if (!keys.includes('CtrlOrMeta')) { + return [keys] + } + + // Expand CtrlOrMeta to both Control and Meta versions + const results: Array> = [] + + const keysWithControl = keys.map(key => key === 'CtrlOrMeta' ? 'Control' : key) + const keysWithMeta = keys.map(key => key === 'CtrlOrMeta' ? 'Meta' : key) + + results.push(keysWithControl) + results.push(keysWithMeta) + + return results +} + +/** + * Generates all keyboard permutations for a given hotkey configuration + * Handles CtrlOrMeta expansion and creates all possible combinations + * @param hotkey - The hotkey configuration + * @returns All possible keyboard combinations that should trigger the action + */ +export const getHotkeyPermutations = (hotkey: Array): Array> => { + const normalizedHotkeys = normalizeHotkey(hotkey) + const allPermutations: Array> = [] + + for (const normalizedHotkey of normalizedHotkeys) { + const modifiers = normalizedHotkey.filter((key) => + keyboardModifiers.includes(key as any), + ) as Array + + const nonModifiers = normalizedHotkey.filter( + (key) => !keyboardModifiers.includes(key as any), + ) + + const allModifierCombinations = getAllPermutations(modifiers) + const permutations = allModifierCombinations.map(c => [...c, ...nonModifiers]) + + allPermutations.push(...permutations) + } + + return allPermutations +} + +/** + * Checks if the currently pressed keys match any of the hotkey permutations + * @param downList - The list of currently pressed keys + * @param hotkey - The hotkey configuration to check against + * @returns True if the pressed keys match the hotkey + */ +export const isHotkeyCombinationPressed = ( + downList: Array, + hotkey: Array, +): boolean => { + const permutations = getHotkeyPermutations(hotkey) + const downKeys = downList.map(key => key.toUpperCase()) + + return permutations.some( + (combo) => + // every key in the combo must be pressed + combo.every(key => downKeys.includes(String(key).toUpperCase())) && + // and no extra keys beyond the combo + downKeys.every(key => combo.map(k => String(k).toUpperCase()).includes(key)), + ) +} From 30fe2dd0b36610a8a461758e0e07ecae718c502c Mon Sep 17 00:00:00 2001 From: Zaidhaan Hussain Date: Sat, 29 Nov 2025 12:40:46 +0800 Subject: [PATCH 3/8] feat: refactor hotkey UI configuration + update docs --- docs/configuration.md | 11 +- .../devtools/src/context/devtools-store.ts | 2 +- packages/devtools/src/styles/use-styles.ts | 5 + packages/devtools/src/tabs/hotkey-config.tsx | 97 ++++++++++ packages/devtools/src/tabs/settings-tab.tsx | 178 ++---------------- packages/devtools/src/utils/hotkey.ts | 58 ++---- 6 files changed, 148 insertions(+), 203 deletions(-) create mode 100644 packages/devtools/src/tabs/hotkey-config.tsx diff --git a/docs/configuration.md b/docs/configuration.md index c19be03d..20e586f9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -45,12 +45,21 @@ The `config` object is mainly focused around user interaction with the devtools - `openHotkey` - The hotkey set to open the devtools ```ts -type ModifierKey = 'Alt' | 'Control' | 'Meta' | 'Shift'; +type ModifierKey = 'Alt' | 'Control' | 'Meta' | 'Shift' | 'CtrlOrMeta'; type KeyboardKey = ModifierKey | (string & {}); { openHotkey: Array } ``` +- `inspectHotkey` - The hotkey set to open the source inspector + +```ts +type ModifierKey = 'Alt' | 'Control' | 'Meta' | 'Shift' | 'CtrlOrMeta'; +type KeyboardKey = ModifierKey | (string & {}); + +{ inspectHotkey: Array } +``` + - `requireUrlFlag` - Requires a flag present in the url to enable devtools ```ts diff --git a/packages/devtools/src/context/devtools-store.ts b/packages/devtools/src/context/devtools-store.ts index d1d231c9..f312bc6a 100644 --- a/packages/devtools/src/context/devtools-store.ts +++ b/packages/devtools/src/context/devtools-store.ts @@ -49,7 +49,7 @@ export type DevtoolsStore = { panelLocation: 'top' | 'bottom' /** * The hotkey to open the dev tools - * @default "shift+a" + * @default ["Shift", "A"] */ openHotkey: Array /** diff --git a/packages/devtools/src/styles/use-styles.ts b/packages/devtools/src/styles/use-styles.ts index b8fb32b0..cda20cad 100644 --- a/packages/devtools/src/styles/use-styles.ts +++ b/packages/devtools/src/styles/use-styles.ts @@ -466,6 +466,11 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => { display: flex; gap: 0.5rem; `, + settingsStack: css` + display: flex; + flex-direction: column; + gap: 1rem; + `, // No Plugins Fallback Styles noPluginsFallback: css` diff --git a/packages/devtools/src/tabs/hotkey-config.tsx b/packages/devtools/src/tabs/hotkey-config.tsx new file mode 100644 index 00000000..461e30ed --- /dev/null +++ b/packages/devtools/src/tabs/hotkey-config.tsx @@ -0,0 +1,97 @@ +import { Show } from 'solid-js' +import { Button, Input } from '@tanstack/devtools-ui' + +import { uppercaseFirstLetter } from '../utils/sanitize' +import { useStyles } from '../styles/use-styles' +import type { KeyboardKey } from '../context/devtools-store' + +interface HotkeyConfigProps { + title: string + description: string + hotkey: Array + modifiers: Array + onHotkeyChange: (hotkey: Array) => void +} + +const MODIFIER_DISPLAY_NAMES: Record = { + Shift: 'Shift', + Alt: 'Alt', + Meta: 'Meta', + Control: 'Control', + CtrlOrMeta: 'Ctrl Or Meta', +} + +export const HotkeyConfig = (props: HotkeyConfigProps) => { + const styles = useStyles() + + const toggleModifier = (modifier: KeyboardKey) => { + if (props.hotkey.includes(modifier)) { + props.onHotkeyChange(props.hotkey.filter((key) => key !== modifier)) + } else { + const existingModifiers = props.hotkey.filter((key) => + props.modifiers.includes(key as any), + ) + const otherKeys = props.hotkey.filter( + (key) => !props.modifiers.includes(key as any), + ) + props.onHotkeyChange([...existingModifiers, modifier, ...otherKeys]) + } + } + + const getNonModifierValue = () => { + return props.hotkey + .filter((key) => !props.modifiers.includes(key as any)) + .join('+') + } + + const handleKeyInput = (input: string) => { + const makeModifierArray = (key: string) => { + if (key.length === 1) return [uppercaseFirstLetter(key)] + const modifiersArray: Array = [] + for (const character of key) { + const newLetter = uppercaseFirstLetter(character) + if (!modifiersArray.includes(newLetter)) modifiersArray.push(newLetter) + } + return modifiersArray + } + + const hotkeyModifiers = props.hotkey.filter((key) => + props.modifiers.includes(key as any), + ) + const newKeys = input + .split('+') + .flatMap((key) => makeModifierArray(key)) + .filter(Boolean) + props.onHotkeyChange([...hotkeyModifiers, ...newKeys]) + } + + const getDisplayHotkey = () => { + return props.hotkey.join(' + ') + } + + return ( +
+

{props.description}

+
+ + {props.modifiers.map((modifier) => ( + + ))} + +
+ + Final shortcut is: {getDisplayHotkey()} +
+ ) +} diff --git a/packages/devtools/src/tabs/settings-tab.tsx b/packages/devtools/src/tabs/settings-tab.tsx index 44b02e8c..35f4164b 100644 --- a/packages/devtools/src/tabs/settings-tab.tsx +++ b/packages/devtools/src/tabs/settings-tab.tsx @@ -1,6 +1,5 @@ import { Show } from 'solid-js' import { - Button, Checkbox, Input, MainPanel, @@ -16,15 +15,16 @@ import { Link, SettingsCog, } from '@tanstack/devtools-ui/icons' + import { useDevtoolsSettings } from '../context/use-devtools-context' -import { uppercaseFirstLetter } from '../utils/sanitize' import { useStyles } from '../styles/use-styles' +import { HotkeyConfig } from './hotkey-config' import type { KeyboardKey } from '../context/devtools-store' export const SettingsTab = () => { const { setSettings, settings } = useDevtoolsSettings() const styles = useStyles() - + const modifiers: Array = [ 'Control', 'Alt', @@ -33,64 +33,6 @@ export const SettingsTab = () => { 'CtrlOrMeta', ] - const createHotkeyHandler = - (hotkeyKey: 'openHotkey' | 'inspectHotkey') => - (newHotkey: KeyboardKey) => - () => { - const hotkey = settings()[hotkeyKey] - if (hotkey.includes(newHotkey)) { - return setSettings({ - [hotkeyKey]: hotkey.filter((key) => key !== newHotkey), - }) - } - const existingModifiers = hotkey.filter((key) => - modifiers.includes(key as any), - ) - const otherKeys = hotkey.filter( - (key) => !modifiers.includes(key as any), - ) - setSettings({ - [hotkeyKey]: [...existingModifiers, newHotkey, ...otherKeys], - }) - } - - const createHotkeyDisplay = - (hotkeyKey: 'openHotkey' | 'inspectHotkey') => () => { - return settings()[hotkeyKey].join(' + ') - } - - const createHotkeyNonModifierValue = - (hotkeyKey: 'openHotkey' | 'inspectHotkey') => () => { - return settings()[hotkeyKey] - .filter((key) => !modifiers.includes(key as any)) - .join('+') - } - - const createHotkeyInputHandler = - (hotkeyKey: 'openHotkey' | 'inspectHotkey') => (e: string) => { - const makeModifierArray = (key: string) => { - if (key.length === 1) return [uppercaseFirstLetter(key)] - const modifiersArray: Array = [] - for (const character of key) { - const newLetter = uppercaseFirstLetter(character) - if (!modifiersArray.includes(newLetter)) - modifiersArray.push(newLetter) - } - return modifiersArray - } - const hotkey = settings()[hotkeyKey] - const hotkeyModifiers = hotkey.filter((key) => - modifiers.includes(key as any), - ) - const newKeys = e - .split('+') - .flatMap((key) => makeModifierArray(key)) - .filter(Boolean) - return setSettings({ - [hotkeyKey]: [...hotkeyModifiers, ...newKeys], - }) - } - return (
@@ -193,109 +135,23 @@ export const SettingsTab = () => { Customize keyboard shortcuts for quick access. - - {/* Open/Close Devtools Hotkey */} -
-

Open/Close Devtools

-
- - - - - - - -
- + setSettings({ openHotkey: hotkey })} /> - Final shortcut is: {createHotkeyDisplay('openHotkey')()} -
- {/* Inspector Hotkey */} -
-

Source Inspector

-
- - - - - - - -
- setSettings({ inspectHotkey: hotkey })} /> - Final shortcut is: {createHotkeyDisplay('inspectHotkey')()}
diff --git a/packages/devtools/src/utils/hotkey.ts b/packages/devtools/src/utils/hotkey.ts index 4e8c5316..0f533796 100644 --- a/packages/devtools/src/utils/hotkey.ts +++ b/packages/devtools/src/utils/hotkey.ts @@ -3,75 +3,53 @@ import { getAllPermutations } from './sanitize' import type { KeyboardKey, ModifierKey } from '../context/devtools-store' -/** - * Normalizes a keyboard key array by expanding CtrlOrMeta to platform-specific modifiers - * @param keys - The keyboard keys to normalize - * @returns An array of normalized keyboard key arrays with CtrlOrMeta expanded - */ +/** Expands CtrlOrMeta into separate Control and Meta variants */ export const normalizeHotkey = (keys: Array): Array> => { - // Check if CtrlOrMeta is present + // no normalization needed if CtrlOrMeta not used if (!keys.includes('CtrlOrMeta')) { return [keys] } - - // Expand CtrlOrMeta to both Control and Meta versions - const results: Array> = [] - - const keysWithControl = keys.map(key => key === 'CtrlOrMeta' ? 'Control' : key) - const keysWithMeta = keys.map(key => key === 'CtrlOrMeta' ? 'Meta' : key) - - results.push(keysWithControl) - results.push(keysWithMeta) - - return results + + return [ + keys.map(key => key === 'CtrlOrMeta' ? 'Control' : key), + keys.map(key => key === 'CtrlOrMeta' ? 'Meta' : key) + ] } /** * Generates all keyboard permutations for a given hotkey configuration * Handles CtrlOrMeta expansion and creates all possible combinations - * @param hotkey - The hotkey configuration - * @returns All possible keyboard combinations that should trigger the action */ export const getHotkeyPermutations = (hotkey: Array): Array> => { const normalizedHotkeys = normalizeHotkey(hotkey) - const allPermutations: Array> = [] - - for (const normalizedHotkey of normalizedHotkeys) { + + return normalizedHotkeys.flatMap((normalizedHotkey) => { const modifiers = normalizedHotkey.filter((key) => keyboardModifiers.includes(key as any), ) as Array - + const nonModifiers = normalizedHotkey.filter( (key) => !keyboardModifiers.includes(key as any), ) - + const allModifierCombinations = getAllPermutations(modifiers) - const permutations = allModifierCombinations.map(c => [...c, ...nonModifiers]) - - allPermutations.push(...permutations) - } - - return allPermutations + return allModifierCombinations.map(combo => [...combo, ...nonModifiers]) + }) } -/** - * Checks if the currently pressed keys match any of the hotkey permutations - * @param downList - The list of currently pressed keys - * @param hotkey - The hotkey configuration to check against - * @returns True if the pressed keys match the hotkey - */ +/** Checks if the currently pressed keys match any of the hotkey permutations */ export const isHotkeyCombinationPressed = ( - downList: Array, + keys: Array, hotkey: Array, ): boolean => { const permutations = getHotkeyPermutations(hotkey) - const downKeys = downList.map(key => key.toUpperCase()) + const pressedKeys = keys.map(key => key.toUpperCase()) return permutations.some( (combo) => // every key in the combo must be pressed - combo.every(key => downKeys.includes(String(key).toUpperCase())) && + combo.every(key => pressedKeys.includes(String(key).toUpperCase())) && // and no extra keys beyond the combo - downKeys.every(key => combo.map(k => String(k).toUpperCase()).includes(key)), + pressedKeys.every(key => combo.map(k => String(k).toUpperCase()).includes(key)), ) } From a1f9310c07367d97d47af13b636af2bf42fdfa73 Mon Sep 17 00:00:00 2001 From: Zaidhaan Hussain Date: Sat, 29 Nov 2025 12:52:48 +0800 Subject: [PATCH 4/8] chore: add some tests --- packages/devtools/src/utils/hotkey.test.ts | 109 +++++++++++++++++++++ packages/devtools/src/utils/hotkey.ts | 5 + 2 files changed, 114 insertions(+) create mode 100644 packages/devtools/src/utils/hotkey.test.ts diff --git a/packages/devtools/src/utils/hotkey.test.ts b/packages/devtools/src/utils/hotkey.test.ts new file mode 100644 index 00000000..435adb84 --- /dev/null +++ b/packages/devtools/src/utils/hotkey.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it } from 'vitest' +import { + getHotkeyPermutations, + isHotkeyCombinationPressed, + normalizeHotkey, +} from './hotkey' +import type { KeyboardKey } from '../context/devtools-store' + +describe('hotkey utilities', () => { + describe('normalizeHotkey', () => { + it('should return unchanged array when CtrlOrMeta is not present', () => { + const hotkey: Array = ['Shift', 'A'] + const result = normalizeHotkey(hotkey) + expect(result).toEqual([['Shift', 'A']]) + }) + + it('should expand CtrlOrMeta to Control and Meta variants', () => { + const hotkey: Array = ['Shift', 'CtrlOrMeta'] + const result = normalizeHotkey(hotkey) + expect(result).toHaveLength(2) + expect(result).toContainEqual(['Shift', 'Control']) + expect(result).toContainEqual(['Shift', 'Meta']) + }) + }) + + describe('getHotkeyPermutations', () => { + it('should generate permutations for modifiers in any order', () => { + const hotkey: Array = ['Shift', 'Control', 'A'] + const result = getHotkeyPermutations(hotkey) + expect(result).toContainEqual(['Shift', 'Control', 'A']) + expect(result).toContainEqual(['Control', 'Shift', 'A']) + }) + + it('should handle CtrlOrMeta expansion with multiple permutations', () => { + const hotkey: Array = ['Shift', 'CtrlOrMeta'] + const result = getHotkeyPermutations(hotkey) + expect(result).toContainEqual(['Shift', 'Control']) + expect(result).toContainEqual(['Control', 'Shift']) + expect(result).toContainEqual(['Shift', 'Meta']) + expect(result).toContainEqual(['Meta', 'Shift']) + }) + + it('should handle single key hotkey with no modifiers', () => { + const hotkey: Array = ['A'] + const result = getHotkeyPermutations(hotkey) + expect(result).toEqual([['A']]) + }) + + it('should not have duplicate permutations', () => { + const hotkey: Array = ['Shift', 'Alt', 'A'] + const result = getHotkeyPermutations(hotkey) + const stringified = result.map((combo) => JSON.stringify(combo)) + const unique = new Set(stringified) + expect(unique.size).toBe(stringified.length) + }) + }) + + describe('isHotkeyCombinationPressed', () => { + it('should match exact key combination', () => { + expect(isHotkeyCombinationPressed(['Shift', 'A'], ['Shift', 'A'])).toBe( + true, + ) + }) + + it('should be case-insensitive', () => { + expect( + isHotkeyCombinationPressed(['shift', 'a'], ['Shift', 'A']), + ).toBe(true) + }) + + it('should match regardless of modifier order', () => { + expect( + isHotkeyCombinationPressed( + ['A', 'Control', 'Shift'], + ['Shift', 'Control', 'A'], + ), + ).toBe(true) + }) + + it('should handle CtrlOrMeta with Control', () => { + expect( + isHotkeyCombinationPressed( + ['Shift', 'Control'], + ['Shift', 'CtrlOrMeta'], + ), + ).toBe(true) + }) + + it('should handle CtrlOrMeta with Meta', () => { + expect( + isHotkeyCombinationPressed(['Shift', 'Meta'], ['Shift', 'CtrlOrMeta']), + ).toBe(true) + }) + + it('should reject incomplete key combinations', () => { + expect(isHotkeyCombinationPressed(['Shift'], ['Shift', 'A'])).toBe(false) + }) + + it('should reject extra keys', () => { + expect(isHotkeyCombinationPressed(['Shift', 'A', 'B'], ['Shift', 'A'])).toBe( + false, + ) + }) + + it('should handle single key hotkey', () => { + expect(isHotkeyCombinationPressed(['A'], ['A'])).toBe(true) + }) + }) +}) diff --git a/packages/devtools/src/utils/hotkey.ts b/packages/devtools/src/utils/hotkey.ts index 0f533796..808ddb6f 100644 --- a/packages/devtools/src/utils/hotkey.ts +++ b/packages/devtools/src/utils/hotkey.ts @@ -32,6 +32,11 @@ export const getHotkeyPermutations = (hotkey: Array): Array !keyboardModifiers.includes(key as any), ) + // handle case with no modifiers (just non-modifier keys) + if (modifiers.length === 0) { + return [nonModifiers] + } + const allModifierCombinations = getAllPermutations(modifiers) return allModifierCombinations.map(combo => [...combo, ...nonModifiers]) }) From 5ab18acabd3fac937c0628713b28da87e9efc677 Mon Sep 17 00:00:00 2001 From: Zaidhaan Hussain Date: Sat, 29 Nov 2025 13:02:42 +0800 Subject: [PATCH 5/8] chore: changeset --- .changeset/curly-news-work.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/curly-news-work.md diff --git a/.changeset/curly-news-work.md b/.changeset/curly-news-work.md new file mode 100644 index 00000000..92378511 --- /dev/null +++ b/.changeset/curly-news-work.md @@ -0,0 +1,5 @@ +--- +'@tanstack/devtools': minor +--- + +add inspectHotkey to devtools configuration From 4f051d71142c9f58d85ff8d9c7f73a2f94e4f060 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:59:53 +0000 Subject: [PATCH 6/8] ci: apply automated fixes --- packages/devtools/src/utils/hotkey.test.ts | 12 ++++---- packages/devtools/src/utils/hotkey.ts | 32 +++++++++++++--------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/devtools/src/utils/hotkey.test.ts b/packages/devtools/src/utils/hotkey.test.ts index 435adb84..ea654e4b 100644 --- a/packages/devtools/src/utils/hotkey.test.ts +++ b/packages/devtools/src/utils/hotkey.test.ts @@ -63,9 +63,9 @@ describe('hotkey utilities', () => { }) it('should be case-insensitive', () => { - expect( - isHotkeyCombinationPressed(['shift', 'a'], ['Shift', 'A']), - ).toBe(true) + expect(isHotkeyCombinationPressed(['shift', 'a'], ['Shift', 'A'])).toBe( + true, + ) }) it('should match regardless of modifier order', () => { @@ -97,9 +97,9 @@ describe('hotkey utilities', () => { }) it('should reject extra keys', () => { - expect(isHotkeyCombinationPressed(['Shift', 'A', 'B'], ['Shift', 'A'])).toBe( - false, - ) + expect( + isHotkeyCombinationPressed(['Shift', 'A', 'B'], ['Shift', 'A']), + ).toBe(false) }) it('should handle single key hotkey', () => { diff --git a/packages/devtools/src/utils/hotkey.ts b/packages/devtools/src/utils/hotkey.ts index 808ddb6f..fe46db8b 100644 --- a/packages/devtools/src/utils/hotkey.ts +++ b/packages/devtools/src/utils/hotkey.ts @@ -4,15 +4,17 @@ import { getAllPermutations } from './sanitize' import type { KeyboardKey, ModifierKey } from '../context/devtools-store' /** Expands CtrlOrMeta into separate Control and Meta variants */ -export const normalizeHotkey = (keys: Array): Array> => { +export const normalizeHotkey = ( + keys: Array, +): Array> => { // no normalization needed if CtrlOrMeta not used if (!keys.includes('CtrlOrMeta')) { return [keys] } - + return [ - keys.map(key => key === 'CtrlOrMeta' ? 'Control' : key), - keys.map(key => key === 'CtrlOrMeta' ? 'Meta' : key) + keys.map((key) => (key === 'CtrlOrMeta' ? 'Control' : key)), + keys.map((key) => (key === 'CtrlOrMeta' ? 'Meta' : key)), ] } @@ -20,25 +22,27 @@ export const normalizeHotkey = (keys: Array): Array): Array> => { +export const getHotkeyPermutations = ( + hotkey: Array, +): Array> => { const normalizedHotkeys = normalizeHotkey(hotkey) - + return normalizedHotkeys.flatMap((normalizedHotkey) => { const modifiers = normalizedHotkey.filter((key) => keyboardModifiers.includes(key as any), ) as Array - + const nonModifiers = normalizedHotkey.filter( (key) => !keyboardModifiers.includes(key as any), ) - + // handle case with no modifiers (just non-modifier keys) if (modifiers.length === 0) { return [nonModifiers] } - + const allModifierCombinations = getAllPermutations(modifiers) - return allModifierCombinations.map(combo => [...combo, ...nonModifiers]) + return allModifierCombinations.map((combo) => [...combo, ...nonModifiers]) }) } @@ -48,13 +52,15 @@ export const isHotkeyCombinationPressed = ( hotkey: Array, ): boolean => { const permutations = getHotkeyPermutations(hotkey) - const pressedKeys = keys.map(key => key.toUpperCase()) + const pressedKeys = keys.map((key) => key.toUpperCase()) return permutations.some( (combo) => // every key in the combo must be pressed - combo.every(key => pressedKeys.includes(String(key).toUpperCase())) && + combo.every((key) => pressedKeys.includes(String(key).toUpperCase())) && // and no extra keys beyond the combo - pressedKeys.every(key => combo.map(k => String(k).toUpperCase()).includes(key)), + pressedKeys.every((key) => + combo.map((k) => String(k).toUpperCase()).includes(key), + ), ) } From c77920f51c1498c8895f7482acefe449da3f7656 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Mon, 8 Dec 2025 10:27:01 +0100 Subject: [PATCH 7/8] remove extra modifiers --- packages/devtools/src/tabs/settings-tab.tsx | 4 +--- pnpm-lock.yaml | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/devtools/src/tabs/settings-tab.tsx b/packages/devtools/src/tabs/settings-tab.tsx index 35f4164b..05fc9b4c 100644 --- a/packages/devtools/src/tabs/settings-tab.tsx +++ b/packages/devtools/src/tabs/settings-tab.tsx @@ -26,11 +26,9 @@ export const SettingsTab = () => { const styles = useStyles() const modifiers: Array = [ - 'Control', + 'CtrlOrMeta', 'Alt', - 'Meta', 'Shift', - 'CtrlOrMeta', ] return ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ae61a7c..3fe24dab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -453,6 +453,8 @@ importers: specifier: ^4.2.4 version: 4.2.4 + examples/react/start/generated/prisma: {} + examples/react/time-travel: dependencies: '@tanstack/devtools-event-client': From 958b0a4ca914a11ba97524d2ea1b633cdb898739 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:28:35 +0000 Subject: [PATCH 8/8] ci: apply automated fixes --- packages/devtools/src/tabs/settings-tab.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/devtools/src/tabs/settings-tab.tsx b/packages/devtools/src/tabs/settings-tab.tsx index 05fc9b4c..60a42eea 100644 --- a/packages/devtools/src/tabs/settings-tab.tsx +++ b/packages/devtools/src/tabs/settings-tab.tsx @@ -25,11 +25,7 @@ export const SettingsTab = () => { const { setSettings, settings } = useDevtoolsSettings() const styles = useStyles() - const modifiers: Array = [ - 'CtrlOrMeta', - 'Alt', - 'Shift', - ] + const modifiers: Array = ['CtrlOrMeta', 'Alt', 'Shift'] return (