From 604fa7ac0b3fc20fa7fc4a5b752a73ef3bc2ad2c Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Mon, 22 Jun 2026 17:56:12 +0300 Subject: [PATCH] =?UTF-8?q?fix(sidebar):=20=D1=83=D0=B1=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BC=D0=B8=D0=B3=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=81?= =?UTF-8?q?=D1=81=D1=8B=D0=BB=D0=BE=D0=BA=20=C2=AB=D0=9F=D0=BE=D0=B4=D0=B3?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BA=20=D1=8D=D0=BA?= =?UTF-8?q?=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD=D1=83=C2=BB=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=20=D0=BE=D1=82=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ссылки exam-prep (/exam-prep/*) скрывались отдельным async-механизмом (/api/exam-prep/ tracks), не входящим в синхронный кэш-CSS → мелькали на долю секунды после обновления. Теперь hideDisabledFeatures кэширует точные хрефы скрытых треков в localStorage (ls_examhide), а _applyFeatureCss добавляет их в инъект-CSS синхронно из кэша на ранней загрузке (до сборки сайдбара). При включении трека он убирается из кэша → снова виден (re-apply _applyFeatureCss после свежего fetch). hideEmptySidebarGroups перенесён в конец hideDisabledFeatures (учитывает скрытие exam-prep). Co-Authored-By: Claude Opus 4.8 (1M context) --- js/api.js | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/js/api.js b/js/api.js index 7e1c6ea..66498f1 100644 --- a/js/api.js +++ b/js/api.js @@ -862,14 +862,21 @@ const FEATURE_WIDGETS = { на ранней загрузке (ДО построения сайдбара/виджетов) — против мигания (FOUC), затем обновляется по свежему /api/features. */ function _applyFeatureCss(feats) { - if (!feats) return; const sels = []; - for (const [key, hrefs] of Object.entries(FEATURE_HREFS)) { - if (feats[key] === false) { - hrefs.forEach(h => sels.push(`[href="${h}"]`)); - (FEATURE_WIDGETS[key] || []).forEach(s => sels.push(s)); + if (feats) { + for (const [key, hrefs] of Object.entries(FEATURE_HREFS)) { + if (feats[key] === false) { + hrefs.forEach(h => sels.push(`[href="${h}"]`)); + (FEATURE_WIDGETS[key] || []).forEach(s => sels.push(s)); + } } } + // Скрытые exam-prep треки (подготовка): кэш хрефов с прошлой загрузки — против мигания. + // /api/exam-prep/tracks асинхронен, поэтому держим точный список скрытых ссылок в кэше. + try { + JSON.parse(localStorage.getItem('ls_examhide') || '[]') + .forEach(h => sels.push(`[href="${h}"]`)); + } catch { /* пусто */ } const css = sels.length ? sels.join(',') + '{display:none !important}' : ''; let el = document.getElementById('ls-feat-hide'); if (!el) { @@ -879,13 +886,13 @@ function _applyFeatureCss(feats) { } el.textContent = css; // Геймификация: класс на (доступен раньше body) → kill-switch без мигания. - document.documentElement.classList.toggle('no-gamification', feats.gamification === false); + if (feats) document.documentElement.classList.toggle('no-gamification', feats.gamification === false); } /* Ранняя синхронная попытка из кэша прошлой загрузки — нет мигания на повторных заходах. (FEATURE_HREFS — const, поэтому этот вызов идёт ПОСЛЕ его объявления.) */ try { const _cachedFeats = JSON.parse(localStorage.getItem('ls_feat_cache') || 'null'); - if (_cachedFeats) _applyFeatureCss(_cachedFeats); + _applyFeatureCss(_cachedFeats); // применит и кэш фич, и кэш скрытых exam-prep ссылок } catch { /* нет кэша / приватный режим — просто ждём async */ } /* Прячет группы сайдбара (.sb-group), у которых не осталось ни одного видимого пункта, @@ -942,7 +949,6 @@ async function hideDisabledFeatures() { document.querySelector('[onclick*="tab-account"]')?.click(); } } - hideEmptySidebarGroups(); // после обновления видимости пунктов — спрятать пустые группы // Exam-prep track links (/exam-prep/): показываем только включённые // (exam_tracks.enabled) и доступные пользователю треки. /api/exam-prep/tracks @@ -952,10 +958,16 @@ async function hideDisabledFeatures() { try { const data = await apiFetch('/api/exam-prep/tracks'); const allowed = new Set((data.tracks || []).map(t => t.exam_key)); + // Собираем точные хрефы скрытых треков и кэшируем — чтобы на СЛЕДУЮЩЕЙ загрузке + // _applyFeatureCss спрятал их синхронно из кэша ещё до сборки сайдбара (без мигания). + const hide = []; examLinks.forEach(el => { - const m = (el.getAttribute('href') || '').match(/^\/exam-prep\/([^/?#]+)/); - if (m && !allowed.has(m[1])) el.style.display = 'none'; + const href = el.getAttribute('href') || ''; + const m = href.match(/^\/exam-prep\/([^/?#]+)/); + if (m && !allowed.has(m[1])) hide.push(href); }); + try { localStorage.setItem('ls_examhide', JSON.stringify(hide)); } catch {} + _applyFeatureCss(_featuresCache); // обновить