feat: add toc highlight
This commit is contained in:
parent
aff9b4f90a
commit
24859da9ff
@ -27,7 +27,66 @@ import ComponentsCheckbox, {
|
|||||||
import ComponentsDialog, {
|
import ComponentsDialog, {
|
||||||
tableOfContents as componentsDialogToc,
|
tableOfContents as componentsDialogToc,
|
||||||
} from "./docs/components/Dialog.mdx";
|
} 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 = {
|
const overrideComponents = {
|
||||||
pre: forwardRef<HTMLPreElement, any>((props: any, ref) => (
|
pre: forwardRef<HTMLPreElement, any>((props: any, ref) => (
|
||||||
@ -45,6 +104,12 @@ const overrideComponents = {
|
|||||||
<table ref={ref} {...props} className={`${props.className}`} />
|
<table ref={ref} {...props} className={`${props.className}`} />
|
||||||
</div>
|
</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(
|
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 { type Toc } from "@stefanprobst/rehype-extract-toc";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { HeadingContext } from "./HeadingContext";
|
||||||
|
|
||||||
function RecursivelyToc({ toc }: { toc: Toc }) {
|
function RecursivelyToc({ toc }: { toc: Toc }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [activeHeadings] = useContext(HeadingContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul>
|
<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"
|
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` }}
|
style={{ paddingLeft: `${tocEntry.depth - 1}rem` }}
|
||||||
data-active={
|
data-active={
|
||||||
location.hash.length > 0
|
activeHeadings.includes(tocEntry.id ?? "")
|
||||||
|
? true
|
||||||
|
: location.hash.length > 0
|
||||||
? location.hash === `#${tocEntry.id}`
|
? location.hash === `#${tocEntry.id}`
|
||||||
: true
|
: true
|
||||||
}
|
}
|
||||||
@ -40,8 +44,10 @@ export default function DynamicLayout({
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
toc: Toc;
|
toc: Toc;
|
||||||
}) {
|
}) {
|
||||||
|
const [activeHeadings, setActiveHeadings] = useState<string[]>([]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<HeadingContext.Provider value={[activeHeadings, setActiveHeadings]}>
|
||||||
<div className="w-full flex flex-col items-center">
|
<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">
|
<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}
|
{children}
|
||||||
@ -52,6 +58,6 @@ export default function DynamicLayout({
|
|||||||
|
|
||||||
<RecursivelyToc toc={toc} />
|
<RecursivelyToc toc={toc} />
|
||||||
</nav>
|
</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