feat(flashcards): глобальный quick-add FAB + виджет «повтори карточку»
Backend: - POST /api/flashcards/quick — добавить карточку из любой точки; колода по выбору или автоколода «Быстрые карточки» (создаётся при первом обращении) - GET /api/flashcards/random — случайная карточка из всего пула пользователя Frontend: - /js/flashcard-fab.js — плавающая кнопка «запомнить» на всех страницах (учебник, лаборатория, симуляция…). Поповер: вопрос/ответ/колода, Ctrl+Enter. Гейт по фиче-флагу flashcards; исключены classroom/login/error/сама /flashcards. Загружается лениво из sidebar.js (на 45 страницах с шапкой). - dashboard: виджет #w-flashcard в колонке прогресса — флип-карта (вопрос↔ответ), кнопка «Другая», счётчик пула, CTA при пустом пуле; слушает событие flashcard:added для авто-обновления. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -241,8 +241,64 @@ function getStats(req, res) {
|
||||
res.json({ decks_count, cards_count, due_count, reviewed_today });
|
||||
}
|
||||
|
||||
/* ── POST /api/flashcards/quick — быстрое добавление из любой точки ──────
|
||||
Кладёт карточку в указанную колоду (deckId) либо в личную колоду
|
||||
«Быстрые карточки» (создаётся при первом обращении). */
|
||||
const QUICK_DECK_TITLE = 'Быстрые карточки';
|
||||
function quickAdd(req, res) {
|
||||
const uid = req.user.id;
|
||||
const front = stripTags((req.body.front || '').slice(0, 5000)).trim();
|
||||
const back = stripTags((req.body.back || '').slice(0, 5000)).trim();
|
||||
if (!front) return res.status(400).json({ error: 'Лицевая сторона обязательна' });
|
||||
|
||||
let deck = null;
|
||||
const deckId = Number(req.body.deckId) || 0;
|
||||
if (deckId) {
|
||||
deck = db.prepare(`SELECT id, title, color FROM flashcard_decks WHERE id = ? AND user_id = ?`)
|
||||
.get(deckId, uid);
|
||||
}
|
||||
if (!deck) {
|
||||
deck = db.prepare(`SELECT id, title, color FROM flashcard_decks WHERE user_id = ? AND title = ? ORDER BY id LIMIT 1`)
|
||||
.get(uid, QUICK_DECK_TITLE);
|
||||
if (!deck) {
|
||||
const r = db.prepare(
|
||||
`INSERT INTO flashcard_decks (user_id, title, description, color) VALUES (?,?,?,?)`
|
||||
).run(uid, QUICK_DECK_TITLE, 'Карточки, добавленные на лету', '#9B5DE5');
|
||||
deck = { id: r.lastInsertRowid, title: QUICK_DECK_TITLE, color: '#9B5DE5', created: true };
|
||||
}
|
||||
}
|
||||
|
||||
const maxIdx = db.prepare(`SELECT MAX(order_idx) AS m FROM flashcard_cards WHERE deck_id = ?`)
|
||||
.get(deck.id)?.m ?? -1;
|
||||
const r = db.prepare(`INSERT INTO flashcard_cards (deck_id, front, back, order_idx) VALUES (?,?,?,?)`)
|
||||
.run(deck.id, front, back, maxIdx + 1);
|
||||
res.json({ id: r.lastInsertRowid, deck_id: deck.id, deck_title: deck.title, deck_color: deck.color, front, back });
|
||||
}
|
||||
|
||||
/* ── GET /api/flashcards/random — случайная карточка из всего пула ───────
|
||||
Для дашборд-виджета «повтори карточку». */
|
||||
function getRandom(req, res) {
|
||||
const uid = req.user.id;
|
||||
const total = db.prepare(`
|
||||
SELECT COUNT(*) AS n FROM flashcard_cards c
|
||||
JOIN flashcard_decks d ON d.id = c.deck_id WHERE d.user_id = ?
|
||||
`).get(uid).n;
|
||||
if (!total) return res.json({ card: null, total: 0 });
|
||||
|
||||
const card = db.prepare(`
|
||||
SELECT c.id, c.front, c.back, c.deck_id,
|
||||
d.title AS deck_title, d.color AS deck_color
|
||||
FROM flashcard_cards c
|
||||
JOIN flashcard_decks d ON d.id = c.deck_id
|
||||
WHERE d.user_id = ?
|
||||
ORDER BY RANDOM() LIMIT 1
|
||||
`).get(uid);
|
||||
res.json({ card, total });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listDecks, createDeck, updateDeck, deleteDeck,
|
||||
getCards, addCard, addCardsBulk, updateCard, deleteCard,
|
||||
getStudySession, submitReview, getStats,
|
||||
quickAdd, getRandom,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@ const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.post ('/quick', fc.quickAdd);
|
||||
router.get ('/random', fc.getRandom);
|
||||
router.get ('/decks', fc.listDecks);
|
||||
router.post ('/decks', fc.createDeck);
|
||||
router.put ('/decks/:id', fc.updateDeck);
|
||||
|
||||
@@ -245,6 +245,39 @@
|
||||
.cont-sub { font-size: 0.72rem; color: var(--text-3); margin-top: 2px; }
|
||||
.cont-pct { font-family: 'Unbounded', sans-serif; font-size: 0.82rem; font-weight: 800; color: var(--violet); }
|
||||
|
||||
/* ── flashcard review widget ── */
|
||||
.fcw-card { perspective: 1000px; cursor: pointer; }
|
||||
.fcw-inner {
|
||||
position: relative; transform-style: preserve-3d;
|
||||
transition: transform 0.5s cubic-bezier(.34,1.1,.64,1); min-height: 118px;
|
||||
}
|
||||
.fcw-card.flipped .fcw-inner { transform: rotateY(180deg); }
|
||||
.fcw-face {
|
||||
position: absolute; inset: 0; backface-visibility: hidden; -webkit-backface-visibility: hidden;
|
||||
border-radius: 14px; padding: 14px 16px; display: flex; flex-direction: column; gap: 6px;
|
||||
border: 1.5px solid rgba(15,23,42,0.08); box-sizing: border-box;
|
||||
}
|
||||
.fcw-front { background: linear-gradient(135deg, rgba(155,93,229,0.06), rgba(6,214,224,0.05)); }
|
||||
.fcw-back { background: linear-gradient(135deg, rgba(6,214,100,0.07), rgba(6,214,224,0.05)); transform: rotateY(180deg); }
|
||||
.fcw-deck { font-size: 0.66rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; color: var(--violet); }
|
||||
.fcw-back .fcw-deck { color: #059652; }
|
||||
.fcw-text { flex: 1; font-size: 0.92rem; font-weight: 600; color: var(--text); line-height: 1.35;
|
||||
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.fcw-hint { font-size: 0.68rem; color: var(--text-3); display: flex; align-items: center; gap: 5px; }
|
||||
.fcw-hint svg { width: 12px; height: 12px; }
|
||||
.fcw-actions { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; }
|
||||
.fcw-count { font-size: 0.72rem; color: var(--text-3); font-weight: 600; }
|
||||
.fcw-btn {
|
||||
display: inline-flex; align-items: center; gap: 6px; padding: 7px 14px; border-radius: 99px;
|
||||
border: 1.5px solid rgba(155,93,229,0.3); background: rgba(155,93,229,0.06); color: var(--violet);
|
||||
font-family: 'Manrope', sans-serif; font-size: 0.76rem; font-weight: 700; cursor: pointer;
|
||||
transition: all 0.15s; text-decoration: none;
|
||||
}
|
||||
.fcw-btn:hover { background: rgba(155,93,229,0.14); border-color: var(--violet); }
|
||||
.fcw-btn svg { width: 13px; height: 13px; stroke: currentColor; }
|
||||
.fcw-empty { text-align: center; padding: 16px 12px; color: var(--text-3); }
|
||||
.fcw-empty p { font-size: 0.82rem; margin-bottom: 10px; }
|
||||
|
||||
/* ── subjects progress bars ── */
|
||||
.subj-prog-row {
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
@@ -1460,6 +1493,14 @@
|
||||
|
||||
<!-- Col 3: Progress -->
|
||||
<div class="widget" id="w-progress-col">
|
||||
<!-- Flashcard review (random card from pool) -->
|
||||
<div id="w-flashcard" style="display:none;margin-bottom:18px">
|
||||
<div class="w-head">
|
||||
<div class="w-title">Повтори карточку</div>
|
||||
<a class="w-more" href="/flashcards">Все карточки</a>
|
||||
</div>
|
||||
<div id="fcw-body"></div>
|
||||
</div>
|
||||
<!-- Combined Activity Widget (heatmap + streak calendar) -->
|
||||
<div id="w-activity" style="display:none">
|
||||
<div class="w-head">
|
||||
@@ -3888,8 +3929,76 @@
|
||||
loadContinueWidget();
|
||||
loadWeakWidget();
|
||||
loadTheoryWidget();
|
||||
loadFlashcardWidget();
|
||||
}
|
||||
|
||||
/* ══ WIDGET: Flashcard review (random card from pool) ════════════════ */
|
||||
let _fcwTotal = 0;
|
||||
async function loadFlashcardWidget() {
|
||||
if (isTeacher) return;
|
||||
const w = document.getElementById('w-flashcard');
|
||||
if (!w || w.dataset.cfgHidden) return;
|
||||
try {
|
||||
const r = await LS.api('/api/flashcards/random');
|
||||
renderFlashcardWidget(r);
|
||||
w.style.display = '';
|
||||
} catch { /* фича выключена или ошибка — оставляем скрытым */ }
|
||||
}
|
||||
|
||||
function renderFlashcardWidget(r) {
|
||||
const body = document.getElementById('fcw-body');
|
||||
if (!body) return;
|
||||
if (!r || !r.card) {
|
||||
_fcwTotal = 0;
|
||||
body.innerHTML = `<div class="fcw-empty">
|
||||
<p>Пока нет карточек. Создавай их в любой точке системы кнопкой внизу справа.</p>
|
||||
<button class="fcw-btn" type="button" onclick="document.getElementById('fc-fab')?.click()">
|
||||
${lci('plus','width:13px;height:13px')} Создать карточку
|
||||
</button>
|
||||
</div>`;
|
||||
reIcons();
|
||||
return;
|
||||
}
|
||||
_fcwTotal = r.total || 0;
|
||||
const c = r.card;
|
||||
const back = (c.back || '').trim() || '(ответ не заполнен)';
|
||||
const col = c.deck_color || '#9B5DE5';
|
||||
body.innerHTML = `
|
||||
<div class="fcw-card" onclick="fcwFlip(this)">
|
||||
<div class="fcw-inner">
|
||||
<div class="fcw-face fcw-front">
|
||||
<div class="fcw-deck" style="color:${esc(col)}">${esc(c.deck_title || 'Карточка')}</div>
|
||||
<div class="fcw-text">${esc(c.front)}</div>
|
||||
<div class="fcw-hint">${lci('rotate-cw','width:12px;height:12px')} нажми, чтобы перевернуть</div>
|
||||
</div>
|
||||
<div class="fcw-face fcw-back">
|
||||
<div class="fcw-deck">Ответ</div>
|
||||
<div class="fcw-text">${esc(back)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fcw-actions">
|
||||
<button class="fcw-btn" type="button" onclick="fcwNext(event)">
|
||||
${lci('shuffle','width:13px;height:13px')} Другая
|
||||
</button>
|
||||
<span class="fcw-count">${_fcwTotal} ${_fcwTotal === 1 ? 'карточка' : (_fcwTotal % 10 >= 2 && _fcwTotal % 10 <= 4 && (_fcwTotal % 100 < 12 || _fcwTotal % 100 > 14) ? 'карточки' : 'карточек')} в пуле</span>
|
||||
</div>`;
|
||||
reIcons();
|
||||
}
|
||||
|
||||
function fcwFlip(el) { el.classList.toggle('flipped'); }
|
||||
|
||||
async function fcwNext(e) {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
const r = await LS.api('/api/flashcards/random');
|
||||
renderFlashcardWidget(r);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Обновлять виджет, когда карточку добавили через глобальный FAB
|
||||
window.addEventListener('flashcard:added', () => { if (!isTeacher) loadFlashcardWidget(); });
|
||||
|
||||
/* ══ INIT ═════════════════════════════════════════════════════════════ */
|
||||
window.addEventListener('pageshow', e => { if (e.persisted) location.reload(); });
|
||||
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
'use strict';
|
||||
/*
|
||||
* flashcard-fab.js — глобальная плавающая кнопка «создать карточку».
|
||||
* Доступна на любой странице (учебник, лаборатория, симуляция…): ученик
|
||||
* мгновенно фиксирует то, что хочет запомнить, не уходя со страницы.
|
||||
*
|
||||
* Подключается лениво из sidebar.js (на всех страницах с шапкой).
|
||||
* Карточка уходит в POST /api/flashcards/quick (колода по выбору или
|
||||
* автоколода «Быстрые карточки»). Уважает фиче-флаг flashcards.
|
||||
*/
|
||||
(function () {
|
||||
if (typeof LS === 'undefined' || !LS.isLoggedIn || !LS.isLoggedIn()) return;
|
||||
if (document.getElementById('fc-fab')) return;
|
||||
|
||||
// Страницы, где FAB не нужен (полноэкранные / системные / сама страница карточек)
|
||||
const EXCLUDE = ['/login', '/403', '/404', '/500', '/classroom', '/guest-board', '/flashcards'];
|
||||
const path = location.pathname.replace(/\.html$/, '');
|
||||
if (EXCLUDE.some(p => path === p || path.startsWith(p + '/'))) return;
|
||||
|
||||
let _decks = null;
|
||||
let _open = false;
|
||||
|
||||
const esc = (s) => (LS.esc ? LS.esc(s) : String(s == null ? '' : s));
|
||||
|
||||
// Гейт по фиче-флагу
|
||||
(LS.loadFeatures ? LS.loadFeatures() : Promise.resolve({}))
|
||||
.then(feats => { if (!feats || feats.flashcards !== false) inject(); })
|
||||
.catch(() => inject());
|
||||
|
||||
function pageLabel() {
|
||||
let t = (document.title || '').split(/[—·|]/)[0].trim();
|
||||
return t && t.length < 48 ? t : '';
|
||||
}
|
||||
|
||||
function inject() {
|
||||
ensureStyles();
|
||||
|
||||
const fab = document.createElement('button');
|
||||
fab.id = 'fc-fab'; fab.type = 'button';
|
||||
fab.setAttribute('aria-label', 'Создать карточку');
|
||||
fab.title = 'Создать карточку (запомнить)';
|
||||
fab.innerHTML =
|
||||
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
||||
'<rect x="3" y="5" width="14" height="15" rx="2.5"/><path d="M20 8v9"/><path d="M10 9.5v6M7 12.5h6"/></svg>';
|
||||
fab.addEventListener('click', (e) => { e.stopPropagation(); toggle(); });
|
||||
document.body.appendChild(fab);
|
||||
|
||||
const ctx = pageLabel();
|
||||
const pop = document.createElement('div');
|
||||
pop.id = 'fc-pop';
|
||||
pop.innerHTML =
|
||||
'<div class="fc-pop-head">' +
|
||||
'<span class="fc-pop-title">Новая карточка</span>' +
|
||||
(ctx ? '<span class="fc-pop-ctx" title="Текущая страница">' + esc(ctx) + '</span>' : '') +
|
||||
'<button class="fc-pop-x" type="button" aria-label="Закрыть"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>' +
|
||||
'</div>' +
|
||||
'<label class="fc-lbl">Вопрос / лицевая сторона</label>' +
|
||||
'<textarea id="fc-front" class="fc-ta" rows="2" placeholder="Что спросить…" maxlength="5000"></textarea>' +
|
||||
'<label class="fc-lbl">Ответ / обратная сторона</label>' +
|
||||
'<textarea id="fc-back" class="fc-ta" rows="2" placeholder="Что вспомнить…" maxlength="5000"></textarea>' +
|
||||
'<label class="fc-lbl">Колода</label>' +
|
||||
'<select id="fc-deck" class="fc-sel"><option value="">Быстрые карточки</option></select>' +
|
||||
'<div class="fc-pop-foot">' +
|
||||
'<span class="fc-hint">Ctrl+Enter — сохранить</span>' +
|
||||
'<button id="fc-save" class="fc-save" type="button">Сохранить</button>' +
|
||||
'</div>';
|
||||
document.body.appendChild(pop);
|
||||
|
||||
pop.querySelector('.fc-pop-x').addEventListener('click', close);
|
||||
pop.querySelector('#fc-save').addEventListener('click', save);
|
||||
pop.addEventListener('click', (e) => e.stopPropagation());
|
||||
pop.addEventListener('keydown', (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); save(); }
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && _open) close(); });
|
||||
document.addEventListener('click', () => { if (_open) close(); });
|
||||
}
|
||||
|
||||
function toggle() { _open ? close() : openPop(); }
|
||||
|
||||
async function openPop() {
|
||||
_open = true;
|
||||
document.getElementById('fc-pop').classList.add('fc-show');
|
||||
document.getElementById('fc-fab').classList.add('fc-fab-on');
|
||||
if (_decks === null) {
|
||||
try { const r = await LS.api('/api/flashcards/decks'); _decks = r.decks || []; }
|
||||
catch { _decks = []; }
|
||||
fillDecks();
|
||||
}
|
||||
setTimeout(() => document.getElementById('fc-front')?.focus(), 70);
|
||||
}
|
||||
|
||||
function close() {
|
||||
_open = false;
|
||||
document.getElementById('fc-pop')?.classList.remove('fc-show');
|
||||
document.getElementById('fc-fab')?.classList.remove('fc-fab-on');
|
||||
}
|
||||
|
||||
function fillDecks() {
|
||||
const sel = document.getElementById('fc-deck');
|
||||
if (!sel) return;
|
||||
const cur = sel.value;
|
||||
sel.innerHTML = '<option value="">Быстрые карточки</option>' +
|
||||
(_decks || []).map(d => `<option value="${d.id}">${esc(d.title)}</option>`).join('');
|
||||
if (cur) sel.value = cur;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const front = (document.getElementById('fc-front').value || '').trim();
|
||||
const back = (document.getElementById('fc-back').value || '').trim();
|
||||
if (!front) { LS.toast('Заполни лицевую сторону (вопрос)', 'error'); document.getElementById('fc-front').focus(); return; }
|
||||
const deckId = document.getElementById('fc-deck').value;
|
||||
const btn = document.getElementById('fc-save');
|
||||
btn.disabled = true; btn.textContent = 'Сохраняю…';
|
||||
try {
|
||||
const r = await LS.api('/api/flashcards/quick', { method: 'POST', body: { front, back, deckId: deckId || undefined } });
|
||||
LS.toast('Карточка добавлена → «' + (r.deck_title || 'колода') + '»', 'success');
|
||||
document.getElementById('fc-front').value = '';
|
||||
document.getElementById('fc-back').value = '';
|
||||
// если создалась новая (быстрая) колода — перечитаем список при следующем открытии
|
||||
if (r.deck_id && _decks && !_decks.some(d => d.id === r.deck_id)) _decks = null;
|
||||
window.dispatchEvent(new CustomEvent('flashcard:added', { detail: r }));
|
||||
document.getElementById('fc-front').focus();
|
||||
} catch (e) {
|
||||
LS.toast('Ошибка: ' + (e && e.message || 'не удалось сохранить'), 'error');
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = 'Сохранить';
|
||||
}
|
||||
}
|
||||
|
||||
function ensureStyles() {
|
||||
if (document.getElementById('fc-fab-style')) return;
|
||||
const s = document.createElement('style');
|
||||
s.id = 'fc-fab-style';
|
||||
s.textContent = `
|
||||
#fc-fab {
|
||||
position: fixed; right: 20px; bottom: 20px; z-index: 90;
|
||||
width: 54px; height: 54px; border-radius: 50%; border: none; cursor: pointer;
|
||||
background: linear-gradient(135deg, #06D6E0, #9B5DE5);
|
||||
color: #fff; display: grid; place-items: center;
|
||||
box-shadow: 0 6px 22px rgba(155,93,229,0.45);
|
||||
transition: transform .18s cubic-bezier(.34,1.4,.64,1), box-shadow .18s;
|
||||
}
|
||||
#fc-fab svg { width: 24px; height: 24px; }
|
||||
#fc-fab:hover { transform: translateY(-2px) scale(1.05); box-shadow: 0 10px 30px rgba(155,93,229,0.55); }
|
||||
#fc-fab:active { transform: scale(0.96); }
|
||||
#fc-fab.fc-fab-on { transform: rotate(45deg); }
|
||||
#fc-fab::after {
|
||||
content: 'Запомнить'; position: absolute; right: 64px; top: 50%; transform: translateY(-50%);
|
||||
background: rgba(15,23,42,0.9); color: #fff; padding: 5px 11px; border-radius: 8px;
|
||||
font: 600 0.76rem 'Manrope', sans-serif; white-space: nowrap;
|
||||
opacity: 0; pointer-events: none; transition: opacity .15s;
|
||||
}
|
||||
#fc-fab:hover::after { opacity: 1; }
|
||||
#fc-fab.fc-fab-on::after { display: none; }
|
||||
|
||||
#fc-pop {
|
||||
position: fixed; right: 20px; bottom: 86px; z-index: 91;
|
||||
width: 320px; max-width: calc(100vw - 32px);
|
||||
background: #fff; border: 1.5px solid rgba(15,23,42,0.08); border-radius: 18px;
|
||||
box-shadow: 0 18px 50px rgba(15,23,42,0.22);
|
||||
padding: 16px; opacity: 0; transform: translateY(10px) scale(0.97);
|
||||
transform-origin: bottom right; pointer-events: none;
|
||||
transition: opacity .18s, transform .18s cubic-bezier(.34,1.3,.64,1);
|
||||
font-family: 'Manrope', sans-serif;
|
||||
}
|
||||
#fc-pop.fc-show { opacity: 1; transform: none; pointer-events: auto; }
|
||||
.fc-pop-head { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
||||
.fc-pop-title { font-family: 'Unbounded', sans-serif; font-size: 0.92rem; font-weight: 800; color: #0F172A; }
|
||||
.fc-pop-ctx {
|
||||
font-size: 0.66rem; font-weight: 700; color: #7c3aed;
|
||||
background: rgba(155,93,229,0.1); padding: 3px 8px; border-radius: 99px;
|
||||
max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.fc-pop-x { margin-left: auto; width: 28px; height: 28px; border: none; background: rgba(15,23,42,0.05);
|
||||
border-radius: 8px; cursor: pointer; color: #56687A; display: grid; place-items: center; }
|
||||
.fc-pop-x svg { width: 15px; height: 15px; }
|
||||
.fc-pop-x:hover { background: rgba(241,91,181,0.12); color: #db2777; }
|
||||
.fc-lbl { display: block; font-size: 0.68rem; font-weight: 700; color: #56687A;
|
||||
text-transform: uppercase; letter-spacing: 0.04em; margin: 8px 0 4px; }
|
||||
.fc-ta, .fc-sel {
|
||||
width: 100%; box-sizing: border-box; padding: 9px 12px;
|
||||
border: 1.5px solid rgba(15,23,42,0.12); border-radius: 10px;
|
||||
font-family: 'Manrope', sans-serif; font-size: 0.86rem; color: #0F172A;
|
||||
background: #fafbfc; resize: vertical; transition: border-color .15s;
|
||||
}
|
||||
.fc-ta:focus, .fc-sel:focus { outline: none; border-color: #9B5DE5; background: #fff; }
|
||||
.fc-sel { resize: none; cursor: pointer; }
|
||||
.fc-pop-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 14px; }
|
||||
.fc-hint { font-size: 0.68rem; color: #94a3b8; }
|
||||
.fc-save {
|
||||
padding: 9px 20px; border: none; border-radius: 99px; cursor: pointer;
|
||||
background: linear-gradient(135deg, #06D6E0, #9B5DE5); color: #fff;
|
||||
font-family: 'Manrope', sans-serif; font-size: 0.84rem; font-weight: 700;
|
||||
transition: filter .15s, transform .12s;
|
||||
}
|
||||
.fc-save:hover { filter: brightness(1.06); }
|
||||
.fc-save:active { transform: translateY(1px); }
|
||||
.fc-save:disabled { opacity: 0.6; cursor: default; }
|
||||
@media (max-width: 560px) {
|
||||
#fc-fab { width: 50px; height: 50px; right: 16px; bottom: 16px; }
|
||||
#fc-pop { right: 12px; left: 12px; width: auto; bottom: 78px; }
|
||||
#fc-fab::after { display: none; }
|
||||
}
|
||||
.app-layout.dark #fc-pop { background: #1A1D27; border-color: rgba(255,255,255,0.08); }
|
||||
.app-layout.dark .fc-pop-title { color: #E8ECF2; }
|
||||
.app-layout.dark .fc-ta, .app-layout.dark .fc-sel { background: #11141c; color: #E8ECF2; border-color: rgba(255,255,255,0.1); }
|
||||
`;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})();
|
||||
@@ -192,4 +192,13 @@
|
||||
LS.hideDisabledFeatures?.();
|
||||
LS.notif?.init?.();
|
||||
}
|
||||
|
||||
// Глобальная плавающая кнопка «создать карточку» (на всех страницах с шапкой)
|
||||
if (typeof LS !== 'undefined' && LS.isLoggedIn?.() && !document.getElementById('fc-fab-loader')) {
|
||||
const s = document.createElement('script');
|
||||
s.id = 'fc-fab-loader';
|
||||
s.src = '/js/flashcard-fab.js';
|
||||
s.defer = true;
|
||||
document.body.appendChild(s);
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user