Narrow a column to some of its values: those that occur, a range between two bounds, or a condition the application defines. Reach for it when people look for records by a field rather than by free text.
import { columnFilter, rowFilter } from "@umriss-ui/table";
Every condition stands in the table toolbar with the count of matches, a cross to lift it and “Reset” for all. A click on a condition opens its filter again.
An absent value matches no condition. The list filter offers only values the pre-filter admits, so it never gives away rows a user may not see.
filter="list" offers the values found in the column, an absent one included; each condition then stands in the table toolbar.
import { useTable } from "@umriss-ui/table";
interface Incident {
id: string;
service: string;
assignee: string | null;
}
const INCIDENTS: Incident[] = [
{ id: "INC-1048", service: "Checkout", assignee: "Jonas Keller" },
{ id: "INC-1047", service: "Webhooks", assignee: null },
{ id: "INC-1046", service: "Image service", assignee: "Tomasz Nowak" },
{ id: "INC-1045", service: "Sign-in", assignee: "Ada Mwangi" },
{ id: "INC-1041", service: "Checkout", assignee: null },
];
export default function ListFilter() {
const { Table, Column } = useTable(INCIDENTS, { rowKey: (i) => i.id });
return (
<Table ariaLabel="Incidents">
<Column value="id" label="Incident" rowHeader />
<Column value="service" label="Service" filter="list" />
<Column value="assignee" label="Assignee" filter="list" />
</Table>
);
}
filter="range" gives numbers and dates two inclusive bounds, either left open; date bounds are whole calendar days.
import { useTable } from "@umriss-ui/table";
interface Invoice {
id: string;
supplier: string;
amount: number;
due: Date;
}
const INVOICES: Invoice[] = [
{ id: "INV-26-0318", supplier: "Brandlow Office Supply", amount: 1951.24, due: new Date(2026, 3, 15) },
{ id: "INV-26-0317", supplier: "Kettering & Shaw Events", amount: 15_110, due: new Date(2026, 3, 12) },
{ id: "INV-26-0309", supplier: "Nimbrel Software", amount: 7288, due: new Date(2026, 3, 8) },
{ id: "INV-26-0302", supplier: "Fenwright Legal", amount: 3770, due: new Date(2026, 2, 16, 23, 45) },
{ id: "INV-26-0226", supplier: "Corrin Travel", amount: 8117.2, due: new Date(2026, 2, 12) },
{ id: "INV-26-0221", supplier: "Stellbrook Consulting", amount: 8700, due: new Date(2026, 2, 23) },
];
export default function RangeFilter() {
const { Table, Column } = useTable(INVOICES, { rowKey: (i) => i.id });
return (
<Table ariaLabel="Invoices">
<Column value="id" label="Invoice" rowHeader />
<Column value="supplier" label="Supplier" />
<Column value="amount" label="Amount (€)" format={{ decimals: 2 }} filter="range" aggregate="sum" />
<Column value="due" label="Due" filter="range" format="date" />
</Table>
);
}
columnFilter takes matches, describe and the Input for the panel; the header funnel, the panel and the condition come from the table.
import { Checkbox } from "@umriss-ui/core";
import { columnFilter, useTable } from "@umriss-ui/table";
/* Fixed so the example looks the same every day; an application would
write `new Date()`. `setCondition(null)` lifts the condition. */
const TODAY = new Date(2026, 2, 17);
const overdue = columnFilter<Date, true>({
matches: (due) => due.getTime() < TODAY.getTime(),
Input: ({ condition, setCondition }) => (
<Checkbox label="Overdue only" checked={condition === true} onChange={(event) => setCondition(event.target.checked ? true : null)} />
),
describe: () => "overdue",
});
interface Invoice {
id: string;
supplier: string;
due: Date;
}
const INVOICES: Invoice[] = [
{ id: "INV-26-0318", supplier: "Brandlow Office Supply", due: new Date(2026, 3, 15) },
{ id: "INV-26-0309", supplier: "Nimbrel Software", due: new Date(2026, 3, 8) },
{ id: "INV-26-0226", supplier: "Corrin Travel", due: new Date(2026, 2, 12) },
{ id: "INV-26-0302", supplier: "Fenwright Legal", due: new Date(2026, 2, 16) },
{ id: "INV-26-0221", supplier: "Stellbrook Consulting", due: new Date(2026, 2, 23) },
];
export default function OwnFilterMinimal() {
const { Table, Column } = useTable(INVOICES, { rowKey: (i) => i.id });
return (
<Table ariaLabel="Invoices">
<Column value="id" label="Invoice" rowHeader />
<Column value="supplier" label="Supplier" />
<Column value="due" label="Due" format="date" filter={overdue} />
</Table>
);
}
A filter from columnFilter<number, …> fits every number column of every table; an absent value matches none of its conditions.
import { RadioGroup } from "@umriss-ui/core";
import { columnFilter, useTable } from "@umriss-ui/table";
type Load = "part-time" | "full-time";
const LOADS = [
{ value: "part-time", label: "Part-time – under 32 h" },
{ value: "full-time", label: "Full-time – 32 h and more" },
] as const;
const workload = columnFilter<number, Load>({
matches: (hours, load) => (load === "part-time" ? hours < 32 : hours >= 32),
Input: ({ condition, setCondition, column }) => (
<RadioGroup aria-label={column.label} size="sm" options={LOADS} value={condition} onChange={(load) => setCondition(load)} />
),
describe: (load) => load,
});
interface Person {
id: string;
name: string;
role: string;
capacity: number | null;
}
const PEOPLE: Person[] = [
{ id: "maya", name: "Maya Lindgren", role: "Product manager", capacity: 32 },
{ id: "arjun", name: "Arjun Mehta", role: "Developer", capacity: 40 },
{ id: "noah", name: "Noah Fischer", role: "Designer", capacity: 24 },
{ id: "kofi", name: "Kofi Mensah", role: "Developer", capacity: 32 },
{ id: "david", name: "David Kowalski", role: "QA engineer", capacity: 20 },
{ id: "new", name: "Starts in April", role: "Developer", capacity: null },
];
export default function OwnFilter() {
const { Table, Column } = useTable(PEOPLE, { rowKey: (p) => p.id });
return (
<Table ariaLabel="Team">
<Column value="name" label="Person" rowHeader />
<Column value="role" label="Role" />
<Column value="capacity" label="Hours a week" filter={workload} />
</Table>
);
}
t.setFilter sets a condition from a quick filter or a link, null lifts it; t.view carries it for initialView to restore.
import { Badge, Button, Stack, Text } from "@umriss-ui/core";
import { Search, Toolbar, useTable } from "@umriss-ui/table";
const TONE = { delivered: "success", "out for delivery": "accent", "failed attempt": "danger" } as const;
type Status = keyof typeof TONE;
interface Shipment {
id: string;
customer: string;
status: Status;
}
const SHIPMENTS: Shipment[] = [
{ id: "FP-1004210", customer: "Holloway Garden Supplies", status: "delivered" },
{ id: "FP-1004223", customer: "Oakridge Pharmacy", status: "failed attempt" },
{ id: "FP-1004236", customer: "Brixley Cycles", status: "out for delivery" },
{ id: "FP-1004249", customer: "Tamsin's Bakery", status: "delivered" },
{ id: "FP-1004262", customer: "Pellham Hardware", status: "failed attempt" },
{ id: "FP-1004275", customer: "Greywick Studio", status: "out for delivery" },
];
const QUICK_FILTERS: readonly Status[] = ["out for delivery", "failed attempt"];
/* The table holds the condition; the buttons only ask it to change and read
`t.filter` to show which one stands. That is why the column's funnel, the
chip in the toolbar, "Reset" and `t.view` all agree with them without a
line of synchronisation. `setFilter` takes the
condition of the column's filter - for `filter="list"` the chosen values -
and passes over a column that has none, with a warning in development.
Something the user may lift belongs here; `preFilter` is for rows the user
may not see at all. A restriction by no column, or by several fields at
once, is a row filter (the examples below). */
export default function ConditionsFromOutside() {
const t = useTable(SHIPMENTS, { rowKey: (s) => s.id });
const { Table, Column } = t;
return (
<Stack gap={3}>
<Stack direction="row" gap={2} wrap>
{QUICK_FILTERS.map((status) => {
const active = JSON.stringify(t.filter.status) === JSON.stringify([status]);
return (
<Button
key={status}
size="sm"
variant={active ? "primary" : "secondary"}
aria-pressed={active}
onClick={() => t.setFilter("status", [status])}
>
{SHIPMENTS.filter((s) => s.status === status).length} {status}
</Button>
);
})}
<Button size="sm" variant="ghost" onClick={() => t.setFilter("status", null)}>
All statuses
</Button>
</Stack>
<Table ariaLabel="Shipments">
<Toolbar>
<Search placeholder="Shipment or customer" />
</Toolbar>
<Column value="id" label="Shipment" rowHeader />
<Column value="customer" label="Customer" />
<Column value="status" label="Status" filter="list">
{(status) => <Badge tone={TONE[status]}>{status}</Badge>}
</Column>
</Table>
<Stack gap={1}>
<Text as="span" size="sm" tone="secondary">
What the application would keep
</Text>
<Text as="code" size="sm" mono>
{JSON.stringify(t.view)}
</Text>
</Stack>
</Stack>
);
}
A row filter asks the whole row; a control of one's own sets it with t.setFilter and reads it with t.conditionOf. The table holds the choice, counts it and resets it - no column needed.
import { MultiSelect } from "@umriss-ui/core";
import { Pagination, Search, Toolbar, rowFilter, useTable } from "@umriss-ui/table";
/* Data from the logistics world, written out here so the example runs on its own. */
/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const MINUTE = 60_000;
/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
const NOW = at(10, 30);
interface Vehicle {
id: string;
plate: string;
type: "van" | "e-van" | "truck";
/** Payload, in kg. */
capacity: number;
depot: string;
}
const VEHICLES: readonly Vehicle[] = [
{ id: "v1", plate: "FP 214 K", type: "van", capacity: 1200, depot: "north" },
{ id: "v2", plate: "FP 377 K", type: "e-van", capacity: 900, depot: "north" },
{ id: "v3", plate: "FP 118 R", type: "truck", capacity: 7500, depot: "north" },
{ id: "v4", plate: "FP 402 R", type: "van", capacity: 1200, depot: "river" },
{ id: "v5", plate: "FP 455 R", type: "e-van", capacity: 900, depot: "river" },
{ id: "v6", plate: "FP 290 E", type: "van", capacity: 1200, depot: "east" },
{ id: "v7", plate: "FP 311 E", type: "e-van", capacity: 900, depot: "east" },
{ id: "v8", plate: "FP 520 E", type: "truck", capacity: 7500, depot: "east" },
];
const DRIVERS = [
{ id: "d1", name: "Martin Hale", depot: "north" },
{ id: "d2", name: "Nadia Petrova", depot: "north" },
{ id: "d3", name: "Owen Carter", depot: "north" },
{ id: "d4", name: "Lucia Romero", depot: "river" },
{ id: "d5", name: "Ben Adeyemi", depot: "river" },
{ id: "d6", name: "Hanna Berg", depot: "east" },
{ id: "d7", name: "Yusuf Demir", depot: "east" },
{ id: "d8", name: "Clara Wendt", depot: "east" },
] as const;
const CUSTOMERS = [
"Holloway Garden Supplies",
"Marlow & Finch Books",
"Oakridge Pharmacy",
"Brixley Cycles",
"Tamsin's Bakery",
"Northfold Office",
"Pellham Hardware",
"Greywick Studio",
"Juniper Lane Florist",
"Ashcombe Dental",
] as const;
interface Stop {
id: string;
shipment: string;
customer: string;
/** The window the customer was promised, `[from, to]`. */
window: readonly [number, number];
/** When the tour plans to be there. */
arrival: number;
}
interface Tour {
id: string;
vehicle: string;
driver: string;
depot: string;
/** Leaves the depot, loaded; back at the depot. */
from: number;
to: number;
/** Minutes of loading before `from`. */
loading: number;
km: number;
stops: readonly Stop[];
}
/** One tour per vehicle. Stops every half hour or so, each with a two-hour
window; every seventh arrival misses its window - the late ones a
dispatcher looks for. */
function tour(index: number): Tour {
const r = random(700 + index);
const vehicle = VEHICLES[index]!;
const truck = vehicle.type === "truck";
const from = at(truck ? 6 : 7, index % 2 === 0 ? 0 : 30);
const count = truck ? 5 : 8 + Math.floor(r() * 4);
const stops: Stop[] = [];
let arrival = from + (20 + Math.floor(r() * 15)) * MINUTE;
for (let i = 0; i < count; i++) {
const n = index * 20 + i;
const slot = Math.floor((arrival - at(0)) / (2 * 60 * MINUTE)) * 2;
/* A late stop's window closed half an hour or more before the arrival. */
const late = i >= 2 && n % 7 === 3;
const halfHour = 30 * MINUTE;
const windowFrom = late ? Math.floor(arrival / halfHour) * halfHour - 5 * halfHour : at(slot);
stops.push({
id: `s-${index + 1}-${i + 1}`,
shipment: `FP-${(1_004_210 + n * 13).toString()}`,
customer: CUSTOMERS[(n * 3) % CUSTOMERS.length]!,
window: [windowFrom, windowFrom + 2 * 60 * MINUTE],
arrival,
});
arrival += (truck ? 45 : 22) * MINUTE + Math.floor(r() * 18) * MINUTE;
}
return {
id: `T-${String(index + 1).padStart(2, "0")}`,
vehicle: vehicle.id,
driver: DRIVERS[index]!.id,
depot: vehicle.depot,
from,
to: arrival + 30 * MINUTE,
loading: truck ? 45 : 30,
km: Math.round((truck ? 140 : 70) + r() * 40),
stops,
};
}
const TOURS: readonly Tour[] = VEHICLES.map((_, index) => tour(index));
interface Shipment {
id: string;
customer: string;
/** In kg. */
weight: number;
status: "delivered" | "out for delivery" | "failed attempt";
window: readonly [number, number];
tour: string;
}
/** One shipment per stop. Before now, delivered - or a failed attempt, where
nobody was in; after now, out for delivery. */
const SHIPMENTS: readonly Shipment[] = TOURS.flatMap((one) =>
one.stops.map((stop) => {
const n = Number(stop.shipment.slice(3));
return {
id: stop.shipment,
customer: stop.customer,
weight: Math.round((VEHICLES.find((v) => v.id === one.vehicle)!.type === "truck" ? 180 : 2) + ((n * 7919) % 230) / 10),
status: stop.arrival > NOW ? "out for delivery" : n % 11 === 0 ? "failed attempt" : "delivered",
window: stop.window,
tour: one.id,
} satisfies Shipment;
}),
);
/* Defined once, outside the component. The object is the key from here on:
the id is only its name in the view. */
const tours = rowFilter({
id: "tours",
label: "Tours",
matches: (shipment: Shipment, chosen: readonly string[]) => chosen.includes(shipment.tour),
});
const TOUR_OPTIONS = [...new Set(SHIPMENTS.map((s) => s.tour))].map((tour) => ({ value: tour, label: tour }));
export default function FieldNoColumnShows() {
const t = useTable(SHIPMENTS, { rowKey: (s) => s.id, rowFilters: [tours] });
const { Table, Column } = t;
return (
<Table ariaLabel="Shipments by tour">
<Toolbar>
<Search placeholder="Shipment or customer" />
<MultiSelect
aria-label="Tours"
placeholder="All tours"
options={TOUR_OPTIONS}
/* Typed as the filter's condition - no cast. */
value={t.conditionOf(tours) ?? []}
/* The whole new choice, every time; an empty one lifts the condition. */
onChange={(next) => t.setFilter(tours, next.length > 0 ? next : null)}
/>
</Toolbar>
<Column value="id" label="Shipment" rowHeader />
<Column value="customer" label="Customer" />
<Column value="weight" label="Weight (kg)" />
<Pagination />
</Table>
);
}
Hand the table the rows a query answers, as they come - with a fallback that is the same array on every render and loading while nothing has arrived. The filters stay the table's.
import { useCallback, useEffect, useState } from "react";
import { Button, MultiSelect } from "@umriss-ui/core";
import { Pagination, Toolbar, rowFilter, useTable } from "@umriss-ui/table";
/* Data from the logistics world, written out here so the example runs on its own. */
/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const MINUTE = 60_000;
/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
const NOW = at(10, 30);
interface Vehicle {
id: string;
plate: string;
type: "van" | "e-van" | "truck";
/** Payload, in kg. */
capacity: number;
depot: string;
}
const VEHICLES: readonly Vehicle[] = [
{ id: "v1", plate: "FP 214 K", type: "van", capacity: 1200, depot: "north" },
{ id: "v2", plate: "FP 377 K", type: "e-van", capacity: 900, depot: "north" },
{ id: "v3", plate: "FP 118 R", type: "truck", capacity: 7500, depot: "north" },
{ id: "v4", plate: "FP 402 R", type: "van", capacity: 1200, depot: "river" },
{ id: "v5", plate: "FP 455 R", type: "e-van", capacity: 900, depot: "river" },
{ id: "v6", plate: "FP 290 E", type: "van", capacity: 1200, depot: "east" },
{ id: "v7", plate: "FP 311 E", type: "e-van", capacity: 900, depot: "east" },
{ id: "v8", plate: "FP 520 E", type: "truck", capacity: 7500, depot: "east" },
];
const DRIVERS = [
{ id: "d1", name: "Martin Hale", depot: "north" },
{ id: "d2", name: "Nadia Petrova", depot: "north" },
{ id: "d3", name: "Owen Carter", depot: "north" },
{ id: "d4", name: "Lucia Romero", depot: "river" },
{ id: "d5", name: "Ben Adeyemi", depot: "river" },
{ id: "d6", name: "Hanna Berg", depot: "east" },
{ id: "d7", name: "Yusuf Demir", depot: "east" },
{ id: "d8", name: "Clara Wendt", depot: "east" },
] as const;
const CUSTOMERS = [
"Holloway Garden Supplies",
"Marlow & Finch Books",
"Oakridge Pharmacy",
"Brixley Cycles",
"Tamsin's Bakery",
"Northfold Office",
"Pellham Hardware",
"Greywick Studio",
"Juniper Lane Florist",
"Ashcombe Dental",
] as const;
interface Stop {
id: string;
shipment: string;
customer: string;
/** The window the customer was promised, `[from, to]`. */
window: readonly [number, number];
/** When the tour plans to be there. */
arrival: number;
}
interface Tour {
id: string;
vehicle: string;
driver: string;
depot: string;
/** Leaves the depot, loaded; back at the depot. */
from: number;
to: number;
/** Minutes of loading before `from`. */
loading: number;
km: number;
stops: readonly Stop[];
}
/** One tour per vehicle. Stops every half hour or so, each with a two-hour
window; every seventh arrival misses its window - the late ones a
dispatcher looks for. */
function tour(index: number): Tour {
const r = random(700 + index);
const vehicle = VEHICLES[index]!;
const truck = vehicle.type === "truck";
const from = at(truck ? 6 : 7, index % 2 === 0 ? 0 : 30);
const count = truck ? 5 : 8 + Math.floor(r() * 4);
const stops: Stop[] = [];
let arrival = from + (20 + Math.floor(r() * 15)) * MINUTE;
for (let i = 0; i < count; i++) {
const n = index * 20 + i;
const slot = Math.floor((arrival - at(0)) / (2 * 60 * MINUTE)) * 2;
/* A late stop's window closed half an hour or more before the arrival. */
const late = i >= 2 && n % 7 === 3;
const halfHour = 30 * MINUTE;
const windowFrom = late ? Math.floor(arrival / halfHour) * halfHour - 5 * halfHour : at(slot);
stops.push({
id: `s-${index + 1}-${i + 1}`,
shipment: `FP-${(1_004_210 + n * 13).toString()}`,
customer: CUSTOMERS[(n * 3) % CUSTOMERS.length]!,
window: [windowFrom, windowFrom + 2 * 60 * MINUTE],
arrival,
});
arrival += (truck ? 45 : 22) * MINUTE + Math.floor(r() * 18) * MINUTE;
}
return {
id: `T-${String(index + 1).padStart(2, "0")}`,
vehicle: vehicle.id,
driver: DRIVERS[index]!.id,
depot: vehicle.depot,
from,
to: arrival + 30 * MINUTE,
loading: truck ? 45 : 30,
km: Math.round((truck ? 140 : 70) + r() * 40),
stops,
};
}
const TOURS: readonly Tour[] = VEHICLES.map((_, index) => tour(index));
interface Shipment {
id: string;
customer: string;
/** In kg. */
weight: number;
status: "delivered" | "out for delivery" | "failed attempt";
window: readonly [number, number];
tour: string;
}
/** One shipment per stop. Before now, delivered - or a failed attempt, where
nobody was in; after now, out for delivery. */
const SHIPMENTS: readonly Shipment[] = TOURS.flatMap((one) =>
one.stops.map((stop) => {
const n = Number(stop.shipment.slice(3));
return {
id: stop.shipment,
customer: stop.customer,
weight: Math.round((VEHICLES.find((v) => v.id === one.vehicle)!.type === "truck" ? 180 : 2) + ((n * 7919) % 230) / 10),
status: stop.arrival > NOW ? "out for delivery" : n % 11 === 0 ? "failed attempt" : "delivered",
window: stop.window,
tour: one.id,
} satisfies Shipment;
}),
);
/* Stands in for any query hook: `data` stays the same array until an answer
brings new rows, `isPending` holds until the first one. The first answer
comes at once, so that the example stands still; "Reload" takes 800 ms. */
function useShipmentsQuery() {
const [data, setData] = useState<readonly Shipment[] | undefined>(undefined);
const [fetching, setFetching] = useState(0);
useEffect(() => {
const timer = setTimeout(() => setData([...SHIPMENTS]), fetching === 0 ? 0 : 800);
return () => clearTimeout(timer);
}, [fetching]);
const refetch = useCallback(() => {
setData(undefined);
setFetching((n) => n + 1);
}, []);
return { data, isPending: data === undefined, refetch };
}
type Status = Shipment["status"];
const STATUSES: { value: Status; label: string }[] = [
{ value: "out for delivery", label: "Out for delivery" },
{ value: "failed attempt", label: "Failed attempt" },
{ value: "delivered", label: "Delivered" },
];
const statuses = rowFilter({
id: "statuses",
label: "Status",
matches: (shipment: Shipment, chosen: readonly Status[]) => chosen.includes(shipment.status),
});
/* Outside the component: `data ?? []` would be a new array on every render,
and the table would calculate everything anew each time while it waits. */
const NO_SHIPMENTS: readonly Shipment[] = [];
export default function RowsFromARequest() {
const { data, isPending, refetch } = useShipmentsQuery();
/* The answer goes in as it is - not copied into a state of one's own, which
would lag a render behind and could go stale. Rows derived from it (mapped,
joined) belong in a `useMemo` over `data`. The condition survives a reload:
the table holds it, not the rows. */
const t = useTable(data ?? NO_SHIPMENTS, { rowKey: (s) => s.id, rowFilters: [statuses] });
const { Table, Column } = t;
return (
<Table ariaLabel="Shipments from a request" loading={isPending}>
<Toolbar>
<MultiSelect<Status>
aria-label="Status"
placeholder="Every status"
options={STATUSES}
value={t.conditionOf(statuses) ?? []}
onChange={(next) => t.setFilter(statuses, next.length > 0 ? next : null)}
/>
<Button variant="ghost" onClick={refetch}>
Reload
</Button>
</Toolbar>
<Column value="id" label="Shipment" rowHeader />
<Column value="customer" label="Customer" />
<Column value="status" label="Status" />
<Pagination />
</Table>
);
}
"Overdue" is a window that has closed on a shipment not yet delivered - two fields and a moment. The condition carries what varies; describe puts it in the toolbar, where its cross lifts it.
import { Checkbox, Stack, Switch } from "@umriss-ui/core";
import { Pagination, Toolbar, rowFilter, useTable } from "@umriss-ui/table";
/* Data from the logistics world, written out here so the example runs on its own. */
/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const MINUTE = 60_000;
/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
const NOW = at(10, 30);
interface Vehicle {
id: string;
plate: string;
type: "van" | "e-van" | "truck";
/** Payload, in kg. */
capacity: number;
depot: string;
}
const VEHICLES: readonly Vehicle[] = [
{ id: "v1", plate: "FP 214 K", type: "van", capacity: 1200, depot: "north" },
{ id: "v2", plate: "FP 377 K", type: "e-van", capacity: 900, depot: "north" },
{ id: "v3", plate: "FP 118 R", type: "truck", capacity: 7500, depot: "north" },
{ id: "v4", plate: "FP 402 R", type: "van", capacity: 1200, depot: "river" },
{ id: "v5", plate: "FP 455 R", type: "e-van", capacity: 900, depot: "river" },
{ id: "v6", plate: "FP 290 E", type: "van", capacity: 1200, depot: "east" },
{ id: "v7", plate: "FP 311 E", type: "e-van", capacity: 900, depot: "east" },
{ id: "v8", plate: "FP 520 E", type: "truck", capacity: 7500, depot: "east" },
];
const DRIVERS = [
{ id: "d1", name: "Martin Hale", depot: "north" },
{ id: "d2", name: "Nadia Petrova", depot: "north" },
{ id: "d3", name: "Owen Carter", depot: "north" },
{ id: "d4", name: "Lucia Romero", depot: "river" },
{ id: "d5", name: "Ben Adeyemi", depot: "river" },
{ id: "d6", name: "Hanna Berg", depot: "east" },
{ id: "d7", name: "Yusuf Demir", depot: "east" },
{ id: "d8", name: "Clara Wendt", depot: "east" },
] as const;
const CUSTOMERS = [
"Holloway Garden Supplies",
"Marlow & Finch Books",
"Oakridge Pharmacy",
"Brixley Cycles",
"Tamsin's Bakery",
"Northfold Office",
"Pellham Hardware",
"Greywick Studio",
"Juniper Lane Florist",
"Ashcombe Dental",
] as const;
interface Stop {
id: string;
shipment: string;
customer: string;
/** The window the customer was promised, `[from, to]`. */
window: readonly [number, number];
/** When the tour plans to be there. */
arrival: number;
}
interface Tour {
id: string;
vehicle: string;
driver: string;
depot: string;
/** Leaves the depot, loaded; back at the depot. */
from: number;
to: number;
/** Minutes of loading before `from`. */
loading: number;
km: number;
stops: readonly Stop[];
}
/** One tour per vehicle. Stops every half hour or so, each with a two-hour
window; every seventh arrival misses its window - the late ones a
dispatcher looks for. */
function tour(index: number): Tour {
const r = random(700 + index);
const vehicle = VEHICLES[index]!;
const truck = vehicle.type === "truck";
const from = at(truck ? 6 : 7, index % 2 === 0 ? 0 : 30);
const count = truck ? 5 : 8 + Math.floor(r() * 4);
const stops: Stop[] = [];
let arrival = from + (20 + Math.floor(r() * 15)) * MINUTE;
for (let i = 0; i < count; i++) {
const n = index * 20 + i;
const slot = Math.floor((arrival - at(0)) / (2 * 60 * MINUTE)) * 2;
/* A late stop's window closed half an hour or more before the arrival. */
const late = i >= 2 && n % 7 === 3;
const halfHour = 30 * MINUTE;
const windowFrom = late ? Math.floor(arrival / halfHour) * halfHour - 5 * halfHour : at(slot);
stops.push({
id: `s-${index + 1}-${i + 1}`,
shipment: `FP-${(1_004_210 + n * 13).toString()}`,
customer: CUSTOMERS[(n * 3) % CUSTOMERS.length]!,
window: [windowFrom, windowFrom + 2 * 60 * MINUTE],
arrival,
});
arrival += (truck ? 45 : 22) * MINUTE + Math.floor(r() * 18) * MINUTE;
}
return {
id: `T-${String(index + 1).padStart(2, "0")}`,
vehicle: vehicle.id,
driver: DRIVERS[index]!.id,
depot: vehicle.depot,
from,
to: arrival + 30 * MINUTE,
loading: truck ? 45 : 30,
km: Math.round((truck ? 140 : 70) + r() * 40),
stops,
};
}
const TOURS: readonly Tour[] = VEHICLES.map((_, index) => tour(index));
interface Shipment {
id: string;
customer: string;
/** In kg. */
weight: number;
status: "delivered" | "out for delivery" | "failed attempt";
window: readonly [number, number];
tour: string;
}
/** One shipment per stop. Before now, delivered - or a failed attempt, where
nobody was in; after now, out for delivery. */
const SHIPMENTS: readonly Shipment[] = TOURS.flatMap((one) =>
one.stops.map((stop) => {
const n = Number(stop.shipment.slice(3));
return {
id: stop.shipment,
customer: stop.customer,
weight: Math.round((VEHICLES.find((v) => v.id === one.vehicle)!.type === "truck" ? 180 : 2) + ((n * 7919) % 230) / 10),
status: stop.arrival > NOW ? "out for delivery" : n % 11 === 0 ? "failed attempt" : "delivered",
window: stop.window,
tour: one.id,
} satisfies Shipment;
}),
);
interface Overdue {
/** The moment the windows are read against. */
at: number;
/** Count a failed attempt as overdue too. */
failed: boolean;
}
/* What varies - the moment, the choice about failed attempts - stands in the
condition, never in a closure over the component's state: the table sees a
new condition, it cannot see a closure change. */
const overdue = rowFilter({
id: "overdue",
label: "Overdue",
matches: (shipment: Shipment, o: Overdue) =>
shipment.window[1] < o.at &&
(shipment.status === "out for delivery" || (o.failed && shipment.status === "failed attempt")),
describe: (o) => (o.failed ? "with failed attempts" : "not delivered"),
});
export default function QuestionOverSeveralFields() {
const t = useTable(SHIPMENTS, {
rowKey: (s) => s.id,
rowFilters: [overdue],
pageSize: 5,
/* A view can start with it set, like any condition. */
initialView: { conditions: { overdue: { at: NOW, failed: false } } },
});
const { Table, Column } = t;
const condition = t.conditionOf(overdue);
/* The demo reads its windows at a fixed "now"; an application takes Date.now(). */
const set = (on: boolean, failed: boolean) => t.setFilter(overdue, on ? { at: NOW, failed } : null);
return (
<Stack gap={3}>
<Stack direction="row" gap={4} wrap>
<Switch label="Overdue only" checked={condition !== null} onChange={(e) => set(e.target.checked, condition?.failed ?? false)} />
<Checkbox
label="Count failed attempts"
disabled={condition === null}
checked={condition?.failed ?? false}
onChange={(e) => set(true, e.target.checked)}
/>
</Stack>
<Table ariaLabel="Shipments and their windows">
<Toolbar />
<Column value="id" label="Shipment" rowHeader />
<Column value="customer" label="Customer" />
<Column value="status" label="Status" filter="list" />
<Pagination />
</Table>
</Stack>
);
}
The Input of a filter of your own receives values, the column's values each once and in order, so it offers only thresholds some row meets.
import { RadioGroup } from "@umriss-ui/core";
import { columnFilter, useTable } from "@umriss-ui/table";
const atLeast = columnFilter<number, number>({
matches: (hours, least) => hours >= least,
Input: ({ condition, setCondition, values, column }) => (
<RadioGroup
aria-label={column.label}
size="sm"
options={values.map((hours) => ({ value: String(hours), label: `${hours} h and more` }))}
value={condition === null ? null : String(condition)}
onChange={(hours) => setCondition(Number(hours))}
/>
),
describe: (least) => `${least} h and more`,
});
interface Person {
id: string;
name: string;
role: string;
capacity: number;
}
const PEOPLE: Person[] = [
{ id: "maya", name: "Maya Lindgren", role: "Product manager", capacity: 32 },
{ id: "arjun", name: "Arjun Mehta", role: "Developer", capacity: 40 },
{ id: "noah", name: "Noah Fischer", role: "Designer", capacity: 24 },
{ id: "kofi", name: "Kofi Mensah", role: "Developer", capacity: 32 },
{ id: "david", name: "David Kowalski", role: "QA engineer", capacity: 20 },
];
export default function OfferTheValuesThatOccur() {
const { Table, Column } = useTable(PEOPLE, { rowKey: (p) => p.id });
return (
<Table ariaLabel="Team">
<Column value="name" label="Person" rowHeader />
<Column value="role" label="Role" />
<Column value="capacity" label="Hours a week" filter={atLeast} />
</Table>
);
}
The keys of First table, Popover, Checkbox, Input, MultiSelect, Select and Button apply here.
ColumnFilter<W, B>| Name | Type | Default | Description |
|---|---|---|---|
matchesrequired | (value: Present<W>, condition: B) => boolean | — | Whether a value that is present satisfies the condition. Never called for
an absent value: that satisfies none. Shown in: Write the smallest filter of your own, Offer conditions of your own, Offer the values that occur |
Inputrequired | ComponentType<FilterInputProps<W, B>> | — | What stands in the filter's panel. The footer with "reset" and "done" the
table puts underneath it. Shown in: Write the smallest filter of your own, Offer conditions of your own, Offer the values that occur |
describerequired | (condition: B) => string | — | The text of the condition in the table toolbar, behind the column's name. Shown in: Write the smallest filter of your own, Offer conditions of your own, Offer the values that occur |
FilterInputProps<W, B>| Name | Type | Default | Description |
|---|---|---|---|
conditionrequired | B | null | — | The condition that holds right now; null as long as the filter requires nothing.Shown in: Write the smallest filter of your own, Offer conditions of your own, Offer the values that occur |
setConditionrequired | (condition: B | null) => void | — | Sets the condition; null lifts it.Shown in: Write the smallest filter of your own, Offer conditions of your own, Offer the values that occur |
valuesrequired | readonly Present<W>[] | — | The values that occur in the rows the pre-filter admits – each one once,
ordered, without the absent one. Shown in: Offer the values that occur |
columnrequired | { id: string; label: string } | — | The column whose filter this is. Shown in: Offer conditions of your own, Offer the values that occur |
RowFilter<Z, B>| Name | Type | Default | Description |
|---|---|---|---|
idrequired | string | — | Its name in t.view.conditions and in what server mode reports - the one
place the filter is spelled. Not the id of a column of the same table.Shown in: Filter by a field no column shows, Rows from a request, Ask a question over several fields and 3 more |
labelrequired | string | — | What it is called: before the text of its chip, and in the chip's
accessible names. Shown in: Filter by a field no column shows, Rows from a request, Ask a question over several fields and 3 more |
matchesrequired | (row: Z, condition: B) => boolean | — | Whether the row satisfies the condition. Any question over the whole row;
whatever varies belongs in the condition - the table cannot see a change
in a closure. Shown in: Filter by a field no column shows, Rows from a request, Ask a question over several fields and 3 more |
describe | (condition: B) => string | — | The text of the condition in the table toolbar, behind the label. Without
it the condition has no chip: the application's control shows it. Shown in: Ask a question over several fields, A dispatcher's bar (Toolbar controls) |
Present<W>The value without the absence - that is what children gets.
type Present<W> = Exclude<W, Absent>;AbsentA value that is not there: null or undefined.
type Absent = null | undefined;What umriss deliberately does not build, and why: ADR-0032.