From cfff7878a97df53223ee65ff07863d09b66cacf3 Mon Sep 17 00:00:00 2001 From: Branimir Georgiev Date: Sun, 26 Apr 2026 13:45:16 +0300 Subject: [PATCH 1/8] 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) --- astro-site/src/components/MobileToc.astro | 23 +++++++++ astro-site/src/layouts/DocLayout.astro | 2 + astro-site/src/styles/global.css | 57 +++++++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 astro-site/src/components/MobileToc.astro diff --git a/astro-site/src/components/MobileToc.astro b/astro-site/src/components/MobileToc.astro new file mode 100644 index 0000000..4345c62 --- /dev/null +++ b/astro-site/src/components/MobileToc.astro @@ -0,0 +1,23 @@ +--- +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 && ( +
+ On this page +
    + {h2s.map((h) => ( +
  • + {h.text} +
  • + ))} +
+
+)} diff --git a/astro-site/src/layouts/DocLayout.astro b/astro-site/src/layouts/DocLayout.astro index e9a11b6..95c7085 100644 --- a/astro-site/src/layouts/DocLayout.astro +++ b/astro-site/src/layouts/DocLayout.astro @@ -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 { @@ -66,6 +67,7 @@ const ogImage = new URL("/tutorial-git/images/og-banner.png", Astro.site);

{title}

+
diff --git a/astro-site/src/styles/global.css b/astro-site/src/styles/global.css index 90df62a..fb2baec 100644 --- a/astro-site/src/styles/global.css +++ b/astro-site/src/styles/global.css @@ -572,6 +572,11 @@ a:hover { color: var(--color-fg-muted); } +/* Mobile ToC — hidden on desktop, shown when sidebar disappears */ +.mobile-toc { + display: none; +} + /* Responsive */ @media (max-width: 960px) { .site-main { @@ -583,6 +588,58 @@ a:hover { .resize-handle { display: none; } + + .mobile-toc { + display: block; + margin-bottom: var(--space-lg); + border: 1px solid var(--color-border); + border-radius: 4px; + padding: 0; + } + + .mobile-toc summary { + padding: var(--space-sm) var(--space-md); + font-weight: 600; + font-size: var(--font-size-base); + color: var(--color-fg); + cursor: pointer; + list-style: none; + } + + .mobile-toc summary::after { + content: "\25B6"; + float: right; + font-size: 0.7em; + color: var(--color-fg-faint); + transition: transform 0.15s; + } + + .mobile-toc[open] summary::after { + transform: rotate(90deg); + } + + .mobile-toc summary::-webkit-details-marker { + display: none; + } + + .mobile-toc ul { + list-style: none; + padding: 0 var(--space-md) var(--space-sm); + } + + .mobile-toc li { + padding: var(--space-xs) 0; + } + + .mobile-toc a { + color: var(--color-link); + text-decoration: none; + font-size: var(--font-size-sm); + } + + .mobile-toc a:hover { + text-decoration: underline; + } } @media (max-width: 768px) { From 9e3b62c7cf4e22afafa00c5eec8d9d061813261c Mon Sep 17 00:00:00 2001 From: Branimir Georgiev Date: Sun, 26 Apr 2026 13:48:15 +0300 Subject: [PATCH 2/8] fix: make mobile ToC sticky so it stays visible while scrolling Co-Authored-By: Claude Opus 4.6 (1M context) --- astro-site/src/styles/global.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/astro-site/src/styles/global.css b/astro-site/src/styles/global.css index fb2baec..0af3ad9 100644 --- a/astro-site/src/styles/global.css +++ b/astro-site/src/styles/global.css @@ -595,6 +595,10 @@ a:hover { border: 1px solid var(--color-border); border-radius: 4px; padding: 0; + position: sticky; + top: 0; + z-index: 10; + background: var(--color-bg); } .mobile-toc summary { From f5868196c6997fdd701a35ecd688ca03cdc00b1c Mon Sep 17 00:00:00 2001 From: Branimir Georgiev Date: Sun, 26 Apr 2026 13:57:09 +0300 Subject: [PATCH 3/8] 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) --- .claude/worktrees/agent-abc53eb2 | 1 + astro-site/src/components/Header.astro | 27 ++++++++ astro-site/src/components/MobileToc.astro | 23 ------- astro-site/src/layouts/DocLayout.astro | 2 - astro-site/src/styles/global.css | 83 ++++++----------------- 5 files changed, 50 insertions(+), 86 deletions(-) create mode 160000 .claude/worktrees/agent-abc53eb2 delete mode 100644 astro-site/src/components/MobileToc.astro diff --git a/.claude/worktrees/agent-abc53eb2 b/.claude/worktrees/agent-abc53eb2 new file mode 160000 index 0000000..a9d1fae --- /dev/null +++ b/.claude/worktrees/agent-abc53eb2 @@ -0,0 +1 @@ +Subproject commit a9d1faee4bb53b601f763e5aff2d543242ec49ab diff --git a/astro-site/src/components/Header.astro b/astro-site/src/components/Header.astro index c28adf1..1f16342 100644 --- a/astro-site/src/components/Header.astro +++ b/astro-site/src/components/Header.astro @@ -90,4 +90,31 @@ const base = import.meta.env.BASE_URL; navToggle.setAttribute("aria-expanded", "false"); siteTabs.classList.remove("open"); }); + + // Populate "On this page" section from the sidebar ToC + const sidebarToc = document.querySelector(".sidebar-toc"); + if (sidebarToc) { + const topLinks = sidebarToc.querySelectorAll(":scope > ul > li"); + if (topLinks.length > 0) { + const label = document.createElement("div"); + label.className = "nav-toc-label"; + label.textContent = "On this page"; + siteTabs.appendChild(label); + + topLinks.forEach((li) => { + const source = li.querySelector(":scope > a, :scope > details > summary > a"); + if (source) { + const link = document.createElement("a"); + link.href = source.getAttribute("href"); + link.textContent = source.textContent; + link.className = "nav-toc-link"; + link.addEventListener("click", () => { + navToggle.setAttribute("aria-expanded", "false"); + siteTabs.classList.remove("open"); + }); + siteTabs.appendChild(link); + } + }); + } + } diff --git a/astro-site/src/components/MobileToc.astro b/astro-site/src/components/MobileToc.astro deleted file mode 100644 index 4345c62..0000000 --- a/astro-site/src/components/MobileToc.astro +++ /dev/null @@ -1,23 +0,0 @@ ---- -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 && ( -
- On this page -
    - {h2s.map((h) => ( -
  • - {h.text} -
  • - ))} -
-
-)} diff --git a/astro-site/src/layouts/DocLayout.astro b/astro-site/src/layouts/DocLayout.astro index 95c7085..e9a11b6 100644 --- a/astro-site/src/layouts/DocLayout.astro +++ b/astro-site/src/layouts/DocLayout.astro @@ -4,7 +4,6 @@ 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 { @@ -67,7 +66,6 @@ const ogImage = new URL("/tutorial-git/images/og-banner.png", Astro.site);

{title}

-
diff --git a/astro-site/src/styles/global.css b/astro-site/src/styles/global.css index 0af3ad9..052f96b 100644 --- a/astro-site/src/styles/global.css +++ b/astro-site/src/styles/global.css @@ -572,11 +572,6 @@ a:hover { color: var(--color-fg-muted); } -/* Mobile ToC — hidden on desktop, shown when sidebar disappears */ -.mobile-toc { - display: none; -} - /* Responsive */ @media (max-width: 960px) { .site-main { @@ -588,62 +583,6 @@ a:hover { .resize-handle { display: none; } - - .mobile-toc { - display: block; - margin-bottom: var(--space-lg); - border: 1px solid var(--color-border); - border-radius: 4px; - padding: 0; - position: sticky; - top: 0; - z-index: 10; - background: var(--color-bg); - } - - .mobile-toc summary { - padding: var(--space-sm) var(--space-md); - font-weight: 600; - font-size: var(--font-size-base); - color: var(--color-fg); - cursor: pointer; - list-style: none; - } - - .mobile-toc summary::after { - content: "\25B6"; - float: right; - font-size: 0.7em; - color: var(--color-fg-faint); - transition: transform 0.15s; - } - - .mobile-toc[open] summary::after { - transform: rotate(90deg); - } - - .mobile-toc summary::-webkit-details-marker { - display: none; - } - - .mobile-toc ul { - list-style: none; - padding: 0 var(--space-md) var(--space-sm); - } - - .mobile-toc li { - padding: var(--space-xs) 0; - } - - .mobile-toc a { - color: var(--color-link); - text-decoration: none; - font-size: var(--font-size-sm); - } - - .mobile-toc a:hover { - text-decoration: underline; - } } @media (max-width: 768px) { @@ -686,6 +625,28 @@ a:hover { background: rgba(255, 255, 255, 0.1); } + .nav-toc-label { + padding: var(--space-sm) var(--space-md); + font-size: var(--font-size-sm); + font-weight: 600; + color: rgba(255, 255, 255, 0.5); + text-transform: uppercase; + letter-spacing: 0.05em; + border-top: 1px solid rgba(255, 255, 255, 0.1); + } + + .nav-toc-link { + padding: var(--space-xs) var(--space-md) var(--space-xs) var(--space-lg); + font-size: var(--font-size-sm); + color: rgba(255, 255, 255, 0.8); + text-decoration: none; + display: block; + } + + .nav-toc-link:hover { + background: rgba(255, 255, 255, 0.1); + } + .content { padding: var(--space-lg) var(--space-md); } From c855fb991ba8db5cebfd950b9421b56b4f2f32f7 Mon Sep 17 00:00:00 2001 From: Branimir Georgiev Date: Sun, 26 Apr 2026 13:57:41 +0300 Subject: [PATCH 4/8] chore: add .claude/ to gitignore and remove from index Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/worktrees/agent-abc53eb2 | 1 - .gitignore | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) delete mode 160000 .claude/worktrees/agent-abc53eb2 diff --git a/.claude/worktrees/agent-abc53eb2 b/.claude/worktrees/agent-abc53eb2 deleted file mode 160000 index a9d1fae..0000000 --- a/.claude/worktrees/agent-abc53eb2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a9d1faee4bb53b601f763e5aff2d543242ec49ab diff --git a/.gitignore b/.gitignore index e3d97f6..24df554 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ dist/ # JetBrains IDE .idea/ +# Claude Code +.claude/ + # Unit test reports TEST*.xml From 62e4b8bfd32f763a4fa3d96e645cae438b748fbf Mon Sep 17 00:00:00 2001 From: Branimir Georgiev Date: Sun, 26 Apr 2026 14:00:57 +0300 Subject: [PATCH 5/8] 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) --- astro-site/src/components/Header.astro | 47 +++++++++++++------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/astro-site/src/components/Header.astro b/astro-site/src/components/Header.astro index 1f16342..6756895 100644 --- a/astro-site/src/components/Header.astro +++ b/astro-site/src/components/Header.astro @@ -92,29 +92,30 @@ const base = import.meta.env.BASE_URL; }); // Populate "On this page" section from the sidebar ToC - const sidebarToc = document.querySelector(".sidebar-toc"); - if (sidebarToc) { + document.addEventListener("DOMContentLoaded", () => { + const sidebarToc = document.querySelector(".sidebar-toc"); + if (!sidebarToc) return; const topLinks = sidebarToc.querySelectorAll(":scope > ul > li"); - if (topLinks.length > 0) { - const label = document.createElement("div"); - label.className = "nav-toc-label"; - label.textContent = "On this page"; - siteTabs.appendChild(label); + if (topLinks.length === 0) return; - topLinks.forEach((li) => { - const source = li.querySelector(":scope > a, :scope > details > summary > a"); - if (source) { - const link = document.createElement("a"); - link.href = source.getAttribute("href"); - link.textContent = source.textContent; - link.className = "nav-toc-link"; - link.addEventListener("click", () => { - navToggle.setAttribute("aria-expanded", "false"); - siteTabs.classList.remove("open"); - }); - siteTabs.appendChild(link); - } - }); - } - } + const label = document.createElement("div"); + label.className = "nav-toc-label"; + label.textContent = "On this page"; + siteTabs.appendChild(label); + + topLinks.forEach((li) => { + const source = li.querySelector(":scope > a, :scope > details > summary > a"); + if (source) { + const link = document.createElement("a"); + link.href = source.getAttribute("href"); + link.textContent = source.textContent; + link.className = "nav-toc-link"; + link.addEventListener("click", () => { + navToggle.setAttribute("aria-expanded", "false"); + siteTabs.classList.remove("open"); + }); + siteTabs.appendChild(link); + } + }); + }); From 97c671a048e36df240566c8a7825a673beb52f21 Mon Sep 17 00:00:00 2001 From: Branimir Georgiev Date: Sun, 26 Apr 2026 14:05:21 +0300 Subject: [PATCH 6/8] 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) --- astro-site/src/components/Header.astro | 27 ------- astro-site/src/components/MobileToc.astro | 58 +++++++++++++++ astro-site/src/layouts/DocLayout.astro | 2 + astro-site/src/styles/global.css | 89 +++++++++++++++++------ 4 files changed, 127 insertions(+), 49 deletions(-) create mode 100644 astro-site/src/components/MobileToc.astro diff --git a/astro-site/src/components/Header.astro b/astro-site/src/components/Header.astro index 6756895..b7bdf98 100644 --- a/astro-site/src/components/Header.astro +++ b/astro-site/src/components/Header.astro @@ -91,31 +91,4 @@ const base = import.meta.env.BASE_URL; siteTabs.classList.remove("open"); }); - // Populate "On this page" section from the sidebar ToC - document.addEventListener("DOMContentLoaded", () => { - const sidebarToc = document.querySelector(".sidebar-toc"); - if (!sidebarToc) return; - const topLinks = sidebarToc.querySelectorAll(":scope > ul > li"); - if (topLinks.length === 0) return; - - const label = document.createElement("div"); - label.className = "nav-toc-label"; - label.textContent = "On this page"; - siteTabs.appendChild(label); - - topLinks.forEach((li) => { - const source = li.querySelector(":scope > a, :scope > details > summary > a"); - if (source) { - const link = document.createElement("a"); - link.href = source.getAttribute("href"); - link.textContent = source.textContent; - link.className = "nav-toc-link"; - link.addEventListener("click", () => { - navToggle.setAttribute("aria-expanded", "false"); - siteTabs.classList.remove("open"); - }); - siteTabs.appendChild(link); - } - }); - }); diff --git a/astro-site/src/components/MobileToc.astro b/astro-site/src/components/MobileToc.astro new file mode 100644 index 0000000..0459b56 --- /dev/null +++ b/astro-site/src/components/MobileToc.astro @@ -0,0 +1,58 @@ +--- +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 && ( +
+ +
    + {h2s.map((h) => ( +
  • + {h.text} +
  • + ))} +
+
+)} + + diff --git a/astro-site/src/layouts/DocLayout.astro b/astro-site/src/layouts/DocLayout.astro index e9a11b6..bc3e5e2 100644 --- a/astro-site/src/layouts/DocLayout.astro +++ b/astro-site/src/layouts/DocLayout.astro @@ -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 { @@ -61,6 +62,7 @@ const ogImage = new URL("/tutorial-git/images/og-banner.png", Astro.site);
+
diff --git a/astro-site/src/styles/global.css b/astro-site/src/styles/global.css index 052f96b..e85b59e 100644 --- a/astro-site/src/styles/global.css +++ b/astro-site/src/styles/global.css @@ -572,8 +572,75 @@ a:hover { color: var(--color-fg-muted); } +/* Mobile ToC bar — hidden on desktop */ +.mobile-toc-bar { + display: none; +} + /* Responsive */ @media (max-width: 960px) { + .mobile-toc-bar { + display: block; + position: sticky; + top: 0; + z-index: 10; + background: var(--color-bg); + border-bottom: 1px solid var(--color-border); + } + + .mobile-toc-toggle { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: var(--space-sm) var(--space-md); + background: none; + border: none; + font-family: var(--font-text); + font-size: var(--font-size-base); + font-weight: 600; + color: var(--color-fg); + cursor: pointer; + } + + .mobile-toc-toggle:hover { + background: var(--color-bg-hover); + } + + .mobile-toc-chevron { + transition: transform 0.15s; + } + + .mobile-toc-toggle[aria-expanded="true"] .mobile-toc-chevron { + transform: rotate(180deg); + } + + .mobile-toc-list { + display: none; + list-style: none; + padding: 0 var(--space-md) var(--space-sm); + max-height: 60vh; + overflow-y: auto; + } + + .mobile-toc-list.open { + display: block; + } + + .mobile-toc-list li { + padding: var(--space-xs) 0; + } + + .mobile-toc-list a { + color: var(--color-link); + text-decoration: none; + font-size: var(--font-size-sm); + } + + .mobile-toc-list a:hover { + text-decoration: underline; + } + .site-main { grid-template-columns: 1fr; } @@ -625,28 +692,6 @@ a:hover { background: rgba(255, 255, 255, 0.1); } - .nav-toc-label { - padding: var(--space-sm) var(--space-md); - font-size: var(--font-size-sm); - font-weight: 600; - color: rgba(255, 255, 255, 0.5); - text-transform: uppercase; - letter-spacing: 0.05em; - border-top: 1px solid rgba(255, 255, 255, 0.1); - } - - .nav-toc-link { - padding: var(--space-xs) var(--space-md) var(--space-xs) var(--space-lg); - font-size: var(--font-size-sm); - color: rgba(255, 255, 255, 0.8); - text-decoration: none; - display: block; - } - - .nav-toc-link:hover { - background: rgba(255, 255, 255, 0.1); - } - .content { padding: var(--space-lg) var(--space-md); } From 30bb11a3afefb6fdfc89e451d674fe83f7bc07f0 Mon Sep 17 00:00:00 2001 From: Branimir Georgiev Date: Sun, 26 Apr 2026 14:07:18 +0300 Subject: [PATCH 7/8] 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) --- astro-site/src/layouts/DocLayout.astro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astro-site/src/layouts/DocLayout.astro b/astro-site/src/layouts/DocLayout.astro index bc3e5e2..7800be0 100644 --- a/astro-site/src/layouts/DocLayout.astro +++ b/astro-site/src/layouts/DocLayout.astro @@ -62,11 +62,11 @@ const ogImage = new URL("/tutorial-git/images/og-banner.png", Astro.site);
-
+

{title}

From 350f66b422ce91faab0937fe0b1b18c079448682 Mon Sep 17 00:00:00 2001 From: Branimir Georgiev Date: Sun, 26 Apr 2026 14:10:14 +0300 Subject: [PATCH 8/8] refactor: replace sticky bar with floating ToC button (approach C) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- astro-site/src/components/MobileToc.astro | 56 ++++++++------- astro-site/src/layouts/DocLayout.astro | 2 +- astro-site/src/styles/global.css | 86 +++++++++++++---------- 3 files changed, 77 insertions(+), 67 deletions(-) diff --git a/astro-site/src/components/MobileToc.astro b/astro-site/src/components/MobileToc.astro index 0459b56..3fd4566 100644 --- a/astro-site/src/components/MobileToc.astro +++ b/astro-site/src/components/MobileToc.astro @@ -10,49 +10,51 @@ const h2s = headings.filter((h) => h.depth === 2); --- {h2s.length > 0 && ( -
- -
    - {h2s.map((h) => ( -
  • - {h.text} -
  • - ))} -
+
+
On this page
+
    + {h2s.map((h) => ( +
  • + {h.text} +
  • + ))} +
+
)} diff --git a/astro-site/src/layouts/DocLayout.astro b/astro-site/src/layouts/DocLayout.astro index 7800be0..6eccdfd 100644 --- a/astro-site/src/layouts/DocLayout.astro +++ b/astro-site/src/layouts/DocLayout.astro @@ -66,13 +66,13 @@ const ogImage = new URL("/tutorial-git/images/og-banner.png", Astro.site);
-

{title}

+