Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions packages/components/src/components/search/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ const SearchButton = forwardRef<HTMLButtonElement, SearchButtonProps>(
<button
className={cn(
"flex w-full items-center gap-2 px-3 py-2 text-left text-sm",
"bg-white dark:bg-stone-900",
"border border-stone-200 dark:border-stone-700",
"bg-transparent",
"border border-stone-200 dark:border-white/10",
"rounded-xl",
"hover:bg-stone-50 dark:hover:bg-stone-800",
"focus:outline-none focus:ring-2 focus:ring-stone-400 dark:focus:ring-stone-600",
"text-stone-500",
"hover:border-stone-300 dark:hover:border-white/20",
"focus:outline-none focus:ring-0 focus:ring-offset-0",
"transition-colors",
"cursor-pointer",
className
)}
onClick={handleClick}
Expand All @@ -45,14 +47,14 @@ const SearchButton = forwardRef<HTMLButtonElement, SearchButtonProps>(
{...props}
>
<SearchIcon
className="shrink-0 text-stone-400 dark:text-stone-500"
className="shrink-0 text-stone-800 dark:text-stone-500"
size={16}
/>
<span className="flex-1 text-stone-500 dark:text-stone-400">
{children || "Search..."}
</span>
{showShortcut && (
<kbd className="hidden items-center gap-0.5 rounded border border-stone-200 bg-stone-100 px-1.5 py-0.5 font-medium font-sans text-stone-500 text-xs sm:inline-flex dark:border-stone-700 dark:bg-stone-800 dark:text-stone-400">
<kbd className="hidden items-center gap-0.5 rounded px-1.5 py-0.5 font-medium font-sans text-stone-500 text-xs sm:inline-flex dark:text-stone-400">
{shortcutText}
</kbd>
)}
Expand Down
37 changes: 34 additions & 3 deletions packages/components/src/components/search/search.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { useState } from "react";
import { Icon } from "../icon";
import {
Search,
SearchButton,
Expand Down Expand Up @@ -56,6 +57,10 @@ const meta: Meta<typeof Search> = {
control: "text",
description: "Additional CSS classes",
},
paddingTop: {
control: "text",
description: "Distance from top of viewport when position is 'top'",
},
emptyState: {
control: false,
description: "Custom empty state component",
Expand All @@ -81,6 +86,7 @@ const mockResults = [
metadata: {
breadcrumbs: ["Documentation", "Introduction"],
},
icon: <Icon color="gray" icon="hash" iconLibrary="lucide" size={18} />,
},
{
id: "2",
Expand All @@ -90,6 +96,7 @@ const mockResults = [
metadata: {
breadcrumbs: ["Documentation", "Setup"],
},
icon: <Icon color="gray" icon="hash" iconLibrary="lucide" size={18} />,
},
{
id: "3",
Expand All @@ -99,6 +106,7 @@ const mockResults = [
metadata: {
breadcrumbs: ["Components", "Search"],
},
icon: <Icon color="gray" icon="hash" iconLibrary="lucide" size={18} />,
},
{
id: "4",
Expand All @@ -108,6 +116,7 @@ const mockResults = [
metadata: {
breadcrumbs: ["Documentation", "Customization"],
},
icon: <Icon color="gray" icon="hash" iconLibrary="lucide" size={18} />,
},
{
id: "5",
Expand All @@ -117,23 +126,43 @@ const mockResults = [
metadata: {
breadcrumbs: ["Documentation", "Customization"],
},
icon: <Icon color="gray" icon="hash" iconLibrary="lucide" size={18} />,
},
];

export const Default: Story = {
args: {
placeholder: "Search...",
position: "top",
className: "",
paddingTop: "64px",
},
parameters: {
docs: {
source: {
transform: (
_code: string,
storyContext: { args: { placeholder?: string; position?: string } }
storyContext: {
args: {
placeholder?: string;
position?: string;
className?: string;
paddingTop?: string;
};
}
) => {
const placeholder = storyContext.args.placeholder || "Search...";
const position = storyContext.args.position || "top";
const className = storyContext.args.className || "";
const paddingTop = storyContext.args.paddingTop || "64px";

const classNameProps = [
className && ` className="${className}"`,
paddingTop !== "64px" && ` paddingTop="${paddingTop}"`,
]
.filter(Boolean)
.join("\n");

return `import { useState } from 'react';
import { Search, SearchButton, SearchResult } from '@mintlify/components';

Expand Down Expand Up @@ -186,7 +215,7 @@ function MyComponent() {
placeholder="${placeholder}"
position="${position}"
onSelectResult={handleSelectResult}
recentSearches={recentSearches}
recentSearches={recentSearches}${classNameProps ? `\n${classNameProps}` : ""}
/>
</>
);
Expand Down Expand Up @@ -232,10 +261,11 @@ function MyComponent() {

return (
<div className="w-[400px] p-6">
<SearchButton onClick={() => setIsOpen(true)} showShortcut={false}>
<SearchButton onClick={() => setIsOpen(true)} showShortcut={true}>
{args.placeholder}
</SearchButton>
<Search
className={args.className}
isLoading={isLoading}
isOpen={isOpen}
onClose={() => {
Expand All @@ -244,6 +274,7 @@ function MyComponent() {
}}
onSearch={handleSearch}
onSelectResult={handleSelectResult}
paddingTop={args.paddingTop}
placeholder={args.placeholder}
position={args.position}
recentSearches={recentSearches}
Expand Down
67 changes: 45 additions & 22 deletions packages/components/src/components/search/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {
Transition,
TransitionChild,
} from "@headlessui/react";
import { Loader2Icon, SearchIcon, SearchXIcon } from "lucide-react";
import {
ChevronRightIcon,
Loader2Icon,
SearchIcon,
SearchXIcon,
} from "lucide-react";
import {
Fragment,
forwardRef,
Expand All @@ -27,6 +32,7 @@ type SearchResult = {
header: string;
content: string;
link: string;
icon?: ReactNode;
metadata?: {
breadcrumbs?: string[];
[key: string]: unknown;
Expand All @@ -46,46 +52,59 @@ type SearchProps = {
emptyState?: ReactNode;
loadingState?: ReactNode;
position?: "top" | "center";
paddingTop?: string;
};

type SearchHitProps = {
isActive: boolean;
header: string;
description: string;
icon?: ReactNode;
metadata?: SearchResult["metadata"];
};

const SearchHit = ({
isActive,
header,
description,
icon,
metadata,
}: SearchHitProps) => {
return (
<div
className={cn(
"flex w-full cursor-pointer flex-col gap-1 rounded-md px-2.5 py-2 transition-colors",
"flex w-full cursor-pointer items-start gap-2 rounded-xl border border-transparent bg-transparent px-3 py-2 text-stone-500 transition-colors focus:ring-0 focus:ring-offset-0",
isActive && "bg-stone-100 dark:bg-white/5"
)}
>
{metadata?.breadcrumbs && metadata.breadcrumbs.length > 0 && (
<div className="flex items-center gap-1 truncate text-stone-500 text-xs dark:text-stone-400">
{metadata.breadcrumbs.map((crumb, idx) => (
// biome-ignore lint/suspicious/noArrayIndexKey: Breadcrumbs are positional and may contain duplicates
<Fragment key={idx}>
{idx > 0 && <span className="text-stone-400"> &gt; </span>}
<span className="truncate">{crumb}</span>
</Fragment>
))}
{icon && (
<div className="shrink-0 self-center text-stone-700 dark:text-stone-300">
{icon}
</div>
)}
<div className="truncate font-medium text-sm text-stone-900 dark:text-white">
{header}
</div>
{description && (
<div className="line-clamp-2 text-sm text-stone-600 dark:text-stone-400">
{description}
<div className="flex flex-1 flex-col gap-1">
{metadata?.breadcrumbs && metadata.breadcrumbs.length > 0 && (
<div className="flex items-center gap-1 truncate text-stone-500 text-xs dark:text-stone-400">
{metadata.breadcrumbs.map((crumb, idx) => (
// biome-ignore lint/suspicious/noArrayIndexKey: Breadcrumbs are positional and may contain duplicates
<Fragment key={idx}>
{idx > 0 && <span className="text-stone-400"> &gt; </span>}
<span className="truncate">{crumb}</span>
</Fragment>
))}
</div>
)}
<div className="truncate font-medium text-sm text-stone-900 dark:text-white">
{header}
</div>
{description && (
<div className="line-clamp-2 text-sm text-stone-600 dark:text-stone-400">
{description}
</div>
)}
</div>
{isActive && (
<ChevronRightIcon className="size-5 shrink-0 self-center text-stone-400 dark:text-stone-500" />
)}
</div>
);
Expand All @@ -106,6 +125,7 @@ const Search = forwardRef<HTMLInputElement, SearchProps>(
emptyState,
loadingState,
position = "top",
paddingTop = "64px",
},
ref
) => {
Expand Down Expand Up @@ -219,7 +239,7 @@ const Search = forwardRef<HTMLInputElement, SearchProps>(
autoComplete="off"
autoFocus
className={cn(
"peer h-full w-full rounded-md bg-white pr-14 pl-11 text-stone-950 tracking-tight shadow-sm outline-none ring ring-black/5 transition placeholder:text-stone-400 focus:ring-black/90 dark:bg-stone-900 dark:text-white dark:focus:ring-white placeholder:dark:text-white/50",
"peer h-full w-full rounded-xl bg-white pr-14 pl-11 text-stone-950 tracking-tight shadow-sm outline-none ring ring-black/5 transition placeholder:text-stone-400 focus:ring-black/90 dark:bg-stone-900 dark:text-white dark:focus:ring-white placeholder:dark:text-white/50",
"[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none",
!isContentScrolled && query && "shadow-lg"
)}
Expand Down Expand Up @@ -255,14 +275,15 @@ const Search = forwardRef<HTMLInputElement, SearchProps>(
</div>
{recentSearches.map((result) => (
<ComboboxOption
className="last:mb-1.5"
className="last:mb-2"
key={result.id}
value={result}
>
{({ focus }) => (
<SearchHit
description={result.content}
header={result.header}
icon={result.icon}
isActive={focus}
metadata={result.metadata}
/>
Expand All @@ -275,14 +296,15 @@ const Search = forwardRef<HTMLInputElement, SearchProps>(
{showResults &&
results.map((result) => (
<ComboboxOption
className="last:mb-1.5"
className="last:mb-2"
key={result.id}
value={result}
>
{({ focus }) => (
<SearchHit
description={result.content}
header={result.header}
icon={result.icon}
isActive={focus}
metadata={result.metadata}
/>
Expand Down Expand Up @@ -351,8 +373,9 @@ const Search = forwardRef<HTMLInputElement, SearchProps>(
<div
className={cn(
"fixed inset-0 z-10 flex justify-center p-4",
position === "top" ? "items-start pt-16" : "items-center"
position === "top" ? "items-start" : "items-center"
)}
style={position === "top" ? { paddingTop } : undefined}
>
<TransitionChild
as={Fragment}
Expand All @@ -363,7 +386,7 @@ const Search = forwardRef<HTMLInputElement, SearchProps>(
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<DialogPanel className="flex w-full max-w-[640px] flex-col overflow-hidden rounded-xl border border-stone-200 bg-white shadow-2xl dark:border-white/10 dark:bg-stone-900">
<DialogPanel className="flex w-full max-w-[640px] flex-col overflow-hidden rounded-2xl border border-stone-200 bg-white shadow-2xl dark:border-white/10 dark:bg-stone-900">
{searchContent}
</DialogPanel>
</TransitionChild>
Expand Down