diff --git a/packages/ui/package.json b/packages/ui/package.json index 048ef5e389..99781d8fc5 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -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", diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index db07069d40..30777112fd 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -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' diff --git a/packages/ui/src/components/primitives/tabs.tsx b/packages/ui/src/components/primitives/tabs.tsx new file mode 100644 index 0000000000..051de1dfb2 --- /dev/null +++ b/packages/ui/src/components/primitives/tabs.tsx @@ -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 & { + variant?: 'default' | 'line' +}) { + return ( + + + + ) +} + +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) { + const { variant, orientation } = React.use(TabsContext) + return ( + + ) +} + +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) { + const { variant, orientation } = React.use(TabsContext) + return ( + + ) +} + +function TabsContent({ className, ...props }: React.ComponentProps) { + return +} + +export { Tabs, TabsContent, TabsList, TabsTrigger } diff --git a/packages/ui/stories/components/primitives/Tabs.stories.tsx b/packages/ui/stories/components/primitives/Tabs.stories.tsx new file mode 100644 index 0000000000..8c0ccdad83 --- /dev/null +++ b/packages/ui/stories/components/primitives/Tabs.stories.tsx @@ -0,0 +1,169 @@ +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@cherrystudio/ui' +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { + 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 + +// Default (Segmented Control Style) +export const Default: Story = { + render: () => ( + + + Account + Password + + +
+

Account

+

+ Make changes to your account here. Click save when you're done. +

+
+
+ +
+

Password

+

+ Change your password here. After saving, you'll be logged out. +

+
+
+
+ ) +} + +// Line Style (Figma) +export const LineStyle: Story = { + render: () => ( + + + Tab 1 + Tab 2 + Tab 3 + + +
Content for Tab 1
+
+ +
Content for Tab 2
+
+ +
Content for Tab 3
+
+
+ ) +} + +// Vertical +export const Vertical: Story = { + render: () => ( + + + Music + Podcasts + Live + + + Music content + + + Podcasts content + + + Live content + + + ) +} + +// With Icons +export const WithIcons: Story = { + render: () => ( + + + + + + + + Home + + + + + + + Settings + + + + Home Content + + + Settings Content + + + ) +} diff --git a/yarn.lock b/yarn.lock index 95dbb2a79d..66dfdbe45b 100644 --- a/yarn.lock +++ b/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"