mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
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:
parent
24c9c157f9
commit
53883a27be
@ -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",
|
||||
|
||||
@ -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'
|
||||
|
||||
143
packages/ui/src/components/primitives/tabs.tsx
Normal file
143
packages/ui/src/components/primitives/tabs.tsx
Normal 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 }
|
||||
169
packages/ui/stories/components/primitives/Tabs.stories.tsx
Normal file
169
packages/ui/stories/components/primitives/Tabs.stories.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
27
yarn.lock
27
yarn.lock
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user