feat: add toc highlight
This commit is contained in:
parent
aff9b4f90a
commit
24859da9ff
@ -27,7 +27,66 @@ import ComponentsCheckbox, {
|
||||
import ComponentsDialog, {
|
||||
tableOfContents as componentsDialogToc,
|
||||
} from "./docs/components/Dialog.mdx";
|
||||
import { forwardRef } from "react";
|
||||
import { ForwardedRef, forwardRef, useContext, useEffect, useRef } from "react";
|
||||
import { HeadingContext } from "./HeadingContext";
|
||||
|
||||
function buildThresholdList() {
|
||||
let thresholds = [];
|
||||
let numSteps = 20;
|
||||
|
||||
for (let i = 1.0; i <= numSteps; i++) {
|
||||
let ratio = i / numSteps;
|
||||
thresholds.push(ratio);
|
||||
}
|
||||
|
||||
thresholds.push(0);
|
||||
return thresholds;
|
||||
}
|
||||
|
||||
function HashedHeaders(Level: `h${1 | 2 | 3 | 4 | 5 | 6}`) {
|
||||
return (prop: any, ref: ForwardedRef<HTMLHeadingElement>) => {
|
||||
const internalRef = useRef<HTMLHeadingElement | null>(null);
|
||||
const [_, setActiveHeadings] = useContext(HeadingContext);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([{ target, intersectionRatio }]) => {
|
||||
if (intersectionRatio > 0.5) {
|
||||
setActiveHeadings((prev) => [...prev, target.id]);
|
||||
} else {
|
||||
setActiveHeadings((prev) => prev.filter((id) => id !== target.id));
|
||||
}
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
rootMargin: "0px",
|
||||
threshold: buildThresholdList(),
|
||||
}
|
||||
);
|
||||
if (internalRef.current) {
|
||||
observer.observe(internalRef.current);
|
||||
}
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [internalRef.current]);
|
||||
|
||||
return (
|
||||
<Level
|
||||
{...prop}
|
||||
className={`${prop.className}`}
|
||||
ref={(el) => {
|
||||
internalRef.current = el;
|
||||
if (typeof ref === "function") {
|
||||
ref(el);
|
||||
} else if (el && ref) {
|
||||
ref.current = el;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const overrideComponents = {
|
||||
pre: forwardRef<HTMLPreElement, any>((props: any, ref) => (
|
||||
@ -45,6 +104,12 @@ const overrideComponents = {
|
||||
<table ref={ref} {...props} className={`${props.className}`} />
|
||||
</div>
|
||||
)),
|
||||
h1: forwardRef<HTMLHeadingElement, any>(HashedHeaders("h1")),
|
||||
h2: forwardRef<HTMLHeadingElement, any>(HashedHeaders("h2")),
|
||||
h3: forwardRef<HTMLHeadingElement, any>(HashedHeaders("h3")),
|
||||
h4: forwardRef<HTMLHeadingElement, any>(HashedHeaders("h4")),
|
||||
h5: forwardRef<HTMLHeadingElement, any>(HashedHeaders("h5")),
|
||||
h6: forwardRef<HTMLHeadingElement, any>(HashedHeaders("h6")),
|
||||
};
|
||||
|
||||
const router = createBrowserRouter(
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { ReactNode, Fragment } from "react";
|
||||
import { ReactNode, Fragment, useState, useContext } from "react";
|
||||
import { type Toc } from "@stefanprobst/rehype-extract-toc";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { HeadingContext } from "./HeadingContext";
|
||||
|
||||
function RecursivelyToc({ toc }: { toc: Toc }) {
|
||||
const location = useLocation();
|
||||
const [activeHeadings] = useContext(HeadingContext);
|
||||
|
||||
return (
|
||||
<ul>
|
||||
@ -16,7 +18,9 @@ function RecursivelyToc({ toc }: { toc: Toc }) {
|
||||
className="text-neutral-500 data-[active='true']:text-black dark:data-[active='true']:text-white text-sm font-medium"
|
||||
style={{ paddingLeft: `${tocEntry.depth - 1}rem` }}
|
||||
data-active={
|
||||
location.hash.length > 0
|
||||
activeHeadings.includes(tocEntry.id ?? "")
|
||||
? true
|
||||
: location.hash.length > 0
|
||||
? location.hash === `#${tocEntry.id}`
|
||||
: true
|
||||
}
|
||||
@ -40,8 +44,10 @@ export default function DynamicLayout({
|
||||
children: ReactNode;
|
||||
toc: Toc;
|
||||
}) {
|
||||
const [activeHeadings, setActiveHeadings] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeadingContext.Provider value={[activeHeadings, setActiveHeadings]}>
|
||||
<div className="w-full flex flex-col items-center">
|
||||
<main className="w-full [:not(:where([class~='not-prose'],[class~='not-prose']_*))]:prose-sm prose lg:[:not(:where([class~='not-prose'],_[class~='not-prose']_*))]:prose-lg p-8 dark:prose-invert">
|
||||
{children}
|
||||
@ -52,6 +58,6 @@ export default function DynamicLayout({
|
||||
|
||||
<RecursivelyToc toc={toc} />
|
||||
</nav>
|
||||
</>
|
||||
</HeadingContext.Provider>
|
||||
);
|
||||
}
|
||||
|
12
packages/react/src/HeadingContext.tsx
Normal file
12
packages/react/src/HeadingContext.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { Dispatch, SetStateAction, createContext } from "react";
|
||||
|
||||
export const HeadingContext = createContext<
|
||||
[string[], Dispatch<SetStateAction<string[]>>]
|
||||
>([
|
||||
[],
|
||||
() => {
|
||||
if (process.env && process.env.NODE_ENV === "development") {
|
||||
console.log("HeadingContext outside");
|
||||
}
|
||||
},
|
||||
]);
|
Loading…
x
Reference in New Issue
Block a user