diff --git a/package.json b/package.json
index 67bdfc07..afba44da 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@vanilla-extract/sprinkles": "^1.6.1",
"axios": "^1.6.2",
"clsx": "^2.0.0",
+ "framer-motion": "^10.18.0",
"next": "14.0.3",
"react": "^18",
"react-dom": "^18",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 31555464..e3b4a390 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -29,6 +29,9 @@ dependencies:
clsx:
specifier: ^2.0.0
version: 2.0.0
+ framer-motion:
+ specifier: ^10.18.0
+ version: 10.18.0(react-dom@18.2.0)(react@18.2.0)
next:
specifier: 14.0.3
version: 14.0.3(@babel/core@7.23.5)(react-dom@18.2.0)(react@18.2.0)
@@ -1596,6 +1599,20 @@ packages:
/@emotion/hash@0.9.1:
resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==}
+ /@emotion/is-prop-valid@0.8.8:
+ resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
+ requiresBuild: true
+ dependencies:
+ '@emotion/memoize': 0.7.4
+ dev: false
+ optional: true
+
+ /@emotion/memoize@0.7.4:
+ resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
/@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0):
resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==}
peerDependencies:
@@ -7819,6 +7836,24 @@ packages:
engines: {node: '>= 0.6'}
dev: true
+ /framer-motion@10.18.0(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w==}
+ peerDependencies:
+ react: ^18.0.0
+ react-dom: ^18.0.0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ dependencies:
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ tslib: 2.6.2
+ optionalDependencies:
+ '@emotion/is-prop-valid': 0.8.8
+ dev: false
+
/fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
diff --git a/src/assets/icons/pencil-active.svg b/src/assets/icons/pencil-active.svg
new file mode 100644
index 00000000..d06ee955
--- /dev/null
+++ b/src/assets/icons/pencil-active.svg
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/pencil-default.svg b/src/assets/icons/pencil-default.svg
new file mode 100644
index 00000000..a3f538f5
--- /dev/null
+++ b/src/assets/icons/pencil-default.svg
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/template-active.svg b/src/assets/icons/template-active.svg
new file mode 100644
index 00000000..fb4341e5
--- /dev/null
+++ b/src/assets/icons/template-active.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/src/assets/icons/template-default.svg b/src/assets/icons/template-default.svg
new file mode 100644
index 00000000..a4d3709a
--- /dev/null
+++ b/src/assets/icons/template-default.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/src/components/Tabs/Tabs.stories.tsx b/src/components/Tabs/Tabs.stories.tsx
index 305698e5..b33eb2ee 100644
--- a/src/components/Tabs/Tabs.stories.tsx
+++ b/src/components/Tabs/Tabs.stories.tsx
@@ -1,7 +1,23 @@
+import { type ReactNode } from 'react';
+
import type { StoryObj } from '@storybook/react';
import { type Meta } from '@storybook/react';
-import Tabs from '.';
+import Tabs, { type TabsRootProps } from '.';
+import { type TabsTriggerProps } from './components/TabsTrigger';
+
+interface TabsMeta extends TabsRootProps, TabsTriggerProps {
+ tabListChildrenProp: ReactNode;
+ tabContentChildrenProp: ReactNode;
+ tabContentValueProp: string;
+}
+
+const CATEGORY = {
+ TABS: 'Tabs',
+ TABS_LIST: 'Tabs.List',
+ TABS_TRIGGER: 'Tabs.Trigger',
+ TABS_CONTENT: 'Tabs.Content',
+};
const COMPONENT_DESCRIPTION = `
- \`\`: 모든 컴포넌트에 대한 컨텍스트와 상태를 제공합니다.
@@ -10,13 +26,61 @@ const COMPONENT_DESCRIPTION = `
- \`\`: 선택된 탭에 해당되는 컨텐츠를 보여줍니다.
`;
-const meta: Meta = {
+const meta: Meta = {
title: 'Components/Tabs',
component: Tabs,
- tags: ['autodocs'],
argTypes: {
+ type: {
+ description: 'Tabs 컴포넌트의 종류',
+ table: {
+ category: CATEGORY.TABS,
+ },
+ },
defaultValue: {
description: 'Tabs 컴포넌트의 초기에 활성화될 탭의 기본값',
+ table: {
+ category: CATEGORY.TABS,
+ },
+ },
+ tabListChildrenProp: {
+ name: 'children',
+ description: '여러 개의 Tabs.Trigger 컴포넌트를 감싸줍니다.',
+ table: {
+ type: { summary: 'ReactNode' },
+ category: CATEGORY.TABS_LIST,
+ },
+ },
+ value: {
+ description:
+ 'Tabs.Trigger 컴포넌트와 Tabs.Content 컴포넌트를 연결하는 고유한 값',
+ table: {
+ type: { summary: 'string' },
+ category: CATEGORY.TABS_TRIGGER,
+ },
+ },
+ icon: {
+ description: 'Tabs.Trigger 컴포넌트 내 아이콘',
+ table: {
+ type: { summary: 'Icons' },
+ category: CATEGORY.TABS_TRIGGER,
+ },
+ },
+ tabContentChildrenProp: {
+ name: 'children',
+ description: '활성화되었을 때 보여줄 컨텐츠를 감싸줍니다.',
+ table: {
+ type: { summary: 'ReactNode' },
+ category: CATEGORY.TABS_CONTENT,
+ },
+ },
+ tabContentValueProp: {
+ name: 'value',
+ description:
+ 'Tabs.Trigger 컴포넌트와 Tabs.Content 컴포넌트를 연결하는 고유한 값',
+ table: {
+ type: { summary: 'string' },
+ category: CATEGORY.TABS_CONTENT,
+ },
},
},
parameters: {
@@ -33,9 +97,53 @@ export default meta;
type Story = StoryObj;
-export const Basic: Story = {
+export const Filter: Story = {
+ render: () => (
+
+
+ 끄적이는
+ 참고하는
+
+
+ 끄적이는 내용
+
+
+ 참고하는 내용
+
+
+ ),
+};
+
+export const FilterWithIcon: Story = {
render: () => (
-
+
+
+
+ 끄적이는
+
+
+ 참고하는
+
+
+
+ 끄적이는 내용
+
+
+ 참고하는 내용
+
+
+ ),
+};
+
+export const Switcher: Story = {
+ render: () => (
+
끄적이는
참고하는
@@ -49,3 +157,30 @@ export const Basic: Story = {
),
};
+
+export const SwitcherWithIcon: Story = {
+ render: () => (
+
+
+
+ 끄적이는
+
+
+ 참고하는
+
+
+
+ 끄적이는 내용
+
+
+ 참고하는 내용
+
+
+ ),
+};
diff --git a/src/components/Tabs/components/TabsContent.tsx b/src/components/Tabs/components/TabsContent.tsx
index c7432cdf..bec8b6c0 100644
--- a/src/components/Tabs/components/TabsContent.tsx
+++ b/src/components/Tabs/components/TabsContent.tsx
@@ -2,7 +2,7 @@ import type { PropsWithChildren } from 'react';
import { useTabsContext } from '../../../hooks/useTabsContext';
-interface TabsContentProps {
+export interface TabsContentProps {
value: string;
}
diff --git a/src/components/Tabs/components/TabsList.tsx b/src/components/Tabs/components/TabsList.tsx
index 882c1f53..199b301b 100644
--- a/src/components/Tabs/components/TabsList.tsx
+++ b/src/components/Tabs/components/TabsList.tsx
@@ -1,9 +1,13 @@
import type { PropsWithChildren } from 'react';
+import { useTabsContext } from '@hooks/useTabsContext';
+
import * as styles from '../style.css';
const TabsList = ({ children }: PropsWithChildren) => {
- return ;
+ const { type } = useTabsContext();
+
+ return ;
};
export default TabsList;
diff --git a/src/components/Tabs/components/TabsTrigger.tsx b/src/components/Tabs/components/TabsTrigger.tsx
index f400adea..28353447 100644
--- a/src/components/Tabs/components/TabsTrigger.tsx
+++ b/src/components/Tabs/components/TabsTrigger.tsx
@@ -1,26 +1,53 @@
-import type { PropsWithChildren } from 'react';
+import { type PropsWithChildren } from 'react';
+import clsx from 'clsx';
+import { motion } from 'framer-motion';
+
+import Icon from '@components/Icon';
+import { type Icons } from '@constants/icon';
import { useTabsContext } from '../../../hooks/useTabsContext';
import * as styles from '../style.css';
-interface TabsTriggerProps {
+export interface TabsTriggerProps {
value: string;
+ icon?: { default: Icons; active: Icons };
}
const TabsTrigger = ({
children,
value,
+ icon,
}: PropsWithChildren) => {
- const { selectedTab, onSelectTab } = useTabsContext();
+ const { type, selectedTab, onSelectTab } = useTabsContext();
+
+ const isActive = selectedTab === value;
+ const isActiveFilterTab = isActive && type === 'filter';
+ const isActiveSwitcherTab = isActive && type === 'switcher';
return (
-
+
+ {isActiveFilterTab && (
+
+ )}
+ {isActiveSwitcherTab && (
+
+ )}
);
};
diff --git a/src/components/Tabs/index.tsx b/src/components/Tabs/index.tsx
index 07a44962..97600e30 100644
--- a/src/components/Tabs/index.tsx
+++ b/src/components/Tabs/index.tsx
@@ -4,12 +4,16 @@ import TabsContent from './components/TabsContent';
import TabsList from './components/TabsList';
import TabsTrigger from './components/TabsTrigger';
+type TabsType = 'filter' | 'switcher';
+
interface TabsContextProps {
+ type?: TabsType;
selectedTab: string;
onSelectTab: (selectedTab: string) => void;
}
export interface TabsRootProps {
+ type?: TabsType;
defaultValue: string;
}
@@ -17,6 +21,7 @@ export const TabsContext = createContext(null);
const TabsRoot = ({
children,
+ type = 'filter',
defaultValue,
}: PropsWithChildren) => {
const [selectedTab, setSelectedTab] = useState(defaultValue);
@@ -26,7 +31,9 @@ const TabsRoot = ({
};
return (
-
+
{children}
);
diff --git a/src/components/Tabs/style.css.ts b/src/components/Tabs/style.css.ts
index 3c1b4735..102c6975 100644
--- a/src/components/Tabs/style.css.ts
+++ b/src/components/Tabs/style.css.ts
@@ -4,35 +4,99 @@ import { recipe } from '@vanilla-extract/recipes';
import { COLORS } from '@styles/tokens';
import * as utils from '@styles/utils.css';
-export const tabsList = style([
- utils.flexCenter,
- {
- padding: '16px 0 12px',
- gap: '40px',
- backgroundColor: COLORS['Grey/White'],
+export const tabsList = recipe({
+ base: [
+ utils.flexCenter,
+ {
+ width: 'fit-content',
+ margin: '16px 0 12px',
+ },
+ ],
+ variants: {
+ type: {
+ switcher: {
+ borderRadius: '100px',
+ backgroundColor: COLORS['Grey/200'],
+ },
+ filter: {
+ gap: '20px',
+ backgroundColor: COLORS['Grey/White'],
+ },
+ },
},
-]);
+});
+
+export const tabItem = style({
+ position: 'relative',
+});
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,
+ base: [
+ utils.flexCenter,
+ {
+ gap: '5px',
+ whiteSpace: 'nowrap',
+ transition: 'all 150ms ease-in-out',
+
+ ':hover': {
+ fontWeight: 700,
+ },
},
- },
+ ],
variants: {
- isActive: {
- true: {
- fontWeight: 700,
- borderBottomColor: '#121212',
+ type: {
+ switcher: {
+ position: 'relative',
+ width: '183px',
+ height: '42px',
+ padding: '8px 0',
+ fontSize: '16px',
+ fontWeight: 500,
+ lineHeight: '19px',
+ letterSpacing: '-0.5px',
+ color: COLORS['Grey/500'],
+ zIndex: 20,
},
+ filter: [
+ {
+ fontSize: '15px',
+ fontWeight: 500,
+ lineHeight: '18px',
+ letterSpacing: '0px',
+ position: 'relative',
+ paddingBottom: '4px',
+ borderBottom: '2px solid transparent',
+ },
+ ],
},
},
});
+
+export const isActiveSwitcher = style({
+ fontWeight: 700,
+ color: COLORS['Blue/Dark'],
+});
+
+export const isActiveFilter = style({
+ fontWeight: 700,
+});
+
+export const underline = style({
+ position: 'absolute',
+ bottom: '-2px',
+ left: 0,
+ right: 0,
+ height: '2px',
+ background: COLORS['Grey/900'],
+});
+
+export const handle = style({
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ height: '42px',
+ borderRadius: '100px',
+ backgroundColor: COLORS['Grey/White'],
+ border: `1.5px solid ${COLORS['Blue/Default']}`,
+});
diff --git a/src/constants/icon.ts b/src/constants/icon.ts
index 9d171579..45150291 100644
--- a/src/constants/icon.ts
+++ b/src/constants/icon.ts
@@ -4,10 +4,14 @@ import Close from '@assets/icons/close.svg';
import Copy from '@assets/icons/copy.svg';
import Logout from '@assets/icons/logout.svg';
import Menu from '@assets/icons/menu.svg';
+import PencilActive from '@assets/icons/pencil-active.svg';
+import PencilDefault from '@assets/icons/pencil-default.svg';
import Profle from '@assets/icons/profile.svg';
import ProfileDialog from '@assets/icons/profileDialog.svg';
import Setting from '@assets/icons/setting.svg';
import Submit from '@assets/icons/submit.svg';
+import TemplateActive from '@assets/icons/template-active.svg';
+import TemplateDefault from '@assets/icons/template-default.svg';
export const iconFactory = {
add: Add,
@@ -16,10 +20,14 @@ export const iconFactory = {
copy: Copy,
logout: Logout,
menu: Menu,
+ pencilActive: PencilActive,
+ pencilDefault: PencilDefault,
profile: Profle,
profileDialog: ProfileDialog,
setting: Setting,
submit: Submit,
+ templateActive: TemplateActive,
+ templateDefault: TemplateDefault,
};
export type Icons = keyof typeof iconFactory;