feat(stereo): сворачиваемый аккордеон панели управления (UX)
Панель за фазы A–C разрослась до ~14 всегда-раскрытых секций (длинный скролл, тяжело ориентироваться). Сделал её удобнее: - _stereoInitPanel() (вызов из _openStereo, идемпотентно) оборачивает контролы каждой секции в .st-acc-body; заголовки .gp-section-title → кликабельные .st-acc-hdr с шевроном; состояние секций в localStorage. - Тройку фигурных секций (Многогранники/Правильные/Тела вращения) слил в одну «Фигуры» (под-метки .st-sublabel). По умолчанию открыты «Фигуры» и «Параметры», остальное свёрнуто. - Кнопки «Развернуть всё / Свернуть всё» (stereoAccAll), клавиатура (Enter/Space на заголовке), role=button/tabindex. - Только раскладка: ни один контрол/обработчик не изменён (узлы лишь перемещены в тело секции). Затронуты stereo.js + lab.css. Верификация: node --check OK; headless DOM-смоук (мини-DOM + реальный stereo.js в vm) 22/22: 12 сворачиваемых секций, тройка фигур слита (2 под-метки внутри «Фигуры»), пары заголовок→тело, дефолт-открытие, тоггл+персист, развернуть/свернуть всё, идемпотентная переинициализация, ни одна строка контролов не потеряна. Эмодзи/eval/new Function — 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -388,6 +388,31 @@
|
||||
border-color: var(--violet) !important; color: var(--violet) !important; background: rgba(155,93,229,.12) !important;
|
||||
}
|
||||
|
||||
/* ── stereo panel: collapsible accordion (UX) ── */
|
||||
.stereo-panel .st-acc-toolbar { display: flex; gap: 6px; margin: 0 0 8px; }
|
||||
.stereo-panel .st-acc-toolbar button {
|
||||
flex: 1; padding: 5px 6px; border-radius: 8px; border: 1px solid var(--border);
|
||||
background: transparent; color: var(--text-3);
|
||||
font-family: 'Manrope', sans-serif; font-size: .62rem; font-weight: 700;
|
||||
cursor: pointer; transition: all .12s;
|
||||
}
|
||||
.stereo-panel .st-acc-toolbar button:hover {
|
||||
color: var(--violet); border-color: rgba(155,93,229,.4); background: rgba(155,93,229,.06);
|
||||
}
|
||||
.stereo-panel .st-acc-hdr {
|
||||
cursor: pointer; justify-content: space-between; user-select: none;
|
||||
margin: 3px 0; padding: 8px 8px; border-radius: 9px;
|
||||
background: rgba(255,255,255,.025); transition: background .12s, color .12s;
|
||||
}
|
||||
.stereo-panel .st-acc-hdr::after { display: none; } /* drop the divider line */
|
||||
.stereo-panel .st-acc-hdr:hover { background: rgba(155,93,229,.09); color: var(--violet); }
|
||||
.stereo-panel .st-acc-hdr.open { color: var(--text-2); background: rgba(155,93,229,.06); }
|
||||
.stereo-panel .st-acc-chev { display: flex; align-items: center; opacity: .6; transition: transform .18s; }
|
||||
.stereo-panel .st-acc-chev svg { width: 13px; height: 13px; stroke: currentColor; stroke-width: 2.5; fill: none; }
|
||||
.stereo-panel .st-acc-hdr.open .st-acc-chev { transform: rotate(180deg); }
|
||||
.stereo-panel .st-acc-body { margin: 0 0 8px; padding: 0 1px; }
|
||||
.stereo-panel .st-sublabel { opacity: .8; margin: 8px 0 6px; }
|
||||
|
||||
.gp-preset-group { margin-bottom: 8px; }
|
||||
.gp-preset-label {
|
||||
font-size: 0.68rem; font-weight: 700; text-transform: uppercase;
|
||||
|
||||
@@ -4643,9 +4643,86 @@ class StereoSim {
|
||||
dodecahedron: ['a'],
|
||||
};
|
||||
|
||||
/* ── Collapsible accordion for the (long) stereo control panel ──
|
||||
Wraps each section's controls into a toggle body. Only the panel layout
|
||||
changes; every control/handler is preserved verbatim. */
|
||||
function _stereoInitPanel() {
|
||||
const panel = document.querySelector('#sim-stereo .stereo-panel');
|
||||
if (!panel || panel._accInit) return;
|
||||
panel._accInit = true;
|
||||
|
||||
const MERGE = { 'Правильные многогранники': 1, 'Тела вращения': 1 }; // folded into «Фигуры»
|
||||
const RENAME = { 'Многогранники': 'Фигуры' };
|
||||
const DEFAULT_OPEN = { 'Фигуры': 1, 'Параметры': 1 };
|
||||
const CHEV = '<span class="st-acc-chev"><svg viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"></polyline></svg></span>';
|
||||
|
||||
const kids = Array.prototype.slice.call(panel.children);
|
||||
const isTitle = (n) => n.classList && n.classList.contains('gp-section-title');
|
||||
const txt = (n) => (n.textContent || '').trim();
|
||||
|
||||
for (let i = 0; i < kids.length; i++) {
|
||||
const head = kids[i];
|
||||
if (!isTitle(head) || MERGE[txt(head)]) continue; // act only on real section heads
|
||||
const body = document.createElement('div');
|
||||
body.className = 'st-acc-body';
|
||||
let j = i + 1;
|
||||
while (j < kids.length) {
|
||||
const nx = kids[j];
|
||||
if (isTitle(nx) && !MERGE[txt(nx)]) break; // next section begins
|
||||
if (isTitle(nx)) nx.classList.add('st-sublabel'); // merged sub-section label
|
||||
body.appendChild(nx); // move contiguous controls into body
|
||||
j++;
|
||||
}
|
||||
const name = RENAME[txt(head)] || txt(head);
|
||||
head.classList.add('st-acc-hdr');
|
||||
head.style.marginTop = ''; head.style.marginBottom = '';
|
||||
head.setAttribute('role', 'button');
|
||||
head.tabIndex = 0;
|
||||
if (RENAME[txt(head)]) head.textContent = name;
|
||||
head.insertAdjacentHTML('beforeend', CHEV);
|
||||
head.after(body);
|
||||
_stereoBindAcc(head, body, name, !!DEFAULT_OPEN[name]);
|
||||
}
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'st-acc-toolbar';
|
||||
bar.innerHTML = '<button type="button" onclick="stereoAccAll(true)">Развернуть всё</button>'
|
||||
+ '<button type="button" onclick="stereoAccAll(false)">Свернуть всё</button>';
|
||||
panel.insertBefore(bar, panel.firstChild);
|
||||
}
|
||||
|
||||
function _stereoBindAcc(head, body, name, defOpen) {
|
||||
const key = 'stereo-acc-' + name;
|
||||
let stored = null; try { stored = localStorage.getItem(key); } catch (_) {}
|
||||
const open = stored === null ? defOpen : stored === '1';
|
||||
head.classList.toggle('open', open);
|
||||
body.style.display = open ? '' : 'none';
|
||||
const toggle = () => {
|
||||
const nowOpen = body.style.display === 'none';
|
||||
body.style.display = nowOpen ? '' : 'none';
|
||||
head.classList.toggle('open', nowOpen);
|
||||
try { localStorage.setItem(key, nowOpen ? '1' : '0'); } catch (_) {}
|
||||
};
|
||||
head.addEventListener('click', toggle);
|
||||
head.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } });
|
||||
}
|
||||
|
||||
function stereoAccAll(open) {
|
||||
const panel = document.querySelector('#sim-stereo .stereo-panel');
|
||||
if (!panel) return;
|
||||
Array.prototype.forEach.call(panel.querySelectorAll('.st-acc-hdr'), (h) => {
|
||||
const body = h.nextElementSibling;
|
||||
if (!body || !body.classList.contains('st-acc-body')) return;
|
||||
body.style.display = open ? '' : 'none';
|
||||
h.classList.toggle('open', open);
|
||||
try { localStorage.setItem('stereo-acc-' + (h.textContent || '').trim(), open ? '1' : '0'); } catch (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
function _openStereo(figureType) {
|
||||
document.getElementById('sim-topbar-title').textContent = 'Стереометрия 3D';
|
||||
_simShow('sim-stereo');
|
||||
try { _stereoInitPanel(); } catch (_) {}
|
||||
document.getElementById('stereo-stats').style.display = '';
|
||||
// Deep-link from a textbook: openSim('stereo:pyramid') or /lab?stereofig=pyramid
|
||||
if (!figureType) {
|
||||
|
||||
@@ -92,6 +92,16 @@
|
||||
снапшот `_undoStack`/`_redoStack`, кап 60; хуки в create/remove/clear; Ctrl+Z / Ctrl+Shift+Z /
|
||||
Ctrl+Y + кнопки «Отменить»/«Вернуть»). Видимость — не шаг истории (намеренно).
|
||||
|
||||
### UX панели управления (2026-06-17)
|
||||
|
||||
- [x] Панель `.stereo-panel` разрослась за фазы A–C до ~14 всегда-раскрытых секций (длинный скролл) →
|
||||
**сворачиваемый аккордеон**: `_stereoInitPanel()` (вызывается из `_openStereo`, идемпотентно) оборачивает
|
||||
контролы каждой секции в `.st-acc-body`, заголовки `.gp-section-title` → кликабельные `.st-acc-hdr`
|
||||
с шевроном; состояние каждой секции в localStorage (`stereo-acc-<имя>`). Тройка фигурных секций
|
||||
(Многогранники/Правильные/Тела вращения) слита в одну «Фигуры» (под-метки `.st-sublabel`). По
|
||||
умолчанию открыты «Фигуры» и «Параметры». Кнопки «Развернуть всё / Свернуть всё» (`stereoAccAll`).
|
||||
Сами контролы/обработчики не тронуты — только раскладка. Только `stereo.js` + `lab.css`.
|
||||
|
||||
### Фаза B — Умные точки
|
||||
|
||||
- [x] B1 — **Деление отрезка m:n** (`setDivideMode`/`setDivideRatio`): задаёшь m,n → кликаешь 2 точки A,B
|
||||
|
||||
Reference in New Issue
Block a user