Skip to content
Merged
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ dist/
# JetBrains IDE
.idea/

# Claude Code
.claude/

# Unit test reports
TEST*.xml

Expand Down
1 change: 1 addition & 0 deletions astro-site/src/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,5 @@ const base = import.meta.env.BASE_URL;
navToggle.setAttribute("aria-expanded", "false");
siteTabs.classList.remove("open");
});

</script>
60 changes: 60 additions & 0 deletions astro-site/src/components/MobileToc.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
interface Heading {
depth: number;
slug: string;
text: string;
}

const { headings = [] } = Astro.props as { headings: Heading[] };
const h2s = headings.filter((h) => h.depth === 2);
---

{h2s.length > 0 && (
<div class="mobile-toc" id="mobile-toc">
<button class="mobile-toc-fab" id="mobile-toc-fab" aria-label="Table of contents" aria-expanded="false">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true">
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>
</button>
<div class="mobile-toc-overlay" id="mobile-toc-overlay">
<div class="mobile-toc-header">On this page</div>
<ul>
{h2s.map((h) => (
<li>
<a href={`#${h.slug}`}>{h.text}</a>
</li>
))}
</ul>
</div>
</div>
)}

<script is:inline>
(function () {
var fab = document.getElementById("mobile-toc-fab");
var overlay = document.getElementById("mobile-toc-overlay");
var toc = document.getElementById("mobile-toc");
if (!fab || !overlay) return;

fab.addEventListener("click", function (e) {
e.stopPropagation();
var open = fab.getAttribute("aria-expanded") === "true";
fab.setAttribute("aria-expanded", String(!open));
overlay.classList.toggle("open");
});

overlay.querySelectorAll("a").forEach(function (link) {
link.addEventListener("click", function () {
fab.setAttribute("aria-expanded", "false");
overlay.classList.remove("open");
});
});

document.addEventListener("click", function (e) {
if (!overlay.classList.contains("open")) return;
if (toc.contains(e.target)) return;
fab.setAttribute("aria-expanded", "false");
overlay.classList.remove("open");
});
})();
</script>
2 changes: 2 additions & 0 deletions astro-site/src/layouts/DocLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Header from "../components/Header.astro";
import TableOfContents from "../components/TableOfContents.astro";
import TutorialLinks from "../components/TutorialLinks.astro";
import Footer from "../components/Footer.astro";
import MobileToc from "../components/MobileToc.astro";
import site from "../data/site.json";

interface Props {
Expand Down Expand Up @@ -71,6 +72,7 @@ const ogImage = new URL("/tutorial-git/images/og-banner.png", Astro.site);
<div class="resize-handle" data-side="right"></div>
<TutorialLinks />
</div>
<MobileToc headings={headings} />
<Footer />

<script is:inline>
Expand Down
75 changes: 75 additions & 0 deletions astro-site/src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -572,8 +572,83 @@ a:hover {
color: var(--color-fg-muted);
}

/* Mobile ToC — floating button, hidden on desktop */
.mobile-toc {
display: none;
}

/* Responsive */
@media (max-width: 960px) {
.mobile-toc {
display: block;
position: fixed;
bottom: var(--space-lg);
right: var(--space-lg);
z-index: 100;
}

.mobile-toc-fab {
width: 48px;
height: 48px;
border-radius: 50%;
border: none;
background: var(--color-primary);
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}

.mobile-toc-fab:hover {
background: var(--color-primary-dark);
}

.mobile-toc-overlay {
display: none;
position: absolute;
bottom: 56px;
right: 0;
width: 250px;
max-height: 60vh;
overflow-y: auto;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
padding: var(--space-sm) 0;
}

.mobile-toc-overlay.open {
display: block;
}

.mobile-toc-header {
padding: var(--space-sm) var(--space-md);
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--color-fg-muted);
}

.mobile-toc-overlay ul {
list-style: none;
padding: 0;
}

.mobile-toc-overlay li a {
display: block;
padding: var(--space-xs) var(--space-md);
color: var(--color-fg);
text-decoration: none;
font-size: var(--font-size-sm);
}

.mobile-toc-overlay li a:hover {
background: var(--color-bg-hover);
color: var(--color-link);
}

.site-main {
grid-template-columns: 1fr;
}
Expand Down
Loading