diff --git a/web/src/lib/AppShell.svelte b/web/src/lib/AppShell.svelte
deleted file mode 100644
index d49f066..0000000
--- a/web/src/lib/AppShell.svelte
+++ /dev/null
@@ -1,207 +0,0 @@
-
-
-
-
-
-
-
- {@render children?.()}
-
-
-
diff --git a/web/src/lib/ui/app/layout/app-layout.svelte b/web/src/lib/ui/app/layout/app-layout.svelte
new file mode 100644
index 0000000..3dfbe95
--- /dev/null
+++ b/web/src/lib/ui/app/layout/app-layout.svelte
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+ {@render children()}
+
+
+
+
diff --git a/web/src/lib/ui/app/layout/app-mobile-nav.svelte b/web/src/lib/ui/app/layout/app-mobile-nav.svelte
new file mode 100644
index 0000000..84b696d
--- /dev/null
+++ b/web/src/lib/ui/app/layout/app-mobile-nav.svelte
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
diff --git a/web/src/lib/ui/app/layout/app-nav-links.svelte b/web/src/lib/ui/app/layout/app-nav-links.svelte
new file mode 100644
index 0000000..712545f
--- /dev/null
+++ b/web/src/lib/ui/app/layout/app-nav-links.svelte
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+ {#each bottomNav as item (item.href)}
+ {@const Icon = item.icon}
+ {@const active = isActive(item.href)}
+ {#if collapsed}
+
+
+
+
+
+
+ {item.label}
+
+ {:else}
+
+
+ {item.label}
+
+ {/if}
+ {/each}
+
diff --git a/web/src/lib/ui/app/layout/app-sidebar.svelte b/web/src/lib/ui/app/layout/app-sidebar.svelte
new file mode 100644
index 0000000..fd588be
--- /dev/null
+++ b/web/src/lib/ui/app/layout/app-sidebar.svelte
@@ -0,0 +1,56 @@
+
+
+
diff --git a/web/src/lib/ui/app/layout/app-topbar.svelte b/web/src/lib/ui/app/layout/app-topbar.svelte
new file mode 100644
index 0000000..da8e5b2
--- /dev/null
+++ b/web/src/lib/ui/app/layout/app-topbar.svelte
@@ -0,0 +1,38 @@
+
+
+
+
+ {#if mobileNav}
+
{@render mobileNav()}
+ {/if}
+
+ {#if title}
+
{title}
+ {/if}
+ {#if description}
+
{description}
+ {/if}
+
+ {#if actions}
+
{@render actions()}
+ {/if}
+
+
diff --git a/web/src/lib/ui/app/layout/nav.ts b/web/src/lib/ui/app/layout/nav.ts
new file mode 100644
index 0000000..35e4d90
--- /dev/null
+++ b/web/src/lib/ui/app/layout/nav.ts
@@ -0,0 +1,29 @@
+import type { Component } from 'svelte';
+import Activity from '@lucide/svelte/icons/activity';
+import Boxes from '@lucide/svelte/icons/boxes';
+import BookOpen from '@lucide/svelte/icons/book-open';
+import CalendarClock from '@lucide/svelte/icons/calendar-clock';
+import Gauge from '@lucide/svelte/icons/gauge';
+import LayoutDashboard from '@lucide/svelte/icons/layout-dashboard';
+import Network from '@lucide/svelte/icons/network';
+import Settings from '@lucide/svelte/icons/settings';
+import Zap from '@lucide/svelte/icons/zap';
+
+export type NavItem = {
+ href: string;
+ label: string;
+ icon: Component;
+};
+
+export const mainNav: NavItem[] = [
+ { 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 }
+];
+
+export const bottomNav: NavItem[] = [{ href: '/settings', label: 'Настройки', icon: Settings }];
diff --git a/web/src/lib/ui/app/layout/theme-menu.svelte b/web/src/lib/ui/app/layout/theme-menu.svelte
new file mode 100644
index 0000000..3fcc0ed
--- /dev/null
+++ b/web/src/lib/ui/app/layout/theme-menu.svelte
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+ Тема
+
+
+
+ Светлая
+
+
+
+ Тёмная
+
+
+
+ Как в системе
+
+
+
+
diff --git a/web/src/lib/ui/app/page-header/page-header.svelte b/web/src/lib/ui/app/page-header/page-header.svelte
new file mode 100644
index 0000000..eaaeafb
--- /dev/null
+++ b/web/src/lib/ui/app/page-header/page-header.svelte
@@ -0,0 +1,40 @@
+
+
+
+
+ {#if Icon}
+
+
+
+ {/if}
+
+
{title}
+ {#if description}
+
{description}
+ {/if}
+
+
+ {#if actions}
+
{@render actions()}
+ {/if}
+
diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte
index 5362d4d..48f9660 100644
--- a/web/src/routes/+layout.svelte
+++ b/web/src/routes/+layout.svelte
@@ -3,7 +3,7 @@
import { onMount } from 'svelte';
import './layout.css';
import favicon from '$lib/assets/favicon.svg';
- import AppShell from '$lib/AppShell.svelte';
+ import AppLayout from '$lib/ui/app/layout/app-layout.svelte';
import { applyTheme, readTheme, THEME_STORAGE_KEY, type ThemePreference } from '$lib/theme.js';
import { Toaster } from 'svelte-sonner';
@@ -42,4 +42,4 @@
EvoBGP
-{@render children()}
+{@render children()}
diff --git a/web/src/routes/directories/+page.svelte b/web/src/routes/directories/+page.svelte
index 14febd1..fa568d1 100644
--- a/web/src/routes/directories/+page.svelte
+++ b/web/src/routes/directories/+page.svelte
@@ -46,6 +46,7 @@
import Trash2 from '@lucide/svelte/icons/trash-2';
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
import BookOpen from '@lucide/svelte/icons/book-open';
+ import PageHeader from '$lib/ui/app/page-header/page-header.svelte';
// --- Communities ---
let communities = $state([]);
@@ -183,19 +184,13 @@
}
-
-
-
-
-
-
-
Справочники
-
Сообщества BGP и DoH-профили для резолвинга доменов.
-
-
+