Refine mobile menu navigation

This commit is contained in:
Server 2026-08-14 11:53:04 +00:00
parent 9376bcbd19
commit 485e7e4fba
6 changed files with 31 additions and 11 deletions

View file

@ -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

View file

@ -1,3 +1,3 @@
"""Single source of truth for the SharedDocsLib package version."""
__version__ = "0.4.2"
__version__ = "0.4.3"

View file

@ -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; }

View file

@ -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"));

View file

@ -63,6 +63,7 @@
<section id="mobile-menu-view" class="mobile-view mobile-menu-view">
<h1 id="mobile-site-title" class="mobile-site-title">{{DOCSLIB_TITLE}}</h1>
<nav id="mobile-header-buttons" class="mobile-header-buttons" aria-label="⋯"></nav>
<button id="mobile-theme-button" class="mobile-theme-button glyph-button" aria-label="◐"><span id="mobile-theme-icon"></span></button>
<nav id="mobile-page-tree" class="page-tree mobile-page-tree" aria-label="≡"></nav>
</section>

View file

@ -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():