Pull to refresh

В поисках идеального css-фреймворка. Требования, реализация, maxmertkit

Reading time 14 min
Views 70K


Я обожаю twitter bootstrap. Прост, местами логичен, достаточно красив, подходит для быстрого прототипирования веб-интерфейсов. Но этого оказалось недостаточно. Взяв twitter bootstrap в большой проект, мне пришлось целиком его разобрать и переосмыслить css-фреймворки как боевые единицы в веб-проектах. В результате переосмысления родились требования к любому css-фреймворку, удобному как верстальщику, так и frontent-разработчику.


Требования к фреймворку


  1. Для классов фреймворка необходимо использовать неймспэйсы. Это позволяет видеть какой класс принадлежит фреймворку, а какой написали вы. К тому же избавляет от риска переопределения класса фреймворка, из-за которого все может сломаться.
  2. Все компоненты фреймворка, будь это кнопки или выпадающие меню, должны быть независимыми виджетами, при удалении которых фреймворк внешне не пострадает (я молчу про возможность компиляции).
  3. Для внешней модификации фреймворка должен быть отдельный файл с темой, изменив который вы получаете внешне совсем другой фреймворк.
  4. Нужно иметь возможность менять имена классов! Причем делать это в файле темы.
  5. Необходимо избавить frontend-разработчика от необходимости помнить тонну классов для стилизации кнопочек, менюшек, попапов. Здесь под стилизацией я понимаю применение к ним различных состояний и статусов, например error, loading, disabled и т.д.
  6. При добавлении в тему фреймворка какого-нибудь модификатора (расскажу о них позже), например, статуса, все подключеные виджеты должны уметь показывать этот новый статус без правки исходного кода этого виджета. Например добавляем в файл темы новый статус “deepspace”, который имеет цвет фона #000. После чего элементы абсолютно всех виджетов (табы, кнопки, попапы) при применении статуса “deepspace” становятся черными.
  7. При применении модификатора к виджету-родителю, все виджеты-дети наследуют этот модификатор. Например, если мы имеем группу, внутри которой кнопки и текстовые поля, и стоит задача сделать всю эту группу большой и поставить класс “disabled” всем элементам внутри группы. Для этого добавляем модификаторы (классы, например, big и disabled) не к внутренним элементам, а к группе.
  8. Все стандартные иконки – шрифтовые. Это решает очень много проблем.
  9. Необходимо иметь возможность быстро собирать новые виджеты с учетом вышеперечисленных требований.

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

Краткая история создания


Очень кратко. Я сделал три версии. Первая была сделана дня за три. Естественно, на чистом css такое не напишешь, поэтому для первой версии я выбрал LESS. Сахар оказался не достаточно сладок, и для второй версии я выбрал SASS. Оказалось он гораздо гибче и позволяет делать вещи, которые на LESS реализовать сложно, а порой просто нельзя. Вторую версию я делал около трех недель, но в силу того, что я только начал осваивать SASS, вместо вспаханного поля я получил кратер. Попытка переписать то, что уже есть, все только усугубила, поэтому я приступил к третьей версии, которую вы можете видеть сейчас. Названия я давать не умею, поэтому просто добавил kit к своему никнейму. Сайт – maxmert.com.

Maxmertkit. Структура файлов.


Все файлы ни в коем случае не должны лежать в одной большой куче. Это, в конце концов, не только не красиво, но и не практично.



Classes. Папка содержит все базовые классы, от которых наследуются все остальные виджеты. На данный момент он один – object.
Modificators. Содержит все возможные модификаторы, применяемые к виджетам. На данный момент это статус, размер и другое.
Widgets. Внутри все виджеты, от кнопки до галереи.
Animations. CSS-анимации для виджетов.
_init.scss. Инициализация типографики и базовых классов.
_mixins.scss. Примеси.
maxmertkit*.scss. Файлы для компиляции. По сути они просто подключают необходимые модули. Если вам что-то не нужно, просто закоментируйте импорт.
style.scss. Ваши стили.

Кредо.


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

Неймспейсы.

Для грамотного написания своих классов в фреймворке жизненно необходимы неймспейсы. Для чего? Я думаю частенько возникает желание назвать класс для бейджа badge, а для таблицы – table. Только все время приходится вспоминать все используемые во фреймворке классы. Не совсем практично. В maxmertkit такого недостатка нет.

