Something went wrong. Try again.
Identities for entities did.bot
agent llm did
Something went wrong. Try again.
5.8 kB · 180 lines
Astro
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181---// One subpage of a section (see src/data/section.ts): its number and title,// its lede, its picture, its paragraphs, and the navigation out of it.// Shared by /features/, /architecture/ and /about/'s [slug] routes.//// Position in the section's list is the only source of the number and of// what "previous" and "next" mean, so reordering the data file reorders and// renumbers the pages with it.//// The picture slot takes either an `image` (a file, through astro:assets,// sized from the import so the page does not reflow) or a `diagram`// (mermaid, drawn in the browser). A subpage sets one or the other.//// The breadcrumb is the section's own title behind an arrow and inside// square brackets — `[< about]` — not a sentence built around the title:// any phrasing that reads for one section ("all features") comes out wrong// for the next ("all about"). Square brackets, deliberately not the// guillemet-brackets `⟦…⟧`, which mean "unwritten filler" everywhere else// on this site (see src/data/filler.ts).import { Image } from "astro:assets";import Diagram from "./Diagram.astro";import type { Section, Subpage } from "../data/section";
interface Props { page: Subpage; section: Section; /** The section's URL prefix, with both slashes, e.g. "/features/". */ base: string;}
const { page, section, base } = Astro.props;
const index = section.pages.findIndex((entry) => entry.slug === page.slug);const previous = index > 0 ? section.pages[index - 1] : undefined;const next = index < section.pages.length - 1 ? section.pages[index + 1] : undefined;---<h1><span class="ordinal">{index + 1}.</span> {page.title}</h1><p class="lede">{page.lede}</p>{page.image && ( <figure class="subpage-figure"> <Image src={page.image.src} alt={page.image.alt} widths={[640, 1200]} sizes="(min-width: 44rem) 40rem, 100vw" /> </figure>)}{page.diagram && <Diagram diagram={page.diagram} />}{page.body.map((paragraph) => <p>{paragraph}</p>)}
<nav class="subpage-nav" aria-label={`within ${section.title}`}> {previous && ( <a class="step prev" href={`${base}${previous.slug}/`} rel="prev"> <svg class="chev" viewBox="0 0 32 72" aria-hidden="true" focusable="false"> <path d="M23 6 L9 36 L23 66" /> </svg> <span class="step-label"> <span class="ordinal">{index}.</span> {previous.title} </span> </a> )} {next && ( <a class="step next" href={`${base}${next.slug}/`} rel="next"> <svg class="chev" viewBox="0 0 32 72" aria-hidden="true" focusable="false"> <path d="M9 6 L23 36 L9 66" /> </svg> <span class="step-label"> <span class="ordinal">{index + 2}.</span> {next.title} </span> </a> )}</nav>
<ul class="related-links"> <li><a href={base}>[← {section.title}]</a></li></ul>
<style> .ordinal { color: var(--ink-dim); }
/* The step arrows: two big, mostly-transparent chevrons in the gutters either side of the measure, in the same register as the landing page's own #scroll-hint arrow — accent-coloured, low-contrast until wanted, and animated only where motion is allowed. They are `position: fixed` so they stay reachable the whole way down a long subpage rather than waiting at the bottom of it.
Each arrow's label (the neighbouring page's number and title) is in the document at all times and only visually faded, so it is the link's accessible name for a screen reader and appears on hover or keyboard focus for everyone else. */ .step { position: fixed; top: 50%; z-index: 5; display: flex; flex-direction: column; align-items: center; gap: 6px; text-decoration: none; color: var(--accent); opacity: 0.22; transition: opacity 260ms ease; } .step.prev { left: clamp(8px, calc(50% - 24rem), 4vw); transform: translateY(-50%); } .step.next { right: clamp(8px, calc(50% - 24rem), 4vw); transform: translateY(-50%); } .step:hover, .step:focus-visible { opacity: 0.92; }
.chev { width: 34px; height: 76px; fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; } /* A slow lean toward the page it leads to, the horizontal cousin of the landing page's `bob`. */ .step.prev .chev { animation: lean-left 2.4s ease-in-out infinite; } .step.next .chev { animation: lean-right 2.4s ease-in-out infinite; } @keyframes lean-left { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(-5px); } } @keyframes lean-right { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(5px); } }
.step-label { max-width: 9rem; text-align: center; font-size: 0.8em; line-height: 1.4; color: var(--ink); opacity: 0; transition: opacity 260ms ease; } .step:hover .step-label, .step:focus-visible .step-label { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .chev { animation: none; } }
/* Below the width where a gutter exists, the arrows stop being furniture in the margin and become an ordinary pair of links under the text. */ @media (max-width: 62rem) { .subpage-nav { display: flex; justify-content: space-between; gap: 12px; margin: 40px 0 24px; } .step { position: static; flex-direction: row; align-items: center; transform: none; opacity: 0.85; padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); } .step.next { flex-direction: row-reverse; } .chev { width: 18px; height: 40px; animation: none; } .step-label { opacity: 1; max-width: none; text-align: left; } }</style>