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 ── * ── The overlay ──
* *
* `ctrl+shift+space` opens a list in the middle of the screen; `j`/`k` move through it and `l` applies the * `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 over a * focused theme to the app BEHIND it. That is the whole idea, and it is why this is a portal with no
* translucent backdrop rather than a modal dialog: you are meant to watch the application change while * backdrop rather than a modal dialog: you are meant to watch the application change while you browse,
* you browse, so as little of it as possible should be covered. * so nothing of it should be covered.
* *
* Ported from the owner's `vimazing`, keys and all. * Ported from the owner's `vimazing`, keys and all.
*/ */
@@ -156,9 +156,9 @@ export const ThemeSwitcher = () => {
{open && {open &&
createPortal( createPortal(
<div <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" 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)} onClick={() => setOpen(false)}
> >
<div <div
@@ -218,18 +218,23 @@ export const Spotlight = () => {
// anywhere — including inside a panel that has its own key handling. // anywhere — including inside a panel that has its own key handling.
useEffect(() => { useEffect(() => {
const onKey = (ev: KeyboardEvent) => { 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. // the physical key is `Space` everywhere.
// //
// Every other modifier must be ABSENT. `ctrl+shift+space` is the theme browser, and one chord opening // Every other modifier must be ABSENT. `ctrl+shift+space` and `ctrl+alt+space` are the theme
// both is the bug that took a capture-phase listener to fix on the theme side. // 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 // 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 // "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` // 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 // below covers the only thing the guard was protecting against — a layout that would otherwise type
// a character. // 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(); ev.preventDefault();
// Take focus off whatever had it, explicitly, before the overlay mounts. The effect below focuses // 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 // 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" "codeEditor": "Code Editor"
}, },
"theme": { "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}}", "pillLabel": "Theme: {{name}}",
"star": "star", "star": "star",
"unstar": "unstar", "unstar": "unstar",
"cycleHint": "ctrl+shift+←/→ cycle · ctrl+shift+space browse", "cycleHint": "ctrl+shift+←/→ cycle · ctrl+shift+↑/↓ browse",
"everyShortcutHint": "ctrl+? for every shortcut", "everyShortcutHint": "ctrl+? for every shortcut",
"favouritesOnly": "Favourites only ({{count}})", "favouritesOnly": "Favourites only ({{count}})",
"favourites": "Favourites ({{count}})", "favourites": "Favourites ({{count}})",
+2 -2
View File
@@ -478,11 +478,11 @@
"codeEditor": "Editor de código" "codeEditor": "Editor de código"
}, },
"theme": { "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}}", "pillLabel": "Tema: {{name}}",
"star": "marcar", "star": "marcar",
"unstar": "desmarcar", "unstar": "desmarcar",
"cycleHint": "ctrl+shift+←/→ recorrer · ctrl+shift+space explorar", "cycleHint": "ctrl+shift+←/→ recorrer · ctrl+shift+↑/↓ explorar",
"everyShortcutHint": "ctrl+? para todos los atajos", "everyShortcutHint": "ctrl+? para todos los atajos",
"favouritesOnly": "Solo favoritos ({{count}})", "favouritesOnly": "Solo favoritos ({{count}})",
"favourites": "Favoritos ({{count}})", "favourites": "Favoritos ({{count}})",
+2 -2
View File
@@ -478,11 +478,11 @@
"codeEditor": "Éditeur de code" "codeEditor": "Éditeur de code"
}, },
"theme": { "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}}", "pillLabel": "Thème : {{name}}",
"star": "marquer", "star": "marquer",
"unstar": "dé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", "everyShortcutHint": "ctrl+? pour tous les raccourcis",
"favouritesOnly": "Favoris uniquement ({{count}})", "favouritesOnly": "Favoris uniquement ({{count}})",
"favourites": "Favoris ({{count}})", "favourites": "Favoris ({{count}})",
+2 -2
View File
@@ -478,11 +478,11 @@
"codeEditor": "Editor de código" "codeEditor": "Editor de código"
}, },
"theme": { "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}}", "pillLabel": "Tema: {{name}}",
"star": "marcar", "star": "marcar",
"unstar": "desmarcar", "unstar": "desmarcar",
"cycleHint": "ctrl+shift+←/→ percorrer · ctrl+shift+space explorar", "cycleHint": "ctrl+shift+←/→ percorrer · ctrl+shift+↑/↓ explorar",
"everyShortcutHint": "ctrl+? para todos os atalhos", "everyShortcutHint": "ctrl+? para todos os atalhos",
"favouritesOnly": "Apenas favoritos ({{count}})", "favouritesOnly": "Apenas favoritos ({{count}})",
"favourites": "Favoritos ({{count}})", "favourites": "Favoritos ({{count}})",
+2 -2
View File
@@ -478,11 +478,11 @@
"codeEditor": "Kod-editor" "codeEditor": "Kod-editor"
}, },
"theme": { "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}}", "pillLabel": "Tema: {{name}}",
"star": "stjärnmärk", "star": "stjärnmärk",
"unstar": "avmarkera stjärna", "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", "everyShortcutHint": "ctrl+? för alla kortkommandon",
"favouritesOnly": "Endast favoriter ({{count}})", "favouritesOnly": "Endast favoriter ({{count}})",
"favourites": "Favoriter ({{count}})", "favourites": "Favoriter ({{count}})",
+2 -2
View File
@@ -42,7 +42,7 @@ export const SHORTCUT_SECTIONS: ShortcutSection[] = [
titleKey: 'shortcuts.navigation', titleKey: 'shortcuts.navigation',
shortcuts: [ shortcuts: [
{ {
keys: 'Shift+Space', keys: 'Ctrl+Space',
descriptionKey: 'shortcuts.spotlight', descriptionKey: 'shortcuts.spotlight',
implementedIn: 'Layout/Spotlight/Spotlight.tsx', implementedIn: 'Layout/Spotlight/Spotlight.tsx',
}, },
@@ -54,7 +54,7 @@ export const SHORTCUT_SECTIONS: ShortcutSection[] = [
noteKey: 'shortcuts.themesNote', noteKey: 'shortcuts.themesNote',
shortcuts: [ shortcuts: [
{ {
keys: 'Ctrl+Shift+Space', keys: 'Ctrl+Shift+↑ / ↓',
descriptionKey: 'shortcuts.openBrowser', descriptionKey: 'shortcuts.openBrowser',
implementedIn: 'theme/useThemeKeys.ts', 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+→ next theme, from anywhere, no UI
* ctrl+shift+← previous theme * 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 * 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 * 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 onKeyDown = (event: KeyboardEvent) => {
const { themeId: current, focused: focus, open: isOpen } = state.current; 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 // Toggle the list. Vertical arrows on the SAME modifier as the horizontal cycle — one family:
// shipped with first, and fingers are quicker to learn than to unlearn. // `ctrl+shift+←/→` steps the theme, `ctrl+shift+↑/↓` opens the browse. Was `ctrl+shift+space`
if (event.ctrlKey && (event.shiftKey || event.altKey) && event.code === '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(); event.preventDefault();
/* /*
Two problems solved by two lines, and both were reported from the same keystroke. 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 `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, DropdownMenuTrigger, and a focused trigger opens on ArrowDown (it did on Space too, when Space
the chord opened the overlay and the menu behind it. React attaches its handlers at the root, which was the chord) — so if the pill happened to have focus, the chord opened the overlay and the
bubbles before window, so the trigger saw the key first no matter what this listener did with it. menu behind it. React attaches its handlers at the root, which bubbles before window, so the
This listener now runs in the CAPTURE phase and stops the event dead. 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 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. the guard below correctly refuses to steal keys from an input, and the input never lost focus.