редизайн кнопок 1st version
BIN
audit-buttons-01-login.png
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
audit-buttons-02-dashboard.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
audit-buttons-03-users-form.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
audit-buttons-04-settings-menu.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
audit-buttons-05-settings-general.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
audit-buttons-06-teacher.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
audit-buttons-07-student.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
audit-buttons-08-modal.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
audit-buttons-calendar.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
audit-buttons-classrooms.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
audit-buttons-groups.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
audit-buttons-rules.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
audit-buttons-schedule-view.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
audit-buttons-settings-database.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
audit-buttons-settings-study-forms.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
audit-buttons-settings-time-slots.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
audit-buttons-structure.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
audit-buttons-subjects.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
audit-buttons-users.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
audit-buttons-workload.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
420
docs/BUTTON_STYLING_PLAN.md
Normal file
@@ -0,0 +1,420 @@
|
||||
# План стилизации кнопок
|
||||
|
||||
Дата аудита: 01.06.2026
|
||||
Среда аудита: `https://localhost`
|
||||
Метод: визуальный обход через Playwright, фиксация скриншотов и computed styles.
|
||||
|
||||
## 1. Краткое резюме
|
||||
|
||||
Интерфейс уже имеет узнаваемую базу: тёмная админ-панель, фиолетовый акцент, компактные таблицы и отдельные публичные кабинеты преподавателя/студента. Главная проблема не в отдельных кнопках, а в отсутствии единого контракта между смыслом действия и его визуальным видом.
|
||||
|
||||
Сейчас одинаковые классы могут выглядеть по-разному в разных SPA, а одинаковые действия могут получать разные цвета, размеры и плотность. Из-за этого пользователь каждый раз заново считывает интерфейс: где главное действие, где безопасное второстепенное, а где рискованное.
|
||||
|
||||
Цель новой структуры: закрепить единую кнопку как системный компонент с понятными ролями, размерами и правилами использования.
|
||||
|
||||
## 2. Что было проверено
|
||||
|
||||
| Раздел | Проверенные сценарии | Скриншот |
|
||||
|---|---|---|
|
||||
| Авторизация | Вход, показ пароля, переключение темы | [`audit-buttons-01-login.png`](../audit-buttons-01-login.png) |
|
||||
| Дашборд админки | Быстрые переходы, recheck, сайдбар | [`audit-buttons-02-dashboard.png`](../audit-buttons-02-dashboard.png) |
|
||||
| Пользователи | Создание пользователя, архивирование | [`audit-buttons-03-users-form.png`](../audit-buttons-03-users-form.png) |
|
||||
| Меню настроек | Выпадающее меню, выход, переход в настройки | [`audit-buttons-04-settings-menu.png`](../audit-buttons-04-settings-menu.png) |
|
||||
| Настройки | Навигация отдельной SPA | [`audit-buttons-05-settings-general.png`](../audit-buttons-05-settings-general.png) |
|
||||
| Кабинет преподавателя | Недели, сегодня, выход, тема | [`audit-buttons-06-teacher.png`](../audit-buttons-06-teacher.png) |
|
||||
| Кабинет студента | Недели, сегодня, выход, тема | [`audit-buttons-07-student.png`](../audit-buttons-07-student.png) |
|
||||
| Модальное окно | Создание ручной сетки, отмена, закрытие | [`audit-buttons-08-modal.png`](../audit-buttons-08-modal.png) |
|
||||
|
||||
Дополнительно были сохранены скриншоты отдельных разделов: календарный график, аудитории, группы, дисциплины, расписание, формы обучения, база данных, временные слоты.
|
||||
|
||||
## 3. Текущее состояние
|
||||
|
||||
### 3.1. Авторизация
|
||||
|
||||

|
||||
|
||||
Найдены кнопки:
|
||||
|
||||
| Кнопка | Текущая роль | Комментарий |
|
||||
|---|---|---|
|
||||
| `Войти` | Primary | Корректно считывается как главное действие экрана. |
|
||||
| `Показать пароль` | Icon utility | Нужен единый icon-only стиль и обязательный `aria-label`. |
|
||||
| `Переключить тему` | Icon utility | Отличается от такой же кнопки в кабинетах преподавателя и студента. |
|
||||
|
||||
Вывод: экран логина близок к правильной иерархии, но icon-only controls нужно привести к общей форме и размеру.
|
||||
|
||||
### 3.2. Дашборд админки
|
||||
|
||||

|
||||
|
||||
Найдены кнопки и ссылки-кнопки:
|
||||
|
||||
| Кнопка | Текущий вид | Проблема |
|
||||
|---|---|---|
|
||||
| `Конструктор расписания` | Primary link | Хороший кандидат на основной CTA блока. |
|
||||
| `Точечные изменения / Просмотр` | Secondary link | Выглядит как второстепенное действие, это корректно. |
|
||||
| `Перепроверить` | `btn-primary`, маленькая высота | Primary используется для сервисного действия, хотя это не главное действие страницы. |
|
||||
| `Настройки` | Sidebar button | Визуально близко к навигации, но реализовано как кнопка. |
|
||||
| `Переключить тему` | Круглая icon button | Отличается от публичных кабинетов. |
|
||||
|
||||
Основной вывод: `btn-primary` сейчас означает и главный CTA, и компактную служебную кнопку. Это размывает иерархию.
|
||||
|
||||
### 3.3. Формы и таблицы админки
|
||||
|
||||

|
||||
|
||||
Типичные кнопки:
|
||||
|
||||
| Кнопка | Смысл | Текущее впечатление |
|
||||
|---|---|---|
|
||||
| `Создать` | Подтверждение формы | Главная кнопка, но размер и стиль отличаются между основной админкой и настройками. |
|
||||
| `Архивировать` | Рискованное, но часто обратимое действие | Визуально слишком близко к удалению. |
|
||||
| `Изменить` | Второстепенное действие строки | В разных разделах бывает фиолетовой или нейтральной. |
|
||||
| `Удалить` | Деструктивное действие | Встречается в разных размерах и оттенках красного. |
|
||||
|
||||
Проблема: таблицы используют кнопки как плотный action bar, но размеры скачут от `24x28` до `113x28` и `96x40`. Это создаёт визуальный шум в строках.
|
||||
|
||||
### 3.4. Меню настроек и выход
|
||||
|
||||

|
||||
|
||||
Кнопка `Выйти` находится рядом с навигационными пунктами и выглядит почти как обычный пункт меню. Семантически это сессионное действие с риском потерять текущий контекст, поэтому ему нужен отдельный quiet-danger стиль.
|
||||
|
||||
Рекомендация: `Выйти` не должен выглядеть как обычная навигация. Достаточно `Danger subtle`: красный текст на спокойной поверхности без агрессивной заливки.
|
||||
|
||||
### 3.5. Отдельная SPA настроек
|
||||
|
||||

|
||||
|
||||
В настройках виден тот же сайдбарный паттерн, но кнопки внутри разделов получают другую визуальную систему.
|
||||
|
||||
Пример различий:
|
||||
|
||||
| Класс | Основная админка | Настройки |
|
||||
|---|---|---|
|
||||
| `btn-primary` | Часто прозрачный фон, белый текст, визуальная заливка может приходить из другого слоя | Фиолетовая заливка `rgb(139, 92, 246)`, высота `40px`, жирность `700` |
|
||||
| `btn-delete` | Красный tint, высота часто `28px`, font-weight `400` | Красный tint, высота `40px`, font-weight `700` |
|
||||
| `btn-secondary` | Фиолетовый tint или светлая surface-кнопка | Нейтральная surface-кнопка |
|
||||
|
||||
Вывод: одинаковое имя класса не является дизайн-контрактом. Компонент кнопки нужно нормализовать на уровне токенов и модификаторов.
|
||||
|
||||
### 3.6. Кабинеты преподавателя и студента
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
В публичных кабинетах используется отдельная светлая система:
|
||||
|
||||
| Элемент | Преподаватель | Студент |
|
||||
|---|---|---|
|
||||
| `Сегодня` | Primary blue `rgb(29, 78, 216)` | Primary teal `rgb(15, 118, 110)` |
|
||||
| `Выйти` | Outline, белый фон, radius `8px` | Outline, белый фон, radius `8px` |
|
||||
| `‹` / `›` | Outline icon-like buttons | Outline icon-like buttons |
|
||||
| `Переключить тему` | Текстовый символ `◐` | Текстовый символ `◐` |
|
||||
|
||||
Проблема: одно действие `Сегодня` меняет цвет в зависимости от кабинета. Это допустимо для брендинга ролей, но плохо для системных кнопок. Ролевые цвета лучше использовать в расписании, бейджах и карточках, а не в базовой кнопочной иерархии.
|
||||
|
||||
### 3.7. Модальные окна
|
||||
|
||||

