AI Agents Fancy Stable
ApprovalCard
A human-in-the-loop gate before a side effect: the agent states what it is about to do, and the footer swaps its approve and deny buttons for a one-line verdict once someone decides
Preview
svelte
<script lang="ts">
import { ApprovalCard } from "fancy-ui-svelte";
import type { ApprovalState } from "fancy-ui-svelte";
// No timer, no loop: the demo only moves when someone actually decides
// something, which is the whole point of the component.
let branchState = $state<ApprovalState>("pending");
let migrationState = $state<ApprovalState>("pending");
const settled = $derived(
[branchState, migrationState].filter((state) => state !== "pending").length
);
function reset() {
branchState = "pending";
migrationState = "pending";
}
</script>
<div class="flex w-full max-w-xl flex-col gap-3 p-6">
<ApprovalCard
title="Create branch fix/retry-backoff"
description="Branches from develop and pushes an empty commit so CI has something to run."
bind:state={branchState}
/>
<ApprovalCard
title="Run database migration"
description="Adds two columns to invoices and backfills 41,880 rows. There is no down migration."
destructive
approveLabel="Run migration"
denyLabel="Not now"
bind:state={migrationState}
>
<pre
class="ft-approval-preview text-foreground/80 overflow-x-auto rounded-md px-2.5 py-2 font-mono text-xs">pnpm db:migrate --env production --yes</pre>
</ApprovalCard>
<p class="text-muted-foreground mt-1 text-xs">
{#if settled === 0}
Both gates are waiting. Nothing runs until you press something.
{:else}
{settled} of 2 decided.
<button
type="button"
class="text-foreground underline decoration-dotted underline-offset-2"
onclick={reset}
>
Reset
</button>
{/if}
</p>
</div>
<style>
.ft-approval-preview {
background: color-mix(in oklab, currentColor 7%, transparent);
}
</style>Installation
pnpm add fancy-ui-svelte
import { ApprovalCard } from 'fancy-ui-svelte'
Usage
svelte
<script lang="ts">
import { ApprovalCard } from 'fancy-ui-svelte';
</script>
<ApprovalCard />Examples
Basic Usage
svelte
<script lang="ts">
import { ApprovalCard } from "$lib/fancy-ui/approval-card";
import type { ApprovalState } from "$lib/fancy-ui/approval-card";
// No timer, no loop: the demo only moves when someone actually decides
// something, which is the whole point of the component.
let branchState = $state<ApprovalState>("pending");
let migrationState = $state<ApprovalState>("pending");
const settled = $derived(
[branchState, migrationState].filter((state) => state !== "pending").length
);
function reset() {
branchState = "pending";
migrationState = "pending";
}
</script>
<div class="flex w-full max-w-xl flex-col gap-3 p-6">
<ApprovalCard
title="Create branch fix/retry-backoff"
description="Branches from develop and pushes an empty commit so CI has something to run."
bind:state={branchState}
/>
<ApprovalCard
title="Run database migration"
description="Adds two columns to invoices and backfills 41,880 rows. There is no down migration."
destructive
approveLabel="Run migration"
denyLabel="Not now"
bind:state={migrationState}
>
<pre
class="ft-approval-preview text-foreground/80 overflow-x-auto rounded-md px-2.5 py-2 font-mono text-xs">pnpm db:migrate --env production --yes</pre>
</ApprovalCard>
<p class="text-muted-foreground mt-1 text-xs">
{#if settled === 0}
Both gates are waiting. Nothing runs until you press something.
{:else}
{settled} of 2 decided.
<button
type="button"
class="text-foreground underline decoration-dotted underline-offset-2"
onclick={reset}
>
Reset
</button>
{/if}
</p>
</div>
<style>
.ft-approval-preview {
background: color-mix(in oklab, currentColor 7%, transparent);
}
</style>Props
| Prop | Type | Default | Description |
|---|---|---|---|
title * | string | - | What permission is being asked for, e.g. "Run database migration" |
description | string | - | Muted second line under the title — the consequence, the blast radius |
state | "pending" | "approved" | "denied" | "pending" | Which side of the gate we are on (bindable). Buttons exist only while pending; writing a resolved value from outside settles the card without firing a callback |
destructive | boolean | false | Marks the action as irreversible: red approve button, a warning tint on the card, and an alert mark on the shield so the warning is not carried by colour alone |
approveLabel | string | "Approve" | Label for the approve button |
denyLabel | string | "Deny" | Label for the deny button |
onApprove | () => void | - | Called when approve is pressed, after state has been written |
onDeny | () => void | - | Called when deny is pressed, after state has been written |
busy | boolean | false | The consumer is executing the decision: both buttons go disabled and the card is marked aria-busy |
Slots
| Slot | Description |
|---|---|
children | Detail region between the header and the footer — a diff, a command preview, the payload about to be posted |
Links
Related components
Ag
AgentPlan
Glanceable checklist of an agent's plan: a done/total count and completion bar over one row per step, with a status glyph, an optional detail line, and substeps indented under a rail
AI Agents Stable
Ai
AiDataTable
Compact comparison table for a model's structured answer: real table semantics, tabular numeric columns, check-or-dash booleans, and one tintable column — rendered in the order it arrived, with no sorting
AI Agents Stable
Ar
ArtifactCard
A generated document as a tangible object: title, kind, a version navigator, and the first six lines of the text itself streaming in behind a fade
AI Agents Stable