Поправил шрифты и некоторые визуальные косяки

This commit is contained in:
dipatrik10
2026-09-27 17:47:36 +03:00
parent e3b6797713
commit c6083b35e0
37 changed files with 644 additions and 63 deletions

View File

@@ -463,6 +463,17 @@ if (!session || !AUTHORIZED_ROLES.includes(session.role)) {
### Темизация ### Темизация
Общий `ui-foundation.css` подключается последним во всех пяти оболочках.
Текст, заголовки и элементы управления используют прежний стек `Inter`,
`-apple-system`, `BlinkMacSystemFont`, `Segoe UI`, `sans-serif` без внешнего CDN.
Если Inter не установлен, браузер выбирает системный шрифт без засечек.
Базовые стили содержат запасной стек на случай недоступности общего файла.
Список свободных аудиторий на дашборде имеет высоту 180 px и внутреннюю
вертикальную прокрутку, доступную с клавиатуры. В запросах преподавателей
статус, комментарий и кнопки размещаются под содержимым заявки; кнопки
переносятся целиком при нехватке ширины, статусы не растягиваются по высоте.
CSS-переменные позволяют поддерживать светлую/тёмную тему: CSS-переменные позволяют поддерживать светлую/тёмную тему:
```css ```css

View File

@@ -107,7 +107,9 @@ COPY proxy.conf /usr/local/apache2/conf/extra/magistr-proxy.conf
Все базовые образы backend/frontend и локальный PostgreSQL зафиксированы одновременно точным Все базовые образы backend/frontend и локальный PostgreSQL зафиксированы одновременно точным
tag и manifest digest. Первый этап frontend собирает зафиксированный tag и manifest digest. Первый этап frontend собирает зафиксированный
OpenTelemetry bundle; второй раздаёт только runtime- OpenTelemetry bundle; второй раздаёт только runtime-
файлы, без `node_modules`, тестов и build-исходников. Apache подключает `mod_headers`, файлы, без `node_modules`, тестов и build-исходников. В корень образа также копируются
`ui-foundation.css`, `schedule-overview.css` и `schedule-overview.js`: они необходимы
для общего шрифта, компонентов и обзора занятий в личных кабинетах. Apache подключает `mod_headers`,
`mod_proxy` и `mod_proxy_http`; proxy сохраняет исходный `Host`, чтобы `localhost` корректно `mod_proxy` и `mod_proxy_http`; proxy сохраняет исходный `Host`, чтобы `localhost` корректно
маршрутизировался в tenant `default`. Перед копированием runtime-файлов устанавливаются маршрутизировался в tenant `default`. Перед копированием runtime-файлов устанавливаются
доступные исправления Alpine через `apk upgrade --no-cache`. Сервер доступные исправления Alpine через `apk upgrade --no-cache`. Сервер

View File

@@ -18,6 +18,7 @@ COPY edu-office/ /usr/local/apache2/htdocs/edu-office/
COPY student/ /usr/local/apache2/htdocs/student/ COPY student/ /usr/local/apache2/htdocs/student/
COPY teacher/ /usr/local/apache2/htdocs/teacher/ COPY teacher/ /usr/local/apache2/htdocs/teacher/
COPY index.html script.js style.css theme-toggle.js auth-session.js telemetry.js /usr/local/apache2/htdocs/ COPY index.html script.js style.css theme-toggle.js auth-session.js telemetry.js /usr/local/apache2/htdocs/
COPY ui-foundation.css schedule-overview.css schedule-overview.js /usr/local/apache2/htdocs/
COPY --from=frontend-assets /build/dist/vendor/ /usr/local/apache2/htdocs/vendor/ COPY --from=frontend-assets /build/dist/vendor/ /usr/local/apache2/htdocs/vendor/
COPY security.conf /usr/local/apache2/conf/extra/magistr-security.conf COPY security.conf /usr/local/apache2/conf/extra/magistr-security.conf
COPY proxy.conf /usr/local/apache2/conf/extra/magistr-proxy.conf COPY proxy.conf /usr/local/apache2/conf/extra/magistr-proxy.conf

View File

@@ -219,6 +219,13 @@
align-content: flex-start; align-content: flex-start;
gap: 0.5rem; gap: 0.5rem;
margin-top: 0.25rem; margin-top: 0.25rem;
height: 180px;
min-height: 180px;
max-height: 180px;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
padding-right: 0.25rem;
} }
.dashboard-actions { .dashboard-actions {

View File

@@ -67,7 +67,7 @@ html {
} }
body { body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family: var(--ui-font-sans, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
background: var(--bg-primary); background: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
min-height: 100vh; min-height: 100vh;

View File

@@ -66,7 +66,7 @@
.quality-hero h2 { .quality-hero h2 {
max-width: 790px; max-width: 790px;
margin: 0; margin: 0;
font-family: Georgia, 'Times New Roman', serif; font-family: var(--ui-font-display);
font-size: clamp(2rem, 4.5vw, 4.4rem); font-size: clamp(2rem, 4.5vw, 4.4rem);
font-weight: 500; font-weight: 500;
letter-spacing: -0.045em; letter-spacing: -0.045em;
@@ -229,7 +229,7 @@
.quality-score-inner strong { .quality-score-inner strong {
color: var(--text-primary); color: var(--text-primary);
font-family: Georgia, 'Times New Roman', serif; font-family: var(--ui-font-display);
font-size: 3.2rem; font-size: 3.2rem;
font-weight: 500; font-weight: 500;
line-height: 0.95; line-height: 0.95;
@@ -271,7 +271,7 @@
.quality-score-copy h3 { .quality-score-copy h3 {
margin: 0.55rem 0 0; margin: 0.55rem 0 0;
color: var(--text-primary); color: var(--text-primary);
font-family: Georgia, 'Times New Roman', serif; font-family: var(--ui-font-display);
font-size: clamp(1.6rem, 3vw, 2.5rem); font-size: clamp(1.6rem, 3vw, 2.5rem);
font-weight: 500; font-weight: 500;
} }
@@ -338,7 +338,7 @@
.quality-metric-index { .quality-metric-index {
color: color-mix(in srgb, var(--text-secondary) 65%, transparent); color: color-mix(in srgb, var(--text-secondary) 65%, transparent);
font-family: Georgia, 'Times New Roman', serif; font-family: var(--ui-font-display);
font-size: 0.78rem; font-size: 0.78rem;
} }
@@ -346,7 +346,7 @@
display: block; display: block;
margin-top: 1.25rem; margin-top: 1.25rem;
color: var(--text-primary); color: var(--text-primary);
font-family: Georgia, 'Times New Roman', serif; font-family: var(--ui-font-display);
font-size: 2rem; font-size: 2rem;
font-weight: 500; font-weight: 500;
} }
@@ -402,7 +402,7 @@
.quality-recommendations h3 { .quality-recommendations h3 {
margin: 0; margin: 0;
color: var(--text-primary); color: var(--text-primary);
font-family: Georgia, 'Times New Roman', serif; font-family: var(--ui-font-display);
font-size: 1.65rem; font-size: 1.65rem;
font-weight: 500; font-weight: 500;
} }
@@ -537,7 +537,7 @@
.quality-penalty { .quality-penalty {
color: var(--quality-red); color: var(--quality-red);
font-family: Georgia, 'Times New Roman', serif; font-family: var(--ui-font-display);
font-size: 1.15rem; font-size: 1.15rem;
} }
@@ -583,7 +583,7 @@
background: color-mix(in srgb, var(--quality-cyan) 10%, transparent); background: color-mix(in srgb, var(--quality-cyan) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--quality-cyan) 35%, transparent); border: 1px solid color-mix(in srgb, var(--quality-cyan) 35%, transparent);
border-radius: 50%; border-radius: 50%;
font-family: Georgia, 'Times New Roman', serif; font-family: var(--ui-font-display);
font-size: 1.6rem; font-size: 1.6rem;
} }

View File

@@ -30,7 +30,7 @@
right: -1.5rem; right: -1.5rem;
bottom: 1.2rem; bottom: 1.2rem;
color: rgba(255, 255, 255, .055); color: rgba(255, 255, 255, .055);
font-family: Georgia, serif; font-family: var(--ui-font-display);
font-size: clamp(4rem, 10vw, 9rem); font-size: clamp(4rem, 10vw, 9rem);
letter-spacing: -.08em; letter-spacing: -.08em;
pointer-events: none; pointer-events: none;
@@ -53,7 +53,7 @@
.version-hero h2 { .version-hero h2 {
max-width: 760px; max-width: 760px;
margin: 0; margin: 0;
font-family: Georgia, 'Times New Roman', serif; font-family: var(--ui-font-display);
font-size: clamp(2rem, 4.5vw, 4.5rem); font-size: clamp(2rem, 4.5vw, 4.5rem);
font-weight: 500; font-weight: 500;
letter-spacing: -.05em; letter-spacing: -.05em;
@@ -136,7 +136,7 @@
background: linear-gradient(105deg, color-mix(in srgb, var(--version-copper) 10%, var(--bg-card)), var(--bg-card)); background: linear-gradient(105deg, color-mix(in srgb, var(--version-copper) 10%, var(--bg-card)), var(--bg-card));
border-color: color-mix(in srgb, var(--version-copper) 35%, var(--bg-card-border)); border-color: color-mix(in srgb, var(--version-copper) 35%, var(--bg-card-border));
} }
.version-constructor-bar h2 { margin: 0; color: var(--text-primary); font-family: Georgia, serif; font-size: 1.55rem; } .version-constructor-bar h2 { margin: 0; color: var(--text-primary); font-family: var(--ui-font-display); font-size: 1.55rem; }
.version-constructor-bar p { margin: .35rem 0 0; color: var(--text-secondary); font-size: .82rem; } .version-constructor-bar p { margin: .35rem 0 0; color: var(--text-secondary); font-size: .82rem; }
.version-constructor-controls { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .65rem; align-items: end; } .version-constructor-controls { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .65rem; align-items: end; }
.version-constructor-field { min-width: 0; } .version-constructor-field { min-width: 0; }
@@ -219,7 +219,7 @@
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.version-section-head > span { color: var(--version-copper); font-family: Georgia, serif; font-size: 2rem; } .version-section-head > span { color: var(--version-copper); font-family: var(--ui-font-display); font-size: 2rem; }
.version-draft-list { display: grid; gap: .8rem; } .version-draft-list { display: grid; gap: .8rem; }
.version-card { .version-card {
@@ -237,7 +237,7 @@
right: .5rem; right: .5rem;
top: -.8rem; top: -.8rem;
color: color-mix(in srgb, var(--version-copper) 12%, transparent); color: color-mix(in srgb, var(--version-copper) 12%, transparent);
font-family: Georgia, serif; font-family: var(--ui-font-display);
font-size: 5rem; font-size: 5rem;
} }
@@ -252,7 +252,7 @@
place-content: center; place-content: center;
text-align: center; text-align: center;
} }
.version-empty > span { color: var(--version-copper); font-family: Georgia, serif; font-size: 4rem; } .version-empty > span { color: var(--version-copper); font-family: var(--ui-font-display); font-size: 4rem; }
.version-empty p { max-width: 360px; } .version-empty p { max-width: 360px; }
.version-diff-score { .version-diff-score {
@@ -262,7 +262,7 @@
margin: 1rem 0; margin: 1rem 0;
} }
.version-diff-score div { padding: .75rem; background: var(--bg-input); border-radius: var(--radius-sm); text-align: center; } .version-diff-score div { padding: .75rem; background: var(--bg-input); border-radius: var(--radius-sm); text-align: center; }
.version-diff-score strong { display: block; color: var(--text-primary); font-family: Georgia, serif; font-size: 1.75rem; } .version-diff-score strong { display: block; color: var(--text-primary); font-family: var(--ui-font-display); font-size: 1.75rem; }
.version-diff-score span { color: var(--text-secondary); font-size: .68rem; text-transform: uppercase; } .version-diff-score span { color: var(--text-secondary); font-size: .68rem; text-transform: uppercase; }
.version-change-list { display: grid; gap: .55rem; max-height: 390px; overflow: auto; } .version-change-list { display: grid; gap: .55rem; max-height: 390px; overflow: auto; }
@@ -292,7 +292,7 @@
} }
.version-modal[hidden] { display: none; } .version-modal[hidden] { display: none; }
.version-modal-card { position: relative; width: min(560px, 100%); padding: 1.6rem; background: var(--bg-primary); border: 1px solid var(--bg-card-border); border-radius: var(--radius-lg, 20px); box-shadow: 0 30px 80px rgba(0, 0, 0, .45); } .version-modal-card { position: relative; width: min(560px, 100%); padding: 1.6rem; background: var(--bg-primary); border: 1px solid var(--bg-card-border); border-radius: var(--radius-lg, 20px); box-shadow: 0 30px 80px rgba(0, 0, 0, .45); }
.version-modal-card h3 { margin: 0; color: var(--text-primary); font-family: Georgia, serif; font-size: 2rem; } .version-modal-card h3 { margin: 0; color: var(--text-primary); font-family: var(--ui-font-display); font-size: 2rem; }
.version-modal-card > p { margin: .6rem 0 1rem; color: var(--text-secondary); line-height: 1.55; } .version-modal-card > p { margin: .6rem 0 1rem; color: var(--text-secondary); line-height: 1.55; }
.version-modal-card form { display: grid; gap: .65rem; } .version-modal-card form { display: grid; gap: .65rem; }
.version-modal-card label { color: var(--text-secondary); font-size: .75rem; font-weight: 700; } .version-modal-card label { color: var(--text-secondary); font-size: .75rem; font-weight: 700; }

