Corp/backend/static/css/components/chat-detail.css
Игорь Надеин 3ba512a937 feat: Smart scroll button with unread badge and auto-scroll
Implements Telegram-like behavior for new messages:

1. Smart Auto-scroll (WS messages):
   ✅ User at bottom + present time → auto-scroll to new message
   ✅ User NOT at bottom + present time → show unread badge
   ❌ Historical view (hasMoreAfter=true) → no action

2. Smart Scroll Button (#scrollBtn):
   - Shows unread badge when new messages arrive
   - Click behavior:
     * Has unread + lastReadId → scroll to first unread
     * No unread → scroll to bottom
   - Auto-hides badge when user scrolls to bottom

3. Badge Styling:
   - Red badge with count on top-right of button
   - Pulse animation on appearance
   - Button subtle pulse when has unread

Key Logic:
- Only auto-scroll when hasMoreAfter=false (present time)
- Historical bottom (hasMoreAfter=true) doesn't trigger auto-scroll
- Badge resets on: manual scroll to bottom, button click
- Respects user's scroll position unless at bottom
2026-01-13 15:07:52 +03:00

700 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 */
.scroll-to-bottom {
position: relative; /* Для абсолютного позиционирования badge */
}
.scroll-to-bottom .unread-badge {
position: absolute;
top: -6px;
right: -6px;
min-width: 20px;
height: 20px;
padding: 0 6px;
display: none; /* По умолчанию скрыт */
align-items: center;
justify-content: center;
background: var(--bs-danger);
color: white;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
line-height: 1;
box-shadow: 0 2px 6px rgba(220, 53, 69, 0.4);
animation: badgePulse 0.3s ease;
}
.scroll-to-bottom.has-unread {
/* Опционально: можем изменить вид кнопки когда есть непрочитанные */
animation: buttonPulse 2s ease infinite;
}
@keyframes badgePulse {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
opacity: 1;
}
}
@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);
}