Files
vidconf/frontend/src/styles/admin.css
Max Ronzhin b6f57384b0 feat(ui): общий футер на всех страницах, кроме комнаты
`AppFooter` — версия инстанса из публичного `/api/health` (не требует
авторизации, поэтому работает и на экранах входа; поля `version` может не
быть — тогда версия не показывается) плюс копирайт с ТЕКУЩИМ годом и
ссылкой на профиль автора.

Подключён во все раскладки: `page-shell` (календарь, админка, профиль, мои
конференции), `AuthLayout` (вход, регистрация, подтверждение email),
`join-shell` и `lobby-shell`. В комнате конференции футера нет — там
полноэкранное видео.

Прежний футер админки (`.admin-footer`) удалён вместе со стилем — дубля не
осталось. В брендовой панели экранов auth строка «© 2026 VidConf ·
self-hosted» ужата до «self-hosted»: год там был захардкожен, а два
копирайта на одном экране — дубль.
2026-07-27 01:31:52 +03:00

184 lines
17 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* VidConf — стили страницы «Администрирование».
* Табличные/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); }
/* Мобильная ширина: 4 вкладки не помещаются в один ряд (см. аудит) —
переносим в 2 ряда по 2, а необязательный поясняющий текст под заголовком
прячем, чтобы не съедал экран (сам заголовок тянет .page-head h1 из
lobby.css). */
@media (max-width: 640px) {
.admin-page .page-head p { display: none; }
.admin-page .section-tabs { flex-wrap: wrap; }
/* Таблица «Пользователи» — на мобильном оставляем только колонку
«Пользователь»; роль/команда/блокировка редактируются через карточку
профиля (открывается кликом по имени, см. AdminUserProfileDialog). */
.admin-page .users-table th:not(:first-child),
.admin-page .users-table td:not(:first-child) {
display: none;
}
/* Таблица «Конференции» — оставляем «Конференция» и «Владелец», статус/
тип/начало/действия скрыты; клик по названию (.conf-cell-name) открывает
то же редактирование, что раньше было доступно только через карандаш в
скрытой колонке действий — там же теперь есть и удаление (см. кнопку
«Удалить» в EditConferenceModal). */
.admin-page .conferences-table th:not(:nth-child(-n+2)),
.admin-page .conferences-table td:not(:nth-child(-n+2)) {
display: none;
}
}
.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); }
/* Название конференции с №, оформленное кнопкой — открывает то же
редактирование, что и карандаш в row-actions правее (см.
AdminConferencesTab). На мобильном (.conferences-table ниже) это
единственный вход в редактирование строки, на десктопе — просто более
крупная область клика поверх уже существующей иконки. */
.admin-page .conf-cell-name { background: none; border: none; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; text-align: left; display: block; width: 100%; }
.admin-page .conf-cell-name:hover { 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; }
}
/* Футер админки (`.admin-footer`) убран в 0.0.6: вместо него общий для всех
страниц компонент `AppFooter` (см. `styles/footer.css`). */
/* =====================================================================
* Тёмная тема: заливка 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); }