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:
+87
-57
@@ -293,14 +293,14 @@
|
||||
}
|
||||
.act-tab:hover { color: var(--violet); }
|
||||
.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 {
|
||||
padding: 2px 8px; border-radius: 6px; border: 1px solid rgba(15,23,42,0.08);
|
||||
font-family: 'Manrope', sans-serif; font-size: 0.62rem; font-weight: 700;
|
||||
color: var(--text-3); background: transparent; cursor: pointer; transition: all 0.12s;
|
||||
padding: 3px 11px; border-radius: 7px; border: none;
|
||||
font-family: 'Manrope', sans-serif; font-size: 0.64rem; font-weight: 700;
|
||||
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.active { background: rgba(155,93,229,0.08); border-color: rgba(155,93,229,0.25); color: var(--violet); }
|
||||
.act-scale-btn:hover { 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.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-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; }
|
||||
.hm-trend { font-size: 0.7rem; font-weight: 700; color: var(--text-3); }
|
||||
.hm-trend.up { color: #059652; }
|
||||
.hm-trend.down { color: #E0335E; }
|
||||
/* Hero stat row */
|
||||
.hm-hero { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 13px; }
|
||||
.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-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-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; }
|
||||
.mhm-cell {
|
||||
width: 14px; height: 14px; border-radius: 50%; background: rgba(15,23,42,0.05);
|
||||
cursor: pointer; transition: transform 0.12s, box-shadow 0.12s;
|
||||
width: 14px; height: 14px; border-radius: 4px; background: rgba(15,23,42,0.05);
|
||||
cursor: pointer; transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||||
animation: hmCellIn 0.3s ease both;
|
||||
}
|
||||
.mhm-cell:hover { transform: scale(1.45); z-index: 2; position: relative; }
|
||||
.mhm-cell.has-data:hover { box-shadow: 0 0 8px var(--cell-color, rgba(155,93,229,0.5)); }
|
||||
.mhm-cell.has-data { box-shadow: inset 0 0 0 0.5px rgba(15,23,42,0.06); }
|
||||
.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); } }
|
||||
|
||||
/* Subject-colored cells */
|
||||
@@ -335,16 +346,18 @@
|
||||
.mhm-cell.s-phys { --cell-color: rgba(245,158,11,0.85); }
|
||||
.mhm-cell.s-mix { --cell-color: rgba(155,93,229,0.6); }
|
||||
|
||||
/* Heatmap footer stats */
|
||||
.hm-footer {
|
||||
display: flex; align-items: center; gap: 14px; margin-top: 8px; padding-top: 8px;
|
||||
border-top: 1px solid rgba(15,23,42,0.05);
|
||||
font-size: 0.68rem; color: var(--text-3); font-weight: 600; flex-wrap: wrap;
|
||||
/* Heatmap legend */
|
||||
.hm-legend-row {
|
||||
display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
|
||||
margin-top: 13px; padding-top: 11px; border-top: 1px solid rgba(15,23,42,0.06);
|
||||
}
|
||||
.hm-footer strong { color: var(--text); font-weight: 800; }
|
||||
.hm-legend { display: flex; align-items: center; gap: 4px; margin-left: auto; }
|
||||
.hm-legend-dot { width: 10px; height: 10px; border-radius: 50%; }
|
||||
.hm-legend-label { font-size: 0.58rem; }
|
||||
.hm-legend { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
|
||||
.hm-legend-chip {
|
||||
display: inline-flex; align-items: center; gap: 5px; padding: 2px 9px;
|
||||
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 */
|
||||
.hm-day-popup {
|
||||
@@ -370,11 +383,14 @@
|
||||
|
||||
/* Dark mode */
|
||||
.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-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-btns { background: rgba(255,255,255,0.05); }
|
||||
.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 .hm-footer { border-color: rgba(255,255,255,0.06); }
|
||||
.app-layout.dark .hm-footer strong { color: #E8ECF2; }
|
||||
.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-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 .hdp-date, .app-layout.dark .hm-day-popup .hdp-subj { color: #E8ECF2; }
|
||||
|
||||
@@ -540,15 +556,15 @@
|
||||
/* C2: Streak calendar */
|
||||
.streak-cal { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; }
|
||||
.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);
|
||||
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.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: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; }
|
||||
@keyframes scPulse {
|
||||
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-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-wd { font-size: 0.56rem; color: var(--text-3); font-weight: 700; text-align: center; text-transform: uppercase; }
|
||||
|
||||
@@ -1778,13 +1794,6 @@
|
||||
</div>
|
||||
<!-- Pane 1: Heatmap -->
|
||||
<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)">6н</button>
|
||||
<button class="act-scale-btn active" onclick="setHmScale(12,this)">12н</button>
|
||||
<button class="act-scale-btn" onclick="setHmScale(26,this)">6м</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="activity-heatmap"></div>
|
||||
</div>
|
||||
<!-- Pane 2: Streak calendar -->
|
||||
@@ -3526,6 +3535,10 @@
|
||||
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 _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) ══════════════════════ */
|
||||
async function loadActivityWidget() {
|
||||
@@ -3595,10 +3608,8 @@
|
||||
let style;
|
||||
if (n && !isFuture) {
|
||||
const color = (ACT_TYPES[_domType(types)] || {}).color || '#9B5DE5';
|
||||
const alpha = Math.min(1, 0.32 + Math.log2(n + 1) * 0.22);
|
||||
const sz = Math.min(14, 8 + Math.min(n, 3) * 2);
|
||||
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"`;
|
||||
const alpha = Math.min(1, 0.42 + Math.log2(n + 1) * 0.2);
|
||||
style = ` style="background:${_hexA(color, alpha.toFixed(2))};--cell-color:${_hexA(color, 0.55)};animation-delay:${cellIdx * 4}ms"`;
|
||||
} else if (isFuture) {
|
||||
style = ` style="opacity:0.15;animation-delay:${cellIdx * 4}ms"`;
|
||||
} else {
|
||||
@@ -3609,33 +3620,46 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Footer: total + weekly trend + activity-type legend
|
||||
// Hero: total + weekly trend pill + scale control
|
||||
const diff = thisWeek - lastWeek;
|
||||
const trendTxt = diff > 0 ? `+${diff} к прошлой` : diff < 0 ? `${diff} к прошлой` : 'как на прошлой';
|
||||
const trendCls = diff > 0 ? 'up' : diff < 0 ? 'down' : '';
|
||||
let footerHtml = `<span><strong>${totalAll}</strong> занятий за ${weeks} нед.</span>`;
|
||||
footerHtml += `<span>эта неделя <strong>${thisWeek}</strong> <span class="hm-trend ${trendCls}">${trendTxt}</span></span>`;
|
||||
footerHtml += `<div class="hm-legend">` +
|
||||
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('') +
|
||||
`</div>`;
|
||||
let trendCls, trendTxt, trendIcon;
|
||||
if (diff > 0) { trendCls = 'up'; trendTxt = `+${diff}`; trendIcon = ICN_TREND_UP; }
|
||||
else if (diff < 0) { trendCls = 'down'; trendTxt = `${diff}`; trendIcon = ICN_TREND_DOWN; }
|
||||
else { trendCls = 'flat'; trendTxt = 'без изменений'; trendIcon = ICN_TREND_FLAT; }
|
||||
const wkWord = weeks === 26 ? '6 мес.' : `${weeks} нед.`;
|
||||
const scaleHtml = [[6,'6н'],[12,'12н'],[26,'6м']]
|
||||
.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 =
|
||||
heroHtml +
|
||||
`<div class="hm-months">${monthHtml}</div>` +
|
||||
`<div class="hm-body">` +
|
||||
`<div class="hm-weekdays">${wdNames.map(w => `<div class="hm-wd">${w}</div>`).join('')}</div>` +
|
||||
`<div class="mini-heatmap">${cellsHtml}</div>` +
|
||||
`</div>` +
|
||||
`<div class="hm-footer">${footerHtml}</div>`;
|
||||
legendHtml;
|
||||
|
||||
setTimeout(() => { initHeatmapTooltip(); initHeatmapClick(); }, 50);
|
||||
}
|
||||
|
||||
/* ══ Heatmap scale switch ══════════════════════════════════════════ */
|
||||
function setHmScale(weeks, btn) {
|
||||
function setHmScale(weeks) {
|
||||
_hmScale = weeks;
|
||||
document.querySelectorAll('.act-scale-btn').forEach(b => b.classList.remove('active'));
|
||||
if (btn) btn.classList.add('active');
|
||||
renderHeatmap();
|
||||
renderHeatmap(); // rebuilds the scale control with the active state
|
||||
}
|
||||
|
||||
/* ══ Activity tab switch ═══════════════════════════════════════════ */
|
||||
@@ -3881,7 +3905,13 @@
|
||||
if (isFuture) cls += ' future';
|
||||
// Pulse today if no session yet
|
||||
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>';
|
||||
body.innerHTML = html;
|
||||
|
||||
Reference in New Issue
Block a user