@axis-ds/cli 1.6.0 → 1.6.1

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/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:!animate-none\" 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": "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"
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:!animate-none\" 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-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"
17
17
  }
18
18
  ]
19
19
  }
@@ -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:!animate-none\",\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:!animate-none\" 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:!animate-none\" />\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-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"
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:!animate-none\",\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": "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"
17
17
  }
18
18
  ]
19
19
  }
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "components/agentic/streaming-text.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 StreamingTextProps {\n /** 접근성 라이브 영역 모드 */\n \"aria-live\"?: \"polite\" | \"assertive\" | \"off\";\n /** 접근성 레이블 */\n \"aria-label\"?: string;\n /** 스트리밍할 텍스트 */\n text: string;\n /** 스트리밍 속도 (ms) */\n speed?: number;\n /** 스트리밍 완료 여부 */\n isComplete?: boolean;\n /** 커서 표시 여부 */\n showCursor?: boolean;\n /** 완료 콜백 */\n onComplete?: () => void;\n /** 추가 클래스 */\n className?: string;\n}\n\n/**\n * 텍스트를 실시간으로 스트리밍하는 컴포넌트\n * AI 응답을 타이핑 효과로 표시할 때 사용\n */\nexport function StreamingText({\n text,\n speed = 20,\n isComplete = false,\n showCursor = true,\n onComplete,\n \"aria-live\": ariaLive,\n \"aria-label\": ariaLabel,\n className,\n}: StreamingTextProps) {\n // 글자는 코드 포인트 단위로 센다(이모지 등 서로게이트 쌍을 반쪽으로 보이지 않게)\n const chars = React.useMemo(() => Array.from(text), [text]);\n // 상태는 \"보여 줄 글자 수\"만 두고 화면 문자열은 text 에서 잘라 만든다.\n // effect 가 정리 후 다시 실행돼도(StrictMode · 부모 재렌더) 이 위치에서 이어 간다.\n const [visibleLength, setVisibleLength] = React.useState(0);\n const [prevText, setPrevText] = React.useState(text);\n // text 변경: 이어 붙이기면 진행 위치 유지, 통째 교체면 처음부터(렌더 중 상태 조정)\n if (text !== prevText) {\n setPrevText(text);\n if (!text.startsWith(prevText)) setVisibleLength(0);\n }\n\n const targetLength = chars.length;\n const isStreaming = !isComplete && visibleLength < targetLength;\n\n // onComplete 는 ref 로 읽어 effect 의존성에서 뺀다(부모가 매 렌더 새 함수를 넘겨도 진행 유지)\n const onCompleteRef = React.useRef(onComplete);\n React.useEffect(() => {\n onCompleteRef.current = onComplete;\n });\n\n React.useEffect(() => {\n if (!isStreaming) return;\n const interval = setInterval(() => {\n setVisibleLength((n) => Math.min(n + 1, targetLength));\n }, speed);\n return () => clearInterval(interval);\n }, [isStreaming, targetLength, speed]);\n\n // 화면이 text 끝까지 따라잡은 순간 한 번 onComplete 를 부른다(isComplete 로 즉시 표시한 경우는 제외)\n const wasStreamingRef = React.useRef(false);\n React.useEffect(() => {\n if (isStreaming) {\n wasStreamingRef.current = true;\n return;\n }\n if (wasStreamingRef.current) {\n wasStreamingRef.current = false;\n if (!isComplete) onCompleteRef.current?.();\n }\n }, [isStreaming, isComplete]);\n\n const displayedText = isComplete ? text : chars.slice(0, visibleLength).join(\"\");\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel || \"AI 응답\"}\n aria-live={ariaLive ?? \"polite\"}\n aria-busy={isStreaming}\n className={cn(\"relative\", className)}\n >\n <span className=\"text-[var(--axis-text-primary)] whitespace-pre-wrap\">\n {displayedText}\n </span>\n {showCursor && isStreaming && (\n <span className=\"inline-block w-0.5 h-4 ml-0.5 bg-[var(--axis-text-primary)] animate-pulse motion-reduce:!animate-none\" aria-hidden=\"true\" />\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 StreamingTextProps {\n /** 접근성 라이브 영역 모드 */\n \"aria-live\"?: \"polite\" | \"assertive\" | \"off\";\n /** 접근성 레이블 */\n \"aria-label\"?: string;\n /** 스트리밍할 텍스트 */\n text: string;\n /** 스트리밍 속도 (ms) */\n speed?: number;\n /** 스트리밍 완료 여부 */\n isComplete?: boolean;\n /** 커서 표시 여부 */\n showCursor?: boolean;\n /** 완료 콜백 */\n onComplete?: () => void;\n /** 추가 클래스 */\n className?: string;\n}\n\n/**\n * 텍스트를 실시간으로 스트리밍하는 컴포넌트\n * AI 응답을 타이핑 효과로 표시할 때 사용\n */\nexport function StreamingText({\n text,\n speed = 20,\n isComplete = false,\n showCursor = true,\n onComplete,\n \"aria-live\": ariaLive,\n \"aria-label\": ariaLabel,\n className,\n}: StreamingTextProps) {\n // 글자는 코드 포인트 단위로 센다(이모지 등 서로게이트 쌍을 반쪽으로 보이지 않게)\n const chars = React.useMemo(() => Array.from(text), [text]);\n // 상태는 \"보여 줄 글자 수\"만 두고 화면 문자열은 text 에서 잘라 만든다.\n // effect 가 정리 후 다시 실행돼도(StrictMode · 부모 재렌더) 이 위치에서 이어 간다.\n const [visibleLength, setVisibleLength] = React.useState(0);\n const [prevText, setPrevText] = React.useState(text);\n // text 변경: 이어 붙이기면 진행 위치 유지, 통째 교체면 처음부터(렌더 중 상태 조정)\n if (text !== prevText) {\n setPrevText(text);\n if (!text.startsWith(prevText)) setVisibleLength(0);\n }\n\n const targetLength = chars.length;\n const isStreaming = !isComplete && visibleLength < targetLength;\n\n // onComplete 는 ref 로 읽어 effect 의존성에서 뺀다(부모가 매 렌더 새 함수를 넘겨도 진행 유지)\n const onCompleteRef = React.useRef(onComplete);\n React.useEffect(() => {\n onCompleteRef.current = onComplete;\n });\n\n React.useEffect(() => {\n if (!isStreaming) return;\n const interval = setInterval(() => {\n setVisibleLength((n) => Math.min(n + 1, targetLength));\n }, speed);\n return () => clearInterval(interval);\n }, [isStreaming, targetLength, speed]);\n\n // 화면이 text 끝까지 따라잡은 순간 한 번 onComplete 를 부른다(isComplete 로 즉시 표시한 경우는 제외)\n const wasStreamingRef = React.useRef(false);\n React.useEffect(() => {\n if (isStreaming) {\n wasStreamingRef.current = true;\n return;\n }\n if (wasStreamingRef.current) {\n wasStreamingRef.current = false;\n if (!isComplete) onCompleteRef.current?.();\n }\n }, [isStreaming, isComplete]);\n\n const displayedText = isComplete ? text : chars.slice(0, visibleLength).join(\"\");\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel || \"AI 응답\"}\n aria-live={ariaLive ?? \"polite\"}\n aria-busy={isStreaming}\n className={cn(\"relative\", className)}\n >\n <span className=\"text-[var(--axis-text-primary)] whitespace-pre-wrap\">\n {displayedText}\n </span>\n {showCursor && isStreaming && (\n <span className=\"inline-block w-0.5 h-4 ml-0.5 bg-[var(--axis-text-primary)] animate-pulse motion-reduce:![animation-duration:5s]\" aria-hidden=\"true\" />\n )}\n </div>\n );\n}\n"
16
16
  }
