feat: add shadcn theme switcher
This commit is contained in:
@@ -1,14 +1,17 @@
|
|||||||
import { AppShell } from './layout/AppShell'
|
import { AppShell } from './layout/AppShell'
|
||||||
import { Router } from './router'
|
import { Router } from './router'
|
||||||
|
import { AppThemeProvider } from '@/components/app/AppThemeProvider'
|
||||||
import { AuthProvider, useAuth } from '@/features/auth/AuthProvider'
|
import { AuthProvider, useAuth } from '@/features/auth/AuthProvider'
|
||||||
import { LoginPage } from '@/features/auth/LoginPage'
|
import { LoginPage } from '@/features/auth/LoginPage'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AppThemeProvider>
|
||||||
<AppContent />
|
<AuthProvider>
|
||||||
</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 { Bell, ChevronRight, LogOut, Menu, UserRound } from 'lucide-react'
|
||||||
|
|
||||||
|
import { AppThemeToggle } from '@/components/app/AppThemeToggle'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
@@ -34,6 +35,7 @@ export function AppTopbar({ onMobileMenuClick }: AppTopbarProps) {
|
|||||||
<Button variant="ghost" size="icon" aria-label="通知">
|
<Button variant="ghost" size="icon" aria-label="通知">
|
||||||
<Bell data-icon="inline-start" />
|
<Bell data-icon="inline-start" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<AppThemeToggle />
|
||||||
<Button variant="ghost" className="hidden gap-2 sm:inline-flex">
|
<Button variant="ghost" className="hidden gap-2 sm:inline-flex">
|
||||||
<UserRound data-icon="inline-start" />
|
<UserRound data-icon="inline-start" />
|
||||||
<span>{user?.display_name || user?.username || '当前用户'}</span>
|
<span>{user?.display_name || user?.username || '当前用户'}</span>
|
||||||
|
|||||||
@@ -39,6 +39,41 @@
|
|||||||
--sidebar-ring: 213 52% 32%;
|
--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 {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: hsl(var(--background));
|
background: hsl(var(--background));
|
||||||
|
|||||||
Reference in New Issue
Block a user