toc.tsx148 lines · main
1'use client'
2
3import { Fragment, useRef, type ComponentProps, type HTMLAttributes, type ReactNode } from 'react'
4import { cn, ScrollArea, ScrollViewport } from 'ui'
5import { removeAnchor } from 'ui/src/components/CustomHTMLElements/CustomHTMLElements.utils'
6
7import type { TOCItemType } from './server/get-toc'
8import * as Primitive from './toc-primitive'
9import { TocThumb } from './toc-thumb'
10
11export interface TOCProps {
12 /**
13 * Custom content in TOC container, before the main TOC
14 */
15 header?: ReactNode
16 /**
17 * Custom content in TOC container, after the main TOC
18 */
19 footer?: ReactNode
20 children: ReactNode
21}
22
23export function Toc(props: HTMLAttributes<HTMLDivElement>) {
24 return (
25 <div
26 id="toc"
27 {...props}
28 className={cn('sticky top-(--header-height) h-fit max-md:hidden', props.className)}
29 style={
30 {
31 ...props.style,
32 } as object
33 }
34 >
35 <div className="flex h-fit w-(--toc-width) max-w-full flex-col gap-3 pe-4">
36 {props.children}
37 </div>
38 </div>
39 )
40}
41
42export function TOCScrollArea({
43 isMenu,
44 ...props
45}: ComponentProps<typeof ScrollArea> & { isMenu?: boolean }) {
46 const viewRef = useRef<HTMLDivElement>(null)
47
48 return (
49 <ScrollArea {...props} className={cn('flex flex-col ps-px', props.className)}>
50 <Primitive.ScrollProvider containerRef={viewRef}>
51 <ScrollViewport
52 className={cn('relative min-h-0 text-sm', isMenu && 'mt-2 mb-4 mx-4 md:mx-6')}
53 ref={viewRef}
54 >
55 {props.children}
56 </ScrollViewport>
57 </Primitive.ScrollProvider>
58 </ScrollArea>
59 )
60}
61
62export function TOCItems({
63 items,
64 showTrack = false,
65}: {
66 items: TOCItemType[]
67 showTrack?: boolean
68}) {
69 const containerRef = useRef<HTMLDivElement>(null)
70
71 if (items.length === 0) return null
72
73 return (
74 <>
75 <TocThumb
76 containerRef={containerRef}
77 className="absolute inset-s-0 mt-(--toc-top) h-(--toc-height) w-px bg-foreground transition-all"
78 />
79 <div
80 ref={containerRef}
81 className={cn(
82 'list-none text-[0.8rem] flex flex-col pl-[calc(0.75rem+5px)] border-foreground/10',
83 showTrack && 'border-s'
84 )}
85 >
86 {items.map((item) => (
87 <TOCItem key={item.url} item={item} />
88 ))}
89 </div>
90 </>
91 )
92}
93
94const formatSlug = (slug: string) => {
95 // [Joshen] We will still provide support for headers declared like this:
96 // ## REST API {#rest-api-overview}
97 // At least for now, this was a docusaurus thing.
98 if (slug.includes('#')) return slug.split('#')[1]
99 return slug
100}
101
102function formatTOCHeader(content: string) {
103 let insideInlineCode = false
104 const res: Array<{ type: 'text'; value: string } | { type: 'code'; value: string }> = []
105
106 for (const x of content) {
107 if (x === '`') {
108 if (!insideInlineCode) {
109 insideInlineCode = true
110 res.push({ type: 'code', value: '' })
111 } else {
112 insideInlineCode = false
113 }
114 } else {
115 if (insideInlineCode) {
116 res[res.length - 1].value += x
117 } else {
118 if (res.length === 0 || res[res.length - 1].type === 'code') {
119 res.push({ type: 'text', value: x })
120 } else {
121 res[res.length - 1].value += x
122 }
123 }
124 }
125 }
126
127 return res
128}
129
130function TOCItem({ item }: { item: TOCItemType }) {
131 return (
132 <Primitive.TOCItem
133 href={`#${formatSlug(item.url)}`}
134 className={cn(
135 'text-foreground-lighter hover:text-brand-link transition-colors py-1 wrap-anywhere first:pt-0 last:pb-0 data-[active=true]:text-foreground',
136 item.depth <= 2 && 'ps-3',
137 item.depth === 3 && 'ps-6',
138 item.depth >= 4 && 'ps-8'
139 )}
140 >
141 {formatTOCHeader(removeAnchor(item.title)).map((x, index) => (
142 <Fragment key={index}>
143 {x.type === 'code' ? <code className="text-code-inline">{x.value}</code> : x.value}
144 </Fragment>
145 ))}
146 </Primitive.TOCItem>
147 )
148}