diff --git a/backend/tests/chemistry8-page.test.js b/backend/tests/chemistry8-page.test.js
index 34a4e0c..0f96f32 100644
--- a/backend/tests/chemistry8-page.test.js
+++ b/backend/tests/chemistry8-page.test.js
@@ -82,6 +82,12 @@ test('ch1: тренажёр задач отрисован для §10', async ()
assert.ok(doc.querySelectorAll('#navDotsp10 .nav-dot').length >= 4, 'навигация по задачам §10');
});
+test('ch1: генетическая карта §22 монтируется (U3)', async () => {
+ const { doc } = await loadDom('chemistry_8_ch1.html', '/js/chem8_ch1_widgets.js');
+ doc.defaultView.goTo('p22'); await wait(120);
+ assert.ok(doc.querySelectorAll('#c-genetic .gm-edge').length >= 6, 'граф классов §22');
+});
+
/* ── Глава 2 ── */
test('ch2: SPA без ошибок, 6 карточек, §24 активен, ПСХЭ', async () => {
const { doc, errors } = await loadDom('chemistry_8_ch2.html', '/js/chem8_ch2_widgets.js');
@@ -203,3 +209,9 @@ test('ch6: SPA без ошибок, 8 карточек, §46 активен, w/c
doc.defaultView.goTo('p51'); await wait(120);
assert.ok(doc.querySelector('#c-ccalc #c-go'), 'калькулятор c §51');
});
+
+test('ch6: анимация растворения §47 монтируется (U3)', async () => {
+ const { doc } = await loadDom('chemistry_8_ch6.html', '/js/chem8_ch6_widgets.js');
+ doc.defaultView.goTo('p47'); await wait(120);
+ assert.ok(doc.querySelector('#c-dissoc .ds-svg'), 'анимация диссоциации §47');
+});
diff --git a/backend/tests/chemistry8.test.js b/backend/tests/chemistry8.test.js
index 4e8d7b5..59fcd8e 100644
--- a/backend/tests/chemistry8.test.js
+++ b/backend/tests/chemistry8.test.js
@@ -65,7 +65,7 @@ test('Chem8.elementCounts — скобки и индексы', () => {
});
test('Chem8 — оставшиеся заглушки возвращают null и не падают', () => {
- for (const fn of ['redoxBalancer', 'orbitalDiagram', 'dissociationAnim', 'geneticMap']) {
+ for (const fn of ['redoxBalancer', 'orbitalDiagram']) {
assert.equal(typeof C[fn], 'function', fn + ' определён');
assert.equal(C[fn]({}), null, fn + ' заглушка возвращает null');
}
diff --git a/frontend/css/chem8-textbook.css b/frontend/css/chem8-textbook.css
index 382dd67..5d971f9 100644
--- a/frontend/css/chem8-textbook.css
+++ b/frontend/css/chem8-textbook.css
@@ -401,6 +401,16 @@ html.dark .lat-card h4{color:var(--pri-l)}
.amph-stage{display:flex;justify-content:center;margin:8px 0}
.amph-out{margin-top:6px}
+/* геном-карта классов (§22) */
+.gm-svg{width:100%;max-width:440px;height:auto;color:var(--text);display:block;margin:4px auto}
+.gm-out{margin-top:8px}
+.gm-edge{transition:stroke .15s,stroke-width .15s}
+
+/* диссоциация/растворение (§47) */
+.ds-svg{width:100%;max-width:300px;height:auto;display:block;margin:6px auto}
+.ds-stage{display:flex;justify-content:center}
+.ds-out{margin-top:6px}
+
/* exa-step (разбор примеров) */
.exa-step{font-family:var(--mono);font-size:.9rem;background:var(--card-soft);border-left:3px solid var(--pri);border-radius:0 8px 8px 0;padding:8px 12px;margin:6px 0}
diff --git a/frontend/js/chem8_ch1_widgets.js b/frontend/js/chem8_ch1_widgets.js
index e51ed8d..f2e377c 100644
--- a/frontend/js/chem8_ch1_widgets.js
+++ b/frontend/js/chem8_ch1_widgets.js
@@ -98,6 +98,9 @@
render();
}
+ /* §22 — генетическая карта классов */
+ function mount_p22() { var el = $('c-genetic'); if (el && !el._b && C().geneticMap) { el._b = 1; C().geneticMap(el, {}); } }
+
W.CHEM8_WIDGETS = { p13: mount_p13, p16: mount_p16, p17: mount_p17, p18: mount_p18 };
- W.FLAG_MOUNTS = { p10: mount_p10, p14: mount_p14, p19: mount_p19, p20: mount_p20, p23: mount_p23 };
+ W.FLAG_MOUNTS = { p10: mount_p10, p14: mount_p14, p19: mount_p19, p20: mount_p20, p22: mount_p22, p23: mount_p23 };
})(window);
diff --git a/frontend/js/chem8_ch6_widgets.js b/frontend/js/chem8_ch6_widgets.js
index bbffc45..e1d8106 100644
--- a/frontend/js/chem8_ch6_widgets.js
+++ b/frontend/js/chem8_ch6_widgets.js
@@ -76,6 +76,9 @@
$('c-go').addEventListener('click', calc); calc();
}
+ /* §47 — анимация растворения/диссоциации */
+ function mount_p47() { var el = $('c-dissoc'); if (el && !el._b && C().dissociationAnim) { el._b = 1; C().dissociationAnim(el, { substance: 'NaCl' }); } }
+
W.CHEM8_WIDGETS = { p46: mount_p46, p50: mount_p50, p51: mount_p51 };
- W.FLAG_MOUNTS = { p48: mount_p48 };
+ W.FLAG_MOUNTS = { p47: mount_p47, p48: mount_p48 };
})(window);
diff --git a/frontend/js/chem8_svg.js b/frontend/js/chem8_svg.js
index ed833cb..a293bc8 100644
--- a/frontend/js/chem8_svg.js
+++ b/frontend/js/chem8_svg.js
@@ -800,6 +800,94 @@
return { el: host };
}
+ /* ──────────────────────────────────────────────────────────────────────────
+ geneticMap(mount) — интерактивный граф генетической связи классов веществ.
+ Клик по переходу (ребру) → реакция-пример. §22.
+ ────────────────────────────────────────────────────────────────────────── */
+ var GM_NODES = [
+ { id: 'me', t: 'Металл', x: 20, y: 22, c: '#0d9488' },
+ { id: 'mox', t: 'Осн. оксид', x: 120, y: 22, c: '#0d9488' },
+ { id: 'base', t: 'Основание', x: 228, y: 22, c: '#0d9488' },
+ { id: 'salt', t: 'Соль', x: 336, y: 55, c: '#d97706' },
+ { id: 'nm', t: 'Неметалл', x: 20, y: 90, c: '#2563eb' },
+ { id: 'nox', t: 'Кисл. оксид', x: 120, y: 90, c: '#2563eb' },
+ { id: 'acid', t: 'Кислота', x: 228, y: 90, c: '#2563eb' }
+ ];
+ var GM_EDGES = [
+ { f: 'me', t: 'mox', r: '2Mg + O2 -> 2MgO', d: 'Металл + кислород → основный оксид' },
+ { f: 'mox', t: 'base', r: 'CaO + H2O -> Ca(OH)2', d: 'Основный оксид + вода → основание (щёлочь)' },
+ { f: 'base', t: 'salt', r: '2NaOH + H2SO4 -> Na2SO4 + 2H2O', d: 'Основание + кислота → соль + вода (нейтрализация)' },
+ { f: 'nm', t: 'nox', r: 'S + O2 -> SO2', d: 'Неметалл + кислород → кислотный оксид' },
+ { f: 'nox', t: 'acid', r: 'SO3 + H2O -> H2SO4', d: 'Кислотный оксид + вода → кислота' },
+ { f: 'acid', t: 'salt', r: '2HCl + Ca(OH)2 -> CaCl2 + 2H2O', d: 'Кислота + основание → соль + вода' }
+ ];
+ function geneticMap(mount, opts) {
+ var host = typeof mount === 'string' ? global.document.querySelector(mount) : mount;
+ if (!host) return null;
+ var byId = {}; GM_NODES.forEach(function (n) { byId[n.id] = n; });
+ function cx(n) { return n.x + 44; } function cy(n) { return n.y + 16; }
+ var edgesSvg = GM_EDGES.map(function (e, i) {
+ var a = byId[e.f], b = byId[e.t];
+ return '
Ряд металла: металл → основный оксид → основание → соль
Na → Na₂O → NaOH → NaCl
Ряд неметалла: неметалл → кислотный оксид → кислота → соль
S → SO₃ → H₂SO₄ → Na₂SO₄
Эти ряды «встречаются» в солях — продукте реакции кислоты и основания.
') + +flag('Генетическая карта классов','Кликни по стрелке-переходу — увидишь реакцию-пример. Два ряда (металл и неметалл) сходятся в соли.','') +'Ca → CaO → Ca(OH)₂ → CaCl₂. Попробуй записать уравнение каждого перехода!
По выданным реактивам осуществи цепочку превращений и докажи получение каждого вещества (например, получи из меди — оксид меди, затем соль).
') +rememberBox(['Металл и неметалл — начала двух генетических рядов.','Соль — точка встречи кислотного и основного «миров».']) diff --git a/frontend/textbooks/chemistry_8_ch6.html b/frontend/textbooks/chemistry_8_ch6.html index 31bcbc0..3e7b1f6 100644 --- a/frontend/textbooks/chemistry_8_ch6.html +++ b/frontend/textbooks/chemistry_8_ch6.html @@ -174,9 +174,7 @@ function bp46(){ document.getElementById('p46-body').innerHTML = function bp47(){ document.getElementById('p47-body').innerHTML = hero(4,'§ 47 · Глава 6','Растворение веществ в воде','растворитель + в-во','Что происходит, когда сахар «исчезает» в чае: вода разбирает вещество на частицы.',['раствор','гидратация']) +makeCard('theory','Как идёт растворение','§47','Раствор = растворитель (чаще вода) + растворённое вещество. При растворении молекулы воды окружают частицы вещества и «растаскивают» их — это гидратация. При этом тепло может выделяться (растворение щёлочи) или поглощаться (растворение соли).
') - +'