17
17
  ]
18
18
  }
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "components/agentic/thinking-indicator.tsx",
14
14
  "type": "registry:ui",
15
- "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nexport interface ThinkingIndicatorProps {\n /** 표시할 텍스트 */\n text?: string;\n /** 크기 */\n size?: \"sm\" | \"md\" | \"lg\";\n /** 추가 클래스 */\n className?: string;\n}\n\nconst sizeStyles = {\n sm: \"text-xs gap-1\",\n md: \"text-sm gap-1.5\",\n lg: \"text-base gap-2\",\n};\n\nconst dotSizes = {\n sm: \"w-1 h-1\",\n md: \"w-1.5 h-1.5\",\n lg: \"w-2 h-2\",\n};\n\n/**\n * AI가 생각 중임을 표시하는 인디케이터\n */\nexport function ThinkingIndicator({\n text = \"생각 중\",\n size = \"md\",\n className,\n}: ThinkingIndicatorProps) {\n return (\n <div\n role=\"status\"\n aria-live=\"polite\"\n aria-label={text}\n className={cn(\n \"flex items-center text-[var(--axis-text-secondary)]\",\n sizeStyles[size],\n className\n )}\n >\n <span>{text}</span>\n <div className=\"flex gap-0.5\" aria-hidden=\"true\">\n {[0, 1, 2].map((i) => (\n <div\n key={i}\n className={cn(\n \"rounded-full bg-current animate-bounce motion-reduce:!animate-none\",\n dotSizes[size]\n )}\n style={{ animationDelay: `${i * 150}ms` }}\n />\n ))}\n </div>\n </div>\n );\n}\n"
15
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nexport interface ThinkingIndicatorProps {\n /** 표시할 텍스트 */\n text?: string;\n /** 크기 */\n size?: \"sm\" | \"md\" | \"lg\";\n /** 추가 클래스 */\n className?: string;\n}\n\nconst sizeStyles = {\n sm: \"text-xs gap-1\",\n md: \"text-sm gap-1.5\",\n lg: \"text-base gap-2\",\n};\n\nconst dotSizes = {\n sm: \"w-1 h-1\",\n md: \"w-1.5 h-1.5\",\n lg: \"w-2 h-2\",\n};\n\n/**\n * AI가 생각 중임을 표시하는 인디케이터\n */\nexport function ThinkingIndicator({\n text = \"생각 중\",\n size = \"md\",\n className,\n}: ThinkingIndicatorProps) {\n return (\n <div\n role=\"status\"\n aria-live=\"polite\"\n aria-label={text}\n className={cn(\n \"flex items-center text-[var(--axis-text-secondary)]\",\n sizeStyles[size],\n className\n )}\n >\n <span>{text}</span>\n <div className=\"flex gap-0.5\" aria-hidden=\"true\">\n {[0, 1, 2].map((i) => (\n <div\n key={i}\n className={cn(\n \"rounded-full bg-current animate-bounce motion-reduce:![animation-duration:2.5s]\",\n dotSizes[size]\n )}\n style={{ animationDelay: `${i * 150}ms` }}\n />\n ))}\n </div>\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:!animate-none\" 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-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"
17
17
  }
18
18
  ]
