;
+ onScopeChange: (scope: SearchScope) => void;
+ scope: SearchScope;
+}) {
+ const scopes: SearchScope[] = ["components", "docs", "everything"];
+
+ return (
+
+ {scopes.map((nextScope) => (
+
+ ))}
+
+ );
+}
+
+function SearchResultContent({
+ item,
+ query,
+}: {
+ item: SearchItem;
+ query: string;
+}) {
+ return (
+
+ {item.title}
+ {item.snippet ? (
+
+
+
+ ) : item.description ? (
+
+ {item.description}
+
+ ) : null}
+
+ );
+}
+
+function SearchTriggerButton({
+ buttonText,
+ buttonTextMobile,
+ onOpen,
+}: {
+ buttonText?: string;
+ buttonTextMobile?: string;
+ onOpen: () => void;
+}) {
+ return (
+
+ );
+}
+
+function ComponentResultsGroup({
groupHeading,
+ hasDocumentationSearch,
items,
+ labels,
onSelect,
- searchPlaceholder = "Search...",
-}: SearchDialogProps) {
- const [open, setOpen] = useState(false);
+ query,
+}: {
+ groupHeading?: string;
+ hasDocumentationSearch: boolean;
+ items: SearchItem[];
+ labels: Record;
+ onSelect: (item: SearchItem) => void;
+ query: string;
+}) {
+ return (
+
+ {items.map((item) => (
+ {
+ onSelect(item);
+ }}
+ value={getItemValue(item)}
+ >
+
+
+ ))}
+
+ );
+}
+
+function DocumentationStatusItem({
+ documentationSearchLength,
+ trimmedQuery,
+}: {
+ documentationSearchLength: number;
+ trimmedQuery: string;
+}) {
+ return (
+
+
+ Type at least {documentationSearchLength} characters to search docs.
+
+
+ );
+}
+
+function DocumentationLoadingItem({ trimmedQuery }: { trimmedQuery: string }) {
+ return (
+
+ Searching docs...
+
+ );
+}
+
+function DocumentationResultsGroup({
+ docsGroupHeading,
+ documentationItems,
+ documentationLoading,
+ documentationSearchLength,
+ onSelect,
+ query,
+ scope,
+ trimmedQuery,
+}: {
+ docsGroupHeading?: string;
+ documentationItems: SearchItem[];
+ documentationLoading: boolean;
+ documentationSearchLength: number;
+ onSelect: (item: SearchItem) => void;
+ query: string;
+ scope: SearchScope;
+ trimmedQuery: string;
+}) {
+ const showMinimumLengthPrompt =
+ scope === "docs" && trimmedQuery.length < documentationSearchLength;
+ const showDocumentationItems =
+ !documentationLoading && trimmedQuery.length >= documentationSearchLength;
- const sortedItems = [...items].sort((a, b) => a.title.localeCompare(b.title));
+ return (
+
+ {showMinimumLengthPrompt ? (
+
+ ) : null}
+ {documentationLoading ? (
+
+ ) : null}
+ {showDocumentationItems
+ ? documentationItems.map((item) => (
+ {
+ onSelect(item);
+ }}
+ value={getItemValue(item)}
+ >
+
+
+ ))
+ : null}
+
+ );
+}
+
+function SearchDialogList({
+ currentEmptyText,
+ docsGroupHeading,
+ documentationItems,
+ documentationLoading,
+ documentationSearchLength,
+ groupHeading,
+ hasDocumentationSearch,
+ labels,
+ onComponentSelect,
+ onDocumentationSelect,
+ query,
+ scope,
+ showComponents,
+ showDocumentation,
+ sortedItems,
+ trimmedQuery,
+}: {
+ currentEmptyText: string;
+ docsGroupHeading?: string;
+ documentationItems: SearchItem[];
+ documentationLoading: boolean;
+ documentationSearchLength: number;
+ groupHeading?: string;
+ hasDocumentationSearch: boolean;
+ labels: Record;
+ onComponentSelect: (item: SearchItem) => void;
+ onDocumentationSelect: (item: SearchItem) => void;
+ query: string;
+ scope: SearchScope;
+ showComponents: boolean;
+ showDocumentation: boolean;
+ sortedItems: SearchItem[];
+ trimmedQuery: string;
+}) {
+ return (
+
+ {currentEmptyText}
+ {showComponents ? (
+
+ ) : null}
+ {showDocumentation ? (
+
+ ) : null}
+
+ );
+}
- useKeyboardShortcut(() => {
- if (enableKeyboardShortcut) {
+type DocumentationSearchOptions = {
+ docsSearch?: (query: string) => Promise;
+ minDocsSearchLength?: number;
+};
+
+function useDocumentationSearch({
+ docsSearch,
+ minDocsSearchLength,
+}: DocumentationSearchOptions) {
+ const [documentationItems, setDocumentationItems] = useState(
+ [],
+ );
+ const [documentationLoading, setDocumentationLoading] = useState(false);
+ const activeDocumentationRequest = useRef(0);
+ const documentationSearchLength = minDocsSearchLength ?? 2;
+
+ const runDocumentationSearch = useCallback(
+ (nextQuery: string, nextScope: SearchScope) => {
+ const nextTrimmedQuery = nextQuery.trim();
+ const nextRequest = activeDocumentationRequest.current + 1;
+ activeDocumentationRequest.current = nextRequest;
+
+ if (
+ !docsSearch ||
+ nextScope === "components" ||
+ nextTrimmedQuery.length < documentationSearchLength
+ ) {
+ setDocumentationItems([]);
+ setDocumentationLoading(false);
+ return;
+ }
+
+ setDocumentationLoading(true);
+
+ docsSearch(nextTrimmedQuery)
+ .then((results) => {
+ if (activeDocumentationRequest.current === nextRequest) {
+ setDocumentationItems(results);
+ }
+ })
+ .catch(() => {
+ if (activeDocumentationRequest.current === nextRequest) {
+ setDocumentationItems([]);
+ }
+ })
+ .finally(() => {
+ if (activeDocumentationRequest.current === nextRequest) {
+ setDocumentationLoading(false);
+ }
+ });
+ },
+ [docsSearch, documentationSearchLength],
+ );
+
+ return {
+ documentationItems,
+ documentationLoading,
+ documentationSearchLength,
+ hasDocumentationSearch: docsSearch !== undefined,
+ runDocumentationSearch,
+ };
+}
+
+type SearchDialogHandlersOptions = {
+ enableKeyboardShortcut?: boolean;
+ onDocsSelect?: (item: SearchItem) => void;
+ onSelect: (item: SearchItem) => void;
+ runDocumentationSearch: (query: string, scope: SearchScope) => void;
+};
+
+function useSearchDialogHandlers({
+ enableKeyboardShortcut,
+ onDocsSelect,
+ onSelect,
+ runDocumentationSearch,
+}: SearchDialogHandlersOptions) {
+ const [open, setOpen] = useState(false);
+ const [query, setQuery] = useState("");
+ const [scope, setScope] = useState("components");
+
+ const toggleOpen = useCallback(() => {
+ if (enableKeyboardShortcut ?? true) {
setOpen((previous) => !previous);
}
- });
+ }, [enableKeyboardShortcut]);
- const handleSelect = (item: SearchItem) => {
- setOpen(false);
- onSelect(item);
+ useKeyboardShortcut(toggleOpen);
+
+ const handleQueryChange = useCallback(
+ (nextQuery: string) => {
+ setQuery(nextQuery);
+ runDocumentationSearch(nextQuery, scope);
+ },
+ [runDocumentationSearch, scope],
+ );
+
+ const handleScopeChange = useCallback(
+ (nextScope: SearchScope) => {
+ setScope(nextScope);
+ runDocumentationSearch(query, nextScope);
+ },
+ [query, runDocumentationSearch],
+ );
+
+ const handleOpenChange = useCallback(
+ (nextOpen: boolean) => {
+ setOpen(nextOpen);
+
+ if (nextOpen) {
+ runDocumentationSearch(query, scope);
+ }
+ },
+ [query, runDocumentationSearch, scope],
+ );
+
+ const handleComponentSelect = useCallback(
+ (item: SearchItem) => {
+ setOpen(false);
+ onSelect(item);
+ },
+ [onSelect],
+ );
+
+ const handleDocumentationSelect = useCallback(
+ (item: SearchItem) => {
+ setOpen(false);
+ (onDocsSelect ?? onSelect)(item);
+ },
+ [onDocsSelect, onSelect],
+ );
+
+ return {
+ handleComponentSelect,
+ handleDocumentationSelect,
+ handleOpenChange,
+ handleQueryChange,
+ handleScopeChange,
+ open,
+ query,
+ scope,
};
+}
+function getCurrentEmptyText({
+ docsEmptyText,
+ documentationSearchLength,
+ emptyText,
+ scope,
+ trimmedQuery,
+}: {
+ docsEmptyText?: string;
+ documentationSearchLength: number;
+ emptyText?: string;
+ scope: SearchScope;
+ trimmedQuery: string;
+}) {
+ if (scope === "docs" && trimmedQuery.length < documentationSearchLength) {
+ return `Type at least ${documentationSearchLength} characters to search docs.`;
+ }
+
+ if (scope === "docs") {
+ return docsEmptyText ?? "No docs found.";
+ }
+
+ return emptyText ?? "No results found.";
+}
+
+type SearchDialogViewProps = Pick<
+ SearchDialogProps,
+ | "buttonText"
+ | "buttonTextMobile"
+ | "docsGroupHeading"
+ | "groupHeading"
+ | "searchPlaceholder"
+> & {
+ currentEmptyText: string;
+ documentationSearch: ReturnType;
+ handlers: ReturnType;
+ labels: Record;
+ showDocumentation: boolean;
+ sortedItems: SearchItem[];
+ trimmedQuery: string;
+};
+
+function SearchDialogView({
+ buttonText,
+ buttonTextMobile,
+ currentEmptyText,
+ docsGroupHeading,
+ documentationSearch,
+ groupHeading,
+ handlers,
+ labels,
+ searchPlaceholder,
+ showDocumentation,
+ sortedItems,
+ trimmedQuery,
+}: SearchDialogViewProps) {
return (
<>
-
-
-
-
- {emptyText}
-
- {sortedItems.map((item) => (
- {
- handleSelect(item);
- }}
- value={`${item.title} ${item.description || ""} ${item.keywords || ""} ${item.id}`}
- >
-
- {item.title}
- {item.description ? (
-
- {item.description}
-
- ) : null}
-
-
- ))}
-
-
+
+ {documentationSearch.hasDocumentationSearch ? (
+
+ ) : null}
+
>
);
}
+
+export function SearchDialog({
+ buttonText,
+ buttonTextMobile,
+ docsEmptyText,
+ docsGroupHeading,
+ docsSearch,
+ emptyText,
+ enableKeyboardShortcut,
+ groupHeading,
+ items,
+ minDocsSearchLength,
+ onDocsSelect,
+ onSelect,
+ scopeLabels,
+ searchPlaceholder,
+}: SearchDialogProps) {
+ const documentationSearch = useDocumentationSearch({
+ docsSearch,
+ minDocsSearchLength,
+ });
+ const handlers = useSearchDialogHandlers({
+ enableKeyboardShortcut,
+ onDocsSelect,
+ onSelect,
+ runDocumentationSearch: documentationSearch.runDocumentationSearch,
+ });
+ const labels = { ...DEFAULT_SCOPE_LABELS, ...scopeLabels };
+ const trimmedQuery = handlers.query.trim();
+ const sortedItems = useMemo(
+ () => [...items].sort((a, b) => a.title.localeCompare(b.title)),
+ [items],
+ );
+ const currentEmptyText = getCurrentEmptyText({
+ docsEmptyText,
+ documentationSearchLength: documentationSearch.documentationSearchLength,
+ emptyText,
+ scope: handlers.scope,
+ trimmedQuery,
+ });
+ const showDocumentation =
+ documentationSearch.hasDocumentationSearch &&
+ handlers.scope !== "components";
+
+ return (
+
+ );
+}
diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts
index 7389b28d..42cd1655 100644
--- a/packages/ui/src/components/index.ts
+++ b/packages/ui/src/components/index.ts
@@ -655,7 +655,7 @@ export {
} from "./share-section";
// Registry/Documentation components
-export { SearchDialog } from "./search-dialog";
+export { SearchDialog, type SearchItem } from "./search-dialog";
// Theme & Language providers
export { LangProvider } from "./lang-provider";
diff --git a/packages/ui/src/components/search-dialog/index.ts b/packages/ui/src/components/search-dialog/index.ts
index a6e13f99..9e2b6209 100644
--- a/packages/ui/src/components/search-dialog/index.ts
+++ b/packages/ui/src/components/search-dialog/index.ts
@@ -1 +1 @@
-export { SearchDialog } from "./search-dialog";
+export { SearchDialog, type SearchItem } from "./search-dialog";
diff --git a/packages/ui/src/components/search-dialog/search-dialog.mdx b/packages/ui/src/components/search-dialog/search-dialog.mdx
index af82ed6b..fe819eb0 100644
--- a/packages/ui/src/components/search-dialog/search-dialog.mdx
+++ b/packages/ui/src/components/search-dialog/search-dialog.mdx
@@ -49,12 +49,18 @@ const items = [
| ---- | ---- | -------- | ----------- |
| `buttonText` | `string` | No | - |
| `buttonTextMobile` | `string` | No | - |
+| `docsEmptyText` | `string` | No | - |
+| `docsGroupHeading` | `string` | No | - |
+| `docsSearch` | `(query: string) => Promise<SearchItem[]>` | No | Optional async docs search provider |
| `emptyText` | `string` | No | - |
| `enableKeyboardShortcut` | `boolean` | No | `true` or `false` |
| `groupHeading` | `string` | No | - |
| `items` | `SearchItem[]` | Yes | Array of `SearchItem` objects |
+| `minDocsSearchLength` | `number` | No | Minimum query length before docs search runs |
+| `onDocsSelect` | `(item: SearchItem) => void` | No | Optional docs result callback |
| `onSelect` | `(item: SearchItem) => void` | Yes | Callback: `(item: SearchItem) => void` |
| `searchPlaceholder` | `string` | No | - |
+| `scopeLabels` | `Partial<Record<"components" \| "docs" \| "everything", string>>` | No | Optional tab labels |
## Type Definitions
@@ -63,8 +69,10 @@ const items = [
```tsx
type SearchItem = {
description?: string
+ href?: string
id: string
keywords?: string
+ snippet?: string
title: string
}
```
@@ -72,7 +80,8 @@ type SearchItem = {
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `description` | `string` | No | - |
+| `href` | `string` | No | - |
| `id` | `string` | Yes | - |
| `keywords` | `string` | No | - |
+| `snippet` | `string` | No | - |
| `title` | `string` | Yes | - |
-
diff --git a/packages/ui/src/components/search-dialog/search-dialog.stories.tsx b/packages/ui/src/components/search-dialog/search-dialog.stories.tsx
index ad86453e..3b0b3279 100644
--- a/packages/ui/src/components/search-dialog/search-dialog.stories.tsx
+++ b/packages/ui/src/components/search-dialog/search-dialog.stories.tsx
@@ -1,15 +1,24 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
-import { SearchDialog } from "./search-dialog";
+import { SearchDialog, type SearchItem } from "./search-dialog";
+
+function createItem(
+ id: string,
+ title: string,
+ description: string,
+): SearchItem {
+ return { description, href: `/components/${id}`, id, title };
+}
+
+const items = [
+ createItem("button", "Button", "Accessible button primitive."),
+ createItem("command", "Command", "Composable command menu."),
+];
const meta = {
args: {
- items: [{
- id: "1",
- title: "Example Title",
- onSelect: (item) => {},
- }],
- onSelect: (item) => {},
+ items: items,
+ onSelect: () => {},
},
component: SearchDialog,
title: "Learning/SearchDialog",
@@ -18,4 +27,24 @@ const meta = {
export default meta;
type Story = StoryObj;
-export const Default: Story = {};
+export const Default: Story = {
+ args: {
+ items: items,
+ onSelect: () => {},
+ },
+};
+
+export const WithDocs: Story = {
+ args: {
+ docsSearch: async () => [
+ {
+ description: "/docs",
+ href: "/docs",
+ id: "docs",
+ snippet: "Install VLLNT UI and configure dark mode.",
+ title: "Documentation",
+ },
+ ],
+ searchPlaceholder: "Search docs and components...",
+ },
+};
diff --git a/packages/ui/src/components/search-dialog/search-dialog.test.tsx b/packages/ui/src/components/search-dialog/search-dialog.test.tsx
new file mode 100644
index 00000000..5c2ec60d
--- /dev/null
+++ b/packages/ui/src/components/search-dialog/search-dialog.test.tsx
@@ -0,0 +1,77 @@
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { SearchDialog, type SearchItem } from "./search-dialog";
+
+const componentItems: SearchItem[] = [
+ {
+ description: "Accessible button primitive.",
+ href: "/components/button",
+ id: "button",
+ title: "Button",
+ },
+];
+
+describe("SearchDialog", () => {
+ it("renders docs search scopes when docs search is configured", () => {
+ render(
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: /search/i }));
+
+ expect(screen.getByRole("tab", { name: "Components" })).toHaveAttribute(
+ "aria-selected",
+ "true",
+ );
+ expect(screen.getByRole("tab", { name: "Docs" })).toBeInTheDocument();
+ expect(screen.getByRole("tab", { name: "Everything" })).toBeInTheDocument();
+ });
+
+ it("searches docs and highlights matching snippet text", async () => {
+ const documentationResult: SearchItem = {
+ description: "/docs",
+ href: "/docs",
+ id: "docs:/docs",
+ snippet: "Use dark mode themes in production.",
+ title: "Documentation",
+ };
+ const documentationSearch = vi
+ .fn()
+ .mockResolvedValue([documentationResult]);
+ const onDocumentationSelect = vi.fn();
+
+ render(
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: /search/i }));
+ fireEvent.click(screen.getByRole("tab", { name: "Docs" }));
+ fireEvent.change(
+ screen.getByPlaceholderText("Search docs and components..."),
+ {
+ target: { value: "dark" },
+ },
+ );
+
+ await waitFor(() => {
+ expect(documentationSearch).toHaveBeenCalledWith("dark");
+ });
+
+ await screen.findByText("Documentation");
+ expect(screen.getByText("dark").tagName).toBe("MARK");
+
+ fireEvent.click(screen.getByText("Documentation"));
+ expect(onDocumentationSelect).toHaveBeenCalledWith(documentationResult);
+ });
+});
diff --git a/packages/ui/src/components/search-dialog/search-dialog.tsx b/packages/ui/src/components/search-dialog/search-dialog.tsx
index b2a7b7f6..451ca7b4 100644
--- a/packages/ui/src/components/search-dialog/search-dialog.tsx
+++ b/packages/ui/src/components/search-dialog/search-dialog.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Search } from "lucide-react";
@@ -15,13 +15,17 @@ import {
CommandList,
} from "../command";
-type SearchItem = {
+export type SearchItem = {
description?: string;
+ href?: string;
id: string;
keywords?: string;
+ snippet?: string;
title: string;
};
+type SearchScope = "components" | "docs" | "everything";
+
function useKeyboardShortcut(callback: () => void) {
useEffect(() => {
const down = (event: KeyboardEvent) => {
@@ -56,83 +60,634 @@ function useKeyboardShortcut(callback: () => void) {
type SearchDialogProps = {
buttonText?: string;
buttonTextMobile?: string;
+ docsEmptyText?: string;
+ docsGroupHeading?: string;
+ docsSearch?: (query: string) => Promise;
emptyText?: string;
enableKeyboardShortcut?: boolean;
groupHeading?: string;
items: SearchItem[];
+ minDocsSearchLength?: number;
+ onDocsSelect?: (item: SearchItem) => void;
onSelect: (item: SearchItem) => void;
+ scopeLabels?: Partial>;
searchPlaceholder?: string;
};
-export function SearchDialog({
- buttonText = "Search...",
- buttonTextMobile = "Search...",
- emptyText = "No results found.",
- enableKeyboardShortcut = true,
+const DEFAULT_SCOPE_LABELS: Record = {
+ components: "Components",
+ docs: "Docs",
+ everything: "Everything",
+};
+
+function getItemValue(item: SearchItem) {
+ return [
+ item.title,
+ item.description,
+ item.snippet,
+ item.keywords,
+ item.href,
+ item.id,
+ ]
+ .filter(Boolean)
+ .join(" ");
+}
+
+function HighlightedText({ query, text }: { query: string; text: string }) {
+ const trimmedQuery = query.trim();
+
+ if (!trimmedQuery) {
+ return text;
+ }
+
+ const index = text.toLowerCase().indexOf(trimmedQuery.toLowerCase());
+
+ if (index === -1) {
+ return text;
+ }
+
+ return (
+ <>
+ {text.slice(0, index)}
+
+ {text.slice(index, index + trimmedQuery.length)}
+
+ {text.slice(index + trimmedQuery.length)}
+ >
+ );
+}
+
+function ScopeTabs({
+ labels,
+ onScopeChange,
+ scope,
+}: {
+ labels: Record;
+ onScopeChange: (scope: SearchScope) => void;
+ scope: SearchScope;
+}) {
+ const scopes: SearchScope[] = ["components", "docs", "everything"];
+
+ return (
+
+ {scopes.map((nextScope) => (
+
+ ))}
+
+ );
+}
+
+function SearchResultContent({
+ item,
+ query,
+}: {
+ item: SearchItem;
+ query: string;
+}) {
+ return (
+
+ {item.title}
+ {item.snippet ? (
+
+
+
+ ) : item.description ? (
+
+ {item.description}
+
+ ) : null}
+
+ );
+}
+
+function SearchTriggerButton({
+ buttonText,
+ buttonTextMobile,
+ onOpen,
+}: {
+ buttonText?: string;
+ buttonTextMobile?: string;
+ onOpen: () => void;
+}) {
+ return (
+
+ );
+}
+
+function ComponentResultsGroup({
groupHeading,
+ hasDocumentationSearch,
items,
+ labels,
onSelect,
- searchPlaceholder = "Search...",
-}: SearchDialogProps) {
- const [open, setOpen] = useState(false);
+ query,
+}: {
+ groupHeading?: string;
+ hasDocumentationSearch: boolean;
+ items: SearchItem[];
+ labels: Record;
+ onSelect: (item: SearchItem) => void;
+ query: string;
+}) {
+ return (
+
+ {items.map((item) => (
+ {
+ onSelect(item);
+ }}
+ value={getItemValue(item)}
+ >
+
+
+ ))}
+
+ );
+}
+
+function DocumentationStatusItem({
+ documentationSearchLength,
+ trimmedQuery,
+}: {
+ documentationSearchLength: number;
+ trimmedQuery: string;
+}) {
+ return (
+
+
+ Type at least {documentationSearchLength} characters to search docs.
+
+
+ );
+}
+
+function DocumentationLoadingItem({ trimmedQuery }: { trimmedQuery: string }) {
+ return (
+
+ Searching docs...
+
+ );
+}
+
+function DocumentationResultsGroup({
+ docsGroupHeading,
+ documentationItems,
+ documentationLoading,
+ documentationSearchLength,
+ onSelect,
+ query,
+ scope,
+ trimmedQuery,
+}: {
+ docsGroupHeading?: string;
+ documentationItems: SearchItem[];
+ documentationLoading: boolean;
+ documentationSearchLength: number;
+ onSelect: (item: SearchItem) => void;
+ query: string;
+ scope: SearchScope;
+ trimmedQuery: string;
+}) {
+ const showMinimumLengthPrompt =
+ scope === "docs" && trimmedQuery.length < documentationSearchLength;
+ const showDocumentationItems =
+ !documentationLoading && trimmedQuery.length >= documentationSearchLength;
- const sortedItems = [...items].sort((a, b) => a.title.localeCompare(b.title));
+ return (
+
+ {showMinimumLengthPrompt ? (
+
+ ) : null}
+ {documentationLoading ? (
+
+ ) : null}
+ {showDocumentationItems
+ ? documentationItems.map((item) => (
+ {
+ onSelect(item);
+ }}
+ value={getItemValue(item)}
+ >
+
+
+ ))
+ : null}
+
+ );
+}
+
+function SearchDialogList({
+ currentEmptyText,
+ docsGroupHeading,
+ documentationItems,
+ documentationLoading,
+ documentationSearchLength,
+ groupHeading,
+ hasDocumentationSearch,
+ labels,
+ onComponentSelect,
+ onDocumentationSelect,
+ query,
+ scope,
+ showComponents,
+ showDocumentation,
+ sortedItems,
+ trimmedQuery,
+}: {
+ currentEmptyText: string;
+ docsGroupHeading?: string;
+ documentationItems: SearchItem[];
+ documentationLoading: boolean;
+ documentationSearchLength: number;
+ groupHeading?: string;
+ hasDocumentationSearch: boolean;
+ labels: Record;
+ onComponentSelect: (item: SearchItem) => void;
+ onDocumentationSelect: (item: SearchItem) => void;
+ query: string;
+ scope: SearchScope;
+ showComponents: boolean;
+ showDocumentation: boolean;
+ sortedItems: SearchItem[];
+ trimmedQuery: string;
+}) {
+ return (
+
+ {currentEmptyText}
+ {showComponents ? (
+
+ ) : null}
+ {showDocumentation ? (
+
+ ) : null}
+
+ );
+}
- useKeyboardShortcut(() => {
- if (enableKeyboardShortcut) {
+type DocumentationSearchOptions = {
+ docsSearch?: (query: string) => Promise;
+ minDocsSearchLength?: number;
+};
+
+function useDocumentationSearch({
+ docsSearch,
+ minDocsSearchLength,
+}: DocumentationSearchOptions) {
+ const [documentationItems, setDocumentationItems] = useState(
+ [],
+ );
+ const [documentationLoading, setDocumentationLoading] = useState(false);
+ const activeDocumentationRequest = useRef(0);
+ const documentationSearchLength = minDocsSearchLength ?? 2;
+
+ const runDocumentationSearch = useCallback(
+ (nextQuery: string, nextScope: SearchScope) => {
+ const nextTrimmedQuery = nextQuery.trim();
+ const nextRequest = activeDocumentationRequest.current + 1;
+ activeDocumentationRequest.current = nextRequest;
+
+ if (
+ !docsSearch ||
+ nextScope === "components" ||
+ nextTrimmedQuery.length < documentationSearchLength
+ ) {
+ setDocumentationItems([]);
+ setDocumentationLoading(false);
+ return;
+ }
+
+ setDocumentationLoading(true);
+
+ docsSearch(nextTrimmedQuery)
+ .then((results) => {
+ if (activeDocumentationRequest.current === nextRequest) {
+ setDocumentationItems(results);
+ }
+ })
+ .catch(() => {
+ if (activeDocumentationRequest.current === nextRequest) {
+ setDocumentationItems([]);
+ }
+ })
+ .finally(() => {
+ if (activeDocumentationRequest.current === nextRequest) {
+ setDocumentationLoading(false);
+ }
+ });
+ },
+ [docsSearch, documentationSearchLength],
+ );
+
+ return {
+ documentationItems,
+ documentationLoading,
+ documentationSearchLength,
+ hasDocumentationSearch: docsSearch !== undefined,
+ runDocumentationSearch,
+ };
+}
+
+type SearchDialogHandlersOptions = {
+ enableKeyboardShortcut?: boolean;
+ onDocsSelect?: (item: SearchItem) => void;
+ onSelect: (item: SearchItem) => void;
+ runDocumentationSearch: (query: string, scope: SearchScope) => void;
+};
+
+function useSearchDialogHandlers({
+ enableKeyboardShortcut,
+ onDocsSelect,
+ onSelect,
+ runDocumentationSearch,
+}: SearchDialogHandlersOptions) {
+ const [open, setOpen] = useState(false);
+ const [query, setQuery] = useState("");
+ const [scope, setScope] = useState("components");
+
+ const toggleOpen = useCallback(() => {
+ if (enableKeyboardShortcut ?? true) {
setOpen((previous) => !previous);
}
- });
+ }, [enableKeyboardShortcut]);
- const handleSelect = (item: SearchItem) => {
- setOpen(false);
- onSelect(item);
+ useKeyboardShortcut(toggleOpen);
+
+ const handleQueryChange = useCallback(
+ (nextQuery: string) => {
+ setQuery(nextQuery);
+ runDocumentationSearch(nextQuery, scope);
+ },
+ [runDocumentationSearch, scope],
+ );
+
+ const handleScopeChange = useCallback(
+ (nextScope: SearchScope) => {
+ setScope(nextScope);
+ runDocumentationSearch(query, nextScope);
+ },
+ [query, runDocumentationSearch],
+ );
+
+ const handleOpenChange = useCallback(
+ (nextOpen: boolean) => {
+ setOpen(nextOpen);
+
+ if (nextOpen) {
+ runDocumentationSearch(query, scope);
+ }
+ },
+ [query, runDocumentationSearch, scope],
+ );
+
+ const handleComponentSelect = useCallback(
+ (item: SearchItem) => {
+ setOpen(false);
+ onSelect(item);
+ },
+ [onSelect],
+ );
+
+ const handleDocumentationSelect = useCallback(
+ (item: SearchItem) => {
+ setOpen(false);
+ (onDocsSelect ?? onSelect)(item);
+ },
+ [onDocsSelect, onSelect],
+ );
+
+ return {
+ handleComponentSelect,
+ handleDocumentationSelect,
+ handleOpenChange,
+ handleQueryChange,
+ handleScopeChange,
+ open,
+ query,
+ scope,
};
+}
+function getCurrentEmptyText({
+ docsEmptyText,
+ documentationSearchLength,
+ emptyText,
+ scope,
+ trimmedQuery,
+}: {
+ docsEmptyText?: string;
+ documentationSearchLength: number;
+ emptyText?: string;
+ scope: SearchScope;
+ trimmedQuery: string;
+}) {
+ if (scope === "docs" && trimmedQuery.length < documentationSearchLength) {
+ return `Type at least ${documentationSearchLength} characters to search docs.`;
+ }
+
+ if (scope === "docs") {
+ return docsEmptyText ?? "No docs found.";
+ }
+
+ return emptyText ?? "No results found.";
+}
+
+type SearchDialogViewProps = Pick<
+ SearchDialogProps,
+ | "buttonText"
+ | "buttonTextMobile"
+ | "docsGroupHeading"
+ | "groupHeading"
+ | "searchPlaceholder"
+> & {
+ currentEmptyText: string;
+ documentationSearch: ReturnType;
+ handlers: ReturnType;
+ labels: Record;
+ showDocumentation: boolean;
+ sortedItems: SearchItem[];
+ trimmedQuery: string;
+};
+
+function SearchDialogView({
+ buttonText,
+ buttonTextMobile,
+ currentEmptyText,
+ docsGroupHeading,
+ documentationSearch,
+ groupHeading,
+ handlers,
+ labels,
+ searchPlaceholder,
+ showDocumentation,
+ sortedItems,
+ trimmedQuery,
+}: SearchDialogViewProps) {
return (
<>
-
-
-
-
- {emptyText}
-
- {sortedItems.map((item) => (
- {
- handleSelect(item);
- }}
- value={`${item.title} ${item.description || ""} ${item.keywords || ""} ${item.id}`}
- >
-
- {item.title}
- {item.description ? (
-
- {item.description}
-
- ) : null}
-
-
- ))}
-
-
+
+ {documentationSearch.hasDocumentationSearch ? (
+
+ ) : null}
+
>
);
}
+
+export function SearchDialog({
+ buttonText,
+ buttonTextMobile,
+ docsEmptyText,
+ docsGroupHeading,
+ docsSearch,
+ emptyText,
+ enableKeyboardShortcut,
+ groupHeading,
+ items,
+ minDocsSearchLength,
+ onDocsSelect,
+ onSelect,
+ scopeLabels,
+ searchPlaceholder,
+}: SearchDialogProps) {
+ const documentationSearch = useDocumentationSearch({
+ docsSearch,
+ minDocsSearchLength,
+ });
+ const handlers = useSearchDialogHandlers({
+ enableKeyboardShortcut,
+ onDocsSelect,
+ onSelect,
+ runDocumentationSearch: documentationSearch.runDocumentationSearch,
+ });
+ const labels = { ...DEFAULT_SCOPE_LABELS, ...scopeLabels };
+ const trimmedQuery = handlers.query.trim();
+ const sortedItems = useMemo(
+ () => [...items].sort((a, b) => a.title.localeCompare(b.title)),
+ [items],
+ );
+ const currentEmptyText = getCurrentEmptyText({
+ docsEmptyText,
+ documentationSearchLength: documentationSearch.documentationSearchLength,
+ emptyText,
+ scope: handlers.scope,
+ trimmedQuery,
+ });
+ const showDocumentation =
+ documentationSearch.hasDocumentationSearch &&
+ handlers.scope !== "components";
+
+ return (
+
+ );
+}
diff --git a/packages/ui/src/components/search-dialog/search-dialog.visual.tsx b/packages/ui/src/components/search-dialog/search-dialog.visual.tsx
index 078e80d8..3ac39c2d 100644
--- a/packages/ui/src/components/search-dialog/search-dialog.visual.tsx
+++ b/packages/ui/src/components/search-dialog/search-dialog.visual.tsx
@@ -4,7 +4,12 @@ import { SearchDialog } from "./search-dialog";
test.describe("SearchDialog Visual", () => {
test("default", async ({ mount, page }) => {
- await mount();
+ await mount(
+ {}}
+ />,
+ );
await expect(page).toHaveScreenshot("search-dialog-default.png");
});
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 40e57535..34c7e8b6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -67,7 +67,7 @@ importers:
version: 10.1.0(@types/react@19.2.13)(react@19.2.4)
shadcn:
specifier: canary
- version: 3.5.1-canary.0(@types/node@22.19.10)(typescript@5.9.3)
+ version: 4.2.0-canary.0(@types/node@22.19.10)(typescript@5.9.3)
tailwind-merge:
specifier: ^3.3.1
version: 3.4.0
@@ -102,6 +102,9 @@ importers:
eslint:
specifier: ^9.39.1
version: 9.39.2(jiti@1.21.7)
+ pagefind:
+ specifier: ^1.5.2
+ version: 1.5.2
postcss:
specifier: ^8.5
version: 8.5.6
@@ -368,10 +371,6 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
- '@antfu/ni@25.0.0':
- resolution: {integrity: sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA==}
- hasBin: true
-
'@asamuzakjp/css-color@3.2.0':
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
@@ -1676,6 +1675,41 @@ packages:
'@open-draft/until@2.1.0':
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
+ '@pagefind/darwin-arm64@1.5.2':
+ resolution: {integrity: sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@pagefind/darwin-x64@1.5.2':
+ resolution: {integrity: sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@pagefind/freebsd-x64@1.5.2':
+ resolution: {integrity: sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@pagefind/linux-arm64@1.5.2':
+ resolution: {integrity: sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@pagefind/linux-x64@1.5.2':
+ resolution: {integrity: sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==}
+ cpu: [x64]
+ os: [linux]
+
+ '@pagefind/windows-arm64@1.5.2':
+ resolution: {integrity: sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@pagefind/windows-x64@1.5.2':
+ resolution: {integrity: sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==}
+ cpu: [x64]
+ os: [win32]
+
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -2835,6 +2869,9 @@ packages:
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+ '@types/validate-npm-package-name@4.0.2':
+ resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==}
+
'@types/wait-on@5.3.4':
resolution: {integrity: sha512-EBsPjFMrFlMbbUFf9D1Fp+PAB2TwmUn7a3YtHyD9RLuTIk1jDd8SxXVAoez2Ciy+8Jsceo2MYEYZzJ/DvorOKw==}
@@ -3293,10 +3330,6 @@ packages:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
- ansis@4.2.0:
- resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
- engines: {node: '>=14'}
-
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
@@ -4608,9 +4641,6 @@ packages:
fuzzysort@3.1.0:
resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==}
- fzf@0.5.2:
- resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==}
-
generator-function@2.0.1:
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
engines: {node: '>= 0.4'}
@@ -4999,6 +5029,10 @@ packages:
eslint: '*'
typescript: '>=4.7.4'
+ is-in-ssh@1.0.0:
+ resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==}
+ engines: {node: '>=20'}
+
is-inside-container@1.0.0:
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
engines: {node: '>=14.16'}
@@ -5908,6 +5942,10 @@ packages:
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
engines: {node: '>=18'}
+ open@11.0.0:
+ resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
+ engines: {node: '>=20'}
+
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -5958,8 +5996,9 @@ packages:
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
- package-manager-detector@1.6.0:
- resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
+ pagefind@1.5.2:
+ resolution: {integrity: sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==}
+ hasBin: true
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
@@ -6128,6 +6167,10 @@ packages:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'}
+ postcss-selector-parser@7.1.1:
+ resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
+ engines: {node: '>=4'}
+
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
@@ -6139,6 +6182,10 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
+ powershell-utils@0.1.0:
+ resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
+ engines: {node: '>=20'}
+
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -6525,8 +6572,8 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
- shadcn@3.5.1-canary.0:
- resolution: {integrity: sha512-aw+JbFI0RxoxHdr2Q+vON6zCC6GggKQjOU1U/R1LNrNzDhXEnk6DKtdXZtN+Ej1oRadCAOV7yDMbi4jber/7Ig==}
+ shadcn@4.2.0-canary.0:
+ resolution: {integrity: sha512-J8dSM3vMuOSJjvIkgja1JMqF3i8yAVGh/aFLx2yrC8Pm2F4nA2071rfhiJLmH10zo42AB8hQKYyrJIKniDhA9A==}
hasBin: true
sharp@0.34.5:
@@ -7144,6 +7191,7 @@ packages:
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
+ deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
v8-to-istanbul@9.3.0:
@@ -7158,6 +7206,10 @@ packages:
typescript:
optional: true
+ validate-npm-package-name@7.0.2:
+ resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==}
+ engines: {node: ^20.17.0 || >=22.9.0}
+
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
@@ -7420,6 +7472,10 @@ packages:
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
engines: {node: '>=18'}
+ wsl-utils@0.3.1:
+ resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
+ engines: {node: '>=20'}
+
xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
@@ -7509,13 +7565,6 @@ snapshots:
'@alloc/quick-lru@5.2.0': {}
- '@antfu/ni@25.0.0':
- dependencies:
- ansis: 4.2.0
- fzf: 0.5.2
- package-manager-detector: 1.6.0
- tinyexec: 1.0.2
-
'@asamuzakjp/css-color@3.2.0':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
@@ -8734,6 +8783,27 @@ snapshots:
'@open-draft/until@2.1.0': {}
+ '@pagefind/darwin-arm64@1.5.2':
+ optional: true
+
+ '@pagefind/darwin-x64@1.5.2':
+ optional: true
+
+ '@pagefind/freebsd-x64@1.5.2':
+ optional: true
+
+ '@pagefind/linux-arm64@1.5.2':
+ optional: true
+
+ '@pagefind/linux-x64@1.5.2':
+ optional: true
+
+ '@pagefind/windows-arm64@1.5.2':
+ optional: true
+
+ '@pagefind/windows-x64@1.5.2':
+ optional: true
+
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -9936,6 +10006,8 @@ snapshots:
'@types/unist@3.0.3': {}
+ '@types/validate-npm-package-name@4.0.2': {}
+
'@types/wait-on@5.3.4':
dependencies:
'@types/node': 22.19.10
@@ -10435,8 +10507,6 @@ snapshots:
ansi-styles@6.2.3: {}
- ansis@4.2.0: {}
-
any-promise@1.3.0: {}
anymatch@3.1.3:
@@ -11953,8 +12023,6 @@ snapshots:
fuzzysort@3.1.0: {}
- fzf@0.5.2: {}
-
generator-function@2.0.1: {}
gensync@1.0.0-beta.2: {}
@@ -12383,6 +12451,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ is-in-ssh@1.0.0: {}
+
is-inside-container@1.0.0:
dependencies:
is-docker: 3.0.0
@@ -13774,6 +13844,15 @@ snapshots:
is-inside-container: 1.0.0
wsl-utils: 0.1.0
+ open@11.0.0:
+ dependencies:
+ default-browser: 5.5.0
+ define-lazy-prop: 3.0.0
+ is-in-ssh: 1.0.0
+ is-inside-container: 1.0.0
+ powershell-utils: 0.1.0
+ wsl-utils: 0.3.1
+
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -13836,7 +13915,15 @@ snapshots:
package-json-from-dist@1.0.1: {}
- package-manager-detector@1.6.0: {}
+ pagefind@1.5.2:
+ optionalDependencies:
+ '@pagefind/darwin-arm64': 1.5.2
+ '@pagefind/darwin-x64': 1.5.2
+ '@pagefind/freebsd-x64': 1.5.2
+ '@pagefind/linux-arm64': 1.5.2
+ '@pagefind/linux-x64': 1.5.2
+ '@pagefind/windows-arm64': 1.5.2
+ '@pagefind/windows-x64': 1.5.2
parent-module@1.0.1:
dependencies:
@@ -13972,6 +14059,11 @@ snapshots:
cssesc: 3.0.0
util-deprecate: 1.0.2
+ postcss-selector-parser@7.1.1:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
postcss-value-parser@4.2.0: {}
postcss@8.4.31:
@@ -13986,6 +14078,8 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
+ powershell-utils@0.1.0: {}
+
prelude-ls@1.2.1: {}
prettier-linter-helpers@1.0.1:
@@ -14494,15 +14588,15 @@ snapshots:
setprototypeof@1.2.0: {}
- shadcn@3.5.1-canary.0(@types/node@22.19.10)(typescript@5.9.3):
+ shadcn@4.2.0-canary.0(@types/node@22.19.10)(typescript@5.9.3):
dependencies:
- '@antfu/ni': 25.0.0
'@babel/core': 7.29.0
'@babel/parser': 7.29.0
'@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
'@babel/preset-typescript': 7.28.5(@babel/core@7.29.0)
'@dotenvx/dotenvx': 1.52.0
'@modelcontextprotocol/sdk': 1.26.0(zod@3.25.76)
+ '@types/validate-npm-package-name': 4.0.2
browserslist: 4.28.1
commander: 14.0.3
cosmiconfig: 9.0.0(typescript@5.9.3)
@@ -14517,13 +14611,17 @@ snapshots:
kleur: 4.1.5
msw: 2.12.9(@types/node@22.19.10)(typescript@5.9.3)
node-fetch: 3.3.2
+ open: 11.0.0
ora: 8.2.0
postcss: 8.5.6
+ postcss-selector-parser: 7.1.1
prompts: 2.4.2
recast: 0.23.11
stringify-object: 5.0.0
+ tailwind-merge: 3.4.0
ts-morph: 26.0.0
tsconfig-paths: 4.2.0
+ validate-npm-package-name: 7.0.2
zod: 3.25.76
zod-to-json-schema: 3.25.1(zod@3.25.76)
transitivePeerDependencies:
@@ -15271,6 +15369,8 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
+ validate-npm-package-name@7.0.2: {}
+
vary@1.1.2: {}
vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
@@ -15515,6 +15615,11 @@ snapshots:
dependencies:
is-wsl: 3.1.1
+ wsl-utils@0.3.1:
+ dependencies:
+ is-wsl: 3.1.1
+ powershell-utils: 0.1.0
+
xml-name-validator@5.0.0: {}
xml@1.0.1: {}
diff --git a/turbo.json b/turbo.json
index 4661c108..7cc67799 100644
--- a/turbo.json
+++ b/turbo.json
@@ -4,7 +4,13 @@
"tasks": {
"build": {
"dependsOn": ["^build"],
- "outputs": [".next/**", "!.next/cache/**", "dist/**", "public/r/**"]
+ "outputs": [
+ ".next/**",
+ "!.next/cache/**",
+ "dist/**",
+ "public/r/**",
+ "public/_pagefind/**"
+ ]
},
"clean": {
"cache": false