e76485cadc
Полная инфраструктура: hub, 5 ch-скелетов, lab-скелет, миграция 039, расширение phys.js на 11 хелперов + 2 класса симуляций для новых тем 7-го класса. ФАЙЛЫ: - backend/src/db/migrations/039_physics_7_hub.sql — self-sufficient миграция (parent physics-7 + 6 children: ch1..ch5 + lab). Palette: sky/blue для hub, глав: indigo/violet/red/amber/emerald/cyan. - frontend/textbooks/physics_7_hub.html (862 строки) — hub с прогресс-картами 6 разделов, шпаргалкой курса в 5 mini-карточках, 10 интегрированных боссов финала курса (через ачивку «Магистр физики 7», +150 XP), темой/lang storage через ключи physics7_*. Sidebar-фикс на десктопе встроен. - frontend/textbooks/physics_7_ch1..ch5.html (350-390 строк каждый) — скелеты глав с header, paragraph selector, sidebar, прогресс/XP, goTo, search-модалом, KaTeX с delimiters, sidebar-фиксом, cache-busting ?v=20260530. Каждая глава имеет правильное число параграфов (7/6/14/8/7) + sec-finalN. - frontend/textbooks/physics_7_lab.html (306 строк) — скелет лаб. практикума на 6 ЛР с teal/cyan палитрой и ачивкой «Лаборант 7 класса» (+80 XP). - backend/scripts/gen_phys7_ch.js / gen_phys7_lab.js — генераторы из единого шаблона (для регенерации при правках инфраструктуры). PHYS.JS НОВЫЕ ХЕЛПЕРЫ (всё работает, smoke-test пройден): - forceVector(x,y,F,angle,color,label) — стрелка силы с подписью - dynamometer(x,y,h,Fmax,F) — динамометр с пружиной и шкалой - blockOnSurface(x,y,w,h,label,weights) — брусок со стопкой гирь - connectedVessels(x,y,kindA,kindB,levelY) — сообщающиеся сосуды - hydraulicPress(x,y,sSmall,sLarge,fSmall) — гидравлический пресс - mercuryBarometer(x,y,hMm) — ртутный барометр Торричелли - aneroidBarometer(cx,cy,r,p) — стрелочный барометр-анероид - uManometer(x,y,w,h,deltaH) — U-образный жидкостный манометр - rulerWithError(x,y,lenCm,mmPerDiv) — линейка со шкалой и ценой деления - bimetal(x,y,w,h,deltaT) — биметаллическая пластина (гнётся от ΔT) - expandingRod(x,y,l0,alpha,deltaT) — стержень с тепловым расширением - class HillSlideSim — тележка на горке (§42, закон сохранения; графики Ek/Ep/Etot) - class PendulumSim — математический маятник (§42, осцилляции) Все 13 экспортированы в window.PHYS, smoke-test показал физически разумные значения энергий. Parse-check + node --check проходят. Уроки phys 9 учтены сразу: cache-busting на phys.js, sidebar-фикс @media min-width:981px, delimiters для renderMathInElement. PHASE 0 DONE. Дальше: Phase 1 Wave 1 — §§1-2 (Физика как наука + Тело/явление/величина).
364 lines
25 KiB
HTML
364 lines
25 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||
<meta http-equiv="Pragma" content="no-cache">
|
||
<meta http-equiv="Expires" content="0">
|
||
<title>Физика 7 · Глава 4 · Давление</title>
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||
<link rel="stylesheet" href="/css/phys-textbook-widgets.css">
|
||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
|
||
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
|
||
<script src="/js/api.js" defer></script>
|
||
<script src="/js/xp.js" defer></script>
|
||
<script src="/js/phys.js?v=20260530" defer></script>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--bg:#f0f9ff; --card:#fff; --card-soft:#f8fafc; --text:#0f172a; --muted:#475569;
|
||
--border:#bae6fd; --pri:#0284c7; --pri2:#0c4a6e; --pri-soft:#e0f2fe;
|
||
--acc:#d97706; --acc-d:#92400e; --acc-soft:#fef3c7;
|
||
--ok:#10b981; --ok-bg:#d1fae5; --fail:#dc2626; --fail-bg:#fee2e2; --warn:#f59e0b; --warn-bg:#fef3c7;
|
||
--sh:0 4px 16px rgba(2,132,199,.08); --sh-h:0 12px 36px rgba(2,132,199,.16);
|
||
}
|
||
html.dark{--bg:#0c1e2e;--card:#0e2436;--card-soft:#0b1a28;--text:#e0f2fe;--muted:#7dd3fc;--border:#1e3a5f;--pri-soft:rgba(2,132,199,.18)}
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
html,body{min-height:100vh}
|
||
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;transition:background .25s,color .25s}
|
||
|
||
.hdr{position:relative;background:linear-gradient(135deg,#78350f,#d97706 60%,#fbbf24);color:#fff;padding:24px 22px 22px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.18)}
|
||
.hdr-inner{position:relative;z-index:1;max-width:1240px;margin:0 auto;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
||
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;letter-spacing:-.01em}
|
||
.hdr-sub{font-size:.88rem;opacity:.9;margin-top:3px}
|
||
.hdr-side{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap}
|
||
.hdr-btn{padding:8px 12px;background:rgba(255,255,255,.16);border:none;color:#fff;border-radius:9px;cursor:pointer;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;font-family:inherit;text-decoration:none}
|
||
.hdr-btn:hover{background:rgba(255,255,255,.26)}
|
||
.ic{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||
|
||
.main{max-width:1240px;margin:0 auto;padding:22px;width:100%;display:grid;grid-template-columns:1fr 280px;gap:24px}
|
||
@media(max-width:980px){.main{grid-template-columns:1fr;padding:14px}}
|
||
|
||
.col-main{min-width:0}
|
||
.psel{background:var(--card);border:1.5px solid var(--border);border-radius:14px;padding:16px;margin-bottom:18px;box-shadow:var(--sh)}
|
||
.psel-head{font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px}
|
||
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px}
|
||
.psel-card{padding:12px;background:var(--card-soft);border:1.5px solid var(--border);border-radius:10px;cursor:pointer;transition:transform .15s,border-color .15s,box-shadow .15s;text-align:left}
|
||
.psel-card:hover{border-color:var(--acc);transform:translateY(-2px);box-shadow:0 4px 14px rgba(0,0,0,.06)}
|
||
.psel-card.active{border-color:var(--acc);background:var(--acc-soft)}
|
||
.psel-num{font-size:.7rem;font-weight:800;color:var(--acc-d);letter-spacing:.04em;text-transform:uppercase;margin-bottom:3px}
|
||
.psel-title{font-size:.86rem;font-weight:700;line-height:1.35}
|
||
.psel-prog{height:4px;background:rgba(0,0,0,.07);border-radius:3px;overflow:hidden;margin-top:7px}
|
||
.psel-prog-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--acc-d));border-radius:3px;transition:width .4s}
|
||
|
||
.sec{display:none;background:var(--card);border:1.5px solid var(--border);border-radius:14px;padding:22px;box-shadow:var(--sh);position:relative}
|
||
.sec.active{display:block}
|
||
.sec[data-watermark]::before{content:attr(data-watermark);position:absolute;right:18px;top:-12px;font-family:'Unbounded',sans-serif;font-size:5.2rem;font-weight:900;color:var(--acc-soft);pointer-events:none;line-height:1;user-select:none}
|
||
.sec-header{display:flex;align-items:baseline;gap:14px;margin-bottom:18px;padding-bottom:14px;border-bottom:1.5px solid var(--border);position:relative;z-index:1}
|
||
.sec-num{background:linear-gradient(135deg,var(--acc),var(--acc-d));color:#fff;padding:5px 12px;border-radius:9px;font-family:'Unbounded',sans-serif;font-weight:800;font-size:.86rem;letter-spacing:.04em}
|
||
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.35rem;font-weight:800;color:var(--text)}
|
||
.placeholder{padding:32px 20px;text-align:center;color:var(--muted);font-size:.95rem;background:var(--card-soft);border:1.5px dashed var(--border);border-radius:10px}
|
||
|
||
.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
|
||
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
|
||
.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
|
||
.sidecard-row b{color:var(--pri);font-weight:700}
|
||
@media(max-width:980px){.col-side{position:static;max-height:none}}
|
||
|
||
.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
|
||
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc-d);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between}
|
||
.xp-level{font-size:1.1rem;font-weight:900;color:var(--acc-d);font-family:'Unbounded',sans-serif}
|
||
.xp-bar{height:9px;background:rgba(245,158,11,.15);border-radius:6px;overflow:hidden;margin:7px 0}
|
||
.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)}
|
||
.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between}
|
||
|
||
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none}
|
||
.col-side-backdrop.show{display:block}
|
||
@media(min-width:981px){#sidebar-btn{display:none}.col-side-backdrop.show{display:none}}
|
||
@media(max-width:980px){
|
||
.col-side{position:fixed;top:0;right:0;height:100vh;width:300px;max-width:88vw;background:var(--bg);box-shadow:-12px 0 24px rgba(0,0,0,.18);padding:18px 16px;overflow-y:auto;transform:translateX(100%);transition:transform .25s ease;z-index:9991;max-height:none}
|
||
.col-side.open{transform:none}
|
||
}
|
||
|
||
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,var(--acc-d),var(--acc));color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(0,0,0,.25);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
|
||
.ach-popup.show{display:flex}
|
||
|
||
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
|
||
|
||
/* Search modal */
|
||
.search-modal{position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9993;display:none;align-items:flex-start;justify-content:center;padding-top:80px}
|
||
.search-modal.show{display:flex}
|
||
.search-box{background:var(--card);border-radius:14px;width:520px;max-width:92vw;padding:14px;box-shadow:0 24px 64px rgba(0,0,0,.35);border:1.5px solid var(--border)}
|
||
.search-inp{width:100%;padding:11px 14px;background:var(--card-soft);border:1.5px solid var(--border);border-radius:9px;color:var(--text);font-size:.95rem;font-family:inherit;outline:0}
|
||
.search-inp:focus{border-color:var(--acc)}
|
||
.search-list{margin-top:12px;max-height:320px;overflow-y:auto}
|
||
.search-item{padding:10px 12px;border-radius:9px;cursor:pointer;border:1px solid transparent;font-size:.9rem}
|
||
.search-item:hover,.search-item.cur{background:var(--acc-soft);border-color:var(--acc)}
|
||
.search-item .num{display:inline-block;padding:2px 8px;background:var(--acc);color:#fff;border-radius:99px;font-size:.7rem;font-weight:700;margin-right:8px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="hdr">
|
||
<div class="hdr-inner">
|
||
<div>
|
||
<a href="/textbook/physics-7" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К физике 7</a>
|
||
</div>
|
||
<div>
|
||
<h1>Физика 7 · Глава 4</h1>
|
||
<div class="hdr-sub">Давление · §§28–35</div>
|
||
</div>
|
||
<div class="hdr-side">
|
||
<button id="search-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> Поиск</button>
|
||
<button id="sidebar-btn" class="hdr-btn"><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="theme-btn" class="hdr-btn"><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></button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="main">
|
||
<div class="col-main">
|
||
<div class="psel">
|
||
<div class="psel-head">Параграфы главы 4</div>
|
||
<div class="psel-grid" id="psel-grid"></div>
|
||
</div>
|
||
|
||
<section id="sec-p28" class="sec" data-watermark="p">
|
||
<div class="sec-header"><span class="sec-num">§ 28</span><h2 class="sec-h">Давление. Единицы давления</h2></div>
|
||
<div id="p28-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p29" class="sec" data-watermark="∴">
|
||
<div class="sec-header"><span class="sec-num">§ 29</span><h2 class="sec-h">Давление газа</h2></div>
|
||
<div id="p29-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p30" class="sec" data-watermark="⊕">
|
||
<div class="sec-header"><span class="sec-num">§ 30</span><h2 class="sec-h">Закон Паскаля</h2></div>
|
||
<div id="p30-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p31" class="sec" data-watermark="≡">
|
||
<div class="sec-header"><span class="sec-num">§ 31</span><h2 class="sec-h">Гидростатическое давление</h2></div>
|
||
<div id="p31-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p32" class="sec" data-watermark="U">
|
||
<div class="sec-header"><span class="sec-num">§ 32</span><h2 class="sec-h">Сообщающиеся сосуды</h2></div>
|
||
<div id="p32-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p33" class="sec" data-watermark="⌒">
|
||
<div class="sec-header"><span class="sec-num">§ 33</span><h2 class="sec-h">Газы и их вес</h2></div>
|
||
<div id="p33-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p34" class="sec" data-watermark="∼">
|
||
<div class="sec-header"><span class="sec-num">§ 34</span><h2 class="sec-h">Атмосферное давление</h2></div>
|
||
<div id="p34-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-p35" class="sec" data-watermark="⏚">
|
||
<div class="sec-header"><span class="sec-num">§ 35</span><h2 class="sec-h">Барометры и манометры</h2></div>
|
||
<div id="p35-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
<section id="sec-final4" class="sec" data-watermark="★">
|
||
<div class="sec-header"><span class="sec-num">Финал</span><h2 class="sec-h">Итоги главы 4</h2></div>
|
||
<div id="final4-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
|
||
</section>
|
||
</div>
|
||
|
||
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
|
||
<div class="col-side-backdrop" id="col-side-backdrop"></div>
|
||
</main>
|
||
|
||
<div class="ach-popup" id="ach-popup"><svg class="ic" viewBox="0 0 24 24"><polygon points="12,2 15,9 22,9.3 17,14 18.5,21 12,17 5.5,21 7,14 2,9.3 9,9"/></svg><span id="ach-text"></span></div>
|
||
|
||
<div class="search-modal" id="search-modal"><div class="search-box">
|
||
<input type="text" class="search-inp" id="search-inp" placeholder="Поиск по параграфам... (Esc — закрыть, Ctrl+K)">
|
||
<div class="search-list" id="search-list"></div>
|
||
</div></div>
|
||
|
||
<footer class="foot">Интерактивный учебник «Физика 7 класс» · Глава 4 · LearnSpace</footer>
|
||
|
||
<script>
|
||
'use strict';
|
||
|
||
const LS_PREFIX = 'physics7_ch4';
|
||
const _TB_SLUG = 'physics-7-ch4';
|
||
|
||
const PARAS = [{id:'p28',num:'§ 28',title:"Давление. Единицы давления",wm:'p'},{id:'p29',num:'§ 29',title:"Давление газа",wm:'∴'},{id:'p30',num:'§ 30',title:"Закон Паскаля",wm:'⊕'},{id:'p31',num:'§ 31',title:"Гидростатическое давление",wm:'≡'},{id:'p32',num:'§ 32',title:"Сообщающиеся сосуды",wm:'U'},{id:'p33',num:'§ 33',title:"Газы и их вес",wm:'⌒'},{id:'p34',num:'§ 34',title:"Атмосферное давление",wm:'∼'},{id:'p35',num:'§ 35',title:"Барометры и манометры",wm:'⏚'},{id:'final4',num:'Финал',title:"Итоги главы 4",wm:'★'}];
|
||
const TOTAL_PARAS = PARAS.length;
|
||
|
||
const SIDEBARS = {};
|
||
PARAS.forEach(p => { SIDEBARS[p.id] = { title: 'Шпаргалка ' + p.num, rows: [['В разработке','контент появится с волной соответствующего §']] }; });
|
||
const TIPS = [{ sec: PARAS[0].id, html: 'Скелет главы готов. Контент параграфов выйдет в одной из ближайших фаз.' }];
|
||
const ACH_LABELS = { start: 'Начало главы 4', ch_done: 'Властелин давления' };
|
||
|
||
const STATE = { current: null, progress: {}, xp: 0, level: 1, achievements: new Map(), _built: new Set() };
|
||
|
||
function _xpForLevel(lv){ return Math.round(100 * Math.pow(lv-1, 1.6)); }
|
||
function calcLevel(xp){ let lv = 1; while(_xpForLevel(lv+1) <= xp) lv++; return lv; }
|
||
|
||
function loadProgress(){
|
||
try{
|
||
const s = localStorage.getItem(LS_PREFIX + '_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
|
||
const a = localStorage.getItem(LS_PREFIX + '_achievements');
|
||
if(a){ const p = JSON.parse(a); if(p && typeof p === 'object'){ for(const [id,t] of Object.entries(p)) STATE.achievements.set(id, (t && t !== id) ? t : (ACH_LABELS[id] || id)); } }
|
||
STATE.xp = +(localStorage.getItem('physics7_xp') || 0); STATE.level = calcLevel(STATE.xp);
|
||
}catch(e){}
|
||
}
|
||
function saveProgress(){
|
||
try{
|
||
localStorage.setItem(LS_PREFIX + '_progress', JSON.stringify(STATE.progress));
|
||
localStorage.setItem(LS_PREFIX + '_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
|
||
localStorage.setItem('physics7_xp', String(STATE.xp));
|
||
}catch(e){}
|
||
}
|
||
function bumpProgress(key, delta){
|
||
STATE.progress[key] = Math.max(0, Math.min(100, (STATE.progress[key]||0) + delta));
|
||
saveProgress(); refreshProgressUI();
|
||
if(STATE.progress[key] >= 100 && key === PARAS[PARAS.length-1].id) achievement('ch_done', 'Властелин давления');
|
||
}
|
||
function addXp(n, src){
|
||
if(!n) return;
|
||
const prev = STATE.level; STATE.xp = Math.max(0, (STATE.xp||0) + n); STATE.level = calcLevel(STATE.xp);
|
||
saveProgress(); refreshProgressUI();
|
||
if(window.LS && window.LS.xp) window.LS.xp.add(n, 'physics7-ch4-' + (src||'misc'));
|
||
if(STATE.level > prev){
|
||
const pop = document.getElementById('ach-popup');
|
||
if(pop){ document.getElementById('ach-text').textContent = 'Уровень ' + STATE.level + '!'; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'), 2600); }
|
||
}
|
||
}
|
||
function achievement(id, label){
|
||
if(STATE.achievements.has(id)) return;
|
||
STATE.achievements.set(id, label || ACH_LABELS[id] || id);
|
||
saveProgress();
|
||
const pop = document.getElementById('ach-popup');
|
||
if(pop){ document.getElementById('ach-text').textContent = 'Ачивка: ' + (label || ACH_LABELS[id] || id); pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'), 3000); }
|
||
addXp(20, 'ach-' + id);
|
||
}
|
||
|
||
function refreshProgressUI(){
|
||
document.querySelectorAll('[data-prog-card]').forEach(el => {
|
||
const k = el.dataset.progCard;
|
||
const fl = el.querySelector('.psel-prog-fill');
|
||
if(fl) fl.style.width = (STATE.progress[k]||0) + '%';
|
||
});
|
||
if(STATE.current && document.getElementById('sidebar-content')){ try{ buildSidebar(STATE.current); }catch(e){} }
|
||
}
|
||
|
||
function buildParaSelector(){
|
||
const grid = document.getElementById('psel-grid');
|
||
if(!grid) return;
|
||
grid.innerHTML = PARAS.map(p =>
|
||
'<button class="psel-card" data-id="' + p.id + '" data-prog-card="' + p.id + '">'
|
||
+ '<div class="psel-num">' + p.num + '</div>'
|
||
+ '<div class="psel-title">' + p.title + '</div>'
|
||
+ '<div class="psel-prog"><div class="psel-prog-fill" style="width:' + (STATE.progress[p.id]||0) + '%"></div></div>'
|
||
+ '</button>'
|
||
).join('');
|
||
grid.querySelectorAll('.psel-card').forEach(c => c.addEventListener('click', () => goTo(c.dataset.id)));
|
||
}
|
||
|
||
function ensureBuilt(id){
|
||
if(STATE._built.has(id)) return;
|
||
STATE._built.add(id);
|
||
}
|
||
|
||
function goTo(id){
|
||
STATE.current = id; ensureBuilt(id);
|
||
document.querySelectorAll('.sec').forEach(s => s.classList.remove('active'));
|
||
const el = document.getElementById('sec-' + id); if(el) el.classList.add('active');
|
||
document.querySelectorAll('.psel-card').forEach(c => c.classList.toggle('active', c.dataset.id === id));
|
||
buildSidebar(id);
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
if((STATE.progress[id]||0) < 10) bumpProgress(id, 10);
|
||
if(window.renderMathInElement && el){
|
||
setTimeout(() => {
|
||
try{ renderMathInElement(el, { delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); }catch(e){}
|
||
}, 0);
|
||
}
|
||
}
|
||
|
||
function buildSidebar(id){
|
||
const box = document.getElementById('sidebar-content');
|
||
if(!box) return;
|
||
const sb = SIDEBARS[id] || SIDEBARS[PARAS[0].id];
|
||
const xpForLv = _xpForLevel(STATE.level), xpNext = _xpForLevel(STATE.level+1);
|
||
const xpInLv = STATE.xp - xpForLv, xpRange = xpNext - xpForLv;
|
||
const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100;
|
||
let html = '';
|
||
html += '<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><span class="xp-level">Ур. ' + STATE.level + '</span></div><div class="xp-bar"><div class="xp-fill" style="width:' + xpPct + '%"></div></div><div class="xp-nums"><span>' + STATE.xp + ' XP</span><span>' + xpNext + ' XP</span></div></div>';
|
||
html += '<div class="sidecard"><h4>' + sb.title + '</h4>';
|
||
sb.rows.forEach(([k,v]) => { html += '<div class="sidecard-row"><b>' + k + '</b>' + (v ? ' — ' + v : '') + '</div>'; });
|
||
html += '</div>';
|
||
const tip = TIPS.find(t => t.sec === id) || TIPS[0];
|
||
if(tip){
|
||
html += '<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border-color:var(--warn)"><h4 style="color:#92400e">Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem">' + tip.html + '</div></div>';
|
||
}
|
||
if(STATE.achievements.size > 0){
|
||
html += '<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">' + STATE.achievements.size + '</span></h4>';
|
||
[...STATE.achievements.values()].slice(-4).forEach(t => { html += '<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ' + t + '</div>'; });
|
||
html += '</div>';
|
||
}
|
||
box.innerHTML = html;
|
||
if(window.renderMathInElement){
|
||
try{ renderMathInElement(box, { delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); }catch(e){}
|
||
}
|
||
}
|
||
|
||
function initTheme(){
|
||
const t = localStorage.getItem(LS_PREFIX + '_theme') || localStorage.getItem('physics7_theme') || 'light';
|
||
if(t === 'dark') document.documentElement.classList.add('dark');
|
||
document.getElementById('theme-lab').textContent = t === 'dark' ? 'Светлая' : 'Тёмная';
|
||
document.getElementById('theme-btn').addEventListener('click', () => {
|
||
document.documentElement.classList.toggle('dark');
|
||
const dark = document.documentElement.classList.contains('dark');
|
||
localStorage.setItem(LS_PREFIX + '_theme', dark ? 'dark' : 'light');
|
||
localStorage.setItem('physics7_theme', dark ? 'dark' : 'light');
|
||
document.getElementById('theme-lab').textContent = dark ? 'Светлая' : 'Тёмная';
|
||
});
|
||
}
|
||
|
||
function initSidebarToggle(){
|
||
const side = document.getElementById('col-side'), back = document.getElementById('col-side-backdrop'), btn = document.getElementById('sidebar-btn');
|
||
if(!side || !btn) return;
|
||
function open(){ side.classList.add('open'); back.classList.add('show'); }
|
||
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
|
||
btn.addEventListener('click', () => { if(side.classList.contains('open')) close(); else open(); });
|
||
back.addEventListener('click', close);
|
||
document.addEventListener('keydown', e => { if(e.key === 'Escape') close(); });
|
||
}
|
||
|
||
function initSearch(){
|
||
const btn = document.getElementById('search-btn'), modal = document.getElementById('search-modal'), inp = document.getElementById('search-inp'), list = document.getElementById('search-list');
|
||
if(!btn || !modal) return;
|
||
let cur = 0;
|
||
function render(q){
|
||
const ql = (q||'').toLowerCase().trim();
|
||
const items = PARAS.filter(p => !ql || p.title.toLowerCase().includes(ql) || p.num.toLowerCase().includes(ql));
|
||
list.innerHTML = items.map((p,i) => '<div class="search-item' + (i === cur ? ' cur' : '') + '" data-id="' + p.id + '"><span class="num">' + p.num + '</span>' + p.title + '</div>').join('');
|
||
list.querySelectorAll('.search-item').forEach(el => el.addEventListener('click', () => { goTo(el.dataset.id); close(); }));
|
||
}
|
||
function open(){ modal.classList.add('show'); inp.value = ''; cur = 0; render(''); setTimeout(() => inp.focus(), 50); }
|
||
function close(){ modal.classList.remove('show'); }
|
||
btn.addEventListener('click', open);
|
||
modal.addEventListener('click', e => { if(e.target === modal) close(); });
|
||
inp.addEventListener('input', () => { cur = 0; render(inp.value); });
|
||
inp.addEventListener('keydown', e => {
|
||
const items = list.querySelectorAll('.search-item');
|
||
if(e.key === 'ArrowDown'){ e.preventDefault(); cur = Math.min(items.length-1, cur+1); render(inp.value); }
|
||
else if(e.key === 'ArrowUp'){ e.preventDefault(); cur = Math.max(0, cur-1); render(inp.value); }
|
||
else if(e.key === 'Enter'){ e.preventDefault(); const sel = items[cur]; if(sel){ goTo(sel.dataset.id); close(); } }
|
||
else if(e.key === 'Escape'){ e.preventDefault(); close(); }
|
||
});
|
||
document.addEventListener('keydown', e => { if((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
|
||
}
|
||
|
||
function init(){
|
||
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
|
||
buildParaSelector(); refreshProgressUI(); goTo(PARAS[0].id);
|
||
setTimeout(() => achievement('start'), 600);
|
||
}
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
</script>
|
||
</body>
|
||
</html>
|