diff options
| -rw-r--r-- | 174bg/handbook/package-lock.json | 22 | ||||
| -rw-r--r-- | 174bg/handbook/package.json | 1 | ||||
| -rw-r--r-- | 174bg/handbook/source/index.css | 1 | ||||
| -rw-r--r-- | 174bg/handbook/source/index.html | 83 | ||||
| -rw-r--r-- | 174bg/handbook/source/search.js | 36 | ||||
| -rw-r--r-- | 174bg/handbook/source/table-of-contents.js | 78 |
6 files changed, 149 insertions, 72 deletions
diff --git a/174bg/handbook/package-lock.json b/174bg/handbook/package-lock.json index ad0563c..d43c9e3 100644 --- a/174bg/handbook/package-lock.json +++ b/174bg/handbook/package-lock.json @@ -6,6 +6,7 @@ "": { "dependencies": { "@tailwindcss/cli": "^4.3.0", + "@tailwindcss/forms": "^0.5.11", "@tailwindcss/typography": "^0.5.19", "chokidar-cli": "^3.0.0", "copyfiles": "^2.4.1", @@ -1434,6 +1435,18 @@ "tailwindcss": "dist/index.mjs" } }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz", + "integrity": "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==", + "license": "MIT", + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" + } + }, "node_modules/@tailwindcss/node": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", @@ -4980,6 +4993,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, "node_modules/miniflare": { "version": "4.20260526.0", "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260526.0.tgz", diff --git a/174bg/handbook/package.json b/174bg/handbook/package.json index b885c05..445febc 100644 --- a/174bg/handbook/package.json +++ b/174bg/handbook/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@tailwindcss/cli": "^4.3.0", + "@tailwindcss/forms": "^0.5.11", "@tailwindcss/typography": "^0.5.19", "chokidar-cli": "^3.0.0", "copyfiles": "^2.4.1", diff --git a/174bg/handbook/source/index.css b/174bg/handbook/source/index.css index ec705bb..bd1ea15 100644 --- a/174bg/handbook/source/index.css +++ b/174bg/handbook/source/index.css @@ -1,5 +1,6 @@ @import "tailwindcss"; @plugin "@tailwindcss/typography"; +@plugin "@tailwindcss/forms"; @utility hide-scrollbar { &::-webkit-scrollbar { diff --git a/174bg/handbook/source/index.html b/174bg/handbook/source/index.html index 8e5bb7a..cf9351e 100644 --- a/174bg/handbook/source/index.html +++ b/174bg/handbook/source/index.html @@ -8,10 +8,17 @@ </head> <body class="dark:bg-stone-900 flex flex-row"> <nav - class="w-64 p-4 border-r border-stone-700 h-screen sticky top-0 overflow-y-auto hide-scrollbar" + class="w-64 p-4 border-r border-stone-700 h-screen sticky top-0 overflow-y-auto hide-scrollbar flex flex-col gap-4" > + <div id="search"> + <input + type="text" + id="search-input" + placeholder="Search... (/)" + class="w-full p-2 rounded bg-stone-800 text-white focus:outline-none focus:ring-2 focus:ring-stone-500" + /> + </div> <div id="table-of-contents"></div> - <div id="search"></div> </nav> <main class="prose dark:prose-invert mx-auto p-4 max-w-xl"> <article id="introduction"> @@ -1533,74 +1540,6 @@ </main> </body> <script src="wikilinks.js"></script> - <script> - document.addEventListener("DOMContentLoaded", () => { - generateTableOfContents(); - }); - - function generateTableOfContents() { - const container = document.getElementById("table-of-contents"); - if (!container) return; - - const sections = document.querySelectorAll( - "main article[id], main section[id]", - ); - - const entries = []; - sections.forEach((section) => { - const heading = section.querySelector("h1, h2, h3, h4, h5, h6"); - if (!heading) return; - entries.push({ - id: section.id, - level: parseInt(heading.tagName.charAt(1), 10), - text: heading.textContent.trim(), - }); - }); - - if (entries.length === 0) return; - - const baseLevel = Math.min(...entries.map((entry) => entry.level)); - - const rootList = document.createElement("ul"); - rootList.className = "space-y-1 text-sm"; - const stack = [{ level: baseLevel - 1, list: rootList, item: null }]; - - entries.forEach((entry) => { - while ( - stack.length > 1 && - entry.level <= stack[stack.length - 1].level - ) { - stack.pop(); - } - - const parent = stack[stack.length - 1]; - - let parentList = parent.list; - if (!parentList) { - parentList = document.createElement("ul"); - parentList.className = - "mt-1 ml-3 space-y-1 border-l border-stone-200 pl-3 dark:border-stone-700"; - parent.item.appendChild(parentList); - parent.list = parentList; - } - - const listItem = document.createElement("li"); - const link = document.createElement("a"); - link.href = "#" + entry.id; - link.textContent = entry.text; - link.className = - "block rounded px-2 py-1 text-stone-600 no-underline transition-colors hover:bg-stone-100 hover:text-stone-900 dark:text-stone-400 dark:hover:bg-stone-800 dark:hover:text-stone-100"; - listItem.appendChild(link); - parentList.appendChild(listItem); - - stack.push({ - level: entry.level, - list: null, - item: listItem, - }); - }); - - container.appendChild(rootList); - } - </script> + <script src="table-of-contents.js"></script> + <script src="search.js"></script> </html> diff --git a/174bg/handbook/source/search.js b/174bg/handbook/source/search.js new file mode 100644 index 0000000..d42337f --- /dev/null +++ b/174bg/handbook/source/search.js @@ -0,0 +1,36 @@ +/** + * search.js + * + * Provides simple client-side filtering of the handbook's articles. + * + * How it works: + * 1. Pressing the "/" key anywhere on the page focuses the search input + * (id "search-input"), unless focus is already needed elsewhere. + * 2. As the user types, every `<article>` in `<main>` is shown or hidden + * based on whether its text content includes the (case-insensitive) + * query string. + */ +document.addEventListener("keypress", function (event) { + if (event.key === "/") { + const searchInput = document.getElementById("search-input"); + if (searchInput) { + searchInput.focus(); + event.preventDefault(); + } + } +}); + +document + .querySelector("#search-input") + .addEventListener("input", function (event) { + const query = event.target.value.toLowerCase(); + const articles = document.querySelectorAll("main article"); + articles.forEach((article) => { + const text = article.textContent.toLowerCase(); + if (text.includes(query)) { + article.style.display = ""; + } else { + article.style.display = "none"; + } + }); + }); diff --git a/174bg/handbook/source/table-of-contents.js b/174bg/handbook/source/table-of-contents.js new file mode 100644 index 0000000..dd87353 --- /dev/null +++ b/174bg/handbook/source/table-of-contents.js @@ -0,0 +1,78 @@ +/** + * table-of-contents.js + * + * Builds a nested table of contents from the page's sections and injects it + * into the element with id "table-of-contents". + * + * How it works: + * 1. Finds every `<article>` and `<section>` in `<main>` that has an `id`. + * 2. Reads the first heading (h1–h6) in each to determine its title and + * nesting level. + * 3. Builds a hierarchical <ul>/<li> tree using a stack to track the + * current nesting level, indenting nested levels with a left border. + * 4. Each entry links to its section via an `#id` anchor. + * + * The TOC is only rendered if the container exists and at least one + * qualifying section with a heading is found. + */ +document.addEventListener("DOMContentLoaded", () => { + const container = document.getElementById("table-of-contents"); + if (!container) return; + + const sections = document.querySelectorAll( + "main article[id], main section[id]", + ); + + const entries = []; + sections.forEach((section) => { + const heading = section.querySelector("h1, h2, h3, h4, h5, h6"); + if (!heading) return; + entries.push({ + id: section.id, + level: parseInt(heading.tagName.charAt(1), 10), + text: heading.textContent.trim(), + }); + }); + + if (entries.length === 0) return; + + const baseLevel = Math.min(...entries.map((entry) => entry.level)); + + const rootList = document.createElement("ul"); + rootList.className = "space-y-1 text-sm"; + const stack = [{ level: baseLevel - 1, list: rootList, item: null }]; + + entries.forEach((entry) => { + while (stack.length > 1 && entry.level <= stack[stack.length - 1].level) { + stack.pop(); + } + + const parent = stack[stack.length - 1]; + + let parentList = parent.list; + if (!parentList) { + parentList = document.createElement("ul"); + parentList.className = + "mt-1 ml-3 space-y-1 border-l border-stone-200 pl-3 dark:border-stone-700"; + parent.item.appendChild(parentList); + parent.list = parentList; + } + + const listItem = document.createElement("li"); + const link = document.createElement("a"); + link.href = "#" + entry.id; + link.textContent = entry.text; + link.className = + "block rounded px-2 py-1 text-stone-600 no-underline transition-colors hover:bg-stone-100 hover:text-stone-900 dark:text-stone-400 dark:hover:bg-stone-800 dark:hover:text-stone-100"; + listItem.appendChild(link); + parentList.appendChild(listItem); + + stack.push({ + level: entry.level, + list: null, + item: listItem, + }); + }); + + container.appendChild(rootList); +}); |
