Files
magistr/docs/FRONTEND.md

40 KiB
Raw Blame History

🎨 Frontend

Общая информация

Параметр Значение
Фреймворк Нет (Vanilla JavaScript)
Модульная система ES6 Modules (import/export)
Стили CSS (модульный подход)
Шрифт Inter на странице входа, системный шрифт в кабинетах расписания
Веб-сервер Apache httpd:alpine

Структура файлов

frontend/
├── index.html              # 🔐 Страница авторизации (общая)
├── script.js               # Логика авторизации
├── style.css               # Стили страницы авторизации
├── theme-toggle.js         # Переключение светлой/тёмной темы
├── Dockerfile              # httpd:alpine
│
├── admin/                  # 👨‍💼 Интерфейс администратора
│   ├── index.html          # SPA-оболочка с sidebar
│   ├── css/
│   │   ├── main.css        # CSS-переменные, цвета, типографика
│   │   ├── layout.css      # Раскладка (sidebar, topbar, content)
│   │   ├── components.css  # Кнопки, таблицы, карточки, формы
│   │   ├── modals.css      # Модальные окна
│   │   ├── auditorium-workload.css # Таблицы расписаний и загруженности
│   │   └── departments-data.css # Стили создания кафедры/специальности
│   ├── js/
│   │   ├── main.js         # Инициализация, маршрутизация, навигация
│   │   ├── api.js          # HTTP-обёртка (fetch + Authorization)
│   │   ├── utils.js        # Утилиты
│   │   ├── otel.js         # OpenTelemetry (клиентская телеметрия, только прод)
│   │   └── views/          # Модули представлений
│   │       ├── dashboard.js     # Дашборд
│   │       ├── users.js         # Управление пользователями
│   │       ├── teacher-requests.js # Заявки кафедр на преподавателей
│   │       ├── groups.js        # Управление группами
│   │       ├── classrooms.js    # Управление аудиториями
│   │       ├── subjects.js      # Управление дисциплинами
│   │       ├── university-structure.js # Кафедры, специальности и профили обучения
│   │       ├── department-workspace.js # Кабинет кафедры в общей панели
│   │       ├── schedule-view.js # Read-only просмотр расписаний в матрицах
│   │       ├── schedule.js      # Конструктор правил расписания
│   │       ├── academic-calendar.js # Календарные учебные графики
│   │       └── auditorium-workload.js # Загруженность аудиторий, преподавателей и кафедр
│   ├── views/              # HTML-шаблоны представлений
│   │   ├── dashboard.html
│   │   ├── users.html
│   │   ├── teacher-requests.html
│   │   ├── groups.html
│   │   ├── classrooms.html
│   │   ├── subjects.html
│   │   ├── university-structure.html
│   │   ├── department-workspace.html
│   │   ├── schedule-view.html
│   │   ├── schedule.html
│   │   ├── academic-calendar.html
│   │   └── auditorium-workload.html
│   │
│   └── settings/           # ⚙️ Страница настроек (отдельный SPA)
│       ├── index.html      # Оболочка с собственной sidebar
│       ├── css/
│       │   ├── main.css    # CSS-переменные, базовые стили
│       │   └── layout.css  # Sidebar, topbar, content
│       ├── js/
│       │   ├── main.js     # Навигация по вкладкам настроек
│       │   └── views/
│       │       └── time-slots.js # Настройка временных слотов
│       └── views/
│           ├── general.html    # Общие настройки (заглушка)
│           └── time-slots.html # Базовая, субботняя и ручные сетки времени
│
├── teacher/                # 👩‍🏫 Интерфейс преподавателя
│   └── index.html          # Недельный просмотр динамического расписания преподавателя
├── department/             # 🏛 Кабинет кафедры
│   └── index.html          # Redirect в `/admin/#department-workspace`
├── edu-office/             # 🗓 Кабинет учебного отдела
│   └── index.html          # Redirect в `/admin/#schedule-view`
│
└── student/                # 🎓 Интерфейс студента
    └── index.html          # Недельный просмотр динамического расписания группы

Система маршрутизации (Admin SPA)

Админ-панель работает как Single Page Application без фреймворка.

Навигация реализована через data-tab атрибуты на элементах sidebar:

