@@ -117,9 +117,10 @@ function renderInlineCode(input: string) {
117117 <h1 class =" mb-0 text-[40px] leading-[1.1] font-semibold tracking-tight" >
118118 <span set:html ={ renderInlineCode (post .data .title )} />
119119 </h1 >
120- <div class =" text-base text-zinc-600 dark:text-zinc-400" >{ formattedDate } </div >
121- { hasReadTime && (
122- <div class = " flex items-center gap-3 text-sm text-zinc-500 dark:text-zinc-400" >
120+
121+ <div class =" mt-5 flex flex-wrap items-center justify-between gap-4 text-sm text-zinc-600 dark:text-zinc-400" >
122+ <div class =" flex flex-wrap items-center gap-3" >
123+ <span >{ formattedDate } </span >
123124 { hasReadTime && (
124125 <span class = " inline-flex items-center gap-1" >
125126 <svg
@@ -135,16 +136,57 @@ function renderInlineCode(input: string) {
135136 <circle cx = " 12" cy = " 12" r = " 10" />
136137 <path d = " M12 6v6l4 2" />
137138 </svg >
138- <span >{ readTime } min </span >
139+ <span >{ readTime } minutes </span >
139140 </span >
140141 )}
141142 </div >
142- )}
143- { hasReadTime && (
144- <div class = " pt-3" >
145- <div class = " h-px w-full bg-zinc-200 dark:bg-zinc-800" ></div >
143+
144+ <div class =" flex items-center gap-2" >
145+ <span class =" hidden sm:inline" >Share:</span >
146+ <button
147+ type =" button"
148+ data-share =" x"
149+ class =" grid h-8 w-8 place-items-center rounded-full border border-zinc-200 text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-900"
150+ aria-label =" Share on X"
151+ >
152+ <svg viewBox =" 0 0 24 24" class =" h-4 w-4" aria-hidden =" true" fill =" currentColor" >
153+ <path
154+ d =" M18.9 2H22l-6.8 7.8L23 22h-6.6l-5.2-6.7L5.3 22H2l7.4-8.5L1 2h6.7l4.7 6.1L18.9 2zm-1.1 18h1.7L6.8 4H5L17.8 20z"
155+ />
156+ </svg >
157+ </button >
158+
159+ <button
160+ type =" button"
161+ data-share =" facebook"
162+ class =" grid h-8 w-8 place-items-center rounded-full border border-zinc-200 text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-900"
163+ aria-label =" Share on Facebook"
164+ >
165+ <svg viewBox =" 0 0 24 24" class =" h-4 w-4" aria-hidden =" true" fill =" currentColor" >
166+ <path
167+ d =" M13.5 22v-8h2.7l.4-3h-3.1V9.1c0-.9.3-1.5 1.6-1.5h1.7V5c-.3 0-1.5-.1-2.8-.1-2.8 0-4.6 1.7-4.6 4.8V11H7v3h2.7v8h3.8z"
168+ />
169+ </svg >
170+ </button >
171+
172+ <button
173+ type =" button"
174+ data-share =" linkedin"
175+ class =" grid h-8 w-8 place-items-center rounded-full border border-zinc-200 text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-900"
176+ aria-label =" Share on LinkedIn"
177+ >
178+ <svg viewBox =" 0 0 24 24" class =" h-4 w-4" aria-hidden =" true" fill =" currentColor" >
179+ <path
180+ d =" M20.5 3h-17C2.7 3 2 3.7 2 4.5v17c0 .8.7 1.5 1.5 1.5h17c.8 0 1.5-.7 1.5-1.5v-17c0-.8-.7-1.5-1.5-1.5zM8 20H5v-9h3v9zM6.5 9.8C5.5 9.8 4.7 9 4.7 8s.8-1.8 1.8-1.8 1.8.8 1.8 1.8-.8 1.8-1.8 1.8zM20 20h-3v-4.9c0-1.2 0-2.7-1.7-2.7s-2 1.3-2 2.6V20h-3v-9h2.9v1.2h.1c.4-.8 1.4-1.7 2.9-1.7 3.1 0 3.7 2 3.7 4.7V20z"
181+ />
182+ </svg >
183+ </button >
146184 </div >
147- )}
185+ </div >
186+
187+ <div class =" pt-3" >
188+ <div class =" h-px w-full bg-zinc-200 dark:bg-zinc-800" ></div >
189+ </div >
148190 </div >
149191 <Content />
150192 </article >
@@ -177,7 +219,114 @@ function renderInlineCode(input: string) {
177219 }
178220 };
179221
222+ const setupShare = () => {
223+ const buttons = document.querySelectorAll('button[data-share]');
224+ if (!buttons.length) return;
225+
226+ const getShareUrl = (provider, pageUrl) => {
227+ const u = encodeURIComponent(pageUrl);
228+ switch (provider) {
229+ case 'x':
230+ return `https://twitter.com/intent/tweet?url=${u}`;
231+ case 'facebook':
232+ return `https://www.facebook.com/sharer/sharer.php?u=${u}`;
233+ case 'linkedin':
234+ return `https://www.linkedin.com/sharing/share-offsite/?url=${u}`;
235+ default:
236+ return '';
237+ }
238+ };
239+
240+ buttons.forEach((btn) => {
241+ btn.addEventListener('click', async () => {
242+ if (!(btn instanceof HTMLElement)) return;
243+ const provider = btn.getAttribute('data-share');
244+ const pageUrl = window.location.href;
245+ const shareUrl = getShareUrl(provider, pageUrl);
246+ if (!shareUrl) {
247+ const ok = await copyText(pageUrl);
248+ if (ok && typeof window.__toast === 'function') window.__toast('Copied link');
249+ return;
250+ }
251+ window.open(shareUrl, '_blank', 'noopener,noreferrer');
252+ });
253+ });
254+ };
255+
256+ const setupTocScrollSpy = () => {
257+ const aside = document.querySelector('aside[data-toc]');
258+ if (!aside) return;
259+
260+ const links = Array.from(aside.querySelectorAll('a[data-toc-link]'));
261+ if (!links.length) return;
262+
263+ const map = new Map();
264+ for (const a of links) {
265+ const id = a.getAttribute('data-toc-link');
266+ if (id) map.set(id, a);
267+ }
268+
269+ const headings = Array.from(
270+ document.querySelectorAll('article.prose h2[id], article.prose h3[id]')
271+ ).filter((h) => map.has(h.getAttribute('id')));
272+
273+ if (!headings.length) return;
274+
275+ let activeId = '';
276+
277+ const setActive = (id) => {
278+ if (!id || id === activeId) return;
279+ const prev = activeId ? map.get(activeId) : null;
280+ if (prev) {
281+ prev.removeAttribute('data-active');
282+ prev.removeAttribute('aria-current');
283+ }
284+ const next = map.get(id);
285+ if (next) {
286+ next.setAttribute('data-active', 'true');
287+ next.setAttribute('aria-current', 'true');
288+ activeId = id;
289+ }
290+ };
291+
292+ const computeActive = () => {
293+ const offset = 140;
294+ let current = headings[0].getAttribute('id');
295+ for (const h of headings) {
296+ const top = h.getBoundingClientRect().top;
297+ if (top - offset <= 0) current = h.getAttribute('id');
298+ else break;
299+ }
300+ if (current) setActive(current);
301+ };
302+
303+ let ticking = false;
304+ const onScroll = () => {
305+ if (ticking) return;
306+ ticking = true;
307+ window.requestAnimationFrame(() => {
308+ computeActive();
309+ ticking = false;
310+ });
311+ };
312+
313+ window.addEventListener('scroll', onScroll, { passive: true });
314+ window.addEventListener('resize', onScroll);
315+ computeActive();
316+
317+ for (const a of links) {
318+ a.addEventListener('click', () => {
319+ const id = a.getAttribute('data-toc-link');
320+ if (!id) return;
321+ window.setTimeout(() => setActive(id), 0);
322+ });
323+ }
324+ };
325+
180326 const init = () => {
327+ setupShare();
328+ setupTocScrollSpy();
329+
181330 const root = document.querySelector('article.prose');
182331 if (!root) return;
183332
@@ -212,16 +361,18 @@ function renderInlineCode(input: string) {
212361 })();
213362 </script >
214363
215- <aside class =" hidden lg:block" >
216- <div class =" sticky top-24 max-h-[calc(100vh-8rem)] overflow-auto border-l border-zinc-200 pl-6 dark:border-zinc-800" >
217- <div class =" text-sm font-semibold text-zinc-700 dark:text-zinc-50" >Table of content</div >
364+ <aside class =" hidden lg:block" data-toc >
365+ <div class =" sticky top-24 max-h-[calc(100vh-8rem)] overflow-auto" >
366+ <div class =" text-[16px] font-semibold text-zinc-700 dark:text-zinc-50" >Table of Contents</div >
367+ <div class =" mt-3 h-px w-full bg-zinc-200 dark:bg-zinc-800" ></div >
218368 { toc .length > 0 ? (
219- <nav class = " mt-3 " >
220- <ul class = " space-y-2 text-sm" >
369+ <nav class = " mt-4 " >
370+ <ul class = " space-y-1 text-sm" >
221371 { toc .map ((h ) => (
222372 <li class = { h .depth === 3 ? ' pl-4' : ' ' } >
223373 <a
224- class = " group flex items-start gap-2 no-underline text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white"
374+ data-toc-link = { h .slug }
375+ class = " toc-link block rounded-lg px-3 py-2 no-underline text-zinc-700 transition-colors dark:text-zinc-300"
225376 href = { ` #${h .slug } ` }
226377 >
227378 <span class = " leading-snug" >{ h .text } </span >
@@ -236,4 +387,15 @@ function renderInlineCode(input: string) {
236387 </div >
237388 </aside >
238389 </div >
390+
391+ <style is:inline >
392+ a.toc-link:hover {
393+ background: rgb(var(--brand-primary-rgb) / 0.09);
394+ color: rgb(var(--brand-link-rgb));
395+ }
396+ a.toc-link[data-active='true'] {
397+ background: rgb(var(--brand-primary-rgb) / 0.14);
398+ color: rgb(var(--brand-link-rgb));
399+ }
400+ </style >
239401</BaseLayout >
0 commit comments