compote-ui 0.83.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.
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 +29 -0
  16. package/dist/components/data-table-v9/xlsx/index.js +84 -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 +13 -3
  29. package/skills/data-table/SKILL.md +42 -4
  30. package/skills/data-table/references/column-options.md +4 -0
  31. package/skills/data-table/references/export.md +160 -0
@@ -0,0 +1,84 @@
1
+ import { PhMicrosoftExcelLogo } from '../../../icons';
2
+ import { downloadBlob } from '../../../utils/download';
3
+ import { DEFAULT_XLSX_DATE_FORMATS, toXlsxCell } from './xlsx-cell';
4
+ export { DEFAULT_XLSX_DATE_FORMATS, toXlsxCell, xlsxNumberFormat } from './xlsx-cell';
5
+ // Column widths in characters of Excel's default font.
6
+ const MIN_COLUMN_WIDTH = 8;
7
+ const MAX_COLUMN_WIDTH = 60;
8
+ // One character of slack: char counts already overshoot narrow lowercase text, and this keeps
9
+ // wide letters (W, M, capitals) from being cut off.
10
+ const COLUMN_WIDTH_PADDING = 1;
11
+ /** Characters in the longest line of `text` (code points, so č or 🙂 count once). */
12
+ function longestLine(text) {
13
+ let longest = 0;
14
+ for (const line of text.split(/\r\n|\r|\n/))
15
+ longest = Math.max(longest, Array.from(line).length);
16
+ return longest;
17
+ }
18
+ /**
19
+ * A width per column that fits its header and the longest cell text (the text the table
20
+ * shows, which is close to what Excel shows), plus padding, between 8 and 60 characters.
21
+ */
22
+ export function xlsxColumnWidths(data) {
23
+ return data.columns.map((column, index) => {
24
+ let longest = longestLine(column.header);
25
+ for (const cells of data.rows) {
26
+ const text = cells[index]?.text;
27
+ if (text)
28
+ longest = Math.max(longest, longestLine(text));
29
+ }
30
+ return Math.min(MAX_COLUMN_WIDTH, Math.max(MIN_COLUMN_WIDTH, longest + COLUMN_WIDTH_PADDING));
31
+ });
32
+ }
33
+ /** The export data as sheet rows: a bold header row, then typed cells. */
34
+ export function exportDataToSheet(data, formats = DEFAULT_XLSX_DATE_FORMATS) {
35
+ const header = data.columns.map((column) => ({
36
+ type: String,
37
+ value: column.header,
38
+ fontWeight: 'bold'
39
+ }));
40
+ const rows = data.rows.map((cells) => data.columns.map((column, index) => {
41
+ const cell = cells[index];
42
+ return cell ? toXlsxCell(column, cell, formats) : null;
43
+ }));
44
+ return [header, ...rows];
45
+ }
46
+ /**
47
+ * The export data as an .xlsx file. Loads `write-excel-file` (an optional peer dependency) on
48
+ * first call, so importing this module costs nothing until an export runs — and stays SSR-safe.
49
+ */
50
+ export async function exportDataToXlsxBlob(data, options = {}) {
51
+ const { default: writeXlsxFile } = await import('write-excel-file/browser');
52
+ const formats = { ...DEFAULT_XLSX_DATE_FORMATS, ...pickFormats(options) };
53
+ const sheetData = exportDataToSheet(data, formats);
54
+ // XlsxCell is a subset of the package's cell shape; cast via its own (type-only) type.
55
+ return writeXlsxFile(sheetData, {
56
+ sheet: options.sheet,
57
+ stickyRowsCount: 1,
58
+ columns: xlsxColumnWidths(data).map((width) => ({ width }))
59
+ }).toBlob();
60
+ }
61
+ function pickFormats({ date, time, dateTime }) {
62
+ return {
63
+ ...(date !== undefined ? { date } : {}),
64
+ ...(time !== undefined ? { time } : {}),
65
+ ...(dateTime !== undefined ? { dateTime } : {})
66
+ };
67
+ }
68
+ /**
69
+ * An Excel exporter: numbers, dates and booleans as typed cells, formatted like the table.
70
+ * Needs `write-excel-file` installed.
71
+ */
72
+ export function createXlsxExporter(options = {}) {
73
+ const { id = 'xlsx', label = 'Excel' } = options;
74
+ return {
75
+ id,
76
+ label,
77
+ icon: PhMicrosoftExcelLogo,
78
+ export: async ({ filename, getData }) => {
79
+ const blob = await exportDataToXlsxBlob(getData(), options);
80
+ downloadBlob(blob, `${filename}.xlsx`);
81
+ }
82
+ };
83
+ }
84
+ export const xlsxExporter = createXlsxExporter();
@@ -0,0 +1,51 @@
1
+ import { type ZonedOrCalendarDate } from '../../../utils/date';
2
+ import type { DataTableExportCell, DataTableExportColumn } from '../export/export-data';
3
+ /** Excel number formats for date columns. */
4
+ export type XlsxDateFormats = {
5
+ date: string;
6
+ time: string;
7
+ dateTime: string;
8
+ };
9
+ export declare const DEFAULT_XLSX_DATE_FORMATS: XlsxDateFormats;
10
+ /**
11
+ * One sheet cell, in the shape `write-excel-file` takes. Declared here so the published types
12
+ * don't depend on the optional peer.
13
+ */
14
+ export type XlsxCell = null | {
15
+ type: StringConstructor;
16
+ value: string;
17
+ fontWeight?: 'bold';
18
+ } | {
19
+ type: NumberConstructor;
20
+ value: number;
21
+ format: string;
22
+ } | {
23
+ type: DateConstructor;
24
+ value: Date;
25
+ format: string;
26
+ } | {
27
+ type: BooleanConstructor;
28
+ value: boolean;
29
+ };
30
+ /**
31
+ * The Excel format string matching how the table shows `value`, or undefined when the
32
+ * column's Intl options have no faithful Excel equivalent (the cell is then written as text).
33
+ * Decimals are chosen per value, exactly as many as Intl shows (read from `formatToParts`, so
34
+ * Intl's own rounding decides): `#,##0.###` would leave a trailing `.` on whole numbers.
35
+ */
36
+ export declare function xlsxNumberFormat(column: DataTableExportColumn, value: number): string | undefined;
37
+ /** Which part of a date a column shows, and so which part Excel gets. */
38
+ export type XlsxDatePart = 'date' | 'time' | 'date-time';
39
+ /**
40
+ * The date as Excel should show it. Excel dates have no zone, and `write-excel-file` reads a
41
+ * `Date` in UTC, so the wall clock of the zone the table shows is written as UTC. A `date`
42
+ * column drops the time (midnight); a `time` column keeps only the time of day, on Excel's day
43
+ * zero (1899-12-30), so times sort and compare by time alone.
44
+ */
45
+ export declare function toXlsxDate(date: ZonedOrCalendarDate, part?: XlsxDatePart): Date;
46
+ /**
47
+ * One export cell as a typed sheet cell: numbers, dates and booleans stay computable in Excel;
48
+ * everything else — and any column with its own `cell` renderer but no `exportValue` — is the
49
+ * text the table shows. Typed cells can't hold formulas, so no `'` guard is needed.
50
+ */
51
+ export declare function toXlsxCell(column: DataTableExportColumn, cell: DataTableExportCell, formats?: XlsxDateFormats): XlsxCell;
@@ -0,0 +1,123 @@
1
+ import { GregorianCalendar, toCalendar } from '@internationalized/date';
2
+ import { readDateInZone } from '../../../utils/date';
3
+ import { dateCellTimeZone } from '../date-cell';
4
+ import { TYPE_NUMBER_FORMAT_DEFAULTS } from '../data-table-utils';
5
+ export const DEFAULT_XLSX_DATE_FORMATS = {
6
+ date: 'dd.mm.yyyy',
7
+ time: 'hh:mm',
8
+ dateTime: 'dd.mm.yyyy hh:mm'
9
+ };
10
+ const NUMBER_TYPES = new Set(['number', 'currency', 'percent']);
11
+ const DATE_TYPES = new Set(['date', 'time', 'date-time']);
12
+ const isEmpty = (value) => value === null || value === undefined || value === '';
13
+ function textCell(cell) {
14
+ return cell.text === '' ? null : { type: String, value: cell.text };
15
+ }
16
+ /**
17
+ * The Excel format string matching how the table shows `value`, or undefined when the
18
+ * column's Intl options have no faithful Excel equivalent (the cell is then written as text).
19
+ * Decimals are chosen per value, exactly as many as Intl shows (read from `formatToParts`, so
20
+ * Intl's own rounding decides): `#,##0.###` would leave a trailing `.` on whole numbers.
21
+ */
22
+ export function xlsxNumberFormat(column, value) {
23
+ const defaults = column.type ? TYPE_NUMBER_FORMAT_DEFAULTS[column.type] : undefined;
24
+ let formatter;
25
+ try {
26
+ formatter = new Intl.NumberFormat(column.locale, {
27
+ ...defaults,
28
+ ...column.formatOptions
29
+ });
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ const resolved = formatter.resolvedOptions();
35
+ const unsupported = resolved.notation !== 'standard' ||
36
+ resolved.minimumSignificantDigits !== undefined ||
37
+ resolved.maximumSignificantDigits !== undefined ||
38
+ resolved.signDisplay !== 'auto' ||
39
+ resolved.minimumIntegerDigits !== 1 ||
40
+ (resolved.roundingIncrement ?? 1) !== 1 ||
41
+ // Excel display rounds half away from zero, which is Intl's `halfExpand` only.
42
+ (resolved.roundingMode ?? 'halfExpand') !== 'halfExpand' ||
43
+ resolved.style === 'unit' ||
44
+ (resolved.style === 'currency' &&
45
+ (resolved.currencyDisplay === 'name' || resolved.currencySign === 'accounting'));
46
+ if (unsupported)
47
+ return undefined;
48
+ const fraction = formatter.formatToParts(value).find((part) => part.type === 'fraction');
49
+ const decimals = fraction?.value.length ?? 0;
50
+ const grouping = resolved.useGrouping === false ? '0' : '#,##0';
51
+ const format = decimals > 0 ? `${grouping}.${'0'.repeat(decimals)}` : grouping;
52
+ if (resolved.style === 'percent')
53
+ return `${format}%`;
54
+ if (resolved.style === 'currency')
55
+ return `${format} "${resolved.currency}"`;
56
+ return format;
57
+ }
58
+ /**
59
+ * The date as Excel should show it. Excel dates have no zone, and `write-excel-file` reads a
60
+ * `Date` in UTC, so the wall clock of the zone the table shows is written as UTC. A `date`
61
+ * column drops the time (midnight); a `time` column keeps only the time of day, on Excel's day
62
+ * zero (1899-12-30), so times sort and compare by time alone.
63
+ */
64
+ export function toXlsxDate(date, part = 'date-time') {
65
+ const gregorian = toCalendar(date, new GregorianCalendar());
66
+ const year = gregorian.era === 'BC' ? 1 - gregorian.year : gregorian.year;
67
+ const result = new Date(0);
68
+ // Not Date.UTC: it reads years 0–99 as 1900–1999.
69
+ if (part === 'time')
70
+ result.setUTCFullYear(1899, 11, 30);
71
+ else
72
+ result.setUTCFullYear(year, gregorian.month - 1, gregorian.day);
73
+ if (part !== 'date' && 'hour' in gregorian) {
74
+ result.setUTCHours(gregorian.hour, gregorian.minute, gregorian.second, gregorian.millisecond);
75
+ }
76
+ return result;
77
+ }
78
+ function dateFormatFor(type, formats) {
79
+ if (type === 'time')
80
+ return formats.time;
81
+ if (type === 'date-time')
82
+ return formats.dateTime;
83
+ return formats.date;
84
+ }
85
+ /**
86
+ * One export cell as a typed sheet cell: numbers, dates and booleans stay computable in Excel;
87
+ * everything else — and any column with its own `cell` renderer but no `exportValue` — is the
88
+ * text the table shows. Typed cells can't hold formulas, so no `'` guard is needed.
89
+ */
90
+ export function toXlsxCell(column, cell, formats = DEFAULT_XLSX_DATE_FORMATS) {
91
+ const { type } = column;
92
+ if (column.hasCustomCell && !column.hasExportValue)
93
+ return textCell(cell);
94
+ if (type === undefined)
95
+ return textCell(cell);
96
+ if (NUMBER_TYPES.has(type)) {
97
+ if (isEmpty(cell.value))
98
+ return null;
99
+ const value = Number(cell.value);
100
+ if (!Number.isFinite(value))
101
+ return textCell(cell);
102
+ const format = xlsxNumberFormat(column, value);
103
+ return format === undefined ? textCell(cell) : { type: Number, value, format };
104
+ }
105
+ if (DATE_TYPES.has(type)) {
106
+ if (isEmpty(cell.value))
107
+ return null;
108
+ const formatOptions = column.formatOptions;
109
+ const date = readDateInZone(cell.value, dateCellTimeZone(formatOptions));
110
+ if (!date)
111
+ return textCell(cell);
112
+ return {
113
+ type: Date,
114
+ value: toXlsxDate(date, type),
115
+ format: dateFormatFor(type, formats)
116
+ };
117
+ }
118
+ if (type === 'boolean') {
119
+ // Like the table, only a real true/false is a checkbox; anything else shows `-`.
120
+ return typeof cell.value === 'boolean' ? { type: Boolean, value: cell.value } : null;
121
+ }
122
+ return textCell(cell);
123
+ }
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ let { class: className = '', ...restProps } = $props();
3
+ </script>
4
+
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ width="1em"
8
+ height="1em"
9
+ viewBox="0 0 256 256"
10
+ class={className}
11
+ {...restProps}
12
+ >
13
+ <path
14
+ fill="currentColor"
15
+ d="M224 144v64a8 8 0 0 1-8 8H40a8 8 0 0 1-8-8v-64a8 8 0 0 1 16 0v56h160v-56a8 8 0 0 1 16 0m-101.66 5.66a8 8 0 0 0 11.32 0l40-40a8 8 0 0 0-11.32-11.32L136 124.69V32a8 8 0 0 0-16 0v92.69L93.66 98.34a8 8 0 0 0-11.32 11.32Z"
16
+ />
17
+ </svg>
18
+ <!-- Icon from Phosphor by Phosphor Icons - https://github.com/phosphor-icons/core/blob/main/LICENSE -->
@@ -0,0 +1,5 @@
1
+ declare const PhDownloadSimple: import("svelte").Component<{
2
+ class?: string;
3
+ } & Record<string, any>, {}, "">;
4
+ type PhDownloadSimple = ReturnType<typeof PhDownloadSimple>;
5
+ export default PhDownloadSimple;
@@ -10,6 +10,7 @@ export { default as PhCaretRight } from './PhCaretRight.svelte';
10
10
  export { default as PhCaretUp } from './PhCaretUp.svelte';
11
11
  export { default as PhCheck } from './PhCheck.svelte';
12
12
  export { default as PhColumns } from './PhColumns.svelte';
13
+ export { default as PhDownloadSimple } from './PhDownloadSimple.svelte';
13
14
  export { default as PhEye } from './PhEye.svelte';
14
15
  export { default as PhEyeSlash } from './PhEyeSlash.svelte';
15
16
  export { default as PhFile } from './PhFile.svelte';
@@ -10,6 +10,7 @@ export { default as PhCaretRight } from './PhCaretRight.svelte';
10
10
  export { default as PhCaretUp } from './PhCaretUp.svelte';
11
11
  export { default as PhCheck } from './PhCheck.svelte';
12
12
  export { default as PhColumns } from './PhColumns.svelte';
13
+ export { default as PhDownloadSimple } from './PhDownloadSimple.svelte';
13
14
  export { default as PhEye } from './PhEye.svelte';
14
15
  export { default as PhEyeSlash } from './PhEyeSlash.svelte';
15
16
  export { default as PhFile } from './PhFile.svelte';
@@ -0,0 +1,13 @@
1
+ export type CsvOptions = {
2
+ /** Field separator. Defaults to `;`, which Excel expects under most European locales. */
3
+ separator?: string;
4
+ /**
5
+ * Prepend a UTF-8 BOM so Excel reads the file as UTF-8 (otherwise č/ć/š/đ/ž break).
6
+ * Defaults to `true`.
7
+ */
8
+ bom?: boolean;
9
+ };
10
+ /** Quotes a field when it holds a quote, the separator or a line break (RFC 4180). */
11
+ export declare function escapeCsvField(value: string, separator: string): string;
12
+ /** Serializes rows of text as CSV, records separated by CRLF. */
13
+ export declare function toCsv(rows: string[][], { separator, bom }?: CsvOptions): string;
@@ -0,0 +1,18 @@
1
+ const BOM = '';
2
+ /** Quotes a field when it holds a quote, the separator or a line break (RFC 4180). */
3
+ export function escapeCsvField(value, separator) {
4
+ if (value.includes('"') || value.includes(separator) || /[\r\n]/.test(value)) {
5
+ return `"${value.replaceAll('"', '""')}"`;
6
+ }
7
+ return value;
8
+ }
9
+ /** Serializes rows of text as CSV, records separated by CRLF. */
10
+ export function toCsv(rows, { separator = ';', bom = true } = {}) {
11
+ if (separator.length === 0 || /["\r\n]/.test(separator)) {
12
+ throw new Error(`Invalid CSV separator: ${JSON.stringify(separator)}`);
13
+ }
14
+ const body = rows
15
+ .map((row) => row.map((field) => escapeCsvField(field, separator)).join(separator))
16
+ .join('\r\n');
17
+ return bom ? BOM + body : body;
18
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Hands `blob` to the browser as a download named `filename`. Browser-only: call it from an
3
+ * event handler, never during SSR.
4
+ */
5
+ export declare function downloadBlob(blob: Blob, filename: string): void;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Hands `blob` to the browser as a download named `filename`. Browser-only: call it from an
3
+ * event handler, never during SSR.
4
+ */
5
+ export function downloadBlob(blob, filename) {
6
+ const url = URL.createObjectURL(blob);
7
+ const link = document.createElement('a');
8
+ link.href = url;
9
+ link.download = filename;
10
+ link.style.display = 'none';
11
+ document.body.append(link);
12
+ link.click();
13
+ link.remove();
14
+ // Revoking right away can cancel the download in some browsers (Safari); the URL only
15
+ // pins the blob in memory until then.
16
+ setTimeout(() => URL.revokeObjectURL(url), 10_000);
17
+ }
@@ -62,7 +62,12 @@ export function selectFilterOptions(facets, emptyLabel = '(empty)') {
62
62
  label: value,
63
63
  count
64
64
  }));
65
- options.sort((a, b) => a.label.localeCompare(b.label));
65
+ // A column whose values are all numbers sorts by magnitude (so 2 < 10, -5 < 1.5); anything
66
+ // else sorts by label with digit runs compared numerically ("Item 2" < "Item 10").
67
+ const allNumbers = [...facets.keys()].every((key) => isEmpty(key) || Number.isFinite(key));
68
+ options.sort(allNumbers
69
+ ? (a, b) => Number(a.value) - Number(b.value)
70
+ : (a, b) => a.label.localeCompare(b.label, undefined, { numeric: true }));
66
71
  if (emptyCount > 0) {
67
72
  options.push({ value: EMPTY_SELECT_VALUE, label: emptyLabel, count: emptyCount });
68
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compote-ui",
3
- "version": "0.83.0",
3
+ "version": "0.85.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -53,15 +53,23 @@
53
53
  "types": "./dist/components/data-table-v9/virtual/index.d.ts",
54
54
  "svelte": "./dist/components/data-table-v9/virtual/index.js"
55
55
  },
56
+ "./data-table/xlsx": {
57
+ "types": "./dist/components/data-table-v9/xlsx/index.d.ts",
58
+ "svelte": "./dist/components/data-table-v9/xlsx/index.js"
59
+ },
56
60
  "./theme.css": "./dist/theme.css"
57
61
  },
