import { useState, useEffect } from "react"; import { createStyles, Box, Text, Group, rem, useMantineTheme, } from "@mantine/core"; import { Icon, ICONS } from "vseth-canine-ui"; import { getAccentColor } from "../utilities/colors"; const useStyles = createStyles((theme) => ({ link: { ...theme.fn.focusStyles(), display: "block", textDecoration: "none", color: theme.colorScheme === "dark" ? theme.colors.dark[0] : theme.black, lineHeight: 1.2, fontSize: theme.fontSizes.sm, padding: "5px", borderTopRightRadius: theme.radius.lg, borderBottomRightRadius: theme.radius.lg, borderLeft: `2px solid ${ theme.colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[3] }`, "&:hover": { backgroundColor: theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[0], }, }, linkActive: { fontWeight: 1000, borderLeftColor: theme.colors[theme.primaryColor][theme.colorScheme === "dark" ? 6 : 7], color: theme.colors[theme.primaryColor][theme.colorScheme === "dark" ? 2 : 7], "&, &:hover": { backgroundColor: theme.colorScheme === "dark" ? theme.fn.rgba(theme.colors[theme.primaryColor][9], 0.25) : theme.colors[theme.primaryColor][0], }, }, })); export default function TOC() { const [items, setItems] = useState([]); const [active, setActive] = useState(""); const theme = useMantineTheme(); const { classes, cx } = useStyles(); const findClosestHeading = () => { const headings = document.querySelectorAll("h1, h2, h3"); const currentPosition = window.scrollY; let closestHeading = null; let closestDistance = Infinity; headings.forEach((heading) => { const headingPosition = heading.getBoundingClientRect().top + window.scrollY; const distance = Math.abs(currentPosition - headingPosition); if (distance < closestDistance) { closestHeading = heading; closestDistance = distance; } }); return closestHeading; }; useEffect(() => { const headings = [...document.querySelectorAll("h1, h2, h3")]; const links = headings.map((item) => ({ link: "#" + item.id, label: item.innerText, order: Number(item.localName[item.localName.length - 1]), })); setItems(links); setActive("#" + findClosestHeading().id); document.addEventListener("scroll", function (e) { setActive("#" + findClosestHeading().id); }); }, []); const entries = items.map((item) => ( <Box component="a" href={item.link} key={item.label} className={cx(classes.link, { [classes.linkActive]: active === item.link, })} sx={(theme) => ({ paddingLeft: `calc(${item.order} * ${theme.spacing.md}px)`, })} > {item.label} </Box> )); return ( <div style={{ position: "sticky", top: "2rem", maxHeight: "calc(100vh - 2rem)", overflowY: "auto", marginBottom: "20px", }} > <Group mb="md"> <Icon icon={ICONS.LIST} color={getAccentColor(theme)} /> <Text>Table of contents</Text> </Group> {entries} </div> ); }