Как мы отказались от использования Styled-System для создания компонентов и изобрели собственный велосипед

Всем привет! Меня зовут Саша, я сооснователь и по совместительству главный разработчик в Quarkly. В этой заметке я хочу рассказать о том, как концепция атомарного CSS, которой мы придерживаемся, вкупе с недостатками функционала Styled-System (и Rebass, как частного случая использования этой библиотеки) сподвигли нас к созданию своего собственного инструмента, который мы назвали Atomize.
Небольшая преамбула. Наш проект Quarkly — это микс графического редактора (вроде Figma, Sketch) и конструктора сайтов (по типу Webflow) с добавлением функционала, присущего классическим IDE. Про Quarkly мы обязательно напишем отдельный пост, там есть про что рассказать и что показать, ну а сегодня речь пойдет про упомянутый выше Atomize.

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

Если мало времени, чтобы осилить весь пост сейчас, то более лаконично ознакомиться с Atomize можно у нас на GitHub.

А чтобы знакомство было приятнее, мы запускаем конкурс по сборке react-компонентов с использованием Atomize. Подробнее об этом в конце поста.

С чего всё началось


Начиная разрабатывать Quarkly, мы условились, что хотим дать нашему пользователю возможность верстать на компонентах, но без необходимости использовать отдельный CSS-файл. Чтобы код был максимально минималистичен, но сохранял все возможности CSS, в отличие от инлайновых стилей.

Задача не инновационная и, на первый взгляд, вполне решаемая с помощью Styled-System и Rebass. Но этой функциональности нам оказалось недостаточно, а кроме того мы столкнулись со следующими проблемами:

  • неудобная работа с брейкпоинтами;
  • отсутствие возможности писать стили на состояние hover, focus etc;
  • механизм работы с темами показался нам недостаточно гибким.


Что представляет собой Atomize (кратко)


image

Из ключевых особенностей Atomize мы можем выделить следующие:

  • возможность использования переменных из темы в составных css-свойствах;
  • поддержка hover и любых других псевдоклассов;
  • короткие алиасы на каждое свойство (как в emmet);
  • возможность указывать стили на конкретный брейкпоинт, сохраняя при этом читаемость разметки;
  • минималистичный интерфейс.


При этом у Atomize есть два основных предназначения:

  • создание компонентов с поддержкой атомарного CSS и тем;
  • создание виджетов для интерактивного редактирования в проекте Quarkly.


Atomize, инструкция по применению


Перед началом работы необходимо установить зависимости:

npm i react react-dom styled-components @quarkly/atomize @quarkly/theme


Atomize является оберткой вокруг styled-component и имеет похожий API. Достаточно вызвать метод с именем необходимого элемента:

import atomize from '@quarkly/atomize';
 
const MyBox = atomize.div();


На выходе мы получаем react компонент, способный принимать любые CSS в виде пропсов.
Для удобства использования была разработана система алиасов свойств. К примеру bgc === backgroundColor

ReactDOM.render(<MyBox bgc="red" />, root);


С полным списком свойств и алиасов можно ознакомиться здесь.

Также предусмотрен механизм наследования в React:

const MySuperComponent = ({ className }) => {
   // some logic here
   return <div className={className} />;
};
 
const MyWrappedComponent = atomize(MySuperComponent);


Работа с темами


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

Чтобы использовать переменные из темы, достаточно описать свойство в теме и обратиться к этому свойству, используя префикс "--".

Переменные можно использовать как в JSX:

import Theme from "@quarkly/theme";
 
const theme = {
   colors: {
       dark: "#04080C",
   },
};
export const MyComp = () => (
   <Theme>
       <Box bgc="--colors-dark" height="100px" width="100px" />
   </Theme>
);


