@axis-ds/mcp 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +18 -0
  2. package/dist/index.js +3831 -2403
  3. package/dist/registry/agentic-a11y-labels.json +16 -0
  4. package/dist/registry/agentic-agent-avatar.json +19 -0
  5. package/dist/registry/agentic-approval-card.json +19 -0
  6. package/dist/registry/agentic-attachment-card.json +19 -0
  7. package/dist/registry/agentic-code-block.json +18 -0
  8. package/dist/registry/agentic-context-panel.json +18 -0
  9. package/dist/registry/agentic-diff-viewer.json +19 -0
  10. package/dist/registry/agentic-feedback-buttons.json +19 -0
  11. package/dist/registry/agentic-message-bubble.json +19 -0
  12. package/dist/registry/agentic-plan-card.json +19 -0
  13. package/dist/registry/agentic-recovery-banner.json +18 -0
  14. package/dist/registry/agentic-run-progress.json +19 -0
  15. package/dist/registry/agentic-source-panel.json +19 -0
  16. package/dist/registry/agentic-step-timeline.json +19 -0
  17. package/dist/registry/agentic-streaming-text.json +18 -0
  18. package/dist/registry/agentic-surface-renderer.json +18 -0
  19. package/dist/registry/agentic-thinking-indicator.json +18 -0
  20. package/dist/registry/agentic-token-usage-indicator.json +18 -0
  21. package/dist/registry/agentic-tool-call-card.json +19 -0
  22. package/dist/registry/badge.json +20 -0
  23. package/dist/registry/button.json +21 -0
  24. package/dist/registry/card.json +18 -0
  25. package/dist/registry/dialog.json +20 -0
  26. package/dist/registry/index.json +217 -0
  27. package/dist/registry/input.json +18 -0
  28. package/dist/registry/label.json +21 -0
  29. package/dist/registry/registry.json +218 -0
  30. package/dist/registry/select.json +20 -0
  31. package/dist/registry/separator.json +20 -0
  32. package/dist/registry/tabs.json +20 -0
  33. package/dist/registry/toast.json +21 -0
  34. package/dist/registry/utils.json +19 -0
  35. package/package.json +7 -4
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-a11y-labels",
4
+ "type": "registry:lib",
5
+ "title": "Agentic A11y Labels",
6
+ "description": "AXIS Agentic UI 접근성 레이블 상수 - 스크린 리더용 상태 · 심각도 · 역할 텍스트",
7
+ "dependencies": [],
8
+ "registryDependencies": [],
9
+ "files": [
10
+ {
11
+ "path": "registry/axis/lib/agentic-a11y-labels.ts",
12
+ "type": "registry:lib",
13
+ "content": "/**\n * 접근성(a11y)을 위한 레이블 상수\n * WCAG 2.1 AA 기준 준수를 위한 스크린 리더 텍스트\n */\n\n/** 상태 레이블 (도구 호출, 실행 단계 등) */\nexport const STATUS_LABELS = {\n pending: \"대기 중\",\n running: \"실행 중\",\n complete: \"완료\",\n success: \"성공\",\n error: \"오류\",\n skipped: \"건너뜀\",\n idle: \"대기 중\",\n} as const;\n\n/** 심각도 레이블 (알림, 승인 카드 등) */\nexport const SEVERITY_LABELS = {\n info: \"정보\",\n warning: \"주의\",\n error: \"오류\",\n} as const;\n\n/** 에이전트 상태 레이블 */\nexport const AGENT_STATUS_LABELS = {\n online: \"온라인\",\n busy: \"사용 중\",\n offline: \"오프라인\",\n} as const;\n\n/** 에이전트 타입 레이블 */\nexport const AGENT_TYPE_LABELS = {\n assistant: \"어시스턴트\",\n tool: \"도구\",\n system: \"시스템\",\n} as const;\n\n/** 소스 타입 레이블 */\nexport const SOURCE_TYPE_LABELS = {\n web: \"웹 페이지\",\n file: \"파일\",\n database: \"데이터베이스\",\n api: \"API\",\n} as const;\n\nexport type StatusLabel = keyof typeof STATUS_LABELS;\nexport type SeverityLabel = keyof typeof SEVERITY_LABELS;\nexport type AgentStatusLabel = keyof typeof AGENT_STATUS_LABELS;\nexport type AgentTypeLabel = keyof typeof AGENT_TYPE_LABELS;\n/** 메시지 역할 레이블 */\nexport const MESSAGE_ROLE_LABELS = {\n user: \"사용자\",\n assistant: \"어시스턴트\",\n system: \"시스템\",\n} as const;\n\n/** 피드백 레이블 */\nexport const FEEDBACK_LABELS = {\n like: \"좋아요\",\n dislike: \"싫어요\",\n none: \"피드백 없음\",\n} as const;\n\n/** 첨부파일 타입 레이블 */\nexport const ATTACHMENT_TYPE_LABELS = {\n image: \"이미지\",\n document: \"문서\",\n code: \"코드\",\n archive: \"압축 파일\",\n other: \"기타 파일\",\n} as const;\n\n/** Diff 변경 타입 레이블 */\nexport const DIFF_LABELS = {\n added: \"추가됨\",\n removed: \"삭제됨\",\n unchanged: \"변경 없음\",\n} as const;\n\n/** 계획 상태 레이블 */\nexport const PLAN_STATUS_LABELS = {\n draft: \"초안\",\n pending: \"승인 대기\",\n approved: \"승인됨\",\n rejected: \"거절됨\",\n executing: \"실행 중\",\n completed: \"완료\",\n} as const;\n\nexport type SourceTypeLabel = keyof typeof SOURCE_TYPE_LABELS;\nexport type MessageRoleLabel = keyof typeof MESSAGE_ROLE_LABELS;\nexport type FeedbackLabel = keyof typeof FEEDBACK_LABELS;\nexport type AttachmentTypeLabel = keyof typeof ATTACHMENT_TYPE_LABELS;\nexport type DiffLabel = keyof typeof DIFF_LABELS;\nexport type PlanStatusLabel = keyof typeof PLAN_STATUS_LABELS;\n"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-agent-avatar",
4
+ "type": "registry:ui",
5
+ "title": "Agent Avatar",
6
+ "description": "AI 에이전트 아바타 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/agentic-a11y-labels.json",
10
+ "https://axis-design-system.pages.dev/r/utils.json"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/agentic/agent-avatar.tsx",
15
+ "type": "registry:ui",
16
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { AGENT_STATUS_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport interface AgentAvatarProps {\n /** 에이전트 이름 */\n name: string;\n /** 이미지 URL */\n src?: string;\n /** 크기 */\n size?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n /** 상태 표시 */\n status?: \"online\" | \"busy\" | \"offline\";\n /** 에이전트 타입 */\n type?: \"assistant\" | \"tool\" | \"system\";\n /** 추가 클래스 */\n className?: string;\n}\n\nconst sizeStyles = {\n sm: \"w-6 h-6 text-xs\",\n md: \"w-8 h-8 text-sm\",\n lg: \"w-10 h-10 text-base\",\n xl: \"w-12 h-12 text-lg\",\n};\n\nconst statusStyles = {\n online: \"bg-[var(--axis-status-success)]\",\n busy: \"bg-[var(--axis-status-warning)]\",\n offline: \"bg-[var(--axis-status-neutral)]\",\n};\n\nconst typeColors = {\n assistant: \"bg-[var(--axis-agent-avatar-assistant)]\",\n tool: \"bg-[var(--axis-agent-avatar-tool)]\",\n system: \"bg-[var(--axis-agent-avatar-system)]\",\n};\n\n/**\n * 에이전트 아바타 컴포넌트\n */\nexport function AgentAvatar({\n name,\n src,\n size = \"md\",\n status,\n type = \"assistant\",\n className,\n}: AgentAvatarProps) {\n const initials = name\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")\n .slice(0, 2)\n .toUpperCase();\n\n const statusLabel = status ? AGENT_STATUS_LABELS[status] : \"\";\n const ariaLabel = `${name}${statusLabel ? `, ${statusLabel}` : \"\"}`;\n\n return (\n <div\n role=\"img\"\n aria-label={ariaLabel}\n className={cn(\"relative inline-flex\", className)}\n >\n {src ? (\n <img\n src={src}\n alt=\"\"\n aria-hidden=\"true\"\n className={cn(\n \"rounded-full object-cover\",\n sizeStyles[size]\n )}\n />\n ) : (\n <div\n aria-hidden=\"true\"\n className={cn(\n \"rounded-full flex items-center justify-center font-medium text-[var(--axis-agent-avatar-text)]\",\n sizeStyles[size],\n typeColors[type]\n )}\n >\n {initials}\n </div>\n )}\n {status && (\n <span\n aria-hidden=\"true\"\n className={cn(\n \"absolute bottom-0 right-0 rounded-full border-2 border-[var(--axis-surface-default)]\",\n size === \"sm\" ? \"w-2 h-2\" : \"w-3 h-3\",\n statusStyles[status]\n )}\n />\n )}\n </div>\n );\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-approval-card",
4
+ "type": "registry:ui",
5
+ "title": "Approval Card",
6
+ "description": "사용자 승인 요청 카드 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/agentic-a11y-labels.json",
10
+ "https://axis-design-system.pages.dev/r/utils.json"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/agentic/approval-card.tsx",
15
+ "type": "registry:ui",
16
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { SEVERITY_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type Severity = \"info\" | \"warning\" | \"error\";\n\nexport interface ApprovalAction {\n label: string;\n variant?: \"default\" | \"secondary\" | \"ghost\" | \"destructive\";\n onClick: () => void;\n}\n\nexport interface ApprovalCardProps {\n /** 고유 ID (접근성용) */\n id?: string;\n /** 제목 */\n title: string;\n /** 설명 */\n description?: string;\n /** 심각도 (색상 결정) */\n severity?: Severity;\n /** 액션 버튼 목록 */\n actions: ApprovalAction[];\n /** 추가 내용 */\n children?: React.ReactNode;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst severityStyles: Record<Severity, { border: string; icon: string; bg: string }> = {\n info: {\n border: \"border-l-[var(--axis-status-info)]\",\n icon: \"text-[var(--axis-status-info)]\",\n bg: \"bg-[var(--axis-surface-info)]\",\n },\n warning: {\n border: \"border-l-[var(--axis-status-warning)]\",\n icon: \"text-[var(--axis-status-warning)]\",\n bg: \"bg-[var(--axis-surface-warning)]\",\n },\n error: {\n border: \"border-l-[var(--axis-status-error)]\",\n icon: \"text-[var(--axis-status-error)]\",\n bg: \"bg-[var(--axis-surface-error)]\",\n },\n};\n\nconst severityIcons: Record<Severity, React.ReactNode> = {\n info: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n ),\n warning: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\" />\n </svg>\n ),\n error: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n ),\n};\n\nconst actionVariantStyles: Record<string, string> = {\n default: \"bg-[var(--axis-button-bg-default)] text-[var(--axis-button-text-default)] hover:bg-[var(--axis-button-bg-hover)]\",\n secondary: \"bg-[var(--axis-button-secondary-bg-default)] text-[var(--axis-text-primary)] hover:bg-[var(--axis-button-secondary-bg-hover)]\",\n ghost: \"bg-transparent text-[var(--axis-text-primary)] hover:bg-[var(--axis-surface-secondary)]\",\n destructive: \"bg-[var(--axis-button-destructive-bg-default)] text-[var(--axis-button-destructive-text-default)] hover:bg-[var(--axis-button-destructive-bg-hover)]\",\n};\n\n/**\n * 사용자 승인이 필요한 작업을 표시하는 카드 컴포넌트\n */\nexport function ApprovalCard({\n id,\n title,\n description,\n severity = \"info\",\n actions,\n children,\n className,\n}: ApprovalCardProps) {\n const styles = severityStyles[severity];\n const generatedId = React.useId();\n const baseId = id || generatedId;\n const titleId = `${baseId}-title`;\n const descId = `${baseId}-desc`;\n const severityLabel = SEVERITY_LABELS[severity];\n\n return (\n <div\n role=\"region\"\n aria-labelledby={titleId}\n aria-describedby={description ? descId : undefined}\n className={cn(\n \"rounded-lg border border-l-4 p-4\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n styles.border,\n className\n )}\n >\n <div className=\"flex items-start gap-3\">\n <div className={cn(\"flex-shrink-0 mt-0.5\", styles.icon)} aria-hidden=\"true\">\n {severityIcons[severity]}\n </div>\n <div className=\"flex-1 min-w-0\">\n <span className=\"sr-only\">{severityLabel} 알림:</span>\n <h4 id={titleId} className=\"text-sm font-semibold text-[var(--axis-text-primary)]\">\n {title}\n </h4>\n {description && (\n <p id={descId} className=\"mt-1 text-sm text-[var(--axis-text-secondary)]\">\n {description}\n </p>\n )}\n {children && <div className=\"mt-3\">{children}</div>}\n <div className=\"flex flex-wrap gap-2 mt-4\">\n {actions.map((action, index) => (\n <button\n key={index}\n onClick={action.onClick}\n className={cn(\n \"px-3 py-1.5 text-sm font-medium rounded-md transition-colors\",\n actionVariantStyles[action.variant || \"default\"]\n )}\n >\n {action.label}\n </button>\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-attachment-card",
4
+ "type": "registry:ui",
5
+ "title": "Attachment Card",
6
+ "description": "파일/이미지 첨부 표시 카드 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/agentic-a11y-labels.json",
10
+ "https://axis-design-system.pages.dev/r/utils.json"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/agentic/attachment-card.tsx",
15
+ "type": "registry:ui",
16
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { ATTACHMENT_TYPE_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type AttachmentType = \"image\" | \"document\" | \"code\" | \"archive\" | \"other\";\nexport type AttachmentStatus = \"uploading\" | \"complete\" | \"error\";\n\nexport interface AttachmentCardProps {\n /** 파일 타입 */\n type: AttachmentType;\n /** 파일명 */\n name: string;\n /** 파일 크기 (bytes) */\n size?: number;\n /** 파일 URL */\n url?: string;\n /** 썸네일 URL (이미지인 경우) */\n thumbnail?: string;\n /** 업로드 상태 */\n status?: AttachmentStatus;\n /** 업로드 진행률 (0-100) */\n progress?: number;\n /** 삭제 콜백 */\n onRemove?: () => void;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst typeIcons: Record<AttachmentType, React.ReactNode> = {\n image: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z\" />\n </svg>\n ),\n document: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\" />\n </svg>\n ),\n code: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4\" />\n </svg>\n ),\n archive: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4\" />\n </svg>\n ),\n other: (\n <svg className=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z\" />\n </svg>\n ),\n};\n\nconst formatFileSize = (bytes: number): string => {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n};\n\n/**\n * 파일/이미지 첨부를 표시하는 카드 컴포넌트\n */\nexport function AttachmentCard({\n type,\n name,\n size,\n url,\n thumbnail,\n status = \"complete\",\n progress,\n onRemove,\n className,\n}: AttachmentCardProps) {\n const typeLabel = ATTACHMENT_TYPE_LABELS[type];\n\n return (\n <div\n role=\"article\"\n aria-label={`${typeLabel}: ${name}`}\n className={cn(\n \"flex items-center gap-3 rounded-lg border p-3\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n status === \"error\" && \"border-[var(--axis-status-error-border)]\",\n className\n )}\n >\n {/* 썸네일 또는 아이콘 */}\n {thumbnail ? (\n <img\n src={thumbnail}\n alt=\"\"\n aria-hidden=\"true\"\n className=\"w-10 h-10 rounded object-cover flex-shrink-0\"\n />\n ) : (\n <div\n className=\"w-10 h-10 rounded flex items-center justify-center flex-shrink-0 bg-[var(--axis-surface-secondary)] text-[var(--axis-icon-secondary)]\"\n aria-hidden=\"true\"\n >\n {typeIcons[type]}\n </div>\n )}\n\n {/* 정보 */}\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium text-[var(--axis-text-primary)] truncate\">\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:underline\"\n aria-label={`${name} (새 창에서 열림)`}\n >\n {name}\n </a>\n ) : (\n name\n )}\n </p>\n <div className=\"flex items-center gap-2 text-xs text-[var(--axis-text-tertiary)]\">\n <span>{typeLabel}</span>\n {size !== undefined && (\n <>\n <span aria-hidden=\"true\">·</span>\n <span>{formatFileSize(size)}</span>\n </>\n )}\n {status === \"error\" && (\n <span className=\"text-[var(--axis-text-error)]\">업로드 실패</span>\n )}\n </div>\n\n {/* 업로드 진행률 */}\n {status === \"uploading\" && progress !== undefined && (\n <div className=\"mt-1.5 h-1 rounded-full bg-[var(--axis-surface-secondary)] overflow-hidden\">\n <div\n className=\"h-full rounded-full bg-[var(--axis-status-running)] transition-all\"\n style={{ width: `${progress}%` }}\n role=\"progressbar\"\n aria-valuenow={progress}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label=\"업로드 진행률\"\n />\n </div>\n )}\n </div>\n\n {/* 삭제 버튼 */}\n {onRemove && (\n <button\n onClick={onRemove}\n aria-label={`${name} 삭제`}\n className=\"flex-shrink-0 p-1 rounded-md text-[var(--axis-text-tertiary)] hover:text-[var(--axis-text-secondary)] hover:bg-[var(--axis-surface-secondary)] transition-colors\"\n >\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n )}\n </div>\n );\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-code-block",
4
+ "type": "registry:ui",
5
+ "title": "Code Block",
6
+ "description": "코드 표시 + 복사 버튼 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/utils.json"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/agentic/code-block.tsx",
14
+ "type": "registry:ui",
15
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nexport interface CodeBlockProps {\n /** 코드 문자열 */\n code: string;\n /** 언어 (구문 강조용 힌트) */\n language?: string;\n /** 파일명 */\n filename?: string;\n /** 줄 번호 표시 */\n showLineNumbers?: boolean;\n /** 복사 완료 콜백 */\n onCopy?: (code: string) => void;\n /** 최대 높이 (스크롤) */\n maxHeight?: string;\n /** 추가 클래스 */\n className?: string;\n}\n\n/**\n * 코드 표시 + 복사 버튼 컴포넌트\n */\nexport function CodeBlock({\n code,\n language,\n filename,\n showLineNumbers = false,\n onCopy,\n maxHeight,\n className,\n}: CodeBlockProps) {\n const [copied, setCopied] = React.useState(false);\n\n const handleCopy = async () => {\n try {\n await navigator.clipboard.writeText(code);\n setCopied(true);\n onCopy?.(code);\n setTimeout(() => setCopied(false), 2000);\n } catch {\n // clipboard API 미지원 시 fallback 없음\n }\n };\n\n const lines = code.split(\"\\n\");\n\n return (\n <div\n className={cn(\n \"rounded-lg border overflow-hidden\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n className\n )}\n >\n {/* 헤더 */}\n <div className=\"flex items-center justify-between px-3 py-2 border-b border-[var(--axis-border-default)] bg-[var(--axis-surface-secondary)]\">\n <div className=\"flex items-center gap-2 min-w-0\">\n {filename && (\n <code className=\"text-xs font-mono text-[var(--axis-text-primary)] truncate\">\n {filename}\n </code>\n )}\n {language && (\n <span className=\"text-xs px-1.5 py-0.5 rounded bg-[var(--axis-surface-tertiary)] text-[var(--axis-text-tertiary)]\">\n {language}\n </span>\n )}\n </div>\n <button\n onClick={handleCopy}\n aria-label={copied ? \"복사됨\" : \"코드 복사\"}\n className=\"flex items-center gap-1 px-2 py-1 text-xs rounded-md text-[var(--axis-text-secondary)] hover:bg-[var(--axis-surface-tertiary)] transition-colors\"\n >\n {copied ? (\n <>\n <svg className=\"w-3.5 h-3.5 text-[var(--axis-status-success)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n <span>복사됨</span>\n </>\n ) : (\n <>\n <svg className=\"w-3.5 h-3.5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\" />\n </svg>\n <span>복사</span>\n </>\n )}\n </button>\n </div>\n\n {/* 코드 본문 */}\n <div\n className=\"overflow-auto\"\n style={maxHeight ? { maxHeight } : undefined}\n >\n <pre className=\"p-3 text-sm leading-relaxed\">\n <code className=\"text-[var(--axis-text-primary)] font-mono\">\n {showLineNumbers ? (\n <table className=\"border-collapse w-full\" role=\"presentation\">\n <tbody>\n {lines.map((line, i) => (\n <tr key={i}>\n <td\n className=\"select-none text-right pr-4 text-[var(--axis-text-tertiary)] w-8\"\n aria-hidden=\"true\"\n >\n {i + 1}\n </td>\n <td className=\"whitespace-pre\">{line}</td>\n </tr>\n ))}\n </tbody>\n </table>\n ) : (\n code\n )}\n </code>\n </pre>\n </div>\n </div>\n );\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-context-panel",
4
+ "type": "registry:ui",
5
+ "title": "Context Panel",
6
+ "description": "모델/설정 메타 정보 패널 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/utils.json"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/agentic/context-panel.tsx",
14
+ "type": "registry:ui",
15
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nexport interface ModelInfo {\n /** 모델 이름 */\n name: string;\n /** 모델 제공자 */\n provider?: string;\n /** 모델 버전 */\n version?: string;\n}\n\nexport interface ContextPanelProps {\n /** 모델 정보 */\n modelInfo?: ModelInfo;\n /** 시스템 프롬프트 */\n systemPrompt?: string;\n /** 첨부 파일 목록 */\n attachedFiles?: string[];\n /** 추가 설정 키-값 */\n settings?: Record<string, string>;\n /** 접힘 가능 여부 */\n collapsible?: boolean;\n /** 기본 펼침 상태 */\n defaultExpanded?: boolean;\n /** 추가 클래스 */\n className?: string;\n}\n\n/**\n * 모델/설정 메타 정보를 표시하는 패널 컴포넌트\n */\nexport function ContextPanel({\n modelInfo,\n systemPrompt,\n attachedFiles,\n settings,\n collapsible = true,\n defaultExpanded = false,\n className,\n}: ContextPanelProps) {\n const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);\n const contentId = React.useId();\n\n const hasContent = modelInfo || systemPrompt || (attachedFiles && attachedFiles.length > 0) || (settings && Object.keys(settings).length > 0);\n if (!hasContent) return null;\n\n const header = (\n <div className=\"flex items-center gap-2\">\n <svg className=\"w-4 h-4 text-[var(--axis-icon-secondary)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z\" />\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M15 12a3 3 0 11-6 0 3 3 0 016 0z\" />\n </svg>\n <span className=\"text-xs font-medium text-[var(--axis-text-secondary)]\">컨텍스트</span>\n </div>\n );\n\n const content = (\n <div id={contentId} className=\"space-y-3 mt-2\">\n {modelInfo && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">모델</h5>\n <div className=\"flex items-center gap-2\">\n <code className=\"text-sm font-mono text-[var(--axis-text-primary)]\">{modelInfo.name}</code>\n {modelInfo.provider && (\n <span className=\"text-xs text-[var(--axis-text-tertiary)]\">{modelInfo.provider}</span>\n )}\n {modelInfo.version && (\n <span className=\"text-xs px-1.5 py-0.5 rounded bg-[var(--axis-surface-tertiary)] text-[var(--axis-text-tertiary)]\">\n {modelInfo.version}\n </span>\n )}\n </div>\n </div>\n )}\n\n {systemPrompt && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">시스템 프롬프트</h5>\n <p className=\"text-xs text-[var(--axis-text-primary)] p-2 rounded bg-[var(--axis-surface-secondary)] line-clamp-3 whitespace-pre-wrap\">\n {systemPrompt}\n </p>\n </div>\n )}\n\n {attachedFiles && attachedFiles.length > 0 && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">\n 첨부 파일 ({attachedFiles.length})\n </h5>\n <div className=\"space-y-1\">\n {attachedFiles.map((file, i) => (\n <div key={i} className=\"flex items-center gap-1.5 text-xs text-[var(--axis-text-primary)]\">\n <svg className=\"w-3 h-3 text-[var(--axis-icon-secondary)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13\" />\n </svg>\n <span className=\"truncate\">{file}</span>\n </div>\n ))}\n </div>\n </div>\n )}\n\n {settings && Object.keys(settings).length > 0 && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">설정</h5>\n <div className=\"space-y-1\">\n {Object.entries(settings).map(([key, value]) => (\n <div key={key} className=\"flex items-center justify-between text-xs\">\n <span className=\"text-[var(--axis-text-secondary)]\">{key}</span>\n <code className=\"font-mono text-[var(--axis-text-primary)]\">{value}</code>\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n );\n\n return (\n <div\n className={cn(\n \"rounded-lg border p-3\",\n \"bg-[var(--axis-surface-secondary)] border-[var(--axis-border-default)]\",\n className\n )}\n >\n {collapsible ? (\n <>\n <button\n onClick={() => setIsExpanded(!isExpanded)}\n aria-expanded={isExpanded}\n aria-controls={contentId}\n className=\"w-full flex items-center justify-between hover:opacity-80 transition-opacity\"\n >\n {header}\n <svg\n className={cn(\n \"w-4 h-4 text-[var(--axis-text-secondary)] transition-transform\",\n isExpanded && \"rotate-180\"\n )}\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M19 9l-7 7-7-7\" />\n </svg>\n </button>\n {isExpanded && content}\n </>\n ) : (\n <>\n {header}\n {content}\n </>\n )}\n </div>\n );\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-diff-viewer",
4
+ "type": "registry:ui",
5
+ "title": "Diff Viewer",
6
+ "description": "코드 변경사항 비교 표시 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/agentic-a11y-labels.json",
10
+ "https://axis-design-system.pages.dev/r/utils.json"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/agentic/diff-viewer.tsx",
15
+ "type": "registry:ui",
16
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { DIFF_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type DiffViewMode = \"unified\" | \"split\";\n\nexport interface DiffViewerProps {\n /** 변경 전 코드 */\n before: string;\n /** 변경 후 코드 */\n after: string;\n /** 파일명 */\n filename?: string;\n /** 표시 모드 */\n viewMode?: DiffViewMode;\n /** 추가 클래스 */\n className?: string;\n}\n\ntype DiffLineType = \"added\" | \"removed\" | \"unchanged\";\n\ninterface DiffLine {\n type: DiffLineType;\n content: string;\n oldLineNumber?: number;\n newLineNumber?: number;\n}\n\n/** LCS 기반 diff 알고리즘 */\nfunction computeDiff(before: string, after: string): DiffLine[] {\n const oldLines = before.split(\"\\n\");\n const newLines = after.split(\"\\n\");\n const m = oldLines.length;\n const n = newLines.length;\n\n // LCS 테이블 구축\n const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n if (oldLines[i - 1] === newLines[j - 1]) {\n dp[i][j] = dp[i - 1][j - 1] + 1;\n } else {\n dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);\n }\n }\n }\n\n // 역추적으로 diff 생성\n const result: DiffLine[] = [];\n let i = m;\n let j = n;\n\n while (i > 0 || j > 0) {\n if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {\n result.unshift({ type: \"unchanged\", content: oldLines[i - 1], oldLineNumber: i, newLineNumber: j });\n i--;\n j--;\n } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {\n result.unshift({ type: \"added\", content: newLines[j - 1], newLineNumber: j });\n j--;\n } else if (i > 0) {\n result.unshift({ type: \"removed\", content: oldLines[i - 1], oldLineNumber: i });\n i--;\n }\n }\n\n return result;\n}\n\nconst lineTypeStyles: Record<DiffLineType, string> = {\n added: \"bg-[color:color-mix(in_srgb,var(--axis-diff-added)_10%,transparent)]\",\n removed: \"bg-[color:color-mix(in_srgb,var(--axis-diff-removed)_10%,transparent)]\",\n unchanged: \"\",\n};\n\nconst lineTypeTextStyles: Record<DiffLineType, string> = {\n added: \"text-[var(--axis-diff-added-text)]\",\n removed: \"text-[var(--axis-diff-removed-text)]\",\n unchanged: \"text-[var(--axis-text-primary)]\",\n};\n\nconst lineTypeSymbols: Record<DiffLineType, string> = {\n added: \"+\",\n removed: \"-\",\n unchanged: \" \",\n};\n\n/**\n * 코드 변경사항 비교 표시 컴포넌트\n */\nexport function DiffViewer({\n before,\n after,\n filename,\n viewMode = \"unified\",\n className,\n}: DiffViewerProps) {\n const [mode, setMode] = React.useState<DiffViewMode>(viewMode);\n const diffLines = React.useMemo(() => computeDiff(before, after), [before, after]);\n\n const addedCount = diffLines.filter((l) => l.type === \"added\").length;\n const removedCount = diffLines.filter((l) => l.type === \"removed\").length;\n\n return (\n <div\n role=\"region\"\n aria-label={filename ? `${filename} 변경사항` : \"코드 변경사항\"}\n className={cn(\n \"rounded-lg border overflow-hidden\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n className\n )}\n >\n {/* 헤더 */}\n <div className=\"flex items-center justify-between px-3 py-2 border-b border-[var(--axis-border-default)] bg-[var(--axis-surface-secondary)]\">\n <div className=\"flex items-center gap-2 min-w-0\">\n <svg className=\"w-4 h-4 text-[var(--axis-icon-secondary)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 6h16M4 12h16M4 18h7\" />\n </svg>\n {filename && (\n <code className=\"text-xs font-mono text-[var(--axis-text-primary)] truncate\">{filename}</code>\n )}\n <span className=\"text-xs text-[var(--axis-diff-added)]\" aria-label={`${addedCount}줄 ${DIFF_LABELS.added}`}>\n +{addedCount}\n </span>\n <span className=\"text-xs text-[var(--axis-diff-removed)]\" aria-label={`${removedCount}줄 ${DIFF_LABELS.removed}`}>\n -{removedCount}\n </span>\n </div>\n <div className=\"flex items-center gap-1 bg-[var(--axis-surface-tertiary)] rounded-md p-0.5\">\n <button\n onClick={() => setMode(\"unified\")}\n aria-pressed={mode === \"unified\"}\n className={cn(\n \"px-2 py-0.5 text-xs rounded transition-colors\",\n mode === \"unified\"\n ? \"bg-[var(--axis-surface-default)] text-[var(--axis-text-primary)] shadow-sm\"\n : \"text-[var(--axis-text-tertiary)] hover:text-[var(--axis-text-secondary)]\"\n )}\n >\n 통합\n </button>\n <button\n onClick={() => setMode(\"split\")}\n aria-pressed={mode === \"split\"}\n className={cn(\n \"px-2 py-0.5 text-xs rounded transition-colors\",\n mode === \"split\"\n ? \"bg-[var(--axis-surface-default)] text-[var(--axis-text-primary)] shadow-sm\"\n : \"text-[var(--axis-text-tertiary)] hover:text-[var(--axis-text-secondary)]\"\n )}\n >\n 분할\n </button>\n </div>\n </div>\n\n {/* Diff 본문 */}\n <div className=\"overflow-auto\">\n {mode === \"unified\" ? (\n <UnifiedView lines={diffLines} />\n ) : (\n <SplitView lines={diffLines} />\n )}\n </div>\n </div>\n );\n}\n\nfunction UnifiedView({ lines }: { lines: DiffLine[] }) {\n return (\n <table className=\"w-full text-xs font-mono border-collapse\" role=\"presentation\">\n <tbody>\n {lines.map((line, i) => (\n <tr key={i} className={lineTypeStyles[line.type]}>\n <td className=\"select-none text-right px-2 py-0.5 text-[var(--axis-text-tertiary)] w-10 border-r border-[var(--axis-border-default)]\">\n {line.oldLineNumber ?? \"\"}\n </td>\n <td className=\"select-none text-right px-2 py-0.5 text-[var(--axis-text-tertiary)] w-10 border-r border-[var(--axis-border-default)]\">\n {line.newLineNumber ?? \"\"}\n </td>\n <td className={cn(\"select-none w-4 text-center py-0.5\", lineTypeTextStyles[line.type])}>\n {lineTypeSymbols[line.type]}\n </td>\n <td className={cn(\"px-2 py-0.5 whitespace-pre\", lineTypeTextStyles[line.type])}>\n {line.content}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n );\n}\n\nfunction SplitView({ lines }: { lines: DiffLine[] }) {\n // 왼쪽: removed + unchanged, 오른쪽: added + unchanged\n const leftLines: (DiffLine | null)[] = [];\n const rightLines: (DiffLine | null)[] = [];\n\n for (const line of lines) {\n if (line.type === \"unchanged\") {\n leftLines.push(line);\n rightLines.push(line);\n } else if (line.type === \"removed\") {\n leftLines.push(line);\n rightLines.push(null);\n } else {\n leftLines.push(null);\n rightLines.push(line);\n }\n }\n\n return (\n <table className=\"w-full text-xs font-mono border-collapse\" role=\"presentation\">\n <tbody>\n {leftLines.map((leftLine, i) => {\n const rightLine = rightLines[i];\n return (\n <tr key={i}>\n {/* 왼쪽 (before) */}\n <td className={cn(\"select-none text-right px-2 py-0.5 text-[var(--axis-text-tertiary)] w-10 border-r border-[var(--axis-border-default)]\", leftLine ? lineTypeStyles[leftLine.type] : \"\")}>\n {leftLine?.oldLineNumber ?? \"\"}\n </td>\n <td className={cn(\"select-none w-4 text-center py-0.5\", leftLine ? lineTypeTextStyles[leftLine.type] : \"\")}>\n {leftLine ? lineTypeSymbols[leftLine.type] : \"\"}\n </td>\n <td className={cn(\"px-2 py-0.5 whitespace-pre w-1/2 border-r border-[var(--axis-border-default)]\", leftLine ? cn(lineTypeStyles[leftLine.type], lineTypeTextStyles[leftLine.type]) : \"\")}>\n {leftLine?.content ?? \"\"}\n </td>\n {/* 오른쪽 (after) */}\n <td className={cn(\"select-none text-right px-2 py-0.5 text-[var(--axis-text-tertiary)] w-10 border-r border-[var(--axis-border-default)]\", rightLine ? lineTypeStyles[rightLine.type] : \"\")}>\n {rightLine?.newLineNumber ?? \"\"}\n </td>\n <td className={cn(\"select-none w-4 text-center py-0.5\", rightLine ? lineTypeTextStyles[rightLine.type] : \"\")}>\n {rightLine ? lineTypeSymbols[rightLine.type] : \"\"}\n </td>\n <td className={cn(\"px-2 py-0.5 whitespace-pre w-1/2\", rightLine ? cn(lineTypeStyles[rightLine.type], lineTypeTextStyles[rightLine.type]) : \"\")}>\n {rightLine?.content ?? \"\"}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n );\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-feedback-buttons",
4
+ "type": "registry:ui",
5
+ "title": "Feedback Buttons",
6
+ "description": "좋아요/싫어요 피드백 버튼 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/agentic-a11y-labels.json",
10
+ "https://axis-design-system.pages.dev/r/utils.json"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/agentic/feedback-buttons.tsx",
15
+ "type": "registry:ui",
16
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { FEEDBACK_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type FeedbackType = \"like\" | \"dislike\" | \"none\";\n\nexport interface FeedbackButtonsProps {\n /** 메시지 ID */\n messageId: string;\n /** 초기 피드백 상태 */\n initialFeedback?: FeedbackType;\n /** 피드백 변경 콜백 */\n onFeedback?: (messageId: string, feedback: FeedbackType) => void;\n /** 크기 */\n size?: \"sm\" | \"md\";\n /** 추가 클래스 */\n className?: string;\n}\n\nconst sizeStyles = {\n sm: \"p-1\",\n md: \"p-1.5\",\n};\n\nconst iconSizes = {\n sm: \"w-3.5 h-3.5\",\n md: \"w-4 h-4\",\n};\n\n/**\n * 좋아요/싫어요 피드백 버튼 컴포넌트\n */\nexport function FeedbackButtons({\n messageId,\n initialFeedback = \"none\",\n onFeedback,\n size = \"sm\",\n className,\n}: FeedbackButtonsProps) {\n const [feedback, setFeedback] = React.useState<FeedbackType>(initialFeedback);\n\n const handleFeedback = (type: FeedbackType) => {\n const newFeedback = feedback === type ? \"none\" : type;\n setFeedback(newFeedback);\n onFeedback?.(messageId, newFeedback);\n };\n\n return (\n <div\n role=\"group\"\n aria-label=\"메시지 피드백\"\n className={cn(\"inline-flex items-center gap-1\", className)}\n >\n <button\n onClick={() => handleFeedback(\"like\")}\n aria-pressed={feedback === \"like\"}\n aria-label={FEEDBACK_LABELS.like}\n className={cn(\n \"rounded-md transition-colors\",\n sizeStyles[size],\n feedback === \"like\"\n ? \"text-[var(--axis-status-success)] bg-[color:color-mix(in_srgb,var(--axis-status-success)_10%,transparent)]\"\n : \"text-[var(--axis-text-tertiary)] hover:text-[var(--axis-text-secondary)] hover:bg-[var(--axis-surface-secondary)]\"\n )}\n >\n <svg className={iconSizes[size]} fill={feedback === \"like\" ? \"currentColor\" : \"none\"} stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M14 9V5a3 3 0 00-3-3l-4 9v11h11.28a2 2 0 002-1.7l1.38-9a2 2 0 00-2-2.3H14zm-9 11H3a2 2 0 01-2-2v-7a2 2 0 012-2h2\" />\n </svg>\n </button>\n <button\n onClick={() => handleFeedback(\"dislike\")}\n aria-pressed={feedback === \"dislike\"}\n aria-label={FEEDBACK_LABELS.dislike}\n className={cn(\n \"rounded-md transition-colors\",\n sizeStyles[size],\n feedback === \"dislike\"\n ? \"text-[var(--axis-status-error)] bg-[color:color-mix(in_srgb,var(--axis-status-error)_10%,transparent)]\"\n : \"text-[var(--axis-text-tertiary)] hover:text-[var(--axis-text-secondary)] hover:bg-[var(--axis-surface-secondary)]\"\n )}\n >\n <svg className={iconSizes[size]} fill={feedback === \"dislike\" ? \"currentColor\" : \"none\"} stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M10 15V19a3 3 0 003 3l4-9V2H5.72a2 2 0 00-2 1.7l-1.38 9a2 2 0 002 2.3H10zm9-13h2a2 2 0 012 2v7a2 2 0 01-2 2h-2\" />\n </svg>\n </button>\n </div>\n );\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-message-bubble",
4
+ "type": "registry:ui",
5
+ "title": "Message Bubble",
6
+ "description": "대화 메시지 버블 컴포넌트 (user/assistant/system)",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/agentic-a11y-labels.json",
10
+ "https://axis-design-system.pages.dev/r/utils.json"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/agentic/message-bubble.tsx",
15
+ "type": "registry:ui",
16
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { MESSAGE_ROLE_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type MessageRole = \"user\" | \"assistant\" | \"system\";\nexport type MessageStatus = \"sending\" | \"sent\" | \"error\";\n\nexport interface MessageBubbleProps {\n /** 메시지 역할 */\n role: MessageRole;\n /** 메시지 내용 */\n content: React.ReactNode;\n /** 타임스탬프 */\n timestamp?: Date;\n /** 아바타 (ReactNode 또는 이미지 URL) */\n avatar?: React.ReactNode | string;\n /** 전송 상태 */\n status?: MessageStatus;\n /** 하단 액션 영역 */\n actions?: React.ReactNode;\n /** 메타데이터 (모델명 등) */\n metadata?: string;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst roleStyles: Record<MessageRole, { container: string; bubble: string }> = {\n user: {\n container: \"flex-row-reverse\",\n bubble: \"bg-[var(--axis-agent-message-user-bg)] text-[var(--axis-agent-message-user-text)] rounded-br-sm\",\n },\n assistant: {\n container: \"flex-row\",\n bubble: \"bg-[var(--axis-surface-secondary)] text-[var(--axis-text-primary)] rounded-bl-sm\",\n },\n system: {\n container: \"justify-center\",\n bubble: \"bg-[var(--axis-surface-tertiary)] text-[var(--axis-text-secondary)] text-xs italic\",\n },\n};\n\nconst statusIcons: Record<MessageStatus, React.ReactNode> = {\n sending: (\n <svg className=\"w-3 h-3 animate-spin\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n ),\n sent: (\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ),\n error: (\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 8v4m0 4h.01\" />\n </svg>\n ),\n};\n\n/**\n * 대화 메시지 버블 컴포넌트\n */\nexport function MessageBubble({\n role,\n content,\n timestamp,\n avatar,\n status,\n actions,\n metadata,\n className,\n}: MessageBubbleProps) {\n const roleLabel = MESSAGE_ROLE_LABELS[role];\n\n const renderAvatar = () => {\n if (!avatar) return null;\n if (typeof avatar === \"string\") {\n return (\n <img\n src={avatar}\n alt=\"\"\n aria-hidden=\"true\"\n className=\"w-8 h-8 rounded-full object-cover flex-shrink-0\"\n />\n );\n }\n return <div className=\"flex-shrink-0\">{avatar}</div>;\n };\n\n return (\n <div\n role=\"article\"\n aria-label={`${roleLabel} 메시지`}\n className={cn(\"flex items-start gap-2\", roleStyles[role].container, className)}\n >\n {role !== \"system\" && renderAvatar()}\n <div className={cn(\"max-w-[80%]\", role === \"system\" && \"max-w-full\")}>\n <div\n className={cn(\n \"rounded-lg px-3 py-2\",\n roleStyles[role].bubble\n )}\n >\n <div className=\"text-sm whitespace-pre-wrap\">{content}</div>\n </div>\n <div className=\"flex items-center gap-2 mt-1 px-1\">\n {timestamp && (\n <time\n dateTime={timestamp.toISOString()}\n className=\"text-xs text-[var(--axis-text-tertiary)]\"\n >\n {timestamp.toLocaleTimeString(\"ko-KR\", { hour: \"2-digit\", minute: \"2-digit\" })}\n </time>\n )}\n {metadata && (\n <span className=\"text-xs text-[var(--axis-text-tertiary)]\">{metadata}</span>\n )}\n {status && (\n <span\n className={cn(\n \"text-[var(--axis-text-tertiary)]\",\n status === \"error\" && \"text-[var(--axis-text-error)]\"\n )}\n aria-label={status === \"sending\" ? \"전송 중\" : status === \"sent\" ? \"전송됨\" : \"전송 실패\"}\n >\n {statusIcons[status]}\n </span>\n )}\n </div>\n {actions && <div className=\"mt-1\">{actions}</div>}\n </div>\n </div>\n );\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-plan-card",
4
+ "type": "registry:ui",
5
+ "title": "Plan Card",
6
+ "description": "AI 실행 계획 표시/승인 카드 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/agentic-a11y-labels.json",
10
+ "https://axis-design-system.pages.dev/r/utils.json"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/agentic/plan-card.tsx",
15
+ "type": "registry:ui",
16
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { PLAN_STATUS_LABELS, STATUS_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type PlanStatus = \"draft\" | \"pending\" | \"approved\" | \"rejected\" | \"executing\" | \"completed\";\nexport type PlanStepStatus = \"pending\" | \"running\" | \"complete\" | \"error\" | \"skipped\";\n\nexport interface PlanStep {\n id: string;\n label: string;\n status: PlanStepStatus;\n description?: string;\n}\n\nexport interface PlanCardProps {\n /** 계획 제목 */\n title: string;\n /** 단계 목록 */\n steps: PlanStep[];\n /** 계획 상태 */\n status?: PlanStatus;\n /** 승인 콜백 */\n onApprove?: () => void;\n /** 수정 콜백 */\n onEdit?: () => void;\n /** 거절 콜백 */\n onReject?: () => void;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst planStatusStyles: Record<PlanStatus, { border: string; badge: string }> = {\n draft: {\n border: \"border-l-[var(--axis-status-neutral)]\",\n badge: \"bg-[var(--axis-status-neutral-bg)] text-[var(--axis-status-neutral-strong)]\",\n },\n pending: {\n border: \"border-l-[var(--axis-status-warning)]\",\n badge: \"bg-[var(--axis-badge-warning-bg)] text-[var(--axis-badge-warning-text)]\",\n },\n approved: {\n border: \"border-l-[var(--axis-status-success)]\",\n badge: \"bg-[var(--axis-badge-success-bg)] text-[var(--axis-badge-success-text)]\",\n },\n rejected: {\n border: \"border-l-[var(--axis-status-error)]\",\n badge: \"bg-[var(--axis-badge-error-bg)] text-[var(--axis-badge-error-text)]\",\n },\n executing: {\n border: \"border-l-[var(--axis-status-running)]\",\n badge: \"bg-[var(--axis-badge-info-bg)] text-[var(--axis-badge-info-text)]\",\n },\n completed: {\n border: \"border-l-[var(--axis-status-success)]\",\n badge: \"bg-[var(--axis-badge-success-bg)] text-[var(--axis-badge-success-text)]\",\n },\n};\n\nconst stepStatusIcons: Record<PlanStepStatus, React.ReactNode> = {\n pending: (\n <svg className=\"w-4 h-4 text-[var(--axis-status-neutral)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" strokeWidth={2} />\n </svg>\n ),\n running: (\n <svg className=\"w-4 h-4 text-[var(--axis-status-running)] animate-spin\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n ),\n complete: (\n <svg className=\"w-4 h-4 text-[var(--axis-status-success)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ),\n error: (\n <svg className=\"w-4 h-4 text-[var(--axis-status-error)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n ),\n skipped: (\n <svg className=\"w-4 h-4 text-[var(--axis-status-neutral)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 5l7 7-7 7M5 5l7 7-7 7\" />\n </svg>\n ),\n};\n\n/**\n * AI 실행 계획을 표시하고 승인/거절하는 카드 컴포넌트\n */\nexport function PlanCard({\n title,\n steps,\n status = \"pending\",\n onApprove,\n onEdit,\n onReject,\n className,\n}: PlanCardProps) {\n const styles = planStatusStyles[status];\n const titleId = React.useId();\n const statusLabel = PLAN_STATUS_LABELS[status];\n const completedCount = steps.filter((s) => s.status === \"complete\").length;\n\n return (\n <div\n role=\"region\"\n aria-labelledby={titleId}\n className={cn(\n \"rounded-lg border border-l-[length:var(--axis-plan-card-accent-width)] overflow-hidden\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n styles.border,\n className\n )}\n >\n {/* 헤더 */}\n <div className=\"p-4\">\n <div className=\"flex items-center justify-between mb-2\">\n <div className=\"flex items-center gap-2\">\n <svg className=\"w-4 h-4 text-[var(--axis-icon-secondary)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\" />\n </svg>\n <h4 id={titleId} className=\"text-sm font-semibold text-[var(--axis-text-primary)]\">\n {title}\n </h4>\n </div>\n <span className={cn(\"text-xs px-2 py-0.5 rounded-full font-medium\", styles.badge)}>\n {statusLabel}\n </span>\n </div>\n\n {/* 진행률 */}\n <div className=\"flex items-center gap-2 text-xs text-[var(--axis-text-tertiary)]\">\n <span>{completedCount}/{steps.length} 단계</span>\n {steps.length > 0 && (\n <div className=\"flex-1 h-1 rounded-full bg-[var(--axis-surface-secondary)] overflow-hidden\">\n <div\n className=\"h-full rounded-full bg-[var(--axis-status-success)] transition-all\"\n style={{ width: `${(completedCount / steps.length) * 100}%` }}\n aria-hidden=\"true\"\n />\n </div>\n )}\n </div>\n </div>\n\n {/* 단계 목록 */}\n <div role=\"list\" aria-label=\"실행 단계\" className=\"border-t border-[var(--axis-border-default)] divide-y divide-[var(--axis-border-default)]\">\n {steps.map((step) => {\n const stepStatusLabel = STATUS_LABELS[step.status] || step.status;\n return (\n <div\n key={step.id}\n role=\"listitem\"\n aria-current={step.status === \"running\" ? \"step\" : undefined}\n className=\"flex items-start gap-3 px-4 py-2.5\"\n >\n <span aria-hidden=\"true\" className=\"mt-0.5 flex-shrink-0\">\n {stepStatusIcons[step.status]}\n </span>\n <div className=\"flex-1 min-w-0\">\n <span className=\"sr-only\">{stepStatusLabel}:</span>\n <p className=\"text-sm text-[var(--axis-text-primary)]\">{step.label}</p>\n {step.description && (\n <p className=\"text-xs text-[var(--axis-text-secondary)] mt-0.5\">{step.description}</p>\n )}\n </div>\n </div>\n );\n })}\n </div>\n\n {/* 액션 버튼 */}\n {(status === \"pending\" || status === \"draft\") && (onApprove || onEdit || onReject) && (\n <div className=\"flex gap-2 p-3 border-t border-[var(--axis-border-default)] bg-[var(--axis-surface-secondary)]\">\n {onApprove && (\n <button\n onClick={onApprove}\n className=\"px-3 py-1.5 text-xs font-medium rounded-md bg-[var(--axis-status-success)] text-[var(--axis-status-success-contrast)] hover:bg-[var(--axis-status-success-strong)] transition-colors\"\n >\n 승인\n </button>\n )}\n {onEdit && (\n <button\n onClick={onEdit}\n className=\"px-3 py-1.5 text-xs font-medium rounded-md bg-[var(--axis-button-secondary-bg-default)] text-[var(--axis-text-primary)] hover:bg-[var(--axis-button-secondary-bg-hover)] transition-colors\"\n >\n 수정\n </button>\n )}\n {onReject && (\n <button\n onClick={onReject}\n className=\"px-3 py-1.5 text-xs font-medium rounded-md text-[var(--axis-status-error)] hover:text-[var(--axis-plan-card-reject-hover-text)] hover:bg-[color:color-mix(in_srgb,var(--axis-status-error)_10%,transparent)] transition-colors\"\n >\n 거절\n </button>\n )}\n </div>\n )}\n </div>\n );\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-recovery-banner",
4
+ "type": "registry:ui",
5
+ "title": "Recovery Banner",
6
+ "description": "에러 복구 배너 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/utils.json"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/agentic/recovery-banner.tsx",
14
+ "type": "registry:ui",
15
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nexport interface RecoveryBannerProps {\n /** 에러 메시지 */\n message: string;\n /** 상세 설명 */\n description?: string;\n /** 재시도 콜백 */\n onRetry?: () => void;\n /** 무시 콜백 */\n onDismiss?: () => void;\n /** 추가 클래스 */\n className?: string;\n}\n\n/**\n * 에러 발생 시 복구 옵션을 제공하는 배너\n */\nexport function RecoveryBanner({\n message,\n description,\n onRetry,\n onDismiss,\n className,\n}: RecoveryBannerProps) {\n const titleId = React.useId();\n const descId = React.useId();\n\n return (\n <div\n className={cn(\n \"flex items-start gap-3 p-4 rounded-lg border\",\n \"bg-[var(--axis-surface-error)] border-[var(--axis-status-error-border)]\",\n className\n )}\n role=\"alert\"\n aria-labelledby={titleId}\n aria-describedby={description ? descId : undefined}\n >\n <svg\n className=\"w-5 h-5 text-[var(--axis-status-error)] flex-shrink-0 mt-0.5\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n <div className=\"flex-1 min-w-0\">\n <p id={titleId} className=\"text-sm font-medium text-[var(--axis-text-error)]\">\n {message}\n </p>\n {description && (\n <p id={descId} className=\"mt-1 text-sm text-[var(--axis-text-secondary)]\">\n {description}\n </p>\n )}\n <div className=\"flex gap-2 mt-3\">\n {onRetry && (\n <button\n onClick={onRetry}\n aria-describedby={descId}\n className=\"px-3 py-1.5 text-sm font-medium rounded-md bg-[var(--axis-button-destructive-bg-default)] text-[var(--axis-button-destructive-text-default)] hover:bg-[var(--axis-button-destructive-bg-hover)] transition-colors\"\n >\n 재시도\n </button>\n )}\n {onDismiss && (\n <button\n onClick={onDismiss}\n className=\"px-3 py-1.5 text-sm font-medium rounded-md bg-transparent text-[var(--axis-text-primary)] hover:bg-[var(--axis-surface-secondary)] transition-colors\"\n >\n 무시\n </button>\n )}\n </div>\n </div>\n </div>\n );\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-run-progress",
4
+ "type": "registry:ui",
5
+ "title": "Run Progress",
6
+ "description": "AI 에이전트 실행 진행률 표시 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/agentic-a11y-labels.json",
10
+ "https://axis-design-system.pages.dev/r/utils.json"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/agentic/run-progress.tsx",
15
+ "type": "registry:ui",
16
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { STATUS_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type StepStatus = \"pending\" | \"running\" | \"complete\" | \"error\";\n\nexport interface Step {\n id: string;\n label: string;\n status: StepStatus;\n description?: string;\n}\n\nexport interface RunProgressProps {\n /** 전체 실행 상태 */\n status: \"idle\" | \"running\" | \"complete\" | \"error\";\n /** 단계 목록 */\n steps: Step[];\n /** 현재 진행 중인 단계 인덱스 */\n currentStep?: number;\n /** 취소 콜백 */\n onCancel?: () => void;\n /** 재시도 콜백 */\n onRetry?: () => void;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst statusColors: Record<StepStatus, string> = {\n pending: \"bg-[var(--axis-status-neutral-muted)] text-[var(--axis-status-neutral-muted-contrast)]\",\n running: \"bg-[var(--axis-status-running)] text-[var(--axis-status-running-contrast)] animate-pulse\",\n complete: \"bg-[var(--axis-status-success)] text-[var(--axis-status-success-contrast)]\",\n error: \"bg-[var(--axis-status-error)] text-[var(--axis-status-error-contrast)]\",\n};\n\nconst statusIcons: Record<StepStatus, React.ReactNode> = {\n pending: null,\n running: (\n <svg className=\"w-3 h-3 animate-spin\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n ),\n complete: (\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ),\n error: (\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n ),\n};\n\n/**\n * 에이전트 실행 진행 상태를 표시하는 컴포넌트\n */\nexport function RunProgress({\n status,\n steps,\n currentStep,\n onCancel,\n onRetry,\n className,\n}: RunProgressProps) {\n const progressPercent = React.useMemo(() => {\n const completed = steps.filter((s) => s.status === \"complete\").length;\n return Math.round((completed / steps.length) * 100);\n }, [steps]);\n\n return (\n <div\n className={cn(\n \"rounded-lg border p-4\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n className\n )}\n >\n {/* 헤더 */}\n <div className=\"flex items-center justify-between mb-4\">\n <div className=\"flex items-center gap-2\">\n {status === \"running\" && (\n <div className=\"w-2 h-2 rounded-full bg-[var(--axis-status-running)] animate-pulse\" />\n )}\n <span className=\"text-sm font-medium text-[var(--axis-text-primary)]\">\n {status === \"idle\" && \"대기 중\"}\n {status === \"running\" && \"실행 중\"}\n {status === \"complete\" && \"완료\"}\n {status === \"error\" && \"오류 발생\"}\n </span>\n </div>\n <span className=\"text-xs text-[var(--axis-text-secondary)]\">\n {progressPercent}%\n </span>\n </div>\n\n {/* 진행 바 */}\n <div\n role=\"progressbar\"\n aria-valuenow={progressPercent}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label={`실행 진행률 ${progressPercent}%`}\n className=\"w-full h-1.5 rounded-full bg-[var(--axis-surface-secondary)] mb-4\"\n >\n <div\n className=\"h-full rounded-full bg-[var(--axis-status-running)] transition-all duration-300\"\n style={{ width: `${progressPercent}%` }}\n />\n </div>\n\n {/* 단계 목록 */}\n <div role=\"list\" aria-label=\"실행 단계\" className=\"space-y-2\">\n {steps.map((step, index) => (\n <div\n key={step.id}\n role=\"listitem\"\n aria-current={currentStep === index ? \"step\" : undefined}\n className={cn(\n \"flex items-center gap-3 p-2 rounded-md transition-colors\",\n currentStep === index && \"bg-[var(--axis-surface-secondary)]\"\n )}\n >\n <div\n className={cn(\n \"w-5 h-5 rounded-full flex items-center justify-center text-xs\",\n statusColors[step.status]\n )}\n aria-hidden=\"true\"\n >\n {statusIcons[step.status] || index + 1}\n </div>\n <div className=\"flex-1 min-w-0\">\n <span className=\"sr-only\">{STATUS_LABELS[step.status]}:</span>\n <p className=\"text-sm font-medium text-[var(--axis-text-primary)] truncate\">\n {step.label}\n </p>\n {step.description && (\n <p className=\"text-xs text-[var(--axis-text-secondary)] truncate\">\n {step.description}\n </p>\n )}\n </div>\n </div>\n ))}\n </div>\n\n {/* 액션 버튼 */}\n {(onCancel || onRetry) && (\n <div className=\"flex justify-end gap-2 mt-4 pt-4 border-t border-[var(--axis-border-default)]\">\n {status === \"running\" && onCancel && (\n <button\n onClick={onCancel}\n className=\"px-3 py-1.5 text-sm rounded-md bg-[var(--axis-surface-secondary)] text-[var(--axis-text-primary)] hover:bg-[var(--axis-surface-tertiary)]\"\n >\n 취소\n </button>\n )}\n {status === \"error\" && onRetry && (\n <button\n onClick={onRetry}\n className=\"px-3 py-1.5 text-sm rounded-md bg-[var(--axis-button-bg-default)] text-[var(--axis-button-text-default)] hover:bg-[var(--axis-button-bg-hover)]\"\n >\n 재시도\n </button>\n )}\n </div>\n )}\n </div>\n );\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-source-panel",
4
+ "type": "registry:ui",
5
+ "title": "Source Panel",
6
+ "description": "참조 소스 표시 패널 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/agentic-a11y-labels.json",
10
+ "https://axis-design-system.pages.dev/r/utils.json"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/agentic/source-panel.tsx",
15
+ "type": "registry:ui",
16
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { SOURCE_TYPE_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type SourceType = \"web\" | \"file\" | \"database\" | \"api\";\n\nexport interface Source {\n id: string;\n type: SourceType;\n title: string;\n url?: string;\n path?: string;\n snippet?: string;\n relevance?: number;\n}\n\nexport interface SourcePanelProps {\n sources: Source[];\n expandable?: boolean;\n defaultExpanded?: boolean;\n maxItems?: number;\n className?: string;\n}\n\nconst sourceIcons: Record<SourceType, React.ReactNode> = {\n web: (\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9\" />\n </svg>\n ),\n file: (\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\" />\n </svg>\n ),\n database: (\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4\" />\n </svg>\n ),\n api: (\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z\" />\n </svg>\n ),\n};\n\n/**\n * AI 응답의 출처/근거를 표시하는 패널\n */\nexport function SourcePanel({\n sources,\n expandable = true,\n defaultExpanded = false,\n maxItems = 5,\n className,\n}: SourcePanelProps) {\n const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);\n const displayedSources = isExpanded ? sources : sources.slice(0, maxItems);\n const hasMore = sources.length > maxItems;\n const listId = React.useId();\n\n if (sources.length === 0) return null;\n\n return (\n <div\n className={cn(\n \"rounded-lg border p-3\",\n \"bg-[var(--axis-surface-secondary)] border-[var(--axis-border-default)]\",\n className\n )}\n >\n <h4 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-2\">\n 출처 ({sources.length}개)\n </h4>\n <div id={listId} role=\"list\" aria-label=\"출처 목록\" className=\"space-y-2\">\n {displayedSources.map((source) => (\n <div\n key={source.id}\n role=\"listitem\"\n className=\"flex items-start gap-2 p-2 rounded-md bg-[var(--axis-surface-default)]\"\n >\n <span className=\"text-[var(--axis-icon-secondary)] mt-0.5\" aria-hidden=\"true\">\n {sourceIcons[source.type]}\n </span>\n <div className=\"flex-1 min-w-0\">\n <span className=\"sr-only\">{SOURCE_TYPE_LABELS[source.type]}:</span>\n <p className=\"text-sm font-medium text-[var(--axis-text-primary)] truncate\">\n {source.url ? (\n <a\n href={source.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:underline\"\n aria-label={`${source.title} (새 창에서 열림)`}\n >\n {source.title}\n <span className=\"sr-only\">, 새 창에서 열림</span>\n </a>\n ) : (\n source.title\n )}\n </p>\n {source.snippet && (\n <p className=\"text-xs text-[var(--axis-text-secondary)] line-clamp-2 mt-0.5\">\n {source.snippet}\n </p>\n )}\n {source.path && (\n <code className=\"text-xs text-[var(--axis-text-tertiary)]\">\n {source.path}\n </code>\n )}\n </div>\n {source.relevance !== undefined && (\n <span className=\"text-xs text-[var(--axis-text-tertiary)]\">\n {Math.round(source.relevance * 100)}%\n </span>\n )}\n </div>\n ))}\n </div>\n {expandable && hasMore && (\n <button\n onClick={() => setIsExpanded(!isExpanded)}\n aria-expanded={isExpanded}\n aria-controls={listId}\n className=\"mt-2 text-xs text-[var(--axis-text-brand)] hover:underline\"\n >\n {isExpanded ? \"접기\" : `${sources.length - maxItems}개 더 보기`}\n </button>\n )}\n </div>\n );\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-step-timeline",
4
+ "type": "registry:ui",
5
+ "title": "Step Timeline",
6
+ "description": "에이전트 실행 단계 타임라인 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/agentic-a11y-labels.json",
10
+ "https://axis-design-system.pages.dev/r/utils.json"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/agentic/step-timeline.tsx",
15
+ "type": "registry:ui",
16
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { STATUS_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type TimelineStepStatus = \"pending\" | \"running\" | \"complete\" | \"error\" | \"skipped\";\n\nexport interface TimelineStep {\n id: string;\n label: string;\n status: TimelineStepStatus;\n timestamp?: Date;\n description?: string;\n}\n\nexport interface StepTimelineProps {\n steps: TimelineStep[];\n orientation?: \"vertical\" | \"horizontal\";\n className?: string;\n}\n\n/**\n * 단계별 타임라인 컴포넌트\n */\nexport function StepTimeline({\n steps,\n orientation = \"vertical\",\n className,\n}: StepTimelineProps) {\n const isVertical = orientation === \"vertical\";\n\n return (\n <div\n role=\"list\"\n aria-label=\"작업 타임라인\"\n className={cn(\n isVertical ? \"flex flex-col gap-4\" : \"flex items-center gap-2\",\n className\n )}\n >\n {steps.map((step, index) => (\n <div\n key={step.id}\n role=\"listitem\"\n aria-current={step.status === \"running\" ? \"step\" : undefined}\n className={cn(\n \"relative flex\",\n isVertical ? \"items-start gap-3\" : \"flex-col items-center\"\n )}\n >\n {/* 연결선 */}\n {index < steps.length - 1 && (\n <div\n aria-hidden=\"true\"\n className={cn(\n \"absolute bg-[var(--axis-border-default)]\",\n isVertical\n ? \"left-2.5 top-6 w-0.5 h-full -translate-x-1/2\"\n : \"top-2.5 left-6 h-0.5 w-full\"\n )}\n />\n )}\n\n {/* 상태 인디케이터 */}\n <div\n aria-hidden=\"true\"\n className={cn(\n \"relative z-10 w-5 h-5 rounded-full flex items-center justify-center\",\n step.status === \"complete\" && \"bg-[var(--axis-status-success)]\",\n step.status === \"running\" && \"bg-[var(--axis-status-running)] animate-pulse\",\n step.status === \"error\" && \"bg-[var(--axis-status-error)]\",\n step.status === \"skipped\" && \"bg-[var(--axis-status-neutral-muted)]\",\n step.status === \"pending\" && \"bg-[var(--axis-status-neutral-muted)]\"\n )}\n >\n {step.status === \"complete\" && (\n <svg className=\"w-3 h-3 text-[var(--axis-status-success-contrast)]\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={3} d=\"M5 13l4 4L19 7\" />\n </svg>\n )}\n </div>\n\n {/* 라벨 */}\n <div className={cn(isVertical ? \"flex-1\" : \"text-center mt-2\")}>\n <span className=\"sr-only\">{STATUS_LABELS[step.status]}:</span>\n <p className=\"text-sm font-medium text-[var(--axis-text-primary)]\">\n {step.label}\n </p>\n {step.description && (\n <p className=\"text-xs text-[var(--axis-text-secondary)]\">\n {step.description}\n </p>\n )}\n {step.timestamp && (\n <p className=\"text-xs text-[var(--axis-text-tertiary)]\">\n {step.timestamp.toLocaleTimeString()}\n </p>\n )}\n </div>\n </div>\n ))}\n </div>\n );\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-streaming-text",
4
+ "type": "registry:ui",
5
+ "title": "Streaming Text",
6
+ "description": "LLM 스트리밍 텍스트 출력 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/utils.json"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/agentic/streaming-text.tsx",
14
+ "type": "registry:ui",
15
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nexport interface StreamingTextProps {\n /** 접근성 라이브 영역 모드 */\n \"aria-live\"?: \"polite\" | \"assertive\" | \"off\";\n /** 접근성 레이블 */\n \"aria-label\"?: string;\n /** 스트리밍할 텍스트 */\n text: string;\n /** 스트리밍 속도 (ms) */\n speed?: number;\n /** 스트리밍 완료 여부 */\n isComplete?: boolean;\n /** 커서 표시 여부 */\n showCursor?: boolean;\n /** 완료 콜백 */\n onComplete?: () => void;\n /** 추가 클래스 */\n className?: string;\n}\n\n/**\n * 텍스트를 실시간으로 스트리밍하는 컴포넌트\n * AI 응답을 타이핑 효과로 표시할 때 사용\n */\nexport function StreamingText({\n text,\n speed = 20,\n isComplete = false,\n showCursor = true,\n onComplete,\n \"aria-live\": ariaLive,\n \"aria-label\": ariaLabel,\n className,\n}: StreamingTextProps) {\n // 글자는 코드 포인트 단위로 센다(이모지 등 서로게이트 쌍을 반쪽으로 보이지 않게)\n const chars = React.useMemo(() => Array.from(text), [text]);\n // 상태는 \"보여 줄 글자 수\"만 두고 화면 문자열은 text 에서 잘라 만든다.\n // effect 가 정리 후 다시 실행돼도(StrictMode · 부모 재렌더) 이 위치에서 이어 간다.\n const [visibleLength, setVisibleLength] = React.useState(0);\n const [prevText, setPrevText] = React.useState(text);\n // text 변경: 이어 붙이기면 진행 위치 유지, 통째 교체면 처음부터(렌더 중 상태 조정)\n if (text !== prevText) {\n setPrevText(text);\n if (!text.startsWith(prevText)) setVisibleLength(0);\n }\n\n const targetLength = chars.length;\n const isStreaming = !isComplete && visibleLength < targetLength;\n\n // onComplete 는 ref 로 읽어 effect 의존성에서 뺀다(부모가 매 렌더 새 함수를 넘겨도 진행 유지)\n const onCompleteRef = React.useRef(onComplete);\n React.useEffect(() => {\n onCompleteRef.current = onComplete;\n });\n\n React.useEffect(() => {\n if (!isStreaming) return;\n const interval = setInterval(() => {\n setVisibleLength((n) => Math.min(n + 1, targetLength));\n }, speed);\n return () => clearInterval(interval);\n }, [isStreaming, targetLength, speed]);\n\n // 화면이 text 끝까지 따라잡은 순간 한 번 onComplete 를 부른다(isComplete 로 즉시 표시한 경우는 제외)\n const wasStreamingRef = React.useRef(false);\n React.useEffect(() => {\n if (isStreaming) {\n wasStreamingRef.current = true;\n return;\n }\n if (wasStreamingRef.current) {\n wasStreamingRef.current = false;\n if (!isComplete) onCompleteRef.current?.();\n }\n }, [isStreaming, isComplete]);\n\n const displayedText = isComplete ? text : chars.slice(0, visibleLength).join(\"\");\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel || \"AI 응답\"}\n aria-live={ariaLive ?? \"polite\"}\n aria-busy={isStreaming}\n className={cn(\"relative\", className)}\n >\n <span className=\"text-[var(--axis-text-primary)] whitespace-pre-wrap\">\n {displayedText}\n </span>\n {showCursor && isStreaming && (\n <span className=\"inline-block w-0.5 h-4 ml-0.5 bg-[var(--axis-text-primary)] animate-pulse\" aria-hidden=\"true\" />\n )}\n </div>\n );\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-surface-renderer",
4
+ "type": "registry:ui",
5
+ "title": "Surface Renderer",
6
+ "description": "동적 UI Surface 렌더러 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/utils.json"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/agentic/surface-renderer.tsx",
14
+ "type": "registry:ui",
15
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nexport type SurfaceType = \"text\" | \"code\" | \"table\" | \"chart\" | \"form\" | \"custom\";\n\nexport interface Surface {\n id: string;\n type: SurfaceType;\n title?: string;\n content: unknown;\n metadata?: Record<string, unknown>;\n}\n\nexport interface SurfaceRendererProps {\n surface: Surface;\n renderCustom?: (surface: Surface) => React.ReactNode;\n className?: string;\n}\n\n/**\n * 다양한 타입의 Surface를 동적으로 렌더링하는 컴포넌트\n */\nexport function SurfaceRenderer({\n surface,\n renderCustom,\n className,\n}: SurfaceRendererProps) {\n const renderContent = () => {\n switch (surface.type) {\n case \"text\":\n return (\n <div className=\"prose prose-sm max-w-none text-[var(--axis-text-primary)]\">\n {String(surface.content)}\n </div>\n );\n\n case \"code\":\n return (\n <pre className=\"p-3 rounded-md bg-[var(--axis-surface-secondary)] overflow-x-auto\">\n <code className=\"text-sm font-mono text-[var(--axis-text-primary)]\">\n {String(surface.content)}\n </code>\n </pre>\n );\n\n case \"table\": {\n const tableData = surface.content as { headers: string[]; rows: string[][] };\n return (\n <div className=\"overflow-x-auto\">\n <table role=\"table\" className=\"w-full text-sm\">\n {surface.title && (\n <caption className=\"sr-only\">{surface.title}</caption>\n )}\n <thead>\n <tr className=\"border-b border-[var(--axis-border-default)]\">\n {tableData.headers.map((header, i) => (\n <th\n key={i}\n scope=\"col\"\n className=\"px-3 py-2 text-left font-medium text-[var(--axis-text-secondary)]\"\n >\n {header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {tableData.rows.map((row, i) => (\n <tr\n key={i}\n className=\"border-b border-[var(--axis-border-default)] last:border-0\"\n >\n {row.map((cell, j) => (\n <td\n key={j}\n className=\"px-3 py-2 text-[var(--axis-text-primary)]\"\n >\n {cell}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n }\n\n case \"chart\":\n return (\n <div className=\"p-4 text-center text-[var(--axis-text-secondary)]\">\n [차트: {surface.title || \"데이터 시각화\"}]\n </div>\n );\n\n case \"form\":\n return (\n <div className=\"p-4 text-center text-[var(--axis-text-secondary)]\">\n [폼: {surface.title || \"입력 양식\"}]\n </div>\n );\n\n case \"custom\":\n if (renderCustom) {\n return renderCustom(surface);\n }\n return (\n <div className=\"p-4 text-center text-[var(--axis-text-secondary)]\">\n [커스텀 Surface]\n </div>\n );\n\n default:\n return null;\n }\n };\n\n return (\n <div\n className={cn(\n \"rounded-lg border p-4\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n className\n )}\n >\n {surface.title && (\n <h4 className=\"text-sm font-medium text-[var(--axis-text-primary)] mb-3\">\n {surface.title}\n </h4>\n )}\n {renderContent()}\n </div>\n );\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-thinking-indicator",
4
+ "type": "registry:ui",
5
+ "title": "Thinking Indicator",
6
+ "description": "AI 사고 과정 표시 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/utils.json"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/agentic/thinking-indicator.tsx",
14
+ "type": "registry:ui",
15
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nexport interface ThinkingIndicatorProps {\n /** 표시할 텍스트 */\n text?: string;\n /** 크기 */\n size?: \"sm\" | \"md\" | \"lg\";\n /** 추가 클래스 */\n className?: string;\n}\n\nconst sizeStyles = {\n sm: \"text-xs gap-1\",\n md: \"text-sm gap-1.5\",\n lg: \"text-base gap-2\",\n};\n\nconst dotSizes = {\n sm: \"w-1 h-1\",\n md: \"w-1.5 h-1.5\",\n lg: \"w-2 h-2\",\n};\n\n/**\n * AI가 생각 중임을 표시하는 인디케이터\n */\nexport function ThinkingIndicator({\n text = \"생각 중\",\n size = \"md\",\n className,\n}: ThinkingIndicatorProps) {\n return (\n <div\n role=\"status\"\n aria-live=\"polite\"\n aria-label={text}\n className={cn(\n \"flex items-center text-[var(--axis-text-secondary)]\",\n sizeStyles[size],\n className\n )}\n >\n <span>{text}</span>\n <div className=\"flex gap-0.5\" aria-hidden=\"true\">\n {[0, 1, 2].map((i) => (\n <div\n key={i}\n className={cn(\n \"rounded-full bg-current animate-bounce\",\n dotSizes[size]\n )}\n style={{ animationDelay: `${i * 150}ms` }}\n />\n ))}\n </div>\n </div>\n );\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-token-usage-indicator",
4
+ "type": "registry:ui",
5
+ "title": "Token Usage Indicator",
6
+ "description": "토큰 사용량 및 비용 표시 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/utils.json"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/agentic/token-usage-indicator.tsx",
14
+ "type": "registry:ui",
15
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nexport interface TokenUsageIndicatorProps {\n /** 현재 사용 토큰 수 */\n current: number;\n /** 최대 토큰 수 */\n max: number;\n /** 비용 (USD) */\n cost?: number;\n /** 경고 임계치 (0-1, 기본 0.8) */\n warningThreshold?: number;\n /** 컴팩트 모드 */\n compact?: boolean;\n /** 추가 클래스 */\n className?: string;\n}\n\n/**\n * 토큰 사용량 및 비용 표시 컴포넌트\n */\nexport function TokenUsageIndicator({\n current,\n max,\n cost,\n warningThreshold = 0.8,\n compact = false,\n className,\n}: TokenUsageIndicatorProps) {\n const ratio = max > 0 ? current / max : 0;\n const percentage = Math.min(Math.round(ratio * 100), 100);\n\n const getColor = () => {\n if (ratio >= 1) return \"bg-[var(--axis-status-error)]\";\n if (ratio >= warningThreshold) return \"bg-[var(--axis-status-warning)]\";\n return \"bg-[var(--axis-status-success)]\";\n };\n\n const getTextColor = () => {\n if (ratio >= 1) return \"text-[var(--axis-status-error)]\";\n if (ratio >= warningThreshold) return \"text-[var(--axis-status-warning)]\";\n return \"text-[var(--axis-text-secondary)]\";\n };\n\n const formatNumber = (n: number) =>\n n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);\n\n if (compact) {\n return (\n <span\n role=\"meter\"\n aria-valuenow={current}\n aria-valuemin={0}\n aria-valuemax={max}\n aria-label={`토큰 사용량: ${current} / ${max}`}\n className={cn(\"inline-flex items-center gap-1 text-xs\", getTextColor(), className)}\n >\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M13 10V3L4 14h7v7l9-11h-7z\" />\n </svg>\n {formatNumber(current)}/{formatNumber(max)}\n {cost !== undefined && (\n <span className=\"text-[var(--axis-text-tertiary)]\">(${cost.toFixed(4)})</span>\n )}\n </span>\n );\n }\n\n return (\n <div\n role=\"meter\"\n aria-valuenow={current}\n aria-valuemin={0}\n aria-valuemax={max}\n aria-label={`토큰 사용량: ${current} / ${max} (${percentage}%)`}\n className={cn(\"space-y-1\", className)}\n >\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"text-[var(--axis-text-secondary)]\">토큰 사용량</span>\n <span className={getTextColor()}>\n {formatNumber(current)} / {formatNumber(max)}\n </span>\n </div>\n <div className=\"h-1.5 rounded-full bg-[var(--axis-surface-secondary)] overflow-hidden\">\n <div\n className={cn(\"h-full rounded-full transition-all\", getColor())}\n style={{ width: `${percentage}%` }}\n aria-hidden=\"true\"\n />\n </div>\n {cost !== undefined && (\n <div className=\"text-xs text-[var(--axis-text-tertiary)] text-right\">\n ${cost.toFixed(4)}\n </div>\n )}\n </div>\n );\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "agentic-tool-call-card",
4
+ "type": "registry:ui",
5
+ "title": "Tool Call Card",
6
+ "description": "도구 호출 결과 표시 카드 컴포넌트",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "https://axis-design-system.pages.dev/r/agentic-a11y-labels.json",
10
+ "https://axis-design-system.pages.dev/r/utils.json"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/agentic/tool-call-card.tsx",
15
+ "type": "registry:ui",
16
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\nimport { STATUS_LABELS } from \"@/registry/axis/lib/agentic-a11y-labels\";\n\nexport type ToolStatus = \"pending\" | \"running\" | \"success\" | \"error\";\n\nexport interface ToolCallCardProps {\n /** 도구 이름 */\n toolName: string;\n /** 도구 설명 */\n description?: string;\n /** 실행 상태 */\n status: ToolStatus;\n /** 입력 파라미터 */\n input?: Record<string, unknown>;\n /** 출력 결과 */\n output?: unknown;\n /** 에러 메시지 */\n error?: string;\n /** 실행 시간 (ms) */\n duration?: number;\n /** 펼침 상태 */\n defaultExpanded?: boolean;\n /** 접근성 레이블 */\n \"aria-label\"?: string;\n /** 추가 클래스 */\n className?: string;\n}\n\nconst statusConfig: Record<ToolStatus, { color: string; icon: React.ReactNode }> = {\n pending: {\n color: \"text-[var(--axis-status-neutral)]\",\n icon: (\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <circle cx=\"12\" cy=\"12\" r=\"10\" strokeWidth={2} />\n </svg>\n ),\n },\n running: {\n color: \"text-[var(--axis-status-running)]\",\n icon: (\n <svg className=\"w-4 h-4 animate-spin\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path className=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\" />\n </svg>\n ),\n },\n success: {\n color: \"text-[var(--axis-status-success)]\",\n icon: (\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n ),\n },\n error: {\n color: \"text-[var(--axis-status-error)]\",\n icon: (\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n ),\n },\n};\n\n/**\n * AI 도구 호출 정보를 표시하는 카드 컴포넌트\n */\nexport function ToolCallCard({\n toolName,\n description,\n status,\n input,\n output,\n error,\n duration,\n defaultExpanded = false,\n \"aria-label\": ariaLabel,\n className,\n}: ToolCallCardProps) {\n const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);\n const config = statusConfig[status];\n const contentId = React.useId();\n const statusLabel = STATUS_LABELS[status] || status;\n\n return (\n <div\n className={cn(\n \"rounded-lg border overflow-hidden\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\n className\n )}\n >\n {/* 헤더 */}\n <button\n onClick={() => setIsExpanded(!isExpanded)}\n aria-expanded={isExpanded}\n aria-controls={contentId}\n aria-label={ariaLabel || `${toolName} - ${statusLabel}`}\n className=\"w-full flex items-center justify-between p-3 hover:bg-[var(--axis-surface-secondary)] transition-colors\"\n >\n <div className=\"flex items-center gap-2\">\n <span className={config.color} aria-hidden=\"true\">{config.icon}</span>\n <code className=\"text-sm font-mono text-[var(--axis-text-primary)]\">\n {toolName}\n </code>\n {description && (\n <span className=\"text-xs text-[var(--axis-text-secondary)]\">\n — {description}\n </span>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n {duration !== undefined && (\n <span className=\"text-xs text-[var(--axis-text-tertiary)]\">\n {duration}ms\n </span>\n )}\n <svg\n className={cn(\n \"w-4 h-4 text-[var(--axis-text-secondary)] transition-transform\",\n isExpanded && \"rotate-180\"\n )}\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M19 9l-7 7-7-7\" />\n </svg>\n </div>\n </button>\n\n {/* 내용 */}\n {isExpanded && (\n <div id={contentId} className=\"border-t border-[var(--axis-border-default)] p-3 space-y-3\">\n {input && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">\n 입력\n </h5>\n <pre className=\"text-xs p-2 rounded bg-[var(--axis-surface-secondary)] overflow-x-auto\">\n <code className=\"text-[var(--axis-text-primary)]\">\n {JSON.stringify(input, null, 2)}\n </code>\n </pre>\n </div>\n )}\n {output !== undefined && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-secondary)] mb-1\">\n 출력\n </h5>\n <pre className=\"text-xs p-2 rounded bg-[var(--axis-surface-secondary)] overflow-x-auto\">\n <code className=\"text-[var(--axis-text-primary)]\">\n {typeof output === \"string\" ? output : JSON.stringify(output, null, 2)}\n </code>\n </pre>\n </div>\n )}\n {error && (\n <div>\n <h5 className=\"text-xs font-medium text-[var(--axis-text-error)] mb-1\">\n 에러\n </h5>\n <pre className=\"text-xs p-2 rounded bg-[var(--axis-surface-error)] overflow-x-auto\">\n <code className=\"text-[var(--axis-text-error)]\">{error}</code>\n </pre>\n </div>\n )}\n </div>\n )}\n </div>\n );\n}\n"
17
+ }
18
+ ]
19
+ }