Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/components/ui/docs/toc.tsx
T

121 lines
2.9 KiB
TypeScript

import clsx from "clsx";
import { useEffect, useMemo, useState } from "react";
import ButtonEvent from "../buttons/ButtonEvent";
interface Item {
title: string;
url: string;
items?: Item[];
}
interface Items {
items?: Item[];
}
interface TocProps {
title: string;
toc: Items;
}
export function DashboardTableOfContents({ title, toc }: TocProps) {
const itemIds = useMemo(
() =>
toc.items
? toc.items
.flatMap((item) => [item.url, item?.items?.map((item) => item.url)])
.flat()
.filter(Boolean)
.map((id) => id?.split("#")[1])
: [],
[toc]
);
const activeHeading = useActiveItem(itemIds.filter(Boolean) as string[]);
const mounted = useMounted();
if (!toc?.items || !mounted) {
return null;
}
return (
<div className="space-y-2">
<p className="font-medium">{title}</p>
<Tree tree={toc} activeItem={activeHeading} />
</div>
);
}
function useActiveItem(itemIds: string[]) {
const [activeId, setActiveId] = useState<undefined | string>(undefined);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id ?? undefined);
}
});
},
{ rootMargin: `0% 0% -80% 0%` }
);
itemIds?.forEach((id) => {
const element = document.getElementById(id);
if (element) {
observer.observe(element);
}
});
return () => {
itemIds?.forEach((id) => {
const element = document.getElementById(id);
if (element) {
observer.unobserve(element);
}
});
};
}, [itemIds]);
return activeId;
}
interface TreeProps {
tree: Items;
level?: number;
activeItem?: string;
}
function Tree({ tree, level = 1, activeItem }: TreeProps) {
return tree?.items?.length && level < 3 ? (
<ul className={clsx("m-0 list-none", { "pl-4": level !== 1 })}>
{tree.items.map((item, index) => {
return (
<li key={index} className={clsx("mt-0 pt-2")}>
<ButtonEvent
event={{ action: "click", category: "toc", label: item.title, value: item.url }}
to={item.url}
className={clsx(
"hover:text-text-neutral-primary inline-block no-underline transition-colors",
item.url === `#${activeItem}` ? "text-text-neutral-primary font-medium" : "text-text-neutral-tertiary"
)}
>
{item.title}
</ButtonEvent>
{item.items?.length ? <Tree tree={item} level={level + 1} activeItem={activeItem} /> : null}
</li>
);
})}
</ul>
) : null;
}
function useMounted() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return mounted;
}