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 { theme, themeId, setTheme, themes } = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { favorites, isFavorite, toggle } = useThemeFavorites();
|
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);
|
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.
|
// `favorites` is a fresh Set each render; its CONTENTS are what matter, so key on those.
|
||||||
}, [themes, themeId, onlyFavorites, [...favorites].sort().join(',')]);
|
}, [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]);
|
const families = useMemo(() => groupByFamily(ordered), [ordered]);
|
||||||
|
|
||||||
return (
|
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
|
* 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.
|
* 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 ──
|
* ── Not a UI ──
|
||||||
*
|
*
|
||||||
* This hook owns the keys and the selection; something else draws the list. It lives in the theme package
|
* 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.
|
* knowledge. This hook owns keys and selection and nothing else.
|
||||||
*/
|
*/
|
||||||
onToggleFavorite?: (themeId: ThemeId) => void;
|
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 = {}) {
|
export function useThemeKeys(options: ThemeKeysOptions = {}) {
|
||||||
@@ -57,6 +69,8 @@ export function useThemeKeys(options: ThemeKeysOptions = {}) {
|
|||||||
// on every render, since it closes over a mutation.
|
// on every render, since it closes over a mutation.
|
||||||
const onToggleFavorite = useRef(options.onToggleFavorite);
|
const onToggleFavorite = useRef(options.onToggleFavorite);
|
||||||
onToggleFavorite.current = options.onToggleFavorite;
|
onToggleFavorite.current = options.onToggleFavorite;
|
||||||
|
const browseOrder = useRef(options.browseOrder);
|
||||||
|
browseOrder.current = options.browseOrder;
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [focused, setFocused] = useState<ThemeId>(themeId);
|
const [focused, setFocused] = useState<ThemeId>(themeId);
|
||||||
const startedOn = useRef<ThemeId>(themeId);
|
const startedOn = useRef<ThemeId>(themeId);
|
||||||
@@ -74,6 +88,20 @@ export function useThemeKeys(options: ThemeKeysOptions = {}) {
|
|||||||
[themes],
|
[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(() => {
|
useEffect(() => {
|
||||||
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;
|
||||||
@@ -164,13 +192,13 @@ export function useThemeKeys(options: ThemeKeysOptions = {}) {
|
|||||||
case 'J':
|
case 'J':
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setFocused((prev) => step(prev, 1));
|
setFocused((prev) => browseStep(prev, 1));
|
||||||
return;
|
return;
|
||||||
case 'k':
|
case 'k':
|
||||||
case 'K':
|
case 'K':
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setFocused((prev) => step(prev, -1));
|
setFocused((prev) => browseStep(prev, -1));
|
||||||
return;
|
return;
|
||||||
case 'l':
|
case 'l':
|
||||||
case 'L':
|
case 'L':
|
||||||
@@ -204,7 +232,7 @@ export function useThemeKeys(options: ThemeKeysOptions = {}) {
|
|||||||
// the dropdown trigger opening alongside the overlay.
|
// the dropdown trigger opening alongside the overlay.
|
||||||
window.addEventListener('keydown', onKeyDown, true);
|
window.addEventListener('keydown', onKeyDown, true);
|
||||||
return () => window.removeEventListener('keydown', onKeyDown, true);
|
return () => window.removeEventListener('keydown', onKeyDown, true);
|
||||||
}, [setTheme, step]);
|
}, [setTheme, step, browseStep]);
|
||||||
|
|
||||||
return { open, setOpen, focused, setFocused };
|
return { open, setOpen, focused, setFocused };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user