Compare commits
No commits in common. "f7811cd999aff878fb1099e4a0c57451e530138e" and "f189c37282faa5b0d4afa544e38b362da51a4150" have entirely different histories.
f7811cd999
...
f189c37282
6 changed files with 49 additions and 562 deletions
28
README.md
28
README.md
|
|
@ -15,13 +15,9 @@ 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 содержит полное название документации, поиск и переключатель темы, а слева постоянно видны header-кнопки и дерево страниц. Поисковый placeholder медленно печатает первые слова случайного названия, добавляет троеточие только в конце и сохраняет готовый текст 5 секунд; при вводе поле раскрывает ранжированные результаты вниз. Проверка manifest выполняется автоматически в фоне без отдельной кнопки обновления.
|
||||
|
||||
На десктопе компактный header содержит toggle боковой панели, полное название документации, поиск и переключатель темы. Sidebar по умолчанию открыт, его состояние не сохраняется между загрузками, а при сворачивании внутренняя раскладка остаётся неподвижной и обрезается границей панели без переноса текста. Поисковый placeholder медленно печатает первые слова случайного названия, добавляет троеточие только в конце и сохраняет готовый текст 5 секунд; при вводе поле раскрывает ранжированные результаты вниз. Проверка manifest выполняется автоматически в фоне без отдельной кнопки обновления.
|
||||
|
||||
Обновлённый runtime сохраняет совместимость со старыми скомпилированными HTML-оболочками: отсутствие desktop-toggle, мобильного переключателя темы или общего контейнера page-transition не останавливает запуск клиента.
|
||||
|
||||
На мобильных устройствах используется viewport `100dvh` и нижний dock. Кнопка `☰` заменяет содержимое страницы копией desktop-sidebar, которая выезжает с левой границы экрана; между header-кнопками и деревом доступен круглый переключатель темы. Выбор страницы в открытом мобильном меню закрывает его и показывает новый контент моментально, без page-transition. Ввод в нижний поиск заменяет содержимое результатами, расположенными снизу вверх по релевантности. При остальных переходах контент вместе с кнопками предыдущей и следующей страницы плавно смещается на 30 px по направлению навигации и меняет прозрачность.
|
||||
На мобильных устройствах используется viewport `100dvh` и нижний dock. Кнопка `☰` заменяет содержимое страницы копией desktop-sidebar. Ввод в нижний поиск заменяет содержимое результатами, расположенными снизу вверх по релевантности. Тема и обновление на мобильном скрыты.
|
||||
|
||||
## Установка из Git
|
||||
|
||||
|
|
@ -248,18 +244,6 @@ document.addEventListener("on_update_available", (event) => {
|
|||
});
|
||||
```
|
||||
|
||||
Непосредственно перед заменой содержимого при переходе между страницами отправляется всплывающее событие `on_page_change`. В `page` находится полная запись целевой страницы из manifest, а в `previousPage` — предыдущая страница или `null` при первом рендере:
|
||||
|
||||
```javascript
|
||||
document.addEventListener("on_page_change", (event) => {
|
||||
const { page, previousPage, number, title, index } = event.detail;
|
||||
console.log(`${previousPage?.number ?? "—"} → ${number}: ${title}`, {
|
||||
page,
|
||||
index,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Код из `custom_js` считается доверенным и выполняется в контексте страницы. Изменение системного или пользовательского CSS/JS меняет `revision`, поэтому обновлённый runtime сохраняется в IndexedDB и применяется после перезагрузки клиента.
|
||||
|
||||
Версия библиотеки, которой был скомпилирован физический `index.html`, доступна пользовательскому JavaScript в глобальной строковой переменной:
|
||||
|
|
@ -270,14 +254,6 @@ console.log(globalThis.COMPILED_FROM_VERSION); // например, "0.3.7"
|
|||
|
||||
Присваивание добавляется в bootstrap-скрипт самой HTML-ки. Автообновление manifest не меняет `COMPILED_FROM_VERSION`: значение обновится только после новой компиляции и замены `index.html`.
|
||||
|
||||
Для печати всей документации из `custom_js` или консоли доступна глобальная функция:
|
||||
|
||||
```javascript
|
||||
PrintFullDocumentation();
|
||||
```
|
||||
|
||||
Она сразу закрывает интерфейс тематическим loading-overlay, формирует единый документ из всех страниц в порядке manifest, подставляет и декодирует все изображения, ожидает готовности шрифтов и только затем открывает системный диалог печати. Каждая страница документации начинается с нового печатного листа. Печатная тема всегда использует белый фон, чёрный текст и компактные заголовки независимо от выбранной экранной темы.
|
||||
|
||||
## Live-preview
|
||||
|
||||
В live-режиме браузер опрашивает только `/live`. Сервер следит за файлами, в которых объявлены страницы, а также за системными и пользовательскими CSS/JS. При изменении он пересобирает manifest; клиент сохраняет его и перезагружается с новым runtime. В запускаемом файле вызов `run()` обязательно помещать под `if __name__ == "__main__"`, чтобы watcher не запускал второй сервер.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
"""Single source of truth for the SharedDocsLib package version."""
|
||||
|
||||
__version__ = "0.5.1"
|
||||
__version__ = "0.3.7"
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@
|
|||
--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;
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +38,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: var(--accessibility-font-family); font-size: 16px; line-height: 1.55; }
|
||||
body { margin: 0; color: var(--text); background: var(--shell); font-family: system-ui; font-size: 16px; line-height: 1.55; }
|
||||
button, input { font: inherit; }
|
||||
button, a { -webkit-tap-highlight-color: transparent; }
|
||||
button { color: inherit; }
|
||||
|
|
@ -48,19 +46,13 @@ button { color: inherit; }
|
|||
|
||||
.app-shell { --desktop-sidebar-width: clamp(270px, 25vw, 400px); display: flex; width: 100%; height: 100%; flex-direction: column; overflow: hidden; background: var(--shell); }
|
||||
.desktop-header { position: relative; z-index: 20; display: grid; min-height: 76px; flex: 0 0 76px; grid-template-columns: minmax(260px, 1fr) auto; align-items: center; gap: 18px; padding: 12px 20px; background: var(--shell); }
|
||||
.desktop-title-group { display: flex; min-width: 0; align-items: center; gap: 12px; }
|
||||
.desktop-title { overflow: hidden; margin: 0; font-size: 1.55rem; font-weight: 450; line-height: 1.1; letter-spacing: -.025em; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.desktop-sidebar-toggle { display: grid; width: 44px; height: 44px; flex: 0 0 44px; place-items: center; border-radius: 50%; color: var(--text); background: var(--control); font-size: 1.6rem; }
|
||||
.desktop-sidebar-toggle:hover { background: var(--active); }
|
||||
.header-link { display: block; width: 100%; padding: 6px 12px; border: 0; border-radius: 20px; color: var(--text); background: transparent; text-decoration: none; line-height: 1.2; transition: background .16s ease; }
|
||||
.header-link:hover { background: var(--control); }
|
||||
.header-link.active { color: var(--text); background: var(--active); }
|
||||
|
||||
.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; }
|
||||
.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); }
|
||||
.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); }
|
||||
.sidebar-header-buttons .header-link { color: var(--text); }
|
||||
.sidebar-header-buttons .header-link:hover { background: var(--control); }
|
||||
|
|
@ -107,19 +99,18 @@ body.desktop-sidebar-collapsed .desktop-sidebar { width: 0; flex-basis: 0; paddi
|
|||
@keyframes refresh-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.page-surface { min-height: 0; flex: 1; overflow-x: hidden; overflow-y: auto; overscroll-behavior-y: contain; border-radius: 36px 0 0; background: var(--surface); scroll-behavior: smooth; -webkit-overflow-scrolling: touch; }
|
||||
.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.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 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 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 var(--accessibility-font-family); }
|
||||
.content code { padding: .12em .35em; border-radius: 5px; background: var(--shell); font: .88em/1.5 ui-monospace, monospace; }
|
||||
.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); }
|
||||
|
|
@ -142,42 +133,7 @@ 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-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); }
|
||||
.print-loading-spinner { width: 48px; height: 48px; box-sizing: border-box; border: solid 4px currentColor; border-top-color: transparent; border-radius: 9999px; animation: refresh-spin .8s linear infinite; }
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.app-shell { display: block; }
|
||||
|
|
@ -186,9 +142,9 @@ body.accessibility-open .content, body.accessibility-open .page-navigation { dis
|
|||
.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: 2.45em; }
|
||||
.content h2 { font-size: 1.55em; }
|
||||
.content p { font-size: 1.05em; }
|
||||
.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; }
|
||||
.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; }
|
||||
|
|
@ -198,15 +154,11 @@ body.accessibility-open .content, body.accessibility-open .page-navigation { dis
|
|||
body.mobile-menu-open .mobile-menu-view { display: block; }
|
||||
body.mobile-search-open .mobile-search-view { display: flex; }
|
||||
.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: 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.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-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 .header-link { padding: 12px 18px; border-radius: 28px; font-size: 1.05rem; }
|
||||
.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.05em; }
|
||||
.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; }
|
||||
.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; }
|
||||
|
|
@ -220,16 +172,6 @@ body.accessibility-open .content, body.accessibility-open .page-navigation { dis
|
|||
.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 {
|
||||
from { opacity: 0; transform: translateX(-100%); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
|
@ -237,23 +179,9 @@ body.accessibility-open .content, body.accessibility-open .page-navigation { dis
|
|||
}
|
||||
|
||||
@media print {
|
||||
:root, :root[data-theme="dark"] { color-scheme: light; }
|
||||
html, body { height: auto; max-height: none; overflow: visible; color: #000 !important; background: #fff !important; }
|
||||
.app-shell, .workspace, .page-surface, .content, .full-print-document, .print-page { color: #000 !important; background: #fff !important; }
|
||||
.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; }
|
||||
html, body { height: auto; max-height: none; overflow: visible; }
|
||||
.app-shell { display: block; }
|
||||
.desktop-header, .desktop-sidebar, .mobile-dock, .page-navigation, .print-loading-overlay, .accessibility-button, .accessibility-view { display: none !important; }
|
||||
.desktop-header, .desktop-sidebar, .mobile-dock, .page-navigation { display: none; }
|
||||
.workspace, .page-surface { display: block; height: auto; overflow: visible; }
|
||||
.content { width: 100%; padding: 0; }
|
||||
body.docslib-print-all > .app-shell { display: none; }
|
||||
body.docslib-print-all > .full-print-document { display: block; }
|
||||
.print-page { break-after: page; page-break-after: always; }
|
||||
.print-page:last-child { break-after: auto; page-break-after: auto; }
|
||||
.print-page-heading { margin: 0 0 2rem; padding-bottom: 1rem; border-bottom: 1px solid #777; }
|
||||
.print-page-heading h1 { margin: 0; color: #000 !important; font-size: 1.35rem !important; line-height: 1.25; }
|
||||
.print-page-number { display: block; margin-bottom: .3rem; color: #555 !important; }
|
||||
.print-page-content h1 { font-size: 1.8rem !important; line-height: 1.25; }
|
||||
.print-page-content h2 { font-size: 1.4rem !important; line-height: 1.3; }
|
||||
.print-page-content h3 { font-size: 1.15rem !important; line-height: 1.35; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
||||
const elements = {
|
||||
desktopTitle: document.getElementById("desktop-site-title"),
|
||||
desktopSidebarToggle: document.getElementById("desktop-sidebar-toggle"),
|
||||
mobileTitle: document.getElementById("mobile-site-title"),
|
||||
desktopHeaderButtons: document.getElementById("desktop-header-buttons"),
|
||||
mobileHeaderButtons: document.getElementById("mobile-header-buttons"),
|
||||
|
|
@ -22,25 +21,13 @@
|
|||
mobileSearchButton: document.getElementById("mobile-search-button"),
|
||||
mobileSearchResults: document.getElementById("mobile-search-results"),
|
||||
content: document.getElementById("content"),
|
||||
pageTransition: document.getElementById("page-transition"),
|
||||
pageSurface: document.getElementById("page-surface"),
|
||||
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"),
|
||||
accessibilityButton: null,
|
||||
mobileAccessibilityButton: null,
|
||||
accessibilityView: null,
|
||||
accessibilityBack: null,
|
||||
accessibilitySizeValue: null,
|
||||
accessibilitySizeDecrease: null,
|
||||
accessibilitySizeIncrease: null,
|
||||
accessibilityReset: null,
|
||||
accessibilityFontOptions: null,
|
||||
};
|
||||
|
||||
let manifest = window.__DOCSLIB_MANIFEST__ || embedded;
|
||||
|
|
@ -57,13 +44,6 @@
|
|||
let lastPlaceholderPage = -1;
|
||||
let customScriptLoaded = false;
|
||||
let lastSyncedRevision = String(manifest.revision || "");
|
||||
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"));
|
||||
|
|
@ -155,134 +135,6 @@
|
|||
document.body.classList.toggle("mobile-search-open", mode === "search");
|
||||
}
|
||||
|
||||
function setDesktopSidebar(open) {
|
||||
document.body.classList.toggle("desktop-sidebar-collapsed", !open);
|
||||
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" xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor" aria-hidden="true"><path d="M423.5-743.5Q400-767 400-800t23.5-56.5Q447-880 480-880t56.5 23.5Q560-833 560-800t-23.5 56.5Q513-720 480-720t-56.5-23.5ZM360-80v-520H120v-80h720v80H600v520h-80v-240h-80v240h-80Z"></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) {
|
||||
|
|
@ -294,15 +146,11 @@
|
|||
|
||||
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 });
|
||||
if (replace) history.replaceState(null, "", hash);
|
||||
else if (location.hash !== hash) location.hash = hash;
|
||||
closeSearch({ clear: true });
|
||||
setMobileMode("content");
|
||||
if (replace) {
|
||||
history.replaceState(null, "", hash);
|
||||
renderPage(number);
|
||||
} else if (location.hash !== hash) location.hash = hash;
|
||||
else renderPage(number);
|
||||
renderPage(number);
|
||||
}
|
||||
|
||||
function buildTree(container) {
|
||||
|
|
@ -378,98 +226,15 @@
|
|||
return asset ? `data:${asset.mime};base64,${asset.data}` : "";
|
||||
}
|
||||
|
||||
function hydrateAssets(root = elements.content, { eager = false } = {}) {
|
||||
root.querySelectorAll("[data-docslib-asset]").forEach((element) => {
|
||||
function hydrateAssets() {
|
||||
elements.content.querySelectorAll("[data-docslib-asset]").forEach((element) => {
|
||||
const name = element.dataset.docslibAsset;
|
||||
const uri = assetDataUri(name);
|
||||
if (uri && element.tagName === "IMG") {
|
||||
if (eager) element.loading = "eager";
|
||||
element.src = uri;
|
||||
}
|
||||
if (uri && element.tagName === "IMG") element.src = uri;
|
||||
else if (!uri) element.setAttribute("title", name);
|
||||
});
|
||||
}
|
||||
|
||||
function removeFullPrintDocument() {
|
||||
document.body.classList.remove("docslib-print-all");
|
||||
document.getElementById("docslib-full-print")?.remove();
|
||||
}
|
||||
|
||||
function showPrintLoading() {
|
||||
document.getElementById("docslib-print-loading")?.remove();
|
||||
const overlay = document.createElement("div");
|
||||
overlay.id = "docslib-print-loading";
|
||||
overlay.className = "print-loading-overlay";
|
||||
overlay.setAttribute("aria-hidden", "true");
|
||||
const spinner = document.createElement("div");
|
||||
spinner.className = "print-loading-spinner";
|
||||
overlay.append(spinner);
|
||||
document.body.append(overlay);
|
||||
return overlay;
|
||||
}
|
||||
|
||||
async function waitForPrintAssets(root) {
|
||||
const images = [...root.querySelectorAll("img")];
|
||||
await Promise.all(images.map(async (image) => {
|
||||
if (!image.complete) {
|
||||
await new Promise((resolve) => {
|
||||
image.addEventListener("load", resolve, { once: true });
|
||||
image.addEventListener("error", resolve, { once: true });
|
||||
});
|
||||
}
|
||||
if (typeof image.decode === "function" && image.naturalWidth > 0) {
|
||||
await image.decode().catch(() => {});
|
||||
}
|
||||
}));
|
||||
if (document.fonts && document.fonts.ready) await document.fonts.ready;
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
}
|
||||
|
||||
async function PrintFullDocumentation() {
|
||||
const overlay = showPrintLoading();
|
||||
removeFullPrintDocument();
|
||||
try {
|
||||
const printable = document.createElement("div");
|
||||
printable.id = "docslib-full-print";
|
||||
printable.className = "full-print-document";
|
||||
|
||||
manifest.pages.forEach((page) => {
|
||||
const article = document.createElement("article");
|
||||
article.className = "content print-page";
|
||||
article.dataset.page = page.number;
|
||||
|
||||
const heading = document.createElement("header");
|
||||
heading.className = "print-page-heading";
|
||||
const number = document.createElement("span");
|
||||
number.className = "print-page-number";
|
||||
number.textContent = page.number;
|
||||
const title = document.createElement("h1");
|
||||
title.textContent = page.title;
|
||||
heading.append(number, title);
|
||||
|
||||
const body = document.createElement("div");
|
||||
body.className = "print-page-content";
|
||||
body.innerHTML = page.content || "";
|
||||
article.append(heading, body);
|
||||
hydrateAssets(article, { eager: true });
|
||||
printable.append(article);
|
||||
});
|
||||
|
||||
document.body.append(printable);
|
||||
await waitForPrintAssets(printable);
|
||||
document.body.classList.add("docslib-print-all");
|
||||
window.print();
|
||||
} catch (error) {
|
||||
removeFullPrintDocument();
|
||||
throw error;
|
||||
} finally {
|
||||
overlay.remove();
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.PrintFullDocumentation = PrintFullDocumentation;
|
||||
window.addEventListener("afterprint", removeFullPrintDocument);
|
||||
|
||||
function updateActiveNavigation(number) {
|
||||
document.querySelectorAll(".tree-link[data-page]").forEach((button) => {
|
||||
button.classList.toggle("active", button.dataset.page === number);
|
||||
|
|
@ -494,18 +259,15 @@
|
|||
button.onclick = () => navigate(page.number);
|
||||
}
|
||||
|
||||
function applyPage(page, index) {
|
||||
const previousPage = manifest.pages.find((candidate) => candidate.number === currentNumber) || null;
|
||||
document.dispatchEvent(new CustomEvent("on_page_change", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
page,
|
||||
previousPage,
|
||||
number: page.number,
|
||||
title: page.title,
|
||||
index,
|
||||
},
|
||||
}));
|
||||
function renderPage(number) {
|
||||
if (!manifest.pages.length) {
|
||||
elements.content.replaceChildren();
|
||||
elements.previous.disabled = elements.next.disabled = true;
|
||||
return;
|
||||
}
|
||||
let index = manifest.pages.findIndex((page) => page.number === number);
|
||||
if (index < 0) index = 0;
|
||||
const page = manifest.pages[index];
|
||||
currentNumber = page.number;
|
||||
if (routeNumber() !== page.number) history.replaceState(null, "", `#/page/${encodeURIComponent(page.number)}`);
|
||||
document.title = `${page.title} · ${manifest.title}`;
|
||||
|
|
@ -524,56 +286,6 @@
|
|||
elements.pageSurface.scrollTo({ top: 0, behavior: "instant" });
|
||||
}
|
||||
|
||||
async function renderPage(number) {
|
||||
if (!manifest.pages.length) {
|
||||
elements.content.replaceChildren();
|
||||
elements.previous.disabled = elements.next.disabled = true;
|
||||
return;
|
||||
}
|
||||
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 transitionElement = elements.pageTransition || elements.content;
|
||||
const animate = currentIndex >= 0 && !skipAnimation && !reducedMotion.matches && typeof transitionElement.animate === "function";
|
||||
const transitionId = ++pageTransitionId;
|
||||
|
||||
pageAnimation?.cancel();
|
||||
pageAnimation = null;
|
||||
if (animate) {
|
||||
const outgoing = transitionElement.animate(
|
||||
[
|
||||
{ opacity: 1, transform: "translateX(0)" },
|
||||
{ opacity: 0, transform: `translateX(${-30 * direction}px)` },
|
||||
],
|
||||
{ duration: 150, easing: "ease-in", fill: "forwards" },
|
||||
);
|
||||
pageAnimation = outgoing;
|
||||
await outgoing.finished.catch(() => {});
|
||||
if (transitionId !== pageTransitionId) return;
|
||||
outgoing.cancel();
|
||||
}
|
||||
|
||||
applyPage(page, index);
|
||||
if (!animate || transitionId !== pageTransitionId) return;
|
||||
const incoming = transitionElement.animate(
|
||||
[
|
||||
{ opacity: 0, transform: `translateX(${30 * direction}px)` },
|
||||
{ opacity: 1, transform: "translateX(0)" },
|
||||
],
|
||||
{ duration: 210, easing: "ease-out" },
|
||||
);
|
||||
pageAnimation = incoming;
|
||||
incoming.finished.catch(() => {}).finally(() => {
|
||||
if (pageAnimation === incoming) pageAnimation = null;
|
||||
});
|
||||
}
|
||||
|
||||
function plainText(html) {
|
||||
const node = document.createElement("div");
|
||||
node.innerHTML = html;
|
||||
|
|
@ -712,16 +424,10 @@
|
|||
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
const icon = theme === "dark" ? "☀" : "◐";
|
||||
elements.themeIcon.textContent = icon;
|
||||
if (elements.mobileThemeIcon) elements.mobileThemeIcon.textContent = icon;
|
||||
elements.themeIcon.textContent = theme === "dark" ? "☀" : "◐";
|
||||
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";
|
||||
|
|
@ -848,15 +554,12 @@
|
|||
|
||||
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);
|
||||
|
|
@ -866,34 +569,8 @@
|
|||
elements.mobileSearchResults.replaceChildren();
|
||||
setMobileMode(mobileMode === "menu" ? "content" : "menu");
|
||||
});
|
||||
try {
|
||||
elements.desktopSidebarToggle.addEventListener("click", () => {
|
||||
setDesktopSidebar(document.body.classList.contains("desktop-sidebar-collapsed"));
|
||||
});
|
||||
} 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();
|
||||
document.getElementById("theme-button").addEventListener("click", () => {
|
||||
applyTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark");
|
||||
});
|
||||
elements.refresh.addEventListener("pointerenter", () => elements.toolbar.classList.add("refresh-expanded"));
|
||||
elements.refresh.addEventListener("pointerleave", () => elements.toolbar.classList.remove("refresh-expanded"));
|
||||
|
|
@ -912,9 +589,7 @@
|
|||
event.preventDefault();
|
||||
(mobileLayout.matches ? elements.mobileSearchInput : elements.desktopSearchInput).focus();
|
||||
}
|
||||
if (event.key === "Escape" && document.body.classList.contains("accessibility-open")) {
|
||||
setAccessibilityOpen(false);
|
||||
} else if (event.key === "Escape") closeSearch({ clear: true });
|
||||
if (event.key === "Escape") closeSearch({ clear: true });
|
||||
});
|
||||
mobileLayout.addEventListener?.("change", () => {
|
||||
closeSearch({ clear: true });
|
||||
|
|
|
|||
|
|
@ -22,10 +22,7 @@
|
|||
<div id="docslib-loading" aria-hidden="true"><div class="docslib-loading-spinner"></div></div>
|
||||
<div id="app" class="app-shell">
|
||||
<header class="desktop-header">
|
||||
<div class="desktop-title-group">
|
||||
<button id="desktop-sidebar-toggle" class="desktop-sidebar-toggle glyph-button" aria-label="☰" aria-expanded="true">☰</button>
|
||||
<h2 id="desktop-site-title" class="desktop-title">{{DOCSLIB_TITLE}}</h2>
|
||||
</div>
|
||||
<h2 id="desktop-site-title" class="desktop-title">{{DOCSLIB_TITLE}}</h2>
|
||||
<div id="desktop-toolbar" class="desktop-toolbar">
|
||||
<div id="desktop-search-shell" class="desktop-search-shell">
|
||||
<div class="search-control">
|
||||
|
|
@ -51,19 +48,16 @@
|
|||
<section class="workspace">
|
||||
|
||||
<section id="page-surface" class="page-surface">
|
||||
<div id="page-transition" class="page-transition">
|
||||
<main id="content" class="content" tabindex="-1"></main>
|
||||
<footer class="page-navigation">
|
||||
<button id="previous-page" class="nav-card"></button>
|
||||
<button id="next-page" class="nav-card"></button>
|
||||
</footer>
|
||||
</div>
|
||||
<main id="content" class="content" tabindex="-1"></main>
|
||||
<footer class="page-navigation">
|
||||
<button id="previous-page" class="nav-card"></button>
|
||||
<button id="next-page" class="nav-card"></button>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
|
||||
|
|
|
|||
|
|
@ -28,30 +28,6 @@ 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 'viewBox="0 -960 960 960" fill="currentColor"' in client
|
||||
assert "M423.5-743.5Q400-767" in client
|
||||
assert '<circle cx="12" cy="4.5"' not 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 ".accessibility-glyph path { fill: none" not in css
|
||||
assert "@font-face" not in css
|
||||
|
||||
|
||||
def test_all_endpoints_and_self_contained_build(tmp_path):
|
||||
assets = tmp_path / "assets"
|
||||
assets.mkdir()
|
||||
|
|
@ -121,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.5.1";' in html
|
||||
assert 'globalThis.COMPILED_FROM_VERSION = "0.3.7";' in html
|
||||
assert ".content { --custom-test: yes; }" in html
|
||||
assert r'document.body.dataset.customTest = \"yes\";' in html
|
||||
assert 'id="docslib-loading"' in html
|
||||
|
|
@ -140,12 +116,6 @@ def test_all_endpoints_and_self_contained_build(tmp_path):
|
|||
assert "cached," in system_js
|
||||
assert "applied: false" in system_js
|
||||
assert 'new CustomEvent("on_update_available"' in system_js
|
||||
assert 'new CustomEvent("on_page_change"' in system_js
|
||||
assert "page," in system_js
|
||||
assert "previousPage," in system_js
|
||||
assert "number: page.number" in system_js
|
||||
assert "title: page.title" in system_js
|
||||
assert system_js.index('new CustomEvent("on_page_change"') < system_js.index("currentNumber = page.number")
|
||||
assert "currentRevision: lastSyncedRevision" in system_js
|
||||
assert 'config.versionUrl || siblingResourceUrl(config.manifestUrl, "version")' in system_js
|
||||
assert 'target.searchParams.set(' in system_js
|
||||
|
|
@ -180,55 +150,28 @@ 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: var(--accessibility-font-family); font-size: 16px" in html
|
||||
assert "font-family: system-ui; 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
|
||||
assert ".nav-arrow-next { border-top-width: 2px; border-right-width: 2px; }" in html
|
||||
assert 'arrow.textContent = direction' not in html
|
||||
assert "globalThis.PrintFullDocumentation = PrintFullDocumentation" in system_js
|
||||
assert 'printable.id = "docslib-full-print"' in system_js
|
||||
assert "manifest.pages.forEach((page) =>" in system_js
|
||||
assert 'window.addEventListener("afterprint", removeFullPrintDocument)' in system_js
|
||||
assert "async function PrintFullDocumentation()" in system_js
|
||||
assert "await waitForPrintAssets(printable)" in system_js
|
||||
assert 'if (eager) element.loading = "eager"' in system_js
|
||||
assert "hydrateAssets(article, { eager: true })" in system_js
|
||||
assert 'image.addEventListener("load", resolve, { once: true })' in system_js
|
||||
assert 'await image.decode().catch(() => {})' in system_js
|
||||
assert "await document.fonts.ready" in system_js
|
||||
system_css = manifest["client"]["css"]["system"]
|
||||
assert 'body.docslib-print-all > .full-print-document { display: block; }' in system_css
|
||||
assert 'color: #000 !important; background: #fff !important;' in system_css
|
||||
assert ".print-page { break-after: page; page-break-after: always; }" in system_css
|
||||
assert ".print-loading-overlay { position: fixed; inset: 0; z-index: 2147483647; display: grid; width: 100vw; height: 100dvh" in system_css
|
||||
assert ".print-page-heading h1 { margin: 0; color: #000 !important; font-size: 1.35rem !important" in system_css
|
||||
assert ".print-page-content h1 { font-size: 1.8rem !important" in system_css
|
||||
assert "--shell: #000000" in html
|
||||
assert 'id="desktop-search-input"' in html
|
||||
assert '<header class="desktop-header">' in html
|
||||
assert 'id="page-transition" class="page-transition"' in html
|
||||
assert 'id="desktop-sidebar-toggle"' in html
|
||||
assert 'aria-label="☰" aria-expanded="true">☰</button>' in html
|
||||
assert '<aside class="desktop-sidebar">' in html
|
||||
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"]
|
||||
assert "setTimeout(placeholderTick, 5000)" in html
|
||||
assert '.sidebar-header-buttons .header-link:not(.active):first-child' not in html
|
||||
assert ".sidebar-header-buttons { border: 1px solid transparent" not in html
|
||||
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: 18px; border: 1px solid var(--border); border-radius: 24px; background: var(--surface); }" 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
|
||||
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 ".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
|
||||
|
|
@ -242,36 +185,6 @@ def test_all_endpoints_and_self_contained_build(tmp_path):
|
|||
assert "background: transparent; font-size: 2.3rem" in html
|
||||
assert ".mobile-page-tree .tree-link { min-height: 46px; padding: 9px 14px; border-radius: 24px" in html
|
||||
assert ".mobile-menu-button { width: 72px; height: 72px; flex: 0 0 72px; border-radius: 50%; color: var(--text); background: var(--control); font-size: 1.6rem; }" in html
|
||||
assert ".desktop-sidebar-toggle { display: grid; width: 44px; height: 44px; flex: 0 0 44px" in html
|
||||
assert ".desktop-sidebar > * { width: calc(var(--desktop-sidebar-width) - 36px); min-width: calc(var(--desktop-sidebar-width) - 36px); }" in system_css
|
||||
assert "body.desktop-sidebar-collapsed .desktop-sidebar { width: 0; flex-basis: 0;" in html
|
||||
assert 'elements.desktopSidebarToggle.addEventListener("click"' in system_js
|
||||
assert 'try {\n elements.desktopSidebarToggle.addEventListener("click"' in system_js
|
||||
assert "older compiled HTML may not include the sidebar toggle" in system_js
|
||||
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 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
|
||||
assert 'const outgoing = transitionElement.animate(' in system_js
|
||||
assert 'const incoming = transitionElement.animate(' in system_js
|
||||
assert 'transform: `translateX(${-30 * direction}px)`' in system_js
|
||||
assert 'transform: `translateX(${30 * direction}px)`' in system_js
|
||||
assert 'opacity: 0' in system_js
|
||||
assert "!reducedMotion.matches" in system_js
|
||||
assert ".nav-card { display: flex; min-height: 48px; align-items: center; gap: 8px; padding: 7px 20px; border: 0; border-radius: 30px" in html
|
||||
assert "background: linear-gradient(0deg, var(--shell) 0%, transparent)" in html
|
||||
assert "<script src=" not in html
|
||||
|
|
@ -287,7 +200,7 @@ def test_all_endpoints_and_self_contained_build(tmp_path):
|
|||
|
||||
|
||||
def test_build_writes_static_bundle(tmp_path):
|
||||
assert __version__ == "0.5.1"
|
||||
assert __version__ == "0.3.7"
|
||||
|
||||
@Page("1", "Static page")
|
||||
def static_page():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue