Как создать компонент вкладок

Базовый обзор того, как создать компонент вкладок, аналогичный тем, которые есть в приложениях для iOS и Android.

В этом посте я хочу поделиться мыслями о создании компонента Tabs для Интернета, который является адаптивным, поддерживает ввод с нескольких устройств и работает в разных браузерах. Попробуйте демоверсию.

Демоверсия

Если вы предпочитаете видео, посмотрите ролик на YouTube:

Обзор

Вкладки – это распространенный компонент систем проектирования, но они могут принимать разные формы. Сначала были вкладки для компьютеров, созданные на основе элемента <frame>, а теперь у нас есть мобильные компоненты, которые анимируют контент на основе физических свойств. Все они пытаются сделать одно и то же: сэкономить место.

Сегодня основными элементами вкладок являются кнопки навигации, которые переключают видимость контента в окне отображения. Многие различные области контента используют одно и то же пространство, но условно представлены на основе кнопки, выбранной в навигации.

коллаж довольно хаотичен из-за огромного разнообразия стилей, которые веб применил к концепции компонента
Коллаж из стилей веб-дизайна для компонента вкладок за последние 10 лет

Тактики для сайтов

В целом, я считаю, что этот компонент довольно прост в сборке благодаря нескольким важным функциям веб-платформы:

  • scroll-snap-points для удобного пролистывания и взаимодействия с клавиатурой с подходящими позициями остановки прокрутки.
  • Ссылки на контент с помощью хешей URL для поддержки привязки прокрутки на странице и обмена данными в браузере.
  • Поддержка программ чтения с экрана с разметкой элементов <a> и id="#hash".
  • prefers-reduced-motion для включения перекрестных переходов и мгновенной прокрутки на странице
  • Черновик веб-функции @scroll-timeline для динамического подчеркивания и изменения цвета выбранной вкладки

HTML

По сути, UX здесь таков: нажмите на ссылку, URL-адрес будет представлять состояние вложенной страницы, а затем посмотрите, как область контента обновляется по мере прокрутки браузера к соответствующему элементу.

В нем есть несколько структурных элементов контента: ссылки и :target. Нам нужен список ссылок, для которого отлично подходит <nav>, и список элементов <article>, для которого отлично подходит <section>. Каждый хеш ссылки будет соответствовать разделу, позволяя браузеру прокручивать страницу с помощью привязки.

При нажатии на кнопку-ссылку появляется контент, на котором фокусируется внимание пользователя.

Например, при нажатии на ссылку фокус автоматически переходит на статью :target в Chrome 89 без использования JS. После этого пользователь может прокручивать статью с помощью устройства ввода, как обычно. Это дополнительный контент, как указано в разметке.

Для организации вкладок я использовал следующую разметку:

<snap-tabs>
  <header>
    <nav>
      <a></a>
      <a></a>
      <a></a>
      <a></a>
    </nav>
  </header>
  <section>
    <article></article>
    <article></article>
    <article></article>
    <article></article>
  </section>
</snap-tabs>

Я могу установить связь между элементами <a> и <article> с помощью свойств href и id следующим образом:

<snap-tabs>
  <header>
    <nav>
      <a href="#responsive"></a>
      <a href="#accessible"></a>
      <a href="#overscroll"></a>
      <a href="#more"></a>
    </nav>
  </header>
  <section>
    <article id="responsive"></article>
    <article id="accessible"></article>
    <article id="overscroll"></article>
    <article id="more"></article>
  </section>
</snap-tabs>

Затем я заполнил статьи разным количеством текста, а ссылки – заголовками разной длины и наборами изображений. Имея контент для работы, мы можем начать макет.

Прокручиваемые макеты

В этом компоненте есть три типа областей прокрутки:

  • Панель навигации (розовая) прокручивается по горизонтали.
  • Область контента (синяя) прокручивается по горизонтали.
  • Каждую статью (зеленый) можно прокручивать по вертикали.
Три цветных поля со стрелками, которые указывают на области прокрутки и направление прокрутки.

При прокрутке задействованы два типа элементов:

  1. Окно
    Окно с заданными размерами, у которого есть свойство overflow.
  2. Слишком большая область.
    В этом макете это контейнеры списков: навигационные ссылки, статьи в разделах и содержимое статей.

Макет "<snap-tabs>"

В качестве макета верхнего уровня я выбрал flex (Flexbox). Я задаю для свойства direction значение column, чтобы заголовок и раздел располагались вертикально. Это наше первое окно прокрутки, и оно скрывает все, что выходит за его пределы. Скоро в заголовке и разделе будет использоваться прокрутка с выходом за границы, как в отдельных зонах.

HTML
<snap-tabs>
  <header></header>
  <section></section>
</snap-tabs>
CSS
  snap-tabs {
  display: flex;
  flex-direction: column;

  /* establish primary containing box */
  overflow: hidden;
  position: relative;

  & > section {
    /* be pushy about consuming all space */
    block-size: 100%;
  }

  & > header {
    /* defend against 
needing 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }

Указывает на разноцветную диаграмму с тремя свитками:

  • <header> теперь готов стать (розовым) контейнером прокрутки.
  • <section> готов стать контейнером прокрутки (синий).

Фреймы, которые я выделил ниже с помощью VisBug, помогают нам увидеть окна, созданные контейнерами прокрутки.

Элементы header и section выделены ярко-розовым цветом, чтобы показать, сколько места они занимают в компоненте.

Раскладка вкладок <header>

Следующий макет почти такой же: я использую flex, чтобы создать вертикальный порядок.

HTML
<snap-tabs>
  <header>
    <nav></nav>
    <span class="snap-indicator"></span>
  </header>
  <section></section>
</snap-tabs>
CSS
header {
  display: flex;
  flex-direction: column;
}

.snap-indicator должен перемещаться по горизонтали вместе с группой ссылок, и этот макет заголовка помогает создать такую структуру. Здесь нет элементов с абсолютным позиционированием.

Элементы nav и span.indicator имеют ярко-розовые наложения, обозначающие пространство, которое они занимают в компоненте.

Далее, стили прокрутки. Оказывается, мы можем использовать одни и те же стили прокрутки для двух горизонтальных областей прокрутки (заголовка и раздела), поэтому я создал служебный класс .scroll-snap-x.

.scroll-snap-x {
  /* browser decide if x is ok to scroll and show bars on, y hidden */
  overflow: auto hidden;
  /* prevent scroll chaining on x scroll */
  overscroll-behavior-x: contain;
  /* scrolling should snap children on x */
  scroll-snap-type: x mandatory;

  @media (hover: none) {
    scrollbar-width: none;

    &::-webkit-scrollbar {
      width: 0;
      height: 0;
    }
  }
}

Для каждого из них нужно настроить переполнение по оси X, ограничение прокрутки, чтобы предотвратить эффект достижения конца прокрутки, скрытые полосы прокрутки для сенсорных устройств и привязку прокрутки для фиксации областей представления контента. Порядок перехода между вкладками с помощью клавиатуры удобен, а фокус перемещается естественным образом. Контейнеры с привязкой прокрутки также получают удобное взаимодействие в стиле карусели с клавиатуры.

Макет заголовка вкладок <nav>

Ссылки навигации должны быть расположены в одну строку, без переносов строк, по вертикали. по центру, и каждый элемент ссылки должен привязываться к контейнеру привязки прокрутки. Отличная работа в 2021 году!

HTML
<nav>
  <a></a>
  <a></a>
  <a></a>
  <a></a>
</nav>
CSS
  nav {
  display: flex;

  & a {
    scroll-snap-align: start;

    display: inline-flex;
    align-items: center;
    white-space: nowrap;
  }
}

Каждая ссылка имеет свой стиль и размер, поэтому в макете навигации нужно указать только направление и поток. Уникальная ширина элементов навигации делает переход между вкладками интересным, поскольку индикатор подстраивает свою ширину под новую цель. В зависимости от количества элементов в этом контейнере браузер будет отображать полосу прокрутки или нет.

Элементы &quot;a&quot; в элементе &quot;nav&quot; выделены ярко-розовым цветом, чтобы показать, сколько места они занимают в компоненте и где они выходят за его пределы.

Раскладка вкладок <section>

Этот раздел является гибким элементом и должен занимать большую часть пространства. Также необходимо создать столбцы, в которых будут размещаться статьи. Ещё раз спасибо за отличную работу в 2021 году! block-size: 100% растягивает этот элемент, чтобы максимально заполнить родительский элемент, а затем создает серию столбцов, ширина которых составляет 100% от ширины родительского элемента. Проценты здесь отлично работают, потому что мы написали строгие ограничения для родителя.

HTML
<section>
  <article></article>
  <article></article>
  <article></article>
  <article></article>
</section>
CSS
  section {
  block-size: 100%;

  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
}

Это означает, что мы хотим максимально увеличить высоту, но заголовок с параметром flex-shrink: 0 не позволяет этого сделать. Таким образом, мы задаем высоту строки для набора столбцов полной высоты. Стиль auto-flow указывает сетке всегда размещать дочерние элементы в горизонтальную линию без переноса, что нам и нужно, чтобы они выходили за пределы родительского окна.

Элементы статьи выделены ярко-розовым цветом, чтобы показать, сколько места они занимают в компоненте и где они выходят за его пределы.

Иногда мне сложно в них разобраться. Этот элемент раздела вписывается в блок, но также создает набор блоков. Надеюсь, визуальные материалы и объяснения помогают.

Раскладка вкладок <article>

Пользователь должен иметь возможность прокручивать контент статьи, а полосы прокрутки должны появляться только в том случае, если контент не помещается на экране. Элементы статьи расположены аккуратно. Они одновременно являются родительским и дочерним элементом прокрутки. Браузер действительно обрабатывает некоторые сложные взаимодействия с сенсорным экраном, мышью и клавиатурой для нас здесь.

HTML
<article>
  <h2></h2>
  <p></p>
  <p></p>
  <h2></h2>
  <p></p>
  <p></p>
  ...
</article>
CSS
article {
  scroll-snap-align: start;

  overflow-y: auto;
  overscroll-behavior-y: contain;
}

Я выбрал привязку статей к родительскому скроллеру. Мне очень нравится, как элементы навигационных ссылок и элементы статьи привязываются к началу строки соответствующих контейнеров прокрутки. Это выглядит и ощущается как гармоничные отношения.

Элемент article и его дочерние элементы имеют ярко-розовые наложения, очерчивающие пространство, которое они занимают в компоненте, и направление, в котором они переполняются.

Статья является дочерним элементом сетки, и ее размер предопределен как область области просмотра, для которой мы хотим обеспечить прокрутку UX. Это значит, что мне не нужно указывать здесь высоту или ширину, а только то, как будет вести себя переполнение. Я устанавливаю overflow-y в auto, а затем также перехватываю взаимодействия прокрутки с помощью удобного свойства overscroll-behavior.

3 scroll areas recap

Ниже я выбрал в настройках системы "всегда показывать полосы прокрутки". Я думаю, что вдвойне важно, чтобы макет работал с включенной настройкой, так как мне нужно проверить макет и оркестровку прокрутки.

Теперь отображаются три полосы прокрутки, которые занимают место в макете, но наш компонент по-прежнему выглядит отлично.

Я думаю, что просмотр желоба полосы прокрутки в этом компоненте помогает четко показать, где находятся области прокрутки, направление, которое они поддерживают, и то, как они взаимодействуют друг с другом. Подумайте, как каждый из этих фреймов окна прокрутки также является родительским элементом flex или grid для макета.

Инструменты разработчика помогают нам визуализировать это:

Области прокрутки имеют наложения инструментов сетки и Flexbox, очерчивающие пространство, которое они занимают в компоненте, и направление, в котором они переполняются.
Инструменты разработчика Chromium, показывающие макет элемента навигации flexbox, заполненный элементами привязки, макет раздела сетки, заполненный элементами статьи, и элементы статьи, заполненные абзацами и элементом заголовка.

Макеты прокрутки готовы: они поддерживают привязку, ссылки на контент и управление с клавиатуры. Надежная основа для улучшения UX, стиля и удовольствия.

Подробнее о функции

Дочерние элементы с привязкой прокрутки сохраняют свое заблокированное положение при изменении размера. Это означает, что JavaScript не нужно будет ничего выводить на экран при повороте устройства или изменении размера браузера. Попробуйте сделать это в режиме эмуляции мобильных устройств в инструментах разработчика Chromium. Выберите любой режим, кроме Адаптивный, и измените размер рамки устройства. Обратите внимание, что элемент остается видимым и заблокированным вместе со своим контентом. Эта возможность доступна с тех пор, как в Chromium была обновлена реализация в соответствии со спецификацией. Подробнее об этом рассказывается в записи блога.

Анимация

Цель анимации – наглядно связать действия пользователя с реакцией интерфейса. Это помогает пользователю легко найти весь контент. Я буду добавлять эффект движения с целью и условно. Теперь пользователи могут указать свои предпочтения в отношении движения в своей операционной системе, и мне очень нравится реагировать на их предпочтения в своих интерфейсах.

Я свяжу подчеркивание вкладки с позицией прокрутки статьи. Привязка – это не только красивое выравнивание, но и закрепление начала и конца анимации. Это позволяет сохранить связь между <nav>, которая действует как мини-карта, и контентом. Мы будем проверять настройки движения пользователя как в CSS, так и в JS. Есть несколько отличных мест, где можно проявить заботу!

Поведение прокрутки

Можно улучшить анимацию :target и element.scrollIntoView(). По умолчанию они включены. Браузер просто устанавливает позицию прокрутки. Что, если мы хотим перейти к этой позиции прокрутки, а не мигать там?

@media (prefers-reduced-motion: no-preference) {
  .scroll-snap-x {
    scroll-behavior: smooth;
  }
}

Поскольку мы вводим здесь движение, и движение, которое пользователь не контролирует (например, прокрутку), мы применяем этот стиль только в том случае, если у пользователя нет предпочтений в его операционной системе в отношении уменьшения движения. Таким образом, мы вводим движение прокрутки только для тех, кто не против.

Индикатор вкладок

Цель этой анимации – помочь связать индикатор с состоянием контента. Я решил использовать перекрестное затухание цвета border-bottom для пользователей, которые предпочитают уменьшенное движение, и анимацию скольжения, связанную с прокруткой, и затухание цвета для пользователей, которые не против движения.

В инструментах разработчика Chromium я могу переключить настройки и продемонстрировать два разных стиля перехода. Мне было очень весело его строить.

@media (prefers-reduced-motion: reduce) {
  snap-tabs > header a {
    border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
    transition: color .7s ease, border-color .5s ease;

    &:is(:target,:active,[active]) {
      color: var(--text-active-color);
      border-block-end-color: hsl(var(--accent));
    }
  }

  snap-tabs .snap-indicator {
    visibility: hidden;
  }
}

Я скрываю .snap-indicator, когда пользователь предпочитает уменьшенное движение, так как оно мне больше не нужно. Затем я заменяю его на border-block-end стилей и transition. Также обратите внимание на взаимодействие вкладок, что активный элемент навигации не только имеет фирменное подчеркивание, но и цвет его текста также темнее. Активный элемент имеет более высокий контраст цвета текста и яркий акцент подсветки.

Всего несколько дополнительных строк CSS позволят пользователю почувствовать, что его заметили (в том смысле, что мы вдумчиво уважаем его предпочтения в отношении движения). Мне нравится.

@scroll-timeline

В предыдущем разделе я показал, как я обрабатываю стили перекрестного затухания с уменьшенным движением, а в этом разделе я покажу, как я связал индикатор и область прокрутки вместе. Дальше у нас кое-что интересное. Надеюсь, вы рады не меньше меня.

const { matches:motionOK } = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
);

