{
  "name": "@axe/ui",
  "version": "v0.22.0",
  "origin": "https://design.axelabs.ai",
  "schema": "ComponentDoc (src/lib/docs-schema.ts)",
  "delivery": {
    "react": {
      "root": "@axe/ui",
      "server": "@axe/ui/server",
      "format": "ESM JS + TypeScript declarations",
      "status": "private-unpublished"
    },
    "serverRendered": {
      "command": "axe-ui-export --config axe-ui.consumer.json",
      "check": "axe-ui-export --config axe-ui.consumer.json --check",
      "lock": "axe-ui.lock.json (@axe/ui/consumer-lock@1)"
    }
  },
  "count": 75,
  "groups": [
    {
      "key": "layout",
      "label": "레이아웃"
    },
    {
      "key": "navigation",
      "label": "내비게이션"
    },
    {
      "key": "action",
      "label": "액션"
    },
    {
      "key": "input",
      "label": "입력"
    },
    {
      "key": "container",
      "label": "컨테이너"
    },
    {
      "key": "content",
      "label": "콘텐츠"
    },
    {
      "key": "overlay",
      "label": "오버레이"
    },
    {
      "key": "feedback",
      "label": "피드백"
    },
    {
      "key": "data",
      "label": "데이터"
    },
    {
      "key": "chat",
      "label": "채팅 · AI"
    },
    {
      "key": "utility",
      "label": "유틸리티"
    }
  ],
  "components": [
    {
      "name": "Accordion",
      "slug": "accordion",
      "group": "container",
      "summary": "collapsible disclosure 스택(single/multiple).",
      "description": "Radix Accordion 위에 .axe-accordion__* 스타일을 얹은 접이식 항목 스택이다. type=\"single\"(한 번에 하나) 과 type=\"multiple\"(동시 여러 개) 을 모두 지원하며 FAQ, 설정 그룹, 긴 폼의 섹션 접기 등에 쓴다. 각 트리거는 heading 안 버튼으로 렌더되고 chevron 이 자동으로 붙는다. 부드러운 height 접기는 Radix 가 노출하는 --radix-accordion-content-height 변수를 @keyframes 로 잡아 처리한다(하나의 영역만 접는 경량 패턴은 Collapsible).",
      "keywords": [
        "accordion",
        "아코디언",
        "disclosure",
        "faq",
        "접기",
        "collapsible",
        "single",
        "multiple",
        "섹션",
        "radix"
      ],
      "props": [
        {
          "name": "Accordion.type",
          "type": "\"single\" | \"multiple\"",
          "required": true,
          "description": "single = 한 번에 하나만 열림, multiple = 동시 여러 개."
        },
        {
          "name": "Accordion.collapsible",
          "type": "boolean",
          "description": "type=\"single\" 에서 열린 항목을 다시 눌러 전부 닫기 허용."
        },
        {
          "name": "Accordion.defaultValue",
          "type": "string | string[]",
          "description": "uncontrolled 초기 열림 value(single=string, multiple=string[])."
        },
        {
          "name": "Accordion.value",
          "type": "string | string[]",
          "description": "controlled 열림 value."
        },
        {
          "name": "Accordion.onValueChange",
          "type": "(value: string | string[]) => void",
          "description": "열림 value 변경 콜백."
        },
        {
          "name": "Accordion.…rest",
          "type": "React.ComponentPropsWithoutRef<Radix Accordion.Root>",
          "description": "Accordion 은 Radix Root 를 그대로 재노출(axe 클래스 없음) — className 등은 Root 로 직접 전파."
        },
        {
          "name": "AccordionItem.value",
          "type": "string",
          "required": true,
          "description": "항목 식별 value(열림 상태 매핑 키)."
        },
        {
          "name": "AccordionItem.…rest",
          "type": "React.ComponentPropsWithoutRef<Radix Accordion.Item>",
          "description": "className 은 axe-accordion__item 과 병합, 나머지는 Radix Item 으로 전파."
        },
        {
          "name": "AccordionTrigger.…rest",
          "type": "React.ComponentPropsWithoutRef<Radix Accordion.Trigger>",
          "description": "children=제목/질문. .axe-accordion__header 안에 렌더되고 chevron 자동. 나머지는 Radix Trigger 로 전파."
        },
        {
          "name": "AccordionContent.…rest",
          "type": "React.ComponentPropsWithoutRef<Radix Accordion.Content>",
          "description": "children 은 .axe-accordion__content-inner 로 감싸 렌더. 나머지는 Radix Content 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-accordion__item",
          "purpose": "각 항목 래퍼 — 하단 border-subtle(마지막 항목 제외).",
          "stability": "stable"
        },
        {
          "className": "axe-accordion__header",
          "purpose": "트리거를 감싸는 heading 래퍼(margin 0, flex).",
          "stability": "stable"
        },
        {
          "className": "axe-accordion__trigger",
          "purpose": "트리거 버튼(전폭, label+chevron, hover/focus 트림).",
          "stability": "stable"
        },
        {
          "className": "axe-accordion__trigger-label",
          "purpose": "트리거 라벨 span(flex:1).",
          "stability": "stable"
        },
        {
          "className": "axe-accordion__chevron",
          "purpose": "chevron SVG — [data-state=open] 시 90deg 회전.",
          "stability": "stable"
        },
        {
          "className": "axe-accordion__content",
          "purpose": "접히는 영역(overflow hidden). data-state open/closed 에 --radix-accordion-content-height 애니메이션.",
          "stability": "stable"
        },
        {
          "className": "axe-accordion__content-inner",
          "purpose": "content 내부 padding 래퍼.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "↑/↓ — 트리거 간 이동(roving tabindex)",
          "Home/End — 첫/마지막 트리거로",
          "Enter/Space — 항목 열기/접기"
        ],
        "aria": "각 트리거는 heading(.axe-accordion__header) 안 버튼으로, aria-expanded + aria-controls 를 가지며 컨텐츠는 연결된 region 이다. 화살표 네비/포커스 관리는 Radix 가 제공. chevron 은 aria-hidden 장식.",
        "notes": "smooth height 는 --radix-accordion-content-height 를 @keyframes from/to 로 잡아 처리하고 prefers-reduced-motion 시 즉시 토글한다. Accordion 루트는 axe 클래스 없이 Radix Root 그대로(레이아웃 중립) — 컨테이너 스타일이 필요하면 className 을 직접 부여한다."
      },
      "examples": [
        {
          "title": "single (FAQ)",
          "lang": "tsx",
          "code": "import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from \"@axe/ui\";\n\n<Accordion type=\"single\" collapsible defaultValue=\"faq-1\">\n  <AccordionItem value=\"faq-1\">\n    <AccordionTrigger>배포는 어떻게 하나요?</AccordionTrigger>\n    <AccordionContent>axe ship &lt;svc&gt; 로 합니다.</AccordionContent>\n  </AccordionItem>\n  <AccordionItem value=\"faq-2\">\n    <AccordionTrigger>롤백은?</AccordionTrigger>\n    <AccordionContent>blue/green 전환으로…</AccordionContent>\n  </AccordionItem>\n</Accordion>"
        },
        {
          "title": "multiple",
          "lang": "tsx",
          "code": "import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from \"@axe/ui\";\n\n<Accordion type=\"multiple\" defaultValue={[\"a\", \"c\"]}>\n  <AccordionItem value=\"a\"><AccordionTrigger>A</AccordionTrigger><AccordionContent>…</AccordionContent></AccordionItem>\n  <AccordionItem value=\"b\"><AccordionTrigger>B</AccordionTrigger><AccordionContent>…</AccordionContent></AccordionItem>\n  <AccordionItem value=\"c\"><AccordionTrigger>C</AccordionTrigger><AccordionContent>…</AccordionContent></AccordionItem>\n</Accordion>"
        },
        {
          "title": "CSS-only (정적 open 항목)",
          "lang": "html",
          "code": "<div class=\"axe-accordion__item\">\n  <h3 class=\"axe-accordion__header\">\n    <button class=\"axe-accordion__trigger\" data-state=\"open\" aria-expanded=\"true\">\n      <span class=\"axe-accordion__trigger-label\">배포는 어떻게 하나요?</span>\n      <svg class=\"axe-accordion__chevron\" viewBox=\"0 0 12 12\" aria-hidden=\"true\">\n        <path d=\"M4 2.5 L8 6 L4 9.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" />\n      </svg>\n    </button>\n  </h3>\n  <div class=\"axe-accordion__content\" data-state=\"open\">\n    <div class=\"axe-accordion__content-inner\">axe ship &lt;svc&gt; 로 합니다.</div>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "AccordionItem.value 는 유일하게 — 열림 상태가 이 키로 매핑된다.",
          "한 번에 하나만 펼치는 게 자연스러우면 type=\"single\" + collapsible."
        ],
        "dont": [
          "단일 영역 하나만 접는 데 Accordion 을 쓰지 말 것(그건 Collapsible).",
          "AccordionTrigger 안에 인터랙티브 요소(버튼/링크)를 중첩하지 말 것(포커스 충돌)."
        ]
      },
      "consumeCss": "정적 open/closed 는 트리거·컨텐츠에 data-state=\"open|closed\" 를 직접 부여하면 .axe-accordion__* 스타일로 재현된다(chevron 회전 포함). 단 부드러운 height 애니메이션은 Radix 가 --radix-accordion-content-height 를 런타임 측정해 주입하므로 CSS-only 로는 즉시 토글만. 화살표 키 네비도 JS(Radix) 몫이다.",
      "url": "https://design.axelabs.ai/components/accordion"
    },
    {
      "name": "AgentExecutor",
      "slug": "agent-executor",
      "group": "chat",
      "summary": "Phase 8 AI-first 컴포지트 — 채팅 셸(AgentExecutor) + 점진 테이블(StreamingTable) + 멀티모달 입력(MultiModalChatInput).",
      "description": "Phase 1-7 primitive(ChatInput / MessageBubble / StreamingMarkdown / ToolCallCard / Spinner / Button) 위에 얹는 high-level 조합 3종을 한 모듈로 export 한다. AgentExecutor 는 controlled 채팅 셸(messages 는 부모 보유, onSubmit 으로 새 사용자 메시지)이며 assistant 메시지는 StreamingMarkdown, 인라인 tool 은 ToolCallCard 로 렌더하고 sticky-bottom 자동 스크롤을 한다. StreamingTable 은 row 를 하나씩 append 하는 제너릭 테이블(진행 bar·pending spinner 행), MultiModalChatInput 은 ChatInput 을 첨부(drag/drop·paste·file picker)로 확장한 입력이다. 셋 다 barrel 최상위에서 직접 export 된다.",
      "keywords": [
        "agent",
        "에이전트",
        "agent executor",
        "chat shell",
        "streaming table",
        "multimodal",
        "멀티모달",
        "첨부",
        "attachment",
        "drag drop",
        "tool call",
        "composite",
        "phase 8"
      ],
      "props": [
        {
          "name": "messages",
          "type": "ChatMessage[]",
          "required": true,
          "description": "AgentExecutor: 렌더할 메시지 배열. ChatMessage = { id, role, content, tools?, attachments? }."
        },
        {
          "name": "onSubmit",
          "type": "(text: string, attachments?: File[]) => void",
          "required": true,
          "description": "AgentExecutor: 사용자 제출 콜백. attachments 는 inputSlot 으로 MultiModalChatInput 을 쓸 때만."
        },
        {
          "name": "onCancel",
          "type": "() => void",
          "description": "AgentExecutor: 주어지면 streaming=true 일 때 streaming-hint 에 '취소' 버튼 노출."
        },
        {
          "name": "streaming",
          "type": "boolean",
          "defaultValue": "false",
          "description": "AgentExecutor: 마지막 assistant 메시지 끝 cursor + 하단 '응답 생성 중…' hint."
        },
        {
          "name": "tools",
          "type": "ToolCallEvent[]",
          "description": "AgentExecutor: 메시지에 속하지 않는 free-floating tool 카드(보통 message.tools 권장)."
        },
        {
          "name": "placeholder",
          "type": "string",
          "description": "AgentExecutor: 기본 ChatInput 의 placeholder(inputSlot 미사용 시)."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "description": "AgentExecutor: 기본 ChatInput 비활성(보통 streaming 중 true)."
        },
        {
          "name": "inputRef",
          "type": "React.Ref<ChatInputHandle>",
          "description": "AgentExecutor: 기본 ChatInput 의 외부 ref(focus/clear)."
        },
        {
          "name": "inputSlot",
          "type": "React.ReactNode",
          "description": "AgentExecutor: 입력 자리에 넣을 커스텀 노드 — 지정 시 기본 ChatInput 대체(예: MultiModalChatInput)."
        },
        {
          "name": "ref → AgentExecutorHandle",
          "type": "{ focus(): void; scrollToBottom(): void }",
          "description": "AgentExecutor: imperative 핸들 — 입력 focus·강제 맨아래 스크롤."
        },
        {
          "name": "StreamingTable.columns",
          "type": "StreamingTableColumn<R>[]",
          "required": true,
          "description": "StreamingTable: 열 정의. { key, header, render?, align?('left'|'right'|'center'), width? }."
        },
        {
          "name": "StreamingTable.rows",
          "type": "R[]",
          "required": true,
          "description": "StreamingTable: 현재까지의 행(추가되면 페이드인)."
        },
        {
          "name": "StreamingTable.getRowKey",
          "type": "(row: R, idx: number) => string",
          "required": true,
          "description": "StreamingTable: 안정적 row key."
        },
        {
          "name": "StreamingTable.streaming",
          "type": "boolean",
          "defaultValue": "false",
          "description": "StreamingTable: 마지막에 spinner pending 행 노출(다음 행 대기)."
        },
        {
          "name": "StreamingTable.expectedRowCount",
          "type": "number",
          "description": "StreamingTable: 총 행 수를 알면 상단 progress bar(0..100%)."
        },
        {
          "name": "StreamingTable.emptyLabel",
          "type": "React.ReactNode",
          "defaultValue": "\"—\"",
          "description": "StreamingTable: 행 0 & 비스트리밍 시 placeholder."
        },
        {
          "name": "MultiModalChatInput.onSubmit",
          "type": "(text: string, attachments: File[]) => void",
          "description": "MultiModalChatInput: 텍스트+첨부 제출(ChatInput 과 다른 시그니처 — 그래서 별도 컴포넌트)."
        },
        {
          "name": "MultiModalChatInput.accept",
          "type": "string",
          "description": "MultiModalChatInput: accept 패턴(예 'image/*,application/pdf'). '.pdf'/'image/*'/'application/pdf' 모두 매칭."
        },
        {
          "name": "MultiModalChatInput.maxFiles",
          "type": "number",
          "defaultValue": "5",
          "description": "MultiModalChatInput: 최대 첨부 개수(초과 시 onReject('count')). "
        },
        {
          "name": "MultiModalChatInput.maxSizeBytes",
          "type": "number",
          "defaultValue": "20 * 1024 * 1024",
          "description": "MultiModalChatInput: 파일당 최대 크기(초과 시 onReject('size'))."
        },
        {
          "name": "MultiModalChatInput.onReject",
          "type": "(reason: \"count\" | \"size\" | \"type\", file: File) => void",
          "description": "MultiModalChatInput: 거부 콜백(toast 등)."
        },
        {
          "name": "MultiModalChatInput.toolbarLeft / toolbarRight / hint",
          "type": "React.ReactNode",
          "description": "MultiModalChatInput: 툴바 슬롯·힌트(hint 기본 '↵ 보내기 · ⇧↵ 줄바꿈 · 끌어 놓기 / 붙여넣기로 첨부')."
        },
        {
          "name": "MultiModalChatInput.ref → ChatInputHandle",
          "type": "{ focus(): void; clear(): void }",
          "description": "MultiModalChatInput: imperative 핸들(clear 는 텍스트+첨부 모두 비움)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-agent-executor",
          "purpose": "AgentExecutor 루트(세로 flex, 높이 100%, 테두리 카드).",
          "stability": "stable"
        },
        {
          "className": "axe-agent-executor__scroll",
          "purpose": "메시지 스크롤 영역(reduced-motion 시 smooth 해제).",
          "stability": "stable"
        },
        {
          "className": "axe-agent-executor__empty",
          "purpose": "messages 0 일 때 '대화를 시작하세요' 빈 상태.",
          "stability": "stable"
        },
        {
          "className": "axe-agent-executor__user-content",
          "purpose": "사용자 메시지 본문(pre-wrap).",
          "stability": "stable"
        },
        {
          "className": "axe-agent-executor__tools",
          "purpose": "메시지 내부 ToolCallCard 목록 래퍼. --free 변형 = free-floating tools.",
          "stability": "stable"
        },
        {
          "className": "axe-agent-executor__attachments",
          "purpose": "사용자 메시지의 첨부 chip 행.",
          "stability": "stable"
        },
        {
          "className": "axe-agent-executor__streaming-hint",
          "purpose": "하단 '응답 생성 중…' 상태 바(Spinner + 선택 취소 버튼).",
          "stability": "stable"
        },
        {
          "className": "axe-agent-executor__input",
          "purpose": "입력 도크(상단 구분선, ChatInput 또는 inputSlot).",
          "stability": "stable"
        },
        {
          "className": "axe-chat-input--dragover",
          "purpose": "MultiModal: drag-over 하이라이트(accent 테두리·ring).",
          "stability": "stable"
        },
        {
          "className": "axe-chat-input--disabled",
          "purpose": "MultiModal: 비활성(opacity·pointer-events none).",
          "stability": "stable"
        },
        {
          "className": "axe-chat-input__drop-overlay",
          "purpose": "MultiModal: '끌어 놓아 첨부' 드롭 오버레이(aria-hidden).",
          "stability": "stable"
        },
        {
          "className": "axe-attach-list",
          "purpose": "첨부 chip 목록(입력 위).",
          "stability": "stable"
        },
        {
          "className": "axe-attach-chip",
          "purpose": "첨부 chip. __thumb(이미지 썸네일)/__icon(비이미지)/__name/__size/__remove 파트. --compact = AgentExecutor 사용자 메시지용 mini.",
          "stability": "stable"
        },
        {
          "className": "axe-attach-button",
          "purpose": "'📎 첨부' 파일 피커 버튼(maxFiles 도달 시 disabled).",
          "stability": "stable"
        },
        {
          "className": "axe-attach-file-input",
          "purpose": "숨긴 <input type=file>(display:none).",
          "stability": "internal"
        },
        {
          "className": "axe-streaming-table",
          "purpose": "StreamingTable 루트(elevated 카드). data-streaming 속성.",
          "stability": "stable"
        },
        {
          "className": "axe-streaming-table__progress",
          "purpose": "상단 progress 트랙. __progress-bar 는 --axe-progress(0..1) scaleX 로 채움.",
          "stability": "stable"
        },
        {
          "className": "axe-streaming-table__scroll",
          "purpose": "가로 스크롤 래퍼(min-width 480px 테이블).",
          "stability": "stable"
        },
        {
          "className": "axe-streaming-table__table",
          "purpose": "테이블 본체(<table>).",
          "stability": "stable"
        },
        {
          "className": "axe-streaming-table__th",
          "purpose": "헤더 셀. --right/--center 정렬 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-streaming-table__td",
          "purpose": "데이터 셀(tabular-nums). --right/--center 정렬 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-streaming-table__tr--in",
          "purpose": "새 행 페이드인(axe-stream-row-in, reduced-motion 시 off).",
          "stability": "internal"
        },
        {
          "className": "axe-streaming-table__pending",
          "purpose": "'다음 행 대기 중…' spinner 행(streaming 시).",
          "stability": "stable"
        },
        {
          "className": "axe-streaming-table__empty",
          "purpose": "행 0 & 비스트리밍 시 emptyLabel 셀.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "AgentExecutor: 기본 ChatInput 은 ↵ 보내기 · ⇧↵ 줄바꿈",
          "MultiModalChatInput: ↵ 보내기 · ⇧↵ 줄바꿈(IME 조합 중이면 submit 억제) · 붙여넣기/끌어 놓기로 첨부",
          "MultiModalChatInput: 각 chip 의 제거 버튼(aria-label \"<파일명> 제거\")로 삭제"
        ],
        "aria": "AgentExecutor 빈 상태는 aria-live=\"polite\", 하단 streaming-hint 는 role=\"status\" aria-live=\"polite\"(Spinner label=\"Streaming\")로 진행을 고지한다. StreamingTable 은 시맨틱 <table> 이고 pending 행이 aria-live=\"polite\" 로 '다음 행 대기 중' 을 알리지만, 상단 progress bar 는 aria-hidden 장식이라 수치 진행은 시각으로만 전달된다. MultiModalChatInput 의 파일 버튼은 aria-label=\"파일 첨부\"(maxFiles 도달 시 disabled), 드롭 오버레이는 aria-hidden 이다.",
        "notes": "AgentExecutor 의 자동 스크롤은 rAF 후 scrollTop 이동이며 사용자가 바닥에서 32px 이상 위로 스크롤하면 stick 해제된다(read 방해 없음). scroll-behavior 는 prefers-reduced-motion 에서 auto 로, StreamingTable 의 행 페이드인·progress transition 도 reduced-motion 에서 정지된다. MultiModal 의 dragenter/leave 는 중첩 카운팅으로 자식 위 호버 시 깜빡임을 막는다."
      },
      "examples": [
        {
          "title": "AgentExecutor — 기본 채팅 셸",
          "lang": "tsx",
          "code": "import { AgentExecutor, type ChatMessage } from \"@axe/ui\";\n\nconst [messages, setMessages] = useState<ChatMessage[]>([]);\n\n<AgentExecutor\n  messages={messages}\n  streaming={pending}\n  onCancel={abort}\n  placeholder=\"무엇이든 물어보세요\"\n  onSubmit={(text) => send(text)}\n/>"
        },
        {
          "title": "AgentExecutor — 인라인 tool call + 멀티모달 입력",
          "lang": "tsx",
          "code": "import { AgentExecutor, MultiModalChatInput, type ChatMessage } from \"@axe/ui\";\n\nconst messages: ChatMessage[] = [\n  {\n    id: \"a1\",\n    role: \"assistant\",\n    content: \"장부를 조회했습니다.\",\n    tools: [{ id: \"t1\", name: \"query_trial_balance\", status: \"success\", output: \"…\" }],\n  },\n];\n\n<AgentExecutor\n  messages={messages}\n  onSubmit={(text, files) => send(text, files)}\n  inputSlot={\n    <MultiModalChatInput\n      accept=\"image/*,application/pdf\"\n      maxFiles={3}\n      onSubmit={(text, files) => send(text, files)}\n      onReject={(reason, f) => toast(`${f.name}: ${reason}`)}\n    />\n  }\n/>"
        },
        {
          "title": "StreamingTable — 행 점진 append",
          "lang": "tsx",
          "code": "import { StreamingTable } from \"@axe/ui\";\n\n<StreamingTable\n  columns={[\n    { key: \"name\", header: \"종목\" },\n    { key: \"irr\", header: \"IRR\", align: \"right\" },\n  ]}\n  rows={rows}\n  streaming={pending}\n  expectedRowCount={20}\n  getRowKey={(r) => r.id}\n/>"
        },
        {
          "title": "StreamingTable — CSS-only 정적 (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-streaming-table\">\n  <div class=\"axe-streaming-table__scroll\">\n    <table class=\"axe-streaming-table__table\">\n      <thead class=\"axe-streaming-table__thead\">\n        <tr><th class=\"axe-streaming-table__th\">종목</th>\n            <th class=\"axe-streaming-table__th axe-streaming-table__th--right\">IRR</th></tr>\n      </thead>\n      <tbody>\n        <tr class=\"axe-streaming-table__tr\">\n          <td class=\"axe-streaming-table__td\">A사</td>\n          <td class=\"axe-streaming-table__td axe-streaming-table__td--right\">18.4%</td>\n        </tr>\n      </tbody>\n    </table>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "tool call 은 되도록 해당 message.tools 에 넣는다 — free-floating tools prop 은 pre-warm 등 예외용.",
          "streaming 중에는 disabled 로 기본 ChatInput 을 잠그거나 onCancel 을 제공해 4-state 를 완성한다.",
          "MultiModalChatInput 은 onSubmit 시그니처(text, files)가 ChatInput 과 다름을 인지하고 호출 측을 맞춘다."
        ],
        "dont": [
          "messages 를 컴포넌트 내부에서 변형하려 하지 말 것 — controlled(부모 소유)다.",
          "StreamingTable 의 progress 를 스크린리더 진행 고지로 기대하지 말 것 — bar 는 aria-hidden 시각 전용.",
          "AgentExecutorHandle(focus/scrollToBottom)과 ChatInputHandle(focus/clear)을 혼동하지 말 것 — 서로 다른 ref."
        ]
      },
      "consumeCss": "StreamingTable 은 시맨틱 <table> 마크업이라 서버 렌더(maud/jinja)로도 그대로 재현 가능하고, progress 는 __progress-bar 에 --axe-progress(0..1)만 주면 된다(구 inline width:% 도 하위호환). AgentExecutor·MultiModalChatInput 은 드래그·붙여넣기·자동 스크롤·첨부 상태가 모두 JS 이므로 CSS-only 로는 정적 셸/입력 스냅샷만 재현된다. 이 모듈 스타일(agent.css)은 아직 components.css 에 통합되지 않아 소비 측에서 명시적으로 import 해야 한다.",
      "url": "https://design.axelabs.ai/components/agent-executor"
    },
    {
      "name": "AlertDialog",
      "slug": "alert-dialog",
      "group": "overlay",
      "summary": "되돌릴 수 없는 행동을 확인시키는 경고 모달 (Radix AlertDialog 기반).",
      "description": "Dialog 와 비주얼은 거의 같지만, 사용자가 Cancel 또는 Action 중 하나를 반드시 명시적으로 선택하게 만드는 확인 전용 모달이다. 닫기 X 가 없고 바깥 클릭으로도 닫히지 않으며(Esc 는 Cancel 과 동치), role=alertdialog 로 보조 기술에 '주의' 모달임을 알린다. AlertDialogAction 은 variant=\"danger\" 로 파괴적 확인(삭제·영구취소)에 맞춘 danger 버튼이 되고, AlertDialogCancel 은 ghost 버튼으로 렌더된다. Confirm 용도라 size 기본값이 sm 이며 full/xl 변형은 없다.",
      "keywords": [
        "alert-dialog",
        "alertdialog",
        "확인",
        "confirm",
        "삭제",
        "destructive",
        "경고",
        "modal",
        "radix"
      ],
      "props": [
        {
          "name": "open",
          "type": "boolean",
          "description": "열림 상태 (controlled). Radix AlertDialog.Root prop."
        },
        {
          "name": "onOpenChange",
          "type": "(open: boolean) => void",
          "description": "열림/닫힘 전환 콜백 (Root)."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "defaultValue": "false",
          "description": "uncontrolled 초기 열림 상태 (Root)."
        },
        {
          "name": "size",
          "type": "\"sm\" | \"md\" | \"lg\"",
          "defaultValue": "\"sm\"",
          "description": "AlertDialogContent 의 max-width. sm=440 / md=560 / lg=720. Dialog 와 달리 xl/full 없음."
        },
        {
          "name": "variant (AlertDialogAction)",
          "type": "\"primary\" | \"danger\"",
          "defaultValue": "\"primary\"",
          "description": "확인 버튼 톤. danger 면 axe-btn--danger 로 파괴적 확인에 맞춘 색을 입힌다."
        },
        {
          "name": "...rest (AlertDialogContent)",
          "type": "React.ComponentPropsWithoutRef<typeof AlertDialog.Content>",
          "description": "onEscapeKeyDown 등 Radix Content props 를 전파."
        },
        {
          "name": "...rest (Action/Cancel)",
          "type": "각 Radix AlertDialog.Action / .Cancel props",
          "description": "onClick·asChild 등 대응 Radix 파트 props 를 그대로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-alert-dialog__overlay",
          "purpose": "화면 전체 흐린 backdrop(z-modal).",
          "stability": "stable"
        },
        {
          "className": "axe-alert-dialog__content",
          "purpose": "중앙 정렬 확인 카드 표면.",
          "stability": "stable"
        },
        {
          "className": "axe-alert-dialog__content--sm",
          "purpose": "max-width 440px 변형(기본).",
          "stability": "stable"
        },
        {
          "className": "axe-alert-dialog__content--md",
          "purpose": "max-width 560px 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-alert-dialog__content--lg",
          "purpose": "max-width 720px 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-alert-dialog__header",
          "purpose": "title+설명 스택(close X 없어 padding-right 0).",
          "stability": "stable"
        },
        {
          "className": "axe-alert-dialog__title",
          "purpose": "제목(display 폰트·semibold).",
          "stability": "stable"
        },
        {
          "className": "axe-alert-dialog__description",
          "purpose": "결과 설명 문단(secondary 톤).",
          "stability": "stable"
        },
        {
          "className": "axe-alert-dialog__footer",
          "purpose": "우정렬 Cancel|Action 라인(상단 구분선).",
          "stability": "stable"
        },
        {
          "className": "axe-alert-dialog__action",
          "purpose": "확인 버튼 보정(min-width·중앙정렬). axe-btn 위에 얹힘.",
          "stability": "stable"
        },
        {
          "className": "axe-alert-dialog__cancel",
          "purpose": "취소 버튼 보정. axe-btn axe-btn--ghost 위에 얹힘.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Esc 로 취소(Cancel 과 동치)",
          "Tab/Shift+Tab 은 콘텐츠 안에서만 순환(포커스 트랩)",
          "열릴 때 기본 포커스가 Cancel(안전쪽)로 이동"
        ],
        "aria": "Radix 가 콘텐츠에 role=alertdialog·aria-modal=true 를 부여하고, AlertDialogTitle 을 aria-labelledby, AlertDialogDescription 을 aria-describedby 로 자동 연결한다. Cancel/Action 은 각각 취소·확정 시맨틱을 가진 버튼이다.",
        "notes": "Dialog 와 달리 바깥(오버레이) 클릭으로 닫히지 않아 실수 확정을 막는다. 기본 포커스가 Action 이 아닌 Cancel 에 놓여 파괴적 행동을 안전쪽으로 default 한다. data-state 로 진입/퇴장 애니메이션이 갈리며 prefers-reduced-motion 시 제거된다."
      },
      "examples": [
        {
          "title": "파괴적 확인 (삭제)",
          "lang": "tsx",
          "code": "import {\n  AlertDialog,\n  AlertDialogTrigger,\n  AlertDialogContent,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogCancel,\n  AlertDialogAction,\n  Button,\n} from \"@axe/ui\";\n\n<AlertDialog>\n  <AlertDialogTrigger asChild>\n    <Button variant=\"danger\">삭제</Button>\n  </AlertDialogTrigger>\n  <AlertDialogContent>\n    <AlertDialogHeader>\n      <AlertDialogTitle>문서 3개 영구 삭제</AlertDialogTitle>\n      <AlertDialogDescription>\n        이 작업은 되돌릴 수 없습니다.\n      </AlertDialogDescription>\n    </AlertDialogHeader>\n    <AlertDialogFooter>\n      <AlertDialogCancel>취소</AlertDialogCancel>\n      <AlertDialogAction variant=\"danger\" onClick={confirmDelete}>\n        삭제\n      </AlertDialogAction>\n    </AlertDialogFooter>\n  </AlertDialogContent>\n</AlertDialog>"
        },
        {
          "title": "CSS-only 정적 표면 (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-alert-dialog__overlay\"></div>\n<div class=\"axe-alert-dialog__content axe-alert-dialog__content--sm\" role=\"alertdialog\" aria-modal=\"true\" aria-labelledby=\"ad-t\">\n  <div class=\"axe-alert-dialog__header\">\n    <h2 id=\"ad-t\" class=\"axe-alert-dialog__title\">문서 3개 영구 삭제</h2>\n    <p class=\"axe-alert-dialog__description\">이 작업은 되돌릴 수 없습니다.</p>\n  </div>\n  <div class=\"axe-alert-dialog__footer\">\n    <button class=\"axe-btn axe-btn--ghost axe-alert-dialog__cancel\">취소</button>\n    <button class=\"axe-btn axe-btn--danger axe-alert-dialog__action\">삭제</button>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "제목·설명에 결과(무엇이·되돌릴 수 없음)를 구체적으로 서술.",
          "파괴적 확인 버튼은 variant=\"danger\" 로.",
          "확인이 필요 없는 단순 안내/폼은 Dialog 를 쓸 것."
        ],
        "dont": [
          "여기에 close X 나 바깥클릭 닫기를 임의로 추가하지 말 것 — 명시적 선택 강제가 목적.",
          "size=full/xl 을 기대하지 말 것 — confirm 모달은 sm~lg 만."
        ]
      },
      "consumeCss": "정적 표면은 .axe-alert-dialog__* + .axe-btn 마크업으로 재현 가능하다. 단 role=alertdialog·포커스 트랩·Esc=Cancel·바깥클릭 무시 같은 확인 시맨틱은 Radix 런타임이 만들므로, 실제 확인 흐름은 React 컴포넌트로 소비해야 한다. AlertDialogContent 는 내부에서 오버레이를 항상 함께 렌더하므로 별도 export 인 AlertDialogOverlay 를 기본 구성에서 추가로 둘 필요는 없다(커스텀 조립용).",
      "url": "https://design.axelabs.ai/components/alert-dialog"
    },
    {
      "name": "AppShell",
      "slug": "app-shell",
      "group": "layout",
      "summary": "페이지 전체 레이아웃 chrome — 슬롯 골격과 Design/Gate 공용 workspace 표면.",
      "description": "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로 노출한다.",
      "keywords": [
        "app shell",
        "앱셸",
        "layout",
        "레이아웃",
        "chrome",
        "docs",
        "dashboard",
        "landing",
        "toc",
        "shell"
      ],
      "props": [
        {
          "name": "variant",
          "type": "\"docs\" | \"dashboard\" | \"landing\"",
          "defaultValue": "\"docs\"",
          "description": "그리드 골격 선택. data-shell 속성으로 CSS 가 분기."
        },
        {
          "name": "topbar",
          "type": "React.ReactNode",
          "description": "상단 바 슬롯(보통 <TopNav>)."
        },
        {
          "name": "sidebar",
          "type": "React.ReactNode",
          "description": "좌측 네비 슬롯(보통 <Sidebar>). landing 에서는 무시."
        },
        {
          "name": "toc",
          "type": "React.ReactNode",
          "description": "우측 목차 슬롯(보통 <Toc>). variant=\"docs\" 에서만 표시."
        },
        {
          "name": "footer",
          "type": "React.ReactNode",
          "description": "하단 슬롯(보통 <Footer>)."
        },
        {
          "name": "contentClassName",
          "type": "string",
          "description": "본문 <main> 에 추가 className."
        },
        {
          "name": "contentAs",
          "type": "\"main\" | \"div\"",
          "defaultValue": "\"main\"",
          "description": "본문 태그. 다른 문서 안에 specimen으로 중첩할 때만 div."
        },
        {
          "name": "contentId",
          "type": "string",
          "defaultValue": "\"main\"",
          "description": "본문 id와 skip-link target."
        },
        {
          "name": "showSkipLink",
          "type": "boolean",
          "defaultValue": "true",
          "description": "스킵 링크 렌더 여부. 중첩 specimen에서는 false."
        },
        {
          "name": "sidebarLabel",
          "type": "string",
          "defaultValue": "\"Primary\"",
          "description": "sidebar <aside> 의 aria-label."
        },
        {
          "name": "tocLabel",
          "type": "string",
          "defaultValue": "\"On this page\"",
          "description": "toc <aside> 의 aria-label."
        },
        {
          "name": "subnav",
          "type": "React.ReactNode | false",
          "description": "≤1023px disclosure 내용. 기본 = sidebar 재사용. 명시 DOM id가 있는 sidebar는 고유 id의 별도 노드를 넘기거나 false로 끈다."
        },
        {
          "name": "subnavLabel",
          "type": "React.ReactNode",
          "defaultValue": "\"메뉴\"",
          "description": "≤1023px disclosure summary 라벨."
        },
        {
          "name": "children",
          "type": "React.ReactNode",
          "description": "본문 — <main id=\"main\"> 안에 렌더."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLDivElement>",
          "description": "나머지는 루트 <div> 로 전파."
        },
        {
          "name": "Toc.items",
          "type": "TocItem[]",
          "required": true,
          "description": "{ id, label, level?: 2 | 3 } 배열. level=2 top / 3 sub(들여쓰기), 기본 2."
        },
        {
          "name": "Toc.title",
          "type": "React.ReactNode",
          "defaultValue": "\"On this page\"",
          "description": "목차 상단 라벨. string 이면 <nav> aria-label 로도 사용."
        },
        {
          "name": "Toc.activeId",
          "type": "string",
          "description": "현재 활성 heading id(외부 스크롤 spy 가 전달). 해당 링크에 aria-current=\"location\"."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-app-shell",
          "purpose": "루트 <div>. data-shell=\"docs|dashboard|landing\" 로 그리드 분기.",
          "stability": "stable"
        },
        {
          "className": "axe-skip-link",
          "purpose": "본문(#main)으로 건너뛰는 스킵 링크(포커스 시 노출).",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__topbar",
          "purpose": "상단 바 영역(topbar + ≤1023px subnav 포함).",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__main",
          "purpose": "sidebar|content|toc 그리드 행.",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__sidebar",
          "purpose": "좌측 네비 슬롯 <aside>(sticky/scroll).",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__content",
          "purpose": "본문 <main id=\"main\">. variant 별 max-width.",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__toc",
          "purpose": "우측 목차 슬롯 <aside>(docs 전용).",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__footer",
          "purpose": "하단 footer 슬롯.",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__toc-title",
          "purpose": "Toc 상단 라벨.",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__toc-list",
          "purpose": "Toc 링크 <ul>.",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__toc-link",
          "purpose": "Toc 앵커 링크. 활성 시 aria-current=\"location\".",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__toc-link--h3",
          "purpose": "h3(level 3) Toc 링크 — 들여쓰기 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__subnav",
          "purpose": "≤1023px 보조 네비 <details> disclosure(≥1024px display:none).",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__subnav-summary",
          "purpose": "disclosure <summary> 토글.",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__subnav-label",
          "purpose": "disclosure summary 라벨 span.",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__subnav-panel",
          "purpose": "disclosure 펼침 패널(네비 복제 렌더).",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__subnav-toc",
          "purpose": "subnav 패널 안 목차 래퍼(docs + toc 있을 때).",
          "stability": "stable"
        },
        {
          "className": "axe-app-shell__subnav-chevron",
          "purpose": "subnav summary chevron(장식, [open] 회전은 CSS).",
          "stability": "internal"
        },
        {
          "className": "axe-workspace-shell",
          "purpose": "Design/Gate 공용 neutral workspace root. AppShell docs variant와 함께 사용.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-bar",
          "purpose": "56px workspace top bar.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-bar__left",
          "purpose": "top bar 좌측 rail track.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-bar__center",
          "purpose": "bounded center route track.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-bar__right",
          "purpose": "검색과 context action track.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-bar__routes",
          "purpose": "desktop 중앙 route navigation.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-bar__mobile-brand",
          "purpose": "rail이 접힐 때 표시하는 mobile product identity.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-bar__icon-button",
          "purpose": "44px menu/context icon trigger.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-disclosure",
          "purpose": "native details 기반 navigation/context sheet root.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-disclosure--navigation",
          "purpose": "≤1023px 좌측 navigation disclosure.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-disclosure--context",
          "purpose": "<1440px 우측 context disclosure.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-disclosure__panel",
          "purpose": "viewport 고정 disclosure panel.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-rail",
          "purpose": "full-height primary rail flex root.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-rail__brand",
          "purpose": "56px entity/search header.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-rail__body",
          "purpose": "rail 내부의 유일한 세로 scroll 영역.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-rail__identity",
          "purpose": "48px 고정 identity footer.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-identity",
          "purpose": "pill 없이 rail 폭을 채우는 고정 identity row.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-identity__avatar",
          "purpose": "20px brand-tone identity avatar.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-identity__label",
          "purpose": "말줄임 처리되는 identity label.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-identity-menu",
          "purpose": "rail 하단에서 위로 열리는 native identity menu.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-identity-menu__panel",
          "purpose": "identity action을 담는 고정 폭 popover panel.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-rail__nav",
          "purpose": "30px row와 1px seam을 소유하는 navigation.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-rail__group-label",
          "purpose": "대메뉴 또는 비상호작용 group label.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-rail__link",
          "purpose": "hover/active/focus가 정렬된 navigation link.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-entity",
          "purpose": "rail entity anchor 또는 native details switcher.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-search",
          "purpose": "text 또는 data-icon-only 검색 trigger.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-search-dialog",
          "purpose": "static CommandPalette markup을 native dialog로 호스트하는 workspace 검색 표면.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-context",
          "purpose": "full-height 우측 context rail.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-context__header",
          "purpose": "현재 업무의 제목과 요약.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-context__eyebrow",
          "purpose": "context overline.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-context__title",
          "purpose": "context title.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-context__summary",
          "purpose": "context 설명.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-context__tabs",
          "purpose": "균등 분할 context tab row.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-context__tab",
          "purpose": "현재 tab 하단 indicator trigger/link.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-context__body",
          "purpose": "context panel content.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-context__section-title",
          "purpose": "context section caption.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-context__link",
          "purpose": "32px context location link.",
          "stability": "stable"
        },
        {
          "className": "axe-workspace-context__footer",
          "purpose": "context rail 고정 하단 metadata.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Tab 첫 스텝에서 .axe-skip-link 가 나타나 Enter 로 #main 본문 점프",
          "≤1023px subnav: <summary> 에서 Enter/Space 로 펼침/접힘(네이티브 <details>)",
          "subnav 펼침 중 Esc — 닫고 summary 로 포커스 복원",
          "workspace disclosure는 summary에서 Enter/Space로 열고, host JS가 Esc·외부 클릭·링크 이동 시 닫은 뒤 trigger로 포커스를 복원"
        ],
        "aria": "본문은 <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}가 필요하다.",
        "notes": "무JS(jinja/maud) 환경에서도 subnav와 workspace disclosure는 네이티브 <details> 토글로 동작한다. Host JS가 있으면 패널 링크·외부 pointerdown·Esc 닫기와 포커스 복원을 얹는다. 검색은 정적 .axe-cmdk markup만으로 모달 동작이 완성되지 않으므로 native dialog 또는 동등한 focus 관리 runtime이 필요하다."
      },
      "examples": [
        {
          "title": "docs variant",
          "lang": "tsx",
          "code": "import { AppShell, Sidebar, Toc, TopNav, Footer } from \"@axe/ui\";\n\n<AppShell\n  variant=\"docs\"\n  topbar={<TopNav /* ... */ />}\n  sidebar={\n    <Sidebar items={navItems} value={cur} onValueChange={setCur} />\n  }\n  toc={\n    <Toc\n      activeId={activeHeading}\n      items={[\n        { id: \"intro\", label: \"소개\" },\n        { id: \"api\", label: \"API\", level: 3 },\n      ]}\n    />\n  }\n  footer={<Footer />}\n>\n  <article className=\"axe-prose\">…</article>\n</AppShell>"
        },
        {
          "title": "dashboard variant (toc 없음)",
          "lang": "tsx",
          "code": "import { AppShell, Sidebar } from \"@axe/ui\";\n\n<AppShell\n  variant=\"dashboard\"\n  sidebar={<Sidebar items={navItems} value={cur} onValueChange={setCur} />}\n  subnavLabel=\"탐색\"\n>\n  <section>…대시보드 본문…</section>\n</AppShell>"
        },
        {
          "title": "CSS-only 골격 (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-app-shell\" data-shell=\"docs\">\n  <a href=\"#main\" class=\"axe-skip-link\">본문으로 건너뛰기</a>\n  <div class=\"axe-app-shell__topbar\">\n    <!-- topbar -->\n    <details class=\"axe-app-shell__subnav\">\n      <summary class=\"axe-app-shell__subnav-summary\">\n        <span class=\"axe-app-shell__subnav-label\">메뉴</span>\n      </summary>\n      <div class=\"axe-app-shell__subnav-panel\">\n        <nav class=\"axe-sidebar\" aria-label=\"Primary\"><!-- 네비 복제 --></nav>\n      </div>\n    </details>\n  </div>\n  <div class=\"axe-app-shell__main\">\n    <aside class=\"axe-app-shell__sidebar\" aria-label=\"Primary\">…</aside>\n    <main id=\"main\" class=\"axe-app-shell__content\">…</main>\n    <aside class=\"axe-app-shell__toc\" aria-label=\"On this page\">…</aside>\n  </div>\n  <div class=\"axe-app-shell__footer\"><!-- footer --></div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "본문 스크롤 대상 랜드마크로 <main id=\"main\"> 를 유지(스킵 링크 타깃).",
          "문서 안에 AppShell을 specimen으로 중첩할 때만 contentAs=\"div\" + 고유 contentId + showSkipLink={false}를 함께 사용.",
          "toc 는 variant=\"docs\" 에서만 의미가 있다 — dashboard/landing 에는 넘기지 말 것.",
          "sidebar 자손에 명시 DOM id/htmlFor/aria-labelledby가 있으면 고유 id를 쓰는 별도 subnav 노드를 넘기거나 subnav={false}로 중복 렌더를 피할 것.",
          "sidebar 를 넘기면 ≤1023px subnav 가 자동으로 그 네비를 재사용한다."
        ],
        "dont": [
          "AppShellSubnav 를 직접 import 하지 말 것 — 내부 전용(barrel 미노출), subnav/subnavLabel props 로 합성.",
          "subnav <summary> 에 aria-expanded 를 수동으로 붙이지 말 것(네이티브가 이미 고지)."
        ]
      },
      "consumeCss": "그리드 분기는 클래스가 아니라 루트의 data-shell=\"docs|dashboard|landing\" 속성이 결정한다 — CSS-only 로 쓸 때 반드시 이 속성을 준다. ≤1023px 보조 네비는 네이티브 <details> 라 JS 없이도 토글되며, 데스크톱 컬럼과 동일 markup 을 subnav 패널에 복제해 넣으면 된다.",
      "url": "https://design.axelabs.ai/components/app-shell"
    },
    {
      "name": "Avatar",
      "slug": "avatar",
      "group": "content",
      "summary": "user/agent 신원 배지 — 이미지 실패 시 initials 폴백.",
      "description": "ChatMessage 의 sender, AgentInbox 의 agent 신원, 온보딩 프로필 placeholder 등 신원 표시의 표준. Radix Avatar 를 감싸 이미지 로드 실패/미지정 시 Fallback(initials)을 보여준다. 원형 표면에 sm/md/lg 세 크기(24/32/48px)를 제공하며, Fallback 의 delayMs 로 빠른 로드 시 initials 깜빡임(LCP 저해)을 억제한다.",
      "keywords": [
        "avatar",
        "아바타",
        "profile",
        "프로필",
        "identity",
        "신원",
        "initials",
        "이니셜",
        "user",
        "agent",
        "radix"
      ],
      "props": [
        {
          "name": "Avatar.size",
          "type": "\"sm\" | \"md\" | \"lg\"",
          "defaultValue": "\"md\"",
          "description": "크기 — sm 24 / md 32 / lg 48px(폰트 크기 동반)."
        },
        {
          "name": "Avatar.…rest",
          "type": "React.ComponentPropsWithoutRef<Radix Avatar.Root>",
          "description": "className 은 axe-avatar 와 병합, 나머지는 Radix Root 로 전파."
        },
        {
          "name": "AvatarImage.…rest",
          "type": "React.ComponentPropsWithoutRef<Radix Avatar.Image>",
          "description": "src/alt 등. 로드 실패/지연 시 Fallback 이 대신 노출. 나머지는 Radix Image 로 전파."
        },
        {
          "name": "AvatarFallback.delayMs",
          "type": "number",
          "description": "이 ms 만큼 지연 후 fallback 표시 — 빠른 이미지 로드 시 initials 깜빡임 방지(Radix)."
        },
        {
          "name": "AvatarFallback.…rest",
          "type": "React.ComponentPropsWithoutRef<Radix Avatar.Fallback>",
          "description": "children=initials 등. className 은 axe-avatar__fallback 과 병합, 나머지는 Radix Fallback 으로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-avatar",
          "purpose": "루트 — 원형(border-radius 9999px) inline-flex, overflow hidden, bg-subtle.",
          "stability": "stable"
        },
        {
          "className": "axe-avatar--sm",
          "purpose": "24×24px(폰트 10px).",
          "stability": "stable"
        },
        {
          "className": "axe-avatar--md",
          "purpose": "32×32px(폰트 12px, 기본).",
          "stability": "stable"
        },
        {
          "className": "axe-avatar--lg",
          "purpose": "48×48px(폰트 18px).",
          "stability": "stable"
        },
        {
          "className": "axe-avatar__image",
          "purpose": "이미지 — object-fit:cover, 원형 상속.",
          "stability": "stable"
        },
        {
          "className": "axe-avatar__fallback",
          "purpose": "initials 폴백 — 중앙 정렬, uppercase, bg-subtle.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "이미지에는 의미 있는 alt 를 지정한다(빈 alt=순수 장식). Fallback 의 initials 는 스크린리더에서 이름을 대체하지 못하므로, 사람 이름 같은 의미는 AvatarImage 의 alt 나 인접 텍스트로 제공해야 한다.",
        "notes": "Radix Avatar 가 이미지 로드 상태를 관리 — 실패/미지정 시 Fallback 으로 전환하고 delayMs 로 초기 깜빡임을 억제한다. 자체 포커스/role 은 없다(순수 표시). 클릭 대상이면 <button>/<a> 로 감싼다."
      },
      "examples": [
        {
          "title": "이미지 + 폴백",
          "lang": "tsx",
          "code": "import { Avatar, AvatarImage, AvatarFallback } from \"@axe/ui\";\n\n<Avatar size=\"md\">\n  <AvatarImage src=\"/u/axe.png\" alt=\"강수훈\" />\n  <AvatarFallback delayMs={300}>강</AvatarFallback>\n</Avatar>"
        },
        {
          "title": "이니셜만",
          "lang": "tsx",
          "code": "import { Avatar, AvatarFallback } from \"@axe/ui\";\n\n<Avatar size=\"lg\">\n  <AvatarFallback>AX</AvatarFallback>\n</Avatar>"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<!-- 이미지가 확실할 때 -->\n<span class=\"axe-avatar axe-avatar--md\">\n  <img class=\"axe-avatar__image\" src=\"/u/axe.png\" alt=\"강수훈\" />\n</span>\n\n<!-- 이니셜만 -->\n<span class=\"axe-avatar axe-avatar--lg\">\n  <span class=\"axe-avatar__fallback\">AX</span>\n</span>"
        }
      ],
      "bestPractices": {
        "do": [
          "Fallback initials 는 1~2자로 짧게 유지한다.",
          "이미지에 alt 로 신원을 담고, 순수 장식이면 alt=\"\" 로 명시한다."
        ],
        "dont": [
          "Avatar 자체에 onClick 만 걸어 클릭 UI 로 쓰지 말 것(포커스/role 없음 — 버튼/링크로 감싼다)."
        ]
      },
      "consumeCss": "정적 마크업으로 재현 가능 — 이미지가 확실하면 <span class=\"axe-avatar\"><img class=\"axe-avatar__image\">, 이니셜만이면 <span class=\"axe-avatar__fallback\">. Radix 의 로드-실패 자동 폴백만 CSS-only 로는 불가하므로(정적 선택 필요) 서버 렌더 시 둘 중 하나를 직접 고른다.",
      "url": "https://design.axelabs.ai/components/avatar"
    },
    {
      "name": "Badge",
      "slug": "badge",
      "group": "content",
      "summary": "상태·분류를 나타내는 소형 인라인 라벨.",
      "description": "의미 색상(success/warning/danger/info/accent)을 가진 <span> 토큰이다. default 변형은 base 클래스만 붙고, 나머지 변형은 `axe-badge--<variant>` 를 추가한다. 순수 표시 요소라 상호작용/role 이 없으며, className 과 나머지 HTML 속성은 루트 <span> 으로 전파된다.",
      "keywords": [
        "badge",
        "배지",
        "라벨",
        "tag",
        "칩",
        "상태",
        "status",
        "pill"
      ],
      "props": [
        {
          "name": "variant",
          "type": "\"default\" | \"accent\" | \"success\" | \"warning\" | \"danger\" | \"info\"",
          "defaultValue": "\"default\"",
          "description": "의미 색상. default 는 modifier 없이 중립 톤, 나머지는 각 의미 색을 입힌다."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLSpanElement>",
          "description": "className 병합 후 나머지 속성은 루트 <span> 으로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-badge",
          "purpose": "배지 루트 <span> (default 톤 포함).",
          "stability": "stable"
        },
        {
          "className": "axe-badge--accent",
          "purpose": "accent(기능 강조) 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-badge--success",
          "purpose": "성공/정상 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-badge--warning",
          "purpose": "주의 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-badge--danger",
          "purpose": "위험/오류 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-badge--info",
          "purpose": "정보 변형.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "네이티브 <span> — 별도 role 없음. 스크린리더는 텍스트 콘텐츠만 읽으므로 의미를 색에만 의존시키지 말고 라벨 텍스트로도 전달할 것.",
        "notes": "default 변형은 modifier 클래스가 붙지 않는다(base 클래스가 곧 default 룩)."
      },
      "examples": [
        {
          "title": "의미 색상",
          "lang": "tsx",
          "code": "import { Badge } from \"@axe/ui\";\n\nexport function Example() {\n  return (\n    <>\n      <Badge variant=\"success\">활성</Badge>\n      <Badge variant=\"warning\">보류</Badge>\n      <Badge variant=\"danger\">실패</Badge>\n    </>\n  );\n}"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<span class=\"axe-badge axe-badge--success\">활성</span>\n<span class=\"axe-badge\">중립</span>"
        }
      ],
      "bestPractices": {
        "do": [
          "색과 함께 텍스트 라벨로도 상태를 전달할 것.",
          "짧은 명사/형용사 한 단어로 유지."
        ],
        "dont": [
          "긴 문장이나 링크를 넣지 말 것 — 표시 토큰 용도.",
          "클릭 동작을 걸지 말 것(상호작용은 Button/Toggle)."
        ]
      },
      "consumeCss": "CSS-only 로 완전 재현 가능. default 는 `axe-badge` 만, 변형은 `axe-badge axe-badge--<variant>` 로 붙인다.",
      "playground": {
        "componentKey": "badge",
        "defaults": {
          "variant": "accent",
          "children": "LIVE"
        }
      },
      "url": "https://design.axelabs.ai/components/badge"
    },
    {
      "name": "Breadcrumb",
      "slug": "breadcrumb",
      "group": "navigation",
      "summary": "현재 위치까지의 페이지 경로 트레일 (semantic <nav> + <ol>).",
      "description": "<nav aria-label=\"Breadcrumb\"> 안에 <ol> 리스트로 경로를 표시하는 compound 컴포넌트다. Breadcrumb · BreadcrumbList · BreadcrumbItem · BreadcrumbLink · BreadcrumbSeparator · BreadcrumbCurrent 를 조합해 마크업한다. 마지막(현재 페이지) 항목은 링크가 아니라 BreadcrumbCurrent 로 표시하며 aria-current=\"page\" 가 자동 부여된다. 구분자 기본값은 \"›\" 이며 separator prop 이나 children 으로 교체할 수 있다. 순수 semantic + CSS — 상호작용 JS 가 없다.",
      "keywords": [
        "breadcrumb",
        "빵부스러기",
        "경로",
        "이동경로",
        "trail",
        "navigation"
      ],
      "props": [
        {
          "name": "ariaLabel",
          "type": "string",
          "defaultValue": "\"Breadcrumb\"",
          "description": "<Breadcrumb> nav 의 aria-label (i18n override)."
        },
        {
          "name": "separator",
          "type": "React.ReactNode",
          "defaultValue": "\"›\"",
          "description": "<BreadcrumbSeparator> 의 구분 문자/요소. children 이 있으면 children 이 우선."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes",
          "description": "각 sub-export 는 대응 HTML 요소 props 를 그대로 전파: Breadcrumb→<nav>, BreadcrumbList→<ol>, BreadcrumbItem→<li>, BreadcrumbLink→<a>, BreadcrumbCurrent→<span>, BreadcrumbSeparator→<li>."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-breadcrumb",
          "purpose": "<nav> 루트 (text-sm, secondary 색).",
          "stability": "stable"
        },
        {
          "className": "axe-breadcrumb__list",
          "purpose": "<ol> 리스트 (flex wrap, gap).",
          "stability": "stable"
        },
        {
          "className": "axe-breadcrumb__item",
          "purpose": "<li> 개별 항목.",
          "stability": "stable"
        },
        {
          "className": "axe-breadcrumb__link",
          "purpose": "<a> 링크 (hover/focus-visible 상태).",
          "stability": "stable"
        },
        {
          "className": "axe-breadcrumb__current",
          "purpose": "마지막 현재 항목 <span> (primary 색, ellipsis truncate 24ch).",
          "stability": "stable"
        },
        {
          "className": "axe-breadcrumb__separator",
          "purpose": "구분자 <li> (muted, aria-hidden).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "표준 링크 Tab 순회 (별도 키 상호작용 없음)"
        ],
        "aria": "루트 <nav aria-label=\"Breadcrumb\">(ariaLabel prop 로 변경). BreadcrumbCurrent = aria-current=\"page\" 자동. BreadcrumbSeparator = role=\"presentation\" + aria-hidden=\"true\" (SR 가 구분자를 읽지 않음).",
        "notes": "마지막 항목은 링크가 아니라 BreadcrumbCurrent(비-링크 현재 위치)로 표시. 순수 semantic 마크업 — JS 불요."
      },
      "examples": [
        {
          "title": "기본 트레일",
          "lang": "tsx",
          "code": "import {\n  Breadcrumb, BreadcrumbList, BreadcrumbItem,\n  BreadcrumbLink, BreadcrumbSeparator, BreadcrumbCurrent,\n} from \"@axe/ui\";\n\n<Breadcrumb>\n  <BreadcrumbList>\n    <BreadcrumbItem><BreadcrumbLink href=\"/\">홈</BreadcrumbLink></BreadcrumbItem>\n    <BreadcrumbSeparator />\n    <BreadcrumbItem><BreadcrumbLink href=\"/ui\">UI</BreadcrumbLink></BreadcrumbItem>\n    <BreadcrumbSeparator />\n    <BreadcrumbItem><BreadcrumbCurrent>Layout</BreadcrumbCurrent></BreadcrumbItem>\n  </BreadcrumbList>\n</Breadcrumb>"
        },
        {
          "title": "커스텀 구분자",
          "lang": "tsx",
          "code": "import { BreadcrumbSeparator } from \"@axe/ui\";\n\n<BreadcrumbSeparator separator=\"/\" />"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<nav class=\"axe-breadcrumb\" aria-label=\"Breadcrumb\">\n  <ol class=\"axe-breadcrumb__list\">\n    <li class=\"axe-breadcrumb__item\"><a class=\"axe-breadcrumb__link\" href=\"/\">홈</a></li>\n    <li class=\"axe-breadcrumb__separator\" role=\"presentation\" aria-hidden=\"true\">›</li>\n    <li class=\"axe-breadcrumb__item\"><a class=\"axe-breadcrumb__link\" href=\"/ui\">UI</a></li>\n    <li class=\"axe-breadcrumb__separator\" role=\"presentation\" aria-hidden=\"true\">›</li>\n    <li class=\"axe-breadcrumb__item\"><span class=\"axe-breadcrumb__current\" aria-current=\"page\">Layout</span></li>\n  </ol>\n</nav>"
        }
      ],
      "bestPractices": {
        "do": [
          "마지막(현재 페이지) 항목은 BreadcrumbCurrent 로 (링크 아님).",
          "항목은 최상위 → 현재 순서로 나열.",
          "구분자는 aria-hidden 을 유지해 SR 가 읽지 않게."
        ],
        "dont": [
          "구분자에 의미 있는 텍스트를 넣지 말 것 (장식 전용).",
          "BreadcrumbCurrent 를 링크로 감싸지 말 것."
        ]
      },
      "consumeCss": "완전 CSS 소비 가능 — 위 html 예제처럼 순수 <nav>/<ol> 마크업으로 동일 결과를 낸다. JS 상태가 전혀 없다.",
      "url": "https://design.axelabs.ai/components/breadcrumb"
    },
    {
      "name": "Button",
      "slug": "button",
      "group": "action",
      "summary": "행동을 트리거하는 기본 버튼.",
      "description": "primary/secondary/ghost/danger 4 변형과 sm/md/lg 크기를 CSS 변수로 처리한다. loading 은 자식 앞에 sm Spinner 를 렌더하고 버튼을 disabled + aria-busy 로 만든다. leftIcon/rightIcon 은 `axe-btn__icon` 슬롯으로 래핑된다. asChild(Radix Slot)로 <a> 등 임의 element 에 버튼 스타일을 입힐 수 있으나 이때 Slot 은 단일 자식만 허용하므로 Spinner/icon 슬롯은 렌더되지 않는다.",
      "keywords": [
        "button",
        "버튼",
        "cta",
        "action",
        "asChild",
        "loading",
        "icon"
      ],
      "props": [
        {
          "name": "variant",
          "type": "\"primary\" | \"secondary\" | \"ghost\" | \"danger\"",
          "defaultValue": "\"secondary\"",
          "description": "시각 강조 단계 / 의미(danger = 파괴적 행동)."
        },
        {
          "name": "size",
          "type": "\"sm\" | \"md\" | \"lg\"",
          "defaultValue": "\"md\"",
          "description": "높이·패딩·폰트 크기. md 는 modifier 없이 기본."
        },
        {
          "name": "asChild",
          "type": "boolean",
          "defaultValue": "false",
          "description": "true 면 <button> 대신 Radix Slot 으로 단일 자식 element 에 스타일/props 를 병합(예 <a>). 이때 Spinner/leftIcon/rightIcon 미렌더."
        },
        {
          "name": "loading",
          "type": "boolean",
          "defaultValue": "false",
          "description": "disabled + aria-busy + 자식 앞 sm Spinner. `axe-btn--loading` 도 추가."
        },
        {
          "name": "leftIcon",
          "type": "React.ReactNode",
          "description": "자식 앞 icon 슬롯(`axe-btn__icon` 래핑)."
        },
        {
          "name": "rightIcon",
          "type": "React.ReactNode",
          "description": "자식 뒤 icon 슬롯(`axe-btn__icon` 래핑)."
        },
        {
          "name": "type",
          "type": "\"button\" | \"submit\" | \"reset\"",
          "defaultValue": "\"button\"",
          "description": "네이티브 button type. 폼 제출은 명시적으로 submit 지정."
        },
        {
          "name": "...rest",
          "type": "React.ButtonHTMLAttributes<HTMLButtonElement>",
          "description": "onClick·aria-label·name 등 나머지는 루트 element 로 전파(asChild 면 자식 element 로). 단 disabled·type 은 rest 가 아니라 명시 처리되어(disabled=loading||disabled, asChild 는 aria-disabled 로만) 전달된다."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-btn",
          "purpose": "버튼 루트.",
          "stability": "stable"
        },
        {
          "className": "axe-btn--primary",
          "purpose": "primary 변형(CTA).",
          "stability": "stable"
        },
        {
          "className": "axe-btn--secondary",
          "purpose": "secondary 변형(기본).",
          "stability": "stable"
        },
        {
          "className": "axe-btn--ghost",
          "purpose": "ghost 변형(배경 없음).",
          "stability": "stable"
        },
        {
          "className": "axe-btn--danger",
          "purpose": "danger 변형(파괴적 행동).",
          "stability": "stable"
        },
        {
          "className": "axe-btn--sm",
          "purpose": "small 크기.",
          "stability": "stable"
        },
        {
          "className": "axe-btn--lg",
          "purpose": "large 크기.",
          "stability": "stable"
        },
        {
          "className": "axe-btn--loading",
          "purpose": "loading 상태 modifier.",
          "stability": "stable"
        },
        {
          "className": "axe-btn__icon",
          "purpose": "left/right icon 슬롯 래퍼.",
          "stability": "stable"
        },
        {
          "className": "axe-interactive",
          "purpose": "Button 이 아닌 임의 클릭 요소(내비 행·표 링크·카드)에 다는 공유 affordance 유틸리티 — hover/focus-visible/press 어휘만 준다. Button 자신은 .axe-btn 어휘를 쓰므로 겹쳐 달지 않는다.",
          "stability": "stable"
        },
        {
          "className": "axe-interactive--active",
          "purpose": ".axe-interactive 의 활성 상태(accent 텍스트 + accent-soft 배경). aria-current=\"page\" · aria-selected=\"true\" · data-active=\"true\" 와 동등.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Enter/Space 로 실행(네이티브 <button>)"
        ],
        "aria": "기본은 네이티브 <button> — role 불요. loading 시 disabled + aria-busy=\"true\". asChild 는 native disabled 를 걸 수 없어 loading/disabled 를 aria-disabled=\"true\" 로만 전달한다(키보드 활성화까지 막지는 않음 — 확실한 비활성이 필요하면 asChild 없이 쓸 것).",
        "notes": "아이콘 전용 버튼은 aria-label 을 직접 넘길 것(...rest 로 전파). :active scale(0.98) 누름 피드백은 native :disabled 에서만 빠진다 — asChild 로 렌더된 aria-disabled 버튼(비-loading)은 :disabled 가 매치되지 않아 여전히 눌림 피드백이 적용된다(확실한 비활성은 asChild 없이)."
      },
      "examples": [
        {
          "title": "변형 + 크기",
          "lang": "tsx",
          "code": "import { Button } from \"@axe/ui\";\n\nexport function Example() {\n  return (\n    <>\n      <Button variant=\"primary\" onClick={() => {}}>저장</Button>\n      <Button variant=\"ghost\" size=\"sm\">취소</Button>\n      <Button variant=\"danger\" loading={false}>삭제</Button>\n    </>\n  );\n}"
        },
        {
          "title": "asChild — 링크에 버튼 스타일",
          "lang": "tsx",
          "code": "import { Button } from \"@axe/ui\";\n\n<Button asChild variant=\"secondary\">\n  <a href=\"/docs\">문서 열기</a>\n</Button>"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<button class=\"axe-btn axe-btn--primary\" type=\"button\">저장</button>\n<button class=\"axe-btn axe-btn--secondary\" type=\"button\">\n  <span class=\"axe-btn__icon\">→</span>다음\n</button>"
        }
      ],
      "bestPractices": {
        "do": [
          "라벨은 동사로(‘저장’, ‘삭제’).",
          "파괴적 행동은 variant=\"danger\".",
          "제출 버튼은 type=\"submit\" 명시."
        ],
        "dont": [
          "한 화면에 primary 를 여러 개 두지 말 것.",
          "asChild 로 감쌀 때 loading/icon 슬롯에 의존하지 말 것(미렌더).",
          "아이콘만 있는 버튼에 aria-label 을 빠뜨리지 말 것."
        ]
      },
      "consumeCss": "프레임/변형은 CSS-only 로 재현 가능하나, loading Spinner 는 React <Spinner> 컴포넌트라 CSS-only 로는 스피너 마크업을 직접 넣어야 한다. asChild 병합도 React(Radix Slot) 전용.",
      "playground": {
        "componentKey": "button",
        "defaults": {
          "variant": "primary",
          "size": "md",
          "loading": false,
          "disabled": false,
          "children": "저장"
        },
        "controls": [
          {
            "prop": "disabled",
            "kind": "switch"
          }
        ]
      },
      "url": "https://design.axelabs.ai/components/button"
    },
    {
      "name": "Calendar",
      "slug": "calendar",
      "group": "input",
      "summary": "단일 날짜 선택 그리드(월 단위) — 외부 dep 0, 순수 날짜 산술 + role=\"grid\".",
      "description": "한 달을 네이티브 <button> 셀 그리드로 그려 날짜 하나(value: Date | null)를 고르는 controlled 컴포넌트. Radix·date 라이브러리 없이 순수 날짜 산술로 동작하고, role=\"grid\"/\"row\"/\"gridcell\" + 셀별 aria-selected 로 접근성을 표현한다(서버가 같은 markup 을 계산해 넣으면 maud/jinja 소비자도 동일 모양). weekStartsOn 으로 주 시작 요일을, minDate/maxDate/isDateDisabled 로 선택 범위를 제한한다. 경계 — 단일 날짜 선택만 한다: 기간(range) 선택·시각(시·분) 선택·input 옆 팝오버 DatePicker 합성은 범위 밖(이 컴포넌트를 내부에 합성해 별도 제공).",
      "keywords": [
        "calendar",
        "달력",
        "날짜",
        "date",
        "date picker",
        "날짜 선택",
        "month",
        "grid",
        "그리드"
      ],
      "props": [
        {
          "name": "value",
          "type": "Date | null",
          "required": true,
          "description": "선택된 날짜 또는 미선택(null) — controlled."
        },
        {
          "name": "onValueChange",
          "type": "(value: Date) => void",
          "required": true,
          "description": "날짜 셀 클릭 시 그 Date 로 호출."
        },
        {
          "name": "weekStartsOn",
          "type": "Weekday",
          "defaultValue": "0",
          "description": "주 시작 요일(0=일 … 6=토). 요일 헤더와 리드 빈칸 계산에 반영."
        },
        {
          "name": "minDate",
          "type": "Date",
          "description": "선택 가능한 최소 날짜(포함). 이전 날짜 셀은 disabled."
        },
        {
          "name": "maxDate",
          "type": "Date",
          "description": "선택 가능한 최대 날짜(포함). 이후 날짜 셀은 disabled."
        },
        {
          "name": "isDateDisabled",
          "type": "(date: Date) => boolean",
          "description": "개별 날짜 비활성 술어(min/max 위에 추가 적용). 예: 일요일 비활성."
        },
        {
          "name": "weekdayLabels",
          "type": "[string, string, string, string, string, string, string]",
          "defaultValue": "[\"일\",\"월\",\"화\",\"수\",\"목\",\"금\",\"토\"]",
          "description": "요일 라벨(일요일 기준 7개). weekStartsOn 만큼 회전되어 헤더에 표시."
        },
        {
          "name": "formatMonth",
          "type": "(year: number, month: number) => string",
          "defaultValue": "(y, m) => `${y}년 ${m + 1}월`",
          "description": "헤더 월 표제 포맷(month 는 0-based)."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "defaultValue": "false",
          "description": "전체 비활성 — 모든 셀·내비 버튼 disabled + --disabled 클래스."
        },
        {
          "name": "ariaLabel",
          "type": "string",
          "defaultValue": "\"달력\"",
          "description": "role=\"grid\" 요소의 aria-label."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\" | \"defaultValue\">",
          "description": "className 병합, 나머지는 루트 div 로 전파. ref 는 루트 div 로 forward."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-calendar",
          "purpose": "루트 div.",
          "stability": "stable"
        },
        {
          "className": "axe-calendar--disabled",
          "purpose": "전체 비활성 상태.",
          "stability": "stable"
        },
        {
          "className": "axe-calendar__header",
          "purpose": "이전/제목/다음 3열 헤더.",
          "stability": "stable"
        },
        {
          "className": "axe-calendar__title",
          "purpose": "월 표제(aria-live=\"polite\").",
          "stability": "stable"
        },
        {
          "className": "axe-calendar__nav",
          "purpose": "이전/다음 달 버튼.",
          "stability": "stable"
        },
        {
          "className": "axe-calendar__grid",
          "purpose": "role=\"grid\" 날짜 그리드 컨테이너.",
          "stability": "stable"
        },
        {
          "className": "axe-calendar__weekdays",
          "purpose": "요일 헤더 row(role=\"row\").",
          "stability": "stable"
        },
        {
          "className": "axe-calendar__weekday",
          "purpose": "요일 셀(role=\"columnheader\").",
          "stability": "stable"
        },
        {
          "className": "axe-calendar__week",
          "purpose": "한 주 row(role=\"row\", 7칸).",
          "stability": "stable"
        },
        {
          "className": "axe-calendar__day",
          "purpose": "날짜 셀 <button>.",
          "stability": "stable"
        },
        {
          "className": "axe-calendar__day--outside",
          "purpose": "이달 밖 빈칸(격자 정렬용, aria-disabled).",
          "stability": "stable"
        },
        {
          "className": "axe-calendar__day--selected",
          "purpose": "선택된 날짜(accent 채움).",
          "stability": "stable"
        },
        {
          "className": "axe-calendar__day--today",
          "purpose": "오늘 표시(테두리/링).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "←/→는 하루, ↑/↓는 한 주 단위로 roving focus를 이동한다.",
          "Home/End는 현재 주의 첫날/마지막 날로 이동한다.",
          "PageUp/PageDown은 같은 날짜를 기준으로 이전/다음 달로 이동한다.",
          "Enter/Space는 포커스된 날짜를 선택한다."
        ],
        "aria": "그리드는 role=\"grid\" + aria-label. 요일 헤더 줄 role=\"row\" 안에 각 요일 role=\"columnheader\"(aria-label). 각 주 role=\"row\", 셀 래퍼 role=\"gridcell\" + 선택 셀에 aria-selected. 날짜 <button>의 접근성 이름은 보이는 일자 숫자를 포함한 전체 연월일·요일이고, 오늘이면 aria-current=\"date\". 이전/다음 버튼은 aria-label \"이전 달\"/\"다음 달\", 월 표제는 aria-live=\"polite\" 로 이동 시 낭독. 이달 밖 빈칸은 aria-disabled=\"true\".",
        "notes": "날짜 버튼은 roving tabindex로 한 셀만 Tab 순서에 놓인다. 키보드 이동이 다른 달을 넘으면 월 표제와 그리드를 함께 갱신하고 새 날짜로 포커스를 복원한다. min/max/isDateDisabled/disabled로 막힌 날짜는 건너뛰며 native disabled도 유지한다. prefers-reduced-motion은 CSS에서 처리한다."
      },
      "examples": [
        {
          "title": "기본 (controlled)",
          "lang": "tsx",
          "code": "import { Calendar } from \"@axe/ui\";\nimport { useState } from \"react\";\n\nfunction DueDatePicker() {\n  const [due, setDue] = useState<Date | null>(null);\n  return <Calendar value={due} onValueChange={setDue} />;\n}"
        },
        {
          "title": "월 시작·범위 제한",
          "lang": "tsx",
          "code": "import { Calendar } from \"@axe/ui\";\n\n<Calendar\n  value={due}\n  onValueChange={setDue}\n  weekStartsOn={1}\n  minDate={new Date(2026, 0, 1)}\n  maxDate={new Date(2026, 11, 31)}\n  isDateDisabled={(d) => d.getDay() === 0}\n/>"
        },
        {
          "title": "CSS-only 정적 스냅샷 (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-calendar\">\n  <div class=\"axe-calendar__header\">\n    <button type=\"button\" class=\"axe-calendar__nav\" aria-label=\"이전 달\">‹</button>\n    <div class=\"axe-calendar__title\" aria-live=\"polite\">2026년 5월</div>\n    <button type=\"button\" class=\"axe-calendar__nav\" aria-label=\"다음 달\">›</button>\n  </div>\n  <div class=\"axe-calendar__grid\" role=\"grid\" aria-label=\"달력\">\n    <div class=\"axe-calendar__weekdays\" role=\"row\">\n      <div class=\"axe-calendar__weekday\" role=\"columnheader\" aria-label=\"일\">일</div>\n      <!-- 월~토 columnheader 반복 -->\n    </div>\n    <div class=\"axe-calendar__week\" role=\"row\">\n      <div class=\"axe-calendar__day axe-calendar__day--outside\" role=\"gridcell\" aria-disabled=\"true\"></div>\n      <div role=\"gridcell\" aria-selected=\"true\">\n          <button type=\"button\" class=\"axe-calendar__day axe-calendar__day--selected\" aria-label=\"14일, 2026년 7월 화요일\" tabindex=\"0\">14</button>\n      </div>\n      <div role=\"gridcell\" aria-selected=\"false\">\n          <button type=\"button\" class=\"axe-calendar__day axe-calendar__day--today\" aria-current=\"date\" tabindex=\"-1\">15</button>\n      </div>\n      <!-- 나머지 날짜 gridcell 반복 -->\n    </div>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "value 를 부모 state 로 소유(controlled) — 미선택은 null.",
          "선택 가능 구간이 있으면 minDate/maxDate 로 명시(빠진 날짜는 native disabled).",
          "다른 로케일/주 시작이 필요하면 weekdayLabels·weekStartsOn·formatMonth 로 오버라이드."
        ],
        "dont": [
          "기간(range)·시각 선택을 기대하지 말 것 — 단일 날짜 전용, 상위 DatePicker 로 합성.",
          "value 에 시·분이 담긴 Date 를 넣어도 동치 비교는 날짜(자정)만 본다 — 시각은 무시됨."
        ]
      },
      "consumeCss": "스타일(.axe-calendar*)은 순수 CSS 고 aria 구조도 서버가 그대로 계산 가능하다 — 서버(maud/jinja)가 한 달치 셀·요일 헤더·aria-selected/aria-current 를 산출해 같은 markup 을 그리면 CSS-only 로 동일 재현된다. 단 달 이동(이전/다음)과 선택 반영은 JS 몫이라 정적 렌더는 한 달의 스냅샷이다.",
      "url": "https://design.axelabs.ai/components/calendar"
    },
    {
      "name": "Callout",
      "slug": "callout",
      "group": "feedback",
      "summary": "본문 흐름 안에 끼우는 시맨틱 강조 박스(info/success/warning/danger/note).",
      "description": "아이콘 + 제목 + 본문의 3파트 알림 블록. variant 가 시맨틱 색과 기본 아이콘(ⓘ/✓/⚠/✕/•)을 결정하고, accent 가 색 강조 방식을 고른다 — icon(기본, 아이콘+전체 트림) · left-bar(좌측 4px 바만, 더 미니멀) · both(둘 다) · none(색 트림 제거). 루트에 role=\"note\" 가 고정되어 보조 설명 영역임을 알린다.",
      "keywords": [
        "callout",
        "콜아웃",
        "admonition",
        "note",
        "alert",
        "info",
        "warning",
        "안내",
        "경고"
      ],
      "props": [
        {
          "name": "variant",
          "type": "\"info\" | \"success\" | \"warning\" | \"danger\" | \"note\"",
          "defaultValue": "\"info\"",
          "description": "시맨틱 색과 기본 아이콘을 결정. note 는 중립 회색."
        },
        {
          "name": "accent",
          "type": "\"icon\" | \"left-bar\" | \"both\" | \"none\"",
          "defaultValue": "\"icon\"",
          "description": "색 강조 방식. icon=아이콘+전체 트림(기본) · left-bar=좌측 4px 색 바만 · both=바+아이콘 · none=색 트림 제거(배경/텍스트만)."
        },
        {
          "name": "title",
          "type": "React.ReactNode",
          "description": "굵은 제목 줄(`.axe-callout__title`). 있을 때만 렌더. HTMLAttributes 의 title 은 이 prop 으로 대체됨."
        },
        {
          "name": "icon",
          "type": "React.ReactNode",
          "description": "좌측 아이콘 override. 미지정 시 variant 기본 아이콘 사용. accent 가 icon 또는 both 일 때만 렌더된다."
        },
        {
          "name": "children",
          "type": "React.ReactNode",
          "description": "콜아웃 본문 — 제목 아래 `.axe-callout__content` 안에 들어간다."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLDivElement>, \"title\">",
          "description": "className 병합, 나머지는 루트 div 로 전파. rest 가 role 보다 뒤에 전개되므로 role=\"alert\" 등으로 덮어쓸 수 있다."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-callout",
          "purpose": "루트 div — flex row·border·radius-md·sm 텍스트.",
          "stability": "stable"
        },
        {
          "className": "axe-callout__icon",
          "purpose": "좌측 아이콘 박스(flex-shrink 0). variant 색 상속.",
          "stability": "stable"
        },
        {
          "className": "axe-callout__content",
          "purpose": "제목+본문 컨테이너(flex 1, min-width 0).",
          "stability": "stable"
        },
        {
          "className": "axe-callout__title",
          "purpose": "굵은 제목 줄.",
          "stability": "stable"
        },
        {
          "className": "axe-callout--info",
          "purpose": "info 변형 — info-soft 배경·info 테두리/아이콘.",
          "stability": "stable"
        },
        {
          "className": "axe-callout--success",
          "purpose": "success 변형 — success-soft 트림.",
          "stability": "stable"
        },
        {
          "className": "axe-callout--warning",
          "purpose": "warning 변형 — warning-soft 트림.",
          "stability": "stable"
        },
        {
          "className": "axe-callout--danger",
          "purpose": "danger 변형 — danger-soft 트림.",
          "stability": "stable"
        },
        {
          "className": "axe-callout--note",
          "purpose": "note 변형 — 중립 subtle 배경·secondary 텍스트.",
          "stability": "stable"
        },
        {
          "className": "axe-callout--accent-icon",
          "purpose": "accent=icon(기본) — 아이콘+전체 트림. 추가 규칙 없는 마커.",
          "stability": "stable"
        },
        {
          "className": "axe-callout--accent-left-bar",
          "purpose": "accent=left-bar — 좌측 테두리만 4px 로 강조.",
          "stability": "stable"
        },
        {
          "className": "axe-callout--accent-both",
          "purpose": "accent=both — 좌측 4px 바 + 아이콘.",
          "stability": "stable"
        },
        {
          "className": "axe-callout--accent-none",
          "purpose": "accent=none — 색 트림을 border-subtle 로 눌러 배경/텍스트만 남김.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "루트에 role=\"note\" 고정 — 부가 설명 영역임을 보조기술에 알린다. 아이콘은 `aria-hidden`(의미는 텍스트로).",
        "notes": "긴급 통지가 필요하면 rest 로 role=\"alert\"(또는 aria-live)를 넘겨 덮어쓴다 — warning/danger variant 여도 기본 role 은 alert 가 아니다."
      },
      "examples": [
        {
          "title": "기본(warning)",
          "lang": "tsx",
          "code": "import { Callout } from \"@axe/ui\";\n\n<Callout variant=\"warning\" title=\"마감 임박\">\n  IC 자료 제출이 D-1 남았습니다.\n</Callout>"
        },
        {
          "title": "left-bar accent (미니멀 정보용)",
          "lang": "tsx",
          "code": "import { Callout } from \"@axe/ui\";\n\n<Callout variant=\"info\" accent=\"left-bar\">\n  이 값은 공단 고지서 기준으로 검증되었습니다.\n</Callout>"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-callout axe-callout--warning axe-callout--accent-icon\" role=\"note\">\n  <div class=\"axe-callout__icon\" aria-hidden>⚠</div>\n  <div class=\"axe-callout__content\">\n    <div class=\"axe-callout__title\">마감 임박</div>\n    IC 자료 제출이 D-1 남았습니다.\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "variant 는 의미에 맞게 — 성공은 success, 되돌릴 수 없는 위험만 danger.",
          "본문 흐름을 방해하지 않는 부가 안내는 note 또는 accent=\"left-bar\" 로 톤을 낮춘다."
        ],
        "dont": [
          "한 화면에 danger 콜아웃을 여러 개 쌓아 경고 피로를 만들지 말 것.",
          "실시간 통지가 목적이 아니면 role=\"alert\" 로 덮어쓰지 말 것(불필요한 SR 인터럽트)."
        ]
      },
      "consumeCss": "상태 없는 순수 CSS — `.axe-callout` + variant + accent 클래스 조합으로 서버 렌더(maud/jinja)에서 동일 재현. 아이콘 문자는 직접 넣는다.",
      "playground": {
        "componentKey": "callout",
        "defaults": {
          "variant": "info",
          "accent": "icon",
          "title": "주의",
          "children": "배포는 axe ship 으로 release-gate 를 거칩니다."
        },
        "controls": [
          {
            "prop": "title",
            "kind": "text"
          }
        ]
      },
      "url": "https://design.axelabs.ai/components/callout"
    },
    {
      "name": "Card",
      "slug": "card",
      "group": "container",
      "summary": "콘텐츠를 담는 elevated 표면 컨테이너.",
      "description": "border-subtle 테두리 · radius-lg · shadow-xs 로 배경에서 한 겹 떠 보이는 그릇. title/body 슬롯 prop 으로 간단한 카드를 바로 구성하거나, children 으로 임의 콘텐츠를 담는다. interactive 를 주면 cursor pointer 와 hover lift(translateY + shadow-md)가 붙어 클릭 가능한 카드처럼 보인다.",
      "keywords": [
        "card",
        "카드",
        "panel",
        "surface",
        "container",
        "패널"
      ],
      "props": [
        {
          "name": "interactive",
          "type": "boolean",
          "description": "hover lift(-2px translateY + shadow-md)와 cursor pointer 를 부여하는 시각 변형. 실제 클릭 시맨틱은 주지 않는다(a11y 참고)."
        },
        {
          "name": "title",
          "type": "React.ReactNode",
          "description": "제목 슬롯. 있을 때만 `.axe-card__title`(display 폰트) 렌더. HTMLAttributes 의 title 속성은 이 prop 으로 대체됨."
        },
        {
          "name": "body",
          "type": "React.ReactNode",
          "description": "본문 슬롯. 있을 때만 `.axe-card__body`(secondary 텍스트) 렌더."
        },
        {
          "name": "children",
          "type": "React.ReactNode",
          "description": "title/body 슬롯 아래에 이어 붙는 자유 콘텐츠."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLDivElement>, \"title\">",
          "description": "className 은 병합, 나머지는 루트 div 로 전파(onClick 등)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-card",
          "purpose": "카드 루트 div — elevated 배경·subtle 테두리·radius-lg·padding·shadow-xs.",
          "stability": "stable"
        },
        {
          "className": "axe-card--interactive",
          "purpose": "클릭 가능 변형 — cursor pointer + hover 시 lift(테두리 강조·translateY·shadow-md).",
          "stability": "stable"
        },
        {
          "className": "axe-card__title",
          "purpose": "제목 슬롯 — display 폰트·lg·semibold.",
          "stability": "stable"
        },
        {
          "className": "axe-card__body",
          "purpose": "본문 슬롯 — secondary 색·sm.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "시맨틱 없는 컨테이너 <div> — role 을 자동 부여하지 않는다.",
        "notes": "interactive 는 순수 시각 힌트다 — tabIndex·role·키보드 핸들러를 붙이지 않는다. 카드 전체를 실제로 조작 가능하게 하려면 소비자가 role=\"button\"+tabIndex+onKeyDown 을 rest 로 넘기거나(권장) 내부에 <a>/<button> 을 두어야 한다."
      },
      "examples": [
        {
          "title": "슬롯 prop 로 구성",
          "lang": "tsx",
          "code": "import { Card } from \"@axe/ui\";\n\n<Card\n  interactive\n  title=\"딜 파이프라인\"\n  body=\"이번 분기 신규 소싱 12건, IC 상정 3건.\"\n  onClick={openPipeline}\n/>"
        },
        {
          "title": "children 조합",
          "lang": "tsx",
          "code": "import { Card, MetricCard } from \"@axe/ui\";\n\n<Card title=\"포트폴리오 요약\">\n  <MetricCard label=\"TVPI\" value=\"1.8x\" />\n</Card>"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-card axe-card--interactive\">\n  <div class=\"axe-card__title\">딜 파이프라인</div>\n  <div class=\"axe-card__body\">이번 분기 신규 소싱 12건.</div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "단순 제목+본문이면 title/body prop 을, 복합 레이아웃이면 children 을 쓴다.",
          "interactive 카드는 role/tabIndex/키보드 핸들러를 함께 부여해 접근성을 맞춘다."
        ],
        "dont": [
          "interactive 만으로 클릭 가능한 UI 라고 가정하지 말 것(키보드·포커스 없음).",
          "중첩 Card 로 그림자를 겹쳐 시각적 depth 를 과하게 쌓지 말 것."
        ]
      },
      "consumeCss": "순수 CSS 클래스만으로 재현 가능(상태 없음). maud/jinja 등 비-React 서버 렌더에서 `.axe-card` 마크업을 그대로 써도 동일 결과.",
      "url": "https://design.axelabs.ai/components/card"
    },
    {
      "name": "Chart",
      "slug": "chart",
      "group": "data",
      "summary": "recharts 를 axe 토큰으로 감싼 얇은 wrapper — LineChart · BarChart · AreaChart 3종이 동일한 data/xKey/series API 를 공유.",
      "description": "recharts 는 강력하지만 컬러·축·grid 기본값이 우리 토큰과 충돌한다. 본 wrapper 는 series 색을 semantic 팔레트(accent → success → warning → danger → info)로 순환 배정하고, 축·grid·tooltip 을 CSS 변수로 묶어 light/dark 를 자동 처리한다. 세 컴포넌트 모두 data(레코드 배열) + xKey(x축 key) + series(그릴 계열) 형태로 호출하며, height 미지정 시 240px 다. prefers-reduced-motion 이 걸리면 애니메이션을 끄고, 데이터가 비면 EmptyState 로 대체한다. 변형: BarChart 에 stacked, AreaChart 에 gradient, series 항목에 dashed(예측 계열 점선). recharts 가 raw ~395KB 라 대시보드 첫 페인트 번들에서는 LazyCharts 변형을 권장한다.",
      "keywords": [
        "chart",
        "차트",
        "그래프",
        "graph",
        "line chart",
        "bar chart",
        "area chart",
        "recharts",
        "라인차트",
        "막대차트",
        "영역차트",
        "시계열",
        "series",
        "dashboard",
        "data viz"
      ],
      "props": [
        {
          "name": "data",
          "type": "Datum[]",
          "required": true,
          "description": "레코드 배열. 각 원소는 x축 라벨(data[i][xKey])과 series 별 y값을 담는다. 빈 배열이면 EmptyState(\"데이터 없음\")로 대체."
        },
        {
          "name": "xKey",
          "type": "string",
          "required": true,
          "description": "x축으로 쓸 key. data[i][xKey] 값이 카테고리 축 라벨."
        },
        {
          "name": "series",
          "type": "ChartSeries[]",
          "required": true,
          "description": "그릴 계열 목록. 각 ChartSeries = { key: string(=data 의 y값 key), label?: string(범례/tooltip 표시명, 미지정 시 key), color?: string(미지정 시 semantic 팔레트 순환), dashed?: boolean(Line/Area 점선 — 예측 계열 등) }."
        },
        {
          "name": "height",
          "type": "number",
          "defaultValue": "240",
          "description": "차트 컨테이너 높이(px). ResponsiveContainer 가 width 100% 를 채우므로 높이는 명시 권장."
        },
        {
          "name": "showLegend",
          "type": "boolean",
          "description": "범례 표시. 미지정 시 series 가 2개 이상이면 자동 표시(series.length > 1)."
        },
        {
          "name": "showGrid",
          "type": "boolean",
          "defaultValue": "true",
          "description": "CartesianGrid(점선 3 3) 표시."
        },
        {
          "name": "onPointClick",
          "type": "(point: Datum) => void",
          "description": "point/bar 클릭 콜백. 클릭된 activeTooltipIndex 의 data 원소를 그대로 넘긴다. 마우스 전용 — 키보드로는 도달 불가(a11y 참고)."
        },
        {
          "name": "animate",
          "type": "boolean",
          "description": "recharts 진입 애니메이션. 미지정 시 prefers-reduced-motion: reduce 면 자동 false, 아니면 true."
        },
        {
          "name": "ariaLabel",
          "type": "string",
          "description": "role=\"img\" 컨테이너의 aria-label — SR 사용자가 차트가 무엇인지 파악. 미지정 시 `\"{n}-series chart\"` 라는 무의미한 fallback 이 붙으므로 사실상 필수."
        },
        {
          "name": "ariaDescribedBy",
          "type": "string",
          "description": "확장 설명 요소의 id(보통 화면에 숨긴 데이터 표 대체). 실제 수치를 SR·키보드 사용자에게 전달하는 채널."
        },
        {
          "name": "className",
          "type": "string",
          "description": "루트 .axe-chart div 에 병합."
        },
        {
          "name": "stacked",
          "type": "boolean",
          "defaultValue": "false",
          "description": "[BarChart 전용] bar 를 누적(stack). false 면 group 배치 + 상단 모서리 라운드(3px)."
        },
        {
          "name": "gradient",
          "type": "boolean",
          "defaultValue": "true",
          "description": "[AreaChart 전용] 영역 fill 에 linearGradient(0%→35% / 100%→2%) 적용. false 면 단색 fill(opacity 0.25)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-chart",
          "purpose": "루트 컨테이너 div — width 100% + height 지정자, text-primary. recharts SVG 를 감싼다.",
          "stability": "stable"
        },
        {
          "className": "axe-chart__tooltip",
          "purpose": "커스텀 tooltip 컨테이너(bg-elevated · border-subtle · shadow). recharts 기본 tooltip 대체.",
          "stability": "stable"
        },
        {
          "className": "axe-chart__tooltip-label",
          "purpose": "tooltip 상단 x축 라벨(하단 border 구분선).",
          "stability": "stable"
        },
        {
          "className": "axe-chart__tooltip-list",
          "purpose": "tooltip 내 계열 행 목록(flex column).",
          "stability": "stable"
        },
        {
          "className": "axe-chart__tooltip-row",
          "purpose": "계열 1개 행 — swatch · name · value 3열 grid.",
          "stability": "stable"
        },
        {
          "className": "axe-chart__tooltip-swatch",
          "purpose": "계열 색 표시 정사각 칩(10px, aria-hidden).",
          "stability": "stable"
        },
        {
          "className": "axe-chart__tooltip-name",
          "purpose": "계열 표시명(text-secondary).",
          "stability": "stable"
        },
        {
          "className": "axe-chart__tooltip-value",
          "purpose": "계열 값(text-primary, tabular-nums, semibold).",
          "stability": "stable"
        },
        {
          "className": "axe-chart--empty",
          "purpose": "data 가 비었을 때 루트에 추가되어 EmptyState 를 감싼다. 컴포넌트가 emit 하지만 현재 전용 CSS 규칙은 없음(레이아웃은 .axe-chart 상속).",
          "stability": "internal"
        }
      ],
      "a11y": {
        "aria": "컨테이너 div 에 role=\"img\" + aria-label(ariaLabel ?? `\"{n}-series chart\"`) 를 부여해 차트 전체를 하나의 이미지로 노출한다. ariaDescribedBy 로 확장 설명 요소를 연결할 수 있다. tooltip 색 swatch 는 aria-hidden.",
        "notes": "recharts SVG 는 기본적으로 포커스·키보드 탐색이 불가하다 — 개별 point/bar 는 키보드로 도달할 수 없고 onPointClick 은 마우스 전용이다. 따라서 실제 수치가 중요하면 ariaDescribedBy 로 화면에 숨긴 데이터 표(.axe-sr-only)를 반드시 함께 제공해 SR·키보드 사용자가 값을 읽을 수 있게 한다. 색만으로 계열을 구분하지 않도록 label 을 항상 지정하고, 예측/추정 계열은 dashed 로 형태 단서를 더한다."
      },
      "examples": [
        {
          "title": "실적 대비 예측 (LineChart, dashed 계열)",
          "lang": "tsx",
          "code": "import { LineChart } from \"@axe/ui\";\n\nconst data = [\n  { month: \"1월\", actual: 120, forecast: 120 },\n  { month: \"2월\", actual: 138, forecast: 135 },\n  { month: \"3월\", actual: 152, forecast: 150 },\n];\n\n<LineChart\n  data={data}\n  xKey=\"month\"\n  series={[\n    { key: \"actual\", label: \"실적\" },\n    { key: \"forecast\", label: \"예측\", dashed: true },\n  ]}\n  height={280}\n  ariaLabel=\"월별 실적 대비 예측 추이\"\n/>"
        },
        {
          "title": "채널 구성 (BarChart stacked)",
          "lang": "tsx",
          "code": "import { BarChart } from \"@axe/ui\";\n\n<BarChart\n  data={[\n    { quarter: \"Q1\", direct: 40, channel: 24 },\n    { quarter: \"Q2\", direct: 52, channel: 30 },\n  ]}\n  xKey=\"quarter\"\n  series={[\n    { key: \"direct\", label: \"직접\" },\n    { key: \"channel\", label: \"채널\" },\n  ]}\n  stacked\n  ariaLabel=\"분기별 채널 매출 구성\"\n/>"
        },
        {
          "title": "AUM 추이 + SR 표 대체 (AreaChart, ariaDescribedBy)",
          "lang": "tsx",
          "code": "import { AreaChart } from \"@axe/ui\";\n\n<>\n  <AreaChart\n    data={aum}\n    xKey=\"date\"\n    series={[{ key: \"value\", label: \"AUM\", color: \"var(--accent)\" }]}\n    ariaLabel=\"일별 운용자산(AUM) 추이\"\n    ariaDescribedBy=\"aum-table\"\n    onPointClick={(p) => console.log(p)}\n  />\n  {/* 색 그래프에 도달 못 하는 SR·키보드 사용자를 위한 동일 데이터 표 */}\n  <table id=\"aum-table\" className=\"axe-sr-only\">\n    <caption>일별 운용자산</caption>\n    <tbody>\n      {aum.map((r) => (\n        <tr key={r.date}>\n          <th scope=\"row\">{r.date}</th>\n          <td>{r.value}</td>\n        </tr>\n      ))}\n    </tbody>\n  </table>\n</>"
        }
      ],
      "bestPractices": {
        "do": [
          "ariaLabel 을 항상 지정한다(미지정 fallback 은 무의미).",
          "실제 수치가 중요하면 ariaDescribedBy 로 .axe-sr-only 데이터 표를 함께 제공한다.",
          "각 series.label 을 지정해 색뿐 아니라 이름으로도 계열을 구분한다.",
          "예측·추정 계열은 dashed 로 실선/점선을 섞어 형태 단서를 준다.",
          "대시보드 첫 화면 차트는 LazyCharts 변형으로 recharts 청크를 지연 로드한다."
        ],
        "dont": [
          "onPointClick 에 필수 정보 접근을 의존하지 말 것 — 마우스 전용이라 키보드로 도달 불가.",
          "6개 이상 계열을 색 지정 없이 두지 말 것 — semantic 팔레트가 5색에서 순환해 색이 겹친다.",
          "색만으로 계열 의미를 구분하지 말 것(color-blind·흑백 인쇄).",
          "recharts 무게(~395KB)를 first-paint 번들에 싣지 말 것."
        ]
      },
      "consumeCss": "본 컴포넌트는 recharts(React) 위에 서 있어 순수 CSS-only 로는 소비할 수 없다 — HTML 마크업만으로 재현 불가. .axe-chart__tooltip* 클래스는 컴포넌트가 렌더하는 tooltip 을 테마가 재스타일하기 위한 계약이다. components.css가 data-display.css와 feedback.css를 모두 import하므로 표준 진입점 하나면 스타일은 완결된다.",
      "url": "https://design.axelabs.ai/components/chart"
    },
    {
      "name": "ChatInput",
      "slug": "chat-input",
      "group": "input",
      "summary": "Enter 전송·자동 높이 조절·툴바 슬롯을 갖춘 AI 채팅 입력창.",
      "description": "uncontrolled <textarea> 를 감싼 forwardRef 컴포넌트로, 값은 내부 ref 로만 읽는다(defaultValue 로 초기화, onSubmit 은 trim 된 문자열을 넘긴 뒤 자동으로 비운다). Enter 로 전송하고 Shift+Enter 로 줄바꿈하며, 한국어 IME 조합 중(isComposing)에는 전송을 막는다. 입력에 따라 높이가 최대 240px 까지 자동으로 늘어난다(autosize). ref 는 DOM 이 아니라 ChatInputHandle({ focus(), clear() })을 노출해 외부에서 포커스·비우기를 명령형으로 제어한다. toolbarLeft/toolbarRight 슬롯으로 첨부·모드 선택·전송 버튼을 배치하고, 소비자가 넘긴 onKeyDown 이 preventDefault 하면 내부 Enter-to-send 를 건너뛰는 escape hatch 를 둔다.",
      "keywords": [
        "chat input",
        "채팅 입력",
        "textarea",
        "prompt",
        "프롬프트",
        "enter to send",
        "autosize",
        "ai"
      ],
      "props": [
        {
          "name": "placeholder",
          "type": "string",
          "defaultValue": "\"메시지를 입력하세요…\"",
          "description": "textarea placeholder — i18n 친화."
        },
        {
          "name": "onSubmit",
          "type": "(value: string) => void",
          "description": "전송 트리거 — Enter(Shift+Enter 는 줄바꿈)와 외부 버튼 양쪽에서 호출. 빈 값(trim 후)은 전송하지 않으며 전송 후 입력이 비워진다."
        },
        {
          "name": "toolbarLeft",
          "type": "React.ReactNode",
          "description": "툴바 좌측 슬롯 — 첨부·모드 선택 등."
        },
        {
          "name": "toolbarRight",
          "type": "React.ReactNode",
          "description": "툴바 우측 슬롯 — 전송 버튼 등(없으면 hint 만 표시)."
        },
        {
          "name": "hint",
          "type": "React.ReactNode",
          "defaultValue": "\"↵ 보내기 · ⇧↵ 줄바꿈\"",
          "description": "툴바 우측 힌트 텍스트. false/null 이면 미표시."
        },
        {
          "name": "ref",
          "type": "React.Ref<ChatInputHandle>",
          "description": "DOM 이 아니라 명령형 핸들을 노출 — { focus(): void; clear(): void }."
        },
        {
          "name": "...rest",
          "type": "Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, \"onSubmit\">",
          "description": "defaultValue·aria-label·onKeyDown·onInput 등은 내부 textarea 로 전파(내부 핸들러와 체이닝). className 은 루트 .axe-chat-input 과 병합."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-chat-input",
          "purpose": "입력창 루트(세로 flex, 토큰 테두리/배경). :focus-within 에서 accent 테두리 + var(--ring).",
          "stability": "stable"
        },
        {
          "className": "axe-chat-input__textarea",
          "purpose": "실제 <textarea> — 테두리 0·투명 배경·resize:none, autosize 대상.",
          "stability": "stable"
        },
        {
          "className": "axe-chat-input__toolbar",
          "purpose": "하단 툴바 행(space-between 으로 좌/우 슬롯 배치).",
          "stability": "stable"
        },
        {
          "className": "axe-chat-input__hint",
          "purpose": "우측 힌트 텍스트(mono·tertiary·xs).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Enter 로 전송(값이 비어있으면 무시)",
          "Shift+Enter 로 줄바꿈",
          "IME 조합 중 Enter 는 전송하지 않고 한글 확정에 사용"
        ],
        "aria": "textarea 에 aria-label 을 넘기지 않으면 \"메시지 입력\" 으로 기본화. 그 외 role 은 네이티브 <textarea> 를 그대로 사용.",
        "notes": "소비자 onKeyDown 이 먼저 호출되고, 그것이 preventDefault 하면 내부 Enter-to-send 를 건너뛴다(커스텀 키 처리용 escape hatch). onInput 은 항상 autosize 를 수행한 뒤 소비자 onInput 도 호출한다."
      },
      "examples": [
        {
          "title": "기본 — 전송 버튼 슬롯",
          "lang": "tsx",
          "code": "import { ChatInput, Button } from \"@axe/ui\";\nimport { useRef } from \"react\";\nimport type { ChatInputHandle } from \"@axe/ui\";\n\nfunction Composer({ onSend }: { onSend: (text: string) => void }) {\n  const ref = useRef<ChatInputHandle>(null);\n  return (\n    <ChatInput\n      ref={ref}\n      onSubmit={onSend}\n      toolbarRight={\n        <Button variant=\"primary\" onClick={() => ref.current?.focus()}>\n          보내기\n        </Button>\n      }\n    />\n  );\n}"
        },
        {
          "title": "CSS-only 껍데기 (비-React, 동작 없음)",
          "lang": "html",
          "code": "<div class=\"axe-chat-input\">\n  <textarea class=\"axe-chat-input__textarea\" rows=\"1\"\n            aria-label=\"메시지 입력\" placeholder=\"메시지를 입력하세요…\"></textarea>\n  <div class=\"axe-chat-input__toolbar\">\n    <div><!-- toolbarLeft --></div>\n    <div><span class=\"axe-chat-input__hint\">↵ 보내기 · ⇧↵ 줄바꿈</span></div>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "전송 버튼은 toolbarRight 에 두고, 클릭 시 소비자 쪽에서도 값을 얻고 싶으면 ref 로 관리할 것.",
          "커스텀 키 처리가 필요하면 onKeyDown 에서 preventDefault 로 내부 Enter-to-send 를 끌 것."
        ],
        "dont": [
          "controlled value/onChange 를 기대하지 말 것 — uncontrolled 이며 값은 ref 로 읽는다.",
          "전송 후 수동으로 비우려 하지 말 것 — onSubmit 성공 시 자동으로 비워진다."
        ]
      },
      "consumeCss": "시각 껍데기(테두리·툴바·힌트·focus-within 링)는 .axe-chat-input__* 마크업만으로 재현되지만, Enter-to-send·autosize·IME 가드·clear() 는 모두 JS 동작이라 CSS-only 로는 재현되지 않는다.",
      "url": "https://design.axelabs.ai/components/chat-input"
    },
    {
      "name": "Checkbox",
      "slug": "checkbox",
      "group": "input",
      "summary": "Radix 기반 tri-state 체크박스 (checked / unchecked / indeterminate).",
      "description": "@radix-ui/react-checkbox 의 Root 를 감싼 얇은 래퍼로, box 와 check/dash 아이콘 CSS 만 담당하고 상태·키보드·포커스는 Radix 가 처리한다. checked 는 boolean 또는 \"indeterminate\" 를 받아 부분선택(전체선택 헤더 등)을 표현한다. Indicator 는 forceMount 로 항상 렌더되고 표시할 아이콘은 data-state 에 따라 CSS 로 분기된다. 커스텀 prop 은 없고 Radix Checkbox.Root 의 props 를 그대로 노출한다.",
      "keywords": [
        "checkbox",
        "체크박스",
        "tri-state",
        "indeterminate",
        "부분선택",
        "form",
        "폼"
      ],
      "props": [
        {
          "name": "checked",
          "type": "boolean | \"indeterminate\"",
          "description": "controlled 상태. \"indeterminate\" = 부분선택 (aria-checked=\"mixed\")."
        },
        {
          "name": "defaultChecked",
          "type": "boolean | \"indeterminate\"",
          "description": "uncontrolled 초기값."
        },
        {
          "name": "onCheckedChange",
          "type": "(checked: boolean | \"indeterminate\") => void",
          "description": "상태 변경 콜백."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "description": "비활성 — data-disabled → opacity 0.5, 클릭 불가."
        },
        {
          "name": "required",
          "type": "boolean",
          "description": "폼 제출 시 필수."
        },
        {
          "name": "name",
          "type": "string",
          "description": "폼 필드명 — 지정 시 Radix 가 제출용 hidden input 을 렌더."
        },
        {
          "name": "value",
          "type": "string",
          "defaultValue": "\"on\"",
          "description": "체크 시 폼에 전송되는 값."
        },
        {
          "name": "...rest",
          "type": "React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>",
          "description": "asChild 등 나머지 Radix Checkbox.Root props 전파 (className 은 axe-checkbox 와 병합)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-checkbox",
          "purpose": "체크박스 루트 (18px box). data-state=checked/indeterminate 시 accent 배경.",
          "stability": "stable"
        },
        {
          "className": "axe-checkbox__indicator",
          "purpose": "아이콘 컨테이너 (Radix Indicator, forceMount).",
          "stability": "internal"
        },
        {
          "className": "axe-checkbox__icon",
          "purpose": "check/dash SVG 공통 (기본 display:none).",
          "stability": "internal"
        },
        {
          "className": "axe-checkbox__icon--check",
          "purpose": "체크 아이콘 — data-state=checked 에서만 표시.",
          "stability": "internal"
        },
        {
          "className": "axe-checkbox__icon--dash",
          "purpose": "대시(부분선택) 아이콘 — data-state=indeterminate 에서만 표시.",
          "stability": "internal"
        }
      ],
      "a11y": {
        "keyboard": [
          "Space 로 토글",
          "Tab 으로 포커스"
        ],
        "aria": "Radix 가 role=\"checkbox\" + aria-checked(true/false/\"mixed\") 를 부여. indeterminate → aria-checked=\"mixed\". disabled → aria-disabled + data-disabled.",
        "notes": "box 자체엔 텍스트가 없으므로 <Label htmlFor> 또는 aria-label 로 이름을 제공. name 지정 시 Radix 가 폼 제출용 hidden input 을 렌더. 포커스 링 = :focus-visible box-shadow var(--ring)."
      },
      "examples": [
        {
          "title": "controlled",
          "lang": "tsx",
          "code": "import { Checkbox, Label } from \"@axe/ui\";\n\nconst [agreed, setAgreed] = useState(false);\n\n<div style={{ display: \"flex\", gap: 8, alignItems: \"center\" }}>\n  <Checkbox\n    id=\"terms\"\n    checked={agreed}\n    onCheckedChange={(v) => setAgreed(v === true)}\n  />\n  <Label htmlFor=\"terms\">약관에 동의합니다</Label>\n</div>"
        },
        {
          "title": "indeterminate (전체 선택)",
          "lang": "tsx",
          "code": "import { Checkbox } from \"@axe/ui\";\n\n// all ? true : some ? \"indeterminate\" : false\n<Checkbox\n  checked={selectAllState}\n  onCheckedChange={toggleAll}\n  aria-label=\"전체 선택\"\n/>"
        }
      ],
      "bestPractices": {
        "do": [
          "항상 <Label htmlFor> 또는 aria-label 로 이름 제공.",
          "부분선택 부모는 checked=\"indeterminate\" 로 표현."
        ],
        "dont": [
          "indeterminate 를 disabled 로 흉내내지 말 것.",
          "box 옆 텍스트를 라벨 연결 없이 두지 말 것."
        ]
      },
      "consumeCss": "CSS-only 로는 tri-state 를 완전 재현하기 어려움 — 아이콘 표시가 Radix 가 세팅하는 data-state 에 의존하므로 정적 마크업에선 data-state=\"checked\" 등을 직접 지정해야 함. React 사용 권장.",
      "playground": {
        "componentKey": "checkbox",
        "defaults": {
          "checked": true,
          "disabled": false
        },
        "controls": [
          {
            "prop": "checked",
            "kind": "switch"
          }
        ]
      },
      "url": "https://design.axelabs.ai/components/checkbox"
    },
    {
      "name": "Code",
      "slug": "code",
      "group": "content",
      "summary": "인라인 코드 토큰 <code>.",
      "description": "본문 사이에 코드/식별자를 표기하는 인라인 <code> 이다. 블록 + copy 버튼을 가진 CodeBlock 과 의미상 분리된다. variant 로 시각 강조(muted=정보, accent=강조), size 로 sm/md 를 고른다. default/md 는 modifier 없이 base 클래스만 붙는다.",
      "keywords": [
        "code",
        "인라인코드",
        "inline code",
        "monospace",
        "토큰",
        "식별자"
      ],
      "props": [
        {
          "name": "variant",
          "type": "\"default\" | \"muted\" | \"accent\"",
          "defaultValue": "\"default\"",
          "description": "시각 강조. muted=정보(약), accent=강조. default 는 modifier 없음."
        },
        {
          "name": "size",
          "type": "\"sm\" | \"md\"",
          "defaultValue": "\"md\"",
          "description": "글자 크기. sm 만 `axe-code-inline--size-sm` 추가, md 는 base."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLElement>",
          "description": "children/className 병합 후 나머지 속성은 <code> 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-code-inline",
          "purpose": "인라인 <code> 루트(default 톤 포함).",
          "stability": "stable"
        },
        {
          "className": "axe-code-inline--muted",
          "purpose": "muted 강조 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-code-inline--accent",
          "purpose": "accent 강조 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-code-inline--size-sm",
          "purpose": "small 크기 modifier.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "시맨틱 <code> — 별도 role 불요. 표시 전용, 상호작용 없음.",
        "notes": "여러 줄/블록 코드에는 쓰지 말 것(인라인 전용) — 블록은 CodeBlock/ServerCodeBlock."
      },
      "examples": [
        {
          "title": "인라인 강조",
          "lang": "tsx",
          "code": "import { Code } from \"@axe/ui\";\n\nexport function Example() {\n  return (\n    <>\n      <p><Code>npm install</Code> 로 설치하고 <Code variant=\"accent\">axe ship</Code> 로 배포.</p>\n      <Code variant=\"muted\" size=\"sm\">deprecated</Code>\n    </>\n  );\n}"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<code class=\"axe-code-inline\">npm install</code>\n<code class=\"axe-code-inline axe-code-inline--accent\">axe ship</code>"
        }
      ],
      "bestPractices": {
        "do": [
          "짧은 식별자·명령·경로에 사용.",
          "강조가 필요할 때만 accent."
        ],
        "dont": [
          "여러 줄 코드 블록에 쓰지 말 것 — CodeBlock 사용."
        ]
      },
      "consumeCss": "CSS-only 로 완전 재현 가능 — `axe-code-inline` 에 `--muted`/`--accent`/`--size-sm` 를 조합.",
      "url": "https://design.axelabs.ai/components/code"
    },
    {
      "name": "CodeBlock",
      "slug": "code-block",
      "group": "content",
      "summary": "복사 버튼이 있는 클라이언트 코드 블록.",
      "description": "코드 문자열을 <pre><code> 로 렌더하고, 헤더에 filename/language 라벨과 Copy 버튼을 둔다(클라이언트 컴포넌트 — clipboard API 사용). 복사 성공 시 버튼 라벨이 1.4초간 ‘Copied’ 로 바뀐다. 헤더는 language·filename·showCopy 중 하나라도 있을 때만 렌더된다. 문법 하이라이팅은 하지 않는다 — 토큰 색이 필요하면 서버 컴포넌트 ServerCodeBlock 을 사용.",
      "keywords": [
        "code block",
        "코드블록",
        "copy",
        "복사",
        "clipboard",
        "pre",
        "snippet"
      ],
      "props": [
        {
          "name": "code",
          "type": "string",
          "required": true,
          "description": "표시·복사할 코드 문자열."
        },
        {
          "name": "language",
          "type": "string",
          "description": "헤더 라벨(filename 없을 때). 하이라이팅에는 쓰이지 않음."
        },
        {
          "name": "showCopy",
          "type": "boolean",
          "defaultValue": "true",
          "description": "Copy 버튼 표시 여부. false 여도 language/filename 있으면 헤더는 렌더."
        },
        {
          "name": "filename",
          "type": "string",
          "description": "헤더 라벨(language 보다 우선)."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLDivElement>",
          "description": "className 병합 후 나머지 속성은 루트 <div> 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-codeblock",
          "purpose": "블록 루트 <div>.",
          "stability": "stable"
        },
        {
          "className": "axe-codeblock__header",
          "purpose": "라벨 + copy 버튼 헤더 행.",
          "stability": "stable"
        },
        {
          "className": "axe-codeblock__lang",
          "purpose": "filename/language 라벨.",
          "stability": "stable"
        },
        {
          "className": "axe-codeblock__copy",
          "purpose": "Copy 버튼.",
          "stability": "stable"
        },
        {
          "className": "axe-codeblock__pre",
          "purpose": "코드 본문 <pre>.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Tab 으로 Copy 버튼 포커스",
          "Enter/Space 로 복사"
        ],
        "aria": "Copy 버튼은 aria-label=\"Copy code\" 를 가진 네이티브 <button type=\"button\">. 복사 성공/실패는 버튼 텍스트(Copy↔Copied)로 시각 피드백된다. clipboard API 실패 시 조용히 무시(에러 throw 없음).",
        "notes": "복사 상태 텍스트 변화가 스크린리더에 즉시 announce 되지는 않는다(live region 미사용)."
      },
      "examples": [
        {
          "title": "기본 (filename + copy)",
          "lang": "tsx",
          "code": "import { CodeBlock } from \"@axe/ui\";\n\n<CodeBlock\n  filename=\"deploy.sh\"\n  code={`axe work docs slug\ncd ~/.worktrees/docs/slug\naxe ship docs`}\n/>"
        },
        {
          "title": "복사 버튼 없이",
          "lang": "tsx",
          "code": "import { CodeBlock } from \"@axe/ui\";\n\n<CodeBlock language=\"json\" showCopy={false} code={`{ \"port\": 4212 }`} />"
        },
        {
          "title": "CSS-only (프레임만 — copy 는 JS 필요)",
          "lang": "html",
          "code": "<div class=\"axe-codeblock\">\n  <div class=\"axe-codeblock__header\">\n    <span class=\"axe-codeblock__lang\">deploy.sh</span>\n  </div>\n  <pre class=\"axe-codeblock__pre\"><code>axe ship docs</code></pre>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "긴 명령/설정 스니펫에 사용.",
          "출처를 알리려면 filename 지정."
        ],
        "dont": [
          "문법 색이 필요한 곳에 쓰지 말 것 — ServerCodeBlock 사용.",
          "인라인 토큰에 쓰지 말 것 — Code 사용."
        ]
      },
      "consumeCss": "프레임(헤더/라벨/pre)은 CSS-only 로 재현 가능하나 Copy 버튼 동작은 클라이언트 JS(useState + clipboard) 필요 — CSS-only 로는 정적 버튼만.",
      "url": "https://design.axelabs.ai/components/code-block"
    },
    {
      "name": "Collapsible",
      "slug": "collapsible",
      "group": "container",
      "summary": "단일 disclosure 영역(한 트리거 ↔ 한 컨텐츠 show/hide).",
      "description": "Radix Collapsible 위에 얇은 .axe-collapsible__* 스타일을 얹은 경량 접기/펴기 패턴이다. 필터 패널 토글, \"더 보기\"/\"접기\" 같은 하나의 영역만 접고 펴는 용도. 여러 항목을 묶어 스택으로 관리하는 것은 Accordion 이 맡는다(경계). 트리거에 자식을 직접 넘기면 전폭 버튼 + chevron 으로 감싸고, asChild 를 쓰면 임의 트리거로 대체한다(chevron 미주입). 부드러운 height 접기는 Radix 가 노출하는 --radix-collapsible-content-height 변수를 CSS 애니메이션으로 처리.",
      "keywords": [
        "collapsible",
        "콜랩서블",
        "disclosure",
        "접기",
        "펴기",
        "toggle",
        "show hide",
        "더 보기",
        "필터 패널",
        "radix"
      ],
      "props": [
        {
          "name": "Collapsible.defaultOpen",
          "type": "boolean",
          "description": "uncontrolled 초기 열림 상태."
        },
        {
          "name": "Collapsible.open",
          "type": "boolean",
          "description": "controlled 열림 상태."
        },
        {
          "name": "Collapsible.onOpenChange",
          "type": "(open: boolean) => void",
          "description": "열림 상태 변경 콜백."
        },
        {
          "name": "Collapsible.disabled",
          "type": "boolean",
          "description": "전체 비활성화(트리거 무반응)."
        },
        {
          "name": "Collapsible.…rest",
          "type": "React.ComponentPropsWithoutRef<Radix Collapsible.Root>",
          "description": "나머지는 Radix Root 로 전파(className 은 axe-collapsible 과 병합)."
        },
        {
          "name": "CollapsibleTrigger.asChild",
          "type": "boolean",
          "defaultValue": "false",
          "description": "true 면 children 을 그대로 트리거로 사용 — 전폭 버튼 래핑·chevron 주입을 건너뛴다."
        },
        {
          "name": "CollapsibleTrigger.…rest",
          "type": "React.ComponentPropsWithoutRef<Radix Collapsible.Trigger>",
          "description": "나머지는 Radix Trigger 로 전파."
        },
        {
          "name": "CollapsibleContent.…rest",
          "type": "React.ComponentPropsWithoutRef<Radix Collapsible.Content>",
          "description": "children 은 .axe-collapsible__content-inner 로 감싸 렌더. 나머지는 Radix Content 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-collapsible__trigger",
          "purpose": "기본 트리거 버튼(전폭, label+chevron, hover/focus 트림). asChild 시 미부여.",
          "stability": "stable"
        },
        {
          "className": "axe-collapsible__trigger-label",
          "purpose": "트리거 라벨 span(flex:1).",
          "stability": "stable"
        },
        {
          "className": "axe-collapsible__chevron",
          "purpose": "chevron SVG — [data-state=open] 시 90deg 회전.",
          "stability": "stable"
        },
        {
          "className": "axe-collapsible__content",
          "purpose": "접히는 영역(overflow hidden). data-state open/closed 에 --radix-collapsible-content-height 애니메이션.",
          "stability": "stable"
        },
        {
          "className": "axe-collapsible__content-inner",
          "purpose": "content 내부 padding 래퍼.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "트리거에서 Enter/Space 로 열기/접기"
        ],
        "aria": "Radix disclosure 위임 — 트리거에 aria-expanded + aria-controls, 컨텐츠는 닫힘 시 hidden. 포커스 링은 트리거에 표시. chevron 은 aria-hidden 장식.",
        "notes": "chevron 회전·smooth height 는 Radix 가 노출하는 data-state / --radix-collapsible-content-height 를 우리 CSS 로 처리하며 prefers-reduced-motion 을 존중한다. 루트에 부여되는 axe-collapsible 클래스는 훅(마커)일 뿐 대응 CSS 규칙이 없다 — 단일 show/hide 전용이고, 여러 항목 스택이 필요하면 Accordion 을 쓴다."
      },
      "examples": [
        {
          "title": "기본(defaultOpen)",
          "lang": "tsx",
          "code": "import { Collapsible, CollapsibleTrigger, CollapsibleContent } from \"@axe/ui\";\n\n<Collapsible defaultOpen>\n  <CollapsibleTrigger>고급 필터</CollapsibleTrigger>\n  <CollapsibleContent>\n    …접히는 필터 본문…\n  </CollapsibleContent>\n</Collapsible>"
        },
        {
          "title": "asChild(임의 트리거)",
          "lang": "tsx",
          "code": "import { Collapsible, CollapsibleTrigger, CollapsibleContent, Button } from \"@axe/ui\";\n\n<Collapsible>\n  <CollapsibleTrigger asChild>\n    <Button variant=\"ghost\">더 보기</Button>\n  </CollapsibleTrigger>\n  <CollapsibleContent>…</CollapsibleContent>\n</Collapsible>"
        },
        {
          "title": "CSS-only (정적 open)",
          "lang": "html",
          "code": "<div class=\"axe-collapsible\">\n  <button class=\"axe-collapsible__trigger\" data-state=\"open\" aria-expanded=\"true\">\n    <span class=\"axe-collapsible__trigger-label\">고급 필터</span>\n    <svg class=\"axe-collapsible__chevron\" viewBox=\"0 0 12 12\" aria-hidden=\"true\">\n      <path d=\"M4 2.5 L8 6 L4 9.5\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" />\n    </svg>\n  </button>\n  <div class=\"axe-collapsible__content\" data-state=\"open\">\n    <div class=\"axe-collapsible__content-inner\">…본문…</div>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "하나의 영역만 접고 펼 때 쓴다(FAQ 목록 등 다항목은 Accordion).",
          "asChild 로 커스텀 트리거를 쓸 때는 chevron 등 열림 표시를 직접 배치한다."
        ],
        "dont": [
          "여러 Collapsible 을 나열해 accordion 을 흉내내지 말 것(키보드 네비가 없다)."
        ]
      },
      "consumeCss": "정적 open/closed 는 트리거·컨텐츠에 data-state=\"open|closed\" 를 직접 부여하면 .axe-collapsible__* 스타일로 재현된다(chevron 회전 포함). 단 부드러운 height 애니메이션은 Radix 가 --radix-collapsible-content-height 를 런타임 측정해 주입하므로 CSS-only 로는 즉시 토글만 가능.",
      "url": "https://design.axelabs.ai/components/collapsible"
    },
    {
      "name": "Combobox",
      "slug": "combobox",
      "group": "input",
      "summary": "타이핑으로 필터링하는 인라인 자동완성 (input + listbox).",
      "description": "input(role=combobox) 아래에 필터된 listbox 가 떠 있는 WAI-ARIA combobox 패턴. CommandPalette 와 같은 필터·키보드 내비·한국어 IME 가드 로직을 쓰되, modal dialog 대신 Popover anchor 로 인라인 렌더한다. 포커스는 항상 input 에 머물고 aria-activedescendant 로 가상 포커스만 이동하며, 팝업은 portal 로 떠 overflow:hidden 부모에 갇히지 않는다. 옵션이 적고 고정이면 Select 를 검토.",
      "keywords": [
        "combobox",
        "콤보박스",
        "자동완성",
        "autocomplete",
        "검색",
        "typeahead",
        "listbox"
      ],
      "props": [
        {
          "name": "items",
          "type": "ComboboxItem[]",
          "required": true,
          "description": "옵션 목록. 각 항목 = { value, label, keywords? } (keywords 는 label 외 추가 검색 대상)."
        },
        {
          "name": "value",
          "type": "string",
          "description": "선택된 value (controlled)."
        },
        {
          "name": "onValueChange",
          "type": "(value: string) => void",
          "description": "항목 선택 시 콜백."
        },
        {
          "name": "placeholder",
          "type": "string",
          "defaultValue": "\"검색…\"",
          "description": "input placeholder."
        },
        {
          "name": "emptyText",
          "type": "string",
          "defaultValue": "\"결과 없음\"",
          "description": "필터 결과가 0건일 때 표시할 텍스트."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "defaultValue": "false",
          "description": "입력·열림 비활성."
        },
        {
          "name": "className",
          "type": "string",
          "description": "루트 .axe-combobox 에 병합."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-combobox",
          "purpose": "루트 래퍼(Popover anchor).",
          "stability": "stable"
        },
        {
          "className": "axe-combobox__input",
          "purpose": "필터 input(role=combobox) — Select 트리거와 동일 sizing/border/ring 토큰.",
          "stability": "stable"
        },
        {
          "className": "axe-combobox__content",
          "purpose": "포털로 뜨는 팝업 패널.",
          "stability": "stable"
        },
        {
          "className": "axe-combobox__list",
          "purpose": "옵션 스크롤 리스트(role=listbox).",
          "stability": "stable"
        },
        {
          "className": "axe-combobox__item",
          "purpose": "옵션 row(role=option) — aria-selected/hover 시 강조.",
          "stability": "stable"
        },
        {
          "className": "axe-combobox__empty",
          "purpose": "결과 없음 안내 영역.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "↓ 로 열기 / 다음 항목, ↑ 로 이전 항목",
          "Enter 로 활성 항목 선택 (단, 한글 IME 조합 중 Enter 는 글자 확정으로 처리해 선택되지 않음)",
          "Esc 로 닫고 입력 쿼리 초기화"
        ],
        "aria": "input 에 role=combobox·aria-autocomplete=list·aria-expanded·aria-controls(listbox), 열림 시 aria-activedescendant 로 활성 옵션을 가리킴. 리스트=role=listbox, 각 옵션=role=option + aria-selected. 실제 DOM 포커스는 input 에 고정된다.",
        "notes": "Popover open/close auto-focus 를 막아 포커스가 input 밖으로 나가지 않는다. 옵션 클릭은 mousedown 으로 확정(input blur 로 닫히기 전). 활성 옵션은 scrollIntoView 로 항상 시야에 유지. portal 렌더라 overflow:hidden 부모에 갇히지 않음."
      },
      "examples": [
        {
          "title": "기본 (controlled)",
          "lang": "tsx",
          "code": "import { Combobox } from \"@axe/ui\";\n\n<Combobox\n  items={[\n    { value: \"kr\", label: \"한국\", keywords: \"korea south\" },\n    { value: \"us\", label: \"미국\", keywords: \"usa united states\" },\n    { value: \"jp\", label: \"일본\", keywords: \"japan\" },\n  ]}\n  value={value}\n  onValueChange={setValue}\n  placeholder=\"국가 검색…\"\n  emptyText=\"일치하는 국가 없음\"\n/>"
        }
      ],
      "bestPractices": {
        "do": [
          "라벨 외 별칭·영문 표기로도 찾게 하려면 각 item 에 keywords 를 채울 것.",
          "결과 0건 상황을 emptyText 로 안내."
        ],
        "dont": [
          "옵션이 소수이고 자유 검색이 불필요하면 Combobox 대신 Select 를 쓸 것.",
          "value 를 controlled 로 두면서 onValueChange 를 빼먹지 말 것(선택이 반영되지 않음)."
        ]
      },
      "consumeCss": "필터·키보드 내비·IME 가드가 전부 React 상태 로직이라 CSS-only 로는 동작을 재현할 수 없다(정적 마크업은 껍데기만). 스타일은 .axe-combobox__* 토큰으로 노출되나 실제 소비는 React 컴포넌트를 통한다.",
      "url": "https://design.axelabs.ai/components/combobox"
    },
    {
      "name": "CommandPalette",
      "slug": "command-palette",
      "group": "overlay",
      "summary": "⌘K 스타일 검색·명령 팔레트 오버레이.",
      "description": "document.body portal의 viewport 모달 combobox — 입력한 질의로 items 를 label·keywords·group 기준 부분일치 필터하고, group 별로 묶어 listbox 로 보여준다. open 상태는 소비자가 제어(controlled)하며 선택 시 onSelect + onClose 가 함께 불린다. 포커스 트랩·이전 포커스 복원·aria-activedescendant 로 WAI-ARIA dialog+combobox 패턴을 직접 구현한다. 항목은 CmdItem({ id, label, keywords?, meta?, group? }) 배열로 주입한다.",
      "keywords": [
        "command palette",
        "cmdk",
        "커맨드 팔레트",
        "명령",
        "검색",
        "⌘K",
        "combobox",
        "spotlight",
        "quick open"
      ],
      "props": [
        {
          "name": "open",
          "type": "boolean",
          "required": true,
          "description": "열림 여부. 소비자가 제어(false 면 null 반환 — 언마운트)."
        },
        {
          "name": "onClose",
          "type": "() => void",
          "required": true,
          "description": "닫기 요청 — Esc·backdrop 클릭·항목 선택 후 호출."
        },
        {
          "name": "items",
          "type": "CmdItem[]",
          "required": true,
          "description": "후보 목록. CmdItem = { id, label, keywords?, meta?, group? }."
        },
        {
          "name": "onSelect",
          "type": "(item: CmdItem) => void",
          "required": true,
          "description": "항목 확정 시 호출(뒤이어 onClose 도 실행됨)."
        },
        {
          "name": "placeholder",
          "type": "string",
          "defaultValue": "\"검색…\"",
          "description": "검색 input placeholder."
        },
        {
          "name": "ariaLabel",
          "type": "string",
          "defaultValue": "\"명령 검색\"",
          "description": "dialog의 접근성 이름. 검색 input 이름도 이 값에서 파생."
        },
        {
          "name": "className",
          "type": "string",
          "description": "루트 backdrop(.axe-cmdk-backdrop)에 덧대는 클래스."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-cmdk-backdrop",
          "purpose": "화면 전체 backdrop — 바깥 클릭 시 닫힘.",
          "stability": "stable"
        },
        {
          "className": "axe-cmdk",
          "purpose": "떠 있는 role=dialog 패널(포커스 트랩 컨테이너).",
          "stability": "stable"
        },
        {
          "className": "axe-cmdk__search-row",
          "purpose": "검색 input + 명시적 닫기 버튼 행.",
          "stability": "stable"
        },
        {
          "className": "axe-cmdk__input",
          "purpose": "상단 검색 input(role=combobox).",
          "stability": "stable"
        },
        {
          "className": "axe-cmdk__close",
          "purpose": "검색 닫기 버튼(Esc 힌트 포함).",
          "stability": "stable"
        },
        {
          "className": "axe-cmdk__list",
          "purpose": "결과 스크롤 영역(role=listbox).",
          "stability": "stable"
        },
        {
          "className": "axe-cmdk__group",
          "purpose": "동일 group 항목 묶음 래퍼.",
          "stability": "stable"
        },
        {
          "className": "axe-cmdk__group-label",
          "purpose": "group 헤더 라벨(group 있을 때만).",
          "stability": "stable"
        },
        {
          "className": "axe-cmdk__item",
          "purpose": "항목 행(role=option). 강조는 [aria-selected=\"true\"]·:hover 로.",
          "stability": "stable"
        },
        {
          "className": "axe-cmdk__item-meta",
          "purpose": "항목 우측 메타(단축키 등, mono).",
          "stability": "stable"
        },
        {
          "className": "axe-cmdk__empty",
          "purpose": "결과 없음 안내.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "↑/↓ 활성 항목 이동(범위 clamp)",
          "Enter 활성 항목 선택(한글 IME 조합 중 Enter 는 글자 확정으로 무시)",
          "Esc 닫기",
          "Tab/Shift+Tab 팔레트 내부 순환(포커스 트랩 — 밖으로 탈출 금지)"
        ],
        "aria": "패널 = role=dialog + aria-modal=true + aria-label. input = role=combobox + 접근 이름 + aria-expanded=true + aria-autocomplete=list + aria-controls(listbox) + aria-activedescendant(활성 option id). 리스트 = role=listbox, 그룹 = role=group + aria-labelledby, 항목 = role=option + aria-selected.",
        "notes": "document.body portal이라 transformed/backdrop-filter 조상에 fixed modal이 갇히지 않는다. 열릴 때 같은 window의 다른 CommandPalette를 닫아 단일 modal만 유지하고, 최초 opener 포커스를 보존한 채 rAF로 input에 포커스한다. 마지막 palette가 닫힐 때만 body scroll lock을 풀고 최초 opener로 포커스를 복원한다. 방향키/Enter 선택은 input에서만 가로채므로 닫기 버튼의 네이티브 Enter를 침범하지 않는다. 활성 항목은 scrollIntoView(block:nearest)로 시야 유지. ⌘K 전역 단축키는 컴포넌트가 등록하지 않음 — open 토글은 소비자 몫."
      },
      "examples": [
        {
          "title": "기본 (controlled)",
          "lang": "tsx",
          "code": "import { useState } from \"react\";\nimport { CommandPalette, type CmdItem } from \"@axe/ui\";\n\nconst items: CmdItem[] = [\n  { id: \"new\", label: \"새 문서\", group: \"문서\", meta: \"⌘N\" },\n  { id: \"find\", label: \"검색\", keywords: \"find grep\", group: \"문서\", meta: \"⌘F\" },\n  { id: \"theme\", label: \"테마 전환\", group: \"설정\" },\n];\n\nexport function Launcher() {\n  const [open, setOpen] = useState(false);\n  return (\n    <>\n      <button onClick={() => setOpen(true)}>명령 팔레트 (⌘K)</button>\n      <CommandPalette\n        open={open}\n        items={items}\n        ariaLabel=\"문서 명령 검색\"\n        placeholder=\"명령 검색…\"\n        onClose={() => setOpen(false)}\n        onSelect={(item) => console.log(item.id)}\n      />\n    </>\n  );\n}"
        },
        {
          "title": "CSS-only (비-React 정적 마크업)",
          "lang": "html",
          "code": "<div class=\"axe-cmdk-backdrop\">\n  <div class=\"axe-cmdk\" role=\"dialog\" aria-modal=\"true\" aria-label=\"문서 명령 검색\">\n    <div class=\"axe-cmdk__search-row\">\n    <input\n      class=\"axe-cmdk__input\"\n      role=\"combobox\"\n      aria-label=\"문서 명령 검색어\"\n      aria-expanded=\"true\"\n      aria-autocomplete=\"list\"\n      aria-controls=\"cmdk-list\"\n      placeholder=\"검색…\"\n    />\n      <button class=\"axe-cmdk__close\" type=\"button\" aria-label=\"검색 닫기\">Esc</button>\n    </div>\n    <div id=\"cmdk-list\" class=\"axe-cmdk__list\" role=\"listbox\">\n      <div class=\"axe-cmdk__group\">\n        <div class=\"axe-cmdk__group-label\">문서</div>\n        <div class=\"axe-cmdk__item\" role=\"option\" aria-selected=\"true\">\n          <span>새 문서</span><span class=\"axe-cmdk__item-meta\">⌘N</span>\n        </div>\n        <div class=\"axe-cmdk__item\" role=\"option\" aria-selected=\"false\">\n          <span>검색</span><span class=\"axe-cmdk__item-meta\">⌘F</span>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "open/닫기 상태와 ⌘K 단축키는 소비자가 관리(예 useHotkeys)한다.",
          "meta 에 단축키를, group 으로 항목을 의미 단위로 묶는다.",
          "검색 보강이 필요하면 keywords 에 동의어(영/한)를 넣는다."
        ],
        "dont": [
          "긴 목록에 무거운 onSelect 를 두지 말 것 — 선택 직후 onClose 로 닫힌다.",
          "backdrop 을 다른 오버레이 위에 겹치지 말 것(단일 모달 전제)."
        ]
      },
      "consumeCss": "필터·포커스 트랩·활성 이동은 JS 로직 — CSS-only 로는 정적 표시만 재현된다. 항목 강조는 [aria-selected=\"true\"] 속성으로 그려지므로, 비-React 로 쓸 땐 활성 option 에 aria-selected 를 직접 토글해야 한다.",
      "url": "https://design.axelabs.ai/components/command-palette"
    },
    {
      "name": "ContextMenu",
      "slug": "context-menu",
      "group": "overlay",
      "summary": "우클릭(또는 길게 누르기)으로 대상 영역 위에 여는 컨텍스트 액션 메뉴 (Radix ContextMenu 기반).",
      "description": "DropdownMenu 와 **같은** .axe-dropdown-menu__* 표면·항목·indicator·shortcut·separator 를 재사용한다(새 CSS 없음). 차이는 트리거뿐 — DropdownMenu 는 버튼 클릭으로 트리거 아래에, ContextMenu 는 대상 영역 우클릭으로 커서 위치에 뜬다(그래서 sideOffset 개념이 없다). ContextMenu(Root)·ContextMenuTrigger(우클릭 대상)·ContextMenuContent·ContextMenuItem 를 기본으로, Label·Separator·Shortcut·Checkbox/RadioItem·Sub 를 조합한다.",
      "keywords": [
        "context menu",
        "컨텍스트 메뉴",
        "우클릭",
        "right click",
        "long press",
        "액션 메뉴",
        "radix"
      ],
      "props": [
        {
          "name": "open",
          "type": "boolean",
          "description": "열림 상태 (controlled). Radix Root prop."
        },
        {
          "name": "onOpenChange",
          "type": "(open: boolean) => void",
          "description": "열림/닫힘 콜백 (Root)."
        },
        {
          "name": "modal",
          "type": "boolean",
          "defaultValue": "true",
          "description": "true 면 바깥 스크롤/포커스를 잠금 (Root)."
        },
        {
          "name": "withPortal",
          "type": "boolean",
          "defaultValue": "true",
          "description": "ContextMenuContent 를 portal 로 띄울지 — overflow:hidden 부모 탈출."
        },
        {
          "name": "variant",
          "type": "\"default\" | \"danger\"",
          "defaultValue": "\"default\"",
          "description": "ContextMenuItem 톤. danger=파괴적 액션의 텍스트/highlighted bg 를 danger 로."
        },
        {
          "name": "icon",
          "type": "React.ReactNode",
          "description": "ContextMenuItem 좌측 아이콘 슬롯 (선택)."
        },
        {
          "name": "...rest",
          "type": "각 Radix ContextMenu.* props",
          "description": "Trigger(asChild·우클릭 대상)·Content(collisionPadding)·Item(onSelect/disabled)·CheckboxItem/RadioItem 등 각 파트는 대응 Radix 컴포넌트 props 를 그대로 전파. DropdownMenu 와 달리 sideOffset·Arrow 는 없다(커서 위치 표시)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-dropdown-menu__content",
          "purpose": "커서 위치에 뜨는 메뉴 패널 표면(DropdownMenu 와 공유).",
          "stability": "stable"
        },
        {
          "className": "axe-dropdown-menu__sub-content",
          "purpose": "서브메뉴 패널(공유).",
          "stability": "stable"
        },
        {
          "className": "axe-dropdown-menu__item",
          "purpose": "메뉴 항목 row(공유).",
          "stability": "stable"
        },
        {
          "className": "axe-dropdown-menu__item--danger",
          "purpose": "danger 변형 항목(공유).",
          "stability": "stable"
        },
        {
          "className": "axe-dropdown-menu__label",
          "purpose": "그룹 제목(공유).",
          "stability": "stable"
        },
        {
          "className": "axe-dropdown-menu__separator",
          "purpose": "그룹 구분선(공유).",
          "stability": "stable"
        },
        {
          "className": "axe-dropdown-menu__shortcut",
          "purpose": "우측 정렬 단축키 힌트 슬롯(공유).",
          "stability": "stable"
        },
        {
          "className": "axe-dropdown-menu__item--checkbox",
          "purpose": "checkbox 항목 layout 변형(자동 적용).",
          "stability": "internal"
        },
        {
          "className": "axe-dropdown-menu__item--radio",
          "purpose": "radio 항목 layout 변형(자동 적용).",
          "stability": "internal"
        },
        {
          "className": "axe-dropdown-menu__item--sub-trigger",
          "purpose": "서브메뉴 여는 항목 layout(자동 적용).",
          "stability": "internal"
        },
        {
          "className": "axe-dropdown-menu__item-indicator",
          "purpose": "체크/도트 표시 슬롯(Radix ItemIndicator).",
          "stability": "internal"
        },
        {
          "className": "axe-dropdown-menu__item-icon",
          "purpose": "항목 좌측 아이콘 슬롯 span.",
          "stability": "internal"
        },
        {
          "className": "axe-dropdown-menu__item-label",
          "purpose": "항목 가운데 라벨 span.",
          "stability": "internal"
        },
        {
          "className": "axe-dropdown-menu__sub-chevron",
          "purpose": "sub-trigger 우측 chevron 아이콘.",
          "stability": "internal"
        }
      ],
      "a11y": {
        "keyboard": [
          "포커스된 트리거에서 ContextMenu 키(또는 Shift+F10)로 열기",
          "↑/↓ 로 항목 이동, Home/End 로 처음/끝",
          "글자 입력 시 type-ahead 로 해당 항목 점프",
          "→ 로 서브메뉴 열기, ← 로 닫고 상위로",
          "Enter/Space 로 항목 실행, Esc 로 닫기(트리거로 포커스 복원)"
        ],
        "aria": "Radix 가 콘텐츠에 role=menu, 각 항목에 role=menuitem(체크박스=menuitemcheckbox, 라디오=menuitemradio)을 부여. 현재 항목은 data-highlighted, 비활성은 data-disabled. 마우스 우클릭 외에 터치의 길게 누르기(long-press)로도 열린다.",
        "notes": "커서(또는 long-press) 지점에 앵커되어 뜬다. modal 기본(바깥 클릭·스크롤 잠금). 닫힘 시 트리거로 포커스 복원. prefers-reduced-motion 시 애니메이션 제거."
      },
      "examples": [
        {
          "title": "기본 (우클릭 영역)",
          "lang": "tsx",
          "code": "import {\n  ContextMenu,\n  ContextMenuTrigger,\n  ContextMenuContent,\n  ContextMenuLabel,\n  ContextMenuItem,\n  ContextMenuShortcut,\n  ContextMenuSeparator,\n} from \"@axe/ui\";\n\n<ContextMenu>\n  <ContextMenuTrigger asChild>\n    <div className=\"card\">우클릭 가능한 영역</div>\n  </ContextMenuTrigger>\n  <ContextMenuContent>\n    <ContextMenuLabel>액션</ContextMenuLabel>\n    <ContextMenuItem onSelect={open}>\n      열기 <ContextMenuShortcut>⏎</ContextMenuShortcut>\n    </ContextMenuItem>\n    <ContextMenuItem onSelect={duplicate}>복제</ContextMenuItem>\n    <ContextMenuSeparator />\n    <ContextMenuItem variant=\"danger\" onSelect={remove}>삭제</ContextMenuItem>\n  </ContextMenuContent>\n</ContextMenu>"
        },
        {
          "title": "서브메뉴",
          "lang": "tsx",
          "code": "import {\n  ContextMenu,\n  ContextMenuTrigger,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuSub,\n  ContextMenuSubTrigger,\n  ContextMenuSubContent,\n} from \"@axe/ui\";\n\n<ContextMenu>\n  <ContextMenuTrigger asChild>\n    <div className=\"row\">행</div>\n  </ContextMenuTrigger>\n  <ContextMenuContent>\n    <ContextMenuItem onSelect={edit}>편집</ContextMenuItem>\n    <ContextMenuSub>\n      <ContextMenuSubTrigger>이동</ContextMenuSubTrigger>\n      <ContextMenuSubContent>\n        <ContextMenuItem onSelect={() => moveTo(\"inbox\")}>보관함</ContextMenuItem>\n        <ContextMenuItem onSelect={() => moveTo(\"archive\")}>아카이브</ContextMenuItem>\n      </ContextMenuSubContent>\n    </ContextMenuSub>\n  </ContextMenuContent>\n</ContextMenu>"
        }
      ],
      "bestPractices": {
        "do": [
          "우클릭 컨텍스트가 있는 표면(행·카드·캔버스)에만 붙인다 — 발견성이 낮으니 보조 경로로.",
          "같은 액션을 툴바/DropdownMenu 등 눈에 보이는 경로로도 제공.",
          "항목 의미는 DropdownMenu 와 동일하게 유지(같은 표면·같은 규칙)."
        ],
        "dont": [
          "유일한 액션 경로로 삼지 말 것 — 우클릭은 터치/키보드 발견성이 낮다.",
          "메뉴에 폼·긴 인터랙티브 컨텐츠를 넣지 말 것."
        ]
      },
      "consumeCss": "열림 트리거(우클릭/long-press)·커서 앵커링·항목 하이라이트가 Radix 런타임에 의존해 CSS-only 로는 재현할 수 없다. 표면 스타일은 DropdownMenu 의 .axe-dropdown-menu__* 를 그대로 공유하므로 별도 CSS 토큰이 없다 — 실제 소비는 React 컴포넌트로.",
      "url": "https://design.axelabs.ai/components/context-menu"
    },
    {
      "name": "DataTable",
      "slug": "data-table",
      "group": "data",
      "summary": "정렬·페이지 슬라이싱·행 클릭을 갖춘 제네릭 데이터 표.",
      "description": "columns 정의(key·header·accessor·render·sortable·align·width)와 rows 배열로 구동되는 SSOT 표. 정렬은 controlled(sortBy+sortDir+onSort)와 uncontrolled(defaultSortBy) 두 모드를 지원하고, accessor 가 있으면 그 값으로, 없으면 row[key] 로 비교한다(숫자·Date·한국어 로케일 자동 판별). page+pageSize 를 주면 자체적으로 slice 만 하며, 페이지 이동 UI 는 Pagination 컴포넌트가 별도로 담당한다. loading 시 skeleton 행, 빈 rows 시 emptyState 를 렌더한다.",
      "keywords": [
        "data table",
        "데이터테이블",
        "표",
        "정렬",
        "sort",
        "pagination",
        "페이지네이션",
        "sortable",
        "sticky header"
      ],
      "props": [
        {
          "name": "columns",
          "type": "DataTableColumn<Row>[]",
          "required": true,
          "description": "열 정의 배열. 각 항목: key(정렬/aria 식별자) · header(ReactNode) · accessor?(정렬값 추출, 미지정 시 row[key]) · render?(셀 커스텀, 미지정 시 row[key]) · sortable? · align?(\"left\"|\"right\"|\"center\") · width?(CSS 폭)."
        },
        {
          "name": "rows",
          "type": "Row[]",
          "required": true,
          "description": "표시할 데이터 행. Row 는 제네릭 — 어떤 객체든 가능."
        },
        {
          "name": "getRowKey",
          "type": "(row: Row, index: number) => string",
          "description": "React key 로만 사용(onRowClick 은 row 객체를 직접 받으므로 무관). 미지정 시 배열 index 사용."
        },
        {
          "name": "sortBy",
          "type": "string",
          "description": "controlled 정렬 — 현재 정렬 열의 key. onSort 와 함께 줘야 controlled 로 인식."
        },
        {
          "name": "sortDir",
          "type": "\"asc\" | \"desc\"",
          "description": "controlled 정렬 방향. controlled 인데 미지정이면 \"asc\" 로 취급."
        },
        {
          "name": "onSort",
          "type": "(key: string, dir: SortDir) => void",
          "description": "정렬 헤더 클릭 콜백. sortBy 와 함께 주면 controlled. sortBy 만 주고 이걸 안 주면 controlled 로 인식되지 않아 dev 경고와 함께 sortBy 값이 무시되고, 헤더 클릭 정렬은 내부 state 로 폴백해 계속 동작한다."
        },
        {
          "name": "defaultSortBy",
          "type": "string",
          "description": "uncontrolled 초기 정렬 열. 이후 정렬 state 는 컴포넌트가 보유."
        },
        {
          "name": "defaultSortDir",
          "type": "\"asc\" | \"desc\"",
          "defaultValue": "\"asc\"",
          "description": "uncontrolled 초기 정렬 방향."
        },
        {
          "name": "page",
          "type": "number",
          "description": "1-based 현재 페이지. pageSize 와 함께 있어야 slice 발생."
        },
        {
          "name": "pageSize",
          "type": "number",
          "description": "페이지당 행 수. page 와 함께 주면 (page-1)*pageSize 부터 slice. loading skeleton 행 수의 기본값이기도 함(미지정 시 5)."
        },
        {
          "name": "onPageChange",
          "type": "(page: number) => void",
          "description": "시그니처 대칭을 위해 노출되나 컴포넌트가 내부에서 호출하지 않는다 — 페이지 전환은 짝을 이루는 Pagination 이 발생시키고, 그 값을 부모가 page 로 되돌려 준다."
        },
        {
          "name": "onRowClick",
          "type": "(row: Row) => void",
          "description": "행 클릭 콜백. 지정 시 행에 `--clickable` 클래스와 클릭 핸들러가 붙는다."
        },
        {
          "name": "rowClickable",
          "type": "boolean",
          "defaultValue": "false",
          "description": "onRowClick 이 있을 때 행 전체를 role=button+tabIndex 위젯으로 만들지. 기본 false(table 시맨틱 보존) — 셀 안에 `<a>`/`<button>` 등 interactive 가 있으면 nested interactive 충돌을 피하려 false 로 두고, 그 경우 마우스 클릭은 되지만 키보드/role 위임은 셀 내부 컨트롤이 책임진다."
        },
        {
          "name": "loading",
          "type": "boolean",
          "description": "true 면 tbody 자리에 skeleton 행(pageSize 또는 5개)을 렌더."
        },
        {
          "name": "emptyState",
          "type": "React.ReactNode",
          "description": "rows 가 비었을 때 placeholder. 미지정 시 muted \"데이터 없음\"."
        },
        {
          "name": "stickyHeader",
          "type": "boolean",
          "description": "thead 를 sticky 로 고정. `.axe-data-table__scroll` 에 max-height(480px)가 걸려 세로 스크롤이 생길 때 유효."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLDivElement>, \"onClick\">",
          "description": "className 병합 후 나머지는 루트 div 로 전파. onClick 은 행 단위 onRowClick 과 혼동을 막으려 제외됨. forwardRef 는 루트 div 를 가리킨다."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-data-table",
          "purpose": "루트 div — elevated 배경·border·radius-lg·overflow hidden(radius clip).",
          "stability": "stable"
        },
        {
          "className": "axe-data-table--sticky",
          "purpose": "stickyHeader 모디파이어 — scroll 영역 max-height + thead sticky 활성.",
          "stability": "stable"
        },
        {
          "className": "axe-data-table__scroll",
          "purpose": "가로(및 sticky 시 세로) 스크롤 컨테이너.",
          "stability": "stable"
        },
        {
          "className": "axe-data-table__table",
          "purpose": "`<table>` — width 100%·min-width 600px·border-collapse.",
          "stability": "stable"
        },
        {
          "className": "axe-data-table__thead",
          "purpose": "thead — subtle 배경.",
          "stability": "stable"
        },
        {
          "className": "axe-data-table__th",
          "purpose": "헤더 셀 — xs·uppercase·secondary. `--right/--center` 로 정렬.",
          "stability": "stable"
        },
        {
          "className": "axe-data-table__th--sortable",
          "purpose": "정렬 가능 헤더 — padding 을 0 으로 두고 내부 sort-btn 이 padding 을 갖는다.",
          "stability": "stable"
        },
        {
          "className": "axe-data-table__sort-btn",
          "purpose": "정렬 헤더의 실제 `<button>` — 전폭 flex·hover/focus 시 강조.",
          "stability": "stable"
        },
        {
          "className": "axe-data-table__td",
          "purpose": "본문 셀 — tabular-nums·middle 정렬. `--right/--center` 로 정렬.",
          "stability": "stable"
        },
        {
          "className": "axe-data-table__tr",
          "purpose": "본문 행 — 하단 border·hover 시 accent 4% 배경.",
          "stability": "stable"
        },
        {
          "className": "axe-data-table__tr--clickable",
          "purpose": "onRowClick 이 있을 때 — cursor pointer·focus-visible outline.",
          "stability": "stable"
        },
        {
          "className": "axe-data-table__empty",
          "purpose": "빈 상태 셀 — 중앙 muted 텍스트(colSpan 전폭).",
          "stability": "stable"
        },
        {
          "className": "axe-data-table__th--active",
          "purpose": "현재 정렬 중인 헤더 — 컴포넌트 정렬 state 로만 토글되는 내부 훅.",
          "stability": "internal"
        },
        {
          "className": "axe-data-table__sort-arrow",
          "purpose": "정렬 방향 화살표(∧/∨/↕) — aria-hidden 장식.",
          "stability": "internal"
        },
        {
          "className": "axe-data-table__tr--empty",
          "purpose": "빈 상태 행 마커 — hover 무효화.",
          "stability": "internal"
        },
        {
          "className": "axe-data-table__tr--skeleton",
          "purpose": "loading skeleton 행.",
          "stability": "internal"
        },
        {
          "className": "axe-data-table__skel",
          "purpose": "shimmer 플레이스홀더 바(indicators.css 의 axe-shimmer 공유).",
          "stability": "internal"
        }
      ],
      "a11y": {
        "keyboard": [
          "정렬 헤더는 네이티브 `<button>` — Tab 포커스 후 Enter/Space 로 정렬 토글(asc↔desc)",
          "rowClickable=true 인 행은 Enter/Space 로 onRowClick 실행(preventDefault 로 스크롤 방지)"
        ],
        "aria": "정렬 가능 열의 `<th>` 는 aria-sort=ascending|descending|none, scope=\"col\". 정렬 화살표는 aria-hidden. rowClickable=true 인 행만 role=\"button\"+tabIndex=0 를 받고, 기본(false)은 table 시맨틱을 보존한다.",
        "notes": "dev 모드에서 sortBy 만 주고 onSort 를 빠뜨리면 console.warn 으로 controlled 오설정을 경고한다. 셀 내부에 링크/버튼이 있으면 rowClickable 을 false 로 둬 nested interactive 충돌을 피한다."
      },
      "examples": [
        {
          "title": "uncontrolled 정렬 + 커스텀 셀",
          "lang": "tsx",
          "code": "import { DataTable } from \"@axe/ui\";\nimport type { DataTableColumn } from \"@axe/ui\";\n\ntype Deal = { name: string; stage: string; amount: number };\n\nconst columns: DataTableColumn<Deal>[] = [\n  { key: \"name\", header: \"딜\", sortable: true },\n  { key: \"stage\", header: \"단계\" },\n  {\n    key: \"amount\",\n    header: \"투자액\",\n    sortable: true,\n    align: \"right\",\n    render: (r) => `${r.amount.toLocaleString()}원`,\n  },\n];\n\n<DataTable\n  columns={columns}\n  rows={deals}\n  getRowKey={(r) => r.name}\n  defaultSortBy=\"amount\"\n  defaultSortDir=\"desc\"\n  onRowClick={(r) => openDeal(r)}\n/>"
        },
        {
          "title": "controlled 정렬 + Pagination 짝",
          "lang": "tsx",
          "code": "import { DataTable, Pagination } from \"@axe/ui\";\n\nconst [sortBy, setSortBy] = useState(\"amount\");\nconst [sortDir, setSortDir] = useState<\"asc\" | \"desc\">(\"desc\");\nconst [page, setPage] = useState(1);\nconst pageSize = 10;\n\n<>\n  <DataTable\n    columns={columns}\n    rows={rows}\n    sortBy={sortBy}\n    sortDir={sortDir}\n    onSort={(key, dir) => { setSortBy(key); setSortDir(dir); }}\n    page={page}\n    pageSize={pageSize}\n    stickyHeader\n    loading={isLoading}\n  />\n  <Pagination\n    page={page}\n    pageSize={pageSize}\n    total={rows.length}\n    onPageChange={setPage}\n  />\n</>"
        },
        {
          "title": "CSS-only 정적 표 (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-data-table\">\n  <div class=\"axe-data-table__scroll\">\n    <table class=\"axe-data-table__table\">\n      <thead class=\"axe-data-table__thead\">\n        <tr>\n          <th class=\"axe-data-table__th\" scope=\"col\">딜</th>\n          <th class=\"axe-data-table__th axe-data-table__th--right\" scope=\"col\">투자액</th>\n        </tr>\n      </thead>\n      <tbody>\n        <tr class=\"axe-data-table__tr\">\n          <td class=\"axe-data-table__td\">AXE 포트A</td>\n          <td class=\"axe-data-table__td axe-data-table__td--right\">3,000,000원</td>\n        </tr>\n      </tbody>\n    </table>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "숫자 열은 align=\"right\" 로 tabular-nums 정렬 효과를 살린다.",
          "정렬을 서버/URL 과 동기화해야 하면 controlled(sortBy+sortDir+onSort)로, 화면 내 정렬이면 defaultSortBy 로.",
          "페이지 이동 UI 는 Pagination 을 짝으로 두고 page state 만 부모가 소유한다."
        ],
        "dont": [
          "sortBy 를 주면서 onSort 를 빠뜨리지 말 것 — controlled 로 인식되지 않아 sortBy 값이 무시되고(내부 state 로 폴백) dev 경고가 뜬다.",
          "셀에 링크/버튼을 넣으면서 rowClickable 을 true 로 두지 말 것(중첩 위젯 충돌).",
          "onPageChange 로 slice 를 기대하지 말 것 — slice 는 page+pageSize 만으로 일어난다."
        ]
      },
      "consumeCss": "정적 표 골격은 `.axe-data-table*` 클래스로 서버 렌더에서 재현 가능하나, 정렬·페이지 슬라이싱·skeleton·aria-sort 토글은 React 컴포넌트의 state 로직이 필요하다. CSS-only 로 갈 때는 aria-sort 와 정렬 화살표를 직접 세팅해야 한다.",
      "url": "https://design.axelabs.ai/components/data-table"
    },
    {
      "name": "DescriptionList",
      "slug": "description-list",
      "group": "data",
      "summary": "레코드 하나의 필드를 세로로 읽는 키·값 목록(<dl>).",
      "description": "artifact·문서·계약 상세 화면의 '필드명 : 값' 두 열을 담당한다. 루트 <dl> 이 곧 2열 grid 이고 키 열 폭은 --axe-kv-key-w (기본 8.75rem) 로 표면마다 조정한다. 여러 레코드를 행으로 비교하는 DataTable 과 역할이 다르다 — 이쪽은 레코드 하나의 필드를 훑는 용도다. 화면 폭이 30rem 이하로 좁아지면 한 열로 쌓여 값이 눌리지 않는다. React 는 각 행을 <div class=\"axe-kv__row\"> 로 감싸지만 CSS 가 display: contents 로 grid 참여만 넘기므로, 래퍼 없는 평면 dt/dd 마크업도 동일하게 정렬된다.",
      "keywords": [
        "description list",
        "definition list",
        "정의 목록",
        "키값",
        "key value",
        "kv",
        "dl",
        "detail",
        "상세",
        "메타데이터",
        "metadata"
      ],
      "props": [
        {
          "name": "items",
          "type": "DescriptionListEntry[]",
          "description": "키·값 행 목록. `{ id?: string; term: ReactNode; value: ReactNode }`. id 는 React key 로만 쓰이며 생략 시 배열 인덱스를 쓴다. children 과 함께 주면 items 가 먼저 렌더된다."
        },
        {
          "name": "compact",
          "type": "boolean",
          "description": "행 간격을 좁힌다(axe-kv--compact). 좁은 context rail 이나 카드 안쪽에서."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLDListElement>",
          "description": "className 병합, 나머지는 루트 <dl> 로 전파. ref 도 <dl> 로 forward."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-kv",
          "purpose": "루트 <dl>. 키 열(--axe-kv-key-w) + 값 열 2열 grid.",
          "stability": "stable"
        },
        {
          "className": "axe-kv--compact",
          "purpose": "행 간격을 좁히는 밀도 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-kv__row",
          "purpose": "dt+dd 한 쌍을 감싸는 선택적 <div>. display: contents 라 박스를 만들지 않는다.",
          "stability": "stable"
        },
        {
          "className": "axe-kv__key",
          "purpose": "키 <dt> — muted 색.",
          "stability": "stable"
        },
        {
          "className": "axe-kv__value",
          "purpose": "값 <dd> — margin 0 + 긴 식별자 줄바꿈(overflow-wrap: anywhere).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "네이티브 <dl>/<dt>/<dd> 시맨틱을 그대로 쓴다 — 별도 role 불요. 행 래퍼 <div> 는 HTML 명세가 dl 자식으로 허용하는 그룹핑이며, display: contents 여도 dt/dd 의 관계는 유지된다.",
        "notes": "상호작용 없는 정적 표면이라 키보드 계약이 없다. 값이 링크나 버튼이면 그 요소 자신의 포커스 계약을 따른다. 좁은 폭에서 한 열로 쌓일 때도 dt 가 dd 앞에 오는 읽기 순서는 그대로다."
      },
      "examples": [
        {
          "title": "기본",
          "lang": "tsx",
          "code": "import { DescriptionList } from \"@axe/ui\";\n\n<DescriptionList\n  items={[\n    { id: \"kind\", term: \"kind\", value: \"decision\" },\n    { id: \"visibility\", term: \"visibility\", value: \"org\" },\n    { id: \"created_at\", term: \"created_at\", value: \"2026-07-28 14:02 KST\" },\n    { id: \"created_by\", term: \"created_by\", value: \"index-agent\" },\n  ]}\n/>"
        },
        {
          "title": "좁은 rail — compact + 키 열 축소",
          "lang": "tsx",
          "code": "import { DescriptionList } from \"@axe/ui\";\n\n<DescriptionList\n  compact\n  style={{ \"--axe-kv-key-w\": \"5.5rem\" } as React.CSSProperties}\n  items={[\n    { id: \"status\", term: \"상태\", value: \"결재 대기\" },\n    { id: \"requester\", term: \"기안자\", value: \"강태훈\" },\n    { id: \"due\", term: \"기한\", value: \"2026-08-05\" },\n  ]}\n/>"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<dl class=\"axe-kv\">\n  <dt class=\"axe-kv__key\">kind</dt>\n  <dd class=\"axe-kv__value\">decision</dd>\n  <dt class=\"axe-kv__key\">visibility</dt>\n  <dd class=\"axe-kv__value\">org</dd>\n</dl>"
        }
      ],
      "bestPractices": {
        "do": [
          "키는 짧은 명사로 고정하고 값 쪽에 서술을 둘 것.",
          "긴 식별자(해시·URL)는 값에 그대로 둬도 된다 — 값 열이 줄바꿈한다.",
          "표면마다 키 열 폭이 다르면 --axe-kv-key-w 로 맞출 것."
        ],
        "dont": [
          "여러 레코드를 비교하는 표로 쓰지 말 것 — 그건 DataTable.",
          "값 안에 또 다른 dl 을 중첩해 grid 정렬을 깨지 말 것."
        ]
      },
      "consumeCss": "Radix 무의존 순수 CSS 라 maud·jinja 도 동일 마크업으로 재현한다. React 가 쓰는 <div class=\"axe-kv__row\"> 래퍼는 선택 사항 — dl 직계 dt/dd 만으로도 같은 정렬이 나온다. 키 열 폭만 --axe-kv-key-w 로 넘기면 되고 나머지 값은 전부 토큰이라 light/dark 자동 전환된다.",
      "url": "https://design.axelabs.ai/components/description-list"
    },
    {
      "name": "Dialog",
      "slug": "dialog",
      "group": "overlay",
      "summary": "포커스를 가두는 모달 다이얼로그 (Radix Dialog 기반).",
      "description": "Radix Dialog 위에 .axe-dialog__* 스타일을 입힌 wrapper 다. Dialog(Root)·DialogTrigger·DialogContent·DialogTitle·DialogDescription·DialogClose 를 조합해 쓰고, DialogHeader/DialogFooter 는 title·설명·액션 라인을 잡아주는 선택적 레이아웃 헬퍼다. 포커스 트랩·Esc 닫기·바깥 클릭 닫기·ARIA role/labelledby/describedby·바깥 트리 aria-hidden 격리는 전부 Radix 가 처리하고, @axe/ui 는 size 변형과 표면 스타일만 담당한다. 닫기 X 버튼은 자동으로 그려지지 않으므로 DialogClose 를 직접 배치한다(헤더는 우상단 X 자리를 위해 padding-right 를 비워둔다).",
      "keywords": [
        "dialog",
        "다이얼로그",
        "모달",
        "modal",
        "팝업",
        "overlay",
        "radix"
      ],
      "props": [
        {
          "name": "open",
          "type": "boolean",
          "description": "열림 상태 (controlled). Radix Dialog.Root prop."
        },
        {
          "name": "onOpenChange",
          "type": "(open: boolean) => void",
          "description": "열림/닫힘 전환 콜백 (Root)."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "defaultValue": "false",
          "description": "uncontrolled 초기 열림 상태 (Root)."
        },
        {
          "name": "modal",
          "type": "boolean",
          "defaultValue": "true",
          "description": "true 면 바깥과의 상호작용을 차단하고 포커스를 가둔다 (Root)."
        },
        {
          "name": "size",
          "type": "\"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\"",
          "defaultValue": "\"md\"",
          "description": "DialogContent 의 max-width. sm=440 / md=560 / lg=720 / xl=960 / full=100vw(전체화면·radius 0)."
        },
        {
          "name": "withOverlay",
          "type": "boolean",
          "defaultValue": "true",
          "description": "DialogContent 가 흐린 backdrop 오버레이를 함께 렌더할지. false 면 콘텐츠만 뜬다."
        },
        {
          "name": "...rest (DialogContent)",
          "type": "React.ComponentPropsWithoutRef<typeof Dialog.Content>",
          "description": "onEscapeKeyDown·onPointerDownOutside·forceMount 등 Radix Content props 를 그대로 전파."
        },
        {
          "name": "...rest (Header/Footer)",
          "type": "React.HTMLAttributes<HTMLDivElement>",
          "description": "DialogHeader·DialogFooter 는 className 병합 후 나머지 속성을 루트 <div> 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-dialog__overlay",
          "purpose": "화면 전체를 덮는 흐린 backdrop(fixed inset:0).",
          "stability": "stable"
        },
        {
          "className": "axe-dialog__content",
          "purpose": "중앙 정렬 카드 표면(fixed·translate 중앙).",
          "stability": "stable"
        },
        {
          "className": "axe-dialog__content--sm",
          "purpose": "max-width 440px 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-dialog__content--md",
          "purpose": "max-width 560px 변형(기본).",
          "stability": "stable"
        },
        {
          "className": "axe-dialog__content--lg",
          "purpose": "max-width 720px 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-dialog__content--xl",
          "purpose": "max-width 960px 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-dialog__content--full",
          "purpose": "전체화면 변형(radius 0·100dvh).",
          "stability": "stable"
        },
        {
          "className": "axe-dialog__header",
          "purpose": "title+설명 스택. 우상단 close X 자리로 padding-right 확보.",
          "stability": "stable"
        },
        {
          "className": "axe-dialog__title",
          "purpose": "제목(display 폰트·semibold).",
          "stability": "stable"
        },
        {
          "className": "axe-dialog__description",
          "purpose": "보조 설명 문단(secondary 톤).",
          "stability": "stable"
        },
        {
          "className": "axe-dialog__footer",
          "purpose": "우정렬 액션 버튼 라인(상단 구분선).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Esc 로 닫기(트리거로 포커스 복원)",
          "Tab/Shift+Tab 은 콘텐츠 안에서만 순환(포커스 트랩)",
          "열릴 때 첫 포커스 대상으로 자동 이동, 닫힐 때 트리거로 복원"
        ],
        "aria": "Radix 가 콘텐츠에 role=dialog 를 부여하고, DialogTitle 을 aria-labelledby, DialogDescription 을 aria-describedby 로 자동 연결한다. 모달 격리는 aria-modal 속성이 아니라 바깥 트리 전체에 aria-hidden 을 거는 방식(aria-hidden 패키지의 hideOthers)으로 구현된다 — 설치된 @radix-ui/react-dialog 는 콘텐츠에 aria-modal 을 붙이지 않는다. 트리거에는 aria-haspopup=dialog·aria-expanded·aria-controls 가 붙는다.",
        "notes": "DialogTitle 은 접근성상 필수 — 생략하면 Radix 가 개발 콘솔에 경고한다. 시각적으로 숨기려면 sr-only 로. modal=true 기본이라 바깥 스크롤이 잠기고 포인터가 차단된다. data-state=open|closed 로 진입/퇴장 애니메이션이 갈리며 prefers-reduced-motion 시 애니메이션이 제거된다."
      },
      "examples": [
        {
          "title": "기본 (확인 다이얼로그)",
          "lang": "tsx",
          "code": "import {\n  Dialog,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogDescription,\n  DialogFooter,\n  DialogClose,\n  Button,\n} from \"@axe/ui\";\n\n<Dialog>\n  <DialogTrigger asChild>\n    <Button>초대 보내기</Button>\n  </DialogTrigger>\n  <DialogContent size=\"sm\">\n    <DialogHeader>\n      <DialogTitle>멤버 초대</DialogTitle>\n      <DialogDescription>\n        입력한 이메일로 워크스페이스 초대 링크가 발송됩니다.\n      </DialogDescription>\n    </DialogHeader>\n    <DialogFooter>\n      <DialogClose asChild>\n        <Button variant=\"ghost\">취소</Button>\n      </DialogClose>\n      <Button variant=\"primary\" onClick={sendInvite}>보내기</Button>\n    </DialogFooter>\n  </DialogContent>\n</Dialog>"
        },
        {
          "title": "controlled + 전체화면",
          "lang": "tsx",
          "code": "import { Dialog, DialogContent, DialogTitle } from \"@axe/ui\";\n\n<Dialog open={open} onOpenChange={setOpen}>\n  <DialogContent size=\"full\">\n    <DialogTitle>문서 뷰어</DialogTitle>\n    {/* … 전체화면 콘텐츠 … */}\n  </DialogContent>\n</Dialog>"
        },
        {
          "title": "CSS-only 정적 표면 (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-dialog__overlay\"></div>\n<div class=\"axe-dialog__content axe-dialog__content--sm\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"dlg-t\">\n  <div class=\"axe-dialog__header\">\n    <h2 id=\"dlg-t\" class=\"axe-dialog__title\">멤버 초대</h2>\n    <p class=\"axe-dialog__description\">초대 링크가 발송됩니다.</p>\n  </div>\n  <div class=\"axe-dialog__footer\">\n    <button class=\"axe-btn axe-btn--ghost\">취소</button>\n    <button class=\"axe-btn axe-btn--primary\">보내기</button>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "DialogTitle 을 항상 넣을 것(접근성 라벨) — 숨기려면 sr-only.",
          "닫기 경로(취소/X)를 명시적으로 제공하고 DialogClose 로 감쌀 것.",
          "파괴적 확인(삭제 등)에는 Dialog 대신 AlertDialog 를 쓸 것."
        ],
        "dont": [
          "size=full 을 작은 확인 모달에 쓰지 말 것 — 콘텐츠 밀도에 맞춰.",
          "DialogContent 안에 또 다른 포커스 트랩(중첩 모달)을 임의로 겹치지 말 것."
        ]
      },
      "consumeCss": "정적 열림 표면(오버레이+카드)은 .axe-dialog__* 마크업만으로 재현 가능하다. 다만 포털·포커스 트랩·Esc/바깥클릭 닫기·닫힘 애니메이션(data-state=closed)은 Radix 런타임이 만들므로, 실제 상호작용 다이얼로그는 React 컴포넌트로 소비해야 한다.",
      "url": "https://design.axelabs.ai/components/dialog"
    },
    {
      "name": "DocViz",
      "slug": "doc-viz",
      "group": "data",
      "summary": "인쇄 문서(.axe-doc)용 서버 SVG 데이터 비주얼 5종.",
      "description": "DocFigure(caption/note 래퍼) · DocBarChart(grouped bar) · DocLineChart(line, dashed·markers) · DocDonut(pathLength=100 도넛) · DocDiagram(수동 grid 투자구조도)로 구성된 컴포넌트군. 전부 sync 순수 함수이며 \"use client\" 가 없어 RSC 와 client island 양쪽에서 렌더된다(JS 0, 서버 SVG). 화면 대시보드 차트가 아니라 IC 메모·계약서 같은 종이 문서 전용 — 화면 차트는 chart/Chart.tsx(recharts)로 경계를 나눈다. resolved-spec 규약: 축 ticks·표시 문자열(₩60억, 15.8%)은 호출자가 미리 계산해 넘기며 함수 prop 은 받지 않는다. markup·수식이 _viz.html.jinja 매크로와 1:1 파리티(golden fixtures 로 검증).",
      "keywords": [
        "doc-viz",
        "docviz",
        "차트",
        "인쇄",
        "print",
        "svg",
        "bar chart",
        "line chart",
        "donut",
        "diagram",
        "투자구조도",
        "figure",
        "ic memo",
        "지분 구성",
        "server svg",
        "rsc"
      ],
      "props": [
        {
          "name": "title",
          "type": "string",
          "required": true,
          "description": "(공통) SVG <title> — 데이터 요약문(스크린리더 대체 텍스트)."
        },
        {
          "name": "id",
          "type": "string",
          "description": "(공통) 미지정 시 useId 기반 안전 id(safeAutoId) 자동 생성."
        },
        {
          "name": "caption",
          "type": "string",
          "description": "(공통) figcaption(.axe-doc-viz__caption)."
        },
        {
          "name": "width",
          "type": "number",
          "description": "(공통) viewBox 폭. 기본 bar/line 482, donut 231, diagram 은 grid 산출."
        },
        {
          "name": "height",
          "type": "number",
          "description": "(공통) viewBox 높이. 기본 bar/line 200, donut 170, diagram 은 grid 산출."
        },
        {
          "name": "srTable",
          "type": "boolean",
          "defaultValue": "true",
          "description": "(공통) 동일 데이터 스크린리더 표(.axe-doc__sr) 동봉. 인쇄에선 display:none."
        },
        {
          "name": "labels",
          "type": "string[]",
          "required": true,
          "description": "(Bar/Line) x 카테고리 라벨."
        },
        {
          "name": "series",
          "type": "DocBarSeries[] | DocLineSeries[]",
          "required": true,
          "description": "(Bar/Line) 시리즈 ≤6. DocLineSeries 는 dashed?·markers? 추가."
        },
        {
          "name": "ticks",
          "type": "number[]",
          "required": true,
          "description": "(Bar/Line) 축 눈금(resolved). 첫=축 최소, 마지막=축 최대. niceTicks 로 전처리 가능."
        },
        {
          "name": "tickLabels",
          "type": "string[]",
          "description": "(Bar/Line) 눈금 표시 문자열. 미지정 시 ticks 문자열화."
        },
        {
          "name": "valueLabels",
          "type": "string[][]",
          "description": "(Bar) 막대 위 표시 문자열 [series][category] — 포맷 함수 대체."
        },
        {
          "name": "slices",
          "type": "DocDonutSlice[]",
          "required": true,
          "description": "(Donut) { label, value, display? }. 마지막 슬라이스가 잔여로 합계 오차 흡수."
        },
        {
          "name": "center",
          "type": "{ value: string; label?: string }",
          "description": "(Donut) 중앙 강조 — value + 선택 label."
        },
        {
          "name": "nodes",
          "type": "DocDiagramNode[]",
          "required": true,
          "description": "(Diagram) { id, label, sub?, kind?, col, row }. kind = corp|fund|spc|person|target(형태 구분)."
        },
        {
          "name": "edges",
          "type": "DocDiagramEdge[]",
          "required": true,
          "description": "(Diagram) { from, to, label?, sub?, kind?, fromSide?, toSide?, offset? }. kind = equity|new|old|flow."
        },
        {
          "name": "cols",
          "type": "number",
          "required": true,
          "description": "(Diagram) 그리드 열 수."
        },
        {
          "name": "colW",
          "type": "number",
          "defaultValue": "150",
          "description": "(Diagram) 셀 폭(노드 120×44 가 중앙)."
        },
        {
          "name": "rowH",
          "type": "number",
          "defaultValue": "86",
          "description": "(Diagram) 셀 높이."
        },
        {
          "name": "note",
          "type": "string",
          "description": "(Figure) 표/그림 밖 각주 한 줄(.axe-doc__tnotes)."
        },
        {
          "name": "wide",
          "type": "boolean",
          "description": "(Figure) landscape 2-col 탈출(.axe-doc-viz--wide = column-span:all)."
        },
        {
          "name": "children",
          "type": "React.ReactNode",
          "required": true,
          "description": "(Figure) 래핑 대상(표·차트 2-up 등)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-doc-viz",
          "purpose": "모든 doc-viz figure 의 루트(break-inside:avoid).",
          "stability": "stable"
        },
        {
          "className": "axe-doc-viz--bar",
          "purpose": "DocBarChart 변형(루트). --line·--donut 은 series 색 채널 동형. DocDiagram 루트는 --diagram(4번째 변형)이나 스타일은 __node/__edge 가 담당(전용 --diagram CSS 규칙 없음).",
          "stability": "stable"
        },
        {
          "className": "axe-doc-viz--wide",
          "purpose": "landscape 2-col 탈출(column-span:all) — DocFigure wide.",
          "stability": "stable"
        },
        {
          "className": "axe-doc-viz__caption",
          "purpose": "figcaption 라벨.",
          "stability": "stable"
        },
        {
          "className": "axe-doc-viz__svg",
          "purpose": "차트 <svg>(role=img).",
          "stability": "stable"
        },
        {
          "className": "axe-doc-viz__series",
          "purpose": "SVG 시리즈 그룹. --1~--6 색 사다리 + --dashed(line 예측)로 currentColor 채널.",
          "stability": "stable"
        },
        {
          "className": "axe-doc-viz__key",
          "purpose": "범례 항목. --1~--6 이 series 와 같은 색 토큰 공유.",
          "stability": "stable"
        },
        {
          "className": "axe-doc-viz__legend",
          "purpose": "SVG 밖 HTML 범례 ul(스크린리더·한글 줄바꿈).",
          "stability": "stable"
        },
        {
          "className": "axe-doc-viz__slice",
          "purpose": "DocDonut 슬라이스 circle(stroke-dasharray 백분율).",
          "stability": "stable"
        },
        {
          "className": "axe-doc-viz__donut-value",
          "purpose": "도넛 중앙 강조 값. --donut-label 은 보조 라벨.",
          "stability": "stable"
        },
        {
          "className": "axe-doc-viz__grid",
          "purpose": "격자선 g(축·눈금·라벨과 함께 드로잉 세부).",
          "stability": "internal"
        },
        {
          "className": "axe-doc-viz__value",
          "purpose": "막대 위 값 텍스트(드로잉 세부).",
          "stability": "internal"
        },
        {
          "className": "axe-doc-diagram__node",
          "purpose": "다이어그램 노드 박스. --fund|spc|person|target 이 테두리 형태 구분.",
          "stability": "stable"
        },
        {
          "className": "axe-doc-diagram__node-label",
          "purpose": "노드 라벨 텍스트(--node-sub = 보조).",
          "stability": "stable"
        },
        {
          "className": "axe-doc-diagram__edge",
          "purpose": "엣지 그룹. --new(accent)·--old(파선)·--flow(점선)가 이중 부호.",
          "stability": "stable"
        },
        {
          "className": "axe-doc-diagram__edge-label",
          "purpose": "엣지 라벨 텍스트(halo, --edge-sub = 보조).",
          "stability": "stable"
        },
        {
          "className": "axe-doc-diagram__edge-arrow",
          "purpose": "화살촉 marker path(marker 자체에 edge 클래스 부여, 드로잉 세부).",
          "stability": "internal"
        },
        {
          "className": "axe-doc__sr",
          "purpose": "동일 데이터 스크린리더 표(off-screen, 인쇄 display:none).",
          "stability": "stable"
        },
        {
          "className": "axe-doc__tnotes",
          "purpose": "DocFigure note 각주 리스트.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "각 차트 <svg> = role=img + aria-labelledby → <title>(title prop 필수, 데이터 요약문). srTable(기본 true) 이 동일 수치를 off-screen <table>(.axe-doc__sr, scope 지정)로 동봉해 스크린리더가 실제 값에 접근한다.",
        "notes": "색맹·흑백 복사 안전을 위한 이중 부호: 노드 kind 는 색이 아닌 테두리 형태, edge old 는 파선, line 예측 시리즈는 dashed. 범례는 SVG 밖 HTML ul 로 두어 낭독 순서·한글 줄바꿈을 보장. 상호작용 없는 정적 인쇄 그래픽이라 키보드 처리는 없음."
      },
      "examples": [
        {
          "title": "DocBarChart (grouped bar)",
          "lang": "tsx",
          "code": "import { DocBarChart } from \"@axe/ui\";\n\n<DocBarChart\n  title=\"분기별 매출 (억원)\"\n  caption=\"그림 1. 분기 실적\"\n  labels={[\"1Q\", \"2Q\", \"3Q\", \"4Q\"]}\n  series={[\n    { label: \"2024\", values: [12, 18, 15, 22] },\n    { label: \"2025\", values: [16, 21, 19, 28] },\n  ]}\n  ticks={[0, 10, 20, 30]}\n/>"
        },
        {
          "title": "DocLineChart (실적 실선 + 전망 파선)",
          "lang": "tsx",
          "code": "import { DocLineChart, niceTicks } from \"@axe/ui\";\n\n<DocLineChart\n  title=\"AUM 추이 (억원)\"\n  labels={[\"1월\", \"2월\", \"3월\", \"4월\"]}\n  series={[\n    { label: \"실적\", values: [40, 44, 47, 52], markers: true },\n    { label: \"전망\", values: [52, 55, 58, 61], dashed: true },\n  ]}\n  ticks={niceTicks(61)}\n/>"
        },
        {
          "title": "DocDonut (지분 구성)",
          "lang": "tsx",
          "code": "import { DocDonut } from \"@axe/ui\";\n\n<DocDonut\n  title=\"지분 구성\"\n  slices={[\n    { label: \"AXE\", value: 15.8, display: \"15.8%\" },\n    { label: \"창업자\", value: 60, display: \"60.0%\" },\n    { label: \"기타\", value: 24.2, display: \"24.2%\" },\n  ]}\n  center={{ value: \"15.8%\", label: \"AXE 지분\" }}\n/>"
        },
        {
          "title": "DocDiagram (투자 구조)",
          "lang": "tsx",
          "code": "import { DocDiagram } from \"@axe/ui\";\n\n<DocDiagram\n  title=\"투자 구조\"\n  cols={2}\n  nodes={[\n    { id: \"fund\", label: \"AXE 2호 펀드\", kind: \"fund\", col: 0, row: 0 },\n    { id: \"spc\", label: \"투자목적 SPC\", kind: \"spc\", col: 1, row: 0 },\n    { id: \"target\", label: \"타겟社\", kind: \"target\", col: 0, row: 1 },\n  ]}\n  edges={[\n    { from: \"fund\", to: \"spc\", label: \"출자\", kind: \"flow\" },\n    { from: \"spc\", to: \"target\", label: \"지분 30%\", kind: \"new\" },\n  ]}\n/>"
        },
        {
          "title": "DocFigure (wide 래퍼 + note)",
          "lang": "tsx",
          "code": "import { DocFigure, DocBarChart } from \"@axe/ui\";\n\n<DocFigure caption=\"그림 1. 분기 실적\" note=\"단위: 억원 · 감사 전 수치\" wide>\n  <DocBarChart\n    title=\"분기별 매출\"\n    labels={[\"1Q\", \"2Q\", \"3Q\", \"4Q\"]}\n    series={[{ label: \"매출\", values: [12, 18, 15, 22] }]}\n    ticks={[0, 10, 20, 30]}\n  />\n</DocFigure>"
        }
      ],
      "bestPractices": {
        "do": [
          "ticks·표시 문자열(₩60억, 15.8%)은 호출자가 resolved-spec 으로 넘긴다(함수 prop 없음).",
          "앱 코드에서는 niceTicks() 로 축 눈금을 전처리하되 golden/parity 경로에서는 ticks 를 명시한다.",
          "title 은 그래픽을 대체하는 완결된 요약문으로 쓴다(스크린리더가 이걸 읽음).",
          "예측·2nd 시나리오는 dashed, 노드 종류는 kind 로 이중 부호화(흑백 복사 대비)."
        ],
        "dont": [
          "화면 대시보드에 쓰지 말 것 — 그건 chart/Chart.tsx(recharts). doc-viz 는 .axe-doc 인쇄 전용.",
          "특별한 이유 없이 srTable=false 로 스크린리더 표를 끄지 말 것(수치 접근성 상실).",
          "SVG 에 색 attribute 를 직접 넣지 말 것 — 색은 series/key 클래스의 currentColor 단일 채널.",
          "시리즈 7개 이상 넣지 말 것(색 사다리 --1~--6 한계)."
        ]
      },
      "consumeCss": "비-React 소비 채널은 서버측 _viz.html.jinja 매크로(bar_chart/line_chart/donut/diagram)로, 본 컴포넌트와 markup·수식이 1:1 파리티(golden fixtures 검증). 손 HTML 로 SVG 를 재현하기보다 컴포넌트나 jinja 매크로를 쓴다. 색은 --doc-viz-1…6 토큰 + __series--N/__key--N currentColor 로만 지정한다.",
      "url": "https://design.axelabs.ai/components/doc-viz"
    },
    {
      "name": "DropdownMenu",
      "slug": "dropdown-menu",
      "group": "overlay",
      "summary": "버튼 클릭으로 여는 메뉴 — 액션·토글·라디오·단축키를 한 표면에 묶는다 (Radix DropdownMenu 기반).",
      "description": "Popover 와 모양은 비슷하나 *메뉴 의미*(role=menu/menuitem, 항목 highlighted, 화살표 키 내비게이션)를 가진다. Radix DropdownMenu 위에 .axe-dropdown-menu__* 스타일만 입혔고, 위치 계산·키보드·type-ahead·focus 복원은 Radix 가 처리한다. DropdownMenu(Root)·DropdownMenuTrigger·DropdownMenuContent·DropdownMenuItem 를 기본 골격으로, Label·Separator·Shortcut·CheckboxItem·RadioGroup/RadioItem·Sub(SubTrigger/SubContent)·Arrow 를 조합해 쓴다.",
      "keywords": [
        "dropdown",
        "menu",
        "드롭다운",
        "메뉴",
        "액션 메뉴",
        "context actions",
        "kebab",
        "radix"
      ],
      "props": [
        {
          "name": "open",
          "type": "boolean",
          "description": "열림 상태 (controlled). Radix Root prop."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "description": "uncontrolled 초기 열림 상태 (Root)."
        },
        {
          "name": "onOpenChange",
          "type": "(open: boolean) => void",
          "description": "열림/닫힘 콜백 (Root)."
        },
        {
          "name": "modal",
          "type": "boolean",
          "defaultValue": "true",
          "description": "true 면 바깥 스크롤/포커스를 잠금 (Root)."
        },
        {
          "name": "sideOffset",
          "type": "number",
          "defaultValue": "6",
          "description": "DropdownMenuContent 와 트리거 사이 간격(px)."
        },
        {
          "name": "withPortal",
          "type": "boolean",
          "defaultValue": "true",
          "description": "DropdownMenuContent 를 portal 로 띄울지 — overflow:hidden 부모 탈출."
        },
        {
          "name": "variant",
          "type": "\"default\" | \"danger\"",
          "defaultValue": "\"default\"",
          "description": "DropdownMenuItem 톤. danger=파괴적 액션(삭제·로그아웃)의 텍스트/highlighted bg 를 danger 로."
        },
        {
          "name": "icon",
          "type": "React.ReactNode",
          "description": "DropdownMenuItem 좌측 아이콘 슬롯 (선택)."
        },
        {
          "name": "...rest",
          "type": "각 Radix DropdownMenu.* props",
          "description": "Trigger(asChild)·Content(side/align/collisionPadding)·Item(onSelect/disabled)·CheckboxItem/RadioItem(checked/value/onCheckedChange) 등 각 파트는 대응 Radix 컴포넌트 props 를 그대로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-dropdown-menu__content",
          "purpose": "포털로 뜨는 메뉴 패널 표면(elevated bg·border·shadow).",
          "stability": "stable"
        },
        {
          "className": "axe-dropdown-menu__sub-content",
          "purpose": "서브메뉴 패널 — 같은 표면 + 살짝 가벼운 그림자.",
          "stability": "stable"
        },
        {
          "className": "axe-dropdown-menu__item",
          "purpose": "메뉴 항목 row(좌 indicator/icon · 중 label · 우 shortcut).",
          "stability": "stable"
        },
        {
          "className": "axe-dropdown-menu__item--danger",
          "purpose": "danger 변형 항목(텍스트+highlighted bg 가 danger 톤).",
          "stability": "stable"
        },
        {
          "className": "axe-dropdown-menu__label",
          "purpose": "그룹 제목(대문자 트래킹, tertiary 색).",
          "stability": "stable"
        },
        {
          "className": "axe-dropdown-menu__separator",
          "purpose": "그룹 구분선(1px border-subtle).",
          "stability": "stable"
        },
        {
          "className": "axe-dropdown-menu__shortcut",
          "purpose": "우측 정렬 단축키 힌트 슬롯(Kbd 래핑).",
          "stability": "stable"
        },
        {
          "className": "axe-dropdown-menu__arrow",
          "purpose": "트리거를 가리키는 화살표(Radix Arrow).",
          "stability": "stable"
        },
        {
          "className": "axe-dropdown-menu__item--checkbox",
          "purpose": "checkbox 항목 — indicator 자리 확보 layout 변형(자동 적용).",
          "stability": "internal"
        },
        {
          "className": "axe-dropdown-menu__item--radio",
          "purpose": "radio 항목 layout 변형(자동 적용).",
          "stability": "internal"
        },
        {
          "className": "axe-dropdown-menu__item--sub-trigger",
          "purpose": "서브메뉴 여는 항목 — 우측 chevron 포함 layout(자동 적용).",
          "stability": "internal"
        },
        {
          "className": "axe-dropdown-menu__item-indicator",
          "purpose": "체크/도트 표시 슬롯(Radix ItemIndicator).",
          "stability": "internal"
        },
        {
          "className": "axe-dropdown-menu__item-icon",
          "purpose": "항목 좌측 아이콘 슬롯 span.",
          "stability": "internal"
        },
        {
          "className": "axe-dropdown-menu__item-label",
          "purpose": "항목 가운데 라벨 span.",
          "stability": "internal"
        },
        {
          "className": "axe-dropdown-menu__sub-chevron",
          "purpose": "sub-trigger 우측 chevron 아이콘.",
          "stability": "internal"
        }
      ],
      "a11y": {
        "keyboard": [
          "트리거에서 Space/Enter/↓ 로 열기",
          "↑/↓ 로 항목 이동, Home/End 로 처음/끝",
          "글자 입력 시 type-ahead 로 해당 항목 점프",
          "→ 로 서브메뉴 열기, ← 로 닫고 상위로",
          "Enter/Space 로 항목 실행, Esc 로 닫기(트리거로 포커스 복원)"
        ],
        "aria": "Radix 가 트리거에 aria-haspopup=menu·aria-expanded 를, 콘텐츠에 role=menu, 각 항목에 role=menuitem(체크박스=menuitemcheckbox+aria-checked, 라디오=menuitemradio)을 부여. 현재 항목은 data-highlighted, 비활성은 data-disabled 로 표시.",
        "notes": "modal 기본(바깥 클릭·스크롤 잠금). 열림 시 첫 항목으로 focus, 닫힘 시 트리거로 복원. prefers-reduced-motion 시 open/close 애니메이션 제거."
      },
      "examples": [
        {
          "title": "기본 (라벨 · 단축키 · 구분선 · danger)",
          "lang": "tsx",
          "code": "import {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuLabel,\n  DropdownMenuItem,\n  DropdownMenuShortcut,\n  DropdownMenuSeparator,\n} from \"@axe/ui\";\nimport { Button } from \"@axe/ui\";\n\n<DropdownMenu>\n  <DropdownMenuTrigger asChild>\n    <Button variant=\"secondary\">옵션</Button>\n  </DropdownMenuTrigger>\n  <DropdownMenuContent>\n    <DropdownMenuLabel>액션</DropdownMenuLabel>\n    <DropdownMenuItem onSelect={createNew}>\n      새 항목 <DropdownMenuShortcut>⌘N</DropdownMenuShortcut>\n    </DropdownMenuItem>\n    <DropdownMenuItem onSelect={rename}>이름 변경</DropdownMenuItem>\n    <DropdownMenuSeparator />\n    <DropdownMenuItem variant=\"danger\" onSelect={remove}>삭제</DropdownMenuItem>\n  </DropdownMenuContent>\n</DropdownMenu>"
        },
        {
          "title": "체크박스 · 라디오 · 서브메뉴",
          "lang": "tsx",
          "code": "import {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n} from \"@axe/ui\";\nimport { Button } from \"@axe/ui\";\n\n<DropdownMenu>\n  <DropdownMenuTrigger asChild>\n    <Button>보기</Button>\n  </DropdownMenuTrigger>\n  <DropdownMenuContent>\n    <DropdownMenuCheckboxItem checked={grid} onCheckedChange={setGrid}>\n      그리드\n    </DropdownMenuCheckboxItem>\n    <DropdownMenuSeparator />\n    <DropdownMenuRadioGroup value={sort} onValueChange={setSort}>\n      <DropdownMenuRadioItem value=\"name\">이름순</DropdownMenuRadioItem>\n      <DropdownMenuRadioItem value=\"date\">날짜순</DropdownMenuRadioItem>\n    </DropdownMenuRadioGroup>\n    <DropdownMenuSeparator />\n    <DropdownMenuSub>\n      <DropdownMenuSubTrigger>내보내기</DropdownMenuSubTrigger>\n      <DropdownMenuSubContent>\n        <DropdownMenuItem onSelect={exportCsv}>CSV</DropdownMenuItem>\n        <DropdownMenuItem onSelect={exportPdf}>PDF</DropdownMenuItem>\n      </DropdownMenuSubContent>\n    </DropdownMenuSub>\n  </DropdownMenuContent>\n</DropdownMenu>"
        }
      ],
      "bestPractices": {
        "do": [
          "트리거는 asChild 로 Button 등 실제 인터랙티브 요소에 씌운다.",
          "파괴적 항목은 variant=\"danger\" + 구분선으로 안전 항목과 분리.",
          "단축키가 있으면 DropdownMenuShortcut 으로 우측에 힌트."
        ],
        "dont": [
          "메뉴 안에 폼 입력·긴 인터랙티브 컨텐츠를 넣지 말 것 — 그 자리는 Popover/Dialog.",
          "아이콘만 있는 트리거에 접근 가능한 이름(aria-label)을 빠뜨리지 말 것."
        ]
      },
      "consumeCss": "위치 계산·열림/닫힘·항목 하이라이트가 Radix 런타임(data-state, data-highlighted)에 의존해 CSS-only 로는 동작하는 메뉴를 재현할 수 없다. .axe-dropdown-menu__* 토큰은 정적 표면 렌더에 노출되나 실제 소비는 React 컴포넌트를 통해서 한다. ContextMenu 가 동일한 이 표면 클래스를 공유한다.",
      "url": "https://design.axelabs.ai/components/dropdown-menu"
    },
    {
      "name": "EmptyState",
      "slug": "empty-state",
      "group": "feedback",
      "summary": "\"데이터/결과 없음\" 상태의 표준 placeholder — icon·title·description·action 4파트.",
      "description": "list/table/result 화면이 \"아직 없음\" 또는 \"찾을 수 없음\"을 친절히 안내하는 그릇. 중앙 정렬 컬럼에 dashed 테두리, 배경은 transparent 라 Card 안/밖 어디에 넣어도 자연스럽다. size 가 padding·아이콘 박스·폰트 비례를, tone 이 아이콘/테두리 tint(semantic)를 결정한다. action/secondaryAction 으로 \"다음 행동\"을 한 클릭으로 유도한다.",
      "keywords": [
        "empty state",
        "empty-state",
        "빈 상태",
        "placeholder",
        "no data",
        "no results",
        "zero state",
        "결과 없음"
      ],
      "props": [
        {
          "name": "title",
          "type": "string",
          "required": true,
          "description": "한 줄 제목(`.axe-empty-state__title`). ReactNode 가 아닌 문자열."
        },
        {
          "name": "icon",
          "type": "React.ReactNode",
          "description": "중앙 상단 아이콘(emoji/svg/컴포넌트). 미지정 시 아이콘 슬롯 자체가 렌더되지 않는다."
        },
        {
          "name": "description",
          "type": "React.ReactNode",
          "description": "보조 설명(`.axe-empty-state__description`, max 48ch)."
        },
        {
          "name": "action",
          "type": "React.ReactNode",
          "description": "1차 행동 — 보통 <Button variant=\"primary\">."
        },
        {
          "name": "secondaryAction",
          "type": "React.ReactNode",
          "description": "2차 행동 — link 또는 ghost 버튼. action 옆에 나란히 렌더."
        },
        {
          "name": "size",
          "type": "\"sm\" | \"md\" | \"lg\"",
          "defaultValue": "\"md\"",
          "description": "padding·아이콘 박스·폰트 크기 비례."
        },
        {
          "name": "tone",
          "type": "\"default\" | \"success\" | \"warning\" | \"danger\"",
          "defaultValue": "\"default\"",
          "description": "아이콘 tint + subtle 테두리 accent. default 는 추가 클래스 없이 text-muted."
        },
        {
          "name": "announce",
          "type": "boolean",
          "defaultValue": "false",
          "description": "true 면 role=\"status\" + aria-live=\"polite\" 를 부여해 SR 에 자동 발화. 비동기로 빈 결과가 생길 때(예: 검색 0건)만 켠다."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLDivElement>, \"title\">",
          "description": "className 병합, 나머지는 루트 div 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-empty-state",
          "purpose": "루트 div — 중앙 정렬 컬럼·dashed 테두리·transparent 배경.",
          "stability": "stable"
        },
        {
          "className": "axe-empty-state__icon",
          "purpose": "원형 아이콘 박스(subtle 배경, 크기는 size 에 비례).",
          "stability": "stable"
        },
        {
          "className": "axe-empty-state__title",
          "purpose": "제목 — display 폰트.",
          "stability": "stable"
        },
        {
          "className": "axe-empty-state__description",
          "purpose": "설명 문단(secondary·max 48ch).",
          "stability": "stable"
        },
        {
          "className": "axe-empty-state__actions",
          "purpose": "action + secondaryAction row(flex wrap, 중앙).",
          "stability": "stable"
        },
        {
          "className": "axe-empty-state--size-sm",
          "purpose": "작은 크기 — padding·아이콘 36px·폰트 축소.",
          "stability": "stable"
        },
        {
          "className": "axe-empty-state--size-lg",
          "purpose": "큰 크기 — padding·아이콘 64px·폰트 확대(md 는 기본, 클래스 없음).",
          "stability": "stable"
        },
        {
          "className": "axe-empty-state--tone-success",
          "purpose": "success tone — 아이콘 success-soft/success + 테두리 accent.",
          "stability": "stable"
        },
        {
          "className": "axe-empty-state--tone-warning",
          "purpose": "warning tone — 아이콘 warning tint + 테두리 accent.",
          "stability": "stable"
        },
        {
          "className": "axe-empty-state--tone-danger",
          "purpose": "danger tone — 아이콘 danger tint + 테두리 accent(default 는 클래스 없음).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "기본은 시맨틱 중립 placeholder(정적 mount 마다 읽히는 noise 회피). announce=true 일 때만 role=\"status\" + aria-live=\"polite\" 로 SR 통지. 아이콘은 `aria-hidden`.",
        "notes": "title 은 heading 이 아니라 div — 화면 맥락상 제목이 필요하면 상위에서 heading 을 제공한다. announce 는 비동기 빈 결과 통지에만(정적 placeholder 에 켜면 매 mount 발화되는 noise)."
      },
      "examples": [
        {
          "title": "행동 유도가 있는 빈 상태",
          "lang": "tsx",
          "code": "import { EmptyState, Button } from \"@axe/ui\";\n\n<EmptyState\n  icon=\"📭\"\n  title=\"아직 딜이 없습니다\"\n  description=\"첫 번째 딜을 소싱하면 여기에 표시됩니다.\"\n  action={<Button variant=\"primary\">딜 추가</Button>}\n/>"
        },
        {
          "title": "검색 0건 통지(announce)",
          "lang": "tsx",
          "code": "import { EmptyState } from \"@axe/ui\";\n\n<EmptyState\n  size=\"sm\"\n  title=\"검색 결과가 없습니다\"\n  description=\"다른 키워드로 다시 시도해 보세요.\"\n  announce\n/>"
        },
        {
          "title": "CSS-only (비-React, warning tone)",
          "lang": "html",
          "code": "<div class=\"axe-empty-state axe-empty-state--size-lg axe-empty-state--tone-warning\">\n  <div class=\"axe-empty-state__icon\" aria-hidden>⚠</div>\n  <div class=\"axe-empty-state__title\">권한이 없습니다</div>\n  <div class=\"axe-empty-state__description\">관리자에게 접근을 요청하세요.</div>\n  <div class=\"axe-empty-state__actions\">\n    <button class=\"axe-btn axe-btn--secondary\">요청 보내기</button>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "\"다음 행동\"이 있으면 action 으로 한 클릭에 연결한다.",
          "정적 placeholder 는 announce 를 끄고, 비동기 빈 결과에만 켠다."
        ],
        "dont": [
          "tone 을 장식으로 남발하지 말 것 — 실제 semantic(성공/경고/위험)일 때만.",
          "긴 안내문을 description 에 몰아넣지 말 것(한두 문장)."
        ]
      },
      "consumeCss": "순수 CSS 로 재현 가능(announce 만 React 에서 role/aria-live 부여 — CSS-only 마크업에서는 직접 role=\"status\"를 부여). components.css가 feedback.css를 자동 import하므로 표준 진입점 하나면 스타일이 포함된다.",
      "url": "https://design.axelabs.ai/components/empty-state"
    },
    {
      "name": "ErrorBoundary",
      "slug": "error-boundary",
      "group": "feedback",
      "summary": "렌더 중 throw 를 격리·복구하는 React 에러 경계.",
      "description": "자식 트리의 렌더 도중 발생한 throw 를 잡아 트리 전체 white-out 대신 복구 가능한 fallback 을 보여주는 class 컴포넌트. 기본 fallback 은 잡은 error 를 humanizeApiError 로 {title, detail} 로 변환해 Callout(variant=\"danger\") + '다시 시도' Button 으로 렌더한다(한국어 사용자 메시지). fallback prop 으로 직접 렌더를 대체하고, onError 로 Sentry/console/datadog 로깅을 붙이며, resetKeys 원소가 바뀌면(예 pathname·query·userId) 자동으로 reset 된다. getDerivedStateFromError/componentDidCatch 는 hooks 로 대체 불가라 class + \"use client\" 로 구현.",
      "keywords": [
        "error boundary",
        "에러 경계",
        "fallback",
        "복구",
        "retry",
        "다시 시도",
        "componentDidCatch",
        "resetKeys",
        "humanizeApiError",
        "white-out",
        "안전망"
      ],
      "props": [
        {
          "name": "children",
          "type": "React.ReactNode",
          "required": true,
          "description": "감쌀 트리. 이 안의 렌더 throw 만 포착된다."
        },
        {
          "name": "fallback",
          "type": "(error: Error, reset: () => void) => React.ReactNode",
          "description": "받으면 기본 Callout fallback 을 대체. reset 을 호출하면 boundary 가 복구."
        },
        {
          "name": "onError",
          "type": "(error: Error, errorInfo: React.ErrorInfo) => void",
          "description": "외부 로깅 hook — componentDidCatch 에서 호출(Sentry/console/datadog)."
        },
        {
          "name": "resetKeys",
          "type": "unknown[]",
          "description": "원소 중 하나라도 Object.is 로 바뀌면 자동 reset(라우트/쿼리 변경 패턴)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-error-boundary",
          "purpose": "기본 fallback wrapper(spacing 컬럼). custom fallback 시엔 렌더 안 됨.",
          "stability": "stable"
        },
        {
          "className": "axe-error-boundary__detail",
          "purpose": "humanizeApiError 의 detail 문단(기본 fallback 내부).",
          "stability": "internal"
        },
        {
          "className": "axe-error-boundary__actions",
          "purpose": "'다시 시도' 액션 row(기본 fallback 내부).",
          "stability": "internal"
        }
      ],
      "a11y": {
        "keyboard": [
          "기본 fallback 의 '다시 시도' 버튼 — Enter/Space(네이티브 <button>)"
        ],
        "aria": "boundary 자체는 role 이 없다(순수 로직). 기본 fallback 의 시맨틱은 Callout(variant=\"danger\")과 Button 에 위임된다. custom fallback 을 쓰면 접근성 표지는 caller 책임.",
        "notes": "componentDidCatch → onError 로 오류를 흘리고, resetKeys 원소가 Object.is 로 바뀌면 componentDidUpdate 가 자동 복구한다. 포착 범위는 React 에러 경계 한계와 동일 — 렌더/라이프사이클 중 throw 만 잡고, 이벤트 핸들러·비동기·SSR 밖 오류는 잡지 못한다."
      },
      "examples": [
        {
          "title": "기본 (로깅 + 라우트 기준 자동 복구)",
          "lang": "tsx",
          "code": "import { ErrorBoundary } from \"@axe/ui\";\nimport { usePathname } from \"next/navigation\";\n\nfunction Page({ children }: { children: React.ReactNode }) {\n  const pathname = usePathname();\n  return (\n    <ErrorBoundary\n      resetKeys={[pathname]}\n      onError={(error, info) => reportToSentry(error, info)}\n    >\n      {children}\n    </ErrorBoundary>\n  );\n}"
        },
        {
          "title": "custom fallback",
          "lang": "tsx",
          "code": "import { ErrorBoundary } from \"@axe/ui\";\n\n<ErrorBoundary\n  fallback={(error, reset) => (\n    <div role=\"alert\">\n      <p>차트를 그리지 못했어요: {error.message}</p>\n      <button onClick={reset}>다시 시도</button>\n    </div>\n  )}\n>\n  <StreamingChart />\n</ErrorBoundary>"
        }
      ],
      "bestPractices": {
        "do": [
          "LLM 응답·streaming·외부 tool 결과처럼 render throw 가능성이 높은 island 를 개별로 감싸 격리한다.",
          "resetKeys 에 pathname·query·userId 등을 넣어 새 입력 시 자동 복구되게 한다.",
          "onError 로 반드시 로깅을 붙여 삼켜진 에러가 관측 가능하게 한다."
        ],
        "dont": [
          "이벤트 핸들러·비동기 코드의 에러를 잡을 거라 기대하지 말 것(그건 try/catch 몫 — 경계 밖).",
          "앱 최상단 하나만 두지 말 것 — 한 곳이 터지면 전체가 white-out 되므로 경계를 잘게 나눈다."
        ]
      },
      "consumeCss": "에러 포착·복구는 순수 React 로직이라 CSS-only 로 소비할 수 없다. .axe-error-boundary* 클래스는 기본 fallback 의 얇은 스킨일 뿐이며, custom fallback 을 넘기면 전혀 렌더되지 않는다. 시각을 바꾸려면 fallback prop 으로 직접 렌더하는 편이 정석.",
      "url": "https://design.axelabs.ai/components/error-boundary"
    },
    {
      "name": "Footer",
      "slug": "footer",
      "group": "layout",
      "summary": "페이지 하단 — 로고 + 링크 컬럼 + 소셜 + 하단 바.",
      "description": "TopNav 의 페어 컴포넌트로, 미니멀(하단 바만)부터 풀(로고 + 링크 컬럼 + 소셜 + 하단 바)까지 같은 컴포넌트로 커버한다. logo·columns 가 상단 그리드(__top)를, bottom·social 이 하단 바(__bottom)를 이루며 둘 다 내용이 있을 때만 렌더된다. columns/social 은 데이터 배열(FooterColumn/FooterSocial)로 넘기고, top 과 bottom 사이 임의 슬롯은 children 으로 끼운다. AppShell 의 footer 슬롯에 하나 두는 것을 전제로 한 forwardRef `<footer>` 다.",
      "keywords": [
        "footer",
        "푸터",
        "하단",
        "columns",
        "social",
        "copyright",
        "저작권",
        "sitemap",
        "layout"
      ],
      "props": [
        {
          "name": "logo",
          "type": "React.ReactNode",
          "description": "좌상단 로고 슬롯(__logo). 보통 <Logo /> 를 넣는다."
        },
        {
          "name": "columns",
          "type": "FooterColumn[]",
          "description": "링크 컬럼 배열. FooterColumn = { title: string; items: FooterLink[] }, FooterLink = { href; label; external? }. external:true 면 링크가 새 탭(target=_blank, rel=noopener noreferrer)."
        },
        {
          "name": "bottom",
          "type": "React.ReactNode",
          "description": "하단 바 좌측 슬롯. 저작권·version·build 등. (social 은 우측.)"
        },
        {
          "name": "social",
          "type": "FooterSocial[]",
          "description": "하단 우측 소셜 링크 배열. FooterSocial = { href; label; icon? }. icon 없으면 label 텍스트 표시, 링크는 항상 새 탭 + aria-label=label."
        },
        {
          "name": "children",
          "type": "React.ReactNode",
          "description": "__top 뒤·__bottom 앞에 렌더되는 임의 커스텀 슬롯."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLElement>",
          "description": "className(axe-footer 뒤 병합) 등 나머지는 루트 <footer> 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-footer",
          "purpose": "푸터 루트(<footer>). 세로 스택 + 상단 보더.",
          "stability": "stable"
        },
        {
          "className": "axe-footer__top",
          "purpose": "상단 영역 그리드(로고 | 컬럼). ≥768px 에서 2-트랙.",
          "stability": "stable"
        },
        {
          "className": "axe-footer__logo",
          "purpose": "좌상단 로고 슬롯 래퍼.",
          "stability": "stable"
        },
        {
          "className": "axe-footer__columns",
          "purpose": "링크 컬럼들의 auto-fit 그리드.",
          "stability": "stable"
        },
        {
          "className": "axe-footer__column",
          "purpose": "링크 컬럼 하나(제목 + 항목).",
          "stability": "stable"
        },
        {
          "className": "axe-footer__column-title",
          "purpose": "컬럼 제목(uppercase, 작은 라벨).",
          "stability": "stable"
        },
        {
          "className": "axe-footer__column-items",
          "purpose": "컬럼 링크 목록 <ul>(list-style 제거).",
          "stability": "stable"
        },
        {
          "className": "axe-footer__link",
          "purpose": "컬럼 링크 <a>(hover 시 text-primary).",
          "stability": "stable"
        },
        {
          "className": "axe-footer__bottom",
          "purpose": "하단 바(좌 bottom · 우 social, 상단 보더).",
          "stability": "stable"
        },
        {
          "className": "axe-footer__bottom-left",
          "purpose": "하단 바 좌측 슬롯(저작권 등).",
          "stability": "stable"
        },
        {
          "className": "axe-footer__social",
          "purpose": "소셜 링크들의 가로 배치.",
          "stability": "stable"
        },
        {
          "className": "axe-footer__social-link",
          "purpose": "소셜 아이콘 링크(28×28 정사각, hover 배경).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Tab 으로 링크 순회",
          "Enter 로 이동(네이티브 앵커)"
        ],
        "aria": "루트가 <footer> — 페이지 직계일 때 contentinfo 랜드마크가 된다. 소셜 링크는 아이콘일 수 있어 각 <a> 에 aria-label={label} 을 부여해 접근가능 이름을 보장한다.",
        "notes": "external:true 컬럼 링크와 모든 소셜 링크는 target=_blank + rel=noopener noreferrer 로 새 탭에서 열린다. contentinfo 랜드마크는 페이지당 하나가 권장이므로 AppShell footer 슬롯에 Footer 하나만 둘 것."
      },
      "examples": [
        {
          "title": "풀 (로고 + 컬럼 + 소셜 + 하단)",
          "lang": "tsx",
          "code": "import { Footer, Logo } from \"@axe/ui\";\n\n<Footer\n  logo={<Logo />}\n  columns={[\n    { title: \"제품\", items: [\n      { href: \"/frame\", label: \"frame\" },\n      { href: \"/hive\", label: \"hive\" },\n    ]},\n    { title: \"회사\", items: [\n      { href: \"https://axelabs.ai\", label: \"axelabs.ai\", external: true },\n    ]},\n  ]}\n  social={[\n    { href: \"https://github.com/axelabs-ai\", label: \"GitHub\" },\n  ]}\n  bottom={<span>© 2026 AXE Labs · v0.5.0</span>}\n/>"
        },
        {
          "title": "미니멀 (하단 바만)",
          "lang": "tsx",
          "code": "import { Footer } from \"@axe/ui\";\n\n<Footer bottom={<span>© 2026 AXE Labs</span>} />"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<footer class=\"axe-footer\">\n  <div class=\"axe-footer__top\">\n    <div class=\"axe-footer__logo\">\n      <a class=\"axe-logo\" href=\"/\">AXE Labs</a>\n    </div>\n    <div class=\"axe-footer__columns\">\n      <div class=\"axe-footer__column\">\n        <div class=\"axe-footer__column-title\">제품</div>\n        <ul class=\"axe-footer__column-items\">\n          <li><a class=\"axe-footer__link\" href=\"/frame\">frame</a></li>\n          <li><a class=\"axe-footer__link\" href=\"/hive\">hive</a></li>\n        </ul>\n      </div>\n    </div>\n  </div>\n  <div class=\"axe-footer__bottom\">\n    <div class=\"axe-footer__bottom-left\">© 2026 AXE Labs</div>\n    <div class=\"axe-footer__social\">\n      <a class=\"axe-footer__social-link\"\n         href=\"https://github.com/axelabs-ai\"\n         aria-label=\"GitHub\" target=\"_blank\" rel=\"noopener noreferrer\">GH</a>\n    </div>\n  </div>\n</footer>"
        }
      ],
      "bestPractices": {
        "do": [
          "페이지당 하나, AppShell 의 footer 슬롯에 배치.",
          "외부 링크는 external:true 로 넘겨 rel=noopener 를 자동 부여."
        ],
        "dont": [
          "소셜 링크의 label 을 생략하지 말 것 — 아이콘일 때 접근가능 이름의 원천.",
          "여러 <footer> contentinfo 랜드마크를 중복 배치하지 말 것."
        ]
      },
      "consumeCss": "composite 이지만 Radix 상태가 없어 완전 CSS-only 재현 가능. 위 마크업 구조 그대로면 동일 레이아웃을 얻는다. 단 external 링크의 target/rel 과 소셜 링크의 aria-label 은 컴포넌트가 자동으로 넣던 것이므로 CSS-only 소비 시 마크업에 직접 기입할 것.",
      "url": "https://design.axelabs.ai/components/footer"
    },
    {
      "name": "FormField",
      "slug": "form",
      "group": "input",
      "summary": "Label · hint · error · 필수(*) 마크를 묶는 폼 필드 컴포지션 래퍼.",
      "description": "FormField 는 controlled value 를 갖지 않는 순수 컴포지션 래퍼로, 라벨/보조설명/에러/필수표시를 axe-form-field 레이아웃으로 감싼다. 자식이 단일 valid element 면 aria-describedby(hint/error id)와 aria-invalid(error 시)를 자동 주입하되, 소비자가 직접 준 aria-invalid 는 그대로 이기고 직접 준 aria-describedby 는 자동 id 와 병합된다. htmlFor 는 필수이며 자식 control 의 id 와 매칭해 클릭-포커스와 a11y 를 성립시킨다. 함께 export 되는 Label 은 Radix Label 래퍼로 단독으로도 쓴다.",
      "keywords": [
        "form",
        "form-field",
        "폼",
        "필드",
        "label",
        "라벨",
        "hint",
        "error",
        "에러",
        "required"
      ],
      "props": [
        {
          "name": "label",
          "type": "React.ReactNode",
          "required": true,
          "description": "(FormField) 라벨 텍스트/노드 (string 외 노드 가능)."
        },
        {
          "name": "htmlFor",
          "type": "string",
          "required": true,
          "description": "(FormField) 자식 control 의 id — Label 의 htmlFor 와 매칭 (필수)."
        },
        {
          "name": "hint",
          "type": "React.ReactNode",
          "description": "(FormField) 보조 설명 (작은 회색, error 가 있으면 가려짐)."
        },
        {
          "name": "error",
          "type": "React.ReactNode",
          "description": "(FormField) 에러 메시지 — 있으면 hint 자리 대체, role=\"alert\", danger 색."
        },
        {
          "name": "required",
          "type": "boolean",
          "description": "(FormField) 우상단 (*) 마크 표시 — 시각 표시만 (자식 control 의 required 와는 별개)."
        },
        {
          "name": "children",
          "type": "React.ReactNode",
          "required": true,
          "description": "(FormField) 감쌀 control — 단일 valid element 면 aria-describedby/aria-invalid 자동 wiring."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLDivElement>",
          "description": "(FormField) className 은 axe-form-field 와 병합, 나머지는 루트 <div> 로 전파."
        },
        {
          "name": "htmlFor (Label)",
          "type": "string",
          "description": "(Label) 연결할 control 의 id — click-to-focus."
        },
        {
          "name": "...rest (Label)",
          "type": "React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>",
          "description": "(Label) className 은 axe-label 과 병합, 나머지 Radix Label props 를 <label> 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-label",
          "purpose": "라벨 루트 (Radix Label, cursor:pointer, user-select:none).",
          "stability": "stable"
        },
        {
          "className": "axe-form-field",
          "purpose": "필드 래퍼 (flex column, gap).",
          "stability": "stable"
        },
        {
          "className": "axe-form-field--error",
          "purpose": "에러 상태 modifier — 내부 Input/Select/Checkbox/Radio/Switch 에 빨간 테두리를 전파.",
          "stability": "stable"
        },
        {
          "className": "axe-form-field__label",
          "purpose": "필드의 Label 파트 (axe-label 상속).",
          "stability": "stable"
        },
        {
          "className": "axe-form-field__required",
          "purpose": "필수(*) 마크 (danger 색, aria-hidden).",
          "stability": "stable"
        },
        {
          "className": "axe-form-field__control",
          "purpose": "control 래퍼 (Input/Select/Checkbox 등이 들어옴).",
          "stability": "stable"
        },
        {
          "className": "axe-form-field__hint",
          "purpose": "보조 설명 텍스트 (text-xs, tertiary).",
          "stability": "stable"
        },
        {
          "className": "axe-form-field__error",
          "purpose": " 에러 텍스트 (text-xs, danger, role=\"alert\").",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "라벨 클릭 시 htmlFor 로 연결된 control 로 포커스 이동"
        ],
        "aria": "에러 메시지는 role=\"alert\" 로 즉시 안내됨. 단일 element 자식엔 aria-describedby(hint/error) 와 aria-invalid(error 시)가 자동 연결됨. (*) 마크는 aria-hidden 이라 스크린리더가 읽지 않음.",
        "notes": "Fragment 자식은 cloneElement 대상이 아니라 aria 자동 wiring 이 안 됨 — 단일 element 로 감싸거나 수동 지정. 실제 필수 검증은 control 의 required 로 (required prop 은 시각 표시). Radix Label 이 텍스트 선택 중 더블클릭 오포커스를 막음."
      },
      "examples": [
        {
          "title": "hint + required",
          "lang": "tsx",
          "code": "import { FormField, Input } from \"@axe/ui\";\n\n<FormField label=\"이메일\" htmlFor=\"email\" hint=\"회사 메일을 사용하세요\" required>\n  <Input id=\"email\" type=\"email\" />\n</FormField>"
        },
        {
          "title": "error",
          "lang": "tsx",
          "code": "import { FormField, Input } from \"@axe/ui\";\n\n<FormField label=\"비밀번호\" htmlFor=\"pw\" error=\"8자 이상이어야 합니다\">\n  <Input id=\"pw\" type=\"password\" />\n</FormField>"
        },
        {
          "title": "Label 단독",
          "lang": "tsx",
          "code": "import { Label, Checkbox } from \"@axe/ui\";\n\n<div style={{ display: \"flex\", gap: 8, alignItems: \"center\" }}>\n  <Checkbox id=\"remember\" />\n  <Label htmlFor=\"remember\">로그인 유지</Label>\n</div>"
        },
        {
          "title": "CSS-only (비-React, error 상태)",
          "lang": "html",
          "code": "<div class=\"axe-form-field axe-form-field--error\">\n  <label class=\"axe-label axe-form-field__label\" for=\"pw\">\n    비밀번호<span class=\"axe-form-field__required\" aria-hidden=\"true\">*</span>\n  </label>\n  <div class=\"axe-form-field__control\">\n    <input id=\"pw\" class=\"axe-input\" type=\"password\"\n           aria-invalid=\"true\" aria-describedby=\"pw-err\" />\n  </div>\n  <p id=\"pw-err\" class=\"axe-form-field__error\" role=\"alert\">8자 이상이어야 합니다</p>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "htmlFor 를 자식 control 의 id 와 반드시 매칭.",
          "에러는 error prop 으로 (role=alert 자동) — hint 와 동시 표기는 지양."
        ],
        "dont": [
          "required prop 만으로 실제 검증을 대신하지 말 것 (control 에 required 별도).",
          "여러 control 을 한 FormField 에 넣어 aria 자동 wiring 을 깨지 말 것."
        ]
      },
      "consumeCss": "CSS-only 로 재현 가능 — axe-form-field 골격(__label/__control/__hint 또는 __error)을 직접 조립. 단 aria-describedby/aria-invalid 자동 연결은 React 전용이라 수동으로 걸어야 함.",
      "url": "https://design.axelabs.ai/components/form"
    },
    {
      "name": "Hero",
      "slug": "hero",
      "group": "container",
      "summary": "페이지 첫 화면 — eyebrow·title(h1)·subtitle·badges·actions·image 슬롯.",
      "description": "랜딩/문서 인덱스 상단의 공통 골격. title 은 항상 <h1> 로 렌더되고 useId 로 만든 id 를 section 의 aria-labelledby 로 연결한다. image 슬롯을 주면 데스크톱(≥768px)에서 2-col, 없으면 1-col. size 로 타이틀 티어(md/lg/xl + 마케팅용 display 32~80px fluid)를, align 으로 좌/중앙 정렬을 고른다.",
      "keywords": [
        "hero",
        "히어로",
        "landing",
        "jumbotron",
        "masthead",
        "첫화면",
        "랜딩"
      ],
      "props": [
        {
          "name": "title",
          "type": "React.ReactNode",
          "required": true,
          "description": "h1 내용(`.axe-hero__title`). ReactNode 라 <br/>·<span> 자유. HTMLAttributes 의 title 은 이 prop 으로 대체됨."
        },
        {
          "name": "eyebrow",
          "type": "React.ReactNode",
          "description": "title 위 작은 상단 라벨(Badge 등). 앞에 브랜드 점 ::before 가 붙는다."
        },
        {
          "name": "subtitle",
          "type": "React.ReactNode",
          "description": "부제 문단(`.axe-hero__subtitle`)."
        },
        {
          "name": "badges",
          "type": "React.ReactNode",
          "description": "title 아래 Badge cluster."
        },
        {
          "name": "actions",
          "type": "React.ReactNode",
          "description": "CTA 버튼 묶음(`.axe-hero__actions`)."
        },
        {
          "name": "image",
          "type": "React.ReactNode",
          "description": "우측 비주얼 슬롯. 지정하면 axe-hero--with-image 로 데스크톱 2-col 이 된다."
        },
        {
          "name": "align",
          "type": "\"left\" | \"center\"",
          "defaultValue": "\"left\"",
          "description": "콘텐츠 정렬. center 는 텍스트·badges·actions 를 중앙 정렬."
        },
        {
          "name": "size",
          "type": "\"md\" | \"lg\" | \"xl\" | \"display\"",
          "defaultValue": "\"lg\"",
          "description": "타이틀 크기 티어. display 는 랜딩/마케팅 전용 디스플레이 티어(32~80px fluid)."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLElement>, \"title\">",
          "description": "className 병합, 나머지는 루트 <section> 으로 전파. aria-labelledby 를 명시하면 자동 생성 h1 id 대신 그 값을 쓴다."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-hero",
          "purpose": "루트 <section> — grid 레이아웃.",
          "stability": "stable"
        },
        {
          "className": "axe-hero--with-image",
          "purpose": "image 슬롯 존재 — ≥768px 에서 1.1fr/1fr 2-col.",
          "stability": "stable"
        },
        {
          "className": "axe-hero--center",
          "purpose": "중앙 정렬 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-hero--size-md",
          "purpose": "타이틀 3xl 티어.",
          "stability": "stable"
        },
        {
          "className": "axe-hero--size-lg",
          "purpose": "타이틀 4xl 티어(기본).",
          "stability": "stable"
        },
        {
          "className": "axe-hero--size-xl",
          "purpose": "타이틀 5xl 티어.",
          "stability": "stable"
        },
        {
          "className": "axe-hero--size-display",
          "purpose": "디스플레이 티어 — display-md fluid + 넓은 content/subtitle.",
          "stability": "stable"
        },
        {
          "className": "axe-hero__content",
          "purpose": "좌측 텍스트 컬럼(flex column, max 42rem).",
          "stability": "stable"
        },
        {
          "className": "axe-hero__eyebrow",
          "purpose": "상단 라벨 — uppercase·tracking-widest, 브랜드 점 ::before.",
          "stability": "stable"
        },
        {
          "className": "axe-hero__title",
          "purpose": "h1 타이틀 — display 폰트.",
          "stability": "stable"
        },
        {
          "className": "axe-hero__subtitle",
          "purpose": "부제 문단.",
          "stability": "stable"
        },
        {
          "className": "axe-hero__badges",
          "purpose": "Badge cluster(flex wrap).",
          "stability": "stable"
        },
        {
          "className": "axe-hero__actions",
          "purpose": "CTA 버튼 row(flex wrap).",
          "stability": "stable"
        },
        {
          "className": "axe-hero__image",
          "purpose": "우측 비주얼 슬롯 컨테이너.",
          "stability": "stable"
        },
        {
          "className": "axe-hero--brand",
          "purpose": "브랜드 와인 radial wash + Clash 타이틀 페이스. prop 미노출 — className 으로 opt-in. 내부 <em> 은 브랜드 색 키워드 강조.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "루트 <section> 이 aria-labelledby 로 <h1 id> 과 연결되어 landmark 이름을 갖는다(id 는 useId 자동, aria-labelledby prop 으로 override 가능).",
        "notes": "title 은 항상 <h1> 이므로 문서 개요상 페이지당 Hero 는 하나로 유지한다(h1 중복 회피). image 슬롯은 장식이면 소비자가 내부에서 aria-hidden/빈 alt 를 준다."
      },
      "examples": [
        {
          "title": "마케팅 히어로(display·center)",
          "lang": "tsx",
          "code": "import { Hero, Badge, Button } from \"@axe/ui\";\n\n<Hero\n  eyebrow={<Badge>AXE Labs</Badge>}\n  title={<>AI-native <span>ERP</span></>}\n  subtitle=\"소싱부터 결산까지, 하나의 통제 평면.\"\n  actions={<Button variant=\"primary\">시작하기</Button>}\n  size=\"display\"\n  align=\"center\"\n/>"
        },
        {
          "title": "2-col(image 슬롯)",
          "lang": "tsx",
          "code": "import { Hero, Button } from \"@axe/ui\";\n\n<Hero\n  title=\"포트폴리오를 한눈에\"\n  subtitle=\"KPI·리스크·엑싯 신호를 하나의 보드에서.\"\n  actions={<Button variant=\"primary\">데모 보기</Button>}\n  image={<img src=\"/board.png\" alt=\"포트폴리오 보드 스크린샷\" />}\n/>"
        },
        {
          "title": "CSS-only (비-React, 브랜드 워시)",
          "lang": "html",
          "code": "<section class=\"axe-hero axe-hero--size-display axe-hero--center axe-hero--brand\" aria-labelledby=\"hero-t\">\n  <div class=\"axe-hero__content\">\n    <div class=\"axe-hero__eyebrow\">AXE Labs</div>\n    <h1 id=\"hero-t\" class=\"axe-hero__title\">미래를 <em>설계</em>하다</h1>\n    <p class=\"axe-hero__subtitle\">소싱부터 결산까지.</p>\n    <div class=\"axe-hero__actions\">\n      <button class=\"axe-btn axe-btn--primary\">시작하기</button>\n    </div>\n  </div>\n</section>"
        }
      ],
      "bestPractices": {
        "do": [
          "size=\"display\" 는 랜딩/마케팅 상단에만 — 문서 인덱스는 md/lg 로 절제.",
          "eyebrow 는 짧은 라벨 한 줄로(카테고리/제품명)."
        ],
        "dont": [
          "한 페이지에 Hero 를 여러 개 두어 h1 을 중복시키지 말 것.",
          "actions 에 primary 버튼을 여러 개 나열하지 말 것(주 행동 하나)."
        ]
      },
      "consumeCss": "상태 없는 순수 CSS — 서버 렌더(maud/jinja)에서 `.axe-hero` 골격을 그대로 재현할 수 있다. aria-labelledby↔h1 id 연결은 마크업에서 직접 맞춘다. 브랜드 워시는 axe-hero--brand 클래스만 추가하면 된다(전용 prop 없음).",
      "url": "https://design.axelabs.ai/components/hero"
    },
    {
      "name": "HoverCard",
      "slug": "hover-card",
      "group": "overlay",
      "summary": "hover(또는 focus)만으로 뜨는 가벼운 미리보기 카드 — @-멘션·링크 프리뷰용 (Radix HoverCard 기반).",
      "description": "Tooltip 보다 풍부한 컨텐츠(아바타·요약·메타 줄)를, Popover 보다 가벼운 의도(클릭 없이 hover, 키보드 trap 없음)로 보여준다. Popover 와 **같은** .axe-popover__* 표면(elevated bg·border·shadow·arrow)을 재사용하며 새 CSS 토큰이 없다. 차이는 트리거뿐 — Popover 는 클릭, HoverCard 는 hover/focus 이고 openDelay/closeDelay 로 noisy 등장을 막는다. HoverCard(Root)·HoverCardTrigger·HoverCardContent·HoverCardArrow 를 조합한다.",
      "keywords": [
        "hover card",
        "hovercard",
        "미리보기",
        "preview",
        "mention",
        "멘션",
        "링크 프리뷰",
        "radix"
      ],
      "props": [
        {
          "name": "openDelay",
          "type": "number",
          "defaultValue": "200",
          "description": "hover 후 열리기까지 지연(ms) — HoverCard(Root) 에서 기본값 부여."
        },
        {
          "name": "closeDelay",
          "type": "number",
          "defaultValue": "150",
          "description": "떠난 뒤 닫히기까지 지연(ms) — 트리거↔카드 사이 이동 시 깜빡임 방지."
        },
        {
          "name": "open",
          "type": "boolean",
          "description": "열림 상태 (controlled). Radix Root prop."
        },
        {
          "name": "onOpenChange",
          "type": "(open: boolean) => void",
          "description": "열림/닫힘 콜백 (Root)."
        },
        {
          "name": "sideOffset",
          "type": "number",
          "defaultValue": "8",
          "description": "HoverCardContent 와 트리거 사이 간격(px) — Popover 와 동일."
        },
        {
          "name": "withPortal",
          "type": "boolean",
          "defaultValue": "true",
          "description": "HoverCardContent 를 portal 로 띄울지 — overflow:hidden 부모 탈출."
        },
        {
          "name": "...rest",
          "type": "각 Radix HoverCard.* props",
          "description": "Trigger(asChild)·Content(side/align/collisionPadding)·Arrow 등 각 파트는 대응 Radix 컴포넌트 props 를 그대로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-popover__content",
          "purpose": "미리보기 카드 표면(Popover 와 공유 — elevated bg·border·shadow).",
          "stability": "stable"
        },
        {
          "className": "axe-popover__arrow",
          "purpose": "트리거를 가리키는 화살표(Popover 와 공유·Radix Arrow).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "트리거로 Tab 이동(키보드 focus)만으로도 카드가 열림",
          "Esc 로 닫기"
        ],
        "aria": "Radix HoverCard 는 보조 정보용이라 트리거에 팝업 role 을 강제하지 않는다(트리거는 보통 링크/앵커 그대로). hover 와 키보드 focus 두 경로로 열리며 focus 를 가로채지 않는다(trap 없음).",
        "notes": "openDelay/closeDelay 로 등장·소멸을 디바운스. hover 전용이라 터치 환경에서는 뜨지 않으니 필수 정보를 여기에만 두지 말 것. prefers-reduced-motion 시 애니메이션 제거."
      },
      "examples": [
        {
          "title": "멘션 미리보기",
          "lang": "tsx",
          "code": "import {\n  HoverCard,\n  HoverCardTrigger,\n  HoverCardContent,\n  HoverCardArrow,\n} from \"@axe/ui\";\n\n<HoverCard>\n  <HoverCardTrigger asChild>\n    <a href=\"/u/kang\">@강수훈</a>\n  </HoverCardTrigger>\n  <HoverCardContent side=\"top\" align=\"start\">\n    <HoverCardArrow />\n    <div className=\"preview\">\n      <strong>강수훈</strong>\n      <p>AXE Labs · 운영</p>\n    </div>\n  </HoverCardContent>\n</HoverCard>"
        },
        {
          "title": "지연·배치 커스텀",
          "lang": "tsx",
          "code": "import {\n  HoverCard,\n  HoverCardTrigger,\n  HoverCardContent,\n} from \"@axe/ui\";\n\n<HoverCard openDelay={350} closeDelay={100}>\n  <HoverCardTrigger asChild>\n    <a href=\"/docs/deploy\">배포 SSOT</a>\n  </HoverCardTrigger>\n  <HoverCardContent side=\"bottom\" align=\"center\" sideOffset={12}>\n    <p>origin/main 의 commit SHA 가 배포의 SSOT…</p>\n  </HoverCardContent>\n</HoverCard>"
        }
      ],
      "bestPractices": {
        "do": [
          "트리거는 asChild 로 실제 링크/앵커에 씌워 자연스러운 hover 대상으로.",
          "보조·부가 정보만 담는다 — 없어도 흐름이 막히지 않아야.",
          "closeDelay 로 트리거↔카드 이동 시 깜빡임을 방지."
        ],
        "dont": [
          "필수 정보·주요 액션을 hover 뒤에 숨기지 말 것 — 터치·키보드 발견성이 낮다.",
          "클릭이 필요한 무거운 상호작용은 Popover 로 (HoverCard 는 trap 없음)."
        ]
      },
      "consumeCss": "hover/focus 등장·지연·위치 계산이 Radix 런타임(data-state)에 의존해 CSS-only 로는 재현할 수 없다. 표면은 Popover 의 .axe-popover__* 를 그대로 공유해 별도 토큰이 없다 — 실제 소비는 React 컴포넌트로.",
      "url": "https://design.axelabs.ai/components/hover-card"
    },
    {
      "name": "Input",
      "slug": "input",
      "group": "input",
      "summary": "테마 토큰 기반 텍스트 입력 필드 — 단일행 Input 과 다행 Textarea.",
      "description": "네이티브 <input>/<textarea> 에 axe-input 스타일만 입힌 얇은 forwardRef 래퍼로, ref 와 native 속성(placeholder·type·disabled·aria-invalid 등)을 그대로 전파한다. Input 은 size 로 높이 프리셋(md 36px / lg 44px)을 고르고, Textarea 는 세로 리사이즈 가능하며 min-height 80px 다. FormField 로 감싸면 error 상태에서 aria-invalid 가 자동 연결돼 form.css 가 빨간 테두리로 시각 동기화한다.",
      "keywords": [
        "input",
        "textarea",
        "입력",
        "텍스트필드",
        "form",
        "폼"
      ],
      "props": [
        {
          "name": "size",
          "type": "\"md\" | \"lg\"",
          "defaultValue": "\"md\"",
          "description": "Input 높이 프리셋 (md=36px, lg=44px). Textarea 에는 없음."
        },
        {
          "name": "type",
          "type": "React.HTMLInputTypeAttribute",
          "defaultValue": "\"text\"",
          "description": "네이티브 input type. 미지정 시 text 로 기본화 (Input 전용)."
        },
        {
          "name": "...rest",
          "type": "Omit<React.InputHTMLAttributes<HTMLInputElement>, \"size\">",
          "description": "Input — className 은 axe-input 과 병합되고 나머지는 <input> 으로 전파."
        },
        {
          "name": "...rest (Textarea)",
          "type": "React.TextareaHTMLAttributes<HTMLTextAreaElement>",
          "description": "Textarea 는 커스텀 prop 없음 — className 은 'axe-input axe-input--textarea' 와 병합, 나머지 textarea 속성 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-input",
          "purpose": "입력 루트 (Input·Textarea 공통). height 36px, 토큰 테두리/배경, :focus 시 var(--ring).",
          "stability": "stable"
        },
        {
          "className": "axe-input--lg",
          "purpose": "Input 큰 크기 변형 (height 44px, text-base) — size='lg' 일 때.",
          "stability": "stable"
        },
        {
          "className": "axe-input--textarea",
          "purpose": "Textarea 변형 (height auto, min-height 80px, resize:vertical).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "네이티브 텍스트 편집/캐럿 이동"
        ],
        "aria": "네이티브 <input>/<textarea> — 컴포넌트가 role/aria 를 덧붙이지 않음. aria-invalid=\"true\" 를 주면 form.css 가 빨간 테두리로 시각 동기화.",
        "notes": "라벨 연결은 별도 — <Label htmlFor> 또는 FormField 로 감싸 control id 를 매칭. 포커스 링은 :focus 에서 box-shadow var(--ring)."
      },
      "examples": [
        {
          "title": "기본",
          "lang": "tsx",
          "code": "import { Input } from \"@axe/ui\";\n\nexport function Example() {\n  return (\n    <>\n      <Input type=\"email\" placeholder=\"you@company.com\" />\n      <Input size=\"lg\" placeholder=\"큰 입력\" />\n    </>\n  );\n}"
        },
        {
          "title": "Textarea",
          "lang": "tsx",
          "code": "import { Textarea } from \"@axe/ui\";\n\n<Textarea rows={4} placeholder=\"메모를 입력하세요\" />"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<input class=\"axe-input\" type=\"email\" placeholder=\"you@company.com\" />\n<input class=\"axe-input axe-input--lg\" placeholder=\"큰 입력\" />\n<textarea class=\"axe-input axe-input--textarea\" placeholder=\"메모\"></textarea>"
        }
      ],
      "bestPractices": {
        "do": [
          "라벨은 <Label htmlFor> 또는 FormField 로 연결.",
          "검증 실패 시 aria-invalid 를 함께 세팅 (색만 바꾸지 말 것)."
        ],
        "dont": [
          "placeholder 를 라벨 대용으로 쓰지 말 것.",
          "size 에 md/lg 외 값을 기대하지 말 것."
        ]
      },
      "consumeCss": "CSS-only 로 완전 재현 가능 — 네이티브 <input>/<textarea> 에 axe-input(+변형) 클래스만 붙이면 됨. error 색은 aria-invalid=\"true\" 로.",
      "playground": {
        "componentKey": "input",
        "defaults": {
          "size": "md",
          "type": "text",
          "disabled": false,
          "placeholder": "이름을 입력하세요"
        },
        "controls": [
          {
            "prop": "placeholder",
            "kind": "text"
          },
          {
            "prop": "type",
            "kind": "select",
            "options": [
              "text",
              "search",
              "email",
              "password",
              "number",
              "url",
              "date"
            ]
          },
          {
            "prop": "disabled",
            "kind": "switch"
          }
        ]
      },
      "url": "https://design.axelabs.ai/components/input"
    },
    {
      "name": "Kbd",
      "slug": "kbd",
      "group": "utility",
      "summary": "키보드 입력을 표시하는 <kbd> 토큰.",
      "description": "네이티브 <kbd> 시맨틱 요소에 `axe-kbd` 스타일만 입힌 소품이다. 자체 prop 은 없고 children·className·나머지 HTML 속성을 그대로 <kbd> 로 전파한다. ⌘K, Esc 같은 단축키 힌트 표기에 쓴다.",
      "keywords": [
        "kbd",
        "키보드",
        "단축키",
        "shortcut",
        "hotkey",
        "key",
        "utility"
      ],
      "props": [
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLElement>",
          "description": "children/className 병합 후 나머지 속성은 <kbd> 로 전파. 자체 prop 없음."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-kbd",
          "purpose": "키 캡 스타일 <kbd> 루트 (유일 클래스).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "시맨틱 <kbd> 요소 — 별도 role 불요. 상호작용 없음(표시 전용). 여러 키 조합은 각각 <Kbd> 로 나눠 스크린리더가 개별 키로 읽게 하는 편이 낫다."
      },
      "examples": [
        {
          "title": "단축키 힌트",
          "lang": "tsx",
          "code": "import { Kbd } from \"@axe/ui\";\n\nexport function Example() {\n  return (\n    <>\n      <span><Kbd>⌘</Kbd> <Kbd>K</Kbd> 로 검색</span>\n      <Kbd>Esc</Kbd>\n    </>\n  );\n}"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<kbd class=\"axe-kbd\">⌘</kbd> <kbd class=\"axe-kbd\">K</kbd>"
        }
      ],
      "bestPractices": {
        "do": [
          "조합 키는 개별 <Kbd> 로 분리해 나열."
        ],
        "dont": [
          "긴 설명 텍스트를 감싸지 말 것 — 키 라벨만."
        ]
      },
      "consumeCss": "CSS-only 로 완전 재현 가능 — `<kbd class=\"axe-kbd\">` 만으로 동일.",
      "url": "https://design.axelabs.ai/components/kbd"
    },
    {
      "name": "Layout",
      "slug": "layout",
      "group": "layout",
      "summary": "Stack · Cluster · Grid · Container — inline flex/grid 재발명을 대체하는 레이아웃 프리미티브 4종.",
      "description": "spacing scale 토큰(--space-N)에 1:1 매핑되는 얇은 레이아웃 원소 묶음이다. Stack = 세로 flex+gap, Cluster = 가로 flex+wrap+gap(툴바·헤더 액션 줄), Grid = 고정/auto-fill 반응형 grid(대시보드 카드), Container = max-width 중앙 정렬+좌우 gutter. 각 서비스(matrix 헤더의 inline `display:flex;gap`, cortex `.cortex-metric-grid` 등)가 반복 재발명하던 패턴을 SSOT 로 흡수한다. 전부 forwardRef + className 병합이며 순수 CSS 클래스로도 동일하게 소비 가능(상태 없음).",
      "keywords": [
        "layout",
        "레이아웃",
        "stack",
        "cluster",
        "grid",
        "container",
        "flex",
        "gap",
        "spacing",
        "간격",
        "그리드",
        "컨테이너",
        "정렬"
      ],
      "props": [
        {
          "name": "Stack.gap",
          "type": "0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10",
          "defaultValue": "4",
          "description": "항목 간 세로 간격(--space-N, 0=간격 없음)."
        },
        {
          "name": "Stack.align",
          "type": "\"start\" | \"center\" | \"end\" | \"stretch\"",
          "description": "교차축(가로) 정렬. 미지정 시 flex 기본값 stretch(modifier 클래스 없음)."
        },
        {
          "name": "Stack.as",
          "type": "React.ElementType",
          "defaultValue": "\"div\"",
          "description": "렌더 태그. main/section/header 등 시맨틱 요소로 교체."
        },
        {
          "name": "Cluster.gap",
          "type": "0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10",
          "defaultValue": "3",
          "description": "항목 간 간격(--space-N)."
        },
        {
          "name": "Cluster.align",
          "type": "\"start\" | \"center\" | \"end\" | \"baseline\" | \"stretch\"",
          "defaultValue": "\"center\"",
          "description": "교차축(세로) 정렬. 기본 center 는 루트 클래스가 제공(명시 시에만 modifier 부여)."
        },
        {
          "name": "Cluster.justify",
          "type": "\"start\" | \"center\" | \"end\" | \"between\"",
          "description": "주축(가로) 분배. 미지정 시 flex-start."
        },
        {
          "name": "Cluster.nowrap",
          "type": "boolean",
          "description": "줄바꿈 비활성화(한 줄 강제). 기본은 wrap."
        },
        {
          "name": "Cluster.as",
          "type": "React.ElementType",
          "defaultValue": "\"div\"",
          "description": "렌더 태그."
        },
        {
          "name": "Grid.cols",
          "type": "1 | 2 | 3 | 4 | 5 | 6",
          "defaultValue": "3",
          "description": "고정 컬럼 수. auto=false 일 때만 적용."
        },
        {
          "name": "Grid.gap",
          "type": "0 | 1 | 2 | 3 | 4 | 5 | 6",
          "defaultValue": "4",
          "description": "항목 간 간격(--space-N)."
        },
        {
          "name": "Grid.responsive",
          "type": "boolean",
          "defaultValue": "true",
          "description": "좁은 화면에서 컬럼 단계 축소(≤1023.98px: cols≥3→2, ≤639.98px: cols≥2→1). auto 모드엔 무효."
        },
        {
          "name": "Grid.auto",
          "type": "boolean",
          "description": "auto-fill 모드 — cols 대신 항목 최소 폭(min)으로 컬럼 수 자동 결정."
        },
        {
          "name": "Grid.min",
          "type": "string",
          "defaultValue": "\"15rem\"",
          "description": "auto 모드 항목 최소 폭(--axe-grid-min). auto=false 면 무시."
        },
        {
          "name": "Container.size",
          "type": "\"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\"",
          "defaultValue": "\"lg\"",
          "description": "최대 폭 토큰. sm 40 / md 56 / lg 72 / xl 80rem / full 100%."
        },
        {
          "name": "Container.as",
          "type": "React.ElementType",
          "defaultValue": "\"div\"",
          "description": "렌더 태그. main/section 등으로 교체."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLElement>",
          "description": "네 컴포넌트 공통 — className 은 병합, 나머지(id·style·data-* 등)는 루트 요소로 전파(Grid 는 HTMLDivElement)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-stack",
          "purpose": "Stack 루트 — flex column + gap + min-width:0.",
          "stability": "stable"
        },
        {
          "className": "axe-stack--gap-{0-10}",
          "purpose": "세로 간격 = --space-N (gap-0 은 0).",
          "stability": "stable"
        },
        {
          "className": "axe-stack--align-{start|center|end|stretch}",
          "purpose": "교차축(가로) 정렬.",
          "stability": "stable"
        },
        {
          "className": "axe-cluster",
          "purpose": "Cluster 루트 — flex row wrap + align-items:center + gap.",
          "stability": "stable"
        },
        {
          "className": "axe-cluster--gap-{0-10}",
          "purpose": "간격 = --space-N.",
          "stability": "stable"
        },
        {
          "className": "axe-cluster--align-{start|center|end|baseline|stretch}",
          "purpose": "교차축(세로) 정렬.",
          "stability": "stable"
        },
        {
          "className": "axe-cluster--justify-{start|center|end|between}",
          "purpose": "주축(가로) 분배.",
          "stability": "stable"
        },
        {
          "className": "axe-cluster--nowrap",
          "purpose": "flex-wrap:nowrap(한 줄 강제).",
          "stability": "stable"
        },
        {
          "className": "axe-grid",
          "purpose": "Grid 루트 — display:grid + gap + min-width:0.",
          "stability": "stable"
        },
        {
          "className": "axe-grid--gap-{0-6}",
          "purpose": "간격 = --space-N.",
          "stability": "stable"
        },
        {
          "className": "axe-grid--cols-{1-6}",
          "purpose": "고정 N컬럼(repeat(N, minmax(0,1fr))).",
          "stability": "stable"
        },
        {
          "className": "axe-grid--auto",
          "purpose": "auto-fill 모드 — repeat(auto-fill, minmax(--axe-grid-min, 1fr)).",
          "stability": "stable"
        },
        {
          "className": "axe-grid--responsive",
          "purpose": "미디어쿼리로 cols-N 을 좁은 화면에서 단계 축소.",
          "stability": "stable"
        },
        {
          "className": "axe-container",
          "purpose": "Container 루트 — max-width(--axe-container-max) 중앙 정렬 + 좌우 gutter(--space-4).",
          "stability": "stable"
        },
        {
          "className": "axe-container--{sm|md|lg|xl|full}",
          "purpose": "--axe-container-max 토큰 설정(40/56/72/80rem/100%).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "네 컴포넌트 모두 시맨틱 없는 표현용 컨테이너 — role/aria 를 자동 부여하지 않는다.",
        "notes": "랜드마크가 필요한 자리(본문·헤더·내비)에는 `as`(Stack/Cluster/Container) 로 main/section/header/nav 를 지정해 문서 구조를 세운다. Grid/Stack/Cluster 는 DOM 순서를 재배치하지 않으므로 시각 순서 == 읽기/탭 순서. Stack·Cluster·Grid 루트에는 min-width:0 이 있어 긴 콘텐츠가 flex/grid 트랙을 밀어 넘치는 문제를 방지한다(Container 루트는 예외 — max-width 중앙 정렬만)."
      },
      "examples": [
        {
          "title": "Stack — 세로 흐름",
          "lang": "tsx",
          "code": "import { Stack } from \"@axe/ui\";\n\n<Stack gap={5} as=\"main\">\n  <h1>포트폴리오</h1>\n  <p>이번 분기 요약…</p>\n</Stack>"
        },
        {
          "title": "Cluster — 헤더 액션 줄",
          "lang": "tsx",
          "code": "import { Cluster, Button } from \"@axe/ui\";\n\n<Cluster gap={2} justify=\"between\">\n  <h2>딜 파이프라인</h2>\n  <Cluster gap={2}>\n    <Button variant=\"ghost\">필터</Button>\n    <Button variant=\"primary\">새 딜</Button>\n  </Cluster>\n</Cluster>"
        },
        {
          "title": "Grid — auto-fill 카드 그리드",
          "lang": "tsx",
          "code": "import { Grid, MetricCard } from \"@axe/ui\";\n\n<Grid auto min=\"16rem\" gap={4}>\n  <MetricCard label=\"TVPI\" value=\"1.8x\" />\n  <MetricCard label=\"DPI\" value=\"0.4x\" />\n  <MetricCard label=\"IRR\" value=\"22%\" />\n</Grid>"
        },
        {
          "title": "Container — 본문 폭 제한",
          "lang": "tsx",
          "code": "import { Container, Stack } from \"@axe/ui\";\n\n<Container size=\"md\" as=\"main\">\n  <Stack gap={6}>…</Stack>\n</Container>"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-container axe-container--md\">\n  <div class=\"axe-stack axe-stack--gap-5\">\n    <div class=\"axe-cluster axe-cluster--gap-2 axe-cluster--justify-between\">\n      <h2>제목</h2>\n      <button class=\"axe-btn axe-btn--primary\">액션</button>\n    </div>\n    <div class=\"axe-grid axe-grid--auto axe-grid--gap-4\">\n      <div class=\"axe-card\">…</div>\n      <div class=\"axe-card\">…</div>\n    </div>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "간격은 gap prop(토큰)으로만 — 개별 margin 을 섞지 말 것.",
          "랜드마크가 필요하면 as 로 main/section/header 를 지정한다.",
          "대시보드 카드처럼 개수가 유동적이면 Grid auto + min, 고정 열이면 cols 를 쓴다."
        ],
        "dont": [
          "Stack/Cluster/Grid 를 클릭 타깃으로 쓰지 말 것(표현용 컨테이너 — 포커스/role 없음).",
          "Container 를 AppShell content slot(이미 max-width 처리) 안에 중첩해 폭을 이중 제한하지 말 것."
        ]
      },
      "consumeCss": "네 프리미티브 모두 상태 없는 순수 CSS 클래스 — maud/jinja 등 서버 렌더에서 `.axe-stack`/`.axe-cluster`/`.axe-grid`/`.axe-container` 마크업을 그대로 써도 동일 결과. gap/cols/size 는 modifier 클래스, Container 는 --axe-container-max, Grid auto 는 --axe-grid-min 인라인 var 로 미세 조정.",
      "url": "https://design.axelabs.ai/components/layout"
    },
    {
      "name": "LazyCharts",
      "slug": "lazy-charts",
      "group": "data",
      "summary": "React.lazy + Suspense로 recharts 청크를 실제 렌더 시점까지 지연 로드하는 framework-neutral LazyLineChart · LazyBarChart · LazyAreaChart — props는 각 즉시 로드 차트와 동일.",
      "description": "recharts 청크를 대시보드 첫 페인트에서 분리하는 framework-neutral 변형이다. React.lazy가 Chart 모듈을 지연 import하고 Suspense fallback은 같은 height의 Skeleton을 예약한다. 기본 높이도 양쪽 모두 240px라 prop을 생략해도 layout shift가 없다. props는 LineChartProps / BarChartProps / AreaChartProps를 그대로 전달한다. lazy wrapper는 ref를 forward하지 않으므로 ref가 필요하면 즉시 로드 LineChart/BarChart/AreaChart를 쓴다.",
      "keywords": [
        "lazy chart",
        "lazy charts",
        "지연 로딩",
        "code splitting",
        "코드 스플리팅",
        "React.lazy",
        "Suspense",
        "dynamic import",
        "recharts",
        "skeleton",
        "번들 최적화",
        "LazyLineChart",
        "LazyBarChart",
        "LazyAreaChart",
        "차트"
      ],
      "props": [
        {
          "name": "data",
          "type": "Datum[]",
          "required": true,
          "description": "레코드 배열(즉시 로드 차트와 동일). 각 원소는 xKey 라벨 + series 별 y값."
        },
        {
          "name": "xKey",
          "type": "string",
          "required": true,
          "description": "x축 key. data[i][xKey] 가 카테고리 축 라벨."
        },
        {
          "name": "series",
          "type": "ChartSeries[]",
          "required": true,
          "description": "그릴 계열 목록. ChartSeries = { key, label?, color?, dashed? } — Chart 문서와 동일 형태."
        },
        {
          "name": "height",
          "type": "number",
          "defaultValue": "240",
          "description": "로딩 Skeleton과 로드된 차트가 항상 같은 높이를 공유한다. 미지정 시 양쪽 모두 240px."
        },
        {
          "name": "showLegend",
          "type": "boolean",
          "description": "범례 표시. 미지정 시 series 2개 이상이면 자동."
        },
        {
          "name": "showGrid",
          "type": "boolean",
          "defaultValue": "true",
          "description": "CartesianGrid 표시."
        },
        {
          "name": "onPointClick",
          "type": "(point: Datum) => void",
          "description": "point/bar 클릭 콜백(마우스 전용). 즉시 로드 차트와 동일."
        },
        {
          "name": "animate",
          "type": "boolean",
          "description": "진입 애니메이션. 미지정 시 prefers-reduced-motion 이면 자동 false."
        },
        {
          "name": "ariaLabel",
          "type": "string",
          "description": "role=\"img\" 컨테이너의 aria-label. 미지정 fallback 은 무의미하므로 사실상 필수."
        },
        {
          "name": "ariaDescribedBy",
          "type": "string",
          "description": "확장 설명(숨긴 데이터 표) 요소의 id."
        },
        {
          "name": "className",
          "type": "string",
          "description": "로드된 차트 루트 .axe-chart 에 병합(로딩 Skeleton 에는 미적용)."
        },
        {
          "name": "stacked",
          "type": "boolean",
          "defaultValue": "false",
          "description": "[LazyBarChart 전용] bar 누적."
        },
        {
          "name": "gradient",
          "type": "boolean",
          "defaultValue": "true",
          "description": "[LazyAreaChart 전용] 영역 gradient fill."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-chart",
          "purpose": "로드 완료 후 내부 즉시 로드 차트의 루트 컨테이너 — className prop 이 여기 병합된다. Lazy 변형이 새로 정의하지 않고 Chart 의 클래스를 그대로 상속(data-display.css).",
          "stability": "stable"
        },
        {
          "className": "axe-skeleton",
          "purpose": "청크 로드 전 fallback — height 만큼의 Skeleton placeholder(기본 rect 변형, role=\"presentation\"·aria-hidden). Lazy 변형의 로딩 상태 구현 세부로, Skeleton(indicators.css)에서 상속.",
          "stability": "internal"
        }
      ],
      "a11y": {
        "aria": "로딩 중에는 Skeleton(role=\"presentation\" + aria-hidden)이 렌더돼 SR 에 무시된다. 로드 완료 후에는 내부 차트의 role=\"img\" + aria-label 계약을 그대로 상속한다(Chart 문서 참고).",
        "notes": "Suspense가 청크 로드 전 Skeleton을 보여주고, Chart의 initialDimension이 SSR·hydrate 크기를 안정화한다. 로딩 Skeleton은 prefers-reduced-motion 시 shimmer를 멈춘다. 실제 수치의 SR·키보드 접근성은 로드된 차트에 달려 있으므로 ariaLabel·ariaDescribedBy를 챙긴다."
      },
      "examples": [
        {
          "title": "대시보드 차트 지연 로드",
          "lang": "tsx",
          "code": "import { LazyLineChart } from \"@axe/ui\";\n\n<LazyLineChart\n  data={monthly}\n  xKey=\"month\"\n  series={[{ key: \"aum\", label: \"AUM\" }]}\n  height={280}          // Skeleton·차트 높이 일치 → layout shift 0\n  ariaLabel=\"월별 운용자산 추이\"\n/>"
        },
        {
          "title": "누적 막대 (LazyBarChart)",
          "lang": "tsx",
          "code": "import { LazyBarChart } from \"@axe/ui\";\n\n<LazyBarChart\n  data={[\n    { quarter: \"Q1\", direct: 40, channel: 24 },\n    { quarter: \"Q2\", direct: 52, channel: 30 },\n  ]}\n  xKey=\"quarter\"\n  series={[\n    { key: \"direct\", label: \"직접\" },\n    { key: \"channel\", label: \"채널\" },\n  ]}\n  stacked\n  height={260}\n  ariaLabel=\"분기별 채널 매출 구성\"\n/>"
        }
      ],
      "bestPractices": {
        "do": [
          "화면 설계에 맞는 height를 명시한다. 생략해도 Skeleton과 차트는 공용 기본값 240px로 일치한다.",
          "대시보드 첫 화면·below-the-fold 차트에 써서 recharts 청크를 초기 번들에서 뺀다.",
          "ariaLabel·ariaDescribedBy 를 즉시 로드 차트와 동일하게 지정한다."
        ],
        "dont": [
          "ref가 필요한 자리에 쓰지 말 것 — lazy wrapper는 ref를 forward하지 않는다(즉시 로드 LineChart/BarChart/AreaChart 사용).",
          "이미 화면 상단에서 즉시 보여야 하는 소형 차트까지 지연시키지 말 것(불필요한 Skeleton 깜빡임).",
          "첫 화면의 핵심 차트를 이유 없이 지연해 정보 노출을 늦추지 말 것."
        ]
      },
      "consumeCss": "Lazy 변형 자체는 고유 .axe-* 클래스를 정의하지 않는다 — 로딩은 Skeleton(.axe-skeleton*), 로드 후는 Chart(.axe-chart*)의 클래스를 조합한다. 순수 CSS-only 소비는 불가(React + recharts 의존). 스타일은 data-display.css(차트) + indicators.css(skeleton shimmer)를 로드해야 한다.",
      "url": "https://design.axelabs.ai/components/lazy-charts"
    },
    {
      "name": "Logo",
      "slug": "logo",
      "group": "utility",
      "summary": "브랜드 워드마크 — 홈으로 가는 텍스트 앵커.",
      "description": "display 폰트·brand-claret 컬러·letter-spacing 으로 조판된 워드마크를 `<a>` 로 렌더한다. TopNav 좌상단·Footer 로고 슬롯의 홈 링크로 쓰는 텍스트 전용 로고이며(이미지·SVG 아님), size 세 단계와 표시 텍스트를 prop 으로 바꾼다. href 기본값은 `/` 라 별도 설정 없이 홈 링크로 동작한다.",
      "keywords": [
        "logo",
        "로고",
        "워드마크",
        "wordmark",
        "brand",
        "브랜드",
        "home",
        "axe labs"
      ],
      "props": [
        {
          "name": "size",
          "type": "\"sm\" | \"md\" | \"lg\"",
          "defaultValue": "\"md\"",
          "description": "워드마크 글자 크기. md 는 modifier 클래스 없는 기본."
        },
        {
          "name": "text",
          "type": "string",
          "defaultValue": "\"AXE Labs\"",
          "description": "표시(그리고 접근가능 이름이 되는) 텍스트."
        },
        {
          "name": "href",
          "type": "string",
          "defaultValue": "\"/\"",
          "description": "클릭 목적지. AnchorHTMLAttributes 상속 필드이나 기본 / 로 홈 링크."
        },
        {
          "name": "...rest",
          "type": "React.AnchorHTMLAttributes<HTMLAnchorElement>",
          "description": "className(axe-logo 뒤 병합)·target·onClick 등 나머지는 <a> 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-logo",
          "purpose": "워드마크 앵커 루트(display 폰트·brand-claret 컬러).",
          "stability": "stable"
        },
        {
          "className": "axe-logo--sm",
          "purpose": "sm 크기 변형(text-base).",
          "stability": "stable"
        },
        {
          "className": "axe-logo--lg",
          "purpose": "lg 크기 변형(text-2xl).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Tab 으로 포커스",
          "Enter 로 이동(네이티브 앵커)"
        ],
        "aria": "네이티브 <a> — text 가 접근가능 이름이 된다. 이미지가 아니라 aria-label 불요.",
        "notes": "text 를 빈 문자열로 두면 링크의 접근가능 이름이 사라진다 — 최소한의 브랜드 텍스트를 유지할 것. md 는 modifier 없이 axe-logo 단독."
      },
      "examples": [
        {
          "title": "기본",
          "lang": "tsx",
          "code": "import { Logo } from \"@axe/ui\";\n\n// 기본: text=\"AXE Labs\", size=\"md\", href=\"/\"\n<Logo />"
        },
        {
          "title": "크기·텍스트·목적지 지정",
          "lang": "tsx",
          "code": "import { Logo } from \"@axe/ui\";\n\n<Logo size=\"lg\" text=\"AXE\" href=\"https://axelabs.ai\" />"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<a class=\"axe-logo\" href=\"/\">AXE Labs</a>\n<a class=\"axe-logo axe-logo--sm\" href=\"/\">AXE Labs</a>\n<a class=\"axe-logo axe-logo--lg\" href=\"/\">AXE Labs</a>"
        }
      ],
      "bestPractices": {
        "do": [
          "헤더/푸터 좌상단의 홈 링크로 사용.",
          "text 는 브랜드명을 유지(접근가능 이름 = 이 텍스트)."
        ],
        "dont": [
          "text 를 빈 문자열로 두지 말 것 — 이름 없는 링크가 된다.",
          "강조 목적으로 lg 를 남발하지 말 것 — 한 뷰의 워드마크는 하나."
        ]
      },
      "consumeCss": "완전 CSS-only 소비 가능. `<a class=\"axe-logo axe-logo--lg\">` 처럼 루트 + 선택적 size modifier(sm/lg, md 는 무-modifier)만으로 동일 조판.",
      "url": "https://design.axelabs.ai/components/logo"
    },
    {
      "name": "MasterDetailLayout",
      "slug": "master-detail",
      "group": "layout",
      "summary": "반응형 2컬럼 master/detail 패턴(모바일에서 스택 + back).",
      "description": "좌측 목록(list/tree)과 우측 상세(detail)를 데스크톱에서는 가로로, 모바일(< breakpoint)에서는 한 컬럼씩 스택으로 보여준다. Cortex MemoryBrowser 의 list+detail 모바일 토글 패턴을 SSOT 로 흡수했다. 모바일 detail 화면 상단에 back 버튼이 자동으로 붙고, master↔detail 전이 시 포커스를 기억·복원한다. mobileShowDetail + onMobileBack 을 주면 controlled, 안 주면 detail !== null 여부로 자동 전환하는 uncontrolled 로 동작한다.",
      "keywords": [
        "master detail",
        "마스터 디테일",
        "list detail",
        "목록 상세",
        "responsive",
        "반응형",
        "2컬럼",
        "split view",
        "모바일 토글"
      ],
      "props": [
        {
          "name": "master",
          "type": "React.ReactNode",
          "required": true,
          "description": "좌측 컬럼 내용(list/tree)."
        },
        {
          "name": "detail",
          "type": "React.ReactNode | null",
          "required": true,
          "description": "우측 detail 내용. null 이면 emptyState(또는 기본 placeholder) 표시."
        },
        {
          "name": "masterWidth",
          "type": "number | string",
          "defaultValue": "\"320px\"",
          "description": "데스크톱 master 컬럼 폭(--axe-md-master-width). number=px."
        },
        {
          "name": "breakpoint",
          "type": "number",
          "defaultValue": "768",
          "description": "모바일 전환 임계 px(< breakpoint = 스택 모드)."
        },
        {
          "name": "mobileBackLabel",
          "type": "string",
          "defaultValue": "\"목록으로\"",
          "description": "모바일 detail 상단 back 버튼 텍스트."
        },
        {
          "name": "mobileShowDetail",
          "type": "boolean",
          "description": "controlled — true 면 모바일에서 detail 화면. onMobileBack 와 함께 사용."
        },
        {
          "name": "onMobileBack",
          "type": "() => void",
          "description": "controlled back 핸들러. mobileShowDetail 와 함께 주면 controlled 모드로 전환."
        },
        {
          "name": "emptyState",
          "type": "React.ReactNode",
          "description": "detail === null 일 때 표시할 빈 자리 노드(미지정 시 기본 placeholder)."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLDivElement>, \"children\">",
          "description": "className 은 병합, style 은 내부 CSS 변수와 병합, 나머지는 루트 div 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-master-detail",
          "purpose": "루트 flex row(데스크톱 2컬럼, gap --space-4).",
          "stability": "stable"
        },
        {
          "className": "axe-master-detail--mobile",
          "purpose": "JS 측정 후 부여 — flex column(스택), gap 0.",
          "stability": "stable"
        },
        {
          "className": "axe-master-detail__master",
          "purpose": "좌측 <aside> — 폭 --axe-md-master-width(기본 320px), overflow auto.",
          "stability": "stable"
        },
        {
          "className": "axe-master-detail__master--hidden",
          "purpose": "모바일 detail 화면에서 master 숨김(display:none).",
          "stability": "stable"
        },
        {
          "className": "axe-master-detail__detail",
          "purpose": "우측 <section> — flex:1 로 남는 공간 차지, overflow auto.",
          "stability": "stable"
        },
        {
          "className": "axe-master-detail__detail--hidden",
          "purpose": "모바일 master 화면에서 detail 숨김(display:none).",
          "stability": "stable"
        },
        {
          "className": "axe-master-detail__back",
          "purpose": "모바일 detail 상단 back 버튼(hover/focus 트림).",
          "stability": "stable"
        },
        {
          "className": "axe-master-detail__back-arrow",
          "purpose": "back 버튼 ← 화살표 span(aria-hidden).",
          "stability": "stable"
        },
        {
          "className": "axe-master-detail__empty",
          "purpose": "detail 미지정 + emptyState 없을 때의 기본 placeholder(\"항목을 선택하세요\").",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "back 버튼은 네이티브 <button> — Enter/Space 로 목록 복귀"
        ],
        "aria": "master=<aside>, detail=<section>. 모바일에서 숨는 컬럼에 aria-hidden 을 부여해 AX 트리에서 제외한다. back 버튼 aria-label = \"<mobileBackLabel> 돌아가기\", 화살표 span 은 aria-hidden.",
        "notes": "모바일 전이 시 포커스 관리: master→detail 진입 때 직전 master 내 포커스를 기억하고 back 버튼으로 이동(rAF 다음 프레임), detail→master 복귀 때 기억한 element 로 복원. 이 포커스 관리는 모바일(isMobile)에서만 동작한다. 뷰포트 감지는 matchMedia 기반 SSR-safe — 초기엔 데스크톱(false)으로 렌더 후 mount 시 보정한다."
      },
      "examples": [
        {
          "title": "uncontrolled (자동 전환)",
          "lang": "tsx",
          "code": "import { MasterDetailLayout } from \"@axe/ui\";\nimport { useState } from \"react\";\n\nfunction Browser() {\n  const [id, setId] = useState<string | null>(null);\n  return (\n    <MasterDetailLayout\n      master={<List onSelect={setId} />}\n      detail={id ? <Detail id={id} /> : null}\n      masterWidth=\"320px\"\n    />\n  );\n}"
        },
        {
          "title": "controlled (모바일 화면 소유)",
          "lang": "tsx",
          "code": "import { MasterDetailLayout } from \"@axe/ui\";\n\n<MasterDetailLayout\n  master={<List onSelect={open} />}\n  detail={selected ? <Detail item={selected} /> : null}\n  breakpoint={768}\n  mobileBackLabel=\"목록으로\"\n  mobileShowDetail={showDetail}\n  onMobileBack={() => setShowDetail(false)}\n/>"
        }
      ],
      "bestPractices": {
        "do": [
          "detail 이 비었을 때의 안내는 emptyState 로 명시적으로 준다.",
          "controlled 모드에서는 라우팅 상태(URL)와 mobileShowDetail 를 동기화한다."
        ],
        "dont": [
          "mobileShowDetail 만 주고 onMobileBack 을 빼먹지 말 것(back 이 무동작).",
          "master 폭을 지나치게 좁혀 목록이 조작 불가능하게 만들지 말 것."
        ]
      },
      "consumeCss": "데스크톱 2컬럼 정적 레이아웃은 `.axe-master-detail` 마크업으로 재현 가능하나, 모바일 토글·back·포커스 이동은 JS(matchMedia + 상태)에 의존 — CSS-only 로는 반응형 단일 컬럼 전환까지만. master 폭은 --axe-md-master-width 로 제어. (컴포넌트가 부여하는 `--show-detail` modifier 는 상태 훅일 뿐 대응 CSS 규칙이 없다.)",
      "url": "https://design.axelabs.ai/components/master-detail"
    },
    {
      "name": "MessageBubble",
      "slug": "message-bubble",
      "group": "chat",
      "summary": "아바타·역할 라벨·본문 슬롯을 가진 채팅 메시지 한 줄.",
      "description": "user/assistant/system 세 역할(role)로 한 메시지를 렌더하는 forwardRef<div> 컴포넌트다. 좌측 원형 아바타(기본 = 역할 첫 글자 U/A/S)와 우측 본문(역할 라벨 USER/AI/SYSTEM + children)으로 구성되며, children 에 StreamingMarkdown·ToolCallCard·첨부 등 아무 노드나 넣는다. 역할별로 axe-message--{role} 수정자 클래스가 붙지만 현재 시각 차이는 user 아바타의 accent 색뿐이다. avatar/label 을 넘겨 기본 문자·라벨을 덮어쓰고, showRole=false 로 라벨 줄을 감출 수 있다.",
      "keywords": [
        "message",
        "메시지",
        "chat bubble",
        "말풍선",
        "avatar",
        "역할",
        "user",
        "assistant",
        "system"
      ],
      "props": [
        {
          "name": "role",
          "type": "\"user\" | \"assistant\" | \"system\"",
          "required": true,
          "description": "메시지 화자. axe-message--{role} 수정자와 기본 아바타·라벨을 결정."
        },
        {
          "name": "children",
          "type": "React.ReactNode",
          "description": "본문 — markdown·tool call·첨부 등 어떤 노드든."
        },
        {
          "name": "avatar",
          "type": "React.ReactNode",
          "description": "아바타 자리에 들어갈 단일 문자 또는 컴포넌트. 기본 = 역할 첫 글자(U/A/S)."
        },
        {
          "name": "label",
          "type": "React.ReactNode",
          "description": "본문 위 라벨(시간·화자명 등). 기본 = 역할 라벨(USER/AI/SYSTEM)."
        },
        {
          "name": "showRole",
          "type": "boolean",
          "defaultValue": "true",
          "description": "역할 라벨 줄 표시 여부. false 면 라벨 없이 본문만."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLDivElement>",
          "description": "className 은 루트와 병합, 나머지는 루트 <div> 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-message",
          "purpose": "메시지 루트(아바타+본문 가로 flex, 세로 패딩).",
          "stability": "stable"
        },
        {
          "className": "axe-message--user",
          "purpose": "user 역할 수정자 — 아바타를 accent 색으로. (--assistant/--system 클래스도 붙지만 현재 전용 규칙 없음.)",
          "stability": "stable"
        },
        {
          "className": "axe-message__avatar",
          "purpose": "좌측 원형 아바타(28px, gray-9 배경, display 폰트). aria-hidden.",
          "stability": "stable"
        },
        {
          "className": "axe-message__body",
          "purpose": "우측 본문 컬럼(flex:1, min-width:0 으로 오버플로 방지).",
          "stability": "stable"
        },
        {
          "className": "axe-message__role",
          "purpose": "본문 위 역할 라벨(uppercase·wide tracking·secondary).",
          "stability": "stable"
        },
        {
          "className": "axe-message__content",
          "purpose": "children 래퍼 — 직계 형제 사이 space-3 세로 간격.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "루트는 의미 없는 <div> — 별도 role 을 붙이지 않는다. 아바타 요소는 aria-hidden 이라 스크린리더가 무시하고 역할은 텍스트 라벨(axe-message__role)로만 전달된다.",
        "notes": "키보드 상호작용 없음(순수 표시 컴포넌트). showRole=false 로 라벨을 숨기면 시각적 화자 구분이 아바타 색에만 의존하므로, 스크린리더 사용자에게 역할이 중요하면 label 을 남기거나 별도로 제공할 것."
      },
      "examples": [
        {
          "title": "기본 — assistant + user",
          "lang": "tsx",
          "code": "import { MessageBubble } from \"@axe/ui\";\n\nexport function Example() {\n  return (\n    <>\n      <MessageBubble role=\"user\">오늘 환율 알려줘</MessageBubble>\n      <MessageBubble role=\"assistant\" label=\"AI · 09:12\">\n        현재 원/달러 환율은 …\n      </MessageBubble>\n    </>\n  );\n}"
        },
        {
          "title": "커스텀 아바타 + 라벨 숨김",
          "lang": "tsx",
          "code": "import { MessageBubble } from \"@axe/ui\";\n\n<MessageBubble role=\"user\" avatar=\"강\" showRole={false}>\n  요약해줘\n</MessageBubble>"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-message axe-message--assistant\">\n  <div class=\"axe-message__avatar\" aria-hidden=\"true\">A</div>\n  <div class=\"axe-message__body\">\n    <div class=\"axe-message__role\">AI</div>\n    <div class=\"axe-message__content\">현재 원/달러 환율은 …</div>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "children 에 StreamingMarkdown·ToolCallCard 등을 넣어 한 메시지 안에 콘텐츠를 조합할 것.",
          "시간·화자명을 보여주려면 label 슬롯을 쓸 것(기본 역할 라벨을 대체)."
        ],
        "dont": [
          "역할을 문자열로 자유 지정하지 말 것 — \"user\"|\"assistant\"|\"system\" 만 유효.",
          "아바타에 의미 있는 정보를 넣지 말 것 — aria-hidden 이라 보조기술에 전달되지 않는다."
        ]
      },
      "consumeCss": "순수 표시 컴포넌트라 CSS-only 로 완전 재현 가능 — axe-message(+역할 수정자)/__avatar/__body/__role/__content 마크업만 맞추면 동일하다.",
      "url": "https://design.axelabs.ai/components/message-bubble"
    },
    {
      "name": "MessageWithSources",
      "slug": "message-with-sources",
      "group": "chat",
      "summary": "MessageBubble 확장 — 본문에 출처 인용·신뢰도 배지·토큰 메타·복사 버튼을 합친 RAG 응답 카드.",
      "description": "MessageBubble(텍스트) 위에 신뢰 가능한 AI 응답에 필요한 네 가지를 얹는다: (1) sources <details> 인용 목록, (2) 우상단 confidence 배지(0..1 → red/amber/green band), (3) footer 의 토큰/비용 metabar, (4) 복사 버튼. Cortex RecallChat 의 details+copy+confidence 패턴을 SSOT 로 일반화한 것으로, override 가 아니라 composition 이다 — role/avatar/label 등 MessageBubbleProps 를 그대로 상속(onCopy 만 자체 시그니처로 교체). Streaming-aware: sources/tokens 가 아직 undefined 면 해당 영역이 아예 그려지지 않아 progressive disclosure 가 자연스럽다.",
      "keywords": [
        "message with sources",
        "출처",
        "citation",
        "rag",
        "confidence",
        "신뢰도",
        "tokens",
        "토큰",
        "cost",
        "copy",
        "복사",
        "recall",
        "chat"
      ],
      "props": [
        {
          "name": "role",
          "type": "\"user\" | \"assistant\" | \"system\"",
          "required": true,
          "description": "MessageBubble 상속 — 아바타/라벨/정렬 결정."
        },
        {
          "name": "sources",
          "type": "SourceItem[]",
          "description": "인용 목록. undefined 또는 빈 배열이면 <details> 미렌더. SourceItem = { id, title, snippet?, href?, confidence?, type? }."
        },
        {
          "name": "tokens",
          "type": "TokenMeta",
          "description": "토큰/비용 메타. footer 에 in·out·cost 로 표시. TokenMeta = { input?, output?, cost? }(cost=USD)."
        },
        {
          "name": "confidence",
          "type": "number",
          "description": "0..1. 우상단 배지로 표시. <0.5 low(red) · <0.8 med(amber) · ≥0.8 high(green)."
        },
        {
          "name": "onCopy",
          "type": "(text: string) => void",
          "description": "복사 콜백. 미지정 시 navigator.clipboard.writeText 로 fallback."
        },
        {
          "name": "copyText",
          "type": "string",
          "description": "복사에 넘길 텍스트. 미지정 시 children 이 string 이면 그것, 아니면 빈 문자열."
        },
        {
          "name": "defaultSourcesOpen",
          "type": "boolean",
          "defaultValue": "false",
          "description": "sources <details> 를 처음부터 펼칠지."
        },
        {
          "name": "footer",
          "type": "React.ReactNode",
          "description": "토큰 metabar 옆(또는 대신) 붙는 자유 footer — 예: action 버튼."
        },
        {
          "name": "children",
          "type": "React.ReactNode",
          "description": "본문. string 이면 copyText 기본값으로도 쓰인다."
        },
        {
          "name": "avatar / label / showRole",
          "type": "MessageBubbleProps",
          "description": "MessageBubble 상속 props 그대로 통과."
        },
        {
          "name": "...rest",
          "type": "Omit<MessageBubbleProps, \"onCopy\">",
          "description": "그 외 MessageBubble/HTMLAttributes 는 루트로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-message-with-sources",
          "purpose": "루트 modifier(axe-message 위). content 를 세로 flex 로 재배치.",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__head",
          "purpose": "본문 텍스트 + 우상단 액션(confidence/copy)을 나누는 상단 행.",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__body-text",
          "purpose": "본문 텍스트 영역(pre-wrap, break-word).",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__head-actions",
          "purpose": "우상단 confidence 배지 + 복사 버튼 그룹.",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__confidence",
          "purpose": "confidence 배지. --low/--med/--high 변형이 band 색(danger/warning/success-soft)을 결정.",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__copy",
          "purpose": "복사 버튼(ghost, focus-visible ring).",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__sources",
          "purpose": "출처 <details> 컨테이너(상단 구분선).",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__sources-summary",
          "purpose": "<summary> '출처 N건' 토글(▸ 마커 회전).",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__sources-list",
          "purpose": "출처 항목 <ul>.",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__source",
          "purpose": "개별 출처 <li>(type|title|conf 3열 grid).",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__source-type",
          "purpose": "출처 분류 칩(doc/memory/web 등).",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__source-title",
          "purpose": "출처 제목 — href 있으면 <a target=_blank>, 없으면 <span>.",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__source-snippet",
          "purpose": "출처 발췌문(italic, 전 열 span).",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__source-conf",
          "purpose": "출처별 신뢰도 배지. --low/--med/--high band 색.",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__footer",
          "purpose": "토큰 metabar + 자유 footer 를 담는 하단 행(파선 구분).",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__tokens",
          "purpose": "in·out·cost 토큰 metabar(mono).",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__token",
          "purpose": "토큰 항목 하나(label + val).",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__token-label",
          "purpose": "토큰 라벨(in/out/cost, uppercase muted).",
          "stability": "stable"
        },
        {
          "className": "axe-message-with-sources__token-val",
          "purpose": "토큰 값(axe-tabular-nums 로 자릿수 정렬).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Enter / Space — sources <summary> 펼침/접힘(네이티브 <details>)",
          "Enter / Space — 복사 버튼 실행(네이티브 <button>)",
          "Tab — 출처 링크(<a>)로 순차 이동"
        ],
        "aria": "출처는 네이티브 <details>/<summary> 라 펼침 상태를 스스로 고지한다(수동 aria-expanded 불요). confidence 배지는 aria-label·title 로 'confidence NN%' 를 노출해 색 band 에만 의존하지 않는다. 복사 버튼은 aria-label=\"복사\" 이며, 복사 완료 고지는 버튼과 분리된 별도 visually-hidden span(role=\"status\" aria-live=\"polite\")이 담당한다 — 버튼 자체에 aria-live 를 박으면 hover/focus 마다 잡음이 생기므로 의도적으로 분리했다.",
        "notes": "출처 링크는 target=\"_blank\" 에 rel=\"noopener noreferrer\" 가 붙는다. sources/tokens 가 undefined 면 해당 영역이 렌더되지 않아, 스트리밍 도중 메타가 늦게 도착해도 레이아웃이 점진적으로 채워진다."
      },
      "examples": [
        {
          "title": "RAG 응답 (출처 + 신뢰도 + 토큰)",
          "lang": "tsx",
          "code": "import { MessageWithSources } from \"@axe/ui\";\n\n<MessageWithSources\n  role=\"assistant\"\n  confidence={0.82}\n  tokens={{ input: 1240, output: 320, cost: 0.0089 }}\n  sources={[\n    { id: \"1\", type: \"doc\", title: \"2024 감사보고서\", snippet: \"매출 142억…\", href: \"/docs/audit\", confidence: 0.9 },\n    { id: \"2\", type: \"memory\", title: \"전분기 IC 메모\", confidence: 0.6 },\n  ]}\n>\n  2024년 매출은 142억 원으로 전년 대비 18% 증가했습니다.\n</MessageWithSources>"
        },
        {
          "title": "최소 (본문 + 복사만)",
          "lang": "tsx",
          "code": "import { MessageWithSources } from \"@axe/ui\";\n\n<MessageWithSources role=\"assistant\" copyText=\"요약 결과\">\n  요약 결과\n</MessageWithSources>"
        },
        {
          "title": "CSS-only 정적 (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-message axe-message--assistant axe-message-with-sources\">\n  <div class=\"axe-message__avatar\" aria-hidden=\"true\">A</div>\n  <div class=\"axe-message__body\">\n    <div class=\"axe-message__content\">\n      <div class=\"axe-message-with-sources__head\">\n        <div class=\"axe-message-with-sources__body-text\">2024년 매출은 142억 원…</div>\n        <div class=\"axe-message-with-sources__head-actions\">\n          <span class=\"axe-message-with-sources__confidence axe-message-with-sources__confidence--high\">82%</span>\n        </div>\n      </div>\n      <details class=\"axe-message-with-sources__sources\">\n        <summary class=\"axe-message-with-sources__sources-summary\">출처 1건</summary>\n        <ul class=\"axe-message-with-sources__sources-list\">\n          <li class=\"axe-message-with-sources__source\">\n            <span class=\"axe-message-with-sources__source-type\">doc</span>\n            <a class=\"axe-message-with-sources__source-title\" href=\"/docs/audit\">2024 감사보고서</a>\n          </li>\n        </ul>\n      </details>\n    </div>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "children 이 문자열이면 copyText 를 생략해도 복사가 그 텍스트를 쓴다 — 노드면 copyText 를 명시한다.",
          "confidence 는 모델의 self-reported certainty 를 넘겨 환각 방어 신호로 쓴다.",
          "source.type 로 doc/memory/web 을 구분해 인용 출처의 성격을 드러낸다."
        ],
        "dont": [
          "children 이 ReactNode 인데 copyText 를 안 주면 복사가 빈 문자열이 된다 — 잊지 말 것.",
          "onCopy 미지정 시 클립보드 fallback 은 브라우저 환경 전제 — SSR 단독 경로에서 복사 동작을 기대하지 말 것."
        ]
      },
      "consumeCss": "대부분 CSS-only 로 재현 가능하다: 출처는 네이티브 <details>, confidence/source-conf band 는 --low/--med/--high 변형 클래스, 토큰 metabar 는 정적 마크업이면 충분하다. 다만 복사 버튼의 클립보드 쓰기·'복사됨' 토글·live region 고지는 JS 동작이므로 CSS-only 로는 정적 버튼만 남는다. 루트는 반드시 axe-message + axe-message--<role> 위에 axe-message-with-sources 를 함께 얹는다.",
      "url": "https://design.axelabs.ai/components/message-with-sources"
    },
    {
      "name": "MetricCard",
      "slug": "metric-card",
      "group": "data",
      "summary": "운영자·에이전트 대시보드용 KPI 타일 — 값 + 증감(delta) + 미니 스파크라인.",
      "description": "label/value 를 큰 숫자로 세우고, 선택적으로 delta(방향+값+라벨)·미니 sparkline(inline SVG)·우상단 icon·회색 helperText 를 붙이는 KPI 타일. value 에는 항상 tabular-nums 가 적용돼 동적 갱신 시에도 자릿수가 흔들리지 않는다. loading=true 면 자체 skeleton 모드로 전환돼 별도 Skeleton 컴포넌트가 필요 없다. sparkline 은 축·툴팁 없는 6~24 point 추세선이며 데이터가 2점 미만이면 그려지지 않는다.",
      "keywords": [
        "metric",
        "metric card",
        "kpi",
        "지표",
        "카드",
        "delta",
        "증감",
        "sparkline",
        "스파크라인",
        "dashboard",
        "대시보드",
        "stat"
      ],
      "props": [
        {
          "name": "label",
          "type": "React.ReactNode",
          "required": true,
          "description": "지표 이름 — head 좌측에 작은 라벨로."
        },
        {
          "name": "value",
          "type": "React.ReactNode",
          "required": true,
          "description": "주 수치 — 큰 글씨 + tabular-nums 로 렌더."
        },
        {
          "name": "delta",
          "type": "MetricDelta",
          "description": "증감 배지. `{ value: ReactNode; direction: \"up\" | \"down\" | \"flat\"; label?: ReactNode }`. direction 이 색과 화살표(▲/▼/—)를 정한다 — up=success, down=danger, flat=muted. label 은 'vs. 어제' 같은 부가 맥락."
        },
        {
          "name": "trend",
          "type": "number[]",
          "description": "미니 sparkline 데이터(6~24 point 권장). 2점 미만이면 미표시. currentColor 폴리라인만 그리는 무의존 inline SVG."
        },
        {
          "name": "icon",
          "type": "React.ReactNode",
          "description": "우상단 아이콘 슬롯(보통 16~18px)."
        },
        {
          "name": "helperText",
          "type": "React.ReactNode",
          "description": "값 아래 회색 부가 문장(예: '최근 30일'). delta 와 같은 meta 줄에 놓인다."
        },
        {
          "name": "loading",
          "type": "boolean",
          "description": "true 면 label/value/helper 자리에 skeleton 3줄을 대신 그리고 aria-busy=\"true\" 를 세운다."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLDivElement>, \"title\">",
          "description": "className 병합, 나머지는 루트 div 로 전파. ref 는 루트 div 로 forward."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-metric-card",
          "purpose": "루트 div — border·radius·padding 카드.",
          "stability": "stable"
        },
        {
          "className": "axe-metric-card__head",
          "purpose": "label + icon 을 좌우로 배치하는 상단 줄.",
          "stability": "stable"
        },
        {
          "className": "axe-metric-card__label",
          "purpose": "지표 이름 라벨.",
          "stability": "stable"
        },
        {
          "className": "axe-metric-card__icon",
          "purpose": "우상단 아이콘 박스.",
          "stability": "stable"
        },
        {
          "className": "axe-metric-card__value",
          "purpose": "주 수치 — 큰 글씨 + tabular-nums.",
          "stability": "stable"
        },
        {
          "className": "axe-metric-card__meta",
          "purpose": "delta + helper 를 담는 값 아래 줄.",
          "stability": "stable"
        },
        {
          "className": "axe-metric-card__delta",
          "purpose": "증감 배지 컨테이너(화살표+값+라벨).",
          "stability": "stable"
        },
        {
          "className": "axe-metric-card__delta--up",
          "purpose": "상승 — success 색.",
          "stability": "stable"
        },
        {
          "className": "axe-metric-card__delta--down",
          "purpose": "하락 — danger 색.",
          "stability": "stable"
        },
        {
          "className": "axe-metric-card__delta--flat",
          "purpose": "변화 없음 — muted 색.",
          "stability": "stable"
        },
        {
          "className": "axe-metric-card__delta-arrow",
          "purpose": "방향 화살표 글리프(▲/▼/—). aria-hidden.",
          "stability": "stable"
        },
        {
          "className": "axe-metric-card__delta-label",
          "purpose": "delta 부가 라벨(예: 'vs. 어제').",
          "stability": "stable"
        },
        {
          "className": "axe-metric-card__helper",
          "purpose": "값 아래 회색 부가 문장.",
          "stability": "stable"
        },
        {
          "className": "axe-metric-card__sparkline",
          "purpose": "미니 추세 SVG(polyline, currentColor).",
          "stability": "stable"
        },
        {
          "className": "axe-metric-card--loading",
          "purpose": "skeleton 모드 마커.",
          "stability": "internal"
        },
        {
          "className": "axe-metric-card__skel",
          "purpose": "skeleton 플레이스홀더 바(shimmer ::after).",
          "stability": "internal"
        },
        {
          "className": "axe-metric-card__skel--label",
          "purpose": "label 자리 skeleton(폭 40%).",
          "stability": "internal"
        },
        {
          "className": "axe-metric-card__skel--value",
          "purpose": "value 자리 skeleton(폭 70%).",
          "stability": "internal"
        },
        {
          "className": "axe-metric-card__skel--helper",
          "purpose": "helper 자리 skeleton(폭 55%).",
          "stability": "internal"
        }
      ],
      "a11y": {
        "aria": "루트는 일반 div — 별도 role 없음. loading=true 동안 aria-busy=\"true\". delta 화살표와 sparkline SVG 는 aria-hidden(focusable=false) 이라 보조기술은 값 텍스트만 읽는다.",
        "notes": "방향(up/down)은 색과 화살표로만 시각 전달되므로, 스크린리더 사용자를 위해 delta.value 나 delta.label 에 부호·맥락(예: '+12.3%', '전주 대비 상승')을 담아 텍스트만으로 의미가 서게 할 것. value 의 tabular-nums 는 갱신 시 자릿수 정렬을 보장한다."
      },
      "examples": [
        {
          "title": "delta + sparkline",
          "lang": "tsx",
          "code": "import { MetricCard } from \"@axe/ui\";\n\n<MetricCard\n  label=\"활성 세션\"\n  value=\"1,284\"\n  delta={{ value: \"+12.3%\", direction: \"up\", label: \"vs. 어제\" }}\n  trend={[8, 11, 9, 14, 13, 18, 21]}\n  helperText=\"최근 30일\"\n/>"
        },
        {
          "title": "로딩 skeleton",
          "lang": "tsx",
          "code": "import { MetricCard } from \"@axe/ui\";\n\n<MetricCard label=\"집계 중\" value=\"—\" loading />"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-metric-card\">\n  <div class=\"axe-metric-card__head\">\n    <span class=\"axe-metric-card__label\">활성 세션</span>\n    <span class=\"axe-metric-card__icon\">◇</span>\n  </div>\n  <div class=\"axe-metric-card__value\">1,284</div>\n  <div class=\"axe-metric-card__meta\">\n    <span class=\"axe-metric-card__delta axe-metric-card__delta--up\">\n      <span class=\"axe-metric-card__delta-arrow\" aria-hidden=\"true\">▲</span>\n      <span class=\"axe-metric-card__delta-value\">+12.3%</span>\n      <span class=\"axe-metric-card__delta-label\">vs. 어제</span>\n    </span>\n    <span class=\"axe-metric-card__helper\">최근 30일</span>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "delta.label 로 비교 기준을 명시(무엇 대비 증감인지).",
          "sparkline 은 같은 축척의 시계열에만 — 절대값보다 추세를 보여줄 때.",
          "여러 타일을 나열할 땐 grid/cluster 로 폭을 맞춰 tabular-nums 정렬을 살릴 것."
        ],
        "dont": [
          "방향 정보를 색·화살표에만 의존하지 말 것(색맹·스크린리더).",
          "value 에 단위를 섞어 자릿수를 흔들지 말 것 — 단위는 helperText 나 label 로."
        ]
      },
      "consumeCss": "Radix 무의존·순수 CSS 라 마크업만 맞추면 CSS-only 로 동일 재현 가능. 단 sparkline 은 호스트가 polyline points 를 직접 계산해 axe-metric-card__sparkline SVG 로 넣어야 하고(컴포넌트는 viewBox 0 0 100 24 에 min/max 정규화), delta 화살표 글리프도 direction 에 맞춰 넣어야 한다. delta-value span 은 CSS 규칙이 없는 순수 마크업 훅.",
      "url": "https://design.axelabs.ai/components/metric-card"
    },
    {
      "name": "NavMenu",
      "slug": "nav-menu",
      "group": "navigation",
      "summary": "TopNav 우측에 얹는 통일된 드롭다운 메뉴 1종 (entity-selector · user-menu).",
      "description": "\"버튼 → 팝오버 → 선택 항목 + (선택) 헤더/푸터 액션\" 이라는 동일 구조를 데이터(props)로만 분기하는 단일 컴포넌트다. items 에 active 를 주면 role=menuitemradio 로 렌더되어 entity-selector(작업 대상 전환)가 되고, footer 에 tone=\"danger\" 액션을 두면 user-menu(로그아웃)가 된다. Radix 대신 자체 구현이라 a11y(aria-haspopup/expanded/checked·키보드·focus 복귀)와 click-outside 를 직접 처리한다. 서브메뉴/타입어헤드 같은 무거운 사용처는 DropdownMenu(Radix)를 쓸 것.",
      "keywords": [
        "navmenu",
        "nav menu",
        "dropdown",
        "entity selector",
        "user menu",
        "계정 메뉴",
        "조직 선택",
        "메뉴"
      ],
      "props": [
        {
          "name": "label",
          "type": "React.ReactNode",
          "required": true,
          "description": "트리거 버튼 라벨 (현재 선택 entity / 사용자 이름 등)."
        },
        {
          "name": "items",
          "type": "NavMenuItem[]",
          "required": true,
          "description": "선택 항목 배열. NavMenuItem = { key: string; label: React.ReactNode; meta?: React.ReactNode; active?: boolean; href?: string; onSelect?: () => void }. active 가 정의되면 role=menuitemradio + aria-checked 로 렌더, undefined 면 일반 menuitem. href 면 <a>, 아니면 <button>."
        },
        {
          "name": "header",
          "type": "{ label: React.ReactNode; items?: NavMenuFooterItem[] }",
          "description": "items 위 헤더 영역 — 라벨(현재 계정 이름 등) + (선택) 액션, 아래 구분선. 3-그룹 레이아웃(헤더·items·footer)용."
        },
        {
          "name": "footer",
          "type": "NavMenuFooterItem[]",
          "description": "구분선 아래 액션 영역. NavMenuFooterItem = { key: string; label: React.ReactNode; href?: string; onSelect?: () => void; tone?: \"default\" | \"danger\" }. tone=\"danger\" = 파괴적 액션(로그아웃·계정 삭제)."
        },
        {
          "name": "align",
          "type": "\"left\" | \"right\"",
          "defaultValue": "\"right\"",
          "description": "팝오버 정렬 — trigger 기준."
        },
        {
          "name": "ariaLabel",
          "type": "string",
          "required": true,
          "description": "스크린리더용 메뉴 라벨 (role=menu 의 aria-label). 필수."
        },
        {
          "name": "accent",
          "type": "boolean",
          "defaultValue": "false",
          "description": "accent 톤 트리거 (강조가 필요한 entity-selector 등)."
        },
        {
          "name": "emptyLabel",
          "type": "React.ReactNode",
          "defaultValue": "\"항목 없음\"",
          "description": "items 가 비었을 때 표시할 문구."
        },
        {
          "name": "data-testid",
          "type": "string",
          "description": "루트 wrapper 에 부착할 testid."
        },
        {
          "name": "menuTestid",
          "type": "string",
          "description": "팝오버 노드에 부착할 testid."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-topnav__menu",
          "purpose": "wrapper (trigger + popover 위치 기준 root).",
          "stability": "stable"
        },
        {
          "className": "axe-topnav__menu-trigger",
          "purpose": "트리거 버튼 (data-state open/closed, data-accent).",
          "stability": "stable"
        },
        {
          "className": "axe-topnav__menu-trigger-label",
          "purpose": "트리거 라벨 span.",
          "stability": "internal"
        },
        {
          "className": "axe-topnav__menu-chevron",
          "purpose": "chevron svg (open 시 CSS 로 회전).",
          "stability": "internal"
        },
        {
          "className": "axe-topnav__menu-popover",
          "purpose": "드롭다운 팝오버 (role=menu, data-align left/right).",
          "stability": "stable"
        },
        {
          "className": "axe-topnav__menu-item",
          "purpose": "항목/액션 (a 또는 button).",
          "stability": "stable"
        },
        {
          "className": "axe-topnav__menu-item--active",
          "purpose": "선택된 항목.",
          "stability": "stable"
        },
        {
          "className": "axe-topnav__menu-item--danger",
          "purpose": "파괴적 액션 (tone=\"danger\").",
          "stability": "stable"
        },
        {
          "className": "axe-topnav__menu-item-label",
          "purpose": "항목 라벨 span.",
          "stability": "internal"
        },
        {
          "className": "axe-topnav__menu-item-meta",
          "purpose": "우측 muted 태그 span (역할·count).",
          "stability": "internal"
        },
        {
          "className": "axe-topnav__menu-empty",
          "purpose": "items 빈 상태 문구.",
          "stability": "internal"
        },
        {
          "className": "axe-topnav__menu-header",
          "purpose": "헤더 그룹 컨테이너 (라벨 + 액션, 아래 구분선).",
          "stability": "stable"
        },
        {
          "className": "axe-topnav__menu-header-label",
          "purpose": "헤더 라벨.",
          "stability": "internal"
        },
        {
          "className": "axe-topnav__menu-footer",
          "purpose": "푸터 그룹 컨테이너 (구분선 위 액션 묶음).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "↑/↓ 로 메뉴 항목 순환 (양끝에서 wrap)",
          "Enter/Space 로 항목 실행 (네이티브 button/a)",
          "Esc 로 닫고 트리거로 focus 복원",
          "열리면 첫 항목으로 자동 focus"
        ],
        "aria": "트리거 = aria-haspopup=\"menu\" + aria-expanded + aria-controls(열림 시 popover id). 팝오버 = role=\"menu\" + aria-label(=ariaLabel prop). 항목: active 정의 시 role=\"menuitemradio\" + aria-checked, 아니면 role=\"menuitem\"(href 항목은 active 시 aria-current=\"true\"). header/footer 액션 = role=\"menuitem\", 빈 상태 = role=\"presentation\".",
        "notes": "click-outside(pointerdown capture)로 닫힘. 항목 실행 후 자동 닫힘 + 트리거 focus 복원. reduced-motion 시 팝오버 등장 애니 없음. Radix 미사용 자체 구현, SSR-safe(초기 렌더는 닫힘)."
      },
      "examples": [
        {
          "title": "entity-selector (active 표시)",
          "lang": "tsx",
          "code": "import { NavMenu } from \"@axe/ui\";\n\n<NavMenu\n  label=\"AXE Ventures\"\n  ariaLabel=\"작업 대상 조직 선택\"\n  accent\n  items={[\n    { key: \"axev\", label: \"AXE Ventures\", active: true },\n    { key: \"axec\", label: \"AXE Capital\", active: false, onSelect: () => switchOrg(\"axec\") },\n  ]}\n/>"
        },
        {
          "title": "user-menu (header + footer danger)",
          "lang": "tsx",
          "code": "import { NavMenu } from \"@axe/ui\";\n\n<NavMenu\n  label=\"강수훈\"\n  ariaLabel=\"계정 메뉴\"\n  header={{ label: \"강수훈 · admin\" }}\n  items={[\n    { key: \"settings\", label: \"설정\", href: \"/settings\" },\n  ]}\n  footer={[\n    { key: \"logout\", label: \"로그아웃\", tone: \"danger\", onSelect: signOut },\n  ]}\n/>"
        },
        {
          "title": "CSS-only 열린 팝오버 (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-topnav__menu\">\n  <button class=\"axe-topnav__menu-trigger\" aria-haspopup=\"menu\" aria-expanded=\"true\">\n    <span class=\"axe-topnav__menu-trigger-label\">AXE Ventures</span>\n  </button>\n  <div class=\"axe-topnav__menu-popover\" role=\"menu\" aria-label=\"조직 선택\" data-align=\"right\">\n    <button class=\"axe-topnav__menu-item axe-topnav__menu-item--active\" role=\"menuitemradio\" aria-checked=\"true\">\n      <span class=\"axe-topnav__menu-item-label\">AXE Ventures</span>\n    </button>\n    <button class=\"axe-topnav__menu-item\" role=\"menuitemradio\" aria-checked=\"false\">\n      <span class=\"axe-topnav__menu-item-label\">AXE Capital</span>\n    </button>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "ariaLabel 은 항상 지정 (SR 필수).",
          "entity-selector 는 items[].active 로 현재 선택을 표시.",
          "파괴적 액션(로그아웃)은 footer + tone=\"danger\" 로."
        ],
        "dont": [
          "서브메뉴/타입어헤드 같은 무거운 메뉴에 쓰지 말 것 → DropdownMenu(Radix) 사용.",
          "label 없이 아이콘만 트리거로 쓰지 말 것 (트리거 라벨이 곧 접근명)."
        ]
      },
      "consumeCss": "팝오버 열림/닫힘·키보드 순환·click-outside·focus 복원은 모두 컴포넌트 JS(useState)에 의존한다. CSS-only 로는 위 예제처럼 정적 팝오버 모양만 재현 가능(토글 동작 없음). axe-topnav__menu* 네임스페이스는 TopNav right 슬롯에 얹히도록 설계된 공유 계약이다.",
      "url": "https://design.axelabs.ai/components/nav-menu"
    },
    {
      "name": "Pagination",
      "slug": "pagination",
      "group": "navigation",
      "summary": "페이지 네비게이션 컨트롤 (DataTable 와 짝).",
      "description": "page/pageSize/total 세 값만 받으면 페이지 버튼(boundary + sibling + ellipsis), \"X-Y / Total\" 정보 줄, ←/→ 키보드 이동을 자체 계산해 제공하는 controlled 컴포넌트다. 페이지 수가 boundaryCount·siblingCount 로 정해지는 한계 이하면 ellipsis 없이 전부 표시한다. compact 모드는 좁은 컨테이너/모바일용으로 이전/다음 + \"현재/총\" 만 노출한다. 숫자는 tabular-nums 로 자리수를 시각적으로 안정시키고 총량은 ko-KR 콤마로 포맷한다.",
      "keywords": [
        "pagination",
        "페이지",
        "페이지네이션",
        "paging",
        "page navigation",
        "datatable"
      ],
      "props": [
        {
          "name": "page",
          "type": "number",
          "required": true,
          "description": "1-based 현재 페이지. 내부적으로 [1, totalPages] 로 clamp."
        },
        {
          "name": "pageSize",
          "type": "number",
          "required": true,
          "description": "페이지당 항목 수 (정보 줄·totalPages 계산에 사용)."
        },
        {
          "name": "total",
          "type": "number",
          "required": true,
          "description": "전체 항목 수 (페이지 수가 아님). 0 이면 페이지 리스트 숨김."
        },
        {
          "name": "onPageChange",
          "type": "(page: number) => void",
          "required": true,
          "description": "페이지 변경 콜백. 상위 상태를 갱신 (controlled)."
        },
        {
          "name": "siblingCount",
          "type": "number",
          "defaultValue": "1",
          "description": "활성 페이지 좌우로 보여줄 페이지 수."
        },
        {
          "name": "boundaryCount",
          "type": "number",
          "defaultValue": "1",
          "description": "시작/끝에서 항상 보여줄 페이지 수."
        },
        {
          "name": "showInfo",
          "type": "boolean",
          "defaultValue": "true",
          "description": "\"X-Y / Total\" 정보 줄 표시 (compact 시 무시)."
        },
        {
          "name": "size",
          "type": "\"sm\" | \"md\"",
          "defaultValue": "\"md\"",
          "description": "버튼 크기. md 는 기본(별도 CSS 규칙 없음), sm 은 28px 축소."
        },
        {
          "name": "compact",
          "type": "boolean",
          "defaultValue": "false",
          "description": "모바일/좁은 컨테이너용 — 이전/다음 + \"현재/총\" 만 표시. size 무시."
        },
        {
          "name": "ariaLabel",
          "type": "string",
          "defaultValue": "\"Pagination · 페이지 네비게이션 (←/→ 키)\"",
          "description": "SR 용 nav label."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLElement>",
          "description": "나머지는 루트 <nav> 로 전파 (onChange 는 Omit)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-pagination",
          "purpose": "<nav> 루트 (flex space-between, tabular-nums).",
          "stability": "stable"
        },
        {
          "className": "axe-pagination--size-sm",
          "purpose": "sm 크기 변형 (버튼 28px).",
          "stability": "stable"
        },
        {
          "className": "axe-pagination--compact",
          "purpose": "compact 모바일 변형 (중앙 정렬, 이전/다음 + n/총).",
          "stability": "stable"
        },
        {
          "className": "axe-pagination__info",
          "purpose": "\"X-Y / Total\" 정보 줄.",
          "stability": "stable"
        },
        {
          "className": "axe-pagination__info-range",
          "purpose": "범위(X-Y) — primary 색.",
          "stability": "internal"
        },
        {
          "className": "axe-pagination__info-sep",
          "purpose": "정보 줄 구분자.",
          "stability": "internal"
        },
        {
          "className": "axe-pagination__info-total",
          "purpose": "총량.",
          "stability": "internal"
        },
        {
          "className": "axe-pagination__list",
          "purpose": "페이지 버튼 <ul> (←/→ 키 핸들러가 붙는 영역).",
          "stability": "stable"
        },
        {
          "className": "axe-pagination__btn",
          "purpose": "페이지/nav 버튼 (32px, hover/focus/disabled).",
          "stability": "stable"
        },
        {
          "className": "axe-pagination__btn--active",
          "purpose": "현재 페이지 버튼 (accent 배경).",
          "stability": "stable"
        },
        {
          "className": "axe-pagination__btn--nav",
          "purpose": "이전/다음 (‹ ›) 버튼.",
          "stability": "stable"
        },
        {
          "className": "axe-pagination__ellipsis",
          "purpose": "생략(…) 표시.",
          "stability": "stable"
        },
        {
          "className": "axe-pagination__compact-label",
          "purpose": "compact 모드 \"n / 총\" 컨테이너.",
          "stability": "internal"
        },
        {
          "className": "axe-pagination__compact-current",
          "purpose": "compact 현재 페이지 (semibold).",
          "stability": "internal"
        },
        {
          "className": "axe-pagination__compact-sep",
          "purpose": "compact 구분자.",
          "stability": "internal"
        },
        {
          "className": "axe-pagination__compact-total",
          "purpose": "compact 총 페이지.",
          "stability": "internal"
        }
      ],
      "a11y": {
        "keyboard": [
          "<ul> 영역에서 ← / → 로 이전/다음 페이지 이동 (경계에서 무시)",
          "버튼은 Enter/Space 로 실행 (네이티브 button)"
        ],
        "aria": "루트 <nav aria-label>(ariaLabel prop). 현재 페이지 버튼 = aria-current=\"page\". 이전/다음 = aria-label \"이전 페이지\"/\"다음 페이지\" + 경계 시 aria-disabled(+실제 disabled). 페이지 버튼 aria-label \"N페이지\"(현재 시 \"· 현재\" 추가). ellipsis <li> = aria-hidden=\"true\". compact 라벨 = aria-current=\"page\".",
        "notes": "←/→ 는 리스트 영역 포커스에서만 동작(정보 줄/외곽 nav 는 무시). total=0 이면 페이지 리스트를 숨기고 \"0 / 0\" 정보만 노출. reduced-motion 시 버튼 트랜지션 없음."
      },
      "examples": [
        {
          "title": "기본 (controlled)",
          "lang": "tsx",
          "code": "import { Pagination } from \"@axe/ui\";\n\nconst [page, setPage] = useState(1);\n\n<Pagination page={page} pageSize={20} total={480} onPageChange={setPage} />"
        },
        {
          "title": "compact (모바일)",
          "lang": "tsx",
          "code": "import { Pagination } from \"@axe/ui\";\n\n<Pagination page={3} pageSize={20} total={480} compact onPageChange={setPage} />"
        },
        {
          "title": "CSS-only 정적 바 (비-React)",
          "lang": "html",
          "code": "<nav class=\"axe-pagination axe-pagination--size-md\" aria-label=\"Pagination\">\n  <div class=\"axe-pagination__info\">\n    <span class=\"axe-pagination__info-range\">1-20</span>\n    <span class=\"axe-pagination__info-sep\"> / </span>\n    <span class=\"axe-pagination__info-total\">480</span>\n  </div>\n  <ul class=\"axe-pagination__list\">\n    <li><button class=\"axe-pagination__btn axe-pagination__btn--nav\" aria-label=\"이전 페이지\" disabled aria-disabled=\"true\">‹</button></li>\n    <li><button class=\"axe-pagination__btn axe-pagination__btn--active\" aria-current=\"page\" aria-label=\"1페이지 · 현재\">1</button></li>\n    <li><button class=\"axe-pagination__btn\" aria-label=\"2페이지\">2</button></li>\n    <li aria-hidden=\"true\"><span class=\"axe-pagination__ellipsis\">…</span></li>\n    <li><button class=\"axe-pagination__btn\" aria-label=\"24페이지\">24</button></li>\n    <li><button class=\"axe-pagination__btn axe-pagination__btn--nav\" aria-label=\"다음 페이지\">›</button></li>\n  </ul>\n</nav>"
        }
      ],
      "bestPractices": {
        "do": [
          "page 는 1-based 로 넘기고 onPageChange 에서 상위 상태를 갱신 (controlled).",
          "좁은 컨테이너/모바일은 compact 사용.",
          "큰 total 은 siblingCount/boundaryCount 로 표시 밀도 조절."
        ],
        "dont": [
          "total 을 페이지 수로 착각 말 것 — 전체 항목 개수다.",
          "onPageChange 없이 순수 표시로만 쓰지 말 것 (버튼이 controlled)."
        ]
      },
      "consumeCss": "시각 바는 위 html 예제 마크업으로 재현 가능하나, 페이지 전환·←/→ 키·ellipsis 계산은 컴포넌트 JS(onPageChange)가 담당한다. CSS-only 는 정적 표시용이며 버튼은 동작하지 않는다. size=\"md\" 는 emit 되지만 별도 CSS 규칙이 없는 기본값이다.",
      "url": "https://design.axelabs.ai/components/pagination"
    },
    {
      "name": "PipelineTable",
      "slug": "pipeline-table",
      "group": "data",
      "summary": "run/job/agent 상태 행에 특화된 표 — 행 좌측 status dot + drill-down 클릭.",
      "description": "운영자 대시보드의 상태 테이블. getStatus(row) 를 주면 각 행 좌측에 StatusLight dot 이 자동으로 붙고(4-state: pending=muted·running=running·success=ok·error=danger 와 StatusLight variant 동일), 미지정 시 dot 컬럼 자체가 사라진다. running/blue 상태에서는 pulse 로 \"살아있음\"을 표시한다(pulseOnRunning). onRowClick 으로 drill-down, loading 시 skeleton, 빈 rows 시 emptyState 를 렌더. 정렬/페이징이 필요하면 DataTable, 스트리밍 커서가 필요하면 StreamingTable 을 쓴다.",
      "keywords": [
        "pipeline table",
        "파이프라인",
        "상태표",
        "status",
        "run",
        "job",
        "agent",
        "운영자",
        "dashboard",
        "drill-down"
      ],
      "props": [
        {
          "name": "columns",
          "type": "PipelineColumn<Row>[]",
          "required": true,
          "description": "열 정의 배열. 각 항목: key(식별자) · header(ReactNode) · align?(\"left\"|\"right\"|\"center\") · width?(CSS 폭, 미지정 시 auto) · render?(셀 커스텀, 미지정 시 row[key] 를 string/number 로 출력)."
        },
        {
          "name": "rows",
          "type": "Row[]",
          "required": true,
          "description": "표시할 상태 행. Row 는 제네릭."
        },
        {
          "name": "getStatus",
          "type": "(row: Row) => StatusVariant",
          "description": "행별 상태 → 좌측 dot(StatusLight). 미지정 시 status 컬럼 자체가 생략된다. StatusVariant = ok|green|warning|amber|danger|red|muted|gray|running|blue."
        },
        {
          "name": "pulseOnRunning",
          "type": "boolean",
          "defaultValue": "true",
          "description": "status 가 running 또는 blue 일 때 dot 을 pulse. reduced-motion 시 자동으로 정적."
        },
        {
          "name": "getRowKey",
          "type": "(row: Row, index: number) => string",
          "description": "React key + onRowClick 대상 식별. 미지정 시 배열 index."
        },
        {
          "name": "onRowClick",
          "type": "(row: Row) => void",
          "description": "행 클릭(drill-down) 콜백. 지정하면 행이 role=button+tabIndex 위젯이 되고 Enter/Space 로도 실행된다(DataTable 과 달리 별도 rowClickable 플래그 없음)."
        },
        {
          "name": "loading",
          "type": "boolean",
          "description": "true 면 tbody 자리에 skeleton 행(dot + bar)을 렌더."
        },
        {
          "name": "loadingRows",
          "type": "number",
          "defaultValue": "5",
          "description": "loading skeleton 행 수."
        },
        {
          "name": "emptyState",
          "type": "React.ReactNode",
          "description": "rows 가 비었을 때 placeholder. 미지정 시 \"데이터 없음\"."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLDivElement>, \"onClick\">",
          "description": "className 병합 후 나머지는 루트 div 로 전파. onClick 은 행 단위 onRowClick 과 혼동을 막으려 제외. forwardRef 는 루트 div 를 가리킨다."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-pipeline-table",
          "purpose": "루트 div — elevated 배경·border·radius-lg·overflow hidden(radius clip).",
          "stability": "stable"
        },
        {
          "className": "axe-pipeline-table__scroll",
          "purpose": "가로 스크롤 컨테이너(iOS 모멘텀).",
          "stability": "stable"
        },
        {
          "className": "axe-pipeline-table__table",
          "purpose": "`<table>` — width 100%·min-width 600px·border-collapse.",
          "stability": "stable"
        },
        {
          "className": "axe-pipeline-table__thead",
          "purpose": "thead — subtle 배경.",
          "stability": "stable"
        },
        {
          "className": "axe-pipeline-table__th",
          "purpose": "헤더 셀 — xs·uppercase·secondary. `--right/--center` 로 정렬.",
          "stability": "stable"
        },
        {
          "className": "axe-pipeline-table__th--status",
          "purpose": "status dot 열의 헤더 — 32px 고정 폭(라벨 없는 빈 셀).",
          "stability": "stable"
        },
        {
          "className": "axe-pipeline-table__td",
          "purpose": "본문 셀 — tabular-nums·middle 정렬. `--right/--center` 로 정렬.",
          "stability": "stable"
        },
        {
          "className": "axe-pipeline-table__td--status",
          "purpose": "status dot 셀 — 32px·중앙 정렬.",
          "stability": "stable"
        },
        {
          "className": "axe-pipeline-table__tr",
          "purpose": "본문 행 — 하단 border·hover 시 accent 4% 배경.",
          "stability": "stable"
        },
        {
          "className": "axe-pipeline-table__tr--clickable",
          "purpose": "onRowClick 이 있을 때 — cursor pointer·focus-visible outline.",
          "stability": "stable"
        },
        {
          "className": "axe-pipeline-table__empty",
          "purpose": "빈 상태 셀 — 중앙 muted 텍스트(colSpan 전폭).",
          "stability": "stable"
        },
        {
          "className": "axe-pipeline-table__tr--empty",
          "purpose": "빈 상태 행 마커 — hover 무효화.",
          "stability": "internal"
        },
        {
          "className": "axe-pipeline-table__tr--skeleton",
          "purpose": "loading skeleton 행.",
          "stability": "internal"
        },
        {
          "className": "axe-pipeline-table__skel",
          "purpose": "shimmer 플레이스홀더(indicators.css 의 axe-shimmer 공유).",
          "stability": "internal"
        },
        {
          "className": "axe-pipeline-table__skel--dot",
          "purpose": "skeleton status dot(8px 원형).",
          "stability": "internal"
        },
        {
          "className": "axe-pipeline-table__skel--bar",
          "purpose": "skeleton 텍스트 바(70%·12px).",
          "stability": "internal"
        }
      ],
      "a11y": {
        "keyboard": [
          "onRowClick 이 있는 행은 Enter/Space 로 실행(preventDefault 로 스크롤 방지)"
        ],
        "aria": "onRowClick 이 있을 때만 행이 role=\"button\"+tabIndex=0 를 받는다(없으면 순수 table 시맨틱). status dot 은 StatusLight — 내부 dot span 이 aria-hidden 이라 상태는 순수 시각 신호다.",
        "notes": "PipelineTable 은 StatusLight 를 label 없이 dot-only 로 렌더하므로 스크린리더는 상태를 읽지 못한다 — SR 사용자에게 상태가 필요하면 별도 텍스트/뱃지 컬럼(render 로)이나 행 aria-label 로 보강한다. onRowClick 을 쓸 때는 셀 안에 링크/버튼을 넣지 않는다(nested interactive 충돌)."
      },
      "examples": [
        {
          "title": "상태 dot + drill-down",
          "lang": "tsx",
          "code": "import { PipelineTable } from \"@axe/ui\";\nimport type { PipelineColumn, StatusVariant } from \"@axe/ui\";\n\ntype Run = { id: string; job: string; started: string; state: StatusVariant };\n\nconst columns: PipelineColumn<Run>[] = [\n  { key: \"job\", header: \"작업\", width: \"40%\" },\n  { key: \"started\", header: \"시작\", align: \"right\" },\n];\n\n<PipelineTable\n  columns={columns}\n  rows={runs}\n  getStatus={(r) => r.state}\n  getRowKey={(r) => r.id}\n  onRowClick={(r) => expandRun(r)}\n/>"
        },
        {
          "title": "loading skeleton",
          "lang": "tsx",
          "code": "import { PipelineTable } from \"@axe/ui\";\n\n<PipelineTable\n  columns={columns}\n  rows={[]}\n  getStatus={(r) => r.state}\n  loading\n  loadingRows={6}\n  emptyState=\"실행 이력이 없습니다\"\n/>"
        },
        {
          "title": "CSS-only 정적 행 (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-pipeline-table\">\n  <div class=\"axe-pipeline-table__scroll\">\n    <table class=\"axe-pipeline-table__table\">\n      <thead class=\"axe-pipeline-table__thead\">\n        <tr>\n          <th class=\"axe-pipeline-table__th axe-pipeline-table__th--status\"></th>\n          <th class=\"axe-pipeline-table__th\" scope=\"col\">작업</th>\n        </tr>\n      </thead>\n      <tbody>\n        <tr class=\"axe-pipeline-table__tr\">\n          <td class=\"axe-pipeline-table__td axe-pipeline-table__td--status\">\n            <span class=\"axe-status-light axe-status-light--running axe-status-light--size-sm\">\n              <span class=\"axe-status-light__dot\" aria-hidden=\"true\"></span>\n            </span>\n          </td>\n          <td class=\"axe-pipeline-table__td\">공시 수집</td>\n        </tr>\n      </tbody>\n    </table>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "agent 가 행별 진행을 흘릴 때 getStatus accessor 만 갈아끼우면 dot 이 그대로 따라간다.",
          "좁은 화면 가로 스크롤이 의미 있도록 주요 컬럼에 width 를 명시한다.",
          "SR 접근이 필요하면 상태를 시각 dot 외에 텍스트 컬럼으로도 노출한다."
        ],
        "dont": [
          "정렬/페이징이 필요하면 PipelineTable 대신 DataTable 을 쓸 것.",
          "onRowClick 을 쓰면서 셀에 링크/버튼을 중첩하지 말 것."
        ]
      },
      "consumeCss": "정적 행은 `.axe-pipeline-table*` + `.axe-status-light*` 클래스로 서버 렌더에서 재현 가능. 단 pulse·skeleton·drill-down 위젯화는 React(StatusLight+상태 로직)가 필요하고, dot 만으로는 상태가 SR 에 전달되지 않으니 텍스트를 병기한다.",
      "url": "https://design.axelabs.ai/components/pipeline-table"
    },
    {
      "name": "Popover",
      "slug": "popover",
      "group": "overlay",
      "summary": "트리거에 앵커되어 뜨는 인터랙티브 부동 패널 (Radix Popover 기반).",
      "description": "Tooltip 보다 무겁고 Dialog 보다 가벼운 중간 오버레이다. 폼·미니 메뉴·인스펙터처럼 상호작용이 있는 콘텐츠를 트리거 옆에 띄운다. Dialog 와 달리 포커스를 가두지 않고(modal 기본 false) 바깥 상호작용을 허용하며, Esc 로 닫는다. 기본으로 포털에 렌더되어 overflow:hidden 부모에 갇히지 않고, side/align/sideOffset 등 배치는 Radix Popover 가 popper 로 계산한다. Popover(Root)·PopoverTrigger·PopoverContent 를 조합하고, 필요 시 PopoverArrow·PopoverClose·PopoverAnchor 를 더한다.",
      "keywords": [
        "popover",
        "팝오버",
        "부동패널",
        "floating",
        "인스펙터",
        "미니메뉴",
        "overlay",
        "radix"
      ],
      "props": [
        {
          "name": "open",
          "type": "boolean",
          "description": "열림 상태 (controlled). Radix Popover.Root prop."
        },
        {
          "name": "onOpenChange",
          "type": "(open: boolean) => void",
          "description": "열림/닫힘 전환 콜백 (Root)."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "defaultValue": "false",
          "description": "uncontrolled 초기 열림 상태 (Root)."
        },
        {
          "name": "modal",
          "type": "boolean",
          "defaultValue": "false",
          "description": "true 면 바깥 상호작용 차단·포커스 트랩. 기본은 비모달 (Root)."
        },
        {
          "name": "side",
          "type": "\"top\" | \"right\" | \"bottom\" | \"left\"",
          "defaultValue": "\"bottom\"",
          "description": "트리거 기준 배치 방향 (PopoverContent, Radix Content prop)."
        },
        {
          "name": "align",
          "type": "\"start\" | \"center\" | \"end\"",
          "defaultValue": "\"center\"",
          "description": "교차축 정렬 (PopoverContent, Radix)."
        },
        {
          "name": "sideOffset",
          "type": "number",
          "defaultValue": "8",
          "description": "PopoverContent 와 트리거 사이 간격(px)."
        },
        {
          "name": "withPortal",
          "type": "boolean",
          "defaultValue": "true",
          "description": "PopoverContent 를 포털로 띄울지. false 면 DOM 위치 그대로(트리거 옆)에 렌더."
        },
        {
          "name": "...rest (PopoverContent)",
          "type": "React.ComponentPropsWithoutRef<typeof Popover.Content>",
          "description": "collisionPadding·onOpenAutoFocus·onEscapeKeyDown 등 Radix Content props 를 전파."
        },
        {
          "name": "...rest (PopoverArrow)",
          "type": "React.ComponentPropsWithoutRef<typeof Popover.Arrow>",
          "description": "화살표 표식. 기본 width 12·height 6 로 렌더(Radix Arrow props 전파)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-popover__content",
          "purpose": "부동 패널 표면(elevated 배경·shadow-lg·min-width 200).",
          "stability": "stable"
        },
        {
          "className": "axe-popover__arrow",
          "purpose": "트리거를 가리키는 삼각 화살표(fill=elevated·drop-shadow 보더).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Space/Enter 로 열기(트리거)",
          "열릴 때 포커스가 콘텐츠 첫 요소로 이동",
          "Esc 로 닫기(트리거로 포커스 복원)",
          "Tab 으로 콘텐츠 내부를 이동(비모달이라 가두지 않음)"
        ],
        "aria": "Radix 가 트리거에 aria-haspopup=dialog·aria-expanded·aria-controls 를 부여하고 콘텐츠와 id 로 연결한다. 콘텐츠 자체는 인터랙티브 컨테이너이므로, 의미가 필요하면 소비자가 aria-label/role 을 명시한다.",
        "notes": "Dialog 와 달리 포커스 트랩이 없다(modal 기본 false) — 여러 트리거/폼 필드와 나란히 열어두는 데 적합. transform-origin 을 Radix 의 --radix-popover-content-transform-origin 에 맞춰 방향별 확대 애니메이션을 주고, data-state=open|closed 로 진입/퇴장이 갈린다. focus-visible 시 accent outline, prefers-reduced-motion 시 애니메이션 제거."
      },
      "examples": [
        {
          "title": "기본 (인스펙터 패널)",
          "lang": "tsx",
          "code": "import {\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n  PopoverArrow,\n  Button,\n} from \"@axe/ui\";\n\n<Popover>\n  <PopoverTrigger asChild>\n    <Button variant=\"ghost\">필터</Button>\n  </PopoverTrigger>\n  <PopoverContent side=\"bottom\" align=\"end\">\n    <PopoverArrow />\n    {/* … 필터 폼 … */}\n  </PopoverContent>\n</Popover>"
        },
        {
          "title": "controlled + PopoverClose",
          "lang": "tsx",
          "code": "import {\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n  PopoverClose,\n  Button,\n} from \"@axe/ui\";\n\n<Popover open={open} onOpenChange={setOpen}>\n  <PopoverTrigger asChild>\n    <Button>공유</Button>\n  </PopoverTrigger>\n  <PopoverContent sideOffset={12}>\n    <p>이 문서 링크를 복사합니다.</p>\n    <PopoverClose asChild>\n      <Button variant=\"ghost\">닫기</Button>\n    </PopoverClose>\n  </PopoverContent>\n</Popover>"
        }
      ],
      "bestPractices": {
        "do": [
          "인터랙티브 콘텐츠(폼·미니 메뉴·설정)에 쓸 것.",
          "단순 텍스트 힌트는 Tooltip, 확인·집중이 필요한 흐름은 Dialog 로 분리.",
          "포털 밖 위치가 필요하면 withPortal={false}, 앵커를 분리하려면 PopoverAnchor 사용."
        ],
        "dont": [
          "다량의 텍스트나 전체 워크플로를 넣지 말 것 — 그건 Dialog/Sheet.",
          "포커스 격리가 필요한 파괴적 확인에 비모달 Popover 를 쓰지 말 것 — modal 다이얼로그로."
        ]
      },
      "consumeCss": "표면 스타일(.axe-popover__content/.axe-popover__arrow)은 노출되지만, 트리거 기준 위치 계산(side/align/sideOffset)·포털·open/close(data-state)는 전적으로 Radix popper 런타임에 의존한다. 따라서 CSS-only 로는 앵커된 팝오버를 재현할 수 없고 실제 소비는 React 컴포넌트를 통해서 한다.",
      "url": "https://design.axelabs.ai/components/popover"
    },
    {
      "name": "Progress",
      "slug": "progress",
      "group": "feedback",
      "summary": "측정 가능한 비동기 작업의 진척 바 — determinate / indeterminate.",
      "description": "value 가 숫자면 0~max 사이 채움(translateX 로 indicator 이동), null 이면 indeterminate 로 좌→우 sweep 애니메이션(진척률 모름). variant 로 결과 색(success/danger)을 표현해 4-state 의 완료/실패도 같은 바로 시각화한다. Radix Progress 래퍼라 role/aria-value* 가 자동으로 붙는다. multi-step agent(예: 18섹션 IC 메모 7/18)·파일 업로드·배치 작업에 쓴다.",
      "keywords": [
        "progress",
        "진행률",
        "진척",
        "progressbar",
        "determinate",
        "indeterminate",
        "loading",
        "bar"
      ],
      "props": [
        {
          "name": "value",
          "type": "number | null",
          "defaultValue": "null",
          "description": "0 ~ max 진척값. null 이면 indeterminate(sweep). 범위를 벗어나면 0~max 로 clamp."
        },
        {
          "name": "max",
          "type": "number",
          "defaultValue": "100",
          "description": "분모(진척의 100% 기준)."
        },
        {
          "name": "variant",
          "type": "\"default\" | \"success\" | \"danger\"",
          "defaultValue": "\"default\"",
          "description": "indicator 색. default=--accent · success=--success · danger=--danger."
        },
        {
          "name": "...rest",
          "type": "Omit<React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>, \"value\">",
          "description": "className 병합, 나머지는 Radix Progress.Root(루트 div)로 전파 — 접근 이름을 위한 aria-label/aria-labelledby 등을 여기로 넘긴다."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-progress",
          "purpose": "트랙 루트 — 높이 6px·subtle 배경·pill radius·overflow hidden.",
          "stability": "stable"
        },
        {
          "className": "axe-progress__indicator",
          "purpose": "채움 바 — accent 배경, translateX 로 진척 표현(transition).",
          "stability": "stable"
        },
        {
          "className": "axe-progress--success",
          "purpose": "success 변형 — indicator = --success.",
          "stability": "stable"
        },
        {
          "className": "axe-progress--danger",
          "purpose": "danger 변형 — indicator = --danger.",
          "stability": "stable"
        },
        {
          "className": "axe-progress--indeterminate",
          "purpose": "indeterminate 변형 — indicator 를 33% 폭 sweep 으로 전환.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "Radix 가 role=\"progressbar\" + aria-valuemin=0 · aria-valuemax=max · aria-valuenow(=value, indeterminate 면 생략) · aria-valuetext(기본 \"N%\", indeterminate 면 생략) · data-state(loading|complete|indeterminate)를 붙인다.",
        "notes": "비대화 요소라 키보드 상호작용 없음. Radix 는 접근 이름을 자동 생성하지 않으므로 aria-label 또는 연결 라벨(aria-labelledby)을 반드시 준다. prefers-reduced-motion 시 indicator transition·sweep 정지(indeterminate 는 전체 폭 0.6 opacity 로 정적 표시)."
      },
      "examples": [
        {
          "title": "determinate (7/18)",
          "lang": "tsx",
          "code": "import { Progress } from \"@axe/ui\";\n\n<Progress value={7} max={18} aria-label=\"IC 메모 생성 진척\" />"
        },
        {
          "title": "indeterminate + 결과 색",
          "lang": "tsx",
          "code": "import { Progress } from \"@axe/ui\";\n\nexport function Example() {\n  return (\n    <>\n      {/* 진척률 모름 (sweep) */}\n      <Progress value={null} aria-label=\"토큰 갱신 중\" />\n      {/* 완료/실패 결과 색 */}\n      <Progress value={100} variant=\"success\" aria-label=\"완료\" />\n      <Progress value={40} variant=\"danger\" aria-label=\"중단됨\" />\n    </>\n  );\n}"
        },
        {
          "title": "CSS-only (비-React, 60%)",
          "lang": "html",
          "code": "<div class=\"axe-progress\" role=\"progressbar\"\n     aria-valuemin=\"0\" aria-valuemax=\"100\" aria-valuenow=\"60\"\n     aria-label=\"업로드 진척\">\n  <div class=\"axe-progress__indicator\" style=\"transform: translateX(-40%)\"></div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "진척률을 알면 determinate(value 숫자)로 — 사용자가 남은 양을 가늠하게 한다.",
          "aria-label 등 접근 이름을 항상 준다(무엇의 진척인지)."
        ],
        "dont": [
          "진척률을 모르는데 determinate 로 가짜 퍼센트를 만들지 말 것 — value=null(indeterminate) 또는 Spinner.",
          "variant 를 진행 중 장식으로 쓰지 말 것 — success/danger 는 실제 완료/실패 결과일 때만."
        ]
      },
      "consumeCss": "순수 CSS 로 재현 가능 — .axe-progress + .axe-progress__indicator 에 인라인 transform: translateX(-(100-pct)%) 로 채우고, role/aria-value* 는 마크업에서 직접 부여한다. 스타일(indicators.css)은 components.css 가 @import 하므로 별도 로드 불요.",
      "playground": {
        "componentKey": "progress",
        "defaults": {
          "value": 60,
          "max": 100,
          "variant": "default"
        },
        "controls": [
          {
            "prop": "value",
            "kind": "number",
            "min": 0,
            "max": 100,
            "step": 1
          }
        ]
      },
      "url": "https://design.axelabs.ai/components/progress"
    },
    {
      "name": "Prose",
      "slug": "prose",
      "group": "content",
      "summary": "임의 마크다운/HTML 트리에 AXE 타이포·간격·컬러를 입히는 본문 래퍼.",
      "description": "단일 `.axe-prose` 클래스로 안의 모든 표준 HTML(h1–h6·p·ul/ol·blockquote·hr·table·pre/code·img)에 디자인 토큰 기반 타이포 스케일·수직 리듬·링크/표/코드 스타일을 부여한다. MDX 컴포넌트나 react-markdown 이 렌더한 트리를 한 겹 감싸는 용도이며, 자체 상호작용 상태는 없는 forwardRef `<div>` 다. 최대 폭은 `--prose-max` 토큰이 잡아 정보 밀도 높은 문서 페이지의 가독 폭을 유지한다.",
      "keywords": [
        "prose",
        "본문",
        "마크다운",
        "markdown",
        "mdx",
        "typography",
        "타이포",
        "article",
        "문서"
      ],
      "props": [
        {
          "name": "children",
          "type": "React.ReactNode",
          "description": "prose 스타일을 받을 표준 HTML/마크다운 트리."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLDivElement>",
          "description": "className(축약 없이 axe-prose 뒤에 병합) 포함 나머지 속성은 루트 div 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-prose",
          "purpose": "본문 래퍼 루트. 하위 표준 요소를 자손 셀렉터(.axe-prose h1 …)로 스타일링.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "role 없는 순수 <div>. 의미(heading·표·리스트)는 안에 넣는 자식 마크업이 담당한다.",
        "notes": "heading 위계(h1→h2→h3, 건너뜀 없이)·표의 <th scope>·이미지 alt·링크 텍스트 등 접근성은 감싸는 소비자 마크업의 책임. Prose 는 시각 스타일만 입히고 구조를 바꾸지 않는다. 링크는 밑줄(text-decoration)로 어포던스를 유지하며 색상만 hover 전환한다(모션 아님)."
      },
      "examples": [
        {
          "title": "기본",
          "lang": "tsx",
          "code": "import { Prose } from \"@axe/ui\";\n\n<Prose>\n  <h1>배포 SSOT</h1>\n  <p>\n    origin/main 의 commit SHA 가 배포의 <a href=\"/ops/runbook\">단일 진실</a>이다.\n  </p>\n  <table>\n    <thead>\n      <tr><th>서비스</th><th>포트</th></tr>\n    </thead>\n    <tbody>\n      <tr><td>frame proxy</td><td>3712</td></tr>\n    </tbody>\n  </table>\n</Prose>"
        },
        {
          "title": "react-markdown 트리 감싸기",
          "lang": "tsx",
          "code": "import { Prose } from \"@axe/ui\";\nimport ReactMarkdown from \"react-markdown\";\n\n<Prose>\n  <ReactMarkdown>{markdownSource}</ReactMarkdown>\n</Prose>"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-prose\">\n  <h1>배포 SSOT</h1>\n  <p>origin/main 의 commit SHA 가 배포의 단일 진실이다.</p>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "MDX·react-markdown 출력처럼 렌더 완료된 표준 HTML 트리를 감싸기.",
          "가독 폭 조정은 --prose-max 토큰/래퍼로 (임의 max-width 하드코딩 지양)."
        ],
        "dont": [
          "heading 위계를 건너뛰지 말 것(h1→h3 점프) — 스타일은 되지만 a11y 위반.",
          "버튼·폼 같은 인터랙티브 UI 를 Prose 안에 섞지 말 것 — 타이포 전용 컨텍스트."
        ]
      },
      "consumeCss": "완전 CSS-only 소비 가능. React 없이 `.axe-prose` div 한 겹으로 감싸면 동일한 타이포·간격·표·코드 스타일을 받는다(내부 상태 없음).",
      "url": "https://design.axelabs.ai/components/prose"
    },
    {
      "name": "RadioGroup",
      "slug": "radio",
      "group": "input",
      "summary": "Radix 기반 단일 선택 라디오 그룹 (roving tabindex).",
      "description": "@radix-ui/react-radio-group 의 Root/Item 을 감싼 래퍼로, dot+ring CSS 만 담당하고 방향키 이동·roving tabindex·폼 연동은 Radix 가 처리한다. RadioGroup 이 선택 상태(value)를 들고, 각 RadioGroupItem 이 하나의 선택지(value 필수)다. 그룹은 기본 세로 배치(axe-radio__group, flex column)이며 항목마다 <Label htmlFor> 를 붙여 클릭 영역을 넓힌다.",
      "keywords": [
        "radio",
        "radio-group",
        "라디오",
        "단일선택",
        "form",
        "폼"
      ],
      "props": [
        {
          "name": "value",
          "type": "string",
          "description": "(RadioGroup) controlled 선택값."
        },
        {
          "name": "defaultValue",
          "type": "string",
          "description": "(RadioGroup) uncontrolled 초기 선택값."
        },
        {
          "name": "onValueChange",
          "type": "(value: string) => void",
          "description": "(RadioGroup) 선택 변경 콜백."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "description": "(RadioGroup) 그룹 전체 비활성."
        },
        {
          "name": "required",
          "type": "boolean",
          "description": "(RadioGroup) 폼 제출 시 선택 필수."
        },
        {
          "name": "name",
          "type": "string",
          "description": "(RadioGroup) 폼 필드명 — 지정 시 제출용 hidden input 생성."
        },
        {
          "name": "orientation",
          "type": "\"horizontal\" | \"vertical\" | undefined",
          "description": "(RadioGroup) 방향키 매핑 힌트 (기본 미지정)."
        },
        {
          "name": "...rest",
          "type": "React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>",
          "description": "(RadioGroup) loop·dir·asChild 등 나머지 Radix Root props 전파 (className 은 axe-radio__group 과 병합)."
        },
        {
          "name": "value (RadioGroupItem)",
          "type": "string",
          "required": true,
          "description": "(RadioGroupItem) 이 선택지의 값 (필수)."
        },
        {
          "name": "disabled (RadioGroupItem)",
          "type": "boolean",
          "description": "(RadioGroupItem) 이 항목만 비활성."
        },
        {
          "name": "...rest (RadioGroupItem)",
          "type": "React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>",
          "description": "(RadioGroupItem) id 등 나머지 Radix Item props 전파 (Label htmlFor 매칭용 id, className 은 axe-radio__item 과 병합)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-radio__group",
          "purpose": "그룹 컨테이너 (flex column, gap).",
          "stability": "stable"
        },
        {
          "className": "axe-radio__item",
          "purpose": "라디오 버튼 (18px 원형). data-state=checked 시 accent 테두리.",
          "stability": "stable"
        },
        {
          "className": "axe-radio__indicator",
          "purpose": "선택 dot 컨테이너 — ::after 로 8px accent 점을 그림 (Radix Indicator, checked 일 때만 마운트).",
          "stability": "internal"
        }
      ],
      "a11y": {
        "keyboard": [
          "↑/↓ (또는 ←/→) 로 항목 이동하며 즉시 선택",
          "Tab 으로 그룹 진입/탈출 — 선택된 항목만 tab-stop (roving tabindex)"
        ],
        "aria": "Radix 가 그룹 role=\"radiogroup\", 항목 role=\"radio\" + aria-checked 를 부여. disabled → aria-disabled + data-disabled.",
        "notes": "각 항목에 <Label htmlFor={item id}> 를 연결해 클릭 영역을 넓힘. name 지정 시 Radix 가 폼 제출용 hidden input 렌더. 포커스 링 = :focus-visible box-shadow var(--ring)."
      },
      "examples": [
        {
          "title": "기본",
          "lang": "tsx",
          "code": "import { RadioGroup, RadioGroupItem, Label } from \"@axe/ui\";\n\nconst [plan, setPlan] = useState(\"pro\");\n\n<RadioGroup value={plan} onValueChange={setPlan}>\n  <div style={{ display: \"flex\", gap: 8, alignItems: \"center\" }}>\n    <RadioGroupItem id=\"p-free\" value=\"free\" />\n    <Label htmlFor=\"p-free\">Free</Label>\n  </div>\n  <div style={{ display: \"flex\", gap: 8, alignItems: \"center\" }}>\n    <RadioGroupItem id=\"p-pro\" value=\"pro\" />\n    <Label htmlFor=\"p-pro\">Pro</Label>\n  </div>\n</RadioGroup>"
        }
      ],
      "bestPractices": {
        "do": [
          "항목마다 <Label htmlFor> 로 라벨 연결.",
          "value 는 각 항목에 고유하게."
        ],
        "dont": [
          "RadioGroupItem 을 RadioGroup 밖에서 쓰지 말 것.",
          "단일 on/off 는 Radio 말고 Checkbox/Switch 사용."
        ]
      },
      "consumeCss": "CSS-only 로는 선택 상태(data-state=checked)가 Radix 런타임에 의존해 완전 재현이 어려움 — 정적 시연은 선택 항목에 data-state=\"checked\" 와 indicator 요소를 직접 넣어야 함. React 사용 권장.",
      "playground": {
        "componentKey": "radio",
        "defaults": {
          "disabled": false
        }
      },
      "url": "https://design.axelabs.ai/components/radio"
    },
    {
      "name": "ResponsiveTabs",
      "slug": "responsive-tabs",
      "group": "navigation",
      "summary": "뷰포트에 따라 데스크톱 pill 탭 ↔ 모바일 하단 고정 bottom-nav 로 전환.",
      "description": "같은 tabs/value API 로 두 모드를 그린다. breakpoint 이상(데스크톱) 은 Radix Tabs primitive 위 pill 스타일, 미만(모바일) 은 화면 하단 fixed bottom-nav(아이콘 + 라벨 + 배지). SSR-safe — 첫 paint 는 데스크톱을 가정하고 matchMedia 구독 후 분기한다. 모바일 bottom-nav 는 safe-area-inset-bottom 을 반영해 iOS 노치/제스처 바를 침범하지 않는다. children 은 보통 <TabsContent value=\"...\"/> 들이며, 데스크톱은 Radix 가 활성만 노출하고 모바일은 활성 value 와 매칭되는 자식만 렌더한다.",
      "keywords": [
        "responsive tabs",
        "반응형 탭",
        "bottom nav",
        "하단 네비",
        "mobile nav",
        "pill",
        "탭바",
        "badge"
      ],
      "props": [
        {
          "name": "tabs",
          "type": "ResponsiveTabItem[]",
          "required": true,
          "description": "탭 정의 배열. ResponsiveTabItem = { value, label, icon?, badge?: string | number, disabled? }."
        },
        {
          "name": "value",
          "type": "string",
          "description": "활성 탭 value (controlled)."
        },
        {
          "name": "defaultValue",
          "type": "string",
          "description": "uncontrolled 초기 value. 미지정 시 tabs[0].value."
        },
        {
          "name": "onValueChange",
          "type": "(value: string) => void",
          "description": "탭 전환 시 호출."
        },
        {
          "name": "breakpoint",
          "type": "number",
          "defaultValue": "768",
          "description": "데스크톱 → 모바일 분기점(px). 미만이면 bottom-nav."
        },
        {
          "name": "bottomNavAlwaysVisible",
          "type": "boolean",
          "defaultValue": "true",
          "description": "모바일 bottom-nav 를 화면 하단 fixed 로. false 면 inline 배치."
        },
        {
          "name": "children",
          "type": "React.ReactNode",
          "description": "보통 <TabsContent value=\"...\"/> 자식들. 모바일은 활성 value 와 매칭되는 것만 렌더."
        },
        {
          "name": "className",
          "type": "string",
          "description": "루트에 추가 className."
        },
        {
          "name": "ariaLabel",
          "type": "string",
          "defaultValue": "\"주요 탐색\"",
          "description": "두 모드의 tablist(<nav>/Radix List)에 적용되는 aria-label."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-responsive-tabs",
          "purpose": "루트(데스크톱=Radix Root, 모바일=<div>).",
          "stability": "stable"
        },
        {
          "className": "axe-responsive-tabs--mobile",
          "purpose": "모바일 모드 루트 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-responsive-tabs__pills",
          "purpose": "데스크톱 pill 묶음(Radix List, role=tablist).",
          "stability": "stable"
        },
        {
          "className": "axe-responsive-tabs__pill",
          "purpose": "데스크톱 pill 트리거(Radix Trigger). 활성 = [data-state=\"active\"].",
          "stability": "stable"
        },
        {
          "className": "axe-responsive-tabs__pill-icon",
          "purpose": "pill 안 아이콘 span(aria-hidden).",
          "stability": "stable"
        },
        {
          "className": "axe-responsive-tabs__content",
          "purpose": "패널 컨테이너(데스크톱/모바일 공용).",
          "stability": "stable"
        },
        {
          "className": "axe-responsive-tabs__bottom-nav",
          "purpose": "모바일 하단 네비 <nav>(role=tablist).",
          "stability": "stable"
        },
        {
          "className": "axe-responsive-tabs__bottom-nav--fixed",
          "purpose": "하단 고정 변형(bottomNavAlwaysVisible=true).",
          "stability": "stable"
        },
        {
          "className": "axe-responsive-tabs__bottom-btn",
          "purpose": "모바일 탭 <button>(role=tab).",
          "stability": "stable"
        },
        {
          "className": "axe-responsive-tabs__bottom-btn--active",
          "purpose": "활성 모바일 탭.",
          "stability": "stable"
        },
        {
          "className": "axe-responsive-tabs__bottom-icon",
          "purpose": "모바일 탭 아이콘 span(aria-hidden).",
          "stability": "stable"
        },
        {
          "className": "axe-responsive-tabs__bottom-label",
          "purpose": "모바일 탭 라벨 span.",
          "stability": "stable"
        },
        {
          "className": "axe-responsive-tabs__badge",
          "purpose": "탭 우상단 배지(알림 수 등). pill/bottom-btn 양쪽 공용.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "데스크톱: ← → 로 pill 이동 + Home/End (Radix roving)",
          "모바일: ← → ↑ ↓ 로 탭 순환 이동, Home/End (자체 onKeyDown). disabled 탭은 건너뜀"
        ],
        "aria": "데스크톱은 Radix Tabs(List/Trigger)가 role=tablist/tab·aria-selected·aria-controls·roving tabindex 를 부여. 모바일은 자체 ARIA tabs 패턴 — <nav role=\"tablist\">, 각 <button role=\"tab\" id=\"axe-rt-tab-{value}\" aria-selected aria-controls=\"axe-rt-panel-{value}\"> 에 roving tabindex(활성 0 / 나머지 -1), 패널은 <div role=\"tabpanel\" id=\"axe-rt-panel-{value}\" aria-labelledby=\"axe-rt-tab-{value}\" tabIndex=0>. 아이콘 span 은 aria-hidden.",
        "notes": "SSR 은 viewport 를 모르므로 첫 paint 는 데스크톱 가정 → hydration 후 matchMedia 로 모바일 전환(초기 mismatch 최소화). 하단 fixed 네비는 .axe-safe-area-bottom(공용 유틸)로 safe-area-inset-bottom 을 패딩에 반영해 iOS 홈 인디케이터를 침범하지 않는다."
      },
      "examples": [
        {
          "title": "기본 (icon + badge)",
          "lang": "tsx",
          "code": "import { ResponsiveTabs, TabsContent } from \"@axe/ui\";\n\n<ResponsiveTabs\n  ariaLabel=\"대시보드\"\n  defaultValue=\"overview\"\n  tabs={[\n    { value: \"overview\", label: \"개요\", icon: <HomeIcon /> },\n    { value: \"alerts\", label: \"알림\", icon: <BellIcon />, badge: 3 },\n    { value: \"settings\", label: \"설정\", icon: <GearIcon />, disabled: true },\n  ]}\n>\n  <TabsContent value=\"overview\">…</TabsContent>\n  <TabsContent value=\"alerts\">…</TabsContent>\n  <TabsContent value=\"settings\">…</TabsContent>\n</ResponsiveTabs>"
        },
        {
          "title": "controlled + 낮은 breakpoint",
          "lang": "tsx",
          "code": "import { ResponsiveTabs, TabsContent } from \"@axe/ui\";\n\n<ResponsiveTabs\n  value={tab}\n  onValueChange={setTab}\n  breakpoint={640}\n  tabs={[\n    { value: \"list\", label: \"목록\" },\n    { value: \"map\", label: \"지도\" },\n  ]}\n>\n  <TabsContent value=\"list\">…</TabsContent>\n  <TabsContent value=\"map\">…</TabsContent>\n</ResponsiveTabs>"
        },
        {
          "title": "모바일 bottom-nav (CSS-only 시각)",
          "lang": "html",
          "code": "<div class=\"axe-responsive-tabs axe-responsive-tabs--mobile\">\n  <div id=\"axe-rt-panel-overview\" role=\"tabpanel\"\n       aria-labelledby=\"axe-rt-tab-overview\" tabindex=\"0\"\n       class=\"axe-responsive-tabs__content\">…</div>\n  <nav class=\"axe-responsive-tabs__bottom-nav axe-responsive-tabs__bottom-nav--fixed axe-safe-area-bottom\"\n       role=\"tablist\" aria-label=\"대시보드\">\n    <button type=\"button\" id=\"axe-rt-tab-overview\" role=\"tab\"\n            aria-selected=\"true\" aria-controls=\"axe-rt-panel-overview\" tabindex=\"0\"\n            class=\"axe-responsive-tabs__bottom-btn axe-responsive-tabs__bottom-btn--active\">\n      <span class=\"axe-responsive-tabs__bottom-label\">개요</span>\n    </button>\n    <button type=\"button\" id=\"axe-rt-tab-alerts\" role=\"tab\"\n            aria-selected=\"false\" aria-controls=\"axe-rt-panel-alerts\" tabindex=\"-1\"\n            class=\"axe-responsive-tabs__bottom-btn\">\n      <span class=\"axe-responsive-tabs__bottom-label\">알림</span>\n      <span class=\"axe-responsive-tabs__badge\">3</span>\n    </button>\n  </nav>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "children 을 <TabsContent value=\"...\"/> 로 주고 value 를 tabs 항목과 일치시킨다(모바일 매칭 기준).",
          "탭 개수는 하단 네비에 맞게 3~5개로 제한하고 라벨은 짧게.",
          "PWA/모바일 표면에서는 bottomNavAlwaysVisible 를 유지해 safe-area 를 반영시킨다."
        ],
        "dont": [
          "aria-selected/role 을 직접 붙이지 말 것 — 두 모드 모두 자동 부여.",
          "패널 콘텐츠를 <TabsContent> 밖에 두지 말 것 — 모바일 value 매칭에서 누락된다."
        ]
      },
      "consumeCss": "데스크톱 pill 은 Radix [data-state] 에 의존해 무JS 전환 불가(정적 markup 만). 모바일 bottom-nav 는 순수 <button> + 클래스라 CSS-only 로 재현 가능하다. --desktop 변형과 __pill-label 은 markup 에만 존재하는 훅으로 전용 CSS 규칙이 없다(스타일은 각각 루트/부모에서 상속).",
      "url": "https://design.axelabs.ai/components/responsive-tabs"
    },
    {
      "name": "Section",
      "slug": "section",
      "group": "container",
      "summary": "페이지 본문 한 블록 — eyebrow·heading·subtitle 헤더 + children 본문.",
      "description": "본문을 의미 단위로 끊는 표준 섹션. heading 은 headingLevel(h1/h2/h3, 기본 h2)로 시맨틱 레벨만 바꾸고 시각 크기는 항상 section 스타일로 고정한다. id 를 주면 anchor link target 이 되고, scroll-margin-top 5rem 이 자동 적용되어 sticky TopNav 에 가려지지 않는다. heading id 는 useId 로 만들어 section 의 aria-labelledby 로 연결된다.",
      "keywords": [
        "section",
        "섹션",
        "block",
        "landmark",
        "anchor",
        "heading",
        "본문",
        "블록"
      ],
      "props": [
        {
          "name": "title",
          "type": "React.ReactNode",
          "required": true,
          "description": "heading 내용(`.axe-section__title`). HTMLAttributes 의 title 은 이 prop 으로 대체됨."
        },
        {
          "name": "id",
          "type": "string",
          "description": "anchor link target. 지정 시 scroll-margin-top 이 자동 적용된다."
        },
        {
          "name": "eyebrow",
          "type": "React.ReactNode",
          "description": "title 위 작은 라벨(Badge 등). 앞에 neutral marker ::before 가 붙는다."
        },
        {
          "name": "subtitle",
          "type": "React.ReactNode",
          "description": "부제 문단(`.axe-section__subtitle`, max 56ch)."
        },
        {
          "name": "headingLevel",
          "type": "\"h1\" | \"h2\" | \"h3\"",
          "defaultValue": "\"h2\"",
          "description": "heading 의 시맨틱 레벨만 결정 — 시각 크기는 항상 section 스타일. 문서 개요에 맞춰 고른다."
        },
        {
          "name": "align",
          "type": "\"left\" | \"center\"",
          "defaultValue": "\"left\"",
          "description": "정렬. center 는 헤더·subtitle 을 중앙 정렬."
        },
        {
          "name": "children",
          "type": "React.ReactNode",
          "description": "본문 콘텐츠 — `.axe-section__body` 안에 렌더."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLElement>, \"title\">",
          "description": "className 병합, 나머지는 루트 <section> 으로 전파. aria-labelledby 를 명시하면 자동 생성 heading id 대신 그 값을 쓴다."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-section",
          "purpose": "루트 <section> — flex column, scroll-margin-top 5rem.",
          "stability": "stable"
        },
        {
          "className": "axe-section--center",
          "purpose": "중앙 정렬 변형(text-align + align-items center).",
          "stability": "stable"
        },
        {
          "className": "axe-section__header",
          "purpose": "eyebrow+title+subtitle 묶음.",
          "stability": "stable"
        },
        {
          "className": "axe-section__eyebrow",
          "purpose": "상단 라벨 — uppercase·tracking-widest, neutral marker ::before.",
          "stability": "stable"
        },
        {
          "className": "axe-section__title",
          "purpose": "heading — display 폰트·2xl(레벨과 무관하게 고정 시각 크기).",
          "stability": "stable"
        },
        {
          "className": "axe-section__subtitle",
          "purpose": "부제 문단(secondary·max 56ch).",
          "stability": "stable"
        },
        {
          "className": "axe-section__body",
          "purpose": "children 본문 컨테이너.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "루트 <section> 이 aria-labelledby 로 heading id 와 연결되어 이름 있는 landmark 가 된다(id 는 useId 자동, aria-labelledby prop override 가능).",
        "notes": "headingLevel 은 시각 크기가 아니라 문서 heading outline 을 맞추기 위한 것 — 페이지 구조(h1 아래 h2, 그 아래 h3)에 맞게 고른다. scroll-margin-top 5rem 이 sticky TopNav 로 인한 anchor jump 가림을 보정한다."
      },
      "examples": [
        {
          "title": "anchor 섹션",
          "lang": "tsx",
          "code": "import { Section } from \"@axe/ui\";\n\n<Section\n  id=\"features\"\n  eyebrow=\"Platform\"\n  title=\"하나의 통제 평면\"\n  subtitle=\"7개 MCP 서비스가 같은 SSOT 를 공유합니다.\"\n>\n  <p>frame·hive·gate·index·matrix·cortex·blueprint 이 한 축으로 묶입니다.</p>\n</Section>"
        },
        {
          "title": "중앙 정렬 + h3 레벨",
          "lang": "tsx",
          "code": "import { Section } from \"@axe/ui\";\n\n<Section align=\"center\" headingLevel=\"h3\" title=\"자주 묻는 질문\">\n  <FaqList />\n</Section>"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<section id=\"features\" class=\"axe-section\" aria-labelledby=\"sec-t\">\n  <header class=\"axe-section__header\">\n    <div class=\"axe-section__eyebrow\">Platform</div>\n    <h2 id=\"sec-t\" class=\"axe-section__title\">하나의 통제 평면</h2>\n    <p class=\"axe-section__subtitle\">7개 MCP 서비스가 같은 SSOT 를 공유합니다.</p>\n  </header>\n  <div class=\"axe-section__body\">…</div>\n</section>"
        }
      ],
      "bestPractices": {
        "do": [
          "headingLevel 은 페이지 heading 트리에 맞춰 선택(시각 크기 목적으로 쓰지 말 것).",
          "anchor 로 링크할 섹션에는 안정적인 id 를 부여한다."
        ],
        "dont": [
          "큰 글씨를 원한다고 headingLevel 을 h1 로 올리지 말 것(시각 크기는 어차피 고정).",
          "subtitle 에 문단 여러 개를 몰아넣지 말 것 — 본문은 children 으로."
        ]
      },
      "consumeCss": "상태 없는 순수 CSS — 서버 렌더(maud/jinja)에서 `.axe-section` 골격을 그대로 재현. heading 레벨 태그와 aria-labelledby↔id 연결은 마크업에서 직접 맞춘다.",
      "url": "https://design.axelabs.ai/components/section"
    },
    {
      "name": "Seg",
      "slug": "seg",
      "group": "utility",
      "summary": "항상 하나가 선택되는 단일 필수 선택용 segmented control.",
      "description": "문서 종류·뷰 모드처럼 **항상 하나**가 선택돼 있는 단일 필수 선택(라디오 의미)을 위한 segmented control. VariantSwitcher·DocSeg 가 inline style 로 재발명하던 패턴을 흡수한 SSOT 다. 외부 dep 0 — 네이티브 <button aria-pressed> 묶음이며 스타일은 100% CSS 클래스(.axe-seg)로 처리한다. \"0개 또는 N개\"가 가능한 다중/해제형은 ToggleGroup 을, 낱개 on/off 는 Toggle 을 쓴다. controlled 전용이고 value 는 제네릭 T(string 서브타입)로 좁혀진다.",
      "keywords": [
        "seg",
        "segmented control",
        "세그먼트",
        "단일 선택",
        "variant switcher",
        "탭 대체",
        "필수 선택"
      ],
      "props": [
        {
          "name": "value",
          "type": "T",
          "required": true,
          "description": "선택된 값 (controlled) — 항상 하나."
        },
        {
          "name": "options",
          "type": "SegOption<T>[]",
          "required": true,
          "description": "선택지 목록. 각 항목 = { value: T; label: React.ReactNode }."
        },
        {
          "name": "onChange",
          "type": "(value: T) => void",
          "required": true,
          "description": "선택 변경 콜백 — 고른 값을 넘김."
        },
        {
          "name": "ariaLabel",
          "type": "string",
          "required": true,
          "description": "role=\"group\" 컨테이너의 접근성 라벨."
        },
        {
          "name": "size",
          "type": "\"sm\" | \"md\"",
          "defaultValue": "\"md\"",
          "description": "버튼 크기."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'>",
          "description": "className 등 나머지는 루트 <div role=\"group\"> 으로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-seg",
          "purpose": "segmented control 루트 컨테이너 (role=\"group\", pill 트랙).",
          "stability": "stable"
        },
        {
          "className": "axe-seg__btn",
          "purpose": "각 세그먼트 버튼.",
          "stability": "stable"
        },
        {
          "className": "axe-seg__btn--active",
          "purpose": "활성 세그먼트의 대체 표식 — aria-pressed=\"true\" 와 동일 스타일(둘 다 CSS 셀렉터로 매칭).",
          "stability": "stable"
        },
        {
          "className": "axe-seg--sm",
          "purpose": "sm 크기 변형 (버튼 높이 24px).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Tab 으로 세그먼트 순회 (각 버튼이 tab stop)",
          "Enter/Space 로 해당 세그먼트 선택"
        ],
        "aria": "컨테이너 role=\"group\" + aria-label(ariaLabel). 선택된 버튼만 aria-pressed=\"true\" — 항상 정확히 하나가 눌린 상태.",
        "notes": "의미상 라디오(단일 필수 선택)지만 role=\"radio\"/radiogroup 이 아니라 group + aria-pressed toggle 버튼으로 구현했다(시각·조작 단순성 우선). 스크린리더는 라디오 \"n중 m\" 이 아니라 toggle 버튼의 pressed 로 읽는다 — 진짜 폼 라디오 시맨틱이 필요하면 Radio/RadioGroup 을 쓸 것."
      },
      "examples": [
        {
          "title": "기본 (문서 종류 전환)",
          "lang": "tsx",
          "code": "import { Seg } from \"@axe/ui\";\nimport { useState } from \"react\";\n\nfunction DocTypePicker() {\n  const [docType, setDocType] = useState(\"ic\");\n  return (\n    <Seg\n      ariaLabel=\"문서 종류\"\n      value={docType}\n      onChange={setDocType}\n      options={[\n        { value: \"ic\", label: \"IC Memo\" },\n        { value: \"lp\", label: \"LP Letter\" },\n      ]}\n    />\n  );\n}"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-seg\" role=\"group\" aria-label=\"문서 종류\">\n  <button type=\"button\" class=\"axe-seg__btn\" aria-pressed=\"true\">IC Memo</button>\n  <button type=\"button\" class=\"axe-seg__btn\" aria-pressed=\"false\">LP Letter</button>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "선택지가 2~5개로 한눈에 들어오는 단일 필수 선택에 쓸 것.",
          "ariaLabel 로 무엇을 고르는 그룹인지 명시할 것."
        ],
        "dont": [
          "선택이 해제될 수 있거나 다중이면 ToggleGroup 을 쓸 것.",
          "선택지가 많거나 라우팅을 동반하는 콘텐츠 전환은 Tabs 를 고려할 것."
        ]
      },
      "consumeCss": "활성 표시는 aria-pressed=\"true\" 또는 .axe-seg__btn--active 클래스 둘 다 가능(CSS 가 양쪽 셀렉터 지원). aria 속성을 넣기 어려운 서버렌더 환경이면 --active 클래스로 대체하면 동일 모양.",
      "playground": {
        "componentKey": "seg",
        "defaults": {
          "size": "md"
        }
      },
      "url": "https://design.axelabs.ai/components/seg"
    },
    {
      "name": "Select",
      "slug": "select",
      "group": "input",
      "summary": "옵션 목록에서 하나를 고르는 커스텀 셀렉트 (Radix Select 기반).",
      "description": "네이티브 <select> 와 달리 옵션 row 의 typography·spacing·아이콘을 디자인 토큰으로 통제한다. 키보드 내비게이션·type-ahead·포털 위치 계산은 Radix Select 가 처리하고, @axe/ui 는 트리거·팝업·focus ring 스타일만 입힌다. Select(Root)·SelectTrigger·SelectValue·SelectContent·SelectItem·SelectGroup·SelectLabel·SelectSeparator 를 조합해 쓴다.",
      "keywords": [
        "select",
        "셀렉트",
        "드롭다운",
        "dropdown",
        "picker",
        "옵션",
        "radix"
      ],
      "props": [
        {
          "name": "value",
          "type": "string",
          "description": "선택된 값 (controlled). Radix Select.Root prop."
        },
        {
          "name": "onValueChange",
          "type": "(value: string) => void",
          "description": "값이 바뀔 때 콜백 (Root)."
        },
        {
          "name": "defaultValue",
          "type": "string",
          "description": "uncontrolled 초기 선택값 (Root)."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "defaultValue": "false",
          "description": "전체 비활성 (Root)."
        },
        {
          "name": "size",
          "type": "\"md\" | \"lg\"",
          "defaultValue": "\"md\"",
          "description": "SelectTrigger 높이 토큰. md=36px / lg=44px (Input 과 동일)."
        },
        {
          "name": "position",
          "type": "\"item-aligned\" | \"popper\"",
          "defaultValue": "\"popper\"",
          "description": "SelectContent 배치 전략. popper=트리거 아래 앵커."
        },
        {
          "name": "sideOffset",
          "type": "number",
          "defaultValue": "6",
          "description": "SelectContent 와 트리거 사이 간격(px)."
        },
        {
          "name": "placeholder",
          "type": "string",
          "description": "SelectValue 의 미선택 표시 텍스트 (선택 전 muted 색)."
        },
        {
          "name": "value (SelectItem)",
          "type": "string",
          "required": true,
          "description": "각 SelectItem 의 옵션 값 — 선택 시 Root onValueChange 로 전달."
        },
        {
          "name": "...rest",
          "type": "각 Radix Select.* props",
          "description": "트리거/콘텐츠/아이템 등 각 파트는 대응 Radix 컴포넌트 props(asChild 포함)를 그대로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-select__trigger",
          "purpose": "트리거 버튼 루트(닫힌 상태 표시).",
          "stability": "stable"
        },
        {
          "className": "axe-select__trigger--lg",
          "purpose": "lg 사이즈 변형(44px).",
          "stability": "stable"
        },
        {
          "className": "axe-select__content",
          "purpose": "포털로 뜨는 팝업 패널.",
          "stability": "stable"
        },
        {
          "className": "axe-select__viewport",
          "purpose": "팝업 내부 스크롤 뷰포트(패딩 래퍼) — Radix Viewport.",
          "stability": "internal"
        },
        {
          "className": "axe-select__label",
          "purpose": "SelectLabel — 그룹 제목(대문자 트래킹).",
          "stability": "stable"
        },
        {
          "className": "axe-select__item",
          "purpose": "옵션 row.",
          "stability": "stable"
        },
        {
          "className": "axe-select__indicator",
          "purpose": "선택된 항목의 체크 표시 슬롯 — Radix ItemIndicator.",
          "stability": "internal"
        },
        {
          "className": "axe-select__separator",
          "purpose": "그룹 구분선.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Space/Enter/↓ 로 열기",
          "↑/↓ 로 항목 이동, Home/End 로 처음/끝",
          "글자 입력 시 type-ahead 로 해당 항목 점프",
          "Enter 로 선택, Esc 로 닫기(트리거로 포커스 복원)"
        ],
        "aria": "Radix 가 트리거에 aria-haspopup=listbox·aria-expanded 를, 콘텐츠에 role=listbox, 각 아이템에 role=option(선택은 data-state=checked)을 부여. 현재 값은 SelectValue 가 표시.",
        "notes": "focus-visible·열림 시 --ring 적용, 미선택 트리거는 data-placeholder 로 muted 색. 부모 .axe-form-field--error 안에서 자동으로 danger 보더/링. prefers-reduced-motion 시 open/close 애니메이션 제거."
      },
      "examples": [
        {
          "title": "기본",
          "lang": "tsx",
          "code": "import {\n  Select,\n  SelectTrigger,\n  SelectValue,\n  SelectContent,\n  SelectItem,\n} from \"@axe/ui\";\n\n<Select value={v} onValueChange={setV}>\n  <SelectTrigger>\n    <SelectValue placeholder=\"국가 선택…\" />\n  </SelectTrigger>\n  <SelectContent>\n    <SelectItem value=\"kr\">한국</SelectItem>\n    <SelectItem value=\"us\">미국</SelectItem>\n    <SelectItem value=\"jp\">일본</SelectItem>\n  </SelectContent>\n</Select>"
        },
        {
          "title": "그룹 · 라벨 · 구분선 · lg 사이즈",
          "lang": "tsx",
          "code": "import {\n  Select,\n  SelectTrigger,\n  SelectValue,\n  SelectContent,\n  SelectGroup,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n} from \"@axe/ui\";\n\n<Select value={v} onValueChange={setV}>\n  <SelectTrigger size=\"lg\">\n    <SelectValue placeholder=\"담당자…\" />\n  </SelectTrigger>\n  <SelectContent>\n    <SelectGroup>\n      <SelectLabel>운영</SelectLabel>\n      <SelectItem value=\"a\">강수훈</SelectItem>\n      <SelectItem value=\"b\">한진우</SelectItem>\n    </SelectGroup>\n    <SelectSeparator />\n    <SelectGroup>\n      <SelectLabel>외부</SelectLabel>\n      <SelectItem value=\"c\">외부 감사인</SelectItem>\n    </SelectGroup>\n  </SelectContent>\n</Select>"
        }
      ],
      "bestPractices": {
        "do": [
          "SelectValue 에 placeholder 를 주어 미선택 상태를 명확히.",
          "라벨은 <FormField label=… htmlFor=…> 로 연결하거나 트리거에 aria-label 부여.",
          "옵션이 많고 자유 검색이 필요하면 Combobox 를 대신 검토."
        ],
        "dont": [
          "네이티브 <option> 마크업을 섞지 말 것 — 파트는 SelectItem 등으로만.",
          "SelectContent 를 overflow:hidden 부모 안에서 position 을 임의 변경하지 말 것(포털이 기본)."
        ]
      },
      "consumeCss": "포털 위치 계산·open/close·항목 하이라이트가 Radix 런타임(data-state)에 의존해 CSS-only 로는 동작하는 위젯을 재현할 수 없다. .axe-select__* 토큰은 노출되나 실제 소비는 React 컴포넌트를 통해서 한다.",
      "url": "https://design.axelabs.ai/components/select"
    },
    {
      "name": "ServerCodeBlock",
      "slug": "server-code-block",
      "group": "content",
      "summary": "shiki 문법 하이라이팅 서버 코드 블록(RSC).",
      "description": "async React Server Component 로, shiki 로 코드를 토큰화해 <pre><code> 안을 .line/토큰 <span> 구조로 채운다. 클라이언트 CodeBlock 과 동일한 `axe-codeblock*` 마크업을 공유하되 Copy 버튼이 없다(clipboard 는 클라이언트 JS 필요). 색은 var(--shiki-*) 로 나오고 실제 값은 code-theme.css 가 디자인 토큰에 매핑해 light/dark 자동 대응. 하이라이터는 모듈 레벨 싱글턴 promise 로 캐시된다. shiki 는 server-only 이므로 \"use client\" 파일에서 import 금지.",
      "keywords": [
        "server code block",
        "shiki",
        "syntax highlighting",
        "문법강조",
        "RSC",
        "server component",
        "코드블록"
      ],
      "props": [
        {
          "name": "code",
          "type": "string",
          "required": true,
          "description": "하이라이팅할 코드 문자열."
        },
        {
          "name": "language",
          "type": "string",
          "description": "grammar 선택 + 헤더 라벨. alias 정규화(ts/typescript, js/javascript, py/python, sh/zsh/shell→bash 등). 미로드 언어는 plain text 로 fallback. 지원: ts, tsx, js, json, bash, html, css, python, rust."
        },
        {
          "name": "title",
          "type": "string",
          "description": "헤더 라벨(language 보다 우선)."
        },
        {
          "name": "className",
          "type": "string",
          "description": "루트 <div> 에 병합. (rest props 전파 없음 — 명시된 4개 prop 만.)"
        }
      ],
      "cssClasses": [
        {
          "className": "axe-codeblock",
          "purpose": "블록 루트 <div>(CodeBlock 과 markup parity).",
          "stability": "stable"
        },
        {
          "className": "axe-codeblock__header",
          "purpose": "라벨 헤더 행(title/language 있을 때만).",
          "stability": "stable"
        },
        {
          "className": "axe-codeblock__lang",
          "purpose": "title/language 라벨.",
          "stability": "stable"
        },
        {
          "className": "axe-codeblock__pre",
          "purpose": "코드 본문 <pre>.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "상호작용 요소 없음(정적 표시) — role 불요. Copy 버튼이 없어 키보드 포커스 대상도 없다. 헤더는 title 또는 language 가 있을 때만 렌더돼 빈 헤더가 생기지 않는다.",
        "notes": "async 서버 컴포넌트라 클라이언트 트리에서는 동작하지 않는다. code-theme.css 를 앱 globals 에서 wiring 해야 --shiki-* 변수가 채워진다(이 컴포넌트는 import 하지 않음)."
      },
      "examples": [
        {
          "title": "서버 컴포넌트에서",
          "lang": "tsx",
          "code": "import { ServerCodeBlock } from \"@axe/ui/server\";\n\n// app/**/page.tsx (RSC — await 가능)\n<ServerCodeBlock\n  language=\"tsx\"\n  title=\"Button.tsx\"\n  code={source}\n/>"
        },
        {
          "title": "라벨 없이(헤더 생략)",
          "lang": "tsx",
          "code": "import { ServerCodeBlock } from \"@axe/ui/server\";\n\n<ServerCodeBlock code={snippet} />"
        }
      ],
      "bestPractices": {
        "do": [
          "문법 색이 필요한 정적 코드에 사용.",
          "지원 언어(ts/tsx/js/json/bash/html/css/python/rust) 안에서 language 지정.",
          "복사 기능이 필요하면 클라이언트 CodeBlock 을 병용."
        ],
        "dont": [
          "\"use client\" 파일에서 import 하지 말 것(shiki server-only).",
          "미지원 언어를 기대하지 말 것 — plain text 로 fallback."
        ]
      },
      "consumeCss": "프레임(`axe-codeblock*`)은 CSS-only 로 재현 가능하나 토큰 색은 shiki 서버 렌더가 채우는 `.line`/token <span>(비-axe 클래스) 구조라 CSS-only 로는 하이라이팅을 재현할 수 없다. code-theme.css 가 --shiki-* 변수를 매핑해야 색이 나온다.",
      "url": "https://design.axelabs.ai/components/server-code-block"
    },
    {
      "name": "Sheet",
      "slug": "sheet",
      "group": "overlay",
      "summary": "화면 가장자리에서 슬라이드인하는 드로어 패널 (Radix Dialog 기반).",
      "description": "Dialog 와 같은 Radix Dialog 프리미티브 위에 .axe-sheet__* 스타일을 입힌 wrapper 지만, 중앙 카드 대신 화면 가장자리에서 밀려 들어오는 drawer 다. size 대신 side 변형(left/right/top/bottom)을 가지며 모바일 네비게이션·상세 패널·필터 서랍에 쓴다. right(28rem)·left(20rem)는 세로 전폭 사이드 패널, top/bottom 은 가로 전폭에 최대 높이 80vh 이고, bottom 은 iOS 세이프에어리어 여백을 갖는다. 포커스 트랩·Esc 닫기·바깥 클릭 닫기·ARIA modal 은 Radix 가, 슬라이드 애니메이션과 표면은 @axe/ui 가 담당한다.",
      "keywords": [
        "sheet",
        "시트",
        "드로어",
        "drawer",
        "패널",
        "서랍",
        "모바일",
        "네비게이션",
        "overlay",
        "radix"
      ],
      "props": [
        {
          "name": "open",
          "type": "boolean",
          "description": "열림 상태 (controlled). Radix Dialog.Root prop."
        },
        {
          "name": "onOpenChange",
          "type": "(open: boolean) => void",
          "description": "열림/닫힘 전환 콜백 (Root)."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "defaultValue": "false",
          "description": "uncontrolled 초기 열림 상태 (Root)."
        },
        {
          "name": "modal",
          "type": "boolean",
          "defaultValue": "true",
          "description": "true 면 바깥 상호작용 차단·포커스 트랩 (Root)."
        },
        {
          "name": "side",
          "type": "\"left\" | \"right\" | \"top\" | \"bottom\"",
          "defaultValue": "\"right\"",
          "description": "어느 가장자리에서 슬라이드할지. left=20rem·right=28rem 세로 전폭, top/bottom=가로 전폭·max-height 80vh."
        },
        {
          "name": "withOverlay",
          "type": "boolean",
          "defaultValue": "true",
          "description": "SheetContent 가 흐린 backdrop 오버레이를 함께 렌더할지."
        },
        {
          "name": "...rest (SheetContent)",
          "type": "React.ComponentPropsWithoutRef<typeof Dialog.Content>",
          "description": "onEscapeKeyDown·onPointerDownOutside·forceMount 등 Radix Content props 를 전파."
        },
        {
          "name": "...rest (Header/Footer)",
          "type": "React.HTMLAttributes<HTMLDivElement>",
          "description": "SheetHeader·SheetFooter 는 className 병합 후 나머지 속성을 루트 <div> 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-sheet__overlay",
          "purpose": "화면 전체 흐린 backdrop(fixed inset:0).",
          "stability": "stable"
        },
        {
          "className": "axe-sheet__content",
          "purpose": "가장자리 패널 표면(flex column·overflow auto).",
          "stability": "stable"
        },
        {
          "className": "axe-sheet__content--right",
          "purpose": "우측 세로 패널(min 28rem/92vw) 변형(기본).",
          "stability": "stable"
        },
        {
          "className": "axe-sheet__content--left",
          "purpose": "좌측 세로 패널(min 20rem/92vw) 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-sheet__content--top",
          "purpose": "상단 가로 패널(max-height 80vh) 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-sheet__content--bottom",
          "purpose": "하단 가로 패널(80vh·세이프에어리어 여백) 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-sheet__header",
          "purpose": "title+설명 스택(우상단 close X 자리 padding-right).",
          "stability": "stable"
        },
        {
          "className": "axe-sheet__title",
          "purpose": "제목(display 폰트·semibold).",
          "stability": "stable"
        },
        {
          "className": "axe-sheet__description",
          "purpose": "보조 설명 문단(secondary 톤).",
          "stability": "stable"
        },
        {
          "className": "axe-sheet__footer",
          "purpose": "하단 고정 우정렬 액션 라인(margin-top auto·상단 구분선).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Esc 로 닫기(트리거로 포커스 복원)",
          "Tab/Shift+Tab 은 패널 안에서만 순환(포커스 트랩)",
          "열릴 때 첫 포커스 대상으로 자동 이동, 닫힐 때 트리거로 복원"
        ],
        "aria": "Radix Dialog 기반이라 콘텐츠에 role=dialog·aria-modal=true 가 붙고, SheetTitle 을 aria-labelledby, SheetDescription 을 aria-describedby 로 자동 연결한다. 닫기 X 는 자동 생성되지 않으므로 SheetClose 를 직접 배치한다.",
        "notes": "SheetTitle 은 접근성상 필수(생략 시 Radix 개발 경고). modal 이라 바깥 스크롤이 잠긴다. side 별로 다른 슬라이드 방향 애니메이션을 쓰고 data-state=closed 로 퇴장이 갈리며, prefers-reduced-motion 시 모든 슬라이드/트랜지션이 제거된다."
      },
      "examples": [
        {
          "title": "좌측 네비게이션 드로어",
          "lang": "tsx",
          "code": "import {\n  Sheet,\n  SheetTrigger,\n  SheetContent,\n  SheetHeader,\n  SheetTitle,\n  SheetDescription,\n  SheetClose,\n  Button,\n} from \"@axe/ui\";\n\n<Sheet>\n  <SheetTrigger asChild>\n    <Button variant=\"ghost\">메뉴</Button>\n  </SheetTrigger>\n  <SheetContent side=\"left\">\n    <SheetHeader>\n      <SheetTitle>탐색</SheetTitle>\n      <SheetDescription>워크스페이스 이동</SheetDescription>\n    </SheetHeader>\n    {/* … 네비 링크 … */}\n    <SheetClose asChild>\n      <Button variant=\"ghost\">닫기</Button>\n    </SheetClose>\n  </SheetContent>\n</Sheet>"
        },
        {
          "title": "하단 시트 (모바일 액션)",
          "lang": "tsx",
          "code": "import { Sheet, SheetContent, SheetTitle, SheetFooter, Button } from \"@axe/ui\";\n\n<Sheet open={open} onOpenChange={setOpen}>\n  <SheetContent side=\"bottom\">\n    <SheetTitle>공유 옵션</SheetTitle>\n    <SheetFooter>\n      <Button variant=\"primary\">링크 복사</Button>\n    </SheetFooter>\n  </SheetContent>\n</Sheet>"
        },
        {
          "title": "CSS-only 정적 표면 (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-sheet__overlay\"></div>\n<div class=\"axe-sheet__content axe-sheet__content--left\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"sh-t\">\n  <div class=\"axe-sheet__header\">\n    <h2 id=\"sh-t\" class=\"axe-sheet__title\">탐색</h2>\n    <p class=\"axe-sheet__description\">워크스페이스 이동</p>\n  </div>\n  <div class=\"axe-sheet__footer\">\n    <button class=\"axe-btn axe-btn--ghost\">닫기</button>\n  </div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "SheetTitle 을 항상 넣을 것(접근성 라벨) — 숨기려면 sr-only.",
          "모바일 네비/필터/상세는 side=left|right, 짧은 액션 목록은 side=bottom.",
          "닫기 경로를 SheetClose 로 명시할 것."
        ],
        "dont": [
          "긴 폼·읽기 흐름을 top/bottom(80vh 상한)에 밀어 넣지 말 것 — 세로 패널이 적합.",
          "중앙 확인 모달 용도로 쓰지 말 것 — 그건 Dialog/AlertDialog."
        ]
      },
      "consumeCss": "정적 열림 표면은 .axe-sheet__* 마크업으로 재현 가능하다. 단 포털·포커스 트랩·Esc/바깥클릭 닫기·닫힘 슬라이드(data-state=closed)는 Radix 런타임이 만들므로, 실제 상호작용 드로어는 React 컴포넌트로 소비해야 한다.",
      "url": "https://design.axelabs.ai/components/sheet"
    },
    {
      "name": "Sidebar",
      "slug": "sidebar",
      "group": "navigation",
      "summary": "앱 셸 좌측 세로 네비게이션 — 활성 항목 + 접이식 그룹.",
      "description": "Radix 비의존 자체 구현. semantic <nav> + <ul> + collapsible 그룹 위에 controlled/uncontrolled 활성 상태와 그룹 펼침/접힘을 얹었다. items 배열로 data-driven 하게 쓰거나, SidebarItem/SidebarGroup 을 children 으로 조합(composition)해 쓴다 — 둘을 동시에 넘기지 말 것. collapsible=false 면 모든 그룹이 항상 펼쳐진 정적 헤더로 렌더된다. 보통 AppShell 의 sidebar 슬롯에 넣는다.",
      "keywords": [
        "sidebar",
        "사이드바",
        "side nav",
        "네비게이션",
        "navigation",
        "menu",
        "collapsible",
        "그룹"
      ],
      "props": [
        {
          "name": "items",
          "type": "SidebarNode[]",
          "description": "Data-driven 항목 트리. SidebarItemNode({ kind: 'item', value, label, icon?, href?, disabled? }) 와 SidebarGroupNode({ kind: 'group', label, icon?, id?, items }) 의 배열. children 과 동시 사용 금지."
        },
        {
          "name": "value",
          "type": "string",
          "description": "활성 항목 value (controlled). 주면 onValueChange 도 함께 줘야 한다(아니면 dev 경고)."
        },
        {
          "name": "defaultValue",
          "type": "string",
          "description": "uncontrolled 초기 활성 value."
        },
        {
          "name": "onValueChange",
          "type": "(value: string) => void",
          "description": "항목 선택 시 호출."
        },
        {
          "name": "collapsible",
          "type": "boolean",
          "defaultValue": "true",
          "description": "그룹 접기/펴기 허용. false 면 그룹 헤더가 비-interactive <div> 로 항상 펼침."
        },
        {
          "name": "defaultExpanded",
          "type": "Record<string, boolean>",
          "description": "group.id 별 초기 펼침 상태. 미지정 시 모든 그룹 펼침(보수적)."
        },
        {
          "name": "ariaLabel",
          "type": "string",
          "defaultValue": "\"Sidebar\"",
          "description": "루트 <nav> 의 aria-label(스크린리더)."
        },
        {
          "name": "children",
          "type": "React.ReactNode",
          "description": "composition 모드 — SidebarItem/SidebarGroup 을 직접 배치. items 대신 사용."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLElement>, \"onChange\">",
          "description": "나머지는 루트 <nav> 로 전파."
        },
        {
          "name": "SidebarItem.value",
          "type": "string",
          "required": true,
          "description": "활성 비교 키."
        },
        {
          "name": "SidebarItem.active",
          "type": "boolean",
          "description": "명시적 active — Provider 없이 쓰거나 강제 표시할 때. 미지정 시 컨텍스트의 active===value 로 판정."
        },
        {
          "name": "SidebarItem.href",
          "type": "string",
          "description": "주면 <a>, 없으면 <button> 으로 렌더."
        },
        {
          "name": "SidebarItem.icon",
          "type": "React.ReactNode",
          "description": "라벨 앞 아이콘(aria-hidden)."
        },
        {
          "name": "SidebarItem.disabled",
          "type": "boolean",
          "description": "비활성. 클릭 무시 + aria-disabled/disabled."
        },
        {
          "name": "SidebarItem.onSelect",
          "type": "(value: string) => void",
          "description": "선택 시 항목 로컬 콜백(컨텍스트 setActive 와 별개)."
        },
        {
          "name": "SidebarGroup.label",
          "type": "React.ReactNode",
          "required": true,
          "description": "그룹 헤더 라벨."
        },
        {
          "name": "SidebarGroup.id",
          "type": "string",
          "description": "defaultExpanded 매핑 키. 미지정 시 label 의 string 값."
        },
        {
          "name": "SidebarGroup.defaultOpen",
          "type": "boolean",
          "defaultValue": "true",
          "description": "uncontrolled 초기 펼침."
        },
        {
          "name": "SidebarGroup.open",
          "type": "boolean",
          "description": "controlled 펼침 상태."
        },
        {
          "name": "SidebarGroup.onOpenChange",
          "type": "(open: boolean) => void",
          "description": "펼침 토글 시 호출."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-sidebar",
          "purpose": "루트 <nav>.",
          "stability": "stable"
        },
        {
          "className": "axe-sidebar__list",
          "purpose": "최상위 항목 <ul role=\"list\">.",
          "stability": "stable"
        },
        {
          "className": "axe-sidebar__item",
          "purpose": "단일 항목 <li>.",
          "stability": "stable"
        },
        {
          "className": "axe-sidebar__item--active",
          "purpose": "활성 항목 <li>.",
          "stability": "stable"
        },
        {
          "className": "axe-sidebar__item--disabled",
          "purpose": "비활성 항목 <li>.",
          "stability": "stable"
        },
        {
          "className": "axe-sidebar__link",
          "purpose": "항목의 클릭 대상 <a>/<button>.",
          "stability": "stable"
        },
        {
          "className": "axe-sidebar__icon",
          "purpose": "항목/그룹 앞 아이콘 span.",
          "stability": "stable"
        },
        {
          "className": "axe-sidebar__label",
          "purpose": "항목 라벨 텍스트 span.",
          "stability": "stable"
        },
        {
          "className": "axe-sidebar__group",
          "purpose": "그룹 <li>.",
          "stability": "stable"
        },
        {
          "className": "axe-sidebar__group-header",
          "purpose": "그룹 토글 <button>(collapsible) 헤더.",
          "stability": "stable"
        },
        {
          "className": "axe-sidebar__group-header--static",
          "purpose": "비-collapsible 그룹 헤더 <div>(토글 불가).",
          "stability": "stable"
        },
        {
          "className": "axe-sidebar__group-label",
          "purpose": "그룹 헤더 라벨 span.",
          "stability": "stable"
        },
        {
          "className": "axe-sidebar__group-list",
          "purpose": "그룹 하위 항목 패널 <ul>. 접힘 시 hidden 속성.",
          "stability": "stable"
        },
        {
          "className": "axe-sidebar__chevron",
          "purpose": "그룹 펼침 표시 chevron span(장식).",
          "stability": "internal"
        }
      ],
      "a11y": {
        "keyboard": [
          "Tab — 링크/버튼/그룹 헤더를 문서 순서로 이동(자체 roving tabindex 없음)",
          "Enter/Space — 그룹 헤더에서 펼침/접힘(네이티브 <button>)"
        ],
        "aria": "루트 <nav aria-label> 안에 <ul role=\"list\">. 활성 항목의 <a>/<button> 에 aria-current=\"page\". 그룹 헤더는 <button aria-expanded aria-controls={panelId}>, 하위 패널 <ul> 은 접힘 시 hidden 속성으로 AX 트리에서 제외. disabled 항목은 <button disabled> 또는 <a aria-disabled=\"true\">.",
        "notes": "controlled 로 쓸 땐 value 와 onValueChange 를 반드시 함께 준다(하나만 주면 dev 콘솔 경고 + 선택이 silent 무시). collapsible=false 면 그룹 헤더가 <div class=\"...--static\"> 로 렌더되어 포커스/토글 불가."
      },
      "examples": [
        {
          "title": "data-driven (controlled)",
          "lang": "tsx",
          "code": "import { Sidebar } from \"@axe/ui\";\n\n<Sidebar\n  ariaLabel=\"Primary\"\n  value={current}\n  onValueChange={setCurrent}\n  defaultExpanded={{ proj: true }}\n  items={[\n    { kind: \"item\", value: \"home\", label: \"홈\" },\n    {\n      kind: \"group\",\n      id: \"proj\",\n      label: \"프로젝트\",\n      items: [\n        { kind: \"item\", value: \"frame\", label: \"frame\" },\n        { kind: \"item\", value: \"hive\", label: \"hive\" },\n      ],\n    },\n  ]}\n/>"
        },
        {
          "title": "composition (uncontrolled)",
          "lang": "tsx",
          "code": "import { Sidebar, SidebarGroup, SidebarItem } from \"@axe/ui\";\n\n<Sidebar defaultValue=\"home\">\n  <SidebarItem value=\"home\">홈</SidebarItem>\n  <SidebarGroup label=\"프로젝트\" defaultOpen>\n    <SidebarItem value=\"frame\">frame</SidebarItem>\n    <SidebarItem value=\"hive\" href=\"/hive\">hive</SidebarItem>\n  </SidebarGroup>\n</Sidebar>"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<nav class=\"axe-sidebar\" aria-label=\"Primary\">\n  <ul class=\"axe-sidebar__list\" role=\"list\">\n    <li class=\"axe-sidebar__item axe-sidebar__item--active\">\n      <a href=\"/home\" class=\"axe-sidebar__link\" aria-current=\"page\">\n        <span class=\"axe-sidebar__label\">홈</span>\n      </a>\n    </li>\n    <li class=\"axe-sidebar__group\">\n      <button type=\"button\" class=\"axe-sidebar__group-header\"\n              aria-expanded=\"true\" aria-controls=\"grp-proj\">\n        <span class=\"axe-sidebar__group-label\">프로젝트</span>\n        <span class=\"axe-sidebar__chevron\" aria-hidden=\"true\">›</span>\n      </button>\n      <ul id=\"grp-proj\" role=\"list\" class=\"axe-sidebar__group-list\">\n        <li class=\"axe-sidebar__item\">\n          <a href=\"/frame\" class=\"axe-sidebar__link\">\n            <span class=\"axe-sidebar__label\">frame</span>\n          </a>\n        </li>\n      </ul>\n    </li>\n  </ul>\n</nav>"
        }
      ],
      "bestPractices": {
        "do": [
          "controlled 로 쓸 땐 value 와 onValueChange 를 항상 쌍으로 넘긴다.",
          "라우팅이 있으면 SidebarItem 에 href 를 줘 <a> 로 렌더(우클릭/새 탭 지원).",
          "그룹 초기 펼침은 group.id + defaultExpanded 로 제어한다."
        ],
        "dont": [
          "items 와 children 을 동시에 넘기지 말 것(한쪽만).",
          "aria-current 를 직접 붙이지 말 것 — 활성 판정이 자동 부여한다."
        ]
      },
      "consumeCss": "비-React(jinja/maud)에서도 동일 markup 으로 재현 가능. 활성 표시는 <li> 에 axe-sidebar__item--active + 링크에 aria-current=\"page\", 그룹 펼침은 헤더 aria-expanded 와 패널 <ul> 의 hidden 속성을 직접 토글한다(펼침 상태 CSS 훅 없음 — hidden 이 진실).",
      "url": "https://design.axelabs.ai/components/sidebar"
    },
    {
      "name": "Skeleton",
      "slug": "skeleton",
      "group": "feedback",
      "summary": "콘텐츠 도착 전 자리를 차지하는 placeholder — shimmer 로딩 시각.",
      "description": "LLM 스트리밍 응답·표·카드·메시지가 도착하기 전 그 자리를 미리 점유해 layout shift 를 막는다. variant 로 모양(text=1em 라인 / circle=정원 아바타 / rect=기본 카드·이미지)을 고르고, width/height 로 크기를 지정한다(number 는 px 자동 변환, string 은 그대로). shimmer 는 ::after transform sweep(compositor-only)이며 aria-hidden 이라 스크린리더에 노출되지 않는다. ToolCallCard 의 pending 자리 동반 시각.",
      "keywords": [
        "skeleton",
        "스켈레톤",
        "placeholder",
        "loading",
        "shimmer",
        "layout shift",
        "자리표시"
      ],
      "props": [
        {
          "name": "variant",
          "type": "\"text\" | \"circle\" | \"rect\"",
          "defaultValue": "\"rect\"",
          "description": "모양. text=1em 라인(radius-sm) · circle=정원(aspect 1/1) · rect=기본 사각."
        },
        {
          "name": "width",
          "type": "number | string",
          "description": "너비. number 면 px 로 변환, string 이면 그대로(예 \"60%\"). 인라인 style 로 적용."
        },
        {
          "name": "height",
          "type": "number | string",
          "description": "높이. number 면 px, string 이면 그대로. text variant 는 미지정 시 1em."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLDivElement>",
          "description": "className 병합, style 은 width/height 뒤에 전개되어 덮어쓸 수 있고, 나머지는 루트 div 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-skeleton",
          "purpose": "루트 div — subtle 배경·radius-md·overflow hidden, ::after shimmer sweep.",
          "stability": "stable"
        },
        {
          "className": "axe-skeleton--text",
          "purpose": "text 변형 — height 1em·radius-sm(라인 모양).",
          "stability": "stable"
        },
        {
          "className": "axe-skeleton--circle",
          "purpose": "circle 변형 — 완전 원(radius 9999·aspect 1/1).",
          "stability": "stable"
        },
        {
          "className": "axe-skeleton--rect",
          "purpose": "rect 변형(기본) — 별도 규칙 없는 마커, width/height 로 크기 지정.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "루트에 role=\"presentation\" + aria-hidden=\"true\" 고정 — placeholder 는 스크린리더가 무시한다(로딩 노이즈 방지).",
        "notes": "로딩 자체를 SR 에 알리려면 상위 컨테이너에 aria-busy 또는 별도 상태 통지(status/live region)를 둔다. prefers-reduced-motion 시 ::after shimmer(content:none)가 꺼지고 정적 회색만 남는다."
      },
      "examples": [
        {
          "title": "텍스트 라인 여러 줄",
          "lang": "tsx",
          "code": "import { Skeleton } from \"@axe/ui\";\n\nexport function Example() {\n  return (\n    <>\n      <Skeleton variant=\"text\" width=\"80%\" />\n      <Skeleton variant=\"text\" width=\"60%\" />\n    </>\n  );\n}"
        },
        {
          "title": "아바타 + 카드 자리",
          "lang": "tsx",
          "code": "import { Skeleton } from \"@axe/ui\";\n\nexport function Example() {\n  return (\n    <>\n      <Skeleton variant=\"circle\" width={40} />\n      <Skeleton variant=\"rect\" width=\"100%\" height={120} />\n    </>\n  );\n}"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-skeleton axe-skeleton--text\" role=\"presentation\"\n     aria-hidden=\"true\" style=\"width: 80%\"></div>\n<div class=\"axe-skeleton axe-skeleton--circle\" role=\"presentation\"\n     aria-hidden=\"true\" style=\"width: 40px\"></div>"
        }
      ],
      "bestPractices": {
        "do": [
          "실제 콘텐츠와 비슷한 폭/높이로 잡아 layout shift 를 없앤다.",
          "text 라인은 폭을 조금씩 다르게 줘 실제 텍스트처럼 보이게 한다."
        ],
        "dont": [
          "장식용 무한 shimmer 로 남겨두지 말 것 — 콘텐츠가 오면 즉시 교체.",
          "정보 전달용 요소를 skeleton 으로 대체하지 말 것(aria-hidden 이라 SR 에 안 읽힌다)."
        ]
      },
      "consumeCss": "순수 CSS 로 재현 가능 — .axe-skeleton + variant 클래스에 인라인 width/height, role=\"presentation\" aria-hidden=\"true\" 를 직접 부여한다. 스타일(indicators.css)은 components.css 가 @import 하므로 별도 로드 불요.",
      "playground": {
        "componentKey": "skeleton",
        "defaults": {
          "variant": "rect",
          "width": 180,
          "height": 16
        },
        "controls": [
          {
            "prop": "width",
            "kind": "number",
            "min": 0,
            "max": 400,
            "step": 4
          },
          {
            "prop": "height",
            "kind": "number",
            "min": 0,
            "max": 200,
            "step": 2
          }
        ]
      },
      "url": "https://design.axelabs.ai/components/skeleton"
    },
    {
      "name": "Slider",
      "slug": "slider",
      "group": "input",
      "summary": "범위 안에서 단일 값을 고르는 슬라이더 (네이티브 range, dep 0).",
      "description": "네이티브 <input type=range> 위의 얇은 래퍼로 외부 의존성이 없다. 키보드 내비게이션(←/→·Home/End)·role=slider·aria-valuenow 는 브라우저가 기본 제공하고, @axe/ui 는 track/thumb 를 토큰으로 그린다. 진행 채움은 --axe-slider-pct(0~100) CSS 변수로 표현해 JS 없이도 서버(maud/jinja)가 같은 모양을 낼 수 있다. showValue·formatValue 로 현재 값을 우측에 표시한다.",
      "keywords": [
        "slider",
        "슬라이더",
        "range",
        "범위",
        "볼륨",
        "값 조절",
        "input"
      ],
      "props": [
        {
          "name": "value",
          "type": "number",
          "required": true,
          "description": "현재 값 (controlled)."
        },
        {
          "name": "onValueChange",
          "type": "(value: number) => void",
          "required": true,
          "description": "값이 바뀔 때 콜백."
        },
        {
          "name": "min",
          "type": "number",
          "defaultValue": "0",
          "description": "최소값."
        },
        {
          "name": "max",
          "type": "number",
          "defaultValue": "100",
          "description": "최대값."
        },
        {
          "name": "step",
          "type": "number",
          "defaultValue": "1",
          "description": "증감 단위."
        },
        {
          "name": "showValue",
          "type": "boolean",
          "defaultValue": "false",
          "description": "우측에 현재 값 라벨(<output>) 표시."
        },
        {
          "name": "formatValue",
          "type": "(value: number) => string",
          "description": "값 라벨 포맷터 (예: (v) => `${v}%`). showValue 일 때만 사용."
        },
        {
          "name": "ariaLabel",
          "type": "string",
          "description": "visible label 이 없을 때 range input 의 접근성 라벨(필수에 준함)."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "defaultValue": "false",
          "description": "비활성 (흐림 + 커서 차단)."
        },
        {
          "name": "...rest",
          "type": "React.InputHTMLAttributes<HTMLInputElement>",
          "description": "나머지는 range input 으로 전파 (value·onChange·type·min·max·step 제외)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-slider",
          "purpose": "flex 래퍼(range input + 선택적 값 라벨).",
          "stability": "stable"
        },
        {
          "className": "axe-slider--disabled",
          "purpose": "비활성 변형(전체 흐림).",
          "stability": "stable"
        },
        {
          "className": "axe-slider__input",
          "purpose": "range input — 토큰 track/thumb, --axe-slider-pct 로 accent 채움.",
          "stability": "stable"
        },
        {
          "className": "axe-slider__value",
          "purpose": "우측 현재 값 라벨(tabular-nums).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "←/↓ 감소, →/↑ 증가 (step 단위)",
          "Home/End 로 최소/최대",
          "PageUp/PageDown 으로 큰 폭 증감 (네이티브 range 기본)"
        ],
        "aria": "네이티브 <input type=range> — 브라우저가 role=slider + aria-valuenow/valuemin/valuemax 를 자동 제공. visible label 이 없으면 ariaLabel 을 반드시 지정.",
        "notes": "focus-visible 시 thumb 에 --ring. 채움은 --axe-slider-pct(0~100) CSS 변수로 그라데이션(WebKit)·::-moz-range-progress(Firefox) 양쪽 스타일. prefers-reduced-motion 시 thumb 트랜지션 제거."
      },
      "examples": [
        {
          "title": "값 표시 · 포맷",
          "lang": "tsx",
          "code": "import { Slider } from \"@axe/ui\";\n\n<Slider\n  value={vol}\n  onValueChange={setVol}\n  min={0}\n  max={100}\n  showValue\n  formatValue={(v) => `${v}%`}\n  ariaLabel=\"볼륨\"\n/>"
        },
        {
          "title": "CSS-only (비-React) — 서버가 채움 비율 주입",
          "lang": "html",
          "code": "<div class=\"axe-slider\">\n  <input\n    class=\"axe-slider__input\"\n    type=\"range\"\n    min=\"0\"\n    max=\"100\"\n    value=\"40\"\n    aria-label=\"볼륨\"\n    style=\"--axe-slider-pct: 40\"\n  />\n  <output class=\"axe-slider__value\">40%</output>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "visible label 이 없으면 ariaLabel 을 반드시 지정.",
          "정밀 입력이 필요하면 showValue 로 현재 값을 함께 노출."
        ],
        "dont": [
          "정확한 수치 입력이 핵심이면 슬라이더만 두지 말 것 — 숫자 Input 을 병행.",
          "step 을 지나치게 잘게 두어 키보드 조작이 과도하게 걸리게 하지 말 것."
        ]
      },
      "consumeCss": "완전 CSS-only 소비 가능 — 서버가 --axe-slider-pct(0~100)만 계산해 인라인 style 로 넣으면 동일한 accent 채움이 나온다. Firefox 는 ::-moz-range-progress 로 채움을 그린다.",
      "playground": {
        "componentKey": "slider",
        "defaults": {
          "showValue": true,
          "disabled": false
        }
      },
      "url": "https://design.axelabs.ai/components/slider"
    },
    {
      "name": "Spinner",
      "slug": "spinner",
      "group": "feedback",
      "summary": "진척률을 알 수 없는 로딩 시그널 — CSS-only 회전 원.",
      "description": "동기 LLM 호출·network round-trip 처럼 진척을 측정할 수 없는 대기 상태의 표준 인디케이터. SVG circle + stroke-dasharray 회전으로 JS 의존 없이 돈다. size 로 크기(sm 16 / md 20 / lg 32px)를, label 로 접근/시각 텍스트를 정한다 — showLabel=false(기본)면 sr-only, true 면 옆에 함께 렌더. ToolCallCard 의 running 상태에 인라인으로 자연스럽게 들어간다.",
      "keywords": [
        "spinner",
        "스피너",
        "loading",
        "로딩",
        "indeterminate",
        "busy",
        "대기"
      ],
      "props": [
        {
          "name": "size",
          "type": "\"sm\" | \"md\" | \"lg\"",
          "defaultValue": "\"md\"",
          "description": "크기 — sm 16px · md 20px · lg 32px. 실제 치수는 svg width/height(px)로 적용."
        },
        {
          "name": "label",
          "type": "string",
          "defaultValue": "\"Loading\"",
          "description": "접근 텍스트. showLabel=false 면 sr-only(aria-label), true 면 시각 라벨로도 렌더."
        },
        {
          "name": "showLabel",
          "type": "boolean",
          "defaultValue": "false",
          "description": "true 면 스피너 옆에 label 을 눈에 보이게 표시."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLSpanElement>",
          "description": "className 병합, 나머지는 루트 <span> 으로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-spinner",
          "purpose": "루트 span — inline-flex·accent 색·라벨과 gap.",
          "stability": "stable"
        },
        {
          "className": "axe-spinner__label",
          "purpose": "시각 라벨(showLabel=true) — text-sm·secondary.",
          "stability": "stable"
        },
        {
          "className": "axe-spinner__svg",
          "purpose": "회전하는 SVG(0.9s linear). reduced-motion 시 hidden.",
          "stability": "internal"
        },
        {
          "className": "axe-spinner__track",
          "purpose": "배경 원 stroke(currentColor 18%).",
          "stability": "internal"
        },
        {
          "className": "axe-spinner__head",
          "purpose": "회전 헤드 stroke(dasharray 1/4 가시).",
          "stability": "internal"
        },
        {
          "className": "axe-spinner__dot",
          "purpose": "reduced-motion 대체 dot(평소 숨김, motion off 시 pulse).",
          "stability": "internal"
        },
        {
          "className": "axe-spinner--sm",
          "purpose": "size 마커 — 전용 CSS 규칙 없음(크기는 svg px). sm/md/lg 동일.",
          "stability": "internal"
        }
      ],
      "a11y": {
        "aria": "루트 <span> 에 role=\"status\" + aria-live=\"polite\" + (showLabel=false 일 때)aria-label=label. SVG 는 aria-hidden focusable=false. showLabel 여부와 무관하게 label 텍스트가 항상 DOM 에 존재(false 면 .axe-sr-only 로 시각만 숨김) → SR 에 상태가 전달된다.",
        "notes": "비대화 요소라 키보드 상호작용 없음. prefers-reduced-motion 시 회전 SVG 를 숨기고 dot pulse(깜박임)로 대체."
      },
      "examples": [
        {
          "title": "기본(sr-only 라벨)",
          "lang": "tsx",
          "code": "import { Spinner } from \"@axe/ui\";\n\n<Spinner label=\"Agent 실행 중\" />"
        },
        {
          "title": "라벨 표시 + 크기",
          "lang": "tsx",
          "code": "import { Spinner } from \"@axe/ui\";\n\n<Spinner size=\"lg\" label=\"불러오는 중…\" showLabel />"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<span class=\"axe-spinner axe-spinner--md\" role=\"status\" aria-live=\"polite\" aria-label=\"Loading\">\n  <svg class=\"axe-spinner__svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" focusable=\"false\">\n    <circle class=\"axe-spinner__track\" cx=\"12\" cy=\"12\" r=\"10\" stroke-width=\"3\" />\n    <circle class=\"axe-spinner__head\" cx=\"12\" cy=\"12\" r=\"10\" stroke-width=\"3\" stroke-linecap=\"round\" />\n  </svg>\n  <span class=\"axe-spinner__dot\" aria-hidden=\"true\"></span>\n  <span class=\"axe-sr-only\">Loading</span>\n</span>"
        }
      ],
      "bestPractices": {
        "do": [
          "진척을 측정할 수 없을 때만 Spinner — 측정 가능하면 Progress.",
          "의미 있는 label 을 준다(무엇을 기다리는지)."
        ],
        "dont": [
          "한 화면에 여러 스피너를 흩뿌리지 말 것 — 로딩 영역을 하나로 묶는다.",
          "버튼 안 로딩에 큰 lg 를 쓰지 말 것(sm/md 가 인라인에 맞다)."
        ]
      },
      "consumeCss": "CSS-only 로 재현 가능하되 SVG 구조 전체 + .axe-sr-only 유틸 클래스 + role/aria-live 를 마크업에서 직접 넣어야 한다. size 클래스는 전용 규칙이 없어 svg width/height(sm 16·md 20·lg 32)로 크기를 정한다. 스타일(indicators.css)은 components.css 가 @import 하므로 별도 로드 불요.",
      "playground": {
        "componentKey": "spinner",
        "defaults": {
          "size": "md",
          "label": "Loading",
          "showLabel": false
        }
      },
      "url": "https://design.axelabs.ai/components/spinner"
    },
    {
      "name": "Splitter",
      "slug": "splitter",
      "group": "layout",
      "summary": "키보드·a11y 갖춘 2-pane 리사이즈 컨테이너.",
      "description": "두 pane 을 가로(좌/우) 또는 세로(상/하)로 나누고 사이 핸들로 크기를 조절한다. Blueprint 의 마우스 전용 ResizeHandle 을 role=\"separator\" + 화살표/Home/End 키보드 + 터치 히트영역 확장으로 일반화했다. controlled(size + onSizeChange)와 uncontrolled(defaultSize) 를 모두 지원하며, 드래그 중에는 rAF 로 첫 pane DOM 에 직접 써 리렌더를 억제하고 커밋은 pointerup 1회만 수행한다.",
      "keywords": [
        "splitter",
        "스플리터",
        "resize",
        "리사이즈",
        "resizable",
        "pane",
        "split",
        "separator",
        "드래그",
        "패널 분할"
      ],
      "props": [
        {
          "name": "orientation",
          "type": "\"horizontal\" | \"vertical\"",
          "required": true,
          "description": "pane 배치 축. horizontal = 좌/우, vertical = 상/하."
        },
        {
          "name": "defaultSize",
          "type": "number | string",
          "defaultValue": "\"50%\"",
          "description": "uncontrolled 초기 크기. number=px, string=CSS length(예 \"50%\"). string 이면 mount 후 컨테이너 측정으로 px 환산."
        },
        {
          "name": "size",
          "type": "number",
          "description": "controlled px. 지정 시 defaultSize 무시 — 크기를 소비자가 소유."
        },
        {
          "name": "onSizeChange",
          "type": "(px: number) => void",
          "description": "크기 확정 시 호출. 키보드는 즉시, 드래그는 pointerup 에서 1회. controlled 소비자는 여기서 size 를 갱신."
        },
        {
          "name": "minSize",
          "type": "number",
          "defaultValue": "80",
          "description": "첫 pane 최소 px."
        },
        {
          "name": "maxSize",
          "type": "number",
          "description": "첫 pane 최대 px. 미지정 시 컨테이너 측정값 − minSize 로 자동 산출."
        },
        {
          "name": "children",
          "type": "[React.ReactNode, React.ReactNode]",
          "required": true,
          "description": "정확히 2개. [0]=첫 pane(크기 제어 대상), [1]=둘째 pane(남는 공간)."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLDivElement>, \"children\" | \"onChange\">",
          "description": "className 은 병합, 나머지는 루트 div 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-splitter",
          "purpose": "루트 flex 컨테이너(width/height 100%, min 0).",
          "stability": "stable"
        },
        {
          "className": "axe-splitter--horizontal",
          "purpose": "flex-direction:row(좌/우 분할).",
          "stability": "stable"
        },
        {
          "className": "axe-splitter--vertical",
          "purpose": "flex-direction:column(상/하 분할).",
          "stability": "stable"
        },
        {
          "className": "axe-splitter__pane",
          "purpose": "pane 공통 — min 0 + overflow:auto.",
          "stability": "stable"
        },
        {
          "className": "axe-splitter__pane--second",
          "purpose": "둘째 pane — flex:1 1 auto 로 남는 공간 차지(첫 pane 은 인라인 width/height 로 크기 고정).",
          "stability": "stable"
        },
        {
          "className": "axe-splitter__handle",
          "purpose": "드래그/포커스 핸들(role=separator). 시각 6px + ::before 26px 히트영역(WCAG 2.5.5). hover/focus 시 accent.",
          "stability": "stable"
        },
        {
          "className": "axe-splitter__handle-bar",
          "purpose": "핸들 중앙 시각 바(2×36px, aria-hidden 장식).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "←/→(horizontal) 또는 ↑/↓(vertical) — 핸들 포커스 시 ±5px",
          "Shift + 화살표 — ±20px",
          "Home — 첫 pane 을 minSize 로",
          "End — 첫 pane 을 maxSize(지정/자동)로"
        ],
        "aria": "핸들은 role=\"separator\" + tabIndex 0. aria-orientation 은 핸들 바의 축 기준이라 orientation 과 반대(좌우 분할=vertical 바, 상하 분할=horizontal 바). aria-valuenow/valuemin/valuemax = 첫 pane px, aria-valuetext 는 사람이 읽는 \"Left/Top pane Npx of Mpx (P%)\", aria-label=\"Resize panels\". 시각 바(__handle-bar)는 aria-hidden.",
        "notes": "포인터 캡처 드래그는 rAF 로 프레임당 1회 첫 pane DOM 에 직접 쓰고, state/onSizeChange 커밋은 pointerup 1회(고주사율 기기 과렌더 방지). prefers-reduced-motion 에서 핸들 트랜지션 제거. 알려진 한계: controlled 소비자가 onSizeChange 를 무시(size prop 유지)하고 리렌더도 없으면 드래그 잔여 px 가 남을 수 있음."
      },
      "examples": [
        {
          "title": "uncontrolled (좌/우)",
          "lang": "tsx",
          "code": "import { Splitter } from \"@axe/ui\";\n\n<Splitter orientation=\"horizontal\" defaultSize=\"40%\" minSize={200}>\n  <nav>목록</nav>\n  <main>상세</main>\n</Splitter>"
        },
        {
          "title": "controlled",
          "lang": "tsx",
          "code": "import { Splitter } from \"@axe/ui\";\nimport { useState } from \"react\";\n\nfunction Panes() {\n  const [w, setW] = useState(320);\n  return (\n    <Splitter orientation=\"horizontal\" size={w} onSizeChange={setW} minSize={160}>\n      <aside>사이드</aside>\n      <section>본문</section>\n    </Splitter>\n  );\n}"
        }
      ],
      "bestPractices": {
        "do": [
          "리사이즈 대상(사이드바·에디터 등)을 첫 pane 에 두고 본문을 둘째 pane 에 둔다.",
          "minSize 를 충분히 줘 pane 이 조작 불가능할 만큼 접히지 않게 한다."
        ],
        "dont": [
          "children 을 2개가 아닌 수로 넘기지 말 것(타입이 정확히 2-tuple).",
          "controlled 로 쓰면서 onSizeChange 를 무시하지 말 것(드래그가 커밋되지 않음)."
        ]
      },
      "consumeCss": "리사이즈는 포인터/키보드 JS 로직에 의존 — 순수 CSS 로는 재현 불가(정적 2단 레이아웃만 가능). 첫 pane 크기는 컴포넌트가 인라인 style(width/height)로 주므로 CSS-only 소비 시 직접 지정해야 한다.",
      "url": "https://design.axelabs.ai/components/splitter"
    },
    {
      "name": "StatusBanner",
      "slug": "status-banner",
      "group": "feedback",
      "summary": "폼/영역 옆에 고정되는 동적 상태 전이 알림 — SR 자동 발화.",
      "description": "저장됨·차단됨·분할 중·에러 같은 *동적* 상태 전이를 inline 한 자리에 알린다. Callout(정적 안내, role=\"note\")과 달리 <output> 루트에 role=\"status\"/\"alert\" 를 부여해 스크린리더가 자동 발화한다 — error/blocked 는 assertive(alert), 나머지는 polite(status). variant 가 좌측 4px accent bar 색과 기본 아이콘(✓/⊘/⋯/ⓘ/⚠/✕)을 정하고, autoHideMs 로 N ms 후 자동 unmount, dismissible 로 수동 닫기를 켠다. Toast(우상단 floating 스택)와 달리 폼 바로 아래 등 제자리에 머문다.",
      "keywords": [
        "status banner",
        "status-banner",
        "상태 배너",
        "inline",
        "live region",
        "저장됨",
        "알림",
        "announcement"
      ],
      "props": [
        {
          "name": "variant",
          "type": "\"saved\" | \"blocked\" | \"splitting\" | \"info\" | \"success\" | \"warning\" | \"error\"",
          "required": true,
          "description": "상태 종류 — 좌측 bar 색·기본 아이콘·발화 강도를 결정. error/blocked 는 assertive(alert), 그 외 polite(status)."
        },
        {
          "name": "title",
          "type": "React.ReactNode",
          "description": "굵은 제목 줄(.axe-status-banner__title). 있을 때만 렌더. HTMLAttributes 의 title 은 이 prop 으로 대체."
        },
        {
          "name": "description",
          "type": "React.ReactNode",
          "description": "보조 설명(.axe-status-banner__description). 있을 때만 렌더."
        },
        {
          "name": "dismissible",
          "type": "boolean",
          "description": "true 면 우측에 닫기(✕) 버튼을 표시 — 클릭 시 unmount + onDismiss 호출."
        },
        {
          "name": "onDismiss",
          "type": "() => void",
          "description": "수동 닫기(✕) 클릭 시 호출."
        },
        {
          "name": "autoHideMs",
          "type": "number",
          "description": "지정 시 N ms 후 자동 unmount(+ onAutoHide). 미지정이면 영구 표시."
        },
        {
          "name": "onAutoHide",
          "type": "() => void",
          "description": "autoHideMs 로 자동 사라질 때 호출."
        },
        {
          "name": "children",
          "type": "React.ReactNode",
          "description": "title/description 아래 body 안에 추가로 렌더(예: 액션 버튼)."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLOutputElement>, \"title\">",
          "description": "className 병합, 나머지는 루트 <output> 으로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-status-banner",
          "purpose": "루트 <output> — flex row·subtle 배경·좌측 4px bar·enter 애니메이션.",
          "stability": "stable"
        },
        {
          "className": "axe-status-banner__icon",
          "purpose": "좌측 아이콘 박스(20px, aria-hidden). variant 색 상속.",
          "stability": "stable"
        },
        {
          "className": "axe-status-banner__body",
          "purpose": "제목+설명+children 컨테이너(flex 1).",
          "stability": "stable"
        },
        {
          "className": "axe-status-banner__title",
          "purpose": "굵은 제목 줄(semibold).",
          "stability": "stable"
        },
        {
          "className": "axe-status-banner__description",
          "purpose": "설명 문단(secondary).",
          "stability": "stable"
        },
        {
          "className": "axe-status-banner__dismiss",
          "purpose": "우측 닫기(✕) 버튼 — focus-visible outline.",
          "stability": "stable"
        },
        {
          "className": "axe-status-banner--saved",
          "purpose": "saved 변형 — success bar + success-soft 배경.",
          "stability": "stable"
        },
        {
          "className": "axe-status-banner--success",
          "purpose": "success 변형 — success bar + success-soft 배경.",
          "stability": "stable"
        },
        {
          "className": "axe-status-banner--splitting",
          "purpose": "splitting 변형 — warning bar + 아이콘 회전(spin).",
          "stability": "stable"
        },
        {
          "className": "axe-status-banner--warning",
          "purpose": "warning 변형 — warning bar + warning-soft 배경.",
          "stability": "stable"
        },
        {
          "className": "axe-status-banner--blocked",
          "purpose": "blocked 변형 — danger bar + danger-soft 배경.",
          "stability": "stable"
        },
        {
          "className": "axe-status-banner--error",
          "purpose": "error 변형 — danger bar + danger-soft 배경.",
          "stability": "stable"
        },
        {
          "className": "axe-status-banner--info",
          "purpose": "info 변형 — accent bar + accent-soft 배경.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "dismissible 버튼은 네이티브 <button> — Enter/Space 로 닫기"
        ],
        "aria": "루트가 <output> 이며 error/blocked 는 role=\"alert\"(암묵적 aria-live=\"assertive\"+atomic), 그 외는 role=\"status\"(암묵적 polite). aria-live 를 명시로 중복 부여하지 않는다(일부 SR 이중 발화 방지). 아이콘은 aria-hidden, 닫기 버튼 aria-label=\"닫기\".",
        "notes": "prefers-reduced-motion 시 등장 애니메이션과 splitting 아이콘 회전이 정지. autoHideMs 는 setTimeout 기반이라 언마운트로만 사라지며(가시성 토글 아님), 되돌리려면 재마운트."
      },
      "examples": [
        {
          "title": "저장됨(polite, 2초 후 자동 사라짐)",
          "lang": "tsx",
          "code": "import { StatusBanner } from \"@axe/ui\";\n\n<StatusBanner variant=\"saved\" title=\"저장됨\" autoHideMs={2000} />"
        },
        {
          "title": "에러(assertive, 수동 닫기)",
          "lang": "tsx",
          "code": "import { StatusBanner } from \"@axe/ui\";\n\n<StatusBanner\n  variant=\"error\"\n  title=\"차단됨\"\n  description=\"tail-mass 가 책 P90 을 초과합니다.\"\n  dismissible\n  onDismiss={clearError}\n/>"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<output class=\"axe-status-banner axe-status-banner--error\" role=\"alert\">\n  <span class=\"axe-status-banner__icon\" aria-hidden=\"true\">✕</span>\n  <div class=\"axe-status-banner__body\">\n    <div class=\"axe-status-banner__title\">차단됨</div>\n    <div class=\"axe-status-banner__description\">tail-mass 가 책 P90 을 초과합니다.</div>\n  </div>\n</output>"
        }
      ],
      "bestPractices": {
        "do": [
          "동적 상태 전이(저장/차단/실패)에 쓰고, 사용자 행동을 즉시 막는 실패에만 error/blocked(assertive).",
          "성공 통지는 autoHideMs 로 잠깐 띄우고 자동 정리."
        ],
        "dont": [
          "정적 페이지 안내는 StatusBanner 로 하지 말 것 — 그건 Callout(role=\"note\").",
          "우상단 floating 스택 통지에 쓰지 말 것 — 그건 Toast."
        ]
      },
      "consumeCss": "정적 마크업은 순수 CSS 로 재현 가능하나 role(error/blocked→alert, 그 외→status)을 직접 부여해야 하고, autoHideMs/dismissible/가시성 토글은 React 전용이라 CSS-only 로는 재현 불가. 스타일(chat-ext.css)은 components.css 가 @import 하므로 별도 로드 불요.",
      "url": "https://design.axelabs.ai/components/status-banner"
    },
    {
      "name": "StatusLight",
      "slug": "status-light",
      "group": "feedback",
      "summary": "운영자 대시보드용 색 dot(+선택 label/timestamp) — 4-state 색 표준.",
      "description": "PipelineTable·ReporterCard 등이 상태를 색 dot 으로 표시하던 패턴을 SSOT 로 올린 것. variant 색이 AI-first 4-state(성공/경고/위험/대기/실행)를 표준화하며, 별칭(green/amber/red/gray/blue)은 canonical(ok/warning/danger/muted/running)로 접혀 CSS 는 5종만 정의한다. dot 은 currentColor 로 색을 상속하고, label/timestamp 는 optional 이라 단독 dot 으로도 inline row 로도 쓴다. pulse 로 \"지금 살아있다\"를 강조(주로 running).",
      "keywords": [
        "status light",
        "status-light",
        "상태 표시등",
        "dot",
        "indicator",
        "pipeline",
        "operator",
        "대시보드"
      ],
      "props": [
        {
          "name": "variant",
          "type": "\"ok\" | \"green\" | \"warning\" | \"amber\" | \"danger\" | \"red\" | \"muted\" | \"gray\" | \"running\" | \"blue\"",
          "defaultValue": "\"muted\"",
          "description": "색/의미. 별칭이 canonical 로 접힘 — green→ok(success) · amber→warning · red→danger · gray→muted(대기) · blue→running(info)."
        },
        {
          "name": "pulse",
          "type": "boolean",
          "description": "dot 깜박임(box-shadow ring). \"살아있음\" 강조용, 주로 running. reduced-motion 시 자동 정적."
        },
        {
          "name": "label",
          "type": "React.ReactNode",
          "description": "dot 옆 라벨. 생략 시 dot only. text-primary·medium."
        },
        {
          "name": "timestamp",
          "type": "React.ReactNode",
          "description": "label 다음 회색 타임스탬프(tabular-nums). ISO 문자열/ReactNode 모두 가능."
        },
        {
          "name": "size",
          "type": "\"sm\" | \"md\" | \"lg\"",
          "defaultValue": "\"sm\"",
          "description": "dot 크기 — sm 8 · md 10 · lg 12px. label 은 부모 폰트를 따른다."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLSpanElement>",
          "description": "className 병합, 나머지는 루트 <span> 으로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-status-light",
          "purpose": "루트 span — inline-flex·baseline 정렬·gap.",
          "stability": "stable"
        },
        {
          "className": "axe-status-light__dot",
          "purpose": "색 dot — currentColor 배경(색은 variant 가 결정).",
          "stability": "stable"
        },
        {
          "className": "axe-status-light__label",
          "purpose": "라벨 — text-primary·medium.",
          "stability": "stable"
        },
        {
          "className": "axe-status-light__timestamp",
          "purpose": "타임스탬프 — text-muted·tabular-nums·xs.",
          "stability": "stable"
        },
        {
          "className": "axe-status-light--ok",
          "purpose": "ok(success) — color --success.",
          "stability": "stable"
        },
        {
          "className": "axe-status-light--warning",
          "purpose": "warning — color --warning.",
          "stability": "stable"
        },
        {
          "className": "axe-status-light--danger",
          "purpose": "danger — color --danger.",
          "stability": "stable"
        },
        {
          "className": "axe-status-light--muted",
          "purpose": "muted(대기) — color --text-muted.",
          "stability": "stable"
        },
        {
          "className": "axe-status-light--running",
          "purpose": "running — color --info(pulse 권장).",
          "stability": "stable"
        },
        {
          "className": "axe-status-light--size-sm",
          "purpose": "dot 8px.",
          "stability": "stable"
        },
        {
          "className": "axe-status-light--size-md",
          "purpose": "dot 10px.",
          "stability": "stable"
        },
        {
          "className": "axe-status-light--size-lg",
          "purpose": "dot 12px.",
          "stability": "stable"
        },
        {
          "className": "axe-status-light--pulse",
          "purpose": "dot 깜박임(box-shadow ring, opacity 유지).",
          "stability": "stable"
        },
        {
          "className": "axe-status-light--with-content",
          "purpose": "label/timestamp 존재 시 붙는 마커 — 전용 CSS 규칙 없음.",
          "stability": "internal"
        }
      ],
      "a11y": {
        "aria": "dot 은 aria-hidden=\"true\" — 순수 시각. 컴포넌트는 role/aria-live 를 부여하지 않는다.",
        "notes": "색만으로는 의미가 SR·색각이상 사용자에게 전달되지 않는다(WCAG 1.4.1). 의미를 노출하려면 label 로 텍스트를 주거나 상위에서 aria-label/aria-live 를 부여한다. prefers-reduced-motion 시 pulse 정지."
      },
      "examples": [
        {
          "title": "단독 dot",
          "lang": "tsx",
          "code": "import { StatusLight } from \"@axe/ui\";\n\nexport function Example() {\n  return (\n    <>\n      <StatusLight variant=\"ok\" label=\"정상\" />\n      <StatusLight variant=\"muted\" label=\"대기\" />\n    </>\n  );\n}"
        },
        {
          "title": "실행 중 + label + timestamp + pulse",
          "lang": "tsx",
          "code": "import { StatusLight } from \"@axe/ui\";\n\n<StatusLight\n  variant=\"running\"\n  pulse\n  size=\"md\"\n  label=\"수집기 실행 중\"\n  timestamp=\"18:00:12\"\n/>"
        },
        {
          "title": "CSS-only (비-React, canonical 색만)",
          "lang": "html",
          "code": "<span class=\"axe-status-light axe-status-light--running axe-status-light--size-md axe-status-light--pulse axe-status-light--with-content\">\n  <span class=\"axe-status-light__dot\" aria-hidden=\"true\"></span>\n  <span class=\"axe-status-light__label\">수집기 실행 중</span>\n  <span class=\"axe-status-light__timestamp\">18:00:12</span>\n</span>"
        }
      ],
      "bestPractices": {
        "do": [
          "색과 함께 label(또는 상위 텍스트)로 의미를 이중 인코딩한다.",
          "\"지금 돌고 있다\"를 보여줄 running 에만 pulse 를 아껴 쓴다."
        ],
        "dont": [
          "색 dot 하나로만 성패를 표현하지 말 것(색각이상·SR 미전달).",
          "모든 행에 pulse 를 켜 시각 소음을 만들지 말 것."
        ]
      },
      "consumeCss": "순수 CSS 로 재현 가능 — canonical 색 클래스(ok/warning/danger/muted/running)만 쓰고 별칭은 직접 매핑한다. label/timestamp 존재 시 --with-content 마커를 붙이며(전용 규칙 없음), dot 에 aria-hidden 을 직접 준다. 스타일(dashboard.css)은 components.css 가 @import 하므로 별도 로드 불요.",
      "playground": {
        "componentKey": "status-light",
        "defaults": {
          "variant": "running",
          "pulse": true,
          "size": "md",
          "label": "수집기 실행 중",
          "timestamp": "18:00:12"
        },
        "controls": [
          {
            "prop": "variant",
            "kind": "select",
            "options": [
              "ok",
              "warning",
              "danger",
              "muted",
              "running"
            ]
          },
          {
            "prop": "label",
            "kind": "text"
          },
          {
            "prop": "timestamp",
            "kind": "text"
          }
        ]
      },
      "url": "https://design.axelabs.ai/components/status-light"
    },
    {
      "name": "StreamingMarkdown",
      "slug": "streaming-markdown",
      "group": "chat",
      "summary": "스트리밍 중인 LLM 응답을 Prose 로 감싸고 끝에 깜박이는 cursor 를 얹는 얇은 래퍼.",
      "description": "마크다운 파싱은 하지 않는다 — 이미 파싱된 children(react-markdown 결과 등)을 그대로 <Prose> 안에 렌더하고, isStreaming=true 일 때만 본문 끝에 axe-stream-cursor span 을 붙인다. 즉 '스트리밍처럼 보이게 하는 표시 층'일 뿐 파서·버퍼링·토큰 조합은 호출 측 책임이다. 루트는 Prose(axe-prose)라 산문 타이포그래피를 그대로 상속하며, maxWidth 를 100% 로 풀어 채팅 컬럼 폭에 맞춘다. 보통 단독으로 쓰기보다 AgentExecutor 가 assistant 메시지에 자동으로 사용한다.",
      "keywords": [
        "streaming markdown",
        "스트리밍",
        "cursor",
        "커서",
        "llm",
        "prose",
        "assistant",
        "chat"
      ],
      "props": [
        {
          "name": "isStreaming",
          "type": "boolean",
          "defaultValue": "false",
          "description": "true 면 children 끝에 깜박이는 cursor(axe-stream-cursor, aria-hidden)를 노출. 파싱/버퍼링은 하지 않음."
        },
        {
          "name": "children",
          "type": "React.ReactNode",
          "description": "이미 파싱된 마크다운/노드. Prose 본문으로 그대로 렌더 — 문자열이면 마크다운 파싱 없이 텍스트로 나온다."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLDivElement>",
          "description": "나머지는 Prose 루트 <div> 로 전파. 단 style 을 넘기면 기본 style(maxWidth:100%)을 덮어씀(rest 가 뒤에 전개)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-stream-cursor",
          "purpose": "본문 끝 깜박이는 cursor span(장식, aria-hidden). axe-blink keyframe(1s steps) 로 점멸.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "cursor span 은 aria-hidden 순수 장식 — 보조기술에 읽히지 않는다. StreamingMarkdown 자체는 live region 을 두지 않으므로, 스트리밍 진행 상태의 음성 고지는 상위(AgentExecutor 의 axe-agent-executor__streaming-hint role=\"status\")가 담당한다.",
        "notes": "cursor 점멸(axe-blink)은 reset.css 의 전역 prefers-reduced-motion 블록(*, ::before, ::after 의 animation-duration 강제 축소)에 걸려 모션 축소 환경에서 사실상 멈춘다 — 컴포넌트 개별 가드는 없다."
      },
      "examples": [
        {
          "title": "기본 (스트리밍 중)",
          "lang": "tsx",
          "code": "import { StreamingMarkdown } from \"@axe/ui\";\nimport Markdown from \"react-markdown\";\n\n<StreamingMarkdown isStreaming={isPending}>\n  <Markdown>{partialText}</Markdown>\n</StreamingMarkdown>"
        },
        {
          "title": "완료 상태 (cursor 없음)",
          "lang": "tsx",
          "code": "import { StreamingMarkdown } from \"@axe/ui\";\nimport Markdown from \"react-markdown\";\n\n<StreamingMarkdown isStreaming={false}>\n  <Markdown>{finalText}</Markdown>\n</StreamingMarkdown>"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-prose axe-streaming\">\n  <p>부분 응답 텍스트…</p>\n  <span class=\"axe-stream-cursor\" aria-hidden=\"true\"></span>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "마크다운 파싱은 호출 측(react-markdown 등)에서 하고 그 결과를 children 으로 넘긴다.",
          "isStreaming 은 마지막 assistant 메시지의 진행 여부와 1:1 로 연동한다."
        ],
        "dont": [
          "문자열을 그대로 넘기면 마크다운이 파싱되지 않는다 — 원문 텍스트로 보인다.",
          "진행 고지를 이 컴포넌트에 기대지 말 것 — live region 은 상위(AgentExecutor)가 준다."
        ]
      },
      "consumeCss": "루트에 붙는 axe-streaming 은 스타일 규칙이 없는 마커 클래스이므로, 실제 시각 계약은 Prose 의 axe-prose + cursor 의 axe-stream-cursor 뿐이다. CSS-only 로 재현하려면 axe-prose 래퍼 안에 본문을 넣고 끝에 <span class=\"axe-stream-cursor\">를 붙이면 된다(파싱/스트리밍은 서버 렌더 측 책임).",
      "url": "https://design.axelabs.ai/components/streaming-markdown"
    },
    {
      "name": "Switch",
      "slug": "switch",
      "group": "input",
      "summary": "on/off 를 즉시 전환하는 토글 스위치 (Radix Switch 기반).",
      "description": "즉시 반영되는 이진 설정(알림 켜기/끄기 등)에 쓰는 접근성 토글. Space/Enter 토글·role=switch·aria-checked 는 Radix 가 처리하고, @axe/ui 는 트랙과 썸(thumb) 스타일만 입힌다. 시각 라벨이 필요하면 별도 <Label htmlFor> 로 연결한다. 제출 시점이 따로 있는 참/거짓 값에는 Checkbox 를 검토.",
      "keywords": [
        "switch",
        "스위치",
        "토글",
        "toggle",
        "on off",
        "설정",
        "radix"
      ],
      "props": [
        {
          "name": "checked",
          "type": "boolean",
          "description": "on/off 상태 (controlled). Radix Switch.Root prop."
        },
        {
          "name": "defaultChecked",
          "type": "boolean",
          "description": "uncontrolled 초기 상태."
        },
        {
          "name": "onCheckedChange",
          "type": "(checked: boolean) => void",
          "description": "상태가 바뀔 때 콜백."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "defaultValue": "false",
          "description": "비활성 (opacity 저하 + 커서 차단)."
        },
        {
          "name": "id",
          "type": "string",
          "description": "<Label htmlFor> 매칭용 — click-to-toggle · a11y 연결."
        },
        {
          "name": "name",
          "type": "string",
          "description": "폼 제출 시 필드명 (Radix hidden input)."
        },
        {
          "name": "...rest",
          "type": "React.ComponentPropsWithoutRef<typeof Switch.Root>",
          "description": "나머지는 Radix Switch.Root 로 전파 (required·value 등)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-switch",
          "purpose": "트랙(루트 button) — 폭 36 / 높이 20, checked 시 accent.",
          "stability": "stable"
        },
        {
          "className": "axe-switch__thumb",
          "purpose": "좌우로 이동하는 손잡이(썸).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Space/Enter 로 토글"
        ],
        "aria": "Radix 가 role=switch + aria-checked 를 관리. 시각 라벨은 <Label htmlFor={id}> 로 연결(트랙 자체엔 텍스트가 없음).",
        "notes": "focus-visible 시 --ring. data-state=checked 로 트랙 accent·썸 이동, data-disabled 로 흐림. 부모 .axe-form-field--error 안에서 danger 링/checked 색. prefers-reduced-motion 시 썸 이동 트랜지션 제거."
      },
      "examples": [
        {
          "title": "라벨과 함께",
          "lang": "tsx",
          "code": "import { Switch, Label } from \"@axe/ui\";\n\n<div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n  <Switch id=\"notify\" checked={on} onCheckedChange={setOn} />\n  <Label htmlFor=\"notify\">이메일 알림</Label>\n</div>"
        },
        {
          "title": "CSS-only (비-React) — 켜진 상태",
          "lang": "html",
          "code": "<button class=\"axe-switch\" role=\"switch\" aria-checked=\"true\" data-state=\"checked\">\n  <span class=\"axe-switch__thumb\"></span>\n</button>"
        }
      ],
      "bestPractices": {
        "do": [
          "결과가 즉시 반영되는 설정에 사용(저장 버튼 없이 켜지는 항목).",
          "항상 라벨을 연결 — 무엇을 켜고 끄는지 스크린리더가 읽게."
        ],
        "dont": [
          "'저장' 을 눌러야 확정되는 폼 값에는 쓰지 말 것 — Checkbox 가 적절.",
          "on/off 외 3가지 이상 상태를 담지 말 것."
        ]
      },
      "consumeCss": "CSS-only 로는 시각 상태만 재현된다 — 서버가 data-state='checked'/'disabled' 를 계산해 넣어야 하며(썸 위치·색), 실제 토글 동작은 JS(또는 Radix)가 필요.",
      "playground": {
        "componentKey": "switch",
        "defaults": {
          "checked": true,
          "disabled": false
        }
      },
      "url": "https://design.axelabs.ai/components/switch"
    },
    {
      "name": "Table",
      "slug": "table",
      "group": "content",
      "summary": "시맨틱 <table> 을 감싼 최소 표 — 가로 스크롤 래퍼 + 기본 표 스타일만.",
      "description": "정렬·페이징·상태 dot 같은 로직이 없는 순수 프리미티브 표. 렌더 시 자동으로 `.axe-table-wrap`(overflow-x·테두리·radius) div 로 감싸고 그 안에 `.axe-table` <table> 을 놓는다. thead th / tbody td 는 element 셀렉터로 스타일되므로 소비자는 시맨틱 `<thead>/<tbody>/<th>/<td>` 마크업만 채우면 된다. 상호작용이 필요하면 DataTable, 운영자 상태 행이면 PipelineTable 을 쓴다.",
      "keywords": [
        "table",
        "테이블",
        "표",
        "tabular",
        "thead",
        "tbody",
        "데이터"
      ],
      "props": [
        {
          "name": "children",
          "type": "React.ReactNode",
          "description": "표 내용 — `<thead>`·`<tbody>` 등. 내부 `<table>` 의 자식으로 그대로 렌더."
        },
        {
          "name": "...rest",
          "type": "React.TableHTMLAttributes<HTMLTableElement>",
          "description": "className 은 cn 으로 병합되어 안쪽 `<table>` 에 붙고(래퍼 div 아님), 나머지도 모두 `<table>` 로 전파. forwardRef 도 `<table>` 요소를 가리킨다."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-table-wrap",
          "purpose": "자동 생성되는 바깥 래퍼 div — overflow-x:auto·border·radius-md·elevated 배경. 좁은 뷰포트에서 가로 스크롤을 담당.",
          "stability": "stable"
        },
        {
          "className": "axe-table",
          "purpose": "`<table>` 루트 — width 100%·border-collapse·sm 텍스트·tabular-nums. thead th / tbody td / tbody tr:hover 는 이 클래스 하위 element 셀렉터로 스타일됨(별도 클래스 없음).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "네이티브 `<table>` 시맨틱 그대로 — 컴포넌트가 role/aria 를 추가하지 않는다. 헤더 셀은 소비자가 `<th scope=\"col\">`(또는 `scope=\"row\"`)로 명시해야 스크린리더가 행/열 관계를 읽는다.",
        "notes": "`.axe-table-wrap` 은 가로 스크롤 영역이지만 tabIndex 를 부여하지 않으므로 키보드만으로는 스크롤 포커스를 못 받는다 — 좁은 폭에서 키보드 접근이 중요하면 래퍼에 tabIndex/aria-label 을 직접 보강한다."
      },
      "examples": [
        {
          "title": "기본",
          "lang": "tsx",
          "code": "import { Table } from \"@axe/ui\";\n\n<Table>\n  <thead>\n    <tr>\n      <th scope=\"col\">펀드</th>\n      <th scope=\"col\">약정액</th>\n      <th scope=\"col\">소진율</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td>AXE 1호</td>\n      <td>120억</td>\n      <td>62%</td>\n    </tr>\n    <tr>\n      <td>AXE 2호</td>\n      <td>200억</td>\n      <td>41%</td>\n    </tr>\n  </tbody>\n</Table>"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-table-wrap\">\n  <table class=\"axe-table\">\n    <thead>\n      <tr>\n        <th scope=\"col\">펀드</th>\n        <th scope=\"col\">약정액</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <td>AXE 1호</td>\n        <td>120억</td>\n      </tr>\n    </tbody>\n  </table>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "헤더 셀에 scope 를 명시해 행/열 관계를 SR 에 전달한다.",
          "정렬·페이징·행 클릭이 필요해지면 Table 대신 DataTable 로 승격한다."
        ],
        "dont": [
          "레이아웃 목적으로 쓰지 말 것 — 실제 표 데이터에만.",
          "래퍼 div 를 직접 만들지 말 것 — 컴포넌트가 `.axe-table-wrap` 을 자동으로 붙인다(CSS-only 소비일 때만 손으로 감싼다)."
        ]
      },
      "consumeCss": "완전 CSS-only 소비 가능 — 상태가 없다. `<div class=\"axe-table-wrap\"><table class=\"axe-table\">…` 시맨틱 마크업으로 서버 렌더(maud/jinja)에서 동일 재현. thead th·tbody td 는 element 셀렉터라 셀에 추가 클래스가 필요 없다.",
      "url": "https://design.axelabs.ai/components/table"
    },
    {
      "name": "Tabs",
      "slug": "tabs",
      "group": "container",
      "summary": "접근성 탭 네비게이션(가로/세로) — Radix Tabs 위 .axe-tabs 스타일.",
      "description": "@radix-ui/react-tabs 위에 .axe-tabs__* 스타일을 입힌 얇은 wrapper. orientation=\"horizontal\"(기본) 은 ← → 키 이동 + 활성 트리거 하단 underline, orientation=\"vertical\" 은 ↑ ↓ 키 이동 + 좌측 rail + 활성 트리거 좌측 indicator 로 렌더된다. Tabs/TabsList/TabsTrigger/TabsContent 4-파트로 구성하며 roving tabindex·arrow nav·aria 배선은 전부 Radix 가 처리한다.",
      "keywords": [
        "tabs",
        "탭",
        "tab navigation",
        "radix",
        "horizontal",
        "vertical",
        "segmented"
      ],
      "props": [
        {
          "name": "orientation",
          "type": "\"horizontal\" | \"vertical\"",
          "defaultValue": "\"horizontal\"",
          "description": "Tabs 루트 — 방향. .axe-tabs--{orientation} 및 Radix orientation 에 반영."
        },
        {
          "name": "...rest",
          "type": "React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>",
          "description": "Tabs 루트 — 나머지는 Radix Tabs.Root 로 전파(value, defaultValue, onValueChange, dir, activationMode 등)."
        },
        {
          "name": "TabsTrigger.value",
          "type": "string",
          "required": true,
          "description": "이 트리거가 활성화하는 패널 키. 대응 TabsContent.value 와 매칭."
        },
        {
          "name": "TabsContent.value",
          "type": "string",
          "required": true,
          "description": "이 패널의 키. 대응 TabsTrigger.value 와 매칭."
        },
        {
          "name": "TabsList / TabsTrigger / TabsContent ...rest",
          "type": "React.ComponentPropsWithoutRef<typeof TabsPrimitive.*>",
          "description": "각각 Radix Tabs.List/Trigger/Content 로 전파(disabled, forceMount 등)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-tabs",
          "purpose": "Tabs 루트(Radix Root).",
          "stability": "stable"
        },
        {
          "className": "axe-tabs--horizontal",
          "purpose": "가로 방향 변형(활성 하단 underline).",
          "stability": "stable"
        },
        {
          "className": "axe-tabs--vertical",
          "purpose": "세로 방향 변형(좌측 rail + 활성 좌측 indicator).",
          "stability": "stable"
        },
        {
          "className": "axe-tabs__list",
          "purpose": "트리거 묶음(Radix List, role=tablist).",
          "stability": "stable"
        },
        {
          "className": "axe-tabs__trigger",
          "purpose": "탭 트리거(Radix Trigger, role=tab). 활성 = [data-state=\"active\"], 비활성 disabled = [data-disabled].",
          "stability": "stable"
        },
        {
          "className": "axe-tabs__content",
          "purpose": "탭 패널(Radix Content, role=tabpanel). 비활성 = [data-state=\"inactive\"] 로 숨김.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "← → — horizontal 에서 트리거 이동(roving)",
          "↑ ↓ — vertical 에서 트리거 이동",
          "Home / End — 첫/마지막 트리거",
          "Tab — 활성 트리거에서 패널로 이동"
        ],
        "aria": "role=tablist(List) / tab(Trigger) / tabpanel(Content) 와 aria-selected·aria-controls·aria-labelledby, roving tabindex 를 전부 Radix 가 부여한다. 활성 상태는 [data-state=\"active\"] 로 노출된다(underline/indicator 는 CSS 가 이 속성을 따라감).",
        "notes": "기본 activationMode 는 automatic — 화살표로 트리거에 포커스가 닿으면 즉시 활성/패널 전환된다. Tabs 에 value/onValueChange 를 주면 controlled, defaultValue 만 주면 uncontrolled(둘 다 Radix Root prop)."
      },
      "examples": [
        {
          "title": "가로(기본)",
          "lang": "tsx",
          "code": "import { Tabs, TabsList, TabsTrigger, TabsContent } from \"@axe/ui\";\n\n<Tabs defaultValue=\"overview\">\n  <TabsList>\n    <TabsTrigger value=\"overview\">Overview</TabsTrigger>\n    <TabsTrigger value=\"usage\">Usage</TabsTrigger>\n  </TabsList>\n  <TabsContent value=\"overview\">개요 내용…</TabsContent>\n  <TabsContent value=\"usage\">사용법 내용…</TabsContent>\n</Tabs>"
        },
        {
          "title": "세로(controlled)",
          "lang": "tsx",
          "code": "import { Tabs, TabsList, TabsTrigger, TabsContent } from \"@axe/ui\";\n\n<Tabs orientation=\"vertical\" value={tab} onValueChange={setTab}>\n  <TabsList>\n    <TabsTrigger value=\"general\">일반</TabsTrigger>\n    <TabsTrigger value=\"security\">보안</TabsTrigger>\n  </TabsList>\n  <TabsContent value=\"general\">…</TabsContent>\n  <TabsContent value=\"security\">…</TabsContent>\n</Tabs>"
        },
        {
          "title": "정적 markup (CSS-only 시각)",
          "lang": "html",
          "code": "<div class=\"axe-tabs axe-tabs--horizontal\">\n  <div class=\"axe-tabs__list\" role=\"tablist\">\n    <button class=\"axe-tabs__trigger\" role=\"tab\" data-state=\"active\"\n            aria-selected=\"true\" aria-controls=\"p-1\" id=\"t-1\">Overview</button>\n    <button class=\"axe-tabs__trigger\" role=\"tab\" data-state=\"inactive\"\n            aria-selected=\"false\" aria-controls=\"p-2\" id=\"t-2\">Usage</button>\n  </div>\n  <div class=\"axe-tabs__content\" role=\"tabpanel\" id=\"p-1\"\n       aria-labelledby=\"t-1\" data-state=\"active\">개요 내용…</div>\n  <div class=\"axe-tabs__content\" role=\"tabpanel\" id=\"p-2\"\n       aria-labelledby=\"t-2\" data-state=\"inactive\" hidden>사용법 내용…</div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "각 TabsTrigger.value 를 대응 TabsContent.value 와 정확히 일치시킨다.",
          "탭 라벨은 짧은 명사로 — 긴 문장/문단 라벨은 피한다."
        ],
        "dont": [
          "탭으로 순차적 마법사(wizard) 흐름을 만들지 말 것 — 탭은 동등한 뷰 전환용.",
          "aria-selected/role 을 직접 붙이지 말 것 — Radix 가 부여한다(중복 금지)."
        ]
      },
      "consumeCss": "활성/비활성 전환은 Radix 가 세팅하는 [data-state] 속성에 의존한다 — 순수 CSS-only(무JS) 소비로는 상태 전환을 재현할 수 없고, data-state 를 수동으로 박아 넣어 정적 렌더만 가능(underline/indicator 는 [data-state=\"active\"] 를 따라가고 비활성 패널은 hidden 이 담당).",
      "url": "https://design.axelabs.ai/components/tabs"
    },
    {
      "name": "ThemeToggle",
      "slug": "theme-toggle",
      "group": "utility",
      "summary": "light / system / dark 전환 컨트롤 — useTheme 위의 얇은 UI.",
      "description": "라이트/시스템/다크 테마를 고르는 컨트롤. useTheme 을 통해 storage·DOM 반영을 ThemeProvider 에 위임하므로 항상 동기화된다(design 페이지가 documentElement 를 직접 만지던 버그를 SSOT 로 흡수). mode=\"segment\"(기본) 는 3-버튼 segmented(활성=primary), mode=\"cycle\" 은 단일 버튼이 light → system → dark 를 순환한다. 아이콘은 inline SVG(외부 아이콘 dep 0)로 currentColor 를 따라간다. 내부 버튼은 @axe/ui Button 을 재사용한다.",
      "keywords": [
        "theme toggle",
        "테마",
        "다크 모드",
        "light dark system",
        "useTheme",
        "다크모드 토글",
        "segment",
        "cycle"
      ],
      "props": [
        {
          "name": "mode",
          "type": "\"segment\" | \"cycle\"",
          "defaultValue": "\"segment\"",
          "description": "segmented 3-버튼(light/system/dark) 또는 단일 순환 버튼."
        },
        {
          "name": "size",
          "type": "\"sm\" | \"md\"",
          "defaultValue": "\"sm\"",
          "description": "내부 버튼 크기."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLDivElement>",
          "description": "className 등 나머지는 루트 <div> 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-theme-toggle",
          "purpose": "루트 래퍼 (버튼들을 감싸는 inline-flex 행).",
          "stability": "stable"
        },
        {
          "className": "axe-theme-toggle__icon",
          "purpose": "테마 아이콘 SVG 슬롯 (1em 정사각 박스).",
          "stability": "internal"
        }
      ],
      "a11y": {
        "keyboard": [
          "Enter/Space 로 버튼 실행 (내부는 네이티브 <button> 인 Button)",
          "segment: Tab 으로 3버튼 순회",
          "cycle: Enter/Space 로 다음 테마 순환"
        ],
        "aria": "segment: 래퍼 role=\"group\" + aria-label=\"테마 선택\", 각 버튼에 aria-label(라이트/시스템/다크)과 활성 버튼 aria-pressed=\"true\". cycle: 단일 버튼에 동적 aria-label(\"테마: <현재> (클릭 시 <다음>)\")과 title. 아이콘 SVG 는 aria-hidden 이라 접근명은 aria-label/title 로만 제공.",
        "notes": "useTheme() 에 의존하므로 반드시 ThemeProvider 하위에서 렌더해야 한다. 활성 버튼은 Button variant=\"primary\", 비활성은 variant=\"ghost\". 아이콘은 currentColor 로 테마색을 따른다."
      },
      "examples": [
        {
          "title": "segment (기본, 3-버튼)",
          "lang": "tsx",
          "code": "import { ThemeToggle } from \"@axe/ui\";\n\n// ThemeProvider 하위 어디서든\n<ThemeToggle />"
        },
        {
          "title": "cycle (단일 순환 버튼)",
          "lang": "tsx",
          "code": "import { ThemeToggle } from \"@axe/ui\";\n\n<ThemeToggle mode=\"cycle\" size=\"md\" />"
        }
      ],
      "bestPractices": {
        "do": [
          "앱 셸(TopNav 등)에서 테마 storage/DOM 반영은 ThemeProvider + useTheme 에 맡길 것.",
          "폭이 좁은 헤더/모바일에선 cycle, 넉넉하면 segment 를 쓸 것."
        ],
        "dont": [
          "documentElement 나 localStorage 를 직접 만지지 말 것 — useTheme 을 통할 것(과거 버그의 원인).",
          "ThemeProvider 밖에서 렌더하지 말 것(useTheme 이 동작하지 않음)."
        ]
      },
      "consumeCss": "CSS-only 로는 .axe-theme-toggle + .axe-btn 로 시각 껍데기만 재현 가능하고, 실제 테마 전환(storage·DOM 반영)은 JS(useTheme)가 필요하다 — 순수 마크업만으로는 상태가 바뀌지 않는다.",
      "url": "https://design.axelabs.ai/components/theme-toggle"
    },
    {
      "name": "Timeline",
      "slug": "timeline",
      "group": "data",
      "summary": "날짜로 묶은 활동 피드 — 좌측 dot + 수직 레일, 한국어 상대 날짜 헤더.",
      "description": "items 를 groupBy(day/week/month/커스텀 함수) 로 묶어 최신→과거 순으로 그리는 활동 타임라인. 좌측 dot 과 이를 잇는 수직선은 CSS ::before 로만 그린다. 그룹 헤더는 relativeLabels 가 켜지면 ko-KR 상대 표기('오늘'/'어제'/'N일 전'/'이번 주'/'지난 달')로, 꺼지면 절대 날짜로 나온다. stickyHeaders 로 스크롤 중 날짜 헤더를 상단 고정하고, loadMore/hasMore/loading 으로 무한 스크롤 '더 보기'를 붙인다. item.variant 가 dot 색(success/warning/danger/info)을 정한다.",
      "keywords": [
        "timeline",
        "타임라인",
        "activity",
        "활동",
        "feed",
        "피드",
        "일지",
        "history",
        "이력",
        "relative time",
        "상대 날짜"
      ],
      "props": [
        {
          "name": "items",
          "type": "TimelineItem[]",
          "required": true,
          "description": "타임라인 항목들. `{ id; date: Date | string; title?; content; icon?; variant? }`. date 문자열은 new Date() 로 파싱. content 만 필수, title 없으면 시각만 단독 표시."
        },
        {
          "name": "groupBy",
          "type": "\"day\" | \"week\" | \"month\" | ((d: Date) => string)",
          "defaultValue": "\"day\"",
          "description": "그룹 기준. 함수를 주면 반환 문자열이 그룹 키가 되고 헤더는 절대 날짜로 표기."
        },
        {
          "name": "relativeLabels",
          "type": "boolean",
          "defaultValue": "true",
          "description": "그룹 헤더를 '오늘'/'어제'/'이번 주' 등 ko-KR 상대 표기로. false 면 절대 날짜."
        },
        {
          "name": "stickyHeaders",
          "type": "boolean",
          "defaultValue": "true",
          "description": "스크롤 컨테이너 안에서 날짜 헤더를 상단 고정(position: sticky)."
        },
        {
          "name": "compact",
          "type": "boolean",
          "defaultValue": "false",
          "description": "간격을 좁히고 dot 을 축소한 dense 모드. compact 에선 item.icon 을 그리지 않는다."
        },
        {
          "name": "loadMore",
          "type": "() => void",
          "description": "'더 보기' 버튼 클릭 핸들러. hasMore 와 함께 쓴다."
        },
        {
          "name": "hasMore",
          "type": "boolean",
          "defaultValue": "false",
          "description": "true 면 목록 하단에 '더 보기' 버튼을 렌더."
        },
        {
          "name": "loading",
          "type": "boolean",
          "defaultValue": "false",
          "description": "true 면 '더 보기' 대신 '불러오는 중...'(aria-live) 표시."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLOListElement>, \"onChange\">",
          "description": "className 병합, 나머지는 루트 <ol> 로 전파. ref 는 루트 ol 로 forward."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-timeline",
          "purpose": "루트 <ol> — 세로 그룹 스택.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline--sticky",
          "purpose": "날짜 헤더 sticky-top 활성.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline--compact",
          "purpose": "dense 모드 — 간격/ dot 축소.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__group",
          "purpose": "한 날짜 묶음 <li>(헤더 + 항목 목록).",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__date-header",
          "purpose": "그룹 날짜 헤더 <time>.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__items",
          "purpose": "그룹 안 항목들의 중첩 <ol>.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__item",
          "purpose": "항목 <li> — 레일 + 본문 2열.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__item--success",
          "purpose": "dot success 색.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__item--warning",
          "purpose": "dot warning 색.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__item--danger",
          "purpose": "dot danger 색.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__item--info",
          "purpose": "dot info 색.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__rail",
          "purpose": "좌측 dot+수직선 열(선은 ::before). aria-hidden.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__dot",
          "purpose": "항목 원형 마커(아이콘 담김).",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__body",
          "purpose": "제목/시각/본문을 담는 우측 본문.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__title-row",
          "purpose": "제목 + 시각을 좌우로 놓는 줄.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__title",
          "purpose": "항목 제목 텍스트.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__time",
          "purpose": "항목 시각 <time>(HH:MM).",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__time--solo",
          "purpose": "제목 없을 때 시각 단독 표기.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__content",
          "purpose": "항목 본문 영역.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__loadmore",
          "purpose": "'더 보기'/로딩 줄 <li>.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__loadmore-btn",
          "purpose": "'더 보기' 버튼.",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__loading",
          "purpose": "'불러오는 중...' 텍스트(aria-live).",
          "stability": "stable"
        },
        {
          "className": "axe-timeline__empty",
          "purpose": "항목 0개일 때 '기록이 없습니다' <li>.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "시맨틱 <ol>/<li> 중첩(그룹 ol > li > time + 항목 ol > li)으로 구조를 표현. 그룹 헤더와 항목 시각은 <time datetime> 으로 머신 리더블. 좌측 rail/dot 은 aria-hidden — 장식.",
        "notes": "'더 보기'는 진짜 <button>(loadMore 호출). 로딩 문구는 aria-live=\"polite\" 로 알림. 항목 순서는 항상 날짜 desc(최신 먼저) 로 컴포넌트가 정렬한다. 상대 라벨은 ko-KR 고정(new Date() 기준이라 클라이언트 렌더 필요 — 그래서 \"use client\")."
      },
      "examples": [
        {
          "title": "일 단위 그룹 + 더 보기",
          "lang": "tsx",
          "code": "import { Timeline } from \"@axe/ui\";\n\n<Timeline\n  items={[\n    { id: \"a\", date: new Date(), title: \"회고 작성\", content: \"주간 회고 초안 완료\" },\n    { id: \"b\", date: \"2026-05-20\", title: \"Frame 배포\", content: \"v0.4.2 blue/green 전환\", variant: \"success\" },\n  ]}\n  groupBy=\"day\"\n  stickyHeaders\n  hasMore\n  loadMore={() => fetchMore()}\n/>"
        },
        {
          "title": "CSS-only 정적 스냅샷 (비-React)",
          "lang": "html",
          "code": "<ol class=\"axe-timeline axe-timeline--sticky\">\n  <li class=\"axe-timeline__group\">\n    <time class=\"axe-timeline__date-header\" datetime=\"2026-05-21\">오늘</time>\n    <ol class=\"axe-timeline__items\">\n      <li class=\"axe-timeline__item axe-timeline__item--success\">\n        <span class=\"axe-timeline__rail\" aria-hidden=\"true\">\n          <span class=\"axe-timeline__dot\"></span>\n        </span>\n        <div class=\"axe-timeline__body\">\n          <div class=\"axe-timeline__title-row\">\n            <span class=\"axe-timeline__title\">Frame 배포</span>\n            <time class=\"axe-timeline__time\" datetime=\"2026-05-21T09:14\">09:14</time>\n          </div>\n          <div class=\"axe-timeline__content\">v0.4.2 blue/green 전환</div>\n        </div>\n      </li>\n    </ol>\n  </li>\n</ol>"
        }
      ],
      "bestPractices": {
        "do": [
          "groupBy 는 데이터 밀도에 맞춰 — 하루 수십 건이면 day, 장기 이력이면 month.",
          "stickyHeaders 를 살리려면 스크롤 되는 컨테이너 안에 둘 것.",
          "variant 는 상태가 있는 이벤트(배포 성공/실패 등)에만 — 무의미한 색 남발 금지."
        ],
        "dont": [
          "loadMore 없이 hasMore 만 켜지 말 것(버튼이 아무 것도 안 함).",
          "정적 prerender 에서 상대 라벨을 기대하지 말 것 — 빌드 시각으로 동결된다."
        ]
      },
      "consumeCss": "레일 수직선·dot·sticky 는 순수 CSS 라 서버가 같은 중첩 마크업(ol.axe-timeline > li.__group > time.__date-header + ol.__items > li.__item ...)을 그리면 CSS-only 로 동일 재현된다. 단 그룹핑·정렬·상대 날짜 라벨은 컴포넌트가 계산하는 것이라 서버(maud/jinja)가 직접 산출해 넣어야 하고, '더 보기' 인터랙션은 JS 몫이다.",
      "url": "https://design.axelabs.ai/components/timeline"
    },
    {
      "name": "Toast",
      "slug": "toast",
      "group": "overlay",
      "summary": "화면 모서리에 쌓이는 비동기 알림 — sonner 래퍼 + AXE 토큰/클래스.",
      "description": "Toaster provider 를 루트에 한 번 마운트하고, 어디서든 imperative `toast.*` 로 호출한다. sonner 를 감싸 --bg-elevated 표면·좌측 시맨틱 accent bar·light/dark 자동(theme=\"system\")을 입혔고, 내부 마크업에 .axe-toast/.axe-toast__* 클래스를 classNames prop 으로 주입한다. AI-first 4-state 에 맞춰 loading→success/danger 갱신(같은 id 재사용)과 toast.promise 자동 전이를 지원하며, sonner.error 의 별칭으로 우리 시맨틱인 toast.danger 를 추가로 노출한다.",
      "keywords": [
        "toast",
        "토스트",
        "sonner",
        "notification",
        "알림",
        "snackbar",
        "promise",
        "loading"
      ],
      "props": [
        {
          "name": "position",
          "type": "\"top-left\" | \"top-center\" | \"top-right\" | \"bottom-left\" | \"bottom-center\" | \"bottom-right\"",
          "defaultValue": "\"bottom-right\"",
          "description": "토스트 스택이 뜨는 화면 모서리."
        },
        {
          "name": "theme",
          "type": "\"light\" | \"dark\" | \"system\"",
          "defaultValue": "\"system\"",
          "description": "표면 톤. 기본 system — 표면 색이 CSS 변수 기반이라 실제 시각은 토큰이 결정하지만, sonner 내부 상태 클래스에 이 값이 전달된다."
        },
        {
          "name": "expand",
          "type": "boolean",
          "defaultValue": "false",
          "description": "false 면 겹쳐 쌓고 hover 시에만 펼침, true 면 항상 펼쳐 나열."
        },
        {
          "name": "closeButton",
          "type": "boolean",
          "defaultValue": "true",
          "description": "각 토스트에 닫기(✕) 버튼(.axe-toast__close)을 표시할지."
        },
        {
          "name": "visibleToasts",
          "type": "number",
          "defaultValue": "4",
          "description": "동시에 보이는 최대 개수. 초과분은 뒤에 큐잉."
        },
        {
          "name": "duration",
          "type": "number",
          "defaultValue": "4000",
          "description": "자동 dismiss 까지의 ms. loading 토스트는 갱신/dismiss 전까지 유지된다."
        },
        {
          "name": "className",
          "type": "string",
          "description": "viewport 루트에 .axe-toast-viewport 와 함께 병합. (개별 토스트가 아니라 sonner 컨테이너에 붙는다.)"
        },
        {
          "name": "...rest",
          "type": "Omit<ToasterProps, \"className\">",
          "description": "그 외 sonner Toaster 옵션(offset·gap·richColors·hotkey·containerAriaLabel·toastOptions 등)을 그대로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-toast",
          "purpose": "개별 토스트 루트 — elevated 배경·좌측 3px accent bar·shadow-md.",
          "stability": "stable"
        },
        {
          "className": "axe-toast--success",
          "purpose": "success 토스트 — 좌측 bar = --success.",
          "stability": "stable"
        },
        {
          "className": "axe-toast--info",
          "purpose": "info 토스트 — 좌측 bar = --info.",
          "stability": "stable"
        },
        {
          "className": "axe-toast--warning",
          "purpose": "warning 토스트 — 좌측 bar = --warning.",
          "stability": "stable"
        },
        {
          "className": "axe-toast--danger",
          "purpose": "danger/error 토스트 — 좌측 bar = --danger (sonner error 에 매핑).",
          "stability": "stable"
        },
        {
          "className": "axe-toast--loading",
          "purpose": "loading 토스트 — 좌측 bar = --accent.",
          "stability": "stable"
        },
        {
          "className": "axe-toast__title",
          "purpose": "제목 줄 — semibold·text-primary.",
          "stability": "stable"
        },
        {
          "className": "axe-toast__description",
          "purpose": "보조 설명 — text-secondary·text-xs.",
          "stability": "stable"
        },
        {
          "className": "axe-toast__action",
          "purpose": "액션 버튼 — cta 배경.",
          "stability": "stable"
        },
        {
          "className": "axe-toast__cancel",
          "purpose": "취소 버튼 — 투명 배경·subtle 테두리.",
          "stability": "stable"
        },
        {
          "className": "axe-toast__close",
          "purpose": "닫기(✕) 버튼 — 원형·subtle 배경.",
          "stability": "stable"
        },
        {
          "className": "axe-toast-viewport",
          "purpose": "viewport 컨테이너 마커(className 기본). 실제 톤 오버라이드는 [data-sonner-toaster] 속성 셀렉터가 담당 — 전용 규칙 없음.",
          "stability": "internal"
        }
      ],
      "a11y": {
        "keyboard": [
          "Alt+T(sonner 기본 hotkey)로 토스트 알림 영역에 포커스",
          "각 토스트는 tabIndex 0 — Tab 으로 순회, 액션/닫기 버튼으로 진입"
        ],
        "aria": "sonner 가 알림 영역을 <section aria-live=\"polite\" aria-relevant=\"additions text\" aria-atomic=\"false\" aria-label=\"Notifications Alt+T\"> 로 렌더 — 새 토스트가 polite 로 발화된다(danger/error 도 기본 polite, assertive 아님). 닫기 버튼 aria-label=\"Close toast\".",
        "notes": "모든 애니메이션은 prefers-reduced-motion 을 sonner 가 존중. provider 는 앱당 한 번만 마운트(여러 개면 발화·스택이 갈린다)."
      },
      "examples": [
        {
          "title": "마운트 + 기본 호출",
          "lang": "tsx",
          "code": "// app/layout.tsx — 루트에 한 번\nimport { Toaster } from \"@axe/ui\";\n\n<Toaster position=\"bottom-right\" />\n\n// 어디서든\nimport { toast } from \"@axe/ui\";\n\ntoast.success(\"저장됨\");\ntoast.danger(\"저장 실패 — 네트워크 오류\");"
        },
        {
          "title": "AI-first 4-state (같은 id 갱신 / promise)",
          "lang": "tsx",
          "code": "import { toast } from \"@axe/ui\";\n\n// 수동 전이\nconst id = toast.loading(\"Agent 실행 중…\");\ntry {\n  const r = await runAgent();\n  toast.success(`완료 — ${r.summary}`, { id });\n} catch (e) {\n  toast.danger(humanizeApiError(e), { id });\n}\n\n// 자동 전이\ntoast.promise(runAgent(), {\n  loading: \"Agent 실행 중…\",\n  success: (r) => `완료 — ${r.summary}`,\n  error: (e) => humanizeApiError(e),\n});"
        }
      ],
      "bestPractices": {
        "do": [
          "provider(Toaster)는 앱 루트에 정확히 하나.",
          "진행 중 작업은 loading 으로 시작해 같은 id 로 success/danger 갱신(토스트 난립 방지).",
          "되돌릴 수 없는 실패에만 danger — 정상 완료는 success."
        ],
        "dont": [
          "inline 상태 통지(폼 바로 아래 '저장됨')를 toast 로 처리하지 말 것 — 그건 StatusBanner.",
          "긴 문장을 title 에 몰아넣지 말 것(description 으로 분리)."
        ]
      },
      "consumeCss": "CSS-only 로는 재현 불가 — sonner 런타임이 마크업·스택·자동 dismiss·live region 을 그리고, .axe-toast* 클래스는 classNames prop 으로 그 내부에 주입된다. 스타일(overlays.css)은 components.css 가 @import 하므로 별도 로드는 불요.",
      "url": "https://design.axelabs.ai/components/toast"
    },
    {
      "name": "Toggle",
      "slug": "toggle",
      "group": "action",
      "summary": "하나의 on/off 누름 상태를 갖는 단일 standalone 토글 버튼.",
      "description": "음소거·별표·굵게처럼 **하나의** 눌림(pressed) 상태가 필요한 곳에 쓰는 두-상태 누름 버튼. 네이티브 <button type=\"button\"> 에 aria-pressed 만 얹은 외부 dep 0 구현이며 스타일은 100% CSS 클래스(.axe-toggle)로 처리한다. 묶음이 필요하면 ToggleGroup, 폼 필드 의미의 켜짐/꺼짐이면 Switch, 항상 하나만 선택되는 필수 선택이면 Seg 를 쓴다. controlled 전용 — pressed/onPressedChange 를 항상 함께 넘긴다.",
      "keywords": [
        "toggle",
        "토글",
        "pressed",
        "누름",
        "on off",
        "aria-pressed",
        "mute",
        "standalone"
      ],
      "props": [
        {
          "name": "pressed",
          "type": "boolean",
          "required": true,
          "description": "눌린 상태 (controlled)."
        },
        {
          "name": "onPressedChange",
          "type": "(pressed: boolean) => void",
          "required": true,
          "description": "토글 콜백 — 다음 pressed 값을 인자로 넘김."
        },
        {
          "name": "size",
          "type": "\"sm\" | \"md\"",
          "defaultValue": "\"md\"",
          "description": "버튼 크기."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "defaultValue": "false",
          "description": "비활성화 — 네이티브 disabled 속성."
        },
        {
          "name": "ariaLabel",
          "type": "string",
          "description": "접근성 라벨 — 아이콘-only(가시 텍스트 없음)일 때 필수."
        },
        {
          "name": "...rest",
          "type": "Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onChange' | 'aria-pressed' | 'type'>",
          "description": "className·children·onClick 등 나머지는 루트 <button> 으로 전파. 소비자 onClick 은 내부 onPressedChange 보다 먼저 호출되며 e.preventDefault() 하면 그 클릭의 토글을 억제한다."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-toggle",
          "purpose": "토글 버튼 루트 — 자체 border/bg 를 들고 다니는 standalone 변형.",
          "stability": "stable"
        },
        {
          "className": "axe-toggle--sm",
          "purpose": "sm 크기 변형 (높이 24px).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Enter/Space 로 눌러 pressed 토글 (네이티브 <button>)"
        ],
        "aria": "네이티브 <button type=\"button\"> 에 aria-pressed={pressed}(true/false) 로 눌림 상태 표시 — toggle button 패턴. 아이콘-only 일 땐 ariaLabel 로 접근명을 반드시 제공(없으면 접근명 부재).",
        "notes": "disabled 시 네이티브 disabled 속성 부여 → 포커스·클릭 불가. pressed 는 클래스가 아니라 aria-pressed 속성으로만 표현된다."
      },
      "examples": [
        {
          "title": "기본 (controlled)",
          "lang": "tsx",
          "code": "import { Toggle } from \"@axe/ui\";\nimport { useState } from \"react\";\n\nfunction MuteButton() {\n  const [muted, setMuted] = useState(false);\n  return (\n    <Toggle pressed={muted} onPressedChange={setMuted} ariaLabel=\"음소거\">\n      {muted ? \"🔇\" : \"🔈\"}\n    </Toggle>\n  );\n}"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<button type=\"button\" class=\"axe-toggle\" aria-pressed=\"true\" aria-label=\"별표\">★</button>"
        }
      ],
      "bestPractices": {
        "do": [
          "아이콘-only 토글엔 ariaLabel 을 반드시 넘길 것.",
          "pressed 상태를 상위 state 에 두고 controlled 로 쓸 것."
        ],
        "dont": [
          "묶음(다중/해제 가능한 단일)엔 Toggle 여러 개 대신 ToggleGroup 을 쓸 것.",
          "폼 값(켜짐/꺼짐 저장)엔 Toggle 대신 role=\"switch\" 인 Switch 를 쓸 것."
        ]
      },
      "consumeCss": "pressed 는 aria-pressed=\"true\" 속성으로만 표현되므로 maud/jinja 서버렌더는 같은 markup 에서 이 속성만 토글하면 동일 모양. disabled 는 네이티브 disabled 속성.",
      "playground": {
        "componentKey": "toggle",
        "defaults": {
          "pressed": true,
          "size": "md",
          "disabled": false,
          "children": "★ 별표"
        }
      },
      "url": "https://design.axelabs.ai/components/toggle"
    },
    {
      "name": "ToggleGroup",
      "slug": "toggle-group",
      "group": "action",
      "summary": "눌림 상태 버튼 묶음 — 다중 토글 또는 해제 가능한 단일 선택.",
      "description": "정렬/서식 툴바·필터 칩처럼 \"0개 또는 N개\"가 자연스러운 곳을 위한 버튼 묶음. type=\"multiple\" 은 다중 토글(체크박스 의미, value: T[]), type=\"single\" 은 **해제 가능한** 단일 선택(같은 항목 재클릭 시 null)이다. 항상 하나가 선택되는 단일 필수 선택이면 Seg 를, 낱개 토글이면 Toggle 을 쓴다. 외부 dep 0 — 네이티브 <button aria-pressed> 묶음이며 스타일은 100% CSS 클래스(.axe-toggle-group)로 처리한다. controlled 전용이고 value 는 제네릭 T(string 서브타입)로 좁혀진다.",
      "keywords": [
        "toggle group",
        "토글 그룹",
        "multiple",
        "single",
        "서식 툴바",
        "필터",
        "aria-pressed",
        "segmented"
      ],
      "props": [
        {
          "name": "type",
          "type": "\"multiple\" | \"single\"",
          "required": true,
          "description": "다중 토글(체크박스 의미) 또는 해제 가능한 단일 선택(같은 항목 재클릭 시 null). value/onValueChange 타입을 판별."
        },
        {
          "name": "value",
          "type": "T[] (multiple) | T | null (single)",
          "required": true,
          "description": "눌린 값(들) (controlled). multiple=값 배열, single=값 또는 해제 시 null."
        },
        {
          "name": "onValueChange",
          "type": "(value: T[]) => void (multiple) | (value: T | null) => void (single)",
          "required": true,
          "description": "값 변경 콜백. type 에 대응하는 다음 값을 넘김."
        },
        {
          "name": "options",
          "type": "ToggleGroupOption<T>[]",
          "required": true,
          "description": "버튼 항목 목록. 각 항목 = { value: T; label: React.ReactNode; disabled?: boolean }."
        },
        {
          "name": "ariaLabel",
          "type": "string",
          "required": true,
          "description": "role=\"group\" 컨테이너의 접근성 라벨."
        },
        {
          "name": "size",
          "type": "\"sm\" | \"md\"",
          "defaultValue": "\"md\"",
          "description": "버튼 크기."
        },
        {
          "name": "disabled",
          "type": "boolean",
          "defaultValue": "false",
          "description": "묶음 전체 비활성화 (개별은 option.disabled)."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'>",
          "description": "className 등 나머지는 루트 <div role=\"group\"> 으로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-toggle-group",
          "purpose": "묶음 루트 컨테이너 (role=\"group\", pill 트랙).",
          "stability": "stable"
        },
        {
          "className": "axe-toggle-group__btn",
          "purpose": "묶음 내부 각 토글 버튼.",
          "stability": "stable"
        },
        {
          "className": "axe-toggle-group--sm",
          "purpose": "sm 크기 변형 (버튼 높이 24px).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "Tab 으로 버튼 순회 (roving tabindex 없이 각 버튼이 tab stop)",
          "Enter/Space 로 해당 항목 토글"
        ],
        "aria": "컨테이너 role=\"group\" + aria-label(ariaLabel). 각 버튼은 aria-pressed 로 눌림 표시 — multiple 은 여러 개가 동시에 true, single 은 최대 1개 true(재클릭 시 전부 false).",
        "notes": "개별 비활성 = option.disabled → 해당 <button> 의 disabled 속성. 전체 비활성 = 모든 버튼 disabled(시각은 버튼 :disabled 규칙이 담당). 라디오 의미가 아니므로 role 은 radiogroup 이 아닌 group 이다."
      },
      "examples": [
        {
          "title": "다중 토글 (서식 툴바)",
          "lang": "tsx",
          "code": "import { ToggleGroup } from \"@axe/ui\";\nimport { useState } from \"react\";\n\nfunction FormatToolbar() {\n  const [fmt, setFmt] = useState<string[]>([]);\n  return (\n    <ToggleGroup\n      type=\"multiple\"\n      ariaLabel=\"텍스트 서식\"\n      value={fmt}\n      onValueChange={setFmt}\n      options={[\n        { value: \"bold\", label: \"B\" },\n        { value: \"italic\", label: \"I\" },\n        { value: \"underline\", label: \"U\" },\n      ]}\n    />\n  );\n}"
        },
        {
          "title": "해제 가능한 단일 (정렬)",
          "lang": "tsx",
          "code": "import { ToggleGroup } from \"@axe/ui\";\nimport { useState } from \"react\";\n\nfunction AlignPicker() {\n  const [align, setAlign] = useState<string | null>(null);\n  return (\n    <ToggleGroup\n      type=\"single\"\n      ariaLabel=\"정렬\"\n      value={align}\n      onValueChange={setAlign}\n      options={[\n        { value: \"left\", label: \"왼쪽\" },\n        { value: \"center\", label: \"가운데\" },\n        { value: \"right\", label: \"오른쪽\" },\n      ]}\n    />\n  );\n}"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-toggle-group\" role=\"group\" aria-label=\"텍스트 서식\">\n  <button type=\"button\" class=\"axe-toggle-group__btn\" aria-pressed=\"true\">B</button>\n  <button type=\"button\" class=\"axe-toggle-group__btn\" aria-pressed=\"false\">I</button>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "필터/서식처럼 0~N개가 자연스러운 곳에 쓸 것.",
          "ariaLabel 로 이 묶음이 무엇을 제어하는지 명시할 것."
        ],
        "dont": [
          "항상 하나가 선택돼야 하는 필수 단일 선택엔 ToggleGroup 대신 Seg 를 쓸 것.",
          "낱개 on/off 하나엔 Toggle 을 쓸 것."
        ]
      },
      "consumeCss": "눌림은 각 버튼의 aria-pressed 속성으로 표현 — 서버렌더는 이 속성만 토글하면 동일 모양. 참고: 컴포넌트가 전체 비활성 시 붙이는 axe-toggle-group--disabled 클래스에는 대응 CSS 규칙이 없어(현재 no-op) 시각은 버튼 disabled 속성이 담당한다.",
      "playground": {
        "componentKey": "toggle-group",
        "defaults": {
          "type": "multiple",
          "size": "md",
          "disabled": false
        }
      },
      "url": "https://design.axelabs.ai/components/toggle-group"
    },
    {
      "name": "Tone",
      "slug": "tone",
      "group": "utility",
      "summary": "표면 톤 래퍼 — Signature / Editorial / Console.",
      "description": "subtree 안에서만 디스플레이 페이스 · radius 스케일 · 숫자 표기를 재조합하는 스코프드 레이어다. 구조층(gray ramp · bg/text · 보더 · 상태색 success/warning/danger/info · 대비 AA · 그림자)은 건드리지 않아 다테넌트 대비/의미 보장이 유지되고, 브랜드 리컬러(theme-manifest --brand/accent/cta)와 직교한다. 본문 텍스트는 톤 무관 --font-sans 를 유지하며 디스플레이 자리만 교체한다. signature=브랜드 기본(Pretendard · radius 기본), editorial=문서/격식(display Clash opt-in · crisp radius), console=데이터/터미널(display mono · tight radius · tabular-nums). 각 톤은 tone-controlled 값 전부를 재선언(complete reset)하므로 톤을 중첩해도 안쪽 톤이 바깥 값을 상속하지 않는다. 각각 axelabs.ai·blueprint / gate·layer·IC / index·matrix 표면에 대응한다.",
      "keywords": [
        "tone",
        "톤",
        "surface",
        "표면",
        "editorial",
        "console",
        "signature",
        "theme",
        "preset",
        "utility",
        "밀도",
        "density"
      ],
      "props": [
        {
          "name": "variant",
          "type": "\"signature\" | \"editorial\" | \"console\"",
          "defaultValue": "\"signature\"",
          "description": "표면 톤. `.axe-tone-<variant>` 스코프를 적용한다."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLDivElement>",
          "description": "className 병합 후 나머지 속성은 래퍼 <div> 로 전파. ref 지원(forwardRef)."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-tone-signature",
          "purpose": "브랜드 기본 톤 — Pretendard · radius 기본 (complete reset).",
          "stability": "stable"
        },
        {
          "className": "axe-tone-editorial",
          "purpose": "문서/격식 톤 — display=Clash(opt-in) · crisp radius.",
          "stability": "stable"
        },
        {
          "className": "axe-tone-console",
          "purpose": "데이터/터미널 톤 — display=mono · tight radius · tabular-nums.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "순수 시각 래퍼 — 별도 role/aria 없음. 상호작용·시맨틱 미부여(디스플레이 페이스·radius·숫자 표기만 재조합). 대비·상태색은 톤 무관 불변이라 어느 톤에서도 WCAG 대비가 유지된다. 시맨틱이 필요하면 톤 안/밖의 실제 콘텐츠 요소에 부여한다."
      },
      "examples": [
        {
          "title": "톤별 래핑",
          "lang": "tsx",
          "code": "import { Tone, Card, Badge } from \"@axe/ui\";\n\nexport function Example() {\n  return (\n    <Tone variant=\"editorial\">\n      <Card>\n        <h3>Executive Summary</h3>\n        <Badge variant=\"success\">승인</Badge>\n      </Card>\n    </Tone>\n  );\n}"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-tone-console\">\n  <table class=\"axe-doc__table\">…</table>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "표면 단위로 감싼다 — 대시보드 라우트는 console, 계약/메모 라우트는 editorial.",
          "브랜드 색은 theme-manifest 로 별도 관리 — 톤과 직교하므로 함께 써도 충돌하지 않는다."
        ],
        "dont": [
          "대비/상태 의미를 톤으로 바꾸려 하지 말 것 — 구조층은 불변(그건 별도 디자인 승인).",
          "본문 가독성을 위해 mono/Clash 를 긴 산문 본문에 강제하지 말 것 — 디스플레이 자리 전용."
        ]
      },
      "consumeCss": "CSS-only 로 완전 재현 — `<div class=\"axe-tone-editorial\">` 래퍼만으로 동일. 폰트 opt-in(Clash)은 host 로더에 의존.",
      "url": "https://design.axelabs.ai/components/tone"
    },
    {
      "name": "ToolCallCard",
      "slug": "tool-call-card",
      "group": "chat",
      "summary": "에이전트 도구 호출 하나를 접이식 카드로 — 이름·상태 배지 + 펼치면 input/output.",
      "description": "LLM/agent 가 호출한 tool 한 건을 표시한다. 헤더에 도구명과 상태(대기/실행 중/완료/오류)를 보여주고, 클릭·Enter·Space 로 펼치면 input(호출 인자)과 output(결과)을 mono 폰트 본문에 드러낸다. 상태는 data-status, 펼침은 data-open 속성으로 노출돼 CSS 가 chevron 회전·테두리를 분기한다. 펼침 상태는 내부 useState(defaultOpen 초기값) — 완전한 controlled 모드는 없다. AgentExecutor 가 메시지별 tools 를 이 카드로 렌더한다.",
      "keywords": [
        "tool call",
        "도구 호출",
        "tool",
        "agent",
        "function call",
        "collapsible",
        "status",
        "chat"
      ],
      "props": [
        {
          "name": "name",
          "type": "string",
          "required": true,
          "description": "도구명. 헤더에 chevron 과 함께 표시."
        },
        {
          "name": "status",
          "type": "\"pending\" | \"running\" | \"success\" | \"error\"",
          "defaultValue": "\"success\"",
          "description": "ToolStatus. 각각 대기/실행 중/완료/오류 라벨로 렌더되고 data-status 속성에도 반영."
        },
        {
          "name": "input",
          "type": "React.ReactNode",
          "description": "도구 호출 입력(JSON 문자열 등). 펼침 시 'input' 라벨 아래 표시."
        },
        {
          "name": "output",
          "type": "React.ReactNode",
          "description": "도구 호출 결과. 펼침 시 'output' 라벨 아래 표시."
        },
        {
          "name": "defaultOpen",
          "type": "boolean",
          "defaultValue": "false",
          "description": "초기 펼침 여부(초기값 전용 — 이후는 내부 상태로 토글)."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLDivElement>",
          "description": "나머지는 루트 <div> 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-toolcall",
          "purpose": "루트 <div>. data-open / data-status 속성으로 CSS 가 분기.",
          "stability": "stable"
        },
        {
          "className": "axe-toolcall__header",
          "purpose": "토글 헤더(role=\"button\"). 클릭/Enter/Space 로 펼침.",
          "stability": "stable"
        },
        {
          "className": "axe-toolcall__name",
          "purpose": "도구명 + chevron 을 담는 좌측 그룹(accent 색·semibold).",
          "stability": "stable"
        },
        {
          "className": "axe-toolcall__status",
          "purpose": "우측 상태 라벨 텍스트(대기/실행 중/완료/오류).",
          "stability": "stable"
        },
        {
          "className": "axe-toolcall__body",
          "purpose": "펼침 시 input/output 본문(mono, pre-wrap, overflow-x auto).",
          "stability": "stable"
        },
        {
          "className": "axe-toolcall__chevron",
          "purpose": "펼침 표시 chevron(장식, aria-hidden). [data-open=\"true\"] 에서 90deg 회전.",
          "stability": "internal"
        }
      ],
      "a11y": {
        "keyboard": [
          "Enter / Space — 헤더에서 펼침/접힘(preventDefault 로 스크롤 방지)",
          "Tab — 헤더(tabIndex=0)로 포커스 진입"
        ],
        "aria": "헤더는 네이티브 <button> 이 아니라 <div role=\"button\" tabIndex={0}> 이며 aria-expanded 로 펼침 상태를 고지하고 Enter/Space 를 수동 처리한다. chevron 은 aria-hidden 장식이고, 상태는 시각(data-status 색)과 텍스트(axe-toolcall__status 라벨) 양쪽으로 전달돼 색에만 의존하지 않는다.",
        "notes": "펼침 본문은 open && (input || output) 일 때만 마운트된다 — 둘 다 없으면 헤더만 렌더(펼쳐도 빈 본문이 생기지 않음)."
      },
      "examples": [
        {
          "title": "완료된 도구 호출",
          "lang": "tsx",
          "code": "import { ToolCallCard } from \"@axe/ui\";\n\n<ToolCallCard\n  name=\"search_evidence\"\n  status=\"success\"\n  input={JSON.stringify({ query: \"2024 매출\" }, null, 2)}\n  output={JSON.stringify({ hits: 3 }, null, 2)}\n/>"
        },
        {
          "title": "실행 중 · 처음부터 펼침",
          "lang": "tsx",
          "code": "import { ToolCallCard } from \"@axe/ui\";\n\n<ToolCallCard\n  name=\"query_trial_balance\"\n  status=\"running\"\n  defaultOpen\n  input={`{ \"period\": \"2024-Q4\" }`}\n/>"
        },
        {
          "title": "CSS-only 정적 스냅샷 (비-React)",
          "lang": "html",
          "code": "<div class=\"axe-toolcall\" data-open=\"true\" data-status=\"success\">\n  <div class=\"axe-toolcall__header\" role=\"button\" tabindex=\"0\" aria-expanded=\"true\">\n    <span class=\"axe-toolcall__name\">\n      <span class=\"axe-toolcall__chevron\" aria-hidden=\"true\">›</span>\n      search_evidence\n    </span>\n    <span class=\"axe-toolcall__status\">완료</span>\n  </div>\n  <div class=\"axe-toolcall__body\">{ \"hits\": 3 }</div>\n</div>"
        }
      ],
      "bestPractices": {
        "do": [
          "input/output 은 미리 포맷(JSON.stringify(…, null, 2))해서 넘긴다 — 본문이 pre-wrap 이라 개행이 유지된다.",
          "status 를 실제 실행 단계와 연동해 색·라벨이 진실을 말하게 한다."
        ],
        "dont": [
          "완전한 controlled 펼침을 기대하지 말 것 — defaultOpen 은 초기값일 뿐 이후는 내부 상태.",
          "상태를 색으로만 구분하지 말 것 — 라벨 텍스트가 함께 나오도록 status 를 정확히 준다."
        ]
      },
      "consumeCss": "펼침 상태는 React state 라 CSS-only 로는 토글되지 않는다 — 정적 렌더(maud/jinja)에서는 루트에 data-open=\"true|false\" 와 data-status 를 직접 찍고, 펼친 스냅샷이면 axe-toolcall__body 를 함께 마크업한다. chevron 회전·테두리는 이 data 속성만 있으면 CSS 가 그린다.",
      "url": "https://design.axelabs.ai/components/tool-call-card"
    },
    {
      "name": "Tooltip",
      "slug": "tooltip",
      "group": "overlay",
      "summary": "hover/focus 로 뜨는 가벼운 힌트 — 인터랙티브 컨텐츠는 담지 않는다 (Radix Tooltip 기반).",
      "description": "단일 라인~2·3줄 짧은 설명용. 반전 색(어두운 bg·밝은 텍스트) 표면에 pointer-events:none 이라 마우스로 잡을 수 없다(그 자리는 Popover/HoverCard). 키보드 focus 로도 등장해 접근성을 지키고, 기본 portal 이라 overflow:hidden 부모에 갇히지 않는다. TooltipProvider(앱 루트 1회, delay 글로벌 공유)·Tooltip(Root)·TooltipTrigger·TooltipContent 를 조합한다.",
      "keywords": [
        "tooltip",
        "툴팁",
        "hint",
        "힌트",
        "설명",
        "hover",
        "radix"
      ],
      "props": [
        {
          "name": "delayDuration",
          "type": "number",
          "defaultValue": "200",
          "description": "TooltipProvider — hover 후 등장까지 지연(ms). 개별 Tooltip(Root) 에서 override 가능."
        },
        {
          "name": "skipDelayDuration",
          "type": "number",
          "defaultValue": "200",
          "description": "TooltipProvider — 한 툴팁을 닫은 뒤 이 시간 내 다른 트리거로 이동하면 지연 없이 즉시 등장."
        },
        {
          "name": "open",
          "type": "boolean",
          "description": "열림 상태 (controlled). Tooltip(Root) prop."
        },
        {
          "name": "onOpenChange",
          "type": "(open: boolean) => void",
          "description": "열림/닫힘 콜백 (Root)."
        },
        {
          "name": "sideOffset",
          "type": "number",
          "defaultValue": "6",
          "description": "TooltipContent 와 트리거 사이 간격(px)."
        },
        {
          "name": "withPortal",
          "type": "boolean",
          "defaultValue": "true",
          "description": "TooltipContent 를 portal 로 띄울지 — overflow:hidden 부모 탈출."
        },
        {
          "name": "withArrow",
          "type": "boolean",
          "defaultValue": "true",
          "description": "트리거를 가리키는 화살표 표시 여부."
        },
        {
          "name": "...rest",
          "type": "각 Radix Tooltip.* props",
          "description": "Provider(disableHoverableContent)·Trigger(asChild)·Content(side/align/collisionPadding) 등 각 파트는 대응 Radix 컴포넌트 props 를 그대로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-tooltip__content",
          "purpose": "툴팁 표면(반전 색·pointer-events:none·small radius).",
          "stability": "stable"
        },
        {
          "className": "axe-tooltip__arrow",
          "purpose": "트리거를 가리키는 화살표(Radix Arrow).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "트리거로 Tab 이동(키보드 focus)만으로도 툴팁 등장",
          "Esc 로 닫기"
        ],
        "aria": "Radix 가 트리거에 aria-describedby 로 툴팁 컨텐츠를 연결하고 컨텐츠에 role=tooltip 을 부여. hover 와 키보드 focus 두 경로로 등장한다. content 는 pointer-events:none 이라 마우스로 잡을 수 없다.",
        "notes": "TooltipProvider 로 delay 를 글로벌 공유한다. 첫 툴팁은 짧게 진입하고, 연속 툴팁은 skipDelayDuration 안에서 delay와 재등장 애니메이션을 모두 생략해 즉시 전환한다. 네이티브 disabled 버튼은 이벤트를 안 내보내 툴팁이 안 뜨니, 필요하면 트리거를 감싸는 span 등에 씌운다. prefers-reduced-motion 시 애니메이션 제거."
      },
      "examples": [
        {
          "title": "기본 (Provider + Tooltip)",
          "lang": "tsx",
          "code": "import {\n  TooltipProvider,\n  Tooltip,\n  TooltipTrigger,\n  TooltipContent,\n} from \"@axe/ui\";\nimport { Button } from \"@axe/ui\";\n\n// Provider 는 앱 루트에 1회 두는 것을 권장\n<TooltipProvider>\n  <Tooltip>\n    <TooltipTrigger asChild>\n      <Button variant=\"ghost\">?</Button>\n    </TooltipTrigger>\n    <TooltipContent side=\"top\">이 값은 자동 계산됩니다</TooltipContent>\n  </Tooltip>\n</TooltipProvider>"
        },
        {
          "title": "아이콘 버튼 (aria-label 병행) · 화살표 없음",
          "lang": "tsx",
          "code": "import {\n  Tooltip,\n  TooltipTrigger,\n  TooltipContent,\n} from \"@axe/ui\";\nimport { Button } from \"@axe/ui\";\n\n<Tooltip>\n  <TooltipTrigger asChild>\n    <Button aria-label=\"삭제\" variant=\"ghost\">\n      <TrashIcon />\n    </Button>\n  </TooltipTrigger>\n  <TooltipContent side=\"bottom\" withArrow={false}>삭제</TooltipContent>\n</Tooltip>"
        }
      ],
      "bestPractices": {
        "do": [
          "짧은 힌트만 — 한 줄~2·3줄. 긴 설명은 본문/Popover 로.",
          "아이콘 전용 버튼엔 aria-label 도 함께(툴팁은 hover 전용이라 스크린리더 이름을 대체하지 않음).",
          "TooltipProvider 를 앱 루트에 1회 두어 delay 를 일관되게."
        ],
        "dont": [
          "링크·버튼 등 인터랙티브 컨텐츠를 넣지 말 것 — content 는 pointer-events:none 이다.",
          "필수 정보를 툴팁에만 두지 말 것 — 터치 환경에서 뜨지 않는다."
        ]
      },
      "consumeCss": "hover/focus 등장·delay·위치 계산이 Radix 런타임(data-state)에 의존해 CSS-only 로는 재현할 수 없다. .axe-tooltip__* 토큰은 정적 표면에 노출되나 실제 소비는 React 컴포넌트를 통해서 한다.",
      "url": "https://design.axelabs.ai/components/tooltip"
    },
    {
      "name": "TopNav",
      "slug": "top-nav",
      "group": "navigation",
      "summary": "상단 네비게이션 바 — 데스크톱 inline · 모바일 2-드로어.",
      "description": "logo·items·right 세 슬롯을 받는 상단 바. 데스크톱(≥768px)은 세 슬롯을 가로 inline 으로 배치하고, 모바일(≤767.98px)은 같은 요소를 재배치해 [햄버거] nav items 좌 드로어 · [kebab] right 슬롯 우 드로어로 만든다(한 번에 하나만 열림). 본체는 server-compatible 하고 드로어 상호작용(focus trap·scroll lock·Esc)만 client 로 분리한다. items 를 두 번 렌더하지 않고 단일 요소를 CSS 로 재배치하는 것이 핵심 설계다.",
      "keywords": [
        "topnav",
        "top nav",
        "navbar",
        "상단바",
        "네비게이션",
        "header",
        "헤더",
        "네비"
      ],
      "props": [
        {
          "name": "logo",
          "type": "React.ReactNode",
          "description": "좌측 로고/워드마크 슬롯. .axe-topnav__left 안에 렌더."
        },
        {
          "name": "items",
          "type": "NavItem[]",
          "defaultValue": "[]",
          "description": "중앙 네비 항목. NavItem = { href: string; label: string; active?: boolean }. 데스크톱 inline, 모바일 좌 드로어로 표시."
        },
        {
          "name": "right",
          "type": "React.ReactNode",
          "description": "우측 슬롯 (ThemeToggle · NavMenu 등). 모바일에선 kebab(⋯) 우 드로어로."
        },
        {
          "name": "mobileLabels",
          "type": "Partial<TopNavMobileLabels>",
          "description": "모바일 양 drawer의 dialog/open/close accessible label 6개를 locale에 맞게 교체. 미지정 값은 English default."
        },
        {
          "name": "isActive",
          "type": "(href: string) => boolean",
          "description": "항목 active 판정 콜백. items[].active 가 우선하며, 없을 때만 이 함수로 resolve. server 단계에서 resolve 되어 client 로는 plain data 만 전달."
        },
        {
          "name": "mobileDrawer",
          "type": "boolean",
          "description": "@deprecated v0.6 부터 no-op. 모바일 2-드로어가 새 기본이라 더 분기하지 않는다. 호출부 호환용으로 시그니처만 유지, v0.7 제거 예정."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLElement>",
          "description": "나머지는 루트 <nav> 로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-topnav",
          "purpose": "nav 루트 바 (높이 --axe-topnav-height 에 고정).",
          "stability": "stable"
        },
        {
          "className": "axe-topnav__left",
          "purpose": "로고/브랜드 좌측 슬롯.",
          "stability": "stable"
        },
        {
          "className": "axe-topnav__items",
          "purpose": "중앙 항목 컨테이너 — 데스크톱 inline(가로 fade 스크롤) / 모바일 좌 드로어.",
          "stability": "stable"
        },
        {
          "className": "axe-topnav__item",
          "purpose": "개별 nav 링크.",
          "stability": "stable"
        },
        {
          "className": "axe-topnav__item--active",
          "purpose": "활성 항목 (aria-current=\"page\" 와 동등 스타일).",
          "stability": "stable"
        },
        {
          "className": "axe-topnav__right",
          "purpose": "우측 슬롯 — 데스크톱 inline / 모바일 우 드로어. 한 번만 렌더.",
          "stability": "stable"
        },
        {
          "className": "axe-topnav__brand",
          "purpose": "로고 슬롯에 텍스트 워드마크를 쓸 때 붙이는 브랜드 식자 클래스 (Logo 컴포넌트 대체 가능).",
          "stability": "stable"
        },
        {
          "className": "axe-topnav__hamburger",
          "purpose": "모바일 좌 드로어 토글 버튼 (바 좌측, CSS 로 모바일만 표시). JS 구동.",
          "stability": "internal"
        },
        {
          "className": "axe-topnav__kebab",
          "purpose": "모바일 우 드로어 토글 버튼 (바 우측 36px plain 아이콘). JS 구동.",
          "stability": "internal"
        },
        {
          "className": "axe-topnav__drawer-close",
          "purpose": "modal drawer 내부의 명시적 닫기 버튼. 키보드·터치·스위치/SR 사용자가 Esc 없이 닫을 수 있음.",
          "stability": "internal"
        },
        {
          "className": "axe-mobile-scrim",
          "purpose": "드로어 열림 시 반투명 scrim (공용 모바일 드로어 클래스). 탭 시 닫힘.",
          "stability": "internal"
        }
      ],
      "a11y": {
        "keyboard": [
          "데스크톱: 표준 링크 Tab 순회",
          "모바일 드로어 열림 시 Esc 로 닫기",
          "드로어 열림 중 Tab 이 패널 안에서 순환 (focus trap)",
          "열릴 때 첫 focusable 로 focus, 닫힐 때 토글 버튼으로 focus 복원",
          "각 drawer 내부의 명시적 닫기 버튼으로 Esc 없이 닫기"
        ],
        "aria": "루트 <nav aria-label=\"Main navigation\">. 활성 항목 = aria-current=\"page\". 햄버거/kebab 버튼 = aria-expanded + aria-controls(드로어 id) + 내용 기반 동적 aria-label. 열린 패널 = role=dialog + aria-modal이며 배경 sibling은 inert.",
        "notes": "드로어 열림 중 body scroll lock. 토글과 패널의 키보드 focus ring을 보존한다. scrim 탭 또는 항목 클릭 시 닫히며 desktop breakpoint로 커지면 modal/inert 상태를 해제한다."
      },
      "examples": [
        {
          "title": "기본 (logo · items · right)",
          "lang": "tsx",
          "code": "import { TopNav, ThemeToggle } from \"@axe/ui\";\n\n<TopNav\n  logo={<a className=\"axe-topnav__brand\" href=\"/\">AXE Labs</a>}\n  items={[\n    { href: \"/overview\", label: \"개요\", active: true },\n    { href: \"/tokens\", label: \"Tokens\" },\n  ]}\n  right={<ThemeToggle />}\n  mobileLabels={{\n    right: \"테마 도구\",\n    openRight: \"테마 도구 열기\",\n    closeRight: \"테마 도구 닫기\",\n  }}\n/>"
        },
        {
          "title": "CSS-only 데스크톱 바 (비-React)",
          "lang": "html",
          "code": "<nav class=\"axe-topnav\" aria-label=\"Main navigation\">\n  <div class=\"axe-topnav__left\"><a class=\"axe-topnav__brand\" href=\"/\">AXE Labs</a></div>\n  <div class=\"axe-topnav__items\">\n    <a class=\"axe-topnav__item\" aria-current=\"page\" href=\"/overview\">개요</a>\n    <a class=\"axe-topnav__item\" href=\"/tokens\">Tokens</a>\n  </div>\n  <div class=\"axe-topnav__right\"><!-- ThemeToggle 등 --></div>\n</nav>"
        }
      ],
      "bestPractices": {
        "do": [
          "logo 슬롯엔 홈 링크를 둘 것.",
          "active 는 items[].active 또는 isActive 중 하나로 일관되게 지정.",
          "right 슬롯은 항목이 아닌 계정/테마 등 보조 액션에 사용."
        ],
        "dont": [
          "mobileDrawer prop 에 의존하지 말 것 (no-op, v0.7 제거 예정).",
          "items 를 밖에서 별도로 다시 렌더하지 말 것 — 모바일 드로어가 같은 요소를 재배치한다."
        ]
      },
      "consumeCss": "데스크톱 바는 순수 마크업(<nav class=\"axe-topnav\">…)으로 재현 가능. 모바일 2-드로어(햄버거·kebab·scrim·focus trap·scroll lock)는 컴포넌트의 JS 상태(data-open)에 의존하므로 CSS-only 로는 데스크톱 inline 레이아웃만 재현된다.",
      "url": "https://design.axelabs.ai/components/top-nav"
    },
    {
      "name": "Tree",
      "slug": "tree",
      "group": "data",
      "summary": "재귀 collapsible 트리 — 키보드 nav·단일 선택·텍스트 필터 내장(Radix 무의존).",
      "description": "중첩 nodes 를 role=\"tree\"/treeitem 시맨틱으로 직접 그리는 트리 뷰. 펼침은 controlled(expanded/onExpandedChange) 또는 uncontrolled(defaultExpanded) 로, 선택은 단일 controlled(selected/onSelectedChange) 로 관리한다. filter 문자열(대소문자 무시)을 주면 매칭 노드 + 모든 조상 + 매칭 노드의 모든 자손이 가시화되고 조상 chain 은 자동 펼쳐진다. ↑↓←→/Home/End/Enter 전 키보드 상호작용과 roving tabindex 를 자체 구현. renderLabel 로 라벨 영역을 커스터마이즈할 수 있다.",
      "keywords": [
        "tree",
        "treeview",
        "트리",
        "폴더",
        "folder",
        "hierarchy",
        "계층",
        "nav",
        "filter",
        "필터",
        "collapsible"
      ],
      "props": [
        {
          "name": "nodes",
          "type": "TreeNode[]",
          "required": true,
          "description": "트리 데이터. `{ id; label: ReactNode; icon?; children?: TreeNode[]; data?: unknown; disabled? }`. children 이 있으면 chevron 이 붙는다."
        },
        {
          "name": "defaultExpanded",
          "type": "string[]",
          "description": "uncontrolled 초기 펼침 id 목록."
        },
        {
          "name": "expanded",
          "type": "string[]",
          "description": "controlled 펼침 id 목록. 주면 내부 상태 대신 이 값을 쓴다."
        },
        {
          "name": "onExpandedChange",
          "type": "(ids: string[]) => void",
          "description": "펼침 변경 콜백(chevron 클릭·←/→ 키)."
        },
        {
          "name": "selected",
          "type": "string",
          "description": "controlled 단일 선택 id. 매칭 노드에 aria-selected + __item--selected."
        },
        {
          "name": "onSelectedChange",
          "type": "(id: string) => void",
          "description": "선택 변경 콜백(행 클릭·Enter/Space). disabled 노드는 발화하지 않음."
        },
        {
          "name": "renderLabel",
          "type": "(node: TreeNode, state: { isExpanded: boolean; isSelected: boolean; level: number }) => React.ReactNode",
          "description": "라벨 영역 커스텀 렌더. 미지정 시 node.label 을 그대로."
        },
        {
          "name": "indent",
          "type": "number",
          "defaultValue": "16",
          "description": "depth 당 들여쓰기 px(row 의 inline paddingLeft = (level-1)*indent)."
        },
        {
          "name": "filter",
          "type": "string",
          "defaultValue": "\"\"",
          "description": "텍스트 필터(대소문자 무시). label 이 문자열인 노드만 매칭 대상."
        },
        {
          "name": "ariaLabel",
          "type": "string",
          "defaultValue": "\"Tree\"",
          "description": "루트 <ul role=\"tree\"> 의 aria-label."
        },
        {
          "name": "...rest",
          "type": "Omit<React.HTMLAttributes<HTMLUListElement>, \"onChange\">",
          "description": "className 병합, 나머지는 루트 <ul> 로 전파. ref 는 루트 ul 로 forward."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-tree",
          "purpose": "루트 <ul role=\"tree\">.",
          "stability": "stable"
        },
        {
          "className": "axe-tree__item",
          "purpose": "<li role=\"treeitem\"> — 한 노드.",
          "stability": "stable"
        },
        {
          "className": "axe-tree__item--selected",
          "purpose": "선택된 노드(배경/텍스트 강조).",
          "stability": "stable"
        },
        {
          "className": "axe-tree__item--disabled",
          "purpose": "비활성 노드(흐림·클릭 무시).",
          "stability": "stable"
        },
        {
          "className": "axe-tree__row",
          "purpose": "클릭 영역 — chevron+icon+label 을 담고 inline paddingLeft 로 들여쓰기.",
          "stability": "stable"
        },
        {
          "className": "axe-tree__chevron",
          "purpose": "펼침 토글 <button>(tabIndex -1).",
          "stability": "stable"
        },
        {
          "className": "axe-tree__chevron--leaf",
          "purpose": "자식 없는 노드의 chevron 자리 정렬용 빈 spacer.",
          "stability": "stable"
        },
        {
          "className": "axe-tree__icon",
          "purpose": "라벨 앞 아이콘 슬롯(aria-hidden).",
          "stability": "stable"
        },
        {
          "className": "axe-tree__label",
          "purpose": "노드 라벨 텍스트 영역.",
          "stability": "stable"
        },
        {
          "className": "axe-tree__group",
          "purpose": "자식들의 중첩 <ul role=\"group\">.",
          "stability": "stable"
        }
      ],
      "a11y": {
        "keyboard": [
          "↑/↓ — 가시 노드 사이 이동",
          "← — 펼쳐진 노드면 접고, 아니면 부모로 이동",
          "→ — 자식 있고 닫혔으면 펼치고, 열렸으면 첫 자식으로",
          "Enter/Space — 선택(선택 콜백 발화)",
          "Home/End — 처음/마지막 가시 노드"
        ],
        "aria": "루트 <ul role=\"tree\" aria-label>, 각 노드 <li role=\"treeitem\"> 에 aria-level, 자식 있으면 aria-expanded, 선택 시 aria-selected, 비활성 시 aria-disabled. 자식 묶음은 <ul role=\"group\">. chevron 버튼은 aria-label \"접기\"/\"펼치기\".",
        "notes": "roving tabindex — 활성 treeitem 만 tabIndex 0, 나머지는 -1. 활성 노드가 정해지기 전엔 root 가 tab 진입점이며 focus 시 첫 treeitem 으로 위임. activeId 변경 시 트리 내부에 focus 가 있을 때만 해당 항목으로 focus 이동(외부 클릭 도난 방지). filter 활성 시 매칭 조상 chain 은 강제 펼쳐진다(effective expanded)."
      },
      "examples": [
        {
          "title": "선택 + 필터 (controlled)",
          "lang": "tsx",
          "code": "import { Tree } from \"@axe/ui\";\nimport { useState } from \"react\";\n\nfunction FileTree() {\n  const [selected, setSelected] = useState<string>();\n  const [query, setQuery] = useState(\"\");\n  return (\n    <Tree\n      nodes={[\n        {\n          id: \"a\",\n          label: \"프로젝트\",\n          children: [\n            { id: \"a1\", label: \"frame\" },\n            { id: \"a2\", label: \"hive\" },\n          ],\n        },\n      ]}\n      defaultExpanded={[\"a\"]}\n      selected={selected}\n      onSelectedChange={setSelected}\n      filter={query}\n      ariaLabel=\"파일 트리\"\n    />\n  );\n}"
        },
        {
          "title": "CSS-only 정적 스냅샷 (비-React)",
          "lang": "html",
          "code": "<ul class=\"axe-tree\" role=\"tree\" aria-label=\"파일 트리\">\n  <li class=\"axe-tree__item\" role=\"treeitem\" aria-level=\"1\" aria-expanded=\"true\" tabindex=\"0\">\n    <div class=\"axe-tree__row\">\n      <button type=\"button\" class=\"axe-tree__chevron\" tabindex=\"-1\" aria-label=\"접기\">▾</button>\n      <span class=\"axe-tree__label\">프로젝트</span>\n    </div>\n    <ul class=\"axe-tree__group\" role=\"group\">\n      <li class=\"axe-tree__item axe-tree__item--selected\" role=\"treeitem\" aria-level=\"2\" aria-selected=\"true\" tabindex=\"-1\">\n        <div class=\"axe-tree__row\" style=\"padding-left: 16px\">\n          <span class=\"axe-tree__chevron axe-tree__chevron--leaf\" aria-hidden=\"true\"></span>\n          <span class=\"axe-tree__label\">frame</span>\n        </div>\n      </li>\n    </ul>\n  </li>\n</ul>"
        }
      ],
      "bestPractices": {
        "do": [
          "필터 대상은 label 이 문자열인 노드뿐 — 검색 가능한 라벨은 문자열로 둘 것.",
          "controlled 펼침을 쓸 땐 expanded 와 onExpandedChange 를 함께.",
          "선택 상태는 부모에서 소유(selected/onSelectedChange) — 단일 선택 모델."
        ],
        "dont": [
          "다중 선택을 기대하지 말 것 — selected 는 단일 id.",
          "disabled 노드에 선택 로직을 걸지 말 것(콜백이 발화하지 않음)."
        ]
      },
      "consumeCss": "스타일(.axe-tree*)은 순수 CSS 라 서버가 role/aria 를 갖춘 정적 트리 마크업을 그리면 시각은 그대로 재현된다. 단 들여쓰기는 row 의 inline paddingLeft(=(level-1)*indent)로 들어가므로 CSS-only 소비자는 depth 별 padding-left 를 직접 넣어야 하고, 펼침/접힘·키보드 nav·필터 자동펼침은 React 컴포넌트(또는 동등 JS) 없이는 동작하지 않는다 — html 예제는 한 상태의 스냅샷일 뿐.",
      "url": "https://design.axelabs.ai/components/tree"
    },
    {
      "name": "TypingIndicator",
      "slug": "typing-indicator",
      "group": "chat",
      "summary": "AI 응답 생성 중을 나타내는 3-dot wave 애니메이션 인디케이터.",
      "description": "LLM 이 다음 토큰을 생성하기 직전 \"사고 중\" 단계를 표현하는 점 깜박이(wave)다. generic 로딩을 뜻하는 Spinner 와 달리 채팅 흐름의 \"타이핑 중\" 정서를 전담하며, AgentExecutor 의 스트리밍 cursor 와 보완 관계(cursor 는 텍스트 스트리밍 중, TypingIndicator 는 텍스트 시작 전)에 있다. MessageBubble assistant 안의 children 으로 넣거나 메시지 목록 마지막 자리에 단독으로 띄운다. role=\"status\" + aria-live=\"polite\" 로 스크린리더가 label 을 발화하고, prefers-reduced-motion 에서는 애니메이션이 멈춘 정적 점으로 낮춰진다.",
      "keywords": [
        "typing indicator",
        "타이핑",
        "생각 중",
        "dots",
        "점 애니메이션",
        "streaming",
        "loading",
        "ai"
      ],
      "props": [
        {
          "name": "label",
          "type": "string",
          "defaultValue": "\"AI 생각 중\"",
          "description": "aria-label 겸 sr-only 텍스트 — 스크린리더가 읽는 상태 문구."
        },
        {
          "name": "size",
          "type": "\"sm\" | \"md\" | \"lg\"",
          "defaultValue": "\"md\"",
          "description": "점 크기·간격 프리셋(sm 5px / md 7px / lg 10px)."
        },
        {
          "name": "dot",
          "type": "3 | 4 | 5",
          "defaultValue": "3",
          "description": "렌더할 점 개수."
        },
        {
          "name": "...rest",
          "type": "React.HTMLAttributes<HTMLSpanElement>",
          "description": "className 은 루트와 병합, 나머지는 루트 <span> 으로 전파."
        }
      ],
      "cssClasses": [
        {
          "className": "axe-typing-indicator",
          "purpose": "루트 <span>(inline-flex, 점들을 가로 정렬).",
          "stability": "stable"
        },
        {
          "className": "axe-typing-indicator--sm",
          "purpose": "sm 크기 변형(--axe-typing-dot: 5px, 좁은 간격/패딩).",
          "stability": "stable"
        },
        {
          "className": "axe-typing-indicator--md",
          "purpose": "md 크기 변형(--axe-typing-dot: 7px, 기본).",
          "stability": "stable"
        },
        {
          "className": "axe-typing-indicator--lg",
          "purpose": "lg 크기 변형(--axe-typing-dot: 10px, 넓은 간격/패딩).",
          "stability": "stable"
        },
        {
          "className": "axe-typing-indicator__dot",
          "purpose": "각 점 — axe-typing-wave 1.2s 무한 애니메이션(개별 animation-delay 는 인라인 style 로 부여).",
          "stability": "stable"
        }
      ],
      "a11y": {
        "aria": "role=\"status\" + aria-live=\"polite\" + aria-label(label). 점들은 각각 aria-hidden 이고, 안에 별도 sr-only <span> 이 label 을 담아 시각 점과 무관하게 스크린리더가 상태를 발화한다.",
        "notes": "sr-only 텍스트는 공유 유틸 클래스 .axe-sr-only(indicators.css)로 시각적으로 숨긴다 — TypingIndicator 가 소유한 클래스는 아니다. prefers-reduced-motion: reduce 에서는 점 애니메이션이 none 으로 꺼지고 opacity 0.5 정적 상태가 된다. 키보드 상호작용 없음."
      },
      "examples": [
        {
          "title": "기본 — 메시지 말미에 단독",
          "lang": "tsx",
          "code": "import { TypingIndicator } from \"@axe/ui\";\n\n<TypingIndicator />"
        },
        {
          "title": "MessageBubble 안에서 · 크기·점 개수 조정",
          "lang": "tsx",
          "code": "import { MessageBubble, TypingIndicator } from \"@axe/ui\";\n\n<MessageBubble role=\"assistant\">\n  <TypingIndicator size=\"lg\" dot={4} label=\"답변 작성 중\" />\n</MessageBubble>"
        },
        {
          "title": "CSS-only (비-React)",
          "lang": "html",
          "code": "<span class=\"axe-typing-indicator axe-typing-indicator--md\"\n      role=\"status\" aria-live=\"polite\" aria-label=\"AI 생각 중\">\n  <span class=\"axe-typing-indicator__dot\" style=\"animation-delay:0ms\" aria-hidden=\"true\"></span>\n  <span class=\"axe-typing-indicator__dot\" style=\"animation-delay:150ms\" aria-hidden=\"true\"></span>\n  <span class=\"axe-typing-indicator__dot\" style=\"animation-delay:300ms\" aria-hidden=\"true\"></span>\n  <span class=\"axe-sr-only\">AI 생각 중</span>\n</span>"
        }
      ],
      "bestPractices": {
        "do": [
          "네트워크·tool 실행 등 generic 로딩에는 Spinner 를, 텍스트 스트리밍 중 커서에는 StreamingMarkdown 을 쓰고, 이건 응답 생성 직전 단계에만 쓸 것.",
          "label 을 상황에 맞게 바꿔 스크린리더에 정확한 상태를 전달할 것."
        ],
        "dont": [
          "무한 스피너 대용으로 상시 띄우지 말 것 — 응답 시작과 함께 제거.",
          "dot 에 3/4/5 외 값을 기대하지 말 것."
        ]
      },
      "consumeCss": "CSS-only 로 재현 가능하되, 점의 staggered wave 를 얻으려면 각 __dot 에 animation-delay 를 인라인으로 직접 부여해야 한다(컴포넌트가 i*150ms 로 주던 값). 없으면 모든 점이 동시에 깜박인다. sr-only 라벨엔 공유 .axe-sr-only 클래스가 필요하다.",
      "url": "https://design.axelabs.ai/components/typing-indicator"
    }
  ]
}