From fa14bf461bb451b4dcd6e965b98ea57ce1363050 Mon Sep 17 00:00:00 2001 From: Ubbe Date: Tue, 19 Aug 2025 08:56:35 +0100 Subject: [PATCH] feat(frontend): add Line Tabs component (#10674) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes 🏗️ Screenshot 2025-08-18 at 23 11 46 https://github.com/user-attachments/assets/4a83ed59-068e-46e0-8e76-4f34ed9dd976 - Needed for the new Agent Runs views ( [designs](https://www.figma.com/design/14jjs3hH3Hmkq4hGqxZWco/agent-runs-unification?node-id=187-8653&t=3BV5fF6NDXN7BlI8-1) ) - Took **shadcn** tabs as a base and applied styles on top ## Checklist 📋 ### For code changes - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Run storybook locally - [x] Play with the new tabs component ### For configuration changes None --- .../molecules/TabsLine/TabsLine.stories.tsx | 258 ++++++++++++++++++ .../molecules/TabsLine/TabsLine.tsx | 136 +++++++++ 2 files changed, 394 insertions(+) create mode 100644 autogpt_platform/frontend/src/components/molecules/TabsLine/TabsLine.stories.tsx create mode 100644 autogpt_platform/frontend/src/components/molecules/TabsLine/TabsLine.tsx diff --git a/autogpt_platform/frontend/src/components/molecules/TabsLine/TabsLine.stories.tsx b/autogpt_platform/frontend/src/components/molecules/TabsLine/TabsLine.stories.tsx new file mode 100644 index 0000000000..8d1922c420 --- /dev/null +++ b/autogpt_platform/frontend/src/components/molecules/TabsLine/TabsLine.stories.tsx @@ -0,0 +1,258 @@ +import type { Meta, StoryObj } from "@storybook/nextjs"; +import { + TabsLine, + TabsLineContent, + TabsLineList, + TabsLineTrigger, +} from "./TabsLine"; + +const meta = { + title: "Molecules/TabsLine", + component: TabsLine, + parameters: { + layout: "fullscreen", + }, + tags: ["autodocs"], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Helper component to demonstrate tabs functionality +function TabsDemo() { + return ( +
+

TabsLine Examples

+ +
+
+

Basic Tabs

+ + + Account + Password + Settings + + +
+ Make changes to your account here. Click save when you're + done. +
+
+ +
+ Change your password here. After saving, you'll be logged + out. +
+
+ +
+ Update your preferences and settings here. +
+
+
+
+ +
+

Many Tabs

+ + + Overview + Analytics + Reports + + Notifications + + + Integrations + + Billing + + +
+ Dashboard overview with key metrics and recent activity. +
+
+ +
+ Detailed analytics and performance metrics. +
+
+ +
+ Generate and view reports for your account. +
+
+ +
+ Manage your notification preferences. +
+
+ +
+ Connect and manage third-party integrations. +
+
+ +
+ View and manage your billing information. +
+
+
+
+ +
+

Disabled Tab

+ + + Active Tab + + Disabled Tab + + Another Active + + +
+ This is an active tab that you can interact with. +
+
+ +
+ This content is for the disabled tab. +
+
+ +
+ Another active tab with different content. +
+
+
+
+
+
+ ); +} + +export const Default: Story = { + render: () => , +}; + +export const BasicTabs: Story = { + render: () => ( +
+ + + Account + Password + + +
+ Make changes to your account here. Click save when you're done. +
+
+ +
+ Change your password here. After saving, you'll be logged out. +
+
+
+
+ ), +}; + +export const ManyTabs: Story = { + render: () => ( +
+ + + Home + About + Services + Portfolio + Contact + Blog + + +
Welcome to our homepage!
+
+ +
Learn more about our company.
+
+ +
Discover our range of services.
+
+ +
+ View our previous work and projects. +
+
+ +
Get in touch with us today.
+
+ +
Read our latest blog posts.
+
+
+
+ ), +}; + +export const WithDisabledTab: Story = { + render: () => ( +
+ + + Available + + Disabled + + Enabled + + +
+ This tab is available and can be clicked. +
+
+ +
+ This tab is disabled and cannot be accessed. +
+
+ +
+ This tab is also enabled and functional. +
+
+
+
+ ), +}; + +export const FullWidth: Story = { + render: () => ( +
+ + + Tab One + Tab Two + Tab Three + + +
+ Content for the first tab with full width layout. +
+
+ +
+ Content for the second tab with full width layout. +
+
+ +
+ Content for the third tab with full width layout. +
+
+
+
+ ), +}; diff --git a/autogpt_platform/frontend/src/components/molecules/TabsLine/TabsLine.tsx b/autogpt_platform/frontend/src/components/molecules/TabsLine/TabsLine.tsx new file mode 100644 index 0000000000..1d16e1e771 --- /dev/null +++ b/autogpt_platform/frontend/src/components/molecules/TabsLine/TabsLine.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; +import * as React from "react"; + +interface TabsLineContextValue { + activeTabElement: HTMLElement | null; + setActiveTabElement: React.Dispatch>; +} + +const TabsLineContext = React.createContext( + undefined, +); + +function useTabsLine() { + const context = React.useContext(TabsLineContext); + if (!context) { + throw new Error("useTabsLine must be used within a TabsLine"); + } + return context; +} + +function TabsLine( + props: React.ComponentPropsWithoutRef, +) { + const [activeTabElement, setActiveTabElement] = + React.useState(null); + + return ( + + + + ); +} + +const TabsLineList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { activeTabElement } = useTabsLine(); + const listRef = React.useRef(null); + + return ( +
+ { + if (typeof ref === "function") ref(node); + else if (ref) ref.current = node; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + listRef.current = node; + }} + className={cn( + "inline-flex w-full items-center justify-start border-b border-zinc-200", + className, + )} + {...props} + /> + {activeTabElement && ( +
+ )} +
+ ); +}); +TabsLineList.displayName = "TabsLineList"; + +const TabsLineTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const elementRef = React.useRef(null); + const { setActiveTabElement } = useTabsLine(); + + React.useEffect(() => { + if (!elementRef.current) return; + + const observer = new MutationObserver(() => { + if (!elementRef.current) return; + if (elementRef.current.getAttribute("data-state") === "active") { + setActiveTabElement(elementRef.current); + } + }); + + observer.observe(elementRef.current, { attributes: true }); + + // Initial check + if (elementRef.current.getAttribute("data-state") === "active") { + setActiveTabElement(elementRef.current); + } + + return () => observer.disconnect(); + }, [setActiveTabElement]); + + return ( + { + if (typeof ref === "function") ref(node); + else if (ref) ref.current = node; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + elementRef.current = node; + }} + className={cn( + "relative inline-flex items-center justify-center whitespace-nowrap px-3 py-2 font-sans text-[1rem] font-medium leading-[1.5rem] text-zinc-700 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-purple-600", + className, + )} + {...props} + /> + ); +}); +TabsLineTrigger.displayName = "TabsLineTrigger"; + +const TabsLineContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsLineContent.displayName = "TabsLineContent"; + +export { TabsLine, TabsLineContent, TabsLineList, TabsLineTrigger };