Первоначальная версия VidConf
This commit is contained in:
150
frontend/src/styles/admin.css
Normal file
150
frontend/src/styles/admin.css
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* VidConf — стили страницы «Администрирование».
|
||||
* Табличные/toolbar-паттерны — 1:1 из design/mockups/admin.html
|
||||
* (.section-tab/.toolbar-row/.search-wrap/.table-card/.badge-role-*).
|
||||
* Вкладка «Настройки» в макете отсутствует — построена по тому же
|
||||
* визуальному языку (.settings-card повторяет .table-card/.invite-card).
|
||||
* page-shell/page-main/page-head — общие из lobby.css; кнопки, модалки и
|
||||
* переключатель switch — из shell.css и calendar.css.
|
||||
*
|
||||
* Все правила скопированы под `.admin-page`, чтобы избежать конфликта имён
|
||||
* классов (`.icon-btn`, `.chip`) с другими светлыми страницами оболочки —
|
||||
* в проекте уже был прецедент такой коллизии (см. комментарий в
|
||||
* my-conferences.css про порядок правил в исходнике).
|
||||
*/
|
||||
|
||||
.admin-page .section-tabs { display: flex; gap: var(--space-2); margin-bottom: var(--space-5); border-bottom: 1px solid var(--color-border); }
|
||||
.admin-page .section-tab { padding: 10px 4px; margin-right: var(--space-6); background: none; border: none; border-bottom: 2px solid transparent; font: var(--text-body-lg); font-weight: 600; color: var(--color-ink-500); cursor: pointer; }
|
||||
.admin-page .section-tab:hover { color: var(--color-ink-900); }
|
||||
.admin-page .section-tab.is-active { color: var(--color-ink-700); border-color: var(--color-accent-active); }
|
||||
|
||||
.admin-page .section-tabs-inline { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: var(--space-4); }
|
||||
.admin-page .chip { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: var(--radius-full); border: 1.5px solid var(--color-border); background: var(--color-surface); font: var(--text-body); font-weight: 600; color: var(--color-ink-500); cursor: pointer; }
|
||||
.admin-page .chip.is-active { border-color: var(--color-ink-700); color: var(--color-ink-700); background: var(--color-bg-alt); }
|
||||
|
||||
.admin-page .toolbar-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-5); gap: var(--space-4); flex-wrap: wrap; }
|
||||
/* Группа «поиск + кнопка действия» слева в toolbar-row («Добавить
|
||||
пользователя») — без неё третий элемент в flex-контейнере с
|
||||
justify-content: space-between распределился бы отдельной колонкой, а не
|
||||
встал рядом с поиском. */
|
||||
.admin-page .toolbar-left { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
|
||||
.admin-page .search-wrap { position: relative; width: 320px; max-width: 100%; }
|
||||
.admin-page .search-wrap .icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--color-ink-400); }
|
||||
/* color/::placeholder — намеренно НЕ в тёмном скоупе: раньше их не было
|
||||
вовсе, текст/плейсхолдер рисовались UA-дефолтом браузера, а не хардкодом
|
||||
светлой темы, — в светлой это давало почти совпадающий с ink-900/ink-300
|
||||
результат, а в тёмной ломалось (тёмный UA-текст на тёмном фоне). var()
|
||||
резолвится в обеих темах сам, поэтому одно правило на все три скоупа
|
||||
корректно и для светлой, и для тёмной — оборачивать не в чем и незачем. */
|
||||
.admin-page .search-wrap input { width: 100%; font: var(--text-body); color: var(--color-ink-900); padding: 9px 12px 9px 38px; border-radius: var(--radius-full); border: 1px solid var(--color-border); background: var(--color-surface); }
|
||||
.admin-page .search-wrap input::placeholder { color: var(--color-ink-300); }
|
||||
.admin-page .search-wrap input:focus { outline: none; border-color: var(--color-focus-ring); }
|
||||
.admin-page .toolbar-count { font: var(--text-body); color: var(--color-ink-500); white-space: nowrap; }
|
||||
|
||||
.admin-page .table-card { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-2xl); overflow: hidden; margin-bottom: var(--space-6); }
|
||||
.admin-page table { width: 100%; border-collapse: collapse; }
|
||||
.admin-page thead th { text-align: left; background: var(--color-bg-alt); font: var(--text-caption); color: var(--color-ink-500); padding: 12px 20px; text-transform: uppercase; }
|
||||
.admin-page tbody td { padding: 14px 20px; font: var(--text-body-lg); color: var(--color-ink-900); border-top: 1px solid var(--color-border); }
|
||||
.admin-page tbody tr:hover { background: var(--color-bg-alt); }
|
||||
|
||||
.admin-page .badge-role-admin { background: var(--color-ink-700); color: #fff; }
|
||||
.admin-page .badge-role-user { background: var(--color-bg-alt); color: var(--color-ink-500); }
|
||||
|
||||
.admin-page .row-actions { display: flex; gap: 6px; }
|
||||
.admin-page .icon-btn { width: 32px; height: 32px; border-radius: var(--radius-md); border: 1px solid var(--color-border); background: var(--color-surface); color: var(--color-ink-500); display: flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; }
|
||||
.admin-page .icon-btn:hover:not(:disabled) { border-color: var(--color-border-strong); color: var(--color-ink-900); }
|
||||
.admin-page .icon-btn.danger:hover:not(:disabled) { border-color: var(--color-danger); color: var(--color-danger); background: var(--color-danger-bg); }
|
||||
.admin-page .icon-btn:disabled { opacity: .45; cursor: not-allowed; }
|
||||
|
||||
.admin-page .user-cell { display: flex; align-items: center; gap: 10px; }
|
||||
.admin-page .user-cell .avatar { width: 32px; height: 32px; font-size: 12px; }
|
||||
.admin-page .user-cell div span { display: block; font: 13px var(--font-body); color: var(--color-ink-500); }
|
||||
/* Клик по имени открывает карточку профиля — визуально
|
||||
остаётся обычным текстом (не похоже на ссылку), подчёркивание — только hover. */
|
||||
.admin-page .user-cell-name { background: none; border: none; padding: 0; font: inherit; color: inherit; cursor: pointer; text-align: left; }
|
||||
.admin-page .user-cell-name:hover { text-decoration: underline; color: var(--color-ink-700); }
|
||||
|
||||
.admin-page select { font: var(--text-body); color: var(--color-ink-900); padding: 6px 10px; border-radius: var(--radius-md); border: 1px solid var(--color-border); background: var(--color-surface); font-family: var(--font-body); }
|
||||
.admin-page select:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.admin-page select:focus { outline: none; border-color: var(--color-focus-ring); }
|
||||
|
||||
.admin-page .pagination-row { display: flex; justify-content: center; gap: var(--space-3); margin-bottom: var(--space-8); }
|
||||
|
||||
/* ---------- Вкладка «Команды» (нет в макете — паттерн toolbar-row/table-card) ---------- */
|
||||
/* color/::placeholder — то же обоснование, что у .search-wrap input выше. */
|
||||
.admin-page .team-add-form { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
|
||||
.admin-page .team-add-form input { width: 280px; max-width: 100%; font: var(--text-body); color: var(--color-ink-900); padding: 9px 14px; border-radius: var(--radius-full); border: 1px solid var(--color-border); background: var(--color-surface); }
|
||||
.admin-page .team-add-form input::placeholder { color: var(--color-ink-300); }
|
||||
.admin-page .team-add-form input:focus { outline: none; border-color: var(--color-focus-ring); }
|
||||
.admin-page .team-rename-form input { width: 240px; max-width: 100%; font: var(--text-body-lg); color: var(--color-ink-900); padding: 6px 10px; border-radius: var(--radius-md); border: 1px solid var(--color-border-strong); background: var(--color-surface); }
|
||||
.admin-page .team-rename-form input:focus { outline: none; border-color: var(--color-focus-ring); }
|
||||
|
||||
/* ---------- Вкладка «Настройки» (нет в макете — паттерн table-card/invite-card) ---------- */
|
||||
/* Соседние карточки внутри строки грида должны быть одной высоты: без
|
||||
align-items:stretch (значение по умолчанию для grid) каждая плитка
|
||||
сжималась бы по своему контенту, и при разном наполнении («Модули» с
|
||||
четырьмя переключателями против правой колонки, «Рассылка саммари»
|
||||
против «Часового пояса») ряд выглядел рваным. */
|
||||
.admin-page .settings-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-5); }
|
||||
.admin-page .settings-card { display: flex; flex-direction: column; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-2xl); padding: var(--space-6); }
|
||||
.admin-page .settings-card h2 { font: var(--text-h2); margin: 0 0 4px; color: var(--color-ink-900); }
|
||||
.admin-page .settings-card .desc { font: var(--text-body); color: var(--color-ink-500); margin: 0 0 var(--space-4); }
|
||||
.admin-page .settings-actions { grid-column: 1 / -1; display: flex; justify-content: flex-end; }
|
||||
|
||||
/* Первый ряд грида: левая плитка «Модули» (grid-item, растягивается по
|
||||
умолчанию align-items:stretch) и правая колонка — вертикальный стек из
|
||||
«Уровень AI» и «Эталон mail-домена». Стек как grid-item тоже растягивается
|
||||
до высоты ряда; внутри него flex-grow на карточках распределяет остаток
|
||||
высоты поровну между двумя правыми плитками, поэтому итоговая высота
|
||||
левой и суммарная высота правых двух совпадают без фиксации пикселей.
|
||||
Важно: flex-basis только auto и flex-shrink:0 — карточка не должна
|
||||
ужиматься ниже высоты своего контента (список уровней AI/подпись+инпут),
|
||||
иначе при `flex-basis: 0` контент переполняет сжатый бокс и визуально
|
||||
наезжает на соседнюю плитку снизу (overflow: visible по умолчанию не
|
||||
обрезает переполнение, а просто рисует поверх соседа). */
|
||||
.admin-page .settings-stack { display: flex; flex-direction: column; gap: var(--space-5); }
|
||||
.admin-page .settings-stack .settings-card { flex: 1 0 auto; }
|
||||
|
||||
/* Инпут эталонного домена дизейблится при выключенном тумблере верификации,
|
||||
но значение поля при этом не сбрасывается (сохраняется в стейте формы). */
|
||||
.admin-page .settings-card .field input:disabled { opacity: .55; cursor: not-allowed; background: var(--color-bg-alt); }
|
||||
|
||||
/* Тело карточки занимает всё оставшееся место после заголовка/описания —
|
||||
именно за счёт flex:1 плитка ниже соседней по контенту получает
|
||||
аккуратно распределённую пустоту, а не пустой хвост снизу. */
|
||||
.admin-page .settings-card-body { display: flex; flex-direction: column; flex: 1; }
|
||||
.admin-page .settings-card-body--spread { justify-content: space-between; }
|
||||
.admin-page .settings-card-body--center { justify-content: center; }
|
||||
|
||||
.admin-page .ai-level-list { display: flex; flex-direction: column; gap: var(--space-3); }
|
||||
.admin-page .ai-level-option { display: flex; align-items: flex-start; gap: 10px; padding: var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); cursor: pointer; }
|
||||
.admin-page .ai-level-option:hover { border-color: var(--color-border-strong); }
|
||||
.admin-page .ai-level-option.is-disabled { cursor: not-allowed; opacity: .6; }
|
||||
.admin-page .ai-level-option input[type="radio"] { margin-top: 3px; width: 16px; height: 16px; accent-color: var(--color-ink-700); flex-shrink: 0; }
|
||||
.admin-page .ai-level-option strong { display: block; font: var(--text-body); font-weight: 600; color: var(--color-ink-900); }
|
||||
/* align-items: flex-start (не center) — причины недоступности
|
||||
Блок 1) могут быть длинными («модель X не скачана — запустите install.sh
|
||||
с пресетом N») и переноситься на несколько строк; при center иконка
|
||||
съезжала бы к середине блока текста, а не к его первой строке. */
|
||||
.admin-page .ai-level-reason { display: flex; align-items: flex-start; gap: 5px; font: var(--text-caption); color: var(--color-warning); margin-top: 4px; }
|
||||
.admin-page .ai-level-reason svg { flex-shrink: 0; margin-top: 1px; }
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.admin-page .settings-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* ---------- Футер админки ----------
|
||||
Версия инстанса из `/api/health` (см. AdminPage.tsx) — до появления поля
|
||||
на backend строка сокращается до одного «VidConf» без версии. */
|
||||
.admin-page .admin-footer { margin-top: var(--space-6); padding-top: var(--space-4); border-top: 1px solid var(--color-border); font: var(--text-caption); color: var(--color-ink-400); text-align: center; }
|
||||
|
||||
/* =====================================================================
|
||||
* Тёмная тема: заливка var(--color-ink-700) (индиго) сочеталась с жёстким
|
||||
* color:#fff; в тёмной теме ink-700 = мята (светлый тон) — белый текст
|
||||
* перестаёт читаться, заменено на var(--color-accent-text) (тёмный, для
|
||||
* заливки мятой). См. design/mockups/dark/README.md, категория 1.
|
||||
* ===================================================================== */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) .admin-page .badge-role-admin { color: var(--color-accent-text); }
|
||||
}
|
||||
:root[data-theme='dark'] .admin-page .badge-role-admin { color: var(--color-accent-text); }
|
||||
270
frontend/src/styles/auth.css
Normal file
270
frontend/src/styles/auth.css
Normal file
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* VidConf — стили экранов auth (вход/регистрация/подтверждение email).
|
||||
* 1:1 воспроизводит design/mockups/auth.html.
|
||||
*/
|
||||
|
||||
.layout { display: flex; min-height: 100vh; }
|
||||
|
||||
/* ---------- Брендовая панель ---------- */
|
||||
.brand-panel {
|
||||
width: 42%;
|
||||
background: linear-gradient(160deg, #EDEFFA 0%, #F1F1F1 55%, #F7F7F8 100%);
|
||||
padding: var(--space-12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
.brand-panel::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -120px;
|
||||
bottom: -120px;
|
||||
width: 380px;
|
||||
height: 380px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 30% 30%, rgba(212, 242, 227, 0.45), rgba(212, 242, 227, 0) 70%);
|
||||
}
|
||||
.brand-copy { z-index: 1; }
|
||||
.eyebrow {
|
||||
font: var(--text-caption);
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--color-ink-400);
|
||||
margin: 0 0 var(--space-4);
|
||||
}
|
||||
.brand-headline {
|
||||
font: var(--text-display-lg);
|
||||
font-family: var(--font-display);
|
||||
color: var(--color-ink-700);
|
||||
margin: 0 0 var(--space-4);
|
||||
max-width: 460px;
|
||||
}
|
||||
.brand-sub {
|
||||
font: var(--text-body-lg);
|
||||
color: var(--color-ink-500);
|
||||
max-width: 420px;
|
||||
margin: 0;
|
||||
}
|
||||
.brand-stats { display: flex; gap: var(--space-4); z-index: 1; margin-top: var(--space-8); }
|
||||
.stat-glass {
|
||||
background: var(--color-surface-glass);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||
box-shadow: var(--shadow-glass);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
min-width: 140px;
|
||||
}
|
||||
.stat-glass strong { display: block; font: var(--text-h1); font-family: var(--font-display); color: var(--color-ink-700); }
|
||||
.stat-glass span { font: var(--text-caption); color: var(--color-ink-500); text-transform: none; letter-spacing: normal; }
|
||||
|
||||
/* ---------- Форма-панель ---------- */
|
||||
.form-panel {
|
||||
width: 58%;
|
||||
padding: var(--space-12) var(--space-16);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.screen-block { max-width: 440px; width: 100%; }
|
||||
.screen-title { font: var(--text-h1); color: var(--color-ink-900); margin: 0 0 var(--space-8); }
|
||||
|
||||
.tabs { display: flex; gap: var(--space-2); margin-bottom: var(--space-6); background: var(--color-bg-alt); padding: 4px; border-radius: var(--radius-full); width: fit-content; }
|
||||
.tab {
|
||||
padding: 8px 20px;
|
||||
border-radius: var(--radius-full);
|
||||
border: none;
|
||||
background: transparent;
|
||||
font: var(--text-body);
|
||||
color: var(--color-ink-500);
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
}
|
||||
.tab.is-active { background: var(--color-surface); color: var(--color-ink-700); font-weight: 600; box-shadow: var(--shadow-sm); }
|
||||
|
||||
.field { margin-bottom: var(--space-5); }
|
||||
.field label { display: block; font: var(--text-body); font-weight: 600; color: var(--color-ink-900); margin-bottom: var(--space-2); }
|
||||
.input-wrap { position: relative; display: flex; align-items: center; }
|
||||
.input-wrap .field-icon { position: absolute; left: 14px; color: var(--color-ink-400); pointer-events: none; }
|
||||
.input-wrap input, .input-wrap select {
|
||||
width: 100%;
|
||||
font: var(--text-body-lg);
|
||||
font-family: var(--font-body);
|
||||
color: var(--color-ink-900);
|
||||
padding: 11px 14px 11px 42px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
outline: none;
|
||||
}
|
||||
.input-wrap input::placeholder { color: var(--color-ink-300); }
|
||||
.input-wrap input:hover, .input-wrap select:hover { border-color: var(--color-border-strong); }
|
||||
.input-wrap input:focus, .input-wrap select:focus { border-color: var(--color-focus-ring); box-shadow: 0 0 0 3px rgba(59, 77, 149, 0.12); }
|
||||
/* Нативный select в WebKit/Safari игнорирует border/border-radius/background
|
||||
без явного отключения UA-рендеринга: без appearance:none контрол рисуется
|
||||
системным виджетом («аквариумная» рамка ОС) поверх авторских стилей — этим
|
||||
объясняется «браузерный дефолт» без скругления и с меньшей высотой. */
|
||||
.input-wrap select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
padding-right: 38px;
|
||||
cursor: pointer;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236976AC' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 14px center;
|
||||
background-size: 16px;
|
||||
}
|
||||
.input-wrap .toggle-eye {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
left: auto;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--color-ink-400);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.field.has-error input { border-color: var(--color-danger); box-shadow: 0 0 0 3px rgba(199, 54, 79, 0.1); }
|
||||
.error-msg { display: flex; align-items: center; gap: 6px; margin-top: var(--space-2); font: var(--text-body); color: var(--color-danger); }
|
||||
.hint-msg { margin-top: var(--space-2); font: var(--text-body); color: var(--color-ink-500); }
|
||||
|
||||
.row-between { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-6); }
|
||||
.checkbox-row { display: flex; align-items: center; gap: 8px; font: var(--text-body); color: var(--color-ink-500); }
|
||||
.link { color: var(--color-ink-700); font-weight: 600; text-decoration: none; background: none; border: none; cursor: pointer; font: var(--text-body); }
|
||||
.link:hover { text-decoration: underline; }
|
||||
|
||||
.btn-block { width: 100%; }
|
||||
.btn-primary.btn-lg { font: var(--text-body-lg); font-weight: 700; padding: 13px 24px; }
|
||||
.btn-secondary.btn-lg { font: var(--text-body-lg); font-weight: 600; padding: 12px 24px; }
|
||||
|
||||
/* ---------- Подтверждение email ---------- */
|
||||
.confirm-card {
|
||||
max-width: 440px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-2xl);
|
||||
padding: var(--space-8);
|
||||
text-align: left;
|
||||
}
|
||||
.confirm-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-success-bg);
|
||||
color: var(--color-success);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
.confirm-card h3 { font: var(--text-h2); color: var(--color-ink-900); margin: 0 0 var(--space-2); }
|
||||
.confirm-card p { font: var(--text-body-lg); color: var(--color-ink-500); margin: 0 0 var(--space-6); }
|
||||
.confirm-card .mail-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--color-bg-alt);
|
||||
border-radius: var(--radius-full);
|
||||
padding: 6px 14px;
|
||||
font: var(--text-mono-sm);
|
||||
color: var(--color-ink-700);
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
.confirm-card.pending .confirm-icon { background: var(--color-warning-bg); color: var(--color-warning); }
|
||||
.confirm-card.error .confirm-icon { background: var(--color-danger-bg); color: var(--color-danger); }
|
||||
|
||||
/* ---------- Переключатель темы: плавающая плашка (см. AuthLayout.tsx) ---------- */
|
||||
.theme-toggle--floating {
|
||||
position: fixed;
|
||||
top: 24px;
|
||||
right: 32px;
|
||||
z-index: 50;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
* Тёмная тема: точечные патчи (design/mockups/dark/README.md, категория
|
||||
* «декоративные градиенты/тени») — литеральные rgba построены на индиго/
|
||||
* старом danger-hex светлой темы, поэтому не перекрашиваются через var().
|
||||
* ===================================================================== */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) .brand-panel {
|
||||
background: linear-gradient(160deg, #242424 0%, #1E1E1E 55%, #1A1A1A 100%);
|
||||
}
|
||||
:root:not([data-theme]) .brand-panel::after {
|
||||
background: radial-gradient(circle at 30% 30%, rgba(127, 221, 168, 0.16), rgba(127, 221, 168, 0) 70%);
|
||||
}
|
||||
:root:not([data-theme]) .stat-glass { border-color: rgba(255, 255, 255, 0.08); }
|
||||
}
|
||||
:root[data-theme='dark'] .brand-panel {
|
||||
background: linear-gradient(160deg, #242424 0%, #1E1E1E 55%, #1A1A1A 100%);
|
||||
}
|
||||
:root[data-theme='dark'] .brand-panel::after {
|
||||
background: radial-gradient(circle at 30% 30%, rgba(127, 221, 168, 0.16), rgba(127, 221, 168, 0) 70%);
|
||||
}
|
||||
:root[data-theme='dark'] .stat-glass { border-color: rgba(255, 255, 255, 0.08); }
|
||||
|
||||
/* ---------- Узкие экраны ---------- */
|
||||
/* ВАЖНО: этот блок должен идти строго ПОСЛЕДНИМ в файле — при равной
|
||||
специфичности с безусловными правилами выше (`.brand-panel`, `.form-panel`
|
||||
и т.п. — те же 0,1,0) побеждает порядок в исходнике, а не сам факт
|
||||
совпадения media query (тот же баг уже ловили в lobby.css/.action, гейт 3
|
||||
— см. комментарий там). Макет auth.html — desktop-only
|
||||
(`html { min-width: 1280px }`), адаптива в нём нет.
|
||||
Причина исходного бага: `.layout` — flex-row из двух панелей с width
|
||||
в процентах и без flex-shrink:0, а `.brand-panel` — overflow:hidden (нужен
|
||||
для декоративного круга). По спецификации flex, overflow:hidden отключает
|
||||
автоматический min-width:auto (= min-content) и делает его 0 — поэтому
|
||||
при узком viewport панель схлопывалась намного уже своего текста, а не
|
||||
переносила его: overflow:hidden просто обрезал то, что не поместилось,
|
||||
вместо переноса строк. Решение — не подбирать min-width, а на узких
|
||||
экранах складывать колонки в одну: тогда ширина каждой панели равна 100%
|
||||
и конкуренции за пространство между ними нет вовсе. */
|
||||
@media (max-width: 900px) {
|
||||
.layout {
|
||||
flex-direction: column;
|
||||
min-height: auto;
|
||||
}
|
||||
.brand-panel {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: var(--space-8) var(--space-6) var(--space-6);
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.brand-panel::after {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
right: -90px;
|
||||
bottom: -90px;
|
||||
}
|
||||
.brand-headline { max-width: none; }
|
||||
.brand-sub { max-width: none; }
|
||||
.brand-stats { flex-wrap: wrap; }
|
||||
.stat-glass { flex: 1 1 140px; min-width: 0; }
|
||||
|
||||
.form-panel {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: var(--space-8) var(--space-6);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.screen-block { max-width: 100%; }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* Совсем узкий телефон (375px) — экономим отступы и убираем декоративный
|
||||
круг: на компактном hero он ближе к тексту и уже не читается как акцент. */
|
||||
.brand-panel { padding: var(--space-6) var(--space-4); }
|
||||
.brand-panel::after { display: none; }
|
||||
.brand-stats { gap: var(--space-3); }
|
||||
.stat-glass { flex-basis: 100%; }
|
||||
|
||||
.form-panel { padding: var(--space-6) var(--space-4) var(--space-10); }
|
||||
}
|
||||
273
frontend/src/styles/calendar.css
Normal file
273
frontend/src/styles/calendar.css
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* VidConf — стили экрана календаря конференций: менеджер
|
||||
* запланированных/закреплённых конференций (переговорных комнат и их
|
||||
* занятости в модели нет). Базовая раскладка/токены — 1:1 из design/mockups/calendar.html
|
||||
* (.layout-grid, .week-card, .create-card, .pin-block, .recurrence-panel).
|
||||
* Тема FullCalendar адаптирована под dayGrid-«чипы» вместо timeGrid-слотов
|
||||
* (см. комментарий в ConferenceCalendar.tsx).
|
||||
*/
|
||||
|
||||
/* ---------- Грид: сетка вхождений + карточка создания/редактирования ---------- */
|
||||
.layout-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 400px;
|
||||
gap: var(--space-6);
|
||||
align-items: start;
|
||||
}
|
||||
/* min-width:0 — без этого грид-элемент с 1fr-треком не сжимается уже своего
|
||||
min-content (тут это внутренняя таблица FullCalendar с 5 колонками) —
|
||||
классический grid blowout: трек и вся страница раздуваются шире вьюпорта,
|
||||
появляется горизонтальный скролл (см. тот же приём в lobby.css/.action). */
|
||||
.layout-grid > * { min-width: 0; }
|
||||
|
||||
.week-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-2xl);
|
||||
padding: var(--space-6);
|
||||
}
|
||||
.week-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-5); flex-wrap: wrap; gap: var(--space-3); }
|
||||
|
||||
.date-nav { display: flex; align-items: center; gap: var(--space-3); }
|
||||
.date-nav-arrow {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-ink-500);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.date-nav-arrow:hover { border-color: var(--color-border-strong); color: var(--color-ink-900); }
|
||||
.date-nav span { font: var(--text-h3); color: var(--color-ink-900); min-width: 200px; text-align: center; text-transform: capitalize; }
|
||||
.date-nav .btn-secondary { flex-shrink: 0; white-space: nowrap; }
|
||||
|
||||
.view-toggle { display: flex; gap: 4px; background: var(--color-bg-alt); border: 1px solid var(--color-border); border-radius: var(--radius-full); padding: 4px; }
|
||||
.view-toggle button { border: none; background: transparent; border-radius: var(--radius-full); padding: 7px 16px; font: var(--text-body); font-weight: 600; color: var(--color-ink-500); }
|
||||
.view-toggle button.is-active { background: var(--color-surface); color: var(--color-ink-700); box-shadow: var(--shadow-sm); }
|
||||
|
||||
/* ---------- Тема FullCalendar (dayGrid) ---------- */
|
||||
.vc-calendar { font-family: var(--font-body); }
|
||||
.vc-calendar .fc { --fc-border-color: var(--color-border); --fc-page-bg-color: var(--color-surface); --fc-neutral-bg-color: var(--color-bg-alt); --fc-today-bg-color: rgba(212,242,227,.16); }
|
||||
.vc-calendar .fc-theme-standard td, .vc-calendar .fc-theme-standard th { border-color: var(--color-border); }
|
||||
.vc-calendar .fc-col-header-cell { background: var(--color-bg-alt); padding: 6px 0; }
|
||||
.vc-calendar .fc-col-header-cell-cushion { font: var(--text-caption); color: var(--color-ink-500); text-decoration: none; padding: 6px 0; }
|
||||
.vc-calendar .fc-daygrid-day-frame { min-height: 460px; padding: var(--space-2); }
|
||||
.vc-calendar .fc-daygrid-day-number { font: var(--text-h3); color: var(--color-ink-900); padding: 4px 6px; }
|
||||
.vc-calendar .fc-day-today { background: rgba(212,242,227,.16) !important; }
|
||||
.vc-calendar .fc-day-today .fc-daygrid-day-number { color: var(--color-ink-700); }
|
||||
.vc-calendar .fc-daygrid-day-events { margin: 0; display: flex; flex-direction: column; gap: var(--space-2); }
|
||||
.vc-calendar .fc-daygrid-event-harness { margin: 0 !important; }
|
||||
.vc-calendar .fc-daygrid-more-link { font: var(--text-caption); color: var(--color-ink-500); }
|
||||
|
||||
/*
|
||||
* Событие-вхождение конференции — карточка-«чип» в духе .conf-chip мокапа.
|
||||
* FullCalendar оборачивает событие в .fc-daygrid-event (наследует классы из
|
||||
* `classNames`) — переопределяем его внешний вид под чип целиком, без
|
||||
* отдельного контейнера.
|
||||
*/
|
||||
.vc-calendar .fc-daygrid-event.conf-chip {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
background: var(--color-surface) !important;
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
padding: var(--space-3);
|
||||
white-space: normal;
|
||||
cursor: pointer;
|
||||
transition: box-shadow var(--transition-fast), border-color var(--transition-fast);
|
||||
}
|
||||
.vc-calendar .fc-daygrid-event.conf-chip:hover { box-shadow: var(--shadow-sm); border-color: var(--color-border-strong); }
|
||||
.vc-calendar .fc-daygrid-event.conf-chip .fc-event-time,
|
||||
.vc-calendar .fc-daygrid-event.conf-chip .fc-event-title { display: none; } /* заменено собственным eventContent */
|
||||
|
||||
.chip-time { display: block; font: var(--text-caption); color: var(--color-ink-500); text-transform: none; letter-spacing: normal; }
|
||||
.chip-title { display: flex; align-items: center; gap: 6px; font: var(--text-body); font-weight: 700; color: var(--color-ink-900); }
|
||||
.chip-lock { width: 13px; height: 13px; flex-shrink: 0; }
|
||||
|
||||
.conf-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 9px; border-radius: var(--radius-full); font: var(--text-caption); font-size: 11px; width: fit-content; }
|
||||
.conf-badge.scheduled { background: var(--color-warning-bg); color: var(--color-warning); }
|
||||
.conf-badge.pinned { background: var(--color-bg-alt); color: var(--color-ink-700); border: 1px solid var(--color-border-strong); }
|
||||
.conf-badge.live { background: var(--color-success-bg); color: var(--color-success); }
|
||||
.conf-badge.ended { background: var(--color-bg-alt); color: var(--color-ink-500); }
|
||||
|
||||
.vc-empty-hint { text-align: center; padding: var(--space-8); color: var(--color-ink-500); font: var(--text-body-lg); }
|
||||
|
||||
/* ---------- Карточка создания/редактирования конференции ---------- */
|
||||
.create-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-2xl);
|
||||
padding: var(--space-6);
|
||||
position: sticky;
|
||||
top: 96px;
|
||||
}
|
||||
.create-card h2 { font: var(--text-h2); color: var(--color-ink-900); margin: 0 0 var(--space-5); display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
.field-row { display: flex; gap: var(--space-3); }
|
||||
.field-row .field { flex: 1; }
|
||||
|
||||
.next-occurrence { display: flex; align-items: center; gap: 8px; background: var(--color-bg-alt); border-radius: var(--radius-md); padding: 10px 14px; font: var(--text-body); color: var(--color-ink-700); font-weight: 600; margin-bottom: var(--space-5); }
|
||||
|
||||
/* Тумблер (используется и для «закрытая по паролю», и для «закрепить») */
|
||||
.toggle-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) 0; border-top: 1px solid var(--color-border); }
|
||||
.toggle-copy strong { display: block; font: var(--text-body); color: var(--color-ink-900); }
|
||||
.toggle-copy span { font: 12px var(--font-body); color: var(--color-ink-500); }
|
||||
.switch { position: relative; width: 44px; height: 24px; flex-shrink: 0; }
|
||||
.switch input { opacity: 0; width: 0; height: 0; position: absolute; }
|
||||
.slider { position: absolute; inset: 0; background: var(--color-border-strong); border-radius: var(--radius-full); transition: background var(--transition-fast); cursor: pointer; }
|
||||
.slider::before { content:""; position:absolute; width:18px; height:18px; left:3px; top:3px; background:#fff; border-radius:50%; transition: transform var(--transition-fast); }
|
||||
.switch input:checked + .slider { background: var(--color-accent-active); }
|
||||
.switch input:checked + .slider::before { transform: translateX(20px); }
|
||||
.switch input:focus-visible + .slider { outline: 2px solid var(--color-focus-ring); outline-offset: 2px; }
|
||||
/* Disabled-переключатель (напр. анонс записи конференций в «Модулях»
|
||||
админки) — приглушение вместо hover/checked-цвета. */
|
||||
.switch input:disabled + .slider { opacity: .5; cursor: not-allowed; }
|
||||
|
||||
.password-block { margin-top: var(--space-4); }
|
||||
|
||||
/* ---------- Блок повторений (recurrence) ---------- */
|
||||
.recurrence-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-4);
|
||||
padding-top: var(--space-4);
|
||||
border-top: 1px dashed var(--color-border);
|
||||
}
|
||||
|
||||
.recurrence-option-row {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 10px 12px; border: 1px solid var(--color-border); border-radius: var(--radius-md);
|
||||
cursor: pointer; font: var(--text-body); font-weight: 600; color: var(--color-ink-900);
|
||||
transition: border-color var(--transition-fast), background var(--transition-fast);
|
||||
}
|
||||
.recurrence-option-row:has(input:checked) { border-color: var(--color-ink-700); background: var(--color-bg-alt); }
|
||||
.recurrence-option-row input[type="radio"] { width: 16px; height: 16px; accent-color: var(--color-ink-700); flex-shrink: 0; }
|
||||
|
||||
.recurrence-sub { padding: var(--space-3) 12px 4px 38px; }
|
||||
|
||||
.weekday-toggles { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.weekday-chip {
|
||||
width: 34px; height: 34px; border-radius: 50%;
|
||||
border: 1.5px solid var(--color-border); background: var(--color-surface);
|
||||
font: var(--text-caption); color: var(--color-ink-500);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.weekday-chip.is-selected { background: var(--color-ink-700); border-color: var(--color-ink-700); color: #fff; }
|
||||
|
||||
.recurrence-hint { font: 12px var(--font-body); color: var(--color-ink-500); margin-top: 8px; }
|
||||
|
||||
/* ---------- Кнопки действий карточки редактирования ---------- */
|
||||
.btn-row { display: flex; gap: var(--space-3); margin-top: var(--space-5); }
|
||||
.btn-row .btn { flex: 1; justify-content: center; }
|
||||
.danger-link-row { text-align: center; margin-top: var(--space-4); }
|
||||
.btn-text { background: none; border: none; color: var(--color-danger); font: var(--text-body); font-weight: 600; padding: 4px; cursor: pointer; }
|
||||
.btn-text:hover { text-decoration: underline; }
|
||||
.btn.full { width: 100%; justify-content: center; margin-top: var(--space-5); }
|
||||
|
||||
.btn-danger { background: var(--color-danger); color: #fff; font: var(--text-body); font-weight: 700; padding: 9px 18px; border-radius: var(--radius-full); border: none; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; }
|
||||
.btn-danger:hover:not(:disabled) { filter: brightness(0.92); }
|
||||
|
||||
/* ---------- Пикер участников (ParticipantsPicker) ----------
|
||||
* Новый элемент без готового макета — собран из утверждённых паттернов:
|
||||
* чипы как `.chip` фильтров «Моих конференций», выпадающий список результатов
|
||||
* переиспользует классы дроплиста селекта (`.vc-select-panel`/`.vc-select-option`,
|
||||
* §4.16), поле поиска — как `.search-wrap` админки.
|
||||
*/
|
||||
.participants-picker { position: relative; }
|
||||
.participants-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: var(--space-3); }
|
||||
.participant-chip {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 4px 10px 4px 4px; border-radius: var(--radius-full);
|
||||
border: 1px solid var(--color-border); background: var(--color-bg-alt);
|
||||
font: var(--text-body); font-weight: 600; color: var(--color-ink-900);
|
||||
}
|
||||
.participant-chip.is-organizer { padding-right: 12px; border-color: var(--color-border-strong); }
|
||||
.participant-chip-role { font: var(--text-caption); color: var(--color-ink-500); font-weight: 600; }
|
||||
.participant-chip-remove {
|
||||
width: 18px; height: 18px; flex-shrink: 0; border-radius: 50%; border: none;
|
||||
background: transparent; color: var(--color-ink-400);
|
||||
display: flex; align-items: center; justify-content: center; cursor: pointer;
|
||||
}
|
||||
.participant-chip-remove:hover:not(:disabled) { background: var(--color-border); color: var(--color-ink-900); }
|
||||
.participant-chip-remove:disabled { cursor: not-allowed; opacity: .5; }
|
||||
|
||||
.participants-search-wrap { position: relative; }
|
||||
.participants-search-wrap .icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--color-ink-400); }
|
||||
.participants-search-wrap input {
|
||||
width: 100%; font: var(--text-body-lg); color: var(--color-ink-900);
|
||||
padding: 10px 14px 10px 38px; border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border); background: var(--color-surface); font-family: var(--font-body);
|
||||
}
|
||||
.participants-search-wrap input:focus { outline: none; border-color: var(--color-focus-ring); box-shadow: 0 0 0 2px var(--color-focus-ring); }
|
||||
.participants-search-wrap input:disabled { background: var(--color-bg-alt); color: var(--color-ink-300); cursor: not-allowed; }
|
||||
|
||||
.participants-results { top: calc(100% + 6px); max-height: 240px; }
|
||||
.participant-option-body { display: flex; align-items: center; gap: 8px; font: var(--text-body); color: var(--color-ink-900); }
|
||||
.participants-results-hint { padding: 10px 14px; font: var(--text-body); color: var(--color-ink-500); }
|
||||
|
||||
/* ---------- Диалог быстрого просмотра вхождения (ConferenceOccurrenceDialog) ---------- */
|
||||
.detail-row { display: flex; align-items: flex-start; gap: 10px; padding: 10px 0; border-bottom: 1px solid var(--color-border); font: var(--text-body); color: var(--color-ink-900); }
|
||||
.detail-row:last-of-type { border-bottom: none; }
|
||||
.detail-row svg { color: var(--color-ink-400); margin-top: 2px; flex-shrink: 0; }
|
||||
.detail-row .label { color: var(--color-ink-500); min-width: 90px; flex-shrink: 0; }
|
||||
.title-lock { width: 15px; height: 15px; margin-right: 6px; flex-shrink: 0; }
|
||||
|
||||
/* =====================================================================
|
||||
* Тёмная тема: точечные патчи (design/mockups/dark/README.md).
|
||||
* ===================================================================== */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) .vc-calendar .fc { --fc-today-bg-color: rgba(127, 221, 168, 0.10); }
|
||||
:root:not([data-theme]) .vc-calendar .fc-day-today { background: rgba(127, 221, 168, 0.10) !important; }
|
||||
:root:not([data-theme]) .weekday-chip.is-selected { color: var(--color-accent-text); }
|
||||
:root:not([data-theme]) .btn-danger { background: var(--color-danger-solid); }
|
||||
:root:not([data-theme]) .btn-danger:hover:not(:disabled) { background: var(--color-danger-solid-hover); filter: none; }
|
||||
}
|
||||
:root[data-theme='dark'] .vc-calendar .fc { --fc-today-bg-color: rgba(127, 221, 168, 0.10); }
|
||||
:root[data-theme='dark'] .vc-calendar .fc-day-today { background: rgba(127, 221, 168, 0.10) !important; }
|
||||
:root[data-theme='dark'] .weekday-chip.is-selected { color: var(--color-accent-text); }
|
||||
:root[data-theme='dark'] .btn-danger { background: var(--color-danger-solid); }
|
||||
:root[data-theme='dark'] .btn-danger:hover:not(:disabled) { background: var(--color-danger-solid-hover); filter: none; }
|
||||
|
||||
/* ---------- Узкие экраны ---------- */
|
||||
/* ВАЖНО: этот блок должен идти строго ПОСЛЕДНИМ в файле — при равной
|
||||
специфичности с безусловными правилами выше (`.create-card`, `.field-row`
|
||||
и т.п. — те же 0,1,0/0,1,1) побеждает порядок в исходнике, а не сам факт
|
||||
совпадения media query (тот же баг уже ловили в lobby.css/.action, гейт 3
|
||||
— см. комментарий там). Макет calendar.html — desktop-only
|
||||
(`html { min-width: 1280px }`, DESIGN_SYSTEM.md §«Breakpoints»), адаптива
|
||||
в нём нет — раскладка ниже подобрана по здравому смыслу: календарь сверху,
|
||||
карточка создания/редактирования — под ним (сохраняем порядок вёрстки).
|
||||
*/
|
||||
@media (max-width: 960px) {
|
||||
/* Двухколоночный грид (сетка + сайдбар 400px) → одна колонка, сайдбар
|
||||
перестаёт быть sticky (в потоке одной колонки залипание при скролле
|
||||
страницы выглядит и работает хуже, чем простое следование за контентом). */
|
||||
.layout-grid { grid-template-columns: 1fr; }
|
||||
.create-card { position: static; top: auto; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.week-card, .create-card { padding: var(--space-4); }
|
||||
.week-toolbar { flex-direction: column; align-items: stretch; }
|
||||
.date-nav { justify-content: space-between; }
|
||||
.date-nav span { min-width: 0; flex: 1; }
|
||||
|
||||
/* Недельная сетка FullCalendar (5 рабочих дней) не помещается на ~360px
|
||||
содержимого карточки без потери читаемости — даём ей прокручиваться
|
||||
ГОРИЗОНТАЛЬНО ВНУТРИ карточки (min-width задаёт компактную, но ещё
|
||||
читаемую ширину колонки), а не раздувать всю страницу (см. min-width:0
|
||||
на .layout-grid выше — здесь наоборот сознательно возвращаем разумный
|
||||
min-width самому графику, но уже внутри своего overflow-контейнера). */
|
||||
.vc-calendar { overflow-x: auto; }
|
||||
.vc-calendar .fc { min-width: 560px; }
|
||||
|
||||
.field-row { flex-wrap: wrap; }
|
||||
.recurrence-sub { padding-left: var(--space-4); }
|
||||
}
|
||||
80
frontend/src/styles/join.css
Normal file
80
frontend/src/styles/join.css
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* VidConf — стили экрана подключения к конференции.
|
||||
* 1:1 воспроизводит design/mockups/join.html (3 состояния одного флоу).
|
||||
* Экран публичный (доступен гостю без аккаунта), поэтому не переиспользует
|
||||
* `.page-shell`/`ShellTopbar` — свой минимальный топбар с брендом.
|
||||
*/
|
||||
|
||||
.join-shell { min-height: 100vh; display: flex; flex-direction: column; background: var(--color-bg); }
|
||||
|
||||
.join-topbar { display: flex; align-items: center; justify-content: space-between; padding: var(--space-5) var(--space-8); }
|
||||
|
||||
.join-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-6) var(--space-8) var(--space-16);
|
||||
}
|
||||
|
||||
.screen-block { width: 100%; max-width: 440px; }
|
||||
|
||||
.flow-card { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-2xl); padding: var(--space-8); }
|
||||
.flow-icon {
|
||||
width: 52px; height: 52px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-bg-alt);
|
||||
color: var(--color-ink-700);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
margin: 0 auto var(--space-5);
|
||||
}
|
||||
.flow-icon .icon { width: 24px; height: 24px; }
|
||||
.flow-card h1 { font: var(--text-h1); color: var(--color-ink-900); margin: 0 0 var(--space-2); text-align: center; }
|
||||
.flow-card .flow-sub { font: var(--text-body-lg); color: var(--color-ink-500); margin: 0 0 var(--space-6); text-align: center; }
|
||||
|
||||
.field-optional { font-weight: 400; color: var(--color-ink-400); }
|
||||
.input-wrap { position: relative; }
|
||||
.input-wrap .icon {
|
||||
position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
|
||||
color: var(--color-ink-400);
|
||||
width: 20px; height: 20px;
|
||||
}
|
||||
.input-wrap input {
|
||||
width: 100%; font: var(--text-body-lg); color: var(--color-ink-900);
|
||||
padding: 11px 14px 11px 42px; border-radius: var(--radius-md); border: 1px solid var(--color-border);
|
||||
background: var(--color-surface); outline: none;
|
||||
}
|
||||
.input-wrap input::placeholder { color: var(--color-ink-300); }
|
||||
.input-wrap input:hover { border-color: var(--color-border-strong); }
|
||||
.input-wrap input:focus { border-color: var(--color-focus-ring); box-shadow: 0 0 0 3px rgba(59, 77, 149, 0.12); }
|
||||
.input-wrap .toggle-eye {
|
||||
left: auto; right: 14px; top: 50%; transform: translateY(-50%);
|
||||
background: none; border: none; padding: 0; cursor: pointer; color: var(--color-ink-400);
|
||||
}
|
||||
.field.has-error input { border-color: var(--color-danger); box-shadow: 0 0 0 3px rgba(199, 54, 79, 0.1); }
|
||||
.error-msg { display: flex; align-items: center; gap: 6px; margin-top: var(--space-2); font: var(--text-body); color: var(--color-danger); }
|
||||
|
||||
.link-back { display: block; text-align: center; margin-top: var(--space-5); font: var(--text-body); font-weight: 600; color: var(--color-ink-500); text-decoration: none; background: none; border: none; width: 100%; cursor: pointer; }
|
||||
.link-back:hover { color: var(--color-ink-700); }
|
||||
|
||||
/* Плашка целевой конференции (состояния «представиться»/«пароль») */
|
||||
.target-conf { display: flex; align-items: center; gap: 12px; background: var(--color-bg-alt); border-radius: var(--radius-lg); padding: var(--space-4); margin-bottom: var(--space-6); }
|
||||
.target-conf .icon-wrap {
|
||||
width: 40px; height: 40px; border-radius: 50%;
|
||||
background: var(--color-surface); border: 1px solid var(--color-border); color: var(--color-ink-700);
|
||||
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
}
|
||||
.target-conf .icon-wrap .icon { width: 18px; height: 18px; }
|
||||
.target-conf strong { display: block; font: var(--text-body); color: var(--color-ink-900); }
|
||||
.target-conf span { font: var(--text-mono-sm); color: var(--color-ink-500); }
|
||||
|
||||
/* =====================================================================
|
||||
* Тёмная тема: точечные патчи (design/mockups/dark/README.md).
|
||||
* ===================================================================== */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) .input-wrap input:focus { box-shadow: 0 0 0 3px rgba(214, 168, 61, 0.18); }
|
||||
:root:not([data-theme]) .field.has-error input { box-shadow: 0 0 0 3px rgba(235, 147, 161, 0.18); }
|
||||
}
|
||||
:root[data-theme='dark'] .input-wrap input:focus { box-shadow: 0 0 0 3px rgba(214, 168, 61, 0.18); }
|
||||
:root[data-theme='dark'] .field.has-error input { box-shadow: 0 0 0 3px rgba(235, 147, 161, 0.18); }
|
||||
210
frontend/src/styles/lobby.css
Normal file
210
frontend/src/styles/lobby.css
Normal file
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* VidConf — стили экрана лобби.
|
||||
*
|
||||
* `.page-shell`/`.page-main`/`.page-head` — общая разметка светлой оболочки,
|
||||
* переиспользуется и другими страницами (см. CalendarPage.tsx), поэтому эти
|
||||
* три класса общие и не привязаны к разметке лобби.
|
||||
*
|
||||
* Остальное — 1:1 воспроизводит хаб действий design/mockups/lobby.html
|
||||
* (мозаика из 4 действий + медальон логотипа в центре пересечения).
|
||||
*/
|
||||
|
||||
.page-shell { min-height: 100vh; background: var(--color-bg); }
|
||||
main.page-main { padding: var(--space-8); max-width: 1440px; margin: 0 auto; }
|
||||
|
||||
.page-head { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: var(--space-6); }
|
||||
.page-head h1 { font: var(--text-display-lg); font-family: var(--font-display); color: var(--color-ink-700); margin: 0 0 6px; }
|
||||
.page-head p { font: var(--text-body-lg); color: var(--color-ink-500); margin: 0; }
|
||||
|
||||
/* Общая адаптивная подстройка контейнера/шапки страниц оболочки (Calendar/
|
||||
MyConferences/Admin) — узкие экраны не должны получать горизонтальную
|
||||
прокрутку из-за фиксированных отступов space-8. Раскладка контента внутри
|
||||
каждой страницы (сетки/сайдбары) — зона соответствующих блоков. */
|
||||
@media (max-width: 640px) {
|
||||
main.page-main { padding: var(--space-5) var(--space-4); }
|
||||
.page-head { flex-direction: column; align-items: flex-start; gap: var(--space-3); }
|
||||
}
|
||||
|
||||
/* ---------- Лобби-хаб (мозаика действий) ---------- */
|
||||
.lobby-shell {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background:
|
||||
radial-gradient(1100px 520px at 10% -10%, rgba(59, 77, 149, 0.06), transparent 60%),
|
||||
radial-gradient(900px 480px at 105% 110%, rgba(212, 242, 227, 0.28), transparent 55%),
|
||||
var(--color-bg);
|
||||
}
|
||||
|
||||
/* Тёмная тема: декоративные радиальные пятна фона построены на индиго/мятном
|
||||
светлой темы (литеральные rgba, не var()) — патч в тон новой палитры
|
||||
(янтарь/мята той же светлоты, что и в комнате конференции). */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) .lobby-shell {
|
||||
background:
|
||||
radial-gradient(1100px 520px at 10% -10%, rgba(214, 168, 61, 0.05), transparent 60%),
|
||||
radial-gradient(900px 480px at 105% 110%, rgba(127, 221, 168, 0.14), transparent 55%),
|
||||
var(--color-bg);
|
||||
}
|
||||
}
|
||||
:root[data-theme='dark'] .lobby-shell {
|
||||
background:
|
||||
radial-gradient(1100px 520px at 10% -10%, rgba(214, 168, 61, 0.05), transparent 60%),
|
||||
radial-gradient(900px 480px at 105% 110%, rgba(127, 221, 168, 0.14), transparent 55%),
|
||||
var(--color-bg);
|
||||
}
|
||||
|
||||
.hub-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-6) var(--space-8) var(--space-12);
|
||||
}
|
||||
|
||||
.hub-wrap { display: flex; flex-direction: column; align-items: center; gap: var(--space-8); width: 100%; }
|
||||
.hub-intro { text-align: center; max-width: 560px; }
|
||||
.hub-intro .eyebrow { font: var(--text-caption); color: var(--color-ink-400); margin: 0 0 var(--space-2); }
|
||||
.hub-intro h1 { font: var(--text-display-lg); font-family: var(--font-display); color: var(--color-ink-700); margin: 0; }
|
||||
|
||||
.hub {
|
||||
position: relative;
|
||||
width: min(1420px, 100%);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 340px);
|
||||
gap: var(--space-8);
|
||||
}
|
||||
|
||||
/* Карточка-действие. Медальон с иконкой «сидит» на верхней кромке. */
|
||||
.action {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-10) var(--space-12) var(--space-8);
|
||||
margin-top: 34px;
|
||||
/* min-width:0 — иначе grid-элемент с 1fr-треком не сжимается уже своего
|
||||
min-content (длинные заглавные слова вроде «ПОДКЛЮЧИТЬСЯ» на Unbounded);
|
||||
без этого узкий вьюпорт даёт classic grid blowout — трек и вся сетка
|
||||
раздуваются шире контейнера, появляется горизонтальный скролл. */
|
||||
min-width: 0;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-3xl);
|
||||
text-decoration: none;
|
||||
color: var(--color-ink-900);
|
||||
box-shadow: var(--shadow-sm);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition:
|
||||
transform var(--transition-base),
|
||||
box-shadow var(--transition-base),
|
||||
border-color var(--transition-base);
|
||||
}
|
||||
.action:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); border-color: var(--color-border-strong); }
|
||||
.action:active { transform: translateY(-1px); }
|
||||
.action:disabled { cursor: not-allowed; opacity: 0.7; transform: none; }
|
||||
|
||||
.action-icon {
|
||||
position: absolute;
|
||||
top: -34px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 68px; height: 68px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-surface);
|
||||
border: var(--border-thick) solid var(--color-ink-700);
|
||||
color: var(--color-ink-700);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.action-icon .icon { width: 30px; height: 30px; }
|
||||
|
||||
.action-title {
|
||||
margin: 0;
|
||||
font: 600 40px/1.2 var(--font-display);
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--color-ink-700);
|
||||
text-transform: uppercase;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.action-object { margin: 0; font: 600 21px/1.35 var(--font-body); color: var(--color-ink-900); overflow-wrap: break-word; }
|
||||
.action-note { margin: 0; font: 400 17px/1.5 var(--font-body); color: var(--color-ink-500); overflow-wrap: break-word; }
|
||||
|
||||
/* Главное действие — CTA дизайн-системы (пастельная мята) */
|
||||
.action--create { background: var(--color-accent); border-color: transparent; }
|
||||
.action--create:hover:not(:disabled) { background: var(--color-accent-hover); border-color: transparent; }
|
||||
.action--create:active:not(:disabled) { background: var(--color-accent-active); }
|
||||
.action--create .action-title { font: 700 54px/1.1 var(--font-display); }
|
||||
.action--create .action-title, .action--create .action-object { color: var(--color-accent-text); }
|
||||
.action--create .action-note { color: var(--color-ink-700); }
|
||||
.action--create .action-icon { background: var(--color-accent); border-color: var(--color-accent-text); color: var(--color-accent-text); }
|
||||
|
||||
/* ---------- Медальон VidConf в центре мозаики ---------- */
|
||||
.hub-logo {
|
||||
position: absolute;
|
||||
top: 373px; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 156px; height: 156px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-surface);
|
||||
border: var(--border-thick) solid var(--color-ink-700);
|
||||
box-shadow: var(--shadow-lg);
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
gap: 6px;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
.hub-logo .logo-mark { width: 56px; height: 56px; color: var(--color-ink-700); }
|
||||
.hub-logo .logo-word { font: 600 16px/1 var(--font-display); color: var(--color-ink-700); }
|
||||
|
||||
/* ---------- Появление ---------- */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.action, .hub-logo { animation: vc-hub-rise 480ms cubic-bezier(0.22, 0.9, 0.3, 1) backwards; }
|
||||
.action--create { animation-delay: 60ms; }
|
||||
.action--calendar { animation-delay: 140ms; }
|
||||
.action--join { animation-delay: 220ms; }
|
||||
.action--mine { animation-delay: 300ms; }
|
||||
.hub-logo { animation-delay: 420ms; animation-name: vc-hub-rise-logo; }
|
||||
@keyframes vc-hub-rise {
|
||||
from { opacity: 0; transform: translateY(14px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes vc-hub-rise-logo {
|
||||
from { opacity: 0; transform: translate(-50%, calc(-50% + 14px)); }
|
||||
to { opacity: 1; transform: translate(-50%, -50%); }
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Узкие экраны ---------- */
|
||||
/* ВАЖНО: этот блок должен идти строго ПОСЛЕДНИМ в файле. При равной
|
||||
специфичности селекторов (`.action-title` и т.п. — 0,1,0 что здесь, что в
|
||||
безусловных правилах выше) порядок в исходнике решает, какое правило
|
||||
применится, а не сам факт совпадения media query — если этот блок стоит
|
||||
раньше своих безусловных дублей, он будет молча перебит ими на любой
|
||||
ширине. Так уже ловили баг (правки гейта, гейт 3): грид расползался на
|
||||
375px, потому что этот блок раньше стоял сразу после базового `.hub {}`,
|
||||
до `.action-title`/`.action--create .action-title` и т.п. */
|
||||
@media (max-width: 960px) {
|
||||
/* Мозаика 2×2 → одна колонка; фиксированная высота 340px больше не нужна —
|
||||
тайлы подстраиваются под контент. Медальон-логотип в центре сетки на
|
||||
узких экранах убираем совсем: он был позиционирован под чёткую 2×2
|
||||
геометрию (см. .hub-logo выше) и на одну колонку не переносится. */
|
||||
.hub-wrap { gap: var(--space-6); }
|
||||
.hub { grid-template-columns: 1fr; grid-template-rows: repeat(4, auto); gap: var(--space-5); }
|
||||
.hub-logo { display: none; }
|
||||
.hub-main { padding: var(--space-5) var(--space-4) var(--space-8); }
|
||||
|
||||
.action { padding: var(--space-8) var(--space-5) var(--space-6); margin-top: 28px; }
|
||||
.action-icon { width: 56px; height: 56px; top: -28px; }
|
||||
.action-icon .icon { width: 26px; height: 26px; }
|
||||
.action-title { font-size: 28px; }
|
||||
.action--create .action-title { font-size: 34px; }
|
||||
.action-object { font-size: 17px; }
|
||||
.action-note { font-size: 15px; }
|
||||
}
|
||||
85
frontend/src/styles/my-conferences.css
Normal file
85
frontend/src/styles/my-conferences.css
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* VidConf — стили страницы «Мои конференции».
|
||||
* 1:1 из design/mockups/my-conferences.html (.conf-grid/.conf-card/.pills-row
|
||||
* /.code-pill). page-shell/page-main/page-head — общие из lobby.css,
|
||||
* btn-primary/btn-secondary/modal-overlay — из shell.css.
|
||||
*/
|
||||
|
||||
/* flex-wrap — иначе три чипа фильтра на узких экранах не помещаются в одну
|
||||
строку и раздувают страницу горизонтально (ряд был жёстко nowrap). */
|
||||
.filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: var(--space-6); }
|
||||
.chip { display: inline-flex; align-items: center; gap: 6px; padding: 9px 16px; border-radius: var(--radius-full); border: 1.5px solid var(--color-border); background: var(--color-surface); font: var(--text-body); font-weight: 600; color: var(--color-ink-500); cursor: pointer; }
|
||||
.chip.is-active { border-color: var(--color-ink-700); color: var(--color-ink-700); background: var(--color-bg-alt); }
|
||||
|
||||
.conf-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-5); }
|
||||
/* min-width:0 — грид-элементы иначе не сжимаются уже своего min-content
|
||||
(grid blowout), см. тот же приём в calendar.css/.layout-grid. */
|
||||
.conf-grid > * { min-width: 0; }
|
||||
|
||||
.conf-card { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-2xl); padding: var(--space-6); display: flex; flex-direction: column; gap: var(--space-4); transition: box-shadow var(--transition-base), border-color var(--transition-base); }
|
||||
.conf-card:hover { box-shadow: var(--shadow-md); border-color: var(--color-border-strong); }
|
||||
.conf-card.is-ended { opacity: .85; }
|
||||
|
||||
.conf-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); }
|
||||
.conf-title-wrap h3 { margin: 0 0 4px; font: var(--text-h3); color: var(--color-ink-900); display: flex; align-items: center; gap: 6px; }
|
||||
.conf-title-wrap p { margin: 0; font: var(--text-body); color: var(--color-ink-500); }
|
||||
|
||||
.conf-badge { display: inline-flex; align-items: center; gap: 5px; padding: 4px 12px; border-radius: var(--radius-full); font: var(--text-caption); white-space: nowrap; }
|
||||
.conf-badge.pinned { background: var(--color-bg-alt); color: var(--color-ink-700); border: 1px solid var(--color-border-strong); }
|
||||
.conf-badge.scheduled { background: var(--color-warning-bg); color: var(--color-warning); }
|
||||
.conf-badge.live { background: var(--color-success-bg); color: var(--color-success); }
|
||||
.conf-badge.ended { background: var(--color-bg-alt); color: var(--color-ink-500); }
|
||||
|
||||
.conf-meta-row { display: flex; align-items: center; gap: var(--space-4); font: var(--text-body); color: var(--color-ink-500); }
|
||||
.conf-meta-row .meta-item { display: flex; align-items: center; gap: 6px; }
|
||||
|
||||
.pills-row { display: flex; gap: var(--space-3); }
|
||||
.pill-group { flex: 1; min-width: 0; }
|
||||
.pill-label { display: block; font: var(--text-caption); color: var(--color-ink-400); margin-bottom: 4px; }
|
||||
.code-pill { display: flex; align-items: center; gap: 6px; background: var(--color-bg-alt); border: 1px solid var(--color-border); border-radius: var(--radius-full); padding: 6px 6px 6px 14px; }
|
||||
.pill-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: var(--text-mono-sm); color: var(--color-ink-700); }
|
||||
.copy-btn { position: relative; width: 28px; height: 28px; flex-shrink: 0; border-radius: 50%; border: none; background: var(--color-surface); color: var(--color-ink-500); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: color var(--transition-fast), box-shadow var(--transition-fast); }
|
||||
.copy-btn:hover { color: var(--color-ink-700); box-shadow: var(--shadow-sm); }
|
||||
.copy-btn .icon-check { color: var(--color-success); }
|
||||
.copy-toast {
|
||||
position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
|
||||
background: var(--color-ink-900); color: #fff; font: var(--text-caption); text-transform: none; letter-spacing: normal;
|
||||
padding: 4px 10px; border-radius: var(--radius-sm); white-space: nowrap;
|
||||
}
|
||||
|
||||
.conf-card-footer { display: flex; gap: var(--space-3); margin-top: auto; }
|
||||
.conf-card-footer .btn { flex: 1; justify-content: center; }
|
||||
.icon-btn { width: 40px; height: 40px; flex-shrink: 0; border-radius: 50%; border: 1px solid var(--color-border); background: var(--color-surface); color: var(--color-ink-500); display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
||||
.icon-btn:hover { border-color: var(--color-border-strong); color: var(--color-ink-900); }
|
||||
|
||||
/* ---------- Модалка редактирования (переиспользует ConferenceFormCard) ---------- */
|
||||
/* .create-card сама по себе — визуальная панель (фон/обводка/радиус/паддинг из
|
||||
calendar.css); здесь только гасим её sticky-позиционирование календаря и
|
||||
ограничиваем высоту внутри модалки. */
|
||||
.modal-form-wrap { width: 100%; max-width: 480px; }
|
||||
.modal-form-wrap .create-card { position: static; max-height: calc(100vh - var(--space-12)); overflow-y: auto; }
|
||||
|
||||
/* Тёмная тема: тултип «Скопировано» — заливка была var(--color-ink-900)
|
||||
(тёмный индиго в светлой теме → стал бы светло-серым в тёмной, ломая
|
||||
тёмный тултип поверх тёмной страницы); заменена на буквальный #333333
|
||||
(= --color-room-surface-raised), текст остаётся var(--color-ink-900)
|
||||
(теперь светлый) — тултип снова тёмный чип со светлым текстом. */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) .copy-toast { background: #333333; color: var(--color-ink-900); }
|
||||
}
|
||||
:root[data-theme='dark'] .copy-toast { background: #333333; color: var(--color-ink-900); }
|
||||
|
||||
/* ---------- Узкие экраны ---------- */
|
||||
/* ВАЖНО: блок должен идти строго ПОСЛЕДНИМ в файле — при равной
|
||||
специфичности с `.conf-grid` выше порядок в исходнике решает, какое
|
||||
правило применится (тот же баг уже ловили в lobby.css/.action, гейт 3).
|
||||
Макет my-conferences.html — desktop-only, адаптива в нём нет; 2 колонки
|
||||
карточек на узких экранах схлопываются в 1. */
|
||||
@media (max-width: 860px) {
|
||||
.conf-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.conf-card { padding: var(--space-5); }
|
||||
.pills-row { flex-direction: column; }
|
||||
}
|
||||
25
frontend/src/styles/profile.css
Normal file
25
frontend/src/styles/profile.css
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* VidConf — страница профиля. Новый экран без готового
|
||||
* макета в design/mockups/ — собран из утверждённых паттернов оболочки:
|
||||
* карточка `--radius-2xl`/`--shadow-sm` как у `.conf-card`/`.settings-card`,
|
||||
* поля форм — `.field` (shell.css), аватар — общий `.avatar` (§1.5).
|
||||
* page-shell/page-main/page-head — общие из lobby.css.
|
||||
*/
|
||||
|
||||
.profile-card {
|
||||
max-width: 480px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-2xl);
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: var(--space-8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
/* Второй блок карточки («Смена пароля») — отступ между карточками профиля. */
|
||||
.profile-card + .profile-card { margin-top: var(--space-6); }
|
||||
.profile-card h2 { font: var(--text-h3); color: var(--color-ink-900); margin: 0; }
|
||||
|
||||
.profile-avatar-block { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-3); }
|
||||
.profile-avatar-actions { display: flex; align-items: center; gap: var(--space-3); }
|
||||
482
frontend/src/styles/room.css
Normal file
482
frontend/src/styles/room.css
Normal file
@@ -0,0 +1,482 @@
|
||||
/*
|
||||
* 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; }
|
||||
.stage { flex: 1; display: flex; flex-direction: column; padding: var(--space-5) var(--space-6); gap: var(--space-4); min-width: 0; }
|
||||
|
||||
/*
|
||||
* Демонстрация экрана: `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 при 1–2 участниках, см. выше). `!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); }
|
||||
}
|
||||
|
||||
/*
|
||||
* `.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 { flex: 1; min-height: 0; display: flex; }
|
||||
.room-single-tile .lk-participant-tile { flex: 1; min-height: 220px; width: 100%; }
|
||||
|
||||
/* Нижний тулбар: свои кнопки на хуках 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;
|
||||
}
|
||||
|
||||
/* ---------- Панель чата (см. 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; }
|
||||
|
||||
@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); }
|
||||
|
||||
/*
|
||||
* ---------- Чип «Вы демонстрируете экран» ----------
|
||||
* Рендерится внутри `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); }
|
||||
|
||||
/* ---------- Заглушка «конференция в мини-окне» (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); }
|
||||
|
||||
/* ---------- Аватар вместо иконки-заглушки при выключенной камере ---------- */
|
||||
.room-tile-avatar {
|
||||
width: 40%;
|
||||
height: 40%;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
max-width: 96px;
|
||||
max-height: 96px;
|
||||
font-size: clamp(12px, 3vw, 28px);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
126
frontend/src/styles/select.css
Normal file
126
frontend/src/styles/select.css
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* VidConf — кастомный селект (DESIGN_SYSTEM.md §4.16).
|
||||
* Триггер повторяет геометрию инпута (§4.4), дроплист — паттерн `.menu`
|
||||
* (design/mockups/lobby.html). Все правила на var()-токенах — тёмная тема
|
||||
* переключается сама через каскад tokens.css, отдельных патчей не требуется
|
||||
* (см. §4.16 «Обе темы»).
|
||||
*/
|
||||
|
||||
.vc-select {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.vc-select-trigger {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
font: var(--text-body-lg);
|
||||
color: var(--color-ink-900);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
font-family: var(--font-body);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
.vc-select-trigger:hover:not(:disabled) {
|
||||
border-color: var(--color-border-strong);
|
||||
}
|
||||
.vc-select-trigger:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--color-focus-ring);
|
||||
box-shadow: 0 0 0 2px var(--color-focus-ring);
|
||||
}
|
||||
.vc-select-trigger[aria-expanded='true'] {
|
||||
border-color: var(--color-focus-ring);
|
||||
box-shadow: 0 0 0 2px var(--color-focus-ring);
|
||||
}
|
||||
.vc-select-trigger:disabled {
|
||||
background: var(--color-bg-alt);
|
||||
color: var(--color-ink-300);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.vc-select-trigger.has-error {
|
||||
border-width: 2px;
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.vc-select-value,
|
||||
.vc-select-placeholder {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.vc-select-placeholder {
|
||||
color: var(--color-ink-300);
|
||||
}
|
||||
.vc-select-error-icon {
|
||||
color: var(--color-danger);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.vc-select-chevron {
|
||||
color: var(--color-ink-500);
|
||||
flex-shrink: 0;
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
.vc-select-chevron.is-open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.vc-select-trigger:disabled .vc-select-chevron {
|
||||
color: var(--color-ink-300);
|
||||
}
|
||||
|
||||
.vc-select-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: var(--space-2);
|
||||
list-style: none;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.vc-select-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
font: var(--text-body-lg);
|
||||
font-weight: 400;
|
||||
color: var(--color-ink-900);
|
||||
cursor: pointer;
|
||||
}
|
||||
.vc-select-option.is-active {
|
||||
background: var(--color-bg-alt);
|
||||
color: var(--color-ink-700);
|
||||
}
|
||||
.vc-select-option.is-selected {
|
||||
font-weight: 600;
|
||||
}
|
||||
.vc-select-option.is-selected svg {
|
||||
color: var(--color-ink-700);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.vc-select-option.is-disabled {
|
||||
color: var(--color-ink-300);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.vc-select-option.is-disabled.is-active {
|
||||
background: transparent;
|
||||
color: var(--color-ink-300);
|
||||
}
|
||||
455
frontend/src/styles/shell.css
Normal file
455
frontend/src/styles/shell.css
Normal file
@@ -0,0 +1,455 @@
|
||||
/*
|
||||
* VidConf — общие стили светлой оболочки (auth/lobby/calendar/admin).
|
||||
* 1:1 воспроизводит классы из design/mockups/{auth,lobby}.html, чтобы
|
||||
* JSX-разметка страниц могла использовать те же имена классов, что и
|
||||
* утверждённые макеты. Токены — из tokens.css.
|
||||
*/
|
||||
|
||||
/* ---------- Топбар (лобби/календарь/админка) ---------- */
|
||||
.topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-4) var(--space-8);
|
||||
background: var(--color-surface);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.brand-mark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font: var(--text-h3);
|
||||
color: var(--color-ink-700);
|
||||
text-decoration: none;
|
||||
}
|
||||
.brand-mark .logo-mark {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.topbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
/* ---------- Переключатель темы «солнце/луна» (см. design/mockups/dark/*.html) ---------- */
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
background: var(--color-bg-alt);
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-full);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
.theme-toggle-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-ink-500);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.theme-toggle-btn .icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.theme-toggle-btn:hover {
|
||||
color: var(--color-ink-900);
|
||||
}
|
||||
.theme-toggle-btn.is-active {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-ink-700);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* ---------- Пилюля пользователя (декоративная, см. lobby.html) ---------- */
|
||||
.user-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 16px 6px 6px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-full);
|
||||
font: var(--text-body);
|
||||
font-weight: 600;
|
||||
color: var(--color-ink-900);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
.user-pill:hover {
|
||||
border-color: var(--color-border-strong);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-ink-700);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font: var(--text-caption);
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Фото аватара (Avatar.tsx) — покрывает заглушку с инициалами целиком. */
|
||||
.avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.user-pill .avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
/* ---------- Бургер и скрывающаяся панель навигации (см. lobby.html) ---------- */
|
||||
.menu-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.burger {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-ink-700);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
.burger:hover {
|
||||
border-color: var(--color-border-strong);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.burger[aria-expanded='true'] {
|
||||
border-color: var(--color-ink-700);
|
||||
}
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 10px);
|
||||
right: 0;
|
||||
min-width: 236px;
|
||||
padding: var(--space-2);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
display: none;
|
||||
z-index: 30;
|
||||
}
|
||||
.menu.is-open {
|
||||
display: block;
|
||||
}
|
||||
.menu a,
|
||||
.menu button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
font: var(--text-body-lg);
|
||||
font-weight: 600;
|
||||
color: var(--color-ink-900);
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.menu a:hover,
|
||||
.menu button:hover {
|
||||
background: var(--color-bg-alt);
|
||||
color: var(--color-ink-700);
|
||||
}
|
||||
.menu a.is-active {
|
||||
background: var(--color-bg-alt);
|
||||
color: var(--color-ink-700);
|
||||
}
|
||||
.menu a .icon,
|
||||
.menu button .icon {
|
||||
color: var(--color-ink-400);
|
||||
}
|
||||
.menu-divider {
|
||||
height: 1px;
|
||||
background: var(--color-border);
|
||||
margin: var(--space-2) 6px;
|
||||
}
|
||||
.menu a.danger,
|
||||
.menu button.danger {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
.menu a.danger .icon,
|
||||
.menu button.danger .icon {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
/* ---------- Топбар на узких экранах ---------- */
|
||||
@media (max-width: 640px) {
|
||||
.topbar {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
.topbar-right {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.user-pill {
|
||||
padding: 6px;
|
||||
gap: 0;
|
||||
}
|
||||
.user-pill-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Кнопки (общие для оболочки) ---------- */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-body);
|
||||
white-space: nowrap;
|
||||
transition: background var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-accent-text);
|
||||
font: var(--text-body);
|
||||
font-weight: 700;
|
||||
padding: 11px 22px;
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--color-accent-hover);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.btn-primary:active:not(:disabled) {
|
||||
background: var(--color-accent-active);
|
||||
}
|
||||
.btn-primary:disabled {
|
||||
background: rgba(166, 174, 203, 0.35);
|
||||
color: var(--color-surface);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: var(--color-ink-700);
|
||||
border: 1.5px solid var(--color-border-pill);
|
||||
font: var(--text-body);
|
||||
font-weight: 600;
|
||||
padding: 9px 18px;
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--color-bg-alt);
|
||||
}
|
||||
.btn-secondary:disabled {
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-ink-300);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ---------- Бейджи статуса ---------- */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 12px;
|
||||
border-radius: var(--radius-full);
|
||||
font: var(--text-caption);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.badge-free { background: var(--color-success-bg); color: var(--color-success); }
|
||||
.badge-busy { background: var(--color-danger-bg); color: var(--color-danger); }
|
||||
.badge-booked { background: var(--color-warning-bg); color: var(--color-warning); }
|
||||
.badge-locked { background: var(--color-bg-alt); color: var(--color-ink-700); }
|
||||
|
||||
/* ---------- Индикатор загрузки (bootstrap-сессия, join и т.п.) ---------- */
|
||||
.app-loader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
color: var(--color-ink-500);
|
||||
font: var(--text-body-lg);
|
||||
background: var(--color-bg);
|
||||
}
|
||||
.spin {
|
||||
animation: vidconf-spin 0.9s linear infinite;
|
||||
}
|
||||
@keyframes vidconf-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ---------- Тосты ---------- */
|
||||
.toast-stack {
|
||||
position: fixed;
|
||||
right: var(--space-6);
|
||||
bottom: var(--space-6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
z-index: 100;
|
||||
}
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 18px;
|
||||
border-radius: var(--radius-md);
|
||||
font: var(--text-body);
|
||||
font-weight: 600;
|
||||
box-shadow: var(--shadow-lg);
|
||||
max-width: 360px;
|
||||
}
|
||||
.toast-error { background: var(--color-danger-bg); color: var(--color-danger); }
|
||||
.toast-success { background: var(--color-success-bg); color: var(--color-success); }
|
||||
.toast-info { background: var(--color-surface); color: var(--color-ink-900); border: 1px solid var(--color-border); }
|
||||
|
||||
/* ---------- Ховер-карточка конференции (ConferenceHoverCard) ----------
|
||||
* Новый элемент без готового макета в design/mockups/ — собран из уже
|
||||
* утверждённых паттернов (карточка-поповер `--shadow-lg`, бейдж закреплённости
|
||||
* с `Repeat` из §4.6). Рендерится порталом в document.body с `position: fixed`
|
||||
* (см. ConferenceHoverCard.tsx) — `pointer-events: none`, чтобы не перекрывать
|
||||
* клики по элементу под ней.
|
||||
*/
|
||||
.conf-hover-card {
|
||||
pointer-events: none;
|
||||
z-index: 250;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: var(--space-4);
|
||||
font: var(--text-body);
|
||||
color: var(--color-ink-900);
|
||||
}
|
||||
.conf-hover-card-hint { margin: 0; color: var(--color-ink-500); }
|
||||
.conf-hover-card-title { margin: 0 0 var(--space-2); font: var(--text-h3); color: var(--color-ink-900); }
|
||||
.conf-hover-card-row { display: flex; align-items: center; gap: 6px; margin: 0 0 6px; color: var(--color-ink-700); }
|
||||
.conf-hover-card-row:last-of-type { margin-bottom: 0; }
|
||||
.conf-hover-card-label { display: flex; align-items: center; gap: 5px; font: var(--text-caption); color: var(--color-ink-400); margin-right: 4px; }
|
||||
.conf-hover-card-participants { margin-top: var(--space-3); padding-top: var(--space-3); border-top: 1px solid var(--color-border); }
|
||||
.conf-hover-card-participants ul { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; max-height: 140px; overflow-y: auto; }
|
||||
.conf-hover-card-participants li { display: flex; align-items: center; gap: 8px; font: var(--text-body); color: var(--color-ink-900); }
|
||||
.conf-hover-card-tag { font: var(--text-caption); color: var(--color-ink-500); }
|
||||
|
||||
/* ---------- Модальные диалоги (светлая оболочка) ---------- */
|
||||
/* Общая обёртка модалок оболочки (диалог быстрого просмотра вхождения
|
||||
конференции, редактирование в «Моих конференциях» и т.п.) — центрированная
|
||||
модалка (--shadow-lg) поверх затемнённого фона. */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(46, 52, 84, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-6);
|
||||
z-index: 200;
|
||||
}
|
||||
.modal-panel {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-2xl);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: var(--space-6);
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
max-height: calc(100vh - var(--space-12));
|
||||
overflow-y: auto;
|
||||
}
|
||||
.modal-panel.is-wide { max-width: 480px; }
|
||||
.modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-5); }
|
||||
.modal-head h2 { font: var(--text-h2); color: var(--color-ink-900); margin: 0; }
|
||||
.modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-ink-500);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.modal-close:hover { border-color: var(--color-border-strong); color: var(--color-ink-900); }
|
||||
.modal-actions { display: flex; gap: var(--space-3); margin-top: var(--space-5); }
|
||||
.modal-actions .btn { flex: 1; justify-content: center; }
|
||||
|
||||
/* ---------- Поля форм в модалках (общие для всех диалогов оболочки) ---------- */
|
||||
.field { margin-bottom: var(--space-5); }
|
||||
.field:last-child { margin-bottom: 0; }
|
||||
.field label { display: flex; align-items: center; gap: 6px; font: var(--text-body); font-weight: 600; color: var(--color-ink-900); margin-bottom: var(--space-2); }
|
||||
.field input, .field textarea, .field select {
|
||||
width: 100%;
|
||||
font: var(--text-body-lg);
|
||||
color: var(--color-ink-900);
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
.field input:focus, .field textarea:focus, .field select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-focus-ring);
|
||||
box-shadow: 0 0 0 3px rgba(59, 77, 149, 0.12);
|
||||
}
|
||||
.field-hint { font: var(--text-body); color: var(--color-ink-500); margin: 4px 0 0; }
|
||||
|
||||
.error-banner {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
background: var(--color-danger-bg);
|
||||
border: 1px solid rgba(199, 54, 79, 0.3);
|
||||
color: var(--color-danger);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-4);
|
||||
font: var(--text-body);
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
.error-banner strong { display: block; margin-bottom: 2px; }
|
||||
.error-banner svg { flex-shrink: 0; margin-top: 1px; }
|
||||
|
||||
/* =====================================================================
|
||||
* Тёмная тема: точечные патчи (design/mockups/dark/README.md, «Точечные
|
||||
* правки поверх скопированного CSS»). Все переменные --color-* / --shadow-*
|
||||
* уже перекрашиваются сами через каскад tokens.css — здесь только правила,
|
||||
* где использован литеральный hex/rgba (не var()) и поэтому не могли
|
||||
* перекраситься автоматически. Оба блока (авто по ОС + явный выбор)
|
||||
* продублированы намеренно, как в самом tokens.css.
|
||||
* ===================================================================== */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) .avatar { color: var(--color-accent-text); }
|
||||
:root:not([data-theme]) .btn-primary:disabled { background: rgba(122, 122, 122, 0.3); color: var(--color-ink-300); }
|
||||
:root:not([data-theme]) .modal-overlay { background: rgba(0, 0, 0, 0.6); }
|
||||
:root:not([data-theme]) .field input:focus,
|
||||
:root:not([data-theme]) .field textarea:focus,
|
||||
:root:not([data-theme]) .field select:focus { box-shadow: 0 0 0 3px rgba(214, 168, 61, 0.18); }
|
||||
:root:not([data-theme]) .error-banner { border-color: rgba(235, 147, 161, 0.3); }
|
||||
}
|
||||
:root[data-theme='dark'] .avatar { color: var(--color-accent-text); }
|
||||
:root[data-theme='dark'] .btn-primary:disabled { background: rgba(122, 122, 122, 0.3); color: var(--color-ink-300); }
|
||||
:root[data-theme='dark'] .modal-overlay { background: rgba(0, 0, 0, 0.6); }
|
||||
:root[data-theme='dark'] .field input:focus,
|
||||
:root[data-theme='dark'] .field textarea:focus,
|
||||
:root[data-theme='dark'] .field select:focus { box-shadow: 0 0 0 3px rgba(214, 168, 61, 0.18); }
|
||||
:root[data-theme='dark'] .error-banner { border-color: rgba(235, 147, 161, 0.3); }
|
||||
315
frontend/src/styles/tokens.css
Normal file
315
frontend/src/styles/tokens.css
Normal file
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* VidConf — design tokens
|
||||
* ВНИМАНИЕ: это копия design/tokens.css (источник истины — design/DESIGN_SYSTEM.md).
|
||||
* Файл продублирован во frontend/, т.к. Vite не обслуживает статику вне корня
|
||||
* проекта. При изменении design/tokens.css копию нужно обновить вручную.
|
||||
* Значения синхронизированы 1:1.
|
||||
*
|
||||
* Три темы, независимые механизмы переключения:
|
||||
* - Оболочка, светлая (дефолт) — :root.
|
||||
* - Оболочка, тёмная — :root[data-theme="dark"] (переключатель
|
||||
* пользователя, см. useTheme/ThemeToggle) ИЛИ автоматически по
|
||||
* @media (prefers-color-scheme: dark), если data-theme не проставлен
|
||||
* явно (см. блок ниже; design/mockups/dark/README.md)
|
||||
* — палитра 1:1 из утверждённых тёмных
|
||||
* макетов оболочки.
|
||||
* - Комната конференции — [data-theme="room"] (скоуп на контейнер
|
||||
* экрана конференции; отдельный неймспейс --color-room-*, НЕ переключается
|
||||
* системной темой и НЕ смешивается с dark-оболочкой — комната всегда
|
||||
* тёмная независимо от выбора пользователя).
|
||||
*
|
||||
* Шрифты самохостятся через npm-пакеты @fontsource (без внешних CDN —
|
||||
* инвариант приватности), с кириллическим subset для русского интерфейса.
|
||||
* @font-face объявляют сами пакеты; их CSS импортируется в
|
||||
* frontend/src/main.tsx ДО ./index.css. Здесь — только имена семейств в
|
||||
* переменных ниже (--font-display/--font-body/--font-mono), которые ДОЛЖНЫ
|
||||
* совпадать с family из @fontsource: 'Unbounded' (@fontsource/unbounded),
|
||||
* 'Inter' (@fontsource/inter — статический, не variable, у которого было бы
|
||||
* 'Inter Variable'), 'JetBrains Mono' (@fontsource/jetbrains-mono).
|
||||
*/
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
/* ---------- Типографика ---------- */
|
||||
--font-display: 'Unbounded', 'Arial Rounded MT Bold', -apple-system, 'Segoe UI', sans-serif;
|
||||
--font-body: 'Inter', -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'SFMono-Regular', Menlo, Consolas, monospace;
|
||||
|
||||
--text-display-2xl: 700 64px/1.05 var(--font-display);
|
||||
--text-display-xl: 700 48px/1.1 var(--font-display);
|
||||
--text-display-lg: 600 34px/1.2 var(--font-display);
|
||||
--text-h1: 700 28px/1.25 var(--font-body);
|
||||
--text-h2: 600 22px/1.3 var(--font-body);
|
||||
--text-h3: 600 18px/1.35 var(--font-body);
|
||||
--text-body-lg: 400 16px/1.5 var(--font-body);
|
||||
--text-body: 400 14px/1.5 var(--font-body);
|
||||
--text-caption: 600 12px/1.4 var(--font-body);
|
||||
--text-mono-sm: 500 13px/1.4 var(--font-mono);
|
||||
|
||||
/* ---------- Spacing (8pt grid, база 4px) ---------- */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
--space-12: 48px;
|
||||
--space-16: 64px;
|
||||
--space-20: 80px;
|
||||
--space-24: 96px;
|
||||
|
||||
/* ---------- Радиусы ---------- */
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 20px;
|
||||
--radius-2xl: 24px;
|
||||
--radius-3xl: 32px;
|
||||
--radius-full: 999px;
|
||||
|
||||
/* ---------- Границы ---------- */
|
||||
--border-thin: 1px;
|
||||
--border-md: 1.5px;
|
||||
--border-thick: 2px;
|
||||
--border-speaker: 3px;
|
||||
|
||||
/* ---------- Breakpoints (справочно, desktop-first) ---------- */
|
||||
--bp-desktop: 1440px;
|
||||
--bp-min: 1280px;
|
||||
|
||||
/* ---------- Motion ---------- */
|
||||
--transition-fast: 120ms ease;
|
||||
--transition-base: 200ms ease;
|
||||
|
||||
/* ---------- Цвета: светлая тема (оболочка) ---------- */
|
||||
--color-bg: #F1F1F1;
|
||||
--color-bg-alt: #F7F7F8;
|
||||
--color-surface: #FFFFFF;
|
||||
--color-surface-glass: rgba(255, 255, 255, 0.55);
|
||||
|
||||
--color-border: #E1E3E9;
|
||||
--color-border-strong: #C7CCDC;
|
||||
--color-border-pill: #4A5A9E;
|
||||
|
||||
--color-ink-900: #2E3454;
|
||||
--color-ink-700: #3B4D95;
|
||||
--color-ink-500: #535F94;
|
||||
--color-ink-400: #6976AC;
|
||||
--color-ink-300: #A6AECB;
|
||||
|
||||
/* Пастельная мята — нейтральный акцент оболочки (акцент не обязан
|
||||
быть зелёным). Mapping см. design/DESIGN_SYSTEM.md §1.1/§1.3. */
|
||||
--color-accent: #D4F2E3;
|
||||
--color-accent-hover: #C7EBD9;
|
||||
--color-accent-active: #BAE4CE;
|
||||
--color-accent-text: #2E3454;
|
||||
|
||||
--color-success: #0A7A4C;
|
||||
--color-success-bg: #E4F7ED;
|
||||
--color-success-dot: #46916B;
|
||||
|
||||
--color-danger: #C7364F;
|
||||
--color-danger-bg: #FCE7EB;
|
||||
--color-danger-dot: #C65366;
|
||||
|
||||
--color-warning: #7F560D;
|
||||
--color-warning-bg: #FBF0DC;
|
||||
--color-warning-dot: #B58A26;
|
||||
|
||||
--color-focus-ring: #3B4D95;
|
||||
|
||||
/* ---------- Тени (светлая тема) ---------- */
|
||||
--shadow-sm: 0 1px 2px rgba(46, 52, 84, 0.06);
|
||||
--shadow-md: 0 4px 16px rgba(46, 52, 84, 0.08);
|
||||
--shadow-lg: 0 12px 32px rgba(46, 52, 84, 0.12);
|
||||
--shadow-glass: 0 8px 24px rgba(59, 77, 149, 0.15);
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
* Тёмная тема: оболочка (auth/лобби/календарь/join/«мои конференции»/админка)
|
||||
* Утверждено 2026-07-18. Палитра — 1:1 копия блока :root из
|
||||
* design/mockups/dark/*.html (все 6 файлов идентичны), происхождение каждого
|
||||
* значения задокументировано в design/mockups/dark/README.md и продублировано
|
||||
* в design/DESIGN_SYSTEM.md §1.2b. Принцип: тёмная оболочка не изобретает
|
||||
* новых цветов, а переиспользует уже утверждённые --color-room-* в новых
|
||||
* ролях (см. таблицу происхождения), поэтому тёмное лобби и тёмная комната
|
||||
* читаются как одна система.
|
||||
*
|
||||
* Механизм активации (два независимых триггера, одни и те же переопределения
|
||||
* --color-* / --shadow-* — те же имена, что в :root светлой темы выше):
|
||||
* 1) явный выбор пользователя — атрибут data-theme="dark" на <html>/:root;
|
||||
* 2) дефолт по системной теме — @media (prefers-color-scheme: dark),
|
||||
* только пока data-theme НЕ проставлен явно (:root:not([data-theme])).
|
||||
* Явный data-theme="light" отключает автодетект и форсирует светлую
|
||||
* тему (базовый :root выше) даже при тёмной ОС.
|
||||
* Оба блока продублированы намеренно (plain CSS, без препроцессора) — при
|
||||
* правке палитры менять оба синхронно.
|
||||
* ===================================================================== */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) {
|
||||
color-scheme: dark;
|
||||
|
||||
--color-bg: #1E1E1E;
|
||||
--color-bg-alt: #242424;
|
||||
--color-surface: #2A2A2A;
|
||||
--color-surface-glass: rgba(51, 51, 51, 0.55);
|
||||
|
||||
--color-border: #3F3F3F;
|
||||
--color-border-strong: #585858;
|
||||
--color-border-pill: #7FDDA8;
|
||||
|
||||
--color-ink-900: #E8E8E8;
|
||||
--color-ink-700: #7FDDA8;
|
||||
--color-ink-500: #B0B0B0;
|
||||
--color-ink-400: #5D9A78;
|
||||
--color-ink-300: #7A7A7A;
|
||||
|
||||
--color-accent: #7FDDA8;
|
||||
--color-accent-hover: #75CA9A;
|
||||
--color-accent-active: #6CB78C;
|
||||
--color-accent-text: #10331F;
|
||||
|
||||
--color-success: #7FDDA8;
|
||||
--color-success-bg: rgba(127, 221, 168, 0.14);
|
||||
--color-success-dot: #7FDDA8;
|
||||
|
||||
--color-danger: #EB93A1;
|
||||
--color-danger-bg: rgba(235, 147, 161, 0.14);
|
||||
--color-danger-dot: #EB93A1;
|
||||
--color-danger-solid: #B85468;
|
||||
--color-danger-solid-hover: #B9495F;
|
||||
|
||||
--color-warning: #D6A83D;
|
||||
--color-warning-bg: rgba(214, 168, 61, 0.16);
|
||||
--color-warning-dot: #D6A83D;
|
||||
|
||||
--color-focus-ring: #D6A83D;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5);
|
||||
--shadow-glass: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
color-scheme: dark;
|
||||
|
||||
--color-bg: #1E1E1E;
|
||||
--color-bg-alt: #242424;
|
||||
--color-surface: #2A2A2A;
|
||||
--color-surface-glass: rgba(51, 51, 51, 0.55);
|
||||
|
||||
--color-border: #3F3F3F;
|
||||
--color-border-strong: #585858;
|
||||
--color-border-pill: #7FDDA8;
|
||||
|
||||
--color-ink-900: #E8E8E8;
|
||||
--color-ink-700: #7FDDA8;
|
||||
--color-ink-500: #B0B0B0;
|
||||
--color-ink-400: #5D9A78;
|
||||
--color-ink-300: #7A7A7A;
|
||||
|
||||
--color-accent: #7FDDA8;
|
||||
--color-accent-hover: #75CA9A;
|
||||
--color-accent-active: #6CB78C;
|
||||
--color-accent-text: #10331F;
|
||||
|
||||
--color-success: #7FDDA8;
|
||||
--color-success-bg: rgba(127, 221, 168, 0.14);
|
||||
--color-success-dot: #7FDDA8;
|
||||
|
||||
--color-danger: #EB93A1;
|
||||
--color-danger-bg: rgba(235, 147, 161, 0.14);
|
||||
--color-danger-dot: #EB93A1;
|
||||
--color-danger-solid: #B85468;
|
||||
--color-danger-solid-hover: #B9495F;
|
||||
|
||||
--color-warning: #D6A83D;
|
||||
--color-warning-bg: rgba(214, 168, 61, 0.16);
|
||||
--color-warning-dot: #D6A83D;
|
||||
|
||||
--color-focus-ring: #D6A83D;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5);
|
||||
--shadow-glass: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
/* ---------- Тёмная тема: комната конференции ---------- */
|
||||
[data-theme='room'] {
|
||||
color-scheme: dark;
|
||||
|
||||
--color-room-bg: #1E1E1E;
|
||||
--color-room-surface: #2A2A2A;
|
||||
--color-room-surface-raised: #333333;
|
||||
|
||||
--color-room-tile: #363636;
|
||||
--color-room-tile-border: #3F3F3F;
|
||||
--color-room-tile-hover: #3D3D3D;
|
||||
|
||||
--color-room-text-primary: #E8E8E8;
|
||||
--color-room-text-secondary: #B0B0B0;
|
||||
--color-room-text-tertiary: #7A7A7A;
|
||||
|
||||
--color-room-mic-on: #7FDDA8;
|
||||
--color-room-mic-off: #EB93A1;
|
||||
--color-room-speaker-ring: #D6A83D;
|
||||
|
||||
--color-room-danger: #EB93A1;
|
||||
--color-room-danger-bg: #B85468;
|
||||
--color-room-danger-bg-hover: #B9495F;
|
||||
|
||||
--color-room-focus-ring: #D6A83D;
|
||||
|
||||
--shadow-speaker-glow: 0 0 0 3px var(--color-room-speaker-ring), 0 0 24px rgba(214, 168, 61, 0.35);
|
||||
--shadow-room-panel: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* ---------- Базовые сбросы, общие для макетов ---------- */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font: var(--text-body-lg);
|
||||
color: var(--color-ink-900);
|
||||
background: var(--color-bg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
[data-theme='room'] body,
|
||||
[data-theme='room'] {
|
||||
background: var(--color-room-bg);
|
||||
color: var(--color-room-text-primary);
|
||||
}
|
||||
|
||||
a { color: inherit; }
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--color-focus-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
[data-theme='room'] :focus-visible {
|
||||
outline-color: var(--color-room-focus-ring);
|
||||
}
|
||||
|
||||
svg.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
Reference in New Issue
Block a user