<a href="#" class="nav-item" data-tab="users">Пользователи</a>
<a href="#" class="nav-item" data-tab="groups">Группы</a>
<a href="#" class="nav-item" data-tab="schedule">Расписание занятий</a>
<a href="#" class="nav-item" data-tab="academic-calendar">Календарный график</a>

При клике на пункт меню main.js:

  1. Загружает HTML-шаблон из views/{tab}.html через fetch()
  2. Вставляет его в #app-content
  3. Подключает соответствующий JS-модуль из js/views/{tab}.js
  4. Обновляет заголовок страницы (#page-title)

main.js также фильтрует вкладки по роли:

Роль Доступные вкладки
ADMIN Все вкладки
EDUCATION_OFFICE Просмотр расписаний, конструктор расписания, календарный график, загруженность, аудитории, оборудование
DEPARTMENT Кабинет кафедры, просмотр расписаний
SCHEDULE_VIEWER Только просмотр расписаний

Пути /department/ и /edu-office/ оставлены как входные redirect-страницы в общую панель. Отдельные кабинеты не дублируют UI админ-панели.

Пункты меню, недоступные роли, не отображаются в sidebar: main.js выставляет hidden, а layout.css явно скрывает такие элементы, чтобы базовый display: flex у .nav-item не возвращал их на экран.

Разделы админ-панели

Tab Описание API
teacher-requests Очередь заявок кафедр на создание преподавателей с редактированием перед одобрением /api/teacher-requests, /api/departments
users CRUD пользователей /api/users
groups CRUD групп, мультифильтр списка по формам обучения, настройка 0/2/3 подгрупп для лабораторных и назначения графиков /api/groups, /api/subgroups
classrooms Аудитории /api/classrooms
subjects Дисциплины /api/subjects
university-structure Кафедры, специальности и профили обучения; профили доступны отдельной внутренней вкладкой и через кнопку специальности /api/departments, /api/specialties, /api/specialties/{id}/profiles
department-workspace Кабинет кафедры: дисциплины, импорт, комментарии, преподаватели, привязка преподавателей, заявки на новых преподавателей и нагрузка /api/department/*, /api/department/teacher-requests, /api/workload/teachers
schedule-view Read-only просмотр расписаний: по одной выбранной дате строится двухнедельный диапазон, найденные расписания выбираются в переключателе, а на экране отображается одна активная совмещённая таблица чётной/нечётной недели /api/schedule/search
schedule Конструктор правил динамического расписания с выезжающей визуальной матрицей групп по дням и времени /api/admin/schedule-rules, /api/admin/time-slots, /api/admin/calendar/years, /api/lesson-types, /api/subgroups
academic-calendar Учебные годы, семестры, создание календарных графиков, Excel-подобный редактор дневной сетки и привязка дисциплин к семестрам графика /api/admin/calendar, /api/admin/academic-calendars, /api/admin/academic-calendars/{id}/subjects, /api/admin/calendar/activity-types, /api/education-forms, /api/subjects
auditorium-workload Динамическая загруженность аудиторий, преподавателей и кафедр: сводная матрица по дате или совмещённая таблица выбранной сущности по чётной/нечётной неделе /api/classrooms, /api/users/teachers, /api/departments, /api/admin/time-slots, /api/equipments, /api/groups, /api/schedule, /api/admin/calendar/years

Особенности админских вкладок

  • Вкладка groups загружает кафедры, специальности, профили, учебные годы и календарные графики. Список групп открывается через /api/groups?includeArchived=true, поэтому в таблице видны активные, будущие, завершившие обучение и архивные группы со статусом. Группа создаётся через /api/groups с specialtyId и specialtyProfileId, блок подгрупп использует /api/subgroups и /api/groups/{id}/subgroups, а блок назначений использует /api/groups/{id}/calendar-assignments. После назначения графика в таблице назначений сразу выводятся дисциплины графика, сгруппированные по номерам семестров. В селекты подгрупп и назначений попадают только группы с active=true.
  • Вкладка teacher-requests показывает pending-заявки кафедр на создание преподавателей. Администратор может скорректировать кафедру, логин, ФИО и должность, задать пароль минимум 8 символов, затем одобрить заявку через /api/teacher-requests/{id}/approve или отклонить её через /api/teacher-requests/{id}/reject. Для роли ADMIN счётчик pending-заявок выводится в пункте меню «Заявки» и рядом с заголовком страницы, чтобы очередь была видна без открытия вкладки.
  • Вкладка department-workspace в блоке преподавателей объединяет данные /api/department/teachers и /api/workload/teachers: каждый преподаватель показывается одной карточкой с должностью и нагрузкой за выбранный период, преподаватели без занятий получают нулевую нагрузку, а преподаватели из расписания добавляются без дублей. Если дата начала периода выбрана позже даты окончания, поле окончания очищается, а расчёт нагрузки ждёт корректный период.
  • Вкладка department-workspace позволяет кафедре добавить существующего активного преподавателя на свою кафедру через /api/department/teachers/{teacherId}/assignments, отправить заявку на нового преподавателя через /api/department/teacher-requests и видеть статусы собственных заявок в таблице.
  • Компоновка department-workspace использует собственные CSS-сетки department-workspace-filter-grid и department-workspace-actions-grid: фильтры периода отделены от сеток расписания, загрузка дисциплин занимает широкую колонку, формы преподавателей выравниваются справа, а списки и таблицы идут полноширинными блоками ниже.
  • Вкладка schedule-view показывает найденные занятия в режиме одной активной таблицы. Пользователь выбирает, что смотреть: группу, преподавателя, аудиторию или кафедру; frontend запрашивает двухнедельный диапазон от понедельника выбранной даты и собирает найденные расписания в переключатель результатов. На странице не выводится стек таблиц: виден один выбранный результат, а остальные доступны через чипы и кнопки предыдущего/следующего расписания; чипы результатов переносятся и отделены от счётчика стабильным отступом. Для режима кафедры и роли DEPARTMENT расписание ограничивается кафедрой пользователя; преподавательские и студенческие отдельные страницы пока остаются самостоятельными. Таблица строится как строки пар и столбцы дней недели. Нечётная неделя отображается в верхней половине ячейки, чётная — в нижней, а одинаковые занятия в обе недели схлопываются в цельную ячейку. Бейдж диапазона недель скрывается для занятий на весь семестр, а для занятий до конца семестра показывает только неделю начала в формате (с 5 нед.). На мобильной ширине вместо широкой недельной матрицы показывается один день активного расписания с переключателем дней.
  • Вкладка auditorium-workload стала общей вкладкой Загруженность: в поле «Что смотреть» выбираются аудитории, преподаватели или кафедры. Сводная матрица по выбранной дате использует одинаковую структуру: строки — выбранный тип сущности, столбцы — эффективные временные слоты дня из /api/admin/time-slots/effective, занятость собирается из динамического расписания /api/schedule по группам. Кафедральная матрица группирует занятия по кафедре преподавателя. Для аудиторий доступны фильтры корпуса, вместимости и оборудования. В поле «Отображение» можно выбрать конкретную аудиторию, преподавателя или кафедру; тогда сводная матрица заменяется одной таблицей по дням недели и времени для двухнедельного периода от выбранной даты. Таблица выбранной сущности растягивается до нижней части экрана. Ячейка делится вертикально только если верхняя и нижняя недели отличаются: нечётная неделя отображается сверху, чётная — снизу. Если состояние или занятие одинаковое, ячейка остаётся цельной. Чётность берётся из расписания, а для свободных дней рассчитывается по семестрам из /api/admin/calendar/years.
  • Вкладка university-structure содержит внутренние разделы Кафедры, Специальности и Профили: администратор создаёт профили как из общего списка, так и через кнопку Профили у конкретной специальности.
  • Вкладка schedule не обращается к старым lessons API. Создание и редактирование расписания выполняется через правила /api/admin/schedule-rules, где каждое правило содержит группы, отдельные часы и недели начала для лекций, лабораторных и практик, а также набор базовых слотов. Группы выбираются через выпадающий мультиселект. Поле подгруппы появляется только при выборе лабораторной работы; для лекций и практик оно не отображается. Если в правиле выбрана одна группа, селект подгруппы содержит пункт Вся группа; если выбрано несколько групп, лабораторный слот показывает мультиселект подгрупп, чтобы выбрать разные подгруппы разных групп. Типы занятий в слоте сортируются в порядке: лекция, лабораторная работа, практика. Список слотов отображается без внутреннего вертикального скролла: при добавлении строк форма расширяется вниз, а кнопка сохранения остаётся отдельным блоком под слотами. Из календарной системы здесь используется список семестров для выбора периода действия правила. Справа доступна сворачиваемая визуальная матрица: пользователь выбирает учебный год, семестр и группы, после чего матрица строится только по правилам выбранного семестра. Столбцы — выбранные в фильтре группы, строки — только день и время, где есть активные пары, ячейки показывают дисциплину, диапазон недель, тип, формат, преподавателя, аудиторию и подгруппы. Период недель не показывается для занятия на весь семестр; если занятие идёт до конца семестра не с первой недели, выводится только неделя начала в формате (с 5 нед.), а ограниченный диапазон — как (с 1 по 3 нед.). Если нечётная и чётная недели отличаются, ячейка делится на две половины; одинаковые занятия схлопываются в цельную ячейку. Кнопка с тремя точками в правой части карточки пары открывает контекстное меню: можно открыть полное правило в форме, изменить только день и базовую пару выбранного слота через компактную модалку или удалить правило целиком. В списке правил действия отображаются едиными кнопками одинакового размера с отступами между ними.
  • Вкладка academic-calendar полностью отделяет календарную систему от расписания занятий и внутри себя разделена на три вкладки: Графики для учебных годов, семестров и карточек календарных графиков, Сетки для редактора дневной сетки, Дисциплины для ручной привязки дисциплин из /api/subjects к номерам учебных семестров графика. Администратор выбирает форму обучения из общего справочника /api/education-forms, заполняет дневную сетку по курсам и кодам активностей, назначает ручную временную сетку на конкретную дату, а сохранение сетки идёт через /api/admin/academic-calendars/{id}/grid.
  • При сохранении правила во вкладке schedule frontend различает 409 Conflict от остальных ошибок API. Если backend возвращает conflictRule, открывается широкое модальное окно разрешения конфликта: пользователь видит новое и ранее созданное правило, чипы причины конфликта из conflictReasons и подсветку соответствующих полей (teacher, classroom, group). Пользователь меняет у конфликтующего правила день, чётность, пару, преподавателя или аудиторию, после чего frontend сохраняет конфликтующее правило через PUT /api/admin/schedule-rules/{id} и автоматически повторяет исходный POST или PUT. Если после переноса появляется следующий конфликт, показывается следующее конфликтующее правило без потери исходного черновика.
  • Редактор годового графика во вкладке academic-calendar показывает компактную табличную сетку: курсы раскрываются отдельными секциями со стрелкой, семестры внутри курса на широком экране идут рядом в две равные колонки одинаковой высоты, столбцы подписаны номерами недель учебного года, строки — днями недели, а ячейки содержат буквенный код активности. В рамках одного курса семестровые таблицы получают одинаковое число недельных колонок: недостающие колонки заполняются пустыми ячейками, поэтому левая и правая части занимают всю ширину секции курса без внешней пустоты. Подробная расшифровка и изменение кода открываются в компактном модальном окне по клику на ячейку; при наведении отображается кастомная подсказка с датой, курсом, кодом активности и временной сеткой, а код активности в подсказке центрируется внутри квадратного индикатора. Ячейки можно выделять протяжкой мышью и менять код активности через ту же модалку, выбранные ячейки подсвечиваются мягкой заливкой, а кнопка «Применить» закрывает окно.
  • Вкладка classrooms теперь показывает архивные аудитории через includeArchived=true. Кнопка удаления заменена на архивирование: аудитория выводится из эксплуатации, но остаётся в историческом расписании. Для архивных аудиторий доступно восстановление.
  • Вкладка users поддерживает роли EDUCATION_OFFICE, DEPARTMENT и SCHEDULE_VIEWER; удаление пользователя работает как архивирование.

Страница настроек (/admin/settings/)

Настройки — это отдельный SPA со своей боковой панелью и вкладками, не связанными с основной админ-панелью.

  • Доступ: через dropdown «Настройки» в footer боковой панели админки для ADMIN и EDUCATION_OFFICE
  • Кнопка «Назад в панель» для возврата в /admin/
  • Текущие вкладки:
    • Общие настройки — заглушка (только ADMIN)
    • Временные слоты — выбор сетки через выпадающий список, CRUD слотов выбранной сетки, добавление ручных сеток через модальное окно и удаление выбранной ручной сетки. Базовая сетка применяется по умолчанию, субботняя — автоматически по субботам; ручное применение выполняется в сетке календарного графика.

API-клиент (api.js)

Все HTTP-запросы проходят через обёртку apiFetch(). Access JWT читается из localStorage перед каждым запросом:

export async function apiFetch(endpoint, method = 'GET', body = null, retryOnUnauthorized = true) {
    const response = await fetch(endpoint, {
        method,
        headers: getHeaders(body ? 'application/json' : null),
        credentials: 'same-origin',
        body: body ? JSON.stringify(body) : undefined
    });

    if (response.status === 401 && retryOnUnauthorized) {
        const refreshed = await refreshAccessToken();
        if (refreshed) return apiFetch(endpoint, method, body, false);
        clearAuthState();
        window.location.href = '/';
    }

    if (!response.ok) {
        throw new Error(data?.message || `Ошибка HTTP: ${response.status}`);
    }

    return await response.json();
}

// Shortcut-методы
export const api = {
    get: (url) => apiFetch(url, 'GET'),
    post: (url, body) => apiFetch(url, 'POST', body),
    put: (url, body) => apiFetch(url, 'PUT', body),
    delete: (url, body) => apiFetch(url, 'DELETE', body)
};

При 401 клиент один раз вызывает POST /api/auth/refresh, обновляет localStorage.token и повторяет исходный запрос. Если refresh неуспешен, auth state очищается и пользователь возвращается на страницу входа.

После мутаций api.js инвалидирует кэш не только по точному URL, но и по связанным префиксам. Для заявок и кафедральных привязок очищаются /api/users, /api/users/teachers, /api/teacher-requests, /api/department/teacher-requests и /api/department/teachers, чтобы таблицы заявок и списки преподавателей обновлялись без ручного сброса страницы.


Аутентификация (Frontend)

Страница входа (/index.html)

  1. Пользователь вводит логин/пароль
  2. script.js отправляет POST /api/auth/login
  3. При успехе сохраняет в localStorage:
    • token — access JWT
    • role — роль пользователя
    • departmentId — кафедра пользователя
    • userId — ID пользователя для личного расписания преподавателя
  4. Refresh-токен сохраняется браузером как HttpOnly cookie и недоступен JavaScript
  5. Перенаправляет на соответствующий интерфейс:
    • ADMIN/admin/
    • EDUCATION_OFFICE/admin/#schedule-view
    • DEPARTMENT/admin/#department-workspace
    • SCHEDULE_VIEWER/admin/#schedule-view
    • TEACHER/teacher/
    • STUDENT/student/

Проверка авторизации

На каждой странице проверяется наличие токена и роли:

export function isAuthenticatedAsAdmin() {
    const role = localStorage.getItem('role');
    return getToken() && role === 'ADMIN';
}

Выход

Кнопка «Выйти» вызывает POST /api/auth/logout, затем очищает localStorage и перенаправляет на /.


Кабинеты расписания

Учебный отдел (/admin/#schedule-view)

Учебный отдел работает в общей админ-панели с ограниченным набором вкладок:

  • расширенный поиск /api/schedule/search по дате в периоде, преподавателю, аудитории и кафедре;
  • конструктор расписания через /api/admin/schedule-rules;
  • создание точечного изменения пары через /api/edu-office/schedule/overrides на уровне API;
  • матричный просмотр загруженности аудиторий, преподавателей и кафедр через общую вкладку Загруженность;
  • аудитории, оборудование, календарный график и загруженность.

Кафедра (/admin/#department-workspace)

Кабинет кафедры встроен в общую панель и работает в контексте departmentId текущего пользователя:

  • список дисциплин кафедры через /api/department/subjects;
  • загрузка дисциплин из текстового списка в формате код; название;
  • комментарии к дисциплинам;
  • просмотр расписания кафедры через вкладку schedule-view;
  • загруженность преподавателей кафедры.

Просмотр расписаний (/admin/#schedule-view)

Роль SCHEDULE_VIEWER видит только read-only вкладку просмотра расписаний. Доступны фильтры по дате в периоде, группе, преподавателю, аудитории, кафедре, дисциплине, типу занятия, чётности и разрезу таблиц. Найденные занятия отображаются отдельными совмещёнными таблицами чётной/нечётной недели с парами по строкам и днями недели по столбцам.

Преподаватель (/teacher/)

Страница показывает недельную сетку занятий преподавателя. ID преподавателя берётся из localStorage.userId, который сохраняется после POST /api/auth/login.

Основные элементы:

  • навигация по неделям: предыдущая, текущая, следующая;
  • выбор даты через input[type="date"];
  • запрос GET /api/schedule?teacherId={userId}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD};
  • отображение дисциплины, времени, типа занятия, лабораторных подгрупп, аудитории и всех групп правила.

Если пользователь вошёл до появления поля userId, страница попросит выполнить вход заново.

Студент (/student/)

В текущей модели студент не связан с конкретной группой, поэтому страница использует выбор группы из /api/groups.

Основные элементы:

  • селект группы с сохранением выбора в localStorage.studentGroupId;
  • недельная сетка по дням;
  • запрос GET /api/schedule?groupId={groupId}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD};
  • отображение дисциплины, времени, преподавателя, аудитории, формата, типа занятия и лабораторных подгрупп.

CSS-архитектура

Модульный подход

Стили разделены на модульные файлы (порядок подключения важен):

  1. main.css — CSS-переменные (цвета, шрифты, отступы), глобальные стили, тёмная тема
  2. layout.css — Sidebar, topbar, content area, dropdown настроек, responsive
  3. components.css — Кнопки, таблицы, карточки, badge, формы, theme-toggle
  4. modals.css — Модальные окна
  5. departments-data.css — Стили создания кафедры/специальности

Темизация

CSS-переменные позволяют поддерживать светлую/тёмную тему:

:root {
    --bg-primary: #ffffff;
    --text-primary: #1a1a2e;
    --accent: #4F46E5;
}

[data-theme="dark"] {
    --bg-primary: #0f0f23;
    --text-primary: #e2e8f0;
    --accent: #8B5CF6;
}

Система кнопок

Кнопки админ-панели, отдельной SPA настроек, страницы входа и личных кабинетов используют единую семантическую модель:

  • btn — обязательная база для всех кнопок и ссылок-кнопок.
  • Размеры: btn-sm — 32px для табличных строк, btn-md — 40px для форм и панелей, btn-lg — 48px для входа и главных широких действий.
  • Иконки: btn-icon-sm — 32x32, btn-icon-md — 40x40. Все icon-only кнопки должны иметь aria-label на русском языке.
  • Варианты: btn-primary для основного действия блока, btn-secondary для редактирования/обновления/показа, btn-ghost для очистки/отмены/сворачивания, btn-danger для удаления, btn-danger-subtle для архивирования, выхода и других рискованных действий без немедленного удаления.
  • Единый радиус кнопок — 8px. Локальные переопределения .btn-primary, .btn-secondary, .btn-delete в feature CSS запрещены: новые экраны должны наследовать токены из frontend/admin/css/components.css.
  • Primary-акцент общий для продукта: #4F46E5 в светлой теме и #8B5CF6 в тёмной. Ролевые цвета кнопок в кабинетах преподавателя и студента не используются.

Переключение — через theme-toggle.js.


Боковая панель (Sidebar)

  • Скрытие/раскрытие — кнопка-крестик в правом верхнем углу sidebar
  • Десктоп (>768px): sidebar складывается влево, контент расширяется; состояние сохраняется в localStorage (sidebar-collapsed)
  • Мобильные (≤768px): sidebar скрывается за кнопкой-гамбургер, выезжает как overlay с затемнением
  • Dropdown «Настройки» в footer sidebar — содержит ссылку на страницу настроек и кнопку выхода

OpenTelemetry (otel.js)

Клиентская телеметрия (document-load, fetch, XHR) отправляется через BatchSpanProcessor на /otel/v1/traces.

  • На production — загружается автоматически через динамический import()
  • На localhost — пропускается, чтобы избежать таймаутов CDN esm.sh
if (!['localhost', '127.0.0.1'].includes(window.location.hostname)) {
    import('./otel.js').catch(e => console.warn('OTel init skipped:', e.message));
}

Адаптивность

Интерфейс адаптирован под мобильные устройства:

  • Sidebar скрывается на экранах < 768px, выезжает как overlay
  • Появляется кнопка-гамбургер (#menu-toggle)
  • Кнопка-крестик закрывает sidebar на всех устройствах
  • Таблицы получают горизонтальный скролл