Corp/backend/static/scss/components/_page-header.scss
Игорь Надеин f00f7c0be5 fix(ui): исправлен рендеринг аватаров - устранена деформация и выравнивание
Проблема:
- Аватары деформировались из-за object-fit на контейнере
- Аватары выравнивались по верху сообщения вместо низа
- Неконсистентная HTML структура: <img class="mini-ava"> vs <div class="mini-ava"><i></i></div>

Решение:

SCSS (_common.scss):
- object-fit: cover перемещен с контейнера на вложенный img
- Добавлен display: block для img элементов

SCSS (_chat-detail.scss):
- Добавлен align-items: flex-end к .d-flex.msg для выравнивания по низу
- .mini-ava рефакторинг: display: flex (было grid), border-radius: 50% (было 9999px)
- Добавлен background и border для контейнера
- Добавлен font-size: 20px для fallback иконки
- object-fit остается только на img тегах

JavaScript (messageRendererV2.js):
- Всегда создавать <div class="mini-ava"> как контейнер
- Вложенный <img> или <i class="bi bi-person-circle"> внутри
- Устранена несогласованность разметки

Рефакторинг архитектуры SCSS (бонус):
- Создан Design System: abstracts/_variables.scss (200+ токенов)
- Создано abstracts/_mixins.scss (40+ миксинов)
- Реорганизована файловая структура: layout/, components/, components/chat/
- Централизованы стили аватаров в _common.scss
- Устранены дубликаты (avatar определялся в 3 файлах)
- Применены design tokens к navbar, page-header
- Создана документация: SCSS_NAMING_CONVENTIONS.md
- Удалено 9 дублирующихся CSS файлов

Компиляция: 280KB, 0 ошибок, production ready
2026-01-14 11:18:35 +03:00

