CI / changes (push) Successful in 7s
CI / openapi (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m3s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m4s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go-prime (push) Has been skipped
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Has been skipped
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Has been skipped
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been skipped
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been skipped
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been skipped
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been skipped
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been skipped
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been skipped
Updated the AppShell component to enhance the sidebar's layout by making it sticky and ensuring it occupies the full height of the screen. Additionally, modified the main navigation to allow for vertical scrolling, improving usability and accessibility for users navigating through the application.
209 lines
7.1 KiB
Svelte
209 lines
7.1 KiB
Svelte
<script lang="ts">
|
|
import { resolve } from '$app/paths';
|
|
import { page } from '$app/state';
|
|
import { cn } from '$lib/utils.js';
|
|
import { Button } from '$lib/components/ui/button/index.js';
|
|
import { buttonVariants } from '$lib/components/ui/button/button.svelte';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuLabel,
|
|
DropdownMenuRadioGroup,
|
|
DropdownMenuRadioItem,
|
|
DropdownMenuTrigger
|
|
} from '$lib/components/ui/dropdown-menu/index.js';
|
|
import { Separator } from '$lib/components/ui/separator/index.js';
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '$lib/components/ui/tooltip/index.js';
|
|
import type { ThemePreference } from '$lib/theme.js';
|
|
|
|
import Activity from '@lucide/svelte/icons/activity';
|
|
import Boxes from '@lucide/svelte/icons/boxes';
|
|
import BookOpen from '@lucide/svelte/icons/book-open';
|
|
import LayoutDashboard from '@lucide/svelte/icons/layout-dashboard';
|
|
import Network from '@lucide/svelte/icons/network';
|
|
import Settings from '@lucide/svelte/icons/settings';
|
|
import Gauge from '@lucide/svelte/icons/gauge';
|
|
import Zap from '@lucide/svelte/icons/zap';
|
|
import CalendarClock from '@lucide/svelte/icons/calendar-clock';
|
|
import PanelLeftClose from '@lucide/svelte/icons/panel-left-close';
|
|
import PanelLeftOpen from '@lucide/svelte/icons/panel-left-open';
|
|
import Sun from '@lucide/svelte/icons/sun';
|
|
import Moon from '@lucide/svelte/icons/moon';
|
|
import Monitor from '@lucide/svelte/icons/monitor';
|
|
|
|
const nav = [
|
|
{ href: '/', label: 'Обзор', icon: LayoutDashboard },
|
|
{ href: '/modules', label: 'Модули', icon: Boxes },
|
|
{ href: '/directories', label: 'Справочники', icon: BookOpen },
|
|
{ href: '/network', label: 'Сеть', icon: Network },
|
|
{ href: '/revisions', label: 'Ревизии', icon: Activity },
|
|
{ href: '/operations', label: 'Операции', icon: Zap },
|
|
{ href: '/schedule', label: 'Расписание', icon: CalendarClock },
|
|
{ href: '/monitoring', label: 'Мониторинг', icon: Gauge },
|
|
] as const;
|
|
|
|
const navBottom = [
|
|
{ href: '/settings', label: 'Настройки', icon: Settings },
|
|
] as const;
|
|
|
|
let collapsed = $state(false);
|
|
let { children, theme = $bindable<ThemePreference>('system') } = $props();
|
|
|
|
function isActive(href: string) {
|
|
const pathname = page.url.pathname;
|
|
if (href === '/') return pathname === '/';
|
|
return pathname === href || pathname.startsWith(href + '/');
|
|
}
|
|
</script>
|
|
|
|
<TooltipProvider delayDuration={0}>
|
|
<div class="bg-background flex min-h-screen">
|
|
<aside
|
|
class={cn(
|
|
'border-border bg-sidebar sticky top-0 flex h-screen shrink-0 flex-col overflow-hidden border-r transition-all duration-200',
|
|
collapsed ? 'w-14' : 'w-56'
|
|
)}
|
|
aria-label="Основная навигация"
|
|
>
|
|
<!-- Header -->
|
|
<div class={cn('flex items-center gap-2 p-3', collapsed && 'justify-center')}>
|
|
{#if !collapsed}
|
|
<div class="min-w-0 flex-1">
|
|
<a href={resolve('/')} class="text-sidebar-foreground block truncate font-semibold tracking-tight">EvoBGP</a>
|
|
<p class="text-sidebar-foreground/50 truncate text-xs">Панель управления</p>
|
|
</div>
|
|
{/if}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
class={cn(
|
|
buttonVariants({ variant: 'ghost', size: 'icon-sm' }),
|
|
'text-sidebar-foreground/60 hover:text-sidebar-foreground shrink-0'
|
|
)}
|
|
aria-label="Тема оформления"
|
|
>
|
|
<span class="flex size-4 items-center justify-center">
|
|
<Sun class="size-4 dark:hidden" />
|
|
<Moon class="hidden size-4 dark:block" />
|
|
</span>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" class="w-44">
|
|
<DropdownMenuLabel>Тема</DropdownMenuLabel>
|
|
<DropdownMenuRadioGroup bind:value={theme}>
|
|
<DropdownMenuRadioItem value="light" class="gap-2">
|
|
<Sun class="size-4" />
|
|
Светлая
|
|
</DropdownMenuRadioItem>
|
|
<DropdownMenuRadioItem value="dark" class="gap-2">
|
|
<Moon class="size-4" />
|
|
Тёмная
|
|
</DropdownMenuRadioItem>
|
|
<DropdownMenuRadioItem value="system" class="gap-2">
|
|
<Monitor class="size-4" />
|
|
Как в системе
|
|
</DropdownMenuRadioItem>
|
|
</DropdownMenuRadioGroup>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
<button
|
|
onclick={() => (collapsed = !collapsed)}
|
|
class={cn(
|
|
buttonVariants({ variant: 'ghost', size: 'icon-sm' }),
|
|
'text-sidebar-foreground/60 hover:text-sidebar-foreground shrink-0'
|
|
)}
|
|
aria-label={collapsed ? 'Развернуть меню' : 'Свернуть меню'}
|
|
>
|
|
{#if collapsed}
|
|
<PanelLeftOpen class="size-4" />
|
|
{:else}
|
|
<PanelLeftClose class="size-4" />
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
<Separator />
|
|
|
|
<!-- Main nav -->
|
|
<nav class="flex flex-1 flex-col gap-0.5 overflow-y-auto p-2">
|
|
{#each nav as item (item.href)}
|
|
{@const Icon = item.icon}
|
|
{@const active = isActive(item.href)}
|
|
{#if collapsed}
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<a
|
|
href={resolve(item.href)}
|
|
class={cn(
|
|
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'icon-sm' }),
|
|
'w-full no-underline flex items-center justify-center',
|
|
active && 'ring-sidebar-primary/50 bg-sidebar-accent/90 text-sidebar-accent-foreground ring-1'
|
|
)}
|
|
aria-current={active ? 'page' : undefined}
|
|
>
|
|
<Icon class="size-4" />
|
|
</a>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">{item.label}</TooltipContent>
|
|
</Tooltip>
|
|
{:else}
|
|
<a
|
|
href={resolve(item.href)}
|
|
class={cn(
|
|
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'sm' }),
|
|
'w-full justify-start gap-2 no-underline',
|
|
active &&
|
|
'border-l-sidebar-primary bg-sidebar-accent/80 text-sidebar-accent-foreground border-l-2 shadow-sm'
|
|
)}
|
|
aria-current={active ? 'page' : undefined}
|
|
>
|
|
<Icon class="size-4" />
|
|
{item.label}
|
|
</a>
|
|
{/if}
|
|
{/each}
|
|
</nav>
|
|
|
|
<!-- Bottom nav -->
|
|
<div class="flex flex-col gap-0.5 p-2">
|
|
<Separator class="mb-2" />
|
|
{#each navBottom as item (item.href)}
|
|
{@const Icon = item.icon}
|
|
{@const active = isActive(item.href)}
|
|
{#if collapsed}
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<a
|
|
href={resolve(item.href)}
|
|
class={cn(
|
|
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'icon-sm' }),
|
|
'w-full no-underline flex items-center justify-center',
|
|
active && 'ring-sidebar-primary/50 bg-sidebar-accent/90 text-sidebar-accent-foreground ring-1'
|
|
)}
|
|
>
|
|
<Icon class="size-4" />
|
|
</a>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right">{item.label}</TooltipContent>
|
|
</Tooltip>
|
|
{:else}
|
|
<a
|
|
href={resolve(item.href)}
|
|
class={cn(
|
|
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'sm' }),
|
|
'w-full justify-start gap-2 no-underline',
|
|
active &&
|
|
'border-l-sidebar-primary bg-sidebar-accent/80 text-sidebar-accent-foreground border-l-2 shadow-sm'
|
|
)}
|
|
>
|
|
<Icon class="size-4" />
|
|
{item.label}
|
|
</a>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="min-w-0 flex-1 p-6">
|
|
{@render children?.()}
|
|
</main>
|
|
</div>
|
|
</TooltipProvider>
|