Skip to content

Commit c09bef9

Browse files
antfuclaude
andcommitted
refactor: decompose commands UI and extract keybinding utilities
Extract keybinding logic (formatKeybinding, normalizeKeyEvent, evaluateWhen, collectAllKeybindings) into standalone keybindings.ts module with tests. Decompose CommandPalette.vue into CommandPaletteItem and KeybindingBadge sub-components. Split ViewBuiltinSettings.vue (800 lines) into SettingsAppearance, SettingsShortcuts, and SettingsDocks components. Convert KNOWN_BROWSER_SHORTCUTS from Set to Record with descriptions for better conflict messages. Remove default Escape keybinding from close-panel. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 599db24 commit c09bef9

File tree

12 files changed

+1181
-911
lines changed

12 files changed

+1181
-911
lines changed

docs/kit/commands.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,19 @@ Supported operators: `==`, `!=`, `&&`, `||`, `!` (negation), bare truthy checks.
181181

182182
Users can customize shortcuts in the DevTools Settings page under **Keyboard Shortcuts**. Overrides are stored in shared state and persist across sessions. Setting an empty array disables a shortcut.
183183

184+
### Shortcut Editor
185+
186+
The Settings page includes an inline shortcut editor with:
187+
188+
- **Key capture** — click the input and press any key combination
189+
- **Modifier toggles** — toggle Cmd/Ctrl, Alt, Shift individually
190+
- **Conflict detection** — warns when a shortcut conflicts with:
191+
- Common browser shortcuts (e.g. `Cmd+T` → "Open new tab", `Cmd+W` → "Close tab")
192+
- Other registered commands
193+
- Weak shortcuts (single key without modifiers)
194+
195+
The list of known browser shortcuts (`KNOWN_BROWSER_SHORTCUTS`) is exported from `@vitejs/devtools-kit` and maps each key combination to a human-readable description.
196+
184197
## Command Palette
185198

186199
The built-in command palette is toggled with `Mod+K` (or `Ctrl+K` on Windows/Linux). It provides:

packages/core/src/client/webcomponents/components/command-palette/CommandPalette.vue

