blob: 238c086e09b8f2b80f285458e961cad7655e0ed8 [file]
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import * as Primitive from "fumadocs-core/toc";
import {
type ComponentProps,
createContext,
type RefObject,
use,
useEffect,
useEffectEvent,
useRef
} from "react";
import { useOnChange } from "fumadocs-core/utils/use-on-change";
import { cn, mergeRefs } from "@/lib/utils";
const TOCContext = createContext<Primitive.TOCItemType[]>([]);
export function useTOCItems(): Primitive.TOCItemType[] {
return use(TOCContext);
}
export function TOCProvider({
toc,
children,
...props
}: ComponentProps<typeof Primitive.AnchorProvider>) {
return (
<TOCContext value={toc}>
<Primitive.AnchorProvider toc={toc} {...props}>
{children}
</Primitive.AnchorProvider>
</TOCContext>
);
}
export function TOCScrollArea({ ref, className, ...props }: ComponentProps<"div">) {
const viewRef = useRef<HTMLDivElement>(null);
return (
<div
ref={mergeRefs(viewRef, ref)}
className={cn(
"relative ms-px min-h-0 overflow-auto mask-[linear-gradient(to_bottom,transparent,white_16px,white_calc(100%-16px),transparent)] py-3 text-sm [scrollbar-width:none]",
className
)}
{...props}
>
<Primitive.ScrollProvider containerRef={viewRef}>{props.children}</Primitive.ScrollProvider>
</div>
);
}
type TocThumbType = [top: number, height: number];
interface RefProps {
containerRef: RefObject<HTMLElement | null>;
}
export function TocThumb({ containerRef, ...props }: ComponentProps<"div"> & RefProps) {
const thumbRef = useRef<HTMLDivElement>(null);
const active = Primitive.useActiveAnchors();
function update(info: TocThumbType): void {
const element = thumbRef.current;
if (!element) return;
element.style.setProperty("--fd-top", `${info[0]}px`);
element.style.setProperty("--fd-height", `${info[1]}px`);
}
const onPrint = useEffectEvent(() => {
if (containerRef.current) {
update(calc(containerRef.current, active));
}
});
useEffect(() => {
if (!containerRef.current) return;
const container = containerRef.current;
const observer = new ResizeObserver(onPrint);
observer.observe(container);
return () => {
observer.disconnect();
};
}, [containerRef, onPrint]);
useOnChange(active, () => {
if (containerRef.current) {
update(calc(containerRef.current, active));
}
});
return <div ref={thumbRef} data-hidden={active.length === 0} {...props} />;
}
function calc(container: HTMLElement, active: string[]): TocThumbType {
if (active.length === 0 || container.clientHeight === 0) {
return [0, 0];
}
let upper = Number.MAX_VALUE,
lower = 0;
for (const item of active) {
const element = container.querySelector<HTMLElement>(`a[href="#${item}"]`);
if (!element) continue;
const styles = getComputedStyle(element);
upper = Math.min(upper, element.offsetTop + parseFloat(styles.paddingTop));
lower = Math.max(
lower,
element.offsetTop + element.clientHeight - parseFloat(styles.paddingBottom)
);
}
return [upper, lower - upper];
}