Files
Learn_System/backend/scripts/gen_phys11_stubs.js
T
Maxim Dolgolyov 22b95ed072 feat(phys11 W0): инфра — миграция БД, phys-fx.js, hub + 8 stub-глав
Миграция 031_physics_11_hub.sql:
- hub textbook 'physics-11' (cyan, sort 12, para_count 45)
- 8 children по главам: ch1 cyan, ch2 violet, ch3 amber, ch4 blue,
  ch5 pink, ch6 green, ch7 rose, ch8 indigo

frontend/js/phys-fx.js (~360 строк):
- Глобальный requestAnimationFrame-цикл (Ticker) с подписками
- util.subscribe/unsubscribe + IntersectionObserver-пауза невидимых
- util.svgFrame, util.axes, util.slider — общие хелперы
- PHYS.Oscillogram: гарм. колебания с амплитудой/частотой/фазой/затуханием
- PHYS.SpringMass: пружинный маятник (T=2π√(m/k)) с зигзаг-пружиной
- PHYS.Pendulum: математический маятник (T=2π√(l/g)) с дугой

frontend/textbooks/physics_11_hub.html:
- Header cyan-gradient + watermark ФИЗИКА
- 4-кол grid карточек глав (8 шт., responsive)
- Прогресс-бар курса + API /api/textbooks/physics-11/children

frontend/textbooks/physics_11_ch1..ch8.html:
- Stub-страницы по образцу geometry_10_r1..r4 (W0)
- Список параграфов с ключевыми формулами + 'Будет добавлено в волне WN'
- Каждая глава со своей темой (gradient, watermark, цветами)
- phys-fx.js подключён сразу (ready для W1+)

backend/scripts/gen_phys11_stubs.js — генератор для повторных сборок.
2026-05-29 17:42:36 +03:00

