first commit
1
assets/ceph.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><circle cx="80" cy="80" r="40" fill="white" fill-opacity=".16"/><circle cx="80" cy="80" r="16" fill="white"/><circle cx="45" cy="65" r="15" fill="white"/><circle cx="115" cy="65" r="15" fill="white"/><circle cx="62" cy="112" r="15" fill="white"/><circle cx="98" cy="112" r="15" fill="white"/><path d="M57 71l16 7M103 71l-16 7M70 104l7-13M90 91l7 13" stroke="#BE123C" stroke-width="6" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#FB7185"/><stop offset="1" stop-color="#BE123C"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 723 B |
1
assets/config.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><circle cx="80" cy="80" r="34" fill="white"/><path d="M80 44v18M80 98v18M44 80h18M98 80h18M55 55l13 13M92 92l13 13M105 55L92 68M68 92l-13 13" stroke="#4338CA" stroke-width="8" stroke-linecap="round"/><circle cx="80" cy="80" r="12" fill="#4338CA"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#818CF8"/><stop offset="1" stop-color="#4338CA"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 562 B |
1
assets/database.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><ellipse cx="80" cy="50" rx="42" ry="18" fill="white"/><path d="M38 50v60c0 10 19 18 42 18s42-8 42-18V50" fill="white" fill-opacity=".86"/><path d="M38 78c0 10 19 18 42 18s42-8 42-18M38 104c0 10 19 18 42 18s42-8 42-18" stroke="#0F766E" stroke-width="7"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#2DD4BF"/><stop offset="1" stop-color="#0F766E"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 569 B |
1
assets/gitea.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M49 60h53c16 0 28 12 28 28s-12 28-28 28H73c-22 0-40-18-40-40 0-9 7-16 16-16z" fill="white"/><circle cx="67" cy="86" r="7" fill="#4D7C0F"/><circle cx="101" cy="86" r="7" fill="#4D7C0F"/><path d="M75 102c7 5 16 5 24 0" stroke="#4D7C0F" stroke-width="6" stroke-linecap="round"/><path d="M103 57l13-13M113 45h14v14" stroke="#FEF3C7" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#A3E635"/><stop offset="1" stop-color="#65A30D"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 718 B |
1
assets/grafana.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M82 124c-27 0-49-18-49-43 0-22 18-40 41-40 18 0 33 12 33 29 0 14-11 25-25 25-11 0-20-8-20-18 0-8 7-15 16-15 6 0 11 4 11 10" stroke="white" stroke-width="10" stroke-linecap="round"/><circle cx="116" cy="43" r="13" fill="#FDE68A"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#F97316"/><stop offset="1" stop-color="#7C2D12"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 553 B |
1
assets/kubernetes.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M80 30l45 26v52l-45 26-45-26V56l45-26z" fill="white" fill-opacity=".18" stroke="white" stroke-width="8" stroke-linejoin="round"/><circle cx="80" cy="82" r="22" fill="white"/><path d="M80 48v68M46 82h68M56 58l48 48M104 58l-48 48" stroke="#2563EB" stroke-width="7" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#60A5FA"/><stop offset="1" stop-color="#1D4ED8"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 611 B |
1
assets/ldap.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><circle cx="80" cy="56" r="22" fill="white"/><path d="M42 122c6-28 20-42 38-42s32 14 38 42" fill="white"/><circle cx="43" cy="80" r="13" fill="white" fill-opacity=".82"/><circle cx="117" cy="80" r="13" fill="white" fill-opacity=".82"/><path d="M56 86h48" stroke="#047857" stroke-width="8" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#6EE7B7"/><stop offset="1" stop-color="#047857"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 628 B |
1
assets/messages.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M36 48h88c8 0 14 6 14 14v38c0 8-6 14-14 14H78l-28 18v-18H36c-8 0-14-6-14-14V62c0-8 6-14 14-14z" fill="white"/><path d="M50 72h60M50 92h42" stroke="#0891B2" stroke-width="8" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#67E8F9"/><stop offset="1" stop-color="#0891B2"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 521 B |
1
assets/opensearch.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><circle cx="70" cy="70" r="30" stroke="white" stroke-width="12"/><path d="M93 93l27 27" stroke="white" stroke-width="13" stroke-linecap="round"/><path d="M49 69h42M58 53h24M58 85h24" stroke="#0369A1" stroke-width="7" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#38BDF8"/><stop offset="1" stop-color="#0369A1"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 556 B |
1
assets/prometheus.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M80 34c22 24 34 44 34 62 0 20-15 34-34 34s-34-14-34-34c0-18 12-38 34-62z" fill="white"/><path d="M66 82c12 8 16 8 28 0M65 103h30M80 54v32" stroke="#DC2626" stroke-width="8" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#FCA5A5"/><stop offset="1" stop-color="#DC2626"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 521 B |
1
assets/rabbitmq.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M42 112V48c0-8 6-14 14-14s14 6 14 14v28h20V48c0-8 6-14 14-14s14 6 14 14v64H42z" fill="white"/><rect x="64" y="88" width="18" height="18" rx="4" fill="#EA580C"/><rect x="94" y="88" width="18" height="18" rx="4" fill="#EA580C"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#FDBA74"/><stop offset="1" stop-color="#EA580C"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 550 B |
1
assets/redmine.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><rect x="38" y="38" width="84" height="84" rx="18" fill="white"/><path d="M58 66h44M58 86h30M58 106h44" stroke="#B91C1C" stroke-width="8" stroke-linecap="round"/><circle cx="104" cy="86" r="10" fill="#FCA5A5"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#F87171"/><stop offset="1" stop-color="#B91C1C"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 525 B |
1
assets/rules.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><rect x="40" y="34" width="80" height="92" rx="16" fill="white"/><path d="M60 62h42M60 82h42M60 102h24" stroke="#6D28D9" stroke-width="8" stroke-linecap="round"/><path d="M47 62l6 6 12-15M47 102l6 6 12-15" stroke="#22C55E" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#C4B5FD"/><stop offset="1" stop-color="#6D28D9"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 603 B |
1
assets/search.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><circle cx="70" cy="70" r="30" stroke="white" stroke-width="12"/><path d="M94 94l28 28" stroke="white" stroke-width="13" stroke-linecap="round"/><path d="M60 70h20M70 60v20" stroke="#6D28D9" stroke-width="7" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#C4B5FD"/><stop offset="1" stop-color="#6D28D9"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 547 B |
1
assets/server.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><rect x="42" y="32" width="76" height="96" rx="14" fill="white"/><path d="M56 55h48M56 80h48M56 105h48" stroke="#334155" stroke-width="8" stroke-linecap="round"/><circle cx="103" cy="55" r="5" fill="#22C55E"/><circle cx="103" cy="80" r="5" fill="#FACC15"/><circle cx="103" cy="105" r="5" fill="#EF4444"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#94A3B8"/><stop offset="1" stop-color="#334155"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 619 B |
1
assets/xmpp.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M46 44c10 38 21 61 34 82 13-21 24-44 34-82" stroke="white" stroke-width="12" stroke-linecap="round"/><path d="M46 44c24 29 44 45 68 60M114 44c-24 29-44 45-68 60" stroke="white" stroke-opacity=".62" stroke-width="8" stroke-linecap="round"/><circle cx="80" cy="84" r="10" fill="#15803D"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#86EFAC"/><stop offset="1" stop-color="#15803D"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 610 B |
1
assets/xpath.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M62 50L34 80l28 30M98 50l28 30-28 30" stroke="white" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/><path d="M87 42L69 118" stroke="#FDE68A" stroke-width="10" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#FBBF24"/><stop offset="1" stop-color="#B45309"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 526 B |
127
index.html
Normal file
@ -0,0 +1,127 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Навигационная панель экосистемы с разделами, подразделами, поиском и быстрыми переходами."
|
||||
/>
|
||||
<title>Панель экосистемы</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="ambient ambient-one"></div>
|
||||
<div class="ambient ambient-two"></div>
|
||||
|
||||
<main class="shell">
|
||||
<section class="hero" aria-labelledby="page-title">
|
||||
<div class="hero-content">
|
||||
<p class="eyebrow">Infrastructure map</p>
|
||||
<h1 id="page-title">Панель экосистемы</h1>
|
||||
<p class="lead">
|
||||
Единая точка входа в пользовательские, административные,
|
||||
мониторинговые и инфраструктурные интерфейсы.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="control-panel" aria-label="Настройки панели">
|
||||
<label class="search-field">
|
||||
<span>Поиск</span>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Grafana, S3, журнал, сервер..."
|
||||
autocomplete="off"
|
||||
data-search-input
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div class="control-group">
|
||||
<span>Тема</span>
|
||||
<button class="segmented is-active" type="button" data-theme="dark">
|
||||
Темная
|
||||
</button>
|
||||
<button class="segmented" type="button" data-theme="light">
|
||||
Светлая
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<span>Вид</span>
|
||||
<button class="segmented is-active" type="button" data-view="cards">
|
||||
Плитки
|
||||
</button>
|
||||
<button class="segmented" type="button" data-view="list">
|
||||
Список
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control-group accent-group">
|
||||
<span>Акцент</span>
|
||||
<button
|
||||
class="accent-swatch is-active"
|
||||
type="button"
|
||||
data-accent="violet"
|
||||
aria-label="Фиолетовый акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch"
|
||||
type="button"
|
||||
data-accent="cyan"
|
||||
aria-label="Голубой акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch"
|
||||
type="button"
|
||||
data-accent="lime"
|
||||
aria-label="Лаймовый акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch"
|
||||
type="button"
|
||||
data-accent="amber"
|
||||
aria-label="Янтарный акцент"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="overview" aria-label="Сводка">
|
||||
<div>
|
||||
<span data-total-sections>0</span>
|
||||
<p>раздела</p>
|
||||
</div>
|
||||
<div>
|
||||
<span data-total-groups>0</span>
|
||||
<p>подразделов</p>
|
||||
</div>
|
||||
<div>
|
||||
<span data-total-items>0</span>
|
||||
<p>элементов</p>
|
||||
</div>
|
||||
<div>
|
||||
<span data-visible-items>0</span>
|
||||
<p>найдено</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="catalog cards-view" aria-live="polite" data-catalog-root></section>
|
||||
|
||||
<template id="empty-template">
|
||||
<div class="empty-state">
|
||||
<img src="assets/search.svg" alt="" />
|
||||
<h2>Ничего не найдено</h2>
|
||||
<p>Попробуй изменить запрос или искать по названию системы, тегу или роли.</p>
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
405
script.js
Normal file
@ -0,0 +1,405 @@
|
||||
const STORAGE_KEY = "ecosystem-panel-preferences";
|
||||
|
||||
// Меняй навигацию здесь: разделы -> подразделы -> элементы.
|
||||
// url можно заменить на реальные адреса, tags помогают поиску.
|
||||
const ecosystemSections = [
|
||||
{
|
||||
id: "user",
|
||||
number: "1",
|
||||
title: "Интерфейс пользователя",
|
||||
description: "Рабочие инструменты пользователя для фильтрации и обработки сообщений.",
|
||||
groups: [
|
||||
{
|
||||
id: "message-filtering",
|
||||
number: "1.1",
|
||||
title: "Управление фильтрацией сообщений",
|
||||
description: "Правила, задания, события и статистика фильтрации.",
|
||||
items: [
|
||||
createItem("1.1.1", "Редактирование правил", "rules", ["правила", "фильтрация", "редактор"]),
|
||||
createItem("1.1.2", "Управление заданиями", "redmine", ["redmine", "задачи", "задания"]),
|
||||
createItem("1.1.3", "Применение правил", "rules", ["правила", "применение", "фильтрация"]),
|
||||
createItem("1.1.4", "Просмотр статистики", "grafana", ["grafana", "статистика", "дашборд"]),
|
||||
createItem("1.1.5", "Управление событиями фильтрации", "grafana", ["grafana", "события", "алерты"]),
|
||||
createItem("1.1.6", "Просмотр журнала событий фильтрации", "grafana", ["grafana", "журнал", "события"]),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "message-processing",
|
||||
number: "1.2",
|
||||
title: "Обработка сообщений",
|
||||
description: "Просмотр, оформление и доступ к хранилищам сообщений.",
|
||||
items: [
|
||||
createItem("1.2.1", "Просмотр и оформление", "messages", ["сообщения", "просмотр", "оформление"]),
|
||||
createItem("1.2.2", "Прямой доступ", "ceph", ["ceph", "доступ", "объекты"]),
|
||||
createItem("1.2.3", "S3 Хранилище", "ceph", ["ceph", "s3", "хранилище"]),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "admin",
|
||||
number: "2",
|
||||
title: "Интерфейс администратора",
|
||||
description: "Администрирование сервисов, кластеров, мониторинга и аппаратной части.",
|
||||
groups: [
|
||||
{
|
||||
id: "service-monitoring",
|
||||
number: "2.1",
|
||||
title: "Управление и мониторинг сервисов",
|
||||
description: "Кластер, модули, события мониторинга и журналы ошибок.",
|
||||
items: [
|
||||
createItem("2.1.1", "Управление кластером", "kubernetes", ["kubernetes", "кластер", "k8s"]),
|
||||
createItem("2.1.2", "Мониторинг состояния модулей", "grafana", ["grafana", "модули", "мониторинг"]),
|
||||
createItem("2.1.3", "Управление событиями мониторинга", "grafana", ["grafana", "события", "алерты"]),
|
||||
createItem("2.1.4", "Просмотр журнала событий мониторинга", "grafana", ["grafana", "журнал", "мониторинг"]),
|
||||
createItem("2.1.5", "Просмотр журналов ошибок", "opensearch", ["opensearch", "ошибки", "логи"]),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "service-configuration",
|
||||
number: "2.2",
|
||||
title: "Настройка сервисов",
|
||||
description: "Пользователи, очереди, xPath, модули и конфигурации.",
|
||||
items: [
|
||||
createItem("2.2.1", "Управление пользователями и очередями", "rabbitmq", ["пользователи", "очереди", "rabbitmq"]),
|
||||
createItem("2.2.2", "Отладка xPath-запросов", "xpath", ["xpath", "отладка", "запросы"]),
|
||||
createItem("2.2.3", "Конфигурирование модулей", "config", ["конфигурация", "модули", "настройки"]),
|
||||
createItem("2.2.4", "Просмотр журнала конфигурирования", "gitea", ["gitea", "журнал", "конфигурация"]),
|
||||
createItem("2.2.5", "Прямой доступ к конфигурации", "gitea", ["gitea", "конфигурация", "git"]),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "hardware",
|
||||
number: "2.3",
|
||||
title: "Управление и мониторинг аппаратной части",
|
||||
description: "Сенсоры, ресурсы и прямые панели серверов.",
|
||||
items: [
|
||||
createItem("2.3.1", "Мониторинг показаний сенсоров", "grafana", ["grafana", "сенсоры", "железо"]),
|
||||
createItem("2.3.2", "Мониторинг потребления ресурсов", "grafana", ["grafana", "ресурсы", "метрики"]),
|
||||
createItem("2.3.3", "СЕРВЕР01", "ipmi", ["ipmi", "сервер01", "железо"]),
|
||||
createItem("2.3.4", "СЕРВЕР02", "ipmi", ["ipmi", "сервер02", "железо"]),
|
||||
createItem("2.3.5", "СЕРВЕР03", "ipmi", ["ipmi", "сервер03", "железо"]),
|
||||
createItem("2.3.6", "СЕРВЕР04", "ipmi", ["ipmi", "сервер04", "железо"]),
|
||||
createItem("2.3.7", "СЕРВЕР05", "ipmi", ["ipmi", "сервер05", "железо"]),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "service-panels",
|
||||
number: "2.4",
|
||||
title: "Настройка отдельных сервисов",
|
||||
description: "Панели управления инфраструктурными сервисами.",
|
||||
items: [
|
||||
createItem("2.4.1", "Панель управления LDAP Account Manager", "ldap", ["ldap", "accounts", "пользователи"]),
|
||||
createItem("2.4.2", "Панель управления кластера RabbitMQ", "rabbitmq", ["rabbitmq", "кластер", "очереди"]),
|
||||
createItem("2.4.3", "Панель управления кластера Ceph", "ceph", ["ceph", "кластер", "storage"]),
|
||||
createItem("2.4.4", "Панель управления Prometheus", "prometheus", ["prometheus", "метрики", "мониторинг"]),
|
||||
createItem("2.4.5", "Панель управления XMPP-сервера", "xmpp", ["xmpp", "чат", "сервер"]),
|
||||
createItem("2.4.6", "Панель управления PostgreSQL", "postgresql", ["postgresql", "база", "db"]),
|
||||
createItem("2.4.7", "Подключение к БД", "cloudbeaver", ["cloudbeaver", "database", "sql"]),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "packages",
|
||||
number: "2.5",
|
||||
title: "Образы и пакеты",
|
||||
description: "Реестры образов и deb-пакетов.",
|
||||
items: [
|
||||
createItem("2.5.1", "Реестр docker-образов", "gitea", ["gitea", "docker", "registry"]),
|
||||
createItem("2.5.2", "Репозиторий deb-пакетов", "gitea", ["gitea", "deb", "packages"]),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const serviceMeta = {
|
||||
ceph: { label: "Ceph", icon: "assets/ceph.svg" },
|
||||
cloudbeaver: { label: "CloudBeaver", icon: "assets/database.svg" },
|
||||
config: { label: "Config", icon: "assets/config.svg" },
|
||||
gitea: { label: "Gitea", icon: "assets/gitea.svg" },
|
||||
grafana: { label: "Grafana", icon: "assets/grafana.svg" },
|
||||
ipmi: { label: "IPMI", icon: "assets/server.svg" },
|
||||
kubernetes: { label: "Kubernetes", icon: "assets/kubernetes.svg" },
|
||||
ldap: { label: "LDAP", icon: "assets/ldap.svg" },
|
||||
messages: { label: "Messages", icon: "assets/messages.svg" },
|
||||
opensearch: { label: "OpenSearch", icon: "assets/opensearch.svg" },
|
||||
postgresql: { label: "PostgreSQL", icon: "assets/database.svg" },
|
||||
prometheus: { label: "Prometheus", icon: "assets/prometheus.svg" },
|
||||
rabbitmq: { label: "RabbitMQ", icon: "assets/rabbitmq.svg" },
|
||||
redmine: { label: "Redmine", icon: "assets/redmine.svg" },
|
||||
rules: { label: "Rules", icon: "assets/rules.svg" },
|
||||
xpath: { label: "xPath", icon: "assets/xpath.svg" },
|
||||
xmpp: { label: "XMPP", icon: "assets/xmpp.svg" },
|
||||
};
|
||||
|
||||
const defaultPreferences = {
|
||||
theme: window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark",
|
||||
view: "cards",
|
||||
accent: "violet",
|
||||
};
|
||||
|
||||
const preferences = { ...defaultPreferences, ...readPreferences() };
|
||||
const catalog = document.querySelector("[data-catalog-root]");
|
||||
const searchInput = document.querySelector("[data-search-input]");
|
||||
const themeButtons = document.querySelectorAll("[data-theme]");
|
||||
const viewButtons = document.querySelectorAll("[data-view]");
|
||||
const accentButtons = document.querySelectorAll("[data-accent]");
|
||||
const emptyTemplate = document.querySelector("#empty-template");
|
||||
|
||||
const totals = calculateTotals(ecosystemSections);
|
||||
document.querySelector("[data-total-sections]").textContent = totals.sections;
|
||||
document.querySelector("[data-total-groups]").textContent = totals.groups;
|
||||
document.querySelector("[data-total-items]").textContent = totals.items;
|
||||
|
||||
applyPreferences(preferences);
|
||||
renderCatalog();
|
||||
|
||||
searchInput.addEventListener("input", renderCatalog);
|
||||
|
||||
themeButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
preferences.theme = button.dataset.theme;
|
||||
applyPreferences(preferences);
|
||||
savePreferences(preferences);
|
||||
});
|
||||
});
|
||||
|
||||
viewButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
preferences.view = button.dataset.view;
|
||||
applyPreferences(preferences);
|
||||
savePreferences(preferences);
|
||||
});
|
||||
});
|
||||
|
||||
accentButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
preferences.accent = button.dataset.accent;
|
||||
applyPreferences(preferences);
|
||||
savePreferences(preferences);
|
||||
});
|
||||
});
|
||||
|
||||
function createItem(number, title, type, tags = [], url = "#") {
|
||||
return { number, title, type, tags, url };
|
||||
}
|
||||
|
||||
function renderCatalog() {
|
||||
const query = normalize(searchInput.value);
|
||||
const fragment = document.createDocumentFragment();
|
||||
let visibleItems = 0;
|
||||
|
||||
ecosystemSections.forEach((section) => {
|
||||
const sectionElement = createSectionElement(section);
|
||||
let visibleGroups = 0;
|
||||
let sectionVisibleItems = 0;
|
||||
|
||||
section.groups.forEach((group) => {
|
||||
const filteredItems = group.items.filter((item) => matchesQuery(item, group, section, query));
|
||||
|
||||
if (!filteredItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
visibleGroups += 1;
|
||||
sectionVisibleItems += filteredItems.length;
|
||||
sectionElement.querySelector(".section-groups").append(createGroupElement(group, filteredItems));
|
||||
});
|
||||
|
||||
if (visibleGroups) {
|
||||
sectionElement.querySelector("[data-section-count]").textContent = sectionVisibleItems;
|
||||
fragment.append(sectionElement);
|
||||
visibleItems += sectionVisibleItems;
|
||||
}
|
||||
});
|
||||
|
||||
catalog.replaceChildren();
|
||||
|
||||
if (visibleItems) {
|
||||
catalog.append(fragment);
|
||||
attachCardInteractions();
|
||||
} else {
|
||||
catalog.append(emptyTemplate.content.cloneNode(true));
|
||||
}
|
||||
|
||||
document.querySelector("[data-visible-items]").textContent = visibleItems;
|
||||
}
|
||||
|
||||
function createSectionElement(section) {
|
||||
const sectionElement = document.createElement("article");
|
||||
sectionElement.className = "catalog-section";
|
||||
sectionElement.innerHTML = `
|
||||
<header class="section-head">
|
||||
<div>
|
||||
<p class="eyebrow">${section.number} раздел</p>
|
||||
<h2>${section.title}</h2>
|
||||
<p>${section.description}</p>
|
||||
</div>
|
||||
<span class="section-count"><span data-section-count>0</span> элементов</span>
|
||||
</header>
|
||||
<div class="section-groups"></div>
|
||||
`;
|
||||
return sectionElement;
|
||||
}
|
||||
|
||||
function createGroupElement(group, items) {
|
||||
const groupElement = document.createElement("section");
|
||||
groupElement.className = "catalog-group";
|
||||
|
||||
const cards = items.map(createCardElement);
|
||||
groupElement.innerHTML = `
|
||||
<header class="group-head">
|
||||
<div>
|
||||
<p class="group-number">${group.number}</p>
|
||||
<h3>${group.title}</h3>
|
||||
<p>${group.description}</p>
|
||||
</div>
|
||||
<span>${items.length}</span>
|
||||
</header>
|
||||
`;
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "items-grid";
|
||||
grid.append(...cards);
|
||||
groupElement.append(grid);
|
||||
|
||||
return groupElement;
|
||||
}
|
||||
|
||||
function createCardElement(item) {
|
||||
const meta = serviceMeta[item.type] || serviceMeta.config;
|
||||
const link = document.createElement("a");
|
||||
link.className = "nav-card";
|
||||
link.href = item.url;
|
||||
link.dataset.type = item.type;
|
||||
|
||||
if (item.url === "#") {
|
||||
link.setAttribute("aria-label", `${item.title}: URL еще не настроен`);
|
||||
link.addEventListener("click", (event) => event.preventDefault());
|
||||
} else {
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener noreferrer";
|
||||
}
|
||||
|
||||
link.innerHTML = `
|
||||
<span class="shine"></span>
|
||||
<img src="${meta.icon}" alt="" loading="lazy" />
|
||||
<div class="card-content">
|
||||
<p class="item-number">${item.number}</p>
|
||||
<h4>${item.title}</h4>
|
||||
<p class="item-type">${meta.label}</p>
|
||||
<div class="tags">${item.tags.map((tag) => `<span>${tag}</span>`).join("")}</div>
|
||||
</div>
|
||||
<span class="arrow" aria-hidden="true">→</span>
|
||||
`;
|
||||
|
||||
return link;
|
||||
}
|
||||
|
||||
function matchesQuery(item, group, section, query) {
|
||||
if (!query) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const meta = serviceMeta[item.type] || serviceMeta.config;
|
||||
const haystack = normalize(
|
||||
[
|
||||
item.number,
|
||||
item.title,
|
||||
item.type,
|
||||
meta.label,
|
||||
item.tags.join(" "),
|
||||
group.title,
|
||||
section.title,
|
||||
].join(" "),
|
||||
);
|
||||
|
||||
return haystack.includes(query);
|
||||
}
|
||||
|
||||
function attachCardInteractions() {
|
||||
document.querySelectorAll(".nav-card").forEach((card) => {
|
||||
let leaveTimer;
|
||||
|
||||
card.addEventListener("pointerenter", () => {
|
||||
window.clearTimeout(leaveTimer);
|
||||
card.classList.add("is-hovered");
|
||||
});
|
||||
|
||||
card.addEventListener("pointermove", (event) => {
|
||||
window.clearTimeout(leaveTimer);
|
||||
card.classList.add("is-hovered");
|
||||
|
||||
const rect = card.getBoundingClientRect();
|
||||
const xPercent = clamp(((event.clientX - rect.left) / rect.width) * 100, 8, 92);
|
||||
const yPercent = clamp(((event.clientY - rect.top) / rect.height) * 100, 8, 92);
|
||||
const rotateY = ((xPercent - 50) / 50) * 2.4;
|
||||
const rotateX = ((50 - yPercent) / 50) * 2.4;
|
||||
|
||||
card.style.setProperty("--card-x", `${xPercent}%`);
|
||||
card.style.setProperty("--card-y", `${yPercent}%`);
|
||||
card.style.setProperty("--tilt-x", `${rotateX}deg`);
|
||||
card.style.setProperty("--tilt-y", `${rotateY}deg`);
|
||||
});
|
||||
|
||||
card.addEventListener("pointerleave", () => {
|
||||
leaveTimer = window.setTimeout(() => {
|
||||
card.classList.remove("is-hovered");
|
||||
card.style.setProperty("--card-x", "50%");
|
||||
card.style.setProperty("--card-y", "50%");
|
||||
card.style.setProperty("--tilt-x", "0deg");
|
||||
card.style.setProperty("--tilt-y", "0deg");
|
||||
}, 80);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function applyPreferences(nextPreferences) {
|
||||
document.documentElement.dataset.theme = nextPreferences.theme;
|
||||
document.documentElement.dataset.accent = nextPreferences.accent;
|
||||
catalog.classList.toggle("cards-view", nextPreferences.view === "cards");
|
||||
catalog.classList.toggle("list-view", nextPreferences.view === "list");
|
||||
setActiveButton(themeButtons, "theme", nextPreferences.theme);
|
||||
setActiveButton(viewButtons, "view", nextPreferences.view);
|
||||
setActiveButton(accentButtons, "accent", nextPreferences.accent);
|
||||
}
|
||||
|
||||
function setActiveButton(buttons, key, value) {
|
||||
buttons.forEach((button) => {
|
||||
const isActive = button.dataset[key] === value;
|
||||
button.classList.toggle("is-active", isActive);
|
||||
button.setAttribute("aria-pressed", String(isActive));
|
||||
});
|
||||
}
|
||||
|
||||
function calculateTotals(sections) {
|
||||
return sections.reduce(
|
||||
(total, section) => {
|
||||
total.sections += 1;
|
||||
total.groups += section.groups.length;
|
||||
total.items += section.groups.reduce((sum, group) => sum + group.items.length, 0);
|
||||
return total;
|
||||
},
|
||||
{ sections: 0, groups: 0, items: 0 },
|
||||
);
|
||||
}
|
||||
|
||||
function normalize(value) {
|
||||
return value.toLowerCase().replaceAll("ё", "е").trim();
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
function readPreferences() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function savePreferences(nextPreferences) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(nextPreferences));
|
||||
}
|
||||
687
styles.css
Normal file
@ -0,0 +1,687 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #080a12;
|
||||
--bg-soft: #111827;
|
||||
--panel: rgba(15, 23, 42, 0.74);
|
||||
--panel-strong: rgba(15, 23, 42, 0.92);
|
||||
--surface: rgba(255, 255, 255, 0.055);
|
||||
--text: #f8fafc;
|
||||
--muted: #aeb8c8;
|
||||
--border: rgba(255, 255, 255, 0.12);
|
||||
--shadow: 0 24px 80px rgba(0, 0, 0, 0.34);
|
||||
--accent: #8b5cf6;
|
||||
--accent-2: #22d3ee;
|
||||
--accent-rgb: 139, 92, 246;
|
||||
--card-x: 50%;
|
||||
--card-y: 50%;
|
||||
}
|
||||
|
||||
html[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
--bg: #f5f7fb;
|
||||
--bg-soft: #ffffff;
|
||||
--panel: rgba(255, 255, 255, 0.78);
|
||||
--panel-strong: rgba(255, 255, 255, 0.96);
|
||||
--surface: rgba(15, 23, 42, 0.045);
|
||||
--text: #101827;
|
||||
--muted: #5f6b7a;
|
||||
--border: rgba(15, 23, 42, 0.12);
|
||||
--shadow: 0 24px 70px rgba(56, 65, 91, 0.18);
|
||||
}
|
||||
|
||||
html[data-accent="cyan"] {
|
||||
--accent: #06b6d4;
|
||||
--accent-2: #3b82f6;
|
||||
--accent-rgb: 6, 182, 212;
|
||||
}
|
||||
|
||||
html[data-accent="lime"] {
|
||||
--accent: #84cc16;
|
||||
--accent-2: #14b8a6;
|
||||
--accent-rgb: 132, 204, 22;
|
||||
}
|
||||
|
||||
html[data-accent="amber"] {
|
||||
--accent: #f59e0b;
|
||||
--accent-2: #f43f5e;
|
||||
--accent-rgb: 245, 158, 11;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(var(--accent-rgb), 0.24), transparent 36rem),
|
||||
radial-gradient(circle at bottom right, rgba(34, 211, 238, 0.16), transparent 34rem),
|
||||
linear-gradient(135deg, var(--bg), var(--bg-soft));
|
||||
color: var(--text);
|
||||
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
transition: background 220ms ease, color 220ms ease;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
a {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.ambient {
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
width: 30rem;
|
||||
height: 30rem;
|
||||
border-radius: 999px;
|
||||
filter: blur(18px);
|
||||
opacity: 0.42;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ambient-one {
|
||||
top: -10rem;
|
||||
right: -8rem;
|
||||
background: rgba(var(--accent-rgb), 0.46);
|
||||
}
|
||||
|
||||
.ambient-two {
|
||||
bottom: -13rem;
|
||||
left: -10rem;
|
||||
background: rgba(34, 211, 238, 0.3);
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: min(1320px, calc(100% - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 42px 0;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(330px, 460px);
|
||||
gap: 24px;
|
||||
align-items: stretch;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.hero-content,
|
||||
.control-panel,
|
||||
.overview,
|
||||
.catalog-section,
|
||||
.empty-state {
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(22px);
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
min-height: 330px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
padding: clamp(28px, 5vw, 56px);
|
||||
border-radius: 38px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-content::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(120deg, rgba(var(--accent-rgb), 0.28), transparent 34%),
|
||||
url("data:image/svg+xml,%3Csvg width='140' height='140' viewBox='0 0 140 140' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.09'%3E%3Cpath d='M0 70h140M70 0v140'/%3E%3Ccircle cx='70' cy='70' r='38'/%3E%3C/g%3E%3C/svg%3E");
|
||||
mask-image: linear-gradient(to bottom, black, transparent 88%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-content > * {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.eyebrow,
|
||||
.group-number,
|
||||
.item-number {
|
||||
margin: 0;
|
||||
color: var(--accent);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 840px;
|
||||
margin: 14px 0 18px;
|
||||
font-size: clamp(2.6rem, 8vw, 6.4rem);
|
||||
line-height: 0.9;
|
||||
letter-spacing: -0.08em;
|
||||
}
|
||||
|
||||
.lead {
|
||||
max-width: 740px;
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: clamp(1rem, 2vw, 1.2rem);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 18px;
|
||||
border-radius: 28px;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-field span,
|
||||
.control-group > span {
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.search-field input {
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
padding: 0 16px;
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease, background 160ms ease;
|
||||
}
|
||||
|
||||
.search-field input:focus {
|
||||
border-color: rgba(var(--accent-rgb), 0.62);
|
||||
box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.16);
|
||||
background: var(--panel-strong);
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 48px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.control-group > span {
|
||||
min-width: 68px;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.segmented,
|
||||
.accent-swatch {
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
transition: transform 160ms ease, background 160ms ease, color 160ms ease;
|
||||
}
|
||||
|
||||
.segmented {
|
||||
flex: 1;
|
||||
min-height: 34px;
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font-size: 0.88rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.segmented:hover,
|
||||
.accent-swatch:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.segmented.is-active {
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.accent-group {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.accent-swatch {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 999px;
|
||||
box-shadow: inset 0 0 0 4px rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.accent-swatch[data-accent="violet"] {
|
||||
background: linear-gradient(135deg, #8b5cf6, #22d3ee);
|
||||
}
|
||||
|
||||
.accent-swatch[data-accent="cyan"] {
|
||||
background: linear-gradient(135deg, #06b6d4, #3b82f6);
|
||||
}
|
||||
|
||||
.accent-swatch[data-accent="lime"] {
|
||||
background: linear-gradient(135deg, #84cc16, #14b8a6);
|
||||
}
|
||||
|
||||
.accent-swatch[data-accent="amber"] {
|
||||
background: linear-gradient(135deg, #f59e0b, #f43f5e);
|
||||
}
|
||||
|
||||
.accent-swatch.is-active {
|
||||
border-color: var(--text);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
margin-bottom: 18px;
|
||||
border-radius: 26px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.overview div {
|
||||
padding: 18px 20px;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
}
|
||||
|
||||
.overview span {
|
||||
display: block;
|
||||
font-size: clamp(1.8rem, 4vw, 2.8rem);
|
||||
font-weight: 850;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.06em;
|
||||
}
|
||||
|
||||
.overview p {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.catalog {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.catalog-section {
|
||||
border-radius: 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
align-items: flex-start;
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(var(--accent-rgb), 0.14), transparent 22rem),
|
||||
rgba(255, 255, 255, 0.035);
|
||||
}
|
||||
|
||||
.section-head h2,
|
||||
.group-head h3,
|
||||
.nav-card h4,
|
||||
.empty-state h2 {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.section-head h2 {
|
||||
margin-top: 8px;
|
||||
font-size: clamp(1.6rem, 4vw, 2.6rem);
|
||||
}
|
||||
|
||||
.section-head p:not(.eyebrow),
|
||||
.group-head p:not(.group-number) {
|
||||
max-width: 760px;
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.section-count {
|
||||
white-space: nowrap;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
padding: 8px 12px;
|
||||
color: var(--muted);
|
||||
background: var(--surface);
|
||||
font-size: 0.86rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.section-groups {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.catalog-group {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 24px;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.group-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.group-head h3 {
|
||||
margin-top: 6px;
|
||||
font-size: 1.22rem;
|
||||
}
|
||||
|
||||
.group-head > span {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 999px;
|
||||
background: rgba(var(--accent-rgb), 0.14);
|
||||
color: var(--accent);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.items-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.nav-card {
|
||||
--tilt-x: 0deg;
|
||||
--tilt-y: 0deg;
|
||||
position: relative;
|
||||
min-height: 224px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 22px;
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
transform-origin: center;
|
||||
transform: perspective(900px) rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
|
||||
will-change: transform;
|
||||
transition:
|
||||
transform 260ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||
border-color 220ms ease,
|
||||
background 220ms ease,
|
||||
box-shadow 220ms ease;
|
||||
}
|
||||
|
||||
.nav-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background: radial-gradient(
|
||||
circle at var(--card-x) var(--card-y),
|
||||
rgba(var(--accent-rgb), 0.24),
|
||||
transparent 34%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 180ms ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-card.is-hovered,
|
||||
.nav-card:focus-visible {
|
||||
border-color: rgba(var(--accent-rgb), 0.52);
|
||||
background: var(--panel-strong);
|
||||
box-shadow: 0 22px 70px rgba(var(--accent-rgb), 0.16);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.nav-card.is-hovered::before,
|
||||
.nav-card:focus-visible::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nav-card img {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 14px 24px rgba(0, 0, 0, 0.22));
|
||||
}
|
||||
|
||||
.card-content {
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.nav-card h4 {
|
||||
font-size: 1.08rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.item-type {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tags span {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
padding: 4px 8px;
|
||||
background: rgba(var(--accent-rgb), 0.1);
|
||||
color: var(--muted);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
bottom: 14px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 999px;
|
||||
background: rgba(var(--accent-rgb), 0.14);
|
||||
color: var(--accent);
|
||||
font-size: 1.15rem;
|
||||
transition: transform 180ms ease, background 180ms ease, color 180ms ease;
|
||||
}
|
||||
|
||||
.nav-card.is-hovered .arrow,
|
||||
.nav-card:focus-visible .arrow {
|
||||
transform: translateX(4px);
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.shine {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
110deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.16) 42%,
|
||||
transparent 64%
|
||||
);
|
||||
transform: translateX(-130%);
|
||||
transition: transform 540ms ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-card.is-hovered .shine,
|
||||
.nav-card:focus-visible .shine {
|
||||
transform: translateX(130%);
|
||||
}
|
||||
|
||||
.catalog.list-view .items-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.catalog.list-view .nav-card {
|
||||
min-height: 112px;
|
||||
display: grid;
|
||||
grid-template-columns: 58px minmax(0, 1fr) 42px;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.catalog.list-view .arrow {
|
||||
position: static;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
min-height: 280px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
gap: 12px;
|
||||
padding: 36px;
|
||||
border-radius: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state img {
|
||||
width: 74px;
|
||||
height: 74px;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
max-width: 480px;
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.hero {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.items-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.shell {
|
||||
width: min(100% - 22px, 1320px);
|
||||
padding: 22px 0;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
min-height: 300px;
|
||||
border-radius: 28px;
|
||||
}
|
||||
|
||||
.control-panel,
|
||||
.catalog-section,
|
||||
.empty-state {
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.control-group > span {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.accent-group > span {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.overview {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.section-head,
|
||||
.group-head {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.section-count {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.section-groups {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.items-grid {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.nav-card {
|
||||
min-height: 210px;
|
||||
}
|
||||
|
||||
.catalog.list-view .nav-card {
|
||||
grid-template-columns: 48px minmax(0, 1fr);
|
||||
min-height: 136px;
|
||||
}
|
||||
|
||||
.catalog.list-view .arrow {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.nav-card img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||