file: blocks/footer.mdx # Footer 페이지 하단에 사이트 정보, 링크, 법적 고지 등을 표시하는 블록입니다. - npm: npx @seed-design/cli@latest add block:footer-01 - pnpm: pnpm dlx @seed-design/cli@latest add block:footer-01 - yarn: yarn dlx @seed-design/cli@latest add block:footer-01 - bun: bun x @seed-design/cli@latest add block:footer-01 * npm: npx @seed-design/cli@latest add block:footer-02 * pnpm: pnpm dlx @seed-design/cli@latest add block:footer-02 * yarn: yarn dlx @seed-design/cli@latest add block:footer-02 * bun: bun x @seed-design/cli@latest add block:footer-02 - npm: npx @seed-design/cli@latest add block:footer-03 - pnpm: pnpm dlx @seed-design/cli@latest add block:footer-03 - yarn: yarn dlx @seed-design/cli@latest add block:footer-03 - bun: bun x @seed-design/cli@latest add block:footer-03 * npm: npx @seed-design/cli@latest add block:footer-04 * pnpm: pnpm dlx @seed-design/cli@latest add block:footer-04 * yarn: yarn dlx @seed-design/cli@latest add block:footer-04 * bun: bun x @seed-design/cli@latest add block:footer-04 --- file: blocks/layout.mdx # Layout 페이지의 콘텐츠 영역을 반응형으로 제한하고 중앙 정렬하는 페이지 레이아웃 블록입니다. ## Usage ```tsx import { Layout } from "@seed-design/react"; ``` ```tsx {/* 콘텐츠 */} ``` ## Props ### `Layout.Root` ### `Layout.Content` ## Density `density` prop으로 콘텐츠 영역의 최대 너비를 조절합니다. ## Blocks - npm: npx @seed-design/cli@latest add block:layout-01 - pnpm: pnpm dlx @seed-design/cli@latest add block:layout-01 - yarn: yarn dlx @seed-design/cli@latest add block:layout-01 - bun: bun x @seed-design/cli@latest add block:layout-01 * npm: npx @seed-design/cli@latest add block:layout-02 * pnpm: pnpm dlx @seed-design/cli@latest add block:layout-02 * yarn: yarn dlx @seed-design/cli@latest add block:layout-02 * bun: bun x @seed-design/cli@latest add block:layout-02 --- file: blocks/side-navigation.mdx # Side Navigation 서비스의 최상위 메뉴 간 이동을 돕고 앱의 전체 구조를 탐색할 수 있게 하는 컴포넌트입니다. ## Installation - npm: npx @seed-design/cli@latest add ui:side-navigation - pnpm: pnpm dlx @seed-design/cli@latest add ui:side-navigation - yarn: yarn dlx @seed-design/cli@latest add ui:side-navigation - bun: bun x @seed-design/cli@latest add ui:side-navigation ## Examples - npm: npx @seed-design/cli@latest add block:side-navigation-01 - pnpm: pnpm dlx @seed-design/cli@latest add block:side-navigation-01 - yarn: yarn dlx @seed-design/cli@latest add block:side-navigation-01 - bun: bun x @seed-design/cli@latest add block:side-navigation-01 * npm: npx @seed-design/cli@latest add block:side-navigation-02 * pnpm: pnpm dlx @seed-design/cli@latest add block:side-navigation-02 * yarn: yarn dlx @seed-design/cli@latest add block:side-navigation-02 * bun: bun x @seed-design/cli@latest add block:side-navigation-02 ## Props ### SideNavigationProvider ### SideNavigationRoot ### SideNavigationHeader ### SideNavigationContent ### SideNavigationFooter ### SideNavigationTrigger ### SideNavigationGroup ### SideNavigationItemButton ### SideNavigationInset --- file: components/(deprecated)/action-chip.mdx # Action Chip 더 이상 사용되지 않습니다. [Chip](/react/components/chip)의 `Chip.Button`에 `variant="solid"`를 사용하세요. ## Preview ```tsx import { ActionChip } from "@seed-design/react"; export default function ActionChipPreview() { return 라벨; } ``` ## Usage ```tsx import { ActionChip } from "@seed-design/react"; ``` ```tsx ``` ## Props ## Examples ### Medium ```tsx import { ActionChip } from "@seed-design/react"; export default function ActionChipMedium() { return 라벨; } ``` ### Small ```tsx import { ActionChip } from "@seed-design/react"; export default function ActionChipSmall() { return 라벨; } ``` ### Icon Only ```tsx import { IconPlusFill } from "@karrotmarket/react-monochrome-icon"; import { ActionChip, Icon } from "@seed-design/react"; export default function ActionChipIconOnly() { return ( } /> ); } ``` ### Prefix Icon ```tsx import { IconPlusFill } from "@karrotmarket/react-monochrome-icon"; import { ActionChip, PrefixIcon } from "@seed-design/react"; export default function ActionChipPrefixIcon() { return ( } /> 라벨 ); } ``` ### Suffix Icon ```tsx import { IconChevronDownFill } from "@karrotmarket/react-monochrome-icon"; import { ActionChip, SuffixIcon } from "@seed-design/react"; export default function ActionChipSuffixIcon() { return ( 라벨 } /> ); } ``` --- file: components/(deprecated)/action-sheet.mdx # Action Sheet 더 이상 사용되지 않습니다. [Swipeable Menu Sheet](/react/components/swipeable-menu-sheet)을 사용하세요. ## Preview ```tsx import { ActionSheetContent, ActionSheetItem, ActionSheetRoot, ActionSheetTrigger, } from "seed-design/ui/action-sheet"; import { ActionButton } from "seed-design/ui/action-button"; const ActionSheetPreview = () => { return ( Open ); }; export default ActionSheetPreview; ``` ## Installation - npm: npx @seed-design/cli@latest add ui:action-sheet - pnpm: pnpm dlx @seed-design/cli@latest add ui:action-sheet - yarn: yarn dlx @seed-design/cli@latest add ui:action-sheet - bun: bun x @seed-design/cli@latest add ui:action-sheet ## Props ### `ActionSheetRoot` ### `ActionSheetContent` ### `ActionSheetItem` ## Examples ### Portalled ``으로 ``를 감싸서 컨텐츠를 원하는 요소에 렌더링할 수 있습니다. Portal은 기본적으로 `document.body`에 렌더링됩니다. ```tsx import { Portal } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { ActionSheetContent, ActionSheetItem, ActionSheetRoot, ActionSheetTrigger, } from "seed-design/ui/action-sheet"; const ActionSheetPortalled = () => { return ( Open ); }; export default ActionSheetPortalled; ``` --- file: components/(deprecated)/columns.mdx # Columns 여러 열로 구성된 레이아웃을 구성합니다. 디자인 토큰을 JSX에서 사용할 수 있도록 도와줍니다. 더 이상 사용되지 않습니다. [`HStack`](/react/components/layout/h-stack)과 `wrap` prop을 사용하세요. ## Preview ```tsx import { Column, Columns } from "@seed-design/react"; /** * @deprecated use `HStack` instead. */ export default function ColumnsPreview() { return ( 1 Content Width 2 ); } ``` ## Usage ```tsx import { Columns, Column } from "@seed-design/react"; ``` ```tsx ``` ## Props ### `Columns` ### `Column` --- file: components/(deprecated)/control-chip.mdx # Control Chip 더 이상 사용되지 않습니다. [Chip](/react/components/chip)의 `Chip.Toggle` 또는 `Chip.Button`을 사용하세요. ## Preview ```tsx import { ControlChip } from "seed-design/ui/control-chip"; export default function ControlChipPreview() { return 라벨; } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:control-chip - pnpm: pnpm dlx @seed-design/cli@latest add ui:control-chip - yarn: yarn dlx @seed-design/cli@latest add ui:control-chip - bun: bun x @seed-design/cli@latest add ui:control-chip ## Props ### ControlChip.Button ### ControlChip.Toggle ### ControlChip.RadioRoot ### ControlChip.RadioItem ## Examples ### Medium ```tsx import { ControlChip } from "seed-design/ui/control-chip"; export default function ActionChipMedium() { return 라벨; } ``` ### Small ```tsx import { ControlChip } from "seed-design/ui/control-chip"; export default function ActionChipSmall() { return 라벨; } ``` ### Icon Only ```tsx import { IconPlusFill } from "@karrotmarket/react-monochrome-icon"; import { Icon } from "@seed-design/react"; import { ControlChip } from "seed-design/ui/control-chip"; export default function ControlChipIconOnly() { return ( } /> ); } ``` ### Prefix Icon ```tsx import { IconPlusFill } from "@karrotmarket/react-monochrome-icon"; import { PrefixIcon } from "@seed-design/react"; import { ControlChip } from "seed-design/ui/control-chip"; export default function ControlChipPrefixIcon() { return ( } /> 라벨 ); } ``` ### Suffix Icon ```tsx import { IconChevronDownFill } from "@karrotmarket/react-monochrome-icon"; import { SuffixIcon } from "@seed-design/react"; import { ControlChip } from "seed-design/ui/control-chip"; export default function ControlChipSuffixIcon() { return ( 라벨 } /> ); } ``` --- file: components/(deprecated)/error-state.mdx # Error State 사용자에게 오류 혹은 조회 결과가 없음을 알리는 컴포넌트입니다. 더 이상 사용되지 않습니다. [`ResultSection`](/react/components/result-section)을 사용하세요. ## Preview ```tsx import { VStack } from "@seed-design/react"; import { ErrorState } from "seed-design/ui/error-state"; export default function ErrorStatePreview() { return ( ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:error-state - pnpm: pnpm dlx @seed-design/cli@latest add ui:error-state - yarn: yarn dlx @seed-design/cli@latest add ui:error-state - bun: bun x @seed-design/cli@latest add ui:error-state ## Props ## Examples ### Basement layer-basement 배경 위에서는 basement variant를 사용합니다. ```tsx import { VStack } from "@seed-design/react"; import { ErrorState } from "seed-design/ui/error-state"; export default function ErrorStateBasement() { return ( ); } ``` --- file: components/(deprecated)/extended-action-sheet.mdx # Extended Action Sheet 더 이상 사용되지 않습니다. [Swipeable Menu Sheet](/react/components/swipeable-menu-sheet)을 사용하세요. ## Preview ```tsx import { IconEyeSlashLine } from "@karrotmarket/react-monochrome-icon"; import { PrefixIcon } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { ExtendedActionSheetContent, ExtendedActionSheetGroup, ExtendedActionSheetItem, ExtendedActionSheetRoot, ExtendedActionSheetTrigger, } from "seed-design/ui/extended-action-sheet"; const ExtendedActionSheetPreview = () => { return ( Open } /> Action 1 } /> Action 2 } /> Action 3 } /> Action 4 } /> Action 5 ); }; export default ExtendedActionSheetPreview; ``` ## Installation - npm: npx @seed-design/cli@latest add ui:extended-action-sheet - pnpm: pnpm dlx @seed-design/cli@latest add ui:extended-action-sheet - yarn: yarn dlx @seed-design/cli@latest add ui:extended-action-sheet - bun: bun x @seed-design/cli@latest add ui:extended-action-sheet ## Props ### `ExtendedActionSheetRoot` ### `ExtendedActionSheetTrigger` ### `ExtendedActionSheetContent` ### `ExtendedActionSheetGroup` ### `ExtendedActionSheetItem` --- file: components/(deprecated)/extended-fab.mdx # Extended FAB 더 이상 사용되지 않습니다. [Contextual Floating Button](/react/components/contextual-floating-button)을 사용하세요. ## Preview ```tsx import { IconBellFill } from "@karrotmarket/react-monochrome-icon"; import { ExtendedFab, PrefixIcon } from "@seed-design/react"; export default function ExtendedFabPreview() { return ( } /> 알림 설정 ); } ``` ## Usage ```tsx import { ExtendedFab, PrefixIcon } from "@seed-design/react"; import { IconPlusLine } from "@karrotmarket/react-monochrome-icon"; ``` ```tsx } /> 라벨 ``` ## Props ## Examples ### Medium ```tsx import { IconBellFill } from "@karrotmarket/react-monochrome-icon"; import { ExtendedFab, PrefixIcon } from "@seed-design/react"; export default function ExtendedFabMedium() { return ( } /> 알림 설정 ); } ``` ### Small ```tsx import { IconBellFill } from "@karrotmarket/react-monochrome-icon"; import { ExtendedFab, PrefixIcon } from "@seed-design/react"; export default function ExtendedFabSmall() { return ( } /> 알림 설정 ); } ``` ### Neutral Solid ```tsx import { IconBellFill } from "@karrotmarket/react-monochrome-icon"; import { ExtendedFab, PrefixIcon } from "@seed-design/react"; export default function ExtendedFabNeutralSolid() { return ( } /> 알림 설정 ); } ``` ### Layer Floating ```tsx import { IconBellFill } from "@karrotmarket/react-monochrome-icon"; import { ExtendedFab, PrefixIcon } from "@seed-design/react"; export default function ExtendedFabLayerFloating() { return ( } /> 알림 설정 ); } ``` --- file: components/(deprecated)/fab.mdx # FAB 더 이상 사용되지 않습니다. [Contextual Floating Button](/react/components/contextual-floating-button)을 사용하세요. ## Preview ```tsx import IconPlusLine from "@karrotmarket/react-monochrome-icon/IconPlusLine"; import { Fab, Icon } from "@seed-design/react"; export default function FabPreview() { return ( } /> ); } ``` ## Usage ```tsx import { Fab, Icon } from "@seed-design/react"; import { IconPlusLine } from "@karrotmarket/react-monochrome-icon"; ``` ```tsx } /> ``` ## Props --- file: components/(deprecated)/inline-banner.mdx # Inline Banner 더 이상 사용되지 않습니다. [Page Banner](/react/components/page-banner)를 사용하세요. ## Preview ```tsx import { VStack } from "@seed-design/react"; import { ActionableInlineBanner, DismissibleInlineBanner, InlineBanner, } from "seed-design/ui/inline-banner"; export default function InlineBannerPreview() { return ( ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:inline-banner - pnpm: pnpm dlx @seed-design/cli@latest add ui:inline-banner - yarn: yarn dlx @seed-design/cli@latest add ui:inline-banner - bun: bun x @seed-design/cli@latest add ui:inline-banner ## Props ### `InlineBanner` ### `ActionableInlineBanner` ### `DismissibleInlineBanner` ## Examples ### Content Layout #### With Link Label `linkProps` prop은 `InlineBanner`에서만 제공돼요. ```tsx import { InlineBanner } from "seed-design/ui/inline-banner"; export default function InlineBannerWithLinkLabel() { return ( ); } ``` #### Text Only ```tsx import { VStack } from "@seed-design/react"; import { ActionableInlineBanner, DismissibleInlineBanner, InlineBanner, } from "seed-design/ui/inline-banner"; export default function InlineBannerTextOnly() { return ( ); } ``` #### With Icon ```tsx import { IconBellFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; import { ActionableInlineBanner, DismissibleInlineBanner, InlineBanner, } from "seed-design/ui/inline-banner"; export default function InlineBannerWithIcon() { return ( } description="사업자 정보를 등록해주세요." /> } description="사업자 정보를 등록해주세요." /> } description="사업자 정보를 등록해주세요." /> ); } ``` #### With Title Text ```tsx import { VStack } from "@seed-design/react"; import { ActionableInlineBanner, DismissibleInlineBanner, InlineBanner, } from "seed-design/ui/inline-banner"; export default function InlineBannerWithTitleText() { return ( ); } ``` #### With All ```tsx import { IconBellFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; import { ActionableInlineBanner, DismissibleInlineBanner, InlineBanner, } from "seed-design/ui/inline-banner"; export default function InlineBannerWithAll() { return ( } title="타이틀" description="사업자 정보를 등록해주세요." /> } title="타이틀" description="사업자 정보를 등록해주세요." /> } title="타이틀" description="사업자 정보를 등록해주세요." /> ); } ``` ### Customizable Parts #### Rendering `LinkInlineBanner`’s Link Label as Child ```tsx import { InlineBanner } from "seed-design/ui/inline-banner"; export default function InlineBannerLinkLabelAsChild() { return ( 자세히 보기 ), }} /> ); } ``` ### Variants #### Neutral Weak (Default) ```tsx import { IconBellFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; import { ActionableInlineBanner, DismissibleInlineBanner, InlineBanner, } from "seed-design/ui/inline-banner"; export default function InlineBannerNeutralWeak() { return ( } description="사업자 정보를 등록해주세요." /> } description="사업자 정보를 등록해주세요." /> } description="사업자 정보를 등록해주세요." /> ); } ``` #### Positive Weak ```tsx import { IconCheckmarkCircleFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; import { ActionableInlineBanner, DismissibleInlineBanner, InlineBanner, } from "seed-design/ui/inline-banner"; export default function InlineBannerPositiveWeak() { return ( } description="사업자 정보를 등록해주세요." /> } description="사업자 정보를 등록해주세요." /> } description="사업자 정보를 등록해주세요." /> ); } ``` #### Informative Weak ```tsx import { IconILowercaseSerifCircleFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; import { ActionableInlineBanner, DismissibleInlineBanner, InlineBanner, } from "seed-design/ui/inline-banner"; export default function InlineBannerInformativeWeak() { return ( } description="사업자 정보를 등록해주세요." /> } description="사업자 정보를 등록해주세요." /> } description="사업자 정보를 등록해주세요." /> ); } ``` #### Warning Weak ```tsx import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; import { ActionableInlineBanner, DismissibleInlineBanner, InlineBanner, } from "seed-design/ui/inline-banner"; export default function InlineBannerWarningWeak() { return ( } description="사업자 정보를 등록해주세요." /> } description="사업자 정보를 등록해주세요." /> } description="사업자 정보를 등록해주세요." /> ); } ``` #### Warning Solid ```tsx import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; import { ActionableInlineBanner, DismissibleInlineBanner, InlineBanner, } from "seed-design/ui/inline-banner"; export default function InlineBannerWarningSolid() { return ( } description="사업자 정보를 등록해주세요." /> } description="사업자 정보를 등록해주세요." /> } description="사업자 정보를 등록해주세요." /> ); } ``` #### Critical Weak ```tsx import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; import { ActionableInlineBanner, InlineBanner } from "seed-design/ui/inline-banner"; export default function InlineBannerCriticalWeak() { return ( } description="사업자 정보를 등록해주세요." /> } description="사업자 정보를 등록해주세요." /> ); } ``` #### Critical Solid `criticalWeak`과 `criticalSolid` variant는 `InlineBanner`와 `ActionableInlineBanner`에서만 제공돼요. variant가 `criticalWeak`이나 `criticalSolid`인 경우 `position: sticky` 등을 활용하여 화면을 스크롤했을 때도 인라인 배너가 상단에 고정되도록 해 주세요. ```tsx import { ActionableInlineBanner, InlineBanner } from "seed-design/ui/inline-banner"; import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; export default function InlineBannerCriticalSolid() { return ( } description="사업자 정보를 등록해주세요." /> } description="사업자 정보를 등록해주세요." /> ); } ``` --- file: components/(deprecated)/inline.mdx # Inline 인라인 레이아웃을 구성합니다. 디자인 토큰을 JSX에서 사용할 수 있도록 도와줍니다. 더 이상 사용되지 않습니다. [`HStack`](/react/components/layout/h-stack)과 `wrap` prop을 사용하세요. ## Preview ```tsx import { Box, Inline } from "@seed-design/react"; /** * @deprecated Use `HStack` instead. */ export default function InlinePreview() { return ( 1 2 3 ); } ``` ## Usage ```tsx import { Inline } from "@seed-design/react"; ``` ```tsx ``` ## Props --- file: components/(deprecated)/link-content.mdx # Link Content 더 이상 사용되지 않습니다. [Action Button](/react/components/action-button)에 `variant="ghost"`를 사용하세요. ## Preview ```tsx import { IconChevronRightLine } from "@karrotmarket/react-monochrome-icon"; import { LinkContent, SuffixIcon } from "@seed-design/react"; export default function LinkContentPreview() { return ( 새 글 } /> ); } ``` ## Usage ```tsx import { LinkContent, SuffixIcon } from "@seed-design/react"; import { IconChevronRightLine } from "@karrotmarket/react-monochrome-icon"; ``` ```tsx Label } /> ``` ## Props ## Examples ### Size ```tsx import { IconChevronRightLine } from "@karrotmarket/react-monochrome-icon"; import { LinkContent, SuffixIcon, VStack } from "@seed-design/react"; export default function LinkContentSize() { return ( 추가 } /> 추가 } /> 추가 } /> ); } ``` ### Color ```tsx import { IconChevronRightLine } from "@karrotmarket/react-monochrome-icon"; import { LinkContent, VStack, SuffixIcon } from "@seed-design/react"; export default function LinkContentColor() { return ( 더보기 } /> 더보기 } /> 더보기 } /> 더보기 } /> ); } ``` --- file: components/(deprecated)/menu-sheet.mdx # Menu Sheet 사용자의 작업과 관련된 선택지를 제공하는 시트 형태의 컴포넌트입니다. 더 이상 사용되지 않습니다. [Swipeable Menu Sheet](/react/components/swipeable-menu-sheet)을 사용하세요. ## Preview ```tsx import { IconEyeSlashLine } from "@karrotmarket/react-monochrome-icon"; import { ActionButton } from "seed-design/ui/action-button"; import { MenuSheetContent, MenuSheetGroup, MenuSheetItem, MenuSheetRoot, MenuSheetTrigger, } from "seed-design/ui/menu-sheet"; const MenuSheetPreview = () => { return ( Open } /> } /> } /> } /> } tone="critical" /> ); }; export default MenuSheetPreview; ``` Stackflow와 Menu Sheet를 함께 사용하는 방법에 대해 알아보세요. ## Installation - npm: npx @seed-design/cli@latest add ui:menu-sheet - pnpm: pnpm dlx @seed-design/cli@latest add ui:menu-sheet - yarn: yarn dlx @seed-design/cli@latest add ui:menu-sheet - bun: bun x @seed-design/cli@latest add ui:menu-sheet ## Props ### `MenuSheetRoot` ### `MenuSheetTrigger` ### `MenuSheetContent` ### `MenuSheetGroup` ### `MenuSheetItem` ## Examples ### Trigger ``는 `aria-haspopup="dialog"` 속성을 설정하고, MenuSheet의 `open` 상태에 따라 `aria-expanded` 속성을 자동으로 설정합니다. 이 속성은 스크린 리더와 같은 보조 기술에 유용합니다. ## Preview ```tsx import { IconEyeSlashLine } from "@karrotmarket/react-monochrome-icon"; import { ActionButton } from "seed-design/ui/action-button"; import { MenuSheetContent, MenuSheetGroup, MenuSheetItem, MenuSheetRoot, MenuSheetTrigger, } from "seed-design/ui/menu-sheet"; const MenuSheetPreview = () => { return ( Open } /> } /> } /> } /> } tone="critical" /> ); }; export default MenuSheetPreview; ``` ### With Title `MenuSheetContent`의 `title` prop을 사용하여 시트 헤더에 제목을 표시합니다. ```tsx import { IconEyeSlashLine } from "@karrotmarket/react-monochrome-icon"; import { ActionButton } from "seed-design/ui/action-button"; import { MenuSheetContent, MenuSheetGroup, MenuSheetItem, MenuSheetRoot, MenuSheetTrigger, } from "seed-design/ui/menu-sheet"; const MenuSheetWithTitle = () => { return ( Open } /> } description="Ut nulla et id dolor labore ullamco irure est id occaecat." /> } description="Ut nulla et id dolor labore ullamco irure est id occaecat." /> } /> } tone="critical" /> ); }; export default MenuSheetWithTitle; ``` ### With Title and Description `MenuSheetContent`의 `description` prop을 사용하여 제목 아래에 부가 설명을 추가합니다. ```tsx import { IconEyeSlashLine } from "@karrotmarket/react-monochrome-icon"; import { ActionButton } from "seed-design/ui/action-button"; import { MenuSheetContent, MenuSheetGroup, MenuSheetItem, MenuSheetRoot, MenuSheetTrigger, } from "seed-design/ui/menu-sheet"; export default function MenuSheetWithTitleAndDescription() { return ( Open } /> } description="Ut nulla et id dolor labore ullamco irure est id occaecat." /> } description="Ut nulla et id dolor labore ullamco irure est id occaecat." /> } /> } tone="critical" /> ); } ``` ### Label Align `MenuSheetContent`의 `labelAlign` prop으로 메뉴 항목의 레이블 정렬을 설정합니다. #### `labelAlign="left"` (with `PrefixIcon`) 레이블을 왼쪽 정렬합니다. ```tsx import { IconEyeSlashLine } from "@karrotmarket/react-monochrome-icon"; import { ActionButton } from "seed-design/ui/action-button"; import { MenuSheetContent, MenuSheetGroup, MenuSheetItem, MenuSheetRoot, MenuSheetTrigger, } from "seed-design/ui/menu-sheet"; const MenuSheetWithPrefixIcon = () => { return ( Open } /> } /> } /> } /> } tone="critical" /> ); }; export default MenuSheetWithPrefixIcon; ``` #### `labelAlign="center"` (without `PrefixIcon`) 레이블을 중앙 정렬합니다. (일반적으로, `prefixIcon` 없는 경우) ```tsx import { ActionButton } from "seed-design/ui/action-button"; import { MenuSheetContent, MenuSheetGroup, MenuSheetItem, MenuSheetRoot, MenuSheetTrigger, } from "seed-design/ui/menu-sheet"; const MenuSheetWithoutPrefixIcon = () => { return ( Open ); }; export default MenuSheetWithoutPrefixIcon; ``` #### Overriding `labelAlign` 필요한 경우 `MenuSheetContent`에 지정한 `labelAlign`을 `MenuSheetGroup` 또는 `MenuSheetItem`에 지정한 `labelAlign`으로 덮어쓸 수 있습니다. ```tsx import { ActionButton } from "seed-design/ui/action-button"; import { MenuSheetContent, MenuSheetGroup, MenuSheetItem, MenuSheetRoot, MenuSheetTrigger, } from "seed-design/ui/menu-sheet"; const MenuSheetOverridingLabelAlign = () => { return ( Open ); }; export default MenuSheetOverridingLabelAlign; ``` ### `onOpenChange` Details `onOpenChange` 두 번째 인자로 `details`가 제공됩니다. #### `reason` **열릴 때** (`open: true`) - `"trigger"`: `MenuSheetTrigger` (`MenuSheet.Trigger`)로 열림 **닫힐 때** (`open: false`) - `"closeButton"`: `MenuSheet.CloseButton`으로 닫힘 - `"escapeKeyDown"`: ESC 키 사용 - `"interactOutside"`: 외부 영역 클릭 ```tsx import { IconEyeSlashLine } from "@karrotmarket/react-monochrome-icon"; import { HStack, Text, VStack } from "@seed-design/react"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { MenuSheetContent, MenuSheetGroup, MenuSheetItem, MenuSheetRoot, MenuSheetTrigger, } from "seed-design/ui/menu-sheet"; export default function MenuSheetOnOpenChangeReason() { const [open, setOpen] = useState(false); const [openReason, setOpenReason] = useState(null); const [closeReason, setCloseReason] = useState(null); return ( { setOpen(open); (open ? setOpenReason : setCloseReason)(details?.reason ?? null); }} > 열기 } /> } /> } /> 마지막 열림 이유: {openReason ?? "-"} 마지막 닫힘 이유: {closeReason ?? "-"} ); } ``` ### Skip Animation `skipAnimation` prop을 사용하여 MenuSheet의 enter/exit 애니메이션을 건너뛸 수 있습니다. ```tsx import { IconEyeSlashLine } from "@karrotmarket/react-monochrome-icon"; import { ActionButton } from "seed-design/ui/action-button"; import { MenuSheetContent, MenuSheetGroup, MenuSheetItem, MenuSheetRoot, MenuSheetTrigger, } from "seed-design/ui/menu-sheet"; const MenuSheetSkipAnimation = () => { return ( Open } /> } /> } /> ); }; export default MenuSheetSkipAnimation; ``` --- file: components/(deprecated)/stack.mdx # Stack 세로로 쌓이는 레이아웃을 구성합니다. 디자인 토큰을 JSX에서 사용할 수 있도록 도와줍니다. 더 이상 사용되지 않습니다. [`VStack`](/react/components/layout/v-stack) 컴포넌트를 사용하세요. ## Preview ```tsx import { Box, Stack } from "@seed-design/react"; /** * @deprecated Use `VStack` instead. */ export default function StackPreview() { return ( 1 2 3 ); } ``` ## Usage ```tsx import { Stack } from "@seed-design/react"; ``` ```tsx ``` ## Props --- file: components/(foundation)/iconography/composition.mdx # Composition 아이콘을 사용하는 방법을 설명합니다. ## 개요 [당근 아이콘 라이브러리](/foundations/iconography/library)는 디자인 시스템에 독립적(agnostic)으로 설계되었으며 SEED의 의견이 포함되어 있지 않습니다. 때로는 라이브러리에 없는 커스텀 아이콘을 사용하면서도 아이콘 합성 패턴을 활용해야 하는 경우가 있습니다. 이를 위해 `svg` 속성을 통해 아이콘을 받아 디자인 시스템에 연결하는 유틸리티 컴포넌트를 제공합니다: - `Icon`: 아이콘을 독립적으로 사용하거나 아이콘만 표시하는 컴포넌트에서 사용합니다. - `PrefixIcon`, `SuffixIcon`: 컴포넌트 내에 아이콘과 텍스트를 함께 배치할 때 사용합니다. ## Icon 컴포넌트 `Icon` 컴포넌트는 아이콘을 독립적으로 사용하거나 아이콘만 표시하는 컴포넌트에서 사용합니다. ```jsx import { Icon } from "@seed-design/react"; import { IconHeartFill } from "@karrotmarket/react-monochrome-icon"; // 독립형 아이콘 } /> // 아이콘만 표시하는 버튼 } /> ``` ## PrefixIcon과 SuffixIcon 컴포넌트 내에 아이콘과 텍스트를 함께 배치할 때는 `PrefixIcon`, `SuffixIcon` 컴포넌트를 children으로 배치합니다. ```jsx import { PrefixIcon, SuffixIcon } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { IconPlusFill, IconChevronRightFill } from "@karrotmarket/react-monochrome-icon"; // 앞에 아이콘 배치 } /> 라벨 // 뒤에 아이콘 배치 라벨 } /> // 아이콘 양쪽에 배치 } /> 라벨 } /> ``` 이 패턴은 다음과 같은 컴포넌트에서 일관되게 사용됩니다: - `ActionButton`: 버튼 내 아이콘 배치 - `Button`: 일반 버튼 내 아이콘 배치 - `ToggleButton`: 토글 버튼 내 아이콘 배치 - `ActionChip`: 액션 칩 내 아이콘 배치 - `ControlChip`: 컨트롤 칩 내 아이콘 배치 --- file: components/(foundation)/iconography/library.mdx # Library 리액트 아이콘 패키지는 아이콘을 사용할 때 필요한 컴포넌트를 제공합니다. ## Overview 아이콘 라이브러리에 포함된 아이콘을 확인합니다. ## Installation - npm: npm install @karrotmarket/react-monochrome-icon @karrotmarket/react-multicolor-icon - pnpm: pnpm add @karrotmarket/react-monochrome-icon @karrotmarket/react-multicolor-icon - yarn: yarn add @karrotmarket/react-monochrome-icon @karrotmarket/react-multicolor-icon - bun: bun add @karrotmarket/react-monochrome-icon @karrotmarket/react-multicolor-icon --- file: components/(foundation)/layout/box.mdx # Box Box 컴포넌트는 가장 기초적인 레이아웃 컴포넌트입니다. 디자인 토큰을 JSX에서 사용할 수 있도록 도와줍니다. ## Preview ```tsx import { Box } from "@seed-design/react"; export default function BoxPreview() { return ( Box Example ); } ``` ## Usage ```tsx import { Box } from "@seed-design/react"; ``` ```tsx ``` ## Props ## Examples ### Gradient ```tsx import { Box, VStack } from "@seed-design/react"; export default function BoxGradient() { return ( Box Gradient Box Gradient ); } ``` ## Responsive Design Box의 레이아웃 프로퍼티에 반응형 값을 전달하여 viewport 크기에 따라 스타일을 조정할 수 있습니다. --- file: components/(foundation)/layout/flex.mdx # Flex Flex 컴포넌트는 flexbox를 사용하며 디자인 토큰을 JSX에서 사용할 수 있도록 도와줍니다. ## Preview ```tsx import { Flex } from "@seed-design/react"; export default function FlexPreview() { return ( 1 2 3 4 5 ); } ``` ## Props ## Responsive Design Flex의 레이아웃 프로퍼티에 반응형 값을 전달하여 viewport 크기에 따라 스타일을 조정할 수 있습니다. --- file: components/(foundation)/layout/float.mdx # Float Float 컴포넌트는 특정 위치에 고정된 요소를 배치할 때 사용합니다. ## Preview ```tsx import { Box, Float } from "@seed-design/react"; import { ContextualFloatingButton } from "seed-design/ui/contextual-floating-button"; export default function FloatPreview() { return ( Top Start Top Center Top End Middle Start Middle Center Middle End Bottom Start Bottom Center Bottom End ); } ``` ## Props ## Examples ### Offset X `offsetX` 속성을 사용하면 좌우 방향으로 위치를 조정할 수 있습니다. `start`, `middle`에서는 우측 방향으로 조정되고, `end`에서는 좌측 방향으로 조정됩니다. ```tsx import { Box, Float } from "@seed-design/react"; import { ContextualFloatingButton } from "seed-design/ui/contextual-floating-button"; export default function FloatOffsetX() { return ( Middle Start Middle Center Middle End ); } ``` ### Offset Y `offsetY` 속성을 사용하면 상하 방향으로 위치를 조정할 수 있습니다. `top`, `middle`에서는 하측 방향으로 조정되고, `bottom`에서는 상측 방향으로 조정됩니다. ```tsx import { Box, Float } from "@seed-design/react"; import { ContextualFloatingButton } from "seed-design/ui/contextual-floating-button"; export default function FloatOffsetY() { return ( Top Center Middle Center Bottom Center ); } ``` --- file: components/(foundation)/layout/grid.mdx # Grid Grid 컴포넌트는 CSS Grid를 사용하며 디자인 토큰을 JSX에서 사용할 수 있도록 도와줍니다. ## Preview ```tsx import { Flex, Grid } from "@seed-design/react"; export default function GridPreview() { return ( {Array.from({ length: 6 }).map((_, index) => ( {index + 1} ))} ); } ``` ## Props ### `Grid` ### `GridItem` `GridItem`은 Grid 컨테이너 내에서 아이템의 배치를 제어하는 컴포넌트입니다. Grid 내부의 모든 아이템이 `GridItem`일 필요는 없습니다. ## Columns and Rows ``의 `columns` 또는 `rows` prop에 number를 지정하여 `grid-template-columns` 또는 `grid-template-rows`를 `repeat(${columns|rows}, minmax(0, 1fr))`로 설정할 수 있습니다. ```tsx import { Divider, Flex, Grid, HStack } from "@seed-design/react"; export default function GridNumber() { return ( {[1, 2, 3, 4, 5].map((n) => ( {n} ))} {[1, 2, 3, 4, 5].map((n) => ( {n} ))} ); } ``` ``의 `columns` 또는 `rows`에 `grid-template-columns` 또는 `grid-template-rows` 값을 직접 지정할 수도 있습니다. ```tsx import { Divider, Flex, Grid, HStack } from "@seed-design/react"; export default function GridString() { return ( {[1, 2, 3, 4, 5].map((n) => ( {n} ))} {[1, 2, 3, 4, 5].map((n) => ( {n} ))} ); } ``` ## Spanning Items ``을 활용하여 그리드 아이템이 여러 열이나 행을 차지하도록 할 수 있습니다. - `colSpan` 또는 `colStart`, `colEnd` prop으로 열 span을 지정합니다. `colSpan="full"`을 전달하면 행 전체 (좌우 양 끝)을 차지합니다. - `rowSpan` 또는 `rowStart`, `rowEnd` prop으로 행 span을 지정합니다. `rowSpan="full"`을 전달하면 열 전체 (상하 양 끝)을 차지합니다. ```tsx import { Divider, Grid, GridItem, HStack } from "@seed-design/react"; export default function Spanning() { return ( 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 ); } ``` ## Auto Flow ``의 `autoFlow` prop을 사용하여 아이템이 배치되는 방향을 지정할 수 있습니다. ```tsx import { useState } from "react"; import { Grid, type GridProps, GridItem, VStack } from "@seed-design/react"; import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control"; type AutoFlow = NonNullable; export default function AutoFlow() { const [autoFlow, setAutoFlow] = useState("row"); const isColumn = autoFlow.startsWith("column"); const color = isColumn ? "green" : "purple"; const gridProps = isColumn ? { rows: 3 } : { columns: 3 }; const spanProps = isColumn ? { rowSpan: 2 } : { colSpan: 2 }; return ( {[1, 2].map((n) => ( {n} ))} {[3, 4, 5].map((n) => ( {n} ))} setAutoFlow(value as AutoFlow)} aria-label="Auto Flow" > row row dense column column dense ); } ``` ## Auto Rows / Columns ``의 `autoRows` 또는 `autoColumns` prop을 사용하여 암시적으로 생성되는 행이나 열의 크기를 지정할 수 있습니다. 동적으로 아이템이 추가되는 그리드에서 유용합니다. ```tsx import { Divider, Flex, Grid, HStack } from "@seed-design/react"; export default function AutoRowsColumns() { return ( {[1, 2, 3, 4, 5].map((n) => ( {n === 2 ? "Ea anim non aute minim ea deserunt enim Elit deserunt laborum et quis sit." : n} ))} {[1, 2, 3, 4, 5].map((n) => ( {n === 2 ? "Ea anim non aute minim ea deserunt enim Elit deserunt laborum et quis sit." : n} ))} ); } ``` ## Using `asChild` Prop `GridItem`은 `asChild` prop을 사용하여 자식 요소에 직접 grid 속성을 적용할 수 있습니다. `asChild` prop에 대해 자세히 알아봅니다. ```tsx Link spanning 2 columns ``` ## Using Box for Grid Item Placement `GridItem`은 내부적으로 `colSpan`, `colStart`, `colEnd`, `rowSpan`, `rowStart`, `rowEnd` prop을 `gridColumn` 및 `gridRow` 스타일로 변환하여 `Box`에 적용합니다. 따라서 `Box` 컴포넌트에서 `gridColumn`, `gridRow` prop을 직접 사용할 수도 있습니다. ```tsx colSpan=2 colSpan=full colStart=2 colEnd=4 ``` ## Responsive Design Grid의 `columns`, `rows`, `gap` 등에 반응형 값을 전달하여 viewport 크기에 따라 레이아웃을 조정할 수 있습니다. --- file: components/(foundation)/layout/h-stack.mdx # HStack 가로로 쌓이는 레이아웃을 구성합니다. 디자인 토큰을 JSX에서 사용할 수 있도록 도와줍니다. ## Preview ```tsx import { Box, HStack } from "@seed-design/react"; export default function HStackPreview() { return ( 1 2 3 ); } ``` ## Usage ```tsx import { HStack } from "@seed-design/react"; ``` ```tsx ``` ## Props ## Responsive Design HStack의 레이아웃 프로퍼티에 반응형 값을 전달하여 viewport 크기에 따라 스타일을 조정할 수 있습니다. --- file: components/(foundation)/layout/v-stack.mdx # VStack 세로로 쌓이는 레이아웃을 구성합니다. 디자인 토큰을 JSX에서 사용할 수 있도록 도와줍니다. ## Preview ```tsx import { Box, VStack } from "@seed-design/react"; export default function VStackPreview() { return ( 1 2 3 ); } ``` ## Usage ```tsx import { VStack } from "@seed-design/react"; ``` ```tsx ``` ## Props ## Responsive Design VStack의 레이아웃 프로퍼티에 반응형 값을 전달하여 viewport 크기에 따라 스타일을 조정할 수 있습니다. --- file: components/(foundation)/typography/text.mdx # Text 텍스트를 표시하는 기본 타이포그래피 컴포넌트입니다. 일관된 글꼴 스타일과 크기를 적용하여 텍스트를 렌더링할 때 사용됩니다. ## Preview ```tsx import { Flex, Text } from "@seed-design/react"; export default function TextPreview() { return ( t1Regular t2Regular t3Regular t4Regular t5Regular t6Bold t7Bold t8Bold t9Bold t10Bold t11Bold t12Bold t13Bold t14Bold ); } ``` ## Usage ```tsx import { Text } from "@seed-design/react"; ``` ```tsx ``` ## Props ## Examples ### Text Styles Figma의 Text Style과 대응되는 `textStyle` 속성을 사용하는 것이 기본 방법입니다. `textStyle`은 글꼴 크기, 줄 간격, 글꼴 굵기를 한 번에 설정합니다. ```tsx import { Flex, Text } from "@seed-design/react"; export default function TextTextStyles() { return ( t1Regular t2Regular t3Regular t4Regular t5Regular t6Bold t7Bold t8Bold t9Bold t10Bold ); } ``` #### Font Sizes 더 구체적으로 변경이 필요한 경우 `fontSize` 및 `lineHeight` 속성을 각각 사용할 수 있습니다. `lineHeight` 속성은 `fontSize`에 대응되는 기본값으로 설정되며, `lineHeight` 속성으로 변경이 가능합니다. ```tsx import { Flex, Text } from "@seed-design/react"; export default function TextFontSizes() { return ( t1 t2 t3 t4 t5 t6 t7 t8 t9 t10 ); } ``` #### Font Weights 더 구체적으로 변경이 필요한 경우 `fontWeight` 속성을 사용할 수 있습니다. ```tsx import { Flex, Text } from "@seed-design/react"; export default function TextFontWeights() { return ( regular medium bold ); } ``` ### Text Decoration Lines `textDecorationLine` 속성을 사용하여 텍스트에 밑줄 또는 취소선을 추가할 수 있습니다. - 링크가 아닌 텍스트를 강조하기 위해 밑줄을 적용할 때는 사용자 경험을 고려해야 합니다. 밑줄이 있는 텍스트는 링크로 인식될 수 있으므로, 혼동을 피하기 위해 적절한 상황에서만 사용해야 합니다. - 본문 밖 영역에서 인라인 텍스트를 링크 용도로 사용하고자 하는 경우 [ActionButton](/react/components/action-button)을 `variant="ghost" bleedX="asPadding" bleedY="asPadding"` 옵션으로 사용하는 것을 고려해보세요. ```tsx import { Flex, Text } from "@seed-design/react"; export default function TextTextDecorationLines() { return ( underline line-through ); } ``` ### Max Lines `maxLines` 속성을 사용하여 텍스트가 차지하는 최대 줄 수를 제한할 수 있습니다. 지정된 줄 수를 초과하는 텍스트는 생략 부호(ellipsis) `…` 로 표시됩니다. ```tsx import { Flex, Text } from "@seed-design/react"; export default function TextMaxLines() { return ( maxLines=1 Aliquip pariatur adipisicing elit consectetur velit commodo Lorem nulla eu. Occaecat sint voluptate ut dolore eiusmod minim qui reprehenderit. Do aliquip tempor ipsum aliqua enim. Incididunt irure do ullamco esse sit enim mollit nisi anim laboris do. Ut fugiat aliquip velit eiusmod ad incididunt. Consequat qui quis in ad culpa officia eu in Lorem. Elit voluptate est veniam aliqua magna Lorem proident incididunt amet aliquip. Aliquip sint sit ex eiusmod sint. Eiusmod incididunt consequat fugiat. maxLines=2 Aliquip pariatur adipisicing elit consectetur velit commodo Lorem nulla eu. Occaecat sint voluptate ut dolore eiusmod minim qui reprehenderit. Do aliquip tempor ipsum aliqua enim. Incididunt irure do ullamco esse sit enim mollit nisi anim laboris do. Ut fugiat aliquip velit eiusmod ad incididunt. Consequat qui quis in ad culpa officia eu in Lorem. Elit voluptate est veniam aliqua magna Lorem proident incididunt amet aliquip. Aliquip sint sit ex eiusmod sint. Eiusmod incididunt consequat fugiat. maxLines=3 Aliquip pariatur adipisicing elit consectetur velit commodo Lorem nulla eu. Occaecat sint voluptate ut dolore eiusmod minim qui reprehenderit. Do aliquip tempor ipsum aliqua enim. Incididunt irure do ullamco esse sit enim mollit nisi anim laboris do. Ut fugiat aliquip velit eiusmod ad incididunt. Consequat qui quis in ad culpa officia eu in Lorem. Elit voluptate est veniam aliqua magna Lorem proident incididunt amet aliquip. Aliquip sint sit ex eiusmod sint. Eiusmod incididunt consequat fugiat. ); } ``` ### User Select `userSelect` 속성을 사용하여 사용자가 텍스트를 선택할 수 있는지 여부를 제어할 수 있습니다. ```tsx import { Flex, Text } from "@seed-design/react"; export default function TextUserSelect() { return ( auto none text ); } ``` ### Static 폰트 스케일링에 반응하지 않는 static 토큰을 활용하는 텍스트 스타일을 적용할 수 있습니다. ```tsx import { Flex, Text } from "@seed-design/react"; export default function TextStatic() { return ( t1StaticRegular t2StaticRegular t3StaticRegular t4StaticRegular t5StaticRegular t6StaticBold t7StaticBold t8StaticBold t9StaticBold t10StaticBold ); } ``` ### White Space `whiteSpace` prop은 `maxLines` prop을 사용하지 않을 때만 적용됩니다. ```tsx import { Box, Text, VStack } from "@seed-design/react"; const sampleText = `이것은 여러 개 의 공백, 줄바꿈과 들여쓰기를 포함한 샘플 텍스트입니다. 각각의 white-space 속성\n값이 어떻게 작동하는지 보여줍니다.`; export default function TextWhiteSpace() { return ( {(["normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"] as const).map( (value) => ( {value} {sampleText} ), )} ); } ``` --- file: components/accordion.mdx # Accordion 여러 개의 관련된 콘텐츠 섹션을 수직으로 나열하고, 각 섹션을 펼치거나 접어 정보를 탐색할 수 있는 컴포넌트입니다. 사용 가능 버전: @seed-design/react@2.0.0, @seed-design/css@2.0.0 ## Preview ```tsx import { Box } from "@seed-design/react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "seed-design/ui/accordion"; export default function AccordionPreview() { return (