464 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
'use strict';
/* Генератор stub-файлов для Физики 11 (W0).
* Запуск: node backend/scripts/gen_phys11_stubs.js
*/
const fs = require('fs');
const path = require('path');
const OUT = path.join(__dirname, '..', '..', 'frontend', 'textbooks');
const CHAPTERS = [
{ n:1, slug:'physics-11-ch1', name:'Механические колебания и волны',
paraRange:'§1–§6', wm:'∿', themeName:'cyan',
gradient:['#155e75','#0891b2','#a5f3fc'],
pri:'#0891b2', pri2:'#0e7490', priSoft:'#cffafe',
desc:'Колебательное движение, гармонические колебания, маятники, превращения энергии, резонанс, продольные и поперечные волны, звук.',
paras:[
{n:1, title:'Колебательное движение. Гармонические колебания', sub:'$T = \\Delta t / N$, $\\nu = 1/T$, $\\omega = 2\\pi/T$, $x = A\\cos(\\omega t + \\varphi_0)$'},
{n:2, title:'Пружинный и математический маятники', sub:'$T_{пр} = 2\\pi\\sqrt{m/k}$, $T_{мат} = 2\\pi\\sqrt{l/g}$'},
{n:3, title:'Превращения энергии при гарм. колебаниях', sub:'$W_{мех} = kA^2/2 = m\\omega^2 A^2/2$'},
{n:4, title:'Свободные и вынужденные колебания. Резонанс', sub:'Затухание, диссипация, $\\omega_{рез} \\approx \\omega_0$'},
{n:5, title:'Распространение колебаний в упругой среде. Продольные и поперечные волны', sub:'$\\lambda = vT$'},
{n:6, title:'Звуковые волны', sub:'16 Гц 20 кГц, $v_{зв}^{возд} \\approx 340$ м/с'}
]
},
{ n:2, slug:'physics-11-ch2', name:'Электромагнитные колебания и волны',
paraRange:'§7–§13', wm:'⚡', themeName:'violet',
gradient:['#5b21b6','#7c3aed','#c4b5fd'],
pri:'#7c3aed', pri2:'#5b21b6', priSoft:'#ede9fe',
desc:'Колебательный контур, формула Томсона, переменный ток, трансформатор, передача электроэнергии, ЭМ волны.',
paras:[
{n:7, title:'Колебательный контур. Свободные ЭМ колебания. Формула Томсона', sub:'$T = 2\\pi\\sqrt{LC}$'},
{n:8, title:'Вынужденные ЭМ колебания. Переменный ток', sub:'$i = I_0\\sin(\\omega t)$, $I = I_0/\\sqrt{2}$'},
{n:9, title:'Преобразование переменного тока. Трансформатор', sub:'$k = N_1/N_2 = U_1/U_2$'},
{n:10, title:'Производство, передача и потребление электроэнергии', sub:'ГЭС, ТЭС, АЭС; потери $P = I^2 R$'},
{n:11, title:'Экологические проблемы производства и передачи электроэнергии', sub:'ВЭС, СЭС, гео- и приливные'},
{n:12, title:'ЭМ волны. Шкала ЭМ волн', sub:'$c = 3 \\cdot 10^8$ м/с'},
{n:13, title:'Действие ЭМ излучения на живые организмы', sub:'Ионизирующее vs неионизирующее'}
]
},
{ n:3, slug:'physics-11-ch3', name:'Оптика',
paraRange:'§14–§23', wm:'◇', themeName:'amber',
gradient:['#b45309','#d97706','#fcd34d'],
pri:'#d97706', pri2:'#b45309', priSoft:'#fef3c7',
desc:'Электромагнитная природа света, интерференция, дифракция, отражение, зеркала, преломление, тонкая линза, оптические приборы.',
paras:[
{n:14, title:'ЭМ природа света. Скорость света', sub:'Опыты Рёмера, Майкельсона'},
{n:15, title:'Интерференция света', sub:'$\\Delta = k\\lambda$ (max), $\\Delta = (2k+1)\\lambda/2$ (min)'},
{n:16, title:'Принцип Гюйгенса – Френеля. Дифракция. Дифракционная решётка', sub:'$d\\sin\\varphi = k\\lambda$'},
{n:17, title:'Прямолинейное распространение и отражение света. Зеркала', sub:'$\\angle_{пад} = \\angle_{отр}$'},
{n:18, title:'Сферические зеркала. Построение изображений', sub:'$\\frac{1}{F} = \\frac{1}{d} + \\frac{1}{f}$'},
{n:19, title:'Закон преломления света. Полное отражение', sub:'$n_1\\sin\\alpha = n_2\\sin\\beta$, $\\sin\\alpha_{пр} = 1/n$'},
{n:20, title:'Прохождение света через оптические элементы', sub:'Призмы, оптоволокно'},
{n:21, title:'Формула тонкой линзы', sub:'$D = 1/F$, $\\Gamma = f/d$'},
{n:22, title:'Оптические приборы для действительных изображений', sub:'Фотоаппарат, проектор'},
{n:23, title:'Оптические приборы для увеличения угла зрения', sub:'Лупа, микроскоп, телескоп'}
]
},
{ n:4, slug:'physics-11-ch4', name:'Основы СТО',
paraRange:'§24–§26', wm:'c', themeName:'blue',
gradient:['#1e3a8a','#2563eb','#93c5fd'],
pri:'#2563eb', pri2:'#1d4ed8', priSoft:'#dbeafe',
desc:'Принцип относительности Галилея, постулаты Эйнштейна, преобразования Лоренца, релятивистская динамика, E=mc².',
paras:[
{n:24, title:'Принцип относ. Галилея и ЭМ явления. Эксп. предпосылки СТО', sub:'Опыт Майкельсона – Морли'},
{n:25, title:'Постулаты специальной теории относительности', sub:'$\\Delta t = \\gamma\\Delta t_0$, $l = l_0/\\gamma$'},
{n:26, title:'Элементы релятивистской динамики. Взаимосвязь массы и энергии', sub:'$E_0 = mc^2$, $E^2 = (mc^2)^2 + (pc)^2$'}
]
},
{ n:5, slug:'physics-11-ch5', name:'Фотоны. Действия света',
paraRange:'§27–§29', wm:'γ', themeName:'pink',
gradient:['#831843','#db2777','#fbcfe8'],
pri:'#db2777', pri2:'#9d174d', priSoft:'#fce7f3',
desc:'Фотоэффект, квантовая гипотеза Планка, фотон, уравнение Эйнштейна, давление света, корпускулярно-волновой дуализм.',
paras:[
{n:27, title:'Фотоэффект. Эксперим. законы. Квантовая гипотеза Планка', sub:'$E = h\\nu$, $h = 6{,}63 \\cdot 10^{-34}$ Дж·с'},
{n:28, title:'Фотон. Уравнение Эйнштейна для фотоэффекта', sub:'$h\\nu = A_{вых} + \\frac{mv_{max}^2}{2}$'},
{n:29, title:'Давление света. Корпускулярно-волновой дуализм', sub:'$p_{фот} = h\\nu/c$. Опыт Лебедева'}
]
},
{ n:6, slug:'physics-11-ch6', name:'Физика атома',
paraRange:'§30–§34', wm:'⚛', themeName:'emerald',
gradient:['#065f46','#10b981','#a7f3d0'],
pri:'#10b981', pri2:'#047857', priSoft:'#d1fae5',
desc:'Ядерная модель атома Резерфорда, квантовые постулаты Бора, спектры испускания и поглощения, лазеры.',
paras:[
{n:30, title:'Сложное строение атома. Ядерная модель атома', sub:'Опыт Резерфорда, размер ядра $\\sim 10^{-15}$ м'},
{n:31, title:'Квантовые постулаты Бора', sub:'$E_n = -E_1/n^2 = -13{,}6/n^2$ эВ'},
{n:32, title:'Излучение и поглощение света атомом. Спектры', sub:'$h\\nu = E_n - E_m$, линейчатые спектры'},
{n:33, title:'Спонтанное и индуцированное излучение', sub:'Подготовка к лазерам'},
{n:34, title:'Лазеры', sub:'Инверсная населённость, когерентность'}
]
},
{ n:7, slug:'physics-11-ch7', name:'Ядерная физика и элементарные частицы',
paraRange:'§35–§44', wm:'☢', themeName:'rose',
gradient:['#7f1d1d','#dc2626','#fca5a5'],
pri:'#dc2626', pri2:'#991b1b', priSoft:'#fee2e2',
desc:'Протонно-нейтронная модель ядра, ядерные реакции, энергия связи, радиоактивность, ядерный реактор, термояд, элементарные частицы.',
paras:[
{n:35, title:'Протонно-нейтронная модель строения ядра атома', sub:'$A = Z + N$, изотопы'},
{n:36, title:'Ядерные реакции. Законы сохранения в ядерных реакциях', sub:'Сохранение заряда, нуклонов, энергии'},
{n:37, title:'Энергия связи ядра атома', sub:'$E_{св} = \\Delta m \\cdot c^2$, $\\Delta m = Zm_p + Nm_n - m_я$'},
{n:38, title:'Радиоактивность', sub:'$\\alpha$, $\\beta$, $\\gamma$ распады'},
{n:39, title:'Закон радиоактивного распада', sub:'$N = N_0 \\cdot 2^{-t/T}$, период полураспада $T$'},
{n:40, title:'Деление тяжёлых ядер. Цепные ядерные реакции', sub:'$^{235}$U, $k$ — коэф. размножения'},
{n:41, title:'Ядерный реактор', sub:'Управляющие стержни, замедлитель'},
{n:42, title:'Реакции ядерного синтеза', sub:'Термояд, $^2$H + $^3$H $\\to ^4$He + n'},
{n:43, title:'Ионизирующее излучение. Элементы дозиметрии', sub:'Доза $D$, эквивалент $H$, зиверт'},
{n:44, title:'Элементарные частицы и их взаимодействия', sub:'Стандартная модель, 4 фундаментальных взаимодействия'}
]
},
{ n:8, slug:'physics-11-ch8', name:'Основы единой физической картины мира',
paraRange:'§45', wm:'∞', themeName:'indigo',
gradient:['#3730a3','#6366f1','#c7d2fe'],
pri:'#6366f1', pri2:'#4338ca', priSoft:'#e0e7ff',
desc:'Современная естественнонаучная картина мира, эволюция физических теорий, четыре фундаментальных взаимодействия.',
paras:[
{n:45, title:'Современная естественнонаучная картина мира', sub:'Эволюция представлений: механика → ЭМ → квант'}
]
}
];
function makeChapter(c){
/* В какой волне будет реализована эта глава (см. PLAN_FIZIKA_11.md) */
const waveOf = {1:'W1-W2', 2:'W3-W4', 3:'W5-W7', 4:'W8', 5:'W9', 6:'W10-W11', 7:'W12-W13', 8:'W14'};
const wave = waveOf[c.n] || 'W1+';
const parasHtml = c.paras.map(p => `
<article class="para-card">
<div class="para-num">§ ${p.n}</div>
<div class="para-body">
<h2 class="para-title">${p.title}</h2>
<p class="para-sub">${p.sub}</p>
<div class="para-status">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Будет добавлено в волне ${wave}
</div>
</div>
</article>`).join('\n');
return `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Физика 11 · Глава ${c.n} · ${c.name}</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@400;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.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-fx.js?v=1" defer></script>
<style>
:root{
--bg:#f8fafc; --card:#fff;
--text:#0f172a; --muted:#475569;
--border:#e2e8f0;
--pri:${c.pri}; --pri-d:${c.pri2};
--pri-soft:${c.priSoft};
--dark:${c.gradient[0]};
--sh:0 4px 16px rgba(0,0,0,.06);
}
html.dark{
--bg:#020617; --card:#0a1929;
--text:#dbeafe; --muted:#94a3b8;
--border:#1e293b;
}
*{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.55;transition:background .25s,color .25s}
.hdr{position:relative;background:linear-gradient(110deg,${c.gradient[0]} 0%,${c.gradient[1]} 55%,${c.gradient[2]} 100%);color:#fff;padding:32px 24px 28px;overflow:hidden}
.hdr::before{content:'${c.wm}';position:absolute;right:8px;top:-20%;font-family:'Outfit',sans-serif;font-size:clamp(8rem,22vw,18rem);font-weight:900;color:rgba(255,255,255,.10);line-height:1;pointer-events:none;user-select:none}
.hdr-inner{position:relative;z-index:1;max-width:1100px;margin:0 auto;display:flex;align-items:center;gap:18px;flex-wrap:wrap}
.hdr-back{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;background:rgba(255,255,255,.14);border-radius:9px;color:#fff;text-decoration:none;font-size:.85rem;font-weight:600;transition:background .15s}
.hdr-back:hover{background:rgba(255,255,255,.24)}
.hdr h1{font-family:'Outfit',sans-serif;font-size:1.7rem;font-weight:900;letter-spacing:-.01em}
.hdr-sub{font-size:.92rem;opacity:.85;margin-top:4px}
.ic{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
main{max-width:980px;margin:0 auto;padding:32px 24px 60px}
.intro-card{background:var(--card);border:1.5px solid var(--border);border-radius:16px;padding:22px 26px;margin-bottom:28px;box-shadow:var(--sh)}
.intro-num{display:inline-block;padding:4px 10px;background:var(--pri-soft);color:var(--pri-d);border-radius:99px;font-size:.72rem;font-weight:800;letter-spacing:.06em;margin-bottom:8px;text-transform:uppercase}
.intro-card h2{font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:800;margin-bottom:6px}
.intro-card p{color:var(--muted);font-size:.95rem}
.para-grid{display:grid;grid-template-columns:1fr;gap:14px}
.para-card{background:var(--card);border:1.5px solid var(--border);border-radius:14px;padding:18px 20px;display:flex;gap:16px;align-items:flex-start;transition:transform .15s,box-shadow .15s,border-color .15s}
.para-card:hover{transform:translateY(-2px);box-shadow:var(--sh);border-color:var(--pri)}
.para-num{font-family:'Outfit',sans-serif;font-size:1rem;font-weight:900;color:#fff;background:linear-gradient(135deg,var(--pri),var(--pri-d));padding:8px 12px;border-radius:9px;min-width:56px;text-align:center;letter-spacing:-.02em;flex-shrink:0}
.para-body{flex:1}
.para-title{font-family:'Outfit',sans-serif;font-size:1.05rem;font-weight:800;margin-bottom:4px;color:var(--text)}
.para-sub{font-size:.88rem;color:var(--muted);margin-bottom:10px;line-height:1.55}
.para-status{display:inline-flex;align-items:center;gap:6px;font-size:.78rem;color:var(--muted);background:rgba(0,0,0,.04);padding:6px 10px;border-radius:8px;font-weight:600}
html.dark .para-status{background:rgba(255,255,255,.06)}
.para-status .ic{width:14px;height:14px}
.banner-soon{margin-top:30px;text-align:center;padding:20px;background:linear-gradient(135deg,var(--pri-soft),transparent);border:1px dashed var(--pri);border-radius:14px;color:var(--pri-d);font-weight:700;font-size:.92rem}
.banner-soon b{font-family:'Outfit',sans-serif}
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border)}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<div>
<a href="/textbook/physics-11" class="hdr-back">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К курсу физики 11
</a>
</div>
<div>
<h1>Глава ${c.n}. ${c.name}</h1>
<div class="hdr-sub">${c.desc.split('.')[0]} · ${c.paraRange}</div>
</div>
</div>
</header>
<main>
<div class="intro-card">
<span class="intro-num">Глава ${c.n}</span>
<h2>${c.name}</h2>
<p>${c.desc} Глава содержит ${c.paras.length} параграф${c.paras.length === 1 ? '' : (c.paras.length < 5 ? 'а' : 'ов')} и финальный этап с боссами.</p>
</div>
<div class="para-grid">
${parasHtml}
</div>
<div class="banner-soon">
<b>Глава в разработке.</b> Полная реализация — в следующих волнах. Базовая библиотека <code>phys-fx.js</code> уже доступна.
</div>
</main>
<footer class="foot">
Физика — 11 класс · Глава ${c.n} · LearnSpace
</footer>
</body>
</html>
`;
}
function makeHub(){
const cards = CHAPTERS.map((c, i) => `
<a href="/textbook/${c.slug}" class="ch-card" style="--ch:${c.pri};--ch-d:${c.pri2};--ch-soft:${c.priSoft}">
<div class="ch-cover" style="background:linear-gradient(135deg,${c.gradient[0]},${c.gradient[1]} 60%,${c.gradient[2]})">
<div class="ch-cover-wm">${c.wm}</div>
<div class="ch-num">Глава ${c.n}</div>
<div class="ch-title">${c.name}</div>
<div class="ch-range">${c.paraRange} + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">${c.desc}</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-${c.n}">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-${c.n}" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-${c.n}">Открыть главу</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>`).join('\n');
return `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Физика 11 класс — учебник</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&family=Inter:wght@400;500;600;700&family=Unbounded:wght@400;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.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>
<style>
:root{
--bg:#ecfeff; --card:#fff;
--text:#0f172a; --muted:#475569;
--border:#a5f3fc;
--pri:#0891b2; --pri-d:#0e7490;
--pri-soft:#cffafe;
--sh:0 4px 16px rgba(8,145,178,.10);
--sh-h:0 12px 36px rgba(8,145,178,.18);
}
html.dark{
--bg:#062326; --card:#0a2e35;
--text:#cffafe; --muted:#67e8f9;
--border:#0f4750;
}
*{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.55;transition:background .25s,color .25s}
.hdr{position:relative;background:linear-gradient(110deg,#155e75 0%,#0891b2 55%,#67e8f9 100%);color:#fff;padding:32px 24px 28px;overflow:hidden;border-bottom:2px solid rgba(165,243,252,.18)}
.hdr::before{content:'ФИЗИКА';position:absolute;right:-14px;top:-18%;font-family:'Outfit',sans-serif;font-size:clamp(5rem,16vw,13rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(207,250,254,.12);line-height:1;pointer-events:none;user-select:none}
.hdr-inner{position:relative;z-index:1;max-width:1180px;margin:0 auto;display:flex;align-items:center;gap:18px;flex-wrap:wrap}
.hdr-back{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;background:rgba(255,255,255,.14);border-radius:9px;color:#fff;text-decoration:none;font-size:.85rem;font-weight:600;transition:background .15s}
.hdr-back:hover{background:rgba(255,255,255,.24)}
.hdr h1{font-family:'Outfit',sans-serif;font-size:1.85rem;font-weight:900;letter-spacing:-.01em}
.hdr-sub{font-size:.92rem;opacity:.88;margin-top:4px}
.ic{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
main{max-width:1180px;margin:0 auto;padding:32px 24px 60px}
.prog-overall{background:linear-gradient(135deg,var(--pri-soft),rgba(103,232,249,.12));border:1px solid var(--border);border-radius:14px;padding:14px 18px;margin-bottom:28px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}
.po-icon{width:46px;height:46px;border-radius:12px;background:linear-gradient(135deg,#0891b2,#67e8f9);color:#fff;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-family:'Outfit',sans-serif;font-size:1.4rem;font-weight:900}
.po-text{flex:1;min-width:160px}
.po-label{font-size:.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px}
.po-bar{height:8px;background:rgba(8,145,178,.14);border-radius:5px;overflow:hidden;margin-top:6px}
.po-fill{height:100%;background:linear-gradient(90deg,var(--pri),#67e8f9);border-radius:5px;transition:width .5s}
.ch-grid{display:grid;grid-template-columns:1fr;gap:18px;margin-bottom:30px}
@media(min-width:680px){.ch-grid{grid-template-columns:1fr 1fr}}
@media(min-width:1100px){.ch-grid{grid-template-columns:repeat(4,1fr)}}
.ch-card{background:var(--card);border:1.5px solid var(--border);border-radius:18px;overflow:hidden;display:flex;flex-direction:column;transition:transform .2s,box-shadow .2s,border-color .2s;cursor:pointer;text-decoration:none;color:inherit}
.ch-card:hover{transform:translateY(-4px);box-shadow:var(--sh-h)}
.ch-cover{padding:22px 22px 18px;color:#fff;position:relative;overflow:hidden}
.ch-cover-wm{position:absolute;right:-8px;top:-22px;font-size:5.2rem;font-weight:900;font-family:'Outfit',sans-serif;line-height:1;color:rgba(255,255,255,.20);pointer-events:none;letter-spacing:-.04em}
.ch-num{display:inline-block;padding:4px 10px;background:rgba(255,255,255,.22);border-radius:99px;font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;position:relative;z-index:1}
.ch-title{font-family:'Outfit',sans-serif;font-size:1.05rem;font-weight:800;letter-spacing:-.01em;position:relative;z-index:1;line-height:1.3}
.ch-range{font-size:.82rem;opacity:.88;margin-top:4px;position:relative;z-index:1;font-weight:500}
.ch-body{padding:16px 20px 18px;display:flex;flex-direction:column;flex:1}
.ch-desc{font-size:.86rem;color:var(--text);opacity:.84;flex:1;margin-bottom:12px;line-height:1.55}
.ch-prog{margin-bottom:12px}
.ch-prog-label{display:flex;justify-content:space-between;font-size:.74rem;color:var(--muted);font-weight:600;margin-bottom:4px}
.ch-prog-bar{height:6px;background:rgba(0,0,0,.07);border-radius:4px;overflow:hidden}
.ch-prog-fill{height:100%;border-radius:4px;background:linear-gradient(90deg,var(--ch),var(--ch-d));transition:width .5s}
.ch-action{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-radius:11px;font-weight:700;font-size:.9rem;color:#fff;background:linear-gradient(135deg,var(--ch),var(--ch-d));transition:filter .15s}
.ch-action:hover{filter:brightness(1.08)}
.banner-soon{margin-top:18px;text-align:center;padding:20px;background:linear-gradient(135deg,var(--pri-soft),transparent);border:1px dashed var(--pri);border-radius:14px;color:var(--pri-d);font-weight:700;font-size:.92rem}
.banner-soon b{font-family:'Outfit',sans-serif;display:block;margin-bottom:4px;font-size:1.05rem}
.foot{text-align:center;padding:24px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border)}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<div>
<a href="/textbooks" class="hdr-back">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К каталогу
</a>
</div>
<div>
<h1>Физика — 11 класс</h1>
<div class="hdr-sub">Жилко · Маркович · Сокольский (2021) · 8 глав · 45 параграфов</div>
</div>
</div>
</header>
<main>
<section class="prog-overall">
<div class="po-icon">∿</div>
<div class="po-text">
<div class="po-label">Общий прогресс по курсу</div>
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
</div>
</section>
<div class="ch-grid">
${cards}
</div>
<div class="banner-soon">
<b>Курс в активной разработке (W0)</b>
Инфраструктура готова: миграция БД, библиотека phys-fx.js (Oscillogram, SpringMass, Pendulum) и 8 stub-страниц глав. Реализация по плану PLAN_FIZIKA_11.md — 15 волн (~26 сессий).
</div>
</main>
<footer class="foot">
Интерактивный учебник «Физика — 11 класс» · LearnSpace
</footer>
<script>
'use strict';
var TOTAL = 45;
var CH_PARA = {${CHAPTERS.map(c => "'" + c.slug + "': " + c.paras.length).join(', ')}};
var CH_IDX = {${CHAPTERS.map(c => "'" + c.slug + "': " + c.n).join(', ')}};
function setChProg(idx, readCount, total){
var pct = total ? Math.round(readCount * 100 / total) : 0;
var labelEl = document.getElementById('prog-' + idx);
var fillEl = document.getElementById('fill-' + idx);
var btnEl = document.getElementById('btn-' + idx);
if (labelEl) labelEl.textContent = pct + '%';
if (fillEl) fillEl.style.width = pct + '%';
if (btnEl){
if (readCount > 0 && readCount < total) btnEl.textContent = 'Продолжить';
else if (readCount >= total) btnEl.textContent = 'Открыть снова';
else btnEl.textContent = 'Открыть главу';
}
}
function renderProgress(children){
var totalRead = 0;
for (var i = 0; i < children.length; i++){
var ch = children[i];
var idx = CH_IDX[ch.slug]; if (!idx) continue;
var read = ch.progress ? ch.progress.read.length : 0;
var total = ch.para_count || CH_PARA[ch.slug] || 1;
totalRead += read;
setChProg(idx, read, total);
}
var pct = Math.round(totalRead * 100 / TOTAL);
var overallEl = document.getElementById('overall-text');
var fillEl = document.getElementById('overall-fill');
if (overallEl) overallEl.textContent = totalRead + ' из ' + TOTAL + ' параграфов · ' + pct + '%';
if (fillEl) fillEl.style.width = pct + '%';
}
function loadProgress(){
if (typeof window.LS === 'undefined' || typeof window.LS.api !== 'function'){
renderProgress([]); return;
}
window.LS.api('/api/textbooks/physics-11/children')
.then(function(data){
if (data && data.children) renderProgress(data.children);
else renderProgress([]);
})
.catch(function(){ renderProgress([]); });
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', loadProgress);
else loadProgress();
window.addEventListener('focus', loadProgress);
</script>
</body>
</html>
`;
}
/* Write all 9 files */
fs.writeFileSync(path.join(OUT, 'physics_11_hub.html'), makeHub(), 'utf8');
console.log('Wrote: physics_11_hub.html');
CHAPTERS.forEach(c => {
const fname = 'physics_11_ch' + c.n + '.html';
fs.writeFileSync(path.join(OUT, fname), makeChapter(c), 'utf8');
console.log('Wrote:', fname);
});
console.log('Done. 9 stub files generated.');