화면 상단에서 현재 화면의 제목과 탐색 액션을 보여주는 내비게이션 바 컴포넌트입니다.
npx @seed-design/cli add ui:app-bar pnpm dlx @seed-design/cli add ui:app-bar yarn dlx @seed-design/cli add ui:app-bar bun x @seed-design/cli add ui:app-bar
의존성 설치
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:app-bar
* @requires @seed-design/lynx-react@>=0.1.0 <1.0.0
* @requires @seed-design/lynx-css@>=0.1.0 <1.0.0
**/
import IconChevronLeftLine from "@karrotmarket/lynx-monochrome-icon/IconChevronLeftLine" ;
import IconXmarkLine from "@karrotmarket/lynx-monochrome-icon/IconXmarkLine" ;
import { forwardRef, type ReactNode } from "@lynx-js/react" ;
import { AppBar as SeedAppBar } from "@seed-design/lynx-react" ;
export interface AppBarProps extends SeedAppBar . RootProps {}
/**
* @see https://seed-design.io/lynx/components/app-bar
*/
export const AppBar = SeedAppBar.Root;
export interface AppBarLeftProps extends SeedAppBar . LeftProps {}
export const AppBarLeft = SeedAppBar.Left;
export interface AppBarRightProps extends SeedAppBar . RightProps {}
export const AppBarRight = SeedAppBar.Right;
export interface AppBarSlotProps extends SeedAppBar . SlotProps {}
export const AppBarSlot = SeedAppBar.Slot;
export interface AppBarMainProps extends SeedAppBar . MainProps {
/**
* The title of the app bar.
* If custom children are provided, this prop will be ignored.
*/
title ?: ReactNode ;
/**
* The subtitle of the app bar.
* If custom children are provided, this prop will be ignored.
*/
subtitle ?: ReactNode ;
}
function shouldRenderCustomMain ( children : ReactNode ) {
return (
children != null &&
typeof children !== "string" &&
typeof children !== "number" &&
typeof children !== "boolean"
);
}
export const AppBarMain = forwardRef < unknown , AppBarMainProps >(
({ title , subtitle , children , ... otherProps }, ref ) => {
if ( shouldRenderCustomMain (children)) {
return (
< SeedAppBar.Main ref = {ref} { ... otherProps}>
{children}
</ SeedAppBar.Main >
);
}
return (
< SeedAppBar.Main ref = {ref} layout = {subtitle ? "withSubtitle" : "titleOnly" } { ... otherProps}>
< SeedAppBar.Title >{children ?? title}</ SeedAppBar.Title >
{subtitle ? < SeedAppBar.Subtitle >{subtitle}</ SeedAppBar.Subtitle > : null }
</ SeedAppBar.Main >
);
},
);
AppBarMain.displayName = "AppBarMain" ;
export interface AppBarIconButtonProps extends SeedAppBar . IconButtonProps {}
export const AppBarIconButton = SeedAppBar.IconButton;
export interface AppBarBackButtonProps extends Omit < AppBarIconButtonProps , "icon" > {
icon ?: AppBarIconButtonProps [ "icon" ];
}
export const AppBarBackButton = forwardRef < unknown , AppBarBackButtonProps >(
(
{
icon = < IconChevronLeftLine />,
"accessibility-label" : accessibilityLabel = "뒤로" ,
... otherProps
},
ref,
) => {
return (
< AppBarIconButton
ref = {ref}
accessibility-label = {accessibilityLabel}
icon = {icon}
{ ... otherProps}
/>
);
},
);
AppBarBackButton.displayName = "AppBarBackButton" ;
export interface AppBarCloseButtonProps extends Omit < AppBarIconButtonProps , "icon" > {
icon ?: AppBarIconButtonProps [ "icon" ];
}
export const AppBarCloseButton = forwardRef < unknown , AppBarCloseButtonProps >(
(
{ icon = < IconXmarkLine />, "accessibility-label" : accessibilityLabel = "닫기" , ... otherProps },
ref,
) => {
return (
< AppBarIconButton
ref = {ref}
accessibility-label = {accessibilityLabel}
icon = {icon}
{ ... otherProps}
/>
);
},
);
AppBarCloseButton.displayName = "AppBarCloseButton" ;
/**
* 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.
*/
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
bindlayoutchange?EventHandler < LayoutChangeDetailEvent < Target >> | undefined
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
layout?"titleOnly" | "withSubtitle" | undefined
theme?"cupertino" | "android" | undefined
transitionStyle?"slideFromRightIOS" | "fadeFromBottomAndroid" | "fadeIn" | undefined
tone?"layer" | "transparent" | undefined
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
bindlayoutchange?EventHandler < LayoutChangeDetailEvent < Target >> | undefined
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
icon?ReactElement < LynxIconElementProps, string | JSXElementConstructor < any >> | undefined
accessibility-label?string | undefined
accessibility-element?boolean | undefined
accessibility-traits?"text" | "image" | "button" | "link" | "header" | "search" | "selected" | "playable" | "keyboard" | "summary" | "disabled" | "updating" | "adjustable" | "tabbar" | "none" | undefined
children?ReactNode
className?string | undefined
bindtap?EventHandler < BaseTouchEvent < Target >> | undefined
main-thread:bindtap?EventHandler < BaseTouchEvent < Element >> | undefined
icon?ReactElement < LynxIconElementProps, string | JSXElementConstructor < any >> | undefined
className?string | undefined
bindtap?EventHandler < BaseTouchEvent < Target >> | undefined
main-thread:bindtap?EventHandler < BaseTouchEvent < Element >> | undefined
accessibility-label?string | undefined
accessibility-element?boolean | undefined
accessibility-traits?"text" | "image" | "button" | "link" | "header" | "search" | "selected" | "playable" | "keyboard" | "summary" | "disabled" | "updating" | "adjustable" | "tabbar" | "none" | undefined
children?ReactNode
icon?ReactElement < LynxIconElementProps, string | JSXElementConstructor < any >> | undefined
className?string | undefined
bindtap?EventHandler < BaseTouchEvent < Target >> | undefined
main-thread:bindtap?EventHandler < BaseTouchEvent < Element >> | undefined
accessibility-label?string | undefined
accessibility-element?boolean | undefined
accessibility-traits?"text" | "image" | "button" | "link" | "header" | "search" | "selected" | "playable" | "keyboard" | "summary" | "disabled" | "updating" | "adjustable" | "tabbar" | "none" | undefined
children?ReactNode
설치한 snippet은 Root와 주요 슬롯을 AppBar, AppBarLeft, AppBarMain, AppBarRight로 export 합니다. AppBarBackButton, AppBarCloseButton은 기본 아이콘만 제공하며, 실제 이동 로직은 bindtap으로 직접 전달합니다.
import {
AppBar,
AppBarBackButton,
AppBarLeft,
AppBarMain,
AppBarRight,
} from "@/components/ui/app-bar" ;
export function Header ({ goBack } : { goBack : () => void }) {
return (
< AppBar >
< AppBarLeft >
< AppBarBackButton bindtap = {goBack} />
</ AppBarLeft >
< AppBarMain title = "동네생활" />
< AppBarRight />
</ AppBar >
);
}
AppBarMain에 title, subtitle을 전달하면 내부에서 AppBar.Title, AppBar.Subtitle을 조립합니다. subtitle이 있으면 layout="withSubtitle"가 자동으로 적용됩니다.
import { AppBar, AppBarMain } from "@/components/ui/app-bar" ;
export function Header () {
return (
< AppBar >
< AppBarMain title = "관심 목록" subtitle = "3개의 새 소식" />
</ AppBar >
);
}
직접 슬롯을 조립하고 싶다면 AppBarMain의 children으로 compound 컴포넌트를 전달합니다.
import { AppBar, AppBarMain } from "@/components/ui/app-bar" ;
import { AppBar as SeedAppBar } from "@seed-design/lynx-react" ;
export function Header () {
return (
< AppBar >
< AppBarMain layout = "withSubtitle" >
< SeedAppBar.Title >제목</ SeedAppBar.Title >
< SeedAppBar.Subtitle >부제목</ SeedAppBar.Subtitle >
</ AppBarMain >
</ AppBar >
);
}
왼쪽과 오른쪽에는 icon button 또는 custom slot을 배치할 수 있습니다. Cupertino theme에서는 좌우 슬롯 폭을 읽어 가운데 title이 치우치지 않도록 padding을 보정합니다.
import {
AppBar,
AppBarBackButton,
AppBarCloseButton,
AppBarLeft,
AppBarMain,
AppBarRight,
AppBarSlot,
} from "@/components/ui/app-bar" ;
export function Header ({ goBack , close } : { goBack : () => void ; close : () => void }) {
return (
< AppBar >
< AppBarLeft >
< AppBarBackButton bindtap = {goBack} />
</ AppBarLeft >
< AppBarMain title = "작성하기" />
< AppBarRight >
< AppBarSlot >
< text >완료</ text >
</ AppBarSlot >
< AppBarCloseButton bindtap = {close} />
</ AppBarRight >
</ AppBar >
);
}
Lynx에서는 HTML ARIA 속성 대신 Lynx native 접근성 속성을 사용합니다. AppBarIconButton은 기본적으로 accessibility-element={true}와 accessibility-traits="button"을 적용하며, 아이콘만으로 의미를 알 수 없는 버튼에는 accessibility-label을 전달해야 합니다.
import IconBellLine from "@karrotmarket/lynx-monochrome-icon/IconBellLine" ;
< AppBarIconButton accessibility-label = "알림" icon = {< IconBellLine />} />
AppBarBackButton, AppBarCloseButton은 각각 "뒤로", "닫기" 라벨을 기본값으로 제공합니다.
theme은 "cupertino" 또는 "android"를 사용할 수 있습니다. 명시하지 않으면 AppBar 내부에서 SystemInfo.platform을 읽어 Android에서는 "android", 그 외 플랫폼이나 값이 없을 때는 "cupertino"를 사용합니다.
< AppBar >
< AppBarMain title = "자동 theme" />
</ AppBar >
수동으로 지정한 theme은 platform 기본값보다 우선합니다.
< AppBar theme = "android" >
< AppBarMain title = "Android layout" />
</ AppBar >
Lynx AppBar는 React AppBar와 같은 Rootage 기반 variant를 사용하지만, Stackflow와 화면 전환 로직은 포함하지 않습니다.
이벤트 핸들링 : onClick 대신 Lynx 이벤트인 bindtap을 전달합니다.
렌더링 요소 : HTML 요소 대신 네이티브 <view> / <text> 요소를 렌더링합니다.
접근성 라벨링 : aria-label 대신 Lynx native accessibility-label을 사용합니다.
platform theme : 웹처럼 data-* selector에 의존하지 않고, recipe variant className으로 "cupertino" / "android" 디자인을 적용합니다.
navigation 책임 : AppBarBackButton과 AppBarCloseButton은 이동을 자동 실행하지 않습니다. Stackflow pop() 같은 동작은 호출부에서 연결합니다.
현재 Lynx AppBar는 순수 UI 컴포넌트로 제공되며 다음 기능은 포함하지 않습니다.
기능 웹 대응 대체 방법 Stackflow activity 연동 useActivity, useActions 기반 back/close 처리bindtap에서 앱의 navigation 함수를 직접 호출swipe-back 전환 애니메이션 Stackflow screen transition 화면 전환을 소유한 navigation 계층에서 처리 data-* selector 기반 상태 스타일웹 recipe selector theme, tone variant className 사용HTML button 속성 type, DOM click eventLynx <view> 기반 AppBarIconButton과 bindtap 사용 HTML ARIA 라벨 aria-labelLynx native accessibility-label 사용