diff --git a/frontend/src/components/common/ResponsiveSheet/ResponsiveSheet.stories.tsx b/frontend/src/components/common/ResponsiveSheet/ResponsiveSheet.stories.tsx new file mode 100644 index 000000000..f91328302 --- /dev/null +++ b/frontend/src/components/common/ResponsiveSheet/ResponsiveSheet.stories.tsx @@ -0,0 +1,91 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { colors } from '@/styles/theme/colors'; +import Button from '../Button/Button'; +import ResponsiveSheet from './ResponsiveSheet'; + +const meta = { + title: 'Components/Common/ResponsiveSheet', + component: ResponsiveSheet, + parameters: { + layout: 'fullscreen', + }, + tags: ['autodocs'], + argTypes: { + isOpen: { + control: 'boolean', + description: '시트의 열림/닫힘 상태입니다.', + }, + onClose: { + action: 'closed', + description: '닫기(배경 클릭/ESC) 시 호출됩니다.', + }, + children: { + control: false, + description: '시트 내부에 렌더링될 컨텐츠입니다.', + }, + sheetBackground: { + control: 'color', + description: '시트의 배경색입니다. 기본값은 흰색입니다.', + }, + stacked: { + control: 'boolean', + description: + '위에 다른 시트를 겹쳐 띄울 때 뒤에서 보이도록 들어올립니다. 바텀시트일 때만 적용됩니다.', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// 화면 너비가 700px 이하면 바텀시트, 그보다 넓으면 가운데 모달로 열린다. +// 툴바의 뷰포트를 700px 이하로 바꾸면 바텀시트 모양을 확인할 수 있다. +// 닫힌 채로 시작한다. 처음부터 열면 docs 페이지 전체를 오버레이가 덮는다. +const SheetTemplate = (args: Story['args']) => { + const [isOpen, setIsOpen] = useState(args?.isOpen ?? false); + + const handleClose = () => { + setIsOpen(false); + args?.onClose?.(); + }; + + return ( +
+ + +
+

시트 콘텐츠

+

+ 작은 화면에서는 하단 시트로, 큰 화면에서는 모달로 열립니다. +

+
+
+
+ ); +}; + +export const Default: Story = { + args: { + isOpen: false, + onClose: () => {}, + children: null, + }, + render: (args) => , +}; + +// 배경색을 바꾼 경우. 바텀시트·모달 카드 모두에 적용된다. +export const CustomBackground: Story = { + args: { + isOpen: false, + onClose: () => {}, + children: null, + sheetBackground: colors.gray[100], + }, + render: (args) => , +}; diff --git a/frontend/src/components/common/ToggleButton/ToggleButton.stories.tsx b/frontend/src/components/common/ToggleButton/ToggleButton.stories.tsx new file mode 100644 index 000000000..e8b025a62 --- /dev/null +++ b/frontend/src/components/common/ToggleButton/ToggleButton.stories.tsx @@ -0,0 +1,81 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import ToggleButton from './ToggleButton'; + +const meta = { + title: 'Components/Common/ToggleButton', + component: ToggleButton, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + active: { + control: 'boolean', + description: '선택된 상태인지 여부입니다. 배경색과 글자색이 바뀝니다.', + }, + children: { + control: 'text', + description: '버튼 내부에 들어갈 텍스트를 지정합니다.', + }, + onClick: { + description: '버튼 클릭 시 실행될 함수입니다.', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// 여러 개를 나란히 두고 하나만 고르는 필터 칩 형태로 쓰는 경우 +const ToggleGroup = () => { + const options = ['전체', '모집 중', '모집 예정', '모집 마감']; + const [selected, setSelected] = useState(options[0]); + + return ( +
+ {options.map((option) => ( + setSelected(option)} + > + {option} + + ))} +
+ ); +}; + +// 선택되지 않은 기본 상태 +export const Inactive: Story = { + args: { + active: false, + children: '모집 중', + }, +}; + +// 선택된 상태 +export const Active: Story = { + args: { + active: true, + children: '모집 중', + }, +}; + +// 문구가 길어졌을 때의 모양 +export const LongText: Story = { + args: { + active: true, + children: '이번 학기 신입 부원을 모집하는 동아리만 보기', + }, +}; + +// 클릭할 때마다 선택이 바뀌는 그룹 +export const Group: Story = { + args: { + active: false, + children: null, + }, + render: () => , +}; diff --git a/frontend/src/components/common/WebviewTopBar/WebviewTopBar.stories.tsx b/frontend/src/components/common/WebviewTopBar/WebviewTopBar.stories.tsx new file mode 100644 index 000000000..404f63228 --- /dev/null +++ b/frontend/src/components/common/WebviewTopBar/WebviewTopBar.stories.tsx @@ -0,0 +1,62 @@ +import { MemoryRouter } from 'react-router-dom'; +import type { Meta, StoryObj } from '@storybook/react'; +import WebviewTopBar from './WebviewTopBar'; + +const meta = { + title: 'Components/Common/WebviewTopBar', + component: WebviewTopBar, + parameters: { + layout: 'fullscreen', + }, + tags: ['autodocs'], + decorators: [ + (Story) => ( + + + + ), + ], + argTypes: { + title: { + control: 'text', + description: '상단바 가운데에 표시할 제목입니다.', + }, + onBack: { + action: 'back', + description: + '뒤로가기 버튼 클릭 시 실행될 함수입니다. 없으면 앱 브릿지 → 브라우저 history → fallbackPath 순으로 이동합니다.', + }, + fallbackPath: { + control: 'text', + description: + '앱이 처리하지 않고 이전 화면도 없을 때 이동할 경로입니다. 기본값은 `/`입니다.', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: '동아리 상세', + onBack: () => {}, + }, +}; + +// 제목이 길어도 뒤로가기 버튼과 오른쪽 여백 사이에 가운데 정렬로 남는지 확인 +export const LongTitle: Story = { + args: { + title: '모아동 중앙동아리 연합 신입 부원 모집 안내와 자주 묻는 질문', + onBack: () => {}, + }, +}; + +// onBack이 없는 경우. 컴포넌트가 window.history.state로 이전 화면을 판단해서 +// 스토리에서는 history 분기를 재현할 수 없고 항상 fallbackPath로 이동한다. +export const WithFallbackPath: Story = { + args: { + title: '동아리 상세', + fallbackPath: '/promotions', + }, +};