Стили и контролы PortalUI
Как собирать разметку WebPart на встроенных классах портала, чтобы виджет выглядел как остальной UI (кнопки, поля, подсказки, сообщения) и корректно переживал светлую/тёмную тему.
Подключение своего CSS: манифест → CSS/JS.
Действия и data-wp-action: Интерфейс и API.
Формы списков: Кастомная форма.
0. Принципы
Заголовок раздела «0. Принципы»- Сначала классы портала, потом свой CSS. Кнопки, инпуты, ошибки и пустые состояния уже стилизованы глобально (
/ui/ui.min.css+ bridge). - Свой layout — под префиксом. Корневой контейнер
.webpart-my-widget, дальше BEM:.webpart-my-widget__toolbar. Не переопределяйте глобально.btn/.tbx__control. - Цвета — через CSS-переменные, не хардкод
#fff/#333. Иначе сломается тёмная тема и тема узла. - Иконки — Font Awesome уже на страницах портала (
fa-solid fa-…). Текст кнопки оборачивайте в<span class="btn__text">. - XSS — весь пользовательский текст через
HtmlEncode(см. хелперы в ui-and-api).
Минимальный каркас:
<div class="webpart-my-widget"> <p class="webpart-hint">Укажите список в настройках WebPart.</p>
<div class="webpart-my-widget__toolbar"> <button type="button" class="btn btn--settings btn--sm" data-wp-action="refresh"> <i class="fa-solid fa-rotate" aria-hidden="true"></i> <span class="btn__text">Обновить</span> </button> </div>
<form class="webpart-my-widget__form"> <label>Название <input class="tbx__control" type="text" name="title" required> </label> <button type="button" class="btn btn--primary btn--sm" data-wp-action="save"> <span class="btn__text">Сохранить</span> </button> </form></div>1. Кнопки — btn
Заголовок раздела «1. Кнопки — btn»База: класс btn + модификатор вида + размер.
Рекомендуемые комбинации для WebPart
Заголовок раздела «Рекомендуемые комбинации для WebPart»| Классы | Когда использовать |
|---|---|
btn btn--settings btn--sm | Основная кнопка панели виджета (обновить, фильтр, «Создать» в тулбаре). Самый частый паттерн в демо-модулях. |
btn btn--primary btn--sm | Главное действие формы / диалога (сохранить, создать заявку). |
btn btn--ghost btn--sm | Второстепенное: «Назад», «Отмена», ссылка-кнопка, сброс фильтра. |
btn btn--cancel btn--sm | Явная отмена в диалогах. |
btn btn--danger-muted btn--sm | Опасное, но не разрушительное действие (очистить, отклонить). |
btn btn--content btn--sm | Как settings — мягкий акцент (контентные действия). |
Размеры: btn--sm (в виджетах почти всегда), без модификатора — крупнее (редко в WebPart).
Пример: тулбар
Заголовок раздела «Пример: тулбар»<div class="webpart-demo__toolbar"> <button type="button" class="btn btn--settings btn--sm" data-wp-action="changePeriod" data-wp-delta="prev"> <span class="btn__text">‹</span> </button> <button type="button" class="btn btn--settings btn--sm" data-wp-action="changePeriod" data-wp-delta="next"> <span class="btn__text">›</span> </button> <button type="button" class="btn btn--primary btn--sm" data-wp-action="openCreate"> <i class="fa-solid fa-plus" aria-hidden="true"></i> <span class="btn__text">Создать</span> </button></div>Пример: первичное / вторичное в форме
Заголовок раздела «Пример: первичное / вторичное в форме»<div class="webpart-demo__actions"> <button type="button" class="btn btn--primary btn--sm" data-wp-action="save"> <i class="fa-solid fa-check" aria-hidden="true"></i> <span class="btn__text">Сохранить</span> </button> <button type="button" class="btn btn--ghost btn--sm" data-wp-action="cancel"> <span class="btn__text">Отмена</span> </button></div>Ссылка как кнопка
Заголовок раздела «Ссылка как кнопка»<a class="btn btn--sm btn--ghost" href="/portal/helpdesk/pages/desk">Рабочий стол</a>Не делайте: свои .my-btn { background: blue } для обычных действий — тема и hover уже в PortalUI.
2. Поля ввода — tbx__control
Заголовок раздела «2. Поля ввода — tbx__control»Единый вид для <input>, <select>, <textarea>:
<label>Название <input class="tbx__control" type="text" name="title" required placeholder="…"></label>
<label>Статус <select class="tbx__control" name="status"> <option value="new">Новая</option> </select></label>
<label>Описание <textarea class="tbx__control" name="description" rows="3"></textarea></label>
<label>Срок <input class="tbx__control" type="date" name="due"></label>
<label>Начало <input class="tbx__control" type="datetime-local" name="start_at"></label>
<label>Число <input class="tbx__control" type="number" name="amount" step="any"></label>Для многострочного иногда используют tbx__control tbx__control--area (если нужен вариант из kit) — в большинстве WebPart достаточно tbx__control на textarea.
Поиск:
<input type="search" class="tbx__control webpart-address-book__search-input" name="q" placeholder="Поиск…" data-wp-action="search">Свой класс (webpart-…__search-input) — только для ширины/отступов, не для «перекраски» рамки.
3. Подсказки, пусто и ошибки
Заголовок раздела «3. Подсказки, пусто и ошибки»| Класс | Назначение | Пример текста |
|---|---|---|
webpart-hint | Спокойная подсказка / не настроено | «Укажите список в настройках WebPart» |
webpart-empty | Нет данных (тот же визуал, что hint) | «Нет записей за выбранный период» |
form-error | Ошибка валидации / отказа API | «Не удалось сохранить» |
form-success | Успех короткой операции | «Сохранено» |
loading-text | Загрузка (часто рисует платформа) | «Загрузка…» |
<p class="webpart-hint">Укажите список в настройках WebPart.</p><p class="webpart-empty">Нет задач на этой неделе.</p><p class="form-error">Недостаточно прав для создания.</p><p class="form-success">Заявка создана.</p><p class="loading-text">Загрузка…</p>В C#:
$"<p class=\"webpart-hint\">{Escape(text)}</p>"$"<p class=\"form-error\">{Escape(text)}</p>"4. Сообщения-баннеры — msg
Заголовок раздела «4. Сообщения-баннеры — msg»Для более заметных уведомлений (как в админке):
<div class="msg msg--info">Синхронизация выполняется по расписанию.</div><div class="msg msg--success">Данные обновлены.</div><div class="msg msg--warning">Проверьте обязательные поля.</div><div class="msg msg--danger">Сервис временно недоступен.</div>| Модификатор | Смысл |
|---|---|
msg--info | Информация |
msg--success | Успех |
msg--warning | Предупреждение |
msg--danger | Ошибка / критично |
msg--neutral | Нейтральный блок |
В компактных виджетах чаще хватает form-error / webpart-hint; msg — когда нужен полноценный баннер.
5. Бейджи статуса — badge
Заголовок раздела «5. Бейджи статуса — badge»<span class="badge badge--ok">Активна</span><span class="badge badge--muted">Черновик</span>Свои статусы (цвета под бизнес-логику) — лучше под префиксом WebPart:
.webpart-demo__status--high { /* только цвет/фон статуса, опираясь на var(--ui-…) */ color: var(--color-danger, #b42318); background: var(--ui-danger-bg-soft, #fef3f2);}6. Группы полей — form-group (опционально)
Заголовок раздела «6. Группы полей — form-group (опционально)»Если нужна разметка «как в формах портала» (label сверху, отступ между полями):
<div class="form-group"> <label for="wp-title">Название</label> <input id="wp-title" class="tbx__control" name="title" type="text"></div><div class="form-group"> <label for="wp-status">Статус</label> <select id="wp-status" class="tbx__control" name="status">…</select></div>В демо-модулях часто проще: <label>Текст<input class="tbx__control"></label> без form-group — оба варианта допустимы; form-group удобен для длинных настроечных форм.
7. Карточки и панели — blk (по необходимости)
Заголовок раздела «7. Карточки и панели — blk (по необходимости)»Блоки PortalUI (blk, blk__header, blk__body, blk__footer) — для самостоятельной «карточки» внутри страницы. В WebPart на странице узел уже даёт оболочку; используйте blk, если виджет сам рисует несколько панелей:
<section class="blk webpart-demo__card"> <header class="blk__header">Сводка</header> <div class="blk__body"> <p class="webpart-hint">Нет данных за период.</p> </div> <footer class="blk__footer"> <button type="button" class="btn btn--ghost btn--sm" data-wp-action="refresh"> <span class="btn__text">Обновить</span> </button> </footer></section>Сетки, таймлайны, календари — почти всегда свой CSS под .webpart-…, а не blk.
8. Таблицы — tbl (по необходимости)
Заголовок раздела «8. Таблицы — tbl (по необходимости)»Для простой HTML-таблицы в стиле админки:
<div class="tbl-wrap"> <table class="tbl"> <thead> <tr><th>Название</th><th>Статус</th><th></th></tr> </thead> <tbody> <tr> <td>Заявка #12</td> <td><span class="badge badge--ok">Открыта</span></td> <td> <button type="button" class="btn btn--ghost btn--sm" data-wp-action="open" data-wp-id="12"> <span class="btn__text">Открыть</span> </button> </td> </tr> </tbody> </table></div>Сложные гриды (карточки сотрудников, таймлайн) — своя вёрстка + токены.
9. Прочие контролы kit (реже в WebPart)
Заголовок раздела «9. Прочие контролы kit (реже в WebPart)»Подключаются теми же глобальными стилями PortalUI; используйте, если нужен знакомый паттерн UI портала:
| Семейство | Назначение |
|---|---|
tg / tg-field | Переключатель (toggle) |
tb / tb__tab / tb__panel | Вкладки |
dd / dd__* | Выпадающее меню |
acc / acc__* | Аккордеон |
fu / fu__* | Зона загрузки файла |
ldr / ldr--* | Спиннеры / оверлей загрузки |
dv / dv-label | Разделитель с подписью |
Для типичной WebPart со списком и формой достаточно btn + tbx + hint/error. Остальное — по мере необходимости; сверяйтесь с разметкой админки/списков в работающем Portal.
10. CSS-переменные (тема)
Заголовок раздела «10. CSS-переменные (тема)»В своём assets/main.css опирайтесь на токены — тогда виджет следует теме портала и тёмному режиму.
Предпочтительные алиасы (удобны в WebPart)
Заголовок раздела «Предпочтительные алиасы (удобны в WebPart)»| Переменная | Смысл |
|---|---|
--color-text | Основной текст |
--color-text-muted | Приглушённый текст |
--color-border | Рамки, разделители |
--color-surface | Фон карточки / панели |
--color-surface-muted / --color-bg-subtle | Вторичный фон |
--color-bg | Фон области |
--color-primary | Акцент |
--color-danger | Ошибка / опасность |
Пример в своём CSS:
.webpart-my-widget { border: 1px solid var(--color-border, #e5e7eb); background: var(--color-surface, #fff); border-radius: var(--ui-radius-sm, 8px); padding: 0.75rem 1rem;}
.webpart-my-widget__title { color: var(--color-text, #111827); font-size: 1rem; margin: 0 0 0.5rem;}
.webpart-my-widget__meta { color: var(--color-text-muted, #6b7280); font-size: 0.8125rem;}Базовые токены kit: --ui-accent, --ui-text, --ui-border, --ui-surface, --ui-radius-sm, --ui-shadow-sm и др. Алиасы --color-* проксируют их в portal-bridge.css.
Не задавайте жёсткий color: #000 / background: #fff на корне виджета без fallback на переменную.
11. Свой CSS: правила
Заголовок раздела «11. Свой CSS: правила»- Файл:
assets/main.css→ в манифесте"styles": ["dist/main.css"](pack переименует вmain.v{version}.css). - Все селекторы начинаются с уникального корня:
.webpart-contoso-tasks. - Не пишите
.btn { … }и.tbx__control { border-color: red }глобально — только уточнения вроде:
.webpart-contoso-tasks .tbx__control { max-width: 20rem; /* layout, не тема */}- Модалки, сетки, таймлайн — целиком ваши классы; кнопки/поля внутри — портальные.
12. Сводка «что брать по умолчанию»
Заголовок раздела «12. Сводка «что брать по умолчанию»»| Задача | Классы |
|---|---|
| Кнопка в тулбаре | btn btn--settings btn--sm + btn__text |
| Главное действие формы | btn btn--primary btn--sm |
| Отмена / назад | btn btn--ghost btn--sm |
| Поле / select / textarea | tbx__control |
| Не настроено / пусто | webpart-hint / webpart-empty |
| Ошибка | form-error |
| Успех | form-success |
| Статус-чип | badge badge--ok / badge--muted |
| Layout / сетка | свой .webpart-…__* + var(--color-…) |
13. Антипаттерны
Заголовок раздела «13. Антипаттерны»| Плохо | Почему | Лучше |
|---|---|---|
<button style="background:#4f46e5"> | Ломает тему | btn btn--primary btn--sm |
| Свой инпут без классов | Другой радиус/бордер | tbx__control |
Глобальный .btn { padding: … } в пакете | Ломает весь портал на странице | Только под .webpart-… |
#fff / #111 в CSS | Тёмная тема | var(--color-surface) / var(--color-text) |
| Текст ошибки без класса | Незаметный / не в стиле | form-error или msg msg--danger |
Кнопка без type="button" в форме | Случайный submit | Всегда type="button" + data-wp-action |
14. Связанные документы
Заголовок раздела «14. Связанные документы»- Интерфейс, контролы и API —
data-wp-action, шаблоны, JS - Манифест — подключение
styles/scripts - Кастомная форма — поля списка на
tbx__control - Тема узла / токены — как тема влияет на
--ui-*