class=”-{имяВиджета}” – все названия виджетов, например -table, -tooltip, -badge, -modal и т.д.
class=”-{имяСтатуса}-” – имя статуса, например, -error-, -warning-, -info-, -disabled-, -unstyled- и т.д.
class=”_{имяРазмера}” – имя размера, например, _tiny, _small, _big, _huge.
class=”_{именаДругихМодификаторов}_” – например, _loading_, _unclickable_, _active_ и т.д.

Что важно, для любого виджета используются одни и те же модификаторы. Например модификатор _loading_ можно поставить как кнопке, так и таблице или табу. Это позволяет не запоминать длинные составные классы для каждого из виджетов.

Рассмотрим рядовой пример. Есть кнопка, которая удаляет какой-либо контент. Имеется и несколько условий. Сразу после загрузки страницы мы должны проверить, может ли данный пользователь удалять этот контент или нет (например, хватает ли кармы). Причем если не может кнопку все-равно показывать, но со статусом disabled, если может, со статусом error (чтобы кнопка была красной). Пока задача ставится целиком для css. Поэтапно.

  1. Страница загружена, показываем disabled кнопку со статусом загрузки.


    <a class="-btn -disabled- _loading_ -error-">Button</a>


  2. Далее осуществляем проверку на на наличие кармы. Предположим пользователь не может удалять контент. Тогда просто удалим модификатор _loading_.


    <a class="-btn -disabled- -error-">Button</a>

  3. Если кармы все же хватает и пользователь может удалять контент, удаляем модификатор _loading_ и -disabled-.


    <a class="-btn -error-">Button</a>

  4. Если пользователь нажал на кнопку, добавляем модификатор _active_.


    <a class="-btn -error- _active_">Button</a>

  5. Когда началось обращение к серверу, добавим модификатор _loading_.

    <a class="-btn -error- _active_ _loading_">Button</a>


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

Наследование модификаторов

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

Это группа без модификаторов.

<div class="-group">
   <a class="-btn">I like it</a>
   <a class="-btn">
       <i class="-icon-thumbs-up"></i>
   </a>
</div>

Добавим статус -primary-.

<div class="-group -primary-">
   <a class="-btn">I like it</a>
   <a class="-btn">
       <i class="-icon-thumbs-up"></i>
   </a>
</div>

Добавим статус -dark- кнопке внутри группы.

<div class="-group -primary-">
   <a class="-btn">I like it</a>
   <a class="-btn -dark-">
       <i class="-icon-thumbs-up"></i>
   </a>
</div>

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

<div class="-group -primary- _huge">
   <a class="-btn">I like it</a>
   <a class="-btn -dark-">
       <i class="-icon-thumbs-up"></i>
   </a>
</div>


Промежуточные итоги

Итак, пока мы решили следующие задачи (нумерация соответствует списку в начале):
    1. Используем легко запоминающиеся неймспейсы.
    2. Используем независимые друг от друга виджеты, которые можно легко исключить из сборки.
    5. Нет необходимости помнить много составных классов для разных виджетов, потому что одни и те же модификаторы применяются ко всем виджетам.
    7. Модификаторы наследуются виджетами-детьми.
    8. Шрифтовые иконки.

Остаются пункты:
    4. Возможность легко менять имена виджетов и модификаторов.
    6. При изменении модификаторов (цвета, размера и т.д.), их удаления или добавления все изменения должны быть применены ко всем виджетам. То есть если мы добавили какой-нибудь модификатор статуса, то его можно сразу же применить к любому из виджетов без правки его кода.
    9. Легкая сборка новых виджетов с учетом вышеперечисленных требований.

Файл темы

Файл темы успешно решает задачи 4 и 6. Посмотрим каким образом. Он находится в папке themes. Название этого файла может быть любым, начинающимся с подчеркивания (требование SASS, файлы с такими названиями не компилируются отдельно), только не забудьте импортировать нужный файл темы в проект.

Имена переменных внутри файла темы образуются следующим образом:
${объектРодитель}__{данныйОбъект}-{свойство}__{составнаяЧасть}-{свойство}
Например,

$object__group
object – родитель (как я уже и говорил, пока что он один);
group – это объявляемый виджет.
$object__group__appendix
appendix – это составная часть, находящаяся внутри виджета group (это может быть header, content и вообще все, что угодно)
$object__group__appendix-border-width
Это объявление толщины границы текстового придатка внутри группы.
$object__dropdown__header-padding
Внутренний отступ заголовка выпадающего дропдауна.

