Time presets menu
A short list of named times that act on one click: snooze a thread, or remind me if they haven't replied. Pick a date opens the calendar for anything else.
Playground
Press the button, then change the controls. The code under the preview lists only the props you changed.
import { TimePresetsMenu } from '@/ui/time-presets-menu';<TimePresetsMenu title="Snooze until" />Snooze
From the thread's Snooze button. Two presets a recruiter's week needs, each with the day and time it lands on, then Pick a date… for anything else. A pick acts at once and an Undo toast confirms it.
import { ClockIcon } from '@phosphor-icons/react';
import { Button } from '@/ui/button';
import { TimePresetsMenu } from '@/ui/time-presets-menu';
import { toast } from '@/ui/toast';
// From the thread's Snooze button. Two presets, then Pick a date… for anything else. A pick acts at once with an Undo toast.
export default function TimePresetsSnooze() {
return (
<TimePresetsMenu
trigger={
<Button>
<ClockIcon />
Snooze
</Button>
}
title="Snooze until"
presets={[
{ id: 'tomorrow', label: 'Tomorrow', when: 'Fri 16 Oct, 09:00' },
{ id: 'next-week', label: 'Next week', when: 'Mon 19 Oct, 09:00' },
]}
onSelect={(preset) => toast.add({ title: `Snoozed until ${preset.when}`, actionProps: { children: 'Undo' } })}
onPickDate={() => toast.add({ title: 'The calendar opens here' })}
/>
);
}Remind me, with a default
From Send and remind me. The member's own default sits third, marked "· default" on the selected fill, with one line on what happens next.
import { Button } from '@/ui/button';
import { TimePresetsMenu } from '@/ui/time-presets-menu';
import { toast } from '@/ui/toast';
// From Send and remind me. The member's own default sits third, marked, with one line on what happens next.
export default function TimePresetsRemind() {
return (
<TimePresetsMenu
trigger={<Button variant="primary">Send and remind me</Button>}
title="Remind me if Maya hasn’t replied"
presets={[
{ id: 'tomorrow', label: 'Tomorrow', when: 'Fri 16 Oct, 09:00' },
{ id: 'next-week', label: 'Next week', when: 'Mon 19 Oct, 09:00' },
{ id: 'default', label: 'In 3 working days', when: 'Tue 20 Oct, 09:00', isDefault: true },
]}
help="If no reply by then, it comes back to Needs reply with a nudge drafted. A reply cancels it. Weekends don’t count."
onSelect={(preset) => toast.add({ title: `Sent. Reminder on ${preset.when}`, actionProps: { children: 'Undo' } })}
onPickDate={() => toast.add({ title: 'The calendar opens here' })}
/>
);
}When things go wrong
A long trigger label cuts off; the menu keeps its width. When the snooze fails to save, the error toast says what happened and what to do, and stays until closed. Each preset acts once: the menu closes on the first press.
import { ClockIcon } from '@phosphor-icons/react';
import { Button } from '@/ui/button';
import { TimePresetsMenu } from '@/ui/time-presets-menu';
import { toast } from '@/ui/toast';
// The snooze fails to save: the error toast stays until closed and says what to do. The thread stays where it was.
export default function TimePresetsUnhappy() {
return (
<TimePresetsMenu
trigger={
<Button>
<ClockIcon />
Snooze Zoë O’Brien-Smith’s reply about the Senior Data Engineer role
</Button>
}
title="Snooze until"
presets={[
{ id: 'tomorrow', label: 'Tomorrow', when: 'Fri 16 Oct, 09:00' },
{ id: 'next-week', label: 'Next week', when: 'Mon 19 Oct, 09:00' },
]}
onSelect={() => toast.add({ type: 'error', title: 'Couldn’t snooze this thread', description: 'Check your connection and try again.' })}
/>
);
}Motion
The menu uses the Popup motion, as every menu. Press Replay to close and open it again.
import { ClockIcon } from '@phosphor-icons/react';
import { useState } from 'react';
import { Button } from '@/ui/button';
import { TimePresetsMenu } from '@/ui/time-presets-menu';
// Replay closes the menu, then opens it again, so the 120ms exit and the 160ms entry run back to back.
export default function TimePresetsMotion() {
const [open, setOpen] = useState(false);
const replay = () => {
setOpen(false);
setTimeout(() => setOpen(true), 400);
};
return (
<div className="flex items-start gap-150">
<TimePresetsMenu
trigger={
<Button>
<ClockIcon />
Snooze
</Button>
}
title="Snooze until"
presets={[
{ id: 'tomorrow', label: 'Tomorrow', when: 'Fri 16 Oct, 09:00' },
{ id: 'next-week', label: 'Next week', when: 'Mon 19 Oct, 09:00' },
]}
onSelect={() => {}}
open={open}
onOpenChange={setOpen}
modal={false}
/>
<Button variant="ghost" onClick={replay}>
Replay
</Button>
</div>
);
}