UI: адаптив, журнал, спиннеры; docs: README и api_routes

- Навигация: выезжающее меню при узком экране (nav-mobile.js)
- Журнал: карточки <620px, компактная пагинация, время в две строки <920px
- Создание кластера: оверлей загрузки, инкрементальное обновление таблицы заданий
- Документация: полноэкранный спиннер при загрузке и навигации
- Главная: масштабирование CTA, статистика 2 колонки <520px, донаты перенос <710px
- README: env.example, новые фичи UI, автор в конце файла
- api_routes: маршрут /cluster-create, спиннеры, шаблоны; автор в конце
- env.example: автор перенесён в конец файла
This commit is contained in:
Sergey Antropoff
2026-04-05 00:18:19 +03:00
parent 4b703801e1
commit f0b24c8901
12 changed files with 2426 additions and 173 deletions
+67 -17
View File
@@ -24,6 +24,25 @@
el.classList.add("hidden");
}
/** Полноэкранный спиннер как на панели (page-loading-overlay в documentation.html). */
function showDocumentationPageLoadingOverlay() {
var el = document.getElementById("documentation-page-loading-overlay");
if (!el) return;
el.classList.remove("hidden");
el.setAttribute("aria-busy", "true");
el.setAttribute("aria-hidden", "false");
document.body.classList.add("documentation-page-loading");
}
function hideDocumentationPageLoadingOverlay() {
var el = document.getElementById("documentation-page-loading-overlay");
if (!el) return;
el.classList.add("hidden");
el.setAttribute("aria-busy", "false");
el.setAttribute("aria-hidden", "true");
document.body.classList.remove("documentation-page-loading");
}
function getPathFromLocation() {
var q = new URLSearchParams(window.location.search || "");
var p = (q.get("path") || "").trim();
@@ -193,10 +212,36 @@
return page;
}
function renderMarkdownToRoot(text, docPath, rootEl, errEl, loadEl) {
/**
* Таблицы Markdown с более чем двумя колонками: класс и data-label для мини-карточек при ширине до 480px (CSS).
* @param {HTMLElement} rootEl
*/
function markupWideMarkdownTables(rootEl) {
if (!rootEl || !rootEl.querySelectorAll) return;
rootEl.querySelectorAll(".markdown-body table").forEach(function (table) {
var thList = table.querySelectorAll("thead tr th");
if (!thList.length) return;
var n = thList.length;
if (n <= 2) return;
table.classList.add("markdown-body-table--stack");
var labels = [];
for (var i = 0; i < thList.length; i++) {
labels.push((thList[i].textContent || "").trim());
}
var tbody = table.querySelector("tbody");
if (!tbody) return;
tbody.querySelectorAll("tr").forEach(function (tr) {
var tds = tr.querySelectorAll("td");
for (var j = 0; j < tds.length; j++) {
if (labels[j]) tds[j].setAttribute("data-label", labels[j]);
}
});
});
}
function renderMarkdownToRoot(text, docPath, rootEl, errEl) {
if (typeof marked === "undefined" || typeof DOMPurify === "undefined") {
showErr(errEl, "Не загружены скрипты marked или DOMPurify из /static/js/vendor/.");
if (loadEl) loadEl.classList.add("hidden");
applyDocumentationTabTitle("");
return;
}
@@ -223,6 +268,7 @@
while (rootEl.firstChild) rootEl.removeChild(rootEl.firstChild);
var pageInner = sectionizeFromNodes(temp);
rootEl.appendChild(pageInner);
markupWideMarkdownTables(rootEl);
rootEl.removeAttribute("hidden");
} catch (e2) {
showErr(errEl, "Ошибка разбора: " + (e2.message || String(e2)));
@@ -242,18 +288,20 @@
var rootEl = document.getElementById("readme-doc-root");
var errEl = document.getElementById("readme-error");
var loadEl = document.getElementById("readme-loading");
if (!rootEl || !loadEl) return;
if (!rootEl) {
hideDocumentationPageLoadingOverlay();
return;
}
hideErr(errEl);
rootEl.setAttribute("hidden", "hidden");
while (rootEl.firstChild) rootEl.removeChild(rootEl.firstChild);
loadEl.classList.remove("hidden");
updateBreadcrumbs(docPath);
var url = fetchUrlForPath(docPath);
showDocumentationPageLoadingOverlay();
try {
hideErr(errEl);
rootEl.setAttribute("hidden", "hidden");
while (rootEl.firstChild) rootEl.removeChild(rootEl.firstChild);
updateBreadcrumbs(docPath);
var url = fetchUrlForPath(docPath);
var r = await fetch(url, {
headers: { Accept: "text/markdown, text/plain, */*" },
});
@@ -267,13 +315,11 @@
/* raw */
}
showErr(errEl, "Не удалось загрузить документ: " + (detail || r.statusText));
loadEl.classList.add("hidden");
applyDocumentationTabTitle("");
return;
}
renderMarkdownToRoot(text, docPath, rootEl, errEl, loadEl);
loadEl.classList.add("hidden");
renderMarkdownToRoot(text, docPath, rootEl, errEl);
var newUrl = docUrlForPath(docPath);
if (push) {
@@ -283,8 +329,9 @@
}
} catch (e) {
showErr(errEl, "Ошибка: " + (e.message || String(e)));
loadEl.classList.add("hidden");
applyDocumentationTabTitle("");
} finally {
hideDocumentationPageLoadingOverlay();
}
}
@@ -309,7 +356,10 @@
function run() {
var rootEl = document.getElementById("readme-doc-root");
if (!rootEl) return;
if (!rootEl) {
hideDocumentationPageLoadingOverlay();
return;
}
window.addEventListener("popstate", onPopState);
rootEl.addEventListener("click", onRootClick);