-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expand file tree
/
Copy pathtable-of-contents.tsx
More file actions
135 lines (121 loc) · 4.05 KB
/
table-of-contents.tsx
File metadata and controls
135 lines (121 loc) · 4.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
124
125
126
127
128
129
130
131
132
133
134
135
// todo: paste table of contents from Nextra and customize styles
import { clsx } from "clsx"
import { type Heading } from "nextra"
import { removeLinks } from "nextra/remove-links"
import { useActiveAnchor, useThemeConfig } from "nextra-theme-docs"
import { useEffect, useRef, type ReactElement } from "react"
import scrollIntoView from "scroll-into-view-if-needed"
import { Anchor } from "@/app/conf/_design-system/anchor"
import { renderComponent } from "./utils/render-component"
import { BackToTop } from "./back-to-top"
export type TableOfContentsProps = {
toc: Heading[]
filePath: string
}
const linkClassName = clsx(
"text-xs",
"text-neu-700",
"hover:text-neu-800 hover:underline",
)
export function TableOfContents({
toc,
filePath,
}: TableOfContentsProps): ReactElement {
const activeAnchor = useActiveAnchor()
const tocRef = useRef<HTMLUListElement>(null)
const themeConfig = useThemeConfig()
const hasHeadings = toc.length > 0
const hasMetaInfo = Boolean(
themeConfig.feedback.content ||
themeConfig.editLink.component ||
themeConfig.toc.extraContent ||
themeConfig.toc.backToTop,
)
const activeSlug = Object.entries(activeAnchor).find(
([, { isActive }]) => isActive,
)?.[0]
const activeIndex = toc.findIndex(({ id }) => id === activeSlug)
useEffect(() => {
if (!activeSlug) return
const anchor = tocRef.current?.querySelector(`a[href="#${activeSlug}"]`)
if (anchor) {
scrollIntoView(anchor, {
behavior: "smooth",
block: "center",
inline: "center",
scrollMode: "always",
boundary: tocRef.current!.parentElement,
})
}
}, [activeSlug])
return (
<div
className={clsx(
"nextra-scrollbar sticky top-16 overflow-y-auto px-4 pt-6 text-sm [hyphens:auto]",
"max-h-[calc(100vh-var(--nextra-navbar-height)-env(safe-area-inset-bottom))] ltr:-mr-4 rtl:-ml-4",
)}
>
{hasHeadings && (
<>
<p className="typography-menu mb-4 text-xs">
{renderComponent(themeConfig.toc.title)}
</p>
<ul ref={tocRef}>
{toc.map(({ id, value, depth }) => (
<li className="_my-2 _scroll-my-6 _scroll-py-6" key={id}>
<a
href={`#${id}`}
className={clsx(
"gql-focus-visible break-words text-neu-700 hover:text-neu-800 hover:underline contrast-more:text-neu-900",
{
2: "",
3: "ltr:ml-4 rtl:mr-4",
4: "ltr:ml-8 rtl:mr-8",
5: "ltr:ml-12 rtl:mr-12",
6: "ltr:ml-16 rtl:mr-16",
}[depth],
"block",
activeAnchor[id]?.isActive
? "text-pri-base contrast-more:!text-pri-base"
: "",
)}
>
{removeLinks(value)}
</a>
</li>
))}
</ul>
</>
)}
{hasMetaInfo && (
<div
className={clsx(
hasHeadings && "nextra-toc-footer mt-8 pt-8",
"sticky bottom-0 flex flex-col items-start gap-2 pb-8",
"-mx-1 px-1", // to hide focused toc links
)}
>
{themeConfig.feedback.content ? (
<Anchor
className={linkClassName}
href={themeConfig.feedback.useLink()}
>
{renderComponent(themeConfig.feedback.content)}
</Anchor>
) : null}
{renderComponent(themeConfig.editLink.component, {
filePath,
className: linkClassName,
children: renderComponent(themeConfig.editLink.content),
})}
{renderComponent(themeConfig.toc.extraContent)}
{themeConfig.toc.backToTop && (
<BackToTop className={linkClassName} hidden={activeIndex < 2}>
{renderComponent(themeConfig.toc.backToTop)}
</BackToTop>
)}
</div>
)}
</div>
)
}