редизайн кнопок 1st version

This commit is contained in:
2026-06-01 02:41:28 +03:00
parent 30972db9ee
commit 3217586657
62 changed files with 1502 additions and 479 deletions

BIN
audit-buttons-01-login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
audit-buttons-08-modal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

BIN
audit-buttons-calendar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

BIN
audit-buttons-groups.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

BIN
audit-buttons-rules.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
audit-buttons-structure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
audit-buttons-subjects.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
audit-buttons-users.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
audit-buttons-workload.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

420
docs/BUTTON_STYLING_PLAN.md Normal file
View 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. Авторизация
![Экран авторизации](../audit-buttons-01-login.png)
Найдены кнопки:
| Кнопка | Текущая роль | Комментарий |
|---|---|---|
| `Войти` | Primary | Корректно считывается как главное действие экрана. |
| `Показать пароль` | Icon utility | Нужен единый icon-only стиль и обязательный `aria-label`. |
| `Переключить тему` | Icon utility | Отличается от такой же кнопки в кабинетах преподавателя и студента. |
Вывод: экран логина близок к правильной иерархии, но icon-only controls нужно привести к общей форме и размеру.
### 3.2. Дашборд админки
![Дашборд админки](../audit-buttons-02-dashboard.png)
Найдены кнопки и ссылки-кнопки:
| Кнопка | Текущий вид | Проблема |
|---|---|---|
| `Конструктор расписания` | Primary link | Хороший кандидат на основной CTA блока. |
| `Точечные изменения / Просмотр` | Secondary link | Выглядит как второстепенное действие, это корректно. |
| `Перепроверить` | `btn-primary`, маленькая высота | Primary используется для сервисного действия, хотя это не главное действие страницы. |
| `Настройки` | Sidebar button | Визуально близко к навигации, но реализовано как кнопка. |
| `Переключить тему` | Круглая icon button | Отличается от публичных кабинетов. |
Основной вывод: `btn-primary` сейчас означает и главный CTA, и компактную служебную кнопку. Это размывает иерархию.
### 3.3. Формы и таблицы админки
![Форма пользователей](../audit-buttons-03-users-form.png)
Типичные кнопки:
| Кнопка | Смысл | Текущее впечатление |
|---|---|---|
| `Создать` | Подтверждение формы | Главная кнопка, но размер и стиль отличаются между основной админкой и настройками. |
| `Архивировать` | Рискованное, но часто обратимое действие | Визуально слишком близко к удалению. |
| `Изменить` | Второстепенное действие строки | В разных разделах бывает фиолетовой или нейтральной. |
| `Удалить` | Деструктивное действие | Встречается в разных размерах и оттенках красного. |
Проблема: таблицы используют кнопки как плотный action bar, но размеры скачут от `24x28` до `113x28` и `96x40`. Это создаёт визуальный шум в строках.
### 3.4. Меню настроек и выход
![Меню настроек](../audit-buttons-04-settings-menu.png)
Кнопка `Выйти` находится рядом с навигационными пунктами и выглядит почти как обычный пункт меню. Семантически это сессионное действие с риском потерять текущий контекст, поэтому ему нужен отдельный quiet-danger стиль.
Рекомендация: `Выйти` не должен выглядеть как обычная навигация. Достаточно `Danger subtle`: красный текст на спокойной поверхности без агрессивной заливки.
### 3.5. Отдельная SPA настроек
![Общие настройки](../audit-buttons-05-settings-general.png)
В настройках виден тот же сайдбарный паттерн, но кнопки внутри разделов получают другую визуальную систему.
Пример различий:
| Класс | Основная админка | Настройки |
|---|---|---|
| `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. Кабинеты преподавателя и студента
![Кабинет преподавателя](../audit-buttons-06-teacher.png)
![Кабинет студента](../audit-buttons-07-student.png)
В публичных кабинетах используется отдельная светлая система:
| Элемент | Преподаватель | Студент |
|---|---|---|
| `Сегодня` | 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. Модальные окна
![Модальное окно создания ручной сетки](../audit-buttons-08-modal.png)
Найдены:
| Кнопка | Роль | Рекомендация |
|---|---|---|
| `Создать сетку` | 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. Приложение: дополнительные скриншоты
| Раздел | Скриншот |
|---|---|
| Конструктор правил | ![Конструктор правил](../audit-buttons-rules.png) |
| Календарный график | ![Календарный график](../audit-buttons-calendar.png) |
| Просмотр расписаний | ![Просмотр расписаний](../audit-buttons-schedule-view.png) |
| Аудиторный фонд | ![Аудиторный фонд](../audit-buttons-classrooms.png) |
| Группы обучения | ![Группы обучения](../audit-buttons-groups.png) |
| Структура вуза | ![Структура вуза](../audit-buttons-structure.png) |
| Дисциплины | ![Дисциплины](../audit-buttons-subjects.png) |
| Пользователи | ![Пользователи](../audit-buttons-users.png) |
| Временные слоты | ![Временные слоты](../audit-buttons-settings-time-slots.png) |
| Формы обучения | ![Формы обучения](../audit-buttons-settings-study-forms.png) |
| База данных | ![База данных](../audit-buttons-settings-database.png) |

View File

@@ -317,16 +317,27 @@ CSS-переменные позволяют поддерживать светл
:root { :root {
--bg-primary: #ffffff; --bg-primary: #ffffff;
--text-primary: #1a1a2e; --text-primary: #1a1a2e;
--accent: #6366f1; --accent: #4F46E5;
} }
[data-theme="dark"] { [data-theme="dark"] {
--bg-primary: #0f0f23; --bg-primary: #0f0f23;
--text-primary: #e2e8f0; --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`. Переключение — через `theme-toggle.js`.
--- ---

View File

@@ -654,32 +654,6 @@
gap: 0.45rem; 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 { .schedule-view-extra-filters {
margin-top: 0.9rem; margin-top: 0.9rem;
border-top: 1px solid var(--bg-card-border); border-top: 1px solid var(--bg-card-border);
@@ -944,8 +918,8 @@
width: 100%; width: 100%;
} }
.schedule-view-period-buttons .btn-secondary, .schedule-view-period-buttons .btn,
.schedule-view-result-actions .btn-secondary { .schedule-view-result-actions .btn {
flex: 1; flex: 1;
} }

View File

