feat(library): enhance animations and UI for LibraryAgentList and LibraryFolder components

- Integrated framer-motion for improved animations in the `LibraryAgentList`, enhancing user experience during folder and agent transitions.
- Updated `LibraryFolder` component styles for better layout and spacing.
- Refactored `LibraryFolderEditDialog` to improve form handling and UI consistency.
- Adjusted `LibraryTabs` component for cleaner code structure and improved readability.

These changes significantly enhance the visual appeal and usability of the library management interface, making interactions smoother and more intuitive for users.
This commit is contained in:
abhi1992002
2026-02-13 20:09:14 +05:30
parent a6c2f645f1
commit 4f99f32fbf
4 changed files with 138 additions and 41 deletions

View File

@@ -11,11 +11,72 @@ import { Button } from "@/components/atoms/Button/Button";
import { ArrowLeftIcon, HeartIcon } from "@phosphor-icons/react";
import { Text } from "@/components/atoms/Text/Text";
import { Tab } from "../LibraryTabs/LibraryTabs";
import { LayoutGroup } from "framer-motion";
import {
AnimatePresence,
LayoutGroup,
motion,
useReducedMotion,
} from "framer-motion";
import { LibraryFolderEditDialog } from "../LibraryFolderEditDialog/LibraryFolderEditDialog";
import { LibraryFolderDeleteDialog } from "../LibraryFolderDeleteDialog/LibraryFolderDeleteDialog";
import { useLibraryAgentList } from "./useLibraryAgentList";
const containerVariants = {
hidden: {},
show: {
transition: {
staggerChildren: 0.04,
delayChildren: 0.04,
},
},
exit: {
opacity: 0,
filter: "blur(4px)",
transition: {
duration: 0.15,
ease: [0.25, 0.1, 0.25, 1],
},
},
};
const itemVariants = {
hidden: {
opacity: 0,
y: 8,
scale: 0.96,
filter: "blur(4px)",
},
show: {
opacity: 1,
y: 0,
scale: 1,
filter: "blur(0px)",
transition: {
duration: 0.25,
ease: [0.25, 0.1, 0.25, 1],
},
},
};
const reducedContainerVariants = {
hidden: {},
show: {
transition: { staggerChildren: 0.02 },
},
exit: {
opacity: 0,
transition: { duration: 0.15 },
},
};
const reducedItemVariants = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { duration: 0.2 },
},
};
interface Props {
searchTerm: string;
librarySort: LibraryAgentSort;
@@ -37,6 +98,14 @@ export function LibraryAgentList({
activeTab,
onTabChange,
}: Props) {
const shouldReduceMotion = useReducedMotion();
const activeContainerVariants = shouldReduceMotion
? reducedContainerVariants
: containerVariants;
const activeItemVariants = shouldReduceMotion
? reducedItemVariants
: itemVariants;
const {
isFavoritesTab,
agentLoading,
@@ -112,26 +181,38 @@ export function LibraryAgentList({
loader={<LoadingSpinner size="medium" />}
>
<LayoutGroup>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{showFolders &&
foldersData?.folders.map((folder) => (
<LibraryFolder
key={folder.id}
id={folder.id}
name={folder.name}
agentCount={folder.agent_count ?? 0}
color={folder.color ?? undefined}
icon={folder.icon ?? "📁"}
onAgentDrop={handleAgentDrop}
onClick={() => onFolderSelect(folder.id)}
onEdit={() => setEditingFolder(folder)}
onDelete={() => setDeletingFolder(folder)}
/>
<AnimatePresence mode="wait">
<motion.div
key={`${activeTab}-${selectedFolderId || "all"}`}
className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
variants={activeContainerVariants}
initial="hidden"
animate="show"
exit="exit"
>
{showFolders &&
foldersData?.folders.map((folder) => (
<motion.div key={folder.id} variants={activeItemVariants}>
<LibraryFolder
id={folder.id}
name={folder.name}
agentCount={folder.agent_count ?? 0}
color={folder.color ?? undefined}
icon={folder.icon ?? "📁"}
onAgentDrop={handleAgentDrop}
onClick={() => onFolderSelect(folder.id)}
onEdit={() => setEditingFolder(folder)}
onDelete={() => setDeletingFolder(folder)}
/>
</motion.div>
))}
{agents.map((agent) => (
<motion.div key={agent.id} variants={activeItemVariants}>
<LibraryAgentCard agent={agent} />
</motion.div>
))}
{agents.map((agent) => (
<LibraryAgentCard key={agent.id} agent={agent} />
))}
</div>
</motion.div>
</AnimatePresence>
</LayoutGroup>
</InfiniteScroll>
)}

View File

@@ -57,7 +57,7 @@ export function LibraryFolder({
<div
data-testid="library-folder"
data-folder-id={id}
className={`group relative inline-flex h-[10.625rem] w-full max-w-[25rem] cursor-pointer flex-col items-start justify-start gap-2.5 rounded-medium border bg-white p-4 transition-all duration-200 hover:shadow-md ${
className={`group relative inline-flex h-[10.625rem] w-full max-w-[25rem] cursor-pointer flex-col items-start justify-between gap-2.5 rounded-medium border bg-white p-4 transition-all duration-200 hover:shadow-md ${
isDragOver
? "border-blue-400 bg-blue-50 ring-2 ring-blue-200"
: "border-zinc-100"

View File

@@ -82,11 +82,10 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) {
queryKey: getGetV2ListLibraryFoldersQueryKey(),
});
const previousData = queryClient.getQueriesData<
getV2ListLibraryFoldersResponseSuccess
>({
queryKey: getGetV2ListLibraryFoldersQueryKey(),
});
const previousData =
queryClient.getQueriesData<getV2ListLibraryFoldersResponseSuccess>({
queryKey: getGetV2ListLibraryFoldersQueryKey(),
});
queryClient.setQueriesData<getV2ListLibraryFoldersResponseSuccess>(
{ queryKey: getGetV2ListLibraryFoldersQueryKey() },
@@ -123,8 +122,7 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) {
queryClient.setQueryData(queryKey, data);
}
}
const detail =
error.detail ?? error.response?.detail ?? "";
const detail = error.detail ?? error.response?.detail ?? "";
if (
typeof detail === "string" &&
detail.toLowerCase().includes("already exists")
@@ -191,7 +189,7 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) {
<Form
form={form}
onSubmit={(values) => onSubmit(values)}
className="flex flex-col justify-center gap-4 px-1"
className="flex flex-col justify-center gap-2 px-1"
>
<FormField
control={form.control}
@@ -204,7 +202,8 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) {
id={field.name}
label="Folder name"
placeholder="Enter folder name"
className="w-full rounded-[10px]"
className="w-full"
wrapperClassName="!mb-0"
/>
</FormControl>
<FormMessage />
@@ -224,6 +223,7 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) {
placeholder="Select a color"
value={field.value}
onValueChange={field.onChange}
wrapperClassName="!mb-0"
options={FOLDER_COLORS.map((color) => ({
value: color.value,
label: color.label,
@@ -273,10 +273,13 @@ export function LibraryFolderEditDialog({ folder, isOpen, setIsOpen }: Props) {
field.onChange(emoji);
}}
emojiSize={32}
className="w-full"
className="w-full rounded-2xl px-2"
>
<EmojiPicker.Group>
<EmojiPicker.List hideStickyHeader containerHeight={295} />
<EmojiPicker.Group className="pt-2">
<EmojiPicker.List
hideStickyHeader
containerHeight={295}
/>
</EmojiPicker.Group>
</EmojiPicker>
</div>

View File

@@ -19,11 +19,16 @@ interface Props {
layoutId?: string;
}
export function LibraryTabs({ tabs, activeTab, onTabChange, layoutId = "library-tabs" }: Props) {
export function LibraryTabs({
tabs,
activeTab,
onTabChange,
layoutId = "library-tabs",
}: Props) {
const { registerFavoritesTabRef } = useFavoriteAnimation();
return (
<div className="flex gap-2 items-center">
<div className="flex items-center gap-2">
{tabs.map((tab) => (
<TabButton
key={tab.id}
@@ -31,7 +36,9 @@ export function LibraryTabs({ tabs, activeTab, onTabChange, layoutId = "library-
isActive={activeTab === tab.id}
onSelect={onTabChange}
layoutId={layoutId}
onRefReady={tab.id === "favorites" ? registerFavoritesTabRef : undefined}
onRefReady={
tab.id === "favorites" ? registerFavoritesTabRef : undefined
}
/>
))}
</div>
@@ -46,7 +53,13 @@ interface TabButtonProps {
onRefReady?: (element: HTMLElement | null) => void;
}
function TabButton({ tab, isActive, onSelect, layoutId, onRefReady }: TabButtonProps) {
function TabButton({
tab,
isActive,
onSelect,
layoutId,
onRefReady,
}: TabButtonProps) {
const [isLoaded, setIsLoaded] = useState(false);
const buttonRef = useRef<HTMLDivElement>(null);
@@ -82,7 +95,7 @@ function TabButton({ tab, isActive, onSelect, layoutId, onRefReady }: TabButtonP
onSelect(tab.id);
setIsLoaded(true);
}}
className="w-fit h-fit flex"
className="flex h-fit w-fit"
style={{ willChange: "transform" }}
>
<motion.div
@@ -96,9 +109,9 @@ function TabButton({ tab, isActive, onSelect, layoutId, onRefReady }: TabButtonP
},
}}
className={cn(
"flex items-center gap-1.5 bg-zinc-200 border-zinc-200 text-black hover:bg-zinc-300 hover:border-zinc-300 overflow-hidden transition-colors duration-75 ease-out py-2 px-3 cursor-pointer h-fit",
"flex h-fit cursor-pointer items-center gap-1.5 overflow-hidden border border-zinc-200 px-3 py-2 text-black transition-colors duration-75 ease-out hover:border-zinc-300 hover:bg-zinc-300",
isActive && activeColor,
isActive ? "px-4" : "px-3"
isActive ? "px-4" : "px-3",
)}
style={{
borderRadius: "25px",
@@ -122,7 +135,7 @@ function TabButton({ tab, isActive, onSelect, layoutId, onRefReady }: TabButtonP
>
<motion.span
layoutId={`${layoutId}-text-${tab.id}`}
className="font-sans font-medium text-sm text-black"
className="font-sans text-sm font-medium text-black"
>
{tab.title}
</motion.span>