theme overlay is one plain rotation, favourites in place, tab toggles a favourites filter

This commit is contained in:
2026-08-25 17:06:46 +00:00
parent 5f803c3fb0
commit d0f83b58c9
8 changed files with 73 additions and 29 deletions
+15 -6
View File
@@ -36,12 +36,11 @@ import type { ThemeId } from './themes';
* The starting theme is captured when the list opens, so `h` and `Escape` can undo an entire browse
* regardless of how many previews happened in between.
*
* Browsing walks `browseOrder` — the list as DISPLAYED — not the registry. The overlay pins favourites
* in a block at the top, and stepping in registry order through a display ordered differently meant the
* focus hopped up into that block and back out whenever a registry-neighbour happened to be starred.
* The favourites block is a shortlist, deliberately outside the rotation; the global cycle
* (`ctrl+shift+←/→`) still covers every theme, because with no list on screen there is no order to
* disagree with.
* Browsing walks `browseOrder` — the list as DISPLAYED — not the registry. The two orders must be the
* same list or the focus visibly jumps, which is exactly what happened when the overlay lifted
* favourites to a top block while stepping walked registry order. The overlay now shows one plain
* rotation and `tab` flips a favourites-only filter (`onToggleFilter`); either way, what you see is
* what `j`/`k` walk.
*
* ── Not a UI ──
*
@@ -61,6 +60,8 @@ type ThemeKeysOptions = {
* browse falls back to the full registry rotation.
*/
browseOrder?: readonly ThemeId[];
/** Flip the favourites-only filter — bound to `tab` while the list is open. App state, like the stars. */
onToggleFilter?: () => void;
};
export function useThemeKeys(options: ThemeKeysOptions = {}) {
@@ -71,6 +72,8 @@ export function useThemeKeys(options: ThemeKeysOptions = {}) {
onToggleFavorite.current = options.onToggleFavorite;
const browseOrder = useRef(options.browseOrder);
browseOrder.current = options.browseOrder;
const onToggleFilter = useRef(options.onToggleFilter);
onToggleFilter.current = options.onToggleFilter;
const [open, setOpen] = useState(false);
const [focused, setFocused] = useState<ThemeId>(themeId);
const startedOn = useRef<ThemeId>(themeId);
@@ -188,6 +191,12 @@ export function useThemeKeys(options: ThemeKeysOptions = {}) {
event.preventDefault();
onToggleFavorite.current?.(focus);
return;
case 'Tab':
// Favourites only ↔ everything. preventDefault or the browser also moves focus, and the
// overlay has nothing focusable to move it to.
event.preventDefault();
onToggleFilter.current?.();
return;
case 'j':
case 'J':
case 'ArrowDown':