Files
vidconf/frontend/src/styles/room.css
Max Ronzhin 5d39e0f076
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
feat(room): кнопка микрофона в мини-окне (PiP)
Document PiP не показывает тулбар комнаты — во время звонка в мини-окне
нельзя было замьютить себя, не разворачивая основное окно. Кнопка лежит
поверх единственной плитки мини-плеера и использует тот же useTrackToggle,
что и тулбар: React-контекст LiveKitRoom не разрывается порталом в
pipWindow.document.body, поэтому состояние читается и меняется одинаково
что в основном окне, что в мини-плеере.
2026-07-30 00:25:19 +03:00

896 lines
41 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) */
.room-toolbar {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
background: var(--color-room-surface);
border-top: 1px solid var(--color-room-tile-border);
padding: var(--space-3) var(--space-6);
flex-shrink: 0;
}
.tb-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
background: transparent;
border: none;
padding: 8px 18px;
border-radius: var(--radius-md);
color: var(--color-room-text-primary);
min-width: 76px;
cursor: pointer;
}
.tb-btn .icon-shell {
width: 48px;
height: 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); 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); }
.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; }
.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; }
.chat-messages {
flex: 1;
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;
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-emoji-wrap { position: relative; display: flex; flex-shrink: 0; }
.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-trigger:hover { color: var(--color-room-text-primary); }
.chat-emoji-trigger.is-open { color: var(--color-room-mic-on); border-color: var(--color-room-speaker-ring); }
.chat-emoji-trigger:disabled { opacity: 0.5; cursor: default; }
.chat-emoji-popover {
position: absolute;
bottom: calc(100% + var(--space-2));
left: 0;
z-index: 50;
width: 224px;
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 2px;
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-option {
background: none;
border: none;
font-size: 20px;
line-height: 1;
padding: 6px;
border-radius: var(--radius-md);
cursor: pointer;
}
.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;
}
}
/*
* ---------- Диалог «Настройки устройств» ----------
* Нет отдельного макета для диалога в 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;
}
/* ---------- Заглушка «конференция в мини-окне» (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; }
}