diff --git a/packages/fern-docs/components/src/FernDropdown.tsx b/packages/fern-docs/components/src/FernDropdown.tsx index 7219237022..179457dba1 100644 --- a/packages/fern-docs/components/src/FernDropdown.tsx +++ b/packages/fern-docs/components/src/FernDropdown.tsx @@ -84,6 +84,7 @@ export declare namespace FernDropdown { }; triggerAsChild?: boolean; radioGroupProps?: ComponentProps; + header?: ReactNode; } } @@ -105,7 +106,8 @@ export const FernDropdown = forwardRef { @@ -138,6 +140,11 @@ export const FernDropdown = forwardRef + {header ? ( +
+ {header} +
+ ) : null} {Array.isArray(value) ? (
{options.map((option, idx) => diff --git a/packages/fern-docs/components/src/header/DropdownSearch.tsx b/packages/fern-docs/components/src/header/DropdownSearch.tsx new file mode 100644 index 0000000000..a1f0179825 --- /dev/null +++ b/packages/fern-docs/components/src/header/DropdownSearch.tsx @@ -0,0 +1,40 @@ +"use client"; + +import * as React from "react"; +import { cn } from "../cn"; + +export interface DropdownSearchProps extends React.InputHTMLAttributes { + label?: string; + isOpen?: boolean; + listboxId?: string; +} + +export const DropdownSearch = React.forwardRef( + ({ className, label = "Search products", isOpen, listboxId, ...rest }, ref) => { + return ( +
+ + +
+ ); + } +); + +DropdownSearch.displayName = "DropdownSearch"; diff --git a/packages/fern-docs/components/src/header/ProductDropdownClient.tsx b/packages/fern-docs/components/src/header/ProductDropdownClient.tsx index 122d383815..5ce3894f3b 100644 --- a/packages/fern-docs/components/src/header/ProductDropdownClient.tsx +++ b/packages/fern-docs/components/src/header/ProductDropdownClient.tsx @@ -4,10 +4,13 @@ import { slugToHref } from "@fern-api/docs-utils"; import type { FernNavigation } from "@fern-api/fdr-sdk"; import { useIsDesktop } from "@fern-ui/react-commons"; import { ChevronDown, ChevronsUpDown } from "lucide-react"; +// === NEW === +import * as React from "react"; import { cn } from "../cn"; import { FernDropdown } from "../FernDropdown"; import { FernSelectionItem } from "../FernSelectionItem"; import { useCurrentProductId, useCurrentProductSlug } from "../state/navigation"; +import { DropdownSearch } from "./DropdownSearch"; export interface ProductDropdownItem { productId: string; @@ -39,6 +42,15 @@ export function ProductDropdownClient({ products.find((product) => product.default) ?? products.find((product) => product.productId === fallbackProduct.productId); + // === NEW: search (only used for large menus) === + const enableSearch = products.length > 10; + const [query, setQuery] = React.useState(""); + const normalized = query.trim().toLowerCase(); + const filteredProducts = React.useMemo(() => { + if (!enableSearch || !normalized) return products; + return products.filter((p) => `${p.title} ${p.subtitle ?? ""}`.toLowerCase().includes(normalized)); + }, [enableSearch, normalized, products]); + if (!currentProduct) { return null; } @@ -46,7 +58,7 @@ export function ProductDropdownClient({ return ( { + options={filteredProducts.map(({ icon, image, productId, title, slug, subtitle, defaultSlug, href }) => { const productHref = href ?? slugToHref( @@ -79,6 +91,19 @@ export function ProductDropdownClient({ radioGroupProps={{ className: "fern-product-selector-radio-group" }} + header={ + enableSearch ? ( + setQuery(e.target.value)} + /> + ) : null + } + onValueChange={() => { + if (enableSearch) setQuery(""); + }} >