|
||||
|
||||
Найдены:
|
||||
|
||||
| Кнопка | Роль | Рекомендация |
|
||||
|---|---|---|
|
||||
| `Создать сетку` | Primary | Оставить primary. |
|
||||
| `Отмена` | Secondary/Ghost | Сделать ghost или secondary без акцента. |
|
||||
| `Закрыть` (`×`) | Icon close | Привести к icon-only button `40x40` или `36x36`, с `aria-label="Закрыть"`. |
|
||||
|
||||
Модальная структура в целом правильная, но её нужно подчинить тем же токенам, что и остальные кнопки.
|
||||
|
||||
## 4. Семантическая группировка найденных кнопок
|
||||
|
||||
### Primary: главное действие
|
||||
|
||||
Примеры: `Войти`, `Создать`, `Добавить`, `Сохранить правило`, `Сохранить год`, `Сохранить семестр`, `Сохранить график`, `Сохранить сетку`, `Сохранить слот`, `Создать сетку`.
|
||||
|
||||
Правило: одна primary-кнопка на форму, модальное окно или смысловой блок. Если на экране несколько форм, primary может быть в каждой форме, но не должна превращаться в оформление всех кнопок подряд.
|
||||
|
||||
### Secondary: обычное второстепенное действие
|
||||
|
||||
Примеры: `Изменить`, `Обновить`, `Показать`, `Тест`, `Сменить статус`, `Преподаватели`, `Загрузить сетку`, `Заполнить диапазон`.
|
||||
|
||||
Правило: использовать для действий, которые помогают выполнить задачу, но не являются главным подтверждением.
|
||||
|
||||
### Tertiary / Ghost: тихое действие
|
||||
|
||||
Примеры: `Очистить`, `Очистить форму`, `Отмена`, `Назад в панель`, `Свернуть`, `Из формы`, `Все`, `Точечные изменения / Просмотр`.
|
||||
|
||||
Правило: использовать там, где действие должно быть доступно, но не конкурировать с сохранением или созданием.
|
||||
|
||||
### Danger: деструктивное действие
|
||||
|
||||
Примеры: `Удалить`.
|
||||
|
||||
Правило: только для необратимых или почти необратимых операций. Красный цвет должен быть зарезервирован именно под риск.
|
||||
|
||||
### Danger subtle: рискованное, но не удаление
|
||||
|
||||
Примеры: `Архив`, `Архивировать`, `Удалить сетку`, `Выйти`.
|
||||
|
||||
Правило: использовать для действий с негативным или сессионным эффектом, но без визуальной паники solid-red кнопки.
|
||||
|
||||
### Icon button: компактное служебное действие
|
||||
|
||||
Примеры: `Переключить тему`, `Показать пароль`, `Скрыть панель`, `Закрыть`, `‹`, `›`.
|
||||
|
||||
Правило: фиксированный квадратный размер, единое состояние hover/focus, обязательный доступный текст через `aria-label`.
|
||||
|
||||
### Navigation item: навигация, а не кнопка действия
|
||||
|
||||
Примеры: пункты сайдбара `Дашборд`, `Конструктор правил`, `Календарный график`, `Пользователи`, `Общие настройки`.
|
||||
|
||||
Правило: не смешивать навигацию с action button. У неё отдельные active, hover и focus-состояния.
|
||||
|
||||
## 5. Предлагаемая дизайн-система кнопок
|
||||
|
||||
### 5.1. Токены
|
||||
|
||||
| Токен | Светлая тема | Тёмная тема | Назначение |
|
||||
|---|---|---|---|
|
||||
| `--button-primary-bg` | `#4F46E5` | `#8B5CF6` | Основное действие |
|
||||
| `--button-primary-bg-hover` | `#4338CA` | `#7C3AED` | Hover primary |
|
||||
| `--button-primary-text` | `#FFFFFF` | `#FFFFFF` | Текст primary |
|
||||
| `--button-secondary-bg` | `#FFFFFF` | `rgba(255,255,255,0.04)` | Второстепенная поверхность |
|
||||
| `--button-secondary-bg-hover` | `#F1F5F9` | `rgba(255,255,255,0.08)` | Hover secondary |
|
||||
| `--button-secondary-border` | `#CBD5E1` | `rgba(255,255,255,0.10)` | Граница secondary |
|
||||
| `--button-secondary-text` | `#334155` | `#F8FAFC` | Текст secondary |
|
||||
| `--button-ghost-bg-hover` | `#F8FAFC` | `rgba(255,255,255,0.06)` | Hover ghost |
|
||||
| `--button-danger-bg` | `#DC2626` | `#DC2626` | Удаление |
|
||||
| `--button-danger-bg-hover` | `#B91C1C` | `#B91C1C` | Hover удаления |
|
||||
| `--button-danger-subtle-bg` | `#FEF2F2` | `rgba(239,68,68,0.13)` | Риск без solid-заливки |
|
||||
| `--button-danger-subtle-text` | `#B91C1C` | `#FECACA` | Текст risk subtle |
|
||||
| `--button-focus-ring` | `rgba(79,70,229,0.35)` | `rgba(139,92,246,0.45)` | Focus outline |
|
||||
|
||||
Примечание: для всего продукта лучше выбрать один primary-акцент. Для текущей тёмной админки логично оставить фиолетовый `#8B5CF6`, а для светлой темы использовать более читаемый `#4F46E5`. Ролевые цвета преподавателя и студента лучше вынести из кнопок в расписание и бейджи.
|
||||
|
||||
### 5.2. Размеры
|
||||
|
||||
| Размер | Высота | Padding | Шрифт | Где применять |
|
||||
|---|---:|---|---|---|
|
||||
| `sm` | `32px` | `0 12px` | `13px / 600` | Таблицы, компактные действия строк |
|
||||
| `md` | `40px` | `0 16px` | `14px / 600` | Формы, фильтры, модальные окна |
|
||||
| `lg` | `48px` | `0 20px` | `15px / 700` | Вход, главный CTA экрана |
|
||||
| `icon-sm` | `32x32` | `0` | icon `18px` | Табличные иконки |
|
||||
| `icon-md` | `40x40` | `0` | icon `20px` | Верхние панели, модалки, навигация недель |
|
||||
|
||||
Базовый `border-radius`: `8px`.
|
||||
|
||||
Почему `8px`: интерфейс административный и рабочий. Радиус `8px` сохраняет мягкость, но выглядит собраннее, чем повсеместные `10px`, особенно в таблицах и плотных формах.
|
||||
|
||||
### 5.3. Варианты
|
||||
|
||||
#### Primary
|
||||
|
||||
Использовать для главного подтверждения.
|
||||
|
||||
| Состояние | Стиль |
|
||||
|---|---|
|
||||
| Default | Фон primary, белый текст, без видимой границы |
|
||||
| Hover | Фон темнее на 8-12% |
|
||||
| Focus | Видимый ring `2px` |
|
||||
| Disabled | Нейтральный фон, muted-текст, `cursor: not-allowed` |
|
||||
|
||||
Примеры: `Войти`, `Создать`, `Сохранить правило`, `Создать сетку`.
|
||||
|
||||
#### Secondary
|
||||
|
||||
Использовать для обычных действий рядом с primary.
|
||||
|
||||
| Состояние | Стиль |
|
||||
|---|---|
|
||||
| Default | Surface-фон, тонкая граница, основной текст |
|
||||
| Hover | Чуть более светлая/контрастная поверхность |
|
||||
| Focus | Такой же focus ring, как у primary |
|
||||
| Disabled | Opacity `0.5` |
|
||||
|
||||
Примеры: `Изменить`, `Обновить`, `Показать`, `Тест`.
|
||||
|
||||
#### Ghost
|
||||
|
||||
Использовать для тихих действий.
|
||||
|
||||
| Состояние | Стиль |
|
||||
|---|---|
|
||||
| Default | Прозрачный фон, muted/regular text |
|
||||
| Hover | Лёгкий surface-hover |
|
||||
| Focus | Ring без изменения иерархии |
|
||||
|
||||
Примеры: `Отмена`, `Очистить`, `Назад в панель`, `Свернуть`.
|
||||
|
||||
#### Danger
|
||||
|
||||
Использовать только для удаления.
|
||||
|
||||
| Состояние | Стиль |
|
||||
|---|---|
|
||||
| Default | Красная заливка, белый текст |
|
||||
| Hover | Более тёмный красный |
|
||||
| Focus | Красный или общий focus ring |
|
||||
|
||||
Примеры: `Удалить`.
|
||||
|
||||
#### Danger subtle
|
||||
|
||||
Использовать для рискованных действий без полного удаления.
|
||||
|
||||
| Состояние | Стиль |
|
||||
|---|---|
|
||||
| Default | Красный tint-фон, красный текст, мягкая border |
|
||||
| Hover | Более насыщенный tint |
|
||||
| Focus | Видимый ring |
|
||||
|
||||
Примеры: `Архивировать`, `Архив`, `Выйти`, `Удалить сетку`.
|
||||
|
||||
#### Icon button
|
||||
|
||||
Использовать для действий, где текст заменён иконкой.
|
||||
|
||||
| Параметр | Правило |
|
||||
|---|---|
|
||||
| Размер | `32x32` в таблицах, `40x40` в тулбарах |
|
||||
| Иконка | 18-20px, один набор иконок |
|
||||
| Текст | Только через `aria-label` |
|
||||
| Форма | Квадрат с radius `8px`, не круг, кроме специальных случаев avatar/fab |
|
||||
|
||||
Примеры: `Переключить тему`, `Показать пароль`, `Закрыть`, `Предыдущая неделя`, `Следующая неделя`.
|
||||
|
||||
## 6. Маппинг текущих кнопок на новую систему
|
||||
|
||||
| Текущие кнопки | Новый тип | Размер |
|
||||
|---|---|---|
|
||||
| `Войти` | `Primary` | `lg` |
|
||||
| `Создать`, `Добавить` | `Primary` | `md` |
|
||||
| `Сохранить правило`, `Сохранить год`, `Сохранить семестр`, `Сохранить график`, `Сохранить сетку`, `Сохранить слот` | `Primary` | `md` |
|
||||
| `Создать сетку` | `Primary` | `md` |
|
||||
| `Изменить`, `Обновить`, `Показать`, `Тест`, `Сменить статус` | `Secondary` | `sm` в таблицах, `md` в формах |
|
||||
| `Преподаватели`, `Подгруппы`, `Календарь`, `Сетка` | `Secondary` | `sm` |
|
||||
| `Очистить`, `Очистить форму`, `Отмена`, `Свернуть`, `Из формы`, `Все` | `Ghost` | `sm` или `md` |
|
||||
| `Точечные изменения / Просмотр`, `Назад в панель` | `Ghost navigation` | `md` |
|
||||
| `Удалить` | `Danger` | `sm` в таблицах, `md` в модалках |
|
||||
| `Архив`, `Архивировать`, `Выйти`, `Удалить сетку` | `Danger subtle` | `sm` или `md` |
|
||||
| `Переключить тему`, `Показать пароль`, `Скрыть панель`, `Закрыть`, `‹`, `›` | `Icon button` | `icon-sm` или `icon-md` |
|
||||
| Пункты сайдбара | `Nav item` | отдельный nav-компонент |
|
||||
|
||||
## 7. Правила композиции
|
||||
|
||||
### Формы
|
||||
|
||||
Primary-кнопка должна стоять в конце формы или action row. Рядом с ней допустимы secondary/ghost-действия, но не ещё одна primary без причины.
|
||||
|
||||
Рекомендуемый порядок:
|
||||
|
||||
| Сценарий | Порядок |
|
||||
|---|---|
|
||||
| Форма создания | `Очистить` как ghost, `Создать` как primary |
|
||||
| Форма редактирования | `Отмена` как ghost, `Сохранить` как primary |
|
||||
| Фильтр | `Сбросить` как ghost, `Показать` как secondary или primary, если это единственное действие блока |
|
||||
|
||||
### Таблицы
|
||||
|
||||
В строках таблицы использовать только `sm`.
|
||||
|
||||
Рекомендуемый порядок:
|
||||
|
||||
1. Основное безопасное действие: `Изменить`, `Преподаватели`, `Подгруппы`.
|
||||
2. Дополнительное действие: `Календарь`, `Сетка`.
|
||||
3. Рискованное действие: `Архивировать` или `Удалить`.
|
||||
|
||||
Удаление лучше отделять визуально от остальных кнопок небольшим gap.
|
||||
|
||||
### Модальные окна
|
||||
|
||||
В модалке primary должен соответствовать заголовку и цели модалки.
|
||||
|
||||
Пример:
|
||||
|
||||
| Заголовок | Primary | Secondary/Ghost |
|
||||
|---|---|---|
|
||||
| `Новая ручная сетка` | `Создать сетку` | `Отмена` |
|
||||
| `Удалить слот` | `Удалить` как danger | `Отмена` |
|
||||
| `Редактировать слот` | `Сохранить слот` | `Отмена` |
|
||||
|
||||
Кнопка закрытия всегда icon-only с `aria-label="Закрыть"`.
|
||||
|
||||
### Навигация
|
||||
|
||||
Сайдбарные пункты не должны наследовать стили action-кнопок. У них отдельная логика:
|
||||
|
||||
| Состояние | Стиль |
|
||||
|---|---|
|
||||
| Default | Прозрачный фон, muted-текст |
|
||||
| Hover | Нейтральный surface-hover |
|
||||
| Active | Акцентный tint-фон, accent-текст |
|
||||
| Focus | Видимый ring/outline |
|
||||
|
||||
## 8. Главные исправления по приоритету
|
||||
|
||||
| Приоритет | Что сделать | Почему важно |
|
||||
|---|---|---|
|
||||
| P1 | Нормализовать `btn-primary`, `btn-secondary`, `btn-delete` между основной админкой и настройками | Один класс должен означать один визуальный контракт |
|
||||
| P1 | Развести `Danger` и `Danger subtle` | `Удалить` и `Архивировать` не должны выглядеть одинаково опасными |
|
||||
| P1 | Ввести размерную шкалу `sm/md/lg/icon` | Таблицы и формы станут визуально ровнее |
|
||||
| P2 | Унифицировать `Переключить тему`, `Закрыть`, `‹`, `›`, `Показать пароль` как icon buttons | Повысит предсказуемость и доступность |
|
||||
| P2 | Убрать ролевые primary-цвета из student/teacher кнопок | Одинаковые действия будут считываться одинаково |
|
||||
| P3 | Заменить текстовые символы `◐`, `×`, `‹`, `›` на иконки единого набора | Интерфейс станет аккуратнее и доступнее |
|
||||
|
||||
## 9. Визуальный референс поведения
|
||||
|
||||
```text
|
||||
Primary
|
||||
[ Сохранить правило ]
|
||||
Фон: акцентный, текст белый. Только главное действие.
|
||||
|
||||
Secondary
|
||||
[ Изменить ]
|
||||
Фон: поверхность, тонкая граница. Обычное действие.
|
||||
|
||||
Ghost
|
||||
[ Отмена ]
|
||||
Фон: прозрачный. Тихое действие без конкуренции.
|
||||
|
||||
Danger
|
||||
[ Удалить ]
|
||||
Фон: красный. Только необратимое действие.
|
||||
|
||||
Danger subtle
|
||||
[ Архивировать ]
|
||||
Фон: красный tint. Рискованное, но не полноценное удаление.
|
||||
|
||||
Icon
|
||||
[ icon ]
|
||||
Фиксированный квадрат. Доступное название через aria-label.
|
||||
```
|
||||
|
||||
## 10. Почему это решение улучшит интерфейс
|
||||
|
||||
Единая кнопочная система снижает когнитивную нагрузку. Пользователь перестаёт разгадывать каждый экран и начинает быстро узнавать роли: главное действие, второстепенное действие, отмена, удаление, навигация.
|
||||
|
||||
Разделение `Danger` и `Danger subtle` особенно важно для админ-панели. Сейчас `Архивировать` визуально конкурирует с `Удалить`, хотя риск у этих действий разный. После унификации красный solid будет означать только настоящее удаление.
|
||||
|
||||
Единая размерная шкала сделает таблицы спокойнее. В строках не будет скачков высоты и плотности, а формы и модалки начнут выглядеть как части одной системы.
|
||||
|
||||
Общий primary-цвет вернёт продукту целостность. Ролевые цвета можно сохранить там, где они помогают считывать расписание и принадлежность данных, но базовые действия должны оставаться стабильными.
|
||||
|
||||
## 11. Приложение: дополнительные скриншоты
|
||||
|
||||
| Раздел | Скриншот |
|
||||
|---|---|
|
||||
| Конструктор правил |  |
|
||||
| Календарный график |  |
|
||||
| Просмотр расписаний |  |
|
||||
| Аудиторный фонд |  |
|
||||
| Группы обучения |  |
|
||||
| Структура вуза |  |
|
||||
| Дисциплины |  |
|
||||
| Пользователи |  |
|
||||
| Временные слоты |  |
|
||||
| Формы обучения |  |
|
||||
| База данных |  |
|
||||
@@ -317,16 +317,27 @@ CSS-переменные позволяют поддерживать светл
|
||||
:root {
|
||||
--bg-primary: #ffffff;
|
||||
--text-primary: #1a1a2e;
|
||||
--accent: #6366f1;
|
||||
--accent: #4F46E5;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg-primary: #0f0f23;
|
||||
--text-primary: #e2e8f0;
|
||||
--accent: #818cf8;
|
||||
--accent: #8B5CF6;
|
||||
}
|
||||
```
|
||||
|
||||
### Система кнопок
|
||||
|
||||
Кнопки админ-панели, отдельной SPA настроек, страницы входа и личных кабинетов используют единую семантическую модель:
|
||||
|
||||
- `btn` — обязательная база для всех кнопок и ссылок-кнопок.
|
||||
- Размеры: `btn-sm` — 32px для табличных строк, `btn-md` — 40px для форм и панелей, `btn-lg` — 48px для входа и главных широких действий.
|
||||
- Иконки: `btn-icon-sm` — 32x32, `btn-icon-md` — 40x40. Все icon-only кнопки должны иметь `aria-label` на русском языке.
|
||||
- Варианты: `btn-primary` для основного действия блока, `btn-secondary` для редактирования/обновления/показа, `btn-ghost` для очистки/отмены/сворачивания, `btn-danger` для удаления, `btn-danger-subtle` для архивирования, выхода и других рискованных действий без немедленного удаления.
|
||||
- Единый радиус кнопок — `8px`. Локальные переопределения `.btn-primary`, `.btn-secondary`, `.btn-delete` в feature CSS запрещены: новые экраны должны наследовать токены из `frontend/admin/css/components.css`.
|
||||
- Primary-акцент общий для продукта: `#4F46E5` в светлой теме и `#8B5CF6` в тёмной. Ролевые цвета кнопок в кабинетах преподавателя и студента не используются.
|
||||
|
||||
Переключение — через `theme-toggle.js`.
|
||||
|
||||
---
|
||||
|
||||
@@ -654,32 +654,6 @@
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.schedule-view-shell .btn-secondary {
|
||||
min-height: 42px;
|
||||
padding: 0.65rem 0.9rem;
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
font: inherit;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition), border-color var(--transition), color var(--transition), transform var(--transition);
|
||||
}
|
||||
|
||||
.schedule-view-shell .btn-secondary:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.schedule-view-shell .btn-secondary:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.schedule-view-extra-filters {
|
||||
margin-top: 0.9rem;
|
||||
border-top: 1px solid var(--bg-card-border);
|
||||
@@ -944,8 +918,8 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.schedule-view-period-buttons .btn-secondary,
|
||||
.schedule-view-result-actions .btn-secondary {
|
||||
.schedule-view-period-buttons .btn,
|
||||
.schedule-view-result-actions .btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,45 @@
|
||||
/* ===== Button Tokens ===== */
|
||||
:root {
|
||||
--button-primary-bg: #8B5CF6;
|
||||
--button-primary-bg-hover: #7C3AED;
|
||||
--button-primary-text: #FFFFFF;
|
||||
--button-secondary-bg: rgba(255, 255, 255, 0.04);
|
||||
--button-secondary-bg-hover: rgba(255, 255, 255, 0.08);
|
||||
--button-secondary-border: rgba(255, 255, 255, 0.10);
|
||||
--button-secondary-text: #F8FAFC;
|
||||
--button-ghost-bg-hover: rgba(255, 255, 255, 0.06);
|
||||
--button-danger-bg: #DC2626;
|
||||
--button-danger-bg-hover: #B91C1C;
|
||||
--button-danger-subtle-bg: rgba(239, 68, 68, 0.13);
|
||||
--button-danger-subtle-bg-hover: rgba(239, 68, 68, 0.20);
|
||||
--button-danger-subtle-border: rgba(239, 68, 68, 0.25);
|
||||
--button-danger-subtle-text: #FECACA;
|
||||
--button-focus-ring: rgba(139, 92, 246, 0.45);
|
||||
--button-disabled-bg: rgba(148, 163, 184, 0.16);
|
||||
--button-disabled-text: #94A3B8;
|
||||
--button-radius: 8px;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--button-primary-bg: #4F46E5;
|
||||
--button-primary-bg-hover: #4338CA;
|
||||
--button-primary-text: #FFFFFF;
|
||||
--button-secondary-bg: #FFFFFF;
|
||||
--button-secondary-bg-hover: #F1F5F9;
|
||||
--button-secondary-border: #CBD5E1;
|
||||
--button-secondary-text: #334155;
|
||||
--button-ghost-bg-hover: #F8FAFC;
|
||||
--button-danger-bg: #DC2626;
|
||||
--button-danger-bg-hover: #B91C1C;
|
||||
--button-danger-subtle-bg: #FEF2F2;
|
||||
--button-danger-subtle-bg-hover: #FEE2E2;
|
||||
--button-danger-subtle-border: #FECACA;
|
||||
--button-danger-subtle-text: #B91C1C;
|
||||
--button-focus-ring: rgba(79, 70, 229, 0.35);
|
||||
--button-disabled-bg: #CBD5E1;
|
||||
--button-disabled-text: #64748B;
|
||||
}
|
||||
|
||||
/* ===== Cards ===== */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
@@ -48,6 +90,189 @@
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* ===== Dashboard ===== */
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.dashboard-metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(180px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.dashboard-metric-card {
|
||||
--metric-color: var(--accent);
|
||||
--metric-bg: rgba(99, 102, 241, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
min-height: 112px;
|
||||
padding: 1.25rem;
|
||||
border-left: 4px solid var(--metric-color);
|
||||
}
|
||||
|
||||
.dashboard-metric-card--students {
|
||||
--metric-color: #10b981;
|
||||
--metric-bg: rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
.dashboard-metric-card--teachers {
|
||||
--metric-color: #f59e0b;
|
||||
--metric-bg: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
.dashboard-metric-card--classrooms {
|
||||
--metric-color: #3b82f6;
|
||||
--metric-bg: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.dashboard-metric-card .metric-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: var(--metric-color);
|
||||
background: var(--metric-bg);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.dashboard-middle {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(320px, 1fr);
|
||||
gap: 1.25rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.dashboard-monitor-card,
|
||||
.dashboard-compact-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dashboard-monitor-card {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.dashboard-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-card-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dashboard-time-slot {
|
||||
flex: 0 0 auto;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
color: var(--accent-hover);
|
||||
background: var(--bg-hover);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard-monitor-table {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dashboard-side-stack {
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 1.25rem;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.dashboard-compact-card {
|
||||
min-height: 0;
|
||||
padding: 1.25rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.dashboard-card-title {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dashboard-badge-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.dashboard-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.65rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.dashboard-action-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
min-height: 40px;
|
||||
padding: 0.6rem 0.85rem;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.dashboard-action-link.btn-secondary {
|
||||
color: var(--button-secondary-text);
|
||||
background: var(--button-secondary-bg);
|
||||
border: 1px solid var(--button-secondary-border);
|
||||
border-radius: var(--button-radius);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dashboard-action-link.btn-secondary:hover {
|
||||
color: var(--button-secondary-text);
|
||||
background: var(--button-secondary-bg-hover);
|
||||
border-color: var(--button-secondary-border);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.dashboard-metrics-grid {
|
||||
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.dashboard-middle {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-side-stack {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-rows: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.dashboard-metrics-grid,
|
||||
.dashboard-side-stack {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-card-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Form Structure ===== */
|
||||
.form-row {
|
||||
display: flex;
|
||||
@@ -167,14 +392,34 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.department-workspace-grid {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.department-subject-import-card,
|
||||
.department-teachers-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.department-teachers-card .card-header-row {
|
||||
margin-bottom: 0.85rem;
|
||||
}
|
||||
|
||||
.department-teachers-card .metric-grid {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.metric-card,
|
||||
.department-comment-item {
|
||||
padding: 0.85rem;
|
||||
padding: 0.8rem 0.85rem;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
@@ -183,7 +428,7 @@
|
||||
.metric-card strong,
|
||||
.department-comment-item strong {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
margin-bottom: 0.28rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -299,8 +544,9 @@
|
||||
|
||||
.subject-import-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 1fr) minmax(200px, 3fr) auto;
|
||||
gap: 0.75rem;
|
||||
grid-template-columns: 160px minmax(260px, 1fr) auto;
|
||||
column-gap: 0.65rem;
|
||||
row-gap: 0.75rem;
|
||||
align-items: end;
|
||||
padding: 0.75rem;
|
||||
background: var(--bg-input);
|
||||
@@ -382,16 +628,97 @@
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.schedule-slot-summary,
|
||||
.schedule-semester-line {
|
||||
.schedule-slot-summary {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.schedule-semester-line {
|
||||
display: flex;
|
||||
.academic-year-semesters {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
gap: 0.35rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.academic-years-table tbody td {
|
||||
padding-top: 0.6rem;
|
||||
padding-bottom: 0.6rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.academic-years-table thead th:first-child,
|
||||
.academic-years-table thead th:nth-child(2),
|
||||
.academic-years-table thead th:last-child {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.academic-year-center-cell {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.academic-year-semesters-cell {
|
||||
min-width: 520px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.academic-year-actions-cell {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.academic-year-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
min-height: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.academic-year-semester-item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.5rem 0.65rem;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.subject-import-code-field,
|
||||
.subject-import-name-field {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.subject-import-row .form-group label {
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.academic-year-semesters {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.academic-year-semesters-cell {
|
||||
min-width: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
.academic-year-semester-meta {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.academic-year-semester-type {
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.academic-year-semester-period {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.schedule-inline-table {
|
||||
@@ -403,34 +730,33 @@
|
||||
top: 50%;
|
||||
right: 0;
|
||||
z-index: 905;
|
||||
width: 42px;
|
||||
height: 74px;
|
||||
border: 1px solid var(--bg-card-border);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--button-secondary-border);
|
||||
border-right: 0;
|
||||
border-radius: 10px 0 0 10px;
|
||||
background: color-mix(in srgb, var(--bg-card) 92%, var(--accent));
|
||||
color: var(--text-primary);
|
||||
font: inherit;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
border-radius: var(--button-radius) 0 0 var(--button-radius);
|
||||
background: var(--button-secondary-bg);
|
||||
color: var(--button-secondary-text);
|
||||
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-50%);
|
||||
transition: right var(--transition), background var(--transition), border-color var(--transition), color var(--transition);
|
||||
transition: right var(--transition), background var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition);
|
||||
}
|
||||
|
||||
.schedule-visual-handle:hover,
|
||||
.schedule-visual-handle.open {
|
||||
border-color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 20%, var(--bg-card));
|
||||
color: var(--accent-hover);
|
||||
border-color: var(--button-secondary-border);
|
||||
background: var(--button-secondary-bg-hover);
|
||||
color: var(--button-secondary-text);
|
||||
}
|
||||
|
||||
.schedule-visual-handle.open {
|
||||
right: min(92vw, 1180px);
|
||||
}
|
||||
|
||||
.schedule-visual-handle.open .schedule-visual-handle-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.schedule-visual-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -1868,108 +2194,222 @@ input[type="number"] {
|
||||
}
|
||||
|
||||
/* ===== Buttons ===== */
|
||||
.btn-primary {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1.75rem;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: #fff;
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.45rem;
|
||||
min-width: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--button-radius);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: all var(--transition);
|
||||
box-shadow: 0 4px 15px var(--accent-glow);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition:
|
||||
background var(--transition),
|
||||
border-color var(--transition),
|
||||
color var(--transition),
|
||||
box-shadow var(--transition),
|
||||
transform var(--transition);
|
||||
}
|
||||
|
||||
.btn-primary::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(rgba(255, 255, 255, 0.2), transparent);
|
||||
border-radius: inherit;
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition);
|
||||
.btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px var(--accent-glow);
|
||||
.btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--button-focus-ring);
|
||||
}
|
||||
|
||||
.btn-primary:hover::before {
|
||||
opacity: 1;
|
||||
.btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 2px 10px var(--accent-glow);
|
||||
.btn:disabled,
|
||||
.btn[aria-disabled="true"] {
|
||||
background: var(--button-disabled-bg);
|
||||
border-color: transparent;
|
||||
color: var(--button-disabled-text);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.75;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
padding: 0.35rem 0.7rem;
|
||||
background: rgba(248, 113, 113, 0.1);
|
||||
border: 1px solid rgba(248, 113, 113, 0.2);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--error);
|
||||
.btn-sm {
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
min-height: 40px;
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
min-height: 48px;
|
||||
padding: 0 20px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-icon-sm,
|
||||
.btn-icon-md {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
padding: 0;
|
||||
border-radius: var(--button-radius);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.btn-icon-sm {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.btn-icon-md {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.btn-icon-sm svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.btn-icon-md svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--button-primary-bg);
|
||||
border-color: transparent;
|
||||
color: var(--button-primary-text);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--button-primary-bg-hover);
|
||||
color: var(--button-primary-text);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--button-secondary-bg);
|
||||
border-color: var(--button-secondary-border);
|
||||
color: var(--button-secondary-text);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--button-secondary-bg-hover);
|
||||
border-color: var(--button-secondary-border);
|
||||
color: var(--button-secondary-text);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.btn-ghost:hover:not(:disabled) {
|
||||
background: var(--button-ghost-bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--button-danger-bg);
|
||||
border-color: transparent;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: var(--button-danger-bg-hover);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.btn-danger-subtle {
|
||||
background: var(--button-danger-subtle-bg);
|
||||
border-color: var(--button-danger-subtle-border);
|
||||
color: var(--button-danger-subtle-text);
|
||||
}
|
||||
|
||||
.btn-danger-subtle:hover:not(:disabled) {
|
||||
background: var(--button-danger-subtle-bg-hover);
|
||||
border-color: var(--button-danger-subtle-border);
|
||||
color: var(--button-danger-subtle-text);
|
||||
}
|
||||
|
||||
.group-actions-cell {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.group-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 88px 104px 104px 116px;
|
||||
gap: 0.35rem;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.group-action-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-restore-group {
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: var(--button-radius);
|
||||
font-family: inherit;
|
||||
font-size: 0.8rem;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition), transform var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: rgba(248, 113, 113, 0.2);
|
||||
.btn-restore-group {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border: 1px solid rgba(245, 158, 11, 0.24);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.btn-restore-group:hover {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.btn-icon-toggle {
|
||||
background: transparent;
|
||||
border: 1px solid var(--bg-card-border);
|
||||
color: var(--text-secondary);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.btn-icon-toggle:hover {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
transform: rotate(45deg);
|
||||
box-shadow: 0 0 10px var(--accent-glow);
|
||||
}
|
||||
|
||||
.btn-edit-classroom {
|
||||
padding: 0.35rem 0.7rem;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--accent-hover);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.btn-edit-classroom:hover {
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.group-actions .group-action-btn {
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ===== Alerts ===== */
|
||||
.form-alert {
|
||||
display: none;
|
||||
@@ -2341,20 +2781,19 @@ tbody tr:hover {
|
||||
|
||||
/* ===== Theme Toggle Button ===== */
|
||||
.theme-toggle {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--button-radius);
|
||||
background: var(--button-secondary-bg);
|
||||
border: 1px solid var(--button-secondary-border);
|
||||
color: var(--button-secondary-text);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
transition: background 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease;
|
||||
padding: 0;
|
||||
transition: background var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition), transform var(--transition);
|
||||
z-index: 100;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -2366,12 +2805,18 @@ tbody tr:hover {
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 16px var(--accent-glow);
|
||||
background: var(--button-secondary-bg-hover);
|
||||
border-color: var(--button-secondary-border);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.theme-toggle:active {
|
||||
transform: scale(0.95);
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.theme-toggle:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--button-focus-ring);
|
||||
}
|
||||
|
||||
.theme-toggle--fixed {
|
||||
|
||||
@@ -25,15 +25,7 @@
|
||||
}
|
||||
|
||||
.sidebar-close-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.25rem;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
@@ -355,12 +347,7 @@
|
||||
/* ===== Mobile Menu Toggle ===== */
|
||||
.menu-toggle {
|
||||
display: none;
|
||||
padding: 0.4rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
|
||||
@@ -51,9 +51,9 @@
|
||||
--text-primary: #0f172a;
|
||||
--text-secondary: #475569;
|
||||
--text-placeholder: #94a3b8;
|
||||
--accent: #6366f1;
|
||||
--accent-hover: #4f46e5;
|
||||
--accent-glow: rgba(99, 102, 241, 0.3);
|
||||
--accent: #4F46E5;
|
||||
--accent-hover: #4338CA;
|
||||
--accent-glow: rgba(79, 70, 229, 0.3);
|
||||
--accent-secondary: #d946ef;
|
||||
|
||||
--error: #ef4444;
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
</svg>
|
||||
<span>Magistr</span>
|
||||
</div>
|
||||
<button class="sidebar-close-btn" id="sidebar-close-btn" aria-label="Скрыть панель">
|
||||
<button class="btn btn-icon-sm btn-ghost sidebar-close-btn" id="sidebar-close-btn" aria-label="Скрыть панель">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
@@ -193,7 +193,7 @@
|
||||
Общие настройки
|
||||
</a>
|
||||
<div class="settings-menu-divider"></div>
|
||||
<button class="settings-menu-item settings-menu-item--danger" id="btn-logout">
|
||||
<button class="btn btn-md btn-danger-subtle btn-logout settings-menu-item settings-menu-item--danger" id="btn-logout">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
@@ -213,7 +213,7 @@
|
||||
<!-- Main -->
|
||||
<main class="main">
|
||||
<header class="topbar">
|
||||
<button class="menu-toggle" id="menu-toggle" aria-label="Меню">
|
||||
<button class="btn btn-icon-md btn-ghost menu-toggle" id="menu-toggle" aria-label="Меню">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
|
||||
@@ -27,7 +27,7 @@ export function hideAlert(elementId) {
|
||||
|
||||
export function applyRippleEffect() {
|
||||
document.addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('.btn-primary, .btn-delete, .btn-logout');
|
||||
const btn = e.target.closest('.btn-primary, .btn-danger, .btn-danger-subtle, .btn-logout');
|
||||
if (!btn) return;
|
||||
|
||||
const rect = btn.getBoundingClientRect();
|
||||
|
||||
@@ -217,12 +217,14 @@ export async function initAcademicCalendar() {
|
||||
}
|
||||
academicYearsTbody.innerHTML = academicYears.map(year => `
|
||||
<tr>
|
||||
<td>${escapeHtml(year.title)}</td>
|
||||
<td>${escapeHtml(year.startDate)} - ${escapeHtml(year.endDate)}</td>
|
||||
<td>${renderSemesterList(year.semesters || [])}</td>
|
||||
<td>
|
||||
<button class="btn-edit-classroom btn-edit-year" data-id="${year.id}">Изменить</button>
|
||||
<button class="btn-delete btn-delete-year" data-id="${year.id}">Удалить</button>
|
||||
<td class="academic-year-center-cell academic-year-title-cell">${escapeHtml(year.title)}</td>
|
||||
<td class="academic-year-center-cell academic-year-period-cell">${escapeHtml(formatDisplayDateRange(year.startDate, year.endDate))}</td>
|
||||
<td class="academic-year-semesters-cell">${renderSemesterList(year.semesters || [])}</td>
|
||||
<td class="academic-year-actions-cell">
|
||||
<div class="academic-year-actions">
|
||||
<button class="btn btn-sm btn-secondary btn-edit-year" data-id="${year.id}">Изменить</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete-year" data-id="${year.id}">Удалить</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
@@ -230,12 +232,19 @@ export async function initAcademicCalendar() {
|
||||
|
||||
function renderSemesterList(items) {
|
||||
if (!items.length) return '<span class="muted">Нет семестров</span>';
|
||||
return items.map(semester => `
|
||||
<div class="schedule-semester-line">
|
||||
${escapeHtml(semesterLabel(semester))}
|
||||
<button class="btn-edit-classroom btn-edit-semester" data-id="${semester.id}">Изменить</button>
|
||||
return `<div class="academic-year-semesters">${items.map(semester => {
|
||||
const title = semesterTypeTitle(semester);
|
||||
const period = formatDisplayDateRange(semester.startDate, semester.endDate);
|
||||
return `
|
||||
<div class="academic-year-semester-item">
|
||||
<div class="academic-year-semester-meta">
|
||||
<span class="academic-year-semester-type">${escapeHtml(title)}</span>
|
||||
<span class="academic-year-semester-period">${escapeHtml(period)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
<button class="btn btn-sm btn-secondary btn-edit-semester" data-id="${semester.id}" aria-label="Изменить ${escapeHtml(title)} семестр ${escapeHtml(period)}">Изменить</button>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}</div>`;
|
||||
}
|
||||
|
||||
async function saveAcademicYear(event) {
|
||||
@@ -282,7 +291,7 @@ export async function initAcademicCalendar() {
|
||||
if (editSemesterButton) {
|
||||
const semester = semesters.find(item => item.id == editSemesterButton.dataset.id);
|
||||
if (!semester) return;
|
||||
semesterFormTitle.textContent = 'Редактирование семестра';
|
||||
semesterFormTitle.textContent = `Редактирование семестра: ${semesterTypeTitle(semester)}`;
|
||||
semesterIdInput.value = semester.id;
|
||||
semesterYearSelect.value = semester.academicYearId || '';
|
||||
semesterTypeSelect.value = normalizeSemesterType(semester.semesterType);
|
||||
@@ -391,9 +400,9 @@ export async function initAcademicCalendar() {
|
||||
<td>${escapeHtml(calendar.studyFormName || '-')}</td>
|
||||
<td>${escapeHtml(String(calendar.courseCount || '-'))}</td>
|
||||
<td>
|
||||
<button class="btn-edit-classroom btn-edit-calendar" data-id="${calendar.id}">Изменить</button>
|
||||
<button class="btn-edit-classroom btn-open-calendar-grid" data-id="${calendar.id}">Сетка</button>
|
||||
<button class="btn-delete btn-delete-calendar" data-id="${calendar.id}">Удалить</button>
|
||||
<button class="btn btn-sm btn-secondary btn-edit-calendar" data-id="${calendar.id}">Изменить</button>
|
||||
<button class="btn btn-sm btn-secondary btn-open-calendar-grid" data-id="${calendar.id}">Сетка</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete-calendar" data-id="${calendar.id}">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
@@ -1164,7 +1173,13 @@ export async function initAcademicCalendar() {
|
||||
|
||||
function semesterLabel(semester) {
|
||||
const type = normalizeSemesterType(semester.semesterType);
|
||||
return `${semester.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type} (${semester.startDate} - ${semester.endDate})`;
|
||||
return `${semester.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type} (${formatDisplayDateRange(semester.startDate, semester.endDate)})`;
|
||||
}
|
||||
|
||||
function semesterTypeTitle(semester) {
|
||||
const type = normalizeSemesterType(semester.semesterType);
|
||||
const label = SEMESTER_LABELS[type] || type || 'семестр';
|
||||
return label.charAt(0).toUpperCase() + label.slice(1);
|
||||
}
|
||||
|
||||
function normalizeSemesterType(type) {
|
||||
@@ -1195,6 +1210,16 @@ export async function initAcademicCalendar() {
|
||||
return `${day}.${month}.${year}`;
|
||||
}
|
||||
|
||||
function formatDisplayDate(value) {
|
||||
const [year, month, day] = String(value || '').split('-');
|
||||
if (!year || !month || !day) return String(value || '');
|
||||
return `${day}-${month}-${year}`;
|
||||
}
|
||||
|
||||
function formatDisplayDateRange(startDate, endDate) {
|
||||
return `${formatDisplayDate(startDate)} — ${formatDisplayDate(endDate)}`;
|
||||
}
|
||||
|
||||
function dateRangeCaption(rows) {
|
||||
if (!rows.length) return '';
|
||||
const dates = rows.map(row => row.date).sort();
|
||||
|
||||
@@ -87,16 +87,16 @@ export async function initClassrooms() {
|
||||
<span class="badge ${c.isAvailable ? 'badge-available' : 'badge-unavailable'}">
|
||||
${c.status === 'ARCHIVED' ? 'Архив' : (c.isAvailable ? 'Доступна' : 'Не доступна')}
|
||||
</span>
|
||||
<button class="btn-icon-toggle" data-id="${c.id}" data-current-status="${c.isAvailable}" title="Сменить статус">
|
||||
<button class="btn btn-icon-sm btn-secondary btn-icon-toggle" data-id="${c.id}" data-current-status="${c.isAvailable}" title="Сменить статус" aria-label="Сменить статус">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align: right;">
|
||||
<button class="btn-edit-classroom" data-id="${c.id}">Изменить</button>
|
||||
<button class="btn btn-sm btn-secondary classroom-edit-action" data-id="${c.id}">Изменить</button>
|
||||
${c.status === 'ARCHIVED'
|
||||
? `<button class="btn-restore-classroom" data-id="${c.id}" style="margin-left: 0.5rem;">Восстановить</button>`
|
||||
: `<button class="btn-delete" data-id="${c.id}" style="margin-left: 0.5rem;">Архивировать</button>`}
|
||||
: `<button class="btn btn-sm btn-danger-subtle btn-archive-classroom" data-id="${c.id}" style="margin-left: 0.5rem;">Архивировать</button>`}
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
@@ -125,9 +125,9 @@ export async function initClassrooms() {
|
||||
});
|
||||
|
||||
classroomsTbody.addEventListener('click', async (e) => {
|
||||
const btnDelete = e.target.closest('.btn-delete');
|
||||
const btnDelete = e.target.closest('.btn-archive-classroom');
|
||||
const btnToggleStatus = e.target.closest('.btn-icon-toggle');
|
||||
const btnEdit = e.target.closest('.btn-edit-classroom');
|
||||
const btnEdit = e.target.closest('.classroom-edit-action');
|
||||
|
||||
if (btnDelete) {
|
||||
if (!confirm('Вывести аудиторию из эксплуатации? Историческое расписание сохранится.')) return;
|
||||
@@ -261,7 +261,7 @@ export async function initClassrooms() {
|
||||
<tr>
|
||||
<td>${eq.id}</td>
|
||||
<td>${escapeHtml(eq.name)}</td>
|
||||
<td><button class="btn-delete btn-delete-equipment" data-id="${eq.id}">Удалить</button></td>
|
||||
<td><button class="btn btn-sm btn-danger btn-delete-equipment" data-id="${eq.id}">Удалить</button></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export async function initDepartmentWorkspace() {
|
||||
<td>${escapeHtml(subject.name || '-')}</td>
|
||||
<td>${escapeHtml(subject.description || '-')}</td>
|
||||
<td>
|
||||
<button type="button" class="btn-edit-classroom department-comments-open"
|
||||
<button type="button" class="btn btn-sm btn-secondary department-comments-open"
|
||||
data-subject-id="${escapeHtml(subject.id)}"
|
||||
data-subject-name="${escapeHtml(subject.name || '')}">
|
||||
Открыть
|
||||
@@ -120,7 +120,7 @@ export async function initDepartmentWorkspace() {
|
||||
<label>Название</label>
|
||||
<input type="text" class="subject-import-name" value="${escapeHtml(row.name || '')}" placeholder="Базы данных">
|
||||
</div>
|
||||
<button type="button" class="btn-delete subject-import-remove" data-index="${index}">Удалить</button>
|
||||
<button type="button" class="btn btn-sm btn-danger subject-import-remove" data-index="${index}">Удалить</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
@@ -171,13 +171,15 @@ export async function initGroups() {
|
||||
<td>${escapeHtml(specialityLabel(group.specialtyId || group.specialityCode))}</td>
|
||||
<td>${escapeHtml(group.specialtyProfileName || '-')}</td>
|
||||
<td><span class="badge ${statusBadgeClass(group)}">${escapeHtml(statusLabel(group))}</span></td>
|
||||
<td style="text-align: right; white-space: nowrap;">
|
||||
<button class="btn-edit-classroom btn-edit-group" data-id="${group.id}">Изменить</button>
|
||||
<button class="btn-primary btn-manage-subgroups" data-id="${group.id}" style="padding: 0.25rem 0.5rem; font-size: 0.85rem;">Подгруппы</button>
|
||||
<button class="btn-primary btn-manage-calendar" data-id="${group.id}" style="padding: 0.25rem 0.5rem; font-size: 0.85rem;">Календарь</button>
|
||||
<td class="group-actions-cell">
|
||||
<div class="group-actions">
|
||||
<button class="btn btn-sm btn-secondary btn-edit-group group-action-btn" data-id="${group.id}">Изменить</button>
|
||||
<button class="btn btn-sm btn-secondary btn-manage-subgroups group-action-btn" data-id="${group.id}">Подгруппы</button>
|
||||
<button class="btn btn-sm btn-secondary btn-manage-calendar group-action-btn" data-id="${group.id}">Календарь</button>
|
||||
${group.status === 'ARCHIVED'
|
||||
? `<button class="btn-restore-group" data-id="${group.id}" style="margin-left: 0.5rem;">Восстановить</button>`
|
||||
: `<button class="btn-delete" data-id="${group.id}" style="margin-left: 0.5rem;">Архив</button>`}
|
||||
? `<button class="btn-restore-group group-action-btn" data-id="${group.id}">Восстановить</button>`
|
||||
: `<button class="btn btn-sm btn-danger-subtle btn-archive-group group-action-btn" data-id="${group.id}">Архив</button>`}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
@@ -345,7 +347,7 @@ export async function initGroups() {
|
||||
}
|
||||
|
||||
async function handleGroupTableClick(event) {
|
||||
const btnDelete = event.target.closest('.btn-delete');
|
||||
const btnDelete = event.target.closest('.btn-archive-group');
|
||||
const btnEdit = event.target.closest('.btn-edit-group');
|
||||
const btnRestore = event.target.closest('.btn-restore-group');
|
||||
const btnSubgroups = event.target.closest('.btn-manage-subgroups');
|
||||
@@ -427,8 +429,8 @@ export async function initGroups() {
|
||||
<td>${escapeHtml(sub.name)}</td>
|
||||
<td>${escapeHtml(String(sub.studentCapacity ?? '-'))} чел.</td>
|
||||
<td>
|
||||
<button class="btn-edit-classroom btn-edit-subgroup" data-id="${sub.id}">Изменить</button>
|
||||
<button class="btn-delete btn-delete-subgroup" data-id="${sub.id}">Удалить</button>
|
||||
<button class="btn btn-sm btn-secondary btn-edit-subgroup" data-id="${sub.id}">Изменить</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete-subgroup" data-id="${sub.id}">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
@@ -539,7 +541,7 @@ export async function initGroups() {
|
||||
<td>${escapeHtml(assignment.academicYearTitle)}</td>
|
||||
<td>${escapeHtml(assignment.calendarTitle)}</td>
|
||||
<td>
|
||||
<button class="btn-delete btn-delete-assignment" data-id="${assignment.id}">Удалить</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete-assignment" data-id="${assignment.id}">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
@@ -247,8 +247,8 @@ export async function initSchedule() {
|
||||
<td>${renderRuleSlotsSummary(rule.slots)}</td>
|
||||
<td>
|
||||
<div class="schedule-rule-table-actions">
|
||||
<button type="button" class="schedule-rule-table-button schedule-rule-table-button-edit btn-edit-rule" data-id="${rule.id}">Изменить</button>
|
||||
<button type="button" class="schedule-rule-table-button schedule-rule-table-button-delete btn-delete-rule" data-id="${rule.id}">Удалить</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary btn-edit-rule" data-id="${rule.id}">Изменить</button>
|
||||
<button type="button" class="btn btn-sm btn-danger btn-delete-rule" data-id="${rule.id}">Удалить</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -857,7 +857,7 @@ export async function initSchedule() {
|
||||
<div class="form-group"><label>Тип</label><select class="slot-lesson-type" required>${options(orderedLessonTypes().map(toLessonTypeOption), slot.lessonTypeId)}</select></div>
|
||||
${isLaboratorySlot(slot) ? renderSubgroupField(slot, index) : ''}
|
||||
<div class="form-group"><label>Формат</label><select class="slot-format" required>${options([{ value: 'Очно', label: 'Очно' }, { value: 'Онлайн', label: 'Онлайн' }], slot.lessonFormat || 'Очно')}</select></div>
|
||||
<button type="button" class="btn-delete schedule-slot-remove" data-index="${index}">Удалить</button>
|
||||
<button type="button" class="btn btn-sm btn-danger schedule-slot-remove" data-index="${index}">Удалить</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
@@ -100,8 +100,8 @@ export async function initSubjects() {
|
||||
<td>${escapeHtml(departmentLabel(s.departmentId))}</td>
|
||||
<td><div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">${teachersHtml}</div></td>
|
||||
<td style="text-align: right; white-space: nowrap;">
|
||||
<button class="btn-primary btn-manage-teachers" data-id="${s.id}" style="padding: 0.25rem 0.5rem; font-size: 0.85rem;">Преподаватели</button>
|
||||
<button class="btn-delete btn-delete-subject" data-id="${s.id}">Удалить</button>
|
||||
<button class="btn btn-sm btn-secondary btn-manage-teachers" data-id="${s.id}">Преподаватели</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete-subject" data-id="${s.id}">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
@@ -175,7 +175,7 @@ export async function initSubjects() {
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(ts.fullName || ts.username)}</strong> <span class="text-muted">(${escapeHtml(ts.username)})</span></td>
|
||||
<td>
|
||||
<button class="btn-delete btn-delete-ts" data-user-id="${ts.userId}" data-subject-id="${ts.subjectId}">Удалить</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete-ts" data-user-id="${ts.userId}" data-subject-id="${ts.subjectId}">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
@@ -129,8 +129,8 @@ export async function initUniversityStructure() {
|
||||
<td>${escapeHtml(d.departmentName || d.name)}</td>
|
||||
<td>${escapeHtml(String(d.departmentCode || d.code))}</td>
|
||||
<td>
|
||||
<button class="btn-edit-classroom btn-edit-department" data-id="${d.id}">Изменить</button>
|
||||
<button class="btn-delete btn-delete-department" data-id="${d.id}">Удалить</button>
|
||||
<button class="btn btn-sm btn-secondary btn-edit-department" data-id="${d.id}">Изменить</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete-department" data-id="${d.id}">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
@@ -147,9 +147,9 @@ export async function initUniversityStructure() {
|
||||
<td>${escapeHtml(s.specialityName || s.name)}</td>
|
||||
<td>${escapeHtml(s.specialityCode || s.specialtyCode || s.specialty_code)}</td>
|
||||
<td>
|
||||
<button class="btn-edit-classroom btn-edit-specialty" data-id="${s.id}">Изменить</button>
|
||||
<button class="btn-primary btn-manage-profiles" data-id="${s.id}" style="padding: 0.25rem 0.5rem; font-size: 0.85rem;">Профили</button>
|
||||
<button class="btn-delete btn-delete-specialty" data-id="${s.id}">Удалить</button>
|
||||
<button class="btn btn-sm btn-secondary btn-edit-specialty" data-id="${s.id}">Изменить</button>
|
||||
<button class="btn btn-sm btn-secondary btn-manage-profiles" data-id="${s.id}">Профили</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete-specialty" data-id="${s.id}">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
@@ -338,8 +338,8 @@ export async function initUniversityStructure() {
|
||||
<td>${escapeHtml(p.name)}</td>
|
||||
<td>${escapeHtml(p.description || '-')}</td>
|
||||
<td>
|
||||
<button class="btn-edit-classroom btn-edit-profile" data-id="${p.id}">Изменить</button>
|
||||
<button class="btn-delete btn-delete-profile" data-id="${p.id}">Удалить</button>
|
||||
<button class="btn btn-sm btn-secondary btn-edit-profile" data-id="${p.id}">Изменить</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete-profile" data-id="${p.id}">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
@@ -453,8 +453,8 @@ export async function initUniversityStructure() {
|
||||
<td>${escapeHtml(p.name)}</td>
|
||||
<td>${escapeHtml(p.description || '-')}</td>
|
||||
<td>
|
||||
<button class="btn-edit-classroom btn-edit-tab-profile" data-id="${p.id}">Изменить</button>
|
||||
<button class="btn-delete btn-delete-tab-profile" data-id="${p.id}">Удалить</button>
|
||||
<button class="btn btn-sm btn-secondary btn-edit-tab-profile" data-id="${p.id}">Изменить</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete-tab-profile" data-id="${p.id}">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
@@ -50,7 +50,7 @@ export async function initUsers() {
|
||||
<td>
|
||||
${u.status === 'ARCHIVED'
|
||||
? `<button class="btn-restore-user" data-id="${u.id}">Восстановить</button>`
|
||||
: `<button class="btn-delete" data-id="${u.id}">Архивировать</button>`}
|
||||
: `<button class="btn btn-sm btn-danger-subtle btn-archive-user" data-id="${u.id}">Архивировать</button>`}
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
@@ -90,7 +90,7 @@ export async function initUsers() {
|
||||
});
|
||||
|
||||
usersTbody.addEventListener('click', async (e) => {
|
||||
const deleteBtn = e.target.closest('.btn-delete');
|
||||
const deleteBtn = e.target.closest('.btn-archive-user');
|
||||
if (!deleteBtn) return;
|
||||
|
||||
if (!confirm('Архивировать пользователя? История расписания и связей сохранится.')) return;
|
||||
|
||||
@@ -25,15 +25,7 @@
|
||||
}
|
||||
|
||||
.sidebar-close-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.25rem;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
@@ -265,12 +257,7 @@
|
||||
/* ===== Mobile Menu Toggle ===== */
|
||||
.menu-toggle {
|
||||
display: none;
|
||||
padding: 0.4rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
--text-primary: #0f172a;
|
||||
--text-secondary: #475569;
|
||||
--text-placeholder: #94a3b8;
|
||||
--accent: #6366f1;
|
||||
--accent-hover: #4f46e5;
|
||||
--accent-glow: rgba(99, 102, 241, 0.3);
|
||||
--accent: #4F46E5;
|
||||
--accent-hover: #4338CA;
|
||||
--accent-glow: rgba(79, 70, 229, 0.3);
|
||||
--accent-secondary: #d946ef;
|
||||
--error: #ef4444;
|
||||
--success: #10b981;
|
||||
@@ -77,20 +77,19 @@ body {
|
||||
|
||||
/* ===== Переключатель темы ===== */
|
||||
.theme-toggle {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--button-radius);
|
||||
background: var(--button-secondary-bg);
|
||||
border: 1px solid var(--button-secondary-border);
|
||||
color: var(--button-secondary-text);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
transition: background 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease;
|
||||
padding: 0;
|
||||
transition: background var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition), transform var(--transition);
|
||||
z-index: 100;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -102,12 +101,18 @@ body {
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 16px var(--accent-glow);
|
||||
background: var(--button-secondary-bg-hover);
|
||||
border-color: var(--button-secondary-border);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.theme-toggle:active {
|
||||
transform: scale(0.95);
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.theme-toggle:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--button-focus-ring);
|
||||
}
|
||||
|
||||
.theme-toggle--fixed {
|
||||
@@ -265,63 +270,6 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary,
|
||||
.btn-delete {
|
||||
min-height: 40px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0 0.9rem;
|
||||
color: var(--text-primary);
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: transform var(--transition), background var(--transition), border-color var(--transition);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-input);
|
||||
border-color: var(--bg-card-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: rgba(239, 68, 68, 0.13);
|
||||
border-color: rgba(239, 68, 68, 0.25);
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
[data-theme="light"] .btn-delete {
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-primary:disabled,
|
||||
.btn-secondary:disabled,
|
||||
.btn-delete:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.form-alert {
|
||||
grid-column: 1 / -1;
|
||||
min-height: 0;
|
||||
@@ -614,7 +562,7 @@ body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.settings-form .btn-primary {
|
||||
.settings-form .btn {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
@@ -630,9 +578,16 @@ body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.settings-header-row .btn-primary,
|
||||
.card-header-row .btn-secondary,
|
||||
.settings-form .btn-primary {
|
||||
.settings-header-row .btn,
|
||||
.card-header-row .btn,
|
||||
.settings-form .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.settings-modal-close.btn-icon-sm {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
min-height: 32px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: var(--button-radius) !important;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</svg>
|
||||
<span>Настройки</span>
|
||||
</div>
|
||||
<button class="sidebar-close-btn" id="sidebar-close-btn" aria-label="Скрыть панель">
|
||||
<button class="btn btn-icon-sm btn-ghost sidebar-close-btn" id="sidebar-close-btn" aria-label="Скрыть панель">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
@@ -94,7 +94,7 @@
|
||||
<!-- Основная область -->
|
||||
<main class="main">
|
||||
<header class="topbar">
|
||||
<button class="menu-toggle" id="menu-toggle" aria-label="Меню">
|
||||
<button class="btn btn-icon-md btn-ghost menu-toggle" id="menu-toggle" aria-label="Меню">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function initDatabase() {
|
||||
<td><code style="font-size: 0.82rem;">${escapeHtml(t.url)}</code></td>
|
||||
<td>${escapeHtml(t.username || '—')}</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td><button class="btn-delete" data-domain="${escapeHtml(t.domain)}">Удалить</button></td>
|
||||
<td><button class="btn btn-sm btn-danger btn-delete-tenant" data-domain="${escapeHtml(t.domain)}">Удалить</button></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
@@ -136,7 +136,7 @@ export async function initDatabase() {
|
||||
|
||||
// === Удаление тенанта ===
|
||||
tenantsTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
const btn = e.target.closest('.btn-delete-tenant');
|
||||
if (!btn) return;
|
||||
|
||||
const domain = btn.dataset.domain;
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function initEduForms() {
|
||||
<tr>
|
||||
<td>${ef.id}</td>
|
||||
<td>${escapeHtml(ef.name)}</td>
|
||||
<td><button class="btn-delete" data-id="${ef.id}">Удалить</button></td>
|
||||
<td><button class="btn btn-sm btn-danger btn-delete-edu-form" data-id="${ef.id}">Удалить</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export async function initEduForms() {
|
||||
});
|
||||
|
||||
efTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
const btn = e.target.closest('.btn-delete-edu-form');
|
||||
if (!btn) return;
|
||||
if (!confirm('Удалить форму обучения?')) return;
|
||||
try {
|
||||
|
||||
@@ -143,8 +143,8 @@ export async function initTimeSlotsSettings() {
|
||||
<td>${escapeHtml(trimTime(slot.endTime))}</td>
|
||||
<td>${escapeHtml(String(slot.durationMinutes ?? '-'))}</td>
|
||||
<td class="actions-cell">
|
||||
<button type="button" class="btn-secondary btn-edit-time-slot" data-id="${slot.id}">Изменить</button>
|
||||
<button type="button" class="btn-delete btn-delete-time-slot" data-id="${slot.id}">Удалить</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary btn-edit-time-slot" data-id="${slot.id}">Изменить</button>
|
||||
<button type="button" class="btn btn-sm btn-danger btn-delete-time-slot" data-id="${slot.id}">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
@@ -61,10 +61,10 @@
|
||||
<label for="tenant-password">Пароль</label>
|
||||
<input type="password" id="tenant-password" placeholder="••••••••" required>
|
||||
</div>
|
||||
<button type="button" class="btn-primary" id="btn-test-connection" style="height: fit-content;">
|
||||
<button type="button" class="btn btn-md btn-secondary" id="btn-test-connection" style="height: fit-content;">
|
||||
Тест
|
||||
</button>
|
||||
<button type="submit" class="btn-primary" style="height: fit-content;">
|
||||
<button type="submit" class="btn btn-md btn-primary" style="height: fit-content;">
|
||||
Добавить
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<label for="new-ef-name">Название</label>
|
||||
<input type="text" id="new-ef-name" placeholder="Бакалавриат" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Создать</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Создать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-ef-alert" role="alert"></div>
|
||||
</form>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<h2>Временные слоты</h2>
|
||||
<p>Настройка базовой, субботней и ручных сеток времени.</p>
|
||||
</div>
|
||||
<button type="button" class="btn-primary" id="time-slots-refresh">Обновить</button>
|
||||
<button type="button" class="btn btn-md btn-secondary" id="time-slots-refresh">Обновить</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-grid">
|
||||
@@ -12,8 +12,8 @@
|
||||
<div class="card-header-row">
|
||||
<h3>Сетка времени</h3>
|
||||
<div class="scope-header-actions">
|
||||
<button type="button" class="btn-primary" id="time-slot-scope-open-create">Новая ручная сетка</button>
|
||||
<button type="button" class="btn-secondary" id="time-slot-scope-delete">Удалить сетку</button>
|
||||
<button type="button" class="btn btn-md btn-primary" id="time-slot-scope-open-create">Новая ручная сетка</button>
|
||||
<button type="button" class="btn btn-md btn-danger-subtle" id="time-slot-scope-delete">Удалить сетку</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<article class="settings-card">
|
||||
<div class="card-header-row">
|
||||
<h3 id="time-slot-form-title">Новый слот</h3>
|
||||
<button type="button" class="btn-secondary" id="time-slot-reset">Очистить</button>
|
||||
<button type="button" class="btn btn-md btn-ghost" id="time-slot-reset">Очистить</button>
|
||||
</div>
|
||||
|
||||
<form id="time-slot-form" class="settings-form" novalidate>
|
||||
@@ -73,7 +73,7 @@
|
||||
<input type="number" id="time-slot-duration" min="1" step="1" placeholder="90">
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn-primary">Сохранить слот</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Сохранить слот</button>
|
||||
<div class="form-alert" id="time-slot-alert" role="alert"></div>
|
||||
</form>
|
||||
</article>
|
||||
@@ -106,7 +106,12 @@
|
||||
<h3 id="time-slot-scope-modal-title">Новая ручная сетка</h3>
|
||||
<p class="settings-modal-subtitle">Создайте отдельную сетку времени, которую затем можно назначать датам в календарном графике.</p>
|
||||
</div>
|
||||
<button type="button" class="settings-modal-close" id="time-slot-scope-modal-close" aria-label="Закрыть">×</button>
|
||||
<button type="button" class="btn btn-icon-sm btn-secondary settings-modal-close" id="time-slot-scope-modal-close" aria-label="Закрыть">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="time-slot-scope-form" class="settings-modal-form" novalidate>
|
||||
@@ -116,8 +121,8 @@
|
||||
</label>
|
||||
|
||||
<div class="settings-modal-actions">
|
||||
<button type="submit" class="btn-primary">Создать сетку</button>
|
||||
<button type="button" class="btn-secondary" id="time-slot-scope-modal-cancel">Отмена</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Создать сетку</button>
|
||||
<button type="button" class="btn btn-md btn-ghost" id="time-slot-scope-modal-cancel">Отмена</button>
|
||||
</div>
|
||||
|
||||
<div class="form-alert" id="time-slot-scope-alert" role="alert"></div>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<label for="academic-year-end">Окончание</label>
|
||||
<input type="date" id="academic-year-end" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Сохранить год</button>
|
||||
<button type="button" class="btn-edit-classroom" id="academic-year-reset">Очистить</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Сохранить год</button>
|
||||
<button type="button" class="btn btn-md btn-ghost" id="academic-year-reset">Очистить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="academic-year-alert" role="alert"></div>
|
||||
</form>
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="card">
|
||||
<div class="card-header-row">
|
||||
<h2>Учебные годы</h2>
|
||||
<button class="btn-primary" id="academic-years-refresh">Обновить</button>
|
||||
<button class="btn btn-md btn-secondary" id="academic-years-refresh">Обновить</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
@@ -72,8 +72,8 @@
|
||||
<label for="semester-end">Окончание</label>
|
||||
<input type="date" id="semester-end" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Сохранить семестр</button>
|
||||
<button type="button" class="btn-edit-classroom" id="semester-reset">Очистить</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Сохранить семестр</button>
|
||||
<button type="button" class="btn btn-md btn-ghost" id="semester-reset">Очистить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="semester-alert" role="alert"></div>
|
||||
</form>
|
||||
@@ -82,7 +82,7 @@
|
||||
<div class="card create-card">
|
||||
<div class="card-header-row">
|
||||
<h2 id="academic-calendar-form-title">Календарный учебный график</h2>
|
||||
<button type="button" class="btn-edit-classroom" id="academic-calendar-reset">Очистить</button>
|
||||
<button type="button" class="btn btn-md btn-ghost" id="academic-calendar-reset">Очистить</button>
|
||||
</div>
|
||||
<form id="academic-calendar-form" novalidate>
|
||||
<input type="hidden" id="academic-calendar-id">
|
||||
@@ -119,7 +119,7 @@
|
||||
<label for="academic-calendar-course-count">Курсов</label>
|
||||
<input type="number" id="academic-calendar-course-count" min="1" max="8" value="4" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Сохранить график</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Сохранить график</button>
|
||||
</div>
|
||||
<div class="form-alert" id="academic-calendar-alert" role="alert"></div>
|
||||
</form>
|
||||
@@ -128,7 +128,7 @@
|
||||
<div class="card">
|
||||
<div class="card-header-row">
|
||||
<h2>Календарные графики</h2>
|
||||
<button class="btn-primary" id="academic-calendars-refresh">Обновить</button>
|
||||
<button class="btn btn-md btn-secondary" id="academic-calendars-refresh">Обновить</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
@@ -161,8 +161,8 @@
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn-primary" id="calendar-editor-load">Загрузить сетку</button>
|
||||
<button type="button" class="btn-primary" id="calendar-editor-save">Сохранить сетку</button>
|
||||
<button type="button" class="btn btn-md btn-secondary" id="calendar-editor-load">Загрузить сетку</button>
|
||||
<button type="button" class="btn btn-md btn-primary" id="calendar-editor-save">Сохранить сетку</button>
|
||||
</div>
|
||||
<div class="calendar-fill-panel">
|
||||
<div class="form-group">
|
||||
@@ -185,7 +185,7 @@
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn-edit-classroom" id="calendar-fill-apply">Заполнить диапазон</button>
|
||||
<button type="button" class="btn btn-md btn-secondary" id="calendar-fill-apply">Заполнить диапазон</button>
|
||||
</div>
|
||||
<div class="form-alert" id="calendar-editor-alert" role="alert"></div>
|
||||
<div id="calendar-totals" class="calendar-totals"></div>
|
||||
@@ -229,8 +229,8 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="calendar-day-dialog-actions">
|
||||
<button type="button" class="btn-primary" id="calendar-day-dialog-time-save">Применить</button>
|
||||
<button type="button" class="btn-edit-classroom" id="calendar-day-dialog-time-clear">Убрать ручную сетку</button>
|
||||
<button type="button" class="btn btn-md btn-primary" id="calendar-day-dialog-time-save">Применить</button>
|
||||
<button type="button" class="btn btn-md btn-danger-subtle" id="calendar-day-dialog-time-clear">Убрать ручную сетку</button>
|
||||
</div>
|
||||
<div class="form-alert" id="calendar-day-dialog-time-alert" role="alert"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="card">
|
||||
<div class="card-header-row" style="margin-bottom: 1.5rem;">
|
||||
<h2>Загруженность</h2>
|
||||
<button type="button" class="btn-primary" id="workload-refresh">Обновить</button>
|
||||
<button type="button" class="btn btn-md btn-secondary" id="workload-refresh">Обновить</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-row workload-filter-row" style="margin-bottom: 2rem; align-items: flex-end; gap: 1.5rem;">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="card create-card">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<h2 style="margin: 0;">Новая аудитория</h2>
|
||||
<button type="button" class="btn-primary" id="btn-manage-equipments">Справочник оборудования</button>
|
||||
<button type="button" class="btn btn-md btn-secondary" id="btn-manage-equipments">Справочник оборудования</button>
|
||||
</div>
|
||||
<form id="create-classroom-form">
|
||||
<div class="form-row">
|
||||
@@ -36,7 +36,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; align-items: flex-end;">
|
||||
<button type="submit" class="btn-primary">Добавить</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Добавить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-alert" id="create-classroom-alert" role="alert"></div>
|
||||
@@ -103,7 +103,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; align-items: flex-end;">
|
||||
<button type="submit" class="btn-primary" style="width: 100%;">Сохранить</button>
|
||||
<button type="submit" class="btn btn-md btn-primary" style="width: 100%;">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-alert" id="edit-classroom-alert" role="alert"></div>
|
||||
@@ -128,7 +128,7 @@
|
||||
<label for="manage-equipment-name">Название оборудования</label>
|
||||
<input type="text" id="manage-equipment-name" placeholder="Проектор" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary" style="height: fit-content; margin-bottom: 0;">Добавить</button>
|
||||
<button type="submit" class="btn btn-md btn-primary" style="height: fit-content; margin-bottom: 0;">Добавить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="manage-equipment-alert" role="alert"></div>
|
||||
</form>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!-- ===== Dashboard Page ===== -->
|
||||
<div class="dashboard-container" style="display: flex; flex-direction: column; gap: 1.5rem;">
|
||||
<div class="dashboard-container">
|
||||
<!-- Метрики (Информационные карточки) -->
|
||||
<div class="metrics-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.25rem;">
|
||||
<div class="metric-card card" style="display: flex; align-items: center; gap: 1rem; padding: 1.25rem; border-left: 4px solid var(--primary);">
|
||||
<div class="metric-icon" style="background: rgba(99, 102, 241, 0.1); color: var(--primary); padding: 0.75rem; border-radius: 12px; display: flex; align-items: center; justify-content: center;">
|
||||
<div class="dashboard-metrics-grid">
|
||||
<div class="card dashboard-metric-card dashboard-metric-card--groups">
|
||||
<div class="metric-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
@@ -17,8 +17,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card card" style="display: flex; align-items: center; gap: 1rem; padding: 1.25rem; border-left: 4px solid #10b981;">
|
||||
<div class="metric-icon" style="background: rgba(16, 185, 129, 0.1); color: #10b981; padding: 0.75rem; border-radius: 12px; display: flex; align-items: center; justify-content: center;">
|
||||
<div class="card dashboard-metric-card dashboard-metric-card--students">
|
||||
<div class="metric-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
@@ -32,8 +32,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card card" style="display: flex; align-items: center; gap: 1rem; padding: 1.25rem; border-left: 4px solid #f59e0b;">
|
||||
<div class="metric-icon" style="background: rgba(245, 158, 11, 0.1); color: #f59e0b; padding: 0.75rem; border-radius: 12px; display: flex; align-items: center; justify-content: center;">
|
||||
<div class="card dashboard-metric-card dashboard-metric-card--teachers">
|
||||
<div class="metric-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
@@ -45,8 +45,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card card" style="display: flex; align-items: center; gap: 1rem; padding: 1.25rem; border-left: 4px solid #3b82f6;">
|
||||
<div class="metric-icon" style="background: rgba(59, 130, 246, 0.1); color: #3b82f6; padding: 0.75rem; border-radius: 12px; display: flex; align-items: center; justify-content: center;">
|
||||
<div class="card dashboard-metric-card dashboard-metric-card--classrooms">
|
||||
<div class="metric-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M9 3v18M15 3v18M3 9h18M3 15h18" />
|
||||
@@ -60,14 +60,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Мониторинг в реальном времени и Быстрый переход -->
|
||||
<div class="dashboard-middle" style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.25rem; align-items: start;">
|
||||
<div class="dashboard-middle">
|
||||
<!-- Идет сейчас -->
|
||||
<div class="card" style="min-height: 250px; display: flex; flex-direction: column;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<div class="card dashboard-monitor-card">
|
||||
<div class="dashboard-card-header">
|
||||
<h2 style="margin: 0;">Мониторинг пар (Идет сейчас)</h2>
|
||||
<div id="current-time-slot" style="background: var(--bg-secondary); padding: 0.35rem 0.75rem; border-radius: 20px; font-size: 0.85rem; font-weight: 600; color: var(--primary);">—</div>
|
||||
<div id="current-time-slot" class="dashboard-time-slot">—</div>
|
||||
</div>
|
||||
<div class="table-wrap" style="flex: 1;">
|
||||
<div class="table-wrap dashboard-monitor-table">
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -87,20 +87,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Свободные аудитории и Быстрые действия -->
|
||||
<div style="display: flex; flex-direction: column; gap: 1.25rem;">
|
||||
<div class="dashboard-side-stack">
|
||||
<!-- Свободные аудитории -->
|
||||
<div class="card" style="padding: 1.25rem; display: flex; flex-direction: column; gap: 0.75rem;">
|
||||
<h3 style="margin: 0; font-size: 1.1rem; font-weight: 600;">Свободные аудитории (ближайшие)</h3>
|
||||
<div id="free-classrooms-list" style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem;">
|
||||
<div class="card dashboard-compact-card">
|
||||
<h3 class="dashboard-card-title">Свободные аудитории (ближайшие)</h3>
|
||||
<div id="free-classrooms-list" class="dashboard-badge-list">
|
||||
<div style="color: var(--text-secondary); font-size: 0.9rem;">Загрузка...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Быстрые действия -->
|
||||
<div class="card" style="padding: 1.25rem; display: flex; flex-direction: column; gap: 0.75rem;">
|
||||
<h3 style="margin: 0; font-size: 1.1rem; font-weight: 600;">Быстрый переход</h3>
|
||||
<div style="display: grid; grid-template-columns: 1fr; gap: 0.5rem; margin-top: 0.5rem;">
|
||||
<a href="#schedule" class="btn-primary" style="display: flex; align-items: center; justify-content: center; gap: 0.5rem; text-decoration: none; text-align: center; font-size: 0.9rem; padding: 0.6rem;">
|
||||
<div class="card dashboard-compact-card">
|
||||
<h3 class="dashboard-card-title">Быстрый переход</h3>
|
||||
<div class="dashboard-actions">
|
||||
<a href="#schedule" class="btn btn-md btn-primary dashboard-action-link">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||||
@@ -109,7 +109,7 @@
|
||||
</svg>
|
||||
Конструктор расписания
|
||||
</a>
|
||||
<a href="#schedule-view" class="btn-secondary" style="display: flex; align-items: center; justify-content: center; gap: 0.5rem; text-decoration: none; text-align: center; font-size: 0.9rem; padding: 0.6rem;">
|
||||
<a href="#schedule-view" class="btn btn-md btn-secondary dashboard-action-link">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
@@ -135,7 +135,7 @@
|
||||
</h2>
|
||||
<div style="color: var(--text-secondary); font-size: 0.85rem; margin-top: 0.25rem;">Анализ накладок и коллизий расписания на текущую неделю</div>
|
||||
</div>
|
||||
<button class="btn-primary" id="btn-recheck-conflicts" style="font-size: 0.85rem; padding: 0.35rem 0.75rem;">Перепроверить</button>
|
||||
<button class="btn btn-sm btn-secondary" id="btn-recheck-conflicts">Перепроверить</button>
|
||||
</div>
|
||||
|
||||
<div id="conflicts-container" style="display: flex; flex-direction: column; gap: 0.75rem; margin-top: 1rem;">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="card">
|
||||
<div class="card-header-row">
|
||||
<h2>Рабочая область кафедры</h2>
|
||||
<button type="button" class="btn-primary" id="department-workspace-refresh">Обновить</button>
|
||||
<button type="button" class="btn btn-md btn-secondary" id="department-workspace-refresh">Обновить</button>
|
||||
</div>
|
||||
|
||||
<div class="schedule-view-filter-grid">
|
||||
@@ -33,11 +33,11 @@
|
||||
<div class="subject-import-slots-panel">
|
||||
<div class="card-header-row">
|
||||
<label class="schedule-section-label" style="margin:0">Дисциплины для загрузки</label>
|
||||
<button type="button" class="btn-edit-classroom" id="subject-import-add">Добавить строку</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" id="subject-import-add">Добавить строку</button>
|
||||
</div>
|
||||
<div id="subject-import-rows" class="subject-import-rows-list"></div>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Загрузить</button>
|
||||
<button type="submit" class="btn btn-md btn-secondary">Загрузить</button>
|
||||
<div class="form-alert" id="department-subject-import-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -75,7 +75,7 @@
|
||||
<div class="card" id="department-comments-card" hidden>
|
||||
<div class="card-header-row">
|
||||
<h2 id="department-comments-title">Комментарии</h2>
|
||||
<button type="button" class="btn-edit-classroom" id="department-comments-close">Скрыть</button>
|
||||
<button type="button" class="btn btn-sm btn-ghost" id="department-comments-close">Скрыть</button>
|
||||
</div>
|
||||
|
||||
<form id="department-comment-form" class="form-row">
|
||||
@@ -84,7 +84,7 @@
|
||||
<label for="department-comment-text">Новый комментарий</label>
|
||||
<input type="text" id="department-comment-text" placeholder="Добавьте примечание по дисциплине">
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Сохранить</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Сохранить</button>
|
||||
</form>
|
||||
<div class="form-alert" id="department-comment-alert" role="alert"></div>
|
||||
<div class="department-comment-list" id="department-comment-list">Выберите дисциплину</div>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<option value="">Выберите специальность</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Создать</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Создать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-group-alert" role="alert"></div>
|
||||
</form>
|
||||
@@ -127,7 +127,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; align-items: flex-end;">
|
||||
<button type="submit" class="btn-primary" style="width: 100%;">Сохранить</button>
|
||||
<button type="submit" class="btn btn-md btn-primary" style="width: 100%;">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-alert" id="edit-group-alert" role="alert"></div>
|
||||
@@ -162,8 +162,8 @@
|
||||
<input type="number" id="manage-subgroup-capacity" min="1" placeholder="12" required>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; margin-bottom: 0;">
|
||||
<button type="submit" class="btn-primary" id="btn-save-subgroup">Создать</button>
|
||||
<button type="button" class="btn-secondary" id="btn-cancel-subgroup-edit" style="display: none;">Отмена</button>
|
||||
<button type="submit" class="btn btn-md btn-primary" id="btn-save-subgroup">Создать</button>
|
||||
<button type="button" class="btn btn-md btn-ghost" id="btn-cancel-subgroup-edit" style="display: none;">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-alert" id="manage-subgroup-alert" role="alert"></div>
|
||||
@@ -219,7 +219,7 @@
|
||||
<option value="">Выберите учебный год</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary" style="height: fit-content; margin-bottom: 0;">Назначить</button>
|
||||
<button type="submit" class="btn btn-md btn-primary" style="height: fit-content; margin-bottom: 0;">Назначить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="manage-calendar-alert" role="alert"></div>
|
||||
</form>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h2>Расписания</h2>
|
||||
<span class="muted" id="schedule-view-period-label">Период не выбран</span>
|
||||
</div>
|
||||
<button type="button" class="btn-primary" id="schedule-view-load">Показать</button>
|
||||
<button type="button" class="btn btn-md btn-secondary" id="schedule-view-load">Показать</button>
|
||||
</div>
|
||||
|
||||
<div class="schedule-view-control-grid">
|
||||
@@ -87,9 +87,17 @@
|
||||
<span class="muted" id="schedule-view-count">0 занятий</span>
|
||||
</div>
|
||||
<div class="schedule-view-result-actions">
|
||||
<button type="button" class="btn-secondary" id="schedule-view-result-prev" aria-label="Предыдущее расписание">‹</button>
|
||||
<button type="button" class="btn btn-icon-md btn-secondary" id="schedule-view-result-prev" aria-label="Предыдущее расписание">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<span id="schedule-view-active-label">Нет выбранного расписания</span>
|
||||
<button type="button" class="btn-secondary" id="schedule-view-result-next" aria-label="Следующее расписание">›</button>
|
||||
<button type="button" class="btn btn-icon-md btn-secondary" id="schedule-view-result-next" aria-label="Следующее расписание">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="schedule-view-result-tabs" id="schedule-view-result-tabs"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="card create-card">
|
||||
<div class="card-header-row">
|
||||
<h2 id="schedule-rule-form-title">Новое правило расписания</h2>
|
||||
<button type="button" class="btn-edit-classroom" id="schedule-rule-reset">Очистить форму</button>
|
||||
<button type="button" class="btn btn-sm btn-ghost" id="schedule-rule-reset">Очистить форму</button>
|
||||
</div>
|
||||
<form id="schedule-rule-form" novalidate>
|
||||
<input type="hidden" id="schedule-rule-id">
|
||||
@@ -79,13 +79,13 @@
|
||||
<div class="schedule-slots-panel">
|
||||
<div class="card-header-row">
|
||||
<h2>Слоты правила</h2>
|
||||
<button type="button" class="btn-edit-classroom" id="schedule-slot-add">Добавить слот</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" id="schedule-slot-add">Добавить слот</button>
|
||||
</div>
|
||||
<div id="schedule-rule-slots" class="schedule-slots-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-row schedule-rule-actions">
|
||||
<button type="submit" class="btn-primary">Сохранить правило</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Сохранить правило</button>
|
||||
</div>
|
||||
<div class="form-alert" id="schedule-rule-alert" role="alert"></div>
|
||||
</form>
|
||||
@@ -94,7 +94,7 @@
|
||||
<div class="card">
|
||||
<div class="card-header-row">
|
||||
<h2>Правила динамического расписания</h2>
|
||||
<button class="btn-primary" id="schedule-refresh">Обновить</button>
|
||||
<button class="btn btn-md btn-secondary" id="schedule-refresh">Обновить</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table id="schedule-table">
|
||||
@@ -119,10 +119,15 @@
|
||||
</div>
|
||||
|
||||
<button type="button"
|
||||
class="schedule-visual-handle"
|
||||
class="btn btn-icon-md btn-secondary schedule-visual-handle"
|
||||
id="schedule-visual-toggle"
|
||||
aria-label="Показать визуальное расписание"
|
||||
aria-expanded="false">‹</button>
|
||||
aria-expanded="false">
|
||||
<svg class="schedule-visual-handle-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="schedule-visual-backdrop" id="schedule-visual-backdrop" hidden></div>
|
||||
|
||||
@@ -133,7 +138,7 @@
|
||||
<h2>Визуальное расписание групп</h2>
|
||||
<span class="muted" id="schedule-visual-summary">Правила ещё не загружены</span>
|
||||
</div>
|
||||
<button type="button" class="btn-edit-classroom" id="schedule-visual-close">Свернуть</button>
|
||||
<button type="button" class="btn btn-sm btn-ghost" id="schedule-visual-close">Свернуть</button>
|
||||
</div>
|
||||
<div class="schedule-visual-toolbar">
|
||||
<div class="schedule-visual-period-grid">
|
||||
@@ -153,8 +158,8 @@
|
||||
<div class="schedule-visual-filter-head">
|
||||
<span>Группы</span>
|
||||
<div>
|
||||
<button type="button" class="btn-edit-classroom" id="schedule-visual-use-form">Из формы</button>
|
||||
<button type="button" class="btn-edit-classroom" id="schedule-visual-select-all">Все</button>
|
||||
<button type="button" class="btn btn-sm btn-ghost" id="schedule-visual-use-form">Из формы</button>
|
||||
<button type="button" class="btn btn-sm btn-ghost" id="schedule-visual-select-all">Все</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="schedule-visual-group-filter" id="schedule-visual-group-filter">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Добавить</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Добавить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-subject-alert" role="alert"></div>
|
||||
</form>
|
||||
@@ -69,7 +69,7 @@
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary" style="height: fit-content; margin-bottom: 0;">Привязать</button>
|
||||
<button type="submit" class="btn btn-md btn-primary" style="height: fit-content; margin-bottom: 0;">Привязать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="manage-ts-alert" role="alert"></div>
|
||||
</form>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<label for="dept-code">Код кафедры</label>
|
||||
<input type="number" id="dept-code" placeholder="Например: 1" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Создать</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Создать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-dept-alert" role="alert"></div>
|
||||
</form>
|
||||
@@ -64,7 +64,7 @@
|
||||
<label for="spec-code">Код специальности</label>
|
||||
<input type="text" id="spec-code" placeholder="Например: 09.03.04" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Создать</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Создать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-spec-alert" role="alert"></div>
|
||||
</form>
|
||||
@@ -115,8 +115,8 @@
|
||||
<input type="text" id="tab-profile-description" placeholder="Необязательно">
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: flex-end;">
|
||||
<button type="submit" class="btn-primary" id="btn-tab-save-profile">Создать</button>
|
||||
<button type="button" class="btn-secondary" id="btn-tab-cancel-profile-edit" style="display: none;">Отмена</button>
|
||||
<button type="submit" class="btn btn-md btn-primary" id="btn-tab-save-profile">Создать</button>
|
||||
<button type="button" class="btn btn-md btn-ghost" id="btn-tab-cancel-profile-edit" style="display: none;">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-alert" id="tab-profile-alert" role="alert"></div>
|
||||
@@ -165,7 +165,7 @@
|
||||
<label for="edit-dept-code">Код кафедры</label>
|
||||
<input type="number" id="edit-dept-code" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Сохранить</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Сохранить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="edit-dept-alert" role="alert"></div>
|
||||
</form>
|
||||
@@ -188,7 +188,7 @@
|
||||
<label for="edit-spec-code">Код специальности</label>
|
||||
<input type="text" id="edit-spec-code" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Сохранить</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Сохранить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="edit-spec-alert" role="alert"></div>
|
||||
</form>
|
||||
@@ -222,8 +222,8 @@
|
||||
<input type="text" id="manage-profile-description" placeholder="Необязательно">
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: flex-end;">
|
||||
<button type="submit" class="btn-primary" id="btn-save-profile">Создать</button>
|
||||
<button type="button" class="btn-secondary" id="btn-cancel-profile-edit" style="display: none;">Отмена</button>
|
||||
<button type="submit" class="btn btn-md btn-primary" id="btn-save-profile">Создать</button>
|
||||
<button type="button" class="btn btn-md btn-ghost" id="btn-cancel-profile-edit" style="display: none;">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-alert" id="manage-profile-alert" role="alert"></div>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<label for="new-department">ID Кафедры</label>
|
||||
<input type="number" id="new-department" placeholder="ID" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Создать</button>
|
||||
<button type="submit" class="btn btn-md btn-primary">Создать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-alert" role="alert"></div>
|
||||
</form>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
<input type="password" id="password" name="password" placeholder="Введите пароль" autocomplete="current-password" required>
|
||||
<button type="button" class="toggle-password" id="toggle-password" aria-label="Показать пароль">
|
||||
<button type="button" class="btn btn-icon-md btn-secondary toggle-password" id="toggle-password" aria-label="Показать пароль">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
@@ -69,7 +69,7 @@
|
||||
|
||||
<div class="form-alert stagger-3" id="form-alert" role="alert"></div>
|
||||
|
||||
<button type="submit" class="btn-submit stagger-4" id="btn-submit">
|
||||
<button type="submit" class="btn btn-lg btn-primary btn-submit stagger-4" id="btn-submit">
|
||||
<span class="btn-text">Войти</span>
|
||||
<span class="btn-loader" hidden>
|
||||
<svg class="spinner" width="20" height="20" viewBox="0 0 24 24">
|
||||
|
||||
@@ -163,3 +163,8 @@
|
||||
}
|
||||
});
|
||||
})();
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document
|
||||
.querySelector('#login-form button[type="submit"]')
|
||||
?.classList.add('btn', 'btn-lg', 'btn-primary');
|
||||
});
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
--panel-border: #dbe3ef;
|
||||
--text: #162033;
|
||||
--muted: #66758f;
|
||||
--accent: #0f766e;
|
||||
--accent-strong: #0b5d57;
|
||||
--accent: #4F46E5;
|
||||
--accent-strong: #4338CA;
|
||||
--blue: #2563eb;
|
||||
--warning: #b45309;
|
||||
--shadow: 0 12px 34px rgba(22, 32, 51, 0.08);
|
||||
@@ -28,8 +28,8 @@
|
||||
--panel-border: #263449;
|
||||
--text: #edf3fb;
|
||||
--muted: #9cadc5;
|
||||
--accent: #2dd4bf;
|
||||
--accent-strong: #5eead4;
|
||||
--accent: #8B5CF6;
|
||||
--accent-strong: #7C3AED;
|
||||
--blue: #60a5fa;
|
||||
--warning: #f59e0b;
|
||||
--shadow: 0 18px 44px rgba(0, 0, 0, 0.28);
|
||||
@@ -94,6 +94,7 @@
|
||||
padding: 0 14px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
@@ -112,6 +113,47 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--panel);
|
||||
border-color: var(--panel-border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: color-mix(in srgb, var(--accent) 7%, var(--panel));
|
||||
}
|
||||
|
||||
.btn-danger-subtle {
|
||||
background: #FEF2F2;
|
||||
border-color: #FECACA;
|
||||
color: #B91C1C;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-danger-subtle {
|
||||
background: rgba(239, 68, 68, 0.13);
|
||||
border-color: rgba(239, 68, 68, 0.25);
|
||||
color: #FECACA;
|
||||
}
|
||||
|
||||
.btn-icon-md {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.btn-icon-md svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
min-height: 40px;
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1fr) auto auto;
|
||||
@@ -269,8 +311,12 @@
|
||||
<span id="week-label">Загрузка периода...</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" id="theme-toggle-local" type="button" aria-label="Переключить тему">◐</button>
|
||||
<a class="btn" href="/" id="logout-link">Выйти</a>
|
||||
<button class="btn btn-icon-md btn-secondary" id="theme-toggle-local" type="button" aria-label="Переключить тему">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<a class="btn btn-danger-subtle" href="/" id="logout-link">Выйти</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -279,9 +325,17 @@
|
||||
<option value="">Загрузка групп...</option>
|
||||
</select>
|
||||
<div class="week-nav">
|
||||
<button class="btn" id="prev-week" type="button" aria-label="Предыдущая неделя">‹</button>
|
||||
<button class="btn btn-primary" id="today-week" type="button">Сегодня</button>
|
||||
<button class="btn" id="next-week" type="button" aria-label="Следующая неделя">›</button>
|
||||
<button class="btn btn-icon-md btn-secondary" id="prev-week" type="button" aria-label="Предыдущая неделя">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-md btn-primary" id="today-week" type="button">Сегодня</button>
|
||||
<button class="btn btn-icon-md btn-secondary" id="next-week" type="button" aria-label="Следующая неделя">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<input class="date-input" id="date-picker" type="date" aria-label="Дата недели">
|
||||
</section>
|
||||
|
||||
@@ -25,6 +25,24 @@
|
||||
--accent-hover: #a78bfa;
|
||||
--accent-glow: rgba(139, 92, 246, 0.4);
|
||||
--accent-secondary: #ec4899;
|
||||
--button-primary-bg: #8B5CF6;
|
||||
--button-primary-bg-hover: #7C3AED;
|
||||
--button-primary-text: #FFFFFF;
|
||||
--button-secondary-bg: rgba(255, 255, 255, 0.04);
|
||||
--button-secondary-bg-hover: rgba(255, 255, 255, 0.08);
|
||||
--button-secondary-border: rgba(255, 255, 255, 0.10);
|
||||
--button-secondary-text: #F8FAFC;
|
||||
--button-ghost-bg-hover: rgba(255, 255, 255, 0.06);
|
||||
--button-danger-bg: #DC2626;
|
||||
--button-danger-bg-hover: #B91C1C;
|
||||
--button-danger-subtle-bg: rgba(239, 68, 68, 0.13);
|
||||
--button-danger-subtle-bg-hover: rgba(239, 68, 68, 0.20);
|
||||
--button-danger-subtle-border: rgba(239, 68, 68, 0.25);
|
||||
--button-danger-subtle-text: #FECACA;
|
||||
--button-focus-ring: rgba(139, 92, 246, 0.45);
|
||||
--button-disabled-bg: rgba(148, 163, 184, 0.16);
|
||||
--button-disabled-text: #94A3B8;
|
||||
--button-radius: 8px;
|
||||
|
||||
/* Status Colors */
|
||||
--error: #ef4444;
|
||||
@@ -48,10 +66,27 @@
|
||||
--text-primary: #0f172a;
|
||||
--text-secondary: #475569;
|
||||
--text-placeholder: #94a3b8;
|
||||
--accent: #6366f1;
|
||||
--accent-hover: #4f46e5;
|
||||
--accent-glow: rgba(99, 102, 241, 0.3);
|
||||
--accent: #4F46E5;
|
||||
--accent-hover: #4338CA;
|
||||
--accent-glow: rgba(79, 70, 229, 0.3);
|
||||
--accent-secondary: #d946ef;
|
||||
--button-primary-bg: #4F46E5;
|
||||
--button-primary-bg-hover: #4338CA;
|
||||
--button-primary-text: #FFFFFF;
|
||||
--button-secondary-bg: #FFFFFF;
|
||||
--button-secondary-bg-hover: #F1F5F9;
|
||||
--button-secondary-border: #CBD5E1;
|
||||
--button-secondary-text: #334155;
|
||||
--button-ghost-bg-hover: #F8FAFC;
|
||||
--button-danger-bg: #DC2626;
|
||||
--button-danger-bg-hover: #B91C1C;
|
||||
--button-danger-subtle-bg: #FEF2F2;
|
||||
--button-danger-subtle-bg-hover: #FEE2E2;
|
||||
--button-danger-subtle-border: #FECACA;
|
||||
--button-danger-subtle-text: #B91C1C;
|
||||
--button-focus-ring: rgba(79, 70, 229, 0.35);
|
||||
--button-disabled-bg: #CBD5E1;
|
||||
--button-disabled-text: #64748B;
|
||||
--error: #ef4444;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
@@ -389,59 +424,19 @@ body {
|
||||
|
||||
/* ===== Submit Button ===== */
|
||||
.btn-submit {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
padding: 0.95rem;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
transition:
|
||||
transform var(--transition),
|
||||
box-shadow var(--transition),
|
||||
opacity var(--transition);
|
||||
box-shadow: 0 4px 15px var(--accent-glow);
|
||||
}
|
||||
|
||||
.btn-submit::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: linear-gradient(rgba(255,255,255,0.2), transparent);
|
||||
border-radius: inherit;
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition);
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px var(--accent-glow);
|
||||
}
|
||||
|
||||
.btn-submit:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-submit:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 2px 10px var(--accent-glow);
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-submit:disabled {
|
||||
opacity: 0.6;
|
||||
background: var(--button-disabled-bg);
|
||||
color: var(--button-disabled-text);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* ===== Ripple Effect ===== */
|
||||
@@ -480,13 +475,12 @@ body {
|
||||
|
||||
/* ===== Theme Toggle Button ===== */
|
||||
.theme-toggle {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
color: var(--text-primary);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--button-radius);
|
||||
background: var(--button-secondary-bg);
|
||||
border: 1px solid var(--button-secondary-border);
|
||||
color: var(--button-secondary-text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -505,12 +499,13 @@ body {
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 16px var(--accent-glow);
|
||||
background: var(--button-secondary-bg-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-toggle:active {
|
||||
transform: scale(0.95);
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.theme-toggle--fixed {
|
||||
@@ -541,3 +536,94 @@ body {
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.45rem;
|
||||
min-width: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--button-radius);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition), transform var(--transition);
|
||||
}
|
||||
|
||||
.btn:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--button-focus-ring);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
min-height: 48px;
|
||||
padding: 0 20px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-icon-md {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.btn-icon-md svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--button-primary-bg);
|
||||
border-color: transparent;
|
||||
color: var(--button-primary-text);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--button-primary-bg-hover);
|
||||
color: var(--button-primary-text);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--button-secondary-bg);
|
||||
border-color: var(--button-secondary-border);
|
||||
color: var(--button-secondary-text);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--button-secondary-bg-hover);
|
||||
color: var(--button-secondary-text);
|
||||
}
|
||||
#login-form .btn.btn-primary {
|
||||
min-height: 48px !important;
|
||||
padding: 0 20px !important;
|
||||
border-radius: var(--button-radius) !important;
|
||||
background: var(--button-primary-bg) !important;
|
||||
color: var(--button-primary-text) !important;
|
||||
font-size: 15px !important;
|
||||
font-weight: 700 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
#login-form .btn.btn-primary:hover {
|
||||
background: var(--button-primary-bg-hover) !important;
|
||||
}
|
||||
#toggle-password.btn-icon-md {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
min-height: 40px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: var(--button-radius) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
--panel-border: #d8dee8;
|
||||
--text: #172033;
|
||||
--muted: #67758d;
|
||||
--accent: #1d4ed8;
|
||||
--accent-strong: #1e40af;
|
||||
--accent: #4F46E5;
|
||||
--accent-strong: #4338CA;
|
||||
--green: #047857;
|
||||
--orange: #b45309;
|
||||
--shadow: 0 12px 30px rgba(23, 32, 51, 0.08);
|
||||
@@ -28,8 +28,8 @@
|
||||
--panel-border: #2a374b;
|
||||
--text: #eef4fb;
|
||||
--muted: #a2b0c5;
|
||||
--accent: #60a5fa;
|
||||
--accent-strong: #93c5fd;
|
||||
--accent: #8B5CF6;
|
||||
--accent-strong: #7C3AED;
|
||||
--green: #34d399;
|
||||
--orange: #f59e0b;
|
||||
--shadow: 0 18px 42px rgba(0, 0, 0, 0.28);
|
||||
@@ -107,6 +107,7 @@
|
||||
padding: 0 14px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
@@ -125,6 +126,47 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--panel);
|
||||
border-color: var(--panel-border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: color-mix(in srgb, var(--accent) 7%, var(--panel));
|
||||
}
|
||||
|
||||
.btn-danger-subtle {
|
||||
background: #FEF2F2;
|
||||
border-color: #FECACA;
|
||||
color: #B91C1C;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-danger-subtle {
|
||||
background: rgba(239, 68, 68, 0.13);
|
||||
border-color: rgba(239, 68, 68, 0.25);
|
||||
color: #FECACA;
|
||||
}
|
||||
|
||||
.btn-icon-md {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.btn-icon-md svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
min-height: 40px;
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
padding: 0 12px;
|
||||
}
|
||||
@@ -275,16 +317,28 @@
|
||||
<span id="week-label">Загрузка периода...</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" id="theme-toggle-local" type="button" aria-label="Переключить тему">◐</button>
|
||||
<a class="btn" href="/" id="logout-link">Выйти</a>
|
||||
<button class="btn btn-icon-md btn-secondary" id="theme-toggle-local" type="button" aria-label="Переключить тему">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<a class="btn btn-danger-subtle" href="/" id="logout-link">Выйти</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="toolbar">
|
||||
<div class="week-nav">
|
||||
<button class="btn" id="prev-week" type="button" aria-label="Предыдущая неделя">‹</button>
|
||||
<button class="btn btn-primary" id="today-week" type="button">Сегодня</button>
|
||||
<button class="btn" id="next-week" type="button" aria-label="Следующая неделя">›</button>
|
||||
<button class="btn btn-icon-md btn-secondary" id="prev-week" type="button" aria-label="Предыдущая неделя">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-md btn-primary" id="today-week" type="button">Сегодня</button>
|
||||
<button class="btn btn-icon-md btn-secondary" id="next-week" type="button" aria-label="Следующая неделя">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<input class="date-input" id="date-picker" type="date" aria-label="Дата недели">
|
||||
</section>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
const isDark = () => document.documentElement.getAttribute('data-theme') !== 'light';
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'theme-toggle';
|
||||
btn.className = 'btn btn-icon-md btn-secondary theme-toggle';
|
||||
btn.setAttribute('aria-label', 'Переключить тему');
|
||||
btn.innerHTML = isDark() ? moonSVG : sunSVG;
|
||||
|
||||
|
||||