Add runtime-generated accessibility controls
This commit is contained in:
parent
dc482cde94
commit
845515d400
5 changed files with 259 additions and 21 deletions
|
|
@ -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 не останавливает запуск клиента.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
"""Single source of truth for the SharedDocsLib package version."""
|
||||
|
||||
__version__ = "0.4.5"
|
||||
__version__ = "0.5.0"
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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 = '<svg class="accessibility-glyph" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="4.5" r="2.1"></circle><path d="M5 8.2h14M12 7.2v12.3M8.1 21l3.9-7.4 3.9 7.4"></path></svg>';
|
||||
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 = `
|
||||
<button id="accessibility-back" class="accessibility-back" type="button" aria-label="Back"><span class="accessibility-back-arrow" aria-hidden="true"></span></button>
|
||||
<h1 id="accessibility-title" class="accessibility-title">Accessibility</h1>
|
||||
<div class="accessibility-size-row">
|
||||
<div class="accessibility-size-control" role="group" aria-label="Text size">
|
||||
<button id="accessibility-size-decrease" class="accessibility-step glyph-button" type="button" aria-label="Decrease text size">−</button>
|
||||
<output id="accessibility-size-value" aria-live="polite">16</output>
|
||||
<button id="accessibility-size-increase" class="accessibility-step glyph-button" type="button" aria-label="Increase text size">+</button>
|
||||
</div>
|
||||
<button id="accessibility-reset" class="accessibility-reset glyph-button" type="button" aria-label="Reset accessibility settings">↻</button>
|
||||
</div>
|
||||
<div id="accessibility-font-options" class="accessibility-font-options" role="group" aria-label="Font family">
|
||||
<button type="button" data-accessibility-font="system-ui" aria-pressed="true">system-ui</button>
|
||||
<button type="button" data-accessibility-font="sans-serif" aria-pressed="false">sans-serif</button>
|
||||
<button type="button" data-accessibility-font="serif" aria-pressed="false">serif</button>
|
||||
<button type="button" data-accessibility-font="monospace" aria-pressed="false">monospace</button>
|
||||
</div>
|
||||
<article class="accessibility-preview" aria-label="Preview">
|
||||
<h1>Header 1</h1>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer vitae justo sed arcu posuere feugiat.</p>
|
||||
<h2>Header 2</h2>
|
||||
<p>Praesent commodo, sapien at aliquet posuere, neque erat feugiat mauris, vitae luctus orci lorem sed nulla.</p>
|
||||
</article>`;
|
||||
(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 });
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue