Skip to content

Commit 97c671a

Browse files
brabojclaude
andcommitted
refactor: replace hamburger ToC with sticky bar (approach B)
Remove ToC injection from hamburger menu. Add a dedicated sticky "On this page" bar below the header, visible only at <=960px. Expands to show h2 section links, closes on link tap or outside click. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 62e4b8b commit 97c671a

4 files changed

Lines changed: 127 additions & 49 deletions

File tree

astro-site/src/components/Header.astro

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -91,31 +91,4 @@ const base = import.meta.env.BASE_URL;
9191
siteTabs.classList.remove("open");
9292
});
9393

94-
// Populate "On this page" section from the sidebar ToC
95-
document.addEventListener("DOMContentLoaded", () => {
96-
const sidebarToc = document.querySelector(".sidebar-toc");
97-
if (!sidebarToc) return;
98-
const topLinks = sidebarToc.querySelectorAll(":scope > ul > li");
99-
if (topLinks.length === 0) return;
100-
101-
const label = document.createElement("div");
102-
label.className = "nav-toc-label";
103-
label.textContent = "On this page";
104-
siteTabs.appendChild(label);
105-
106-
topLinks.forEach((li) => {
107-
const source = li.querySelector(":scope > a, :scope > details > summary > a");
108-
if (source) {
109-
const link = document.createElement("a");
110-
link.href = source.getAttribute("href");
111-
link.textContent = source.textContent;
112-
link.className = "nav-toc-link";
113-
link.addEventListener("click", () => {
114-
navToggle.setAttribute("aria-expanded", "false");
115-
siteTabs.classList.remove("open");
116-
});
117-
siteTabs.appendChild(link);
118-
}
119-
});
120-
});
12194
</script>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
interface Heading {
3+
depth: number;
4+
slug: string;
5+
text: string;
6+
}
7+
8+
const { headings = [] } = Astro.props as { headings: Heading[] };
9+
const h2s = headings.filter((h) => h.depth === 2);
10+
---
11+
12+
{h2s.length > 0 && (
13+
<div class="mobile-toc-bar" id="mobile-toc-bar">
14+
<button class="mobile-toc-toggle" id="mobile-toc-toggle" aria-expanded="false">
15+
<span>On this page</span>
16+
<svg class="mobile-toc-chevron" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true">
17+
<path d="M7 10l5 5 5-5z"/>
18+
</svg>
19+
</button>
20+
<ul class="mobile-toc-list" id="mobile-toc-list">
21+
{h2s.map((h) => (
22+
<li>
23+
<a href={`#${h.slug}`}>{h.text}</a>
24+
</li>
25+
))}
26+
</ul>
27+
</div>
28+
)}
29+
30+
<script is:inline>
31+
(function () {
32+
var toggle = document.getElementById("mobile-toc-toggle");
33+
var list = document.getElementById("mobile-toc-list");
34+
var bar = document.getElementById("mobile-toc-bar");
35+
if (!toggle || !list) return;
36+
37+
toggle.addEventListener("click", function (e) {
38+
e.stopPropagation();
39+
var open = toggle.getAttribute("aria-expanded") === "true";
40+
toggle.setAttribute("aria-expanded", String(!open));
41+
list.classList.toggle("open");
42+
});
43+
44+
list.querySelectorAll("a").forEach(function (link) {
45+
link.addEventListener("click", function () {
46+
toggle.setAttribute("aria-expanded", "false");
47+
list.classList.remove("open");
48+
});
49+
});
50+
51+
document.addEventListener("click", function (e) {
52+
if (!list.classList.contains("open")) return;
53+
if (bar.contains(e.target)) return;
54+
toggle.setAttribute("aria-expanded", "false");
55+
list.classList.remove("open");
56+
});
57+
})();
58+
</script>

