Слой дизайна
Фронтенд Wippy — это множество независимо публикуемых модулей, отрисовываемых в одно приложение. Два дома очевидны: тема, которую потребляет каждый surface, и модуль, который владеет самим собой. Промежуток между ними неочевиден, и именно в нём накапливается дублирование — идея, которую несколько модулей действительно разделяют, но компонента для неё в теме нет.
Эта страница называет три слоя, даёт критерий выбора между ними и показывает, как выглядит каждый выбор, когда он сделан верно и неверно.
Слои
| Слой | Достигает | Владеет |
|---|---|---|
| Тема | Каждого surface, включая модули, которыми вы не владеете | Компонентами PrimeVue, общими семантическими токенами, документированными классами |
| Общий слой дизайна | Только модулей, которые его подключили | Словарём, который эти модули разделяют и за которым нет компонента темы |
| Модуль | Самого себя | Тем, что действительно специфично для одного surface |
Тема универсальна, и это ограничение
Тема стилизует разметку, которой вы не владеете. Любой модуль — включая сторонний плагин, написанный тем, кто никогда не видел вашего приложения, — отрисовывается в тот же хост и красится той же темой. Именно это делает тему универсальным слоем, и работает это в обе стороны:
Ничто специфичное для приложения не может попасть в тему, потому что оно будет навязано каждому модулю, который об этом не просил.
Модуль не может зависеть от того, что в теме есть что-то специфичное для
приложения. Контракт — это компоненты PrimeVue + общие семантические токены
Wippy + документированные классы — ничего из того, что приложение добавило
сверху. Обратите внимание: собственные пресеты PrimeVue тоже не являются
контрактом: Wippy запускает PrimeVue с theme: 'none', поэтому опираться
следует на семантические токены Wippy.
/* ХОРОШО — общие семантические токены Wippy, присутствуют для каждого модуля */
.my-panel {
color: var(--p-text-color);
background: var(--p-content-background);
border: 1px solid var(--p-content-border-color);
}
/* ПЛОХО — токен, специфичный для приложения. Ваш модуль теперь работает только
внутри одного приложения и молча теряет объявление в любом другом месте:
неопределённое пользовательское свойство делает объявление невалидным на
этапе вычисления значения, поэтому оно отбрасывается, а элемент тихо
наследует значение. */
.my-panel { background: var(--kx-surface-2); }
Это же ответ на вопрос «можно ли положить наш общий словарь в фасад?» Только если он действительно должен достигать произвольной, не принадлежащей вам разметки. Если он ограничен вашим набором модулей, ему не место в теме — ему место в слое ниже.
Основа и когда компонент может от неё отказаться
PrimeVue и Tailwind в том виде, в котором их поставляет хост, — рекомендуемая основа для любого компонента. Компонент может от неё отказаться, но отказ сужается в тот момент, когда компонент отрисовывает что-либо привычное, и лестница идёт только в одну сторону:
| Компонент… | Тогда он должен загрузить |
|---|---|
| нейтрален к представлению — canvas, SVG, график без элементов управления, без токенов, без утилит, без прокрутки | ничего: hostCssKeys: [] |
| потребляет семантические токены или тёмную тему | themeConfigUrl |
| может прокручиваться | iframeCssUrl |
| отрисовывает markdown | markdownCssUrl |
| отрисовывает что-либо, что можно выразить средствами Tailwind | Tailwind — пишите утилиты, а не собственный CSS |
| отрисовывает что-либо, для чего PrimeVue поставляет компонент — кнопка, поле ввода, форма, таблица, диалог, меню, тег, всплывающая подсказка, любой элемент обратной связи | primeVueCssUrl и PrimeVuePlugin |
График на canvas — архетипический законный отказ: у него нет классического UI, поэтому основа ему не нужна вовсе. Добавьте тому же графику панель инструментов — и он больше не нейтрален к представлению: кнопка становится кнопкой PrimeVue, и вся интеграция приходит вместе с ней.
Обратите внимание на связку: утилиты Tailwind поставляются вместе с
primeVueCssUrl. Отдельного ключа host CSS для Tailwind нет, поэтому на
практике компонент, которому нужен Tailwind, загружает и ресурс PrimeVue.
(preflightCssUrl не входит в объединение ключей; если preflight Tailwind
действительно нужен внутри shadow root, загрузите его императивно — требуется
редко.)
Практическое следствие для этой страницы: большая часть того, что нужно модулю, уже есть в основе. Общий слой дизайна — узкая полоса над ней, а не место, где заново делают то, что PrimeVue и Tailwind уже покрывают. Механику см. в разделе Внедрение CSS.
Общий слой дизайна
Некоторые идеи повторяются в известном наборе модулей и не имеют компонента в теме: карточка контента, строка заголовка surface, то, что surface показывает, когда показывать нечего, размеры, в которых бывает тег. Реальные, общие и бездомные.
Они поставляются как опубликованный пакет, материализуемый в каждого потребителя на этапе сборки. Это должен быть именно пакет, а не алиас пути, потому что потребители живут в разных репозиториях — фальсифицируемая проверка для этого слоя в том, что модуль из другого репозитория, не имеющий доступа по пути к производителю, потребляет словарь и собирается.
Модуль-производитель объявляет пакет как артефакт этапа сборки, а каждый
потребитель материализует его в собственное дерево. Об объявлении, формате
node-package, о том, что среда исполнения согласует за вас, и о связующем
коде, который сборка всё же должна предоставить сама, см.
Артефакты этапа сборки.
Модуль
Всё остальное плюс каждое намеренное отклонение от общего словаря.
Как решить, куда что относится
Спрашивайте по порядку. Побеждает первое «да».
- Это значение? Цвет, радиус, отступ, тень, severity. → Тема. Читайте семантический токен. Никогда не литерал.
- Поставляет ли тема компонент для этого? Button, Dialog, Select, Tag. → Тема. Используйте компонент. Стилизуйте его, вешая класс на него — никогда не пересобирайте его.
- Нужна ли эта же концепция двум или более вашим модулям, и нет ли за ней компонента темы? → Общий слой дизайна.
- Иначе → Модуль.
Вопрос 2 — тот, на котором спотыкаются, и за ним стоит строгое правило.
Разобранные примеры
Приведённые ниже примеры взяты из Kickside — приложения на Wippy, CSS модулей которого на 15.4% состоял из точных клонов-дубликатов, прежде чем в нём появился этот слой.
Никогда не пересобирайте компонент темы
PrimeVue поставляет Button. Девять модулей Kickside отказались от него и
самостоятельно сделали .kx-btn на нативном <button>; семь других модулей
использовали компонент. Оба диалекта были локально разумны — просто не было
общего места, куда положить кнопку, поэтому половина приложения изобрела свою.
При сравнении друг с другом они совпадали по font-size и line-height и больше
ни по чему.
Плохо: нативный элемент button с .kx-btn .kx-btn-primary — вторая
реализация компонента, который тема уже поставляет. (Здесь он намеренно записан
как селектор: гейт документации отклоняет нативные продуктовые элементы
управления в примерах кода, и это то же правило, применённое слоем выше.)
Хорошо: компонент темы с классом на нём, когда его нужно поправить.
<Button label="Save" class="kx-save" />
Когда компонент темы не подходит, это не лицензия на его пересборку. Повесьте
класс на компонент и стилизуйте этот класс — в фасаде, если правка нужна всему
приложению, в модуле, если она локальна. Модуль knowledge в Kickside всё ещё
несёт .kn-btn / .kn-primary на нативных кнопках; это незавершённая
миграция, а не образец для копирования.
Severity принадлежит теме, а не вам
Severity — success, danger, warn, info — это семантика темы с
опубликованными шкалами. Kickside выводил её заново шестнадцать раз в четырёх
схемах именования (tone-gn, t-ok, kx-tone-success, tone-success).
Одно и то же имя класса означало три разных цвета в трёх модулях, поэтому
публикация любого одного определения молча перекрасила бы остальные.
/* ПЛОХО — severity выведен заново под локальным именем модуля */
.tone-gn { color: #16a34a; }
/* ХОРОШО — severity из темы */
.status-dot.success { background: var(--p-success-500); }
Tone всё же может существовать в общем слое — но только как декоративный цвет категории, никогда как severity. Если он может означать «это не удалось», это severity, и он принадлежит теме.
Общий словарь, для которого в теме нет места
/* ХОРОШО — PrimeVue не поставляет ни Card, ни surface Header, ни EmptyState.
Они повторяются в модулях, и за ними нет ничего из темы, так что это ровно
то, для чего существует общий слой. */
@import "@kickside/ui-kit/kx-card.css";
@import "@kickside/ui-kit/kx-state.css";
Принять — значит импортировать и удалить
CSS-правило @import должно предшествовать всем остальным правилам в таблице
стилей. Поэтому общая таблица всегда оказывается первой, и всё, что модуль
объявляет после неё, побеждает при равной специфичности. Модуль, который
импортирует пакет и сохраняет собственную копию, не изменил ровным счётом
ничего.
/* ПЛОХО — импорт инертен; локальная копия всё равно побеждает */
@import "@kickside/ui-kit/kx-card.css";
.kx-card { border-radius: 14px; border: 1px solid var(--p-content-border-color); }
/* ХОРОШО — импортируйте, удалите локальную копию, оставьте только
задокументированную дельту */
@import "@kickside/ui-kit/kx-card.css";
/* Карточки этого surface встроены в плотный список, поэтому теряют подъём. */
.kx-card:hover { transform: none; }
Оставляйте только дельту — никогда не переписывайте всё тело правила. И никогда не сводите два намерения к одному имени: если имя класса означает разное в двух модулях, это две концепции под одним именем. Разделите имя; не выбирайте победителя и не перекрашивайте проигравшего.
Специфичность против темы
CSS модуля внедряется в shadow root первым; таблица стилей PrimeVue из темы
добавляется после. Оба — элементы <style>, поэтому решает порядок в
документе, а тема идёт второй. Правилу модуля, которое должно победить класс
компонента темы, нужна большая специфичность — а не более поздняя строка в
файле. (adoptedStyleSheets несёт пользовательский CSS фасада, а не тему,
поэтому обращение к принятой таблице стилей здесь тоже не выигрывает.)
Сильнее всего это бьёт по сквозным классам, когда ваш класс попадает на элемент темы:
/* ПЛОХО — этот класс применён к собственному элементу footer PrimeVue, поэтому
при равной специфичности побеждает тема и padding никогда не применяется. */
.kx-modal-foot { padding: 14px 18px; }
/* ХОРОШО — ограничен корнем диалога, поэтому специфичнее темы */
.kx-modal > .kx-modal-foot { padding: 14px 18px; }
Что может содержать общий слой
Всё, что набор модулей действительно разделяет и чем тема не владеет: CSS-словарь, производные токены, внутренние компоненты, вспомогательный код, тестовую обвязку. Дублирование при этом одного и того же рода — в Kickside было девятнадцать копий одного тестового bootstrap наряду с клонированным CSS.
Поставляйте семантическими кусками. Каждая единица должна быть одной
названной концепцией, о которой потребитель может рассуждать, — kx-card,
kx-state, kx-tag. Предпочитайте более мелкие пакеты, чтобы потребитель брал
только нужное; один пакет, поставляющий несколько ясно названных единиц,
работоспособен, но это не та форма, к которой стоит стремиться.
Никогда не делайте свалку. Никаких common, shared, misc, utils.
Единица, имя которой не говорит, что внутри, накопит всё, чему больше некуда
было деться, и вы заново создадите проблему, ради решения которой этот слой
существует.
Нормализация — это визуальное изменение
Объединение разошедшихся копий двигает пиксели. В Kickside был селектор с девятнадцатью определениями в семнадцати различных телах. Сравните каждое тело, выберите канон, зафиксируйте, почему выбрали именно его, оставьте намеренное отклонение задокументированным переопределением — и посмотрите на результат. Модульные тесты не видят вёрстку.
Смежные материалы
- Темизация — каталог токенов и как тема достигает и хоста, и дочерних элементов
- Чек-лист соответствия — правила для модуля, по которым проверяется фронтенд
- Артефакты этапа сборки — объявление пакета и его материализация у потребителя
- Управление зависимостями — объявление и разрешение того, что потребляет модуль