Newer
Older
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";
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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>
);
}