compote-ui 0.83.0 → 0.84.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 (31) hide show
  1. package/dist/components/data-table-v9/create-table.svelte.js +55 -5
  2. package/dist/components/data-table-v9/export/csv-exporter.d.ts +14 -0
  3. package/dist/components/data-table-v9/export/csv-exporter.js +43 -0
  4. package/dist/components/data-table-v9/export/export-data.d.ts +98 -0
  5. package/dist/components/data-table-v9/export/export-data.js +55 -0
  6. package/dist/components/data-table-v9/export/types.d.ts +23 -0
  7. package/dist/components/data-table-v9/export/types.js +1 -0
  8. package/dist/components/data-table-v9/index.d.ts +6 -0
  9. package/dist/components/data-table-v9/index.js +4 -0
  10. package/dist/components/data-table-v9/toolbar/data-table-export.svelte +146 -0
  11. package/dist/components/data-table-v9/toolbar/data-table-export.svelte.d.ts +35 -0
  12. package/dist/components/data-table-v9/types.d.ts +23 -0
  13. package/dist/components/data-table-v9/virtual/index.d.ts +2 -2
  14. package/dist/components/data-table-v9/virtual/index.js +1 -1
  15. package/dist/components/data-table-v9/xlsx/index.d.ts +24 -0
  16. package/dist/components/data-table-v9/xlsx/index.js +60 -0
  17. package/dist/components/data-table-v9/xlsx/xlsx-cell.d.ts +51 -0
  18. package/dist/components/data-table-v9/xlsx/xlsx-cell.js +123 -0
  19. package/dist/icons/PhDownloadSimple.svelte +18 -0
  20. package/dist/icons/PhDownloadSimple.svelte.d.ts +5 -0
  21. package/dist/icons/index.d.ts +1 -0
  22. package/dist/icons/index.js +1 -0
  23. package/dist/utils/csv.d.ts +13 -0
  24. package/dist/utils/csv.js +18 -0
  25. package/dist/utils/download.d.ts +5 -0
  26. package/dist/utils/download.js +17 -0
  27. package/dist/utils/select-filter.js +6 -1
  28. package/package.json +129 -119
  29. package/skills/data-table/SKILL.md +400 -362
  30. package/skills/data-table/references/column-options.md +181 -177
  31. package/skills/data-table/references/export.md +158 -0
