@@ -3,40 +3,183 @@ import Layout from "@layouts/Layout.astro";
33import Section from " @ui/Section.astro" ;
44import Prose from " @components/prose/prose.astro" ;
55import TwoCols from " @components/TwoCols.astro" ;
6- import { marked } from ' marked' ;
76
87import " ../styles/markdown.css" ;
98
109const { title, description, toc= false } = Astro .props ;
1110
12- import remarkToc from ' remark-toc'
13-
14- // const html0 = marked.parse(Astro.slots.render);
15- const html = await Astro .slots .render (' default' );
16-
17- // html.use(remarkToc)
18- // .process(await read('src/content/pages/childcare.mdx'))
19-
2011---
2112
2213<Layout title ={ title } description ={ description } >
2314 <Section >
24- { toc ?
25- <TwoCols >
26- <Fragment slot = " content" >
27- <Prose class = " pb-20" >
28- <slot />
29- </Prose >
30- </Fragment >
31- <Fragment slot = " sidebar" >
32- <article set :html = { html } />
33- </Fragment >
34- </TwoCols >
35- :
36- <Prose class = " pb-20" >
37- <slot />
38- </Prose >
39- }
40-
15+ {
16+ toc ? (
17+ <TwoCols >
18+ <Fragment slot = " content" >
19+ <div
20+ id = " toc"
21+ class = " mt-24 mb-6 last:mb-0 rounded-lg shadow-md bg-white p-6"
22+ >
23+ <span class = " text-lg font-bold" >Table of contents</span >
24+ </div >
25+ </Fragment >
26+ <Fragment slot = " sidebar" >
27+ <slot />
28+ </Fragment >
29+ </TwoCols >
30+ ) : (
31+ <slot />
32+ )
33+ }
4134 </Section >
4235</Layout >
36+
37+ <script is:inline >
38+ document.addEventListener("DOMContentLoaded", () => {
39+ const tocContainer = document.getElementById("toc");
40+ if (!tocContainer) return;
41+
42+ const headings = Array.from(
43+ document.querySelectorAll(
44+ "article h2, article h3, article h4, article summary span:nth-child(1)",
45+ ),
46+ );
47+ if (!headings.length) return;
48+
49+ const rootUl = document.createElement("ul");
50+ let currentUl = rootUl;
51+ let lastLevel = 2;
52+ const parents = [rootUl];
53+
54+ headings.forEach((heading, index) => {
55+ if (!heading.id) {
56+ heading.id = `heading-${index}`;
57+ }
58+
59+ const level = parseInt(heading.tagName[1], 10);
60+ const li = document.createElement("li");
61+ const a = document.createElement("a");
62+ a.href = `#${heading.id}`;
63+ a.textContent = heading.textContent;
64+ li.appendChild(a);
65+
66+ if (level > lastLevel) {
67+ const newUl = document.createElement("ul");
68+ parents[parents.length - 1].lastElementChild?.appendChild(newUl);
69+ parents.push(newUl);
70+ } else if (level < lastLevel) {
71+ parents.splice(-(lastLevel - level));
72+ }
73+
74+ currentUl = parents[parents.length - 1];
75+ currentUl.appendChild(li);
76+ lastLevel = level;
77+ });
78+
79+ // Add "Back to top" link
80+ const backToTop = document.createElement("li");
81+ const topLink = document.createElement("a");
82+ topLink.href = "#";
83+ topLink.textContent = "↑ Back to top";
84+
85+ topLink.classList.add("hidden");
86+ topLink.classList.add("lg:block");
87+ topLink.style.marginTop = "1em";
88+ backToTop.appendChild(topLink);
89+ rootUl.appendChild(backToTop);
90+
91+ tocContainer.appendChild(rootUl);
92+
93+ // Scroll spy: highlight active link
94+ const observer = new IntersectionObserver(
95+ (entries) => {
96+ entries.forEach((entry) => {
97+ const id = entry.target.id;
98+ const tocLink = tocContainer.querySelector(`a[href="#${id}"]`);
99+ if (tocLink) {
100+ if (entry.isIntersecting) {
101+ tocContainer
102+ .querySelectorAll("a")
103+ .forEach((a) => a.classList.remove("active"));
104+ tocLink.classList.add("active");
105+ }
106+ }
107+ });
108+ },
109+ {
110+ rootMargin: "-10% 0px -60% 0px",
111+ threshold: 0,
112+ },
113+ );
114+
115+ headings.forEach((heading) => observer.observe(heading));
116+ });
117+ </script >
118+
119+ <style is:global >
120+ #toc {
121+ font-size: 0.95rem;
122+ line-height: 1.5;
123+ padding: 1rem;
124+ border-left: 1px solid #e0e0e0;
125+ position: sticky;
126+ top: 1rem;
127+ }
128+
129+ #toc ul {
130+ list-style: none;
131+ padding-left: 0;
132+ margin: 0;
133+ }
134+
135+ #toc li {
136+ margin: 0.25em 0;
137+ }
138+
139+ #toc li ul {
140+ margin-left: 1em;
141+ border-left: 1px dashed #ddd;
142+ padding-left: 0.75em;
143+ }
144+
145+ #toc a {
146+ text-decoration: none;
147+ color: #333;
148+ padding: 0.25em 0.5em;
149+ transition:
150+ background 0.2s ease,
151+ color 0.2s ease;
152+ }
153+
154+ #toc a:hover {
155+ background: #f0f0f0;
156+ color: #007acc;
157+ }
158+
159+ #toc a.active {
160+ font-weight: 600;
161+ color: #007acc;
162+ border-left: 3px solid #007acc;
163+ background: rgba(0, 122, 204, 0.1);
164+ padding-left: 0.5em;
165+ }
166+
167+ #toc a[href="#"] {
168+ font-size: 0.9em;
169+ color: #555;
170+ margin-top: 0.5em;
171+ }
172+
173+ #toc a[href="#"]:hover {
174+ color: #007acc;
175+ text-decoration: underline;
176+ }
177+
178+
179+ article h2,
180+ article h3,
181+ article h4,
182+ article summary span:nth-child(1) {
183+ scroll-margin-top: 120px;
184+ }
185+ </style >
0 commit comments