);
}
```
### 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 (
ko-KR
단어 내부 줄바꿈 처리를 적절하게 하여 가독성을 높입니다.
이렇게매우긴단어를줄바꿈하지않는경우레이아웃문제를일으킬가능성이있습니다.{" "}
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
en-US
There are some long words that need to be broken properly to improve readability.
SupercalifragilisticexpialidociousEvenThoughTheSoundOfItIsSomethingQuiteAtrocious{" "}
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
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 (
TabsTab 1Tab 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 / 31:116 / 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 (
);
}
```
---
file: components/attachment-display-field.mdx
# Attachment Display Field
외부 소스에서 제공된 미디어를 URL 기반으로 표시하고 관리하는 컴포넌트입니다.
사용 가능 버전: @seed-design/react@2.0.0, @seed-design/css@2.0.0
## Preview
```tsx
import { VStack } from "@seed-design/react";
import type { DisplayItemEntry } from "@seed-design/react/primitive";
import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field";
const sampleEntries: DisplayItemEntry[] = [
{
id: "1",
thumbnailUrl: "https://picsum.photos/seed/seed1/200/200",
status: "success",
},
{
id: "2",
thumbnailUrl: "https://picsum.photos/seed/seed2/200/200",
status: "success",
},
];
// 외부 미디어 피커 모킹. 실제 환경에서는 네이티브 브릿지/모달/서버 호출 등으로 교체하세요.
async function openMediaPicker(): Promise {
const id = crypto.randomUUID();
return [
{
id,
thumbnailUrl: `https://picsum.photos/seed/${id}/200/200`,
status: "success",
},
];
}
export default function AttachmentDisplayPreview() {
return (
{
addEntries(await openMediaPicker());
}}
/>
);
}
```
## Installation
### Default
순서 변경이 불필요한 경우 사용할 수 있는 컴포넌트를 포함합니다.
- npm: npx @seed-design/cli@latest add ui:attachment-display-field
- pnpm: pnpm dlx @seed-design/cli@latest add ui:attachment-display-field
- yarn: yarn dlx @seed-design/cli@latest add ui:attachment-display-field
- bun: bun x @seed-design/cli@latest add ui:attachment-display-field
### Reorderable
드래그 앤 드롭을 통한 항목 순서 변경이 필요한 경우 활용할 수 있는 컴포넌트를 포함합니다. 프로젝트에 [dnd-kit](https://dndkit.com/overview) 의존성이 추가됩니다.
- npm: npx @seed-design/cli@latest add ui:attachment-display-field-reorderable
- pnpm: pnpm dlx @seed-design/cli@latest add ui:attachment-display-field-reorderable
- yarn: yarn dlx @seed-design/cli@latest add ui:attachment-display-field-reorderable
- bun: bun x @seed-design/cli@latest add ui:attachment-display-field-reorderable
## Props
### `AttachmentDisplayField`
### `AttachmentDisplay`
### `AttachmentDisplayItem`
## Usage
### 기본 사용법
`AttachmentDisplayField` 안에 `AttachmentDisplay` 또는 `AttachmentDisplayReorderable`을 조합하여 사용합니다.
`AttachmentDisplay`는 HTML ``을 사용하지 않습니다. `onTriggerClick`으로 외부 미디어 피커를 호출하고, 콜백 인자로 전달되는 `addEntries`에 그 결과를 넘겨 표시하세요. `addEntries`는 `maxEntries` 상한과 single-mode(`maxEntries={1}`) 치환을 내부에서 처리하므로, `entries`를 직접 펼쳐 넣는 것보다 안전합니다.
[`AttachmentField`](/react/components/attachment-field)와 달리 `AttachmentDisplayField`는 파일의 유효성을 검증하거나 파일 객체를 직접 다루지 않습니다.
```tsx
import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field";
{
const pickedEntries = await openMediaPicker();
addEntries(pickedEntries);
}}
/>
;
```
`entries`와 `onEntriesChange`로 목록을 직접 제어하는 controlled 방식도 지원합니다. 이 경우에도 `addEntries`는 동일하게 동작합니다([Controlled](#controlled) 참고).
### Item 직접 구성하기
`AttachmentDisplay`, `AttachmentDisplayReorderable`은 `children`을 render prop으로 사용합니다.
`entries`를 활용하여 `AttachmentDisplayItem`을 직접 렌더링할 수 있습니다. 이때 `DisplayItemEntry` 타입이 제공하는 `id`를 `key`로 활용하는 것을 권장합니다.
`children`을 제공하지 않는 경우 자동으로 `entries`를 `AttachmentDisplayItem`으로 렌더링합니다.
```tsx
import {
AttachmentDisplay,
AttachmentDisplayField,
AttachmentDisplayItem,
} from "seed-design/ui/attachment-display-field";
{
addEntries(await openMediaPicker());
}}
>
{({ entries }) =>
entries.map((entry) => (
))
}
;
```
## Adding Entries
### Trigger
`AttachmentDisplay`는 trigger(업로드 버튼)가 포함된 레이아웃을 제공합니다. trigger 클릭 시 `onTriggerClick` 콜백이 실행됩니다. 일반적으로 외부 미디어 피커 호출을 수행합니다. 콜백은 `{ addEntries, updateEntryStatus }`를 인자로 받아, 피커 결과를 추가하고 곧바로 업로드 상태를 갱신할 수 있습니다.
```tsx
import type { DisplayItemEntry } from "@seed-design/react/primitive";
import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field";
const defaultEntries: DisplayItemEntry[] = [
{
id: "1",
thumbnailUrl: "https://picsum.photos/seed/trigger1/200/200",
status: "success",
},
];
// 외부 미디어 피커 모킹. 실제 환경에서는 네이티브 브릿지/모달/서버 호출 등으로 교체하세요.
async function openMediaPicker(): Promise {
const id = crypto.randomUUID();
return [
{
id,
thumbnailUrl: `https://picsum.photos/seed/${id}/200/200`,
status: "success",
},
];
}
export default function AttachmentDisplayTrigger() {
return (
{
addEntries(await openMediaPicker());
}}
/>
);
}
```
### Listening to Entry Changes
`entries`는 현재 표시되고 있는 항목의 목록입니다. `onEntriesChange` 콜백으로 `entries`에 등록된 파일 변경 이벤트를 감지할 수 있습니다.
```tsx
"use client";
import { Text, VStack } from "@seed-design/react";
import type { DisplayItemEntry } from "@seed-design/react/primitive";
import { useRef, useState } from "react";
import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field";
// 외부 미디어 피커 모킹. 실제 환경에서는 네이티브 브릿지/모달/서버 호출 등으로 교체하세요.
async function openMediaPicker(): Promise {
const id = crypto.randomUUID();
return [
{
id,
thumbnailUrl: `https://picsum.photos/seed/${id}/200/200`,
status: "success",
},
];
}
export default function AttachmentDisplayValueChanges() {
const [entries, setEntries] = useState([]);
const entriesRef = useRef(entries);
const [logs, setLogs] = useState([]);
entriesRef.current = entries;
const addLog = (message: string) => {
setLogs((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${message}`]);
};
// addEntries로 추가하든 제거 버튼으로 지우든 변경은 항상 onEntriesChange로 흐르므로,
// 추가/삭제 감지를 여기 한 곳에서 처리합니다.
const handleEntriesChange = (next: DisplayItemEntry[]) => {
const prev = entriesRef.current;
const added = next.filter((n) => !prev.some((p) => p.id === n.id));
const removed = prev.filter((p) => !next.some((n) => n.id === p.id));
if (added.length > 0) addLog(`added: ${added.map((a) => a.id).join(", ")}`);
if (removed.length > 0) addLog(`removed: ${removed.map((r) => r.id).join(", ")}`);
setEntries(next);
};
return (
{logs.length === 0 ? (
아이템을 추가하거나 삭제하면 로그가 표시됩니다.
) : (
logs.map((log, index) => (
{log}
))
)}
{
addEntries(await openMediaPicker());
}}
/>
);
}
```
## Managing Item Status
`entries`의 각 항목은 `pending`, `uploading`, `success`, `error`의 status를 가질 수 있습니다. 새로 추가되는 항목의 status 기본값은 의도에 맞게 자유롭게 지정할 수 있습니다(외부 피커가 막 던진 항목이라면 `uploading`, 이미 업로드 완료된 미디어를 hydrate한다면 `success`).
외부 업로드 API와 연동하는 경우, `onTriggerClick`·`onRetry` 콜백으로 함께 전달되는 `updateEntryStatus` 헬퍼를 사용하여 각 항목의 status를 업데이트합니다.
```tsx
"use client";
import { VStack } from "@seed-design/react";
import type { DisplayItemEntry, DisplayItemStatusDetails } from "@seed-design/react/primitive";
import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field";
const defaultEntries: DisplayItemEntry[] = [
{
id: "1",
thumbnailUrl: "https://picsum.photos/seed/upload1/200/200",
status: "uploading",
progress: 30,
},
{
id: "2",
thumbnailUrl: "https://picsum.photos/seed/upload2/200/200",
status: "success",
},
{
id: "3",
thumbnailUrl: "https://picsum.photos/seed/upload3/200/200",
status: "error",
},
];
// 외부 미디어 피커 모킹. 실제 환경에서는 네이티브 브릿지/모달/서버 호출 등으로 교체하세요.
async function openMediaPicker(): Promise {
const id = crypto.randomUUID();
return [
{
id,
thumbnailUrl: `https://picsum.photos/seed/${id}/200/200`,
status: "uploading",
},
];
}
// 실제 환경에서는 네이티브 브릿지 또는 외부 업로드 API와 연동하세요.
// status는 컴포넌트가 콜백으로 전달하는 updateEntryStatus 헬퍼로만 갱신합니다.
function simulateUpload(
id: string,
updateEntryStatus: (id: string, details: DisplayItemStatusDetails) => void,
) {
updateEntryStatus(id, { status: "uploading", progress: 0 });
let progress = 0;
const interval = setInterval(() => {
progress += 20;
if (progress >= 100) {
clearInterval(interval);
updateEntryStatus(id, Math.random() > 0.5 ? { status: "success" } : { status: "error" });
} else {
updateEntryStatus(id, { status: "uploading", progress });
}
}, 300);
}
export default function AttachmentDisplayStatus() {
return (
{
const pickedEntries = await openMediaPicker();
addEntries(pickedEntries);
for (const entry of pickedEntries) {
simulateUpload(entry.id, updateEntryStatus);
}
}}
onRetry={(entry, { updateEntryStatus }) => simulateUpload(entry.id, updateEntryStatus)}
/>
);
}
```
- `uploading`: [ProgressCircle](/react/components/progress-circle)이 표시됩니다.
- `progress`를 설정하여 업로드 진행률을 표시할 수 있습니다.
- `progress`를 지정하지 않는 경우 [indeterminate](/react/components/progress-circle#indeterminate) 상태로 표시됩니다.
- `error`: 재시도 버튼이 표시됩니다.
- 클릭 시 `AttachmentDisplay`에 지정한 `onRetry` 콜백이 `(entry, { updateEntryStatus })` 인자로 실행됩니다. `updateEntryStatus`로 해당 항목을 다시 `uploading` 상태로 되돌려 업로드를 재시도하세요.
## Reordering Entries
`AttachmentDisplayReorderable`을 사용하면 드래그로 항목의 순서를 변경할 수 있습니다.
해당 컴포넌트는 `dnd-kit` 의존성 분리를 위해 별도 snippet [`ui:attachment-display-field-reorderable`](#reorderable)로 제공됩니다.
Context를 통해 `reorderEntry`가 제공되므로, 필요한 경우 원하는 드래그 앤 드롭 동작을 직접 구현하거나, 이미 프로젝트에서 사용 중인 드래그 앤 드롭 라이브러리와 연동하여 사용할 수 있습니다.
```tsx
"use client";
import type { DisplayItemEntry } from "@seed-design/react/primitive";
import { useState } from "react";
import { AttachmentDisplayField } from "seed-design/ui/attachment-display-field";
import { AttachmentDisplayReorderable } from "seed-design/ui/attachment-display-field-reorderable";
const defaultEntries: DisplayItemEntry[] = [
{ id: "1", thumbnailUrl: "https://picsum.photos/seed/reorder1/200/200", status: "success" },
{ id: "2", thumbnailUrl: "https://picsum.photos/seed/reorder2/200/200", status: "success" },
{ id: "3", thumbnailUrl: "https://picsum.photos/seed/reorder3/200/200", status: "success" },
];
// 외부 미디어 피커 모킹. 실제 환경에서는 네이티브 브릿지/모달/서버 호출 등으로 교체하세요.
async function openMediaPicker(): Promise {
const id = crypto.randomUUID();
return [
{
id,
thumbnailUrl: `https://picsum.photos/seed/${id}/200/200`,
status: "success",
},
];
}
export default function AttachmentDisplayReorderableExample() {
const [entries, setEntries] = useState(defaultEntries);
return (
{
addEntries(await openMediaPicker());
}}
/>
);
}
```
## Examples
### Disabled
`disabled` prop으로 trigger 버튼을 비활성화할 수 있습니다.
```tsx
import { VStack } from "@seed-design/react";
import type { DisplayItemEntry } from "@seed-design/react/primitive";
import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field";
const sampleEntries: DisplayItemEntry[] = [
{
id: "1",
thumbnailUrl: "https://picsum.photos/seed/disabled1/200/200",
status: "success",
},
];
export default function AttachmentDisplayDisabled() {
return (
{}} />
);
}
```
### Read Only
`readOnly` prop으로 읽기 전용 상태를 표현할 수 있습니다. trigger, 파일 제거 버튼, 순서 변경 모두 비활성화됩니다.
```tsx
import { VStack } from "@seed-design/react";
import type { DisplayItemEntry } from "@seed-design/react/primitive";
import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field";
const sampleEntries: DisplayItemEntry[] = [
{
id: "1",
thumbnailUrl: "https://picsum.photos/seed/readonly1/200/200",
status: "success",
},
{
id: "2",
thumbnailUrl: "https://picsum.photos/seed/readonly2/200/200",
status: "success",
},
];
export default function AttachmentDisplayReadOnly() {
return (
{}} />
);
}
```
### Controlled
`entries`와 `onEntriesChange`를 사용하여 외부에서 아이템 목록을 제어할 수 있습니다.
```tsx
"use client";
import { HStack, Text, VStack } from "@seed-design/react";
import type { DisplayItemEntry } from "@seed-design/react/primitive";
import { useState } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field";
// 외부 미디어 피커 모킹. 실제 환경에서는 네이티브 브릿지/모달/서버 호출 등으로 교체하세요.
async function openMediaPicker(): Promise {
const id = crypto.randomUUID();
return [
{
id,
thumbnailUrl: `https://picsum.photos/seed/${id}/200/200`,
status: "success",
},
];
}
export default function AttachmentDisplayControlled() {
const [entries, setEntries] = useState([]);
return (
{
addEntries(await openMediaPicker());
}}
/>
현재 아이템: {entries.length}개 setEntries([])}>
전체 삭제
);
}
```
### Custom Inset
`--seed-attachment-input-extend-x` CSS 변수를 사용하여 스크롤되는 아이템 목록이 레이아웃 바깥으로 빠져나오도록 구성할 수 있습니다.
```tsx
import { vars } from "@seed-design/css/vars";
import { VStack } from "@seed-design/react";
import type { DisplayItemEntry } from "@seed-design/react/primitive";
import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field";
import { TextField, TextFieldInput } from "seed-design/ui/text-field";
const defaultEntries: DisplayItemEntry[] = Array.from({ length: 8 }, (_, i) => ({
id: String(i + 1),
thumbnailUrl: `https://picsum.photos/seed/inset${i + 1}/200/200`,
status: "success",
}));
export default function AttachmentDisplayCustomInset() {
return (
{
// 외부 미디어 피커 호출 자리
}}
/>
);
}
```
### Field Integration
`label`, `description`, `errorMessage` 등 Field 관련 prop을 전달할 수 있습니다.
```tsx
"use client";
import { VStack } from "@seed-design/react";
import type { DisplayItemEntry } from "@seed-design/react/primitive";
import { useState } from "react";
import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field";
const defaultEntries: DisplayItemEntry[] = [
{ id: "1", thumbnailUrl: "https://picsum.photos/seed/field1/200/200", status: "success" },
];
// 외부 미디어 피커 모킹. 실제 환경에서는 네이티브 브릿지/모달/서버 호출 등으로 교체하세요.
async function openMediaPicker(): Promise {
const id = crypto.randomUUID();
return [
{
id,
thumbnailUrl: `https://picsum.photos/seed/${id}/200/200`,
status: "success",
},
];
}
export default function AttachmentDisplayFieldExample() {
const [entries, setEntries] = useState(defaultEntries);
const invalid = entries.length < 1;
return (
{
addEntries(await openMediaPicker());
}}
/>
);
}
```
### Customizing Items
Snippet이 제공하는 기본 아이템 구성 외에 추가적인 커스터마이징이 필요한 경우, `@seed-design/react`에서 제공하는 `AttachmentDisplay.ItemBadge` 등의 요소를 활용하여 직접 아이템을 구성할 수 있습니다.
아래 예시에서는 `AttachmentDisplay.ItemBadge`를 사용하여 첫 번째 이미지에 "대표사진" 배지를 표시합니다.
```tsx
"use client";
import { IconArrowClockwiseCircularFill, IconXmarkFill } from "@karrotmarket/react-monochrome-icon";
import { AttachmentDisplay as SeedAttachmentDisplay, Icon, VStack } from "@seed-design/react";
import type { DisplayItemEntry } from "@seed-design/react/primitive";
import { AttachmentDisplay, AttachmentDisplayField } from "seed-design/ui/attachment-display-field";
import { ProgressCircle } from "seed-design/ui/progress-circle";
const LABEL_REMOVE = "삭제";
const LABEL_RETRY = "재시도";
function CustomImageItem({
entry,
isCover,
onRetry,
}: {
entry: DisplayItemEntry;
isCover?: boolean;
onRetry?: () => void;
}) {
return (
{isCover && 대표사진}
{(e) => (
)}
{onRetry && (
} />
{LABEL_RETRY}
)}
} />
);
}
const defaultEntries: DisplayItemEntry[] = [
{ id: "1", thumbnailUrl: "https://picsum.photos/seed/customizing1/200/200", status: "success" },
{ id: "2", thumbnailUrl: "https://picsum.photos/seed/customizing2/200/200", status: "success" },
{ id: "3", thumbnailUrl: "https://picsum.photos/seed/customizing3/200/200", status: "success" },
];
export default function AttachmentDisplayCustomizingItems() {
return (
{
// 외부 미디어 피커 호출 자리
}}
>
{({ entries }) =>
entries.map((entry, index) => (
))
}
);
}
```
## Attachment Display Field vs. Attachment Field
HTML ``을 사용해야 하는 경우 `AttachmentField`를, 외부 소스와 연동하여 URL 기반으로 미디어를 표시해야 하는 경우 `AttachmentDisplayField`를 사용하세요.
| | Attachment Field | Attachment Display |
| -------------- | -------------------------- | ---------------------------- |
| 미디어 소스 | HTML `` | 이미지 URL |
| 데이터 모델 | `File` 기반 `FileEntry` | URL 기반 `DisplayItemEntry` |
| 파일 선택 | `` | 다루지 않음 (`onTriggerClick` 위임) |
| 드래그 앤 드롭으로 업로드 | `AttachmentDropzone` | 다루지 않음 |
| 파일 검증 | accept, maxFileSize 등 | 다루지 않음 |
| Form 연동 | `` 동기화 | 다루지 않음 |
---
file: components/attachment-field.mdx
# Attachment Field
파일을 선택하거나 드래그 앤 드롭으로 업로드할 수 있는 컴포넌트입니다.
사용 가능 버전: @seed-design/react@2.0.0, @seed-design/css@2.0.0
## Preview
```tsx
import { VStack } from "@seed-design/react";
import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field";
export default function AttachmentFieldPreview() {
return (
);
}
```
## Installation
### Default
파일 순서 변경이 불필요한 경우 사용할 수 있는 컴포넌트를 포함합니다.
- npm: npx @seed-design/cli@latest add ui:attachment-field
- pnpm: pnpm dlx @seed-design/cli@latest add ui:attachment-field
- yarn: yarn dlx @seed-design/cli@latest add ui:attachment-field
- bun: bun x @seed-design/cli@latest add ui:attachment-field
### Reorderable
드래그 앤 드롭을 통한 파일 순서 변경이 필요한 경우 활용할 수 있는 컴포넌트를 포함합니다. 프로젝트에 [dnd-kit](https://dndkit.com/overview) 의존성이 추가됩니다.
- npm: npx @seed-design/cli@latest add ui:attachment-field-reorderable
- pnpm: pnpm dlx @seed-design/cli@latest add ui:attachment-field-reorderable
- yarn: yarn dlx @seed-design/cli@latest add ui:attachment-field-reorderable
- bun: bun x @seed-design/cli@latest add ui:attachment-field-reorderable
## Props
### `AttachmentField`
### `AttachmentInput`
### `AttachmentDropzone`
### `AttachmentInputItem`
## Usage
### 기본 사용법
`AttachmentField` 안에 `AttachmentInput`, `AttachmentDropzone`, `AttachmentInputReorderable`, `AttachmentDropzoneReorderable` 중 하나를 조합하여 사용합니다.
```tsx
import {
AttachmentField,
AttachmentInput,
AttachmentDropzone,
} from "seed-design/ui/attachment-field";
;
;
```
```tsx
import { AttachmentField } from "seed-design/ui/attachment-field";
import {
AttachmentInputReorderable,
AttachmentDropzoneReorderable,
} from "seed-design/ui/attachment-field-reorderable";
;
;
```
### Item 직접 구성하기
`AttachmentInput`, `AttachmentDropzone`, `AttachmentInputReorderable`, `AttachmentDropzoneReorderable`은 `children`을 render prop으로 사용합니다.
`acceptedFileEntries`을 활용하여 `AttachmentInputItem`을 직접 렌더링할 수 있습니다. 이때 `FileEntry` 타입이 제공하는 `id`를 `key`로 활용하는 것을 권장합니다.
`children`을 제공하지 않는 경우 자동으로 `acceptedFileEntries`에 등록된 파일을 `AttachmentInputItem`으로 렌더링합니다.
```tsx
import {
AttachmentField,
AttachmentInput,
AttachmentInputItem,
} from "seed-design/ui/attachment-field";
{({ acceptedFileEntries }) =>
acceptedFileEntries.map((entry) => (
))
}
;
```
## Uploading Files
### Trigger
`AttachmentInput`를 사용하면 trigger(업로드 버튼)가 포함된 레이아웃을 사용할 수 있습니다.
```tsx
import type { FileEntry } from "@seed-design/react/primitive";
import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field";
const defaultAcceptedFileEntries: FileEntry[] = [
{
id: "1",
file: new File(["hello"], "document.pdf", { type: "application/pdf" }),
status: "success",
},
];
export default function AttachmentFieldTriggerExample() {
return (
);
}
```
### Dropzone
`AttachmentDropzone`을 사용하면 드래그 앤 드롭 영역이 포함된 레이아웃을 사용할 수 있습니다.
```tsx
import { VStack } from "@seed-design/react";
import type { FileEntry } from "@seed-design/react/primitive";
import { AttachmentField, AttachmentDropzone } from "seed-design/ui/attachment-field";
const defaultAcceptedFileEntries: FileEntry[] = [
{
id: "1",
file: new File(["hello"], "document.pdf", { type: "application/pdf" }),
status: "success",
},
];
export default function AttachmentFieldDropzone() {
return (
);
}
```
### Listening to Accepted File Changes
`acceptedFileEntries`는 유효성 검사를 마친 파일의 목록입니다. `onAcceptedFileEntriesChange` 콜백으로 `acceptedFileEntries`에 등록된 파일 변경 이벤트를 감지할 수 있습니다.
```tsx
import { VStack, Text } from "@seed-design/react";
import { useState } from "react";
import type { FileStatusDetails } from "@seed-design/react/primitive";
import {
AttachmentField,
AttachmentInput,
AttachmentInputItem,
} from "seed-design/ui/attachment-field";
function simulateUpload(
_file: File,
id: string,
updateFileEntryStatus: (id: string, details: FileStatusDetails) => void,
) {
updateFileEntryStatus(id, { status: "uploading", progress: 0 });
let progress = 0;
const interval = setInterval(() => {
progress += 25;
if (progress >= 100) {
clearInterval(interval);
updateFileEntryStatus(id, { status: "success" });
} else {
updateFileEntryStatus(id, { status: "uploading", progress });
}
}, 500);
}
export default function AttachmentFieldValueChanges() {
const [logs, setLogs] = useState([]);
const addLog = (message: string) => {
setLogs((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${message}`]);
};
return (
{logs.length === 0 ? (
파일을 추가하거나 삭제하면 로그가 표시됩니다.
) : (
logs.map((log, index) => (
{log}
))
)}
{
addLog(`onFileAccept: ${entries.map((e) => e.file.name).join(", ")}`);
for (const entry of entries) {
simulateUpload(entry.file, entry.id, updateFileEntryStatus);
}
}}
onAcceptedFileEntriesChange={(files) => {
addLog(
`onAcceptedFileEntriesChange: ${files.map((f) => `${f.file.name} (${f.status})`).join(", ")}`,
);
}}
onFileReject={(files) => {
addLog(
`onFileReject: ${files.map((f) => `${f.file.name} (${f.errors.join(", ")})`).join(", ")}`,
);
}}
>
{({ acceptedFileEntries }) =>
acceptedFileEntries.map((fileEntry) => (
))
}
);
}
```
## Validating Files
사용자가 선택한 파일이 `acceptedFileEntries`에 등록되기 전 유효성을 확인할 수 있습니다.
유효하지 않은 파일에 대해 각각 `onFileReject` 콜백이 실행됩니다. 해당 콜백에서 파일과 에러 코드들을 확인하여 에러 메시지를 표시할 수 있습니다.
### Max Files
`maxFiles`로 업로드 가능한 최대 파일 수를 제한할 수 있습니다. 기본값은 `1`입니다. 최대 수에 도달한 경우 trigger 및 dropzone이 비활성화됩니다.
업로드 가능한 파일의 수보다 많은 파일을 선택한 경우 업로드 가능한 파일까지 `acceptedFileEntries`에 등록됩니다. 나머지 파일에 대해서는 각각 `onFileReject` 콜백이 실행됩니다. 이때 reject된 파일의 에러는 `"TOO_MANY_FILES"`입니다.
```tsx
import { VStack } from "@seed-design/react";
import type { FileEntry } from "@seed-design/react/primitive";
import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field";
const defaultAcceptedFileEntries: FileEntry[] = [
{
id: "1",
file: new File(["hello"], "document.pdf", { type: "application/pdf" }),
status: "success",
},
];
export default function AttachmentFieldMaxFiles() {
return (
);
}
```
### Invalid File Type
`accept`로 업로드 가능한 파일의 종류를 제한할 수 있습니다.
MIME type(`image/png`, `image/*`) 또는 확장자(`.png`, `.jpg, .jpeg`) 형식을 지정할 수 있으며, `string[]`을 전달하는 경우 각 `string` `,`로 join합니다.
``에 등록되는 `accept` 속성을 통해 사용자가 선택할 수 있는 파일의 종류를 제한하는 것은 브라우저 UI에서만 동작하는 편의 기능입니다. 따라서 사용자는 브라우저 파일 선택 다이얼로그의 `모든 파일 보기`와 같은 기능을 통해 제한된 종류의 파일도 선택할 수 있습니다. 이렇게 선택된 파일의 경우 `"INVALID_TYPE"` 에러와 함께 `onFileReject` 콜백이 실행됩니다.
파일 종류 검증이 필요한 경우 해당 검증은 서버에서도 수행되어야 합니다.
```tsx
import { useState } from "react";
import { VStack } from "@seed-design/react";
import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field";
function getErrorMessage(errorCode: string): string {
switch (errorCode) {
case "INVALID_TYPE":
return "지원하지 않는 파일 형식입니다";
default:
return "업로드에 실패했습니다";
}
}
export default function AttachmentFieldInvalidFileType() {
const [errorMessage, setErrorMessage] = useState();
return (
setErrorMessage(undefined)}
onFileReject={(files) => {
const messages = files.map(
({ file, errors }) => `"${file.name}": ${errors.map(getErrorMessage).join(", ")}`,
);
setErrorMessage(messages.join("\n"));
}}
>
);
}
```
### File Size
`minFileSize`, `maxFileSize` prop을 활용할 수 있습니다.
- 사용자가 선택한 파일이 `minFileSize`보다 작은 경우 `"FILE_TOO_SMALL"` 에러와 함께 `onFileReject` 콜백이 실행됩니다.
- 사용자가 선택한 파일이 `maxFileSize`보다 큰 경우 `"FILE_TOO_LARGE"` 에러와 함께 `onFileReject` 콜백이 실행됩니다.
```tsx
import { useState } from "react";
import { VStack } from "@seed-design/react";
import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field";
import { formatBytes } from "seed-design/lib/format-bytes";
const MIN_FILE_SIZE = 1 * 1024; // 1KB
const MAX_FILE_SIZE = 10 * 1024; // 10KB
function getErrorMessage(errorCode: string): string {
switch (errorCode) {
case "FILE_TOO_LARGE":
return `크기가 ${formatBytes(MAX_FILE_SIZE)}를 초과합니다`;
case "FILE_TOO_SMALL":
return `크기가 ${formatBytes(MIN_FILE_SIZE)} 미만입니다`;
case "TOO_MANY_FILES":
return "업로드 가능한 파일 개수를 초과했습니다";
default:
return "업로드에 실패했습니다";
}
}
export default function AttachmentFieldValidation() {
const [errorMessage, setErrorMessage] = useState();
return (
setErrorMessage(undefined)}
onFileReject={(files) => {
const messages = files.map(
({ file, errors }) => `"${file.name}": ${errors.map(getErrorMessage).join(", ")}`,
);
setErrorMessage(messages.join("\n"));
}}
>
);
}
```
### Custom Validation
`validate` prop으로 각 파일에 대한 유효성 검사를 직접 추가할 수 있습니다. 커스텀 에러 코드를 반환하여 `onFileReject`에서 에러 종류별로 메시지를 분기할 수 있습니다.
```tsx
import { useState } from "react";
import { VStack } from "@seed-design/react";
import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field";
function validateFileName(file: File) {
const nameWithoutExt = file.name.replace(/\.[^.]+$/, "");
if (nameWithoutExt.length > 8) {
return ["FILENAME_TOO_LONG"];
}
return null;
}
export default function AttachmentFieldCustomValidation() {
const [errorMessage, setErrorMessage] = useState();
return (
setErrorMessage(undefined)}
onFileReject={(files) => {
if (files.every((f) => f.errors.includes("FILENAME_TOO_LONG")) === false) {
return;
}
const names = files.map((f) => f.file.name).join(", ");
setErrorMessage(`"${names}"은(는) 파일 이름이 8자를 초과합니다.`);
}}
>
);
}
```
## Managing File Status
`acceptedFileEntries`의 각 항목은 `pending`, `uploading`, `success`, `error`의 status를 가질 수 있습니다. 새로 추가되는 항목의 status 기본값은 `pending`입니다.
파일 선택 직후 외부 업로드 API와 연동하는 경우, 사용자에게 각 파일 항목의 업로딩 상태를 보여줄 수 있습니다. `onFileAccept` 콜백에서 새로 추가된 파일을 받고, 함께 제공되는 `updateFileEntryStatus` 헬퍼를 사용하여 항목의 status를 업데이트합니다.
```tsx
import { useCallback } from "react";
import { VStack } from "@seed-design/react";
import type { FileStatusDetails } from "@seed-design/react/primitive";
import {
AttachmentField,
AttachmentInput,
AttachmentInputItem,
} from "seed-design/ui/attachment-field";
// 실제 환경에서는 fetch 등으로 교체하세요.
async function uploadFile(
file: File,
onProgress: (progress: number) => void,
): Promise<{ url: string }> {
const totalChunks = 5;
for (let i = 1; i <= totalChunks; i++) {
await new Promise((r) => setTimeout(r, 200 + Math.random() * 300));
onProgress(Math.round((i / totalChunks) * 100));
}
if (Math.random() > 0.5) {
throw new Error("Network error");
}
return { url: `https://example.com/uploads/${file.name}` };
}
export default function AttachmentFieldStatus() {
const startUpload = useCallback(
(
file: File,
id: string,
updateFileEntryStatus: (id: string, details: FileStatusDetails) => void,
) => {
updateFileEntryStatus(id, { status: "uploading", progress: 0 });
uploadFile(file, (progress) => {
updateFileEntryStatus(id, { status: "uploading", progress });
})
.then(() => updateFileEntryStatus(id, { status: "success" }))
.catch(() => updateFileEntryStatus(id, { status: "error" }));
},
[],
);
return (
{
for (const entry of entries) {
startUpload(entry.file, entry.id, updateFileEntryStatus);
}
}}
>
{({ acceptedFileEntries, updateFileEntryStatus }) =>
acceptedFileEntries.map((fileEntry) => (
startUpload(fileEntry.file, fileEntry.id, updateFileEntryStatus)}
/>
))
}
);
}
```
- `uploading`: [ProgressCircle](/react/components/progress-circle)이 표시됩니다.
- `progress`를 설정하여 업로드 진행률을 표시할 수 있습니다.
- `progress`를 지정하지 않는 경우 [indeterminate](/react/components/progress-circle#indeterminate) 상태로 표시됩니다.
- `error`: 재시도 버튼이 표시됩니다.
- 클릭 시 `AttachmentInputItem`에 지정한 `onRetry` 콜백이 실행됩니다.
유효성 검증에 성공한 파일 항목은 status와 관계없이 `acceptedFileEntries`에 유지되므로, 네이티브 form 제출 시 포함됩니다.
## Reordering Files
`AttachmentInputReorderable` 또는 `AttachmentDropzoneReorderable`을 사용하면 드래그로 파일의 순서를 변경할 수 있습니다.
두 컴포넌트는 `dnd-kit` 의존성 분리를 위해 별도 snippet [`ui:attachment-field-reorderable`](#reorderable)로 제공됩니다.
Context를 통해 `reorderFileEntry`가 제공되므로, 필요한 경우 원하는 드래그 앤 드롭 동작을 직접 구현하거나, 이미 프로젝트에서 사용 중인 드래그 앤 드롭 라이브러리와 연동하여 사용할 수 있습니다.
```tsx
import type { FileEntry } from "@seed-design/react/primitive";
import { AttachmentField } from "seed-design/ui/attachment-field";
import { AttachmentInputReorderable } from "seed-design/ui/attachment-field-reorderable";
function createMockImageFile(name: string, base64: string): File {
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
return new File([bytes], name, { type: "image/png" });
}
const defaultAcceptedFileEntries: FileEntry[] = [
{
id: "1",
file: createMockImageFile(
"sunset-landscape.png",
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGN4FcEDAAN+AU+hW/ICAAAAAElFTkSuQmCC",
),
status: "success",
},
{
id: "2",
file: createMockImageFile(
"city-night.png",
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGOYbPwKAAMNAbHKe2UaAAAAAElFTkSuQmCC",
),
status: "success",
},
{
id: "3",
file: createMockImageFile(
"morning-coffee.png",
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGN4tZkDAAQwAaYlKXDxAAAAAElFTkSuQmCC",
),
status: "success",
},
];
export default function AttachmentFieldReorderableExample() {
return (
);
}
```
## Examples
### Showing Thumbnails
파일 이름 및 크기 대신 이미지 미리보기를 표시하려면 `accept`를 `"image/*"`, `["image/png", "image/jpeg"]` 등으로 설정하여 사용자가 이미지 파일만 선택할 수 있도록 제한합니다.
```tsx
import { VStack } from "@seed-design/react";
import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field";
export default function AttachmentFieldAcceptImage() {
return (
);
}
```
[`"image/heic"`](https://caniuse.com/heif) 등 일부 이미지 형식은 브라우저에
따라 이미지 미리보기가 표시되지 않을 수 있습니다.
### Disabled
`disabled` prop으로 trigger 및 dropzone을 비활성화하여 신규 파일 선택을 차단하고, ``을 `disabled` 처리하여 폼 제출 시 값이 전송되지 않도록 합니다.
```tsx
import { VStack } from "@seed-design/react";
import type { FileEntry } from "@seed-design/react/primitive";
import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field";
const defaultFiles: FileEntry[] = [
{
id: "mock-1",
file: new File(["hello"], "document.pdf", { type: "application/pdf" }),
status: "success",
},
{
id: "mock-2",
file: new File(["world"], "image.png", { type: "image/png" }),
status: "success",
},
];
export default function AttachmentFieldDisabled() {
return (
);
}
```
### Read Only
`readOnly` prop으로 첨부된 파일을 읽기 전용 상태로 표시할 수 있습니다. trigger, dropzone, 파일 제거 버튼, 순서 변경 모두 비활성화되지만 ``의 값은 유지되어 form 제출 시 함께 전송됩니다.
```tsx
import { VStack } from "@seed-design/react";
import type { FileEntry } from "@seed-design/react/primitive";
import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field";
const defaultFiles: FileEntry[] = [
{
id: "mock-1",
file: new File(["hello"], "document.pdf", { type: "application/pdf" }),
status: "success",
},
{
id: "mock-2",
file: new File(["world"], "image.png", { type: "image/png" }),
status: "success",
},
];
export default function AttachmentFieldReadOnly() {
return (
);
}
```
### Controlled
`acceptedFileEntries`와 `onAcceptedFileEntriesChange`를 사용하여 외부에서 파일 목록을 제어할 수 있습니다.
```tsx
import { VStack, HStack, Text } from "@seed-design/react";
import { useState } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import type { FileEntry } from "@seed-design/react/primitive";
import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field";
export default function AttachmentFieldControlled() {
const [acceptedFileEntries, setAcceptedFileEntries] = useState([]);
return (
현재 파일: {JSON.stringify(acceptedFileEntries.map((f) => f.file.name))} setAcceptedFileEntries([])}
>
전체 삭제
);
}
```
### Custom Inset
`--seed-attachment-input-extend-x` CSS 변수를 사용하여 스크롤되는 아이템 목록이 레이아웃 바깥으로 빠져나오도록 구성할 수 있습니다.
```tsx
import { VStack } from "@seed-design/react";
import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field";
import { TextField, TextFieldInput } from "seed-design/ui/text-field";
import { vars } from "@seed-design/css/vars";
const mockedFiles = Array.from({ length: 8 }, (_, i) => {
const file = new File(["file content"], `file${i + 1}.txt`, { type: "text/plain" });
Object.defineProperty(file, "size", { value: 1 });
return file;
});
export default function AttachmentFieldCustomInset() {
return (
({
id: `${index}`,
file,
status: "pending",
}))}
rootProps={{
style: {
"--seed-attachment-input-extend-x": vars.$dimension.spacingX.globalGutter,
} as React.CSSProperties,
}}
>
);
}
```
### Field Integration
`label`, `description`, `errorMessage` 등의 Field 관련 prop을 사용할 수 있습니다.
```tsx
import { Divider, VStack } from "@seed-design/react";
import { AttachmentField, AttachmentInput } from "seed-design/ui/attachment-field";
export default function AttachmentFieldField() {
return (
);
}
```
### Form (Uncontrolled)
`acceptedFileEntries`에 등록된 파일이 ``의 `files`로 동기화되므로 `