diff --git a/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts b/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts new file mode 100644 index 00000000..c5ec7d8e --- /dev/null +++ b/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts @@ -0,0 +1,53 @@ +import { tv, type VariantProps } from 'tailwind-variants'; + +export type TabsVariants = VariantProps; + +export const tabsTheme = tv({ + slots: { + root: ['flex', 'flex-col', 'w-full'], + triggerWrapper: [ + 'px-1', + 'pb-5', + 'text-elements-low-em', + 'border-b-2', + 'border-transparent', + 'hover:border-border-interactive/10', + 'hover:text-elements-mid-em', + 'focus-within:border-border-interactive/10', + 'data-[state=active]:font-medium', + 'data-[state=active]:text-elements-high-em', + 'data-[state=active]:border-elements-high-em', + ], + trigger: [ + 'flex', + 'gap-1.5', + 'cursor-default', + 'select-none', + 'items-center', + 'justify-center', + 'outline-none', + 'leading-none', + 'tracking-[-0.006em]', + 'rounded-md', + 'focus-ring', + ], + triggerList: [ + 'flex', + 'shrink-0', + 'gap-5', + 'border-b', + 'border-border-interactive/10', + ], + content: ['text-elements-high-em', 'grow', 'outline-none', 'tab-content'], + }, + variants: { + fillWidth: { + true: { + trigger: ['flex-1'], + }, + }, + }, + defaultVariants: { + fillWidth: false, + }, +}); diff --git a/packages/frontend/src/components/shared/Tabs/Tabs.tsx b/packages/frontend/src/components/shared/Tabs/Tabs.tsx new file mode 100644 index 00000000..1a0eb7f2 --- /dev/null +++ b/packages/frontend/src/components/shared/Tabs/Tabs.tsx @@ -0,0 +1,42 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; +import { Root as TabsRoot } from '@radix-ui/react-tabs'; + +import { tabsTheme } from './Tabs.theme'; +import TabsContent from './TabsContent'; +import TabsList from './TabsList'; +import TabsTrigger from './TabsTrigger'; +import TabsProvider, { TabsProviderProps } from './TabsProvider'; + +export interface TabsProps extends ComponentPropsWithoutRef { + /** + * The configuration for the tabs component. + */ + config?: TabsProviderProps; +} + +/** + * A component that allows users to switch between different tabs. + * @returns JSX element representing the tabs component. + */ +export const Tabs = ({ config, className, ...props }: TabsProps) => { + const { root } = tabsTheme(config); + + return ( + + + + ); +}; + +/** + * Assigns the TabsTrigger class to the Trigger property of the Tabs object. + */ +Tabs.Trigger = TabsTrigger; +/** + * Assigns the TabsList object to the List property of the Tabs object. + */ +Tabs.List = TabsList; +/** + * Assigns the TabsContent component to the Content property of the Tabs component. + */ +Tabs.Content = TabsContent; diff --git a/packages/frontend/src/components/shared/Tabs/TabsContent/TabsContent.tsx b/packages/frontend/src/components/shared/Tabs/TabsContent/TabsContent.tsx new file mode 100644 index 00000000..21200991 --- /dev/null +++ b/packages/frontend/src/components/shared/Tabs/TabsContent/TabsContent.tsx @@ -0,0 +1,26 @@ +import React, { + forwardRef, + type ComponentPropsWithoutRef, + type ElementRef, +} from 'react'; +import { Content } from '@radix-ui/react-tabs'; + +import { tabsTheme } from '../Tabs.theme'; + +export interface TabsContentProps + extends ComponentPropsWithoutRef {} + +/** + * A component that represents the content of the tabs component. + */ +const TabsContent = forwardRef, TabsContentProps>( + ({ className, ...props }, ref) => { + const { content } = tabsTheme(); + return ; + }, +); + +// Assigns the display name to the TabsContent component. +TabsContent.displayName = 'TabsContent'; + +export { TabsContent }; diff --git a/packages/frontend/src/components/shared/Tabs/TabsContent/index.ts b/packages/frontend/src/components/shared/Tabs/TabsContent/index.ts new file mode 100644 index 00000000..0e80ffbd --- /dev/null +++ b/packages/frontend/src/components/shared/Tabs/TabsContent/index.ts @@ -0,0 +1,3 @@ +import { TabsContent } from './TabsContent'; + +export default TabsContent; diff --git a/packages/frontend/src/components/shared/Tabs/TabsList/TabsList.tsx b/packages/frontend/src/components/shared/Tabs/TabsList/TabsList.tsx new file mode 100644 index 00000000..f5eb1498 --- /dev/null +++ b/packages/frontend/src/components/shared/Tabs/TabsList/TabsList.tsx @@ -0,0 +1,25 @@ +import React, { + forwardRef, + type ComponentPropsWithoutRef, + type ElementRef, +} from 'react'; +import { List } from '@radix-ui/react-tabs'; + +import { tabsTheme } from 'components/shared/Tabs/Tabs.theme'; + +export interface TabsListProps extends ComponentPropsWithoutRef {} + +/** + * A component that represents the list of tabs. + */ +const TabsList = forwardRef, TabsListProps>( + ({ className, ...props }, ref) => { + const { triggerList } = tabsTheme({ className }); + return ; + }, +); + +// Assigns the display name to the TabsList component. +TabsList.displayName = 'TabsList'; + +export { TabsList }; diff --git a/packages/frontend/src/components/shared/Tabs/TabsList/index.ts b/packages/frontend/src/components/shared/Tabs/TabsList/index.ts new file mode 100644 index 00000000..f02fd4e5 --- /dev/null +++ b/packages/frontend/src/components/shared/Tabs/TabsList/index.ts @@ -0,0 +1,3 @@ +import { TabsList } from './TabsList'; + +export default TabsList; diff --git a/packages/frontend/src/components/shared/Tabs/TabsProvider.tsx b/packages/frontend/src/components/shared/Tabs/TabsProvider.tsx new file mode 100644 index 00000000..42fb1ea1 --- /dev/null +++ b/packages/frontend/src/components/shared/Tabs/TabsProvider.tsx @@ -0,0 +1,43 @@ +import React, { + createContext, + useContext, + type PropsWithChildren, +} from 'react'; +import { TabsVariants } from './Tabs.theme'; + +export interface TabsProviderProps extends Partial {} + +type TabsProviderContext = ReturnType; + +const TabsContext = createContext>({}); + +// For inferring return type +const useTabsValues = (props: TabsProviderProps) => { + return props; +}; + +/** + * A provider component that allows users to switch between different tabs. + * @returns JSX element representing the tabs provider component. + */ +export const TabsProvider = ({ + children, + ...props +}: PropsWithChildren): JSX.Element => { + const values = useTabsValues(props); + return {children}; +}; + +/** + * A hook that returns the context of the tabs provider. + * @returns The context of the tabs provider. + */ +export const useTabs = () => { + const context = useContext(TabsContext); + if (context === undefined) { + throw new Error('useTabs was used outside of its Provider'); + } + return context; +}; + +export default TabsProvider; diff --git a/packages/frontend/src/components/shared/Tabs/TabsTrigger/TabsTrigger.tsx b/packages/frontend/src/components/shared/Tabs/TabsTrigger/TabsTrigger.tsx new file mode 100644 index 00000000..be7d3bfb --- /dev/null +++ b/packages/frontend/src/components/shared/Tabs/TabsTrigger/TabsTrigger.tsx @@ -0,0 +1,49 @@ +import React, { + forwardRef, + type ComponentPropsWithoutRef, + type ElementRef, + type PropsWithChildren, + type ReactNode, +} from 'react'; +import { Trigger } from '@radix-ui/react-tabs'; + +import { tabsTheme } from 'components/shared/Tabs/Tabs.theme'; +import { useTabs } from 'components/shared/Tabs/TabsProvider'; + +export interface TabsTriggerProps + extends ComponentPropsWithoutRef { + /** + * The icon to display in the trigger. + */ + icon?: ReactNode; +} + +/** + * A component that represents the trigger for the tabs component. + */ +const TabsTrigger = forwardRef< + ElementRef, + PropsWithChildren +>(({ className, icon, children, ...props }, ref) => { + const config = useTabs(); + const { triggerWrapper, trigger } = tabsTheme(config); + + return ( + + {/* Need to add button in the trigger children because there's focus state inside the children */} + + + ); +}); + +TabsTrigger.displayName = 'TabsTrigger'; + +export { TabsTrigger }; diff --git a/packages/frontend/src/components/shared/Tabs/TabsTrigger/index.ts b/packages/frontend/src/components/shared/Tabs/TabsTrigger/index.ts new file mode 100644 index 00000000..57b0b3e6 --- /dev/null +++ b/packages/frontend/src/components/shared/Tabs/TabsTrigger/index.ts @@ -0,0 +1,3 @@ +import { TabsTrigger } from './TabsTrigger'; + +export default TabsTrigger; diff --git a/packages/frontend/src/components/shared/Tabs/index.ts b/packages/frontend/src/components/shared/Tabs/index.ts new file mode 100644 index 00000000..856dbbb3 --- /dev/null +++ b/packages/frontend/src/components/shared/Tabs/index.ts @@ -0,0 +1 @@ +export * from './Tabs';