theme browse steps the displayed list, favourites block out of the rotation
This commit is contained in:
@@ -35,8 +35,6 @@ export const ThemeSwitcher = () => {
|
||||
const { theme, themeId, setTheme, themes } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
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);
|
||||
|
||||
/*
|
||||
@@ -57,6 +55,17 @@ export const ThemeSwitcher = () => {
|
||||
// `favorites` is a fresh Set each render; its CONTENTS are what matter, so key on those.
|
||||
}, [themes, themeId, onlyFavorites, [...favorites].sort().join(',')]);
|
||||
|
||||
// What `j`/`k` walk in the overlay: the displayed list MINUS the pinned favourites block. The block is
|
||||
// a shortlist sitting outside the rotation — stepping through it and the main list as one ring made the
|
||||
// focus hop up and back on every starred registry-neighbour. With the favourites filter on, the
|
||||
// favourites ARE the list, so then they are what gets browsed.
|
||||
const browseOrder = useMemo(
|
||||
() => (onlyFavorites ? ordered : ordered.filter((entry) => !favorites.has(entry.id))).map((entry) => entry.id),
|
||||
[ordered, onlyFavorites, [...favorites].sort().join(',')],
|
||||
);
|
||||
|
||||
const { open, setOpen, focused, setFocused } = useThemeKeys({ onToggleFavorite: toggle, browseOrder });
|
||||
|
||||
const families = useMemo(() => groupByFamily(ordered), [ordered]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -36,6 +36,13 @@ 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.
|
||||
*
|
||||
* ── Not a UI ──
|
||||
*
|
||||
* This hook owns the keys and the selection; something else draws the list. It lives in the theme package
|
||||
@@ -49,6 +56,11 @@ type ThemeKeysOptions = {
|
||||
* knowledge. This hook owns keys and selection and nothing else.
|
||||
*/
|
||||
onToggleFavorite?: (themeId: ThemeId) => void;
|
||||
/**
|
||||
* The ids `j`/`k` traverse while the list is open, in DISPLAY order — see the note above. Absent, the
|
||||
* browse falls back to the full registry rotation.
|
||||
*/
|
||||
browseOrder?: readonly ThemeId[];
|
||||
};
|
||||
|
||||
export function useThemeKeys(options: ThemeKeysOptions = {}) {
|
||||
@@ -57,6 +69,8 @@ export function useThemeKeys(options: ThemeKeysOptions = {}) {
|
||||
// on every render, since it closes over a mutation.
|
||||
const onToggleFavorite = useRef(options.onToggleFavorite);
|
||||
onToggleFavorite.current = options.onToggleFavorite;
|
||||
const browseOrder = useRef(options.browseOrder);
|
||||
browseOrder.current = options.browseOrder;
|
||||
const [open, setOpen] = useState(false);
|
||||
const [focused, setFocused] = useState<ThemeId>(themeId);
|
||||
const startedOn = useRef<ThemeId>(themeId);
|
||||
@@ -74,6 +88,20 @@ export function useThemeKeys(options: ThemeKeysOptions = {}) {
|
||||
[themes],
|
||||
);
|
||||
|
||||
// The in-list step. Walks `browseOrder` when given; a focus that is not IN that order — the current
|
||||
// theme is a favourite, or the mouse hovered one — enters the rotation at its nearest end rather than
|
||||
// wrapping from an index that does not exist.
|
||||
const browseStep = useCallback(
|
||||
(from: ThemeId, delta: number): ThemeId => {
|
||||
const ids = browseOrder.current;
|
||||
if (!ids || ids.length === 0) return step(from, delta);
|
||||
const at = ids.indexOf(from);
|
||||
if (at === -1) return delta > 0 ? ids[0]! : ids[ids.length - 1]!;
|
||||
return ids[(at + delta + ids.length) % ids.length]!;
|
||||
},
|
||||
[step],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const { themeId: current, focused: focus, open: isOpen } = state.current;
|
||||
@@ -164,13 +192,13 @@ export function useThemeKeys(options: ThemeKeysOptions = {}) {
|
||||
case 'J':
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
setFocused((prev) => step(prev, 1));
|
||||
setFocused((prev) => browseStep(prev, 1));
|
||||
return;
|
||||
case 'k':
|
||||
case 'K':
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
setFocused((prev) => step(prev, -1));
|
||||
setFocused((prev) => browseStep(prev, -1));
|
||||
return;
|
||||
case 'l':
|
||||
case 'L':
|
||||
@@ -204,7 +232,7 @@ export function useThemeKeys(options: ThemeKeysOptions = {}) {
|
||||
// the dropdown trigger opening alongside the overlay.
|
||||
window.addEventListener('keydown', onKeyDown, true);
|
||||
return () => window.removeEventListener('keydown', onKeyDown, true);
|
||||
}, [setTheme, step]);
|
||||
}, [setTheme, step, browseStep]);
|
||||
|
||||
return { open, setOpen, focused, setFocused };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user