@@ -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 ===== */ /* ===== Cards ===== */
.card { .card {
background: var(--bg-card); background: var(--bg-card);
@@ -48,6 +90,189 @@
letter-spacing: 0.04em; 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 Structure ===== */
.form-row { .form-row {
display: flex; display: flex;
@@ -167,14 +392,34 @@
gap: 1rem; 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 { .metric-grid {
display: grid; display: grid;
gap: 0.75rem; gap: 0.6rem;
} }
.metric-card, .metric-card,
.department-comment-item { .department-comment-item {
padding: 0.85rem; padding: 0.8rem 0.85rem;
background: var(--bg-input); background: var(--bg-input);
border: 1px solid var(--bg-card-border); border: 1px solid var(--bg-card-border);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
@@ -183,7 +428,7 @@
.metric-card strong, .metric-card strong,
.department-comment-item strong { .department-comment-item strong {
display: block; display: block;
margin-bottom: 0.35rem; margin-bottom: 0.28rem;
color: var(--text-primary); color: var(--text-primary);
} }
@@ -299,8 +544,9 @@
.subject-import-row { .subject-import-row {
display: grid; display: grid;
grid-template-columns: minmax(120px, 1fr) minmax(200px, 3fr) auto; grid-template-columns: 160px minmax(260px, 1fr) auto;
gap: 0.75rem; column-gap: 0.65rem;
row-gap: 0.75rem;
align-items: end; align-items: end;
padding: 0.75rem; padding: 0.75rem;
background: var(--bg-input); background: var(--bg-input);
@@ -382,16 +628,97 @@
min-width: 260px; min-width: 260px;
} }
.schedule-slot-summary, .schedule-slot-summary {
.schedule-semester-line {
margin-bottom: 0.35rem; margin-bottom: 0.35rem;
} }
.schedule-semester-line { .academic-year-semesters {
display: flex; 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; align-items: center;
justify-content: space-between; justify-content: center;
gap: 0.5rem; 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 { .schedule-inline-table {
@@ -403,34 +730,33 @@
top: 50%; top: 50%;
right: 0; right: 0;
z-index: 905; z-index: 905;
width: 42px; width: 40px;
height: 74px; height: 40px;
border: 1px solid var(--bg-card-border); border: 1px solid var(--button-secondary-border);
border-right: 0; border-right: 0;
border-radius: 10px 0 0 10px; border-radius: var(--button-radius) 0 0 var(--button-radius);
background: color-mix(in srgb, var(--bg-card) 92%, var(--accent)); background: var(--button-secondary-bg);
color: var(--text-primary); color: var(--button-secondary-text);
font: inherit;
font-size: 1.8rem;
font-weight: 800;
line-height: 1;
cursor: pointer;
box-shadow: -8px 0 24px rgba(0, 0, 0, 0.2); box-shadow: -8px 0 24px rgba(0, 0, 0, 0.2);
transform: translateY(-50%); 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:hover,
.schedule-visual-handle.open { .schedule-visual-handle.open {
border-color: var(--accent); border-color: var(--button-secondary-border);
background: color-mix(in srgb, var(--accent) 20%, var(--bg-card)); background: var(--button-secondary-bg-hover);
color: var(--accent-hover); color: var(--button-secondary-text);
} }
.schedule-visual-handle.open { .schedule-visual-handle.open {
right: min(92vw, 1180px); right: min(92vw, 1180px);
} }
.schedule-visual-handle.open .schedule-visual-handle-icon {
transform: rotate(180deg);
}
.schedule-visual-backdrop { .schedule-visual-backdrop {
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -1868,108 +2194,222 @@ input[type="number"] {
} }
/* ===== Buttons ===== */ /* ===== Buttons ===== */
.btn-primary { .btn {
position: relative; display: inline-flex;
overflow: hidden; align-items: center;
padding: 0.75rem 1.75rem; justify-content: center;
background: linear-gradient(135deg, var(--accent), var(--accent-secondary)); gap: 0.45rem;
border: none; min-width: 0;
border-radius: var(--radius-sm); border: 1px solid transparent;
color: #fff; border-radius: var(--button-radius);
background: transparent;
color: inherit;
font-family: inherit; font-family: inherit;
font-size: 0.95rem; font-size: 0.875rem;
font-weight: 600; font-weight: 600;
letter-spacing: 0.02em; line-height: 1;
cursor: pointer; text-align: center;
text-decoration: none;
white-space: nowrap; white-space: nowrap;
transition: all var(--transition); cursor: pointer;
box-shadow: 0 4px 15px var(--accent-glow); user-select: none;
transition:
background var(--transition),
border-color var(--transition),
color var(--transition),
box-shadow var(--transition),
transform var(--transition);
} }
.btn-primary::before { .btn:hover:not(:disabled) {
content: ''; transform: translateY(-1px);
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-primary:hover { .btn:focus-visible {
transform: translateY(-2px); outline: none;
box-shadow: 0 8px 25px var(--accent-glow); box-shadow: 0 0 0 3px var(--button-focus-ring);
} }
.btn-primary:hover::before { .btn:active:not(:disabled) {
opacity: 1; transform: translateY(0);
} }
.btn-primary:active { .btn:disabled,
transform: translateY(1px); .btn[aria-disabled="true"] {
box-shadow: 0 2px 10px var(--accent-glow); background: var(--button-disabled-bg);
border-color: transparent;
color: var(--button-disabled-text);
cursor: not-allowed;
opacity: 0.75;
transform: none;
} }
.btn-delete { .btn-sm {
padding: 0.35rem 0.7rem; min-height: 32px;
background: rgba(248, 113, 113, 0.1); padding: 0 12px;
border: 1px solid rgba(248, 113, 113, 0.2); font-size: 13px;
border-radius: var(--radius-sm); font-weight: 600;
color: var(--error); }
.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-family: inherit;
font-size: 0.8rem; font-size: 13px;
font-weight: 600;
cursor: pointer; cursor: pointer;
transition: background var(--transition), transform var(--transition); transition: background var(--transition), transform var(--transition);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.btn-delete:hover { .btn-restore-group {
background: rgba(248, 113, 113, 0.2); 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); transform: scale(1.05);
} }
.btn-icon-toggle { .btn-icon-toggle {
background: transparent; width: 32px;
border: 1px solid var(--bg-card-border); height: 32px;
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;
} }
.btn-icon-toggle:hover { .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); transform: translateY(-1px);
} }
.group-actions .group-action-btn {
min-height: 32px;
padding: 0 12px;
font-size: 13px;
}
/* ===== Alerts ===== */ /* ===== Alerts ===== */
.form-alert { .form-alert {
display: none; display: none;
@@ -2341,20 +2781,19 @@ tbody tr:hover {
/* ===== Theme Toggle Button ===== */ /* ===== Theme Toggle Button ===== */
.theme-toggle { .theme-toggle {
width: 42px; width: 40px;
height: 42px; height: 40px;
border: none; border-radius: var(--button-radius);
border-radius: 50%; background: var(--button-secondary-bg);
background: var(--bg-card); border: 1px solid var(--button-secondary-border);
border: 1px solid var(--bg-card-border); color: var(--button-secondary-text);
color: var(--text-primary); display: inline-flex;
cursor: pointer;
display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
-webkit-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; z-index: 100;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -2366,12 +2805,18 @@ tbody tr:hover {
} }
.theme-toggle:hover { .theme-toggle:hover {
transform: scale(1.1); background: var(--button-secondary-bg-hover);
box-shadow: 0 4px 16px var(--accent-glow); border-color: var(--button-secondary-border);
transform: translateY(-1px);
} }
.theme-toggle:active { .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 { .theme-toggle--fixed {

View File

@@ -25,15 +25,7 @@
} }
.sidebar-close-btn { .sidebar-close-btn {
background: transparent;
border: none;
color: var(--text-secondary); 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); transition: all var(--transition);
} }
@@ -355,12 +347,7 @@
/* ===== Mobile Menu Toggle ===== */ /* ===== Mobile Menu Toggle ===== */
.menu-toggle { .menu-toggle {
display: none; display: none;
padding: 0.4rem;
background: none;
border: none;
color: var(--text-primary); color: var(--text-primary);
cursor: pointer;
border-radius: var(--radius-sm);
transition: background var(--transition); transition: background var(--transition);
} }

View File

@@ -51,9 +51,9 @@
--text-primary: #0f172a; --text-primary: #0f172a;
--text-secondary: #475569; --text-secondary: #475569;
--text-placeholder: #94a3b8; --text-placeholder: #94a3b8;
--accent: #6366f1; --accent: #4F46E5;
--accent-hover: #4f46e5; --accent-hover: #4338CA;
--accent-glow: rgba(99, 102, 241, 0.3); --accent-glow: rgba(79, 70, 229, 0.3);
--accent-secondary: #d946ef; --accent-secondary: #d946ef;
--error: #ef4444; --error: #ef4444;

View File

@@ -49,7 +49,7 @@
</svg> </svg>
<span>Magistr</span> <span>Magistr</span>
</div> </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"> <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" /> <polyline points="15 18 9 12 15 6" />
</svg> </svg>
@@ -193,7 +193,7 @@
Общие настройки Общие настройки
</a> </a>
<div class="settings-menu-divider"></div> <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" <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"> stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /> <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
@@ -213,7 +213,7 @@
<!-- Main --> <!-- Main -->
<main class="main"> <main class="main">
<header class="topbar"> <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" <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"> stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="6" x2="21" y2="6" /> <line x1="3" y1="6" x2="21" y2="6" />

View File

@@ -27,7 +27,7 @@ export function hideAlert(elementId) {
export function applyRippleEffect() { export function applyRippleEffect() {
document.addEventListener('click', function (e) { 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; if (!btn) return;
const rect = btn.getBoundingClientRect(); const rect = btn.getBoundingClientRect();

View File

@@ -217,12 +217,14 @@ export async function initAcademicCalendar() {
} }
academicYearsTbody.innerHTML = academicYears.map(year => ` academicYearsTbody.innerHTML = academicYears.map(year => `
<tr> <tr>
<td>${escapeHtml(year.title)}</td> <td class="academic-year-center-cell academic-year-title-cell">${escapeHtml(year.title)}</td>
<td>${escapeHtml(year.startDate)} - ${escapeHtml(year.endDate)}</td> <td class="academic-year-center-cell academic-year-period-cell">${escapeHtml(formatDisplayDateRange(year.startDate, year.endDate))}</td>
<td>${renderSemesterList(year.semesters || [])}</td> <td class="academic-year-semesters-cell">${renderSemesterList(year.semesters || [])}</td>
<td> <td class="academic-year-actions-cell">
<button class="btn-edit-classroom btn-edit-year" data-id="${year.id}">Изменить</button> <div class="academic-year-actions">
<button class="btn-delete btn-delete-year" data-id="${year.id}">Удалить</button> <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> </td>
</tr> </tr>
`).join(''); `).join('');
@@ -230,12 +232,19 @@ export async function initAcademicCalendar() {
function renderSemesterList(items) { function renderSemesterList(items) {
if (!items.length) return '<span class="muted">Нет семестров</span>'; if (!items.length) return '<span class="muted">Нет семестров</span>';
return items.map(semester => ` return `<div class="academic-year-semesters">${items.map(semester => {
<div class="schedule-semester-line"> const title = semesterTypeTitle(semester);
${escapeHtml(semesterLabel(semester))} const period = formatDisplayDateRange(semester.startDate, semester.endDate);
<button class="btn-edit-classroom btn-edit-semester" data-id="${semester.id}">Изменить</button> return `
</div> <div class="academic-year-semester-item">
`).join(''); <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>
<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) { async function saveAcademicYear(event) {
@@ -282,7 +291,7 @@ export async function initAcademicCalendar() {
if (editSemesterButton) { if (editSemesterButton) {
const semester = semesters.find(item => item.id == editSemesterButton.dataset.id); const semester = semesters.find(item => item.id == editSemesterButton.dataset.id);
if (!semester) return; if (!semester) return;
semesterFormTitle.textContent = 'Редактирование семестра'; semesterFormTitle.textContent = `Редактирование семестра: ${semesterTypeTitle(semester)}`;
semesterIdInput.value = semester.id; semesterIdInput.value = semester.id;
semesterYearSelect.value = semester.academicYearId || ''; semesterYearSelect.value = semester.academicYearId || '';
semesterTypeSelect.value = normalizeSemesterType(semester.semesterType); semesterTypeSelect.value = normalizeSemesterType(semester.semesterType);
@@ -391,9 +400,9 @@ export async function initAcademicCalendar() {
<td>${escapeHtml(calendar.studyFormName || '-')}</td> <td>${escapeHtml(calendar.studyFormName || '-')}</td>
<td>${escapeHtml(String(calendar.courseCount || '-'))}</td> <td>${escapeHtml(String(calendar.courseCount || '-'))}</td>
<td> <td>
<button class="btn-edit-classroom btn-edit-calendar" data-id="${calendar.id}">Изменить</button> <button class="btn btn-sm btn-secondary 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 btn-sm btn-secondary 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-danger btn-delete-calendar" data-id="${calendar.id}">Удалить</button>
</td> </td>
</tr> </tr>
`).join(''); `).join('');
@@ -1164,7 +1173,13 @@ export async function initAcademicCalendar() {
function semesterLabel(semester) { function semesterLabel(semester) {
const type = normalizeSemesterType(semester.semesterType); 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) { function normalizeSemesterType(type) {
@@ -1195,6 +1210,16 @@ export async function initAcademicCalendar() {
return `${day}.${month}.${year}`; 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) { function dateRangeCaption(rows) {
if (!rows.length) return ''; if (!rows.length) return '';
const dates = rows.map(row => row.date).sort(); const dates = rows.map(row => row.date).sort();

View File

@@ -87,16 +87,16 @@ export async function initClassrooms() {
<span class="badge ${c.isAvailable ? 'badge-available' : 'badge-unavailable'}"> <span class="badge ${c.isAvailable ? 'badge-available' : 'badge-unavailable'}">
${c.status === 'ARCHIVED' ? 'Архив' : (c.isAvailable ? 'Доступна' : 'Не доступна')} ${c.status === 'ARCHIVED' ? 'Архив' : (c.isAvailable ? 'Доступна' : 'Не доступна')}
</span> </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> <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> </button>
</div> </div>
</td> </td>
<td style="text-align: right;"> <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' ${c.status === 'ARCHIVED'
? `<button class="btn-restore-classroom" data-id="${c.id}" style="margin-left: 0.5rem;">Восстановить</button>` ? `<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> </td>
</tr>`; </tr>`;
}).join(''); }).join('');
@@ -125,9 +125,9 @@ export async function initClassrooms() {
}); });
classroomsTbody.addEventListener('click', async (e) => { 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 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 (btnDelete) {
if (!confirm('Вывести аудиторию из эксплуатации? Историческое расписание сохранится.')) return; if (!confirm('Вывести аудиторию из эксплуатации? Историческое расписание сохранится.')) return;
@@ -261,7 +261,7 @@ export async function initClassrooms() {
<tr> <tr>
<td>${eq.id}</td> <td>${eq.id}</td>
<td>${escapeHtml(eq.name)}</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> </tr>
`).join(''); `).join('');
} }

View File

@@ -94,7 +94,7 @@ export async function initDepartmentWorkspace() {
<td>${escapeHtml(subject.name || '-')}</td> <td>${escapeHtml(subject.name || '-')}</td>
<td>${escapeHtml(subject.description || '-')}</td> <td>${escapeHtml(subject.description || '-')}</td>
<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-id="${escapeHtml(subject.id)}"
data-subject-name="${escapeHtml(subject.name || '')}"> data-subject-name="${escapeHtml(subject.name || '')}">
Открыть Открыть
@@ -120,7 +120,7 @@ export async function initDepartmentWorkspace() {
<label>Название</label> <label>Название</label>
<input type="text" class="subject-import-name" value="${escapeHtml(row.name || '')}" placeholder="Базы данных"> <input type="text" class="subject-import-name" value="${escapeHtml(row.name || '')}" placeholder="Базы данных">
</div> </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> </div>
`).join(''); `).join('');
} }

View File

@@ -171,13 +171,15 @@ export async function initGroups() {
<td>${escapeHtml(specialityLabel(group.specialtyId || group.specialityCode))}</td> <td>${escapeHtml(specialityLabel(group.specialtyId || group.specialityCode))}</td>
<td>${escapeHtml(group.specialtyProfileName || '-')}</td> <td>${escapeHtml(group.specialtyProfileName || '-')}</td>
<td><span class="badge ${statusBadgeClass(group)}">${escapeHtml(statusLabel(group))}</span></td> <td><span class="badge ${statusBadgeClass(group)}">${escapeHtml(statusLabel(group))}</span></td>
<td style="text-align: right; white-space: nowrap;"> <td class="group-actions-cell">
<button class="btn-edit-classroom btn-edit-group" data-id="${group.id}">Изменить</button> <div class="group-actions">
<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 btn-sm btn-secondary btn-edit-group group-action-btn" data-id="${group.id}">Изменить</button>
<button class="btn-primary btn-manage-calendar" data-id="${group.id}" style="padding: 0.25rem 0.5rem; font-size: 0.85rem;">Календарь</button> <button class="btn btn-sm btn-secondary btn-manage-subgroups group-action-btn" data-id="${group.id}">Подгруппы</button>
${group.status === 'ARCHIVED' <button class="btn btn-sm btn-secondary btn-manage-calendar group-action-btn" data-id="${group.id}">Календарь</button>
? `<button class="btn-restore-group" data-id="${group.id}" style="margin-left: 0.5rem;">Восстановить</button>` ${group.status === 'ARCHIVED'
: `<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> </td>
</tr> </tr>
`).join(''); `).join('');
@@ -345,7 +347,7 @@ export async function initGroups() {
} }
async function handleGroupTableClick(event) { 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 btnEdit = event.target.closest('.btn-edit-group');
const btnRestore = event.target.closest('.btn-restore-group'); const btnRestore = event.target.closest('.btn-restore-group');
const btnSubgroups = event.target.closest('.btn-manage-subgroups'); const btnSubgroups = event.target.closest('.btn-manage-subgroups');
@@ -427,8 +429,8 @@ export async function initGroups() {
<td>${escapeHtml(sub.name)}</td> <td>${escapeHtml(sub.name)}</td>
<td>${escapeHtml(String(sub.studentCapacity ?? '-'))} чел.</td> <td>${escapeHtml(String(sub.studentCapacity ?? '-'))} чел.</td>
<td> <td>
<button class="btn-edit-classroom btn-edit-subgroup" data-id="${sub.id}">Изменить</button> <button class="btn btn-sm btn-secondary 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-danger btn-delete-subgroup" data-id="${sub.id}">Удалить</button>
</td> </td>
</tr> </tr>
`).join(''); `).join('');
@@ -539,7 +541,7 @@ export async function initGroups() {
<td>${escapeHtml(assignment.academicYearTitle)}</td> <td>${escapeHtml(assignment.academicYearTitle)}</td>
<td>${escapeHtml(assignment.calendarTitle)}</td> <td>${escapeHtml(assignment.calendarTitle)}</td>
<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> </td>
</tr> </tr>
`).join(''); `).join('');

View File

@@ -247,8 +247,8 @@ export async function initSchedule() {
<td>${renderRuleSlotsSummary(rule.slots)}</td> <td>${renderRuleSlotsSummary(rule.slots)}</td>
<td> <td>
<div class="schedule-rule-table-actions"> <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="btn btn-sm btn-secondary 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-danger btn-delete-rule" data-id="${rule.id}">Удалить</button>
</div> </div>
</td> </td>
</tr> </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> <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) : ''} ${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> <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> </div>
`).join(''); `).join('');
} }

View File

@@ -100,8 +100,8 @@ export async function initSubjects() {
<td>${escapeHtml(departmentLabel(s.departmentId))}</td> <td>${escapeHtml(departmentLabel(s.departmentId))}</td>
<td><div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">${teachersHtml}</div></td> <td><div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">${teachersHtml}</div></td>
<td style="text-align: right; white-space: nowrap;"> <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 btn-sm btn-secondary btn-manage-teachers" data-id="${s.id}">Преподаватели</button>
<button class="btn-delete btn-delete-subject" data-id="${s.id}">Удалить</button> <button class="btn btn-sm btn-danger btn-delete-subject" data-id="${s.id}">Удалить</button>
</td> </td>
</tr> </tr>
`; `;
@@ -175,7 +175,7 @@ export async function initSubjects() {
<tr> <tr>
<td><strong>${escapeHtml(ts.fullName || ts.username)}</strong> <span class="text-muted">(${escapeHtml(ts.username)})</span></td> <td><strong>${escapeHtml(ts.fullName || ts.username)}</strong> <span class="text-muted">(${escapeHtml(ts.username)})</span></td>
<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> </td>
</tr> </tr>
`).join(''); `).join('');

View File

@@ -129,8 +129,8 @@ export async function initUniversityStructure() {
<td>${escapeHtml(d.departmentName || d.name)}</td> <td>${escapeHtml(d.departmentName || d.name)}</td>
<td>${escapeHtml(String(d.departmentCode || d.code))}</td> <td>${escapeHtml(String(d.departmentCode || d.code))}</td>
<td> <td>
<button class="btn-edit-classroom btn-edit-department" data-id="${d.id}">Изменить</button> <button class="btn btn-sm btn-secondary 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-danger btn-delete-department" data-id="${d.id}">Удалить</button>
</td> </td>
</tr> </tr>
`).join(''); `).join('');
@@ -147,9 +147,9 @@ export async function initUniversityStructure() {
<td>${escapeHtml(s.specialityName || s.name)}</td> <td>${escapeHtml(s.specialityName || s.name)}</td>
<td>${escapeHtml(s.specialityCode || s.specialtyCode || s.specialty_code)}</td> <td>${escapeHtml(s.specialityCode || s.specialtyCode || s.specialty_code)}</td>
<td> <td>
<button class="btn-edit-classroom btn-edit-specialty" data-id="${s.id}">Изменить</button> <button class="btn btn-sm btn-secondary 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 btn-sm btn-secondary btn-manage-profiles" data-id="${s.id}">Профили</button>
<button class="btn-delete btn-delete-specialty" data-id="${s.id}">Удалить</button> <button class="btn btn-sm btn-danger btn-delete-specialty" data-id="${s.id}">Удалить</button>
</td> </td>
</tr> </tr>
`).join(''); `).join('');
@@ -338,8 +338,8 @@ export async function initUniversityStructure() {
<td>${escapeHtml(p.name)}</td> <td>${escapeHtml(p.name)}</td>
<td>${escapeHtml(p.description || '-')}</td> <td>${escapeHtml(p.description || '-')}</td>
<td> <td>
<button class="btn-edit-classroom btn-edit-profile" data-id="${p.id}">Изменить</button> <button class="btn btn-sm btn-secondary 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-danger btn-delete-profile" data-id="${p.id}">Удалить</button>
</td> </td>
</tr> </tr>
`).join(''); `).join('');
@@ -453,8 +453,8 @@ export async function initUniversityStructure() {
<td>${escapeHtml(p.name)}</td> <td>${escapeHtml(p.name)}</td>
<td>${escapeHtml(p.description || '-')}</td> <td>${escapeHtml(p.description || '-')}</td>
<td> <td>
<button class="btn-edit-classroom btn-edit-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-delete btn-delete-tab-profile" data-id="${p.id}">Удалить</button> <button class="btn btn-sm btn-danger btn-delete-tab-profile" data-id="${p.id}">Удалить</button>
</td> </td>
</tr> </tr>
`).join(''); `).join('');

View File

@@ -50,7 +50,7 @@ export async function initUsers() {
<td> <td>
${u.status === 'ARCHIVED' ${u.status === 'ARCHIVED'
? `<button class="btn-restore-user" data-id="${u.id}">Восстановить</button>` ? `<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> </td>
</tr> </tr>
`).join(''); `).join('');
@@ -90,7 +90,7 @@ export async function initUsers() {
}); });
usersTbody.addEventListener('click', async (e) => { usersTbody.addEventListener('click', async (e) => {
const deleteBtn = e.target.closest('.btn-delete'); const deleteBtn = e.target.closest('.btn-archive-user');
if (!deleteBtn) return; if (!deleteBtn) return;
if (!confirm('Архивировать пользователя? История расписания и связей сохранится.')) return; if (!confirm('Архивировать пользователя? История расписания и связей сохранится.')) return;

View File

@@ -25,15 +25,7 @@
} }
.sidebar-close-btn { .sidebar-close-btn {
background: transparent;
border: none;
color: var(--text-secondary); 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); transition: all var(--transition);
} }
@@ -265,12 +257,7 @@
/* ===== Mobile Menu Toggle ===== */ /* ===== Mobile Menu Toggle ===== */
.menu-toggle { .menu-toggle {
display: none; display: none;
padding: 0.4rem;
background: none;
border: none;
color: var(--text-primary); color: var(--text-primary);
cursor: pointer;
border-radius: var(--radius-sm);
transition: background var(--transition); transition: background var(--transition);
} }

View File

@@ -46,9 +46,9 @@
--text-primary: #0f172a; --text-primary: #0f172a;
--text-secondary: #475569; --text-secondary: #475569;
--text-placeholder: #94a3b8; --text-placeholder: #94a3b8;
--accent: #6366f1; --accent: #4F46E5;
--accent-hover: #4f46e5; --accent-hover: #4338CA;
--accent-glow: rgba(99, 102, 241, 0.3); --accent-glow: rgba(79, 70, 229, 0.3);
--accent-secondary: #d946ef; --accent-secondary: #d946ef;
--error: #ef4444; --error: #ef4444;
--success: #10b981; --success: #10b981;
@@ -77,20 +77,19 @@ body {
/* ===== Переключатель темы ===== */ /* ===== Переключатель темы ===== */
.theme-toggle { .theme-toggle {
width: 42px; width: 40px;
height: 42px; height: 40px;
border: none; border-radius: var(--button-radius);
border-radius: 50%; background: var(--button-secondary-bg);
background: var(--bg-card); border: 1px solid var(--button-secondary-border);
border: 1px solid var(--bg-card-border); color: var(--button-secondary-text);
color: var(--text-primary); display: inline-flex;
cursor: pointer;
display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
-webkit-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; z-index: 100;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -102,12 +101,18 @@ body {
} }
.theme-toggle:hover { .theme-toggle:hover {
transform: scale(1.1); background: var(--button-secondary-bg-hover);
box-shadow: 0 4px 16px var(--accent-glow); border-color: var(--button-secondary-border);
transform: translateY(-1px);
} }
.theme-toggle:active { .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 { .theme-toggle--fixed {
@@ -265,63 +270,6 @@ body {
cursor: pointer; 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 { .form-alert {
grid-column: 1 / -1; grid-column: 1 / -1;
min-height: 0; min-height: 0;
@@ -614,7 +562,7 @@ body {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.settings-form .btn-primary { .settings-form .btn {
width: fit-content; width: fit-content;
} }
@@ -630,9 +578,16 @@ body {
flex-direction: column; flex-direction: column;
} }
.settings-header-row .btn-primary, .settings-header-row .btn,
.card-header-row .btn-secondary, .card-header-row .btn,
.settings-form .btn-primary { .settings-form .btn {
width: 100%; 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;
}

View File

@@ -32,7 +32,7 @@
</svg> </svg>
<span>Настройки</span> <span>Настройки</span>
</div> </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"> <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" /> <polyline points="15 18 9 12 15 6" />
</svg> </svg>
@@ -94,7 +94,7 @@
<!-- Основная область --> <!-- Основная область -->
<main class="main"> <main class="main">
<header class="topbar"> <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" <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"> stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="6" x2="21" y2="6" /> <line x1="3" y1="6" x2="21" y2="6" />

View File

@@ -68,7 +68,7 @@ export async function initDatabase() {
<td><code style="font-size: 0.82rem;">${escapeHtml(t.url)}</code></td> <td><code style="font-size: 0.82rem;">${escapeHtml(t.url)}</code></td>
<td>${escapeHtml(t.username || '—')}</td> <td>${escapeHtml(t.username || '—')}</td>
<td>${statusBadge}</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>`; </tr>`;
}).join(''); }).join('');
} }
@@ -136,7 +136,7 @@ export async function initDatabase() {
// === Удаление тенанта === // === Удаление тенанта ===
tenantsTbody.addEventListener('click', async (e) => { tenantsTbody.addEventListener('click', async (e) => {
const btn = e.target.closest('.btn-delete'); const btn = e.target.closest('.btn-delete-tenant');
if (!btn) return; if (!btn) return;
const domain = btn.dataset.domain; const domain = btn.dataset.domain;

View File

@@ -35,7 +35,7 @@ export async function initEduForms() {
<tr> <tr>
<td>${ef.id}</td> <td>${ef.id}</td>
<td>${escapeHtml(ef.name)}</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(''); </tr>`).join('');
} }
@@ -56,7 +56,7 @@ export async function initEduForms() {
}); });
efTbody.addEventListener('click', async (e) => { 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 (!btn) return;
if (!confirm('Удалить форму обучения?')) return; if (!confirm('Удалить форму обучения?')) return;
try { try {

View File

@@ -143,8 +143,8 @@ export async function initTimeSlotsSettings() {
<td>${escapeHtml(trimTime(slot.endTime))}</td> <td>${escapeHtml(trimTime(slot.endTime))}</td>
<td>${escapeHtml(String(slot.durationMinutes ?? '-'))}</td> <td>${escapeHtml(String(slot.durationMinutes ?? '-'))}</td>
<td class="actions-cell"> <td class="actions-cell">
<button type="button" class="btn-secondary btn-edit-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-delete btn-delete-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> </td>
</tr> </tr>
`).join(''); `).join('');

View File

@@ -61,10 +61,10 @@
<label for="tenant-password">Пароль</label> <label for="tenant-password">Пароль</label>
<input type="password" id="tenant-password" placeholder="••••••••" required> <input type="password" id="tenant-password" placeholder="••••••••" required>
</div> </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>
<button type="submit" class="btn-primary" style="height: fit-content;"> <button type="submit" class="btn btn-md btn-primary" style="height: fit-content;">
Добавить Добавить
</button> </button>
</div> </div>

View File

@@ -7,7 +7,7 @@
<label for="new-ef-name">Название</label> <label for="new-ef-name">Название</label>
<input type="text" id="new-ef-name" placeholder="Бакалавриат" required> <input type="text" id="new-ef-name" placeholder="Бакалавриат" required>
</div> </div>
<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-ef-alert" role="alert"></div> <div class="form-alert" id="create-ef-alert" role="alert"></div>
</form> </form>

View File

@@ -4,7 +4,7 @@
<h2>Временные слоты</h2> <h2>Временные слоты</h2>
<p>Настройка базовой, субботней и ручных сеток времени.</p> <p>Настройка базовой, субботней и ручных сеток времени.</p>
</div> </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>
<div class="settings-grid"> <div class="settings-grid">
@@ -12,8 +12,8 @@
<div class="card-header-row"> <div class="card-header-row">
<h3>Сетка времени</h3> <h3>Сетка времени</h3>
<div class="scope-header-actions"> <div class="scope-header-actions">
<button type="button" class="btn-primary" id="time-slot-scope-open-create">Новая ручная сетка</button> <button type="button" class="btn btn-md 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-danger-subtle" id="time-slot-scope-delete">Удалить сетку</button>
</div> </div>
</div> </div>
@@ -47,7 +47,7 @@
<article class="settings-card"> <article class="settings-card">
<div class="card-header-row"> <div class="card-header-row">
<h3 id="time-slot-form-title">Новый слот</h3> <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> </div>
<form id="time-slot-form" class="settings-form" novalidate> <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"> <input type="number" id="time-slot-duration" min="1" step="1" placeholder="90">
</label> </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> <div class="form-alert" id="time-slot-alert" role="alert"></div>
</form> </form>
</article> </article>
@@ -106,7 +106,12 @@
<h3 id="time-slot-scope-modal-title">Новая ручная сетка</h3> <h3 id="time-slot-scope-modal-title">Новая ручная сетка</h3>
<p class="settings-modal-subtitle">Создайте отдельную сетку времени, которую затем можно назначать датам в календарном графике.</p> <p class="settings-modal-subtitle">Создайте отдельную сетку времени, которую затем можно назначать датам в календарном графике.</p>
</div> </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> </div>
<form id="time-slot-scope-form" class="settings-modal-form" novalidate> <form id="time-slot-scope-form" class="settings-modal-form" novalidate>
@@ -116,8 +121,8 @@
</label> </label>
<div class="settings-modal-actions"> <div class="settings-modal-actions">
<button type="submit" class="btn-primary">Создать сетку</button> <button type="submit" class="btn btn-md btn-primary">Создать сетку</button>
<button type="button" class="btn-secondary" id="time-slot-scope-modal-cancel">Отмена</button> <button type="button" class="btn btn-md btn-ghost" id="time-slot-scope-modal-cancel">Отмена</button>
</div> </div>
<div class="form-alert" id="time-slot-scope-alert" role="alert"></div> <div class="form-alert" id="time-slot-scope-alert" role="alert"></div>

View File

@@ -15,8 +15,8 @@
<label for="academic-year-end">Окончание</label> <label for="academic-year-end">Окончание</label>
<input type="date" id="academic-year-end" required> <input type="date" id="academic-year-end" required>
</div> </div>
<button type="submit" class="btn-primary">Сохранить год</button> <button type="submit" class="btn btn-md btn-primary">Сохранить год</button>
<button type="button" class="btn-edit-classroom" id="academic-year-reset">Очистить</button> <button type="button" class="btn btn-md btn-ghost" id="academic-year-reset">Очистить</button>
</div> </div>
<div class="form-alert" id="academic-year-alert" role="alert"></div> <div class="form-alert" id="academic-year-alert" role="alert"></div>
</form> </form>
@@ -25,7 +25,7 @@
<div class="card"> <div class="card">
<div class="card-header-row"> <div class="card-header-row">
<h2>Учебные годы</h2> <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>
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
@@ -72,8 +72,8 @@
<label for="semester-end">Окончание</label> <label for="semester-end">Окончание</label>
<input type="date" id="semester-end" required> <input type="date" id="semester-end" required>
</div> </div>
<button type="submit" class="btn-primary">Сохранить семестр</button> <button type="submit" class="btn btn-md btn-primary">Сохранить семестр</button>
<button type="button" class="btn-edit-classroom" id="semester-reset">Очистить</button> <button type="button" class="btn btn-md btn-ghost" id="semester-reset">Очистить</button>
</div> </div>
<div class="form-alert" id="semester-alert" role="alert"></div> <div class="form-alert" id="semester-alert" role="alert"></div>
</form> </form>
@@ -82,7 +82,7 @@
<div class="card create-card"> <div class="card create-card">
<div class="card-header-row"> <div class="card-header-row">
<h2 id="academic-calendar-form-title">Календарный учебный график</h2> <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> </div>
<form id="academic-calendar-form" novalidate> <form id="academic-calendar-form" novalidate>
<input type="hidden" id="academic-calendar-id"> <input type="hidden" id="academic-calendar-id">
@@ -119,7 +119,7 @@
<label for="academic-calendar-course-count">Курсов</label> <label for="academic-calendar-course-count">Курсов</label>
<input type="number" id="academic-calendar-course-count" min="1" max="8" value="4" required> <input type="number" id="academic-calendar-course-count" min="1" max="8" value="4" required>
</div> </div>
<button type="submit" class="btn-primary">Сохранить график</button> <button type="submit" class="btn btn-md btn-primary">Сохранить график</button>
</div> </div>
<div class="form-alert" id="academic-calendar-alert" role="alert"></div> <div class="form-alert" id="academic-calendar-alert" role="alert"></div>
</form> </form>
@@ -128,7 +128,7 @@
<div class="card"> <div class="card">
<div class="card-header-row"> <div class="card-header-row">
<h2>Календарные графики</h2> <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>
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
@@ -161,8 +161,8 @@
<option value="">Загрузка...</option> <option value="">Загрузка...</option>
</select> </select>
</div> </div>
<button type="button" class="btn-primary" id="calendar-editor-load">Загрузить сетку</button> <button type="button" class="btn btn-md btn-secondary" id="calendar-editor-load">Загрузить сетку</button>
<button type="button" class="btn-primary" id="calendar-editor-save">Сохранить сетку</button> <button type="button" class="btn btn-md btn-primary" id="calendar-editor-save">Сохранить сетку</button>
</div> </div>
<div class="calendar-fill-panel"> <div class="calendar-fill-panel">
<div class="form-group"> <div class="form-group">
@@ -185,7 +185,7 @@
<option value="">Загрузка...</option> <option value="">Загрузка...</option>
</select> </select>
</div> </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>
<div class="form-alert" id="calendar-editor-alert" role="alert"></div> <div class="form-alert" id="calendar-editor-alert" role="alert"></div>
<div id="calendar-totals" class="calendar-totals"></div> <div id="calendar-totals" class="calendar-totals"></div>
@@ -229,8 +229,8 @@
</select> </select>
</div> </div>
<div class="calendar-day-dialog-actions"> <div class="calendar-day-dialog-actions">
<button type="button" class="btn-primary" id="calendar-day-dialog-time-save">Применить</button> <button type="button" class="btn btn-md 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-danger-subtle" id="calendar-day-dialog-time-clear">Убрать ручную сетку</button>
</div> </div>
<div class="form-alert" id="calendar-day-dialog-time-alert" role="alert"></div> <div class="form-alert" id="calendar-day-dialog-time-alert" role="alert"></div>
</div> </div>

View File

@@ -1,7 +1,7 @@
<div class="card"> <div class="card">
<div class="card-header-row" style="margin-bottom: 1.5rem;"> <div class="card-header-row" style="margin-bottom: 1.5rem;">
<h2>Загруженность</h2> <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>
<div class="filter-row workload-filter-row" style="margin-bottom: 2rem; align-items: flex-end; gap: 1.5rem;"> <div class="filter-row workload-filter-row" style="margin-bottom: 2rem; align-items: flex-end; gap: 1.5rem;">

View File

@@ -2,7 +2,7 @@
<div class="card create-card"> <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;"> <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> <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> </div>
<form id="create-classroom-form"> <form id="create-classroom-form">
<div class="form-row"> <div class="form-row">
@@ -36,7 +36,7 @@
</div> </div>
</div> </div>
<div class="form-group" style="display: flex; align-items: flex-end;"> <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> </div>
<div class="form-alert" id="create-classroom-alert" role="alert"></div> <div class="form-alert" id="create-classroom-alert" role="alert"></div>
@@ -103,7 +103,7 @@
</div> </div>
</div> </div>
<div class="form-group" style="display: flex; align-items: flex-end;"> <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> </div>
<div class="form-alert" id="edit-classroom-alert" role="alert"></div> <div class="form-alert" id="edit-classroom-alert" role="alert"></div>
@@ -128,7 +128,7 @@
<label for="manage-equipment-name">Название оборудования</label> <label for="manage-equipment-name">Название оборудования</label>
<input type="text" id="manage-equipment-name" placeholder="Проектор" required> <input type="text" id="manage-equipment-name" placeholder="Проектор" required>
</div> </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>
<div class="form-alert" id="manage-equipment-alert" role="alert"></div> <div class="form-alert" id="manage-equipment-alert" role="alert"></div>
</form> </form>
@@ -152,4 +152,4 @@
</table> </table>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,9 +1,9 @@
<!-- ===== Dashboard Page ===== --> <!-- ===== 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="dashboard-metrics-grid">
<div class="metric-card card" style="display: flex; align-items: center; gap: 1rem; padding: 1.25rem; border-left: 4px solid var(--primary);"> <div class="card dashboard-metric-card dashboard-metric-card--groups">
<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="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"> <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" /> <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" /> <circle cx="9" cy="7" r="4" />
@@ -17,8 +17,8 @@
</div> </div>
</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="card dashboard-metric-card dashboard-metric-card--students">
<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="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"> <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" /> <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" /> <circle cx="9" cy="7" r="4" />
@@ -32,8 +32,8 @@
</div> </div>
</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="card dashboard-metric-card dashboard-metric-card--teachers">
<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="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"> <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" /> <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" /> <circle cx="12" cy="7" r="4" />
@@ -45,8 +45,8 @@
</div> </div>
</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="card dashboard-metric-card dashboard-metric-card--classrooms">
<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="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"> <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" /> <rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M9 3v18M15 3v18M3 9h18M3 15h18" /> <path d="M9 3v18M15 3v18M3 9h18M3 15h18" />
@@ -60,14 +60,14 @@
</div> </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 class="card dashboard-monitor-card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> <div class="dashboard-card-header">
<h2 style="margin: 0;">Мониторинг пар (Идет сейчас)</h2> <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>
<div class="table-wrap" style="flex: 1;"> <div class="table-wrap dashboard-monitor-table">
<table style="width: 100%; border-collapse: collapse;"> <table style="width: 100%; border-collapse: collapse;">
<thead> <thead>
<tr> <tr>
@@ -87,20 +87,20 @@
</div> </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;"> <div class="card dashboard-compact-card">
<h3 style="margin: 0; font-size: 1.1rem; font-weight: 600;">Свободные аудитории (ближайшие)</h3> <h3 class="dashboard-card-title">Свободные аудитории (ближайшие)</h3>
<div id="free-classrooms-list" style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem;"> <div id="free-classrooms-list" class="dashboard-badge-list">
<div style="color: var(--text-secondary); font-size: 0.9rem;">Загрузка...</div> <div style="color: var(--text-secondary); font-size: 0.9rem;">Загрузка...</div>
</div> </div>
</div> </div>
<!-- Быстрые действия --> <!-- Быстрые действия -->
<div class="card" style="padding: 1.25rem; display: flex; flex-direction: column; gap: 0.75rem;"> <div class="card dashboard-compact-card">
<h3 style="margin: 0; font-size: 1.1rem; font-weight: 600;">Быстрый переход</h3> <h3 class="dashboard-card-title">Быстрый переход</h3>
<div style="display: grid; grid-template-columns: 1fr; gap: 0.5rem; margin-top: 0.5rem;"> <div class="dashboard-actions">
<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;"> <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"> <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> <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line> <line x1="16" y1="2" x2="16" y2="6"></line>
@@ -109,7 +109,7 @@
</svg> </svg>
Конструктор расписания Конструктор расписания
</a> </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"> <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" /> <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" /> <circle cx="12" cy="12" r="3" />
@@ -135,7 +135,7 @@
</h2> </h2>
<div style="color: var(--text-secondary); font-size: 0.85rem; margin-top: 0.25rem;">Анализ накладок и коллизий расписания на текущую неделю</div> <div style="color: var(--text-secondary); font-size: 0.85rem; margin-top: 0.25rem;">Анализ накладок и коллизий расписания на текущую неделю</div>
</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>
<div id="conflicts-container" style="display: flex; flex-direction: column; gap: 0.75rem; margin-top: 1rem;"> <div id="conflicts-container" style="display: flex; flex-direction: column; gap: 0.75rem; margin-top: 1rem;">

View File

@@ -1,7 +1,7 @@
<div class="card"> <div class="card">
<div class="card-header-row"> <div class="card-header-row">
<h2>Рабочая область кафедры</h2> <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>
<div class="schedule-view-filter-grid"> <div class="schedule-view-filter-grid">
@@ -33,11 +33,11 @@
<div class="subject-import-slots-panel"> <div class="subject-import-slots-panel">
<div class="card-header-row"> <div class="card-header-row">
<label class="schedule-section-label" style="margin:0">Дисциплины для загрузки</label> <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>
<div id="subject-import-rows" class="subject-import-rows-list"></div> <div id="subject-import-rows" class="subject-import-rows-list"></div>
</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> <div class="form-alert" id="department-subject-import-alert" role="alert"></div>
</form> </form>
</div> </div>
@@ -75,7 +75,7 @@
<div class="card" id="department-comments-card" hidden> <div class="card" id="department-comments-card" hidden>
<div class="card-header-row"> <div class="card-header-row">
<h2 id="department-comments-title">Комментарии</h2> <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> </div>
<form id="department-comment-form" class="form-row"> <form id="department-comment-form" class="form-row">
@@ -84,7 +84,7 @@
<label for="department-comment-text">Новый комментарий</label> <label for="department-comment-text">Новый комментарий</label>
<input type="text" id="department-comment-text" placeholder="Добавьте примечание по дисциплине"> <input type="text" id="department-comment-text" placeholder="Добавьте примечание по дисциплине">
</div> </div>
<button type="submit" class="btn-primary">Сохранить</button> <button type="submit" class="btn btn-md btn-primary">Сохранить</button>
</form> </form>
<div class="form-alert" id="department-comment-alert" role="alert"></div> <div class="form-alert" id="department-comment-alert" role="alert"></div>
<div class="department-comment-list" id="department-comment-list">Выберите дисциплину</div> <div class="department-comment-list" id="department-comment-list">Выберите дисциплину</div>

View File

@@ -39,7 +39,7 @@
<option value="">Выберите специальность</option> <option value="">Выберите специальность</option>
</select> </select>
</div> </div>
<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-group-alert" role="alert"></div> <div class="form-alert" id="create-group-alert" role="alert"></div>
</form> </form>
@@ -127,7 +127,7 @@
</select> </select>
</div> </div>
<div class="form-group" style="display: flex; align-items: flex-end;"> <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> </div>
<div class="form-alert" id="edit-group-alert" role="alert"></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> <input type="number" id="manage-subgroup-capacity" min="1" placeholder="12" required>
</div> </div>
<div style="display: flex; gap: 0.5rem; margin-bottom: 0;"> <div style="display: flex; gap: 0.5rem; margin-bottom: 0;">
<button type="submit" class="btn-primary" id="btn-save-subgroup">Создать</button> <button type="submit" class="btn btn-md btn-primary" id="btn-save-subgroup">Создать</button>
<button type="button" class="btn-secondary" id="btn-cancel-subgroup-edit" style="display: none;">Отмена</button> <button type="button" class="btn btn-md btn-ghost" id="btn-cancel-subgroup-edit" style="display: none;">Отмена</button>
</div> </div>
</div> </div>
<div class="form-alert" id="manage-subgroup-alert" role="alert"></div> <div class="form-alert" id="manage-subgroup-alert" role="alert"></div>
@@ -219,7 +219,7 @@
<option value="">Выберите учебный год</option> <option value="">Выберите учебный год</option>
</select> </select>
</div> </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>
<div class="form-alert" id="manage-calendar-alert" role="alert"></div> <div class="form-alert" id="manage-calendar-alert" role="alert"></div>
</form> </form>

View File

@@ -5,7 +5,7 @@
<h2>Расписания</h2> <h2>Расписания</h2>
<span class="muted" id="schedule-view-period-label">Период не выбран</span> <span class="muted" id="schedule-view-period-label">Период не выбран</span>
</div> </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>
<div class="schedule-view-control-grid"> <div class="schedule-view-control-grid">
@@ -87,9 +87,17 @@
<span class="muted" id="schedule-view-count">0 занятий</span> <span class="muted" id="schedule-view-count">0 занятий</span>
</div> </div>
<div class="schedule-view-result-actions"> <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> <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> </div>
<div class="schedule-view-result-tabs" id="schedule-view-result-tabs"></div> <div class="schedule-view-result-tabs" id="schedule-view-result-tabs"></div>

View File

@@ -1,7 +1,7 @@
<div class="card create-card"> <div class="card create-card">
<div class="card-header-row"> <div class="card-header-row">
<h2 id="schedule-rule-form-title">Новое правило расписания</h2> <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> </div>
<form id="schedule-rule-form" novalidate> <form id="schedule-rule-form" novalidate>
<input type="hidden" id="schedule-rule-id"> <input type="hidden" id="schedule-rule-id">
@@ -79,13 +79,13 @@
<div class="schedule-slots-panel"> <div class="schedule-slots-panel">
<div class="card-header-row"> <div class="card-header-row">
<h2>Слоты правила</h2> <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>
<div id="schedule-rule-slots" class="schedule-slots-list"></div> <div id="schedule-rule-slots" class="schedule-slots-list"></div>
</div> </div>
<div class="form-row schedule-rule-actions"> <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>
<div class="form-alert" id="schedule-rule-alert" role="alert"></div> <div class="form-alert" id="schedule-rule-alert" role="alert"></div>
</form> </form>
@@ -94,7 +94,7 @@
<div class="card"> <div class="card">
<div class="card-header-row"> <div class="card-header-row">
<h2>Правила динамического расписания</h2> <h2>Правила динамического расписания</h2>
<button class="btn-primary" id="schedule-refresh">Обновить</button> <button class="btn btn-md btn-secondary" id="schedule-refresh">Обновить</button>
</div> </div>
<div class="table-wrap"> <div class="table-wrap">
<table id="schedule-table"> <table id="schedule-table">
@@ -119,10 +119,15 @@
</div> </div>
<button type="button" <button type="button"
class="schedule-visual-handle" class="btn btn-icon-md btn-secondary schedule-visual-handle"
id="schedule-visual-toggle" id="schedule-visual-toggle"
aria-label="Показать визуальное расписание" 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> <div class="schedule-visual-backdrop" id="schedule-visual-backdrop" hidden></div>
@@ -133,7 +138,7 @@
<h2>Визуальное расписание групп</h2> <h2>Визуальное расписание групп</h2>
<span class="muted" id="schedule-visual-summary">Правила ещё не загружены</span> <span class="muted" id="schedule-visual-summary">Правила ещё не загружены</span>
</div> </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>
<div class="schedule-visual-toolbar"> <div class="schedule-visual-toolbar">
<div class="schedule-visual-period-grid"> <div class="schedule-visual-period-grid">
@@ -153,8 +158,8 @@
<div class="schedule-visual-filter-head"> <div class="schedule-visual-filter-head">
<span>Группы</span> <span>Группы</span>
<div> <div>
<button type="button" class="btn-edit-classroom" id="schedule-visual-use-form">Из формы</button> <button type="button" class="btn btn-sm btn-ghost" 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-select-all">Все</button>
</div> </div>
</div> </div>
<div class="schedule-visual-group-filter" id="schedule-visual-group-filter"> <div class="schedule-visual-group-filter" id="schedule-visual-group-filter">

View File

@@ -17,7 +17,7 @@
<option value="">Загрузка...</option> <option value="">Загрузка...</option>
</select> </select>
</div> </div>
<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-subject-alert" role="alert"></div> <div class="form-alert" id="create-subject-alert" role="alert"></div>
</form> </form>
@@ -69,7 +69,7 @@
<option value="">Загрузка...</option> <option value="">Загрузка...</option>
</select> </select>
</div> </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>
<div class="form-alert" id="manage-ts-alert" role="alert"></div> <div class="form-alert" id="manage-ts-alert" role="alert"></div>
</form> </form>
@@ -92,4 +92,4 @@
</table> </table>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -20,7 +20,7 @@
<label for="dept-code">Код кафедры</label> <label for="dept-code">Код кафедры</label>
<input type="number" id="dept-code" placeholder="Например: 1" required> <input type="number" id="dept-code" placeholder="Например: 1" required>
</div> </div>
<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-dept-alert" role="alert"></div> <div class="form-alert" id="create-dept-alert" role="alert"></div>
</form> </form>
@@ -64,7 +64,7 @@
<label for="spec-code">Код специальности</label> <label for="spec-code">Код специальности</label>
<input type="text" id="spec-code" placeholder="Например: 09.03.04" required> <input type="text" id="spec-code" placeholder="Например: 09.03.04" required>
</div> </div>
<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-spec-alert" role="alert"></div> <div class="form-alert" id="create-spec-alert" role="alert"></div>
</form> </form>
@@ -115,8 +115,8 @@
<input type="text" id="tab-profile-description" placeholder="Необязательно"> <input type="text" id="tab-profile-description" placeholder="Необязательно">
</div> </div>
<div style="display: flex; gap: 0.5rem; align-items: flex-end;"> <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="submit" class="btn btn-md 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="button" class="btn btn-md btn-ghost" id="btn-tab-cancel-profile-edit" style="display: none;">Отмена</button>
</div> </div>
</div> </div>
<div class="form-alert" id="tab-profile-alert" role="alert"></div> <div class="form-alert" id="tab-profile-alert" role="alert"></div>
@@ -165,7 +165,7 @@
<label for="edit-dept-code">Код кафедры</label> <label for="edit-dept-code">Код кафедры</label>
<input type="number" id="edit-dept-code" required> <input type="number" id="edit-dept-code" required>
</div> </div>
<button type="submit" class="btn-primary">Сохранить</button> <button type="submit" class="btn btn-md btn-primary">Сохранить</button>
</div> </div>
<div class="form-alert" id="edit-dept-alert" role="alert"></div> <div class="form-alert" id="edit-dept-alert" role="alert"></div>
</form> </form>
@@ -188,7 +188,7 @@
<label for="edit-spec-code">Код специальности</label> <label for="edit-spec-code">Код специальности</label>
<input type="text" id="edit-spec-code" required> <input type="text" id="edit-spec-code" required>
</div> </div>
<button type="submit" class="btn-primary">Сохранить</button> <button type="submit" class="btn btn-md btn-primary">Сохранить</button>
</div> </div>
<div class="form-alert" id="edit-spec-alert" role="alert"></div> <div class="form-alert" id="edit-spec-alert" role="alert"></div>
</form> </form>
@@ -222,8 +222,8 @@
<input type="text" id="manage-profile-description" placeholder="Необязательно"> <input type="text" id="manage-profile-description" placeholder="Необязательно">
</div> </div>
<div style="display: flex; gap: 0.5rem; align-items: flex-end;"> <div style="display: flex; gap: 0.5rem; align-items: flex-end;">
<button type="submit" class="btn-primary" id="btn-save-profile">Создать</button> <button type="submit" class="btn btn-md btn-primary" id="btn-save-profile">Создать</button>
<button type="button" class="btn-secondary" id="btn-cancel-profile-edit" style="display: none;">Отмена</button> <button type="button" class="btn btn-md btn-ghost" id="btn-cancel-profile-edit" style="display: none;">Отмена</button>
</div> </div>
</div> </div>
<div class="form-alert" id="manage-profile-alert" role="alert"></div> <div class="form-alert" id="manage-profile-alert" role="alert"></div>

View File

@@ -34,7 +34,7 @@
<label for="new-department">ID Кафедры</label> <label for="new-department">ID Кафедры</label>
<input type="number" id="new-department" placeholder="ID" required> <input type="number" id="new-department" placeholder="ID" required>
</div> </div>
<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-alert" role="alert"></div> <div class="form-alert" id="create-alert" role="alert"></div>
</form> </form>

View File

@@ -57,7 +57,7 @@
<path d="M7 11V7a5 5 0 0 1 10 0v4"/> <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg> </svg>
<input type="password" id="password" name="password" placeholder="Введите пароль" autocomplete="current-password" required> <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"> <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"/> <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"/> <circle cx="12" cy="12" r="3"/>
@@ -69,7 +69,7 @@
<div class="form-alert stagger-3" id="form-alert" role="alert"></div> <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-text">Войти</span>
<span class="btn-loader" hidden> <span class="btn-loader" hidden>
<svg class="spinner" width="20" height="20" viewBox="0 0 24 24"> <svg class="spinner" width="20" height="20" viewBox="0 0 24 24">

View File

@@ -163,3 +163,8 @@
} }
}); });
})(); })();
document.addEventListener('DOMContentLoaded', () => {
document
.querySelector('#login-form button[type="submit"]')
?.classList.add('btn', 'btn-lg', 'btn-primary');
});

View File

@@ -15,8 +15,8 @@
--panel-border: #dbe3ef; --panel-border: #dbe3ef;
--text: #162033; --text: #162033;
--muted: #66758f; --muted: #66758f;
--accent: #0f766e; --accent: #4F46E5;
--accent-strong: #0b5d57; --accent-strong: #4338CA;
--blue: #2563eb; --blue: #2563eb;
--warning: #b45309; --warning: #b45309;
--shadow: 0 12px 34px rgba(22, 32, 51, 0.08); --shadow: 0 12px 34px rgba(22, 32, 51, 0.08);
@@ -28,8 +28,8 @@
--panel-border: #263449; --panel-border: #263449;
--text: #edf3fb; --text: #edf3fb;
--muted: #9cadc5; --muted: #9cadc5;
--accent: #2dd4bf; --accent: #8B5CF6;
--accent-strong: #5eead4; --accent-strong: #7C3AED;
--blue: #60a5fa; --blue: #60a5fa;
--warning: #f59e0b; --warning: #f59e0b;
--shadow: 0 18px 44px rgba(0, 0, 0, 0.28); --shadow: 0 18px 44px rgba(0, 0, 0, 0.28);
@@ -94,6 +94,7 @@
padding: 0 14px; padding: 0 14px;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
} }
.btn:hover { .btn:hover {
@@ -112,6 +113,47 @@
color: #fff; 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 { .toolbar {
display: grid; display: grid;
grid-template-columns: minmax(220px, 1fr) auto auto; grid-template-columns: minmax(220px, 1fr) auto auto;
@@ -269,8 +311,12 @@
<span id="week-label">Загрузка периода...</span> <span id="week-label">Загрузка периода...</span>
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn" id="theme-toggle-local" type="button" aria-label="Переключить тему"></button> <button class="btn btn-icon-md btn-secondary" id="theme-toggle-local" type="button" aria-label="Переключить тему">
<a class="btn" href="/" id="logout-link">Выйти</a> <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> </div>
</header> </header>
@@ -279,9 +325,17 @@
<option value="">Загрузка групп...</option> <option value="">Загрузка групп...</option>
</select> </select>
<div class="week-nav"> <div class="week-nav">
<button class="btn" id="prev-week" type="button" aria-label="Предыдущая неделя"></button> <button class="btn btn-icon-md btn-secondary" id="prev-week" type="button" aria-label="Предыдущая неделя">
<button class="btn btn-primary" id="today-week" type="button">Сегодня</button> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<button class="btn" id="next-week" type="button" aria-label="Следующая неделя"></button> <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> </div>
<input class="date-input" id="date-picker" type="date" aria-label="Дата недели"> <input class="date-input" id="date-picker" type="date" aria-label="Дата недели">
</section> </section>

View File

@@ -25,6 +25,24 @@
--accent-hover: #a78bfa; --accent-hover: #a78bfa;
--accent-glow: rgba(139, 92, 246, 0.4); --accent-glow: rgba(139, 92, 246, 0.4);
--accent-secondary: #ec4899; --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 */ /* Status Colors */
--error: #ef4444; --error: #ef4444;
@@ -48,10 +66,27 @@
--text-primary: #0f172a; --text-primary: #0f172a;
--text-secondary: #475569; --text-secondary: #475569;
--text-placeholder: #94a3b8; --text-placeholder: #94a3b8;
--accent: #6366f1; --accent: #4F46E5;
--accent-hover: #4f46e5; --accent-hover: #4338CA;
--accent-glow: rgba(99, 102, 241, 0.3); --accent-glow: rgba(79, 70, 229, 0.3);
--accent-secondary: #d946ef; --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; --error: #ef4444;
--success: #10b981; --success: #10b981;
--warning: #f59e0b; --warning: #f59e0b;
@@ -389,59 +424,19 @@ body {
/* ===== Submit Button ===== */ /* ===== Submit Button ===== */
.btn-submit { .btn-submit {
position: relative;
overflow: hidden;
width: 100%; 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; 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 { .btn-submit:active {
transform: translateY(1px); transform: translateY(0);
box-shadow: 0 2px 10px var(--accent-glow);
} }
.btn-submit:disabled { .btn-submit:disabled {
opacity: 0.6; background: var(--button-disabled-bg);
color: var(--button-disabled-text);
cursor: not-allowed; cursor: not-allowed;
transform: none; transform: none;
box-shadow: none;
} }
/* ===== Ripple Effect ===== */ /* ===== Ripple Effect ===== */
@@ -480,13 +475,12 @@ body {
/* ===== Theme Toggle Button ===== */ /* ===== Theme Toggle Button ===== */
.theme-toggle { .theme-toggle {
width: 42px; width: 40px;
height: 42px; height: 40px;
border: none; border-radius: var(--button-radius);
border-radius: 50%; background: var(--button-secondary-bg);
background: var(--bg-card); border: 1px solid var(--button-secondary-border);
border: 1px solid var(--bg-card-border); color: var(--button-secondary-text);
color: var(--text-primary);
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -505,12 +499,13 @@ body {
} }
.theme-toggle:hover { .theme-toggle:hover {
transform: scale(1.1); background: var(--button-secondary-bg-hover);
box-shadow: 0 4px 16px var(--accent-glow); transform: translateY(-1px);
box-shadow: none;
} }
.theme-toggle:active { .theme-toggle:active {
transform: scale(0.95); transform: translateY(0);
} }
.theme-toggle--fixed { .theme-toggle--fixed {
@@ -540,4 +535,95 @@ body {
width: 18px; width: 18px;
height: 18px; 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;
}

View File

@@ -15,8 +15,8 @@
--panel-border: #d8dee8; --panel-border: #d8dee8;
--text: #172033; --text: #172033;
--muted: #67758d; --muted: #67758d;
--accent: #1d4ed8; --accent: #4F46E5;
--accent-strong: #1e40af; --accent-strong: #4338CA;
--green: #047857; --green: #047857;
--orange: #b45309; --orange: #b45309;
--shadow: 0 12px 30px rgba(23, 32, 51, 0.08); --shadow: 0 12px 30px rgba(23, 32, 51, 0.08);
@@ -28,8 +28,8 @@
--panel-border: #2a374b; --panel-border: #2a374b;
--text: #eef4fb; --text: #eef4fb;
--muted: #a2b0c5; --muted: #a2b0c5;
--accent: #60a5fa; --accent: #8B5CF6;
--accent-strong: #93c5fd; --accent-strong: #7C3AED;
--green: #34d399; --green: #34d399;
--orange: #f59e0b; --orange: #f59e0b;
--shadow: 0 18px 42px rgba(0, 0, 0, 0.28); --shadow: 0 18px 42px rgba(0, 0, 0, 0.28);
@@ -107,6 +107,7 @@
padding: 0 14px; padding: 0 14px;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
} }
.btn:hover { .btn:hover {
@@ -125,6 +126,47 @@
color: #fff; 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 { .date-input {
padding: 0 12px; padding: 0 12px;
} }
@@ -275,16 +317,28 @@
<span id="week-label">Загрузка периода...</span> <span id="week-label">Загрузка периода...</span>
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn" id="theme-toggle-local" type="button" aria-label="Переключить тему"></button> <button class="btn btn-icon-md btn-secondary" id="theme-toggle-local" type="button" aria-label="Переключить тему">
<a class="btn" href="/" id="logout-link">Выйти</a> <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> </div>
</header> </header>
<section class="toolbar"> <section class="toolbar">
<div class="week-nav"> <div class="week-nav">
<button class="btn" id="prev-week" type="button" aria-label="Предыдущая неделя"></button> <button class="btn btn-icon-md btn-secondary" id="prev-week" type="button" aria-label="Предыдущая неделя">
<button class="btn btn-primary" id="today-week" type="button">Сегодня</button> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<button class="btn" id="next-week" type="button" aria-label="Следующая неделя"></button> <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> </div>
<input class="date-input" id="date-picker" type="date" aria-label="Дата недели"> <input class="date-input" id="date-picker" type="date" aria-label="Дата недели">
</section> </section>

View File

@@ -23,7 +23,7 @@
const isDark = () => document.documentElement.getAttribute('data-theme') !== 'light'; const isDark = () => document.documentElement.getAttribute('data-theme') !== 'light';
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.className = 'theme-toggle'; btn.className = 'btn btn-icon-md btn-secondary theme-toggle';
btn.setAttribute('aria-label', 'Переключить тему'); btn.setAttribute('aria-label', 'Переключить тему');
btn.innerHTML = isDark() ? moonSVG : sunSVG; btn.innerHTML = isDark() ? moonSVG : sunSVG;