astro-site/src/layouts/DocLayout.astro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Header from "../components/Header.astro";
44
import TableOfContents from "../components/TableOfContents.astro";
55
import TutorialLinks from "../components/TutorialLinks.astro";
66
import Footer from "../components/Footer.astro";
7+
import MobileToc from "../components/MobileToc.astro";
78
import site from "../data/site.json";
89
910
interface Props {
@@ -61,6 +62,7 @@ const ogImage = new URL("/tutorial-git/images/og-banner.png", Astro.site);
6162
<body>
6263
<a href="#main-content" class="skip-link">Skip to main content</a>
6364
<Header currentPath={currentPath} />
65+
<MobileToc headings={headings} />
6466
<div class="site-main">
6567
<TableOfContents headings={headings} />
6668
<div class="resize-handle" data-side="left"></div>

astro-site/src/styles/global.css

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -572,8 +572,75 @@ a:hover {
572572
color: var(--color-fg-muted);
573573
}
574574

575+
/* Mobile ToC bar — hidden on desktop */
576+
.mobile-toc-bar {
577+
display: none;
578+
}
579+
575580
/* Responsive */
576581
@media (max-width: 960px) {
582+
.mobile-toc-bar {
583+
display: block;
584+
position: sticky;
585+
top: 0;
586+
z-index: 10;
587+
background: var(--color-bg);
588+
border-bottom: 1px solid var(--color-border);
589+
}
590+
591+
.mobile-toc-toggle {
592+
display: flex;
593+
align-items: center;
594+
justify-content: space-between;
595+
width: 100%;
596+
padding: var(--space-sm) var(--space-md);
597+
background: none;
598+
border: none;
599+
font-family: var(--font-text);
600+
font-size: var(--font-size-base);
601+
font-weight: 600;
602+
color: var(--color-fg);
603+
cursor: pointer;
604+
}
605+
606+
.mobile-toc-toggle:hover {
607+
background: var(--color-bg-hover);
608+
}
609+
610+
.mobile-toc-chevron {
611+
transition: transform 0.15s;
612+
}
613+
614+
.mobile-toc-toggle[aria-expanded="true"] .mobile-toc-chevron {
615+
transform: rotate(180deg);
616+
}
617+
618+
.mobile-toc-list {
619+
display: none;
620+
list-style: none;
621+
padding: 0 var(--space-md) var(--space-sm);
622+
max-height: 60vh;
623+
overflow-y: auto;
624+
}
625+
626+
.mobile-toc-list.open {
627+
display: block;
628+
}
629+
630+
.mobile-toc-list li {
631+
padding: var(--space-xs) 0;
632+
}
633+
634+
.mobile-toc-list a {
635+
color: var(--color-link);
636+
text-decoration: none;
637+
font-size: var(--font-size-sm);
638+
}
639+
640+
.mobile-toc-list a:hover {
641+
text-decoration: underline;
642+
}
643+
577644
.site-main {
578645
grid-template-columns: 1fr;
579646
}
@@ -625,28 +692,6 @@ a:hover {
625692
background: rgba(255, 255, 255, 0.1);
626693
}
627694

628-
.nav-toc-label {
629-
padding: var(--space-sm) var(--space-md);
630-
font-size: var(--font-size-sm);
631-
font-weight: 600;
632-
color: rgba(255, 255, 255, 0.5);
633-
text-transform: uppercase;
634-
letter-spacing: 0.05em;
635-
border-top: 1px solid rgba(255, 255, 255, 0.1);
636-
}
637-
638-
.nav-toc-link {
639-
padding: var(--space-xs) var(--space-md) var(--space-xs) var(--space-lg);
640-
font-size: var(--font-size-sm);
641-
color: rgba(255, 255, 255, 0.8);
642-
text-decoration: none;
643-
display: block;
644-
}
645-
646-
.nav-toc-link:hover {
647-
background: rgba(255, 255, 255, 0.1);
648-
}
649-
650695
.content {
651696
padding: var(--space-lg) var(--space-md);
652697
}

0 commit comments

Comments
 (0)