feat(textbooks): Wave 3 — UX-фичи Алгебры 8 (+636 строк)

1. Ctrl+K поиск: модалка со списком, индексирует параграфы, виджеты, карточки, термины глоссария. Стрелками выбор, Enter переход
2. Клавишные шорткаты: 1-7 → §§, ←/→ навигация, Esc закрыть модалки, ? показать справку. Игнорируется при фокусе в input
3. Закладки: SVG-кнопка в углу каждой .card (filled/outlined), хранятся в LocalStorage algebra8_bookmarks. В сайдбаре раздел 'Мои закладки' с переходом и удалением
4. Глоссарий-tooltips: 13 терминов (арифметический корень, радикал, иррациональное, модуль, промежуток, интервал, отрезок, система, совокупность, двойное неравенство и др.). DOM-walker оборачивает термины в .gloss с подчёркиванием, hover показывает определение в floating-tooltip
5. Mini-map: фиксированная панель справа с точкой на каждый .card/.wg в активной секции, активная подсвечивается по скроллу, скрывается на ≤980px
6. 3-уровневая подсказка: 'Подсказка' рядом с 'Проверить' в simp4 и compare. Уровень 1: намёк, 2: шаг, 3: полный ответ (−5 очков)
7. Шпаргалка drawer на мобильном: hamburger-кнопка в шапке, sidebar выезжает справа на ≤980px (transform translateX)
This commit is contained in:
Maxim Dolgolyov
2026-05-27 12:18:11 +03:00
parent 1ee16a3a38
commit 898629a5b6
+639 -3
View File
@@ -475,6 +475,75 @@ input,select,textarea{font-family:inherit}
/* Task 2: geo proof animation badge */
.proof-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;background:linear-gradient(135deg,var(--ok),#059669);color:#fff;border-radius:8px;font-weight:700;font-size:.85rem;animation:badgeIn .4s cubic-bezier(.34,1.56,.64,1)}
@keyframes badgeIn{from{transform:scale(.5);opacity:0}to{transform:scale(1);opacity:1}}
/* ═══════════════════════════════════════════════
WAVE 3 — UX / NAVIGATION
═══════════════════════════════════════════════ */
/* Task 1: Ctrl+K Search modal */
.search-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);z-index:9998;padding-top:80px}
.search-modal.open{display:block;animation:fadeIn .2s ease}
.search-box{max-width:560px;margin:0 auto;background:var(--card);border-radius:14px;box-shadow:0 16px 50px rgba(0,0,0,.4);overflow:hidden}
.search-input{width:100%;padding:18px 22px;border:none;background:transparent;color:var(--text);font-size:1.05rem;font-family:inherit;outline:none;border-bottom:1px solid var(--border)}
.search-results{max-height:50vh;overflow-y:auto}
.search-result{padding:11px 22px;cursor:pointer;border-bottom:1px solid var(--border);transition:background .12s}
.search-result:hover,.search-result.selected{background:var(--pri-soft)}
.search-result-title{font-weight:700;font-size:.92rem;color:var(--text)}
.search-result-sub{font-size:.78rem;color:var(--muted);margin-top:2px}
.search-empty{padding:22px;text-align:center;color:var(--muted);font-size:.9rem}
.search-hint-badge{padding:4px 8px;background:rgba(255,255,255,.18);border-radius:6px;font-size:.72rem;font-weight:700;letter-spacing:.04em}
/* Task 2: Shortcuts modal */
.shortcuts-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);backdrop-filter:blur(3px);z-index:9998;align-items:center;justify-content:center}
.shortcuts-modal.open{display:flex;animation:fadeIn .2s ease}
.shortcuts-box{background:var(--card);border-radius:16px;padding:24px 28px;min-width:300px;max-width:400px;box-shadow:0 16px 50px rgba(0,0,0,.35)}
.shortcuts-box h3{font-size:1rem;font-weight:800;color:var(--pri2);margin-bottom:14px;border-bottom:1px solid var(--border);padding-bottom:10px}
.shortcut-row{display:flex;align-items:center;gap:10px;padding:6px 0;font-size:.88rem}
.shortcut-key{display:inline-flex;align-items:center;justify-content:center;min-width:32px;padding:3px 8px;background:var(--pri-soft);border:1px solid var(--border);border-radius:5px;font-family:monospace;font-size:.82rem;font-weight:700;color:var(--pri2)}
.shortcut-desc{color:var(--text);flex:1}
/* Task 3: Bookmarks */
.bm-btn{position:absolute;top:10px;right:10px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:7px;background:transparent;color:var(--muted);transition:color .15s,background .15s;z-index:2;padding:0}
.bm-btn:hover{background:var(--pri-soft);color:var(--pri)}
.bm-btn.saved{color:var(--pri)}
.bm-icon-outline{display:block}.bm-icon-filled{display:none}
.bm-btn.saved .bm-icon-outline{display:none}.bm-btn.saved .bm-icon-filled{display:block}
.sidecard-bm-row{display:flex;align-items:center;gap:6px;padding:5px 0;border-bottom:1px dashed var(--border);font-size:.8rem}
.sidecard-bm-row:last-child{border-bottom:none}
.sidecard-bm-title{flex:1;color:var(--text);font-weight:600;cursor:pointer}
.sidecard-bm-title:hover{color:var(--pri);text-decoration:underline}
.sidecard-bm-del{color:var(--fail);font-size:.78rem;cursor:pointer;padding:2px 5px;border-radius:4px}
.sidecard-bm-del:hover{background:var(--fail-bg)}
/* Task 4: Glossary tooltips */
.gloss{border-bottom:1.5px dashed var(--sec-acc,var(--pri));cursor:help;font-style:normal}
.gloss-tip{position:fixed;max-width:280px;padding:10px 14px;background:var(--card);border:1px solid var(--sec-acc,var(--pri));border-radius:10px;font-size:.82rem;line-height:1.5;box-shadow:0 8px 24px rgba(0,0,0,0.15);z-index:9990;display:none;pointer-events:none}
.gloss-tip.show{display:block;animation:tipIn .15s ease}
@keyframes tipIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
/* Task 5: Mini-map */
.minimap{position:fixed;right:16px;bottom:80px;display:flex;flex-direction:column;gap:6px;z-index:50;background:var(--card);padding:8px;border-radius:10px;border:1px solid var(--border);box-shadow:var(--sh)}
.mm-dot{width:10px;height:10px;border-radius:50%;background:rgba(0,0,0,.15);cursor:pointer;transition:transform .15s,background .15s}
.dark .mm-dot{background:rgba(255,255,255,.2)}
.mm-dot:hover{transform:scale(1.4);background:var(--sec-acc,var(--pri))}
.mm-dot.active{background:var(--sec-acc,var(--pri));transform:scale(1.3)}
@media(max-width:980px){.minimap{display:none}}
/* Task 6: Hint system */
.hint-box{background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border-left:4px solid var(--warn);padding:10px 14px;border-radius:9px;margin-top:10px;font-size:.88rem;animation:fadeIn .2s ease}
.dark .hint-box{background:linear-gradient(135deg,rgba(245,158,11,.18),rgba(233,30,99,.12));color:var(--text)}
.hint-level-badge{display:inline-block;padding:2px 7px;border-radius:5px;font-size:.72rem;font-weight:800;background:var(--warn);color:#451a03;margin-bottom:5px;letter-spacing:.04em}
/* Task 7: Mobile Cheatsheet Sidebar button */
#sidebar-btn{display:none}
@media(max-width:980px){
#sidebar-btn{display:inline-flex}
.col-side{position:fixed;right:0;top:0;width:300px;max-width:90vw;height:100vh;background:var(--card);box-shadow:-12px 0 32px rgba(0,0,0,.2);transform:translateX(100%);transition:transform .25s;z-index:1000;overflow-y:auto;padding:20px;display:block}
.col-side.open{transform:translateX(0)}
.col-side-close{display:block;position:absolute;top:12px;right:12px;z-index:2}
.col-side.side-open{transform:translateX(0)}
}
@media(min-width:981px){.col-side-close{display:none}.col-side{transform:none!important}}
</style>
</head>
<body>
@@ -486,11 +555,18 @@ input,select,textarea{font-family:inherit}
<div class="hdr-sub">Квадратные корни и их свойства. Действительные числа</div>
</div>
<div class="hdr-side">
<input id="search-inp" class="hdr-search" type="text" placeholder="Поиск...">
<button id="sidebar-btn" class="hdr-btn" onclick="toggleSidebar()" title="Шпаргалка">
<svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg>
Шпаргалка
</button>
<button id="side-open-btn" class="hdr-btn" onclick="openSidebar()" title="Шпаргалка" style="display:none">
<svg class="ic" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
<span>Шпаргалка</span>
</button>
<button id="search-open-btn" class="hdr-btn" onclick="openSearch()" title="Поиск (Ctrl+K)">
<svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg>
<span class="search-hint-badge">Ctrl+K</span>
</button>
<button id="theme-btn" class="hdr-btn" title="Сменить тему">
<svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
<span id="theme-lab">Тёмная</span>
@@ -588,7 +664,10 @@ input,select,textarea{font-family:inherit}
</div>
<aside class="col-side">
<aside class="col-side" id="col-side">
<button class="col-side-close btn small" onclick="toggleSidebar()" title="Закрыть" style="margin-bottom:10px">
<svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
<div id="sidebar-content"></div>
</aside>
</main>
@@ -607,6 +686,34 @@ input,select,textarea{font-family:inherit}
<span id="ach-text">Достижение!</span>
</div>
<!-- Wave 3: Search modal -->
<div id="search-modal" class="search-modal" onclick="if(event.target===this)closeSearch()">
<div class="search-box">
<input id="search-modal-input" class="search-input" type="text" placeholder="Поиск по параграфам, терминам, интерактивам..." autocomplete="off">
<div id="search-results" class="search-results"></div>
</div>
</div>
<!-- Wave 3: Shortcuts modal -->
<div id="shortcuts-modal" class="shortcuts-modal" onclick="if(event.target===this)closeShortcutsModal()">
<div class="shortcuts-box">
<h3>Горячие клавиши</h3>
<div class="shortcut-row"><span class="shortcut-key">Ctrl+K</span><span class="shortcut-desc">Открыть поиск</span></div>
<div class="shortcut-row"><span class="shortcut-key">17</span><span class="shortcut-desc">Перейти к §1–§6 или Финалу</span></div>
<div class="shortcut-row"><span class="shortcut-key"></span><span class="shortcut-desc">Предыдущий параграф</span></div>
<div class="shortcut-row"><span class="shortcut-key"></span><span class="shortcut-desc">Следующий параграф</span></div>
<div class="shortcut-row"><span class="shortcut-key">Esc</span><span class="shortcut-desc">Закрыть модалку / поиск</span></div>
<div class="shortcut-row"><span class="shortcut-key">?</span><span class="shortcut-desc">Показать эту справку</span></div>
<div style="margin-top:14px;text-align:right"><button class="btn small primary" onclick="closeShortcutsModal()">Закрыть</button></div>
</div>
</div>
<!-- Wave 3: Glossary tooltip -->
<div id="gloss-tip" class="gloss-tip"></div>
<!-- Wave 3: Minimap -->
<div id="minimap" class="minimap" title="Мини-карта секции"></div>
<script>
'use strict';
@@ -1100,7 +1207,7 @@ function openSidebar(){
function closeSidebar(){
const side = document.querySelector('.col-side');
const overlay = document.getElementById('side-overlay');
if(side){ side.classList.remove('side-open'); }
if(side){ side.classList.remove('side-open','open'); }
if(overlay){ overlay.classList.remove('show'); }
}
function initMobileSidebar(){
@@ -2691,6 +2798,7 @@ function buildP4(){
<span class="lab-mono" style="font-size:1.4rem"></span>
<input id="simp4-b" class="inp num" type="number" placeholder="b" style="width:60px">
<button class="btn primary" onclick="simp4Check()">Проверить</button>
<button class="btn" onclick="simp4Hint()">Подсказка</button>
<button class="btn" onclick="simp4Next()">Дальше</button>
</div>
<div class="row-c" style="margin-top:10px">
@@ -4189,5 +4297,533 @@ function finalSummary(){
}
</script>
<script>
'use strict';
/* ════════════════════════════════════════════════════════
WAVE 3 — UX / NAVIGATION
════════════════════════════════════════════════════════ */
/* ─── Task 1: Ctrl+K Search ─── */
const SEARCH_INDEX = [];
function buildSearchIndex(){
SEARCH_INDEX.length = 0;
// 1. Параграфы
PARAS.forEach(p=>{
SEARCH_INDEX.push({ title: p.num + ' ' + p.name, sub: p.sub || '', action: ()=>goTo(p.id) });
(p.topics||[]).forEach(t=>{
SEARCH_INDEX.push({ title: t, sub: p.num + ' ' + p.name, action: ()=>goTo(p.id) });
});
});
// 2. Интерактивы (wg-title из DOM)
document.querySelectorAll('.wg').forEach(wg=>{
const titleEl = wg.querySelector('.wg-title');
const badgeEl = wg.querySelector('.wg-badge');
if(titleEl){
const sec = wg.closest('.sec');
const secName = sec ? (sec.querySelector('.sec-h') ? sec.querySelector('.sec-h').textContent : '') : '';
SEARCH_INDEX.push({
title: titleEl.textContent.trim(),
sub: (badgeEl ? badgeEl.textContent.trim() + ' · ' : '') + secName,
action: ()=>{
const secId = sec ? sec.id.replace('sec-','') : null;
if(secId) goTo(secId);
setTimeout(()=>wg.scrollIntoView({behavior:'smooth',block:'center'}), 350);
}
});
}
});
// 3. Карточки (card-title)
document.querySelectorAll('.card').forEach(card=>{
const titleEl = card.querySelector('.card-title');
if(titleEl){
const sec = card.closest('.sec');
const secName = sec ? (sec.querySelector('.sec-h') ? sec.querySelector('.sec-h').textContent : '') : '';
SEARCH_INDEX.push({
title: titleEl.textContent.trim(),
sub: secName,
action: ()=>{
const secId = sec ? sec.id.replace('sec-','') : null;
if(secId) goTo(secId);
setTimeout(()=>card.scrollIntoView({behavior:'smooth',block:'center'}), 350);
}
});
}
});
// 4. Ключевые термины глоссария
Object.entries(GLOSSARY).forEach(([term, def])=>{
SEARCH_INDEX.push({
title: term,
sub: def.length > 60 ? def.slice(0,60) + '…' : def,
action: ()=>{}
});
});
}
let _searchIdx = -1;
function openSearch(){
const modal = document.getElementById('search-modal');
if(!modal) return;
modal.classList.add('open');
const inp = document.getElementById('search-modal-input');
if(inp){ inp.value = ''; inp.focus(); }
_searchIdx = -1;
_renderSearchResults('');
}
function closeSearch(){
const modal = document.getElementById('search-modal');
if(modal) modal.classList.remove('open');
}
function _renderSearchResults(q){
const box = document.getElementById('search-results');
if(!box) return;
const trimmed = q.trim().toLowerCase();
let items = SEARCH_INDEX;
if(trimmed){
items = SEARCH_INDEX.filter(it=>{
return it.title.toLowerCase().includes(trimmed) || it.sub.toLowerCase().includes(trimmed);
}).slice(0, 18);
} else {
items = SEARCH_INDEX.slice(0, 12);
}
if(!items.length){
box.innerHTML = '<div class="search-empty">Ничего не найдено</div>';
return;
}
box.innerHTML = items.map((it, i)=>`
<div class="search-result" data-idx="${i}" onclick="_searchPick(${i})">
<div class="search-result-title">${_highlightMatch(it.title, trimmed)}</div>
${it.sub ? `<div class="search-result-sub">${it.sub}</div>` : ''}
</div>
`).join('');
box.querySelectorAll('.search-result').forEach((el,i)=>{ el._searchItem = items[i]; });
}
function _highlightMatch(text, q){
if(!q) return text;
const idx = text.toLowerCase().indexOf(q);
if(idx < 0) return text;
return text.slice(0,idx) + '<mark style="background:var(--warn-bg);border-radius:3px">' + text.slice(idx, idx+q.length) + '</mark>' + text.slice(idx+q.length);
}
function _searchPick(i){
const box = document.getElementById('search-results');
if(!box) return;
const items = [...box.querySelectorAll('.search-result')];
if(items[i] && items[i]._searchItem){
items[i]._searchItem.action();
closeSearch();
}
}
function _initSearchInput(){
const inp = document.getElementById('search-modal-input');
if(!inp) return;
inp.addEventListener('input', ()=>{ _searchIdx = -1; _renderSearchResults(inp.value); });
inp.addEventListener('keydown', e=>{
const box = document.getElementById('search-results');
const rows = box ? [...box.querySelectorAll('.search-result')] : [];
if(e.key === 'ArrowDown'){
e.preventDefault();
_searchIdx = Math.min(_searchIdx + 1, rows.length - 1);
} else if(e.key === 'ArrowUp'){
e.preventDefault();
_searchIdx = Math.max(_searchIdx - 1, 0);
} else if(e.key === 'Enter'){
e.preventDefault();
if(_searchIdx >= 0 && rows[_searchIdx] && rows[_searchIdx]._searchItem){
rows[_searchIdx]._searchItem.action();
closeSearch();
} else if(rows[0] && rows[0]._searchItem){
rows[0]._searchItem.action();
closeSearch();
}
return;
}
rows.forEach((r,i)=>r.classList.toggle('selected', i === _searchIdx));
if(rows[_searchIdx]) rows[_searchIdx].scrollIntoView({block:'nearest'});
});
}
/* ─── Task 2: Keyboard shortcuts ─── */
const PARA_ORDER = ['p1','p2','p3','p4','p5','p6','final'];
function navNext(){
const idx = PARA_ORDER.indexOf(STATE.current);
if(idx >= 0 && idx < PARA_ORDER.length - 1) goTo(PARA_ORDER[idx + 1]);
}
function navPrev(){
const idx = PARA_ORDER.indexOf(STATE.current);
if(idx > 0) goTo(PARA_ORDER[idx - 1]);
}
function showShortcutsHelp(){
const m = document.getElementById('shortcuts-modal');
if(m) m.classList.add('open');
}
function closeShortcutsModal(){
const m = document.getElementById('shortcuts-modal');
if(m) m.classList.remove('open');
}
function _initKeyboard(){
document.addEventListener('keydown', e=>{
// Ctrl+K / Cmd+K — search
if((e.ctrlKey || e.metaKey) && e.key === 'k'){
e.preventDefault();
openSearch();
return;
}
// Escape — close modals
if(e.key === 'Escape'){
closeSearch();
closeShortcutsModal();
return;
}
// Ignore if inside input
const tag = e.target.tagName;
if(tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
// ? — shortcuts help
if(e.key === '?' || (e.shiftKey && e.key === '/')){
showShortcutsHelp();
return;
}
// 1-7 — go to para
const map = {'1':'p1','2':'p2','3':'p3','4':'p4','5':'p5','6':'p6','7':'final'};
if(map[e.key]){ goTo(map[e.key]); return; }
// Arrow keys — prev/next
if(e.key === 'ArrowRight'){ navNext(); return; }
if(e.key === 'ArrowLeft'){ navPrev(); return; }
});
}
/* ─── Task 3: Bookmarks ─── */
const BM_KEY = 'algebra8_bookmarks';
function loadBookmarks(){
try{ return JSON.parse(localStorage.getItem(BM_KEY) || '[]'); }catch(e){ return []; }
}
function saveBookmarks(bms){
try{ localStorage.setItem(BM_KEY, JSON.stringify(bms)); }catch(e){}
}
function _getCardId(card){
const sec = card.closest('.sec');
const secId = sec ? sec.id : 'unknown';
const allCards = sec ? [...sec.querySelectorAll('.card')] : [];
const idx = allCards.indexOf(card);
return secId + ':card-' + idx;
}
function _getCardTitle(card){
const t = card.querySelector('.card-title');
return t ? t.textContent.trim() : 'Карточка';
}
function _isBookmarked(id){
return loadBookmarks().some(b=>b.id === id);
}
function toggleBookmark(btn, card){
const id = _getCardId(card);
const title = _getCardTitle(card);
const sec = card.closest('.sec');
const para = sec ? sec.id.replace('sec-','') : 'p1';
let bms = loadBookmarks();
if(_isBookmarked(id)){
bms = bms.filter(b=>b.id !== id);
btn.classList.remove('saved');
} else {
bms.push({ id, title, para });
btn.classList.add('saved');
}
saveBookmarks(bms);
buildSidebar(STATE.current);
}
function _attachBookmarkButtons(){
document.querySelectorAll('.card').forEach(card=>{
if(card.querySelector('.bm-btn')) return; // already added
const id = _getCardId(card);
const saved = _isBookmarked(id);
const btn = document.createElement('button');
btn.className = 'bm-btn' + (saved ? ' saved' : '');
btn.title = 'Закладка';
btn.innerHTML = `
<svg class="ic bm-icon-outline" viewBox="0 0 24 24" style="width:16px;height:16px"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
<svg class="ic bm-icon-filled" viewBox="0 0 24 24" style="width:16px;height:16px;fill:var(--pri);stroke:var(--pri)"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
`;
btn.addEventListener('click', e=>{ e.stopPropagation(); toggleBookmark(btn, card); });
card.appendChild(btn);
});
}
/* Patch buildSidebar to include bookmarks section */
const _origBuildSidebar = window.buildSidebar;
window.buildSidebar = function(id){
_origBuildSidebar(id);
const box = document.getElementById('sidebar-content');
const bms = loadBookmarks();
if(bms.length > 0){
let html = `<div class="sidecard"><h4>Мои закладки <span style="color:var(--pri);float:right">${bms.length}</span></h4>`;
bms.slice().reverse().slice(0,8).forEach(b=>{
const PNAMES = {p1:'§1',p2:'§2',p3:'§3',p4:'§4',p5:'§5',p6:'§6',final:'Финал'};
html += `<div class="sidecard-bm-row">
<span class="sidecard-bm-title" onclick="goTo('${b.para}')" title="Перейти">${b.title} <small style="color:var(--muted)">${PNAMES[b.para]||b.para}</small></span>
<span class="sidecard-bm-del" onclick="_deleteBm('${b.id}')" title="Удалить">&#10005;</span>
</div>`;
});
html += '</div>';
box.insertAdjacentHTML('afterbegin', html);
}
};
function _deleteBm(id){
saveBookmarks(loadBookmarks().filter(b=>b.id !== id));
// Update bookmark button state if card is visible
document.querySelectorAll('.bm-btn').forEach(btn=>{
const card = btn.closest('.card');
if(card && _getCardId(card) === id) btn.classList.remove('saved');
});
buildSidebar(STATE.current);
}
/* ─── Task 4: Glossary tooltips ─── */
const GLOSSARY = {
'арифметический корень': 'Неотрицательное число, квадрат которого равен подкоренному. Обозначается √a, где a ≥ 0.',
'радикал': 'Знак квадратного корня √. От лат. radix — корень.',
'подкоренное': 'Выражение под знаком корня. Должно быть неотрицательным для извлечения арифм. корня.',
'иррациональное число': 'Число, которое нельзя представить в виде m/n. Бесконечная непериодическая десятичная дробь. Примеры: √2, π.',
'рациональное число': 'Число вида m/n, где m целое, n натуральное. Конечная или периодическая десятичная.',
'действительное число': 'Любое число на координатной прямой. Объединение рациональных и иррациональных. Обозначается ℝ.',
'модуль': 'Расстояние от числа до 0 на координатной прямой. |a| = a при a≥0, |a| = -a при a<0.',
'промежуток': 'Часть числовой прямой между двумя точками или один луч.',
'интервал': 'Открытый промежуток (a; b) — концы не включены.',
'отрезок': 'Закрытый промежуток [a; b] — концы включены.',
'система неравенств': 'Несколько неравенств с общим решением (пересечение). Обозначается {.',
'совокупность': 'Несколько неравенств; решение — объединение. Обозначается [.',
'двойное неравенство': 'Запись типа a < x < b эквивалентна системе { x > a; x < b }.',
};
function _applyGlossary(root){
if(!root) return;
const terms = Object.keys(GLOSSARY).sort((a,b)=>b.length-a.length);
const re = new RegExp('(' + terms.map(t=>t.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join('|') + ')', 'gi');
// Only process text nodes in card-body and wg (skip scripts, inputs, already-marked)
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node){
const p = node.parentElement;
if(!p) return NodeFilter.FILTER_REJECT;
const tag = p.tagName;
if(['SCRIPT','STYLE','INPUT','TEXTAREA','SELECT'].includes(tag)) return NodeFilter.FILTER_REJECT;
if(p.classList && p.classList.contains('gloss')) return NodeFilter.FILTER_REJECT;
if(p.classList && (p.classList.contains('katex') || p.classList.contains('katex-html'))) return NodeFilter.FILTER_REJECT;
// Only inside .card-body or .wg
if(!p.closest('.card-body') && !p.closest('.wg')) return NodeFilter.FILTER_REJECT;
if(!re.test(node.textContent)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
});
const nodes = [];
let n;
while((n = walker.nextNode())) nodes.push(n);
nodes.forEach(node=>{
re.lastIndex = 0;
const parts = node.textContent.split(re);
if(parts.length < 2) return;
const frag = document.createDocumentFragment();
parts.forEach(part=>{
re.lastIndex = 0;
if(re.test(part)){
const termKey = Object.keys(GLOSSARY).find(k=>k.toLowerCase() === part.toLowerCase());
const span = document.createElement('span');
span.className = 'gloss';
span.dataset.term = termKey || part.toLowerCase();
span.textContent = part;
frag.appendChild(span);
} else {
frag.appendChild(document.createTextNode(part));
}
re.lastIndex = 0;
});
node.parentNode.replaceChild(frag, node);
});
}
function _initGlossTooltip(){
const tip = document.getElementById('gloss-tip');
if(!tip) return;
document.addEventListener('mouseover', e=>{
const gloss = e.target.closest('.gloss');
if(!gloss){ return; }
const term = gloss.dataset.term;
const def = GLOSSARY[term] || GLOSSARY[Object.keys(GLOSSARY).find(k=>k.toLowerCase()===term)] || '';
if(!def) return;
tip.textContent = def;
tip.classList.add('show');
const r = gloss.getBoundingClientRect();
let top = r.bottom + 6;
let left = r.left;
if(top + 120 > window.innerHeight) top = r.top - 120;
if(left + 290 > window.innerWidth) left = window.innerWidth - 296;
if(left < 4) left = 4;
tip.style.top = top + 'px';
tip.style.left = left + 'px';
});
document.addEventListener('mouseout', e=>{
if(!e.target.closest('.gloss')) tip.classList.remove('show');
else if(e.relatedTarget && !e.relatedTarget.closest('.gloss')) tip.classList.remove('show');
});
}
/* ─── Task 5: Mini-map ─── */
let _mmScrollHandler = null;
function _buildMinimap(secId){
const mm = document.getElementById('minimap');
if(!mm) return;
const sec = document.getElementById('sec-' + secId);
if(!sec){ mm.innerHTML = ''; return; }
const targets = [...sec.querySelectorAll('.card, .wg')];
if(targets.length < 2){ mm.innerHTML = ''; return; }
mm.innerHTML = '';
targets.forEach((el, i)=>{
const dot = document.createElement('div');
dot.className = 'mm-dot';
const kindBadge = el.classList.contains('wg') ? 'wg' : 'card';
const titleEl = el.querySelector('.card-title, .wg-title');
dot.title = titleEl ? titleEl.textContent.trim() : kindBadge + ' ' + (i+1);
if(kindBadge === 'wg') dot.style.borderRadius = '3px';
dot.addEventListener('click', ()=>el.scrollIntoView({behavior:'smooth',block:'center'}));
mm.appendChild(dot);
});
// Update active dot on scroll
if(_mmScrollHandler) window.removeEventListener('scroll', _mmScrollHandler);
_mmScrollHandler = ()=>{
const dots = [...mm.querySelectorAll('.mm-dot')];
const midY = window.scrollY + window.innerHeight * 0.5;
let activeI = 0;
targets.forEach((el, i)=>{
const r = el.getBoundingClientRect();
if(r.top + window.scrollY <= midY) activeI = i;
});
dots.forEach((d,i)=>d.classList.toggle('active', i === activeI));
};
window.addEventListener('scroll', _mmScrollHandler, { passive:true });
setTimeout(_mmScrollHandler, 100);
}
/* ─── Task 6: Hint system for simp4 & comp ─── */
/* Enhanced SIMP4_TASKS with hints */
const SIMP4_HINTS = [
['Ищи точный квадрат в 72', '72 = 36 × 2', '√72 = 6√2'],
['Ищи точный квадрат в 50', '50 = 25 × 2', '√50 = 5√2'],
['Ищи точный квадрат в 48', '48 = 16 × 3', '√48 = 4√3'],
['Ищи точный квадрат в 200', '200 = 100 × 2', '√200 = 10√2'],
['Ищи точный квадрат в 75', '75 = 25 × 3', '√75 = 5√3'],
['Ищи точный квадрат в 98', '98 = 49 × 2', '√98 = 7√2'],
['Ищи точный квадрат в 18', '18 = 9 × 2', '√18 = 3√2'],
['Ищи точный квадрат в 128', '128 = 64 × 2', '√128 = 8√2'],
['Ищи точный квадрат в 80', '80 = 16 × 5', '√80 = 4√5'],
['Ищи точный квадрат в 108', '108 = 36 × 3', '√108 = 6√3'],
['Ищи точный квадрат в 147', '147 = 49 × 3', '√147 = 7√3'],
];
const _simp4HintLevel = {};
function simp4Hint(){
const idx = simp4State.idx;
const key = 'simp4_' + idx;
const level = (_simp4HintLevel[key] || 0);
const hints = SIMP4_HINTS[idx] || [];
const hintText = hints[level] || hints[hints.length - 1] || '—';
const nextLevel = Math.min(level + 1, hints.length - 1);
_simp4HintLevel[key] = nextLevel;
let box = document.getElementById('simp4-hint-box');
if(!box){
box = document.createElement('div');
box.id = 'simp4-hint-box';
const fb = document.getElementById('simp4-fb');
if(fb) fb.parentNode.insertBefore(box, fb);
}
const levelNames = ['Намёк','Шаг','Ответ'];
box.className = 'hint-box';
box.innerHTML = `<span class="hint-level-badge">Подсказка ${level + 1}: ${levelNames[level] || 'Ответ'}</span><br>${hintText}`;
if(level === 2) simp4State.score = Math.max(0, simp4State.score - 5);
}
const COMP_HINTS = [
['Возведи оба в квадрат', '(3√2)² = 18, (2√3)² = 12', '18 > 12 → 3√2 > 2√3'],
['Возведи оба в квадрат', '(4√3)² = 48, (3√5)² = 45', '48 > 45 → 4√3 > 3√5'],
['Возведи оба в квадрат', '(5√2)² = 50, 7² = 49', '50 > 49 → 5√2 > 7'],
['Возведи оба в квадрат', '(2√7)² = 28, (3√3)² = 27', '28 > 27 → 2√7 > 3√3'],
['Возведи оба в квадрат', '(√17)² = 17, 4² = 16', '17 > 16 → √17 > 4'],
['Возведи оба в квадрат', '(√35)² = 35, 6² = 36', '35 < 36 35 < 6'],
];
const _compHintLevel = {};
function compHint(){
const key = 'comp_' + compIdx;
const level = (_compHintLevel[key] || 0);
const hints = COMP_HINTS[compIdx] || [];
const hintText = hints[level] || hints[hints.length-1] || '—';
_compHintLevel[key] = Math.min(level + 1, hints.length - 1);
let box = document.getElementById('comp-hint-box');
if(!box){
box = document.createElement('div');
box.id = 'comp-hint-box';
const fb = document.getElementById('comp-fb');
if(fb) fb.parentNode.insertBefore(box, fb);
}
const levelNames = ['Намёк','Шаг','Ответ'];
box.className = 'hint-box';
box.innerHTML = `<span class="hint-level-badge">Подсказка ${level + 1}: ${levelNames[level] || 'Ответ'}</span><br>${hintText}`;
}
/* ─── Task 7: Mobile sidebar toggle ─── */
function toggleSidebar(){
const side = document.getElementById('col-side');
if(!side) return;
const isOpen = side.classList.contains('open') || side.classList.contains('side-open');
if(isOpen){
side.classList.remove('open','side-open');
const overlay = document.getElementById('side-overlay');
if(overlay) overlay.classList.remove('show');
} else {
side.classList.add('open','side-open');
const overlay = document.getElementById('side-overlay');
if(overlay) overlay.classList.add('show');
}
}
/* ─── Patch goTo to trigger Wave 3 post-build ─── */
const _origGoToFinish = window._goToFinish;
window._goToFinish = function(id){
_origGoToFinish(id);
// Build minimap after section is rendered
setTimeout(()=>{
_buildMinimap(id);
_attachBookmarkButtons();
_applyGlossary(document.getElementById('sec-' + id));
}, 80);
};
/* ─── Wave 3 INIT ─── */
function initWave3(){
_initKeyboard();
_initSearchInput();
_initGlossTooltip();
// Rebuild search index after everything is built
setTimeout(buildSearchIndex, 1200);
}
document.addEventListener('DOMContentLoaded', ()=>setTimeout(initWave3, 100));
</script>
</body>
</html>