import { useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import { Check, Star } from 'lucide-react';
import { favouritesFirst, groupByFamily, rotateToSelected } from 'theme';
import * as Dropdown from '@/components/ui/dropdown-menu';
import { useTheme, useThemeKeys } from '@/components/ui/ThemeProvider';
import { useThemeFavorites } from '@/state/useThemeFavorites';
import { useTranslation } from '@/lib/i18n';
const {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} = Dropdown;
/**
* Themes, out of the avatar menu and into the header.
*
* They were buried under an avatar next to "Sign out", which is where a preference goes when nobody has
* decided it matters. With fourteen of them it is a thing you use, so it gets its own control.
*
* ── The overlay ──
*
* `ctrl+shift+↑/↓` opens a list in the middle of the screen; `j`/`k` move through it and `l` applies the
* focused theme to the app BEHIND it. That is the whole idea, and it is why this is a portal with no
* backdrop rather than a modal dialog: you are meant to watch the application change while you browse,
* so nothing of it should be covered.
*
* Ported from the owner's `vimazing`, keys and all.
*/
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);
/*
One ordering, used by both lists.
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
would answer "which am I on" by destroying "what is next to it". A rotation answers both. See
`theme/rotate.ts`.
Then favourites first, which is a second rotation-preserving pass rather than a filter: each half keeps
the order it was given, so the theme you are wearing still leads.
*/
const ordered = useMemo(() => {
const rotated = rotateToSelected(themes, themeId);
const listed = onlyFavorites ? rotated.filter((entry) => favorites.has(entry.id)) : rotated;
return favouritesFirst(listed, favorites);
// `favorites` is a fresh Set each render; its CONTENTS are what matter, so key on those.
}, [themes, themeId, onlyFavorites, [...favorites].sort().join(',')]);
const families = useMemo(() => groupByFamily(ordered), [ordered]);
return (
<>
{/*
The CONTENT does not scroll — the list inside it does.
It used to carry `overflow-y-auto` itself, which scrolled the instruction line away with the
themes. Two hundred entries meant the one line telling you the keyboard shortcuts existed was
visible for the first half-second and then gone for good, which is the same as not having written
it. Header and footer are now siblings of a scroll box rather than passengers in one.
*/}
{entries.map((entry) => (
setTheme(entry.id)} className="cursor-pointer pr-1">
{entry.name}
{/* A sibling of the row's action, never nested inside it — starring is not choosing. */}
))}
))
)}
{open &&
createPortal(
setOpen(false)}
>
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. */}