96 lines
3.7 KiB
TypeScript
96 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import { useTheme } from "@/components/theme/theme-provider";
|
|
import { Moon, Sun, Monitor, Sunset, Waves, Check } from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
|
|
// The four looks, shown as two "modes" with a shared System row. Each entry
|
|
// is one next-themes value (see app/layout.tsx for the class mapping); the
|
|
// little dot is the theme's own accent so the picker previews the vibe.
|
|
const OPTIONS = [
|
|
{ value: "default", label: "Default", icon: Sun, swatch: "#4f56e0" },
|
|
{ value: "sunset", label: "Sunset", icon: Sunset, swatch: "#c2410c" },
|
|
{ value: "dark", label: "Dark", icon: Moon, swatch: "#7c83f2" },
|
|
{ value: "ocean", label: "Ocean", icon: Waves, swatch: "#5eead4" },
|
|
] as const;
|
|
|
|
const CURRENT_ICONS = {
|
|
default: Sun,
|
|
sunset: Sunset,
|
|
dark: Moon,
|
|
ocean: Waves,
|
|
system: Monitor,
|
|
} as const;
|
|
|
|
export function ThemeToggle({ collapsed }: { collapsed?: boolean }) {
|
|
const { theme, setTheme } = useTheme();
|
|
// The picker always shows the four real themes; "system" is handled by a
|
|
// dedicated row since it resolves to one of them per device preference.
|
|
const known = OPTIONS.find((o) => o.value === theme);
|
|
const isSystem = theme === "system" || theme === undefined;
|
|
const Icon = (isSystem ? CURRENT_ICONS.system : known?.icon) ?? CURRENT_ICONS.system;
|
|
const label = isSystem ? "System" : known?.label ?? "Default";
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
render={
|
|
<Button
|
|
variant="ghost"
|
|
size={collapsed ? "icon" : "default"}
|
|
className={collapsed ? undefined : "w-full justify-start gap-2"}
|
|
>
|
|
<Icon className="size-4" />
|
|
{!collapsed && <span>Theme</span>}
|
|
</Button>
|
|
}
|
|
/>
|
|
<DropdownMenuContent align="start" side="right" className="w-52">
|
|
<DropdownMenuLabel>Appearance</DropdownMenuLabel>
|
|
{OPTIONS.map((option) => (
|
|
<button
|
|
key={option.value}
|
|
type="button"
|
|
onClick={() => setTheme(option.value)}
|
|
className="group relative flex w-full cursor-pointer items-center gap-2.5 rounded-md px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50"
|
|
>
|
|
<option.icon className="size-4 text-muted-foreground group-focus:text-current" />
|
|
<span>{option.label}</span>
|
|
<span
|
|
className="ml-auto size-3 rounded-full ring-1 ring-foreground/15"
|
|
style={{ backgroundColor: option.swatch }}
|
|
aria-hidden
|
|
/>
|
|
{theme === option.value && <Check className="absolute right-2 -mr-4 size-4" />}
|
|
</button>
|
|
))}
|
|
<DropdownMenuSeparator />
|
|
<button
|
|
type="button"
|
|
onClick={() => setTheme("system")}
|
|
className="relative flex w-full cursor-pointer items-center gap-2.5 rounded-md px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50"
|
|
>
|
|
<Monitor className="size-4 text-muted-foreground" />
|
|
<span>Match system</span>
|
|
<span
|
|
className="ml-auto size-3 rounded-full ring-1 ring-foreground/15"
|
|
style={{
|
|
background: "linear-gradient(90deg, #4f56e0 50%, #7c83f2 50%)",
|
|
}}
|
|
aria-hidden
|
|
/>
|
|
{isSystem && <Check className="absolute right-2 -mr-4 size-4" />}
|
|
</button>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|