@axis-ds/mcp 1.3.4 → 1.3.5

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.
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/agent-avatar.tsx",
15
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"
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-text-success)]\",\n busy: \"bg-[var(--axis-text-warning)]\",\n offline: \"bg-[var(--axis-text-tertiary)]\",\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
17
  }
18
18
  ]
19
19
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/approval-card.tsx",
15
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"
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-text-warning)]\",\n bg: \"bg-[var(--axis-surface-warning)]\",\n },\n error: {\n border: \"border-l-[var(--axis-status-error)]\",\n icon: \"text-[var(--axis-text-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
17
  }
18
18
  ]
19
19
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/attachment-card.tsx",
15
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"
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-border-error)]\",\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
17
  }
18
18
  ]
19
19
  }
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "components/agentic/code-block.tsx",
14
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"
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-secondary)]\">\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-text-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 {/* 코드 본문: 스크롤 영역이라 키보드로 포커스해 스크롤할 수 있게 한다(WCAG 2.1.1, axe scrollable-region-focusable, WI-0069) */}\n <div\n role=\"region\"\n tabIndex={0}\n aria-label={filename ? `${filename} 코드` : language ? `${language} 코드` : \"코드\"}\n className=\"overflow-auto rounded-b-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--axis-border-focus)]\"\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
16
  }
17
17
  ]
18
18
  }
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "components/agentic/context-panel.tsx",
14
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"
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-secondary)]\">{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-secondary)]\">\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
16
  }
17
17
  ]
18
18
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/diff-viewer.tsx",
15
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"
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 의 aria-label 은 보조기술이 읽지 않는다(axe aria-prohibited-attr, WI-0069).\n 보이는 기호는 숨기고 읽을 문장은 sr-only 로 둔다 */}\n <span className=\"text-xs text-[var(--axis-text-secondary)]\">\n <span aria-hidden=\"true\">+{addedCount}</span>\n <span className=\"sr-only\">{`${addedCount}줄 ${DIFF_LABELS.added}`}</span>\n </span>\n <span className=\"text-xs text-[var(--axis-text-error)]\">\n <span aria-hidden=\"true\">-{removedCount}</span>\n <span className=\"sr-only\">{`${removedCount}줄 ${DIFF_LABELS.removed}`}</span>\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-secondary)] hover:text-[var(--axis-text-primary)]\"\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-secondary)] hover:text-[var(--axis-text-primary)]\"\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-secondary)] 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-secondary)] 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-secondary)] 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-secondary)] 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
17
  }
18
18
  ]
19
19
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/feedback-buttons.tsx",
15
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"
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-text-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-text-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
17
  }
18
18
  ]
19
19
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/message-bubble.tsx",
15
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 motion-reduce:![animation-duration:3s]\" 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"
16
+ "content": "\"use client\";\n\nimport * 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 * 시각 표시 로케일(기본 \"ko-KR\"). formatTime 이 있으면 쓰지 않습니다.\n */\n locale?: string;\n /**\n * 시각 표시 시간대(IANA 이름, 예: \"Asia/Seoul\"). 지정하면 서버 렌더와 브라우저 결과가 같아\n * 첫 렌더부터 시각을 표시합니다. 없으면 브라우저 시간대로 표시하고, 서버 렌더 · 하이드레이션\n * 첫 렌더에서는 비워 두었다가 하이드레이션 뒤에 채웁니다(서버와 브라우저 시간대가 달라 생기는\n * 하이드레이션 불일치 방지, WI-0069).\n */\n timeZone?: string;\n /**\n * 시각 문자열을 직접 만드는 함수. 지정하면 locale · timeZone 대신 이 결과를 첫 렌더부터 씁니다.\n * 서버 렌더와 브라우저에서 같은 값을 돌려줘야 합니다(Date 의 지역 시간 메서드를 쓰면 시간대에 따라 달라짐).\n * 함수라서 서버 컴포넌트에서는 넘길 수 없습니다(그때는 locale · timeZone 을 쓰세요).\n */\n formatTime?: (date: Date) => string;\n}\n\nconst noopSubscribe = () => () => {};\n\n/** 하이드레이션이 끝났는지. 서버 렌더와 하이드레이션 첫 렌더는 false, 그 뒤와 클라이언트 전용 렌더는 true */\nfunction useHydrated(): boolean {\n return React.useSyncExternalStore(\n noopSubscribe,\n () => true,\n () => false\n );\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 aria-hidden=\"true\" className=\"w-3 h-3 animate-spin motion-reduce:![animation-duration:3s]\" 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 aria-hidden=\"true\" 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 aria-hidden=\"true\" 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 locale = \"ko-KR\",\n timeZone,\n formatTime,\n}: MessageBubbleProps) {\n const roleLabel = MESSAGE_ROLE_LABELS[role];\n const hydrated = useHydrated();\n // 서버와 브라우저에서 같은 결과가 보장될 때만 첫 렌더부터 시각을 쓴다\n const deterministic = formatTime !== undefined || timeZone !== undefined;\n const timeText =\n timestamp && (deterministic || hydrated)\n ? formatTime\n ? formatTime(timestamp)\n : timestamp.toLocaleTimeString(locale, { hour: \"2-digit\", minute: \"2-digit\", timeZone })\n : \"\";\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 {timeText}\n </time>\n )}\n {metadata && (\n <span className=\"text-xs text-[var(--axis-text-tertiary)]\">{metadata}</span>\n )}\n {status && (\n <span\n role=\"img\"\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
17
  }
18
18
  ]
19
19
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/plan-card.tsx",
15
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 motion-reduce:![animation-duration:3s]\" 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-plan-card-approve-bg)] text-[var(--axis-status-success-contrast)] hover:bg-[var(--axis-plan-card-approve-hover-bg)] 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-plan-card-reject-text)] 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"
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-text-tertiary)]\" 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 motion-reduce:![animation-duration:3s]\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path 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-text-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-text-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-text-tertiary)]\" 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-plan-card-approve-bg)] text-[var(--axis-button-text-default)] hover:bg-[var(--axis-plan-card-approve-hover-bg)] 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-plan-card-reject-text)] 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
17
  }
18
18
  ]
19
19
  }
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "components/agentic/recovery-banner.tsx",
14
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"
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-border-error)]\",\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-text-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={description ? descId : undefined}\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
16
  }
17
17
  ]
18
18
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/run-progress.tsx",
15
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 motion-reduce:![animation-duration:5s]\",\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 motion-reduce:![animation-duration:3s]\" 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 motion-reduce:![animation-duration:5s]\" />\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"
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 motion-reduce:![animation-duration:5s]\",\n complete: \"bg-[var(--axis-text-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 motion-reduce:![animation-duration:3s]\" 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 motion-reduce:![animation-duration:5s]\" />\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
17
  }
18
18
  ]
19
19
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/step-timeline.tsx",
15
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 motion-reduce:![animation-duration:5s]\",\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"
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 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 * 시각 표시 로케일. 없으면 실행 환경의 기본 로케일입니다(이전과 같음). formatTime 이 있으면 쓰지 않습니다.\n */\n locale?: string;\n /**\n * 시각 표시 시간대(IANA 이름, 예: \"Asia/Seoul\"). locale 과 함께 지정하면 서버 렌더와 브라우저 결과가 같아\n * 첫 렌더부터 시각을 표시합니다. 둘 중 하나라도 없으면 실행 환경 값에 따라 달라지므로, 서버 렌더 ·\n * 하이드레이션 첫 렌더에서는 비워 두었다가 하이드레이션 뒤에 채웁니다(하이드레이션 불일치 방지, WI-0069).\n */\n timeZone?: string;\n /**\n * 시각 문자열을 직접 만드는 함수. 지정하면 locale · timeZone 대신 이 결과를 첫 렌더부터 씁니다.\n * 서버 렌더와 브라우저에서 같은 값을 돌려줘야 합니다. 함수라서 서버 컴포넌트에서는 넘길 수 없습니다.\n */\n formatTime?: (date: Date) => string;\n}\n\nconst noopSubscribe = () => () => {};\n\n/** 하이드레이션이 끝났는지. 서버 렌더와 하이드레이션 첫 렌더는 false, 그 뒤와 클라이언트 전용 렌더는 true */\nfunction useHydrated(): boolean {\n return React.useSyncExternalStore(\n noopSubscribe,\n () => true,\n () => false\n );\n}\n\n/**\n * 단계별 타임라인 컴포넌트\n */\nexport function StepTimeline({\n steps,\n orientation = \"vertical\",\n className,\n locale,\n timeZone,\n formatTime,\n}: StepTimelineProps) {\n const isVertical = orientation === \"vertical\";\n const hydrated = useHydrated();\n // 서버와 브라우저에서 같은 결과가 보장될 때만 첫 렌더부터 시각을 쓴다\n const deterministic = formatTime !== undefined || (locale !== undefined && timeZone !== undefined);\n const formatStepTime = (date: Date): string => {\n if (!deterministic && !hydrated) return \"\";\n if (formatTime) return formatTime(date);\n return timeZone !== undefined\n ? date.toLocaleTimeString(locale, { timeZone })\n : date.toLocaleTimeString(locale);\n };\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-text-success)]\",\n step.status === \"running\" && \"bg-[var(--axis-status-running)] animate-pulse motion-reduce:![animation-duration:5s]\",\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 <time dateTime={step.timestamp.toISOString()}>{formatStepTime(step.timestamp)}</time>\n </p>\n )}\n </div>\n </div>\n ))}\n </div>\n );\n}\n"
17
17
  }
18
18
  ]
19
19
  }
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "components/agentic/token-usage-indicator.tsx",
14
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"
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-text-error)]\";\n if (ratio >= warningThreshold) return \"text-[var(--axis-text-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
16
  }
17
17
  ]
18
18
  }
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "path": "components/agentic/tool-call-card.tsx",
15
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 motion-reduce:![animation-duration:3s]\" 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"
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-text-tertiary)]\",\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 motion-reduce:![animation-duration:3s]\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\" />\n <path 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-text-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-text-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-secondary)]\">\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
17
  }
18
18
  ]
19
19
  }
@@ -14,7 +14,7 @@
14
14
  {
15
15
  "path": "components/ui/dialog.tsx",
16
16
  "type": "registry:ui",
17
- "content": "import * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/** 다이얼로그의 루트 컴포넌트. 열림/닫힘 상태를 관리한다. */\nconst Dialog = DialogPrimitive.Root;\n\n/** 다이얼로그를 여는 트리거 버튼 컴포넌트 */\nconst DialogTrigger = DialogPrimitive.Trigger;\n\n/** 다이얼로그 콘텐츠를 포털로 렌더링하는 컴포넌트 */\nconst DialogPortal = DialogPrimitive.Portal;\n\n/** 다이얼로그를 닫는 버튼 컴포넌트 */\nconst DialogClose = DialogPrimitive.Close;\n\n/** 다이얼로그가 열릴 때 배경을 덮는 오버레이 컴포넌트. 페이드 인/아웃 애니메이션을 포함한다. */\nconst DialogOverlay = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\n \"fixed inset-0 z-50 bg-[var(--axis-dialog-overlay-bg)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 motion-reduce:!animate-none\",\n className\n )}\n {...props}\n />\n));\nDialogOverlay.displayName = \"DialogOverlay\";\n\n/** 다이얼로그의 본문 콘텐츠 컴포넌트. 화면 중앙에 위치하며, 오버레이와 함께 포털로 렌더링된다. */\nconst DialogContent = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n ref={ref}\n className={cn(\n \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-[shadow:var(--axis-shadow-lg)] duration-200\",\n \"bg-[var(--axis-dialog-content-bg)] border-[var(--axis-dialog-content-border)]\",\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] motion-reduce:!animate-none\",\n \"sm:rounded-[var(--axis-radius-surface,0.5rem)]\",\n className\n )}\n {...props}\n >\n {children}\n </DialogPrimitive.Content>\n </DialogPortal>\n));\nDialogContent.displayName = \"DialogContent\";\n\n/** 다이얼로그 상단 헤더 영역. 제목과 설명을 포함하는 레이아웃 컴포넌트이다. */\nconst DialogHeader = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col space-y-1.5 text-center sm:text-left\",\n className\n )}\n {...props}\n />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\n/** 다이얼로그 하단 푸터 영역. 액션 버튼들을 배치하는 레이아웃 컴포넌트이다. */\nconst DialogFooter = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n className\n )}\n {...props}\n />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\n/** 다이얼로그의 제목 컴포넌트. 접근성을 위해 aria-labelledby로 연결된다. */\nconst DialogTitle = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn(\n \"text-lg font-semibold leading-none tracking-tight text-[var(--axis-text-primary)]\",\n className\n )}\n {...props}\n />\n));\nDialogTitle.displayName = \"DialogTitle\";\n\n/** 다이얼로그의 설명 텍스트 컴포넌트. 접근성을 위해 aria-describedby로 연결된다. */\nconst DialogDescription = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn(\"text-sm text-[var(--axis-text-secondary)]\", className)}\n {...props}\n />\n));\nDialogDescription.displayName = \"DialogDescription\";\n\nexport {\n Dialog,\n DialogPortal,\n DialogOverlay,\n DialogClose,\n DialogTrigger,\n DialogContent,\n DialogHeader,\n DialogFooter,\n DialogTitle,\n DialogDescription,\n};\n"
17
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/**\n * 다이얼로그의 루트 컴포넌트. 열림/닫힘 상태를 관리한다.\n *\n * Radix 는 닫힌 동안에도 트리거에 aria-controls 를 두는데, 콘텐츠는 열릴 때만 DOM 에 있어\n * 없는 id 를 가리킨다(axe aria-valid-attr-value, WI-0069). 열림 상태를 이 래퍼가 알고 있다가\n * 닫힌 동안에는 트리거의 aria-controls 를 뺀다. open · defaultOpen · onOpenChange 는 그대로 통과한다.\n */\nconst DialogOpenContext = React.createContext<boolean | null>(null);\n\nfunction Dialog({\n open: openProp,\n defaultOpen,\n onOpenChange,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Root>) {\n const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen ?? false);\n const isControlled = openProp !== undefined;\n const open = isControlled ? openProp : uncontrolledOpen;\n const handleOpenChange = React.useCallback(\n (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n onOpenChange?.(next);\n },\n [isControlled, onOpenChange]\n );\n return (\n <DialogOpenContext.Provider value={open}>\n <DialogPrimitive.Root open={open} onOpenChange={handleOpenChange} {...props} />\n </DialogOpenContext.Provider>\n );\n}\nDialog.displayName = \"Dialog\";\n\n/** 다이얼로그를 여는 트리거 버튼 컴포넌트. 닫힌 동안에는 aria-controls 를 두지 않는다. */\nconst DialogTrigger = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>\n>((props, ref) => {\n const open = React.useContext(DialogOpenContext);\n // Dialog 밖에서 쓰면(null) Radix 동작 그대로. 사용자가 넘긴 aria-controls 는 뒤에서 덮어쓴다\n return <DialogPrimitive.Trigger ref={ref} {...(open === false ? { \"aria-controls\": undefined } : {})} {...props} />;\n});\nDialogTrigger.displayName = \"DialogTrigger\";\n\n/** 다이얼로그 콘텐츠를 포털로 렌더링하는 컴포넌트 */\nconst DialogPortal = DialogPrimitive.Portal;\n\n/** 다이얼로그를 닫는 버튼 컴포넌트 */\nconst DialogClose = DialogPrimitive.Close;\n\n/** 다이얼로그가 열릴 때 배경을 덮는 오버레이 컴포넌트. 페이드 인/아웃 애니메이션을 포함한다. */\nconst DialogOverlay = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n ref={ref}\n className={cn(\n \"fixed inset-0 z-50 bg-[var(--axis-dialog-overlay-bg)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 motion-reduce:!animate-none\",\n className\n )}\n {...props}\n />\n));\nDialogOverlay.displayName = \"DialogOverlay\";\n\n/** 다이얼로그의 본문 콘텐츠 컴포넌트. 화면 중앙에 위치하며, 오버레이와 함께 포털로 렌더링된다. */\nconst DialogContent = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n ref={ref}\n className={cn(\n \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-[shadow:var(--axis-shadow-lg)] duration-200\",\n \"bg-[var(--axis-dialog-content-bg)] border-[var(--axis-dialog-content-border)]\",\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] motion-reduce:!animate-none\",\n \"sm:rounded-[var(--axis-radius-surface,0.5rem)]\",\n className\n )}\n {...props}\n >\n {children}\n </DialogPrimitive.Content>\n </DialogPortal>\n));\nDialogContent.displayName = \"DialogContent\";\n\n/** 다이얼로그 상단 헤더 영역. 제목과 설명을 포함하는 레이아웃 컴포넌트이다. */\nconst DialogHeader = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col space-y-1.5 text-center sm:text-left\",\n className\n )}\n {...props}\n />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\n/** 다이얼로그 하단 푸터 영역. 액션 버튼들을 배치하는 레이아웃 컴포넌트이다. */\nconst DialogFooter = ({\n className,\n ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n className\n )}\n {...props}\n />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\n/** 다이얼로그의 제목 컴포넌트. 접근성을 위해 aria-labelledby로 연결된다. */\nconst DialogTitle = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn(\n \"text-lg font-semibold leading-none tracking-tight text-[var(--axis-text-primary)]\",\n className\n )}\n {...props}\n />\n));\nDialogTitle.displayName = \"DialogTitle\";\n\n/** 다이얼로그의 설명 텍스트 컴포넌트. 접근성을 위해 aria-describedby로 연결된다. */\nconst DialogDescription = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn(\"text-sm text-[var(--axis-text-secondary)]\", className)}\n {...props}\n />\n));\nDialogDescription.displayName = \"DialogDescription\";\n\nexport {\n Dialog,\n DialogPortal,\n DialogOverlay,\n DialogClose,\n DialogTrigger,\n DialogContent,\n DialogHeader,\n DialogFooter,\n DialogTitle,\n DialogDescription,\n};\n"
18
18
  }
19
19
  ]
20
20
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "axis",
3
3
  "homepage": "https://axis-design-system.pages.dev",
4
- "version": "1.7.0",
4
+ "version": "1.8.0",
5
5
  "items": [
6
6
  {
7
7
  "name": "utils",
@@ -57,7 +57,7 @@
57
57
  "type": "registry:ui",
58
58
  "title": "Dialog",
59
59
  "description": "AXIS 다이얼로그 컴포넌트 - 모달 UI",
60
- "sha256": "17453188103044027ccb4379bc0f8163a73b4edb7f119e1144831c60a1c5eb16"
60
+ "sha256": "a6c0c65bae56587e78f9b53b92f6949472b51c81722c3d31ee1c76645ec7d94b"
61
61
  },
62
62
  {
63
63
  "name": "select",
@@ -78,7 +78,7 @@
78
78
  "type": "registry:ui",
79
79
  "title": "Toast",
80
80
  "description": "AXIS 토스트 컴포넌트 - 알림 메시지",
81
- "sha256": "0d5168f10be4223f9d31bf815045c88abbd57a96ab932b05388d43a26e82546c"
81
+ "sha256": "c56513bb2338c57ffee093d7c610ce2ec493857621607ad735b1c4bb4806a21a"
82
82
  },
83
83
  {
84
84
  "name": "agentic-a11y-labels",
@@ -92,7 +92,7 @@
92
92
  "type": "registry:ui",
93
93
  "title": "Run Progress",
94
94
  "description": "AI 에이전트 실행 진행률 표시 컴포넌트",
95
- "sha256": "4d7611b71d67ccf57ce89b1a3ffdf954575e22e8dd8ff13aa86b7710367e8b24"
95
+ "sha256": "7c5729965b71edc5977a8ff41289b9e8973f0d67ae5469eb419c13207877ef7f"
96
96
  },
97
97
  {
98
98
  "name": "agentic-streaming-text",
@@ -106,14 +106,14 @@
106
106
  "type": "registry:ui",
107
107
  "title": "Approval Card",
108
108
  "description": "사용자 승인 요청 카드 컴포넌트",
109
- "sha256": "7da9521e6f7e684d8fa0ccb8156877aa251c9cbf4d7f4491a5fa346daeb8111a"
109
+ "sha256": "412651c84e63a69339fc8788e4d75657fde05b0116a9161f7c32e73a3cd1dbd7"
110
110
  },
111
111
  {
112
112
  "name": "agentic-step-timeline",
113
113
  "type": "registry:ui",
114
114
  "title": "Step Timeline",
115
115
  "description": "에이전트 실행 단계 타임라인 컴포넌트",
116
- "sha256": "cd6719280a88c471f6d1e20493f07b5d1055d37bf899129462ac89df6dfe883c"
116
+ "sha256": "6e451cc2f98a62cd4132221dc5187d710a5ddebb940f602cb838b93004a35a95"
117
117
  },
118
118
  {
119
119
  "name": "agentic-thinking-indicator",
@@ -134,14 +134,14 @@
134
134
  "type": "registry:ui",
135
135
  "title": "Recovery Banner",
136
136
  "description": "에러 복구 배너 컴포넌트",
137
- "sha256": "b4f6fe103ee7eab0b4e6684192a40e58e55dcc86d6dec6b285cb5a6d72b57560"
137
+ "sha256": "17b3c5f59ccbecba3dcdaa37a4002d0c9b5843d0aa4a8c0635c9051163ff2572"
138
138
  },
139
139
  {
140
140
  "name": "agentic-agent-avatar",
141
141
  "type": "registry:ui",
142
142
  "title": "Agent Avatar",
143
143
  "description": "AI 에이전트 아바타 컴포넌트",
144
- "sha256": "f976f582e9ed76b9708750374874a93a153845524863abb53416301366a6df3c"
144
+ "sha256": "2ac9c96807bc5103c9d7266dd7d2daacaac7e3856e17c9f862555c65e0218355"
145
145
  },
146
146
  {
147
147
  "name": "agentic-surface-renderer",
@@ -155,63 +155,63 @@
155
155
  "type": "registry:ui",
156
156
  "title": "Tool Call Card",
157
157
  "description": "도구 호출 결과 표시 카드 컴포넌트",
158
- "sha256": "22f55b62257075be3e5bb393aea8077eb26cf913ae0595802213769025492b1f"
158
+ "sha256": "7601f15cb9d796a5bae068ccb5a12b1c02078e2273bdb6be5e9d00324fd54bfb"
159
159
  },
160
160
  {
161
161
  "name": "agentic-message-bubble",
162
162
  "type": "registry:ui",
163
163
  "title": "Message Bubble",
164
164
  "description": "대화 메시지 버블 컴포넌트 (user/assistant/system)",
165
- "sha256": "9833745acfd486a8375e4ede01f51e9e440dd3f7f06a52850c82f951d14b3fec"
165
+ "sha256": "10137195374ecc5db3aba5809eb1229b8fe3c4b3e64a00e1427ed0a5d52f6408"
166
166
  },
167
167
  {
168
168
  "name": "agentic-feedback-buttons",
169
169
  "type": "registry:ui",
170
170
  "title": "Feedback Buttons",
171
171
  "description": "좋아요/싫어요 피드백 버튼 컴포넌트",
172
- "sha256": "736cfcb470ad4d32fc3b70d1697984dd02e2d75a34f98d023908fc6831792f78"
172
+ "sha256": "f3d4919281f7f811af1e8bf98cbbc32914f830b441e50a53f71d8315c69c6440"
173
173
  },
174
174
  {
175
175
  "name": "agentic-token-usage-indicator",
176
176
  "type": "registry:ui",
177
177
  "title": "Token Usage Indicator",
178
178
  "description": "토큰 사용량 및 비용 표시 컴포넌트",
179
- "sha256": "5c8c5ea1762f6cf0a2432745e296b7d3e5d244d4a33669858a9d1558be2274d5"
179
+ "sha256": "b64329101d9a9badf43b05245fbde3235ce0fc9ae5e98747fad874fc786d18b1"
180
180
  },
181
181
  {
182
182
  "name": "agentic-context-panel",
183
183
  "type": "registry:ui",
184
184
  "title": "Context Panel",
185
185
  "description": "모델/설정 메타 정보 패널 컴포넌트",
186
- "sha256": "a9a618b1d657aed7c59a1a7753d2e0cdb1446b6d53f2ff6fa03763a1146da538"
186
+ "sha256": "930ef920ebd5bef9d68b38b03e4e5b4d77429a898db323512443c9847621c2eb"
187
187
  },
188
188
  {
189
189
  "name": "agentic-code-block",
190
190
  "type": "registry:ui",
191
191
  "title": "Code Block",
192
192
  "description": "코드 표시 + 복사 버튼 컴포넌트",
193
- "sha256": "12f888378fac091cc2568b454eb275f0ed8ddb69d3533a85cc70bc960bbb2d4b"
193
+ "sha256": "e67665de3ccbd157e3b4037418760e401b1b77dbacb3456a8dc60594a217aa68"
194
194
  },
195
195
  {
196
196
  "name": "agentic-plan-card",
197
197
  "type": "registry:ui",
198
198
  "title": "Plan Card",
199
199
  "description": "AI 실행 계획 표시/승인 카드 컴포넌트",
200
- "sha256": "ed6e7145a338b2b59a05aa3f829cf3f95e391d73d5a66c685cbd9349271e26da"
200
+ "sha256": "d0ad94a23363711259bd49c54aa5205054a4c6a13bd8abdfeef3fa534b68398e"
201
201
  },
202
202
  {
203
203
  "name": "agentic-attachment-card",
204
204
  "type": "registry:ui",
205
205
  "title": "Attachment Card",
206
206
  "description": "파일/이미지 첨부 표시 카드 컴포넌트",
207
- "sha256": "1b5df56505341ef6b9a0adab7c50eced05e402b9a06cfdd3628ec4fda08e8af9"
207
+ "sha256": "d7116c7dc5c27cb6bc286a0316139d81ffd77c61bbb67a54e034d592183f7f66"
208
208
  },
209
209
  {
210
210
  "name": "agentic-diff-viewer",
211
211
  "type": "registry:ui",
212
212
  "title": "Diff Viewer",
213
213
  "description": "코드 변경사항 비교 표시 컴포넌트",
214
- "sha256": "1c3d2b81011f6522b0d37e039c0b1ccf597c0eca0aa1a4749331142ae0625ca2"
214
+ "sha256": "953104e633bce2adb4d6c2a11024e70ffae7cfa6fcd75b2f618e41b5982e81d9"
215
215
  }
216
216
  ]
217
217
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://ui.shadcn.com/schema/registry.json",
3
3
  "name": "axis",
4
4
  "homepage": "https://axis-design-system.pages.dev",
5
- "version": "1.7.0",
5
+ "version": "1.8.0",
6
6
  "items": [
7
7
  {
8
8
  "name": "utils",
@@ -58,7 +58,7 @@
58
58
  "type": "registry:ui",
59
59
  "title": "Dialog",
60
60
  "description": "AXIS 다이얼로그 컴포넌트 - 모달 UI",
61
- "sha256": "17453188103044027ccb4379bc0f8163a73b4edb7f119e1144831c60a1c5eb16"
61
+ "sha256": "a6c0c65bae56587e78f9b53b92f6949472b51c81722c3d31ee1c76645ec7d94b"
62
62
  },
63
63
  {
64
64
  "name": "select",
@@ -79,7 +79,7 @@
79
79
  "type": "registry:ui",
80
80
  "title": "Toast",
81
81
  "description": "AXIS 토스트 컴포넌트 - 알림 메시지",
82
- "sha256": "0d5168f10be4223f9d31bf815045c88abbd57a96ab932b05388d43a26e82546c"
82
+ "sha256": "c56513bb2338c57ffee093d7c610ce2ec493857621607ad735b1c4bb4806a21a"
83
83
  },
84
84
  {
85
85
  "name": "agentic-a11y-labels",
@@ -93,7 +93,7 @@
93
93
  "type": "registry:ui",
94
94
  "title": "Run Progress",
95
95
  "description": "AI 에이전트 실행 진행률 표시 컴포넌트",
96
- "sha256": "4d7611b71d67ccf57ce89b1a3ffdf954575e22e8dd8ff13aa86b7710367e8b24"
96
+ "sha256": "7c5729965b71edc5977a8ff41289b9e8973f0d67ae5469eb419c13207877ef7f"
97
97
  },
98
98
  {
99
99
  "name": "agentic-streaming-text",
@@ -107,14 +107,14 @@
107
107
  "type": "registry:ui",
108
108
  "title": "Approval Card",
109
109
  "description": "사용자 승인 요청 카드 컴포넌트",
110
- "sha256": "7da9521e6f7e684d8fa0ccb8156877aa251c9cbf4d7f4491a5fa346daeb8111a"
110
+ "sha256": "412651c84e63a69339fc8788e4d75657fde05b0116a9161f7c32e73a3cd1dbd7"
111
111
  },
112
112
  {
113
113
  "name": "agentic-step-timeline",
114
114
  "type": "registry:ui",
115
115
  "title": "Step Timeline",
116
116
  "description": "에이전트 실행 단계 타임라인 컴포넌트",
117
- "sha256": "cd6719280a88c471f6d1e20493f07b5d1055d37bf899129462ac89df6dfe883c"
117
+ "sha256": "6e451cc2f98a62cd4132221dc5187d710a5ddebb940f602cb838b93004a35a95"
118
118
  },
119
119
  {
120
120
  "name": "agentic-thinking-indicator",
@@ -135,14 +135,14 @@
135
135
  "type": "registry:ui",
136
136
  "title": "Recovery Banner",
137
137
  "description": "에러 복구 배너 컴포넌트",
138
- "sha256": "b4f6fe103ee7eab0b4e6684192a40e58e55dcc86d6dec6b285cb5a6d72b57560"
138
+ "sha256": "17b3c5f59ccbecba3dcdaa37a4002d0c9b5843d0aa4a8c0635c9051163ff2572"
139
139
  },
140
140
  {
141
141
  "name": "agentic-agent-avatar",
142
142
  "type": "registry:ui",
143
143
  "title": "Agent Avatar",
144
144
  "description": "AI 에이전트 아바타 컴포넌트",
145
- "sha256": "f976f582e9ed76b9708750374874a93a153845524863abb53416301366a6df3c"
145
+ "sha256": "2ac9c96807bc5103c9d7266dd7d2daacaac7e3856e17c9f862555c65e0218355"
146
146
  },
147
147
  {
148
148
  "name": "agentic-surface-renderer",
@@ -156,63 +156,63 @@
156
156
  "type": "registry:ui",
157
157
  "title": "Tool Call Card",
158
158
  "description": "도구 호출 결과 표시 카드 컴포넌트",
159
- "sha256": "22f55b62257075be3e5bb393aea8077eb26cf913ae0595802213769025492b1f"
159
+ "sha256": "7601f15cb9d796a5bae068ccb5a12b1c02078e2273bdb6be5e9d00324fd54bfb"
160
160
  },
161
161
  {
162
162
  "name": "agentic-message-bubble",
163
163
  "type": "registry:ui",
164
164
  "title": "Message Bubble",
165
165
  "description": "대화 메시지 버블 컴포넌트 (user/assistant/system)",
166
- "sha256": "9833745acfd486a8375e4ede01f51e9e440dd3f7f06a52850c82f951d14b3fec"
166
+ "sha256": "10137195374ecc5db3aba5809eb1229b8fe3c4b3e64a00e1427ed0a5d52f6408"
167
167
  },
168
168
  {
169
169
  "name": "agentic-feedback-buttons",
170
170
  "type": "registry:ui",
171
171
  "title": "Feedback Buttons",
172
172
  "description": "좋아요/싫어요 피드백 버튼 컴포넌트",
173
- "sha256": "736cfcb470ad4d32fc3b70d1697984dd02e2d75a34f98d023908fc6831792f78"
173
+ "sha256": "f3d4919281f7f811af1e8bf98cbbc32914f830b441e50a53f71d8315c69c6440"
174
174
  },
175
175
  {
176
176
  "name": "agentic-token-usage-indicator",
177
177
  "type": "registry:ui",
178
178
  "title": "Token Usage Indicator",
179
179
  "description": "토큰 사용량 및 비용 표시 컴포넌트",
180
- "sha256": "5c8c5ea1762f6cf0a2432745e296b7d3e5d244d4a33669858a9d1558be2274d5"
180
+ "sha256": "b64329101d9a9badf43b05245fbde3235ce0fc9ae5e98747fad874fc786d18b1"
181
181
  },
182
182
  {
183
183
  "name": "agentic-context-panel",
184
184
  "type": "registry:ui",
185
185
  "title": "Context Panel",
186
186
  "description": "모델/설정 메타 정보 패널 컴포넌트",
187
- "sha256": "a9a618b1d657aed7c59a1a7753d2e0cdb1446b6d53f2ff6fa03763a1146da538"
187
+ "sha256": "930ef920ebd5bef9d68b38b03e4e5b4d77429a898db323512443c9847621c2eb"
188
188
  },
189
189
  {
190
190
  "name": "agentic-code-block",
191
191
  "type": "registry:ui",
192
192
  "title": "Code Block",
193
193
  "description": "코드 표시 + 복사 버튼 컴포넌트",
194
- "sha256": "12f888378fac091cc2568b454eb275f0ed8ddb69d3533a85cc70bc960bbb2d4b"
194
+ "sha256": "e67665de3ccbd157e3b4037418760e401b1b77dbacb3456a8dc60594a217aa68"
195
195
  },
196
196
  {
197
197
  "name": "agentic-plan-card",
198
198
  "type": "registry:ui",
199
199
  "title": "Plan Card",
200
200
  "description": "AI 실행 계획 표시/승인 카드 컴포넌트",
201
- "sha256": "ed6e7145a338b2b59a05aa3f829cf3f95e391d73d5a66c685cbd9349271e26da"
201
+ "sha256": "d0ad94a23363711259bd49c54aa5205054a4c6a13bd8abdfeef3fa534b68398e"
202
202
  },
203
203
  {
204
204
  "name": "agentic-attachment-card",
205
205
  "type": "registry:ui",
206
206
  "title": "Attachment Card",
207
207
  "description": "파일/이미지 첨부 표시 카드 컴포넌트",
208
- "sha256": "1b5df56505341ef6b9a0adab7c50eced05e402b9a06cfdd3628ec4fda08e8af9"
208
+ "sha256": "d7116c7dc5c27cb6bc286a0316139d81ffd77c61bbb67a54e034d592183f7f66"
209
209
  },
210
210
  {
211
211
  "name": "agentic-diff-viewer",
212
212
  "type": "registry:ui",
213
213
  "title": "Diff Viewer",
214
214
  "description": "코드 변경사항 비교 표시 컴포넌트",
215
- "sha256": "1c3d2b81011f6522b0d37e039c0b1ccf597c0eca0aa1a4749331142ae0625ca2"
215
+ "sha256": "953104e633bce2adb4d6c2a11024e70ffae7cfa6fcd75b2f618e41b5982e81d9"
216
216
  }
217
217
  ]
218
218
  }
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "path": "components/ui/toast.tsx",
17
17
  "type": "registry:ui",
18
- "content": "import * as React from \"react\";\nimport * as ToastPrimitives from \"@radix-ui/react-toast\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/** 토스트 알림 시스템의 프로바이더 컴포넌트. 앱 루트에 배치하여 토스트 기능을 활성화한다. */\nconst ToastProvider = ToastPrimitives.Provider;\n\n/** 토스트 알림이 표시되는 뷰포트 영역. 화면 하단 우측에 고정 위치로 렌더링된다. */\nconst ToastViewport = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Viewport>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Viewport\n ref={ref}\n className={cn(\n \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n className\n )}\n {...props}\n />\n));\nToastViewport.displayName = ToastPrimitives.Viewport.displayName;\n\nconst toastVariants = cva(\n \"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full motion-reduce:!animate-none\",\n {\n variants: {\n variant: {\n default:\n \"border-[var(--axis-border-default)] bg-[var(--axis-surface-default)] text-[var(--axis-text-primary)]\",\n success:\n \"border-[var(--axis-border-success)] bg-[var(--axis-surface-success)] text-[var(--axis-text-success)]\",\n error:\n \"border-[var(--axis-border-error)] bg-[var(--axis-surface-error)] text-[var(--axis-text-error)]\",\n warning:\n \"border-transparent bg-[var(--axis-surface-warning)] text-[var(--axis-text-warning)]\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\n/** 토스트 알림의 루트 컴포넌트. default, success, error, warning 변형을 지원하며 스와이프로 닫을 수 있다. */\nconst Toast = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Root>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => (\n <ToastPrimitives.Root\n ref={ref}\n className={cn(toastVariants({ variant }), className)}\n {...props}\n />\n));\nToast.displayName = ToastPrimitives.Root.displayName;\n\n/** 토스트 내에 표시되는 액션 버튼 컴포넌트. 사용자 상호작용을 위한 CTA를 제공한다. */\nconst ToastAction = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Action>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Action\n ref={ref}\n className={cn(\n \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border px-3 text-sm font-medium transition-colors\",\n \"border-[var(--axis-border-default)] bg-transparent\",\n \"hover:bg-[var(--axis-surface-secondary)]\",\n \"focus:outline-none focus:ring-2 focus:ring-[var(--axis-border-focus)]\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n className\n )}\n {...props}\n />\n));\nToastAction.displayName = ToastPrimitives.Action.displayName;\n\n/** 토스트를 닫는 닫기 버튼 컴포넌트. 호버 시 표시되며 X 아이콘을 포함한다. */\nconst ToastClose = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Close>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Close\n ref={ref}\n className={cn(\n \"absolute right-2 top-2 rounded-md p-1 text-[var(--axis-text-secondary)] opacity-0 transition-opacity\",\n \"hover:text-[var(--axis-text-primary)]\",\n \"focus:opacity-100 focus:outline-none focus:ring-2\",\n \"group-hover:opacity-100\",\n className\n )}\n toast-close=\"\"\n {...props}\n >\n <svg className=\"h-4 w-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 </ToastPrimitives.Close>\n));\nToastClose.displayName = ToastPrimitives.Close.displayName;\n\n/** 토스트의 제목을 표시하는 컴포넌트 */\nconst ToastTitle = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Title>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Title\n ref={ref}\n className={cn(\"text-sm font-semibold\", className)}\n {...props}\n />\n));\nToastTitle.displayName = ToastPrimitives.Title.displayName;\n\n/** 토스트의 상세 설명을 표시하는 컴포넌트 */\nconst ToastDescription = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Description>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Description\n ref={ref}\n className={cn(\"text-sm opacity-90\", className)}\n {...props}\n />\n));\nToastDescription.displayName = ToastPrimitives.Description.displayName;\n\nexport {\n ToastProvider,\n ToastViewport,\n Toast,\n ToastTitle,\n ToastDescription,\n ToastClose,\n ToastAction,\n};\n"
18
+ "content": "import * as React from \"react\";\nimport * as ToastPrimitives from \"@radix-ui/react-toast\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/** 토스트 알림 시스템의 프로바이더 컴포넌트. 앱 루트에 배치하여 토스트 기능을 활성화한다. */\nconst ToastProvider = ToastPrimitives.Provider;\n\n/** 토스트 알림이 표시되는 뷰포트 영역. 화면 하단 우측에 고정 위치로 렌더링된다. */\nconst ToastViewport = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Viewport>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Viewport\n ref={ref}\n className={cn(\n \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n className\n )}\n {...props}\n />\n));\nToastViewport.displayName = ToastPrimitives.Viewport.displayName;\n\nconst toastVariants = cva(\n \"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full motion-reduce:!animate-none\",\n {\n variants: {\n variant: {\n default:\n \"border-[var(--axis-border-default)] bg-[var(--axis-surface-default)] text-[var(--axis-text-primary)]\",\n success:\n \"border-[var(--axis-border-success)] bg-[var(--axis-surface-success)] text-[var(--axis-text-success)]\",\n error:\n \"border-[var(--axis-border-error)] bg-[var(--axis-surface-error)] text-[var(--axis-text-error)]\",\n warning:\n \"border-transparent bg-[var(--axis-surface-warning)] text-[var(--axis-text-warning)]\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\n/** 토스트 알림의 루트 컴포넌트. default, success, error, warning 변형을 지원하며 스와이프로 닫을 수 있다. */\nconst Toast = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Root>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => (\n <ToastPrimitives.Root\n ref={ref}\n className={cn(toastVariants({ variant }), className)}\n {...props}\n />\n));\nToast.displayName = ToastPrimitives.Root.displayName;\n\n/** 토스트 내에 표시되는 액션 버튼 컴포넌트. 사용자 상호작용을 위한 CTA를 제공한다. */\nconst ToastAction = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Action>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Action\n ref={ref}\n className={cn(\n \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border px-3 text-sm font-medium transition-colors\",\n \"border-[var(--axis-border-default)] bg-transparent\",\n \"hover:bg-[var(--axis-surface-secondary)]\",\n \"focus:outline-none focus:ring-2 focus:ring-[var(--axis-border-focus)]\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n className\n )}\n {...props}\n />\n));\nToastAction.displayName = ToastPrimitives.Action.displayName;\n\n/** 토스트를 닫는 닫기 버튼 컴포넌트. 호버 시 표시되며 X 아이콘을 포함한다. */\nconst ToastClose = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Close>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Close\n ref={ref}\n className={cn(\n \"absolute right-2 top-2 rounded-md p-1 text-[var(--axis-text-secondary)] opacity-0 transition-opacity\",\n \"hover:text-[var(--axis-text-primary)]\",\n \"focus:opacity-100 focus:outline-none focus:ring-2\",\n \"group-hover:opacity-100\",\n className\n )}\n toast-close=\"\"\n {...props}\n >\n <svg className=\"h-4 w-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 </ToastPrimitives.Close>\n));\nToastClose.displayName = ToastPrimitives.Close.displayName;\n\n/** 토스트의 제목을 표시하는 컴포넌트 */\nconst ToastTitle = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Title>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Title\n ref={ref}\n className={cn(\"text-sm font-semibold\", className)}\n {...props}\n />\n));\nToastTitle.displayName = ToastPrimitives.Title.displayName;\n\n/** 토스트의 상세 설명을 표시하는 컴포넌트 */\nconst ToastDescription = React.forwardRef<\n React.ElementRef<typeof ToastPrimitives.Description>,\n React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n <ToastPrimitives.Description\n ref={ref}\n className={cn(\"text-sm\", className)}\n {...props}\n />\n));\nToastDescription.displayName = ToastPrimitives.Description.displayName;\n\nexport {\n ToastProvider,\n ToastViewport,\n Toast,\n ToastTitle,\n ToastDescription,\n ToastClose,\n ToastAction,\n};\n"
19
19
  }
20
20
  ]
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axis-ds/mcp",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
4
4
  "type": "module",
5
5
  "description": "AXIS Design System - MCP Server for AI-assisted component discovery",
6
6
  "bin": {