Redesign responsive documentation client
This commit is contained in:
parent
b1e5963813
commit
054452bba7
5 changed files with 513 additions and 327 deletions
|
|
@ -13,7 +13,11 @@ SharedDocsLib — небольшой Python-фреймворк для докум
|
|||
- `index.zip`, содержащий только `index.html`;
|
||||
- live-preview с перекомпиляцией изменившегося Python-модуля.
|
||||
|
||||
UI не содержит языко-зависимых служебных подписей: поиск, тема и мобильные меню обозначены пиктограммами, а в статусе показываются ISO-дата manifest и короткий `revision`. Светлая палитра нейтральная, тёмная использует настоящий AMOLED-чёрный. На мобильных устройствах header-кнопки открываются в отдельном правом drawer высотой `100dvh`.
|
||||
UI не содержит языко-зависимых служебных подписей. Базовый шрифт — `system-ui 16px`, а UTF-8-пиктограммы используют Arial. AMOLED-тема включена по умолчанию и сохраняется локально после переключения.
|
||||
|
||||
На десктопе слева постоянно видны header-кнопки и дерево страниц. Поисковый placeholder печатает первые слова случайных названий, а при вводе поле раскрывает ранжированные результаты вниз. Кнопка `⟳` вращается во время проверки manifest; при наведении она заменяет поиск и тему, показывая ISO-дату и короткий `revision`, а при нажатии синхронизирует manifest и перезагружает страницу.
|
||||
|
||||
На мобильных устройствах используется viewport `100dvh` и нижний dock. Кнопка `☰` заменяет содержимое страницы копией desktop-sidebar. Ввод в нижний поиск заменяет содержимое результатами, расположенными снизу вверх по релевантности. Тема и обновление на мобильном скрыты.
|
||||
|
||||
## Установка из Git
|
||||
|
||||
|
|
|
|||
|
|
@ -1,141 +1,171 @@
|
|||
:root {
|
||||
--bg: #f5f5f5;
|
||||
--surface: #ffffff;
|
||||
--sidebar: #ededed;
|
||||
--text: #171717;
|
||||
--muted: #6f6f6f;
|
||||
--faint: #a3a3a3;
|
||||
--border: #d9d9d9;
|
||||
--hover: #e3e3e3;
|
||||
--accent: #171717;
|
||||
--accent-soft: rgba(0, 0, 0, .08);
|
||||
--code: #ebebeb;
|
||||
--shadow: 0 18px 60px rgba(0, 0, 0, .16);
|
||||
--shell: #ffffff;
|
||||
--surface: #f2f2f2;
|
||||
--elevated: #e5e5e5;
|
||||
--control: #dedede;
|
||||
--active: #cacaca;
|
||||
--text: #111111;
|
||||
--muted: #6d6d6d;
|
||||
--faint: #9a9a9a;
|
||||
--border: #c9c9c9;
|
||||
--shadow: 0 24px 70px rgba(0, 0, 0, .16);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--bg: #000000;
|
||||
--surface: #070707;
|
||||
--sidebar: #000000;
|
||||
--text: #f5f5f5;
|
||||
--muted: #a3a3a3;
|
||||
--faint: #666666;
|
||||
--border: #242424;
|
||||
--hover: #121212;
|
||||
--accent: #ffffff;
|
||||
--accent-soft: rgba(255, 255, 255, .10);
|
||||
--code: #090909;
|
||||
--shadow: 0 22px 70px rgba(0, 0, 0, .8);
|
||||
--shell: #000000;
|
||||
--surface: #111111;
|
||||
--elevated: #222222;
|
||||
--control: #242424;
|
||||
--active: #686868;
|
||||
--text: #f4f4f4;
|
||||
--muted: #a5a5a5;
|
||||
--faint: #747474;
|
||||
--border: #333333;
|
||||
--shadow: 0 28px 80px rgba(0, 0, 0, .72);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; }
|
||||
html, body { width: 100%; height: 100dvh; min-height: 100dvh; max-height: 100dvh; overflow: hidden; }
|
||||
html { background: var(--bg); }
|
||||
body { margin: 0; color: var(--text); background: var(--bg); font: 15px/1.65 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
||||
html { background: var(--shell); }
|
||||
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; }
|
||||
.app-shell { position: relative; width: 100%; height: 100%; overflow: hidden; }
|
||||
.sidebar { position: fixed; inset: 0 auto 0 0; z-index: 20; display: flex; width: 288px; height: 100%; flex-direction: column; border-right: 1px solid var(--border); background: var(--sidebar); }
|
||||
.brand-row { display: flex; align-items: center; min-height: 64px; padding: 13px 16px; }
|
||||
.brand-copy { display: flex; min-width: 0; flex: 1; flex-direction: column; line-height: 1.2; }
|
||||
.brand-copy strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; }
|
||||
.sidebar-actions { display: flex; gap: 7px; padding: 5px 12px 13px; border-bottom: 1px solid var(--border); }
|
||||
.action-button, .icon-button, .key-button { border: 0; color: var(--muted); background: transparent; cursor: pointer; }
|
||||
.action-button { display: grid; width: 36px; height: 36px; flex: 0 0 auto; place-items: center; padding: 0; border-radius: 7px; }
|
||||
.action-button:hover, .icon-button:hover, .key-button:hover { color: var(--text); background: var(--hover); }
|
||||
.icon-button { display: grid; width: 36px; height: 36px; flex: 0 0 auto; place-items: center; border-radius: 7px; }
|
||||
.page-tree { flex: 1; overflow: auto; padding: 12px 9px 24px; scrollbar-width: thin; }
|
||||
.tree-group { margin: 1px 0; }
|
||||
.tree-children { margin-left: 12px; padding-left: 8px; border-left: 1px solid var(--border); }
|
||||
.tree-link { display: flex; width: 100%; min-height: 33px; align-items: center; gap: 7px; padding: 5px 9px; border: 0; border-radius: 6px; color: var(--muted); background: transparent; cursor: pointer; text-align: left; }
|
||||
.tree-link:hover { color: var(--text); background: var(--hover); }
|
||||
.tree-link.active { color: var(--text); background: var(--accent-soft); font-weight: 600; }
|
||||
.tree-number { min-width: 26px; color: var(--faint); font: 10px ui-monospace, monospace; }
|
||||
.tree-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.update-status { display: flex; min-height: 48px; align-items: center; gap: 9px; padding: 0 17px; border-top: 1px solid var(--border); color: var(--muted); font-size: 11px; }
|
||||
.update-status i { width: 7px; height: 7px; border-radius: 50%; background: #55a96b; box-shadow: 0 0 0 3px rgba(85, 169, 107, .11); }
|
||||
.update-status.syncing i { background: #d8a441; animation: pulse 1s infinite; }
|
||||
.update-status.error i { background: #d75b5b; }
|
||||
@keyframes pulse { 50% { opacity: .35; } }
|
||||
.workspace { height: 100%; min-height: 0; margin-left: 288px; overflow-x: hidden; overflow-y: auto; overscroll-behavior-y: contain; scroll-behavior: smooth; -webkit-overflow-scrolling: touch; }
|
||||
.topbar { position: sticky; top: 0; z-index: 10; display: flex; min-height: 58px; align-items: center; gap: 10px; padding: 0 30px; border-bottom: 1px solid color-mix(in srgb, var(--border), transparent 18%); background: color-mix(in srgb, var(--bg), transparent 7%); backdrop-filter: blur(16px); }
|
||||
.breadcrumbs { min-width: 0; flex: 1; overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
||||
.breadcrumbs span + span::before { margin: 0 8px; color: var(--faint); content: "/"; }
|
||||
.header-actions { display: flex; max-width: 55%; align-items: center; }
|
||||
.header-buttons { display: flex; align-items: center; gap: 8px; overflow-x: auto; scrollbar-width: none; }
|
||||
.header-buttons::-webkit-scrollbar { display: none; }
|
||||
.header-link { padding: 5px 10px; border: 1px solid var(--border); border-radius: 7px; color: var(--muted); text-decoration: none; font-size: 12px; }
|
||||
.header-link:hover { color: var(--text); border-color: var(--faint); background: var(--surface); }
|
||||
.header-link.active { color: var(--accent); border-color: color-mix(in srgb, var(--accent), transparent 45%); background: var(--accent-soft); }
|
||||
.content { width: min(820px, calc(100% - 64px)); min-height: calc(100dvh - 180px); margin: 0 auto; padding: 62px 0 70px; outline: none; }
|
||||
.content > :first-child { margin-top: 0; }
|
||||
.content h1 { margin: 0 0 1.4rem; font-size: clamp(2rem, 5vw, 3.1rem); line-height: 1.1; letter-spacing: -.04em; }
|
||||
.content h2 { margin: 2.4rem 0 .75rem; padding-bottom: .35rem; border-bottom: 1px solid var(--border); font-size: 1.55rem; letter-spacing: -.02em; }
|
||||
.content h3 { margin: 1.8rem 0 .5rem; font-size: 1.15rem; }
|
||||
.content p { margin: .7rem 0 1rem; }
|
||||
.content a { color: var(--accent); text-underline-offset: 3px; }
|
||||
.content img { display: block; max-width: 100%; height: auto; margin: 1.35rem auto; border-radius: 8px; box-shadow: 0 7px 25px rgba(0, 0, 0, .12); }
|
||||
.content figure { margin: 1.5rem 0; }
|
||||
.content figure img { margin-bottom: .55rem; }
|
||||
.content figcaption { color: var(--muted); text-align: center; font-size: 12px; }
|
||||
.content pre { overflow: auto; margin: 1.2rem 0; padding: 17px 19px; border: 1px solid var(--border); border-radius: 9px; background: var(--code); line-height: 1.55; }
|
||||
.content code { padding: .12em .32em; border-radius: 4px; background: var(--code); font: .88em/1.5 "SFMono-Regular", Consolas, monospace; }
|
||||
.content pre code { padding: 0; background: transparent; }
|
||||
.content blockquote { margin: 1.3rem 0; padding: .3rem 1.2rem; border-left: 3px solid var(--accent); color: var(--muted); }
|
||||
.content blockquote footer { font-size: 12px; }
|
||||
.content hr { margin: 2.5rem 0; border: 0; border-top: 1px solid var(--border); }
|
||||
.content details { margin: 1rem 0; padding: .8rem 1rem; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); }
|
||||
.content summary { cursor: pointer; font-weight: 650; }
|
||||
.callout { margin: 1.25rem 0; padding: 13px 16px; border: 1px solid color-mix(in srgb, var(--accent), transparent 68%); border-left: 4px solid var(--accent); border-radius: 7px; background: var(--accent-soft); }
|
||||
.callout strong { display: block; margin-bottom: 3px; }
|
||||
.badge { display: inline-block; padding: 1px 7px; border-radius: 999px; color: var(--accent); background: var(--accent-soft); font-size: .8em; font-weight: 700; }
|
||||
.table-wrap { overflow-x: auto; margin: 1.2rem 0; }
|
||||
.content table { width: 100%; border-collapse: collapse; }
|
||||
.content th, .content td { padding: 9px 12px; border: 1px solid var(--border); text-align: left; }
|
||||
.content th { background: var(--sidebar); font-size: 12px; }
|
||||
.page-navigation { display: grid; width: min(820px, calc(100% - 64px)); grid-template-columns: 1fr 1fr; gap: 14px; margin: 0 auto; padding: 0 0 58px; }
|
||||
.nav-card { min-height: 66px; padding: 11px 15px; border: 1px solid var(--border); border-radius: 9px; color: var(--muted); background: var(--surface); cursor: pointer; text-align: left; }
|
||||
.nav-card:last-child { text-align: right; }
|
||||
.nav-card:hover:not(:disabled) { color: var(--text); border-color: var(--accent); transform: translateY(-1px); }
|
||||
.nav-card:disabled { visibility: hidden; }
|
||||
.nav-card small { display: block; margin-bottom: 2px; color: var(--faint); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; }
|
||||
.search-dialog { width: min(650px, calc(100% - 28px)); max-height: min(650px, calc(100dvh - 40px)); margin-top: 9dvh; padding: 0; border: 1px solid var(--border); border-radius: 12px; color: var(--text); background: var(--surface); box-shadow: var(--shadow); }
|
||||
.search-dialog::backdrop { background: rgba(0, 0, 0, .38); backdrop-filter: blur(2px); }
|
||||
.search-box { display: flex; align-items: center; gap: 9px; padding: 11px; border-bottom: 1px solid var(--border); }
|
||||
.search-box label { display: flex; flex: 1; align-items: center; gap: 9px; }
|
||||
.search-box input { width: 100%; border: 0; outline: 0; color: var(--text); background: transparent; }
|
||||
.key-button { padding: 4px 7px; border: 1px solid var(--border); border-radius: 5px; font-size: 10px; }
|
||||
.search-results { max-height: 480px; overflow: auto; padding: 7px; }
|
||||
.search-result { display: block; width: 100%; padding: 10px 12px; border: 0; border-radius: 7px; color: var(--text); background: transparent; cursor: pointer; text-align: left; }
|
||||
.search-result:hover, .search-result:focus { outline: 0; background: var(--hover); }
|
||||
.search-result strong { display: block; }
|
||||
.search-result span { display: block; overflow: hidden; margin-top: 3px; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
||||
.empty-state { padding: 48px 20px; color: var(--muted); text-align: center; }
|
||||
.sidebar-overlay, .actions-overlay, .mobile-only { display: none; }
|
||||
button { color: inherit; }
|
||||
.glyph-button, .refresh-glyph, .nav-arrow { font-family: Arial, sans-serif; }
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.mobile-only { display: grid; }
|
||||
.sidebar { transform: translateX(-102%); transition: transform .22s ease; box-shadow: var(--shadow); }
|
||||
body.sidebar-open .sidebar { transform: none; }
|
||||
.sidebar-overlay, .actions-overlay { position: fixed; inset: 0; z-index: 15; background: rgba(0, 0, 0, .58); }
|
||||
body.sidebar-open .sidebar-overlay { display: block; }
|
||||
.header-actions { position: fixed; inset: 0 0 0 auto; z-index: 30; display: flex; width: min(88vw, 420px); max-width: none; height: 100dvh; flex-direction: column; align-items: stretch; padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) 12px; border-left: 1px solid var(--border); background: var(--sidebar); box-shadow: var(--shadow); transform: translateX(102%); transition: transform .22s ease; }
|
||||
body.actions-open .header-actions { transform: none; }
|
||||
body.actions-open .actions-overlay { display: block; }
|
||||
.actions-close { align-self: flex-end; margin-bottom: 8px; }
|
||||
.header-buttons { flex: 1; flex-direction: column; align-items: stretch; gap: 6px; overflow-y: auto; }
|
||||
.header-link { width: 100%; padding: 11px 13px; border-color: transparent; font-size: 14px; }
|
||||
.workspace { margin-left: 0; }
|
||||
.topbar { padding: 0 14px; }
|
||||
.content, .page-navigation { width: min(100% - 34px, 820px); }
|
||||
.content { padding-top: 42px; }
|
||||
.app-shell { display: grid; width: 100%; height: 100%; grid-template-columns: clamp(280px, 20vw, 370px) minmax(0, 1fr); overflow: hidden; background: var(--shell); }
|
||||
.desktop-sidebar { display: flex; min-width: 0; height: 100%; flex-direction: column; overflow: hidden; padding: 40px 34px 26px; background: var(--shell); }
|
||||
.site-title { overflow: hidden; flex: 0 0 auto; margin: 0 0 30px; font-size: clamp(1.8rem, 2.35vw, 3rem); font-weight: 400; line-height: 1.08; letter-spacing: -.035em; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.sidebar-header-buttons { display: flex; flex: 0 0 auto; flex-direction: column; gap: 6px; margin-bottom: 42px; }
|
||||
.header-link { display: block; width: 100%; padding: 8px 15px; border: 1px solid transparent; border-radius: 24px; color: var(--text); background: transparent; text-decoration: none; line-height: 1.25; transition: background .16s ease, border-color .16s ease; }
|
||||
.header-link:hover { background: var(--control); }
|
||||
.header-link.active { color: var(--text); background: var(--active); }
|
||||
.header-link:not(.active):not(:hover) { border-color: var(--border); }
|
||||
.sidebar-header-buttons .header-link:not(.active):first-child { border-color: transparent; }
|
||||
|
||||
.page-tree { min-height: 0; overflow: auto; scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
|
||||
.desktop-sidebar > .page-tree { flex: 1; padding: 0 2px 34px; }
|
||||
.tree-group { margin: 2px 0; }
|
||||
.tree-children { margin-left: 19px; padding-left: 12px; border-left: 1px solid var(--muted); }
|
||||
.tree-link { display: flex; width: 100%; min-height: 36px; align-items: baseline; gap: 5px; padding: 6px 13px; border: 0; border-radius: 22px; color: var(--text); background: transparent; cursor: pointer; text-align: left; transition: background .15s ease; }
|
||||
.tree-link:hover { background: var(--control); }
|
||||
.tree-link.active { background: var(--active); }
|
||||
.tree-number { flex: 0 0 auto; color: var(--muted); }
|
||||
.tree-title { min-width: 0; overflow-wrap: anywhere; }
|
||||
|
||||
.workspace { display: flex; min-width: 0; height: 100%; min-height: 0; flex-direction: column; overflow: hidden; }
|
||||
.desktop-toolbar { position: relative; z-index: 20; display: flex; min-height: 128px; flex: 0 0 128px; align-items: center; justify-content: flex-end; gap: 18px; padding: 24px 28px; background: var(--shell); }
|
||||
.desktop-search-shell { position: relative; width: min(660px, 52vw); max-width: min(660px, 52vw); flex: 0 1 min(660px, 52vw); transition: max-width .24s ease, opacity .16s ease, transform .24s ease; }
|
||||
.search-control { display: flex; width: 100%; height: 64px; align-items: center; border-radius: 34px; background: var(--control); }
|
||||
.search-control input, .mobile-search-control input { min-width: 0; flex: 1; height: 100%; padding: 0 10px 0 28px; border: 0; outline: 0; color: var(--text); background: transparent; appearance: none; }
|
||||
.search-control input::placeholder, .mobile-search-control input::placeholder { color: var(--faint); opacity: 1; }
|
||||
.search-control input::-webkit-search-cancel-button, .mobile-search-control input::-webkit-search-cancel-button { display: none; }
|
||||
.glyph-button { display: grid; place-items: center; border: 0; cursor: pointer; }
|
||||
.search-glyph { width: 64px; height: 64px; flex: 0 0 64px; border-radius: 50%; color: var(--text); background: color-mix(in srgb, var(--control), var(--text) 8%); font-size: 2rem; }
|
||||
.desktop-search-results { position: absolute; inset: 74px 0 auto; display: flex; max-height: min(540px, calc(100dvh - 154px)); flex-direction: column; gap: 4px; overflow: auto; padding: 10px; border-radius: 28px; background: var(--elevated); box-shadow: var(--shadow); opacity: 0; pointer-events: none; transform: translateY(-10px) scale(.985); transform-origin: top; transition: opacity .16s ease, transform .18s ease; }
|
||||
.desktop-search-shell.open .desktop-search-results { opacity: 1; pointer-events: auto; transform: none; }
|
||||
.search-result { display: block; width: 100%; padding: 13px 18px; border: 0; border-radius: 22px; color: var(--text); background: transparent; cursor: pointer; text-align: left; }
|
||||
.search-result:hover, .search-result:focus-visible { outline: 0; background: var(--active); }
|
||||
.search-result strong { display: block; overflow: hidden; font-size: 1.08rem; font-weight: 550; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.search-result span { display: block; overflow: hidden; margin-top: 1px; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.theme-button { width: 64px; height: 64px; max-width: 64px; flex: 0 0 64px; border-radius: 50%; color: var(--text); background: var(--control); font-size: 1.8rem; transition: max-width .24s ease, opacity .16s ease, transform .24s ease; }
|
||||
.theme-button:hover { background: var(--active); }
|
||||
.refresh-button { display: flex; width: 64px; max-width: min(430px, 44vw); height: 64px; flex: 0 0 auto; align-items: center; justify-content: flex-start; gap: 14px; overflow: hidden; padding: 0; border: 0; border-radius: 34px; color: var(--text); background: var(--control); cursor: pointer; transition: width .26s ease, background .16s ease; }
|
||||
.refresh-button:hover { background: var(--active); }
|
||||
.refresh-glyph { display: grid; width: 64px; height: 64px; flex: 0 0 64px; place-items: center; font-size: 2rem; }
|
||||
.refresh-button.loading .refresh-glyph { animation: refresh-spin .8s linear infinite; }
|
||||
.manifest-identity { overflow: hidden; padding-right: 24px; color: var(--text); font-size: .82rem; white-space: nowrap; opacity: 0; transition: opacity .12s ease .03s; }
|
||||
.desktop-toolbar.refresh-expanded .desktop-search-shell { max-width: 0; opacity: 0; pointer-events: none; transform: translateX(18px); }
|
||||
.desktop-toolbar.refresh-expanded .theme-button { max-width: 0; opacity: 0; pointer-events: none; transform: scale(.72); }
|
||||
.desktop-toolbar.refresh-expanded .refresh-button { width: min(430px, 64vw); }
|
||||
.desktop-toolbar.refresh-expanded .manifest-identity { opacity: 1; }
|
||||
@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: 54px 0 0; background: var(--surface); scroll-behavior: smooth; -webkit-overflow-scrolling: touch; }
|
||||
.content { width: min(1120px, calc(100% - 120px)); min-height: calc(100dvh - 330px); margin: 0 auto; padding: 68px 0 64px; outline: none; }
|
||||
.content > :first-child { margin-top: 0; }
|
||||
.content h1 { margin: 0 0 2rem; font-size: clamp(2.35rem, 3.1vw, 4rem); 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 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); }
|
||||
.content details { margin: 1rem 0; padding: 1rem 1.1rem; border: 1px solid var(--border); border-radius: 16px; background: var(--shell); }
|
||||
.content summary { cursor: pointer; font-weight: 600; }
|
||||
.callout { margin: 1.3rem 0; padding: 14px 17px; border: 1px solid var(--border); border-left: 4px solid var(--text); border-radius: 14px; background: var(--shell); }
|
||||
.callout strong { display: block; margin-bottom: 3px; }
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 999px; color: var(--shell); background: var(--text); font-size: .8em; font-weight: 700; }
|
||||
.table-wrap { overflow-x: auto; margin: 1.3rem 0; }
|
||||
.content table { width: 100%; border-collapse: collapse; }
|
||||
.content th, .content td { padding: 10px 13px; border: 1px solid var(--border); text-align: left; }
|
||||
.content th { background: var(--control); }
|
||||
.page-navigation { display: flex; width: min(1120px, calc(100% - 120px)); gap: 14px; margin: 0 auto; padding: 0 0 64px; }
|
||||
.nav-card { display: flex; min-height: 56px; align-items: center; gap: 10px; padding: 9px 20px; border: 0; border-radius: 30px; color: var(--text); background: var(--control); cursor: pointer; }
|
||||
.nav-card:hover:not(:disabled) { background: var(--active); }
|
||||
.nav-card:disabled { display: none; }
|
||||
.nav-card small { color: inherit; font-size: 1.3rem; }
|
||||
#next-page { color: var(--shell); background: var(--text); }
|
||||
:root[data-theme="dark"] #next-page { color: #000; background: #eee; }
|
||||
|
||||
.mobile-view, .mobile-dock { display: none; }
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.app-shell { display: block; }
|
||||
.desktop-sidebar, .desktop-toolbar { display: none; }
|
||||
.workspace { width: 100%; height: 100%; }
|
||||
.page-surface { display: block; width: 100%; border-radius: 0; background: var(--shell); }
|
||||
.content { width: 100%; min-height: 0; 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; }
|
||||
.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; }
|
||||
.nav-card { max-width: calc(100vw - 56px); min-height: 54px; }
|
||||
.mobile-view { display: none; min-height: 0; flex: 1; overflow: auto; background: var(--shell); }
|
||||
body.mobile-menu-open .page-surface, body.mobile-search-open .page-surface { display: none; }
|
||||
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)); }
|
||||
.mobile-site-title { margin: 0 0 34px; font-size: clamp(2.4rem, 11vw, 4rem); font-weight: 400; line-height: 1.1; letter-spacing: -.035em; overflow-wrap: anywhere; }
|
||||
.mobile-header-buttons { display: flex; flex-direction: column; gap: 6px; margin-bottom: 38px; }
|
||||
.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; 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; }
|
||||
.mobile-search-results .search-result { flex: 0 0 auto; padding: 8px 0; border-radius: 0; }
|
||||
.mobile-search-results .search-result:hover, .mobile-search-results .search-result:focus-visible { background: transparent; }
|
||||
.mobile-search-results .search-result strong { font-size: 1.25rem; }
|
||||
.mobile-search-results .search-result span { font-size: 1rem; }
|
||||
.mobile-dock { position: fixed; inset: auto 0 0; z-index: 50; display: flex; align-items: center; gap: 18px; padding: 16px 28px max(22px, env(safe-area-inset-bottom)); background: linear-gradient(0deg, var(--shell) 74%, transparent); }
|
||||
.mobile-menu-button { width: 72px; height: 72px; flex: 0 0 72px; border-radius: 50%; color: var(--text); background: var(--control); font-size: 2rem; }
|
||||
body.mobile-menu-open .mobile-menu-button { background: var(--active); }
|
||||
.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: 22px; font-size: 1rem; }
|
||||
.mobile-search-control .search-glyph { width: 64px; height: 64px; flex-basis: 64px; background: transparent; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
|
||||
}
|
||||
|
||||
@media print {
|
||||
.sidebar, .topbar, .page-navigation { display: none; }
|
||||
.workspace { margin: 0; }
|
||||
html, body { height: auto; max-height: none; overflow: visible; }
|
||||
.app-shell { display: block; }
|
||||
.desktop-sidebar, .desktop-toolbar, .mobile-dock, .page-navigation { display: none; }
|
||||
.workspace, .page-surface { display: block; height: auto; overflow: visible; }
|
||||
.content { width: 100%; padding: 0; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,30 +3,47 @@
|
|||
|
||||
const embedded = JSON.parse(document.getElementById("docslib-manifest").textContent);
|
||||
const config = JSON.parse(document.getElementById("docslib-config").textContent);
|
||||
const mobileLayout = matchMedia("(max-width: 820px)");
|
||||
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
||||
const elements = {
|
||||
title: document.getElementById("site-title"),
|
||||
tree: document.getElementById("page-tree"),
|
||||
desktopTitle: document.getElementById("desktop-site-title"),
|
||||
mobileTitle: document.getElementById("mobile-site-title"),
|
||||
desktopHeaderButtons: document.getElementById("desktop-header-buttons"),
|
||||
mobileHeaderButtons: document.getElementById("mobile-header-buttons"),
|
||||
desktopTree: document.getElementById("desktop-page-tree"),
|
||||
mobileTree: document.getElementById("mobile-page-tree"),
|
||||
toolbar: document.getElementById("desktop-toolbar"),
|
||||
desktopSearchShell: document.getElementById("desktop-search-shell"),
|
||||
desktopSearchInput: document.getElementById("desktop-search-input"),
|
||||
desktopSearchButton: document.getElementById("desktop-search-button"),
|
||||
desktopSearchResults: document.getElementById("desktop-search-results"),
|
||||
mobileSearchInput: document.getElementById("mobile-search-input"),
|
||||
mobileSearchButton: document.getElementById("mobile-search-button"),
|
||||
mobileSearchResults: document.getElementById("mobile-search-results"),
|
||||
content: document.getElementById("content"),
|
||||
breadcrumbs: document.getElementById("breadcrumbs"),
|
||||
pageSurface: document.getElementById("page-surface"),
|
||||
previous: document.getElementById("previous-page"),
|
||||
next: document.getElementById("next-page"),
|
||||
status: document.getElementById("update-status"),
|
||||
searchDialog: document.getElementById("search-dialog"),
|
||||
searchInput: document.getElementById("search-input"),
|
||||
searchResults: document.getElementById("search-results"),
|
||||
themeIcon: document.getElementById("theme-icon"),
|
||||
headerButtons: document.getElementById("header-buttons"),
|
||||
headerActions: document.getElementById("header-actions"),
|
||||
headerActionsAnchor: document.getElementById("header-actions-anchor"),
|
||||
openActions: document.getElementById("open-actions"),
|
||||
workspace: document.querySelector(".workspace"),
|
||||
refresh: document.getElementById("refresh-button"),
|
||||
identity: document.getElementById("manifest-identity"),
|
||||
mobileMenuButton: document.getElementById("mobile-menu-button"),
|
||||
};
|
||||
|
||||
let manifest = embedded;
|
||||
let currentNumber = null;
|
||||
const mobileLayout = matchMedia("(max-width: 800px)");
|
||||
let syncing = false;
|
||||
let mobileMode = "content";
|
||||
let modeBeforeSearch = "content";
|
||||
let placeholderTimer = 0;
|
||||
let placeholderText = "";
|
||||
let placeholderTarget = "";
|
||||
let placeholderIndex = 0;
|
||||
let placeholderPhase = "typing";
|
||||
let lastPlaceholderPage = -1;
|
||||
|
||||
const dbPromise = new Promise((resolve, reject) => {
|
||||
if (!window.indexedDB) return reject(new Error("IndexedDB unavailable"));
|
||||
if (!window.indexedDB) return reject(new Error("IndexedDB"));
|
||||
const request = indexedDB.open("docslib-cache", 1);
|
||||
request.onupgradeneeded = () => request.result.createObjectStore("manifests");
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
|
|
@ -61,10 +78,14 @@
|
|||
return [updated, revision].filter(Boolean).join(" · ") || "—";
|
||||
}
|
||||
|
||||
function setStatus(state = "", detail = "") {
|
||||
elements.status.className = `update-status ${state}`.trim();
|
||||
elements.status.querySelector("span").textContent = manifestIdentity();
|
||||
elements.status.title = detail || String(manifest.revision || "");
|
||||
function updateIdentity() {
|
||||
elements.identity.textContent = manifestIdentity();
|
||||
elements.refresh.title = String(manifest.revision || "");
|
||||
}
|
||||
|
||||
function setSyncing(value) {
|
||||
syncing = value;
|
||||
elements.refresh.classList.toggle("loading", value);
|
||||
}
|
||||
|
||||
function routeNumber() {
|
||||
|
|
@ -72,28 +93,36 @@
|
|||
return match ? decodeURIComponent(match[1]) : null;
|
||||
}
|
||||
|
||||
function isPlainClick(event) {
|
||||
return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
|
||||
}
|
||||
|
||||
function setMobileMode(mode) {
|
||||
mobileMode = mode;
|
||||
document.body.classList.toggle("mobile-menu-open", mode === "menu");
|
||||
document.body.classList.toggle("mobile-search-open", mode === "search");
|
||||
}
|
||||
|
||||
function closeSearch({ clear = false } = {}) {
|
||||
elements.desktopSearchShell.classList.remove("open");
|
||||
if (clear) {
|
||||
elements.desktopSearchInput.value = "";
|
||||
elements.mobileSearchInput.value = "";
|
||||
}
|
||||
if (mobileMode === "search") setMobileMode(modeBeforeSearch);
|
||||
}
|
||||
|
||||
function navigate(number, replace = false) {
|
||||
const hash = `#/page/${encodeURIComponent(number)}`;
|
||||
if (replace) history.replaceState(null, "", hash);
|
||||
else if (location.hash !== hash) location.hash = hash;
|
||||
closeSearch({ clear: true });
|
||||
setMobileMode("content");
|
||||
renderPage(number);
|
||||
document.body.classList.remove("sidebar-open");
|
||||
document.body.classList.remove("actions-open");
|
||||
}
|
||||
|
||||
function placeHeaderActions() {
|
||||
if (mobileLayout.matches) {
|
||||
if (elements.headerActions.parentElement !== document.body) document.body.append(elements.headerActions);
|
||||
} else {
|
||||
if (elements.headerActions.previousElementSibling !== elements.headerActionsAnchor) {
|
||||
elements.headerActionsAnchor.after(elements.headerActions);
|
||||
}
|
||||
document.body.classList.remove("actions-open");
|
||||
}
|
||||
}
|
||||
|
||||
function makeTree() {
|
||||
elements.tree.replaceChildren();
|
||||
function buildTree(container) {
|
||||
container.replaceChildren();
|
||||
const root = { children: new Map(), page: null };
|
||||
manifest.pages.forEach((page) => {
|
||||
let node = root;
|
||||
|
|
@ -113,7 +142,7 @@
|
|||
button.dataset.page = child.page.number;
|
||||
const number = document.createElement("span");
|
||||
number.className = "tree-number";
|
||||
number.textContent = child.page.number;
|
||||
number.textContent = `${child.page.number}.`;
|
||||
const title = document.createElement("span");
|
||||
title.className = "tree-title";
|
||||
title.textContent = child.page.title;
|
||||
|
|
@ -130,7 +159,34 @@
|
|||
parent.append(group);
|
||||
});
|
||||
};
|
||||
appendNodes(elements.tree, root);
|
||||
appendNodes(container, root);
|
||||
}
|
||||
|
||||
function buildHeaderButtons(container) {
|
||||
container.replaceChildren();
|
||||
const buttons = (manifest.ui && manifest.ui.header_buttons) || [];
|
||||
buttons.forEach((button) => {
|
||||
if (!button.label || (!button.href && !button.page)) return;
|
||||
const link = document.createElement("a");
|
||||
link.className = "header-link";
|
||||
link.textContent = button.label;
|
||||
if (button.page) {
|
||||
link.href = `#/page/${encodeURIComponent(button.page)}`;
|
||||
link.dataset.page = button.page;
|
||||
link.addEventListener("click", (event) => {
|
||||
if (!isPlainClick(event)) return;
|
||||
event.preventDefault();
|
||||
navigate(button.page);
|
||||
});
|
||||
} else {
|
||||
link.href = button.href;
|
||||
if (button.target === "_blank") {
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener noreferrer";
|
||||
}
|
||||
}
|
||||
container.append(link);
|
||||
});
|
||||
}
|
||||
|
||||
function assetDataUri(name) {
|
||||
|
|
@ -143,13 +199,34 @@
|
|||
const name = element.dataset.docslibAsset;
|
||||
const uri = assetDataUri(name);
|
||||
if (uri && element.tagName === "IMG") element.src = uri;
|
||||
else if (!uri) {
|
||||
element.setAttribute("title", name);
|
||||
element.classList.add("missing-asset");
|
||||
}
|
||||
else if (!uri) element.setAttribute("title", name);
|
||||
});
|
||||
}
|
||||
|
||||
function updateActiveNavigation(number) {
|
||||
document.querySelectorAll(".tree-link[data-page]").forEach((button) => {
|
||||
button.classList.toggle("active", button.dataset.page === number);
|
||||
});
|
||||
document.querySelectorAll(".header-link[data-page]").forEach((button) => {
|
||||
const target = button.dataset.page;
|
||||
button.classList.toggle("active", number === target || number.startsWith(`${target}.`));
|
||||
});
|
||||
}
|
||||
|
||||
function setupNavButton(button, page, direction) {
|
||||
button.disabled = !page;
|
||||
button.replaceChildren();
|
||||
if (!page) return;
|
||||
const arrow = document.createElement("small");
|
||||
arrow.className = "nav-arrow";
|
||||
arrow.textContent = direction;
|
||||
const title = document.createElement("span");
|
||||
title.textContent = page.title;
|
||||
if (direction === "←") button.append(arrow, title);
|
||||
else button.append(title, arrow);
|
||||
button.onclick = () => navigate(page.number);
|
||||
}
|
||||
|
||||
function renderPage(number) {
|
||||
if (!manifest.pages.length) {
|
||||
elements.content.replaceChildren();
|
||||
|
|
@ -166,80 +243,15 @@
|
|||
hydrateAssets();
|
||||
elements.content.querySelectorAll("a[href^='#/page/']").forEach((link) => {
|
||||
link.addEventListener("click", (event) => {
|
||||
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
||||
if (!isPlainClick(event)) return;
|
||||
event.preventDefault();
|
||||
navigate(decodeURIComponent(link.hash.slice(7)));
|
||||
});
|
||||
});
|
||||
elements.tree.querySelectorAll(".tree-link").forEach((button) => button.classList.toggle("active", button.dataset.page === page.number));
|
||||
elements.headerButtons.querySelectorAll("[data-page]").forEach((button) => {
|
||||
const target = button.dataset.page;
|
||||
button.classList.toggle("active", page.number === target || page.number.startsWith(`${target}.`));
|
||||
});
|
||||
elements.breadcrumbs.replaceChildren();
|
||||
const numberParts = page.number.split(".");
|
||||
numberParts.forEach((_, partIndex) => {
|
||||
const candidate = manifest.pages.find((item) => item.number === numberParts.slice(0, partIndex + 1).join("."));
|
||||
if (candidate) {
|
||||
const crumb = document.createElement("span");
|
||||
crumb.textContent = candidate.title;
|
||||
elements.breadcrumbs.append(crumb);
|
||||
}
|
||||
});
|
||||
updateActiveNavigation(page.number);
|
||||
setupNavButton(elements.previous, manifest.pages[index - 1], "←");
|
||||
setupNavButton(elements.next, manifest.pages[index + 1], "→");
|
||||
elements.content.focus({ preventScroll: true });
|
||||
elements.workspace.scrollTo({ top: 0, behavior: "instant" });
|
||||
}
|
||||
|
||||
function setupNavButton(button, page, direction) {
|
||||
button.disabled = !page;
|
||||
button.replaceChildren();
|
||||
if (!page) return;
|
||||
const small = document.createElement("small");
|
||||
small.textContent = direction;
|
||||
const title = document.createElement("span");
|
||||
title.textContent = page.title;
|
||||
button.append(small, title);
|
||||
button.onclick = () => navigate(page.number);
|
||||
}
|
||||
|
||||
function renderHeaderButtons() {
|
||||
elements.headerButtons.replaceChildren();
|
||||
const buttons = (manifest.ui && manifest.ui.header_buttons) || [];
|
||||
buttons.forEach((button) => {
|
||||
if (!button.label || (!button.href && !button.page)) return;
|
||||
const link = document.createElement("a");
|
||||
link.className = "header-link";
|
||||
link.textContent = button.label;
|
||||
if (button.page) {
|
||||
link.href = `#/page/${encodeURIComponent(button.page)}`;
|
||||
link.dataset.page = button.page;
|
||||
link.addEventListener("click", (event) => {
|
||||
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
||||
event.preventDefault();
|
||||
navigate(button.page);
|
||||
});
|
||||
} else {
|
||||
link.href = button.href;
|
||||
}
|
||||
if (!button.page && button.target === "_blank") {
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener noreferrer";
|
||||
}
|
||||
link.addEventListener("click", () => document.body.classList.remove("actions-open"));
|
||||
elements.headerButtons.append(link);
|
||||
});
|
||||
elements.openActions.hidden = buttons.length === 0;
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
document.documentElement.lang = "und";
|
||||
elements.title.textContent = manifest.title;
|
||||
makeTree();
|
||||
renderHeaderButtons();
|
||||
renderPage(routeNumber() || currentNumber || (manifest.pages[0] && manifest.pages[0].number));
|
||||
setStatus();
|
||||
elements.pageSurface.scrollTo({ top: 0, behavior: "instant" });
|
||||
}
|
||||
|
||||
function plainText(html) {
|
||||
|
|
@ -248,34 +260,124 @@
|
|||
return (node.textContent || "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function search(query) {
|
||||
function rankedPages(query) {
|
||||
const needle = query.trim().toLocaleLowerCase();
|
||||
const matches = !needle ? manifest.pages : manifest.pages.filter((page) =>
|
||||
`${page.number} ${page.title} ${plainText(page.content)}`.toLocaleLowerCase().includes(needle)
|
||||
);
|
||||
elements.searchResults.replaceChildren();
|
||||
matches.slice(0, 30).forEach((page) => {
|
||||
if (!needle) return [];
|
||||
const tokens = needle.split(/\s+/).filter(Boolean);
|
||||
return manifest.pages
|
||||
.map((page, order) => {
|
||||
const title = page.title.toLocaleLowerCase();
|
||||
const number = page.number.toLocaleLowerCase();
|
||||
const text = plainText(page.content);
|
||||
const haystack = `${number} ${title} ${text.toLocaleLowerCase()}`;
|
||||
let score = 0;
|
||||
if (title === needle) score += 1000;
|
||||
if (title.startsWith(needle)) score += 500;
|
||||
if (title.includes(needle)) score += 300;
|
||||
if (number === needle) score += 600;
|
||||
if (haystack.includes(needle)) score += 120;
|
||||
tokens.forEach((token) => {
|
||||
if (title.startsWith(token)) score += 80;
|
||||
else if (title.includes(token)) score += 45;
|
||||
else if (haystack.includes(token)) score += 12;
|
||||
});
|
||||
return { page, text, score, order };
|
||||
})
|
||||
.filter((item) => item.score > 0)
|
||||
.sort((a, b) => b.score - a.score || a.order - b.order);
|
||||
}
|
||||
|
||||
function renderSearchResults(container, query, limit) {
|
||||
container.replaceChildren();
|
||||
rankedPages(query).slice(0, limit).forEach(({ page, text }) => {
|
||||
const button = document.createElement("button");
|
||||
button.className = "search-result";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = `${page.number} ${page.title}`;
|
||||
title.textContent = `${page.number}. ${page.title}`;
|
||||
const excerpt = document.createElement("span");
|
||||
excerpt.textContent = plainText(page.content).slice(0, 150) || "—";
|
||||
excerpt.textContent = text.slice(0, 110) || "—";
|
||||
button.append(title, excerpt);
|
||||
button.addEventListener("click", () => {
|
||||
elements.searchDialog.close();
|
||||
navigate(page.number);
|
||||
});
|
||||
elements.searchResults.append(button);
|
||||
button.addEventListener("click", () => navigate(page.number));
|
||||
container.append(button);
|
||||
});
|
||||
if (!matches.length) elements.searchResults.innerHTML = '<div class="empty-state">∅</div>';
|
||||
}
|
||||
|
||||
function openSearch() {
|
||||
if (!elements.searchDialog.open) elements.searchDialog.showModal();
|
||||
elements.searchInput.value = "";
|
||||
search("");
|
||||
setTimeout(() => elements.searchInput.focus(), 0);
|
||||
function handleDesktopSearch() {
|
||||
const query = elements.desktopSearchInput.value;
|
||||
renderSearchResults(elements.desktopSearchResults, query, 8);
|
||||
elements.desktopSearchShell.classList.toggle("open", Boolean(query.trim()));
|
||||
}
|
||||
|
||||
function handleMobileSearch() {
|
||||
const query = elements.mobileSearchInput.value;
|
||||
if (query.trim()) {
|
||||
if (mobileMode !== "search") modeBeforeSearch = mobileMode;
|
||||
setMobileMode("search");
|
||||
renderSearchResults(elements.mobileSearchResults, query, 8);
|
||||
} else if (mobileMode === "search") {
|
||||
setMobileMode(modeBeforeSearch);
|
||||
elements.mobileSearchResults.replaceChildren();
|
||||
}
|
||||
}
|
||||
|
||||
function choosePlaceholder() {
|
||||
if (!manifest.pages.length) return "";
|
||||
let index = Math.floor(Math.random() * manifest.pages.length);
|
||||
if (manifest.pages.length > 1 && index === lastPlaceholderPage) index = (index + 1) % manifest.pages.length;
|
||||
lastPlaceholderPage = index;
|
||||
const words = manifest.pages[index].title.trim().split(/\s+/).filter(Boolean);
|
||||
const count = Math.min(words.length, words.length > 2 && Math.random() > .5 ? 3 : 2);
|
||||
return words.slice(0, Math.max(1, count)).join(" ");
|
||||
}
|
||||
|
||||
function applyPlaceholder(value) {
|
||||
const display = value ? `${value}…` : "";
|
||||
elements.desktopSearchInput.placeholder = display;
|
||||
elements.mobileSearchInput.placeholder = display;
|
||||
}
|
||||
|
||||
function placeholderTick() {
|
||||
clearTimeout(placeholderTimer);
|
||||
if (elements.desktopSearchInput.value || elements.mobileSearchInput.value) {
|
||||
placeholderTimer = setTimeout(placeholderTick, 400);
|
||||
return;
|
||||
}
|
||||
if (!placeholderTarget) placeholderTarget = choosePlaceholder();
|
||||
if (!placeholderTarget) return;
|
||||
if (placeholderPhase === "typing") {
|
||||
placeholderIndex += 1;
|
||||
placeholderText = placeholderTarget.slice(0, placeholderIndex);
|
||||
applyPlaceholder(placeholderText);
|
||||
if (placeholderIndex >= placeholderTarget.length) {
|
||||
placeholderPhase = "waiting";
|
||||
placeholderTimer = setTimeout(placeholderTick, 1600);
|
||||
} else placeholderTimer = setTimeout(placeholderTick, 75 + Math.random() * 65);
|
||||
} else if (placeholderPhase === "waiting") {
|
||||
placeholderPhase = "erasing";
|
||||
placeholderTimer = setTimeout(placeholderTick, 180);
|
||||
} else {
|
||||
placeholderIndex -= 1;
|
||||
placeholderText = placeholderTarget.slice(0, Math.max(0, placeholderIndex));
|
||||
applyPlaceholder(placeholderText);
|
||||
if (placeholderIndex <= 0) {
|
||||
placeholderTarget = choosePlaceholder();
|
||||
placeholderPhase = "typing";
|
||||
placeholderTimer = setTimeout(placeholderTick, 420);
|
||||
} else placeholderTimer = setTimeout(placeholderTick, 36);
|
||||
}
|
||||
}
|
||||
|
||||
function startPlaceholder() {
|
||||
clearTimeout(placeholderTimer);
|
||||
placeholderText = "";
|
||||
placeholderTarget = choosePlaceholder();
|
||||
placeholderIndex = 0;
|
||||
placeholderPhase = "typing";
|
||||
if (reducedMotion.matches) {
|
||||
applyPlaceholder(placeholderTarget);
|
||||
return;
|
||||
}
|
||||
placeholderTimer = setTimeout(placeholderTick, 350);
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
|
|
@ -284,29 +386,47 @@
|
|||
localStorage.setItem("docslib-theme", theme);
|
||||
}
|
||||
|
||||
async function syncManifest({ reload = true } = {}) {
|
||||
setStatus("syncing");
|
||||
function renderAll() {
|
||||
document.documentElement.lang = "und";
|
||||
elements.desktopTitle.textContent = manifest.title;
|
||||
elements.mobileTitle.textContent = manifest.title;
|
||||
buildHeaderButtons(elements.desktopHeaderButtons);
|
||||
buildHeaderButtons(elements.mobileHeaderButtons);
|
||||
buildTree(elements.desktopTree);
|
||||
buildTree(elements.mobileTree);
|
||||
renderPage(routeNumber() || currentNumber || (manifest.pages[0] && manifest.pages[0].number));
|
||||
updateIdentity();
|
||||
startPlaceholder();
|
||||
}
|
||||
|
||||
async function syncManifest({ reloadOnChange = false } = {}) {
|
||||
if (syncing) return false;
|
||||
setSyncing(true);
|
||||
let changed = false;
|
||||
try {
|
||||
const response = await fetch(config.manifestUrl, { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
if (!response.ok) throw new Error(String(response.status));
|
||||
const remote = await response.json();
|
||||
if (!remote.schema_version || !Array.isArray(remote.pages)) throw new Error("Invalid manifest");
|
||||
if (remote.revision !== manifest.revision || remote.updated_at !== manifest.updated_at) {
|
||||
if (!remote.schema_version || !Array.isArray(remote.pages)) throw new Error("manifest");
|
||||
changed = remote.revision !== manifest.revision || remote.updated_at !== manifest.updated_at;
|
||||
if (changed) {
|
||||
await cachePut(remote).catch(() => {});
|
||||
manifest = remote;
|
||||
if (reload) {
|
||||
sessionStorage.setItem("docslib-updated", remote.revision || remote.updated_at);
|
||||
if (reloadOnChange) {
|
||||
location.reload();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
renderAll();
|
||||
setStatus();
|
||||
} else {
|
||||
setStatus();
|
||||
}
|
||||
elements.refresh.classList.remove("error");
|
||||
updateIdentity();
|
||||
return changed;
|
||||
} catch (error) {
|
||||
setStatus("error", String(error));
|
||||
console.warn("SharedDocsLib update check failed:", error);
|
||||
elements.refresh.classList.add("error");
|
||||
elements.refresh.title = String(error);
|
||||
return false;
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -315,13 +435,12 @@
|
|||
const response = await fetch(config.liveUrl, { cache: "no-store" });
|
||||
if (!response.ok) return;
|
||||
const live = await response.json();
|
||||
if (live.error) setStatus("error", live.error);
|
||||
else if (live.revision && live.revision !== manifest.revision) await syncManifest({ reload: false });
|
||||
} catch (_) { /* The normal manifest remains usable when the dev server stops. */ }
|
||||
if (live.revision && live.revision !== manifest.revision) await syncManifest();
|
||||
} catch (_) { /* local preview may stop */ }
|
||||
}
|
||||
|
||||
async function start() {
|
||||
applyTheme(localStorage.getItem("docslib-theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"));
|
||||
applyTheme(localStorage.getItem("docslib-theme") || "dark");
|
||||
try {
|
||||
const cached = await cacheGet();
|
||||
if (
|
||||
|
|
@ -330,32 +449,48 @@
|
|||
String(cached.updated_at || "") >= String(embedded.updated_at || "")
|
||||
) manifest = cached;
|
||||
else await cachePut(embedded);
|
||||
} catch (error) {
|
||||
console.warn("SharedDocsLib cache unavailable:", error);
|
||||
}
|
||||
} catch (_) { /* embedded manifest remains available */ }
|
||||
renderAll();
|
||||
if (sessionStorage.getItem("docslib-updated")) sessionStorage.removeItem("docslib-updated");
|
||||
syncManifest();
|
||||
await syncManifest({ reloadOnChange: true });
|
||||
if (config.livePreview) setInterval(pollLive, 1200);
|
||||
}
|
||||
|
||||
document.getElementById("search-button").addEventListener("click", openSearch);
|
||||
document.getElementById("close-search").addEventListener("click", () => elements.searchDialog.close());
|
||||
elements.searchInput.addEventListener("input", () => search(elements.searchInput.value));
|
||||
document.getElementById("theme-button").addEventListener("click", () => applyTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark"));
|
||||
document.getElementById("open-sidebar").addEventListener("click", () => document.body.classList.add("sidebar-open"));
|
||||
document.getElementById("close-sidebar").addEventListener("click", () => document.body.classList.remove("sidebar-open"));
|
||||
document.getElementById("sidebar-overlay").addEventListener("click", () => document.body.classList.remove("sidebar-open"));
|
||||
document.getElementById("open-actions").addEventListener("click", () => document.body.classList.add("actions-open"));
|
||||
document.getElementById("close-actions").addEventListener("click", () => document.body.classList.remove("actions-open"));
|
||||
document.getElementById("actions-overlay").addEventListener("click", () => document.body.classList.remove("actions-open"));
|
||||
if (mobileLayout.addEventListener) mobileLayout.addEventListener("change", placeHeaderActions);
|
||||
else mobileLayout.addListener(placeHeaderActions);
|
||||
elements.desktopSearchInput.addEventListener("input", handleDesktopSearch);
|
||||
elements.desktopSearchButton.addEventListener("click", () => elements.desktopSearchInput.focus());
|
||||
elements.mobileSearchInput.addEventListener("input", handleMobileSearch);
|
||||
elements.mobileSearchButton.addEventListener("click", () => elements.mobileSearchInput.focus());
|
||||
elements.mobileMenuButton.addEventListener("click", () => {
|
||||
elements.mobileSearchInput.value = "";
|
||||
elements.mobileSearchResults.replaceChildren();
|
||||
setMobileMode(mobileMode === "menu" ? "content" : "menu");
|
||||
});
|
||||
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"));
|
||||
elements.refresh.addEventListener("focus", () => elements.toolbar.classList.add("refresh-expanded"));
|
||||
elements.refresh.addEventListener("blur", () => elements.toolbar.classList.remove("refresh-expanded"));
|
||||
elements.refresh.addEventListener("click", async () => {
|
||||
if (syncing) return;
|
||||
await syncManifest();
|
||||
location.reload();
|
||||
});
|
||||
document.addEventListener("pointerdown", (event) => {
|
||||
if (!elements.desktopSearchShell.contains(event.target)) elements.desktopSearchShell.classList.remove("open");
|
||||
});
|
||||
window.addEventListener("hashchange", () => renderPage(routeNumber()));
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") { event.preventDefault(); openSearch(); }
|
||||
if (event.key === "Escape" && elements.searchDialog.open) elements.searchDialog.close();
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
|
||||
event.preventDefault();
|
||||
(mobileLayout.matches ? elements.mobileSearchInput : elements.desktopSearchInput).focus();
|
||||
}
|
||||
if (event.key === "Escape") closeSearch({ clear: true });
|
||||
});
|
||||
placeHeaderActions();
|
||||
mobileLayout.addEventListener?.("change", () => {
|
||||
closeSearch({ clear: true });
|
||||
setMobileMode("content");
|
||||
});
|
||||
|
||||
start();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="und">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<title>{{DOCSLIB_TITLE}}</title>
|
||||
<style>
|
||||
|
|
@ -12,45 +12,56 @@
|
|||
</head>
|
||||
<body>
|
||||
<div id="app" class="app-shell">
|
||||
<aside id="sidebar" class="sidebar">
|
||||
<div class="brand-row">
|
||||
<div class="brand-copy"><strong id="site-title">{{DOCSLIB_TITLE}}</strong></div>
|
||||
<button id="close-sidebar" class="icon-button mobile-only" aria-label="×">×</button>
|
||||
</div>
|
||||
<div class="sidebar-actions">
|
||||
<button id="search-button" class="action-button" aria-label="⌕"><span>⌕</span></button>
|
||||
<button id="theme-button" class="icon-button" aria-label="◐"><span id="theme-icon">◐</span></button>
|
||||
</div>
|
||||
<nav id="page-tree" class="page-tree" aria-label="≡"></nav>
|
||||
<div id="update-status" class="update-status"><i></i><span>—</span></div>
|
||||
<aside class="desktop-sidebar">
|
||||
<h1 id="desktop-site-title" class="site-title">{{DOCSLIB_TITLE}}</h1>
|
||||
<nav id="desktop-header-buttons" class="sidebar-header-buttons" aria-label="⋯"></nav>
|
||||
<nav id="desktop-page-tree" class="page-tree" aria-label="≡"></nav>
|
||||
</aside>
|
||||
<div id="sidebar-overlay" class="sidebar-overlay"></div>
|
||||
<div id="actions-overlay" class="actions-overlay"></div>
|
||||
|
||||
<section class="workspace">
|
||||
<header class="topbar">
|
||||
<button id="open-sidebar" class="icon-button mobile-only" aria-label="☰">☰</button>
|
||||
<div id="breadcrumbs" class="breadcrumbs"></div>
|
||||
<button id="open-actions" class="icon-button mobile-only" aria-label="⋮">⋮</button>
|
||||
<span id="header-actions-anchor" hidden></span>
|
||||
<aside id="header-actions" class="header-actions">
|
||||
<button id="close-actions" class="icon-button mobile-only actions-close" aria-label="×">×</button>
|
||||
<div id="header-buttons" class="header-buttons"></div>
|
||||
</aside>
|
||||
<header id="desktop-toolbar" class="desktop-toolbar">
|
||||
<div id="desktop-search-shell" class="desktop-search-shell">
|
||||
<div class="search-control">
|
||||
<input id="desktop-search-input" type="search" aria-label="⌕" autocomplete="off" spellcheck="false">
|
||||
<button id="desktop-search-button" class="glyph-button search-glyph" aria-label="⌕">⌕</button>
|
||||
</div>
|
||||
<div id="desktop-search-results" class="desktop-search-results"></div>
|
||||
</div>
|
||||
<button id="theme-button" class="glyph-button theme-button" aria-label="◐"><span id="theme-icon">☀</span></button>
|
||||
<button id="refresh-button" class="refresh-button" aria-label="⟳">
|
||||
<span class="refresh-glyph">⟳</span>
|
||||
<span id="manifest-identity" class="manifest-identity">—</span>
|
||||
</button>
|
||||
</header>
|
||||
<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 id="page-surface" class="page-surface">
|
||||
<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>
|
||||
<nav id="mobile-page-tree" class="page-tree mobile-page-tree" aria-label="≡"></nav>
|
||||
</section>
|
||||
|
||||
<section id="mobile-search-view" class="mobile-view mobile-search-view">
|
||||
<div id="mobile-search-results" class="mobile-search-results"></div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
<dialog id="search-dialog" class="search-dialog">
|
||||
<div class="search-box">
|
||||
<label><span>⌕</span><input id="search-input" type="search" aria-label="⌕" autocomplete="off"></label>
|
||||
<button id="close-search" class="key-button" aria-label="×">×</button>
|
||||
|
||||
<div class="mobile-dock">
|
||||
<button id="mobile-menu-button" class="mobile-menu-button glyph-button" aria-label="☰">☰</button>
|
||||
<div class="mobile-search-control">
|
||||
<input id="mobile-search-input" type="search" aria-label="⌕" autocomplete="off" spellcheck="false">
|
||||
<button id="mobile-search-button" class="glyph-button search-glyph" aria-label="⌕">⌕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="search-results" class="search-results"></div>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
<script id="docslib-manifest" type="application/json">{{DOCSLIB_MANIFEST}}</script>
|
||||
<script id="docslib-config" type="application/json">{{DOCSLIB_CONFIG}}</script>
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -74,12 +74,18 @@ def test_all_endpoints_and_self_contained_build(tmp_path):
|
|||
assert "100vh" not in html
|
||||
assert "html, body { width: 100%; height: 100dvh" in html
|
||||
assert "overflow-y: auto" in html
|
||||
assert "document.body.append(elements.headerActions)" in html
|
||||
assert "elements.workspace.scrollTo" in html
|
||||
assert "--bg: #000000" in html
|
||||
assert "--accent: #ffffff" in html
|
||||
assert 'id="header-actions"' in html
|
||||
assert 'id="actions-overlay"' in html
|
||||
assert "elements.pageSurface.scrollTo" in html
|
||||
assert "font-family: system-ui; font-size: 16px" in html
|
||||
assert ".glyph-button, .refresh-glyph, .nav-arrow { font-family: Arial" in html
|
||||
assert "--shell: #000000" in html
|
||||
assert 'id="desktop-search-input"' in html
|
||||
assert 'id="mobile-search-input"' in html
|
||||
assert 'id="mobile-menu-view"' in html
|
||||
assert 'id="mobile-search-view"' in html
|
||||
assert "placeholderTick" in html
|
||||
assert "refresh-expanded" in html
|
||||
assert "mobile-search-open" in html
|
||||
assert "flex-direction: column-reverse" in html
|
||||
assert "<script src=" not in html
|
||||
assert "<link rel=" not in html
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue