{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "author": "DesignX ", "name": "approval-card", "type": "registry:component", "title": "Approval Card", "description": "Question and review cards an agent uses to ask for a decision mid-task.", "dependencies": [ "lucide-react@^0.577.0", "motion@^14.0.0" ], "registryDependencies": [ "@dx/action-swap-roll", "@dx/lib-ease", "@dx/motion-button", "@dx/motion-checkbox", "@dx/motion-input", "@dx/radio", "@dx/utils" ], "files": [ { "path": "components/agents/approval-card/index.tsx", "type": "registry:component", "target": "components/agents/approval-card/index.tsx", "content": "import {\n ArrowLeft,\n ArrowRight,\n Check,\n CircleHelp,\n LoaderCircle,\n MessageSquareText,\n X,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { AgentDisclosure } from \"@/components/agents/agent-disclosure\";\nimport { ActionSwapRollText } from \"@/components/motion/action-swap-roll\";\nimport { Button } from \"@/components/motion/button\";\nimport { Checkbox } from \"@/components/motion/checkbox\";\nimport { Input } from \"@/components/motion/input\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/motion/radio\";\nimport { EASE_OUT, SPRING_SWAP } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n ApprovalCardAnswer,\n ApprovalCardAnswers,\n ApprovalCardProps,\n ApprovalCardQuestion,\n ApprovalCardStatus,\n} from \"./types\";\n\nexport type {\n ApprovalCardAnswer,\n ApprovalCardAnswers,\n ApprovalCardOption,\n ApprovalCardProps,\n ApprovalCardQuestion,\n ApprovalCardStatus,\n} from \"./types\";\n\nconst EMPTY_ANSWER: ApprovalCardAnswer = { selected: [], custom: \"\" };\n\nfunction getStatusLabel(status: ApprovalCardStatus) {\n if (status === \"submitting\") return \"Submitting\";\n if (status === \"approved\") return \"Approved\";\n if (status === \"rejected\") return \"Rejected\";\n if (status === \"changes-requested\") return \"Changes requested\";\n if (status === \"answered\") return \"Response submitted\";\n return \"Input required\";\n}\n\nfunction getStatusClass(status: ApprovalCardStatus) {\n if (status === \"approved\" || status === \"answered\") {\n return \"text-emerald-600 dark:text-emerald-400\";\n }\n if (status === \"rejected\") return \"text-rose-600 dark:text-rose-400\";\n if (status === \"changes-requested\") {\n return \"text-amber-600 dark:text-amber-400\";\n }\n return \"text-muted-foreground\";\n}\n\nfunction getStatusBadgeClass(status: ApprovalCardStatus) {\n if (status === \"pending\" || status === \"changes-requested\") {\n return \"border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400\";\n }\n if (status === \"submitting\") {\n return \"border-blue-500/30 bg-blue-500/10 text-blue-600 dark:text-blue-400\";\n }\n if (status === \"approved\" || status === \"answered\") {\n return \"border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400\";\n }\n return \"border-rose-500/30 bg-rose-500/10 text-rose-600 dark:text-rose-400\";\n}\n\nfunction isAnswered(answer: ApprovalCardAnswer) {\n return answer.selected.length > 0 || Boolean(answer.custom?.trim());\n}\n\nfunction QuestionOptions({\n question,\n answer,\n disabled,\n onChange,\n onSingleSelect,\n}: {\n question: ApprovalCardQuestion;\n answer: ApprovalCardAnswer;\n disabled: boolean;\n onChange: (answer: ApprovalCardAnswer) => void;\n onSingleSelect?: () => void;\n}) {\n const custom = answer.custom ?? \"\";\n\n return (\n
\n {question.options?.length ? (\n question.multiple ? (\n
\n {question.options.map((option) => (\n \n onChange({\n ...answer,\n selected: checked\n ? [...answer.selected, option.value]\n : answer.selected.filter((value) => value !== option.value),\n })\n }\n className=\"min-h-9 rounded-lg px-1.5 py-1\"\n />\n ))}\n
\n ) : (\n {\n onChange({ selected: [value], custom: \"\" });\n onSingleSelect?.();\n }}\n className=\"gap-0.5\"\n >\n {question.options.map((option) => (\n \n ))}\n \n )\n ) : null}\n\n {question.allowCustom ? (\n \n onChange({\n selected: question.multiple ? answer.selected : [],\n custom: value,\n })\n }\n className={cn(\"p-0.5\", question.options?.length && \"mt-1.5\")}\n classNames={{\n field:\n \"h-10 rounded-xl border-0 bg-background/70 focus-within:bg-background\",\n input: \"px-3 text-sm\",\n }}\n />\n ) : null}\n
\n );\n}\n\nfunction ProgressDots({ current, ids }: { current: number; ids: string[] }) {\n return (\n \n \n Question {current + 1} of {ids.length}\n \n {ids.map((id, index) => (\n \n ))}\n \n );\n}\n\nexport function ApprovalCard({\n title = \"Approval required\",\n description,\n children,\n questions = [],\n status = \"pending\",\n answers,\n defaultAnswers = {},\n onAnswersChange,\n step,\n defaultStep = 0,\n onStepChange,\n onSubmit,\n onApprove,\n onReject,\n onRequestChanges,\n onDismiss,\n approveLabel = \"Approve\",\n submitLabel = \"Submit response\",\n result,\n className,\n}: ApprovalCardProps) {\n const reduce = useReducedMotion() ?? false;\n const [internalAnswers, setInternalAnswers] =\n useState(defaultAnswers);\n const [internalStep, setInternalStep] = useState(defaultStep);\n const autoAdvanceTimer = useRef(undefined);\n const currentAnswers = answers ?? internalAnswers;\n const currentStep = Math.min(\n Math.max(0, step ?? internalStep),\n Math.max(0, questions.length - 1),\n );\n const question = questions[currentStep];\n const questionMode = questions.length > 0;\n const multipleQuestions = questions.length > 1;\n const pending = status === \"pending\";\n const busy = status === \"submitting\";\n const interactive = pending || busy;\n const currentAnswer = question\n ? (currentAnswers[question.id] ?? EMPTY_ANSWER)\n : EMPTY_ANSWER;\n const displayTitle = question?.title ?? title;\n const titleKey = question?.id ?? String(status);\n const statusLabel = getStatusLabel(status);\n\n const clearAutoAdvance = useCallback(() => {\n if (autoAdvanceTimer.current === undefined) return;\n window.clearTimeout(autoAdvanceTimer.current);\n autoAdvanceTimer.current = undefined;\n }, []);\n\n useEffect(() => clearAutoAdvance, [clearAutoAdvance]);\n\n const setAnswers = useCallback(\n (next: ApprovalCardAnswers) => {\n if (answers === undefined) setInternalAnswers(next);\n onAnswersChange?.(next);\n },\n [answers, onAnswersChange],\n );\n\n const setStep = (next: number) => {\n clearAutoAdvance();\n if (step === undefined) setInternalStep(next);\n onStepChange?.(next);\n };\n\n const updateCurrentAnswer = (next: ApprovalCardAnswer) => {\n if (!question) return;\n setAnswers({ ...currentAnswers, [question.id]: next });\n };\n\n const continueQuestion = () => {\n if (currentStep < questions.length - 1) {\n setStep(currentStep + 1);\n return;\n }\n onSubmit?.(currentAnswers);\n };\n\n const queueAutoAdvance = () => {\n if (\n !question ||\n question.multiple ||\n question.autoAdvance === false ||\n currentStep >= questions.length - 1 ||\n busy\n ) {\n return;\n }\n\n clearAutoAdvance();\n autoAdvanceTimer.current = window.setTimeout(() => {\n setStep(currentStep + 1);\n }, 240);\n };\n\n return (\n \n
\n \n {busy ? (\n \n ) : interactive ? (\n questionMode ? (\n \n ) : (\n \n )\n ) : status === \"rejected\" ? (\n \n ) : (\n \n )}\n \n\n
\n
\n

\n \n {displayTitle}\n \n

\n {questionMode && interactive ? (\n multipleQuestions ? (\n \n {currentStep + 1}/{questions.length}\n \n ) : null\n ) : (\n \n {statusLabel}\n \n )}\n {onDismiss ? (\n \n \n \n ) : null}\n
\n\n \n {questionMode && question ? (\n \n \n {question.description ? (\n

\n {question.description}\n

\n ) : null}\n \n \n
\n ) : (\n
\n {description ? (\n

\n {description}\n

\n ) : null}\n {children ?
{children}
: null}\n
\n )}\n\n {questionMode ? (\n
\n {multipleQuestions ? (\n <>\n setStep(currentStep - 1)}\n className=\"rounded-full\"\n >\n \n \n item.id)}\n />\n \n ) : null}\n \n {busy ? (\n \n ) : currentStep === questions.length - 1 ? (\n <>\n {submitLabel}\n \n \n ) : (\n \n )}\n \n
\n ) : (\n
\n \n {approveLabel}\n \n {onRequestChanges ? (\n \n Request changes\n \n ) : null}\n {onReject ? (\n \n Reject\n \n ) : null}\n
\n )}\n
\n\n {!interactive ? (\n

\n {result ?? statusLabel}\n

\n ) : null}\n
\n
\n \n );\n}\n" }, { "path": "components/agents/agent-disclosure.tsx", "type": "registry:component", "target": "components/agents/agent-disclosure.tsx", "content": "import { motion, type HTMLMotionProps, useReducedMotion } from \"motion/react\";\nimport type { CSSProperties } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface AgentDisclosureProps\n extends Omit, \"animate\" | \"initial\"> {\n open: boolean;\n openHeight?: CSSProperties[\"height\"];\n}\n\n/** Shared transform-only reveal for collapsible agent content. */\nexport function AgentDisclosure({\n open,\n openHeight = \"auto\",\n className,\n style,\n transition,\n ...props\n}: AgentDisclosureProps) {\n const reduce = useReducedMotion() ?? false;\n\n return (\n \n );\n}\n" }, { "path": "components/agents/approval-card/types.ts", "type": "registry:component", "target": "components/agents/approval-card/types.ts", "content": "import type { ReactNode } from \"react\";\n\nexport type ApprovalCardStatus =\n | \"pending\"\n | \"submitting\"\n | \"approved\"\n | \"rejected\"\n | \"changes-requested\"\n | \"answered\";\n\nexport interface ApprovalCardOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\nexport interface ApprovalCardQuestion {\n id: string;\n title: ReactNode;\n description?: ReactNode;\n options?: ApprovalCardOption[];\n multiple?: boolean;\n autoAdvance?: boolean;\n allowCustom?: boolean;\n customPlaceholder?: string;\n}\n\nexport interface ApprovalCardAnswer {\n selected: string[];\n custom?: string;\n}\n\nexport type ApprovalCardAnswers = Record;\n\nexport interface ApprovalCardProps {\n title?: ReactNode;\n description?: ReactNode;\n children?: ReactNode;\n questions?: ApprovalCardQuestion[];\n status?: ApprovalCardStatus;\n answers?: ApprovalCardAnswers;\n defaultAnswers?: ApprovalCardAnswers;\n onAnswersChange?: (answers: ApprovalCardAnswers) => void;\n step?: number;\n defaultStep?: number;\n onStepChange?: (step: number) => void;\n onSubmit?: (answers: ApprovalCardAnswers) => void;\n onApprove?: () => void;\n onReject?: () => void;\n onRequestChanges?: () => void;\n onDismiss?: () => void;\n approveLabel?: ReactNode;\n submitLabel?: ReactNode;\n result?: ReactNode;\n className?: string;\n}\n" } ], "categories": [ "agents" ], "meta": { "group": "agents", "docs": "https://dxuireact.com/docs/components/approval-card" } }