From d0f83b58c9f6c7e2df1bbd035b8128f6241987de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 25 Aug 2026 17:06:46 +0000 Subject: [PATCH] theme overlay is one plain rotation, favourites in place, tab toggles a favourites filter --- .../Dashboard/Layout/Header/ThemeSwitcher.tsx | 65 ++++++++++++++----- src/apps/officer-web/locales/de-DE.json | 6 +- src/apps/officer-web/locales/en-GB.json | 2 +- src/apps/officer-web/locales/es-ES.json | 2 +- src/apps/officer-web/locales/fr-FR.json | 2 +- src/apps/officer-web/locales/pt-PT.json | 2 +- src/apps/officer-web/locales/sv-SE.json | 2 +- src/workspaces/theme/src/useThemeKeys.ts | 21 ++++-- 8 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Header/ThemeSwitcher.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Header/ThemeSwitcher.tsx index bca49b61..60d4905f 100644 --- a/src/apps/officer-web/Screens/Dashboard/Layout/Header/ThemeSwitcher.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Layout/Header/ThemeSwitcher.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import { Check, Star } from 'lucide-react'; import { favouritesFirst, groupByFamily, rotateToSelected } from 'theme'; @@ -38,7 +38,7 @@ export const ThemeSwitcher = () => { const [onlyFavorites, setOnlyFavorites] = useState(false); /* - One ordering, used by both lists. + The DROPDOWN's ordering. ROTATED to the theme you are wearing rather than sorted: with 200+ installed, the list's only structure is the registry order — families sit together in it — and lifting the selected theme out of its family @@ -55,16 +55,35 @@ 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(',')], - ); + /* + The OVERLAY's ordering: the same rotation, favourites IN PLACE. - const { open, setOpen, focused, setFocused } = useThemeKeys({ onToggleFavorite: toggle, browseOrder }); + The dropdown lifts favourites to a top block, and the overlay used to as well — which made keyboard + browsing hop into the block and back out, because the rotation and the display disagreed about what + "next" meant. In the overlay you are browsing, not picking from a menu, so it shows ONE list in + rotation order and the favourites filter (`tab`, or the star button) is how the shortlist is reached. + */ + const overlayList = useMemo(() => { + const rotated = rotateToSelected(themes, themeId); + return onlyFavorites ? rotated.filter((entry) => favorites.has(entry.id)) : rotated; + }, [themes, themeId, onlyFavorites, [...favorites].sort().join(',')]); + + const browseOrder = useMemo(() => overlayList.map((entry) => entry.id), [overlayList]); + + const { open, setOpen, focused, setFocused } = useThemeKeys({ + onToggleFavorite: toggle, + browseOrder, + onToggleFilter: () => setOnlyFavorites((was) => !was), + }); + + // The filter can pull the focused theme out from under the browse — flipping to favourites-only while + // an unstarred one is focused. Land on the first of what is now shown rather than focusing nothing. + useEffect(() => { + if (!open) return; + if (overlayList.some((entry) => entry.id === focused)) return; + const first = overlayList[0]; + if (first) setFocused(first.id); + }, [open, overlayList, focused, setFocused]); const families = useMemo(() => groupByFamily(ordered), [ordered]); @@ -179,11 +198,27 @@ export const ThemeSwitcher = () => { }} onClick={(ev) => ev.stopPropagation()} > - {/* The SAME `ordered` list as the dropdown — rotated to what you are wearing, favourites - first. Both lists agreeing is the point: "the one below the one I have on" has to mean the - same thing in both, or the browse teaches you an order the menu then contradicts. */} + {/* The filter, mirrored from the dropdown — same state, so the two surfaces never disagree + about what "the list" currently is. `tab` does the same thing from the keyboard. */} +
+ +
+ {overlayList.length === 0 && ( +

{t('theme.noFavourites')}

+ )}