diff --git a/frontend/flashcards.html b/frontend/flashcards.html
index 1f00004..0f08984 100644
--- a/frontend/flashcards.html
+++ b/frontend/flashcards.html
@@ -177,6 +177,37 @@
.bulk-img-thumb { max-width: 110px; max-height: 64px; border-radius: 6px; display: block;
border: 1px solid var(--border); object-fit: cover; }
+ /* ── formula insert (KaTeX) ── */
+ .card-side-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px; }
+ .card-side-head .card-side-lbl { margin-bottom: 0; }
+ .fx-mini { background: none; border: none; cursor: pointer; color: var(--violet); font-weight: 700;
+ font-family: 'Times New Roman', serif; font-style: italic; font-size: .92rem; line-height: 1;
+ padding: 1px 5px; border-radius: 6px; opacity: .7; transition: .15s; }
+ .fx-mini:hover { opacity: 1; background: rgba(155,93,229,.08); }
+ .fx-mode-row { display: flex; gap: 8px; margin-bottom: 12px; }
+ .fx-mode-btn { flex: 1; padding: 8px; border: 1.5px solid var(--border); border-radius: 9px; background: #fff;
+ cursor: pointer; font-family: 'Manrope', sans-serif; font-size: .76rem; font-weight: 700;
+ color: var(--text-2); transition: .15s; }
+ .fx-mode-btn.active { border-color: var(--violet); background: rgba(155,93,229,.08); color: var(--violet); }
+ .fx-cats { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
+ .fx-cat-btn { padding: 4px 11px; border: 1px solid var(--border); border-radius: 20px; background: #fff;
+ cursor: pointer; font-family: 'Manrope', sans-serif; font-size: .72rem; font-weight: 600;
+ color: var(--text-2); transition: .15s; }
+ .fx-cat-btn.active { background: var(--violet); color: #fff; border-color: var(--violet); }
+ .fx-palette { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 12px; max-height: 132px; overflow-y: auto; }
+ .fx-sym { min-width: 34px; height: 32px; padding: 0 8px; border: 1px solid var(--border); border-radius: 8px;
+ background: #fff; cursor: pointer; font-size: .98rem; color: var(--text);
+ display: inline-flex; align-items: center; justify-content: center; transition: .12s; }
+ .fx-sym:hover { background: rgba(155,93,229,.1); border-color: var(--violet); }
+ .fx-sym .ic { width: 15px; height: 15px; }
+ #fx-input { width: 100%; box-sizing: border-box; font-family: 'Courier New', monospace; }
+ .fx-preview-label { font-size: .7rem; font-weight: 700; color: var(--text-3); text-transform: uppercase;
+ letter-spacing: .05em; margin: 12px 0 6px; }
+ .fx-preview { min-height: 50px; padding: 14px; border: 1.5px dashed var(--border); border-radius: 10px;
+ background: var(--surface-2); display: flex; align-items: center; justify-content: center;
+ font-size: 1.15rem; overflow-x: auto; }
+ .fx-ph { color: var(--text-3); font-size: .82rem; font-style: italic; }
+
.card-add-bar { display: flex; gap: 10px; align-items: center; }
.card-add-input { flex: 1; padding: 10px 14px; border: 1.5px solid var(--border); border-radius: 10px;
font-family: 'Manrope', sans-serif; font-size: .88rem; background: #fff;
@@ -382,6 +413,7 @@
onkeydown="addCardOnEnter(event)" onpaste="onNewCardPaste(event,'front')" />
+
-
Ответ
+
+ Ответ
+
+
@@ -1123,6 +1184,138 @@ async function saveBulk() {
closeModal('modal-bulk');
}
+/* ════ Formula insert (KaTeX) ════
+ Палитра символов перенесена из редактора теории (lesson-editor.html).
+ Текст карточки свободный — вставляем \( … \) (в строке) или \[ … \] (блоком)
+ в активное поле; в режиме изучения KaTeX уже рендерит эти разделители. */
+const FX_SYMS = {
+ 'Греческие': [
+ ['\\alpha','α'],['\\beta','β'],['\\gamma','γ'],['\\delta','δ'],['\\epsilon','ε'],
+ ['\\zeta','ζ'],['\\eta','η'],['\\theta','θ'],['\\lambda','λ'],['\\mu','μ'],
+ ['\\nu','ν'],['\\xi','ξ'],['\\pi','π'],['\\rho','ρ'],['\\sigma','σ'],
+ ['\\tau','τ'],['\\phi','φ'],['\\chi','χ'],['\\psi','ψ'],['\\omega','ω'],
+ ['\\Gamma','Γ'],['\\Delta','Δ'],['\\Theta','Θ'],['\\Lambda','Λ'],['\\Pi','Π'],
+ ['\\Sigma','Σ'],['\\Phi','Φ'],['\\Psi','Ψ'],['\\Omega','Ω'],
+ ],
+ 'Операции': [
+ ['\\frac{a}{b}','a/b'],['\\sqrt{x}','√'],['\\sqrt[n]{x}','ⁿ√'],
+ ['\\sum','∑'],['\\prod','∏'],['\\int','∫'],['\\oint','∮'],
+ ['\\lim','lim'],['\\infty','∞'],['\\partial','∂'],['\\nabla','∇'],
+ ['\\pm','±'],['\\mp','∓'],['\\times','×'],['\\div','÷'],['\\cdot','·'],
+ ],
+ 'Степени': [
+ ['^{2}','x²'],['^{3}','x³'],['_{n}','xₙ'],['_{i}','xᵢ'],
+ ['e^{x}','eˣ'],['10^{n}','10ⁿ'],
+ ],
+ 'Отношения': [
+ ['\\leq','≤'],['\\geq','≥'],['\\neq','≠'],['\\approx','≈'],['\\equiv','≡'],
+ ['\\sim','∼'],['\\propto','∝'],['\\ll','≪'],['\\gg','≫'],
+ ],
+ 'Стрелки': [
+ ['\\to','→'],['\\leftarrow','←'],['\\Rightarrow','⇒'],['\\Leftrightarrow','⇔'],
+ ['\\uparrow','↑'],['\\downarrow','↓'],
+ ],
+ 'Скобки': [
+ ['\\left( \\right)','(…)'],['\\left[ \\right]','[…]'],['\\left\\{ \\right\\}','{…}'],
+ ['\\left| \\right|','|…|'],
+ ],
+ 'Физика': [
+ ['\\vec{F}','F⃗'],['\\hat{x}','x̂'],['\\hbar','ℏ'],['\\Delta t','Δt'],
+ ['\\mathbf{E}','E'],['\\mathbf{B}','B'],
+ ],
+};
+let _fxField = null; // целевое поле для вставки (textarea/input)
+let _fxMode = 'inline';
+let _fxCat = 'Греческие';
+
+function bindFormulaUI() {
+ // запоминаем последнее сфокусированное поле редактора карточек
+ document.addEventListener('focusin', (e) => {
+ const t = e.target;
+ if (t && ((t.classList && t.classList.contains('card-textarea')) ||
+ t.id === 'new-card-front' || t.id === 'new-card-back')) {
+ _fxField = t;
+ }
+ });
+ // делегирование на палитру/категории (без inline-onclick — латех с «\» не ломается)
+ document.getElementById('fx-cats')?.addEventListener('click', (e) => {
+ const b = e.target.closest('.fx-cat-btn'); if (b) fxSetCat(b.dataset.cat, b);
+ });
+ document.getElementById('fx-palette')?.addEventListener('click', (e) => {
+ const b = e.target.closest('.fx-sym'); if (b) fxInsertSym(b.dataset.tex || '');
+ });
+ document.getElementById('fx-input')?.addEventListener('keydown', (e) => {
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); fxInsert(); }
+ });
+}
+
+function openFormula(btn) {
+ if (btn) { const ta = btn.closest('.card-side')?.querySelector('textarea'); if (ta) _fxField = ta; }
+ if (!_fxField || !document.body.contains(_fxField)) _fxField = document.getElementById('new-card-front');
+ document.getElementById('fx-input').value = '';
+ _fxBuildCats();
+ _fxBuildPalette();
+ updateFxPreview();
+ document.getElementById('modal-formula').classList.add('open');
+ setTimeout(() => document.getElementById('fx-input').focus(), 50);
+}
+
+function fxSetMode(m) {
+ _fxMode = m;
+ document.getElementById('fx-mode-inline').classList.toggle('active', m === 'inline');
+ document.getElementById('fx-mode-block').classList.toggle('active', m === 'block');
+ updateFxPreview();
+}
+
+function _fxBuildCats() {
+ document.getElementById('fx-cats').innerHTML = Object.keys(FX_SYMS).map(c =>
+ `
`
+ ).join('');
+}
+function fxSetCat(cat, btn) {
+ _fxCat = cat;
+ document.querySelectorAll('#fx-cats .fx-cat-btn').forEach(b => b.classList.remove('active'));
+ if (btn) btn.classList.add('active');
+ _fxBuildPalette();
+}
+function _fxBuildPalette() {
+ document.getElementById('fx-palette').innerHTML = (FX_SYMS[_fxCat] || []).map(([latex, disp]) =>
+ `
`
+ ).join('');
+}
+function fxInsertSym(latex) {
+ const ta = document.getElementById('fx-input');
+ const s = ta.selectionStart ?? ta.value.length, e = ta.selectionEnd ?? ta.value.length;
+ ta.value = ta.value.slice(0, s) + latex + ta.value.slice(e);
+ ta.selectionStart = ta.selectionEnd = s + latex.length;
+ ta.focus();
+ updateFxPreview();
+}
+
+function updateFxPreview() {
+ const latex = document.getElementById('fx-input').value;
+ const pv = document.getElementById('fx-preview');
+ if (!latex.trim()) { pv.innerHTML = '
Превью формулы появится здесь'; return; }
+ const wrapped = _fxMode === 'block' ? `\\[${latex}\\]` : `\\(${latex}\\)`;
+ pv.innerHTML = mathHtmlFC(wrapped);
+}
+
+function fxInsert() {
+ const latex = document.getElementById('fx-input').value.trim();
+ if (!latex) { closeModal('modal-formula'); return; }
+ const wrapped = _fxMode === 'block' ? `\\[ ${latex} \\]` : `\\( ${latex} \\)`;
+ const ta = _fxField;
+ if (ta) {
+ const s = ta.selectionStart ?? ta.value.length, e = ta.selectionEnd ?? ta.value.length;
+ ta.value = ta.value.slice(0, s) + wrapped + ta.value.slice(e);
+ ta.selectionStart = ta.selectionEnd = s + wrapped.length;
+ ta.dispatchEvent(new Event('input', { bubbles: true }));
+ ta.dispatchEvent(new Event('change', { bubbles: true }));
+ ta.focus();
+ }
+ closeModal('modal-formula');
+}
+
/* ════ Study mode ════ */
let _studyCards = [];
let _studyIdx = 0;