From 601f5841816b6e4fe2dde89ed5fda958e95fbf9f Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Wed, 17 Jun 2026 17:48:08 +0300 Subject: [PATCH] =?UTF-8?q?feat(stereo):=20=D1=81=D0=B2=D0=BE=D1=80=D0=B0?= =?UTF-8?q?=D1=87=D0=B8=D0=B2=D0=B0=D0=B5=D0=BC=D1=8B=D0=B9=20=D0=B0=D0=BA?= =?UTF-8?q?=D0=BA=D0=BE=D1=80=D0=B4=D0=B5=D0=BE=D0=BD=20=D0=BF=D0=B0=D0=BD?= =?UTF-8?q?=D0=B5=D0=BB=D0=B8=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20(UX)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Панель за фазы 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) --- frontend/css/lab.css | 25 +++++++++++ frontend/js/labs/stereo.js | 77 ++++++++++++++++++++++++++++++++++ plans/STEREO_3D_IMPROVEMENT.md | 10 +++++ 3 files changed, 112 insertions(+) diff --git a/frontend/css/lab.css b/frontend/css/lab.css index e63e634..61aa02f 100644 --- a/frontend/css/lab.css +++ b/frontend/css/lab.css @@ -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; diff --git a/frontend/js/labs/stereo.js b/frontend/js/labs/stereo.js index 19095f9..d3fd2f1 100644 --- a/frontend/js/labs/stereo.js +++ b/frontend/js/labs/stereo.js @@ -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 = ''; + + 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 = '' + + ''; + 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) { diff --git a/plans/STEREO_3D_IMPROVEMENT.md b/plans/STEREO_3D_IMPROVEMENT.md index e93a325..52332d6 100644 --- a/plans/STEREO_3D_IMPROVEMENT.md +++ b/plans/STEREO_3D_IMPROVEMENT.md @@ -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