JS-рантайм
griffincss.js — компактный IIFE-скрипт (9,7 КБ, 3,9 КБ gzip), который сканирует DOM, парсит data-gr-layout атрибуты и генерирует CSS Grid правила на лету. Все четыре рантайма библиотеки вместе — 10,5 КБ gzip, а бандлом griffincss-all.js — 9,0 КБ одним файлом.
Архитектура
DOM-сканирование
Обходит все элементы с data-gr-layout и его вариантами, собирает наборы раскладок для каждого элемента.
Генерация CSS
Внедряет <style id="griffincss-dynamic"> с тремя секциями: FOUC guard, Grid Layouts и Grid Areas. Это единственный лист, который библиотека создаёт из JS, — см. Строгий CSP.
Класс на набор
Набор раскладок элемента хешируется в класс .gr-l-<хеш>. Правила пишутся под класс, поэтому одинаковая базовая раскладка с разной адаптивностью не конфликтует.
Непересекающиеся брейкпоинты
Диапазоны в range-синтаксисе: (768px <= width < 1024px). Без арифметики -1px, поэтому единицы измерения не важны и щелей на дробных ширинах нет.
Жизненный цикл
- Скрипт загружается (IIFE выполняется немедленно).
- Первым действием ставит FOUC-защиту:
[data-gr-layout]:not(.gr-ready) { opacity: 0 }. В статическом CSS этого правила нет — если скрипт не выполнился, контент остаётся видимым. - Если
document.readyState === 'loading'— ждётDOMContentLoaded. Griffincss.init()читает CSS-переменные--gr-bp-*из computed styles.- Сканирует DOM: находит все элементы с layout-атрибутами.
- Группирует раскладки по элементам, хеширует набор в класс
.gr-l-<хеш>и вешает его на контейнер. - Генерирует CSS:
grid-template-areas,grid-areaклассы, правила авто-скрытия. - Внедряет CSS в
<style id="griffincss-dynamic">в<head>. - Выполняет авто-присвоение
gr-area-*детям без явного класса. - Добавляет
.gr-readyна grid-контейнеры (снятие FOUC-защиты). - Поднимает наблюдателя за
body: контейнер, вставленный позже, раскладывается сам (с 0.26.0; выключатель —data-observe="false").
API
Глобальный объект window.Griffincss предоставляет следующие методы:
Griffincss.init()
// Запускает сканирование DOM и генерацию CSS.
// Вызывается автоматически при загрузке страницы.
// При data-auto="false" на теге script нужно вызвать вручную.
Griffincss.init();
Griffincss.refresh(root?)
// Повторно сканирует DOM и обновляет CSS.
// Добавленные узлы рантайм видит сам (наблюдатель ниже); refresh()
// нужен, когда наблюдение выключено или когда data-gr-layout
// поменялся на уже существующем элементе.
// root — опциональный корневой элемент (по умолчанию document).
Griffincss.refresh();
Griffincss.refresh(document.getElementById('dynamic-content'));
Griffincss.observe(root?)
// MutationObserver за новыми элементами с layout-атрибутами.
// После init() автостарт поднимает его на document.body сам;
// вызов нужен, когда наблюдение выключено (data-observe="false")
// или при ручной инициализации с data-auto="false".
// root — опционально (по умолчанию document.body).
Griffincss.observe();
// Для конкретного контейнера:
Griffincss.observe(document.getElementById('app'));
Griffincss.destroy()
// Полностью останавливает рантайм:
// - Отключает все MutationObserver и отменяет отложенные перерисовки
// - Снимает с элементов .gr-ready, .gr-l-* и назначенные им gr-area-*
// (явно проставленные в разметке gr-area-* остаются)
// - Очищает кеш сгенерированных CSS-правил
// - Удаляет <style id="griffincss-dynamic">
Griffincss.destroy();
Griffincss.parseLayout(str)
// Парсит layout-строку и возвращает двумерный массив.
// Полезно для отладки и понимания формата.
Griffincss.parseLayout('a3b4-c2d5');
// → [['a','a','a','b','b','b','b'], ['c','c','d','d','d','d','d']]
Griffincss.parseLayout('hdr4-main4-side2_2-ftr4');
// → [['hdr','hdr','hdr','hdr'],
// ['main','main','main','main'],
// ['side','side','.','.'],
// ['ftr','ftr','ftr','ftr']]
parseLayout — отличный инструмент для изучения формата. Откройте консоль браузера и экспериментируйте с разными строками.
Griffincss.version
// Версия рантайма — строка, совпадающая с версией пакета griffincss-core.
Griffincss.version; // '0.26.1'
Валидация раскладок
Рантайм проверяет layout-строку до генерации CSS. Раскладка отвергается, если в ней есть:
| Ошибка | Пример |
|---|---|
| счётчик без имени области | 3a |
| нулевой счётчик | a0b2 |
| счётчик больше 64 | a99999b |
| имя длиннее 32 символов | aaaa…a1 |
| недопустимые символы в имени | a.b1 |
| пустой ряд | a1- |
При ошибке контейнер пропускается целиком — сломанный CSS не генерируется, а в консоль уходит
Griffincss: … с описанием. Контейнер при этом остаётся видимым:
FOUC-защита с него снимается.
Пустое значение (data-gr-layout="") ошибкой не считается и предупреждения
не даёт — раскладывать просто нечего. Контейнер всё равно открывается: FOUC-защита ловит
элемент по наличию атрибута, и без .gr-ready он остался бы прозрачным навсегда.
Griffincss.parseLayout() на невалидной строке бросает исключение — это удобно
для проверки раскладок в консоли.
Авто-инициализация
По умолчанию скрипт автоматически инициализируется при загрузке страницы. Чтобы отключить это поведение:
<script src="griffincss.js" data-auto="false"></script>
<script>
// Ручная инициализация в нужный момент
document.addEventListener('DOMContentLoaded', function () {
Griffincss.init();
Griffincss.observe(); // наблюдение за поздними узлами — тоже вручную
});
</script>
Три переключателя на теге скрипта — все со значением false:
| Атрибут | Что выключает |
|---|---|
data-auto="false" | Автостарт целиком: ни FOUC-защиты, ни разбора, пока не вызван init() |
data-stream="false" | Потоковую раскладку по мере разбора документа: один проход на DOMContentLoaded |
data-observe="false" | Наблюдение за живым деревом после init(): поздние контейнеры ждут refresh() или observe() |
Что в <head>, что defer
Синхронно и в <head> нужны только два файла, и оба
из пакета griffincss-core: рантайм темы ставит атрибуты темы,
стиля и режима до первой отрисовки — сохранённая тёмная тема не мигает
светлой, — а рантайм раскладок ставит FOUC-защиту и складывает
data-gr-layout по мере разбора документа. Подключённые
с defer, они делали бы то же самое, но после отрисовки:
светлый кадр перед тёмным и контейнеры, которые сначала показаны как
есть, а потом гаснут и раскладываются. Остальным рантаймам и слою виджетов
до первой отрисовки ставить нечего — их место в defer:
они выполняются по порядку после разбора документа, до
DOMContentLoaded. Рантаймы компонентов и утилит стартуют
сразу, не дожидаясь события; слой виджетов с отложенного тега ждёт
DOMContentLoaded — к нему выполнятся и следующие отложенные
теги слоя (griffinjs-fields.js, griffinjs-countries.js),
и старт увидит поставку целиком, как при синхронных тегах в подвале.
<script src="griffincss-theme.js"></script> <!-- синхронно: атрибуты темы до отрисовки -->
<script src="griffincss.js"></script> <!-- синхронно: FOUC-защита, раскладки по мере разбора -->
<script defer src="griffincss-ui.js"></script> <!-- поведение компонентов -->
<script defer src="griffincss-utils.js"></script> <!-- каскад скруглений, произвольные значения -->
<script defer src="griffinjs.js"></script> <!-- слой виджетов -->
| Файл | Где | Чем платит другой вариант |
|---|---|---|
griffincss-theme.js | синхронно, <head> | с defer — кадр светлой темы перед сохранённой тёмной |
griffincss.js | синхронно, <head> | с defer — контейнеры отрисованы как есть, затем гаснут FOUC-защитой и раскладываются; потокового режима нет |
griffincss-ui.js | defer | синхронно в <head> он всё равно ждёт DOMContentLoaded — выигрыша нет, а блокирующий путь длиннее. Панели вкладок до старта видны все (база без скрипта); если это заметно, ставьте hidden неактивным в разметке |
griffincss-utils.js | defer | синхронно он достраивает углы вложенных .gr-radius по мере разбора, с defer — одним проходом после; первый уровень и его прямые дети скруглены статикой в обоих случаях |
griffinjs.js | defer | до старта разметка живёт базой без скрипта — <details>, <dialog>, дорожка со scroll-snap; это рабочее состояние, а не поломка. Старт — на DOMContentLoaded, после всех отложенных тегов; тег, вставленный скриптом в готовый документ, стартует сразу после своего файла |
griffincss-all.js | синхронно, <head> | бандл «для простоты»: одним файлом легче суммы четырёх, но тема и раскладки внутри него держат в блокирующем пути и компоненты с утилитами — 9,0 КБ gzip вместо 5,2 КБ у двух файлов пакета core |
Свой стартовый код, которому нужны рантаймы, — тоже defer
и после них: инлайновый <script> выполняется при разборе,
раньше любого отложенного файла, и Griffincss.ui в нём ещё
undefined. Готовый файл целиком —
starter.html.
Строгий CSP
<style> создают два файла библиотеки, оба из пакета
griffincss-core: griffincss.js пишет в свой лист
правила раскладок data-gr-layout, а griffincss-theme.js
на два кадра ставит лист с transition: none, чтобы тема менялась
за один кадр, а не пятнами («Тема»). Ни
griffincss-ui.js, ни griffincss-utils.js, ни слой виджетов
griffinjs.js листов не заводят: им от политики нужен только
script-src. Поэтому весь разговор о style-src — про два файла,
и цена ошибки у них разная.
Под политикой style-src без 'unsafe-inline' браузер откажется
применять созданный скриптом лист. Для рантайма раскладок это потеря: страница останется
на месте — контент виден, ресет, компоненты и утилиты работают, — но раскладки
data-gr-layout не сложатся. Для рантайма темы — только вид: тема
переключится, но переходами, как до 0.26.0. Чтобы этого не случилось, оба рантайма
переносят на свой лист nonce со своего же тега <script>.
Готовая политика
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m';
style-src 'self' 'nonce-r4nd0m'
<link rel="stylesheet" href="griffincss-core.css">
<script src="griffincss-theme.js" nonce="r4nd0m"></script>
<script src="griffincss.js" nonce="r4nd0m"></script>
r4nd0m здесь — заглушка: настоящий nonce генерируется сервером
заново на каждый ответ и подставляется и в заголовок, и в атрибут тега. Значение рантайм
читает у document.currentScript в момент выполнения скрипта и больше нигде
не хранит: в текст сгенерированного CSS оно не попадает.
Что нужно политике при каждом варианте подключения
| Что подключено | Что требуется от политики |
|---|---|
Только CSS-файлы (<link>) |
style-src 'self' или домен CDN. 'unsafe-inline' не нужен |
griffincss.js — рантайм раскладок |
script-src для самого файла и 'nonce-…'
в style-src, тот же nonce — атрибутом на теге |
griffincss-theme.js — переключатель темы |
то же: 'nonce-…' в style-src и атрибутом на теге.
Без него теряется только смена за один кадр — тема переключается переходами |
griffincss-all.js — бандл |
то же: внутри бандла оба рантайма пакета core |
griffincss-ui.js, griffincss-utils.js |
только script-src: <style> они не создают |
griffinjs.js — слой виджетов |
только script-src: <style> слой не создаёт,
оформление приходит из griffinjs.css. С догрузкой полей
(data-fields на теге или GriffinJS.config.fields)
вставленный слоем <script> получает nonce
с тега слоя — под script-src 'nonce-…' атрибут на теге обязателен,
иначе бандл полей отброшен молча, а поля остаются базой без скрипта |
Чем это проверить
Заблокированный лист виден в консоли: все три движка называют нарушение и директиву — Chromium пишет «Applying inline style violates the following Content Security Policy directive», Firefox — «blocked an inline style (style-src-elem)», WebKit — «Refused to apply a stylesheet…». Тихо теряется не сообщение, а раскладка: без строки в консоли по одному виду страницы догадаться трудно.
Быстрая проверка в консоли — лист заведён и в нём есть правила:
var el = document.getElementById('griffincss-dynamic');
el.sheet ? el.sheet.cssRules.length : 'лист заблокирован политикой';
Рантайм, подключённый сборщиком, nonce взять неоткуда.
document.currentScript есть только у настоящего тега
<script>; при импорте из бандла он null. Под строгой
политикой подключайте griffincss.js и griffincss-theme.js
тегом — так, как показано выше.
Библиотека отвечает за свой код и говорит только о нём: какие директивы нужны её файлам и почему. Безопасность приложения целиком — не её предмет, и аудита она не заменяет.
Авто-присвоение областей (Auto-Assign)
Если дочерние элементы не имеют явного класса gr-area-*, JS-рантайм присваивает их автоматически:
- Читает имена областей из базового
data-gr-layout(без суффикса брейкпоинта). - Собирает имена, уже проставленные детям явно, — они исключаются из раздачи.
- Оставшиеся свободные имена раздаёт детям без класса, в порядке первого появления в раскладке.
- Дубликаты невозможны: одно имя не выдаётся дважды, даже если оно занимает несколько рядов.
- Детям сверх числа областей класс не выдаётся.
Важно: авто-присвоение использует только базовый data-gr-layout, не адаптивные варианты. Порядок присвоения фиксирован и не меняется при смене брейкпоинта.
Авто-скрытие (Auto-Hide)
JS-рантайм генерирует CSS-правила, которые автоматически скрывают элементы с gr-area-*, чьё имя отсутствует в grid-template-areas для текущего брейкпоинта.
/* Сгенерированное правило для набора с data-gr-layout="a1b1" */
.gr-l-3exjl0 > [class*="gr-area-"]:not(.gr-area-a):not(.gr-area-b) {
display: none;
}
Сохранение display: правило использует :not() и добавляет display: none только скрываемым элементам. Элементы, которые должны быть видны, не получают принудительного display — сохраняется их нативный тип (flex, block, table и т.д.).
Дедупликация CSS
JS-рантайм группирует раскладки по уникальным наборам и генерирует каждое CSS-правило ровно один раз:
- Каждый уникальный набор раскладок → один класс
.gr-l-<хеш>и один комплект правил - Каждое уникальное имя области → один класс
.gr-area-Xв/* Grid Areas */ - Элементы с одинаковым набором получают один и тот же класс; разные наборы — разные классы, даже если базовая раскладка совпадает
/* Структура сгенерированного CSS */
@layer griffincss.reset, griffincss.tokens, griffincss.core, griffincss.ui, griffincss.utils, griffincss.style, griffincss.hidden;
@layer griffincss.core {
/* FOUC guard */
[data-gr-layout]:not(.gr-ready), … { opacity: 0; }
[data-gr-layout].gr-ready, … { opacity: 1; transition: opacity 0.3s ease; }
@media (prefers-reduced-motion: reduce) {
[data-gr-layout].gr-ready, … { transition: none; }
}
/* Grid Layouts */
.gr-l-k7gi9w { … }
@media (width < 768px) { .gr-l-19l1jw3 { … } }
@media (width >= 768px) { .gr-l-19l1jw3 { … } }
/* Grid Areas */
.gr-area-a { grid-area: a; }
.gr-area-b { grid-area: b; }
.gr-area-hdr { grid-area: hdr; }
/* end */
}
Открытие контейнера уважает prefers-reduced-motion.
Переход opacity 0.3s — видимое движение, и под настройкой
«уменьшить движение» его нет: содержимое появляется мгновенно.
Снимается анимация снятия защиты, а не сама защита.
.gr-ready и .gr-l-* принадлежат рантайму.
Присвоение className целиком — так делают React и Vue при изменении
своего пропа — снимает с контейнера оба класса разом. Раскладка исчезает,
а FOUC-защита ловит элемент по атрибуту и оставляет его в opacity: 0:
пустое место без единой ошибки в консоли. Рантайм это переживает —
наблюдатель слушает и атрибут class, замечает пропажу
маркеров на разложенном контейнере и возвращает их, не трогая классы,
поставленные вами. С data-observe="false" возвращать их некому.
Подробнее — шестая
оговорка для фреймворков.
Слой каскада. Весь вывод рантайма лежит в @layer griffincss.core —
там же, где статический griffincss-core.css. Порядок слоёв объявляется целиком,
поэтому страница, на которой подключён только JS, задаёт его так же. Практический смысл:
пользовательский CSS вне слоёв выигрывает у библиотеки без !important,
каким бы ни был порядок подключения файлов.
Непересекающиеся брейкпоинты
В отличие от традиционного подхода с накоплением min-width, griffincss использует диапазоны:
| Layout | @media-правило |
|---|---|
Базовый (data-gr-layout) | (width < 640px) — до первого адаптивного |
data-gr-layout-sm | (640px <= width < 768px) |
data-gr-layout-md | (768px <= width < 1024px) |
data-gr-layout-lg | (1024px <= width < 1280px) |
data-gr-layout-xl (последний) | (width >= 1280px) — от последнего |
Зачем? Это гарантирует, что только один набор правил активен в любой момент. Правила авто-скрытия из одного брейкпоинта не конфликтуют с другим.
Границы считаются по атрибутам элемента, а не по всей шкале. В таблице выше
заданы все четыре варианта, поэтому диапазоны идут подряд. Если на элементе только
data-gr-layout и data-gr-layout-lg, диапазонов будет два:
(width < 1024px) и (width >= 1024px). Раскладка без единого
адаптивного варианта правило в @media не заворачивает вовсе.
MutationObserver
После init() рантайм подписывается на изменения document.body
и сам вызывает refresh() при появлении новых элементов с layout-атрибутами —
SPA, подгруженный фрагмент, innerHTML. Так с 0.26.0: FOUC-защита ловит
любой контейнер по атрибуту, в том числе вставленный после загрузки, — это обещание
его показать, и без наблюдателя обещание не выполнялось: вставленный позже блок
оставался прозрачным, пока автор не вызовет refresh() сам.
Перерисовка идёт с дебаунсом и только по тому корню, за которым ведётся наблюдение, —
серия мутаций даёт один проход, а остальной документ не пересканируется.
Замер на странице «Паттерны»: порция из 500 контейнеров — один проход,
4–7 мс на трёх движках.
// Позже добавляем контент динамически
app.innerHTML += '<div data-gr-layout="a2b2">...</div>';
// наблюдатель вызовет refresh() сам
// Выключить: <script src="griffincss.js" data-observe="false">
// и тогда — свой корень или свой момент
Griffincss.observe(document.getElementById('app'));
Griffincss.refresh();
Наблюдение идёт за добавлением узлов, не за атрибутами.
Наблюдатель подписан на childList, subtree и атрибут
class, поэтому смена data-gr-layout на уже существующем
элементе его не разбудит — вызовите Griffincss.refresh() сами.
Атрибут class в списке ровно за одним: заметить, что с разложенного
контейнера сняли .gr-ready или .gr-l-*, и вернуть их. Такой повторный проход безопасен: прежний класс
.gr-l-* снимается, а выданные раньше gr-area-* переназначаются
по новой раскладке (классы, проставленные в разметке вручную, остаются на месте).