Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/bright-tables-sum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@tailor-platform/app-shell": minor
---

Add `Column.footer` for static labels and rendered summaries in a sticky row that follows column layout without adding synthetic data rows.
Footer renderers receive the current page's read-only rows through the exported `FooterRenderContext<TRow>`, mirroring the named `HeaderRenderContext` API.
During loading and page changes, the footer row stays mounted with skeletons instead of displaying stale totals or evaluating footer renderers.
The existing `DataTable.Footer` pagination and bulk-action container is unchanged.

```tsx
import type { Column } from "@tailor-platform/app-shell";

const columns: Column<{ quantity: number | null }>[] = [
{
id: "quantity",
label: "Quantity",
type: "number",
footer: ({ rows }) => rows.reduce((sum, row) => sum + (row.quantity ?? 0), 0),
},
];
```
17 changes: 9 additions & 8 deletions docs-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"hashes": {
"typeSurface": "f28bf34ae1466912",
"outline": "6138eac23571e177",
"snapshot": "d795320d53bd9ddb",
"snapshot": "049e72553f5b7a6f",
"outputMd": "db2c4388ca783a75",
"examples": "65d5fc73550e395a"
}
Expand Down Expand Up @@ -418,6 +418,7 @@
"Filter",
"FilterConfig",
"FilterOperator",
"FooterRenderContext",
"HeaderRenderContext",
"MetadataFieldOptions",
"NodeType",
Expand Down Expand Up @@ -453,11 +454,11 @@
"withURLCollectionState"
],
"hashes": {
"typeSurface": "3ca8fce7d2efca2d",
"outline": "0b7a57e7d2fc04a5",
"snapshot": null,
"outputMd": "1073852822f9e535",
"examples": "b3468077b9455521"
"typeSurface": "05cbf8a4bdb7a863",
"outline": "a1cfaa391d7f68d8",
"snapshot": "5c6c7491553784ee",
"outputMd": "8b1e693010c77535",
"examples": "686d2336dc14620a"
}
},
"date-picker": {
Expand Down Expand Up @@ -1363,7 +1364,7 @@
"hashes": {
"typeSurface": "bc60676f880b485b",
"outline": "9381eff2f7987a0e",
"snapshot": "18b98aa5db32996b",
"snapshot": "2399605a11c63aed",
"outputMd": "f85ec64ceb7f4acf",
"examples": "2b9d94065f2f77b7"
}
Expand Down Expand Up @@ -1751,7 +1752,7 @@
"packages/core/skills/app-shell-patterns/references/components/combobox.md": "dfb775c7c4307409",
"packages/core/skills/app-shell-patterns/references/components/command-palette.md": "7e5bc675a593791a",
"packages/core/skills/app-shell-patterns/references/components/csv-importer.md": "53d3c795ca21b9dc",
"packages/core/skills/app-shell-patterns/references/components/data-table.md": "1073852822f9e535",
"packages/core/skills/app-shell-patterns/references/components/data-table.md": "8b1e693010c77535",
"packages/core/skills/app-shell-patterns/references/components/date-picker.md": "bc69f14829004f9d",
"packages/core/skills/app-shell-patterns/references/components/default-header.md": "375e55f4b12fa2be",
"packages/core/skills/app-shell-patterns/references/components/default-sidebar.md": "3422388a0cc78571",
Expand Down
38 changes: 38 additions & 0 deletions docs-src/components/data-table.docs.examples.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,44 @@
import { useRef, useState } from "react";
import { Button, DataTable, createColumnHelper, useDataTable } from "@tailor-platform/app-shell";

export function ColumnFooters() {
type Line = { id: string; item: string; quantity: number | null };
const [rows, setRows] = useState<Line[]>([
{ id: "1", item: "Small carton", quantity: 2 },
{ id: "2", item: "Large carton", quantity: 3 },
]);
const { column } = createColumnHelper<Line>();
const table = useDataTable({
data: { rows },
columns: [
column({
id: "item",
label: "Item",
type: "text",
footer: "Total",
}),
column({
id: "quantity",
label: "Quantity",
type: "number",
edit: {
onValueCommitted: (row, quantity) =>
setRows((current) =>
current.map((line) => (line.id === row.id ? { ...line, quantity } : line)),
),
},
footer: ({ rows }) => rows.reduce((sum, row) => sum + (row.quantity ?? 0), 0),
Comment thread
interacsean marked this conversation as resolved.
Resolved
}),
],
});

return (
<DataTable.Root value={table}>
<DataTable.Table />
</DataTable.Root>
);
}

export function ImmediateSaving() {
type Line = { id: string; item: string; ordered: number; received: number | null };
const initialRows: Line[] = [
Expand Down
70 changes: 47 additions & 23 deletions docs-src/components/data-table.docs.outline.md

Large diffs are not rendered by default.

110 changes: 87 additions & 23 deletions docs/components/data-table.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,14 @@ export function InlineEditingDemo() {
};

const columns = [
column({ id: "sku", label: "SKU", type: "text", width: 120, pin: "left" }),
column({
id: "sku",
label: "SKU",
type: "text",
width: 120,
pin: "left",
footer: "Totals",
}),
column({ id: "product", label: "Product", type: "text" }),
column({
id: "supplierId",
Expand Down Expand Up @@ -237,13 +244,20 @@ export function InlineEditingDemo() {
update(row.id, { warehouseId: value }, `Warehouse → ${value ?? "none"}`),
},
}),
column({ id: "ordered", label: "Ordered", type: "number", width: 88 }),
column({
id: "ordered",
label: "Ordered",
type: "number",
width: 88,
footer: ({ rows }) => rows.reduce((sum, row) => sum + row.ordered, 0),
}),
column({
id: "received",
label: "Received",
type: "number",
width: 104,
typeOptions: { maxDecimals: 0 },
footer: ({ rows }) => rows.reduce((sum, row) => sum + (row.received ?? 0), 0),
edit: {
canEdit: (_row, { selected }) => selected,
onValueCommitted: (row, value) => {
Expand Down Expand Up @@ -326,6 +340,10 @@ export function InlineEditingDemo() {
type: "number",
width: 112,
typeOptions: { maxDecimals: 3 },
footer: ({ rows }) =>
rows
.reduce((sum, row) => sum + (row.weightKg ?? 0), 0)
.toLocaleString(undefined, { maximumFractionDigits: 3 }),
Comment thread
interacsean marked this conversation as resolved.
Resolved
edit: {
onValueCommitted: (row, value) => {
if (
Expand Down Expand Up @@ -478,7 +496,7 @@ export function InlineEditingDemo() {
{feedback}
</p>
)}
<DataTable.Root value={table}>
<DataTable.Root value={table} className="max-h-[320px]">
Comment thread
interacsean marked this conversation as resolved.
Resolved
<DataTable.Table />
<DataTable.Footer>
<div aria-live="polite" className="space-y-1 text-sm text-muted-foreground">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ const DataTableLabPage = () => {
</div>

<Section
title="Inline editing"
title="Inline editing + column footers"
description={
<>
A goods receipt using <code>onValueCommitted</code> — scroll right for every editor.
Expand All @@ -567,7 +567,12 @@ const DataTableLabPage = () => {
Lot no. rejects empty text. Warehouse, QC, and Note can be cleared. Dates use an
unconstrained calendar: Expected rejects days before September 2026, Best before
accepts October 2026–December 2027, and Received at adds a time field. There is no
built-in unsaved-changes prompt when leaving.
built-in unsaved-changes prompt when leaving. The sticky <strong>Totals</strong> row
uses <code>Column.footer</code> to sum Ordered, Received, and Weight across all six
records, not just selected rows. Edit Weight directly, or tick a row to edit Received,
and confirm to update the totals. Scroll inside the table to keep the header and
totals in view. These summaries are not data rows; the separate footer below still
shows change and saving feedback.
</>
}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`DataTable column footer > renders typed and render-only footers after the body, leaving missing and null content blank 1`] = `
<tfoot
class="astw:border-border astw:text-label-md astw:[&>tr]:last:border-b-0 astw:sticky astw:bottom-0 astw:z-10 astw:bg-card astw:border-t-0 astw:shadow-[inset_0_1px_0_0_var(--border)]"
data-slot="data-table-column-footer"
>
<tr
class="astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors astw:hover:bg-transparent"
data-slot="table-row"
>
<td
class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&:has([role=checkbox])]:pr-0 astw:text-left"
data-col-key="name"
data-slot="data-table-column-footer-cell"
>
Total
</td>
<td
class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&:has([role=checkbox])]:pr-0 astw:text-right"
data-col-key="quantity"
data-slot="data-table-column-footer-cell"
>
<strong>
5
</strong>
</td>
<td
class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&:has([role=checkbox])]:pr-0 astw:text-left"
data-col-key="note"
data-slot="data-table-column-footer-cell"
/>
<td
class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&:has([role=checkbox])]:pr-0 astw:text-left"
data-col-key="id"
data-slot="data-table-column-footer-cell"
/>
</tr>
</tfoot>
`;
Loading
Loading