From 485e7e4fbac440c9f08052b8acf8f348c9b86de5 Mon Sep 17 00:00:00 2001 From: Server <10.9.8.250@reg.snw.su> Date: Fri, 14 Aug 2026 11:53:04 +0000 Subject: [PATCH] Refine mobile menu navigation --- README.md | 2 +- docslib/_version.py | 2 +- docslib/templates/client.css | 4 +++- docslib/templates/client.js | 21 ++++++++++++++++----- docslib/templates/index.html | 1 + tests/test_server.py | 12 +++++++++--- 6 files changed, 31 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e897db2..de7a143 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ UI не содержит языко-зависимых служебных под На десктопе компактный header содержит toggle боковой панели, полное название документации, поиск и переключатель темы. Sidebar по умолчанию открыт, его состояние не сохраняется между загрузками, а при сворачивании внутренняя раскладка остаётся неподвижной и обрезается границей панели без переноса текста. Поисковый placeholder медленно печатает первые слова случайного названия, добавляет троеточие только в конце и сохраняет готовый текст 5 секунд; при вводе поле раскрывает ранжированные результаты вниз. Проверка manifest выполняется автоматически в фоне без отдельной кнопки обновления. -На мобильных устройствах используется viewport `100dvh` и нижний dock. Кнопка `☰` заменяет содержимое страницы копией desktop-sidebar, которая выезжает с левой границы экрана. Ввод в нижний поиск заменяет содержимое результатами, расположенными снизу вверх по релевантности. При переходе между страницами контент вместе с кнопками предыдущей и следующей страницы плавно смещается на 30 px по направлению навигации и меняет прозрачность; тема и обновление на мобильном скрыты. +На мобильных устройствах используется viewport `100dvh` и нижний dock. Кнопка `☰` заменяет содержимое страницы копией desktop-sidebar, которая выезжает с левой границы экрана; между header-кнопками и деревом доступен круглый переключатель темы. Выбор страницы в открытом мобильном меню закрывает его и показывает новый контент моментально, без page-transition. Ввод в нижний поиск заменяет содержимое результатами, расположенными снизу вверх по релевантности. При остальных переходах контент вместе с кнопками предыдущей и следующей страницы плавно смещается на 30 px по направлению навигации и меняет прозрачность. ## Установка из Git diff --git a/docslib/_version.py b/docslib/_version.py index cb58ca4..b443f7d 100644 --- a/docslib/_version.py +++ b/docslib/_version.py @@ -1,3 +1,3 @@ """Single source of truth for the SharedDocsLib package version.""" -__version__ = "0.4.2" +__version__ = "0.4.3" diff --git a/docslib/templates/client.css b/docslib/templates/client.css index d5e492c..4001cda 100644 --- a/docslib/templates/client.css +++ b/docslib/templates/client.css @@ -165,8 +165,10 @@ body.desktop-sidebar-collapsed .desktop-sidebar { width: 0; flex-basis: 0; paddi .mobile-menu-view { padding: 52px 28px calc(150px + env(safe-area-inset-bottom)); } body.mobile-menu-open .mobile-menu-view { animation: mobile-menu-enter .26s cubic-bezier(.2, .8, .2, 1) both; } .mobile-site-title { margin: 0 0 34px; font-size: clamp(2.4rem, 11vw, 4rem); font-weight: 400; line-height: 1.1; letter-spacing: -.035em; overflow-wrap: anywhere; } - .mobile-header-buttons { display: flex; flex-direction: column; gap: 6px; margin-bottom: 38px; border: 1px solid var(--border); border-radius: 24px; background: var(--surface); } + .mobile-header-buttons { display: flex; flex-direction: column; gap: 6px; margin-bottom: 18px; border: 1px solid var(--border); border-radius: 24px; background: var(--surface); } .mobile-header-buttons .header-link { padding: 12px 18px; border-radius: 28px; font-size: 1.05rem; } + .mobile-theme-button { display: grid; width: 54px; height: 54px; margin: 0 0 32px; place-items: center; border: 1px solid var(--border); border-radius: 50%; color: var(--text); background: var(--surface); font-size: 1.35rem; } + .mobile-theme-button:hover { background: var(--control); } .mobile-page-tree { overflow: visible; padding-bottom: 24px; } .mobile-page-tree .tree-link { min-height: 46px; padding: 9px 14px; border-radius: 24px; font-size: 1.05rem; } .mobile-page-tree .tree-children { margin-left: 20px; padding-left: 12px; } diff --git a/docslib/templates/client.js b/docslib/templates/client.js index 13adab2..2e4202b 100644 --- a/docslib/templates/client.js +++ b/docslib/templates/client.js @@ -27,6 +27,8 @@ previous: document.getElementById("previous-page"), next: document.getElementById("next-page"), themeIcon: document.getElementById("theme-icon"), + mobileThemeButton: document.getElementById("mobile-theme-button"), + mobileThemeIcon: document.getElementById("mobile-theme-icon"), refresh: document.getElementById("refresh-button"), identity: document.getElementById("manifest-identity"), mobileMenuButton: document.getElementById("mobile-menu-button"), @@ -48,6 +50,7 @@ let lastSyncedRevision = String(manifest.revision || ""); let pageTransitionId = 0; let pageAnimation = null; + let skipNextPageAnimation = false; const dbPromise = new Promise((resolve, reject) => { if (!window.indexedDB) return reject(new Error("IndexedDB")); @@ -155,6 +158,7 @@ function navigate(number, replace = false) { const hash = `#/page/${encodeURIComponent(number)}`; + if (mobileLayout.matches && mobileMode === "menu") skipNextPageAnimation = true; closeSearch({ clear: true }); setMobileMode("content"); if (replace) { @@ -381,11 +385,13 @@ let index = manifest.pages.findIndex((page) => page.number === number); if (index < 0) index = 0; const page = manifest.pages[index]; + const skipAnimation = skipNextPageAnimation; + skipNextPageAnimation = false; if (page.number === currentNumber && elements.content.childNodes.length) return; const currentIndex = manifest.pages.findIndex((candidate) => candidate.number === currentNumber); const direction = currentIndex >= 0 && index < currentIndex ? -1 : 1; - const animate = currentIndex >= 0 && !reducedMotion.matches && typeof elements.pageTransition.animate === "function"; + const animate = currentIndex >= 0 && !skipAnimation && !reducedMotion.matches && typeof elements.pageTransition.animate === "function"; const transitionId = ++pageTransitionId; pageAnimation?.cancel(); @@ -557,10 +563,16 @@ function applyTheme(theme) { document.documentElement.dataset.theme = theme; - elements.themeIcon.textContent = theme === "dark" ? "☀" : "◐"; + const icon = theme === "dark" ? "☀" : "◐"; + elements.themeIcon.textContent = icon; + elements.mobileThemeIcon.textContent = icon; localStorage.setItem("docslib-theme", theme); } + function toggleTheme() { + applyTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark"); + } + function renderAll() { window.__DOCSLIB_MANIFEST__ = manifest; document.documentElement.lang = "und"; @@ -705,9 +717,8 @@ elements.desktopSidebarToggle.addEventListener("click", () => { setDesktopSidebar(document.body.classList.contains("desktop-sidebar-collapsed")); }); - document.getElementById("theme-button").addEventListener("click", () => { - applyTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark"); - }); + document.getElementById("theme-button").addEventListener("click", toggleTheme); + elements.mobileThemeButton.addEventListener("click", toggleTheme); elements.refresh.addEventListener("pointerenter", () => elements.toolbar.classList.add("refresh-expanded")); elements.refresh.addEventListener("pointerleave", () => elements.toolbar.classList.remove("refresh-expanded")); elements.refresh.addEventListener("focus", () => elements.toolbar.classList.add("refresh-expanded")); diff --git a/docslib/templates/index.html b/docslib/templates/index.html index 6cb516f..890846d 100644 --- a/docslib/templates/index.html +++ b/docslib/templates/index.html @@ -63,6 +63,7 @@

