spotlight on ctrl+space, theme browser on ctrl+shift+up/down, no backdrop behind the theme list
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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}})",
|
||||||
|
|||||||
@@ -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}})",
|
||||||
|
|||||||
@@ -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}})",
|
||||||
|
|||||||
@@ -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}})",
|
||||||
|
|||||||
@@ -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}})",
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user