Bootstrap 5.3 as CSS base + B3 theme layer; dialogs isolated as .dlg; offcanvas sidebar; icons via SVG assets + masks

This commit is contained in:
2026-10-02 23:44:00 +03:00
parent 24b72a2525
commit 9a4e11e5ba
12 changed files with 185 additions and 179 deletions

View File

@@ -16,6 +16,14 @@ body {
Цвет несёт смысл: акцент — действия и фокус, зелёный/янтарный — статусы защиты, Цвет несёт смысл: акцент — действия и фокус, зелёный/янтарный — статусы защиты,
винный — деструктивные действия и негативная тональность. */ винный — деструктивные действия и негативная тональность. */
:root { :root {
--bs-body-font-family: 'Inter', system-ui, -apple-system, sans-serif;
--bs-body-bg: #fafafa;
--bs-body-color: #131316;
--bs-border-color: #e6e6e9;
--bs-border-radius: 0.5rem;
--bs-border-radius-lg: 0.625rem;
--bs-link-color: #7a2333;
--bs-link-hover-color: #9d2b3c;
--c-bg: #fafafa; --c-bg: #fafafa;
--c-surface: #ffffff; --c-surface: #ffffff;
--c-text: #131316; --c-text: #131316;
@@ -281,30 +289,30 @@ tr:last-child td { border-bottom: none; }
.confirm-text { margin: 0; font-size: 0.875rem; white-space: pre-line; overflow-wrap: anywhere; } .confirm-text { margin: 0; font-size: 0.875rem; white-space: pre-line; overflow-wrap: anywhere; }
/* Modals */ /* Modals */
dialog.modal { dialog.dlg {
border: 1px solid var(--c-border); border-radius: var(--radius-lg); padding: 0; border: 1px solid var(--c-border); border-radius: var(--radius-lg); padding: 0;
background: var(--c-surface); color: var(--c-text); background: var(--c-surface); color: var(--c-text);
width: min(34rem, calc(100vw - 2rem)); width: min(34rem, calc(100vw - 2rem));
box-shadow: 0 16px 48px rgba(19, 19, 22, 0.28); box-shadow: 0 16px 48px rgba(19, 19, 22, 0.28);
} }
dialog.modal::backdrop { background: rgba(19, 19, 22, 0.5); } dialog.dlg::backdrop { background: rgba(19, 19, 22, 0.5); }
/* Пока открыта модалка — страница не прокручивается, позиция скролла сохраняется; /* Пока открыта модалка — страница не прокручивается, позиция скролла сохраняется;
прокрутка остаётся внутри .modal-body */ прокрутка остаётся внутри .modal-body */
html.modal-open body { html.modal-open body {
position: fixed; left: 0; right: 0; width: 100%; top: var(--modal-top, 0); position: fixed; left: 0; right: 0; width: 100%; top: var(--modal-top, 0);
padding-right: var(--modal-scrollbar, 0); padding-right: var(--modal-scrollbar, 0);
} }
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.5rem; border-bottom: 1px solid var(--c-border); } .dlg-header { display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.5rem; border-bottom: 1px solid var(--c-border); }
.modal-title { margin: 0; font-size: 1.0625rem; font-weight: 600; } .dlg-title { margin: 0; font-size: 1.0625rem; font-weight: 600; }
.modal-close { border: none; background: none; cursor: pointer; font-size: 1.25rem; color: var(--c-text-muted); padding: 0 0.25rem; } .dlg-close { border: none; background: none; cursor: pointer; font-size: 1.25rem; color: var(--c-text-muted); padding: 0 0.25rem; }
.modal-close:hover { color: var(--c-text); } .dlg-close:hover { color: var(--c-text); }
.modal-body { padding: 1.5rem; max-height: 70vh; overflow-y: auto; display: flex; flex-direction: column; } .dlg-body { padding: 1.5rem; max-height: 70vh; overflow-y: auto; display: flex; flex-direction: column; }
/* Кнопки действий — внизу справа; при увеличенной модалке пустота снизу не остаётся */ /* Кнопки действий — внизу справа; при увеличенной модалке пустота снизу не остаётся */
.modal-body > form.form-stack:last-child { flex: 1 1 auto; } .dlg-body > form.form-stack:last-child { flex: 1 1 auto; }
.modal-body > form.form-stack:last-child > button:last-child, .dlg-body > form.form-stack:last-child > button:last-child,
.modal-body > form.form-stack:last-child > .flex:last-child, .dlg-body > form.form-stack:last-child > .flex:last-child,
.modal-body.form-stack > button:last-child, .dlg-body.form-stack > button:last-child,
.modal-body.form-stack > .flex:last-child, .dlg-body.form-stack > .flex:last-child,
.modal-body > button:last-child, .modal-body > button:last-child,
.modal-body > .flex:last-child { .modal-body > .flex:last-child {
margin-top: auto; margin-top: auto;
@@ -462,8 +470,8 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami
.js-pub-count.is-hidden { display: none; } .js-pub-count.is-hidden { display: none; }
/* Увеличенный диалог (оценка + комментарии) */ /* Увеличенный диалог (оценка + комментарии) */
dialog.modal.modal-lg { width: min(72rem, calc(100vw - 2rem)); } dialog.dlg.dlg-lg { width: min(72rem, calc(100vw - 2rem)); }
dialog.modal.modal-lg .modal-body { max-height: 88vh; min-height: 62vh; } dialog.dlg.dlg-lg .dlg-body { max-height: 88vh; min-height: 62vh; }
.mini-btn svg, .cell-btn svg { width: 1rem; height: 1rem; } .mini-btn svg, .cell-btn svg { width: 1rem; height: 1rem; }
.chip { display: inline-block; margin: 2px 4px 2px 0; padding: 2px 8px; border-radius: var(--radius-sm); font-size: 11.5px; line-height: 1.5; } .chip { display: inline-block; margin: 2px 4px 2px 0; padding: 2px 8px; border-radius: var(--radius-sm); font-size: 11.5px; line-height: 1.5; }
@@ -738,28 +746,19 @@ th.th-filterable { padding-bottom: 0.35rem; }
.sort-row .sort-title { flex: 1 1 auto; } .sort-row .sort-title { flex: 1 1 auto; }
} }
/* ---- Мобильное меню: бургер в хэдере, ссылки в выезжающем сайдбаре справа ---- */ /* ---- Мобильное меню: бургер в хэдере, сайдбар справа (BS offcanvas) ---- */
.nav-burger { display: none; } .nav-burger { display: none; }
.mobile-nav { display: none; } #mobile-nav { --bs-offcanvas-width: 17rem; --bs-offcanvas-bg: var(--c-surface); --bs-offcanvas-border-color: var(--c-border); --bs-offcanvas-zindex: 120; }
.mobile-nav-overlay { display: none; } #mobile-nav .mobile-nav-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
#mobile-nav .mobile-nav-links { display: flex; flex-direction: column; gap: 0.25rem; }
#mobile-nav .nav-link { padding: 0.5rem 0.25rem; font-size: 1rem; }
#mobile-nav .mobile-nav-user { display: flex; flex-direction: column; gap: 0.75rem; margin-top: auto; padding-top: 1rem; border-top: 1px solid var(--c-border); }
.mobile-nav-overlay { position: fixed; inset: 0; z-index: 1040; background: rgba(0, 0, 0, 0.45); }
html.nav-open, html.nav-open body { overflow: hidden; }
@media (max-width: 720px) { @media (max-width: 720px) {
.nav { display: none; } .nav { display: none; }
.page-header .user { display: none; } .page-header .user { display: none; }
.page-header .theme-btn { margin-left: auto; } .page-header .theme-btn { margin-left: auto; }
.nav-burger { display: inline-flex; } .nav-burger { display: inline-flex; }
.mobile-nav {
display: flex; flex-direction: column; gap: 1rem;
position: fixed; top: 0; right: 0; bottom: 0; left: auto; width: 17rem; max-width: 86vw;
z-index: 120; background: var(--c-surface);
border-right: none; border-left: 1px solid var(--c-border);
padding: 1rem; overflow-y: auto;
transform: translateX(100%); transition: transform 0.2s;
} }
.mobile-nav.open { transform: none; }
.mobile-nav-overlay { display: block; position: fixed; inset: 0; z-index: 110; background: rgba(0, 0, 0, 0.45); }
.mobile-nav-head { display: flex; align-items: center; justify-content: space-between; }
.mobile-nav-links { display: flex; flex-direction: column; gap: 0.25rem; }
.mobile-nav-links .nav-link { padding: 0.5rem 0.25rem; font-size: 1rem; }
.mobile-nav-user { display: flex; flex-direction: column; gap: 0.75rem; margin-top: auto; padding-top: 1rem; border-top: 1px solid var(--c-border); }
}
html.nav-open, html.nav-open body { overflow: hidden; }

File diff suppressed because one or more lines are too long

View File

@@ -166,7 +166,7 @@ function attachCommentAutocomplete(input) {
/* Если снизу внутри модалки мало места — раскрываем список вверх, чтобы он не обрезался. */ /* Если снизу внутри модалки мало места — раскрываем список вверх, чтобы он не обрезался. */
function positionList() { function positionList() {
var wrap = input.parentNode; var wrap = input.parentNode;
var scrollParent = input.closest('.modal-body') || document.documentElement; var scrollParent = input.closest('.dlg-body') || document.documentElement;
var wr = wrap.getBoundingClientRect(); var wr = wrap.getBoundingClientRect();
var sr = scrollParent.getBoundingClientRect(); var sr = scrollParent.getBoundingClientRect();
var below = sr.bottom - wr.bottom; var below = sr.bottom - wr.bottom;

View File

@@ -202,7 +202,7 @@
document.addEventListener('click', function (e) { document.addEventListener('click', function (e) {
var el = e.target; var el = e.target;
var close = el.closest('.modal-close'); var close = el.closest('.dlg-close');
if (close) { if (close) {
var dlg = close.closest('dialog'); var dlg = close.closest('dialog');
if (dlg) dlg.close(); if (dlg) dlg.close();
@@ -246,7 +246,7 @@
var n = document.getElementById('mobile-nav'); var n = document.getElementById('mobile-nav');
var o = document.getElementById('mobile-nav-overlay'); var o = document.getElementById('mobile-nav-overlay');
var b = document.getElementById('nav-burger'); var b = document.getElementById('nav-burger');
if (n) n.classList.add('open'); if (n) n.classList.add('show');
if (o) o.hidden = false; if (o) o.hidden = false;
if (b) b.setAttribute('aria-expanded', 'true'); if (b) b.setAttribute('aria-expanded', 'true');
document.documentElement.classList.add('nav-open'); document.documentElement.classList.add('nav-open');
@@ -255,7 +255,7 @@
var n = document.getElementById('mobile-nav'); var n = document.getElementById('mobile-nav');
var o = document.getElementById('mobile-nav-overlay'); var o = document.getElementById('mobile-nav-overlay');
var b = document.getElementById('nav-burger'); var b = document.getElementById('nav-burger');
if (n) n.classList.remove('open'); if (n) n.classList.remove('show');
if (o) o.hidden = true; if (o) o.hidden = true;
if (b) b.setAttribute('aria-expanded', 'false'); if (b) b.setAttribute('aria-expanded', 'false');
document.documentElement.classList.remove('nav-open'); document.documentElement.classList.remove('nav-open');

View File

@@ -148,12 +148,12 @@
</div> </div>
<!-- Диалог: оценка + комментарии --> <!-- Диалог: оценка + комментарии -->
<dialog id="modal-grade" class="modal modal-lg"> <dialog id="modal-grade" class="dlg dlg-lg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title" id="grade-title">Оценка и комментарии</h2> <h2 class="dlg-title" id="grade-title">Оценка и комментарии</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" id="grade-form" th:action="@{/admin/groups/{id}/grades(id=${group.id})}" method="post"> <form class="form-stack" id="grade-form" th:action="@{/admin/groups/{id}/grades(id=${group.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="studentId" id="grade-student"> <input type="hidden" name="studentId" id="grade-student">
@@ -173,12 +173,12 @@
<!-- Диалог: индивидуальное задание (защита) --> <!-- Диалог: индивидуальное задание (защита) -->
<!-- Диалог: назначение варианта --> <!-- Диалог: назначение варианта -->
<dialog id="modal-variant" class="modal modal-lg"> <dialog id="modal-variant" class="dlg dlg-lg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Назначить вариант</h2> <h2 class="dlg-title">Назначить вариант</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body form-stack"> <div class="dlg-body form-stack">
<p class="text-sm text-muted" id="variant-who">—</p> <p class="text-sm text-muted" id="variant-who">—</p>
<div><label class="label" for="variant-level">Уровень</label> <div><label class="label" for="variant-level">Уровень</label>
<select class="input" id="variant-level"></select></div> <select class="input" id="variant-level"></select></div>
@@ -209,12 +209,12 @@
</div> </div>
</dialog> </dialog>
<dialog id="modal-defense" class="modal"> <dialog id="modal-defense" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Индивидуальное задание</h2> <h2 class="dlg-title">Индивидуальное задание</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" id="defense-form" th:action="@{/admin/defenses/save}" method="post"> <form class="form-stack" id="defense-form" th:action="@{/admin/defenses/save}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="groupId" th:value="${group.id}"> <input type="hidden" name="groupId" th:value="${group.id}">
@@ -257,12 +257,12 @@
<!-- Диалог добавления студента --> <!-- Диалог добавления студента -->
<dialog id="modal-add-student" class="modal"> <dialog id="modal-add-student" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Добавить студента</h2> <h2 class="dlg-title">Добавить студента</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" id="enroll-form" method="post" <form class="form-stack" id="enroll-form" method="post"
th:action="@{/admin/groups/{gid}/students/enroll(gid=${group.id})}"> th:action="@{/admin/groups/{gid}/students/enroll(gid=${group.id})}">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
@@ -336,12 +336,12 @@
</div> </div>
<!-- Большая модалка «Выдача вариантов»: содержимое подгружается фрагментом по лабе --> <!-- Большая модалка «Выдача вариантов»: содержимое подгружается фрагментом по лабе -->
<dialog id="modal-lab-assign" class="modal modal-lg"> <dialog id="modal-lab-assign" class="dlg dlg-lg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title" id="assign-modal-title">Выдача вариантов</h2> <h2 class="dlg-title" id="assign-modal-title">Выдача вариантов</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body" id="assign-modal-body"></div> <div class="dlg-body" id="assign-modal-body"></div>
</dialog> </dialog>
<script th:src="@{/js/admin-group.js(v='15')}" defer></script> <script th:src="@{/js/admin-group.js(v='15')}" defer></script>

View File

@@ -27,12 +27,12 @@
</table> </table>
</div> </div>
<dialog id="modal-create-group" class="modal"> <dialog id="modal-create-group" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Новая группа</h2> <h2 class="dlg-title">Новая группа</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/groups}" method="post"> <form class="form-stack" th:action="@{/admin/groups}" method="post">
<div><label class="label" for="name">Название</label> <div><label class="label" for="name">Название</label>
<input class="input" id="name" name="name" placeholder="ПИ-11БО" required autofocus></div> <input class="input" id="name" name="name" placeholder="ПИ-11БО" required autofocus></div>
@@ -42,12 +42,12 @@
</div> </div>
</dialog> </dialog>
<dialog id="modal-import-group" class="modal"> <dialog id="modal-import-group" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Импорт студентов</h2> <h2 class="dlg-title">Импорт студентов</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/groups/import}" method="post" enctype="multipart/form-data"> <form class="form-stack" th:action="@{/admin/groups/import}" method="post" enctype="multipart/form-data">
<p class="text-sm text-muted">Файл .xls, .xlsx или .csv со столбцами: студенческий номер, ФИО, основа обучения. Логин — студенческий номер, пароль сгенерируется и покажется после импорта.</p> <p class="text-sm text-muted">Файл .xls, .xlsx или .csv со столбцами: студенческий номер, ФИО, основа обучения. Логин — студенческий номер, пароль сгенерируется и покажется после импорта.</p>
<div><label class="label" for="file">Файл со списком</label> <div><label class="label" for="file">Файл со списком</label>

View File

@@ -128,12 +128,12 @@
</div> </div>
<div th:each="level : ${lab.levels}" th:remove="tag"> <div th:each="level : ${lab.levels}" th:remove="tag">
<dialog th:id="|modal-level-rename-${level.id}|" class="modal"> <dialog th:id="|modal-level-rename-${level.id}|" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Переименовать уровень</h2> <h2 class="dlg-title">Переименовать уровень</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/labs/{labId}/levels/save(labId=${lab.id})}" method="post"> <form class="form-stack" th:action="@{/admin/labs/{labId}/levels/save(labId=${lab.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="levelId" th:value="${level.id}"> <input type="hidden" name="levelId" th:value="${level.id}">
@@ -144,12 +144,12 @@
</form> </form>
</div> </div>
</dialog> </dialog>
<dialog th:id="|modal-variant-add-${level.id}|" class="modal"> <dialog th:id="|modal-variant-add-${level.id}|" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title" th:text="|Новый вариант — ${level.title}|">Новый вариант</h2> <h2 class="dlg-title" th:text="|Новый вариант — ${level.title}|">Новый вариант</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/labs/{labId}/variants/save(labId=${lab.id})}" <form class="form-stack" th:action="@{/admin/labs/{labId}/variants/save(labId=${lab.id})}"
method="post"> method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
@@ -168,12 +168,12 @@
</div> </div>
</dialog> </dialog>
<th:block th:each="v : ${level.variants}"> <th:block th:each="v : ${level.variants}">
<dialog th:id="|modal-variant-edit-${v.id}|" class="modal"> <dialog th:id="|modal-variant-edit-${v.id}|" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title" th:text="|Вариант ${v.number}|">Вариант</h2> <h2 class="dlg-title" th:text="|Вариант ${v.number}|">Вариант</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/labs/{labId}/variants/save(labId=${lab.id})}" <form class="form-stack" th:action="@{/admin/labs/{labId}/variants/save(labId=${lab.id})}"
method="post"> method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">

View File

@@ -49,12 +49,12 @@
</div> </div>
<dialog id="modal-import-lab" class="modal"> <dialog id="modal-import-lab" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Импорт лабораторной</h2> <h2 class="dlg-title">Импорт лабораторной</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/labs/import}" method="post" enctype="multipart/form-data"> <form class="form-stack" th:action="@{/admin/labs/import}" method="post" enctype="multipart/form-data">
<div><label class="label" for="labFile">Файл</label> <div><label class="label" for="labFile">Файл</label>
<input class="input" id="labFile" type="file" name="file" accept=".zip" required></div> <input class="input" id="labFile" type="file" name="file" accept=".zip" required></div>
@@ -72,12 +72,12 @@
</div> </div>
</dialog> </dialog>
<dialog id="modal-create-lab" class="modal"> <dialog id="modal-create-lab" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Новая лабораторная</h2> <h2 class="dlg-title">Новая лабораторная</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/labs/create}" method="post"> <form class="form-stack" th:action="@{/admin/labs/create}" method="post">
<div class="grid grid-2"> <div class="grid grid-2">
<div><label class="label" for="labNumber">Номер</label> <div><label class="label" for="labNumber">Номер</label>

View File

@@ -15,17 +15,18 @@
</noscript> </noscript>
<link rel="icon" type="image/svg+xml" th:href="@{/favicon.svg(v='2')}"> <link rel="icon" type="image/svg+xml" th:href="@{/favicon.svg(v='2')}">
<!-- v=4: сброс кэша стилей после редизайна; при изменении css поднимать версию --> <!-- v=4: сброс кэша стилей после редизайна; при изменении css поднимать версию -->
<link rel="stylesheet" th:href="@{/css/app.css(v='15')}"> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link id="hljs-light" rel="stylesheet" th:href="@{/css/hljs-github.css(v='15')}"> <link rel="stylesheet" th:href="@{/css/app.css(v='17')}">
<link id="hljs-dark" rel="stylesheet" th:href="@{/css/hljs-github-dark.css(v='15')}" disabled> <link id="hljs-light" rel="stylesheet" th:href="@{/css/hljs-github.css(v='17')}">
<script th:src="@{/js/highlight.min.js(v='15')}" defer></script> <link id="hljs-dark" rel="stylesheet" th:href="@{/css/hljs-github-dark.css(v='17')}" disabled>
<script th:src="@{/js/table-scroll.js(v='15')}" defer></script> <script th:src="@{/js/highlight.min.js(v='17')}" defer></script>
<script th:src="@{/js/tablekit.js(v='15')}" defer></script> <script th:src="@{/js/table-scroll.js(v='17')}" defer></script>
<script th:src="@{/js/theme-init.js(v='15')}"></script> <script th:src="@{/js/tablekit.js(v='17')}" defer></script>
<script th:src="@{/js/app.js(v='15')}" defer></script> <script th:src="@{/js/theme-init.js(v='17')}"></script>
<script th:src="@{/js/ui-state.js(v='15')}" defer></script> <script th:src="@{/js/app.js(v='17')}" defer></script>
<script th:src="@{/js/md-content.js(v='15')}" defer></script> <script th:src="@{/js/ui-state.js(v='17')}" defer></script>
<script th:src="@{/js/submit-guard.js(v='15')}" defer></script> <script th:src="@{/js/md-content.js(v='17')}" defer></script>
<script th:src="@{/js/submit-guard.js(v='17')}" defer></script>
</head> </head>
<body> <body>
<div class="page"> <div class="page">
@@ -64,10 +65,10 @@
</header> </header>
<div id="mobile-nav-overlay" class="mobile-nav-overlay" hidden></div> <div id="mobile-nav-overlay" class="mobile-nav-overlay" hidden></div>
<nav id="mobile-nav" class="mobile-nav" aria-label="Меню"> <nav id="mobile-nav" class="mobile-nav offcanvas offcanvas-end" aria-label="Меню" tabindex="-1">
<div class="mobile-nav-head"> <div class="mobile-nav-head">
<span class="logo">LMS<b>.</b></span> <span class="logo">LMS<b>.</b></span>
<button class="modal-close mobile-nav-close" type="button" aria-label="Закрыть меню">✕</button> <button class="dlg-close mobile-nav-close" type="button" aria-label="Закрыть меню">✕</button>
</div> </div>
<div class="mobile-nav-links nav"> <div class="mobile-nav-links nav">
<th:block th:if="${currentUser != null}"> <th:block th:if="${currentUser != null}">
@@ -116,12 +117,12 @@
<button class="toast-close" type="button" aria-label="Закрыть">✕</button> <button class="toast-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
</div> </div>
<dialog id="modal-confirm" class="modal"> <dialog id="modal-confirm" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title" id="confirm-title">Подтверждение</h2> <h2 class="dlg-title" id="confirm-title">Подтверждение</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<p class="confirm-text" id="confirm-text">—</p> <p class="confirm-text" id="confirm-text">—</p>
<div class="flex justify-end gap-2 mt-4"> <div class="flex justify-end gap-2 mt-4">
<button class="btn btn-secondary" type="button" id="confirm-cancel">Отмена</button> <button class="btn btn-secondary" type="button" id="confirm-cancel">Отмена</button>

View File

@@ -106,13 +106,13 @@
</div> </div>
<th:block th:if="${currentUser != null and currentUser.admin}"> <th:block th:if="${currentUser != null and currentUser.admin}">
<dialog th:id="|modal-rename-section-${section.id}|" class="modal"> <dialog th:id="|modal-rename-section-${section.id}|" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Переименовать раздел</h2> <h2 class="dlg-title">Переименовать раздел</h2>
<button class="modal-close" type="button" aria-label="Закрыть" <button class="dlg-close" type="button" aria-label="Закрыть"
th:attr="data-modal=|modal-rename-section-${section.id}|">✕</button> th:attr="data-modal=|modal-rename-section-${section.id}|">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post"> <form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${back}"> <input type="hidden" name="back" th:value="${back}">
@@ -122,13 +122,13 @@
</form> </form>
</div> </div>
</dialog> </dialog>
<dialog th:id="|modal-move-${section.id}|" class="modal"> <dialog th:id="|modal-move-${section.id}|" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Перенести статьи из раздела</h2> <h2 class="dlg-title">Перенести статьи из раздела</h2>
<button class="modal-close" type="button" aria-label="Закрыть" <button class="dlg-close" type="button" aria-label="Закрыть"
th:attr="data-modal=|modal-move-${section.id}|">✕</button> th:attr="data-modal=|modal-move-${section.id}|">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post"> <form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${back}"> <input type="hidden" name="back" th:value="${back}">
@@ -150,12 +150,12 @@
</div> </div>
</div> </div>
<dialog id="modal-rename-chapter" class="modal"> <dialog id="modal-rename-chapter" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Переименовать главу</h2> <h2 class="dlg-title">Переименовать главу</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/chapters/{id}/rename(id=${chapter.id})}" method="post"> <form class="form-stack" th:action="@{/admin/content/chapters/{id}/rename(id=${chapter.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${back}"> <input type="hidden" name="back" th:value="${back}">
@@ -166,12 +166,12 @@
</div> </div>
</dialog> </dialog>
<dialog id="modal-new-section" class="modal"> <dialog id="modal-new-section" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Новый раздел в главе</h2> <h2 class="dlg-title">Новый раздел в главе</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/sections/create}" method="post"> <form class="form-stack" th:action="@{/admin/content/sections/create}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="chapterId" th:value="${chapter.id}"> <input type="hidden" name="chapterId" th:value="${chapter.id}">

View File

@@ -108,13 +108,13 @@
</div> </div>
<th:block th:if="${currentUser != null and currentUser.admin}"> <th:block th:if="${currentUser != null and currentUser.admin}">
<dialog th:id="|modal-rename-section-${section.id}|" class="modal"> <dialog th:id="|modal-rename-section-${section.id}|" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Переименовать раздел</h2> <h2 class="dlg-title">Переименовать раздел</h2>
<button class="modal-close" type="button" aria-label="Закрыть" <button class="dlg-close" type="button" aria-label="Закрыть"
th:attr="data-modal=|modal-rename-section-${section.id}|">✕</button> th:attr="data-modal=|modal-rename-section-${section.id}|">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post"> <form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<div><label class="label">Название</label> <div><label class="label">Название</label>
@@ -123,13 +123,13 @@
</form> </form>
</div> </div>
</dialog> </dialog>
<dialog th:id="|modal-move-${section.id}|" class="modal"> <dialog th:id="|modal-move-${section.id}|" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Перенести статьи из раздела</h2> <h2 class="dlg-title">Перенести статьи из раздела</h2>
<button class="modal-close" type="button" aria-label="Закрыть" <button class="dlg-close" type="button" aria-label="Закрыть"
th:attr="data-modal=|modal-move-${section.id}|">✕</button> th:attr="data-modal=|modal-move-${section.id}|">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post"> <form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<p class="text-sm text-muted">Выберите раздел-приёмник: статьи перенесутся туда, а текущий раздел будет удалён.</p> <p class="text-sm text-muted">Выберите раздел-приёмник: статьи перенесутся туда, а текущий раздел будет удалён.</p>
@@ -148,13 +148,13 @@
</div> </div>
<th:block th:if="${currentUser != null and currentUser.admin}"> <th:block th:if="${currentUser != null and currentUser.admin}">
<dialog th:id="|modal-rename-chapter-${chapter.id}|" class="modal"> <dialog th:id="|modal-rename-chapter-${chapter.id}|" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Переименовать главу</h2> <h2 class="dlg-title">Переименовать главу</h2>
<button class="modal-close" type="button" aria-label="Закрыть" <button class="dlg-close" type="button" aria-label="Закрыть"
th:attr="data-modal=|modal-rename-chapter-${chapter.id}|">✕</button> th:attr="data-modal=|modal-rename-chapter-${chapter.id}|">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/chapters/{id}/rename(id=${chapter.id})}" method="post"> <form class="form-stack" th:action="@{/admin/content/chapters/{id}/rename(id=${chapter.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<div><label class="label">Название</label> <div><label class="label">Название</label>
@@ -166,12 +166,12 @@
</th:block> </th:block>
</div> </div>
<dialog id="modal-import" class="modal"> <dialog id="modal-import" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Импорт материалов (zip)</h2> <h2 class="dlg-title">Импорт материалов (zip)</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/import}" method="post" enctype="multipart/form-data"> <form class="form-stack" th:action="@{/admin/content/import}" method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<p class="text-sm text-muted"> <p class="text-sm text-muted">
@@ -192,12 +192,12 @@
</div> </div>
</dialog> </dialog>
<dialog id="modal-new-chapter" class="modal"> <dialog id="modal-new-chapter" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Новая глава</h2> <h2 class="dlg-title">Новая глава</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/chapters/create}" method="post"> <form class="form-stack" th:action="@{/admin/content/chapters/create}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<div><label class="label" for="chTitle">Название</label> <div><label class="label" for="chTitle">Название</label>
@@ -207,12 +207,12 @@
</div> </div>
</dialog> </dialog>
<dialog id="modal-new-article" class="modal"> <dialog id="modal-new-article" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Новая статья</h2> <h2 class="dlg-title">Новая статья</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/articles/create}" method="post"> <form class="form-stack" th:action="@{/admin/content/articles/create}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<div><label class="label" for="naChapter">Существующая глава и раздел</label> <div><label class="label" for="naChapter">Существующая глава и раздел</label>

View File

@@ -78,12 +78,12 @@
</table> </table>
</div> </div>
<dialog id="modal-rename-section" class="modal"> <dialog id="modal-rename-section" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Переименовать раздел</h2> <h2 class="dlg-title">Переименовать раздел</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post"> <form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${back}"> <input type="hidden" name="back" th:value="${back}">
@@ -94,12 +94,12 @@
</div> </div>
</dialog> </dialog>
<dialog id="modal-move-section" class="modal"> <dialog id="modal-move-section" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Перенести статьи и удалить раздел</h2> <h2 class="dlg-title">Перенести статьи и удалить раздел</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post"> <form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="back" th:value="${backParent}"> <input type="hidden" name="back" th:value="${backParent}">
@@ -118,12 +118,12 @@
</div> </div>
</dialog> </dialog>
<dialog id="modal-new-article" class="modal"> <dialog id="modal-new-article" class="dlg">
<div class="modal-header"> <div class="dlg-header">
<h2 class="modal-title">Новая статья в разделе</h2> <h2 class="dlg-title">Новая статья в разделе</h2>
<button class="modal-close" type="button" aria-label="Закрыть">✕</button> <button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
</div> </div>
<div class="modal-body"> <div class="dlg-body">
<form class="form-stack" th:action="@{/admin/content/articles/create}" method="post"> <form class="form-stack" th:action="@{/admin/content/articles/create}" method="post">
<input type="hidden" name="_csrf" th:value="${csrfToken}"> <input type="hidden" name="_csrf" th:value="${csrfToken}">
<input type="hidden" name="sectionId" th:value="${section.id}"> <input type="hidden" name="sectionId" th:value="${section.id}">