diff --git a/README.md b/README.md index ca3a114..a536211 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ SharedDocsLib — небольшой Python-фреймворк для докум UI не содержит языко-зависимых служебных подписей. Базовый шрифт — `system-ui 16px`, а служебные UTF-8-пиктограммы используют Arial. Навигационные стрелки отрисовываются CSS-границами без текстовых символов. AMOLED-тема включена по умолчанию и сохраняется локально после переключения. +Accessibility-кнопка создаётся системным JavaScript, поэтому появляется и в ранее скомпилированных HTML после обновления manifest. На десктопе она закреплена в левом нижнем углу ниже loading-overlay, на мобильном располагается рядом с переключателем темы внутри меню. Псевдостраница настроек позволяет менять размер текста от 12 до 28 px, сбрасывать параметры и выбирать только локальные браузерные семейства `system-ui`, `sans-serif`, `serif` и `monospace`; сетевые шрифты не используются. Размер применяется только к sidebar, документации и кнопкам Previous/Next, а семейство — ко всему интерфейсу, кроме UTF-8 glyph-кнопок. Параметры сохраняются в `localStorage`. + На десктопе компактный header содержит toggle боковой панели, полное название документации, поиск и переключатель темы. Sidebar по умолчанию открыт, его состояние не сохраняется между загрузками, а при сворачивании внутренняя раскладка остаётся неподвижной и обрезается границей панели без переноса текста. Поисковый placeholder медленно печатает первые слова случайного названия, добавляет троеточие только в конце и сохраняет готовый текст 5 секунд; при вводе поле раскрывает ранжированные результаты вниз. Проверка manifest выполняется автоматически в фоне без отдельной кнопки обновления. Обновлённый runtime сохраняет совместимость со старыми скомпилированными HTML-оболочками: отсутствие desktop-toggle, мобильного переключателя темы или общего контейнера page-transition не останавливает запуск клиента. diff --git a/docslib/_version.py b/docslib/_version.py index 81b5cd7..1cc63bb 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.5" +__version__ = "0.5.0" diff --git a/docslib/templates/client.css b/docslib/templates/client.css index 4001cda..014b1c5 100644 --- a/docslib/templates/client.css +++ b/docslib/templates/client.css @@ -13,6 +13,8 @@ --border: #c9c9c9; --tree-line: #c5c5c5; --shadow: 0 24px 70px rgba(0, 0, 0, .4); + --accessibility-font-family: system-ui; + --accessibility-font-size: 16px; color-scheme: light; } @@ -38,7 +40,7 @@ [hidden] { display: none !important; } html, body { width: 100%; height: 100dvh; min-height: 100dvh; max-height: 100dvh; overflow: hidden; } html { background: var(--shell); } -body { margin: 0; color: var(--text); background: var(--shell); font-family: system-ui; font-size: 16px; line-height: 1.55; } +body { margin: 0; color: var(--text); background: var(--shell); font-family: var(--accessibility-font-family); font-size: 16px; line-height: 1.55; } button, input { font: inherit; } button, a { -webkit-tap-highlight-color: transparent; } button { color: inherit; } @@ -56,6 +58,7 @@ button { color: inherit; } .desktop-main { display: flex; min-width: 0; min-height: 0; flex: 1; overflow: hidden; } .desktop-sidebar { display: flex; width: var(--desktop-sidebar-width); min-width: 0; height: 100%; flex: 0 0 var(--desktop-sidebar-width); flex-direction: column; overflow: hidden; padding: 0 18px; background: var(--shell); opacity: 1; transition: width .24s ease, flex-basis .24s ease, padding .24s ease, opacity .16s ease; } +.desktop-sidebar, .mobile-menu-view, .content, .page-navigation { font-size: var(--accessibility-font-size); } .desktop-sidebar > * { width: calc(var(--desktop-sidebar-width) - 36px); min-width: calc(var(--desktop-sidebar-width) - 36px); } body.desktop-sidebar-collapsed .desktop-sidebar { width: 0; flex-basis: 0; padding-right: 0; padding-left: 0; opacity: 0; pointer-events: none; } .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); } @@ -107,16 +110,16 @@ body.desktop-sidebar-collapsed .desktop-sidebar { width: 0; flex-basis: 0; paddi .page-transition { min-width: 0; } .content { width: min(1120px, calc(100% - 72px)); margin: 0 auto; padding: 42px 0 44px; outline: none; } .content > :first-child { margin-top: 0; } -.content h1 { margin: 0 0 1.5rem; font-size: clamp(2.15rem, 2.7vw, 3.35rem); font-weight: 400; line-height: 1.08; letter-spacing: -.035em; } -.content h2 { margin: 2.5rem 0 .9rem; padding-bottom: .35rem; border-bottom: 1px solid var(--border); font-size: 1.65rem; font-weight: 500; } -.content h3 { margin: 1.9rem 0 .55rem; font-size: 1.25rem; font-weight: 550; } -.content p { margin: .8rem 0 1.2rem; font-size: 1.08rem; } +.content h1 { margin: 0 0 1.5rem; font-size: clamp(2.15em, 2.7vw, 3.35em); font-weight: 400; line-height: 1.08; letter-spacing: -.035em; } +.content h2 { margin: 2.5rem 0 .9rem; padding-bottom: .35rem; border-bottom: 1px solid var(--border); font-size: 1.65em; font-weight: 500; } +.content h3 { margin: 1.9rem 0 .55rem; font-size: 1.25em; font-weight: 550; } +.content p { margin: .8rem 0 1.2rem; font-size: 1.08em; } .content a { color: var(--text); text-underline-offset: 4px; } .content img { display: block; max-width: 100%; height: auto; margin: 1.5rem auto; border-radius: 20px; } .content figure { margin: 1.6rem 0; } .content figcaption { margin-top: .55rem; color: var(--muted); text-align: center; } .content pre { overflow: auto; margin: 1.3rem 0; padding: 18px 20px; border: 1px solid var(--border); border-radius: 18px; background: var(--shell); } -.content code { padding: .12em .35em; border-radius: 5px; background: var(--shell); font: .88em/1.5 ui-monospace, monospace; } +.content code { padding: .12em .35em; border-radius: 5px; background: var(--shell); font: .88em/1.5 var(--accessibility-font-family); } .content pre code { padding: 0; background: transparent; } .content blockquote { margin: 1.4rem 0; padding: .35rem 1.25rem; border-left: 2px solid var(--text); color: var(--muted); } .content hr { margin: 2.6rem 0; border: 0; border-top: 1px solid var(--border); } @@ -139,6 +142,39 @@ body.desktop-sidebar-collapsed .desktop-sidebar { width: 0; flex-basis: 0; paddi #next-page { color: var(--shell); background: var(--text); } :root[data-theme="dark"] #next-page { color: #000; background: #eee; } +.accessibility-icon-button { display: grid; place-items: center; padding: 0; border: 1px solid var(--border); border-radius: 50%; color: var(--text); background: var(--surface); cursor: pointer; } +.accessibility-icon-button:hover { background: var(--control); } +.accessibility-icon-button:focus-visible, .accessibility-back:focus-visible, .accessibility-step:focus-visible, .accessibility-reset:focus-visible, .accessibility-font-options button:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; } +.accessibility-glyph { width: 25px; height: 25px; overflow: visible; fill: currentColor; } +.accessibility-glyph path { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; } +.accessibility-button { position: fixed; z-index: 40; bottom: max(16px, env(safe-area-inset-bottom)); left: 20px; width: 50px; height: 50px; } +body.accessibility-open .accessibility-button { display: none; } +body.accessibility-open .content, body.accessibility-open .page-navigation { display: none; } +.accessibility-view { position: relative; width: min(1120px, calc(100% - 72px)); margin: 0 auto; padding: 42px 0 64px; } +.accessibility-title { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } +.accessibility-back, .accessibility-reset, .accessibility-step { display: grid; place-items: center; border: 0; color: var(--text); background: var(--control); cursor: pointer; } +.accessibility-back { width: 48px; height: 48px; margin-bottom: 28px; border-radius: 50%; } +.accessibility-back:hover, .accessibility-reset:hover, .accessibility-step:hover { background: var(--active); } +.accessibility-back-arrow { width: 12px; height: 12px; border-bottom: 2px solid currentColor; border-left: 2px solid currentColor; transform: rotate(45deg); } +.accessibility-size-row { display: flex; align-items: center; gap: 18px; } +.accessibility-size-control { display: flex; height: 52px; align-items: center; overflow: hidden; border-radius: 28px; background: var(--control); } +.accessibility-step { width: 52px; height: 52px; flex: 0 0 52px; background: transparent; font-size: 1.45rem; } +.accessibility-step:disabled { color: var(--faint); cursor: default; opacity: .55; } +.accessibility-size-control output { min-width: 42px; color: var(--text); font-size: 1rem; text-align: center; } +.accessibility-reset { width: 52px; height: 52px; flex: 0 0 52px; border-radius: 50%; font-size: 1.35rem; } +.accessibility-font-options { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 36px; } +.accessibility-font-options button { min-height: 48px; padding: 9px 18px; border: 1px solid transparent; border-radius: 26px; color: var(--text); background: var(--control); cursor: pointer; } +.accessibility-font-options button:hover { background: var(--active); } +.accessibility-font-options button[aria-pressed="true"] { color: var(--selection-text); background: var(--selection-bg); } +.accessibility-font-options [data-accessibility-font="system-ui"] { font-family: system-ui; } +.accessibility-font-options [data-accessibility-font="sans-serif"] { font-family: sans-serif; } +.accessibility-font-options [data-accessibility-font="serif"] { font-family: serif; } +.accessibility-font-options [data-accessibility-font="monospace"] { font-family: monospace; } +.accessibility-preview { max-width: 920px; margin-top: 56px; font-size: var(--accessibility-font-size); } +.accessibility-preview h1 { margin: 0 0 12px; font-size: 2em; font-weight: 600; line-height: 1.15; } +.accessibility-preview h2 { margin: 42px 0 10px; font-size: 1.55em; font-weight: 600; line-height: 1.2; } +.accessibility-preview p { margin: 0; font-size: 1.05em; line-height: 1.45; } + .mobile-view, .mobile-dock { display: none; } .full-print-document { display: none; } .print-loading-overlay { position: fixed; inset: 0; z-index: 2147483647; display: grid; width: 100vw; height: 100dvh; place-items: center; color: var(--text); background: var(--shell); } @@ -151,9 +187,9 @@ body.desktop-sidebar-collapsed .desktop-sidebar { width: 0; flex-basis: 0; paddi .workspace { width: 100%; height: 100%; } .page-surface { display: block; width: 100%; border-radius: 0; background: var(--shell); } .content { width: 100%; padding: 52px 28px 28px; } - .content h1 { margin-bottom: 1.55rem; font-size: clamp(2.45rem, 12vw, 4rem); } - .content h2 { font-size: 1.55rem; } - .content p { font-size: 1.05rem; } + .content h1 { margin-bottom: 1.55rem; font-size: 2.45em; } + .content h2 { font-size: 1.55em; } + .content p { font-size: 1.05em; } .page-navigation { width: 100%; flex-direction: column; align-items: flex-start; gap: 10px; padding: 0 28px calc(150px + env(safe-area-inset-bottom)); } #next-page { order: 1; } #previous-page { order: 2; } @@ -166,11 +202,12 @@ body.desktop-sidebar-collapsed .desktop-sidebar { width: 0; flex-basis: 0; paddi 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: 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-header-buttons .header-link { padding: 12px 18px; border-radius: 28px; font-size: 1.05em; } + .mobile-preference-actions { display: flex; align-items: center; gap: 10px; margin-bottom: 32px; } + .mobile-theme-button, .mobile-accessibility-button { display: grid; width: 54px; height: 54px; margin: 0; 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-link { min-height: 46px; padding: 9px 14px; border-radius: 24px; font-size: 1.05em; } .mobile-page-tree .tree-children { margin-left: 20px; padding-left: 12px; } .mobile-search-view { flex-direction: column; justify-content: flex-end; padding: 40px 28px calc(142px + env(safe-area-inset-bottom)); overflow: hidden; } .mobile-search-results { display: flex; max-height: 100%; flex: 1; flex-direction: column-reverse; justify-content: flex-start; gap: 20px; overflow-y: auto; } @@ -184,6 +221,11 @@ body.desktop-sidebar-collapsed .desktop-sidebar { width: 0; flex-basis: 0; paddi .mobile-search-control { display: flex; min-width: 0; height: 72px; flex: 1; align-items: center; border-radius: 38px; background: var(--control); } .mobile-search-control input { padding-left: 30px; font-size: 1.3rem; } .mobile-search-control .search-glyph { width: 64px; height: 64px; flex-basis: 64px; background: transparent; font-size: 2.3rem; } + .accessibility-button { display: none; } + .accessibility-view { width: 100%; padding: 36px 28px calc(150px + env(safe-area-inset-bottom)); } + .accessibility-back { margin-bottom: 24px; } + .accessibility-font-options { margin-top: 30px; } + .accessibility-preview { margin-top: 46px; } } @keyframes mobile-menu-enter { @@ -202,7 +244,7 @@ body.desktop-sidebar-collapsed .desktop-sidebar { width: 0; flex-basis: 0; paddi .content a, .content figcaption, .content blockquote { color: #000 !important; } .content pre, .content code, .content details, .content th, .callout { color: #000 !important; background: #fff !important; border-color: #777 !important; } .app-shell { display: block; } - .desktop-header, .desktop-sidebar, .mobile-dock, .page-navigation, .print-loading-overlay { display: none !important; } + .desktop-header, .desktop-sidebar, .mobile-dock, .page-navigation, .print-loading-overlay, .accessibility-button, .accessibility-view { display: none !important; } .workspace, .page-surface { display: block; height: auto; overflow: visible; } .content { width: 100%; padding: 0; } body.docslib-print-all > .app-shell { display: none; } diff --git a/docslib/templates/client.js b/docslib/templates/client.js index 7548790..45c33ac 100644 --- a/docslib/templates/client.js +++ b/docslib/templates/client.js @@ -32,6 +32,15 @@ refresh: document.getElementById("refresh-button"), identity: document.getElementById("manifest-identity"), mobileMenuButton: document.getElementById("mobile-menu-button"), + accessibilityButton: null, + mobileAccessibilityButton: null, + accessibilityView: null, + accessibilityBack: null, + accessibilitySizeValue: null, + accessibilitySizeDecrease: null, + accessibilitySizeIncrease: null, + accessibilityReset: null, + accessibilityFontOptions: null, }; let manifest = window.__DOCSLIB_MANIFEST__ || embedded; @@ -51,6 +60,10 @@ let pageTransitionId = 0; let pageAnimation = null; let skipNextPageAnimation = false; + let accessibilitySize = 16; + let accessibilityFont = "system-ui"; + + const accessibilityFonts = new Set(["system-ui", "sans-serif", "serif", "monospace"]); const dbPromise = new Promise((resolve, reject) => { if (!window.indexedDB) return reject(new Error("IndexedDB")); @@ -147,6 +160,129 @@ elements.desktopSidebarToggle.setAttribute("aria-expanded", String(open)); } + function createAccessibilityButton(id, className) { + const button = document.createElement("button"); + button.id = id; + button.className = `${className} accessibility-icon-button`; + button.type = "button"; + button.setAttribute("aria-label", "Accessibility settings"); + button.setAttribute("aria-controls", "accessibility-view"); + button.setAttribute("aria-expanded", "false"); + button.innerHTML = ''; + return button; + } + + function ensureAccessibilityUi() { + let launcher = document.getElementById("accessibility-button"); + if (!launcher) { + launcher = createAccessibilityButton("accessibility-button", "accessibility-button"); + document.body.append(launcher); + } + + let mobileLauncher = document.getElementById("mobile-accessibility-button"); + let mobileActions = document.querySelector(".mobile-preference-actions"); + if (!mobileActions && elements.mobileTree) { + mobileActions = document.createElement("div"); + mobileActions.className = "mobile-preference-actions"; + elements.mobileTree.before(mobileActions); + } + if (mobileActions && elements.mobileThemeButton && elements.mobileThemeButton.parentElement !== mobileActions) { + mobileActions.append(elements.mobileThemeButton); + } + if (!mobileLauncher && mobileActions) { + mobileLauncher = createAccessibilityButton("mobile-accessibility-button", "mobile-accessibility-button"); + mobileActions.append(mobileLauncher); + } + + let view = document.getElementById("accessibility-view"); + if (!view) { + view = document.createElement("section"); + view.id = "accessibility-view"; + view.className = "accessibility-view"; + view.setAttribute("aria-labelledby", "accessibility-title"); + view.hidden = true; + view.innerHTML = ` + +

