Footer quotes + right-hand table of contents
- two static quotes above the name line in every footer - Toc component renders h1-h3 from Astro's render() headings as a sticky right column on >=88rem screens (hidden below); scroll-position highlighting via a small passive listener; shown only when a page has 2+ headings, so listings and short notes stay clean Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
---
|
||||
interface Props {
|
||||
headings: { depth: number; slug: string; text: string }[];
|
||||
}
|
||||
const { headings } = Astro.props;
|
||||
const shown = headings.filter((h) => h.depth <= 3);
|
||||
const min = Math.min(...shown.map((h) => h.depth));
|
||||
---
|
||||
|
||||
<nav class="toc">
|
||||
<p class="toc-title">On this page</p>
|
||||
<ul>
|
||||
{
|
||||
shown.map((h) => (
|
||||
<li class={`toc-d${h.depth - min}`}>
|
||||
<a href={'#' + h.slug}>{h.text}</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
// highlight the section currently in view
|
||||
const toc = document.querySelector('.toc');
|
||||
if (toc) {
|
||||
const links = [...toc.querySelectorAll('a[href^="#"]')] as HTMLAnchorElement[];
|
||||
const heads = links
|
||||
.map((a) => ({
|
||||
a,
|
||||
el: document.getElementById(decodeURIComponent(a.hash.slice(1))),
|
||||
}))
|
||||
.filter((x) => x.el);
|
||||
const update = () => {
|
||||
let cur = heads[0];
|
||||
for (const h of heads) {
|
||||
if (h.el!.getBoundingClientRect().top <= 110) cur = h;
|
||||
else break;
|
||||
}
|
||||
heads.forEach((h) => h.a.classList.toggle('toc-active', h === cur));
|
||||
};
|
||||
if (heads.length) {
|
||||
document.addEventListener('scroll', update, { passive: true });
|
||||
update();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+83
-1
@@ -3,14 +3,17 @@ import 'katex/dist/katex.min.css';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { buildNavTree } from '../lib/nav.mjs';
|
||||
import NavTree from '../components/NavTree.astro';
|
||||
import Toc from '../components/Toc.astro';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
headings?: { depth: number; slug: string; text: string }[];
|
||||
}
|
||||
const { title, description } = Astro.props;
|
||||
const { title, description, headings = [] } = Astro.props;
|
||||
const isHome = Astro.url.pathname === '/';
|
||||
const navTree = buildNavTree(await getCollection('garden'));
|
||||
const showToc = headings.filter((h) => h.depth <= 3).length >= 2;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
@@ -35,6 +38,13 @@ const navTree = buildNavTree(await getCollection('garden'));
|
||||
<slot />
|
||||
</main>
|
||||
<footer>
|
||||
<div class="footer-quotes">
|
||||
<p>
|
||||
Failure is unique among sin, in that we can turn it into a
|
||||
virtue when it drives us to succeed.
|
||||
</p>
|
||||
<p>The living diminish, but the machine endures...</p>
|
||||
</div>
|
||||
<p>
|
||||
Wesley Ray · <a href="https://blog.c0smere.net">blog</a> ·
|
||||
<a href="https://git.c0smere.net/wes">git</a> ·
|
||||
@@ -53,6 +63,13 @@ const navTree = buildNavTree(await getCollection('garden'));
|
||||
<p class="sidebar-title"><a href="/">the garden</a></p>
|
||||
<NavTree nodes={navTree} currentPath={Astro.url.pathname} />
|
||||
</aside>
|
||||
{
|
||||
showToc && (
|
||||
<aside class="toc-col">
|
||||
<Toc headings={headings} />
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -164,6 +181,10 @@ const navTree = buildNavTree(await getCollection('garden'));
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--faint);
|
||||
}
|
||||
/* TOC only exists as a right column on wide screens */
|
||||
.toc-col {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 72rem) {
|
||||
.layout {
|
||||
display: grid;
|
||||
@@ -184,6 +205,60 @@ const navTree = buildNavTree(await getCollection('garden'));
|
||||
padding: 1.5rem 0.5rem 1.5rem 0;
|
||||
}
|
||||
}
|
||||
@media (min-width: 88rem) {
|
||||
.layout {
|
||||
grid-template-columns: 17rem minmax(0, 44rem) 13rem;
|
||||
max-width: 80rem;
|
||||
}
|
||||
.toc-col {
|
||||
display: block;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
align-self: start;
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem 0 1.5rem 0.5rem;
|
||||
}
|
||||
}
|
||||
.toc-title {
|
||||
margin: 0 0 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.toc ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 0.8rem;
|
||||
border-left: 1px solid var(--faint);
|
||||
}
|
||||
.toc li {
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
.toc li a {
|
||||
display: block;
|
||||
padding-left: 0.75rem;
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
border-left: 2px solid transparent;
|
||||
margin-left: -1px;
|
||||
}
|
||||
.toc li.toc-d1 a {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
.toc li.toc-d2 a {
|
||||
padding-left: 2.25rem;
|
||||
}
|
||||
.toc li a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
.toc li a.toc-active {
|
||||
color: var(--accent);
|
||||
border-left-color: var(--accent);
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
scroll-margin-top: 1rem;
|
||||
}
|
||||
.sidebar-title {
|
||||
margin: 0 0 0.5rem;
|
||||
font-weight: 600;
|
||||
@@ -253,6 +328,13 @@ const navTree = buildNavTree(await getCollection('garden'));
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.footer-quotes {
|
||||
font-style: italic;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.footer-quotes p {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
.odo-display {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -59,10 +59,15 @@ const { entry, listing } = Astro.props;
|
||||
const title = entry
|
||||
? (entry.data.title ?? entry.id.split('/').pop())
|
||||
: listing.name;
|
||||
const Content = entry ? (await render(entry)).Content : null;
|
||||
const rendered = entry ? await render(entry) : null;
|
||||
const Content = rendered?.Content ?? null;
|
||||
---
|
||||
|
||||
<Base title={title} description={entry?.data.description}>
|
||||
<Base
|
||||
title={title}
|
||||
description={entry?.data.description}
|
||||
headings={rendered?.headings ?? []}
|
||||
>
|
||||
<h1>{title}</h1>
|
||||
{Content && <Content />}
|
||||
{
|
||||
|
||||
@@ -4,10 +4,15 @@ import Base from '../layouts/Base.astro';
|
||||
|
||||
const entries = await getCollection('garden');
|
||||
const home = entries.find((e) => e.id === 'index');
|
||||
const Content = home ? (await render(home)).Content : null;
|
||||
const rendered = home ? await render(home) : null;
|
||||
const Content = rendered?.Content ?? null;
|
||||
const title = home?.data.title ?? 'Wesley Ray';
|
||||
---
|
||||
|
||||
<Base title={title} description={home?.data.description}>
|
||||
<Base
|
||||
title={title}
|
||||
description={home?.data.description}
|
||||
headings={rendered?.headings ?? []}
|
||||
>
|
||||
{Content && <Content />}
|
||||
</Base>
|
||||
|
||||
Reference in New Issue
Block a user