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. */}
+
diff --git a/src/apps/officer-web/locales/de-DE.json b/src/apps/officer-web/locales/de-DE.json
index 58b983d8..b4f7e992 100644
--- a/src/apps/officer-web/locales/de-DE.json
+++ b/src/apps/officer-web/locales/de-DE.json
@@ -478,11 +478,11 @@
"codeEditor": "Code-Editor"
},
"theme": {
- "pillTitle": "Design: {{name}} · Strg+Umschalt+←/→ durchblättern · Strg+Umschalt+Leertaste durchsuchen · Strg+Umschalt+F {{starAction}}",
+ "pillTitle": "Design: {{name}} · Strg+Umschalt+←/→ durchblättern · Strg+Umschalt+↑/↓ durchsuchen · Strg+Umschalt+F {{starAction}}",
"pillLabel": "Design: {{name}}",
"star": "markieren",
"unstar": "Markierung aufheben",
- "cycleHint": "Strg+Umschalt+←/→ durchblättern · Strg+Umschalt+Leertaste durchsuchen",
+ "cycleHint": "Strg+Umschalt+←/→ durchblättern · Strg+Umschalt+↑/↓ durchsuchen",
"everyShortcutHint": "Strg+? für alle Tastenkombinationen",
"favouritesOnly": "Nur Favoriten ({{count}})",
"favourites": "Favoriten ({{count}})",
@@ -494,7 +494,7 @@
"addToFavouritesKey": "Zu Favoriten hinzufügen (f)",
"removeFromFavouritesKey": "Aus Favoriten entfernen (f)",
"active": "aktiv",
- "browseHint": "j/k oder ↑/↓ bewegen · l oder → Vorschau · f markieren · h oder ← zurücksetzen · Eingabetaste behalten · Esc abbrechen"
+ "browseHint": "j/k oder ↑/↓ bewegen · l oder → Vorschau · f markieren · Tab Favoriten · h oder ← zurücksetzen · Eingabetaste behalten · Esc abbrechen"
},
"spotlight": {
"placeholder": "Gehe zu…",
diff --git a/src/apps/officer-web/locales/en-GB.json b/src/apps/officer-web/locales/en-GB.json
index f07249f9..29cfa63d 100644
--- a/src/apps/officer-web/locales/en-GB.json
+++ b/src/apps/officer-web/locales/en-GB.json
@@ -494,7 +494,7 @@
"addToFavouritesKey": "Add to favourites (f)",
"removeFromFavouritesKey": "Remove from favourites (f)",
"active": "active",
- "browseHint": "j/k or ↑/↓ move · l or → preview · f star · h or ← reset · enter keep · esc cancel"
+ "browseHint": "j/k or ↑/↓ move · l or → preview · f star · tab favourites · h or ← reset · enter keep · esc cancel"
},
"spotlight": {
"placeholder": "Go to…",
diff --git a/src/apps/officer-web/locales/es-ES.json b/src/apps/officer-web/locales/es-ES.json
index dd416d51..c72f79fe 100644
--- a/src/apps/officer-web/locales/es-ES.json
+++ b/src/apps/officer-web/locales/es-ES.json
@@ -494,7 +494,7 @@
"addToFavouritesKey": "Añadir a favoritos (f)",
"removeFromFavouritesKey": "Quitar de favoritos (f)",
"active": "activo",
- "browseHint": "j/k o ↑/↓ mover · l o → previsualizar · f marcar · h o ← reiniciar · enter mantener · esc cancelar"
+ "browseHint": "j/k o ↑/↓ mover · l o → previsualizar · f marcar · tab favoritos · h o ← reiniciar · enter mantener · esc cancelar"
},
"spotlight": {
"placeholder": "Ir a…",
diff --git a/src/apps/officer-web/locales/fr-FR.json b/src/apps/officer-web/locales/fr-FR.json
index 8f091e17..afb0ee73 100644
--- a/src/apps/officer-web/locales/fr-FR.json
+++ b/src/apps/officer-web/locales/fr-FR.json
@@ -494,7 +494,7 @@
"addToFavouritesKey": "Ajouter aux favoris (f)",
"removeFromFavouritesKey": "Retirer des favoris (f)",
"active": "actif",
- "browseHint": "j/k ou ↑/↓ déplacer · l ou → aperçu · f marquer · h ou ← réinitialiser · entrée garder · esc annuler"
+ "browseHint": "j/k ou ↑/↓ déplacer · l ou → aperçu · f marquer · tab favoris · h ou ← réinitialiser · entrée garder · esc annuler"
},
"spotlight": {
"placeholder": "Aller à…",
diff --git a/src/apps/officer-web/locales/pt-PT.json b/src/apps/officer-web/locales/pt-PT.json
index da7535fb..5c152a50 100644
--- a/src/apps/officer-web/locales/pt-PT.json
+++ b/src/apps/officer-web/locales/pt-PT.json
@@ -494,7 +494,7 @@
"addToFavouritesKey": "Adicionar aos favoritos (f)",
"removeFromFavouritesKey": "Remover dos favoritos (f)",
"active": "ativo",
- "browseHint": "j/k ou ↑/↓ mover · l ou → pré-visualizar · f marcar · h ou ← reiniciar · enter manter · esc cancelar"
+ "browseHint": "j/k ou ↑/↓ mover · l ou → pré-visualizar · f marcar · tab favoritos · h ou ← reiniciar · enter manter · esc cancelar"
},
"spotlight": {
"placeholder": "Ir para…",
diff --git a/src/apps/officer-web/locales/sv-SE.json b/src/apps/officer-web/locales/sv-SE.json
index 0bc4748b..08154979 100644
--- a/src/apps/officer-web/locales/sv-SE.json
+++ b/src/apps/officer-web/locales/sv-SE.json
@@ -494,7 +494,7 @@
"addToFavouritesKey": "Lägg till i favoriter (f)",
"removeFromFavouritesKey": "Ta bort från favoriter (f)",
"active": "aktiv",
- "browseHint": "j/k eller ↑/↓ flytta · l eller → förhandsgranska · f stjärna · h eller ← återställ · enter behåll · esc avbryt"
+ "browseHint": "j/k eller ↑/↓ flytta · l eller → förhandsgranska · f stjärna · tab favoriter · h eller ← återställ · enter behåll · esc avbryt"
},
"spotlight": {
"placeholder": "Gå till…",
diff --git a/src/workspaces/theme/src/useThemeKeys.ts b/src/workspaces/theme/src/useThemeKeys.ts
index 905ab698..72908763 100644
--- a/src/workspaces/theme/src/useThemeKeys.ts
+++ b/src/workspaces/theme/src/useThemeKeys.ts
@@ -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);
const startedOn = useRef(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':