Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
196 lines
4.8 KiB
TypeScript
196 lines
4.8 KiB
TypeScript
'use client';
|
|
|
|
import { useMemo } from 'react';
|
|
|
|
import { Computer, Moon, Sun } from 'lucide-react';
|
|
import { useTheme } from 'next-themes';
|
|
|
|
import { cn } from '../lib/utils';
|
|
import { Button } from '../shadcn/button';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '../shadcn/card';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuGroup,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSub,
|
|
DropdownMenuSubContent,
|
|
DropdownMenuSubTrigger,
|
|
DropdownMenuTrigger,
|
|
} from '../shadcn/dropdown-menu';
|
|
import { Trans } from './trans';
|
|
|
|
const MODES = ['light', 'dark', 'system'];
|
|
|
|
export function ModeToggle(props: { className?: string }) {
|
|
const { setTheme, theme } = useTheme();
|
|
|
|
const Items = useMemo(() => {
|
|
return MODES.map((mode) => {
|
|
const isSelected = theme === mode;
|
|
|
|
return (
|
|
<DropdownMenuItem
|
|
className={cn('space-x-2', {
|
|
'bg-muted': isSelected,
|
|
})}
|
|
key={mode}
|
|
onClick={() => {
|
|
setTheme(mode);
|
|
setCookeTheme(mode);
|
|
}}
|
|
>
|
|
<Icon theme={mode} />
|
|
|
|
<span>
|
|
<Trans i18nKey={`common.${mode}Theme`} />
|
|
</span>
|
|
</DropdownMenuItem>
|
|
);
|
|
});
|
|
}, [setTheme, theme]);
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
render={
|
|
<Button variant="ghost" size="icon" className={props.className} />
|
|
}
|
|
>
|
|
<Sun className="h-[0.9rem] w-[0.9rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
|
<Moon className="absolute h-[0.9rem] w-[0.9rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
|
<span className="sr-only">Toggle theme</span>
|
|
</DropdownMenuTrigger>
|
|
|
|
<DropdownMenuContent align="end">{Items}</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|
|
|
|
export function SubMenuModeToggle() {
|
|
const { setTheme, theme, resolvedTheme } = useTheme();
|
|
|
|
const MenuItems = useMemo(
|
|
() =>
|
|
MODES.map((mode) => {
|
|
const isSelected = theme === mode;
|
|
|
|
return (
|
|
<DropdownMenuItem
|
|
className={cn('flex cursor-pointer items-center gap-x-2', {
|
|
'bg-muted': isSelected,
|
|
})}
|
|
key={mode}
|
|
onClick={() => {
|
|
setTheme(mode);
|
|
setCookeTheme(mode);
|
|
}}
|
|
>
|
|
<Icon theme={mode} />
|
|
|
|
<Trans i18nKey={`common.${mode}Theme`} />
|
|
</DropdownMenuItem>
|
|
);
|
|
}),
|
|
[setTheme, theme],
|
|
);
|
|
|
|
return (
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuSub>
|
|
<DropdownMenuSubTrigger
|
|
className={
|
|
'hidden w-full items-center justify-between gap-x-2 lg:flex'
|
|
}
|
|
>
|
|
<Icon theme={resolvedTheme} />
|
|
|
|
<Trans i18nKey={'common.theme'} />
|
|
</DropdownMenuSubTrigger>
|
|
|
|
<DropdownMenuSubContent>{MenuItems}</DropdownMenuSubContent>
|
|
</DropdownMenuSub>
|
|
|
|
<div className={'lg:hidden'}>
|
|
<DropdownMenuLabel>
|
|
<Trans i18nKey={'common.theme'} />
|
|
</DropdownMenuLabel>
|
|
|
|
{MenuItems}
|
|
</div>
|
|
</DropdownMenuGroup>
|
|
);
|
|
}
|
|
|
|
function setCookeTheme(theme: string) {
|
|
document.cookie = `theme=${theme}; path=/; max-age=31536000`;
|
|
}
|
|
|
|
function Icon({ theme }: { theme: string | undefined }) {
|
|
switch (theme) {
|
|
case 'light':
|
|
return <Sun className="h-4" />;
|
|
case 'dark':
|
|
return <Moon className="h-4" />;
|
|
case 'system':
|
|
return <Computer className="h-4" />;
|
|
}
|
|
}
|
|
|
|
export function ThemePreferenceCard({
|
|
currentTheme,
|
|
}: {
|
|
currentTheme: string;
|
|
}) {
|
|
const { setTheme, theme = currentTheme } = useTheme();
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>
|
|
<Trans i18nKey="common.theme" />
|
|
</CardTitle>
|
|
|
|
<CardDescription>
|
|
<Trans i18nKey="common.themeDescription" />
|
|
</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
<div className="grid grid-cols-3 gap-3">
|
|
{MODES.map((mode) => {
|
|
const isSelected = theme === mode;
|
|
|
|
return (
|
|
<Button
|
|
key={mode}
|
|
variant={isSelected ? 'default' : 'outline'}
|
|
className={'flex items-center justify-center gap-2'}
|
|
onClick={() => {
|
|
setTheme(mode);
|
|
setCookeTheme(mode);
|
|
}}
|
|
>
|
|
{mode === 'light' && <Sun className="size-4" />}
|
|
{mode === 'dark' && <Moon className="size-4" />}
|
|
{mode === 'system' && <Computer className="size-4" />}
|
|
|
|
<span className="text-sm">
|
|
<Trans i18nKey={`common.${mode}Theme`} />
|
|
</span>
|
|
</Button>
|
|
);
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|