feat: add Tabs component and related subcomponents

- Introduced a new Tabs component along with TabsList, TabsTrigger, and TabsContent for improved content organization.
- Updated package.json and yarn.lock to include @radix-ui/react-tabs dependency.
- Enhanced index.ts to export the new Tabs components for easier access in the UI library.
- Created stories for the Tabs component in Storybook to demonstrate various usage scenarios.
This commit is contained in:
MyPrototypeWhat 2025-11-21 17:09:23 +08:00
parent 24c9c157f9
commit 53883a27be
5 changed files with 341 additions and 0 deletions

View File

@ -54,6 +54,7 @@
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@radix-ui/react-use-controllable-state": "^1.2.2",
"class-variance-authority": "^0.7.1",

View File

@ -64,3 +64,4 @@ export * from './primitives/popover'
export * from './primitives/radioGroup'
export * from './primitives/select'
export * from './primitives/shadcn-io/dropzone'
export * from './primitives/tabs'

View File

@ -0,0 +1,143 @@
import { cn } from '@cherrystudio/ui/utils/index'
import * as TabsPrimitive from '@radix-ui/react-tabs'
import { cva } from 'class-variance-authority'
import * as React from 'react'
const TabsContext = React.createContext<{
variant?: 'default' | 'line'
orientation?: 'horizontal' | 'vertical'
}>({
variant: 'default',
orientation: 'horizontal'
})
function Tabs({
className,
variant = 'default',
orientation = 'horizontal',
...props
}: React.ComponentProps<typeof TabsPrimitive.Root> & {
variant?: 'default' | 'line'
}) {
return (
<TabsContext value={{ variant, orientation }}>
<TabsPrimitive.Root
data-slot="tabs"
orientation={orientation}
className={cn('flex flex-col gap-2', orientation === 'vertical' && 'flex-row', className)}
{...props}
/>
</TabsContext>
)
}
const tabsListVariants = cva('inline-flex items-center justify-center', {
variants: {
variant: {
default: 'bg-muted text-muted-foreground h-9 w-fit rounded-lg p-[3px]',
line: 'bg-transparent gap-4 justify-start border-b-0 p-0'
},
orientation: {
horizontal: 'flex-row',
vertical: 'flex-col h-fit'
}
},
compoundVariants: [
{
variant: 'default',
orientation: 'vertical',
class: 'h-fit w-fit flex-col'
},
{
variant: 'line',
orientation: 'vertical',
class: 'flex-col items-stretch pb-0'
}
],
defaultVariants: {
variant: 'default',
orientation: 'horizontal'
}
})
function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
const { variant, orientation } = React.use(TabsContext)
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(tabsListVariants({ variant, orientation }), className)}
{...props}
/>
)
}
const tabsTriggerVariants = cva(
[
'inline-flex items-center justify-center whitespace-nowrap text-sm font-medium',
'disabled:pointer-events-none disabled:opacity-50',
'transition-all',
'[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*="size-"])]:size-4'
],
{
variants: {
variant: {
default: [
'h-[calc(100%-1px)] flex-1 gap-1.5 px-2 py-1 rounded-md',
'text-foreground border border-transparent',
'dark:text-muted-foreground',
'focus-visible:ring-[3px] focus-visible:outline-1 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring',
'data-[state=active]:bg-background data-[state=active]:shadow-sm',
'dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30'
],
line: [
'relative gap-2 px-2 py-2',
'font-normal text-muted-foreground hover:text-foreground',
'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'data-[state=active]:text-primary',
'after:absolute after:bg-primary/10 after:rounded-full',
'data-[state=active]:after:bg-primary'
]
},
orientation: {
horizontal: '',
vertical: 'rounded-full'
}
},
compoundVariants: [
{
variant: 'line',
orientation: 'horizontal',
class: 'after:bottom-0 after:left-0 after:h-[2px] after:w-full data-[state=active]:after:h-[4px]'
},
{
variant: 'line',
orientation: 'vertical',
class: [
'justify-center after:bottom-0 after:left-0 after:h-[4px] after:w-full after:bg-transparent data-[state=active]:after:bg-primary',
'hover:text-primary hover:bg-primary/10'
]
}
],
defaultVariants: {
variant: 'default',
orientation: 'horizontal'
}
}
)
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
const { variant, orientation } = React.use(TabsContext)
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(tabsTriggerVariants({ variant, orientation }), className)}
{...props}
/>
)
}
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
return <TabsPrimitive.Content data-slot="tabs-content" className={cn('flex-1 outline-none', className)} {...props} />
}
export { Tabs, TabsContent, TabsList, TabsTrigger }

View File

