@czi-sds/mcp 0.16.0 → 0.18.0

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.
Files changed (167) hide show
  1. package/README.md +2 -1
  2. package/data/component-docs/Accordion.md +291 -0
  3. package/data/component-docs/Autocomplete.md +525 -0
  4. package/data/component-docs/Banner.md +231 -0
  5. package/data/component-docs/Button.md +224 -0
  6. package/data/component-docs/ButtonDropdown.md +256 -0
  7. package/data/component-docs/ButtonGroup.md +320 -0
  8. package/data/component-docs/ButtonToggle.md +397 -0
  9. package/data/component-docs/Callout.md +268 -0
  10. package/data/component-docs/CellBasic.md +122 -0
  11. package/data/component-docs/CellComponent.md +63 -0
  12. package/data/component-docs/CellHeader.md +128 -0
  13. package/data/component-docs/ComplexFilter.md +294 -0
  14. package/data/component-docs/ContentCard.md +406 -0
  15. package/data/component-docs/Dialog.md +651 -0
  16. package/data/component-docs/Dropdown.md +155 -0
  17. package/data/component-docs/DropdownMenu.md +949 -0
  18. package/data/component-docs/HeatmapChart.md +782 -0
  19. package/data/component-docs/Hero.md +363 -0
  20. package/data/component-docs/Icon.md +283 -0
  21. package/data/component-docs/InputCheckbox.md +205 -0
  22. package/data/component-docs/InputDropdown.md +482 -0
  23. package/data/component-docs/InputRadio.md +173 -0
  24. package/data/component-docs/InputSearch.md +81 -0
  25. package/data/component-docs/InputSlider.md +189 -0
  26. package/data/component-docs/InputText.md +110 -0
  27. package/data/component-docs/InputToggle.md +141 -0
  28. package/data/component-docs/IntentMessage.md +293 -0
  29. package/data/component-docs/Legend.md +442 -0
  30. package/data/component-docs/Link.md +23 -0
  31. package/data/component-docs/List.md +300 -0
  32. package/data/component-docs/LoadingIndicator.md +198 -0
  33. package/data/component-docs/Menu.md +298 -0
  34. package/data/component-docs/MenuItem.md +100 -0
  35. package/data/component-docs/NavigationFooter.md +165 -0
  36. package/data/component-docs/NavigationHeader.md +500 -0
  37. package/data/component-docs/NavigationJumpTo.md +414 -0
  38. package/data/component-docs/Notification.md +288 -0
  39. package/data/component-docs/Pagination.md +67 -0
  40. package/data/component-docs/Panel.md +636 -0
  41. package/data/component-docs/Popover.md +448 -0
  42. package/data/component-docs/PreComposedTable.md +2947 -0
  43. package/data/component-docs/ProteinStructureViewer.md +1044 -0
  44. package/data/component-docs/SegmentedControl.md +396 -0
  45. package/data/component-docs/StackedBarChart.md +475 -0
  46. package/data/component-docs/Table.md +2000 -0
  47. package/data/component-docs/TableHeader.md +53 -0
  48. package/data/component-docs/TableRow.md +74 -0
  49. package/data/component-docs/Tabs.md +455 -0
  50. package/data/component-docs/Tag.md +647 -0
  51. package/data/component-docs/TagFilter.md +131 -0
  52. package/data/component-docs/Tooltip.md +561 -0
  53. package/data/component-docs/TooltipCondensed.md +203 -0
  54. package/data/component-docs/TooltipTable.md +145 -0
  55. package/data/component-docs/index.json +267 -0
  56. package/data/component-list.json +2 -5
  57. package/data/component-props/Accordion.json +31 -11
  58. package/data/component-props/Alert.json +3 -3
  59. package/data/component-props/Autocomplete.json +150 -15
  60. package/data/component-props/Banner.json +31 -17
  61. package/data/component-props/Button.json +45 -20
  62. package/data/component-props/ButtonDropdown.json +36 -17
  63. package/data/component-props/ButtonDropdownLegacy.json +11 -11
  64. package/data/component-props/ButtonGroup.json +32 -11
  65. package/data/component-props/ButtonIcon.json +10 -10
  66. package/data/component-props/ButtonLegacy.json +19 -19
  67. package/data/component-props/ButtonToggle.json +32 -19
  68. package/data/component-props/ButtonToggleLegacy.json +11 -11
  69. package/data/component-props/Callout.json +58 -17
  70. package/data/component-props/CellBasic.json +94 -41
  71. package/data/component-props/CellComponent.json +23 -7
  72. package/data/component-props/CellHeader.json +62 -19
  73. package/data/component-props/ComplexFilter.json +52 -21
  74. package/data/component-props/ContentCard.json +70 -47
  75. package/data/component-props/Dialog.json +47 -14
  76. package/data/component-props/Dropdown.json +68 -31
  77. package/data/component-props/DropdownMenu.json +128 -23
  78. package/data/component-props/HeatmapChart.json +88 -12
  79. package/data/component-props/Hero.json +80 -39
  80. package/data/component-props/Icon.json +20 -9
  81. package/data/component-props/InputCheckbox.json +36 -14
  82. package/data/component-props/InputDropdown.json +75 -27
  83. package/data/component-props/InputRadio.json +31 -13
  84. package/data/component-props/InputSearch.json +41 -18
  85. package/data/component-props/InputSlider.json +46 -19
  86. package/data/component-props/InputText.json +26 -19
  87. package/data/component-props/InputToggle.json +26 -9
  88. package/data/component-props/IntentMessage.json +38 -9
  89. package/data/component-props/Link.json +13 -7
  90. package/data/component-props/LoadingIndicator.json +11 -3
  91. package/data/component-props/MenuItem.json +38 -19
  92. package/data/component-props/MenuSelect.json +8 -8
  93. package/data/component-props/NavigationFooter.json +49 -19
  94. package/data/component-props/NavigationHeader.json +150 -37
  95. package/data/component-props/NavigationJumpTo.json +33 -7
  96. package/data/component-props/Notification.json +39 -21
  97. package/data/component-props/Pagination.json +39 -11
  98. package/data/component-props/Panel.json +62 -9
  99. package/data/component-props/Popover.json +80 -7
  100. package/data/component-props/PreComposedTable.json +99 -27
  101. package/data/component-props/ProteinStructureViewer.json +117 -0
  102. package/data/component-props/SegmentedControl.json +42 -3
  103. package/data/component-props/StackedBarChart.json +116 -42
  104. package/data/component-props/TableRow.json +38 -19
  105. package/data/component-props/Tabs.json +66 -9
  106. package/data/component-props/Tag.json +34 -17
  107. package/data/component-props/TagFilter.json +10 -3
  108. package/data/component-props/Tooltip.json +49 -18
  109. package/data/component-props/TooltipCondensed.json +18 -9
  110. package/data/component-props/TooltipTable.json +17 -7
  111. package/data/resources/{zeroheight-rules.md → documentation-rules.md} +43 -30
  112. package/data/resources/figma-rules.md +2 -2
  113. package/data/tailwind.json +20 -14
  114. package/dist/stdio.js +112 -208
  115. package/package.json +9 -5
  116. package/data/component-props/ButtonDropdownV2.json +0 -34
  117. package/data/component-props/ButtonNew.json +0 -47
  118. package/data/component-props/ButtonToggleV2.json +0 -44
  119. package/data/component-props/ButtonV2.json +0 -43
  120. package/data/zeroheight-cache.json +0 -10259
  121. package/data/zeroheight-exports/Accordion.md +0 -774
  122. package/data/zeroheight-exports/Banner.md +0 -691
  123. package/data/zeroheight-exports/Borders.md +0 -117
  124. package/data/zeroheight-exports/Buttons.md +0 -1854
  125. package/data/zeroheight-exports/Callout.md +0 -931
  126. package/data/zeroheight-exports/Colors.md +0 -1755
  127. package/data/zeroheight-exports/Content-Card.md +0 -309
  128. package/data/zeroheight-exports/Contributing-to-SDS.md +0 -98
  129. package/data/zeroheight-exports/Control-Inputs.md +0 -2818
  130. package/data/zeroheight-exports/Corners.md +0 -104
  131. package/data/zeroheight-exports/Cover-page.md +0 -3
  132. package/data/zeroheight-exports/Dialog.md +0 -1367
  133. package/data/zeroheight-exports/Downloads.md +0 -236
  134. package/data/zeroheight-exports/Drop-Shadows.md +0 -79
  135. package/data/zeroheight-exports/Dropdown-Input.md +0 -1776
  136. package/data/zeroheight-exports/Dropdown-Menus.md +0 -913
  137. package/data/zeroheight-exports/Element-Status-Tracker.md +0 -267
  138. package/data/zeroheight-exports/Field-Inputs.md +0 -1405
  139. package/data/zeroheight-exports/Filters.md +0 -1111
  140. package/data/zeroheight-exports/Getting-Started.md +0 -1157
  141. package/data/zeroheight-exports/Hero.md +0 -414
  142. package/data/zeroheight-exports/Icons.md +0 -2067
  143. package/data/zeroheight-exports/Intent.md +0 -86
  144. package/data/zeroheight-exports/Introduction-to-Bases.md +0 -35
  145. package/data/zeroheight-exports/Introduction-to-Cells.md +0 -35
  146. package/data/zeroheight-exports/Introduction-to-DNA.md +0 -35
  147. package/data/zeroheight-exports/Introduction-to-Genes.md +0 -35
  148. package/data/zeroheight-exports/Introduction.md +0 -115
  149. package/data/zeroheight-exports/Link.md +0 -432
  150. package/data/zeroheight-exports/Lists.md +0 -986
  151. package/data/zeroheight-exports/Loading-Indicator.md +0 -41
  152. package/data/zeroheight-exports/Navigation.md +0 -1190
  153. package/data/zeroheight-exports/Notification.md +0 -1038
  154. package/data/zeroheight-exports/Panel.md +0 -1030
  155. package/data/zeroheight-exports/README.md +0 -122
  156. package/data/zeroheight-exports/Responsive-Design.md +0 -330
  157. package/data/zeroheight-exports/Search-Input.md +0 -91
  158. package/data/zeroheight-exports/Search.md +0 -822
  159. package/data/zeroheight-exports/Segmented-Control.md +0 -609
  160. package/data/zeroheight-exports/Spacing.md +0 -297
  161. package/data/zeroheight-exports/Table.md +0 -5345
  162. package/data/zeroheight-exports/Tabs.md +0 -1324
  163. package/data/zeroheight-exports/Tags.md +0 -1545
  164. package/data/zeroheight-exports/Text-Link.md +0 -268
  165. package/data/zeroheight-exports/Theming.md +0 -362
  166. package/data/zeroheight-exports/Tooltips.md +0 -2015
  167. package/data/zeroheight-exports/Typography.md +0 -800
