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.
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.
Import 2 Companies and 210 Candidates
Companies2 new
Candidates198 new · 12 match existing
import { PlanCard } from '@/ui/plan-card';<PlanCard title="Import 2 Companies and 210 Candidates" />Import plan, waiting
The shield marks a change that waits for approval. The title counts what Approve does and the files it came from sit under it. Then counts per record type, new fields with their options, how the file's stages map (a stage Flo guessed carries a warning pill), and the rows: every one can be left out, and a person who looks like someone already in Ascern asks what to do. The footer says what is left out and how long Undo lasts, then Deny and the one primary, Approve. Try it.
Import 3 Companies, 4 Jobs and 212 Candidates
Northbridge tracker.xlsx · Shortlists Q3.csv
Companies2 new · 1 fills empty fields
Jobs4 new
Candidates198 new · 12 match existing · 2 need you
New fields
Visa status on CandidatesSponsoredRight to work
Stages
Sent to client → CV sent
Final → Interview Guessed
Candidates
And 209 more
Needs you
Shortlists Q3.csv row 14: no email or phone, so Ascern can’t tell who this is.
import { Lozenge } from '@/ui/lozenge';
import { PlanCard, PlanChoice, PlanCount, PlanItem, PlanSection } from '@/ui/plan-card';
// Leave a row out, pick what to do with Owen Hart, then Approve or Deny.
export default function PlanCardImport() {
const wait = () => new Promise<void>((done) => setTimeout(done, 900));
return (
<PlanCard
className="max-w-130"
title="Import 3 Companies, 4 Jobs and 212 Candidates"
sources="Northbridge tracker.xlsx · Shortlists Q3.csv"
proposedAt="10:42"
onOpenPlan={() => {}}
note="1 left out. Undo import takes it back for 14 days."
onApprove={wait}
onDeny={wait}
onUndo={wait}
>
<PlanSection>
<PlanCount label="Companies">2 new · 1 fills empty fields</PlanCount>
<PlanCount label="Jobs">4 new</PlanCount>
<PlanCount label="Candidates">198 new · 12 match existing · 2 need you</PlanCount>
</PlanSection>
<PlanSection title="New fields">
<p className="flex flex-wrap items-center gap-050">
Visa status <span className="text-text-subtle">on Candidates</span>
<Lozenge>Sponsored</Lozenge>
<Lozenge>Right to work</Lozenge>
</p>
</PlanSection>
<PlanSection title="Stages">
<p>
Sent to client <span className="text-text-subtle">→</span> CV sent
</p>
<p className="flex flex-wrap items-center gap-050">
Final <span className="text-text-subtle">→</span> Interview <Lozenge appearance="warning">Guessed</Lozenge>
</p>
</PlanSection>
<PlanSection title="Candidates">
<PlanItem name="Maya Chen" status={<Lozenge appearance="success">New</Lozenge>} />
<PlanItem name="Test Person" defaultIncluded={false} status={<Lozenge appearance="success">New</Lozenge>} />
<PlanItem name="Owen Hart" description="Looks like Owen Hart at Brightwave, already in Ascern." status={<Lozenge>Matches existing</Lozenge>}>
<PlanChoice
label="What to do with Owen Hart"
defaultValue="link"
options={[
['link', 'Link to existing'],
['create', 'Create anyway'],
['skip', 'Skip'],
]}
/>
</PlanItem>
<p className="ps-300 text-text-subtle">And 209 more</p>
</PlanSection>
<PlanSection title="Needs you">
<p className="text-text-subtle">Shortlists Q3.csv row 14: no email or phone, so Ascern can’t tell who this is.</p>
</PlanSection>
</PlanCard>
);
}Importing, then decided
A big import writes in parts with a progress bar and the row count, and carries on if the chat closes. The outcome then replaces the card in one status line: Imported with Undo import, Denied, Expired, or Failed with the reason.
Import 3 Companies, 4 Jobs and 212 Candidates
Importing 124 of 219 rows. Keep this chat open: if you leave, the import carries on when you open it again.
Imported
Denied, nothing was imported
Expired, nothing was imported. Ask Flo again.
Approved, but the import didn’t work: the tracker changed since Flo read it.
import { PlanCard } from '@/ui/plan-card';
// Writing in parts with the row count, then one status line: imported with Undo, denied, expired or failed.
export default function PlanCardOutcomes() {
const title = 'Import 3 Companies, 4 Jobs and 212 Candidates';
const none = () => {};
return (
<div className="flex w-full max-w-130 flex-col gap-150">
<PlanCard title={title} status="importing" progress={{ value: 124, max: 219 }} onApprove={none} onDeny={none} />
<PlanCard title={title} status="imported" onApprove={none} onDeny={none} onUndo={none} />
<PlanCard title={title} status="denied" onApprove={none} onDeny={none} />
<PlanCard title={title} status="expired" onApprove={none} onDeny={none} />
<PlanCard title={title} status="failed" outcome="the tracker changed since Flo read it." onApprove={none} onDeny={none} />
</div>
);
}Target list
Each company Flo proposes, with its tier, Flo's reason and the decision-makers it found. Companies that can't be added say why and have no tick box.
Add 3 fintech targets for Northbridge
import { Link } from '@/ui/link';
import { Lozenge } from '@/ui/lozenge';
import { PlanCard, PlanItem, PlanSection } from '@/ui/plan-card';
// Companies that can't be added say why and have no tick box.
export default function PlanCardTargets() {
const wait = () => new Promise<void>((done) => setTimeout(done, 900));
return (
<PlanCard
className="max-w-130"
title="Add 3 fintech targets for Northbridge"
proposedAt="10:42"
note="2 targets to add. Undo import takes them back for 14 days."
onApprove={wait}
onDeny={wait}
>
<PlanSection>
<PlanItem
name="Brightwave Payments · Tier 1"
description={
<>
Hiring 6 data roles since its Series C in August. Lena Ford · Head of Talent ·{' '}
<Link href="#lena-ford" external>
LinkedIn
</Link>
</>
}
status={<Lozenge appearance="success">New company</Lozenge>}
/>
<PlanItem name="Lumen Ledger · Tier 2" description="New London platform team." status={<Lozenge appearance="success">New company</Lozenge>} />
<PlanItem name="Halcyon Bank" blocked="Already a client." status={<Lozenge appearance="danger">Do not contact</Lozenge>} />
</PlanSection>
</PlanCard>
);
}Nothing left to add
When every row is left out or blocked, Approve can't be pressed and the note says why.
Add 1 target for Northbridge
import { Lozenge } from '@/ui/lozenge';
import { PlanCard, PlanItem, PlanSection } from '@/ui/plan-card';
// Every row left out or blocked: Approve can't be pressed, and the note says why.
export default function PlanCardNothing() {
const none = () => {};
return (
<PlanCard className="max-w-130" title="Add 1 target for Northbridge" note="Nothing left to add." approveDisabled onApprove={none} onDeny={none}>
<PlanSection>
<PlanItem name="Halcyon Bank" blocked="Already a target." status={<Lozenge>Already a target</Lozenge>} />
</PlanSection>
</PlanCard>
);
}Flo composer
The one box for talking to Flo: a rounded island with the message, the files, attach, the mic and a round Send that turns into Stop while Flo answers.
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.