첫 번째 항목의 내용입니다.

두 번째 항목의 내용입니다.

세 번째 항목의 내용입니다.

); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:accordion - pnpm: pnpm dlx @seed-design/cli@latest add ui:accordion - yarn: yarn dlx @seed-design/cli@latest add ui:accordion - bun: bun x @seed-design/cli@latest add ui:accordion ## Props ### `Accordion` ### `AccordionItem` ### `AccordionTrigger` ### `AccordionContent` ## Examples ### Inline Variant 기본 variant입니다. 아이템이 연속된 흐름으로 제공되며, 아이템 사이에 구분선이 표시됩니다. ```tsx import { Box } from "@seed-design/react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "seed-design/ui/accordion"; export default function AccordionInline() { return (

첫 번째 항목의 내용입니다.

두 번째 항목의 내용입니다.

세 번째 항목의 내용입니다.

); } ``` ### Separated `variant="separated"` 를 사용하면 각 항목이 분리된 카드 형태로 표시됩니다. ```tsx import { Box } from "@seed-design/react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "seed-design/ui/accordion"; export default function AccordionSeparated() { return (

첫 번째 항목의 내용입니다.

두 번째 항목의 내용입니다.

세 번째 항목의 내용입니다.

); } ``` ### Multiple 기본적으로 한 번에 하나의 항목만 펼칠 수 있습니다. `multiple` prop을 사용하면 여러 항목을 동시에 펼칠 수 있습니다. ```tsx import { Box } from "@seed-design/react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "seed-design/ui/accordion"; export default function AccordionMultiple() { return (

여러 항목을 동시에 펼칠 수 있습니다.

각 항목은 다른 항목과 독립적으로 열고 닫힙니다.

세 번째 항목의 내용입니다.

); } ``` ### Always one open `values`와 `onValuesChange`를 사용해 controlled 패턴으로 운영하면, 빈 배열이 들어올 때 setter를 호출하지 않는 가드만 추가하여 항상 하나의 항목이 열려 있도록 강제할 수 있습니다. ```tsx import { Box } from "@seed-design/react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "seed-design/ui/accordion"; import { useState } from "react"; export default function AccordionControlledRequiredOpen() { const [values, setValues] = useState(["item-1"]); return ( { if (next.length === 0) return; setValues(next); }} >

현재 항목은 다시 눌러도 닫히지 않고, 다른 항목을 선택할 때만 전환됩니다.

평일 오후 2시 이전 주문은 당일 출고되며, 주말 주문은 다음 영업일에 출고됩니다.

수령 후 7일 이내에 교환 또는 반품을 요청할 수 있습니다.

); } ``` ### Size `size`로 Accordion의 크기를 정합니다. (default: `medium`) `responsive`는 화면 너비에 따라 size가 자동으로 전환되는 값입니다. 여러 화면 너비를 함께 지원하는 제품에서 `size=responsive`를 사용하여 대응합니다. ```tsx import { Box, VStack } from "@seed-design/react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "seed-design/ui/accordion"; export default function AccordionSize() { return ( 항목의 내용입니다. 항목의 내용입니다. 항목의 내용입니다. ); } ``` ### Prefix `prefix` prop에 아이콘 같은 앞쪽 요소를 전달할 수 있습니다. ```tsx import { Box, Icon } from "@seed-design/react"; import { IconCardLine, IconQuestionmarkCircleLine, IconTruckLine, } from "@karrotmarket/react-monochrome-icon"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "seed-design/ui/accordion"; export default function AccordionWithPrefixIcon() { return ( } />} title="배송 방법" />

일반 배송, 빠른 배송, 방문 수령 중 주문 상황에 맞는 방법을 선택할 수 있습니다.

} />} title="결제 및 쿠폰" />

카드, 간편결제, 보유 쿠폰을 한 번에 확인하고 결제에 적용할 수 있습니다.

} />} title="문의와 환불" />

주문 취소 가능 시간, 환불 소요 기간, 고객센터 문의 방법을 확인할 수 있습니다.

); } ``` ### Description `description` prop으로 트리거에 부가 설명을 추가할 수 있습니다. ```tsx import { Box } from "@seed-design/react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "seed-design/ui/accordion"; export default function AccordionWithDescription() { return (

첫 번째 항목의 내용입니다.

두 번째 항목의 내용입니다.

세 번째 항목의 내용입니다.

); } ``` ### Disabled `disabled` prop으로 전체 또는 개별 항목을 비활성화할 수 있습니다. - `Accordion`에 `disabled`를 설정하면 모든 항목이 비활성화됩니다. - `AccordionItem`에 `disabled`를 설정하면 해당 항목만 비활성화됩니다. ```tsx import { Box } from "@seed-design/react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "seed-design/ui/accordion"; export default function AccordionDisabled() { return (

이 항목은 활성화 상태입니다.

이 항목은 비활성화 상태입니다.

이 항목은 활성화 상태입니다.

); } ``` ### Controlled `values`와 `onValuesChange`를 사용하여 열림 상태를 직접 제어할 수 있습니다. ```tsx import { Box } from "@seed-design/react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "seed-design/ui/accordion"; import { useState } from "react"; export default function AccordionControlled() { const [values, setValues] = useState(["item-1"]); return (

첫 번째 항목의 내용입니다.

두 번째 항목의 내용입니다.

세 번째 항목의 내용입니다.

); } ``` ### Value Array Changes controlled 모드에서는 현재 열려 있는 항목이 `values` 배열로 전달됩니다. 아래 예시는 트리거를 누를 때마다 최신 `values`와 최근 `onValuesChange` 결과를 함께 보여줍니다. ```tsx import { Box, Text, VStack } from "@seed-design/react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "seed-design/ui/accordion"; import { useState } from "react"; const DEFAULT_VALUES = ["shipping"]; export default function AccordionValueChanges() { const [values, setValues] = useState(DEFAULT_VALUES); const [history, setHistory] = useState([DEFAULT_VALUES]); return ( { setValues(nextValues); setHistory((prev) => [nextValues, ...prev].slice(0, 5)); }} >

빠른 배송, 새벽 배송, 방문 수령 옵션을 비교할 수 있습니다.

카드, 계좌이체, 간편결제 중에서 원하는 결제 수단을 선택할 수 있습니다.

주문 취소 가능 시간과 환불 소요 기간을 확인할 수 있습니다.

values: {JSON.stringify(values)} onValuesChange history: {history.map((snapshot, index) => ( {index + 1}. {JSON.stringify(snapshot)} ))}
); } ``` ### Default Expanded `defaultValues`를 사용하여 초기 열림 상태를 지정할 수 있습니다. ```tsx import { Box } from "@seed-design/react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "seed-design/ui/accordion"; export default function AccordionDefaultExpanded() { return (

첫 번째 항목은 기본으로 펼쳐진 상태입니다.

두 번째 항목의 내용입니다.

