@rt-tools/ui-kit-v2
v0.39.0
Published
Набор компонентов Angular и дизайн-система к ним: шкалы оформления, светлая и тёмная темы. Компоненты сигнальные и `OnPush`, разметка размечена BEM-директивами из `@rt-tools/core`, каждый интерактивный элемент несёт `qa-dataid`.
Readme
@rt-tools/ui-kit-v2
Набор компонентов Angular и дизайн-система к ним: шкалы оформления, светлая и тёмная темы.
Компоненты сигнальные и OnPush, разметка размечена BEM-директивами из @rt-tools/core,
каждый интерактивный элемент несёт qa-dataid.
Пакет живёт рядом с @rt-tools/ui-kit, а не вместо него: селекторы (rt-* против rtui-*),
свойства оформления и наборы компонентов у них не пересекаются, поэтому одно приложение может
держать оба.
Установка
pnpm add @rt-tools/ui-kit-v2 @rt-tools/core @rt-tools/utilsquill нужен только тем, кто ставит редактор текста. Он и два компонента на нём — поле ввода
сообщения и чат — стоят отдельным входом @rt-tools/ui-kit-v2/rich-editor:
import { RtChatComponent, RtMessageComposerComponent, RtRichEditorComponent } from '@rt-tools/ui-kit-v2/rich-editor';Основной вход quill не называет, и приложение без этих трёх компонентов собирается без него.
pdfjs-dist нужен только тем, кто открывает просмотрщик документа PDF. Он стоит отдельным входом
@rt-tools/ui-kit-v2/pdf-viewer, а файл воркера pdf.worker.min.mjs приложение кладёт себе в
assets/pdfjs/ при сборке — подробности на странице обзора rt-pdf-viewer:
import { RtPdfViewerComponent } from '@rt-tools/ui-kit-v2/pdf-viewer';Основной вход pdfjs-dist не называет, и приложение без просмотрщика собирается без него.
Точки входа компонентов
Каждый компонент собран своей точкой входа по имени своего каталога: @rt-tools/ui-kit-v2/table,
@rt-tools/ui-kit-v2/calendar и так далее. Настройки, словари, среда исполнения, токены и чистые
функции лежат во входе @rt-tools/ui-kit-v2/core. Корень пакета переотдаёт их все, и прежние
импорты из @rt-tools/ui-kit-v2 работают как раньше. В кусок бандла сборщик кладёт только модули
тех компонентов, что импортированы.
Компонент внутри @defer берётся из своей точки входа. Отложенный блок грузит импорт целиком, и
импорт из корня оставил бы в бандле весь пакет.
Типы идут отдельной строкой import type: импорт, где рядом с компонентом стоит тип, Angular не
откладывает.
import { RtTableComponent } from '@rt-tools/ui-kit-v2/table';
import type { IRtStatTile } from '@rt-tools/ui-kit-v2/stat-tile';Подключение
import { provideRtIDBStorage } from '@rt-tools/core';
import { provideRtIcons } from '@rt-tools/ui-kit-v2';
export const appConfig: ApplicationConfig = {
providers: [provideHttpClient(), provideRtIcons(), provideRtIDBStorage()],
};provideRtIDBStorage() нужен rt-data-table, rt-data-list и rt-table: они хранят настройку
колонок в базе браузера. Без него страница с ними пустая, а в консоли стоит
NG0201: No provider found for _IDBStorageService.
Оформление — одной строкой в точке входа приложения; в пакете лежит и собранный CSS со всеми свойствами, и исходники партиалов:
@use '@rt-tools/ui-kit-v2/styles/tokens.css';Стили кита объявлены слоем каскада rt-kit — тремя подслоями в порядке vendor, base,
components. Правило приложения, объявленное вне слоя, выигрывает у любого правила кита
независимо от специфичности: перебирать её счётом и обходить чужую вёрстку больше не нужно.
Объявления свойств на корне страницы в слой не уходят — их приложение перебивает порядком,
объявляя своё после tokens.css.
Тёмная тема кита включается только признаком на корне: [data-theme='dark'] или класс
html.rt-theme-dark. Системную настройку CSS кита не читает: по ней признак ставит ThemeService
в режиме auto. Своя тема Material или класс тёмной темы приложения кит не переключают.
На узле ниже корня [data-theme='dark'] даёт тёмный кусок страницы.
Под материальным набором тёмная тема не перебивает цвета, которые набор берёт из ролей темы
приложения --rt-sys-*. Поиск, поля отбора и пагинация следуют теме приложения, а она отвечает
тёмной сама.
Без темы Material на странице набор рисует тёмные цвета кита.
Оба признака на корне ставят на body фон-градиент тёмной темы. Приложению со своим фоном
страницы его нужно перебить своим правилом body.
Приложение, которое переезжает с @rt-tools/ui-kit и пока держит оба кита, подключает ещё один
файл — файл совместимости, он лежит рядом с tokens.css:
@use '@rt-tools/ui-kit-v2/styles/tokens.coexist.css';Первый кит объявляет на корне страницы десять ступеней с теми же именами — --rt-radius-* и
--rt-shadow-sm/md/lg — и своими значениями. Его файл подключается позже, и внутри компонентов
второго кита действуют его ступени: скругление поля отбора выходит 8px вместо 4px. Файл
совместимости ставит значения второго кита на узел материального набора (.rt-preset-material,
[data-preset='material']), и такой узел рисуется своей шкалой, что бы ни лежало на корне. Набор
может стоять и на самом корне страницы: селектор файла весит на ступень больше корня первого кита,
и порядок подключения ничего не решает.
С этим файлом приложению не нужны свои скругления поверх кита: поле отбора, поле поиска и кнопки страниц берут значения второго кита сами.
Файл удаляется вместе с первым китом. Он повторяет значения шкалы второго кита, поэтому без первого кита вид от его удаления не меняется.
Подслой vendor — для чужого CSS, который чинит кит. Тема редактора текста подключается в
него, иначе она выигрывает у правил кита и панель редактора перестраивается:
@import 'quill/dist/quill.snow.css' layer(rt-kit.vendor);Сброс приложения вида * { margin: 0; padding: 0; } вне слоя выигрывает у кита по тому же
правилу. Он обнуляет поля компонентов: ячейки таблицы, пункты меню и кнопки теряют отступы.
Сброс кладётся в тот же подслой vendor. Подслой нижний, и кит перебивает сброс своими
правилами, а остальная вёрстка приложения получает его как прежде:
@layer rt-kit.vendor {
* {
margin: 0;
padding: 0;
}
}Полосу прокрутки кит рисует только своим узлам — с классом на rt-. Узлы приложения он не
трогает, и их ::-webkit-scrollbar работает и в Chrome. Своя полоса приложения, поставленная всем
узлам, кладётся в тот же подслой vendor: вне слоя она перебивает полосы кита, и таблица
перестаёт прятать полосу, которую человек выключил в настройке колонок.
Иконки лежат отдельными файлами в node_modules/@rt-tools/ui-kit-v2/src/assets/icons: сборка
копирует каталог в свою статику, а provideRtIcons('/icons') называет адрес, по которому он
опубликован. Файл забирается тогда, когда значок с этим именем попросила разметка, и дальше
живёт в inline-sprite: страница платит за те значки, которые нарисовала, а не за весь набор.
{
"glob": "**/*.svg",
"input": "node_modules/@rt-tools/ui-kit-v2/src/assets/icons",
"output": "icons"
}Приложение, которое объявляет материальный набор оформления, публикует и второй набор значков —
рисунки Material из src/assets/icons-material. Его адрес по умолчанию /icons-material, свой
называется вторым аргументом: provideRtIcons('/icons', '/icons-material'). Без этой записи
кит рисует свои значки и под материальным набором: пустого места не будет, но и рисунка Material
тоже.
{
"glob": "**/*.svg",
"input": "node_modules/@rt-tools/ui-kit-v2/src/assets/icons-material",
"output": "icons-material"
}Приложение, пришедшее с первого кита, называет значки именами Material — их принимает вход
glyph у rt-icon, а у кнопки rtButton тот же вход icon. Имя с парой в перечне кита рисуется
этой парой, без пары — лигатурой шрифта Material Symbols. Этот шрифт кит не везёт: приложение
подключает его само, семья переназначается свойством --rt-icon-glyph-font. Третий аргумент
выбирает способ: provideRtIcons('/icons', '/icons-material', { glyphStrategy: 'font' }) рисует
шрифтом любое имя, по умолчанию стоит map-first.
Шрифт кит везёт сам. Начертания Montserrat лежат в
node_modules/@rt-tools/ui-kit-v2/src/assets/fonts, объявления @font-face — в слое оформления,
и адрес в них относительный: сборка приложения разрешает его от места файла стилей и уносит
файлы в свою статику сама. Отдельной настройки это не просит — в отличие от значков, которые
публикуются по адресу.
Начертание переменное, одно на все насыщенности: кит объявляет веса 400, 500, 600 и 700 и берёт их из одной оси. Подмножеств четыре — кириллица, кириллическое расширение, латиница и латинское расширение, каждое в прямом и наклонном начертании; браузер качает только те, чьи знаки на странице встретились. Всего 332 КБ, русской странице достаются два файла из восьми.
Письменностей, которых в Montserrat нет, кит не везёт: тайская, хангыль и деванагари добавляются
приложением — своим @font-face и своим именем в конце --rt-font-family-sans. Именовать
семейство, которого кит не везёт, он не будет: названное и не привезённое рисуется тем, что
случайно стоит на машине читателя, а отступы и высоты строк кита рассчитаны под Montserrat.
Что внутри
Компоненты — 73 семейства: поля формы (rt-input, rt-select, rt-multiselect,
rt-date-picker, rt-date-range, rt-file-input, rt-rich-editor, rt-field), таблица с
настройкой колонок и пагинацией, панель правки записи с маршрутом и защитой несохранённого,
диалоги, чат с композером, календарь, шапка страницы с навигацией, тосты, просмотрщик фотографий,
показ размеченного текста (rt-markdown-text) и остальное.
Дизайн-система — src/styles:
tokens.source.mjs— вход источника: ступени шкал, назначения и ответ тёмной темы рядом со своим назначением. Сами узлы лежат частями рядом —tokens.scale-*.mjsдля шкал,tokens.light-*.mjsдля назначений,tokens.dark-layout.mjsиtokens.coarse-pointer.mjsдля порядка тёмного файла и грубого указателя, — а вход складывает их в два ряда. Три файла ниже и имена вsrc/lib/tokens/rt-design-tokens.tsсобираются отсюда командойpnpm run build:tokens-sourceи руками не правятся;_primitives.scss— шкалы: нейтральные и цветные ряды, отступы, радиусы, тени, типографика;_semantic.scss— назначения поверх шкал (--rt-color-bg-surface,--rt-color-text-muted);_coarse-pointer.scss— поправка назначений для сенсорного экрана: поле ввода не мельче 16px;_theme-dark.scss— переопределения назначений для[data-theme='dark'];_theme-dark-rules.scss— правила тёмной темы, которые свойств не объявляют: подложка экранов входа и инверсия логотипа;_theme-scope.scss— те же назначения для узла, а не для корня: под ними живёт меткаrtTheme, и каждое правило объявляет весь набор целиком;_scrollbar.scss— оформление полос прокрутки;_breakpoints.scss— SCSS-переменные порогов: свойства оформления в@mediaне работают, поэтому пороги остаются переменными препроцессора;_mixins.scss— миксины поверхности карточки и её заголовка;_login.scss— раскладка страницы входа.
_coexist.scss — ступени второго кита на узле материального набора на время переезда с первого
кита. Его собирает тот же генератор из источника шкалы, а файл совместимости собран из него
отдельно.
Всё, кроме _breakpoints.scss и _coexist.scss, собирает _index.scss — из него и собран
tokens.css. Пороги и
миксины подключаются партиалами напрямую:
@use 'pkg:@rt-tools/ui-kit-v2/styles/breakpoints' as bp;
@use 'pkg:@rt-tools/ui-kit-v2/styles/mixins';Среда исполнения — ThemeService (три состояния темы, признак на <html data-theme>,
переживает перезагрузку), RtThemeScopeDirective (метка rtTheme: своя тема у узла),
BreakpointsService (сигналы совпадения по ширине), NotificationBus (шина тостов).
Умолчания кита — provideRtKit: приложение одной раздачей задаёт тему старта, вид кнопки,
шторы, таблицы, списка, полей выбора и поведение подсказки. Раздача необязательна, а вход, написанный в разметке, её
перебивает.
Подписи — 131 ключ: кнопки диалогов, тексты ошибок поля, расшифровки иконок для скринридера. Своего языка у кита нет и быть не может — его знает приложение.
Без единой настройки кит рисует английский набор, вшитый умолчанием: пустая aria-подпись
означала бы кнопку без имени для скринридера, а это хуже чужого языка. Свои подписи приложение
отдаёт функцией-переводчиком:
import { provideRtKitLabels, TRtKitLabelKey, TRtKitLabelParams } from '@rt-tools/ui-kit-v2';
provideRtKitLabels({
translator: computed(() => (key: TRtKitLabelKey, params?: TRtKitLabelParams) => myDictionary(key, params)),
locale: myLocale, // Signal<string>; ею кит форматирует даты
});Способ доставки — дело приложения: Transloco, $localize, собственный словарь. Кит знает
только ключи (TRtKitLabelKey — перечень выводится из английского набора, поэтому опечатка не
доживает до рантайма) и то, что на них отвечают готовой строкой: подстановки вида {{name}}
переводчик делает сам. Подпись, которой он не дал, берётся английской — поштучно, не набором.
Скругление одним входом
Каждый компонент с поверхностью принимает вход radius — шаг общей шкалы: none, xs, sm,
ms, md, lg, xl, 2xl, full. Без входа компонент скруглён по макету, и умолчание у
каждого своё.
<rt-card radius="none">…</rt-card>
<rt-input radius="full" />
<button rtButton label="Сохранить" radius="sm"></button>Шаг ложится на хост атрибутом data-rt-radius и переназначает своё свойство главной поверхности.
Вложенный компонент его не наследует: скруглённая карточка не скругляет кнопки внутри.
Внутренние части — точка, значок, кольцо фокуса — держат своё скругление. Круглую кнопку для
всего приложения задаёт настройка кита components.button.radius.
Входа нет у подтверждения, всплывающего уведомления, подсказки и просмотра фото: их рисует
директива или сервис поверх страницы, и тега для шага у приложения нет. Все шаги всех компонентов
показаны на странице витрины Foundation / Radius.
Настройка компонента снаружи
Компонент не берёт ступень шкалы там, где рисует себя: между ступенью и местом употребления
стоит своё свойство компонента — --rt-<блок>-<что>. Объявлено оно в корне блока, значением по
умолчанию стоит та самая ступень, поэтому без единой строки от приложения кит выглядит так же,
как выглядел.
Своими свойствами покрыты четыре рода назначений:
| Род | Имя свойства | Где применяется |
| ------------- | ----------------------------------- | --------------------------------------------------------------- |
| скругление | --rt-<блок>-*-radius | рамка блока, пузырь, значок, плашка, кольцо аватара |
| тень | --rt-<блок>-*-shadow | карточка, панель в перекрытии, кольцо фокуса, приподнятая точка |
| толщина рамки | --rt-<блок>-*-border-width | обводка, разделитель строк, дорожка, полоса под вкладкой |
| длительность | --rt-<блок>-*-transition-duration | переход цвета, фона, размера и сдвига |
Отступ, кегль и габариты остаются на ступенях намеренно: роль отступа одним словом не называется, и свойство под неё вышло бы именем места, а не именем роли.
Приложение переопределяет свойство правилом той же цели и большей силы — наследования тут мало, объявление живёт на самом блоке:
.my-app .rt-button {
--rt-btn-radius: 0;
}Часть, которую браузер уносит из поддерева блока — панель списка, меню, перетаскиваемая строка, — объявляет свойство на себе, и целиться в неё надо тоже отдельно:
.my-app .rt-select__panel {
--rt-select-panel-shadow: none;
}Что ни один компонент этот порядок не обошёл, проверяет pnpm run check:tokens-styles: ступень,
взятая в стилях компонента напрямую, роняет прогон. Показ на витрине — страница
Foundation / Design Tokens / Component Props.
Свой логотип
rt-logo рисует то, что даст приложение: своих начертаний кит не везёт.
:root {
--rt-logo-wordmark: url('/logo/wordmark.svg');
--rt-logo-tagline: url('/logo/tagline.svg');
--rt-logo-wordmark-dark: url('/logo/wordmark-inverse.svg');
--rt-logo-tagline-dark: url('/logo/tagline-inverse.svg');
}Пропорции своего файла компоненту сообщает вход aspect — без него ширина считается по
умолчанию варианта и узкое начертание обрастёт пустотой.
Разработка
pnpm run build:ui-kit-v2 # сборка пакета и CSS с токенами
pnpm exec nx test @rt-tools/ui-kit-v2
pnpm exec nx lint @rt-tools/ui-kit-v2
pnpm exec stylelint "projects/ui-kit-v2/**/*.scss"Переход на слой каскада
Стили кита переехали в слой rt-kit, и это меняет вид у тех, кто настраивал кит поверх. Ошибки
сборки не будет — узнаётся глазами, поэтому названо здесь прямо.
- Правило приложения теперь выигрывает у правила кита независимо от специфичности. Место, где кит перебивали поднятой специфичностью или обходом чужой вёрстки, продолжит работать, но надобности в этом больше нет: обычное правило сильнее любого правила из слоя.
- Правило кита теперь проигрывает и чужой библиотеке, чей CSS объявлен вне слоя. Тема
редактора текста подключается в подслой
vendor— импортом с именем подслоя; подключённая мимо, она перестраивает панель редактора. - Кит больше не настаивает на своём восклицательным знаком. Таблица в карточном виде
перестала перебивать
min-widthприложения: приложению, которому нужен и горизонтальный скролл, и карточный вид, остаётся сузить свой селектор до широкого экрана.
