summaryrefslogtreecommitdiff
path: root/layouts/partials/components
diff options
context:
space:
mode:
authorEltrac <hi@guhub.cn>2025-05-16 15:03:14 +0800
committerEltrac <hi@guhub.cn>2025-05-16 15:03:14 +0800
commit4b4629edb733d5ebd6485755fd2f63b0457f62c8 (patch)
treeb772561bab2cd21aa6a5acaea8af65999b83f78e /layouts/partials/components
parent2e1c4f08ac54232787d15d0175f0e99e2a701f78 (diff)
perf: replace tocbot with statically generated toc
Generate TOC with Hugo's built-in {{ .TableOfContents }} and add minimal script to implement collapsible list and active styles. Encapsulate TOC as a component where all JavaSript and CSS is in one place with HTML. Configure markup.
Diffstat (limited to 'layouts/partials/components')
-rw-r--r--layouts/partials/components/toc.html130
1 files changed, 130 insertions, 0 deletions
diff --git a/layouts/partials/components/toc.html b/layouts/partials/components/toc.html
new file mode 100644
index 00000000..660d4bb2
--- /dev/null
+++ b/layouts/partials/components/toc.html
@@ -0,0 +1,130 @@
+<div id="toc-container"><div id="toc">{{ .TableOfContents }}</div></div>
+<style>
+/* toc */
+#toc-container {
+ position: absolute;
+ bottom: 0;
+ right: calc(50% + var(--page-width) / 2);
+ left: auto;
+ height: 100%;
+ top: 12rem;
+}
+
+@media screen and (max-width: 1200px) {
+ #toc-container {
+ display: none
+ }
+}
+
+#toc {
+ /* position */
+ position: sticky;
+ top: 3rem;
+ /* styling */
+ font-size: 1rem;
+ padding: 0 2rem;
+}
+
+@media screen and (max-width: 1700px) {
+ #toc {
+ font-size: 0.9rem;
+ }
+}
+
+@media screen and (max-width: 1500px) {
+ #toc {
+ font-size: 0.825rem;
+ }
+}
+
+#toc ul, #toc ol {
+ margin-block-start: 0;
+ margin-block-end: 0;
+ padding-inline-start: 1rem;
+}
+
+#toc li {
+ list-style: none;
+}
+
+#toc li > a {
+ color: var(--secondary-text-color);
+ text-decoration: none;
+ transition: color .3s;
+ display: inline-flex;
+ align-items: center;
+ gap: .5rem;
+}
+
+#toc li > a::before {
+ content: '';
+ display: inline-block;
+ width: 1rem;
+ height: .3rem;
+ background-color: var(--block-background-color);
+ border-radius: .2rem;
+ transition: background .3s;
+}
+
+#toc li a:hover,
+#toc li.active > a {
+ color: var(--text-color);
+}
+
+#toc li.active > a::before {
+ background-color: var(--secondary-text-color);
+}
+
+#toc ul {
+ overflow: hidden;
+ position: relative;
+}
+
+#toc .toc-list ul {
+ max-height: 25rem;
+ overflow-y: auto
+}
+
+#toc li:not(.active) ul {
+ max-height: 0
+}
+</style>
+<script>
+document.addEventListener('DOMContentLoaded', () => {
+ const toc = document.getElementById('toc');
+ if (!toc) return;
+
+ const headings = document.querySelectorAll('h2, h3');
+ const observer = new IntersectionObserver(entries => {
+ entries.forEach(entry => {
+ if (!entry.isIntersecting) return;
+
+ const id = entry.target.id;
+ const tocLink = toc.querySelector(`a[href="#${id}"]`);
+ if (!tocLink) return;
+
+ const listItem = tocLink.parentElement;
+ const isH2 = entry.target.tagName === 'H2';
+
+ // Clear all active states when a new H2 is encountered
+ if (isH2) {
+ toc.querySelectorAll('li.active').forEach(el => el.classList.remove('active'));
+ }
+
+ // Set current item as active
+ listItem.classList.add('active');
+
+ // If this is an H3, find and activate its parent H2
+ if (!isH2) {
+ const parentH2Item = listItem.closest('ul').previousElementSibling?.closest('li');
+ if (parentH2Item) parentH2Item.classList.add('active');
+ }
+ });
+ }, {
+ rootMargin: '0px 0px -50% 0px',
+ threshold: 0.5
+ });
+
+ headings.forEach(heading => observer.observe(heading));
+});
+</script>