mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-07 22:33:57 -05:00
feat(frontend): add Line Tabs component (#10674)
## Changes 🏗️ <img width="800" height="644" alt="Screenshot 2025-08-18 at 23 11 46" src="https://github.com/user-attachments/assets/8c9e1257-5b33-4e4d-937d-e8924b18d7dd" /> 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
This commit is contained in:
@@ -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<typeof TabsLine>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// Helper component to demonstrate tabs functionality
|
||||
function TabsDemo() {
|
||||
return (
|
||||
<div className="flex flex-col gap-8 p-8">
|
||||
<h2 className="text-2xl font-bold">TabsLine Examples</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg font-semibold">Basic Tabs</h3>
|
||||
<TabsLine defaultValue="tab1" className="w-full">
|
||||
<TabsLineList>
|
||||
<TabsLineTrigger value="tab1">Account</TabsLineTrigger>
|
||||
<TabsLineTrigger value="tab2">Password</TabsLineTrigger>
|
||||
<TabsLineTrigger value="tab3">Settings</TabsLineTrigger>
|
||||
</TabsLineList>
|
||||
<TabsLineContent value="tab1">
|
||||
<div className="p-4 text-sm">
|
||||
Make changes to your account here. Click save when you're
|
||||
done.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="tab2">
|
||||
<div className="p-4 text-sm">
|
||||
Change your password here. After saving, you'll be logged
|
||||
out.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="tab3">
|
||||
<div className="p-4 text-sm">
|
||||
Update your preferences and settings here.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
</TabsLine>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg font-semibold">Many Tabs</h3>
|
||||
<TabsLine defaultValue="overview" className="w-full">
|
||||
<TabsLineList>
|
||||
<TabsLineTrigger value="overview">Overview</TabsLineTrigger>
|
||||
<TabsLineTrigger value="analytics">Analytics</TabsLineTrigger>
|
||||
<TabsLineTrigger value="reports">Reports</TabsLineTrigger>
|
||||
<TabsLineTrigger value="notifications">
|
||||
Notifications
|
||||
</TabsLineTrigger>
|
||||
<TabsLineTrigger value="integrations">
|
||||
Integrations
|
||||
</TabsLineTrigger>
|
||||
<TabsLineTrigger value="billing">Billing</TabsLineTrigger>
|
||||
</TabsLineList>
|
||||
<TabsLineContent value="overview">
|
||||
<div className="p-4 text-sm">
|
||||
Dashboard overview with key metrics and recent activity.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="analytics">
|
||||
<div className="p-4 text-sm">
|
||||
Detailed analytics and performance metrics.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="reports">
|
||||
<div className="p-4 text-sm">
|
||||
Generate and view reports for your account.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="notifications">
|
||||
<div className="p-4 text-sm">
|
||||
Manage your notification preferences.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="integrations">
|
||||
<div className="p-4 text-sm">
|
||||
Connect and manage third-party integrations.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="billing">
|
||||
<div className="p-4 text-sm">
|
||||
View and manage your billing information.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
</TabsLine>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg font-semibold">Disabled Tab</h3>
|
||||
<TabsLine defaultValue="active1" className="w-full">
|
||||
<TabsLineList>
|
||||
<TabsLineTrigger value="active1">Active Tab</TabsLineTrigger>
|
||||
<TabsLineTrigger value="disabled" disabled>
|
||||
Disabled Tab
|
||||
</TabsLineTrigger>
|
||||
<TabsLineTrigger value="active2">Another Active</TabsLineTrigger>
|
||||
</TabsLineList>
|
||||
<TabsLineContent value="active1">
|
||||
<div className="p-4 text-sm">
|
||||
This is an active tab that you can interact with.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="disabled">
|
||||
<div className="p-4 text-sm">
|
||||
This content is for the disabled tab.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="active2">
|
||||
<div className="p-4 text-sm">
|
||||
Another active tab with different content.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
</TabsLine>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <TabsDemo />,
|
||||
};
|
||||
|
||||
export const BasicTabs: Story = {
|
||||
render: () => (
|
||||
<div className="p-8">
|
||||
<TabsLine defaultValue="account" className="w-[400px]">
|
||||
<TabsLineList>
|
||||
<TabsLineTrigger value="account">Account</TabsLineTrigger>
|
||||
<TabsLineTrigger value="password">Password</TabsLineTrigger>
|
||||
</TabsLineList>
|
||||
<TabsLineContent value="account">
|
||||
<div className="p-4 text-sm">
|
||||
Make changes to your account here. Click save when you're done.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="password">
|
||||
<div className="p-4 text-sm">
|
||||
Change your password here. After saving, you'll be logged out.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
</TabsLine>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const ManyTabs: Story = {
|
||||
render: () => (
|
||||
<div className="p-8">
|
||||
<TabsLine defaultValue="home" className="w-full">
|
||||
<TabsLineList>
|
||||
<TabsLineTrigger value="home">Home</TabsLineTrigger>
|
||||
<TabsLineTrigger value="about">About</TabsLineTrigger>
|
||||
<TabsLineTrigger value="services">Services</TabsLineTrigger>
|
||||
<TabsLineTrigger value="portfolio">Portfolio</TabsLineTrigger>
|
||||
<TabsLineTrigger value="contact">Contact</TabsLineTrigger>
|
||||
<TabsLineTrigger value="blog">Blog</TabsLineTrigger>
|
||||
</TabsLineList>
|
||||
<TabsLineContent value="home">
|
||||
<div className="p-4 text-sm">Welcome to our homepage!</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="about">
|
||||
<div className="p-4 text-sm">Learn more about our company.</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="services">
|
||||
<div className="p-4 text-sm">Discover our range of services.</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="portfolio">
|
||||
<div className="p-4 text-sm">
|
||||
View our previous work and projects.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="contact">
|
||||
<div className="p-4 text-sm">Get in touch with us today.</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="blog">
|
||||
<div className="p-4 text-sm">Read our latest blog posts.</div>
|
||||
</TabsLineContent>
|
||||
</TabsLine>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithDisabledTab: Story = {
|
||||
render: () => (
|
||||
<div className="p-8">
|
||||
<TabsLine defaultValue="available" className="w-[400px]">
|
||||
<TabsLineList>
|
||||
<TabsLineTrigger value="available">Available</TabsLineTrigger>
|
||||
<TabsLineTrigger value="disabled" disabled>
|
||||
Disabled
|
||||
</TabsLineTrigger>
|
||||
<TabsLineTrigger value="enabled">Enabled</TabsLineTrigger>
|
||||
</TabsLineList>
|
||||
<TabsLineContent value="available">
|
||||
<div className="p-4 text-sm">
|
||||
This tab is available and can be clicked.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="disabled">
|
||||
<div className="p-4 text-sm">
|
||||
This tab is disabled and cannot be accessed.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="enabled">
|
||||
<div className="p-4 text-sm">
|
||||
This tab is also enabled and functional.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
</TabsLine>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const FullWidth: Story = {
|
||||
render: () => (
|
||||
<div className="p-8">
|
||||
<TabsLine defaultValue="tab1" className="w-full">
|
||||
<TabsLineList className="grid w-full grid-cols-3">
|
||||
<TabsLineTrigger value="tab1">Tab One</TabsLineTrigger>
|
||||
<TabsLineTrigger value="tab2">Tab Two</TabsLineTrigger>
|
||||
<TabsLineTrigger value="tab3">Tab Three</TabsLineTrigger>
|
||||
</TabsLineList>
|
||||
<TabsLineContent value="tab1">
|
||||
<div className="p-4 text-sm">
|
||||
Content for the first tab with full width layout.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="tab2">
|
||||
<div className="p-4 text-sm">
|
||||
Content for the second tab with full width layout.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
<TabsLineContent value="tab3">
|
||||
<div className="p-4 text-sm">
|
||||
Content for the third tab with full width layout.
|
||||
</div>
|
||||
</TabsLineContent>
|
||||
</TabsLine>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -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<React.SetStateAction<HTMLElement | null>>;
|
||||
}
|
||||
|
||||
const TabsLineContext = React.createContext<TabsLineContextValue | undefined>(
|
||||
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<typeof TabsPrimitive.Root>,
|
||||
) {
|
||||
const [activeTabElement, setActiveTabElement] =
|
||||
React.useState<HTMLElement | null>(null);
|
||||
|
||||
return (
|
||||
<TabsLineContext.Provider value={{ activeTabElement, setActiveTabElement }}>
|
||||
<TabsPrimitive.Root {...props} />
|
||||
</TabsLineContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const TabsLineList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { activeTabElement } = useTabsLine();
|
||||
const listRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<TabsPrimitive.List
|
||||
ref={(node) => {
|
||||
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 && (
|
||||
<div
|
||||
className="transition-left transition-right absolute bottom-0 h-0.5 bg-purple-600 duration-200 ease-in-out"
|
||||
style={{
|
||||
left: activeTabElement.offsetLeft,
|
||||
width: activeTabElement.offsetWidth,
|
||||
willChange: "left, width",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
TabsLineList.displayName = "TabsLineList";
|
||||
|
||||
const TabsLineTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const elementRef = React.useRef<HTMLButtonElement>(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 (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={(node) => {
|
||||
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<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-stone-400 focus-visible:ring-offset-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsLineContent.displayName = "TabsLineContent";
|
||||
|
||||
export { TabsLine, TabsLineContent, TabsLineList, TabsLineTrigger };
|
||||
Reference in New Issue
Block a user