세 번째 항목의 내용입니다.

); } ``` ### Custom Content `AccordionContent`는 열림/닫힘 애니메이션 컨테이너 역할만 합니다. 기본 패딩, 배경색, 테두리, 타이포그래피 스타일은 제공하지 않으므로 내부 콘텐츠에서 직접 구성해야 합니다. 아래 예시처럼 `Box`로 패딩과 배경을 명시적으로 주는 패턴을 권장합니다. ```tsx import { Box, Text, VStack } from "@seed-design/react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "seed-design/ui/accordion"; export default function AccordionCustomContent() { return ( 일반 배송 주문 후 영업일 기준 2-3일 내에 배송됩니다. 제주 및 도서산간 지역은 1-2일이 추가 소요될 수 있습니다. 1. 고객센터로 반품/교환 요청 2. 상품 수거 (택배 방문 수거) 3. 검수 후 환불 또는 교환 처리 ); } ``` ## Accessibility [WAI-ARIA Accordion Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/)을 따릅니다. `AccordionTrigger`는 내부적으로 `heading > button` 구조를 구성합니다. 기본 heading level은 `h3`이며, 문서 구조에 맞춰 다른 level이 필요하면 `headingLevel` prop으로 조정할 수 있습니다. WAI-ARIA APG 예시에서도 상위 섹션 구조에 맞춰 `h3`를 사용합니다. - Pattern: [WAI-ARIA APG Accordion Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/) - Example: [WAI-ARIA APG Accordion Example](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/examples/accordion/) ### Heading Level Escape Hatch 상위 섹션 heading이 이미 존재한다면 `headingLevel`로 accordion header의 level을 맞춰 주세요. 예를 들어 accordion이 `h3` 섹션 안에 들어간다면 각 항목 header는 `h4`로 내리는 식으로 문서 outline을 유지할 수 있습니다. ```tsx import { Box, Text, VStack } from "@seed-design/react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "seed-design/ui/accordion"; export default function AccordionHeadingLevel() { return ( 주문 도움말

배송 관련 세부 내용은 h4 heading 아래의 accordion section으로 제공할 수 있습니다.

); } ``` ### 키보드 인터랙션 | 키 | 동작 | | ----------------- | --------------------------------------- | | `Enter` / `Space` | 포커스된 트리거의 패널을 펼치거나 접습니다. | | `Tab` | 다음 포커스 가능한 요소로 이동합니다. | | `Shift + Tab` | 이전 포커스 가능한 요소로 이동합니다. | | `ArrowDown` | 다음 트리거로 포커스를 이동합니다. 마지막이면 첫 번째로 순환합니다. | | `ArrowUp` | 이전 트리거로 포커스를 이동합니다. 첫 번째이면 마지막으로 순환합니다. | | `Home` | 첫 번째 트리거로 포커스를 이동합니다. | | `End` | 마지막 트리거로 포커스를 이동합니다. | --- file: components/action-button.mdx # Action Button 명확한 액션을 쉽게 수행할 수 있도록 돕는 기본 인터랙션 컴포넌트입니다. 사용 가능 버전: @seed-design/react@0.0.1, @seed-design/css@0.0.1 ## Preview ```tsx import { ActionButton } from "seed-design/ui/action-button"; export default function ActionButtonPreview() { return 라벨; } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:action-button - pnpm: pnpm dlx @seed-design/cli@latest add ui:action-button - yarn: yarn dlx @seed-design/cli@latest add ui:action-button - bun: bun x @seed-design/cli@latest add ui:action-button ## Props ## Examples ### Brand Solid ```tsx import { ActionButton } from "seed-design/ui/action-button"; export default function ActionButtonBrandSolid() { return 라벨; } ``` ### Neutral Solid ```tsx import { ActionButton } from "seed-design/ui/action-button"; export default function ActionButtonNeutralSolid() { return 라벨; } ``` ### Neutral Weak ```tsx import { ActionButton } from "seed-design/ui/action-button"; export default function ActionButtonNeutralWeak() { return 라벨; } ``` ### Critical Solid ```tsx import { ActionButton } from "seed-design/ui/action-button"; export default function ActionButtonCriticalSolid() { return 라벨; } ``` ### Brand Outline ```tsx import { ActionButton } from "seed-design/ui/action-button"; export default function ActionButtonBrandOutline() { return 라벨; } ``` ### Neutral Outline ```tsx import { ActionButton } from "seed-design/ui/action-button"; export default function ActionButtonNeutralOutline() { return 라벨; } ``` ### Ghost Ghost variant는 `color` 속성을 사용해 레이블과 아이콘의 색상을, `fontWeight` 속성을 사용해 글꼴의 굵기를 변경할 수 있습니다. ```tsx import { HStack, PrefixIcon, VStack } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { IconTagFill } from "@karrotmarket/react-monochrome-icon"; export default function ActionButtonGhost() { return ( } /> Default (fg.neutral) } /> Neutral Subtle } /> Brand Default (Bold) Medium Regular ); } ``` ### Icon Only ```tsx import { IconPlusFill } from "@karrotmarket/react-monochrome-icon"; import { Icon } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; export default function ActionButtonIconOnly() { return ( } /> ); } ``` ### Prefix Icon ```tsx import { IconPlusFill } from "@karrotmarket/react-monochrome-icon"; import { PrefixIcon } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; export default function ActionButtonPrefixIcon() { return ( } /> 라벨 ); } ``` ### Suffix Icon ```tsx import { IconChevronRightFill } from "@karrotmarket/react-monochrome-icon"; import { SuffixIcon } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; export default function ActionButtonSuffixIcon() { return ( 라벨 } /> ); } ``` ### Disabled ```tsx import { ActionButton } from "seed-design/ui/action-button"; export default function ActionButtonDisabled() { return 라벨; } ``` ### Loading ```tsx import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; export default function ActionButtonLoading() { const [loading, setLoading] = useState(false); function handleClick() { setLoading(true); setTimeout(() => setLoading(false), 2000); } // 이벤트 핸들링이 필요할 수 있으므로 loading은 disabled를 포함하지 않습니다. 이벤트 발생을 원하지 않는 경우, disabled 속성을 추가해주세요. return ( 시간이 걸리는 액션 ); } ``` ### Bleed `bleedX`, `bleedY` 속성을 사용해 버튼이 레이아웃에서 "빠져나오게" 할 수 있습니다. Ghost variant를 시각적으로 정렬해야 할 때 유용합니다. ```tsx import { HStack, Text, VStack } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; export default function ActionButtonBleed() { return ( Bleed Example Bleed Y Bleed Example Bleed all sides ); } ``` --- file: components/alert-dialog.mdx # Alert Dialog 사용자의 확인이 반드시 필요한 경우 강력한 표현 및 경고 수단으로 활용하는 컴포넌트입니다. 사용 가능 버전: @seed-design/react@0.0.1, @seed-design/css@0.0.1 ## Preview ```tsx import { ResponsivePair } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogRoot, AlertDialogTitle, AlertDialogTrigger, } from "seed-design/ui/alert-dialog"; const AlertDialogSingle = () => { return ( // You can set z-index dialog with "--layer-index" custom property. useful for stackflow integration. 열기 주의 이 작업은 되돌릴 수 없습니다. 취소 확인 ); }; export default AlertDialogSingle; ``` Stackflow와 Alert Dialog를 함께 사용하는 방법에 대해 알아보세요. ## Installation - npm: npx @seed-design/cli@latest add ui:alert-dialog - pnpm: pnpm dlx @seed-design/cli@latest add ui:alert-dialog - yarn: yarn dlx @seed-design/cli@latest add ui:alert-dialog - bun: bun x @seed-design/cli@latest add ui:alert-dialog ## Props ### `AlertDialogRoot` ### `AlertDialogTrigger` ### `AlertDialogContent` ### `AlertDialogHeader` ### `AlertDialogTitle` ### `AlertDialogDescription` ### `AlertDialogFooter` ## Examples ### Trigger ``는 `aria-haspopup="dialog"` 속성을 설정하고, AlertDialog의 `open` 상태에 따라 `aria-expanded` 속성을 자동으로 설정합니다. 이 속성은 스크린 리더와 같은 보조 기술에 유용합니다. ## Preview ```tsx import { ResponsivePair } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogRoot, AlertDialogTitle, AlertDialogTrigger, } from "seed-design/ui/alert-dialog"; const AlertDialogSingle = () => { return ( // You can set z-index dialog with "--layer-index" custom property. useful for stackflow integration. 열기 주의 이 작업은 되돌릴 수 없습니다. 취소 확인 ); }; export default AlertDialogSingle; ``` ### Responsive Wrapping `` 컴포넌트를 사용해 버튼 컨텐츠가 길어지는 경우 레이아웃을 세로로 접을 수 있습니다. ```tsx import { PrefixIcon, ResponsivePair } from "@seed-design/react"; import { IconCheckFill } from "@seed-design/react-icon"; import { ActionButton } from "seed-design/ui/action-button"; import { AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogRoot, AlertDialogTitle, AlertDialogTrigger, } from "seed-design/ui/alert-dialog"; const AlertDialogWrap = () => { return ( // You can set z-index dialog with "--layer-index" custom property. useful for stackflow integration. 열기 Wrapping ResponsivePair 컴포넌트를 사용해 버튼 컨텐츠가 길어지는 경우 레이아웃을 세로로 접을 수 있습니다. 취소 } />긴 레이블 예시 ); }; export default AlertDialogWrap; ``` ### Single Action ```tsx import { ActionButton } from "seed-design/ui/action-button"; import { AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogRoot, AlertDialogTitle, AlertDialogTrigger, } from "seed-design/ui/alert-dialog"; const AlertDialogSingle = () => { // You can set z-index dialog with "--layer-index" custom property. useful for stackflow integration. return ( 열기 제목 단일 선택지를 제공합니다. 확인 ); }; export default AlertDialogSingle; ``` ### Neutral Secondary Action ```tsx import { ResponsivePair } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogRoot, AlertDialogTitle, AlertDialogTrigger, } from "seed-design/ui/alert-dialog"; const AlertDialogNeutral = () => { return ( // You can set z-index dialog with "--layer-index" custom property. useful for stackflow integration. 열기 제목 중립적인 선택지를 제공합니다. {/* ResponsivePair component wraps layout if button content is too long. */} 취소 확인 ); }; export default AlertDialogNeutral; ``` ### Nonpreferred ```tsx import { VStack } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogRoot, AlertDialogTitle, AlertDialogTrigger, } from "seed-design/ui/alert-dialog"; const AlertDialogNonpreferred = () => { return ( // You can set z-index dialog with "--layer-index" custom property. useful for stackflow integration. 열기 제목 중립적인 선택지를 제공합니다. 라벨 라벨 ); }; export default AlertDialogNonpreferred; ``` ### Critical Action ```tsx import { ResponsivePair } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogRoot, AlertDialogTitle, AlertDialogTrigger, } from "seed-design/ui/alert-dialog"; const AlertDialogCritical = () => { return ( // You can set z-index dialog with "--layer-index" custom property. useful for stackflow integration. 열기 제목 파괴적, 비가역적 작업을 경고합니다. {/* ResponsivePair component wraps layout if button content is too long. */} 취소 확인 ); }; export default AlertDialogCritical; ``` ### Controlled Trigger 외의 방식으로 AlertDialog를 열고 닫을 수 있습니다. 이 경우 `open` prop을 사용하여 AlertDialog의 상태를 제어합니다. ```tsx import { ResponsivePair } from "@seed-design/react"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogRoot, AlertDialogTitle, } from "seed-design/ui/alert-dialog"; const AlertDialogControlled = () => { const [open, setOpen] = useState(false); return ( <> setOpen(true)}> 열기 주의 이 작업은 되돌릴 수 없습니다. setOpen(false)}> 취소 setOpen(false)}> 확인 ); }; export default AlertDialogControlled; ``` ### Prevent Close `AlertDialogAction`의 `onClick`에서 `e.preventDefault()`를 호출하면 다이얼로그가 닫히지 않습니다. ```tsx import { Box, VStack } from "@seed-design/react"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogRoot, AlertDialogTitle, AlertDialogTrigger, } from "seed-design/ui/alert-dialog"; import { Switch } from "seed-design/ui/switch"; export default function AlertDialogPreventClose() { const [preventClose, setPreventClose] = useState(true); return ( 열기 닫기 방지 확인 버튼을 눌러도 다이얼로그가 닫히지 않도록 설정할 수 있습니다. { if (preventClose) { e.preventDefault(); } }} > 확인 ); } ``` ### `onOpenChange` Details `onOpenChange` 두 번째 인자로 `details`가 제공됩니다. #### `reason` **열릴 때** (`open: true`) - `"trigger"`: `AlertDialogTrigger` (`Dialog.Trigger`)로 열림 **닫힐 때** (`open: false`) - `"closeButton"`: `AlertDialogAction`으로 닫힘 - `"escapeKeyDown"`: ESC 키 사용 - `"interactOutside"`: 외부 영역 클릭 - `AlertDialogRoot`는 기본적으로 `closeOnInteractOutside={false}`입니다. `interactOutside`는 이 옵션을 `true`로 설정한 경우에만 발생할 수 있습니다. - `"cascadeDismiss"`: 상위 레이어 닫힘으로 인한 연쇄 닫힘 ```tsx import { HStack, ResponsivePair, Text, VStack } from "@seed-design/react"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogRoot, AlertDialogTitle, AlertDialogTrigger, } from "seed-design/ui/alert-dialog"; export default function AlertDialogOnOpenChangeReason() { const [open, setOpen] = useState(false); const [openReason, setOpenReason] = useState(null); const [closeReason, setCloseReason] = useState(null); return ( { setOpen(open); (open ? setOpenReason : setCloseReason)(details?.reason ?? null); }} > 열기 알림 ESC 키를 누르거나 버튼을 클릭하여 닫아보세요. 취소 확인 마지막 열림 이유: {openReason ?? "-"} 마지막 닫힘 이유: {closeReason ?? "-"} ); } ``` ### Portalled Portal은 기본적으로 `document.body`에 렌더링됩니다. ```tsx import { ResponsivePair, Portal } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogRoot, AlertDialogTitle, AlertDialogTrigger, } from "seed-design/ui/alert-dialog"; const AlertDialogPortalled = () => { return ( // You can set z-index dialog with "--layer-index" custom property. useful for stackflow integration. 열기 주의 이 작업은 되돌릴 수 없습니다. 취소 확인 ); }; export default AlertDialogPortalled; ``` ### Skip Animation `skipAnimation` prop을 사용하여 AlertDialog의 enter/exit 애니메이션을 건너뛸 수 있습니다. ```tsx import { ResponsivePair } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogRoot, AlertDialogTitle, AlertDialogTrigger, } from "seed-design/ui/alert-dialog"; const AlertDialogSkipAnimation = () => { return ( 열기 주의 이 작업은 되돌릴 수 없습니다. 취소 확인 ); }; export default AlertDialogSkipAnimation; ``` ### Stackflow ```tsx import { useActivityZIndexBase } from "@seed-design/stackflow"; import { type StaticActivityComponentType, useFlow } from "@stackflow/react/future"; import { AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogRoot, AlertDialogTitle, } from "seed-design/ui/alert-dialog"; declare module "@stackflow/config" { interface Register { ActivityAlertDialogStackflow: {}; } } const ActivityAlertDialogStackflow: StaticActivityComponentType< "ActivityAlertDialogStackflow" > = () => { const { pop } = useFlow(); return ( !open && pop()}> 제목 Stackflow 확인 ); }; export default ActivityAlertDialogStackflow; ``` --- file: components/article.mdx # Article Article은 일관된 selection 및 줄바꿈 정책을 사용할 수 있게 돕는 유틸리티 컴포넌트입니다. 사용 가능 버전: @seed-design/react@1.0.6, @seed-design/css@1.0.6 ## Preview ```tsx import { Article, Text, VStack } from "@seed-design/react"; export default function ArticlePreview() { return (
Article은 일관된 selection 및 줄바꿈 정책을 사용할 수 있게 돕는 유틸리티 컴포넌트입니다. 여기를 드래그해서 선택해보세요.
); } ``` ## Usage ```tsx import { Article } from "@seed-design/react"; ``` ```tsx
Article
``` ## Props [BoxProps](/react/components/layout/box#props)와 동일합니다. ## Examples ### Word Break Behavior [`lang` global attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/lang)를 통해 추론된 언어에 따라 단어 내 줄바꿈(word-break) 동작을 설정합니다. 한국어 문서/요소에서는 한 어절 안에서 줄바꿈이 발생하지 않도록 하고, 그 외 언어는 표준 규칙을 따릅니다. 컨테이너를 벗어날 정도로 긴 단어 안에서는 줄바꿈이 발생합니다. 일본어에서 문장 부호 직전, [스테가나](https://ko.wikipedia.org/wiki/스테가나) 직전, [반복 부호](https://ko.wikipedia.org/wiki/반복_부호) 직전 등 어색한 위치에서 줄바꿈이 발생하지 않도록 조정됩니다. ```tsx import { Article, Text, VStack } from "@seed-design/react"; export default function ArticleWordBreak() { return (
ja-JP 日本語の禁則処理では、特定の文字の前後で改行を制御します。例えば人々々々と続く場合や、小さい文字ぁぁぁが連続する場合、そして句読点。。。が続く場合の改行位置を確認できます。 また長い文章では自動的に適切な位置で改行されますが々ぁ。などの文字の前では改行されないことを確認してください。{" "} https://www.example.com/this-is-a-very-long-url-that-might-cause-layout-issues-if-the-word-break-is-not-handled-properly?utm_source=newsletter&utm_medium=email&utm_campaign=spring_sale
핸들을 잡고 너비를 조정해보세요.
); } ``` ### User Select Behavior `
` 내부 요소는 사용자가 선택(user-select)할 수 있습니다. ```tsx import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon"; import { Article, Divider, Icon, Text, VStack } from "@seed-design/react"; import { PageBanner } from "seed-design/ui/page-banner"; export default function ArticleSelectable() { return ( } description="상위 요소에 `user-select: none;` 스타일 적용됨" tone="warning" variant="solid" /> Article 밖은 선택할 수 없습니다. 상위 요소에 `user-select: none;` 스타일이 적용되어 있어 이 영역의 텍스트는 선택할 수 없습니다. 길게 탭하거나 더블 클릭해보세요.
Article 안 상위 요소에 `user-select: none;` 스타일이 적용되었지만 Article 내부는 선택할 수 있습니다. 길게 탭하거나 더블 클릭해서 텍스트를 선택해보세요.
Article 밖은 선택할 수 없습니다. 길게 탭하거나 더블 클릭해보세요.
); } ``` #### Disable User Selection Text 컴포넌트의 [`userSelect="none"` prop](/react/components/typography/text#user-select)을 사용하여 `
` 내부 요소를 선택 불가능하게 만들 수 있습니다. Text 컴포넌트에 대해 자세히 알아봅니다. ```tsx import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon"; import { Article, Divider, Icon, Text, VStack } from "@seed-design/react"; import { PageBanner } from "seed-design/ui/page-banner"; export default function ArticleSelectable() { return ( } description="상위 요소에 `user-select: none;` 스타일 적용됨" tone="warning" variant="solid" /> Article 밖은 선택할 수 없습니다. 상위 요소에 `user-select: none;` 스타일이 적용되어 있어 이 영역의 텍스트는 선택할 수 없습니다. 길게 탭하거나 더블 클릭해보세요.
Article 안 상위 요소에 `user-select: none;` 스타일이 적용되었지만 Article 내부는 선택할 수 있습니다. 길게 탭하거나 더블 클릭해서 텍스트를 선택해보세요. 이 요소는 Article 내부에 있지만 선택할 수 없습니다.
Article 밖은 선택할 수 없습니다. 길게 탭하거나 더블 클릭해보세요.
); } ``` #### Prevent `PullToRefresh` or `TabsCarousel` Gestures `
` 내부 요소는 사용자 선택이 가능합니다. 따라서, 드래그 동작을 통해 내부 요소를 선택 시 의도하지 않은 PTR(당겨서 새로고침) 또는 탭 스와이프 제스처가 발생할 수 있습니다. `PullToRefresh.preventPull` 또는 `Tabs.carouselPreventDrag`를 `
`에 전달하여 Article에서 발생한 이벤트가 제스처를 트리거하지 않도록 할 수 있습니다. PullToRefresh 컴포넌트에서 제스처를 방지할 영역을 지정하는 방법에 대해 알아봅니다. TabsCarousel 컴포넌트에서 제스처를 방지할 영역을 지정하는 방법에 대해 알아봅니다. ```tsx import { VStack, Icon, Text, Article, Divider, PullToRefresh } from "@seed-design/react"; import { type StaticActivityComponentType } from "@stackflow/react/future"; import { AppBar, AppBarMain } from "seed-design/ui/app-bar"; import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen"; import { PullToRefreshContent, PullToRefreshIndicator, PullToRefreshRoot, } from "seed-design/ui/pull-to-refresh"; import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon"; import { PageBanner } from "seed-design/ui/page-banner"; declare module "@stackflow/config" { interface Register { ActivityArticlePreventPull: {}; } } const ActivityArticlePreventPull: StaticActivityComponentType< "ActivityArticlePreventPull" > = () => { return ( Pull To Refresh {}} onPtrRefresh={async () => { await new Promise((resolve) => setTimeout(resolve, 1000)); }} > } description="상위 요소에 `user-select: none;` 스타일 적용됨" tone="warning" variant="solid" />
Article 이 요소는 Article 내부에 있으므로 텍스트 선택이 가능합니다. 이 Article은 PTR 제스처를 호출하지 않도록 설정되어 있습니다. 여기를 아래로 끌어 당기면 PTR이 작동하는 대신 텍스트가 선택됩니다.
Article 외부 이 요소는 Article 외부에 있으므로 텍스트 선택이 불가능합니다. 이 요소는 PTR을 호출할 수 있습니다. 여기를 아래로 끌어 당겨보세요.
); }; export default ActivityArticlePreventPull; ``` ```tsx import { VStack, Icon, Text, Article, Divider, Tabs } from "@seed-design/react"; import { type StaticActivityComponentType } from "@stackflow/react/future"; import { AppBar, AppBarMain } from "seed-design/ui/app-bar"; import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen"; import { TabsCarousel, TabsContent, TabsList, TabsRoot, TabsTrigger } from "seed-design/ui/tabs"; import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon"; import { PageBanner } from "seed-design/ui/page-banner"; declare module "@stackflow/config" { interface Register { ActivityArticlePreventDrag: {}; } } const ActivityArticlePreventDrag: StaticActivityComponentType< "ActivityArticlePreventDrag" > = () => { return ( Tabs Tab 1 Tab 2 } description="상위 요소에 `user-select: none;` 스타일 적용됨" tone="warning" variant="solid" />
Article 이 요소는 Article 내부에 있으므로 텍스트 선택이 가능합니다. 이 Article은 Tabs 제스처를 호출하지 않도록 설정되어 있습니다. 여기를 왼쪽으로 스와이프하면 탭이 전환되는 대신 텍스트가 선택됩니다.
Article 외부 이 요소는 Article 외부에 있으므로 텍스트 선택이 불가능합니다. 이 요소는 탭 스와이프를 호출할 수 있습니다. 여기를 왼쪽으로 스와이프해보세요.
안녕하세요!
); }; export default ActivityArticlePreventDrag; ``` ### Using `asChild` or `as` prop `
`은 기본적으로 `
`로 렌더링되지만, `asChild` 또는 `as` prop을 사용하여 다른 요소로 변경할 수 있습니다. `asChild` prop에 대해 자세히 알아봅니다. ```tsx import { Article, Divider, VStack, Text } from "@seed-design/react"; export default function ArticleAs() { return (
`as` prop으로 Article을 section으로 변경 Nulla exercitation quis aliqua nostrud.
`asChild` prop으로 Article을 section으로 변경 Elit fugiat elit exercitation laborum id veniam consequat ipsum sit voluptate velit.
); } ``` --- file: components/aspect-ratio.mdx # Aspect Ratio 가로(width)가 정해지면 비율에 따라 세로(height)가 자동으로 결정되는 레이아웃 컨테이너입니다. 사용 가능 버전: @seed-design/react@1.2.0, @seed-design/css@1.2.0 ## Preview ```tsx import { AspectRatio, Text, VStack } from "@seed-design/react"; export default function AspectRatioPreview() { return ( 4 / 3 1:1 16 / 9 ); } ``` ## Usage ```tsx import { AspectRatio } from "@seed-design/react"; ``` ```tsx ... ``` ## Props ## Examples ### Ratio 다양한 비율을 지정할 수 있습니다. `1`은 정사각형, `4/3`은 일반적인 사진 비율, `16/9`는 와이드스크린 비율입니다. ```tsx import { AspectRatio, Box, HStack } from "@seed-design/react"; export default function AspectRatioRatio() { return ( 1:1 Square 4:3 16:9 ); } ``` --- file: components/attachment-display-field.mdx # Attachment Display Field 외부 소스에서 제공된 미디어를 URL 기반으로 표시하고 관리하는 컴포넌트입니다. 사용 가능 버전: @seed-design/react@2.0.0, @seed-design/css@2.0.0 ## Preview ```tsx import { VStack } from "@seed-design/react"; import type { DisplayItemEntry } from "@seed-design/react/primitive"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; const sampleEntries: DisplayItemEntry[] = [ { id: "1", thumbnailUrl: "https://picsum.photos/seed/seed1/200/200", status: "success", }, { id: "2", thumbnailUrl: "https://picsum.photos/seed/seed2/200/200", status: "success", }, ]; // 외부 미디어 피커 모킹. 실제 환경에서는 네이티브 브릿지/모달/서버 호출 등으로 교체하세요. async function openMediaPicker(): Promise { const id = crypto.randomUUID(); return [ { id, thumbnailUrl: `https://picsum.photos/seed/${id}/200/200`, status: "success", }, ]; } export default function AttachmentDisplayPreview() { return ( { addEntries(await openMediaPicker()); }} /> ); } ``` ## Installation ### Default 순서 변경이 불필요한 경우 사용할 수 있는 컴포넌트를 포함합니다. - npm: npx @seed-design/cli@latest add ui:attachment-display-field - pnpm: pnpm dlx @seed-design/cli@latest add ui:attachment-display-field - yarn: yarn dlx @seed-design/cli@latest add ui:attachment-display-field - bun: bun x @seed-design/cli@latest add ui:attachment-display-field ### Reorderable 드래그 앤 드롭을 통한 항목 순서 변경이 필요한 경우 활용할 수 있는 컴포넌트를 포함합니다. 프로젝트에 [dnd-kit](https://dndkit.com/overview) 의존성이 추가됩니다. - npm: npx @seed-design/cli@latest add ui:attachment-display-field-reorderable - pnpm: pnpm dlx @seed-design/cli@latest add ui:attachment-display-field-reorderable - yarn: yarn dlx @seed-design/cli@latest add ui:attachment-display-field-reorderable - bun: bun x @seed-design/cli@latest add ui:attachment-display-field-reorderable ## Props ### `AttachmentDisplayField` ### `AttachmentDisplay` ### `AttachmentDisplayItem` ## Usage ### 기본 사용법 `AttachmentDisplayField` 안에 `AttachmentDisplay` 또는 `AttachmentDisplayReorderable`을 조합하여 사용합니다. `AttachmentDisplay`는 HTML ``을 사용하지 않습니다. `onTriggerClick`으로 외부 미디어 피커를 호출하고, 콜백 인자로 전달되는 `addEntries`에 그 결과를 넘겨 표시하세요. `addEntries`는 `maxEntries` 상한과 single-mode(`maxEntries={1}`) 치환을 내부에서 처리하므로, `entries`를 직접 펼쳐 넣는 것보다 안전합니다. [`AttachmentField`](/react/components/attachment-field)와 달리 `AttachmentDisplayField`는 파일의 유효성을 검증하거나 파일 객체를 직접 다루지 않습니다. ```tsx import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; { const pickedEntries = await openMediaPicker(); addEntries(pickedEntries); }} /> ; ``` `entries`와 `onEntriesChange`로 목록을 직접 제어하는 controlled 방식도 지원합니다. 이 경우에도 `addEntries`는 동일하게 동작합니다([Controlled](#controlled) 참고). ### Item 직접 구성하기 `AttachmentDisplay`, `AttachmentDisplayReorderable`은 `children`을 render prop으로 사용합니다. `entries`를 활용하여 `AttachmentDisplayItem`을 직접 렌더링할 수 있습니다. 이때 `DisplayItemEntry` 타입이 제공하는 `id`를 `key`로 활용하는 것을 권장합니다. `children`을 제공하지 않는 경우 자동으로 `entries`를 `AttachmentDisplayItem`으로 렌더링합니다. ```tsx import { AttachmentDisplay, AttachmentDisplayField, AttachmentDisplayItem, } from "seed-design/ui/attachment-display-field"; { addEntries(await openMediaPicker()); }} > {({ entries }) => entries.map((entry) => ( )) } ; ``` ## Adding Entries ### Trigger `AttachmentDisplay`는 trigger(업로드 버튼)가 포함된 레이아웃을 제공합니다. trigger 클릭 시 `onTriggerClick` 콜백이 실행됩니다. 일반적으로 외부 미디어 피커 호출을 수행합니다. 콜백은 `{ addEntries, updateEntryStatus }`를 인자로 받아, 피커 결과를 추가하고 곧바로 업로드 상태를 갱신할 수 있습니다. ```tsx import type { DisplayItemEntry } from "@seed-design/react/primitive"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; const defaultEntries: DisplayItemEntry[] = [ { id: "1", thumbnailUrl: "https://picsum.photos/seed/trigger1/200/200", status: "success", }, ]; // 외부 미디어 피커 모킹. 실제 환경에서는 네이티브 브릿지/모달/서버 호출 등으로 교체하세요. async function openMediaPicker(): Promise { const id = crypto.randomUUID(); return [ { id, thumbnailUrl: `https://picsum.photos/seed/${id}/200/200`, status: "success", }, ]; } export default function AttachmentDisplayTrigger() { return ( { addEntries(await openMediaPicker()); }} /> ); } ``` ### Listening to Entry Changes `entries`는 현재 표시되고 있는 항목의 목록입니다. `onEntriesChange` 콜백으로 `entries`에 등록된 파일 변경 이벤트를 감지할 수 있습니다. ```tsx "use client"; import { Text, VStack } from "@seed-design/react"; import type { DisplayItemEntry } from "@seed-design/react/primitive"; import { useRef, useState } from "react"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; // 외부 미디어 피커 모킹. 실제 환경에서는 네이티브 브릿지/모달/서버 호출 등으로 교체하세요. async function openMediaPicker(): Promise { const id = crypto.randomUUID(); return [ { id, thumbnailUrl: `https://picsum.photos/seed/${id}/200/200`, status: "success", }, ]; } export default function AttachmentDisplayValueChanges() { const [entries, setEntries] = useState([]); const entriesRef = useRef(entries); const [logs, setLogs] = useState([]); entriesRef.current = entries; const addLog = (message: string) => { setLogs((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${message}`]); }; // addEntries로 추가하든 제거 버튼으로 지우든 변경은 항상 onEntriesChange로 흐르므로, // 추가/삭제 감지를 여기 한 곳에서 처리합니다. const handleEntriesChange = (next: DisplayItemEntry[]) => { const prev = entriesRef.current; const added = next.filter((n) => !prev.some((p) => p.id === n.id)); const removed = prev.filter((p) => !next.some((n) => n.id === p.id)); if (added.length > 0) addLog(`added: ${added.map((a) => a.id).join(", ")}`); if (removed.length > 0) addLog(`removed: ${removed.map((r) => r.id).join(", ")}`); setEntries(next); }; return ( {logs.length === 0 ? ( 아이템을 추가하거나 삭제하면 로그가 표시됩니다. ) : ( logs.map((log, index) => ( {log} )) )} { addEntries(await openMediaPicker()); }} /> ); } ``` ## Managing Item Status `entries`의 각 항목은 `pending`, `uploading`, `success`, `error`의 status를 가질 수 있습니다. 새로 추가되는 항목의 status 기본값은 의도에 맞게 자유롭게 지정할 수 있습니다(외부 피커가 막 던진 항목이라면 `uploading`, 이미 업로드 완료된 미디어를 hydrate한다면 `success`). 외부 업로드 API와 연동하는 경우, `onTriggerClick`·`onRetry` 콜백으로 함께 전달되는 `updateEntryStatus` 헬퍼를 사용하여 각 항목의 status를 업데이트합니다. ```tsx "use client"; import { VStack } from "@seed-design/react"; import type { DisplayItemEntry, DisplayItemStatusDetails } from "@seed-design/react/primitive"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; const defaultEntries: DisplayItemEntry[] = [ { id: "1", thumbnailUrl: "https://picsum.photos/seed/upload1/200/200", status: "uploading", progress: 30, }, { id: "2", thumbnailUrl: "https://picsum.photos/seed/upload2/200/200", status: "success", }, { id: "3", thumbnailUrl: "https://picsum.photos/seed/upload3/200/200", status: "error", }, ]; // 외부 미디어 피커 모킹. 실제 환경에서는 네이티브 브릿지/모달/서버 호출 등으로 교체하세요. async function openMediaPicker(): Promise { const id = crypto.randomUUID(); return [ { id, thumbnailUrl: `https://picsum.photos/seed/${id}/200/200`, status: "uploading", }, ]; } // 실제 환경에서는 네이티브 브릿지 또는 외부 업로드 API와 연동하세요. // status는 컴포넌트가 콜백으로 전달하는 updateEntryStatus 헬퍼로만 갱신합니다. function simulateUpload( id: string, updateEntryStatus: (id: string, details: DisplayItemStatusDetails) => void, ) { updateEntryStatus(id, { status: "uploading", progress: 0 }); let progress = 0; const interval = setInterval(() => { progress += 20; if (progress >= 100) { clearInterval(interval); updateEntryStatus(id, Math.random() > 0.5 ? { status: "success" } : { status: "error" }); } else { updateEntryStatus(id, { status: "uploading", progress }); } }, 300); } export default function AttachmentDisplayStatus() { return ( { const pickedEntries = await openMediaPicker(); addEntries(pickedEntries); for (const entry of pickedEntries) { simulateUpload(entry.id, updateEntryStatus); } }} onRetry={(entry, { updateEntryStatus }) => simulateUpload(entry.id, updateEntryStatus)} /> ); } ``` - `uploading`: [ProgressCircle](/react/components/progress-circle)이 표시됩니다. - `progress`를 설정하여 업로드 진행률을 표시할 수 있습니다. - `progress`를 지정하지 않는 경우 [indeterminate](/react/components/progress-circle#indeterminate) 상태로 표시됩니다. - `error`: 재시도 버튼이 표시됩니다. - 클릭 시 `AttachmentDisplay`에 지정한 `onRetry` 콜백이 `(entry, { updateEntryStatus })` 인자로 실행됩니다. `updateEntryStatus`로 해당 항목을 다시 `uploading` 상태로 되돌려 업로드를 재시도하세요. ## Reordering Entries `AttachmentDisplayReorderable`을 사용하면 드래그로 항목의 순서를 변경할 수 있습니다. 해당 컴포넌트는 `dnd-kit` 의존성 분리를 위해 별도 snippet [`ui:attachment-display-field-reorderable`](#reorderable)로 제공됩니다. Context를 통해 `reorderEntry`가 제공되므로, 필요한 경우 원하는 드래그 앤 드롭 동작을 직접 구현하거나, 이미 프로젝트에서 사용 중인 드래그 앤 드롭 라이브러리와 연동하여 사용할 수 있습니다. ```tsx "use client"; import type { DisplayItemEntry } from "@seed-design/react/primitive"; import { useState } from "react"; import { AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; import { AttachmentDisplayReorderable } from "seed-design/ui/attachment-display-field-reorderable"; const defaultEntries: DisplayItemEntry[] = [ { id: "1", thumbnailUrl: "https://picsum.photos/seed/reorder1/200/200", status: "success" }, { id: "2", thumbnailUrl: "https://picsum.photos/seed/reorder2/200/200", status: "success" }, { id: "3", thumbnailUrl: "https://picsum.photos/seed/reorder3/200/200", status: "success" }, ]; // 외부 미디어 피커 모킹. 실제 환경에서는 네이티브 브릿지/모달/서버 호출 등으로 교체하세요. async function openMediaPicker(): Promise { const id = crypto.randomUUID(); return [ { id, thumbnailUrl: `https://picsum.photos/seed/${id}/200/200`, status: "success", }, ]; } export default function AttachmentDisplayReorderableExample() { const [entries, setEntries] = useState(defaultEntries); return ( { addEntries(await openMediaPicker()); }} /> ); } ``` ## Examples ### Disabled `disabled` prop으로 trigger 버튼을 비활성화할 수 있습니다. ```tsx import { VStack } from "@seed-design/react"; import type { DisplayItemEntry } from "@seed-design/react/primitive"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; const sampleEntries: DisplayItemEntry[] = [ { id: "1", thumbnailUrl: "https://picsum.photos/seed/disabled1/200/200", status: "success", }, ]; export default function AttachmentDisplayDisabled() { return ( {}} /> ); } ``` ### Read Only `readOnly` prop으로 읽기 전용 상태를 표현할 수 있습니다. trigger, 파일 제거 버튼, 순서 변경 모두 비활성화됩니다. ```tsx import { VStack } from "@seed-design/react"; import type { DisplayItemEntry } from "@seed-design/react/primitive"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; const sampleEntries: DisplayItemEntry[] = [ { id: "1", thumbnailUrl: "https://picsum.photos/seed/readonly1/200/200", status: "success", }, { id: "2", thumbnailUrl: "https://picsum.photos/seed/readonly2/200/200", status: "success", }, ]; export default function AttachmentDisplayReadOnly() { return ( {}} /> ); } ``` ### Controlled `entries`와 `onEntriesChange`를 사용하여 외부에서 아이템 목록을 제어할 수 있습니다. ```tsx "use client"; import { HStack, Text, VStack } from "@seed-design/react"; import type { DisplayItemEntry } from "@seed-design/react/primitive"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; // 외부 미디어 피커 모킹. 실제 환경에서는 네이티브 브릿지/모달/서버 호출 등으로 교체하세요. async function openMediaPicker(): Promise { const id = crypto.randomUUID(); return [ { id, thumbnailUrl: `https://picsum.photos/seed/${id}/200/200`, status: "success", }, ]; } export default function AttachmentDisplayControlled() { const [entries, setEntries] = useState([]); return ( { addEntries(await openMediaPicker()); }} /> 현재 아이템: {entries.length}개 setEntries([])}> 전체 삭제 ); } ``` ### Custom Inset `--seed-attachment-input-extend-x` CSS 변수를 사용하여 스크롤되는 아이템 목록이 레이아웃 바깥으로 빠져나오도록 구성할 수 있습니다. ```tsx import { vars } from "@seed-design/css/vars"; import { VStack } from "@seed-design/react"; import type { DisplayItemEntry } from "@seed-design/react/primitive"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; import { TextField, TextFieldInput } from "seed-design/ui/text-field"; const defaultEntries: DisplayItemEntry[] = Array.from({ length: 8 }, (_, i) => ({ id: String(i + 1), thumbnailUrl: `https://picsum.photos/seed/inset${i + 1}/200/200`, status: "success", })); export default function AttachmentDisplayCustomInset() { return ( { // 외부 미디어 피커 호출 자리 }} /> ); } ``` ### Field Integration `label`, `description`, `errorMessage` 등 Field 관련 prop을 전달할 수 있습니다. ```tsx "use client"; import { VStack } from "@seed-design/react"; import type { DisplayItemEntry } from "@seed-design/react/primitive"; import { useState } from "react"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; const defaultEntries: DisplayItemEntry[] = [ { id: "1", thumbnailUrl: "https://picsum.photos/seed/field1/200/200", status: "success" }, ]; // 외부 미디어 피커 모킹. 실제 환경에서는 네이티브 브릿지/모달/서버 호출 등으로 교체하세요. async function openMediaPicker(): Promise { const id = crypto.randomUUID(); return [ { id, thumbnailUrl: `https://picsum.photos/seed/${id}/200/200`, status: "success", }, ]; } export default function AttachmentDisplayFieldExample() { const [entries, setEntries] = useState(defaultEntries); const invalid = entries.length < 1; return ( { addEntries(await openMediaPicker()); }} /> ); } ``` ### Customizing Items Snippet이 제공하는 기본 아이템 구성 외에 추가적인 커스터마이징이 필요한 경우, `@seed-design/react`에서 제공하는 `AttachmentDisplay.ItemBadge` 등의 요소를 활용하여 직접 아이템을 구성할 수 있습니다. 아래 예시에서는 `AttachmentDisplay.ItemBadge`를 사용하여 첫 번째 이미지에 "대표사진" 배지를 표시합니다. ```tsx "use client"; import { IconArrowClockwiseCircularFill, IconXmarkFill } from "@karrotmarket/react-monochrome-icon"; import { AttachmentDisplay as SeedAttachmentDisplay, Icon, VStack } from "@seed-design/react"; import type { DisplayItemEntry } from "@seed-design/react/primitive"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; import { ProgressCircle } from "seed-design/ui/progress-circle"; const LABEL_REMOVE = "삭제"; const LABEL_RETRY = "재시도"; function CustomImageItem({ entry, isCover, onRetry, }: { entry: DisplayItemEntry; isCover?: boolean; onRetry?: () => void; }) { return ( {isCover && 대표사진} {(e) => ( )} {onRetry && ( } /> {LABEL_RETRY} )} } /> ); } const defaultEntries: DisplayItemEntry[] = [ { id: "1", thumbnailUrl: "https://picsum.photos/seed/customizing1/200/200", status: "success" }, { id: "2", thumbnailUrl: "https://picsum.photos/seed/customizing2/200/200", status: "success" }, { id: "3", thumbnailUrl: "https://picsum.photos/seed/customizing3/200/200", status: "success" }, ]; export default function AttachmentDisplayCustomizingItems() { return ( { // 외부 미디어 피커 호출 자리 }} > {({ entries }) => entries.map((entry, index) => ( )) } ); } ``` ## Attachment Display Field vs. Attachment Field HTML ``을 사용해야 하는 경우 `AttachmentField`를, 외부 소스와 연동하여 URL 기반으로 미디어를 표시해야 하는 경우 `AttachmentDisplayField`를 사용하세요. | | Attachment Field | Attachment Display | | -------------- | -------------------------- | ---------------------------- | | 미디어 소스 | HTML `` | 이미지 URL | | 데이터 모델 | `File` 기반 `FileEntry` | URL 기반 `DisplayItemEntry` | | 파일 선택 | `` | 다루지 않음 (`onTriggerClick` 위임) | | 드래그 앤 드롭으로 업로드 | `AttachmentDropzone` | 다루지 않음 | | 파일 검증 | accept, maxFileSize 등 | 다루지 않음 | | Form 연동 | `` 동기화 | 다루지 않음 | --- file: components/attachment-field.mdx # Attachment Field 파일을 선택하거나 드래그 앤 드롭으로 업로드할 수 있는 컴포넌트입니다. 사용 가능 버전: @seed-design/react@2.0.0, @seed-design/css@2.0.0 ## Preview ```tsx import { VStack } from "@seed-design/react"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; export default function AttachmentFieldPreview() { return ( ); } ``` ## Installation ### Default 파일 순서 변경이 불필요한 경우 사용할 수 있는 컴포넌트를 포함합니다. - npm: npx @seed-design/cli@latest add ui:attachment-field - pnpm: pnpm dlx @seed-design/cli@latest add ui:attachment-field - yarn: yarn dlx @seed-design/cli@latest add ui:attachment-field - bun: bun x @seed-design/cli@latest add ui:attachment-field ### Reorderable 드래그 앤 드롭을 통한 파일 순서 변경이 필요한 경우 활용할 수 있는 컴포넌트를 포함합니다. 프로젝트에 [dnd-kit](https://dndkit.com/overview) 의존성이 추가됩니다. - npm: npx @seed-design/cli@latest add ui:attachment-field-reorderable - pnpm: pnpm dlx @seed-design/cli@latest add ui:attachment-field-reorderable - yarn: yarn dlx @seed-design/cli@latest add ui:attachment-field-reorderable - bun: bun x @seed-design/cli@latest add ui:attachment-field-reorderable ## Props ### `AttachmentField` ### `AttachmentInput` ### `AttachmentDropzone` ### `AttachmentInputItem` ## Usage ### 기본 사용법 `AttachmentField` 안에 `AttachmentInput`, `AttachmentDropzone`, `AttachmentInputReorderable`, `AttachmentDropzoneReorderable` 중 하나를 조합하여 사용합니다. ```tsx import { AttachmentField, AttachmentInput, AttachmentDropzone, } from "seed-design/ui/attachment-field"; ; ; ``` ```tsx import { AttachmentField } from "seed-design/ui/attachment-field"; import { AttachmentInputReorderable, AttachmentDropzoneReorderable, } from "seed-design/ui/attachment-field-reorderable"; ; ; ``` ### Item 직접 구성하기 `AttachmentInput`, `AttachmentDropzone`, `AttachmentInputReorderable`, `AttachmentDropzoneReorderable`은 `children`을 render prop으로 사용합니다. `acceptedFileEntries`을 활용하여 `AttachmentInputItem`을 직접 렌더링할 수 있습니다. 이때 `FileEntry` 타입이 제공하는 `id`를 `key`로 활용하는 것을 권장합니다. `children`을 제공하지 않는 경우 자동으로 `acceptedFileEntries`에 등록된 파일을 `AttachmentInputItem`으로 렌더링합니다. ```tsx import { AttachmentField, AttachmentInput, AttachmentInputItem, } from "seed-design/ui/attachment-field"; {({ acceptedFileEntries }) => acceptedFileEntries.map((entry) => ( )) } ; ``` ## Uploading Files ### Trigger `AttachmentInput`를 사용하면 trigger(업로드 버튼)가 포함된 레이아웃을 사용할 수 있습니다. ```tsx import type { FileEntry } from "@seed-design/react/primitive"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; const defaultAcceptedFileEntries: FileEntry[] = [ { id: "1", file: new File(["hello"], "document.pdf", { type: "application/pdf" }), status: "success", }, ]; export default function AttachmentFieldTriggerExample() { return ( ); } ``` ### Dropzone `AttachmentDropzone`을 사용하면 드래그 앤 드롭 영역이 포함된 레이아웃을 사용할 수 있습니다. ```tsx import { VStack } from "@seed-design/react"; import type { FileEntry } from "@seed-design/react/primitive"; import { AttachmentField, AttachmentDropzone } from "seed-design/ui/attachment-field"; const defaultAcceptedFileEntries: FileEntry[] = [ { id: "1", file: new File(["hello"], "document.pdf", { type: "application/pdf" }), status: "success", }, ]; export default function AttachmentFieldDropzone() { return ( ); } ``` ### Listening to Accepted File Changes `acceptedFileEntries`는 유효성 검사를 마친 파일의 목록입니다. `onAcceptedFileEntriesChange` 콜백으로 `acceptedFileEntries`에 등록된 파일 변경 이벤트를 감지할 수 있습니다. ```tsx import { VStack, Text } from "@seed-design/react"; import { useState } from "react"; import type { FileStatusDetails } from "@seed-design/react/primitive"; import { AttachmentField, AttachmentInput, AttachmentInputItem, } from "seed-design/ui/attachment-field"; function simulateUpload( _file: File, id: string, updateFileEntryStatus: (id: string, details: FileStatusDetails) => void, ) { updateFileEntryStatus(id, { status: "uploading", progress: 0 }); let progress = 0; const interval = setInterval(() => { progress += 25; if (progress >= 100) { clearInterval(interval); updateFileEntryStatus(id, { status: "success" }); } else { updateFileEntryStatus(id, { status: "uploading", progress }); } }, 500); } export default function AttachmentFieldValueChanges() { const [logs, setLogs] = useState([]); const addLog = (message: string) => { setLogs((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${message}`]); }; return ( {logs.length === 0 ? ( 파일을 추가하거나 삭제하면 로그가 표시됩니다. ) : ( logs.map((log, index) => ( {log} )) )} { addLog(`onFileAccept: ${entries.map((e) => e.file.name).join(", ")}`); for (const entry of entries) { simulateUpload(entry.file, entry.id, updateFileEntryStatus); } }} onAcceptedFileEntriesChange={(files) => { addLog( `onAcceptedFileEntriesChange: ${files.map((f) => `${f.file.name} (${f.status})`).join(", ")}`, ); }} onFileReject={(files) => { addLog( `onFileReject: ${files.map((f) => `${f.file.name} (${f.errors.join(", ")})`).join(", ")}`, ); }} > {({ acceptedFileEntries }) => acceptedFileEntries.map((fileEntry) => ( )) } ); } ``` ## Validating Files 사용자가 선택한 파일이 `acceptedFileEntries`에 등록되기 전 유효성을 확인할 수 있습니다. 유효하지 않은 파일에 대해 각각 `onFileReject` 콜백이 실행됩니다. 해당 콜백에서 파일과 에러 코드들을 확인하여 에러 메시지를 표시할 수 있습니다. ### Max Files `maxFiles`로 업로드 가능한 최대 파일 수를 제한할 수 있습니다. 기본값은 `1`입니다. 최대 수에 도달한 경우 trigger 및 dropzone이 비활성화됩니다. 업로드 가능한 파일의 수보다 많은 파일을 선택한 경우 업로드 가능한 파일까지 `acceptedFileEntries`에 등록됩니다. 나머지 파일에 대해서는 각각 `onFileReject` 콜백이 실행됩니다. 이때 reject된 파일의 에러는 `"TOO_MANY_FILES"`입니다. ```tsx import { VStack } from "@seed-design/react"; import type { FileEntry } from "@seed-design/react/primitive"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; const defaultAcceptedFileEntries: FileEntry[] = [ { id: "1", file: new File(["hello"], "document.pdf", { type: "application/pdf" }), status: "success", }, ]; export default function AttachmentFieldMaxFiles() { return ( ); } ``` ### Invalid File Type `accept`로 업로드 가능한 파일의 종류를 제한할 수 있습니다. MIME type(`image/png`, `image/*`) 또는 확장자(`.png`, `.jpg, .jpeg`) 형식을 지정할 수 있으며, `string[]`을 전달하는 경우 각 `string` `,`로 join합니다. ``에 등록되는 `accept` 속성을 통해 사용자가 선택할 수 있는 파일의 종류를 제한하는 것은 브라우저 UI에서만 동작하는 편의 기능입니다. 따라서 사용자는 브라우저 파일 선택 다이얼로그의 `모든 파일 보기`와 같은 기능을 통해 제한된 종류의 파일도 선택할 수 있습니다. 이렇게 선택된 파일의 경우 `"INVALID_TYPE"` 에러와 함께 `onFileReject` 콜백이 실행됩니다. 파일 종류 검증이 필요한 경우 해당 검증은 서버에서도 수행되어야 합니다. ```tsx import { useState } from "react"; import { VStack } from "@seed-design/react"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; function getErrorMessage(errorCode: string): string { switch (errorCode) { case "INVALID_TYPE": return "지원하지 않는 파일 형식입니다"; default: return "업로드에 실패했습니다"; } } export default function AttachmentFieldInvalidFileType() { const [errorMessage, setErrorMessage] = useState(); return ( setErrorMessage(undefined)} onFileReject={(files) => { const messages = files.map( ({ file, errors }) => `"${file.name}": ${errors.map(getErrorMessage).join(", ")}`, ); setErrorMessage(messages.join("\n")); }} > ); } ``` ### File Size `minFileSize`, `maxFileSize` prop을 활용할 수 있습니다. - 사용자가 선택한 파일이 `minFileSize`보다 작은 경우 `"FILE_TOO_SMALL"` 에러와 함께 `onFileReject` 콜백이 실행됩니다. - 사용자가 선택한 파일이 `maxFileSize`보다 큰 경우 `"FILE_TOO_LARGE"` 에러와 함께 `onFileReject` 콜백이 실행됩니다. ```tsx import { useState } from "react"; import { VStack } from "@seed-design/react"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; import { formatBytes } from "seed-design/lib/format-bytes"; const MIN_FILE_SIZE = 1 * 1024; // 1KB const MAX_FILE_SIZE = 10 * 1024; // 10KB function getErrorMessage(errorCode: string): string { switch (errorCode) { case "FILE_TOO_LARGE": return `크기가 ${formatBytes(MAX_FILE_SIZE)}를 초과합니다`; case "FILE_TOO_SMALL": return `크기가 ${formatBytes(MIN_FILE_SIZE)} 미만입니다`; case "TOO_MANY_FILES": return "업로드 가능한 파일 개수를 초과했습니다"; default: return "업로드에 실패했습니다"; } } export default function AttachmentFieldValidation() { const [errorMessage, setErrorMessage] = useState(); return ( setErrorMessage(undefined)} onFileReject={(files) => { const messages = files.map( ({ file, errors }) => `"${file.name}": ${errors.map(getErrorMessage).join(", ")}`, ); setErrorMessage(messages.join("\n")); }} > ); } ``` ### Custom Validation `validate` prop으로 각 파일에 대한 유효성 검사를 직접 추가할 수 있습니다. 커스텀 에러 코드를 반환하여 `onFileReject`에서 에러 종류별로 메시지를 분기할 수 있습니다. ```tsx import { useState } from "react"; import { VStack } from "@seed-design/react"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; function validateFileName(file: File) { const nameWithoutExt = file.name.replace(/\.[^.]+$/, ""); if (nameWithoutExt.length > 8) { return ["FILENAME_TOO_LONG"]; } return null; } export default function AttachmentFieldCustomValidation() { const [errorMessage, setErrorMessage] = useState(); return ( setErrorMessage(undefined)} onFileReject={(files) => { if (files.every((f) => f.errors.includes("FILENAME_TOO_LONG")) === false) { return; } const names = files.map((f) => f.file.name).join(", "); setErrorMessage(`"${names}"은(는) 파일 이름이 8자를 초과합니다.`); }} > ); } ``` ## Managing File Status `acceptedFileEntries`의 각 항목은 `pending`, `uploading`, `success`, `error`의 status를 가질 수 있습니다. 새로 추가되는 항목의 status 기본값은 `pending`입니다. 파일 선택 직후 외부 업로드 API와 연동하는 경우, 사용자에게 각 파일 항목의 업로딩 상태를 보여줄 수 있습니다. `onFileAccept` 콜백에서 새로 추가된 파일을 받고, 함께 제공되는 `updateFileEntryStatus` 헬퍼를 사용하여 항목의 status를 업데이트합니다. ```tsx import { useCallback } from "react"; import { VStack } from "@seed-design/react"; import type { FileStatusDetails } from "@seed-design/react/primitive"; import { AttachmentField, AttachmentInput, AttachmentInputItem, } from "seed-design/ui/attachment-field"; // 실제 환경에서는 fetch 등으로 교체하세요. async function uploadFile( file: File, onProgress: (progress: number) => void, ): Promise<{ url: string }> { const totalChunks = 5; for (let i = 1; i <= totalChunks; i++) { await new Promise((r) => setTimeout(r, 200 + Math.random() * 300)); onProgress(Math.round((i / totalChunks) * 100)); } if (Math.random() > 0.5) { throw new Error("Network error"); } return { url: `https://example.com/uploads/${file.name}` }; } export default function AttachmentFieldStatus() { const startUpload = useCallback( ( file: File, id: string, updateFileEntryStatus: (id: string, details: FileStatusDetails) => void, ) => { updateFileEntryStatus(id, { status: "uploading", progress: 0 }); uploadFile(file, (progress) => { updateFileEntryStatus(id, { status: "uploading", progress }); }) .then(() => updateFileEntryStatus(id, { status: "success" })) .catch(() => updateFileEntryStatus(id, { status: "error" })); }, [], ); return ( { for (const entry of entries) { startUpload(entry.file, entry.id, updateFileEntryStatus); } }} > {({ acceptedFileEntries, updateFileEntryStatus }) => acceptedFileEntries.map((fileEntry) => ( startUpload(fileEntry.file, fileEntry.id, updateFileEntryStatus)} /> )) } ); } ``` - `uploading`: [ProgressCircle](/react/components/progress-circle)이 표시됩니다. - `progress`를 설정하여 업로드 진행률을 표시할 수 있습니다. - `progress`를 지정하지 않는 경우 [indeterminate](/react/components/progress-circle#indeterminate) 상태로 표시됩니다. - `error`: 재시도 버튼이 표시됩니다. - 클릭 시 `AttachmentInputItem`에 지정한 `onRetry` 콜백이 실행됩니다. 유효성 검증에 성공한 파일 항목은 status와 관계없이 `acceptedFileEntries`에 유지되므로, 네이티브 form 제출 시 포함됩니다. ## Reordering Files `AttachmentInputReorderable` 또는 `AttachmentDropzoneReorderable`을 사용하면 드래그로 파일의 순서를 변경할 수 있습니다. 두 컴포넌트는 `dnd-kit` 의존성 분리를 위해 별도 snippet [`ui:attachment-field-reorderable`](#reorderable)로 제공됩니다. Context를 통해 `reorderFileEntry`가 제공되므로, 필요한 경우 원하는 드래그 앤 드롭 동작을 직접 구현하거나, 이미 프로젝트에서 사용 중인 드래그 앤 드롭 라이브러리와 연동하여 사용할 수 있습니다. ```tsx import type { FileEntry } from "@seed-design/react/primitive"; import { AttachmentField } from "seed-design/ui/attachment-field"; import { AttachmentInputReorderable } from "seed-design/ui/attachment-field-reorderable"; function createMockImageFile(name: string, base64: string): File { const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); return new File([bytes], name, { type: "image/png" }); } const defaultAcceptedFileEntries: FileEntry[] = [ { id: "1", file: createMockImageFile( "sunset-landscape.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGN4FcEDAAN+AU+hW/ICAAAAAElFTkSuQmCC", ), status: "success", }, { id: "2", file: createMockImageFile( "city-night.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGOYbPwKAAMNAbHKe2UaAAAAAElFTkSuQmCC", ), status: "success", }, { id: "3", file: createMockImageFile( "morning-coffee.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGN4tZkDAAQwAaYlKXDxAAAAAElFTkSuQmCC", ), status: "success", }, ]; export default function AttachmentFieldReorderableExample() { return ( ); } ``` ## Examples ### Showing Thumbnails 파일 이름 및 크기 대신 이미지 미리보기를 표시하려면 `accept`를 `"image/*"`, `["image/png", "image/jpeg"]` 등으로 설정하여 사용자가 이미지 파일만 선택할 수 있도록 제한합니다. ```tsx import { VStack } from "@seed-design/react"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; export default function AttachmentFieldAcceptImage() { return ( ); } ``` [`"image/heic"`](https://caniuse.com/heif) 등 일부 이미지 형식은 브라우저에 따라 이미지 미리보기가 표시되지 않을 수 있습니다. ### Disabled `disabled` prop으로 trigger 및 dropzone을 비활성화하여 신규 파일 선택을 차단하고, ``을 `disabled` 처리하여 폼 제출 시 값이 전송되지 않도록 합니다. ```tsx import { VStack } from "@seed-design/react"; import type { FileEntry } from "@seed-design/react/primitive"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; const defaultFiles: FileEntry[] = [ { id: "mock-1", file: new File(["hello"], "document.pdf", { type: "application/pdf" }), status: "success", }, { id: "mock-2", file: new File(["world"], "image.png", { type: "image/png" }), status: "success", }, ]; export default function AttachmentFieldDisabled() { return ( ); } ``` ### Read Only `readOnly` prop으로 첨부된 파일을 읽기 전용 상태로 표시할 수 있습니다. trigger, dropzone, 파일 제거 버튼, 순서 변경 모두 비활성화되지만 ``의 값은 유지되어 form 제출 시 함께 전송됩니다. ```tsx import { VStack } from "@seed-design/react"; import type { FileEntry } from "@seed-design/react/primitive"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; const defaultFiles: FileEntry[] = [ { id: "mock-1", file: new File(["hello"], "document.pdf", { type: "application/pdf" }), status: "success", }, { id: "mock-2", file: new File(["world"], "image.png", { type: "image/png" }), status: "success", }, ]; export default function AttachmentFieldReadOnly() { return ( ); } ``` ### Controlled `acceptedFileEntries`와 `onAcceptedFileEntriesChange`를 사용하여 외부에서 파일 목록을 제어할 수 있습니다. ```tsx import { VStack, HStack, Text } from "@seed-design/react"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import type { FileEntry } from "@seed-design/react/primitive"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; export default function AttachmentFieldControlled() { const [acceptedFileEntries, setAcceptedFileEntries] = useState([]); return ( 현재 파일: {JSON.stringify(acceptedFileEntries.map((f) => f.file.name))} setAcceptedFileEntries([])} > 전체 삭제 ); } ``` ### Custom Inset `--seed-attachment-input-extend-x` CSS 변수를 사용하여 스크롤되는 아이템 목록이 레이아웃 바깥으로 빠져나오도록 구성할 수 있습니다. ```tsx import { VStack } from "@seed-design/react"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; import { TextField, TextFieldInput } from "seed-design/ui/text-field"; import { vars } from "@seed-design/css/vars"; const mockedFiles = Array.from({ length: 8 }, (_, i) => { const file = new File(["file content"], `file${i + 1}.txt`, { type: "text/plain" }); Object.defineProperty(file, "size", { value: 1 }); return file; }); export default function AttachmentFieldCustomInset() { return ( ({ id: `${index}`, file, status: "pending", }))} rootProps={{ style: { "--seed-attachment-input-extend-x": vars.$dimension.spacingX.globalGutter, } as React.CSSProperties, }} > ); } ``` ### Field Integration `label`, `description`, `errorMessage` 등의 Field 관련 prop을 사용할 수 있습니다. ```tsx import { Divider, VStack } from "@seed-design/react"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; export default function AttachmentFieldField() { return ( ); } ``` ### Form (Uncontrolled) `acceptedFileEntries`에 등록된 파일이 ``의 `files`로 동기화되므로 `
`과 `name` prop을 사용하여 formData에 포함시킬 수 있습니다. ```tsx import { VStack } from "@seed-design/react"; import { useState, type FormEvent } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; type FieldErrors = { files?: string; }; export default function AttachmentFieldForm() { const [fieldErrors, setFieldErrors] = useState({}); const handleSubmit = (event: FormEvent) => { event.preventDefault(); const formData = new FormData(event.currentTarget); const files = formData.getAll("files") as File[]; if (files.length === 0) { setFieldErrors({ files: "최소 1개의 파일을 업로드해주세요" }); return; } window.alert(`제출된 파일: ${files.map((f) => f.name).join(", ")}`); }; return ( { if (files.length > 0) { setFieldErrors({}); } }} {...(fieldErrors.files && { invalid: true, errorMessage: fieldErrors.files })} > 제출 ); } ``` ### React Hook Form ```tsx import { HStack, VStack } from "@seed-design/react"; import { useCallback, type FormEvent } from "react"; import { useController, useForm } from "react-hook-form"; import { ActionButton } from "seed-design/ui/action-button"; import type { FileEntry } from "@seed-design/react/primitive"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; interface FormValues { files: FileEntry[]; } export default function AttachmentFieldReactHookForm() { const { handleSubmit, reset, control } = useForm({ reValidateMode: "onSubmit", defaultValues: { files: [], }, }); const { field: { value, onChange, ...field }, fieldState, } = useController({ name: "files", control, rules: { validate: (value) => value.length > 0 || "최소 1개의 파일을 업로드해주세요", }, }); const onValid = useCallback( (data: FormValues) => window.alert(`제출된 파일: ${data.files.map((f) => f.file.name).join(", ")}`), [], ); const onReset = useCallback( (event: FormEvent) => { event.preventDefault(); reset(); }, [reset], ); return ( 초기화 제출 ); } ``` ### Customizing Items Snippet이 제공하는 기본 아이템 구성 외에 추가적인 커스터마이징이 필요한 경우, `@seed-design/react`에서 제공하는 `AttachmentInput.ItemBadge` 등의 요소를 활용하여 직접 아이템을 구성할 수 있습니다. 아래 예시에서는 `AttachmentInput.ItemBadge`를 사용하여 첫 번째 이미지에 "대표사진" 배지를 표시합니다. ```tsx "use client"; import { AttachmentInput as SeedAttachmentInput, Icon, VStack } from "@seed-design/react"; import type { FileEntry } from "@seed-design/react/primitive"; import { IconArrowClockwiseCircularFill, IconXmarkFill } from "@karrotmarket/react-monochrome-icon"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; import { ProgressCircle } from "seed-design/ui/progress-circle"; const LABEL_REMOVE_FILE = "파일 제거"; const LABEL_RETRY = "재시도"; function CustomImageItem({ fileEntry, isCover, onRetry, }: { fileEntry: FileEntry; isCover?: boolean; onRetry?: () => void; }) { return ( {isCover && 대표사진} {(entry) => ( )} {onRetry && ( } /> {LABEL_RETRY} )} } /> ); } export default function AttachmentFieldCustomizingItems() { return ( {({ acceptedFileEntries }) => acceptedFileEntries.map((fileEntry, index) => ( )) } ); } ``` --- file: components/avatar.mdx # Avatar 사용자의 프로필 이미지를 표시하는 컴포넌트입니다. 사용 가능 버전: @seed-design/react@0.0.1, @seed-design/css@0.0.1 ## Preview ```tsx import { Box, Flex } from "@seed-design/react"; import { Avatar, AvatarBadge } from "seed-design/ui/avatar"; import { IdentityPlaceholder } from "seed-design/ui/identity-placeholder"; export default function AvatarPreview() { return ( } > } /> ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:avatar - pnpm: pnpm dlx @seed-design/cli@latest add ui:avatar - yarn: yarn dlx @seed-design/cli@latest add ui:avatar - bun: bun x @seed-design/cli@latest add ui:avatar ## Props ### `Avatar` ### `AvatarBadge` ### `AvatarStack` ## Examples ### Size ```tsx import { Flex } from "@seed-design/react"; import { Avatar } from "seed-design/ui/avatar"; export default function AvatarSize() { return ( ); } ``` ### Badge & Badge Mask ```tsx import { IdentityPlaceholder } from "seed-design/ui/identity-placeholder"; import { Avatar, AvatarBadge } from "seed-design/ui/avatar"; import { Box, HStack } from "@seed-design/react"; export default function () { return ( } > } > 뱃지를 설명하는 대체 텍스트를 제공해야 합니다. } > 뱃지를 설명하는 대체 텍스트를 제공해야 합니다. ); } ``` ### Stack ```tsx import { Avatar, AvatarStack } from "seed-design/ui/avatar"; import { IdentityPlaceholder } from "seed-design/ui/identity-placeholder"; export default function AvatarStackExample() { return ( } /> } /> } /> } /> ); } ``` ### Fallback Image `fallback` prop으로 이미지가 로드되지 않았을 때 보여질 요소를 지정할 수 있습니다. 주로 [IdentityPlaceholder](/react/components/identity-placeholder)를 사용합니다. ```tsx import { Flex } from "@seed-design/react"; import { Avatar } from "seed-design/ui/avatar"; import { IdentityPlaceholder } from "seed-design/ui/identity-placeholder"; export default function AvatarFallbackExample() { return ( } /> } /> ); } ``` --- file: components/badge.mdx # Badge 객체의 속성이나 상태를 시각적으로 표현하는 작은 텍스트 라벨입니다. 사용자의 주의를 끌고 콘텐츠의 빠른 인지와 탐색을 돕기 위해 사용됩니다. 사용 가능 버전: @seed-design/react@0.0.1, @seed-design/css@0.0.1 ## Preview ```tsx import { Badge } from "@seed-design/react"; export default function BadgePreview() { return 라벨; } ``` ## Usage ```tsx import { Badge } from "@seed-design/react"; ``` ```tsx Badge ``` ## Props ## Examples ### Truncating Behavior Badge는 긴 텍스트를 잘라내고 생략 부호를 표시합니다. ```tsx import { Badge, VStack } from "@seed-design/react"; export default function BadgeTruncating() { return ( In velit velit deserunt amet veniam incididunt consectetur incididunt Lorem. In velit velit deserunt amet veniam incididunt consectetur incididunt Lorem. ); } ``` ### Tones and Variants #### Neutral ```tsx import { Badge, HStack, VStack } from "@seed-design/react"; export default function BadgeNeutral() { return ( 라벨 라벨 라벨 라벨 라벨 라벨 ); } ``` #### Brand ```tsx import { Badge, HStack, VStack } from "@seed-design/react"; export default function BadgeBrand() { return ( 라벨 라벨 라벨 라벨 라벨 라벨 ); } ``` #### Informative ```tsx import { Badge, HStack, VStack } from "@seed-design/react"; export default function BadgeInformative() { return ( 라벨 라벨 라벨 라벨 라벨 라벨 ); } ``` #### Positive ```tsx import { Badge, HStack, VStack } from "@seed-design/react"; export default function BadgePositive() { return ( 라벨 라벨 라벨 라벨 라벨 라벨 ); } ``` #### Warning ```tsx import { Badge, HStack, VStack } from "@seed-design/react"; export default function BadgeWarning() { return ( 라벨 라벨 라벨 라벨 라벨 라벨 ); } ``` #### Critical ```tsx import { Badge, HStack, VStack } from "@seed-design/react"; export default function BadgeCritical() { return ( 라벨 라벨 라벨 라벨 라벨 라벨 ); } ``` --- file: components/bottom-sheet.mdx # Bottom Sheet 화면 하단에서 올라오는 모달 컴포넌트입니다. 추가 정보나 액션 목록을 제공하면서도 현재 컨텍스트를 유지할 때 사용됩니다. 사용 가능 버전: @seed-design/react@0.0.1, @seed-design/css@0.0.1 ## Preview ```tsx import { ActionButton } from "seed-design/ui/action-button"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; const BottomSheetPreview = () => { return ( Open {/* If you need to omit padding, pass px={0}. */} Content 확인 ); }; export default BottomSheetPreview; ``` Stackflow와 Bottom Sheet를 함께 사용하는 방법에 대해 알아보세요. ## Installation - npm: npx @seed-design/cli@latest add ui:bottom-sheet - pnpm: pnpm dlx @seed-design/cli@latest add ui:bottom-sheet - yarn: yarn dlx @seed-design/cli@latest add ui:bottom-sheet - bun: bun x @seed-design/cli@latest add ui:bottom-sheet ## Props ### `BottomSheetRoot` ### `BottomSheetTrigger` ### `BottomSheetContent` ### `BottomSheetBody` ### `BottomSheetFooter` ## Examples ### Trigger ``는 `asChild` 패턴을 사용해 자식 요소가 BottomSheet를 열 수 있도록 합니다. ``는 `aria-haspopup="dialog"` 속성을 설정하고, `BottomSheet`의 `open` 상태에 따라 `aria-expanded` 속성을 자동으로 설정합니다. 이 속성은 스크린 리더와 같은 보조 기술에 유용합니다. ```tsx import { ActionButton } from "seed-design/ui/action-button"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; const BottomSheetTriggerExample = () => { return ( Open Content 확인 ); }; export default BottomSheetTriggerExample; ``` ### Controlled Trigger 외의 방식으로 BottomSheet를 열고 닫을 수 있습니다. 이 경우 `open` prop을 사용하여 BottomSheet의 상태를 제어합니다. ```tsx import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, } from "seed-design/ui/bottom-sheet"; const BottomSheetControlled = () => { const [open, setOpen] = useState(false); const scheduleOpen = () => { setTimeout(() => { setOpen(true); }, 1000); }; return ( <> 1초 후 열기 {/* If you need to omit padding, pass px={0}. */} Content 확인 ); }; export default BottomSheetControlled; ``` ### `onOpenChange` Details `onOpenChange` 두 번째 인자로 `details`가 제공됩니다. #### `reason` **열릴 때** (`open: true`) - `"trigger"`: `BottomSheetTrigger` (`BottomSheet.Trigger`)로 열림 **닫힐 때** (`open: false`) - `"closeButton"`: `BottomSheet.CloseButton`으로 닫힘 - `"escapeKeyDown"`: ESC 키 사용 - `"interactOutside"`: 외부 영역 클릭 - `"drag"`: 드래그로 닫힘 - `"handleClickOnLastSnapPoint"`: 마지막 스냅 포인트에서 핸들 클릭으로 닫힘 ```tsx import { HStack, Text, VStack } from "@seed-design/react"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { BottomSheetBody, BottomSheetContent, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; const snapPoints = ["200px", "400px", 1]; export default function BottomSheetOnOpenChangeReason() { const [open, setOpen] = useState(false); const [snap, setSnap] = useState(snapPoints[0]); const [openReason, setOpenReason] = useState(null); const [closeReason, setCloseReason] = useState(null); return ( { setOpen(open); (open ? setOpenReason : setCloseReason)(details?.reason ?? null); }} snapPoints={snapPoints} activeSnapPoint={snap} setActiveSnapPoint={setSnap} > 열기 ESC 키를 누르거나, 외부 영역을 클릭하거나, 아래로 스와이프하거나, 핸들을 탭하여 snap point를 순환해보세요. 마지막 열림 이유: {openReason ?? "-"} 마지막 닫힘 이유: {closeReason ?? "-"} ); } ``` ### Header Align ``에 `headerAlign` prop을 설정하여 title과 description의 정렬을 설정할 수 있습니다. ```tsx import { HStack } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; const BottomSheetHeaderAlign = () => { return ( Left (기본값) Content 확인 Center Content 확인 ); }; export default BottomSheetHeaderAlign; ``` ### Max Height ``에 `maxHeight` prop을 전달하여 BottomSheet의 최대 높이를 설정할 수 있습니다. ```tsx import { Box, VStack } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; const BottomSheetMaxHeight = () => { return ( Open {/* If you need to omit padding, pass px={0}. */} 확인 ); }; export default BottomSheetMaxHeight; ``` ### Snap Points `snapPoints` prop을 사용하여 BottomSheet의 커스텀 스냅 포인트를 설정할 수 있습니다. ```tsx import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; const snapPoints = ["300px", "500px", 1]; const BottomSheetSnapPoints = () => { const [snap, setSnap] = useState(snapPoints[0]); return ( Open Content 확인 ); }; export default BottomSheetSnapPoints; ``` ### Fade From Index ``에 `fadeFromIndex` prop을 전달하여 뒷 배경이 어두워지는 시작 인덱스를 설정할 수 있습니다. ```tsx import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; const snapPoints = ["300px", "500px", 1]; const BottomSheetFadeFromIndex = () => { const [snap, setSnap] = useState(snapPoints[0]); return ( Open Content 확인 ); }; export default BottomSheetFadeFromIndex; ``` ### Show Handle ``에 `showHandle` prop을 전달하여 Handle을 표시할 수 있습니다. 기본 값은 `false`입니다. ```tsx import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; const BottomSheetShowHandle = () => { const [isSheetOpen, setIsSheetOpen] = useState(false); return ( Open {/* If you need to omit padding, pass px={0}. */} Content setIsSheetOpen(false)}> 닫기 ); }; export default BottomSheetShowHandle; ``` ### Show Close Button ``에 `showCloseButton` prop을 전달하여 닫기 버튼을 표시할 수 있습니다. 기본 값은 `true`입니다. showCloseButton을 `false`로 설정하면 닫기 버튼이 표시되지 않습니다. 이 경우 유저가 BottomSheet를 닫을 수 있는 방법을 제공해야 합니다. ```tsx import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; const BottomSheetShowCloseButton = () => { const [isSheetOpen, setIsSheetOpen] = useState(false); return ( Open {/* If you need to omit padding, pass px={0}. */} Content setIsSheetOpen(false)}> 닫기 ); }; export default BottomSheetShowCloseButton; ``` ### Dismissible `dismissible` prop을 `false`로 설정하면 closeOnEscape, closeOnInteractOutside, draggable 기능이 비활성화됩니다. 의도적으로 BottomSheet를 닫을 수 없게 하고 싶을 때 사용합니다. 이외에는 유저가 BottomSheet를 닫을 수 있는 방법을 제공해야 합니다. ```tsx import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; const BottomSheetDismissible = () => { const [open, setOpen] = useState(false); return ( Open {/* If you need to omit padding, pass px={0}. */} Content setOpen(false)}> 닫기 ); }; export default BottomSheetDismissible; ``` ### With Scroll Fog ``에 [Scroll Fog](/react/components/scroll-fog)를 사용하여 스크롤 힌트 효과를 적용할 수 있습니다. ScrollFog는 항상 효과를 표시하므로, 충분한 padding을 제공해야 합니다. 권장 padding인 하단 `80px`, 상단 `20px`을 유지해야 합니다. ```tsx import { Box, ScrollFog, VStack } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; const BottomSheetMaxHeight = () => { return ( Open {/* If you need to omit padding, pass px={0}. */} 확인 ); }; export default BottomSheetMaxHeight; ``` ### Bottom Inset ``에 `style` prop을 전달하여 아래 여백을 주기 위해 사용합니다. ```tsx import { ActionButton } from "seed-design/ui/action-button"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; const BottomSheetBottomInset = () => { return ( Open {/* If you need to omit padding, pass px={0}. */} Content 확인 ); }; export default BottomSheetBottomInset; ``` ### Handle Only ``에 `handleOnly` prop을 제공하는 경우 쓸어서(swipe) Bottom Sheet를 움직일 수 있는 영역이 핸들 부분으로 제한됩니다. 이 옵션은 ``와 함께 사용할 때만 작동합니다. ```tsx import { ActionButton } from "seed-design/ui/action-button"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; const BottomSheetHandleOnly = () => { return ( Open Content 확인 ); }; export default BottomSheetHandleOnly; ``` ### Skip Animation `skipAnimation` prop을 사용하여 BottomSheet의 enter/exit 애니메이션을 건너뛸 수 있습니다. ```tsx import { ActionButton } from "seed-design/ui/action-button"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; const BottomSheetSkipAnimation = () => { return ( Open Content 확인 ); }; export default BottomSheetSkipAnimation; ``` --- file: components/callout.mdx # Callout 사용자에게 중요한 정보나 팁을 시각적으로 강조하여 전달하는 메시지 컴포넌트입니다. 사용 가능 버전: @seed-design/react@0.0.1, @seed-design/css@0.0.1 ## Preview ```tsx import { VStack } from "@seed-design/react"; import { ActionableCallout, Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutPreview() { return ( ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:callout - pnpm: pnpm dlx @seed-design/cli@latest add ui:callout - yarn: yarn dlx @seed-design/cli@latest add ui:callout - bun: bun x @seed-design/cli@latest add ui:callout ## Props ### `Callout` ### `ActionableCallout` ### `DismissibleCallout` ## Examples ### Content Layout #### Text Only ```tsx import { VStack } from "@seed-design/react"; import { ActionableCallout, Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutTextOnly() { return ( ); } ``` #### With Icon ```tsx import { IconCalendarFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; import { ActionableCallout, Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutWithIcon() { return ( } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> ); } ``` #### With Title Text ```tsx import { VStack } from "@seed-design/react"; import { ActionableCallout, Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutWithTitleText() { return ( ); } ``` #### With Link Label `ActionableCallout`에서는 `linkProps`를 제공하지 않아요. ```tsx import { VStack } from "@seed-design/react"; import { Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutWithLinkLabel() { return ( ); } ``` #### With All ```tsx import { IconCalendarFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; import { Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutWithAll() { return ( } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." linkProps={{ children: "시도해 보기" }} /> } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." linkProps={{ children: "시도해 보기" }} /> ); } ``` ### Customizable Parts #### Rendering Link Label as Child ```tsx import { VStack } from "@seed-design/react"; import { Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutLinkLabelAsChild() { return ( 시도해 보기 ), }} /> 시도해 보기 ), }} /> ); } ``` ### Tones #### Neutral (Default) ```tsx import { IconCalendarFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; import { ActionableCallout, Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutNeutral() { return ( } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> ); } ``` #### Informative ```tsx import { IconCalendarFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; import { ActionableCallout, Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutInformative() { return ( } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> ); } ``` #### Positive ```tsx import { IconCalendarFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; import { ActionableCallout, Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutPositive() { return ( } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> ); } ``` #### Warning ```tsx import { IconCalendarFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; import { ActionableCallout, Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutWarning() { return ( } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> ); } ``` #### Critical ```tsx import { IconCalendarFill } from "@karrotmarket/react-monochrome-icon"; import { VStack } from "@seed-design/react"; import { ActionableCallout, Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutCritical() { return ( } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> } description="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> ); } ``` #### Magic ```tsx import { IconSparkle2 } from "@karrotmarket/react-multicolor-icon"; import { VStack } from "@seed-design/react"; import { ActionableCallout, Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutMagic() { return ( } /> } /> } /> ); } ``` --- file: components/checkbox.mdx # Checkbox 사용자가 하나 이상의 옵션을 선택할 수 있게 해주는 컴포넌트입니다. 목록에서 여러 항목을 선택하거나 약관 동의와 같은 선택적 작업에 사용됩니다. 사용 가능 버전: @seed-design/react@0.0.1, @seed-design/css@0.0.1 ## Preview ```tsx import { VStack } from "@seed-design/react"; import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox"; export default function CheckboxPreview() { return ( ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:checkbox - pnpm: pnpm dlx @seed-design/cli@latest add ui:checkbox - yarn: yarn dlx @seed-design/cli@latest add ui:checkbox - bun: bun x @seed-design/cli@latest add ui:checkbox ## Props ### `CheckboxGroup` ### `Checkbox` ### `Checkmark` ## Examples ### Sizes ```tsx import { HStack } from "@seed-design/react"; import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox"; export default function CheckboxSize() { return ( ); } ``` ### Tones and Variants #### Brand ```tsx import { VStack } from "@seed-design/react"; import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox"; export default function CheckboxBrand() { return ( ); } ``` #### Neutral ```tsx import { VStack } from "@seed-design/react"; import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox"; export default function CheckboxNeutral() { return ( ); } ``` ### Indeterminate ```tsx import { VStack } from "@seed-design/react"; import { Checkbox } from "seed-design/ui/checkbox"; export default function CheckboxIndeterminate() { return ( ); } ``` ### Weights `weight="default"`와 `weight="stronger"`는 더 이상 사용되지 않습니다. 대신 `weight="regular"`와 `weight="bold"`를 사용하세요. ```tsx import { VStack } from "@seed-design/react"; import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox"; export default function CheckboxWeights() { return ( ); } ``` ### Long Label ```tsx import { VStack } from "@seed-design/react"; import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox"; export default function CheckboxLongLabel() { return ( ); } ``` ### Disabled ```tsx import { VStack } from "@seed-design/react"; import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox"; export default function CheckboxDisabled() { return ( ); } ``` ### Listening to Value Changes `onCheckedChange`를 사용하여 체크박스의 선택 상태 변경을 감지할 수 있습니다. 이벤트를 활용해야 하는 경우 `inputProps`를 통해 내부 `` 요소에 직접 이벤트 핸들러를 추가할 수 있습니다. ```tsx import { VStack, Text } from "@seed-design/react"; import { Checkbox } from "seed-design/ui/checkbox"; import { useState } from "react"; export default function CheckboxValueChanges() { const [count, setCount] = useState(0); const [lastValue, setLastValue] = useState(null); return ( { setCount((prev) => prev + 1); setLastValue(checked); }} /> onCheckedChange called: {count} times, last value: {`${lastValue ?? "-"}`} ); } ``` ### Use Cases #### React Hook Form ```tsx import { HStack, VStack } from "@seed-design/react"; import { useCallback, type FormEvent } from "react"; import { useController, useForm, type Control } from "react-hook-form"; import { ActionButton } from "seed-design/ui/action-button"; import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox"; const POSSIBLE_FRUIT_VALUES = ["apple", "melon", "mango"] as const; type FormValues = Record<(typeof POSSIBLE_FRUIT_VALUES)[number], boolean>; export default function CheckboxReactHookForm() { const { handleSubmit, reset, setValue, control } = useForm({ defaultValues: { apple: false, melon: true, mango: false, }, }); const onValid = useCallback((data: FormValues) => { window.alert(JSON.stringify(data, null, 2)); }, []); const onReset = useCallback( (event: FormEvent) => { event.preventDefault(); reset(); }, [reset], ); return ( {POSSIBLE_FRUIT_VALUES.map((name) => ( ))} 초기화 setValue("mango", true)} > mango 선택 제출 ); } interface CheckboxItemProps { name: keyof FormValues; control: Control; } function CheckboxItem({ name, control }: CheckboxItemProps) { const { field: { value, ...restProps }, fieldState: { invalid }, } = useController({ name, control }); return ( ); } ``` #### Using `Checkmark` `Checkmark`는 독립적인 체크 마크 컴포넌트로, Checkbox Primitive 컴포넌트와 조합하여 커스텀 레이아웃을 위해 사용할 수 있습니다. ```tsx import { HStack, Text, VStack } from "@seed-design/react"; import { Checkbox } from "@seed-design/react/primitive"; import { Checkmark } from "seed-design/ui/checkbox"; function CustomCheckbox({ children, ...props }: Checkbox.RootProps) { return ( {children} ); } export default function CheckboxCheckmark() { return ( regular medium bold ); } ``` ### Fieldset Integration `CheckboxGroup`을 사용하여 여러 체크박스를 그룹화하고 `label`, `description`, `errorMessage` 등의 Fieldset 관련 prop을 사용할 수 있습니다. ```tsx import { ActionButton, HStack, VStack } from "@seed-design/react"; import { useState } from "react"; import { Checkbox, CheckboxGroup } from "seed-design/ui/checkbox"; export default function CheckboxFieldset() { const [firstErrors, setFirstErrors] = useState>({}); const [secondErrors, setSecondErrors] = useState>({}); const handleFirstSubmit = (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.currentTarget); const fruits = formData.getAll("fruit"); if (fruits.includes("apple")) { setFirstErrors({ apple: "Apple은 선택할 수 없습니다." }); return; } setFirstErrors({}); alert(JSON.stringify(fruits, null, 2)); }; const handleSecondSubmit = (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.currentTarget); const agreements = formData.getAll("agreement"); const hasTerms = agreements.includes("terms"); const hasPrivacy = agreements.includes("privacy"); if (!hasTerms || !hasPrivacy) { setSecondErrors({ ...(!hasTerms && { terms: "필수 항목에 동의해 주세요." }), ...(!hasPrivacy && { privacy: "필수 항목에 동의해 주세요." }), }); return; } setSecondErrors({}); alert(JSON.stringify(agreements, null, 2)); }; return (
제출
제출
); } ``` ## Scale Feedback `Checkbox`는 눌렸을 때 `Checkmark`가 살짝 줄어드는 피드백을 제공합니다. `Checkmark`를 감싸는 요소에 [Scale Feedback](/react/components/concepts/scale-feedback)을 직접 적용하는 경우, 바깥 요소와 `Checkmark`가 함께 줄어들어 축소가 이중으로 나타납니다. `Checkmark`의 상위 요소에서 `--seed-checkmark-feedback-scale`을 `1`로 설정하면 `Checkmark`의 축소만 끌 수 있습니다. 이 변수는 상속되므로 `Checkbox` 전체를 감싸는 요소에 설정해도 됩니다. ```css title="MyRow.css" .my-row { --seed-checkmark-feedback-scale: 1; } ``` --- file: components/chip-tabs.mdx # Chip Tabs Chip 형태로 표현된 탭 컴포넌트입니다. 카테고리나 필터를 선택하여 콘텐츠를 전환할 때 사용됩니다. 사용 가능 버전: @seed-design/react@0.0.1, @seed-design/css@0.0.1 ## Preview ```tsx import * as React from "react"; import { ChipTabsList, ChipTabsRoot, ChipTabsTrigger } from "seed-design/ui/chip-tabs"; export default function ChipTabsPreview() { const [value, setValue] = React.useState("1"); return ( <> setValue(value)} > 라벨1 라벨2 라벨3 {value === "1" &&
content 1
} {value === "2" &&
content 2
} {value === "3" &&
content 3
} ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:chip-tabs - pnpm: pnpm dlx @seed-design/cli@latest add ui:chip-tabs - yarn: yarn dlx @seed-design/cli@latest add ui:chip-tabs - bun: bun x @seed-design/cli@latest add ui:chip-tabs ## Props ### `ChipTabsRoot` ### `ChipTabsList` ### `ChipTabsTrigger` ### `ChipTabsCarousel` ### `ChipTabsContent` ## Examples ### Size=Medium ```tsx import * as React from "react"; import { ChipTabsList, ChipTabsRoot, ChipTabsTrigger } from "seed-design/ui/chip-tabs"; export default function ChipTabsSizeMedium() { const [value, setValue] = React.useState("1"); return ( <> setValue(value)} > 라벨1 라벨2 라벨3 {value === "1" &&
content 1
} {value === "2" &&
content 2
} {value === "3" &&
content 3
} ); } ``` ### Size=Large ```tsx import * as React from "react"; import { ChipTabsList, ChipTabsRoot, ChipTabsTrigger } from "seed-design/ui/chip-tabs"; export default function ChipTabsSizeLarge() { const [value, setValue] = React.useState("1"); return ( <> setValue(value)} > 라벨1 라벨2 라벨3 {value === "1" &&
content 1
} {value === "2" &&
content 2
} {value === "3" &&
content 3
} ); } ``` ### Variant=Neutral Solid ```tsx import * as React from "react"; import { ChipTabsList, ChipTabsRoot, ChipTabsTrigger } from "seed-design/ui/chip-tabs"; export default function ChipTabsVariantNeutralSolid() { const [value, setValue] = React.useState("1"); return ( <> setValue(value)} > 라벨1 라벨2 라벨3 {value === "1" &&
content 1
} {value === "2" &&
content 2
} {value === "3" &&
content 3
} ); } ``` ### Variant=Neutral Outline ```tsx import * as React from "react"; import { ChipTabsList, ChipTabsRoot, ChipTabsTrigger } from "seed-design/ui/chip-tabs"; export default function ChipTabsVariantNeutralOutline() { const [value, setValue] = React.useState("1"); return ( <> setValue(value)} > 라벨1 라벨2 라벨3 {value === "1" &&
content 1
} {value === "2" &&
content 2
} {value === "3" &&
content 3
} ); } ``` ### Notification ```tsx import * as React from "react"; import { ChipTabsList, ChipTabsRoot, ChipTabsTrigger } from "seed-design/ui/chip-tabs"; export default function ChipTabsNotification() { const [value, setValue] = React.useState("1"); return ( <> setValue(value)} > 라벨1 라벨2 라벨3 {value === "1" &&
content 1
} {value === "2" &&
content 2
} {value === "3" &&
content 3
} ); } ``` ### With Scroll Fog 탭이 많아 가로 스크롤이 필요한 경우 [Scroll Fog](/react/components/scroll-fog)를 적용하여 좌우 스크롤 힌트를 제공할 수 있습니다. 권장 padding인 `20px`을 유지해야 합니다. ```tsx import { ScrollFog } from "@seed-design/react"; import { ChipTabsCarousel, ChipTabsList, ChipTabsRoot, ChipTabsTrigger, } from "seed-design/ui/chip-tabs"; export default function ChipTabsWithScrollFog() { return (
{Array.from({ length: 15 }, (_, i) => ( 라벨{i + 1} ))}
); } ``` --- file: components/chip.mdx # Chip 사용자가 선택하거나 입력하는 값을 표시하는 컴포넌트입니다. 사용 가능 버전: @seed-design/react@0.1.4, @seed-design/css@0.1.4 ## Preview ```tsx import { HStack, VStack } from "@seed-design/react"; import { Chip } from "seed-design/ui/chip"; export default function ChipPreview() { return ( Button Chip Toggle Chip Radio Chip 1 Radio Chip 2 ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:chip - pnpm: pnpm dlx @seed-design/cli@latest add ui:chip - yarn: yarn dlx @seed-design/cli@latest add ui:chip - bun: bun x @seed-design/cli@latest add ui:chip ## Props ### `Chip.Button` ### `Chip.Toggle` ### `Chip.RadioRoot` ### `Chip.RadioItem` ## Examples ### Sizes #### Small ```tsx import { HStack, VStack } from "@seed-design/react"; import { Chip } from "seed-design/ui/chip"; export default function ChipSmall() { return ( Small Button Small Toggle Small Radio 1 Small Radio 2 ); } ``` #### Medium ```tsx import { HStack, VStack } from "@seed-design/react"; import { Chip } from "seed-design/ui/chip"; export default function ChipMedium() { return ( Medium Button Medium Toggle Medium Radio 1 Medium Radio 2 ); } ``` #### Large ```tsx import { HStack, VStack } from "@seed-design/react"; import { Chip } from "seed-design/ui/chip"; export default function ChipLarge() { return ( Large Button Large Toggle Large Radio 1 Large Radio 2 ); } ``` ### Variants #### Solid ```tsx import { HStack, VStack } from "@seed-design/react"; import { Chip } from "seed-design/ui/chip"; export default function ChipSolid() { return ( Solid Button Solid Toggle Solid Radio 1 Solid Radio 2 ); } ``` #### Outline Strong ```tsx import { HStack, VStack } from "@seed-design/react"; import { Chip } from "seed-design/ui/chip"; export default function ChipOutlineStrong() { return ( Outline Strong Button Outline Strong Toggle Outline Strong Radio 1 Outline Strong Radio 2 ); } ``` #### Outline Weak ```tsx import { HStack, VStack } from "@seed-design/react"; import { Chip } from "seed-design/ui/chip"; export default function ChipOutlineWeak() { return ( Outline Weak Button Outline Weak Toggle Outline Weak Radio 1 Outline Weak Radio 2 ); } ``` ### Prefix Icon ```tsx import { HStack, VStack, Icon } from "@seed-design/react"; import { Chip } from "seed-design/ui/chip"; import { IconHeartFill } from "@karrotmarket/react-monochrome-icon"; export default function ChipPrefixIcon() { return ( } /> With Icon Button } /> With Icon Toggle } /> With Icon Radio 1 } /> With Icon Radio 2 ); } ``` ### Suffix Icon ```tsx import { HStack, VStack, Icon } from "@seed-design/react"; import { Chip } from "seed-design/ui/chip"; import { IconChevronDownLine } from "@karrotmarket/react-monochrome-icon"; export default function ChipSuffixIcon() { return ( Button with Suffix } /> Toggle with Suffix } /> Radio with Suffix 1 } /> Radio with Suffix 2 } /> ); } ``` ### Icon Only ```tsx import { HStack, VStack, Icon } from "@seed-design/react"; import { Chip } from "seed-design/ui/chip"; import { IconArrowClockwiseCircularLine, IconBellLine, IconBellSlashLine, IconTimer_10Line, IconTimer_3Line, } from "@karrotmarket/react-monochrome-icon"; import { useState } from "react"; export default function ChipIconOnly() { const [checked, setChecked] = useState(false); return ( } /> : } /> } /> } /> ); } ``` ### Prefix Avatar [Avatar](/react/components/avatar)와 함께 사용할 수 있습니다. ```tsx import { HStack, VStack } from "@seed-design/react"; import { Chip } from "seed-design/ui/chip"; import { Avatar } from "seed-design/ui/avatar"; import { IdentityPlaceholder } from "seed-design/ui/identity-placeholder"; export default function ChipPrefixAvatar() { return ( } /> With Avatar Button } /> With Avatar Toggle } /> With Avatar Radio 1 } /> With Avatar Radio 2 ); } ``` ### Listening to Value Changes - `Chip.Toggle` - `onCheckedChange`를 사용하여 토글 상태 변경을 감지할 수 있습니다. - 이벤트를 활용해야 하는 경우 `inputProps`를 통해 내부 `` 요소에 직접 이벤트 핸들러를 추가할 수 있습니다. - `Chip.RadioRoot` / `Chip.RadioItem` - `Chip.RadioRoot`의 `onValueChange`를 사용하여 라디오 버튼의 선택 값 변경을 감지할 수 있습니다. - 이벤트를 활용해야 하는 경우 `Chip.RadioItem`의 `inputProps`를 통해 내부 `` 요소에 직접 이벤트 핸들러를 추가할 수 있습니다. ```tsx import { HStack, VStack, Text } from "@seed-design/react"; import { Chip } from "seed-design/ui/chip"; import { useState } from "react"; export default function ChipValueChanges() { const [toggleCount, setToggleCount] = useState(0); const [toggleLastValue, setToggleLastValue] = useState(null); const [radioCount, setRadioCount] = useState(0); const [radioLastValue, setRadioLastValue] = useState(null); return ( { setToggleCount((prev) => prev + 1); setToggleLastValue(checked); }} > Toggle Chip onCheckedChange called: {toggleCount} times, last value: {`${toggleLastValue ?? "-"}`} { setRadioCount((prev) => prev + 1); setRadioLastValue(value); }} > Radio 1 Radio 2 onValueChange called: {radioCount} times, last value: {radioLastValue ?? "-"} ); } ``` ## Migrating from ActionChip/ControlChip [Action Chip](/react/components/action-chip)과 [Control Chip](/react/components/control-chip)을 대체합니다. ### ActionChip → Chip.Button ```tsx // Before import { ActionChip } from "@seed-design/react"; Label // After import { Chip } from "@seed-design/react"; Label ``` ### ControlChip → Chip.Toggle ```tsx // Before import { ControlChip } from "@seed-design/react"; Label // After import { Chip } from "@seed-design/react"; Label ``` --- file: components/concepts/composition.mdx # Composition 컴포넌트를 조합하여 복잡한 UI를 구성하는 방법과 원칙을 설명합니다. ## `as` Prop `as` prop은 React 컴포넌트가 렌더링하는 기본 HTML 요소를 변경하는 데 사용됩니다. 컴포넌트의 기능은 유지하면서 기본 요소를 변경할 수 있는 간단한 방법을 제공합니다. ```jsx 단락 텍스트 인라인 텍스트 제목 텍스트 ``` ## `asChild` Prop `asChild` prop은 컴포넌트의 기능을 자식 요소에 합성하는 데 사용됩니다. 이를 활용해 HTML 태그를 변경하거나, 다른 컴포넌트에 기능을 주입할 수 있습니다. [Radix UI](https://www.radix-ui.com/primitives/docs/utilities/slot)에서 제공하는 asChild 패턴과 동일한 기능을 제공합니다. ### 요소 타입 변경하기 ```jsx // FAB는 기본적으로 button 태그를 사용하지만, asChild를 사용해 링크로 변경할 수 있습니다. } /> ``` > 기본 요소 타입을 변경하는 경우, 해당 요소가 접근성을 유지하고 올바르게 작동하는지 확인하는 것은 개발자의 책임입니다. 예를 들어, 버튼을 `div`로 변경하면 더 이상 키보드로 접근할 수 없게 될 수 있습니다. ### React 컴포넌트와 조합하기 ```jsx // 라우터 라이브러리에서 제공하는 Link 컴포넌트에 기능을 합성할 수 있습니다. ``` `asChild`를 사용하여 컴포넌트를 조합할 때는 두 가지 원칙을 따라야 합니다. #### 1. 컴포넌트는 props를 전개해야 합니다 asChild는 기능을 제공하고 접근성을 유지하기 위한 고유의 props와 이벤트 핸들러를 전달합니다. 컴포넌트가 이러한 props를 지원하지 않으면 제대로 작동하지 않습니다. ```jsx // 잘못된 방법 const MyButton = () => } /> } /> : } /> {isCopied ? "복사됨" : "URL 복사"} } /> } href={href} target="_blank" rel="noreferrer" /> ); } ``` ### `input`s in List Items `ListSwitchItem`, `ListCheckItem`, `ListRadioItem`을 사용해서 리스트 항목에 `input` 요소를 포함할 수 있습니다. 이때, [Switchmark](/react/components/switch#using-switchmark), [Checkmark](/react/components/checkbox#using-checkmark) 또는 [Radiomark](/react/components/radio-group#using-radiomark)와 같은 컨트롤 요소를 `prefix`나 `suffix` 영역에 넣어 사용합니다. ```tsx import { IconTrashcanLine } from "@karrotmarket/react-monochrome-icon"; import { IconSparkle2 } from "@karrotmarket/react-multicolor-icon"; import { Icon } from "@seed-design/react"; import { List, ListDivider, ListSwitchItem } from "seed-design/ui/list"; import { Switchmark } from "seed-design/ui/switch"; export default function ListSwitch() { return ( } />} suffix={} /> } />} suffix={} defaultChecked /> ); } ``` ```tsx import { Badge, HStack } from "@seed-design/react"; import { List, ListDivider, ListCheckItem } from "seed-design/ui/list"; import { Checkmark } from "seed-design/ui/checkbox"; export default function ListCheckbox() { return ( 알림 수신 동의 권장 } detail="푸시 알림을 받으시겠습니까?" suffix={} defaultChecked /> } title="마케팅 정보 수신 동의" detail="마케팅 정보를 받으시겠습니까?" defaultChecked /> } title="Ghost Variant" /> ); } ``` ```tsx import { RadioGroup } from "@seed-design/react/primitive"; import { List, ListDivider, ListRadioItem } from "seed-design/ui/list"; import { Radiomark } from "seed-design/ui/radio-group"; export default function ListRadio() { return ( } /> } value="option2" title="옵션 2" detail="두 번째 선택지" /> } value="option3" title="옵션 3" detail="세 번째 선택지" /> ); } ``` ### Accessibility `List`는 기본적으로 `
    `입니다. `ListCheckItem`와 `ListRadioItem`를 사용하는 경우 `List`에 적절한 role을 부여해야 합니다. ```tsx {/* [!code highlight] */} } title="알림 수신 동의" detail="푸시 알림을 받으시겠습니까?" /> } title="마케팅 정보 수신 동의" detail="마케팅 정보를 받으시겠습니까?" /> ``` ```tsx {/* [!code highlight] */} {/*
    */} } value="짜장" title="짜장" /> } value="짬뽕" title="짬뽕" /> ``` ### Disabled ```tsx import { IconChevronRightLine, IconPersonCircleLine, IconSlashCircleLine, } from "@karrotmarket/react-monochrome-icon"; import { Divider, Icon, VStack } from "@seed-design/react"; import { RadioGroup } from "@seed-design/react/primitive"; import { List, ListButtonItem, ListCheckItem, ListRadioItem } from "seed-design/ui/list"; import { Checkmark } from "seed-design/ui/checkbox"; import { Radiomark } from "seed-design/ui/radio-group"; export default function ListDisabled() { return ( } />} title="활성화된 ListButtonItem" detail="Cupidatat et pariatur amet." suffix={} />} /> } />} title="활성화된 ListCheckItem" suffix={} /> } />} title="활성화된 ListRadioItem" suffix={} value="foo" /> } />} title="비활성화된 ListButtonItem" detail="Cupidatat et pariatur amet." suffix={} />} /> } />} title="비활성화된 ListCheckItem" suffix={} /> } />} title="비활성화된 ListRadioItem" suffix={} value="foo" /> ); } ``` ### Variants #### Highlighted ```tsx import { IconPersonCircleLine } from "@karrotmarket/react-monochrome-icon"; import { Box, Icon, VStack } from "@seed-design/react"; import { useState } from "react"; import { List, ListDivider, ListItem, ListButtonItem } from "seed-design/ui/list"; import { Switch } from "seed-design/ui/switch"; export default function ListHighlighted() { const [highlighted, setHighlighted] = useState(true); return ( } />} title="버튼" detail="Enim aute duis magna mollit aute sit aliquip duis ut tempor sunt." onClick={() => {}} /> } />} title="하이라이트된 버튼" detail="Enim aute duis magna mollit aute sit aliquip duis ut tempor sunt." onClick={() => {}} /> } />} title="하이라이트 및 비활성화된 버튼" detail="Enim aute duis magna mollit aute sit aliquip duis ut tempor sunt." onClick={() => {}} /> } />} title="하이라이트" highlighted={highlighted} /> ); } ``` ### With Bottom Sheet ```tsx import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; import { ActionButton } from "seed-design/ui/action-button"; import { Checkmark } from "seed-design/ui/checkbox"; import { List, ListCheckItem } from "seed-design/ui/list"; import { PrefixIcon, VStack } from "@seed-design/react"; import { useState } from "react"; import { IconArrowClockwiseCircularFill } from "@karrotmarket/react-monochrome-icon"; const TYPES = ["버스", "지하철", "택시", "자전거", "도보"] as const; export default function ListBottomSheet() { const [isOpen, setIsOpen] = useState(false); const [selectedTypes, setSelectedTypes] = useState<(typeof TYPES)[number][]>([]); return ( BottomSheet 열기
    { e.preventDefault(); setSelectedTypes([]); }} onSubmit={(e) => { e.preventDefault(); setIsOpen(false); }} > {TYPES.map((type) => ( } onCheckedChange={() => { setSelectedTypes((prev) => prev.includes(type) ? prev.filter((t) => t !== type) : [...prev, type], ); }} /> ))} 경로 찾기 } /> 초기화
    ); } ``` ### Alignment `alignItems` prop으로 `prefix`와 `suffix`의 정렬 방식을 조정할 수 있습니다. ```tsx import { List, ListItem } from "seed-design/ui/list"; import { HStack } from "@seed-design/react"; import { IdentityPlaceholder } from "seed-design/ui/identity-placeholder"; import { Avatar } from "seed-design/ui/avatar"; export default function ListAlignment() { return ( } />} title="Prefix에 Avatar 넣기. Veniam elit velit esse ea incididunt sunt sit aute." detail="Et proident sit ullamco ut voluptate. Voluptate eiusmod occaecat adipisicing quis qui esse." /> } />} title="Prefix에 Avatar 넣고 상단으로 정렬하기. Veniam elit velit esse ea incididunt sunt sit aute." detail="일반적으로 `title`이 길어질 때 `alignItems`를 `flex-start`로 설정합니다." /> ); } ``` ### Border Radius `List`에 `itemBorderRadius` prop을 설정하여 active 상태에서 적용되는 리스트 항목의 radius를 조정할 수 있습니다. 리스트가 카드 등 모서리가 둥근 컨테이너 안에 포함되는 경우 유용합니다. ```tsx import { List, ListCheckItem, ListRadioItem } from "seed-design/ui/list"; import { ListHeader } from "seed-design/ui/list-header"; import { Checkmark } from "seed-design/ui/checkbox"; import { Radiomark } from "seed-design/ui/radio-group"; import { HStack, VStack } from "@seed-design/react"; import { RadioGroup } from "@seed-design/react/primitive"; export default function ListBorderRadius() { return ( 카드 borderRadius: r3_5 } /> } /> 카드 borderRadius: 22px } /> } /> ); } ``` ## Customization and Composition ### Usage @seed-design/react 패키지에서 제공하는 `List`와 `ListHeader` 컴포넌트는 다음과 같은 구조로 사용됩니다. ``` ListHeader List.Root └── List.Item ├── List.Prefix (선택사항) ├── List.Content │ ├── List.Title │ └── List.Detail (선택사항) └── List.Suffix (선택사항) └── List.Item ├── ... ``` ```tsx import { ListHeader, List, Icon } from "@seed-design/react"; 내 정보 } /> 내 프로필 다른 사람들에게 보이는 내 정보를 관리합니다. } /> {/* ... */} ``` - `ListHeader`: 리스트의 제목이나 설명을 표시하는 헤더 역할 - `List.Root`: 모든 리스트 항목을 감싸는 컨테이너 역할 - `List.Item`: 개별 리스트 항목. 클릭 가능한 영역을 정의 - `List.Prefix`: 아이콘, [Avatar](/react/components/avatar), [Checkmark](/react/components/checkbox#using-checkmark) 등을 표시할 수 있는 시작 영역 - `List.Content`: 주요 콘텐츠가 들어가는 중앙 영역 - `List.Title`: 리스트 항목의 제목 - `List.Detail`: 부가 설명이나 세부 정보 - `List.Suffix`: 아이콘, [Action Button](/react/components/action-button), [Toggle Button](/react/components/toggle-button) 등을 표시할 수 있는 끝 영역 ### `asChild` prop으로 적절한 시맨틱 요소와 조합하기 `asChild` prop에 대해 자세히 알아봅니다. #### Using `asChild` prop in `List.Content` 리스트 항목 전체 영역을 클릭 가능한 버튼으로 만드는 경우 활용할 수 있는 패턴입니다. 이 경우 `List.Item`에 `asChild` prop을 사용하지 않도록 유의하세요. `List.Prefix` 또는 `List.Suffix`에 버튼을 넣는 경우 `button`이 중첩되는 등 유효하지 않은 HTML이 생성됩니다. ```tsx import { List as SeedList } from "@seed-design/react"; {/* [!code highlight] */} {/* [!code highlight] */} {/* [!code highlight] */} alert("보기 클릭됨")} > 보기 ``` [Snippet](/react/components/concepts/snippet)으로 제공되는 `ListButtonItem` 및 `ListLinkItem`는 이 패턴을 쉽게 구현할 수 있도록 돕습니다. ```tsx import { ListButtonItem } from "seed-design/ui/list"; alert("사용자 클릭됨")} title="사용자" detail="항목 6개" suffix={ alert("보기 클릭됨")} > 보기 } /> ``` #### Using `asChild` prop in `List.Item` 리스트 항목 전체 영역을 `label`로 만들고, `List.Prefix` 또는 `List.Suffix`에 [Switchmark](/react/components/switch#using-switchmark), [Checkmark](/react/components/checkbox#using-checkmark) 또는 [Radiomark](/react/components/radio-group#using-radiomark)를 넣는 경우 활용할 수 있는 패턴입니다. ```tsx import { List as SeedList } from "@seed-design/react"; import { Checkbox } from "@seed-design/react/primitive"; {/* [!code highlight] */} {/* ``` [Snippet](/react/components/concepts/snippet)으로 제공되는 `ListSwitchItem`, `ListCheckItem` 및 `ListRadioItem`는 이 패턴을 쉽게 구현할 수 있도록 돕습니다. ```tsx import { ListCheckItem } from "seed-design/ui/list"; } /> ``` --- file: components/manner-temp-badge.mdx # Manner Temp Badge 매너 온도를 배지 형태로 표현하는 컴포넌트입니다. 콤팩트한 공간에서 사용자의 매너 온도 레벨을 간단히 표시할 때 사용됩니다. 사용 가능 버전: @seed-design/react@0.0.1, @seed-design/css@0.0.1 ## Preview ```tsx import { VStack } from "@seed-design/react"; import { MannerTempBadge } from "seed-design/ui/manner-temp-badge"; export default function BadgePreview() { return ( ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:manner-temp-badge - pnpm: pnpm dlx @seed-design/cli@latest add ui:manner-temp-badge - yarn: yarn dlx @seed-design/cli@latest add ui:manner-temp-badge - bun: bun x @seed-design/cli@latest add ui:manner-temp-badge ## Props --- file: components/manner-temp.mdx # Manner Temp 사용자의 매너온도를 시각적으로 표현하는 컴포넌트입니다. 신뢰도/매너 정도를 직관적으로 보여주는 데에 사용합니다. 사용 가능 버전: @seed-design/react@0.0.9, @seed-design/css@0.0.9 ## Preview ```tsx import { VStack } from "@seed-design/react"; import { MannerTemp } from "seed-design/ui/manner-temp"; export default function MannerTempPreview() { return ( ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:manner-temp - pnpm: pnpm dlx @seed-design/cli@latest add ui:manner-temp - yarn: yarn dlx @seed-design/cli@latest add ui:manner-temp - bun: bun x @seed-design/cli@latest add ui:manner-temp ## Props --- file: components/menu.mdx # Menu 사용자가 취할 수 있는 선택지나 액션 리스트를 제공하는 컴포넌트입니다. 사용 가능 버전: @seed-design/react@2.0.0, @seed-design/css@2.0.0 ## Preview ```tsx import { IconPencilLine, IconPlusLine, IconTrashcanLine, } from "@karrotmarket/react-monochrome-icon"; import { ActionButton } from "seed-design/ui/action-button"; import { MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuRoot, MenuTrigger, } from "seed-design/ui/menu"; export default function MenuPreview() { return ( 열기 작업 } /> } /> } /> ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:menu - pnpm: pnpm dlx @seed-design/cli@latest add ui:menu - yarn: yarn dlx @seed-design/cli@latest add ui:menu - bun: bun x @seed-design/cli@latest add ui:menu ## Usage Menu는 아래와 같은 구조로 구성됩니다. ```tsx import { MenuRoot, MenuTrigger, MenuContent, MenuGroup, MenuGroupLabel, MenuItem, } from "seed-design/ui/menu"; ``` ```tsx ... 그룹 제목 ``` - `MenuRoot`: 메뉴의 상태(열림/닫힘)와 위치를 관리합니다. - `MenuTrigger`: 클릭 시 메뉴를 열고 닫는 트리거입니다. - `MenuAnchor`: 메뉴의 위치 기준점만 제공하며, 열고 닫는 동작은 포함하지 않습니다. 트리거 동작을 직접 제어해야 하는 경우에 `MenuTrigger` 대신 사용합니다. - `MenuContent`: 메뉴 항목들을 감싸는 플로팅 컨테이너입니다. - `MenuGroup`: 관련된 항목들을 그룹으로 묶습니다. 모든 `MenuItem`은 `MenuGroup` 안에 있어야 합니다. - `MenuGroupLabel`: 그룹의 제목을 표시합니다. - `MenuItem`: 개별 메뉴 항목입니다. ## Props ### `MenuRoot` ### `MenuTrigger` ### `MenuAnchor` ### `MenuContent` ### `MenuGroup` ### `MenuGroupLabel` ### `MenuItem` ## Examples ### Trigger ``는 `aria-haspopup="menu"` 속성을 설정하고, Menu의 `open` 상태에 따라 `aria-expanded` 속성을 자동으로 설정합니다. 이 속성은 스크린 리더와 같은 보조 기술에 유용합니다. ## Preview ```tsx import { IconPencilLine, IconPlusLine, IconTrashcanLine, } from "@karrotmarket/react-monochrome-icon"; import { ActionButton } from "seed-design/ui/action-button"; import { MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuRoot, MenuTrigger, } from "seed-design/ui/menu"; export default function MenuPreview() { return ( 열기 작업 } /> } /> } /> ); } ``` ### Size `size`로 MenuRoot의 크기를 정합니다. (default: `medium`) `responsive`는 화면 너비에 따라 size가 자동으로 전환되는 값입니다. 여러 화면 너비를 함께 지원하는 제품에서 `size=responsive`를 사용하여 대응합니다. ```tsx import { IconPencilLine, IconPlusLine } from "@karrotmarket/react-monochrome-icon"; import { HStack } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuRoot, MenuTrigger, } from "seed-design/ui/menu"; export default function MenuSize() { return ( Medium 작업 } /> } /> Small 작업 } /> } /> Responsive 작업 } /> } /> ); } ``` ### With Description `MenuItem`의 `description` prop을 사용하여 항목에 부가 설명을 추가합니다. ```tsx import { IconArrowUpBracketDownLine, IconPencilLine, IconPlusLine, } from "@karrotmarket/react-monochrome-icon"; import { ActionButton } from "seed-design/ui/action-button"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "seed-design/ui/menu"; export default function MenuWithDescription() { return ( 열기 } /> } /> } /> ); } ``` ### Tone `MenuItem`의 `tone` prop으로 항목의 톤을 설정합니다. ```tsx import { IconPencilLine, IconPlusLine, IconTrashcanLine, } from "@karrotmarket/react-monochrome-icon"; import { ActionButton } from "seed-design/ui/action-button"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "seed-design/ui/menu"; export default function MenuTone() { return ( 열기 } /> } /> } /> ); } ``` ### Disabled Items `MenuItem`의 `disabled` prop을 사용하여 특정 항목을 비활성화합니다. ```tsx import { IconArrowUpBracketDownLine, IconPencilLine, IconPlusLine, } from "@karrotmarket/react-monochrome-icon"; import { ActionButton } from "seed-design/ui/action-button"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "seed-design/ui/menu"; export default function MenuDisabled() { return ( 열기 } /> } disabled /> } /> ); } ``` ### Placement `MenuRoot`의 `placement` prop으로 메뉴의 위치를 설정합니다. 기본값은 `"bottom"`입니다. ```tsx import { IconPencilLine, IconPlusLine } from "@karrotmarket/react-monochrome-icon"; import { Box } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger, type MenuRootProps, } from "seed-design/ui/menu"; function PlacementMenu({ placement }: { placement: NonNullable }) { return ( {placement} } /> } /> ); } export default function MenuPlacement() { return (
    ); } ``` ### Anchor `MenuAnchor`의 `children`은 Menu가 위치를 잡는 데에만 사용되며, `MenuTrigger`와 달리 클릭으로 열고 닫는 동작이 포함되지 않습니다. `open` prop을 controlled하게 사용하여 열리고 닫힌 상태를 직접 관리해야 합니다. 아래 예시의 경우 `onOpenChange` 콜백에 조건을 추가하여 바깥 영역을 클릭한 경우에도 메뉴가 닫히지 않도록 구현되어 있습니다. ```tsx import { Avatar } from "seed-design/ui/avatar"; import { IdentityPlaceholder } from "seed-design/ui/identity-placeholder"; import { IconPencilLine, IconPlusLine } from "@karrotmarket/react-monochrome-icon"; import { useState } from "react"; import { Switch } from "seed-design/ui/switch"; import { MenuAnchor, MenuContent, MenuGroup, MenuItem, MenuRoot } from "seed-design/ui/menu"; import { HStack } from "@seed-design/react"; export default function MenuAnchorExample() { const [open, setOpen] = useState(false); return ( { if (!nextOpen && details?.reason === "interactOutside") return; setOpen(nextOpen); }} > } /> } /> } /> ); } ``` ### Match Width `MenuRoot`의 `matchReferenceWidth` prop을 사용하면 메뉴의 너비가 reference 요소(Trigger 또는 Anchor)의 너비에 맞춰집니다. ```tsx import { ActionButton } from "seed-design/ui/action-button"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "seed-design/ui/menu"; export default function MenuMatchReferenceWidth() { return ( 열기 ); } ``` ### `onOpenChange` Details `onOpenChange` 두 번째 인자로 `details`가 제공됩니다. #### `reason` **열릴 때** (`open: true`) - `"trigger"`: `MenuTrigger`로 열림 **닫힐 때** (`open: false`) - `"trigger"`: `MenuTrigger`로 닫힘 - `"escapeKeyDown"`: ESC 키 사용 - `"interactOutside"`: 외부 영역 클릭 - `"itemClick"`: 메뉴 항목 클릭 - `"cascadeDismiss"`: 상위 레이어 닫힘으로 인한 연쇄 닫힘 ```tsx import { IconPencilLine, IconPlusLine } from "@karrotmarket/react-monochrome-icon"; import { HStack, Text, VStack } from "@seed-design/react"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "seed-design/ui/menu"; export default function MenuOpenChangeReason() { const [open, setOpen] = useState(false); const [openReason, setOpenReason] = useState(null); const [closeReason, setCloseReason] = useState(null); return ( { setOpen(open); (open ? setOpenReason : setCloseReason)(details?.reason ?? null); }} > 열기 } /> } /> 마지막 열림 이유: {openReason ?? "-"} 마지막 닫힘 이유: {closeReason ?? "-"} ); } ``` --- file: components/page-banner.mdx # Page Banner 페이지 상단에 위치하며 사용자에게 전체적인 상태나 중요한 메시지를 전달하는 상위 레벨 메시지 컴포넌트입니다. 사용 가능 버전: @seed-design/react@0.1.14, @seed-design/css@0.1.14 ## Preview ```tsx import { VStack } from "@seed-design/react"; import { ActionablePageBanner, DismissiblePageBanner, PageBanner, } from "seed-design/ui/page-banner"; export default function PageBannerPreview() { return ( ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:page-banner - pnpm: pnpm dlx @seed-design/cli@latest add ui:page-banner - yarn: yarn dlx @seed-design/cli@latest add ui:page-banner - bun: bun x @seed-design/cli@latest add ui:page-banner ## Props ### `PageBanner` ### `PageBannerButton` ### `ActionablePageBanner` ### `DismissiblePageBanner` ## Examples ### With Button `PageBanner`의 `suffix` prop에 `PageBannerButton`을 전달하여 버튼을 추가할 수 있습니다. ```tsx import { PageBanner, PageBannerButton } from "seed-design/ui/page-banner"; export default function PageBannerWithButton() { return ( 자세히 보기} /> ); } ``` ### Rendering `PageBannerButton` as a Child ```tsx import { PageBanner, PageBannerButton } from "seed-design/ui/page-banner"; export default function PageBannerButtonAsChild() { return ( 새 탭에서 열기 } /> ); } ``` ### Tones and Variants #### Neutral ```tsx import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon"; import { ActionablePageBanner, DismissiblePageBanner, PageBanner, PageBannerButton, } from "seed-design/ui/page-banner"; export default function PageBannerNeutral() { return (
    } title="미노출" description="사업자 정보를 등록해주세요." suffix={등록하기} /> } title="미노출" description="사업자 정보를 등록해주세요." suffix={등록하기} /> } title="미노출" description="사업자 정보를 등록해주세요." /> } title="미노출" description="사업자 정보를 등록해주세요." /> } title="미노출" description="사업자 정보를 등록해주세요." /> } title="미노출" description="사업자 정보를 등록해주세요." />
    ); } ``` #### Positive ```tsx import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon"; import { ActionablePageBanner, DismissiblePageBanner, PageBanner, PageBannerButton, } from "seed-design/ui/page-banner"; export default function PageBannerPositive() { return (
    } title="미노출" description="사업자 정보를 등록해주세요." suffix={등록하기} /> } title="미노출" description="사업자 정보를 등록해주세요." suffix={등록하기} /> } title="미노출" description="사업자 정보를 등록해주세요." /> } title="미노출" description="사업자 정보를 등록해주세요." /> } title="미노출" description="사업자 정보를 등록해주세요." /> } title="미노출" description="사업자 정보를 등록해주세요." />
    ); } ``` #### Informative ```tsx import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon"; import { ActionablePageBanner, DismissiblePageBanner, PageBanner, PageBannerButton, } from "seed-design/ui/page-banner"; export default function PageBannerInformative() { return (
    } title="미노출" description="사업자 정보를 등록해주세요." suffix={등록하기} /> } title="미노출" description="사업자 정보를 등록해주세요." suffix={등록하기} /> } title="미노출" description="사업자 정보를 등록해주세요." /> } title="미노출" description="사업자 정보를 등록해주세요." /> } title="미노출" description="사업자 정보를 등록해주세요." /> } title="미노출" description="사업자 정보를 등록해주세요." />
    ); } ``` #### Warning ```tsx import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon"; import { ActionablePageBanner, DismissiblePageBanner, PageBanner, PageBannerButton, } from "seed-design/ui/page-banner"; export default function PageBannerWarning() { return (
    } title="미노출" description="사업자 정보를 등록해주세요." suffix={등록하기} /> } title="미노출" description="사업자 정보를 등록해주세요." suffix={등록하기} /> } title="미노출" description="사업자 정보를 등록해주세요." /> } title="미노출" description="사업자 정보를 등록해주세요." /> } title="미노출" description="사업자 정보를 등록해주세요." /> } title="미노출" description="사업자 정보를 등록해주세요." />
    ); } ``` #### Critical ```tsx import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon"; import { ActionablePageBanner, DismissiblePageBanner, PageBanner, PageBannerButton, } from "seed-design/ui/page-banner"; export default function PageBannerCritical() { return (
    } title="미노출" description="사업자 정보를 등록해주세요." suffix={등록하기} /> } title="미노출" description="사업자 정보를 등록해주세요." suffix={등록하기} /> } title="미노출" description="사업자 정보를 등록해주세요." /> } title="미노출" description="사업자 정보를 등록해주세요." /> } title="미노출" description="사업자 정보를 등록해주세요." /> } title="미노출" description="사업자 정보를 등록해주세요." />
    ); } ``` #### Magic `tone="magic"`은 `variant="weak"` 조합으로만 사용할 수 있습니다. ```tsx import { IconSparkle2 } from "@karrotmarket/react-multicolor-icon"; import { VStack } from "@seed-design/react"; import { ActionablePageBanner, DismissiblePageBanner, PageBanner, PageBannerButton, } from "seed-design/ui/page-banner"; export default function PageBannerMagic() { return ( } title="새로운 기능" description="마법 같은 소식이 도착했어요!" suffix={둘러보기} /> } title="새로운 기능" description="마법 같은 소식이 도착했어요!" /> } title="새로운 기능" description="마법 같은 소식이 도착했어요!" /> ); } ``` --- file: components/pagination.mdx # Pagination 여러 페이지로 나뉜 목록에서 원하는 페이지로 이동할 때 사용하는 컴포넌트입니다. 사용 가능 버전: @seed-design/react-pagination@1.0.0, @seed-design/css@2.6.0 ## Preview ```tsx import { Pagination } from "seed-design/ui/pagination"; export default function PaginationPreview() { return ; } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:pagination - pnpm: pnpm dlx @seed-design/cli@latest add ui:pagination - yarn: yarn dlx @seed-design/cli@latest add ui:pagination - bun: bun x @seed-design/cli@latest add ui:pagination ## Usage ```tsx import { Pagination } from "seed-design/ui/pagination"; ``` `totalPages`는 0 이상의 전체 페이지 수입니다. 페이지가 0개 또는 1개면 Pagination을 표시하지 않습니다. 화면 너비가 480px 이상이면 이전·다음 버튼과 생략 표시를 포함해 최대 9개 슬롯을, 그보다 좁으면 최대 7개 슬롯을 표시합니다. 작은 화면의 페이지 영역은 5개 슬롯을 유지하며, 현재 페이지의 앞뒤 페이지를 직접 선택할 수 있도록 첫 페이지와 마지막 페이지를 필요한 구간에서만 표시합니다. 첫 페이지에서는 이전 버튼을, 마지막 페이지에서는 다음 버튼을 숨기되 각 영역의 너비는 유지합니다. 따라서 첫 페이지와 마지막 페이지 주변에서도 나머지 버튼의 위치가 바뀌지 않습니다. 여러 페이지로 나뉜 일반 목록이나 검색 결과의 아래쪽 중앙에 배치하세요. 데이터 표에서 페이지당 행 수와 표시 범위를 함께 제어해야 한다면 [Table Pagination](/react/components/table-pagination)을 사용하세요. ## Localization 스니펫 파일 상단의 `numberFormatter`와 `paginationText`를 앱의 언어에 맞게 수정하세요. 숫자 형식뿐 아니라 루트, 이전·다음 버튼, 개별 페이지의 접근성 이름도 이곳에서 바꿀 수 있습니다. ## Props ## Examples ### Controlled `page`와 `onPageChange`를 사용하면 페이지 상태를 외부에서 제어할 수 있습니다. 변경된 목록의 로딩, 포커스 이동, 결과 안내는 Pagination이 아니라 목록 영역에서 처리하세요. ```tsx "use client"; import { Text, VStack } from "@seed-design/react"; import { useState } from "react"; import { Pagination } from "seed-design/ui/pagination"; export default function PaginationControlled() { const [page, setPage] = useState(5); return ( 현재 페이지: {page} ); } ``` ### onPageChange Details `onPageChange`의 두 번째 인자는 변경 원인을 나타내는 `reason`과 변경 전 페이지인 `previousPage`를 제공합니다. | reason | 발생 조건 | | ------------ | ------------------------------- | | `page-item` | 페이지 번호를 선택했을 때 | | `previous` | 이전 페이지 버튼을 선택했을 때 | | `next` | 다음 페이지 버튼을 선택했을 때 | | `constraint` | 외부에서 전달한 페이지가 유효 범위를 벗어나 보정됐을 때 | 외부 페이지가 범위를 벗어나면 보정된 페이지를 먼저 렌더링하고, 이후 `constraint` 변경을 한 번 알립니다. ## Accessibility 루트는 `nav`로 렌더링되며 기본 접근성 이름은 `페이지 탐색`입니다. 같은 화면에 Pagination이 여러 개 있으면 `aria-label`이나 `aria-labelledby`로 대상을 구분하세요. 현재 페이지 버튼에는 `aria-current="page"`가 적용됩니다. 키보드 사용자는 Tab으로 버튼 사이를 이동하고 Enter 또는 Space로 페이지를 선택할 수 있습니다. --- file: components/progress-circle.mdx # Progress Circle 작업이 진행 중임을 알리거나 작업 시간을 시각적으로 나타내는 데 사용됩니다. 사용 가능 버전: @seed-design/react@0.0.1, @seed-design/css@0.0.1 ## Preview ```tsx import { ProgressCircle } from "seed-design/ui/progress-circle"; export default function ProgressCirclePreview() { return ; } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:progress-circle - pnpm: pnpm dlx @seed-design/cli@latest add ui:progress-circle - yarn: yarn dlx @seed-design/cli@latest add ui:progress-circle - bun: bun x @seed-design/cli@latest add ui:progress-circle ## Props ## Examples ### Neutral ```tsx import { ProgressCircle } from "seed-design/ui/progress-circle"; export default function ProgressCircleNeutral() { return ; } ``` ### Brand ```tsx import { ProgressCircle } from "seed-design/ui/progress-circle"; export default function ProgressCircleBrand() { return ; } ``` ### Static White ```tsx import { ProgressCircle } from "seed-design/ui/progress-circle"; export default function ProgressCircleStaticWhite() { return (
    ); } ``` ### Size=40 ```tsx import { ProgressCircle } from "seed-design/ui/progress-circle"; export default function ProgressCircle40() { return ; } ``` ### Size=24 ```tsx import { ProgressCircle } from "seed-design/ui/progress-circle"; export default function ProgressCircle24() { return ; } ``` ### Determinate ```tsx import { ProgressCircle } from "seed-design/ui/progress-circle"; export default function ProgressCircleDeterminate() { return ; } ``` ### Indeterminate ```tsx import { ProgressCircle } from "seed-design/ui/progress-circle"; export default function ProgressCirclePreview() { // if you want to show an indeterminate progress circle, you can pass `undefined` or omit the `value` prop return ; } ``` --- file: components/pull-to-refresh.mdx # Pull To Refresh 사용자가 화면을 아래로 당겨 콘텐츠를 새로고침할 수 있게 해주는 컴포넌트입니다. 모바일 환경에서 최신 콘텐츠를 불러올 때 사용됩니다. 사용 가능 버전: @seed-design/react@0.0.1, @seed-design/css@0.0.1 ```tsx import { AppBar, AppScreen } from "@seed-design/stackflow"; import { type StaticActivityComponentType } from "@stackflow/react/future"; import { VStack } from "@seed-design/react"; import { PullToRefreshContent, PullToRefreshIndicator, PullToRefreshRoot, } from "seed-design/ui/pull-to-refresh"; declare module "@stackflow/config" { interface Register { ActivityPullToRefreshPreview: {}; } } const ActivityPullToRefreshPreview: StaticActivityComponentType< "ActivityPullToRefreshPreview" > = () => { // AppScreen is imported from @seed-design/stackflow instead of snippet for demo purpose. // AppScreen snippet is integrating PullToRefresh, so it's not necessary to use it here. return ( Pull To Refresh {}} onPtrRefresh={async () => { await new Promise((resolve) => setTimeout(resolve, 1000)); }} > Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam autem deserunt reprehenderit ducimus sunt. Quod laudantium excepturi tempora fuga repellendus accusantium nam maiores? Quas debitis, neque ullam eligendi minus sit? ); }; export default ActivityPullToRefreshPreview; ``` ## Installation - npm: npx @seed-design/cli@latest add ui:pull-to-refresh - pnpm: pnpm dlx @seed-design/cli@latest add ui:pull-to-refresh - yarn: yarn dlx @seed-design/cli@latest add ui:pull-to-refresh - bun: bun x @seed-design/cli@latest add ui:pull-to-refresh ## Props ### `PullToRefreshRoot` ### `PullToRefreshIndicator` ### `PullToRefreshContent` ## Examples ### PTR in Tabs ```tsx import { VStack } from "@seed-design/react"; import { type StaticActivityComponentType } from "@stackflow/react/future"; import { AppBar, AppBarMain } from "seed-design/ui/app-bar"; import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen"; import { PullToRefreshContent, PullToRefreshIndicator, PullToRefreshRoot, } from "seed-design/ui/pull-to-refresh"; import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "seed-design/ui/tabs"; declare module "@stackflow/config" { interface Register { ActivityPullToRefreshTabs: {}; } } const ActivityPullToRefreshTabs: StaticActivityComponentType<"ActivityPullToRefreshTabs"> = () => { return ( Pull To Refresh Tab 1 Tab 2 {}} onPtrRefresh={async () => { await new Promise((resolve) => setTimeout(resolve, 1000)); }} > Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam autem deserunt reprehenderit ducimus sunt. Quod laudantium excepturi tempora fuga repellendus accusantium nam maiores? Quas debitis, neque ullam eligendi minus sit? PTR is not available in this tab. ); }; export default ActivityPullToRefreshTabs; ``` ### Disabled `disabled` 속성을 사용하여 PTR를 비활성화할 수 있습니다. ```tsx import { Box, HStack, Text } from "@seed-design/react"; import { useState } from "react"; import { PullToRefreshContent, PullToRefreshIndicator, PullToRefreshRoot, } from "seed-design/ui/pull-to-refresh"; import { Switch } from "seed-design/ui/switch"; const PullToRefreshDisabled = () => { const [disabled, setDisabled] = useState(false); return ( {}} onPtrRefresh={async () => { await new Promise((resolve) => setTimeout(resolve, 1000)); }} disabled={disabled} > Disabled ); }; export default PullToRefreshDisabled; ``` ### Prevent Pull `PullToRefresh.preventPull` 속성을 사용하여 특정 영역에서 PTR 동작을 방지할 수 있습니다. `user-select: auto;`등을 통해 텍스트 선택이 가능한 영역에서 주로 사용됩니다. ```tsx import { AppBar, AppScreen } from "@seed-design/stackflow"; import { type StaticActivityComponentType } from "@stackflow/react/future"; import { VStack, PullToRefresh, Box } from "@seed-design/react"; import { PullToRefreshContent, PullToRefreshIndicator, PullToRefreshRoot, } from "seed-design/ui/pull-to-refresh"; declare module "@stackflow/config" { interface Register { ActivityPullToRefreshPreventPull: {}; } } const ActivityPullToRefreshPreventPull: StaticActivityComponentType< "ActivityPullToRefreshPreventPull" > = () => { // AppScreen is imported from @seed-design/stackflow instead of snippet for demo purpose. // AppScreen snippet is integrating PullToRefresh, so it's not necessary to use it here. return ( Pull To Refresh {}} onPtrRefresh={async () => { await new Promise((resolve) => setTimeout(resolve, 1000)); }} > 이 영역은 당겨서 새로고침이 가능합니다. Amet in laborum proident fugiat mollit quis aute mollit esse nostrud. Excepteur ea proident ipsum duis. Nulla Lorem pariatur exercitation velit anim. 이 영역은 당겨서 새로고침이 불가능합니다. Aliquip ad amet eu dolore id enim excepteur laboris officia anim in. Irure irure nulla sit eiusmod aliqua sint excepteur amet laboris. 이 영역은 당겨서 새로고침이 가능합니다. Amet in laborum proident fugiat mollit quis aute mollit esse nostrud. Excepteur ea proident ipsum duis. Nulla Lorem pariatur exercitation velit anim. 이 영역은 당겨서 새로고침이 불가능합니다. Aliquip ad amet eu dolore id enim excepteur laboris officia anim in. Irure irure nulla sit eiusmod aliqua sint excepteur amet laboris. ); }; export default ActivityPullToRefreshPreventPull; ``` --- file: components/quantity-picker.mdx # Quantity Picker 정수 단위의 수량을 늘리거나 줄일 때 사용하는 컴포넌트입니다. 사용 가능 버전: @seed-design/react@2.1.0, @seed-design/css@2.3.0 ## Preview ```tsx import { QuantityPicker } from "seed-design/ui/quantity-picker"; export default function QuantityPickerPreview() { return ; } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:quantity-picker - pnpm: pnpm dlx @seed-design/cli@latest add ui:quantity-picker - yarn: yarn dlx @seed-design/cli@latest add ui:quantity-picker - bun: bun x @seed-design/cli@latest add ui:quantity-picker ## Props ## Examples ### Layout 사용 가능 버전: @seed-design/react@2.2.0, @seed-design/css@2.4.0 `layout="hug"`는 콘텐츠에 맞는 기존 너비를 유지합니다. 부모가 Flex 레이아웃이고 남은 공간을 채워야 한다면 `layout="fill"`을 사용하세요. 이때 양쪽 버튼 크기는 유지되고 Value Display 영역만 늘어납니다. ```tsx import { HStack, Text, VStack } from "@seed-design/react"; import { QuantityPicker } from "seed-design/ui/quantity-picker"; export default function QuantityPickerLayout() { return ( Hug (기본) Fill ); } ``` ### Value Text `getValueText`를 사용하면 표시되는 수량에 단위나 보조 설명을 덧붙일 수 있습니다. ```tsx import { QuantityPicker } from "seed-design/ui/quantity-picker"; export default function QuantityPickerValueText() { return ( `${valueText}개`} /> ); } ``` ### Controlled `value`와 `onValueChange`를 사용해 수량 상태를 외부에서 제어할 수 있습니다. ```tsx import { Text, VStack } from "@seed-design/react"; import { useState } from "react"; import { QuantityPicker } from "seed-design/ui/quantity-picker"; export default function QuantityPickerControlled() { const [quantity, setQuantity] = useState(2); return ( 현재 수량: {quantity}개 ); } ``` ### Removable `removable`을 사용하면 값이 `min`에 도달했을 때 Decrement 버튼이 Remove 버튼으로 전환됩니다. `onRemove`에서 제거 동작을 처리하세요. ```tsx import { Text, VStack } from "@seed-design/react"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { QuantityPicker } from "seed-design/ui/quantity-picker"; export default function QuantityPickerRemovable() { const [removed, setRemoved] = useState(false); if (removed) { return ( 상품을 삭제했습니다. setRemoved(false)}> 되돌리기 ); } return ( setRemoved(true)} /> 최소 수량에서 Decrement 버튼이 Remove 버튼으로 전환됩니다. ); } ``` ### Loading `loading`으로 모든 action 또는 특정 action의 실행을 일시적으로 막고 loading indicator를 표시할 수 있습니다. ```tsx import { HStack, VStack } from "@seed-design/react"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { QuantityPicker } from "seed-design/ui/quantity-picker"; export default function QuantityPickerLoading() { const [decrementLoading, setDecrementLoading] = useState(false); const [incrementLoading, setIncrementLoading] = useState(false); const allLoading = decrementLoading && incrementLoading; function toggleAllLoading() { const nextLoading = !allLoading; setDecrementLoading(nextLoading); setIncrementLoading(nextLoading); } return ( 전체 setDecrementLoading((current) => !current)} > Decrement setIncrementLoading((current) => !current)} > Increment ); } ``` ### Form `inputProps`에 `name`을 전달하면 현재 수량이 hidden input으로 제출됩니다. ```tsx import { VStack } from "@seed-design/react"; import type { FormEvent } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { QuantityPicker } from "seed-design/ui/quantity-picker"; export default function QuantityPickerForm() { function handleSubmit(event: FormEvent) { event.preventDefault(); const formData = new FormData(event.currentTarget); window.alert(`제출한 수량: ${formData.get("quantity")}개`); } return (
    제출
    ); } ``` ## Accessibility Root에는 Quantity Picker의 용도를 설명하는 `aria-label` 또는 `aria-labelledby`를 제공하세요. 필요하면 `decrementAriaLabel`, `incrementAriaLabel`, `removeAriaLabel`로 각 action의 접근성 이름을 맥락에 맞게 변경할 수 있습니다. `getValueText`를 사용하면 ValueDisplay의 숫자를 사람이 읽기 쉬운 텍스트로 제공할 수 있습니다. 이 값은 hidden input에 제출되는 raw integer 값에 영향을 주지 않습니다. --- file: components/radio-group.mdx # Radio Group 여러 옵션 중 하나를 선택할 수 있도록 할 때 사용하는 컴포넌트입니다. 사용 가능 버전: @seed-design/react@0.2.3, @seed-design/css@0.2.3 ## Preview ```tsx import { VStack } from "@seed-design/react"; import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group"; export default function RadioGroupPreview() { return ( ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:radio-group - pnpm: pnpm dlx @seed-design/cli@latest add ui:radio-group - yarn: yarn dlx @seed-design/cli@latest add ui:radio-group - bun: bun x @seed-design/cli@latest add ui:radio-group ## Props ### `RadioGroup` ### `RadioGroupItem` ### `Radiomark` ## Examples ### Sizes ```tsx import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group"; import { VStack } from "@seed-design/react"; export default function RadioGroupSize() { return ( ); } ``` ### Tones #### Brand ```tsx import { VStack } from "@seed-design/react"; import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group"; export default function RadioGroupBrand() { return ( ); } ``` #### Neutral ```tsx import { VStack } from "@seed-design/react"; import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group"; export default function RadioGroupNeutral() { return ( ); } ``` ### Weights ```tsx import { VStack } from "@seed-design/react"; import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group"; export default function RadioGroupWeights() { return ( ); } ``` ### Long Label ```tsx import { VStack } from "@seed-design/react"; import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group"; export default function RadioGroupLongLabel() { return ( ); } ``` ### Disabled ```tsx import { VStack } from "@seed-design/react"; import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group"; export default function RadioGroupDisabled() { return ( ); } ``` ### Listening to Value Changes `RadioGroup`의 `onValueChange`를 사용하여 라디오 버튼의 선택 값 변경을 감지할 수 있습니다. 이벤트를 활용해야 하는 경우 `RadioGroupItem`의 `inputProps`를 통해 내부 `` 요소에 직접 이벤트 핸들러를 추가할 수 있습니다. ```tsx import { VStack, Text } from "@seed-design/react"; import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group"; import { useState } from "react"; export default function RadioGroupValueChanges() { const [count, setCount] = useState(0); const [lastValue, setLastValue] = useState(null); return ( { setCount((prev) => prev + 1); setLastValue(value); }} > onValueChange called: {count} times, last value: {lastValue ?? "-"} ); } ``` ### Use Cases #### React Hook Form ```tsx import { HStack, VStack } from "@seed-design/react"; import { useCallback, type FormEvent } from "react"; import { useController, useForm } from "react-hook-form"; import { ActionButton } from "seed-design/ui/action-button"; import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group"; const POSSIBLE_COLORS = ["red", "blue", "green"] as const; interface FormValues { color: (typeof POSSIBLE_COLORS)[number]; } export default function RadioGroupReactHookForm() { const { handleSubmit, reset, setValue, control } = useForm({ defaultValues: { color: "blue", }, }); const { field } = useController({ name: "color", control }); const onValid = useCallback((data: FormValues) => { window.alert(JSON.stringify(data, null, 2)); }, []); const onReset = useCallback( (event: FormEvent) => { event.preventDefault(); reset(); }, [reset], ); return ( setValue("color", value as FormValues["color"])} aria-label="Color selection" > {POSSIBLE_COLORS.map((color) => ( ))} Submit Reset ); } ``` #### Using `Radiomark` `Radiomark`는 독립적인 라디오 마크 컴포넌트로, Radio Group Primitive 컴포넌트와 조합하여 커스텀 레이아웃을 위해 사용할 수 있습니다. ```tsx import { HStack, Text, VStack } from "@seed-design/react"; import { Radiomark } from "seed-design/ui/radio-group"; import { RadioGroup } from "@seed-design/react/primitive"; function CustomRadioGroupItem({ children, ...props }: RadioGroup.ItemProps) { return ( {children} ); } export default function RadioGroupRadiomark() { return ( regular medium bold ); } ``` ### RadioGroupField Integration `label`, `description`, `errorMessage` 등의 RadioGroupField 관련 prop을 사용할 수 있습니다. ```tsx import { ActionButton, HStack, VStack } from "@seed-design/react"; import { useState } from "react"; import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group"; export default function RadioGroupRadioGroupField() { const [firstErrorMessage, setFirstErrorMessage] = useState(); const [secondErrorMessage, setSecondErrorMessage] = useState(); const handleFirstSubmit = (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.currentTarget); const contact = formData.get("contact"); if (contact === "email") { setFirstErrorMessage("이메일은 선택할 수 없습니다."); return; } setFirstErrorMessage(undefined); alert(JSON.stringify({ contact }, null, 2)); }; const handleSecondSubmit = (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.currentTarget); const option = formData.get("option"); if (option === "option1") { setSecondErrorMessage("옵션 1은 선택할 수 없습니다."); return; } setSecondErrorMessage(undefined); alert(JSON.stringify({ option }, null, 2)); }; return (
    제출
    제출
    ); } ``` ## Scale Feedback `RadioGroupItem`은 눌렸을 때 `Radiomark`가 살짝 줄어드는 피드백을 제공합니다. `Radiomark`를 감싸는 요소에 [Scale Feedback](/react/components/concepts/scale-feedback)을 직접 적용하는 경우, 바깥 요소와 `Radiomark`가 함께 줄어들어 축소가 이중으로 나타납니다. `Radiomark`의 상위 요소에서 `--seed-radiomark-feedback-scale`을 `1`로 설정하면 `Radiomark`의 축소만 끌 수 있습니다. 이 변수는 상속되므로 `RadioGroupItem` 전체를 감싸는 요소에 설정해도 됩니다. ```css title="MyRow.css" .my-row { --seed-radiomark-feedback-scale: 1; } ``` --- file: components/reaction-button.mdx # Reaction Button 사용자가 콘텐츠에 대한 반응을 표현할 수 있게 해주는 컴포넌트입니다. 좋아요, 관심있어요 등의 감정적 피드백을 간편하게 제공할 때 사용됩니다. 사용 가능 버전: @seed-design/react@0.0.1, @seed-design/css@0.0.1 ## Preview ```tsx import { IconFaceSmileCircleFill } from "@karrotmarket/react-monochrome-icon"; import { Count, PrefixIcon } from "@seed-design/react"; import { ReactionButton } from "seed-design/ui/reaction-button"; export default function ReactionButtonPreview() { return ( } /> 도움돼요 1 ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:reaction-button - pnpm: pnpm dlx @seed-design/cli@latest add ui:reaction-button - yarn: yarn dlx @seed-design/cli@latest add ui:reaction-button - bun: bun x @seed-design/cli@latest add ui:reaction-button ## Props ## Examples ### Small ```tsx import { IconFaceSmileCircleFill } from "@karrotmarket/react-monochrome-icon"; import { Count, PrefixIcon } from "@seed-design/react"; import { ReactionButton } from "seed-design/ui/reaction-button"; export default function ReactionButtonSmall() { return ( } /> 도움돼요 1 ); } ``` ### Xsmall ```tsx import { IconFaceSmileCircleFill } from "@karrotmarket/react-monochrome-icon"; import { Count, PrefixIcon } from "@seed-design/react"; import { ReactionButton } from "seed-design/ui/reaction-button"; export default function ReactionButtonXsmall() { return ( } /> 도움돼요 1 ); } ``` ### Disabled ```tsx import { IconFaceSmileCircleFill } from "@karrotmarket/react-monochrome-icon"; import { PrefixIcon } from "@seed-design/react"; import { ReactionButton } from "seed-design/ui/reaction-button"; export default function ReactionButtonDisabled() { return ( } /> 비활성 ); } ``` ### Loading ```tsx import { IconFaceSmileCircleFill } from "@karrotmarket/react-monochrome-icon"; import { PrefixIcon } from "@seed-design/react"; import { useState } from "react"; import { ReactionButton } from "seed-design/ui/reaction-button"; export default function ReactionButtonLoading() { const [pressed, setPressed] = useState(false); const [loading, setLoading] = useState(false); function handleToggle() { setLoading(true); setTimeout(() => { setLoading(false); setPressed((prev) => !prev); }, 2000); } // 이벤트 핸들링이 필요할 수 있으므로 loading은 disabled를 포함하지 않습니다. 이벤트 발생을 원하지 않는 경우, disabled 속성을 추가해주세요. return ( } /> 시간이 걸리는 토글 ); } ``` --- file: components/result-section.mdx # Result Section 데이터 로딩 결과, 사용자의 액션 완료 여부 등 사용자에 액션에 대한 결과를 제공하는 템플릿입니다. 주로 전체 화면이나 특정 영역을 차지하여 다음 액션을 유도하거나 현재 상황을 안내하는 역할을 합니다. 사용 가능 버전: @seed-design/react@1.1.10, @seed-design/css@1.1.10 ## Preview ```tsx import { IconDiamond } from "@karrotmarket/react-multicolor-icon"; import { VStack, Icon, Box } from "@seed-design/react"; import { ResultSection } from "seed-design/ui/result-section"; export default function ResultSectionPreview() { return ( } size="x10" /> } title="결과 타이틀" description="부가 설명을 적어주세요" primaryActionProps={{ children: "Primary Action", onClick: () => window.alert("Primary Action Clicked"), }} secondaryActionProps={{ children: "Secondary Action", onClick: () => window.alert("Secondary Action Clicked"), }} /> ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:result-section - pnpm: pnpm dlx @seed-design/cli@latest add ui:result-section - yarn: yarn dlx @seed-design/cli@latest add ui:result-section - bun: bun x @seed-design/cli@latest add ui:result-section ## Props ## Examples ### Sizes #### Large ```tsx import { VStack } from "@seed-design/react"; import { ResultSection } from "seed-design/ui/result-section"; export default function ResultSectionLarge() { return ( ); } ``` #### Medium ```tsx import { VStack } from "@seed-design/react"; import { ResultSection } from "seed-design/ui/result-section"; export default function ResultSectionMedium() { return ( ); } ``` ### With CTA & Progress Circle ```tsx import { ActionButton } from "seed-design/ui/action-button"; import { AppBar, AppBarMain } from "seed-design/ui/app-bar"; import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen"; import { ProgressCircle } from "seed-design/ui/progress-circle"; import { ResultSection } from "seed-design/ui/result-section"; import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon"; import { Box, Flex, Icon, VStack } from "@seed-design/react"; import { type StaticActivityComponentType } from "@stackflow/react/future"; import { useEffect, useState, type ComponentProps } from "react"; declare module "@stackflow/config" { interface Register { ActivityResultSectionCtaProgressCircle: {}; } } type RefundStatus = "in-progress" | "failed"; const resultSectionProperties = { "in-progress": { title: "환불을 요청하고 있어요", description: "잠시만 기다려주세요", asset: ( ), }, failed: { title: "다시 시도해주세요", description: "환불 요청에 실패했어요", asset: ( } size="x10" color="fg.critical" /> ), }, } satisfies Record>; const ActivityResultSectionCtaProgressCircle: StaticActivityComponentType< "ActivityResultSectionCtaProgressCircle" > = () => { const [refundStatus, setRefundStatus] = useState("in-progress"); useEffect(() => { const timer = setTimeout(() => setRefundStatus("failed"), 3000); return () => clearTimeout(timer); }, []); return ( { setRefundStatus("in-progress"); setTimeout(() => setRefundStatus("failed"), 3000); }} > 다시 시도 ); }; export default ActivityResultSectionCtaProgressCircle; ``` ### Success Example Result Section 에셋 영역에서 다크 모드와 라이트 모드에서 서로 다른 Lottie 애니메이션을 사용하는 예시입니다. [클라이언트에서 현재 테마 정보 감지하는 방법](/react/getting-started/styling/theming#클라이언트에서-현재-테마-정보-감지하기)을 참고하여 테마에 맞는 Lottie 애니메이션을 Result Section에 적용할 수 있습니다. ```tsx import dynamic from "next/dynamic"; import { VStack, Box } from "@seed-design/react"; import { ResultSection } from "seed-design/ui/result-section"; import { useTheme } from "@/hooks/useTheme"; const Player = dynamic(() => import("@lottiefiles/react-lottie-player").then((mod) => mod.Player), { ssr: false, }); const LOTTIE_URLS = { light: "https://asset-town.krrt.io/production/motion/bd9f3c71-5b81-40b0-8eea-eeebd668edae/c17fa891bb007b9e4e6b281e483b5491cb905703.json", dark: "https://asset-town.krrt.io/production/motion/19bf4654-5286-4def-a651-c674a20ce1ee/89c9e404edc356cf143dab80b627fde01ed8a8fb.json", }; export default function ResultSectionSuccessWithLottie() { const { userColorScheme } = useTheme(); const lottieUrl = LOTTIE_URLS[userColorScheme]; return ( } title="성공했어요" description="요청이 성공적으로 완료되었습니다" primaryActionProps={{ children: "확인", onClick: () => window.alert("확인 클릭"), }} /> ); } ``` --- file: components/scroll-fog.mdx # Scroll Fog 스크롤 가능한 영역에서 사용자에게 추가 콘텐츠가 있음을 시각적으로 알려주는 힌트 역할을 합니다. 사용 가능 버전: @seed-design/react@1.1.3, @seed-design/css@1.1.3 ## Preview ```tsx import { Box, ScrollFog, VStack } from "@seed-design/react"; export default function ScrollFogPreview() { return (
    {Array.from({ length: 20 }, (_, i) => ( {i + 1} ))}
    ); } ``` 해당 Scroll Fog 컴포넌트는 [Chakra ScrollArea](https://chakra-ui.com/components/scroll-area), [Radix ScrollArea](https://www.radix-ui.com/primitives/components/scroll-area), [Base UI ScrollArea](https://base-ui.com/react/components/scroll-area) 보다는 더 간단한 스크롤 힌트만을 위한 컴포넌트입니다. 이후 스크롤바에 대한 기능이 추가적으로 필요하다면 확장될 수 있습니다. ## Usage ```tsx import { ScrollFog } from "@seed-design/react"; ``` ```tsx
    {/* 스크롤 가능한 콘텐츠 */}
    Content
    ``` ## Props ### Scroll Fog ## Examples ### Padding ScrollFog는 항상 fog 효과를 표시하므로, 지정된 방향에 충분한 padding이 필요합니다. - **최소 padding**: 20px을 유지해야 합니다. - **화면 전체 차지하는 세로 스크롤**: 하단 80px, 상단 20px padding 권장 - **가로 스크롤**: 20px 좌우 padding 권장 ```tsx // 상하 스크롤 - 상하 padding 필요
    Content
    // 좌우 스크롤 - 좌우 padding 필요
    Content
    ``` - [Bottom Sheet - 하단 80px, 상단 20px padding](/react/components/bottom-sheet#with-scroll-fog) - [Chip Tabs - 20px 좌우 padding](/react/components/chip-tabs#with-scroll-fog) ### Horizontal Scroll 가로 스크롤이 필요한 경우 `placement={["left", "right"]}`를 사용하여 좌우 fog 효과를 적용할 수 있습니다. 좌우 20px의 padding을 제공하여 컨텐츠가 잘리지 않도록 해야 합니다. ```tsx import { HStack, ScrollFog } from "@seed-design/react"; export default function ScrollFogHorizontal() { return (
    {Array.from({ length: 15 }, (_, i) => (
    {i + 1}
    ))}
    ); } ``` ### Size `size` 속성을 이용해 스크롤 효과의 크기를 조절할 수 있습니다. `sizes` 속성을 이용해 각 방향별 크기를 조절할 수 있습니다. ```tsx import { ScrollFog } from "@seed-design/react"; export default function ScrollFogSize() { return (
    {Array.from({ length: 100 }, (_, i) => (
    {i + 1}
    ))}
    ); } ``` ```tsx import { ScrollFog } from "@seed-design/react"; export default function ScrollFogSize() { return (
    {Array.from({ length: 100 }, (_, i) => (
    {i + 1}
    ))}
    ); } ``` ### All Directions 2D 스크롤이 필요한 경우 `placement={["top", "bottom", "left", "right"]}`를 사용하여 모든 방향에 fog 효과를 적용할 수 있습니다. ```tsx import { ScrollFog } from "@seed-design/react"; export default function ScrollFogAllDirections() { return (
    {Array.from({ length: 100 }, (_, i) => (
    {i + 1}
    ))}
    ); } ``` ### Hide Scrollbar `hideScrollBar` 속성을 이용해 스크롤바를 숨길 수 있습니다. ```tsx import { ScrollFog } from "@seed-design/react"; export default function ScrollFogHideScrollbar() { return (
    {Array.from({ length: 100 }, (_, i) => (
    {i + 1}
    ))}
    ); } ``` --- file: components/segmented-control.mdx # Segmented Control 여러 옵션 중 하나를 선택하여 관련 콘텐츠를 즉시 필터링하거나 전환할 때 사용하는 컴포넌트입니다. 사용 가능 버전: @seed-design/react@0.0.1, @seed-design/css@0.0.1 ## Preview ```tsx import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control"; export default function SegmentedControlPreview() { return ( Hot New ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:segmented-control - pnpm: pnpm dlx @seed-design/cli@latest add ui:segmented-control - yarn: yarn dlx @seed-design/cli@latest add ui:segmented-control - bun: bun x @seed-design/cli@latest add ui:segmented-control ## Props ### `SegmentedControl` `value`와 `defaultValue` 중 적어도 하나를 제공해야 합니다. ### `SegmentedControlItem` ## Examples ### Disabled ```tsx import { VStack } from "@seed-design/react"; import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control"; export default function SegmentedControlPreview() { return ( Hot New Marinara Alfredo Pesto Carbonara Bolognese ); } ``` ### Notification ```tsx import { ActionButton, VStack } from "@seed-design/react"; import { useState } from "react"; import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control"; export default function SegmentedControlNotification() { const [sortBy, setSortBy] = useState("monthly"); const [hasSeenAnnual, setHasSeenAnnual] = useState(false); return ( { setSortBy(value); if (value === "annual") setHasSeenAnnual(true); }} > Monthly Annual Enterprise Custom setHasSeenAnnual(false)} > Reset Notification ); } ``` ### Long Label ```tsx import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control"; export default function SegmentedControlLongLabel() { return ( 가격 높은 순 할인율 높은 순 인기 많은 순 ); } ``` ### Fixed Width `SegmentedControl`의 `style` prop에 `width`를 제공해서 직접 너비를 설정할 수 있습니다. ```tsx import { VStack } from "@seed-design/react"; import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control"; export default function SegmentedControlFixedWidth() { return ( New Hot One Way Trip Round Trip Multi-City Journey ); } ``` ### Listening to Value Changes `SegmentedControl`의 `onValueChange` prop을 사용하여 선택 값 변경을 감지할 수 있습니다. 이벤트를 활용해야 하는 경우 `SegmentedControlItem`의 `inputProps`를 통해 내부 `` 요소에 직접 이벤트 핸들러를 추가할 수 있습니다. ```tsx import { VStack, Text } from "@seed-design/react"; import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control"; import { useState } from "react"; export default function SegmentedControlValueChanges() { const [count, setCount] = useState(0); const [lastValue, setLastValue] = useState(null); return ( { setCount((prev) => prev + 1); setLastValue(value); }} > Hot New onValueChange called: {count} times, last value: {lastValue ?? "-"} ); } ``` --- file: components/select-box.mdx # Select Box 명확한 테두리를 가진 컨테이너를 활용하여, 정의된 목록 중 하나 이상의 옵션을 선택하는 UI 요소입니다. 사용 가능 버전: @seed-design/react@0.0.1, @seed-design/css@0.0.1 ## Preview ```tsx import { HStack } from "@seed-design/react"; import { CheckSelectBox, CheckSelectBoxCheckmark, CheckSelectBoxGroup, RadioSelectBoxItem, RadioSelectBoxRadiomark, RadioSelectBoxRoot, } from "seed-design/ui/select-box"; export default function SelectBoxPreview() { return ( } /> } /> } /> } /> } /> } /> ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:select-box - pnpm: pnpm dlx @seed-design/cli@latest add ui:select-box - yarn: yarn dlx @seed-design/cli@latest add ui:select-box - bun: bun x @seed-design/cli@latest add ui:select-box ## Props ### Check Select Box #### `CheckSelectBoxGroup` #### `CheckSelectBox` #### `CheckSelectBoxCheckmark` ### Radio Select Box #### `RadioSelectBoxRoot` #### `RadioSelectBoxItem` #### `RadioSelectBoxRadiomark` ## Examples ### React Hook Form ```tsx import { HStack, VStack } from "@seed-design/react"; import { useCallback, type FormEvent } from "react"; import { useController, useForm, type Control } from "react-hook-form"; import { ActionButton } from "seed-design/ui/action-button"; import { CheckSelectBox, CheckSelectBoxCheckmark, CheckSelectBoxGroup, RadioSelectBoxItem, RadioSelectBoxRadiomark, RadioSelectBoxRoot, } from "seed-design/ui/select-box"; const POSSIBLE_FRUIT_VALUES = ["apple", "melon", "mango"] as const; type CheckFormValues = Record<(typeof POSSIBLE_FRUIT_VALUES)[number], boolean>; interface RadioFormValues { fruit: (typeof POSSIBLE_FRUIT_VALUES)[number]; } export default function SelectBoxReactHookForm() { // CheckSelectBox Form const checkForm = useForm({ defaultValues: { apple: false, melon: true, mango: false }, }); // RadioSelectBox Form const radioForm = useForm({ defaultValues: { fruit: "melon" }, }); const { field: radioField } = useController({ name: "fruit", control: radioForm.control }); const onCheckValid = useCallback((data: CheckFormValues) => { window.alert(`CheckSelectBox:\n${JSON.stringify(data, null, 2)}`); }, []); const onRadioValid = useCallback((data: RadioFormValues) => { window.alert(`RadioSelectBox:\n${JSON.stringify(data, null, 2)}`); }, []); const onCheckReset = useCallback( (event: FormEvent) => { event.preventDefault(); checkForm.reset(); }, [checkForm], ); const onRadioReset = useCallback( (event: FormEvent) => { event.preventDefault(); radioForm.reset(); }, [radioForm], ); return ( {POSSIBLE_FRUIT_VALUES.map((name) => ( ))} 초기화 제출 {POSSIBLE_FRUIT_VALUES.map((value) => ( } /> ))} 초기화 제출 ); } interface CheckSelectBoxItemProps { name: keyof CheckFormValues; control: Control; } function CheckSelectBoxItem({ name, control }: CheckSelectBoxItemProps) { const { field: { value, ...restProps }, fieldState: { invalid }, } = useController({ name, control }); return ( } /> ); } ``` ### Customizing Label ```tsx import { Badge, HStack } from "@seed-design/react"; import { CheckSelectBox, CheckSelectBoxCheckmark, CheckSelectBoxGroup, RadioSelectBoxItem, RadioSelectBoxRadiomark, RadioSelectBoxRoot, } from "seed-design/ui/select-box"; export default function SelectBoxCustomizingLabel() { return ( } /> Melon New } description="Elit cupidatat dolore fugiat enim veniam culpa." suffix={} /> } /> } /> Melon New } description="Elit cupidatat dolore fugiat enim veniam culpa." suffix={} /> } /> ); } ``` ### Listening to Value Changes `CheckSelectBox`는 `onCheckedChange`를 사용하여 체크박스의 선택 상태 변경을 감지할 수 있습니다. `RadioSelectBoxRoot`는 `onValueChange`를 사용하여 라디오 버튼의 선택 값 변경을 감지할 수 있습니다. ```tsx import { HStack, Text, VStack } from "@seed-design/react"; import { CheckSelectBox, CheckSelectBoxCheckmark, CheckSelectBoxGroup, RadioSelectBoxItem, RadioSelectBoxRadiomark, RadioSelectBoxRoot, } from "seed-design/ui/select-box"; import { useState } from "react"; export default function SelectBoxValueChanges() { const [checkCount, setCheckCount] = useState(0); const [checkLastValue, setCheckLastValue] = useState(null); const [radioCount, setRadioCount] = useState(0); const [radioLastValue, setRadioLastValue] = useState(null); return ( } onCheckedChange={(checked) => { setCheckCount((prev) => prev + 1); setCheckLastValue(checked); }} /> onCheckedChange called: {checkCount} times, last value: {`${checkLastValue ?? "-"}`} { setRadioCount((prev) => prev + 1); setRadioLastValue(value); }} > } /> } /> onValueChange called: {radioCount} times, last value: {radioLastValue ?? "-"} ); } ``` ### Grid Layout (Columns) `columns` prop을 사용하여 여러 열로 배치할 수 있습니다. `columns`가 1보다 크면 하위 요소의 `layout`이 자동으로 `"vertical"`로 설정됩니다. 필요한 경우 `layout` prop을 직접 설정하여 개별 항목의 레이아웃을 오버라이드할 수 있습니다. ```tsx import { IconDiamond, IconIcecreamcone } from "@karrotmarket/react-multicolor-icon"; import { VStack } from "@seed-design/react"; import { CheckSelectBox, CheckSelectBoxCheckmark, CheckSelectBoxGroup, RadioSelectBoxItem, RadioSelectBoxRadiomark, RadioSelectBoxRoot, } from "seed-design/ui/select-box"; export default function SelectBoxColumns() { return ( } label="옵션 1" description="layout=vertical" suffix={} /> } label="옵션 2" description="layout=vertical" suffix={} /> } defaultChecked layout="horizontal" label="layout=horizontal" description="layout을 horizontal로 오버라이드" suffix={} /> } label="옵션 4" description="layout=vertical" suffix={} /> } label="옵션 1" suffix={} /> } label="옵션 2" suffix={} /> } label="layout=horizontal" description="layout을 horizontal로 오버라이드" layout="horizontal" suffix={} /> } label="옵션 4" suffix={} /> } label="옵션 5" suffix={} /> } label="옵션 6" suffix={} /> ); } ``` ### With Suffix `suffix` prop을 사용하여 체크마크, 라디오 마크, 또는 커스텀 요소를 표시할 수 있습니다. `CheckSelectBoxCheckmark`와 `RadioSelectBoxRadiomark`를 사용하거나, 아이콘이나 텍스트 등 자유로운 요소를 전달할 수 있습니다. ```tsx import { IconPersonCircleLine } from "@karrotmarket/react-monochrome-icon"; import { Text, HStack, Box } from "@seed-design/react"; import { CheckSelectBox, CheckSelectBoxCheckmark, CheckSelectBoxGroup, RadioSelectBoxItem, RadioSelectBoxRadiomark, RadioSelectBoxRoot, } from "seed-design/ui/select-box"; export default function SelectBoxWithSuffix() { return ( } /> +1,000원 } /> } /> } /> +1,000원 } /> } /> ); } ``` ### Collapsible Footer `footer` prop으로 추가 콘텐츠를 표시할 수 있습니다. `footerVisibility` prop으로 footer의 표시 조건을 제어할 수 있습니다. - `"when-selected"` (기본값): 항목이 선택되었을 때만 표시 - `"when-not-selected"`: 항목이 선택되지 않았을 때만 표시 - `"always"`: 항상 표시 ```tsx import { Box, HStack, Text } from "@seed-design/react"; import { CheckSelectBox, CheckSelectBoxCheckmark, CheckSelectBoxGroup, RadioSelectBoxItem, RadioSelectBoxRadiomark, RadioSelectBoxRoot, } from "seed-design/ui/select-box"; export default function SelectBoxCollapsibleFooter() { return ( } footer={ 선택되었을 때만 보입니다. } /> } footerVisibility="always" footer={ 항상 보입니다. } /> } footerVisibility="when-not-selected" footer={ 선택되지 않았을 때만 보입니다. } /> } footer={ 선택되었을 때만 보입니다. } /> } footerVisibility="always" footer={ 항상 보입니다. } /> } footerVisibility="when-not-selected" footer={ 선택되지 않았을 때만 보입니다. } /> ); } ``` ### Fieldset/RadioGroupField Integration Fieldset/RadioGroupField 관련 prop을 사용할 수 있습니다. - `label` 및 `labelWeight` - `indicator` 및 `showRequiredIndicator` - `description` 및 `errorMessage` - `disabled`, `invalid`, `name`, `form`: `RadioSelectBoxRoot`에만 지원됩니다. ```tsx import { ActionButton, HStack, VStack, Box, Text } from "@seed-design/react"; import { useState } from "react"; import { CheckSelectBox, CheckSelectBoxCheckmark, CheckSelectBoxGroup, RadioSelectBoxItem, RadioSelectBoxRadiomark, RadioSelectBoxRoot, } from "seed-design/ui/select-box"; export default function SelectBoxFieldset() { const [checkErrors, setCheckErrors] = useState>({}); const [radioErrorMessage, setRadioErrorMessage] = useState(); const handleCheckSubmit = (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.currentTarget); const fruits = formData.getAll("fruit"); if (fruits.includes("apple")) { setCheckErrors({ apple: "Apple은 선택할 수 없습니다." }); return; } setCheckErrors({}); alert(JSON.stringify(fruits, null, 2)); }; const handleRadioSubmit = (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.currentTarget); const color = formData.get("color"); if (color === "red") { setRadioErrorMessage("Red는 선택할 수 없습니다."); return; } setRadioErrorMessage(undefined); alert(JSON.stringify({ color }, null, 2)); }; return (
    } footer={ Apple을 선택하고 제출하면 에러 메시지가 표시됩니다. } /> } /> } /> 제출
    } footer={ Red를 선택하고 제출하면 에러 메시지가 표시됩니다. } /> } disabled /> } /> 제출
    ); } ``` --- file: components/select.mdx # Select 트리거를 눌러 열리는 목록에서 값을 선택하는 컴포넌트입니다. 사용 가능 버전: @seed-design/react@2.1.0, @seed-design/css@2.3.0 ## Preview ```tsx import { Box } from "@seed-design/react"; import { SelectContent, SelectGroup, SelectItem, SelectRoot, SelectTrigger, } from "seed-design/ui/select"; export default function SelectPreview() { return ( ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:select - pnpm: pnpm dlx @seed-design/cli@latest add ui:select - yarn: yarn dlx @seed-design/cli@latest add ui:select - bun: bun x @seed-design/cli@latest add ui:select ## Usage ```tsx import { SelectRoot, SelectTrigger, SelectContent, SelectGroup, SelectItem, } from "seed-design/ui/select"; ``` ```tsx ``` - `SelectRoot`: 선택 값(`value`)과 열림 상태를 관리합니다. - `SelectTrigger`: 선택된 값 또는 placeholder를 표시하며, 클릭 시 목록을 엽니다. - `SelectContent`: 옵션 목록을 감싸는 플로팅 컨테이너입니다. - `SelectGroup`: 관련된 옵션들을 그룹으로 묶습니다. 모든 `SelectItem`은 `SelectGroup` 안에 있어야 합니다. `label`로 그룹의 제목을 표시할 수 있습니다. - `SelectItem`: 개별 옵션입니다. `value`가 필요하며, 선택되면 체크마크가 표시됩니다. ## Props ### `SelectRoot` ### `SelectTrigger` ### `SelectContent` ### `SelectGroup` ### `SelectItem` ## Examples ### Size `size`로 트리거와 목록의 크기를 정합니다. (default: `large`) `responsive` 사용 시 화면 너비에 따라 size가 자동으로 전환됩니다. ```tsx import { VStack } from "@seed-design/react"; import { SelectContent, SelectGroup, SelectItem, SelectRoot, SelectTrigger, } from "seed-design/ui/select"; export default function SelectSize() { return ( ); } ``` ### Groups `SelectGroup`으로 옵션을 묶고, `label`로 그룹의 제목을 표시합니다. `SelectGroup`의 개수와 관계없이 모든 `SelectItem`은 `SelectGroup` 안에 있어야 합니다. 그룹이 두 개 이상이면 그룹 사이에 구분선이 자동으로 그려집니다. ```tsx import { Box } from "@seed-design/react"; import { SelectContent, SelectGroup, SelectItem, SelectRoot, SelectTrigger, } from "seed-design/ui/select"; export default function SelectGroups() { return ( ); } ``` ### Multiple Selection `SelectRoot`에 `multiple`을 지정하면 여러 옵션을 선택할 수 있습니다. 옵션을 선택해도 목록이 닫히지 않으며, 이미 선택된 옵션을 다시 누르면 선택이 해제됩니다. 트리거에는 선택된 옵션들의 `textValue`가 `", "`로 이어져 표시됩니다. 이 문구는 [Custom Value Format](#custom-value-format)으로 바꿀 수 있습니다. ```tsx import { Box } from "@seed-design/react"; import { SelectContent, SelectGroup, SelectItem, SelectRoot, SelectTrigger, } from "seed-design/ui/select"; export default function SelectMultiple() { return ( ); } ``` ### With Description `SelectItem`의 `description` prop으로 옵션에 부가 설명을 추가합니다. ```tsx import { Box } from "@seed-design/react"; import { SelectContent, SelectGroup, SelectItem, SelectRoot, SelectTrigger, } from "seed-design/ui/select"; export default function SelectWithDescription() { return ( ); } ``` ### With Prefix Icon `SelectItem`의 `prefixIcon` prop으로 옵션에 아이콘을 표시합니다. 1개의 옵션이 선택된 경우 해당 옵션의 아이콘이 트리거의 prefix 아이콘으로 표시됩니다. 선택된 옵션이 없거나, 선택된 옵션에 아이콘이 없거나, 여러 옵션이 선택된 경우 트리거에는 `SelectTrigger`에 지정한 `prefixIcon`이 표시됩니다. ```tsx import { IconGlobeLine, IconLockLine, IconPerson2Line, IconPersonLine, } from "@karrotmarket/react-monochrome-icon"; import { Box } from "@seed-design/react"; import { SelectContent, SelectGroup, SelectItem, SelectRoot, SelectTrigger, } from "seed-design/ui/select"; export default function SelectWithPrefixIcon() { return ( } /> } /> } /> } /> ); } ``` ### Custom Item Label `SelectItem`의 `label`에는 ReactNode를 넘길 수 있습니다. 이 노드는 목록의 옵션에만 렌더링됩니다. `SelectItem`의 `label`로 string이 아닌 ReactNode를 지정하는 경우 트리거에는 `SelectItem`의 `textValue`가 표시됩니다. `textValue`는 `label`이 string이면 `label`, 아니면 `value`입니다. 따라서, `label`이 string이 아닌 경우 `textValue`를 함께 지정하는 것을 권장합니다. `textValue`는 트리거에 표시되는 문구, 키보드로 타이핑해 옵션을 찾을 때 매칭되는 문자열(typeahead), 폼 제출용 native `