{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"plan-review","type":"registry:ui","title":"PlanReview","description":"The trust boundary made visible: per-operation accept\/reject on a proposed change plan. Never auto-applies.","package":"teachers-aid-ui","dependencies":["@particle-academy\/react-fancy"],"registryDependencies":["types"],"files":[{"path":"components\/fancy\/plan-review\/PlanReview.tsx","content":"import { Badge, Button, Heading, Text, type Color } from '@particle-academy\/react-fancy';\r\nimport { useState } from 'react';\r\nimport type { ChangeAction, ChangePlan } from '..\/types';\r\n\r\nconst ACTION_COLOR: Record<ChangeAction, Color> = {\r\n    create: 'green',\r\n    update: 'blue',\r\n    delete: 'red',\r\n};\r\n\r\nexport interface PlanReviewProps {\r\n    plan?: ChangePlan | null;\r\n    onApply: () => void;\r\n    onDiscard: () => void;\r\n    busy?: boolean;\r\n    \/** Shown in the footer. Set to null to drop the note entirely. *\/\r\n    draftNote?: React.ReactNode;\r\n}\r\n\r\n\/**\r\n * The approval surface \u2014 the only place a proposal becomes rows.\r\n *\r\n * Every operation is inspectable before anything is written, raw attributes\r\n * included. A reviewer who cannot see what they are approving is not really\r\n * approving it, and \"Apply\" on an opaque list is just a slower yes.\r\n *\/\r\nexport function PlanReview({ plan, onApply, onDiscard, busy, draftNote }: PlanReviewProps) {\r\n    const [expanded, setExpanded] = useState<number | null>(null);\r\n\r\n    if (!plan || !plan.operations?.length) return null;\r\n\r\n    const summary = Object.entries(plan.summary ?? {})\r\n        .map(([key, count]) => `${count} \u00d7 ${key.replace('_', ' ')}`)\r\n        .join(' \u00b7 ');\r\n\r\n    return (\r\n        <section className=\"rounded-lg border border-amber-300 bg-amber-50\/60 overflow-hidden\">\r\n            <header className=\"flex flex-wrap items-center justify-between gap-3 border-b border-amber-200 bg-amber-100\/60 px-4 py-3\">\r\n                <div>\r\n                    <Heading as=\"h2\" size=\"sm\" weight=\"semibold\" className=\"!text-sm\">\r\n                        Proposed changes \u2014 nothing has been saved yet\r\n                    <\/Heading>\r\n                    {summary && <Text className=\"!text-xs !text-secondary-600 !mt-0.5\">{summary}<\/Text>}\r\n                <\/div>\r\n                <div className=\"flex gap-2\">\r\n                    <Button\r\n                        onClick={onDiscard}\r\n                        disabled={busy}\r\n                        className=\"!border !border-secondary-300 !bg-white !text-secondary-700 !text-sm !px-3 !py-1.5 !rounded-md\"\r\n                    >\r\n                        Discard\r\n                    <\/Button>\r\n                    <Button\r\n                        onClick={onApply}\r\n                        loading={busy}\r\n                        disabled={busy}\r\n                        className=\"!bg-brand hover:!bg-primary-600 !text-white !font-semibold !text-sm !px-4 !py-1.5 !rounded-md\"\r\n                    >\r\n                        Apply {plan.count} change{plan.count === 1 ? '' : 's'}\r\n                    <\/Button>\r\n                <\/div>\r\n            <\/header>\r\n\r\n            <ol className=\"divide-y divide-amber-200\">\r\n                {plan.operations.map((op, i) => {\r\n                    const open = expanded === i;\r\n                    const attributes = Object.entries(op.attributes ?? {});\r\n\r\n                    return (\r\n                        <li key={i} className=\"px-4 py-3\">\r\n                            <button\r\n                                type=\"button\"\r\n                                onClick={() => setExpanded(open ? null : i)}\r\n                                className=\"flex w-full items-start gap-3 text-left\"\r\n                                aria-expanded={open}\r\n                            >\r\n                                <Badge color={ACTION_COLOR[op.action] ?? 'gray'} variant=\"soft\" size=\"sm\">\r\n                                    {op.action}\r\n                                <\/Badge>\r\n                                <span className=\"flex-1\">\r\n                                    <Text className=\"!text-sm !font-medium\">{op.description}<\/Text>\r\n                                    {op.ref && (\r\n                                        <Text className=\"!text-xs !text-secondary-500 !mt-0.5\">\r\n                                            referenced later as ${op.ref}\r\n                                        <\/Text>\r\n                                    )}\r\n                                <\/span>\r\n                                <span className=\"text-xs text-secondary-500 shrink-0\">\r\n                                    {open ? 'Hide' : `${attributes.length} field${attributes.length === 1 ? '' : 's'}`}\r\n                                <\/span>\r\n                            <\/button>\r\n\r\n                            {open && (\r\n                                <dl className=\"mt-3 grid gap-2 rounded-md bg-white\/70 p-3 text-sm\">\r\n                                    {attributes.map(([key, value]) => (\r\n                                        <div key={key} className=\"grid sm:grid-cols-[10rem_1fr] gap-1 sm:gap-3\">\r\n                                            <dt className=\"text-xs font-medium uppercase tracking-wide text-secondary-500\">\r\n                                                {key}\r\n                                            <\/dt>\r\n                                            <dd className=\"whitespace-pre-wrap break-words text-secondary-800\">\r\n                                                {value !== null && typeof value === 'object'\r\n                                                    ? JSON.stringify(value, null, 2)\r\n                                                    : String(value)}\r\n                                            <\/dd>\r\n                                        <\/div>\r\n                                    ))}\r\n                                <\/dl>\r\n                            )}\r\n                        <\/li>\r\n                    );\r\n                })}\r\n            <\/ol>\r\n\r\n            {draftNote !== null && (\r\n                <footer className=\"border-t border-amber-200 px-4 py-2\">\r\n                    <Text className=\"!text-xs !text-secondary-600\">\r\n                        {draftNote ?? (\r\n                            <>\r\n                                Applied changes are saved as <strong>drafts<\/strong>. Publishing stays a\r\n                                separate, deliberate step.\r\n                            <\/>\r\n                        )}\r\n                    <\/Text>\r\n                <\/footer>\r\n            )}\r\n        <\/section>\r\n    );\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/plan-review\/PlanReview.tsx"}]}