Corp/backend/static/css/components/chat-detail.css
Игорь Надеин 4a4f3b2fa0 refactor: Clean architecture + unified badge styling
PART 1: Extract UnreadBadgeManager (Clean Architecture)

Created managers/unreadBadgeManager.js:
✅ Single Responsibility: Manages ONLY badge UI
✅ Methods: increment(), reset(), setCount(), hasUnread()
✅ Handles all DOM manipulation internally
✅ Reusable and testable

Cleaned ChatControllerV2:
❌ Removed 150+ lines of UI logic
❌ Removed duplicate systems (_unreadCount + _newMessagesCount)
❌ Removed deprecated methods:
   - _incrementUnreadBadge()
   - _resetUnreadBadge()
   - _showNewMessagesIndicator()
   - _hideNewMessagesIndicator()
   - _findOrCreateNewMessagesButton()
✅ Now uses: this.badgeManager.increment/reset()

PART 2: Unified Badge Styling

Aligned unread-badge with sticky-date design:
- Same typography (font-size, weight, line-height)
- Same spacing (padding: 6px 12px)
- Same border-radius (10-12px)
- Same shadows (box-shadow hierarchy)
- Same transitions (0.2s ease)
- Same animation patterns (scale, hover effects)
- Added border: 2px for contrast with background

Result:
✅ Consistent UI across all badges
✅ Matches Telegram-like design system
✅ Better visual hierarchy
✅ ~950 lines in Controller (was 1125)
✅ Clean separation of concerns
2026-01-13 15:36:17 +03:00

717 lines
18 KiB
CSS
Raw 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.