View File

@@ -187,6 +187,7 @@
} }
.absence-registry-title { .absence-registry-title {
flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: 1rem;
} }
@@ -220,21 +221,31 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
min-height: 28px; min-height: 28px;
padding: 0 0.65rem; padding: 0.25rem 0.65rem;
flex: 0 0 auto;
align-self: flex-start;
max-width: 100%;
line-height: 1.4;
border: 1px solid currentColor; border: 1px solid currentColor;
border-radius: 999px; border-radius: 999px;
font-size: 0.69rem; font-size: 0.69rem;
font-weight: 800; font-weight: 800;
letter-spacing: 0.03em; letter-spacing: 0.03em;
white-space: nowrap; white-space: normal;
} }
.absence-status.is-pending { color: #f59e0b; } .absence-status.is-pending { color: #f59e0b; }
.absence-status.is-approved { color: #fb7185; } .absence-status.is-approved { color: #34d399; }
.absence-status.is-resolved { color: #34d399; } .absence-status.is-resolved { color: #34d399; }
.absence-status.is-rejected, .absence-status.is-rejected,
.absence-status.is-cancelled { color: var(--text-secondary); } .absence-status.is-cancelled { color: var(--text-secondary); }
[data-theme="light"] .absence-status.is-pending { color: #92400e; }
[data-theme="light"] .absence-status.is-approved,
[data-theme="light"] .absence-status.is-resolved { color: #047857; }
.absence-status.is-rejected { color: #fb7185; }
[data-theme="light"] .absence-status.is-rejected { color: #be123c; }
.absence-registry-buttons { .absence-registry-buttons {
justify-content: flex-end; justify-content: flex-end;
flex-wrap: wrap; flex-wrap: wrap;
@@ -481,7 +492,7 @@ body.absence-wizard-open {
.teacher-dialogue-grid { .teacher-dialogue-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem; gap: 1rem;
} }
@@ -604,7 +615,7 @@ body.absence-wizard-open {
.teacher-dialogue-item { .teacher-dialogue-item {
display: grid; display: grid;
grid-template-columns: 7px minmax(0, 1fr) minmax(180px, .42fr); grid-template-columns: 7px minmax(0, 1fr);
gap: .85rem; gap: .85rem;
padding: .85rem; padding: .85rem;
border: 1px solid var(--bg-card-border); border: 1px solid var(--bg-card-border);
@@ -613,6 +624,7 @@ body.absence-wizard-open {
} }
.teacher-dialogue-mark { .teacher-dialogue-mark {
grid-row: 1 / 3;
border-radius: 99px; border-radius: 99px;
background: #94a3b8; background: #94a3b8;
} }
@@ -631,6 +643,11 @@ body.absence-wizard-open {
font-size: .68rem; font-size: .68rem;
} }
.teacher-dialogue-copy {
min-width: 0;
overflow-wrap: anywhere;
}
.teacher-dialogue-copy h3 { .teacher-dialogue-copy h3 {
margin: .22rem 0; margin: .22rem 0;
font-size: .92rem; font-size: .92rem;
@@ -668,17 +685,32 @@ body.absence-wizard-open {
.teacher-dialogue-copy ol { display: grid; gap: .3rem; padding-left: 1.15rem; } .teacher-dialogue-copy ol { display: grid; gap: .3rem; padding-left: 1.15rem; }
.teacher-dialogue-action { .teacher-dialogue-action {
grid-column: 2;
min-width: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: stretch;
gap: .55rem; gap: .55rem;
} }
.teacher-dialogue-action > div { .teacher-dialogue-action > div {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-start;
flex-wrap: wrap; flex-wrap: wrap;
gap: .35rem; gap: .5rem;
}
.teacher-dialogue-action > div > .btn {
flex: 1 1 120px;
min-width: 0;
white-space: normal;
line-height: 1.35;
padding-block: 0.4rem;
}
.teacher-dialogue-toolbar > .btn {
align-self: center;
min-height: 44px;
} }
.teacher-dialogue-comment { .teacher-dialogue-comment {
@@ -801,6 +833,6 @@ body.absence-wizard-open {
.teacher-dialogue-action { .teacher-dialogue-action {
grid-column: 2; grid-column: 2;
align-items: flex-start; align-items: stretch;
} }
} }

View File

@@ -15,6 +15,7 @@
<link rel="stylesheet" href="css/teacher-absences.css"> <link rel="stylesheet" href="css/teacher-absences.css">
<link rel="stylesheet" href="css/schedule-quality.css"> <link rel="stylesheet" href="css/schedule-quality.css">
<link rel="stylesheet" href="css/schedule-versions.css"> <link rel="stylesheet" href="css/schedule-versions.css">
<link rel="stylesheet" href="../ui-foundation.css">
</head> </head>
<body> <body>
@@ -78,7 +79,7 @@
<!-- Раздел: Расписание --> <!-- Раздел: Расписание -->
<div class="sidebar-section-divider"></div> <div class="sidebar-section-divider"></div>
<div class="sidebar-section-title" style="font-size: 0.7rem; font-weight: 700; color: rgba(255,255,255,0.4); padding: 0.5rem 1.25rem 0.25rem; text-transform: uppercase; letter-spacing: 0.05em;">Расписание</div> <div class="sidebar-section-title">Расписание</div>
<a href="#" class="nav-item" data-tab="schedule"> <a href="#" class="nav-item" data-tab="schedule">
<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">
@@ -149,7 +150,7 @@
<!-- Раздел: Субъекты и ресурсы --> <!-- Раздел: Субъекты и ресурсы -->
<div class="sidebar-section-divider"></div> <div class="sidebar-section-divider"></div>
<div class="sidebar-section-title" style="font-size: 0.7rem; font-weight: 700; color: rgba(255,255,255,0.4); padding: 0.5rem 1.25rem 0.25rem; text-transform: uppercase; letter-spacing: 0.05em;">Ресурсы</div> <div class="sidebar-section-title">Ресурсы</div>
<a href="#" class="nav-item" data-tab="classrooms"> <a href="#" class="nav-item" data-tab="classrooms">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
@@ -184,7 +185,7 @@
<!-- Раздел: Система --> <!-- Раздел: Система -->
<div class="sidebar-section-divider"></div> <div class="sidebar-section-divider"></div>
<div class="sidebar-section-title" style="font-size: 0.7rem; font-weight: 700; color: rgba(255,255,255,0.4); padding: 0.5rem 1.25rem 0.25rem; text-transform: uppercase; letter-spacing: 0.05em;">Система</div> <div class="sidebar-section-title">Система</div>
<a href="#" class="nav-item" data-tab="users"> <a href="#" class="nav-item" data-tab="users">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"

View File

@@ -935,7 +935,7 @@ export async function initAcademicCalendar() {
<span class="calendar-subject-chip"> <span class="calendar-subject-chip">
<span>${escapeHtml(subjectRowLabel(row))}</span> <span>${escapeHtml(subjectRowLabel(row))}</span>
<button type="button" <button type="button"
class="calendar-subject-remove" class="btn btn-icon-sm btn-danger-subtle calendar-subject-remove"
data-semester="${escapeHtml(String(row.semesterNumber))}" data-semester="${escapeHtml(String(row.semesterNumber))}"
data-subject="${escapeHtml(String(row.subjectId))}" data-subject="${escapeHtml(String(row.subjectId))}"
aria-label="Убрать ${escapeHtml(subjectRowLabel(row))}">×</button> aria-label="Убрать ${escapeHtml(subjectRowLabel(row))}">×</button>

View File

@@ -94,11 +94,13 @@ export async function initClassrooms() {
</button> </button>
</div> </div>
</td> </td>
<td style="text-align: right;"> <td class="table-actions-cell">
<div class="table-actions">
<button class="btn btn-sm btn-secondary classroom-edit-action" 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 btn-sm btn-secondary btn-restore-classroom" data-id="${c.id}">Восстановить</button>`
: `<button class="btn btn-sm btn-danger-subtle btn-archive-classroom" 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}">Архивировать</button>`}
</div>
</td> </td>
</tr>`; </tr>`;
}).join(''); }).join('');

View File

@@ -333,7 +333,7 @@ export async function initGroups() {
<button class="btn btn-sm btn-secondary btn-manage-subgroups group-action-btn" data-id="${group.id}">Подгруппы</button> <button class="btn btn-sm btn-secondary btn-manage-subgroups group-action-btn" data-id="${group.id}">Подгруппы</button>
<button class="btn btn-sm btn-secondary btn-manage-calendar group-action-btn" data-id="${group.id}">Календарь</button> <button class="btn btn-sm btn-secondary btn-manage-calendar group-action-btn" data-id="${group.id}">Календарь</button>
${group.status === 'ARCHIVED' ${group.status === 'ARCHIVED'
? `<button class="btn-restore-group group-action-btn" data-id="${group.id}">Восстановить</button>` ? `<button class="btn btn-sm btn-secondary 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>`} : `<button class="btn btn-sm btn-danger-subtle btn-archive-group group-action-btn" data-id="${group.id}">Архив</button>`}
</div> </div>
</td> </td>

View File

@@ -865,7 +865,7 @@ export async function initScheduleView() {
const editButton = canEditScheduleOverrides(role) && lesson.scheduleRuleSlotId && sourceDate const editButton = canEditScheduleOverrides(role) && lesson.scheduleRuleSlotId && sourceDate
? ` ? `
<button type="button" <button type="button"
class="schedule-override-edit" class="btn btn-sm btn-secondary schedule-override-edit"
data-schedule-override-edit data-schedule-override-edit
data-base-rule-slot-id="${escapeHtml(lesson.scheduleRuleSlotId)}" data-base-rule-slot-id="${escapeHtml(lesson.scheduleRuleSlotId)}"
data-lesson-date="${escapeHtml(sourceDate)}"> data-lesson-date="${escapeHtml(sourceDate)}">

View File

@@ -990,7 +990,7 @@ export async function initSchedule() {
${subgroupText ? `<small>${escapeHtml(subgroupText)}</small>` : ''} ${subgroupText ? `<small>${escapeHtml(subgroupText)}</small>` : ''}
${meta ? `<small>${escapeHtml(meta)}</small>` : ''} ${meta ? `<small>${escapeHtml(meta)}</small>` : ''}
<button type="button" <button type="button"
class="schedule-visual-edit-rule" class="btn btn-icon-sm btn-ghost schedule-visual-edit-rule"
data-rule-id="${escapeHtml(String(lesson.ruleId))}" data-rule-id="${escapeHtml(String(lesson.ruleId))}"
data-slot-id="${escapeHtml(String(lesson.slotId || ''))}" data-slot-id="${escapeHtml(String(lesson.slotId || ''))}"
data-slot-index="${escapeHtml(String(lesson.slotIndex ?? ''))}" data-slot-index="${escapeHtml(String(lesson.slotIndex ?? ''))}"

View File

@@ -86,7 +86,7 @@ export async function initUsers() {
<td><span class="badge ${ROLE_BADGE[u.role] || ''}">${ROLE_LABELS[u.role] || escapeHtml(u.role)}</span></td> <td><span class="badge ${ROLE_BADGE[u.role] || ''}">${ROLE_LABELS[u.role] || escapeHtml(u.role)}</span></td>
<td> <td>
${u.status === 'ARCHIVED' ${u.status === 'ARCHIVED'
? `<button class="btn-restore-user" data-id="${u.id}">Восстановить</button>` ? `<button class="btn btn-sm btn-secondary btn-restore-user" data-id="${u.id}">Восстановить</button>`
: `<button class="btn btn-sm btn-danger-subtle btn-archive-user" 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>

View File

@@ -61,7 +61,7 @@ html {
} }
body { body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family: var(--ui-font-sans, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
background: var(--bg-primary); background: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
min-height: 100vh; min-height: 100vh;

View File

@@ -8,6 +8,7 @@
<link rel="stylesheet" href="../css/components.css"> <link rel="stylesheet" href="../css/components.css">
<link rel="stylesheet" href="css/main.css"> <link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/layout.css"> <link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="../../ui-foundation.css">
</head> </head>
<body> <body>

View File

@@ -108,7 +108,7 @@
<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="btn btn-icon-sm btn-secondary settings-modal-close" id="time-slot-scope-modal-close" aria-label="Закрыть"> <button type="button" class="btn btn-icon-md btn-ghost 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"> <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="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line>

View File

@@ -273,7 +273,7 @@
<div class="modal-overlay" id="calendar-day-dialog"> <div class="modal-overlay" id="calendar-day-dialog">
<div class="modal-content card calendar-day-dialog-card"> <div class="modal-content card calendar-day-dialog-card">
<h2>Ячейка графика</h2> <h2>Ячейка графика</h2>
<button class="modal-close" id="calendar-day-dialog-close">&times;</button> <button type="button" class="btn btn-icon-md btn-ghost modal-close" id="calendar-day-dialog-close" aria-label="Закрыть окно">&times;</button>
<div class="calendar-day-dialog-grid"> <div class="calendar-day-dialog-grid">
<div> <div>
<span>Дата</span> <span>Дата</span>

View File

@@ -70,7 +70,7 @@
<div class="modal-overlay" id="modal-edit-classroom"> <div class="modal-overlay" id="modal-edit-classroom">
<div class="modal-content card"> <div class="modal-content card">
<h2>Редактировать аудиторию</h2> <h2>Редактировать аудиторию</h2>
<button class="modal-close" id="modal-edit-classroom-close">&times;</button> <button type="button" class="btn btn-icon-md btn-ghost modal-close" id="modal-edit-classroom-close" aria-label="Закрыть окно">&times;</button>
<form id="edit-classroom-form"> <form id="edit-classroom-form">
<input type="hidden" id="edit-classroom-id"> <input type="hidden" id="edit-classroom-id">
<div class="form-row" style="margin-top: 1rem;"> <div class="form-row" style="margin-top: 1rem;">
@@ -116,7 +116,7 @@
<div class="modal-content card" style="max-width: 600px; width: 90%;"> <div class="modal-content card" style="max-width: 600px; width: 90%;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2>Справочник оборудования</h2> <h2>Справочник оборудования</h2>
<button class="modal-close" id="modal-manage-equipments-close" style="position: static; font-size: 2rem; background: none; border: none; cursor: pointer; color: var(--text-secondary);">&times;</button> <button type="button" class="btn btn-icon-md btn-ghost modal-close modal-close--static" id="modal-manage-equipments-close" aria-label="Закрыть окно">&times;</button>
</div> </div>
<!-- Форма добавления --> <!-- Форма добавления -->

View File

@@ -91,7 +91,7 @@
<!-- Свободные аудитории --> <!-- Свободные аудитории -->
<div class="card dashboard-compact-card"> <div class="card dashboard-compact-card">
<h3 class="dashboard-card-title">Свободные аудитории (ближайшие)</h3> <h3 class="dashboard-card-title">Свободные аудитории (ближайшие)</h3>
<div id="free-classrooms-list" class="dashboard-badge-list"> <div id="free-classrooms-list" class="dashboard-badge-list" tabindex="0" role="region" aria-label="Свободные аудитории">
<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>

View File

@@ -118,7 +118,7 @@
<div class="modal-overlay" id="modal-edit-group"> <div class="modal-overlay" id="modal-edit-group">
<div class="modal-content card edit-group-modal"> <div class="modal-content card edit-group-modal">
<h2>Редактировать группу</h2> <h2>Редактировать группу</h2>
<button class="modal-close" id="modal-edit-group-close">&times;</button> <button type="button" class="btn btn-icon-md btn-ghost modal-close" id="modal-edit-group-close" aria-label="Закрыть окно">&times;</button>
<form id="edit-group-form"> <form id="edit-group-form">
<input type="hidden" id="edit-group-id"> <input type="hidden" id="edit-group-id">
<div class="form-row edit-group-form-row edit-group-form-row-main"> <div class="form-row edit-group-form-row edit-group-form-row-main">
@@ -177,7 +177,7 @@
<h2>Подгруппы группы</h2> <h2>Подгруппы группы</h2>
<div id="subgroups-group-context" style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;"></div> <div id="subgroups-group-context" style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;"></div>
</div> </div>
<button class="modal-close" id="modal-manage-subgroups-close" style="position: static; font-size: 2rem; background: none; border: none; cursor: pointer; color: var(--text-secondary);">&times;</button> <button type="button" class="btn btn-icon-md btn-ghost modal-close modal-close--static" id="modal-manage-subgroups-close" aria-label="Закрыть окно">&times;</button>
</div> </div>
<!-- Форма --> <!-- Форма -->
@@ -233,7 +233,7 @@
<h2>Календарные графики группы</h2> <h2>Календарные графики группы</h2>
<div id="calendar-group-context" style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;"></div> <div id="calendar-group-context" style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;"></div>
</div> </div>
<button class="modal-close" id="modal-manage-calendar-close" style="position: static; font-size: 2rem; background: none; border: none; cursor: pointer; color: var(--text-secondary);">&times;</button> <button type="button" class="btn btn-icon-md btn-ghost modal-close modal-close--static" id="modal-manage-calendar-close" aria-label="Закрыть окно">&times;</button>
</div> </div>
<!-- Форма --> <!-- Форма -->

View File

@@ -71,7 +71,7 @@
<div class="version-modal" id="version-modal" hidden> <div class="version-modal" id="version-modal" hidden>
<div class="version-modal-card" role="dialog" aria-modal="true" aria-labelledby="version-modal-title"> <div class="version-modal-card" role="dialog" aria-modal="true" aria-labelledby="version-modal-title">
<button type="button" class="version-modal-close" id="version-modal-close" aria-label="Закрыть">×</button> <button type="button" class="btn btn-icon-md btn-ghost version-modal-close" id="version-modal-close" aria-label="Закрыть окно">×</button>
<span class="version-eyebrow" id="version-modal-kicker">Новая операция</span> <span class="version-eyebrow" id="version-modal-kicker">Новая операция</span>
<h3 id="version-modal-title">Версия расписания</h3> <h3 id="version-modal-title">Версия расписания</h3>
<p id="version-modal-copy"></p> <p id="version-modal-copy"></p>

View File

@@ -38,7 +38,7 @@
<div class="version-modal schedule-draft-modal" id="schedule-draft-modal" hidden> <div class="version-modal schedule-draft-modal" id="schedule-draft-modal" hidden>
<div class="version-modal-card" role="dialog" aria-modal="true" aria-labelledby="schedule-draft-title"> <div class="version-modal-card" role="dialog" aria-modal="true" aria-labelledby="schedule-draft-title">
<button type="button" class="version-modal-close" id="schedule-draft-close" aria-label="Закрыть">×</button> <button type="button" class="btn btn-icon-md btn-ghost version-modal-close" id="schedule-draft-close" aria-label="Закрыть окно">×</button>
<span class="version-eyebrow">Новая рабочая копия</span> <span class="version-eyebrow">Новая рабочая копия</span>
<h3 id="schedule-draft-title">Создать черновик</h3> <h3 id="schedule-draft-title">Создать черновик</h3>
<p id="schedule-draft-copy">Все правила выбранного расписания будут скопированы.</p> <p id="schedule-draft-copy">Все правила выбранного расписания будут скопированы.</p>
@@ -152,7 +152,7 @@
<div class="modal-overlay schedule-conflict-modal" id="schedule-conflict-modal" role="dialog" aria-modal="true" aria-hidden="true"> <div class="modal-overlay schedule-conflict-modal" id="schedule-conflict-modal" role="dialog" aria-modal="true" aria-hidden="true">
<div class="modal-content schedule-conflict-card"> <div class="modal-content schedule-conflict-card">
<button type="button" class="modal-close" id="schedule-conflict-close" aria-label="Закрыть окно">&times;</button> <button type="button" class="btn btn-icon-md btn-ghost modal-close" id="schedule-conflict-close" aria-label="Закрыть окно">&times;</button>
<div class="schedule-conflict-head"> <div class="schedule-conflict-head">
<span class="schedule-conflict-kicker">Конфликт слота</span> <span class="schedule-conflict-kicker">Конфликт слота</span>
<h2>Освободите место для нового правила</h2> <h2>Освободите место для нового правила</h2>
@@ -190,7 +190,7 @@
<div class="modal-overlay schedule-move-slot-modal" id="schedule-move-slot-modal" role="dialog" aria-modal="true" aria-hidden="true"> <div class="modal-overlay schedule-move-slot-modal" id="schedule-move-slot-modal" role="dialog" aria-modal="true" aria-hidden="true">
<div class="modal-content schedule-move-slot-card"> <div class="modal-content schedule-move-slot-card">
<button type="button" class="modal-close" id="schedule-move-slot-close" aria-label="Закрыть окно">&times;</button> <button type="button" class="btn btn-icon-md btn-ghost modal-close" id="schedule-move-slot-close" aria-label="Закрыть окно">&times;</button>
<div class="schedule-conflict-head"> <div class="schedule-conflict-head">
<span class="schedule-conflict-kicker">Перенос слота</span> <span class="schedule-conflict-kicker">Перенос слота</span>
<h2 id="schedule-move-slot-title">Изменить день и пару</h2> <h2 id="schedule-move-slot-title">Изменить день и пару</h2>

View File

@@ -75,7 +75,7 @@
<h2>Преподаватели дисциплины</h2> <h2>Преподаватели дисциплины</h2>
<div id="subject-teachers-context" style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;"></div> <div id="subject-teachers-context" style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;"></div>
</div> </div>
<button class="modal-close" id="modal-manage-teachers-close" style="position: static; font-size: 2rem; background: none; border: none; cursor: pointer; color: var(--text-secondary);">&times;</button> <button type="button" class="btn btn-icon-md btn-ghost modal-close modal-close--static" id="modal-manage-teachers-close" aria-label="Закрыть окно">&times;</button>
</div> </div>
<!-- Форма --> <!-- Форма -->

View File

@@ -169,7 +169,7 @@
<div class="modal-overlay" id="modal-edit-department"> <div class="modal-overlay" id="modal-edit-department">
<div class="modal-content card"> <div class="modal-content card">
<h2>Редактирование кафедры</h2> <h2>Редактирование кафедры</h2>
<button class="modal-close" id="modal-edit-department-close">&times;</button> <button type="button" class="btn btn-icon-md btn-ghost modal-close" id="modal-edit-department-close" aria-label="Закрыть окно">&times;</button>
<form id="edit-department-form"> <form id="edit-department-form">
<input type="hidden" id="edit-dept-id"> <input type="hidden" id="edit-dept-id">
<div class="form-row"> <div class="form-row">
@@ -192,7 +192,7 @@
<div class="modal-overlay" id="modal-edit-specialty"> <div class="modal-overlay" id="modal-edit-specialty">
<div class="modal-content card"> <div class="modal-content card">
<h2>Редактирование специальности</h2> <h2>Редактирование специальности</h2>
<button class="modal-close" id="modal-edit-specialty-close">&times;</button> <button type="button" class="btn btn-icon-md btn-ghost modal-close" id="modal-edit-specialty-close" aria-label="Закрыть окно">&times;</button>
<form id="edit-specialty-form"> <form id="edit-specialty-form">
<input type="hidden" id="edit-spec-id"> <input type="hidden" id="edit-spec-id">
<div class="form-row"> <div class="form-row">
@@ -219,7 +219,7 @@
<h2>Профили обучения</h2> <h2>Профили обучения</h2>
<div id="profiles-spec-context" style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;"></div> <div id="profiles-spec-context" style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;"></div>
</div> </div>
<button class="modal-close" id="modal-manage-profiles-close" style="position: static; font-size: 2rem; background: none; border: none; cursor: pointer; color: var(--text-secondary);">&times;</button> <button type="button" class="btn btn-icon-md btn-ghost modal-close modal-close--static" id="modal-manage-profiles-close" aria-label="Закрыть окно">&times;</button>
</div> </div>
<!-- Форма создания/редактирования профиля --> <!-- Форма создания/редактирования профиля -->

View File

@@ -6,6 +6,7 @@
<meta name="description" content="Страница авторизации"> <meta name="description" content="Страница авторизации">
<title>Авторизация</title> <title>Авторизация</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="ui-foundation.css">
</head> </head>
<body> <body>
<div class="background"> <div class="background">

View File

@@ -1,4 +1,4 @@
Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data:; object-src 'none'; script-src 'self'; script-src-attr 'none'; style-src 'self'; style-src-attr 'unsafe-hashes' 'sha256-Ap89pw6Ru+bI6/fvSZrLRErTB0Vlrkjhr+GBUApiIWk=' 'sha256-SIXM20kwgT/LFCXoUieNCZ2zVEtDEJLBMFkzuqTDiEs=' 'sha256-Gg5P79IEFo2naj0u+NnV/XRNhPpmG+qszzSTXOPUj1A=' 'sha256-TnG5YZ63fruUpLo7bQSZmwEy7iaTHxpbBhwz5vu9uSg=' 'sha256-XzNZtBQ5RVU4zPLtfvXv2K6GCBeoCM2HUIIQSlEaULA=' 'sha256-nULJId45lkSYkp9G7rs5Ucrzkl/RvOYcmolE2LAWGy0=' 'sha256-zxhlsg7XWHprQXI4sW8mCF1bkpJgB7ybVJwWOPVLGRE=' 'sha256-K0Xrmwt6pAGUrGVS8MwRZJExIhykcVQ8SI1byCheVwc=' 'sha256-4YmfPYnVNL8IzZiGb/0iwhc6yUgCF19Y33KOfCPVmCw=' 'sha256-iNZSE5e3vOhMyMQkK5rpl8610z7E4ZtyqgP2Svv49PU=' 'sha256-MhqAPfMbUlw1OEgW5/QfCxo+BaJmIHx/8235W2X3bVI=' 'sha256-foNl8DYrtM07r6llOCuEQkuDC3P+Z9AhhM6r+IJDuvE=' 'sha256-TZ/59DFnpMwCdrSuMVhcjfKS2+Uktb1ikcGdbop+LrY=' 'sha256-bouNTXMWL7OeoflvcP2VmNuyQ00CJIpU091rlJcNh0o=' 'sha256-SWPOf/xgJrjLW+pINXE1lz0iy53MqwWqXeVJ6eC/feI=' 'sha256-1N/gWxc/W4gFlj8zf/p3KcADNes7ROePFaTOWiqwF4E=' 'sha256-biLFinpqYMtWHmXfkA1BPeCY0/fNt46SAZ+BBk5YUog=' 'sha256-/dAGmjflKS5r+hQF1hmagGz5QABPHf+YFadZo1ne2rc=' 'sha256-Xrwk3bvnw8h9hvxxkkKe/tGHcDa4FiyIkZgQh7zZ5OM=' 'sha256-yg9xEhg50yeViXuIpHVfY4ORsctQahuBNAF5elcPo2o=' 'sha256-wJR14aiwE2jYijrg7Fjqg/N0F1aAZ5CIcUtwN05LcGg=' 'sha256-aYnI2FBeHDuscjjfM9Mw136ryNwuYO8SA40Zevg1KoY=' 'sha256-y6K4epLEZv6QKwcoJSp/0xGIN8fOl7DSHSPNd5QT6Xo=' 'sha256-tdUysXRHnX2Z4LXERihNGxflbZuvaer3/ZEbjN2vQYE=' 'sha256-scdHk/Xhx12Q7ITYKMEP661eR9qxmkAEEIEGSoSvhnk=' 'sha256-SQjLChNmhyZMwxbpXjOF6LYFFUEHnV6PvzvTEmWb7eA=' 'sha256-50MALp7OIkg8n1p4VILXcnKO8ypKpLNL/QYLOgWPOCk=' 'sha256-clWb9y4Y/RHEr3K6egWYXbH1r5Hi2fgpzDHlBdskEeo=' 'sha256-chNIGV8ioVlRP9Ypn/XhEA2EhWFAeWCuUGJL5S+vzz0=' 'sha256-W998Pje85WadRdREdYtdQtKylzNM5QOtfq5HJWoaeTc=' 'sha256-s6IpehL1Dcfvbn/QY1MA4otfjj+O7fzMbbfi7Qc6pmA=' 'sha256-AC3CbEeLPJf2W6daVbkfUpWh5e6gLviBovIHzqhb7I4=' 'sha256-+j7xWNheSULp9DWeiEHZrhTxGekhyQ9qIjUgAiPhJ50=' 'sha256-moRhvd2U0JZLbFHJNyPJuevxlkfGXw5XWMByVNv9Vig=' 'sha256-203TZuzrYa786NHRsoV1tXZ8L/9lEl+/UL46E0VAiGI=' 'sha256-032ZxPG0oQ3+/U6NXKrgm5LnBreb9401xJrjrsb+p+U=' 'sha256-JamKOmpIVQSPv+gDrYtzbxnVclnrvhrOMLmA+WaGPIE=' 'sha256-mnRyqew64Lcv5/Qgb/4HP8LfJi5MjjQCGLPt+/cJZis=' 'sha256-Fx6gwPzP46ZjBgLjBZ6QLksJxr+0rXElPelXtSQMnxE=' 'sha256-fug/5+Ut6nV0WD+m4YV23l7GiD/oMKUnUvZdMiDCZmw=' 'sha256-AkGc/9SiOd74zk72UnCdLs+k10sM4iy2uKmgoXkaHe0=' 'sha256-Ar6JjQB5XpqI8eiPMCdTp/UuHaqgHiv6I+MV5V0AqD0=' 'sha256-q3nqK4VzeI/SowYCYQ/tCfo056B/JMutuSpzbJbHczk=' 'sha256-FHr5kWe1pqEckj58767LzvY6a0Jefvryb6NDWnX+9iA=' 'sha256-3bpbcQ42x+1MqmADGJ3zikAZX1tMXcOSJO5HaQgBLd0=' 'sha256-JZXmJpz7JH1a0DyiOr/syUiirSXF8caWHSEs55ZeuvM=' 'sha256-5z4wfnbsEag4kdsuEHpgdTrXYzlAY7n+RR18FQrPaEk=' 'sha256-97KgMmG+SNbCjjLJMC4FFguhozddOvNKLVC6hCmCMsg=' 'sha256-K1k356n2KePxr1iToCyZJNdGCm9eq1GSLPiovYYcGJw=' 'sha256-Wbi+cQ7sBtwXXZqWfFgLJRH4d6W8PogiptWNj9KIw8c=' 'sha256-ZGygktZXqOYjv8SF3rIcpWd19YFA1qMbC3LhD9PPSIk=' 'sha256-C1diVwUHC99dr4LcsUyLWfgQa8cu0K/7SBK+bd24TWg=' 'sha256-gKWvnkygeYkz1lyPSLpgVWSsmFaaDT2lY+hqijL4Gd8=' 'sha256-XhqU3am1TiOnFUKLIcPS5JCePjRUcUoGqQvjnfL92ik=' 'sha256-COR0sjBXRz2BkYmsZyQELjCUGLEbVii9YMmpBGwfFhM=' 'sha256-N90MKmRow2DpYEVeqcc3uc8pOUsS4Rg4sNmkau1k0xQ=' 'sha256-qcFcRsF+ljnThVJj80IfggPdbDjDPk/ZEeeiFCw/f4s=' 'sha256-niVdTxYNBhpZEFDz7d87VgN56BaVHxkE1mKzrSiTEa8=' 'sha256-N6tSydZ64AHCaOWfwKbUhxXx2fRFDxHOaL3e3CO7GPI=' 'sha256-V3Yf08rYNHRd1lYyQK1mcEb3duonX88OA6nfz8qDvuY=' 'sha256-7VXlcg/uSZugHSa6UtIG2/44ju460LiO4M0CyQfraX8='; upgrade-insecure-requests" Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data:; object-src 'none'; script-src 'self'; script-src-attr 'none'; style-src 'self'; style-src-attr 'unsafe-hashes' 'sha256-Ap89pw6Ru+bI6/fvSZrLRErTB0Vlrkjhr+GBUApiIWk=' 'sha256-SIXM20kwgT/LFCXoUieNCZ2zVEtDEJLBMFkzuqTDiEs=' 'sha256-Gg5P79IEFo2naj0u+NnV/XRNhPpmG+qszzSTXOPUj1A=' 'sha256-TnG5YZ63fruUpLo7bQSZmwEy7iaTHxpbBhwz5vu9uSg=' 'sha256-XzNZtBQ5RVU4zPLtfvXv2K6GCBeoCM2HUIIQSlEaULA=' 'sha256-nULJId45lkSYkp9G7rs5Ucrzkl/RvOYcmolE2LAWGy0=' 'sha256-zxhlsg7XWHprQXI4sW8mCF1bkpJgB7ybVJwWOPVLGRE=' 'sha256-K0Xrmwt6pAGUrGVS8MwRZJExIhykcVQ8SI1byCheVwc=' 'sha256-4YmfPYnVNL8IzZiGb/0iwhc6yUgCF19Y33KOfCPVmCw=' 'sha256-iNZSE5e3vOhMyMQkK5rpl8610z7E4ZtyqgP2Svv49PU=' 'sha256-MhqAPfMbUlw1OEgW5/QfCxo+BaJmIHx/8235W2X3bVI=' 'sha256-foNl8DYrtM07r6llOCuEQkuDC3P+Z9AhhM6r+IJDuvE=' 'sha256-TZ/59DFnpMwCdrSuMVhcjfKS2+Uktb1ikcGdbop+LrY=' 'sha256-bouNTXMWL7OeoflvcP2VmNuyQ00CJIpU091rlJcNh0o=' 'sha256-SWPOf/xgJrjLW+pINXE1lz0iy53MqwWqXeVJ6eC/feI=' 'sha256-1N/gWxc/W4gFlj8zf/p3KcADNes7ROePFaTOWiqwF4E=' 'sha256-biLFinpqYMtWHmXfkA1BPeCY0/fNt46SAZ+BBk5YUog=' 'sha256-/dAGmjflKS5r+hQF1hmagGz5QABPHf+YFadZo1ne2rc=' 'sha256-Xrwk3bvnw8h9hvxxkkKe/tGHcDa4FiyIkZgQh7zZ5OM=' 'sha256-yg9xEhg50yeViXuIpHVfY4ORsctQahuBNAF5elcPo2o=' 'sha256-wJR14aiwE2jYijrg7Fjqg/N0F1aAZ5CIcUtwN05LcGg=' 'sha256-aYnI2FBeHDuscjjfM9Mw136ryNwuYO8SA40Zevg1KoY=' 'sha256-y6K4epLEZv6QKwcoJSp/0xGIN8fOl7DSHSPNd5QT6Xo=' 'sha256-scdHk/Xhx12Q7ITYKMEP661eR9qxmkAEEIEGSoSvhnk=' 'sha256-SQjLChNmhyZMwxbpXjOF6LYFFUEHnV6PvzvTEmWb7eA=' 'sha256-50MALp7OIkg8n1p4VILXcnKO8ypKpLNL/QYLOgWPOCk=' 'sha256-clWb9y4Y/RHEr3K6egWYXbH1r5Hi2fgpzDHlBdskEeo=' 'sha256-chNIGV8ioVlRP9Ypn/XhEA2EhWFAeWCuUGJL5S+vzz0=' 'sha256-W998Pje85WadRdREdYtdQtKylzNM5QOtfq5HJWoaeTc=' 'sha256-s6IpehL1Dcfvbn/QY1MA4otfjj+O7fzMbbfi7Qc6pmA=' 'sha256-AC3CbEeLPJf2W6daVbkfUpWh5e6gLviBovIHzqhb7I4=' 'sha256-+j7xWNheSULp9DWeiEHZrhTxGekhyQ9qIjUgAiPhJ50=' 'sha256-moRhvd2U0JZLbFHJNyPJuevxlkfGXw5XWMByVNv9Vig=' 'sha256-032ZxPG0oQ3+/U6NXKrgm5LnBreb9401xJrjrsb+p+U=' 'sha256-JamKOmpIVQSPv+gDrYtzbxnVclnrvhrOMLmA+WaGPIE=' 'sha256-mnRyqew64Lcv5/Qgb/4HP8LfJi5MjjQCGLPt+/cJZis=' 'sha256-Fx6gwPzP46ZjBgLjBZ6QLksJxr+0rXElPelXtSQMnxE=' 'sha256-fug/5+Ut6nV0WD+m4YV23l7GiD/oMKUnUvZdMiDCZmw=' 'sha256-AkGc/9SiOd74zk72UnCdLs+k10sM4iy2uKmgoXkaHe0=' 'sha256-Ar6JjQB5XpqI8eiPMCdTp/UuHaqgHiv6I+MV5V0AqD0=' 'sha256-q3nqK4VzeI/SowYCYQ/tCfo056B/JMutuSpzbJbHczk=' 'sha256-FHr5kWe1pqEckj58767LzvY6a0Jefvryb6NDWnX+9iA=' 'sha256-3bpbcQ42x+1MqmADGJ3zikAZX1tMXcOSJO5HaQgBLd0=' 'sha256-JZXmJpz7JH1a0DyiOr/syUiirSXF8caWHSEs55ZeuvM=' 'sha256-5z4wfnbsEag4kdsuEHpgdTrXYzlAY7n+RR18FQrPaEk=' 'sha256-97KgMmG+SNbCjjLJMC4FFguhozddOvNKLVC6hCmCMsg=' 'sha256-K1k356n2KePxr1iToCyZJNdGCm9eq1GSLPiovYYcGJw=' 'sha256-Wbi+cQ7sBtwXXZqWfFgLJRH4d6W8PogiptWNj9KIw8c=' 'sha256-ZGygktZXqOYjv8SF3rIcpWd19YFA1qMbC3LhD9PPSIk=' 'sha256-C1diVwUHC99dr4LcsUyLWfgQa8cu0K/7SBK+bd24TWg=' 'sha256-XhqU3am1TiOnFUKLIcPS5JCePjRUcUoGqQvjnfL92ik=' 'sha256-COR0sjBXRz2BkYmsZyQELjCUGLEbVii9YMmpBGwfFhM=' 'sha256-N90MKmRow2DpYEVeqcc3uc8pOUsS4Rg4sNmkau1k0xQ=' 'sha256-qcFcRsF+ljnThVJj80IfggPdbDjDPk/ZEeeiFCw/f4s=' 'sha256-niVdTxYNBhpZEFDz7d87VgN56BaVHxkE1mKzrSiTEa8=' 'sha256-N6tSydZ64AHCaOWfwKbUhxXx2fRFDxHOaL3e3CO7GPI=' 'sha256-V3Yf08rYNHRd1lYyQK1mcEb3duonX88OA6nfz8qDvuY=' 'sha256-7VXlcg/uSZugHSa6UtIG2/44ju460LiO4M0CyQfraX8='; upgrade-insecure-requests"
Header always set Referrer-Policy "strict-origin-when-cross-origin" Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set X-Content-Type-Options "nosniff" Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY" Header always set X-Frame-Options "DENY"

View File

@@ -6,6 +6,7 @@
<title>Расписание студента</title> <title>Расписание студента</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="../schedule-overview.css"> <link rel="stylesheet" href="../schedule-overview.css">
<link rel="stylesheet" href="../ui-foundation.css">
</head> </head>
<body> <body>
<main class="shell"> <main class="shell">

View File

@@ -33,7 +33,7 @@ body {
min-height: 100vh; min-height: 100vh;
background: var(--bg); background: var(--bg);
color: var(--text); color: var(--text);
font-family: "Segoe UI", system-ui, -apple-system, sans-serif; font-family: var(--ui-font-sans, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
} }
.shell { .shell {

View File

@@ -110,7 +110,7 @@ html {
} }
body { body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family: var(--ui-font-sans, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
background: var(--bg-primary); background: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
min-height: 100vh; min-height: 100vh;

View File

@@ -220,7 +220,7 @@ import { trackUxEvent } from '../telemetry.js';
: 'Добавить отметку'; : 'Добавить отметку';
return `<button type="button" class="preference-cell ${typeClass}${pendingClass}" return `<button type="button" class="preference-cell ${typeClass}${pendingClass}"
data-day="${index + 1}" data-slot="${slot.id}" data-preference-id="${item?.id || ''}" data-day="${index + 1}" data-slot="${slot.id}" data-preference-id="${item?.id || ''}"
data-preference-status="${item?.status || ''}" title="${escapeHtml(title)}"> data-preference-status="${item?.status || ''}" title="${escapeHtml(title)}" aria-label="${escapeHtml(title)}">
<span>${item ? preferenceTypeSymbol(item.preferenceType) : '+'}</span> <span>${item ? preferenceTypeSymbol(item.preferenceType) : '+'}</span>
</button>`; </button>`;
}) })
@@ -681,7 +681,7 @@ import { trackUxEvent } from '../telemetry.js';
${groups ? `<span class="chip">${escapeHtml(groups)}</span>` : ''} ${groups ? `<span class="chip">${escapeHtml(groups)}</span>` : ''}
</div> </div>
${lesson.scheduleRuleSlotId && !lesson.scheduleOverrideId ? ` ${lesson.scheduleRuleSlotId && !lesson.scheduleOverrideId ? `
<button class="lesson-request-button" type="button" data-request-lesson="${escapeHtml(lessonKey(lesson))}"> <button class="btn btn-sm btn-secondary lesson-request-button" type="button" data-request-lesson="${escapeHtml(lessonKey(lesson))}">
Запросить изменение Запросить изменение
</button> </button>
` : '<span class="lesson-change-note">Занятие уже изменено учебным отделом</span>'} ` : '<span class="lesson-change-note">Занятие уже изменено учебным отделом</span>'}

View File

@@ -6,6 +6,7 @@
<title>Кабинет преподавателя</title> <title>Кабинет преподавателя</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="../schedule-overview.css"> <link rel="stylesheet" href="../schedule-overview.css">
<link rel="stylesheet" href="../ui-foundation.css">
</head> </head>
<body> <body>
<main class="shell"> <main class="shell">

View File

@@ -207,7 +207,7 @@ body {
min-height: 100vh; min-height: 100vh;
background: var(--bg); background: var(--bg);
color: var(--text); color: var(--text);
font-family: "Segoe UI", system-ui, -apple-system, sans-serif; font-family: var(--ui-font-sans, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
} }
.shell { .shell {

View File

@@ -81,6 +81,12 @@ test('Docker-образ включает локальный telemetry bundle и
assert.match(dockerfile, /scripts\/build-vendor\.mjs/); assert.match(dockerfile, /scripts\/build-vendor\.mjs/);
assert.match(dockerfile, /COPY --from=frontend-assets \/build\/dist\/vendor\//); assert.match(dockerfile, /COPY --from=frontend-assets \/build\/dist\/vendor\//);
assert.match(dockerfile, /magistr-security\.conf/); assert.match(dockerfile, /magistr-security\.conf/);
for (const asset of ['ui-foundation.css', 'schedule-overview.css', 'schedule-overview.js']) {
const copiesAsset = dockerfile.split('\n').some(line =>
line.startsWith('COPY ') && line.includes(asset) && line.includes('/usr/local/apache2/htdocs/')
);
assert.ok(copiesAsset, `Общий ресурс отсутствует в Docker-образе: ${asset}`);
}
assert.match(telemetry, /import\('\/vendor\/otel\.js'\)/); assert.match(telemetry, /import\('\/vendor\/otel\.js'\)/);
assert.doesNotMatch(telemetry, /https?:\/\//); assert.doesNotMatch(telemetry, /https?:\/\//);
}); });

View File

@@ -0,0 +1,94 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
const readFrontendFile = relativePath => readFile(
new URL(`../${relativePath}`, import.meta.url),
'utf8'
);
test('все пользовательские оболочки завершают каскад единой визуальной основой', async () => {
const entrypoints = [
['index.html', 'ui-foundation.css'],
['admin/index.html', '../ui-foundation.css'],
['admin/settings/index.html', '../../ui-foundation.css'],
['teacher/index.html', '../ui-foundation.css'],
['student/index.html', '../ui-foundation.css']
];
for (const [file, expectedHref] of entrypoints) {
const html = await readFrontendFile(file);
const stylesheets = Array.from(
html.matchAll(/<link\b[^>]*rel="stylesheet"[^>]*href="([^"]+)"[^>]*>/g),
match => match[1]
);
assert.equal(stylesheets.at(-1), expectedHref, file);
}
});
test('единая визуальная основа фиксирует типографику, размеры и доступный фокус', async () => {
const foundation = await readFrontendFile('ui-foundation.css');
assert.match(foundation, /--ui-font-sans:/);
assert.match(foundation, /--ui-control-sm:\s*32px/);
assert.match(foundation, /--ui-control-md:\s*40px/);
assert.match(foundation, /--ui-control-lg:\s*48px/);
assert.match(foundation, /\.btn-sm\s*\{[^}]*min-height:\s*var\(--ui-control-sm\)/s);
assert.match(foundation, /\.btn-md\s*\{[^}]*min-height:\s*var\(--ui-control-md\)/s);
assert.match(foundation, /\.btn-lg\s*\{[^}]*min-height:\s*var\(--ui-control-lg\)/s);
assert.match(foundation, /:focus-visible\s*\{[^}]*outline:/s);
assert.match(foundation, /prefers-reduced-motion:\s*reduce/);
});
test('локальные стили используют общие шрифтовые токены', async () => {
const files = [
'style.css',
'student/style.css',
'teacher/style.css',
'admin/css/main.css',
'admin/settings/css/main.css',
'admin/css/schedule-quality.css',
'admin/css/schedule-versions.css'
];
for (const file of files) {
const css = await readFrontendFile(file);
assert.doesNotMatch(css, /font-family:\s*(?:['"]?Inter|['"]?Segoe UI|Georgia|['"]Times New Roman)/, file);
}
});
test('архивные действия и кнопки закрытия используют системные размеры', async () => {
const actionSources = await Promise.all([
readFrontendFile('admin/js/views/classrooms.js'),
readFrontendFile('admin/js/views/groups.js'),
readFrontendFile('admin/js/views/users.js')
]);
actionSources.forEach(source => {
assert.match(source, /class="btn btn-sm btn-secondary btn-restore-/);
});
const modalViews = await Promise.all([
'admin/views/academic-calendar.html',
'admin/views/classrooms.html',
'admin/views/groups.html',
'admin/views/schedule.html',
'admin/views/schedule-versions.html',
'admin/views/subjects.html',
'admin/views/university-structure.html',
'admin/settings/views/time-slots.html'
].map(readFrontendFile));
const closeButtons = modalViews.flatMap(html => Array.from(
html.matchAll(/<button\b[^>]*class="[^"]*(?:modal-close|version-modal-close|settings-modal-close)[^"]*"[^>]*>/g),
match => match[0]
));
assert.ok(closeButtons.length > 0);
closeButtons.forEach(button => {
assert.match(button, /type="button"/);
assert.match(button, /class="[^"]*\bbtn\b[^"]*\bbtn-icon-md\b/);
assert.match(button, /aria-label="Закрыть окно"/);
});
});

421
frontend/ui-foundation.css Normal file
View File

@@ -0,0 +1,421 @@
/*
* Единая визуальная основа Magistr.
*
* Файл подключается последним во всех пользовательских оболочках и задаёт
* общие инварианты: типографику, размеры элементов управления, фокус,
* радиусы, ритм и поведение на узких экранах. Локальные CSS отвечают только
* за компоновку и предметные компоненты конкретного раздела.
*/
:root {
--ui-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--ui-font-display: var(--ui-font-sans);
--ui-font-mono: "Cascadia Mono", "SFMono-Regular", Consolas, monospace;
--ui-control-sm: 32px;
--ui-control-md: 40px;
--ui-control-lg: 48px;
--ui-radius-control: 8px;
--ui-radius-field: 10px;
--ui-radius-panel: 16px;
--ui-radius-large: 24px;
--ui-space-1: 4px;
--ui-space-2: 8px;
--ui-space-3: 12px;
--ui-space-4: 16px;
--ui-space-5: 20px;
--ui-space-6: 24px;
--ui-space-8: 32px;
--ui-shadow-panel: 0 12px 32px rgba(15, 23, 42, 0.08);
--ui-shadow-floating: 0 20px 56px rgba(15, 23, 42, 0.18);
--ui-transition: 160ms cubic-bezier(0.2, 0, 0, 1);
--ui-focus-ring: var(--button-focus-ring, rgba(79, 70, 229, 0.35));
/* Совместимость с уже существующими токенами разделов. */
--button-radius: var(--ui-radius-control);
}
html {
color-scheme: dark;
font-size: 16px;
text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html[data-theme="light"] {
color-scheme: light;
}
body {
font-family: var(--ui-font-sans);
font-size: 1rem;
line-height: 1.5;
text-rendering: optimizeLegibility;
}
button,
input,
select,
textarea {
font: inherit;
letter-spacing: inherit;
}
button,
[role="button"],
a {
-webkit-tap-highlight-color: transparent;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--ui-font-display);
font-weight: 700;
line-height: 1.18;
letter-spacing: -0.025em;
text-wrap: balance;
}
p,
li,
dd {
text-wrap: pretty;
}
time,
input[type="date"],
input[type="time"],
.schedule-overview-next-time,
.academic-year-code,
.version-number,
.metric-value {
font-variant-numeric: tabular-nums;
}
::selection {
background: color-mix(in srgb, var(--accent, #4f46e5) 28%, transparent);
}
/* ===== Доступный фокус ===== */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
outline: 2px solid var(--accent, #4f46e5);
outline-offset: 2px;
}
:where(input, select, textarea):focus-visible {
border-color: var(--accent, #4f46e5);
box-shadow: 0 0 0 3px var(--ui-focus-ring);
}
/* ===== Кнопки ===== */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--ui-space-2);
min-width: 0;
min-height: var(--ui-control-md);
padding: 0 var(--ui-space-4);
border: 1px solid var(--button-secondary-border, var(--panel-border, var(--bg-card-border)));
border-radius: var(--ui-radius-control);
font-family: var(--ui-font-sans);
font-size: 0.875rem;
font-weight: 650;
line-height: 1;
text-align: center;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
user-select: none;
transition:
background-color var(--ui-transition),
border-color var(--ui-transition),
color var(--ui-transition),
box-shadow var(--ui-transition),
transform var(--ui-transition);
}
.btn:hover:not(:disabled):not([aria-disabled="true"]) {
transform: translateY(-1px);
}
.btn:active:not(:disabled):not([aria-disabled="true"]) {
transform: translateY(0);
}
.btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--ui-focus-ring);
}
.btn:disabled,
.btn[aria-disabled="true"] {
cursor: not-allowed;
opacity: 0.62;
transform: none;
}
.btn-sm {
min-height: var(--ui-control-sm);
padding-inline: var(--ui-space-3);
font-size: 0.8125rem;
}
.btn-md {
min-height: var(--ui-control-md);
padding-inline: var(--ui-space-4);
font-size: 0.875rem;
}
.btn-lg {
min-height: var(--ui-control-lg);
padding-inline: var(--ui-space-5);
font-size: 0.9375rem;
font-weight: 700;
}
.btn-icon-sm,
.btn-icon-md {
flex: 0 0 auto;
padding: 0;
border-radius: var(--ui-radius-control);
}
.btn-icon-sm {
width: var(--ui-control-sm);
height: var(--ui-control-sm);
min-height: var(--ui-control-sm);
}
.btn-icon-md {
width: var(--ui-control-md);
height: var(--ui-control-md);
min-height: var(--ui-control-md);
}
.btn-primary {
border-color: var(--button-primary-border, var(--accent, #4f46e5));
background: var(--button-primary-bg, var(--accent, #4f46e5));
color: var(--button-primary-text, #fff);
}
.btn-primary:hover:not(:disabled) {
border-color: var(--button-primary-border, var(--accent-strong, var(--accent-hover, #4338ca)));
background: var(--button-primary-bg-hover, var(--accent-strong, var(--accent-hover, #4338ca)));
color: var(--button-primary-text, #fff);
}
.btn-secondary {
border-color: var(--button-secondary-border, var(--panel-border, var(--bg-card-border)));
background: var(--button-secondary-bg, var(--panel, var(--bg-card)));
color: var(--button-secondary-text, var(--text, var(--text-primary)));
}
.btn-secondary:hover:not(:disabled) {
border-color: color-mix(in srgb, var(--accent, #4f46e5) 38%, var(--button-secondary-border, var(--panel-border, var(--bg-card-border))));
background: var(--button-secondary-bg-hover, var(--bg-hover, color-mix(in srgb, var(--accent, #4f46e5) 7%, var(--panel, var(--bg-card)))));
color: var(--button-secondary-text, var(--text, var(--text-primary)));
}
.btn-ghost {
border-color: var(--button-ghost-border, transparent);
background: transparent;
color: var(--text-secondary, var(--muted));
}
.btn-ghost:hover:not(:disabled) {
border-color: var(--button-ghost-border, var(--panel-border, var(--bg-card-border)));
background: var(--button-ghost-bg-hover, var(--bg-hover, rgba(148, 163, 184, 0.1)));
color: var(--text-primary, var(--text));
}
.btn-danger {
border-color: var(--button-danger-border, #b91c1c);
background: var(--button-danger-bg, #dc2626);
color: #fff;
}
.btn-danger-subtle {
border-color: var(--button-danger-subtle-border, rgba(239, 68, 68, 0.28));
background: var(--button-danger-subtle-bg, rgba(239, 68, 68, 0.12));
color: var(--button-danger-subtle-text, #b91c1c);
}
.btn-danger-subtle:hover:not(:disabled) {
border-color: var(--button-danger-subtle-border, rgba(239, 68, 68, 0.38));
background: var(--button-danger-subtle-bg-hover, rgba(239, 68, 68, 0.18));
color: var(--button-danger-subtle-text, #b91c1c);
}
.input-wrapper .toggle-password {
border-color: transparent;
background: transparent;
color: var(--text-placeholder, var(--muted));
}
/* ===== Поля форм ===== */
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]),
select,
textarea {
border-radius: var(--ui-radius-field);
}
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]),
select {
min-height: var(--ui-control-md);
}
textarea {
min-height: 96px;
line-height: 1.5;
resize: vertical;
}
input::placeholder,
textarea::placeholder {
color: var(--text-placeholder, var(--muted));
opacity: 1;
}
label {
line-height: 1.35;
}
/* ===== Поверхности и ритм ===== */
.card,
.settings-card,
.toolbar,
.schedule-overview,
.teacher-absence-panel,
.preference-board,
.preference-card,
.request-ledger-head,
.request-dialog {
border-radius: var(--ui-radius-panel);
}
.card,
.settings-card,
.toolbar,
.schedule-overview {
box-shadow: var(--ui-shadow-panel);
}
.card:hover {
transform: none;
box-shadow: var(--ui-shadow-panel);
}
.card-header-row,
.settings-header-row,
.dashboard-card-header,
.preference-board-head {
gap: var(--ui-space-4);
}
.form-row,
.filter-row,
.actions,
.week-nav {
column-gap: var(--ui-space-3);
}
table {
font-variant-numeric: tabular-nums;
}
th {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.035em;
}
.table-actions-cell {
text-align: right;
}
.table-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: var(--ui-space-2);
}
.schedule-visual-lesson {
padding-right: 2.55rem;
}
/* ===== Унифицированные кнопки закрытия ===== */
.modal-close,
.version-modal-close,
.settings-modal-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--ui-control-md);
height: var(--ui-control-md);
min-height: var(--ui-control-md);
padding: 0;
border: 1px solid var(--button-ghost-border, var(--panel-border, var(--bg-card-border)));
border-radius: var(--ui-radius-control);
background: transparent;
color: var(--text-secondary, var(--muted));
font-family: var(--ui-font-sans);
font-size: 1.35rem;
line-height: 1;
cursor: pointer;
transition: background-color var(--ui-transition), border-color var(--ui-transition), color var(--ui-transition);
}
.modal-close:hover,
.version-modal-close:hover,
.settings-modal-close:hover {
border-color: color-mix(in srgb, var(--error, #ef4444) 35%, transparent);
background: color-mix(in srgb, var(--error, #ef4444) 10%, transparent);
color: var(--error, #ef4444);
}
.modal-close--static {
position: static;
flex: 0 0 auto;
}
/* ===== Скролл и движение ===== */
* {
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--text-secondary, var(--muted)) 42%, transparent) transparent;
}
@media (max-width: 640px) {
.shell {
padding: var(--ui-space-4);
}
.actions .btn:not(.btn-icon-sm):not(.btn-icon-md) {
flex: 1 1 auto;
}
.modal-content,
.request-dialog,
.version-modal-card {
border-radius: var(--ui-radius-panel);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}