fix: Enhance Growth UI Layout and Consistency (#7768)

This commit is contained in:
Cristhian Zanforlin Lousa 2025-04-23 19:17:22 -03:00 • committed by GitHub
commit 60de34074b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 260 additions and 323 deletions

View file

@ -1,51 +0,0 @@
---
description:
globs:
alwaysApply: true
---
# Concise Task Management Protocol - Sequential Mode
## File Creation
- Filename: `YYYYMMDD_HHMMSS_task_name.md`
- IMPORTANT: Always include a descriptive task name after the timestamp (e.g., `20250404_135621_api_integration.md`)
- Never create files with timestamp only
- Store in `tasks/` directory
## Task Structure
```
# Task: [Task Name]
**Status**: [Not Started | In Progress | Completed]
## Analysis
- [ ] Requirements
- [ ] Subtask 1
- [ ] Challenges
- [ ] Dependencies
## Plan
- [ ] Step 1: [Description]
- [ ] Subtask 1.1
- [ ] Step 2: [Description]
## Execution
- [ ] Implementation 1
- [ ] Details
- [ ] Implementation 2
- [ ] Files modified: [files]
## Summary
- [ ] Files modified: `filename.ext` (lines X-Y)
- [ ] Dependencies added/changed
- [ ] Edge cases considered
- [ ] Known limitations
- [ ] Future impact points
```
## Execution Rules
- Execute one subtask at a time in sequence
- Update the task file after EACH subtask is completed
- Mark completed subtasks with [x] as they are finished
- Update main task status throughout execution
- Document work incrementally, not all at once
- Never proceed to the next section until all subtasks in the current section are completed and marked

View file

@ -53,7 +53,7 @@ const AlertDropdown = forwardRef<HTMLDivElement, AlertDropdownType>(
Notifications Notifications
<div className="flex gap-3 pr-3"> <div className="flex gap-3 pr-3">
<button <button
className="text-foreground hover:text-status-red" className="text-muted-foreground hover:text-status-red"
onClick={() => { onClick={() => {
setOpen(false); setOpen(false);
setTimeout(clearNotificationList, 100); setTimeout(clearNotificationList, 100);

View file

@ -35,6 +35,7 @@ export default function PageLayout({
onClick={() => { onClick={() => {
navigate(backTo); navigate(backTo);
}} }}
data-testid="back_page_button"
> >
<ForwardedIconComponent <ForwardedIconComponent
name="ChevronLeft" name="ChevronLeft"

View file

@ -13,7 +13,7 @@ export default function RenderIcons({
<span <span
className={cn( className={cn(
"flex items-center gap-0.5", "flex items-center gap-0.5",
tableRender ? "justify-start" : "justify-center text-[12px]", tableRender ? "justify-start" : "justify-center text-xs",
)} )}
> >
{shortcutList.map((key, index) => ( {shortcutList.map((key, index) => (

View file

@ -5,7 +5,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip";
// Extract static styles // Extract static styles
const BASE_TOOLTIP_CLASSES = const BASE_TOOLTIP_CLASSES =
"z-[99] max-w-96 bg-tooltip text-[12px] text-tooltip-foreground"; "z-[99] max-w-96 bg-tooltip text-xs text-tooltip-foreground";
// Memoize the tooltip content component // Memoize the tooltip content component
const MemoizedTooltipContent = memo( const MemoizedTooltipContent = memo(

View file

@ -2,7 +2,6 @@ import { usePostLikeComponent } from "@/controllers/API/queries/store";
import { useState } from "react"; import { useState } from "react";
import { getComponent } from "../../../controllers/API"; import { getComponent } from "../../../controllers/API";
import useAlertStore from "../../../stores/alertStore"; import useAlertStore from "../../../stores/alertStore";
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
import { useStoreStore } from "../../../stores/storeStore"; import { useStoreStore } from "../../../stores/storeStore";
import { FlowType } from "../../../types/flow"; import { FlowType } from "../../../types/flow";
import { storeComponent } from "../../../types/store"; import { storeComponent } from "../../../types/store";
@ -42,11 +41,6 @@ export default function StoreCardComponent({
const [downloadsCount, setDownloadsCount] = useState( const [downloadsCount, setDownloadsCount] = useState(
data?.downloads_count ?? 0, data?.downloads_count ?? 0,
); );
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
// const [openPlayground, setOpenPlayground] = useState(false);
const [loadingPlayground, setLoadingPlayground] = useState(false);
const playground =
data.last_tested_version?.includes("1.0.0") && !data.is_component;
const name = data.is_component ? "Component" : "Flow"; const name = data.is_component ? "Component" : "Flow";

View file

@ -1,11 +1,13 @@
import { ForwardedIconComponent } from "@/components/common/genericIconComponent"; import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
import { import {
DATASTAX_DOCS_URL,
DISCORD_URL, DISCORD_URL,
DOCS_URL, DOCS_URL,
GITHUB_URL, GITHUB_URL,
TWITTER_URL, TWITTER_URL,
} from "@/constants/constants"; } from "@/constants/constants";
import { useLogout } from "@/controllers/API/queries/auth"; import { useLogout } from "@/controllers/API/queries/auth";
import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags";
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate"; import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
import useAuthStore from "@/stores/authStore"; import useAuthStore from "@/stores/authStore";
import { useDarkStore } from "@/stores/darkStore"; import { useDarkStore } from "@/stores/darkStore";
@ -45,7 +47,7 @@ export const AccountMenu = () => {
<HeaderMenu> <HeaderMenu>
<HeaderMenuToggle> <HeaderMenuToggle>
<div <div
className="h-7 w-7 rounded-lg focus-visible:outline-0" className="h-6 w-6 rounded-lg focus-visible:outline-0"
data-testid="user-profile-settings" data-testid="user-profile-settings"
> >
<ProfileIcon /> <ProfileIcon />
@ -107,7 +109,10 @@ export const AccountMenu = () => {
</HeaderMenuItemButton> </HeaderMenuItemButton>
</div> </div>
)} )}
<HeaderMenuItemLink newPage href={DOCS_URL}> <HeaderMenuItemLink
newPage
href={ENABLE_DATASTAX_LANGFLOW ? DATASTAX_DOCS_URL : DOCS_URL}
>
<span data-testid="menu_docs_button" id="menu_docs_button"> <span data-testid="menu_docs_button" id="menu_docs_button">
Docs Docs
</span> </span>

View file

@ -189,7 +189,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
[currentFlowName], [currentFlowName],
); );
const handleNameSubmit = useCallback(() => { const handleNameSubmit = useCallback(async () => {
if ( if (
flowName.trim() !== "" && flowName.trim() !== "" &&
flowName !== currentFlowName && flowName !== currentFlowName &&
@ -203,9 +203,10 @@ export const MenuBar = ({}: {}): JSX.Element => {
name: flowName, name: flowName,
id: currentFlowId!, id: currentFlowId!,
}; };
setCurrentFlow(newFlow);
saveFlow(newFlow) saveFlow(newFlow)
.then(() => { .then(() => {
setCurrentFlow(newFlow);
setSuccessData({ title: "Flow name updated successfully" }); setSuccessData({ title: "Flow name updated successfully" });
}) })
.catch((error) => { .catch((error) => {
@ -268,7 +269,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
{currentFolder?.name && ( {currentFolder?.name && (
<div className="hidden truncate md:flex"> <div className="hidden truncate md:flex">
<div <div
className="cursor-pointer truncate pr-1 text-muted-foreground hover:text-primary" className="cursor-pointer truncate pr-1 text-xs text-muted-foreground hover:text-primary"
onClick={() => { onClick={() => {
navigate( navigate(
currentFolder?.id currentFolder?.id
@ -313,7 +314,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
> >
<Input <Input
className={cn( className={cn(
"h-6 w-full shrink-0 cursor-text font-semibold", "h-6 w-full shrink-0 cursor-text text-xs font-semibold",
"bg-transparent pl-1 pr-0 transition-colors duration-200", "bg-transparent pl-1 pr-0 transition-colors duration-200",
"border-0 outline-none focus:border-0 focus:outline-none focus:ring-0 focus:ring-offset-0", "border-0 outline-none focus:border-0 focus:outline-none focus:ring-0 focus:ring-offset-0",
!editingName && "text-primary hover:opacity-80", !editingName && "text-primary hover:opacity-80",
@ -334,7 +335,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
/> />
<span <span
ref={measureRef} ref={measureRef}
className="invisible absolute left-0 top-0 -z-10 w-fit whitespace-pre font-semibold" className="invisible absolute left-0 top-0 -z-10 w-fit whitespace-pre text-xs font-semibold"
aria-hidden="true" aria-hidden="true"
data-testid="flow_name" data-testid="flow_name"
> >

View file

@ -16,13 +16,15 @@ export const HeaderMenu = ({ children }) => (
export const HeaderMenuToggle = ({ children }) => ( export const HeaderMenuToggle = ({ children }) => (
<DropdownMenuTrigger <DropdownMenuTrigger
className="group inline-flex w-full items-center justify-center gap-1 rounded-md pr-0" className="inline-flex w-full items-center justify-center rounded-md pl-4 pr-1"
data-testid="user_menu_button" data-testid="user_menu_button"
id="user_menu_button" id="user_menu_button"
> >
<div className="flex items-center gap-1 rounded-lg px-2 py-1.5 group-hover:bg-muted"> <div className="group flex items-center self-center rounded-md">
{children} <div className="flex h-6 w-10 items-center justify-center rounded-full bg-background transition-colors hover:bg-muted group-hover:bg-muted">
<ChevronsUpDown className="h-4 w-4 text-muted-foreground group-hover:text-foreground" /> <div className="relative right-1 z-10">{children}</div>
<ChevronsUpDown className="relative h-[14px] w-[14px] text-muted-foreground group-hover:text-primary" />
</div>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
); );

View file

@ -12,7 +12,7 @@ export function ProfileIcon() {
return ( return (
<img <img
src={profileImageUrl} src={profileImageUrl}
className="h-7 w-7 shrink-0 focus-visible:outline-0" className="h-6 w-6 shrink-0 focus-visible:outline-0"
/> />
); );
} }

View file

@ -8,13 +8,13 @@ export const LangflowCounts = () => {
const discordCount: number = useDarkStore((state) => state.discordCount); const discordCount: number = useDarkStore((state) => state.discordCount);
return ( return (
<div className="flex items-center gap-5"> <div className="flex items-center gap-3">
<ShadTooltip <ShadTooltip
content="Go to GitHub repo" content="Go to GitHub repo"
side="bottom" side="bottom"
styleClasses="z-10" styleClasses="z-10"
> >
<div className="flex items-center gap-2 text-muted-foreground hover:text-foreground"> <div className="hit-area-hover flex items-center gap-2 rounded-md p-1 text-muted-foreground">
<FaGithub className="h-4 w-4" /> <FaGithub className="h-4 w-4" />
<span className="text-xs font-semibold">{formatNumber(stars)}</span> <span className="text-xs font-semibold">{formatNumber(stars)}</span>
</div> </div>
@ -25,7 +25,7 @@ export const LangflowCounts = () => {
side="bottom" side="bottom"
styleClasses="z-10" styleClasses="z-10"
> >
<div className="flex items-center gap-2 text-muted-foreground hover:text-foreground"> <div className="hit-area-hover flex items-center gap-2 rounded-md p-1 text-muted-foreground">
<FaDiscord className="h-4 w-4" /> <FaDiscord className="h-4 w-4" />
<span className="text-xs font-semibold"> <span className="text-xs font-semibold">
{formatNumber(discordCount)} {formatNumber(discordCount)}

View file

@ -54,9 +54,16 @@ export default function AppHeader(): JSX.Element {
const isEmpty = flows?.length !== examples?.length || folders?.length > 1; const isEmpty = flows?.length !== examples?.length || folders?.length > 1;
const getNotificationBadge = () => {
const baseClasses = "absolute h-1 w-1 rounded-full bg-destructive";
return notificationCenter
? `${baseClasses} right-[5.1rem] top-[10px]`
: "hidden";
};
return ( return (
<div <div
className={`flex h-[44px] w-full items-center justify-between border-b p-6 dark:bg-background ${ className={`flex h-[48px] w-full items-center justify-between border-b px-6 dark:bg-background ${
!isEmpty ? "hidden" : "" !isEmpty ? "hidden" : ""
}`} }`}
data-testid="app-header" data-testid="app-header"
@ -75,7 +82,7 @@ export default function AppHeader(): JSX.Element {
{ENABLE_DATASTAX_LANGFLOW ? ( {ENABLE_DATASTAX_LANGFLOW ? (
<DataStaxLogo className="fill-black dark:fill-[white]" /> <DataStaxLogo className="fill-black dark:fill-[white]" />
) : ( ) : (
<LangflowLogo className="h-5 w-6" /> <LangflowLogo className="h-6 w-6" />
)} )}
</Button> </Button>
{ENABLE_DATASTAX_LANGFLOW && ( {ENABLE_DATASTAX_LANGFLOW && (
@ -93,14 +100,13 @@ export default function AppHeader(): JSX.Element {
{/* Right Section */} {/* Right Section */}
<div <div
className={`relative left-3 z-30 flex items-center gap-2`} className={`relative left-3 z-30 flex items-center gap-1`}
data-testid="header_right_section_wrapper" data-testid="header_right_section_wrapper"
> >
{!ENABLE_DATASTAX_LANGFLOW && (
<> <>
<Button <Button
unstyled unstyled
className="hidden items-center whitespace-nowrap pr-2 2xl:inline" className="hidden items-center whitespace-nowrap pr-2 lg:inline"
onClick={() => onClick={() =>
window.open("https://github.com/langflow-ai/langflow", "_blank") window.open("https://github.com/langflow-ai/langflow", "_blank")
} }
@ -108,7 +114,6 @@ export default function AppHeader(): JSX.Element {
<LangflowCounts /> <LangflowCounts />
</Button> </Button>
</> </>
)}
<AlertDropdown <AlertDropdown
notificationRef={notificationContentRef} notificationRef={notificationContentRef}
onClose={() => setActiveState(null)} onClose={() => setActiveState(null)}
@ -129,19 +134,21 @@ export default function AppHeader(): JSX.Element {
} }
data-testid="notification_button" data-testid="notification_button"
> >
<span <div className="hit-area-hover group items-center rounded-md px-2 py-1 text-muted-foreground">
className={ <span className={getNotificationBadge()} />
notificationCenter
? `absolute right-[5.3rem] top-[10px] h-1 w-1 rounded-full bg-destructive`
: "hidden"
}
/>
<ForwardedIconComponent <ForwardedIconComponent
name="Bell" name="Bell"
className="side-bar-button-size ml-1 h-4 w-4 text-muted-foreground" className={`side-bar-button-size h-4 w-4 ${
activeState === "notifications"
? "text-primary"
: "text-muted-foreground group-hover:text-primary"
}`}
strokeWidth={2} strokeWidth={2}
/> />
<span className="hidden whitespace-nowrap">Notifications</span> <span className="hidden whitespace-nowrap">
Notifications
</span>
</div>
</Button> </Button>
</AlertDropdown> </AlertDropdown>
</ShadTooltip> </ShadTooltip>
@ -150,50 +157,7 @@ export default function AppHeader(): JSX.Element {
orientation="vertical" orientation="vertical"
className="my-auto ml-3 h-7 dark:border-zinc-700" className="my-auto ml-3 h-7 dark:border-zinc-700"
/> />
{ENABLE_DATASTAX_LANGFLOW && (
<>
<ShadTooltip content="Docs" side="bottom" styleClasses="z-10">
<Button
variant="ghost"
className="flex text-sm font-medium"
onClick={() =>
window.open(
"https://docs.datastax.com/en/langflow/index.html",
"_blank",
)
}
>
<ForwardedIconComponent
name="book-open-text"
className="side-bar-button-size h-[18px] w-[18px]"
/>
<span className="hidden whitespace-nowrap 2xl:inline">
Docs
</span>
</Button>
</ShadTooltip>
<ShadTooltip content="Settings" side="bottom" styleClasses="z-10">
<Button
data-testid="user-profile-settings"
variant="ghost"
className="flex text-sm font-medium"
onClick={() => navigate("/settings")}
>
<ForwardedIconComponent
name="Settings"
className="side-bar-button-size h-[18px] w-[18px]"
/>
<span className="hidden whitespace-nowrap 2xl:inline">
Settings
</span>
</Button>
</ShadTooltip>
<Separator
orientation="vertical"
className="my-auto h-7 dark:border-zinc-700"
/>
</>
)}
<div className="flex"> <div className="flex">
<AccountMenu /> <AccountMenu />
</div> </div>

View file

@ -82,9 +82,17 @@ export const GetStartedProgress: FC<{
}; };
return ( return (
<div className="h-[180px] w-full"> <div className="mt-3 h-[180px] w-full">
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<span className="font-[14px]">Get started</span> <span className="text-sm font-semibold">
{percentageGetStarted >= 100 ? (
<>
<span>All Set</span> <span className="pl-1"> 🎉 </span>
</>
) : (
"Get started"
)}
</span>
<button <button
onClick={() => handleUserTrack("dialog_dismissed")} onClick={() => handleUserTrack("dialog_dismissed")}
className="text-muted-foreground hover:text-foreground" className="text-muted-foreground hover:text-foreground"
@ -94,7 +102,7 @@ export const GetStartedProgress: FC<{
</button> </button>
</div> </div>
<div className="mb-1 flex items-center justify-between gap-3"> <div className="mb-1 mt-2 flex items-center justify-between gap-3">
<div className="h-1 w-full rounded-full bg-muted"> <div className="h-1 w-full rounded-full bg-muted">
<div <div
className="h-1 w-[33%] rounded-full bg-accent-pink-foreground" className="h-1 w-[33%] rounded-full bg-accent-pink-foreground"
@ -143,7 +151,7 @@ export const GetStartedProgress: FC<{
)} )}
<span <span
className={cn( className={cn(
"text-sm", "text-xs",
isGithubStarredChild && "text-muted-foreground line-through", isGithubStarredChild && "text-muted-foreground line-through",
)} )}
> >
@ -185,7 +193,7 @@ export const GetStartedProgress: FC<{
)} )}
<span <span
className={cn( className={cn(
"text-sm", "text-xs",
isDiscordJoinedChild && "text-muted-foreground line-through", isDiscordJoinedChild && "text-muted-foreground line-through",
)} )}
> >
@ -215,7 +223,7 @@ export const GetStartedProgress: FC<{
)} )}
/> />
</span> </span>
<span className={cn("text-sm", hasFlows && "line-through")}> <span className={cn("text-xs", hasFlows && "line-through")}>
Create a flow Create a flow
</span> </span>
</div> </div>

View file

@ -41,7 +41,7 @@ export const HeaderButtons = ({
handleDismissDialog={handleDismissDialog} handleDismissDialog={handleDismissDialog}
/> />
<div className="-mx-4 mt-4 w-[280px]"> <div className="-mx-4 mt-1 w-[280px]">
<hr className="border-t-1 w-full" /> <hr className="border-t-1 w-full" />
</div> </div>
</> </>

View file

@ -18,8 +18,10 @@ import {
import { useGetDownloadFolders } from "@/controllers/API/queries/folders/use-get-download-folders"; import { useGetDownloadFolders } from "@/controllers/API/queries/folders/use-get-download-folders";
import { import {
ENABLE_CUSTOM_PARAM, ENABLE_CUSTOM_PARAM,
ENABLE_DATASTAX_LANGFLOW,
ENABLE_FILE_MANAGEMENT, ENABLE_FILE_MANAGEMENT,
} from "@/customization/feature-flags"; } from "@/customization/feature-flags";
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
import { track } from "@/customization/utils/analytics"; import { track } from "@/customization/utils/analytics";
import { createFileUpload } from "@/helpers/create-file-upload"; import { createFileUpload } from "@/helpers/create-file-upload";
import { getObjectsFromFilelist } from "@/helpers/get-objects-from-filelist"; import { getObjectsFromFilelist } from "@/helpers/get-objects-from-filelist";
@ -56,6 +58,8 @@ const SideBarFoldersButtonsComponent = ({
const loading = !folders; const loading = !folders;
const refInput = useRef<HTMLInputElement>(null); const refInput = useRef<HTMLInputElement>(null);
const navigate = useCustomNavigate();
const currentFolder = pathname.split("/"); const currentFolder = pathname.split("/");
const urlWithoutPath = const urlWithoutPath =
pathname.split("/").length < (ENABLE_CUSTOM_PARAM ? 5 : 4); pathname.split("/").length < (ENABLE_CUSTOM_PARAM ? 5 : 4);
@ -348,7 +352,7 @@ const SideBarFoldersButtonsComponent = ({
collapsible={isMobile ? "offcanvas" : "none"} collapsible={isMobile ? "offcanvas" : "none"}
data-testid="folder-sidebar" data-testid="folder-sidebar"
> >
<SidebarHeader className="p-4"> <SidebarHeader className="px-4 py-1">
<HeaderButtons <HeaderButtons
handleUploadFlowsToFolder={handleUploadFlowsToFolder} handleUploadFlowsToFolder={handleUploadFlowsToFolder}
isUpdatingFolder={isUpdatingFolder} isUpdatingFolder={isUpdatingFolder}
@ -447,7 +451,24 @@ const SideBarFoldersButtonsComponent = ({
</SidebarContent> </SidebarContent>
{ENABLE_FILE_MANAGEMENT && ( {ENABLE_FILE_MANAGEMENT && (
<SidebarFooter className="border-t"> <SidebarFooter className="border-t">
<div className="flex w-full items-center gap-2 p-2"> <div className="grid w-full items-center gap-2 p-2">
{!ENABLE_DATASTAX_LANGFLOW && (
<div
className="flex w-full items-center"
data-testid="button-store"
>
<SidebarMenuButton
size="md"
className="text-[13px]"
onClick={() => {
window.open("/store", "_blank");
}}
>
<ForwardedIconComponent name="Store" />
Store
</SidebarMenuButton>
</div>
)}
<SidebarMenuButton <SidebarMenuButton
isActive={checkPathFiles} isActive={checkPathFiles}
onClick={() => handleFilesClick?.()} onClick={() => handleFilesClick?.()}

View file

@ -1073,5 +1073,7 @@ export const DISCORD_URL = "https://discord.com/invite/EqksyE2EX9";
export const GITHUB_URL = "https://github.com/langflow-ai/langflow"; export const GITHUB_URL = "https://github.com/langflow-ai/langflow";
export const TWITTER_URL = "https://x.com/langflow_ai"; export const TWITTER_URL = "https://x.com/langflow_ai";
export const DOCS_URL = "https://docs.langflow.org"; export const DOCS_URL = "https://docs.langflow.org";
export const DATASTAX_DOCS_URL =
"https://docs.datastax.com/en/langflow/index.html";
export const UUID_PARSING_ERROR = "uuid_parsing"; export const UUID_PARSING_ERROR = "uuid_parsing";

View file

@ -62,7 +62,7 @@ const ButtonSendWrapper = ({
<Case condition={showStopButton}> <Case condition={showStopButton}>
<div className="flex items-center gap-2 rounded-md text-[14px] font-medium"> <div className="flex items-center gap-2 rounded-md text-[14px] font-medium">
Stop Stop
<Loading className="h-[16px] w-[16px]" /> <Loading className="h-4 w-4" />
</div> </div>
</Case> </Case>

View file

@ -40,7 +40,7 @@ const NoInputView: React.FC<NoInputViewProps> = ({
> >
<div className="flex items-center gap-2 rounded-md text-[14px] font-medium"> <div className="flex items-center gap-2 rounded-md text-[14px] font-medium">
Stop Stop
<Loading className="h-[16px] w-[16px]" /> <Loading className="h-4 w-4" />
</div> </div>
</Button> </Button>
)} )}

View file

@ -18,7 +18,7 @@ export default function ShortcutDisplay({
{display_name && <span> {display_name} </span>} {display_name && <span> {display_name} </span>}
<span <span
className={cn( className={cn(
"flex h-[16px] w-[16px] items-center justify-center rounded-sm bg-muted text-muted-foreground", "flex h-4 w-4 items-center justify-center rounded-sm bg-muted text-muted-foreground",
display_name && "ml-3", display_name && "ml-3",
)} )}
> >
@ -26,7 +26,7 @@ export default function ShortcutDisplay({
</span> </span>
</div> </div>
) : ( ) : (
<div className="flex content-center items-center justify-center self-center text-[12px]"> <div className="flex content-center items-center justify-center self-center text-xs">
<span> {display_name} </span> <span> {display_name} </span>
<span <span
className={`ml-3 flex items-center rounded-sm bg-primary-hover px-1.5 py-[0.1em] text-muted`} className={`ml-3 flex items-center rounded-sm bg-primary-hover px-1.5 py-[0.1em] text-muted`}

View file

@ -15,15 +15,9 @@ export default function ToolbarSelectItem({
<div className={`flex ${style}`} data-testid={dataTestId}> <div className={`flex ${style}`} data-testid={dataTestId}>
<ForwardedIconComponent <ForwardedIconComponent
name={icon} name={icon}
className={`mr-2 ${ className={`mr-2 mt-[0.15em] h-4 w-4 ${ping && "animate-pulse text-green-500"}`}
icon === "Share3"
? "absolute left-2 top-[0.25em] h-6 w-6"
: "mt-[0.15em] h-4 w-4"
} ${ping && "animate-pulse text-green-500"}`}
/> />
<span className={`${icon === "Share3" ? "ml-[1.8em]" : " "}`}> <span>{value}</span>
{value}
</span>
<span <span
className={`absolute right-2 top-[0.43em] flex items-center rounded-sm bg-muted px-1.5 py-[0.1em] text-muted-foreground`} className={`absolute right-2 top-[0.43em] flex items-center rounded-sm bg-muted px-1.5 py-[0.1em] text-muted-foreground`}
> >

View file

@ -143,7 +143,7 @@ const GridComponent = ({ flowData }: { flowData: FlowType }) => {
</div> </div>
</div> </div>
<div className="line-clamp-2 h-full pt-5 text-sm text-primary"> <div className="line-clamp-2 h-full pt-3 text-sm text-primary">
{flowData.description} {flowData.description}
</div> </div>
</Card> </Card>

View file

@ -103,7 +103,7 @@ const HeaderComponent = ({
value={debouncedSearch} value={debouncedSearch}
onChange={handleSearch} onChange={handleSearch}
/> />
<div className="relative mr-2 flex rounded-lg border border-muted bg-muted"> <div className="relative top-[3px] mr-2 flex h-fit rounded-lg border border-muted bg-muted">
{/* Sliding Indicator */} {/* Sliding Indicator */}
<div <div
className={`absolute top-[3px] h-[33px] w-8 transform rounded-lg bg-background shadow-md transition-transform duration-300 ${ className={`absolute top-[3px] h-[33px] w-8 transform rounded-lg bg-background shadow-md transition-transform duration-300 ${
@ -129,7 +129,7 @@ const HeaderComponent = ({
<ForwardedIconComponent <ForwardedIconComponent
name={viewType === "list" ? "Menu" : "LayoutGrid"} name={viewType === "list" ? "Menu" : "LayoutGrid"}
aria-hidden="true" aria-hidden="true"
className="h-4 w-4 group-hover:text-foreground" className="relative bottom-[1px] h-4 w-4 group-hover:text-foreground"
/> />
</Button> </Button>
))} ))}

View file

@ -13,7 +13,6 @@ import { useFolderStore } from "@/stores/foldersStore";
import { formatNumber } from "@/utils/utils"; import { formatNumber } from "@/utils/utils";
import { ExternalLink } from "lucide-react"; import { ExternalLink } from "lucide-react";
import { FaDiscord, FaGithub } from "react-icons/fa"; import { FaDiscord, FaGithub } from "react-icons/fa";
import { HiArrowRight } from "react-icons/hi";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import useFileDrop from "../hooks/use-on-file-drop"; import useFileDrop from "../hooks/use-on-file-drop";
@ -68,14 +67,14 @@ export const EmptyPageCommunity = ({
onFileDrop={handleFileDrop} onFileDrop={handleFileDrop}
> >
<div className="m-0 h-full w-full bg-background p-0"> <div className="m-0 h-full w-full bg-background p-0">
<div className="z-50 flex h-full w-full flex-col items-center justify-center gap-6"> <div className="z-50 flex h-full w-full flex-col items-center justify-center gap-5">
<div className="z-50 flex flex-col items-center gap-3"> <div className="z-50 flex flex-col items-center gap-2">
<div className="z-50 dark:hidden"> <div className="z-50 dark:hidden">
<img <img
src={logoLightPng} src={logoLightPng}
alt="Langflow Logo Light" alt="Langflow Logo Light"
data-testid="empty_page_logo_light" data-testid="empty_page_logo_light"
className="" className="relative top-3"
/> />
</div> </div>
<div className="z-50 hidden dark:block"> <div className="z-50 hidden dark:block">
@ -83,19 +82,19 @@ export const EmptyPageCommunity = ({
src={logoDarkPng} src={logoDarkPng}
alt="Langflow Logo Dark" alt="Langflow Logo Dark"
data-testid="empty_page_logo_dark" data-testid="empty_page_logo_dark"
className="" className="relative top-3"
/> />
</div> </div>
<span <span
data-testid="mainpage_title" data-testid="mainpage_title"
className="z-50 text-center text-2xl font-semibold text-foreground" className="z-50 text-center font-chivo text-2xl font-semibold text-foreground"
> >
{EMPTY_PAGE_TITLE} {EMPTY_PAGE_TITLE}
</span> </span>
<span <span
data-testid="empty_page_description" data-testid="empty_page_description"
className="z-50 text-center text-[14px] text-secondary-foreground" className="z-50 text-center text-xs text-secondary-foreground"
> >
{folders?.length > 1 {folders?.length > 1
? EMPTY_PAGE_FOLDER_DESCRIPTION ? EMPTY_PAGE_FOLDER_DESCRIPTION
@ -103,7 +102,7 @@ export const EmptyPageCommunity = ({
</span> </span>
</div> </div>
<div className="flex w-full max-w-[510px] flex-col gap-12 sm:gap-8"> <div className="flex w-full max-w-[510px] flex-col gap-12 sm:gap-[29px]">
<Button <Button
unstyled unstyled
className="group mx-3 h-[84px] sm:mx-0" className="group mx-3 h-[84px] sm:mx-0"
@ -113,7 +112,7 @@ export const EmptyPageCommunity = ({
}} }}
data-testid="empty_page_github_button" data-testid="empty_page_github_button"
> >
<div className="relative flex flex-col rounded-lg border-[1px] bg-background p-4 transition-all duration-300 hover:-translate-y-0.5 hover:border-accent-pink-foreground hover:shadow-[0_4px_10px_rgba(198,97,184,0.25)]"> <div className="relative flex flex-col rounded-lg border-[1px] bg-background p-4 transition-all duration-300 hover:border-accent-pink-foreground">
<div className="grid w-full items-center justify-between gap-2"> <div className="grid w-full items-center justify-between gap-2">
<div className="flex gap-3"> <div className="flex gap-3">
<FaGithub className="h-6 w-6" /> <FaGithub className="h-6 w-6" />
@ -143,7 +142,7 @@ export const EmptyPageCommunity = ({
}} }}
data-testid="empty_page_discord_button" data-testid="empty_page_discord_button"
> >
<div className="relative flex flex-col rounded-lg border-[1px] bg-background p-4 transition-all duration-300 hover:-translate-y-0.5 hover:border-discord-color hover:shadow-[0_4px_10px_rgba(88,101,242,0.25)]"> <div className="relative flex flex-col rounded-lg border-[1px] bg-background p-4 transition-all duration-300 hover:border-discord-color">
<div className="grid w-full items-center justify-between gap-2"> <div className="grid w-full items-center justify-between gap-2">
<div className="flex gap-3"> <div className="flex gap-3">
<FaDiscord className="h-6 w-6 text-discord-color" /> <FaDiscord className="h-6 w-6 text-discord-color" />
@ -166,7 +165,7 @@ export const EmptyPageCommunity = ({
<Button <Button
variant="default" variant="default"
className="z-10 m-auto h-10 w-full max-w-[155px] rounded-lg font-bold transition-all duration-300" className="z-10 m-auto mt-3 h-10 w-full max-w-[155px] rounded-lg font-bold transition-all duration-300"
onClick={() => setOpenModal(true)} onClick={() => setOpenModal(true)}
id="new-project-btn" id="new-project-btn"
data-testid="new_project_btn_empty_page" data-testid="new_project_btn_empty_page"
@ -178,7 +177,7 @@ export const EmptyPageCommunity = ({
</div> </div>
<p <p
data-testid="empty_page_drag_and_drop_text" data-testid="empty_page_drag_and_drop_text"
className="absolute bottom-5 left-0 right-0 mt-4 cursor-default text-center text-sm text-muted-foreground" className="absolute bottom-5 left-0 right-0 mt-4 cursor-default text-center text-xs text-muted-foreground"
> >
{EMPTY_PAGE_DRAG_AND_DROP_TEXT} {EMPTY_PAGE_DRAG_AND_DROP_TEXT}
</p> </p>

View file

@ -148,7 +148,10 @@ export default function StorePage(): JSX.Element {
setLoading(false); setLoading(false);
setErrorData({ setErrorData({
title: COMPONENTS_ERROR_ALERT, title: COMPONENTS_ERROR_ALERT,
list: [err["response"]["data"]["detail"]], list: [
err?.response?.data?.detail ??
"There was an error fetching the components",
],
}); });
} }
}); });

View file

@ -1257,6 +1257,10 @@
@apply h-7 w-7 rounded-md; @apply h-7 w-7 rounded-md;
} }
.hit-area-hover {
@apply transition-colors duration-200 hover:bg-muted hover:text-foreground;
}
.node-toolbar-buttons { .node-toolbar-buttons {
@apply flex w-max items-center gap-1 rounded-lg text-foreground; @apply flex w-max items-center gap-1 rounded-lg text-foreground;
} }

View file

@ -80,8 +80,8 @@ withEventDeliveryModes(
const output = await page.getByTestId("div-chat-message").allTextContents(); const output = await page.getByTestId("div-chat-message").allTextContents();
const outputText = output.join("\n"); const outputText = output.join("\n");
expect(outputText.toLowerCase()).toContain("weather"); expect(outputText.toLowerCase()).toContain("travel");
expect(outputText.toLowerCase()).toContain("budget"); expect(outputText.toLowerCase()).toContain("day");
expect(outputText.toLowerCase()).toContain(randomCity.toLowerCase()); expect(outputText.toLowerCase()).toContain(randomCity.toLowerCase());
expect(outputText.toLowerCase()).toContain(randomCity2.toLowerCase()); expect(outputText.toLowerCase()).toContain(randomCity2.toLowerCase());

View file

@ -37,35 +37,6 @@ withEventDeliveryModes(
await initialGPTsetup(page); await initialGPTsetup(page);
if (process?.env?.ASTRA_DB_API_ENDPOINT?.includes("astra-dev")) {
await page.getByTestId("title-Astra DB").first().click();
await page.getByTestId("code-button-modal").click();
await page.waitForSelector("text=Edit Code", {
timeout: 3000,
});
let cleanCode = await extractAndCleanCode(page);
cleanCode = cleanCode!.replace(
'"pre_delete_collection": self.pre_delete_collection or False,',
'"pre_delete_collection": self.pre_delete_collection or False,\n "environment": "dev",',
);
await page.locator("textarea").last().press(`ControlOrMeta+a`);
await page.keyboard.press("Backspace");
await page.locator("textarea").last().fill(cleanCode);
await page.locator('//*[@id="checkAndSaveBtn"]').click();
await page.waitForSelector('[data-testid="title-Astra DB"]', {
timeout: 3000,
});
await page.getByTestId("title-Astra DB").last().click();
await page.getByTestId("code-button-modal").click();
await page.waitForSelector("text=Edit Code", {
timeout: 3000,
});
await page.locator("textarea").last().press(`ControlOrMeta+a`);
await page.keyboard.press("Backspace");
await page.locator("textarea").last().fill(cleanCode);
await page.locator('//*[@id="checkAndSaveBtn"]').click();
}
await page.waitForSelector('[data-testid="title-Astra DB"]', { await page.waitForSelector('[data-testid="title-Astra DB"]', {
timeout: 3000, timeout: 3000,
}); });
@ -263,7 +234,10 @@ withEventDeliveryModes(
path.join(__dirname, "../../assets/test_file.txt"), path.join(__dirname, "../../assets/test_file.txt"),
); );
await page.getByText("test_file.txt").last().isVisible(); await page.getByText("test_file.txt").last().isVisible();
await page.waitForTimeout(500); await page.waitForSelector("text=file uploaded successfully", {
timeout: 10000,
});
await page.waitForTimeout(3000);
await page.getByTestId("select-files-modal-button").click(); await page.getByTestId("select-files-modal-button").click();
await page.getByTestId("button_run_astra db").last().click(); await page.getByTestId("button_run_astra db").last().click();
await page.waitForSelector("text=built successfully", { await page.waitForSelector("text=built successfully", {

View file

@ -19,6 +19,8 @@ test(
await page.keyboard.press("Enter"); await page.keyboard.press("Enter");
await page.waitForTimeout(1000);
let flowName = await page.getByTestId("input-flow-name").inputValue(); let flowName = await page.getByTestId("input-flow-name").inputValue();
expect(flowName).toBe(randomName); expect(flowName).toBe(randomName);
@ -44,6 +46,8 @@ test(
await page.keyboard.press("Enter"); await page.keyboard.press("Enter");
await page.waitForTimeout(1000);
flowName = await page.getByTestId("input-flow-name").inputValue(); flowName = await page.getByTestId("input-flow-name").inputValue();
expect(flowName).toBe(randomName2); expect(flowName).toBe(randomName2);
@ -84,6 +88,8 @@ test(
await page.keyboard.press("Enter"); await page.keyboard.press("Enter");
await page.waitForTimeout(1000);
flowName = await page.getByTestId("input-flow-name").inputValue(); flowName = await page.getByTestId("input-flow-name").inputValue();
expect(flowName).toBe(randomName4); expect(flowName).toBe(randomName4);

View file

@ -1,14 +1,15 @@
import { test } from "@playwright/test"; import { test } from "@playwright/test";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
test("should exists Store", { tag: ["@release"] }, async ({ page }) => { test("should exists Store", { tag: ["@release"] }, async ({ page }) => {
await page.goto("/"); await awaitBootstrapTest(page, { skipModal: true });
await page.getByTestId("button-store").isVisible(); await page.getByTestId("button-store").isVisible();
await page.getByTestId("button-store").isEnabled(); await page.getByTestId("button-store").isEnabled();
}); });
test("should not have an API key", { tag: ["@release"] }, async ({ page }) => { test("should not have an API key", { tag: ["@release"] }, async ({ page }) => {
await page.goto("/"); await awaitBootstrapTest(page, { skipModal: true });
await page.getByTestId("button-store").click(); await page.getByTestId("button-store").click();

View file

@ -14,7 +14,7 @@ test.skip(
if (!process.env.CI) { if (!process.env.CI) {
dotenv.config({ path: path.resolve(__dirname, "../../.env") }); dotenv.config({ path: path.resolve(__dirname, "../../.env") });
} }
await awaitBootstrapTest(page); await awaitBootstrapTest(page, { skipModal: true });
await page.getByText("Close", { exact: true }).click(); await page.getByText("Close", { exact: true }).click();
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
@ -96,7 +96,8 @@ test.skip(
dotenv.config({ path: path.resolve(__dirname, "../../.env") }); dotenv.config({ path: path.resolve(__dirname, "../../.env") });
} }
await page.goto("/"); await awaitBootstrapTest(page, { skipModal: true });
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
await page.getByTestId("button-store").click(); await page.getByTestId("button-store").click();

View file

@ -1,11 +1,14 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import path from "path"; import path from "path";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
test( test(
"should order the visualization (requires store API key)", "should order the visualization (requires store API key)",
{ tag: ["@release"] }, { tag: ["@release"] },
async ({ page }) => { async ({ page, context }) => {
test.skip();
test.skip( test.skip(
!process?.env?.STORE_API_KEY, !process?.env?.STORE_API_KEY,
"STORE_API_KEY required to run this test", "STORE_API_KEY required to run this test",
@ -15,45 +18,71 @@ test(
dotenv.config({ path: path.resolve(__dirname, "../../.env") }); dotenv.config({ path: path.resolve(__dirname, "../../.env") });
} }
await page.goto("/"); await awaitBootstrapTest(page, { skipModal: true });
await page.getByTestId("button-store").click(); await page.waitForTimeout(1000);
await page.waitForSelector('[data-testid="button-store"]', {
state: "visible",
timeout: 10000,
});
await page.getByTestId("api-key-button-store").click({ const [newPageStore] = await Promise.all([
context.waitForEvent("page"),
page.getByTestId("button-store").click(),
]);
await newPageStore.waitForTimeout(1000);
await newPageStore.getByTestId("api-key-button-store").click({
timeout: 200000, timeout: 200000,
}); });
await page await newPageStore.getByTestId("sidebar-nav-Langflow Store").click();
await newPageStore
.getByPlaceholder("Insert your API Key") .getByPlaceholder("Insert your API Key")
.fill(process.env.STORE_API_KEY ?? ""); .fill(process.env.STORE_API_KEY ?? "");
await page.getByTestId("api-key-save-button-store").click(); await newPageStore.getByTestId("api-key-save-button-store").click();
await expect(page.getByText("API key saved successfully")).toBeVisible({ await expect(
newPageStore.getByText("API key saved successfully"),
).toBeVisible({
timeout: 5000, timeout: 5000,
}); });
await page.getByTestId("button-store").click(); await newPageStore.getByTestId("back_page_button").click();
await expect(page.getByText("Basic RAG")).toBeVisible({ timeout: 30000 }); await newPageStore.waitForTimeout(1000);
await page.getByTestId("select-order-store").click(); const newPageStore2 = await context.newPage();
await page.getByText("Alphabetical").click(); await newPageStore2.goto("/store");
await page.getByText("Album Cover Builder").isVisible(); await newPageStore2.waitForTimeout(1000);
await page.getByTestId("select-order-store").click(); await expect(newPageStore2.getByText("Basic RAG")).toBeVisible({
await page.getByText("Popular").click(); timeout: 30000,
});
await page.getByText("Basic RAG").isVisible(); await newPageStore2.getByTestId("select-order-store").click();
await newPageStore2.getByText("Alphabetical").click();
await newPageStore2.getByText("Album Cover Builder").isVisible();
await newPageStore2.getByTestId("select-order-store").click();
await newPageStore2.getByText("Popular").click();
await newPageStore2.getByText("Basic RAG").isVisible();
}, },
); );
test( test(
"should filter by type (requires store API key)", "should filter by type (requires store API key)",
{ tag: ["@release"] }, { tag: ["@release"] },
async ({ page }) => { async ({ page, context }) => {
test.skip();
test.skip( test.skip(
!process?.env?.STORE_API_KEY, !process?.env?.STORE_API_KEY,
"STORE_API_KEY required to run this test", "STORE_API_KEY required to run this test",
@ -61,60 +90,95 @@ test(
if (!process.env.CI) { if (!process.env.CI) {
dotenv.config({ path: path.resolve(__dirname, "../../.env") }); dotenv.config({ path: path.resolve(__dirname, "../../.env") });
} }
await page.goto("/"); await awaitBootstrapTest(page, { skipModal: true });
await page.waitForSelector('[data-testid="button-store"]', {
state: "visible",
timeout: 10000,
});
await page.getByTestId("button-store").click(); await page.getByTestId("button-store").click();
await page.getByTestId("api-key-button-store").click({
const newPageStore = await context.waitForEvent("page", { timeout: 30000 });
await newPageStore.waitForTimeout(1000);
await newPageStore.getByTestId("api-key-button-store").click({
timeout: 200000, timeout: 200000,
}); });
await page await newPageStore.getByTestId("sidebar-nav-Langflow Store").click();
await newPageStore
.getByPlaceholder("Insert your API Key") .getByPlaceholder("Insert your API Key")
.fill(process.env.STORE_API_KEY ?? ""); .fill(process.env.STORE_API_KEY ?? "");
await page.getByTestId("api-key-save-button-store").click(); await newPageStore.getByTestId("api-key-save-button-store").click();
await expect(page.getByText("API key saved successfully")).toBeVisible({ await expect(
newPageStore.getByText("API key saved successfully"),
).toBeVisible({
timeout: 5000, timeout: 5000,
}); });
await page.getByTestId("button-store").click();
await page.waitForSelector('[data-testid="likes-Website Content QA"]', { await newPageStore.getByTestId("back_page_button").click();
await newPageStore.waitForTimeout(1000);
const newPageStore2 = await context.newPage();
await newPageStore2.goto("/store");
await newPageStore2.waitForTimeout(1000);
await newPageStore2.waitForSelector(
'[data-testid="likes-Website Content QA"]',
{
timeout: 100000,
},
);
await newPageStore2.getByText("Website Content QA").isVisible();
await newPageStore2.waitForSelector('[data-testid="flows-button-store"]', {
timeout: 100000, timeout: 100000,
}); });
await page.getByText("Website Content QA").isVisible(); await newPageStore2.getByTestId("flows-button-store").click();
await page.waitForSelector('[data-testid="flows-button-store"]', { await newPageStore2.waitForSelector('[data-testid="icon-Group"]', {
timeout: 100000, timeout: 100000,
}); });
await page.getByTestId("flows-button-store").click(); let iconGroup = await newPageStore2.getByTestId("icon-Group")?.count();
await page.waitForSelector('[data-testid="icon-Group"]', {
timeout: 100000,
});
let iconGroup = await page.getByTestId("icon-Group")?.count();
expect(iconGroup).not.toBe(0); expect(iconGroup).not.toBe(0);
await page.getByText("icon-ToyBrick").last().isHidden(); await newPageStore2.getByText("icon-ToyBrick").last().isHidden();
await page.waitForSelector('[data-testid="components-button-store"]', { await newPageStore2.waitForSelector(
'[data-testid="components-button-store"]',
{
timeout: 100000, timeout: 100000,
}); },
await page.getByTestId("components-button-store").click(); );
await expect(page.getByTestId("icon-Group").last()).toBeHidden({ await newPageStore2.getByTestId("components-button-store").click();
await expect(newPageStore2.getByTestId("icon-Group").last()).toBeHidden({
timeout: 30000, timeout: 30000,
}); });
await page.waitForSelector('[data-testid="icon-ToyBrick"]', { await newPageStore2.waitForSelector('[data-testid="icon-ToyBrick"]', {
timeout: 100000, timeout: 100000,
}); });
let toyBrick = await page.getByTestId("icon-ToyBrick")?.count(); let toyBrick = await newPageStore2.getByTestId("icon-ToyBrick")?.count();
expect(toyBrick).not.toBe(0); expect(toyBrick).not.toBe(0);
await page.waitForSelector('[data-testid="all-button-store"]', { await newPageStore2.waitForSelector('[data-testid="all-button-store"]', {
timeout: 100000, timeout: 100000,
}); });
await page.getByTestId("all-button-store").click(); await newPageStore2.getByTestId("all-button-store").click();
await page.waitForSelector('[data-testid="icon-Group"]', { await newPageStore2.waitForSelector('[data-testid="icon-Group"]', {
timeout: 100000, timeout: 100000,
}); });
await page.waitForSelector('[data-testid="icon-ToyBrick"]', { await newPageStore2.waitForSelector('[data-testid="icon-ToyBrick"]', {
timeout: 100000, timeout: 100000,
}); });
let iconGroupAllCount = await page.getByTestId("icon-Group")?.count(); let iconGroupAllCount = await newPageStore2
await page.waitForTimeout(500); .getByTestId("icon-Group")
let toyBrickAllCount = await page.getByTestId("icon-ToyBrick")?.count(); ?.count();
await page.waitForTimeout(500); await newPageStore2.waitForTimeout(500);
let toyBrickAllCount = await newPageStore2
.getByTestId("icon-ToyBrick")
?.count();
await newPageStore2.waitForTimeout(500);
if (iconGroupAllCount === 0 || toyBrickAllCount === 0) { if (iconGroupAllCount === 0 || toyBrickAllCount === 0) {
expect(false).toBe(true); expect(false).toBe(true);
} }

View file

@ -1,56 +0,0 @@
# Task: Gradient Border Styling Refinement
**Status**: Completed
## Analysis
- [x] Requirements
- [x] Match the gradient border style of the GitHub card shown in the reference image
- [x] Ensure a consistent purple hue that fades from top to bottom
- [x] Maintain proper dark background for content
- [x] Challenges
- [x] Achieving precise gradient fade matching the reference image
- [x] Balancing border visibility while maintaining subtle effect
- [x] Dependencies
- [x] Existing BackgroundGradient component
## Plan
- [x] Step 1: Review the current implementation
- [x] Analyze existing gradient colors and opacity values
- [x] Compare with reference image for differences
- [x] Step 2: Adjust color and gradient parameters
- [x] Modify gradient colors to match the purple hue
- [x] Fine-tune opacity values for natural fade
- [x] Ensure proper background color for inner content
- [x] Step 3: Optimize hover effects
- [x] Refine blur and opacity transitions on hover
- [x] Test with different content types
## Execution
- [x] Modify gradient parameters
- [x] Update gradient direction and opacity stops
- [x] Simplify to single-color fade for consistency
- [x] Adjust container styles
- [x] Set proper padding for border thickness
- [x] Ensure rounded corners match reference
- [x] Test and refine
- [x] Compare implementation with reference image
- [x] Make final adjustments to achieve exact match
## Summary
- [x] Files modified: `src/frontend/src/components/ui/background-gradient.tsx`
- [x] Dependencies added/changed: None
- [x] Edge cases considered: Different screen sizes, container dimensions
- [x] Known limitations: Exact color reproduction may vary slightly based on monitor calibration
- [x] Future impact points: Component can be reused across the application for consistent styling
### Implementation Details
1. Changed the gradient to a simpler purple fade from top to bottom
2. Used opacity values of 0.7 at top and 0.3 at bottom for subtle fade
3. Reduced border thickness to 2px with `p-[2px]`
4. Made the hover effect more subtle with `opacity-60` instead of `opacity-100`
5. Changed blur on hover from `blur-lg` to `blur-md` for a less intense glow