@tanstack/devtools 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,15 @@
1
1
  import type { TabName } from '../tabs'
2
2
  import type { TanStackDevtoolsPlugin } from './devtools-context'
3
3
 
4
- type ModifierKey = 'Alt' | 'Control' | 'Meta' | 'Shift'
4
+ type ModifierKey = 'Alt' | 'Control' | 'Meta' | 'Shift' | 'CtrlOrMeta'
5
5
  type KeyboardKey = ModifierKey | (string & {})
6
+ export type { ModifierKey, KeyboardKey }
6
7
  export const keyboardModifiers: Array<ModifierKey> = [
7
8
  'Alt',
8
9
  'Control',
9
10
  'Meta',
10
11
  'Shift',
12
+ 'CtrlOrMeta',
11
13
  ]
12
14
 
13
15
  type TriggerPosition =
@@ -47,9 +49,14 @@ export type DevtoolsStore = {
47
49
  panelLocation: 'top' | 'bottom'
48
50
  /**
49
51
  * The hotkey to open the dev tools
50
- * @default "shift+a"
52
+ * @default ["Shift", "A"]
51
53
  */
52
54
  openHotkey: Array<KeyboardKey>
55
+ /**
56
+ * The hotkey to open the source inspector
57
+ * @default ["Shift", "CtrlOrMeta"]
58
+ */
59
+ inspectHotkey: Array<KeyboardKey>
53
60
  /**
54
61
  * Whether to require the URL flag to open the dev tools
55
62
  * @default false
@@ -93,6 +100,7 @@ export const initialState: DevtoolsStore = {
93
100
  position: 'bottom-right',
94
101
  panelLocation: 'bottom',
95
102
  openHotkey: ['Shift', 'A'],
103
+ inspectHotkey: ['Shift', 'CtrlOrMeta'],
96
104
  requireUrlFlag: false,
97
105
  urlFlag: 'tanstack-devtools',
98
106
  theme:
@@ -48,6 +48,14 @@ export const usePlugins = () => {
48
48
  setStore((prev) => {
49
49
  const isActive = prev.state.activePlugins.includes(pluginId)
50
50
 
51
+ const currentPlugin = store.plugins?.find(
52
+ (plugin) => plugin.id === pluginId,
53
+ )
54
+
55
+ if (currentPlugin?.destroy && isActive) {
56
+ currentPlugin.destroy(pluginId)
57
+ }
58
+
51
59
  const updatedPlugins = isActive
52
60
  ? prev.state.activePlugins.filter((id) => id !== pluginId)
53
61
  : [...prev.state.activePlugins, pluginId]
package/src/devtools.tsx CHANGED
@@ -3,6 +3,7 @@ import { createShortcut } from '@solid-primitives/keyboard'
3
3
  import { Portal } from 'solid-js/web'
4
4
  import { ThemeContextProvider } from '@tanstack/devtools-ui'
5
5
  import { devtoolsEventClient } from '@tanstack/devtools-client'
6
+
6
7
  import {
7
8
  useDevtoolsSettings,
8
9
  useHeight,
@@ -11,13 +12,12 @@ import {
11
12
  } from './context/use-devtools-context'
12
13
  import { useDisableTabbing } from './hooks/use-disable-tabbing'
13
14
  import { TANSTACK_DEVTOOLS } from './utils/storage'
15
+ import { getHotkeyPermutations } from './utils/hotkey'
14
16
  import { Trigger } from './components/trigger'
15
17
  import { MainPanel } from './components/main-panel'
16
18
  import { ContentPanel } from './components/content-panel'
17
19
  import { Tabs } from './components/tabs'
18
20
  import { TabContent } from './components/tab-content'
19
- import { keyboardModifiers } from './context/devtools-store'
20
- import { getAllPermutations } from './utils/sanitize'
21
21
  import { usePiPWindow } from './context/pip-context'
22
22
  import { SourceInspector } from './components/source-inspector'
23
23
 
@@ -165,18 +165,9 @@ export default function DevTools() {
165
165
  }
166
166
  })
167
167
  createEffect(() => {
168
- // we create all combinations of modifiers
169
- const modifiers = settings().openHotkey.filter((key) =>
170
- keyboardModifiers.includes(key as any),
171
- )
172
- const nonModifiers = settings().openHotkey.filter(
173
- (key) => !keyboardModifiers.includes(key as any),
174
- )
175
-
176
- const allModifierCombinations = getAllPermutations(modifiers)
177
-
178
- for (const combination of allModifierCombinations) {
179
- const permutation = [...combination, ...nonModifiers]
168
+ const permutations = getHotkeyPermutations(settings().openHotkey)
169
+
170
+ for (const permutation of permutations) {
180
171
  createShortcut(permutation, () => {
181
172
  toggleOpen()
182
173
  })
@@ -466,6 +466,11 @@ const stylesFactory = (theme: DevtoolsStore['settings']['theme']) => {
466
466
  display: flex;
467
467
  gap: 0.5rem;
468
468
  `,
469
+ settingsStack: css`
470
+ display: flex;
471
+ flex-direction: column;
472
+ gap: 1rem;
473
+ `,
469
474
 
470
475
  // No Plugins Fallback Styles
471
476
  noPluginsFallback: css`
@@ -0,0 +1,97 @@
1
+ import { Show } from 'solid-js'
2
+ import { Button, Input } from '@tanstack/devtools-ui'
3
+
4
+ import { uppercaseFirstLetter } from '../utils/sanitize'
5
+ import { useStyles } from '../styles/use-styles'
6
+ import type { KeyboardKey } from '../context/devtools-store'
7
+
8
+ interface HotkeyConfigProps {
9
+ title: string
10
+ description: string
11
+ hotkey: Array<KeyboardKey>
12
+ modifiers: Array<KeyboardKey>
13
+ onHotkeyChange: (hotkey: Array<KeyboardKey>) => void
14
+ }
15
+
16
+ const MODIFIER_DISPLAY_NAMES: Record<KeyboardKey, string> = {
17
+ Shift: 'Shift',
18
+ Alt: 'Alt',
19
+ Meta: 'Meta',
20
+ Control: 'Control',
21
+ CtrlOrMeta: 'Ctrl Or Meta',
22
+ }
23
+
24
+ export const HotkeyConfig = (props: HotkeyConfigProps) => {
25
+ const styles = useStyles()
26
+
27
+ const toggleModifier = (modifier: KeyboardKey) => {
28
+ if (props.hotkey.includes(modifier)) {
29
+ props.onHotkeyChange(props.hotkey.filter((key) => key !== modifier))
30
+ } else {
31
+ const existingModifiers = props.hotkey.filter((key) =>
32
+ props.modifiers.includes(key as any),
33
+ )
34
+ const otherKeys = props.hotkey.filter(
35
+ (key) => !props.modifiers.includes(key as any),
36
+ )
37
+ props.onHotkeyChange([...existingModifiers, modifier, ...otherKeys])
38
+ }
39
+ }
40
+
41
+ const getNonModifierValue = () => {
42
+ return props.hotkey
43
+ .filter((key) => !props.modifiers.includes(key as any))
44
+ .join('+')
45
+ }
46
+
47
+ const handleKeyInput = (input: string) => {
48
+ const makeModifierArray = (key: string) => {
49
+ if (key.length === 1) return [uppercaseFirstLetter(key)]
50
+ const modifiersArray: Array<string> = []
51
+ for (const character of key) {
52
+ const newLetter = uppercaseFirstLetter(character)
53
+ if (!modifiersArray.includes(newLetter)) modifiersArray.push(newLetter)
54
+ }
55
+ return modifiersArray
56
+ }
57
+
58
+ const hotkeyModifiers = props.hotkey.filter((key) =>
59
+ props.modifiers.includes(key as any),
60
+ )
61
+ const newKeys = input
62
+ .split('+')
63
+ .flatMap((key) => makeModifierArray(key))
64
+ .filter(Boolean)
65
+ props.onHotkeyChange([...hotkeyModifiers, ...newKeys])
66
+ }
67
+
68
+ const getDisplayHotkey = () => {
69
+ return props.hotkey.join(' + ')
70
+ }
71
+
72
+ return (
73
+ <div class={styles().settingsGroup}>
74
+ <h4 style={{ margin: 0 }}>{props.description}</h4>
75
+ <div class={styles().settingsModifiers}>
76
+ <Show keyed when={props.hotkey}>
77
+ {props.modifiers.map((modifier) => (
78
+ <Button
79
+ variant="success"
80
+ onclick={() => toggleModifier(modifier)}
81
+ outline={!props.hotkey.includes(modifier)}
82
+ >
83
+ {MODIFIER_DISPLAY_NAMES[modifier] || modifier}
84
+ </Button>
85
+ ))}
86
+ </Show>
87
+ </div>
88
+ <Input
89
+ description="Use '+' to combine keys (e.g., 'a+b' or 'd'). This will be used with the enabled modifiers from above"
90
+ placeholder="a"
91
+ value={getNonModifierValue()}
92
+ onChange={handleKeyInput}
93
+ />
94
+ Final shortcut is: {getDisplayHotkey()}
95
+ </div>
96
+ )
97
+ }
@@ -1,6 +1,5 @@
1
- import { Show, createMemo } from 'solid-js'
1
+ import { Show } from 'solid-js'
2
2
  import {
3
- Button,
4
3
  Checkbox,
5
4
  Input,
6
5
  MainPanel,
@@ -16,32 +15,18 @@ import {
16
15
  Link,
17
16
  SettingsCog,
18
17
  } from '@tanstack/devtools-ui/icons'
18
+
19
19
  import { useDevtoolsSettings } from '../context/use-devtools-context'
20
- import { uppercaseFirstLetter } from '../utils/sanitize'
21
20
  import { useStyles } from '../styles/use-styles'
22
- import type { ModifierKey } from '@solid-primitives/keyboard'
21
+ import { HotkeyConfig } from './hotkey-config'
22
+ import type { KeyboardKey } from '../context/devtools-store'
23
23
 
24
24
  export const SettingsTab = () => {
25
25
  const { setSettings, settings } = useDevtoolsSettings()
26
26
  const styles = useStyles()
27
- const hotkey = createMemo(() => settings().openHotkey)
28
- const modifiers: Array<ModifierKey> = ['Control', 'Alt', 'Meta', 'Shift']
29
- const changeHotkey = (newHotkey: ModifierKey) => () => {
30
- if (hotkey().includes(newHotkey)) {
31
- return setSettings({
32
- openHotkey: hotkey().filter((key) => key !== newHotkey),
33
- })
34
- }
35
- const existingModifiers = hotkey().filter((key) =>
36
- modifiers.includes(key as any),
37
- )
38
- const otherModifiers = hotkey().filter(
39
- (key) => !modifiers.includes(key as any),
40
- )
41
- setSettings({
42
- openHotkey: [...existingModifiers, newHotkey, ...otherModifiers],
43
- })
44
- }
27
+
28
+ const modifiers: Array<KeyboardKey> = ['CtrlOrMeta', 'Alt', 'Shift']
29
+
45
30
  return (
46
31
  <MainPanel withPadding>
47
32
  <Section>
@@ -144,71 +129,23 @@ export const SettingsTab = () => {
144
129
  <SectionDescription>
145
130
  Customize keyboard shortcuts for quick access.
146
131
  </SectionDescription>
147
- <div class={styles().settingsGroup}>
148
- <div class={styles().settingsModifiers}>
149
- <Show keyed when={hotkey()}>
150
- <Button
151
- variant="success"
152
- onclick={changeHotkey('Shift')}
153
- outline={!hotkey().includes('Shift')}
154
- >
155
- Shift
156
- </Button>
157
- <Button
158
- variant="success"
159
- onclick={changeHotkey('Alt')}
160
- outline={!hotkey().includes('Alt')}
161
- >
162
- Alt
163
- </Button>
164
- <Button
165
- variant="success"
166
- onclick={changeHotkey('Meta')}
167
- outline={!hotkey().includes('Meta')}
168
- >
169
- Meta
170
- </Button>
171
- <Button
172
- variant="success"
173
- onclick={changeHotkey('Control')}
174
- outline={!hotkey().includes('Control')}
175
- >
176
- Control
177
- </Button>
178
- </Show>
179
- </div>
180
- <Input
181
- label="Hotkey to open/close devtools"
182
- description="Use '+' to combine keys (e.g., 'a+b' or 'd'). This will be used with the enabled modifiers from above"
183
- placeholder="a"
184
- value={hotkey()
185
- .filter((key) => !['Shift', 'Meta', 'Alt', 'Ctrl'].includes(key))
186
- .join('+')}
187
- onChange={(e) => {
188
- const makeModifierArray = (key: string) => {
189
- if (key.length === 1) return [uppercaseFirstLetter(key)]
190
- const modifiers: Array<string> = []
191
- for (const character of key) {
192
- const newLetter = uppercaseFirstLetter(character)
193
- if (!modifiers.includes(newLetter)) modifiers.push(newLetter)
194
- }
195
- return modifiers
196
- }
197
- const modifiers = e
198
- .split('+')
199
- .flatMap((key) => makeModifierArray(key))
200
- .filter(Boolean)
201
- return setSettings({
202
- openHotkey: [
203
- ...hotkey().filter((key) =>
204
- ['Shift', 'Meta', 'Alt', 'Ctrl'].includes(key),
205
- ),
206
- ...modifiers,
207
- ],
208
- })
209
- }}
132
+
133
+ <div class={styles().settingsStack}>
134
+ <HotkeyConfig
135
+ title="Open/Close Devtools"
136
+ description="Hotkey to open/close devtools"
137
+ hotkey={settings().openHotkey}
138
+ modifiers={modifiers}
139
+ onHotkeyChange={(hotkey) => setSettings({ openHotkey: hotkey })}
140
+ />
141
+
142
+ <HotkeyConfig
143
+ title="Source Inspector"
144
+ description="Hotkey to open source inspector"
145
+ hotkey={settings().inspectHotkey}
146
+ modifiers={modifiers}
147
+ onHotkeyChange={(hotkey) => setSettings({ inspectHotkey: hotkey })}
210
148
  />
211
- Final shortcut is: {hotkey().join(' + ')}
212
149
  </div>
213
150
  </Section>
214
151
 
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ getHotkeyPermutations,
4
+ isHotkeyCombinationPressed,
5
+ normalizeHotkey,
6
+ } from './hotkey'
7
+ import type { KeyboardKey } from '../context/devtools-store'
8
+
9
+ describe('hotkey utilities', () => {
10
+ describe('normalizeHotkey', () => {
11
+ it('should return unchanged array when CtrlOrMeta is not present', () => {
12
+ const hotkey: Array<KeyboardKey> = ['Shift', 'A']
13
+ const result = normalizeHotkey(hotkey)
14
+ expect(result).toEqual([['Shift', 'A']])
15
+ })
16
+
17
+ it('should expand CtrlOrMeta to Control and Meta variants', () => {
18
+ const hotkey: Array<KeyboardKey> = ['Shift', 'CtrlOrMeta']
19
+ const result = normalizeHotkey(hotkey)
20
+ expect(result).toHaveLength(2)
21
+ expect(result).toContainEqual(['Shift', 'Control'])
22
+ expect(result).toContainEqual(['Shift', 'Meta'])
23
+ })
24
+ })
25
+
26
+ describe('getHotkeyPermutations', () => {
27
+ it('should generate permutations for modifiers in any order', () => {
28
+ const hotkey: Array<KeyboardKey> = ['Shift', 'Control', 'A']
29
+ const result = getHotkeyPermutations(hotkey)
30
+ expect(result).toContainEqual(['Shift', 'Control', 'A'])
31
+ expect(result).toContainEqual(['Control', 'Shift', 'A'])
32
+ })
33
+
34
+ it('should handle CtrlOrMeta expansion with multiple permutations', () => {
35
+ const hotkey: Array<KeyboardKey> = ['Shift', 'CtrlOrMeta']
36
+ const result = getHotkeyPermutations(hotkey)
37
+ expect(result).toContainEqual(['Shift', 'Control'])
38
+ expect(result).toContainEqual(['Control', 'Shift'])
39
+ expect(result).toContainEqual(['Shift', 'Meta'])
40
+ expect(result).toContainEqual(['Meta', 'Shift'])
41
+ })
42
+
43
+ it('should handle single key hotkey with no modifiers', () => {
44
+ const hotkey: Array<KeyboardKey> = ['A']
45
+ const result = getHotkeyPermutations(hotkey)
46
+ expect(result).toEqual([['A']])
47
+ })
48
+
49
+ it('should not have duplicate permutations', () => {
50
+ const hotkey: Array<KeyboardKey> = ['Shift', 'Alt', 'A']
51
+ const result = getHotkeyPermutations(hotkey)
52
+ const stringified = result.map((combo) => JSON.stringify(combo))
53
+ const unique = new Set(stringified)
54
+ expect(unique.size).toBe(stringified.length)
55
+ })
56
+ })
57
+
58
+ describe('isHotkeyCombinationPressed', () => {
59
+ it('should match exact key combination', () => {
60
+ expect(isHotkeyCombinationPressed(['Shift', 'A'], ['Shift', 'A'])).toBe(
61
+ true,
62
+ )
63
+ })
64
+
65
+ it('should be case-insensitive', () => {
66
+ expect(isHotkeyCombinationPressed(['shift', 'a'], ['Shift', 'A'])).toBe(
67
+ true,
68
+ )
69
+ })
70
+
71
+ it('should match regardless of modifier order', () => {
72
+ expect(
73
+ isHotkeyCombinationPressed(
74
+ ['A', 'Control', 'Shift'],
75
+ ['Shift', 'Control', 'A'],
76
+ ),
77
+ ).toBe(true)
78
+ })
79
+
80
+ it('should handle CtrlOrMeta with Control', () => {
81
+ expect(
82
+ isHotkeyCombinationPressed(
83
+ ['Shift', 'Control'],
84
+ ['Shift', 'CtrlOrMeta'],
85
+ ),
86
+ ).toBe(true)
87
+ })
88
+
89
+ it('should handle CtrlOrMeta with Meta', () => {
90
+ expect(
91
+ isHotkeyCombinationPressed(['Shift', 'Meta'], ['Shift', 'CtrlOrMeta']),
92
+ ).toBe(true)
93
+ })
94
+
95
+ it('should reject incomplete key combinations', () => {
96
+ expect(isHotkeyCombinationPressed(['Shift'], ['Shift', 'A'])).toBe(false)
97
+ })
98
+
99
+ it('should reject extra keys', () => {
100
+ expect(
101
+ isHotkeyCombinationPressed(['Shift', 'A', 'B'], ['Shift', 'A']),
102
+ ).toBe(false)
103
+ })
104
+
105
+ it('should handle single key hotkey', () => {
106
+ expect(isHotkeyCombinationPressed(['A'], ['A'])).toBe(true)
107
+ })
108
+ })
109
+ })
@@ -0,0 +1,66 @@
1
+ import { keyboardModifiers } from '../context/devtools-store'
2
+ import { getAllPermutations } from './sanitize'
3
+
4
+ import type { KeyboardKey, ModifierKey } from '../context/devtools-store'
5
+
6
+ /** Expands CtrlOrMeta into separate Control and Meta variants */
7
+ export const normalizeHotkey = (
8
+ keys: Array<KeyboardKey>,
9
+ ): Array<Array<KeyboardKey>> => {
10
+ // no normalization needed if CtrlOrMeta not used
11
+ if (!keys.includes('CtrlOrMeta')) {
12
+ return [keys]
13
+ }
14
+
15
+ return [
16
+ keys.map((key) => (key === 'CtrlOrMeta' ? 'Control' : key)),
17
+ keys.map((key) => (key === 'CtrlOrMeta' ? 'Meta' : key)),
18
+ ]
19
+ }
20
+
21
+ /**
22
+ * Generates all keyboard permutations for a given hotkey configuration
23
+ * Handles CtrlOrMeta expansion and creates all possible combinations
24
+ */
25
+ export const getHotkeyPermutations = (
26
+ hotkey: Array<KeyboardKey>,
27
+ ): Array<Array<KeyboardKey>> => {
28
+ const normalizedHotkeys = normalizeHotkey(hotkey)
29
+
30
+ return normalizedHotkeys.flatMap((normalizedHotkey) => {
31
+ const modifiers = normalizedHotkey.filter((key) =>
32
+ keyboardModifiers.includes(key as any),
33
+ ) as Array<ModifierKey>
34
+
35
+ const nonModifiers = normalizedHotkey.filter(
36
+ (key) => !keyboardModifiers.includes(key as any),
37
+ )
38
+
39
+ // handle case with no modifiers (just non-modifier keys)
40
+ if (modifiers.length === 0) {
41
+ return [nonModifiers]
42
+ }
43
+
44
+ const allModifierCombinations = getAllPermutations(modifiers)
45
+ return allModifierCombinations.map((combo) => [...combo, ...nonModifiers])
46
+ })
47
+ }
48
+
49
+ /** Checks if the currently pressed keys match any of the hotkey permutations */
50
+ export const isHotkeyCombinationPressed = (
51
+ keys: Array<string>,
52
+ hotkey: Array<KeyboardKey>,
53
+ ): boolean => {
54
+ const permutations = getHotkeyPermutations(hotkey)
55
+ const pressedKeys = keys.map((key) => key.toUpperCase())
56
+
57
+ return permutations.some(
58
+ (combo) =>
59
+ // every key in the combo must be pressed
60
+ combo.every((key) => pressedKeys.includes(String(key).toUpperCase())) &&
61
+ // and no extra keys beyond the combo
62
+ pressedKeys.every((key) =>
63
+ combo.map((k) => String(k).toUpperCase()).includes(key),
64
+ ),
65
+ )
66
+ }