import { createPortal } from 'react-dom'; import { Palette, Check } from 'lucide-react'; import * as Dropdown from '@/components/ui/dropdown-menu'; import type { Theme } from '@/components/ui/ThemeProvider'; import { useTheme, useThemeKeys } from '@/components/ui/ThemeProvider'; 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+alt+space` 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 over a * translucent backdrop rather than a modal dialog: you are meant to watch the application change while * you browse, so as little of it as possible should be covered. * * Ported from the owner's `vimazing`, keys and all. */ export const ThemeSwitcher = () => { const { theme, themeId, setTheme, themes } = useTheme(); const { open, setOpen, focused, setFocused } = useThemeKeys(); // Grouped by family, so `Duck Light`/`Duck Dark` and the two Catppuccins sit together. The registry is // readonly, so this builds mutable arrays rather than pushing into its own type. const families = themes.reduce>((acc, t) => { (acc[t.family] ??= []).push(t); return acc; }, {}); return ( <> ctrl+alt+space · ctrl+shift+h/l {Object.entries(families).map(([family, entries]) => (
{entries.map((t) => ( setTheme(t.id)} className="cursor-pointer"> {t.name} ))}
))}
{open && createPortal(
setOpen(false)} >
ev.stopPropagation()} >
    {themes.map((t) => { const isFocused = t.id === focused; return (
  • ); })}

j/k move · l preview · h reset · enter keep · esc cancel

, document.body, )} ); };