"use client"; import { Fragment, useEffect, useId, useState, type ReactNode } from "react"; import "./footnote-faq.css"; /** * Footnote FAQ * The questions people ask, answered in a few honest sentences of prose. * The phrases a reader would want to check carry footnote numbers; opening * one sets its answer as a note right under the paragraph, without leaving * the text. An index of every question sits underneath for people who scan. */ export type Note = { text: string; q: string; a: ReactNode; id?: string }; export type Paragraph = (string | Note)[]; type Props = { paragraphs: Paragraph[]; /** Prefix for deep links: #faq-2 opens note 2. */ anchor?: string; theme?: "paper" | "night"; motion?: "full" | "reduced"; className?: string; }; export function FootnoteFaq({ paragraphs, anchor = "faq", theme = "paper", motion = "full", className = "" }: Props) { const uid = useId(); // Number the notes in reading order. const notes: (Note & { n: number; p: number })[] = []; paragraphs.forEach((para, p) => para.forEach((part) => typeof part !== "string" && notes.push({ ...part, n: notes.length + 1, p }))); const [open, setOpen] = useState([]); useEffect(() => { const fromHash = () => { const m = window.location.hash.match(new RegExp(`^#${anchor}-(\\d+)$`)); if (m) setOpen((o) => (o.includes(+m[1]) ? o : [...o, +m[1]])); }; fromHash(); window.addEventListener("hashchange", fromHash); return () => window.removeEventListener("hashchange", fromHash); }, [anchor]); const toggle = (n: number) => setOpen((o) => (o.includes(n) ? o.filter((x) => x !== n) : [...o, n])); const reveal = (n: number) => { setOpen((o) => (o.includes(n) ? o : [...o, n])); requestAnimationFrame(() => document.getElementById(`${uid}-mark-${n}`)?.focus({ preventScroll: false })); }; let counter = 0; return (
{paragraphs.map((para, p) => { const here = notes.filter((x) => x.p === p); return (

{para.map((part, i) => { if (typeof part === "string") return {part}; const n = ++counter; const isOpen = open.includes(n); return ( // A span, not a

))} )} ); })} ); }