spotlight on ctrl+space, theme browser on ctrl+shift+up/down, no backdrop behind the theme list

This commit is contained in:
2026-08-25 16:53:29 +00:00
parent 4a6e4a773c
commit 4efca226f6
9 changed files with 39 additions and 30 deletions
@@ -24,10 +24,10 @@ const {
*
* ── The overlay ──
*
* `ctrl+shift+space` opens a list in the middle of the screen; `j`/`k` move through it and `l` applies the
* focused theme to the app BEHIND it. That is the whole idea, and it is why this is a portal over a
* translucent backdrop rather than a modal dialog: you are meant to watch the application change while
* you browse, so as little of it as possible should be covered.
* `ctrl+shift+↑/↓` opens a list in the middle of the screen; `j`/`k` move through it and `l` applies the
* focused theme to the app BEHIND it. That is the whole idea, and it is why this is a portal with no
* backdrop rather than a modal dialog: you are meant to watch the application change while you browse,
* so nothing of it should be covered.
*
* Ported from the owner's `vimazing`, keys and all.
*/
@@ -156,9 +156,9 @@ export const ThemeSwitcher = () => {
{open &&
createPortal(
<div
// No backdrop at all — the whole point of the browse is watching the app change behind the
// list, so nothing may sit between them. The full-screen div stays for click-outside-to-close.
className="fixed inset-0 z-[900] flex items-center justify-center"
// Deliberately light: the point is to see the app change behind it.
style={{ backgroundColor: 'color-mix(in srgb, var(--background) 55%, transparent)' }}
onClick={() => setOpen(false)}
>
<div
@@ -218,18 +218,23 @@ export const Spotlight = () => {
// anywhere — including inside a panel that has its own key handling.
useEffect(() => {
const onKey = (ev: KeyboardEvent) => {
// `code`, not `key`: shift+space reports `key: ' '` on most layouts and something else on a few, while
// `code`, not `key`: ctrl+space reports `key: ' '` on most layouts and something else on a few, while
// the physical key is `Space` everywhere.
//
// Every other modifier must be ABSENT. `ctrl+shift+space` is the theme browser, and one chord opening
// both is the bug that took a capture-phase listener to fix on the theme side.
// Every other modifier must be ABSENT. `ctrl+shift+space` and `ctrl+alt+space` are the theme
// browser, and one chord opening both is the bug that took a capture-phase listener to fix on the
// theme side.
//
// Was shift+space until 2026-08-25: typing fires shift+space constantly (any capitalized word
// followed by a space), so the launcher kept popping open mid-sentence. Ctrl+space doesn't happen in
// normal typing.
//
// No focus check. There was one, inherited from shift-space where it was mandatory, and keeping it
// "as belt-and-braces" was wrong in the plainest way: focus is usually IN the chat composer, so the
// guard meant the launcher did not open from the place it is most often wanted. `preventDefault`
// below covers the only thing the guard was protecting against — a layout that would otherwise type
// a character.
if (ev.shiftKey && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.code === 'Space') {
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && ev.code === 'Space') {
ev.preventDefault();
// Take focus off whatever had it, explicitly, before the overlay mounts. The effect below focuses
// this panel's own input, and on paper that is enough — but "on paper" is what produced arrow keys
+2 -2
View File
@@ -478,11 +478,11 @@
"codeEditor": "Code Editor"
},
"theme": {
"pillTitle": "Theme: {{name}} · ctrl+shift+←/→ cycle · ctrl+shift+space browse · ctrl+shift+f {{starAction}}",
"pillTitle": "Theme: {{name}} · ctrl+shift+←/→ cycle · ctrl+shift+↑/↓ browse · ctrl+shift+f {{starAction}}",
"pillLabel": "Theme: {{name}}",
"star": "star",
"unstar": "unstar",
"cycleHint": "ctrl+shift+←/→ cycle · ctrl+shift+space browse",
"cycleHint": "ctrl+shift+←/→ cycle · ctrl+shift+↑/↓ browse",
"everyShortcutHint": "ctrl+? for every shortcut",
"favouritesOnly": "Favourites only ({{count}})",
"favourites": "Favourites ({{count}})",
+2 -2
View File
@@ -478,11 +478,11 @@
"codeEditor": "Editor de código"
},
"theme": {
"pillTitle": "Tema: {{name}} · ctrl+shift+←/→ recorrer · ctrl+shift+space explorar · ctrl+shift+f {{starAction}}",
"pillTitle": "Tema: {{name}} · ctrl+shift+←/→ recorrer · ctrl+shift+↑/↓ explorar · ctrl+shift+f {{starAction}}",
"pillLabel": "Tema: {{name}}",
"star": "marcar",
"unstar": "desmarcar",
"cycleHint": "ctrl+shift+←/→ recorrer · ctrl+shift+space explorar",
"cycleHint": "ctrl+shift+←/→ recorrer · ctrl+shift+↑/↓ explorar",
"everyShortcutHint": "ctrl+? para todos los atajos",
"favouritesOnly": "Solo favoritos ({{count}})",
"favourites": "Favoritos ({{count}})",
+2 -2
View File
@@ -478,11 +478,11 @@
"codeEditor": "Éditeur de code"
},
"theme": {
"pillTitle": "Thème : {{name}} · ctrl+shift+←/→ parcourir · ctrl+shift+space explorer · ctrl+shift+f {{starAction}}",
"pillTitle": "Thème : {{name}} · ctrl+shift+←/→ parcourir · ctrl+shift+↑/↓ explorer · ctrl+shift+f {{starAction}}",
"pillLabel": "Thème : {{name}}",
"star": "marquer",
"unstar": "démarquer",
"cycleHint": "ctrl+shift+←/→ parcourir · ctrl+shift+space explorer",
"cycleHint": "ctrl+shift+←/→ parcourir · ctrl+shift+↑/↓ explorer",
"everyShortcutHint": "ctrl+? pour tous les raccourcis",
"favouritesOnly": "Favoris uniquement ({{count}})",
"favourites": "Favoris ({{count}})",
+2 -2
View File
@@ -478,11 +478,11 @@
"codeEditor": "Editor de código"
},
"theme": {
"pillTitle": "Tema: {{name}} · ctrl+shift+←/→ percorrer · ctrl+shift+space explorar · ctrl+shift+f {{starAction}}",
"pillTitle": "Tema: {{name}} · ctrl+shift+←/→ percorrer · ctrl+shift+↑/↓ explorar · ctrl+shift+f {{starAction}}",
"pillLabel": "Tema: {{name}}",
"star": "marcar",
"unstar": "desmarcar",
"cycleHint": "ctrl+shift+←/→ percorrer · ctrl+shift+space explorar",
"cycleHint": "ctrl+shift+←/→ percorrer · ctrl+shift+↑/↓ explorar",
"everyShortcutHint": "ctrl+? para todos os atalhos",
"favouritesOnly": "Apenas favoritos ({{count}})",
"favourites": "Favoritos ({{count}})",
+2 -2
View File
@@ -478,11 +478,11 @@
"codeEditor": "Kod-editor"
},
"theme": {
"pillTitle": "Tema: {{name}} · ctrl+shift+←/→ bläddra · ctrl+shift+space bläddra i galleri · ctrl+shift+f {{starAction}}",
"pillTitle": "Tema: {{name}} · ctrl+shift+←/→ bläddra · ctrl+shift+↑/↓ bläddra i galleri · ctrl+shift+f {{starAction}}",
"pillLabel": "Tema: {{name}}",
"star": "stjärnmärk",
"unstar": "avmarkera stjärna",
"cycleHint": "ctrl+shift+←/→ bläddra · ctrl+shift+space galleri",
"cycleHint": "ctrl+shift+←/→ bläddra · ctrl+shift+↑/↓ galleri",
"everyShortcutHint": "ctrl+? för alla kortkommandon",
"favouritesOnly": "Endast favoriter ({{count}})",
"favourites": "Favoriter ({{count}})",
+2 -2
View File
@@ -42,7 +42,7 @@ export const SHORTCUT_SECTIONS: ShortcutSection[] = [
titleKey: 'shortcuts.navigation',
shortcuts: [
{
keys: 'Shift+Space',
keys: 'Ctrl+Space',
descriptionKey: 'shortcuts.spotlight',
implementedIn: 'Layout/Spotlight/Spotlight.tsx',
},
@@ -54,7 +54,7 @@ export const SHORTCUT_SECTIONS: ShortcutSection[] = [
noteKey: 'shortcuts.themesNote',
shortcuts: [
{
keys: 'Ctrl+Shift+Space',
keys: 'Ctrl+Shift+↑ / ↓',
descriptionKey: 'shortcuts.openBrowser',
implementedIn: 'theme/useThemeKeys.ts',
},
+12 -8
View File
@@ -9,7 +9,7 @@ import type { ThemeId } from './themes';
*
* ctrl+shift+→ next theme, from anywhere, no UI
* ctrl+shift+← previous theme
* ctrl+shift+space open the list, then browse it
* ctrl+shift+↑/↓ open the list, then browse it
* ctrl+shift+f star or unstar the current theme, from anywhere
*
* One modifier for all three, which took some choosing. `ctrl+alt+arrows` switches WORKSPACE on GNOME and
@@ -78,18 +78,22 @@ export function useThemeKeys(options: ThemeKeysOptions = {}) {
const onKeyDown = (event: KeyboardEvent) => {
const { themeId: current, focused: focus, open: isOpen } = state.current;
// Toggle the list. `ctrl+alt+space` is still accepted alongside `ctrl+shift+space` — it is what this
// shipped with first, and fingers are quicker to learn than to unlearn.
if (event.ctrlKey && (event.shiftKey || event.altKey) && event.code === 'Space') {
// Toggle the list. Vertical arrows on the SAME modifier as the horizontal cycle — one family:
// `ctrl+shift+←/→` steps the theme, `ctrl+shift+↑/↓` opens the browse. Was `ctrl+shift+space`
// (with a `ctrl+alt+space` legacy) until 2026-08-25; both space chords are gone, not kept as
// aliases, because `ctrl+space` became Spotlight the same day and space chords near it invite
// the wrong reflex.
if (event.ctrlKey && event.shiftKey && !event.altKey && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
event.preventDefault();
/*
Two problems solved by two lines, and both were reported from the same keystroke.
`stopPropagation` stops the header's dropdown ALSO opening. The theme pill is a Radix
DropdownMenuTrigger, and a focused trigger opens on Space — so if the pill happened to have focus,
the chord opened the overlay and the menu behind it. React attaches its handlers at the root, which
bubbles before window, so the trigger saw the key first no matter what this listener did with it.
This listener now runs in the CAPTURE phase and stops the event dead.
DropdownMenuTrigger, and a focused trigger opens on ArrowDown (it did on Space too, when Space
was the chord) — so if the pill happened to have focus, the chord opened the overlay and the
menu behind it. React attaches its handlers at the root, which bubbles before window, so the
trigger saw the key first no matter what this listener did with it. This listener now runs in
the CAPTURE phase and stops the event dead.
The blur stops the overlay opening with focus still in a text field, which made j/k/l do nothing:
the guard below correctly refuses to steal keys from an input, and the input never lost focus.