(Цвет #04080C доступен через свойство --colors-dark)

Так и в самой теме:

import Theme from "@quarkly/theme";
 
const theme = {
   colors: {
       dark: "#04080C",
   },
   borders: {
       dark: "5px solid --colors-dark",
   },
};
export const MyComp = () => (
   <Theme>
       <Box border="--borders-dark" height="100px" width="100px" />
   </Theme>
);


(Мы переиспользовали переменную из цветов, подключив её в тему borders)

Для цветов в JSX-разметке предусмотрен упрощенный синтаксис:

import Theme from "@quarkly/theme";
 
const theme = {
   colors: {
       dark: "#04080C",
   },
};
export const MyComp = () => (
   <Theme>
       <Box bgc="--dark" height="100px" width="100px" />
   </Theme>
);


Для работы с медиа-выражениями в темах предусмотрен breakpoint.
К любому свойству можно добавить префикс в виде имени ключа breakpoint'а.

import Theme from "@quarkly/theme";
 
const theme = {
   breakpoints: {
       sm: [{ type: "max-width", value: 576 }],
       md: [{ type: "max-width", value: 768 }],
       lg: [{ type: "max-width", value: 992 }],
   },
   colors: {
       dark: "#04080C",
   },
   borders: {
       dark: "5px solid --colors-dark",
   },
};
export const MyComp = () => (
   <Theme>
       <Box
           md-bgc="--dark"
           border="--borders-dark"
           height="100px"
           width="100px"
       />
   </Theme>
);


С исходным кодом тем можно ознакомиться здесь.

Эффекты


Основным отличием Atomize от Styled-System являются «effects». Что это и зачем это нужно?
Давайте представим, что вы создаете компонент Button, меняете у него color и border, но как назначить стили на hover, focus etc? Тут на помощь приходят эффекты.

При создании компонента достаточно передать объект с конфигурацией:

const MySuperButton = atomize.button({
 effects: {
   hover: ":hover",
   focus: ":focus",
   active: ":active",
   disabled: ":disabled",
 },
});


Ключом является префикс в имени пропса, а значением — CSS-селектор. Таким образом мы закрыли потребность во всех псевдо-классах.

Теперь если мы укажем префикс hover к любому CSS-свойству, то оно будет применено при определенном эффекте. Например, при наведении курсора:

ReactDOM.render(<MySuperButton hover-bgc="blue" />, root);


Также эффекты можно сочетать с медиа-выражениями:

ReactDOM.render(<MySuperButton md-hover-bgc="blue" />, root);


Несколько примеров


Чтобы визуализировать информацию выше, давайте теперь соберем какой-нибудь интересный компонент. Мы приготовили два примера:
Во втором примере мы задействовали большую часть функционала, а также внешний API.

Но это не всё


Второе предназначение Atomize, как вы упомянули выше, это создание виджетов в Quarkly на основе пользовательских react-компонентов.

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

export default atomize(PokemonCard)(
 {
   name: "PokeCard",
   effects: {
     hover: ":hover",
   },
   description: {
     // past here description for your component
     en: "PokeCard — my awesome component",
   },
   propInfo: {
     // past here props description for your component
     name: {
       control: "input",
     },
   },
 },
 { name: "pikachu" }
);


Поля конфигурации для компонента выглядят так:

  • effects — определяет браузерные псевдоклассы (hover, focus, etc);
  • description — описание компонента, которое будет появляться при наведении курсора на его название;
  • propInfo — конфигурация контролов, которые будут отображаться в правой панели (вкладка props).


Как определить пропсы, которые будут выводиться на правой панели (вкладка props):

propInfo: {
   yourCustomProps: { // имя свойства
       description: { en: "test" }, // описание с учетом локализации
       control: "input" // тип контрола
   }
}


Возможные варианты контролов:

  • input,
  • select,
  • color,
  • font,
  • shadow,
  • transition,
  • transform,
  • filter,
  • background,
  • checkbox-icon,
  • radio-icon,
  • radio-group,
  • checkbox.


Ещё один пример. Здесь мы добавили свой компонент в систему и теперь можем редактировать его интерактивно:


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

А теперь конкурс!


Дабы слегка подогреть интерес сообщества к более тесному знакомству с Atomize, мы решили пойти по простому и понятному (как и сам Atomize) пути — мы запускаем конкурс!

Вся информация о сроках, правилах и призах доступна на официальном сайте конкурса.

Если коротко: для участия и победы необходимо придумать (или найти готовый) интересный и полезный компонент на React и адаптировать его под требования Atomize. Мы выберем и наградим победителей сразу в нескольких номинациях. Дополнительные призы от нашей команды в случае добавления вашего компонента в Quarkly гарантированы.

© Habrahabr.ru