diff --git a/audit-buttons-01-login.png b/audit-buttons-01-login.png new file mode 100644 index 0000000..0126525 Binary files /dev/null and b/audit-buttons-01-login.png differ diff --git a/audit-buttons-02-dashboard.png b/audit-buttons-02-dashboard.png new file mode 100644 index 0000000..37379d0 Binary files /dev/null and b/audit-buttons-02-dashboard.png differ diff --git a/audit-buttons-03-users-form.png b/audit-buttons-03-users-form.png new file mode 100644 index 0000000..3908c5b Binary files /dev/null and b/audit-buttons-03-users-form.png differ diff --git a/audit-buttons-04-settings-menu.png b/audit-buttons-04-settings-menu.png new file mode 100644 index 0000000..4be70fa Binary files /dev/null and b/audit-buttons-04-settings-menu.png differ diff --git a/audit-buttons-05-settings-general.png b/audit-buttons-05-settings-general.png new file mode 100644 index 0000000..56270c9 Binary files /dev/null and b/audit-buttons-05-settings-general.png differ diff --git a/audit-buttons-06-teacher.png b/audit-buttons-06-teacher.png new file mode 100644 index 0000000..2715515 Binary files /dev/null and b/audit-buttons-06-teacher.png differ diff --git a/audit-buttons-07-student.png b/audit-buttons-07-student.png new file mode 100644 index 0000000..a67b4a2 Binary files /dev/null and b/audit-buttons-07-student.png differ diff --git a/audit-buttons-08-modal.png b/audit-buttons-08-modal.png new file mode 100644 index 0000000..91c9241 Binary files /dev/null and b/audit-buttons-08-modal.png differ diff --git a/audit-buttons-calendar.png b/audit-buttons-calendar.png new file mode 100644 index 0000000..7e7860b Binary files /dev/null and b/audit-buttons-calendar.png differ diff --git a/audit-buttons-classrooms.png b/audit-buttons-classrooms.png new file mode 100644 index 0000000..bdaaa0e Binary files /dev/null and b/audit-buttons-classrooms.png differ diff --git a/audit-buttons-groups.png b/audit-buttons-groups.png new file mode 100644 index 0000000..c77a12f Binary files /dev/null and b/audit-buttons-groups.png differ diff --git a/audit-buttons-rules.png b/audit-buttons-rules.png new file mode 100644 index 0000000..39627fc Binary files /dev/null and b/audit-buttons-rules.png differ diff --git a/audit-buttons-schedule-view.png b/audit-buttons-schedule-view.png new file mode 100644 index 0000000..a76f7e3 Binary files /dev/null and b/audit-buttons-schedule-view.png differ diff --git a/audit-buttons-settings-database.png b/audit-buttons-settings-database.png new file mode 100644 index 0000000..b82fcd4 Binary files /dev/null and b/audit-buttons-settings-database.png differ diff --git a/audit-buttons-settings-study-forms.png b/audit-buttons-settings-study-forms.png new file mode 100644 index 0000000..b5af080 Binary files /dev/null and b/audit-buttons-settings-study-forms.png differ diff --git a/audit-buttons-settings-time-slots.png b/audit-buttons-settings-time-slots.png new file mode 100644 index 0000000..79c6eff Binary files /dev/null and b/audit-buttons-settings-time-slots.png differ diff --git a/audit-buttons-structure.png b/audit-buttons-structure.png new file mode 100644 index 0000000..956c41d Binary files /dev/null and b/audit-buttons-structure.png differ diff --git a/audit-buttons-subjects.png b/audit-buttons-subjects.png new file mode 100644 index 0000000..a7b4e42 Binary files /dev/null and b/audit-buttons-subjects.png differ diff --git a/audit-buttons-users.png b/audit-buttons-users.png new file mode 100644 index 0000000..b180fc4 Binary files /dev/null and b/audit-buttons-users.png differ diff --git a/audit-buttons-workload.png b/audit-buttons-workload.png new file mode 100644 index 0000000..658b076 Binary files /dev/null and b/audit-buttons-workload.png differ diff --git a/docs/BUTTON_STYLING_PLAN.md b/docs/BUTTON_STYLING_PLAN.md new file mode 100644 index 0000000..68db2c9 --- /dev/null +++ b/docs/BUTTON_STYLING_PLAN.md @@ -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) | diff --git a/docs/FRONTEND.md b/docs/FRONTEND.md index f52dba3..d8f54d5 100644 --- a/docs/FRONTEND.md +++ b/docs/FRONTEND.md @@ -317,16 +317,27 @@ CSS-переменные позволяют поддерживать светл :root { --bg-primary: #ffffff; --text-primary: #1a1a2e; - --accent: #6366f1; + --accent: #4F46E5; } [data-theme="dark"] { --bg-primary: #0f0f23; --text-primary: #e2e8f0; - --accent: #818cf8; + --accent: #8B5CF6; } ``` +### Система кнопок + +Кнопки админ-панели, отдельной SPA настроек, страницы входа и личных кабинетов используют единую семантическую модель: + +- `btn` — обязательная база для всех кнопок и ссылок-кнопок. +- Размеры: `btn-sm` — 32px для табличных строк, `btn-md` — 40px для форм и панелей, `btn-lg` — 48px для входа и главных широких действий. +- Иконки: `btn-icon-sm` — 32x32, `btn-icon-md` — 40x40. Все icon-only кнопки должны иметь `aria-label` на русском языке. +- Варианты: `btn-primary` для основного действия блока, `btn-secondary` для редактирования/обновления/показа, `btn-ghost` для очистки/отмены/сворачивания, `btn-danger` для удаления, `btn-danger-subtle` для архивирования, выхода и других рискованных действий без немедленного удаления. +- Единый радиус кнопок — `8px`. Локальные переопределения `.btn-primary`, `.btn-secondary`, `.btn-delete` в feature CSS запрещены: новые экраны должны наследовать токены из `frontend/admin/css/components.css`. +- Primary-акцент общий для продукта: `#4F46E5` в светлой теме и `#8B5CF6` в тёмной. Ролевые цвета кнопок в кабинетах преподавателя и студента не используются. + Переключение — через `theme-toggle.js`. --- diff --git a/frontend/admin/css/auditorium-workload.css b/frontend/admin/css/auditorium-workload.css index e7ada53..677c985 100644 --- a/frontend/admin/css/auditorium-workload.css +++ b/frontend/admin/css/auditorium-workload.css @@ -654,32 +654,6 @@ gap: 0.45rem; } -.schedule-view-shell .btn-secondary { - min-height: 42px; - padding: 0.65rem 0.9rem; - border: 1px solid var(--bg-card-border); - border-radius: var(--radius-sm); - background: var(--bg-input); - color: var(--text-primary); - font: inherit; - font-size: 0.9rem; - font-weight: 600; - cursor: pointer; - transition: background var(--transition), border-color var(--transition), color var(--transition), transform var(--transition); -} - -.schedule-view-shell .btn-secondary:hover:not(:disabled) { - background: var(--bg-hover); - border-color: var(--accent); - color: var(--accent-hover); - transform: translateY(-1px); -} - -.schedule-view-shell .btn-secondary:disabled { - cursor: default; - opacity: 0.45; -} - .schedule-view-extra-filters { margin-top: 0.9rem; border-top: 1px solid var(--bg-card-border); @@ -944,8 +918,8 @@ width: 100%; } - .schedule-view-period-buttons .btn-secondary, - .schedule-view-result-actions .btn-secondary { + .schedule-view-period-buttons .btn, + .schedule-view-result-actions .btn { flex: 1; } diff --git a/frontend/admin/css/components.css b/frontend/admin/css/components.css index 446af8d..444e36c 100755 --- a/frontend/admin/css/components.css +++ b/frontend/admin/css/components.css @@ -1,3 +1,45 @@ +/* ===== Button Tokens ===== */ +:root { + --button-primary-bg: #8B5CF6; + --button-primary-bg-hover: #7C3AED; + --button-primary-text: #FFFFFF; + --button-secondary-bg: rgba(255, 255, 255, 0.04); + --button-secondary-bg-hover: rgba(255, 255, 255, 0.08); + --button-secondary-border: rgba(255, 255, 255, 0.10); + --button-secondary-text: #F8FAFC; + --button-ghost-bg-hover: rgba(255, 255, 255, 0.06); + --button-danger-bg: #DC2626; + --button-danger-bg-hover: #B91C1C; + --button-danger-subtle-bg: rgba(239, 68, 68, 0.13); + --button-danger-subtle-bg-hover: rgba(239, 68, 68, 0.20); + --button-danger-subtle-border: rgba(239, 68, 68, 0.25); + --button-danger-subtle-text: #FECACA; + --button-focus-ring: rgba(139, 92, 246, 0.45); + --button-disabled-bg: rgba(148, 163, 184, 0.16); + --button-disabled-text: #94A3B8; + --button-radius: 8px; +} + +[data-theme="light"] { + --button-primary-bg: #4F46E5; + --button-primary-bg-hover: #4338CA; + --button-primary-text: #FFFFFF; + --button-secondary-bg: #FFFFFF; + --button-secondary-bg-hover: #F1F5F9; + --button-secondary-border: #CBD5E1; + --button-secondary-text: #334155; + --button-ghost-bg-hover: #F8FAFC; + --button-danger-bg: #DC2626; + --button-danger-bg-hover: #B91C1C; + --button-danger-subtle-bg: #FEF2F2; + --button-danger-subtle-bg-hover: #FEE2E2; + --button-danger-subtle-border: #FECACA; + --button-danger-subtle-text: #B91C1C; + --button-focus-ring: rgba(79, 70, 229, 0.35); + --button-disabled-bg: #CBD5E1; + --button-disabled-text: #64748B; +} + /* ===== Cards ===== */ .card { background: var(--bg-card); @@ -48,6 +90,189 @@ letter-spacing: 0.04em; } +/* ===== Dashboard ===== */ +.dashboard-container { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.dashboard-metrics-grid { + display: grid; + grid-template-columns: repeat(4, minmax(180px, 1fr)); + gap: 1.25rem; +} + +.dashboard-metric-card { + --metric-color: var(--accent); + --metric-bg: rgba(99, 102, 241, 0.1); + display: flex; + align-items: center; + gap: 1rem; + min-height: 112px; + padding: 1.25rem; + border-left: 4px solid var(--metric-color); +} + +.dashboard-metric-card--students { + --metric-color: #10b981; + --metric-bg: rgba(16, 185, 129, 0.1); +} + +.dashboard-metric-card--teachers { + --metric-color: #f59e0b; + --metric-bg: rgba(245, 158, 11, 0.1); +} + +.dashboard-metric-card--classrooms { + --metric-color: #3b82f6; + --metric-bg: rgba(59, 130, 246, 0.1); +} + +.dashboard-metric-card .metric-icon { + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 48px; + width: 48px; + height: 48px; + color: var(--metric-color); + background: var(--metric-bg); + border-radius: 12px; +} + +.dashboard-middle { + display: grid; + grid-template-columns: minmax(0, 2fr) minmax(320px, 1fr); + gap: 1.25rem; + align-items: stretch; +} + +.dashboard-monitor-card, +.dashboard-compact-card { + display: flex; + flex-direction: column; +} + +.dashboard-monitor-card { + min-height: 100%; +} + +.dashboard-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} + +.dashboard-card-header h2 { + margin: 0; +} + +.dashboard-time-slot { + flex: 0 0 auto; + padding: 0.35rem 0.75rem; + border-radius: 999px; + color: var(--accent-hover); + background: var(--bg-hover); + font-size: 0.85rem; + font-weight: 600; + white-space: nowrap; +} + +.dashboard-monitor-table { + flex: 1; +} + +.dashboard-side-stack { + display: grid; + grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); + gap: 1.25rem; + min-height: 100%; +} + +.dashboard-compact-card { + min-height: 0; + padding: 1.25rem; + gap: 0.75rem; +} + +.dashboard-card-title { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + color: var(--text-primary); +} + +.dashboard-badge-list { + display: flex; + flex-wrap: wrap; + align-content: flex-start; + gap: 0.5rem; + margin-top: 0.25rem; +} + +.dashboard-actions { + display: grid; + grid-template-columns: 1fr; + gap: 0.65rem; + margin-top: auto; +} + +.dashboard-action-link { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + min-height: 40px; + padding: 0.6rem 0.85rem; + text-align: center; + text-decoration: none; + font-size: 0.9rem; +} + +.dashboard-action-link.btn-secondary { + color: var(--button-secondary-text); + background: var(--button-secondary-bg); + border: 1px solid var(--button-secondary-border); + border-radius: var(--button-radius); + box-shadow: none; +} + +.dashboard-action-link.btn-secondary:hover { + color: var(--button-secondary-text); + background: var(--button-secondary-bg-hover); + border-color: var(--button-secondary-border); +} + +@media (max-width: 1200px) { + .dashboard-metrics-grid { + grid-template-columns: repeat(2, minmax(180px, 1fr)); + } + + .dashboard-middle { + grid-template-columns: 1fr; + } + + .dashboard-side-stack { + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-rows: auto; + } +} + +@media (max-width: 700px) { + .dashboard-metrics-grid, + .dashboard-side-stack { + grid-template-columns: 1fr; + } + + .dashboard-card-header { + align-items: flex-start; + flex-direction: column; + } +} + /* ===== Form Structure ===== */ .form-row { display: flex; @@ -167,14 +392,34 @@ gap: 1rem; } +.department-workspace-grid { + align-items: stretch; +} + +.department-subject-import-card, +.department-teachers-card { + display: flex; + flex-direction: column; + align-self: stretch; + min-height: 100%; +} + +.department-teachers-card .card-header-row { + margin-bottom: 0.85rem; +} + +.department-teachers-card .metric-grid { + margin-top: 0; +} + .metric-grid { display: grid; - gap: 0.75rem; + gap: 0.6rem; } .metric-card, .department-comment-item { - padding: 0.85rem; + padding: 0.8rem 0.85rem; background: var(--bg-input); border: 1px solid var(--bg-card-border); border-radius: var(--radius-sm); @@ -183,7 +428,7 @@ .metric-card strong, .department-comment-item strong { display: block; - margin-bottom: 0.35rem; + margin-bottom: 0.28rem; color: var(--text-primary); } @@ -299,8 +544,9 @@ .subject-import-row { display: grid; - grid-template-columns: minmax(120px, 1fr) minmax(200px, 3fr) auto; - gap: 0.75rem; + grid-template-columns: 160px minmax(260px, 1fr) auto; + column-gap: 0.65rem; + row-gap: 0.75rem; align-items: end; padding: 0.75rem; background: var(--bg-input); @@ -382,16 +628,97 @@ min-width: 260px; } -.schedule-slot-summary, -.schedule-semester-line { +.schedule-slot-summary { margin-bottom: 0.35rem; } -.schedule-semester-line { - display: flex; +.academic-year-semesters { + display: grid; + grid-template-columns: repeat(2, minmax(220px, 1fr)); + gap: 0.35rem; + align-items: stretch; +} + +.academic-years-table tbody td { + padding-top: 0.6rem; + padding-bottom: 0.6rem; + vertical-align: middle; +} + +.academic-years-table thead th:first-child, +.academic-years-table thead th:nth-child(2), +.academic-years-table thead th:last-child { + text-align: center; +} + +.academic-year-center-cell { + text-align: center; + font-weight: 700; +} + +.academic-year-semesters-cell { + min-width: 520px; + vertical-align: middle; +} + +.academic-year-actions-cell { + text-align: center; + vertical-align: middle; +} + +.academic-year-actions { + display: inline-flex; align-items: center; - justify-content: space-between; - gap: 0.5rem; + justify-content: center; + gap: 0.4rem; + min-height: 100%; + white-space: nowrap; +} + +.academic-year-semester-item { + display: grid; + grid-template-columns: minmax(220px, 1fr) auto; + align-items: center; + gap: 0.65rem; + padding: 0.5rem 0.65rem; + background: var(--bg-input); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-sm); +} + +.subject-import-code-field, +.subject-import-name-field { + min-width: 0; +} + +.subject-import-row .form-group label { + margin-bottom: 0.45rem; +} + +@media (max-width: 900px) { + .academic-year-semesters { + grid-template-columns: 1fr; + } + + .academic-year-semesters-cell { + min-width: 360px; + } +} + +.academic-year-semester-meta { + display: grid; + gap: 0.2rem; +} + +.academic-year-semester-type { + color: var(--text-primary); + font-size: 0.9rem; + font-weight: 700; +} + +.academic-year-semester-period { + color: var(--text-secondary); + font-size: 0.82rem; } .schedule-inline-table { @@ -403,34 +730,33 @@ top: 50%; right: 0; z-index: 905; - width: 42px; - height: 74px; - border: 1px solid var(--bg-card-border); + width: 40px; + height: 40px; + border: 1px solid var(--button-secondary-border); border-right: 0; - border-radius: 10px 0 0 10px; - background: color-mix(in srgb, var(--bg-card) 92%, var(--accent)); - color: var(--text-primary); - font: inherit; - font-size: 1.8rem; - font-weight: 800; - line-height: 1; - cursor: pointer; + border-radius: var(--button-radius) 0 0 var(--button-radius); + background: var(--button-secondary-bg); + color: var(--button-secondary-text); box-shadow: -8px 0 24px rgba(0, 0, 0, 0.2); transform: translateY(-50%); - transition: right var(--transition), background var(--transition), border-color var(--transition), color var(--transition); + transition: right var(--transition), background var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition); } .schedule-visual-handle:hover, .schedule-visual-handle.open { - border-color: var(--accent); - background: color-mix(in srgb, var(--accent) 20%, var(--bg-card)); - color: var(--accent-hover); + border-color: var(--button-secondary-border); + background: var(--button-secondary-bg-hover); + color: var(--button-secondary-text); } .schedule-visual-handle.open { right: min(92vw, 1180px); } +.schedule-visual-handle.open .schedule-visual-handle-icon { + transform: rotate(180deg); +} + .schedule-visual-backdrop { position: fixed; inset: 0; @@ -1868,108 +2194,222 @@ input[type="number"] { } /* ===== Buttons ===== */ -.btn-primary { - position: relative; - overflow: hidden; - padding: 0.75rem 1.75rem; - background: linear-gradient(135deg, var(--accent), var(--accent-secondary)); - border: none; - border-radius: var(--radius-sm); - color: #fff; +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.45rem; + min-width: 0; + border: 1px solid transparent; + border-radius: var(--button-radius); + background: transparent; + color: inherit; font-family: inherit; - font-size: 0.95rem; + font-size: 0.875rem; font-weight: 600; - letter-spacing: 0.02em; - cursor: pointer; + line-height: 1; + text-align: center; + text-decoration: none; white-space: nowrap; - transition: all var(--transition); - box-shadow: 0 4px 15px var(--accent-glow); + cursor: pointer; + user-select: none; + transition: + background var(--transition), + border-color var(--transition), + color var(--transition), + box-shadow var(--transition), + transform var(--transition); } -.btn-primary::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(rgba(255, 255, 255, 0.2), transparent); - border-radius: inherit; - opacity: 0; - transition: opacity var(--transition); +.btn:hover:not(:disabled) { + transform: translateY(-1px); } -.btn-primary:hover { - transform: translateY(-2px); - box-shadow: 0 8px 25px var(--accent-glow); +.btn:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--button-focus-ring); } -.btn-primary:hover::before { - opacity: 1; +.btn:active:not(:disabled) { + transform: translateY(0); } -.btn-primary:active { - transform: translateY(1px); - box-shadow: 0 2px 10px var(--accent-glow); +.btn:disabled, +.btn[aria-disabled="true"] { + background: var(--button-disabled-bg); + border-color: transparent; + color: var(--button-disabled-text); + cursor: not-allowed; + opacity: 0.75; + transform: none; } -.btn-delete { - padding: 0.35rem 0.7rem; - background: rgba(248, 113, 113, 0.1); - border: 1px solid rgba(248, 113, 113, 0.2); - border-radius: var(--radius-sm); - color: var(--error); +.btn-sm { + min-height: 32px; + padding: 0 12px; + font-size: 13px; + font-weight: 600; +} + +.btn-md { + min-height: 40px; + padding: 0 16px; + font-size: 14px; + font-weight: 600; +} + +.btn-lg { + min-height: 48px; + padding: 0 20px; + font-size: 15px; + font-weight: 700; +} + +.btn-icon-sm, +.btn-icon-md { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + padding: 0; + border-radius: var(--button-radius); + line-height: 1; +} + +.btn-icon-sm { + width: 32px; + height: 32px; +} + +.btn-icon-md { + width: 40px; + height: 40px; +} + +.btn-icon-sm svg { + width: 18px; + height: 18px; +} + +.btn-icon-md svg { + width: 20px; + height: 20px; +} + +.btn-primary { + background: var(--button-primary-bg); + border-color: transparent; + color: var(--button-primary-text); + box-shadow: none; +} + +.btn-primary:hover:not(:disabled) { + background: var(--button-primary-bg-hover); + color: var(--button-primary-text); +} + +.btn-secondary { + background: var(--button-secondary-bg); + border-color: var(--button-secondary-border); + color: var(--button-secondary-text); +} + +.btn-secondary:hover:not(:disabled) { + background: var(--button-secondary-bg-hover); + border-color: var(--button-secondary-border); + color: var(--button-secondary-text); +} + +.btn-ghost { + background: transparent; + border-color: transparent; + color: var(--text-secondary); +} + +.btn-ghost:hover:not(:disabled) { + background: var(--button-ghost-bg-hover); + color: var(--text-primary); +} + +.btn-danger { + background: var(--button-danger-bg); + border-color: transparent; + color: #FFFFFF; +} + +.btn-danger:hover:not(:disabled) { + background: var(--button-danger-bg-hover); + color: #FFFFFF; +} + +.btn-danger-subtle { + background: var(--button-danger-subtle-bg); + border-color: var(--button-danger-subtle-border); + color: var(--button-danger-subtle-text); +} + +.btn-danger-subtle:hover:not(:disabled) { + background: var(--button-danger-subtle-bg-hover); + border-color: var(--button-danger-subtle-border); + color: var(--button-danger-subtle-text); +} + +.group-actions-cell { + text-align: right; + white-space: nowrap; +} + +.group-actions { + display: grid; + grid-template-columns: 88px 104px 104px 116px; + gap: 0.35rem; + justify-content: end; + align-items: center; +} + +.group-action-btn { + width: 100%; +} + +.btn-restore-group { + min-height: 32px; + padding: 0 12px; + border-radius: var(--button-radius); font-family: inherit; - font-size: 0.8rem; + font-size: 13px; + font-weight: 600; cursor: pointer; transition: background var(--transition), transform var(--transition); position: relative; overflow: hidden; } -.btn-delete:hover { - background: rgba(248, 113, 113, 0.2); +.btn-restore-group { + background: rgba(245, 158, 11, 0.1); + border: 1px solid rgba(245, 158, 11, 0.24); + color: var(--warning); +} + +.btn-restore-group:hover { + background: rgba(245, 158, 11, 0.2); transform: scale(1.05); } .btn-icon-toggle { - background: transparent; - border: 1px solid var(--bg-card-border); - color: var(--text-secondary); - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 6px; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - padding: 0; + width: 32px; + height: 32px; } .btn-icon-toggle:hover { - background: var(--bg-card); - border-color: var(--accent); - color: var(--accent); - transform: rotate(45deg); - box-shadow: 0 0 10px var(--accent-glow); -} - -.btn-edit-classroom { - padding: 0.35rem 0.7rem; - background: rgba(99, 102, 241, 0.1); - border: 1px solid rgba(99, 102, 241, 0.2); - border-radius: var(--radius-sm); - color: var(--accent-hover); - cursor: pointer; - transition: all var(--transition); -} - -.btn-edit-classroom:hover { - background: rgba(99, 102, 241, 0.2); transform: translateY(-1px); } +.group-actions .group-action-btn { + min-height: 32px; + padding: 0 12px; + font-size: 13px; +} + /* ===== Alerts ===== */ .form-alert { display: none; @@ -2341,20 +2781,19 @@ tbody tr:hover { /* ===== Theme Toggle Button ===== */ .theme-toggle { - width: 42px; - height: 42px; - border: none; - border-radius: 50%; - background: var(--bg-card); - border: 1px solid var(--bg-card-border); - color: var(--text-primary); - cursor: pointer; - display: flex; + width: 40px; + height: 40px; + border-radius: var(--button-radius); + background: var(--button-secondary-bg); + border: 1px solid var(--button-secondary-border); + color: var(--button-secondary-text); + display: inline-flex; align-items: center; justify-content: center; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); - transition: background 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease; + padding: 0; + transition: background var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition), transform var(--transition); z-index: 100; flex-shrink: 0; } @@ -2366,12 +2805,18 @@ tbody tr:hover { } .theme-toggle:hover { - transform: scale(1.1); - box-shadow: 0 4px 16px var(--accent-glow); + background: var(--button-secondary-bg-hover); + border-color: var(--button-secondary-border); + transform: translateY(-1px); } .theme-toggle:active { - transform: scale(0.95); + transform: translateY(0); +} + +.theme-toggle:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--button-focus-ring); } .theme-toggle--fixed { diff --git a/frontend/admin/css/layout.css b/frontend/admin/css/layout.css index 3265a1a..57bbc88 100755 --- a/frontend/admin/css/layout.css +++ b/frontend/admin/css/layout.css @@ -25,15 +25,7 @@ } .sidebar-close-btn { - background: transparent; - border: none; color: var(--text-secondary); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - padding: 0.25rem; - border-radius: var(--radius-sm); transition: all var(--transition); } @@ -355,12 +347,7 @@ /* ===== Mobile Menu Toggle ===== */ .menu-toggle { display: none; - padding: 0.4rem; - background: none; - border: none; color: var(--text-primary); - cursor: pointer; - border-radius: var(--radius-sm); transition: background var(--transition); } diff --git a/frontend/admin/css/main.css b/frontend/admin/css/main.css index a03d06e..cbfb555 100755 --- a/frontend/admin/css/main.css +++ b/frontend/admin/css/main.css @@ -51,9 +51,9 @@ --text-primary: #0f172a; --text-secondary: #475569; --text-placeholder: #94a3b8; - --accent: #6366f1; - --accent-hover: #4f46e5; - --accent-glow: rgba(99, 102, 241, 0.3); + --accent: #4F46E5; + --accent-hover: #4338CA; + --accent-glow: rgba(79, 70, 229, 0.3); --accent-secondary: #d946ef; --error: #ef4444; diff --git a/frontend/admin/index.html b/frontend/admin/index.html index aebbf5d..6327494 100755 --- a/frontend/admin/index.html +++ b/frontend/admin/index.html @@ -49,7 +49,7 @@ Magistr - + + `).join(''); @@ -230,12 +232,19 @@ export async function initAcademicCalendar() { function renderSemesterList(items) { if (!items.length) return 'Нет семестров'; - return items.map(semester => ` -
- ${escapeHtml(semesterLabel(semester))} - -
- `).join(''); + return `
${items.map(semester => { + const title = semesterTypeTitle(semester); + const period = formatDisplayDateRange(semester.startDate, semester.endDate); + return ` +
+
+ ${escapeHtml(title)} + ${escapeHtml(period)} +
+ +
+ `; + }).join('')}
`; } async function saveAcademicYear(event) { @@ -282,7 +291,7 @@ export async function initAcademicCalendar() { if (editSemesterButton) { const semester = semesters.find(item => item.id == editSemesterButton.dataset.id); if (!semester) return; - semesterFormTitle.textContent = 'Редактирование семестра'; + semesterFormTitle.textContent = `Редактирование семестра: ${semesterTypeTitle(semester)}`; semesterIdInput.value = semester.id; semesterYearSelect.value = semester.academicYearId || ''; semesterTypeSelect.value = normalizeSemesterType(semester.semesterType); @@ -391,9 +400,9 @@ export async function initAcademicCalendar() { ${escapeHtml(calendar.studyFormName || '-')} ${escapeHtml(String(calendar.courseCount || '-'))} - - - + + + `).join(''); @@ -1164,7 +1173,13 @@ export async function initAcademicCalendar() { function semesterLabel(semester) { const type = normalizeSemesterType(semester.semesterType); - return `${semester.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type} (${semester.startDate} - ${semester.endDate})`; + return `${semester.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type} (${formatDisplayDateRange(semester.startDate, semester.endDate)})`; + } + + function semesterTypeTitle(semester) { + const type = normalizeSemesterType(semester.semesterType); + const label = SEMESTER_LABELS[type] || type || 'семестр'; + return label.charAt(0).toUpperCase() + label.slice(1); } function normalizeSemesterType(type) { @@ -1195,6 +1210,16 @@ export async function initAcademicCalendar() { return `${day}.${month}.${year}`; } + function formatDisplayDate(value) { + const [year, month, day] = String(value || '').split('-'); + if (!year || !month || !day) return String(value || ''); + return `${day}-${month}-${year}`; + } + + function formatDisplayDateRange(startDate, endDate) { + return `${formatDisplayDate(startDate)} — ${formatDisplayDate(endDate)}`; + } + function dateRangeCaption(rows) { if (!rows.length) return ''; const dates = rows.map(row => row.date).sort(); diff --git a/frontend/admin/js/views/classrooms.js b/frontend/admin/js/views/classrooms.js index aadbf07..681cf93 100755 --- a/frontend/admin/js/views/classrooms.js +++ b/frontend/admin/js/views/classrooms.js @@ -87,16 +87,16 @@ export async function initClassrooms() { ${c.status === 'ARCHIVED' ? 'Архив' : (c.isAvailable ? 'Доступна' : 'Не доступна')} - - + ${c.status === 'ARCHIVED' ? `` - : ``} + : ``} `; }).join(''); @@ -125,9 +125,9 @@ export async function initClassrooms() { }); classroomsTbody.addEventListener('click', async (e) => { - const btnDelete = e.target.closest('.btn-delete'); + const btnDelete = e.target.closest('.btn-archive-classroom'); const btnToggleStatus = e.target.closest('.btn-icon-toggle'); - const btnEdit = e.target.closest('.btn-edit-classroom'); + const btnEdit = e.target.closest('.classroom-edit-action'); if (btnDelete) { if (!confirm('Вывести аудиторию из эксплуатации? Историческое расписание сохранится.')) return; @@ -261,7 +261,7 @@ export async function initClassrooms() { ${eq.id} ${escapeHtml(eq.name)} - + `).join(''); } diff --git a/frontend/admin/js/views/department-workspace.js b/frontend/admin/js/views/department-workspace.js index f2e55b8..c4f2cbe 100644 --- a/frontend/admin/js/views/department-workspace.js +++ b/frontend/admin/js/views/department-workspace.js @@ -94,7 +94,7 @@ export async function initDepartmentWorkspace() { ${escapeHtml(subject.name || '-')} ${escapeHtml(subject.description || '-')} - + `).join(''); } diff --git a/frontend/admin/js/views/groups.js b/frontend/admin/js/views/groups.js index b73bdf9..8d93e0b 100644 --- a/frontend/admin/js/views/groups.js +++ b/frontend/admin/js/views/groups.js @@ -171,13 +171,15 @@ export async function initGroups() { ${escapeHtml(specialityLabel(group.specialtyId || group.specialityCode))} ${escapeHtml(group.specialtyProfileName || '-')} ${escapeHtml(statusLabel(group))} - - - - - ${group.status === 'ARCHIVED' - ? `` - : ``} + +
+ + + + ${group.status === 'ARCHIVED' + ? `` + : ``} +
`).join(''); @@ -345,7 +347,7 @@ export async function initGroups() { } async function handleGroupTableClick(event) { - const btnDelete = event.target.closest('.btn-delete'); + const btnDelete = event.target.closest('.btn-archive-group'); const btnEdit = event.target.closest('.btn-edit-group'); const btnRestore = event.target.closest('.btn-restore-group'); const btnSubgroups = event.target.closest('.btn-manage-subgroups'); @@ -427,8 +429,8 @@ export async function initGroups() { ${escapeHtml(sub.name)} ${escapeHtml(String(sub.studentCapacity ?? '-'))} чел. - - + + `).join(''); @@ -539,7 +541,7 @@ export async function initGroups() { ${escapeHtml(assignment.academicYearTitle)} ${escapeHtml(assignment.calendarTitle)} - + `).join(''); diff --git a/frontend/admin/js/views/schedule.js b/frontend/admin/js/views/schedule.js index 8cf935a..80e449c 100644 --- a/frontend/admin/js/views/schedule.js +++ b/frontend/admin/js/views/schedule.js @@ -247,8 +247,8 @@ export async function initSchedule() { ${renderRuleSlotsSummary(rule.slots)}
- - + +
@@ -857,7 +857,7 @@ export async function initSchedule() {
${isLaboratorySlot(slot) ? renderSubgroupField(slot, index) : ''}
- + `).join(''); } diff --git a/frontend/admin/js/views/subjects.js b/frontend/admin/js/views/subjects.js index 7d90266..aee5f5a 100755 --- a/frontend/admin/js/views/subjects.js +++ b/frontend/admin/js/views/subjects.js @@ -100,8 +100,8 @@ export async function initSubjects() { ${escapeHtml(departmentLabel(s.departmentId))}
${teachersHtml}
- - + + `; @@ -175,7 +175,7 @@ export async function initSubjects() { ${escapeHtml(ts.fullName || ts.username)} (${escapeHtml(ts.username)}) - + `).join(''); diff --git a/frontend/admin/js/views/university-structure.js b/frontend/admin/js/views/university-structure.js index bcce5c9..18e81b0 100644 --- a/frontend/admin/js/views/university-structure.js +++ b/frontend/admin/js/views/university-structure.js @@ -129,8 +129,8 @@ export async function initUniversityStructure() { ${escapeHtml(d.departmentName || d.name)} ${escapeHtml(String(d.departmentCode || d.code))} - - + + `).join(''); @@ -147,9 +147,9 @@ export async function initUniversityStructure() { ${escapeHtml(s.specialityName || s.name)} ${escapeHtml(s.specialityCode || s.specialtyCode || s.specialty_code)} - - - + + + `).join(''); @@ -338,8 +338,8 @@ export async function initUniversityStructure() { ${escapeHtml(p.name)} ${escapeHtml(p.description || '-')} - - + + `).join(''); @@ -453,8 +453,8 @@ export async function initUniversityStructure() { ${escapeHtml(p.name)} ${escapeHtml(p.description || '-')} - - + + `).join(''); diff --git a/frontend/admin/js/views/users.js b/frontend/admin/js/views/users.js index 5fc555d..4130bcf 100644 --- a/frontend/admin/js/views/users.js +++ b/frontend/admin/js/views/users.js @@ -50,7 +50,7 @@ export async function initUsers() { ${u.status === 'ARCHIVED' ? `` - : ``} + : ``} `).join(''); @@ -90,7 +90,7 @@ export async function initUsers() { }); usersTbody.addEventListener('click', async (e) => { - const deleteBtn = e.target.closest('.btn-delete'); + const deleteBtn = e.target.closest('.btn-archive-user'); if (!deleteBtn) return; if (!confirm('Архивировать пользователя? История расписания и связей сохранится.')) return; diff --git a/frontend/admin/settings/css/layout.css b/frontend/admin/settings/css/layout.css index 7261429..0fbb34c 100644 --- a/frontend/admin/settings/css/layout.css +++ b/frontend/admin/settings/css/layout.css @@ -25,15 +25,7 @@ } .sidebar-close-btn { - background: transparent; - border: none; color: var(--text-secondary); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - padding: 0.25rem; - border-radius: var(--radius-sm); transition: all var(--transition); } @@ -265,12 +257,7 @@ /* ===== Mobile Menu Toggle ===== */ .menu-toggle { display: none; - padding: 0.4rem; - background: none; - border: none; color: var(--text-primary); - cursor: pointer; - border-radius: var(--radius-sm); transition: background var(--transition); } diff --git a/frontend/admin/settings/css/main.css b/frontend/admin/settings/css/main.css index e23dd53..97607dc 100644 --- a/frontend/admin/settings/css/main.css +++ b/frontend/admin/settings/css/main.css @@ -46,9 +46,9 @@ --text-primary: #0f172a; --text-secondary: #475569; --text-placeholder: #94a3b8; - --accent: #6366f1; - --accent-hover: #4f46e5; - --accent-glow: rgba(99, 102, 241, 0.3); + --accent: #4F46E5; + --accent-hover: #4338CA; + --accent-glow: rgba(79, 70, 229, 0.3); --accent-secondary: #d946ef; --error: #ef4444; --success: #10b981; @@ -77,20 +77,19 @@ body { /* ===== Переключатель темы ===== */ .theme-toggle { - width: 42px; - height: 42px; - border: none; - border-radius: 50%; - background: var(--bg-card); - border: 1px solid var(--bg-card-border); - color: var(--text-primary); - cursor: pointer; - display: flex; + width: 40px; + height: 40px; + border-radius: var(--button-radius); + background: var(--button-secondary-bg); + border: 1px solid var(--button-secondary-border); + color: var(--button-secondary-text); + display: inline-flex; align-items: center; justify-content: center; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); - transition: background 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease; + padding: 0; + transition: background var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition), transform var(--transition); z-index: 100; flex-shrink: 0; } @@ -102,12 +101,18 @@ body { } .theme-toggle:hover { - transform: scale(1.1); - box-shadow: 0 4px 16px var(--accent-glow); + background: var(--button-secondary-bg-hover); + border-color: var(--button-secondary-border); + transform: translateY(-1px); } .theme-toggle:active { - transform: scale(0.95); + transform: translateY(0); +} + +.theme-toggle:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--button-focus-ring); } .theme-toggle--fixed { @@ -265,63 +270,6 @@ body { cursor: pointer; } -.btn-primary, -.btn-secondary, -.btn-delete { - min-height: 40px; - border: 1px solid transparent; - border-radius: var(--radius-sm); - padding: 0 0.9rem; - color: var(--text-primary); - font: inherit; - font-weight: 700; - cursor: pointer; - transition: transform var(--transition), background var(--transition), border-color var(--transition); - white-space: nowrap; -} - -.btn-primary { - background: var(--accent); -} - -.btn-primary:hover { - background: var(--accent-hover); - transform: translateY(-1px); -} - -.btn-secondary { - background: var(--bg-input); - border-color: var(--bg-card-border); -} - -.btn-secondary:hover { - background: var(--bg-hover); - transform: translateY(-1px); -} - -.btn-delete { - background: rgba(239, 68, 68, 0.13); - border-color: rgba(239, 68, 68, 0.25); - color: #fecaca; -} - -[data-theme="light"] .btn-delete { - color: #991b1b; -} - -.btn-delete:hover { - background: rgba(239, 68, 68, 0.2); - transform: translateY(-1px); -} - -.btn-primary:disabled, -.btn-secondary:disabled, -.btn-delete:disabled { - cursor: not-allowed; - opacity: 0.45; - transform: none; -} - .form-alert { grid-column: 1 / -1; min-height: 0; @@ -614,7 +562,7 @@ body { grid-template-columns: 1fr; } - .settings-form .btn-primary { + .settings-form .btn { width: fit-content; } @@ -630,9 +578,16 @@ body { flex-direction: column; } - .settings-header-row .btn-primary, - .card-header-row .btn-secondary, - .settings-form .btn-primary { + .settings-header-row .btn, + .card-header-row .btn, + .settings-form .btn { width: 100%; } } +.settings-modal-close.btn-icon-sm { + width: 32px !important; + height: 32px !important; + min-height: 32px !important; + padding: 0 !important; + border-radius: var(--button-radius) !important; +} diff --git a/frontend/admin/settings/index.html b/frontend/admin/settings/index.html index e516bb3..7766864 100644 --- a/frontend/admin/settings/index.html +++ b/frontend/admin/settings/index.html @@ -32,7 +32,7 @@ Настройки - + `; }).join(''); } @@ -136,7 +136,7 @@ export async function initDatabase() { // === Удаление тенанта === tenantsTbody.addEventListener('click', async (e) => { - const btn = e.target.closest('.btn-delete'); + const btn = e.target.closest('.btn-delete-tenant'); if (!btn) return; const domain = btn.dataset.domain; diff --git a/frontend/admin/settings/js/views/edu-forms.js b/frontend/admin/settings/js/views/edu-forms.js index c54fae0..752fe39 100644 --- a/frontend/admin/settings/js/views/edu-forms.js +++ b/frontend/admin/settings/js/views/edu-forms.js @@ -35,7 +35,7 @@ export async function initEduForms() { ${ef.id} ${escapeHtml(ef.name)} - + `).join(''); } @@ -56,7 +56,7 @@ export async function initEduForms() { }); efTbody.addEventListener('click', async (e) => { - const btn = e.target.closest('.btn-delete'); + const btn = e.target.closest('.btn-delete-edu-form'); if (!btn) return; if (!confirm('Удалить форму обучения?')) return; try { diff --git a/frontend/admin/settings/js/views/time-slots.js b/frontend/admin/settings/js/views/time-slots.js index 84932e3..3df9480 100644 --- a/frontend/admin/settings/js/views/time-slots.js +++ b/frontend/admin/settings/js/views/time-slots.js @@ -143,8 +143,8 @@ export async function initTimeSlotsSettings() { ${escapeHtml(trimTime(slot.endTime))} ${escapeHtml(String(slot.durationMinutes ?? '-'))} - - + + `).join(''); diff --git a/frontend/admin/settings/views/database.html b/frontend/admin/settings/views/database.html index 1702885..e7cca00 100644 --- a/frontend/admin/settings/views/database.html +++ b/frontend/admin/settings/views/database.html @@ -61,10 +61,10 @@ - - diff --git a/frontend/admin/settings/views/edu-forms.html b/frontend/admin/settings/views/edu-forms.html index e3c8951..9efdcfc 100644 --- a/frontend/admin/settings/views/edu-forms.html +++ b/frontend/admin/settings/views/edu-forms.html @@ -7,7 +7,7 @@ - + diff --git a/frontend/admin/settings/views/time-slots.html b/frontend/admin/settings/views/time-slots.html index a6800a1..e2735d3 100644 --- a/frontend/admin/settings/views/time-slots.html +++ b/frontend/admin/settings/views/time-slots.html @@ -4,7 +4,7 @@

Временные слоты

Настройка базовой, субботней и ручных сеток времени.

- +
@@ -12,8 +12,8 @@

Сетка времени

- - + +
@@ -47,7 +47,7 @@

Новый слот

- +
@@ -73,7 +73,7 @@ - +
@@ -106,7 +106,12 @@

Новая ручная сетка

Создайте отдельную сетку времени, которую затем можно назначать датам в календарном графике.

- +
@@ -116,8 +121,8 @@
- - + +
diff --git a/frontend/admin/views/academic-calendar.html b/frontend/admin/views/academic-calendar.html index daf0a10..8e47802 100644 --- a/frontend/admin/views/academic-calendar.html +++ b/frontend/admin/views/academic-calendar.html @@ -15,8 +15,8 @@ - - + +
@@ -25,7 +25,7 @@

Учебные годы

- +
@@ -72,8 +72,8 @@ - - + + @@ -82,7 +82,7 @@

Календарный учебный график

- +
@@ -119,7 +119,7 @@
- + @@ -128,7 +128,7 @@

Календарные графики

- +
@@ -161,8 +161,8 @@ - - + +
@@ -185,7 +185,7 @@
- +
@@ -229,8 +229,8 @@
- - + +
diff --git a/frontend/admin/views/auditorium-workload.html b/frontend/admin/views/auditorium-workload.html index 46b998a..496ff55 100644 --- a/frontend/admin/views/auditorium-workload.html +++ b/frontend/admin/views/auditorium-workload.html @@ -1,7 +1,7 @@

Загруженность

- +
diff --git a/frontend/admin/views/classrooms.html b/frontend/admin/views/classrooms.html index 61e72be..c93ed2c 100755 --- a/frontend/admin/views/classrooms.html +++ b/frontend/admin/views/classrooms.html @@ -2,7 +2,7 @@

Новая аудитория

- +
@@ -36,7 +36,7 @@
- +
@@ -103,7 +103,7 @@
- +
@@ -128,7 +128,7 @@ - + @@ -152,4 +152,4 @@
- \ No newline at end of file + diff --git a/frontend/admin/views/dashboard.html b/frontend/admin/views/dashboard.html index 25650d6..b3e7dd6 100644 --- a/frontend/admin/views/dashboard.html +++ b/frontend/admin/views/dashboard.html @@ -1,9 +1,9 @@ -
+
-
-
-
+
+
+
@@ -17,8 +17,8 @@
-
-
+
+
@@ -32,8 +32,8 @@
-
-
+
+
@@ -45,8 +45,8 @@
-
-
+
+
@@ -60,14 +60,14 @@
-
+
-
-
+
+

Мониторинг пар (Идет сейчас)

-
+
-
+
@@ -87,20 +87,20 @@ -
+
-
-

Свободные аудитории (ближайшие)

-
- - + +
@@ -219,7 +219,7 @@
- +
diff --git a/frontend/admin/views/schedule-view.html b/frontend/admin/views/schedule-view.html index 5526996..46b9744 100644 --- a/frontend/admin/views/schedule-view.html +++ b/frontend/admin/views/schedule-view.html @@ -5,7 +5,7 @@

Расписания

Период не выбран - +
@@ -87,9 +87,17 @@ 0 занятий
- + Нет выбранного расписания - +
diff --git a/frontend/admin/views/schedule.html b/frontend/admin/views/schedule.html index 56b114d..87b62b1 100644 --- a/frontend/admin/views/schedule.html +++ b/frontend/admin/views/schedule.html @@ -1,7 +1,7 @@

Новое правило расписания

- +
@@ -79,13 +79,13 @@

Слоты правила

- +
- +
@@ -94,7 +94,7 @@

Правила динамического расписания

- +
@@ -119,10 +119,15 @@ + aria-expanded="false"> + + @@ -133,7 +138,7 @@

Визуальное расписание групп

Правила ещё не загружены - +
@@ -153,8 +158,8 @@
Группы
- - + +
diff --git a/frontend/admin/views/subjects.html b/frontend/admin/views/subjects.html index a848305..113162a 100755 --- a/frontend/admin/views/subjects.html +++ b/frontend/admin/views/subjects.html @@ -17,7 +17,7 @@
- +
@@ -69,7 +69,7 @@
- + @@ -92,4 +92,4 @@
-
\ No newline at end of file +
diff --git a/frontend/admin/views/university-structure.html b/frontend/admin/views/university-structure.html index 53e4ea4..f88235f 100644 --- a/frontend/admin/views/university-structure.html +++ b/frontend/admin/views/university-structure.html @@ -20,7 +20,7 @@
- +
@@ -64,7 +64,7 @@
- +
@@ -115,8 +115,8 @@
- - + +
@@ -165,7 +165,7 @@
- +
@@ -188,7 +188,7 @@
- +
@@ -222,8 +222,8 @@
- - + +
diff --git a/frontend/admin/views/users.html b/frontend/admin/views/users.html index b3e093c..b084dd3 100755 --- a/frontend/admin/views/users.html +++ b/frontend/admin/views/users.html @@ -34,7 +34,7 @@
- +
diff --git a/frontend/index.html b/frontend/index.html index 68e1607..3799de7 100755 --- a/frontend/index.html +++ b/frontend/index.html @@ -57,7 +57,7 @@ -
- - Выйти + + Выйти
@@ -279,9 +325,17 @@
- - - + + +
diff --git a/frontend/style.css b/frontend/style.css index 2ea3645..4e7736f 100755 --- a/frontend/style.css +++ b/frontend/style.css @@ -25,6 +25,24 @@ --accent-hover: #a78bfa; --accent-glow: rgba(139, 92, 246, 0.4); --accent-secondary: #ec4899; + --button-primary-bg: #8B5CF6; + --button-primary-bg-hover: #7C3AED; + --button-primary-text: #FFFFFF; + --button-secondary-bg: rgba(255, 255, 255, 0.04); + --button-secondary-bg-hover: rgba(255, 255, 255, 0.08); + --button-secondary-border: rgba(255, 255, 255, 0.10); + --button-secondary-text: #F8FAFC; + --button-ghost-bg-hover: rgba(255, 255, 255, 0.06); + --button-danger-bg: #DC2626; + --button-danger-bg-hover: #B91C1C; + --button-danger-subtle-bg: rgba(239, 68, 68, 0.13); + --button-danger-subtle-bg-hover: rgba(239, 68, 68, 0.20); + --button-danger-subtle-border: rgba(239, 68, 68, 0.25); + --button-danger-subtle-text: #FECACA; + --button-focus-ring: rgba(139, 92, 246, 0.45); + --button-disabled-bg: rgba(148, 163, 184, 0.16); + --button-disabled-text: #94A3B8; + --button-radius: 8px; /* Status Colors */ --error: #ef4444; @@ -48,10 +66,27 @@ --text-primary: #0f172a; --text-secondary: #475569; --text-placeholder: #94a3b8; - --accent: #6366f1; - --accent-hover: #4f46e5; - --accent-glow: rgba(99, 102, 241, 0.3); + --accent: #4F46E5; + --accent-hover: #4338CA; + --accent-glow: rgba(79, 70, 229, 0.3); --accent-secondary: #d946ef; + --button-primary-bg: #4F46E5; + --button-primary-bg-hover: #4338CA; + --button-primary-text: #FFFFFF; + --button-secondary-bg: #FFFFFF; + --button-secondary-bg-hover: #F1F5F9; + --button-secondary-border: #CBD5E1; + --button-secondary-text: #334155; + --button-ghost-bg-hover: #F8FAFC; + --button-danger-bg: #DC2626; + --button-danger-bg-hover: #B91C1C; + --button-danger-subtle-bg: #FEF2F2; + --button-danger-subtle-bg-hover: #FEE2E2; + --button-danger-subtle-border: #FECACA; + --button-danger-subtle-text: #B91C1C; + --button-focus-ring: rgba(79, 70, 229, 0.35); + --button-disabled-bg: #CBD5E1; + --button-disabled-text: #64748B; --error: #ef4444; --success: #10b981; --warning: #f59e0b; @@ -389,59 +424,19 @@ body { /* ===== Submit Button ===== */ .btn-submit { - position: relative; - overflow: hidden; width: 100%; - padding: 0.95rem; - border: none; - border-radius: var(--radius-sm); - background: linear-gradient(135deg, var(--accent), var(--accent-secondary)); - color: #fff; - font-family: inherit; - font-size: 1rem; - font-weight: 600; - letter-spacing: 0.02em; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; gap: 0.5rem; - transition: - transform var(--transition), - box-shadow var(--transition), - opacity var(--transition); - box-shadow: 0 4px 15px var(--accent-glow); -} - -.btn-submit::before { - content: ''; - position: absolute; - top: 0; left: 0; right: 0; bottom: 0; - background: linear-gradient(rgba(255,255,255,0.2), transparent); - border-radius: inherit; - opacity: 0; - transition: opacity var(--transition); -} - -.btn-submit:hover { - transform: translateY(-2px); - box-shadow: 0 8px 25px var(--accent-glow); -} - -.btn-submit:hover::before { - opacity: 1; } .btn-submit:active { - transform: translateY(1px); - box-shadow: 0 2px 10px var(--accent-glow); + transform: translateY(0); } .btn-submit:disabled { - opacity: 0.6; + background: var(--button-disabled-bg); + color: var(--button-disabled-text); cursor: not-allowed; transform: none; - box-shadow: none; } /* ===== Ripple Effect ===== */ @@ -480,13 +475,12 @@ body { /* ===== Theme Toggle Button ===== */ .theme-toggle { - width: 42px; - height: 42px; - border: none; - border-radius: 50%; - background: var(--bg-card); - border: 1px solid var(--bg-card-border); - color: var(--text-primary); + width: 40px; + height: 40px; + border-radius: var(--button-radius); + background: var(--button-secondary-bg); + border: 1px solid var(--button-secondary-border); + color: var(--button-secondary-text); cursor: pointer; display: flex; align-items: center; @@ -505,12 +499,13 @@ body { } .theme-toggle:hover { - transform: scale(1.1); - box-shadow: 0 4px 16px var(--accent-glow); + background: var(--button-secondary-bg-hover); + transform: translateY(-1px); + box-shadow: none; } .theme-toggle:active { - transform: scale(0.95); + transform: translateY(0); } .theme-toggle--fixed { @@ -540,4 +535,95 @@ body { width: 18px; height: 18px; } -} \ No newline at end of file +} +.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; +} diff --git a/frontend/teacher/index.html b/frontend/teacher/index.html index 65037b0..846f4c1 100644 --- a/frontend/teacher/index.html +++ b/frontend/teacher/index.html @@ -15,8 +15,8 @@ --panel-border: #d8dee8; --text: #172033; --muted: #67758d; - --accent: #1d4ed8; - --accent-strong: #1e40af; + --accent: #4F46E5; + --accent-strong: #4338CA; --green: #047857; --orange: #b45309; --shadow: 0 12px 30px rgba(23, 32, 51, 0.08); @@ -28,8 +28,8 @@ --panel-border: #2a374b; --text: #eef4fb; --muted: #a2b0c5; - --accent: #60a5fa; - --accent-strong: #93c5fd; + --accent: #8B5CF6; + --accent-strong: #7C3AED; --green: #34d399; --orange: #f59e0b; --shadow: 0 18px 42px rgba(0, 0, 0, 0.28); @@ -107,6 +107,7 @@ padding: 0 14px; cursor: pointer; text-decoration: none; + transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease; } .btn:hover { @@ -125,6 +126,47 @@ color: #fff; } + .btn-secondary { + background: var(--panel); + border-color: var(--panel-border); + color: var(--text); + } + + .btn-secondary:hover { + background: color-mix(in srgb, var(--accent) 7%, var(--panel)); + } + + .btn-danger-subtle { + background: #FEF2F2; + border-color: #FECACA; + color: #B91C1C; + } + + [data-theme="dark"] .btn-danger-subtle { + background: rgba(239, 68, 68, 0.13); + border-color: rgba(239, 68, 68, 0.25); + color: #FECACA; + } + + .btn-icon-md { + width: 40px; + height: 40px; + padding: 0; + border-radius: 8px; + } + + .btn-icon-md svg { + width: 20px; + height: 20px; + } + + .btn-md { + min-height: 40px; + padding: 0 16px; + font-size: 14px; + font-weight: 600; + } + .date-input { padding: 0 12px; } @@ -275,16 +317,28 @@ Загрузка периода...
- - Выйти + + Выйти
- - - + + +
diff --git a/frontend/theme-toggle.js b/frontend/theme-toggle.js index 2931635..4a389d4 100755 --- a/frontend/theme-toggle.js +++ b/frontend/theme-toggle.js @@ -23,7 +23,7 @@ const isDark = () => document.documentElement.getAttribute('data-theme') !== 'light'; const btn = document.createElement('button'); - btn.className = 'theme-toggle'; + btn.className = 'btn btn-icon-md btn-secondary theme-toggle'; btn.setAttribute('aria-label', 'Переключить тему'); btn.innerHTML = isDark() ? moonSVG : sunSVG;