Уровни Framework
Framework устроен уровнями: от дизайн-токенов и утилит до Smart-компонентов и макетов. Здесь — что даёт каждый уровень и как выбрать нужный для задачи.
Уровни Framework
Framework устроен как лестница уровней. Каждый следующий опирается на предыдущие и не заменяет их.
| Уровень | Что даёт | Пример | Когда выбирать |
|---|---|---|---|
| Дизайн-токены (Core) | Общие величины: цвет, размер, отступ, шрифт, темы | --sf-primary, --sf-space-1 |
Всегда — это основа всего остального |
| Утилиты | Одно свойство элемента в одном классе | p-4, gap-2, bg-surface-container |
Раскладка и точечное оформление |
| Компоненты | Готовая структура и оформление повторяемого элемента | .sf-button |
Элемент без собственного поведения |
| Smart-компоненты | Собственный тег с поведением, состоянием и событиями | <sf-button>, <sf-tabs> |
Нужны ввод, состояние, события, данные |
| Составные Smart-компоненты | Несколько Smart-компонентов с общей логикой | <sf-datepicker>; таблица данных с поиском и фильтрами |
Повторяется целый сценарий |
| Макеты | Страница целиком как проверяемые данные | JSON-документ страницы | Структурой страницы нужно управлять |
Дизайн-токены и Core
Core — обязательная основа. Он содержит базовые стили, дизайн-токены, темы, порядок каскадных слоёв и Loader.
Дизайн-токен — именованная CSS-переменная с префиксом --sf-, которая
хранит одно дизайнерское решение. Токены устроены в два этажа:
- примитивы — сырые величины: ступени палитры (
--sf-primary-40,--sf-neutral-90) и базовые шаги размеров; - роли — смысловые величины, через которые работает интерфейс:
«основной цвет» (
--sf-primary), «текст на поверхности» (--sf-on-surface), «граница контрола» (--sf-outline), «отступ первой ступени» (--sf-space-1).
Роль ссылается на примитив. Например, в Core записано:
--sf-primary: light-dark(var(--sf-primary-40), var(--sf-primary-80));
Поэтому светлая и тёмная тема — не два набора стилей, а два значения одной
роли. Интерфейс пользуется ролями; примитивы напрямую в разметку не попадают.
Тему Core переключает классом theme-light или theme-dark на элементе
<html> (по умолчанию — по системной настройке).
Стили Framework разложены по каскадным слоям в фиксированном порядке:
sf.reset, sf.tokens, sf.base, sf.utilities, sf.components,
sf.overrides, sf.states. Порядок слоёв, а не случайная специфичность
селекторов, решает, какое правило сильнее. Подробно о токенах, цвете, темах и
слоях — в разделе Токены, цвет и темы (см. Дизайн-токены, Каскадные слои и Темы); как менять дизайн — в Как менять дизайн; о размерах
и лестнице — в Одна лестница размеров.
Утилиты
Утилита — короткий CSS-класс, который задаёт одно свойство: отступ, цвет,
скругление, расположение, размер шрифта. Значение утилита берёт из токенов:
gap-2 означает gap: var(--sf-space-2), а не «16 пикселей».
<div class="flex flex-col gap-2 p-4 bg-surface-0">
<h1 class="text-2">Заголовок</h1>
<p class="color-on-surface-variant">Текст страницы.</p>
</div>
Пример
Отступы и расположение элементов панели заданы одними утилитами.
У утилит нет префикса sf-: префикс носят компоненты. Утилита может действовать
по условию — например, начиная с ширины экрана: md:gap-2. Синтаксис, условия
и раскладка — см. Как устроено имя утилиты, Условия действия и Раскладка.
Компоненты
Компонент — устойчивая HTML-структура с готовым оформлением и вариантами
одного элемента: кнопки, поля, карточки, вкладок. Классы компонентов
начинаются с sf-, варианты пишутся через двойной дефис:
<button type="button" class="sf-button sf-button--default sf-button--primary sf-button--size-1">
<span class="sf-button-text-container">Сохранить</span>
</button>
Разметку компонента пишет проект; оформление и, если нужно, небольшое поведение (раскрыть, закрыть) приносит Framework. Компонент собран из тех же токенов, что и утилиты, поэтому подчиняется темам и общей лестнице размеров. Точное имя класса, варианты и состояния проверяйте на странице компонента в справочнике «Компоненты».
Пример
Подробно — см. Анатомия: корень, части, модификаторы, состояния и Настройки: общий язык компонентов.
Smart-компоненты
Smart-компонент — элемент интерфейса в виде собственного HTML-тега, который сам загружается, читает свои настройки, хранит состояние, перерисовывается и сообщает приложению о действиях человека событиями.
<sf-button text="Сохранить" scheme="primary" size="1"></sf-button>
Например, фильтр каталога показывает выбранные параметры и сообщает приложению, что человек изменил выбор. Приложение получает событие, запрашивает подходящие товары и передаёт новые данные обратно в интерфейс.
<sf-button text="Сохранить" scheme="primary"></sf-button>
Обычный компонент отвечает на вопрос «как элемент выглядит», Smart-компонент добавляет «как он себя ведёт». Своего оформления Smart-компонент не изобретает: он строит ту же разметку с теми же классами компонентов. Поэтому Smart-кнопка и кнопка на классах выглядят одинаково.
Настройки у всех уровней называются одинаково: appearance (как выглядит),
status (о чём сообщает), kind (чем является), size (ступень лестницы),
scheme (какой смысловой цвет). Каждый Smart-компонент описан манифестом —
машиночитаемым файлом с его настройками, событиями, состояниями и уровнем
готовности. Подробно — в разделе Smart-компоненты (см. Что это и что даёт и Манифест).
Составные Smart-компоненты
Составной Smart-компонент собран из других Smart-компонентов и согласует
их работу в одной пользовательской задаче. Таблица данных со строкой поиска,
фильтрами, сортировкой и постраничным выводом — один такой компонент, а не
четыре отдельных, которые проект связывает вручную. Выбор даты
(<sf-datepicker>) — тоже составной компонент: поле, кнопки и календарь в нём
работают как одно целое.
Составной выбор товара может связать варианты, цену, доступность и кнопку покупки: изменение варианта обновляет остальные части, а оформление и взаимодействие остаются одним решением.
<project-order></project-order>
<p id="order-log" role="status">Выберите количество в карточке.</p>
async function start() {
if (typeof window.SF?.loadSmartBase !== 'function') {
await new Promise((resolve, reject) => {
const ready = () => { clearTimeout(timeout); resolve(); };
const timeout = setTimeout(() => { window.removeEventListener('sf-loader-init', ready); reject(new Error('Loader не запустился')); }, 15000);
window.addEventListener('sf-loader-init', ready, { once: true });
});
}
await SF.loadSmartBase();
const { SfBaseElement, html } = SF.smart;
class ProjectQuantity extends SfBaseElement {
static get props() { return { count: { type: Number, default: 1 } }; }
template() {
return html`<button type="button" @click=${() => {
const value = this.getProp('count') + 1;
this.setAttribute('count', String(value));
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true, detail: { value } }));
}}>Количество: ${this.getProp('count')}</button>`;
}
}
class ProjectProduct extends SfBaseElement {
template() { return html`<article class="p-2 bg-surface-container radius-2"><h3>Кружка</h3><project-quantity></project-quantity></article>`; }
}
class ProjectOrder extends SfBaseElement {
template() { return html`<section aria-label="Заказ"><h2>Мой заказ</h2><project-product></project-product></section>`; }
}
ProjectQuantity.define('project-quantity');
ProjectProduct.define('project-product');
ProjectOrder.define('project-order');
document.querySelector('project-order').addEventListener('change', event => {
document.querySelector('#order-log').textContent = `Заказ получил событие от ${event.target.localName}: ${event.detail.value}`;
});
}
start().catch(error => { document.querySelector('#order-log').textContent = error.message; });
Составной компонент отвечает за согласованность частей. Данные, права и решения остаются у проекта: компонент сообщает намерение («человек хочет удалить эти записи»), а приложение решает и возвращает результат. Подробно — см. Составные компоненты и Связь с приложением: порты хоста и намерения.
Составной Smart-компонент принадлежит своему владельцу. Framework содержит универсальные составные компоненты. Составной компонент, который нужен одному продукту, — например, представление записей его административного интерфейса, — продукт хранит и поставляет сам, в пакете продукта (см. Пакеты компонентов продуктов). С данными приложения такой компонент работает через публичный порт Framework (см. Порт хоста).
Макеты и блоки
Макет — описание страницы в виде проверяемого JSON-документа (схема
simai.composition.document). Он хранит, из каких секций, колонок
и компонентов состоит страница и в каком порядке они идут, а Core превращает
это дерево в HTML через браузерный интерфейс SF.Composition. Макет хранит
ссылку на данные, но не сами данные, права или операции.
Макет нужен не всегда. Обычная страница пишется HTML-разметкой. Макет добавляют, когда структуру страницы нужно хранить, передавать между системами, редактировать в инструменте или собирать по настройкам сайта. Подробно — в разделе Макеты и композиция.
Пример: в макете каталога корневой узел описывает страницу, секция содержит заголовок, а колонки разделяют фильтры и карточки товаров. Ссылка на данные говорит проекту, какие товары получить; сами товары, права и операции в документ не записываются.
{
"schema": "simai.composition.document",
"schema_version": 1,
"id": "catalog-page",
"profile": "ui-layout",
"locale": "ru",
"root": {
"id": "page",
"type": "layout.page",
"slots": {
"default": [
{
"id": "catalog",
"type": "layout.section",
"presentation": {"preset": "surface", "modifiers": ["contained"]},
"bindings": [{"owner": "project/catalog", "ref": "products", "target": "items", "revision": "42"}],
"slots": {
"default": [
{"id": "title", "type": "content.heading", "data": {"level": 1, "content": [{"type": "text", "value": "Каталог товаров"}]}},
{"id": "lead", "type": "content.paragraph", "data": {"content": [{"type": "text", "value": "Framework показывает макет, а проект получает товары и проверяет права."}]}},
{
"id": "columns",
"type": "layout.columns",
"props": {"columns": 2},
"presentation": {"modifiers": ["responsive"]},
"slots": {"columns": [
{"id": "filters", "type": "content.paragraph", "data": {"content": [{"type": "text", "value": "Фильтры"}]}},
{"id": "products", "type": "content.paragraph", "data": {"content": [{"type": "text", "value": "Карточки товаров"}]}}
]}
}
]
}
}
]
}
}
}
Блок — пример компоновки на определённую тему: первый экран, список преимуществ, товарная выдача. Проект хранит такой пример и повторно использует его как шаблон Recipe (см. Шаблоны, вставки и варианты). Блок — не тип Framework и не отдельный уровень: у него нет своей схемы, он состоит из тех же секций, колонок, компонентов и Smart-компонентов, что и любой макет.
Пример ниже — проектный блок на HTML: заголовок, описание, цена и кнопка. Он не объявляет новый тип Framework.
<section class="p-3 bg-surface-container radius-3" aria-label="Предложение"><h2>Кружка</h2><p>Керамическая кружка, 300 мл.</p><strong>790 ₽</strong><sf-button text="Подробнее" scheme="primary"></sf-button></section>
Как уровни опираются друг на друга
Одну и ту же кнопку можно проследить через все уровни:
- Токен
--sf-primaryхранит основной цвет; в тёмной теме у него другое значение. - Утилиты и стили компонента берут цвет и отступы из токенов, а не из чисел.
- Класс
sf-buttonсобирает из этого готовую кнопку. - Тег
<sf-button>строит ту же разметку с классомsf-buttonи добавляет поведение: состояние загрузки, блокировку, события. - Составной компонент ставит такие кнопки в панель действий и связывает их с выбором строк таблицы.
- Макет говорит, на каком месте страницы стоит эта таблица.
Отсюда главное свойство Framework: изменение на нижнем уровне доходит до верхних само. Новое значение роли меняет утилиты, компоненты и Smart-компоненты сразу; проекту ничего переписывать не нужно.
Правило выбора уровня
Берите самый простой уровень, который решает задачу целиком и без дублирования.
- Нужно одно свойство (отступ, цвет, выравнивание) — утилита.
- Элемент повторяется и должен выглядеть одинаково — компонент.
- Элементу нужны ввод, состояние, события или данные — Smart-компонент.
- Повторяется целый сценарий из нескольких интерактивных частей — составной Smart-компонент.
- Структуру страницы нужно хранить и собирать как данные — макет.
Обратное тоже верно: не берите уровень сложнее, чем нужно. Статичную подпись не стоит делать Smart-компонентом, а единственный отступ — новым компонентом. Внутри компонента точечную поправку делает утилита.