Needs you list
The list on Home of what is waiting on you today, and the sections of the morning brief. Every row is one thing, with the one action that clears 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.
Yours
import { NeedsYouSection } from '@/ui/needs-you-list';<NeedsYouSection title="Yours" />Needs you on Home
Counts first, one line each, linking to the place that holds them. Then Yours: stuck Rules, notices, briefs, meetings that want an outcome and tasks due, about five rows, then Show all. Clear a row and it leaves; Undo brings it back.
import { useState } from 'react';
import { Button } from '@/ui/button';
import { moveList } from '@/ui/motion';
import { NeedsYouCount, NeedsYouList, NeedsYouRow, NeedsYouSection } from '@/ui/needs-you-list';
import { toast } from '@/ui/toast';
const yours = [
{ id: 'rule', title: 'Stuck: Monday shortlist digest', meta: 'Gmail refused the send. The daily sending limit was reached.', action: 'Open run' },
{ id: 'brief', title: 'Your morning brief is ready', meta: '2 owed a reply, 1 stuck, 3 in interviews.', action: 'Open brief', dismiss: true },
{ id: 'call', title: 'Halcyon Bank intro call', meta: 'Sara Lund, Maya Chen · Ended 2h ago', action: 'Log outcome' },
{ id: 'task', title: 'Send CVs to Halcyon', meta: 'Halcyon Bank · Due today', action: 'Complete' },
];
// Home: counts first, then Yours. Complete or dismiss a row and it leaves; Undo brings it back.
export default function NeedsYouHome() {
const [gone, setGone] = useState<string[]>([]);
const clear = (id: string, title: string) => {
moveList(() => setGone((now) => [...now, id]));
toast.add({ title: `${title} done`, actionProps: { children: 'Undo', onClick: () => moveList(() => setGone((now) => now.filter((g) => g !== id))) } });
};
return (
<NeedsYouList className="w-full max-w-160">
<NeedsYouCount actionLabel="Open Inbox" href="?inbox">
3 replies need you
</NeedsYouCount>
<NeedsYouCount actionLabel="Open Meetings" href="?meetings">
1 recording to file
</NeedsYouCount>
<NeedsYouSection title="Yours" showAllHref="?tasks">
{yours
.filter((row) => !gone.includes(row.id))
.map((row) => (
<NeedsYouRow
key={row.id}
id={row.id}
title={row.title}
meta={row.meta}
action={
<Button variant="ghost" aria-label={`${row.action} ${row.title}`} onClick={() => clear(row.id, row.title)}>
{row.action}
</Button>
}
onDismiss={row.dismiss ? () => clear(row.id, row.title) : undefined}
/>
))}
</NeedsYouSection>
</NeedsYouList>
);
}Morning brief sections
Each section is a 36px heading with its count, then its rows. The brief is a page of actions to work through, so each row's action is a secondary button. Empty sections are hidden.
Owed a reply · 2
Open roles · 1
import { Avatar } from '@/ui/avatar';
import { Button } from '@/ui/button';
import { NeedsYouList, NeedsYouRow, NeedsYouSection } from '@/ui/needs-you-list';
// The morning brief: a page of actions to work through, so each row's action is a secondary button. Empty sections hide.
export default function NeedsYouBrief() {
return (
<NeedsYouList className="w-full max-w-160">
<NeedsYouSection title="Owed a reply" count={2}>
<NeedsYouRow id="maya" title={<span className="flex items-center gap-075"><Avatar name="Maya Chen" />Maya Chen</span>} meta="Re: Senior Data Engineer shortlist · Wrote 2h ago" action={<Button aria-label="Reply to Maya Chen">Reply</Button>} />
<NeedsYouRow id="james" title={<span className="flex items-center gap-075"><Avatar name="James Reed" />James Reed</span>} meta="Interview times for Thursday · Wrote yesterday" action={<Button aria-label="Reply to James Reed">Reply</Button>} />
</NeedsYouSection>
<NeedsYouSection title="Stuck" count={0} />
<NeedsYouSection title="Open roles" count={1}>
<NeedsYouRow id="sde" title="Senior Data Engineer" meta="2 of 5 CVs sent · Interview 3 · Offer 1" action={<Button aria-label="Send CVs for Senior Data Engineer">Send CVs</Button>} />
</NeedsYouSection>
</NeedsYouList>
);
}Empty
Nothing waiting says "Nothing here." in subtle text, with no illustration.
Nothing here.
import { NeedsYouList } from '@/ui/needs-you-list';
// Nothing waiting: one line of subtle text, no illustration on Home.
export default function NeedsYouEmpty() {
return <NeedsYouList className="w-full max-w-160" />;
}When an action fails, and long rows
Dismiss the notice: it leaves at once, the server refuses, and it comes back with a toast that stays until you close it. A long title or reason ends in an ellipsis on one line, and the title shows in full on hover.
Yours
import { useState } from 'react';
import { moveList } from '@/ui/motion';
import { NeedsYouList, NeedsYouRow, NeedsYouSection } from '@/ui/needs-you-list';
import { toast } from '@/ui/toast';
// Dismiss the notice: the row slides out at once, the server refuses, and it slides back with a toast that stays.
export default function NeedsYouFailure() {
const [gone, setGone] = useState(false);
const dismiss = () => {
moveList(() => setGone(true));
setTimeout(() => {
moveList(() => setGone(false));
toast.add({ type: 'error', title: "Not dismissed. Check your connection and try again." });
}, 600);
};
return (
<NeedsYouList className="w-full max-w-160">
<NeedsYouSection title="Yours">
{gone ? null : <NeedsYouRow id="notice" title="Nguyễn Thị Minh Khai replied to the Halcyon Bank shortlist" meta="From the Rule “Tell me when a client replies” · 5 min ago" onDismiss={dismiss} />}
<NeedsYouRow id="long" title="Stuck: Weekly pipeline summary for the Halcyon Bank, Orbital Freight and Northwind Logistics accounts" meta="Gmail refused the send because the daily sending limit for this mailbox was reached. It runs again tomorrow at 08:00." />
</NeedsYouSection>
</NeedsYouList>
);
}Motion
A cleared row leaves with the row in and out pattern. Press Replay to watch the first row leave and come back.
Yours
import { useState } from 'react';
import { Button } from '@/ui/button';
import { moveList } from '@/ui/motion';
import { NeedsYouList, NeedsYouRow, NeedsYouSection } from '@/ui/needs-you-list';
const rows = ['Send CVs to Halcyon', 'Call Maya about her notice period', 'Brief Tom on Orbital Freight'];
// Replay clears the first row, then brings it back: it fades up 4px and leaves in 160ms, the rest slide up in 200ms.
export default function NeedsYouMotion() {
const [shown, setShown] = useState(rows);
const replay = () => {
moveList(() => setShown(rows.slice(1)));
setTimeout(() => moveList(() => setShown(rows)), 900);
};
return (
<div className="flex w-full max-w-160 flex-col items-start gap-300">
<NeedsYouList className="w-full">
<NeedsYouSection title="Yours">
{shown.map((title) => (
<NeedsYouRow key={title} id={title.replace(/\W+/g, '-')} title={title} />
))}
</NeedsYouSection>
</NeedsYouList>
<Button size="sm" onClick={replay}>
Replay
</Button>
</div>
);
}