Lozenge dropdown trigger
A stage lozenge that opens the stage menu. You change an Application's stage where you read it, in the cell or on the record.
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 { LozengeDropdownTrigger } from '@/ui/lozenge-dropdown-trigger';<LozengeDropdownTrigger label="Stage" />Default
The Stage row of an Application. Hover or focus shows the 12px chevron; Enter, Space or a click opens the stage menu with search on top. The new stage shows at once and a spinner holds the chevron's place until it saves, then a toast offers Undo.
import { useState } from 'react';
import { LozengeDropdownTrigger } from '@/ui/lozenge-dropdown-trigger';
import { toast } from '@/ui/toast';
import { stages } from './stages';
// The Stage row of an Application: the new stage shows at once, the spinner holds the chevron's place until it saves.
export default function Stage() {
const [stage, setStage] = useState('interview');
const [saving, setSaving] = useState(false);
return (
<LozengeDropdownTrigger
label="Stage"
items={stages}
value={stage}
loading={saving}
onValueChange={(next) => {
const before = stage;
setStage(next);
setSaving(true);
setTimeout(() => {
setSaving(false);
toast.add({ title: `Maya Chen moved to ${stages.find((s) => s.value === next)?.label}`, actionProps: { children: 'Undo', onClick: () => setStage(before) } });
}, 900);
}}
/>
);
}Appearances
Each stage keeps one hue everywhere, one per phase (Decided 1): Short list and Interview grey, CV sent blue, Offer purple, Placed green, closed red. The trigger only adds the chevron.
import { LozengeDropdownTrigger } from '@/ui/lozenge-dropdown-trigger';
import { stages } from './stages';
// One hue per phase: the trigger only adds the chevron.
export default function Appearances() {
return (
<div className="flex flex-wrap items-center gap-150">
{stages.map((stage) => (
<LozengeDropdownTrigger key={stage.value} label="Stage" items={stages} value={stage.value} />
))}
</div>
);
}Saving and read only
Saving: a spinner in the chevron's place, and the menu waits. Read only, for archived rows and members without edit rights: a plain lozenge with no button.
import { LozengeDropdownTrigger } from '@/ui/lozenge-dropdown-trigger';
import { stages } from './stages';
// Saving holds a spinner; read only (an archived row, a member without edit rights) is a plain lozenge.
export default function States() {
return (
<div className="flex flex-wrap items-center gap-300">
<LozengeDropdownTrigger label="Stage" items={stages} value="offer" loading />
<LozengeDropdownTrigger label="Stage" items={stages} value="placed" readOnly />
</div>
);
}Failed move and long names
A move that fails puts the old stage back and says so in an error toast that stays until closed. A long stage name ends in an ellipsis at 200px.
import { useState } from 'react';
import { LozengeDropdownTrigger, type LozengeOption } from '@/ui/lozenge-dropdown-trigger';
import { toast } from '@/ui/toast';
import { stages } from './stages';
const long: LozengeOption[] = [...stages, { value: 'final', label: 'Final interview with the hiring committee', phase: 'client' }];
// A move that fails puts the old stage back and says so in an error toast that stays until closed. A long stage name
// ends in an ellipsis at 200px.
export default function Failure() {
const [stage, setStage] = useState('final');
const [saving, setSaving] = useState(false);
return (
<LozengeDropdownTrigger
label="Stage"
items={long}
value={stage}
loading={saving}
onValueChange={(next) => {
const before = stage;
setStage(next);
setSaving(true);
setTimeout(() => {
setSaving(false);
setStage(before);
toast.add({ type: 'error', title: 'Couldn’t move Nguyễn Thị Minh Khai', description: 'The stage is back as it was. Check your connection and try again.' });
}, 900);
}}
/>
);
}Filter builder
Filter opens a searchable field list. A field opens its conditions and a value editor for its type. Each filter then shows as a chip in the toolbar; filters combine with and.
Activity timeline
A record's history, newest first: who did what, with the before and after values, plus its emails, notes and meetings in one list.