/**
* chat-detail.css
* Чистая архитектура чата в Telegram/WhatsApp стиле
*/
/* ═══════════════════════════════════════════════════════════
БАЗОВАЯ СТРУКТУРА
═══════════════════════════════════════════════════════════ */
/* КРИТИЧНО: Убираем все внешние скроллы */
body:has(.chat-page) {
overflow: hidden !important;
padding-bottom: 0 !important;
}
body:has(.chat-page) main {
overflow: hidden !important;
padding: 0 !important;
margin: 0 !important;
}
/* Скрываем bottom navbar на странице чата */
body:has(.chat-page) .bottom-navbar {
display: none;
}
/* Страница чата на всю высоту экрана */
.chat-page {
display: flex;
flex-direction: column;
height: calc(100vh - var(--navbar-h));
width: 100%;
max-width: 100%;
overflow: hidden;
background: var(--bs-body-bg);
position: relative;
}
@supports(height: 100svh) {
.chat-page {
height: calc(100svh - var(--navbar-h));
}
}
.chat-header,
.chat-container {
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
.chat-container {
border-radius: var(--bs-border-radius-lg);
border: 1px solid var(--bs-border-color);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
}
/* ═══════════════════════════════════════════════════════════
ШАПКА ЧАТА
═══════════════════════════════════════════════════════════ */
.chat-header {
flex-shrink: 0;
background: var(--bs-body-bg);
padding: var(--space-2) var(--space-3);
position: sticky;
top: 0;
z-index: 1020;
border-bottom: 1px solid var(--bs-border-color);
}
@media (min-width: 768px) {
.chat-header {
padding: var(--space-3) var(--space-4);
border-bottom: none;
}
}
@media (max-width: 767.98px) {
.chat-header {
padding: var(--space-1) var(--space-2);
}
}
.chat-header-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
}
.chat-header-title {
display: flex;
align-items: center;
gap: var(--space-2);
flex: 1;
min-width: 0;
}
.chat-header-title h1 {
font-size: 1.25rem;
font-weight: 700;
margin: 0;
color: var(--bs-body-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-header-title i {
font-size: 1.5rem;
flex-shrink: 0;
}
.chat-header-actions {
display: flex;
align-items: center;
gap: var(--space-1);
flex-shrink: 0;
}
/* Аватар чата с бейджем */
.chat-avatar-wrapper {
position: relative;
width: 40px;
height: 40px;
flex-shrink: 0;
}
.chat-avatar {
width: 100%;
height: 100%;
border-radius: 10px;
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: 10px; /* Идеальный круг как в Telegram */
background: var(--bs-danger); /* Красный как непрочитанные */
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
line-height: 1;
border: 2px solid var(--bs-body-bg);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); /* Мягкая тень */
transition: transform 0.2s ease, background-color 0.2s ease;
}
.chat-type-badge:empty {
display: none;
}
.chat-type-badge.badge-primary {
background: var(--bs-primary);
}
.chat-type-badge.badge-muted {
background: var(--bs-secondary);
}
/* Анимация появления как в Telegram */
@keyframes badge-scale-in {
from {
transform: scale(0);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.chat-type-badge.new {
animation: badge-scale-in 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@media (max-width: 767.98px) {
.chat-header {
padding: var(--space-1) var(--space-2);
}
.chat-header-title h1 {
font-size: 1.1rem;
}
.chat-header-title i {
font-size: 1.25rem;
}
.chat-header-actions .btn-sm span {
display: none;
}
}
/* ═══════════════════════════════════════════════════════════
КОНТЕЙНЕР ЧАТА
═══════════════════════════════════════════════════════════ */
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
position: relative;
}
/* ═══════════════════════════════════════════════════════════
EMOJI PICKER
═══════════════════════════════════════════════════════════ */
.chat-emoji-picker {
position: absolute;
bottom: 100%;
right: 0;
margin-bottom: var(--space-2);
z-index: 1050;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-radius: var(--bs-border-radius-lg);
max-width: 90vw;
}
@media (min-width: 768px) {
.chat-emoji-picker {
right: var(--space-3);
}
}
/* Скрываем по умолчанию, показываем через JS */
.chat-emoji-picker:not([data-visible="true"]) {
display: none;
}
/* ═══════════════════════════════════════════════════════════
ОБЛАСТЬ СООБЩЕНИЙ
═══════════════════════════════════════════════════════════ */
.chat-messages-scroll {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: var(--space-3);
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 70%) transparent;
/* CSS Containment для оптимизации (как в Telegram) */
contain: layout style paint;
will-change: scroll-position;
/* GPU acceleration для плавного scroll */
transform: translateZ(0);
backface-visibility: hidden;
perspective: 1000px;
}
.chat-messages-scroll:hover {
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 55%) transparent;
}
.chat-messages-scroll::-webkit-scrollbar {
width: 8px;
}
.chat-messages-scroll::-webkit-scrollbar-track {
background: transparent;
}
.chat-messages-scroll::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--bs-body-color), transparent 75%);
border-radius: 4px;
}
.chat-messages-scroll:hover::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--bs-body-color), transparent 55%);
}
@media (hover: none), (pointer: coarse) {
.chat-messages-scroll {
scrollbar-width: none;
}
.chat-messages-scroll::-webkit-scrollbar {
display: none;
}
}
@media (max-width: 767.98px) {
.chat-messages-scroll {
padding: var(--space-2);
}
}
/* ═══════════════════════════════════════════════════════════
ИНДИКАТОР ПЕЧАТИ
═══════════════════════════════════════════════════════════ */
.chat-typing-indicator {
flex-shrink: 0;
padding: var(--space-1) var(--space-3);
font-size: 0.875rem;
color: var(--bs-secondary);
min-height: 32px;
display: flex;
align-items: center;
gap: var(--space-1);
}
.chat-typing-indicator.d-none {
display: none !important;
}
/* ═══════════════════════════════════════════════════════════
КОМПОЗЕР (ФОРМА ВВОДА)
═══════════════════════════════════════════════════════════ */
.chat-composer-wrapper {
flex-shrink: 0;
padding: var(--space-3) var(--space-3);
padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
background: var(--bs-body-bg);
border-top: 1px solid var(--bs-border-color);
position: relative;
z-index: 1010;
}
@media (min-width: 768px) {
.chat-composer-wrapper {
padding: var(--space-3) var(--space-4);
padding-bottom: var(--space-3);
border-radius: 0 0 var(--bs-border-radius-lg) var(--bs-border-radius-lg);
}
}
@media (max-width: 767.98px) {
.chat-composer-wrapper {
padding: var(--space-2) var(--space-2);
.message-field {
display: flex;
align-items: flex-end;
gap: var(--space-1);
width: 100%;
}
.composer-input-group {
flex: 1;
display: flex;
align-items: flex-end;
gap: var(--space-1);
background: var(--bs-tertiary-bg);
border-radius: var(--bs-border-radius-lg);
padding: var(--space-1);
border: 1px solid color-mix(in srgb, var(--bs-border-color), transparent 50%);
transition: border-color 0.2s;
}
.composer-input-group:focus-within {
border-color: var(--bs-primary);
}
.message-input {
flex: 1;
resize: none;
border: none;
background: transparent;
outline: none;
font-size: 1rem;
line-height: 1.5;
padding: var(--space-1);
max-height: 120px;
overflow-y: auto;
scrollbar-width: thin;
color: var(--bs-body-color);
}
.message-input::placeholder {
color: var(--bs-secondary-color);
}
.message-input::-webkit-scrollbar {
width: 4px;
}
.message-input::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--bs-body-color), transparent 70%);
border-radius: 2px;
}
.composer-actions {
display: flex;
gap: 2px;
align-items: center;
}
.btn-send {
background: var(--bs-primary);
color: white;
border: none;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
padding: 0;
}
.btn-send:hover:not(:disabled) {
background: color-mix(in srgb, var(--bs-primary), black 10%);
transform: scale(1.05);
color: white;
}
.btn-send:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-attachment,
.message-icon-btn {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
color: var(--bs-body-color);
background: transparent;
border: none;
padding: 0;
}
.btn-attachment:hover,
.message-icon-btn:hover {
background: color-mix(in srgb, var(--bs-body-color), transparent 90%);
}
/* ═══════════════════════════════════════════════════════════
СООБЩЕНИЯ (BUBBLES)
═══════════════════════════════════════════════════════════ */
.bubble {
border-radius: 16px;
padding: var(--space-1) var(--space-2);
word-break: break-word;
white-space: pre-wrap;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
max-width: 75%;
}
@media (max-width: 767.98px) {
.bubble {
max-width: 85%;
}
}
.bubble-me {
background: color-mix(in srgb, var(--bs-primary) 92%, #fff 0%);
color: #fff;
}
.bubble-other {
background: var(--bs-secondary-bg);
color: var(--bs-body-color);
}
.mini-ava {
width: 32px;
height: 32px;
border-radius: 9999px;
overflow: hidden;
display: grid;
place-items: center;
background: var(--bs-secondary-bg);
flex-shrink: 0;
}
.mini-ava img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* ═══════════════════════════════════════════════════════════
ВЛОЖЕНИЯ
═══════════════════════════════════════════════════════════ */
.message-attachments {
margin-top: var(--space-1);
}
.attachment-preview {
padding: var(--space-1) 0;
max-height: 200px;
overflow-y: auto;
}
.attachment-item {
background: var(--bs-secondary-bg);
border-radius: var(--bs-border-radius);
padding: var(--space-1);
transition: background-color 0.15s;
}
.attachment-item:hover {
background: var(--bs-tertiary-bg);
}
.chat-media {
max-width: 100%;
max-height: 400px;
border-radius: 12px;
}
/* ═══════════════════════════════════════════════════════════
РАЗДЕЛИТЕЛИ
═══════════════════════════════════════════════════════════ */
.day-divider,
.unread-divider {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-1);
margin: var(--space-1) 0 var(--space-2);
font-size: .85rem;
line-height: 1;
font-weight: 700;
pointer-events: none;
user-select: none;
}
.day-divider::before,
.day-divider::after,
.unread-divider::before,
.unread-divider::after {
content: "";
flex: 1 1 auto;
height: 1px;
background: color-mix(in srgb, var(--bs-border-color), transparent 30%);
}
.day-divider > span,
.unread-divider > span {
flex: 0 0 auto;
padding: 0 var(--space-1);
background: transparent;
}
.day-divider {
color: var(--bs-secondary-color);
}
.unread-divider {
color: var(--bs-danger);
}
/* ═══════════════════════════════════════════════════════════
КНОПКА "ВНИЗ"
═══════════════════════════════════════════════════════════ */
.scroll-to-bottom {
position: absolute;
right: var(--space-2);
bottom: calc(80px + var(--space-2));
z-index: 2;
display: none;
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--bs-primary);
color: white;
border: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transition: all 0.2s;
}
@media (max-width: 767.98px) {
.scroll-to-bottom {
bottom: calc(var(--space-5) + 80px + env(safe-area-inset-bottom, 12px));
}
}
.scroll-to-bottom.show {
display: flex;
align-items: center;
justify-content: center;
}
.scroll-to-bottom:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
/* Badge непрочитанных на кнопке scrollBtn */
/* Унифицирован со стилем sticky-date */
.scroll-to-bottom {
position: relative; /* Для абсолютного позиционирования badge */
}
.scroll-to-bottom .unread-badge {
position: absolute;
top: -8px;
right: -8px;
/* Layout - как у sticky-date span */
display: none; /* По умолчанию скрыт */
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
/* Typography - как у sticky-date */
font-size: 11px;
font-weight: 600;
line-height: 1;
color: white;
/* Visual - унифицировано с sticky-date */
background-color: var(--bs-danger);
border-radius: 10px;
border: 2px solid var(--bs-body-bg); /* Контраст с фоном */
/* Shadow - как у sticky-date */
box-shadow: 0 2px 6px rgba(220, 53, 69, 0.4);
/* Transitions - как у sticky-date */
transition: background-color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
}
/* Анимация появления - как у sticky-date hover */
@keyframes badgePulse {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
opacity: 1;
}
}
.scroll-to-bottom .unread-badge {
animation: badgePulse 0.3s ease;
}
/* Кнопка с непрочитанными - subtle pulse как у sticky-date.interactive */
.scroll-to-bottom.has-unread {
animation: buttonPulse 2s ease infinite;
}
@keyframes buttonPulse {
0%, 100% {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
50% {
box-shadow: 0 4px 16px rgba(var(--bs-primary-rgb), 0.4);
}
}
/* ═══════════════════════════════════════════════════════════
ИНДИКАТОР ЗАГРУЗКИ
═══════════════════════════════════════════════════════════ */
.history-loader {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-1);
padding: var(--space-2) 0;
color: var(--bs-secondary);
}
.history-loader .spinner-border {
width: 1rem;
height: 1rem;
}
/* ═══════════════════════════════════════════════════════════
ТЕМНАЯ ТЕМА
═══════════════════════════════════════════════════════════ */
[data-bs-theme="dark"] .chat-messages-scroll {
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 78%) transparent;
}
[data-bs-theme="dark"] .chat-messages-scroll:hover {
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 60%) transparent;
}
[data-bs-theme="dark"] .chat-type-badge {
border-color: var(--bs-body-bg);
}