From 1e3d50f8767c50ed2f653df154677c9ffbbb0f42 Mon Sep 17 00:00:00 2001
From: Shine1i
Date: Thu, 26 Feb 2026 12:55:09 +0100
Subject: [PATCH] feat(recipe-studio): add block sheet search + clearer icons
in sheet
---
.../recipe-studio/components/block-sheet.tsx | 194 ++++++++++++++----
1 file changed, 156 insertions(+), 38 deletions(-)
diff --git a/studio/frontend/src/features/recipe-studio/components/block-sheet.tsx b/studio/frontend/src/features/recipe-studio/components/block-sheet.tsx
index 281eccd810..976de9f9a4 100644
--- a/studio/frontend/src/features/recipe-studio/components/block-sheet.tsx
+++ b/studio/frontend/src/features/recipe-studio/components/block-sheet.tsx
@@ -1,4 +1,5 @@
import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
import {
Sheet,
SheetContent,
@@ -12,6 +13,7 @@ import {
CodeIcon,
Copy02Icon,
type Database02Icon,
+ DragDropVerticalIcon,
PlusSignIcon,
Tick02Icon,
Upload01Icon,
@@ -115,6 +117,15 @@ const ROOT_GROUPS: RootGroup[] = [
icon: CodeIcon,
},
];
+const SEARCHABLE_KINDS: SheetKind[] = [
+ "sampler",
+ "seed",
+ "llm",
+ "expression",
+ "note",
+];
+const PROCESSOR_TITLE = "Schema Transform";
+const PROCESSOR_DESCRIPTION = "Transform final dataset schema.";
function BlockSheetButton({
icon,
@@ -124,7 +135,7 @@ function BlockSheetButton({
isActive = false,
draggable = false,
onDragStart,
- showChevron = true,
+ trailing = "chevron",
}: {
icon: typeof Database02Icon;
title: string;
@@ -133,7 +144,7 @@ function BlockSheetButton({
isActive?: boolean;
draggable?: boolean;
onDragStart?: (event: ReactDragEvent) => void;
- showChevron?: boolean;
+ trailing?: "chevron" | "drag" | "none";
}): ReactElement {
return (
{description}
- {showChevron ? (
+ {trailing === "chevron" ? (
+ ) : trailing === "drag" ? (
+
) : null}
);
@@ -184,11 +200,17 @@ export function BlockSheet({
}: BlockSheetProps): ReactElement {
const sheetTitle = getSheetTitle(sheetView);
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
+ const [search, setSearch] = useState("");
const expressionBlocks = useMemo(() => getBlocksForKind("expression"), []);
const noteBlocks = useMemo(() => getBlocksForKind("note"), []);
const seedBlocks = useMemo(() => getBlocksForKind("seed"), []);
const isControlled = typeof open === "boolean";
const sheetOpen = isControlled ? (open as boolean) : uncontrolledOpen;
+ const normalizedSearch = search.trim().toLowerCase();
+ const hasSearch = normalizedSearch.length > 0;
+ const isProcessorView = sheetView === "processor";
+ const isRootView = sheetView === "root";
+ const isScopedBlockView = !isRootView && !isProcessorView;
const setSheetOpen = (nextOpen: boolean) => {
if (!isControlled) {
@@ -196,6 +218,56 @@ export function BlockSheet({
}
onOpenChange?.(nextOpen);
};
+ const matchesSearch = (title: string, description: string) =>
+ title.toLowerCase().includes(normalizedSearch) ||
+ description.toLowerCase().includes(normalizedSearch);
+
+ const searchableBlocks = useMemo(
+ () => SEARCHABLE_KINDS.flatMap((kind) => getBlocksForKind(kind)),
+ [],
+ );
+ const rootSearchBlocks = useMemo(() => {
+ if (!hasSearch) {
+ return [];
+ }
+ return searchableBlocks.filter((item) =>
+ matchesSearch(item.title, item.description),
+ );
+ }, [hasSearch, searchableBlocks, normalizedSearch]);
+
+ const scopedBlocks = useMemo(() => {
+ if (!isScopedBlockView) {
+ return [];
+ }
+ const blocks = getBlocksForKind(VIEW_KIND[sheetView] ?? "sampler");
+ if (!hasSearch) {
+ return blocks;
+ }
+ return blocks.filter((item) => matchesSearch(item.title, item.description));
+ }, [hasSearch, isScopedBlockView, normalizedSearch, sheetView]);
+
+ const rootGroups = useMemo(() => {
+ if (!hasSearch) {
+ return ROOT_GROUPS;
+ }
+ return ROOT_GROUPS.filter((group) => {
+ if (matchesSearch(group.title, group.description)) {
+ return true;
+ }
+ if (group.kind === "processor") {
+ return matchesSearch(PROCESSOR_TITLE, PROCESSOR_DESCRIPTION);
+ }
+ return getBlocksForKind(group.kind).some((item) =>
+ matchesSearch(item.title, item.description),
+ );
+ });
+ }, [hasSearch, normalizedSearch]);
+ const showNoMatches =
+ (isRootView && hasSearch && rootSearchBlocks.length === 0) ||
+ (isScopedBlockView && scopedBlocks.length === 0) ||
+ (isProcessorView &&
+ hasSearch &&
+ !matchesSearch(PROCESSOR_TITLE, PROCESSOR_DESCRIPTION));
const buildDragStart =
(kind: SheetKind, type: BlockType) =>
@@ -206,6 +278,36 @@ export function BlockSheet({
event.dataTransfer.setData("text/plain", serialized);
event.dataTransfer.effectAllowed = "copy";
};
+ const getTrailing = (kind: SheetKind): "drag" | "none" =>
+ kind === "sampler" || kind === "seed" || kind === "llm" ? "drag" : "none";
+ const onBlockClick = (kind: SheetKind, type: BlockType) => {
+ setSheetOpen(false);
+ if (kind === "sampler") {
+ onAddSampler(type as SamplerType);
+ return;
+ }
+ if (kind === "seed") {
+ onAddSeed(type as SeedBlockType);
+ return;
+ }
+ if (kind === "llm") {
+ if (type === "model_provider") {
+ onAddModelProvider();
+ return;
+ }
+ if (type === "model_config") {
+ onAddModelConfig();
+ return;
+ }
+ onAddLlm(type as LlmType);
+ return;
+ }
+ if (kind === "expression") {
+ onAddExpression();
+ return;
+ }
+ onAddMarkdownNote();
+ };
return (
@@ -215,6 +317,7 @@ export function BlockSheet({
setSheetOpen(nextOpen);
if (nextOpen) {
onViewChange("root");
+ setSearch("");
}
}}
>
@@ -238,7 +341,7 @@ export function BlockSheet({
className="absolute gap-0 p-0 shadow-none"
overlayClassName="bg-transparent pointer-events-none backdrop-blur-none supports-backdrop-filter:backdrop-blur-none"
>
-
+
{sheetView !== "root" && (
+ setSearch(event.target.value)}
+ placeholder="Search blocks..."
+ className="corner-squircle mt-3 h-9"
+ />
- {sheetView === "root" &&
- ROOT_GROUPS.map((item, index) => (
+ {isRootView &&
+ hasSearch &&
+ rootSearchBlocks.map((item, index) => (
+
onBlockClick(item.kind, item.type)}
+ />
+ ))}
+ {isRootView &&
+ !hasSearch &&
+ rootGroups.map((item, index) => (
{
if (item.kind === "processor") {
setSheetOpen(false);
@@ -288,18 +418,20 @@ export function BlockSheet({
}}
/>
))}
- {sheetView === "processor" && (
-
+ {isProcessorView && (
+ (!hasSearch ||
+ matchesSearch(PROCESSOR_TITLE, PROCESSOR_DESCRIPTION)) && (
+
+ )
)}
- {sheetView !== "root" &&
- sheetView !== "processor" &&
- getBlocksForKind(VIEW_KIND[sheetView] ?? "sampler").map(
+ {isScopedBlockView &&
+ scopedBlocks.map(
(item, index) => (
{
- setSheetOpen(false);
- if (item.kind === "sampler") {
- onAddSampler(item.type as SamplerType);
- } else if (item.kind === "seed") {
- onAddSeed(item.type as SeedBlockType);
- } else if (item.kind === "llm") {
- if (item.type === "model_provider") {
- onAddModelProvider();
- } else if (item.type === "model_config") {
- onAddModelConfig();
- } else {
- onAddLlm(item.type as LlmType);
- }
- } else if (item.kind === "expression") {
- onAddExpression();
- } else {
- onAddMarkdownNote();
- }
- }}
+ trailing={getTrailing(item.kind)}
+ onClick={() => onBlockClick(item.kind, item.type)}
/>
),
)}
+ {showNoMatches && (
+
+ No blocks match.
+
+ )}