Organize/components/theme/theme-toggle.tsx

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>
);
}