Accessibility

+
+
+ + 16 + +
+ +
+
+ + + + +
+
+

Header 1

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae justo sed arcu posuere feugiat.

+

Header 2

+

Praesent commodo, sapien at aliquet posuere, neque erat feugiat mauris, vitae luctus orci lorem sed nulla.

+
`; + (elements.pageTransition || elements.pageSurface).append(view); + } + + elements.accessibilityButton = launcher; + elements.mobileAccessibilityButton = mobileLauncher; + elements.accessibilityView = view; + elements.accessibilityBack = document.getElementById("accessibility-back"); + elements.accessibilitySizeValue = document.getElementById("accessibility-size-value"); + elements.accessibilitySizeDecrease = document.getElementById("accessibility-size-decrease"); + elements.accessibilitySizeIncrease = document.getElementById("accessibility-size-increase"); + elements.accessibilityReset = document.getElementById("accessibility-reset"); + elements.accessibilityFontOptions = document.getElementById("accessibility-font-options"); + } + + function applyAccessibilityPreferences({ save = true } = {}) { + accessibilitySize = Math.min(28, Math.max(12, Number(accessibilitySize) || 16)); + if (!accessibilityFonts.has(accessibilityFont)) accessibilityFont = "system-ui"; + document.documentElement.style.setProperty("--accessibility-font-size", `${accessibilitySize}px`); + document.documentElement.style.setProperty("--accessibility-font-family", accessibilityFont); + if (elements.accessibilitySizeValue) elements.accessibilitySizeValue.value = String(accessibilitySize); + if (elements.accessibilitySizeDecrease) elements.accessibilitySizeDecrease.disabled = accessibilitySize <= 12; + if (elements.accessibilitySizeIncrease) elements.accessibilitySizeIncrease.disabled = accessibilitySize >= 28; + elements.accessibilityFontOptions?.querySelectorAll("[data-accessibility-font]").forEach((button) => { + button.setAttribute("aria-pressed", String(button.dataset.accessibilityFont === accessibilityFont)); + }); + if (!save) return; + try { + localStorage.setItem("docslib-accessibility-size", String(accessibilitySize)); + localStorage.setItem("docslib-accessibility-font", accessibilityFont); + } catch (_) { /* preferences remain active for this page */ } + } + + function loadAccessibilityPreferences() { + try { + accessibilitySize = Number(localStorage.getItem("docslib-accessibility-size")) || 16; + accessibilityFont = localStorage.getItem("docslib-accessibility-font") || "system-ui"; + } catch (_) { + accessibilitySize = 16; + accessibilityFont = "system-ui"; + } + applyAccessibilityPreferences({ save: false }); + } + + function setAccessibilityOpen(open, { restoreFocus = true } = {}) { + if (!elements.accessibilityView) return; + document.body.classList.toggle("accessibility-open", open); + elements.accessibilityView.hidden = !open; + elements.accessibilityButton?.setAttribute("aria-expanded", String(open)); + elements.mobileAccessibilityButton?.setAttribute("aria-expanded", String(open)); + if (open) { + closeSearch({ clear: true }); + setMobileMode("content"); + elements.pageSurface.scrollTo({ top: 0, behavior: "instant" }); + requestAnimationFrame(() => elements.accessibilityBack?.focus()); + } else if (restoreFocus) { + (mobileLayout.matches ? elements.mobileMenuButton : elements.accessibilityButton)?.focus(); + } + } + function closeSearch({ clear = false } = {}) { elements.desktopSearchShell.classList.remove("open"); if (clear) { @@ -159,6 +295,7 @@ function navigate(number, replace = false) { const hash = `#/page/${encodeURIComponent(number)}`; if (mobileLayout.matches && mobileMode === "menu") skipNextPageAnimation = true; + if (document.body.classList.contains("accessibility-open")) setAccessibilityOpen(false, { restoreFocus: false }); closeSearch({ clear: true }); setMobileMode("content"); if (replace) { @@ -711,12 +848,15 @@ async function start() { applyTheme(localStorage.getItem("docslib-theme") || "dark"); + loadAccessibilityPreferences(); renderAll(); announceRendered(); syncManifest(); if (config.livePreview) setInterval(pollLive, 1200); } + ensureAccessibilityUi(); + elements.desktopSearchInput.addEventListener("input", handleDesktopSearch); elements.desktopSearchButton.addEventListener("click", () => elements.desktopSearchInput.focus()); elements.mobileSearchInput.addEventListener("input", handleMobileSearch); @@ -733,6 +873,28 @@ } catch (_) { /* older compiled HTML may not include the sidebar toggle */ } document.getElementById("theme-button").addEventListener("click", toggleTheme); if (elements.mobileThemeButton) elements.mobileThemeButton.addEventListener("click", toggleTheme); + elements.accessibilityButton?.addEventListener("click", () => setAccessibilityOpen(true)); + elements.mobileAccessibilityButton?.addEventListener("click", () => setAccessibilityOpen(true)); + elements.accessibilityBack?.addEventListener("click", () => setAccessibilityOpen(false)); + elements.accessibilitySizeDecrease?.addEventListener("click", () => { + accessibilitySize -= 2; + applyAccessibilityPreferences(); + }); + elements.accessibilitySizeIncrease?.addEventListener("click", () => { + accessibilitySize += 2; + applyAccessibilityPreferences(); + }); + elements.accessibilityReset?.addEventListener("click", () => { + accessibilitySize = 16; + accessibilityFont = "system-ui"; + applyAccessibilityPreferences(); + }); + elements.accessibilityFontOptions?.addEventListener("click", (event) => { + const button = event.target.closest("[data-accessibility-font]"); + if (!button || !elements.accessibilityFontOptions.contains(button)) return; + accessibilityFont = button.dataset.accessibilityFont; + applyAccessibilityPreferences(); + }); 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")); @@ -750,7 +912,9 @@ event.preventDefault(); (mobileLayout.matches ? elements.mobileSearchInput : elements.desktopSearchInput).focus(); } - if (event.key === "Escape") closeSearch({ clear: true }); + if (event.key === "Escape" && document.body.classList.contains("accessibility-open")) { + setAccessibilityOpen(false); + } else if (event.key === "Escape") closeSearch({ clear: true }); }); mobileLayout.addEventListener?.("change", () => { closeSearch({ clear: true }); diff --git a/tests/test_server.py b/tests/test_server.py index 06386df..0ed5e8e 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -28,6 +28,26 @@ def test_loader_renders_indexeddb_before_background_sync(): assert "manifest = remote" not in client +def test_accessibility_ui_is_generated_by_runtime_for_existing_html(): + templates = Path(docslib.__file__).with_name("templates") + index = (templates / "index.html").read_text(encoding="utf-8") + client = (templates / "client.js").read_text(encoding="utf-8") + css = (templates / "client.css").read_text(encoding="utf-8") + + assert 'id="accessibility-button"' not in index + assert 'id="mobile-accessibility-button"' not in index + assert 'id="accessibility-view"' not in index + assert "function ensureAccessibilityUi()" in client + assert 'createAccessibilityButton("accessibility-button"' in client + assert 'createAccessibilityButton("mobile-accessibility-button"' in client + assert "document.body.append(launcher)" in client + assert "elements.mobileTree.before(mobileActions)" in client + assert "(elements.pageTransition || elements.pageSurface).append(view)" in client + assert "--accessibility-font-family: system-ui" in css + assert "--accessibility-font-size: 16px" in css + assert "@font-face" not in css + + def test_all_endpoints_and_self_contained_build(tmp_path): assets = tmp_path / "assets" assets.mkdir() @@ -97,7 +117,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.5";' in html + assert 'globalThis.COMPILED_FROM_VERSION = "0.5.0";' in html assert ".content { --custom-test: yes; }" in html assert r'document.body.dataset.customTest = \"yes\";' in html assert 'id="docslib-loading"' in html @@ -156,7 +176,7 @@ def test_all_endpoints_and_self_contained_build(tmp_path): assert "--elevated: #d4d4d4" in html assert "--shadow: 0 24px 70px rgba(0, 0, 0, .4)" in html assert "elements.pageSurface.scrollTo" in html - assert "font-family: system-ui; font-size: 16px" in html + assert "font-family: var(--accessibility-font-family); font-size: 16px" in html assert ".glyph-button, .refresh-glyph { font-family: Arial" in html assert ".nav-arrow { display: block; width: 10px; height: 10px; flex: 0 0 10px; border: 0 solid currentColor; transform: rotate(45deg); }" in html assert ".nav-arrow-previous { border-bottom-width: 2px; border-left-width: 2px; }" in html @@ -197,11 +217,11 @@ def test_all_endpoints_and_self_contained_build(tmp_path): assert '".".repeat(dots)' in manifest["client"]["js"]["system"] assert "setTimeout(placeholderTick, 5000)" in html assert '.sidebar-header-buttons .header-link:not(.active):first-child' not in html - assert "border: 1px solid transparent" not in html + assert ".sidebar-header-buttons { 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: 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 ".mobile-theme-button, .mobile-accessibility-button { display: grid; width: 54px; height: 54px; margin: 0; 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 @@ -227,8 +247,18 @@ def test_all_endpoints_and_self_contained_build(tmp_path): assert "const transitionElement = elements.pageTransition || elements.content" in system_js assert "if (elements.mobileThemeIcon) elements.mobileThemeIcon.textContent = icon" in system_js assert "if (elements.mobileThemeButton) elements.mobileThemeButton.addEventListener" in system_js + assert "ensureAccessibilityUi();" in system_js + assert 'new Set(["system-ui", "sans-serif", "serif", "monospace"])' in system_js + assert 'localStorage.setItem("docslib-accessibility-size"' in system_js + assert 'localStorage.setItem("docslib-accessibility-font"' in system_js + assert 'document.documentElement.style.setProperty("--accessibility-font-size"' in system_js + assert 'document.documentElement.style.setProperty("--accessibility-font-family"' in system_js + assert 'body.accessibility-open .content, body.accessibility-open .page-navigation { display: none; }' in system_css + assert ".accessibility-button { position: fixed; z-index: 40;" in system_css + assert ".accessibility-preview { max-width: 920px; margin-top: 56px; font-size: var(--accessibility-font-size); }" in system_css + assert ".glyph-button, .refresh-glyph { font-family: Arial" in system_css assert 'document.body.classList.toggle("desktop-sidebar-collapsed", !open)' in system_js - assert "localStorage.setItem" not in system_js[system_js.index("function setDesktopSidebar"):system_js.index("function closeSearch")] + assert "localStorage.setItem" not in system_js[system_js.index("function setDesktopSidebar"):system_js.index("function createAccessibilityButton")] assert "animation: mobile-menu-enter .26s" in html assert "transform: translateX(-100%)" in html assert "const direction = currentIndex >= 0 && index < currentIndex ? -1 : 1" in system_js @@ -253,7 +283,7 @@ def test_all_endpoints_and_self_contained_build(tmp_path): def test_build_writes_static_bundle(tmp_path): - assert __version__ == "0.4.5" + assert __version__ == "0.5.0" @Page("1", "Static page") def static_page():