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.
+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 = ` + +
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae justo sed arcu posuere feugiat.
+Praesent commodo, sapien at aliquet posuere, neque erat feugiat mauris, vitae luctus orci lorem sed nulla.
+