feat: LS.modal — общий компонент модалок + миграция /exam9 + /my-students

Новый общий компонент LS.modal (api.js) — companion к LS.confirm.
Универсальная form/content-модалка с консистентным поведением:

  LS.modal({
    title, content, size: 'sm'|'md'|'lg',
    actions: [{label, primary, danger, onClick}],
    onClose,
  });
  // Returns { close, root, body, setBody, setActions, setError }

Стандартное поведение:
  - ESC и backdrop-click закрывают (опциональный dismissible:false)
  - z-index 9000 (тот же что LS.confirm — без конфликтов)
  - Auto-focus первого input/select/textarea/button в body
  - prevFocus restore при закрытии
  - Анимация scale+translateY .22s
  - Адаптив: на мобилках padding уменьшается

CSS-классы .ls-mov / .ls-mod / .ls-mod-hdr / .ls-mod-body / .ls-mod-act
впрыскиваются один раз из api.js (id=ls-modal-style), как и стили
toast/confirm.

Миграция exam9 «Назначить вариант»:
  - Убран inline <div class="ex-overlay" id="assign-overlay">…</div>
  - Убраны .ax-actions, .ax-btn, .ax-btn-primary, .ax-error, .ax-success
    CSS (теперь в общих .ls-mod-* стилях)
  - openAssignModal → LS.modal({ title, content: form, actions: [...] })
  - Удалены closeAssignModal/onAssignOverlayClick/onAssignEsc — теперь
    handle'ит LS.modal
  - Удалена unused переменная assignVariantNum (closure теперь над varNum)

  exam9.html:  −53 строк (CSS + HTML модалки)
  app.js:      переписан 90 строк → 70 строк

Миграция my-students «Убрать ученика»:
  - native confirm() → LS.confirm() с danger-стилизацией
  - alert() → LS.toast() для согласованности

Сохранён классroom-овский «ex-overlay»/«ex-panel» CSS (используется
для picker'а вариантов в exam9). Не трогаем classroom.html — у него
своя ecosystem cr-*-overlay.

Дальше — postupенная миграция модалок в textbooks/classes/admin
по мере касания этих страниц. Шаблон установлен.
This commit is contained in:
Maxim Dolgolyov
2026-05-16 18:41:27 +03:00
parent b1e645157a
commit bc22715734
4 changed files with 211 additions and 135 deletions
+7 -2
View File
@@ -277,13 +277,18 @@
};
window.removeStudent = async function (id, name) {
if (!confirm(`Убрать «${name}» из списка «Мои ученики»?\n\nСозданные задания не удалятся.`)) return;
const ok = await LS.confirm(
`Убрать «${name}» из списка?\nСозданные задания не удалятся — ученик продолжит их видеть.`,
{ title: 'Убрать ученика', confirmText: 'Убрать', danger: true }
);
if (!ok) return;
try {
await LS.api('/api/teacher-students/' + id, { method: 'DELETE' });
students = students.filter(s => s.id !== id);
render();
LS.toast(`«${name}» убран из списка`, 'success');
} catch (e) {
alert('Ошибка: ' + e.message);
LS.toast('Ошибка: ' + e.message, 'error');
}
};