Сначала я проверяю настройки движения пользователя в JavaScript. Если результат этого равен false, что означает, что пользователь предпочитает уменьшенное движение, мы не будем запускать какие-либо эффекты движения, связанные с прокруткой.

if (motionOK) {
  // motion based animation code
}

На момент написания этой статьи браузеры не поддерживают @scroll-timeline. Это черновик спецификации, который пока реализован только в экспериментальном режиме. Однако у него есть полифил, который я использую в этой демонстрации.

ScrollTimeline

Хотя временные шкалы прокрутки можно создавать как с помощью CSS, так и с помощью JavaScript, я выбрал JavaScript, чтобы использовать в анимации измерения элементов в реальном времени.

const sectionScrollTimeline = new ScrollTimeline({
  scrollSource: tabsection,  // snap-tabs > section
  orientation: 'inline',     // scroll in the direction letters flow
  fill: 'both',              // bi-directional linking
});

Я хочу, чтобы один элемент следовал за позицией прокрутки другого, и, создавая ScrollTimeline, я определяю драйвер ссылки прокрутки, scrollSource. Обычно анимация в интернете выполняется в соответствии с глобальным временным интервалом, но с помощью пользовательского sectionScrollTimeline в памяти я могу все это изменить.

tabindicator.animate({
    transform: ...,
    width: ...,
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

Прежде чем перейти к ключевым кадрам анимации, важно отметить, что элемент, следующий за прокруткой, tabindicator, будет анимирован на основе пользовательской временной шкалы, то есть прокрутки нашего раздела. Это завершает связь, но отсутствует последний ингредиент, точки с сохранением состояния, между которыми можно анимировать, также известные как ключевые кадры.

Динамические ключевые кадры

Есть очень мощный декларативный способ анимации с помощью @scroll-timeline, но выбранная мной анимация была слишком динамичной. Невозможно перейти между шириной auto и динамически создать количество ключевых кадров на основе длины дочерних элементов.

Однако JavaScript может получить эту информацию, поэтому мы сами переберем дочерние элементы и получим вычисленные значения во время выполнения:

tabindicator.animate({
    transform: [...tabnavitems].map(({offsetLeft}) =>
      `translateX(${offsetLeft}px)`),
    width: [...tabnavitems].map(({offsetWidth}) =>
      `${offsetWidth}px`)
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

Для каждого tabnavitem деструктурируйте позицию offsetLeft и верните строку, которая использует ее в качестве значения translateX. Будет создано четыре ключевых кадра трансформации для анимации. То же самое делается для ширины: у каждого элемента запрашивается его динамическая ширина, которая затем используется в качестве значения ключевого кадра.

Вот пример вывода, основанный на моих шрифтах и настройках браузера:

Ключевые кадры TranslateX:

[...tabnavitems].map(({offsetLeft}) =>
  `translateX(${offsetLeft}px)`)

// results in 4 array items, which represent 4 keyframe states
// ["translateX(0px)", "translateX(121px)", "translateX(238px)", "translateX(464px)"]

Ключевые кадры ширины:

[...tabnavitems].map(({offsetWidth}) =>
  `${offsetWidth}px`)

// results in 4 array items, which represent 4 keyframe states
// ["121px", "117px", "226px", "67px"]

Подводя итог, можно сказать, что индикатор вкладки теперь будет анимироваться по четырем ключевым кадрам в зависимости от положения привязки прокрутки раздела. Точки привязки создают четкое разграничение между нашими ключевыми кадрами и действительно добавляют ощущение синхронизированности анимации.

Активная и неактивная вкладки показаны с оверлеями VisBug, которые демонстрируют, что контрастность обеих вкладок соответствует требованиям.

Пользователь управляет анимацией, наблюдая, как ширина и положение индикатора меняются от одного раздела к другому, идеально отслеживая прокрутку.

Возможно, вы не заметили, но я очень горжусь переходом цвета, когда выделенный элемент навигации становится выбранным.

Невыбранный светло-серый цвет кажется ещё более отодвинутым назад, когда выделенный элемент имеет больший контраст. Обычно цвет текста меняется, например, при наведении курсора и при выборе, но переход этого цвета при прокрутке, синхронизированный с индикатором подчеркивания, – это следующий уровень.

Вот как я это сделал:

tabnavitems.forEach(navitem => {
  navitem.animate({
      color: [...tabnavitems].map(item =>
        item === navitem
          ? `var(--text-active-color)`
          : `var(--text-color)`)
    }, {
      duration: 1000,
      fill: 'both',
      timeline: sectionScrollTimeline,
    }
  );
});

Каждой ссылке на вкладку нужна эта новая анимация цвета, отслеживающая ту же временную шкалу прокрутки, что и индикатор подчеркивания. Я использую ту же временную шкалу, что и раньше: поскольку ее роль заключается в том, чтобы генерировать тик при прокрутке, мы можем использовать этот тик в любом типе анимации, который захотим. Как и раньше, я создаю 4 ключевых кадра в цикле и возвращаю цвета.

[...tabnavitems].map(item =>
  item === navitem
    ? `var(--text-active-color)`
    : `var(--text-color)`)

// results in 4 array items, which represent 4 keyframe states
// [
  "var(--text-active-color)",
  "var(--text-color)",
  "var(--text-color)",
  "var(--text-color)",
]

Ключевой кадр с цветом var(--text-active-color) выделяет ссылку, а в остальном это стандартный цвет текста. Вложенный цикл делает это относительно простым, поскольку внешний цикл представляет собой каждый элемент навигации, а внутренний цикл – личные ключевые кадры каждого элемента навигации. Я проверяю, совпадает ли элемент внешнего цикла с элементом внутреннего цикла, и использую это, чтобы узнать, когда он выбран.

Мне было очень весело писать это. Очень.

Дополнительные улучшения JavaScript

Стоит напомнить, что основа того, что я вам здесь показываю, работает без JavaScript. Теперь давайте посмотрим, как можно улучшить его, когда JS доступен.

Ссылки на контент – это скорее мобильный термин, но я думаю, что в данном случае вкладки выполняют ту же функцию, поскольку вы можете поделиться URL, ведущим непосредственно на контент вкладки. Браузер перейдет к идентификатору, который совпадает с хешем URL. Я обнаружил, что этот обработчик onload создавал эффект на разных платформах.

window.onload = () => {
  if (location.hash) {
    tabsection.scrollLeft = document
      .querySelector(location.hash)
      .offsetLeft;
  }
}

Синхронизация конца прокрутки

Наши пользователи не всегда нажимают или используют клавиатуру, иногда они просто свободно прокручивают, как и должно быть. Когда прокрутка раздела останавливается, то, где она приземляется, должно совпадать с верхней панелью навигации.

Вот как я жду окончания прокрутки: js tabsection.addEventListener('scroll', () => { clearTimeout(tabsection.scrollEndTimer); tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100); });

При прокрутке разделов очистите время ожидания раздела, если оно есть, и запустите новое. Когда прокрутка разделов прекращается, не очищайте время ожидания и активируйте его через 100 мс после остановки. Когда он срабатывает, вызывается функция, которая определяет, где пользователь остановился.

const determineActiveTabSection = () => {
  const i = tabsection.scrollLeft / tabsection.clientWidth;
  const matchingNavItem = tabnavitems[i];

  matchingNavItem && setActiveTab(matchingNavItem);
};

Если привязка прокрутки сработала, то при делении текущей позиции прокрутки на ширину области прокрутки должно получиться целое число, а не десятичная дробь. Затем я пытаюсь получить элемент навигации из нашего кэша через этот вычисленный индекс, и если он что-то находит, я отправляю совпадение, которое должно быть установлено как активное.

const setActiveTab = tabbtn => {
  tabnav
    .querySelector(':scope a[active]')
    .removeAttribute('active');

  tabbtn.setAttribute('active', '');
  tabbtn.scrollIntoView();
};

Установка активной вкладки начинается с очистки любой активной в данный момент вкладки, а затем присвоения входящему элементу навигации атрибута активного состояния. Вызов scrollIntoView() интересно взаимодействует с CSS.

.scroll-snap-x {
  overflow: auto hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;

  @media (prefers-reduced-motion: no-preference) {
    scroll-behavior: smooth;
  }
}

В CSS-файле утилиты горизонтальной привязки прокрутки мы вложили запрос медиа, который применяет smooth прокрутку, если пользователь терпим к движению. JavaScript может свободно вызывать элементы прокрутки в поле зрения, а CSS может декларативно управлять UX. Иногда они составляют восхитительную пару.

Заключение

Теперь, когда вы знаете, как это сделала я, как бы вы это сделали?! Это делает архитектуру компонентов более интересной! Кто первым создаст версию со слотами в своем любимом фреймворке? 🙂

Давайте разнообразим наши подходы и изучим все способы создания веб-сайтов. Создайте Glitch, отправьте мне твит со своей версией, и я добавлю ее в раздел Ремиксы сообщества ниже.

Ремиксы сообщества