Components/ApprovalCard

ApprovalCard

The clarifying questions an agent asks before it acts: single choice, multiple choice, and free text, with paging and a submitted state.

VerifiedSince 0.3.9

ApprovalCard

Basic Usage

A Multi-Question Walkthrough

One question at a time. A single choice auto-advances after 480ms; a multiple choice does not, because it has to wait for the reader to finish picking.

Loading demo...

It Is Not an Authorisation Gate

TxToolConfirmation is a binary authorisation: may this tool call proceed, answered with approve / deny and a risk tier. This component is a questionnaire: the agent asks a few things before it acts and gets back a set of answers. The two share neither a data model nor an output contract — do not substitute one for the other, and do not fold a questionnaire into the confirmation card.

API

Props

PropTypeDefaultDescription
questionsApprovalQuestion[]The questions: { id, question, type?, options, allowCustom?, customPlaceholder? }. Required.
modelValueRecord<string, ApprovalAnswer>v-model, the answer map keyed by question.id. Omit to let the card own it.
indexnumberv-model:index, the visible question. Omit to let the card own it.
sentbooleanv-model:sent, whether the answers were submitted.
openbooleanv-model:open; false collapses the card to a single reopen button.
autoAdvancebooleantrueAdvance after a single-choice pick.
autoAdvanceDelaynumber480Delay before that advance, in ms.
dismissiblebooleantrueRender the dismiss control.
ariaLabelstring'Approval questions'Accessible name of the whole card.
sendLabelstring'Send answers'Accessible name of the send control on the last question.
nextQuestionLabelstring'Next question'Accessible name of the send control elsewhere.
prevLabel / nextLabelstring'Previous' / 'Next'Accessible names of the pager arrows.
dismissLabelstring'Dismiss'Accessible name of the dismiss control.
reopenLabelstring'Open approval'Text of the collapsed button.
sentLabelstring'Answers sent'Confirmation text.
startOverLabelstring'Start over'Text of the restart control.
customPlaceholderstring'Type something…'Placeholder of the free-text row. A question may override it.
customLabelstring'Custom answer'Accessible name of the free-text field.
pagerLabelFormatter(position: number) => stringn => `Go to question ${n}`Accessible name of each pager dot.

Events

EventArgumentsDescription
update:modelValue(answers: Record<string, ApprovalAnswer>)The answer map changed.
update:index(index: number)The page changed.
update:sent(sent: boolean)The submitted state changed.
update:open(open: boolean)Collapsed or reopened.
answer(answer: ApprovalAnswer)Fires as each question is answered, so a host can persist incrementally.
submit(answers: ApprovalAnswer[])Submitted on the last question, answered items in questions order.
dismiss()The card was dismissed.
reopen()The card was reopened.

Slots

SlotScopeDescription
question{ question, index }Replaces the question prompt.
sent{ answers }Replaces the confirmation panel.
footer-extraInserted in the footer, before the send control.

Exposed

MethodDescription
next() / prev() / goTo(index)Paging.
submit()Equivalent to pressing send; a no-op without an answer.
reset()Clears answers, returns to the first question, leaves the submitted state, and reopens.

Interaction Contract

  • All four states are dual-mode. Supply modelValue / index / sent / open and the prop wins; omit it and the card owns it. A streaming host that remounts the card keeps its page and answers only if it controls them.
  • Answers are keyed by option.value, never by index. Reordering the options cannot corrupt them.
  • Single choice and free text are alternatives. Picking an option clears that question's free text, and typing into the free-text field clears the single choice. Multiple choice is unaffected — both can coexist.
  • The send control is enabled when there is a selected option or non-empty free text. It submits on the last question and advances everywhere else.
  • A filled pager dot means that question is answered, not that it was passed. Upstream tests i < currentIndex, which shows answered questions as unanswered after navigating back; that is corrected here.
  • The auto-advance is suppressed under prefers-reduced-motion: reduce — a deliberate divergence from upstream. Upstream keeps advancing, since it only has one global * rule compressing durations. Here the preference is read as "no unrequested change of context", and a page moving out from under a screen reader mid-sentence is exactly that. Advancing is a change of context rather than progress machinery, so it belongs to what the preference suppresses, not to the state machines that "cut tweens, not state machines" protects. Every question stays reachable through the pager dots and the send control, so nothing is lost.
  • Dismissing swaps the card for a single button without clearing state; only reset() or the restart control clears it.
  • Option buttons are button + aria-pressed, wrapped in a role="group" whose aria-labelledby points at the prompt, so a screen reader gets the question as context.

Best Practices

  • Persist on answer rather than waiting for submit — answers to earlier questions should survive a reader dismissing the card halfway.
  • Keep it to three to five questions; beyond that use a form page, since the pager dots stop being scannable around seven or eight.
  • Set allowCustom: false on questions that do not need free text — one fewer empty field is one fewer mis-tap.
  • Override the label props together for a non-English UI; changing only some of them produces mixed-language chrome.
  • Lift sent into the host after submit, or remounting the card drops it back to the unsubmitted state.

Source

  • Component source: packages/tuffex/packages/components/src/approval-card/src/TxApprovalCard.vue.
  • Types: packages/tuffex/packages/components/src/approval-card/src/types.ts.
  • Verified coverage: packages/tuffex/packages/components/src/approval-card/__tests__/approval-card.test.ts (15 cases) covers single-choice replacement and multi-choice accumulation, the free-text exclusion, the send gate, pager disabling and dot states, controlled index / modelValue precedence, dismiss and reopen, the auto-advance not firing on multiple choice, its suppression under reduced motion, and reset() clearing every piece of state.
  • Adapted from Beautiful UI (https://www.beautifului.dev), © 2026 Shane Levine, MIT.
查看源码
packages/tuffex/packages/components/src/approval-card/index.ts