) => (
+ | [role=checkbox]]:translate-y-[2px]',
+ '[&:first-child]:pl-5 [&:last-child]:pr-5',
+ className,
+ )}
+ {...props}
+ />
+)
+
+export const Caption = ({
+ className,
+ ...props
+}: HTMLAttributes) => (
+
+)
diff --git a/src/shared/ui/tabs/index.ts b/src/shared/ui/tabs/index.ts
index 55f323c5..2c149557 100644
--- a/src/shared/ui/tabs/index.ts
+++ b/src/shared/ui/tabs/index.ts
@@ -1 +1 @@
-export { Tabs } from './tabs'
+export * as Tabs from './tabs'
diff --git a/src/shared/ui/tabs/tabs.stories.tsx b/src/shared/ui/tabs/tabs.stories.tsx
index d0cac250..ed8519a3 100644
--- a/src/shared/ui/tabs/tabs.stories.tsx
+++ b/src/shared/ui/tabs/tabs.stories.tsx
@@ -1,52 +1,83 @@
import type { Meta, StoryObj } from '@storybook/react'
+import { ComponentProps } from 'react'
-import { Tabs, TabsProps } from './tabs'
+import * as Tabs from './tabs'
+
+const TabsStory = (props: ComponentProps) => {
+ return (
+
+
+
+ Tab A
+ Tab B
+ Tab C
+
+ Tab A
+ Tab B
+ Tab C
+
+
+ )
+}
const meta = {
title: 'Component/Tabs',
- component: Tabs,
+ component: Tabs.List,
parameters: {
componentSubtitle:
'Tabs allow users to navigate between different sections of content.',
},
argTypes: {
- defaultValue: {
- control: 'text',
- description: 'Set the default value of the Tabs.',
+ variant: {
+ control: 'select',
+ options: ['box', 'underline'],
+ description: 'Set the variant of the Tabs.',
},
- value: {
- control: 'text',
- description: 'Set the value of the Tabs.',
+ size: {
+ control: 'select',
+ options: ['small', 'large'],
+ description: 'Set the size of the Tabs.',
},
- onChange: {
- description:
- 'Callback function that is triggered when the value changes.',
- table: {
- type: { summary: 'function' },
- },
+ width: {
+ control: 'select',
+ options: ['full', 'fit'],
+ description: 'Set the width of the Tabs.',
},
},
args: {
- defaultValue: 'one',
- value: '',
- onChange: () => {},
+ variant: 'box',
+ size: 'small',
+ width: 'fit',
},
-} satisfies Meta
+ render: TabsStory,
+} satisfies Meta
export default meta
type Story = StoryObj
-const TabsStory = (props: TabsProps) => {
- return (
-
- Tab A
- Tab B
- Tab C
-
- )
+export const Small: Story = {
+ args: {
+ size: 'small',
+ },
}
-export const Default: Story = {
- render: TabsStory,
+export const Large: Story = {
+ args: {
+ size: 'large',
+ },
+}
+
+export const Underline: Story = {
+ args: {
+ variant: 'underline',
+ size: 'small',
+ },
+}
+
+export const UnderlineLarge: Story = {
+ args: {
+ variant: 'underline',
+ size: 'large',
+ },
}
diff --git a/src/shared/ui/tabs/tabs.tsx b/src/shared/ui/tabs/tabs.tsx
index 754484b8..719e4d15 100644
--- a/src/shared/ui/tabs/tabs.tsx
+++ b/src/shared/ui/tabs/tabs.tsx
@@ -1,96 +1,174 @@
'use client'
-import { isUndefined } from 'lodash-es'
+import * as TabsPrimitive from '@radix-ui/react-tabs'
+import { cva, VariantProps } from 'class-variance-authority'
import {
- PropsWithChildren,
- useCallback,
- useEffect,
- useMemo,
- useState,
- MouseEvent,
+ ComponentProps,
+ ComponentPropsWithoutRef,
+ createContext,
+ ElementRef,
+ forwardRef,
+ useContext,
} from 'react'
-import { cn } from 'shared/lib'
+import { cn, useFixWidth } from 'shared/lib'
-import { TabsContext, useTabsContext } from './context'
-import * as css from './variants'
+const groupVariants = cva('group/toggle-group flex bg-white', {
+ variants: {
+ variant: {
+ box: '',
+ underline: 'border-b border-gray-200',
+ },
+ size: {
+ small: '',
+ large: '',
+ },
+ },
+})
-export type TabsProps = {
- defaultValue?: string
- value?: string
- onChange?: (value: string) => void
- className?: string
-}
+const itemVariants = cva(
+ [
+ 'flex items-center justify-center font-medium transition-all',
+ 'font-medium text-gray-700 focus-visible:ring-blue-100 [&_svg]:text-gray-500',
+ '[&_span]:inline-block [&_span]:w-fit',
+ '[&_svg]:size-4',
+ ],
+ {
+ variants: {
+ variant: {
+ box: [
+ 'border',
+ '[&:first-child]:rounded-r-none [&:last-child]:rounded-l-none [&:not(:first-child)]:-ml-[1px] [&:not(:first-child)]:border-l-transparent [&:nth-child(n+2):nth-last-child(n+2)]:rounded-none',
+ 'hover:bg-gray-100',
+ 'disabled:text-gray-200 disabled:[&_svg]:text-gray-200',
+ 'data-[state=active]:text-blue-800',
+ 'focus-visible:border-2 focus-visible:!border-blue-800',
+ ],
+ underline: [
+ 'relative rounded-sm text-gray-500',
+ 'hover:text-gray-700',
+ 'data-[state=active]:after:content-[" "] data-[state=active]:text-blue-800 data-[state=active]:after:absolute data-[state=active]:after:-bottom-px data-[state=active]:after:left-0 data-[state=active]:after:right-0 data-[state=active]:after:h-0.5 data-[state=active]:after:bg-blue-800 data-[state=active]:after:transition-all',
+ 'focus-visible:ring-2 focus-visible:ring-blue-800 data-[state=active]:focus-visible:after:bg-[rgba(0,0,0,0)]',
+ ],
+ },
+ size: {
+ small: 'body-large',
+ large: 'title-small',
+ },
+ disabled: {
+ true: 'cursor-not-allowed',
+ false: 'cursor-pointer hover:bg-gray-50/50 active:font-bold',
+ },
+ },
-const TabsRoot = ({
- defaultValue = '',
- value,
- onChange,
- className,
- children,
-}: PropsWithChildren) => {
- const [activatedTab, setActivatedTab] = useState(defaultValue)
+ compoundVariants: [
+ {
+ variant: 'underline',
+ size: 'small',
+ className: 'h-9 px-2.5 py-2',
+ },
+ {
+ variant: 'underline',
+ size: 'large',
+ className: 'h-12 p-3',
+ },
+ {
+ variant: 'box',
+ size: 'small',
+ className: 'h-10 w-[120px] rounded-[10px] p-[10px]',
+ },
+ {
+ variant: 'box',
+ size: 'large',
+ className: 'h-12 w-[146px] rounded-[12px] p-[12px]',
+ },
+ ],
+ },
+)
- const handleTabChange = useCallback(
- (updatedValue: string) => {
- setActivatedTab(updatedValue)
- onChange?.(updatedValue)
- },
- [onChange],
- )
+const ToggleGroupContext = createContext<
+ VariantProps & {
+ width?: 'full' | 'fit'
+ }
+>({
+ size: 'large',
+ variant: 'box',
+ width: 'full',
+})
- useEffect(() => {
- if (isUndefined(value)) return
- setActivatedTab(value)
- }, [value])
+// eslint-disable-next-line prefer-destructuring
+export const Root = TabsPrimitive.Root
- const contextValues = useMemo(
- () => ({
- activatedTab,
- handleTabChange,
- }),
- [activatedTab, handleTabChange],
- )
+type ListProps = ComponentProps &
+ VariantProps & {
+ width?: 'full' | 'fit'
+ }
- return (
-
-
- {children}
-
-
- )
-}
+export const List = ({
+ variant = 'box',
+ size = 'large',
+ width,
+ className,
+ ref,
+ children,
+ ...props
+}: ListProps) => (
+
+
+ {children}
+
+
+)
-type TabTriggerProps = {
- className?: string
- value: string
- onChange?: string
-}
+type TriggerProps = ComponentProps &
+ VariantProps
-const Trigger = ({
- value,
+export const Trigger = ({
+ disabled,
className,
children,
-}: PropsWithChildren) => {
- const { activatedTab, handleTabChange } = useTabsContext()
+ ref,
+ ...props
+}: TriggerProps) => {
+ const { width: widthState, variant, size } = useContext(ToggleGroupContext)
- const isActive = activatedTab === value
+ const { width: widthFromChildren, childrenRef } =
+ useFixWidth(children)
- const handleClick = (event: MouseEvent) => {
- event.stopPropagation()
- handleTabChange(value)
- }
+ const width = widthState === 'full' ? null : widthFromChildren
return (
-
+
+ {children}
+
+
)
}
-export const Tabs = Object.assign(TabsRoot, {
- Trigger,
-})
+export const Content = forwardRef<
+ ElementRef,
+ ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Content.displayName = TabsPrimitive.Content.displayName
|