사용자에게 중요한 정보나 팁을 시각적으로 강조하여 전달하는 메시지 컴포넌트입니다.
import "./styles";
import IconChevronRightLine from "@karrotmarket/lynx-monochrome-icon/IconChevronRightLine";
import IconXmarkLine from "@karrotmarket/lynx-monochrome-icon/IconXmarkLine";
import { root } from "@lynx-js/react";
import { Callout, SuffixIcon, useSeedClassName } from "@seed-design/lynx-react";
function Root() {
const seedClassName = useSeedClassName({ colorMode: "system" });
function handleTap() {
"background only";
}
return (
<page className={seedClassName}>
<view className="callout-preview">
<Callout.Root>
<Callout.Content>
<Callout.Description>새로운 소식을 확인해 보세요.</Callout.Description>
</Callout.Content>
</Callout.Root>
<Callout.Root bindtap={handleTap} accessibility-label="새로운 소식 확인">
<Callout.Content>
<Callout.Title>알림</Callout.Title>
<Callout.Description>새로운 소식을 확인해 보세요.</Callout.Description>
</Callout.Content>
<SuffixIcon icon={<IconChevronRightLine />} />
</Callout.Root>
<Callout.Root>
<Callout.Content>
<Callout.Description>닫을 수 있는 안내 메시지예요.</Callout.Description>
</Callout.Content>
<Callout.CloseButton accessibility-label="닫기">
<SuffixIcon icon={<IconXmarkLine />} />
</Callout.CloseButton>
</Callout.Root>
</view>
</page>
);
}
root.render(<Root />);
npx @seed-design/cli@latest add ui:callout
pnpm dlx @seed-design/cli@latest add ui:callout
yarn dlx @seed-design/cli@latest add ui:callout
bun x @seed-design/cli@latest add ui:callout
의존성 설치
npm install @karrotmarket/lynx-monochrome-icon @seed-design/lynx-react
yarn add @karrotmarket/lynx-monochrome-icon @seed-design/lynx-react
pnpm add @karrotmarket/lynx-monochrome-icon @seed-design/lynx-react
bun add @karrotmarket/lynx-monochrome-icon @seed-design/lynx-react
아래 코드를 복사 후 붙여넣고 사용하세요
/**
* @file ui:callout
* @requires @seed-design/lynx-react@>=0.5.0 <1.0.0
* @requires @seed-design/lynx-css@>=0.9.0 <1.0.0
**/
import IconChevronRightLine from "@karrotmarket/lynx-monochrome-icon/IconChevronRightLine";
import IconXmarkLine from "@karrotmarket/lynx-monochrome-icon/IconXmarkLine";
import * as React from "@lynx-js/react";
import {
Callout as SeedCallout,
PrefixIcon,
SuffixIcon,
type PrefixIconProps,
} from "@seed-design/lynx-react";
export interface CalloutProps
extends Omit<SeedCallout.RootProps, "children" | "open" | "defaultOpen" | "onDismiss"> {
prefixIcon?: PrefixIconProps["icon"];
title?: React.ReactNode;
description: React.ReactNode;
linkProps?: SeedCallout.LinkProps;
}
/**
* @see https://seed-design.io/lynx/components/callout
*/
export const Callout = React.forwardRef<unknown, CalloutProps>(
({ prefixIcon, title, description, linkProps, ...otherProps }, ref) => {
return (
<SeedCallout.Root ref={ref} {...otherProps}>
{prefixIcon ? <PrefixIcon icon={prefixIcon} /> : null}
<SeedCallout.Content>
{title ? <SeedCallout.Title>{title}</SeedCallout.Title> : null}
<SeedCallout.Description>{description}</SeedCallout.Description>
{linkProps ? <SeedCallout.Link {...linkProps} /> : null}
</SeedCallout.Content>
</SeedCallout.Root>
);
},
);
Callout.displayName = "Callout";
export interface ActionableCalloutProps
extends Omit<SeedCallout.RootProps, "children" | "open" | "defaultOpen" | "onDismiss"> {
prefixIcon?: PrefixIconProps["icon"];
title?: React.ReactNode;
description: React.ReactNode;
}
/**
* @see https://seed-design.io/lynx/components/callout
*/
export const ActionableCallout = React.forwardRef<unknown, ActionableCalloutProps>(
({ prefixIcon, title, description, ...otherProps }, ref) => {
return (
<SeedCallout.Root ref={ref} {...otherProps}>
{prefixIcon ? <PrefixIcon icon={prefixIcon} /> : null}
<SeedCallout.Content>
{title ? <SeedCallout.Title>{title}</SeedCallout.Title> : null}
<SeedCallout.Description>{description}</SeedCallout.Description>
</SeedCallout.Content>
<SuffixIcon icon={<IconChevronRightLine />} />
</SeedCallout.Root>
);
},
);
ActionableCallout.displayName = "ActionableCallout";
export interface DismissibleCalloutProps extends Omit<SeedCallout.RootProps, "children"> {
prefixIcon?: PrefixIconProps["icon"];
title?: React.ReactNode;
description: React.ReactNode;
linkProps?: SeedCallout.LinkProps;
dismissLabel?: string;
}
/**
* @see https://seed-design.io/lynx/components/callout
*/
export const DismissibleCallout = React.forwardRef<unknown, DismissibleCalloutProps>(
({ prefixIcon, title, description, linkProps, dismissLabel = "닫기", ...otherProps }, ref) => {
return (
<SeedCallout.Root ref={ref} {...otherProps}>
{prefixIcon ? <PrefixIcon icon={prefixIcon} /> : null}
<SeedCallout.Content>
{title ? <SeedCallout.Title>{title}</SeedCallout.Title> : null}
<SeedCallout.Description>{description}</SeedCallout.Description>
{linkProps ? <SeedCallout.Link {...linkProps} /> : null}
</SeedCallout.Content>
<SeedCallout.CloseButton accessibility-label={dismissLabel}>
<SuffixIcon icon={<IconXmarkLine />} />
</SeedCallout.CloseButton>
</SeedCallout.Root>
);
},
);
DismissibleCallout.displayName = "DismissibleCallout";
/**
* This file is a snippet from SEED Design, helping you get started quickly with @seed-design/* packages.
* You can extend this snippet however you want.
*/
prefixIcon?React.ReactElement<LynxIconElementProps, string | React.JSXElementConstructor<any>> | undefined
title?React.ReactNode
descriptionReact.ReactNode
linkProps?SeedCallout.LinkProps | undefined
bindtap?EventHandler<BaseTouchEvent<Target>> | undefined
main-thread:bindtap?EventHandler<BaseTouchEvent<Element>> | undefined
style?CSSProperties | undefined
className?string | undefined
prefixIcon?React.ReactElement<LynxIconElementProps, string | React.JSXElementConstructor<any>> | undefined
title?React.ReactNode
descriptionReact.ReactNode
bindtap?EventHandler<BaseTouchEvent<Target>> | undefined
main-thread:bindtap?EventHandler<BaseTouchEvent<Element>> | undefined
style?CSSProperties | undefined
className?string | undefined
prefixIcon?React.ReactElement<LynxIconElementProps, string | React.JSXElementConstructor<any>> | undefined
title?React.ReactNode
descriptionReact.ReactNode
linkProps?SeedCallout.LinkProps | undefined
dismissLabel?string | undefined
open?boolean | undefined
defaultOpen?boolean | undefined
onDismiss?(() => void) | undefined
bindtap?EventHandler<BaseTouchEvent<Target>> | undefined
main-thread:bindtap?EventHandler<BaseTouchEvent<Element>> | undefined
style?CSSProperties | undefined
className?string | undefined
설치한 snippet은 기본 Callout, 전체 영역을 탭할 수 있는 ActionableCallout, 닫을 수 있는 DismissibleCallout을 export합니다.
import { Callout } from "@/components/ui/callout";
export function App() {
return <Callout title="알림" description="새로운 소식을 확인해 보세요." />;
}
저수준 조합이 필요하면 @seed-design/lynx-react의 compound API를 직접 사용할 수 있습니다.
import { Callout } from "@seed-design/lynx-react";
export function App() {
return (
<Callout.Root tone="informative">
<Callout.Content>
<Callout.Title>알림</Callout.Title>
<Callout.Description>새로운 소식을 확인해 보세요.</Callout.Description>
</Callout.Content>
</Callout.Root>
);
}
메시지의 의미를 보조하는 아이콘은 prefixIcon에 전달합니다. Lynx monochrome icon을 사용하면 PrefixIcon wrapper가 tone에 맞는 색상과 크기를 자동으로 적용합니다.
import "./styles";
import IconBellFill from "@karrotmarket/lynx-monochrome-icon/IconBellFill";
import { root } from "@lynx-js/react";
import { useSeedClassName } from "@seed-design/lynx-react";
import { Callout } from "@/components/ui/callout";
function Root() {
const seedClassName = useSeedClassName({ colorMode: "system" });
return (
<page className={seedClassName}>
<view className="callout-preview">
<Callout
tone="informative"
prefixIcon={<IconBellFill />}
title="알림"
description="새로운 소식을 확인해 보세요."
/>
</view>
</page>
);
}
root.render(<Root />);
저수준 compound API에서는 Callout.Root의 첫 번째 child로 PrefixIcon을 배치하세요.
import IconBellFill from "@karrotmarket/lynx-monochrome-icon/IconBellFill";
import { Callout, PrefixIcon } from "@seed-design/lynx-react";
<Callout.Root tone="informative">
<PrefixIcon icon={<IconBellFill />} />
<Callout.Content>
<Callout.Title>알림</Callout.Title>
<Callout.Description>새로운 소식을 확인해 보세요.</Callout.Description>
</Callout.Content>
</Callout.Root>;
tone은 neutral, informative, positive, warning, critical, magic을 지원합니다. 기본값은 neutral입니다.
import "./styles";
import { root } from "@lynx-js/react";
import { Callout, useSeedClassName } from "@seed-design/lynx-react";
const tones = ["neutral", "informative", "positive", "warning", "critical", "magic"] as const;
function Root() {
const seedClassName = useSeedClassName({ colorMode: "system" });
return (
<page className={seedClassName}>
<view className="callout-preview">
{tones.map((tone) => (
<Callout.Root key={tone} tone={tone}>
<Callout.Content>
<Callout.Title>{tone}</Callout.Title>
<Callout.Description>Callout의 {tone} tone입니다.</Callout.Description>
</Callout.Content>
</Callout.Root>
))}
</view>
</page>
);
}
root.render(<Root />);
전체 Callout을 액션으로 사용할 때는 bindtap을 전달합니다. 내부에 별도의 Link를 함께 두지 마세요.
import "./styles";
import IconChevronRightLine from "@karrotmarket/lynx-monochrome-icon/IconChevronRightLine";
import { root, useState } from "@lynx-js/react";
import { Callout, SuffixIcon, useSeedClassName } from "@seed-design/lynx-react";
function Root() {
const seedClassName = useSeedClassName({ colorMode: "system" });
const [tapCount, setTapCount] = useState(0);
function handleTap() {
"background only";
setTapCount((count) => count + 1);
}
return (
<page className={seedClassName}>
<view className="callout-preview">
<text className="callout-preview__status">탭 횟수: {JSON.stringify(tapCount)}</text>
<Callout.Root tone="informative" bindtap={handleTap} accessibility-label="업데이트 확인">
<Callout.Content>
<Callout.Title>업데이트</Callout.Title>
<Callout.Description>새로운 기능을 확인해 보세요.</Callout.Description>
</Callout.Content>
<SuffixIcon icon={<IconChevronRightLine />} />
</Callout.Root>
</view>
</page>
);
}
root.render(<Root />);
DismissibleCallout과 저수준 Callout.Root는 open, defaultOpen, onDismiss를 지원합니다. 닫기 버튼에는 현지화된 dismissLabel 또는 accessibility-label을 제공하세요.
import "./styles";
import IconXmarkLine from "@karrotmarket/lynx-monochrome-icon/IconXmarkLine";
import { root, useState } from "@lynx-js/react";
import { ActionButton, Callout, SuffixIcon, useSeedClassName } from "@seed-design/lynx-react";
function Root() {
const seedClassName = useSeedClassName({ colorMode: "system" });
const [open, setOpen] = useState(true);
function handleDismiss() {
"background only";
setOpen(false);
}
function handleOpen() {
"background only";
setOpen(true);
}
return (
<page className={seedClassName}>
<view className="callout-preview">
<text className="callout-preview__status">열림: {JSON.stringify(open)}</text>
<Callout.Root open={open} onDismiss={handleDismiss} tone="positive">
<Callout.Content>
<Callout.Title>완료</Callout.Title>
<Callout.Description>설정이 저장되었어요.</Callout.Description>
</Callout.Content>
<Callout.CloseButton accessibility-label="닫기">
<SuffixIcon icon={<IconXmarkLine />} />
</Callout.CloseButton>
</Callout.Root>
{!open ? <ActionButton bindtap={handleOpen}>다시 열기</ActionButton> : null}
</view>
</page>
);
}
root.render(<Root />);
| 영역 | React Web | Lynx |
|---|
| Native element | div, span, button | view, text |
| Event | onClick | bindtap, main-thread:bindtap |
| Accessibility | HTML semantics, ARIA | accessibility-* |
| Polymorphism | asChild 지원 | 미지원 |
| Link underline | text-decoration | Lynx preset 제약으로 border 기반 표현 |
Lynx에서는 actionable Root가 터치되는 동안 pressed recipe variant를 적용합니다. Callout.Link는 기본적으로 accessibility-traits="link", actionable Root와 CloseButton은 button trait를 사용합니다.
asChild: Lynx에는 DOM Slot 기반 다형 렌더링이 없습니다.
- 웹 focus ring: Lynx는 웹 키보드 포커스 모델을 사용하지 않습니다.
- HTML attribute와 ARIA prop: 대응하는 Lynx native prop과
accessibility-*를 사용하세요.