19
19
  }
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "path": "components/ui/button.tsx",
17
17
  "type": "registry:ui",
18
- "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n {\n variants: {\n variant: {\n default:\n \"bg-[var(--axis-button-bg-default)] text-[var(--axis-button-text-default)] hover:bg-[var(--axis-button-bg-hover)] active:bg-[var(--axis-button-bg-active)] focus-visible:ring-[var(--axis-button-border-focus)]\",\n secondary:\n \"bg-[var(--axis-button-secondary-bg-default)] text-[var(--axis-button-secondary-text-default)] hover:bg-[var(--axis-button-secondary-bg-hover)] active:bg-[var(--axis-button-secondary-bg-active)]\",\n ghost:\n \"bg-[var(--axis-button-ghost-bg-default)] text-[var(--axis-button-ghost-text-default)] hover:bg-[var(--axis-button-ghost-bg-hover)] active:bg-[var(--axis-button-ghost-bg-active)]\",\n destructive:\n \"bg-[var(--axis-button-destructive-bg-default)] text-[var(--axis-button-destructive-text-default)] hover:bg-[var(--axis-button-destructive-bg-hover)] active:bg-[var(--axis-button-destructive-bg-active)]\",\n outline:\n \"border border-[var(--axis-border-default)] bg-transparent hover:bg-[var(--axis-surface-secondary)] text-[var(--axis-text-primary)]\",\n link: \"text-[var(--axis-text-brand)] underline-offset-4 hover:underline\",\n },\n size: {\n sm: \"h-8 px-3 text-xs\",\n default: \"h-10 px-4 py-2\",\n lg: \"h-12 px-6 text-base\",\n icon: \"h-10 w-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\n/**\n * Button 컴포넌트 Props\n * @property variant - 버튼 스타일 변형 (default, secondary, ghost, destructive, outline, link)\n * @property size - 버튼 크기 (sm, default, lg, icon)\n */\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n /** Slot 패턴을 사용하여 자식 요소를 버튼으로 렌더링 */\n asChild?: boolean;\n}\n\n/** 다양한 스타일 변형과 크기를 지원하는 버튼 컴포넌트 */\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\";\n return (\n <Comp\n className={cn(buttonVariants({ variant, size, className }))}\n ref={ref}\n {...props}\n />\n );\n }\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
18
+ "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center whitespace-nowrap rounded-[var(--axis-radius-control,0.375rem)] text-[length:var(--axis-control-font-size-md,0.875rem)] leading-5 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n {\n variants: {\n variant: {\n default:\n \"bg-[var(--axis-button-bg-default)] text-[var(--axis-button-text-default)] hover:bg-[var(--axis-button-bg-hover)] active:bg-[var(--axis-button-bg-active)] focus-visible:ring-[var(--axis-button-border-focus)]\",\n secondary:\n \"bg-[var(--axis-button-secondary-bg-default)] text-[var(--axis-button-secondary-text-default)] hover:bg-[var(--axis-button-secondary-bg-hover)] active:bg-[var(--axis-button-secondary-bg-active)]\",\n ghost:\n \"bg-[var(--axis-button-ghost-bg-default)] text-[var(--axis-button-ghost-text-default)] hover:bg-[var(--axis-button-ghost-bg-hover)] active:bg-[var(--axis-button-ghost-bg-active)]\",\n destructive:\n \"bg-[var(--axis-button-destructive-bg-default)] text-[var(--axis-button-destructive-text-default)] hover:bg-[var(--axis-button-destructive-bg-hover)] active:bg-[var(--axis-button-destructive-bg-active)]\",\n outline:\n \"border border-[var(--axis-border-default)] bg-transparent hover:bg-[var(--axis-surface-secondary)] text-[var(--axis-text-primary)]\",\n link: \"text-[var(--axis-text-brand)] underline-offset-4 hover:underline\",\n },\n size: {\n sm: \"h-[var(--axis-control-h-sm,2rem)] px-3 text-[length:var(--axis-control-font-size-sm,0.75rem)] leading-4\",\n default: \"h-[var(--axis-control-h-md,2.5rem)] px-4 py-2\",\n lg: \"h-[var(--axis-control-h-lg,3rem)] px-6 text-[length:var(--axis-control-font-size-lg,1rem)] leading-6\",\n icon: \"h-[var(--axis-control-h-md,2.5rem)] w-[var(--axis-control-h-md,2.5rem)]\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\n/**\n * Button 컴포넌트 Props\n * @property variant - 버튼 스타일 변형 (default, secondary, ghost, destructive, outline, link)\n * @property size - 버튼 크기 (sm, default, lg, icon)\n */\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n /** Slot 패턴을 사용하여 자식 요소를 버튼으로 렌더링 */\n asChild?: boolean;\n}\n\n/** 다양한 스타일 변형과 크기를 지원하는 버튼 컴포넌트 */\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\";\n return (\n <Comp\n className={cn(buttonVariants({ variant, size, className }))}\n ref={ref}\n {...props}\n />\n );\n }\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
19
19
  }
20
20
  ]
21
21
  }
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "components/ui/card.tsx",
14
14
  "type": "registry:ui",
