diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 817ac3ce..23316166 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,4 +1,5 @@ import type { Preview } from '@storybook/react'; +import '@/src/styles/global.css'; const preview: Preview = { parameters: { diff --git a/pages/_app.tsx b/pages/_app.tsx index 2d29592a..0256c491 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -2,15 +2,21 @@ import type { AppProps } from 'next/app'; import '@/src/styles/global.css'; +import Layout from '@/src/components/Layout/Layout'; import TanstackQueryProvider from '@/src/components/Providers/TanstackQueryProvider'; import Toast from '@/src/components/Toast/Toast'; +import { pretendard } from '@/src/styles/font'; const App = ({ Component, pageProps }: AppProps) => { return ( - - - - +
+ + + + + + +
); }; diff --git a/public/fonts/PretendardVariable.woff2 b/public/fonts/PretendardVariable.woff2 new file mode 100644 index 00000000..49c54b51 Binary files /dev/null and b/public/fonts/PretendardVariable.woff2 differ diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx new file mode 100644 index 00000000..fd3d41df --- /dev/null +++ b/src/components/Layout/Layout.tsx @@ -0,0 +1,7 @@ +import type { PropsWithChildren } from 'react'; + +const Layout = ({ children }: PropsWithChildren) => { + return <>{children}; +}; + +export default Layout; diff --git a/src/components/Tabs/Tabs.stories.tsx b/src/components/Tabs/Tabs.stories.tsx new file mode 100644 index 00000000..21cd4ee6 --- /dev/null +++ b/src/components/Tabs/Tabs.stories.tsx @@ -0,0 +1,51 @@ +import type { StoryObj } from '@storybook/react'; +import { type Meta } from '@storybook/react'; + +import Tabs from './Tabs'; + +const COMPONENT_DESCRIPTION = ` + - \`\`: 모든 컴포넌트에 대한 컨텍스트와 상태를 제공합니다. + - \`\`: \`\` 컴포넌트들을 위한 Wrapper 컴포넌트입니다. + - \`\`: 선택된 탭에 해당되는 컨텐츠를 표시하기 위한 버튼입니다. + - \`\`: 선택된 탭에 해당되는 컨텐츠를 보여줍니다. +`; + +const meta: Meta = { + title: 'Components/Tabs', + component: Tabs, + tags: ['autodocs'], + argTypes: { + defaultValue: { + description: 'Tabs 컴포넌트의 초기에 활성화될 탭의 기본값', + }, + }, + parameters: { + componentSubtitle: '여러 내용을 한 화면에서 전환하며 볼 수 있는 컴포넌트', + docs: { + description: { + component: COMPONENT_DESCRIPTION, + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + render: () => ( + + + 끄적이는 + 참고하는 + + +
끄적이는 내용
+
+ +
참고하는 내용
+
+
+ ), +}; diff --git a/src/components/Tabs/Tabs.tsx b/src/components/Tabs/Tabs.tsx new file mode 100644 index 00000000..74775106 --- /dev/null +++ b/src/components/Tabs/Tabs.tsx @@ -0,0 +1,41 @@ +import { createContext, type PropsWithChildren, useState } from 'react'; + +import TabsContent from './TabsContent'; +import TabsList from './TabsList'; +import TabsTrigger from './TabsTrigger'; + +interface TabsContextProps { + selectedTab: string; + onSelectTab: (selectedTab: string) => void; +} + +export interface TabsRootProps { + defaultValue: string; +} + +export const TabsContext = createContext(null); + +const TabsRoot = ({ + children, + defaultValue, +}: PropsWithChildren) => { + const [selectedTab, setSelectedTab] = useState(defaultValue); + + const handleTabSelect = (selectedTab: string) => { + setSelectedTab(selectedTab); + }; + + return ( + + {children} + + ); +}; + +const Tabs = Object.assign(TabsRoot, { + List: TabsList, + Trigger: TabsTrigger, + Content: TabsContent, +}); + +export default Tabs; diff --git a/src/components/Tabs/TabsContent.tsx b/src/components/Tabs/TabsContent.tsx new file mode 100644 index 00000000..36377170 --- /dev/null +++ b/src/components/Tabs/TabsContent.tsx @@ -0,0 +1,18 @@ +import type { PropsWithChildren } from 'react'; + +import { useTabsContext } from '../../hooks/useTabsContext'; + +interface TabsContentProps { + value: string; +} + +const TabsContent = ({ + children, + value, +}: PropsWithChildren) => { + const { selectedTab } = useTabsContext(); + + return <>{selectedTab === value ? children : null}; +}; + +export default TabsContent; diff --git a/src/components/Tabs/TabsList.tsx b/src/components/Tabs/TabsList.tsx new file mode 100644 index 00000000..0754b24b --- /dev/null +++ b/src/components/Tabs/TabsList.tsx @@ -0,0 +1,9 @@ +import type { PropsWithChildren } from 'react'; + +import * as styles from './style.css'; + +const TabsList = ({ children }: PropsWithChildren) => { + return
    {children}
; +}; + +export default TabsList; diff --git a/src/components/Tabs/TabsTrigger.tsx b/src/components/Tabs/TabsTrigger.tsx new file mode 100644 index 00000000..2bcf4263 --- /dev/null +++ b/src/components/Tabs/TabsTrigger.tsx @@ -0,0 +1,28 @@ +import type { PropsWithChildren } from 'react'; + +import { useTabsContext } from '../../hooks/useTabsContext'; +import * as styles from './style.css'; + +interface TabsTriggerProps { + value: string; +} + +const TabsTrigger = ({ + children, + value, +}: PropsWithChildren) => { + const { selectedTab, onSelectTab } = useTabsContext(); + + return ( +
  • + +
  • + ); +}; + +export default TabsTrigger; diff --git a/src/components/Tabs/style.css.ts b/src/components/Tabs/style.css.ts new file mode 100644 index 00000000..d1fad7c7 --- /dev/null +++ b/src/components/Tabs/style.css.ts @@ -0,0 +1,38 @@ +import { style } from '@vanilla-extract/css'; +import { recipe } from '@vanilla-extract/recipes'; + +import { COLORS } from '@/src/styles/tokens'; +import * as utils from '@/src/styles/utils.css'; + +export const tabsList = style([ + utils.flexCenter, + { + padding: '16px 0 12px', + gap: '40px', + backgroundColor: COLORS['Grey/White'], + }, +]); + +export const tabsTrigger = recipe({ + base: { + fontSize: '15px', + fontWeight: 500, + lineHeight: '18px', + letterSpacing: '0px', + paddingBottom: '4px', + borderBottom: '2px solid transparent', + transition: 'all 150ms ease-in-out', + + ':hover': { + fontWeight: 700, + }, + }, + variants: { + isActive: { + true: { + fontWeight: 700, + borderBottomColor: '#121212', + }, + }, + }, +}); diff --git a/src/hooks/useTabsContext.ts b/src/hooks/useTabsContext.ts new file mode 100644 index 00000000..4bc249f4 --- /dev/null +++ b/src/hooks/useTabsContext.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react'; + +import { TabsContext } from '../components/Tabs/Tabs'; + +export const useTabsContext = () => { + const ctx = useContext(TabsContext); + + if (!ctx) { + throw new Error('useTabsContext hook must be used within a Tabs component'); + } + + return ctx; +}; diff --git a/src/styles/font.ts b/src/styles/font.ts new file mode 100644 index 00000000..5458313d --- /dev/null +++ b/src/styles/font.ts @@ -0,0 +1,24 @@ +import localFont from 'next/font/local'; + +export const pretendard = localFont({ + src: '../../public/fonts/PretendardVariable.woff2', + display: 'swap', + preload: true, + fallback: [ + 'Pretendard Variable', + 'Pretendard', + '-apple-system', + 'BlinkMacSystemFont', + 'system-ui', + 'Roboto', + 'Helvetica Neue', + 'Segoe UI', + 'Apple SD Gothic Neo', + 'Noto Sans KR', + 'Malgun Gothic', + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'sans-serif', + ], +});