Skip to content

Commit 9ea2670

Browse files
brabojclaude
andauthored
feat: add collapsible mobile ToC for in-page navigation (#203)
* feat: add collapsible mobile ToC for in-page navigation Add an "On this page" details element that lists h2 headings, visible only at <=960px where the sidebar ToC is hidden. Placed after the h1 in every chapter page. Closes #110 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: make mobile ToC sticky so it stays visible while scrolling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: move mobile ToC into hamburger menu Replace standalone MobileToc component with an "On this page" section inside the hamburger nav panel. JS clones h2 links from the existing sidebar-toc at runtime. Tapping a section link closes the panel and scrolls to the heading. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: add .claude/ to gitignore and remove from index Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: defer mobile ToC population to DOMContentLoaded The inline script ran before the sidebar-toc element was in the DOM, so querySelector returned null and no links were added. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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> * fix: move mobile ToC inside main content for proper sticky behavior Sticky positioning requires the element to be inside the scrolling container. Moved from between Header and site-main to inside main. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: replace sticky bar with floating ToC button (approach C) Teal FAB in bottom-right corner on mobile. Tap to open an overlay listing h2 section links. Closes on link tap or outside click. Zero layout impact — no sticky, no header changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c1d16af commit 9ea2670

5 files changed

Lines changed: 141 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ dist/
2626
# JetBrains IDE
2727
.idea/
2828

29+
# Claude Code
30+
.claude/
31+
2932
# Unit test reports
3033
TEST*.xml
3134

astro-site/src/components/Header.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,5 @@ const base = import.meta.env.BASE_URL;
9090
navToggle.setAttribute("aria-expanded", "false");
9191
siteTabs.classList.remove("open");
9292
});
93+
9394
</script>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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" id="mobile-toc">
14+
<button class="mobile-toc-fab" id="mobile-toc-fab" aria-label="Table of contents" aria-expanded="false">
15+
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true">
16+
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
17+
</svg>
18+
</button>
19+
<div class="mobile-toc-overlay" id="mobile-toc-overlay">
20+
<div class="mobile-toc-header">On this page</div>
21+
<ul>
22+
{h2s.map((h) => (
23+
<li>
24+
<a href={`#${h.slug}`}>{h.text}</a>
25+
</li>
26+
))}
27+
</ul>
28+
</div>
29+
</div>
30+
)}
31+
32+
<script is:inline>
33+
(function () {
34+
var fab = document.getElementById("mobile-toc-fab");
35+
var overlay = document.getElementById("mobile-toc-overlay");
36+
var toc = document.getElementById("mobile-toc");
37+
if (!fab || !overlay) return;
38+
39+
fab.addEventListener("click", function (e) {
40+
e.stopPropagation();
41+
var open = fab.getAttribute("aria-expanded") === "true";
42+
fab.setAttribute("aria-expanded", String(!open));
43+
overlay.classList.toggle("open");
44+
});
45+
46+
overlay.querySelectorAll("a").forEach(function (link) {
47+
link.addEventListener("click", function () {
48+
fab.setAttribute("aria-expanded", "false");
49+
overlay.classList.remove("open");
50+
});
51+
});
52+
53+
document.addEventListener("click", function (e) {
54+
if (!overlay.classList.contains("open")) return;
55+
if (toc.contains(e.target)) return;
56+
fab.setAttribute("aria-expanded", "false");
57+
overlay.classList.remove("open");
58+
});
59+
})();
60+
</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 {
@@ -71,6 +72,7 @@ const ogImage = new URL("/tutorial-git/images/og-banner.png", Astro.site);
7172
<div class="resize-handle" data-side="right"></div>
7273
<TutorialLinks />
7374
</div>
75+
<MobileToc headings={headings} />
7476
<Footer />
7577

7678
<script is:inline>

astro-site/src/styles/global.css

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

575+
/* Mobile ToC — floating button, hidden on desktop */
576+
.mobile-toc {
577+
display: none;
578+
}
579+
575580
/* Responsive */
576581
@media (max-width: 960px) {
582+
.mobile-toc {
583+
display: block;
584+
position: fixed;
585+
bottom: var(--space-lg);
586+
right: var(--space-lg);
587+
z-index: 100;
588+
}
589+
590+
.mobile-toc-fab {
591+
width: 48px;
592+
height: 48px;
593+
border-radius: 50%;
594+
border: none;
595+
background: var(--color-primary);
596+
color: #fff;
597+
cursor: pointer;
598+
display: flex;
599+
align-items: center;
600+
justify-content: center;
601+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
602+
}
603+
604+
.mobile-toc-fab:hover {
605+
background: var(--color-primary-dark);
606+
}
607+
608+
.mobile-toc-overlay {
609+
display: none;
610+
position: absolute;
611+
bottom: 56px;
612+
right: 0;
613+
width: 250px;
614+
max-height: 60vh;
615+
overflow-y: auto;
616+
background: var(--color-bg);
617+
border: 1px solid var(--color-border);
618+
border-radius: 8px;
619+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
620+
padding: var(--space-sm) 0;
621+
}
622+
623+
.mobile-toc-overlay.open {
624+
display: block;
625+
}
626+
627+
.mobile-toc-header {
628+
padding: var(--space-sm) var(--space-md);
629+
font-size: var(--font-size-sm);
630+
font-weight: 600;
631+
color: var(--color-fg-muted);
632+
}
633+
634+
.mobile-toc-overlay ul {
635+
list-style: none;
636+
padding: 0;
637+
}
638+
639+
.mobile-toc-overlay li a {
640+
display: block;
641+
padding: var(--space-xs) var(--space-md);
642+
color: var(--color-fg);
643+
text-decoration: none;
644+
font-size: var(--font-size-sm);
645+
}
646+
647+
.mobile-toc-overlay li a:hover {
648+
background: var(--color-bg-hover);
649+
color: var(--color-link);
650+
}
651+
577652
.site-main {
578653
grid-template-columns: 1fr;
579654
}

0 commit comments

Comments
 (0)