UNPKG

fumadocs-ui

Version:

The Radix UI version of Fumadocs UI

182 lines (181 loc) 6.14 kB
"use client"; import { cn } from "../../../../utils/cn.js"; import { TOCScrollArea, toc_exports, useItems, useTOCItems } from "../../../../components/toc/index.js"; import { default_exports } from "../../../../components/toc/default.js"; import { clerk_exports } from "../../../../components/toc/clerk.js"; import { useTreePath } from "../../../../contexts/tree.js"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../../components/ui/collapsible.js"; import { jsx, jsxs } from "react/jsx-runtime"; import { ChevronDown } from "lucide-react"; import { createContext, use, useEffect, useEffectEvent, useMemo, useRef, useState } from "react"; import { useTranslations } from "@fuma-translate/react"; import { createPortal } from "react-dom"; import { AnimatePresence, motion } from "motion/react"; //#region src/layouts/flux/page/slots/toc.tsx const TocPopoverContext = createContext(null); const { TOCProvider } = toc_exports; function TOC({ container, trigger, content, header, footer, style = "normal", list }) { const items = useTOCItems(); const { TOCItems, TOCEmpty, TOCItem } = style === "clerk" ? clerk_exports : default_exports; if (items.length === 0 && !header && !footer) return; return /* @__PURE__ */ jsxs(PageTOCPopover, { ...container, children: [/* @__PURE__ */ jsxs(PageTOCPopoverContent, { ...content, children: [ header, /* @__PURE__ */ jsx(TOCScrollArea, { children: /* @__PURE__ */ jsxs(TOCItems, { ...list, children: [items.length === 0 && /* @__PURE__ */ jsx(TOCEmpty, {}), items.map((item) => /* @__PURE__ */ jsx(TOCItem, { item }, item.url))] }) }), footer ] }), /* @__PURE__ */ jsx(PageTOCPopoverTrigger, { ...trigger })] }); } function PageTOCPopover(props) { const [container, setContainer] = useState(null); useEffect(() => { const element = document.getElementById("flux-layout-slot"); if (!element) return; setContainer(element); }, []); if (!container) return; return createPortal(/* @__PURE__ */ jsx(PageTOCPopoverPhysical, { ...props }), container); } function PageTOCPopoverPhysical({ className, children, ...rest }) { const ref = useRef(null); const [open, setOpen] = useState(false); const onClick = useEffectEvent((e) => { if (!open) return; if (ref.current && !ref.current.contains(e.target)) setOpen(false); }); useEffect(() => { window.addEventListener("click", onClick); return () => { window.removeEventListener("click", onClick); }; }, []); return /* @__PURE__ */ jsx(TocPopoverContext, { value: useMemo(() => ({ open, setOpen }), [setOpen, open]), children: /* @__PURE__ */ jsx(Collapsible, { open, onOpenChange: setOpen, "data-toc-popover": "", className: cn("relative h-9 animate-fd-fade-in", className), ...rest, children: /* @__PURE__ */ jsx("header", { ref, className: cn("absolute w-full bottom-0 border rounded-xl transition-colors bg-fd-secondary text-fd-secondary-foreground backdrop-blur-sm", open && "shadow-lg bg-fd-popover/80 text-fd-popover-foreground"), children }) }) }); } function PageTOCPopoverTrigger({ className, ...props }) { const t = useTranslations({ note: "table of contents" }); const { open } = use(TocPopoverContext); const items = useItems(); const selectedIdx = items.findIndex((item) => item.active); const path = useTreePath().at(-1); const spanProps = { transition: { duration: .1 }, initial: { opacity: 0, y: 10 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -10 }, className: cn(open && "text-fd-popover-foreground") }; return /* @__PURE__ */ jsxs(CollapsibleTrigger, { className: cn("flex w-full h-8.5 items-center text-sm text-fd-muted-foreground gap-2.5 px-2 text-start focus-visible:outline-none [&_svg]:size-4", className), "data-toc-popover-trigger": "", ...props, children: [ /* @__PURE__ */ jsx(ProgressCircle, { value: (items.findLastIndex((item) => item.active) + 1) / Math.max(1, items.length), max: 1, className: cn("shrink-0", open && "text-fd-primary") }), /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", children: items[selectedIdx] && !open ? /* @__PURE__ */ jsx(motion.span, { ...spanProps, children: items[selectedIdx].original.title }, selectedIdx) : path ? /* @__PURE__ */ jsx(motion.span, { ...spanProps, children: path.name }, path.$id ?? ":pathId") : /* @__PURE__ */ jsx(motion.span, { ...spanProps, children: t("On this page") }, ":toc") }), /* @__PURE__ */ jsx(ChevronDown, { className: cn("ms-auto shrink-0 transition-transform", open && "rotate-180") }) ] }); } function clamp(input, min, max) { if (input < min) return min; if (input > max) return max; return input; } function ProgressCircle({ value, strokeWidth = 1.5, size = 18, min = 0, max = 100, style, ...restSvgProps }) { const normalizedValue = clamp(value, min, max); const radius = size / 2 - strokeWidth; const circumference = 2 * Math.PI * radius; const progress = normalizedValue / max * circumference; const circleProps = { cx: size / 2, cy: size / 2, r: radius, fill: "none", strokeWidth }; return /* @__PURE__ */ jsxs("svg", { role: "progressbar", viewBox: `0 0 ${size} ${size}`, "aria-valuenow": normalizedValue, "aria-valuemin": min, "aria-valuemax": max, style: { width: size, height: size, ...style }, ...restSvgProps, children: [/* @__PURE__ */ jsx("circle", { ...circleProps, className: "stroke-current/25" }), /* @__PURE__ */ jsx("circle", { ...circleProps, stroke: "currentColor", strokeDasharray: circumference, strokeDashoffset: circumference - progress, strokeLinecap: "round", transform: `rotate(-90 ${size / 2} ${size / 2})`, className: "transition-all" })] }); } function PageTOCPopoverContent(props) { return /* @__PURE__ */ jsx(CollapsibleContent, { "data-toc-popover-content": "", ...props, children: /* @__PURE__ */ jsx("div", { className: "flex flex-col px-2 max-h-[50vh]", children: props.children }) }); } //#endregion export { TOC, TOCProvider };