aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--174bg/handbook/package-lock.json22
-rw-r--r--174bg/handbook/package.json1
-rw-r--r--174bg/handbook/source/index.css1
-rw-r--r--174bg/handbook/source/index.html83
-rw-r--r--174bg/handbook/source/search.js36
-rw-r--r--174bg/handbook/source/table-of-contents.js78
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);
+});