ComponentsAI

Flo approval

The changes Flo proposed in one turn, for you to approve or deny together: N changes for you, Deny and the green Approve, the rows, then the outcome with Undo.

Playground

Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page.

2 changes for you
  • Maya ChenfromLong listtoCV sent
  • Sara LeeEmail Maya's CV · draft
import { FloApproval } from '@/ui/flo-approval';<FloApproval />

Waiting for you

The header counts the changes and holds Deny and the one green Approve with the count. Each change is a proposed change row with the record as a chip and the stage as a lozenge in its phase hue, the old value struck through. Press Approve: it holds a spinner, both buttons disable, then the outcome takes their place with Undo.

2 changes for you
  • Maya ChenfromLong listtoCV sent
  • Sara LeeEmail Maya's CV · draft
import { FloApproval } from '@/ui/flo-approval';
import { Lozenge } from '@/ui/lozenge';
import { ProposedChangeRow } from '@/ui/proposed-change-row';
import { RecordChip } from '@/ui/record-chip';
import { ValueDiff } from '@/ui/value-diff';

const later = () => new Promise<void>((resolve) => setTimeout(resolve, 800));

export default function FloApprovalDefault() {
  return (
    <FloApproval className="w-full max-w-90" onApprove={later} onDeny={later} onUndo={later} decidedAt="10:42">
      <ProposedChangeRow kind="stage_move" record={<RecordChip variant="inline" type="candidate" name="Maya Chen" />} summary="Maya Chen, Stage Long list to CV sent">
        <ValueDiff before="Long list" after={<Lozenge phase="client">CV sent</Lozenge>} />
      </ProposedChangeRow>
      <ProposedChangeRow kind="email" record={<RecordChip variant="inline" type="contact" name="Sara Lee" />} summary="Sara Lee, Email Maya's CV, draft">
        Email Maya&apos;s CV · draft
      </ProposedChangeRow>
    </FloApproval>
  );
}

Deciding

While a decision is on its way the pressed button holds a spinner and the other is disabled, so a second click sends nothing.

1 change for you
  • Maya ChenNoticefrom3 monthsto1 month
import { FloApproval } from '@/ui/flo-approval';
import { ProposedChangeRow } from '@/ui/proposed-change-row';
import { RecordChip } from '@/ui/record-chip';
import { ValueDiff } from '@/ui/value-diff';

export default function FloApprovalDeciding() {
  return (
    <FloApproval className="w-full max-w-90" status="deciding" onApprove={() => {}} onDeny={() => {}}>
      <ProposedChangeRow kind="field_change" record={<RecordChip variant="inline" type="candidate" name="Maya Chen" />} summary="Maya Chen, Notice period 3 months to 1 month">
        <ValueDiff label="Notice" before="3 months" after="1 month" />
      </ProposedChangeRow>
    </FloApproval>
  );
}

Approved, with Undo

The buttons give way to the outcome in words, in success, with the time, and Undo after it. The rows say what was done.

Approved · 10:42
  • Maya Chenmoved to CV sentDone
  • Sara Leeemail drafted in your Outbox, not sentDone
import { FloApproval } from '@/ui/flo-approval';
import { Lozenge } from '@/ui/lozenge';
import { ProposedChangeRow } from '@/ui/proposed-change-row';
import { RecordChip } from '@/ui/record-chip';

const later = () => new Promise<void>((resolve) => setTimeout(resolve, 800));

export default function FloApprovalApproved() {
  return (
    <FloApproval className="w-full max-w-90" defaultStatus="approved" decidedAt="10:42" onApprove={later} onDeny={later} onUndo={later}>
      <ProposedChangeRow status="done" kind="stage_move" record={<RecordChip variant="inline" type="candidate" name="Maya Chen" />} summary="Maya Chen moved to CV sent">
        moved to <Lozenge phase="client">CV sent</Lozenge>
      </ProposedChangeRow>
      <ProposedChangeRow status="done" kind="email" record={<RecordChip variant="inline" type="contact" name="Sara Lee" />} summary="Sara Lee, email drafted">
        email drafted in your Outbox, not sent
      </ProposedChangeRow>
    </FloApproval>
  );
}

