JS-рантайм

griffincss.js — компактный IIFE-скрипт (9,7 КБ, 3,9 КБ gzip), который сканирует DOM, парсит data-gr-layout атрибуты и генерирует CSS Grid правила на лету. Все четыре рантайма библиотеки вместе — 10,5 КБ gzip, а бандлом griffincss-all.js9,0 КБ одним файлом.

3,9 КБ gzip IIFE Нет зависимостей Авто-инициализация

Архитектура

DOM-сканирование

Обходит все элементы с data-gr-layout и его вариантами, собирает наборы раскладок для каждого элемента.

Генерация CSS

Внедряет <style id="griffincss-dynamic"> с тремя секциями: FOUC guard, Grid Layouts и Grid Areas. Это единственный лист, который библиотека создаёт из JS, — см. Строгий CSP.

Класс на набор

Набор раскладок элемента хешируется в класс .gr-l-<хеш>. Правила пишутся под класс, поэтому одинаковая базовая раскладка с разной адаптивностью не конфликтует.

Непересекающиеся брейкпоинты

Диапазоны в range-синтаксисе: (768px <= width < 1024px). Без арифметики -1px, поэтому единицы измерения не важны и щелей на дробных ширинах нет.

Жизненный цикл

  1. Скрипт загружается (IIFE выполняется немедленно).
  2. Первым действием ставит FOUC-защиту: [data-gr-layout]:not(.gr-ready) { opacity: 0 }. В статическом CSS этого правила нет — если скрипт не выполнился, контент остаётся видимым.
  3. Если document.readyState === 'loading' — ждёт DOMContentLoaded.
  4. Griffincss.init() читает CSS-переменные --gr-bp-* из computed styles.
  5. Сканирует DOM: находит все элементы с layout-атрибутами.
  6. Группирует раскладки по элементам, хеширует набор в класс .gr-l-<хеш> и вешает его на контейнер.
  7. Генерирует CSS: grid-template-areas, grid-area классы, правила авто-скрытия.
  8. Внедряет CSS в <style id="griffincss-dynamic"> в <head>.
  9. Выполняет авто-присвоение gr-area-* детям без явного класса.
  10. Добавляет .gr-ready на grid-контейнеры (снятие FOUC-защиты).
  11. Поднимает наблюдателя за 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
счётчик больше 64a99999b
имя длиннее 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> нужны только два файла, и оба из пакета 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.jsdeferсинхронно в <head> он всё равно ждёт DOMContentLoaded — выигрыша нет, а блокирующий путь длиннее. Панели вкладок до старта видны все (база без скрипта); если это заметно, ставьте hidden неактивным в разметке
griffincss-utils.jsdeferсинхронно он достраивает углы вложенных .gr-radius по мере разбора, с defer — одним проходом после; первый уровень и его прямые дети скруглены статикой в обоих случаях
griffinjs.jsdeferдо старта разметка живёт базой без скрипта — <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-рантайм присваивает их автоматически:

  1. Читает имена областей из базового data-gr-layout (без суффикса брейкпоинта).
  2. Собирает имена, уже проставленные детям явно, — они исключаются из раздачи.
  3. Оставшиеся свободные имена раздаёт детям без класса, в порядке первого появления в раскладке.
  4. Дубликаты невозможны: одно имя не выдаётся дважды, даже если оно занимает несколько рядов.
  5. Детям сверх числа областей класс не выдаётся.

Важно: авто-присвоение использует только базовый 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-правило ровно один раз:

/* Структура сгенерированного 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-* переназначаются по новой раскладке (классы, проставленные в разметке вручную, остаются на месте).