58
62
  "peerDependencies": {
59
63
  "@tanstack/svelte-table": ">=9.1.0 <10",
60
- "svelte": "^5.0.0"
64
+ "svelte": "^5.0.0",
65
+ "write-excel-file": "^4.1.0"
61
66
  },
62
67
  "peerDependenciesMeta": {
63
68
  "@tanstack/svelte-table": {
64
69
  "optional": true
70
+ },
71
+ "write-excel-file": {
72
+ "optional": true
65
73
  }
66
74
  },
67
75
  "devDependencies": {
@@ -80,6 +88,7 @@
80
88
  "eslint": "^10.12.0",
81
89
  "eslint-config-prettier": "^10.1.8",
82
90
  "eslint-plugin-svelte": "^3.23.1",
91
+ "fflate": "^0.8.2",
83
92
  "globals": "^17.13.0",
84
93
  "prettier": "^3.9.10",
85
94
  "prettier-plugin-svelte": "^4.1.1",
@@ -94,7 +103,8 @@
94
103
  "unplugin-icons": "^24.0.0",
95
104
  "vite": "^8.3.4",
96
105
  "vitest": "^5.0.4",
97
- "wrangler": "^4.151.0"
106
+ "wrangler": "^4.151.0",
107
+ "write-excel-file": "^4.1.1"
98
108
  },
