Decisions
Approval card
An explicit, keyboard-operable human gate with destructive and always-allow variants and resolved states.
Starts when it scrolls into view
NORD FROST · DARK · RADIUS 6/10 · BASE 16PX · SHADOW
- Type
ApprovalCard- Source
approval.rs- Story
approval- Demo height
- 457 px
- Events
- ApprovalEvent
Code
Cut from the gallery story this page runs, so it stays true.
let tokens = cx.theme().semantic_tokens();
let decision_of = |id: &str| {
self.approval_decisions
.get(id)
.copied()
.unwrap_or_default()
};
let make_handler = || {
cx.listener(|this, event: &ApprovalEvent, _, cx| {
let (id, decision) = match event {
ApprovalEvent::Approved { id } | ApprovalEvent::ApprovedAlways { id } => {
(id.clone(), ApprovalDecision::Approved)
}
ApprovalEvent::Rejected { id } => (id.clone(), ApprovalDecision::Rejected),
};
this.approval_decisions.insert(id, decision);
this.last_approval_event = Some(format!("{event:?}").into());
cx.notify();
})
};
let status: SharedString = self
.last_approval_event
.clone()
.unwrap_or_else(|| "Decide a gate to see its resolved state".into());
let caption = |text: &'static str| {
div()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(text)
};
v_flex()
.gap(tokens.spacing.md)
.child(caption("Default tone with a payload"))
.child(
ApprovalCard::new("gate", "Send order confirmation to 3 suppliers?")
.description(
"Emails will go out immediately and cannot be recalled.",
)
.decision(decision_of("gate"))
.note("Decided in this session")
.child(
h_flex()
.flex_wrap()
.gap_1p5()
.child(
ToolChip::new("gate-chip-1", "email alpenrose")
.status(ToolStatus::Pending),
)
.child(
ToolChip::new("gate-chip-2", "email tillamook")
.status(ToolStatus::Pending),
),
)
.on_event(make_handler()),
)
.child(caption("Destructive tone with \"Always allow\""))
.child(
ApprovalCard::new("purge", "Delete 12 stale supplier records?")
.description("Records older than 18 months are removed permanently.")
.tone(ApprovalTone::Destructive)
.approve_label("Delete records")
.allow_always(true)
.decision(decision_of("purge"))
.note("Decided in this session")
.on_event(make_handler()),
)
.child(
div()
.id("approval-story-status")
.role(Role::Status)
.aria_label(status.clone())
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(status),
)
Events and ownership
- Who owns the state
- ApprovalCard renders state supplied by the application; it does not own durable work.
- What it emits
- Interactive intent is reported through the typed ApprovalEvent contract and stable application IDs.
- What it means
- An explicit, keyboard-operable human gate with destructive and always-allow variants and resolved states.
- Overflow and motion
- Wide content retains context in a bounded surface; reduced motion preserves a useful state.
Event types
- ApprovalEvent
What the demo does and does not show
The demo above is the same Rust as the native component, compiled to WebAssembly and drawn on a WebGPU canvas — not a mock-up, a recording, or a re-implementation. What it paints is what the component paints: layout, colour from the live theme, and motion.
The live browser specimen requires WebGPU; the native component remains the authoritative runtime.
A canvas cannot stand in for the platform, so treat everything that goes through the operating system as unproven here: keyboard handling and shortcuts, screen reader semantics, text input and IME, the clipboard, file drops, and the frame rate you would get on your own GPU. Judge those in a native build. If your browser has no WebGPU, the frame says so rather than showing an empty box.