feat: add shadcn theme switcher
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
import { AppShell } from './layout/AppShell'
|
||||
import { Router } from './router'
|
||||
import { AppThemeProvider } from '@/components/app/AppThemeProvider'
|
||||
import { AuthProvider, useAuth } from '@/features/auth/AuthProvider'
|
||||
import { LoginPage } from '@/features/auth/LoginPage'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<AppThemeProvider>
|
||||
<AuthProvider>
|
||||
<AppContent />
|
||||
</AuthProvider>
|
||||
</AppThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
14
frontend/src/components/app/AppThemeProvider.tsx
Normal file
14
frontend/src/components/app/AppThemeProvider.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import type * as React from 'react'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
|
||||
type AppThemeProviderProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function AppThemeProvider({ children }: AppThemeProviderProps) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
53
frontend/src/components/app/AppThemeToggle.tsx
Normal file
53
frontend/src/components/app/AppThemeToggle.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Laptop, Moon, Sun } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
|
||||
const themeOptions = [
|
||||
{ value: 'light', label: '亮色', icon: Sun },
|
||||
{ value: 'dark', label: '暗色', icon: Moon },
|
||||
{ value: 'system', label: '跟随系统', icon: Laptop }
|
||||
]
|
||||
|
||||
export function AppThemeToggle() {
|
||||
const { theme = 'system', setTheme } = useTheme()
|
||||
const activeTheme = themeOptions.find((option) => option.value === theme) ?? themeOptions[2]
|
||||
const ActiveIcon = activeTheme.icon
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" aria-label={`主题:${activeTheme.label}`}>
|
||||
<ActiveIcon data-icon="inline-start" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuLabel>界面主题</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
|
||||
{themeOptions.map((option) => {
|
||||
const Icon = option.icon
|
||||
return (
|
||||
<DropdownMenuRadioItem key={option.value} value={option.value}>
|
||||
<Icon data-icon="inline-start" />
|
||||
{option.label}
|
||||
</DropdownMenuRadioItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Bell, ChevronRight, LogOut, Menu, UserRound } from 'lucide-react'
|
||||
|
||||
import { AppThemeToggle } from '@/components/app/AppThemeToggle'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
@@ -34,6 +35,7 @@ export function AppTopbar({ onMobileMenuClick }: AppTopbarProps) {
|
||||
<Button variant="ghost" size="icon" aria-label="通知">
|
||||
<Bell data-icon="inline-start" />
|
||||
</Button>
|
||||
<AppThemeToggle />
|
||||
<Button variant="ghost" className="hidden gap-2 sm:inline-flex">
|
||||
<UserRound data-icon="inline-start" />
|
||||
<span>{user?.display_name || user?.username || '当前用户'}</span>
|
||||
|
||||
@@ -39,6 +39,41 @@
|
||||
--sidebar-ring: 213 52% 32%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222 28% 8%;
|
||||
--foreground: 214 24% 94%;
|
||||
--surface: 222 26% 11%;
|
||||
--muted: 219 22% 17%;
|
||||
--muted-foreground: 215 14% 70%;
|
||||
--border: 219 20% 24%;
|
||||
--primary: 186 52% 46%;
|
||||
--primary-foreground: 222 28% 8%;
|
||||
--success: 151 55% 46%;
|
||||
--warning: 38 86% 58%;
|
||||
--danger: 0 70% 60%;
|
||||
--info: 204 76% 58%;
|
||||
--ai-accent: 186 52% 46%;
|
||||
--card: 222 26% 11%;
|
||||
--card-foreground: 214 24% 94%;
|
||||
--popover: 222 26% 11%;
|
||||
--popover-foreground: 214 24% 94%;
|
||||
--secondary: 219 22% 17%;
|
||||
--secondary-foreground: 214 24% 94%;
|
||||
--accent: 219 22% 17%;
|
||||
--accent-foreground: 214 24% 94%;
|
||||
--destructive: 0 70% 60%;
|
||||
--input: 219 20% 24%;
|
||||
--ring: 186 52% 46%;
|
||||
--sidebar: 222 32% 11%;
|
||||
--sidebar-foreground: 214 24% 92%;
|
||||
--sidebar-primary: 186 52% 46%;
|
||||
--sidebar-primary-foreground: 222 28% 8%;
|
||||
--sidebar-accent: 219 26% 20%;
|
||||
--sidebar-accent-foreground: 0 0% 100%;
|
||||
--sidebar-border: 219 24% 24%;
|
||||
--sidebar-ring: 186 52% 46%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: hsl(var(--background));
|
||||
|
||||
Reference in New Issue
Block a user