Files
vidconf/frontend/src/styles/room.css
Max Ronzhin 2be799b19d fix(room): эмодзи-поповер в чате — 5-я колонка вылезала за рамку
Настоящая причина (подтверждена вживую через getBoundingClientRect):
`.chat-input-row button` (стиль круглой кнопки «Отправить», width/height
42px) красил размер и кнопкам эмодзи внутри поповера — они тоже лежат в
.chat-input-row. Сетка на 5 колонок по 42px требовала ~238px при
фиксированной ширине попапа 220px, лишнее уезжало вправо за рамку.
Явные width/height: auto на .chat-emoji-option перебивают унаследованный
размер нужной специфичностью, дальше квадрат считает aspect-ratio от
реальной ширины колонки (~36px). Заодно — max-width на попап и
overflow:hidden/white-space:nowrap на кнопки как общая страховка от
переполнения на очень узких экранах.
2026-08-03 01:56:27 +03:00

1103 lines
53 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.
/*
* VidConf — стили экрана комнаты конференции (тёмная тема, [data-theme="room"]).
* Приближение к design/mockups/room.html.
*
* ВАЖНО (см. также комментарий в RoomPage.tsx): видео-сетка участников
* рендерится компонентами @livekit/components-react (GridLayout/FocusLayout/
* CarouselLayout/ParticipantTile) — их внутренняя разметка не совпадает 1:1 с
* `.tile`/`.tile-preview` из макета. Здесь мы (1) переопределяем CSS-переменные
* темы LiveKit (`--lk-*`) значениями токенов VidConf, чтобы цвета/радиусы
* совпадали с дизайн-системой, и (2) стилизуем только внешние контейнеры,
* которые нам доступны через className. Точная пиксельная подгонка внутренней
* разметку ParticipantTile (позиция имени/иконки микрофона 1:1 как в
* макете) вынесена в отдельную доводку.
*/
[data-theme='room'] { height: 100%; }
.room-shell { display: flex; flex-direction: column; height: 100vh; background: var(--color-room-bg); }
.room-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-3) var(--space-6);
background: var(--color-room-surface);
border-bottom: 1px solid var(--color-room-tile-border);
flex-shrink: 0;
}
.room-title-block h1 { font: var(--text-h3); margin: 0 0 2px; color: var(--color-room-text-primary); }
.room-title-block p { font: var(--text-body); margin: 0; color: var(--color-room-text-secondary); display: flex; align-items: center; gap: 8px; }
.rec-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-room-mic-on); box-shadow: 0 0 0 3px rgba(70, 217, 122, 0.25); }
/* Инвайт-чип (номер конференции + копирование ссылки) — см. RoomTopbar.tsx. */
.room-invite-chip {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: var(--radius-full);
border: 1px solid var(--color-room-tile-border);
background: var(--color-room-tile);
color: var(--color-room-text-primary);
font: var(--text-body);
font-weight: 600;
}
.room-invite-chip:hover { background: var(--color-room-tile-hover); }
.room-invite-chip span { font: var(--text-mono-sm); color: var(--color-room-text-secondary); }
.room-main { display: flex; flex: 1; min-height: 0; }
/* `position: relative` — якорь для кнопки «Показать остальных» (`.stage-show-others`),
которая висит оверлеем над сценой, см. её правило ниже. */
.stage { flex: 1; display: flex; flex-direction: column; padding: var(--space-5) var(--space-6); gap: var(--space-4); min-width: 0; position: relative; }
/*
* Демонстрация экрана: `object-fit: cover` (дефолт VideoTrack для
* камеры) обрезает края — для текста/таблиц на экране это критично, поэтому
* для screenshare-видео явно переключаем на `contain` (letterbox вместо
* обрезки); фон — чёрный, как у самого видео-элемента библиотеки.
* `data-lk-source` — штатный атрибут @livekit/components-react на <video>.
*/
video[data-lk-source='screen_share'] { object-fit: contain; background: #000; }
/*
* `.stage-tiles` — общий className и для FocusLayoutContainer (несколько
* участников), и для GridLayout (один/два участника, см. RoomStage.tsx) —
* тут только то, что нужно ОБОИМ (заполнить оставшуюся высоту `.stage`).
* Раскладку колонок (`grid-template-columns`) переопределяем НИЖЕ отдельно,
* только для варианта с FocusLayoutContainer (`.lk-focus-layout`) — раньше
* это правило висело прямо на `.stage-tiles` и наружу задевало GridLayout:
* при одном участнике его плитка сжималась в узкую 22%-колонку слева, а
* остальная сцена оставалась пустой (баг, поймали на скриншоте).
*/
.stage-tiles {
flex: 1;
min-height: 0;
}
/*
* FocusLayoutContainer рендерит `.lk-focus-layout` (grid, первая колонка —
* узкая, под превью; вторая — широкая, под активного спикера) — сужаем
* первую колонку под наш вкус (дефолтный `1fr` слишком гуляет на широких
* мониторах). Селектор нарочно составной (`.stage-tiles.lk-focus-layout`),
* чтобы НЕ задеть `.lk-grid-layout` (тот же className `.stage-tiles` также
* висит на GridLayout при 12 участниках, см. выше). `!important` не нужен:
* `room.css` импортируется в RoomPage.tsx ПОСЛЕ `@livekit/components-styles`,
* поэтому при равной специфичности наши значения и так побеждают по порядку
* в исходном CSS.
*/
.stage-tiles.lk-focus-layout {
grid-template-columns: minmax(200px, 22%) 1fr;
}
/*
* Колонка миниатюр остальных участников — CarouselLayout без явного
* `orientation` (см. комментарий в RoomStage.tsx): при узкой и высокой форме
* контейнера (десктоп, боковая колонка) сам определяет `vertical` и берёт
* встроенные стили `.lk-carousel[data-lk-orientation="vertical"]` из
* @livekit/components-styles (flex-direction: column, скролл по вертикали,
* тайлы делятся по высоте на --lk-max-visible-tiles). Здесь только
* тонкая настройка скроллбара под тёмную тему комнаты — сами
* flex-direction/overflow не трогаем, чтобы не сломать переключение
* ориентации на мобильной ширине (см. media-запрос ниже).
*/
.lk-carousel::-webkit-scrollbar { width: 6px; height: 6px; }
.lk-carousel::-webkit-scrollbar-thumb { background: var(--color-room-tile-border); border-radius: 3px; }
/*
* Мобильная ширина: @livekit/components-styles сам переключает
* `.lk-focus-layout` на `grid-template-rows: 5fr 1fr` (спикер сверху,
* колонка миниатюр становится узкой горизонтальной полосой снизу,
* `.lk-carousel{order:1}`) — колонка шириной 22% там физически не
* поместится, поэтому здесь возвращаем `grid-template-columns` обратно в
* `1fr` (наше правило выше иначе продолжало бы действовать поверх их
* media-запроса — у обоих селекторов равная специфичность, и то, что
* стоит позже в исходнике, побеждает, а мы просто идём в файле позже).
*/
@media (max-width: 600px) {
.stage-tiles.lk-focus-layout { grid-template-columns: 1fr; }
.stage { padding: var(--space-3) var(--space-4); gap: var(--space-3); }
/* Их `.lk-carousel{order:1}` (ставит миниатюры ПОД спикера) больше не
действует: прямой ребёнок грида теперь обёртка `.stage-side`, а не сама
карусель — переносим порядок на неё, иначе миниатюры уезжают наверх. */
.stage-side { order: 1; }
}
/*
* `.room-single-tile` — сцена мини-плеера (Document PiP, `RoomStage` с
* `variant="pip"`): единственная плитка активного окна на весь контейнер, без
* карусели/грида. Портал рендерит
* `RoomStage` НАПРЯМУЮ в `pipWindow.document.body` — своей `.room-shell`/
* `.room-main` цепочки высот там нет, поэтому `.lk-participant-tile`
* растягиваем явным `flex: 1` (а не полагаемся на %-высоту от родителя, как
* `.stage-tiles` в основном окне) плюс подстраховочный `min-height`, чтобы
* плитка не схлопывалась, если видео ещё не выдало реальные размеры.
*/
.room-single-tile { position: relative; flex: 1; min-height: 0; display: flex; }
.room-single-tile .lk-participant-tile { flex: 1; min-height: 220px; width: 100%; }
/* Кнопка микрофона мини-плеера (задача A5) — своего тулбара в PiP нет,
кнопка лежит поверх единственной плитки левым нижним углом (правый занят
штатным `.lk-focus-toggle-button`/нашей булавкой закрепления — тут её нет,
в PiP закреплять нечего, но геометрия зеркалит `.room-pin-toggle`). */
.room-pip-mic-toggle {
position: absolute;
left: 0.5rem;
bottom: 0.5rem;
z-index: 5;
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-room-mic-on);
color: #10331f;
cursor: pointer;
}
.room-pip-mic-toggle svg { width: 18px; height: 18px; }
.room-pip-mic-toggle.is-off { background: var(--color-room-danger-bg); color: #fff; }
/*
* ---------- Колонка миниатюр (кнопка «Скрыть» + карусель) ----------
* Прямой ребёнок `.lk-focus-layout` — вместо самой карусели, потому что над
* ней стоит кнопка «Скрыть» (см. `RoomStage`). Карусель занимает весь остаток
* колонки: ей важны собственные размеры — `CarouselLayout` по ним и решает,
* вертикальная она или горизонтальная (см. комментарий в `RoomStage`).
* `min-height: 0` обязателен — иначе flex-элемент не даёт себя сжать и
* колонка вылезает за высоту сцены.
*/
.stage-side { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; min-height: 0; }
.stage-side .lk-carousel { flex: 1; min-height: 0; }
.stage-side-hide {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
flex-shrink: 0;
padding: 6px 10px;
border-radius: var(--radius-md);
border: 1px solid var(--color-room-tile-border);
background: var(--color-room-tile);
color: var(--color-room-text-secondary);
font: var(--text-caption);
text-transform: none;
letter-spacing: normal;
cursor: pointer;
}
.stage-side-hide:hover { background: var(--color-room-tile-hover); color: var(--color-room-text-primary); }
.stage-side-hide svg { width: 14px; height: 14px; flex-shrink: 0; }
/*
* ---------- Основная область без карусели ----------
* `.stage-main-only` — сцена, где карусель не рендерится вовсе: плиточные
* режимы («Плитки» всегда, «Живые плитки» когда камеры выключены не у кого),
* «Стандарт» с единственным участником и любой режим при включённом «скрыть
* остальных». Единственный ребёнок (плитка фокуса или сетка `.stage-grid`)
* занимает всё место — тем же приёмом, что `.room-single-tile` выше, но без
* `min-height` мини-плеера: в основном окне высота приходит от `.room-main`.
*/
.stage-main-only { display: flex; }
.stage-main-only > * { flex: 1; min-width: 0; min-height: 0; }
/*
* ---------- Равномерная сетка плиток (`StageGrid`) ----------
* Класс `.lk-grid-layout` библиотеки на месте (оттуда вся геометрия сетки:
* `--lk-col-count`/`--lk-row-count`, gap, padding), здесь — только то, чего у
* неё нет: сброс минимальных размеров (сетка может жить внутри grid-ячейки
* `.lk-focus-layout`, где без `min-*: 0` она распирала бы колонку) и якорь для
* собственного переключателя страниц.
*/
.stage-grid { position: relative; min-width: 0; min-height: 0; }
.stage-grid-pages {
position: absolute;
bottom: calc(var(--lk-grid-gap) / 2);
left: 50%;
transform: translateX(-50%);
z-index: 5;
display: flex;
align-items: center;
gap: 2px;
padding: 2px;
border-radius: var(--radius-full);
background: rgba(30, 30, 30, 0.85);
border: 1px solid var(--color-room-tile-border);
color: var(--color-room-text-primary);
font: var(--text-caption);
}
.stage-grid-pages button {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
border: none;
border-radius: 50%;
background: transparent;
color: inherit;
cursor: pointer;
}
.stage-grid-pages button:hover:not(:disabled) { background: var(--color-room-tile-hover); }
.stage-grid-pages button:disabled { opacity: 0.35; cursor: default; }
.stage-grid-pages svg { width: 16px; height: 16px; }
.stage-grid-pages span { padding: 0 4px; font-variant-numeric: tabular-nums; }
/*
* ---------- Кнопка «Показать остальных» ----------
* Единственный видимый способ вернуть скрытую карусель, поэтому висит прямо на
* сцене, а не только в меню «Вид»: пользователь не должен «терять» участников
* без понятного способа их вернуть. Внизу по центру — там у плиток пусто
* (`.lk-participant-metadata` прижата к левому и правому углам).
*/
.stage-show-others {
position: absolute;
bottom: var(--space-5);
left: 50%;
transform: translateX(-50%);
z-index: 10;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: var(--radius-full);
border: 1px solid var(--color-room-tile-border);
background: rgba(30, 30, 30, 0.9);
color: var(--color-room-text-primary);
font: var(--text-body);
font-weight: 600;
/* Иначе на узком экране подпись ломается на две строки и кнопка вырастает
в высоту вдвое (поймали на 390px). */
white-space: nowrap;
box-shadow: var(--shadow-md);
cursor: pointer;
}
.stage-show-others:hover { background: var(--color-room-tile-hover); }
.stage-show-others svg { width: 18px; height: 18px; flex-shrink: 0; }
/* Нижний тулбар: свои кнопки на хуках LiveKit (TrackToggle/DisconnectButton) */
/*
* Кнопки тулбара плавно уменьшаются (иконка/отступы/шрифт/зазор) на всём
* диапазоне 1200px → 600px — до этого тулбар стал шире, чем при исходном
* проектировании (задачи B1/B2 добавили «Рука»/«Очередь», раньше помещались
* без сжатия 8 кнопок, теперь до 11 — без этого блока получался
* горизонтальный оверфлоу вплоть до самого мобильного брейкпоинта, кнопки
* вылезали за края тулбара).
*
* Обычный `clamp(min, Nvw, max)` тут не подходит: подобранный `N`
* дотягивается до `max` уже на довольно узких экранах (например,
* `3vw` = 24px ровно на 800px viewport) и дальше просто стоит на потолке —
* получается не плавное сжатие в нужном диапазоне, а резкий скачок сильно
* раньше нужной ширины (поймали именно так на первой версии этого блока).
* Вместо этого — явная линейная интерполяция между двумя точками
* (600px→минимум, 1200px→максимум): `calc(MIN + (MAX-MIN) * (100vw - 600px)
* / 600)`, снаружи в `clamp()` только чтобы намертво остановиться на
* границах диапазона. Нижние границы — те же значения, что жёстко
* выставляет мобильный медиа-запрос ниже (`max-width: 600px`), поэтому
* переход в него на 600px визуально бесшовный.
*/
.room-toolbar {
display: flex;
align-items: center;
justify-content: center;
gap: clamp(2px, calc(2px + (100vw - 600px) * 6 / 600), var(--space-2));
background: var(--color-room-surface);
border-top: 1px solid var(--color-room-tile-border);
padding: var(--space-3) clamp(8px, calc(8px + (100vw - 600px) * 16 / 600), var(--space-6));
flex-shrink: 0;
}
.tb-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: clamp(2px, calc(2px + (100vw - 600px) * 2 / 600), 4px);
background: transparent;
border: none;
padding:
clamp(6px, calc(6px + (100vw - 600px) * 2 / 600), 8px)
clamp(6px, calc(6px + (100vw - 600px) * 12 / 600), 18px);
border-radius: var(--radius-md);
color: var(--color-room-text-primary);
min-width: clamp(0px, calc((100vw - 600px) * 76 / 600), 76px);
cursor: pointer;
}
.tb-btn .icon-shell {
width: clamp(40px, calc(40px + (100vw - 600px) * 8 / 600), 48px);
height: clamp(40px, calc(40px + (100vw - 600px) * 8 / 600), 48px);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 1.5px solid var(--color-room-tile-border);
background: var(--color-room-tile);
color: var(--color-room-text-primary);
}
.tb-btn span.label {
font: var(--text-caption);
font-size: clamp(11px, calc(11px + (100vw - 600px) * 1 / 600), 12px);
text-transform: none;
letter-spacing: normal;
color: var(--color-room-text-secondary);
font-weight: 500;
}
.tb-btn:hover .icon-shell { background: var(--color-room-tile-hover); }
/* Короткая подпись мини-окна (см. RoomToolbar.tsx) — «Мини-окно» на узком
экране переносится на 2 строки и делает эту кнопку выше соседних; ниже
порога, где начинается перенос, прячем длинный вариант и показываем
короткий «Мини» — кнопка остаётся однострочной и той же высоты, что и
остальные. */
.tb-btn .label-short { display: none; }
@media (max-width: 1200px) {
.tb-btn .label-full { display: none; }
.tb-btn .label-short { display: inline; }
}
.tb-btn.is-off .icon-shell { background: var(--color-room-mic-off); border-color: var(--color-room-mic-off); color: #3a0f16; }
.tb-btn.is-off span.label { color: var(--color-room-mic-off); font-weight: 700; }
/* Кнопка «Демонстрация» — активное состояние подсвечено зелёным,
как и другие «позитивные» индикаторы комнаты (ср. .rec-dot/--color-room-mic-on). */
.tb-btn.is-sharing .icon-shell { background: var(--color-room-mic-on); border-color: var(--color-room-mic-on); color: #10331f; }
.tb-btn.is-sharing span.label { color: var(--color-room-mic-on); font-weight: 700; }
.tb-btn:disabled { opacity: 0.6; cursor: default; }
/* Кнопка «Рука» — та же зелёная подсветка активного состояния, что у
«Демонстрации» (задача B1): своя поднятая рука — такой же позитивный
индикатор «я сейчас что-то сигнализирую комнате». */
.tb-btn.is-hand-raised .icon-shell { background: var(--color-room-mic-on); border-color: var(--color-room-mic-on); color: #10331f; }
.tb-btn.is-hand-raised span.label { color: var(--color-room-mic-on); font-weight: 700; }
.tb-btn.danger .icon-shell { border-color: var(--color-room-danger); color: var(--color-room-danger); background: transparent; }
.tb-btn.danger:hover .icon-shell { background: var(--color-room-danger-bg); color: #fff; border-color: var(--color-room-danger-bg); }
.tb-btn.danger span.label { color: var(--color-room-danger); font-weight: 700; }
/* Кнопка «Чат» — подсветка при открытой панели + бейдж непрочитанных. */
.tb-btn .icon-shell { position: relative; }
.tb-btn.is-panel-open .icon-shell { background: var(--color-room-surface-raised); border-color: var(--color-room-speaker-ring); color: var(--color-room-speaker-ring); }
.badge-count {
position: absolute;
top: -2px;
right: -2px;
background: var(--color-room-mic-on);
color: #0e3320;
font-size: 10px;
font-weight: 700;
width: 16px;
height: 16px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
/*
* ---------- Кнопка «Вид» и её поповер (десктоп) ----------
* Обёртка вокруг кнопки тулбара — якорь для всплывающей панели над ней
* (`StageViewMenu`). На мобильном ни кнопки, ни поповера нет вовсе: там блок
* настроек вида рендерится секцией внутри шторки (условный рендеринг в React,
* а не скрытие через CSS, — см. `RoomToolbar`).
*/
.tb-menu-wrap { position: relative; display: flex; }
.tb-menu {
position: absolute;
bottom: calc(100% + var(--space-2));
left: 50%;
transform: translateX(-50%);
z-index: 50;
width: 280px;
padding: var(--space-4);
border-radius: var(--radius-lg);
border: 1px solid var(--color-room-tile-border);
background: var(--color-room-surface-raised);
box-shadow: var(--shadow-room-panel);
}
/*
* ---------- Блок «Вид сцены» (`StageViewOptions`) ----------
* Общий для десктопного поповера и мобильной шторки — отсюда нейтральная
* геометрия без привязки к контейнеру (ширину задаёт родитель).
*/
.stage-view-options { display: flex; flex-direction: column; gap: var(--space-3); }
/* Секция «Вид» внутри шторки настроек — отбивка как у `.room-field` (свой
класс, а не `.room-field`, см. комментарий в `DeviceSettingsDialog`). */
.stage-view-section { margin-bottom: var(--space-5); }
.stage-view-modes { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; border: none; }
.stage-view-modes legend {
padding: 0 0 var(--space-2);
font: var(--text-caption);
color: var(--color-room-text-tertiary);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.stage-view-mode {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 8px 10px;
border-radius: var(--radius-md);
border: 1px solid transparent;
cursor: pointer;
}
.stage-view-mode:hover { background: var(--color-room-tile); }
.stage-view-mode.is-active { background: var(--color-room-tile); border-color: var(--color-room-speaker-ring); }
.stage-view-mode input { margin-top: 3px; accent-color: var(--color-room-mic-on); flex-shrink: 0; }
.stage-view-mode-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.stage-view-mode-title { font: var(--text-body); font-weight: 600; color: var(--color-room-text-primary); }
.stage-view-mode-hint { font: var(--text-caption); text-transform: none; letter-spacing: normal; color: var(--color-room-text-secondary); }
.stage-view-toggle {
display: flex;
align-items: center;
gap: 10px;
padding-top: var(--space-3);
border-top: 1px solid var(--color-room-tile-border);
font: var(--text-body);
color: var(--color-room-text-primary);
cursor: pointer;
}
.stage-view-toggle input { accent-color: var(--color-room-mic-on); flex-shrink: 0; }
.stage-view-toggle.is-disabled { color: var(--color-room-text-tertiary); cursor: default; }
.stage-view-note { margin: 0; font: var(--text-caption); text-transform: none; letter-spacing: normal; color: var(--color-room-text-tertiary); }
/* ---------- Панель чата (см. design/mockups/room.html, .chat-panel) ---------- */
.chat-panel {
width: 320px;
flex-shrink: 0;
background: var(--color-room-surface-raised);
border-left: 1px solid var(--color-room-tile-border);
display: flex;
flex-direction: column;
box-shadow: var(--shadow-room-panel);
}
.chat-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--color-room-tile-border);
flex-shrink: 0;
}
.chat-head h2 { font: var(--text-h3); margin: 0; color: var(--color-room-text-primary); }
.chat-head button { background: none; border: none; color: var(--color-room-text-secondary); cursor: pointer; display: flex; }
.chat-head button:hover { color: var(--color-room-text-primary); }
.chat-status-banner {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
padding: var(--space-3) var(--space-5);
background: var(--color-room-danger-bg);
color: #fff;
font: var(--text-body);
flex-shrink: 0;
}
.chat-empty { font: var(--text-body); color: var(--color-room-text-tertiary); margin: auto; text-align: center; }
/* `min-height: 0` обязателен — тот же приём, что у `.stage-side` (комментарий
выше): без него `.chat-panel` (flex-колонка) в Firefox не сжимает
`.chat-messages` до высоты `flex:1`, а даёт ей вырасти по контенту
(список сообщений) и вылезти за пределы панели — Chrome в этой ситуации
более снисходителен, Firefox — нет. */
.chat-messages {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.chat-messages::-webkit-scrollbar { width: 6px; }
.chat-messages::-webkit-scrollbar-thumb { background: var(--color-room-tile-border); border-radius: 3px; }
.msg .msg-meta { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
.msg .msg-author { font: var(--text-body); font-weight: 700; color: var(--color-room-mic-on); }
.msg .msg-guest-badge {
font: var(--text-caption);
color: var(--color-room-text-tertiary);
border: 1px solid var(--color-room-tile-border);
border-radius: var(--radius-sm);
padding: 0 6px;
text-transform: none;
letter-spacing: normal;
}
.msg .msg-time { font: 12px var(--font-body); color: var(--color-room-text-tertiary); }
.msg .msg-bubble {
background: var(--color-room-tile);
border-radius: var(--radius-md);
padding: 10px 14px;
font: var(--text-body);
color: var(--color-room-text-primary);
white-space: pre-wrap;
word-break: break-word;
}
.chat-input-row {
padding: var(--space-4);
border-top: 1px solid var(--color-room-tile-border);
display: flex;
align-items: flex-end;
gap: 8px;
flex-shrink: 0;
}
.chat-input-row textarea {
flex: 1;
/* Firefox даёт `<textarea>` большую автоматическую минимальную ширину,
завязанную на атрибут `cols` (умолчание 20 символов моноширинной
метрики), и как flex-item без `min-width:0` отказывается сжиматься
ниже нее — панель шириной 320px раздувается вправо. Chrome/Safari
считают минимальную ширину textarea мягче, поэтому баг был виден
только в Firefox. */
min-width: 0;
resize: none;
background: var(--color-room-tile);
border: 1px solid var(--color-room-tile-border);
border-radius: var(--radius-lg);
padding: 10px 16px;
color: var(--color-room-text-primary);
font: var(--text-body-lg);
max-height: 96px;
}
.chat-input-row textarea::placeholder { color: var(--color-room-text-tertiary); }
.chat-input-row textarea:disabled { opacity: 0.6; }
.chat-input-row button {
width: 42px;
height: 42px;
border-radius: 50%;
background: var(--color-room-mic-on);
border: none;
color: #10331f;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
cursor: pointer;
}
.chat-input-row button:disabled { opacity: 0.5; cursor: default; }
/* Триггер и опции эмодзи-поповера лежат внутри `.chat-input-row` (форма
отправки) — тот же контейнер, что у круглой зелёной кнопки «Отправить»
(`.chat-input-row button`, специфичность 0,1,1). Голого класса
(`.chat-emoji-trigger`/`.chat-emoji-option`, 0,1,0) для победы над ней не
хватает — ЛЮБАЯ кнопка внутри формы (включая кнопки в самом поповере,
он тоже в этом поддереве) красилась в зелёный независимо от порядка
правил в файле. Каждый селектор ниже уточнён родительским классом ровно
затем, чтобы обойти именно эту гонку специфичности. */
.chat-emoji-wrap { position: relative; display: flex; flex-shrink: 0; }
.chat-emoji-wrap .chat-emoji-trigger {
width: 42px;
height: 42px;
border-radius: 50%;
background: var(--color-room-tile);
border: 1px solid var(--color-room-tile-border);
color: var(--color-room-text-secondary);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.chat-emoji-wrap .chat-emoji-trigger:hover { color: var(--color-room-text-primary); }
.chat-emoji-wrap .chat-emoji-trigger.is-open { color: var(--color-room-mic-on); border-color: var(--color-room-speaker-ring); }
.chat-emoji-wrap .chat-emoji-trigger:disabled { opacity: 0.5; cursor: default; }
/* 5 колонок × 6 строк — ровно 30 эмодзи в EMOJI_OPTIONS (ChatPanel.tsx), без
неполной последней строки. `max-width` — страховка на случай совсем узкого
viewport: фикс-ширина 220px без потолка сама по себе не переполняется при
текущей раскладке (триггер у левого края панели, попап растёт вправо в
свободное место — проверено геометрией и вживую), но фиксированный размер
совсем без ограничителя — плохая практика сама по себе. */
.chat-emoji-popover {
position: absolute;
bottom: calc(100% + var(--space-2));
left: 0;
z-index: 50;
width: 220px;
max-width: calc(100vw - 2 * var(--space-4));
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 4px;
padding: var(--space-3);
border-radius: var(--radius-lg);
border: 1px solid var(--color-room-tile-border);
background: var(--color-room-surface-raised);
box-shadow: var(--shadow-room-panel);
}
.chat-emoji-popover .chat-emoji-option {
/* Настоящая причина переполнения (найдена по факту, не по догадке —
`getBoundingClientRect` показал кнопки 42×42px при колонке ~36px):
`.chat-input-row button` (специфичность 0,1,1) задаёт ВСЕМ кнопкам
формы `width/height: 42px` — это правило круглой кнопки «Отправить»,
а кнопки эмодзи в поповере тоже лежат внутри `.chat-input-row`
(см. комментарий выше про гонку специфичности, она чинилась для
цвета в 0.0.19, но не для размера). Без явного `width`/`height` здесь
побеждает тот 42px, сетка на 5 колонок раздувается за 220px попапа,
и последняя колонка уезжает вправо за рамку. `min-width: 0` сам по
себе НЕ помогает — конфликт не в авто-минимуме грида, а в explicit
width, который обязательно нужно перебить явно. */
width: auto;
height: auto;
min-width: 0;
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: none;
border: none;
font-size: 20px;
line-height: 1;
white-space: nowrap;
border-radius: var(--radius-md);
cursor: pointer;
}
.chat-emoji-popover .chat-emoji-option:hover { background: var(--color-room-tile); }
@media (max-width: 900px) {
.chat-panel {
position: fixed;
inset: 0;
top: 64px;
bottom: 96px;
width: auto;
z-index: 20;
}
}
/* ---------- Поповер очереди поднятых рук (`HandQueueMenu`, задача B1) ----------
* Контейнер — `.tb-menu` (тот же поповер над кнопкой, что у «Вида»), не
* `.chat-panel`: очередь — короткий список, а не история переписки,
* разворачивать её на весь экран/боковой панелью незачем даже на мобильном. */
.hand-queue-menu { width: 300px; padding: var(--space-3); }
.hand-queue-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-2);
/* Высота растёт вместе со списком (при 12 записях поповер компактный), но
не безгранично — после ~10 строк упирается в потолок и скроллится
дальше, иначе на энергичной встрече поповер вылез бы выше экрана. */
max-height: 460px;
overflow-y: auto;
}
.hand-queue-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: var(--radius-md);
background: var(--color-room-tile);
}
.hand-queue-position {
flex-shrink: 0;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--color-room-mic-on);
color: #10331f;
font: var(--text-caption);
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.hand-queue-name {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 6px;
font: var(--text-body);
color: var(--color-room-text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hand-queue-name svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--color-room-mic-on); }
.hand-queue-lower {
flex-shrink: 0;
padding: 6px 10px;
border-radius: var(--radius-md);
border: 1px solid var(--color-room-tile-border);
background: transparent;
color: var(--color-room-text-secondary);
font: var(--text-caption);
text-transform: none;
letter-spacing: normal;
cursor: pointer;
}
.hand-queue-lower:hover { background: var(--color-room-tile-hover); color: var(--color-room-text-primary); }
/*
* ---------- Диалог «Настройки устройств» ----------
* Нет отдельного макета для диалога в design/mockups/room.html — собран из
* уже утверждённых токенов тёмной темы комнаты (`--color-room-*`), тот же
* структурный паттерн (overlay/panel/head/close/field), что и модалки
* оболочки (`shell.css` .modal-*), но перекрашенный под комнату: `.modal-*`
* оболочки используют `--color-ink-*`, которые не переопределяются внутри
* `[data-theme="room"]`, — переиспользовать их напрямую нельзя, получилась
* бы светлая панель на тёмном фоне комнаты.
*/
.room-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-6);
z-index: 200;
}
.room-modal-panel {
background: var(--color-room-surface-raised);
border: 1px solid var(--color-room-tile-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-room-panel);
padding: var(--space-6);
width: 100%;
max-width: 420px;
max-height: calc(100vh - var(--space-12));
overflow-y: auto;
}
.room-modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-5); }
.room-modal-head h2 { font: var(--text-h3); color: var(--color-room-text-primary); margin: 0; }
.room-modal-close {
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid var(--color-room-tile-border);
background: transparent;
color: var(--color-room-text-secondary);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
cursor: pointer;
}
.room-modal-close:hover { color: var(--color-room-text-primary); border-color: var(--color-room-text-secondary); }
.room-field { margin-bottom: var(--space-5); }
.room-field:last-child { margin-bottom: 0; }
.room-field label { display: block; font: var(--text-body); font-weight: 600; color: var(--color-room-text-primary); margin-bottom: var(--space-2); }
.room-field select {
width: 100%;
font: var(--text-body-lg);
color: var(--color-room-text-primary);
padding: 10px 14px;
border-radius: var(--radius-md);
border: 1px solid var(--color-room-tile-border);
background: var(--color-room-tile);
font-family: var(--font-body);
}
.room-field select:focus { outline: none; border-color: var(--color-room-focus-ring); }
.room-field-label { display: block; font: var(--text-body); font-weight: 600; color: var(--color-room-text-primary); margin-bottom: var(--space-2); }
.room-field-hint { font: var(--text-body); color: var(--color-room-text-secondary); margin: 0; }
/*
* ---------- Шторка «Настройки устройств» на мобильном ----------
* Тот же диалог (`DeviceSettingsDialog`), что модалка выше, — только контейнер
* снизу вместо центра экрана: `.room-sheet-overlay`/`.room-sheet-panel` вместо
* `.room-modal-overlay`/`.room-modal-panel`, разметка полей (`.room-field` и
* ниже) общая. Ручка `.room-sheet-handle` — свайп вниз для закрытия (JS считает
* смещение пальца и сам решает, закрывать или вернуть панель на место;
* `transition` тут — только пружина возврата, во время самого драга
* компонент подставляет инлайновый `transition: none`).
*/
.room-sheet-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 200;
}
.room-sheet-panel {
background: var(--color-room-surface-raised);
border: 1px solid var(--color-room-tile-border);
border-bottom: none;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
box-shadow: var(--shadow-room-panel);
padding: var(--space-4) var(--space-6) calc(var(--space-6) + env(safe-area-inset-bottom));
width: 100%;
max-height: 80vh;
overflow-y: auto;
transition: transform 160ms ease-out;
}
.room-sheet-handle {
width: 40px;
height: 4px;
border-radius: 2px;
background: var(--color-room-tile-border);
margin: 0 auto var(--space-4);
touch-action: none;
}
/*
* ---------- Чип «Вы демонстрируете экран» ----------
* Рендерится внутри `RoomParticipantTile` (см. её `TileBody`) — т.е. внутри
* `.lk-participant-tile`, который у @livekit/components-react уже
* `position: relative` (см. её базовые стили), поэтому оверлей позиционируем
* абсолютно прямо относительно плитки, без правок родительских контейнеров.
*/
.stage-sharing-chip {
position: absolute;
top: 12px;
left: 12px;
z-index: 5;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
border-radius: var(--radius-full);
background: rgba(30, 30, 30, 0.85);
border: 1px solid var(--color-room-mic-on);
color: var(--color-room-text-primary);
font: var(--text-body);
box-shadow: var(--shadow-md);
}
.stage-sharing-chip svg { width: 18px; height: 18px; color: var(--color-room-mic-on); flex-shrink: 0; }
.stage-sharing-chip button {
border: none;
border-radius: var(--radius-full);
background: var(--color-room-mic-on);
color: #10331f;
font: var(--text-body);
font-weight: 700;
padding: 4px 12px;
cursor: pointer;
flex-shrink: 0;
}
.stage-sharing-chip button:hover { filter: brightness(0.95); }
/*
* ---------- Кнопка закрепления участника (булавка) ----------
* Рендерится внутри `RoomParticipantTile` (см. её `TileBody`), т.е. внутри
* `.lk-participant-tile` — тот у @livekit/components-styles уже
* `position: relative`. Геометрия и логика появления повторяют штатную
* `.lk-focus-toggle-button` той же библиотеки (правый верхний угол, видна по
* наведению) — чтобы плитка выглядела цельно, а не «нашей кнопкой поверх
* чужой». Отличие: у закреплённой плитки булавка видна ВСЕГДА и подсвечена —
* закрепление должно читаться без наведения.
*/
.room-pin-toggle {
position: absolute;
top: 0.25rem;
right: 0.25rem;
z-index: 5;
display: flex;
padding: 0.25rem;
border: none;
border-radius: calc(var(--lk-border-radius, 0.5rem) / 2);
background: rgba(0, 0, 0, 0.5);
color: var(--color-room-text-primary);
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease-in-out;
transition-delay: 0.2s;
}
.room-pin-toggle svg { width: 20px; height: 20px; }
.lk-participant-tile:hover .room-pin-toggle,
.lk-participant-tile:focus-within .room-pin-toggle,
.room-pin-toggle.is-pinned { opacity: 1; transition-delay: 0s; }
.room-pin-toggle.is-pinned { background: var(--color-room-mic-on); color: #10331f; }
/* Тач-устройства (мобильный/планшет): hover нет вовсе — иначе булавка была бы
недоступна. Тулбар комнаты не задет: кнопка живёт на плитке. */
@media (hover: none) {
.room-pin-toggle { opacity: 1; transition-delay: 0s; }
}
/* Метка закреплённой плитки — рамка по контуру. Класс закрепления висит на
кнопке (корневой div плитки рендерит библиотека, свой класс туда не
добавить), поэтому селектор идёт через `:has()`. */
.lk-participant-tile:has(.room-pin-toggle.is-pinned) {
outline: 2px solid var(--color-room-mic-on);
outline-offset: -2px;
}
/* Бейдж поднятой руки (задача B1) — левый верхний угол, зеркально булавке
закрепления (правый верхний). В отличие от булавки ВСЕГДА видим, пока
рука поднята, — это статус для ВСЕХ участников, а не собственный
элемент управления, видимый по наведению. */
.room-hand-badge {
position: absolute;
top: 0.25rem;
left: 0.25rem;
z-index: 5;
display: flex;
padding: 0.25rem;
border-radius: calc(var(--lk-border-radius, 0.5rem) / 2);
background: var(--color-room-mic-on);
color: #10331f;
}
.room-hand-badge svg { width: 18px; height: 18px; }
/* Кнопки принудительного мьюта организатором (задача B2) — нижний правый
угол чужой плитки, видны по наведению (как булавка закрепления) —
элемент управления, а не статус, прятать по умолчанию уместно. */
.room-organizer-controls {
position: absolute;
bottom: 0.25rem;
right: 0.25rem;
z-index: 5;
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s ease-in-out;
transition-delay: 0.2s;
}
.lk-participant-tile:hover .room-organizer-controls,
.lk-participant-tile:focus-within .room-organizer-controls { opacity: 1; transition-delay: 0s; }
@media (hover: none) {
.room-organizer-controls { opacity: 1; transition-delay: 0s; }
}
.room-organizer-controls button {
display: flex;
padding: 0.25rem;
border: none;
border-radius: calc(var(--lk-border-radius, 0.5rem) / 2);
background: rgba(0, 0, 0, 0.5);
color: var(--color-room-text-primary);
cursor: pointer;
}
.room-organizer-controls button:hover { background: var(--color-room-danger-bg); color: #fff; }
.room-organizer-controls svg { width: 18px; height: 18px; }
/* ---------- Заглушка «конференция в мини-окне» (Document PiP) ---------- */
.room-pip-placeholder {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-4);
color: var(--color-room-text-secondary);
font: var(--text-body-lg);
text-align: center;
}
.room-pip-placeholder svg { width: 48px; height: 48px; color: var(--color-room-text-tertiary); }
/* ---------- Аватар вместо иконки-заглушки при выключенной камере ----------
* Размер считается от УЗКОЙ стороны плитки, а не от ширины и высоты по
* отдельности. Раньше здесь было `width: 40%; height: 40%` — проценты от
* РАЗНЫХ сторон, а круглым `.avatar` делает только `border-radius: 50%`
* (shell.css), поэтому на неквадратной плитке (а они почти всегда такие)
* получался эллипс. Плюс `max-*: 96px` не давал иконке вырасти на большой
* плитке фокуса.
*
* Механика: контейнером container-запросов делаем `.lk-participant-placeholder`
* — блок библиотеки, который `position: absolute; inset: 0`, то есть в
* точности повторяет плитку и уже имеет обе размерности (обязательное условие
* для `container-type: size`). Контейнер именно на нём, а не на самой
* `.lk-participant-tile`: `container-type: size` включает size containment, и
* на плитке он отрезал бы её содержимое (видео, метаданные) от влияния на
* авторазмер — на placeholder-обёртке внутри ничего, кроме аватара, нет,
* ломать нечему.
*
* `cqmin` — минимум из ширины и высоты контейнера, буквально «узкая сторона»:
* - основная (крупная) плитка — 50cqmin, примерно половина плитки;
* - плитки карусели — во всю узкую сторону минус отступ 5px с каждого края.
* `aspect-ratio: 1` + `height: auto` гарантируют круг при любой форме плитки:
* диаметр никогда не больше меньшей стороны, дальше иконка уменьшается
* пропорционально вместе с плиткой. `min-width: 32px` оставлен — на совсем
* крошечной плитке инициалы должны оставаться читаемыми (сжатие плитки ниже
* этого порога и так означает, что показывать там нечего).
*/
.lk-participant-tile .lk-participant-placeholder {
container-type: size;
}
.room-tile-avatar {
width: 50cqmin;
height: auto;
aspect-ratio: 1;
min-width: 32px;
min-height: 0;
max-width: none;
max-height: none;
font-size: clamp(12px, 20cqmin, 44px);
font-weight: 700;
}
.lk-carousel .room-tile-avatar {
width: calc(100cqmin - 10px);
font-size: clamp(12px, 34cqmin, 28px);
}
.room-loader, .room-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-3);
height: 100vh;
color: var(--color-room-text-secondary);
font: var(--text-body-lg);
background: var(--color-room-bg);
}
.room-error .btn-primary { margin-top: var(--space-2); }
/* ---------- Переопределение темы @livekit/components-react токенами VidConf ---------- */
[data-lk-theme='default'] {
--lk-bg: var(--color-room-bg);
--lk-bg2: var(--color-room-surface);
--lk-bg3: var(--color-room-tile);
--lk-bg4: var(--color-room-tile-hover);
--lk-bg5: var(--color-room-tile-hover);
--lk-fg: var(--color-room-text-primary);
--lk-fg2: var(--color-room-text-primary);
--lk-fg3: var(--color-room-text-secondary);
--lk-fg4: var(--color-room-text-secondary);
--lk-border-color: var(--color-room-tile-border);
--lk-accent-fg: #10331f;
--lk-accent-bg: var(--color-room-speaker-ring);
--lk-danger-fg: #fff;
--lk-danger: var(--color-room-danger-bg);
--lk-success: var(--color-room-mic-on);
--lk-control-fg: var(--color-room-text-primary);
--lk-control-bg: var(--color-room-tile);
--lk-control-hover-bg: var(--color-room-tile-hover);
--lk-font-family: var(--font-body);
--lk-border-radius: var(--radius-lg);
--lk-grid-gap: var(--space-4);
--lk-control-bar-height: 0px;
}
/* ---------- Узкие экраны: топбар и тулбар комнаты ----------
ВАЖНО: этот блок обязан идти строго ПОСЛЕДНИМ в файле. Селекторы ниже
(`.tb-btn`, `.room-toolbar`) имеют ту же специфичность, что и безусловные
правила выше, а при равной специфичности побеждает порядок в исходнике, а
не факт совпадения media query — тот же грабли уже ловили в
auth.css/calendar.css/lobby.css (см. комментарии там). Первая версия этого
блока стояла в середине файла: `.room-invite-chip` (объявлен ВЫШЕ) скрылся,
а `.tb-btn`/`.room-toolbar` (объявлены НИЖЕ) продолжали побеждать — тулбар
на мобильном не менялся вовсе.
Топбар: скрываем название конференции и строку статуса — они и так есть
в самой конференции, а вот чип «№ … / Пригласить» с телефона нужен чаще
(скопировать ссылку и позвать участника), поэтому он остаётся.
Тулбар: демонстрация экрана, полноэкранный режим и мини-окно с телефона
практически не нужны, а все 8 кнопок в ширину экрана физически не
помещаются — скрываем их и уплотняем оставшиеся пять
(микрофон/камера/устройства/чат/выход). `flex-wrap` + `min-width: 0` —
страховка на совсем узких экранах, чтобы футер ни при каких подписях не
вылезал за ширину окна. */
@media (max-width: 600px) {
.room-topbar { padding: var(--space-3) var(--space-4); justify-content: flex-end; }
.room-title-block { display: none; }
.tb-btn--screenshare,
.tb-btn--fullscreen,
.tb-btn--pip { display: none; }
.room-toolbar { padding: var(--space-3) var(--space-2); gap: 2px; flex-wrap: wrap; }
.tb-btn { min-width: 0; padding: 6px 6px; }
.tb-btn .icon-shell { width: 40px; height: 40px; }
.tb-btn span.label { font-size: 11px; }
}