앱 셸
제품 화면의 틀입니다. 왼쪽 사이드바는 어디로 갈지, 위 상단 바는 지금 어디인지(브레드크럼)와 어디서나 쓰는 도구를 맡고, 본문만 스크롤합니다.
사이드바는 파인 면 위의 세로 세그먼트입니다. 현재 항목만 솟은 칸(솟은 면 · 컨트롤 경계 · 두께 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)은 앱이 한다 |
| drawerOpen | boolean | — | lg 미만의 드로어 열림. 제어 |
| defaultDrawerOpen | boolean | false | 드로어 열림. 비제어의 처음 값 |
| onDrawerOpenChange | (open: boolean) => void | — | 드로어 안의 링크를 눌러도 false 로 부른다 |
| skipLink | boolean | true | '본문으로 건너뛰기' 링크. 끄는 것은 앱이 따로 둘 때뿐이다 |
| sidebarShortcut | boolean | true | ⌘\ · Ctrl+\ 로 사이드바 접기 |
| mainId | string | "main" | main 의 id. 건너뛰기 링크가 가리킨다. 한 화면에 셸이 둘이면(문서 · 픽스처) 바꾼다 |
| lang | "ko" | "en" | "ja" | — | 내장 문구의 언어. 기본값은 ko 다(app-shell.labels.ts) |
| labels | Partial<AppShellLabels> | — | lang 의 기본 문구 중 바꿀 것만 넘긴다 |
| className | string | — | |
| style | CSSProperties | — | 셸 높이는 기본이 화면 높이(100dvh)다. 상자 안에 그릴 때 height 를 준다 |
Topbar
| prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| start | ReactNode | — | 보통 <Breadcrumb>. 화면 이름은 브레드크럼의 마지막 항목이다(h1 을 두지 않는다) |
| end | ReactNode | — | 검색 트리거 · 알림 · 도움말 |
이 밖에 <header> props(children 제외)를 그대로 받습니다.
Sidebar
| prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| state | "expanded" | "collapsed" | — | 혼자 쓸 때의 접힘. AppShell 안에서는 셸이 정하므로 넘기지 않는다 |
| variant | "inline" | "drawer" | "inline" | drawer: 떠 있는 카드 면 · 항목 44. AppShell 의 드로어 안에서는 셸이 정한다 |
| lang | "ko" | "en" | "ja" | — | 내장 문구의 언어. 기본값은 ko 다(sidebar.labels.ts) |
| labels | Partial<SidebarLabels> | — | lang 의 기본 문구 중 바꿀 것만 넘긴다. nav 는 랜드마크 이름이다 |
이 밖에 <nav> props를 그대로 받습니다.
SidebarItem
| prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| href | string | — | 없고 asChild 도 아니면 <button> 으로 그린다 |
| asChild | boolean | false | 라우터 Link 를 자식으로 받는다. href 만 주면 LinkProvider 의 Link(없으면 <a>)로 그린다 |
| icon | ReactNode | — | 16 아이콘. 다이렉트 메시지는 아바타 20 |
| children필수 | ReactNode | — | 이름. 넘치면 말줄임하고 title 로 전체를 보인다 |
| current | boolean | false | 지금 화면. aria-current="page" 와 솟은 칸 + 600 |
| unread | boolean | false | 안 읽은 것이 있다. 600 · 진한 글자 |
| muted | boolean | false | 알림을 껐다. 흐린 글자 + 종 끔 표시. 안 읽음이어도 굵게 하지 않는다 |
| private | boolean | false | 비공개. icon 이 없으면 아이콘 자리에 자물쇠를 둔다 |
| disabled | boolean | false | 권한이 없다. 포커스는 받고(이유를 들을 수 있게) 링크는 지운다 |
| disabledReason | string | — | 비활성 이유. aria-describedby 와 title 로 같은 문장을 단다 |
| loading | boolean | false | 이름을 불러오는 중. 아이콘 · 이름 자리에 뼈대를 그린다 |
| count | number | — | 조용한 개수 칩(헤어라인 + muted) |
| mentions | number | — | 멘션 · 새 알림. solid 칩(primary). 0 이면 그리지 않는다. 99 를 넘으면 99+ |
| end | ReactNode | — | 끝 자리에 더할 것 |
| keep | boolean | — | 접힌 섹션에서도 남길지. 기본은 current · unread · mentions 가 있을 때 |
| lang | "ko" | "en" | "ja" | — | 내장 문구의 언어. 기본값은 ko 다(sidebar.labels.ts) |
| labels | Partial<SidebarLabels> | — | lang 의 기본 문구 중 바꿀 것만 넘긴다 |
이 밖에 <a> props(children 제외)를 그대로 받습니다.
SidebarSection
| prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| title필수 | ReactNode | — | 머리 글자(12/600 muted). 영어에서만 대문자 · 넓은 자간 |
| open | boolean | — | 펼침. 제어 |
| defaultOpen | boolean | true | 펼침. 비제어의 처음 값 |
| onOpenChange | (open: boolean) => void | — | |
| onAdd | () => void | — | 있으면 머리 끝에 + 버튼(24)을 둔다. 손을 올리거나 포커스가 머리 안에 있을 때 보이고 터치에서는 늘 보인다 |
| addLabel | string | — | + 버튼의 이름(예: "채널 추가"). 제품 문구라 내장하지 않는다. onAdd 가 있으면 넘긴다 |
| empty | ReactNode | — | 항목이 없을 때 보일 것(예: "채널이 없습니다." + 만들기 링크) |
| children | ReactNode | — | SidebarItem 들 |
이 밖에 <div> props(title · children 제외)를 그대로 받습니다.
SidebarUser
| prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| avatar필수 | ReactNode | — | 원 아바타 28 |
| name필수 | string | — | |
| sub | string | — | 역할 · 이메일 같은 둘째 줄 |
이 밖에 <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 을 가져야 한다 |
| max | number | 99 | 이보다 크면 99+ 처럼 줄인다 |
이 밖에 <span> props(children 제외)를 그대로 받습니다.
WorkspaceSwitcher
| prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| workspaces필수 | Workspace[] | — | |
| current필수 | string | null | — | 지금 워크스페이스 id. 전환은 라우팅이라 앱이 상태를 갖는다(제어 전용) |
| onSelect | (id: string) => void | — | href 가 없는 워크스페이스를 골랐을 때 |
| onCreate | () => void | — | 있으면 맨 아래에 "워크스페이스 만들기" |
| createHref | string | — | 만들기를 링크로 둘 때 |
| createDisabledReason | string | — | 만들 권한이 없을 때의 이유. 항목을 숨기지 않고 비활성(포커스는 받는다) + 이유를 단다 |
| children | ReactNode | — | 구분선 아래 '이 워크스페이스의 것'(멤버 초대 · 설정). ./menu 의 DropdownMenuItem 들 |
| open | boolean | — | 메뉴 열림. 제어 |
| defaultOpen | boolean | false | 메뉴 열림. 비제어의 처음 값 |
| onOpenChange | (open: boolean) => void | — | |
| modal | boolean | true | 열린 동안 바깥을 가리고(aria-hidden) 누름을 막는다. 문서 · 픽스처처럼 메뉴 밖도 읽혀야 할 때만 끈다 |
| compact | boolean | false | 아바타만 보인다(이름은 접근성 이름으로 남는다). AppShell 의 접힌 사이드바에서는 넘기지 않아도 된다 |
| lang | "ko" | "en" | "ja" | — | 내장 문구의 언어. 기본값은 ko 다(workspace-switcher.labels.ts) |
| labels | Partial<WorkspaceSwitcherLabels> | — | lang 의 기본 문구 중 바꿀 것만 넘긴다 |
| className | string | — |
WorkspaceAvatar
| prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| id필수 | string | — | 색을 고르는 seed. 이름이 아니라 id 를 준다(이름은 바뀐다) |
| name필수 | string | — | 첫 글자를 이니셜로 쓴다 |
| image | string | — | 로고. 있으면 이니셜 대신 그린다 |
| size | "md" | "sm" | "xs" | "md" | md 24(스위처 트리거) · sm 20(메뉴 항목) · xs 16(브레드크럼) |
이 밖에 <span> props(children 제외)를 그대로 받습니다.