diff --git a/src/routes/Dashboard/DashboardComponentsV2View.test.tsx b/src/routes/Dashboard/DashboardComponentsV2View.test.tsx
index 779f9d865..4e67b3e75 100644
--- a/src/routes/Dashboard/DashboardComponentsV2View.test.tsx
+++ b/src/routes/Dashboard/DashboardComponentsV2View.test.tsx
@@ -216,6 +216,7 @@ import {
type SourceFilterOption,
} from "./DashboardComponentsV2SourceFilter";
import {
+ buildComponentCollectionMatches,
createRegisteredLibrariesFingerprint,
DashboardComponentsV2View,
} from "./DashboardComponentsV2View";
@@ -406,6 +407,53 @@ describe("SourceFilterBar", () => {
});
});
+describe("buildComponentCollectionMatches", () => {
+ it("returns registered library collections matching the query", () => {
+ const result = buildComponentCollectionMatches(
+ [
+ createIndexEntry("standard-component", {
+ kind: "standard",
+ label: "Standard",
+ id: "standard",
+ }),
+ createIndexEntry("load-csv", {
+ kind: "registered",
+ label: "Data tools",
+ id: "data-tools",
+ }),
+ createIndexEntry("clean-data", {
+ kind: "registered",
+ label: "Data tools",
+ id: "data-tools",
+ }),
+ ],
+ "data",
+ );
+
+ expect(result).toEqual([
+ {
+ id: "data-tools",
+ label: "Data tools",
+ count: 2,
+ previewNames: ["load-csv", "clean-data"],
+ },
+ ]);
+ });
+
+ it("returns no collections for empty or unmatched queries", () => {
+ const index = [
+ createIndexEntry("load-csv", {
+ kind: "registered",
+ label: "Data tools",
+ id: "data-tools",
+ }),
+ ];
+
+ expect(buildComponentCollectionMatches(index, "")).toEqual([]);
+ expect(buildComponentCollectionMatches(index, "training")).toEqual([]);
+ });
+});
+
describe("DashboardComponentsV2View", () => {
beforeEach(() => {
routeMocks.aiDescriptionsEnabled = false;
@@ -452,6 +500,35 @@ describe("DashboardComponentsV2View", () => {
});
});
+ it("shows registered library collection results when the query matches", async () => {
+ render();
+
+ fireEvent.change(screen.getByLabelText("Search components"), {
+ target: { value: "github" },
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("GitHub library")).toBeInTheDocument();
+ });
+ });
+
+ it("hides collection results from disabled sources", async () => {
+ render();
+
+ fireEvent.click(
+ screen.getByRole("button", {
+ name: "Registered libraries source (1 component)",
+ }),
+ );
+ fireEvent.change(screen.getByLabelText("Search components"), {
+ target: { value: "github" },
+ });
+
+ await waitFor(() => {
+ expect(screen.queryByText("GitHub library")).not.toBeInTheDocument();
+ });
+ });
+
it("initializes search state from URL params", () => {
routeMocks.search = {
q: "registered",
diff --git a/src/routes/Dashboard/DashboardComponentsV2View.tsx b/src/routes/Dashboard/DashboardComponentsV2View.tsx
index c82f877e7..08c4f8af1 100644
--- a/src/routes/Dashboard/DashboardComponentsV2View.tsx
+++ b/src/routes/Dashboard/DashboardComponentsV2View.tsx
@@ -193,6 +193,45 @@ type ComponentLibraryFolder = Parameters[0];
type UserFolder = { components?: ComponentReference[] };
type RerankMode = "smart" | "deep";
+interface ComponentCollectionMatch {
+ id: string;
+ label: string;
+ count: number;
+ previewNames: string[];
+}
+
+export function buildComponentCollectionMatches(
+ index: IndexEntry[],
+ query: string,
+): ComponentCollectionMatch[] {
+ const trimmedQuery = query.trim().toLowerCase();
+ if (!trimmedQuery) return [];
+
+ const bySourceId = new Map();
+ for (const entry of index) {
+ if (entry.source.kind !== "registered") continue;
+ const current = bySourceId.get(entry.source.id);
+ if (current) {
+ current.count += 1;
+ if (current.previewNames.length < 3)
+ current.previewNames.push(entry.name);
+ } else {
+ bySourceId.set(entry.source.id, {
+ id: entry.source.id,
+ label: entry.source.label,
+ count: 1,
+ previewNames: [entry.name],
+ });
+ }
+ }
+
+ return Array.from(bySourceId.values())
+ .filter((collection) =>
+ collection.label.toLowerCase().includes(trimmedQuery),
+ )
+ .sort((a, b) => a.label.localeCompare(b.label));
+}
+
interface ComponentCardProps {
reference: ComponentReference;
source?: ComponentSearchSource;
@@ -316,6 +355,29 @@ const ComponentCard = ({
);
};
+interface CollectionCardProps {
+ collection: ComponentCollectionMatch;
+}
+
+const CollectionCard = ({ collection }: CollectionCardProps) => (
+
+
+
+
+ {collection.label}
+
+
+ {collection.count} component{collection.count === 1 ? "" : "s"}
+
+
+ {collection.previewNames.length > 0 && (
+
+ Includes {collection.previewNames.join(", ")}
+
+ )}
+
+);
+
interface ComponentDescriptionPanelProps {
prefilledDescription?: string;
generatedDescription?: string;
@@ -820,6 +882,11 @@ export const DashboardComponentsV2View = () => {
0,
LEXICAL_RESULT_LIMIT,
);
+ const collectionMatches = buildComponentCollectionMatches(
+ filteredIndex,
+ deferredQuery,
+ );
+
const aiCandidateMatches: LexicalMatch[] = (() => {
if (trimmedQuery.length === 0) return [];
return broadLexicalMatches;
@@ -1147,7 +1214,11 @@ export const DashboardComponentsV2View = () => {
);
}
- if (lexicalMatches.length === 0 && !rerankActive) {
+ if (
+ lexicalMatches.length === 0 &&
+ collectionMatches.length === 0 &&
+ !rerankActive
+ ) {
return (
No components matched “{trimmedQuery}”. Try different terms or check
@@ -1157,11 +1228,21 @@ export const DashboardComponentsV2View = () => {
}
return (
+ {collectionMatches.length > 0 && (
+
+
+ Collection{collectionMatches.length === 1 ? "" : "s"}
+
+ {collectionMatches.map((collection) => (
+
+ ))}
+
+ )}
{rerankActive
? `AI-ranked ${displayedResults.length} result${displayedResults.length === 1 ? "" : "s"} for “${trimmedQuery}”`
- : `${displayedResults.length} result${displayedResults.length === 1 ? "" : "s"} for “${trimmedQuery}”`}
+ : `${displayedResults.length} component result${displayedResults.length === 1 ? "" : "s"} for “${trimmedQuery}”`}
{rerankActive && (