Search
Filter and select content from a local collection.
Give your AI assistant this page as context.
Connect your library with MCPINTERACTIVE PREVIEW · SAMPLE DATA
Find your next step
Documents, projects and ideas. Right where you need them.
blocks/searchUsage
Get this block from a saved library export or the connected CLI. The imports below refer to local files installed in your app.
Import styles/components.css, then styles/blocks.css, then styles/theme.css once. Wrap your app in ThemeScope from components/ui/utils so typography, focus and overlays share the theme. The installation guide covers runtime dependencies and React/Next.js setup.
This import is the entry point, not a complete integration example. Supply the required props and connect callbacks to your application. The full ZIP includes examples/blocks-preview.tsx with the populated preview and local sample callbacks; adapt those examples to your product.
import { SearchBlock } from './components/blocks/data';
// See Source and props below for the complete typed interface.Design system
This block consumes your semantic colors, spacing, typography and shape tokens. Import the compiled styles and your theme once as described in Export.
Icons use Lucide React. Install lucide-react@1.47.0 alongside React and Base UI.
Dependencies: Badge, Button, Card, Data Table, Empty State, Field, Input, Select, Separator.
Behavior and accessibility
- Provide meaningful labels for interactive controls.
- Connect action callbacks to your application logic.
- Verify keyboard navigation and contrast in your application.
Status: experimental during early access. Sample previews do not send emails, process payments or upload files to a service.
Source and props
This is the exported source, using the same implementation as the editor. The complete source group is included below; related blocks share it to avoid duplicating behavior.
"use client";
import { useMemo, useState, type ComponentProps, type ReactNode } from "react";
import {
ArrowDownRight,
ArrowUpRight,
ChartNoAxesColumn,
ChevronRight,
FileText,
FolderPlus,
ListFilter,
Minus,
Pencil,
Plus,
Search,
SearchX,
X,
} from "lucide-react";
import {
Badge,
Button,
Card,
DataTable,
EmptyState,
Field,
Input,
Select,
Separator,
} from '../ui';
function DataHeading({
eyebrow,
title,
description,
action,
}: {
eyebrow: string;
title: string;
description?: string;
action?: ReactNode;
}) {
return (
<header className="cr-workspace-heading">
<div>
<span className="cr-workspace-eyebrow">{eyebrow}</span>
<h3>{title}</h3>
{description && <p>{description}</p>}
</div>
{action && <div className="cr-workspace-heading-action">{action}</div>}
</header>
);
}
export function DataTableBlock({
title,
description,
columns,
rows,
onCreate,
}: {
title: string;
description?: string;
columns: ComponentProps<typeof DataTable>["columns"];
rows: ComponentProps<typeof DataTable>["rows"];
onCreate: () => void;
}) {
return (
<Card className="cr-workspace-block cr-workspace-data-table">
<DataHeading
eyebrow="Workspace data"
title={title}
description={description}
action={
<Button onClick={onCreate}>
<Plus size={16} aria-hidden="true" />
Add record
</Button>
}
/>
<div className="cr-workspace-list-heading">
<span>All records</span>
<Badge variant="outline">
{rows.length} {rows.length === 1 ? "record" : "records"}
</Badge>
</div>
<DataTable caption={title} columns={columns} rows={rows} />
</Card>
);
}
export function FiltersBlock({
onChange,
statuses,
}: {
onChange: (filters: { query: string; status: string }) => void;
statuses: Array<{ value: string; label: string }>;
}) {
const [query, setQuery] = useState(""),
[status, setStatus] = useState("all");
const active = Number(Boolean(query)) + Number(status !== "all");
return (
<Card className="cr-workspace-block cr-workspace-filter-block">
<div className="cr-workspace-filter-heading">
<span>
<ListFilter size={17} aria-hidden="true" />
<strong>Filter records</strong>
</span>
{active > 0 && <Badge variant="outline">{active} active</Badge>}
</div>
<div className="cr-block-filters cr-workspace-filters">
<Field label="Search">
<div className="cr-workspace-search-field">
<Search size={17} aria-hidden="true" />
<Input
type="search"
value={query}
placeholder="Name or keyword…"
onChange={(event) => {
setQuery(event.target.value);
onChange({ query: event.target.value, status });
}}
/>
</div>
</Field>
<Field label="Status">
<Select
label="Status"
value={status}
options={[
{ value: "all", label: "All statuses" },
...statuses.filter((item) => item.value !== "all"),
]}
onValueChange={(value) => {
const next = value ?? "all";
setStatus(next);
onChange({ query, status: next });
}}
/>
</Field>
<Button
variant="ghost"
disabled={!active}
onClick={() => {
setQuery("");
setStatus("all");
onChange({ query: "", status: "all" });
}}
>
<X size={15} aria-hidden="true" />
Clear filters
</Button>
</div>
</Card>
);
}
export function SearchBlock({
items,
onSelect,
}: {
items: Array<{ id: string; title: string; description: string }>;
onSelect: (id: string) => void;
}) {
const [query, setQuery] = useState("");
const results = useMemo(
() =>
items.filter((item) =>
`${item.title} ${item.description}`
.toLowerCase()
.includes(query.toLowerCase()),
),
[items, query],
);
return (
<Card className="cr-workspace-block cr-workspace-search-block">
<DataHeading
eyebrow="Workspace search"
title="Find your next step"
description="Documents, projects and ideas. Right where you need them."
/>
<div className="cr-workspace-search-field">
<Search size={18} aria-hidden="true" />
<Input
aria-label="Search content"
type="search"
value={query}
placeholder="Search your workspace…"
onChange={(event) => setQuery(event.target.value)}
/>
</div>
<div className="cr-workspace-list-heading">
<span>{query ? "Matching results" : "All items"}</span>
<span className="cr-description" role="status">
{results.length} {results.length === 1 ? "result" : "results"}
</span>
</div>
<ul className="cr-workspace-list cr-workspace-search-results">
{results.map((item) => (
<li key={item.id}>
<button
type="button"
className="cr-notification-item cr-workspace-search-result"
onClick={() => onSelect(item.id)}
>
<span className="cr-workspace-icon-tile">
<FileText size={18} aria-hidden="true" />
</span>
<span>
<strong>{item.title}</strong>
<span className="cr-description">{item.description}</span>
</span>
<ChevronRight size={16} aria-hidden="true" />
</button>
</li>
))}
</ul>
{!results.length && (
<div className="cr-workspace-caught-up">
<span className="cr-workspace-icon-tile">
<SearchX size={22} aria-hidden="true" />
</span>
<strong>No results found</strong>
<p>Try a different name or keyword.</p>
</div>
)}
</Card>
);
}
export function EmptyStateBlock({
title,
description,
actionLabel,
onAction,
}: {
title: string;
description: string;
actionLabel: string;
onAction: () => void;
}) {
return (
<Card className="cr-workspace-block cr-workspace-empty-block">
<EmptyState
title={title}
description={description}
icon={
<span className="cr-workspace-empty-illustration">
<span />
<span />
<span>
<FolderPlus size={30} strokeWidth={1.5} aria-hidden="true" />
</span>
</span>
}
action={
<Button onClick={onAction}>
<Plus size={16} aria-hidden="true" />
{actionLabel}
</Button>
}
/>
</Card>
);
}
export function DetailViewBlock({
title,
status,
fields,
children,
onEdit,
}: {
title: string;
status: string;
fields: Array<{ label: string; value: string }>;
children?: ReactNode;
onEdit: () => void;
}) {
return (
<Card className="cr-workspace-block cr-workspace-detail">
<DataHeading
eyebrow="Record overview"
title={title}
action={
<Button variant="outline" onClick={onEdit}>
<Pencil size={15} aria-hidden="true" />
Edit details
</Button>
}
/>
<div className="cr-workspace-detail-status">
<span>Status</span>
<Badge variant="outline">
<span className="cr-workspace-status-dot" aria-hidden="true" />
{status}
</Badge>
</div>
<dl className="cr-details cr-workspace-details">
{fields.map((field) => (
<div key={field.label}>
<dt>{field.label}</dt>
<dd>{field.value}</dd>
</div>
))}
</dl>
{children && (
<div className="cr-workspace-detail-content">
<Separator />
{children}
</div>
)}
</Card>
);
}
export function StatsBlock({
items,
}: {
items: Array<{
label: string;
value: string;
change?: string;
trend?: "positive" | "negative" | "neutral";
description?: string;
icon?: ReactNode;
}>;
}) {
return (
<div className="cr-block-grid cr-workspace-stats">
{items.map((item) => (
<Card className="cr-workspace-stat-card" key={item.label}>
<div className="cr-workspace-stat-heading">
<span>{item.label}</span>
<span className="cr-workspace-stat-icon" aria-hidden="true">
{item.icon ?? <ChartNoAxesColumn size={17} />}
</span>
</div>
<strong className="cr-stat-value">{item.value}</strong>
{(item.change || item.description) && (
<div className="cr-workspace-stat-footer">
{item.change && (
<Badge
variant="outline"
className="cr-workspace-stat-change"
data-trend={item.trend ?? "neutral"}
>
{item.trend === "positive" ? (
<ArrowUpRight size={13} aria-hidden="true" />
) : item.trend === "negative" ? (
<ArrowDownRight size={13} aria-hidden="true" />
) : (
<Minus size={13} aria-hidden="true" />
)}
{item.change}
</Badge>
)}
{item.description && <span>{item.description}</span>}
</div>
)}
</Card>
))}
</div>
);
}