diff --git a/src/routes/v2/pages/Editor/components/ComponentSearchResults.test.tsx b/src/routes/v2/pages/Editor/components/ComponentSearchResults.test.tsx
index eca59c545..aa6e3ac3f 100644
--- a/src/routes/v2/pages/Editor/components/ComponentSearchResults.test.tsx
+++ b/src/routes/v2/pages/Editor/components/ComponentSearchResults.test.tsx
@@ -42,12 +42,27 @@ const baseProps = {
{ label: "dataset", kind: "type" },
] satisfies ComponentSearchSuggestion[],
isLoading: false,
+ isSearching: false,
isRerankActive: false,
onClearRerank: vi.fn(),
onSuggestedSearch: vi.fn(),
};
describe("ComponentSearchResults", () => {
+ it("shows a skeleton while search is pending", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByTestId("search-results-skeleton")).toBeInTheDocument();
+ expect(screen.getByText("Searching")).toBeInTheDocument();
+ });
+
it("shows actionable no-results guidance with clickable suggestions", () => {
const onSuggestedSearch = vi.fn();
render(
diff --git a/src/routes/v2/pages/Editor/components/ComponentSearchResults.tsx b/src/routes/v2/pages/Editor/components/ComponentSearchResults.tsx
index 2aa54d8df..1a9eaccab 100644
--- a/src/routes/v2/pages/Editor/components/ComponentSearchResults.tsx
+++ b/src/routes/v2/pages/Editor/components/ComponentSearchResults.tsx
@@ -8,6 +8,7 @@ import FolderItem from "@/components/shared/ReactFlow/FlowSidebar/components/Fol
import { Button } from "@/components/ui/button";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import { Separator } from "@/components/ui/separator";
+import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { Paragraph, Text } from "@/components/ui/typography";
import type { ComponentSearchSuggestion } from "@/services/componentSearchSuggestions";
@@ -21,6 +22,7 @@ interface ComponentSearchResultsProps {
browseFolders: UIComponentFolder[];
searchSuggestions: ComponentSearchSuggestion[];
isLoading: boolean;
+ isSearching: boolean;
isRerankActive: boolean;
onClearRerank: () => void;
onSuggestedSearch: (query: string) => void;
@@ -32,17 +34,35 @@ export function ComponentSearchResults({
browseFolders,
searchSuggestions,
isLoading,
+ isSearching,
isRerankActive,
onClearRerank,
onSuggestedSearch,
}: ComponentSearchResultsProps) {
- if (isLoading) {
+ if (isLoading || isSearching) {
return (
-
-
- Search Results
+
+
+
+ {isSearching ? "Searching" : "Search Results"}
+
+
+ {Array.from({ length: 5 }, (_, index) => (
+
+
+
+
+
+
+
+ ))}
+
);
}
diff --git a/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.test.tsx b/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.test.tsx
index 4bcbd4c91..d6996243a 100644
--- a/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.test.tsx
+++ b/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.test.tsx
@@ -27,8 +27,17 @@ vi.mock("@/routes/v2/pages/Editor/hooks/useComponentSearchV2State", () => ({
}));
vi.mock("./ComponentSearchResults", () => ({
- ComponentSearchResults: ({ query }: { query: string }) => (
- {query}
+ ComponentSearchResults: ({
+ query,
+ isSearching,
+ }: {
+ query: string;
+ isSearching: boolean;
+ }) => (
+
+
{query}
+
{String(isSearching)}
+
),
}));
@@ -62,6 +71,7 @@ describe("ComponentSearchV2Content", () => {
expect(input).toHaveValue("csv");
expect(screen.getByTestId("results-query")).toHaveTextContent("");
+ expect(screen.getByTestId("results-searching")).toHaveTextContent("true");
await act(async () => {
vi.advanceTimersByTime(499);
@@ -74,6 +84,7 @@ describe("ComponentSearchV2Content", () => {
});
expect(screen.getByTestId("results-query")).toHaveTextContent("csv");
+ expect(screen.getByTestId("results-searching")).toHaveTextContent("false");
});
it("tracks editor component search completions without query text", async () => {
diff --git a/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.tsx b/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.tsx
index 5d246ed75..f9f30a3aa 100644
--- a/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.tsx
+++ b/src/routes/v2/pages/Editor/components/ComponentSearchV2Content.tsx
@@ -19,9 +19,11 @@ const EDITOR_SEARCH_RESULT_DEBOUNCE_MS = 500;
function DebouncedComponentSearchInput({
initialValue,
onCommit,
+ onLocalChange,
}: {
initialValue: string;
onCommit: (value: string) => void;
+ onLocalChange: (value: string) => void;
}) {
const [localValue, setLocalValue] = useDebouncedSearchValue(
onCommit,
@@ -36,7 +38,10 @@ function DebouncedComponentSearchInput({
placeholder="Search components..."
className="w-full pl-8 text-sm h-8 focus-visible:ring-gray-400/50"
value={localValue}
- onChange={(event) => setLocalValue(event.target.value)}
+ onChange={(event) => {
+ setLocalValue(event.target.value);
+ onLocalChange(event.target.value);
+ }}
aria-label="Search components"
autoComplete="off"
/>
@@ -46,6 +51,7 @@ function DebouncedComponentSearchInput({
export function ComponentSearchV2Content() {
const { track } = useAnalytics();
const [query, setQuery] = useState("");
+ const [localQuery, setLocalQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const [, startSearchTransition] = useTransition();
const {
@@ -65,10 +71,12 @@ export function ComponentSearchV2Content() {
};
const handleSuggestedSearch = (value: string) => {
+ setLocalQuery(value);
startSearchTransition(() => setQuery(value));
};
const trimmedDeferredQuery = deferredQuery.trim();
+ const isSearching = localQuery.trim() !== trimmedDeferredQuery;
useEffect(() => {
if (isLoading || trimmedDeferredQuery.length === 0) return;
@@ -121,6 +129,7 @@ export function ComponentSearchV2Content() {