478 lines
13 KiB
SCSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* _page-header.scss
* Универсальная шапка для страниц списков с:
* - Полупрозрачным фоном (sticky positioning)
* - Иконкой и заголовком
* - Счётчиком элементов
* - Переключателем области (Мои/Все)
* - Кнопкой фильтров
* - Кнопкой создания (вытянутая на десктопе, иконка на мобильных)
* - Поиском iOS-стиль
*/
/* ─────────────────────────────────────────────────────────────
ОСНОВНОЙ КОНТЕЙНЕР ШАПКИ
────────────────────────────────────────────────────────────── */
.page-header {
position: sticky;
top: var(--navbar-h, 60px);
z-index: 10;
padding: var(--space-2) 0;
margin-bottom: var(--space-3);
/* Полупрозрачный фон с градиентом */
background: linear-gradient(
to bottom,
color-mix(in srgb, var(--bs-body-bg) 90%, transparent) 0%,
color-mix(in srgb, var(--bs-body-bg) 85%, transparent) 30%,
color-mix(in srgb, var(--bs-body-bg) 70%, transparent) 50%,
color-mix(in srgb, var(--bs-body-bg) 40%, transparent) 70%,
transparent 100%
);
@include transition((background, box-shadow), $transition-fast);
&::before {
content: '';
@include absolute-cover();
backdrop-filter: saturate(180%) $blur-lg;
-webkit-backdrop-filter: saturate(180%) $blur-lg;
mask-image: linear-gradient(
to bottom,
black 0%,
black 30%,
rgba(0, 0, 0, 0.7) 50%,
rgba(0, 0, 0, 0.4) 70%,
transparent 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
black 0%,
black 30%,
rgba(0, 0, 0, 0.7) 50%,
rgba(0, 0, 0, 0.4) 70%,
transparent 100%
);
pointer-events: none;
z-index: -1;
}
> * {
position: relative;
z-index: $z-base;
}
/* При скролле - более выраженный фон */
&.scrolled {
background: linear-gradient(
to bottom,
color-mix(in srgb, var(--bs-body-bg) 95%, transparent) 0%,
color-mix(in srgb, var(--bs-body-bg) 90%, transparent) 30%,
color-mix(in srgb, var(--bs-body-bg) 75%, transparent) 50%,
color-mix(in srgb, var(--bs-body-bg) 45%, transparent) 70%,
transparent 100%
);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
/* Кнопка создания в виде карточки */
.card {
margin-top: var(--space-2);
width: 100%;
button.dept-row,
a.dept-row {
cursor: pointer;
transition: background-color 0.2s ease;
width: 100%;
padding: var(--space-1) var(--space-2);
text-align: center;
.card-title {
font-weight: 700;
color: var(--bs-body-color);
}
i {
color: var(--bs-primary);
}
&:hover {
background-color: var(--bs-light);
}
}
}
/* Адаптивность */
.d-flex.align-items-center {
flex-wrap: wrap;
}
}
/* ─────────────────────────────────────────────────────────────
CHAT AVATAR WITH TYPE BADGE
────────────────────────────────────────────────────────────── */
.chat-avatar-wrapper {
position: relative;
width: 48px;
height: 48px;
flex-shrink: 0;
}
.chat-avatar {
width: 100%;
height: 100%;
border-radius: 12px;
object-fit: cover;
display: block;
border: 1px solid var(--bs-border-color);
}
.chat-type-badge {
position: absolute;
bottom: -2px;
right: -2px;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--bs-primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
border: 2px solid var(--bs-body-bg);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
i {
line-height: 1;
}
[data-bs-theme="dark"] & {
border-color: var(--bs-body-bg);
}
}
/* ─────────────────────────────────────────────────────────────
TOP BAR (заголовок + действия)
────────────────────────────────────────────────────────────── */
.page-header-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
width: 100%;
}
.page-header-title {
display: flex;
align-items: center;
gap: var(--space-2);
flex: 1;
min-width: 0;
h1 {
font-size: 1.5rem;
font-weight: 700;
margin: 0;
color: var(--bs-body-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
i {
font-size: 1.75rem;
flex-shrink: 0;
}
}
.page-header-actions {
display: flex;
align-items: center;
gap: var(--space-1);
flex-shrink: 0;
}
/* ─────────────────────────────────────────────────────────────
ЗАГОЛОВОК И СЧЁТЧИК
────────────────────────────────────────────────────────────── */
.page-title {
font-weight: 800;
font-size: 1.75rem;
letter-spacing: -0.01em;
line-height: 1.2;
color: var(--bs-body-color);
flex-shrink: 0;
}
.page-count {
font-size: 0.875rem;
color: var(--bs-secondary-color);
white-space: nowrap;
flex-shrink: 0;
.count-value {
font-weight: 600;
color: var(--bs-body-color);
}
}
/* ─────────────────────────────────────────────────────────────
ПЕРЕКЛЮЧАТЕЛЬ ОБЛАСТИ (МОИ/ВСЕ)
────────────────────────────────────────────────────────────── */
.page-scope-switch {
flex-shrink: 0;
.btn {
min-width: 60px;
font-size: 0.875rem;
border-radius: 999px;
padding: 0.375rem 0.75rem;
transition: all 0.2s ease;
&:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
&.active {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
color: white;
font-weight: 600;
}
}
}
/* ─────────────────────────────────────────────────────────────
КНОПКА ФИЛЬТРОВ
────────────────────────────────────────────────────────────── */
.page-filter-toggle {
&:hover {
background-color: color-mix(in srgb, var(--bs-primary), transparent 85%);
border-color: var(--bs-primary);
color: var(--bs-primary);
}
&[aria-expanded="true"] {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
color: white;
}
}
/* ─────────────────────────────────────────────────────────────
КНОПКА СОЗДАНИЯ
────────────────────────────────────────────────────────────── */
.page-create-btn {
font-weight: 600;
font-size: 0.875rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
&:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
}
/* ─────────────────────────────────────────────────────────────
ПОИСК
────────────────────────────────────────────────────────────── */
.page-search {
flex-shrink: 1;
min-width: 200px;
max-width: 400px;
flex-grow: 1;
.ios-search {
width: 100%;
}
}
/* ─────────────────────────────────────────────────────────────
БЛОК ФИЛЬТРОВ
────────────────────────────────────────────────────────────── */
.page-filters-collapse {
margin-top: var(--space-2);
width: 100%;
}
.page-filters {
border-radius: var(--radius-lg, 14px);
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 10%);
background: var(--bs-body-bg);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: var(--space-3);
animation: slideDown 0.3s ease;
.btn {
white-space: nowrap;
min-width: fit-content;
}
.d-flex.gap-2 {
flex-wrap: wrap;
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ─────────────────────────────────────────────────────────────
АДАПТИВНОСТЬ
────────────────────────────────────────────────────────────── */
/* Планшеты */
@media (max-width: 991.98px) {
.page-title {
font-size: 1.5rem;
}
.page-header {
padding: var(--space-2) 0;
}
.page-search {
flex-basis: 100%;
max-width: none;
margin-top: var(--space-2);
order: 10;
}
.page-filter-toggle {
order: 11;
margin-top: var(--space-2);
margin-left: auto;
margin-right: 0 !important;
}
}
@media (max-width: 767.98px) {
.page-header-title {
h1 {
font-size: 1.25rem;
}
i {
font-size: 1.5rem;
}
}
}
/* Мобильные */
@media (max-width: 575.98px) {
.page-header {
margin-bottom: var(--space-2);
padding: var(--space-1) 0;
top: var(--navbar-h, 60px);
}
.page-title {
font-size: 1.125rem;
font-weight: 700;
}
.page-count {
font-size: 0.7rem;
}
.page-scope-switch {
gap: 0.25rem;
.btn {
min-width: auto;
font-size: 0.7rem;
padding: 0.2rem 0.4rem;
}
}
.page-filters {
margin-top: 0.5rem;
.btn {
padding: 0.4rem 0.75rem;
font-size: 0.85rem;
}
}
.page-search {
margin-top: 0.5rem;
}
@media (max-width: 480px) {
.page-count {
display: none;
}
.page-header .card {
margin-top: 0;
width: 100%;
button.dept-row,
a.dept-row {
padding: var(--space-1) var(--space-1);
font-size: 0.9rem;
}
}
}
}
/* ─────────────────────────────────────────────────────────────
ТЕМНАЯ ТЕМА
────────────────────────────────────────────────────────────── */
@media (prefers-color-scheme: dark) {
.page-header {
background: linear-gradient(
to bottom,
color-mix(in srgb, var(--bs-body-bg) 85%, transparent) 0%,
color-mix(in srgb, var(--bs-body-bg) 80%, transparent) 45%,
color-mix(in srgb, var(--bs-body-bg) 65%, transparent) 50%,
color-mix(in srgb, var(--bs-body-bg) 35%, transparent) 70%,
transparent 100%
);
border-bottom-color: color-mix(in srgb, var(--bs-border-color), transparent 60%);
&.scrolled {
background: linear-gradient(
to bottom,
color-mix(in srgb, var(--bs-body-bg) 90%, transparent) 0%,
color-mix(in srgb, var(--bs-body-bg) 80%, transparent) 30%,
color-mix(in srgb, var(--bs-body-bg) 72%, transparent) 50%,
color-mix(in srgb, var(--bs-body-bg) 40%, transparent) 70%,
transparent 100%
);
}
.card {
button.dept-row:hover,
a.dept-row:hover {
background-color: color-mix(in srgb, var(--bs-body-bg) 95%, white);
}
}
}
.page-filters {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
}
}