polish(admin-dash): avatar pills, skeleton loader, mobile breakpoints, palette kept
- Avatar circles in top/worst-5 tables: initials from name, hsl color from hash of name - Structural skeleton on first load: 4 shimmer card boxes + 5 row placeholders (replaces LS.state.loading spinner for better layout-anchored feedback) - @media ≤640px: 2-column main grid, hero card reverts to normal size, quick-grid 2-col - Palette: existing per-card colors (violet/cyan/green/amber) already form a good muted hue family with vivid pink/amber for alert cards — kept as is to avoid regression Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -79,8 +79,27 @@
|
||||
.ov-quick-btn { display: flex; align-items: center; gap: 10px; padding: 14px 16px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; cursor: pointer; font-family: inherit; font-size: 0.88rem; font-weight: 600; color: var(--text); text-align: left; transition: background .12s, border-color .12s, transform .12s; }
|
||||
.ov-quick-btn:hover { background: rgba(155,93,229,0.06); border-color: rgba(155,93,229,0.3); color: var(--violet); transform: translateY(-1px); }
|
||||
.ov-quick-btn svg { width: 16px; height: 16px; flex-shrink: 0; }
|
||||
/* ── avatar pills ───────────────────────────────────────────── */
|
||||
.ov-avatar { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; font-size: 10px; font-weight: 700; color: #fff; margin-right: 6px; vertical-align: middle; flex-shrink: 0; }
|
||||
.ov-cell-user { display: flex; align-items: center; }
|
||||
/* ── skeleton loader ────────────────────────────────────────── */
|
||||
@keyframes ov-shimmer { 0%{background-position:-400px 0} 100%{background-position:400px 0} }
|
||||
.ov-skel-box { border-radius: 12px; background: linear-gradient(90deg,var(--border) 25%,var(--surface) 50%,var(--border) 75%); background-size: 400px 100%; animation: ov-shimmer 1.4s infinite linear; }
|
||||
.ov-skel-cards { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 16px; margin-bottom: 28px; }
|
||||
.ov-skel-card { height: 110px; }
|
||||
.ov-skel-rows { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; }
|
||||
.ov-skel-row { height: 38px; }
|
||||
/* ── misc ───────────────────────────────────────────────────── */
|
||||
.ov-empty { padding: 18px; text-align: center; color: var(--text-3); font-size: 0.85rem; }
|
||||
/* ── mobile breakpoints ─────────────────────────────────────── */
|
||||
@media (max-width: 640px) {
|
||||
.ov-grid.ov-grid-main { grid-template-columns: 1fr 1fr; }
|
||||
.ov-card.hero .ov-card-val { font-size: 1.9rem; }
|
||||
.ov-card.hero .ov-card-icon { width: 38px; height: 38px; border-radius: 12px; }
|
||||
.ov-results-grid { grid-template-columns: 1fr; }
|
||||
.ov-quick-grid { grid-template-columns: 1fr 1fr; }
|
||||
.ov-skel-cards { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
@@ -129,6 +148,45 @@
|
||||
return '<div class="ov-subj-bar-track">' + segs + '</div><div class="ov-subj-legend">' + legend + '</div>';
|
||||
}
|
||||
|
||||
/* ── avatar helpers ─────────────────────────────────────────────────── */
|
||||
function initialsOf(name) {
|
||||
if (!name) return '?';
|
||||
return name.trim().split(/\s+/).slice(0, 2).map(function (w) { return w[0] ? w[0].toUpperCase() : ''; }).join('') || '?';
|
||||
}
|
||||
|
||||
function hashHue(str) {
|
||||
var h = 0;
|
||||
for (var i = 0; i < (str || '').length; i++) {
|
||||
h = ((h << 5) - h + (str || '').charCodeAt(i)) | 0;
|
||||
}
|
||||
return Math.abs(h) % 360;
|
||||
}
|
||||
|
||||
function avatarHtml(name) {
|
||||
var hue = hashHue(name);
|
||||
var bg = 'hsl(' + hue + ',55%,60%)';
|
||||
return '<span class="ov-avatar" style="background:' + bg + '">' + initialsOf(name) + '</span>';
|
||||
}
|
||||
|
||||
/* ── skeleton loader ────────────────────────────────────────────────── */
|
||||
function renderSkeleton(el) {
|
||||
ensureOvStyles();
|
||||
el.innerHTML =
|
||||
'<div class="ov-skel-cards">' +
|
||||
'<div class="ov-skel-box ov-skel-card"></div>' +
|
||||
'<div class="ov-skel-box ov-skel-card"></div>' +
|
||||
'<div class="ov-skel-box ov-skel-card"></div>' +
|
||||
'<div class="ov-skel-box ov-skel-card"></div>' +
|
||||
'</div>' +
|
||||
'<div class="ov-skel-rows">' +
|
||||
'<div class="ov-skel-box ov-skel-row"></div>' +
|
||||
'<div class="ov-skel-box ov-skel-row"></div>' +
|
||||
'<div class="ov-skel-box ov-skel-row"></div>' +
|
||||
'<div class="ov-skel-box ov-skel-row"></div>' +
|
||||
'<div class="ov-skel-box ov-skel-row"></div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function pctClassNum(p) {
|
||||
if (p === null || p === undefined) return '';
|
||||
return p >= 75 ? 'hi' : p >= 50 ? 'mid' : 'lo';
|
||||
@@ -181,8 +239,9 @@
|
||||
|
||||
function renderSessionRows(sessions, e) {
|
||||
return sessions.map(function (s) {
|
||||
var name = s.user_name || '—';
|
||||
return '<tr>' +
|
||||
'<td>' + e(s.user_name || '—') + '</td>' +
|
||||
'<td><span class="ov-cell-user">' + avatarHtml(name) + e(name) + '</span></td>' +
|
||||
'<td>' + e(s.subject_name || '—') + '</td>' +
|
||||
'<td>' + (s.score != null ? s.score : 0) + ' / ' + (s.total != null ? s.total : 0) + '</td>' +
|
||||
'<td><span class="ov-pct ' + pctClassNum(s.percent) + '">' + (s.percent != null ? s.percent : '—') + '%</span></td>' +
|
||||
@@ -368,7 +427,7 @@
|
||||
async function load() {
|
||||
const el = document.getElementById('overview-content');
|
||||
if (!el) return;
|
||||
LS.state.loading(el, 'Загружаю обзор…');
|
||||
renderSkeleton(el);
|
||||
try {
|
||||
const data = await LS.adminGetOverview();
|
||||
_lastLoadTs = Date.now();
|
||||
|
||||
Reference in New Issue
Block a user