AxeShell
먼저 보고, 그다음 계약을 읽습니다
서비스 문서를 감싸는 런타임 워크스페이스 셸 — 레일·전환기는 런타임이, 세션·프롬프트는 위젯 iframe 이 채운다.
Component intent의도와 경계 읽기
AxeShell 은 정적 골격(axe-shell-skeleton.html) + <head> 동기 스텁(axe-shell-stub.js, ≤1KB) + defer 런타임(axe-shell.js) 세 조각이다. 서비스가 하는 일은 골격 두 반쪽 사이에 본문을 넣고 매니페스트 carrier(`<script type="application/json" id="axe-shell-manifest">`) 1개를 싣는 것뿐이다. 레일 메뉴·서비스/법인/계정 전환기·접기·⌘K 는 런타임이 기존 .axe-workspace-* 어휘로 그리고, 우측 세션 목록과 하단 프롬프트는 https://axelabs.ai/shell/* 위젯 iframe 이 채운다. 이 문서의 클래스는 그 두 위젯 호스트·프레임·퇴행 링크·스크림 6개뿐이다. ≥1440px 에서 패널은 3열의 마지막 트랙, 1024–1439px 에서는 우측 시트, ≤1023px 에서는 전체 폭 시트다. iframe 은 한 번 장착되면 옮기지 않는다(옮기면 재로드) — 폭 전환·시트·확장은 모두 CSS 기하다. 계정 메뉴의 세션 행은 매니페스트 viewer.auth 가 정한다: loginHref(게스트) = '로그인' 한 행, 로그인 뒤 = logoutHref '로그아웃' · switchHref '다른 계정으로 로그인'(생략 시 플랫폼 OP 계정 선택). 게스트에게는 결속할 신원(subHash)이 없어 위젯을 장착하지 않는다. React 소비자는 AxeShell 컴포넌트가 같은 골격을 렌더하고 boot/refresh/destroy 만 부른다. 정본 = src/lib/shell/SPEC.md.
예제
SSOT에 등록된 실제 API 기준 snippet입니다. standalone 배지만 독립 실행 단위이며, fragment는 주변 state·handler 문맥을 생략합니다.
import { AxeShell } from "@axe/ui";<AxeShell manifest={manifest}> <h1>결재함</h1></AxeShell><head> <script src="{kit}/shell/axe-shell-stub.js"></script> <script type="application/json" id="axe-shell-manifest" data-axe-shell-entity>{…}</script> <script src="{kit}/shell/axe-shell.js" defer></script> <link rel="stylesheet" href="{kit}/axe-ui.css"></head><body> <!-- {kit}/shell/axe-shell-skeleton.before.html --> <h1>결재함</h1> <!-- {kit}/shell/axe-shell-skeleton.after.html --></body><main id="main" class="axe-app-shell__content">…</main><section id="axe-shell-panel" class="axe-shell-panel" aria-label="세션" data-axe-shell-slot="panel"></section><!-- __main 밖, 셸 루트 직계 --><section class="axe-shell-prompt" aria-label="프롬프트" data-axe-shell-slot="prompt"></section><div class="axe-shell-scrim" data-axe-shell-slot="scrim" hidden></div>Props
TSX 소스가 진실입니다. 주요 export 컴포넌트의 public API만 노출합니다.
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
manifest | AxeShellManifest | null | 필수 | — | 서버가 만든 AxeShellManifest@1. 래퍼가 serializeManifestForScript 로 carrier <script> 에 직렬화한다. null = 아직 모름(오리진을 클라이언트에서만 아는 정적 렌더) — 골격만 렌더하고 부팅을 미룬다. |
children | React.ReactNode | 필수 | — | 서비스 본문. 골격의 <main id="main"> 안(콘텐츠 표식 자리)에 들어간다. |
widgetBase | string | — | SHELL_WIDGET_ORIGIN | 위젯 문서 URL 접두어(오리진 + 선택 경로 접두어). 수신 검증·targetOrigin 은 그 오리진만 쓴다. design 데모·테스트만 바꾼다. DOM 속성에서는 읽지 않는다. |
widgets | boolean | — | true | false = 레일만. 위젯 호스트를 숨기고 본문 하단 예약을 푼다(루트 data-axe-shell-widgets="off"). SSR 부터 그 상태로 렌더해 부팅 때 3열·프롬프트 예약이 걷히는 시프트가 없다. 게스트 매니페스트(viewer.subHash 없음)도 같다. |
deferBoot | boolean | — | false | true = carrier·골격은 렌더하되 런타임 부팅을 미룬다. 뷰어를 클라이언트에서야 확정하는 정적 렌더 소비자(design)가 게스트로 한 번, 로그인으로 또 한 번 부팅하지 않도록 세션 확정 뒤 false 로 내린다. |
onNavigate | (url: URL) => void | — | — | 위젯의 navigate 를 허용목록 재판정 뒤 받는다. 기본 = location.assign. SPA 소비자가 클라이언트 라우팅에 쓴다. |
className | string | — | — | 셸 루트에 덧붙일 클래스(골격 클래스는 유지). 소비자 편집 표면의 스코프용. |
contentClassName | string | — | — | <main id="main"> 에 덧붙일 클래스. |
.axe-* 클래스 계약
React 밖에서도 같은 표면을 그리는 공개 계약입니다. stable은 minor 버전 안에서 이름을 바꾸지 않습니다.
| 클래스 | 안정성 | 용도 |
|---|---|---|
.axe-shell-panel | stable | 골격의 우측 세션 위젯 호스트 <section>. ≥1440 은 3열 sticky, <1440 은 루트 data-axe-shell-panel="open" 일 때만 보이는 우측 시트. |
.axe-shell-panel-toggle | stable | 바 우측의 시트 토글 버튼(.axe-workspace-bar__icon-button 과 함께). ≥1440 에서는 숨는다. |
.axe-shell-prompt | stable | 골격의 하단 프롬프트 위젯 호스트 <section>. 가운데 열 경계에 맞춘 고정 바, 루트 data-axe-shell-prompt="expanded" 에서 위로 확장. |
.axe-shell-frame | internal | 런타임이 호스트 안에 만드는 위젯 <iframe>. 호스트를 테두리 없이 채운다. |
.axe-shell-fallback | internal | 위젯이 10초 안에 ready 를 보내지 않을 때 런타임이 호스트에 남기는 워크스페이스 링크. |
.axe-shell-scrim | internal | 시트(<1440 패널)·모바일 프롬프트 확장 뒤 배경. 표시는 CSS 가 상태 속성으로 정하고, 클릭은 런타임이 collapse 로 받는다. |
접근성
키보드, ARIA, 구현 노트를 함께 검토합니다.
- ⌘K / Ctrl+K — 프롬프트 위젯에 이동 모드로 포커스
- ⌘J / Ctrl+J — 프롬프트 위젯에 질문 모드로 포커스(AI 가 꺼진 서비스는 이동 모드)
- Esc(포커스가 부모 문서에 있을 때) — 패널 시트 → 프롬프트 확장 → 레일 서랍 순으로 닫고 연 트리거로 포커스 복원
두 호스트는 aria-label 이 붙은 <section>("세션"·"프롬프트"), 위젯 <iframe> 은 title 을 가진다. 시트 토글은 aria-controls="axe-shell-panel" + aria-expanded 로 상태를 알린다. 스킵 링크는 #main 으로 간다.
닫힌 시트는 visibility:hidden 이라 포커스·접근성 트리에서 빠지지만 iframe 은 재로드되지 않는다. 모션 없음(고빈도 UI). forced-colors 에서는 호스트 경계가 CanvasText 1px 로 선다.