@@ -0,0 +1,2947 @@
1
+ # PreComposedTable
2
+
3
+ This component is a fully-featured, composable table built on top of TanStack Table with built-in support for sorting, pagination, row selection, filtering, column pinning, and responsive sizing
4
+
5
+ ## Source Code
6
+
7
+ The component's source code in the SDS codebase can be found [here](https://github.com/chanzuckerberg/sci-components/blob/main/packages/components/src/core/PreComposedTable/index.tsx).
8
+
9
+ ## Import
10
+
11
+ **React TypeScript**
12
+
13
+ ```tsx
14
+ import { PreComposedTable } from "@czi-sds/components";
15
+ ```
16
+
17
+ ## Code examples
18
+
19
+ > **Note:** PreComposedTable runs on [TanStack Table](https://tanstack.com/table/latest), which SDS takes as a peer dependency. Install `@tanstack/react-table` alongside it. Column definitions are TanStack's own, so anything its docs describe applies here too.
20
+
21
+ ### Default
22
+
23
+ Data, columns, and the features you want switched on (here sorting, row selection and pagination), with Name pinned to the left edge and Actions to the right. A column left as a plain accessor is drawn as a CellHeader and a CellBasic, while the cells that render something else forward the element and width they are handed. Note that `data`, `columns` and `paginationConfig` sit outside the component so their identity is stable: a new `paginationConfig` object sends the table back to its first page.
24
+
25
+ **Example: DefaultPreComposedTable**
26
+
27
+ ```tsx
28
+ // The Default story: sorting, row selection and pagination switched on over the
29
+ // full column set, with Name pinned to the left edge and Actions to the right.
30
+ //
31
+ // A column left as a plain accessor is drawn as a CellHeader and a CellBasic.
32
+ // A renderer of your own is handed the element and the width to draw with as
33
+ // `as` and `style`, and forwards both. Without `as` it renders a second <td>
34
+ // inside the one the table already drew. A header renderer is handed
35
+ // `data-pinned-edge` too, which is what tells a pinned header to draw its
36
+ // gradient.
37
+ //
38
+ // data, columns and paginationConfig sit outside the component so their
39
+ // identity is stable: a new paginationConfig object sends the table back to its
40
+ // first page.
41
+
42
+ import {
43
+ Button,
44
+ CellBasic,
45
+ CellComponent,
46
+ CellHeader,
47
+ Icon,
48
+ PreComposedTable,
49
+ Tag,
50
+ } from "@czi-sds/components";
51
+ import type {
52
+ CellContext,
53
+ ColumnDef,
54
+ HeaderContext,
55
+ } from "@tanstack/react-table";
56
+ import * as React from "react";
57
+
58
+ type Employee = {
59
+ department: string;
60
+ email: string;
61
+ lastActive: string;
62
+ location: string;
63
+ name: string;
64
+ projects: number;
65
+ role: string;
66
+ salary: number;
67
+ startDate: string;
68
+ status: "Active" | "Inactive";
69
+ };
70
+
71
+ const DATA: Employee[] = [
72
+ {
73
+ department: "Engineering",
74
+ email: "john.doe@example.com",
75
+ lastActive: "2024-01-15",
76
+ location: "San Francisco",
77
+ name: "John Doe",
78
+ projects: 5,
79
+ role: "Software Engineer",
80
+ salary: 85000,
81
+ startDate: "2022-03-15",
82
+ status: "Active",
83
+ },
84
+ {
85
+ department: "Product",
86
+ email: "jane.smith@example.com",
87
+ lastActive: "2024-01-14",
88
+ location: "New York",
89
+ name: "Jane Smith",
90
+ projects: 3,
91
+ role: "Product Manager",
92
+ salary: 95000,
93
+ startDate: "2021-08-20",
94
+ status: "Active",
95
+ },
96
+ {
97
+ department: "Design",
98
+ email: "bob.johnson@example.com",
99
+ lastActive: "2024-01-10",
100
+ location: "Austin",
101
+ name: "Bob Johnson",
102
+ projects: 2,
103
+ role: "Designer",
104
+ salary: 75000,
105
+ startDate: "2023-01-10",
106
+ status: "Inactive",
107
+ },
108
+ {
109
+ department: "Data",
110
+ email: "alice.brown@example.com",
111
+ lastActive: "2024-01-16",
112
+ location: "Seattle",
113
+ name: "Alice Brown",
114
+ projects: 7,
115
+ role: "Data Scientist",
116
+ salary: 90000,
117
+ startDate: "2022-11-05",
118
+ status: "Active",
119
+ },
120
+ {
121
+ department: "Engineering",
122
+ email: "charlie.wilson@example.com",
123
+ lastActive: "2024-01-15",
124
+ location: "Remote",
125
+ name: "Charlie Wilson",
126
+ projects: 4,
127
+ role: "DevOps Engineer",
128
+ salary: 88000,
129
+ startDate: "2023-06-12",
130
+ status: "Active",
131
+ },
132
+ ];
133
+
134
+ /** The cell context, plus how PreComposedTable wants the cell drawn. */
135
+ type CellProps = CellContext<Employee, unknown> & {
136
+ as?: React.ElementType;
137
+ style?: React.CSSProperties;
138
+ };
139
+
140
+ /** The header context, plus which edge a pinned header sits against. */
141
+ type HeaderProps = HeaderContext<Employee, unknown> & {
142
+ "data-pinned-edge"?: "left" | "right";
143
+ style?: React.CSSProperties;
144
+ };
145
+
146
+ function textCell({ as, getValue, style }: CellProps) {
147
+ return (
148
+ <CellBasic
149
+ as={as}
150
+ primaryText={String(getValue())}
151
+ shouldShowTooltipOnHover={false}
152
+ style={style}
153
+ />
154
+ );
155
+ }
156
+
157
+ function numberCell({ as, column, getValue, style }: CellProps) {
158
+ return (
159
+ <CellBasic
160
+ as={as}
161
+ // Room for the sort chevron, which would otherwise sit on the number.
162
+ extraRightPadding={column.getCanSort() ? 20 : 0}
163
+ horizontalAlign="right"
164
+ primaryText={String(getValue())}
165
+ shouldShowTooltipOnHover={false}
166
+ style={style}
167
+ tabularNums
168
+ />
169
+ );
170
+ }
171
+
172
+ const COLUMNS: ColumnDef<Employee>[] = [
173
+ {
174
+ accessorKey: "name",
175
+ cell: ({ as, getValue, row, style }: CellProps) => (
176
+ <CellBasic
177
+ as={as}
178
+ link={`mailto:${row.original.email}`}
179
+ linkComponent="a"
180
+ linkProps={{ target: "_blank" }}
181
+ primaryText={getValue() as string}
182
+ secondaryText={row.original.role}
183
+ shouldShowTooltipOnHover={false}
184
+ shouldShowUnderlineOnHover
185
+ style={style}
186
+ />
187
+ ),
188
+ enableSorting: false,
189
+ header: ({ "data-pinned-edge": pinnedEdge, style }: HeaderProps) => (
190
+ <CellHeader
191
+ data-pinned-edge={pinnedEdge}
192
+ hideSortIcon
193
+ hover
194
+ shouldShowTooltipOnHover
195
+ style={style}
196
+ tooltipProps={{ placement: "top", textAlign: "left", width: "wide" }}
197
+ tooltipSubtitle="Each name links to the person's email address."
198
+ tooltipText="Name"
199
+ >
200
+ Name
201
+ </CellHeader>
202
+ ),
203
+ meta: { isGrow: true, pinning: "left" },
204
+ minSize: 150,
205
+ },
206
+ {
207
+ accessorKey: "email",
208
+ cell: ({ as, getValue, style }: CellProps) => (
209
+ <CellBasic
210
+ as={as}
211
+ link={`mailto:${getValue() as string}`}
212
+ linkProps={{ target: "_blank" }}
213
+ primaryText={getValue() as string}
214
+ primaryTextWrapLineCount={1}
215
+ shouldShowTooltipOnHover={false}
216
+ shouldShowUnderlineOnHover
217
+ style={style}
218
+ />
219
+ ),
220
+ header: "Email",
221
+ meta: { isGrow: true },
222
+ minSize: 200,
223
+ },
224
+ {
225
+ accessorKey: "role",
226
+ cell: textCell,
227
+ header: "Role",
228
+ meta: { verticalAlign: "middle" },
229
+ minSize: 150,
230
+ },
231
+ {
232
+ accessorKey: "department",
233
+ cell: textCell,
234
+ header: "Department",
235
+ minSize: 120,
236
+ },
237
+ {
238
+ accessorKey: "status",
239
+ cell: ({ as, getValue, style }: CellProps) => (
240
+ <CellComponent as={as} style={style}>
241
+ <Tag
242
+ color={getValue() === "Active" ? "positive" : "negative"}
243
+ hover={false}
244
+ label={getValue() as string}
245
+ sdsStyle="rounded"
246
+ sdsType="secondary"
247
+ />
248
+ </CellComponent>
249
+ ),
250
+ header: "Status",
251
+ minSize: 100,
252
+ },
253
+ {
254
+ accessorKey: "lastActive",
255
+ cell: numberCell,
256
+ header: "Last Active",
257
+ meta: { headerCellProps: { horizontalAlign: "right" } },
258
+ minSize: 120,
259
+ },
260
+ {
261
+ accessorKey: "projects",
262
+ cell: numberCell,
263
+ header: "Projects",
264
+ meta: { headerCellProps: { horizontalAlign: "right" } },
265
+ minSize: 100,
266
+ },
267
+ {
268
+ accessorKey: "salary",
269
+ cell: ({ as, column, getValue, style }: CellProps) => (
270
+ <CellBasic
271
+ as={as}
272
+ extraRightPadding={column.getCanSort() ? 20 : 0}
273
+ horizontalAlign="right"
274
+ primaryText={`${(getValue() as number).toLocaleString()}`}
275
+ shouldShowTooltipOnHover={false}
276
+ style={style}
277
+ tabularNums
278
+ />
279
+ ),
280
+ header: "Salary",
281
+ meta: { headerCellProps: { horizontalAlign: "right" } },
282
+ minSize: 120,
283
+ },
284
+ {
285
+ accessorKey: "location",
286
+ cell: textCell,
287
+ header: "Location",
288
+ minSize: 150,
289
+ },
290
+ {
291
+ accessorKey: "startDate",
292
+ cell: numberCell,
293
+ header: "Start Date",
294
+ meta: { headerCellProps: { horizontalAlign: "right" } },
295
+ minSize: 120,
296
+ },
297
+ {
298
+ cell: ({ as, row, style }: CellProps) => (
299
+ <CellComponent as={as} horizontalAlign="right" style={style}>
300
+ <Button
301
+ aria-label={`Edit ${row.original.name}`}
302
+ onClick={() => console.log("Edit", row.original)}
303
+ sdsStyle="minimal"
304
+ sdsType="secondary"
305
+ >
306
+ <Icon sdsIcon="Edit" sdsSize="s" />
307
+ </Button>
308
+ <Button
309
+ aria-label={`Download ${row.original.name}`}
310
+ onClick={() => console.log("Download", row.original)}
311
+ sdsStyle="minimal"
312
+ sdsType="secondary"
313
+ >
314
+ <Icon sdsIcon="Download" sdsSize="s" />
315
+ </Button>
316
+ </CellComponent>
317
+ ),
318
+ enableSorting: false,
319
+ header: "Actions",
320
+ id: "actions",
321
+ meta: {
322
+ headerCellProps: { horizontalAlign: "right" },
323
+ pinning: "right",
324
+ verticalAlign: "middle",
325
+ },
326
+ size: 100,
327
+ },
328
+ ];
329
+
330
+ const PAGINATION = { pageSize: 10 };
331
+
332
+ function App() {
333
+ return (
334
+ <div className="app">
335
+ <PreComposedTable<Employee>
336
+ columns={COLUMNS}
337
+ data={DATA}
338
+ enablePagination
339
+ enableRowSelection
340
+ enableSorting
341
+ paginationConfig={PAGINATION}
342
+ />
343
+ </div>
344
+ );
345
+ }
346
+
347
+ export default App;
348
+ ```
349
+
350
+ ### With global filtering
351
+
352
+ `enableGlobalFiltering` puts a search input above the table that matches across every column at once. The table holds the search term itself, so there is nothing to wire up.
353
+
354
+ **Example: PreComposedTableWithGlobalFiltering**
355
+
356
+ ```tsx
357
+ // The WithGlobalFiltering story: enableGlobalFiltering puts a search input
358
+ // above the table that matches across every column at once. The table holds the
359
+ // search term itself, so there is nothing to wire up, and sorting and selection
360
+ // keep working on whatever rows are left.
361
+ //
362
+ // A column left as a plain accessor is drawn as a CellHeader and a CellBasic.
363
+ // A renderer of your own is handed the element and the width to draw with as
364
+ // `as` and `style`, and forwards both. Without `as` it renders a second <td>
365
+ // inside the one the table already drew. A header renderer is handed
366
+ // `data-pinned-edge` too, which is what tells a pinned header to draw its
367
+ // gradient.
368
+ //
369
+ // data and columns sit outside the component so their identity is stable.
370
+
371
+ import {
372
+ Button,
373
+ CellBasic,
374
+ CellComponent,
375
+ CellHeader,
376
+ Icon,
377
+ PreComposedTable,
378
+ Tag,
379
+ } from "@czi-sds/components";
380
+ import type {
381
+ CellContext,
382
+ ColumnDef,
383
+ HeaderContext,
384
+ } from "@tanstack/react-table";
385
+ import * as React from "react";
386
+
387
+ type Employee = {
388
+ department: string;
389
+ email: string;
390
+ lastActive: string;
391
+ location: string;
392
+ name: string;
393
+ projects: number;
394
+ role: string;
395
+ salary: number;
396
+ startDate: string;
397
+ status: "Active" | "Inactive";
398
+ };
399
+
400
+ const DATA: Employee[] = [
401
+ {
402
+ department: "Engineering",
403
+ email: "john.doe@example.com",
404
+ lastActive: "2024-01-15",
405
+ location: "San Francisco",
406
+ name: "John Doe",
407
+ projects: 5,
408
+ role: "Software Engineer",
409
+ salary: 85000,
410
+ startDate: "2022-03-15",
411
+ status: "Active",
412
+ },
413
+ {
414
+ department: "Product",
415
+ email: "jane.smith@example.com",
416
+ lastActive: "2024-01-14",
417
+ location: "New York",
418
+ name: "Jane Smith",
419
+ projects: 3,
420
+ role: "Product Manager",
421
+ salary: 95000,
422
+ startDate: "2021-08-20",
423
+ status: "Active",
424
+ },
425
+ {
426
+ department: "Design",
427
+ email: "bob.johnson@example.com",
428
+ lastActive: "2024-01-10",
429
+ location: "Austin",
430
+ name: "Bob Johnson",
431
+ projects: 2,
432
+ role: "Designer",
433
+ salary: 75000,
434
+ startDate: "2023-01-10",
435
+ status: "Inactive",
436
+ },
437
+ {
438
+ department: "Data",
439
+ email: "alice.brown@example.com",
440
+ lastActive: "2024-01-16",
441
+ location: "Seattle",
442
+ name: "Alice Brown",
443
+ projects: 7,
444
+ role: "Data Scientist",
445
+ salary: 90000,
446
+ startDate: "2022-11-05",
447
+ status: "Active",
448
+ },
449
+ {
450
+ department: "Engineering",
451
+ email: "charlie.wilson@example.com",
452
+ lastActive: "2024-01-15",
453
+ location: "Remote",
454
+ name: "Charlie Wilson",
455
+ projects: 4,
456
+ role: "DevOps Engineer",
457
+ salary: 88000,
458
+ startDate: "2023-06-12",
459
+ status: "Active",
460
+ },
461
+ ];
462
+
463
+ /** The cell context, plus how PreComposedTable wants the cell drawn. */
464
+ type CellProps = CellContext<Employee, unknown> & {
465
+ as?: React.ElementType;
466
+ style?: React.CSSProperties;
467
+ };
468
+
469
+ /** The header context, plus which edge a pinned header sits against. */
470
+ type HeaderProps = HeaderContext<Employee, unknown> & {
471
+ "data-pinned-edge"?: "left" | "right";
472
+ style?: React.CSSProperties;
473
+ };
474
+
475
+ function textCell({ as, getValue, style }: CellProps) {
476
+ return (
477
+ <CellBasic
478
+ as={as}
479
+ primaryText={String(getValue())}
480
+ shouldShowTooltipOnHover={false}
481
+ style={style}
482
+ />
483
+ );
484
+ }
485
+
486
+ function numberCell({ as, column, getValue, style }: CellProps) {
487
+ return (
488
+ <CellBasic
489
+ as={as}
490
+ // Room for the sort chevron, which would otherwise sit on the number.
491
+ extraRightPadding={column.getCanSort() ? 20 : 0}
492
+ horizontalAlign="right"
493
+ primaryText={String(getValue())}
494
+ shouldShowTooltipOnHover={false}
495
+ style={style}
496
+ tabularNums
497
+ />
498
+ );
499
+ }
500
+
501
+ const COLUMNS: ColumnDef<Employee>[] = [
502
+ {
503
+ accessorKey: "name",
504
+ cell: ({ as, getValue, row, style }: CellProps) => (
505
+ <CellBasic
506
+ as={as}
507
+ link={`mailto:${row.original.email}`}
508
+ linkComponent="a"
509
+ linkProps={{ target: "_blank" }}
510
+ primaryText={getValue() as string}
511
+ secondaryText={row.original.role}
512
+ shouldShowTooltipOnHover={false}
513
+ shouldShowUnderlineOnHover
514
+ style={style}
515
+ />
516
+ ),
517
+ enableSorting: false,
518
+ header: ({ "data-pinned-edge": pinnedEdge, style }: HeaderProps) => (
519
+ <CellHeader
520
+ data-pinned-edge={pinnedEdge}
521
+ hideSortIcon
522
+ hover
523
+ shouldShowTooltipOnHover
524
+ style={style}
525
+ tooltipProps={{ placement: "top", textAlign: "left", width: "wide" }}
526
+ tooltipSubtitle="Each name links to the person's email address."
527
+ tooltipText="Name"
528
+ >
529
+ Name
530
+ </CellHeader>
531
+ ),
532
+ meta: { isGrow: true, pinning: "left" },
533
+ minSize: 150,
534
+ },
535
+ {
536
+ accessorKey: "email",
537
+ cell: ({ as, getValue, style }: CellProps) => (
538
+ <CellBasic
539
+ as={as}
540
+ link={`mailto:${getValue() as string}`}
541
+ linkProps={{ target: "_blank" }}
542
+ primaryText={getValue() as string}
543
+ primaryTextWrapLineCount={1}
544
+ shouldShowTooltipOnHover={false}
545
+ shouldShowUnderlineOnHover
546
+ style={style}
547
+ />
548
+ ),
549
+ header: "Email",
550
+ meta: { isGrow: true },
551
+ minSize: 200,
552
+ },
553
+ {
554
+ accessorKey: "role",
555
+ cell: textCell,
556
+ header: "Role",
557
+ meta: { verticalAlign: "middle" },
558
+ minSize: 150,
559
+ },
560
+ {
561
+ accessorKey: "department",
562
+ cell: textCell,
563
+ header: "Department",
564
+ minSize: 120,
565
+ },
566
+ {
567
+ accessorKey: "status",
568
+ cell: ({ as, getValue, style }: CellProps) => (
569
+ <CellComponent as={as} style={style}>
570
+ <Tag
571
+ color={getValue() === "Active" ? "positive" : "negative"}
572
+ hover={false}
573
+ label={getValue() as string}
574
+ sdsStyle="rounded"
575
+ sdsType="secondary"
576
+ />
577
+ </CellComponent>
578
+ ),
579
+ header: "Status",
580
+ minSize: 100,
581
+ },
582
+ {
583
+ accessorKey: "lastActive",
584
+ cell: numberCell,
585
+ header: "Last Active",
586
+ meta: { headerCellProps: { horizontalAlign: "right" } },
587
+ minSize: 120,
588
+ },
589
+ {
590
+ accessorKey: "projects",
591
+ cell: numberCell,
592
+ header: "Projects",
593
+ meta: { headerCellProps: { horizontalAlign: "right" } },
594
+ minSize: 100,
595
+ },
596
+ {
597
+ accessorKey: "salary",
598
+ cell: ({ as, column, getValue, style }: CellProps) => (
599
+ <CellBasic
600
+ as={as}
601
+ extraRightPadding={column.getCanSort() ? 20 : 0}
602
+ horizontalAlign="right"
603
+ primaryText={`${(getValue() as number).toLocaleString()}`}
604
+ shouldShowTooltipOnHover={false}
605
+ style={style}
606
+ tabularNums
607
+ />
608
+ ),
609
+ header: "Salary",
610
+ meta: { headerCellProps: { horizontalAlign: "right" } },
611
+ minSize: 120,
612
+ },
613
+ {
614
+ accessorKey: "location",
615
+ cell: textCell,
616
+ header: "Location",
617
+ minSize: 150,
618
+ },
619
+ {
620
+ accessorKey: "startDate",
621
+ cell: numberCell,
622
+ header: "Start Date",
623
+ meta: { headerCellProps: { horizontalAlign: "right" } },
624
+ minSize: 120,
625
+ },
626
+ {
627
+ cell: ({ as, row, style }: CellProps) => (
628
+ <CellComponent as={as} horizontalAlign="right" style={style}>
629
+ <Button
630
+ aria-label={`Edit ${row.original.name}`}
631
+ onClick={() => console.log("Edit", row.original)}
632
+ sdsStyle="minimal"
633
+ sdsType="secondary"
634
+ >
635
+ <Icon sdsIcon="Edit" sdsSize="s" />
636
+ </Button>
637
+ <Button
638
+ aria-label={`Download ${row.original.name}`}
639
+ onClick={() => console.log("Download", row.original)}
640
+ sdsStyle="minimal"
641
+ sdsType="secondary"
642
+ >
643
+ <Icon sdsIcon="Download" sdsSize="s" />
644
+ </Button>
645
+ </CellComponent>
646
+ ),
647
+ enableSorting: false,
648
+ header: "Actions",
649
+ id: "actions",
650
+ meta: {
651
+ headerCellProps: { horizontalAlign: "right" },
652
+ pinning: "right",
653
+ verticalAlign: "middle",
654
+ },
655
+ size: 100,
656
+ },
657
+ ];
658
+
659
+ function App() {
660
+ return (
661
+ <div className="app">
662
+ <PreComposedTable<Employee>
663
+ columns={COLUMNS}
664
+ data={DATA}
665
+ enableGlobalFiltering
666
+ enableRowSelection
667
+ enableSorting
668
+ />
669
+ </div>
670
+ );
671
+ }
672
+
673
+ export default App;
674
+ ```
675
+
676
+ ### With pagination
677
+
678
+ The same table paged three rows at a time. `paginationConfig` takes Pagination's own props, so `pageSize` sets the size of every page and `placement` moves the controls, which sit on the right by default.
679
+
680
+ **Example: PreComposedTableWithPagination**
681
+
682
+ ```tsx
683
+ // The WithPagination story: the same table paged three rows at a time.
684
+ // paginationConfig takes the Pagination component's own props, so pageSize sets
685
+ // both the initial page size and the size of every page after it, and
686
+ // placement moves the controls, which sit on the right by default.
687
+ //
688
+ // A column left as a plain accessor is drawn as a CellHeader and a CellBasic.
689
+ // A renderer of your own is handed the element and the width to draw with as
690
+ // `as` and `style`, and forwards both. Without `as` it renders a second <td>
691
+ // inside the one the table already drew. A header renderer is handed
692
+ // `data-pinned-edge` too, which is what tells a pinned header to draw its
693
+ // gradient.
694
+ //
695
+ // data, columns and paginationConfig sit outside the component so their
696
+ // identity is stable: a new paginationConfig object sends the table back to its
697
+ // first page.
698
+
699
+ import {
700
+ Button,
701
+ CellBasic,
702
+ CellComponent,
703
+ CellHeader,
704
+ Icon,
705
+ PreComposedTable,
706
+ Tag,
707
+ } from "@czi-sds/components";
708
+ import type {
709
+ CellContext,
710
+ ColumnDef,
711
+ HeaderContext,
712
+ } from "@tanstack/react-table";
713
+ import * as React from "react";
714
+
715
+ type Employee = {
716
+ department: string;
717
+ email: string;
718
+ lastActive: string;
719
+ location: string;
720
+ name: string;
721
+ projects: number;
722
+ role: string;
723
+ salary: number;
724
+ startDate: string;
725
+ status: "Active" | "Inactive";
726
+ };
727
+
728
+ const DATA: Employee[] = [
729
+ {
730
+ department: "Engineering",
731
+ email: "john.doe@example.com",
732
+ lastActive: "2024-01-15",
733
+ location: "San Francisco",
734
+ name: "John Doe",
735
+ projects: 5,
736
+ role: "Software Engineer",
737
+ salary: 85000,
738
+ startDate: "2022-03-15",
739
+ status: "Active",
740
+ },
741
+ {
742
+ department: "Product",
743
+ email: "jane.smith@example.com",
744
+ lastActive: "2024-01-14",
745
+ location: "New York",
746
+ name: "Jane Smith",
747
+ projects: 3,
748
+ role: "Product Manager",
749
+ salary: 95000,
750
+ startDate: "2021-08-20",
751
+ status: "Active",
752
+ },
753
+ {
754
+ department: "Design",
755
+ email: "bob.johnson@example.com",
756
+ lastActive: "2024-01-10",
757
+ location: "Austin",
758
+ name: "Bob Johnson",
759
+ projects: 2,
760
+ role: "Designer",
761
+ salary: 75000,
762
+ startDate: "2023-01-10",
763
+ status: "Inactive",
764
+ },
765
+ {
766
+ department: "Data",
767
+ email: "alice.brown@example.com",
768
+ lastActive: "2024-01-16",
769
+ location: "Seattle",
770
+ name: "Alice Brown",
771
+ projects: 7,
772
+ role: "Data Scientist",
773
+ salary: 90000,
774
+ startDate: "2022-11-05",
775
+ status: "Active",
776
+ },
777
+ {
778
+ department: "Engineering",
779
+ email: "charlie.wilson@example.com",
780
+ lastActive: "2024-01-15",
781
+ location: "Remote",
782
+ name: "Charlie Wilson",
783
+ projects: 4,
784
+ role: "DevOps Engineer",
785
+ salary: 88000,
786
+ startDate: "2023-06-12",
787
+ status: "Active",
788
+ },
789
+ ];
790
+
791
+ /** The cell context, plus how PreComposedTable wants the cell drawn. */
792
+ type CellProps = CellContext<Employee, unknown> & {
793
+ as?: React.ElementType;
794
+ style?: React.CSSProperties;
795
+ };
796
+
797
+ /** The header context, plus which edge a pinned header sits against. */
798
+ type HeaderProps = HeaderContext<Employee, unknown> & {
799
+ "data-pinned-edge"?: "left" | "right";
800
+ style?: React.CSSProperties;
801
+ };
802
+
803
+ function textCell({ as, getValue, style }: CellProps) {
804
+ return (
805
+ <CellBasic
806
+ as={as}
807
+ primaryText={String(getValue())}
808
+ shouldShowTooltipOnHover={false}
809
+ style={style}
810
+ />
811
+ );
812
+ }
813
+
814
+ function numberCell({ as, column, getValue, style }: CellProps) {
815
+ return (
816
+ <CellBasic
817
+ as={as}
818
+ // Room for the sort chevron, which would otherwise sit on the number.
819
+ extraRightPadding={column.getCanSort() ? 20 : 0}
820
+ horizontalAlign="right"
821
+ primaryText={String(getValue())}
822
+ shouldShowTooltipOnHover={false}
823
+ style={style}
824
+ tabularNums
825
+ />
826
+ );
827
+ }
828
+
829
+ const COLUMNS: ColumnDef<Employee>[] = [
830
+ {
831
+ accessorKey: "name",
832
+ cell: ({ as, getValue, row, style }: CellProps) => (
833
+ <CellBasic
834
+ as={as}
835
+ link={`mailto:${row.original.email}`}
836
+ linkComponent="a"
837
+ linkProps={{ target: "_blank" }}
838
+ primaryText={getValue() as string}
839
+ secondaryText={row.original.role}
840
+ shouldShowTooltipOnHover={false}
841
+ shouldShowUnderlineOnHover
842
+ style={style}
843
+ />
844
+ ),
845
+ enableSorting: false,
846
+ header: ({ "data-pinned-edge": pinnedEdge, style }: HeaderProps) => (
847
+ <CellHeader
848
+ data-pinned-edge={pinnedEdge}
849
+ hideSortIcon
850
+ hover
851
+ shouldShowTooltipOnHover
852
+ style={style}
853
+ tooltipProps={{ placement: "top", textAlign: "left", width: "wide" }}
854
+ tooltipSubtitle="Each name links to the person's email address."
855
+ tooltipText="Name"
856
+ >
857
+ Name
858
+ </CellHeader>
859
+ ),
860
+ meta: { isGrow: true, pinning: "left" },
861
+ minSize: 150,
862
+ },
863
+ {
864
+ accessorKey: "email",
865
+ cell: ({ as, getValue, style }: CellProps) => (
866
+ <CellBasic
867
+ as={as}
868
+ link={`mailto:${getValue() as string}`}
869
+ linkProps={{ target: "_blank" }}
870
+ primaryText={getValue() as string}
871
+ primaryTextWrapLineCount={1}
872
+ shouldShowTooltipOnHover={false}
873
+ shouldShowUnderlineOnHover
874
+ style={style}
875
+ />
876
+ ),
877
+ header: "Email",
878
+ meta: { isGrow: true },
879
+ minSize: 200,
880
+ },
881
+ {
882
+ accessorKey: "role",
883
+ cell: textCell,
884
+ header: "Role",
885
+ meta: { verticalAlign: "middle" },
886
+ minSize: 150,
887
+ },
888
+ {
889
+ accessorKey: "department",
890
+ cell: textCell,
891
+ header: "Department",
892
+ minSize: 120,
893
+ },
894
+ {
895
+ accessorKey: "status",
896
+ cell: ({ as, getValue, style }: CellProps) => (
897
+ <CellComponent as={as} style={style}>
898
+ <Tag
899
+ color={getValue() === "Active" ? "positive" : "negative"}
900
+ hover={false}
901
+ label={getValue() as string}
902
+ sdsStyle="rounded"
903
+ sdsType="secondary"
904
+ />
905
+ </CellComponent>
906
+ ),
907
+ header: "Status",
908
+ minSize: 100,
909
+ },
910
+ {
911
+ accessorKey: "lastActive",
912
+ cell: numberCell,
913
+ header: "Last Active",
914
+ meta: { headerCellProps: { horizontalAlign: "right" } },
915
+ minSize: 120,
916
+ },
917
+ {
918
+ accessorKey: "projects",
919
+ cell: numberCell,
920
+ header: "Projects",
921
+ meta: { headerCellProps: { horizontalAlign: "right" } },
922
+ minSize: 100,
923
+ },
924
+ {
925
+ accessorKey: "salary",
926
+ cell: ({ as, column, getValue, style }: CellProps) => (
927
+ <CellBasic
928
+ as={as}
929
+ extraRightPadding={column.getCanSort() ? 20 : 0}
930
+ horizontalAlign="right"
931
+ primaryText={`${(getValue() as number).toLocaleString()}`}
932
+ shouldShowTooltipOnHover={false}
933
+ style={style}
934
+ tabularNums
935
+ />
936
+ ),
937
+ header: "Salary",
938
+ meta: { headerCellProps: { horizontalAlign: "right" } },
939
+ minSize: 120,
940
+ },
941
+ {
942
+ accessorKey: "location",
943
+ cell: textCell,
944
+ header: "Location",
945
+ minSize: 150,
946
+ },
947
+ {
948
+ accessorKey: "startDate",
949
+ cell: numberCell,
950
+ header: "Start Date",
951
+ meta: { headerCellProps: { horizontalAlign: "right" } },
952
+ minSize: 120,
953
+ },
954
+ {
955
+ cell: ({ as, row, style }: CellProps) => (
956
+ <CellComponent as={as} horizontalAlign="right" style={style}>
957
+ <Button
958
+ aria-label={`Edit ${row.original.name}`}
959
+ onClick={() => console.log("Edit", row.original)}
960
+ sdsStyle="minimal"
961
+ sdsType="secondary"
962
+ >
963
+ <Icon sdsIcon="Edit" sdsSize="s" />
964
+ </Button>
965
+ <Button
966
+ aria-label={`Download ${row.original.name}`}
967
+ onClick={() => console.log("Download", row.original)}
968
+ sdsStyle="minimal"
969
+ sdsType="secondary"
970
+ >
971
+ <Icon sdsIcon="Download" sdsSize="s" />
972
+ </Button>
973
+ </CellComponent>
974
+ ),
975
+ enableSorting: false,
976
+ header: "Actions",
977
+ id: "actions",
978
+ meta: {
979
+ headerCellProps: { horizontalAlign: "right" },
980
+ pinning: "right",
981
+ verticalAlign: "middle",
982
+ },
983
+ size: 100,
984
+ },
985
+ ];
986
+
987
+ const PAGINATION = { pageSize: 3 };
988
+
989
+ function App() {
990
+ return (
991
+ <div className="app">
992
+ <PreComposedTable<Employee>
993
+ columns={COLUMNS}
994
+ data={DATA}
995
+ enablePagination
996
+ enableRowSelection
997
+ enableSorting
998
+ paginationConfig={PAGINATION}
999
+ />
1000
+ </div>
1001
+ );
1002
+ }
1003
+
1004
+ export default App;
1005
+ ```
1006
+
1007
+ ### Fixed width
1008
+
1009
+ `tableWidth` pins the table to 600px rather than letting it fill its container. The columns need more room than that, so the table scrolls sideways while the pinned Name and Actions columns hold their edges.
1010
+
1011
+ **Example: PreComposedTableFixedWidth**
1012
+
1013
+ ```tsx
1014
+ // The FixedWidth story: tableWidth pins the table to 600px instead of filling
1015
+ // its container. The columns need more room than that, so the table scrolls
1016
+ // sideways and the pinned Name and Actions columns hold their edges, each
1017
+ // drawing a gradient once there is something hidden behind it.
1018
+ //
1019
+ // A column left as a plain accessor is drawn as a CellHeader and a CellBasic.
1020
+ // A renderer of your own is handed the element and the width to draw with as
1021
+ // `as` and `style`, and forwards both. Without `as` it renders a second <td>
1022
+ // inside the one the table already drew. A header renderer is handed
1023
+ // `data-pinned-edge` too, which is what tells a pinned header to draw its
1024
+ // gradient.
1025
+ //
1026
+ // data, columns and paginationConfig sit outside the component so their
1027
+ // identity is stable: a new paginationConfig object sends the table back to its
1028
+ // first page.
1029
+
1030
+ import {
1031
+ Button,
1032
+ CellBasic,
1033
+ CellComponent,
1034
+ CellHeader,
1035
+ Icon,
1036
+ PreComposedTable,
1037
+ Tag,
1038
+ } from "@czi-sds/components";
1039
+ import type {
1040
+ CellContext,
1041
+ ColumnDef,
1042
+ HeaderContext,
1043
+ } from "@tanstack/react-table";
1044
+ import * as React from "react";
1045
+
1046
+ type Employee = {
1047
+ department: string;
1048
+ email: string;
1049
+ lastActive: string;
1050
+ location: string;
1051
+ name: string;
1052
+ projects: number;
1053
+ role: string;
1054
+ salary: number;
1055
+ startDate: string;
1056
+ status: "Active" | "Inactive";
1057
+ };
1058
+
1059
+ const DATA: Employee[] = [
1060
+ {
1061
+ department: "Engineering",
1062
+ email: "john.doe@example.com",
1063
+ lastActive: "2024-01-15",
1064
+ location: "San Francisco",
1065
+ name: "John Doe",
1066
+ projects: 5,
1067
+ role: "Software Engineer",
1068
+ salary: 85000,
1069
+ startDate: "2022-03-15",
1070
+ status: "Active",
1071
+ },
1072
+ {
1073
+ department: "Product",
1074
+ email: "jane.smith@example.com",
1075
+ lastActive: "2024-01-14",
1076
+ location: "New York",
1077
+ name: "Jane Smith",
1078
+ projects: 3,
1079
+ role: "Product Manager",
1080
+ salary: 95000,
1081
+ startDate: "2021-08-20",
1082
+ status: "Active",
1083
+ },
1084
+ {
1085
+ department: "Design",
1086
+ email: "bob.johnson@example.com",
1087
+ lastActive: "2024-01-10",
1088
+ location: "Austin",
1089
+ name: "Bob Johnson",
1090
+ projects: 2,
1091
+ role: "Designer",
1092
+ salary: 75000,
1093
+ startDate: "2023-01-10",
1094
+ status: "Inactive",
1095
+ },
1096
+ {
1097
+ department: "Data",
1098
+ email: "alice.brown@example.com",
1099
+ lastActive: "2024-01-16",
1100
+ location: "Seattle",
1101
+ name: "Alice Brown",
1102
+ projects: 7,
1103
+ role: "Data Scientist",
1104
+ salary: 90000,
1105
+ startDate: "2022-11-05",
1106
+ status: "Active",
1107
+ },
1108
+ {
1109
+ department: "Engineering",
1110
+ email: "charlie.wilson@example.com",
1111
+ lastActive: "2024-01-15",
1112
+ location: "Remote",
1113
+ name: "Charlie Wilson",
1114
+ projects: 4,
1115
+ role: "DevOps Engineer",
1116
+ salary: 88000,
1117
+ startDate: "2023-06-12",
1118
+ status: "Active",
1119
+ },
1120
+ ];
1121
+
1122
+ /** The cell context, plus how PreComposedTable wants the cell drawn. */
1123
+ type CellProps = CellContext<Employee, unknown> & {
1124
+ as?: React.ElementType;
1125
+ style?: React.CSSProperties;
1126
+ };
1127
+
1128
+ /** The header context, plus which edge a pinned header sits against. */
1129
+ type HeaderProps = HeaderContext<Employee, unknown> & {
1130
+ "data-pinned-edge"?: "left" | "right";
1131
+ style?: React.CSSProperties;
1132
+ };
1133
+
1134
+ function textCell({ as, getValue, style }: CellProps) {
1135
+ return (
1136
+ <CellBasic
1137
+ as={as}
1138
+ primaryText={String(getValue())}
1139
+ shouldShowTooltipOnHover={false}
1140
+ style={style}
1141
+ />
1142
+ );
1143
+ }
1144
+
1145
+ function numberCell({ as, column, getValue, style }: CellProps) {
1146
+ return (
1147
+ <CellBasic
1148
+ as={as}
1149
+ // Room for the sort chevron, which would otherwise sit on the number.
1150
+ extraRightPadding={column.getCanSort() ? 20 : 0}
1151
+ horizontalAlign="right"
1152
+ primaryText={String(getValue())}
1153
+ shouldShowTooltipOnHover={false}
1154
+ style={style}
1155
+ tabularNums
1156
+ />
1157
+ );
1158
+ }
1159
+
1160
+ const COLUMNS: ColumnDef<Employee>[] = [
1161
+ {
1162
+ accessorKey: "name",
1163
+ cell: ({ as, getValue, row, style }: CellProps) => (
1164
+ <CellBasic
1165
+ as={as}
1166
+ link={`mailto:${row.original.email}`}
1167
+ linkComponent="a"
1168
+ linkProps={{ target: "_blank" }}
1169
+ primaryText={getValue() as string}
1170
+ secondaryText={row.original.role}
1171
+ shouldShowTooltipOnHover={false}
1172
+ shouldShowUnderlineOnHover
1173
+ style={style}
1174
+ />
1175
+ ),
1176
+ enableSorting: false,
1177
+ header: ({ "data-pinned-edge": pinnedEdge, style }: HeaderProps) => (
1178
+ <CellHeader
1179
+ data-pinned-edge={pinnedEdge}
1180
+ hideSortIcon
1181
+ hover
1182
+ shouldShowTooltipOnHover
1183
+ style={style}
1184
+ tooltipProps={{ placement: "top", textAlign: "left", width: "wide" }}
1185
+ tooltipSubtitle="Each name links to the person's email address."
1186
+ tooltipText="Name"
1187
+ >
1188
+ Name
1189
+ </CellHeader>
1190
+ ),
1191
+ meta: { isGrow: true, pinning: "left" },
1192
+ minSize: 150,
1193
+ },
1194
+ {
1195
+ accessorKey: "email",
1196
+ cell: ({ as, getValue, style }: CellProps) => (
1197
+ <CellBasic
1198
+ as={as}
1199
+ link={`mailto:${getValue() as string}`}
1200
+ linkProps={{ target: "_blank" }}
1201
+ primaryText={getValue() as string}
1202
+ primaryTextWrapLineCount={1}
1203
+ shouldShowTooltipOnHover={false}
1204
+ shouldShowUnderlineOnHover
1205
+ style={style}
1206
+ />
1207
+ ),
1208
+ header: "Email",
1209
+ meta: { isGrow: true },
1210
+ minSize: 200,
1211
+ },
1212
+ {
1213
+ accessorKey: "role",
1214
+ cell: textCell,
1215
+ header: "Role",
1216
+ meta: { verticalAlign: "middle" },
1217
+ minSize: 150,
1218
+ },
1219
+ {
1220
+ accessorKey: "department",
1221
+ cell: textCell,
1222
+ header: "Department",
1223
+ minSize: 120,
1224
+ },
1225
+ {
1226
+ accessorKey: "status",
1227
+ cell: ({ as, getValue, style }: CellProps) => (
1228
+ <CellComponent as={as} style={style}>
1229
+ <Tag
1230
+ color={getValue() === "Active" ? "positive" : "negative"}
1231
+ hover={false}
1232
+ label={getValue() as string}
1233
+ sdsStyle="rounded"
1234
+ sdsType="secondary"
1235
+ />
1236
+ </CellComponent>
1237
+ ),
1238
+ header: "Status",
1239
+ minSize: 100,
1240
+ },
1241
+ {
1242
+ accessorKey: "lastActive",
1243
+ cell: numberCell,
1244
+ header: "Last Active",
1245
+ meta: { headerCellProps: { horizontalAlign: "right" } },
1246
+ minSize: 120,
1247
+ },
1248
+ {
1249
+ accessorKey: "projects",
1250
+ cell: numberCell,
1251
+ header: "Projects",
1252
+ meta: { headerCellProps: { horizontalAlign: "right" } },
1253
+ minSize: 100,
1254
+ },
1255
+ {
1256
+ accessorKey: "salary",
1257
+ cell: ({ as, column, getValue, style }: CellProps) => (
1258
+ <CellBasic
1259
+ as={as}
1260
+ extraRightPadding={column.getCanSort() ? 20 : 0}
1261
+ horizontalAlign="right"
1262
+ primaryText={`${(getValue() as number).toLocaleString()}`}
1263
+ shouldShowTooltipOnHover={false}
1264
+ style={style}
1265
+ tabularNums
1266
+ />
1267
+ ),
1268
+ header: "Salary",
1269
+ meta: { headerCellProps: { horizontalAlign: "right" } },
1270
+ minSize: 120,
1271
+ },
1272
+ {
1273
+ accessorKey: "location",
1274
+ cell: textCell,
1275
+ header: "Location",
1276
+ minSize: 150,
1277
+ },
1278
+ {
1279
+ accessorKey: "startDate",
1280
+ cell: numberCell,
1281
+ header: "Start Date",
1282
+ meta: { headerCellProps: { horizontalAlign: "right" } },
1283
+ minSize: 120,
1284
+ },
1285
+ {
1286
+ cell: ({ as, row, style }: CellProps) => (
1287
+ <CellComponent as={as} horizontalAlign="right" style={style}>
1288
+ <Button
1289
+ aria-label={`Edit ${row.original.name}`}
1290
+ onClick={() => console.log("Edit", row.original)}
1291
+ sdsStyle="minimal"
1292
+ sdsType="secondary"
1293
+ >
1294
+ <Icon sdsIcon="Edit" sdsSize="s" />
1295
+ </Button>
1296
+ <Button
1297
+ aria-label={`Download ${row.original.name}`}
1298
+ onClick={() => console.log("Download", row.original)}
1299
+ sdsStyle="minimal"
1300
+ sdsType="secondary"
1301
+ >
1302
+ <Icon sdsIcon="Download" sdsSize="s" />
1303
+ </Button>
1304
+ </CellComponent>
1305
+ ),
1306
+ enableSorting: false,
1307
+ header: "Actions",
1308
+ id: "actions",
1309
+ meta: {
1310
+ headerCellProps: { horizontalAlign: "right" },
1311
+ pinning: "right",
1312
+ verticalAlign: "middle",
1313
+ },
1314
+ size: 100,
1315
+ },
1316
+ ];
1317
+
1318
+ const PAGINATION = { pageSize: 10 };
1319
+
1320
+ function App() {
1321
+ return (
1322
+ <div className="app">
1323
+ <PreComposedTable<Employee>
1324
+ columns={COLUMNS}
1325
+ data={DATA}
1326
+ enablePagination
1327
+ enableRowSelection
1328
+ enableSorting
1329
+ paginationConfig={PAGINATION}
1330
+ tableWidth="600px"
1331
+ />
1332
+ </div>
1333
+ );
1334
+ }
1335
+
1336
+ export default App;
1337
+ ```
1338
+
1339
+ ### With column filtering
1340
+
1341
+ `enableColumnFiltering` adds a row of search inputs under the header, one per column. The filters are held above the table here rather than inside it: `state.columnFilters` and `onColumnFiltersChange` passed through `tableOptions` override the state the component keeps, which is what makes them yours to read, persist or reset.
1342
+
1343
+ **Example: PreComposedTableWithColumnFiltering**
1344
+
1345
+ ```tsx
1346
+ // The WithColumnFiltering story: enableColumnFiltering adds a row of search
1347
+ // inputs under the header, one per column.
1348
+ //
1349
+ // The filter state is held above the table here rather than inside it. A state
1350
+ // you pass through tableOptions overrides the one the component keeps, so
1351
+ // state.columnFilters plus onColumnFiltersChange hands you the filters to read,
1352
+ // persist or reset from your own code. TanStack sends change handlers an
1353
+ // updater that is either the next value or a function of the current one, so
1354
+ // resolve it against your state rather than storing it directly.
1355
+ //
1356
+ // A column left as a plain accessor is drawn as a CellHeader and a CellBasic.
1357
+ // A renderer of your own is handed the element and the width to draw with as
1358
+ // `as` and `style`, and forwards both. Without `as` it renders a second <td>
1359
+ // inside the one the table already drew. A header renderer is handed
1360
+ // `data-pinned-edge` too, which is what tells a pinned header to draw its
1361
+ // gradient.
1362
+ //
1363
+ // data, columns and paginationConfig sit outside the component so their
1364
+ // identity is stable: a new paginationConfig object sends the table back to its
1365
+ // first page.
1366
+
1367
+ import {
1368
+ Button,
1369
+ CellBasic,
1370
+ CellComponent,
1371
+ CellHeader,
1372
+ Icon,
1373
+ PreComposedTable,
1374
+ Tag,
1375
+ } from "@czi-sds/components";
1376
+ import type {
1377
+ CellContext,
1378
+ ColumnDef,
1379
+ ColumnFiltersState,
1380
+ HeaderContext,
1381
+ Updater,
1382
+ } from "@tanstack/react-table";
1383
+ import * as React from "react";
1384
+
1385
+ type Employee = {
1386
+ department: string;
1387
+ email: string;
1388
+ lastActive: string;
1389
+ location: string;
1390
+ name: string;
1391
+ projects: number;
1392
+ role: string;
1393
+ salary: number;
1394
+ startDate: string;
1395
+ status: "Active" | "Inactive";
1396
+ };
1397
+
1398
+ const DATA: Employee[] = [
1399
+ {
1400
+ department: "Engineering",
1401
+ email: "john.doe@example.com",
1402
+ lastActive: "2024-01-15",
1403
+ location: "San Francisco",
1404
+ name: "John Doe",
1405
+ projects: 5,
1406
+ role: "Software Engineer",
1407
+ salary: 85000,
1408
+ startDate: "2022-03-15",
1409
+ status: "Active",
1410
+ },
1411
+ {
1412
+ department: "Product",
1413
+ email: "jane.smith@example.com",
1414
+ lastActive: "2024-01-14",
1415
+ location: "New York",
1416
+ name: "Jane Smith",
1417
+ projects: 3,
1418
+ role: "Product Manager",
1419
+ salary: 95000,
1420
+ startDate: "2021-08-20",
1421
+ status: "Active",
1422
+ },
1423
+ {
1424
+ department: "Design",
1425
+ email: "bob.johnson@example.com",
1426
+ lastActive: "2024-01-10",
1427
+ location: "Austin",
1428
+ name: "Bob Johnson",
1429
+ projects: 2,
1430
+ role: "Designer",
1431
+ salary: 75000,
1432
+ startDate: "2023-01-10",
1433
+ status: "Inactive",
1434
+ },
1435
+ {
1436
+ department: "Data",
1437
+ email: "alice.brown@example.com",
1438
+ lastActive: "2024-01-16",
1439
+ location: "Seattle",
1440
+ name: "Alice Brown",
1441
+ projects: 7,
1442
+ role: "Data Scientist",
1443
+ salary: 90000,
1444
+ startDate: "2022-11-05",
1445
+ status: "Active",
1446
+ },
1447
+ {
1448
+ department: "Engineering",
1449
+ email: "charlie.wilson@example.com",
1450
+ lastActive: "2024-01-15",
1451
+ location: "Remote",
1452
+ name: "Charlie Wilson",
1453
+ projects: 4,
1454
+ role: "DevOps Engineer",
1455
+ salary: 88000,
1456
+ startDate: "2023-06-12",
1457
+ status: "Active",
1458
+ },
1459
+ ];
1460
+
1461
+ /** The cell context, plus how PreComposedTable wants the cell drawn. */
1462
+ type CellProps = CellContext<Employee, unknown> & {
1463
+ as?: React.ElementType;
1464
+ style?: React.CSSProperties;
1465
+ };
1466
+
1467
+ /** The header context, plus which edge a pinned header sits against. */
1468
+ type HeaderProps = HeaderContext<Employee, unknown> & {
1469
+ "data-pinned-edge"?: "left" | "right";
1470
+ style?: React.CSSProperties;
1471
+ };
1472
+
1473
+ function textCell({ as, getValue, style }: CellProps) {
1474
+ return (
1475
+ <CellBasic
1476
+ as={as}
1477
+ primaryText={String(getValue())}
1478
+ shouldShowTooltipOnHover={false}
1479
+ style={style}
1480
+ />
1481
+ );
1482
+ }
1483
+
1484
+ function numberCell({ as, column, getValue, style }: CellProps) {
1485
+ return (
1486
+ <CellBasic
1487
+ as={as}
1488
+ // Room for the sort chevron, which would otherwise sit on the number.
1489
+ extraRightPadding={column.getCanSort() ? 20 : 0}
1490
+ horizontalAlign="right"
1491
+ primaryText={String(getValue())}
1492
+ shouldShowTooltipOnHover={false}
1493
+ style={style}
1494
+ tabularNums
1495
+ />
1496
+ );
1497
+ }
1498
+
1499
+ const COLUMNS: ColumnDef<Employee>[] = [
1500
+ {
1501
+ accessorKey: "name",
1502
+ cell: ({ as, getValue, row, style }: CellProps) => (
1503
+ <CellBasic
1504
+ as={as}
1505
+ link={`mailto:${row.original.email}`}
1506
+ linkComponent="a"
1507
+ linkProps={{ target: "_blank" }}
1508
+ primaryText={getValue() as string}
1509
+ secondaryText={row.original.role}
1510
+ shouldShowTooltipOnHover={false}
1511
+ shouldShowUnderlineOnHover
1512
+ style={style}
1513
+ />
1514
+ ),
1515
+ enableSorting: false,
1516
+ header: ({ "data-pinned-edge": pinnedEdge, style }: HeaderProps) => (
1517
+ <CellHeader
1518
+ data-pinned-edge={pinnedEdge}
1519
+ hideSortIcon
1520
+ hover
1521
+ shouldShowTooltipOnHover
1522
+ style={style}
1523
+ tooltipProps={{ placement: "top", textAlign: "left", width: "wide" }}
1524
+ tooltipSubtitle="Each name links to the person's email address."
1525
+ tooltipText="Name"
1526
+ >
1527
+ Name
1528
+ </CellHeader>
1529
+ ),
1530
+ meta: { isGrow: true, pinning: "left" },
1531
+ minSize: 150,
1532
+ },
1533
+ {
1534
+ accessorKey: "email",
1535
+ cell: ({ as, getValue, style }: CellProps) => (
1536
+ <CellBasic
1537
+ as={as}
1538
+ link={`mailto:${getValue() as string}`}
1539
+ linkProps={{ target: "_blank" }}
1540
+ primaryText={getValue() as string}
1541
+ primaryTextWrapLineCount={1}
1542
+ shouldShowTooltipOnHover={false}
1543
+ shouldShowUnderlineOnHover
1544
+ style={style}
1545
+ />
1546
+ ),
1547
+ header: "Email",
1548
+ meta: { isGrow: true },
1549
+ minSize: 200,
1550
+ },
1551
+ {
1552
+ accessorKey: "role",
1553
+ cell: textCell,
1554
+ header: "Role",
1555
+ meta: { verticalAlign: "middle" },
1556
+ minSize: 150,
1557
+ },
1558
+ {
1559
+ accessorKey: "department",
1560
+ cell: textCell,
1561
+ header: "Department",
1562
+ minSize: 120,
1563
+ },
1564
+ {
1565
+ accessorKey: "status",
1566
+ cell: ({ as, getValue, style }: CellProps) => (
1567
+ <CellComponent as={as} style={style}>
1568
+ <Tag
1569
+ color={getValue() === "Active" ? "positive" : "negative"}
1570
+ hover={false}
1571
+ label={getValue() as string}
1572
+ sdsStyle="rounded"
1573
+ sdsType="secondary"
1574
+ />
1575
+ </CellComponent>
1576
+ ),
1577
+ header: "Status",
1578
+ minSize: 100,
1579
+ },
1580
+ {
1581
+ accessorKey: "lastActive",
1582
+ cell: numberCell,
1583
+ header: "Last Active",
1584
+ meta: { headerCellProps: { horizontalAlign: "right" } },
1585
+ minSize: 120,
1586
+ },
1587
+ {
1588
+ accessorKey: "projects",
1589
+ cell: numberCell,
1590
+ header: "Projects",
1591
+ meta: { headerCellProps: { horizontalAlign: "right" } },
1592
+ minSize: 100,
1593
+ },
1594
+ {
1595
+ accessorKey: "salary",
1596
+ cell: ({ as, column, getValue, style }: CellProps) => (
1597
+ <CellBasic
1598
+ as={as}
1599
+ extraRightPadding={column.getCanSort() ? 20 : 0}
1600
+ horizontalAlign="right"
1601
+ primaryText={`${(getValue() as number).toLocaleString()}`}
1602
+ shouldShowTooltipOnHover={false}
1603
+ style={style}
1604
+ tabularNums
1605
+ />
1606
+ ),
1607
+ header: "Salary",
1608
+ meta: { headerCellProps: { horizontalAlign: "right" } },
1609
+ minSize: 120,
1610
+ },
1611
+ {
1612
+ accessorKey: "location",
1613
+ cell: textCell,
1614
+ header: "Location",
1615
+ minSize: 150,
1616
+ },
1617
+ {
1618
+ accessorKey: "startDate",
1619
+ cell: numberCell,
1620
+ header: "Start Date",
1621
+ meta: { headerCellProps: { horizontalAlign: "right" } },
1622
+ minSize: 120,
1623
+ },
1624
+ {
1625
+ cell: ({ as, row, style }: CellProps) => (
1626
+ <CellComponent as={as} horizontalAlign="right" style={style}>
1627
+ <Button
1628
+ aria-label={`Edit ${row.original.name}`}
1629
+ onClick={() => console.log("Edit", row.original)}
1630
+ sdsStyle="minimal"
1631
+ sdsType="secondary"
1632
+ >
1633
+ <Icon sdsIcon="Edit" sdsSize="s" />
1634
+ </Button>
1635
+ <Button
1636
+ aria-label={`Download ${row.original.name}`}
1637
+ onClick={() => console.log("Download", row.original)}
1638
+ sdsStyle="minimal"
1639
+ sdsType="secondary"
1640
+ >
1641
+ <Icon sdsIcon="Download" sdsSize="s" />
1642
+ </Button>
1643
+ </CellComponent>
1644
+ ),
1645
+ enableSorting: false,
1646
+ header: "Actions",
1647
+ id: "actions",
1648
+ meta: {
1649
+ headerCellProps: { horizontalAlign: "right" },
1650
+ pinning: "right",
1651
+ verticalAlign: "middle",
1652
+ },
1653
+ size: 100,
1654
+ },
1655
+ ];
1656
+
1657
+ function App() {
1658
+ const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
1659
+ []
1660
+ );
1661
+
1662
+ const tableOptions = React.useMemo(
1663
+ () => ({
1664
+ onColumnFiltersChange: (updater: Updater<ColumnFiltersState>) =>
1665
+ setColumnFilters((current) =>
1666
+ typeof updater === "function" ? updater(current) : updater
1667
+ ),
1668
+ state: { columnFilters },
1669
+ }),
1670
+ [columnFilters]
1671
+ );
1672
+
1673
+ return (
1674
+ <div className="app">
1675
+ <PreComposedTable<Employee>
1676
+ columns={COLUMNS}
1677
+ data={DATA}
1678
+ enableColumnFiltering
1679
+ enableSorting
1680
+ tableOptions={tableOptions}
1681
+ />
1682
+ </div>
1683
+ );
1684
+ }
1685
+
1686
+ export default App;
1687
+ ```
1688
+
1689
+ ### With selective column filtering
1690
+
1691
+ Not every column has to take part in the filter row. Status replaces its input with a Dropdown through `meta.filterComponent` and matches on equality rather than the default substring search, while Salary and Start Date opt out altogether with `enableColumnFilter: false`.
1692
+
1693
+ **Example: PreComposedTableWithSelectiveColumnFiltering**
1694
+
1695
+ ```tsx
1696
+ // The WithSelectiveColumnFiltering story: enableColumnFiltering adds a filter
1697
+ // row under the header, but not every column has to take part in it.
1698
+ //
1699
+ // Status replaces its search input with a Dropdown through
1700
+ // meta.filterComponent, and because that writes a whole status rather than
1701
+ // something to search for, the column matches with a filterFn of equalsString.
1702
+ // Salary and Start Date opt out altogether with enableColumnFilter: false,
1703
+ // which leaves their filter cells empty.
1704
+ //
1705
+ // filterComponent is called as a plain function rather than mounted as a
1706
+ // component, so keep state out of it and read the current value back from
1707
+ // column.getFilterValue().
1708
+
1709
+ import {
1710
+ CellBasic,
1711
+ CellComponent,
1712
+ Dropdown,
1713
+ PreComposedTable,
1714
+ Tag,
1715
+ } from "@czi-sds/components";
1716
+ import type { CellContext, ColumnDef } from "@tanstack/react-table";
1717
+ import * as React from "react";
1718
+
1719
+ type Employee = {
1720
+ department: string;
1721
+ email: string;
1722
+ name: string;
1723
+ role: string;
1724
+ salary: number;
1725
+ startDate: string;
1726
+ status: "Active" | "Inactive";
1727
+ };
1728
+
1729
+ const DATA: Employee[] = [
1730
+ {
1731
+ department: "Engineering",
1732
+ email: "john.doe@example.com",
1733
+ name: "John Doe",
1734
+ role: "Software Engineer",
1735
+ salary: 85000,
1736
+ startDate: "2022-03-15",
1737
+ status: "Active",
1738
+ },
1739
+ {
1740
+ department: "Product",
1741
+ email: "jane.smith@example.com",
1742
+ name: "Jane Smith",
1743
+ role: "Product Manager",
1744
+ salary: 95000,
1745
+ startDate: "2021-08-20",
1746
+ status: "Active",
1747
+ },
1748
+ {
1749
+ department: "Design",
1750
+ email: "bob.johnson@example.com",
1751
+ name: "Bob Johnson",
1752
+ role: "Designer",
1753
+ salary: 75000,
1754
+ startDate: "2023-01-10",
1755
+ status: "Inactive",
1756
+ },
1757
+ {
1758
+ department: "Data",
1759
+ email: "alice.brown@example.com",
1760
+ name: "Alice Brown",
1761
+ role: "Data Scientist",
1762
+ salary: 90000,
1763
+ startDate: "2022-11-05",
1764
+ status: "Active",
1765
+ },
1766
+ {
1767
+ department: "Engineering",
1768
+ email: "charlie.wilson@example.com",
1769
+ name: "Charlie Wilson",
1770
+ role: "DevOps Engineer",
1771
+ salary: 88000,
1772
+ startDate: "2023-06-12",
1773
+ status: "Active",
1774
+ },
1775
+ ];
1776
+
1777
+ const STATUS_OPTIONS = [
1778
+ { name: "All" },
1779
+ { name: "Active" },
1780
+ { name: "Inactive" },
1781
+ ];
1782
+
1783
+ /** The cell context, plus how PreComposedTable wants the cell drawn. */
1784
+ type CellProps = CellContext<Employee, unknown> & {
1785
+ as?: React.ElementType;
1786
+ style?: React.CSSProperties;
1787
+ };
1788
+
1789
+ function textCell({ as, getValue, style }: CellProps) {
1790
+ return (
1791
+ <CellBasic
1792
+ as={as}
1793
+ primaryText={String(getValue())}
1794
+ shouldShowTooltipOnHover={false}
1795
+ style={style}
1796
+ />
1797
+ );
1798
+ }
1799
+
1800
+ const COLUMNS: ColumnDef<Employee>[] = [
1801
+ {
1802
+ accessorKey: "name",
1803
+ cell: textCell,
1804
+ header: "Name",
1805
+ meta: { isGrow: true, pinning: "left" },
1806
+ minSize: 150,
1807
+ },
1808
+ {
1809
+ accessorKey: "email",
1810
+ cell: textCell,
1811
+ header: "Email",
1812
+ meta: { isGrow: true },
1813
+ minSize: 200,
1814
+ },
1815
+ { accessorKey: "role", cell: textCell, header: "Role", minSize: 150 },
1816
+ {
1817
+ accessorKey: "department",
1818
+ cell: textCell,
1819
+ header: "Department",
1820
+ minSize: 120,
1821
+ },
1822
+ {
1823
+ accessorKey: "status",
1824
+ cell: ({ as, getValue, style }: CellProps) => (
1825
+ <CellComponent as={as} style={style}>
1826
+ <Tag
1827
+ color={getValue() === "Active" ? "positive" : "negative"}
1828
+ hover={false}
1829
+ label={getValue() as string}
1830
+ sdsStyle="rounded"
1831
+ sdsType="secondary"
1832
+ />
1833
+ </CellComponent>
1834
+ ),
1835
+ filterFn: "equalsString",
1836
+ header: "Status",
1837
+ meta: {
1838
+ filterComponent: ({ column }) => (
1839
+ <Dropdown<{ name: string }, false, false, false>
1840
+ InputDropdownProps={{ width: "100%" }}
1841
+ label="Status"
1842
+ onChange={(_event, value) =>
1843
+ column.setFilterValue(
1844
+ !value || value.name === "All" ? undefined : value.name
1845
+ )
1846
+ }
1847
+ options={STATUS_OPTIONS}
1848
+ />
1849
+ ),
1850
+ },
1851
+ minSize: 140,
1852
+ },
1853
+ {
1854
+ accessorKey: "salary",
1855
+ cell: ({ as, column, getValue, style }: CellProps) => (
1856
+ <CellBasic
1857
+ as={as}
1858
+ extraRightPadding={column.getCanSort() ? 20 : 0}
1859
+ horizontalAlign="right"
1860
+ primaryText={`${(getValue() as number).toLocaleString()}`}
1861
+ shouldShowTooltipOnHover={false}
1862
+ style={style}
1863
+ tabularNums
1864
+ />
1865
+ ),
1866
+ enableColumnFilter: false,
1867
+ header: "Salary",
1868
+ meta: { headerCellProps: { horizontalAlign: "right" } },
1869
+ minSize: 120,
1870
+ },
1871
+ {
1872
+ accessorKey: "startDate",
1873
+ cell: ({ as, column, getValue, style }: CellProps) => (
1874
+ <CellBasic
1875
+ as={as}
1876
+ extraRightPadding={column.getCanSort() ? 20 : 0}
1877
+ horizontalAlign="right"
1878
+ primaryText={String(getValue())}
1879
+ shouldShowTooltipOnHover={false}
1880
+ style={style}
1881
+ tabularNums
1882
+ />
1883
+ ),
1884
+ enableColumnFilter: false,
1885
+ header: "Start Date",
1886
+ meta: {
1887
+ headerCellProps: { horizontalAlign: "right" },
1888
+ pinning: "right",
1889
+ },
1890
+ minSize: 120,
1891
+ },
1892
+ ];
1893
+
1894
+ function App() {
1895
+ return (
1896
+ <div className="app">
1897
+ <PreComposedTable<Employee>
1898
+ columns={COLUMNS}
1899
+ data={DATA}
1900
+ enableColumnFiltering
1901
+ enableRowSelection
1902
+ enableSorting
1903
+ />
1904
+ </div>
1905
+ );
1906
+ }
1907
+
1908
+ export default App;
1909
+ ```
1910
+
1911
+ ### With custom column filters
1912
+
1913
+ Every filter here is a component of its own: a Dropdown for Status, and a pair of number inputs for Salary and Projects matched with TanStack's `inNumberRange` filter. A filter component is called rather than mounted, so it holds no state of its own: it reads `column.getFilterValue()` and writes `column.setFilterValue()`.
1914
+
1915
+ **Example: PreComposedTableWithCustomColumnFilters**
1916
+
1917
+ ```tsx
1918
+ // The WithCustomColumnFilters story: every filter in this table is a component
1919
+ // of its own, supplied through meta.filterComponent. Status filters through a
1920
+ // Dropdown against a filterFn of equalsString, while Salary and Projects use a
1921
+ // pair of number inputs against inNumberRange, which expects a [min, max] pair.
1922
+ //
1923
+ // A filter component is called as a plain function rather than mounted, so it
1924
+ // holds no state: it reads the current value from column.getFilterValue() and
1925
+ // writes the next one with column.setFilterValue(), which is also what lets
1926
+ // something outside the table (a Clear filters button, say) reset it.
1927
+
1928
+ import {
1929
+ CellBasic,
1930
+ CellComponent,
1931
+ Dropdown,
1932
+ InputText,
1933
+ PreComposedTable,
1934
+ Tag,
1935
+ getSpaces,
1936
+ type CommonThemeProps,
1937
+ } from "@czi-sds/components";
1938
+ import styled from "@emotion/styled";
1939
+ import type { CellContext, Column, ColumnDef } from "@tanstack/react-table";
1940
+ import * as React from "react";
1941
+
1942
+ type Employee = {
1943
+ name: string;
1944
+ projects: number;
1945
+ role: string;
1946
+ salary: number;
1947
+ status: "Active" | "Inactive";
1948
+ };
1949
+
1950
+ const DATA: Employee[] = [
1951
+ {
1952
+ name: "John Doe",
1953
+ projects: 5,
1954
+ role: "Software Engineer",
1955
+ salary: 85000,
1956
+ status: "Active",
1957
+ },
1958
+ {
1959
+ name: "Jane Smith",
1960
+ projects: 3,
1961
+ role: "Product Manager",
1962
+ salary: 95000,
1963
+ status: "Active",
1964
+ },
1965
+ {
1966
+ name: "Bob Johnson",
1967
+ projects: 2,
1968
+ role: "Designer",
1969
+ salary: 75000,
1970
+ status: "Inactive",
1971
+ },
1972
+ {
1973
+ name: "Alice Brown",
1974
+ projects: 7,
1975
+ role: "Data Scientist",
1976
+ salary: 90000,
1977
+ status: "Active",
1978
+ },
1979
+ {
1980
+ name: "Charlie Wilson",
1981
+ projects: 4,
1982
+ role: "DevOps Engineer",
1983
+ salary: 88000,
1984
+ status: "Active",
1985
+ },
1986
+ ];
1987
+
1988
+ const STATUS_OPTIONS = [
1989
+ { name: "All" },
1990
+ { name: "Active" },
1991
+ { name: "Inactive" },
1992
+ ];
1993
+
1994
+ /** The cell context, plus how PreComposedTable wants the cell drawn. */
1995
+ type CellProps = CellContext<Employee, unknown> & {
1996
+ as?: React.ElementType;
1997
+ style?: React.CSSProperties;
1998
+ };
1999
+
2000
+ const RangeInputs = styled.div<CommonThemeProps>`
2001
+ ${(props) => {
2002
+ const spaces = getSpaces(props);
2003
+
2004
+ return `
2005
+ align-items: center;
2006
+ display: flex;
2007
+ gap: ${spaces?.xxs}px;
2008
+ `;
2009
+ }}
2010
+ `;
2011
+
2012
+ function NumberRangeFilter({ column }: { column: Column<Employee, unknown> }) {
2013
+ const [min, max] = (column.getFilterValue() as [number?, number?]) ?? [];
2014
+
2015
+ const setBound = (index: 0 | 1, value: string) =>
2016
+ column.setFilterValue((old: [number?, number?] = []) => {
2017
+ const next: [number?, number?] = [old[0], old[1]];
2018
+ next[index] = value ? Number(value) : undefined;
2019
+ return next;
2020
+ });
2021
+
2022
+ return (
2023
+ <RangeInputs>
2024
+ <InputText
2025
+ hideLabel
2026
+ id={`${column.id}-min`}
2027
+ label="Min"
2028
+ onChange={(event) => setBound(0, event.target.value)}
2029
+ placeholder="Min"
2030
+ sdsType="textField"
2031
+ size="small"
2032
+ type="number"
2033
+ value={min ?? ""}
2034
+ />
2035
+ <span>—</span>
2036
+ <InputText
2037
+ hideLabel
2038
+ id={`${column.id}-max`}
2039
+ label="Max"
2040
+ onChange={(event) => setBound(1, event.target.value)}
2041
+ placeholder="Max"
2042
+ sdsType="textField"
2043
+ size="small"
2044
+ type="number"
2045
+ value={max ?? ""}
2046
+ />
2047
+ </RangeInputs>
2048
+ );
2049
+ }
2050
+
2051
+ function numberCell({ as, column, getValue, style }: CellProps) {
2052
+ return (
2053
+ <CellBasic
2054
+ as={as}
2055
+ extraRightPadding={column.getCanSort() ? 20 : 0}
2056
+ horizontalAlign="right"
2057
+ primaryText={String(getValue())}
2058
+ shouldShowTooltipOnHover={false}
2059
+ style={style}
2060
+ tabularNums
2061
+ />
2062
+ );
2063
+ }
2064
+
2065
+ const COLUMNS: ColumnDef<Employee>[] = [
2066
+ {
2067
+ accessorKey: "name",
2068
+ cell: ({ as, getValue, style }: CellProps) => (
2069
+ <CellBasic
2070
+ as={as}
2071
+ primaryText={getValue() as string}
2072
+ shouldShowTooltipOnHover={false}
2073
+ style={style}
2074
+ />
2075
+ ),
2076
+ header: "Name",
2077
+ meta: { isGrow: true, pinning: "left" },
2078
+ minSize: 150,
2079
+ },
2080
+ {
2081
+ accessorKey: "role",
2082
+ cell: ({ as, getValue, style }: CellProps) => (
2083
+ <CellBasic
2084
+ as={as}
2085
+ primaryText={getValue() as string}
2086
+ shouldShowTooltipOnHover={false}
2087
+ style={style}
2088
+ />
2089
+ ),
2090
+ header: "Role",
2091
+ minSize: 150,
2092
+ },
2093
+ {
2094
+ accessorKey: "status",
2095
+ cell: ({ as, getValue, style }: CellProps) => (
2096
+ <CellComponent as={as} style={style}>
2097
+ <Tag
2098
+ color={getValue() === "Active" ? "positive" : "negative"}
2099
+ hover={false}
2100
+ label={getValue() as string}
2101
+ sdsStyle="rounded"
2102
+ sdsType="secondary"
2103
+ />
2104
+ </CellComponent>
2105
+ ),
2106
+ filterFn: "equalsString",
2107
+ header: "Status",
2108
+ meta: {
2109
+ filterComponent: ({ column }) => (
2110
+ <Dropdown<{ name: string }, false, false, false>
2111
+ InputDropdownProps={{ width: "100%" }}
2112
+ label="Status"
2113
+ onChange={(_event, value) =>
2114
+ column.setFilterValue(
2115
+ !value || value.name === "All" ? undefined : value.name
2116
+ )
2117
+ }
2118
+ options={STATUS_OPTIONS}
2119
+ />
2120
+ ),
2121
+ },
2122
+ minSize: 140,
2123
+ },
2124
+ {
2125
+ accessorKey: "salary",
2126
+ cell: ({ as, column, getValue, style }: CellProps) => (
2127
+ <CellBasic
2128
+ as={as}
2129
+ extraRightPadding={column.getCanSort() ? 20 : 0}
2130
+ horizontalAlign="right"
2131
+ primaryText={`${(getValue() as number).toLocaleString()}`}
2132
+ shouldShowTooltipOnHover={false}
2133
+ style={style}
2134
+ tabularNums
2135
+ />
2136
+ ),
2137
+ filterFn: "inNumberRange",
2138
+ header: "Salary",
2139
+ meta: {
2140
+ filterComponent: NumberRangeFilter,
2141
+ headerCellProps: { horizontalAlign: "right" },
2142
+ },
2143
+ minSize: 180,
2144
+ },
2145
+ {
2146
+ accessorKey: "projects",
2147
+ cell: numberCell,
2148
+ filterFn: "inNumberRange",
2149
+ header: "Projects",
2150
+ meta: {
2151
+ filterComponent: NumberRangeFilter,
2152
+ headerCellProps: { horizontalAlign: "right" },
2153
+ },
2154
+ minSize: 180,
2155
+ },
2156
+ ];
2157
+
2158
+ function App() {
2159
+ return (
2160
+ <div className="app">
2161
+ <PreComposedTable<Employee>
2162
+ columns={COLUMNS}
2163
+ data={DATA}
2164
+ enableColumnFiltering
2165
+ enableRowSelection
2166
+ enableSorting
2167
+ />
2168
+ </div>
2169
+ );
2170
+ }
2171
+
2172
+ export default App;
2173
+ ```
2174
+
2175
+ ### With table actions
2176
+
2177
+ The ref exposes the TanStack table as `ref.current.table`, which covers what the props do not: clearing filters, selecting rows from outside the table, paging programmatically. It is an imperative handle rather than state, so the toolbar calls into it from its click handlers.
2178
+
2179
+ **Example: PreComposedTableWithTableActions**
2180
+
2181
+ ```tsx
2182
+ // The WithTableActions story: the ref exposes the underlying TanStack table as
2183
+ // ref.current.table, which is the escape hatch for everything the props do not
2184
+ // cover: clearing filters, selecting rows from outside the table, paging
2185
+ // programmatically, reading the filtered rows back out.
2186
+ //
2187
+ // It is an imperative handle rather than state, so reading from it during
2188
+ // render tells you nothing useful; call into it from an event handler, as the
2189
+ // toolbar does.
2190
+ //
2191
+ // A column left as a plain accessor is drawn as a CellHeader and a CellBasic.
2192
+ // A renderer of your own is handed the element and the width to draw with as
2193
+ // `as` and `style`, and forwards both. Without `as` it renders a second <td>
2194
+ // inside the one the table already drew. A header renderer is handed
2195
+ // `data-pinned-edge` too, which is what tells a pinned header to draw its
2196
+ // gradient.
2197
+ //
2198
+ // data, columns and paginationConfig sit outside the component so their
2199
+ // identity is stable: a new paginationConfig object sends the table back to its
2200
+ // first page.
2201
+
2202
+ import {
2203
+ Button,
2204
+ CellBasic,
2205
+ CellComponent,
2206
+ CellHeader,
2207
+ Icon,
2208
+ PreComposedTable,
2209
+ Tag,
2210
+ getSpaces,
2211
+ type CommonThemeProps,
2212
+ type PreComposedTableRef,
2213
+ } from "@czi-sds/components";
2214
+ import styled from "@emotion/styled";
2215
+ import type {
2216
+ CellContext,
2217
+ ColumnDef,
2218
+ HeaderContext,
2219
+ } from "@tanstack/react-table";
2220
+ import * as React from "react";
2221
+
2222
+ type Employee = {
2223
+ department: string;
2224
+ email: string;
2225
+ lastActive: string;
2226
+ location: string;
2227
+ name: string;
2228
+ projects: number;
2229
+ role: string;
2230
+ salary: number;
2231
+ startDate: string;
2232
+ status: "Active" | "Inactive";
2233
+ };
2234
+
2235
+ const DATA: Employee[] = [
2236
+ {
2237
+ department: "Engineering",
2238
+ email: "john.doe@example.com",
2239
+ lastActive: "2024-01-15",
2240
+ location: "San Francisco",
2241
+ name: "John Doe",
2242
+ projects: 5,
2243
+ role: "Software Engineer",
2244
+ salary: 85000,
2245
+ startDate: "2022-03-15",
2246
+ status: "Active",
2247
+ },
2248
+ {
2249
+ department: "Product",
2250
+ email: "jane.smith@example.com",
2251
+ lastActive: "2024-01-14",
2252
+ location: "New York",
2253
+ name: "Jane Smith",
2254
+ projects: 3,
2255
+ role: "Product Manager",
2256
+ salary: 95000,
2257
+ startDate: "2021-08-20",
2258
+ status: "Active",
2259
+ },
2260
+ {
2261
+ department: "Design",
2262
+ email: "bob.johnson@example.com",
2263
+ lastActive: "2024-01-10",
2264
+ location: "Austin",
2265
+ name: "Bob Johnson",
2266
+ projects: 2,
2267
+ role: "Designer",
2268
+ salary: 75000,
2269
+ startDate: "2023-01-10",
2270
+ status: "Inactive",
2271
+ },
2272
+ {
2273
+ department: "Data",
2274
+ email: "alice.brown@example.com",
2275
+ lastActive: "2024-01-16",
2276
+ location: "Seattle",
2277
+ name: "Alice Brown",
2278
+ projects: 7,
2279
+ role: "Data Scientist",
2280
+ salary: 90000,
2281
+ startDate: "2022-11-05",
2282
+ status: "Active",
2283
+ },
2284
+ {
2285
+ department: "Engineering",
2286
+ email: "charlie.wilson@example.com",
2287
+ lastActive: "2024-01-15",
2288
+ location: "Remote",
2289
+ name: "Charlie Wilson",
2290
+ projects: 4,
2291
+ role: "DevOps Engineer",
2292
+ salary: 88000,
2293
+ startDate: "2023-06-12",
2294
+ status: "Active",
2295
+ },
2296
+ ];
2297
+
2298
+ /** The cell context, plus how PreComposedTable wants the cell drawn. */
2299
+ type CellProps = CellContext<Employee, unknown> & {
2300
+ as?: React.ElementType;
2301
+ style?: React.CSSProperties;
2302
+ };
2303
+
2304
+ /** The header context, plus which edge a pinned header sits against. */
2305
+ type HeaderProps = HeaderContext<Employee, unknown> & {
2306
+ "data-pinned-edge"?: "left" | "right";
2307
+ style?: React.CSSProperties;
2308
+ };
2309
+
2310
+ function textCell({ as, getValue, style }: CellProps) {
2311
+ return (
2312
+ <CellBasic
2313
+ as={as}
2314
+ primaryText={String(getValue())}
2315
+ shouldShowTooltipOnHover={false}
2316
+ style={style}
2317
+ />
2318
+ );
2319
+ }
2320
+
2321
+ function numberCell({ as, column, getValue, style }: CellProps) {
2322
+ return (
2323
+ <CellBasic
2324
+ as={as}
2325
+ // Room for the sort chevron, which would otherwise sit on the number.
2326
+ extraRightPadding={column.getCanSort() ? 20 : 0}
2327
+ horizontalAlign="right"
2328
+ primaryText={String(getValue())}
2329
+ shouldShowTooltipOnHover={false}
2330
+ style={style}
2331
+ tabularNums
2332
+ />
2333
+ );
2334
+ }
2335
+
2336
+ const COLUMNS: ColumnDef<Employee>[] = [
2337
+ {
2338
+ accessorKey: "name",
2339
+ cell: ({ as, getValue, row, style }: CellProps) => (
2340
+ <CellBasic
2341
+ as={as}
2342
+ link={`mailto:${row.original.email}`}
2343
+ linkComponent="a"
2344
+ linkProps={{ target: "_blank" }}
2345
+ primaryText={getValue() as string}
2346
+ secondaryText={row.original.role}
2347
+ shouldShowTooltipOnHover={false}
2348
+ shouldShowUnderlineOnHover
2349
+ style={style}
2350
+ />
2351
+ ),
2352
+ enableSorting: false,
2353
+ header: ({ "data-pinned-edge": pinnedEdge, style }: HeaderProps) => (
2354
+ <CellHeader
2355
+ data-pinned-edge={pinnedEdge}
2356
+ hideSortIcon
2357
+ hover
2358
+ shouldShowTooltipOnHover
2359
+ style={style}
2360
+ tooltipProps={{ placement: "top", textAlign: "left", width: "wide" }}
2361
+ tooltipSubtitle="Each name links to the person's email address."
2362
+ tooltipText="Name"
2363
+ >
2364
+ Name
2365
+ </CellHeader>
2366
+ ),
2367
+ meta: { isGrow: true, pinning: "left" },
2368
+ minSize: 150,
2369
+ },
2370
+ {
2371
+ accessorKey: "email",
2372
+ cell: ({ as, getValue, style }: CellProps) => (
2373
+ <CellBasic
2374
+ as={as}
2375
+ link={`mailto:${getValue() as string}`}
2376
+ linkProps={{ target: "_blank" }}
2377
+ primaryText={getValue() as string}
2378
+ primaryTextWrapLineCount={1}
2379
+ shouldShowTooltipOnHover={false}
2380
+ shouldShowUnderlineOnHover
2381
+ style={style}
2382
+ />
2383
+ ),
2384
+ header: "Email",
2385
+ meta: { isGrow: true },
2386
+ minSize: 200,
2387
+ },
2388
+ {
2389
+ accessorKey: "role",
2390
+ cell: textCell,
2391
+ header: "Role",
2392
+ meta: { verticalAlign: "middle" },
2393
+ minSize: 150,
2394
+ },
2395
+ {
2396
+ accessorKey: "department",
2397
+ cell: textCell,
2398
+ header: "Department",
2399
+ minSize: 120,
2400
+ },
2401
+ {
2402
+ accessorKey: "status",
2403
+ cell: ({ as, getValue, style }: CellProps) => (
2404
+ <CellComponent as={as} style={style}>
2405
+ <Tag
2406
+ color={getValue() === "Active" ? "positive" : "negative"}
2407
+ hover={false}
2408
+ label={getValue() as string}
2409
+ sdsStyle="rounded"
2410
+ sdsType="secondary"
2411
+ />
2412
+ </CellComponent>
2413
+ ),
2414
+ header: "Status",
2415
+ minSize: 100,
2416
+ },
2417
+ {
2418
+ accessorKey: "lastActive",
2419
+ cell: numberCell,
2420
+ header: "Last Active",
2421
+ meta: { headerCellProps: { horizontalAlign: "right" } },
2422
+ minSize: 120,
2423
+ },
2424
+ {
2425
+ accessorKey: "projects",
2426
+ cell: numberCell,
2427
+ header: "Projects",
2428
+ meta: { headerCellProps: { horizontalAlign: "right" } },
2429
+ minSize: 100,
2430
+ },
2431
+ {
2432
+ accessorKey: "salary",
2433
+ cell: ({ as, column, getValue, style }: CellProps) => (
2434
+ <CellBasic
2435
+ as={as}
2436
+ extraRightPadding={column.getCanSort() ? 20 : 0}
2437
+ horizontalAlign="right"
2438
+ primaryText={`${(getValue() as number).toLocaleString()}`}
2439
+ shouldShowTooltipOnHover={false}
2440
+ style={style}
2441
+ tabularNums
2442
+ />
2443
+ ),
2444
+ header: "Salary",
2445
+ meta: { headerCellProps: { horizontalAlign: "right" } },
2446
+ minSize: 120,
2447
+ },
2448
+ {
2449
+ accessorKey: "location",
2450
+ cell: textCell,
2451
+ header: "Location",
2452
+ minSize: 150,
2453
+ },
2454
+ {
2455
+ accessorKey: "startDate",
2456
+ cell: numberCell,
2457
+ header: "Start Date",
2458
+ meta: { headerCellProps: { horizontalAlign: "right" } },
2459
+ minSize: 120,
2460
+ },
2461
+ {
2462
+ cell: ({ as, row, style }: CellProps) => (
2463
+ <CellComponent as={as} horizontalAlign="right" style={style}>
2464
+ <Button
2465
+ aria-label={`Edit ${row.original.name}`}
2466
+ onClick={() => console.log("Edit", row.original)}
2467
+ sdsStyle="minimal"
2468
+ sdsType="secondary"
2469
+ >
2470
+ <Icon sdsIcon="Edit" sdsSize="s" />
2471
+ </Button>
2472
+ <Button
2473
+ aria-label={`Download ${row.original.name}`}
2474
+ onClick={() => console.log("Download", row.original)}
2475
+ sdsStyle="minimal"
2476
+ sdsType="secondary"
2477
+ >
2478
+ <Icon sdsIcon="Download" sdsSize="s" />
2479
+ </Button>
2480
+ </CellComponent>
2481
+ ),
2482
+ enableSorting: false,
2483
+ header: "Actions",
2484
+ id: "actions",
2485
+ meta: {
2486
+ headerCellProps: { horizontalAlign: "right" },
2487
+ pinning: "right",
2488
+ verticalAlign: "middle",
2489
+ },
2490
+ size: 100,
2491
+ },
2492
+ ];
2493
+
2494
+ const PAGINATION = { pageSize: 5 };
2495
+
2496
+ const Toolbar = styled.div<CommonThemeProps>`
2497
+ ${(props) => {
2498
+ const spaces = getSpaces(props);
2499
+
2500
+ return `
2501
+ display: flex;
2502
+ flex-wrap: wrap;
2503
+ gap: ${spaces?.xs}px;
2504
+ margin-bottom: ${spaces?.l}px;
2505
+ `;
2506
+ }}
2507
+ `;
2508
+
2509
+ function App() {
2510
+ const tableRef = React.useRef<PreComposedTableRef<Employee>>(null);
2511
+
2512
+ return (
2513
+ <div className="app">
2514
+ <Toolbar>
2515
+ <Button
2516
+ onClick={() => tableRef.current?.table.resetColumnFilters()}
2517
+ sdsStyle="outline"
2518
+ sdsType="primary"
2519
+ >
2520
+ Clear all filters
2521
+ </Button>
2522
+ <Button
2523
+ onClick={() => tableRef.current?.table.resetSorting()}
2524
+ sdsStyle="outline"
2525
+ sdsType="primary"
2526
+ >
2527
+ Reset sorting
2528
+ </Button>
2529
+ <Button
2530
+ onClick={() => tableRef.current?.table.toggleAllRowsSelected(true)}
2531
+ sdsStyle="outline"
2532
+ sdsType="primary"
2533
+ >
2534
+ Select all rows
2535
+ </Button>
2536
+ <Button
2537
+ onClick={() => tableRef.current?.table.toggleAllRowsSelected(false)}
2538
+ sdsStyle="outline"
2539
+ sdsType="primary"
2540
+ >
2541
+ Deselect all rows
2542
+ </Button>
2543
+ <Button
2544
+ onClick={() => {
2545
+ tableRef.current?.table.resetColumnFilters();
2546
+ tableRef.current?.table.resetSorting();
2547
+ tableRef.current?.table.toggleAllRowsSelected(false);
2548
+ }}
2549
+ sdsStyle="outline"
2550
+ sdsType="secondary"
2551
+ >
2552
+ Reset everything
2553
+ </Button>
2554
+ </Toolbar>
2555
+
2556
+ <PreComposedTable<Employee>
2557
+ columns={COLUMNS}
2558
+ data={DATA}
2559
+ enableColumnFiltering
2560
+ enablePagination
2561
+ enableRowSelection
2562
+ enableSorting
2563
+ paginationConfig={PAGINATION}
2564
+ ref={tableRef}
2565
+ />
2566
+ </div>
2567
+ );
2568
+ }
2569
+
2570
+ export default App;
2571
+ ```
2572
+
2573
+ ### With table options
2574
+
2575
+ `tableOptions` is merged into the underlying TanStack table's options, so anything `useReactTable` accepts can be set through it. This one passes `autoResetPageIndex: false`, so filtering leaves you on the page you were reading instead of returning you to the first.
2576
+
2577
+ **Example: PreComposedTableWithTableOptions**
2578
+
2579
+ ```tsx
2580
+ // The WithTableOptions story: tableOptions is merged into the underlying
2581
+ // TanStack table's options, so anything useReactTable accepts can be set here:
2582
+ // meta, filter functions, debugTable, and the rest.
2583
+ //
2584
+ // This one sets autoResetPageIndex: false, so filtering the table leaves you on
2585
+ // the page you were reading instead of sending you back to the first one.
2586
+ //
2587
+ // A column left as a plain accessor is drawn as a CellHeader and a CellBasic.
2588
+ // A renderer of your own is handed the element and the width to draw with as
2589
+ // `as` and `style`, and forwards both. Without `as` it renders a second <td>
2590
+ // inside the one the table already drew. A header renderer is handed
2591
+ // `data-pinned-edge` too, which is what tells a pinned header to draw its
2592
+ // gradient.
2593
+ //
2594
+ // data, columns and paginationConfig sit outside the component so their
2595
+ // identity is stable: a new paginationConfig object sends the table back to its
2596
+ // first page.
2597
+
2598
+ import {
2599
+ Button,
2600
+ CellBasic,
2601
+ CellComponent,
2602
+ CellHeader,
2603
+ Icon,
2604
+ PreComposedTable,
2605
+ Tag,
2606
+ } from "@czi-sds/components";
2607
+ import type {
2608
+ CellContext,
2609
+ ColumnDef,
2610
+ HeaderContext,
2611
+ } from "@tanstack/react-table";
2612
+ import * as React from "react";
2613
+
2614
+ type Employee = {
2615
+ department: string;
2616
+ email: string;
2617
+ lastActive: string;
2618
+ location: string;
2619
+ name: string;
2620
+ projects: number;
2621
+ role: string;
2622
+ salary: number;
2623
+ startDate: string;
2624
+ status: "Active" | "Inactive";
2625
+ };
2626
+
2627
+ const DATA: Employee[] = [
2628
+ {
2629
+ department: "Engineering",
2630
+ email: "john.doe@example.com",
2631
+ lastActive: "2024-01-15",
2632
+ location: "San Francisco",
2633
+ name: "John Doe",
2634
+ projects: 5,
2635
+ role: "Software Engineer",
2636
+ salary: 85000,
2637
+ startDate: "2022-03-15",
2638
+ status: "Active",
2639
+ },
2640
+ {
2641
+ department: "Product",
2642
+ email: "jane.smith@example.com",
2643
+ lastActive: "2024-01-14",
2644
+ location: "New York",
2645
+ name: "Jane Smith",
2646
+ projects: 3,
2647
+ role: "Product Manager",
2648
+ salary: 95000,
2649
+ startDate: "2021-08-20",
2650
+ status: "Active",
2651
+ },
2652
+ {
2653
+ department: "Design",
2654
+ email: "bob.johnson@example.com",
2655
+ lastActive: "2024-01-10",
2656
+ location: "Austin",
2657
+ name: "Bob Johnson",
2658
+ projects: 2,
2659
+ role: "Designer",
2660
+ salary: 75000,
2661
+ startDate: "2023-01-10",
2662
+ status: "Inactive",
2663
+ },
2664
+ {
2665
+ department: "Data",
2666
+ email: "alice.brown@example.com",
2667
+ lastActive: "2024-01-16",
2668
+ location: "Seattle",
2669
+ name: "Alice Brown",
2670
+ projects: 7,
2671
+ role: "Data Scientist",
2672
+ salary: 90000,
2673
+ startDate: "2022-11-05",
2674
+ status: "Active",
2675
+ },
2676
+ {
2677
+ department: "Engineering",
2678
+ email: "charlie.wilson@example.com",
2679
+ lastActive: "2024-01-15",
2680
+ location: "Remote",
2681
+ name: "Charlie Wilson",
2682
+ projects: 4,
2683
+ role: "DevOps Engineer",
2684
+ salary: 88000,
2685
+ startDate: "2023-06-12",
2686
+ status: "Active",
2687
+ },
2688
+ ];
2689
+
2690
+ /** The cell context, plus how PreComposedTable wants the cell drawn. */
2691
+ type CellProps = CellContext<Employee, unknown> & {
2692
+ as?: React.ElementType;
2693
+ style?: React.CSSProperties;
2694
+ };
2695
+
2696
+ /** The header context, plus which edge a pinned header sits against. */
2697
+ type HeaderProps = HeaderContext<Employee, unknown> & {
2698
+ "data-pinned-edge"?: "left" | "right";
2699
+ style?: React.CSSProperties;
2700
+ };
2701
+
2702
+ function textCell({ as, getValue, style }: CellProps) {
2703
+ return (
2704
+ <CellBasic
2705
+ as={as}
2706
+ primaryText={String(getValue())}
2707
+ shouldShowTooltipOnHover={false}
2708
+ style={style}
2709
+ />
2710
+ );
2711
+ }
2712
+
2713
+ function numberCell({ as, column, getValue, style }: CellProps) {
2714
+ return (
2715
+ <CellBasic
2716
+ as={as}
2717
+ // Room for the sort chevron, which would otherwise sit on the number.
2718
+ extraRightPadding={column.getCanSort() ? 20 : 0}
2719
+ horizontalAlign="right"
2720
+ primaryText={String(getValue())}
2721
+ shouldShowTooltipOnHover={false}
2722
+ style={style}
2723
+ tabularNums
2724
+ />
2725
+ );
2726
+ }
2727
+
2728
+ const COLUMNS: ColumnDef<Employee>[] = [
2729
+ {
2730
+ accessorKey: "name",
2731
+ cell: ({ as, getValue, row, style }: CellProps) => (
2732
+ <CellBasic
2733
+ as={as}
2734
+ link={`mailto:${row.original.email}`}
2735
+ linkComponent="a"
2736
+ linkProps={{ target: "_blank" }}
2737
+ primaryText={getValue() as string}
2738
+ secondaryText={row.original.role}
2739
+ shouldShowTooltipOnHover={false}
2740
+ shouldShowUnderlineOnHover
2741
+ style={style}
2742
+ />
2743
+ ),
2744
+ enableSorting: false,
2745
+ header: ({ "data-pinned-edge": pinnedEdge, style }: HeaderProps) => (
2746
+ <CellHeader
2747
+ data-pinned-edge={pinnedEdge}
2748
+ hideSortIcon
2749
+ hover
2750
+ shouldShowTooltipOnHover
2751
+ style={style}
2752
+ tooltipProps={{ placement: "top", textAlign: "left", width: "wide" }}
2753
+ tooltipSubtitle="Each name links to the person's email address."
2754
+ tooltipText="Name"
2755
+ >
2756
+ Name
2757
+ </CellHeader>
2758
+ ),
2759
+ meta: { isGrow: true, pinning: "left" },
2760
+ minSize: 150,
2761
+ },
2762
+ {
2763
+ accessorKey: "email",
2764
+ cell: ({ as, getValue, style }: CellProps) => (
2765
+ <CellBasic
2766
+ as={as}
2767
+ link={`mailto:${getValue() as string}`}
2768
+ linkProps={{ target: "_blank" }}
2769
+ primaryText={getValue() as string}
2770
+ primaryTextWrapLineCount={1}
2771
+ shouldShowTooltipOnHover={false}
2772
+ shouldShowUnderlineOnHover
2773
+ style={style}
2774
+ />
2775
+ ),
2776
+ header: "Email",
2777
+ meta: { isGrow: true },
2778
+ minSize: 200,
2779
+ },
2780
+ {
2781
+ accessorKey: "role",
2782
+ cell: textCell,
2783
+ header: "Role",
2784
+ meta: { verticalAlign: "middle" },
2785
+ minSize: 150,
2786
+ },
2787
+ {
2788
+ accessorKey: "department",
2789
+ cell: textCell,
2790
+ header: "Department",
2791
+ minSize: 120,
2792
+ },
2793
+ {
2794
+ accessorKey: "status",
2795
+ cell: ({ as, getValue, style }: CellProps) => (
2796
+ <CellComponent as={as} style={style}>
2797
+ <Tag
2798
+ color={getValue() === "Active" ? "positive" : "negative"}
2799
+ hover={false}
2800
+ label={getValue() as string}
2801
+ sdsStyle="rounded"
2802
+ sdsType="secondary"
2803
+ />
2804
+ </CellComponent>
2805
+ ),
2806
+ header: "Status",
2807
+ minSize: 100,
2808
+ },
2809
+ {
2810
+ accessorKey: "lastActive",
2811
+ cell: numberCell,
2812
+ header: "Last Active",
2813
+ meta: { headerCellProps: { horizontalAlign: "right" } },
2814
+ minSize: 120,
2815
+ },
2816
+ {
2817
+ accessorKey: "projects",
2818
+ cell: numberCell,
2819
+ header: "Projects",
2820
+ meta: { headerCellProps: { horizontalAlign: "right" } },
2821
+ minSize: 100,
2822
+ },
2823
+ {
2824
+ accessorKey: "salary",
2825
+ cell: ({ as, column, getValue, style }: CellProps) => (
2826
+ <CellBasic
2827
+ as={as}
2828
+ extraRightPadding={column.getCanSort() ? 20 : 0}
2829
+ horizontalAlign="right"
2830
+ primaryText={`${(getValue() as number).toLocaleString()}`}
2831
+ shouldShowTooltipOnHover={false}
2832
+ style={style}
2833
+ tabularNums
2834
+ />
2835
+ ),
2836
+ header: "Salary",
2837
+ meta: { headerCellProps: { horizontalAlign: "right" } },
2838
+ minSize: 120,
2839
+ },
2840
+ {
2841
+ accessorKey: "location",
2842
+ cell: textCell,
2843
+ header: "Location",
2844
+ minSize: 150,
2845
+ },
2846
+ {
2847
+ accessorKey: "startDate",
2848
+ cell: numberCell,
2849
+ header: "Start Date",
2850
+ meta: { headerCellProps: { horizontalAlign: "right" } },
2851
+ minSize: 120,
2852
+ },
2853
+ {
2854
+ cell: ({ as, row, style }: CellProps) => (
2855
+ <CellComponent as={as} horizontalAlign="right" style={style}>
2856
+ <Button
2857
+ aria-label={`Edit ${row.original.name}`}
2858
+ onClick={() => console.log("Edit", row.original)}
2859
+ sdsStyle="minimal"
2860
+ sdsType="secondary"
2861
+ >
2862
+ <Icon sdsIcon="Edit" sdsSize="s" />
2863
+ </Button>
2864
+ <Button
2865
+ aria-label={`Download ${row.original.name}`}
2866
+ onClick={() => console.log("Download", row.original)}
2867
+ sdsStyle="minimal"
2868
+ sdsType="secondary"
2869
+ >
2870
+ <Icon sdsIcon="Download" sdsSize="s" />
2871
+ </Button>
2872
+ </CellComponent>
2873
+ ),
2874
+ enableSorting: false,
2875
+ header: "Actions",
2876
+ id: "actions",
2877
+ meta: {
2878
+ headerCellProps: { horizontalAlign: "right" },
2879
+ pinning: "right",
2880
+ verticalAlign: "middle",
2881
+ },
2882
+ size: 100,
2883
+ },
2884
+ ];
2885
+
2886
+ const PAGINATION = { pageSize: 3 };
2887
+
2888
+ const TABLE_OPTIONS = { autoResetPageIndex: false };
2889
+
2890
+ function App() {
2891
+ return (
2892
+ <div className="app">
2893
+ <PreComposedTable<Employee>
2894
+ columns={COLUMNS}
2895
+ data={DATA}
2896
+ enableColumnFiltering
2897
+ enablePagination
2898
+ enableRowSelection
2899
+ enableSorting
2900
+ paginationConfig={PAGINATION}
2901
+ tableOptions={TABLE_OPTIONS}
2902
+ />
2903
+ </div>
2904
+ );
2905
+ }
2906
+
2907
+ export default App;
2908
+ ```
2909
+
2910
+ ## Props
2911
+
2912
+ PreComposedTable is generic over your row type, so `data` and `columns` are typed together. It owns the table state (sorting, selection, filtering, pagination, pinning), and every feature is off unless you switch it on.
2913
+
2914
+ | Name | Type | Default | Description |
2915
+ | -------------------------- | ------------------------------------------------------------------------------ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2916
+ | `data` | `TData[]` | - | Array of data objects to display in the table rows |
2917
+ | `columns` | `ColumnDef<TData>[]` | - | Column definitions that specify how data should be rendered and behave |
2918
+ | `border` | `boolean` | `true` | Draws the border around the scrolling table area |
2919
+ | `sdsStyle` | `"lined"` \| `"striped"` | `"lined"` | Whether rows are separated by dividers or by alternating backgrounds |
2920
+ | `enableSorting` | `boolean` | `false` | Makes column headers sortable, wiring each one's chevron and click handler for you |
2921
+ | `enableRowSelection` | `boolean` | `false` | Adds a checkbox column with select-all. To control that column yourself, include one with the `id` `"SdsTableSelectColumn"` and it will be used instead of the generated one |
2922
+ | `enableColumnFiltering` | `boolean` | `false` | Adds a filter row under the header. Each column gets a search input unless its `meta` supplies a `filterComponent` |
2923
+ | `shouldPinSelectRowToLeft` | `boolean` | `true` | Pins the row selection column to the left/right side of the table |
2924
+ | `enablePagination` | `boolean` | `false` | Enables pagination controls at the bottom of the table |
2925
+ | `enableGlobalFiltering` | `boolean` | `false` | Displays a search input that filters across all columns |
2926
+ | `paginationConfig` | `Partial<PaginationProps> & { placement?: "left" \| "center" \| "right" }` | - | Passed through to Pagination, plus a placement for the controls, which sit on the right by default. `pageSize` also sets the initial page size, which is `10` otherwise. Changing this object resets the table to the first page |
2927
+ | `className` | `string` | - | CSS class name to apply to the table container |
2928
+ | `style` | `React.CSSProperties` | - | Inline styles to apply to the table container |
2929
+ | `tableWidth` | `string` | `100%` | Sets the width of the table container |
2930
+ | `tableRowProps` | `Partial<TableRowProps>` | - | Additional props to pass to each table row component |
2931
+ | `onRowSelect` | `(selectedRows: TData[]) => void` | - | Callback triggered when row selection changes; receives array of selected row data |
2932
+ | `tableOptions` | `Partial<Omit<TableOptions<TData>, "data" \| "columns" \| "getCoreRowModel">>` | - | Escape hatch to the underlying TanStack table. Anything here is merged into its options, and a state you supply overrides the one the component keeps, which is how you drive sorting or pagination from outside. The row models and change handlers are always the component's own |
2933
+
2934
+ The component also takes a ref, which exposes the TanStack table instance as `ref.current.table` for anything the props do not cover.
2935
+
2936
+ ## Column meta
2937
+
2938
+ Layout and pinning are set per column through the standard TanStack column definition's `meta` field, which SDS extends with the following. Everything else in a column definition (`accessorKey`, `header`, `cell`, `enableSorting`, `size`, `minSize`, `maxSize`, `filterFn`) behaves as TanStack documents it, and a header or cell given as a plain string is rendered as a CellHeader or CellBasic for you.
2939
+
2940
+ | Name | Type | Default | Description |
2941
+ | ----------------- | ---------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- |
2942
+ | `pinning` | `"left"` \| `"right"` | - | Sticks the column to that edge while the table scrolls sideways, with a gradient at the edge once there is something hidden behind it |
2943
+ | `verticalAlign` | `"top"` \| `"middle"` \| `"bottom"` | - | Vertical alignment of the column's cells |
2944
+ | `widthPercentage` | `number` | - | The column's share of the table width, as a percentage rather than a fixed size |
2945
+ | `isGrow` | `boolean` | - | Lets this column absorb whatever width the others leave over |
2946
+ | `headerCellProps` | `Partial<CellHeaderProps>` | - | Props for the CellHeader generated from a string header, for tooltips, alignment or truncation |
2947
+ | `filterComponent` | `(props: { column: Column<TData>; table: Table<TData> }) => React.ReactNode` | - | Replaces the default search input in this column's filter cell, for a select or a range instead |