Lines changed: 13 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import type { DevToolsClientCommand, DevToolsCommandEntry } from '@vitejs/devtoo
33
import type { DocksContext } from '@vitejs/devtools-kit/client'
44
import Fuse from 'fuse.js'
55
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
6-
import { formatKeybinding } from '../../state/commands'
7-
import DockIcon from '../dock/DockIcon.vue'
6+
import CommandPaletteItem from './CommandPaletteItem.vue'
87
98
const props = defineProps<{
109
context: DocksContext
@@ -238,9 +237,8 @@ function onGlobalKeyDown(e: KeyboardEvent) {
238237
}
239238
}
240239
241-
function getDisplayKeybindings(id: string): string[][] {
242-
const bindings = commandsCtx.value.getKeybindings(id)
243-
return bindings.map(b => formatKeybinding(b.key))
240+
function getKeybindings(id: string) {
241+
return commandsCtx.value.getKeybindings(id)
244242
}
245243
</script>
246244

@@ -287,60 +285,18 @@ function getDisplayKeybindings(id: string): string[][] {
287285

288286
<!-- Items -->
289287
<div class="flex-1 of-y-auto p-1.5">
290-
<button
288+
<CommandPaletteItem
291289
v-for="(item, idx) of filtered"
292-
:id="`cmd-${item.entry.id}`"
293290
:key="item.entry.id"
294-
class="w-full text-left"
295-
@click="enterItem(item)"
296-
@mouseover="selectedIndex = idx"
297-
>
298-
<div
299-
class="flex items-center gap-2 justify-between rounded-md px-2.5 py-1.5 text-sm transition-colors"
300-
:class="selectedIndex === idx ? 'bg-primary/10 text-primary' : 'op80 hover:op100'"
301-
>
302-
<div class="flex items-center gap-2 flex-1 of-hidden min-w-0">
303-
<DockIcon
304-
v-if="item.entry.icon"
305-
:icon="item.entry.icon"
306-
class="w-4 h-4 flex-none op70"
307-
/>
308-
<div class="flex-1 min-w-0">
309-
<div class="flex items-center gap-1.5">
310-
<span class="truncate">
311-
<span v-if="item.parentTitle && !breadcrumb.length" class="op50">{{ item.parentTitle }} &rsaquo; </span>
312-
{{ item.entry.title }}
313-
</span>
314-
<span
315-
v-if="item.entry.source === 'server'"
316-
class="text-[10px] px-1 py-0 rounded bg-blue/10 text-blue shrink-0 leading-4"
317-
>server</span>
318-
</div>
319-
<div v-if="selectedIndex === idx && item.entry.description" class="truncate text-xs op40 mt-0.5">
320-
{{ item.entry.description }}
321-
</div>
322-
</div>
323-
</div>
324-
<div class="flex items-center gap-1.5 flex-none">
325-
<!-- Keybinding badges -->
326-
<template v-for="(keys, ki) in getDisplayKeybindings(item.entry.id)" :key="ki">
327-
<span class="flex items-center gap-0.5">
328-
<kbd
329-
v-for="(key, j) in keys"
330-
:key="j"
331-
class="px-1.5 py-0.5 text-[10px] rounded bg-base border border-base op60 font-mono"
332-
>
333-
{{ key }}
334-
</kbd>
335-
</span>
336-
</template>
337-
<!-- Loading indicator -->
338-
<span v-if="loadingId === item.entry.id" class="i-ph-spinner-gap-duotone w-3.5 h-3.5 animate-spin op50" />
339-
<!-- Drill-down indicator -->
340-
<span v-else-if="item.entry.children?.length" class="i-ph-caret-right w-3 h-3 op40" />
341-
</div>
342-
</div>
343-
</button>
291+
:entry="item.entry"
292+
:parent-title="item.parentTitle"
293+
:show-parent-title="!breadcrumb.length"
294+
:selected="selectedIndex === idx"
295+
:loading="loadingId === item.entry.id"
296+
:keybindings="getKeybindings(item.entry.id)"
297+
@select="selectedIndex = idx"
298+
@activate="enterItem(item)"
299+
/>
344300

345301
<div v-if="!filtered.length" class="py-8 flex flex-col items-center justify-center gap-2 op50 text-sm">
346302
<div class="i-ph-magnifying-glass-duotone w-6 h-6" />
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<script setup lang="ts">
2+
import type { DevToolsCommandEntry, DevToolsCommandKeybinding } from '@vitejs/devtools-kit'
3+
import DockIcon from '../dock/DockIcon.vue'
4+
import KeybindingBadge from './KeybindingBadge.vue'
5+
6+
defineProps<{
7+
entry: DevToolsCommandEntry
8+
parentTitle?: string
9+
showParentTitle: boolean
10+
selected: boolean
11+
loading: boolean
12+
keybindings: DevToolsCommandKeybinding[]
13+
}>()
14+
15+
defineEmits<{
16+
select: []
17+
activate: []
18+
}>()
19+
</script>
20+
21+
<template>
22+
<button
23+
:id="`cmd-${entry.id}`"
24+
class="w-full text-left"
25+
@click="$emit('activate')"
26+
@mouseover="$emit('select')"
27+
>
28+
<div
29+
class="flex items-center gap-2 justify-between rounded-md px-2.5 py-1.5 text-sm transition-colors"
30+
:class="selected ? 'bg-primary/10 text-primary' : 'op80 hover:op100'"
31+
>
32+
<div class="flex items-center gap-2 flex-1 of-hidden min-w-0">
33+
<DockIcon
34+
v-if="entry.icon"
35+
:icon="entry.icon"
36+
class="w-4 h-4 flex-none op70"
37+
/>
38+
<div class="flex-1 min-w-0">
39+
<div class="flex items-center gap-1.5">
40+
<span class="truncate">
41+
<span v-if="parentTitle && showParentTitle" class="op50">{{ parentTitle }} &rsaquo; </span>
42+
{{ entry.title }}
43+
</span>
44+
<span
45+
v-if="entry.source === 'server'"
46+
class="text-[10px] px-1 py-0 rounded bg-blue/10 text-blue shrink-0 leading-4"
47+
>server</span>
48+
</div>
49+
<div v-if="selected && entry.description" class="truncate text-xs op40 mt-0.5">
50+
{{ entry.description }}
51+
</div>
52+
</div>
53+
</div>
54+
<div class="flex items-center gap-1.5 flex-none">
55+
<!-- Keybinding badges -->
56+
<KeybindingBadge
57+
v-for="(kb, ki) in keybindings"
58+
:key="ki"
59+
:key-string="kb.key"
60+
/>
61+
<!-- Loading indicator -->
62+
<span v-if="loading" class="i-ph-spinner-gap-duotone w-3.5 h-3.5 animate-spin op50" />
63+
<!-- Drill-down indicator -->
64+
<span v-else-if="entry.children?.length" class="i-ph-caret-right w-3 h-3 op40" />
65+
</div>
66+
</div>
67+
</button>
68+
</template>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<script setup lang="ts">
2+
import { formatKeybinding } from '../../state/keybindings'
3+
4+
const props = defineProps<{
5+
keyString: string
6+
}>()
7+
8+
const keys = formatKeybinding(props.keyString)
9+
</script>
10+
11+
<template>
12+
<span class="flex items-center gap-0.5">
13+
<kbd
14+
v-for="(key, j) in keys"
15+
:key="j"
16+
class="px-1.5 py-0.5 text-[10px] rounded bg-base border border-base op60 font-mono"
17+
>
18+
{{ key }}
19+
</kbd>
20+
</span>
21+
</template>
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<script setup lang="ts">
2+
import type { DocksContext } from '@vitejs/devtools-kit/client'
3+
import type { SharedState } from '@vitejs/devtools-kit/utils/shared-state'
4+
import type { DevToolsDocksUserSettings } from '../../state/dock-settings'
5+
import { DEFAULT_STATE_USER_SETTINGS } from '@vitejs/devtools-kit/constants'
6+
import { computed } from 'vue'
7+
import { sharedStateToRef } from '../../state/docks'
8+
import { isDockPopupSupported, requestDockPopupOpen, useIsDockPopupOpen } from '../../state/popup'
9+
10+
const props = defineProps<{
11+
context: DocksContext
12+
settingsStore: SharedState<DevToolsDocksUserSettings>
13+
}>()
14+
15+
const settings = sharedStateToRef(props.settingsStore)
16+
const panelStore = props.context.panel.store
17+
const isEmbedded = props.context.clientType === 'embedded'
18+
const isDockPopupOpen = useIsDockPopupOpen()
19+
20+
const dockModeOptions = computed(() => {
21+
const options = [
22+
{ value: 'float', label: 'Float', icon: 'i-ph-cards-three-duotone' },
23+
{ value: 'edge', label: 'Edge', icon: 'i-ph-square-half-bottom-duotone' },
24+
]
25+
if (isDockPopupSupported()) {
26+
options.push({ value: 'popup', label: 'Popup', icon: 'i-ph-arrow-square-out-duotone' })
27+
}
28+
return options
29+
})
30+
31+
const currentDockMode = computed(() => panelStore.mode)
32+
33+
function setDockMode(mode: string) {
34+
if (mode === 'popup') {
35+
requestDockPopupOpen(props.context)
36+
}
37+
else {
38+
panelStore.mode = mode as 'float' | 'edge'
39+
}
40+
}
41+
42+
function resetSettings() {
43+
// eslint-disable-next-line no-alert
44+
if (confirm('Reset all dock settings to defaults?')) {
45+
props.settingsStore.mutate(() => {
46+
return DEFAULT_STATE_USER_SETTINGS()
47+
})
48+
}
49+
}
50+
</script>
51+
52+
<template>
53+
<div class="flex flex-col gap-4">
54+
<!-- Dock mode -->
55+
<div v-if="isEmbedded && !isDockPopupOpen" class="flex flex-col gap-2">
56+
<div class="flex flex-col">
57+
<span class="text-sm">Dock mode</span>
58+
<span class="text-xs op50">How the DevTools panel is displayed</span>
59+
</div>
60+
<div class="flex items-center gap-1 bg-gray/10 rounded-lg p1 w-fit">
61+
<button
62+
v-for="option of dockModeOptions"
63+
:key="option.value"
64+
class="flex items-center gap-1.5 px3 py1.5 rounded-md text-sm transition-all"
65+
:class="currentDockMode === option.value
66+
? 'bg-base shadow text-primary font-medium'
67+
: 'op60 hover:op100 hover:bg-gray/10'"
68+
@click="setDockMode(option.value)"
69+
>
70+
<div :class="option.icon" class="w-4 h-4" />
71+
{{ option.label }}
72+
</button>
73+
</div>
74+
</div>
75+
76+
<!-- Show iframe address bar toggle -->
77+
<label class="flex items-center gap-3 cursor-pointer group">
78+
<button
79+
class="w-10 h-6 rounded-full transition-colors relative shrink-0"
80+
:class="settings.showIframeAddressBar ? 'bg-lime' : 'bg-gray/30'"
81+
@click="settingsStore.mutate((s) => { s.showIframeAddressBar = !s.showIframeAddressBar })"
82+
>
83+
<div
84+
class="absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform"
85+
:class="settings.showIframeAddressBar ? 'translate-x-5' : 'translate-x-1'"
86+
/>
87+
</button>
88+
<div class="flex flex-col">
89+
<span class="text-sm">Show iframe address bar</span>
90+
<span class="text-xs op50">Display navigation controls and URL bar for iframe views</span>
91+
</div>
92+
</label>
93+
94+
<!-- Close on outside click toggle -->
95+
<label class="flex items-center gap-3 cursor-pointer group">
96+
<button
97+
class="w-10 h-6 rounded-full transition-colors relative shrink-0"
98+
:class="settings.closeOnOutsideClick ? 'bg-lime' : 'bg-gray/30'"
99+
@click="settingsStore.mutate((s) => { s.closeOnOutsideClick = !s.closeOnOutsideClick })"
100+
>
101+
<div
102+
class="absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform"
103+
:class="settings.closeOnOutsideClick ? 'translate-x-5' : 'translate-x-1'"
104+
/>
105+
</button>
106+
<div class="flex flex-col">
107+
<span class="text-sm">Close panel on outside click</span>
108+
<span class="text-xs op50">Close the DevTools panel when clicking outside of it (embedded mode only)</span>
109+
</div>
110+
</label>
111+
</div>
112+
113+
<!-- Reset -->
114+
<div class="border-t border-base mt-8 pt-6">
115+
<button
116+
class="px-4 py-2 rounded bg-red/10 text-red hover:bg-red/20 transition-colors flex items-center gap-2 text-sm"
117+
@click="resetSettings"
118+
>
119+
<div class="i-ph-arrow-counter-clockwise" />
120+
Reset All Settings
121+
</button>
122+
</div>
123+
</template>

0 commit comments

Comments
 (0)