Перейти к содержимому

Стили и контролы PortalUI

Как собирать разметку WebPart на встроенных классах портала, чтобы виджет выглядел как остальной UI (кнопки, поля, подсказки, сообщения) и корректно переживал светлую/тёмную тему.

Подключение своего CSS: манифест → CSS/JS.
Действия и data-wp-action: Интерфейс и API.
Формы списков: Кастомная форма.


  1. Сначала классы портала, потом свой CSS. Кнопки, инпуты, ошибки и пустые состояния уже стилизованы глобально (/ui/ui.min.css + bridge).
  2. Свой layout — под префиксом. Корневой контейнер .webpart-my-widget, дальше BEM: .webpart-my-widget__toolbar. Не переопределяйте глобально .btn / .tbx__control.
  3. Цвета — через CSS-переменные, не хардкод #fff / #333. Иначе сломается тёмная тема и тема узла.
  4. Иконки — Font Awesome уже на страницах портала (fa-solid fa-…). Текст кнопки оборачивайте в <span class="btn__text">.
  5. 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>

База: класс btn + модификатор вида + размер.

КлассыКогда использовать
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.


Единый вид для <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) — только для ширины/отступов, не для «перекраски» рамки.


КлассНазначениеПример текста
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>"

Для более заметных уведомлений (как в админке):

<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 — когда нужен полноценный баннер.


<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);
}

Если нужна разметка «как в формах портала» (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.


Для простой 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>

Сложные гриды (карточки сотрудников, таймлайн) — своя вёрстка + токены.


Подключаются теми же глобальными стилями 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.


В своём assets/main.css опирайтесь на токены — тогда виджет следует теме портала и тёмному режиму.

ПеременнаяСмысл
--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 на переменную.


  1. Файл: assets/main.css → в манифесте "styles": ["dist/main.css"] (pack переименует в main.v{version}.css).
  2. Все селекторы начинаются с уникального корня: .webpart-contoso-tasks.
  3. Не пишите .btn { … } и .tbx__control { border-color: red } глобально — только уточнения вроде:
.webpart-contoso-tasks .tbx__control {
max-width: 20rem; /* layout, не тема */
}
  1. Модалки, сетки, таймлайн — целиком ваши классы; кнопки/поля внутри — портальные.

ЗадачаКлассы
Кнопка в тулбареbtn btn--settings btn--sm + btn__text
Главное действие формыbtn btn--primary btn--sm
Отмена / назадbtn btn--ghost btn--sm
Поле / select / textareatbx__control
Не настроено / пустоwebpart-hint / webpart-empty
Ошибкаform-error
Успехform-success
Статус-чипbadge badge--ok / badge--muted
Layout / сеткасвой .webpart-…__* + var(--color-…)

ПлохоПочемуЛучше
<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