@ -0,0 +1,169 @@
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@cherrystudio/ui'
import type { Meta, StoryObj } from '@storybook/react'
const meta: Meta<typeof Tabs> = {
title: 'Components/Primitives/Tabs',
component: Tabs,
parameters: {
layout: 'centered',
docs: {
description: {
component:
'A set of layered sections of content—known as tab panels—that are displayed one at a time. Based on shadcn/ui.'
}
}
},
tags: ['autodocs'],
argTypes: {
variant: {
control: { type: 'select' },
options: ['default', 'line'],
description: 'The visual style of the tabs'
},
defaultValue: {
control: { type: 'text' },
description: 'The value of the tab that should be active when initially rendered'
},
className: {
control: { type: 'text' },
description: 'Additional CSS classes'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
// Default (Segmented Control Style)
export const Default: Story = {
render: () => (
<Tabs defaultValue="account" className="w-[400px]">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="account">
<div className="rounded-md border p-4 mt-2">
<h3 className="text-lg font-medium">Account</h3>
<p className="text-sm text-muted-foreground">
Make changes to your account here. Click save when you're done.
</p>
</div>
</TabsContent>
<TabsContent value="password">
<div className="rounded-md border p-4 mt-2">
<h3 className="text-lg font-medium">Password</h3>
<p className="text-sm text-muted-foreground">
Change your password here. After saving, you'll be logged out.
</p>
</div>
</TabsContent>
</Tabs>
)
}
// Line Style (Figma)
export const LineStyle: Story = {
render: () => (
<Tabs defaultValue="tab1" variant="line" className="w-[400px]">
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
<TabsTrigger value="tab3">Tab 3</TabsTrigger>
</TabsList>
<TabsContent value="tab1">
<div className="p-4 mt-2 border rounded-md bg-muted/10">Content for Tab 1</div>
</TabsContent>
<TabsContent value="tab2">
<div className="p-4 mt-2 border rounded-md bg-muted/10">Content for Tab 2</div>
</TabsContent>
<TabsContent value="tab3">
<div className="p-4 mt-2 border rounded-md bg-muted/10">Content for Tab 3</div>
</TabsContent>
</Tabs>
)
}
// Vertical
export const Vertical: Story = {
render: () => (
<Tabs
defaultValue="music"
orientation="vertical"
variant="line"
className="w-[400px]"
>
<TabsList className="w-[120px]">
<TabsTrigger value="music">Music</TabsTrigger>
<TabsTrigger value="podcasts">Podcasts</TabsTrigger>
<TabsTrigger value="live">Live</TabsTrigger>
</TabsList>
<TabsContent
value="music"
className="flex-1 p-4 border rounded-md bg-muted/10 mt-0"
>
Music content
</TabsContent>
<TabsContent
value="podcasts"
className="flex-1 p-4 border rounded-md bg-muted/10 mt-0"
>
Podcasts content
</TabsContent>
<TabsContent
value="live"
className="flex-1 p-4 border rounded-md bg-muted/10 mt-0"
>
Live content
</TabsContent>
</Tabs>
)
}
// With Icons
export const WithIcons: Story = {
render: () => (
<Tabs defaultValue="home" className="w-[400px]">
<TabsList>
<TabsTrigger value="home" className="gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
<polyline points="9 22 9 12 15 12 15 22" />
</svg>
Home
</TabsTrigger>
<TabsTrigger value="settings" className="gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.38a2 2 0 0 0-.73-2.73l-.15-.1a2 2 0 0 1-1-1.72v-.51a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
<circle cx="12" cy="12" r="3" />
</svg>
Settings
</TabsTrigger>
</TabsList>
<TabsContent value="home" className="mt-2">
Home Content
</TabsContent>
<TabsContent value="settings" className="mt-2">
Settings Content
</TabsContent>
</Tabs>
)
}

View File

@ -2257,6 +2257,7 @@ __metadata:
"@radix-ui/react-radio-group": "npm:^1.3.8"
"@radix-ui/react-select": "npm:^2.2.6"
"@radix-ui/react-slot": "npm:^1.2.4"
"@radix-ui/react-tabs": "npm:^1.1.13"
"@radix-ui/react-tooltip": "npm:^1.2.8"
"@radix-ui/react-use-controllable-state": "npm:^1.2.2"
"@storybook/addon-docs": "npm:^10.0.5"
@ -7647,6 +7648,32 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-tabs@npm:^1.1.13":
version: 1.1.13
resolution: "@radix-ui/react-tabs@npm:1.1.13"
dependencies:
"@radix-ui/primitive": "npm:1.1.3"
"@radix-ui/react-context": "npm:1.1.2"
"@radix-ui/react-direction": "npm:1.1.1"
"@radix-ui/react-id": "npm:1.1.1"
"@radix-ui/react-presence": "npm:1.1.5"
"@radix-ui/react-primitive": "npm:2.1.3"
"@radix-ui/react-roving-focus": "npm:1.1.11"
"@radix-ui/react-use-controllable-state": "npm:1.2.2"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 10c0/a3c78cd8c30dcb95faf1605a8424a1a71dab121dfa6e9c0019bb30d0f36d882762c925b17596d4977990005a255d8ddc0b7454e4f83337fe557b45570a2d8058
languageName: node
linkType: hard
"@radix-ui/react-tooltip@npm:^1.2.8":
version: 1.2.8
resolution: "@radix-ui/react-tooltip@npm:1.2.8"