@payfit/unity-components 2.35.4 → 2.35.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/esm/components/app-menu/parts/AppMenuHeader.js +1 -1
- package/package.json +15 -10
- package/skills/unity-data-table/SKILL.md +512 -0
- package/skills/unity-find-component/SKILL.md +377 -0
- package/skills/unity-layout-and-styling/SKILL.md +400 -0
- package/skills/unity-migrate-from-midnight/SKILL.md +190 -0
- package/skills/unity-migrate-from-midnight/references/midnight-component-map.md +180 -0
- package/skills/unity-navigation/SKILL.md +331 -0
- package/skills/unity-overlays/SKILL.md +352 -0
- package/skills/unity-setup-feature-plugin/SKILL.md +55 -0
- package/skills/unity-tanstack-form/SKILL.md +349 -0
- package/skills/unity-tanstack-form/references/bound-field-components.md +67 -0
- package/skills/unity-tanstack-form/references/schema-adapters.md +108 -0
package/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
# Unity components
|
|
2
2
|
|
|
3
3
|
The Unity component library exports the complete set of reusable components defined in PayFit's new Unity Design system.
|
|
4
|
+
|
|
5
|
+
## 🤖 Using an AI coding agent
|
|
6
|
+
|
|
7
|
+
If you use an AI coding agent (Claude Code, Cursor, Copilot, etc.), run:
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
npx @tanstack/intent@latest install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This wires the agent's config so it discovers and loads the per-package skills under `libs/shared/unity/<pkg>/skills/`. See `_artifacts/domain_map.yaml` for the full skill index.
|
|
@@ -24,7 +24,7 @@ const j = ({
|
|
|
24
24
|
showOnlyMonogram: a
|
|
25
25
|
}
|
|
26
26
|
) : /* @__PURE__ */ e(g, { label: i, env: r }) }) }),
|
|
27
|
-
/* @__PURE__ */ o("div", { className: "uy:flex uy:gap-150", children: [
|
|
27
|
+
/* @__PURE__ */ o("div", { className: "uy:flex uy:gap-150 uy:items-center", children: [
|
|
28
28
|
t,
|
|
29
29
|
/* @__PURE__ */ e("div", { className: "uy:block uy:md:hidden", children: /* @__PURE__ */ e(
|
|
30
30
|
h,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payfit/unity-components",
|
|
3
|
-
"version": "2.35.
|
|
3
|
+
"version": "2.35.6",
|
|
4
4
|
"module": "./dist/esm/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"dist",
|
|
37
|
-
"i18n"
|
|
37
|
+
"i18n",
|
|
38
|
+
"skills"
|
|
38
39
|
],
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"@ariakit/react": "0.4.26",
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
"@hookform/devtools": "4.4.0",
|
|
43
44
|
"@hookform/resolvers": "5.2.1",
|
|
44
45
|
"@internationalized/date": "3.12.1",
|
|
45
|
-
"@payfit/unity-illustrations": "2.35.
|
|
46
|
+
"@payfit/unity-illustrations": "2.35.6",
|
|
46
47
|
"@radix-ui/react-avatar": "1.1.11",
|
|
47
48
|
"@radix-ui/react-slot": "1.2.4",
|
|
48
49
|
"@react-aria/interactions": "3.28.0",
|
|
@@ -74,8 +75,8 @@
|
|
|
74
75
|
},
|
|
75
76
|
"peerDependencies": {
|
|
76
77
|
"@hookform/devtools": "^4",
|
|
77
|
-
"@payfit/unity-icons": "2.35.
|
|
78
|
-
"@payfit/unity-themes": "2.35.
|
|
78
|
+
"@payfit/unity-icons": "2.35.6",
|
|
79
|
+
"@payfit/unity-themes": "2.35.6",
|
|
79
80
|
"@storybook/react-vite": "^10.3.2",
|
|
80
81
|
"@tanstack/react-query": "^5",
|
|
81
82
|
"@tanstack/react-router": "^1.131",
|
|
@@ -89,9 +90,9 @@
|
|
|
89
90
|
"@figma/code-connect": "1.4.3",
|
|
90
91
|
"@hookform/devtools": "4.4.0",
|
|
91
92
|
"@internationalized/date": "3.12.1",
|
|
92
|
-
"@payfit/unity-icons": "2.35.
|
|
93
|
-
"@payfit/unity-illustrations": "2.35.
|
|
94
|
-
"@payfit/unity-themes": "2.35.
|
|
93
|
+
"@payfit/unity-icons": "2.35.6",
|
|
94
|
+
"@payfit/unity-illustrations": "2.35.6",
|
|
95
|
+
"@payfit/unity-themes": "2.35.6",
|
|
95
96
|
"@storybook/addon-a11y": "10.3.5",
|
|
96
97
|
"@storybook/addon-designs": "11.1.3",
|
|
97
98
|
"@storybook/addon-docs": "10.3.5",
|
|
@@ -100,6 +101,7 @@
|
|
|
100
101
|
"@storybook/addon-themes": "10.3.5",
|
|
101
102
|
"@storybook/addon-vitest": "10.3.5",
|
|
102
103
|
"@storybook/react-vite": "10.3.5",
|
|
104
|
+
"@tanstack/intent": "0.0.40",
|
|
103
105
|
"@tanstack/react-devtools": "0.10.1",
|
|
104
106
|
"@tanstack/react-form-devtools": "0.2.22",
|
|
105
107
|
"@tanstack/react-query": "5.99.0",
|
|
@@ -129,9 +131,9 @@
|
|
|
129
131
|
"vite": "7.1.12",
|
|
130
132
|
"vite-plugin-node-polyfills": "0.24.0",
|
|
131
133
|
"vitest": "4.1.0",
|
|
132
|
-
"@payfit/hr-apps-tsconfigs": "0.0.0-use.local",
|
|
133
134
|
"@payfit/code-pushup-tools": "0.0.0-use.local",
|
|
134
135
|
"@payfit/hr-app-eslint": "0.0.0-use.local",
|
|
136
|
+
"@payfit/hr-apps-tsconfigs": "0.0.0-use.local",
|
|
135
137
|
"@payfit/storybook-addon-console-errors": "0.0.0-use.local",
|
|
136
138
|
"@payfit/storybook-config": "0.0.0-use.local",
|
|
137
139
|
"@payfit/vite-configs": "0.0.0-use.local"
|
|
@@ -158,5 +160,8 @@
|
|
|
158
160
|
"zod": {
|
|
159
161
|
"optional": true
|
|
160
162
|
}
|
|
161
|
-
}
|
|
163
|
+
},
|
|
164
|
+
"keywords": [
|
|
165
|
+
"tanstack-intent"
|
|
166
|
+
]
|
|
162
167
|
}
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: unity-data-table
|
|
3
|
+
description: >
|
|
4
|
+
Render tabular data in @payfit/unity-components. Pick between the composite
|
|
5
|
+
DataTable (Tanstack Table integration with sorting, filtering, pagination,
|
|
6
|
+
virtualization, bulk actions, F6 focus trap) and the primitive Table
|
|
7
|
+
(TableHeader/TableBody/TableRow/TableCell/TableEmptyState). Filter and
|
|
8
|
+
FilterToolbar orchestrate the AppliedFilter[] contract; Filter.renderControl
|
|
9
|
+
accepts arbitrary controls (not select-only). Columns AND data MUST be
|
|
10
|
+
memoized. manualPagination requires re-slicing data.
|
|
11
|
+
type: core
|
|
12
|
+
library: '@payfit/unity-components'
|
|
13
|
+
library_version: '2.x'
|
|
14
|
+
sources:
|
|
15
|
+
- 'PayFit/hr-apps:libs/shared/unity/components/src/components/data-table/DataTable.tsx'
|
|
16
|
+
- 'PayFit/hr-apps:libs/shared/unity/components/src/components/data-table/parts/DataTableRoot.tsx'
|
|
17
|
+
- 'PayFit/hr-apps:libs/shared/unity/components/src/components/data-table/parts/DataTableBulkActions.tsx'
|
|
18
|
+
- 'PayFit/hr-apps:libs/shared/unity/components/src/components/table/Table.tsx'
|
|
19
|
+
- 'PayFit/hr-apps:libs/shared/unity/components/src/components/filter-toolbar/FilterToolbar.tsx'
|
|
20
|
+
- 'PayFit/hr-apps:libs/shared/unity/components/src/components/filter/Filter.tsx'
|
|
21
|
+
- 'PayFit/hr-apps:libs/shared/unity/components/src/docs/guides/data/Building Tables.mdx'
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
DataTable is the composite (Tanstack Table + Unity Table + pagination + empty
|
|
25
|
+
states + virtualization). Table is the primitive — use only when you have no
|
|
26
|
+
table state to manage.
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
Minimum working client-side DataTable. Columns and data MUST be memoized.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { useMemo, useState } from 'react'
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
DataTable,
|
|
37
|
+
DataTableRoot,
|
|
38
|
+
TableCell,
|
|
39
|
+
TableRow,
|
|
40
|
+
} from '@payfit/unity-components'
|
|
41
|
+
import {
|
|
42
|
+
createColumnHelper,
|
|
43
|
+
flexRender,
|
|
44
|
+
getCoreRowModel,
|
|
45
|
+
getPaginationRowModel,
|
|
46
|
+
useReactTable,
|
|
47
|
+
} from '@tanstack/react-table'
|
|
48
|
+
|
|
49
|
+
type Employee = {
|
|
50
|
+
id: string
|
|
51
|
+
name: string
|
|
52
|
+
position: string
|
|
53
|
+
status: 'active' | 'inactive'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const columnHelper = createColumnHelper<Employee>()
|
|
57
|
+
|
|
58
|
+
export function EmployeeTable({ employees }: { employees: Employee[] }) {
|
|
59
|
+
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 })
|
|
60
|
+
|
|
61
|
+
const columns = useMemo(
|
|
62
|
+
() => [
|
|
63
|
+
columnHelper.accessor('name', {
|
|
64
|
+
header: 'Name',
|
|
65
|
+
meta: { isRowHeader: true, headerClassName: 'uy:w-1/3' },
|
|
66
|
+
}),
|
|
67
|
+
columnHelper.accessor('position', { header: 'Position' }),
|
|
68
|
+
columnHelper.accessor('status', { header: 'Status' }),
|
|
69
|
+
],
|
|
70
|
+
[],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const data = useMemo(() => employees, [employees])
|
|
74
|
+
|
|
75
|
+
const table = useReactTable({
|
|
76
|
+
data,
|
|
77
|
+
columns,
|
|
78
|
+
getRowId: row => row.id,
|
|
79
|
+
state: { pagination },
|
|
80
|
+
onPaginationChange: setPagination,
|
|
81
|
+
getCoreRowModel: getCoreRowModel(),
|
|
82
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<DataTableRoot>
|
|
87
|
+
<DataTable table={table} layout="fixed">
|
|
88
|
+
{row => (
|
|
89
|
+
<TableRow key={row.id}>
|
|
90
|
+
{row.getVisibleCells().map(cell => (
|
|
91
|
+
<TableCell key={cell.id}>
|
|
92
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
93
|
+
</TableCell>
|
|
94
|
+
))}
|
|
95
|
+
</TableRow>
|
|
96
|
+
)}
|
|
97
|
+
</DataTable>
|
|
98
|
+
</DataTableRoot>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Core Patterns
|
|
104
|
+
|
|
105
|
+
### Define columns with the column helper and ColumnMeta
|
|
106
|
+
|
|
107
|
+
ColumnMeta drives accessibility (`isRowHeader`), keyboard navigation
|
|
108
|
+
(`isFocusable: false` for cells whose children are themselves focusable like
|
|
109
|
+
checkboxes), `helperText` (renders a tooltip next to the header), and
|
|
110
|
+
`headerClassName` (required when `layout="fixed"`).
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { Badge } from '@payfit/unity-components'
|
|
114
|
+
import { createColumnHelper } from '@tanstack/react-table'
|
|
115
|
+
|
|
116
|
+
const columnHelper = createColumnHelper<Employee>()
|
|
117
|
+
|
|
118
|
+
export const employeeColumns = [
|
|
119
|
+
columnHelper.accessor('name', {
|
|
120
|
+
id: 'employee',
|
|
121
|
+
header: 'Employee',
|
|
122
|
+
enableSorting: true,
|
|
123
|
+
meta: { isRowHeader: true, headerClassName: 'uy:w-[260px]' },
|
|
124
|
+
}),
|
|
125
|
+
columnHelper.accessor('status', {
|
|
126
|
+
header: 'Status',
|
|
127
|
+
enableColumnFilter: true,
|
|
128
|
+
filterFn: 'arrIncludesSome',
|
|
129
|
+
cell: info => {
|
|
130
|
+
const status = info.getValue()
|
|
131
|
+
return (
|
|
132
|
+
<Badge variant={status === 'active' ? 'success' : 'neutral'}>
|
|
133
|
+
{status}
|
|
134
|
+
</Badge>
|
|
135
|
+
)
|
|
136
|
+
},
|
|
137
|
+
meta: { helperText: 'Active employees can sign in.' },
|
|
138
|
+
}),
|
|
139
|
+
columnHelper.display({
|
|
140
|
+
id: 'actions',
|
|
141
|
+
header: '',
|
|
142
|
+
cell: ({ row }) => <RowMenu row={row.original} />,
|
|
143
|
+
meta: { isFocusable: false },
|
|
144
|
+
}),
|
|
145
|
+
]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Server-side pagination
|
|
149
|
+
|
|
150
|
+
`manualPagination: true` disables Tanstack's internal slicing. Pass only the
|
|
151
|
+
current page's slice as `data` and supply `pageCount`.
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
import { useMemo, useState } from 'react'
|
|
155
|
+
|
|
156
|
+
import { getCoreRowModel, useReactTable } from '@tanstack/react-table'
|
|
157
|
+
|
|
158
|
+
export function ServerTable({ totalCount, fetchPage }: Props) {
|
|
159
|
+
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 20 })
|
|
160
|
+
const { data: pageRows = [] } = useQuery({
|
|
161
|
+
queryKey: ['employees', pagination],
|
|
162
|
+
queryFn: () => fetchPage(pagination.pageIndex, pagination.pageSize),
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const columns = useMemo(() => employeeColumns, [])
|
|
166
|
+
const data = useMemo(() => pageRows, [pageRows])
|
|
167
|
+
|
|
168
|
+
const table = useReactTable({
|
|
169
|
+
data,
|
|
170
|
+
columns,
|
|
171
|
+
manualPagination: true,
|
|
172
|
+
pageCount: Math.ceil(totalCount / pagination.pageSize),
|
|
173
|
+
state: { pagination },
|
|
174
|
+
onPaginationChange: setPagination,
|
|
175
|
+
getCoreRowModel: getCoreRowModel(),
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<DataTableRoot>
|
|
180
|
+
<DataTable table={table}>
|
|
181
|
+
{row => (
|
|
182
|
+
<TableRow key={row.id}>
|
|
183
|
+
{row.getVisibleCells().map(cell => (
|
|
184
|
+
<TableCell key={cell.id}>
|
|
185
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
186
|
+
</TableCell>
|
|
187
|
+
))}
|
|
188
|
+
</TableRow>
|
|
189
|
+
)}
|
|
190
|
+
</DataTable>
|
|
191
|
+
</DataTableRoot>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Filtering with FilterToolbar
|
|
197
|
+
|
|
198
|
+
`FilterToolbar.onChange` emits `SerializableAppliedFilter[]`. Map that onto
|
|
199
|
+
`table.setColumnFilters` / `table.setGlobalFilter`. `renderControl` takes any
|
|
200
|
+
control — it is a render function on purpose so you can drop in date pickers,
|
|
201
|
+
multi-selects, text fields, etc.
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
import type { FilterDef } from '@payfit/unity-components'
|
|
205
|
+
|
|
206
|
+
import { FilterToolbar, Select, SelectItem } from '@payfit/unity-components'
|
|
207
|
+
import { getFilteredRowModel } from '@tanstack/react-table'
|
|
208
|
+
|
|
209
|
+
const filterDefs: FilterDef[] = [
|
|
210
|
+
{
|
|
211
|
+
id: 'status',
|
|
212
|
+
label: 'Status',
|
|
213
|
+
renderControl: (value, onChange) => (
|
|
214
|
+
<Select selectedKey={value as string} onSelectionChange={onChange}>
|
|
215
|
+
<SelectItem id="active">Active</SelectItem>
|
|
216
|
+
<SelectItem id="inactive">Inactive</SelectItem>
|
|
217
|
+
</Select>
|
|
218
|
+
),
|
|
219
|
+
renderLabel: value => String(value),
|
|
220
|
+
},
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
export function FilteredTable() {
|
|
224
|
+
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
|
225
|
+
|
|
226
|
+
const table = useReactTable({
|
|
227
|
+
data,
|
|
228
|
+
columns,
|
|
229
|
+
state: { columnFilters, pagination },
|
|
230
|
+
onColumnFiltersChange: setColumnFilters,
|
|
231
|
+
onPaginationChange: setPagination,
|
|
232
|
+
getCoreRowModel: getCoreRowModel(),
|
|
233
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
234
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<>
|
|
239
|
+
<FilterToolbar
|
|
240
|
+
filterDefs={filterDefs}
|
|
241
|
+
onChange={applied =>
|
|
242
|
+
setColumnFilters(applied.map(f => ({ id: f.id, value: f.value })))
|
|
243
|
+
}
|
|
244
|
+
/>
|
|
245
|
+
<DataTableRoot>
|
|
246
|
+
<DataTable table={table}>
|
|
247
|
+
{row => (
|
|
248
|
+
<TableRow key={row.id}>
|
|
249
|
+
{row.getVisibleCells().map(cell => (
|
|
250
|
+
<TableCell key={cell.id}>
|
|
251
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
252
|
+
</TableCell>
|
|
253
|
+
))}
|
|
254
|
+
</TableRow>
|
|
255
|
+
)}
|
|
256
|
+
</DataTable>
|
|
257
|
+
</DataTableRoot>
|
|
258
|
+
</>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Bulk actions with row selection
|
|
264
|
+
|
|
265
|
+
`DataTableBulkActions` lives next to `DataTable` inside `DataTableRoot`.
|
|
266
|
+
It auto-shows when rows are selected and provides the F6 focus trap. Use a
|
|
267
|
+
display column with `meta.isFocusable: false` for the checkbox.
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
import {
|
|
271
|
+
CheckboxStandalone,
|
|
272
|
+
DataTable,
|
|
273
|
+
DataTableBulkActions,
|
|
274
|
+
DataTableRoot,
|
|
275
|
+
} from '@payfit/unity-components'
|
|
276
|
+
|
|
277
|
+
const checkboxColumn = columnHelper.display({
|
|
278
|
+
id: 'select',
|
|
279
|
+
header: ({ table }) => (
|
|
280
|
+
<CheckboxStandalone
|
|
281
|
+
isSelected={table.getIsAllPageRowsSelected()}
|
|
282
|
+
isIndeterminate={table.getIsSomePageRowsSelected()}
|
|
283
|
+
onChange={value => table.toggleAllPageRowsSelected(value)}
|
|
284
|
+
slot="selection"
|
|
285
|
+
>
|
|
286
|
+
Select all
|
|
287
|
+
</CheckboxStandalone>
|
|
288
|
+
),
|
|
289
|
+
cell: ({ row }) => (
|
|
290
|
+
<CheckboxStandalone
|
|
291
|
+
isSelected={row.getIsSelected()}
|
|
292
|
+
onChange={value => row.toggleSelected(value)}
|
|
293
|
+
slot="selection"
|
|
294
|
+
>
|
|
295
|
+
Select row
|
|
296
|
+
</CheckboxStandalone>
|
|
297
|
+
),
|
|
298
|
+
enableSorting: false,
|
|
299
|
+
meta: { isFocusable: false },
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
export function BulkTable() {
|
|
303
|
+
const [rowSelection, setRowSelection] = useState({})
|
|
304
|
+
const columns = useMemo(() => [checkboxColumn, ...employeeColumns], [])
|
|
305
|
+
const table = useReactTable({
|
|
306
|
+
data,
|
|
307
|
+
columns,
|
|
308
|
+
state: { rowSelection, pagination },
|
|
309
|
+
onRowSelectionChange: setRowSelection,
|
|
310
|
+
onPaginationChange: setPagination,
|
|
311
|
+
enableRowSelection: true,
|
|
312
|
+
getCoreRowModel: getCoreRowModel(),
|
|
313
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<DataTableRoot>
|
|
318
|
+
<DataTable table={table}>
|
|
319
|
+
{row => (
|
|
320
|
+
<TableRow key={row.id} isSelected={row.getIsSelected()}>
|
|
321
|
+
{row.getVisibleCells().map(cell => (
|
|
322
|
+
<TableCell key={cell.id}>
|
|
323
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
324
|
+
</TableCell>
|
|
325
|
+
))}
|
|
326
|
+
</TableRow>
|
|
327
|
+
)}
|
|
328
|
+
</DataTable>
|
|
329
|
+
<DataTableBulkActions
|
|
330
|
+
table={table}
|
|
331
|
+
actions={[
|
|
332
|
+
{ id: 'archive', label: 'Archive', onAction: rows => archive(rows) },
|
|
333
|
+
{ id: 'delete', label: 'Delete', onAction: rows => remove(rows) },
|
|
334
|
+
]}
|
|
335
|
+
/>
|
|
336
|
+
</DataTableRoot>
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Common Mistakes
|
|
342
|
+
|
|
343
|
+
### CRITICAL Define columns inline (not memoized)
|
|
344
|
+
|
|
345
|
+
Wrong:
|
|
346
|
+
|
|
347
|
+
```tsx
|
|
348
|
+
function MyTable() {
|
|
349
|
+
const columns = [
|
|
350
|
+
{ accessorKey: 'name', header: 'Name' },
|
|
351
|
+
{ accessorKey: 'status', header: 'Status' },
|
|
352
|
+
]
|
|
353
|
+
const table = useReactTable({ data, columns, ... })
|
|
354
|
+
return <DataTable table={table}>{row => ...}</DataTable>
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Correct:
|
|
359
|
+
|
|
360
|
+
```tsx
|
|
361
|
+
function MyTable() {
|
|
362
|
+
const columns = useMemo(() => [
|
|
363
|
+
{ accessorKey: 'name', header: 'Name' },
|
|
364
|
+
{ accessorKey: 'status', header: 'Status' },
|
|
365
|
+
], [])
|
|
366
|
+
const table = useReactTable({ data, columns, ... })
|
|
367
|
+
return <DataTable table={table}>{row => ...}</DataTable>
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
A new columns array each render makes Tanstack Table re-run the whole pipeline; rows re-render even when data is unchanged.
|
|
372
|
+
|
|
373
|
+
Source: DataTable.stories.tsx:656-657; mocks/employee-columns.tsx:14-61
|
|
374
|
+
|
|
375
|
+
### HIGH Return string from cell function instead of JSX
|
|
376
|
+
|
|
377
|
+
Wrong:
|
|
378
|
+
|
|
379
|
+
```tsx
|
|
380
|
+
{ accessorKey: 'status', cell: info => info.getValue() }
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Correct:
|
|
384
|
+
|
|
385
|
+
```tsx
|
|
386
|
+
{
|
|
387
|
+
accessorKey: 'status',
|
|
388
|
+
cell: info => <Badge color={statusColor(info.getValue())}>{info.getValue()}</Badge>
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
flexRender expects ReactNode. A raw string renders unstyled and may overflow. Cell layout is the maintainer-flagged hotspot for table questions.
|
|
393
|
+
|
|
394
|
+
Source: DataTable.stories.tsx:123-159 (cell function patterns); maintainer interview
|
|
395
|
+
|
|
396
|
+
### HIGH Use the primitive Table when DataTable + Tanstack would do
|
|
397
|
+
|
|
398
|
+
Wrong:
|
|
399
|
+
|
|
400
|
+
```tsx
|
|
401
|
+
<Table layout="fixed">
|
|
402
|
+
<TableHeader>…</TableHeader>
|
|
403
|
+
<TableBody>
|
|
404
|
+
{data.map(row => (
|
|
405
|
+
<TableRow>…</TableRow>
|
|
406
|
+
))}
|
|
407
|
+
</TableBody>
|
|
408
|
+
</Table>
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Correct:
|
|
412
|
+
|
|
413
|
+
```tsx
|
|
414
|
+
const table = useReactTable({ data, columns, getCoreRowModel(),
|
|
415
|
+
getPaginationRowModel(), state: { pagination }, onPaginationChange: setPagination })
|
|
416
|
+
<DataTableRoot>
|
|
417
|
+
<DataTable table={table}>{row => /* ... */}</DataTable>
|
|
418
|
+
</DataTableRoot>
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Primitive Table has no state; agent hand-rolls pagination/sorting/selection that DataTable provides out of the box.
|
|
422
|
+
|
|
423
|
+
Source: DataTable.tsx (composite) vs Table.tsx (primitives)
|
|
424
|
+
|
|
425
|
+
### HIGH Manage filter state separately from FilterToolbar
|
|
426
|
+
|
|
427
|
+
Wrong:
|
|
428
|
+
|
|
429
|
+
```tsx
|
|
430
|
+
const [filters, setFilters] = useState([])
|
|
431
|
+
<FilterToolbar filterDefs={…} onChange={setFilters} />
|
|
432
|
+
<DataTable table={table} />
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Correct:
|
|
436
|
+
|
|
437
|
+
```tsx
|
|
438
|
+
const [columnFilters, setColumnFilters] = useState([])
|
|
439
|
+
const [globalFilter, setGlobalFilter] = useState('')
|
|
440
|
+
const table = useReactTable({ /* … */ state: { columnFilters, globalFilter },
|
|
441
|
+
onGlobalFilterChange: setGlobalFilter, onColumnFiltersChange: setColumnFilters,
|
|
442
|
+
getFilteredRowModel() })
|
|
443
|
+
<FilterToolbar filterDefs={…} onChange={mapToTableState(setColumnFilters, setGlobalFilter)} />
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
FilterToolbar.onChange emits AppliedFilter[]. Agent stores them locally without mapping to table.setGlobalFilter / setColumnFilters, so rows do not actually filter.
|
|
447
|
+
|
|
448
|
+
Source: FilterToolbar.tsx; DataTable.stories.tsx:1141-1209
|
|
449
|
+
|
|
450
|
+
### CRITICAL Server-side pagination without slicing data
|
|
451
|
+
|
|
452
|
+
Wrong:
|
|
453
|
+
|
|
454
|
+
```tsx
|
|
455
|
+
useReactTable({
|
|
456
|
+
data: allEmployees,
|
|
457
|
+
manualPagination: true,
|
|
458
|
+
pageCount: Math.ceil(allEmployees.length / 10),
|
|
459
|
+
})
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Correct:
|
|
463
|
+
|
|
464
|
+
```tsx
|
|
465
|
+
const currentData = useMemo(() => {
|
|
466
|
+
const start = pagination.pageIndex * pagination.pageSize
|
|
467
|
+
return allEmployees.slice(start, start + pagination.pageSize)
|
|
468
|
+
}, [pagination])
|
|
469
|
+
useReactTable({
|
|
470
|
+
data: currentData,
|
|
471
|
+
manualPagination: true,
|
|
472
|
+
pageCount: Math.ceil(allEmployees.length / pagination.pageSize),
|
|
473
|
+
state: { pagination },
|
|
474
|
+
onPaginationChange: setPagination,
|
|
475
|
+
})
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
manualPagination: true tells Tanstack not to slice. Agents pass the full dataset and expect TanStack to paginate anyway.
|
|
479
|
+
|
|
480
|
+
Source: DataTable.stories.tsx:146-178 (manual pagination)
|
|
481
|
+
|
|
482
|
+
### MEDIUM Render large datasets without enableVirtualization
|
|
483
|
+
|
|
484
|
+
Wrong:
|
|
485
|
+
|
|
486
|
+
```tsx
|
|
487
|
+
<DataTable table={table}>{row => …}</DataTable>
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Correct:
|
|
491
|
+
|
|
492
|
+
```tsx
|
|
493
|
+
<DataTable
|
|
494
|
+
table={table}
|
|
495
|
+
enableVirtualization
|
|
496
|
+
estimatedRowHeight={40}
|
|
497
|
+
overscan={10}
|
|
498
|
+
>
|
|
499
|
+
{row => …}
|
|
500
|
+
</DataTable>
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Without virtualization, 500+ rows mount in the DOM. Keyboard nav context clones cells; reconciliation is O(n) per render.
|
|
504
|
+
|
|
505
|
+
Source: DataTable.tsx:278-280; Table.tsx import of @tanstack/react-virtual
|
|
506
|
+
|
|
507
|
+
## See also
|
|
508
|
+
|
|
509
|
+
- `unity-layout-and-styling` — `uy:` cell content classes, `headerClassName`
|
|
510
|
+
width helpers, `layout="fixed"` rules.
|
|
511
|
+
- `unity-find-component` — picking between primitive Table and composite
|
|
512
|
+
DataTable when you are not sure which fits.
|