Итак, как же объявляются имена виджетов? Очень просто. При объявлении виджета без его свойств всегда указывается название класса. Таким образом, если вам не нравятся названия классов, вы легко сможете поменять их в файле темы:

$object__modal: -modal;
$object__dropdown: -dropdown;
$object__tooltip: -tooltip;
$object__toolbar: -toolbar;
$object__progressbar: -progress;
$object__progressbar__bar: #{$object__progressbar}-bar;


Посмотрим на модификаторы. Для начала разберем модификаторы статуса.



Для каждого из модификаторов выделен отдельный столбец. Верхняя строка – это название. Удалив столбец с модификатором, например, error, мы получим вполне ожидаемый результат: модификатор error исчезнет и ни один из виджетов не будет изменяться при добавлении соответствующего класса -error-.
Кроме того вы можете поменять название, к примеру на error1, и теперь только добавив к виджету класс -error1- вы измените его статус.
Также вы можете добавить столбец со своим статусом и он сразу же будет работать со всеми виджетами.

Теперь о модификаторах размера.



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

Виджеты, имеющиеся в maxmertkit

На скриншотах далеко не все, что умеют эти виджеты. В сочетании с различными модификаторами они смотрятся совершенно по-другому, появляются десятки вариантов одного виджета. Сходите на maxmert.com, посмотрите примеры, полистайте доки.

  • Таблицы, -table
  • Формы -form
  • Иконки -icon
  • Социальные иконки -icon-social
  • Сетка -grid
  • Кнопка -btn
  • Group -group
  • Табы -tabs
  • Бейджи -badge
  • Метки -labels
  • Выпадающий контент -dropdown
  • Меню -menu
  • Меню в dropdown’е (каюсь за повтор, но я не удержался, больно нравится; меню в дропдауне выглядит немного по-другому)
  • Тултип -tooltip
  • Прогрессбар -progress
  • Уведомления -notify, используются совместно с javascript-плагинами
  • Окна -modal, используются совместно с javascript-плагинами

Полный список можно увидеть внутри папки widgets.

Яваскрипт

На данный момент я написал 8 плагинов.
  1. Popup. Для tooltip’ов и dropdown’ов.
  2. Tabs.
  3. Button. Для создания кнопок-чекбоксов и радоибаттонов.
  4. Modal.
  5. Affix.
  6. Scrollspy.
  7. Notify.
  8. Carousel.

Здесь сложно кого-то удивить. В доках все есть. Есть только несколько моментов, о которых я хотел рассказать.
  1. Во многих плагинах в качестве коллбэка beforeOpen, beforeAction, afterOpen и др. (все в доках) можно писать $.ajax (или использовать $.Deferred внутри коллбэка). До тех пор, пока данные не будут получены, плагин не будет предпринимать никаких дальнейших действий (можно получать, например, контент для dropdown’а). Это очень удобно и позволяет сосредоточиться на протекающих процессах, а не на способах их реализации.
  2. Помимо javascript анимации, реализована css анимация. Смотрится, на мой вкус, потрясающе.
  3. Все плагины отслеживают изменение параметров внутри себя. То есть, чтобы изменить тему, нужно просто передать плагину {theme:’dark’}, он сделает все остальное. Скажем “нет” постоянным вызовам методов.
  4. Во многих плагинах есть “бездна”, куда сбрасывается каждый инстанс плагина. То есть из экземпляра плагина у вас есть доступ к другим экземплярам этого же плагина. Чрезвычайно удобно, если, например, нужно закрывать все dropdown’ы, когда открывается текущий экземпляр.
  5. Есть еще что-то, о чем я наверняка забыл.

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

Создание нового виджета

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

Разберем на примере кнопки.
Новый виджет кладется в папку widgets, название файла начинается с подчеркивания (для предотвращения самостоятельной компиляции). Сначала пишутся все стили, не касающиеся модификаторов, уникальные для этого виджета.

button,
.#{$object__button},
input[type="button"]
{
        @extend %__object;
        text-decoration: none;
        cursor: pointer;
        border-width: $object__button-border-width;
        border-style: solid;
        @include border-radius( $object__button-border-radius );
        @include box-shadow( $object__button-shadow );
}

@extend %__object – наследование от класса объект, о котором многократно говорилось выше. Это дает возможность хорошо применять модификаторы для этого виджета.

Все переменные, конечно же, берутся из файла темы.

