522 lines
27 KiB
HTML
522 lines
27 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>GUI Library Documentation</title>
|
||
<!-- Подключаем шрифты и иконки -->
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
|
||
<style>
|
||
:root {
|
||
--bg-primary: #0f172a;
|
||
--bg-secondary: #1e293b;
|
||
--bg-tertiary: #334155;
|
||
--text-primary: #f1f5f9;
|
||
--text-secondary: #94a3b8;
|
||
--accent: #38bdf8;
|
||
--accent-hover: #0ea5e9;
|
||
--success: #22c55e;
|
||
--border: #334155;
|
||
--code-bg: #0d1117;
|
||
}
|
||
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
body {
|
||
font-family: 'Inter', sans-serif;
|
||
background-color: var(--bg-primary);
|
||
color: var(--text-primary);
|
||
line-height: 1.6;
|
||
display: flex;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* Скроллбар */
|
||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||
::-webkit-scrollbar-track { background: var(--bg-primary); }
|
||
::-webkit-scrollbar-thumb { background: var(--bg-tertiary); border-radius: 4px; }
|
||
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
|
||
|
||
/* Боковая панель */
|
||
.sidebar {
|
||
width: 280px;
|
||
background-color: var(--bg-secondary);
|
||
border-right: 1px solid var(--border);
|
||
position: fixed;
|
||
height: 100vh;
|
||
overflow-y: auto;
|
||
padding: 2rem 1.5rem;
|
||
z-index: 100;
|
||
}
|
||
|
||
.logo {
|
||
font-size: 1.5rem;
|
||
font-weight: 700;
|
||
color: var(--accent);
|
||
margin-bottom: 2rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.search-box {
|
||
width: 100%;
|
||
padding: 0.75rem;
|
||
background: var(--bg-primary);
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
color: var(--text-primary);
|
||
font-family: 'Inter', sans-serif;
|
||
margin-bottom: 1.5rem;
|
||
outline: none;
|
||
transition: border-color 0.2s;
|
||
}
|
||
.search-box:focus { border-color: var(--accent); }
|
||
|
||
.nav-group { margin-bottom: 1.5rem; }
|
||
.nav-title {
|
||
font-size: 0.75rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 0.5rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.nav-link {
|
||
display: block;
|
||
padding: 0.5rem 0.75rem;
|
||
color: var(--text-secondary);
|
||
text-decoration: none;
|
||
border-radius: 6px;
|
||
font-size: 0.9rem;
|
||
transition: all 0.2s;
|
||
}
|
||
.nav-link:hover, .nav-link.active {
|
||
background-color: var(--bg-tertiary);
|
||
color: var(--accent);
|
||
}
|
||
|
||
/* Основной контент */
|
||
.main-content {
|
||
margin-left: 280px;
|
||
flex: 1;
|
||
padding: 3rem 4rem;
|
||
max-width: 1200px;
|
||
}
|
||
|
||
h1 { font-size: 2.5rem; margin-bottom: 1rem; color: var(--text-primary); }
|
||
h2 { font-size: 1.8rem; margin-top: 3rem; margin-bottom: 1rem; color: var(--accent); border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
|
||
h3 { font-size: 1.3rem; margin-top: 2rem; margin-bottom: 0.75rem; color: var(--text-primary); }
|
||
p { margin-bottom: 1rem; color: var(--text-secondary); }
|
||
|
||
.badge {
|
||
display: inline-block;
|
||
padding: 0.25rem 0.5rem;
|
||
background: var(--bg-tertiary);
|
||
color: var(--accent);
|
||
border-radius: 4px;
|
||
font-size: 0.75rem;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
margin-right: 0.5rem;
|
||
}
|
||
|
||
/* Блоки кода */
|
||
.code-wrapper {
|
||
position: relative;
|
||
margin: 1.5rem 0;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
border: 1px solid var(--border);
|
||
}
|
||
.code-header {
|
||
background: var(--bg-secondary);
|
||
padding: 0.5rem 1rem;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-bottom: 1px solid var(--border);
|
||
font-size: 0.85rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
.copy-btn {
|
||
background: transparent;
|
||
border: 1px solid var(--border);
|
||
color: var(--text-secondary);
|
||
padding: 0.25rem 0.75rem;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 0.8rem;
|
||
transition: all 0.2s;
|
||
}
|
||
.copy-btn:hover { background: var(--bg-tertiary); color: var(--text-primary); }
|
||
.copy-btn.copied { border-color: var(--success); color: var(--success); }
|
||
|
||
pre { margin: 0 !important; border-radius: 0 !important; background: var(--code-bg) !important; }
|
||
code { font-family: 'JetBrains Mono', monospace !important; font-size: 0.9rem !important; }
|
||
|
||
/* Таблицы */
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 1.5rem 0;
|
||
background: var(--bg-secondary);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
th, td {
|
||
padding: 1rem;
|
||
text-align: left;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
th { background: var(--bg-tertiary); color: var(--text-primary); font-weight: 600; }
|
||
td { color: var(--text-secondary); }
|
||
td code { background: var(--bg-primary); padding: 0.2rem 0.4rem; border-radius: 4px; color: var(--accent); }
|
||
|
||
/* Аккордеон для API */
|
||
details {
|
||
background: var(--bg-secondary);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
margin-bottom: 1rem;
|
||
overflow: hidden;
|
||
}
|
||
summary {
|
||
padding: 1rem;
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
list-style: none;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
summary::-webkit-details-marker { display: none; }
|
||
summary::after { content: '+'; font-size: 1.5rem; color: var(--accent); }
|
||
details[open] summary::after { content: '−'; }
|
||
details[open] summary { border-bottom: 1px solid var(--border); }
|
||
.details-content { padding: 1.5rem; }
|
||
|
||
.highlight { color: var(--accent); font-weight: 600; }
|
||
|
||
@media (max-width: 768px) {
|
||
.sidebar { transform: translateX(-100%); transition: transform 0.3s; }
|
||
.sidebar.open { transform: translateX(0); }
|
||
.main-content { margin-left: 0; padding: 2rem; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- Боковая навигация -->
|
||
<nav class="sidebar">
|
||
<div class="logo">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
|
||
X11 GUI Lib
|
||
</div>
|
||
<input type="text" class="search-box" placeholder="Поиск по документации..." id="searchInput">
|
||
|
||
<div class="nav-group">
|
||
<div class="nav-title">Начало работы</div>
|
||
<a href="#intro" class="nav-link">Введение</a>
|
||
<a href="#quickstart" class="nav-link">Быстрый старт</a>
|
||
<a href="#architecture" class="nav-link">Архитектура</a>
|
||
</div>
|
||
<div class="nav-group">
|
||
<div class="nav-title">Компоненты</div>
|
||
<a href="#eventbus" class="nav-link">EventBus</a>
|
||
<a href="#rawpicture" class="nav-link">RawPicture</a>
|
||
<a href="#widgets" class="nav-link">Виджеты</a>
|
||
</div>
|
||
<div class="nav-group">
|
||
<div class="nav-title">API Справочник</div>
|
||
<a href="#api-window" class="nav-link">gui::window</a>
|
||
<a href="#api-page" class="nav-link">gui::page</a>
|
||
<a href="#api-combat" class="nav-link">gui::combat</a>
|
||
<a href="#api-widget" class="nav-link">gui::Widget</a>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- Основной контент -->
|
||
<main class="main-content">
|
||
<section id="intro">
|
||
<h1>Документация GUI Библиотеки</h1>
|
||
<p>Легковесная, кроссплатформенная (X11) библиотека для создания графических интерфейсов на C++. Поддерживает программный рендеринг, анимации, иерархию виджетов и событийно-ориентированную архитектуру.</p>
|
||
</section>
|
||
|
||
<section id="quickstart">
|
||
<h2>🚀 Быстрый старт</h2>
|
||
<p>Минимальный пример создания окна с кнопкой и обработкой события.</p>
|
||
<div class="code-wrapper">
|
||
<div class="code-header">
|
||
<span>main.cpp</span>
|
||
<button class="copy-btn" onclick="copyCode(this)">Копировать</button>
|
||
</div>
|
||
<pre><code class="language-cpp">#include "gui.h"
|
||
|
||
int main() {
|
||
// 1. Создаем окно
|
||
gui::window win("Мое Приложение");
|
||
|
||
// 2. Добавляем страницу
|
||
win.addPAGE("main");
|
||
|
||
// 3. Настраиваем страницу виджетами
|
||
win.configur_page("main", [](gui::page& p) {
|
||
// Создаем кнопку
|
||
gui::WidgetData btnData;
|
||
btnData.id = "my_button";
|
||
btnData.text = "Нажми меня!";
|
||
btnData.x = 50; btnData.y = 50;
|
||
btnData.width = 150; btnData.height = 40;
|
||
btnData.bg_color = 0x2563EB; // Синий
|
||
btnData.fg_color = 0xFFFFFF; // Белый текст
|
||
|
||
p.Button(btnData);
|
||
}, true); // true = сделать активной сразу
|
||
|
||
// 4. Подписываемся на событие
|
||
gui::EventBus::get().on("button.click", [](const gui::EventData& e) {
|
||
if (e.source_id == "my_button") {
|
||
std::cout << "Кнопка нажата!" << std::endl;
|
||
}
|
||
});
|
||
|
||
// 5. Главный цикл
|
||
while (true) {
|
||
win.update(); // Обработка событий X11 и отрисовка
|
||
// Добавьте std::this_thread::sleep_for для экономии CPU
|
||
}
|
||
|
||
return 0;
|
||
}</code></pre>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="architecture">
|
||
<h2>🏛 Архитектура</h2>
|
||
<p>Библиотека построена на строгой иерархии:</p>
|
||
<table>
|
||
<thead>
|
||
<tr><th>Уровень</th><th>Класс</th><th>Описание</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr><td><span class="badge">1</span></td><td><code>gui::window</code></td><td>Управляет соединением с X11, главным циклом (`update`) и вкладками.</td></tr>
|
||
<tr><td><span class="badge">2</span></td><td><code>gui::page</code></td><td>Представляет собой вкладку или экран. Наследуется от <code>combat</code>.</td></tr>
|
||
<tr><td><span class="badge">3</span></td><td><code>gui::combat</code></td><td>Базовый контейнер. Хранит коллекцию виджетов и предоставляет фабричные методы для их создания.</td></tr>
|
||
<tr><td><span class="badge">4</span></td><td><code>gui::Widget</code></td><td>Конкретный элемент интерфейса (кнопка, ввод, слайдер). Также наследуется от <code>combat</code>, позволяя создавать вложенные структуры (например, деревья).</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<section id="eventbus">
|
||
<h2>📡 EventBus</h2>
|
||
<p>Глобальная шина событий для слабой связи между компонентами. Реализована как Singleton.</p>
|
||
<div class="code-wrapper">
|
||
<div class="code-header"><span>Использование EventBus</span><button class="copy-btn" onclick="copyCode(this)">Копировать</button></div>
|
||
<pre><code class="language-cpp">// Подписка на событие
|
||
int token = gui::EventBus::get().on("slider.change", [](const gui::EventData& e) {
|
||
std::cout << "Widget: " << e.source_id << ", Value: " << e.value << std::endl;
|
||
});
|
||
|
||
// Генерация события (обычно делается внутри библиотеки, но можно и вручную)
|
||
gui::EventBus::get().emit("custom.event", {"widget_id", "some text", 0.5f, 0, true});
|
||
|
||
// Отписка
|
||
gui::EventBus::get().off(token);</code></pre>
|
||
</div>
|
||
<p>Структура <code>EventData</code> содержит: <code>source_id</code> (ID виджета), <code>text</code>, <code>value</code> (для слайдеров/прогресса), <code>index</code> (для списков), <code>flag</code> (для чекбоксов).</p>
|
||
</section>
|
||
|
||
<section id="rawpicture">
|
||
<h2>🎨 RawPicture</h2>
|
||
<p>Класс для программного рендеринга в буфер. Поддерживает 8, 16, 24 и 32-битные форматы.</p>
|
||
<ul>
|
||
<li><strong>Примитивы:</strong> <code>drawLine</code>, <code>fillRect</code>, <code>drawCircle</code>, <code>fillPolygon</code>, <code>drawText</code>.</li>
|
||
<li><strong>Загрузка:</strong> <code>load("image.bmp")</code> или <code>load("image.ppm")</code>.</li>
|
||
<li><strong>Отображение:</strong> <code>present(display, window, gc, x, y)</code> для вывода на экран X11.</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<section id="widgets">
|
||
<h2>🧩 Доступные виджеты</h2>
|
||
<p>Все виджеты создаются через методы класса <code>combat</code> (или <code>page</code>) путем передачи структуры <code>WidgetData</code>.</p>
|
||
<table>
|
||
<thead><tr><th>Метод</th><th>Тип</th><th>Описание</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>Button()</code></td><td><code>BUTTON</code></td><td>Стандартная кнопка. Поддерживает <code>on_page_click</code> и <code>on_window_click</code>.</td></tr>
|
||
<tr><td><code>Label()</code></td><td><code>LABEL</code></td><td>Текстовая метка.</td></tr>
|
||
<tr><td><code>Input()</code></td><td><code>INPUT</code></td><td>Поле ввода текста. Перехватывает фокус клавиатуры.</td></tr>
|
||
<tr><td><code>Checkbox()</code></td><td><code>CHECKBOX</code></td><td>Флажок. Состояние в <code>data.checked</code>.</td></tr>
|
||
<tr><td><code>Radiobutton()</code></td><td><code>RADIO</code></td><td>Радиокнопка. Автоматически снимает выделение с других радио-кнопок с тем же <code>id</code> на странице.</td></tr>
|
||
<tr><td><code>Slider()</code></td><td><code>SLIDER</code></td><td>Ползунок. Значение 0.0 - 1.0 в <code>data.value</code>. Поддерживает перетаскивание.</td></tr>
|
||
<tr><td><code>Progressbar()</code></td><td><code>PROGRESS</code></td><td>Индикатор прогресса. Поддерживает анимацию <code>indeterminate</code>.</td></tr>
|
||
<tr><td><code>Listbox()</code></td><td><code>LISTBOX</code></td><td>Список элементов. Элементы задаются через <code>data.items</code>.</td></tr>
|
||
<tr><td><code>Dropdown()</code></td><td><code>DROPDOWN</code></td><td>Выпадающий список с анимацией открытия.</td></tr>
|
||
<tr><td><code>TreeView()</code></td><td><code>TREE_VIEW</code></td><td>Контейнер для древовидной структуры. Использует <code>TreeNode</code> как детей.</td></tr>
|
||
<tr><td><code>ScrollView()</code></td><td><code>SCROLL</code></td><td>Область с прокруткой содержимого.</td></tr>
|
||
<tr><td><code>RawPictureWidget()</code></td><td><code>RAW_PIC</code></td><td>Отображает объект <code>RawPicture</code> из <code>data.picture</code>.</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<section id="api-window">
|
||
<h2>📚 API Справочник: gui::window</h2>
|
||
<details>
|
||
<summary><code>window(std::string title)</code></summary>
|
||
<div class="details-content">
|
||
<p>Конструктор. Инициализирует соединение с X11, создает окно заданного размера (по умолчанию 1200x600) и настраивает маску событий.</p>
|
||
</div>
|
||
</details>
|
||
<details>
|
||
<summary><code>void update()</code></summary>
|
||
<div class="details-content">
|
||
<p>Главный метод цикла. Обрабатывает все накопленные события X11, обновляет анимации виджетов (<code>tick</code>), очищает окно и перерисовывает вкладки и активную страницу. <strong>Должен вызываться постоянно.</strong></p>
|
||
</div>
|
||
</details>
|
||
<details>
|
||
<summary><code>void addPAGE(std::string name)</code> / <code>void configur_page(...)</code></summary>
|
||
<div class="details-content">
|
||
<p><code>addPAGE</code> создает пустую страницу. <code>configur_page</code> создает (если нет), применяет лямбда-функцию для наполнения виджетами и может сделать её активной (<code>active = true</code>).</p>
|
||
</div>
|
||
</details>
|
||
<details>
|
||
<summary><code>void openPAGE(std::string name)</code></summary>
|
||
<div class="details-content">
|
||
<p>Делает указанную страницу активной, деактивируя остальные. Генерирует событие <code>"page.open"</code>.</p>
|
||
</div>
|
||
</details>
|
||
</section>
|
||
|
||
<section id="api-combat">
|
||
<h2>📚 API Справочник: gui::combat</h2>
|
||
<p>Базовый класс для контейнеров. Все методы создания виджетов возвращают ссылку на созданный <code>Widget</code>, что позволяет сохранять его для последующего изменения.</p>
|
||
<details>
|
||
<summary>Фабричные методы (<code>Button</code>, <code>Label</code>, <code>Input</code> и т.д.)</summary>
|
||
<div class="details-content">
|
||
<p>Принимают объект <code>WidgetData</code>. Автоматически устанавливают соответствующий <code>WidgetType</code>, добавляют виджет во внутренний вектор и возвращают на него ссылку.</p>
|
||
</div>
|
||
</details>
|
||
<details>
|
||
<summary><code>Widget* find(const std::string& id)</code></summary>
|
||
<div class="details-content">
|
||
<p>Рекурсивный поиск виджета по его уникальному идентификатору <code>id</code> внутри контейнера и всех его дочерних элементов.</p>
|
||
</div>
|
||
</details>
|
||
<details>
|
||
<summary><code>void visit(const std::function<void(Widget&)>& fn)</code></summary>
|
||
<div class="details-content">
|
||
<p>Применяет переданную лямбда-функцию ко всем виджетам в иерархии. Удобно для массовой модификации (например, отключения всех кнопок).</p>
|
||
</div>
|
||
</details>
|
||
</section>
|
||
|
||
<section id="api-widget">
|
||
<h2>📚 API Справочник: gui::Widget</h2>
|
||
<details>
|
||
<summary><code>void setValue(float v)</code> / <code>float getValue()</code></summary>
|
||
<div class="details-content">
|
||
<p>Устанавливает целевое значение. Для прогресс-баров и слайдеров это запускает плавную анимацию изменения <code>data.value</code> к <code>data.target_value</code> внутри метода <code>tick()</code>.</p>
|
||
</div>
|
||
</details>
|
||
<details>
|
||
<summary>Анимация и состояние</summary>
|
||
<div class="details-content">
|
||
<p>Виджет содержит поля <code>anim</code> (для dropdown), <code>phase</code> (для неопределенного прогресса). Метод <code>tick(float dt)</code> автоматически вызывается из <code>window::update()</code> и интерполирует значения.</p>
|
||
</div>
|
||
</details>
|
||
</section>
|
||
|
||
<section id="tips">
|
||
<h2>💡 Советы и лучшие практики</h2>
|
||
<ul>
|
||
<li><strong>Производительность:</strong> Библиотека использует программный рендеринг (CPU). Для сложных сцен избегайте частого пересоздания <code>RawPicture</code>. Используйте <code>blit</code> для кеширования отрисовки.</li>
|
||
<li><strong>ID Виджетов:</strong> Всегда задавайте уникальное поле <code>id</code> в <code>WidgetData</code>, если планируете обращаться к виджету позже или слушать его события.</li>
|
||
<li><strong>Вложенность:</strong> Виджеты <code>TreeView</code> и <code>ScrollView</code> предназначены для содержания других виджетов. Добавляйте их через стандартные методы, они будут учитываться при расчете высоты контента и прокрутки.</li>
|
||
<li><strong>Цвета:</strong> Указываются в формате HEX (например, <code>0xFF0000</code> для красного). Библиотека автоматически преобразует их в формат пикселей X11 при отрисовке.</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<footer style="margin-top: 4rem; padding-top: 2rem; border-top: 1px solid var(--border); color: var(--text-secondary); text-align: center; font-size: 0.9rem;">
|
||
<p>Сгенерировано для X11 C++ GUI Library. 2026</p>
|
||
</footer>
|
||
</main>
|
||
|
||
<!-- Скрипты -->
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-cpp.min.js"></script>
|
||
<script>
|
||
// Функция копирования кода
|
||
function copyCode(button) {
|
||
const codeBlock = button.parentElement.nextElementSibling.querySelector('code');
|
||
const text = codeBlock.innerText;
|
||
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
const originalText = button.innerText;
|
||
button.innerText = 'Скопировано!';
|
||
button.classList.add('copied');
|
||
|
||
setTimeout(() => {
|
||
button.innerText = originalText;
|
||
button.classList.remove('copied');
|
||
}, 2000);
|
||
});
|
||
}
|
||
|
||
// Поиск по документации
|
||
const searchInput = document.getElementById('searchInput');
|
||
const sections = document.querySelectorAll('section');
|
||
const navLinks = document.querySelectorAll('.nav-link');
|
||
|
||
searchInput.addEventListener('input', (e) => {
|
||
const term = e.target.value.toLowerCase();
|
||
|
||
sections.forEach(section => {
|
||
const text = section.innerText.toLowerCase();
|
||
if (text.includes(term)) {
|
||
section.style.display = 'block';
|
||
} else {
|
||
section.style.display = term === '' ? 'block' : 'none';
|
||
}
|
||
});
|
||
|
||
// Подсветка активных ссылок в сайдбаре
|
||
navLinks.forEach(link => {
|
||
const targetId = link.getAttribute('href').substring(1);
|
||
const targetSection = document.getElementById(targetId);
|
||
if (targetSection && targetSection.style.display === 'none') {
|
||
link.style.display = 'none';
|
||
} else {
|
||
link.style.display = 'block';
|
||
}
|
||
});
|
||
});
|
||
|
||
// Плавная прокрутка и активный класс
|
||
navLinks.forEach(link => {
|
||
link.addEventListener('click', (e) => {
|
||
navLinks.forEach(l => l.classList.remove('active'));
|
||
link.classList.add('active');
|
||
});
|
||
});
|
||
|
||
window.addEventListener('scroll', () => {
|
||
let current = '';
|
||
sections.forEach(section => {
|
||
const sectionTop = section.offsetTop;
|
||
if (pageYOffset >= sectionTop - 100) {
|
||
current = section.getAttribute('id');
|
||
}
|
||
});
|
||
|
||
navLinks.forEach(link => {
|
||
link.classList.remove('active');
|
||
if (link.getAttribute('href').substring(1) === current) {
|
||
link.classList.add('active');
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |