Мотивация

Как вырваться из замкнутого круга навязываемых акулами BigTech сомнительных технологий на простор устойчивого развития? Как сделать так, чтобы код внедряемой сейчас Дизайн‑системы — через пару лет опять не превратился в очередную, никому не нужную и максимально не удобную, «тыкву‑легаси»? Как выйти из дурного холивара с модными фреймворками на фронтенде и сделать библиотеку переиспользуемых компонент подходящую сразу для всех технологий и «на века»?)))

Ваша команда работает с большим количеством различных недокументированных проектов, основанном на одном визуальном языке? У вашей корпорации есть строгий брендбук и огромное количество клиентских веб‑сервисов написанных на разных фреймворках, которые по факту выглядят немного по‑разному в «одних и тех же мелочах»? Ваши фронты «пишут каждый свой фреймворк для дизайна заново каждый раз на каждом проекте»? Знакомо? UUI спешит к вам на помощь!

Мода на технологии все стремительней меняется, а браузер и простой интерфейс в нем остается. Вышел новый модный фреймворк и заказчику хочется проект именно на нем? Да пожалуйста! Легко! Так мы становимся максимально независимы от капризной и накаченной лоббированием индустрии реализуя основную задачу. Поставляя единый гайдлайн повсюду.

Эта статья уже год как должна быть написана и опубликована, но водка зло. Она продолжает одну из двух тем в этом блоге: создание гибких и эффективных пользовательских атомарных библиотек, Дизайн‑систем для веб‑приложений.

Репозитории написанные для статьи:

Дизайн‑система с документацией: https://gitlab.com/ushliypakostnik/uui‑library

Пример на Vue: https://gitlab.com/ushliypakostnik/vue‑example

Пример на React: https://gitlab.com/ushliypakostnik/react‑example

Пример на Angular: https://gitlab.com/ushliypakostnik/angular‑example

Что такое UUI?

Да, это именно и специально минимальный стартовый пример. Распиленный ровно только настолько, насколько нужно для того, чтобы дать хорошее начало вашей собственной уникальной и универсальной Дизайн‑системе. Минимально. Кому‑то обязательно захочется переименовать, например, даже префикс элементов, на свой, «фирменный»: <ivanov-ivan-text>. Все сделано для того, чтобы кастомизация была быстрой и совсем не сложной. Всего четыре цвета, четыре компонента…

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

Основной концептуальный паттерн в данном контексте это «атомарный дизайн». И это должно понравится, прежде всего, вашим дизайнерам, если они двигаются по дизайну системно. Они должны предоставить вам гайдлайн: «наборы допустимых значений для основных качеств». И графические прототипы использующие только эти токены и правила гайдлайна. Выражающие все необходимые компоненты и их поведение.

Ожидание / реальность

Большинство хотя бы минимально профпригодных разработчиков клиентской части веб‑приложений считают, что они «умеют верстать, но не хотят», хотя, в результате, это как раз и получается что не умеют. Кроме того, серое большинство, еще и всячески максимально избегает любой коммуникации (коммуникация «для виду», «вежливыми отмазками» не считается). Избегают общих соглашений, взаимодействия с коллегами, другими частями проекта, чужой работой. Сижу у себя в задачах и ветках, модулях и чего‑то там по своему, как мне хочется и как уже умею тихо сочиняю.

Реальная практика показывает, что в крупных организациях внедрение единого модуля, предоставляющего единый визуальный язык, является крайне тяжелой задачей сопряжённой с множеством сложностей, не только организационного, но также когнитивного и психологического характера. Хотя это и вполне возможно. Главное технически сразу начать делать это хорошо и правильно — об этом эта статья. Введение такого модуля, с визуальными слоями и виджетами ПОЛНОСТЬЮ ОТДЕЛЕННЫМИ ОТ БИЗНЕС‑ЛОГИКИ конечных проектов — понятным и легальным образом улучшает коммуникацию, и должно в любом случае увеличивать консистентность кода по больнице. Какой‑то «я так вижу» с ошибками верстки на шаблонах других модулей становится ощутимо меньше. Запутанные канделябры хаотичных стилей исчезают, конфликты специфичности наконец‑то действительно вообще невозможны, отвратительные адовые кастомизации сторонних библиотек уходят, растворяются… Хотя бы визуальные компоненты не размножаются неконтролируемо по репозиториям и модулям, а напротив становятся фокусом совместной работы и опыта.

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

