From 2ccdbc832f3b7d3c03149ae4eb41773c1256fca5 Mon Sep 17 00:00:00 2001
From: Vladislav Kopnin
Date: Fri, 2 Oct 2026 17:02:58 +0300
Subject: [PATCH] Group page: B3 fidelity (cell-main/cell-sub, defense code in
cell), class-based eye/focus, utilities; drop import button
---
web/src/main/resources/static/css/app.css | 84 ++++++++++---------
.../main/resources/static/js/admin-group.js | 17 ++--
.../main/resources/static/js/md-content.js | 3 +-
.../main/resources/templates/admin/group.html | 42 ++--------
4 files changed, 61 insertions(+), 85 deletions(-)
diff --git a/web/src/main/resources/static/css/app.css b/web/src/main/resources/static/css/app.css
index 1f9e48f..1fc75a2 100644
--- a/web/src/main/resources/static/css/app.css
+++ b/web/src/main/resources/static/css/app.css
@@ -102,21 +102,23 @@ a:not(.btn):hover { color: var(--c-text-link-hover); }
.breadcrumb .nav-sep { user-select: none; }
.check-list { display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.25rem; max-height: 12rem; overflow-y: auto; }
-/* Buttons — по спецификации Б3: настоящая рамка вместо box-shadow,
- компактный кегль 13px, бордо только на основном действии */
+/* Buttons — по спецификации Б3: основная кнопка залита акцентом,
+ вторичная и опасная — контурные. Компактный кегль 13px. */
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
- border: 1px solid transparent; border-radius: var(--radius);
+ border: 1px solid var(--c-accent); border-radius: var(--radius);
+ background: var(--c-accent); color: #ffffff;
padding: 0.5rem 1rem; font-size: 0.8125rem; font-weight: 500;
cursor: pointer; text-decoration: none;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.btn:active { opacity: 0.85; }
.btn:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }
+.btn:hover { background: var(--c-accent-hover); border-color: var(--c-accent-hover); }
.btn-primary { background: var(--c-accent); border-color: var(--c-accent); color: #ffffff; }
.btn-primary:hover { background: var(--c-accent-hover); border-color: var(--c-accent-hover); }
.btn-secondary { background: var(--c-surface); border-color: var(--c-border-strong); color: var(--c-text); }
-.btn-secondary:hover { background: var(--c-bg); }
+.btn-secondary:hover { background: var(--c-bg); border-color: var(--c-border-strong); color: var(--c-text); }
.btn-danger { background: var(--c-surface); border-color: #dcb9be; color: var(--c-accent); }
.btn-danger:hover { background: var(--c-accent-soft); border-color: var(--c-accent); }
@@ -153,40 +155,53 @@ a:not(.btn):hover { color: var(--c-text-link-hover); }
.meta b { color: var(--c-text); font-weight: 600; }
.actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.toolbar { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
-.toolbar .table-search { width: 260px; }
.stat-value { margin: 0.5rem 0 0; font-size: 1.875rem; font-weight: 600; }
.stat-title { font-size: 0.875rem; color: var(--c-text-muted); margin: 0; }
-.text-muted { color: var(--c-text-muted); }
-.opacity-30 { opacity: 0.3; }
-.text-center { text-align: center; }
-.text-sm { font-size: 0.875rem; }
-.font-mono { font-family: monospace; }
-
-/* Spacing */
+/* ---- Utilities: плоские классы поверх компонентов (в духе Bootstrap) ---- */
+.flex { display: flex; }
+.flex-wrap { flex-wrap: wrap; }
+.items-center { align-items: center; }
+.items-start { align-items: flex-start; }
+.justify-between { justify-content: space-between; }
+.justify-center { justify-content: center; }
+.m-0 { margin: 0; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.mt-6 { margin-top: 1.5rem; }
.mt-8 { margin-top: 2rem; }
+.mb-0 { margin-bottom: 0; }
+.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
+.mb-8 { margin-bottom: 2rem; }
+.ml-1 { margin-left: 0.25rem; }
.ml-2 { margin-left: 0.5rem; }
+.ml-auto { margin-left: auto; }
.p-0 { padding: 0; }
.p-4 { padding: 1rem; }
.p-6 { padding: 1.5rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
+.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; }
+.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
-
-/* Flex */
-.flex { display: flex; }
-.flex-wrap { flex-wrap: wrap; }
-.items-center { align-items: center; }
-.justify-between { justify-content: space-between; }
.w-full { width: 100%; }
+.w-260 { width: 260px; }
.overflow-x-auto { overflow-x: auto; }
+.text-sm { font-size: 0.875rem; }
+.text-muted { color: var(--c-text-muted); }
+.text-strong { font-weight: 600; color: var(--c-text); }
+.text-center { text-align: center; }
+.text-right { text-align: right; }
+.text-break { overflow-wrap: anywhere; }
+.font-mono { font-family: monospace; }
+.font-medium { font-weight: 500; }
+.font-semibold { font-weight: 600; }
+.opacity-30 { opacity: 0.3; }
+.offscreen { position: fixed; top: -1000px; left: 0; }
/* Badges — «этикетки»: строгая рамка, разрядка капсом; только индикаторы статуса */
.badge {
@@ -212,12 +227,9 @@ tr:last-child td { border-bottom: none; }
.alert-success { background: #dcfce7; color: #166534; }
.alert-warning { background: #fef3c7; color: #92400e; }
-.ml-2 { margin-left: 0.5rem; }
.list-pad { padding-left: 1.25rem; }
.import-card { border-color: rgba(16,185,129,0.4); }
.mini-alert { padding: 0.5rem 0.75rem; border-radius: var(--radius-xs); margin-bottom: 0.5rem; }
-.font-semibold { font-weight: 600; }
-
/* Theme toggle */
.theme-btn { padding: 0.25rem 0.5rem; font-size: 0.75rem; }
@@ -386,8 +398,8 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami
.ac-del:disabled { opacity: 0.3; cursor: not-allowed; }
.ac-empty { padding: 0.4rem 0.6rem; font-size: 0.875rem; color: var(--c-text-muted); }
/* Ячейка матрицы в стиле Б3: вариант и оценка — текстом, подстрока — сдача и защита с точкой */
-.cell-stack { display: flex; flex-direction: column; align-items: center; gap: 0.3rem; }
-.cell-top { display: flex; align-items: center; justify-content: center; gap: 0.4rem; }
+.cell-stack { display: flex; flex-direction: column; align-items: flex-start; gap: 0.375rem; }
+.cell-main { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; }
/* Публикация в ячейке — «глаз»-точка: ○ скрыто, ◉ опубликовано (нативный checkbox под капотом) */
.pub-toggle {
appearance: none; -webkit-appearance: none;
@@ -403,10 +415,10 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami
.pub-toggle:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }
.variant-btn {
display: inline-block; padding: 0; border: none; background: none;
- font-size: 0.8125rem; font-weight: 600; white-space: nowrap; cursor: pointer;
- color: var(--c-accent); text-decoration: none;
+ font-size: 0.84375rem; font-weight: 600; white-space: nowrap; cursor: pointer;
+ color: var(--c-text); text-decoration: none;
}
-.variant-btn:hover { text-decoration: underline; }
+.variant-btn:hover { color: var(--c-accent); text-decoration: underline; }
.variant-btn.manual { font-weight: 700; }
.variant-btn .cell-none { color: var(--c-text-muted); font-weight: 400; }
.variant-btn.manual .cell-none { font-weight: 600; }
@@ -417,7 +429,7 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami
}
.score-link:hover { color: var(--c-accent); text-decoration: underline; }
.score-link.none { color: var(--c-text-muted); font-weight: 400; }
-.cell-sub { display: flex; align-items: center; justify-content: center; gap: 0.75rem; font-size: 0.75rem; }
+.cell-sub { display: flex; align-items: center; gap: 0.875rem; flex-wrap: wrap; font-size: 0.75rem; }
.sub-link { color: var(--c-text-muted); text-decoration: none; font-variant-numeric: tabular-nums; }
.sub-link:hover { color: var(--c-text); text-decoration: underline; }
.sub-dim { color: var(--c-text-muted); }
@@ -448,9 +460,10 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami
background: var(--c-surface); color: var(--c-text); text-decoration: none; font-size: 0.85rem;
}
.mini-btn:hover { background: var(--c-bg); }
-.mini-btn.ok { color: var(--c-tone-pos); }
-.mini-btn.warn { color: #b45309; }
-.mini-btn.partial { color: var(--c-accent); }
+.js-pub-eye .eye-closed { display: none; }
+.js-pub-eye.is-empty .eye-open { display: none; }
+.js-pub-eye.is-empty .eye-closed { display: inline-block; }
+.js-pub-count.is-hidden { display: none; }
/* Увеличенный диалог (оценка + комментарии) */
dialog.modal.modal-lg { width: min(72rem, calc(100vw - 2rem)); }
@@ -489,12 +502,6 @@ dialog.modal.modal-lg .modal-body { max-height: 88vh; min-height: 62vh; }
.section-body { padding-bottom: 0.4rem; }
/* ---- Матрица группы и карточки защиты ---- */
-.m-0 { margin: 0; }
-.mb-8 { margin-bottom: 2rem; }
-.mb-0 { margin-bottom: 0; }
-.text-strong { font-weight: 600; color: var(--c-text); }
-.text-right { text-align: right; }
-.font-medium { font-weight: 500; }
.scroll-x { overflow-x: auto; }
.matrix { border-collapse: separate; border-spacing: 0; }
.matrix th {
@@ -511,7 +518,7 @@ dialog.modal.modal-lg .modal-body { max-height: 88vh; min-height: 62vh; }
.matrix td.student-cell { min-width: 12rem; }
.student-name { font-weight: 600; font-size: 0.84375rem; }
.student-cell .font-mono { margin-top: 0.125rem; }
-.lab-col-head { display: flex; flex-direction: column; align-items: center; gap: 0.375rem; }
+.lab-col-head { display: flex; flex-direction: column; align-items: flex-start; gap: 0.375rem; }
.lab-col-title {
max-width: 9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
font-size: 0.7rem; font-weight: 400; color: var(--c-text-muted); letter-spacing: 0;
@@ -576,7 +583,8 @@ dialog.modal.modal-lg .modal-body { max-height: 88vh; min-height: 62vh; }
.actions-cell .inline-form { display: inline-block; }
/* Действия столбиком: ровные одинаковые кнопки друг под другом */
.actions-col { display: inline-flex; flex-direction: column; gap: 0.35rem; align-items: stretch; min-width: 6.5rem; }
-.actions-col .btn { text-align: center; }
+.actions-col .inline-form { display: block; width: 100%; }
+.actions-col .btn { display: block; width: 100%; text-align: center; }
/* Монохромные SVG-иконки в ячейках матрицы (защита, оценка) */
.cell-ico {
width: 0.95rem; height: 0.95rem; display: block;
diff --git a/web/src/main/resources/static/js/admin-group.js b/web/src/main/resources/static/js/admin-group.js
index 6ff6fd3..e61e656 100644
--- a/web/src/main/resources/static/js/admin-group.js
+++ b/web/src/main/resources/static/js/admin-group.js
@@ -306,16 +306,13 @@ function updateEyeCounts() {
var total = parseInt(eye.getAttribute('data-total') || '0', 10);
var count = parseInt(eye.getAttribute('data-count') || '0', 10);
var all = eye.getAttribute('data-all') === 'true';
- eye.classList.remove('ok', 'partial', 'warn');
- eye.classList.add(all ? 'ok' : (count > 0 ? 'partial' : 'warn'));
- var on = eye.querySelector('.eye-open');
- var off = eye.querySelector('.eye-closed');
- if (on) on.style.display = count > 0 ? '' : 'none';
- if (off) off.style.display = count > 0 ? 'none' : '';
+ eye.classList.toggle('is-empty', count === 0);
+ eye.classList.toggle('is-all', all);
+ eye.classList.toggle('is-partial', count > 0 && !all);
var c = eye.querySelector('.js-pub-count');
if (c) {
c.textContent = count;
- c.style.display = (count > 0 && !all) ? '' : 'none';
+ c.classList.toggle('is-hidden', count === 0 || all);
}
eye.title = all
? 'Оценки опубликованы всей группе — нажмите, чтобы скрыть у всех'
@@ -363,7 +360,7 @@ function applyDefenseToCell(s) {
label.textContent = started ? ''
: (s.status == null ? '—'
: (s.status === 'ready' ? 'готова'
- : (s.code ? 'код' : 'ожидает')));
+ : (s.code ? 'код ' + s.code : 'ожидает')));
}
if (s.status === 'ready') {
btn.title = 'Защита завершена: ' + (s.readyReason === 'timeout' ? 'время вышло' : 'студент нажал «Готов»');
@@ -451,7 +448,7 @@ function defenseReasonText(reason) {
function defenseStatusText(status, code, seconds, reason) {
if (status === 'waiting') {
return code
- ? 'Задание назначено, код выдан — ждём входа студента.'
+ ? 'Задание назначено. Код входа: ' + code + ' — ждём входа студента.'
: 'Задание назначено, код ещё не выдан.';
}
if (status === 'started') return 'Студент выполняет работу, осталось ' + fmtSeconds(seconds) + '.';
@@ -598,7 +595,7 @@ function focusLab() {
if (!s) return;
var labId = s.value;
document.querySelectorAll('th.lab-col[data-lab], td.cell-lab[data-lab], .filter-row th[data-lab]').forEach(function (el) {
- el.style.display = (!labId || el.getAttribute('data-lab') === labId) ? '' : 'none';
+ el.toggleAttribute('hidden', !!labId && el.getAttribute('data-lab') !== labId);
});
}
diff --git a/web/src/main/resources/static/js/md-content.js b/web/src/main/resources/static/js/md-content.js
index f24b7e7..59773f0 100644
--- a/web/src/main/resources/static/js/md-content.js
+++ b/web/src/main/resources/static/js/md-content.js
@@ -4,8 +4,7 @@
var area = document.createElement('textarea');
area.value = text;
area.setAttribute('readonly', '');
- area.style.position = 'fixed';
- area.style.top = '-1000px';
+ area.className = 'offscreen';
document.body.appendChild(area);
area.select();
try { document.execCommand('copy'); } catch (e) { /* игнорируем */ }
diff --git a/web/src/main/resources/templates/admin/group.html b/web/src/main/resources/templates/admin/group.html
index 1e5a368..e33561a 100644
--- a/web/src/main/resources/templates/admin/group.html
+++ b/web/src/main/resources/templates/admin/group.html
@@ -16,7 +16,6 @@