AppShell
먼저 보고, 그다음 계약을 읽습니다
페이지 전체 레이아웃 chrome — 슬롯 골격과 Design/Gate 공용 workspace 표면.
Component intent의도와 경계 읽기
variant 로 그리드 골격을 고른다: docs = Sidebar | Content | TOC, dashboard = Sidebar | Content, landing = Content. `.axe-workspace-shell`을 함께 쓰면 Design과 Gate가 공유하는 280px rail · 68rem bounded canvas · 30px navigation row 계약이 활성화된다. 본질은 CSS-only — Rust(maud)/Python(jinja) 도 같은 markup으로 같은 chrome을 그릴 수 있고, React wrapper는 슬롯 합성과 semantic role만 얹는 thin 층이다. 1024–1439px에서는 우측 context를 topbar disclosure로 옮기고, ≤1023px에서는 navigation과 context 양쪽을 native disclosure로 노출한다.
예제
SSOT에 등록된 실제 API 기준 snippet입니다. standalone 배지만 독립 실행 단위이며, fragment는 주변 state·handler 문맥을 생략합니다.
import { AppShell, Sidebar, Toc, TopNav, Footer } from "@axe/ui";<AppShell variant="docs" topbar={<TopNav /* ... */ />} sidebar={ <Sidebar items={navItems} value={cur} onValueChange={setCur} /> } toc={ <Toc activeId={activeHeading} items={[ { id: "intro", label: "소개" }, { id: "api", label: "API", level: 3 }, ]} /> } footer={<Footer />}> <article className="axe-prose">…</article></AppShell>import { AppShell, Sidebar } from "@axe/ui";<AppShell variant="dashboard" sidebar={<Sidebar items={navItems} value={cur} onValueChange={setCur} />} subnavLabel="탐색"> <section>…대시보드 본문…</section></AppShell><div class="axe-app-shell" data-shell="docs"> <a href="#main" class="axe-skip-link">본문으로 건너뛰기</a> <div class="axe-app-shell__topbar"> <!-- topbar --> <details class="axe-app-shell__subnav"> <summary class="axe-app-shell__subnav-summary"> <span class="axe-app-shell__subnav-label">메뉴</span> </summary> <div class="axe-app-shell__subnav-panel"> <nav class="axe-sidebar" aria-label="Primary"><!-- 네비 복제 --></nav> </div> </details> </div> <div class="axe-app-shell__main"> <aside class="axe-app-shell__sidebar" aria-label="Primary">…</aside> <main id="main" class="axe-app-shell__content">…</main> <aside class="axe-app-shell__toc" aria-label="On this page">…</aside> </div> <div class="axe-app-shell__footer"><!-- footer --></div></div>Props
TSX 소스가 진실입니다. 주요 export 컴포넌트의 public API만 노출합니다.
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
variant | "docs" | "dashboard" | "landing" | — | "docs" | 그리드 골격 선택. data-shell 속성으로 CSS 가 분기. |
topbar | React.ReactNode | — | — | 상단 바 슬롯(보통 <TopNav>). |
sidebar | React.ReactNode | — | — | 좌측 네비 슬롯(보통 <Sidebar>). landing 에서는 무시. |
toc | React.ReactNode | — | — | 우측 목차 슬롯(보통 <Toc>). variant="docs" 에서만 표시. |
footer | React.ReactNode | — | — | 하단 슬롯(보통 <Footer>). |
contentClassName | string | — | — | 본문 <main> 에 추가 className. |
contentAs | "main" | "div" | — | "main" | 본문 태그. 다른 문서 안에 specimen으로 중첩할 때만 div. |
contentId | string | — | "main" | 본문 id와 skip-link target. |
showSkipLink | boolean | — | true | 스킵 링크 렌더 여부. 중첩 specimen에서는 false. |
sidebarLabel | string | — | "Primary" | sidebar <aside> 의 aria-label. |
tocLabel | string | — | "On this page" | toc <aside> 의 aria-label. |
subnav | React.ReactNode | false | — | — | ≤1023px disclosure 내용. 기본 = sidebar 재사용. 명시 DOM id가 있는 sidebar는 고유 id의 별도 노드를 넘기거나 false로 끈다. |
subnavLabel | React.ReactNode | — | "메뉴" | ≤1023px disclosure summary 라벨. |
children | React.ReactNode | — | — | 본문 — <main id="main"> 안에 렌더. |
...rest | React.HTMLAttributes<HTMLDivElement> | — | — | 나머지는 루트 <div> 로 전파. |
Toc.items | TocItem[] | 필수 | — | { id, label, level?: 2 | 3 } 배열. level=2 top / 3 sub(들여쓰기), 기본 2. |
Toc.title | React.ReactNode | — | "On this page" | 목차 상단 라벨. string 이면 <nav> aria-label 로도 사용. |
Toc.activeId | string | — | — | 현재 활성 heading id(외부 스크롤 spy 가 전달). 해당 링크에 aria-current="location". |
.axe-* 클래스 계약
React 밖에서도 같은 표면을 그리는 공개 계약입니다. stable은 minor 버전 안에서 이름을 바꾸지 않습니다.
| 클래스 | 안정성 | 용도 |
|---|---|---|
.axe-app-shell | stable | 루트 <div>. data-shell="docs|dashboard|landing" 로 그리드 분기. |
.axe-skip-link | stable | 본문(#main)으로 건너뛰는 스킵 링크(포커스 시 노출). |
.axe-app-shell__topbar | stable | 상단 바 영역(topbar + ≤1023px subnav 포함). |
.axe-app-shell__main | stable | sidebar|content|toc 그리드 행. |
.axe-app-shell__sidebar | stable | 좌측 네비 슬롯 <aside>(sticky/scroll). |
.axe-app-shell__content | stable | 본문 <main id="main">. variant 별 max-width. |
.axe-app-shell__toc | stable | 우측 목차 슬롯 <aside>(docs 전용). |
.axe-app-shell__footer | stable | 하단 footer 슬롯. |
.axe-app-shell__toc-title | stable | Toc 상단 라벨. |
.axe-app-shell__toc-list | stable | Toc 링크 <ul>. |
.axe-app-shell__toc-link | stable | Toc 앵커 링크. 활성 시 aria-current="location". |
.axe-app-shell__toc-link--h3 | stable | h3(level 3) Toc 링크 — 들여쓰기 변형. |
.axe-app-shell__subnav | stable | ≤1023px 보조 네비 <details> disclosure(≥1024px display:none). |
.axe-app-shell__subnav-summary | stable | disclosure <summary> 토글. |
.axe-app-shell__subnav-label | stable | disclosure summary 라벨 span. |
.axe-app-shell__subnav-panel | stable | disclosure 펼침 패널(네비 복제 렌더). |
.axe-app-shell__subnav-toc | stable | subnav 패널 안 목차 래퍼(docs + toc 있을 때). |
.axe-app-shell__subnav-chevron | internal | subnav summary chevron(장식, [open] 회전은 CSS). |
.axe-workspace-shell | stable | Design/Gate 공용 neutral workspace root. AppShell docs variant와 함께 사용. |
.axe-workspace-bar | stable | 56px workspace top bar. |
.axe-workspace-bar__left | stable | top bar 좌측 rail track. |
.axe-workspace-bar__center | stable | bounded center route track. |
.axe-workspace-bar__right | stable | 검색과 context action track. |
.axe-workspace-bar__routes | stable | desktop 중앙 route navigation. |
.axe-workspace-bar__mobile-brand | stable | rail이 접힐 때 표시하는 mobile product identity. |
.axe-workspace-bar__icon-button | stable | 44px menu/context icon trigger. |
.axe-workspace-disclosure | stable | native details 기반 navigation/context sheet root. |
.axe-workspace-disclosure--navigation | stable | ≤1023px 좌측 navigation disclosure. |
.axe-workspace-disclosure--context | stable | <1440px 우측 context disclosure. |
.axe-workspace-disclosure__panel | stable | viewport 고정 disclosure panel. |
.axe-workspace-rail | stable | full-height primary rail flex root. |
.axe-workspace-rail__brand | stable | 56px entity/search header. |
.axe-workspace-rail__body | stable | rail 내부의 유일한 세로 scroll 영역. |
.axe-workspace-rail__identity | stable | 48px 고정 identity footer. |
.axe-workspace-identity | stable | pill 없이 rail 폭을 채우는 고정 identity row. |
.axe-workspace-identity__avatar | stable | 20px brand-tone identity avatar. |
.axe-workspace-identity__label | stable | 말줄임 처리되는 identity label. |
.axe-workspace-identity-menu | stable | rail 하단에서 위로 열리는 native identity menu. |
.axe-workspace-identity-menu__panel | stable | identity action을 담는 고정 폭 popover panel. |
.axe-workspace-rail__nav | stable | 30px row와 1px seam을 소유하는 navigation. |
.axe-workspace-rail__group-label | stable | 대메뉴 또는 비상호작용 group label. |
.axe-workspace-rail__link | stable | hover/active/focus가 정렬된 navigation link. |
.axe-workspace-entity | stable | rail entity anchor 또는 native details switcher. |
.axe-workspace-search | stable | text 또는 data-icon-only 검색 trigger. |
.axe-workspace-search-dialog | stable | static CommandPalette markup을 native dialog로 호스트하는 workspace 검색 표면. |
.axe-workspace-context | stable | full-height 우측 context rail. |
.axe-workspace-context__header | stable | 현재 업무의 제목과 요약. |
.axe-workspace-context__eyebrow | stable | context overline. |
.axe-workspace-context__title | stable | context title. |
.axe-workspace-context__summary | stable | context 설명. |
.axe-workspace-context__tabs | stable | 균등 분할 context tab row. |
.axe-workspace-context__tab | stable | 현재 tab 하단 indicator trigger/link. |
.axe-workspace-context__body | stable | context panel content. |
.axe-workspace-context__section-title | stable | context section caption. |
.axe-workspace-context__link | stable | 32px context location link. |
.axe-workspace-context__footer | stable | context rail 고정 하단 metadata. |
접근성
키보드, ARIA, 구현 노트를 함께 검토합니다.
- Tab 첫 스텝에서 .axe-skip-link 가 나타나 Enter 로 #main 본문 점프
- ≤1023px subnav: <summary> 에서 Enter/Space 로 펼침/접힘(네이티브 <details>)
- subnav 펼침 중 Esc — 닫고 summary 로 포커스 복원
- workspace disclosure는 summary에서 Enter/Space로 열고, host JS가 Esc·외부 클릭·링크 이동 시 닫은 뒤 trigger로 포커스를 복원
본문은 <main id="main">, sidebar/toc 는 각각 <aside aria-label>. ≤1023px 보조 네비는 네이티브 <details>/<summary> — <summary> 가 expanded 시맨틱을 스스로 고지하므로 aria-expanded 를 수동 부여하지 않는다(이중 고지 방지). 기본 subnav는 sidebar/toc ReactNode를 데스크톱 컬럼과 모바일 패널에 두 번 렌더하지만 미디어쿼리가 항상 한쪽만 표시해 숨긴 쪽은 AX 트리에서 제외된다. 단 소비자가 직접 지정한 DOM id·htmlFor·aria-labelledby는 복제될 수 있으므로 별도 고유 id 노드 또는 subnav={false}가 필요하다.
무JS(jinja/maud) 환경에서도 subnav와 workspace disclosure는 네이티브 <details> 토글로 동작한다. Host JS가 있으면 패널 링크·외부 pointerdown·Esc 닫기와 포커스 복원을 얹는다. 검색은 정적 .axe-cmdk markup만으로 모달 동작이 완성되지 않으므로 native dialog 또는 동등한 focus 관리 runtime이 필요하다.