Denied

A denied card says so in subtle text.

Denied · 10:44
  • Owen HartSalaryfrom£95,000to£110,000Denied
import { FloApproval } from '@/ui/flo-approval';
import { ProposedChangeRow } from '@/ui/proposed-change-row';
import { RecordChip } from '@/ui/record-chip';
import { ValueDiff } from '@/ui/value-diff';

export default function FloApprovalDenied() {
  return (
    <FloApproval className="w-full max-w-90" status="denied" decidedAt="10:44" onApprove={() => {}} onDeny={() => {}}>
      <ProposedChangeRow status="denied" kind="field_change" record={<RecordChip variant="inline" type="candidate" name="Owen Hart" />} summary="Owen Hart, Salary">
        <ValueDiff label="Salary" before="£95,000" after="£110,000" />
      </ProposedChangeRow>
    </FloApproval>
  );
}

When it didn't go through

When onApprove returns a reason, it shows in danger under the rows and the buttons come back.

1 change for you
  • Tom EllisNew Contact at Halcyon Bank
import { FloApproval } from '@/ui/flo-approval';
import { ProposedChangeRow } from '@/ui/proposed-change-row';
import { RecordChip } from '@/ui/record-chip';

export default function FloApprovalFailed() {
  return (
    <FloApproval className="w-full max-w-90" onApprove={() => 'This changed since Flo proposed it. Nothing was changed.'} onDeny={() => {}}>
      <ProposedChangeRow kind="new_person" record={<RecordChip variant="inline" type="contact" name="Tom Ellis" />} summary="Tom Ellis, New Contact at Halcyon Bank">
        New Contact at Halcyon Bank
      </ProposedChangeRow>
    </FloApproval>
  );
}

Expired

A card left waiting past its time stops offering Approve. It says nothing changed and to ask Flo again.

Expired · nothing changed. Ask Flo again.
  • Maya ChenfromLong listtoCV sentExpired
import { FloApproval } from '@/ui/flo-approval';
import { Lozenge } from '@/ui/lozenge';
import { ProposedChangeRow } from '@/ui/proposed-change-row';
import { RecordChip } from '@/ui/record-chip';
import { ValueDiff } from '@/ui/value-diff';

export default function FloApprovalExpired() {
  return (
    <FloApproval className="w-full max-w-90" status="expired" onApprove={() => {}} onDeny={() => {}}>
      <ProposedChangeRow status="expired" kind="stage_move" record={<RecordChip variant="inline" type="candidate" name="Maya Chen" />} summary="Maya Chen, Stage Long list to CV sent">
        <ValueDiff before="Long list" after={<Lozenge phase="client">CV sent</Lozenge>} />
      </ProposedChangeRow>
    </FloApproval>
  );
}

Undo too late

When a change can't be taken back any more, Undo says why under the rows and what to do by hand. The button stays, and the outcome still reads Approved.

Approved · Yesterday
  • Maya Chenmoved to CV sentDone
import { FloApproval } from '@/ui/flo-approval';
import { Lozenge } from '@/ui/lozenge';
import { ProposedChangeRow } from '@/ui/proposed-change-row';
import { RecordChip } from '@/ui/record-chip';

export default function FloApprovalUndoTooLate() {
  return (
    <FloApproval
      className="w-full max-w-90"
      defaultStatus="approved"
      decidedAt="Yesterday"
      onApprove={() => {}}
      onDeny={() => {}}
      onUndo={() => "This can't be undone any more: Maya moved on to Interview since. Move her back by hand."}
    >
      <ProposedChangeRow status="done" kind="stage_move" record={<RecordChip variant="inline" type="candidate" name="Maya Chen" />} summary="Maya Chen moved to CV sent">
        moved to <Lozenge phase="client">CV sent</Lozenge>
      </ProposedChangeRow>
    </FloApproval>
  );
}

On this page