mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
112 lines
2.8 KiB
TypeScript
112 lines
2.8 KiB
TypeScript
'use client';
|
|
import { cva } from 'class-variance-authority';
|
|
import { Airplay, Moon, Sun } from 'lucide-react';
|
|
import { useTheme } from 'next-themes';
|
|
import { type HTMLAttributes, useLayoutEffect, useState } from 'react';
|
|
import { cn } from '../../lib/cn';
|
|
|
|
const itemVariants = cva(
|
|
'size-6.5 rounded-full p-1.5 text-fd-muted-foreground',
|
|
{
|
|
variants: {
|
|
active: {
|
|
true: 'bg-fd-accent text-fd-accent-foreground',
|
|
false: 'text-fd-muted-foreground',
|
|
},
|
|
},
|
|
},
|
|
);
|
|
|
|
const full = [
|
|
['light', Sun] as const,
|
|
['dark', Moon] as const,
|
|
['system', Airplay] as const,
|
|
];
|
|
|
|
export function ThemeToggle({
|
|
className,
|
|
mode = 'light-dark',
|
|
compact = false,
|
|
...props
|
|
}: HTMLAttributes<HTMLElement> & {
|
|
mode?: 'light-dark' | 'light-dark-system',
|
|
compact?: boolean,
|
|
}) {
|
|
const { setTheme, theme, resolvedTheme } = useTheme();
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
useLayoutEffect(() => {
|
|
setMounted(true);
|
|
}, []);
|
|
|
|
// Compact mode: single icon button without container
|
|
if (compact) {
|
|
const value = mounted ? resolvedTheme : null;
|
|
const Icon = value === 'light' ? Moon : Sun;
|
|
|
|
return (
|
|
<button
|
|
className={cn(
|
|
'inline-flex h-8 w-8 items-center justify-center rounded-full text-fd-muted-foreground transition-colors hover:bg-fd-muted hover:text-fd-foreground',
|
|
className,
|
|
)}
|
|
aria-label={`Switch to ${value === 'light' ? 'dark' : 'light'} theme`}
|
|
onClick={() => setTheme(value === 'light' ? 'dark' : 'light')}
|
|
data-theme-toggle=""
|
|
title={`Switch to ${value === 'light' ? 'dark' : 'light'} theme`}
|
|
{...props}
|
|
>
|
|
<Icon className="h-3.5 w-3.5" fill="currentColor" />
|
|
</button>
|
|
);
|
|
}
|
|
|
|
const container = cn(
|
|
'inline-flex items-center rounded-full border p-1',
|
|
className,
|
|
);
|
|
|
|
if (mode === 'light-dark') {
|
|
const value = mounted ? resolvedTheme : null;
|
|
|
|
return (
|
|
<button
|
|
className={container}
|
|
aria-label={`Toggle Theme`}
|
|
onClick={() => setTheme(value === 'light' ? 'dark' : 'light')}
|
|
data-theme-toggle=""
|
|
{...props}
|
|
>
|
|
{full.map(([key, Icon]) => {
|
|
if (key === 'system') return;
|
|
|
|
return (
|
|
<Icon
|
|
key={key}
|
|
fill="currentColor"
|
|
className={cn(itemVariants({ active: value === key }))}
|
|
/>
|
|
);
|
|
})}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
const value = mounted ? theme : null;
|
|
|
|
return (
|
|
<div className={container} data-theme-toggle="" {...props}>
|
|
{full.map(([key, Icon]) => (
|
|
<button
|
|
key={key}
|
|
aria-label={key}
|
|
className={cn(itemVariants({ active: value === key }))}
|
|
onClick={() => setTheme(key)}
|
|
>
|
|
<Icon className="size-full" fill="currentColor" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|