본문으로 건너뛰기

앱 셸

제품 화면의 틀입니다. 왼쪽 사이드바는 어디로 갈지, 위 상단 바는 지금 어디인지(브레드크럼)와 어디서나 쓰는 도구를 맡고, 본문만 스크롤합니다.

사이드바는 파인 면 위의 세로 세그먼트입니다. 현재 항목만 솟은 칸(솟은 면 · 컨트롤 경계 · 두께 1,1 · 600)이고 파랑 글자를 쓰지 않습니다. 멘션 · 새 알림만 solid 개수 칩(primary)이고 나머지 개수는 조용한 칩입니다. 빨강을 쓰지 않습니다.

펼침(240) · 접힘(56) · 좁은 화면(lg 미만, 드로어)은 CSS 미디어 쿼리가 바꿔서 서버 렌더와 첫 화면이 같습니다. 접으면 섹션을 숨기고 항목 이름은 툴팁으로 보이며, 멘션 칩은 아이콘 모서리로 옮깁니다. 섹션의 멘션은 앱이 '안 읽음' 항목의 mentions 로 모읍니다.

쓰는 경우
워크스페이스 · 채널 · 프로젝트 · 문서 · 회의 · 설정처럼 사이드바와 상단 바가 있는 제품 화면 전부.
쓰지 않는 경우
로그인 · 결제창 같은 단독 흐름, 문서 인쇄 · 덱 · 랜딩 표면.
금지
본문 맨 위에 h1 으로 화면 이름을 다시 적지 않습니다. 현재 항목을 파랑 글자나 왼쪽 세로 강조선으로 표시하지 않습니다. 모든 개수를 빨간 칩으로 두지 않습니다. 사이드바 · 상단 바에 반투명 · 블러를 쓰지 않습니다.
// 상태 · 단축키 · 드로어 · 메뉴(radix). "use client"
import { AppShell, Topbar, SidebarSection, WorkspaceSwitcher } from "@uyoolabs/ui/nav";
// 정적 부품. 서버 컴포넌트에서 그린다
import { Sidebar, SidebarHeader, SidebarBody, SidebarFooter, SidebarList, SidebarItem,
  SidebarUser, Count, WorkspaceAvatar } from "@uyoolabs/ui/ui";

AppShell

상자 안에 그린 예시입니다. 실제로는 화면 높이(100dvh)를 씁니다.

WorkspaceSwitcher

이름이 곧 트리거입니다. 목록은 역할을 오른쪽에 보이고, 이 워크스페이스의 것은 구분선 아래, 만들기는 맨 아래입니다.

키보드

키동작
Tab처음 Tab 은 '본문으로 건너뛰기' 링크다. 이어서 사이드바 → 상단 바 → 본문 순서
F6사이드바 · 상단 바 · 본문 영역을 돈다(Shift+F6 은 거꾸로). 본문은 main 자체로 간다
⌘\사이드바 접기 · 펴기(데스크톱, Windows 는 Ctrl+\). IME 조합 중에는 무시한다
EnterSpace메뉴 버튼(lg 미만): 드로어를 열고 닫기 버튼에 선다. 섹션 머리: 접고 편다
Esc드로어를 닫고 메뉴 버튼으로 돌아간다. 가림막을 누르거나 항목을 골라도 닫힌다
EnterSpace↓워크스페이스 전환: 메뉴를 열고 첫 항목에 선다(↑ 는 마지막 항목)
↑↓HomeEnd메뉴 항목 이동. 글자를 치면 그 글자로 시작하는 워크스페이스로 간다

props

소스의 타입과 JSDoc 에서 생성한 표입니다. 컴포넌트를 고치면 pnpm gen 으로 다시 만듭니다.

AppShell

prop타입기본값설명
sidebar필수ReactNode—<Sidebar>(./ui). 펼침 · 접힘 · 드로어 세 모양을 셸이 CSS 로 바꿔 낀다
topbar필수ReactNode—<Topbar>. 접기 · 메뉴 버튼은 셸이 앞에 붙인다
children필수ReactNode—본문. <main tabIndex={-1}> 안에 그린다
sidebarState"expanded" | "collapsed"—데스크톱(lg 1024 이상)의 접힘. 제어
defaultSidebarState"expanded" | "collapsed""expanded"데스크톱의 접힘. 비제어의 처음 값
onSidebarStateChange(state: SidebarState) => void—버튼 · 단축키 둘 다에서 부른다. 저장(쿠키 · localStorage)은 앱이 한다
drawerOpenboolean—lg 미만의 드로어 열림. 제어
defaultDrawerOpenbooleanfalse드로어 열림. 비제어의 처음 값
onDrawerOpenChange(open: boolean) => void—드로어 안의 링크를 눌러도 false 로 부른다
skipLinkbooleantrue'본문으로 건너뛰기' 링크. 끄는 것은 앱이 따로 둘 때뿐이다
sidebarShortcutbooleantrue⌘\ · Ctrl+\ 로 사이드바 접기
mainIdstring"main"main 의 id. 건너뛰기 링크가 가리킨다. 한 화면에 셸이 둘이면(문서 · 픽스처) 바꾼다
lang"ko" | "en" | "ja"—내장 문구의 언어. 기본값은 ko 다(app-shell.labels.ts)
labelsPartial<AppShellLabels>—lang 의 기본 문구 중 바꿀 것만 넘긴다
classNamestring—
styleCSSProperties—셸 높이는 기본이 화면 높이(100dvh)다. 상자 안에 그릴 때 height 를 준다

Topbar

prop타입기본값설명
startReactNode—보통 <Breadcrumb>. 화면 이름은 브레드크럼의 마지막 항목이다(h1 을 두지 않는다)
endReactNode—검색 트리거 · 알림 · 도움말

이 밖에 <header> props(children 제외)를 그대로 받습니다.

Sidebar

prop타입기본값설명
state"expanded" | "collapsed"—혼자 쓸 때의 접힘. AppShell 안에서는 셸이 정하므로 넘기지 않는다
variant"inline" | "drawer""inline"drawer: 떠 있는 카드 면 · 항목 44. AppShell 의 드로어 안에서는 셸이 정한다
lang"ko" | "en" | "ja"—내장 문구의 언어. 기본값은 ko 다(sidebar.labels.ts)
labelsPartial<SidebarLabels>—lang 의 기본 문구 중 바꿀 것만 넘긴다. nav 는 랜드마크 이름이다

이 밖에 <nav> props를 그대로 받습니다.

SidebarItem

prop타입기본값설명
hrefstring—없고 asChild 도 아니면 <button> 으로 그린다
asChildbooleanfalse라우터 Link 를 자식으로 받는다. href 만 주면 LinkProvider 의 Link(없으면 <a>)로 그린다
iconReactNode—16 아이콘. 다이렉트 메시지는 아바타 20
children필수ReactNode—이름. 넘치면 말줄임하고 title 로 전체를 보인다
currentbooleanfalse지금 화면. aria-current="page" 와 솟은 칸 + 600
unreadbooleanfalse안 읽은 것이 있다. 600 · 진한 글자
mutedbooleanfalse알림을 껐다. 흐린 글자 + 종 끔 표시. 안 읽음이어도 굵게 하지 않는다
privatebooleanfalse비공개. icon 이 없으면 아이콘 자리에 자물쇠를 둔다
disabledbooleanfalse권한이 없다. 포커스는 받고(이유를 들을 수 있게) 링크는 지운다
disabledReasonstring—비활성 이유. aria-describedby 와 title 로 같은 문장을 단다
loadingbooleanfalse이름을 불러오는 중. 아이콘 · 이름 자리에 뼈대를 그린다
countnumber—조용한 개수 칩(헤어라인 + muted)
mentionsnumber—멘션 · 새 알림. solid 칩(primary). 0 이면 그리지 않는다. 99 를 넘으면 99+
endReactNode—끝 자리에 더할 것
keepboolean—접힌 섹션에서도 남길지. 기본은 current · unread · mentions 가 있을 때
lang"ko" | "en" | "ja"—내장 문구의 언어. 기본값은 ko 다(sidebar.labels.ts)
labelsPartial<SidebarLabels>—lang 의 기본 문구 중 바꿀 것만 넘긴다

