compote-ui 0.84.0 → 0.85.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.
@@ -1,400 +1,400 @@
1
- ---
2
- name: data-table
3
- description: >
4
- Load when using compote-ui/data-table, compote-ui/data-table/virtual or
5
- compote-ui/data-table/xlsx. Covers TanStack Table v9 wrapper usage, createTable,
6
- createDataTableColumnHelper, table columns, row selection, filters,
7
- formatting, toolbars, CSV/Excel export, virtualized imports, and optional
8
- peer dependencies.
9
- metadata:
10
- type: composition
11
- library: compote-ui
12
- library_version: '0.84.0'
13
- requires:
14
- - component-usage
15
- - theming
16
- sources:
17
- - package.json
18
- - src/lib/components/data-table-v9/index.ts
19
- - src/lib/components/data-table-v9/types.ts
20
- - src/lib/components/data-table-v9/features.ts
21
- - src/lib/components/data-table-v9/column-helper.ts
22
- - src/lib/components/data-table-v9/create-table.svelte.ts
23
- - src/lib/components/data-table-v9/data-table.svelte
24
- - src/lib/components/data-table-v9/data-table-head.svelte
25
- - src/lib/components/data-table-v9/header/column-header-filter.svelte
26
- - src/lib/components/data-table-v9/column-filter-kind.ts
27
- - src/lib/components/data-table-v9/filter-editors/column-filter-editor.svelte
28
- - src/lib/components/data-table-v9/column-filter-editing.svelte.ts
29
- - src/lib/utils/select-filter.ts
30
- - src/lib/utils/date-filter.ts
31
- - src/lib/utils/csv.ts
32
- - src/lib/components/data-table-v9/export/export-data.ts
33
- - src/lib/components/data-table-v9/export/csv-exporter.ts
34
- - src/lib/components/data-table-v9/toolbar/data-table-export.svelte
35
- - src/lib/components/data-table-v9/xlsx/index.ts
36
- - src/lib/components/data-table-v9/xlsx/xlsx-cell.ts
37
- - src/lib/components/data-table-v9/virtual/data-table-virtualized.svelte
38
- - src/lib/components/data-table-v9/virtual/data-table-virtual-rows.svelte
39
- ---
40
-
41
- This skill builds on component-usage and theming. Read those first for Compote namespace imports and theme token behavior.
42
-
43
- # Compote UI — Data Table
44
-
45
- Use `compote-ui/data-table` for regular tables and `compote-ui/data-table/virtual` for virtualized rows.
46
-
47
- ## Integration Setup
48
-
49
- ```bash
50
- bun add @tanstack/svelte-table
51
- ```
52
-
53
- ```svelte
54
- <script lang="ts">
55
- import * as DataTable from 'compote-ui/data-table';
56
-
57
- type Invoice = {
58
- id: string;
59
- customer: string;
60
- total: number;
61
- paid: boolean;
62
- };
63
-
64
- let invoices = $state<Invoice[]>([
65
- { id: 'inv_1', customer: 'Ada', total: 1200, paid: true },
66
- { id: 'inv_2', customer: 'Grace', total: 800, paid: false }
67
- ]);
68
-
69
- const col = DataTable.createDataTableColumnHelper<Invoice>();
70
- const columns = col.columns([
71
- col.accessor('id', { header: 'ID', enableHiding: false }),
72
- col.accessor('customer', { header: 'Customer', grow: true }),
73
- col.accessor('total', { header: 'Total', type: 'currency', align: 'right' }),
74
- col.accessor('paid', { header: 'Paid', type: 'boolean', align: 'center' })
75
- ]);
76
-
77
- const table = DataTable.createTable({
78
- get data() {
79
- return invoices;
80
- },
81
- columns,
82
- getRowId: (row) => row.id,
83
- enableRowSelection: true
84
- });
85
- </script>
86
-
87
- <DataTable.Toolbar>
88
- <DataTable.Title>Invoices</DataTable.Title>
89
- {#snippet center()}
90
- <DataTable.Search {table} class="w-56" />
91
- {/snippet}
92
- {#snippet right()}
93
- <DataTable.ColumnFilter {table} />
94
- <DataTable.ColumnVisibility {table} />
95
- {/snippet}
96
- </DataTable.Toolbar>
97
-
98
- <div class="h-96 min-h-0">
99
- <DataTable.Root {table} caption="Invoices" />
100
- </div>
101
- ```
102
-
103
- ## Core Integration Patterns
104
-
105
- ### Use accessorFn with an explicit id
106
-
107
- ```ts
108
- const columns = col.columns([
109
- col.accessorFn((row) => row.quantity * row.price, {
110
- id: 'total',
111
- header: 'Total',
112
- type: 'currency',
113
- align: 'right'
114
- })
115
- ]);
116
- ```
117
-
118
- ### Make runtime column changes reactive
119
-
120
- ```ts
121
- const idCol = col.accessor('id', { header: 'ID', enableHiding: false });
122
- const totalCol = col.accessor('total', { header: 'Total', type: 'currency' });
123
-
124
- let columns = $state.raw([idCol]);
125
-
126
- const table = DataTable.createTable({
127
- get data() {
128
- return invoices;
129
- },
130
- get columns() {
131
- return columns;
132
- },
133
- getRowId: (row) => row.id
134
- });
135
-
136
- function toggleTotal() {
137
- columns = columns.includes(totalCol)
138
- ? columns.filter((column) => column !== totalCol)
139
- : [...columns, totalCol];
140
- }
141
- ```
142
-
143
- ### Use virtual import for large row sets
144
-
145
- ```bash
146
- bun add @tanstack/svelte-virtual
147
- ```
148
-
149
- ```svelte
150
- <script lang="ts">
151
- import * as VirtualDataTable from 'compote-ui/data-table/virtual';
152
-
153
- const table = VirtualDataTable.createTable({
154
- get data() {
155
- return rows;
156
- },
157
- columns,
158
- getRowId: (row) => row.id,
159
- enableRowSelection: true
160
- });
161
- </script>
162
-
163
- <div class="h-96 min-h-0">
164
- <VirtualDataTable.Root {table} caption="Large dataset" />
165
- </div>
166
- ```
167
-
168
- ### Label toolbar triggers
169
-
170
- `ColumnFilter` and `ColumnVisibility` render icon-only outline buttons by default (funnel and
171
- columns icons, with `aria-label` and a hover tooltip). Pass `triggerLabel` to show text instead. `ColumnFilter`
172
- shows the active filter count as a badge in the corner either way.
173
-
174
- ```svelte
175
- <DataTable.ColumnFilter {table} triggerLabel="Filters" />
176
- <DataTable.ColumnVisibility {table} triggerLabel="Columns" />
177
- ```
178
-
179
- ### Filter from column headers
180
-
181
- Pass `headerFilters` to `DataTable.Root` / `VirtualDataTable.Root` to put a funnel in the header
182
- of every filterable leaf column (not group headers, not `enableColumnFilter: false`, never
183
- `type: 'action'`). The funnel shows on header hover/focus (always on touch screens); once the
184
- column is filtered it stays visible, filled and in the primary color. Clicking it opens a popup
185
- titled with the column name, with a "Clear" button and the editor for the column's type:
186
-
187
- | Column `type` | Popup editor | Filter value |
188
- | ----------------------------------- | -------------------------------------- | -------------------------------- |
189
- | `number`, `currency`, `percent` | From / To number inputs | `[min?, max?]` (`inNumberRange`) |
190
- | `boolean` | All / Yes / No | `true` / `false` |
191
- | `select` | Search + checkbox list with row counts | array of options |
192
- | `date`, `date-time` | From / To date pickers | `[from?, to?]` as `YYYY-MM-DD` |
193
- | anything else (`text`, `time`, ...) | Text search | string |
194
-
195
- Edits apply immediately (text and number inputs after a 300ms debounce — also when the popup
196
- closes first). The header popups and `DataTable.ColumnFilter` edit the same table state, so a
197
- filter set in one shows in the other. `headerFilters` is opt-in and defaults to `false`.
198
-
199
- ```svelte
200
- <div class="h-96 min-h-0">
201
- <DataTable.Root {table} caption="Invoices" headerFilters />
202
- </div>
203
- ```
204
-
205
- ### Build your own filter UI
206
-
207
- For filter controls outside the toolbar and header popups (a facet panel beside the table, a
208
- filter sidebar), use the same building blocks the Funnel uses. They read and write the table's
209
- column filters, so the Funnel, header funnels and your UI always show the same state.
210
-
211
- - `ColumnFilterEditor` — the Funnel's editor for one column, picked by its type (see the table in
212
- "Filter from column headers"). Pass `editing` only if the editor can be torn down while it holds
213
- a pending text/range edit (see below); without it the editor uses one bound to the column's table.
214
- - `ColumnFilterEditing` — the debounced text/range edits pending on a table, shared by every host.
215
- `isActive(columnId)` lets a host refuse writes from an editor it has already dropped (Ark's
216
- NumberInput commits on the blur its unmount triggers).
217
- - `getColumnFilterKind(column)` / `filterKindFor(type)` — the column's `ColumnFilterKind`
218
- (`range`, `boolean`, `select`, `date` or `text`), or `null` for columns that never filter.
219
- - `selectFilterOptions(column.getFacetedUniqueValues())` — a select column's options with row
220
- counts, sorted by label, "(empty)" last with value `EMPTY_SELECT_VALUE` (`null`). Counts are
221
- cross-filtered: every other active filter applies, the column's own does not.
222
- - `getColumnLabel(column)` and the `DataTableColumnInstance<T>` type for the columns you list.
223
-
224
- ```svelte
225
- <script lang="ts">
226
- import * as DataTable from 'compote-ui/data-table';
227
-
228
- let { table }: { table: DataTable.DataTableInstance<Product> } = $props();
229
-
230
- const facets = $derived(
231
- table
232
- .getAllLeafColumns()
233
- .filter((column) => column.getCanFilter() && DataTable.getColumnFilterKind(column))
234
- );
235
- </script>
236
-
237
- {#each facets as column (column.id)}
238
- <h3>{DataTable.getColumnLabel(column)}</h3>
239
- <DataTable.ColumnFilterEditor {column} />
240
- {/each}
241
- ```
242
-
243
- A custom select list writes the same value the Funnel does — an array of options, `undefined`
244
- when none is ticked:
245
-
246
- ```ts
247
- const selected = (column.getFilterValue() as DataTable.SelectFilterValue[] | undefined) ?? [];
248
- column.setFilterValue(next.length ? next : undefined);
249
- ```
250
-
251
- ### Add a refresh button
252
-
253
- `DataTable.Refresh` is data-source agnostic: it calls `onRefresh` and, if that returns a promise,
254
- spins its icon and sets `aria-busy` until the promise settles. Clicks while busy are ignored. Pass
255
- `loading` to force the busy state when a refresh starts elsewhere (polling, another control).
256
-
257
- ```svelte
258
- <script lang="ts">
259
- import { getInvoices } from './invoices.remote';
260
-
261
- const invoices = getInvoices();
262
- </script>
263
-
264
- <DataTable.Toolbar>
265
- {#snippet right()}
266
- <DataTable.Refresh onRefresh={() => invoices.refresh()} />
267
- <DataTable.ColumnFilter {table} />
268
- <DataTable.ColumnVisibility {table} />
269
- {/snippet}
270
- </DataTable.Toolbar>
271
- ```
272
-
273
- Props: `onRefresh: () => void | Promise<unknown>`, `loading?: boolean`, `triggerLabel?: string`
274
- (icon-only with `aria-label="Refresh"` and a tooltip when omitted). Errors thrown by `onRefresh` are not caught.
275
-
276
- ### Export to a file
277
-
278
- `DataTable.Export` downloads the visible columns and the filtered, sorted rows. It writes CSV by
279
- default; pass `exporters` for more formats or your own (an exporter on the server, or a library
280
- the app already has). With selected rows it offers all rows or only the selected ones. Excel
281
- comes from `compote-ui/data-table/xlsx` and needs the optional peer `write-excel-file`.
282
-
283
- ```svelte
284
- <script lang="ts">
285
- import * as DataTable from 'compote-ui/data-table';
286
- import { xlsxExporter } from 'compote-ui/data-table/xlsx';
287
- </script>
288
-
289
- <DataTable.Export {table} filename="invoices" exporters={[DataTable.csvExporter, xlsxExporter]} />
290
- ```
291
-
292
- Exclude a column with `enableExport: false` (action columns are excluded by default). Use
293
- `exportValue` when a cell holds a value a file can't show. See [Export](references/export.md).
294
-
295
- ## Common Mistakes
296
-
297
- ### CRITICAL Missing id for accessorFn column
298
-
299
- Wrong:
300
-
301
- ```ts
302
- col.accessorFn((row) => row.a + row.b, { header: 'Total' });
303
- ```
304
-
305
- Correct:
306
-
307
- ```ts
308
- col.accessorFn((row) => row.a + row.b, { id: 'total', header: 'Total' });
309
- ```
310
-
311
- Accessor function columns cannot derive a stable column id from a key.
312
-
313
- Source: `src/lib/components/data-table-v9/create-table.svelte.ts`
314
-
315
- ### HIGH Non-reactive reassigned columns
316
-
317
- Wrong:
318
-
319
- ```ts
320
- let columns = $state([idCol, nameCol]);
321
- const table = DataTable.createTable({ data: rows, columns });
322
- ```
323
-
324
- Correct:
325
-
326
- ```ts
327
- let columns = $state.raw([idCol, nameCol]);
328
- const table = DataTable.createTable({
329
- get data() {
330
- return rows;
331
- },
332
- get columns() {
333
- return columns;
334
- }
335
- });
336
- ```
337
-
338
- Reassigned columns must be passed through a getter and kept raw so identity checks keep working.
339
-
340
- Source: `src/routes/components/data-table-v9/+page.svelte`, `src/lib/components/data-table-v9/create-table.svelte.ts`
341
-
342
- ### HIGH Rendering table without height constraint
343
-
344
- Wrong:
345
-
346
- ```svelte
347
- <DataTable.Root {table} />
348
- ```
349
-
350
- Correct:
351
-
352
- ```svelte
353
- <div class="h-96 min-h-0">
354
- <DataTable.Root {table} caption="Invoices" />
355
- </div>
356
- ```
357
-
358
- `DataTable.Root` fills its container and uses internal scrolling.
359
-
360
- Source: `src/lib/components/data-table-v9/data-table.svelte`
361
-
362
- ### HIGH Importing virtual table without optional peer
363
-
364
- Wrong:
365
-
366
- ```ts
367
- import * as VirtualDataTable from 'compote-ui/data-table/virtual';
368
- ```
369
-
370
- Correct:
371
-
372
- ```bash
373
- bun add @tanstack/svelte-virtual
374
- ```
375
-
376
- ```ts
377
- import * as VirtualDataTable from 'compote-ui/data-table/virtual';
378
- ```
379
-
380
- The virtual subpath imports `@tanstack/svelte-virtual`.
381
-
382
- Source: `package.json`, `src/lib/components/data-table-v9/virtual/data-table-virtual-rows.svelte`
383
-
384
- ### HIGH Using the Excel exporter without its optional peer
385
-
386
- `compote-ui/data-table/xlsx` imports `write-excel-file` (dynamically, on export). Without it the
387
- app's build fails to resolve the import. Install it first:
388
-
389
- ```bash
390
- bun add write-excel-file
391
- ```
392
-
393
- Source: `package.json`, `src/lib/components/data-table-v9/xlsx/index.ts`
394
-
395
- ## References
396
-
397
- - [Column options](references/column-options.md)
398
- - [Table state and reactivity](references/table-state-and-reactivity.md)
399
- - [Virtual table](references/virtual-table.md)
400
- - [Export](references/export.md)
1
+ ---
2
+ name: data-table
3
+ description: >
4
+ Load when using compote-ui/data-table, compote-ui/data-table/virtual or
5
+ compote-ui/data-table/xlsx. Covers TanStack Table v9 wrapper usage, createTable,
6
+ createDataTableColumnHelper, table columns, row selection, filters,
7
+ formatting, toolbars, CSV/Excel export, virtualized imports, and optional
8
+ peer dependencies.
9
+ metadata:
10
+ type: composition
11
+ library: compote-ui
12
+ library_version: '0.84.0'
13
+ requires:
14
+ - component-usage
15
+ - theming
16
+ sources:
17
+ - package.json
18
+ - src/lib/components/data-table-v9/index.ts
19
+ - src/lib/components/data-table-v9/types.ts
20
+ - src/lib/components/data-table-v9/features.ts
21
+ - src/lib/components/data-table-v9/column-helper.ts
22
+ - src/lib/components/data-table-v9/create-table.svelte.ts
23
+ - src/lib/components/data-table-v9/data-table.svelte
24
+ - src/lib/components/data-table-v9/data-table-head.svelte
25
+ - src/lib/components/data-table-v9/header/column-header-filter.svelte
26
+ - src/lib/components/data-table-v9/column-filter-kind.ts
27
+ - src/lib/components/data-table-v9/filter-editors/column-filter-editor.svelte
28
+ - src/lib/components/data-table-v9/column-filter-editing.svelte.ts
29
+ - src/lib/utils/select-filter.ts
30
+ - src/lib/utils/date-filter.ts
31
+ - src/lib/utils/csv.ts
32
+ - src/lib/components/data-table-v9/export/export-data.ts
33
+ - src/lib/components/data-table-v9/export/csv-exporter.ts
34
+ - src/lib/components/data-table-v9/toolbar/data-table-export.svelte
35
+ - src/lib/components/data-table-v9/xlsx/index.ts
36
+ - src/lib/components/data-table-v9/xlsx/xlsx-cell.ts
37
+ - src/lib/components/data-table-v9/virtual/data-table-virtualized.svelte
38
+ - src/lib/components/data-table-v9/virtual/data-table-virtual-rows.svelte
39
+ ---
40
+
41
+ This skill builds on component-usage and theming. Read those first for Compote namespace imports and theme token behavior.
42
+
43
+ # Compote UI — Data Table
44
+
45
+ Use `compote-ui/data-table` for regular tables and `compote-ui/data-table/virtual` for virtualized rows.
46
+
47
+ ## Integration Setup
48
+
49
+ ```bash
50
+ bun add @tanstack/svelte-table
51
+ ```
52
+
53
+ ```svelte
54
+ <script lang="ts">
55
+ import * as DataTable from 'compote-ui/data-table';
56
+
57
+ type Invoice = {
58
+ id: string;
59
+ customer: string;
60
+ total: number;
61
+ paid: boolean;
62
+ };
63
+
64
+ let invoices = $state<Invoice[]>([
65
+ { id: 'inv_1', customer: 'Ada', total: 1200, paid: true },
66
+ { id: 'inv_2', customer: 'Grace', total: 800, paid: false }
67
+ ]);
68
+
69
+ const col = DataTable.createDataTableColumnHelper<Invoice>();
70
+ const columns = col.columns([
71
+ col.accessor('id', { header: 'ID', enableHiding: false }),
72
+ col.accessor('customer', { header: 'Customer', grow: true }),
73
+ col.accessor('total', { header: 'Total', type: 'currency', align: 'right' }),
74
+ col.accessor('paid', { header: 'Paid', type: 'boolean', align: 'center' })
75
+ ]);
76
+
77
+ const table = DataTable.createTable({
78
+ get data() {
79
+ return invoices;
80
+ },
81
+ columns,
82
+ getRowId: (row) => row.id,
83
+ enableRowSelection: true
84
+ });
85
+ </script>
86
+
87
+ <DataTable.Toolbar>
88
+ <DataTable.Title>Invoices</DataTable.Title>
89
+ {#snippet center()}
90
+ <DataTable.Search {table} class="w-56" />
91
+ {/snippet}
92
+ {#snippet right()}
93
+ <DataTable.ColumnFilter {table} />
94
+ <DataTable.ColumnVisibility {table} />
95
+ {/snippet}
96
+ </DataTable.Toolbar>
97
+
98
+ <div class="h-96 min-h-0">
99
+ <DataTable.Root {table} caption="Invoices" />
100
+ </div>
101
+ ```
102
+
103
+ ## Core Integration Patterns
104
+
105
+ ### Use accessorFn with an explicit id
106
+
107
+ ```ts
108
+ const columns = col.columns([
109
+ col.accessorFn((row) => row.quantity * row.price, {
110
+ id: 'total',
111
+ header: 'Total',
112
+ type: 'currency',
113
+ align: 'right'
114
+ })
115
+ ]);
116
+ ```
117
+
118
+ ### Make runtime column changes reactive
119
+
120
+ ```ts
121
+ const idCol = col.accessor('id', { header: 'ID', enableHiding: false });
122
+ const totalCol = col.accessor('total', { header: 'Total', type: 'currency' });
123
+
124
+ let columns = $state.raw([idCol]);
125
+
126
+ const table = DataTable.createTable({
127
+ get data() {
128
+ return invoices;
129
+ },
130
+ get columns() {
131
+ return columns;
132
+ },
133
+ getRowId: (row) => row.id
134
+ });
135
+
136
+ function toggleTotal() {
137
+ columns = columns.includes(totalCol)
138
+ ? columns.filter((column) => column !== totalCol)
139
+ : [...columns, totalCol];
140
+ }
141
+ ```
142
+
143
+ ### Use virtual import for large row sets
144
+
145
+ ```bash
146
+ bun add @tanstack/svelte-virtual
147
+ ```
148
+
149
+ ```svelte
150
+ <script lang="ts">
151
+ import * as VirtualDataTable from 'compote-ui/data-table/virtual';
152
+
153
+ const table = VirtualDataTable.createTable({
154
+ get data() {
155
+ return rows;
156
+ },
157
+ columns,
158
+ getRowId: (row) => row.id,
159
+ enableRowSelection: true
160
+ });
161
+ </script>
162
+
163
+ <div class="h-96 min-h-0">
164
+ <VirtualDataTable.Root {table} caption="Large dataset" />
165
+ </div>
166
+ ```
167
+
168
+ ### Label toolbar triggers
169
+
170
+ `ColumnFilter` and `ColumnVisibility` render icon-only outline buttons by default (funnel and
171
+ columns icons, with `aria-label` and a hover tooltip). Pass `triggerLabel` to show text instead. `ColumnFilter`
172
+ shows the active filter count as a badge in the corner either way.
173
+
174
+ ```svelte
175
+ <DataTable.ColumnFilter {table} triggerLabel="Filters" />
176
+ <DataTable.ColumnVisibility {table} triggerLabel="Columns" />
177
+ ```
178
+
179
+ ### Filter from column headers
180
+
181
+ Pass `headerFilters` to `DataTable.Root` / `VirtualDataTable.Root` to put a funnel in the header
182
+ of every filterable leaf column (not group headers, not `enableColumnFilter: false`, never
183
+ `type: 'action'`). The funnel shows on header hover/focus (always on touch screens); once the
184
+ column is filtered it stays visible, filled and in the primary color. Clicking it opens a popup
185
+ titled with the column name, with a "Clear" button and the editor for the column's type:
186
+
187
+ | Column `type` | Popup editor | Filter value |
188
+ | ----------------------------------- | -------------------------------------- | -------------------------------- |
189
+ | `number`, `currency`, `percent` | From / To number inputs | `[min?, max?]` (`inNumberRange`) |
190
+ | `boolean` | All / Yes / No | `true` / `false` |
191
+ | `select` | Search + checkbox list with row counts | array of options |
192
+ | `date`, `date-time` | From / To date pickers | `[from?, to?]` as `YYYY-MM-DD` |
193
+ | anything else (`text`, `time`, ...) | Text search | string |
194
+
195
+ Edits apply immediately (text and number inputs after a 300ms debounce — also when the popup
196
+ closes first). The header popups and `DataTable.ColumnFilter` edit the same table state, so a
197
+ filter set in one shows in the other. `headerFilters` is opt-in and defaults to `false`.
198
+
199
+ ```svelte
200
+ <div class="h-96 min-h-0">
201
+ <DataTable.Root {table} caption="Invoices" headerFilters />
202
+ </div>
203
+ ```
204
+
205
+ ### Build your own filter UI
206
+
207
+ For filter controls outside the toolbar and header popups (a facet panel beside the table, a
208
+ filter sidebar), use the same building blocks the Funnel uses. They read and write the table's
209
+ column filters, so the Funnel, header funnels and your UI always show the same state.
210
+
211
+ - `ColumnFilterEditor` — the Funnel's editor for one column, picked by its type (see the table in
212
+ "Filter from column headers"). Pass `editing` only if the editor can be torn down while it holds
213
+ a pending text/range edit (see below); without it the editor uses one bound to the column's table.
214
+ - `ColumnFilterEditing` — the debounced text/range edits pending on a table, shared by every host.
215
+ `isActive(columnId)` lets a host refuse writes from an editor it has already dropped (Ark's
216
+ NumberInput commits on the blur its unmount triggers).
217
+ - `getColumnFilterKind(column)` / `filterKindFor(type)` — the column's `ColumnFilterKind`
218
+ (`range`, `boolean`, `select`, `date` or `text`), or `null` for columns that never filter.
219
+ - `selectFilterOptions(column.getFacetedUniqueValues())` — a select column's options with row
220
+ counts, sorted by label, "(empty)" last with value `EMPTY_SELECT_VALUE` (`null`). Counts are
221
+ cross-filtered: every other active filter applies, the column's own does not.
222
+ - `getColumnLabel(column)` and the `DataTableColumnInstance<T>` type for the columns you list.
223
+
224
+ ```svelte
225
+ <script lang="ts">
226
+ import * as DataTable from 'compote-ui/data-table';
227
+
228
+ let { table }: { table: DataTable.DataTableInstance<Product> } = $props();
229
+
230
+ const facets = $derived(
231
+ table
232
+ .getAllLeafColumns()
233
+ .filter((column) => column.getCanFilter() && DataTable.getColumnFilterKind(column))
234
+ );
235
+ </script>
236
+
237
+ {#each facets as column (column.id)}
238
+ <h3>{DataTable.getColumnLabel(column)}</h3>
239
+ <DataTable.ColumnFilterEditor {column} />
240
+ {/each}
241
+ ```
242
+
243
+ A custom select list writes the same value the Funnel does — an array of options, `undefined`
244
+ when none is ticked:
245
+
246
+ ```ts
247
+ const selected = (column.getFilterValue() as DataTable.SelectFilterValue[] | undefined) ?? [];
248
+ column.setFilterValue(next.length ? next : undefined);
249
+ ```
250
+
251
+ ### Add a refresh button
252
+
253
+ `DataTable.Refresh` is data-source agnostic: it calls `onRefresh` and, if that returns a promise,
254
+ spins its icon and sets `aria-busy` until the promise settles. Clicks while busy are ignored. Pass
255
+ `loading` to force the busy state when a refresh starts elsewhere (polling, another control).
256
+
257
+ ```svelte
258
+ <script lang="ts">
259
+ import { getInvoices } from './invoices.remote';
260
+
261
+ const invoices = getInvoices();
262
+ </script>
263
+
264
+ <DataTable.Toolbar>
265
+ {#snippet right()}
266
+ <DataTable.Refresh onRefresh={() => invoices.refresh()} />
267
+ <DataTable.ColumnFilter {table} />
268
+ <DataTable.ColumnVisibility {table} />
269
+ {/snippet}
270
+ </DataTable.Toolbar>
271
+ ```
272
+
273
+ Props: `onRefresh: () => void | Promise<unknown>`, `loading?: boolean`, `triggerLabel?: string`
274
+ (icon-only with `aria-label="Refresh"` and a tooltip when omitted). Errors thrown by `onRefresh` are not caught.
275
+
276
+ ### Export to a file
277
+
278
+ `DataTable.Export` downloads the visible columns and the filtered, sorted rows. It writes CSV by
279
+ default; pass `exporters` for more formats or your own (an exporter on the server, or a library
280
+ the app already has). With selected rows it offers all rows or only the selected ones. Excel
281
+ comes from `compote-ui/data-table/xlsx` and needs the optional peer `write-excel-file`.
282
+
283
+ ```svelte
284
+ <script lang="ts">
285
+ import * as DataTable from 'compote-ui/data-table';
286
+ import { xlsxExporter } from 'compote-ui/data-table/xlsx';
287
+ </script>
288
+
289
+ <DataTable.Export {table} filename="invoices" exporters={[DataTable.csvExporter, xlsxExporter]} />
290
+ ```
291
+
292
+ Exclude a column with `enableExport: false` (action columns are excluded by default). Use
293
+ `exportValue` when a cell holds a value a file can't show. See [Export](references/export.md).
294
+
295
+ ## Common Mistakes
296
+
297
+ ### CRITICAL Missing id for accessorFn column
298
+
299
+ Wrong:
300
+
301
+ ```ts
302
+ col.accessorFn((row) => row.a + row.b, { header: 'Total' });
303
+ ```
304
+
305
+ Correct:
306
+
307
+ ```ts
308
+ col.accessorFn((row) => row.a + row.b, { id: 'total', header: 'Total' });
309
+ ```
310
+
311
+ Accessor function columns cannot derive a stable column id from a key.
312
+
313
+ Source: `src/lib/components/data-table-v9/create-table.svelte.ts`
314
+
315
+ ### HIGH Non-reactive reassigned columns
316
+
317
+ Wrong:
318
+
319
+ ```ts
320
+ let columns = $state([idCol, nameCol]);
321
+ const table = DataTable.createTable({ data: rows, columns });
322
+ ```
323
+
324
+ Correct:
325
+
326
+ ```ts
327
+ let columns = $state.raw([idCol, nameCol]);
328
+ const table = DataTable.createTable({
329
+ get data() {
330
+ return rows;
331
+ },
332
+ get columns() {
333
+ return columns;
334
+ }
335
+ });
336
+ ```
337
+
338
+ Reassigned columns must be passed through a getter and kept raw so identity checks keep working.
339
+
340
+ Source: `src/routes/components/data-table-v9/+page.svelte`, `src/lib/components/data-table-v9/create-table.svelte.ts`
341
+
342
+ ### HIGH Rendering table without height constraint
343
+
344
+ Wrong:
345
+
346
+ ```svelte
347
+ <DataTable.Root {table} />
348
+ ```
349
+
350
+ Correct:
351
+
352
+ ```svelte
353
+ <div class="h-96 min-h-0">
354
+ <DataTable.Root {table} caption="Invoices" />
355
+ </div>
356
+ ```
357
+
358
+ `DataTable.Root` fills its container and uses internal scrolling.
359
+
360
+ Source: `src/lib/components/data-table-v9/data-table.svelte`
361
+
362
+ ### HIGH Importing virtual table without optional peer
363
+
364
+ Wrong:
365
+
366
+ ```ts
367
+ import * as VirtualDataTable from 'compote-ui/data-table/virtual';
368
+ ```
369
+
370
+ Correct:
371
+
372
+ ```bash
373
+ bun add @tanstack/svelte-virtual
374
+ ```
375
+
376
+ ```ts
377
+ import * as VirtualDataTable from 'compote-ui/data-table/virtual';
378
+ ```
379
+
380
+ The virtual subpath imports `@tanstack/svelte-virtual`.
381
+
382
+ Source: `package.json`, `src/lib/components/data-table-v9/virtual/data-table-virtual-rows.svelte`
383
+
384
+ ### HIGH Using the Excel exporter without its optional peer
385
+
386
+ `compote-ui/data-table/xlsx` imports `write-excel-file` (dynamically, on export). Without it the
387
+ app's build fails to resolve the import. Install it first:
388
+
389
+ ```bash
390
+ bun add write-excel-file
391
+ ```
392
+
393
+ Source: `package.json`, `src/lib/components/data-table-v9/xlsx/index.ts`
394
+
395
+ ## References
396
+
397
+ - [Column options](references/column-options.md)
398
+ - [Table state and reactivity](references/table-state-and-reactivity.md)
399
+ - [Virtual table](references/virtual-table.md)
400
+ - [Export](references/export.md)