Display menu
One popover for how a View reads: Sort, Group and Fields, each opening its own page, then the shared View actions.
Playground
Change the controls to try each prop. Open Display in the preview to walk its pages; the code lists only the props you changed.
import { DisplayMenu } from '@/ui/display-menu';<DisplayMenu />Sort, group and fields
Each row shows its current value in subtle text and opens a page. Sort carries a direction button once a field is chosen. Sort and Group list only the fields the server can sort or group; the chosen one takes the selected fill and a check. Fields lists shown columns in table order with drag handles, then Hidden; the name column has no handle and can't be hidden. Every change edits the table beside it at once.
| Candidate | Company | Stage | Updated |
|---|---|---|---|
| Maya Chen | Halcyon Bank | CV sent | 2026-10-14 |
| Priya Shah | Halcyon Bank | CV sent | 2026-10-09 |
| Daniel Okafor | Orbital Freight | Placed | 2026-10-03 |
import { useState } from 'react';
import { type DisplayField, DisplayMenu } from '@/ui/display-menu';
import { GroupRow } from '@/ui/group-row';
import { Table, type TableColumn, type TableSort } from '@/ui/table';
type Candidate = { id: string; name: string; company: string; stage: string; updated: string; owner: string };
const candidates: Candidate[] = [
{ id: 'c1', name: 'Maya Chen', company: 'Halcyon Bank', stage: 'CV sent', updated: '2026-10-14', owner: 'Joseph Ariel' },
{ id: 'c2', name: 'Daniel Okafor', company: 'Orbital Freight', stage: 'Placed', updated: '2026-10-03', owner: 'Sara Lee' },
{ id: 'c3', name: 'Priya Shah', company: 'Halcyon Bank', stage: 'CV sent', updated: '2026-10-09', owner: 'Joseph Ariel' },
];
const fields: DisplayField[] = [
{ key: 'name', label: 'Candidate', kind: 'text', sortable: true, pinned: true },
{ key: 'company', label: 'Company', kind: 'company', sortable: true, groupable: true },
{ key: 'stage', label: 'Stage', kind: 'status', sortable: true, groupable: true },
{ key: 'updated', label: 'Updated', kind: 'timestamp', sortable: true },
{ key: 'owner', label: 'Owner', kind: 'person', sortable: true, groupable: true },
];
// Display edits the table beside it: sort, group and the columns. The example sorts and groups the rows itself.
export default function DisplayMenuDefault() {
const [sort, setSort] = useState<TableSort | null>({ key: 'updated', direction: 'desc' });
const [group, setGroup] = useState<string | null>(null);
const [shown, setShown] = useState(['name', 'company', 'stage', 'updated']);
const columns: TableColumn<Candidate>[] = shown.map((key) => ({ key, label: fields.find((field) => field.key === key)!.label, cell: (row: Candidate) => row[key as keyof Candidate] }));
const rows = [...candidates].sort((a, b) => {
const by = (key: keyof Candidate) => a[key].localeCompare(b[key]);
const grouped = group ? by(group as keyof Candidate) : 0;
if (grouped) return grouped;
return sort ? (sort.direction === 'asc' ? 1 : -1) * by(sort.key as keyof Candidate) : 0;
});
return (
<div className="flex w-full flex-col gap-100">
<div className="flex justify-end">
<DisplayMenu
fields={fields}
sort={sort}
onSortChange={setSort}
group={group}
onGroupChange={setGroup}
shown={shown}
onShownChange={setShown}
onSaveAsNew={() => {}}
onRename={() => {}}
onDelete={() => {}}
restoreDefaults={[{ key: 'all', label: 'All candidates' }, { key: 'mine', label: 'Mine' }]}
onRestore={() => {}}
/>
</div>
<Table
label="Candidates"
columns={columns}
rows={rows}
sort={sort}
groupOf={group ? (row) => row[group as keyof Candidate] : undefined}
groupRow={(item) => <GroupRow field={fields.find((field) => field.key === group)!.label} value={item.key} count={item.rows.length} span={item.span} />}
/>
</div>
);
}Someone who isn't an Owner or Admin
Everyone can save as new and rename. Only Owners and Admins delete and restore, so those items don't show.
import { type DisplayField, DisplayMenu } from '@/ui/display-menu';
const fields: DisplayField[] = [
{ key: 'name', label: 'Company', kind: 'text', sortable: true, pinned: true },
{ key: 'sector', label: 'Sector', kind: 'tags' },
{ key: 'owner', label: 'Owner', kind: 'person', sortable: true, groupable: true },
];
// A Member saves as new and renames; only Owners and Admins delete and restore, so those items don't show.
export default function DisplayMenuMember() {
return <DisplayMenu data-replay="" fields={fields} defaultShown={['name', 'sector', 'owner']} onSaveAsNew={() => {}} onRename={() => {}} />;
}While a View saves
Every View item disables until the save ends. The only View can't be deleted, and Delete View says why in its tooltip.
import { type DisplayField, DisplayMenu } from '@/ui/display-menu';
const fields: DisplayField[] = [
{ key: 'name', label: 'Job', kind: 'text', sortable: true, pinned: true },
{ key: 'status', label: 'Status', kind: 'status', sortable: true, groupable: true },
];
// While a View save is pending every View item disables. The only View can't be deleted, and says why.
export default function DisplayMenuSaving() {
return (
<DisplayMenu
fields={fields}
defaultShown={['name', 'status']}
busy
onSaveAsNew={() => {}}
onRename={() => {}}
onDelete={() => {}}
deleteDisabled="This is the only View. Save another View first."
/>
);
}Command palette
The ⌘K box. Search every record, run an action, go to a page or ask Flo, from anywhere. It grows out of the Ask bar and folds back into it.
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.