Add tailwind-scrollbar-hide and implement block menu UI

The commit adds a new block menu UI component with sidebar navigation,
integration chips, and scrollable content areas. It includes tailwind-
scrollbar-hide for better UI experience and custom CSS for scroll
containers. The implementation features different content sections
for blocks categorized by type (input, action, output) and supports
search functionality.
This commit is contained in:
Abhimanyu Yadav
2025-05-17 21:18:08 +05:30
parent 1d8c7c5e1a
commit 451284de76
28 changed files with 701 additions and 115 deletions

View File

@@ -82,6 +82,7 @@
"react-shepherd": "^6.1.8",
"recharts": "^2.15.3",
"tailwind-merge": "^2.6.0",
"tailwind-scrollbar-hide": "^2.0.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"zod": "^3.24.4"

View File

@@ -154,3 +154,11 @@ input[type="number"]::-webkit-inner-spin-button {
input[type="number"] {
-moz-appearance: textfield;
}
.scroll-container {
max-height: 200px;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none; /* IE and Edge: hide scrollbar */
transition: scrollbar-width 0.3s ease;
}

View File

@@ -51,6 +51,7 @@ import PrimaryActionBar from "@/components/PrimaryActionButton";
import { useToast } from "@/components/ui/use-toast";
import { useCopyPaste } from "../hooks/useCopyPaste";
import { CronScheduler } from "./cronScheduler";
import { BlockMenu } from "./builder/block-menu/BlockMenu";
// This is for the history, this is the minimum distance a block must move before it is logged
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
@@ -136,6 +137,10 @@ const FlowEditor: React.FC<{
// State to control if save popover should be pinned open
const [pinSavePopover, setPinSavePopover] = useState(false);
const [blockMenuSelected, setBlockMenuSelected] = useState<
"save" | "block" | ""
>("");
const runnerUIRef = useRef<RunnerUIWrapperRef>(null);
const [openCron, setOpenCron] = useState(false);
@@ -623,12 +628,12 @@ const FlowEditor: React.FC<{
const editorControls: Control[] = [
{
label: "Undo",
icon: <IconUndo2 />,
icon: <IconUndo2 className="h-5 w-5" strokeWidth={2} />,
onClick: handleUndo,
},
{
label: "Redo",
icon: <IconRedo2 />,
icon: <IconRedo2 className="h-5 w-5" strokeWidth={2} />,
onClick: handleRedo,
},
];
@@ -676,15 +681,13 @@ const FlowEditor: React.FC<{
<Controls />
<Background className="dark:bg-slate-800" />
<ControlPanel
className="absolute z-10"
controls={editorControls}
topChildren={
<BlocksControl
pinBlocksPopover={pinBlocksPopover} // Pass the state to BlocksControl
blocks={availableNodes}
<BlockMenu
pinBlocksPopover={pinBlocksPopover}
addBlock={addNode}
flows={availableFlows}
nodes={nodes}
blockMenuSelected={blockMenuSelected}
setBlockMenuSelected={setBlockMenuSelected}
/>
}
botChildren={
@@ -697,6 +700,8 @@ const FlowEditor: React.FC<{
agentName={agentName}
onNameChange={setAgentName}
pinSavePopover={pinSavePopover}
blockMenuSelected={blockMenuSelected}
setBlockMenuSelected={setBlockMenuSelected}
/>
}
></ControlPanel>

View File

@@ -0,0 +1,68 @@
// BLOCK MENU TODO: Currently when i click on the control panel button, if it is already open, then it needs to close, currently its not happening
import React, { useState } from "react";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import ControlPanelButton from "@/components/builder/block-menu/ControlPanelButton";
import { ToyBrick } from "lucide-react";
import BlockMenuContent from "./BlockMenuContent";
interface BlockMenuProps {
addBlock: (
id: string,
name: string,
hardcodedValues: Record<string, any>,
) => void;
pinBlocksPopover: boolean;
blockMenuSelected: "save" | "block" | "";
setBlockMenuSelected: React.Dispatch<
React.SetStateAction<"" | "save" | "block">
>;
}
export const BlockMenu: React.FC<BlockMenuProps> = ({
addBlock,
pinBlocksPopover,
blockMenuSelected,
setBlockMenuSelected,
}) => {
const [open, setOpen] = useState(false);
const handlingOnOpen = (newOpen: boolean) => {
if (!pinBlocksPopover) {
setOpen(newOpen);
setBlockMenuSelected(newOpen ? "block" : "");
}
};
return (
<Popover
open={pinBlocksPopover ? true : open}
onOpenChange={handlingOnOpen}
>
<PopoverTrigger className="hover:cursor-pointer">
<ControlPanelButton
data-id="blocks-control-popover-trigger"
data-testid="blocks-control-blocks-button"
selected={blockMenuSelected === "block"}
className="rounded-none"
>
<ToyBrick className="h-5 w-6" strokeWidth={2} />
</ControlPanelButton>
</PopoverTrigger>
<PopoverContent
side="right"
align="start"
sideOffset={16}
className="absolute h-[75vh] w-[46.625rem] overflow-hidden rounded-[1rem] border-none p-0 shadow-[0_2px_6px_0_rgba(0,0,0,0.05)]"
data-id="blocks-control-popover-content"
>
<BlockMenuContent />
</PopoverContent>
</Popover>
);
};

View File

@@ -0,0 +1,27 @@
"use client";
import React, { useState } from "react";
import BlockMenuSearchBar from "./BlockMenuSearchBar";
import BlockMenuSearch from "./search-and-filter//BlockMenuSearch";
import BlockMenuDefault from "./default/BlockMenuDefault";
import { Separator } from "@/components/ui/separator";
const BlockMenuContent: React.FC = () => {
const [searchQuery, setSearchQuery] = useState("");
return (
<div className="flex h-full w-full flex-col">
{/* Search Bar */}
<BlockMenuSearchBar
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
<Separator className="h-[1px] w-full text-zinc-300" />
{/* Content */}
{/* BLOCK MENU TODO : search after 3 characters */}
{searchQuery ? <BlockMenuSearch /> : <BlockMenuDefault />}
</div>
);
};
export default BlockMenuContent;

View File

@@ -0,0 +1,37 @@
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { Search } from "lucide-react";
import React, { useRef } from "react";
interface BlockMenuSearchBarProps {
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
searchQuery: string;
className?: string;
}
const BlockMenuSearchBar: React.FC<BlockMenuSearchBarProps> = ({
searchQuery,
setSearchQuery,
className = "",
}) => {
const inputRef = useRef(null);
return (
<div className="flex min-h-[3.5625rem] items-center gap-2.5 px-4">
<Search className="h-6 w-6 text-zinc-700" strokeWidth={2} />
<Input
ref={inputRef}
type="text"
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
}}
placeholder={"Blocks, Agents, Integrations or Keywords..."}
className={cn(
"m-0 border-none p-0 font-sans text-base font-normal text-zinc-800 shadow-none outline-none placeholder:text-zinc-400 focus:shadow-none focus:outline-none focus:ring-0",
)}
/>
</div>
);
};
export default BlockMenuSearchBar;

View File

@@ -1,32 +0,0 @@
import { cn } from "@/lib/utils";
import React, { ButtonHTMLAttributes } from "react";
import { LucideIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
selected?: boolean;
icon?: LucideIcon;
}
const ControlPanel: React.FC<Props> = ({
selected = false,
icon: Icon,
className,
...rest
}) => {
return (
<Button
className={cn(
"flex h-[4.25rem] w-[4.25rem] items-center justify-center whitespace-normal bg-white p-[1.38rem] text-zinc-800 shadow-none hover:cursor-pointer hover:bg-zinc-100 hover:text-zinc-950 focus:ring-0",
selected &&
"bg-violet-50 text-violet-700 hover:cursor-default hover:bg-violet-50 hover:text-violet-700",
className,
)}
{...rest}
>
{Icon && <Icon className="h-6 w-6" strokeWidth={2} />}
</Button>
);
};
export default ControlPanel;

View File

@@ -0,0 +1,35 @@
// BLOCK MENU TODO: We need a disable state in this, currently it's not in design.
// Using div instead of button, because it's only for design purposes. We will use this to give design to PopoverTrigger.
import { cn } from "@/lib/utils";
import React from "react";
interface Props extends React.HTMLAttributes<HTMLDivElement> {
selected?: boolean;
children?: React.ReactNode; // For icon purpose
disabled?: boolean;
}
const ControlPanelButton: React.FC<Props> = ({
selected = false,
children,
disabled,
className,
...rest
}) => {
return (
<div
className={cn(
"flex h-[4.25rem] w-[4.25rem] items-center justify-center whitespace-normal bg-white p-[1.38rem] text-zinc-800 shadow-none hover:cursor-pointer hover:bg-zinc-100 hover:text-zinc-950 focus:ring-0",
selected &&
"bg-violet-50 text-violet-700 hover:cursor-default hover:bg-violet-50 hover:text-violet-700 active:bg-violet-50 active:text-violet-700",
disabled && className,
)}
{...rest}
>
{children}
</div>
);
};
export default ControlPanelButton;

View File

@@ -22,15 +22,16 @@ const IntegrationChip: React.FC<Props> = ({
)}
{...rest}
>
{icon_url && (
<Image
src={icon_url}
alt="integration-icon"
className="h-9 w-9"
width={36}
height={36}
/>
)}
<div className="relative h-9 w-9 rounded-[0.5rem] bg-transparent">
{icon_url && (
<Image
src={icon_url}
alt="integration-icon"
fill
className="w-full object-contain"
/>
)}
</div>
<span className="font-sans text-sm font-normal leading-[1.375rem] text-zinc-800">
{name}
</span>

View File

@@ -20,7 +20,7 @@ const MenuItem: React.FC<Props> = ({
return (
<Button
className={cn(
"flex h-[2.375rem] w-full justify-between whitespace-normal rounded-[0.5rem] bg-transparent p-2 pl-3 shadow-none hover:bg-transparent focus:ring-0",
"flex h-[2.375rem] w-full min-w-52 justify-between whitespace-normal rounded-[0.5rem] bg-transparent p-2 pl-3 shadow-none hover:cursor-pointer hover:bg-transparent focus:ring-0",
selected && "bg-zinc-100 hover:bg-zinc-100",
className,
)}
@@ -31,7 +31,7 @@ const MenuItem: React.FC<Props> = ({
</span>
{number && (
<span className="font-sans text-sm font-normal leading-[1.375rem] text-zinc-600">
{number}+
{number > 100 ? "100+" : number}
</span>
)}
</Button>

View File

@@ -15,7 +15,7 @@ const SearchHistoryChip: React.FC<Props> = ({
return (
<Button
className={cn(
"h-[2.375rem] space-x-1 whitespace-normal rounded-[1.5rem] bg-zinc-50 py-[0.44rem] pl-[0.38rem] pr-[0.62rem] shadow-none hover:bg-zinc-100 focus:ring-0 active:border active:border-zinc-300 active:bg-zinc-100",
"h-[2.25rem] space-x-1 rounded-[1.5rem] bg-zinc-50 p-[0.375rem] pr-[0.625rem] shadow-none hover:bg-zinc-100 focus:ring-0 active:border active:border-zinc-300 active:bg-zinc-100",
className,
)}
{...rest}

View File

@@ -0,0 +1,7 @@
import React from "react";
const ActionBlocksContent: React.FC = () => {
return <div className="h-full w-full">ActionBlocksContent</div>;
};
export default ActionBlocksContent;

View File

@@ -0,0 +1,122 @@
// BLOCK MENU TODO: Currently I have hide the scrollbar, but need to add better designed custom scroller
import React from "react";
import Block from "../Block";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
const AllBlocksContent: React.FC = () => {
return (
<div className="h-full w-full space-y-3 px-4">
{/* AI Category */}
<div className="space-y-2.5">
<div className="flex items-center justify-between">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
AI
</p>
<span className="rounded-full bg-zinc-100 px-[0.375rem] font-sans text-sm leading-[1.375rem] text-zinc-600">
10
</span>
</div>
<div className="space-y-2">
<Block
title="Add to list"
description="Enables your agent to chat with users in natural language."
/>
<Block
title="Add to list"
description="Enables your agent to chat with users in natural language."
/>
<Block
title="Add to list"
description="Enables your agent to chat with users in natural language."
/>
<Button
variant={"link"}
className="px-0 font-sans text-sm leading-[1.375rem] text-zinc-600 underline hover:text-zinc-800"
>
see all
</Button>
</div>
</div>
<Separator className="h-[1px] w-full text-zinc-300" />
{/* Basic Category */}
<div className="space-y-2.5">
<div className="flex items-center justify-between">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Basic
</p>
<span className="rounded-full bg-zinc-100 px-[0.375rem] font-sans text-sm leading-[1.375rem] text-zinc-600">
6
</span>
</div>
<div className="space-y-2">
<Block
title="Add to list"
description="Enables your agent to chat with users in natural language."
/>
<Block
title="Add to list"
description="Enables your agent to chat with users in natural language."
/>
<Block
title="Add to list"
description="Enables your agent to chat with users in natural language."
/>
<Button
variant={"link"}
className="px-0 font-sans text-sm leading-[1.375rem] text-zinc-600 underline hover:text-zinc-800"
>
see all
</Button>
</div>
</div>
<Separator className="h-[1px] w-full text-zinc-300" />
{/* Communincation Category */}
<div className="space-y-2.5">
<div className="flex items-center justify-between">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Communincation
</p>
<span className="rounded-full bg-zinc-100 px-[0.375rem] font-sans text-sm leading-[1.375rem] text-zinc-600">
6
</span>
</div>
<div className="space-y-2">
<Block
title="Add to list"
description="Enables your agent to chat with users in natural language."
/>
<Block
title="Add to list"
description="Enables your agent to chat with users in natural language."
/>
<Block
title="Add to list"
description="Enables your agent to chat with users in natural language."
/>
<Button
variant={"link"}
className="px-0 font-sans text-sm leading-[1.375rem] text-zinc-600 underline hover:text-zinc-800"
>
see all
</Button>
</div>
</div>
<Separator className="h-[1px] w-full text-zinc-300" />
</div>
);
};
export default AllBlocksContent;

View File

@@ -0,0 +1,36 @@
// BLOCK MENU TODO: Fix scrollbar in all states
import React, { useState } from "react";
import BlockMenuSidebar from "./BlockMenuSidebar";
import { Separator } from "@/components/ui/separator";
import BlockMenuDefaultContent from "./BlockMenuDefaultContent";
export type DefaultStateType =
| "suggestion"
| "all_blocks"
| "input_blocks"
| "action_blocks"
| "output_blocks"
| "integrations"
| "marketplace_agents"
| "my_agents";
const BlockMenuDefault: React.FC = () => {
const [defaultState, setDefaultState] =
useState<DefaultStateType>("suggestion");
return (
<div className="flex flex-1 overflow-y-auto">
{/* Left sidebar */}
<BlockMenuSidebar
defaultState={defaultState}
setDefaultState={setDefaultState}
/>
<Separator className="h-full w-[1px] text-zinc-300" />
<BlockMenuDefaultContent defaultState={defaultState} />
</div>
);
};
export default BlockMenuDefault;

View File

@@ -0,0 +1,33 @@
import React from "react";
import { DefaultStateType } from "./BlockMenuDefault";
import SuggestionContent from "./SuggestionContent";
import AllBlocksContent from "./AllBlocksContent";
import InputBlocksContent from "./InputBlocksContent";
import ActionBlocksContent from "./ActionBlocksContent";
import OutputBlocksContent from "./OutputBlocksContent";
import IntegrationsContent from "./IntegrationsContent";
import MarketplaceAgentsContent from "./MarketplaceAgentsContent";
import MyAgentsContent from "./MyAgentsContent";
interface BlockMenuDefaultContentProps {
defaultState: DefaultStateType;
}
const BlockMenuDefaultContent: React.FC<BlockMenuDefaultContentProps> = ({
defaultState,
}) => {
return (
<div className="scrollbar-hide h-full flex-1 overflow-y-auto pt-4">
{defaultState == "suggestion" && <SuggestionContent />}
{defaultState == "all_blocks" && <AllBlocksContent />}
{defaultState == "input_blocks" && <InputBlocksContent />}
{defaultState == "action_blocks" && <ActionBlocksContent />}
{defaultState == "output_blocks" && <OutputBlocksContent />}
{defaultState == "integrations" && <IntegrationsContent />}
{defaultState == "marketplace_agents" && <MarketplaceAgentsContent />}
{defaultState == "my_agents" && <MyAgentsContent />}
</div>
);
};
export default BlockMenuDefaultContent;

View File

@@ -0,0 +1,73 @@
import React from "react";
import MenuItem from "../MenuItem";
import { DefaultStateType } from "./BlockMenuDefault";
interface BlockMenuSidebarProps {
defaultState: DefaultStateType;
setDefaultState: React.Dispatch<React.SetStateAction<DefaultStateType>>;
}
const BlockMenuSidebar: React.FC<BlockMenuSidebarProps> = ({
defaultState,
setDefaultState,
}) => {
// BLOCK MENU TODO: We need to fetch the number of Blocks/Integrations/Agents when opening the menu.
// Alternatively, this might depend on the strategy we plan in the future.
// We'll add a loading state based on the future plan.
return (
<div className="space-y-2 p-4">
<MenuItem
name={"Suggestion"}
selected={defaultState == "suggestion"}
onClick={() => setDefaultState("suggestion")}
/>
<MenuItem
name={"All blocks"}
number={103}
selected={defaultState == "all_blocks"}
onClick={() => setDefaultState("all_blocks")}
/>
<div className="ml-[0.5365rem] border-l border-black/10 pl-[0.75rem]">
<MenuItem
name={"Input blocks"}
number={12}
selected={defaultState == "input_blocks"}
onClick={() => setDefaultState("input_blocks")}
/>
<MenuItem
name={"Action blocks"}
number={40}
selected={defaultState == "action_blocks"}
onClick={() => setDefaultState("action_blocks")}
/>
<MenuItem
name={"Output blocks"}
number={6}
selected={defaultState == "output_blocks"}
onClick={() => setDefaultState("output_blocks")}
/>
</div>
<MenuItem
name={"Integrations"}
number={24}
selected={defaultState == "integrations"}
onClick={() => setDefaultState("integrations")}
/>
<MenuItem
name={"Marketplace Agents"}
number={103}
selected={defaultState == "marketplace_agents"}
onClick={() => setDefaultState("marketplace_agents")}
/>
<MenuItem
name={"My Agents"}
number={6}
selected={defaultState == "my_agents"}
onClick={() => setDefaultState("my_agents")}
/>
</div>
);
};
export default BlockMenuSidebar;

View File

@@ -0,0 +1,41 @@
import React from "react";
import Block from "../Block";
const InputBlocksContent: React.FC = () => {
return (
<div className="h-full w-full space-y-3 px-4">
<Block title="Date Input" description="Input a date into your agent." />
<Block
title="Dropdown input"
description="Give your users the ability to select from a dropdown menu"
/>
<Block title="File upload" description="Upload a file to your agent" />
<Block
title="Text input"
description="Allow users to select multiple options using checkboxes"
/>
<Block
title="Add to list"
description="Enables your agent to chat with users in natural language."
/>
<Block
title="Add to list"
description="Enables your agent to chat with users in natural language."
/>
<Block
title="Add to list"
description="Enables your agent to chat with users in natural language."
/>
<Block
title="Add to list"
description="Enables your agent to chat with users in natural language."
/>
<Block
title="Add to list"
description="Enables your agent to chat with users in natural language."
/>
</div>
);
};
export default InputBlocksContent;

View File

@@ -0,0 +1,7 @@
import React from "react";
const IntegrationsContent: React.FC = () => {
return <div className="h-full w-full">IntegerationsContent</div>;
};
export default IntegrationsContent;

View File

@@ -0,0 +1,7 @@
import React from "react";
const MarketplaceAgentsContent: React.FC = () => {
return <div className="h-full w-full">MarketplaceAgentsContent</div>;
};
export default MarketplaceAgentsContent;

View File

@@ -0,0 +1,7 @@
import React from "react";
const MyAgentsContent: React.FC = () => {
return <div className="h-full w-full">MyAgentsContent</div>;
};
export default MyAgentsContent;

View File

@@ -0,0 +1,7 @@
import React from "react";
const OutputBlocksContent: React.FC = () => {
return <div className="h-full w-full">OutputBlocksContent</div>;
};
export default OutputBlocksContent;

View File

@@ -0,0 +1,83 @@
import React from "react";
import SearchHistoryChip from "../SearchHistoryChip";
import IntegrationChip from "../IntegrationChip";
import Block from "../Block";
const SuggestionContent: React.FC = () => {
return (
<div className="h-full w-full space-y-6">
{/* Recent Searches */}
<div className="space-y-2.5">
<p className="px-4 font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Recent searches
</p>
<div className="scrollbar-hide flex flex-nowrap gap-2 overflow-x-auto">
<SearchHistoryChip content="image generator" className="ml-4" />
<SearchHistoryChip content="deepfake" />
<SearchHistoryChip content="competitor analysis" />
<SearchHistoryChip content="image generator" />
<SearchHistoryChip content="deepfake" />
<SearchHistoryChip content="competitor analysis" />
<SearchHistoryChip content="image generator" />
<SearchHistoryChip content="deepfake" />
<SearchHistoryChip content="competitor analysis" />
</div>
</div>
{/* Integrations */}
<div className="space-y-2.5 px-4">
<p className="font-sans text-xs font-medium leading-[1.25rem] text-zinc-500">
Integrations
</p>
<div className="grid grid-cols-3 grid-rows-2 gap-2">
<IntegrationChip icon_url="/integrations/x.png" name="Twitter" />
<IntegrationChip icon_url="/integrations/github.png" name="Github" />
<IntegrationChip
icon_url="/integrations/hubspot.png"
name="Hubspot"
/>
<IntegrationChip
icon_url="/integrations/discord.png"
name="Discord"
/>
<IntegrationChip icon_url="/integrations/medium.png" name="Medium" />
<IntegrationChip
icon_url="/integrations/todoist.png"
name="Todoist"
/>
</div>
</div>
{/* Top blocks */}
<div className="space-y-2.5 px-4 pb-4">
<p className="font-sans text-sm font-medium leading-[1.375rem] text-zinc-800">
Top blocks
</p>
<div className="space-y-2">
<Block
title="Find in Dictionary"
description="Enables your agent to chat with users in natural language."
/>
<Block
title="Find in Dictionary"
description="Enables your agent to chat with users in natural language."
/>
<Block
title="Find in Dictionary"
description="Enables your agent to chat with users in natural language."
/>
<Block
title="Find in Dictionary"
description="Enables your agent to chat with users in natural language."
/>
<Block
title="Find in Dictionary"
description="Enables your agent to chat with users in natural language."
/>
</div>
</div>
</div>
);
};
export default SuggestionContent;

View File

@@ -0,0 +1,11 @@
import React from "react";
const BlockMenuSearch: React.FC = () => {
return (
<div className="p-4">
<h2>Filter Block Menu</h2>
</div>
);
};
export default BlockMenuSearch;

View File

@@ -0,0 +1,3 @@
// Default state data
// All blocks

View File

@@ -1,13 +1,7 @@
import { Card, CardContent } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import React from "react";
import ControlPanelButton from "@/components/builder/block-menu/ControlPanelButton";
/**
* Represents a control element for the ControlPanel Component.
@@ -27,6 +21,7 @@ interface ControlPanelProps {
controls: Control[];
topChildren?: React.ReactNode;
botChildren?: React.ReactNode;
className?: string;
}
@@ -45,42 +40,31 @@ export const ControlPanel = ({
className,
}: ControlPanelProps) => {
return (
<Card className={cn("m-4 mt-24 w-14 dark:bg-slate-900", className)}>
<CardContent className="p-0">
<div className="flex flex-col items-center gap-3 rounded-xl py-3">
{topChildren}
<Separator className="dark:bg-slate-700" />
{controls.map((control, index) => (
<Tooltip key={index} delayDuration={500}>
<TooltipTrigger asChild>
<div>
<Button
variant="ghost"
size="icon"
onClick={() => control.onClick()}
data-id={`control-button-${index}`}
data-testid={`blocks-control-${control.label.toLowerCase()}-button`}
disabled={control.disabled || false}
className="dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
>
{control.icon}
<span className="sr-only">{control.label}</span>
</Button>
</div>
</TooltipTrigger>
<TooltipContent
side="right"
className="dark:bg-slate-800 dark:text-slate-100"
>
{control.label}
</TooltipContent>
</Tooltip>
))}
<Separator className="dark:bg-slate-700" />
{botChildren}
</div>
</CardContent>
</Card>
<section
className={cn(
"absolute left-4 top-24 z-10 w-[4.25rem] overflow-hidden rounded-[1rem] border-none bg-white p-0 shadow-[0_1px_5px_0_rgba(0,0,0,0.1)]",
className,
)}
>
<div className="flex flex-col items-center justify-center rounded-[1rem] p-0">
{topChildren}
<Separator className="text-[#E1E1E1]" />
{controls.map((control, index) => (
<ControlPanelButton
key={index}
onClick={() => control.onClick()}
data-id={`control-button-${index}`}
data-testid={`blocks-control-${control.label.toLowerCase()}-button`}
disabled={control.disabled || false}
className="rounded-none"
>
{control.icon}
</ControlPanelButton>
))}
<Separator className="text-[#E1E1E1]" />
{botChildren}
</div>
</section>
);
};
export default ControlPanel;

View File

@@ -16,6 +16,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useToast } from "@/components/ui/use-toast";
import ControlPanelButton from "@/components/builder/block-menu/ControlPanelButton";
interface SaveControlProps {
agentMeta: GraphMeta | null;
@@ -26,6 +27,11 @@ interface SaveControlProps {
onNameChange: (name: string) => void;
onDescriptionChange: (description: string) => void;
pinSavePopover: boolean;
blockMenuSelected: "save" | "block" | "";
setBlockMenuSelected: React.Dispatch<
React.SetStateAction<"" | "save" | "block">
>;
}
/**
@@ -48,6 +54,8 @@ export const SaveControl = ({
onNameChange,
agentDescription,
onDescriptionChange,
blockMenuSelected,
setBlockMenuSelected,
pinSavePopover,
}: SaveControlProps) => {
/**
@@ -82,27 +90,29 @@ export const SaveControl = ({
}, [handleSave, toast]);
return (
<Popover open={pinSavePopover ? true : undefined}>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
data-id="save-control-popover-trigger"
data-testid="blocks-control-save-button"
name="Save"
>
<IconSave className="dark:text-gray-300" />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="right">Save</TooltipContent>
</Tooltip>
<Popover
open={pinSavePopover ? true : undefined}
onOpenChange={(open) => open || setBlockMenuSelected("")}
>
<PopoverTrigger>
<ControlPanelButton
data-id="save-control-popover-trigger"
data-testid="blocks-control-save-button"
selected={blockMenuSelected === "save"}
onClick={() => {
setBlockMenuSelected("save");
}}
className="rounded-none"
>
<IconSave className="h-5 w-5" strokeWidth={2} />
</ControlPanelButton>
</PopoverTrigger>
<PopoverContent
side="right"
sideOffset={15}
sideOffset={16}
align="start"
className="w-[17rem] rounded-xl border-none p-0 shadow-none md:w-[30rem]"
data-id="save-control-popover-content"
>
<Card className="border-none shadow-none dark:bg-slate-900">

View File

@@ -1,4 +1,5 @@
import type { Config } from "tailwindcss";
import scrollbarHide from "tailwind-scrollbar-hide";
const config = {
darkMode: ["class"],
@@ -144,7 +145,7 @@ const config = {
},
},
},
plugins: [require("tailwindcss-animate")],
plugins: [require("tailwindcss-animate"), scrollbarHide],
} satisfies Config;
export default config;

View File

@@ -1222,7 +1222,6 @@
"@fastify/otel@https://codeload.github.com/getsentry/fastify-otel/tar.gz/ae3088d65e286bdc94ac5d722573537d6a6671bb":
version "0.8.0"
uid "1632d3df7ebf8cd86996a50e9e42721aea05b39c"
resolved "https://codeload.github.com/getsentry/fastify-otel/tar.gz/ae3088d65e286bdc94ac5d722573537d6a6671bb#1632d3df7ebf8cd86996a50e9e42721aea05b39c"
dependencies:
"@opentelemetry/core" "^1.30.1"
@@ -11368,6 +11367,11 @@ tailwind-merge@^2.6.0:
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz#ac5fb7e227910c038d458f396b7400d93a3142d5"
integrity sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==
tailwind-scrollbar-hide@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/tailwind-scrollbar-hide/-/tailwind-scrollbar-hide-2.0.0.tgz#2d25a3ba383cc7ec1bd516a4416759bb3428f88b"
integrity sha512-lqiIutHliEiODwBRHy4G2+Tcayo2U7+3+4frBmoMETD72qtah+XhOk5XcPzC1nJvXhXUdfl2ajlMhUc2qC6CIg==
tailwindcss-animate@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4"