Но, давайте уже попробуем!

Установка

В корневой директории вашего проекта создайте файл .npmrc и добавьте в него токен доступа (токен выдаст администратор репозитория). После этого выполните установку:

npm install @ushliypakostnik/uui-library

Вы можете использовать сборки из проектов примеров, если хотите быстро развернуть свое приложение на Vue, React или Angular.

В любом другом случае, вам следует самостоятельно настроить копирование файлов шрифтов из node_modules/@ushliypakostnik/uui-library/public/fonts в директорию статических ресурсов вашего приложения. В проектах на Vite это, как правило, папка public.

Например, добавьте скрипт в package.json своего проекта, очистите кэш npm и снова запустите установку:

{
  "scripts": {
    "postinstall": "node -e \"const fs=require('fs');const path=require('path');const src=path.join(__dirname,'node_modules/@ushliypakostnik/uui-library/public/fonts');const dest=path.join(__dirname,'public/fonts');if(fs.existsSync(src)){fs.cpSync(src,dest,{recursive:true});console.log('Fonts copied to',dest);}else{console.log('Fonts not found');}\""
  }
}

Инициализация

import UUI from 'uui';

new UUI({ isInitWC: true, isAddNormalize: true });

Во время инициализации Дизайн‑Системы вы можете передать конфигурационные параметры экземпляра. При работе над новым проектом «с нуля», имеет смысл активировать опцию isAddNormalize со значением true — это применит нормализацию браузерных стилей. При интеграции ДС в существующую объёмную кодовую базу, эта настройка будет лишней.

// Конфигурация Библиотеки
type TOptions = {
  isInitWC?: boolean; // Запускать ли нативные веб-компоненты?
  isAddNormalize?: boolean; // Применять ли нормализацию CSS?
};

TypeScript

При работе с TypeScript добавьте объявление модуля в файл *.d.ts:

declare module "@ushliypakostnik/uui-library";

Для React также потребуется установить и подключить типы:

npm i -D @types/react
Код
declare module 'react/jsx-runtime' {
  namespace JSX {
    interface IntrinsicElements {
      div: JSX.IntrinsicElements<any>;
      nav: JSX.IntrinsicElements<any>;
      // Прочие стандартные теги, используемые в шаблонах...
      // ...
      'uui-text': JSX.IntrinsicElements<any>;
      'uui-icon': JSX.IntrinsicElements<any>;
      // Прочие кастомные элементы, используемые в шаблонах...
      // ...
    }
  }
}

Pure JS / Vue / Angular

Для работы с нативными Веб‑компонентами (в проектах на Pure JS, Vue3 или Angular) — передайте флаг isInitWC при инициализации.

Импортируйте и запустите Дизайн‑Систему в точке входа вашего приложения:

React

Код
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import UUI from '@ushliypakostnik/uui-library';
import App from './App.tsx';

new UUI();

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <>
      <App />
    </>
  </StrictMode>,
);

Angular

Код
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

import UUI from '@ushliypakostnik/uui-library';

new UUI({ isInitWC: true });

bootstrapApplication(AppComponent, appConfig).catch((err) =>
  console.error(err),
);

В шаблонах Angular с компонентами ДС используйте CUSTOM_ELEMENTS_SCHEMA:

import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