{{DOCSLIB_TITLE}}

+
diff --git a/tests/test_server.py b/tests/test_server.py index 3bd5c89..89faeee 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -97,7 +97,7 @@ def test_all_endpoints_and_self_contained_build(tmp_path): assert html_response.status_code == 200 assert "http://127.0.0.1:9000/manifest.json" in html assert "http://127.0.0.1:9000/version" in html - assert 'globalThis.COMPILED_FROM_VERSION = "0.4.2";' in html + assert 'globalThis.COMPILED_FROM_VERSION = "0.4.3";' in html assert ".content { --custom-test: yes; }" in html assert r'document.body.dataset.customTest = \"yes\";' in html assert 'id="docslib-loading"' in html @@ -184,6 +184,8 @@ def test_all_endpoints_and_self_contained_build(tmp_path): assert 'class="sidebar-header-buttons"' in html assert 'id="mobile-search-input"' in html assert 'id="mobile-menu-view"' in html + assert 'id="mobile-theme-button"' in html + assert 'id="mobile-theme-icon"' in html assert 'id="mobile-search-view"' in html assert "placeholderTick" in html assert '".".repeat(dots)' in manifest["client"]["js"]["system"] @@ -192,7 +194,11 @@ def test_all_endpoints_and_self_contained_build(tmp_path): assert "border: 1px solid transparent" not in html assert ".sidebar-header-buttons { display: flex; flex: 0 0 auto; flex-direction: column; gap: 4px; margin-bottom: 0; border: 1px solid var(--border); border-radius: 16px; background: var(--surface); }" in html assert ".desktop-sidebar > .page-tree { flex: 1; padding: 24px 2px 70px; mask-image: linear-gradient(transparent 0px, #000 24px, #000 calc(100% - 100px), transparent); scrollbar-width: none; }" in manifest["client"]["css"]["system"] - assert ".mobile-header-buttons { display: flex; flex-direction: column; gap: 6px; margin-bottom: 38px; border: 1px solid var(--border); border-radius: 24px; background: var(--surface); }" in html + assert ".mobile-header-buttons { display: flex; flex-direction: column; gap: 6px; margin-bottom: 18px; border: 1px solid var(--border); border-radius: 24px; background: var(--surface); }" in html + assert ".mobile-theme-button { display: grid; width: 54px; height: 54px; margin: 0 0 32px; place-items: center; border: 1px solid var(--border); border-radius: 50%; color: var(--text); background: var(--surface); font-size: 1.35rem; }" in html + assert 'elements.mobileThemeButton.addEventListener("click", toggleTheme)' in system_js + assert 'if (mobileLayout.matches && mobileMode === "menu") skipNextPageAnimation = true' in system_js + assert "currentIndex >= 0 && !skipAnimation && !reducedMotion.matches" in system_js assert ".sidebar-header-buttons .header-link { color: var(--text); }" in html assert ".sidebar-header-buttons .header-link:hover { background: var(--control); }" in html assert ".sidebar-header-buttons .header-link.active { color: #050505; background: #f0f0f0; }" not in html @@ -236,7 +242,7 @@ def test_all_endpoints_and_self_contained_build(tmp_path): def test_build_writes_static_bundle(tmp_path): - assert __version__ == "0.4.2" + assert __version__ == "0.4.3" @Page("1", "Static page") def static_page():