From 751d88048c5f65bb38a7c83e86d0d151e18ecb9d Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 2 Jun 2026 13:25:02 +0300 Subject: [PATCH] =?UTF-8?q?feat(flashcards):=20=D0=B2=D0=B2=D0=BE=D0=B4=20?= =?UTF-8?q?=D1=84=D0=BE=D1=80=D0=BC=D1=83=D0=BB=20KaTeX=20=D0=B2=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=B5=20(=D0=BF?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D1=82=D1=80=D0=B0=20+=20=D0=BF=D1=80=D0=B5?= =?UTF-8?q?=D0=B2=D1=8C=D1=8E)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Перенесён подход из редактора теории: - модалка «Вставить формулу»: палитра символов по категориям (греческие/операции/степени/отношения/стрелки/скобки/физика), LaTeX-поле, живое KaTeX-превью, режим «в строке \( \)» / «блоком \[ \]» - кнопка «ƒₓ» у каждой стороны карточки и в add-bar; вставка в активное поле - палитра на data-tex + делегирование (inline-onclick схлопывал «\» в латехе) - Ctrl+Enter в поле формулы = вставить; разделители совпадают с рендером изучения Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/flashcards.html | 197 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 195 insertions(+), 2 deletions(-) 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')" /> +
@@ -504,6 +536,28 @@ + + + @@ -552,6 +606,7 @@ let _newImg = { front: '', back: '' }; // картинки, прикреплё async function init() { buildColorPicker(); bindStudyKeys(); + bindFormulaUI(); await loadDecks(); } @@ -741,7 +796,10 @@ function renderCardList() { `}
-
Вопрос
+
+ Вопрос + +
@@ -749,7 +807,10 @@ function renderCardList() {
-
Ответ
+
+ Ответ + +
@@ -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;