*/}
}
value="짜장"
title="짜장"
/>
}
value="짬뽕"
title="짬뽕"
/>
```
Disabled \[#disabled]
```tsx
import {
IconChevronRightLine,
IconPersonCircleLine,
IconSlashCircleLine,
} from "@karrotmarket/react-monochrome-icon";
import { Divider, Icon, VStack } from "@seed-design/react";
import { RadioGroup } from "@seed-design/react/primitive";
import { List, ListButtonItem, ListCheckItem, ListRadioItem } from "seed-design/ui/list";
import { Checkmark } from "seed-design/ui/checkbox";
import { Radiomark } from "seed-design/ui/radio-group";
export default function ListDisabled() {
return (
} />}
title="활성화된 ListButtonItem"
detail="Cupidatat et pariatur amet."
suffix={ } />}
/>
} />}
title="활성화된 ListCheckItem"
suffix={ }
/>
} />}
title="활성화된 ListRadioItem"
suffix={ }
value="foo"
/>
} />}
title="비활성화된 ListButtonItem"
detail="Cupidatat et pariatur amet."
suffix={ } />}
/>
} />}
title="비활성화된 ListCheckItem"
suffix={ }
/>
} />}
title="비활성화된 ListRadioItem"
suffix={ }
value="foo"
/>
);
}
```
Variants \[#variants]
Highlighted \[#highlighted]
```tsx
import { IconPersonCircleLine } from "@karrotmarket/react-monochrome-icon";
import { Box, Icon, VStack } from "@seed-design/react";
import { useState } from "react";
import { List, ListDivider, ListItem, ListButtonItem } from "seed-design/ui/list";
import { Switch } from "seed-design/ui/switch";
export default function ListHighlighted() {
const [highlighted, setHighlighted] = useState(true);
return (
} />}
title="버튼"
detail="Enim aute duis magna mollit aute sit aliquip duis ut tempor sunt."
onClick={() => {}}
/>
} />}
title="하이라이트된 버튼"
detail="Enim aute duis magna mollit aute sit aliquip duis ut tempor sunt."
onClick={() => {}}
/>
} />}
title="하이라이트 및 비활성화된 버튼"
detail="Enim aute duis magna mollit aute sit aliquip duis ut tempor sunt."
onClick={() => {}}
/>
} />}
title="하이라이트"
highlighted={highlighted}
/>
);
}
```
With Bottom Sheet \[#with-bottom-sheet]
```tsx
import {
BottomSheetBody,
BottomSheetContent,
BottomSheetFooter,
BottomSheetRoot,
BottomSheetTrigger,
} from "seed-design/ui/bottom-sheet";
import { ActionButton } from "seed-design/ui/action-button";
import { Checkmark } from "seed-design/ui/checkbox";
import { List, ListCheckItem } from "seed-design/ui/list";
import { PrefixIcon, VStack } from "@seed-design/react";
import { useState } from "react";
import { IconArrowClockwiseCircularFill } from "@karrotmarket/react-monochrome-icon";
const TYPES = ["버스", "지하철", "택시", "자전거", "도보"] as const;
export default function ListBottomSheet() {
const [isOpen, setIsOpen] = useState(false);
const [selectedTypes, setSelectedTypes] = useState<(typeof TYPES)[number][]>([]);
return (
BottomSheet 열기
);
}
```
Alignment \[#alignment]
`alignItems` prop으로 `prefix`와 `suffix`의 정렬 방식을 조정할 수 있습니다.
```tsx
import { List, ListItem } from "seed-design/ui/list";
import { HStack } from "@seed-design/react";
import { IdentityPlaceholder } from "seed-design/ui/identity-placeholder";
import { Avatar } from "seed-design/ui/avatar";
export default function ListAlignment() {
return (
} />}
title="Prefix에 Avatar 넣기. Veniam elit velit esse ea incididunt sunt sit aute."
detail="Et proident sit ullamco ut voluptate. Voluptate eiusmod occaecat adipisicing quis qui esse."
/>
} />}
title="Prefix에 Avatar 넣고 상단으로 정렬하기. Veniam elit velit esse ea incididunt sunt sit aute."
detail="일반적으로 `title`이 길어질 때 `alignItems`를 `flex-start`로 설정합니다."
/>
);
}
```
Border Radius \[#border-radius]
`List`에 `itemBorderRadius` prop을 설정하여 active 상태에서 적용되는 리스트 항목의 radius를 조정할 수 있습니다. 리스트가 카드 등 모서리가 둥근 컨테이너 안에 포함되는 경우 유용합니다.
```tsx
import { List, ListCheckItem, ListRadioItem } from "seed-design/ui/list";
import { ListHeader } from "seed-design/ui/list-header";
import { Checkmark } from "seed-design/ui/checkbox";
import { Radiomark } from "seed-design/ui/radio-group";
import { HStack, VStack } from "@seed-design/react";
import { RadioGroup } from "@seed-design/react/primitive";
export default function ListBorderRadius() {
return (
카드 borderRadius: r3_5
}
/>
}
/>
카드 borderRadius: 22px
}
/>
}
/>
);
}
```
Customization and Composition \[#customization-and-composition]
Usage \[#usage-1]
@seed-design/react 패키지에서 제공하는 `List`와 `ListHeader` 컴포넌트는 다음과 같은 구조로 사용됩니다.
```
ListHeader
List.Root
└── List.Item
├── List.Prefix (선택사항)
├── List.Content
│ ├── List.Title
│ └── List.Detail (선택사항)
└── List.Suffix (선택사항)
└── List.Item
├── ...
```
```tsx
import { ListHeader, List, Icon } from "@seed-design/react";
내 정보
} />
내 프로필
다른 사람들에게 보이는 내 정보를 관리합니다.
} />
{/* ... */}
```
- `ListHeader`: 리스트의 제목이나 설명을 표시하는 헤더 역할
- `List.Root`: 모든 리스트 항목을 감싸는 컨테이너 역할
- `List.Item`: 개별 리스트 항목. 클릭 가능한 영역을 정의
- `List.Prefix`: 아이콘, [Avatar](/react/components/avatar), [Checkmark](/react/components/checkbox#using-checkmark) 등을 표시할 수 있는 시작 영역
- `List.Content`: 주요 콘텐츠가 들어가는 중앙 영역
- `List.Title`: 리스트 항목의 제목
- `List.Detail`: 부가 설명이나 세부 정보
- `List.Suffix`: 아이콘, [Action Button](/react/components/action-button), [Toggle Button](/react/components/toggle-button) 등을 표시할 수 있는 끝 영역
`asChild` prop으로 적절한 시맨틱 요소와 조합하기 \[#aschild-prop으로-적절한-시맨틱-요소와-조합하기]
`asChild` prop에 대해 자세히 알아봅니다.
Using `asChild` prop in `List.Content` \[#using-aschild-prop-in-listcontent]
리스트 항목 전체 영역을 클릭 가능한 버튼으로 만드는 경우 활용할 수 있는 패턴입니다. 이 경우 `List.Item`에 `asChild` prop을 사용하지 않도록 유의하세요. `List.Prefix` 또는 `List.Suffix`에 버튼을 넣는 경우 `button`이 중첩되는 등 유효하지 않은 HTML이 생성됩니다.
```tsx
import { List as SeedList } from "@seed-design/react";
{/* [!code highlight] */}
{/* [!code highlight] */}
alert("사용자 클릭됨")}>
사용자
{/* [!code highlight] */}
{/* [!code highlight] */}
alert("보기 클릭됨")}
>
보기
```
[Snippet](/react/components/concepts/snippet)으로 제공되는 `ListButtonItem` 및 `ListLinkItem`는 이 패턴을 쉽게 구현할 수 있도록 돕습니다.
```tsx
import { ListButtonItem } from "seed-design/ui/list";
alert("사용자 클릭됨")}
title="사용자"
detail="항목 6개"
suffix={
alert("보기 클릭됨")}
>
보기
}
/>
```
Using `asChild` prop in `List.Item` \[#using-aschild-prop-in-listitem]
리스트 항목 전체 영역을 `label`로 만들고, `List.Prefix` 또는 `List.Suffix`에 [Switchmark](/react/components/switch#using-switchmark), [Checkmark](/react/components/checkbox#using-checkmark) 또는 [Radiomark](/react/components/radio-group#using-radiomark)를 넣는 경우 활용할 수 있는 패턴입니다.
```tsx
import { List as SeedList } from "@seed-design/react";
import { Checkbox } from "@seed-design/react/primitive";
{/* [!code highlight] */}
{/* */}
{/* [!code highlight] */}
동의
{/* [!code highlight] */}
{/* [!code highlight] */}
```
[Snippet](/react/components/concepts/snippet)으로 제공되는 `ListSwitchItem`, `ListCheckItem` 및 `ListRadioItem`는 이 패턴을 쉽게 구현할 수 있도록 돕습니다.
```tsx
import { ListCheckItem } from "seed-design/ui/list";
}
/>
```
---
file: components/manner-temp-badge.mdx
# Manner Temp Badge
매너 온도를 배지 형태로 표현하는 컴포넌트입니다. 콤팩트한 공간에서 사용자의 매너 온도 레벨을 간단히 표시할 때 사용됩니다.
## Preview
```tsx
import { VStack } from "@seed-design/react";
import { MannerTempBadge } from "seed-design/ui/manner-temp-badge";
export default function BadgePreview() {
return (
);
}
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:manner-temp-badge
- pnpm: pnpm dlx @seed-design/cli@latest add ui:manner-temp-badge
- yarn: yarn dlx @seed-design/cli@latest add ui:manner-temp-badge
- bun: bun x @seed-design/cli@latest add ui:manner-temp-badge
Props \[#props]
- `temperature`
- type: `number`
- required: `true`
- description: The manner temperature of the badge. Level will be calculated based on this value. If level is provided, this will be ignored.
- `level`
- type: `"l1" | "l2" | "l3" | "l4" | "l5" | "l6" | "l7" | "l8" | "l9" | "l10" | undefined`
- default: `"l1"`
---
file: components/manner-temp.mdx
# Manner Temp
사용자의 매너온도를 시각적으로 표현하는 컴포넌트입니다. 신뢰도/매너 정도를 직관적으로 보여주는 데에 사용합니다.
## Preview
```tsx
import { VStack } from "@seed-design/react";
import { MannerTemp } from "seed-design/ui/manner-temp";
export default function MannerTempPreview() {
return (
);
}
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:manner-temp
- pnpm: pnpm dlx @seed-design/cli@latest add ui:manner-temp
- yarn: yarn dlx @seed-design/cli@latest add ui:manner-temp
- bun: bun x @seed-design/cli@latest add ui:manner-temp
Props \[#props]
- `temperature`
- type: `number`
- required: `true`
- description: The manner temperature of the MannerTemp component. Level will be calculated based on this value. If level is provided, this will be ignored.
- `level`
- type: `"l1" | "l2" | "l3" | "l4" | "l5" | "l6" | "l7" | "l8" | "l9" | "l10" | undefined`
- default: `"l1"`
---
file: components/menu-sheet.mdx
# Menu Sheet
사용자의 작업과 관련된 선택지를 제공하는 시트 형태의 컴포넌트입니다.
## Preview
```tsx
import { IconEyeSlashLine } from "@karrotmarket/react-monochrome-icon";
import { ActionButton } from "seed-design/ui/action-button";
import {
MenuSheetContent,
MenuSheetGroup,
MenuSheetItem,
MenuSheetRoot,
MenuSheetTrigger,
} from "seed-design/ui/menu-sheet";
const MenuSheetPreview = () => {
return (
Open
}
/>
} />
} />
} />
} tone="critical" />
);
};
export default MenuSheetPreview;
```
Stackflow와 Menu Sheet를 함께 사용하는 방법에 대해 알아보세요.
Installation \[#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 \[#props]
`MenuSheetRoot` \[#menusheetroot]
- `lazyMount`
- type: `boolean | undefined`
- default: `true`
- description: Whether to enable lazy mounting
- `unmountOnExit`
- type: `boolean | undefined`
- default: `true`
- description: Whether to unmount on exit.
- `skipAnimation`
- type: `boolean | undefined`
- default: `false`
- `children`
- type: `React.ReactNode`
- required: `true`
- `role`
- type: `"dialog" | "alertdialog" | undefined`
- default: `"dialog"`
- description: The role of the dialog.
- `closeOnInteractOutside`
- type: `boolean | undefined`
- default: `true`
- description: Whether to close the dialog when the outside is clicked
- `closeOnEscape`
- type: `boolean | undefined`
- default: `true`
- description: Whether to close the dialog when the escape key is pressed
- `open`
- type: `boolean | undefined`
- `defaultOpen`
- type: `boolean | undefined`
- `onOpenChange`
- type: `((open: boolean, details?: DialogChangeDetails) => void) | undefined`
`MenuSheetTrigger` \[#menusheettrigger]
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`MenuSheetContent` \[#menusheetcontent]
- `title`
- type: `React.ReactNode`
- `description`
- type: `React.ReactNode`
- `layerIndex`
- type: `number | undefined`
- `labelAlign`
- type: `"left" | "center" | undefined`
- default: `"left"`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`MenuSheetGroup` \[#menusheetgroup]
- `labelAlign`
- type: `"left" | "center" | undefined`
- default: `"left"`
`MenuSheetItem` \[#menusheetitem]
- `prefixIcon`
- type: `React.ReactNode`
- `label`
- type: `React.ReactNode`
- required: `true`
- `description`
- type: `React.ReactNode`
- `tone`
- type: `"neutral" | "critical" | undefined`
- default: `"neutral"`
- `labelAlign`
- type: `"left" | "center" | undefined`
- default: `"left"`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
Examples \[#examples]
With Title \[#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 \[#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 \[#label-align]
`MenuSheetContent`의 `labelAlign` prop으로 메뉴 항목의 레이블 정렬을 설정합니다.
`labelAlign="left"` (with `PrefixIcon`) \[#labelalignleft-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`) \[#labelaligncenter-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` \[#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]
`onOpenChange` 두 번째 인자로 `details`가 제공됩니다.
`reason` \[#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)(meta?.reason ?? null);
}}
>
열기
} />
} />
} />
마지막 열림 이유: {openReason ?? "-"}
마지막 닫힘 이유: {closeReason ?? "-"}
);
}
```
Skip Animation \[#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/page-banner.mdx
# Page Banner
페이지 상단에 위치하며 사용자에게 전체적인 상태나 중요한 메시지를 전달하는 상위 레벨 메시지 컴포넌트입니다.
## Preview
```tsx
import { VStack } from "@seed-design/react";
import {
ActionablePageBanner,
DismissiblePageBanner,
PageBanner,
} from "seed-design/ui/page-banner";
export default function PageBannerPreview() {
return (
);
}
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:page-banner
- pnpm: pnpm dlx @seed-design/cli@latest add ui:page-banner
- yarn: yarn dlx @seed-design/cli@latest add ui:page-banner
- bun: bun x @seed-design/cli@latest add ui:page-banner
Props \[#props]
`PageBanner` \[#pagebanner]
- `prefixIcon`
- type: `React.ReactNode`
- `title`
- type: `React.ReactNode`
- `description`
- type: `React.ReactNode`
- required: `true`
- `suffix`
- type: `React.ReactNode`
- `variant`
- type: `"weak" | "solid" | undefined`
- default: `"weak"`
- `tone`
- type: `"neutral" | "informative" | "positive" | "warning" | "critical" | "magic" | undefined`
- default: `"neutral"`
`PageBannerButton` \[#pagebannerbutton]
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`ActionablePageBanner` \[#actionablepagebanner]
- `prefixIcon`
- type: `React.ReactNode`
- `title`
- type: `React.ReactNode`
- `description`
- type: `React.ReactNode`
- required: `true`
- `variant`
- type: `"weak" | "solid" | undefined`
- default: `"weak"`
- `tone`
- type: `"neutral" | "informative" | "positive" | "warning" | "critical" | "magic" | undefined`
- default: `"neutral"`
`DismissiblePageBanner` \[#dismissiblepagebanner]
- `prefixIcon`
- type: `React.ReactNode`
- `title`
- type: `React.ReactNode`
- `description`
- type: `React.ReactNode`
- required: `true`
- `variant`
- type: `"weak" | "solid" | undefined`
- default: `"weak"`
- `tone`
- type: `"neutral" | "informative" | "positive" | "warning" | "critical" | "magic" | undefined`
- default: `"neutral"`
- `defaultOpen`
- type: `boolean | undefined`
- `open`
- type: `boolean | undefined`
- `onDismiss`
- type: `(() => void) | undefined`
Examples \[#examples]
With Button \[#with-button]
`PageBanner`의 `suffix` prop에 `PageBannerButton`을 전달하여 버튼을 추가할 수 있습니다.
```tsx
import { PageBanner, PageBannerButton } from "seed-design/ui/page-banner";
export default function PageBannerWithButton() {
return (
자세히 보기}
/>
);
}
```
Rendering `PageBannerButton` as a Child \[#rendering-pagebannerbutton-as-a-child]
```tsx
import { PageBanner, PageBannerButton } from "seed-design/ui/page-banner";
export default function PageBannerButtonAsChild() {
return (
새 탭에서 열기
}
/>
);
}
```
Tones and Variants \[#tones-and-variants]
Neutral \[#neutral]
```tsx
import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon";
import {
ActionablePageBanner,
DismissiblePageBanner,
PageBanner,
PageBannerButton,
} from "seed-design/ui/page-banner";
export default function PageBannerNeutral() {
return (
}
title="미노출"
description="사업자 정보를 등록해주세요."
suffix={
등록하기 }
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
suffix={
등록하기 }
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
);
}
```
Positive \[#positive]
```tsx
import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon";
import {
ActionablePageBanner,
DismissiblePageBanner,
PageBanner,
PageBannerButton,
} from "seed-design/ui/page-banner";
export default function PageBannerPositive() {
return (
}
title="미노출"
description="사업자 정보를 등록해주세요."
suffix={
등록하기 }
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
suffix={
등록하기 }
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
);
}
```
Informative \[#informative]
```tsx
import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon";
import {
ActionablePageBanner,
DismissiblePageBanner,
PageBanner,
PageBannerButton,
} from "seed-design/ui/page-banner";
export default function PageBannerInformative() {
return (
}
title="미노출"
description="사업자 정보를 등록해주세요."
suffix={
등록하기 }
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
suffix={
등록하기 }
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
);
}
```
Warning \[#warning]
```tsx
import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon";
import {
ActionablePageBanner,
DismissiblePageBanner,
PageBanner,
PageBannerButton,
} from "seed-design/ui/page-banner";
export default function PageBannerWarning() {
return (
}
title="미노출"
description="사업자 정보를 등록해주세요."
suffix={
등록하기 }
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
suffix={
등록하기 }
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
);
}
```
Critical \[#critical]
```tsx
import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon";
import {
ActionablePageBanner,
DismissiblePageBanner,
PageBanner,
PageBannerButton,
} from "seed-design/ui/page-banner";
export default function PageBannerCritical() {
return (
}
title="미노출"
description="사업자 정보를 등록해주세요."
suffix={
등록하기 }
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
suffix={
등록하기 }
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
}
title="미노출"
description="사업자 정보를 등록해주세요."
/>
);
}
```
Magic \[#magic]
`tone="magic"`은 `variant="weak"` 조합으로만 사용할 수 있습니다.
```tsx
import { IconSparkle2 } from "@karrotmarket/react-multicolor-icon";
import { VStack } from "@seed-design/react";
import {
ActionablePageBanner,
DismissiblePageBanner,
PageBanner,
PageBannerButton,
} from "seed-design/ui/page-banner";
export default function PageBannerMagic() {
return (
}
title="새로운 기능"
description="마법 같은 소식이 도착했어요!"
suffix={둘러보기 }
/>
}
title="새로운 기능"
description="마법 같은 소식이 도착했어요!"
/>
}
title="새로운 기능"
description="마법 같은 소식이 도착했어요!"
/>
);
}
```
---
file: components/progress-circle.mdx
# Progress Circle
작업이 진행 중임을 알리거나 작업 시간을 시각적으로 나타내는 데 사용됩니다.
## Preview
```tsx
import { ProgressCircle } from "seed-design/ui/progress-circle";
export default function ProgressCirclePreview() {
return ;
}
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:progress-circle
- pnpm: pnpm dlx @seed-design/cli@latest add ui:progress-circle
- yarn: yarn dlx @seed-design/cli@latest add ui:progress-circle
- bun: bun x @seed-design/cli@latest add ui:progress-circle
Props \[#props]
- `value`
- type: `number | undefined`
- description: The current value of the progress. if undefined, it will be indeterminate.
- `minValue`
- type: `number | undefined`
- default: `0`
- description: The minimum value allowed of the progress.
- `maxValue`
- type: `number | undefined`
- default: `100`
- description: The maximum value allowed of the progress.
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
- `tone`
- type: `"inherit" | "neutral" | "brand" | "staticWhite" | undefined`
- default: `"neutral"`
- `size`
- type: `"inherit" | "24" | "40" | undefined`
- default: `40`
Examples \[#examples]
Neutral \[#neutral]
```tsx
import { ProgressCircle } from "seed-design/ui/progress-circle";
export default function ProgressCircleNeutral() {
return ;
}
```
Brand \[#brand]
```tsx
import { ProgressCircle } from "seed-design/ui/progress-circle";
export default function ProgressCircleBrand() {
return ;
}
```
Static White \[#static-white]
```tsx
import { ProgressCircle } from "seed-design/ui/progress-circle";
export default function ProgressCircleStaticWhite() {
return (
);
}
```
Size=40 \[#size40]
```tsx
import { ProgressCircle } from "seed-design/ui/progress-circle";
export default function ProgressCircle40() {
return ;
}
```
Size=24 \[#size24]
```tsx
import { ProgressCircle } from "seed-design/ui/progress-circle";
export default function ProgressCircle24() {
return ;
}
```
Determinate \[#determinate]
```tsx
import { ProgressCircle } from "seed-design/ui/progress-circle";
export default function ProgressCircleDeterminate() {
return ;
}
```
Indeterminate \[#indeterminate]
```tsx
import { ProgressCircle } from "seed-design/ui/progress-circle";
export default function ProgressCirclePreview() {
// if you want to show an indeterminate progress circle, you can pass `undefined` or omit the `value` prop
return ;
}
```
---
file: components/pull-to-refresh.mdx
# Pull To Refresh
사용자가 화면을 아래로 당겨 콘텐츠를 새로고침할 수 있게 해주는 컴포넌트입니다. 모바일 환경에서 최신 콘텐츠를 불러올 때 사용됩니다.
```tsx
import { AppBar, AppScreen } from "@seed-design/stackflow";
import { ActivityComponentType } from "@stackflow/react/future";
import { VStack } from "@seed-design/react";
import {
PullToRefreshContent,
PullToRefreshIndicator,
PullToRefreshRoot,
} from "seed-design/ui/pull-to-refresh";
declare module "@stackflow/config" {
interface Register {
"react/pull-to-refresh/preview": {};
}
}
const PullToRefreshPreview: ActivityComponentType<"react/pull-to-refresh/preview"> = () => {
// AppScreen is imported from @seed-design/stackflow instead of snippet for demo purpose.
// AppScreen snippet is integrating PullToRefresh, so it's not necessary to use it here.
return (
Pull To Refresh
{}}
onPtrRefresh={async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
}}
>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam autem deserunt
reprehenderit ducimus sunt. Quod laudantium excepturi tempora fuga repellendus
accusantium nam maiores? Quas debitis, neque ullam eligendi minus sit?
);
};
export default PullToRefreshPreview;
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:pull-to-refresh
- pnpm: pnpm dlx @seed-design/cli@latest add ui:pull-to-refresh
- yarn: yarn dlx @seed-design/cli@latest add ui:pull-to-refresh
- bun: bun x @seed-design/cli@latest add ui:pull-to-refresh
Props \[#props]
`PullToRefreshRoot` \[#pulltorefreshroot]
- `threshold`
- type: `number | undefined`
- default: `44`
- description: The threshold value to trigger the refresh. (px)
- `displacementMultiplier`
- type: `number | undefined`
- default: `0.5`
- description: The multiplier to calculate displacement from the touch movement.
- `onPtrPullStart`
- type: `((ctx: PullToRefreshContext) => void) | undefined`
- description: Callback when the pull-to-refresh has started to pull.
- `onPtrPullMove`
- type: `((ctx: PullToRefreshContext) => void) | undefined`
- description: Callback when the pull-to-refresh is moving during the pull.
- `onPtrPullEnd`
- type: `((ctx: PullToRefreshContext) => void) | undefined`
- description: Callback when the pull-to-refresh is released. It does not matter if it is ready or not. If you want to handle the refresh, use \`onPtrRefresh\`.
- `onPtrReady`
- type: `(() => void) | undefined`
- description: Callback when the pull-to-refresh is pulled over the threshold.
- `onPtrRefresh`
- type: `(() => Promise) | undefined`
- description: Callback when the pull-to-refresh is released after ready.
- `disabled`
- type: `boolean | undefined`
- default: `false`
- description: Whether to disable the pull-to-refresh.
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`PullToRefreshIndicator` \[#pulltorefreshindicator]
`PullToRefreshContent` \[#pulltorefreshcontent]
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
Examples \[#examples]
PTR in Tabs \[#ptr-in-tabs]
```tsx
import { VStack } from "@seed-design/react";
import { ActivityComponentType } from "@stackflow/react/future";
import { AppBar, AppBarMain } from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
import {
PullToRefreshContent,
PullToRefreshIndicator,
PullToRefreshRoot,
} from "seed-design/ui/pull-to-refresh";
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "seed-design/ui/tabs";
declare module "@stackflow/config" {
interface Register {
"react/pull-to-refresh/tabs": {};
}
}
const PullToRefreshTabs: ActivityComponentType<"react/pull-to-refresh/tabs"> = () => {
return (
Pull To Refresh
Tab 1
Tab 2
{}}
onPtrRefresh={async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
}}
>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam autem deserunt
reprehenderit ducimus sunt. Quod laudantium excepturi tempora fuga repellendus
accusantium nam maiores? Quas debitis, neque ullam eligendi minus sit?
PTR is not available in this tab.
);
};
export default PullToRefreshTabs;
```
Disabled \[#disabled]
`disabled` 속성을 사용하여 PTR를 비활성화할 수 있습니다.
```tsx
import { Box, HStack, Text } from "@seed-design/react";
import { useState } from "react";
import {
PullToRefreshContent,
PullToRefreshIndicator,
PullToRefreshRoot,
} from "seed-design/ui/pull-to-refresh";
import { Switch } from "seed-design/ui/switch";
const PullToRefreshDisabled = () => {
const [disabled, setDisabled] = useState(false);
return (
{}}
onPtrRefresh={async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
}}
disabled={disabled}
>
Disabled
);
};
export default PullToRefreshDisabled;
```
Prevent Pull \[#prevent-pull]
`PullToRefresh.preventPull` 속성을 사용하여 특정 영역에서 PTR 동작을 방지할 수 있습니다. `user-select: auto;`등을 통해 텍스트 선택이 가능한 영역에서 주로 사용됩니다.
```tsx
import { AppBar, AppScreen } from "@seed-design/stackflow";
import { ActivityComponentType } from "@stackflow/react/future";
import { VStack, PullToRefresh, Box } from "@seed-design/react";
import {
PullToRefreshContent,
PullToRefreshIndicator,
PullToRefreshRoot,
} from "seed-design/ui/pull-to-refresh";
declare module "@stackflow/config" {
interface Register {
"react/pull-to-refresh/prevent-pull": {};
}
}
const PullToRefreshPreventPull: ActivityComponentType<
"react/pull-to-refresh/prevent-pull"
> = () => {
// AppScreen is imported from @seed-design/stackflow instead of snippet for demo purpose.
// AppScreen snippet is integrating PullToRefresh, so it's not necessary to use it here.
return (
Pull To Refresh
{}}
onPtrRefresh={async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
}}
>
이 영역은 당겨서 새로고침이 가능합니다. Amet in laborum proident fugiat mollit quis
aute mollit esse nostrud. Excepteur ea proident ipsum duis. Nulla Lorem pariatur
exercitation velit anim.
이 영역은 당겨서 새로고침이 불가능합니다. Aliquip ad amet eu dolore id enim
excepteur laboris officia anim in. Irure irure nulla sit eiusmod aliqua sint
excepteur amet laboris.
이 영역은 당겨서 새로고침이 가능합니다. Amet in laborum proident fugiat mollit quis
aute mollit esse nostrud. Excepteur ea proident ipsum duis. Nulla Lorem pariatur
exercitation velit anim.
이 영역은 당겨서 새로고침이 불가능합니다. Aliquip ad amet eu dolore id enim
excepteur laboris officia anim in. Irure irure nulla sit eiusmod aliqua sint
excepteur amet laboris.
);
};
export default PullToRefreshPreventPull;
```
---
file: components/radio-group.mdx
# Radio Group
여러 옵션 중 하나를 선택할 수 있도록 할 때 사용하는 컴포넌트입니다.
## Preview
```tsx
import { VStack } from "@seed-design/react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
export default function RadioGroupPreview() {
return (
);
}
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:radio-group
- pnpm: pnpm dlx @seed-design/cli@latest add ui:radio-group
- yarn: yarn dlx @seed-design/cli@latest add ui:radio-group
- bun: bun x @seed-design/cli@latest add ui:radio-group
Props \[#props]
`RadioGroup` \[#radiogroup]
- `label`
- type: `React.ReactNode`
- `labelWeight`
- type: `"medium" | "bold" | undefined`
- default: `"medium"`
- `indicator`
- type: `React.ReactNode`
- `showRequiredIndicator`
- type: `boolean | undefined`
- `description`
- type: `React.ReactNode`
- `errorMessage`
- type: `React.ReactNode`
- `disabled`
- type: `boolean | undefined`
- default: `false`
- `invalid`
- type: `boolean | undefined`
- default: `false`
- `name`
- type: `string | undefined`
- `form`
- type: `string | undefined`
- `value`
- type: `string | undefined`
- `defaultValue`
- type: `string | undefined`
- `onValueChange`
- type: `((value: string) => void) | undefined`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`RadioGroupItem` \[#radiogroupitem]
- `label`
- type: `React.ReactNode`
- `inputProps`
- type: `React.InputHTMLAttributes | undefined`
- `rootRef`
- type: `React.Ref | undefined`
- `weight`
- type: `"regular" | "bold" | undefined`
- default: `"regular"`
- `size`
- type: `"large" | "medium" | undefined`
- default: `"medium"`
- `tone`
- type: `"neutral" | "brand" | undefined`
- default: `"brand"`
- `value`
- type: `string`
- required: `true`
- `disabled`
- type: `boolean | undefined`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`Radiomark` \[#radiomark]
- `tone`
- type: `"neutral" | "brand" | undefined`
- default: `"brand"`
- `size`
- type: `"large" | "medium" | undefined`
- default: `"medium"`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
Examples \[#examples]
Sizes \[#sizes]
```tsx
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
import { VStack } from "@seed-design/react";
export default function RadioGroupSize() {
return (
);
}
```
Tones \[#tones]
Brand \[#brand]
```tsx
import { VStack } from "@seed-design/react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
export default function RadioGroupBrand() {
return (
);
}
```
Neutral \[#neutral]
```tsx
import { VStack } from "@seed-design/react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
export default function RadioGroupNeutral() {
return (
);
}
```
Weights \[#weights]
```tsx
import { VStack } from "@seed-design/react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
export default function RadioGroupWeights() {
return (
);
}
```
Long Label \[#long-label]
```tsx
import { VStack } from "@seed-design/react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
export default function RadioGroupLongLabel() {
return (
);
}
```
Disabled \[#disabled]
```tsx
import { VStack } from "@seed-design/react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
export default function RadioGroupDisabled() {
return (
);
}
```
Listening to Value Changes \[#listening-to-value-changes]
`RadioGroup`의 `onValueChange`를 사용하여 라디오 버튼의 선택 값 변경을 감지할 수 있습니다.
이벤트를 활용해야 하는 경우 `RadioGroupItem`의 `inputProps`를 통해 내부 ` ` 요소에 직접 이벤트 핸들러를 추가할 수 있습니다.
```tsx
import { VStack, Text } from "@seed-design/react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
import { useState } from "react";
export default function RadioGroupValueChanges() {
const [count, setCount] = useState(0);
const [lastValue, setLastValue] = useState(null);
return (
{
setCount((prev) => prev + 1);
setLastValue(value);
}}
>
onValueChange called: {count} times, last value: {lastValue ?? "-"}
);
}
```
Use Cases \[#use-cases]
React Hook Form \[#react-hook-form]
```tsx
import { HStack, VStack } from "@seed-design/react";
import { useCallback, type FormEvent } from "react";
import { useController, useForm } from "react-hook-form";
import { ActionButton } from "seed-design/ui/action-button";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
const POSSIBLE_COLORS = ["red", "blue", "green"] as const;
interface FormValues {
color: (typeof POSSIBLE_COLORS)[number];
}
export default function RadioGroupReactHookForm() {
const { handleSubmit, reset, setValue, control } = useForm({
defaultValues: {
color: "blue",
},
});
const { field } = useController({ name: "color", control });
const onValid = useCallback((data: FormValues) => {
window.alert(JSON.stringify(data, null, 2));
}, []);
const onReset = useCallback(
(event: FormEvent) => {
event.preventDefault();
reset();
},
[reset],
);
return (
setValue("color", value as FormValues["color"])}
aria-label="Color selection"
>
{POSSIBLE_COLORS.map((color) => (
))}
Submit
Reset
);
}
```
Using `Radiomark` \[#using-radiomark]
`Radiomark`는 독립적인 라디오 마크 컴포넌트로, Radio Group Primitive 컴포넌트와 조합하여 커스텀 레이아웃을 위해 사용할 수 있습니다.
```tsx
import { HStack, Text, VStack } from "@seed-design/react";
import { Radiomark } from "seed-design/ui/radio-group";
import { RadioGroup } from "@seed-design/react/primitive";
function CustomRadioGroupItem({ children, ...props }: RadioGroup.ItemProps) {
return (
{children}
);
}
export default function RadioGroupRadiomark() {
return (
regular
medium
bold
);
}
```
RadioGroupField Integration \[#radiogroupfield-integration]
`label`, `description`, `errorMessage` 등의 RadioGroupField 관련 prop을 사용할 수 있습니다.
```tsx
import { ActionButton, HStack, VStack } from "@seed-design/react";
import { useState } from "react";
import { RadioGroup, RadioGroupItem } from "seed-design/ui/radio-group";
export default function RadioGroupRadioGroupField() {
const [firstErrorMessage, setFirstErrorMessage] = useState();
const [secondErrorMessage, setSecondErrorMessage] = useState();
const handleFirstSubmit = (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const contact = formData.get("contact");
if (contact === "email") {
setFirstErrorMessage("이메일은 선택할 수 없습니다.");
return;
}
setFirstErrorMessage(undefined);
alert(JSON.stringify({ contact }, null, 2));
};
const handleSecondSubmit = (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const option = formData.get("option");
if (option === "option1") {
setSecondErrorMessage("옵션 1은 선택할 수 없습니다.");
return;
}
setSecondErrorMessage(undefined);
alert(JSON.stringify({ option }, null, 2));
};
return (
);
}
```
---
file: components/reaction-button.mdx
# Reaction Button
사용자가 콘텐츠에 대한 반응을 표현할 수 있게 해주는 컴포넌트입니다. 좋아요, 관심있어요 등의 감정적 피드백을 간편하게 제공할 때 사용됩니다.
## Preview
```tsx
import { IconFaceSmileCircleFill } from "@karrotmarket/react-monochrome-icon";
import { Count, PrefixIcon } from "@seed-design/react";
import { ReactionButton } from "seed-design/ui/reaction-button";
export default function ReactionButtonPreview() {
return (
} />
도움돼요
1
);
}
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:reaction-button
- pnpm: pnpm dlx @seed-design/cli@latest add ui:reaction-button
- yarn: yarn dlx @seed-design/cli@latest add ui:reaction-button
- bun: bun x @seed-design/cli@latest add ui:reaction-button
Props \[#props]
- `size`
- type: `"xsmall" | "small" | undefined`
- default: `"small"`
- `loading`
- type: `boolean | undefined`
- default: `false`
- description: 버튼에 등록된 비동기 작업이 진행 중임을 사용자에게 알립니다.
- `disabled`
- type: `boolean | undefined`
- default: `false`
- description: 버튼의 비활성화 여부를 나타냅니다.
- `pressed`
- type: `boolean | undefined`
- `defaultPressed`
- type: `boolean | undefined`
- `onPressedChange`
- type: `((pressed: boolean) => void) | undefined`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
Examples \[#examples]
Small \[#small]
```tsx
import { IconFaceSmileCircleFill } from "@karrotmarket/react-monochrome-icon";
import { Count, PrefixIcon } from "@seed-design/react";
import { ReactionButton } from "seed-design/ui/reaction-button";
export default function ReactionButtonSmall() {
return (
} />
도움돼요
1
);
}
```
Xsmall \[#xsmall]
```tsx
import { IconFaceSmileCircleFill } from "@karrotmarket/react-monochrome-icon";
import { Count, PrefixIcon } from "@seed-design/react";
import { ReactionButton } from "seed-design/ui/reaction-button";
export default function ReactionButtonXsmall() {
return (
} />
도움돼요
1
);
}
```
Disabled \[#disabled]
```tsx
import { IconFaceSmileCircleFill } from "@karrotmarket/react-monochrome-icon";
import { PrefixIcon } from "@seed-design/react";
import { ReactionButton } from "seed-design/ui/reaction-button";
export default function ReactionButtonDisabled() {
return (
} />
비활성
);
}
```
Loading \[#loading]
```tsx
import { IconFaceSmileCircleFill } from "@karrotmarket/react-monochrome-icon";
import { PrefixIcon } from "@seed-design/react";
import { useState } from "react";
import { ReactionButton } from "seed-design/ui/reaction-button";
export default function ReactionButtonLoading() {
const [pressed, setPressed] = useState(false);
const [loading, setLoading] = useState(false);
function handleToggle() {
setLoading(true);
setTimeout(() => {
setLoading(false);
setPressed((prev) => !prev);
}, 2000);
}
// 이벤트 핸들링이 필요할 수 있으므로 loading은 disabled를 포함하지 않습니다. 이벤트 발생을 원하지 않는 경우, disabled 속성을 추가해주세요.
return (
} />
시간이 걸리는 토글
);
}
```
---
file: components/result-section.mdx
# Result Section
데이터 로딩 결과, 사용자의 액션 완료 여부 등 사용자에 액션에 대한 결과를 제공하는 템플릿입니다. 주로 전체 화면이나 특정 영역을 차지하여 다음 액션을 유도하거나 현재 상황을 안내하는 역할을 합니다.
## Preview
```tsx
import { IconDiamond } from "@karrotmarket/react-multicolor-icon";
import { VStack, Icon, Box } from "@seed-design/react";
import { ResultSection } from "seed-design/ui/result-section";
export default function ResultSectionPreview() {
return (
} size="x10" />
}
title="결과 타이틀"
description="부가 설명을 적어주세요"
primaryActionProps={{
children: "Primary Action",
onClick: () => window.alert("Primary Action Clicked"),
}}
secondaryActionProps={{
children: "Secondary Action",
onClick: () => window.alert("Secondary Action Clicked"),
}}
/>
);
}
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:result-section
- pnpm: pnpm dlx @seed-design/cli@latest add ui:result-section
- yarn: yarn dlx @seed-design/cli@latest add ui:result-section
- bun: bun x @seed-design/cli@latest add ui:result-section
Props \[#props]
- `size`
- type: `"large" | "medium" | undefined`
- default: `"large"`
- `asset`
- type: `React.ReactNode`
- `title`
- type: `React.ReactNode`
- `description`
- type: `React.ReactNode`
- `primaryActionProps`
- type: `ActionButtonProps | undefined`
- `secondaryActionProps`
- type: `ActionButtonProps | undefined`
Examples \[#examples]
Sizes \[#sizes]
Large \[#large]
```tsx
import { VStack } from "@seed-design/react";
import { ResultSection } from "seed-design/ui/result-section";
export default function ResultSectionLarge() {
return (
);
}
```
Medium \[#medium]
```tsx
import { VStack } from "@seed-design/react";
import { ResultSection } from "seed-design/ui/result-section";
export default function ResultSectionMedium() {
return (
);
}
```
With CTA & Progress Circle \[#with-cta--progress-circle]
```tsx
import { ActionButton } from "seed-design/ui/action-button";
import { AppBar, AppBarMain } from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
import { ProgressCircle } from "seed-design/ui/progress-circle";
import { ResultSection } from "seed-design/ui/result-section";
import { IconExclamationmarkCircleFill } from "@karrotmarket/react-monochrome-icon";
import { Box, Flex, Icon, VStack } from "@seed-design/react";
import { ActivityComponentType } from "@stackflow/react/future";
import { useEffect, useState, type ComponentProps } from "react";
declare module "@stackflow/config" {
interface Register {
"react/result-section/cta-progress-circle": {};
}
}
type RefundStatus = "in-progress" | "failed";
const resultSectionProperties = {
"in-progress": {
title: "환불을 요청하고 있어요",
description: "잠시만 기다려주세요",
asset: (
),
},
failed: {
title: "다시 시도해주세요",
description: "환불 요청에 실패했어요",
asset: (
} size="x10" color="fg.critical" />
),
},
} satisfies Record>;
const ResultSectionStackflow: ActivityComponentType<
"react/result-section/cta-progress-circle"
> = () => {
const [refundStatus, setRefundStatus] = useState("in-progress");
useEffect(() => {
const timer = setTimeout(() => setRefundStatus("failed"), 3000);
return () => clearTimeout(timer);
}, []);
return (
{
setRefundStatus("in-progress");
setTimeout(() => setRefundStatus("failed"), 3000);
}}
>
다시 시도
);
};
export default ResultSectionStackflow;
```
Success Example \[#success-example]
Result Section 에셋 영역에서 다크 모드와 라이트 모드에서 서로 다른 Lottie 애니메이션을 사용하는 예시입니다.
[클라이언트에서 현재 테마 정보 감지하는 방법](/react/getting-started/styling/theming#클라이언트에서-현재-테마-정보-감지하기)을 참고하여 테마에 맞는 Lottie 애니메이션을 Result Section에 적용할 수 있습니다.
```tsx
import dynamic from "next/dynamic";
import { VStack, Box } from "@seed-design/react";
import { ResultSection } from "seed-design/ui/result-section";
import { useTheme } from "@/hooks/useTheme";
const Player = dynamic(() => import("@lottiefiles/react-lottie-player").then((mod) => mod.Player), {
ssr: false,
});
const LOTTIE_URLS = {
light:
"https://asset-town.krrt.io/production/motion/bd9f3c71-5b81-40b0-8eea-eeebd668edae/c17fa891bb007b9e4e6b281e483b5491cb905703.json",
dark: "https://asset-town.krrt.io/production/motion/19bf4654-5286-4def-a651-c674a20ce1ee/89c9e404edc356cf143dab80b627fde01ed8a8fb.json",
};
export default function ResultSectionSuccessWithLottie() {
const { userColorScheme } = useTheme();
const lottieUrl = LOTTIE_URLS[userColorScheme];
return (
}
title="성공했어요"
description="요청이 성공적으로 완료되었습니다"
primaryActionProps={{
children: "확인",
onClick: () => window.alert("확인 클릭"),
}}
/>
);
}
```
---
file: components/scroll-fog.mdx
# Scroll Fog
스크롤 가능한 영역에서 사용자에게 추가 콘텐츠가 있음을 시각적으로 알려주는 힌트 역할을 합니다.
## Preview
```tsx
import { Box, ScrollFog, VStack } from "@seed-design/react";
export default function ScrollFogPreview() {
return (
{Array.from({ length: 20 }, (_, i) => (
{i + 1}
))}
);
}
```
해당 Scroll Fog 컴포넌트는 [Chakra ScrollArea](https://chakra-ui.com/docs/components/scroll-area),
[Radix ScrollArea](https://www.radix-ui.com/primitives/docs/components/scroll-area),
[Base UI ScrollArea](https://base-ui.com/react/components/scroll-area) 보다는 더 간단한 스크롤 힌트만을 위한 컴포넌트입니다.
이후 스크롤바에 대한 기능이 추가적으로 필요하다면 확장될 수 있습니다.
Usage \[#usage]
```tsx
import { ScrollFog } from "@seed-design/react";
```
```tsx
{/* 스크롤 가능한 콘텐츠 */}
Content
```
Props \[#props]
Scroll Fog \[#scroll-fog]
- `placement`
- type: `ScrollPlacement[] | undefined`
- default: `["top", "bottom"]`
- description: Placement of fog effect
- `size`
- type: `number | undefined`
- default: `20`
- description: Size of the fog effect in pixels
- `sizes`
- type: `SizesConfig | undefined`
- description: Size of the fog effect for each direction in pixels
- `hideScrollBar`
- type: `boolean | undefined`
- default: `false`
Examples \[#examples]
Padding \[#padding]
ScrollFog는 항상 fog 효과를 표시하므로, 지정된 방향에 충분한 padding이 필요합니다.
- **최소 padding**: 20px을 유지해야 합니다.
- **화면 전체 차지하는 세로 스크롤**: 하단 80px, 상단 20px padding 권장
- **가로 스크롤**: 20px 좌우 padding 권장
```tsx
// 상하 스크롤 - 상하 padding 필요
Content
// 좌우 스크롤 - 좌우 padding 필요
Content
```
- [Bottom Sheet - 하단 80px, 상단 20px padding](/react/components/bottom-sheet#with-scroll-fog)
- [Chip Tabs - 20px 좌우 padding](/react/components/chip-tabs#with-scroll-fog)
Horizontal Scroll \[#horizontal-scroll]
가로 스크롤이 필요한 경우 `placement={["left", "right"]}`를 사용하여 좌우 fog 효과를 적용할 수 있습니다.
좌우 20px의 padding을 제공하여 컨텐츠가 잘리지 않도록 해야 합니다.
```tsx
import { HStack, ScrollFog } from "@seed-design/react";
export default function ScrollFogHorizontal() {
return (
{Array.from({ length: 15 }, (_, i) => (
{i + 1}
))}
);
}
```
Size \[#size]
`size` 속성을 이용해 스크롤 효과의 크기를 조절할 수 있습니다. `sizes` 속성을 이용해 각 방향별 크기를 조절할 수 있습니다.
```tsx
import { ScrollFog } from "@seed-design/react";
export default function ScrollFogSize() {
return (
{Array.from({ length: 100 }, (_, i) => (
{i + 1}
))}
);
}
```
```tsx
import { ScrollFog } from "@seed-design/react";
export default function ScrollFogSize() {
return (
{Array.from({ length: 100 }, (_, i) => (
{i + 1}
))}
);
}
```
All Directions \[#all-directions]
2D 스크롤이 필요한 경우 `placement={["top", "bottom", "left", "right"]}`를 사용하여 모든 방향에 fog 효과를 적용할 수 있습니다.
```tsx
import { ScrollFog } from "@seed-design/react";
export default function ScrollFogAllDirections() {
return (
{Array.from({ length: 100 }, (_, i) => (
{i + 1}
))}
);
}
```
Hide Scrollbar \[#hide-scrollbar]
`hideScrollBar` 속성을 이용해 스크롤바를 숨길 수 있습니다.
```tsx
import { ScrollFog } from "@seed-design/react";
export default function ScrollFogHideScrollbar() {
return (
{Array.from({ length: 100 }, (_, i) => (
{i + 1}
))}
);
}
```
---
file: components/segmented-control.mdx
# Segmented Control
여러 옵션 중 하나를 선택하여 관련 콘텐츠를 즉시 필터링하거나 전환할 때 사용하는 컴포넌트입니다.
## Preview
```tsx
import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control";
export default function SegmentedControlPreview() {
return (
Hot
New
);
}
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:segmented-control
- pnpm: pnpm dlx @seed-design/cli@latest add ui:segmented-control
- yarn: yarn dlx @seed-design/cli@latest add ui:segmented-control
- bun: bun x @seed-design/cli@latest add ui:segmented-control
Props \[#props]
`SegmentedControl` \[#segmentedcontrol]
`value`와 `defaultValue` 중 적어도 하나를 제공해야 합니다.
- `disabled`
- type: `boolean | undefined`
- `name`
- type: `string | undefined`
- `form`
- type: `string | undefined`
- `value`
- type: `string | undefined`
- `defaultValue`
- type: `string | undefined`
- `onValueChange`
- type: `((value: string) => void) | undefined`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`SegmentedControlItem` \[#segmentedcontrolitem]
- `inputProps`
- type: `React.InputHTMLAttributes | undefined`
- `rootRef`
- type: `React.Ref | undefined`
- `notification`
- type: `boolean | undefined`
- `value`
- type: `string`
- required: `true`
- `disabled`
- type: `boolean | undefined`
- `invalid`
- type: `boolean | undefined`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
Examples \[#examples]
Disabled \[#disabled]
```tsx
import { VStack } from "@seed-design/react";
import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control";
export default function SegmentedControlPreview() {
return (
Hot
New
Marinara
Alfredo
Pesto
Carbonara
Bolognese
);
}
```
Notification \[#notification]
```tsx
import { ActionButton, VStack } from "@seed-design/react";
import { useState } from "react";
import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control";
export default function SegmentedControlNotification() {
const [sortBy, setSortBy] = useState("monthly");
const [hasSeenAnnual, setHasSeenAnnual] = useState(false);
return (
{
setSortBy(value);
if (value === "annual") setHasSeenAnnual(true);
}}
>
Monthly
Annual
Enterprise Custom
setHasSeenAnnual(false)}
>
Reset Notification
);
}
```
Long Label \[#long-label]
```tsx
import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control";
export default function SegmentedControlLongLabel() {
return (
가격 높은 순
할인율 높은 순
인기 많은 순
);
}
```
Fixed Width \[#fixed-width]
`SegmentedControl`의 `style` prop에 `width`를 제공해서 직접 너비를 설정할 수 있습니다.
```tsx
import { VStack } from "@seed-design/react";
import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control";
export default function SegmentedControlFixedWidth() {
return (
New
Hot
One Way Trip
Round Trip
Multi-City Journey
);
}
```
Listening to Value Changes \[#listening-to-value-changes]
`SegmentedControl`의 `onValueChange` prop을 사용하여 선택 값 변경을 감지할 수 있습니다.
이벤트를 활용해야 하는 경우 `SegmentedControlItem`의 `inputProps`를 통해 내부 ` ` 요소에 직접 이벤트 핸들러를 추가할 수 있습니다.
```tsx
import { VStack, Text } from "@seed-design/react";
import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control";
import { useState } from "react";
export default function SegmentedControlValueChanges() {
const [count, setCount] = useState(0);
const [lastValue, setLastValue] = useState(null);
return (
{
setCount((prev) => prev + 1);
setLastValue(value);
}}
>
Hot
New
onValueChange called: {count} times, last value: {lastValue ?? "-"}
);
}
```
---
file: components/select-box.mdx
# Select Box
명확한 테두리를 가진 컨테이너를 활용하여, 정의된 목록 중 하나 이상의 옵션을 선택하는 UI 요소입니다.
## Preview
```tsx
import { HStack } from "@seed-design/react";
import {
CheckSelectBox,
CheckSelectBoxCheckmark,
CheckSelectBoxGroup,
RadioSelectBoxItem,
RadioSelectBoxRadiomark,
RadioSelectBoxRoot,
} from "seed-design/ui/select-box";
export default function SelectBoxPreview() {
return (
} />
}
/>
} />
} />
}
/>
} />
);
}
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:select-box
- pnpm: pnpm dlx @seed-design/cli@latest add ui:select-box
- yarn: yarn dlx @seed-design/cli@latest add ui:select-box
- bun: bun x @seed-design/cli@latest add ui:select-box
Props \[#props]
Check Select Box \[#check-select-box]
`CheckSelectBoxGroup` \[#checkselectboxgroup]
- `label`
- type: `React.ReactNode`
- `labelWeight`
- type: `"medium" | "bold" | undefined`
- default: `"medium"`
- `indicator`
- type: `React.ReactNode`
- `showRequiredIndicator`
- type: `boolean | undefined`
- `description`
- type: `React.ReactNode`
- `errorMessage`
- type: `React.ReactNode`
- `columns`
- type: `number | undefined`
- default: `1`
- description: Number of columns in the grid layout.
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`CheckSelectBox` \[#checkselectbox]
- `label`
- type: `React.ReactNode`
- required: `true`
- `description`
- type: `React.ReactNode`
- `prefixIcon`
- type: `React.ReactNode`
- `suffix`
- type: `React.ReactNode`
- `footer`
- type: `React.ReactNode`
- `inputProps`
- type: `React.InputHTMLAttributes | undefined`
- `rootRef`
- type: `React.Ref | undefined`
- `layout`
- type: `"horizontal" | "vertical" | undefined`
- default: `"horizontal"`
- `footerVisibility`
- type: `"when-selected" | "when-not-selected" | "always" | undefined`
- default: `"when-selected"`
- description: Controls when the footer is visible.
- `disabled`
- type: `boolean | undefined`
- `invalid`
- type: `boolean | undefined`
- `required`
- type: `boolean | undefined`
- `checked`
- type: `boolean | undefined`
- `defaultChecked`
- type: `boolean | undefined`
- `onCheckedChange`
- type: `((checked: boolean) => void) | undefined`
- `indeterminate`
- type: `boolean | undefined`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`CheckSelectBoxCheckmark` \[#checkselectboxcheckmark]
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
Radio Select Box \[#radio-select-box]
`RadioSelectBoxRoot` \[#radioselectboxroot]
- `label`
- type: `React.ReactNode`
- `labelWeight`
- type: `"medium" | "bold" | undefined`
- default: `"medium"`
- `indicator`
- type: `React.ReactNode`
- `showRequiredIndicator`
- type: `boolean | undefined`
- `description`
- type: `React.ReactNode`
- `errorMessage`
- type: `React.ReactNode`
- `columns`
- type: `number | undefined`
- default: `1`
- description: Number of columns in the grid layout.
- `disabled`
- type: `boolean | undefined`
- default: `false`
- `invalid`
- type: `boolean | undefined`
- default: `false`
- `name`
- type: `string | undefined`
- `form`
- type: `string | undefined`
- `value`
- type: `string | undefined`
- `defaultValue`
- type: `string | undefined`
- `onValueChange`
- type: `((value: string) => void) | undefined`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`RadioSelectBoxItem` \[#radioselectboxitem]
- `label`
- type: `React.ReactNode`
- required: `true`
- `description`
- type: `React.ReactNode`
- `prefixIcon`
- type: `React.ReactNode`
- `suffix`
- type: `React.ReactNode`
- `footer`
- type: `React.ReactNode`
- `inputProps`
- type: `React.InputHTMLAttributes | undefined`
- `rootRef`
- type: `React.Ref | undefined`
- `layout`
- type: `"horizontal" | "vertical" | undefined`
- default: `"horizontal"`
- `value`
- type: `string`
- required: `true`
- `footerVisibility`
- type: `"when-selected" | "when-not-selected" | "always" | undefined`
- default: `"when-selected"`
- description: Controls when the footer is visible.
- `disabled`
- type: `boolean | undefined`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`RadioSelectBoxRadiomark` \[#radioselectboxradiomark]
- `tone`
- type: `"neutral" | "brand" | undefined`
- default: `"brand"`
- `size`
- type: `"large" | "medium" | undefined`
- default: `"medium"`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
Examples \[#examples]
React Hook Form \[#react-hook-form]
```tsx
import { HStack, VStack } from "@seed-design/react";
import { useCallback, type FormEvent } from "react";
import { useController, useForm, type Control } from "react-hook-form";
import { ActionButton } from "seed-design/ui/action-button";
import {
CheckSelectBox,
CheckSelectBoxCheckmark,
CheckSelectBoxGroup,
RadioSelectBoxItem,
RadioSelectBoxRadiomark,
RadioSelectBoxRoot,
} from "seed-design/ui/select-box";
const POSSIBLE_FRUIT_VALUES = ["apple", "melon", "mango"] as const;
type CheckFormValues = Record<(typeof POSSIBLE_FRUIT_VALUES)[number], boolean>;
interface RadioFormValues {
fruit: (typeof POSSIBLE_FRUIT_VALUES)[number];
}
export default function SelectBoxReactHookForm() {
// CheckSelectBox Form
const checkForm = useForm({
defaultValues: { apple: false, melon: true, mango: false },
});
// RadioSelectBox Form
const radioForm = useForm({
defaultValues: { fruit: "melon" },
});
const { field: radioField } = useController({ name: "fruit", control: radioForm.control });
const onCheckValid = useCallback((data: CheckFormValues) => {
window.alert(`CheckSelectBox:\n${JSON.stringify(data, null, 2)}`);
}, []);
const onRadioValid = useCallback((data: RadioFormValues) => {
window.alert(`RadioSelectBox:\n${JSON.stringify(data, null, 2)}`);
}, []);
const onCheckReset = useCallback(
(event: FormEvent) => {
event.preventDefault();
checkForm.reset();
},
[checkForm],
);
const onRadioReset = useCallback(
(event: FormEvent) => {
event.preventDefault();
radioForm.reset();
},
[radioForm],
);
return (
{POSSIBLE_FRUIT_VALUES.map((name) => (
))}
초기화
제출
{POSSIBLE_FRUIT_VALUES.map((value) => (
}
/>
))}
초기화
제출
);
}
interface CheckSelectBoxItemProps {
name: keyof CheckFormValues;
control: Control;
}
function CheckSelectBoxItem({ name, control }: CheckSelectBoxItemProps) {
const {
field: { value, ...restProps },
fieldState: { invalid },
} = useController({ name, control });
return (
}
/>
);
}
```
Customizing Label \[#customizing-label]
```tsx
import { Badge, HStack } from "@seed-design/react";
import {
CheckSelectBox,
CheckSelectBoxCheckmark,
CheckSelectBoxGroup,
RadioSelectBoxItem,
RadioSelectBoxRadiomark,
RadioSelectBoxRoot,
} from "seed-design/ui/select-box";
export default function SelectBoxCustomizingLabel() {
return (
} />
Melon
New
>
}
description="Elit cupidatat dolore fugiat enim veniam culpa."
suffix={ }
/>
}
/>
} />
Melon
New
>
}
description="Elit cupidatat dolore fugiat enim veniam culpa."
suffix={ }
/>
}
/>
);
}
```
Listening to Value Changes \[#listening-to-value-changes]
`CheckSelectBox`는 `onCheckedChange`를 사용하여 체크박스의 선택 상태 변경을 감지할 수 있습니다.
`RadioSelectBoxRoot`는 `onValueChange`를 사용하여 라디오 버튼의 선택 값 변경을 감지할 수 있습니다.
```tsx
import { HStack, Text, VStack } from "@seed-design/react";
import {
CheckSelectBox,
CheckSelectBoxCheckmark,
CheckSelectBoxGroup,
RadioSelectBoxItem,
RadioSelectBoxRadiomark,
RadioSelectBoxRoot,
} from "seed-design/ui/select-box";
import { useState } from "react";
export default function SelectBoxValueChanges() {
const [checkCount, setCheckCount] = useState(0);
const [checkLastValue, setCheckLastValue] = useState(null);
const [radioCount, setRadioCount] = useState(0);
const [radioLastValue, setRadioLastValue] = useState(null);
return (
}
onCheckedChange={(checked) => {
setCheckCount((prev) => prev + 1);
setCheckLastValue(checked);
}}
/>
onCheckedChange called: {checkCount} times, last value: {`${checkLastValue ?? "-"}`}
{
setRadioCount((prev) => prev + 1);
setRadioLastValue(value);
}}
>
} />
} />
onValueChange called: {radioCount} times, last value: {radioLastValue ?? "-"}
);
}
```
Grid Layout (Columns) \[#grid-layout-columns]
`columns` prop을 사용하여 여러 열로 배치할 수 있습니다. `columns`가 1보다 크면 하위 요소의 `layout`이 자동으로 `"vertical"`로 설정됩니다.
필요한 경우 `layout` prop을 직접 설정하여 개별 항목의 레이아웃을 오버라이드할 수 있습니다.
```tsx
import { IconDiamond, IconIcecreamcone } from "@karrotmarket/react-multicolor-icon";
import { VStack } from "@seed-design/react";
import {
CheckSelectBox,
CheckSelectBoxCheckmark,
CheckSelectBoxGroup,
RadioSelectBoxItem,
RadioSelectBoxRadiomark,
RadioSelectBoxRoot,
} from "seed-design/ui/select-box";
export default function SelectBoxColumns() {
return (
}
label="옵션 1"
description="layout=vertical"
suffix={ }
/>
}
label="옵션 2"
description="layout=vertical"
suffix={ }
/>
}
defaultChecked
layout="horizontal"
label="layout=horizontal"
description="layout을 horizontal로 오버라이드"
suffix={ }
/>
}
label="옵션 4"
description="layout=vertical"
suffix={ }
/>
}
label="옵션 1"
suffix={ }
/>
}
label="옵션 2"
suffix={ }
/>
}
label="layout=horizontal"
description="layout을 horizontal로 오버라이드"
layout="horizontal"
suffix={ }
/>
}
label="옵션 4"
suffix={ }
/>
}
label="옵션 5"
suffix={ }
/>
}
label="옵션 6"
suffix={ }
/>
);
}
```
With Suffix \[#with-suffix]
`suffix` prop을 사용하여 체크마크, 라디오 마크, 또는 커스텀 요소를 표시할 수 있습니다. `CheckSelectBoxCheckmark`와 `RadioSelectBoxRadiomark`를 사용하거나, 아이콘이나 텍스트 등 자유로운 요소를 전달할 수 있습니다.
```tsx
import { IconPersonCircleLine } from "@karrotmarket/react-monochrome-icon";
import { Text, HStack, Box } from "@seed-design/react";
import {
CheckSelectBox,
CheckSelectBoxCheckmark,
CheckSelectBoxGroup,
RadioSelectBoxItem,
RadioSelectBoxRadiomark,
RadioSelectBoxRoot,
} from "seed-design/ui/select-box";
export default function SelectBoxWithSuffix() {
return (
} />
+1,000원
}
/>
}
/>
}
/>
+1,000원
}
/>
}
/>
);
}
```
Collapsible Footer \[#collapsible-footer]
`footer` prop으로 추가 콘텐츠를 표시할 수 있습니다. `footerVisibility` prop으로 footer의 표시 조건을 제어할 수 있습니다.
- `"when-selected"` (기본값): 항목이 선택되었을 때만 표시
- `"when-not-selected"`: 항목이 선택되지 않았을 때만 표시
- `"always"`: 항상 표시
```tsx
import { Box, HStack, Text } from "@seed-design/react";
import {
CheckSelectBox,
CheckSelectBoxCheckmark,
CheckSelectBoxGroup,
RadioSelectBoxItem,
RadioSelectBoxRadiomark,
RadioSelectBoxRoot,
} from "seed-design/ui/select-box";
export default function SelectBoxCollapsibleFooter() {
return (
}
footer={
선택되었을 때만 보입니다.
}
/>
}
footerVisibility="always"
footer={
항상 보입니다.
}
/>
}
footerVisibility="when-not-selected"
footer={
선택되지 않았을 때만 보입니다.
}
/>
}
footer={
선택되었을 때만 보입니다.
}
/>
}
footerVisibility="always"
footer={
항상 보입니다.
}
/>
}
footerVisibility="when-not-selected"
footer={
선택되지 않았을 때만 보입니다.
}
/>
);
}
```
Fieldset/RadioGroupField Integration \[#fieldsetradiogroupfield-integration]
Fieldset/RadioGroupField 관련 prop을 사용할 수 있습니다.
- `label` 및 `labelWeight`
- `indicator` 및 `showRequiredIndicator`
- `description` 및 `errorMessage`
- `disabled`, `invalid`, `name`, `form`: `RadioSelectBoxRoot`에만 지원됩니다.
```tsx
import { ActionButton, HStack, VStack, Box, Text } from "@seed-design/react";
import { useState } from "react";
import {
CheckSelectBox,
CheckSelectBoxCheckmark,
CheckSelectBoxGroup,
RadioSelectBoxItem,
RadioSelectBoxRadiomark,
RadioSelectBoxRoot,
} from "seed-design/ui/select-box";
export default function SelectBoxFieldset() {
const [checkErrors, setCheckErrors] = useState>({});
const [radioErrorMessage, setRadioErrorMessage] = useState();
const handleCheckSubmit = (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const fruits = formData.getAll("fruit");
if (fruits.includes("apple")) {
setCheckErrors({ apple: "Apple은 선택할 수 없습니다." });
return;
}
setCheckErrors({});
alert(JSON.stringify(fruits, null, 2));
};
const handleRadioSubmit = (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const color = formData.get("color");
if (color === "red") {
setRadioErrorMessage("Red는 선택할 수 없습니다.");
return;
}
setRadioErrorMessage(undefined);
alert(JSON.stringify({ color }, null, 2));
};
return (
);
}
```
---
file: components/skeleton.mdx
# Skeleton
콘텐츠가 로딩되는 동안 이후 나타날 요소의 윤곽을 미리 보여주어 로딩 시간을 짧게 느끼게 하는 UI 요소입니다.
## Preview
```tsx
import { Skeleton, VStack } from "@seed-design/react";
export default function SkeletonPreview() {
return (
);
}
```
Usage \[#usage]
```tsx
import { Skeleton } from "@seed-design/react";
```
```tsx
```
Props \[#props]
- `radius`
- type: `"0" | "8" | "16" | "full" | undefined`
- default: `8`
- `tone`
- type: `"neutral" | "magic" | undefined`
- default: `"neutral"`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
- `height`
- type: `"full" | Dimension | "spacingX.betweenChips" | "spacingX.globalGutter" | "spacingY.componentDefault" | "spacingY.navToTitle" | "spacingY.screenBottom" | "spacingY.betweenText" | (string & {}) | undefined`
- `width`
- type: `"full" | Dimension | "spacingX.betweenChips" | "spacingX.globalGutter" | "spacingY.componentDefault" | "spacingY.navToTitle" | "spacingY.screenBottom" | "spacingY.betweenText" | (string & {}) | undefined`
Examples \[#examples]
Radius \[#radius]
```tsx
import { Flex, Skeleton } from "@seed-design/react";
export default function SkeletonRadius() {
return (
);
}
```
Tone \[#tone]
```tsx
import { Box, Skeleton, VStack } from "@seed-design/react";
export default function SkeletonTone() {
return (
);
}
```
---
file: components/slider.mdx
# Slider
지정된 범위 내에서 하나 또는 두 개의 값을 선택해 입력할 수 있는 컴포넌트입니다.
## Preview
```tsx
import { Slider } from "seed-design/ui/slider";
export default function SliderPreview() {
return "값"} />;
}
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:slider
- pnpm: pnpm dlx @seed-design/cli@latest add ui:slider
- yarn: yarn dlx @seed-design/cli@latest add ui:slider
- bun: bun x @seed-design/cli@latest add ui:slider
Props \[#props]
- `label`
- type: `React.ReactNode`
- `labelWeight`
- type: `"medium" | "bold" | undefined`
- default: `"medium"`
- `indicator`
- type: `React.ReactNode`
- `description`
- type: `React.ReactNode`
- `errorMessage`
- type: `React.ReactNode`
- `showRequiredIndicator`
- type: `boolean | undefined`
- `markers`
- type: `(number | { value: number; label?: React.ReactNode; align?: SliderMarkerVariantProps["align"]; })[] | undefined`
- default: `[]`
- `ticks`
- type: `number[] | undefined`
- default: `[]`
- `tickWeight`
- type: `"thin" | "thick" | undefined`
- default: `"thin"`
- `hideRange`
- type: `boolean | undefined`
- default: `false`
- `hideValueIndicator`
- type: `boolean | undefined`
- default: `false`
- `fieldRef`
- type: `React.Ref | undefined`
- `disabled`
- type: `boolean | undefined`
- default: `false`
- `readOnly`
- type: `boolean | undefined`
- default: `false`
- `invalid`
- type: `boolean | undefined`
- default: `false`
- `name`
- type: `string | undefined`
- `jumpMultiplier`
- type: `number | undefined`
- default: `10`
- `getAriaValuetext`
- type: `((value: number) => string) | undefined`
- `getAriaLabel`
- type: `((thumbIndex: number) => string) | undefined`
- `getAriaLabelledby`
- type: `((thumbIndex: number) => string) | undefined`
- `getValueIndicatorLabel`
- type: `((params: { value: number; thumbIndex: number; }) => React.ReactNode) | undefined`
- default: `(params) => params.value`
- `valueIndicatorTrigger`
- type: `"active" | "hover" | "auto" | undefined`
- default: `"auto"`
- `dragStartDelayInMilliseconds`
- type: `number | undefined`
- default: `150`
- `min`
- type: `number`
- required: `true`
- `max`
- type: `number`
- required: `true`
- `step`
- type: `number | undefined`
- default: `1`
- `allowedValues`
- type: `number[] | undefined`
- default: `[]`
- description: Values that the slider thumbs can snap to. If not provided, the slider will snap to every step.
- `minStepsBetweenThumbs`
- type: `number | undefined`
- default: `0`
- `values`
- type: `number[] | undefined`
- `defaultValues`
- type: `number[] | undefined`
- default: `[(min + max) / 2]`
- `onValuesChange`
- type: `((value: number[]) => void) | undefined`
- `onValuesCommit`
- type: `((value: number[]) => void) | undefined`
- `dir`
- type: `"ltr" | "rtl" | undefined`
- default: `"ltr"`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
Examples \[#examples]
Basic \[#basic]
`min`과 `max`를 설정하여 슬라이더의 값 범위를 지정합니다.
```tsx
import { VStack } from "@seed-design/react";
import { Slider } from "seed-design/ui/slider";
export default function SliderBasic() {
return (
"값"} />
"값"} />
);
}
```
Steps \[#steps]
특정 간격으로만 값을 선택할 수 있도록 설정할 수 있습니다.
```tsx
import { Slider } from "seed-design/ui/slider";
import { useState } from "react";
import { VStack, Text } from "@seed-design/react";
export default function SliderSteps() {
const [value, setValue] = useState([50]);
return (
"값"}
/>
{JSON.stringify(value)}
);
}
```
Allowed Values \[#allowed-values]
`allowedValues` prop을 사용하여 슬라이더에서 선택할 수 있는 값을 제한할 수 있습니다. `allowedValues`가 지정된 경우 `step`과 `minStepsBetweenThumbs`은 무시됩니다.
```tsx
import { VStack, Text } from "@seed-design/react";
import { Slider } from "seed-design/ui/slider";
import { useState } from "react";
const ALLOWED_VALUES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29];
export default function SliderAllowedValues() {
const [values, setValues] = useState([ALLOWED_VALUES[0], ALLOWED_VALUES[2]]);
return (
({ label: value, value }))}
getAriaLabel={() => "값"}
/>
{JSON.stringify(values)}
);
}
```
With Ticks \[#with-ticks]
슬라이더 트랙에 눈금을 표시할 수 있습니다.
Thin \[#thin]
continuous하다고 느껴질 만큼 step이 충분히 작을 때 주로 사용합니다.
```tsx
import { Slider } from "seed-design/ui/slider";
export default function SliderTicksThin() {
return (
"값"}
/>
);
}
```
Thick \[#thick]
discrete하다고 느껴질 만큼 step이 충분히 클 때 또는 tick에만 thumb이 위치할 수 있을 때 주로 사용합니다.
```tsx
import { Slider } from "seed-design/ui/slider";
export default function SliderTicksThick() {
return (
"값"}
/>
);
}
```
With Markers \[#with-markers]
슬라이더 아래에 마커를 표시할 수 있습니다.
스크린 리더는 marker를 읽지 않습니다. 특정 값을 선택했을 때 사용자에게 알려야 하는 경우 [`getAriaValuetext` prop](#getariavaluetext)을 사용하여 현재 선택된 값을 설명하는 문구를 함께 제공하세요.
```tsx
import { VStack } from "@seed-design/react";
import { Slider } from "seed-design/ui/slider";
export default function SliderMarkers() {
return (
`${value}°C`}
getValueIndicatorLabel={({ value }) => `${value}°C`}
getAriaLabel={() => "온도"}
/>
"값"}
/>
);
}
```
Controlled \[#controlled]
`values`와 `onValuesChange` props를 사용하여 슬라이더의 상태를 외부에서 제어합니다.
```tsx
import { VStack, HStack, Text } from "@seed-design/react";
import { Slider } from "seed-design/ui/slider";
import { ActionButton } from "seed-design/ui/action-button";
import { useState } from "react";
const DEFAULT_VALUE = [50];
export default function SliderControlled() {
const [value, setValue] = useState(DEFAULT_VALUE);
return (
"값"}
/>
{JSON.stringify(value)}
setValue([0])} variant="neutralWeak">
Set Min
setValue(DEFAULT_VALUE)} variant="neutralWeak">
Reset
setValue([100])} variant="neutralWeak">
Set Max
);
}
```
Listening to Value Changes \[#listening-to-value-changes]
- `onValuesChange`
- 슬라이더의 값이 변경될 때마다 호출됩니다. 이 prop을 사용하여 슬라이더의 값을 실시간으로 추적할 수 있습니다.
- `onValuesCommit`
- 슬라이더의 값 변경이 완료되었을 때 호출됩니다. 사용자가 슬라이더 조작을 마치고 손을 뗄 때 값을 확정하는 데 유용합니다.
```tsx
import { VStack, Text, HStack } from "@seed-design/react";
import { Slider } from "seed-design/ui/slider";
import { useState } from "react";
export default function SliderOnValuesCommit() {
const [value, setValue] = useState([20]);
const [committedValue, setCommittedValue] = useState([20]);
return (
"값"}
/>
Current value: {JSON.stringify(value)}
Committed value: {JSON.stringify(committedValue)}
);
}
```
Disabled \[#disabled]
```tsx
import { VStack } from "@seed-design/react";
import { Slider } from "seed-design/ui/slider";
export default function SliderDisabled() {
return (
"값"} />
(thumbIndex === 0 ? "최소값" : "최대값")}
/>
);
}
```
Hide Range \[#hide-range]
`hideRange` prop을 사용하여 기본 range 색상을 숨기고 커스텀 스타일을 적용할 수 있습니다.
```tsx
import { VStack } from "@seed-design/react";
import { Slider } from "seed-design/ui/slider";
export default function SliderHideRange() {
return (
"값"} />
(thumbIndex === 0 ? "최소값" : "최대값")}
/>
);
}
```
Customizing Value Indicator \[#customizing-value-indicator]
Value Indicator Label \[#value-indicator-label]
`getValueIndicatorLabel` prop을 사용하여 thumb 위에 표시되는 툴팁의 내용을 커스텀할 수 있습니다.
Value Indicator는 thumb을 누르고 있을 때만 표시되며 스크린 리더는 Value Indicator의 내용을 읽지 않습니다. Value Indicator에 의미가 있는 내용을 포함하는 경우, [`getAriaValuetext` prop](#getariavaluetext)을 함께 사용하는 것을 권장합니다.
```tsx
import { Slider } from "seed-design/ui/slider";
const formatter = new Intl.NumberFormat("ko-KR", { style: "decimal" });
export default function SliderCustomValueIndicatorLabel() {
return (
(
<>
thumb {thumbIndex}
{formatter.format(value)}
>
)}
getAriaValuetext={formatter.format}
getAriaLabel={() => "값"}
/>
);
}
```
Value Indicator Trigger \[#value-indicator-trigger]
`valueIndicatorTrigger` prop으로 value indicator가 표시되는 조건을 설정할 수 있습니다. 기본값은 `"auto"`로, 디바이스 환경에 따라 Value Indicator 표시 조건이 달라집니다.
- **마우스 환경**: thumb hover 시 Value Indicator가 표시됩니다. `valueIndicatorTrigger="hover"`로 설정하면 항상 이렇게 동작합니다.
- **터치 환경**: thumb active 시 Value Indicator가 표시됩니다. `valueIndicatorTrigger="active"`로 설정하면 항상 이렇게 동작합니다.
키보드를 통해 thumb을 포커스하면 `valueIndicatorTrigger` 값에 관계없이 항상 Value Indicator가 표시됩니다.
```tsx
import { VStack } from "@seed-design/react";
import { Slider } from "seed-design/ui/slider";
export default function SliderValueIndicatorTrigger() {
return (
"값"}
/>
"값"}
/>
"값"}
/>
);
}
```
Hide Value Indicator \[#hide-value-indicator]
`hideValueIndicator` prop을 사용하여 thumb 위에 표시되는 Value Indicator 툴팁을 숨길 수 있습니다.
```tsx
import { Slider } from "seed-design/ui/slider";
export default function SliderHideValueIndicator() {
return (
"값"} />
);
}
```
Range Slider \[#range-slider]
두 개의 thumb를 사용하여 범위를 선택할 수 있습니다.
```tsx
import { VStack } from "@seed-design/react";
import { Slider } from "seed-design/ui/slider";
import { useState } from "react";
export default function SliderRange() {
const [priceRange, setPriceRange] = useState([20, 80]);
return (
(thumbIndex === 0 ? "최소값" : "최대값")}
/>
);
}
```
Minimum Steps Between Thumbs \[#minimum-steps-between-thumbs]
두 thumb 사이의 최소 간격을 설정할 수 있습니다.
```tsx
import { VStack, Text } from "@seed-design/react";
import { Slider } from "seed-design/ui/slider";
import { useState } from "react";
export default function SliderRangeMinSteps() {
const [values, setValues] = useState([20, 80]);
return (
(thumbIndex === 0 ? "최소값" : "최대값")}
/>
{JSON.stringify(values)}
);
}
```
Accessibility \[#accessibility]
`getAriaValuetext` \[#getariavaluetext]
스크린 리더는 기본적으로 각 thumb이 가리키는 숫자 값(value)을 읽습니다. \**숫자 값만으로 정보를 충분히 전달할 수 없는 경우 `getAriaValuetext` prop을 사용하여 인간 친화적인 설명을 제공하세요.*\* 단위를 추가하거나, 값의 의미를 설명하는 문구를 포함할 수 있습니다.
```tsx
import { VStack, Text } from "@seed-design/react";
import { useState } from "react";
import { Slider } from "seed-design/ui/slider";
const days = ["일", "월", "화", "수", "목", "금", "토"];
function getHumanReadableDayOfWeek(value: number) {
if (days[value] === undefined) throw new Error("Invalid day value");
return `${days[value]}요일`;
}
export default function SliderGetAriaValuetext() {
const [values, setValues] = useState([1, 3]);
return (
({ label, value }))}
ticks={days.slice(1, -1).map((_, index) => index + 1)}
tickWeight="thick"
values={values}
onValuesChange={setValues}
getAriaLabel={(thumbIndex) => (thumbIndex === 0 ? "시작" : "종료")}
getAriaValuetext={getHumanReadableDayOfWeek}
getValueIndicatorLabel={({ value }) => getHumanReadableDayOfWeek(value)}
/>
values: {JSON.stringify(values)}
aria-valuetext: {JSON.stringify(values.map(getHumanReadableDayOfWeek))}
);
}
```
`getAriaLabel` and `getAriaLabelledby` \[#getarialabel-and-getarialabelledby]
`getAriaLabel` 또는 `getAriaLabelledby` prop을 사용하여 각 thumb에 대한 설명을 제공하세요. 스크린 리더 사용자가 각 thumb의 용도를 이해하는 데 도움을 줍니다.
**특히, 범위를 선택하는 슬라이더의 경우 각 thumb이 범위에서 어떤 역할을 하는지 설명해야 합니다. (최소-최대, 시작-종료 등)**
```tsx
import { VStack, Text, type SliderRootProps } from "@seed-design/react";
import { useState } from "react";
import { Slider } from "seed-design/ui/slider";
const getAriaLabel: NonNullable = (thumbIndex) =>
thumbIndex === 0 ? "최소값" : "최대값";
export default function SliderGetAriaLabel() {
const [values, setValues] = useState([10, 30]);
return (
values: {JSON.stringify(values)}
aria-label: {JSON.stringify(values.map((_, index) => getAriaLabel(index)))}
);
}
```
Field Integration \[#field-integration]
`label`, `description`, `errorMessage` 등의 Field 관련 prop을 사용할 수 있습니다.
```tsx
import { Divider, VStack } from "@seed-design/react";
import { Slider } from "seed-design/ui/slider";
const markers = [
{ value: 0, label: "매우 동의하지 않음" },
{ value: 14, label: "매우 동의함" },
];
export default function SliderField() {
return (
`${value} ${markers.find((marker) => marker.value === value)?.label ?? ""}`.trim()
}
/>
);
}
```
Use Cases \[#use-cases]
Form (Uncontrolled) \[#form-uncontrolled]
```tsx
import { HStack, VStack } from "@seed-design/react";
import { useCallback, useState, type FormEvent } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import { Slider } from "seed-design/ui/slider";
type FieldErrors = {
rating?: string;
priceRange?: string;
};
export default function SliderForm() {
const [fieldErrors, setFieldErrors] = useState({});
const handleSubmit = useCallback((event: FormEvent) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const rating = Number(formData.get("rating"));
const priceRangeValues = formData.getAll("price-range").map(Number);
const newFieldErrors: FieldErrors = {};
if (rating < 3) {
newFieldErrors.rating = "평점은 최소 3점 이상이어야 합니다";
}
const [min, max] = priceRangeValues;
if (max - min < 20) {
newFieldErrors["priceRange"] = "가격 범위는 최소 20만원 이상 차이가 나야 합니다";
}
setFieldErrors(newFieldErrors);
if (Object.keys(newFieldErrors).length > 0) return;
window.alert(JSON.stringify({ rating, "price-range": priceRangeValues }, null, 2));
}, []);
return (
);
}
```
React Hook Form \[#react-hook-form]
```tsx
import { HStack, VStack } from "@seed-design/react";
import { useCallback, type FormEvent } from "react";
import { useController, useForm } from "react-hook-form";
import { ActionButton } from "seed-design/ui/action-button";
import { Slider } from "seed-design/ui/slider";
interface FormValues {
rating: number;
priceRange: [number, number];
}
export default function SliderReactHookForm() {
const { handleSubmit, reset, control } = useForm({
reValidateMode: "onSubmit",
defaultValues: {
rating: 2.5,
priceRange: [20, 80],
},
});
const {
field: { value: ratingValue, onChange: ratingOnChange, onBlur: __ratingOnBlur, ...ratingField },
fieldState: ratingFieldState,
} = useController({
name: "rating",
control,
rules: {
validate: (value) => value >= 3 || "평점은 최소 3점 이상이어야 합니다",
},
});
const {
field: {
value: priceRangeValue,
onChange: priceRangeOnChange,
onBlur: __priceRangeOnBlur,
...priceRangeField
},
fieldState: priceRangeFieldState,
} = useController({
name: "priceRange",
control,
rules: {
validate: (value) => {
const [min, max] = value;
return max - min >= 20 || "가격 범위는 최소 20만원 이상 차이가 나야 합니다";
},
},
});
const onValid = useCallback(
(data: FormValues) => window.alert(JSON.stringify(data, null, 2)),
[],
);
const onReset = useCallback(
(event: FormEvent) => {
event.preventDefault();
reset();
},
[reset],
);
return (
ratingOnChange(values)}
markers={[
{ value: 0, label: "0점" },
{ value: 5, label: "5점" },
]}
getValueIndicatorLabel={({ value }) => `${value}점`}
getAriaValuetext={(value) => `${value}점`}
showRequiredIndicator
{...ratingField}
/>
`${value}만원`}
getAriaValuetext={(value) => `${value}만원`}
getAriaLabel={(index) => (index === 0 ? "최소 가격" : "최대 가격")}
minStepsBetweenThumbs={2}
showRequiredIndicator
{...priceRangeField}
/>
초기화
제출
);
}
```
RTL Support \[#rtl-support]
```tsx
import { VStack } from "@seed-design/react";
import { Slider } from "seed-design/ui/slider";
export default function SliderRtl() {
return (
`${value}°C`}
getValueIndicatorLabel={({ value }) => `${value}°C`}
getAriaLabel={() => "온도"}
/>
"값"}
/>
);
}
```
---
file: components/snackbar.mdx
# Snackbar
화면 하단에 일시적으로 나타나 상태나 결과를 안내하는 컴포넌트입니다.
## Preview
```tsx
import { ActionButton } from "seed-design/ui/action-button";
import { Snackbar, SnackbarProvider, useSnackbarAdapter } from "seed-design/ui/snackbar";
function Component() {
const adapter = useSnackbarAdapter();
return (
adapter.create({
timeout: 5000,
onClose: () => {},
render: () => {}} />,
})
}
>
실행
);
}
export default function SnackbarPreview() {
return (
);
}
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:snackbar
- pnpm: pnpm dlx @seed-design/cli@latest add ui:snackbar
- yarn: yarn dlx @seed-design/cli@latest add ui:snackbar
- bun: bun x @seed-design/cli@latest add ui:snackbar
Props \[#props]
`Snackbar` \[#snackbar]
- `message`
- type: `string`
- required: `true`
- description: 스낵바에 표시할 메시지
- `actionLabel`
- type: `string | undefined`
- description: 스낵바에 표시할 액션 버튼의 라벨
- `onAction`
- type: `(() => void) | undefined`
- description: 액션 버튼 클릭 시 호출되는 콜백
- `shouldCloseOnAction`
- type: `boolean | undefined`
- default: `true`
- description: 액션 버튼 클릭 시 스낵바를 닫을지 여부
- `variant`
- type: `"default" | "positive" | "critical" | undefined`
- default: `"default"`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`SnackbarAdapter.create` Parameters (`CreateSnackbarOptions`) \[#snackbaradaptercreate-parameters-createsnackbaroptions]
- `timeout`
- type: `number | undefined`
- default: `5000`
- description: The duration the snackbar will be visible
- `removeDelay`
- type: `number | undefined`
- default: `200`
- description: The duration for the snackbar to kept alive before it is removed. Useful for exit transitions.
- `onClose`
- type: `(() => void) | undefined`
- description: Function called when the snackbar has been closed and removed
- `render`
- type: `() => React.ReactNode`
- required: `true`
- description: The content to render in the snackbar region
`SnackbarProvider` \[#snackbarprovider]
- `children`
- type: `React.ReactNode`
- required: `true`
- `pauseOnInteraction`
- type: `boolean | undefined`
- default: `true`
- description: Whether to pause the toast when interacted with
Examples \[#examples]
Variants \[#variants]
Positive \[#positive]
```tsx
import { ActionButton } from "seed-design/ui/action-button";
import { Snackbar, SnackbarProvider, useSnackbarAdapter } from "seed-design/ui/snackbar";
function Component() {
const adapter = useSnackbarAdapter();
return (
adapter.create({
timeout: 5000,
onClose: () => {},
render: () => (
{}}
/>
),
})
}
>
실행
);
}
export default function SnackbarPositive() {
return (
);
}
```
Critical \[#critical]
```tsx
import { ActionButton } from "seed-design/ui/action-button";
import { Snackbar, SnackbarProvider, useSnackbarAdapter } from "seed-design/ui/snackbar";
function Component() {
const adapter = useSnackbarAdapter();
return (
adapter.create({
timeout: 50000000,
onClose: () => {},
render: () => (
{}}
/>
),
})
}
>
실행
);
}
export default function SnackbarNegative() {
return (
);
}
```
Pause on Interaction \[#pause-on-interaction]
사용자가 Snackbar와 상호작용(hover 및 active) 하는 동안 Snackbar가 timeout으로 인해 dismiss되지 않도록 하려면 `SnackbarProvider`의 `pauseOnInteraction`을 `true`로 설정합니다.
- `@seed-design/react@0.1.14`까지: `false`: 명시하지 않는 경우, 상호작용 여부와 관계없이 timeout이 지나면 dismiss됩니다.
- 이후 버전: `true`: 명시하지 않는 경우, 사용자가 상호작용하는 동안 timeout이 멈춥니다.
```tsx
import { VStack } from "@seed-design/react";
import { ActionButton } from "seed-design/ui/action-button";
import { Snackbar, SnackbarProvider, useSnackbarAdapter } from "seed-design/ui/snackbar";
import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control";
import { useState } from "react";
function Component() {
const adapter = useSnackbarAdapter();
return (
adapter.create({
timeout: 5000,
onClose: () => {},
render: () => {}} />,
})
}
>
실행
);
}
export default function SnackbarPauseOnInteraction() {
const [pauseOnInteraction, setPauseOnInteraction] = useState(true);
return (
setPauseOnInteraction(value === "true")}
>
false
true
);
}
```
Avoid Overlap \[#avoid-overlap]
` ` 컴포넌트를 사용하여 스낵바가 겹치지 않아야 하는 영역을 지정할 수 있습니다.
` `의 자식 컴포넌트는 forwardRef(~React 18) 혹은 ref prop(React 19~)으로 ref를 전달받아야 합니다.
offset은 다음 상황에서 갱신됩니다.
- SnackbarAvoidOverlap의 자식 컴포넌트가 mount될 때
- SnackbarAvoidOverlap의 자식 컴포넌트가 unmount될 때
- SnackbarAvoidOverlap의 자식 컴포넌트의 높이가 변경될 때
- SnackbarRegion의 높이가 변경될 때
```tsx
import { Flex, VStack } from "@seed-design/react";
import { ActionButton } from "seed-design/ui/action-button";
import {
Snackbar,
SnackbarAvoidOverlap,
SnackbarProvider,
useSnackbarAdapter,
} from "seed-design/ui/snackbar";
function Component() {
const adapter = useSnackbarAdapter();
return (
adapter.create({
timeout: 5000,
onClose: () => {},
render: () => {}} />,
})
}
>
실행
Snackbar가 이 영역과 겹치지 않게 조정됩니다. 스크롤은 무시합니다.
);
}
export default function SnackbarPreview() {
return (
);
}
```
---
file: components/switch.mdx
# Switch
특정 설정 및 상태를 즉시 켜거나 끌 수 있도록 하는 컴포넌트입니다.
## Preview
```tsx
import { Switch } from "seed-design/ui/switch";
export default function SwitchPreview() {
return ;
}
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:switch
- pnpm: pnpm dlx @seed-design/cli@latest add ui:switch
- yarn: yarn dlx @seed-design/cli@latest add ui:switch
- bun: bun x @seed-design/cli@latest add ui:switch
Props \[#props]
`Switch` \[#switch]
- `inputProps`
- type: `React.InputHTMLAttributes | undefined`
- `rootRef`
- type: `React.Ref | undefined`
- `label`
- type: `React.ReactNode`
- `size`
- type: `"16" | "24" | "32" | SwitchVariantDeprecatedSizeProps | undefined`
- `tone`
- type: `"neutral" | "brand" | undefined`
- default: `"brand"`
- `disabled`
- type: `boolean | undefined`
- `invalid`
- type: `boolean | undefined`
- `required`
- type: `boolean | undefined`
- `checked`
- type: `boolean | undefined`
- `defaultChecked`
- type: `boolean | undefined`
- `onCheckedChange`
- type: `((checked: boolean) => void) | undefined`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`Switchmark` \[#switchmark]
- `tone`
- type: `"neutral" | "brand" | undefined`
- default: `"brand"`
- `size`
- type: `"16" | "24" | "32" | undefined`
- default: `32`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
Examples \[#examples]
Sizes \[#sizes]
`size="medium"`과 `size="small"`은 더 이상 사용되지 않습니다. 대신 `size="32"`와 `size="16"`을 사용하세요.
```tsx
import { VStack } from "@seed-design/react";
import { Switch } from "seed-design/ui/switch";
export default function SwitchSizes() {
return (
);
}
```
Tones \[#tones]
Brand \[#brand]
```tsx
import { Switch } from "seed-design/ui/switch";
export default function SwitchBrand() {
return ;
}
```
Neutral \[#neutral]
```tsx
import { Switch } from "seed-design/ui/switch";
export default function SwitchNeutral() {
return ;
}
```
Long Label \[#long-label]
```tsx
import { VStack } from "@seed-design/react";
import { Switch } from "seed-design/ui/switch";
export default function SwitchLongLabel() {
return (
);
}
```
Disabled \[#disabled]
```tsx
import { VStack } from "@seed-design/react";
import { useState } from "react";
import { Switch } from "seed-design/ui/switch";
export default function SwitchDisabled() {
const [disabled, setDisabled] = useState(true);
return (
);
}
```
Listening to Value Changes \[#listening-to-value-changes]
`onCheckedChange`를 사용하여 스위치의 선택 상태 변경을 감지할 수 있습니다.
이벤트를 활용해야 하는 경우 `inputProps`를 통해 내부 ` ` 요소에 직접 이벤트 핸들러를 추가할 수 있습니다.
```tsx
import { VStack, Text } from "@seed-design/react";
import { Switch } from "seed-design/ui/switch";
import { useState } from "react";
export default function SwitchValueChanges() {
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 \[#use-cases]
Using `Switchmark` \[#using-switchmark]
`Switchmark`는 레이블을 제외한 스위치 컴포넌트로, 커스텀 레이아웃을 위해 사용할 수 있습니다.
```tsx
import { HStack, Text, VStack } from "@seed-design/react";
import { Switch } from "@seed-design/react/primitive";
import { Switchmark } from "seed-design/ui/switch";
function CustomSwitch({ children, ...props }: Switch.RootProps) {
return (
{children}
);
}
export default function () {
return (
regular
medium
bold
);
}
```
---
file: components/tag-group.mdx
# Tag Group
아이콘과 텍스트 태그를 수평으로 나열해 여러 속성·상태·메타데이터를 한눈에 보여주는 정보 요약 컴포넌트입니다.
## Preview
```tsx
import { IconLocationpinFill } from "@karrotmarket/react-monochrome-icon";
import { TagGroupRoot, TagGroupItem } from "seed-design/ui/tag-group";
export default function TagGroupPreview() {
return (
} label="500m" />
);
}
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:tag-group
- pnpm: pnpm dlx @seed-design/cli@latest add ui:tag-group
- yarn: yarn dlx @seed-design/cli@latest add ui:tag-group
- bun: bun x @seed-design/cli@latest add ui:tag-group
Props \[#props]
`TagGroupRoot` \[#taggrouproot]
- `separator`
- type: `ReactNode`
- `size`
- type: `"t2" | "t3" | "t4" | undefined`
- default: `"t2"`
- `truncate`
- type: `boolean | undefined`
- default: `false`
- `weight`
- type: `"regular" | "bold" | undefined`
- default: `"regular"`
- `tone`
- type: `"neutralSubtle" | "neutral" | "brand" | undefined`
- default: `"neutralSubtle"`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`TagGroupItem` \[#taggroupitem]
- `size`
- type: `"t2" | "t3" | "t4" | undefined`
- default: `"t2"`
- `weight`
- type: `"regular" | "bold" | undefined`
- default: `"regular"`
- `tone`
- type: `"neutralSubtle" | "neutral" | "brand" | undefined`
- default: `"neutralSubtle"`
- `flexShrink`
- type: `true | 0 | (number & {}) | undefined`
- description: If true, flex-shrink will be set to \`1\`.
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
Examples \[#examples]
Sizes \[#sizes]
```tsx
import { VStack } from "@seed-design/react";
import { TagGroupRoot, TagGroupItem } from "seed-design/ui/tag-group";
export default function TagGroupSizes() {
return (
);
}
```
Weights \[#weights]
```tsx
import { VStack } from "@seed-design/react";
import { TagGroupRoot, TagGroupItem } from "seed-design/ui/tag-group";
export default function TagGroupWeights() {
return (
);
}
```
Tones \[#tones]
```tsx
import { VStack } from "@seed-design/react";
import { TagGroupRoot, TagGroupItem } from "seed-design/ui/tag-group";
export default function TagGroupTones() {
return (
);
}
```
With Icons \[#with-icons]
```tsx
import { IconLocationpinFill, IconMegaphoneFill } from "@karrotmarket/react-monochrome-icon";
import { VStack } from "@seed-design/react";
import { TagGroupRoot, TagGroupItem } from "seed-design/ui/tag-group";
export default function TagGroupWithIcons() {
return (
} />
} label="서초4동" />
);
}
```
Customizing `TagGroupItem` \[#customizing-taggroupitem]
`TagGroupRoot`에 지정한 `size`, `tone`, `weight`은 `TagGroupItem`에서 덮어씌울 수 있습니다.
필요한 경우 [컴파운드 컴포넌트](/react/components/concepts/snippet#compound-components)를 직접 사용하여 `TagGroupItem` 내부 요소를 커스터마이징할 수 있습니다.
```tsx
import {
IconCheckmarkCircleFill,
IconHeartFill,
IconHorizline2VerticalChatbubbleRectangularRightFill,
IconStarFill,
} from "@karrotmarket/react-monochrome-icon";
import { Icon, VStack } from "@seed-design/react";
import { TagGroupRoot, TagGroupItem } from "seed-design/ui/tag-group";
import { TagGroup as SeedTagGroup } from "@seed-design/react";
export default function TagGroupCustomizingItem() {
return (
{/* You can use the compound components to customize the items */}
} color="fg.brand" />
4.5
} label="인증됨" />
} label="10" />
}
label="3"
/>
);
}
```
Customizing Separators \[#customizing-separators]
`TagGroupRoot`에 `separator`를 지정하여 구분 기호를 변경할 수 있습니다.
스크린 리더는 각 태그 항목 사이에 지정된 구분 기호를 읽지 않습니다. 의미가 있는(semantic) 정보를 Separator로 사용하는 것을 피하세요.
```tsx
import { VStack } from "@seed-design/react";
import { TagGroupRoot, TagGroupItem } from "seed-design/ui/tag-group";
export default function TagGroupCustomizingSeparators() {
return (
);
}
```
Wrapping Behavior \[#wrapping-behavior]
기본적으로 `TagGroupRoot`는 컨테이너 너비를 초과하면 줄바꿈이 발생합니다.
`TagGroupRoot`에 `truncate` prop을 지정하면 `TagGroupRoot`가 한 줄로 유지되고, 컨테이너 너비를 초과하는 경우 기본적으로 모든 `TagGroupItem`의 레이블에서 말줄임이 발생할 수 있습니다.
`truncate` prop을 사용하면서 특정 아이템이 축소되지 않도록 하려면 해당 `TagGroupItem`에 `flexShrink={0}`을 지정하세요. `TagGroupItem`에 `flexShrink={number}`를 지정하여 컨테이너 너비를 초과할 때 축소되는 우선순위를 조정할 수 있습니다.
```tsx
import { IconBellFill, IconLocationpinFill } from "@karrotmarket/react-monochrome-icon";
import { Flex, Text, VStack } from "@seed-design/react";
import { TagGroupRoot, TagGroupItem } from "seed-design/ui/tag-group";
import type { PropsWithChildren, ReactNode } from "react";
export default function TagGroupWrappingBehavior() {
return (
} label="부산광역시 해운대구" />
} label="123 456 789 012 345" />
} label="부산광역시 해운대구" />
} label="123 456 789 012 345" />
} label="부산광역시 해운대구" />
}
label="123 456 789 012 345"
/>
}
flexShrink={1}
label="부산광역시 해운대구"
/>
}
label="123 456 789 012 345"
/>
);
}
function Wrapper({ title, children }: PropsWithChildren<{ title: ReactNode }>) {
return (
{title}
{children}
);
}
```
---
file: components/toggle-button.mdx
# Toggle Button
사용자가 특정 상태를 켜거나 끌 수 있게 해주는 버튼 형태의 컴포넌트입니다. 필터링이나 뷰 전환과 같은 즉각적인 상태 변경에 사용됩니다.
## Preview
```tsx
import { useState } from "react";
import { ToggleButton } from "seed-design/ui/toggle-button";
export default function ToggleButtonPreview() {
const [pressed, setPressed] = useState(false);
return (
{pressed ? "선택됨" : "미선택"}
);
}
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:toggle-button
- pnpm: pnpm dlx @seed-design/cli@latest add ui:toggle-button
- yarn: yarn dlx @seed-design/cli@latest add ui:toggle-button
- bun: bun x @seed-design/cli@latest add ui:toggle-button
Props \[#props]
- `variant`
- type: `"brandSolid" | "neutralWeak" | undefined`
- default: `"brandSolid"`
- `size`
- type: `"xsmall" | "small" | undefined`
- default: `"small"`
- `loading`
- type: `boolean | undefined`
- default: `false`
- description: 버튼에 등록된 비동기 작업이 진행 중임을 사용자에게 알립니다.
- `disabled`
- type: `boolean | undefined`
- default: `false`
- description: 버튼의 비활성화 여부를 나타냅니다.
- `pressed`
- type: `boolean | undefined`
- `defaultPressed`
- type: `boolean | undefined`
- `onPressedChange`
- type: `((pressed: boolean) => void) | undefined`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
Examples \[#examples]
Brand Solid \[#brand-solid]
```tsx
import { useState } from "react";
import { ToggleButton } from "seed-design/ui/toggle-button";
export default function ToggleButtonBrandSolid() {
const [pressed, setPressed] = useState(false);
return (
{pressed ? "선택됨" : "미선택"}
);
}
```
Neutral Weak \[#neutral-weak]
```tsx
import { useState } from "react";
import { ToggleButton } from "seed-design/ui/toggle-button";
export default function ToggleButtonBrandSolid() {
const [pressed, setPressed] = useState(false);
return (
{pressed ? "선택됨" : "미선택"}
);
}
```
Small \[#small]
```tsx
import { useState } from "react";
import { ToggleButton } from "seed-design/ui/toggle-button";
export default function ToggleButtonSmall() {
const [pressed, setPressed] = useState(false);
return (
{pressed ? "선택됨" : "미선택"}
);
}
```
Xsmall \[#xsmall]
```tsx
import { useState } from "react";
import { ToggleButton } from "seed-design/ui/toggle-button";
export default function ToggleButtonXsmall() {
const [pressed, setPressed] = useState(false);
return (
{pressed ? "선택됨" : "미선택"}
);
}
```
Prefix Icon \[#prefix-icon]
```tsx
import { IconCheckmarkLine, IconPlusLine } from "@karrotmarket/react-monochrome-icon";
import { PrefixIcon } from "@seed-design/react";
import { useState } from "react";
import { ToggleButton } from "seed-design/ui/toggle-button";
export default function ToggleButtonPrefixIcon() {
const [pressed, setPressed] = useState(false);
return (
: } />
{pressed ? "선택됨" : "미선택"}
);
}
```
Disabled \[#disabled]
```tsx
import { ToggleButton } from "seed-design/ui/toggle-button";
export default function ToggleButtonDisabled() {
return 비활성 ;
}
```
Loading \[#loading]
```tsx
import { useState } from "react";
import { ToggleButton } from "seed-design/ui/toggle-button";
export default function ToggleButtonLoading() {
const [pressed, setPressed] = useState(false);
const [loading, setLoading] = useState(false);
function handleToggle() {
setLoading(true);
setTimeout(() => {
setLoading(false);
setPressed((prev) => !prev);
}, 2000);
}
// 이벤트 핸들링이 필요할 수 있으므로 loading은 disabled를 포함하지 않습니다. 이벤트 발생을 원하지 않는 경우, disabled 속성을 추가해주세요.
return (
시간이 걸리는 토글
);
}
```
---
file: developer-tools/codemods/available-transforms.mdx
# Available Transforms
codemod를 통해 실행 가능한 변환 목록입니다.
{/* Auto-generated from `scripts/generate-codemod-docs.ts` */}
replace-custom-seed-design-typography \[#replace-custom-seed-design-typography]
- npm: npx @seed-design/codemod@latest replace-custom-seed-design-typography \
- pnpm: pnpm dlx @seed-design/codemod@latest replace-custom-seed-design-typography \
- yarn: yarn dlx @seed-design/codemod@latest replace-custom-seed-design-typography \
- bun: bun x @seed-design/codemod@latest replace-custom-seed-design-typography \
```ts title="basic.input.ts"
// @ts-nocheck
import { style } from "@vanilla-extract/css";
import { vars } from "@seed-design/css";
import { f, reset } from "@/shared/styles";
import { typography, text } from '@/src/styles/tokens';
export const title = style([
typography.h4,
{
textAlign: "center",
color: vars.$scale.color.gray900,
margin: 0,
},
]);
export const subtitle = style([
f.typography.title2Bold,
{
marginBottom: "0.375rem",
color: vars.$scale.color.gray900,
},
]);
export const smallText = style([
typo.caption2Regular,
{
color: vars.$scale.color.gray700,
},
]);
export const largeTitle = style([
typo.bodyL1Regular,
{
color: vars.$scale.color.gray900,
},
]);
export const button = style([
typography['$semantic.label2Regular'],
{
width: '100%',
paddingTop: rem(14),
paddingRight: rem(16),
paddingBottom: rem(14),
paddingLeft: rem(16),
},
]);
export const day = style([
{
display: 'flex',
flex: 1,
alignItems: 'center',
justifyContent: 'center',
height: rem(30),
},
typography['$semantic.caption1Regular'],
]);
// Additional test cases for various mappings
export const titleBold = style([
typography.title3Bold,
{
fontWeight: 'bold',
},
]);
export const subtitleRegular = style([
f.typography.subtitle2Regular,
{
color: vars.$scale.color.gray800,
},
]);
export const bodyText = style([
typo.bodyM1Regular,
{
lineHeight: 1.5,
},
]);
export const labelText = style([
typography['$semantic.label1Bold'],
{
textTransform: 'uppercase',
},
]);
export const captionBold = style([
typography['$semantic.caption2Bold'],
{
fontSize: '12px',
},
]);
// Test deprecated tokens with alternative
export const deprecatedBody = style([
typo.bodyL2Regular,
{
margin: '8px 0',
},
]);
export const deprecatedLabel = style([
typography['$semantic.label6Regular'],
{
opacity: 0.8,
},
]);
// Test multiple token choices (should use first one)
export const headerText = style([
typography.h4,
{
textAlign: 'center',
},
]);
export const text = style([
text['$semantic.bodyL2Bold'],
]);
```
```ts title="basic.output.ts"
// @ts-nocheck
import { style } from "@vanilla-extract/css";
import { vars } from "@seed-design/css";
import { f, reset } from "@/shared/styles";
import { typography, text } from '@/src/styles/tokens';
export const title = style([
typography.t10Bold,
{
textAlign: "center",
color: vars.$scale.color.gray900,
margin: 0,
},
]);
export const subtitle = style([
f.typography.t7Bold,
{
marginBottom: "0.375rem",
color: vars.$scale.color.gray900,
},
]);
export const smallText = style([
typo.t2Regular,
{
color: vars.$scale.color.gray700,
},
]);
export const largeTitle = style([
typo.articleBody,
{
color: vars.$scale.color.gray900,
},
]);
export const button = style([
typography.t5Regular,
{
width: '100%',
paddingTop: rem(14),
paddingRight: rem(16),
paddingBottom: rem(14),
paddingLeft: rem(16),
},
]);
export const day = style([
{
display: 'flex',
flex: 1,
alignItems: 'center',
justifyContent: 'center',
height: rem(30),
},
typography.t3Regular,
]);
// Additional test cases for various mappings
export const titleBold = style([
typography.t6Bold,
{
fontWeight: 'bold',
},
]);
export const subtitleRegular = style([
f.typography.t4Regular,
{
color: vars.$scale.color.gray800,
},
]);
export const bodyText = style([
typo.t5Regular,
{
lineHeight: 1.5,
},
]);
export const labelText = style([
typography.t6Bold,
{
textTransform: 'uppercase',
},
]);
export const captionBold = style([
typography.t2Bold,
{
fontSize: '12px',
},
]);
// Test deprecated tokens with alternative
export const deprecatedBody = style([
typo.t4Regular,
{
margin: '8px 0',
},
]);
export const deprecatedLabel = style([
typography.t1Regular,
{
opacity: 0.8,
},
]);
// Test multiple token choices (should use first one)
export const headerText = style([
typography.t10Bold,
{
textAlign: 'center',
},
]);
export const text = style([
text.t4Bold,
]);
```
replace-tailwind-typography \[#replace-tailwind-typography]
- npm: npx @seed-design/codemod@latest replace-tailwind-typography \
- pnpm: pnpm dlx @seed-design/codemod@latest replace-tailwind-typography \
- yarn: yarn dlx @seed-design/codemod@latest replace-tailwind-typography \
- bun: bun x @seed-design/codemod@latest replace-tailwind-typography \
```tsx title="basic.input.tsx"
// @ts-nocheck
export function BasicExample() {
return (
Primary Background
{formState.content}
Hover Primary Low Background
Focus Primary Low Background
Focus Primary Low Background
{description}
Text with prefix
Another text with prefix
Title with prefix
);
}
```
```tsx title="basic.output.tsx"
// @ts-nocheck
export function BasicExample() {
return (
Primary Background
{formState.content}
Hover Primary Low Background
Focus Primary Low Background
Focus Primary Low Background
{description}
Text with prefix
Another text with prefix
Title with prefix
);
}
```
replace-custom-imported-typography-variable \[#replace-custom-imported-typography-variable]
Sprout에서 SEED로 마이그레이션 시 import된 타이포그래피 변수를 변환합니다.
기능 \[#기능]
- 타이포그래피 관련 import 문을 찾아 변수명을 변환합니다.
- 변수가 사용된 모든 위치(템플릿 리터럴, 객체 속성 등)를 찾아 업데이트합니다.
- typography.mjs의 매핑 정보에 따라 Sprout 타이포그래피 변수를 SEED로 변환합니다.
- 같은 SEED 토큰으로 변환되는 중복 import를 제거합니다 (예: subtitle1Regular, bodyM1Regular가 모두 t5Regular로 변환되면 t5Regular는 한 번만 import).
- 별칭(alias)으로 import된 변수명은 유지합니다 (예: `bodyM1Regular as customTypo`에서 `t5Regular as customTypo`로 변환).
* npm: npx @seed-design/codemod@latest replace-custom-imported-typography-variable \
* pnpm: pnpm dlx @seed-design/codemod@latest replace-custom-imported-typography-variable \
* yarn: yarn dlx @seed-design/codemod@latest replace-custom-imported-typography-variable \
* bun: bun x @seed-design/codemod@latest replace-custom-imported-typography-variable \
```ts title="basic.input.ts"
// @ts-nocheck
import {
subtitle1Regular,
subtitle2Regular,
h4,
title1Bold,
title2Bold,
bodyL1Regular,
bodyL2Regular,
caption1Regular,
caption2Regular,
label5Regular,
} from '@src/constants/typography'
import { bodyM1Regular as customTypo } from '@karrot/typography'
const S_StoreRequestTitle = styled.h1`
${subtitle1Regular};
margin: 0 0 0.25rem;
color: ${vars.$scale.color.gray900};
text-align: center;
`
const S_StoreRequestText = styled.p`
${subtitle2Regular};
margin: 0;
color: ${vars.$scale.color.gray600};
text-align: center;
`
// 추가 테스트 케이스
// screenTitle 매핑 테스트
const S_ScreenHeader = styled.h1`
${h4};
margin-bottom: 1rem;
`
// 일반 매핑 테스트
const S_Title = styled.h2`
${title1Bold};
color: ${vars.$scale.color.gray900};
`
// 여러 테스트 케이스를 담은 컴포넌트
const Card = styled.div`
// title2Bold 매핑 테스트
h3 {
${title2Bold};
margin-bottom: 8px;
}
// bodyL1Regular 매핑 테스트
p.description {
${bodyL1Regular};
color: ${vars.$scale.color.gray800};
}
// 대체 토큰으로 매핑되는 케이스
p.content {
${bodyL2Regular};
margin: 0;
}
// 캡션 스타일
p.caption {
${caption1Regular};
color: ${vars.$scale.color.gray600};
}
// 작은 텍스트
small {
${caption2Regular};
color: ${vars.$scale.color.gray500};
}
// 라벨 스타일
label {
${label5Regular};
margin-right: 4px;
}
`
// 다른 모듈에서 가져온 타이포그래피 처리 (별칭 유지해야 함)
const S_CustomContent = styled.div`
${customTypo};
color: ${vars.$scale.color.gray700};
`
// 변수로 사용하는 경우
const titleStyle = title1Bold;
const textStyle = subtitle1Regular;
```
```ts title="basic.output.ts"
// @ts-nocheck
import {
t5Regular,
t4Regular,
t10Bold,
t9Bold,
t7Bold,
articleBody,
t3Regular,
t2Regular,
t1Regular,
} from '@src/constants/typography';
import { t5Regular as customTypo } from '@karrot/typography'
const S_StoreRequestTitle = styled.h1`
${t5Regular};
margin: 0 0 0.25rem;
color: ${vars.$scale.color.gray900};
text-align: center;
`
const S_StoreRequestText = styled.p`
${t4Regular};
margin: 0;
color: ${vars.$scale.color.gray600};
text-align: center;
`
// 추가 테스트 케이스
// screenTitle 매핑 테스트
const S_ScreenHeader = styled.h1`
${t10Bold};
margin-bottom: 1rem;
`
// 일반 매핑 테스트
const S_Title = styled.h2`
${t9Bold};
color: ${vars.$scale.color.gray900};
`
// 여러 테스트 케이스를 담은 컴포넌트
const Card = styled.div`
// title2Bold 매핑 테스트
h3 {
${t7Bold};
margin-bottom: 8px;
}
// bodyL1Regular 매핑 테스트
p.description {
${articleBody};
color: ${vars.$scale.color.gray800};
}
// 대체 토큰으로 매핑되는 케이스
p.content {
${t4Regular};
margin: 0;
}
// 캡션 스타일
p.caption {
${t3Regular};
color: ${vars.$scale.color.gray600};
}
// 작은 텍스트
small {
${t2Regular};
color: ${vars.$scale.color.gray500};
}
// 라벨 스타일
label {
${t1Regular};
margin-right: 4px;
}
`
// 다른 모듈에서 가져온 타이포그래피 처리 (별칭 유지해야 함)
const S_CustomContent = styled.div`
${customTypo};
color: ${vars.$scale.color.gray700};
`
// 변수로 사용하는 경우
const titleStyle = t9Bold;
const textStyle = t5Regular;
```
replace-custom-seed-design-vars \[#replace-custom-seed-design-vars]
- npm: npx @seed-design/codemod@latest replace-custom-seed-design-vars \
- pnpm: pnpm dlx @seed-design/codemod@latest replace-custom-seed-design-vars \
- yarn: yarn dlx @seed-design/codemod@latest replace-custom-seed-design-vars \
- bun: bun x @seed-design/codemod@latest replace-custom-seed-design-vars \
```ts title="basic.input.ts"
// @ts-nocheck
import { vars } from '@/shared/style/vars';
export const date = style({
...vars.typography.caption1Regular,
color: vars.color.gray600,
});
export const color1 = style({
color: vars.color.yellow500,
});
export const color2 = style({
color: vars.color.blue500,
});
export const color3 = style({
color: vars.color.red500,
});
export const title = style({
...vars.typography.bodyM1Bold,
color: vars.color.primary,
});
export const subtitle = style({
...vars.typography.bodyM2Regular,
color: vars.color.secondary,
});
// 배경색 케이스
export const cardBackground = style({
backgroundColor: vars.color.paperSheet,
padding: '16px',
});
// 테두리 케이스
export const divider = style({
borderTop: `1px solid ${vars.color.divider1}`,
marginTop: '8px',
marginBottom: '8px',
});
// 상태 색상 케이스
export const successMessage = style({
...vars.typography.bodyM1Bold,
color: vars.color.success,
backgroundColor: vars.color.successLow,
padding: '12px',
borderRadius: '4px',
});
export const warningMessage = style({
...vars.typography.bodyM1Bold,
color: vars.color.warning,
backgroundColor: vars.color.warningLow,
padding: '12px',
borderRadius: '4px',
});
export const errorMessage = style({
...vars.typography.bodyM1Bold,
color: vars.color.danger,
backgroundColor: vars.color.dangerLow,
padding: '12px',
borderRadius: '4px',
});
// 다양한 타이포그래피 케이스
export const heading = style({
...vars.typography.h4,
color: vars.color.inkText,
});
export const body = style({
...vars.typography.bodyL1Regular,
color: vars.color.inkTextLow,
});
export const label = style({
...vars.typography.label3Bold,
color: vars.color.grayActive,
});
// 알파 색상 케이스
export const overlay = style({
backgroundColor: vars.color.overlayDim,
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
});
// 복합 케이스와 상호작용 상태
export const button = style({
...vars.typography.label2Bold,
backgroundColor: vars.color.primary,
color: vars.color.onPrimary,
borderRadius: '4px',
padding: '8px 16px',
border: 'none',
cursor: 'pointer',
':hover': {
backgroundColor: vars.color.primaryHover,
},
':active': {
backgroundColor: vars.color.primaryPressed,
},
});
export const secondaryButton = style({
...vars.typography.label2Bold,
backgroundColor: vars.color.secondaryLow,
color: vars.color.secondary,
borderRadius: '4px',
padding: '8px 16px',
border: 'none',
cursor: 'pointer',
':hover': {
backgroundColor: vars.color.grayHover,
},
':active': {
backgroundColor: vars.color.grayPressed,
},
});
// 중첩 경로 케이스
export const floatingPanel = style({
backgroundColor: vars.color.paperFloating,
boxShadow: `0 2px 8px ${vars.color.overlayLow}`,
padding: '20px',
borderRadius: '8px',
});
export const dialogPanel = style({
backgroundColor: vars.color.paperDialog,
padding: '24px',
boxShadow: `0 4px 16px ${vars.color.overlayDim}`,
borderRadius: '12px',
});
// 정적 색상 케이스
export const staticColors = style({
color: vars.color.staticBlack,
backgroundColor: vars.color.staticWhite,
border: `1px solid ${vars.color.staticBlackAlpha200}`,
});
// 정적 색상 케이스 (명시적으로 static 프리픽스 테스트)
export const staticExplicitColors = style({
color: vars.color.staticBlack,
backgroundColor: vars.color.staticWhite,
boxShadow: `0 2px 4px ${vars.color.staticBlackAlpha500}`,
outline: `1px solid ${vars.color.staticBlackAlpha200}`,
});
export const specialBackground = style({
background: `linear-gradient(to bottom, ${vars.color.staticWhiteAlpha50}, ${vars.color.staticWhiteAlpha200})`,
backdropFilter: 'blur(8px)',
});
// 복합 스타일 및 다중 속성 케이스
export const multiProperty = style({
...vars.typography.title2Bold,
color: vars.color.accent,
borderLeft: `4px solid ${vars.color.accent}`,
borderRight: `4px solid ${vars.color.accent}`,
boxShadow: `inset 0 0 0 1px ${vars.color.divider2}, 0 2px 4px ${vars.color.overlayLow}`,
padding: '16px',
backgroundColor: vars.color.paperDefault,
});
// 믹스인 스타일 케이스
export const mixinStyle = {
primary: {
background: vars.color.primary,
color: vars.color.onPrimary,
},
secondary: {
background: vars.color.secondary,
},
accent: {
background: vars.color.accent,
color: vars.color.onGrayOverlay50,
},
};
// 특수 속성 및 스케일 색상 케이스
export const scaleColors = style({
fill: vars.color.carrot500,
stroke: vars.color.gray400,
stopColor: vars.color.blue400,
floodColor: vars.color.red400,
lightingColor: vars.color.green400,
});
// 특수 타이포그래피 케이스
export const specialTypographies = {
title: vars.typography.title1Bold,
subtitle: vars.typography.subtitle1Regular,
label: vars.typography.label5Regular,
caption: vars.typography.caption2Bold,
};
// 다양한 경로를 가진 컬러 토큰
export const colorContexts = style({
// 일반 색상
color: vars.color.primaryLow,
// 배경 색상
backgroundColor: vars.color.paperBackground,
// 테두리 색상
borderColor: vars.color.divider3,
// 호버 색상
':hover': {
backgroundColor: vars.color.primaryLowHover,
borderColor: vars.color.primaryLowActive,
},
});
// 복잡한 다중 경로와 중첩된 객체
export const nestedStyleObject = {
button: {
default: {
backgroundColor: vars.color.paperAccent,
color: vars.color.inkText,
border: `1px solid ${vars.color.carrot300}`,
},
hover: {
backgroundColor: vars.color.carrot100,
borderColor: vars.color.carrot400,
},
active: {
backgroundColor: vars.color.carrot200,
color: vars.color.inkTextLow,
},
},
panel: {
header: {
...vars.typography.subtitle2Bold,
color: vars.color.blue600,
borderBottom: `1px solid ${vars.color.blue200}`,
},
body: {
...vars.typography.bodyM2Regular,
color: vars.color.inkTextLow,
backgroundColor: vars.color.blue50,
},
footer: {
backgroundColor: vars.color.blue100,
borderTop: `1px solid ${vars.color.blue200}`,
},
},
};
// 테두리 속성 매핑 테스트
export const borderMapping = style({
// stroke 토큰으로 매핑될 속성
borderColor: vars.color.divider2,
// stroke 토큰이 없어서 fg 토큰으로 대체되는 케이스
borderTopColor: vars.color.primary,
borderBottomColor: vars.color.accent,
// stroke 또는 fg 토큰으로 대체
boxShadow: `0 0 0 1px ${vars.color.primary}, 0 2px 4px ${vars.color.overlayLow}`,
// 복합 속성
border: `1px solid ${vars.color.secondary}`,
});
```
```ts title="basic.output.ts"
// @ts-nocheck
import { vars } from "@/shared/style/vars";
export const date = style({
...vars.typography.t3Regular,
color: vars.color.palette.gray700,
});
export const color1 = style({
color: vars.color.palette.yellow700,
});
export const color2 = style({
color: vars.color.palette.blue600,
});
export const color3 = style({
color: vars.color.palette.red700,
});
export const title = style({
...vars.typography.t5Bold,
color: vars.color.fg.brand,
});
export const subtitle = style({
...vars.typography.t4Regular,
color: vars.color.palette.gray900,
});
// 배경색 케이스
export const cardBackground = style({
backgroundColor: vars.color.bg.layerFloating,
padding: "16px",
});
// 테두리 케이스
export const divider = style({
borderTop: `1px solid ${vars.color.stroke.neutralSubtle}`,
marginTop: "8px",
marginBottom: "8px",
});
// 상태 색상 케이스
export const successMessage = style({
...vars.typography.t5Bold,
color: vars.color.fg.positive,
backgroundColor: vars.color.bg.positiveWeak,
padding: "12px",
borderRadius: "4px",
});
export const warningMessage = style({
...vars.typography.t5Bold,
color: vars.color.bg.warningSolid,
backgroundColor: vars.color.bg.warningWeak,
padding: "12px",
borderRadius: "4px",
});
export const errorMessage = style({
...vars.typography.t5Bold,
color: vars.color.fg.critical,
backgroundColor: vars.color.bg.criticalWeak,
padding: "12px",
borderRadius: "4px",
});
// 다양한 타이포그래피 케이스
export const heading = style({
...vars.typography.t10Bold,
color: vars.color.fg.neutral,
});
export const body = style({
...vars.typography.articleBody,
color: vars.color.fg.neutralSubtle,
});
export const label = style({
...vars.typography.t4Bold,
color: vars.color.fg.neutralMuted,
});
// 알파 색상 케이스
export const overlay = style({
backgroundColor: vars.color.bg.overlay,
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
});
// 복합 케이스와 상호작용 상태
export const button = style({
...vars.typography.t5Bold,
backgroundColor: vars.color.bg.brandSolid,
color: vars.color.palette.staticWhite,
borderRadius: "4px",
padding: "8px 16px",
border: "none",
cursor: "pointer",
":hover": {
backgroundColor: vars.color.bg.brandSolidPressed,
},
":active": {
backgroundColor: vars.color.bg.brandSolidPressed,
},
});
export const secondaryButton = style({
...vars.typography.t5Bold,
backgroundColor: vars.color.bg.neutralWeak,
color: vars.color.palette.gray900,
borderRadius: "4px",
padding: "8px 16px",
border: "none",
cursor: "pointer",
":hover": {
backgroundColor: vars.color.bg.neutralWeakPressed,
},
":active": {
backgroundColor: vars.color.bg.neutralWeakPressed,
},
});
// 중첩 경로 케이스
export const floatingPanel = style({
backgroundColor: vars.color.bg.layerFloating,
boxShadow: `0 2px 8px ${vars.color.bg.overlayMuted}`,
padding: "20px",
borderRadius: "8px",
});
export const dialogPanel = style({
backgroundColor: vars.color.bg.layerFloating,
padding: "24px",
boxShadow: `0 4px 16px ${vars.color.bg.overlay}`,
borderRadius: "12px",
});
// 정적 색상 케이스
export const staticColors = style({
color: vars.color.palette.staticBlack,
backgroundColor: vars.color.palette.staticWhite,
border: `1px solid ${vars.color.palette.staticBlackAlpha200}`,
});
// 정적 색상 케이스 (명시적으로 static 프리픽스 테스트)
export const staticExplicitColors = style({
color: vars.color.palette.staticBlack,
backgroundColor: vars.color.palette.staticWhite,
boxShadow: `0 2px 4px ${vars.color.palette.staticBlackAlpha500}`,
outline: `1px solid ${vars.color.palette.staticBlackAlpha200}`,
});
export const specialBackground = style({
background: `linear-gradient(to bottom, ${vars.color.palette.staticWhiteAlpha50}, ${vars.color.palette.staticWhiteAlpha200})`,
backdropFilter: "blur(8px)",
});
// 복합 스타일 및 다중 속성 케이스
export const multiProperty = style({
...vars.typography.t7Bold,
color: vars.color.fg.informative,
borderLeft: `4px solid ${vars.color.fg.informative}`,
borderRight: `4px solid ${vars.color.fg.informative}`,
boxShadow: `inset 0 0 0 1px ${vars.color.stroke.neutral}, 0 2px 4px ${vars.color.bg.overlayMuted}`,
padding: "16px",
backgroundColor: vars.color.bg.layerDefault,
});
// 믹스인 스타일 케이스
export const mixinStyle = {
primary: {
background: vars.color.bg.brandSolid,
color: vars.color.palette.staticWhite,
},
secondary: {
background: vars.color.palette.gray900,
},
accent: {
background: vars.color.bg.informativeSolid,
color: vars.color.stroke.onImage,
},
};
// 특수 속성 및 스케일 색상 케이스
export const scaleColors = style({
fill: vars.color.palette.carrot600,
stroke: vars.color.palette.gray500,
stopColor: vars.color.palette.blue400,
floodColor: vars.color.palette.red600,
lightingColor: vars.color.palette.green500,
});
// 특수 타이포그래피 케이스
export const specialTypographies = {
title: vars.typography.t9Bold,
subtitle: vars.typography.t5Regular,
label: vars.typography.t1Regular,
caption: vars.typography.t2Bold,
};
// 다양한 경로를 가진 컬러 토큰
export const colorContexts = style({
// 일반 색상
color: vars.color.palette.carrot100,
// 배경 색상
backgroundColor: vars.color.bg.layerBasement,
// 테두리 색상
borderColor: vars.color.palette.gray400,
// 호버 색상
":hover": {
backgroundColor: vars.color.palette.carrot200,
borderColor: vars.color.palette.carrot100,
},
});
// 복잡한 다중 경로와 중첩된 객체
export const nestedStyleObject = {
button: {
default: {
backgroundColor: vars.color.palette.carrot100,
color: vars.color.fg.neutral,
border: `1px solid ${vars.color.palette.carrot400}`,
},
hover: {
backgroundColor: vars.color.palette.carrot200,
borderColor: vars.color.palette.carrot500,
},
active: {
backgroundColor: vars.color.palette.carrot300,
color: vars.color.fg.neutralSubtle,
},
},
panel: {
header: {
...vars.typography.t4Bold,
color: vars.color.palette.blue600,
borderBottom: `1px solid ${vars.color.palette.blue300}`,
},
body: {
...vars.typography.t4Regular,
color: vars.color.fg.neutralSubtle,
backgroundColor: vars.color.palette.blue100,
},
footer: {
backgroundColor: vars.color.palette.blue200,
borderTop: `1px solid ${vars.color.palette.blue300}`,
},
},
};
// 테두리 속성 매핑 테스트
export const borderMapping = style({
// stroke 토큰으로 매핑될 속성
borderColor: vars.color.stroke.neutral,
// stroke 토큰이 없어서 fg 토큰으로 대체되는 케이스
borderTopColor: vars.color.fg.brand,
borderBottomColor: vars.color.fg.informative,
// stroke 또는 fg 토큰으로 대체
boxShadow: `0 0 0 1px ${vars.color.bg.brandSolid}, 0 2px 4px ${vars.color.bg.overlayMuted}`,
// 복합 속성
border: `1px solid ${vars.color.palette.gray900}`,
});
```
replace-stitches-styled-typography \[#replace-stitches-styled-typography]
- npm: npx @seed-design/codemod@latest replace-stitches-styled-typography \
- pnpm: pnpm dlx @seed-design/codemod@latest replace-stitches-styled-typography \
- yarn: yarn dlx @seed-design/codemod@latest replace-stitches-styled-typography \
- bun: bun x @seed-design/codemod@latest replace-stitches-styled-typography \
```tsx title="basic.input.tsx"
// @ts-nocheck
const ContentText = styled('p', {
...userSelectText,
overflow: 'hidden',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
color: '$gray900',
variants: {
size: {
medium: {
$text: 'bodyM1Regular',
},
large: {
$text: 'bodyL1Regular',
},
xlarge: {
$text: "h4",
},
},
},
});
const ContentText2 = styled('p', {
...userSelectText,
overflow: 'hidden',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
color: '$gray900',
variants: {
size: {
medium: {
$typography: 'bodyM1Regular',
},
large: {
$typography: 'bodyL1Regular',
},
xlarge: {
$typography: "h4",
},
},
},
});
```
```tsx title="basic.output.tsx"
// @ts-nocheck
const ContentText = styled('p', {
...userSelectText,
overflow: 'hidden',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
color: '$gray900',
variants: {
size: {
medium: {
$text: "t5Regular",
},
large: {
$text: "articleBody",
},
xlarge: {
$text: "t10Bold",
},
},
},
});
const ContentText2 = styled('p', {
...userSelectText,
overflow: 'hidden',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
color: '$gray900',
variants: {
size: {
medium: {
$typography: "t5Regular",
},
large: {
$typography: "articleBody",
},
xlarge: {
$typography: "t10Bold",
},
},
},
});
```
replace-stitches-theme-color \[#replace-stitches-theme-color]
- npm: npx @seed-design/codemod@latest replace-stitches-theme-color \
- pnpm: pnpm dlx @seed-design/codemod@latest replace-stitches-theme-color \
- yarn: yarn dlx @seed-design/codemod@latest replace-stitches-theme-color \
- bun: bun x @seed-design/codemod@latest replace-stitches-theme-color \
```tsx title="basic.input.tsx"
// @ts-nocheck
import { theme } from "@src/stitches/stitches.config";
const semanticColors = {
primary: theme.colors["primary-semantic"].computedValue,
onPrimary: theme.colors["onPrimary-semantic"].computedValue,
primaryLow: theme.colors["primaryLow-semantic"].computedValue,
success: theme.colors["success-semantic"].computedValue,
warning: theme.colors["warning-semantic"].computedValue,
danger: theme.colors["danger-semantic"].computedValue,
paperDefault: theme.colors["paperDefault-semantic"].computedValue,
paperContents: theme.colors["paperContents-semantic"].computedValue,
paperDialog: theme.colors["paperDialog-semantic"].computedValue,
inkText: theme.colors["inkText-semantic"].computedValue,
inkTextLow: theme.colors["inkTextLow-semantic"].computedValue,
divider1: theme.colors["divider1-semantic"].computedValue,
divider2: theme.colors["divider2-semantic"].computedValue,
overlayDim: theme.colors["overlayDim-semantic"].computedValue,
accent: theme.colors["accent-semantic"].computedValue,
};
const scaleColors = {
gray00: theme.colors.gray00.computedValue,
gray100: theme.colors.gray100.computedValue,
gray200: theme.colors.gray200.computedValue,
gray300: theme.colors.gray300.computedValue,
gray400: theme.colors.gray400.computedValue,
gray500: theme.colors.gray500.computedValue,
gray600: theme.colors.gray600.computedValue,
gray700: theme.colors.gray700.computedValue,
gray800: theme.colors.gray800.computedValue,
gray900: theme.colors.gray900.computedValue,
carrot100: theme.colors.carrot100.computedValue,
carrot500: theme.colors.carrot500.computedValue,
carrot900: theme.colors.carrot900.computedValue,
carrotAlpha50: theme.colors.carrotAlpha50.computedValue,
carrotAlpha100: theme.colors.carrotAlpha100.computedValue,
carrotAlpha200: theme.colors.carrotAlpha200.computedValue,
blue300: theme.colors.blue300.computedValue,
blue600: theme.colors.blue600.computedValue,
red500: theme.colors.red500.computedValue,
};
const staticColors = {
white: theme.colors["white-static"].computedValue,
black: theme.colors["black-static"].computedValue,
blackAlpha200: theme.colors["blackAlpha200-static"].computedValue,
whiteAlpha200: theme.colors["whiteAlpha200-static"].computedValue,
gray900: theme.colors["gray900-static"].computedValue,
carrot50: theme.colors["carrot50-static"].computedValue,
carrot800: theme.colors["carrot800-static"].computedValue,
blue50: theme.colors["blue50-static"].computedValue,
blue800: theme.colors["blue800-static"].computedValue,
red50: theme.colors["red50-static"].computedValue,
red800: theme.colors["red800-static"].computedValue,
};
const Component = () => {
return (
버튼
안내사항
중요
);
};
const Dialog = ({ isOpen, onClose }) => {
const styles = {
overlay: {
backgroundColor: theme.colors["overlayDim-semantic"].computedValue,
},
container: {
backgroundColor: theme.colors["paperDialog-semantic"].computedValue,
borderColor: theme.colors.gray300.computedValue,
},
header: {
borderBottom: `1px solid ${theme.colors["divider1-semantic"].computedValue}`,
},
title: {
color: theme.colors.gray900.computedValue,
},
content: {
color: theme.colors.gray700.computedValue,
backgroundColor: theme.colors["paperContents-semantic"].computedValue,
},
footer: {
borderTop: `1px solid ${theme.colors["divider2-semantic"].computedValue}`,
},
closeButton: {
color: theme.colors.gray500.computedValue,
},
submitButton: {
backgroundColor: theme.colors.blue600.computedValue,
color: theme.colors["white-static"].computedValue,
},
};
return isOpen ? (
) : null;
};
export { semanticColors, scaleColors, staticColors, Component, Dialog };
```
```tsx title="basic.output.tsx"
// @ts-nocheck
import { theme } from "@src/stitches/stitches.config";
const semanticColors = {
primary: theme.colors["bg-brand-solid"].computedValue,
onPrimary: theme.colors["palette-static-white"].computedValue,
primaryLow: theme.colors["palette-carrot-100"].computedValue,
success: theme.colors["bg-positive-solid"].computedValue,
warning: theme.colors["bg-warning-solid"].computedValue,
danger: theme.colors["bg-critical-solid"].computedValue,
paperDefault: theme.colors["bg-layer-default"].computedValue,
paperContents: theme.colors["bg-layer-fill"].computedValue,
paperDialog: theme.colors["bg-layer-floating"].computedValue,
inkText: theme.colors["fg-neutral"].computedValue,
inkTextLow: theme.colors["fg-neutral-subtle"].computedValue,
divider1: theme.colors["stroke-neutral-subtle"].computedValue,
divider2: theme.colors["stroke-neutral-muted"].computedValue,
overlayDim: theme.colors["bg-overlay"].computedValue,
accent: theme.colors["bg-informative-solid"].computedValue,
};
const scaleColors = {
gray00: theme.colors["palette-gray-00"].computedValue,
gray100: theme.colors["palette-gray-200"].computedValue,
gray200: theme.colors["palette-gray-300"].computedValue,
gray300: theme.colors["palette-gray-400"].computedValue,
gray400: theme.colors["palette-gray-500"].computedValue,
gray500: theme.colors["palette-gray-600"].computedValue,
gray600: theme.colors["palette-gray-700"].computedValue,
gray700: theme.colors["palette-gray-800"].computedValue,
gray800: theme.colors["palette-gray-900"].computedValue,
gray900: theme.colors["palette-gray-1000"].computedValue,
carrot100: theme.colors["palette-carrot-200"].computedValue,
carrot500: theme.colors["palette-carrot-600"].computedValue,
carrot900: theme.colors["palette-carrot-800"].computedValue,
carrotAlpha50: theme.colors["palette-carrot-100"].computedValue,
carrotAlpha100: theme.colors["palette-carrot-200"].computedValue,
carrotAlpha200: theme.colors["palette-carrot-200"].computedValue,
blue300: theme.colors["palette-blue-400"].computedValue,
blue600: theme.colors["palette-blue-600"].computedValue,
red500: theme.colors["palette-red-700"].computedValue,
};
const staticColors = {
white: theme.colors["palette-static-white"].computedValue,
black: theme.colors["palette-static-black"].computedValue,
blackAlpha200: theme.colors["palette-static-black-alpha-500"].computedValue,
whiteAlpha200: theme.colors["palette-static-white-alpha-300"].computedValue,
gray900: theme.colors["palette-static-black"].computedValue,
carrot50: theme.colors["palette-carrot-100"].computedValue,
carrot800: theme.colors["palette-carrot-700"].computedValue,
blue50: theme.colors["palette-blue-100"].computedValue,
blue800: theme.colors["palette-blue-700"].computedValue,
red50: theme.colors["palette-red-100"].computedValue,
red800: theme.colors["palette-red-700"].computedValue,
};
const Component = () => {
return (
버튼
안내사항
중요
);
};
const Dialog = ({ isOpen, onClose }) => {
const styles = {
overlay: {
backgroundColor: theme.colors["bg-overlay"].computedValue,
},
container: {
backgroundColor: theme.colors["bg-layer-floating"].computedValue,
borderColor: theme.colors["palette-gray-400"].computedValue,
},
header: {
borderBottom: `1px solid ${theme.colors["stroke-neutral-subtle"].computedValue}`,
},
title: {
color: theme.colors["palette-gray-1000"].computedValue,
},
content: {
color: theme.colors["palette-gray-800"].computedValue,
backgroundColor: theme.colors["bg-layer-fill"].computedValue,
},
footer: {
borderTop: `1px solid ${theme.colors["stroke-neutral-muted"].computedValue}`,
},
closeButton: {
color: theme.colors["palette-gray-600"].computedValue,
},
submitButton: {
backgroundColor: theme.colors["palette-blue-600"].computedValue,
color: theme.colors["palette-static-white"].computedValue,
},
};
return isOpen ? (
) : null;
};
export { semanticColors, scaleColors, staticColors, Component, Dialog };
```
replace-css-seed-design-color-variable \[#replace-css-seed-design-color-variable]
- npm: npx @seed-design/codemod@latest replace-css-seed-design-color-variable \
- pnpm: pnpm dlx @seed-design/codemod@latest replace-css-seed-design-color-variable \
- yarn: yarn dlx @seed-design/codemod@latest replace-css-seed-design-color-variable \
- bun: bun x @seed-design/codemod@latest replace-css-seed-design-color-variable \
```css title="basic.input.css"
:root[data-seed] {
--seed-alert-dialog-backdrop-background: var(--seed-semantic-color-overlay-dim);
--seed-alert-dialog-content-background: var(--seed-semantic-color-paper-dialog);
--seed-alert-dialog-title-color: var(--seed-scale-color-gray-900);
--seed-alert-dialog-description-color: var(--seed-scale-color-gray-900);
--seed-box-button-focus-border: 2px solid var(--seed-scale-color-blue-600);
box-shadow: 0 0 0 2px var(--seed-scale-color-blue-600);
background: linear-gradient(to bottom, var(--seed-scale-color-blue-600), var(--seed-scale-color-blue-700));
}
```
```css title="basic.output.css"
:root[data-seed] {
--seed-alert-dialog-backdrop-background: var(--seed-color-bg-overlay);
--seed-alert-dialog-content-background: var(--seed-color-bg-layer-floating);
--seed-alert-dialog-title-color: var(--seed-color-palette-gray-1000);
--seed-alert-dialog-description-color: var(--seed-color-palette-gray-1000);
--seed-box-button-focus-border: 2px solid var(--seed-color-palette-blue-600);
box-shadow: 0 0 0 2px var(--seed-color-palette-blue-600);
background: linear-gradient(to bottom, var(--seed-color-palette-blue-600), var(--seed-color-palette-blue-800));
}
```
replace-react-icon \[#replace-react-icon]
- npm: npx @seed-design/codemod@latest replace-react-icon \
- pnpm: pnpm dlx @seed-design/codemod@latest replace-react-icon \
- yarn: yarn dlx @seed-design/codemod@latest replace-react-icon \
- bun: bun x @seed-design/codemod@latest replace-react-icon \
```tsx title="basic.input.tsx"
// @ts-nocheck
import {
IconSellRegular,
IconListFill,
IconAddFill as AddIconAlias,
} from "@seed-design/react-icon";
import IconSellFill from "@seed-design/react-icon/IconSellFill";
import IconAddThin from '@karrotmarket/karrot-ui-icon/lib/react/IconAddThin';
import IconCloseThin from '@karrotmarket/karrot-ui-icon/lib/react/IconCloseThin';
import IconCarRegular from '@karrotmarket/karrot-ui-icon/lib/react/IconCarRegular';
function App() {
console.log(IconSellRegular);
return (
<>
>
);
}
```
```tsx title="basic.output.tsx"
// @ts-nocheck
import {
IconPlusSquareLine,
IconDothorizline3VerticalFill,
IconPlusFill as AddIconAlias,
} from "@karrotmarket/react-monochrome-icon";
import IconPlusSquareFill from "@karrotmarket/react-monochrome-icon/IconPlusSquareFill";
import IconPlusLine from "@karrotmarket/react-monochrome-icon/IconPlusLine";
import IconXmarkLine from "@karrotmarket/react-monochrome-icon/IconXmarkLine";
import IconCarFrontsideLine from "@karrotmarket/react-monochrome-icon/IconCarFrontsideLine";
function App() {
console.log(IconPlusSquareLine);
return (
<>
>
);
}
```
replace-seed-design-token-vars \[#replace-seed-design-token-vars]
- npm: npx @seed-design/codemod@latest replace-seed-design-token-vars \
- pnpm: pnpm dlx @seed-design/codemod@latest replace-seed-design-token-vars \
- yarn: yarn dlx @seed-design/codemod@latest replace-seed-design-token-vars \
- bun: bun x @seed-design/codemod@latest replace-seed-design-token-vars \
```tsx title="basic.input.tsx"
import { vars as legacyVars } from "@seed-design/design-token";
import { vars } from "@seed-design/css/vars";
import { vars as typoVars } from "@seed-design/css/vars/component/typography";
const color = vars.$color.palette.gray600;
const typography = typoVars.textStyleT5Bold.enabled.root;
const legacyColor = legacyVars.$static.color.staticWhiteAlpha50;
```
```tsx title="basic.output.tsx"
import { vars as legacyVars } from "@seed-design/design-token";
import { vars } from "@seed-design/css/vars";
import { vars as typoVars } from "@seed-design/css/vars/component/typography";
const color = vars.$color.palette.gray600;
const typography = typoVars.textStyleT5Bold.enabled.root;
const legacyColor = legacyVars.$static.color.staticWhiteAlpha50;
```
replace-semantic-stroke-color \[#replace-semantic-stroke-color]
시맨틱 stroke 컬러 토큰을 v0.2.0에 맞춰 마이그레이션하는 transform입니다.
목적 \[#목적]
@seed-design/css v0.2.0에서 업데이트된 시맨틱 stroke 컬러 토큰을 자동으로 마이그레이션합니다.
이전 버전의 stroke 토큰들을 새로운 네이밍 규칙에 맞게 변환합니다.
변환 매핑 \[#변환-매핑]
우선 순위 변환 (1차) \[#우선-순위-변환-1차]
⚠️ **중요**: 다음 변환은 반드시 다른 변환보다 먼저 처리됩니다.
| 기존 토큰 | 신규 토큰 | 비고 |
| --------------------------------- | ---------------------------------- | ------------------ |
| `vars.$color.stroke.neutralMuted` | `vars.$color.stroke.neutralSubtle` | 가장 먼저 마이그레이션해야 합니다 |
일반 변환 (2차) \[#일반-변환-2차]
| 기존 토큰 | 신규 토큰 |
| --------------------------------- | ------------------------------------ |
| `vars.$color.stroke.onImage` | `vars.$color.stroke.neutralSubtle` |
| `vars.$color.stroke.neutral` | `vars.$color.stroke.neutralMuted` |
| `vars.$color.stroke.fieldFocused` | `vars.$color.stroke.neutralContrast` |
| `vars.$color.stroke.control` | `vars.$color.stroke.neutralWeak` |
| `vars.$color.stroke.field` | `vars.$color.stroke.neutralWeak` |
| `vars.$color.stroke.brand` | `vars.$color.stroke.brandWeak` |
| `vars.$color.stroke.positive` | `vars.$color.stroke.positiveWeak` |
| `vars.$color.stroke.informative` | `vars.$color.stroke.informativeWeak` |
| `vars.$color.stroke.warning` | `vars.$color.stroke.warningWeak` |
| `vars.$color.stroke.critical` | `vars.$color.stroke.criticalWeak` |
지원하는 파일 형식 \[#지원하는-파일-형식]
- TypeScript (`.ts`)
- TypeScript JSX (`.tsx`)
변환 예시 \[#변환-예시]
기본 사용 \[#기본-사용]
```typescript
// Before
import { vars } from "@seed-design/css/vars";
const borderColor = vars.$color.stroke.neutralMuted;
const outlineColor = vars.$color.stroke.neutral;
// After
import { vars } from "@seed-design/css/vars";
const borderColor = vars.$color.stroke.neutralSubtle;
const outlineColor = vars.$color.stroke.neutralMuted;
```
React 컴포넌트에서의 사용 \[#react-컴포넌트에서의-사용]
```tsx
// Before
const Component = () => (
);
// After
const Component = () => (
);
```
우선 순위의 중요성 \[#우선-순위의-중요성]
이 transform은 **2-pass 방식**으로 동작합니다:
1. **1차 변환**: `neutralMuted` → `neutralSubtle`을 먼저 처리
2. **2차 변환**: `neutral` → `neutralMuted` 등 나머지 변환들을 처리
이 순서가 중요한 이유는 연쇄 변환을 방지하기 위해서입니다.
만약 순서가 바뀌면 `neutralMuted`가 `neutralSubtle`로 변환되기 전에
`neutral`이 `neutralMuted`로 변환될 수 있어 예상치 못한 결과가 나올 수 있습니다.
변환되지 않는 토큰들 \[#변환되지-않는-토큰들]
다음 토큰들은 이미 SEED 형식이므로 변환되지 않습니다:
- `vars.$color.stroke.brandWeak`
- `vars.$color.stroke.brandSolid`
- `vars.$color.stroke.neutralSubtle`
- `vars.$color.stroke.neutralContrast`
- `vars.$color.stroke.neutralWeak`
- 기타 `-weak`, `-solid` 접미사를 가진 토큰들
로깅 \[#로깅]
transform 실행 시 다음과 같은 로그가 생성됩니다:
- `.report/replace-semantic-stroke-color-success.log`: 성공한 변환들
- `.report/replace-semantic-stroke-color-issues.log`: 경고 및 실패
- `.report/replace-semantic-stroke-color-debug.log`: 디버그 정보
* npm: npx @seed-design/codemod@latest replace-semantic-stroke-color \
* pnpm: pnpm dlx @seed-design/codemod@latest replace-semantic-stroke-color \
* yarn: yarn dlx @seed-design/codemod@latest replace-semantic-stroke-color \
* bun: bun x @seed-design/codemod@latest replace-semantic-stroke-color \
```tsx title="basic.input.tsx"
// @ts-nocheck
import React from "react";
import { vars } from "@seed-design/css/vars";
// 인라인 스타일에서 stroke 토큰 사용
const Component1 = () => (
);
// 객체 스타일에서 사용
const styles = {
container: {
borderColor: vars.$color.stroke.control,
border: `1px solid ${vars.$color.stroke.field}`,
},
button: {
borderColor: vars.$color.stroke.brand,
outlineColor: vars.$color.stroke.positive,
},
input: {
borderColor: vars.$color.stroke.informative,
"&:focus": {
borderColor: vars.$color.stroke.warning,
},
},
error: {
borderColor: vars.$color.stroke.critical,
},
};
// 삼항 연산자에서 사용
const ConditionalComponent = ({ isActive }: { isActive: boolean }) => (
);
// 새로운 토큰들 (변경되지 않아야 함)
const NewTokensComponent = () => (
);
```
```tsx title="basic.output.tsx"
// @ts-nocheck
import React from "react";
import { vars } from "@seed-design/css/vars";
// 인라인 스타일에서 stroke 토큰 사용
const Component1 = () => (
);
// 객체 스타일에서 사용
const styles = {
container: {
borderColor: vars.$color.stroke.neutralWeak,
border: `1px solid ${vars.$color.stroke.neutralWeak}`,
},
button: {
borderColor: vars.$color.stroke.brandWeak,
outlineColor: vars.$color.stroke.positiveWeak,
},
input: {
borderColor: vars.$color.stroke.informativeWeak,
"&:focus": {
borderColor: vars.$color.stroke.warningWeak,
},
},
error: {
borderColor: vars.$color.stroke.criticalWeak,
},
};
// 삼항 연산자에서 사용
const ConditionalComponent = ({ isActive }: { isActive: boolean }) => (
);
// 새로운 토큰들 (변경되지 않아야 함)
const NewTokensComponent = () => (
);
```
replace-custom-seed-design-text-component \[#replace-custom-seed-design-text-component]
- npm: npx @seed-design/codemod@latest replace-custom-seed-design-text-component \
- pnpm: pnpm dlx @seed-design/codemod@latest replace-custom-seed-design-text-component \
- yarn: yarn dlx @seed-design/codemod@latest replace-custom-seed-design-text-component \
- bun: bun x @seed-design/codemod@latest replace-custom-seed-design-text-component \
```tsx title="basic.input.tsx"
// @ts-nocheck
import Text from 'components/Base/Text';
const Component = () => {
return (
{/* 기존 variant prop 케이스 */}
광고 노출 기준
앱 내 최근 활동 이력을 분석하여 이용자의 관심사와 관련성이 높은 게시글을 노출해요.
{/* typography prop + color prop 케이스 */}
앱 내 최근 활동 이력을 분석하여 이용자의 관심사와 관련성이 높은 게시글을 노출해요.
지역 선택하기
지표별 성과
{/* 다양한 typography prop 형태 */}
접두사 없는 토큰
또 다른 토큰
{/* 다양한 color prop 형태 */}
시맨틱 컬러
스태틱 컬러
textColor prop
{/* 조건부 표현식 케이스 */}
조건부 variant
조건부 color
{/* JSX 표현식 컨테이너 케이스 */}
표현식 컨테이너
);
};
```
```tsx title="basic.output.tsx"
// @ts-nocheck
import { Text } from "@seed-design/react";
const Component = () => {
return (
{/* 기존 variant prop 케이스 */}
광고 노출 기준
앱 내 최근 활동 이력을 분석하여 이용자의 관심사와 관련성이 높은 게시글을 노출해요.
{/* typography prop + color prop 케이스 */}
앱 내 최근 활동 이력을 분석하여 이용자의 관심사와 관련성이 높은 게시글을 노출해요.
지역 선택하기
지표별 성과
{/* 다양한 typography prop 형태 */}
접두사 없는 토큰
또 다른 토큰
{/* 다양한 color prop 형태 */}
시맨틱 컬러
스태틱 컬러
textColor prop
{/* 조건부 표현식 케이스 */}
조건부 variant
조건부 color
{/* JSX 표현식 컨테이너 케이스 */}
표현식 컨테이너
);
};
```
replace-seed-design-token-typography-classname \[#replace-seed-design-token-typography-classname]
- npm: npx @seed-design/codemod@latest replace-seed-design-token-typography-classname \
- pnpm: pnpm dlx @seed-design/codemod@latest replace-seed-design-token-typography-classname \
- yarn: yarn dlx @seed-design/codemod@latest replace-seed-design-token-typography-classname \
- bun: bun x @seed-design/codemod@latest replace-seed-design-token-typography-classname \
```tsx title="basic.input.tsx"
// @ts-nocheck
import { classNames } from "@seed-design/design-token";
import { style } from "@vanilla-extract/css";
const typography = {
one: classNames.$semantic.typography.title2Regular,
two: classNames.$semantic.typography.label3Regular,
three: classNames.$semantic.typography.label5Regular,
four: classNames.$semantic.typography.label6Regular,
}
export const footer = style([
classNames.$semantic.typography.caption1Regular,
{
padding: "16px",
marginTop: "12px",
color: vars.$scale.color.gray700,
backgroundColor: vars.$semantic.color.paperContents,
},
]);
```
```tsx title="basic.output.tsx"
// @ts-nocheck
import { text } from "@seed-design/css/recipes/text";
import { style } from "@vanilla-extract/css";
const typography = {
one: text({ textStyle: "t7Regular" }),
two: text({ textStyle: "t4Regular" }),
three: text({ textStyle: "t1Regular" }),
four: text({ textStyle: "t1Regular" }),
}
export const footer = style([
text({ textStyle: "t3Regular" }),
{
padding: "16px",
marginTop: "12px",
color: vars.$scale.color.gray700,
backgroundColor: vars.$semantic.color.paperContents,
},
]);
```
replace-stitches-styled-color \[#replace-stitches-styled-color]
Stitches로 스타일링된 컴포넌트의 색상을 SEED 형식으로 변환해요.
변환 내용 \[#변환-내용]
- Stitches `styled()` 함수에서 사용된 Sprout 색상 토큰을 SEED 색상 토큰으로 변환합니다.
- 모든 색상 관련 스타일 속성(color, backgroundColor, borderColor 등)을 지원합니다.
- 상태 변이(variants)에 적용된 색상 변경도 함께 변환합니다.
대상 파일 \[#대상-파일]
- `.tsx`, `.jsx`, `.ts`, `.js` 파일에서 Stitches 스타일링 사용 부분
주의사항 \[#주의사항]
- 기존 코드의 구조와 포맷을 최대한 유지하면서 색상 값만 변경합니다.
- 커스텀 색상 값이나 CSS 변수는 변환하지 않습니다.
* npm: npx @seed-design/codemod@latest replace-stitches-styled-color \
* pnpm: pnpm dlx @seed-design/codemod@latest replace-stitches-styled-color \
* yarn: yarn dlx @seed-design/codemod@latest replace-stitches-styled-color \
* bun: bun x @seed-design/codemod@latest replace-stitches-styled-color \
```tsx title="basic.input.tsx"
// @ts-nocheck
const SemanticColorTestComponent = styled("div", {
// Semantic Color 테스트
color: "$primary-semantic",
color: "$onPrimary-semantic",
color: "$primaryLow-semantic",
color: "$secondary-semantic",
color: "$secondaryLow-semantic",
color: "$success-semantic",
color: "$successLow-semantic",
color: "$warning-semantic",
color: "$warningLow-semantic",
color: "$danger-semantic",
color: "$dangerLow-semantic",
color: "$overlayDim-semantic",
color: "$overlayLow-semantic",
color: "$paperSheet-semantic",
color: "$paperDialog-semantic",
color: "$paperFloating-semantic",
color: "$paperContents-semantic",
color: "$paperDefault-semantic",
color: "$paperBackground-semantic",
color: "$paperAccent-semantic",
color: "$primaryHover-semantic",
color: "$primaryPressed-semantic",
color: "$primaryLowHover-semantic",
color: "$primaryLowActive-semantic",
color: "$primaryLowPressed-semantic",
color: "$grayHover-semantic",
color: "$grayPressed-semantic",
color: "$onPrimaryOverlay50-semantic",
color: "$onPrimaryOverlay200-semantic",
color: "$onPrimaryLowOverlay50-semantic",
color: "$onPrimaryLowOverlay100-semantic",
color: "$onPrimaryLowOverlay200-semantic",
color: "$onGrayOverlay50-semantic",
color: "$onGrayOverlay100-semantic",
color: "$divider1-semantic",
color: "$divider2-semantic",
color: "$divider3-semantic",
color: "$accent-semantic",
color: "$inkText-semantic",
color: "$inkTextLow-semantic",
color: "$grayActive-semantic",
});
const ScaleColorTestComponent = styled("div", {
// Scale Color Gray 테스트
color: "$gray00",
color: "$gray50",
color: "$gray100",
color: "$gray200",
color: "$gray300",
color: "$gray400",
color: "$gray500",
color: "$gray600",
color: "$gray700",
color: "$gray800",
color: "$gray900",
color: "$grayAlpha50",
color: "$grayAlpha100",
color: "$grayAlpha200",
color: "$grayAlpha500",
// Scale Color Carrot 테스트
color: "$carrot50",
color: "$carrot100",
color: "$carrot200",
color: "$carrot300",
color: "$carrot400",
color: "$carrot500",
color: "$carrot600",
color: "$carrot700",
color: "$carrot800",
color: "$carrot900",
color: "$carrot950",
color: "$carrotAlpha50",
color: "$carrotAlpha100",
color: "$carrotAlpha200",
// Scale Color Blue 테스트
color: "$blue50",
color: "$blue100",
color: "$blue200",
color: "$blue300",
color: "$blue400",
color: "$blue500",
color: "$blue600",
color: "$blue700",
color: "$blue800",
color: "$blue900",
color: "$blue950",
color: "$blueAlpha50",
color: "$blueAlpha100",
color: "$blueAlpha200",
// Scale Color Red 테스트
color: "$red50",
color: "$red100",
color: "$red200",
color: "$red300",
color: "$red400",
color: "$red500",
color: "$red600",
color: "$red700",
color: "$red800",
color: "$red900",
color: "$red950",
color: "$redAlpha50",
color: "$redAlpha100",
color: "$redAlpha200",
// Scale Color Green 테스트
color: "$green50",
color: "$green100",
color: "$green200",
color: "$green300",
color: "$green400",
color: "$green500",
color: "$green600",
color: "$green700",
color: "$green800",
color: "$green900",
color: "$green950",
color: "$greenAlpha50",
color: "$greenAlpha100",
color: "$greenAlpha200",
// Scale Color Yellow 테스트
color: "$yellow50",
color: "$yellow100",
color: "$yellow200",
color: "$yellow300",
color: "$yellow400",
color: "$yellow500",
color: "$yellow600",
color: "$yellow700",
color: "$yellow800",
color: "$yellow900",
color: "$yellow950",
color: "$yellowAlpha50",
color: "$yellowAlpha100",
color: "$yellowAlpha200",
// Scale Color Purple 테스트
color: "$purple50",
color: "$purple100",
color: "$purple200",
color: "$purple300",
color: "$purple400",
color: "$purple500",
color: "$purple600",
color: "$purple700",
color: "$purple800",
color: "$purple900",
color: "$purple950",
});
const StaticColorTestComponent = styled("div", {
// Static Color 테스트
color: "$black-static",
color: "$white-static",
color: "$gray900-static",
color: "$carrot50-static",
color: "$carrot800-static",
color: "$green50-static",
color: "$green800-static",
color: "$yellow50-static",
color: "$yellow800-static",
color: "$red50-static",
color: "$red800-static",
color: "$blue50-static",
color: "$blue800-static",
color: "$blackAlpha200-static",
color: "$blackAlpha500-static",
color: "$whiteAlpha50-static",
color: "$whiteAlpha200-static",
});
// 복합 속성 테스트
const ComplexPropertyTestComponent = styled("div", {
border: "1px solid $gray700",
boxShadow: "0 0 10px $overlayDim-semantic",
outline: "2px solid $accent-semantic",
textDecoration: "underline $danger-semantic",
// 네스팅된 속성 테스트
"&:before": {
borderBottom: "1px solid $divider1-semantic",
background: "$paperContents-semantic",
},
// 변형 테스트
variants: {
theme: {
primary: {
background: "$primary-semantic",
color: "$onPrimary-semantic",
},
secondary: {
background: "$secondaryLow-semantic",
color: "$inkText-semantic",
},
danger: {
background: "$danger-semantic",
color: "$white-static",
},
},
},
});
// 복합 속성 (색상 토큰이 앞에 있는 경우) 테스트
const BorderAndColorTestComponent = styled("div", {
color: "$scale-red500 solid 1px",
color: "$red600 dashed 2px",
color: "$scale-blue400 dotted 3px",
color: "$scale-green600 double 4px",
color: "$scale-yellow500 groove 2px",
outlineColor: "$scale-purple500",
});
const ImportantTest = styled("div", {
color: "$primary-semantic !important",
color: "$onPrimary-semantic !important",
color: "$primaryLow-semantic !important",
color: "$gray00 !important",
color: "$gray50 !important",
color: "$gray100 !important",
css: {
color: "$scale-gray600 !important",
borderColor: "$scale-gray400 !important",
backgroundColor: "$scale-gray100 !important",
},
});
const Li_FieldItem = styled("li", {
borderBottom: `${rem(1)} solid $semantic-divider1`,
borderBottom: `0.5rem solid $semantic-paperBackground`,
border: check ? "1px solid $semantic-divider1" : "0.5rem solid $semantic-paperBackground",
});
```
```tsx title="basic.output.tsx"
// @ts-nocheck
const SemanticColorTestComponent = styled("div", {
// Semantic Color 테스트
color: "$fg-brand",
color: "$palette-static-white",
color: "$palette-carrot-100",
color: "$palette-gray-900",
color: "$bg-neutral-weak",
color: "$fg-positive",
color: "$bg-positive-weak",
color: "$bg-warning-solid",
color: "$bg-warning-weak",
color: "$fg-critical",
color: "$bg-critical-weak",
color: "$bg-overlay",
color: "$bg-overlay-muted",
color: "$bg-layer-floating",
color: "$bg-layer-floating",
color: "$bg-layer-floating",
color: "$bg-layer-fill",
color: "$bg-layer-default",
color: "$bg-layer-basement",
color: "$palette-carrot-100",
color: "$bg-brand-solid-pressed",
color: "$bg-brand-solid-pressed",
color: "$palette-carrot-200",
color: "$palette-carrot-100",
color: "$palette-carrot-200",
color: "$bg-neutral-weak-pressed",
color: "$bg-neutral-weak-pressed",
color: "$onPrimaryOverlay50-semantic",
color: "$onPrimaryOverlay200-semantic",
color: "$onPrimaryLowOverlay50-semantic",
color: "$onPrimaryLowOverlay100-semantic",
color: "$onPrimaryLowOverlay200-semantic",
color: "$stroke-neutral-subtle",
color: "$onGrayOverlay100-semantic",
color: "$stroke-neutral-subtle",
color: "$stroke-neutral-muted",
color: "$palette-gray-400",
color: "$fg-informative",
color: "$fg-neutral",
color: "$fg-neutral-subtle",
color: "$fg-neutral-muted",
});
const ScaleColorTestComponent = styled("div", {
// Scale Color Gray 테스트
color: "$palette-gray-00",
color: "$palette-gray-100",
color: "$palette-gray-200",
color: "$palette-gray-300",
color: "$palette-gray-400",
color: "$palette-gray-500",
color: "$palette-gray-600",
color: "$palette-gray-700",
color: "$palette-gray-800",
color: "$palette-gray-900",
color: "$palette-gray-1000",
color: "$palette-gray-200",
color: "$palette-gray-300",
color: "$palette-gray-500",
color: "$palette-gray-700",
// Scale Color Carrot 테스트
color: "$palette-carrot-100",
color: "$palette-carrot-200",
color: "$palette-carrot-300",
color: "$palette-carrot-400",
color: "$palette-carrot-500",
color: "$palette-carrot-600",
color: "$palette-carrot-600",
color: "$palette-carrot-700",
color: "$palette-carrot-700",
color: "$palette-carrot-800",
color: "$palette-carrot-800",
color: "$palette-carrot-100",
color: "$palette-carrot-200",
color: "$palette-carrot-200",
// Scale Color Blue 테스트
color: "$palette-blue-100",
color: "$palette-blue-200",
color: "$palette-blue-300",
color: "$palette-blue-400",
color: "$palette-blue-400",
color: "$palette-blue-600",
color: "$palette-blue-600",
color: "$palette-blue-800",
color: "$palette-blue-900",
color: "$palette-blue-900",
color: "$palette-blue-1000",
color: "$palette-blue-100",
color: "$palette-blue-100",
color: "$palette-blue-200",
// Scale Color Red 테스트
color: "$palette-red-100",
color: "$palette-red-200",
color: "$palette-red-300",
color: "$palette-red-400",
color: "$palette-red-600",
color: "$palette-red-700",
color: "$palette-red-700",
color: "$palette-red-800",
color: "$palette-red-900",
color: "$palette-red-900",
color: "$palette-red-900",
color: "$palette-red-100",
color: "$palette-red-200",
color: "$palette-red-300",
// Scale Color Green 테스트
color: "$palette-green-100",
color: "$palette-green-200",
color: "$palette-green-300",
color: "$palette-green-400",
color: "$palette-green-500",
color: "$palette-green-600",
color: "$palette-green-700",
color: "$palette-green-800",
color: "$palette-green-900",
color: "$palette-green-900",
color: "$palette-green-900",
color: "$palette-green-100",
color: "$palette-green-200",
color: "$palette-green-200",
// Scale Color Yellow 테스트
color: "$palette-yellow-100",
color: "$palette-yellow-200",
color: "$palette-yellow-300",
color: "$palette-yellow-400",
color: "$palette-yellow-500",
color: "$palette-yellow-700",
color: "$palette-yellow-700",
color: "$palette-yellow-800",
color: "$palette-yellow-800",
color: "$palette-yellow-900",
color: "$palette-yellow-900",
color: "$palette-yellow-100",
color: "$palette-yellow-100",
color: "$palette-yellow-100",
// Scale Color Purple 테스트
color: "$palette-purple-100",
color: "$palette-purple-300",
color: "$palette-purple-400",
color: "$palette-purple-400",
color: "$palette-purple-500",
color: "$palette-purple-600",
color: "$palette-purple-700",
color: "$palette-purple-800",
color: "$palette-purple-900",
color: "$palette-purple-900",
color: "$palette-purple-1000",
});
const StaticColorTestComponent = styled("div", {
// Static Color 테스트
color: "$palette-static-black",
color: "$palette-static-white",
color: "$palette-static-black",
color: "$palette-carrot-100",
color: "$palette-carrot-700",
color: "$palette-green-100",
color: "$palette-green-700",
color: "$palette-yellow-100",
color: "$palette-yellow-700",
color: "$palette-red-100",
color: "$palette-red-700",
color: "$palette-blue-100",
color: "$palette-blue-700",
color: "$palette-static-black-alpha-500",
color: "$palette-static-black-alpha-700",
color: "$palette-static-white-alpha-50",
color: "$palette-static-white-alpha-300",
});
// 복합 속성 테스트
const ComplexPropertyTestComponent = styled("div", {
border: "1px solid $palette-gray-800",
boxShadow: "0 0 10px $bg-overlay",
outline: "2px solid $fg-informative",
textDecoration: "underline $fg-critical",
// 네스팅된 속성 테스트
"&:before": {
borderBottom: "1px solid $stroke-neutral-subtle",
background: "$bg-layer-fill",
},
// 변형 테스트
variants: {
theme: {
primary: {
background: "$bg-brand-solid",
color: "$palette-static-white",
},
secondary: {
background: "$bg-neutral-weak",
color: "$fg-neutral",
},
danger: {
background: "$bg-critical-solid",
color: "$palette-static-white",
},
},
},
});
// 복합 속성 (색상 토큰이 앞에 있는 경우) 테스트
const BorderAndColorTestComponent = styled("div", {
color: "$palette-red-700 solid 1px",
color: "$palette-red-700 dashed 2px",
color: "$palette-blue-400 dotted 3px",
color: "$palette-green-700 double 4px",
color: "$palette-yellow-700 groove 2px",
outlineColor: "$palette-purple-600",
});
const ImportantTest = styled("div", {
color: "$fg-brand !important",
color: "$palette-static-white !important",
color: "$palette-carrot-100 !important",
color: "$palette-gray-00 !important",
color: "$palette-gray-100 !important",
color: "$palette-gray-200 !important",
css: {
color: "$palette-gray-700 !important",
borderColor: "$palette-gray-500 !important",
backgroundColor: "$palette-gray-200 !important",
},
});
const Li_FieldItem = styled("li", {
borderBottom: `${rem(1)} solid $stroke-neutral-subtle`,
borderBottom: `0.5rem solid $bg-layer-basement`,
border: check ? "1px solid $stroke-neutral-subtle" : "0.5rem solid $bg-layer-basement",
});
```
replace-custom-text-component-color-prop \[#replace-custom-text-component-color-prop]
- npm: npx @seed-design/codemod@latest replace-custom-text-component-color-prop \
- pnpm: pnpm dlx @seed-design/codemod@latest replace-custom-text-component-color-prop \
- yarn: yarn dlx @seed-design/codemod@latest replace-custom-text-component-color-prop \
- bun: bun x @seed-design/codemod@latest replace-custom-text-component-color-prop \
```tsx title="basic.input.tsx"
// @ts-nocheck
const Component = () => {
return (
<>
>
);
};
```
```tsx title="basic.output.tsx"
// @ts-nocheck
const Component = () => {
return (
<>
>
);
};
```
replace-alpha-color \[#replace-alpha-color]
SEED 알파 컬러 토큰을 새로운 이름으로 업데이트하는 transform입니다.
이 transform은 다음과 같은 알파 컬러 토큰들을 새로운 값으로 변경합니다:
- `staticBlackAlpha50` → `staticBlackAlpha200`
- `staticBlackAlpha200` → `staticBlackAlpha500`
- `staticBlackAlpha500` → `staticBlackAlpha700`
- `staticWhiteAlpha200` → `staticWhiteAlpha300`
주의사항 \[#주의사항-1]
- 이 transform은 JavaScript/TypeScript 파일과 CSS 파일을 모두 자동으로 처리합니다.
- **한 번만 실행**: 동일한 파일에 여러 번 실행하면 연쇄 변환이 발생할 수 있습니다
- **SEED 전용**: 이미 SEED를 사용 중인 프로젝트에서만 사용하세요
- **백업 권장**: 변환 전에 코드를 백업하는 것을 권장합니다
- **순서 중요**: 큰 번호부터 작은 번호 순으로 변환하여 연쇄 변환을 방지합니다
* npm: npx @seed-design/codemod@latest replace-alpha-color \
* pnpm: pnpm dlx @seed-design/codemod@latest replace-alpha-color \
* yarn: yarn dlx @seed-design/codemod@latest replace-alpha-color \
* bun: bun x @seed-design/codemod@latest replace-alpha-color \
```tsx title="basic.input.tsx"
// @ts-nocheck
import { vars } from "@seed-design/css/vars";
export const Component = () => {
const styles = {
overlay: {
backgroundColor: vars.$color.palette.staticBlackAlpha50,
},
shadow: {
backgroundColor: vars.$color.palette.staticBlackAlpha200,
},
darkOverlay: {
backgroundColor: vars.$color.palette.staticBlackAlpha500,
},
lightOverlay: {
backgroundColor: vars.$color.palette.staticWhiteAlpha200,
},
};
return (
Light overlay
Shadow
Dark overlay
Light overlay
Direct usage
);
};
```
```tsx title="basic.output.tsx"
// @ts-nocheck
import { vars } from "@seed-design/css/vars";
export const Component = () => {
const styles = {
overlay: {
backgroundColor: vars.$color.palette.staticBlackAlpha200,
},
shadow: {
backgroundColor: vars.$color.palette.staticBlackAlpha500,
},
darkOverlay: {
backgroundColor: vars.$color.palette.staticBlackAlpha700,
},
lightOverlay: {
backgroundColor: vars.$color.palette.staticWhiteAlpha300,
},
};
return (
Light overlay
Shadow
Dark overlay
Light overlay
Direct usage
);
};
```
replace-custom-seed-design-color \[#replace-custom-seed-design-color]
커스텀 컬러 토큰을 SEED 형식으로 변환하는 트랜스폼
- npm: npx @seed-design/codemod@latest replace-custom-seed-design-color \
- pnpm: pnpm dlx @seed-design/codemod@latest replace-custom-seed-design-color \
- yarn: yarn dlx @seed-design/codemod@latest replace-custom-seed-design-color \
- bun: bun x @seed-design/codemod@latest replace-custom-seed-design-color \
```ts title="basic.input.ts"
// @ts-nocheck
import { style } from "@vanilla-extract/css";
import { color, backgroundColor, background } from "@/shared/styles";
export const container = style({
backgroundColor: color.gray100,
color: color.gray900,
borderColor: color.carrot500,
});
export const box = style({
background: color.gray50,
color: color.red500,
border: `1px solid ${color.gray300}`,
});
export const alert = style({
backgroundColor: color.red50,
color: color.red900,
});
export const button = style({
backgroundColor: background.gray100,
color: color.gray900,
});
export const highlight = style({
color: color.carrot500,
backgroundColor: backgroundColor.primary,
});
export const primary = style({
color: color.primary,
});
```
```ts title="basic.output.ts"
// @ts-nocheck
import { style } from "@vanilla-extract/css";
import { color, backgroundColor, background } from "@/shared/styles";
export const container = style({
backgroundColor: color.palette.gray200,
color: color.palette.gray1000,
borderColor: color.palette.carrot600,
});
export const box = style({
background: color.palette.gray100,
color: color.palette.red700,
border: `1px solid ${color.palette.gray400}`,
});
export const alert = style({
backgroundColor: color.palette.red100,
color: color.palette.red900,
});
export const button = style({
backgroundColor: background.palette.gray200,
color: color.palette.gray1000,
});
export const highlight = style({
color: color.palette.carrot600,
backgroundColor: backgroundColor.bg.brandSolid,
});
export const primary = style({
color: color.fg.brand,
});
```
replace-custom-color-to-seed-design-vars-color \[#replace-custom-color-to-seed-design-vars-color]
- npm: npx @seed-design/codemod@latest replace-custom-color-to-seed-design-vars-color \
- pnpm: pnpm dlx @seed-design/codemod@latest replace-custom-color-to-seed-design-vars-color \
- yarn: yarn dlx @seed-design/codemod@latest replace-custom-color-to-seed-design-vars-color \
- bun: bun x @seed-design/codemod@latest replace-custom-color-to-seed-design-vars-color \
```ts title="basic.input.ts"
// @ts-nocheck
import { color, bg, typography, mode } from '@/src/styles/tokens';
export const container = style({
backgroundColor: color['$semantic.paperDefault'],
color: color['$scale.gray900'],
borderColor: color['$scale.carrot500'],
});
export const box = style({
background: color['$scale.gray50'],
color: color['$scale.red500'],
border: `1px solid ${color['$scale.gray300']}`,
});
export const alert = style({
backgroundColor: color['$scale.red50'],
color: color['$scale.red900'],
});
export const button = style({
backgroundColor: color['$scale.gray100'],
color: color['$scale.gray900'],
});
export const highlight = style({
color: color['$scale.carrot500'],
backgroundColor: color['$semantic.primary'],
});
export const primary = style({
color: color['$semantic.primary'],
});
// Additional test cases for various mappings
export const semanticColors = style({
backgroundColor: color['$semantic.primary'],
color: color['$semantic.inkText'],
borderColor: color['$semantic.divider1'],
});
export const scaleColors = style({
background: color['$scale.gray100'],
color: color['$scale.carrot600'],
border: `1px solid ${color['$scale.blue500']}`,
});
export const staticColors = style({
backgroundColor: color['$static.staticWhite'],
color: color['$static.staticBlack'],
borderColor: color['$static.staticBlackAlpha200'],
});
// Test deprecated tokens with alternative
export const deprecatedColors = style({
backgroundColor: color['$semantic.paperContents'],
color: color['$semantic.inkTextLow'],
});
// Test multiple token choices (should use first one)
export const multipleChoices = style({
backgroundColor: color['$semantic.success'],
color: color['$semantic.warning'],
});
export const icon = {
color: color['$scale.gray600'],
bg: bg['$scale.gray100'],
};
```
```ts title="basic.output.ts"
// @ts-nocheck
import { color, bg, typography, mode } from "@/src/styles/tokens";
export const container = style({
backgroundColor: color["bg.layerDefault"],
color: color["palette.gray1000"],
borderColor: color["palette.carrot600"],
});
export const box = style({
background: color["palette.gray100"],
color: color["palette.red700"],
border: `1px solid ${color["palette.gray400"]}`,
});
export const alert = style({
backgroundColor: color["palette.red100"],
color: color["palette.red900"],
});
export const button = style({
backgroundColor: color["palette.gray200"],
color: color["palette.gray1000"],
});
export const highlight = style({
color: color["palette.carrot600"],
backgroundColor: color["bg.brandSolid"],
});
export const primary = style({
color: color["fg.brand"],
});
// Additional test cases for various mappings
export const semanticColors = style({
backgroundColor: color["bg.brandSolid"],
color: color["fg.neutral"],
borderColor: color["stroke.neutralSubtle"],
});
export const scaleColors = style({
background: color["palette.gray200"],
color: color["palette.carrot600"],
border: `1px solid ${color["palette.blue600"]}`,
});
export const staticColors = style({
backgroundColor: color["palette.staticWhite"],
color: color["palette.staticBlack"],
borderColor: color["palette.staticBlackAlpha500"],
});
// Test deprecated tokens with alternative
export const deprecatedColors = style({
backgroundColor: color["bg.layerFill"],
color: color["fg.neutralSubtle"],
});
// Test multiple token choices (should use first one)
export const multipleChoices = style({
backgroundColor: color["bg.positiveSolid"],
color: color["bg.warningSolid"],
});
export const icon = {
color: color["palette.gray700"],
bg: bg["palette.gray200"],
};
```
replace-css-seed-design-typography-variable \[#replace-css-seed-design-typography-variable]
- npm: npx @seed-design/codemod@latest replace-css-seed-design-typography-variable \
- pnpm: pnpm dlx @seed-design/codemod@latest replace-css-seed-design-typography-variable \
- yarn: yarn dlx @seed-design/codemod@latest replace-css-seed-design-typography-variable \
- bun: bun x @seed-design/codemod@latest replace-css-seed-design-typography-variable \
```css title="basic.input.css"
/* Radio Label */
.seed-radio-group[data-size="small"] [data-part="radio-label"] {
margin-inline-start: var(--seed-radio-small-label-margin-inline-start);
margin-block-start: var(--seed-radio-small-label-margin-block-start);
font-size: var(--seed-semantic-typography-label4-regular-font-size);
font-weight: var(--seed-semantic-typography-label4-regular-font-weight);
line-height: var(--seed-semantic-typography-label4-regular-line-height);
letter-spacing: var(--seed-semantic-typography-label4-regular-letter-spacing);
}
.seed-radio-group[data-size="medium"] [data-part="radio-label"] {
margin-inline-start: var(--seed-radio-medium-label-margin-inline-start);
margin-block-start: var(--seed-radio-medium-label-margin-block-start);
font-size: var(--seed-semantic-typography-label3-regular-font-size);
font-weight: var(--seed-semantic-typography-label3-regular-font-weight);
line-height: var(--seed-semantic-typography-label3-regular-line-height);
letter-spacing: var(--seed-semantic-typography-label3-regular-letter-spacing);
}
.seed-radio-group[data-size="large"] [data-part="radio-label"] {
margin-inline-start: var(--seed-radio-large-label-margin-inline-start);
margin-block-start: var(--seed-radio-large-label-margin-block-start);
font-size: var(--seed-semantic-typography-label2-regular-font-size);
font-weight: var(--seed-semantic-typography-label2-regular-font-weight);
line-height: var(--seed-semantic-typography-label2-regular-line-height);
letter-spacing: var(--seed-semantic-typography-label2-regular-letter-spacing);
}
```
```css title="basic.output.css"
/* Radio Label */
.seed-radio-group[data-size="small"] [data-part="radio-label"] {
margin-inline-start: var(--seed-radio-small-label-margin-inline-start);
margin-block-start: var(--seed-radio-small-label-margin-block-start);
font-size: var(--seed-font-size-t2);
font-weight: var(--seed-font-weight-regular);
line-height: var(--seed-line-height-t2);
letter-spacing: normal;
}
.seed-radio-group[data-size="medium"] [data-part="radio-label"] {
margin-inline-start: var(--seed-radio-medium-label-margin-inline-start);
margin-block-start: var(--seed-radio-medium-label-margin-block-start);
font-size: var(--seed-font-size-t4);
font-weight: var(--seed-font-weight-regular);
line-height: var(--seed-line-height-t4);
letter-spacing: normal;
}
.seed-radio-group[data-size="large"] [data-part="radio-label"] {
margin-inline-start: var(--seed-radio-large-label-margin-inline-start);
margin-block-start: var(--seed-radio-large-label-margin-block-start);
font-size: var(--seed-font-size-t5);
font-weight: var(--seed-font-weight-regular);
line-height: var(--seed-line-height-t5);
letter-spacing: normal;
}
```
replace-tailwind-color \[#replace-tailwind-color]
Tailwind CSS 색상 클래스를 SEED 형식으로 변환해요.
변환 내용 \[#변환-내용-1]
- Sprout의 색상 클래스 네이밍을 SEED 형식으로 변환합니다.
- `text-`, `bg-`, `border-` 등의 접두사를 가진 Tailwind 색상 클래스를 변환합니다.
- 색상 강도(shade)에 따른 변환 매핑을 적용합니다.
변환 예시 \[#변환-예시-1]
- `text-carrot-500` → `text-static-orange-base`
- `bg-salmon-100` → `bg-static-red-light`
- `border-navy-900` → `border-static-blue-dark`
대상 파일 \[#대상-파일-1]
- `.tsx`, `.jsx`, `.ts`, `.js`, `.html`, `.css` 파일에서 Tailwind 클래스를 사용하는 부분
주의사항 \[#주의사항-2]
- className 문자열, 템플릿 리터럴, 배열 형태 모두 지원합니다.
- Tailwind 동적 클래스 조합(`clsx`, `cx`, `classnames` 등)도 변환됩니다.
* npm: npx @seed-design/codemod@latest replace-tailwind-color \
* pnpm: pnpm dlx @seed-design/codemod@latest replace-tailwind-color \
* yarn: yarn dlx @seed-design/codemod@latest replace-tailwind-color \
* bun: bun x @seed-design/codemod@latest replace-tailwind-color \
```tsx title="basic.input.tsx"
// @ts-nocheck
export function BackgroundExample() {
return (
Primary Background
Primary Low Background
Hover Primary Low Background
Focus Primary Low Background
Active Primary Low Background
Scale Background
Scale Carrot Low Background
Scale Carrot Mid Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Static Black Background
Static White Background
Static Gray900 Background
);
}
export function SeedBackgroundExample() {
return (
Primary Background
Primary Low Background
Hover Primary Low Background
Focus Primary Low Background
Active Primary Low Background
Scale Background
Scale Carrot Low Background
Scale Carrot Mid Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Static Black Background
Static White Background
Static Gray900 Background
);
}
// Class Variance Authority Example
import { cva } from "class-variance-authority";
// Button component with variants
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-staticWhite hover:bg-primaryLow",
destructive: "bg-danger text-staticWhite hover:bg-dangerLow",
outline: "border border-gray300 bg-staticWhite text-gray900 hover:bg-gray100",
secondary: "bg-seed-secondaryLow text-seed-secondary hover:bg-seed-gray200",
ghost: "text-seed-primary hover:bg-seed-primaryLow",
link: "text-seed-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
```
```tsx title="basic.output.tsx"
// @ts-nocheck
export function BackgroundExample() {
return (
Primary Background
Primary Low Background
Hover Primary Low Background
Focus Primary Low Background
Active Primary Low Background
Scale Background
Scale Carrot Low Background
Scale Carrot Mid Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Static Black Background
Static White Background
Static Gray900 Background
);
}
export function SeedBackgroundExample() {
return (
Primary Background
Primary Low Background
Hover Primary Low Background
Focus Primary Low Background
Active Primary Low Background
Scale Background
Scale Carrot Low Background
Scale Carrot Mid Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Scale Carrot High Background
Static Black Background
Static White Background
Static Gray900 Background
);
}
// Class Variance Authority Example
import { cva } from "class-variance-authority";
// Button component with variants
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
{
variants: {
variant: {
default: "bg-bg-brand-solid text-palette-static-white hover:bg-palette-carrot-100",
destructive: "bg-bg-critical-solid text-palette-static-white hover:bg-bg-critical-weak",
outline: "border border-palette-gray-400 bg-palette-static-white text-palette-gray-1000 hover:bg-palette-gray-200",
secondary: "bg-bg-neutral-weak text-palette-gray-900 hover:bg-palette-gray-300",
ghost: "text-fg-brand hover:bg-palette-carrot-100",
link: "text-fg-brand underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
```
---
file: developer-tools/codemods/introduction.mdx
# Introduction
@seed-design/codemod는 Sprout에서 SEED로 마이그레이션하기 위한 코드 변환 도구예요.
사용 방법 \[#사용-방법]
- npm: npx @seed-design/codemod@latest \ \ \<옵션>
- pnpm: pnpm dlx @seed-design/codemod@latest \ \ \<옵션>
- yarn: yarn dlx @seed-design/codemod@latest \ \ \<옵션>
- bun: bun x @seed-design/codemod@latest \ \ \<옵션>
사용 가능한 transform 목록을 확인하려면 다음 명령어를 실행해요.
- npm: npx @seed-design/codemod@latest --list
- pnpm: pnpm dlx @seed-design/codemod@latest --list
- yarn: yarn dlx @seed-design/codemod@latest --list
- bun: bun x @seed-design/codemod@latest --list
사용 가능한 변환 목록을 확인합니다.
스크립트 실행을 위해서는 **Node.js 20.16.0 이상**의 버전이 필요해요. 버전 요구사항을 만족시키지 않으면 안내 문구가 표시돼요.
nvm을 사용한다면 codemod 실행을 위해 Node.js 버전을 잠시 변경할 수 있어요.
```shell
nvm install 20.16
```
옵션 \[#옵션]
- `--list`
- 사용 가능한 transform 목록을 보여줘요.
- `--log`
- 로그를 파일로 저장해요.
- `./`에 `combined.log`와 `warnings.log` 파일이 생성돼요.
- `--parser`
- jscodeshift가 사용할 파서를 지정해요
- `babel` | `babylon` | `flow` | `ts` | `tsx`
- 기본값: `tsx`
- 예시: `--parser=babel`
- `--extensions`
- 변환할 파일 확장자를 지정해요.
- 지정하지 않으면 `<경로>` 안의 `js,jsx,ts,tsx` 파일을 변환해요. (`d.ts`는 제외)
- 예시: `--extensions="ts,tsx"`
- `--ignore-config`
- 변환하지 않을 파일 패턴이 정의된 파일을 지정해요.
- 예시: `--ignore-config=".gitignore"`
---
file: developer-tools/figma-integration/codegen.mdx
# Codegen
Figma 디자인으로부터 React 코드를 생성하는 플러그인을 제공합니다.
설치 \[#설치]
[SEED Design Codegen](https://www.figma.com/community/plugin/1469592983561656263) 플러그인을 설치합니다.
사용법 \[#사용법]
Figma 디자인을 열고, Dev mode에서 코드 생성을 원하는 디자인 요소를 선택합니다.
Codegen 플러그인은 작은 단위의 코드 생성 및 작업에 적합합니다.
복잡한 작업은 [MCP](/react/ai-integration/mcp)를 활용하는 것을 권장합니다.
관련 문서 \[#관련-문서]
- [Figma Learn](https://help.figma.com/hc/en-us/articles/15023202277399-Use-code-snippets-in-Dev-Mode)
---
file: getting-started/cli/commands.mdx
# Commands
@seed-design/cli 명령어를 안내해요.
공통 동작 \[#공통-동작]
- `add`, `add-all`은 `seed-design.json` 설정 파일을 사용해요.
- 설정 파일이 없거나 읽을 수 없으면 생성할지 먼저 물어봐요.
- 항목 추가 시 필요한 npm 패키지 의존성은 자동으로 설치돼요.
- `add`, `add-all` 실행 시 스니펫이 요구하는 `@seed-design/react`, `@seed-design/css` 버전과 현재 프로젝트 버전이 다르면 경고를 보여줘요.
- 기존 파일과 내용이 다르면 기본적으로 `덮어쓰기 / 백업 후 교체 / 건너뛰기`를 선택할 수 있어요.
- `--on-diff` 옵션을 주면 충돌 처리 방식을 미리 고정할 수 있어요.
- 실패 원인을 자세히 확인하려면 `--verbose` 옵션을 사용할 수 있어요.
init \[#init]
`seed-design.json` 파일을 생성하는 명령어예요.
`seed-design.json` 파일에 대해 알아봅니다.
init 명령어 입력하기 \[#init-명령어-입력하기]
- npm: npx @seed-design/cli@latest init
- pnpm: pnpm dlx @seed-design/cli@latest init
- yarn: yarn dlx @seed-design/cli@latest init
- bun: bun x @seed-design/cli@latest init
seed-design 설정 \[#seed-design-설정]
```sh
◇ TypeScript를 사용중이신가요?
│ Yes
│
◇ React Server Components를 사용중이신가요?
│ No
│
◇ seed-design 폴더 경로를 입력해주세요. (기본값은 프로젝트 루트에 생성됩니다.)
│ ./seed-design
│
◇ 개선을 위해 익명 사용 데이터를 수집할까요?
│ Yes
```
옵션 \[#옵션]
| 옵션 | 설명 |
| ----------------- | ---------------------------------- |
| `-c, --cwd ` | 작업 디렉토리. 기본값은 현재 디렉토리 |
| `-y, --yes` | 질문 없이 기본값으로 `seed-design.json` 생성 |
| `--default` | Deprecated. `--yes`와 동일하게 기본값으로 생성 |
| `--verbose` | 실패 시 stack trace 등 상세 오류 정보 출력 |
| `-h, --help` | 도움말 출력 |
- npm: npx @seed-design/cli@latest init --yes
- pnpm: pnpm dlx @seed-design/cli@latest init --yes
- yarn: yarn dlx @seed-design/cli@latest init --yes
- bun: bun x @seed-design/cli@latest init --yes
add \[#add]
스니펫을 다운로드하고 프로젝트에 추가하는 명령어예요.
자주 쓰이는 UI 패턴의 재사용을 돕는 스니펫에 대해 알아봅니다.
- npm: npx @seed-design/cli@latest add \[...item-ids]
- pnpm: pnpm dlx @seed-design/cli@latest add \[...item-ids]
- yarn: yarn dlx @seed-design/cli@latest add \[...item-ids]
- bun: bun x @seed-design/cli@latest add \[...item-ids]
`item-ids`를 생략하면, 다운로드 가능한 항목을 인터랙티브하게 선택할 수 있어요.
- npm: npx @seed-design/cli@latest add
- pnpm: pnpm dlx @seed-design/cli@latest add
- yarn: yarn dlx @seed-design/cli@latest add
- bun: bun x @seed-design/cli@latest add
```sh copy
◆ 추가할 항목을 선택해주세요 (스페이스 바로 여러 개 선택 가능)
│ ◻ ui:action-button
│ ◻ ui:alert-dialog
│ ◻ ui:callout
│ ◻ ui:checkbox
│ ◻ ui:switch
│ ◻ ui:text-field
│ ◻ ui:tabs
│ ◻ (deprecated) ui:old-component
│ ◻ ...
└
```
여러 항목을 직접 지정할 수도 있어요.
- npm: npx @seed-design/cli@latest add ui:action-button ui:alert-dialog
- pnpm: pnpm dlx @seed-design/cli@latest add ui:action-button ui:alert-dialog
- yarn: yarn dlx @seed-design/cli@latest add ui:action-button ui:alert-dialog
- bun: bun x @seed-design/cli@latest add ui:action-button ui:alert-dialog
옵션 \[#옵션-1]
| 옵션 | 설명 |
| ------------------------- | --------------------------------------------- |
| `-c, --cwd ` | 작업 디렉토리. 기본값은 현재 디렉토리 |
| `-u, --baseUrl ` | 레지스트리 base URL |
| `--on-diff ` | 파일 충돌 처리 방식 지정 (`overwrite` 또는 `backup`) |
| `-a, --all` | **Deprecated**. 현재는 에러를 출력하고 `add-all` 사용을 안내 |
| `--verbose` | 실패 시 stack trace 등 상세 오류 정보 출력 |
| `-h, --help` | 도움말 출력 |
baseUrl 사용하기 \[#baseurl-사용하기]
`--baseUrl` 옵션을 사용하면 스니펫을 다운로드할 레지스트리를 직접 지정할 수 있습니다.
기본값은 배포 환경 기준 [`https://seed-design.io`](https://seed-design.io)입니다.
이 옵션은 특정 SEED React 패키지와 호환되는 스니펫이 필요한 경우 활용할 수 있습니다.
- npm: npx @seed-design/cli@latest add --baseUrl https://1-0.seed-design.pages.dev
- pnpm: pnpm dlx @seed-design/cli@latest add --baseUrl https://1-0.seed-design.pages.dev
- yarn: yarn dlx @seed-design/cli@latest add --baseUrl https://1-0.seed-design.pages.dev
- bun: bun x @seed-design/cli@latest add --baseUrl https://1-0.seed-design.pages.dev
* npm: npx @seed-design/cli@latest add --baseUrl https://1-1.seed-design.pages.dev
* pnpm: pnpm dlx @seed-design/cli@latest add --baseUrl https://1-1.seed-design.pages.dev
* yarn: yarn dlx @seed-design/cli@latest add --baseUrl https://1-1.seed-design.pages.dev
* bun: bun x @seed-design/cli@latest add --baseUrl https://1-1.seed-design.pages.dev
파일 충돌 처리 \[#파일-충돌-처리]
`--on-diff`를 지정하지 않으면, 기존 파일과 새 스니펫이 다를 때 아래 중 하나를 직접 고를 수 있어요.
- 덮어쓰기
- 기존 파일을 `legacy-<파일명>-`로 백업 후 교체
- 건너뛰기
add-all \[#add-all]
레지스트리 단위로 항목을 한 번에 추가하는 명령어예요.
- npm: npx @seed-design/cli@latest add-all \[...registry-ids]
- pnpm: pnpm dlx @seed-design/cli@latest add-all \[...registry-ids]
- yarn: yarn dlx @seed-design/cli@latest add-all \[...registry-ids]
- bun: bun x @seed-design/cli@latest add-all \[...registry-ids]
모든 레지스트리의 모든 항목을 추가하려면 `--all` 옵션을 사용하세요.
- npm: npx @seed-design/cli@latest add-all --all # 모든 레지스트리의 모든 항목 추가
- pnpm: pnpm dlx @seed-design/cli@latest add-all --all # 모든 레지스트리의 모든 항목 추가
- yarn: yarn dlx @seed-design/cli@latest add-all --all # 모든 레지스트리의 모든 항목 추가
- bun: bun x @seed-design/cli@latest add-all --all # 모든 레지스트리의 모든 항목 추가
특정 레지스트리의 모든 항목을 추가할 수도 있습니다.
- npm: npx @seed-design/cli@latest add-all breeze # breeze 레지스트리의 모든 항목 추가
- pnpm: pnpm dlx @seed-design/cli@latest add-all breeze # breeze 레지스트리의 모든 항목 추가
- yarn: yarn dlx @seed-design/cli@latest add-all breeze # breeze 레지스트리의 모든 항목 추가
- bun: bun x @seed-design/cli@latest add-all breeze # breeze 레지스트리의 모든 항목 추가
deprecated 항목을 포함하려면 `--include-deprecated` 옵션을 사용하세요.
- npm: npx @seed-design/cli@latest add-all ui --include-deprecated # ui 레지스트리의 모든 항목 추가 (deprecated 포함)
- pnpm: pnpm dlx @seed-design/cli@latest add-all ui --include-deprecated # ui 레지스트리의 모든 항목 추가 (deprecated 포함)
- yarn: yarn dlx @seed-design/cli@latest add-all ui --include-deprecated # ui 레지스트리의 모든 항목 추가 (deprecated 포함)
- bun: bun x @seed-design/cli@latest add-all ui --include-deprecated # ui 레지스트리의 모든 항목 추가 (deprecated 포함)
레지스트리 ID와 `--all`을 모두 생략하면, 추가할 레지스트리를 인터랙티브하게 선택할 수 있어요.
옵션 \[#옵션-2]
| 옵션 | 설명 |
| ------------------------- | ---------------------------------------- |
| `-a, --all` | 모든 레지스트리의 모든 항목 추가 |
| `--include-deprecated` | deprecated 항목 포함 |
| `-c, --cwd ` | 작업 디렉토리. 기본값은 현재 디렉토리 |
| `-u, --baseUrl ` | 레지스트리 base URL |
| `--on-diff ` | 파일 충돌 처리 방식 지정 (`overwrite` 또는 `backup`) |
| `--verbose` | 실패 시 stack trace 등 상세 오류 정보 출력 |
| `-h, --help` | 도움말 출력 |
compat \[#compat]
현재 프로젝트의 `@seed-design/react`, `@seed-design/css` 버전과 스니펫 요구 버전이 호환되는지 검사해요.
- npm: npx @seed-design/cli@latest compat
- pnpm: pnpm dlx @seed-design/cli@latest compat
- yarn: yarn dlx @seed-design/cli@latest compat
- bun: bun x @seed-design/cli@latest compat
기본값은 `seed-design.json`의 `path` 아래에 이미 추가된 스니펫만 검사해요.
호환성 이슈가 있으면 종료 코드 `1`, 문제가 없으면 `0`으로 종료돼서 CI/에이전트 환경에서도 바로 사용할 수 있어요.
특정 항목만 검사할 수도 있어요.
- npm: npx @seed-design/cli@latest compat ui:action-button ui:alert-dialog
- pnpm: pnpm dlx @seed-design/cli@latest compat ui:action-button ui:alert-dialog
- yarn: yarn dlx @seed-design/cli@latest compat ui:action-button ui:alert-dialog
- bun: bun x @seed-design/cli@latest compat ui:action-button ui:alert-dialog
컴포넌트 shorthand를 쓸 때는 `-c` 옵션을 사용해요.
- npm: npx @seed-design/cli@latest compat -c action-button -c alert-dialog
- pnpm: pnpm dlx @seed-design/cli@latest compat -c action-button -c alert-dialog
- yarn: yarn dlx @seed-design/cli@latest compat -c action-button -c alert-dialog
- bun: bun x @seed-design/cli@latest compat -c action-button -c alert-dialog
모든 registry 항목을 검사하려면 `--all`을 사용하세요.
- npm: npx @seed-design/cli@latest compat --all
- pnpm: pnpm dlx @seed-design/cli@latest compat --all
- yarn: yarn dlx @seed-design/cli@latest compat --all
- bun: bun x @seed-design/cli@latest compat --all
옵션 \[#옵션-3]
| 옵션 | 설명 |
| ----------------------------- | ---------------------------------------------- |
| `-c, --component ` | 검사할 컴포넌트 ID. 여러 번 또는 쉼표로 지정 가능 |
| `-r, --registry ` | `--component`에 shorthand를 사용할 때 기본 registry 지정 |
| `-a, --all` | 모든 registry 항목 검사 |
| `--cwd ` | 작업 디렉토리. 기본값은 현재 디렉토리 |
| `-u, --baseUrl ` | 레지스트리 base URL |
| `--verbose` | 실패 시 stack trace 등 상세 오류 정보 출력 |
| `-h, --help` | 도움말 출력 |
---
file: getting-started/cli/configuration.mdx
# Configuration
seed-design.json 설정을 안내해요.
seed-design.json 만들기 \[#seed-designjson-만들기]
`seed-design.json` 파일은 CLI로 생성하거나 직접 추가할 수 있어요.
CLI로 seed-design을 추가하는 방법을 알아봐요.
아래 파일을 프로젝트 루트에 생성해주세요.
```json title="seed-design.json"
{
"rsc": false,
"tsx": true,
"path": "./seed-design",
"telemetry": true
}
```
기본 형태 \[#기본-형태]
```json title="seed-design.json"
{
"rsc": false,
"tsx": true,
"path": "./seed-design",
"telemetry": true
}
```
`seed-design.json`은 엄격하게 파싱돼요. 문서에 없는 키를 임의로 추가하면 CLI 실행 시 오류가 날 수 있어요.
옵션 한눈에 보기 \[#옵션-한눈에-보기]
| 옵션 | 타입 | 기본값 | 설명 |
| ----------- | --------- | ---------------------------- | ----------------------------------------------------------- |
| `path` | `string` | 없음 (init에서는 `./seed-design`) | 스니펫이 생성될 루트 경로 |
| `rsc` | `boolean` | `false` | React Server Components 사용 여부에 맞춰 `"use client"` 유지/제거 |
| `tsx` | `boolean` | `true` | 생성 파일을 TypeScript(`.ts/.tsx`) 또는 JavaScript(`.js/.jsx`)로 변환 |
| `telemetry` | `boolean` | `true` | 익명 사용 데이터 수집 여부 |
| `$schema` | `string` | 선택값 | JSON 스키마 힌트용 필드 (CLI 동작에는 영향 없음) |
path \[#path]
`path`는 스니펫이 생성되는 루트 디렉토리예요.
`add`/`add-all` 실행 시 파일은 `path` 아래에 레지스트리 ID와 스니펫 내부 경로 기준으로 생성돼요.
고정된 폴더 구조가 아니라, 선택한 항목에 따라 구조가 달라져요.
```json title="seed-design.json"
{
"path": "./seed-design"
}
```
rsc \[#rsc]
[React Server Components](https://react.dev/reference/rsc/server-components)를 사용할지 설정해요.
스니펫에 이미 있는 `"use client"` directive를 유지할지 결정해요.
- `true`: `"use client"`를 유지해요.
- `false`: 파일 최상단의 `"use client"`를 제거해요.
```json title="seed-design.json"
{
"rsc": true | false // default: false
}
```
tsx \[#tsx]
TypeScript를 사용할지 설정해요.
`false`로 설정하면 `.ts/.tsx` 파일이 `.js/.jsx`로 변환돼요.
```json title="seed-design.json"
{
"tsx": true | false // default: true
}
```
telemetry \[#telemetry]
텔레메트리 데이터 수집 여부를 설정해요.
SEED Design CLI는 개선을 위해 익명 사용 데이터를 수집해요.
프로젝트 경로, 파일 내용, 소스 코드는 수집하지 않아요.
```json title="seed-design.json"
{
"telemetry": true | false // default: true
}
```
우선순위 \[#우선순위]
아래 순서로 텔레메트리 활성화 여부를 판단해요.
1. `DISABLE_TELEMETRY=true` 환경 변수
2. `SEED_DISABLE_TELEMETRY=true` 환경 변수
3. `seed-design.json`의 `telemetry`
4. 기본값 `true`
수집되는 데이터 예시 \[#수집되는-데이터-예시]
- 실행 이벤트: `init`, `add`, `add-all`, `compat`
- 실행 통계: 실행 시간(`duration_ms`), 추가된 항목 수
- 설정 정보: `tsx`, `rsc`, `include_deprecated`, `yes_option` 등
수집되지 않는 데이터 \[#수집되지-않는-데이터]
- 프로젝트 경로, 생성된 파일 경로
- 스니펫 파일 내용
- 소스 코드
- 개인 식별 정보
비활성화 방법 \[#비활성화-방법]
**1. seed-design.json 설정**
```json
{
"telemetry": false
}
```
**2. 환경 변수 설정**
```bash
export DISABLE_TELEMETRY=true
# 또는
export SEED_DISABLE_TELEMETRY=true
```
$schema \[#schema]
`$schema`는 선택적으로 추가할 수 있는 문자열 필드예요.
현재 CLI 동작에는 영향을 주지 않고, 에디터의 JSON 자동완성/검증 용도로만 사용할 수 있어요.
---
file: getting-started/installation/manual.mdx
# Manual
Seed Design을 번들러 통합 없이 설치하고 사용하는 방법을 알아봅니다.
의존성 설치 \[#의존성-설치]
필요한 의존성들을 설치해요.
- npm: npm install @seed-design/react @seed-design/css
- pnpm: pnpm add @seed-design/react @seed-design/css
- yarn: yarn add @seed-design/react @seed-design/css
- bun: bun add @seed-design/react @seed-design/css
index.html 수정하기 \[#indexhtml-수정하기]
Seed Design의 컬러 토큰은 ` ` 태그의 `data-seed-color-mode`, `data-seed-user-color-scheme`
속성을 통해 라이트/다크모드 전환을 해요.
테마 속성에 대한 자세한 설명과 활용 방법을 확인합니다.
```tsx title="index.html"
```
```tsx title="index.html"
```
```tsx title="index.html"
```
all.css import하기 \[#allcss-import하기]
스타일시트 크기를 줄이기 위해 `base.css` 및 각 컴포넌트의 스타일시트를 따로 import할 수 있어요. 번들러를 사용하는 경우 문서 상단의 번들러 가이드를 통해 컴포넌트 스타일시트를 자동으로 로드할 수 있어요.
```ts title="index.tsx"
import "@seed-design/css/all.css";
```
seed-design.json 생성하기 \[#seed-designjson-생성하기]
init 명령어 입력하기 \[#init-명령어-입력하기]
```sh
npx @seed-design/cli@latest init
```
seed-design 설정 \[#seed-design-설정]
```sh
◇ TypeScript를 사용중이신가요?
│ Yes
│
◇ React Server Components를 사용중이신가요?
│ No
│
◇ seed-design 폴더 경로를 입력해주세요. (기본값은 프로젝트 루트에 생성됩니다.)
│ ./seed-design
```
아래 파일을 프로젝트 루트에 생성해주세요.
```json title="seed-design.json"
{
"rsc": false,
"tsx": true,
"path": "./seed-design"
}
```
컴포넌트를 한번 사용해볼까요? \[#컴포넌트를-한번-사용해볼까요]
```sh
npx @seed-design/cli@latest add ui:action-button
```
tsconfig.json 설정하기 \[#tsconfigjson-설정하기]
```json title="tsconfig.json"
{
"compilerOptions": {
// ... your options
"paths": {
// [!code highlight]
"seed-design/*": [
// [!code highlight]
"./seed-design/*"
]
}
}
}
```
***
SEED AppScreen을 사용하기 위해 SEED Stackflow 플러그인을 설정하는 방법을 알아봅니다.
---
file: getting-started/installation/rsbuild.mdx
# Rsbuild
Rsbuild 프로젝트에서 Seed Design을 설치하고 사용하는 방법을 알아봅니다.
의존성 설치 \[#의존성-설치]
필요한 의존성들을 설치해요.
- npm: npm install @seed-design/react @seed-design/css
- pnpm: pnpm add @seed-design/react @seed-design/css
- yarn: yarn add @seed-design/react @seed-design/css
- bun: bun add @seed-design/react @seed-design/css
base.css import하기 \[#basecss-import하기]
```ts title="index.tsx"
import "@seed-design/css/base.css";
```
seed-design.json 생성하기 \[#seed-designjson-생성하기]
init 명령어 입력하기 \[#init-명령어-입력하기]
```sh
npx @seed-design/cli@latest init
```
seed-design 설정 \[#seed-design-설정]
```sh
◇ TypeScript를 사용중이신가요?
│ Yes
│
◇ React Server Components를 사용중이신가요?
│ No
│
◇ seed-design 폴더 경로를 입력해주세요. (기본값은 프로젝트 루트에 생성됩니다.)
│ ./seed-design
```
아래 파일을 프로젝트 루트에 생성해주세요.
```json title="seed-design.json"
{
"rsc": false,
"tsx": true,
"path": "./seed-design"
}
```
tsconfig.json 설정하기 \[#tsconfigjson-설정하기]
위에서 설정한 [seed-design.json path](/react/getting-started/cli/configuration#path)의 경로와 동일해야 해요.
```json title="tsconfig.json"
{
"compilerOptions": {
// ... your options
"paths": {
// [!code highlight]
"seed-design/*": [
// [!code highlight]
"./seed-design/*"
]
}
}
}
```
Rsbuild config 수정하기 \[#rsbuild-config-수정하기]
- npm: npm install -D @seed-design/rsbuild-plugin
- pnpm: pnpm add -D @seed-design/rsbuild-plugin
- yarn: yarn add --dev @seed-design/rsbuild-plugin
- bun: bun add --dev @seed-design/rsbuild-plugin
`seedDesignPlugin`은 [theming](/react/getting-started/styling/theming) 및 CSS 파일 자동 로드 등을 담당해요.
```ts title="rsbuild.config.ts"
import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { seedDesignPlugin } from "@seed-design/rsbuild-plugin"; // [!code highlight]
export default defineConfig({
plugins: [
pluginReact(),
seedDesignPlugin(), // [!code highlight]
],
});
```
컴포넌트를 한번 사용해볼까요? \[#컴포넌트를-한번-사용해볼까요]
```sh
npx @seed-design/cli@latest add ui:action-button
```
***
SEED AppScreen을 사용하기 위해 SEED Stackflow 플러그인을 설정하는 방법을 알아봅니다.
---
file: getting-started/installation/vite.mdx
# Vite
Vite 프로젝트에서 Seed Design을 설치하고 사용하는 방법을 알아봅니다.
의존성 설치 \[#의존성-설치]
필요한 의존성들을 설치해요.
- npm: npm install @seed-design/react @seed-design/css
- pnpm: pnpm add @seed-design/react @seed-design/css
- yarn: yarn add @seed-design/react @seed-design/css
- bun: bun add @seed-design/react @seed-design/css
base.css import하기 \[#basecss-import하기]
```ts title="index.tsx"
import "@seed-design/css/base.css";
```
seed-design.json 생성하기 \[#seed-designjson-생성하기]
init 명령어 입력하기 \[#init-명령어-입력하기]
```sh
npx @seed-design/cli@latest init
```
seed-design 설정 \[#seed-design-설정]
```sh
◇ TypeScript를 사용중이신가요?
│ Yes
│
◇ React Server Components를 사용중이신가요?
│ No
│
◇ seed-design 폴더 경로를 입력해주세요. (기본값은 프로젝트 루트에 생성됩니다.)
│ ./seed-design
```
아래 파일을 프로젝트 루트에 생성해주세요.
```json title="seed-design.json"
{
"rsc": false,
"tsx": true,
"path": "./seed-design"
}
```
tsconfig.json 설정하기 \[#tsconfigjson-설정하기]
위에서 설정한 [seed-design.json
path](/react/getting-started/cli/configuration#path)의 경로와 동일해야 해요.
```json title="tsconfig.app.json"
{
"compilerOptions": {
// ... your options
"paths": {
// [!code highlight]
"seed-design/*": [
// [!code highlight]
"./seed-design/*"
]
}
}
}
```
Vite config 수정하기 \[#vite-config-수정하기]
- npm: npm install -D @seed-design/vite-plugin vite-tsconfig-paths
- pnpm: pnpm add -D @seed-design/vite-plugin vite-tsconfig-paths
- yarn: yarn add --dev @seed-design/vite-plugin vite-tsconfig-paths
- bun: bun add --dev @seed-design/vite-plugin vite-tsconfig-paths
`seedDesignPlugin`은 [theming](/react/getting-started/styling/theming) 및 CSS 파일 자동 로드 등을 담당해요.
```ts title="vite.config.ts"
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import { seedDesignPlugin } from "@seed-design/vite-plugin"; // [!code highlight]
import tsconfigPaths from "vite-tsconfig-paths"; // [!code highlight]
export default defineConfig({
plugins: [
react(),
seedDesignPlugin(), // [!code highlight]
tsconfigPaths(), // [!code highlight]
],
});
```
컴포넌트를 한번 사용해볼까요? \[#컴포넌트를-한번-사용해볼까요]
```sh
npx @seed-design/cli@latest add ui:action-button
```
***
SEED AppScreen을 사용하기 위해 SEED Stackflow 플러그인을 설정하는 방법을 알아봅니다.
---
file: getting-started/installation/webpack.mdx
# Webpack
Webpack 프로젝트에서 Seed Design을 설치하고 사용하는 방법을 알아봅니다.
의존성 설치 \[#의존성-설치]
필요한 의존성들을 설치해요.
- npm: npm install @seed-design/react @seed-design/css @seed-design/webpack-plugin
- pnpm: pnpm add @seed-design/react @seed-design/css @seed-design/webpack-plugin
- yarn: yarn add @seed-design/react @seed-design/css @seed-design/webpack-plugin
- bun: bun add @seed-design/react @seed-design/css @seed-design/webpack-plugin
base.css import하기 \[#basecss-import하기]
```ts title="index.tsx"
import "@seed-design/css/base.css";
```
Webpack config 설정하기 \[#webpack-config-설정하기]
```ts title="vite.config.ts"
+import { SeedDesignPlugin } from '@seed-design/webpack-plugin';
export default defineConfig({
plugins: [
+ new SeedDesignPlugin()
]
})
```
seed-design.json 생성하기 \[#seed-designjson-생성하기]
init 명령어 입력하기 \[#init-명령어-입력하기]
```sh
npx @seed-design/cli@latest init
```
seed-design 설정 \[#seed-design-설정]
```sh
◇ TypeScript를 사용중이신가요?
│ Yes
│
◇ React Server Components를 사용중이신가요?
│ No
│
◇ seed-design 폴더 경로를 입력해주세요. (기본값은 프로젝트 루트에 생성됩니다.)
│ ./seed-design
```
아래 파일을 프로젝트 루트에 생성해주세요.
```json title="seed-design.json"
{
"rsc": false,
"tsx": true,
"path": "./seed-design"
}
```
컴포넌트를 한번 사용해볼까요? \[#컴포넌트를-한번-사용해볼까요]
```sh
npx @seed-design/cli@latest add ui:action-button
```
tsconfig.json 설정하기 \[#tsconfigjson-설정하기]
```json title="tsconfig.json"
{
"compilerOptions": {
// ... your options
"paths": {
// [!code highlight]
"seed-design/*": [
// [!code highlight]
"./seed-design/*"
]
}
}
}
```
***
SEED AppScreen을 사용하기 위해 SEED Stackflow 플러그인을 설정하는 방법을 알아봅니다.
---
file: getting-started/styling/cascade-layers.mdx
# Cascade Layers (Experimental)
CSS Cascade Layers를 사용하여 SEED 스타일의 우선순위를 제어하는 방법을 알아봅니다.
Tailwind CSS 4 등 CSS Cascade Layer를 사용하는 CSS 프레임워크와 SEED 컴포넌트를 함께 사용할 때, 프레임워크의 유틸리티 클래스로 SEED 컴포넌트 스타일을 오버라이드하려면 다음과 같은 설정이 필요합니다.
- `@layer` directive로 래핑된 버전의 SEED CSS 사용
- 레이어 간 적절한 우선순위 설정
CSS Cascade Layers는 Chrome 99+, Safari 15.4+, Firefox 97+ 이상에서
지원됩니다. ([Can I Use](https://caniuse.com/css-cascade-layers))
개요 \[#개요]
기본적으로 SEED CSS는 `@layer` 없이 제공됩니다. 이때 `@layer` directive 없이 정의된 unlayered 스타일은 layered 스타일보다 항상 높은 우선순위를 갖습니다. ([MDN](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Cascade_layers#cascade_layer_precedence_order))
```css
/* SEED 컴포넌트 (unlayered) */
/* layered 스타일보다 우선순위를 가짐 */
.seed-floating-action-button__root {
background: var(--seed-color-bg-brand-solid);
}
/* Tailwind 유틸리티 (layered) */
/* class="seed-floating-action-button__root bg-red-100" 사용 시 SEED 스타일이 우선순위를 가짐 */
@layer utilities {
.bg-red-100 {
background-color: var(--color-red-100);
}
}
```
이 문제를 해결하기 위해 SEED는 `@layer`로 래핑된 CSS 파일을 별도로 제공합니다.
설정 \[#설정]
base.css 대신 base.layered.css 사용 \[#basecss-대신-baselayeredcss-사용]
```css title="style.css"
@import "@seed-design/css/base.layered.css";
```
레이어 순서 커스터마이징 \[#레이어-순서-커스터마이징]
`seed-base` 및 `seed-components` 레이어와 다른 레이어의 선언 순서를 적절히 조절합니다.
Tailwind CSS \[#tailwind-css]
Tailwind CSS 4 사용 시 유틸리티 클래스를 사용하여 SEED 컴포넌트 스타일을 오버라이드하려면, `@layer` 선언에서 Tailwind `utilities` 레이어가 `seed-components` 레이어보다 높은 우선순위를 가지도록 설정해야 합니다.
```css
@layer theme, base, seed-base, components, seed-components, utilities;
```
번들러 설정 \[#번들러-설정]
번들러의 module resolution 설정에 `seed-layered` condition을 추가하세요. 이 설정을 통해 SEED React 컴포넌트가 내부적으로 CSS를 import할 때, 자동으로 layered 버전을 사용합니다.
```ts title="vite.config.ts"
import { defineConfig } from "vite";
export default defineConfig({
resolve: {
conditions: ["seed-layered"], // [!code highlight]
},
// ...
});
```
Vite `conditions` 옵션에 대해 알아봅니다.
```ts title="rsbuild.config.ts"
import { defineConfig } from "@rsbuild/core";
export default defineConfig({
resolve: {
conditionNames: ["seed-layered", "..."], // [!code highlight]
},
// ...
});
```
Rsbuild `conditionNames` 옵션에 대해 알아봅니다.
```js title="webpack.config.js"
module.exports = {
resolve: {
conditionNames: ["seed-layered", "..."], // [!code highlight]
},
// ...
};
```
Webpack `conditionNames` 옵션에 대해 알아봅니다.
Chunk Splitting 사용 시 레이어 순서 문제 \[#chunk-splitting-사용-시-레이어-순서-문제]
번들러가 CSS를 여러 청크로 분리하는 경우, `@layer seed-components { ... }` 블록이 포함된 청크가 다른 `@layer` 선언보다 먼저 로드되면서 레이어 우선순위가 의도한 것과 달라질 수 있습니다.
**HTML에 `@layer` 선언을 인라인**하여 모든 ` ` 태그보다 먼저 로드되도록 보장할 수 있습니다.
[`transformIndexHtml`](https://vite.dev/guide/api-plugin#transformindexhtml) 플러그인 훅으로 ``의 맨 앞에 `
```
SEED의 레이어 구조 \[#seed의-레이어-구조]
SEED는 두 개의 레이어를 사용합니다.
| 이름 | 내용 | 파일 | 비고 |
| ----------------- | -------------------------- | ----------------------- | ------------------------------ |
| `seed-base` | 디자인 토큰, 글로벌 스타일, keyframes | `base.layered.css` | 별도 import 필요 |
| `seed-components` | 컴포넌트 스타일 | `recipes/*.layered.css` | SEED React 컴포넌트 사용 시 자동 import |
관련 문서 \[#관련-문서]
Tailwind CSS에서 SEED 토큰을 유틸리티 클래스로 사용하는 방법을 알아봅니다.
---
file: getting-started/styling/tailwind-css.mdx
# Tailwind CSS
Tailwind CSS 프로젝트에서 SEED 디자인 시스템을 사용하는 방법을 알아봅니다.
SEED에서 제공하는 토큰을 Tailwind CSS 3과 4 유틸리티 클래스를 통해 쉽게 사용하고, SEED React 컴포넌트의 스타일을 오버라이드할 수 있습니다.
설정 \[#설정]
의존성 설치 \[#의존성-설치]
- npm: npm install @seed-design/css @seed-design/tailwind4-theme
- pnpm: pnpm add @seed-design/css @seed-design/tailwind4-theme
- yarn: yarn add @seed-design/css @seed-design/tailwind4-theme
- bun: bun add @seed-design/css @seed-design/tailwind4-theme
CSS 파일 설정 \[#css-파일-설정]
```css title="style.css"
@layer theme, base, seed-base, components, seed-components, utilities;
@import "@seed-design/css/base.layered.css";
@import "tailwindcss";
@import "@seed-design/tailwind4-theme";
```
`@layer` directive를 통해 SEED CSS와 Tailwind 간의 스타일 우선순위를
결정합니다. 자세한 내용은 [Cascade
Layers](/react/getting-started/styling/cascade-layers) 문서를 참고하세요.
번들러 설정 \[#번들러-설정]
SEED React 컴포넌트가 layered CSS 파일을 사용하도록 번들러의 module resolution 설정에 `seed-layered` condition을 추가하세요.
번들러별 설정 방법은 [Cascade Layers](/react/getting-started/styling/cascade-layers#번들러-설정) 문서를 참고하세요.
Theming 설정 \[#theming-설정]
Tailwind CSS 스타일시트는 theming 스크립트와 별개로 작동합니다. 아래 가이드를 참고하여 테마 설정을 진행하세요.
- [Vite](/react/getting-started/installation/vite)
- [Rsbuild](/react/getting-started/installation/rsbuild)
- [Webpack](/react/getting-started/installation/webpack)
- [Manual](/react/getting-started/installation/manual)
의존성 설치 \[#의존성-설치-1]
- npm: npm install @seed-design/css @seed-design/tailwind3-plugin
- pnpm: pnpm add @seed-design/css @seed-design/tailwind3-plugin
- yarn: yarn add @seed-design/css @seed-design/tailwind3-plugin
- bun: bun add @seed-design/css @seed-design/tailwind3-plugin
base.css import \[#basecss-import]
```ts title="index.tsx"
import "@seed-design/css/base.css";
```
Tailwind 설정 파일 수정 \[#tailwind-설정-파일-수정]
```js title="tailwind.config.js"
import seedPlugin from "@seed-design/tailwind3-plugin"; // [!code highlight]
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [seedPlugin], // [!code highlight]
};
```
Theming 설정 \[#theming-설정-1]
Tailwind CSS 스타일시트는 theming 스크립트와 별개로 작동합니다. 아래 가이드를 참고하여 테마 설정을 진행하세요.
- [Vite](/react/getting-started/installation/vite)
- [Rsbuild](/react/getting-started/installation/rsbuild)
- [Webpack](/react/getting-started/installation/webpack)
- [Manual](/react/getting-started/installation/manual)
유틸리티 클래스 \[#유틸리티-클래스]
SEED 디자인 시스템의 모든 디자인 토큰을 Tailwind 유틸리티 클래스로 사용할 수 있습니다.
색상 \[#색상]
```tsx
import { VStack, Text } from "@seed-design/react";
export default function TailwindColors() {
return (
{/* 텍스트 색상 */}
{/* 배경 색상 */}
bg-bg-informative-weak
bg-palette-gray-200
{/* 테두리 색상 */}
border-stroke-brand-solid
border-palette-red-500
);
}
```
타이포그래피 \[#타이포그래피]
```tsx
import { VStack } from "@seed-design/react";
export default function TailwindTypography() {
return (
screen-title
t3-regular
t3-bold
t4-medium
article-body
);
}
```
크기 및 여백 \[#크기-및-여백]
```tsx
import { VStack, HStack, Text } from "@seed-design/react";
export default function TailwindSpacing() {
return (
{/* 크기 유틸리티 */}
size-x6
w-x8 h-x4
{/* 패딩 유틸리티 */}
p-x2
px-x4 py-x2
{/* 간격 유틸리티 */}
p-x2 gap-x3
);
}
```
테두리 반경 \[#테두리-반경]
```tsx
import { HStack, VStack, Text } from "@seed-design/react";
export default function TailwindBorderRadius() {
return (
rounded-r2
rounded-r4
rounded-full
);
}
```
그라디언트 \[#그라디언트]
```tsx
import { VStack, Text } from "@seed-design/react";
export default function TailwindGradient() {
return (
bg-gradient-glow-magic-[90deg]
bg-gradient-highlight-magic-[90deg]
);
}
```
관련 문서 \[#관련-문서]
Tailwind CSS를 사용하지 않아도 SEED 토큰을 쉽게 사용할 수 있습니다.
레이아웃 컴포넌트에서 SEED 토큰을 사용하는 방법을 알아봅니다.
---
file: getting-started/styling/theming.mdx
# Theming
SEED Design의 테마 시스템을 이해하고 활용하는 방법을 알아봅니다.
개요 \[#개요]
SEED Design은 라이트 모드와 다크 모드를 지원하는 테마 시스템을 제공합니다.
테마는 HTML 요소의 `data-seed-color-mode`와 `data-seed-user-color-scheme` 속성을 통해 제어됩니다.
두 속성의 역할과 차이점을 이해하고, 전역 테마 설정 방법과 특정 영역에만 테마를 오버라이딩하는 방법을 알아봅니다.
테마 속성 이해하기 \[#테마-속성-이해하기]
SEED Design의 테마 시스템은 두 가지 `data-*` 속성을 사용합니다.
`data-seed-color-mode` \[#data-seed-color-mode]
애플리케이션의 **테마 정책**을 설정하는 속성입니다. 세 가지 값을 가질 수 있습니다.
- `system`: 사용자의 시스템 설정에 따라 자동으로 라이트/다크 모드를 전환합니다.
- `light-only`: 항상 라이트 모드로 고정합니다.
- `dark-only`: 항상 다크 모드로 고정합니다.
```html
```
`data-seed-user-color-scheme` \[#data-seed-user-color-scheme]
사용자 기기의 **실제 컬러 스킴**을 나타내는 속성입니다. 두 가지 값을 가집니다.
- `light`: 사용자의 시스템이 라이트 모드로 설정되어 있음
- `dark`: 사용자의 시스템이 다크 모드로 설정되어 있음
이 값은 `data-seed-color-mode="system"`일 때만 의미를 가지며, `prefers-color-scheme` 미디어 쿼리를 통해 자동으로 업데이트됩니다.
```html
```
작동 원리 \[#작동-원리]
SEED CSS는 두 속성의 조합을 통해 적절한 색상 토큰을 적용합니다:
```css
/* 라이트 모드가 적용되는 경우 */
:root,
:root[data-seed-color-mode="system"][data-seed-user-color-scheme="light"],
:root[data-seed-color-mode="light-only"],
:root [data-seed-color-mode="light-only"] {
/* 라이트 모드 색상 토큰 */
}
/* 다크 모드가 적용되는 경우 */
:root[data-seed-color-mode="system"][data-seed-user-color-scheme="dark"],
:root[data-seed-color-mode="dark-only"],
:root [data-seed-color-mode="dark-only"] {
/* 다크 모드 색상 토큰 */
}
```
전역 테마 적용하기 \[#전역-테마-적용하기]
애플리케이션 전체에 테마를 적용하는 방법은 Installation 문서를 참고하세요.
- [Vite](/react/getting-started/installation/vite)
- [Rsbuild](/react/getting-started/installation/rsbuild)
- [Webpack](/react/getting-started/installation/webpack)
- [Manual](/react/getting-started/installation/manual)
번들러 플러그인을 사용하거나, HTML에 직접 속성과 스크립트를 추가하는 방법을 확인할 수 있습니다.
일부 요소에만 테마 오버라이드하기 \[#일부-요소에만-테마-오버라이드하기]
특정 영역이나 컴포넌트에만 다른 테마를 적용하고 싶을 때는 해당 요소에 `data-seed-color-mode`에 `light-only`, `dark-only` 또는 `system` 값을 설정합니다.
```tsx
import { ActionButton } from "seed-design/ui/action-button";
import { actionButtonVariantMap } from "@seed-design/css/recipes/action-button";
import { Text, VStack } from "@seed-design/react";
function Demo() {
return (
<>
{actionButtonVariantMap.variant.map((variant) => (
{variant}
))}
>
);
}
export default function ThemingColorModeOverride() {
return (
라이트 모드
다크 모드
);
}
```
클라이언트에서 현재 테마 정보 감지하기 \[#클라이언트에서-현재-테마-정보-감지하기]
컴포넌트에서 현재 테마 설정에 따라 다른 동작을 수행해야 할 때가 있습니다.
이런 경우 `data-seed-color-mode`와 `data-seed-user-color-scheme` 속성을 감지하여 사용할 수 있습니다.
다음은 커스텀 훅을 만들어 현재 테마 정보를 감지하는 예제입니다:
```tsx
"use client";
import { useEffect, useState } from "react";
export type ColorMode = "system" | "light-only" | "dark-only";
export type UserColorScheme = "light" | "dark";
export interface ThemeInfo {
colorMode: ColorMode;
userColorScheme: UserColorScheme;
}
function readThemeInfo(): ThemeInfo {
if (typeof document === "undefined") {
return {
colorMode: "system",
userColorScheme: "light",
};
}
const colorMode = document.documentElement.getAttribute(
"data-seed-color-mode"
) as ColorMode;
const userColorScheme = document.documentElement.getAttribute(
"data-seed-user-color-scheme"
);
return {
colorMode: colorMode || "system",
userColorScheme: userColorScheme === "dark" ? "dark" : "light",
};
}
export function useTheme(): ThemeInfo {
const [themeInfo, setThemeInfo] = useState(readThemeInfo);
useEffect(() => {
if (typeof document === "undefined") {
return;
}
const observer = new MutationObserver(() => {
setThemeInfo(readThemeInfo());
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-seed-color-mode", "data-seed-user-color-scheme"],
});
return () => observer.disconnect();
}, []);
return themeInfo;
}
```
이 훅은 다음과 같이 사용할 수 있습니다:
```tsx
"use client";
function MyComponent() {
const { colorMode, userColorScheme } = useTheme();
return (
테마 정책: {colorMode}
현재 컬러 스킴: {userColorScheme}
{userColorScheme === "dark" ? (
) : (
)}
);
}
```
작동 원리 \[#작동-원리-1]
- `MutationObserver`를 사용하여 `data-seed-color-mode`와 `data-seed-user-color-scheme` 속성의 변경을 감지합니다.
- `colorMode`는 애플리케이션의 테마 정책(`system`, `light-only`, `dark-only`)을 나타냅니다.
- `userColorScheme`는 현재 적용된 실제 컬러 스킴(`light` 또는 `dark`)을 나타냅니다.
- 시스템 설정이 변경되거나 테마가 전환될 때 자동으로 상태가 업데이트됩니다.
---
file: migration/guide.mdx
# Guide
Sprout에서 SEED로 마이그레이션하기 위해 필요한 작업들을 안내합니다.
마이그레이션 개요 \[#마이그레이션-개요]
Sprout에서 SEED로 넘어가기 위해서는 크게 아래와 같은 순서로 진행됩니다.
1. 이전 버전 용례 분석
2. SEED 설치
3. 파운데이션 마이그레이션 (일괄 적용 권장)
4. 컴포넌트 마이그레이션 (화면별로 적용 권장 및 점진적 적용 가능)
기본적으로 파운데이션까지는 프로젝트 전체에 일괄 교체를 진행합니다.
컴포넌트는 그 이후에 화면별로 적용하거나, 컴포넌트별로 적용하는 것과 같이 점진적 적용이 가능합니다.
SEED 설치 \[#seed-설치]
SEED를 사용하기 위한 기반 작업들을 먼저 진행합니다.
아래 작업들이 끝나면 SEED 사용이 가능합니다.
의존성 설치 \[#의존성-설치]
필요한 의존성들을 설치해요.
- npm: npm install @seed-design/react @seed-design/css
- pnpm: pnpm add @seed-design/react @seed-design/css
- yarn: yarn add @seed-design/react @seed-design/css
- bun: bun add @seed-design/react @seed-design/css
* `@seed-design/react`는 리액트 컴포넌트 라이브러리가 전부 포함되어 있습니다.
- 기존 `sprout` 패키지들을 대체합니다.
- 컴포넌트 마이그레이션 전까지는 기존 패키지들을 사용해주세요.
* `@seed-design/css`는 스타일 관련된 모든 것이 포함되어 있습니다. (vars, theming, recipe, css, etc.)
- 기존 `@seed-design/stylesheet` 라이브러리를 대체합니다.
- 기존 `@seed-design/design-token` 라이브러리를 대체합니다.
- 파운데이션 마이그레이션 전까지는 기존 패키지들을 사용해주세요.
{/* ----------------------------------------------------------------------- */}
{/* ----------------------------------------------------------------------- */}
CSS import \[#css-import]
Sprout `@seed-design/stylesheet` 패키지의 `global.css`와 상호운용이 가능합니다.
번들러 플러그인([webpack](https://github.com/daangn/seed-design/tree/dev/packages/webpack-plugin), [vite](https://github.com/daangn/seed-design/tree/dev/packages/vite-plugin), [rsbuild](https://github.com/daangn/seed-design/tree/dev/packages/rsbuild-plugin))을 사용하는 경우에는 `all.css` 대신 `base.css`를 사용해주세요.
```ts title="index.tsx"
import "@seed-design/css/all.css"; // [!code ++]
```
SEED 스타일시트 사용법은 [Installation의 css import](/react/getting-started/installation/manual#allcss-import하기) 설명을 참고해주세요.
Theme 적용 \[#theme-적용]
변경사항 \[#변경사항]
기존 Sprout에서 사용되던 data-attribute가 바뀌었습니다.
```html
```
- `data-seed`: Seed Design 전역 테마 적용 (SEED에서 그대로 유지)
- `data-seed-color-mode`: Seed Design 테마 강제 적용
- `light-only`, `dark-only`, `system` 중 하나를 선택할 수 있습니다.
- `data-seed-user-color-scheme`: 유저 기기의 설정
- `light`, `dark` 중 하나의 값을 가집니다.
- Sprout에서 `data-seed-scale-color` 속성을 사용하던 코드가 있다면 `data-seed-user-color-scheme` 속성으로 변경해주세요.
테마 속성에 대한 자세한 설명과 활용 방법을 확인하세요.
해당 속성들을 직접 적용하는 방법도 있지만 `플러그인 적용` 방식을 권장합니다.
지원되지 않는 번들러를 사용하는 경우 직접 `스크립트 적용` 방식을 사용해서 적용해주세요.
플러그인 적용 \[#플러그인-적용]
Vite, Rsbuild, Webpack 등의 번들러를 사용하는 프로젝트에서는 플러그인을 추가하면 바로 Theming을 적용할 수 있습니다.
Vite 사용 시
- [@seed-design/vite-plugin](https://github.com/daangn/seed-design/tree/dev/packages/vite-plugin)
* npm: npm install @seed-design/vite-plugin
* pnpm: pnpm add @seed-design/vite-plugin
* yarn: yarn add @seed-design/vite-plugin
* bun: bun add @seed-design/vite-plugin
Rsbuild 사용 시
- [@seed-design/rsbuild-plugin](https://github.com/daangn/seed-design/tree/dev/packages/rsbuild-plugin)
* npm: npm install @seed-design/rsbuild-plugin
* pnpm: pnpm add @seed-design/rsbuild-plugin
* yarn: yarn add @seed-design/rsbuild-plugin
* bun: bun add @seed-design/rsbuild-plugin
Webpack 사용 시
- [@seed-design/webpack-plugin](https://github.com/daangn/seed-design/tree/dev/packages/webpack-plugin)
* npm: npm install @seed-design/webpack-plugin
* pnpm: pnpm add @seed-design/webpack-plugin
* yarn: yarn add @seed-design/webpack-plugin
* bun: bun add @seed-design/webpack-plugin
스크립트 적용 \[#스크립트-적용]
`@seed-design/css/theming` 패키지에서 제공하는 `generateThemingScript` 함수를 사용해서 스크립트를 생성해요.
```ts title="index.tsx"
import { generateThemingScript } from "@seed-design/css/theming";
```
생성된 스크립트를 직접 적용해주세요.
```html
```
혹은 직접 스크립트를 작성하실 수도 있습니다.
```html
```
파운데이션 마이그레이션 \[#파운데이션-마이그레이션]
파운데이션 마이그레이션에는 크게 `Color`와 `Typography` 두 가지에 대해서 작업이 필요합니다.
용례 분석하기 \[#용례-분석하기]
우선 현재 프로젝트에서 `@seed-design/design-token` 패키지를 사용하는 용례를 분석해야합니다.
- `@seed-design/design-token` 패키지의 vars를 사용하는 용례
- `@seed-design/design-token` 패키지의 vars를 확장해서 사용하는 컴포넌트
- 위 컴포넌트를 사용중인 코드 용례
- `@seed-design/stylesheet` 패키지를 사용하는 용례
- `--seed-` prefix로 시작하는 CSS Variable을 사용하는 용례
- 사용중인 CSS 라이브러리와 `@seed-design/design-token` 패키지와 결합해서 사용하는 용례
아래의 프롬프트를 이용해서 현재 프로젝트에서 사용중인 용례를 분석할 수도 있습니다.
```text title="prompt"
Seed Design System의 디자인 토큰을 새로운 토큰으로 마이그레이션
할건데 코드 어디 부분들을 변경해야 하는지 보고서를 써줘.
1. `@seed-design/design-token` 패키지의 vars 변수를 사용하는 용례
2. `@seed-design/design-token` 패키지의 vars 변수를 확장해서 사용하는 컴포넌트
3. 2번 컴포넌트를 사용중인 코드 용례
4. `@seed-design/styleesheet` 패키지를 사용하는 용례
5. `--seed-` prefix로 시작하는 CSS Variable을 사용하는 용례
6. 사용중인 CSS 라이브러리와 `@seed-design/design-token` 패키지와 결합해서 사용하는 용례
위와 같은 사례들말고도 네가 더 분석해서 추가적으로 마이그레이션 해야하는 부분들도 알려줘.
```
마이그레이션 전략 \[#마이그레이션-전략]
- 만약 여러 패키지를 사용하는 경우에는 공통 패키지 먼저 마이그레이션 합니다.
- 전처리해서 사용하는 경우에는 전처리 코드 자체를 SEED로 마이그레이션 한 후 나머지 코드들을 Codemod로 마이그레이션 하는게 좋습니다.
- Color와 Typography 각각 진행해도 되지만, Color와 Typography 마이그레이션을 동시에 진행 후 한번에 QA를 진행하는게 더 간결합니다.
Typography Preset \[#typography-preset]
많은 프로젝트에서 기존 타이포그래피 토큰을 이용해서 유틸리티 함수를 만들어서 사용하는 경우가 있습니다.
이런 경우에는 아래의 코드를 참고해서 전처리를 진행 후 코드들을 그에 맞게 변경해주세요.
- [replace-custom-seed-design-typography](https://seed-design.io/react/developer-tools/codemods/available-transforms#replace-custom-seed-design-typography) 커스텀 transform을 사용해서 마이그레이션 할 수 있습니다.
```ts title="typography.ts"
import type { TextVariant } from '@seed-design/css/recipes/text';
import { vars as typographyVars } from '@seed-design/css/vars/component/typography';
/**
* @see https://seed-design.io/docs/foundation/typography/overview
* @example
* typography.t1Bold
* typography.t2Regular
* typography.t3Bold
* typography.t4Regular
* typography.t5Bold
* typography.t6Bold
* typography.t7Bold
* typography.screenTitle
* ...
*/
export const typography = Object.entries(typographyVars).reduce(
(acc, [key, value]) => {
const withoutPrefix = key.replace('textStyle', '');
const firstChar = withoutPrefix.charAt(0).toLowerCase();
const restOfString = withoutPrefix.slice(1);
const name = (firstChar + restOfString) as TextVariant['textStyle'];
acc[name] = value.enabled.root;
return acc;
},
{} as Record<
TextVariant['textStyle'],
{
fontSize: string;
fontWeight: string;
lineHeight: string;
}
>,
);
```
Codemod \[#codemod]
기본적으로 마이그레이션은 [Codemod](/react/developer-tools/codemods/introduction)를 이용해 진행합니다.
- npm: npx @seed-design/codemod@latest \ \ \<옵션>
- pnpm: pnpm dlx @seed-design/codemod@latest \ \ \<옵션>
- yarn: yarn dlx @seed-design/codemod@latest \ \ \<옵션>
- bun: bun x @seed-design/codemod@latest \ \ \<옵션>
* Codemod는 기본적으로 `color`와 `typography` transform을 분리해서 제공합니다.
* 현재는 Tailwind CSS, Stitches 혹은 Emotion, Vanilla Extract CSS와 같은 CSS-in-JS 라이브러리에 대한 transform을 제공합니다.
* 각팀에서 커스텀으로 제공되는 transform의 경우에는 `replace-custom-`의 prefix로 제공됩니다.
* 커스텀 transform의 변경 예시를 살펴보고 프로젝트와 상황이 같다면 사용할 수 있습니다.
* 추가로 특정 라이브러리의 transform이 필요하거나, 필요한 경우 커스텀 transform을 만들 수 있습니다. (디자인코어 팀에 문의)
Tailwind CSS \[#tailwind-css]
- Tailwind V3 플러그인을 제공합니다.
- [@seed-design/tailwind3-plugin](https://github.com/daangn/seed-design/tree/dev/packages/tailwind3-plugin)
- [@seed-design/tailwind4-theme](https://github.com/daangn/seed-design/tree/dev/packages/tailwind4-theme)
- Tailwind transform은 [replace-tailwind-typography](/react/developer-tools/codemods/available-transforms#replace-tailwind-typography), [replace-tailwind-color](/react/developer-tools/codemods/available-transforms#replace-tailwind-color) 두 개가 제공됩니다.
- Tailwind 같은 경우는 각 프로젝트에서 커스텀 플러그인을 작성해서 사용중인 경우가 많기 때문에 Codemod가 정확하게 맞지 않을 수 있습니다.
- Codemod 페이지에서 변경 예시를 살펴보고 프로젝트와 상황이 같다면 사용할 수 있습니다.
- 예시와 맞지 않다면 디자인코어팀에 문의해주세요.
Stitches CSS \[#stitches-css]
- Stitches는 [replace-stitches-styled-typography](/react/developer-tools/codemods/available-transforms#replace-stitches-styled-typography), [replace-stitches-styled-color](/react/developer-tools/codemods/available-transforms#replace-stitches-styled-color), [replace-stitches-theme-color](/react/developer-tools/codemods/available-transforms#replace-stitches-theme-color) 의 형태로 제공합니다.
- 자세한 내용은 [Codemod](/react/developer-tools/codemods/introduction) 페이지를 참고해주세요.
Emotion, Vanilla Extract CSS, (CSS-in-JS) \[#emotion-vanilla-extract-css-css-in-js]
- Emotion, Vanilla Extract CSS와 같은 CSS-in-JS 라이브러리는 `@seed-design/design-token`을 사용하는 경우가 대부분입니다.
- [replace-seed-design-token-vars](/react/developer-tools/codemods/available-transforms#replace-seed-design-token-vars) transform을 사용해서 일괄적으로 마이그레이션 할 수 있습니다.
- 해당 transform은 `color`와 `typography` 두 개 동시에 변경합니다.
Native CSS \[#native-css]
- 기본적으로 `--seed-` prefix가 붙은 CSS Variable을 사용하는 경우가 있습니다.
- [replace-css-seed-design-color-variable](/react/developer-tools/codemods/available-transforms#replace-css-seed-design-color-variable) CSS transform도 제공합니다.
Codemod 사용 시 주의사항 \[#codemod-사용-시-주의사항]
Codemod는 다수의 파일을 변경하기 때문에 주의가 필요합니다.
- 변경사항 추적과 히스토리 관리를 위해서 별도의 브랜치를 사용할 것을 권장합니다.
- transform에 따라 커밋을 나누는 것을 권장합니다.
- Codemod 실행 후 `lint`와 `test`를 진행해주세요.
Codemod Log 파일 \[#codemod-log-파일]
Codemod 실행 시 디버깅에 도움을 주는 로그 파일을 생성할 수 있습니다.
- `--log` 옵션을 추가해주세요.
- 로그 파일은 Codemod 실행 경로에 `.report` 폴더에 생성됩니다.
- 로그 파일은 변경사항을 추적하고 히스토리 관리에 도움을 줍니다.
debug.log \[#debuglog]
- 디버깅 정보를 기록합니다.
- 변경 시도한 파일 경로와 시간을 기록합니다.
```log title="debug.log"
2025-03-28 19:17:36 [DEBUG]: Starting transformation of /Project/src/component1.tsx
2025-03-28 19:17:36 [DEBUG]: Starting transformation of /Project/src/component2.tsx
2025-03-28 19:17:36 [DEBUG]: Starting transformation of /Project/src/component3.tsx
2025-03-28 19:17:36 [DEBUG]: Starting transformation of /Project/src/component4.tsx
```
issues.log \[#issueslog]
- 확인이 필요한 변경사항을 기록합니다.
- 문제가 있는 파일 경로와 라인 번호를 기록합니다.
- 문제가 있는 이유를 기록합니다.
- 매핑이 없는 경우를 제외하고는 이미 변경된 상태입니다.
```log title="issues.log"
2025-03-28 18:46:55 [WARN]: /Project/src/component1.tsx (line: 171)
↳ reason: $gray900-static static에서 palette로 변경되는 색상으로 용도 파악 필요
2025-03-28 18:46:55 [WARN]: /Project/src/component2.tsx (line: 194)
↳ reason: $gray900-static static에서 palette로 변경되는 색상으로 용도 파악 필요
2025-03-28 18:46:55 [WARN]: /Project/src/component3.tsx (line: 147)
↳ reason: $gray900-static static에서 palette로 변경되는 색상으로 용도 파악 필요
2025-03-28 18:46:56 [WARN]: /Project/src/component4.tsx (line: 242)
↳ reason: $gray900-static static에서 palette로 변경되는 색상으로 용도 파악 필요
```
자주 발생하는 문제들은 제일 아래 [issues.log 파일에 생긴 문제들은 어떻게 해결해야 하나요?](#issueslog-파일에-생긴-문제들은-어떻게-해결해야-하나요)에 정리해두었습니다.
success.log \[#successlog]
- 변경사항 추적에 도움을 줍니다.
- 변경된 파일 경로와 라인 번호를 기록합니다
- 변경된 토큰을 기록합니다.
- CSS 파일을 변경하는 transform의 경우는 토큰을 일일이 기록하지 않습니다.
- tailwind 같은 경우는 변경된 className을 기록합니다.
```log title="success.log"
2025-03-28 18:46:54 /Project/src/hooks/hook1.tsx (line: 149): $gray900 → $palette-gray-1000 (line 149)
2025-03-28 18:46:54 /Project/src/hooks/hook2.tsx (line: 155): $gray700 → $palette-gray-800 (line 155)
2025-03-28 18:46:54 /Project/src/components/component1.tsx (line: 54): $divider1-semantic → $stroke-neutral-muted (line 54)
2025-03-28 18:46:54 /Project/src/components/component2.tsx (line: 60): $paperDefault-semantic → $bg-layer-default (line 60)
```
Sprout-SEED Compatibility \[#sprout-seed-compatibility]
Color, Typography 마이그레이션이 끝나고 나면 기존 Sprout 컴포넌트와의 호환성 문제가 있을 수 있습니다.
Sprout 내부에 Sprout 토큰을 SEED 토큰으로 전부 교체한 Compatibility 버전을 제공합니다.
여기까지 진행해야 파운데이션 마이그레이션이 끝난 것으로 간주할 수 있습니다.
[Sprout Compatibility 버전](https://github.com/daangn/sprout/pkgs/npm/sprout-components-button) 페이지에서
`-v3-compat` 태그가 붙은 버전중에 최신 버전으로 모든 컴포넌트를 전부 교체해주시면 됩니다. (모든 컴포넌트 동일한 버전을 제공합니다.)
주의할 점: 너무 예전의 버전을 사용하는 경우 컴포넌트 내부에서 breaking changes가 있을 수 있습니다.
Sprout-SEED Compatibility 버전까지 설치가 되었다면 파운데이션 마이그레이션이 끝난 것으로 간주할 수 있습니다.
이론상 이때부터는 `@seed-design/design-token` 패키지와 `@seed-design/stylesheet` 패키지를 제거할 수 있습니다.
또한 SEED 컴포넌트를 점진적 사용이 가능합니다.
Compat Script 제거 \[#compat-script-제거]
```html
```
기존에 25.10.0 앱버전 이상에서 Sprout 백그라운드 색상을 SEED로 변경하기 위해 추가했던 스크립트는
마이그레이션이 끝나고 제거해도 됩니다.
컴포넌트 마이그레이션 \[#컴포넌트-마이그레이션]
TBD
자주 묻는 질문 \[#자주-묻는-질문]
issues.log 파일에 생긴 문제들은 어떻게 해결해야 하나요? \[#issueslog-파일에-생긴-문제들은-어떻게-해결해야-하나요]
토큰 매핑이 안되는 경우 (컬러, 타이포그래피) \[#토큰-매핑이-안되는-경우-컬러-타이포그래피]
- SEED에서 지원하지 않는 토큰을 사용하는 경우입니다. (주로 alpha 색상 토큰이거나 너무 작거나 큰 타이포 토큰입니다.)
- 토큰 매핑을 확인할 수 있는 방법은 [설명](#파운데이션-토큰-매핑은-어디서-확인할-수-있나요)을 참고해주세요.
- 해당 경우는 디자이너분과 상의 후 다른 적절한 토큰으로 직접 매핑해주세요.
- 결정에 어려움이 있는 경우 디자인코어팀에 문의해주세요.
alpha 토큰이 사라진 경우 (컬러) \[#alpha-토큰이-사라진-경우-컬러]
- alpha가 들어간 토큰은 대부분의 경우 더 이상 사용되지 않습니다.
- 직접 CSS에 `alpha` 채널을 적용해주세요. (opacity 속성을 사용하거나, rgba 형식으로 직접 적용)
static 토큰이 palette로 변경된 경우 (컬러) \[#static-토큰이-palette로-변경된-경우-컬러]
- SEED에서 static 컬러 토큰은 웬만해서는 `white`, `black` 두 가지만 지원합니다.
- 나머지 색상 (yellow, green, blue, red, gray) 등은 palette 토큰으로 변경되었습니다.
- 다크모드와 라이트모드에 따라 색상이 달라질 수 있기 때문에 확인이 필요합니다.
- 배경은 변하는데 텍스트 색상이 변하지 않거나, 그 반대의 상황이 나올 수 있습니다.
- 대부분의 경우는 static 토큰이 필요한 경우가 아니기 때문에 palette 토큰으로 변경되었습니다.
- 이 경우는 디자이너분과 상의 후 기존 static 토큰을 다른 적절한 palette 토큰으로 직접 매핑해주세요.
선택지가 두개인 토큰으로 매핑된 경우 (컬러) \[#선택지가-두개인-토큰으로-매핑된-경우-컬러]
- 컬러 토큰의 경우 대체 토큰이 두 가지인 경우가 있습니다. (ex. `primary` -> `bg.brand-solid`, `fg.brand`)
- `fg`는 텍스트, 아이콘 등 UI의 전경 요소에 사용되는 색상입니다. (color, fill, etc.)
- `bg`는 전체 화면 또는 UI의 배경에 사용하는 색상입니다. (backgroundColor, etc.)
- 변경되어야 하는 토큰을 Codemod가 판단하기 어려울 수 있습니다.
- 이 경우는 직접 적절한 토큰으로 매핑해주세요.
alternate 토큰으로 변경된 경우 (타이포그래피) \[#alternate-토큰으로-변경된-경우-타이포그래피]
- 기본적으로 [Mapping](/docs/migration/migration-reference)은 1:1 매핑을 지원합니다.
- 하지만 SEED에서 Deprecated 된 토큰인 경우, 대체 토큰으로 매핑 될 수 있습니다.
- 그런 경우 font size나 line height가 변경되었을 수 있습니다.
- 이 경우는 직접 UI를 확인해야 합니다.
파운데이션 토큰 매핑은 어디서 확인할 수 있나요? \[#파운데이션-토큰-매핑은-어디서-확인할-수-있나요]
[Migration Reference](/docs/migration/migration-reference)에서 토큰 매핑을 확인할 수 있습니다.
Known Issues \[#known-issues]
Vanilla Extract CSS + Vite + SEED 사용시 \[#vanilla-extract-css--vite--seed-사용시]
- `@vanilla-extract/vite-plugin`와 `@seed-design/vite-plugin`을 같이 사용하는 경우 빌드가 실패할 수 있습니다.
- `@vanilla-extract/vite-plugin`의 버전을 `5` 이상으로 업그레이드 해주세요.
---
file: migration/migrating-icons.mdx
# Migrating Icons
구형 아이콘을 신규 아이콘으로 쉽게 마이그레이션해요.
업그레이드 방법 \[#업그레이드-방법]
현재 아이콘을 사용하고 있는 방법에 따라 적용할 수 있는 2가지 방법이 있어요.
Codemod 스크립트 \[#codemod-스크립트]
아래 3개 아이콘 패키지를 사용하고 있다면, 제공되는 codemod 스크립트를 사용해서 리뉴얼된 아이콘 패키지로 손쉽게 업그레이드할 수 있어요.
- [`@seed-deesign/react-icon`](https://www.npmjs.com/package/@seed-design/react-icon)
- [`@seed-design/icon`](https://www.npmjs.com/package/@seed-design/icon) (deprecated)
- [`@karrotmarket/karrot-ui-icon`](https://www.npmjs.com/package/@karrotmarket/karrot-ui-icon) (deprecated) (React 컴포넌트를 사용하는 경우)
3개 패키지에서 사용하고 있는 아이콘은 [`@karrotmarket/react-monochrome-icon`](https://github.com/daangn/seed-icon-v3/pkgs/npm/react-monochrome-icon)의 아이콘으로 대체돼요.
수동 업그레이드 \[#수동-업그레이드]
다음과 같은 경우에는 수동으로 업그레이드해야 해요.
- PNG, SVG 등의 파일로 기존 아이콘을 사용하는 경우
- [`@karrotmarket/karrot-ui-icon`](https://www.npmjs.com/package/@karrotmarket/karrot-ui-icon) (deprecated)에서 제공되는 SVG 파일을 사용하는 경우
- 다음 패키지를 사용하는 경우
- [`@seed-design/vue2-icon`](https://www.npmjs.com/package/@seed-design/vue2-icon) → [`@daangn/vue2-monochrome-icon`](https://github.com/daangn/seed-icon-v3/pkgs/npm/vue2-monochrome-icon)으로 대체돼요.
- [`@seed-design/vue3-icon`](https://www.npmjs.com/package/@seed-design/vue3-icon) → [`@daangn/vue3-monochrome-icon`](https://github.com/daangn/seed-icon-v3/pkgs/npm/vue3-monochrome-icon)으로 대체돼요.
작업 순서 \[#작업-순서]
많은 변경사항이 발생하게 되므로, 커밋하지 않은 변경사항이 없는지 확인하고 시작하는 것이 좋아요.
Codemod 스크립트 \[#codemod-스크립트-1]
리뉴얼된 아이콘 패키지 설치 \[#리뉴얼된-아이콘-패키지-설치]
- 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
2개 패키지 중 필요한 패키지만 설치해요.
- [`@karrotmarket/react-monochrome-icon`](https://github.com/daangn/seed-icon-v3/pkgs/npm/react-monochrome-icon)
- 단색 아이콘 패키지에요. **일반** 아이콘, **카테고리** 아이콘, **서비스** 아이콘이 포함되어 있어요.
- 이 패키지에서 제공되는 서비스 아이콘은 검은색으로만 사용할 수 있어요.
- codemod 실행 시, 기존 패키지들의 아이콘은 모두 이 패키지의 아이콘으로 대체돼요.
- [`@karrotmarket/react-multicolor-icon`](https://github.com/daangn/seed-icon-v3/pkgs/npm/react-multicolor-icon)
- 멀티컬러 아이콘 패키지에요. **카테고리** 아이콘과 **서비스** 아이콘이 포함되어 있어요.
- 이 패키지에서 제공되는 카테고리 아이콘과 서비스 아이콘은 패키지에서 제공되는 색상 그대로만 사용할 수 있어요.
- 기존에 SVG로 사용하던 멀티컬러 아이콘들을 이 패키지로 직접 대체할 수 있어요. 따라서, 필요한 경우에만 설치해도 좋아요.
Codemod 스크립트 실행 \[#codemod-스크립트-실행]
`replace-react-icon` transform을 실행해요.
```shell
npx @seed-design/codemod@latest replace-react-icon
```
해당 transform에 대해 자세히 알아봅니다.
결과 확인 \[#결과-확인]
문제 없이 모든 변환이 완료된 경우 다음과 같은 결과가 표시돼요.
```
All done.
Results:
0 errors // [!code highlight]
439 unmodified
1 skipped
27 ok
Time elapsed: 43.376seconds
```
- `errors`: 파싱 오류 등으로 인해, 아이콘을 참조하는 코드가 있는지 확인하지 못한 파일의 수에요.
- 에러가 발생한 파일에 이전 아이콘을 참조하는 코드가 없다면, 해당 파일에서 발생하는 에러는 무시해도 좋아요.
- 이전 아이콘을 참조하는 코드가 있다면, 문제가 발생한 부분을 수정하고 다시 스크립트를 실행하거나, 해당 파일의 아이콘 마이그레이션을 직접 진행해주세요.
- `unmodified`: 아이콘을 참조하는 코드가 없어, 아무 변환도 이루어지지 않은 파일의 수에요.
- `skipped`: 빈 파일 등 변환 대상이 아니어서, 아무 변환도 이루어지지 않은 파일의 수에요.
- `ok`: 변환이 이루어진 파일의 수에요.
```
ERR /Users/seed/foo/bar.js Transformation error (Unexpected reserved word 'package'. (3:3))
```
codemod 스크립트 실행 중 파싱 오류가 발생할 수 있어요. 파싱 오류는 다음과 같은 이유로 발생해요.
- `assert`를 사용한 [import assertion](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-3.html#import-attributes)과 같은, deprecated된 문법이 있는 경우
- `package`와 같은 예약어를 변수 이름으로 사용한 경우 등
코드 포맷 \[#코드-포맷]
프로젝트에서 사용하는 포매터를 사용해서 코드를 포맷해요.
이전 패키지 제거 \[#이전-패키지-제거]
이전 아이콘이 모두 대체된 것을 확인하고, 이전 패키지를 제거해요.
- npm: npm uninstall @seed-design/icon @seed-design/react-icon @karrotmarket/karrot-ui-icon
- pnpm: pnpm remove @seed-design/icon @seed-design/react-icon @karrotmarket/karrot-ui-icon
- yarn: yarn remove @seed-design/icon @seed-design/react-icon @karrotmarket/karrot-ui-icon
- bun: bun remove @seed-design/icon @seed-design/react-icon @karrotmarket/karrot-ui-icon
사이드 이펙트 확인 \[#사이드-이펙트-확인]
- [사이드 이펙트](#발생-가능한-사이드-이펙트)가 발생했는지 확인하여 의도한 대로 변경이 이루어졌는지 검토해요.
- 디자인 QA 과정에서, 자동으로 변경된 아이콘이 아닌 새로운 아이콘을 사용하도록 결정되었을 수 있어요. 이러한 경우, Figma 디자인 파일을 참고하여 코드에 반영해요.
- 멀티컬러 아이콘 사용이 필요한 곳에는 [멀티컬러 아이콘 패키지](https://github.com/daangn/seed-icon-v3/pkgs/npm/react-multicolor-icon)를 사용해주세요.
수동 업그레이드 \[#수동-업그레이드-1]
이전 패키지 제거 \[#이전-패키지-제거-1]
이전 아이콘 패키지들을 제거해요.
- npm: npm uninstall @seed-design/icon @seed-design/react-icon @karrotmarket/karrot-ui-icon @seed-design/vue2-icon @seed-design/vue3-icon
- pnpm: pnpm remove @seed-design/icon @seed-design/react-icon @karrotmarket/karrot-ui-icon @seed-design/vue2-icon @seed-design/vue3-icon
- yarn: yarn remove @seed-design/icon @seed-design/react-icon @karrotmarket/karrot-ui-icon @seed-design/vue2-icon @seed-design/vue3-icon
- bun: bun remove @seed-design/icon @seed-design/react-icon @karrotmarket/karrot-ui-icon @seed-design/vue2-icon @seed-design/vue3-icon
리뉴얼된 패키지 설치 \[#리뉴얼된-패키지-설치]
React \[#react]
- 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
Vue 2 \[#vue-2]
- npm: npm install @daangn/vue2-monochrome-icon @daangn/vue2-multicolor-icon
- pnpm: pnpm add @daangn/vue2-monochrome-icon @daangn/vue2-multicolor-icon
- yarn: yarn add @daangn/vue2-monochrome-icon @daangn/vue2-multicolor-icon
- bun: bun add @daangn/vue2-monochrome-icon @daangn/vue2-multicolor-icon
Vue 3 \[#vue-3]
- npm: npm install @daangn/vue3-monochrome-icon @daangn/vue3-multicolor-icon
- pnpm: pnpm add @daangn/vue3-monochrome-icon @daangn/vue3-multicolor-icon
- yarn: yarn add @daangn/vue3-monochrome-icon @daangn/vue3-multicolor-icon
- bun: bun add @daangn/vue3-monochrome-icon @daangn/vue3-multicolor-icon
2개 패키지 중 필요한 패키지만 설치해요.
- `@daangn/*-monochrome-icon`
- 단색 아이콘 패키지에요. **일반** 아이콘, **카테고리** 아이콘, **서비스** 아이콘이 포함되어 있어요.
- 이 패키지에서 제공되는 서비스 아이콘은 검은색으로만 사용할 수 있어요.
- codemod 실행 시, 기존 패키지들의 아이콘은 모두 이 패키지의 아이콘으로 대체돼요.
- `@daangn/*-multicolor-icon`
- 멀티컬러 아이콘 패키지에요. **카테고리** 아이콘과 **서비스** 아이콘이 포함되어 있어요.
- 이 패키지에서 제공되는 카테고리 아이콘과 서비스 아이콘은 패키지에서 제공되는 색상 그대로만 사용할 수 있어요.
- 기존에 SVG로 사용하던 멀티컬러 아이콘들을 이 패키지로 직접 대체할 수 있어요. 따라서, 필요한 경우에만 설치해도 좋아요.
아이콘 이름 변경 \[#아이콘-이름-변경]
[아이콘 마이그레이션 레퍼런스](/docs/migration/migration-reference#iconography) 문서를 참고하여 리뉴얼된 패키지를 사용하도록 코드를 수정해요.
- 기존 아이콘의 `IconHome*`은 신규 아이콘에서 `IconHouse*`로 이름이 바뀌었어요.
- 기존 아이콘의 `IconHouse*`는 신규 아이콘에서 `IconWindow4House*`로 이름이 바뀌었어요.
따라서, 수동 마이그레이션 시 `IconHome*`이 `IconWindow4House*`로 잘못 변경되지 않게 주의해야 해요.
**`IconHouse*`를 먼저 마이그레이션**하고, `IconHome*`을 마이그레이션하는 것을 추천해요.
사이드 이펙트 확인 \[#사이드-이펙트-확인-1]
- [사이드 이펙트](#발생-가능한-사이드-이펙트)가 발생했는지 확인하여 의도한 대로 변경이 이루어졌는지 검토해요.
- 디자인 QA 과정에서, [아이콘 마이그레이션 레퍼런스](/docs/migration/migration-reference#iconography)에 따른 아이콘이 아닌 새로운 아이콘을 사용하도록 결정되었을 수 있어요. 이러한 경우, Figma 디자인 파일을 참고하여 코드에 반영해요.
- 멀티컬러 아이콘 사용이 필요한 곳에는 [멀티컬러 아이콘 패키지](https://github.com/daangn/seed-icon-v3/pkgs/npm/react-multicolor-icon)를 사용해주세요.
발생 가능한 사이드 이펙트 \[#발생-가능한-사이드-이펙트]
정확히 대응되는 리뉴얼된 아이콘이 없는 경우 \[#정확히-대응되는-리뉴얼된-아이콘이-없는-경우]
- 이전 아이콘 중 아래 5개 아이콘(총 15개 variant)은 리뉴얼된 아이콘 패키지에 시각적으로 정확히 대응되는 항목이 없어요.
- codemod 스크립트 실행 시 **추천되는 아이콘으로 변환되지만**, 시각적인 차이가 크기 때문에 변경 후 확인이 필요해요.
| 이전 아이콘 이름 | 리뉴얼된 아이콘 이름 |
| --------------------------------------- | ---------------------------------------------------------------------------------------- |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
- codemod 스크립트 사용 시, 5개 아이콘(총 15개 variant)이 사용된 경우 터미널에 메시지가 출력돼요.
```
REP ...을 ...로 변경했지만, 변경된 아이콘이 적절한지 확인이 필요해요
```
- codemod 스크립트를 `--log` flag와 함께 사용 시, 사용된 내역이 `migrate-icons-warnings.log`에 기록돼요.
- 다음을 활용하여 코드를 직접 검색할 수 있어요.
<>
```regex
(IconBold|IconCobuying|IconDelivery|IconSuggest|IconWriteStory)(Thin|Regular|Fill)|(IconTUppercaseSerif|IconShoppingbag2Stacked|IconTruck|IconLightbulbDot5|IconHorizline2VerticalChatbubbleRight)(Line|Fill)
```
```shell
git grep -E '(IconBold|IconCobuying|IconDelivery|IconSuggest|IconWriteStory)(Thin|Regular|Fill)|(IconTUppercaseSerif|IconShoppingbag2Stacked|IconTruck|IconLightbulbDot5|IconHorizline2VerticalChatbubbleRight)(Line|Fill)'
```
>
여러 이전 아이콘이 하나의 리뉴얼된 아이콘으로 대체되는 경우 \[#여러-이전-아이콘이-하나의-리뉴얼된-아이콘으로-대체되는-경우]
**이전 아이콘과 리뉴얼된 아이콘은 `n:1`로 대응돼요.** 따라서, 같은 페이지에 표시되었던 서로 다른 여러 개의 아이콘이, 동일한 리뉴얼된 아이콘으로 대체되는 경우가 있어요. 의도한 목적대로 아이콘이 표시되는지 확인해요.
- 한 이전 아이콘의 3개 variant 중 `Thin` variant와 `Regular` variant는 리뉴얼된 아이콘에서 모두 `Line` variant로 대체돼요.
- 예를 들면, `IconForwardThin`, `IconForwardRegular`는 모두 [`IconArrowRightLine`](/docs/foundation/iconography/library?icon=icon_arrow_right_line)으로 대체돼요.
- 이전 아이콘의 `Fill` variant는 리뉴얼된 아이콘에서도 `Fill` variant로 대체돼요.
- 여러 개의 이전 아이콘이 하나의 리뉴얼된 아이콘으로 대체되는 경우도 있어요.
- 예를 들면, `IconHeadphoneRegular`, `IconHelpcenterRegular`, `IconHelperRegular`는 모두 [`IconHeadsetLine`](/docs/foundation/iconography/library?icon=icon_headset_line)으로 대체돼요.
---
file: stackflow/alert-dialog.mdx
# Alert Dialog
Alert Dialog를 Stackflow와 함께 사용하는 방법을 안내합니다.
Alert Dialog 컴포넌트에 대해 자세히 알아봅니다.
Making an Alert Dialog Activity \[#making-an-alert-dialog-activity]
**일반적인 경우 Alert Dialog를 [Activity](https://stackflow.so/docs/get-started/activity)로 만들어 사용하는 것을 권장합니다.**
- Activity로 관리되므로, 하위 Activity보다 높고 상위 Activity보다는 낮은 z-index를 갖도록 관리하기 쉽습니다.
- 딥링킹이 가능합니다. (URL 접속으로 Alert Dialog를 열 수 있습니다.)
- [@stackflow/plugin-basic-ui](https://www.npmjs.com/package/@stackflow/plugin-basic-ui) `Modal`에서의 마이그레이션이 쉽습니다.
```tsx title='ActivityAlertDialogActivity.tsx'
import { VStack } from "@seed-design/react";
import { useFlow, type StaticActivityComponentType } from "@stackflow/react/future";
import {
AppBar,
AppBarBackButton,
AppBarLeft,
AppBarMain,
AppBarIconButton,
AppBarRight,
} from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
import { ActionButton } from "seed-design/ui/action-button";
import { IconHouseLine } from "@karrotmarket/react-monochrome-icon";
declare module "@stackflow/config" {
interface Register {
ActivityAlertDialogActivity: {};
}
}
const ActivityAlertDialogActivity: StaticActivityComponentType<
"ActivityAlertDialogActivity"
> = () => {
const { push } = useFlow();
return (
push("ActivityHome", {})}>
push("ActivityAlertDialog", {})}
>
ActivityAlertDialog를 Push
push("ActivityAlertDialogActivity", {})}
>
지금 열린 이 Activity를 Push
);
};
export default ActivityAlertDialogActivity;
```
```tsx title='ActivityAlertDialog.tsx'
import { useActivity, useFlow, type StaticActivityComponentType } from "@stackflow/react/future";
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 { Divider, ResponsivePair, VStack } from "@seed-design/react";
import { send } from "@stackflow/compat-await-push";
import { useActivityZIndexBase } from "@seed-design/stackflow";
import { Switch } from "seed-design/ui/switch";
declare module "@stackflow/config" {
interface Register {
ActivityAlertDialog: {};
}
}
const ActivityAlertDialog: StaticActivityComponentType<"ActivityAlertDialog"> = () => {
const activity = useActivity();
const { pop, push } = useFlow();
const [keepMounted, setKeepMounted] = useState(false);
const handleClose = (open: boolean) => {
if (!open) {
pop();
send({
activityId: activity.id,
data: {
message: "hello",
},
});
}
};
const open = keepMounted
? activity.transitionState === "enter-active" || activity.transitionState === "enter-done"
: activity.isActive;
const onOpenChange = keepMounted
? (open: boolean) => !open && activity.isActive && handleClose(open)
: handleClose;
return (
제목
다람쥐 헌 쳇바퀴에 타고파
확인
push("ActivityDetail", {
title: "AlertDialog에서 Push됨",
body: keepMounted
? "AlertDialog가 언마운트되지 않았으므로, 현재 Activity를 pop하는 경우 AlertDialog가 열린 상태로 표시됩니다."
: "AlertDialog가 언마운트되었으므로, 현재 Activity를 pop하는 경우 AlertDialog가 다시 enter 트랜지션을 재생하며 마운트됩니다.",
})
}
>
Push
);
};
export default ActivityAlertDialog;
```
Usage \[#usage]
```tsx
import { useActivityZIndexBase } from "@seed-design/stackflow";
import { useActivity, useFlow, type ActivityComponentType } from "@stackflow/react/future";
// ... more imports
const ActivityAlertDialog: ActivityComponentType<"ActivityAlertDialog"> = () => {
const { pop, push } = useFlow();
return (
{/* [!code highlight] */}
!open && pop()}>
{/* [!code highlight] */}
제목
설명 텍스트
확인
push("AnotherActivity", {})}
>
다른 화면으로 이동
);
};
```
1. `open` prop에 `useActivity().isActive`를 전달하여 Activity가 활성화될 때 Alert Dialog가 열리도록 합니다.
2. `onOpenChange`를 통해 Alert Dialog가 닫힐 때 `pop()`을 실행하여 Activity를 종료합니다.
3. `layerIndex={useActivityZIndexBase()}`로 Alert Dialog Activity의 z-index 기준점을 전달합니다.
- [`useActivityZIndexBase`에 대해 자세히 알아보기](#about-useactivityzindexbase)
Keeping Alert Dialog Mounted \[#keeping-alert-dialog-mounted]
Alert Dialog Activity 위에 다른 Activity를 push할 때 Alert Dialog가 unmount되는 것을 방지하려면,
- `open` 상태를 `isActive` 대신 `transitionState`로 관리하고
- `onOpenChange` 핸들러에서 `!isOpen && isActive`인 경우 `pop()`을 실행하도록 합니다.
이 패턴은 Alert Dialog 액티비티 위에 다른 오버레이 컴포넌트 액티비티를 중첩하여 표시하고 싶은 경우 유용합니다.
```tsx
const { isActive, transitionState } = useActivity();
return (
!open && isActive && pop()}
>
{/* ... */}
);
```
Syncing Alert Dialog State with a Step \[#syncing-alert-dialog-state-with-a-step]
Alert Dialog를 Activity로 만들 수 없는 경우, Alert Dialog가 표시된 상태를 [Step](https://stackflow.so/docs/get-started/navigating-step)으로 만들 수 있습니다.
- 현재 Activity를 유지하면서도, 뒤로 가기 버튼 등으로 Alert Dialog를 닫을 수 있습니다.
- `AlertDialogTrigger`를 사용하여 Alert Dialog를 열고 닫을 수 있습니다.
**Activity로 만들지 않은 Alert Dialog에서 다른 Activity를 push하기 전, z-index 문제를 방지하기 위해 Alert Dialog를 닫으세요.**
Alert Dialog를 닫을 수 없거나, Alert Dialog를 연 Activity로 돌아왔을 때 Alert Dialog가 열린 상태를 유지해야 하는 경우 [Alert Dialog를 Activity로](#making-an-alert-dialog-activity) 만들어 사용하는 것을 권장합니다.
Activity 간 유려한 트랜지션을 제공하기 위해 하위 AppScreen 요소 중 일부가 상위 AppScreen 요소보다 위에 위치합니다. 이 제약으로 인해, 열린 상태의 Alert Dialog는 독립적인 Activity로 만들지 않는 경우 하위 Activity와 상위 Activity 사이에 위치시키는 것이 불가능합니다.
```tsx title='ActivityAlertDialogStep.tsx'
import { HStack, Portal, VStack } from "@seed-design/react";
import { useActivityZIndexBase } from "@seed-design/stackflow";
import {
useActivityParams,
useFlow,
useStepFlow,
type StaticActivityComponentType,
} from "@stackflow/react/future";
import { useEffect, useState } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import { AppBar, AppBarMain, AppBarIconButton, AppBarRight } from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
import { IconHouseLine } from "@karrotmarket/react-monochrome-icon";
import {
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogRoot,
AlertDialogTitle,
AlertDialogTrigger,
} from "seed-design/ui/alert-dialog";
declare module "@stackflow/config" {
interface Register {
ActivityAlertDialogStep: {
"alert-dialog"?: "open";
};
}
}
const ActivityAlertDialogStep: StaticActivityComponentType<"ActivityAlertDialogStep"> = () => {
const [open, setOpen] = useState(false);
const { push } = useFlow();
const { pushStep, popStep } = useStepFlow("ActivityAlertDialogStep");
const params = useActivityParams<"ActivityAlertDialogStep">();
const isOverlayOpen = params["alert-dialog"] === "open";
useEffect(() => setOpen(isOverlayOpen), [isOverlayOpen]);
const onOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
if (newOpen && !isOverlayOpen) {
pushStep((params) => ({ ...params, "alert-dialog": "open" }));
return;
}
if (!newOpen && isOverlayOpen) {
popStep();
return;
}
};
return (
push("ActivityHome", {})}>
Alert Dialog 열기
Step
Alert Dialog가 Step으로 만들어져 있기 때문에 뒤로 가기로 닫을 수 있습니다.
setOpen(false)} variant="neutralWeak">
닫기
{
// 이 Alert Dialog는 Activity로 만들어지지 않았기 때문에, z-index 정리를 위해
// Alert Dialog를 먼저 닫고 다음 Activity를 push해야 합니다.
setOpen(false);
push("ActivityDetail", {
title: "Alert Dialog에서 이동한 화면",
body: "Alert Dialog를 닫고 이동했습니다.",
});
}}
>
Push
);
};
export default ActivityAlertDialogStep;
```
Usage \[#usage-1]
```tsx
import { useActivityZIndexBase } from "@seed-design/stackflow";
import { Portal } from "@seed-design/react";
import {
useActivity,
useActivityParams,
useFlow,
useStepFlow,
type ActivityComponentType,
} from "@stackflow/react/future";
import { useEffect, useState } from "react";
// ... more imports
declare module "@stackflow/config" {
interface Register {
ActivityHome: {
"alert-dialog"?: "open";
};
}
}
const ActivityHome: ActivityComponentType<"ActivityHome"> = () => {
const [open, setOpen] = useState(false);
const { push } = useFlow();
const { pushStep, popStep } = useStepFlow("ActivityHome");
const params = useActivityParams<"ActivityHome">();
// [!code highlight]
const isOverlayOpen = params["alert-dialog"] === "open";
// [!code highlight]
useEffect(() => {
if (!isOverlayOpen) {
setOpen(false);
}
}, [isOverlayOpen]);
// [!code highlight]
const onOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
if (newOpen && !isOverlayOpen) {
pushStep((params) => ({ ...params, "alert-dialog": "open" }));
return;
}
if (!newOpen && isOverlayOpen) {
popStep();
return;
}
};
return (
Open
{/* [!code highlight] */}
{/* [!code highlight] */}
제목
설명 텍스트
setOpen(false)}>취소
{
// [!code highlight]
setOpen(false); // 다른 Activity로 이동하기 전에는 Alert Dialog를 닫으세요.
push("ActivityNext");
}}
>
다음
{/* [!code highlight] */}
);
};
```
1. `Portal`을 사용하여 Alert Dialog가 DOM 상 현재 Activity 밖에 렌더링되도록 합니다.
2. `open` prop를 관리하고, `onOpenChange` 핸들러를 통해 Step 상태와 동기화합니다.
3. 뒤로 가기 버튼 등을 통해 Activity 파라미터가 변경될 때 Alert Dialog의 `open` 상태를 동기화합니다.
4. `layerIndex={useActivityZIndexBase({ activityOffset: 1 })}`로 현재 Activity보다 한 단계 높은 z-index 기준점을 전달합니다.
- [`useActivityZIndexBase`에 대해 자세히 알아보기](#about-useactivityzindexbase)
`useStepOverlay` \[#usestepoverlay]
\#2와 #3을 일반화하여 `useStepOverlay`를 사용하면 편리합니다. `useStepOverlay` 구현 예시는 [코드](https://github.com/daangn/seed-design/blob/dev/examples/stackflow-spa/src/seed-design/stackflow/use-step-overlay.ts)를 참고하세요.
```tsx
import { useActivityZIndexBase } from "@seed-design/stackflow";
import { Portal } from "@seed-design/react";
import { useStepOverlay } from "./use-step-overlay";
// ... more imports
const MyActivity: ActivityComponentType = () => {
// [!code highlight]
const { overlayProps, setOpen } = useStepOverlay();
const { popStep } = useStepFlow("MyActivity");
const { push } = useFlow();
return (
{/* [!code highlight] */}
Open
{/* [!code highlight] */}
{/* [!code highlight] */}
제목
설명 텍스트
setOpen(false)}>취소
{
// [!code highlight]
setOpen(false); // 다른 Activity로 이동하기 전에는 Alert Dialog를 닫으세요.
push("ActivityNext");
}}
>
다음
{/* [!code highlight] */}
);
};
```
About `useActivityZIndexBase` \[#about-useactivityzindexbase]
`useActivityZIndexBase`는 각 Activity의 z-index 기준점을 반환하는 훅입니다.
- `useActivity().zIndex * 5`를 반환합니다. `useActivity().zIndex`는 현재 Activity가 enter된 Activity 중 몇 번째 Activity인지를 나타내는 값입니다.
- AppScreen을 구성하는 요소들(layer, appbar, dim, edge)은 `useActivityZIndexBase()` 값을 바탕으로 계산된 z-index를 갖습니다.
```
┌─ 2번 AlertDialog Activity
│ └─ AlertDialog: 12 (2 × 5 + 2)
├─ 1번 AppScreen Activity
│ ├─ appbar: 12 (1 × 5 + 7)
│ ├─ edge: 9 (1 × 5 + 4)
│ ├─ layer: 7 (1 × 5 + 2)
│ └─ dim: 5 (1 × 5 + 0)
└─ 0번 AppScreen Activity
├─ appbar: 7 (0 × 5 + 7)
├─ edge: 4 (0 × 5 + 4)
├─ layer: 2 (0 × 5 + 2)
└─ dim: 0 (0 × 5 + 0)
```
- `activityOffset` 옵션은 현재 Activity가 아닌 다음 Activity의 z-index 기준점을 사용하고 싶을 때 사용합니다.
- 예를 들어, `useActivityZIndexBase({ activityOffset: 1 })`는 다음 Activity의 z-index 기준점을 반환합니다.
- 이는 Step 패턴에서 현재 Activity 위에 AlertDialog를 표시할 때 유용합니다.
```
┌─ 1번 AppScreen Activity에서 activityOffset: 1로 띄운 AlertDialog (예: Step 패턴)
│ └─ AlertDialog: 12 ((1 + 1) × 5 + 2)
├─ 1번 AppScreen Activity
│ ├─ appbar: 12 (1 × 5 + 7)
│ ├─ edge: 9 (1 × 5 + 4)
│ ├─ layer: 7 (1 × 5 + 2)
│ └─ dim: 5 (1 × 5 + 0)
└─ 0번 AppScreen Activity
├─ appbar: 7 (0 × 5 + 7)
├─ edge: 4 (0 × 5 + 4)
├─ layer: 2 (0 × 5 + 2)
└─ dim: 0 (0 × 5 + 0)
```
- 두 훅 모두 현재 Activity가 enter된 Activity 중 \**몇 번째 Activity인지를 바탕으로 z-index 스타일링에 필요한 값을 반환하는 훅입니다.*\*
- 두 훅이 반환하는 값은 같으나, [@stackflow/plugin-basic-ui](https://www.npmjs.com/package/@stackflow/plugin-basic-ui)가 아닌, SEED 컴포넌트를 사용하는 경우 SEED가 관리하는 `@seed-design/stackflow` 패키지의 `useActivityZIndexBase` 훅을 사용하는 것을 권장합니다.
- `@stackflow/react-ui-core` 패키지의 `useZIndexBase`는 `activityOffset` 옵션을 제공하지 않습니다.
- `@seed-design/stackflow@1.1.1`부터 `useActivityZIndexBase` 훅이 제공됩니다.
`layerIndex`를 `useActivity().zIndex * 5 + n` 형태로 지정하고 있었다면 `useActivityZIndexBase() + n`으로 변경할 수 있습니다. 변경 과정에서 `+ n`이 필요한지 함께 검토해보세요.
---
file: stackflow/app-screen.mdx
# App Screen
Stackflow 네비게이션에서 개별 화면을 구성하는 컴포넌트입니다. 모바일 앱과 같은 화면 전환 경험을 제공할 때 사용됩니다.
```tsx
import { IconBellFill } from "@karrotmarket/react-monochrome-icon";
import { Flex } from "@seed-design/react";
import type { ActivityComponentType } from "@stackflow/react/future";
import {
AppBar,
AppBarCloseButton,
AppBarIconButton,
AppBarLeft,
AppBarMain,
AppBarRight,
} from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
declare module "@stackflow/config" {
interface Register {
"react/app-screen/preview": {};
}
}
const AppScreenPreviewActivity: ActivityComponentType<"react/app-screen/preview"> = () => {
return (
Preview
Preview
);
};
export default AppScreenPreviewActivity;
```
Installation \[#installation]
- npm: npx @seed-design/cli@latest add ui:app-screen
- pnpm: pnpm dlx @seed-design/cli@latest add ui:app-screen
- yarn: yarn dlx @seed-design/cli@latest add ui:app-screen
- bun: bun x @seed-design/cli@latest add ui:app-screen
Usage \[#usage]
```tsx
import {
AppBar,
AppBarBackButton,
AppBarCloseButton,
AppBarIconButton,
AppBarLeft,
AppBarMain,
AppBarRight,
} from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
```
```tsx
Title
{/* actions */}
{/* content */}
```
Props \[#props]
App Screen \[#app-screen]
`AppScreen` \[#appscreen]
- `preventSwipeBack`
- type: `boolean | undefined`
- `tone`
- type: `"layer" | "transparent" | undefined`
- default: `"layer"`
- `theme`
- type: `"cupertino" | "android" | undefined`
- default: `"cupertino"`
- `transitionStyle`
- type: `"slideFromRightIOS" | "fadeFromBottomAndroid" | "fadeIn" | undefined`
- default: `"slideFromRightIOS"`
- `layerOffsetTop`
- type: `"none" | "safeArea" | "appBar" | undefined`
- default: `"appBar"`
- `layerOffsetBottom`
- type: `"none" | "safeArea" | undefined`
- default: `"none"`
- `gradient`
- type: `boolean | undefined`
- default: `true`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
- `swipeBackDisplacementRatioThreshold`
- type: `number | undefined`
- default: `0.4`
- description: The threshold to determine whether the swipe back is intentional.
- `swipeBackVelocityThreshold`
- type: `number | undefined`
- default: `1`
- description: The threshold to determine whether the swipe back is intentional.
- `onSwipeBackStart`
- type: `(() => void) | undefined`
- `onSwipeBackMove`
- type: `((props: { displacement: number; displacementRatio: number; }) => void) | undefined`
- `onSwipeBackEnd`
- type: `((props: { swiped: boolean; }) => void) | undefined`
`AppScreenContent` \[#appscreencontent]
- `ptr`
- type: `boolean | undefined`
- `onPtrReady`
- type: `(() => void) | undefined`
- `onPtrRefresh`
- type: `(() => Promise) | undefined`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
App Bar \[#app-bar]
`AppBar` \[#appbar]
- `theme`
- type: `"cupertino" | "android" | undefined`
- default: `"cupertino"`
- `transitionStyle`
- type: `"slideFromRightIOS" | "fadeFromBottomAndroid" | "fadeIn" | undefined`
- default: `"slideFromRightIOS"`
- `tone`
- type: `"layer" | "transparent" | undefined`
- default: `"layer"`
- `divider`
- type: `boolean | undefined`
- default: `false`
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
- `bg`
- type: `(string & {}) | ScopedColorBg | ScopedColorPalette | ScopedColorBanner | undefined`
- description: Shorthand for \`background\`.
- `background`
- type: `(string & {}) | ScopedColorBg | ScopedColorPalette | ScopedColorBanner | undefined`
`AppBarLeft` \[#appbarleft]
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`AppBarMain` \[#appbarmain]
- `title`
- type: `string | undefined`
- description: The title of the app bar. If children is provided as ReactElement, this prop will be ignored.
- `subtitle`
- type: `string | undefined`
- description: The subtitle of the app bar. If children is provided as ReactElement, this prop will be ignored.
- `layout`
- type: `"titleOnly" | "withSubtitle" | undefined`
- default: `"titleOnly"`
- `theme`
- type: `"cupertino" | "android" | undefined`
- default: `"cupertino"`
- `transitionStyle`
- type: `"slideFromRightIOS" | "fadeFromBottomAndroid" | "fadeIn" | undefined`
- default: `"slideFromRightIOS"`
- `tone`
- type: `"layer" | "transparent" | undefined`
- default: `"layer"`
`AppBarRight` \[#appbarright]
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
`AppBarIconButton`, `AppBarBackButton`, `AppBarCloseButton` \[#appbariconbutton-appbarbackbutton-appbarclosebutton]
- `asChild`
- type: `boolean | undefined`
- default: `false`
- description: Whether the element should be rendered as a child of a slot.
Examples \[#examples]
Tones \[#tones]
AppScreen의 `tone` 속성을 `transparent`로 설정하여 투명한 배경을 사용할 수 있습니다.
- `AppBar`의 배경이 투명해집니다.
- 모바일 OS 상태바를 포함한 `AppScreen` 상단에 그라디언트가 표시됩니다.
- `gradient` 속성을 `false`로 설정하여 숨길 수 있습니다.
```tsx
import { IconBellFill } from "@karrotmarket/react-monochrome-icon";
import { Flex } from "@seed-design/react";
import type { ActivityComponentType } from "@stackflow/react/future";
import {
AppBar,
AppBarCloseButton,
AppBarIconButton,
AppBarLeft,
AppBarMain,
AppBarRight,
} from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
declare module "@stackflow/config" {
interface Register {
"react/app-screen/transparent-bar": {};
}
}
const AppScreenTransparentBarActivity: ActivityComponentType<
"react/app-screen/transparent-bar"
> = () => {
return (
Preview
Preview
);
};
export default AppScreenTransparentBarActivity;
```
With Intersection Observer \[#with-intersection-observer]
Intersection Observer를 사용해 `AppBar`의 `tone` 속성을 동적으로 변경할 수 있습니다.
```tsx
import { IconBellFill } from "@karrotmarket/react-monochrome-icon";
import { Flex } from "@seed-design/react";
import type { ActivityComponentType } from "@stackflow/react/future";
import { useEffect, useRef, useState } from "react";
import {
AppBar,
AppBarCloseButton,
AppBarIconButton,
AppBarLeft,
AppBarMain,
AppBarProps,
AppBarRight,
} from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
declare module "@stackflow/config" {
interface Register {
"react/app-screen/with-intersection-observer": unknown;
}
}
const AppScreenWithIntersectionObserverActivity: ActivityComponentType<
"react/app-screen/with-intersection-observer"
> = () => {
const [tone, setTone] = useState("transparent");
const whiteImageRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (!entry.isIntersecting) {
// 이미지 영역을 벗어나면 tone을 layer로 변경
setTone("layer");
} else {
// 이미지 영역을 포함하면 tone을 transparent로 변경
setTone("transparent");
}
},
{
threshold: [0, 0.1, 0.5, 1],
rootMargin: "0px",
},
);
if (whiteImageRef.current) {
observer.observe(whiteImageRef.current);
}
return () => {
observer.disconnect();
};
}, []);
return (
Preview
하얀 이미지
컨텐츠 영역
);
};
export default AppScreenWithIntersectionObserverActivity;
```
Layer Offset Top \[#layer-offset-top]
`layerOffsetTop` 속성을 사용해 `AppScreenContent`의 상단 오프셋을 조정할 수 있습니다.
`tone="transparent"`와 `gradient`를 사용하는 경우, 일반적으로 `layerOffsetTop="none"`을 함께 설정하여 모바일 OS 상태바 영역까지 콘텐츠 영역을 확장합니다.
디스플레이 컷아웃 (notch) 등 safe area를 올바르게 처리하기 위해 `viewport-fit=cover`가 포함된 `viewport` 메타 태그를 사용하세요.
```html
```
Customizing App Bar \[#customizing-app-bar]
`tone="layer"`인 경우 `AppBar`의 색상을 변경할 수 있습니다.
`AppBarIconButton` 내에 `Icon` 컴포넌트를 사용하여 아이콘을 커스터마이징할 수 있습니다.
아이콘 컴포넌트에 대해 자세히 알아봅니다.
```tsx
import { Flex, Icon } from "@seed-design/react";
import { IconBellFill } from "@karrotmarket/react-monochrome-icon";
import type { ActivityComponentType } from "@stackflow/react/future";
import { AppBar, AppBarIconButton, AppBarMain, AppBarRight } from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
declare module "@stackflow/config" {
interface Register {
"react/app-screen/app-bar-customization": {};
}
}
const AppScreenAppBarCustomizationActivity: ActivityComponentType<
"react/app-screen/app-bar-customization"
> = () => {
return (
} color="palette.blue500" size="x5" />
Preview
);
};
export default AppScreenAppBarCustomizationActivity;
```
Custom Elements in App Bar \[#custom-elements-in-app-bar]
`AppBar` 내에 `AppBarLeft`, `AppBarMain`, `AppBarRight`에 대응되지 않는 커스텀 요소를 배치하는 경우, 다른 요소와 동일한 화면 전환 트랜지션을 적용하기 위해 `AppBarSlot` 컴포넌트를 사용합니다.
`AppBarSlot`은 하위 요소에 `class`를 주입하여 트랜지션을 적용하며, 추가 마크업은 생성하지 않습니다.
```tsx title='ActivityAppBarSlot.tsx'
import { useActivity, useFlow, type StaticActivityComponentType } from "@stackflow/react/future";
import {
AppBar,
AppBarBackButton,
AppBarLeft,
AppBarRight,
AppBarIconButton,
AppBarSlot,
} from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
import { IconBellLine } from "@karrotmarket/react-monochrome-icon";
import { Flex, Text, VStack } from "@seed-design/react";
import { ActionButton } from "seed-design/ui/action-button";
declare module "@stackflow/config" {
interface Register {
ActivityAppBarSlot: Record;
}
}
function FakeSearchBar(props: React.HTMLAttributes) {
return (
검색어를 입력하세요
);
}
const ActivityAppBarSlot: StaticActivityComponentType<"ActivityAppBarSlot"> = () => {
const { isRoot } = useActivity();
const { push } = useFlow();
return (
{!isRoot && (
)}
AppBar.Slot은 커스텀 요소에 stackflow 트랜지션 애니메이션을 적용합니다. 이 페이지에서
뒤로 swipe하면 검색바가 IconButton과 동일하게 fade 트랜지션됩니다.
push("ActivityAppBarSlot", {})} variant="neutralSolid">
이 액티비티 다시 열기
);
};
export default ActivityAppBarSlot;
```
Preventing Swipe Back \[#preventing-swipe-back]
`preventSwipeBack` 속성을 사용해 `theme="cupertino"`인 AppScreen에서 edge 영역을 렌더링하지 않음으로써 스와이프 백 제스처를 방지할 수 있습니다.
Transition Styles \[#transition-styles]
`transitionStyle` 속성을 사용해 AppScreen이 최상위로 push되거나 최상위에서 pop될 때 재생할 트랜지션을 지정할 수 있습니다.
최상위: `useActivity().isTop === true`인 액티비티로 만들어진 AppScreen을
의미합니다.
별도로 지정하지 않는 경우, `transitionStyle`은 `theme`에 따른 기본값을 갖습니다.
- `theme="cupertino"`: `slideFromRightIOS`
- `theme="android"`: `fadeFromBottomAndroid`
최상위 AppScreen이 push/pop될 때, 최상위가 아닌 AppScreen도 함께 트랜지션을 재생합니다.
**`@seed-design/stackflow@1.1.15`까지**
최상위 AppScreen이 push/pop될 때, 최상위가 아닌 AppScreen은 **각 AppScreen의 고유한 `transitionStyle`을 재생합니다**.
- 예를 들면, `transitionStyle="fadeFromBottomAndroid"`인 0번 AppScreen 위에 `transitionStyle="slideFromLeftIOS"`인 1번 AppScreen이 push되는 경우, 0번 AppScreen은 `fadeFromBottomAndroid` 트랜지션을 재생합니다.
- 0번 AppScreen이 위치 변화 없이 그대로 유지된 상태에서(`fadeFromBottomAndroid`) 1번 AppScreen이 우측에서 슬라이드 인(`slideFromLeftIOS`)
**이후 버전**
최상위 AppScreen이 push/pop될 때, 최상위가 아닌 AppScreen은 **최상위 AppScreen의 `transitionStyle`을 재생합니다**.
- 같은 스택 내에 여러 `transitionStyle`이 공존할 때 자연스러운 트랜지션을 제공합니다.
- 예를 들면, `transitionStyle="fadeFromBottomAndroid"`인 0번 AppScreen 위에 `transitionStyle="slideFromLeftIOS"`인 1번 AppScreen이 push되는 경우, 0번 AppScreen은 `slideFromLeftIOS` 트랜지션을 재생합니다.
- 0번 AppScreen이 자연스럽게 좌측으로 조금 밀려나며 어두워지고(`slideFromLeftIOS`) 1번 AppScreen이 우측에서 슬라이드 인(`slideFromLeftIOS`)
```tsx title='ActivityTransitionStyle.tsx'
import { VStack, Text } from "@seed-design/react";
import { useFlow, type StaticActivityComponentType } from "@stackflow/react/future";
import {
AppBar,
AppBarBackButton,
AppBarIconButton,
AppBarLeft,
AppBarMain,
AppBarRight,
} from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent, type AppScreenProps } from "seed-design/ui/app-screen";
import { IconHouseLine } from "@karrotmarket/react-monochrome-icon";
import { ActionButton } from "seed-design/ui/action-button";
import { appScreenVariantMap } from "@seed-design/css/recipes/app-screen";
import { Snackbar, useSnackbarAdapter } from "seed-design/ui/snackbar";
import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control";
import { useState } from "react";
declare module "@stackflow/config" {
interface Register {
ActivityTransitionStyle: {
transitionStyle: NonNullable;
};
}
}
const ActivityTransitionStyle: StaticActivityComponentType<"ActivityTransitionStyle"> = ({
params: { transitionStyle },
}) => {
const { push } = useFlow();
const { create } = useSnackbarAdapter();
const [preventSwipeBack, setPreventSwipeBack] = useState(false);
return (
{
create({ render: () => , timeout: 500 });
}}
onSwipeBackEnd={({ swiped }) => {
create({ render: () => , timeout: 500 });
}}
>
{/* can be undefined if search parameter isn't provided */}
push("ActivityHome", {})}>
{appScreenVariantMap.transitionStyle.map((style) => (
push("ActivityTransitionStyle", { transitionStyle: style })}
>
{style}
))}
Prevent Swipe Back
setPreventSwipeBack(value === "true")}
aria-label="Prevent Swipe Back"
>
false
true
);
};
export default ActivityTransitionStyle;
```
---
file: stackflow/bottom-sheet.mdx
# Bottom Sheet
Bottom Sheet를 Stackflow와 함께 사용하는 방법을 안내합니다.
Bottom Sheet 컴포넌트에 대해 자세히 알아봅니다.
Making a Bottom Sheet Activity \[#making-a-bottom-sheet-activity]
**일반적인 경우 Bottom Sheet를 [Activity](https://stackflow.so/docs/get-started/activity)로 만들어 사용하는 것을 권장합니다.**
- Activity로 관리되므로, 하위 Activity보다 높고 상위 Activity보다는 낮은 z-index를 갖도록 관리하기 쉽습니다.
- 딥링킹이 가능합니다. (URL 접속으로 Bottom Sheet를 열 수 있습니다.)
- [@stackflow/plugin-basic-ui](https://www.npmjs.com/package/@stackflow/plugin-basic-ui) `BottomSheet`에서의 마이그레이션이 쉽습니다.
```tsx title='ActivityBottomSheetActivity.tsx'
import { VStack } from "@seed-design/react";
import { useFlow, type StaticActivityComponentType } from "@stackflow/react/future";
import {
AppBar,
AppBarBackButton,
AppBarLeft,
AppBarMain,
AppBarIconButton,
AppBarRight,
} from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
import { ActionButton } from "seed-design/ui/action-button";
import { IconHouseLine } from "@karrotmarket/react-monochrome-icon";
declare module "@stackflow/config" {
interface Register {
ActivityBottomSheetActivity: {};
}
}
const ActivityBottomSheetActivity: StaticActivityComponentType<
"ActivityBottomSheetActivity"
> = () => {
const { push } = useFlow();
return (
push("ActivityHome", {})}>
push("ActivityBottomSheet", {})}
>
ActivityBottomSheet을 Push
push("ActivityBottomSheetActivity", {})}
>
지금 열린 이 Activity를 Push
);
};
export default ActivityBottomSheetActivity;
```
```tsx title='ActivityBottomSheet.tsx'
import { Box, Divider, HStack, VStack } from "@seed-design/react";
import { useActivity, useFlow, type StaticActivityComponentType } from "@stackflow/react/future";
import { useRef, useState } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import {
BottomSheetBody,
BottomSheetContent,
BottomSheetFooter,
BottomSheetRoot,
} from "seed-design/ui/bottom-sheet";
import { Checkbox } from "seed-design/ui/checkbox";
import { Snackbar, useSnackbarAdapter } from "seed-design/ui/snackbar";
import { TextField, TextFieldInput } from "seed-design/ui/text-field";
import { useActivityZIndexBase } from "@seed-design/stackflow";
import { Switch } from "seed-design/ui/switch";
import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control";
import { appScreenVariantMap, type AppScreenVariant } from "@seed-design/css/recipes/app-screen";
declare module "@stackflow/config" {
interface Register {
ActivityBottomSheet: {};
}
}
const ActivityBottomSheet: StaticActivityComponentType<"ActivityBottomSheet"> = () => {
const { push, pop } = useFlow();
const activity = useActivity();
const form = useRef(null);
const snackbar = useSnackbarAdapter();
const [nameError, setNameError] = useState(null);
const [keepMounted, setKeepMounted] = useState(false);
const [transitionStyle, setTransitionStyle] =
useState("slideFromRightIOS");
const handleSubmit = () => {
if (!form.current) return;
const formData = new FormData(form.current);
if (!formData.get("name")) {
setNameError("이름을 입력해주세요.");
return;
}
setNameError(null);
pop();
snackbar.create({
render: () => (
),
});
};
const open = keepMounted
? activity.transitionState === "enter-active" || activity.transitionState === "enter-done"
: activity.isActive;
const onOpenChange = keepMounted
? (open: boolean) => !open && activity.isActive && pop()
: (open: boolean) => !open && pop();
return (