a11y: WCAG AA contrast + ARIA roles + focus management across all pages

- css/ls.css: --text-3 #8898AA → #56687A (5.1:1 contrast), min-height 44px on .btn-primary/.btn-ghost/.sb-link, new .icon-btn utility (44×44px)
- js/api.js: lsConfirm — role=dialog, aria-modal, aria-labelledby, Tab focus trap, restore focus on close; lsToast — aria-live=polite on container, role=alert on errors; live quiz — role=dialog, role=radiogroup, role=radio, aria-checked, keyboard support
- test-run.html: q-opt divs — role=radio/checkbox, aria-checked, tabindex, keyboard enter/space; confirm modal — role=dialog, aria-modal; btn-flag — aria-pressed; dots — aria-label, aria-current; touch targets 44px
- board.html: btn-del-ann — aria-label; reaction buttons — aria-label, aria-pressed
- All 18 HTML files: replace hardcoded color:#8898AA with color:var(--text-3)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-04-16 11:42:38 +03:00
parent 3a4623a60a
commit 26ba289019
22 changed files with 362 additions and 299 deletions
+18 -18
View File
@@ -79,7 +79,7 @@
flex-shrink: 0;
}
.class-card-name { font-size: 0.85rem; font-weight: 700; color: #0F172A; }
.class-card-meta { font-size: 0.72rem; color: #8898AA; margin-top: 2px; }
.class-card-meta { font-size: 0.72rem; color: var(--text-3); margin-top: 2px; }
.lq-session-area { padding: 14px 12px; border-top: 1px solid rgba(15,23,42,0.07); }
.btn-start {
@@ -104,7 +104,7 @@
background: rgba(6,214,160,0.08); border: 1.5px solid rgba(6,214,160,0.25);
margin-bottom: 8px;
}
.as-label { font-size: 0.7rem; font-weight: 700; color: #8898AA; margin-bottom: 3px; }
.as-label { font-size: 0.7rem; font-weight: 700; color: var(--text-3); margin-bottom: 3px; }
.as-val {
font-family: 'Unbounded', sans-serif; font-size: 0.82rem; font-weight: 800;
color: #059652; display: flex; align-items: center; gap: 6px;
@@ -125,7 +125,7 @@
.lq-search:focus { border-color: var(--violet); }
.lq-search-icon {
position: absolute; left: 13px; top: 50%; transform: translateY(-50%);
color: #8898AA; pointer-events: none; width: 16px; height: 16px;
color: var(--text-3); pointer-events: none; width: 16px; height: 16px;
}
/* section header */
@@ -148,7 +148,7 @@
background-repeat: no-repeat; background-position: right 8px center; padding-right: 24px;
}
.lq-filter-select:focus { border-color: var(--violet); }
.lq-q-count { font-size: 0.7rem; color: #8898AA; font-weight: 600; white-space: nowrap; margin-bottom: 12px; }
.lq-q-count { font-size: 0.7rem; color: var(--text-3); font-weight: 600; white-space: nowrap; margin-bottom: 12px; }
/* load more */
.btn-load-more {
@@ -164,7 +164,7 @@
.lq-result-stats { display: flex; gap: 8px; margin-bottom: 14px; }
.lq-result-stat { flex: 1; padding: 10px 12px; border-radius: 12px; background: rgba(15,23,42,0.04); text-align: center; }
.lq-result-stat-val { font-family: 'Unbounded', sans-serif; font-size: 1.1rem; font-weight: 900; color: #0F172A; }
.lq-result-stat-lbl { font-size: 0.64rem; color: #8898AA; margin-top: 2px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; }
.lq-result-stat-lbl { font-size: 0.64rem; color: var(--text-3); margin-top: 2px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; }
.lq-result-stat.rs-correct { background: rgba(6,214,160,0.08); }
.lq-result-stat.rs-correct .lq-result-stat-val { color: #059652; }
.lq-result-stat.rs-wrong { background: rgba(239,71,111,0.07); }
@@ -192,7 +192,7 @@
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden; line-height: 1.5; min-height: 1.5em;
}
.lq-q-meta { font-size: 0.7rem; color: #8898AA; margin-top: 5px; display: flex; gap: 8px; flex-wrap: wrap; }
.lq-q-meta { font-size: 0.7rem; color: var(--text-3); margin-top: 5px; display: flex; gap: 8px; flex-wrap: wrap; }
.btn-launch {
padding: 7px 16px; border: none; border-radius: 999px;
background: var(--grad-1); color: #fff;
@@ -233,7 +233,7 @@
width: 22px; height: 22px; border-radius: 6px;
background: rgba(15,23,42,0.07); flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-size: 0.72rem; font-weight: 800; color: #8898AA;
font-size: 0.72rem; font-weight: 800; color: var(--text-3);
}
.lq-active-opt.correct .lq-opt-letter { background: #06D6A0; color: #fff; }
@@ -247,7 +247,7 @@
font-family: 'Unbounded', sans-serif; font-size: 1.4rem; font-weight: 900;
color: var(--violet);
}
.lq-counter-label { font-size: 0.78rem; color: #8898AA; font-weight: 600; }
.lq-counter-label { font-size: 0.78rem; color: var(--text-3); font-weight: 600; }
.lq-counter-bar-wrap {
flex: 1; height: 8px; background: rgba(155,93,229,0.1); border-radius: 999px; overflow: hidden;
}
@@ -296,7 +296,7 @@
/* no session state */
.lq-no-session {
text-align: center; padding: 80px 20px; color: #8898AA;
text-align: center; padding: 80px 20px; color: var(--text-3);
}
.lq-no-session-icon { margin-bottom: 14px; opacity: 0.2; }
@@ -362,7 +362,7 @@
Выберите класс
</div>
<div class="class-list" id="class-list">
<div style="padding:30px;text-align:center;color:#8898AA;font-size:0.82rem">
<div style="padding:30px;text-align:center;color:var(--text-3);font-size:0.82rem">
<div class="spinner" style="margin:0 auto 10px"></div>
Загрузка классов…
</div>
@@ -433,7 +433,7 @@
</div>
<div class="lq-q-count" id="q-count" style="display:none"></div>
<div class="lq-q-list" id="q-list">
<div style="padding:30px;text-align:center;color:#8898AA;font-size:0.82rem">
<div style="padding:30px;text-align:center;color:var(--text-3);font-size:0.82rem">
<div class="spinner" style="margin:0 auto 10px"></div>
Загрузка вопросов…
</div>
@@ -560,7 +560,7 @@
const classes = await LS.api('/api/classes');
const list = document.getElementById('class-list');
if (!(classes || []).length) {
list.innerHTML = '<div style="padding:30px;text-align:center;color:#8898AA;font-size:0.82rem">Нет доступных классов</div>';
list.innerHTML = '<div style="padding:30px;text-align:center;color:var(--text-3);font-size:0.82rem">Нет доступных классов</div>';
lucide.createIcons();
return;
}
@@ -578,7 +578,7 @@
list.innerHTML = html;
lucide.createIcons();
} catch {
document.getElementById('class-list').innerHTML = '<div style="padding:20px;text-align:center;color:#8898AA;font-size:0.82rem">Ошибка загрузки</div>';
document.getElementById('class-list').innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-3);font-size:0.82rem">Ошибка загрузки</div>';
}
}
@@ -713,7 +713,7 @@
/* ── load questions ── */
async function loadQuestions(reset = true) {
if (reset) { _qPage = 0; allQuestions = []; }
if (reset) document.getElementById('q-list').innerHTML = '<div style="padding:20px;text-align:center;color:#8898AA"><div class="spinner" style="margin:0 auto 10px"></div> Загрузка…</div>';
if (reset) document.getElementById('q-list').innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-3)"><div class="spinner" style="margin:0 auto 10px"></div> Загрузка…</div>';
const params = new URLSearchParams({ limit: Q_LIMIT, offset: _qPage * Q_LIMIT });
if (_topicFilter) params.set('topic_id', _topicFilter);
if (_diffFilter) params.set('difficulty', _diffFilter);
@@ -730,7 +730,7 @@
if (btnMore) btnMore.style.display = allQuestions.length < _totalQ ? '' : 'none';
if (countEl) { countEl.textContent = `Показано ${allQuestions.length} из ${_totalQ}`; countEl.style.display = ''; }
} catch {
if (reset) document.getElementById('q-list').innerHTML = '<div style="padding:20px;text-align:center;color:#8898AA;font-size:0.82rem">Ошибка загрузки вопросов</div>';
if (reset) document.getElementById('q-list').innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-3);font-size:0.82rem">Ошибка загрузки вопросов</div>';
}
}
@@ -748,7 +748,7 @@
function renderQuestionList() {
const list = document.getElementById('q-list');
if (!allQuestions.length) {
list.innerHTML = '<div style="padding:30px;text-align:center;color:#8898AA;font-size:0.82rem">Вопросов не найдено</div>';
list.innerHTML = '<div style="padding:30px;text-align:center;color:var(--text-3);font-size:0.82rem">Вопросов не найдено</div>';
lucide.createIcons();
return;
}
@@ -864,7 +864,7 @@
renderResults(data, resultsArea);
if (window.LS && LS.sfx) LS.sfx.play('quiz_end');
} catch (e) {
resultsArea.innerHTML = `<div style="padding:20px;text-align:center;color:#8898AA;font-size:0.84rem">${esc(e.message || 'Ошибка загрузки результатов')}</div>`;
resultsArea.innerHTML = `<div style="padding:20px;text-align:center;color:var(--text-3);font-size:0.84rem">${esc(e.message || 'Ошибка загрузки результатов')}</div>`;
}
}
@@ -877,7 +877,7 @@
const maxCount = Math.max(...opts.map(o => o.chosen_count || 0), 1);
if (!opts.length) {
container.innerHTML = '<div style="padding:16px;text-align:center;color:#8898AA;font-size:0.84rem">Нет данных о вариантах ответа</div>';
container.innerHTML = '<div style="padding:16px;text-align:center;color:var(--text-3);font-size:0.84rem">Нет данных о вариантах ответа</div>';
return;
}