UNPKG

fumadocs-ui

Version:

The Radix UI version of Fumadocs UI

271 lines (268 loc) 10.1 kB
'use client'; import { buttonVariants } from "../ui/button.js"; import { i18n_exports } from "../../contexts/i18n.js"; import { useRouter } from "fumadocs-core/framework"; import { cn } from "@fumadocs/ui/cn"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { ChevronRight, Hash, Search } from "lucide-react"; import { Fragment as Fragment$1, createContext, use, useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from "react"; import { cva } from "class-variance-authority"; import { useOnChange } from "fumadocs-core/utils/use-on-change"; import scrollIntoView from "scroll-into-view-if-needed"; import { Dialog, DialogContent, DialogOverlay, DialogTitle } from "@radix-ui/react-dialog"; //#region src/components/dialog/search.tsx const Context = createContext(null); const ListContext = createContext(null); const TagsListContext = createContext(null); function SearchDialog({ open, onOpenChange, search, onSearchChange, isLoading = false, onSelect: onSelectProp, children }) { const router = useRouter(); const onSelect = useEffectEvent((item) => { if (item.type === "action") item.onSelect(); else if (item.external) window.open(item.url, "_blank")?.focus(); else router.push(item.url); onOpenChange(false); onSelectProp?.(item); }); return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsx(Context.Provider, { value: useMemo(() => ({ open, onOpenChange, search, onSearchChange, onSelect, isLoading }), [ isLoading, onOpenChange, onSearchChange, open, search ]), children }) }); } function SearchDialogHeader(props) { return /* @__PURE__ */ jsx("div", { ...props, className: cn("flex flex-row items-center gap-2 p-3", props.className) }); } function SearchDialogInput(props) { const { text } = (0, i18n_exports.useI18n)(); const { search, onSearchChange } = useSearch(); return /* @__PURE__ */ jsx("input", { ...props, value: search, onChange: (e) => onSearchChange(e.target.value), placeholder: text.search, className: "w-0 flex-1 bg-transparent text-lg placeholder:text-fd-muted-foreground focus-visible:outline-none" }); } function SearchDialogClose({ children = "ESC", className, ...props }) { const { onOpenChange } = useSearch(); return /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onOpenChange(false), className: cn(buttonVariants({ color: "outline", size: "sm", className: "font-mono text-fd-muted-foreground" }), className), ...props, children }); } function SearchDialogFooter(props) { return /* @__PURE__ */ jsx("div", { ...props, className: cn("bg-fd-secondary/50 p-3 empty:hidden", props.className) }); } function SearchDialogOverlay(props) { return /* @__PURE__ */ jsx(DialogOverlay, { ...props, className: cn("fixed inset-0 z-50 backdrop-blur-xs bg-fd-overlay data-[state=open]:animate-fd-fade-in data-[state=closed]:animate-fd-fade-out", props.className) }); } function SearchDialogContent({ children, ...props }) { const { text } = (0, i18n_exports.useI18n)(); return /* @__PURE__ */ jsxs(DialogContent, { "aria-describedby": void 0, ...props, className: cn("fixed left-1/2 top-4 md:top-[calc(50%-250px)] z-50 w-[calc(100%-1rem)] max-w-screen-sm -translate-x-1/2 rounded-xl border bg-fd-popover text-fd-popover-foreground shadow-2xl shadow-black/50 overflow-hidden data-[state=closed]:animate-fd-dialog-out data-[state=open]:animate-fd-dialog-in", "*:border-b *:has-[+:last-child[data-empty=true]]:border-b-0 *:data-[empty=true]:border-b-0 *:last:border-b-0", props.className), children: [/* @__PURE__ */ jsx(DialogTitle, { className: "hidden", children: text.search }), children] }); } function SearchDialogList({ items = null, Empty = () => /* @__PURE__ */ jsx("div", { className: "py-12 text-center text-sm text-fd-muted-foreground", children: /* @__PURE__ */ jsx(i18n_exports.I18nLabel, { label: "searchNoResult" }) }), Item = (props$1) => /* @__PURE__ */ jsx(SearchDialogListItem, { ...props$1 }), ...props }) { const ref = useRef(null); const { onSelect } = useSearch(); const [active, setActive] = useState(() => items && items.length > 0 ? items[0].id : null); const onKey = useEffectEvent((e) => { if (!items || e.isComposing) return; if (e.key === "ArrowDown" || e.key == "ArrowUp") { let idx = items.findIndex((item) => item.id === active); if (idx === -1) idx = 0; else if (e.key === "ArrowDown") idx++; else idx--; setActive(items.at(idx % items.length)?.id ?? null); e.preventDefault(); } if (e.key === "Enter") { const selected = items.find((item) => item.id === active); if (selected) onSelect(selected); e.preventDefault(); } }); useEffect(() => { const element = ref.current; if (!element) return; const observer = new ResizeObserver(() => { const viewport$1 = element.firstElementChild; element.style.setProperty("--fd-animated-height", `${viewport$1.clientHeight}px`); }); const viewport = element.firstElementChild; if (viewport) observer.observe(viewport); window.addEventListener("keydown", onKey); return () => { observer.disconnect(); window.removeEventListener("keydown", onKey); }; }, []); useOnChange(items, () => { if (items && items.length > 0) setActive(items[0].id); }); return /* @__PURE__ */ jsx("div", { ...props, ref, "data-empty": items === null, className: cn("overflow-hidden h-(--fd-animated-height) transition-[height]", props.className), children: /* @__PURE__ */ jsx("div", { className: cn("w-full flex flex-col overflow-y-auto max-h-[460px] p-1", !items && "hidden"), children: /* @__PURE__ */ jsxs(ListContext.Provider, { value: useMemo(() => ({ active, setActive }), [active]), children: [items?.length === 0 && Empty(), items?.map((item) => /* @__PURE__ */ jsx(Fragment$1, { children: Item({ item, onClick: () => onSelect(item) }) }, item.id))] }) }) }); } function SearchDialogListItem({ item, className, children, renderHighlights: render = renderHighlights, ...props }) { const { active: activeId, setActive } = useSearchList(); const active = item.id === activeId; if (item.type === "action") children ??= item.node; else children ??= /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx("div", { className: "inline-flex items-center text-fd-muted-foreground text-xs empty:hidden", children: item.breadcrumbs?.map((item$1, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [i > 0 && /* @__PURE__ */ jsx(ChevronRight, { className: "size-4 rtl:rotate-180" }), item$1] }, i)) }), item.type !== "page" && /* @__PURE__ */ jsx("div", { role: "none", className: "absolute start-3 inset-y-0 w-px bg-fd-border" }), /* @__PURE__ */ jsxs("p", { className: cn("min-w-0 truncate", item.type !== "page" && "ps-4", item.type === "page" || item.type === "heading" ? "font-medium" : "text-fd-popover-foreground/80"), children: [item.type === "heading" && /* @__PURE__ */ jsx(Hash, { className: "inline me-1 size-4 text-fd-muted-foreground" }), item.contentWithHighlights ? render(item.contentWithHighlights) : item.content] }) ] }); return /* @__PURE__ */ jsx("button", { type: "button", ref: useCallback((element) => { if (active && element) scrollIntoView(element, { scrollMode: "if-needed", block: "nearest", boundary: element.parentElement }); }, [active]), "aria-selected": active, className: cn("relative select-none px-2.5 py-2 text-start text-sm rounded-lg", active && "bg-fd-accent text-fd-accent-foreground", className), onPointerMove: () => setActive(item.id), ...props, children }); } function SearchDialogIcon(props) { const { isLoading } = useSearch(); return /* @__PURE__ */ jsx(Search, { ...props, className: cn("size-5 text-fd-muted-foreground", isLoading && "animate-pulse duration-400", props.className) }); } const itemVariants = cva("rounded-md border px-2 py-0.5 text-xs font-medium text-fd-muted-foreground transition-colors", { variants: { active: { true: "bg-fd-accent text-fd-accent-foreground" } } }); function TagsList({ tag, onTagChange, allowClear = false, ...props }) { return /* @__PURE__ */ jsx("div", { ...props, className: cn("flex items-center gap-1 flex-wrap", props.className), children: /* @__PURE__ */ jsx(TagsListContext.Provider, { value: useMemo(() => ({ value: tag, onValueChange: onTagChange, allowClear }), [ allowClear, onTagChange, tag ]), children: props.children }) }); } function TagsListItem({ value, className, ...props }) { const { onValueChange, value: selectedValue, allowClear } = useTagsList(); const selected = value === selectedValue; return /* @__PURE__ */ jsx("button", { type: "button", "data-active": selected, className: cn(itemVariants({ active: selected, className })), onClick: () => { onValueChange(selected && allowClear ? void 0 : value); }, tabIndex: -1, ...props, children: props.children }); } function renderHighlights(highlights) { return highlights.map((node, i) => { if (node.styles?.highlight) return /* @__PURE__ */ jsx("span", { className: "text-fd-primary underline", children: node.content }, i); return /* @__PURE__ */ jsx(Fragment$1, { children: node.content }, i); }); } function useSearch() { const ctx = use(Context); if (!ctx) throw new Error("Missing <SearchDialog />"); return ctx; } function useTagsList() { const ctx = use(TagsListContext); if (!ctx) throw new Error("Missing <TagsList />"); return ctx; } function useSearchList() { const ctx = use(ListContext); if (!ctx) throw new Error("Missing <SearchDialogList />"); return ctx; } //#endregion export { SearchDialog, SearchDialogClose, SearchDialogContent, SearchDialogFooter, SearchDialogHeader, SearchDialogIcon, SearchDialogInput, SearchDialogList, SearchDialogListItem, SearchDialogOverlay, TagsList, TagsListItem, useSearch, useSearchList, useTagsList }; //# sourceMappingURL=search.js.map