base
This commit is contained in:
@@ -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>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user