이 밖에 <a> props(children 제외)를 그대로 받습니다.

SidebarSection

prop타입기본값설명
title필수ReactNode—머리 글자(12/600 muted). 영어에서만 대문자 · 넓은 자간
openboolean—펼침. 제어
defaultOpenbooleantrue펼침. 비제어의 처음 값
onOpenChange(open: boolean) => void—
onAdd() => void—있으면 머리 끝에 + 버튼(24)을 둔다. 손을 올리거나 포커스가 머리 안에 있을 때 보이고 터치에서는 늘 보인다
addLabelstring—+ 버튼의 이름(예: "채널 추가"). 제품 문구라 내장하지 않는다. onAdd 가 있으면 넘긴다
emptyReactNode—항목이 없을 때 보일 것(예: "채널이 없습니다." + 만들기 링크)
childrenReactNode—SidebarItem 들

이 밖에 <div> props(title · children 제외)를 그대로 받습니다.

SidebarUser

prop타입기본값설명
avatar필수ReactNode—원 아바타 28
name필수string—
substring—역할 · 이메일 같은 둘째 줄

이 밖에 <button> props를 그대로 받습니다. 기본값을 바꾼 것은 type="button" 입니다.

Count

prop타입기본값설명
value필수number—개수. 0 이하이면 그리지 않는다
variant"quiet" | "solid""quiet"quiet: 정보성 개수(헤어라인 + muted). solid: 멘션 · 새 알림(primary 면). 빨강을 쓰지 않는다. 멘션은 오류가 아니다(W1 결정 #16)
placement"inline" | "corner""inline"corner: 아이콘 버튼 · 접힌 항목의 오른쪽 위에 겹친다. 부모가 position 을 가져야 한다
maxnumber99이보다 크면 99+ 처럼 줄인다

이 밖에 <span> props(children 제외)를 그대로 받습니다.

WorkspaceSwitcher

prop타입기본값설명
workspaces필수Workspace[]—
current필수string | null—지금 워크스페이스 id. 전환은 라우팅이라 앱이 상태를 갖는다(제어 전용)
onSelect(id: string) => void—href 가 없는 워크스페이스를 골랐을 때
onCreate() => void—있으면 맨 아래에 "워크스페이스 만들기"
createHrefstring—만들기를 링크로 둘 때
createDisabledReasonstring—만들 권한이 없을 때의 이유. 항목을 숨기지 않고 비활성(포커스는 받는다) + 이유를 단다
childrenReactNode—구분선 아래 '이 워크스페이스의 것'(멤버 초대 · 설정). ./menu 의 DropdownMenuItem 들
openboolean—메뉴 열림. 제어
defaultOpenbooleanfalse메뉴 열림. 비제어의 처음 값
onOpenChange(open: boolean) => void—
modalbooleantrue열린 동안 바깥을 가리고(aria-hidden) 누름을 막는다. 문서 · 픽스처처럼 메뉴 밖도 읽혀야 할 때만 끈다
compactbooleanfalse아바타만 보인다(이름은 접근성 이름으로 남는다). AppShell 의 접힌 사이드바에서는 넘기지 않아도 된다
lang"ko" | "en" | "ja"—내장 문구의 언어. 기본값은 ko 다(workspace-switcher.labels.ts)
labelsPartial<WorkspaceSwitcherLabels>—lang 의 기본 문구 중 바꿀 것만 넘긴다
classNamestring—

WorkspaceAvatar

prop타입기본값설명
id필수string—색을 고르는 seed. 이름이 아니라 id 를 준다(이름은 바뀐다)
name필수string—첫 글자를 이니셜로 쓴다
imagestring—로고. 있으면 이니셜 대신 그린다
size"md" | "sm" | "xs""md"md 24(스위처 트리거) · sm 20(메뉴 항목) · xs 16(브레드크럼)

이 밖에 <span> props(children 제외)를 그대로 받습니다.