@Component({
  ...
  templateUrl: './app.component.html',
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppComponent {}

Vue3

Код
import UUI from '@ushliypakostnik/uui-library';

new UUI({ isInitWC: true });

import { createApp } from 'vue';
import App from './App.vue';

createApp(App).mount('#app');

Для корректной работы атрибута slot добавьте правило в конфигурацию ESLint (пример для Vue3 + Vite + TypeScript) и перечислите ваши пользовательские элементы:

Код
import { defineConfigWithVueTs } from '@vue/eslint-config-typescript';

export default defineConfigWithVueTs({
  rules: {
    'vue/no-deprecated-slot-attribute': [
      'error',
      {
        // Перечислите ваши пользовательские элементы!
        ignore: ['uui-text', 'uui-icon' /*, остальные элементы... */],
      },
    ],
  },
});

Основное

Гайдлайн

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

Полные списки токенов хранятся в файле src/models/models.ts исходного кода библиотеки. Называть токены в этих наборах рекомендуется несемантически.

// Atoms

// Типографика
enum Fonts {
  // Заголовки
  elena = 'elena',
  // прочие токены...
}

// Скругления
enum Roundings {
  dancing = 'dancing',
  // прочие токены...
}

// Прозрачности
enum Opacities {
  waltz = 'rock',
  // прочие токены...
}

// Тени
enum Shadows {
  antares = 'antares',
  // прочие токены...
}

// Цветовая палитра
enum Colors {
  cat = 'cat',
  // прочие токены...
}

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

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

Атрибуты

При работе с Angular составные имена атрибутов следует записывать в верблюжьей нотации (camelCase), а не через дефис (kebab‑case):

Код
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

@Component({
  ...
  template: '
    <uui-element
      [propertyName]="property"
    >
      ...
    </uui-element>',
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class SomeComponent {
  property: string;
  ...
}

Привязка логических атрибутов

Входные параметры логического типа можно условно разделить на два подтипа: статические (которые никогда не меняются) и динамические (которые специально введены для отображения определённого состояния). И тут есть проблема. Например, если поле ввода Input должно иметь атрибут rounded — его можно просто указать на элементе без значения — скругленный ввод ни при каких обстоятельствах не становится более квадратным. А вот пропс loading, если указан, независимо от значения, всегда покажет его с вращающимся загрузчиком.

<!-- 1. Этот элемент всегда будет круглым! -->
<uui-input rounded />

<!-- 2. Все эти кнопки будут с лоадером: -->
<uui-input loading="true" />
<uui-input loading="false" />
<uui-input loading />
  1. Здесь выручит дублирование разметки по условию, например, во Vue:

<uui-input v-if="loading" loading />
<uui-input v-else />
  1. Или «джаваскрипт на элементе по ID»:

Код
<uui-input id="button" />

<script>
  setTimeout(() => {
    const button = document.getElementById('button');
    if (button) {
      button.setAttribute('loading', 'true');

      setTimeout(() => {
        const button = document.getElementById('button');
        if (button) button.removeAttribute('loading'); // Нужно именно удалить атрибут
      }, 1000);
    }
  }, 1000);
</script>

Во Vue это сработает, без дополнительных ухищрений и принудительных перерендеров — даже когда на элементе изменяются несколько логических параметров одновременно. В Angular потребуется применить знакомые вам приёмы для запуска перерендера, если вы предпочитаете не дублировать разметку в условиях. При наличии нескольких логических атрибутов, изменяющихся динамически, удобно организовать компактный стор в виде хеш‑таблицы внутри родительского компонента, обновляя все логические атрибуты при изменении любого из них. Пример для Vue:

Код
import { defineComponent, ref } from 'vue';

export default defineComponent({
  setup() {
    // Стор
    const attrs = ref<{ [key: string]: boolean }>({});

    // Метод устанавливающий все динамические boolean-атрибуты
    const setAttrs = () => {
      const button = document.getElementById('button');
      if (button) {
        for (const prop in attrs.value) {
          if (attrs.value[prop]) button.setAttribute(prop, `true`);
          else button.removeAttribute(prop);
        }
      }
    };

    // Метод устанавливающий атрибут loading
    const setLoading = (value: boolean) => {
      loading.value = value;
      attrs.value.loading = loading.value;
      setAttrs();
    };

    // Метод устанавливающий другой атрибут
    const setProperty = (value: boolean) => {
      property.value = value;
      attrs.value.property = property.value;
      setAttrs();
    };
  },
});

В React привязка стора к логическому значению возможна, но есть нюанс: если изменяется только логический параметр (с true на false), а остальные пропсы остаются прежними — компонент не будет перерендерен. Решение — динамический ключ:

Код
import { useState } from 'react';

// React Components
import { Button } from '@ushliypakostnik/uui-library';

function Component() {
  const [key, setKey] = useState(0);
  const [loading, setLoading] = useState(false);

  // Устанавливаем значение, предположим, из строки в логическое, которое привязывается
  // И меняем динамический ключ на компоненте
  const setLoadingValue = (value: string) => {
    setLoading(value === 'true');
    setKey(key + 1);
  };

  return (
    <>
      <Button loading={loading} text="Текст кнопки" key={key}></Button>
    </>
  );
}

export default Component;

Альтернативный способ: дублирующие логические литеральные атрибуты

Привязка булевых атрибутов в React и Angular может вызвать затруднения (примеры выше это демонстрируют). Поэтому Дизайн‑Система предлагает проcтое и удобное решение: любой динамический логический пропс можно продублировать с подчёркиванием в начале имени и передавать как строковый литерал 'true' или 'false'. При наличии обоих атрибутов приоритет имеет значение с подчёркиванием. Пример смотрите в компоненте Input.

Для React:

<Component _loading={loading ? 'true' : 'false'}></Component>

Для Angular:

<uui-component [_loading]="loading ? 'true' : 'false'"></uui-component>

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

Атрибуты — динамические ключи

Иногда нам нужно вызвать определенное действие в компоненте, которое не связано ни с каким значением, напрмиер, частая ситуация — установить фокус в поле ввода. Дизайн‑Система позволяет сделать это с помощью изменения значения динамического ключа, в виде передачи в него новой строки (отличной от предыдущей):

<uui-input id="input" placeholder="placeholder"></uui-input>

<script>
  // Через 1 секунду устанавливаем фокус на поле ввода
  setTimeout(() => {
    const input = document.getElementById('input');
    if (input) input.setAttribute('focus', '1');
  }, 1000);
</script>

Пример смотрите в компоненте Input.

Адаптивность

Реализация гибкого поведения интерфейсов на различных размерах экранов — одна из важных задач в современной вёрстке, с которой не все разработчики справляются легко и без досадных ошибок. Дизайн‑система дает простые подходы и инструменты для решения таких проблем. Гайдлайн и дизайнерские макеты в данной реализации определяют три ключевых диапазона:

  • Desktop — стационарные компьютеры и крупные дисплеи — от 1320 пикселей

  • Tablet — планшетные устройства — от 768 до 1319 пикселей

  • Mobile — смартфоны и компактные экраны — до 767 пикселей

Для работы с адаптивностью Дизайн‑Система предлагает следующие инструменты:

  • Утилитарная функция ScreenHelper (IIFE). Вы можете экспортировать ее в проекты из библиотеки. Она же используется для этих целей «внутри компонент».

  • CSS‑классы для ускоренной вёрстки через дублирование блоков под разные экраны. Подробности — в разделе CSS.

ScreenHelper построен на базе стандартного Browser API и проверяет соответствие текущего окна браузера медиа‑выражениям, опирающимся на точки перехода типоразмеров гайдлайна:

Код
// Контрольные точки
const DESIGN = {
  BREAKPOINTS: {
    tablet: 768,
    desktop: 1320,
  },
  // Прочие константы дизайна...
};

// Утилита ScreenHelper
export const ScreenHelper = (() => {
  const DESKTOP = DESIGN.BREAKPOINTS.desktop;
  const TABLET = DESIGN.BREAKPOINTS.tablet;

  // Десктоп?
  const isDesktop = () => {
    return window.matchMedia(`(min-width: ${DESKTOP}px)`).matches;
  };

  // Гаджеты (планшет или телефон)?
  const isGadgets = () => {
    return window.matchMedia(`(max-width: ${DESKTOP - 1}px)`).matches;
  };

  // Планшет?
  const isTablet = () => {
    return window.matchMedia(`(min-width: ${TABLET}px) and (max-width: ${DESKTOP - 1}px)`).matches;
  };

  // Телефон?
  const isMobile = () => {
    return window.matchMedia(`(max-width: ${TABLET - 1}px)`).matches;
  };

  // Не телефон (планшет или десктоп)?
  const isNotMobile = () => {
    return window.matchMedia(`(min-width: ${TABLET}px)`).matches;
  };

  return {
    isDesktop,
    isGadgets,
    isTablet,
    isMobile,
    isNotMobile,
  };
})();

Использование в коде проектов

Для чего это может быть нужно? Представьте: вы разрабатываете приложение на Vue3, и макеты для планшетов и смартфонов ещё не готовы. Нужно временно показывать контент только на десктопах, а на остальных устройствах — заглушку. В главном верхнеуровневом компоненте «Слоя» это реализуется, например, так:

Код
<script lang="ts">
import { defineComponent, onMounted, Ref, ref } from 'vue';
import { ScreenHelper } from '@ushliypakostnik/uui-library';

export default defineComponent({
  setup() {
    let onWindowResize: () => void;
    let isDesktop: Ref<boolean> = ref(false);

    onMounted(() => {
      onWindowResize();
      window.addEventListener('resize', onWindowResize, false);
    });

    onWindowResize = () => {
      // Вызов ScreenHelper в обработчике изменения размера вьюпорта
      isDesktop.value = ScreenHelper.isDesktop();
    };

    return {
      isDesktop,
    };
  },
});
</script>

<template>
  <div class="layout">
    <div v-if="isDesktop">
      <!-- Основной контент ... -->
    </div>

    <!-- Временная заглушка для мобильных и планшетов -->
    <div v-else class="layout__gate">
      <div>Мобильная и планшетная версии в разработке!</div>
    </div>
  </div>
</template>

<style lang="stylus" scoped>
.layout
  // ...

  &__gate
    position fixed
    top 0
    left 0
    right 0
    bottom 0
    width 100vw
    height 100vh
    background #000
    display flex
    align-items center
    justify-content center
    text-align center
</style>

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

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

  1. Помощник в CSS:

Код
import Helper from '../utils/ComponentsHelper';

export default class Icon extends LitElement {
  private _helper;

  constructor() {
    super();
    this._helper = new Helper();
  }

  // Шаблон
  render(): string {
    return html`
      <style>
        .selector {
          // Общие стили для всех экранов - "десктоп фирст )))"
        }
        
        ${this._helper.gadgets()} {
          .selector {
            // Стили только для гаджетов
          }
        }
      </style>
        
      <span class="selector"></span>
    `;
  }
}
  1. Примесь для компонент:

Код
// Миксины
import { CommonMixin } from './mixins/Common';

export default class Input extends CommonMixin(LitElement) {
  // Шаблон
  render(): string {
    // Теперь можно использовать в логике, напмриер, в рендере:
    if (this._isDesktop) {
      // Логика только для десктопов
    }
    
    return html`
    `;
  }
}

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

Состояние

Иногда возникает потребность получить доступ к внутреннему состоянию экземпляра Дизайн‑Системы. Обычные ситуации: модификация разметки в зависимости от активной темы или реакция на открытие/закрытие меню в верхнеуровневом слое. Доступны три подхода:

  • Инициализация в корневом компоненте. Перенесите создание экземпляра ДС из точки входа в верхнеуровневый компонент-слой и вызовите метод getState для получения полного объекта состояния.

  • Наблюдение за пропсом. Отдельные параметры можно получить напрямую из соответствующих компонентов. К примеру, актуальная тема доступна через пропс value компонента Theme.

  • Прямое чтение из localStorage. Состояние Дизайн‑Системы синхронизируется с хранилищем браузера, поэтому нужное значение можно получить напрямую:

// Получаем текущую активную тему
const theme = localStorage.getItem('state:theme');

Пример на Vue3 (первый подход — инициализация в компоненте):

Код
<script lang="ts">
import { defineComponent, onMounted } from 'vue';
import UUI from '@ushliypakostnik/uui-library';

export default defineComponent({
  setup() {
    let uui: UUI;

    onMounted(() => {
      uui = new UUI({ isInitWC: true, isAddNormalize: true });
    });

    const getState = () => {
      console.log('Текущее состояние ДС: ', uui.getState())
    };
  },
});
</script>

CSS

Для применения поставляемых с библиотекой стилей — импортируйте их совместно с библиотекой в точке входа приложения:

import UUI from 'uui';
import 'uui/uui.css';

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

Библиотека концептуально не экспортирует атомы гайдлайна в форме утилитарных CSS‑классов для типографики. От плохих практик нужно отвыкать. Применяйте соответствующий компонент Text.

<!-- Я необучаемый мракодел! -->
<span class="my-custom-class-for-typography">Верстаю по-старинке!<span>

<!-- Я котик! -->
<uui-text value="nina" color="sea" />

Используя во всех возможных случаях только поставленные компоненты ДС, вы поступаете правильно, потому что:

Ваши коды разметки остаются максимально выразительными и консистентными. Все только в одном месте, легче искать.

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

Кроме того, всегда остается вероятной ситуация, что дизайнеры опять захотят что‑либо изменить или добавить. Любые изменения в код аккуратно отверстаный с помощью компонентов ДС — придут «по умолчанию» при обновлении библиотеки.

Цвета, прозрачности, скругления

Доступно использование CSS Custom Properties для применения оттенков, прозрачности и скруглений согласно гайду к произвольным элементам. Компонент Theme позволяет переключать темы.

/* Custom Properties */
:root {
  /* Радиусы */
  --dancing: 4px;
  --swimming: 8px;
  --shooting: 16px;

  /* Прозрачность */
  --rock: 1;
  --psy: 0.66;
  --pop: 0.33;
  --reggae: 0;
}

/* Можно применять в проектах или в коде CSS компонент библиотеки */
.selector {
  color: var(--sea);
  opacity: var(--rock);
  border-radius: var(--dancing);
}

Тени

Тени доступны в форме CSS‑классов. Необходимо подключить глобальные стили ДС.

.antares
  box-shadow 0px 5px 5px -2px var(--sea)

.orion
  box-shadow 0px 5px 20px 0px var(--sea)

.venus
  box-shadow 0px 10px 40px 0px var(--sea)

Можно применять на шаблонах проектах или в коде CSS компонент библиотеки:

  <div class="orion"></div>

В компонентах библиотеки вы можете использовать помощник:

Код
import Helper from '../utils/ComponentsHelper';

export default class Icon extends LitElement {
  private _helper;

  constructor() {
    super();
    this._helper = new Helper();
  }

  // Шаблон
  render(): string {
    return html`
      <style>
        .selector {
          ${this._helper.getShadow('orion')}
        }
      </style>
        
      <div class="selector"></div>
    `;
  }
}

Адаптивные утилиты

ДС включает набор классов для оперативного дублирования небольших фрагментов разметки под различные типоразмеры. Да-да-да, она "разрешает и помогает вам немного мракокодить на шаблонах", но только одним простым и полезным способом.

Представьте: вы создаёте страницу-заглушку, где поведение блоков и переносы текста различаются на прототипах для разных размеров экранов — типоразмеров. Автор ДС, исходя из многолетнего опыта, предполагавт: отдельные задачи проще и реалистичнее решать «специальным инструментом». Реализация «через JS» и функцию ScreenHelper  — кажется допустимой, но избыточной. Здесь помогает CSS.

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

.[видимость]--[display-тип]--[типоразмер]

Варианты:

$visibility = "visible" "hidden"
$display = "block" "inline-block" "inline" "flex" "inline-flex" "grid"
$screen = "desktop" "tablet" "mobile" "gadgets" "not-mobile"

Препроцессор ДС генерирует конструкции для всех комбинаций:

Код
@media only screen and (min-width: 1320px) {
  .visible--block--desktop { display: block !important; }
}

@media only screen and (min-width: 1320px) {
  .hidden--block--desktop { display: none !important; }
}

/* Прочие комбинации... */

Пример: сетка (display: grid;), где макет требует смены порядка элементов на мобильных:

.grid-element {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 0 24px;
}

@media only screen and (max-width: 767px) {
  .grid-element {
    grid-template-columns: 1fr;
    gap: 16px 0;
  }
}

На десктопах/планшетах порядок:

<div class="grid-element">
  <div id="grid-block--1">...</div>
  <div id="grid-block--2">...</div>
  <div id="grid-block--3">...</div>
</div>

На смартфонах — иной:

<div class="grid-element">
  <div id="grid-block--3">...</div>
  <div id="grid-block--2">...</div>
  <div id="grid-block--1">...</div>
</div>

Решение — дублирование сетки с классами видимости:

<div class="grid-element hidden--grid--mobile">
  <div id="grid-block--1">...</div>
  <div id="grid-block--2">...</div>
  <div id="grid-block--3">...</div>
</div>
<div class="grid-element visible--grid--mobile">
  <div id="grid-block--3">...</div>
  <div id="grid-block--2">...</div>
  <div id="grid-block--1">...</div>
</div>

Эквивалентно:

<div class="grid-element visible--grid--not-mobile">...</div>
<div class="grid-element hidden--grid--not-mobile">...</div>

В реальных проектах вы будете применять свой компонент сетки из ДС. Это упрощённый пример, показывающий: иногда вместо «ручного написания CSS» рациональнее «взять готовые классы из ДС».

Другой частый случай — переносы в тексте, он часто бывает один и тот же, но с разными разрывами‑переносами на разных типоразмерах:

<div>
  Lorem ipsum dolor sit amet, consectetur<br
    class="hidden--inline--gadgets"
  /><span class="hidden--inline--not-gadgets">&nbsp;</span
  >adipiscing elit, sed do eiusmod tempor<br
    class="hidden--inline--gadgets"
  /><span class="hidden--inline--not-gadgets">&nbsp;</span
  >incididunt ut labore et dolore magna aliqua.
</div>

Смысл в том, что на шаблонах у вас есть удобное быстрое CSS‑решение, позволяющее контролировать отображение элементов через CSS. «Тут это показал, а тут скрыл».

Добавление компонент

Основные шаги добавления компонента

Для того чтобы добавить компонент в библиотеку:

  1. Веб‑компонентsrc/components/ComponentName.ts Напишите компонент.

  2. React‑обёрткаsrc/react/ComponentName.ts Добавьте обертку для React.

  3. Экспортsrc/main.ts Импортируйте компонент в точку входа. Инициализируйте в любом случае, если это общий компонент который используют другие компоненты библиотеки, и по флагу если нет.

  4. Документацияdocs/components/component-name.md Документируйте.

  5. Навигацияdocs/.vitepress/config.ts Добавьте в меню документации.

  6. Типы → Добавьте в объявления типов для React и в исключение ESlint для Vue.

  7. index.html → Можно протестировать компонент визуально, добавив его в индексный файл и запустив библиотеку в режиме разработки.

  8. Примеры → Можно сделать версию, обновить в проектах‑примерах и протестировать визуально, добавив в корневой компонент.

На что обратить внимание

Если вы хотите чтобы измененное «внутри» значение пропса‑атрибута «отражалось наверх в разметку» — укажите reflect при объявлении.

// Параметры компонента
static get properties() {
  return {
    value: {
      type: String,
      reflect: true, // внимание - отражается на разметку наружу!!!
    },
  };
}

В стартовом проекте два таких компонента, с синхронизированным атрибутом value: Input и Theme.

Далее будет рассмотрено несколько простых приемов которые могут понадобиться даже при построении небольших компонентов-молекул.

Ссылка на элемент по идентификатору

get _control() {
  return this.renderRoot?.querySelector('#control') ?? null;
}

// В рендере
render(): string {
  return html`
    <input id="control" />
  `;
}

Внутреннее состояние и события

Код
export default class Input extends CommonMixin(LitElement) {
  private _inputEvent: Event;
  private _focusEvent: Event;
  private _blurEvent: Event;

  // Внутреннее состояние
  @state()
  protected _hover: boolean = false; // Ховер
  @state()
  protected _active: boolean = false; // Актив

  constructor() {
    // События
    this._inputEvent = new Event('input', { bubbles: true, composed: true });
    this._focusEvent = new Event('focus', { bubbles: true, composed: true });
    this._blurEvent = new Event('blur', { bubbles: true, composed: true });

    // Слушатели
    this.addEventListener('mouseenter', this._onMouseEnter);
    this.addEventListener('mouseleave', this._onMouseLeave);
    this.addEventListener('mouseup', this._onMouseUp);
    this.addEventListener('mousedown', this._onMouseDown);
    this.addEventListener('mouseout', this._onMouseOut);
  }

  // Эффекты

  private _onMouseEnter(): void {
    this._hover = true;
  }

  private _onMouseLeave(): void {
    this._hover = false;
  }

  private _onMouseDown(): void {
    this._active = true;
  }

  private _onMouseUp(): void {
    this._active = false;
  }

  private _onMouseOut(): void {
    this._active = false;
  }

  private _focusHandler(): void {
    this._focus = true;
    this.dispatchEvent(this._focusEvent);
    this._setFocus();
  }

  private _blurHandler(): void {
    this._focus = false;
    this.dispatchEvent(this._blurEvent);
  }

  private _inputHandler(event: Event): void {
    this.value = (event.target as HTMLInputElement).value;
    this.dispatchEvent(this._inputEvent);
  }

  // Шаблон
  render(): string {
    return html`
      <input
        @focus=${this._focusHandler}
        @blur=${this._blurHandler}
        @input=${this._inputHandler}
      />
    `;
  }
}

Добавление динамического ключа и обработка ошибок

Код
export default class Input extends CommonMixin(LitElement) {
  private _focusStore: string | null;

  // Параметры компонента
  static get properties() {
    return {
      focus: {
        type: String,
      },
    };
  }

  constructor() {
    super();
    // this._helper = new Helper();

    // Дефолтные параметры
    this.focus = '';
    this._focusStore = null;
  }

  // Установить фокус на поле ввода
  private _setFocus(isFocus = false): void {
    if (this._control) {
      if (isFocus) this._control?.focus();
      setTimeout(() => {
        this._control?.setSelectionRange(this.value.length, this.value.length);
      }, 0);
    }
  }

  // При обновлении - проверка на допустимые значения атрибутов
  willUpdate(changedProps: Map<PropertyKey, unknown>): void {
    // Обработка полей "динамических ключей"
    if (changedProps.has('focus')) {
      if (this.focus.length && this.focus !== this._focusStore) {
        this._setFocus(true); // Установить фокус на поле ввода
        this._focusStore = this.focus;
      }
    }
    
    /*
    // Обработка строчных полей
    if (changedProps.has('token')) {
      this.token = this._helper.checkProperty(
        'token',
        this._propertiesDefault.token,
        this.token,
        this._name,
      );
    }

    // Обработка логических полей
    if (changedProps.has('_disabled')) {
      if (this._disabled && !['true', 'false'].includes(this._disabled)) {
        this._helper.log([
          this._name,
          ', недопустимое значение атрибута _disabled: ',
          this._disabled,
          ', установлено false.',
        ]);
        this._disabled = 'false';
      }
    }
    */
  }
}

Вывод

Если вы дочитали до этого места, значит вам реально интересно! Это уже замечательно! Надеюсь вы найдете возможность применить эту информацию и разработку в реальной практике! Будем надеяться что это только первая статья в цикле про UUI и будут еще.

Ссылки на стенды:

Библиотека

Проекты примеры:
Vue
Angular
React (последний фикс на этот стенд не раскатился, потому что у меня закончилось бесплатная квота общих вычислительных минут средств выполнения в пространстве на GitLab. Сейчас нет желания это как‑то мучительно исправлять, но и без этого «все и так понятно».)

Удачи в написании ваших пользовательских универсальных Дизайн‑систем!

Комментарии (0)