Далее самое интересное. Мы должны указать, что к виджету “кнопка” применимы определенные модификаторы. Причем когда один из модификаторов применяется к виджету, он не обязательно меняет в нем все от цвета текста до тени, он может поменять в нем только маленькую часть (например при добавлении модификатора -error- к кнопке поменять только цвет текста, а фон и все остальное не трогать). В нашем случае

$__inheritance: object;
$__before-object: '';
$__object: 'button' 'input[type="button"]' '.#{$object__button}';
$__after-object: '';
@include set_modificator($mod__status, color-invert, border-color, gradient-vertical, text-shadow);

$__inheritance: object – класс объекта-родителя (можно задать другой, но об этом немного позже)
$__before-object и $__after-object – о них чуть ниже (но их обязательно указывать даже пустые).
$__object – здесь в кавычках через пробелы или табы указываются объекты, к которым непосредственно будут применяться модификаторы (то есть на которые будут навешиваться классы модификаторов)
@ include set_modificator($mod__status, color-invert, border-color, gradient-vertical, text-shadow) – функция, устанавливающая для виджета модификаторы статуса $mod__status (это те самые столбцы из файла темы), причем при применении одного из модификаторов устанавливаются только инвертированный цвет текста, цвет границ, вертикальный градиент и тень для текста.

Попробуем изменять кнопку, когда пользователь наводит на нее курсор:

$__inheritance: object;
$__before-object: '';
$__object: append-list('button' 'input[type="button"]' '.#{$object__button}', '', ':hover');
$__after-object: '';
@include set_modificator($mod__status, gradient-vertical-darken);


Как вы заметили, здесь вместо обычного списка используется функция append-list, которая просто добавляет ‘:hover’ в конец каждого элемента списка. Мы могли бы не использовать ее, а просто написать

'button:hover' 'input[type="button"]:hover' '.#{$object__button}:hover'

Кроме того компоненты модификаторов, которые использовались ранее, больше не указываются, указываются только новые компоненты, или компоненты, которые переопределят старые (в нашем случае в кнопке с псевдоклассом :hover компонент gradient-vertical-darken переопределит старый gradient-vertical)

То же самое для :active

$__inheritance: object;
$__before-object: '';
$__object: append-list('button' 'input[type="button"]' '.#{$object__button}', '', ':active');
$__after-object: '';
@include set_modificator($mod__status, gradient-vertical-darkener);

Если вам лень писать список в $__object, можно написать вот так:

button,
.#{$object__button},
input[type="button"] {
       $__inheritance: object;
       $__before-object: '';
       $__object: this;
       $__after-object: '';
       @include set_modificator($mod__status, color-invert, border-color, gradient-vertical, text-shadow);
       
       &:hover {
                $__inheritance: object;
                $__before-object: '';
                $__object: this;
                $__after-object: '';
                @include set_modificator($mod__status, gradient-vertical-darken);
       }
       &:active {
                $__inheritance: object;
                $__before-object: '';
                $__object: this;
                $__after-object: '';
                @include set_modificator($mod__status, gradient-vertical-darkener);
       }
}

Вы, конечно, заметили, что блок

$__inheritance: object;
$__before-object: '';
$__object: this;
$__after-object: '';

многократно повторяется. Его можно написать один раз до объявления кнопки.
То же самое без лишнего кода:

$__inheritance: object;
$__before-object: '';
$__object: this;
$__after-object: '';
button,
.#{$object__button},
input[type="button"] {
        @include set_modificator($mod__status, color-invert, border-color, gradient-vertical, text-shadow);
        &:hover {
                @include set_modificator($mod__status, gradient-vertical-darken);
        }
        &:active {
                @include set_modificator($mod__status, gradient-vertical-darkener);
        }
}

А что же делать, если виджет кнопка лежит внутри другого виджета? Вот здесь нам пригодится $__before-object и $__after-object:

$__inheritance: object;
$__before-object: ‘’;
$__object: '.#{$object__group}';
$__after-object: 'button' 'input[type="button"]' '.#{$object__button}';
@include set_modificator($mod__status, color-invert, border-color, gradient-vertical, text-shadow);

Это значит, что модификаторы теперь будут применятся к группе, так как $__object теперь группа, но внешне будет изменяться все, что находится в $__after-object. То есть в результате компиляции мы получим что-то вроде

