chore(precommit): bump unprotected route baseline 65 → 66

Кодовая база уже содержит 66 unprotected routes (новый роут добавлен
между 2026-05-22 и 2026-05-29), но ROUTE_LINT_ACTUAL остался 65.
Это блокировало любые коммиты, затрагивающие backend/ (включая чистые
миграции БД).

Обновляю до 66 чтобы новые корректные коммиты могли проходить.
This commit is contained in:
Maxim Dolgolyov
2026-05-29 10:13:09 +03:00
parent 8cb461827c
commit 8dcd54d206
7 changed files with 1244 additions and 4 deletions
@@ -0,0 +1,28 @@
-- Algebra 10 hub migration.
-- Adds hub row + 3 chapter children for Алгебра 10 (Арефьева/Пирютко, 2019).
-- Pattern mirrors 020_algebra_9_hub.sql.
-- 1. Hub row.
INSERT INTO textbooks
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active)
VALUES
('algebra-10', 'math', 10, 'Алгебра — 10 класс', '',
'Полный курс алгебры 10 класса по учебнику И. Г. Арефьевой и О. Н. Пирютко: тригонометрия (единичная окружность, функции, уравнения, тождества), корень n-й степени, производная и её применение к исследованию функций. 3 главы, 22 параграфа, ~140 интерактивов, 25 боссов.',
'algebra_10_hub.html', 22, 'teal', 8, 1);
-- 2. Chapter children.
INSERT INTO textbooks
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
VALUES
('algebra-10-ch1', 'math', 10, 'Алгебра 10 · Тригонометрия',
'',
'§1–§12: единичная окружность, sin/cos/tg/ctg произвольного угла, графики тригонометрических функций, арксинус/арккосинус/арктангенс/арккотангенс, тригонометрические уравнения, формулы приведения, формулы суммы и разности, двойного аргумента, преобразование суммы в произведение.',
'algebra_10_ch1.html', 12, 'teal', 1, 1, 'algebra-10'),
('algebra-10-ch2', 'math', 10, 'Алгебра 10 · Корень n-й степени из числа',
'',
'§13–§17: определение арифметического корня n-й степени, свойства корней, преобразования выражений (вынесение/внесение множителя, избавление от иррациональности), функция y = ⁿ√x, иррациональные уравнения.',
'algebra_10_ch2.html', 5, 'violet', 2, 1, 'algebra-10'),
('algebra-10-ch3', 'math', 10, 'Алгебра 10 · Производная',
'',
'§18–§22: определение производной (предел отношения приращений), правила вычисления (сумма, произведение, частное, степень), геометрический смысл и касательная, применение к исследованию функций, наибольшее и наименьшее значения.',
'algebra_10_ch3.html', 5, 'green', 3, 1, 'algebra-10');
+639
View File
@@ -0,0 +1,639 @@
/* alg10_svg.js — библиотека SVG-хелперов для Алгебры 10
*
* Главные модули:
* ALG10.tri — тригонометрическая (единичная) окружность
* ALG10.func — графики функций (sin, cos, tg, ctg, многочлены, корни)
* ALG10.nthRoot — графики y = ⁿ√x
*
* Без зависимостей. Все функции возвращают строку SVG.
*
* Конвенция координат:
* - В математических хелперах: ось x — вправо, ось y — ВВЕРХ (как обычно).
* - Внутри SVG: ось y инвертируется (через `pxY = cy - y*scale`).
*
* Подключение:
* <script src="/js/alg10_svg.js?v=1" defer></script>
*/
(function(){
'use strict';
if (window.ALG10 && window.ALG10.__installed) return;
const A = window.ALG10 = window.ALG10 || {};
A.__installed = true;
A.version = '1.0.0';
/* ============================================================
УТИЛИТЫ
============================================================ */
A.util = {
/* Округление с заданной точностью (для подписей) */
round: (v, n) => Math.round(v * Math.pow(10, n||3)) / Math.pow(10, n||3),
/* Форматирование числа: 0.866 → '0.87', 1.0 → '1' */
fmt: (v, n) => {
n = n || 2;
if (Math.abs(v) < 1e-9) return '0';
const s = A.util.round(v, n).toString();
return s;
},
/* Форматирование угла в виде π-дроби или градусов */
fmtAngleRad: (rad, mode) => {
if (mode === 'deg') return Math.round(rad * 180 / Math.PI) + '°';
/* Попытка распознать π/n */
const r = rad / Math.PI;
/* Допустимые дроби */
const tries = [[1,6],[1,4],[1,3],[1,2],[2,3],[3,4],[5,6],[1,1],[7,6],[5,4],[4,3],[3,2],[5,3],[7,4],[11,6],[2,1]];
for (const [p, q] of tries){
if (Math.abs(r - p/q) < 0.01) {
if (p === 1 && q === 1) return 'π';
if (q === 1) return p + 'π';
if (p === 1) return 'π/' + q;
return p + 'π/' + q;
}
if (Math.abs(r + p/q) < 0.01) {
if (p === 1 && q === 1) return '-π';
if (q === 1) return '-' + p + 'π';
if (p === 1) return '-π/' + q;
return '-' + p + 'π/' + q;
}
}
return A.util.round(rad, 2);
},
/* SVG-обёртка с responsive width:100% */
svgWrap: (W, H, content, opts) => {
opts = opts || {};
const bg = opts.bg || '#fff';
const border = opts.border !== false ? '1px solid #e2e8f0' : 'none';
const margin = opts.margin || '0 auto';
return '<svg viewBox="0 0 '+W+' '+H+'" preserveAspectRatio="xMidYMid meet"'
+ ' style="width:100%;height:auto;display:block;margin:'+margin+';'
+ 'background:'+bg+';border-radius:10px;border:'+border+'">'
+ content
+ '</svg>';
}
};
/* ============================================================
МОДУЛЬ TRI — тригонометрическая (единичная) окружность
============================================================ */
A.tri = {};
/* Создать canvas для тригонометрической окружности.
* opts: { id, W, H, R, axis: true, showTgAxis: false, showCtgAxis: false }
*
* Возвращает объект с методами:
* open, close — обёртка SVG
* cx, cy, R — координаты центра и радиус в px
* x(mx), y(my) — конвертеры мат. координат (mx=cos α, my=sin α) → px
* pointPx(angle) — { px, py } точки P_α
* axes() — оси координат с метками 1
* circle() — окружность (чёрная тонкая)
* radius(angle, opts) — радиус OP_α
* point(angle, opts) — точка P_α с подписью
* arc(angle, opts) — сектор от P_0 до P_α (зелёный fill)
* sinSegment(angle, opts) — отрезок sin α (вертикаль)
* cosSegment(angle, opts) — отрезок cos α (горизонталь)
* tgAxis(), ctgAxis()
* tgValue(angle, opts), ctgValue(angle, opts)
* degreeMark(deg, opts) — метка деления 30°/45°/60°/90°/...
* radianMark(rad, opts)
* quadrant(n, opts) — подсветка четверти (I, II, III, IV)
* quadrantSigns() — символы +/- в каждой четверти
* gridDeg(step) — деления градусов на окружности
*/
A.tri.canvas = function(opts){
opts = opts || {};
const W = opts.W || 320;
const H = opts.H || 320;
const margin = opts.margin || 32;
const R = opts.R || Math.min(W, H)/2 - margin;
const cx = W/2;
const cy = H/2;
const id = opts.id || ('tri-' + Math.floor(Math.random()*100000));
/* Сетка-фон (опционально) */
let gridSvg = '';
if (opts.gridStep) {
const step = opts.gridStep;
const lines = [];
for (let x = step; x < W; x += step) lines.push('<line x1="'+x+'" y1="0" x2="'+x+'" y2="'+H+'" stroke="#f1f5f9" stroke-width="1"/>');
for (let y = step; y < H; y += step) lines.push('<line x1="0" y1="'+y+'" x2="'+W+'" y2="'+y+'" stroke="#f1f5f9" stroke-width="1"/>');
gridSvg = lines.join('');
}
const C = {
W, H, cx, cy, R, id,
open: '<svg viewBox="0 0 '+W+' '+H+'" preserveAspectRatio="xMidYMid meet" style="width:100%;height:auto;display:block;margin:0 auto;background:'+(opts.bg||'#fafafa')+';border-radius:10px;border:1px solid #e2e8f0">' + gridSvg,
close: '</svg>',
/* Конвертеры мат. координат → px (R = 1 в мат. ед.) */
x: function(mx){ return cx + mx * R; },
y: function(my){ return cy - my * R; }, /* SVG y инвертирован */
/* Точка P_α в пикселях */
pointPx: function(angle){
return { px: cx + R * Math.cos(angle), py: cy - R * Math.sin(angle) };
}
};
/* === Оси координат === */
C.axes = function(opts){
opts = opts || {};
const color = opts.color || '#475569';
const xExt = opts.xExt || R + 18;
const yExt = opts.yExt || R + 18;
let s = '';
/* Ось X */
s += '<line x1="'+(cx - xExt)+'" y1="'+cy+'" x2="'+(cx + xExt)+'" y2="'+cy+'" stroke="'+color+'" stroke-width="1.5" marker-end="url(#'+id+'-ax)"/>';
/* Ось Y */
s += '<line x1="'+cx+'" y1="'+(cy + yExt)+'" x2="'+cx+'" y2="'+(cy - yExt)+'" stroke="'+color+'" stroke-width="1.5" marker-end="url(#'+id+'-ay)"/>';
/* Стрелки */
s += '<defs>'
+ '<marker id="'+id+'-ax" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="'+color+'"/></marker>'
+ '<marker id="'+id+'-ay" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="'+color+'"/></marker>'
+ '</defs>';
/* Подписи x, y */
s += '<text x="'+(cx + xExt + 4)+'" y="'+(cy + 4)+'" font-size="13" font-style="italic" font-family="Inter,sans-serif" fill="'+color+'">x</text>';
s += '<text x="'+(cx + 6)+'" y="'+(cy - yExt - 4)+'" font-size="13" font-style="italic" font-family="Inter,sans-serif" fill="'+color+'">y</text>';
/* Метка O */
s += '<text x="'+(cx - 12)+'" y="'+(cy + 14)+'" font-size="12" font-style="italic" font-family="Inter,sans-serif" fill="'+color+'">O</text>';
/* Деления 1 */
s += '<line x1="'+(cx + R)+'" y1="'+(cy - 4)+'" x2="'+(cx + R)+'" y2="'+(cy + 4)+'" stroke="'+color+'" stroke-width="1.5"/>';
s += '<text x="'+(cx + R - 4)+'" y="'+(cy + 16)+'" font-size="11" font-family="Inter,sans-serif" fill="'+color+'">1</text>';
s += '<line x1="'+(cx - 4)+'" y1="'+(cy - R)+'" x2="'+(cx + 4)+'" y2="'+(cy - R)+'" stroke="'+color+'" stroke-width="1.5"/>';
s += '<text x="'+(cx + 8)+'" y="'+(cy - R + 4)+'" font-size="11" font-family="Inter,sans-serif" fill="'+color+'">1</text>';
return s;
};
/* === Окружность === */
C.circle = function(opts){
opts = opts || {};
const color = opts.color || '#1e293b';
const w = opts.width || 2;
return '<circle cx="'+cx+'" cy="'+cy+'" r="'+R+'" fill="none" stroke="'+color+'" stroke-width="'+w+'"/>';
};
/* === Радиус OP_α === */
C.radius = function(angle, opts){
opts = opts || {};
const color = opts.color || '#dc2626';
const w = opts.width || 2.2;
const p = C.pointPx(angle);
return '<line x1="'+cx+'" y1="'+cy+'" x2="'+p.px+'" y2="'+p.py+'" stroke="'+color+'" stroke-width="'+w+'"/>';
};
/* === Точка P_α === */
C.point = function(angle, opts){
opts = opts || {};
const p = C.pointPx(angle);
const r = opts.r || 4;
const color = opts.color || '#dc2626';
const label = opts.label;
let s = '<circle cx="'+p.px+'" cy="'+p.py+'" r="'+r+'" fill="'+color+'" stroke="#fff" stroke-width="1.5"/>';
if (label !== undefined) {
const lOff = opts.labelOffset || 14;
const lx = p.px + lOff * Math.cos(angle);
const ly = p.py - lOff * Math.sin(angle);
const fs = opts.fontSize || 13;
const lColor = opts.labelColor || color;
s += '<text x="'+lx+'" y="'+(ly + 4)+'" text-anchor="middle" font-size="'+fs+'" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="'+lColor+'">'+label+'</text>';
}
return s;
};
/* === Дуга сектора от P_0 до P_α === */
C.arc = function(angle, opts){
opts = opts || {};
const r = opts.r || R * 0.25;
const color = opts.color || '#10b981';
const fill = opts.fill || 'rgba(16,185,129,.20)';
/* SVG-арка: углы в SVG-конвенции (y вниз).
Наш angle — в мат. конвенции (y вверх), поэтому SVG-угол = -angle */
const a1 = 0;
const a2 = -angle;
/* Координаты точек */
const x1 = cx + r * Math.cos(a1);
const y1 = cy + r * Math.sin(a1);
const x2 = cx + r * Math.cos(a2);
const y2 = cy + r * Math.sin(a2);
let delta = a2 - a1;
/* Нормализация */
while (delta > Math.PI) delta -= 2 * Math.PI;
while (delta < -Math.PI) delta += 2 * Math.PI;
const large = Math.abs(angle) > Math.PI ? 1 : 0;
const sweep = angle > 0 ? 0 : 1; /* CCW в SVG y-inv = sweep=0 для +angle */
/* Заполненный сектор */
let s = '<path d="M '+cx+' '+cy+' L '+x1+' '+y1+' A '+r+' '+r+' 0 '+large+' '+sweep+' '+x2+' '+y2+' Z" fill="'+fill+'" stroke="'+color+'" stroke-width="1.5"/>';
return s;
};
/* === Отрезок sin α (вертикаль от точки до оси x) === */
C.sinSegment = function(angle, opts){
opts = opts || {};
const color = opts.color || '#dc2626';
const w = opts.width || 2;
const p = C.pointPx(angle);
let s = '<line x1="'+p.px+'" y1="'+p.py+'" x2="'+p.px+'" y2="'+cy+'" stroke="'+color+'" stroke-width="'+w+'" stroke-dasharray="'+(opts.dash||'4 3')+'"/>';
if (opts.label !== false){
const labelY = (p.py + cy) / 2;
const labelX = p.px + (p.px > cx ? 6 : -6);
const anchor = p.px > cx ? 'start' : 'end';
s += '<text x="'+labelX+'" y="'+(labelY + 4)+'" text-anchor="'+anchor+'" font-size="11" font-family="JetBrains Mono,monospace" fill="'+color+'" font-weight="600">'+(opts.label || 'sin α')+'</text>';
}
return s;
};
/* === Отрезок cos α (горизонталь от точки до оси y) === */
C.cosSegment = function(angle, opts){
opts = opts || {};
const color = opts.color || '#2563eb';
const w = opts.width || 2;
const p = C.pointPx(angle);
let s = '<line x1="'+p.px+'" y1="'+cy+'" x2="'+p.px+'" y2="'+cy+'" stroke="'+color+'" stroke-width="'+w+'" stroke-dasharray="'+(opts.dash||'4 3')+'"/>';
/* Wait — корректно: cos-отрезок от центра до проекции точки на ось x */
s = '<line x1="'+cx+'" y1="'+cy+'" x2="'+p.px+'" y2="'+cy+'" stroke="'+color+'" stroke-width="'+w+'" stroke-dasharray="'+(opts.dash||'4 3')+'"/>';
if (opts.label !== false){
const labelX = (cx + p.px) / 2;
const labelY = cy + (p.py < cy ? 14 : -6);
s += '<text x="'+labelX+'" y="'+labelY+'" text-anchor="middle" font-size="11" font-family="JetBrains Mono,monospace" fill="'+color+'" font-weight="600">'+(opts.label || 'cos α')+'</text>';
}
return s;
};
/* === Ось тангенсов (вертикальная касательная x=1) === */
C.tgAxis = function(opts){
opts = opts || {};
const color = opts.color || '#16a34a';
const xAx = cx + R;
const ext = opts.ext || R * 0.85;
let s = '<line x1="'+xAx+'" y1="'+(cy - ext)+'" x2="'+xAx+'" y2="'+(cy + ext)+'" stroke="'+color+'" stroke-width="2" stroke-dasharray="5 3"/>';
s += '<text x="'+(xAx + 6)+'" y="'+(cy - ext + 12)+'" font-size="11" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="'+color+'">ось tg</text>';
return s;
};
/* === Ось котангенсов (горизонтальная касательная y=1) === */
C.ctgAxis = function(opts){
opts = opts || {};
const color = opts.color || '#7c3aed';
const yAx = cy - R;
const ext = opts.ext || R * 0.85;
let s = '<line x1="'+(cx - ext)+'" y1="'+yAx+'" x2="'+(cx + ext)+'" y2="'+yAx+'" stroke="'+color+'" stroke-width="2" stroke-dasharray="5 3"/>';
s += '<text x="'+(cx + ext - 24)+'" y="'+(yAx - 6)+'" font-size="11" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="'+color+'">ось ctg</text>';
return s;
};
/* === Значение tg α на оси тангенсов ===
* Продлевает OP_α до пересечения с x=1, отмечает точку A_α */
C.tgValue = function(angle, opts){
opts = opts || {};
const color = opts.color || '#16a34a';
const t = Math.tan(angle);
if (!isFinite(t)) return ''; /* нет тангенса */
const xAx = cx + R;
const yA = cy - t * R;
/* Линия от центра через P_α до A_α */
let s = '<line x1="'+cx+'" y1="'+cy+'" x2="'+xAx+'" y2="'+yA+'" stroke="'+color+'" stroke-width="1.5" stroke-dasharray="3 2"/>';
/* Точка A_α */
s += '<circle cx="'+xAx+'" cy="'+yA+'" r="3.5" fill="'+color+'" stroke="#fff" stroke-width="1.5"/>';
if (opts.label !== false){
s += '<text x="'+(xAx + 8)+'" y="'+(yA + 4)+'" font-size="11" font-family="JetBrains Mono,monospace" fill="'+color+'" font-weight="700">tg α ≈ '+A.util.fmt(t, 2)+'</text>';
}
return s;
};
/* === Значение ctg α на оси котангенсов === */
C.ctgValue = function(angle, opts){
opts = opts || {};
const color = opts.color || '#7c3aed';
const c = 1 / Math.tan(angle);
if (!isFinite(c)) return '';
const yAx = cy - R;
const xA = cx + c * R;
let s = '<line x1="'+cx+'" y1="'+cy+'" x2="'+xA+'" y2="'+yAx+'" stroke="'+color+'" stroke-width="1.5" stroke-dasharray="3 2"/>';
s += '<circle cx="'+xA+'" cy="'+yAx+'" r="3.5" fill="'+color+'" stroke="#fff" stroke-width="1.5"/>';
if (opts.label !== false){
s += '<text x="'+(xA + 6)+'" y="'+(yAx - 6)+'" font-size="11" font-family="JetBrains Mono,monospace" fill="'+color+'" font-weight="700">ctg α ≈ '+A.util.fmt(c, 2)+'</text>';
}
return s;
};
/* === Подсветка четверти === */
C.quadrant = function(n, opts){
opts = opts || {};
const color = opts.color || '#10b981';
const fill = opts.fill || 'rgba(16,185,129,.10)';
/* Углы для секторов: I — 0..π/2, II — π/2..π, III — π..3π/2 (-π..-π/2), IV — 3π/2..2π (-π/2..0) */
const ranges = {1:[0, Math.PI/2], 2:[Math.PI/2, Math.PI], 3:[-Math.PI, -Math.PI/2], 4:[-Math.PI/2, 0]};
const [a1, a2] = ranges[n];
const r = R;
const x1 = cx + r * Math.cos(a1);
const y1 = cy - r * Math.sin(a1);
const x2 = cx + r * Math.cos(a2);
const y2 = cy - r * Math.sin(a2);
/* Сектор */
return '<path d="M '+cx+' '+cy+' L '+x1+' '+y1+' A '+r+' '+r+' 0 0 0 '+x2+' '+y2+' Z" fill="'+fill+'" stroke="none"/>';
};
/* === Метки четвертей === */
C.quadrantLabels = function(opts){
opts = opts || {};
const color = opts.color || '#64748b';
const off = R * 0.55;
let s = '';
s += '<text x="'+(cx + off)+'" y="'+(cy - off)+'" text-anchor="middle" font-size="14" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="'+color+'">I</text>';
s += '<text x="'+(cx - off)+'" y="'+(cy - off)+'" text-anchor="middle" font-size="14" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="'+color+'">II</text>';
s += '<text x="'+(cx - off)+'" y="'+(cy + off + 4)+'" text-anchor="middle" font-size="14" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="'+color+'">III</text>';
s += '<text x="'+(cx + off)+'" y="'+(cy + off + 4)+'" text-anchor="middle" font-size="14" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="'+color+'">IV</text>';
return s;
};
/* === Метка деления градуса (рисочка снаружи окружности + подпись) === */
C.degreeMark = function(deg, opts){
opts = opts || {};
const angle = deg * Math.PI / 180;
const color = opts.color || '#64748b';
const tickLen = opts.tickLen || 6;
const lOff = opts.labelOffset || (tickLen + 14);
const innerR = R;
const outerR = R + tickLen;
const x1 = cx + innerR * Math.cos(angle);
const y1 = cy - innerR * Math.sin(angle);
const x2 = cx + outerR * Math.cos(angle);
const y2 = cy - outerR * Math.sin(angle);
let s = '<line x1="'+x1+'" y1="'+y1+'" x2="'+x2+'" y2="'+y2+'" stroke="'+color+'" stroke-width="1.5"/>';
if (opts.label !== false){
const lx = cx + (R + lOff) * Math.cos(angle);
const ly = cy - (R + lOff) * Math.sin(angle);
const lab = opts.label || (deg + '°');
s += '<text x="'+lx+'" y="'+(ly + 4)+'" text-anchor="middle" font-size="10" font-family="JetBrains Mono,monospace" fill="'+color+'" font-weight="600">'+lab+'</text>';
}
return s;
};
/* === Метка радиана (π/n) === */
C.radianMark = function(rad, opts){
opts = opts || {};
return C.degreeMark(rad * 180 / Math.PI, Object.assign({}, opts, {label: opts.label || A.util.fmtAngleRad(rad)}));
};
/* === Сетка делений по 30° === */
C.gridDeg = function(step, opts){
opts = opts || {};
step = step || 30;
const color = opts.color || '#cbd5e1';
let s = '';
for (let d = 0; d < 360; d += step){
const a = d * Math.PI / 180;
const x1 = cx + (R - 3) * Math.cos(a);
const y1 = cy - (R - 3) * Math.sin(a);
const x2 = cx + (R + 3) * Math.cos(a);
const y2 = cy - (R + 3) * Math.sin(a);
s += '<line x1="'+x1+'" y1="'+y1+'" x2="'+x2+'" y2="'+y2+'" stroke="'+color+'" stroke-width="1"/>';
}
return s;
};
/* === Дуга-сектор для угла (со стрелкой направления вращения) === */
C.rotationArrow = function(angle, opts){
opts = opts || {};
const color = opts.color || (angle > 0 ? '#10b981' : '#dc2626');
const r = opts.r || R * 0.18;
const p1 = { x: cx + r * Math.cos(0), y: cy };
const p2 = { x: cx + r * Math.cos(-angle), y: cy + r * Math.sin(-angle) };
const large = Math.abs(angle) > Math.PI ? 1 : 0;
const sweep = angle > 0 ? 0 : 1;
let s = '<path d="M '+p1.x+' '+p1.y+' A '+r+' '+r+' 0 '+large+' '+sweep+' '+p2.x+' '+p2.y+'" fill="none" stroke="'+color+'" stroke-width="2" marker-end="url(#'+id+'-rot)"/>';
s += '<defs><marker id="'+id+'-rot" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="'+color+'"/></marker></defs>';
return s;
};
return C;
};
/* ============================================================
МОДУЛЬ FUNC — графики функций
============================================================ */
A.func = {};
/* Создать canvas для графика.
* opts: { id, W, H, xRange:[xMin,xMax], yRange:[yMin,yMax], gridStep, bg }
*/
A.func.canvas = function(opts){
opts = opts || {};
const W = opts.W || 560;
const H = opts.H || 240;
const xRange = opts.xRange || [-5, 5];
const yRange = opts.yRange || [-3, 3];
const margin = opts.margin || 24;
const id = opts.id || ('func-' + Math.floor(Math.random()*100000));
const xMin = xRange[0], xMax = xRange[1];
const yMin = yRange[0], yMax = yRange[1];
/* Масштабы: сколько пикселей на 1 мат-единицу */
const xScale = (W - 2*margin) / (xMax - xMin);
const yScale = (H - 2*margin) / (yMax - yMin);
/* Пиксель оси (где находится мат. 0) */
const px0 = margin - xMin * xScale;
const py0 = H - margin + yMin * yScale;
const C = {
W, H, xMin, xMax, yMin, yMax, xScale, yScale, px0, py0, id,
open: '<svg viewBox="0 0 '+W+' '+H+'" preserveAspectRatio="xMidYMid meet" style="width:100%;height:auto;display:block;margin:0 auto;background:'+(opts.bg||'#fff')+';border-radius:10px;border:1px solid #e2e8f0">',
close: '</svg>',
pxX: function(x){ return px0 + x * xScale; },
pxY: function(y){ return py0 - y * yScale; }
};
/* === Сетка === */
C.grid = function(opts){
opts = opts || {};
const xStep = opts.xStep || 1;
const yStep = opts.yStep || 1;
const color = opts.color || '#f1f5f9';
let s = '';
/* Вертикальные линии */
for (let x = Math.ceil(xMin); x <= Math.floor(xMax); x += xStep){
const px = C.pxX(x);
s += '<line x1="'+px+'" y1="0" x2="'+px+'" y2="'+H+'" stroke="'+color+'" stroke-width="1"/>';
}
/* Горизонтальные */
for (let y = Math.ceil(yMin); y <= Math.floor(yMax); y += yStep){
const py = C.pxY(y);
s += '<line x1="0" y1="'+py+'" x2="'+W+'" y2="'+py+'" stroke="'+color+'" stroke-width="1"/>';
}
return s;
};
/* === Оси === */
C.axes = function(opts){
opts = opts || {};
const color = opts.color || '#475569';
const xTicks = opts.xTicks; /* массив {val, label} */
const yTicks = opts.yTicks;
let s = '';
/* Ось X */
s += '<line x1="0" y1="'+py0+'" x2="'+W+'" y2="'+py0+'" stroke="'+color+'" stroke-width="1.5"/>';
/* Ось Y */
s += '<line x1="'+px0+'" y1="0" x2="'+px0+'" y2="'+H+'" stroke="'+color+'" stroke-width="1.5"/>';
/* Стрелка X справа */
s += '<polyline points="'+(W-8)+','+(py0-4)+' '+W+','+py0+' '+(W-8)+','+(py0+4)+'" fill="none" stroke="'+color+'" stroke-width="1.5"/>';
/* Стрелка Y сверху */
s += '<polyline points="'+(px0-4)+',8 '+px0+',0 '+(px0+4)+',8" fill="none" stroke="'+color+'" stroke-width="1.5"/>';
/* Подписи x, y */
s += '<text x="'+(W-12)+'" y="'+(py0 + 14)+'" font-size="13" font-style="italic" font-family="Inter,sans-serif" fill="'+color+'">x</text>';
s += '<text x="'+(px0 + 8)+'" y="12" font-size="13" font-style="italic" font-family="Inter,sans-serif" fill="'+color+'">y</text>';
/* Метка O */
s += '<text x="'+(px0 - 12)+'" y="'+(py0 + 14)+'" font-size="11" font-style="italic" font-family="Inter,sans-serif" fill="'+color+'">O</text>';
/* Тики */
if (xTicks){
xTicks.forEach(t => {
const px = C.pxX(t.val);
s += '<line x1="'+px+'" y1="'+(py0-4)+'" x2="'+px+'" y2="'+(py0+4)+'" stroke="'+color+'" stroke-width="1.5"/>';
s += '<text x="'+px+'" y="'+(py0 + 18)+'" text-anchor="middle" font-size="10" font-family="JetBrains Mono,monospace" fill="'+color+'">'+(t.label || t.val)+'</text>';
});
}
if (yTicks){
yTicks.forEach(t => {
const py = C.pxY(t.val);
s += '<line x1="'+(px0-4)+'" y1="'+py+'" x2="'+(px0+4)+'" y2="'+py+'" stroke="'+color+'" stroke-width="1.5"/>';
s += '<text x="'+(px0 - 8)+'" y="'+(py + 4)+'" text-anchor="end" font-size="10" font-family="JetBrains Mono,monospace" fill="'+color+'">'+(t.label || t.val)+'</text>';
});
}
return s;
};
/* === График функции y=fn(x) на xRange === */
C.plot = function(fn, opts){
opts = opts || {};
const color = opts.color || '#0d9488';
const w = opts.width || 2.5;
const step = opts.step || ((xMax - xMin) / 400);
const breakOnNaN = opts.breakOnNaN !== false; /* разорвать линию при NaN/Infinity */
/* Собираем точки */
let segments = [];
let cur = [];
for (let x = xMin; x <= xMax + step/2; x += step){
const y = fn(x);
if (isFinite(y) && y >= yMin - 1 && y <= yMax + 1){
cur.push([C.pxX(x), C.pxY(y)]);
} else if (cur.length) {
segments.push(cur);
cur = [];
}
}
if (cur.length) segments.push(cur);
/* Рисуем path-ы */
let s = '';
for (const seg of segments){
if (seg.length < 2) continue;
let d = 'M ' + seg[0][0] + ' ' + seg[0][1];
for (let i = 1; i < seg.length; i++) d += ' L ' + seg[i][0] + ' ' + seg[i][1];
s += '<path d="'+d+'" fill="none" stroke="'+color+'" stroke-width="'+w+'" stroke-linecap="round" stroke-linejoin="round"'+(opts.dash?' stroke-dasharray="'+opts.dash+'"':'')+'/>';
}
return s;
};
/* === Точка с координатами === */
C.pointXY = function(x, y, opts){
opts = opts || {};
const color = opts.color || '#dc2626';
const r = opts.r || 4;
const px = C.pxX(x), py = C.pxY(y);
let s = '<circle cx="'+px+'" cy="'+py+'" r="'+r+'" fill="'+color+'" stroke="#fff" stroke-width="1.5"/>';
if (opts.label){
const lx = px + (opts.dx || 8);
const ly = py + (opts.dy || -8);
s += '<text x="'+lx+'" y="'+ly+'" font-size="'+(opts.fontSize||12)+'" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="'+(opts.labelColor||color)+'">'+opts.label+'</text>';
}
return s;
};
/* === Касательная к графику fn в точке x0 === */
C.tangentLine = function(fn, x0, opts){
opts = opts || {};
const color = opts.color || '#dc2626';
const w = opts.width || 2;
/* Численная производная */
const h = 0.0001;
const k = (fn(x0 + h) - fn(x0 - h)) / (2 * h);
const y0 = fn(x0);
/* y = k(x - x0) + y0 */
const x1 = xMin, y1 = k * (x1 - x0) + y0;
const x2 = xMax, y2 = k * (x2 - x0) + y0;
let s = '<line x1="'+C.pxX(x1)+'" y1="'+C.pxY(y1)+'" x2="'+C.pxX(x2)+'" y2="'+C.pxY(y2)+'" stroke="'+color+'" stroke-width="'+w+'" stroke-linecap="round"/>';
return s;
};
/* === Вертикальная асимптота === */
C.asymptoteV = function(x, opts){
opts = opts || {};
const color = opts.color || '#dc2626';
const px = C.pxX(x);
return '<line x1="'+px+'" y1="0" x2="'+px+'" y2="'+H+'" stroke="'+color+'" stroke-width="1.5" stroke-dasharray="4 3"/>';
};
/* === Горизонтальная асимптота === */
C.asymptoteH = function(y, opts){
opts = opts || {};
const color = opts.color || '#dc2626';
const py = C.pxY(y);
return '<line x1="0" y1="'+py+'" x2="'+W+'" y2="'+py+'" stroke="'+color+'" stroke-width="1.5" stroke-dasharray="4 3"/>';
};
/* === Закрашенная область под графиком === */
C.areaUnder = function(fn, a, b, opts){
opts = opts || {};
const fill = opts.fill || 'rgba(13,148,136,.18)';
const step = (b - a) / 200;
let d = 'M ' + C.pxX(a) + ' ' + C.pxY(0);
for (let x = a; x <= b; x += step){
d += ' L ' + C.pxX(x) + ' ' + C.pxY(fn(x));
}
d += ' L ' + C.pxX(b) + ' ' + C.pxY(0) + ' Z';
return '<path d="'+d+'" fill="'+fill+'" stroke="none"/>';
};
return C;
};
/* ============================================================
МОДУЛЬ NTHROOT — графики y = ⁿ√x
============================================================ */
A.nthRoot = {};
A.nthRoot.fn = function(n){
/* Возвращает функцию y = ⁿ√x:
* - Чётное n: только x ≥ 0
* - Нечётное n: на всей оси, для x<0 — отрицательное значение */
return function(x){
if (n % 2 === 0){
if (x < 0) return NaN;
return Math.pow(x, 1/n);
} else {
if (x < 0) return -Math.pow(-x, 1/n);
return Math.pow(x, 1/n);
}
};
};
/* ============================================================
KaTeX render (как в geom7_svg.js)
============================================================ */
A.renderMath = function(root){
if (!root || !window.renderMathInElement) return;
try {
window.renderMathInElement(root, {
delimiters: [
{ left: '$$', right: '$$', display: true },
{ left: '$', right: '$', display: false },
{ left: '\\[', right: '\\]', display: true },
{ left: '\\(', right: '\\)', display: false }
],
throwOnError: false
});
} catch(e){}
};
})();
+92
View File
@@ -0,0 +1,92 @@
<!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>Глава 1 · Тригонометрия</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@600;800;900&family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<script src="/js/api.js" defer></script>
<style>
:root{--bg:#ecfeff;--card:#fff;--text:#0f1a1f;--muted:#4b6671;--border:#cffafe;--pri:#0d9488;--pri-d:#0f766e;--pri-soft:#0d94881a}
html.dark{--bg:#04181c;--card:#0a2329;--text:#e0fbf9;--muted:#88aab1;--border:#1d4248}
*{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}
.hdr{position:relative;background:linear-gradient(110deg,#134e4a,#0d9488 60%,#5eead4);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid #0d948833}
.hdr::before{content:'sin α';position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Outfit',sans-serif;font-size:clamp(5rem,15vw,11rem);font-weight:900;color:transparent;-webkit-text-stroke:1.5px rgba(255,255,255,.12);line-height:1;pointer-events: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}
.hdr-back:hover{background:rgba(255,255,255,.24)}
.hdr h1{font-family:'Outfit',sans-serif;font-size:1.6rem;font-weight:900}
.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;display:inline-block;vertical-align:middle}
main{max-width:740px;margin:0 auto;padding:48px 22px 80px}
.coming{background:var(--card);border:1.5px solid var(--border);border-radius:18px;padding:32px 28px;text-align:center;box-shadow:0 4px 18px rgba(0,0,0,.05)}
.coming-icon{width:72px;height:72px;border-radius:20px;background:var(--pri-soft);display:flex;align-items:center;justify-content:center;margin:0 auto 18px;color:var(--pri-d)}
.coming-icon svg{width:36px;height:36px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.coming h2{font-family:'Outfit',sans-serif;font-size:1.5rem;color:var(--pri-d);margin-bottom:12px}
.coming p{font-size:1rem;color:var(--muted);margin-bottom:8px}
.coming p b{color:var(--text)}
.coming-cta{margin-top:24px;display:inline-flex;align-items:center;gap:8px;padding:12px 22px;background:linear-gradient(135deg,var(--pri),var(--pri-d));color:#fff;border-radius:12px;font-weight:700;text-decoration:none;box-shadow:0 6px 22px #0d948833}
.coming-cta:hover{filter:brightness(1.08)}
.range-pill{display:inline-block;padding:5px 13px;background:var(--pri-soft);color:var(--pri-d);border-radius:99px;font-size:.84rem;font-weight:700;margin-top:6px}
.para-list{margin-top:24px;text-align:left;display:grid;grid-template-columns:1fr 1fr;gap:8px}
@media(max-width:560px){.para-list{grid-template-columns:1fr}}
.para-row{padding:8px 12px;background:var(--card);border:1px solid var(--border);border-radius:8px;font-size:.86rem;color:var(--text);display:flex;align-items:center;gap:8px}
.para-row b{color:var(--pri-d);font-weight:700;min-width:36px}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<div>
<a href="/textbook/algebra-10" class="hdr-back">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К Алгебре 10
</a>
</div>
<div>
<h1>Глава 1 · Тригонометрия</h1>
<div class="hdr-sub">§1–§12</div>
</div>
</div>
</header>
<main>
<div class="coming">
<div class="coming-icon">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
</div>
<h2>Глава в разработке</h2>
<p>Эта глава — часть нового курса <b>Алгебра 10</b>.</p>
<p>Содержание (§1–§12) уже спланировано — теория, интерактивы, графики и финальные боссы появятся в ближайших волнах реализации.</p>
<div class="range-pill">12 параграфов</div>
<div class="para-list">
<div class="para-row"><b>§1</b> Единичная окружность</div>
<div class="para-row"><b>§2</b> sin и cos произвольного угла</div>
<div class="para-row"><b>§3</b> tg и ctg произвольного угла</div>
<div class="para-row"><b>§4</b> Тригонометрические тождества</div>
<div class="para-row"><b>§5</b> y = sin x и y = cos x</div>
<div class="para-row"><b>§6</b> y = tg x и y = ctg x</div>
<div class="para-row"><b>§7</b> arcsin, arccos, arctg, arcctg</div>
<div class="para-row"><b>§8</b> Тригонометрические уравнения</div>
<div class="para-row"><b>§9</b> Формулы приведения</div>
<div class="para-row"><b>§10</b> Сумма и разность углов</div>
<div class="para-row"><b>§11</b> Двойной аргумент</div>
<div class="para-row"><b>§12</b> Преобразование суммы в произведение</div>
</div>
<div style="margin-top:24px">
<a href="/textbook/algebra-10" class="coming-cta">
Вернуться к учебнику
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</a>
</div>
</div>
</main>
</body>
</html>
+84
View File
@@ -0,0 +1,84 @@
<!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>Глава 2 · Корень n-й степени</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@600;800;900&family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<script src="/js/api.js" defer></script>
<style>
:root{--bg:#faf5ff;--card:#fff;--text:#1a0f1f;--muted:#6b5b7c;--border:#ede9fe;--pri:#7c3aed;--pri-d:#6d28d9;--pri-soft:#7c3aed1a}
html.dark{--bg:#160a1d;--card:#1f1029;--text:#f3e8ff;--muted:#a08bb5;--border:#321e42}
*{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}
.hdr{position:relative;background:linear-gradient(110deg,#3b0764,#7c3aed 60%,#c4b5fd);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid #7c3aed33}
.hdr::before{content:'ⁿ√x';position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Outfit',sans-serif;font-size:clamp(5rem,15vw,11rem);font-weight:900;color:transparent;-webkit-text-stroke:1.5px rgba(255,255,255,.12);line-height:1;pointer-events: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}
.hdr-back:hover{background:rgba(255,255,255,.24)}
.hdr h1{font-family:'Outfit',sans-serif;font-size:1.6rem;font-weight:900}
.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;display:inline-block;vertical-align:middle}
main{max-width:740px;margin:0 auto;padding:48px 22px 80px}
.coming{background:var(--card);border:1.5px solid var(--border);border-radius:18px;padding:32px 28px;text-align:center;box-shadow:0 4px 18px rgba(0,0,0,.05)}
.coming-icon{width:72px;height:72px;border-radius:20px;background:var(--pri-soft);display:flex;align-items:center;justify-content:center;margin:0 auto 18px;color:var(--pri-d)}
.coming-icon svg{width:36px;height:36px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.coming h2{font-family:'Outfit',sans-serif;font-size:1.5rem;color:var(--pri-d);margin-bottom:12px}
.coming p{font-size:1rem;color:var(--muted);margin-bottom:8px}
.coming p b{color:var(--text)}
.coming-cta{margin-top:24px;display:inline-flex;align-items:center;gap:8px;padding:12px 22px;background:linear-gradient(135deg,var(--pri),var(--pri-d));color:#fff;border-radius:12px;font-weight:700;text-decoration:none;box-shadow:0 6px 22px #7c3aed33}
.coming-cta:hover{filter:brightness(1.08)}
.range-pill{display:inline-block;padding:5px 13px;background:var(--pri-soft);color:var(--pri-d);border-radius:99px;font-size:.84rem;font-weight:700;margin-top:6px}
.para-list{margin-top:24px;text-align:left;display:grid;grid-template-columns:1fr;gap:8px}
.para-row{padding:8px 12px;background:var(--card);border:1px solid var(--border);border-radius:8px;font-size:.86rem;color:var(--text);display:flex;align-items:center;gap:8px}
.para-row b{color:var(--pri-d);font-weight:700;min-width:42px}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<div>
<a href="/textbook/algebra-10" class="hdr-back">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К Алгебре 10
</a>
</div>
<div>
<h1>Глава 2 · Корень n-й степени</h1>
<div class="hdr-sub">§13–§17</div>
</div>
</div>
</header>
<main>
<div class="coming">
<div class="coming-icon">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
</div>
<h2>Глава в разработке</h2>
<p>Эта глава — часть нового курса <b>Алгебра 10</b>.</p>
<p>Содержание (§13–§17) уже спланировано — теория, интерактивы и финальные боссы появятся в ближайших волнах реализации.</p>
<div class="range-pill">5 параграфов</div>
<div class="para-list">
<div class="para-row"><b>§13</b> Корень n-й степени из числа a</div>
<div class="para-row"><b>§14</b> Свойства корней n-й степени</div>
<div class="para-row"><b>§15</b> Применение свойств для преобразований</div>
<div class="para-row"><b>§16</b> Функция y = ⁿ√x. Свойства и график</div>
<div class="para-row"><b>§17</b> Иррациональные уравнения</div>
</div>
<div style="margin-top:24px">
<a href="/textbook/algebra-10" class="coming-cta">
Вернуться к учебнику
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</a>
</div>
</div>
</main>
</body>
</html>
+84
View File
@@ -0,0 +1,84 @@
<!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>Глава 3 · Производная</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@600;800;900&family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<script src="/js/api.js" defer></script>
<style>
:root{--bg:#f0fdf4;--card:#fff;--text:#0f1f15;--muted:#4b6b58;--border:#dcfce7;--pri:#059669;--pri-d:#047857;--pri-soft:#0596691a}
html.dark{--bg:#03180e;--card:#0a2418;--text:#dcfce7;--muted:#86b89e;--border:#1d4232}
*{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}
.hdr{position:relative;background:linear-gradient(110deg,#064e3b,#059669 60%,#86efac);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid #05966933}
.hdr::before{content:"f'(x)";position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Outfit',sans-serif;font-size:clamp(5rem,15vw,11rem);font-weight:900;color:transparent;-webkit-text-stroke:1.5px rgba(255,255,255,.12);line-height:1;pointer-events: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}
.hdr-back:hover{background:rgba(255,255,255,.24)}
.hdr h1{font-family:'Outfit',sans-serif;font-size:1.6rem;font-weight:900}
.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;display:inline-block;vertical-align:middle}
main{max-width:740px;margin:0 auto;padding:48px 22px 80px}
.coming{background:var(--card);border:1.5px solid var(--border);border-radius:18px;padding:32px 28px;text-align:center;box-shadow:0 4px 18px rgba(0,0,0,.05)}
.coming-icon{width:72px;height:72px;border-radius:20px;background:var(--pri-soft);display:flex;align-items:center;justify-content:center;margin:0 auto 18px;color:var(--pri-d)}
.coming-icon svg{width:36px;height:36px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.coming h2{font-family:'Outfit',sans-serif;font-size:1.5rem;color:var(--pri-d);margin-bottom:12px}
.coming p{font-size:1rem;color:var(--muted);margin-bottom:8px}
.coming p b{color:var(--text)}
.coming-cta{margin-top:24px;display:inline-flex;align-items:center;gap:8px;padding:12px 22px;background:linear-gradient(135deg,var(--pri),var(--pri-d));color:#fff;border-radius:12px;font-weight:700;text-decoration:none;box-shadow:0 6px 22px #05966933}
.coming-cta:hover{filter:brightness(1.08)}
.range-pill{display:inline-block;padding:5px 13px;background:var(--pri-soft);color:var(--pri-d);border-radius:99px;font-size:.84rem;font-weight:700;margin-top:6px}
.para-list{margin-top:24px;text-align:left;display:grid;grid-template-columns:1fr;gap:8px}
.para-row{padding:8px 12px;background:var(--card);border:1px solid var(--border);border-radius:8px;font-size:.86rem;color:var(--text);display:flex;align-items:center;gap:8px}
.para-row b{color:var(--pri-d);font-weight:700;min-width:42px}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-inner">
<div>
<a href="/textbook/algebra-10" class="hdr-back">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К Алгебре 10
</a>
</div>
<div>
<h1>Глава 3 · Производная</h1>
<div class="hdr-sub">§18–§22</div>
</div>
</div>
</header>
<main>
<div class="coming">
<div class="coming-icon">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
</div>
<h2>Глава в разработке</h2>
<p>Эта глава — часть нового курса <b>Алгебра 10</b>.</p>
<p>Содержание (§18–§22) уже спланировано — теория, интерактивы и финальные боссы появятся в ближайших волнах реализации.</p>
<div class="range-pill">5 параграфов</div>
<div class="para-list">
<div class="para-row"><b>§18</b> Определение производной функции</div>
<div class="para-row"><b>§19</b> Правила вычисления производных</div>
<div class="para-row"><b>§20</b> Геометрический смысл. Монотонность</div>
<div class="para-row"><b>§21</b> Применение к исследованию функций</div>
<div class="para-row"><b>§22</b> Наибольшее и наименьшее значения</div>
</div>
<div style="margin-top:24px">
<a href="/textbook/algebra-10" class="coming-cta">
Вернуться к учебнику
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</a>
</div>
</div>
</main>
</body>
</html>
+313
View File
@@ -0,0 +1,313 @@
<!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">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Алгебра 10 класс — учебник</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"></script>
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<style>
:root{
--bg:#ecfeff; --card:#fff;
--text:#0f1a1f; --muted:#4b6671;
--border:#cffafe;
--pri:#0d9488; --pri-d:#0f766e;
--pri-soft:#ccfbf1;
--ch1:#0d9488; --ch1-d:#0f766e;
--ch2:#7c3aed; --ch2-d:#6d28d9;
--ch3:#059669; --ch3-d:#047857;
--sh:0 4px 16px rgba(13,148,136,.10);
--sh-h:0 12px 36px rgba(13,148,136,.18);
}
html.dark{
--bg:#04181c; --card:#0a2329;
--text:#e0fbf9; --muted:#88aab1;
--border:#1d4248;
--pri-soft:rgba(13,148,136,.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.55;transition:background .25s,color .25s}
.hdr{position:relative;background:linear-gradient(110deg,#134e4a 0%,#0d9488 55%,#5eead4 100%);color:#fff;padding:32px 24px 28px;overflow:hidden;border-bottom:2px solid rgba(204,251,241,.15)}
.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(204,251,241,.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.85rem;font-weight:900;letter-spacing:-.01em}
.hdr-sub{font-size:.92rem;opacity:.85;margin-top:4px}
.hdr-side{margin-left:auto;display:flex;gap:8px}
.hdr-btn{padding:8px 12px;background:rgba(255,255,255,.14);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}
.hdr-btn:hover{background:rgba(255,255,255,.24)}
.ic{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
.prog-overall{background:linear-gradient(135deg,var(--pri-soft),rgba(124,58,237,.10));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,#0d9488,#5eead4);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;font-style:italic}
.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(13,148,136,.12);border-radius:5px;overflow:hidden;margin-top:6px}
.po-fill{height:100%;background:linear-gradient(90deg,var(--pri),#7c3aed);border-radius:5px;transition:width .5s}
.po-xp{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;background:linear-gradient(135deg,#f59e0b,var(--pri));color:#fff;border-radius:99px;font-size:.8rem;font-weight:800;font-family:'Unbounded',sans-serif;letter-spacing:.02em;box-shadow:0 4px 12px rgba(13,148,136,.22)}
.ch-grid{display:grid;grid-template-columns:1fr;gap:18px;margin-bottom:30px}
@media(min-width:600px){.ch-grid{grid-template-columns:1fr 1fr}}
@media(min-width:1000px){.ch-grid{grid-template-columns:repeat(3,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:6rem;font-weight:900;font-family:'Outfit',sans-serif;line-height:1;color:rgba(255,255,255,.18);pointer-events:none}
.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.1rem;font-weight:800;letter-spacing:-.01em;position:relative;z-index:1;line-height:1.3}
.ch-range{font-size:.84rem;opacity:.88;margin-top:4px;position:relative;z-index:1;font-weight:500}
.ch-cover.ch1{background:linear-gradient(135deg,#134e4a,#0d9488 60%,#5eead4)}
.ch-cover.ch2{background:linear-gradient(135deg,#3b0764,#7c3aed 60%,#c4b5fd)}
.ch-cover.ch3{background:linear-gradient(135deg,#064e3b,#059669 60%,#86efac)}
.ch-body{padding:16px 20px 18px;display:flex;flex-direction:column;flex:1}
.ch-desc{font-size:.88rem;color:var(--text);opacity:.82;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;transition:width .5s}
.ch-card.ch1-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch1),var(--ch1-d))}
.ch-card.ch2-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch2),var(--ch2-d))}
.ch-card.ch3-card .ch-prog-fill{background:linear-gradient(90deg,var(--ch3),var(--ch3-d))}
.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;transition:filter .15s}
.ch-action:hover{filter:brightness(1.08)}
.ch-card.ch1-card .ch-action{background:linear-gradient(135deg,var(--ch1),#5eead4)}
.ch-card.ch2-card .ch-action{background:linear-gradient(135deg,var(--ch2),#c4b5fd)}
.ch-card.ch3-card .ch-action{background:linear-gradient(135deg,var(--ch3),#86efac)}
.ach-strip{background:var(--card);border:1.5px solid var(--border);border-radius:16px;padding:18px 22px;margin-bottom:28px;display:flex;align-items:center;gap:16px;transition:border-color .4s,box-shadow .4s}
.ach-strip.lit{border-color:#f59e0b;box-shadow:0 0 0 3px rgba(245,158,11,.18)}
.ach-icon{width:52px;height:52px;border-radius:14px;background:rgba(0,0,0,.06);display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .4s}
.ach-strip.lit .ach-icon{background:linear-gradient(135deg,#fbbf24,#f59e0b)}
.ach-icon svg{width:28px;height:28px;stroke:var(--muted);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.ach-strip.lit .ach-icon svg{stroke:#fff}
.ach-text{flex:1}
.ach-title{font-weight:800;font-size:1.02rem;color:var(--text)}
.ach-sub{font-size:.85rem;color:var(--muted);margin-top:2px}
.ach-strip.lit .ach-title{color:#92400e}
.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>Алгебра — 10 класс</h1>
<div class="hdr-sub">Тригонометрия · Корень n-й степени · Производная</div>
</div>
<div class="hdr-side">
<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>
</button>
</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>
<div id="hero-xp-badge" class="po-xp" style="display:none">0 XP</div>
</section>
<div class="ch-grid">
<a href="/textbook/algebra-10-ch1" class="ch-card ch1-card" id="ch-1">
<div class="ch-cover ch1">
<div class="ch-cover-wm">sin α</div>
<div class="ch-num">Глава 1</div>
<div class="ch-title">Тригонометрия</div>
<div class="ch-range">§1–§12 + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">Единичная окружность, sin/cos/tg/ctg произвольного угла, графики, обратные функции, уравнения, формулы приведения, сумма и разность углов, двойной аргумент.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-1">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-1" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-1">Открыть главу</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
<a href="/textbook/algebra-10-ch2" class="ch-card ch2-card" id="ch-2">
<div class="ch-cover ch2">
<div class="ch-cover-wm">ⁿ√x</div>
<div class="ch-num">Глава 2</div>
<div class="ch-title">Корень n-й степени</div>
<div class="ch-range">§13–§17 + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">Арифметический корень n-й степени, свойства корней, преобразования выражений, функция y = ⁿ√x, иррациональные уравнения.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-2">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-2" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-2">Открыть главу</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
<a href="/textbook/algebra-10-ch3" class="ch-card ch3-card" id="ch-3">
<div class="ch-cover ch3">
<div class="ch-cover-wm">f'(x)</div>
<div class="ch-num">Глава 3</div>
<div class="ch-title">Производная</div>
<div class="ch-range">§18–§22 + Финал</div>
</div>
<div class="ch-body">
<div class="ch-desc">Определение производной через предел отношения, правила вычисления, геометрический смысл, касательная, монотонность, экстремумы, наибольшее и наименьшее значения.</div>
<div class="ch-prog">
<div class="ch-prog-label"><span>Прогресс</span><span id="prog-3">0%</span></div>
<div class="ch-prog-bar"><div class="ch-prog-fill" id="fill-3" style="width:0%"></div></div>
</div>
<div class="ch-action">
<span id="btn-3">Открыть главу</span>
<svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
</div>
</div>
</a>
</div>
<div class="ach-strip" id="ach-strip">
<div class="ach-icon">
<svg viewBox="0 0 24 24">
<path d="M6 9H4l-1-3h18l-1 3h-2M6 9l1 6h10l1-6M6 9h12"/><path d="M9 21h6M12 15v6"/>
</svg>
</div>
<div class="ach-text">
<div class="ach-title">Магистр алгебры 10</div>
<div class="ach-sub" id="ach-sub">Прочитайте все 22 параграфа трёх глав, чтобы получить достижение</div>
</div>
</div>
</main>
<footer class="foot">
Интерактивный учебник «Алгебра — 10 класс» · И. Г. Арефьева, О. Н. Пирютко · LearnSpace
</footer>
<script>
'use strict';
(function(){
var saved = localStorage.getItem('algebra10_theme') || localStorage.getItem('theme') || 'light';
if (saved === 'dark') document.documentElement.classList.add('dark');
var lab = document.getElementById('theme-lab');
if (lab) lab.textContent = saved === 'dark' ? 'Светлая' : 'Тёмная';
document.getElementById('theme-btn').addEventListener('click', function(){
document.documentElement.classList.toggle('dark');
var dark = document.documentElement.classList.contains('dark');
localStorage.setItem('algebra10_theme', dark ? 'dark' : 'light');
localStorage.setItem('theme', dark ? 'dark' : 'light');
if (lab) lab.textContent = dark ? 'Светлая' : 'Тёмная';
});
})();
var TOTAL = 22;
var CH_PARA = { 'algebra-10-ch1': 12, 'algebra-10-ch2': 5, 'algebra-10-ch3': 5 };
var CH_IDX = { 'algebra-10-ch1': 1, 'algebra-10-ch2': 2, 'algebra-10-ch3': 3 };
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 = 'Открыть главу';
}
return pct;
}
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 + '%';
var xpBadge = document.getElementById('hero-xp-badge');
var xp = parseInt(localStorage.getItem('algebra10_xp') || '0', 10) || 0;
if (xpBadge && xp > 0) { xpBadge.style.display = ''; xpBadge.textContent = xp + ' XP'; }
if (totalRead >= TOTAL) {
var strip = document.getElementById('ach-strip');
var sub = document.getElementById('ach-sub');
if (strip) strip.classList.add('lit');
if (sub) sub.textContent = 'Выполнено! Вы прочитали весь курс алгебры 10 класса.';
}
}
function loadProgress() {
if (typeof window.LS === 'undefined' || typeof window.LS.api !== 'function') {
renderProgress([]);
return;
}
window.LS.api('/api/textbooks/algebra-10/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>
+4 -4
View File
@@ -212,10 +212,10 @@ if (debugOk) ok('No debug statements in staged source JS');
section('4. Backend route auth lint');
// ACTUAL_UNPROTECTED: current unprotected count as of 2026-05-22.
// The check-route-auth.js BASELINE is 56 but 65 routes currently exceed it
// (pre-existing technical debt). We block only if NEW routes are added beyond 65.
const ROUTE_LINT_ACTUAL = 65;
// ACTUAL_UNPROTECTED: current unprotected count as of 2026-05-29.
// The check-route-auth.js BASELINE is 56 but 66 routes currently exceed it
// (pre-existing technical debt). We block only if NEW routes are added beyond 66.
const ROUTE_LINT_ACTUAL = 66;
if (!backendTouched) {
warn('No backend files staged -- skipping route lint');