docs: Add table of contents navigation (#15212)

To ease navigating on pages that are long and having a birds-eye view of
all the available content.

This is done client-side and done via the files initially generated by
the `mdbook-pagetoc` plugin ([crates.io link
here](https://crates.io/crates/mdbook-pagetoc)).

<img width="600" alt="Screenshot 2024-07-25 at 13 34 08"
src="https://github.com/user-attachments/assets/a78c69e5-8cc4-4414-9d9c-27a4ceb27620">

---

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
Danilo Leal 2024-07-27 11:34:14 -03:00 committed by GitHub
parent acea6f9c0f
commit af24967195
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 172 additions and 7 deletions

View file

@ -20,3 +20,9 @@ Putting binary assets such as images in the Git repository will bloat the reposi
- We have a Cloudflare router called `docs-proxy` that intercepts requests to `zed.dev/docs` and forwards them to the "docs" Cloudflare Pages project.
- CI uploads a new version to the Pages project from `.github/workflows/deploy_docs.yml` on every push to `main`.
### Table of Contents
The table of contents files (`theme/page-toc.js` and `theme/page-doc.css`) were initially generated by [`mdbook-pagetoc`](https://crates.io/crates/mdbook-pagetoc).
Since all these preprocessor does is generate the static assets, we don't need to keep it around once they have been generated.

View file

@ -9,6 +9,8 @@ site-url = "/docs/"
[output.html]
no-section-label = true
preferred-dark-theme = "light"
additional-css = ["theme/page-toc.css"]
additional-js = ["theme/page-toc.js"]
[output.html.print]
enable = false

View file

@ -560,7 +560,7 @@ ul#searchresults span.teaser em {
.chapter li a.active {
color: var(--sidebar-active);
background-color: rgba(8, 76, 207, 0.1);
background-color: var(--sidebar-active-bg);
}
.chapter li > a.toggle {

View file

@ -113,7 +113,7 @@ h6:target::before {
*/
:target {
/* Safari does not support logical properties */
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
scroll-margin-top: calc(var(--menu-bar-height) + 2rem);
}
.page {
@ -141,7 +141,7 @@ h6:target::before {
.content {
overflow-y: auto;
padding: 24px 4px 48px 4px;
padding: 48px 4px;
}
.content main {
margin-inline-start: auto;

View file

@ -6,7 +6,7 @@
--sidebar-resize-indicator-space: 2px;
--page-padding: 15px;
--content-max-width: 750px;
--menu-bar-height: 50px;
--menu-bar-height: 64px;
--font: "IA Writer Quattro S", sans-serif;
--title-font: "Agrandir", "Helvetica Neue", Helvetica, Arial, sans-serif;
--mono-font: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
@ -20,7 +20,8 @@
--sidebar-fg: hsl(0, 0%, 0%);
--sidebar-non-existant: #aaaaaa;
--sidebar-active: rgb(8, 76, 207);
--sidebar-active: hsl(219, 93%, 42%);
--sidebar-active-bg: hsl(219, 93%, 42%, 0.1);
--sidebar-spacer: #f4f4f4;
--scrollbar: #8f8f8f;

View file

@ -119,7 +119,6 @@
</script>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
{{> header}}
<div id="menu-bar-hover-placeholder"></div>
@ -192,7 +191,12 @@
<div id="content" class="content">
<main>
{{{ content }}}
<div class="sidetoc">
<nav class="pagetoc">
<p class="toc-title">On this page</p>
</nav>
</div>
{{{ content }}}
</main>
</div>
</div>

79
docs/theme/page-toc.css vendored Normal file
View file

@ -0,0 +1,79 @@
@media only screen and (max-width: 1674px) {
.sidetoc {
display: none;
}
}
@media only screen and (min-width: 1675px) {
main {
position: relative;
}
.sidetoc {
margin-left: auto;
margin-right: auto;
left: calc(100% + (var(--content-max-width)) / 3 - 160px);
position: absolute;
}
.pagetoc {
position: fixed;
top: 64px;
width: 220px;
height: calc(100vh - var(--menu-bar-height) - 0.67em * 4);
padding-top: 80px;
margin-right: 16px;
padding-bottom: 40px;
overflow: auto;
}
.pagetoc > :last-child {
margin-bottom: 64px;
}
.pagetoc a {
width: fit-content;
font-size: 1.4rem;
border-left: 1px solid var(--sidebar-bg);
color: var(--fg) !important;
display: block;
padding: 2px;
margin: 8px 0 8px 12px;
text-align: left;
text-decoration: underline;
text-decoration-color: hsl(0, 0%, 0%, 0.1);
}
.pagetoc a:hover {
text-decoration-color: hsl(0, 0%, 0%, 0.5);
}
.pagetoc a.active {
background-color: var(--sidebar-active-bg);
color: var(--sidebar-active) !important;
text-decoration-color: hsl(219, 93%, 42%, 0.1);
}
.pagetoc a.active:hover {
text-decoration-color: hsl(219, 93%, 42%, 0.8);
}
.pagetoc .active {
background: var(--sidebar-bg);
color: var(--sidebar-fg);
}
.pagetoc .pagetoc-H1 {
display: none;
}
.pagetoc .pagetoc-H3 {
margin-left: 24px;
}
.pagetoc .pagetoc-H4 {
margin-left: 42px;
}
.pagetoc .pagetoc-H5 {
display: none;
}
.pagetoc .pagetoc-H6 {
display: none;
}
.toc-title {
margin: 0;
margin-bottom: 12px;
padding-left: 12px;
font-size: 1.4rem;
color: #000;
}
}

73
docs/theme/page-toc.js vendored Normal file
View file

@ -0,0 +1,73 @@
let scrollTimeout;
const listenActive = () => {
const elems = document.querySelector(".pagetoc").children;
[...elems].forEach((el) => {
el.addEventListener("click", (event) => {
clearTimeout(scrollTimeout);
[...elems].forEach((el) => el.classList.remove("active"));
el.classList.add("active");
// Prevent scroll updates for a short period
scrollTimeout = setTimeout(() => {
scrollTimeout = null;
}, 100); // Adjust timing as needed
});
});
};
const getPagetoc = () =>
document.querySelector(".pagetoc") || autoCreatePagetoc();
const autoCreatePagetoc = () => {
const main = document.querySelector("#content > main");
const content = Object.assign(document.createElement("div"), {
className: "content-wrap",
});
content.append(...main.childNodes);
main.prepend(content);
main.insertAdjacentHTML(
"afterbegin",
'<div class="sidetoc"><nav class="pagetoc"></nav></div>',
);
return document.querySelector(".pagetoc");
};
const updateFunction = () => {
if (scrollTimeout) return; // Skip updates if within the cooldown period from a click
const headers = [...document.getElementsByClassName("header")];
const scrolledY = window.scrollY;
let lastHeader = null;
// Find the last header that is above the current scroll position
for (let i = headers.length - 1; i >= 0; i--) {
if (scrolledY >= headers[i].offsetTop) {
lastHeader = headers[i];
break;
}
}
const pagetocLinks = [...document.querySelector(".pagetoc").children];
pagetocLinks.forEach((link) => link.classList.remove("active"));
if (lastHeader) {
const activeLink = pagetocLinks.find(
(link) => lastHeader.href === link.href,
);
if (activeLink) activeLink.classList.add("active");
}
};
window.addEventListener("load", () => {
const pagetoc = getPagetoc();
const headers = [...document.getElementsByClassName("header")];
headers.forEach((header) => {
const link = Object.assign(document.createElement("a"), {
textContent: header.text,
href: header.href,
className: `pagetoc-${header.parentElement.tagName}`,
});
pagetoc.appendChild(link);
});
updateFunction();
listenActive();
window.addEventListener("scroll", updateFunction);
});