15
- "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/** Card 컴포넌트 Props */\nexport type CardProps = React.HTMLAttributes<HTMLDivElement>;\n\n/** 관련 콘텐츠를 그룹화하여 표시하는 카드 컨테이너 컴포넌트 */\nconst Card = React.forwardRef<HTMLDivElement, CardProps>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n \"rounded-lg border bg-[var(--axis-card-bg-default)] border-[var(--axis-card-border-default)] shadow-[shadow:var(--axis-card-shadow)]\",\n className\n )}\n {...props}\n />\n )\n);\nCard.displayName = \"Card\";\n\n/** 카드의 헤더 영역 (제목, 설명 등을 포함) */\nconst CardHeader = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n {...props}\n />\n));\nCardHeader.displayName = \"CardHeader\";\n\n/** 카드의 제목을 표시하는 컴포넌트 */\nconst CardTitle = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n <h3\n ref={ref}\n className={cn(\n \"text-2xl font-semibold leading-none tracking-tight text-[var(--axis-text-primary)]\",\n className\n )}\n {...props}\n />\n));\nCardTitle.displayName = \"CardTitle\";\n\n/** 카드의 부가 설명을 표시하는 컴포넌트 */\nconst CardDescription = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n <p\n ref={ref}\n className={cn(\"text-sm text-[var(--axis-text-secondary)]\", className)}\n {...props}\n />\n));\nCardDescription.displayName = \"CardDescription\";\n\n/** 카드의 본문 콘텐츠 영역 */\nconst CardContent = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n));\nCardContent.displayName = \"CardContent\";\n\n/** 카드의 하단 영역 (액션 버튼 등을 배치) */\nconst CardFooter = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"flex items-center p-6 pt-0\", className)}\n {...props}\n />\n));\nCardFooter.displayName = \"CardFooter\";\n\nexport {\n Card,\n CardHeader,\n CardFooter,\n CardTitle,\n CardDescription,\n CardContent,\n};\n"
15
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/** Card 컴포넌트 Props */\nexport type CardProps = React.HTMLAttributes<HTMLDivElement>;\n\n/** 관련 콘텐츠를 그룹화하여 표시하는 카드 컨테이너 컴포넌트 */\nconst Card = React.forwardRef<HTMLDivElement, CardProps>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n \"rounded-[var(--axis-radius-surface,0.5rem)] border bg-[var(--axis-card-bg-default)] border-[var(--axis-card-border-default)] shadow-[shadow:var(--axis-card-shadow)]\",\n className\n )}\n {...props}\n />\n )\n);\nCard.displayName = \"Card\";\n\n/** 카드의 헤더 영역 (제목, 설명 등을 포함) */\nconst CardHeader = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n {...props}\n />\n));\nCardHeader.displayName = \"CardHeader\";\n\n/** 카드의 제목을 표시하는 컴포넌트 */\nconst CardTitle = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n <h3\n ref={ref}\n className={cn(\n \"text-2xl font-semibold leading-none tracking-tight text-[var(--axis-text-primary)]\",\n className\n )}\n {...props}\n />\n));\nCardTitle.displayName = \"CardTitle\";\n\n/** 카드의 부가 설명을 표시하는 컴포넌트 */\nconst CardDescription = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n <p\n ref={ref}\n className={cn(\"text-sm text-[var(--axis-text-secondary)]\", className)}\n {...props}\n />\n));\nCardDescription.displayName = \"CardDescription\";\n\n/** 카드의 본문 콘텐츠 영역 */\nconst CardContent = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n));\nCardContent.displayName = \"CardContent\";\n\n/** 카드의 하단 영역 (액션 버튼 등을 배치) */\nconst CardFooter = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\"flex items-center p-6 pt-0\", className)}\n {...props}\n />\n));\nCardFooter.displayName = \"CardFooter\";\n\nexport {\n Card,\n CardHeader,\n CardFooter,\n CardTitle,\n CardDescription,\n CardContent,\n};\n"
16
16
  }
17
17
  ]
18
18
  }
@@ -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-lg\",\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": "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"
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.6.2",
4
+ "version": "1.7.0",
5
5
  "items": [
6
6
  {
7
7
  "name": "utils",
@@ -15,14 +15,14 @@
15
15
  "type": "registry:ui",
16
16
  "title": "Button",
17
17
  "description": "AXIS 버튼 컴포넌트 - 다양한 variant와 size 지원",
18
- "sha256": "14cb5f00cdbe68fba7e07a3436c5bf4cbf865c4de75d01b1c4ef978649778b25"
18
+ "sha256": "d91188d37506555c183238fe49ec938dd36244086b9ec6c21d3c92fafdfae510"
19
19
  },
20
20
  {
21
21
  "name": "input",
22
22
  "type": "registry:ui",
23
23
  "title": "Input",
24
24
  "description": "AXIS 입력 필드 컴포넌트",
25
- "sha256": "99ca683f4c3821ab74ef40b9d95bf6d3b499841934cf731b3e69b360e58db9c0"
25
+ "sha256": "d11f23e2c4b4bafb51368d910cc0f796357162172ef644fa17e04909df2de46f"
26
26
  },
27
27
  {
28
28
  "name": "label",
@@ -36,7 +36,7 @@
36
36
  "type": "registry:ui",
37
37
  "title": "Card",
38
38
  "description": "AXIS 카드 컴포넌트 - Header, Content, Footer 포함",
39
- "sha256": "a12d3e5468b0247e564d09040bc4ba0cb818406244f11a6f0d781e54ab640be7"
39
+ "sha256": "34ae5904281cef4468c283d33183e4319da87b2f390afe4fe138b0037b231b36"
40
40
  },
41
41
  {
42
42
  "name": "badge",
@@ -57,14 +57,14 @@
57
57
  "type": "registry:ui",
58
58
  "title": "Dialog",
59
59
  "description": "AXIS 다이얼로그 컴포넌트 - 모달 UI",
60
- "sha256": "cd2307dca13b830e0f34cfbe6ce800f1d4ee3a5a373bf9bf9dd7087758ea82b3"
60
+ "sha256": "17453188103044027ccb4379bc0f8163a73b4edb7f119e1144831c60a1c5eb16"
61
61
  },
62
62
  {
63
63
  "name": "select",
64
64
  "type": "registry:ui",
65
65
  "title": "Select",
66
66
  "description": "AXIS 셀렉트 컴포넌트 - 드롭다운 선택",
67
- "sha256": "3680a718874d05f153d8b46f8a9affbee9fe9bcf0509e20f4d6250c4e3307f81"
67
+ "sha256": "e5bb8064708798ae0cab6f8ee4b3ede34e334c5b16af6d54640ca875a4fb0db2"
68
68
  },
69
69
  {
70
70
  "name": "tabs",
@@ -92,14 +92,14 @@
92
92
  "type": "registry:ui",
93
93
  "title": "Run Progress",
94
94
  "description": "AI 에이전트 실행 진행률 표시 컴포넌트",
95
- "sha256": "bd197371ac791da7319dc72551d7fe86e020ae7f756852b40ede9123aa5d2186"
95
+ "sha256": "4d7611b71d67ccf57ce89b1a3ffdf954575e22e8dd8ff13aa86b7710367e8b24"
96
96
  },
97
97
  {
98
98
  "name": "agentic-streaming-text",
99
99
  "type": "registry:ui",
100
100
  "title": "Streaming Text",
101
101
  "description": "LLM 스트리밍 텍스트 출력 컴포넌트",
102
- "sha256": "c40ac5afe7fd49b22bcc66aeebb77d23d7d8dd6b4b36f8fe4472750eaeae14d9"
102
+ "sha256": "cddc2676622d324cb3f8d8bd1fddb97caa4808350cccaf8b6fde754a8ac8780a"
103
103
  },
104
104
  {
105
105
  "name": "agentic-approval-card",
@@ -113,14 +113,14 @@
113
113
  "type": "registry:ui",
114
114
  "title": "Step Timeline",
115
115
  "description": "에이전트 실행 단계 타임라인 컴포넌트",
116
- "sha256": "2c93015b0ae012b517781c555297a79cc4c5a19842ab55ef7a847db5002c6875"
116
+ "sha256": "cd6719280a88c471f6d1e20493f07b5d1055d37bf899129462ac89df6dfe883c"
117
117
  },
118
118
  {
119
119
  "name": "agentic-thinking-indicator",
120
120
  "type": "registry:ui",
121
121
  "title": "Thinking Indicator",
122
122
  "description": "AI 사고 과정 표시 컴포넌트",
123
- "sha256": "0878b1f0b83b199239f4159beafc9a477ea1ad6c27a0587395e0e05dae85b3e6"
123
+ "sha256": "77d5b051c48416067927ba269bf8546e65f8e3942de1fca81d40f159e01ed4b1"
124
124
  },
125
125
  {
126
126
  "name": "agentic-source-panel",
@@ -155,14 +155,14 @@
155
155
  "type": "registry:ui",
156
156
  "title": "Tool Call Card",
157
157
  "description": "도구 호출 결과 표시 카드 컴포넌트",
158
- "sha256": "b31a198cbb8aa9a84dd7475724abb1c75cfc57e98ce27ab6835b09f320f1f4a2"
158
+ "sha256": "22f55b62257075be3e5bb393aea8077eb26cf913ae0595802213769025492b1f"
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": "9d5bebd9102dabc7afa8eb80c326abdae30ad174d1afd35cdccc9ce321ec90b9"
165
+ "sha256": "9833745acfd486a8375e4ede01f51e9e440dd3f7f06a52850c82f951d14b3fec"
166
166
  },
167
167
  {
168
168
  "name": "agentic-feedback-buttons",
@@ -197,7 +197,7 @@
197
197
  "type": "registry:ui",
198
198
  "title": "Plan Card",
199
199
  "description": "AI 실행 계획 표시/승인 카드 컴포넌트",
200
- "sha256": "c6d2f255b90b302966abd50091190b2af7e979bfe95e54141c538915b4b645e9"
200
+ "sha256": "ed6e7145a338b2b59a05aa3f829cf3f95e391d73d5a66c685cbd9349271e26da"
201
201
  },
202
202
  {
203
203
  "name": "agentic-attachment-card",
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "components/ui/input.tsx",
14
14
  "type": "registry:ui",
15
- "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/** Input 컴포넌트의 Props 인터페이스 */\nexport interface InputProps\n extends React.InputHTMLAttributes<HTMLInputElement> {\n /** 에러 상태 표시. true일 경우 테두리와 포커스 링이 에러 색상으로 변경된다. */\n error?: boolean;\n}\n\n/**\n * 텍스트 입력 필드 컴포넌트.\n * 다양한 HTML input 타입을 지원하며, 에러 상태와 비활성화 상태 스타일을 포함한다.\n */\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n ({ className, type, error, ...props }, ref) => {\n return (\n <input\n type={type}\n className={cn(\n \"flex h-10 w-full rounded-md border px-3 py-2 text-sm transition-colors\",\n \"bg-[var(--axis-input-bg-default)] text-[var(--axis-input-text-default)]\",\n \"border-[var(--axis-input-border-default)]\",\n \"placeholder:text-[var(--axis-input-text-placeholder)]\",\n \"hover:border-[var(--axis-input-border-hover)]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--axis-input-border-focus)] focus-visible:ring-offset-2\",\n \"disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-[var(--axis-input-bg-disabled)]\",\n \"file:border-0 file:bg-transparent file:text-sm file:font-medium\",\n error && \"border-[var(--axis-input-border-error)] focus-visible:ring-[var(--axis-input-border-error)]\",\n className\n )}\n ref={ref}\n {...props}\n />\n );\n }\n);\nInput.displayName = \"Input\";\n\nexport { Input };\n"
15
+ "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/** Input 컴포넌트의 Props 인터페이스 */\nexport interface InputProps\n extends React.InputHTMLAttributes<HTMLInputElement> {\n /** 에러 상태 표시. true일 경우 테두리와 포커스 링이 에러 색상으로 변경된다. */\n error?: boolean;\n}\n\n/**\n * 텍스트 입력 필드 컴포넌트.\n * 다양한 HTML input 타입을 지원하며, 에러 상태와 비활성화 상태 스타일을 포함한다.\n */\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n ({ className, type, error, ...props }, ref) => {\n return (\n <input\n type={type}\n className={cn(\n \"flex h-[var(--axis-field-h,2.5rem)] w-full rounded-[var(--axis-radius-field,0.375rem)] border px-3 py-2 text-[length:var(--axis-control-font-size-md,0.875rem)] leading-5 transition-colors\",\n \"bg-[var(--axis-input-bg-default)] text-[var(--axis-input-text-default)]\",\n \"border-[var(--axis-input-border-default)]\",\n \"placeholder:text-[var(--axis-input-text-placeholder)]\",\n \"hover:border-[var(--axis-input-border-hover)]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--axis-input-border-focus)] focus-visible:ring-offset-2\",\n \"disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-[var(--axis-input-bg-disabled)]\",\n \"file:border-0 file:bg-transparent file:text-sm file:font-medium\",\n error && \"border-[var(--axis-input-border-error)] focus-visible:ring-[var(--axis-input-border-error)]\",\n className\n )}\n ref={ref}\n {...props}\n />\n );\n }\n);\nInput.displayName = \"Input\";\n\nexport { Input };\n"
16
16
  }
17
17
  ]
