favourite a theme from anywhere, and the browser stops fighting the menu
Three reported faults and two additions, all from one keystroke's worth of use. THE MENU OPENED BEHIND THE OVERLAY. The theme pill is a Radix DropdownMenuTrigger, and a focused trigger opens on Space — so ctrl+shift+space opened the browser AND the dropdown behind it. preventDefault could never have fixed it: React attaches its handlers at the root, which bubbles before window, so the trigger saw the key first whatever this listener did with it. The listener now runs in the CAPTURE phase and stops the event dead. THE VIM KEYS DIED WHEN OPENED FROM AN INPUT. Same shape as the Spotlight bug earlier tonight: the guard that refuses to steal keys from a text field was still true after the overlay opened, because focus had never left the field. Opening now blurs, and the guard no longer applies once the list is open — there is no input in it, so there is nothing left for it to protect. Only the GLOBAL bindings defer to a text field now, which is the case it was written for. f STARS WHAT IS FOCUSED, inside the list, so a browse and a shortlist are one pass rather than two. ctrl+shift+f STARS THE CURRENT THEME FROM ANYWHERE, and deliberately without the typing guard the arrows beside it need: word selection is what ctrl+shift+←/→ collides with in a text field, and ctrl+shift+f collides with nothing — no character, no browser binding. Guarding it would only break it in the composer, which is where you are when a theme starts annoying you. The header pill now shows a star when what you are wearing is one of yours — only when true, because a hollow star on every theme is chrome that says nothing two hundred times. The row star in the overlay is a span with role="button", not a button: it sits inside the row's own <button>, and nesting one is invalid HTML that browsers resolve by dropping one of them. useThemeKeys takes the toggle as a callback rather than reaching for it. Favourites are per account and live behind an API, which is app knowledge; this hook owns keys and selection and nothing else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,8 +32,9 @@ const {
|
||||
*/
|
||||
export const ThemeSwitcher = () => {
|
||||
const { theme, themeId, setTheme, themes } = useTheme();
|
||||
const { open, setOpen, focused, setFocused } = useThemeKeys();
|
||||
const { favorites, isFavorite, toggle } = useThemeFavorites();
|
||||
// Declared before the key hook, which needs the toggle: `f` in the list and `ctrl+shift+f` from anywhere.
|
||||
const { open, setOpen, focused, setFocused } = useThemeKeys({ onToggleFavorite: toggle });
|
||||
const [onlyFavorites, setOnlyFavorites] = useState(false);
|
||||
|
||||
/*
|
||||
@@ -61,7 +62,9 @@ export const ThemeSwitcher = () => {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
title={`Theme: ${theme.name} · ctrl+shift+←/→ to cycle · ctrl+shift+space for the list`}
|
||||
title={`Theme: ${theme.name} · ctrl+shift+←/→ cycle · ctrl+shift+space browse · ctrl+shift+f ${
|
||||
isFavorite(themeId) ? 'unstar' : 'star'
|
||||
}`}
|
||||
aria-label={`Theme: ${theme.name}`}
|
||||
// Named rather than a palette glyph: with 227 themes the one you are wearing is worth
|
||||
// reading at a glance, and an icon cannot say which that is. Same pill as JobsIndicator so
|
||||
@@ -70,6 +73,9 @@ export const ThemeSwitcher = () => {
|
||||
style={{ color: 'var(--header-fg)' }}
|
||||
>
|
||||
{theme.name}
|
||||
{/* A star on the pill, so "is this one of mine" is answerable without opening anything. Only
|
||||
when true — a hollow star on every theme would be chrome that says nothing 200 times. */}
|
||||
{isFavorite(themeId) && <Star className="ml-1.5 h-3 w-3 fill-current opacity-80" />}
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
{/*
|
||||
@@ -169,15 +175,34 @@ export const ThemeSwitcher = () => {
|
||||
setTheme(t.id);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={`w-full rounded-md px-4 py-1 text-left transition-all cursor-pointer ${
|
||||
className={`flex w-full items-center gap-2 rounded-md px-4 py-1 text-left transition-all cursor-pointer ${
|
||||
isFocused ? 'text-2xl font-extrabold' : 'text-lg opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
{t.name}
|
||||
{isFavorite(t.id) && (
|
||||
<Star className="ml-2 inline h-3 w-3 fill-current align-middle opacity-70" />
|
||||
)}
|
||||
{t.id === themeId && <span className="ml-2 align-middle text-xs opacity-70">active</span>}
|
||||
<span className="min-w-0 flex-1 truncate">{t.name}</span>
|
||||
{t.id === themeId && <span className="shrink-0 text-xs opacity-70">active</span>}
|
||||
{/*
|
||||
A span, not a button: this sits inside the row's own <button>, and a button inside
|
||||
a button is invalid HTML that browsers resolve by dropping one of them. The click
|
||||
handler and the role are set explicitly instead, and `f` does the same thing from
|
||||
the keyboard.
|
||||
*/}
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
aria-label={isFavorite(t.id) ? `Unstar ${t.name}` : `Star ${t.name}`}
|
||||
title={isFavorite(t.id) ? 'Remove from favourites (f)' : 'Add to favourites (f)'}
|
||||
onClick={(ev) => {
|
||||
// Or the row's own onClick applies the theme and closes the overlay, which is
|
||||
// the opposite of what starring is for.
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggle(t.id);
|
||||
}}
|
||||
className="shrink-0 rounded p-1 opacity-70 hover:opacity-100"
|
||||
>
|
||||
<Star className={`h-4 w-4 ${isFavorite(t.id) ? 'fill-current' : ''}`} />
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
@@ -186,7 +211,7 @@ export const ThemeSwitcher = () => {
|
||||
{/* Always visible: it sits outside the scroll box above, for the same reason the dropdown's
|
||||
header does. */}
|
||||
<p className="mt-6 text-center text-xs opacity-60">
|
||||
j/k or ↑/↓ move · l or → preview · h or ← reset · enter keep · esc cancel
|
||||
j/k or ↑/↓ move · l or → preview · f star · h or ← reset · enter keep · esc cancel
|
||||
</p>
|
||||
</div>
|
||||
</div>,
|
||||
|
||||
Reference in New Issue
Block a user