{
  "name": "ApprovalGate",
  "source": "src/components/ApprovalGate.tsx",
  "propsType": "ApprovalGateProps",
  "description": "ApprovalGate — a proposed action, held until a person decides.\n\nThe human-in-the-loop control. An agent proposes; this is where a person\napproves, rejects, or amends before anything happens. In regulated contexts\ndemonstrable human oversight is a compliance surface rather than a nicety,\nwhich is why this is a first-class component and not a Modal with two\nbuttons in it.\n\nIT ENFORCES NOTHING, AND SAYING SO IS THE POINT\n\nThis renders a decision. It does not gate execution — the caller does, by not\nacting until `onApprove` fires. A component that *looked* like it enforced a\npolicy would be the worst possible thing to ship here: teams would rely on a\nguarantee that lives entirely in their own call site. `risk` changes emphasis\nand nothing else, for the same reason.\n\nNOT A MODAL, DELIBERATELY\n\nA modal steals focus and hides the page. But the page is the evidence: the\nuser needs the plan, the diff, the tool call and the context in front of them\nwhile deciding. This sits inline, keeps everything visible, and never\ntraps focus — an approval a user was rushed through is not oversight.\n\nNEITHER BUTTON IS FOCUSED ON MOUNT. Autofocusing approve turns a decision\ninto an Enter keypress on a page the user has not read. Focus is moved to the\nregion's heading instead, so a screen-reader user lands on what is being\nasked rather than on the answer.\n\nTHE RESOLVED STATES ARE NOT DECORATION. `approved`, `rejected` and `expired`\nreplace the actions with what happened, so the record of the decision stays\non the page. `expired` exists because an approval request that nobody answers\nis the common real outcome, and a gate that sits pending for ever is\nindistinguishable from one that is broken.",
  "import": "import { ApprovalGate } from 'ionbase-ui';",
  "status": "stable",
  "summary": "A proposed action held until a person approves, rejects or amends it. It renders the decision; it does not enforce it.",
  "useWhen": [
    "an agent proposes something with consequences — spending, sending, deleting, deploying, changing permissions",
    "a regulated or audited process needs demonstrable human oversight at a specific point",
    "the proposal is worth showing in full, in context, before it runs"
  ],
  "useInstead": [
    {
      "when": "the action is already happening and needs stopping",
      "use": "AgentStop"
    },
    {
      "when": "a person initiated the action themselves and only needs to confirm it",
      "use": "Modal",
      "why": "confirming your own request is a dialog; reviewing something proposed for you is a decision"
    },
    {
      "when": "you are reporting what happened rather than asking",
      "use": "Alert"
    }
  ],
  "variants": {
    "risk": {
      "low": {
        "use": "reversible and cheap — a draft, a local edit. Neutral surface"
      },
      "medium": {
        "use": "the default. Real but recoverable consequences"
      },
      "high": {
        "use": "spending, sending externally, deleting, granting access. Warning surface, deliberately not error"
      }
    },
    "status": {
      "pending": {
        "use": "the default, and the only state with actions"
      },
      "approved": {
        "use": "resolved — the actions are replaced by the record"
      },
      "rejected": {
        "use": "resolved, declined"
      },
      "expired": {
        "use": "nobody answered. The common real outcome, and the reason a gate cannot sit pending for ever"
      }
    }
  },
  "slots": {
    "title": {
      "accepts": "text",
      "note": "required, and the region's accessible name. Name the action and its object — \"Delete 14 projects\", never \"Confirm action\""
    },
    "children": {
      "accepts": "any",
      "note": "the evidence: the plan, the diff, the tool call. This is what the person is deciding about"
    },
    "resolution": {
      "accepts": "text",
      "note": "shown in place of the actions once resolved; defaults to the status"
    }
  },
  "a11y": {
    "role": "region, named by its title",
    "guarantees": [
      "neither button is focused on mount — autofocusing approve turns a decision into an Enter keypress on an unread page",
      "the title is programmatically focusable, so the caller can move focus to the question rather than to an answer",
      "reject precedes approve in the DOM, so a keyboard reaches the safe answer first",
      "resolution is announced once through a polite live region",
      "focus is never trapped: an approval a user was rushed through is not oversight"
    ],
    "requires": [
      "not acting until `onApprove` fires — this component gates nothing on its own",
      "a `title` that names the action and its object"
    ],
    "notes": [
      "`risk` changes emphasis and nothing else. It does not add confirmation, does not disable anything, and must never be read as a policy the component enforces."
    ]
  },
  "antiPatterns": [
    {
      "dont": "treating the gate as the enforcement point",
      "why": "it renders a decision. If your code acts before `onApprove`, there is no gate — only a picture of one"
    },
    {
      "dont": "putting it in a Modal",
      "why": "the page is the evidence; hiding it to ask about it removes what the decision needs"
    },
    {
      "dont": "autofocusing the approve button",
      "why": "Enter on an unread page is not oversight, and it is exactly what an audit will look for"
    },
    {
      "dont": "an approve-only gate with no reject",
      "why": "a decision with one answer is a notification wearing a button"
    },
    {
      "dont": "`onEdit` when the proposal cannot actually be amended",
      "why": "an edit button that discards the edit is worse than no edit button"
    },
    {
      "dont": "leaving gates pending indefinitely",
      "do": "the `expired` status",
      "why": "a gate nobody answered is indistinguishable from one that is broken"
    }
  ],
  "stylesheet": "src/styles/approval-gate.css",
  "tokens": [
    "--border-default",
    "--border-disabled",
    "--border-focus",
    "--border-information",
    "--border-strong",
    "--border-success",
    "--border-warning-strong",
    "--border-width-default",
    "--border-width-thick",
    "--border-width-thicker",
    "--font-family-sans",
    "--font-weight-medium",
    "--font-weight-semibold",
    "--icon-information",
    "--icon-secondary",
    "--icon-size-md",
    "--icon-success",
    "--icon-tertiary",
    "--icon-warning",
    "--ion-approval-accent",
    "--ion-approval-border",
    "--ion-approval-icon",
    "--ion-approval-surface",
    "--ion-duration-base",
    "--ion-ease-out",
    "--radius-md",
    "--radius-sm",
    "--radius-xs",
    "--spacing-12",
    "--spacing-16",
    "--spacing-32",
    "--spacing-4",
    "--spacing-8",
    "--surface-default",
    "--surface-disabled",
    "--surface-hover",
    "--surface-information-subtle",
    "--surface-muted",
    "--surface-primary",
    "--surface-primary-hover",
    "--surface-warning-subtle",
    "--text-default",
    "--text-disabled",
    "--text-information",
    "--text-on-color",
    "--text-secondary",
    "--text-success",
    "--text-warning",
    "--type-body",
    "--type-body-line-height",
    "--type-body-sm",
    "--type-body-sm-line-height"
  ],
  "props": {
    "title": {
      "type": "React.ReactNode",
      "required": true,
      "origin": "own",
      "description": "What is being approved, stated as the action and its object — \"Delete 14\nprojects\", not \"Confirm action\". Required: it is the region's accessible\nname, and a decision the user cannot name is not a decision."
    },
    "children": {
      "type": "React.ReactNode",
      "required": false,
      "origin": "own",
      "description": "Why it stopped here, and what happens on approval."
    },
    "risk": {
      "type": "ApprovalGateRisk | undefined",
      "required": false,
      "origin": "own",
      "description": "How much is at stake. Drives emphasis only — it does NOT change what the\ncomponent enforces, because the component enforces nothing.",
      "values": [
        "low",
        "medium",
        "high"
      ]
    },
    "status": {
      "type": "ApprovalGateStatus | undefined",
      "required": false,
      "origin": "own",
      "description": "Where the decision has got to. `pending` is the only state with buttons.",
      "values": [
        "pending",
        "approved",
        "rejected",
        "expired"
      ]
    },
    "onApprove": {
      "type": "(() => void) | undefined",
      "required": false,
      "origin": "own"
    },
    "onReject": {
      "type": "(() => void) | undefined",
      "required": false,
      "origin": "own"
    },
    "onEdit": {
      "type": "(() => void) | undefined",
      "required": false,
      "origin": "own",
      "description": "Offer \"edit\" as a third way out. Omit it when the proposal cannot be\namended — an approve/reject pair the user cannot influence is honest;\nan edit button that discards their edit is not."
    },
    "approveLabel": {
      "type": "string | undefined",
      "required": false,
      "origin": "own"
    },
    "rejectLabel": {
      "type": "string | undefined",
      "required": false,
      "origin": "own"
    },
    "editLabel": {
      "type": "string | undefined",
      "required": false,
      "origin": "own"
    },
    "statusLabel": {
      "type": "string | undefined",
      "required": false,
      "origin": "own",
      "description": "What happened, in words — \"Approved\", \"Rejected\", \"Expired without a\ndecision\" in English. Announced when `status` changes, and shown in place\nof the actions when there is no `resolution`. Pass the translation for\n`status`."
    },
    "isSubmitting": {
      "type": "boolean | undefined",
      "required": false,
      "origin": "own",
      "description": "A decision is in flight. Both actions disable; neither disappears."
    },
    "resolution": {
      "type": "React.ReactNode",
      "required": false,
      "origin": "own",
      "description": "Shown in place of the actions once the decision is made."
    }
  },
  "propCounts": {
    "own": 13,
    "aria": 0,
    "dom": 275,
    "other": 0
  }
}
