This commit is contained in:
DCCONSTRUCTIONS
2026-04-18 18:39:25 +03:00
commit 3ba092b60c
4944 changed files with 497564 additions and 0 deletions
@@ -0,0 +1,7 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
export * from "./tabs";
@@ -0,0 +1,362 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Settings, User, Bell } from "lucide-react";
import { HomeIcon } from "../icons/workspace/home-icon";
import { Tabs } from "./tabs";
type TabOption = {
label: string;
value: string;
};
const tabOptions: TabOption[] = [
{ label: "Account", value: "account" },
{ label: "Password", value: "password" },
{ label: "Notifications", value: "notifications" },
];
// cannot use satisfies here because base-ui does not have portable types.
const meta: Meta<typeof Tabs> = {
title: "Components/Tabs",
component: Tabs,
subcomponents: {
TabsList: Tabs.List,
TabsTrigger: Tabs.Trigger,
TabsContent: Tabs.Content,
TabsIndicator: Tabs.Indicator,
},
parameters: {
layout: "centered",
},
args: {
defaultValue: "account",
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Basic: Story = {
render({ defaultValue }) {
return (
<div className="w-[400px]">
<Tabs defaultValue={defaultValue}>
<Tabs.List>
{tabOptions.map((option) => (
<Tabs.Trigger key={option.value} value={option.value}>
{option.label}
</Tabs.Trigger>
))}
<Tabs.Indicator />
</Tabs.List>
{tabOptions.map((option) => (
<Tabs.Content key={option.value} value={option.value} className="p-4">
<div className="text-13">
<h3 className="mb-2 font-medium">{option.label}</h3>
<p className="text-tertiary">Content for the {option.label.toLowerCase()} tab.</p>
</div>
</Tabs.Content>
))}
</Tabs>
</div>
);
},
};
export const Sizes: Story = {
render({ defaultValue }) {
const sizes = ["sm", "md", "lg"] as const;
const sizeLabels: Record<(typeof sizes)[number], string> = {
sm: "Small",
md: "Medium",
lg: "Large",
};
return (
<div className="grid w-[400px] gap-4">
{sizes.map((size) => (
<div key={size} className="flex flex-col gap-2">
<div className="text-13 font-medium">{sizeLabels[size]}</div>
<Tabs defaultValue={defaultValue}>
<Tabs.List>
{tabOptions.map((option) => (
<Tabs.Trigger key={option.value} value={option.value} size={size}>
{option.label}
</Tabs.Trigger>
))}
<Tabs.Indicator />
</Tabs.List>
</Tabs>
</div>
))}
</div>
);
},
};
export const Controlled: Story = {
render() {
const [activeTab, setActiveTab] = useState("account");
return (
<div className="w-[400px]">
<div className="mb-4 text-13">
Active tab: <span className="font-medium">{activeTab}</span>
</div>
<Tabs value={activeTab} onValueChange={(value) => value && setActiveTab(value)}>
<Tabs.List>
{tabOptions.map((option) => (
<Tabs.Trigger key={option.value} value={option.value}>
{option.label}
</Tabs.Trigger>
))}
<Tabs.Indicator />
</Tabs.List>
{tabOptions.map((option) => (
<Tabs.Content key={option.value} value={option.value} className="p-4">
<div className="text-13">Content for {option.label}</div>
</Tabs.Content>
))}
</Tabs>
</div>
);
},
};
export const DisabledTab: Story = {
render({ defaultValue }) {
return (
<div className="w-[400px]">
<Tabs defaultValue={defaultValue}>
<Tabs.List>
<Tabs.Trigger value="account">Account</Tabs.Trigger>
<Tabs.Trigger value="password" disabled>
Password
</Tabs.Trigger>
<Tabs.Trigger value="notifications">Notifications</Tabs.Trigger>
<Tabs.Indicator />
</Tabs.List>
<Tabs.Content value="account" className="p-4">
<div className="text-13">Account content</div>
</Tabs.Content>
<Tabs.Content value="password" className="p-4">
<div className="text-13">Password content (disabled)</div>
</Tabs.Content>
<Tabs.Content value="notifications" className="p-4">
<div className="text-13">Notifications content</div>
</Tabs.Content>
</Tabs>
</div>
);
},
};
export const WithIcons: Story = {
render({ defaultValue }) {
const tabsWithIcons = [
{ label: "Home", value: "home", icon: HomeIcon },
{ label: "Profile", value: "profile", icon: User },
{ label: "Settings", value: "settings", icon: Settings },
{ label: "Notifications", value: "notifications", icon: Bell },
];
return (
<div className="w-[500px]">
<Tabs defaultValue={defaultValue || "home"}>
<Tabs.List>
{tabsWithIcons.map((tab) => (
<Tabs.Trigger key={tab.value} value={tab.value}>
<tab.icon className="mr-2 h-4 w-4" />
{tab.label}
</Tabs.Trigger>
))}
<Tabs.Indicator />
</Tabs.List>
{tabsWithIcons.map((tab) => (
<Tabs.Content key={tab.value} value={tab.value} className="p-4">
<div className="text-13">Content for {tab.label}</div>
</Tabs.Content>
))}
</Tabs>
</div>
);
},
};
export const IconsOnly: Story = {
render({ defaultValue }) {
const iconTabs = [
{ value: "home", icon: HomeIcon },
{ value: "profile", icon: User },
{ value: "settings", icon: Settings },
{ value: "notifications", icon: Bell },
];
return (
<div className="w-[300px]">
<Tabs defaultValue={defaultValue || "home"}>
<Tabs.List>
{iconTabs.map((tab) => (
<Tabs.Trigger key={tab.value} value={tab.value}>
<tab.icon className="h-4 w-4" />
</Tabs.Trigger>
))}
<Tabs.Indicator />
</Tabs.List>
{iconTabs.map((tab) => (
<Tabs.Content key={tab.value} value={tab.value} className="p-4">
<div className="text-13">Content for {tab.value}</div>
</Tabs.Content>
))}
</Tabs>
</div>
);
},
};
export const DynamicTabs: Story = {
render() {
const [tabs, setTabs] = useState([
{ label: "Tab 1", value: "tab1" },
{ label: "Tab 2", value: "tab2" },
]);
const [activeTab, setActiveTab] = useState("tab1");
const addTab = () => {
const newTabNum = tabs.length + 1;
setTabs([...tabs, { label: `Tab ${newTabNum}`, value: `tab${newTabNum}` }]);
};
const removeTab = (valueToRemove: string) => {
const newTabs = tabs.filter((tab) => tab.value !== valueToRemove);
setTabs(newTabs);
if (activeTab === valueToRemove && newTabs.length > 0) {
setActiveTab(newTabs[0].value);
}
};
return (
<div className="w-[500px]">
<div className="mb-4">
<button onClick={addTab} className="rounded-sm bg-layer-1 px-3 py-1.5 text-13 hover:bg-surface-2">
Add Tab
</button>
</div>
<Tabs value={activeTab} onValueChange={(value) => value && setActiveTab(value)}>
<Tabs.List>
{tabs.map((tab) => (
<Tabs.Trigger key={tab.value} value={tab.value}>
{tab.label}
{tabs.length > 1 && (
<button
onClick={(e) => {
e.stopPropagation();
removeTab(tab.value);
}}
className="ml-2 hover:text-danger-primary"
>
×
</button>
)}
</Tabs.Trigger>
))}
<Tabs.Indicator />
</Tabs.List>
{tabs.map((tab) => (
<Tabs.Content key={tab.value} value={tab.value} className="p-4">
<div className="text-13">Content for {tab.label}</div>
</Tabs.Content>
))}
</Tabs>
</div>
);
},
};
export const FullWidth: Story = {
render({ defaultValue }) {
return (
<div className="w-full max-w-2xl">
<Tabs defaultValue={defaultValue}>
<Tabs.List>
<Tabs.Trigger value="account" className="flex-1">
Account
</Tabs.Trigger>
<Tabs.Trigger value="password" className="flex-1">
Password
</Tabs.Trigger>
<Tabs.Trigger value="notifications" className="flex-1">
Notifications
</Tabs.Trigger>
<Tabs.Indicator />
</Tabs.List>
{tabOptions.map((option) => (
<Tabs.Content key={option.value} value={option.value} className="p-4">
<div className="text-13">Content for {option.label}</div>
</Tabs.Content>
))}
</Tabs>
</div>
);
},
};
export const WithComplexContent: Story = {
render({ defaultValue }) {
return (
<div className="w-[600px]">
<Tabs defaultValue={defaultValue}>
<Tabs.List>
{tabOptions.map((option) => (
<Tabs.Trigger key={option.value} value={option.value}>
{option.label}
</Tabs.Trigger>
))}
<Tabs.Indicator />
</Tabs.List>
<Tabs.Content value="account" className="p-4">
<div className="space-y-4">
<div>
<label className="text-13 font-medium">Username</label>
<input type="text" className="mt-1 w-full rounded-sm bg-layer-1 px-3 py-2" />
</div>
<div>
<label className="text-13 font-medium">Email</label>
<input type="email" className="mt-1 w-full rounded-sm bg-layer-1 px-3 py-2" />
</div>
</div>
</Tabs.Content>
<Tabs.Content value="password" className="p-4">
<div className="space-y-4">
<div>
<label className="text-13 font-medium">Current Password</label>
<input type="password" className="mt-1 w-full rounded-sm bg-layer-1 px-3 py-2" />
</div>
<div>
<label className="text-13 font-medium">New Password</label>
<input type="password" className="mt-1 w-full rounded-sm bg-layer-1 px-3 py-2" />
</div>
</div>
</Tabs.Content>
<Tabs.Content value="notifications" className="p-4">
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-13">Email notifications</span>
<input type="checkbox" />
</div>
<div className="flex items-center justify-between">
<span className="text-13">Push notifications</span>
<input type="checkbox" />
</div>
</div>
</Tabs.Content>
</Tabs>
</div>
);
},
};
+148
View File
@@ -0,0 +1,148 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import * as React from "react";
import { Tabs as TabsPrimitive } from "@base-ui-components/react/tabs";
import { cn } from "../utils/classname";
type TabsVariant = "contained";
type TabsContextType = {
variant?: TabsVariant;
};
const TabsContext = React.createContext<TabsContextType | undefined>(undefined);
type TabsCompound = React.ForwardRefExoticComponent<
React.ComponentProps<typeof TabsPrimitive.Root> & {
variant?: TabsVariant;
} & React.RefAttributes<React.ElementRef<typeof TabsPrimitive.Root>>
> & {
List: React.ForwardRefExoticComponent<
React.ComponentProps<typeof TabsPrimitive.List> & {
background?: TabsVariant;
} & React.RefAttributes<React.ElementRef<typeof TabsPrimitive.List>>
>;
Trigger: React.ForwardRefExoticComponent<
React.ComponentProps<typeof TabsPrimitive.Tab> & {
size?: "sm" | "md" | "lg";
variant?: TabsVariant;
} & React.RefAttributes<React.ElementRef<typeof TabsPrimitive.Tab>>
>;
Content: React.ForwardRefExoticComponent<
React.ComponentProps<typeof TabsPrimitive.Panel> & React.RefAttributes<React.ElementRef<typeof TabsPrimitive.Panel>>
>;
Indicator: React.ForwardRefExoticComponent<React.ComponentProps<"div"> & React.RefAttributes<HTMLDivElement>>;
};
const TabsRoot = React.forwardRef(function TabsRoot(
{ className, variant, ...props }: React.ComponentProps<typeof TabsPrimitive.Root> & { variant?: TabsVariant },
ref: React.ForwardedRef<React.ElementRef<typeof TabsPrimitive.Root>>
) {
return (
<TabsContext.Provider value={{ variant }}>
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex h-full w-full flex-col", className)}
{...props}
ref={ref}
/>
</TabsContext.Provider>
);
});
const TabsList = React.forwardRef(function TabsList(
{
className,
background = "contained",
...props
}: React.ComponentProps<typeof TabsPrimitive.List> & {
background?: TabsVariant;
},
ref: React.ForwardedRef<React.ElementRef<typeof TabsPrimitive.List>>
) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"relative flex w-full items-center justify-between gap-1.5 overflow-auto rounded-lg p-0.5 text-13",
{
"bg-layer-3": background === "contained",
},
className
)}
{...props}
ref={ref}
/>
);
});
const TabsTrigger = React.forwardRef(function TabsTrigger(
{
className,
size = "md",
...props
}: React.ComponentProps<typeof TabsPrimitive.Tab> & { size?: "sm" | "md" | "lg"; variant?: TabsVariant },
ref: React.ForwardedRef<React.ElementRef<typeof TabsPrimitive.Tab>>
) {
return (
<TabsPrimitive.Tab
data-slot="tabs-trigger"
className={cn(
"flex w-full min-w-fit cursor-pointer items-center justify-center rounded-md border border-transparent p-1 font-medium text-primary transition-all duration-200 ease-in-out outline-none focus:outline-none",
"data-[selected]:shadow-sm data-[selected]:raised-200 data-[selected]:border data-[selected]:border-subtle-1 data-[selected]:bg-layer-2 data-[selected]:text-primary",
"text-placeholder hover:bg-layer-transparent-hover hover:text-tertiary",
"disabled:cursor-not-allowed disabled:text-placeholder",
{
"text-11": size === "sm",
"text-13": size === "md",
"text-14": size === "lg",
},
className
)}
{...props}
ref={ref}
/>
);
});
const TabsContent = React.forwardRef(function TabsContent(
{ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Panel>,
ref: React.ForwardedRef<React.ElementRef<typeof TabsPrimitive.Panel>>
) {
return (
<TabsPrimitive.Panel
data-slot="tabs-content"
className={cn("relative outline-none", className)}
{...props}
ref={ref}
/>
);
});
const TabsIndicator = React.forwardRef(function TabsIndicator(
{ className, ...props }: React.ComponentProps<"div">,
ref: React.ForwardedRef<HTMLDivElement>
) {
return (
<div
className={cn(
"shadow-sm absolute top-[50%] left-0 z-[-1] h-6 w-[var(--active-tab-width)] translate-x-[var(--active-tab-left)] -translate-y-[50%] rounded-xs bg-surface-1 transition-[width,transform] duration-200 ease-in-out",
className
)}
{...props}
ref={ref}
/>
);
});
export const Tabs = Object.assign(TabsRoot, {
List: TabsList,
Trigger: TabsTrigger,
Content: TabsContent,
Indicator: TabsIndicator,
}) satisfies TabsCompound;
export { TabsList, TabsTrigger, TabsContent, TabsIndicator };