style(dashboard): визуальная полировка блока «Активность»

- hero-строка: крупное число занятий + тренд-пилюля со стрелкой
- сегментированный контрол масштаба (6н/12н/6м)
- ячейки тепловой карты: скруглённые квадраты, интенсивность через alpha, glow при наведении
- легенда типов — чипы-пилюли
- календарь «Месяц»: оттенок активных дней по нагрузке, пилюля стрика, мягкий ring сегодня
- паритет тёмной темы

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-04 13:22:01 +03:00
parent 8e8f54b41b
commit aee8597499
+87 -57
View File
@@ -293,14 +293,14 @@
} }
.act-tab:hover { color: var(--violet); } .act-tab:hover { color: var(--violet); }
.act-tab.active { background: var(--text); color: #fff; } .act-tab.active { background: var(--text); color: #fff; }
.act-scale-btns { display: flex; gap: 3px; } .act-scale-btns { display: inline-flex; gap: 2px; padding: 3px; border-radius: 10px; background: rgba(15,23,42,0.05); }
.act-scale-btn { .act-scale-btn {
padding: 2px 8px; border-radius: 6px; border: 1px solid rgba(15,23,42,0.08); padding: 3px 11px; border-radius: 7px; border: none;
font-family: 'Manrope', sans-serif; font-size: 0.62rem; font-weight: 700; font-family: 'Manrope', sans-serif; font-size: 0.64rem; font-weight: 700;
color: var(--text-3); background: transparent; cursor: pointer; transition: all 0.12s; color: var(--text-3); background: transparent; cursor: pointer; transition: all 0.15s;
} }
.act-scale-btn:hover { border-color: rgba(155,93,229,0.3); color: var(--violet); } .act-scale-btn:hover { color: var(--violet); }
.act-scale-btn.active { background: rgba(155,93,229,0.08); border-color: rgba(155,93,229,0.25); color: var(--violet); } .act-scale-btn.active { background: #fff; color: var(--violet); box-shadow: 0 1px 4px rgba(15,23,42,0.1); }
.act-pane { display: none; } .act-pane { display: none; }
.act-pane.visible { display: block; } .act-pane.visible { display: block; }
@@ -311,21 +311,32 @@
.hm-weekdays { display: flex; flex-direction: column; gap: 2px; width: 22px; flex-shrink: 0; padding-top: 1px; } .hm-weekdays { display: flex; flex-direction: column; gap: 2px; width: 22px; flex-shrink: 0; padding-top: 1px; }
.hm-wd { font-size: 0.5rem; font-weight: 700; color: var(--text-3); height: 14px; line-height: 14px; } .hm-wd { font-size: 0.5rem; font-weight: 700; color: var(--text-3); height: 14px; line-height: 14px; }
.mini-heatmap { display: grid; grid-template-rows: repeat(7, 14px); grid-auto-flow: column; grid-auto-columns: 14px; gap: 2px; } .mini-heatmap { display: grid; grid-template-rows: repeat(7, 14px); grid-auto-flow: column; grid-auto-columns: 14px; gap: 2px; }
.hm-trend { font-size: 0.7rem; font-weight: 700; color: var(--text-3); } /* Hero stat row */
.hm-trend.up { color: #059652; } .hm-hero { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 13px; }
.hm-trend.down { color: #E0335E; } .hm-hero-num-row { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
.hm-hero-num { font-family: 'Unbounded', sans-serif; font-size: 1.72rem; font-weight: 800; color: var(--text); line-height: 1; }
.hm-hero-unit { font-size: 0.76rem; font-weight: 700; color: var(--text-3); }
.hm-hero-sub { font-size: 0.72rem; color: var(--text-3); font-weight: 600; margin-top: 6px; }
.hm-hero-sub strong { color: var(--text); font-weight: 800; }
.hm-trend-pill { display: inline-flex; align-items: center; gap: 3px; padding: 3px 9px; border-radius: 99px;
font-size: 0.66rem; font-weight: 800; font-family: 'Manrope', sans-serif; white-space: nowrap; }
.hm-trend-pill svg { width: 12px; height: 12px; stroke-width: 2.6; }
.hm-trend-pill.up { background: rgba(5,150,82,0.1); color: #059652; }
.hm-trend-pill.down { background: rgba(224,51,94,0.1); color: #E0335E; }
.hm-trend-pill.flat { background: rgba(15,23,42,0.05); color: var(--text-3); }
.hm-empty { padding: 28px 16px; text-align: center; color: var(--text-3); } .hm-empty { padding: 28px 16px; text-align: center; color: var(--text-3); }
.hm-empty-ic { color: var(--violet); opacity: .85; margin-bottom: 8px; } .hm-empty-ic { color: var(--violet); opacity: .85; margin-bottom: 8px; }
.hm-empty-t { font-weight: 700; color: var(--text); font-size: 0.92rem; margin-bottom: 4px; } .hm-empty-t { font-weight: 700; color: var(--text); font-size: 0.92rem; margin-bottom: 4px; }
.hm-empty-s { font-size: 0.78rem; line-height: 1.5; max-width: 320px; margin: 0 auto 12px; } .hm-empty-s { font-size: 0.78rem; line-height: 1.5; max-width: 320px; margin: 0 auto 12px; }
.hm-empty-cta { display: inline-block; padding: 8px 16px; border-radius: 9px; background: var(--violet); color: #fff; font-weight: 700; font-size: 0.82rem; text-decoration: none; } .hm-empty-cta { display: inline-block; padding: 8px 16px; border-radius: 9px; background: var(--violet); color: #fff; font-weight: 700; font-size: 0.82rem; text-decoration: none; }
.mhm-cell { .mhm-cell {
width: 14px; height: 14px; border-radius: 50%; background: rgba(15,23,42,0.05); width: 14px; height: 14px; border-radius: 4px; background: rgba(15,23,42,0.05);
cursor: pointer; transition: transform 0.12s, box-shadow 0.12s; cursor: pointer; transition: transform 0.12s ease, box-shadow 0.12s ease;
animation: hmCellIn 0.3s ease both; animation: hmCellIn 0.3s ease both;
} }
.mhm-cell:hover { transform: scale(1.45); z-index: 2; position: relative; } .mhm-cell.has-data { box-shadow: inset 0 0 0 0.5px rgba(15,23,42,0.06); }
.mhm-cell.has-data:hover { box-shadow: 0 0 8px var(--cell-color, rgba(155,93,229,0.5)); } .mhm-cell:hover { transform: scale(1.34); z-index: 2; position: relative; border-radius: 5px; }
.mhm-cell.has-data:hover { box-shadow: 0 3px 12px var(--cell-color, rgba(155,93,229,0.5)); }
@keyframes hmCellIn { from { opacity: 0; transform: scale(0.3); } to { opacity: 1; transform: scale(1); } } @keyframes hmCellIn { from { opacity: 0; transform: scale(0.3); } to { opacity: 1; transform: scale(1); } }
/* Subject-colored cells */ /* Subject-colored cells */
@@ -335,16 +346,18 @@
.mhm-cell.s-phys { --cell-color: rgba(245,158,11,0.85); } .mhm-cell.s-phys { --cell-color: rgba(245,158,11,0.85); }
.mhm-cell.s-mix { --cell-color: rgba(155,93,229,0.6); } .mhm-cell.s-mix { --cell-color: rgba(155,93,229,0.6); }
/* Heatmap footer stats */ /* Heatmap legend */
.hm-footer { .hm-legend-row {
display: flex; align-items: center; gap: 14px; margin-top: 8px; padding-top: 8px; display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
border-top: 1px solid rgba(15,23,42,0.05); margin-top: 13px; padding-top: 11px; border-top: 1px solid rgba(15,23,42,0.06);
font-size: 0.68rem; color: var(--text-3); font-weight: 600; flex-wrap: wrap;
} }
.hm-footer strong { color: var(--text); font-weight: 800; } .hm-legend { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.hm-legend { display: flex; align-items: center; gap: 4px; margin-left: auto; } .hm-legend-chip {
.hm-legend-dot { width: 10px; height: 10px; border-radius: 50%; } display: inline-flex; align-items: center; gap: 5px; padding: 2px 9px;
.hm-legend-label { font-size: 0.58rem; } border-radius: 99px; background: rgba(15,23,42,0.04);
font-size: 0.6rem; font-weight: 700; color: var(--text-3);
}
.hm-legend-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
/* Day popup */ /* Day popup */
.hm-day-popup { .hm-day-popup {
@@ -370,11 +383,14 @@
/* Dark mode */ /* Dark mode */
.app-layout.dark .act-tab.active { background: #E8ECF2; color: var(--text); } .app-layout.dark .act-tab.active { background: #E8ECF2; color: var(--text); }
.app-layout.dark .act-scale-btn { border-color: rgba(255,255,255,0.08); color: #6B7A8E; } .app-layout.dark .act-scale-btns { background: rgba(255,255,255,0.05); }
.app-layout.dark .act-scale-btn.active { background: rgba(155,93,229,0.12); border-color: rgba(155,93,229,0.3); color: var(--violet); } .app-layout.dark .act-scale-btn { color: #6B7A8E; }
.app-layout.dark .act-scale-btn.active { background: rgba(255,255,255,0.1); color: var(--violet); box-shadow: none; }
.app-layout.dark .mhm-cell { background: rgba(255,255,255,0.04); } .app-layout.dark .mhm-cell { background: rgba(255,255,255,0.04); }
.app-layout.dark .hm-footer { border-color: rgba(255,255,255,0.06); } .app-layout.dark .mhm-cell.has-data { box-shadow: inset 0 0 0 0.5px rgba(255,255,255,0.07); }
.app-layout.dark .hm-footer strong { color: #E8ECF2; } .app-layout.dark .hm-hero-num, .app-layout.dark .hm-hero-sub strong { color: #E8ECF2; }
.app-layout.dark .hm-legend-row { border-color: rgba(255,255,255,0.07); }
.app-layout.dark .hm-legend-chip { background: rgba(255,255,255,0.05); }
.app-layout.dark .hm-day-popup { background: #1A1D27; border-color: rgba(255,255,255,0.08); box-shadow: 0 12px 40px rgba(0,0,0,0.4); } .app-layout.dark .hm-day-popup { background: #1A1D27; border-color: rgba(255,255,255,0.08); box-shadow: 0 12px 40px rgba(0,0,0,0.4); }
.app-layout.dark .hm-day-popup .hdp-date, .app-layout.dark .hm-day-popup .hdp-subj { color: #E8ECF2; } .app-layout.dark .hm-day-popup .hdp-date, .app-layout.dark .hm-day-popup .hdp-subj { color: #E8ECF2; }
@@ -540,15 +556,15 @@
/* C2: Streak calendar */ /* C2: Streak calendar */
.streak-cal { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; } .streak-cal { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; }
.sc-day { .sc-day {
aspect-ratio: 1; border-radius: 6px; display: flex; align-items: center; justify-content: center; aspect-ratio: 1; border-radius: 8px; display: flex; align-items: center; justify-content: center;
font-size: 0.62rem; font-weight: 700; color: var(--text-3); background: rgba(15,23,42,0.03); font-size: 0.62rem; font-weight: 700; color: var(--text-3); background: rgba(15,23,42,0.03);
transition: transform 0.12s; transition: transform 0.12s, box-shadow 0.12s;
} }
.sc-day.today { border: 1.5px solid var(--violet); color: var(--violet); font-weight: 800; } .sc-day.today { box-shadow: inset 0 0 0 1.5px var(--violet); color: var(--violet); font-weight: 800; }
.sc-day.active { background: rgba(155,93,229,0.18); color: #7c3aed; } .sc-day.active { background: rgba(155,93,229,0.18); color: #7c3aed; }
.sc-day.active.today { background: var(--violet); color: #fff; } .sc-day.active.today { background: var(--violet); color: #fff; box-shadow: 0 3px 10px rgba(155,93,229,0.35); }
.sc-day.future { opacity: 0.3; } .sc-day.future { opacity: 0.3; }
.sc-day:hover { transform: scale(1.15); } .sc-day:hover { transform: scale(1.15); box-shadow: 0 4px 12px rgba(15,23,42,0.12); }
.sc-day.pulse { animation: scPulse 2s ease-in-out infinite; } .sc-day.pulse { animation: scPulse 2s ease-in-out infinite; }
@keyframes scPulse { @keyframes scPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(155,93,229,0.3); } 0%, 100% { box-shadow: 0 0 0 0 rgba(155,93,229,0.3); }
@@ -556,7 +572,7 @@
} }
.sc-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } .sc-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.sc-month { font-family: 'Unbounded', sans-serif; font-size: 0.7rem; font-weight: 800; color: var(--text); } .sc-month { font-family: 'Unbounded', sans-serif; font-size: 0.7rem; font-weight: 800; color: var(--text); }
.sc-streak-badge { font-family: 'Unbounded', sans-serif; font-size: 0.7rem; font-weight: 800; color: #F59E0B; display: flex; align-items: center; gap: 4px; } .sc-streak-badge { font-family: 'Unbounded', sans-serif; font-size: 0.68rem; font-weight: 800; color: #E8890B; display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border-radius: 99px; background: rgba(245,158,11,0.13); }
.sc-weekdays { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; margin-bottom: 3px; } .sc-weekdays { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; margin-bottom: 3px; }
.sc-wd { font-size: 0.56rem; color: var(--text-3); font-weight: 700; text-align: center; text-transform: uppercase; } .sc-wd { font-size: 0.56rem; color: var(--text-3); font-weight: 700; text-align: center; text-transform: uppercase; }
@@ -1778,13 +1794,6 @@
</div> </div>
<!-- Pane 1: Heatmap --> <!-- Pane 1: Heatmap -->
<div class="act-pane visible" id="act-heatmap-pane"> <div class="act-pane visible" id="act-heatmap-pane">
<div style="display:flex;justify-content:flex-end;margin-bottom:6px">
<div class="act-scale-btns" id="hm-scale-btns">
<button class="act-scale-btn" onclick="setHmScale(6,this)"></button>
<button class="act-scale-btn active" onclick="setHmScale(12,this)">12н</button>
<button class="act-scale-btn" onclick="setHmScale(26,this)"></button>
</div>
</div>
<div id="activity-heatmap"></div> <div id="activity-heatmap"></div>
</div> </div>
<!-- Pane 2: Streak calendar --> <!-- Pane 2: Streak calendar -->
@@ -3526,6 +3535,10 @@
const ACT_ORDER = ['test', 'exam', 'cards', 'lesson', 'live', 'homework']; const ACT_ORDER = ['test', 'exam', 'cards', 'lesson', 'live', 'homework'];
function _dayTotal(types) { let s = 0; for (const k in (types || {})) s += types[k] || 0; return s; } function _dayTotal(types) { let s = 0; for (const k in (types || {})) s += types[k] || 0; return s; }
function _domType(types) { let best = null, bn = -1; for (const k in (types || {})) { if (types[k] > bn) { bn = types[k]; best = k; } } return best; } function _domType(types) { let best = null, bn = -1; for (const k in (types || {})) { if (types[k] > bn) { bn = types[k]; best = k; } } return best; }
function _hexA(h, a) { const n = parseInt(String(h).slice(1), 16); return `rgba(${(n>>16)&255},${(n>>8)&255},${n&255},${a})`; }
const ICN_TREND_UP = '<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 17 9 11 13 15 21 7"/><polyline points="15 7 21 7 21 13"/></svg>';
const ICN_TREND_DOWN = '<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 7 9 13 13 9 21 17"/><polyline points="15 17 21 17 21 11"/></svg>';
const ICN_TREND_FLAT = '<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="12" x2="20" y2="12"/></svg>';
/* ══ WIDGET: Activity heatmap (all study types) ══════════════════════ */ /* ══ WIDGET: Activity heatmap (all study types) ══════════════════════ */
async function loadActivityWidget() { async function loadActivityWidget() {
@@ -3595,10 +3608,8 @@
let style; let style;
if (n && !isFuture) { if (n && !isFuture) {
const color = (ACT_TYPES[_domType(types)] || {}).color || '#9B5DE5'; const color = (ACT_TYPES[_domType(types)] || {}).color || '#9B5DE5';
const alpha = Math.min(1, 0.32 + Math.log2(n + 1) * 0.22); const alpha = Math.min(1, 0.42 + Math.log2(n + 1) * 0.2);
const sz = Math.min(14, 8 + Math.min(n, 3) * 2); style = ` style="background:${_hexA(color, alpha.toFixed(2))};--cell-color:${_hexA(color, 0.55)};animation-delay:${cellIdx * 4}ms"`;
const margin = (14 - sz) / 2;
style = ` style="background:${color};opacity:${alpha.toFixed(2)};width:${sz}px;height:${sz}px;margin:${margin}px;animation-delay:${cellIdx * 4}ms"`;
} else if (isFuture) { } else if (isFuture) {
style = ` style="opacity:0.15;animation-delay:${cellIdx * 4}ms"`; style = ` style="opacity:0.15;animation-delay:${cellIdx * 4}ms"`;
} else { } else {
@@ -3609,33 +3620,46 @@
} }
} }
// Footer: total + weekly trend + activity-type legend // Hero: total + weekly trend pill + scale control
const diff = thisWeek - lastWeek; const diff = thisWeek - lastWeek;
const trendTxt = diff > 0 ? `+${diff} к прошлой` : diff < 0 ? `${diff} к прошлой` : 'как на прошлой'; let trendCls, trendTxt, trendIcon;
const trendCls = diff > 0 ? 'up' : diff < 0 ? 'down' : ''; if (diff > 0) { trendCls = 'up'; trendTxt = `+${diff}`; trendIcon = ICN_TREND_UP; }
let footerHtml = `<span><strong>${totalAll}</strong> занятий за ${weeks} нед.</span>`; else if (diff < 0) { trendCls = 'down'; trendTxt = `${diff}`; trendIcon = ICN_TREND_DOWN; }
footerHtml += `<span>эта неделя <strong>${thisWeek}</strong> <span class="hm-trend ${trendCls}">${trendTxt}</span></span>`; else { trendCls = 'flat'; trendTxt = 'без изменений'; trendIcon = ICN_TREND_FLAT; }
footerHtml += `<div class="hm-legend">` + const wkWord = weeks === 26 ? '6 мес.' : `${weeks} нед.`;
ACT_ORDER.map(k => `<span class="hm-legend-dot" style="background:${ACT_TYPES[k].color}"></span><span class="hm-legend-label">${ACT_TYPES[k].label}</span>`).join('') + const scaleHtml = [[6,'6н'],[12,'12н'],[26,'']]
`</div>`; .map(([w,l]) => `<button class="act-scale-btn${w === weeks ? ' active' : ''}" onclick="setHmScale(${w},this)">${l}</button>`).join('');
const heroHtml =
`<div class="hm-hero"><div>` +
`<div class="hm-hero-num-row">` +
`<span class="hm-hero-num">${totalAll}</span>` +
`<span class="hm-hero-unit">занятий за ${wkWord}</span>` +
`<span class="hm-trend-pill ${trendCls}">${trendIcon}${trendTxt}</span>` +
`</div>` +
`<div class="hm-hero-sub">на этой неделе <strong>${thisWeek}</strong>, на прошлой <strong>${lastWeek}</strong></div>` +
`</div><div class="act-scale-btns">${scaleHtml}</div></div>`;
const legendHtml = `<div class="hm-legend-row"><div class="hm-legend">` +
ACT_ORDER.map(k => `<span class="hm-legend-chip"><span class="hm-legend-dot" style="background:${ACT_TYPES[k].color}"></span>${ACT_TYPES[k].label}</span>`).join('') +
`</div></div>`;
host.innerHTML = host.innerHTML =
heroHtml +
`<div class="hm-months">${monthHtml}</div>` + `<div class="hm-months">${monthHtml}</div>` +
`<div class="hm-body">` + `<div class="hm-body">` +
`<div class="hm-weekdays">${wdNames.map(w => `<div class="hm-wd">${w}</div>`).join('')}</div>` + `<div class="hm-weekdays">${wdNames.map(w => `<div class="hm-wd">${w}</div>`).join('')}</div>` +
`<div class="mini-heatmap">${cellsHtml}</div>` + `<div class="mini-heatmap">${cellsHtml}</div>` +
`</div>` + `</div>` +
`<div class="hm-footer">${footerHtml}</div>`; legendHtml;
setTimeout(() => { initHeatmapTooltip(); initHeatmapClick(); }, 50); setTimeout(() => { initHeatmapTooltip(); initHeatmapClick(); }, 50);
} }
/* ══ Heatmap scale switch ══════════════════════════════════════════ */ /* ══ Heatmap scale switch ══════════════════════════════════════════ */
function setHmScale(weeks, btn) { function setHmScale(weeks) {
_hmScale = weeks; _hmScale = weeks;
document.querySelectorAll('.act-scale-btn').forEach(b => b.classList.remove('active')); renderHeatmap(); // rebuilds the scale control with the active state
if (btn) btn.classList.add('active');
renderHeatmap();
} }
/* ══ Activity tab switch ═══════════════════════════════════════════ */ /* ══ Activity tab switch ═══════════════════════════════════════════ */
@@ -3881,7 +3905,13 @@
if (isFuture) cls += ' future'; if (isFuture) cls += ' future';
// Pulse today if no session yet // Pulse today if no session yet
if (isToday && !todayHasSession) cls += ' pulse'; if (isToday && !todayHasSession) cls += ' pulse';
html += `<div class="${cls}">${d}</div>`; // Shade active (non-today) days by how much was done
let st = '';
if (isActive && !isToday) {
const a = Math.min(0.42, 0.16 + Math.log2(_dayTotal(_activityDays[key]) + 1) * 0.1);
st = ` style="background:${_hexA('#9B5DE5', a.toFixed(2))}"`;
}
html += `<div class="${cls}"${st}>${d}</div>`;
} }
html += '</div>'; html += '</div>';
body.innerHTML = html; body.innerHTML = html;