Table Schema
The table schema is a type-safe builder for defining your entire table in one place. Instead of manually wiring up columns.tsx, filterFields, sheetFields, and a filter state schema separately, a single schema definition generates all of them.
Defining a Schema
import {
col,
createTableSchema,
type InferTableType,
} from "@/lib/table-schema";
const LEVELS = ["error", "warn", "info", "debug"] as const;
const METHODS = ["GET", "POST", "PUT", "DELETE"] as const;
export const tableSchema = createTableSchema({
level: col.presets.logLevel(LEVELS).description("Log severity"),
date: col.presets.timestamp().label("Date").size(200).sheet(),
latency: col.presets
.duration("ms")
.label("Latency")
.sortable()
.size(110)
.sheet(),
status: col.presets.httpStatus().label("Status").size(60),
method: col.presets.httpMethod(METHODS).size(69),
host: col.string().label("Host").size(125).sheet(),
path: col.presets.pathname().label("Path").size(130).sheet(),
traceId: col.presets.traceId().label("Request ID").hidden().sheet(),
headers: col.record().label("Headers").sheetOnly().sheet(),
});
// Row type inferred from the schema
export type ColumnSchema = InferTableType<typeof tableSchema.definition>;Column Factories
Choose the factory based on your data type:
col.string(); // string — text display, input filter
col.number(); // number — number display, input filter
col.boolean(); // boolean — boolean display, checkbox filter
col.timestamp(); // Date — timestamp display, timerange filter
col.enum(values); // union — badge display, checkbox filter
col.array(item); // array — badge display, checkbox filter
col.record(); // Record — text display, not filterable
col.select(); // boolean — row selection checkbox columnEach factory returns a ColBuilder<T, F> where T is the inferred TypeScript type and F constrains which filter types are valid at compile time:
| Factory | Allowed Filters | Notes |
|---|---|---|
col.string() | "input" | Text search |
col.number() | "input", "slider", "checkbox" | Slider for ranges, checkbox for discrete values |
col.boolean() | "checkbox" | Pre-wired with true/false options |
col.timestamp() | "timerange" | Date range picker |
col.enum(values) | "checkbox" | Options auto-derived from values |
col.array(col.enum(values)) | "checkbox" | Multi-value tags, regions, labels |
col.record() | none (never) | Use .sheetOnly() for detail drawers |
col.select() | none (never) | Row selection checkbox + floating bar |
Row Selection with col.select()
col.select() adds a checkbox column for row selection. It renders a select-all checkbox in the header and a per-row checkbox in each cell. Pair it with DataTableFloatingBar to show bulk actions when rows are selected.
export const tableSchema = createTableSchema({
select: col.select().size(37),
// ...other columns
});
col.select()is not filterable, not sortable, and excluded from the sheet. It should typically be the first column in your schema. See DataTableFloatingBar for the bulk action bar.
Presets
Pre-configured builders for common patterns. All remain fully customizable via chaining.
col.presets.logLevel(["error", "warn", "info", "debug"]);
// → enum + badge + checkbox + defaultOpen
col.presets.httpMethod(["GET", "POST", "PUT", "DELETE"]);
// → enum + text display + checkbox
col.presets.httpStatus();
// → number + checkbox with common codes (200, 201, 204, 301, ..., 504)
// Custom codes: col.presets.httpStatus([200, 400, 500])
col.presets.duration("ms");
// → number + formatted display with unit + slider (0–5000)
// Custom bounds: col.presets.duration("s", { min: 0, max: 60 })
col.presets.timestamp();
// → Date + relative time display + timerange filter + sortable
col.presets.traceId();
// → string + code display + not filterable
col.presets.pathname();
// → string + text display + input filterBuilder Methods
All methods return a new builder instance (immutable) for fluent chaining.
Label & Description
col
.string()
.label("Host") // Column header label (required)
.description("Origin server"); // For AI agents / MCP tools (not shown in UI)Descriptions are essential for AI filter accuracy. Without them, the AI only sees field names and types, which can lead to ambiguous results. Add
.description()to any column you want the AI to handle well.
Display
Controls how the cell value renders. Built-in display types:
| Type | Use case |
|---|---|
"text" | Plain text with overflow tooltip (default for strings) |
"code" | Monospace — IDs, paths, hashes |
"number" | Formatted number with optional unit suffix |
"bar" | Horizontal bar with min/max range and optional unit |
"heatmap" | Background color intensity based on min/max range |
"badge" | Colored chip (default for enums) |
"timestamp" | Relative time ("3m ago"), absolute on hover |
"boolean" | Checkmark / dash icon |
"star" | Filled yellow star (true) / outlined muted star (false) |
"status-code" | HTTP status code coloring |
"level-indicator" | Severity dot indicator |
"custom" | Developer-supplied JSX (not serializable) |
col.number().display("number", { unit: "ms" })
col.number().display("bar", { min: 0, max: 5000, unit: "ms" })
col.number().display("heatmap", { min: 0, max: 100 })
col.enum(v).display("badge", { colorMap: { error: "#ef4444", warn: "#f59e0b" } })
col.enum(v).display("custom", {
cell: (value, row) => <MyComponent value={value} />,
})Filtering
col.string().filterable("input")
col.number().filterable("slider", { min: 0, max: 5000 })
col.enum(v).filterable("checkbox")
col.enum(v).filterable("checkbox", {
options: v.map(v => ({ label: v, value: v })),
component: (props) => <StatusBadge value={props.value} />,
})
col.timestamp().filterable("timerange")
col.string().notFilterable() // Disables filtering (F becomes never)
col.enum(v).defaultOpen() // Expand in filter sidebar by default
col.timestamp().commandDisabled() // Exclude from command paletteFields with
.commandDisabled()are hidden from the manual command palette but still available to the AI. This is useful for fields like date ranges that are easier to express in natural language (e.g., "last 24 hours"). See AI Filters — Schema Considerations.
Passing a filter type not in F is a compile-time error — e.g. col.string().filterable("slider") won't compile.
Visibility & Layout
col.string().hidden(); // Hidden by default (toggleable in column menu)
col.enum(v).hideHeader(); // Hide header label, keep column visible
col.string().resizable(); // Enable drag-to-resize
col.string().size(125); // Fixed width in pixels (initial width if resizable)Sorting & Optionality
col.number().sortable(); // Click-to-sort on column header
col.string().optional(); // T becomes T | undefined in InferTableTypeSheet (Row Detail Drawer)
col.string().sheet() // Include in detail drawer
col.string().sheet({ label: "Server", skeletonClassName: "w-24" })
col.number().sheet({
component: (row) => <>{row.latency}ms</>,
skeletonClassName: "w-16",
})
col.record().sheetOnly() // hidden + notFilterable + enableHiding: falseGenerators
The schema drives four generators that produce everything the table components need.
import {
generateColumns,
generateFilterFields,
generateFilterSchema,
generateSheetFields,
getDefaultColumnVisibility,
} from "@/lib/table-schema";
// TanStack Table ColumnDef[]
const columns = generateColumns<ColumnSchema>(tableSchema.definition);
// Filter sidebar and command palette fields
const filterFields = generateFilterFields<ColumnSchema>(tableSchema.definition);
// Row detail drawer fields
const sheetFields = generateSheetFields<ColumnSchema>(tableSchema.definition);
// Initial column visibility from .hidden() columns
const defaultColumnVisibility = getDefaultColumnVisibility(
tableSchema.definition,
);You can append custom virtual columns that span multiple fields:
const allColumns = [
...generateColumns<ColumnSchema>(tableSchema.definition),
{
id: "timing",
header: "Timing Phases",
cell: ({ row }) => <TimingBar row={row} />,
size: 130,
},
];generateFilterSchema
Bridges the table schema to filter state by generating a filter schema. Add non-column state fields (sort, pagination, live mode) alongside:
import { createSchema, field } from "@/lib/store/schema";
export const filterSchema = createSchema({
...generateFilterSchema(tableSchema.definition).definition,
sort: field.sort(),
live: field.boolean().default(false),
size: field.number().default(40),
});The
/infiniteroute writes the filter state schema manually instead of usinggenerateFilterSchemafor better TypeScript inference. Both approaches work — usegenerateFilterSchemafor convenience, manual for full control.
The mapping from column types to filter state field types:
| Column + Filter | filter state Field |
|---|---|
col.string() + "input" | field.string() |
col.number() + "input" | field.number() |
col.number() + "slider" | field.array(field.number()).delimiter("-") |
col.number() + "checkbox" | field.array(field.number()).delimiter(",") |
col.enum(v) + "checkbox" | field.array(field.stringLiteral(v)) |
col.boolean() + "checkbox" | field.array(field.boolean()).delimiter(",") |
col.timestamp() + "timerange" | field.array(field.timestamp()).delimiter("-") |
See the Builder page for visual schema creation, serialization, and AI integration.
