Table / data grid
| nockerl-design · docs site | Streaming | 137,730 | $2.41 | now | |
|---|---|---|---|---|---|
| api-server · gateway refactor | Idle | 89,210 | $1.58 | 12m | |
| credential-store · allowlist audit | Needs attention | 41,980 | $0.74 | 1h | |
| dueydo · failed deploy | Failed | 15,240 | $0.29 | 3h | |
| Total · 5 sessions | 348,710 | $6.14 |
Loading: skeleton rows hold the column grid
| Session | Tokens | Cost | |
|---|---|---|---|
Sorted by updated (asc) · 1 selected · page 1 of 2 · comfortable. The grid is live: change --color-divider and every hairline moves.
// Web: React, consuming @dizyx/nockerl-tokens. NockerlTable ships from// @dizyx/nockerl-react: the typed columns, sortable headers, checkbox selection// (select-all + per-row), sticky header, footer summary, and loading / empty// states are all in the package. v1 ships columns · sort · sticky · selection ·// empty · loading · density · footer; row virtualization + inline cell editing// are a later slice. (For a single-column sectioned list use NockerlListItem; for// one record's fields use Key-value.) NockerlTable is CONTROLLED. The caller// sorts `rows` (and paginates with NockerlPagination); each column's render(row)// supplies the cell content.import { useState } from 'react';import { NockerlTable, type NockerlTableSort } from '@dizyx/nockerl-react';
export function SessionsTable({ rows, sort, selected, onSort, onSelect }: SessionsTableProps) { return ( <NockerlTable rows={rows} // rendered verbatim, so sort/paginate before passing in getRowId={(r) => r.id} columns={[ { key: 'name', header: 'Session', sortable: true, render: (r) => <NameCell status={r.status} label={r.name} /> }, { key: 'status', header: 'Status', align: 'center', sortable: true, // chips/badges → center render: (r) => <StatusPill status={r.status} /> }, // The alignment CANON: text left (default), numbers right (align: 'end'), chips center // (align: 'center'); a mono column right-aligns by default. mono renders tabular figures. { key: 'tokens', header: 'Tokens', align: 'end', mono: true, sortable: true, render: (r) => r.tokens.toLocaleString() }, { key: 'cost', header: 'Cost', align: 'end', mono: true, sortable: true, render: (r) => `$${r.cost.toFixed(2)}` }, { key: 'updated', header: 'Updated', align: 'end', mono: true, sortable: true, render: (r) => r.updatedLabel }, ]} sort={sort} // { key, dir: 'asc' | 'desc' } drives header arrow + aria-sort onSortChange={onSort} selectable // checkbox column + header select-all (indeterminate when partial) selectedIds={selected} onSelectionChange={onSelect} density="comfortable" // 'comfortable' | 'compact' (padding only) stickyHeader // header stays flush while the body scrolls footer={(all) => <SummaryRow rows={all} />} // totals row pinned to the bottom loading={false} // true → skeleton rows that hold the grid empty={<EmptyState />} // shown when there are no rows ariaLabel="Sessions" /> );}// Android: Jetpack Compose (canonical). There is no NockerlDataTable component;// tabular data is a Row of weighted COLUMNS inside a LazyColumn, with mono numeric// cells and HorizontalDivider hairlines (the ClusterSheet idiom). Sorting is state// the caller owns; the header row carries the clickable sort affordance.
@Composablefun SessionTable(rows: List<Session>, sort: SortState, onSort: (SortKey) -> Unit) { val colors = LocalNockerlColors.current Column(modifier = Modifier.fillMaxWidth()) { // Header row is the strong band; each sortable column is a clickable label + arrow. Row( modifier = Modifier.fillMaxWidth() .background(MaterialTheme.colorScheme.surfaceContainerHighest) .padding(horizontal = 16.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, ) { SortHeader("Session", SortKey.NAME, sort, onSort, Modifier.weight(1f)) // text-left SortHeader("Tokens", SortKey.TOKENS, sort, onSort, Modifier.weight(0.5f), end = true) // numeric → right SortHeader("Cost", SortKey.COST, sort, onSort, Modifier.weight(0.4f), end = true) } HorizontalDivider(thickness = 1.dp, color = colors.divider)
// Body: one Row of weighted cells per record; numeric cells are Monospace + right-aligned. LazyColumn { itemsIndexed(rows, key = { _, r -> r.id }) { i, r -> Row( modifier = Modifier.fillMaxWidth().clickable { /* select */ } .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { Text(r.name, style = MaterialTheme.typography.bodyMedium, maxLines = 1, modifier = Modifier.weight(1f)) // text-left Text(r.tokens.format(), style = MaterialTheme.typography.labelMedium, fontFamily = FontFamily.Monospace, textAlign = TextAlign.End, modifier = Modifier.weight(0.5f)) // numeric → right + mono Text("$${r.cost}", style = MaterialTheme.typography.labelMedium, fontFamily = FontFamily.Monospace, textAlign = TextAlign.End, modifier = Modifier.weight(0.4f)) } if (i < rows.lastIndex) HorizontalDivider(thickness = 1.dp, color = colors.divider) } } }}// macOS: SwiftUI (canonical). AppKit-backed `Table` is the real multi-column grid:// `TableColumn`s, click-to-sort via `sortOrder` + `KeyPathComparator`, and a numeric// column using `.monospacedDigit()`. Selection binds to a Set of row ids.struct SessionsTable: View { let rows: [Session] @State private var selection = Set<Session.ID>() @State private var sortOrder = [KeyPathComparator(\Session.updated)]
var body: some View { Table(sortedRows, selection: $selection, sortOrder: $sortOrder) { TableColumn("Session", value: \.name) { row in Label(row.name, systemImage: "circle.fill") // leading status dot + name .foregroundStyle(NockerlTheme.onSurface) } TableColumn("Status", value: \.status.rank) { row in StatusPill(row.status) } TableColumn("Tokens", value: \.tokens) { row in Text(row.tokens, format: .number) .monospacedDigit().frame(maxWidth: .infinity, alignment: .trailing) // numeric → right } TableColumn("Cost", value: \.cost) { row in Text(row.cost, format: .currency(code: "USD")) .monospacedDigit().frame(maxWidth: .infinity, alignment: .trailing) } TableColumn("Updated", value: \.updated) { row in Text(row.updatedLabel).monospacedDigit() .frame(maxWidth: .infinity, alignment: .trailing) } } }
// `Table` re-sorts when a header is clicked; apply the comparator to the data. private var sortedRows: [Session] { rows.sorted(using: sortOrder) }}Parameters
Section titled “Parameters”NockerlTable takes a rows array, a typed columns definition, and the cross-cutting
concerns (sort, selection, density, sticky, footer, loading, empty) as props. Each NockerlTableColumn
declares its own key / header, a render(row) cell, and per-column align / mono /
sortable / width. v1 ships columns · sort · sticky · selection · empty · loading · density ·
footer; row virtualization and inline cell editing are a deliberate later slice.
NockerlTable is controlled: it renders rows verbatim (sort + paginate before passing them in)
and reports sort intent via onSortChange.
| Prop | Type | Default | Description |
|---|---|---|---|
columns * | NockerlTableColumn<Row>[] | The typed column definitions (order = column order). | |
rows * | Row[] | The rows to render, verbatim. The caller sorts + paginates before passing them in. | |
getRowId | (row: Row, index: number) => string | Stable id per row. It keys React and drives the selection set. Defaults to String(index). | |
sort | NockerlTableSort | null | Controlled sort state (the active column + direction), or null when unsorted. | |
onSortChange | (next: NockerlTableSort) => void | Fired with the next sort when a sortable header is clicked (toggles asc/desc on the same key). | |
selectable | boolean | false | Add a checkbox column + a header select-all (indeterminate on a partial page). |
selectedIds | Set<string> | Controlled set of selected row ids (selectable). | |
onSelectionChange | (next: Set<string>) => void | Fired with the next selected-id set when a box (row or select-all) toggles. | |
stickyHeader | boolean | false | Pin the header (and footer) so they stay flush while the body scrolls. |
loading | boolean | false | Renders skeleton rows that hold the column grid; sets aria-busy. |
loadingRows | number | 3 | How many skeleton rows to show while loading. |
empty | ReactNode | Shown in place of the body when rows is empty (and not loading). | |
footer | (rows: Row[]) => ReactNode | A summary row pinned under the body (column totals, for example). Receives the rendered rows. | |
density | NockerlTableDensity | 'comfortable' | Row density. It adjusts vertical padding only, never the fill. |
maxHeight | string | Max height of the scroll region (proves the sticky header). A CSS length; omit for no cap. | |
ariaLabel | string | Accessible name for the <table>. | |
caption | string | A visually-hidden <caption> describing the table for assistive tech. |
There is no NockerlDataTable. The grid is hand-assembled from a header Row, weighted
column cells, and HorizontalDivider hairlines (the ClusterSheet idiom). These are the
building blocks you compose.
| Parameter | Type | Default | Description |
|---|---|---|---|
Row + Modifier.weight(f) | @Composable | A table row is a Row of cells; each cell's Modifier.weight(f) is its column width. The header is the same Row with strong-band background. | |
textAlign | TextAlign | TextAlign.Start | TextAlign.End right-aligns a numeric cell (and its header) inside its weighted column. |
fontFamily | FontFamily | default | FontFamily.Monospace on numeric / id / metric cells so figures line up, following the in-app convention (LiveModelRow, NodeCard). |
sort (caller state) | SortState | Sorting is caller-owned state; the header cell is clickable and shows an arrow. The body is rendered from the pre-sorted list (rows.sortedBy { … }). | |
clickable { } | Modifier | Row selection / activation: the whole row is one target; a selected row gets a soft cyan wash + trailing check (never a left stripe, Law 6). | |
HorizontalDivider | @Composable | thickness = 1.dp | The hairline BETWEEN rows (color = colors.divider). Cyan (1.5dp) is reserved for the chrome boundary, never a row rule. |
LazyColumn | @Composable | The scrolling body. A sticky header uses stickyHeader { } (intended; see drift). |
macOS SwiftUI ships a real Table (AppKit-backed). You declare TableColumns with a
sort value: key path; Table drives click-to-sort through sortOrder + selection through a
bound Set. State comes from the bindings, so there are no Nockerl-specific parameters.
| Style | Type | Default | Description |
|---|---|---|---|
Table(_, selection:, sortOrder:) | View | The grid. selection binds to a Set<ID> (multi-select); sortOrder binds to the comparator array that drives header sorting. | |
TableColumn(_, value:) | TableColumn | One column. value: is a KeyPathComparator key path; supplying it makes the header sortable (the click + arrow are built in). | |
.monospacedDigit() | Modifier | Tabular figures on numeric cells so columns line up, following the in-app convention (HomeSection, RecordingHUD). | |
.frame(maxWidth: .infinity, alignment: .trailing) | Modifier | Right-aligns a numeric cell within its column. | |
sorted(using: sortOrder) | Method | Apply the bound comparator to the data so the body reflects the header sort. |