Proposed change row
A change Flo proposed, waiting for the member. The same row everywhere it shows: Today, a record's Next, the Inbox rail, a chat and Tasks › To review. Nothing applies until someone approves it.
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.
- Sara LeeNew Contact at Halcyon Bank
import { ProposedChangeRow } from '@/ui/proposed-change-row';<ProposedChangeRow summary="Sara Lee, New Contact at Halcyon Bank"> New Contact at Halcyon Bank</ProposedChangeRow>In a group
One row per change: the kind's icon, the record as an inline record chip, then the change, usually a value diff. Hover or focus a row: it takes the hovered fill and the due date gives way to Approve (✓) and Deny (✕), which fade in over 150ms. Try them: the outcome replaces the buttons, and Undo takes an approval back.
- Maya ChenNotice periodfrom
3 monthsto1 month - Maya Chenfrom
Long listtoCV sent - Halcyon BankSend Sara the shortlistDue tomorrow
import { useState } from 'react';
import { Lozenge } from '@/ui/lozenge';
import { ProposedChangeRow, type ChangeStatus } from '@/ui/proposed-change-row';
import { RecordChip } from '@/ui/record-chip';
import { ValueDiff } from '@/ui/value-diff';
export default function ProposedChangeRowGroup() {
const [status, setStatus] = useState<Record<string, ChangeStatus>>({});
const decide = (id: string, next: ChangeStatus) => () => setStatus((before) => ({ ...before, [id]: next }));
return (
<ul className="w-full max-w-120 rounded-xlarge bg-surface shadow-raised">
<ProposedChangeRow
kind="field_change"
record={<RecordChip variant="inline" type="candidate" name="Maya Chen" />}
summary="Maya Chen, Notice period 3 months to 1 month"
status={status.notice}
onApprove={decide('notice', 'approved')}
onDeny={decide('notice', 'denied')}
onUndo={decide('notice', 'pending')}
>
<ValueDiff label="Notice period" before="3 months" after="1 month" />
</ProposedChangeRow>
<ProposedChangeRow
kind="stage_move"
record={<RecordChip variant="inline" type="candidate" name="Maya Chen" />}
summary="Maya Chen, Stage Long list to CV sent"
status={status.stage}
onApprove={decide('stage', 'approved')}
onDeny={decide('stage', 'denied')}
onUndo={decide('stage', 'pending')}
>
<ValueDiff before="Long list" after={<Lozenge phase="client">CV sent</Lozenge>} />
</ProposedChangeRow>
<ProposedChangeRow
kind="task"
record={<RecordChip variant="inline" type="company" name="Halcyon Bank" src="/examples/halcyon-bank.svg" />}
summary="Halcyon Bank, Send Sara the shortlist"
meta="Due tomorrow"
status={status.task}
onApprove={decide('task', 'approved')}
onDeny={decide('task', 'denied')}
>
Send Sara the shortlist
</ProposedChangeRow>
</ul>
);
}One change, waiting
A change shown on its own, or one approved only by its own click, carries Deny and Approve as secondary buttons. The page's primary stays the page's.
- Sara LeeNew Contact at Halcyon Bank
import { ProposedChangeRow } from '@/ui/proposed-change-row';
import { RecordChip } from '@/ui/record-chip';
export default function ProposedChangeRowAlone() {
return (
<ul className="w-full max-w-120 rounded-xlarge bg-surface shadow-raised">
<ProposedChangeRow
alone
kind="new_person"
record={<RecordChip variant="inline" type="contact" name="Sara Lee" />}
summary="Sara Lee, New Contact at Halcyon Bank"
onApprove={() => {}}
onDeny={() => {}}
>
New Contact at Halcyon Bank
</ProposedChangeRow>
</ul>
);
}Expiring
The expiry shows in the warning colour. A row left open past it stops offering Approve; the app sets status="expired".
- Sara LeeEmail Maya Chen's CVExpires in 20 min
import { ProposedChangeRow } from '@/ui/proposed-change-row';
import { RecordChip } from '@/ui/record-chip';
export default function ProposedChangeRowExpiring() {
return (
<ul className="w-full max-w-120 rounded-xlarge bg-surface shadow-raised">
<ProposedChangeRow
alone
kind="email"
record={<RecordChip variant="inline" type="contact" name="Sara Lee" />}
summary="Sara Lee, Email Maya Chen's CV"
expiry="Expires in 20 min"
onApprove={() => {}}
onDeny={() => {}}
>
Email Maya Chen's CV
</ProposedChangeRow>
</ul>
);
}Deciding
While a decision is on its way the row's buttons are disabled, so a second click sends nothing.
- Priya Nairfrom
CV senttoInterview
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 ProposedChangeRowDeciding() {
return (
<ul className="w-full max-w-120 rounded-xlarge bg-surface shadow-raised">
<ProposedChangeRow
alone
status="deciding"
kind="stage_move"
record={<RecordChip variant="inline" type="candidate" name="Priya Nair" />}
summary="Priya Nair, Stage CV sent to Interview"
onApprove={() => {}}
onDeny={() => {}}
>
<ValueDiff before="CV sent" after={<Lozenge phase="working">Interview</Lozenge>} />
</ProposedChangeRow>
</ul>
);
}Decided
The outcome replaces the buttons, in words: Approved and Done in success, with a check for the icon; Denied and Expired in subtle; Not done and Didn't work in danger. A note follows the outcome. An approved change offers Undo.
- Maya Chenfrom
Long listtoCV sentApproved - Owen HartSalaryfrom
£95,000to£110,000Denied · 110 includes his bonus - Halcyon BankSend Sara the shortlistExpired
- Tom EllisNew ContactDidn't work
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 ProposedChangeRowDecided() {
return (
<ul className="w-full max-w-140 rounded-xlarge bg-surface shadow-raised">
<ProposedChangeRow status="approved" kind="stage_move" record={<RecordChip variant="inline" type="candidate" name="Maya Chen" />} summary="Maya Chen, Stage Long list to CV sent" onUndo={later}>
<ValueDiff before="Long list" after={<Lozenge phase="client">CV sent</Lozenge>} />
</ProposedChangeRow>
<ProposedChangeRow status="denied" kind="field_change" record={<RecordChip variant="inline" type="candidate" name="Owen Hart" />} summary="Owen Hart, Salary" note="110 includes his bonus">
<ValueDiff label="Salary" before="£95,000" after="£110,000" />
</ProposedChangeRow>
<ProposedChangeRow status="expired" kind="task" record={<RecordChip variant="inline" type="company" name="Halcyon Bank" src="/examples/halcyon-bank.svg" />} summary="Halcyon Bank, Send Sara the shortlist">
Send Sara the shortlist
</ProposedChangeRow>
<ProposedChangeRow status="failed" kind="new_person" record={<RecordChip variant="inline" type="contact" name="Tom Ellis" />} summary="Tom Ellis, New Contact">
New Contact
</ProposedChangeRow>
</ul>
);
}Plan card
One approval for a big change: an import from the member's files, or a list of target companies. Every row shows before anything is written; the member leaves rows out, approves once, and can undo the lot for 14 days.
Question card
Flo asks you to choose before it carries on. Each answer, your own words or Skip go back to Flo as your next message.