diff --git a/docs/README.md b/docs/README.md
index b9a953ea89..194b973ff2 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -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.
diff --git a/docs/book.toml b/docs/book.toml
index 445dc0bcb6..1f439be205 100644
--- a/docs/book.toml
+++ b/docs/book.toml
@@ -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
diff --git a/docs/theme/css/chrome.css b/docs/theme/css/chrome.css
index 71775cfc50..9097c2bb50 100644
--- a/docs/theme/css/chrome.css
+++ b/docs/theme/css/chrome.css
@@ -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 {
diff --git a/docs/theme/css/general.css b/docs/theme/css/general.css
index fc4e28b5c0..b08175e8d5 100644
--- a/docs/theme/css/general.css
+++ b/docs/theme/css/general.css
@@ -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;
diff --git a/docs/theme/css/variables.css b/docs/theme/css/variables.css
index 3342ce59ef..15dc370e71 100644
--- a/docs/theme/css/variables.css
+++ b/docs/theme/css/variables.css
@@ -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;
diff --git a/docs/theme/index.hbs b/docs/theme/index.hbs
index a1cdde6bac..8976b54bd9 100644
--- a/docs/theme/index.hbs
+++ b/docs/theme/index.hbs
@@ -119,7 +119,6 @@
-
{{> header}}
@@ -192,7 +191,12 @@
- {{{ content }}}
+
+ {{{ content }}}
diff --git a/docs/theme/page-toc.css b/docs/theme/page-toc.css
new file mode 100644
index 0000000000..dacd61a09b
--- /dev/null
+++ b/docs/theme/page-toc.css
@@ -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;
+ }
+}
diff --git a/docs/theme/page-toc.js b/docs/theme/page-toc.js
new file mode 100644
index 0000000000..62af2e7dc4
--- /dev/null
+++ b/docs/theme/page-toc.js
@@ -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",
+ '
',
+ );
+ 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);
+});