# Фронтенд Интерфейс — серверный рендеринг Django-шаблонами плюс SCSS и vanilla JS. Ассеты npm-пакетов копирует `scripts/copy-assets.mjs`, линтеры вызываются своими CLI через npm scripts, сжатием на стороне Django занимается `django-compressor`. ## Структура | Путь | Содержимое | |---|---| | `network/templates/` | Django-шаблоны | | `network/static/js/` | Собственные скрипты | | `network/static/css/` | Исходники SCSS | | `network/static/lib/` | Ассеты из npm-пакетов, копируются `scripts/copy-assets.mjs` | | `staticfiles/` | Результат `collectstatic`, в репозитории не хранится | ## Слои SCSS `base.html` подключает три файла в этом порядке, и порядок значим — каждый следующий рассчитывает на объявления предыдущего: | Файл | Что в нём | |---|---| | `css/bootstrap.scss` | переменные Bootstrap, слой семантических токенов и те компоненты Bootstrap, которые подключены | | `css/theme.scss` | оформление проекта: кнопки, таблицы, типографика, модалки, компоненты страниц | | `css/app.scss` | шапка, подвал, «тело», движение; подключает `components/` | Страничные `css/pages/*.scss` подключаются своими шаблонами в блоке `{% block css %}`, то есть после общих трёх. Внутри `bootstrap.scss` три яруса: 1. **Пигменты** — `css/variables.scss`: `$orange-400`, `$blue-400`, `$dark-300` и остальная рампа. Отображаются на `$primary`, `$info`, `$success`, `$danger` до того, как Bootstrap прочитает свои умолчания. 2. **Токены** — `--surface`, `--surface-raised`, `--border`, `--text`, `--accent`, `--interactive`, `--status-*`. Это роли, а не цвета: значение меняется вместе с `[data-theme]`, поэтому правилу, написанному через токен, не нужен парный блок в светлой теме. `--bs-*` перенаправлены на них, чтобы вендорный CSS (тема `bootstrap5` у Tom Select) читал палитру проекта. 3. **Компоненты Bootstrap** — подключаются по одному, каждый со своим визуальным разбором: `reboot`, `grid`, `transitions`, `forms/{labels,form-text,form-control,form-select,form-check,validation}`, `button-group`, `nav`, `card`, `pagination`, `close`, `tooltip`, `modal`, `utilities` и `utilities/api`. Кнопки, таблицы и типографика Bootstrap **не** подключены сознательно: это оформление проекта, а не недоделанный Bootstrap. Подключить их значило бы подложить чужие правила под свои и переопределять обратно. Новое правило пишется через токен. Пигмент по имени (`$dark-600`) требует второго правила в блоке `[data-theme='light']`, и именно эти пары постепенно схлопываются по мере перевода страничных файлов. ### Фокус с клавиатуры Кольцо фокуса — одно правило в `theme.scss` и токен `--focus-ring` в обеих темах. Два обстоятельства объясняют его вид: - кольцо рисуется `box-shadow`, а не `outline`: `outline` у вариантов `btn-outline-*` работает рамкой, и десяток страничных правил его снимает; - кольцо двухслойное — 2 px `--surface`, затем 2 px `--focus-ring`. Один цвет не может пройти 3:1 и к фону страницы, и к заливке кнопки: почти белое кольцо даёт 16,9:1 на фоне и 2,6:1 на синей заливке. Внутренний слой цвета фона даёт 6,5:1 к этой же заливке, так что граница видна с любой стороны. Правило перечисляет четыре селектора, потому что трижды фокус берёт не тот элемент, который виден: обёртка Tom Select (`:has(:focus-visible)`), `label` вокруг спрятанного радио и `.btn-check` рядом с таким же `label`. Контрол, который не должен быть виден, прячется миксином `visually-hidden-control` из `variables.scss`, **не** `display: none`: `display: none` выкидывает его и из последовательности табуляции — так фильтры статуса четырёх каталогов, оба расписания станции и форма нового наблюдения были недоступны с клавиатуры вообще. Запрет проверяется тестом `test_no_stylesheet_hides_a_control_outright`. ### Почему `bootstrap.scss` сохраняет `@import` Остальное дерево на `@use`. Исходники Bootstrap 5.3 написаны через `@import`, а Sass запрещает `@use` файла, который сам импортирует, — то есть `@use 'bootstrap' with (...)` для настройки переменных недоступен. Отсюда три следствия: - `.stylelintrc` запрещает `@import` правилом `at-rule-disallowed-list` и освобождает от него один этот файл по имени; - в `COMPRESS_PRECOMPILERS` стоит `--silence-deprecation=import,color-functions,if-function,global-builtin` — все четыре категории приходят из вендорных исходников, настоящие ошибки при этом видны; - пути ведут в `../lib/bootstrap/scss/`, куда файлы кладёт `scripts/copy-assets.mjs`, а не в `node_modules`: в образе `node_modules` нет, из него копируется только бинарь dart-sass. Когда Bootstrap перейдёт на модули, это станет `@use 'bootstrap' with (...)`, а исключение в `.stylelintrc` уйдёт. ## Визуальная проверка Скриншот-диффового харнесса в проекте нет. Вместо него — список страниц, который прогоняется руками (Playwright MCP годится) на **375, 768 и 1440 px**, в **обеих темах**, до и после правки, меняющей вид. ```{note} Компрессор читает файлы через staticfiles storage, то есть из `staticfiles/`, а не из исходников. После правки JS или CSS: `rm -rf staticfiles`, `manage.py collectstatic --noinput` **и перезапуск сервера** — манифест кэшируется в процессе. ``` | Раздел | Страницы | |---|---| | Витрина | `/`, `/rko/`, `/launches/`, `/launches/?scope=past`, `/launches/?scope=calendar`, `/launches//` | | Наблюдения | `/observations/`, `/observations//`, `/observations/new/`, `/vet-observations/` | | Станции | `/stations/`, `/stations//`, `/stations/edit//`, `/stations/register/step1/`, `/stations_all/` | | Спутники | `/satellites/`, `/satellites//`, `/decay/`, `/transmitters/`, `/frequency-picker/`, `/orbit-sandbox/` | | Данные | `/gallery/`, `/gallery/?satellite=`, `/gallery/?station=`, `/spectrum/`, `/analytics/` | | Статистика | `/statistics/`, `/satellite_statistics/`, `/station_statistics/` | | Аккаунт | `/accounts/login/`, `/accounts/password/reset/` (только без `OIDC_AUTH`), `/users//` | | Ошибки | `/404`, `/500` | Отдельно, потому что разметку рисует JS и на скриншоте страницы её нет: модалка спутника (клик по спутнику в любом списке), модалка станции на `/satellites//`, модалки предложения спутника и передатчика на `/transmitters/`. Страница спутника переключает первый экран по состоянию аппарата, поэтому одним скриншотом не проверяется. Нужны три: аппарат, который сеть наблюдала за последние 30 дней (карта плюс живая сводка), аппарат на орбите без свежих наблюдений (карта плюс паспортная сводка) и сошедший с орбиты (вместо карты — фотография). Вкладки открываются и по адресу: `/satellites//#tab-orbit`. Страница запуска устроена так же и тоже требует трёх скриншотов: до старта (фотография и отсчёт), живой режим — у отслеживаемого запуска от T−1 ч до T+72 ч, для проверки без пуска `/launches//?live=1` под `is_staff`, — и архив. `/launches/` проверяется в каждом из трёх сегментов; герой-афиша и плитки при переключении остаются на месте. ```{warning} Видимость столбцов DataTables хранится в cookie `T_<столбец>` и переживает перезагрузку. Пустая или урезанная таблица после отладки — состояние браузера, а не регрессия. ``` ## Команды ```bash npm ci npm run assets # скопировать ассеты пакетов в network/static/lib npm run lint:js # eslint по network/static/js и scripts/ npm run lint:css # stylelint по network/static/css/**/*.{scss,css} npm run lint # оба линтера npm test # jest, tests/js/**/*.test.js ``` Список копируемых файлов — массив `assets` в `package.json`. Путь, которому ничего не соответствует, роняет `npm run assets` с кодом возврата 1, а не пропускается молча. Обновление фронтенд-зависимостей — `./soniks.sh update`. `satellite.js` намеренно остаётся на 6: седьмая версия выходит только как ES-модуль (`"type": "module"`, без UMD-сборки и `dist/satellite.min.js`). Страницы грузят её обычным `