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

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

@@ -18,6 +18,7 @@ COPY edu-office/ /usr/local/apache2/htdocs/edu-office/
COPY student/ /usr/local/apache2/htdocs/student/
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 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 security.conf /usr/local/apache2/conf/extra/magistr-security.conf
COPY proxy.conf /usr/local/apache2/conf/extra/magistr-proxy.conf

View File

@@ -219,6 +219,13 @@
align-content: flex-start;
gap: 0.5rem;
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 {

View File

@@ -67,7 +67,7 @@ html {
}
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);
color: var(--text-primary);
min-height: 100vh;

View File

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

View File

@@ -30,7 +30,7 @@
right: -1.5rem;
bottom: 1.2rem;
color: rgba(255, 255, 255, .055);
font-family: Georgia, serif;
font-family: var(--ui-font-display);
font-size: clamp(4rem, 10vw, 9rem);
letter-spacing: -.08em;
pointer-events: none;
@@ -53,7 +53,7 @@
.version-hero h2 {
max-width: 760px;
margin: 0;
font-family: Georgia, 'Times New Roman', serif;
font-family: var(--ui-font-display);
font-size: clamp(2rem, 4.5vw, 4.5rem);
font-weight: 500;
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));
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-controls { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .65rem; align-items: end; }
.version-constructor-field { min-width: 0; }
@@ -219,7 +219,7 @@
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-card {
@@ -237,7 +237,7 @@
right: .5rem;
top: -.8rem;
color: color-mix(in srgb, var(--version-copper) 12%, transparent);
font-family: Georgia, serif;
font-family: var(--ui-font-display);
font-size: 5rem;
}
@@ -252,7 +252,7 @@
place-content: 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-diff-score {
@@ -262,7 +262,7 @@
margin: 1rem 0;
}
.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-change-list { display: grid; gap: .55rem; max-height: 390px; overflow: auto; }
@@ -292,7 +292,7 @@
}
.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 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 form { display: grid; gap: .65rem; }
.version-modal-card label { color: var(--text-secondary); font-size: .75rem; font-weight: 700; }

View File

@@ -187,6 +187,7 @@
}
.absence-registry-title {
flex-wrap: wrap;
justify-content: space-between;
gap: 1rem;
}
@@ -220,21 +221,31 @@
display: inline-flex;
align-items: center;
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-radius: 999px;
font-size: 0.69rem;
font-weight: 800;
letter-spacing: 0.03em;
white-space: nowrap;
white-space: normal;
}
.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-rejected,
.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 {
justify-content: flex-end;
flex-wrap: wrap;
@@ -481,7 +492,7 @@ body.absence-wizard-open {
.teacher-dialogue-grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
@@ -604,7 +615,7 @@ body.absence-wizard-open {
.teacher-dialogue-item {
display: grid;
grid-template-columns: 7px minmax(0, 1fr) minmax(180px, .42fr);
grid-template-columns: 7px minmax(0, 1fr);
gap: .85rem;
padding: .85rem;
border: 1px solid var(--bg-card-border);
@@ -613,6 +624,7 @@ body.absence-wizard-open {
}
.teacher-dialogue-mark {
grid-row: 1 / 3;
border-radius: 99px;
background: #94a3b8;
}
@@ -631,6 +643,11 @@ body.absence-wizard-open {
font-size: .68rem;
}
.teacher-dialogue-copy {
min-width: 0;
overflow-wrap: anywhere;
}
.teacher-dialogue-copy h3 {
margin: .22rem 0;
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-action {
grid-column: 2;
min-width: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
align-items: stretch;
gap: .55rem;
}
.teacher-dialogue-action > div {
display: flex;
justify-content: flex-end;
justify-content: flex-start;
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 {
@@ -801,6 +833,6 @@ body.absence-wizard-open {
.teacher-dialogue-action {
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/schedule-quality.css">
<link rel="stylesheet" href="css/schedule-versions.css">
<link rel="stylesheet" href="../ui-foundation.css">
</head>
<body>
@@ -78,7 +79,7 @@
<!-- Раздел: Расписание -->
<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">
<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-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">
<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-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">
<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>${escapeHtml(subjectRowLabel(row))}</span>
<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-subject="${escapeHtml(String(row.subjectId))}"
aria-label="Убрать ${escapeHtml(subjectRowLabel(row))}">×</button>

View File

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

View File

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

View File

@@ -990,7 +990,7 @@ export async function initSchedule() {
${subgroupText ? `<small>${escapeHtml(subgroupText)}</small>` : ''}
${meta ? `<small>${escapeHtml(meta)}</small>` : ''}
<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-slot-id="${escapeHtml(String(lesson.slotId || ''))}"
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>
${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>`}
</td>
</tr>

View File

@@ -61,7 +61,7 @@ html {
}
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);
color: var(--text-primary);
min-height: 100vh;

View File

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

View File

@@ -108,7 +108,7 @@
<h3 id="time-slot-scope-modal-title">Новая ручная сетка</h3>
<p class="settings-modal-subtitle">Создайте отдельную сетку времени, которую затем можно назначать датам в календарном графике.</p>
</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">
<line x1="18" y1="6" x2="6" 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-content card calendar-day-dialog-card">
<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>
<span>Дата</span>

View File

@@ -70,7 +70,7 @@
<div class="modal-overlay" id="modal-edit-classroom">
<div class="modal-content card">
<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">
<input type="hidden" id="edit-classroom-id">
<div class="form-row" style="margin-top: 1rem;">
@@ -116,7 +116,7 @@
<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;">
<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>
<!-- Форма добавления -->

View File

@@ -91,7 +91,7 @@
<!-- Свободные аудитории -->
<div class="card dashboard-compact-card">
<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>
</div>

View File

@@ -118,7 +118,7 @@
<div class="modal-overlay" id="modal-edit-group">
<div class="modal-content card edit-group-modal">
<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">
<input type="hidden" id="edit-group-id">
<div class="form-row edit-group-form-row edit-group-form-row-main">
@@ -177,7 +177,7 @@
<h2>Подгруппы группы</h2>
<div id="subgroups-group-context" style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;"></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>
<!-- Форма -->
@@ -233,7 +233,7 @@
<h2>Календарные графики группы</h2>
<div id="calendar-group-context" style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;"></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>
<!-- Форма -->

View File

@@ -71,7 +71,7 @@
<div class="version-modal" id="version-modal" hidden>
<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>
<h3 id="version-modal-title">Версия расписания</h3>
<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-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>
<h3 id="schedule-draft-title">Создать черновик</h3>
<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-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">
<span class="schedule-conflict-kicker">Конфликт слота</span>
<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-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">
<span class="schedule-conflict-kicker">Перенос слота</span>
<h2 id="schedule-move-slot-title">Изменить день и пару</h2>

View File

@@ -75,7 +75,7 @@
<h2>Преподаватели дисциплины</h2>
<div id="subject-teachers-context" style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;"></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>
<!-- Форма -->

View File

@@ -169,7 +169,7 @@
<div class="modal-overlay" id="modal-edit-department">
<div class="modal-content card">
<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">
<input type="hidden" id="edit-dept-id">
<div class="form-row">
@@ -192,7 +192,7 @@
<div class="modal-overlay" id="modal-edit-specialty">
<div class="modal-content card">
<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">
<input type="hidden" id="edit-spec-id">
<div class="form-row">
@@ -219,7 +219,7 @@
<h2>Профили обучения</h2>
<div id="profiles-spec-context" style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 0.25rem;"></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>
<!-- Форма создания/редактирования профиля -->

View File

@@ -6,6 +6,7 @@
<meta name="description" content="Страница авторизации">
<title>Авторизация</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="ui-foundation.css">
</head>
<body>
<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 X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"

View File

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

View File

@@ -33,7 +33,7 @@ body {
min-height: 100vh;
background: var(--bg);
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 {

View File

@@ -110,7 +110,7 @@ html {
}
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);
color: var(--text-primary);
min-height: 100vh;

View File

@@ -220,7 +220,7 @@ import { trackUxEvent } from '../telemetry.js';
: 'Добавить отметку';
return `<button type="button" class="preference-cell ${typeClass}${pendingClass}"
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>
</button>`;
})
@@ -681,7 +681,7 @@ import { trackUxEvent } from '../telemetry.js';
${groups ? `<span class="chip">${escapeHtml(groups)}</span>` : ''}
</div>
${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>
` : '<span class="lesson-change-note">Занятие уже изменено учебным отделом</span>'}

View File

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

View File

@@ -207,7 +207,7 @@ body {
min-height: 100vh;
background: var(--bg);
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 {

View File

@@ -81,6 +81,12 @@ test('Docker-образ включает локальный telemetry bundle и
assert.match(dockerfile, /scripts\/build-vendor\.mjs/);
assert.match(dockerfile, /COPY --from=frontend-assets \/build\/dist\/vendor\//);
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.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;
}
}