Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e029a2bf8 | |||
| f89bf1ad64 |
@@ -122,7 +122,7 @@ SMTP_TIMEOUT_S=30
|
|||||||
# --- Версия инстанса (релиз v0.0.1) ---
|
# --- Версия инстанса (релиз v0.0.1) ---
|
||||||
# install.sh копирует значение из корневого файла VERSION при каждой
|
# install.sh копирует значение из корневого файла VERSION при каждой
|
||||||
# установке/обновлении — руками менять не нужно.
|
# установке/обновлении — руками менять не нужно.
|
||||||
VIDCONF_VERSION=0.0.28
|
VIDCONF_VERSION=0.0.29
|
||||||
|
|
||||||
# --- Профили compose. Дефолт ниже (`media,monitoring`) — только для ручного
|
# --- Профили compose. Дефолт ниже (`media,monitoring`) — только для ручного
|
||||||
# `docker compose up` БЕЗ install.sh: медиа (LiveKit+coturn) + мониторинг,
|
# `docker compose up` БЕЗ install.sh: медиа (LiveKit+coturn) + мониторинг,
|
||||||
|
|||||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -3,6 +3,29 @@
|
|||||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/),
|
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/),
|
||||||
проект придерживается [семантического версионирования](https://semver.org/lang/ru/).
|
проект придерживается [семантического версионирования](https://semver.org/lang/ru/).
|
||||||
|
|
||||||
|
## [0.0.29] — 2026-08-04
|
||||||
|
|
||||||
|
Кнопка «демонстрация экрана» в мини-окне конференции.
|
||||||
|
|
||||||
|
### Добавлено
|
||||||
|
- В мини-окне (Document PiP) рядом с кнопкой микрофона появилась кнопка
|
||||||
|
демонстрации экрана: начать и остановить показ можно, не разворачивая
|
||||||
|
основное окно. Кнопка и кнопка основного тулбара отражают одно
|
||||||
|
состояние — обе читают его из комнаты, а не из разметки.
|
||||||
|
- Своя демонстрация показывается в мини-окне по обычным правилам сцены:
|
||||||
|
забирает крупную плитку при старте, держится, пока говорят другие, и
|
||||||
|
уступает говорящему либо закреплённому участнику после остановки.
|
||||||
|
|
||||||
|
### Примечания
|
||||||
|
- Кнопка есть только там, где мини-окно — настоящее окно со своей
|
||||||
|
разметкой, то есть в Chrome и Edge. В Safari мини-окно выводится
|
||||||
|
средствами системы (нативный «картинка в картинке»), собственных
|
||||||
|
кнопок в нём быть не может; в Firefox мини-окна нет вовсе. Это
|
||||||
|
ограничение браузеров, а не недоработка.
|
||||||
|
- При выборе «весь экран» мини-окно попадает в собственную
|
||||||
|
демонстрацию — как и у всех остальных участников. Чтобы этого
|
||||||
|
избежать, показывайте конкретное окно, а не экран целиком.
|
||||||
|
|
||||||
## [0.0.28] — 2026-08-04
|
## [0.0.28] — 2026-08-04
|
||||||
|
|
||||||
Очередь поднятых рук видна всем участникам + отключаемый модуль.
|
Очередь поднятых рук видна всем участникам + отключаемый модуль.
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ services:
|
|||||||
MEDIA_ROOT: ${MEDIA_ROOT:-/app/media}
|
MEDIA_ROOT: ${MEDIA_ROOT:-/app/media}
|
||||||
# Версия инстанса (релиз v0.0.1) — install.sh копирует значение
|
# Версия инстанса (релиз v0.0.1) — install.sh копирует значение
|
||||||
# из файла VERSION (корень репозитория) в .env; отдаётся в GET /api/health.
|
# из файла VERSION (корень репозитория) в .env; отдаётся в GET /api/health.
|
||||||
VIDCONF_VERSION: ${VIDCONF_VERSION:-0.0.28}
|
VIDCONF_VERSION: ${VIDCONF_VERSION:-0.0.29}
|
||||||
# Число процессов uvicorn (см. backend/Dockerfile). Дефолт 2 рассчитан
|
# Число процессов uvicorn (см. backend/Dockerfile). Дефолт 2 рассчитан
|
||||||
# на 4-ядерный сервер, где ядра делятся с LiveKit. Поднимая значение,
|
# на 4-ядерный сервер, где ядра делятся с LiveKit. Поднимая значение,
|
||||||
# проверьте бюджет соединений с БД: каждый воркер держит свой пул
|
# проверьте бюджет соединений с БД: каждый воркер держит свой пул
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState, type ReactNode } from 'react'
|
import { useEffect, useState, type ReactNode } from 'react'
|
||||||
import { EyeOff, Mic, MicOff, Users } from 'lucide-react'
|
import { EyeOff, Mic, MicOff, ScreenShare, ScreenShareOff, Users } from 'lucide-react'
|
||||||
import { Track, type Participant } from 'livekit-client'
|
import { Track, type Participant } from 'livekit-client'
|
||||||
import {
|
import {
|
||||||
CarouselLayout,
|
CarouselLayout,
|
||||||
@@ -13,8 +13,10 @@ import {
|
|||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
import { RoomParticipantTile } from '@/components/room/RoomParticipantTile'
|
import { RoomParticipantTile } from '@/components/room/RoomParticipantTile'
|
||||||
import { StageGrid } from '@/components/room/StageGrid'
|
import { StageGrid } from '@/components/room/StageGrid'
|
||||||
|
import { useToast } from '@/components/ui/ToastProvider'
|
||||||
import { useIsCompactViewport } from '@/hooks/useIsCompactViewport'
|
import { useIsCompactViewport } from '@/hooks/useIsCompactViewport'
|
||||||
import { pickStageFocus, stageTrackKey } from '@/components/room/stageFocus'
|
import { pickStageFocus, stageTrackKey } from '@/components/room/stageFocus'
|
||||||
|
import { SCREEN_SHARE_CAPTURE_OPTIONS } from '@/lib/screenShareOptions'
|
||||||
import type { StageLayoutMode } from '@/lib/stageLayoutMode'
|
import type { StageLayoutMode } from '@/lib/stageLayoutMode'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,7 +117,7 @@ function PipMicToggle() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
{...mic.buttonProps}
|
{...mic.buttonProps}
|
||||||
className={`room-pip-mic-toggle${mic.enabled ? '' : ' is-off'}`}
|
className={`room-pip-btn room-pip-mic-toggle${mic.enabled ? '' : ' is-off'}`}
|
||||||
aria-label={mic.enabled ? 'Выключить микрофон' : 'Включить микрофон'}
|
aria-label={mic.enabled ? 'Выключить микрофон' : 'Включить микрофон'}
|
||||||
>
|
>
|
||||||
{mic.enabled ? <Mic className="lucide" aria-hidden="true" /> : <MicOff className="lucide" aria-hidden="true" />}
|
{mic.enabled ? <Mic className="lucide" aria-hidden="true" /> : <MicOff className="lucide" aria-hidden="true" />}
|
||||||
@@ -123,6 +125,59 @@ function PipMicToggle() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Кнопка демонстрации экрана в мини-плеере — сестра `PipMicToggle` (0.0.29).
|
||||||
|
* Тот же `useTrackToggle` и те же `SCREEN_SHARE_CAPTURE_OPTIONS`, что у кнопки
|
||||||
|
* основного тулбара (`RoomToolbar`), поэтому обе кнопки — два вида одного
|
||||||
|
* состояния: включённая из мини-окна демонстрация показывает основную кнопку
|
||||||
|
* активной и наоборот, рассинхрону взяться неоткуда (состояние читается из
|
||||||
|
* `RoomContext`, а не из DOM).
|
||||||
|
*
|
||||||
|
* 🔬 ПОЧЕМУ ЭТО ВООБЩЕ РАБОТАЕТ ИЗ ДРУГОГО ОКНА. `getDisplayMedia()` требует
|
||||||
|
* транзиентной активации пользователя, а клик здесь происходит в PiP-окне,
|
||||||
|
* тогда как сам код (React-дерево целиком остаётся в основном документе,
|
||||||
|
* порталом уезжает только DOM) вызывает `navigator` ОСНОВНОГО окна. Замерено
|
||||||
|
* в Chrome 150 отдельной пробой: после клика в Document PiP
|
||||||
|
* `navigator.userActivation.isActive === true` в ОБОИХ окнах — активация
|
||||||
|
* доезжает до опенера, вызов проходит, системный пикер выбора экрана
|
||||||
|
* открывается отдельным окном поверх остальных (а не прячется за заглушкой
|
||||||
|
* «Конференция открыта в отдельном мини-окне» в основном окне). Проверять это
|
||||||
|
* пришлось живьём: спецификация такого поведения не обещает, и на других
|
||||||
|
* движках оно может отличаться — но Document PiP есть только в Chrome/Edge,
|
||||||
|
* так что других движков здесь и не бывает (в Safari мини-окно — нативный
|
||||||
|
* video-PiP без собственного DOM, кнопке там просто негде жить, в Firefox
|
||||||
|
* мини-окна нет вовсе).
|
||||||
|
*
|
||||||
|
* Ошибку показываем тостом основного окна: пользователь его сейчас не видит
|
||||||
|
* (там заглушка), но увидит, как только вернётся, — а единственный частый
|
||||||
|
* «сбой», отмена пикера, и так молча игнорируется, как в основном тулбаре.
|
||||||
|
*/
|
||||||
|
function PipScreenShareToggle() {
|
||||||
|
const toast = useToast()
|
||||||
|
const screenShare = useTrackToggle({
|
||||||
|
source: Track.Source.ScreenShare,
|
||||||
|
captureOptions: SCREEN_SHARE_CAPTURE_OPTIONS,
|
||||||
|
onDeviceError: (error) => {
|
||||||
|
if (error.name === 'NotAllowedError') return
|
||||||
|
toast.show('Не удалось начать демонстрацию экрана', 'error')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
{...screenShare.buttonProps}
|
||||||
|
className={`room-pip-btn room-pip-share-toggle${screenShare.enabled ? ' is-on' : ''}`}
|
||||||
|
aria-label={screenShare.enabled ? 'Остановить демонстрацию экрана' : 'Демонстрация экрана'}
|
||||||
|
>
|
||||||
|
{screenShare.enabled ? (
|
||||||
|
<ScreenShareOff className="lucide" aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<ScreenShare className="lucide" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Основная сцена конференции: превью остальных участников + крупная плитка
|
* Основная сцена конференции: превью остальных участников + крупная плитка
|
||||||
* активного спикера (FocusLayoutContainer + CarouselLayout при нескольких
|
* активного спикера (FocusLayoutContainer + CarouselLayout при нескольких
|
||||||
@@ -441,12 +496,26 @@ export function RoomStage({
|
|||||||
// демонстрации, антидребезг говорящего) — переиспользуем как есть.
|
// демонстрации, антидребезг говорящего) — переиспользуем как есть.
|
||||||
// Булавки на плитке здесь нет намеренно: своего тулбара у мини-окна нет,
|
// Булавки на плитке здесь нет намеренно: своего тулбара у мини-окна нет,
|
||||||
// закрепление делается в основном окне и приезжает сюда через
|
// закрепление делается в основном окне и приезжает сюда через
|
||||||
// `initialPinnedKey`.
|
// `initialPinnedKey`. А вот микрофон и демонстрация экрана в мини-окне есть:
|
||||||
|
// это не «вид», а действия, которые нужны прямо посреди разговора, и ради них
|
||||||
|
// разворачивать основное окно (то есть закрывать мини-окно) бессмысленно.
|
||||||
|
//
|
||||||
|
// СОБСТВЕННАЯ ДЕМОНСТРАЦИЯ в мини-окне ПОКАЗЫВАЕТСЯ — по тем же правилам
|
||||||
|
// `pickStageFocus`, что и в основном окне (last-wins на старте, возврат на
|
||||||
|
// говорящего/закреплённого после остановки). Решение оператора при приёмке
|
||||||
|
// 0.0.29: демонстратор должен видеть, что именно он демонстрирует, ровно то
|
||||||
|
// же, что видят остальные. Пробная версия прятала свой шэр из PiP (чтобы
|
||||||
|
// вместо него была видна аудитория и чтобы при выборе «весь экран» не
|
||||||
|
// получался зеркальный туннель «мини-окно внутри мини-окна») — отклонена.
|
||||||
|
// Туннель при выборе «весь экран» остаётся известным и принятым поведением.
|
||||||
if (variant === 'pip') {
|
if (variant === 'pip') {
|
||||||
return (
|
return (
|
||||||
<section className="stage room-single-tile">
|
<section className="stage room-single-tile">
|
||||||
{focusTrack && <RoomParticipantTile trackRef={focusTrack} onStopSharing={handleStopSharing} />}
|
{focusTrack && <RoomParticipantTile trackRef={focusTrack} onStopSharing={handleStopSharing} />}
|
||||||
|
<div className="room-pip-controls">
|
||||||
<PipMicToggle />
|
<PipMicToggle />
|
||||||
|
<PipScreenShareToggle />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,33 +13,14 @@ import {
|
|||||||
Video,
|
Video,
|
||||||
VideoOff,
|
VideoOff,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Track, type ScreenShareCaptureOptions } from 'livekit-client'
|
import { Track } from 'livekit-client'
|
||||||
import { DisconnectButton, useLocalParticipant, useTrackToggle } from '@livekit/components-react'
|
import { DisconnectButton, useLocalParticipant, useTrackToggle } from '@livekit/components-react'
|
||||||
import { useToast } from '@/components/ui/ToastProvider'
|
import { useToast } from '@/components/ui/ToastProvider'
|
||||||
import { useIsCompactViewport } from '@/hooks/useIsCompactViewport'
|
import { useIsCompactViewport } from '@/hooks/useIsCompactViewport'
|
||||||
import type { HandQueueEntry } from '@/hooks/useChat'
|
import type { HandQueueEntry } from '@/hooks/useChat'
|
||||||
import { StageViewMenu, type StageViewProps } from '@/components/room/StageViewOptions'
|
import { StageViewMenu, type StageViewProps } from '@/components/room/StageViewOptions'
|
||||||
import { HandQueueMenu } from '@/components/room/HandQueueMenu'
|
import { HandQueueMenu } from '@/components/room/HandQueueMenu'
|
||||||
|
import { SCREEN_SHARE_CAPTURE_OPTIONS } from '@/lib/screenShareOptions'
|
||||||
/**
|
|
||||||
* Опции захвата демонстрации экрана: `audio: true` — звук
|
|
||||||
* вкладки/экрана там, где браузер его отдаёт (Chrome/Edge — вкладка почти
|
|
||||||
* всегда, целый экран — только Windows); `selfBrowserSurface: 'exclude'`
|
|
||||||
* — не предлагать в списке
|
|
||||||
* источников собственную вкладку (зеркальный туннель самой конференции);
|
|
||||||
* `surfaceSwitching: 'include'` — разрешить переключать источник прямо во
|
|
||||||
* время демонстрации, не останавливая её; `systemAudio: 'include'` — не
|
|
||||||
* запрещать захват системного звука при выборе «весь экран». Вынесено в
|
|
||||||
* модульную константу — `useTrackToggle` держит `JSON.stringify(captureOptions)`
|
|
||||||
* в зависимостях внутреннего `useMemo`, инлайновый литерал был бы безвреден,
|
|
||||||
* но константа явнее фиксирует неизменность опций.
|
|
||||||
*/
|
|
||||||
const SCREEN_SHARE_CAPTURE_OPTIONS: ScreenShareCaptureOptions = {
|
|
||||||
audio: true,
|
|
||||||
selfBrowserSurface: 'exclude',
|
|
||||||
surfaceSwitching: 'include',
|
|
||||||
systemAudio: 'include',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RoomToolbarProps extends StageViewProps {
|
interface RoomToolbarProps extends StageViewProps {
|
||||||
/** Показывать ли кнопку чата — `JoinOut.chat_enabled` И чат не помечен недоступным (close-код 4404). */
|
/** Показывать ли кнопку чата — `JoinOut.chat_enabled` И чат не помечен недоступным (close-код 4404). */
|
||||||
|
|||||||
29
frontend/src/lib/screenShareOptions.ts
Normal file
29
frontend/src/lib/screenShareOptions.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { ScreenShareCaptureOptions } from 'livekit-client'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Опции захвата демонстрации экрана: `audio: true` — звук
|
||||||
|
* вкладки/экрана там, где браузер его отдаёт (Chrome/Edge — вкладка почти
|
||||||
|
* всегда, целый экран — только Windows); `selfBrowserSurface: 'exclude'`
|
||||||
|
* — не предлагать в списке
|
||||||
|
* источников собственную вкладку (зеркальный туннель самой конференции);
|
||||||
|
* `surfaceSwitching: 'include'` — разрешить переключать источник прямо во
|
||||||
|
* время демонстрации, не останавливая её; `systemAudio: 'include'` — не
|
||||||
|
* запрещать захват системного звука при выборе «весь экран».
|
||||||
|
*
|
||||||
|
* Вынесено в модульную константу — `useTrackToggle` держит
|
||||||
|
* `JSON.stringify(captureOptions)` в зависимостях внутреннего `useMemo`,
|
||||||
|
* инлайновый литерал был бы безвреден, но константа явнее фиксирует
|
||||||
|
* неизменность опций.
|
||||||
|
*
|
||||||
|
* Живёт в `lib/`, а не рядом с тулбаром, потому что кнопок демонстрации теперь
|
||||||
|
* ДВЕ: в основном тулбаре (`RoomToolbar`) и в мини-окне (`RoomStage`,
|
||||||
|
* `PipScreenShareToggle`). Опции у них обязаны совпадать: обе кнопки управляют
|
||||||
|
* одной и той же публикацией, и разойдись они хотя бы в `audio`, демонстрация
|
||||||
|
* получалась бы разной в зависимости от того, откуда её запустили.
|
||||||
|
*/
|
||||||
|
export const SCREEN_SHARE_CAPTURE_OPTIONS: ScreenShareCaptureOptions = {
|
||||||
|
audio: true,
|
||||||
|
selfBrowserSurface: 'exclude',
|
||||||
|
surfaceSwitching: 'include',
|
||||||
|
systemAudio: 'include',
|
||||||
|
}
|
||||||
@@ -133,18 +133,40 @@ video[data-lk-source='screen_share'] { object-fit: contain; background: #000; }
|
|||||||
* `.stage-tiles` в основном окне) плюс подстраховочный `min-height`, чтобы
|
* `.stage-tiles` в основном окне) плюс подстраховочный `min-height`, чтобы
|
||||||
* плитка не схлопывалась, если видео ещё не выдало реальные размеры.
|
* плитка не схлопывалась, если видео ещё не выдало реальные размеры.
|
||||||
*/
|
*/
|
||||||
.room-single-tile { position: relative; flex: 1; min-height: 0; display: flex; }
|
.room-single-tile {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
/* Ширина ряда кнопок мини-плеера (`.room-pip-controls`): две круглые кнопки
|
||||||
|
по 36px плюс зазор. Живёт на общем предке, а не на самом ряду, потому что
|
||||||
|
от неё считается и отступ плашки с именем участника — иначе подпись
|
||||||
|
(«Второй участник — демонстрация экрана») уезжает ПОД кнопки. Появится
|
||||||
|
третья кнопка — правится одно число, а не два места. */
|
||||||
|
--room-pip-controls-width: calc(2 * 36px + 0.375rem);
|
||||||
|
}
|
||||||
.room-single-tile .lk-participant-tile { flex: 1; min-height: 220px; width: 100%; }
|
.room-single-tile .lk-participant-tile { flex: 1; min-height: 220px; width: 100%; }
|
||||||
|
/* Библиотечная плашка с именем прижата к обоим нижним углам (`left: 4px;
|
||||||
|
right: 4px`) и потому начинается ровно там, где лежат наши кнопки. Сдвигаем
|
||||||
|
её содержимое вправо на ширину ряда — сама плашка прозрачная, подпись внутри
|
||||||
|
со своим фоном, так что двигать надо именно отступ, а не `left`. */
|
||||||
|
.room-single-tile .lk-participant-metadata { padding-left: var(--room-pip-controls-width); }
|
||||||
|
|
||||||
/* Кнопка микрофона мини-плеера (задача A5) — своего тулбара в PiP нет,
|
/* Кнопки мини-плеера (микрофон — задача A5, демонстрация экрана — 0.0.29):
|
||||||
кнопка лежит поверх единственной плитки левым нижним углом (правый занят
|
своего тулбара в PiP нет, кнопки лежат поверх единственной плитки левым
|
||||||
штатным `.lk-focus-toggle-button`/нашей булавкой закрепления — тут её нет,
|
нижним углом (правый занят штатным `.lk-focus-toggle-button`/нашей булавкой
|
||||||
в PiP закреплять нечего, но геометрия зеркалит `.room-pin-toggle`). */
|
закрепления — тут её нет, в PiP закреплять нечего, но геометрия зеркалит
|
||||||
.room-pip-mic-toggle {
|
`.room-pin-toggle`). Ряд, а не две абсолютно спозиционированные кнопки:
|
||||||
|
так добавление третьей не потребует пересчёта координат. */
|
||||||
|
.room-pip-controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0.5rem;
|
left: 0.5rem;
|
||||||
bottom: 0.5rem;
|
bottom: 0.5rem;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
.room-pip-btn {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -152,12 +174,16 @@ video[data-lk-source='screen_share'] { object-fit: contain; background: #000; }
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: var(--color-room-mic-on);
|
|
||||||
color: #10331f;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.room-pip-mic-toggle svg { width: 18px; height: 18px; }
|
.room-pip-btn svg { width: 18px; height: 18px; }
|
||||||
|
.room-pip-mic-toggle { background: var(--color-room-mic-on); color: #10331f; }
|
||||||
.room-pip-mic-toggle.is-off { background: var(--color-room-danger-bg); color: #fff; }
|
.room-pip-mic-toggle.is-off { background: var(--color-room-danger-bg); color: #fff; }
|
||||||
|
/* Демонстрация — «выключено» это нейтральное состояние (не тревога, в отличие
|
||||||
|
от снятого микрофона), поэтому неактивная кнопка тёмная под цвет плитки, а
|
||||||
|
активная подсвечена тем же зелёным, что включённый микрофон. */
|
||||||
|
.room-pip-share-toggle { background: var(--color-room-tile-hover); color: var(--color-room-text-primary); }
|
||||||
|
.room-pip-share-toggle.is-on { background: var(--color-room-mic-on); color: #10331f; }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ---------- Колонка миниатюр (кнопка «Скрыть» + карусель) ----------
|
* ---------- Колонка миниатюр (кнопка «Скрыть» + карусель) ----------
|
||||||
|
|||||||
Reference in New Issue
Block a user