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 <[email protected]>
This commit is contained in:
Denozordec
2026-06-26 15:50:30 +07:00
co-authored by Cursor
parent 3026f1d1ce
commit 3e57728ddc
5 changed files with 73 additions and 14 deletions
+1 -1
View File
@@ -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" />
+21 -9
View File
@@ -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>
+29
View File
@@ -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>
)
}
+7 -4
View File
@@ -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>,
)