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)/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; ``` ## 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/(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`: 토글 버튼 내 아이콘 배치 --- 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 ); } ``` ### Safe Area `pt`, `pr`, `pb`, `pl`과 `bleed` 계열 속성(`bleed`, `bleedX`, `bleedY`, `bleedTop`, `bleedRight`, `bleedBottom`, `bleedLeft`)은 `"safeArea"` 값을 받습니다. 이 값을 지정하면 각 방향의 safe area inset을 padding이나 bleed 값으로 사용합니다. ```tsx 배경은 화면 끝까지 채우고, 내용은 safe area 안쪽에 둡니다. ``` `p`(`padding`), `px`(`paddingX`), `py`(`paddingY`)는 `"safeArea"`를 받지 않습니다. 이 속성들은 양쪽에 같은 값을 적용하는데, safe area inset은 방향마다 다를 수 있기 때문입니다. 양쪽 모두 필요하면 `pl`, `pr`처럼 방향별 속성을 함께 지정하세요. 이 속성으로 배경과 스크롤 영역을 화면 끝까지 채우는 방법은 [Safe Area 구현 가이드](/react/components/concepts/safe-area#화면-끝까지-채우기)를 참고하세요. ## 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, type AttachmentDisplayItemEntry } from "@seed-design/react"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; const sampleEntries: AttachmentDisplayItemEntry[] = [ { 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 { AttachmentDisplayItemEntry } from "@seed-design/react"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; const defaultEntries: AttachmentDisplayItemEntry[] = [ { 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, type AttachmentDisplayItemEntry } from "@seed-design/react"; 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: AttachmentDisplayItemEntry[]) => { 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, type AttachmentDisplayItemEntry, type AttachmentDisplayItemStatusDetails, } from "@seed-design/react"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; const defaultEntries: AttachmentDisplayItemEntry[] = [ { 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: AttachmentDisplayItemStatusDetails) => 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 { AttachmentDisplayItemEntry } from "@seed-design/react"; 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: AttachmentDisplayItemEntry[] = [ { 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, type AttachmentDisplayItemEntry } from "@seed-design/react"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; const sampleEntries: AttachmentDisplayItemEntry[] = [ { 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, type AttachmentDisplayItemEntry } from "@seed-design/react"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; const sampleEntries: AttachmentDisplayItemEntry[] = [ { 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, type AttachmentDisplayItemEntry } from "@seed-design/react"; 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, type AttachmentDisplayItemEntry } from "@seed-design/react"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; import { TextField, TextFieldInput } from "seed-design/ui/text-field"; const defaultEntries: AttachmentDisplayItemEntry[] = 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 ( { // 외부 미디어 피커 호출 자리 }} /> ); } ``` `controlProps`에 `extendSafeAreaX`를 함께 지정하면 목록이 `--seed-attachment-input-extend-x`에 좌우 safe area inset을 더한 만큼 넓어져 화면 끝까지 스크롤되고, 첫 항목과 마지막 항목은 safe area 안에 남습니다. `AppScreenContent`처럼 inset을 반영한 컨테이너 안에서 사용하세요. 화면 끝까지 채우는 다른 방법은 [Safe Area 구현 가이드](/react/components/concepts/safe-area#화면-끝까지-채우기)를 참고하세요. ### Field Integration `label`, `description`, `errorMessage` 등 Field 관련 prop을 전달할 수 있습니다. ```tsx "use client"; import { VStack, type AttachmentDisplayItemEntry } from "@seed-design/react"; import { useState } from "react"; import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field"; const defaultEntries: AttachmentDisplayItemEntry[] = [ { 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, type AttachmentDisplayItemEntry, } from "@seed-design/react"; 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: AttachmentDisplayItemEntry; isCover?: boolean; onRetry?: () => void; }) { return ( {isCover && 대표사진} {(e) => ( )} {onRetry && ( } /> {LABEL_RETRY} )} } /> ); } const defaultEntries: AttachmentDisplayItemEntry[] = [ { 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 { AttachmentInputFileEntry } from "@seed-design/react"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; const defaultAcceptedFileEntries: AttachmentInputFileEntry[] = [ { id: "1", file: new File(["hello"], "document.pdf", { type: "application/pdf" }), status: "success", }, ]; export default function AttachmentFieldTriggerExample() { return ( ); } ``` ### Dropzone `AttachmentDropzone`을 사용하면 드래그 앤 드롭 영역이 포함된 레이아웃을 사용할 수 있습니다. ```tsx import { VStack, type AttachmentInputFileEntry } from "@seed-design/react"; import { AttachmentField, AttachmentDropzone } from "seed-design/ui/attachment-field"; const defaultAcceptedFileEntries: AttachmentInputFileEntry[] = [ { 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, type AttachmentInputFileStatusDetails } from "@seed-design/react"; import { useState } from "react"; import { AttachmentField, AttachmentInput, AttachmentInputItem, } from "seed-design/ui/attachment-field"; function simulateUpload( _file: File, id: string, updateFileEntryStatus: (id: string, details: AttachmentInputFileStatusDetails) => 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, type AttachmentInputFileEntry } from "@seed-design/react"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; const defaultAcceptedFileEntries: AttachmentInputFileEntry[] = [ { 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, type AttachmentInputFileStatusDetails } from "@seed-design/react"; 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: AttachmentInputFileStatusDetails) => 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 { AttachmentInputFileEntry } from "@seed-design/react"; 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: AttachmentInputFileEntry[] = [ { 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, type AttachmentInputFileEntry } from "@seed-design/react"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; const defaultFiles: AttachmentInputFileEntry[] = [ { 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, type AttachmentInputFileEntry } from "@seed-design/react"; import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; const defaultFiles: AttachmentInputFileEntry[] = [ { 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, type AttachmentInputFileEntry } from "@seed-design/react"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; 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, }} > ); } ``` `extendSafeAreaX`를 함께 지정하면 목록이 `--seed-attachment-input-extend-x`에 좌우 safe area inset을 더한 만큼 넓어져 화면 끝까지 스크롤되고, 첫 항목과 마지막 항목은 safe area 안에 남습니다. `AppScreenContent`처럼 inset을 반영한 컨테이너 안에서 사용하세요. 화면 끝까지 채우는 다른 방법은 [Safe Area 구현 가이드](/react/components/concepts/safe-area#화면-끝까지-채우기)를 참고하세요. ### 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, type AttachmentInputFileEntry } 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 { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field"; interface FormValues { files: AttachmentInputFileEntry[]; } 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, type AttachmentInputFileEntry, } from "@seed-design/react"; 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: AttachmentInputFileEntry; 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 "use client"; import { Badge } from "seed-design/ui/badge"; export default function BadgePreview() { return 라벨; } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:badge - pnpm: pnpm dlx @seed-design/cli@latest add ui:badge - yarn: yarn dlx @seed-design/cli@latest add ui:badge - bun: bun x @seed-design/cli@latest add ui:badge ## Props ## Usage ```tsx "use client"; import { Badge } from "seed-design/ui/badge"; export function Example() { return 라벨; } ``` ## Prefix and Action `prefix`에는 Label 앞에 표시할 아이콘 요소를 전달합니다. ```tsx "use client"; import { IconHeartFill } from "@karrotmarket/react-monochrome-icon"; import { Badge } from "seed-design/ui/badge"; export default function BadgePrefix() { return }>관심 등록; } ``` 정보 아이콘에는 안내의 양과 사용자가 해야 할 일에 맞는 UI를 연결하세요. 짧은 설명에는 `actionProps.render`로 Help Bubble을 연결할 수 있습니다. `actionProps`에는 정보 아이콘 버튼의 접근성 이름인 `aria-label`을 반드시 전달하세요. ```tsx "use client"; import { HelpBubbleTrigger } from "seed-design/ui/help-bubble"; import { Badge } from "seed-design/ui/badge"; export default function BadgeWithAction() { return ( ( {trigger} ), }} > 판매 완료 ); } ``` 설명이 길거나 사용자의 확인이 필요하면 Bottom Sheet를 연결할 수 있습니다. 다음 예시는 부동산 게시글의 `집주인` Badge에서 집주인 인증에 관한 설명을 제공합니다. ```tsx "use client"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { Badge } from "seed-design/ui/badge"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "seed-design/ui/bottom-sheet"; export default function BadgeWithBottomSheet() { const [open, setOpen] = useState(false); return ( {trigger}, }} > 집주인 매물을 올린 사람이 집주인임을 확인할 수 있어, 중개소에서 등록한 매물과 구분할 수 있어요. setOpen(false)}> 확인 ); } ``` ## Examples ### Truncating Behavior 긴 라벨을 한 줄로 줄여 표시하려면 `style.maxWidth`로 최대 너비를 지정하세요. 지정한 너비를 넘는 내용은 말줄임표로 표시됩니다. ```tsx "use client"; import { Badge } from "seed-design/ui/badge"; export default function BadgeTruncating() { return ( In velit velit deserunt amet veniam incididunt consectetur incididunt Lorem. ); } ``` ### Tones and Variants #### Neutral ```tsx "use client"; import { HStack, VStack } from "@seed-design/react"; import { Badge } from "seed-design/ui/badge"; export default function BadgeNeutral() { return ( 라벨 라벨 라벨 라벨 라벨 라벨 ); } ``` #### Brand ```tsx "use client"; import { HStack, VStack } from "@seed-design/react"; import { Badge } from "seed-design/ui/badge"; export default function BadgeBrand() { return ( 라벨 라벨 라벨 라벨 라벨 라벨 ); } ``` #### Informative ```tsx "use client"; import { HStack, VStack } from "@seed-design/react"; import { Badge } from "seed-design/ui/badge"; export default function BadgeInformative() { return ( 라벨 라벨 라벨 라벨 라벨 라벨 ); } ``` #### Positive ```tsx "use client"; import { HStack, VStack } from "@seed-design/react"; import { Badge } from "seed-design/ui/badge"; export default function BadgePositive() { return ( 라벨 라벨 라벨 라벨 라벨 라벨 ); } ``` #### Warning ```tsx "use client"; import { HStack, VStack } from "@seed-design/react"; import { Badge } from "seed-design/ui/badge"; export default function BadgeWarning() { return ( 라벨 라벨 라벨 라벨 라벨 라벨 ); } ``` #### Critical ```tsx "use client"; import { HStack, VStack } from "@seed-design/react"; import { Badge } from "seed-design/ui/badge"; 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; ``` ### 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 ( ); } ``` ## 설치 - 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 ## 속성 ### `Callout` ### `ActionableCallout` ### `DismissibleCallout` ## 예시 ### 콘텐츠 구성 #### 텍스트만 ```tsx import { VStack } from "@seed-design/react"; import { ActionableCallout, Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutTextOnly() { return ( ); } ``` #### 아이콘 포함 ```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="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> ); } ``` #### 제목 텍스트 포함 ```tsx import { VStack } from "@seed-design/react"; import { ActionableCallout, Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutWithTitleText() { return ( ); } ``` #### 링크 레이블 포함 `ActionableCallout`에서는 `linkProps`를 제공하지 않아요. ```tsx import { VStack } from "@seed-design/react"; import { Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutWithLinkLabel() { return ( ); } ``` #### 모든 요소 포함 ```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: "시도해 보기" }} /> ); } ``` ### 커스터마이징 가능한 요소 #### 링크 레이블을 자식 요소로 렌더링 ```tsx import { VStack } from "@seed-design/react"; import { Callout, DismissibleCallout } from "seed-design/ui/callout"; export default function CalloutLinkLabelAsChild() { return ( 시도해 보기 ), }} /> 시도해 보기 ), }} /> ); } ``` ### 톤 #### 중립 (기본값) ```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="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> ); } ``` #### 정보 ```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="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> ); } ``` #### 긍정 ```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="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> ); } ``` #### 경고 ```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="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> ); } ``` #### 위험 ```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="기능에 대한 안내 또는 유익한 내용을 전달해요. 콜아웃은 꼭 필요한 경우에만 절제하여 사용해요." /> ); } ``` #### 매직 ```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, Checkbox } from "@seed-design/react"; import { Checkmark } from "seed-design/ui/checkbox"; function CustomCheckbox({ children, ...props }: Checkbox.RootPrimitiveProps) { 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 { ChipTabsContent, ChipTabsList, ChipTabsRoot, ChipTabsTrigger, } from "seed-design/ui/chip-tabs"; export default function ChipTabsPreview() { return (
라벨1 라벨2 라벨3 Content 1 Content 2 Content 3
); } const Content = (props: React.PropsWithChildren) => { return (
{props.children}
); }; ``` ## 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` ## Usage ### Content 탭 선택으로만 콘텐츠를 전환할 때는 `ChipTabsContent`를 `ChipTabsRoot` 안에 배치합니다. Trigger와 Content의 `value`를 맞추면 선택된 패널만 표시됩니다. 이 구성은 전환 애니메이션과 좌우 스와이프를 제공하지 않습니다. ```tsx import { ChipTabsContent, ChipTabsList, ChipTabsRoot, ChipTabsTrigger, } from "seed-design/ui/chip-tabs"; export function App() { return ( 라벨1 라벨2 라벨3 첫 번째 콘텐츠 두 번째 콘텐츠 세 번째 콘텐츠 ); } ``` `ChipTabsRoot`는 `value`와 `onValueChange`로 제어하거나 `defaultValue`로 초기 선택 값을 지정합니다. `ChipTabsTrigger`의 `notification`은 label 옆에 작은 `NotificationBadge`를 표시합니다. ### Swipeable `ChipTabsContent`를 `ChipTabsCarousel` 안에 배치하면 탭을 바꿀 때 콘텐츠가 가로로 전환되고, `swipeable`을 설정하면 콘텐츠를 좌우로 밀어 탭을 바꿀 수 있습니다. Registry의 `ChipTabsCarousel`은 내부에서 Camera를 조립하므로 `ChipTabsCarouselCamera`를 따로 추가할 필요가 없습니다. 아래 예제에서 Trigger를 누르거나 콘텐츠를 좌우로 밀면 선택된 chip과 콘텐츠가 함께 바뀝니다. 스와이프 시작·종료·정착은 `ChipTabsCarousel`의 `onSwipeStart`, `onSwipeEnd`, `onSettle`로 처리할 수 있습니다. ```tsx import { ChipTabsCarousel, ChipTabsContent, ChipTabsList, ChipTabsRoot, ChipTabsTrigger, } from "seed-design/ui/chip-tabs"; export default function ChipTabsSwipeable() { return (
라벨1 라벨2 라벨3 Content 1 Content 2 Content 3
); } const Content = (props: React.PropsWithChildren) => { return (
{props.children}
); }; ``` ## Examples ### Size=Medium ```tsx import { ChipTabsContent, ChipTabsList, ChipTabsRoot, ChipTabsTrigger, } from "seed-design/ui/chip-tabs"; export default function ChipTabsSizeMedium() { return (
라벨1 라벨2 라벨3 Content 1 Content 2 Content 3
); } const Content = (props: React.PropsWithChildren) => { return (
{props.children}
); }; ``` ### Size=Large ```tsx import { ChipTabsContent, ChipTabsList, ChipTabsRoot, ChipTabsTrigger, } from "seed-design/ui/chip-tabs"; export default function ChipTabsSizeLarge() { return (
라벨1 라벨2 라벨3 Content 1 Content 2 Content 3
); } const Content = (props: React.PropsWithChildren) => { return (
{props.children}
); }; ``` ### Variant=Neutral Solid ```tsx import { ChipTabsContent, ChipTabsList, ChipTabsRoot, ChipTabsTrigger, } from "seed-design/ui/chip-tabs"; export default function ChipTabsVariantNeutralSolid() { return (
라벨1 라벨2 라벨3 Content 1 Content 2 Content 3
); } const Content = (props: React.PropsWithChildren) => { return (
{props.children}
); }; ``` ### Variant=Neutral Outline ```tsx import { ChipTabsContent, ChipTabsList, ChipTabsRoot, ChipTabsTrigger, } from "seed-design/ui/chip-tabs"; export default function ChipTabsVariantNeutralOutline() { return (
라벨1 라벨2 라벨3 Content 1 Content 2 Content 3
); } const Content = (props: React.PropsWithChildren) => { return (
{props.children}
); }; ``` ### Notification ```tsx import { ChipTabsContent, ChipTabsList, ChipTabsRoot, ChipTabsTrigger, } from "seed-design/ui/chip-tabs"; export default function ChipTabsNotification() { return (
라벨1 라벨2 라벨3 Content 1 Content 2 Content 3
); } const Content = (props: React.PropsWithChildren) => { return (
{props.children}
); }; ``` ### Extending to Safe Area `ChipTabsRoot`에 `extendSafeAreaX`를 지정하면 가로 스크롤 영역과 `ChipTabsCarousel`의 패널이 좌우 safe area inset까지 화면 끝으로 넓어집니다. 스크롤하기 전에는 첫 chip이, 끝까지 스크롤한 뒤에는 마지막 chip이 safe area 경계에서 화면 여백만큼 떨어져 놓입니다. `AppScreenContent`처럼 inset을 반영한 컨테이너 바로 안에서 사용하세요. 화면 끝까지 채우는 다른 방법은 [Safe Area 구현 가이드](/react/components/concepts/safe-area#화면-끝까지-채우기)를 참고하세요. `ChipTabsCarousel`의 패널은 inset만큼 안쪽 여백을 다시 가지므로, 패널 안에서도 `AppScreenContent` 안과 같은 방법으로 요소를 화면 끝까지 넓힐 수 있습니다. 다만 `100vw`나 `position: fixed`처럼 패널의 여백을 따르지 않는 요소는 이 보정을 받지 않습니다. ### With Scroll Fog 탭이 많아 가로 스크롤이 필요한 경우 [Scroll Fog](/react/components/scroll-fog)를 적용하여 좌우 스크롤 힌트를 제공할 수 있습니다. 권장 padding인 `20px`을 유지해야 합니다. ```tsx import { ScrollFog } from "@seed-design/react"; import { ChipTabsContent, ChipTabsList, ChipTabsRoot, ChipTabsTrigger, } from "seed-design/ui/chip-tabs"; const tabs = Array.from({ length: 15 }, (_, i) => String(i + 1)); export default function ChipTabsWithScrollFog() { return (
{tabs.map((value) => ( 라벨{value} ))} {tabs.map((value) => ( Content {value} ))}
); } const Content = (props: React.PropsWithChildren) => { return (
{props.children}
); }; ``` --- 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 `@seed-design/react@3.0.0`에서 제거된 `ActionChip`과 `ControlChip`을 대체합니다. ### 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 // ActionButton은 기본적으로 button 태그를 사용하지만, asChild를 사용해 링크로 변경할 수 있습니다. 글쓰기 ``` > 기본 요소 타입을 변경하는 경우, 해당 요소가 접근성을 유지하고 올바르게 작동하는지 확인하는 것은 개발자의 책임입니다. 예를 들어, 버튼을 `div`로 변경하면 더 이상 키보드로 접근할 수 없게 될 수 있습니다. ### React 컴포넌트와 조합하기 ```jsx // 라우터 라이브러리에서 제공하는 Link 컴포넌트에 기능을 합성할 수 있습니다. ``` `asChild`를 사용하여 컴포넌트를 조합할 때는 세 가지 원칙을 따라야 합니다. #### 1. 컴포넌트는 props를 전개해야 합니다 asChild는 기능을 제공하고 접근성을 유지하기 위한 고유의 props와 이벤트 핸들러를 전달합니다. 컴포넌트가 이러한 props를 지원하지 않으면 제대로 작동하지 않습니다. ```jsx // 잘못된 방법 const MyButton = () => ); } ``` ```tsx title="MyRow.tsx" ``` ```css title="MyRow.css" .my-row { display: flex; align-items: center; gap: 12px; padding: 12px 16px; } .my-row:active { background-color: var(--seed-color-bg-transparent-pressed); --seed-content-scale: var(--seed-feedback-scale); /* [!code highlight] */ } ``` `ContentScale`은 요소의 유일한 자식으로 두세요. 요소의 `display`와 flex·grid 레이아웃 값(`flex-direction`, `align-items`, `gap`, `grid-template-columns` 등)을 이어받아 콘텐츠를 배치하므로, 레이아웃과 패딩은 지금처럼 요소에 작성하면 됩니다. 축소 트랜지션도 `ContentScale`에 포함되어 있습니다. `ContentScale`은 기본으로 `span`을 렌더합니다. 콘텐츠를 배치하는 요소가 이미 있다면 `asChild`로 그 요소를 대신 사용할 수 있습니다. ```tsx
{children}
``` ## 자동으로 처리되는 것 아래 두 가지는 별도로 처리할 필요가 없습니다. - **동작 줄이기 설정**: `prefers-reduced-motion: reduce` 환경에서는 `--seed-feedback-scale`이 항상 `1`이 됩니다. - **크기를 알 수 없는 상황**: JavaScript가 실행되지 않았거나 첫 측정 전이라면 `--seed-feedback-scale`이 `1`이 됩니다. 잘못된 배율이 적용되는 대신 효과만 나타나지 않습니다. ## 알아둘 점 ### 중첩된 SEED 컴포넌트 커스텀 컴포넌트가 `Checkmark`, `Radiomark`, `Switchmark` 등을 감싸는 경우, 바깥 요소와 안쪽 요소가 함께 줄어들어 안쪽 요소가 이중으로 축소됩니다. 각 컴포넌트 문서에 끄는 방법이 안내되어 있습니다. - [Checkbox](/react/components/checkbox#scale-feedback) - [RadioGroup](/react/components/radio-group#scale-feedback) - [Switch](/react/components/switch#scale-feedback) ### `position: fixed` 자손 `scale` 값이 있는 요소는 stacking context이자 `position: fixed` 자손의 containing block이 되므로, 이 요소 안에 `position: fixed` 요소가 있다면 뷰포트가 아니라 \**이 요소를 기준으로 배치됩니다.*\* `ScaleFeedback`이 적용된 요소와 그 안의 `ContentScale`은 눌리지 않은 상태에서도 `scale: 1`을 유지하므로, 누르는 동안 자손이 따라 움직이지는 않습니다. 다만 배치 기준 자체가 뷰포트와 달라지므로 `scale`을 적용한 요소 하위에 `position: fixed` 자손이 있다면 의도한 위치에 표시되는지 확인하세요. ## 프로젝트 전체에서 끄기 배율을 `1`로 고정하는 규칙을 [`@layer`](/react/getting-started/styling/cascade-layers) 없이 추가하고 **SEED CSS보다 뒤에 로드**하세요. 배율을 읽는 모든 곳이 이 변수 하나를 거치므로 SEED 컴포넌트와 커스텀 컴포넌트가 함께 꺼집니다. ```css title="scale-feedback-off.css" .seed-scale-feedback { --seed-feedback-scale: 1; } ``` 평상시 `scale: 1`은 그대로 남으므로 [`position: fixed` 자손](#position-fixed-자손)의 배치 기준은 달라지지 않습니다. --- file: components/concepts/snippet.mdx # Snippet 자주 사용되는 UI 패턴을 스니펫으로 제공하여 개발 효율성을 높이는 방법을 안내합니다. ## 개요 Snippet은 Compound Components를 기본으로 채택하면서도 개발자 경험(DX)을 최적화하기 위한 해결책입니다. 복잡한 compound components를 간단하게 사용할 수 있도록 도와주는 패턴입니다. ## Compound Components 기존의 닫힌 컴포넌트 방식에서는 컴포넌트의 커스터마이징이 제한적이었습니다. 예를 들어, 다음과 같은 닫힌 컴포넌트는: ```tsx Click me ``` 내부 구현이 숨겨져 있어 다음과 같은 커스터마이징 요구사항을 해결하기 어려웠습니다: - label에 스타일을 추가하는 방법 - 체크박스의 아이콘을 변경하는 방법 - ref를 div와 input에 각각 전달하는 방법 이러한 문제를 해결하기 위해 compound components 패턴을 도입했습니다: ```tsx Click me ``` ## Snippet의 필요성 Compound components는 뛰어난 유연성을 제공하지만, 매번 이렇게 자세한 구현을 작성하는 것은 번거로울 수 있습니다. 이러한 유연성과 DX간의 trade-off를 해결하기 위해 Snippet 개념이 도입되었습니다. ## Snippet 사용하기 ### CLI 실행 다음 명령어를 통해 필요한 컴포넌트의 snippet을 프로젝트에 추가할 수 있습니다: ```bash npx @seed-design/cli add ``` CLI 명령어에 대해 더 알아봅니다. ### 생성된 Snippet 활용 CLI 실행 후, `seed-design/ui/*` 디렉토리에 snippet이 생성됩니다. 예를 들어: ```tsx // seed-design/ui/checkbox.tsx export const Checkbox = React.forwardRef( ({ inputProps, rootRef, label, variant = "square", ...otherProps }, ref) => { return ( : null} checked={} indeterminate={} /> {label} ); }, ); ``` ### 사용 Snippet을 통해 복잡한 compound components를 간단하게 사용할 수 있습니다: ```tsx import { Checkbox } from "seed-design/ui/checkbox"; const Demo = () => Click me; ``` --- file: components/content-placeholder.mdx # Content Placeholder 이미지나 콘텐츠가 로드되지 않았을 때, 해당 영역의 성격을 전달하는 대체 시각 요소입니다. 사용 가능 버전: @seed-design/react@1.2.8, @seed-design/css@1.2.6 ## Preview ```tsx import { ContentPlaceholder } from "seed-design/ui/content-placeholder"; export default function ContentPlaceholderPreview() { return ; } ``` ## Usage ## Installation - npm: npx @seed-design/cli@latest add ui:content-placeholder - pnpm: pnpm dlx @seed-design/cli@latest add ui:content-placeholder - yarn: yarn dlx @seed-design/cli@latest add ui:content-placeholder - bun: bun x @seed-design/cli@latest add ui:content-placeholder ## Props ## Examples ### Type Preset `type`을 지정하여 미리 정의된 프리셋 아이콘과 배경을 사용할 수 있습니다. `type`을 지정하지 않는 경우 `"default"` 타입이 렌더링됩니다. ```tsx import { HStack } from "@seed-design/react"; import { ContentPlaceholder } from "seed-design/ui/content-placeholder"; import { contentPlaceholderVariantMap } from "@seed-design/css/recipes/content-placeholder"; export default function ContentPlaceholderTypeExample() { return ( {contentPlaceholderVariantMap.type.map((type) => ( ))} ); } ``` ### Sizes 컴포넌트에 `width`와 `height`를 직접 지정하여 다양한 크기로 사용할 수 있습니다. ```tsx import { HStack, VStack } from "@seed-design/react"; import { ContentPlaceholder } from "seed-design/ui/content-placeholder"; const sizes = [ { label: "48x48", width: 48, height: 48 }, { label: "200x80", width: 200, height: 80 }, { label: "200x120", width: 200, height: 120 }, { label: "200x50", width: 200, height: 50 }, { label: "300x300", width: 300, height: 300 }, { label: "320x200", width: 320, height: 200 }, { label: "40x120", width: 40, height: 120 }, { label: "40x40", width: 40, height: 40 }, ]; export default function ContentPlaceholderSizes() { return ( {sizes.map(({ label, width, height }) => ( {label} ))} ); } ``` ### Customizing Asset - `children`을 전달하여 `type` 프리셋에 없는 아이콘을 사용할 수 있습니다. ```tsx import { IconAppleFill, IconDiamondFill, IconSparkle2Fill, } from "@karrotmarket/react-monochrome-icon"; import { HStack } from "@seed-design/react"; import { ContentPlaceholder } from "seed-design/ui/content-placeholder"; export default function ContentPlaceholderSvgExample() { return ( ); } ``` --- file: components/contextual-floating-button.mdx # Contextual Floating Button 화면 위에 떠 있으며 특정 상황에서만 나타나는 보조적인 동작을 위한 버튼입니다. 사용 가능 버전: @seed-design/react@0.0.30, @seed-design/css@0.0.30 ## Preview ```tsx import { IconBellFill } from "@karrotmarket/react-monochrome-icon"; import { PrefixIcon } from "@seed-design/react"; import { ContextualFloatingButton } from "seed-design/ui/contextual-floating-button"; export default function ContextualFloatingButtonPreview() { return ( } /> 알림 설정 ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:contextual-floating-button - pnpm: pnpm dlx @seed-design/cli@latest add ui:contextual-floating-button - yarn: yarn dlx @seed-design/cli@latest add ui:contextual-floating-button - bun: bun x @seed-design/cli@latest add ui:contextual-floating-button ## Props ## Examples ### Float Composition Contextual Floating Button은 `` 컴포넌트와 함께 사용하면 편리합니다. ```tsx import { IconBellFill } from "@karrotmarket/react-monochrome-icon"; import { Box, Float, PrefixIcon } from "@seed-design/react"; import { ContextualFloatingButton } from "seed-design/ui/contextual-floating-button"; export default function ContextualFloatingButtonFloatComposition() { return ( } /> 알림 설정 ); } ``` ### Solid Variant ```tsx import { IconPlusLine } from "@karrotmarket/react-monochrome-icon"; import { PrefixIcon } from "@seed-design/react"; import { ContextualFloatingButton } from "seed-design/ui/contextual-floating-button"; export default function ContextualFloatingButtonSolid() { return ( } /> Solid Variant ); } ``` ### Layer Variant ```tsx import { IconPlusLine } from "@karrotmarket/react-monochrome-icon"; import { PrefixIcon } from "@seed-design/react"; import { ContextualFloatingButton } from "seed-design/ui/contextual-floating-button"; export default function ContextualFloatingButtonLayer() { return ( } /> Layer Variant ); } ``` ### Icon Only ```tsx import { IconPlusFill } from "@karrotmarket/react-monochrome-icon"; import { Icon } from "@seed-design/react"; import { ContextualFloatingButton } from "seed-design/ui/contextual-floating-button"; export default function ContextualFloatingButtonIconOnly() { return ( } /> ); } ``` ### Loading ```tsx import { IconPlusLine } from "@karrotmarket/react-monochrome-icon"; import { PrefixIcon } from "@seed-design/react"; import { useState } from "react"; import { ContextualFloatingButton } from "seed-design/ui/contextual-floating-button"; export default function ContextualFloatingButtonLoading() { const [loading, setLoading] = useState(false); function handleClick() { setLoading(true); setTimeout(() => setLoading(false), 2000); } // 이벤트 핸들링이 필요할 수 있으므로 loading은 disabled를 포함하지 않습니다. 이벤트 발생을 원하지 않는 경우, disabled 속성을 추가해주세요. return ( } /> 시간이 걸리는 액션 ); } ``` --- file: components/date-picker.mdx # Date Picker 달력에서 하나의 날짜, 날짜 범위 또는 여러 날짜를 선택하는 컴포넌트입니다. 사용 가능 버전: @seed-design/react@2.2.0, @seed-design/css@2.4.0 ## Preview ```tsx import { Box } from "@seed-design/react"; import { DatePicker } from "seed-design/ui/date-picker"; export default function DatePickerPreview() { return ( ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:date-picker - pnpm: pnpm dlx @seed-design/cli@latest add ui:date-picker - yarn: yarn dlx @seed-design/cli@latest add ui:date-picker - bun: bun x @seed-design/cli@latest add ui:date-picker Snippet은 Date Picker 컴포넌트와 컴포넌트 전용 props만 제공합니다. 날짜 값 타입, 제약 조건 helper처럼 UI 구현과 독립적인 API는 `@seed-design/react`에서 가져오세요. ```tsx import { dateOnOrAfter, type DatePickerDate } from "@seed-design/react"; import { DatePicker } from "seed-design/ui/date-picker"; ``` Wheel Picker는 `ui:date-picker`의 Registry 의존성으로 함께 설치됩니다. ## Props ### DatePicker ### TwoMonthDatePicker ### WeekDatePicker ### ContinuousDatePicker ## Examples ### 레이아웃 Date Picker는 레이아웃에 따라 다음 공개 컴포넌트로 나뉩니다. - `DatePicker`: 현재 월의 실제 주 수만 렌더링합니다. - `TwoMonthDatePicker`: 연속한 두 달을 가로로 표시합니다. 이전·다음 버튼은 한 달씩 이동합니다. - `WeekDatePicker`: 한 주만 표시하며 이전·다음 버튼은 한 주씩 이동합니다. - `ContinuousDatePicker`: `monthRange` 안의 월을 세로로 가상화합니다. `monthRange`를 생략하면 `yearRange`를 월 범위로 변환합니다. `height`, `minHeight`, `maxHeight` 중 하나를 반드시 지정합니다. 한 달과 두 달 레이아웃은 항상 6주 높이를 확보하지 않고 각 월에 필요한 4~6주만 렌더링합니다. Date Picker는 자체적으로 고정 폭이나 최대 폭을 제한하지 않고 부모 컨테이너의 너비를 채웁니다. 한 달과 두 달 레이아웃에 필요한 가로 크기는 사용하는 화면에서 적절한 너비의 컨테이너로 감싸서 결정하세요. ```tsx "use client"; import { Box, VStack } from "@seed-design/react"; import { ContinuousDatePicker, DatePicker, TwoMonthDatePicker, WeekDatePicker, } from "seed-design/ui/date-picker"; import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control"; import * as React from "react"; const today = { year: 2026, month: 7, day: 30 }; const layoutOptions = [ { value: "month", label: "한 달" }, { value: "twoMonths", label: "두 달" }, { value: "week", label: "한 주" }, { value: "continuous", label: "연속" }, ] as const; type DatePickerLayout = (typeof layoutOptions)[number]["value"]; export default function DatePickerVisibleRanges() { const [layout, setLayout] = React.useState("month"); const picker = (() => { switch (layout) { case "twoMonths": return ; case "week": return ; case "continuous": return ( ); default: return ; } })(); return ( setLayout(value as DatePickerLayout)} > {layoutOptions.map((option) => ( {option.label} ))} {picker} ); } ``` ### 연속 달력의 노출 월 제한 `ContinuousDatePicker`의 `monthRange`로 스크롤해서 볼 수 있는 첫 월과 마지막 월을 제한할 수 있습니다. 양끝 월을 모두 포함합니다. `yearRange`만 전달하면 시작 연도의 1월부터 종료 연도의 12월까지로 변환합니다. 두 prop을 함께 사용하면 `monthRange`는 `yearRange` 안에 있어야 합니다. `monthRange`는 노출되는 월을 정하고 `constraints`는 날짜별 선택 가능 여부를 정합니다. 아래 예시는 12월 15일부터 30일 뒤인 1월 14일까지만 선택할 수 있게 하면서, 스크롤 범위도 12월과 1월로 제한합니다. ```tsx "use client"; import { Box, dateOnOrAfter, dateOnOrBefore } from "@seed-design/react"; import { ContinuousDatePicker } from "seed-design/ui/date-picker"; const today = { year: 2026, month: 12, day: 15 }; const thirtyDaysLater = { year: 2027, month: 1, day: 14 }; export default function ContinuousDatePickerMonthRange() { return ( ); } ``` ### 선택 방식 `selectionMode`에 따라 하나의 날짜(`single`), 날짜 범위(`range`), 여러 날짜(`multiple`)를 선택합니다. 기본값은 `single`입니다. - `single`의 값은 `DatePickerDate`입니다. - `range`는 시작일을 선택한 직후 `{ start }`, 종료일까지 선택하면 `{ start, end }`를 반환합니다. - `multiple`은 중복 없는 날짜 배열을 항상 오름차순으로 반환합니다. ```tsx "use client"; import { Box, Text, VStack, type DatePickerDate, type DatePickerRangeValue, } from "@seed-design/react"; import { DatePicker } from "seed-design/ui/date-picker"; import * as React from "react"; const today = { year: 2026, month: 7, day: 30 }; export default function DatePickerSelectionModes() { const [single, setSingle] = React.useState(today); const [range, setRange] = React.useState({ start: { year: 2026, month: 7, day: 10 }, end: { year: 2026, month: 7, day: 13 }, }); const [multiple, setMultiple] = React.useState([ { year: 2026, month: 7, day: 7 }, { year: 2026, month: 7, day: 14 }, ]); return ( Single Range Multiple ); } ``` ### 날짜 선택 제약 조건 `constraints`는 후보 날짜를 선택할 수 있는지 판단하는 함수 배열입니다. 모든 함수가 `true`를 반환해야 선택할 수 있습니다. 직접 함수를 작성하거나 `@seed-design/react`의 다음 helper를 조합할 수 있습니다. - `dateOnOrAfter`, `dateOnOrBefore`: 선택 가능한 날짜의 양끝을 포함해 제한합니다. - `excludeDates`: 예약 완료일, 휴무일처럼 선택할 수 없는 날짜를 제외합니다. Range에서는 기본적으로 시작일부터 후보 종료일까지의 전체 구간을 검사합니다. - `rangeDayCountAtLeast`, `rangeDayCountAtMost`: 시작일과 종료일을 모두 포함한 날짜 수를 제한합니다. - `maxSelectionCount`: Multiple에서 선택 가능한 최대 개수를 제한합니다. 이미 선택한 날짜의 해제는 허용합니다. 조건을 만족하지 않는 날짜는 키보드로 포커스할 수 있지만 `aria-disabled` 상태이며 선택할 수 없습니다. 아래 예시는 체크인·체크아웃을 포함한 선택 구간을 1~14일로 제한해 당일치기를 허용하고, 예약 완료일이 포함된 구간은 선택할 수 없도록 구성합니다. ```tsx "use client"; import { Box, Text, VStack, excludeDates, rangeDayCountAtLeast, rangeDayCountAtMost, type DatePickerDate, type DatePickerRangeValue, } from "@seed-design/react"; import { TwoMonthDatePicker } from "seed-design/ui/date-picker"; import * as React from "react"; const bookedDateKeys = new Set(["2026-07-18", "2026-07-19", "2026-07-25"]); function toDateKey(date: DatePickerDate) { return `${date.year}-${String(date.month).padStart(2, "0")}-${String(date.day).padStart(2, "0")}`; } const constraints = [ rangeDayCountAtLeast(1), rangeDayCountAtMost(14), excludeDates((date) => bookedDateKeys.has(toDateKey(date))), ]; export default function DatePickerReservation() { const [value, setValue] = React.useState({ start: { year: 2026, month: 7, day: 10 }, }); return ( 체크인 {toDateKey(value.start)} {value.end ? ` · 체크아웃 ${toDateKey(value.end)}` : " · 체크아웃을 선택하세요"} 선택 구간은 체크인·체크아웃을 포함해 1~14일이어야 합니다. 예약 완료일(7월 18일·19일·25일)이 포함된 구간은 선택할 수 없습니다. ); } ``` ### 시작일을 유지하고 종료일만 변경 이미 시작된 기간을 수정할 때는 `selectionMode="range"`와 `rangeStartReadOnly`를 함께 사용합니다. 기존 시작일은 선택 상태로 표시되지만 변경할 수 없고, 시작일보다 늦은 날짜만 새 종료일로 선택할 수 있습니다. `rangeStartReadOnly`에는 시작일을 포함한 `value` 또는 `defaultValue`가 필요합니다. 제어 방식으로 사용할 때 외부에서 전달하는 시작일이 바뀌면 Date Picker에도 새 값이 반영됩니다. 아래 광고 기간 예시는 시작일이 8월 7일이고 오늘이 8월 10일인 상황입니다. `dateOnOrAfter`를 함께 사용해 이미 지난 8일과 9일은 새 종료일로 선택할 수 없도록 제한합니다. ```tsx "use client"; import { Box, Text, VStack, dateOnOrAfter, type DatePickerRangeValue } from "@seed-design/react"; import { DatePicker } from "seed-design/ui/date-picker"; import * as React from "react"; const today = { year: 2026, month: 8, day: 10 } as const; const initialValue: DatePickerRangeValue = { start: { year: 2026, month: 8, day: 7 }, end: { year: 2026, month: 8, day: 14 }, }; function formatDate(date: DatePickerRangeValue["start"]) { return `${date.year}.${date.month}.${date.day}`; } export default function DatePickerReadOnlyRangeStart() { const [value, setValue] = React.useState(initialValue); return ( 광고 기간 {formatDate(value.start)} {value.end ? ` ~ ${formatDate(value.end)}` : ""} ); } ``` ### 날짜 셀에 추가 정보 표시 기본 날짜 숫자를 유지하면서 가격, 예약 가능 여부, 혼잡도 같은 정보를 추가할 때는 `renderDateCellSupplement`를 우선 사용하세요. 함수에는 날짜와 포맷된 날짜 숫자, 선택·오늘·범위·사용 불가·포커스 상태가 전달됩니다. `renderDateCellSupplement`로 표현할 수 없어 날짜 숫자를 포함한 내부 콘텐츠 전체를 직접 구성해야 할 때만 `renderDateCellContent`를 사용합니다. 이 prop은 일반적인 확장 지점이 아니라 저수준 이스케이프 해치이며, 두 prop은 동시에 사용할 수 없습니다. 컴포넌트는 날짜 셀의 DOM, ARIA, 이벤트, 상태 배경을 계속 소유합니다. 따라서 두 함수에서는 버튼이나 `gridcell`을 만들지 말고 콘텐츠만 반환합니다. 각 주의 셀 높이는 가장 높은 콘텐츠에 맞춰 늘어납니다. 좁은 화면에서 콘텐츠가 가로로 넘치는 경우에는 렌더 함수에서 `maxLines` 같은 말줄임 정책을 직접 적용합니다. ```tsx "use client"; import { Box, Text } from "@seed-design/react"; import { DatePicker } from "seed-design/ui/date-picker"; const prices = new Map([ [10, "12만원"], [11, "9만원"], [12, "11만원"], [14, "8만원"], ]); export default function DatePickerCustomCell() { return ( ( {isUnavailable ? "선택 불가" : (prices.get(date.day) ?? "예약 가능")} )} constraints={[(date) => date.day !== 13]} /> ); } ``` ### Time Picker와 조합 Date Picker는 시간과 시간대를 해석하지 않는 달력 날짜인 `DatePickerDate`를 사용합니다. Time Picker의 `TimePickerValue`와 별도로 상태를 관리한 뒤, 서버 요청이나 도메인 모델 경계에서 두 값을 합칩니다. 시간대가 필요한 서비스는 이 시점에 `CalendarDateTime`, `ZonedDateTime` 또는 서비스의 날짜·시간 DTO로 변환하세요. Date Picker 자체에 `CalendarDateTime`을 전달하면 날짜 선택 UI가 불필요하게 시간대 정책에 결합되므로 지원하지 않습니다. ```tsx "use client"; import { Box, Text, VStack, type DatePickerDate, type TimePickerValue } from "@seed-design/react"; import { DatePicker } from "seed-design/ui/date-picker"; import { TimePicker } from "seed-design/ui/time-picker"; import * as React from "react"; const TODAY: DatePickerDate = { year: 2026, month: 7, day: 30 }; export default function DatePickerDateAndTime() { const [date, setDate] = React.useState(TODAY); const [time, setTime] = React.useState({ hour: 14, minute: 30 }); return ( {date.year}.{date.month}.{date.day}. {String(time.hour).padStart(2, "0")}: {String(time.minute).padStart(2, "0")} ); } ``` ### 날짜로 이동하고 포커스하기 `viewDate`, `defaultViewDate`, `onViewDateChange`는 현재 표시하는 월·주·스크롤 기준점을 제어합니다. Month에서는 월의 첫날로 정규화되므로 특정 날짜 셀을 이동 대상으로 지정할 때는 `actionsRef`를 사용합니다. - `navigateToDate(date)`: 날짜가 보이도록 이동하고 다음 Tab 진입점을 갱신하지만 현재 DOM 포커스는 유지합니다. 외부의 “오늘” 버튼에는 이 action을 권장합니다. - `focusDate(date)`: 날짜가 보이도록 이동한 뒤 날짜 셀에 실제 DOM 포커스를 둡니다. 키보드 단축키나 사용자를 명시적으로 그리드 안으로 이동시키는 동작에 사용합니다. ```tsx "use client"; import { ActionButton, Box, HStack, type DatePickerDate } from "@seed-design/react"; import { DatePicker, type DatePickerActions } from "seed-design/ui/date-picker"; import * as React from "react"; const today: DatePickerDate = { year: 2026, month: 7, day: 30 }; export default function DatePickerNavigationActions() { const actionsRef = React.useRef(null); return ( actionsRef.current?.navigateToDate(today)} > 오늘로 이동 actionsRef.current?.focusDate(today)}> 오늘에 포커스 ); } ``` ### 월·연도 이동 제목을 누르면 Time Picker와 같은 Wheel Picker로 전환합니다. 연도 휠은 반복하지 않고 `yearRange` 안에서 이동하며, 월 휠은 반복합니다. `yearRange`의 기본값은 `today`를 기준으로 앞뒤 100년입니다. Month는 달력 그리드 자리에 Wheel Picker를 표시합니다. Week는 대체할 그리드가 한 주뿐이므로 달력을 그대로 두고, 제목에 붙은 popover 안에 작은(`small`) Wheel Picker를 표시합니다. 제목을 다시 누르거나, popover 바깥을 누르거나, `Escape`를 누르거나, 포커스가 popover 밖으로 나가면 고른 월로 이동하며 닫힙니다. popover를 열면 포커스가 popover로 이동하고, 닫으면 제목으로 돌아옵니다. Week가 두 달에 걸치면 제목과 Wheel Picker는 다음 달을 표시하고, 다음 달이 이동 가능한 월 범위 밖이면 이전 달을 표시합니다. Month 계열의 표시 기준 날짜는 월의 첫날, Week는 locale의 주 시작일로 정규화합니다. ### Locale 기본 locale은 `ko-KR`입니다. 월·요일·날짜·숫자 표기와 주 시작일은 locale을 따릅니다. `weekStartsOn`을 전달하면 주 시작일만 재정의합니다. RTL locale에서는 좌우 방향키와 이전·다음 아이콘 방향도 반전됩니다. 루트, 이전·다음 버튼과 연도·월 휠의 기본 접근성 이름은 한국어와 영어로 제공됩니다. 제품 맥락에 맞는 이름이 필요하면 `ariaLabels`로 재정의합니다. ### Accessibility Date Picker는 WAI-ARIA Grid 패턴을 사용합니다. 하나의 날짜만 `tab` 순서에 들어가며 방향키로 날짜를 이동합니다. - `ArrowLeft`, `ArrowRight`: 하루 전·후 - `ArrowUp`, `ArrowDown`: 일주일 전·후 - `Home`, `End`: 현재 주의 시작·끝 - `PageUp`, `PageDown`: 현재 표시 단위의 이전·다음 - `Shift + PageUp`, `Shift + PageDown`: 일 년 전·후 - `Enter`, `Space`: 포커스한 날짜 선택 화면에 Date Picker의 이름을 나타내는 요소가 있다면 `aria-labelledby`로 연결할 수 있습니다. 그렇지 않으면 locale에 따라 `"날짜 선택"` 또는 `"Select date"`가 사용됩니다. --- file: components/dialog.mdx # Dialog 화면 중앙에 떠서 사용자의 주의를 모으는 다이얼로그입니다. 제목, 본문, 푸터와 닫기 버튼을 갖추어 폼이나 스크롤되는 콘텐츠 등 풍부한 내용을 담을 때 사용합니다. 사용 가능 버전: @seed-design/react@2.1.0, @seed-design/css@2.3.0 ## Preview ```tsx import { HStack, Text } from "@seed-design/react"; import { DialogAction, DialogBody, DialogContent, DialogFooter, DialogRoot, DialogTrigger, } from "seed-design/ui/dialog"; import { ActionButton } from "seed-design/ui/action-button"; const DialogPreview = () => { return ( Open Dialog 본문에는 사용자가 확인해야 할 내용이나 추가 입력 폼을 배치할 수 있습니다. 취소 확인 ); }; export default DialogPreview; ``` 사용자의 확인이나 경고가 목적이라면 Alert Dialog를 사용하세요. ## Installation ### Default 화면 중앙에 떠서 제목, 본문, 푸터를 담는 기본 Dialog 컴포넌트를 포함합니다. - npm: npx @seed-design/cli@latest add ui:dialog - pnpm: pnpm dlx @seed-design/cli@latest add ui:dialog - yarn: yarn dlx @seed-design/cli@latest add ui:dialog - bun: bun x @seed-design/cli@latest add ui:dialog ### Responsive 뷰포트에 따라 Bottom Sheet로 자동 전환되는 반응형 변형입니다. - npm: npx @seed-design/cli@latest add ui:responsive-dialog - pnpm: pnpm dlx @seed-design/cli@latest add ui:responsive-dialog - yarn: yarn dlx @seed-design/cli@latest add ui:responsive-dialog - bun: bun x @seed-design/cli@latest add ui:responsive-dialog ## Props ### `DialogRoot` ### `DialogTrigger` ### `DialogContent` ### `DialogBody` ### `DialogFooter` ### `DialogAction` ### `ResponsiveDialogRoot` ## Examples ### Trigger ``는 `aria-haspopup="dialog"` 속성을 설정하고, Dialog의 `open` 상태에 따라 `aria-expanded` 속성을 자동으로 설정합니다. 이 속성은 스크린 리더와 같은 보조 기술에 유용합니다. ```tsx import { HStack, Text } from "@seed-design/react"; import { DialogAction, DialogBody, DialogContent, DialogFooter, DialogRoot, DialogTrigger, } from "seed-design/ui/dialog"; import { ActionButton } from "seed-design/ui/action-button"; const DialogTriggerExample = () => { return ( Open Trigger를 클릭하면 현재 화면 위에 Dialog가 열립니다. 취소 확인 ); }; export default DialogTriggerExample; ``` ### Controlled Trigger 외의 방식으로 Dialog를 열고 닫을 수 있습니다. 이 경우 `open` prop을 사용하여 Dialog의 상태를 제어합니다. ```tsx import { HStack, Text } from "@seed-design/react"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { DialogAction, DialogBody, DialogContent, DialogFooter, DialogRoot, } from "seed-design/ui/dialog"; const DialogControlled = () => { const [open, setOpen] = useState(false); return ( <> setOpen(true)}> 열기 Labore do culpa dolore irure nisi dolor dolor laboris veniam ipsum excepteur adipisicing laboris non quis. Velit ea ut minim. Magna dolore culpa velit incididunt consequat sint. Fugiat ad culpa labore dolore esse dolore ex aliquip duis aute aliquip ad velit et. 취소 확인 ); }; export default DialogControlled; ``` ### Size ``에 `size` prop을 설정하여 Dialog의 너비를 변경할 수 있습니다. `"medium"` (480px, 기본값)과 `"large"` (800px)를 지원합니다. `md` 미만 화면에서는 뷰포트 너비의 90%를 차지하고, `md` 이상에서 각 size의 최대 너비가 적용됩니다. content의 높이는 뷰포트의 80%로 제한됩니다. ``에 `width`, `maxWidth` prop을 전달하여 너비를 직접 제어할 수도 있습니다. 프리셋 size 대신 뷰포트 기반의 유동적인 너비가 필요할 때 사용합니다. 단일 값은 모든 breakpoint에 적용되므로, md 미만의 기본 너비(뷰포트의 90%)를 유지하려면 `width={{ md: "560px" }}`처럼 breakpoint 객체로 전달하세요. ```tsx import { Flex, HStack, Text, VStack } from "@seed-design/react"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { DialogAction, DialogBody, DialogContent, DialogFooter, DialogRoot, DialogTrigger, } from "seed-design/ui/dialog"; import { Slider } from "seed-design/ui/slider"; const DialogSize = () => { const [width, setWidth] = useState(90); const [maxWidth, setMaxWidth] = useState(640); return ( Medium (480px) 기본 너비로 상세 정보와 주요 액션을 함께 제공합니다. 취소 확인 Large (800px) 넓은 다이얼로그에서 더 많은 폼 필드나 상세 콘텐츠를 다룹니다. 취소 확인 Custom (조절 가능) 뷰포트 너비에 따라 유동적으로 커지되 maxWidth로 최대 크기를 제한합니다. setWidth(values[0])} /> setMaxWidth(values[0])} /> 취소 확인 ); }; export default DialogSize; ``` ### Body ``는 본문 영역을 스크롤 가능하게 만듭니다. 본문이 길어 스크롤되면 헤더 아래에 구분선이 나타나고, 하단은 서서히 사라지는 마스크가 적용됩니다. Body가 뷰포트 높이를 넘겨 스크롤이 생길 때에만 하단 fade 마스크와 `padding-bottom`이 적용됩니다. 본문이 짧아 넘치지 않으면 마스크가 적용되지 않아 마지막 줄이 흐려지지 않습니다. ```tsx import { HStack, Text, VStack } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { DialogAction, DialogBody, DialogContent, DialogFooter, DialogRoot, DialogTrigger, } from "seed-design/ui/dialog"; const DialogBodyExample = () => { return ( 짧은 본문 (fade 없음) 내용이 짧아 스크롤이 없으면 하단 마스크가 적용되지 않아, 마지막 줄이 흐려지지 않습니다. 취소 확인 긴 본문 (fade 적용) {Array.from({ length: 16 }, (_, index) => ( {index + 1}. Body가 넘치면 하단에 fade 마스크와 padding-bottom이 적용되고, 스크롤하면 헤더 아래에 구분선이 나타납니다. ))} 취소 확인 ); }; export default DialogBodyExample; ``` ### Custom Body ``에 `paddingX`, `minHeight`, `maxHeight`, `justifyContent`, `alignItems` prop을 전달하여 본문 영역을 직접 제어할 수 있습니다. 기본 캡(뷰포트의 80%)보다 낮게 스크롤 높이를 제한하거나, 짧은 내용에서도 높이를 고정하거나, 가로 패딩을 제거해 콘텐츠를 가장자리까지 배치할 때 사용합니다. ```tsx import { Box, Flex, HStack, Text, VStack } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { DialogAction, DialogBody, DialogContent, DialogFooter, DialogRoot, DialogTrigger, } from "seed-design/ui/dialog"; const DialogCustomBody = () => { return ( maxHeight 200px {Array.from({ length: 12 }, (_, index) => ( {index + 1}. 본문이 200px을 넘으면 그 안에서 스크롤됩니다. ))} 취소 확인 minHeight + 가운데 정렬 아직 항목이 없습니다 취소 추가 paddingX 0 가장자리까지 닿는 영역입니다 취소 확인 ); }; export default DialogCustomBody; ``` ### Show Close Button ``에 `showCloseButton` prop을 전달하여 우측 상단 닫기 버튼을 표시할 수 있습니다. 기본 값은 `true`입니다. ```tsx import { Flex, HStack, Text } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { DialogAction, DialogBody, DialogContent, DialogFooter, DialogRoot, DialogTrigger, } from "seed-design/ui/dialog"; const DialogShowCloseButton = () => { return ( 닫기 버튼 있음 기본적으로 우측 상단에 닫기 버튼이 표시됩니다. 취소 확인 닫기 버튼 없음 닫기 버튼을 숨길 때는 본문이나 푸터에 닫을 수 있는 액션을 제공하세요. 취소 확인 ); }; export default DialogShowCloseButton; ``` ### Footer Layout `DialogFooter`는 flex 레이아웃만 제공하며, 버튼 배치는 `VStack`, `HStack` 등으로 직접 구성합니다. ```tsx import { HStack, Text, VStack } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { DialogAction, DialogBody, DialogContent, DialogFooter, DialogRoot, DialogTrigger, } from "seed-design/ui/dialog"; const DialogFooterLayout = () => { return ( Footer 레이아웃 DialogFooter는 flex 레이아웃만 제공합니다. 넓은 다이얼로그에서는 주요 액션을 우측에 가로로 정렬할 수 있습니다. 취소 확인 ); }; export default DialogFooterLayout; ``` ### Prevent Close `DialogAction`의 `onClick`에서 `e.preventDefault()`를 호출하면 다이얼로그가 닫히지 않습니다. ```tsx import { HStack } from "@seed-design/react"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { DialogAction, DialogBody, DialogContent, DialogFooter, DialogRoot, DialogTrigger, } from "seed-design/ui/dialog"; import { Switch } from "seed-design/ui/switch"; const DialogPreventClose = () => { const [preventClose, setPreventClose] = useState(true); return ( 열기 취소 { if (preventClose) { e.preventDefault(); } }} > 확인 ); }; export default DialogPreventClose; ``` ### `onOpenChange` Details `onOpenChange` 두 번째 인자로 `details`가 제공됩니다. #### `reason` **열릴 때** (`open: true`) - `"trigger"`: `DialogTrigger`로 열림 **닫힐 때** (`open: false`) - `"closeButton"`: `DialogAction` 또는 우측 상단 닫기 버튼으로 닫힘 - `"escapeKeyDown"`: ESC 키 사용 - `"interactOutside"`: 외부 영역 클릭 - `DialogRoot`는 기본적으로 `closeOnInteractOutside={false}`입니다. `interactOutside`는 이 옵션을 `true`로 설정한 경우에만 발생할 수 있습니다. - `"cascadeDismiss"`: 상위 레이어 닫힘으로 인한 연쇄 닫힘 ```tsx import { HStack, Text, VStack } from "@seed-design/react"; import { useState } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { DialogAction, DialogBody, DialogContent, DialogFooter, DialogRoot, DialogTrigger, } from "seed-design/ui/dialog"; import { Switch } from "seed-design/ui/switch"; const DialogOnOpenChangeReason = () => { const [open, setOpen] = useState(false); const [openReason, setOpenReason] = useState(null); const [closeReason, setCloseReason] = useState(null); const [closeOnInteractOutside, setCloseOnInteractOutside] = useState(false); return ( { setOpen(open); (open ? setOpenReason : setCloseReason)(details?.reason ?? null); }} > 열기 ESC 키를 누르거나 우측 상단 닫기 버튼, 하단 버튼을 눌러 닫아보세요. 아래 스위치를 켠 뒤 바깥 영역을 누르면 interactOutside 이유로 닫힙니다. 취소 확인 마지막 열림 이유: {openReason ?? "-"} 마지막 닫힘 이유: {closeReason ?? "-"} ); }; export default DialogOnOpenChangeReason; ``` #### Confirm Before Close 작성 중인 내용을 실수로 닫는 것을 막고 싶다면, `open`을 제어 상태로 두고 특정 `reason`일 때 `open`을 유지한 채 확인용 Alert Dialog를 띄워 사용자에게 되물을 수 있습니다. ```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, } from "seed-design/ui/alert-dialog"; import { DialogAction, DialogBody, DialogContent, DialogFooter, DialogRoot, DialogTrigger, type DialogRootProps, } from "seed-design/ui/dialog"; import { Switch } from "seed-design/ui/switch"; const closeReasons = [ "closeButton", "escapeKeyDown", "interactOutside", ] as const satisfies NonNullable< Parameters>[1] >["reason"][]; const confirmReasonLabels = { closeButton: "닫기 버튼으로 닫을 때 확인 (closeButton)", escapeKeyDown: "ESC 키로 닫을 때 확인 (escapeKeyDown)", interactOutside: "바깥 클릭으로 닫을 때 확인 (interactOutside)", } as const satisfies Record<(typeof closeReasons)[number], string>; const DialogConfirmBeforeClose = () => { const [open, setOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); const [closeOnInteractOutside, setCloseOnInteractOutside] = useState(true); const [confirmReasons, setConfirmReasons] = useState< Record<(typeof closeReasons)[number], boolean> >({ closeButton: true, escapeKeyDown: true, interactOutside: true }); return ( <> { if (nextOpen) { setOpen(true); return; } const reason = closeReasons.find((reason) => reason === details?.reason); if (reason && confirmReasons[reason]) { setConfirmOpen(true); return; } setOpen(false); }} > 작성 폼 열기 닫으려는 reason별로 확인 다이얼로그 띄우기 {closeReasons.map((reason) => ( setConfirmReasons((prev) => ({ ...prev, [reason]: checked })) } /> ))} 취소 저장 정말 닫을까요? 작성 중인 내용은 저장되지 않습니다. setConfirmOpen(false)}> 계속 작성 { setConfirmOpen(false); setOpen(false); }} > 닫기 ); }; export default DialogConfirmBeforeClose; ``` ### Portalled Portal은 기본적으로 `document.body`에 렌더링됩니다. ```tsx import { HStack, Portal, Text } from "@seed-design/react"; import { ActionButton } from "seed-design/ui/action-button"; import { DialogAction, DialogBody, DialogContent, DialogFooter, DialogRoot, DialogTrigger, } from "seed-design/ui/dialog"; const DialogPortalled = () => { return ( // You can set z-index dialog with "--layer-index" custom property. useful for stackflow integration. 열기 Portal은 기본적으로 document.body에 렌더링됩니다. 취소 확인 ); }; export default DialogPortalled; ``` ### Responsive `ResponsiveDialog`를 사용하면 md 이상에서는 Dialog, sm 이하에서는 Bottom Sheet로 자동 전환됩니다. 뷰포트를 줄여서 전환 동작을 확인해보세요. `onOpenChange`는 열림 상태만 전달하며, Dialog 또는 Bottom Sheet에만 적용되는 Root 옵션은 `dialogRootProps`, `bottomSheetRootProps`로 전달합니다. ```tsx import { HStack, Text, useResponsiveDialogContext } from "@seed-design/react"; import { forwardRef } from "react"; import { ActionButton } from "seed-design/ui/action-button"; import { ResponsiveDialogAction, ResponsiveDialogBody, ResponsiveDialogContent, ResponsiveDialogFooter, ResponsiveDialogRoot, ResponsiveDialogTrigger, type ResponsiveDialogFooterProps, } from "seed-design/ui/responsive-dialog"; const Footer = forwardRef((props, ref) => { const { shouldUseBottomSheet } = useResponsiveDialogContext(); return ( 취소 확인 ); }); const DialogResponsive = () => { return ( Open md 이상에서는 화면 중앙의 Dialog로, sm 이하에서는 화면 하단에서 슬라이드되는 Bottom Sheet로 표시됩니다.