feat(web): тёмная тема (system/manual) и фикс логотипа в icon-режиме сайдбара
- ThemeProvider (next-themes, attribute=class, defaultTheme=system) - ModeToggle в header: светлая / тёмная / системная - suppressHydrationWarning на <html> против FOUC - логотип в SidebarHeader через SidebarMenuButton size="lg" (корректное схлопывание в icon-режиме, aspect-square для иконки) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<html lang="ru" suppressHydrationWarning>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
|
||||
@@ -37,6 +37,8 @@ import { Separator } from '@cfdm/ui/components/separator'
|
||||
import { Link, useRouterState } from '@tanstack/react-router'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import { ModeToggle } from '@/components/mode-toggle'
|
||||
|
||||
interface NavItem {
|
||||
to: string
|
||||
label: string
|
||||
@@ -68,15 +70,22 @@ export function AppShell({ children }: { children: ReactNode }) {
|
||||
<SidebarProvider>
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader>
|
||||
<div className="flex items-center gap-2 px-2 py-1.5">
|
||||
<div className="flex size-8 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
||||
<Server className="size-4" />
|
||||
</div>
|
||||
<div className="flex flex-col text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
|
||||
<span className="font-semibold">VPS Tracker</span>
|
||||
<span className="text-xs text-muted-foreground">Учёт виртуальных серверов</span>
|
||||
</div>
|
||||
</div>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg" render={<Link to="/dashboard" />}>
|
||||
<div
|
||||
className="flex aspect-square size-8 items-center justify-center rounded-md bg-primary text-primary-foreground"
|
||||
aria-hidden
|
||||
>
|
||||
<Server className="size-4" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 leading-none">
|
||||
<span className="font-semibold">VPS Tracker</span>
|
||||
<span className="text-xs text-muted-foreground">Учёт виртуальных серверов</span>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
@@ -116,6 +125,9 @@ export function AppShell({ children }: { children: ReactNode }) {
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="ml-auto">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex flex-1 flex-col gap-4 p-4 md:gap-6 md:p-6">{children}</main>
|
||||
</SidebarInset>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Moon, Sun } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
import { Button } from '@cfdm/ui/components/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@cfdm/ui/components/dropdown-menu'
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger render={<Button variant="ghost" size="icon" />}>
|
||||
<Sun className="size-5 scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
||||
<Moon className="absolute size-5 scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
||||
<span className="sr-only">Сменить тему</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme('light')}>Светлая</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('dark')}>Тёмная</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('system')}>Системная</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<NextThemesProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import '@cfdm/ui/globals.css'
|
||||
|
||||
import { queryClient } from '@/lib/queryClient'
|
||||
import { createRouter } from '@/lib/router'
|
||||
import { ThemeProvider } from '@/components/theme-provider'
|
||||
|
||||
const router = createRouter({ context: { queryClient } })
|
||||
|
||||
@@ -16,9 +17,11 @@ if (!rootEl) throw new Error('Root element #root not found')
|
||||
|
||||
createRoot(rootEl).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster richColors position="top-right" />
|
||||
</QueryClientProvider>
|
||||
<ThemeProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster richColors position="top-right" />
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user