-group.-error- > button { ... }
-group.-error- > input[type="button"] { ... }
-group.-error- > .-btn { ... }
-group.-info- > button { ... }
-group.-info- > input[type="button"] { ... }
-group.-info- > .-btn { ... }
…
и так для всех статусов.

Если же мы будем использовать $__before-object,

$__inheritance: object;
$__before-object: ‘.#{$object__menu}’;
$__object: '.#{$object__group}';
$__after-object: 'button' 'input[type="button"]' '.#{$object__button}';
@include set_modificator($mod__status, color-invert, border-color, gradient-vertical, text-shadow);

то получим нечто похожее на
.-menu > -group.-error- > button { ... }
.-menu > -group.-error- > input[type="button"] { ... }
.-menu > -group.-error- > .-btn { ... }
.-menu > -group.-info- > button { ... }
.-menu > -group.-info- > input[type="button"] { ... }
.-menu > -group.-info- > .-btn { ... }
…
и так для всех статусов.

Последний не рассмотренный вопрос: что делать, если для данного элемента применимы не все модификаторы (то есть нам нужно исключить из списка $mod__status некоторые из них)?

$__inheritance: $object__group-important;
$__before-object: '.#{$object__group}';
$__object: 'button.#{$mod__loading}' 'input[type="button"].#{$mod__loading}' '.#{$object__button}.#{$mod__loading}';
$__after-object: '';
@include set_modificator(exclude-items($mod__status,$mod__status__disabled, default), loading);

@ include set_modificator(exclude-items($mod__status,$mod__status__disabled, default), loading) – исключает из списка $mod__status статусы disabled и default. То есть когда вы поставите модификатор loading отдельной кнопке внутри группы, он не будет применяться, если у кнопки нет никакого статуса или если статус disabled.

Аналогично работает only-items
@ include set_modificator(only-items($mod__status,$mod__status__disabled, default), loading-dark) – оставляет из списка $mod__status только указанные статусы.

Компоненты, доступные для $mod__status:
  • color
  • color-darken
  • color-invert
  • color-important
  • color-invert-important
  • text-shadow
  • border-color
  • border-color-darken
  • border-color-darkener
  • border-color-lighten
  • border-color-lightener
  • border-color-important
  • background-color
  • background-color-lighten
  • background-color-lightener
  • background-color-darken
  • background-color-darkener
  • gradient-vertical
  • gradient-vertical-darken
  • gradient-vertical-darkener
  • gradient-vertical-three
  • gradient-horizontal-three
  • shadow
  • outline
  • loading
  • loading-dark

Это значит что любой из них вы можете использовать следующим образом
set_modificator($mod__status, {имяКомпонента}, {имяКомпонента}, … )

Компоненты, доступные для $mod__size:
  • line-height
  • line-height-small
  • input-line-height
  • font-size
  • font-size-small
  • padding
  • padding-small
  • padding-big
  • padding-huge
  • input-padding

Используется так
set_modificator($mod__size, {имяКомпонента}, {имяКомпонента}, … )

Даже если вы укажете компонент padding, размер объекта будет меняться в зависимости от модификатора размера, но бывают случаи, когда padding меняться должен, но он должен быть маленьким или большим и меняться незначительно. Именно поэтому появляются компоненты padding-small и padding-big.

В качестве послесловия


Сайт – maxmert.com
Github – https://github.com/maxmert/maxmertkit

Мой английский без практики стал совсем русским, поэтому на сайте вы можете встретить ошибки, неправильные обороты или времена. Очень прошу помочь мне и сообщить, если вы увидите такую бяку. Для этого просто выделяете кусок текста и нажимаете на кнопку «Report an error» в левом плавающем меню. Заранее благодарен.

Я достаточно долго тестировал maxmertkit, но все же если вы заметите баги в верстке, напишите issue в github'е.

Благодарю читателей и всех, кто будет использовать этот фреймворк. Надеюсь облегчить нелегкую работу frontend-разработчиков.

Обновление:
  • Выкачена версия 0.0.2. Исправлены основные ошибки, спасибо всем, кто принял участие. Я продолжаю работу в этом направлении :)
  • Добавлена адаптивная верстка. Теперь достаточно применить модификатор _responsive_ к блоку, внутри которого она должна быть (например к body).


Жду предложений, например, какие виджеты вы бы еще хотели? Ну и, конечно, информацию об обнаруженных ошибках в github'e.


Обновлен до версии 1.0.3!
Tags:
Hubs:
+190
Comments 135
Comments Comments 135

Articles