99
109
  "keywords": [
100
110
  "svelte",
@@ -1,14 +1,15 @@
1
1
  ---
2
2
  name: data-table
3
3
  description: >
4
- Load when using compote-ui/data-table or compote-ui/data-table/virtual.
5
- Covers TanStack Table v9 wrapper usage, createTable,
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
6
  createDataTableColumnHelper, table columns, row selection, filters,
7
- formatting, toolbars, virtualized imports, and optional peer dependencies.
7
+ formatting, toolbars, CSV/Excel export, virtualized imports, and optional
8
+ peer dependencies.
8
9
  metadata:
9
10
  type: composition
10
11
  library: compote-ui
11
- library_version: '0.81.0'
12
+ library_version: '0.84.0'
12
13
  requires:
13
14
  - component-usage
14
15
  - theming
@@ -27,6 +28,12 @@ sources:
27
28
  - src/lib/components/data-table-v9/column-filter-editing.svelte.ts
28
29
  - src/lib/utils/select-filter.ts
29
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
30
37
  - src/lib/components/data-table-v9/virtual/data-table-virtualized.svelte
31
38
  - src/lib/components/data-table-v9/virtual/data-table-virtual-rows.svelte
32
39
  ---
@@ -266,6 +273,25 @@ spins its icon and sets `aria-busy` until the promise settles. Clicks while busy
266
273
  Props: `onRefresh: () => void | Promise<unknown>`, `loading?: boolean`, `triggerLabel?: string`
267
274
  (icon-only with `aria-label="Refresh"` and a tooltip when omitted). Errors thrown by `onRefresh` are not caught.
268
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
+
269
295
  ## Common Mistakes
270
296
 
271
297
  ### CRITICAL Missing id for accessorFn column
@@ -355,8 +381,20 @@ The virtual subpath imports `@tanstack/svelte-virtual`.
355
381
 
356
382
  Source: `package.json`, `src/lib/components/data-table-v9/virtual/data-table-virtual-rows.svelte`
357
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
+
358
395
  ## References
359
396
 
360
397
  - [Column options](references/column-options.md)
361
398
  - [Table state and reactivity](references/table-state-and-reactivity.md)
362
399
  - [Virtual table](references/virtual-table.md)
400
+ - [Export](references/export.md)
@@ -40,6 +40,10 @@ Common options:
40
40
  - `grow?: boolean`
41
41
  - `sum?: boolean`
42
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'`.
43
47
 
44
48
  Type defaults:
45
49