18
18
  }
@@ -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.6.2",
5
+ "version": "1.7.0",
6
6
  "items": [
7
7
  {
8
8
  "name": "utils",
@@ -16,14 +16,14 @@
16
16
  "type": "registry:ui",
17
17
  "title": "Button",
18
18
  "description": "AXIS 버튼 컴포넌트 - 다양한 variant와 size 지원",
19
- "sha256": "14cb5f00cdbe68fba7e07a3436c5bf4cbf865c4de75d01b1c4ef978649778b25"
19
+ "sha256": "d91188d37506555c183238fe49ec938dd36244086b9ec6c21d3c92fafdfae510"
20
20
  },
21
21
  {
22
22
  "name": "input",
23
23
  "type": "registry:ui",
24
24
  "title": "Input",
25
25
  "description": "AXIS 입력 필드 컴포넌트",
26
- "sha256": "99ca683f4c3821ab74ef40b9d95bf6d3b499841934cf731b3e69b360e58db9c0"
26
+ "sha256": "d11f23e2c4b4bafb51368d910cc0f796357162172ef644fa17e04909df2de46f"
27
27
  },
28
28
  {
29
29
  "name": "label",
@@ -37,7 +37,7 @@
37
37
  "type": "registry:ui",
38
38
  "title": "Card",
39
39
  "description": "AXIS 카드 컴포넌트 - Header, Content, Footer 포함",
40
- "sha256": "a12d3e5468b0247e564d09040bc4ba0cb818406244f11a6f0d781e54ab640be7"
40
+ "sha256": "34ae5904281cef4468c283d33183e4319da87b2f390afe4fe138b0037b231b36"
41
41
  },
42
42
  {
43
43
  "name": "badge",
@@ -58,14 +58,14 @@
58
58
  "type": "registry:ui",
59
59
  "title": "Dialog",
60
60
  "description": "AXIS 다이얼로그 컴포넌트 - 모달 UI",
61
- "sha256": "cd2307dca13b830e0f34cfbe6ce800f1d4ee3a5a373bf9bf9dd7087758ea82b3"
61
+ "sha256": "17453188103044027ccb4379bc0f8163a73b4edb7f119e1144831c60a1c5eb16"
62
62
  },
63
63
  {
64
64
  "name": "select",
65
65
  "type": "registry:ui",
66
66
  "title": "Select",
67
67
  "description": "AXIS 셀렉트 컴포넌트 - 드롭다운 선택",
68
- "sha256": "3680a718874d05f153d8b46f8a9affbee9fe9bcf0509e20f4d6250c4e3307f81"
68
+ "sha256": "e5bb8064708798ae0cab6f8ee4b3ede34e334c5b16af6d54640ca875a4fb0db2"
69
69
  },
70
70
  {
71
71
  "name": "tabs",
@@ -93,14 +93,14 @@
93
93
  "type": "registry:ui",
94
94
  "title": "Run Progress",
95
95
  "description": "AI 에이전트 실행 진행률 표시 컴포넌트",
96
- "sha256": "bd197371ac791da7319dc72551d7fe86e020ae7f756852b40ede9123aa5d2186"
96
+ "sha256": "4d7611b71d67ccf57ce89b1a3ffdf954575e22e8dd8ff13aa86b7710367e8b24"
97
97
  },
98
98
  {
99
99
  "name": "agentic-streaming-text",
100
100
  "type": "registry:ui",
101
101
  "title": "Streaming Text",
102
102
  "description": "LLM 스트리밍 텍스트 출력 컴포넌트",
103
- "sha256": "c40ac5afe7fd49b22bcc66aeebb77d23d7d8dd6b4b36f8fe4472750eaeae14d9"
103
+ "sha256": "cddc2676622d324cb3f8d8bd1fddb97caa4808350cccaf8b6fde754a8ac8780a"
104
104
  },
105
105
  {
106
106
  "name": "agentic-approval-card",
@@ -114,14 +114,14 @@
114
114
  "type": "registry:ui",
115
115
  "title": "Step Timeline",
116
116
  "description": "에이전트 실행 단계 타임라인 컴포넌트",
117
- "sha256": "2c93015b0ae012b517781c555297a79cc4c5a19842ab55ef7a847db5002c6875"
117
+ "sha256": "cd6719280a88c471f6d1e20493f07b5d1055d37bf899129462ac89df6dfe883c"
118
118
  },
119
119
  {
120
120
  "name": "agentic-thinking-indicator",
121
121
  "type": "registry:ui",
122
122
  "title": "Thinking Indicator",
123
123
  "description": "AI 사고 과정 표시 컴포넌트",
124
- "sha256": "0878b1f0b83b199239f4159beafc9a477ea1ad6c27a0587395e0e05dae85b3e6"
124
+ "sha256": "77d5b051c48416067927ba269bf8546e65f8e3942de1fca81d40f159e01ed4b1"
125
125
  },
126
126
  {
127
127
  "name": "agentic-source-panel",
@@ -156,14 +156,14 @@
156
156
  "type": "registry:ui",
157
157
  "title": "Tool Call Card",
158
158
  "description": "도구 호출 결과 표시 카드 컴포넌트",
159
- "sha256": "b31a198cbb8aa9a84dd7475724abb1c75cfc57e98ce27ab6835b09f320f1f4a2"
159
+ "sha256": "22f55b62257075be3e5bb393aea8077eb26cf913ae0595802213769025492b1f"
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": "9d5bebd9102dabc7afa8eb80c326abdae30ad174d1afd35cdccc9ce321ec90b9"
166
+ "sha256": "9833745acfd486a8375e4ede01f51e9e440dd3f7f06a52850c82f951d14b3fec"
167
167
  },
168
168
  {
169
169
  "name": "agentic-feedback-buttons",
@@ -198,7 +198,7 @@
198
198
  "type": "registry:ui",
199
199
  "title": "Plan Card",
200
200
  "description": "AI 실행 계획 표시/승인 카드 컴포넌트",
201
- "sha256": "c6d2f255b90b302966abd50091190b2af7e979bfe95e54141c538915b4b645e9"
201
+ "sha256": "ed6e7145a338b2b59a05aa3f829cf3f95e391d73d5a66c685cbd9349271e26da"
202
202
  },
203
203
  {
204
204
  "name": "agentic-attachment-card",
@@ -14,7 +14,7 @@
14
14
  {
15
15
  "path": "components/ui/select.tsx",
16
16
  "type": "registry:ui",
17
- "content": "import * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/** 셀렉트의 루트 컴포넌트. 열림/닫힘 및 선택 상태를 관리한다. */\nconst Select = SelectPrimitive.Root;\n\n/** 셀렉트 옵션들을 논리적으로 그룹화하는 컴포넌트 */\nconst SelectGroup = SelectPrimitive.Group;\n\n/** 셀렉트 트리거 내에서 현재 선택된 값을 표시하는 컴포넌트 */\nconst SelectValue = SelectPrimitive.Value;\n\n/** 셀렉트 드롭다운을 여는 트리거 버튼 컴포넌트. 선택된 값과 하향 화살표 아이콘을 표시한다. */\nconst SelectTrigger = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n <SelectPrimitive.Trigger\n ref={ref}\n className={cn(\n \"flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm\",\n \"bg-[var(--axis-input-bg-default)] border-[var(--axis-input-border-default)]\",\n \"placeholder:text-[var(--axis-input-text-placeholder)]\",\n \"hover:border-[var(--axis-input-border-hover)]\",\n \"focus:outline-none focus:ring-2 focus:ring-[var(--axis-input-border-focus)] focus:ring-offset-2\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"[&>span]:line-clamp-1\",\n className\n )}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <svg\n className=\"h-4 w-4 opacity-50\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 9l-7 7-7-7\"\n />\n </svg>\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = \"SelectTrigger\";\n\n/** 셀렉트 드롭다운의 본문 콘텐츠 컴포넌트. 포털을 통해 렌더링되며 진입/퇴장 애니메이션을 포함한다. */\nconst SelectContent = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n ref={ref}\n className={cn(\n \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\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 motion-reduce:!animate-none\",\n position === \"popper\" &&\n \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n className\n )}\n position={position}\n {...props}\n >\n <SelectPrimitive.Viewport\n className={cn(\n \"p-1\",\n position === \"popper\" &&\n \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\"\n )}\n >\n {children}\n </SelectPrimitive.Viewport>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n));\nSelectContent.displayName = \"SelectContent\";\n\n/** 셀렉트 옵션 그룹의 레이블 컴포넌트. 비선택 텍스트로 그룹 제목을 표시한다. */\nconst SelectLabel = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Label>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.Label\n ref={ref}\n className={cn(\n \"py-1.5 pl-8 pr-2 text-sm font-semibold text-[var(--axis-text-secondary)]\",\n className\n )}\n {...props}\n />\n));\nSelectLabel.displayName = \"SelectLabel\";\n\n/** 셀렉트 드롭다운의 개별 선택 항목 컴포넌트. 선택 시 좌측에 체크 아이콘이 표시된다. */\nconst SelectItem = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n <SelectPrimitive.Item\n ref={ref}\n className={cn(\n \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none\",\n \"text-[var(--axis-text-primary)]\",\n \"focus:bg-[var(--axis-surface-secondary)] focus:text-[var(--axis-text-primary)]\",\n \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className\n )}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <SelectPrimitive.ItemIndicator>\n <svg\n className=\"h-4 w-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </SelectPrimitive.ItemIndicator>\n </span>\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n </SelectPrimitive.Item>\n));\nSelectItem.displayName = \"SelectItem\";\n\n/** 셀렉트 옵션 사이의 구분선 컴포넌트 */\nconst SelectSeparator = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.Separator\n ref={ref}\n className={cn(\"-mx-1 my-1 h-px bg-[var(--axis-border-default)]\", className)}\n {...props}\n />\n));\nSelectSeparator.displayName = \"SelectSeparator\";\n\nexport {\n Select,\n SelectGroup,\n SelectValue,\n SelectTrigger,\n SelectContent,\n SelectLabel,\n SelectItem,\n SelectSeparator,\n};\n"
17
+ "content": "import * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { cn } from \"@/registry/axis/lib/utils\";\n\n/** 셀렉트의 루트 컴포넌트. 열림/닫힘 및 선택 상태를 관리한다. */\nconst Select = SelectPrimitive.Root;\n\n/** 셀렉트 옵션들을 논리적으로 그룹화하는 컴포넌트 */\nconst SelectGroup = SelectPrimitive.Group;\n\n/** 셀렉트 트리거 내에서 현재 선택된 값을 표시하는 컴포넌트 */\nconst SelectValue = SelectPrimitive.Value;\n\n/** 셀렉트 드롭다운을 여는 트리거 버튼 컴포넌트. 선택된 값과 하향 화살표 아이콘을 표시한다. */\nconst SelectTrigger = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n <SelectPrimitive.Trigger\n ref={ref}\n className={cn(\n \"flex h-[var(--axis-field-h,2.5rem)] w-full items-center justify-between rounded-[var(--axis-radius-field,0.375rem)] border px-3 py-2 text-sm\",\n \"bg-[var(--axis-input-bg-default)] border-[var(--axis-input-border-default)]\",\n \"placeholder:text-[var(--axis-input-text-placeholder)]\",\n \"hover:border-[var(--axis-input-border-hover)]\",\n \"focus:outline-none focus:ring-2 focus:ring-[var(--axis-input-border-focus)] focus:ring-offset-2\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"[&>span]:line-clamp-1\",\n className\n )}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <svg\n className=\"h-4 w-4 opacity-50\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 9l-7 7-7-7\"\n />\n </svg>\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = \"SelectTrigger\";\n\n/** 셀렉트 드롭다운의 본문 콘텐츠 컴포넌트. 포털을 통해 렌더링되며 진입/퇴장 애니메이션을 포함한다. */\nconst SelectContent = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n ref={ref}\n className={cn(\n \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md\",\n \"bg-[var(--axis-surface-default)] border-[var(--axis-border-default)]\",\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 motion-reduce:!animate-none\",\n position === \"popper\" &&\n \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n className\n )}\n position={position}\n {...props}\n >\n <SelectPrimitive.Viewport\n className={cn(\n \"p-1\",\n position === \"popper\" &&\n \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\"\n )}\n >\n {children}\n </SelectPrimitive.Viewport>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n));\nSelectContent.displayName = \"SelectContent\";\n\n/** 셀렉트 옵션 그룹의 레이블 컴포넌트. 비선택 텍스트로 그룹 제목을 표시한다. */\nconst SelectLabel = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Label>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.Label\n ref={ref}\n className={cn(\n \"py-1.5 pl-8 pr-2 text-sm font-semibold text-[var(--axis-text-secondary)]\",\n className\n )}\n {...props}\n />\n));\nSelectLabel.displayName = \"SelectLabel\";\n\n/** 셀렉트 드롭다운의 개별 선택 항목 컴포넌트. 선택 시 좌측에 체크 아이콘이 표시된다. */\nconst SelectItem = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n <SelectPrimitive.Item\n ref={ref}\n className={cn(\n \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none\",\n \"text-[var(--axis-text-primary)]\",\n \"focus:bg-[var(--axis-surface-secondary)] focus:text-[var(--axis-text-primary)]\",\n \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className\n )}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <SelectPrimitive.ItemIndicator>\n <svg\n className=\"h-4 w-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </SelectPrimitive.ItemIndicator>\n </span>\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n </SelectPrimitive.Item>\n));\nSelectItem.displayName = \"SelectItem\";\n\n/** 셀렉트 옵션 사이의 구분선 컴포넌트 */\nconst SelectSeparator = React.forwardRef<\n React.ElementRef<typeof SelectPrimitive.Separator>,\n React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.Separator\n ref={ref}\n className={cn(\"-mx-1 my-1 h-px bg-[var(--axis-border-default)]\", className)}\n {...props}\n />\n));\nSelectSeparator.displayName = \"SelectSeparator\";\n\nexport {\n Select,\n SelectGroup,\n SelectValue,\n SelectTrigger,\n SelectContent,\n SelectLabel,\n SelectItem,\n SelectSeparator,\n};\n"
18
18
  }
19
19
  ]
20
20
  }