fumadocs-ui
Version:
The Radix UI version of Fumadocs UI
271 lines (268 loc) • 10.1 kB
JavaScript
'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