@@ -1,177 +1,181 @@
1
- # Column Options
2
-
3
- Column helper:
4
-
5
- ```ts
6
- const col = DataTable.createDataTableColumnHelper<Row>();
7
- ```
8
-
9
- Accessors:
10
-
11
- ```ts
12
- col.accessor('fieldName', { header: 'Field' });
13
- col.accessorFn((row) => row.a + row.b, { id: 'sum', header: 'Sum' });
14
- col.group('Group', [col.accessor('name', { header: 'Name' })]);
15
- col.columns([col.accessor('id', { header: 'ID' })]);
16
- ```
17
-
18
- Common options:
19
-
20
- - `header: string`
21
- - `type?: 'text' | 'number' | 'currency' | 'percent' | 'boolean' | 'select' | 'url' | 'phone' | 'date' | 'time' | 'date-time' | 'action'`
22
- - `align?: 'left' | 'center' | 'right'`
23
- - `size?: number`
24
- - `minSize?: number`
25
- - `maxSize?: number`
26
- - `enableResizing?: boolean`
27
- - `enableHiding?: boolean`
28
- - `enableSorting?: boolean`
29
- - `enableColumnFilter?: boolean`
30
- - `filterFn?: FilterFn | string`
31
- - `formatOptions?: Intl.NumberFormatOptions | Intl.DateTimeFormatOptions`
32
- - `formatLocale?: string`
33
- - `cell?: (value, row) => string | number | boolean | null | undefined`
34
- - `cellComponent?: Component`
35
- - `cellProps?: (value, row) => Record<string, unknown>`
36
- - `cellSnippet?: Snippet`
37
- - `pinned?: 'left' | 'right'` — sticky during horizontal scroll. Inside a `col.group(...)`, pinning
38
- a member column also pins the group header fragment above it, so the label stays visible while
39
- the rest of that group scrolls.
40
- - `grow?: boolean`
41
- - `sum?: boolean`
42
- - `footer?: (values: unknown[]) => string | number | undefined`
43
-
44
- Type defaults:
45
-
46
- - `number`, `currency`: right aligned, size `120`
47
- - `percent`: right aligned, size `100`
48
- - `date`: center aligned, size `110`
49
- - `time`: center aligned, size `80`
50
- - `date-time`: center aligned, size `160`
51
- - `boolean`: center aligned, size `90`
52
- - `url`: center aligned, sorting disabled, size `60`
53
- - `phone`: left aligned, size `160`
54
- - `action`: center aligned, size `60`, sorting/hiding/column-filter all disabled — layout-only,
55
- carries no value to format or filter. Supply a `cellComponent`/`cellSnippet` for the row action
56
- buttons; the defaults keep the actions column out of the column-visibility menu and filter
57
- toolbar automatically.
58
-
59
- Use one `grow: true` column to absorb extra horizontal space.
60
-
61
- ## Date columns: display and filter
62
-
63
- `date`, `time` and `date-time` cells are read the same way for display and for the date filter,
64
- in the column's display zone — `formatOptions.timeZone` when set, otherwise the viewer's local
65
- zone:
66
-
67
- - `'2024-03-15'` (and a `CalendarDate`) is a calendar day. It always shows and filters as
68
- 15 March, also in zones west of UTC — it is never turned into a midnight-UTC instant.
69
- - `Date`, epoch milliseconds, strings with an offset (`Z`, `+02:00`, DB `+02`) and
70
- `ZonedDateTime` are instants, shown and filtered on the day they fall on in the display zone.
71
- - A datetime without an offset (`'2024-03-15T10:30'`) is wall-clock time in the display zone.
72
- - Unreadable values (e.g. `'2024-02-30'`) render as `-` and never match an active filter.
73
-
74
- `date` and `date-time` columns filter by calendar-day range. The filter value is
75
- `[from?, to?]`, each `YYYY-MM-DD` or empty (open-ended); both days are inclusive, a reversed
76
- range is swapped, and an empty range removes the filter. Instants are judged by their calendar
77
- day, so 23- and 25-hour DST days need no special handling. `time` columns keep the text filter.
78
-
79
- ```ts
80
- table.getColumn('createdAt')?.setFilterValue(['2024-03-01', '2024-03-31']); // March
81
- table.getColumn('createdAt')?.setFilterValue([undefined, '2024-03-31']); // up to 31 March
82
- ```
83
-
84
- ## Select columns with multiple values
85
-
86
- A `type: 'select'` column may return an array (tags, multi-select attributes). The Funnel then
87
- lists each item as its own option with a row count, a row matches when any of its items is
88
- selected, and the cell renders the items joined with `, `. A row counts once per option even
89
- when its array repeats an item. Missing values — `null`, `undefined`, `''` or an empty array —
90
- share one `(empty)` option.
91
-
92
- The filter value is an array of selected options: each is the `String()` of the cell value, and
93
- the empty option is `null` — so a real `'null'` string stays its own option:
94
-
95
- ```ts
96
- table.getColumn('tags')?.setFilterValue(['Battery', null]); // Battery or no tags
97
- ```
98
-
99
- ```ts
100
- col.accessorFn((row) => row.tags.map((tag) => tag.name), {
101
- id: 'tags',
102
- header: 'Tags',
103
- type: 'select'
104
- });
105
- ```
106
-
107
- ## Locale for number/currency/percent
108
-
109
- Priority: `formatLocale` prop → `useLocaleContext().locale` → browser default. Wrap the app in
110
- `<LocaleProvider locale="sr-RS">` to set the context locale for every formatted column.
111
-
112
- ## Custom cellComponent
113
-
114
- ```svelte
115
- <!-- PriceCell.svelte -->
116
- <script lang="ts">
117
- let { value, prices }: { value: number; prices: { vendor: string; price: number }[] } = $props();
118
- </script>
119
-
120
- <span>{value}</span>
121
- ```
122
-
123
- ```ts
124
- col.accessorFn((row) => Math.min(...row.prices.map((p) => p.price)), {
125
- id: 'minPrice',
126
- header: 'Best Price',
127
- type: 'currency',
128
- align: 'right',
129
- cellComponent: PriceCell,
130
- cellProps: (value, row) => ({ value, prices: row.prices })
131
- });
132
- ```
133
-
134
- ## Row height and cell alignment
135
-
136
- Body cells are fixed at `h-9` (36px), matching the header, with content vertically centered — any
137
- control up to 36px tall (every `Button` `icon-xs`/`icon-sm`/`icon`/`icon-lg` size) fits without
138
- changing row height. Do **not** add negative margins (e.g. `-my-1`) to cell components to
139
- compensate for row growth — in the virtualized table that fights the virtualizer's 37px row
140
- estimate. Cells are `truncate` (single-line, `overflow: hidden`), so give action columns a little
141
- extra `size` if a focus ring on a control near the edge gets clipped.
142
-
143
- ## DataTable.Search
144
-
145
- Global filter input searching across all text columns, debounced (default 300ms).
146
-
147
- - `table: DataTableInstance<T>` — required
148
- - `placeholder?: string` (default `'Search...'`)
149
- - `class?: ClassValue`
150
- - `debounceMs?: number` (default `300`)
151
-
152
- Text/number columns stay searchable even when the first row's value is `null`/`undefined`;
153
- booleans and object-valued columns are excluded from global search. Also available as
154
- `VirtualDataTable.Search` for the virtualized variant.
155
-
156
- ## DataTable.Root props
157
-
158
- `table: DataTableInstance<T>` (required), `caption?: string` (sr-only `<caption>`),
159
- `emptyMessage?: string` (default `'No rows found'`), `class?: ClassValue`,
160
- `headerFilters?: boolean` (default `false`; funnel filter popups in column headers — see
161
- "Filter from column headers" in SKILL.md),
162
- `onRowClick?: (details: { row: T; event: MouseEvent }) => void`,
163
- `onRowDoubleClick?: (details: { row: T; event: MouseEvent }) => void` — `row` is `row.original`.
164
- Clicks on the row-selection checkbox and url-cell buttons do **not** trigger either handler.
165
- `rowClass?: (row: T) => ClassValue` adds classes to the row's `<tr>` (e.g. a tint; also set
166
- `--row-bg` so pinned and selection cells match).
167
- `VirtualDataTable.Root` shares the same props.
168
-
169
- ```svelte
170
- <DataTable.Root
171
- {table}
172
- onRowDoubleClick={({ row, event }) => {
173
- if (event.ctrlKey || event.metaKey) openInNewTab(row.id);
174
- else navigateTo(row.id);
175
- }}
176
- />
177
- ```
1
+ # Column Options
2
+
3
+ Column helper:
4
+
5
+ ```ts
6
+ const col = DataTable.createDataTableColumnHelper<Row>();
7
+ ```
8
+
9
+ Accessors:
10
+
11
+ ```ts
12
+ col.accessor('fieldName', { header: 'Field' });
13
+ col.accessorFn((row) => row.a + row.b, { id: 'sum', header: 'Sum' });
14
+ col.group('Group', [col.accessor('name', { header: 'Name' })]);
15
+ col.columns([col.accessor('id', { header: 'ID' })]);
16
+ ```
17
+
18
+ Common options:
19
+
20
+ - `header: string`
21
+ - `type?: 'text' | 'number' | 'currency' | 'percent' | 'boolean' | 'select' | 'url' | 'phone' | 'date' | 'time' | 'date-time' | 'action'`
22
+ - `align?: 'left' | 'center' | 'right'`
23
+ - `size?: number`
24
+ - `minSize?: number`
25
+ - `maxSize?: number`
26
+ - `enableResizing?: boolean`
27
+ - `enableHiding?: boolean`
28
+ - `enableSorting?: boolean`
29
+ - `enableColumnFilter?: boolean`
30
+ - `filterFn?: FilterFn | string`
31
+ - `formatOptions?: Intl.NumberFormatOptions | Intl.DateTimeFormatOptions`
32
+ - `formatLocale?: string`
33
+ - `cell?: (value, row) => string | number | boolean | null | undefined`
34
+ - `cellComponent?: Component`
35
+ - `cellProps?: (value, row) => Record<string, unknown>`
36
+ - `cellSnippet?: Snippet`
37
+ - `pinned?: 'left' | 'right'` — sticky during horizontal scroll. Inside a `col.group(...)`, pinning
38
+ a member column also pins the group header fragment above it, so the label stays visible while
39
+ the rest of that group scrolls.
40
+ - `grow?: boolean`
41
+ - `sum?: boolean`
42
+ - `footer?: (values: unknown[]) => string | number | undefined`
43
+ - `exportValue?: (value, row) => unknown` — the value `DataTable.Export` writes instead of the
44
+ accessor value (e.g. a number out of an object a `cellComponent` renders). Without `cell`, its
45
+ text is formatted by `type`; it is never passed to `cell`. See [Export](export.md).
46
+ - `enableExport?: boolean` — defaults to `true`, `false` for `type: 'action'`.
47
+
48
+ Type defaults:
49
+
50
+ - `number`, `currency`: right aligned, size `120`
51
+ - `percent`: right aligned, size `100`
52
+ - `date`: center aligned, size `110`
53
+ - `time`: center aligned, size `80`
54
+ - `date-time`: center aligned, size `160`
55
+ - `boolean`: center aligned, size `90`
56
+ - `url`: center aligned, sorting disabled, size `60`
57
+ - `phone`: left aligned, size `160`
58
+ - `action`: center aligned, size `60`, sorting/hiding/column-filter all disabled — layout-only,
59
+ carries no value to format or filter. Supply a `cellComponent`/`cellSnippet` for the row action
60
+ buttons; the defaults keep the actions column out of the column-visibility menu and filter
61
+ toolbar automatically.
62
+
63
+ Use one `grow: true` column to absorb extra horizontal space.
64
+
65
+ ## Date columns: display and filter
66
+
67
+ `date`, `time` and `date-time` cells are read the same way for display and for the date filter,
68
+ in the column's display zone — `formatOptions.timeZone` when set, otherwise the viewer's local
69
+ zone:
70
+
71
+ - `'2024-03-15'` (and a `CalendarDate`) is a calendar day. It always shows and filters as
72
+ 15 March, also in zones west of UTC — it is never turned into a midnight-UTC instant.
73
+ - `Date`, epoch milliseconds, strings with an offset (`Z`, `+02:00`, DB `+02`) and
74
+ `ZonedDateTime` are instants, shown and filtered on the day they fall on in the display zone.
75
+ - A datetime without an offset (`'2024-03-15T10:30'`) is wall-clock time in the display zone.
76
+ - Unreadable values (e.g. `'2024-02-30'`) render as `-` and never match an active filter.
77
+
78
+ `date` and `date-time` columns filter by calendar-day range. The filter value is
79
+ `[from?, to?]`, each `YYYY-MM-DD` or empty (open-ended); both days are inclusive, a reversed
80
+ range is swapped, and an empty range removes the filter. Instants are judged by their calendar
81
+ day, so 23- and 25-hour DST days need no special handling. `time` columns keep the text filter.
82
+
83
+ ```ts
84
+ table.getColumn('createdAt')?.setFilterValue(['2024-03-01', '2024-03-31']); // March
85
+ table.getColumn('createdAt')?.setFilterValue([undefined, '2024-03-31']); // up to 31 March
86
+ ```
87
+
88
+ ## Select columns with multiple values
89
+
90
+ A `type: 'select'` column may return an array (tags, multi-select attributes). The Funnel then
91
+ lists each item as its own option with a row count, a row matches when any of its items is
92
+ selected, and the cell renders the items joined with `, `. A row counts once per option even
93
+ when its array repeats an item. Missing values — `null`, `undefined`, `''` or an empty array —
94
+ share one `(empty)` option.
95
+
96
+ The filter value is an array of selected options: each is the `String()` of the cell value, and
97
+ the empty option is `null` — so a real `'null'` string stays its own option:
98
+
99
+ ```ts
100
+ table.getColumn('tags')?.setFilterValue(['Battery', null]); // Battery or no tags
101
+ ```
102
+
103
+ ```ts
104
+ col.accessorFn((row) => row.tags.map((tag) => tag.name), {
105
+ id: 'tags',
106
+ header: 'Tags',
107
+ type: 'select'
108
+ });
109
+ ```
110
+
111
+ ## Locale for number/currency/percent
112
+
113
+ Priority: `formatLocale` prop → `useLocaleContext().locale` → browser default. Wrap the app in
114
+ `<LocaleProvider locale="sr-RS">` to set the context locale for every formatted column.
115
+
116
+ ## Custom cellComponent
117
+
118
+ ```svelte
119
+ <!-- PriceCell.svelte -->
120
+ <script lang="ts">
121
+ let { value, prices }: { value: number; prices: { vendor: string; price: number }[] } = $props();
122
+ </script>
123
+
124
+ <span>{value}</span>
125
+ ```
126
+
127
+ ```ts
128
+ col.accessorFn((row) => Math.min(...row.prices.map((p) => p.price)), {
129
+ id: 'minPrice',
130
+ header: 'Best Price',
131
+ type: 'currency',
132
+ align: 'right',
133
+ cellComponent: PriceCell,
134
+ cellProps: (value, row) => ({ value, prices: row.prices })
135
+ });
136
+ ```
137
+
138
+ ## Row height and cell alignment
139
+
140
+ Body cells are fixed at `h-9` (36px), matching the header, with content vertically centered — any
141
+ control up to 36px tall (every `Button` `icon-xs`/`icon-sm`/`icon`/`icon-lg` size) fits without
142
+ changing row height. Do **not** add negative margins (e.g. `-my-1`) to cell components to
143
+ compensate for row growth — in the virtualized table that fights the virtualizer's 37px row
144
+ estimate. Cells are `truncate` (single-line, `overflow: hidden`), so give action columns a little
145
+ extra `size` if a focus ring on a control near the edge gets clipped.
146
+
147
+ ## DataTable.Search
148
+
149
+ Global filter input searching across all text columns, debounced (default 300ms).
150
+
151
+ - `table: DataTableInstance<T>` — required
152
+ - `placeholder?: string` (default `'Search...'`)
153
+ - `class?: ClassValue`
154
+ - `debounceMs?: number` (default `300`)
155
+
156
+ Text/number columns stay searchable even when the first row's value is `null`/`undefined`;
157
+ booleans and object-valued columns are excluded from global search. Also available as
158
+ `VirtualDataTable.Search` for the virtualized variant.
159
+
160
+ ## DataTable.Root props
161
+
162
+ `table: DataTableInstance<T>` (required), `caption?: string` (sr-only `<caption>`),
163
+ `emptyMessage?: string` (default `'No rows found'`), `class?: ClassValue`,
164
+ `headerFilters?: boolean` (default `false`; funnel filter popups in column headers — see
165
+ "Filter from column headers" in SKILL.md),
166
+ `onRowClick?: (details: { row: T; event: MouseEvent }) => void`,
167
+ `onRowDoubleClick?: (details: { row: T; event: MouseEvent }) => void` — `row` is `row.original`.
168
+ Clicks on the row-selection checkbox and url-cell buttons do **not** trigger either handler.
169
+ `rowClass?: (row: T) => ClassValue` adds classes to the row's `<tr>` (e.g. a tint; also set
170
+ `--row-bg` so pinned and selection cells match).
171
+ `VirtualDataTable.Root` shares the same props.
172
+
173
+ ```svelte
174
+ <DataTable.Root
175
+ {table}
176
+ onRowDoubleClick={({ row, event }) => {
177
+ if (event.ctrlKey || event.metaKey) openInNewTab(row.id);
178
+ else navigateTo(row.id);
179
+ }}
180
+ />
181
+ ```
@@ -0,0 +1,158 @@
1
+ # Export
2
+
3
+ `DataTable.Export` downloads the table as a file. It sits in the toolbar like `ColumnVisibility`
4
+ and takes the `table`.
5
+
6
+ ```svelte
7
+ <script lang="ts">
8
+ import * as DataTable from 'compote-ui/data-table';
9
+ </script>
10
+
11
+ <DataTable.Toolbar>
12
+ {#snippet right()}
13
+ <DataTable.Export {table} filename="invoices" />
14
+ <DataTable.ColumnVisibility {table} />
15
+ {/snippet}
16
+ </DataTable.Toolbar>
17
+ ```
18
+
19
+ Props:
20
+
21
+ - `table: DataTableInstance<T>`
22
+ - `exporters?: DataTableExporter[]` — defaults to `[csvExporter]`.
23
+ - `filename?: string` — without extension, defaults to `'export'`.
24
+ - `triggerLabel?: string` — icon-only with a tooltip when omitted.
25
+
26
+ One exporter and no selected rows renders a plain button. More exporters, or any selected row,
27
+ renders a menu. When rows are selected the menu offers **All rows (n)** / **Selected rows (k)**.
28
+ `k` counts the selected rows the current filter keeps, exactly what gets exported.
29
+
30
+ ## What gets exported
31
+
32
+ - **Columns:** the visible ones, in the order the table shows them (pinned left, center, pinned
33
+ right). Columns with `enableExport: false` are skipped, and so are `type: 'action'` columns by
34
+ default. A grouped column's header is `Group / Column`.
35
+ - **Rows:** the filtered and sorted rows (`table.getRowModel()`). The table has no pagination,
36
+ so this is every matching row, not one page.
37
+ - **Cells:** the text the table shows (`cell`, else type formatting; `boolean`, `url` and
38
+ `phone` as the table draws them, which ignores `cell`). Empty cells are blank, not `-`. A
39
+ column with `exportValue` and no `cell` formats `exportValue` output by its `type`.
40
+
41
+ ## CSV
42
+
43
+ `csvExporter` writes a UTF-8 BOM (so Excel reads č/ć/š/đ/ž correctly), `;` as the separator
44
+ (what Excel expects under most European locales), and CRLF line endings. Fields starting with
45
+ `= + - @`, tab or CR get a `'` prefix so spreadsheets don't run them as formulas. Exempt, so
46
+ negative numbers and phone numbers stay as they are: text made only of digits, spaces and number
47
+ punctuation (`-5`, `+381 60 123`), and number, currency and percent columns without their own
48
+ `cell`.
49
+
50
+ ```ts
51
+ import { createCsvExporter } from 'compote-ui/data-table';
52
+
53
+ const commaCsv = createCsvExporter({ id: 'csv-comma', label: 'CSV (comma)', separator: ',' });
54
+ // Also: bom?: boolean (default true)
55
+ ```
56
+
57
+ CSV holds the formatted text (`1,234.56 RSD`), so a spreadsheet can't sum it.
58
+
59
+ ## Excel (.xlsx)
60
+
61
+ `xlsxExporter` lives in its own subpath and needs the optional peer `write-excel-file`
62
+ (about 19 KB gzip, loaded only when an export runs, so importing the subpath is SSR-safe):
63
+
64
+ ```sh
65
+ bun add write-excel-file
66
+ ```
67
+
68
+ ```svelte
69
+ <script lang="ts">
70
+ import * as DataTable from 'compote-ui/data-table';
71
+ import { xlsxExporter } from 'compote-ui/data-table/xlsx';
72
+ </script>
73
+
74
+ <DataTable.Export {table} filename="invoices" exporters={[DataTable.csvExporter, xlsxExporter]} />
75
+ ```
76
+
77
+ Cells stay computable in Excel:
78
+
79
+ - `number`, `currency`, `percent` → numbers, also from numeric strings (`"1234.56"`), with an
80
+ Excel format built from the column's Intl options: grouping, the decimals Intl would show,
81
+ `%`, and the currency code (`#,##0.00 "USD"`). Options Excel can't express (`notation`,
82
+ significant digits, `signDisplay`, `currencyDisplay: 'name'`, …) fall back to the text.
83
+ - `date`, `time`, `date-time` → Excel dates, on the day and time the table shows in the column's
84
+ zone (`formatOptions.timeZone`, else local). A `date` column drops the time; a `time` column
85
+ keeps only the time of day, so it sorts by time. Formats default to `dd.mm.yyyy`, `hh:mm` and
86
+ `dd.mm.yyyy hh:mm`.
87
+ - `boolean` → TRUE/FALSE for real `true`/`false`; other values are blank, as the table shows
88
+ `-` for them.
89
+ - Everything else, and any column with its own `cell` but no `exportValue`, is the text the
90
+ table shows. Typed cells can't hold formulas, so there is no `'` prefix.
91
+
92
+ The header row is bold and frozen; column widths follow the table's.
93
+
94
+ ```ts
95
+ import { createXlsxExporter } from 'compote-ui/data-table/xlsx';
96
+
97
+ const excel = createXlsxExporter({
98
+ sheet: 'Invoices',
99
+ date: 'yyyy-mm-dd',
100
+ dateTime: 'yyyy-mm-dd hh:mm'
101
+ // also: time, id, label
102
+ });
103
+ ```
104
+
105
+ `exportDataToXlsxBlob(data, options)` returns the file as a `Blob` (e.g. to upload instead of
106
+ download), and `toXlsxCell` maps one cell.
107
+
108
+ ## Custom exporter
109
+
110
+ An exporter is `{ id, label, icon?, export(context) }`. `context` holds `table`, `scope`
111
+ (`'filtered' | 'selected'`), `filename` and `getData()`, which returns `{ columns, rows }`. Each
112
+ cell is `{ value, text }`. If `export` returns a promise, the trigger stays busy until it
113
+ settles. Errors are not caught.
114
+
115
+ Export on the server, from the table state:
116
+
117
+ ```ts
118
+ import type { DataTableExporter } from 'compote-ui/data-table';
119
+
120
+ const serverExcel: DataTableExporter = {
121
+ id: 'server-xlsx',
122
+ label: 'Excel',
123
+ export: async ({ table }) => {
124
+ const url = await requestExport({
125
+ sorting: table.atoms.sorting.get(),
126
+ columnFilters: table.atoms.columnFilters.get(),
127
+ globalFilter: table.atoms.globalFilter.get(),
128
+ columns: table.getVisibleLeafColumns().map((column) => column.id)
129
+ });
130
+ window.location.assign(url);
131
+ }
132
+ };
133
+ ```
134
+
135
+ Use a library the app already has instead of `write-excel-file` (SheetJS here):
136
+
137
+ ```ts
138
+ import { utils, writeFileXLSX } from 'xlsx';
139
+ import type { DataTableExporter } from 'compote-ui/data-table';
140
+
141
+ const sheetJs: DataTableExporter = {
142
+ id: 'xlsx',
143
+ label: 'Excel',
144
+ export: ({ filename, getData }) => {
145
+ const { columns, rows } = getData();
146
+ const sheet = utils.aoa_to_sheet([
147
+ columns.map((column) => column.header),
148
+ ...rows.map((cells) => cells.map((cell) => cell.text))
149
+ ]);
150
+ const book = utils.book_new();
151
+ utils.book_append_sheet(book, sheet, 'Sheet1');
152
+ writeFileXLSX(book, `${filename}.xlsx`);
153
+ }
154
+ };
155
+ ```
156
+
157
+ `getExportData(table, scope)`, `getExportRows(table, scope)` and `getExportColumns(table)` are
158
+ exported for building export UI outside `DataTable.Export`.