compote-ui 0.79.2 → 0.80.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.
- package/dist/components/data-table-v9/column-filter-editing.svelte.d.ts +41 -0
- package/dist/components/data-table-v9/column-filter-editing.svelte.js +88 -0
- package/dist/components/data-table-v9/column-filter-kind.d.ts +10 -0
- package/dist/components/data-table-v9/column-filter-kind.js +25 -0
- package/dist/components/data-table-v9/create-table.svelte.js +37 -25
- package/dist/components/data-table-v9/data-table-head.svelte +230 -201
- package/dist/components/data-table-v9/data-table-head.svelte.d.ts +2 -0
- package/dist/components/data-table-v9/data-table-utils.d.ts +7 -0
- package/dist/components/data-table-v9/data-table-utils.js +4 -0
- package/dist/components/data-table-v9/data-table.svelte +154 -151
- package/dist/components/data-table-v9/data-table.svelte.d.ts +2 -0
- package/dist/components/data-table-v9/date-cell.d.ts +13 -0
- package/dist/components/data-table-v9/date-cell.js +38 -0
- package/dist/components/data-table-v9/filter-editors/boolean-filter-editor.svelte +45 -0
- package/dist/components/data-table-v9/filter-editors/boolean-filter-editor.svelte.d.ts +7 -0
- package/dist/components/data-table-v9/filter-editors/column-filter-editor.svelte +33 -0
- package/dist/components/data-table-v9/filter-editors/column-filter-editor.svelte.d.ts +30 -0
- package/dist/components/data-table-v9/filter-editors/date-range-filter-editor.svelte +52 -0
- package/dist/components/data-table-v9/filter-editors/date-range-filter-editor.svelte.d.ts +28 -0
- package/dist/components/data-table-v9/filter-editors/range-filter-editor.svelte +51 -0
- package/dist/components/data-table-v9/filter-editors/range-filter-editor.svelte.d.ts +30 -0
- package/dist/components/data-table-v9/filter-editors/select-filter-editor.svelte +59 -0
- package/dist/components/data-table-v9/filter-editors/select-filter-editor.svelte.d.ts +28 -0
- package/dist/components/data-table-v9/filter-editors/text-filter-editor.svelte +20 -0
- package/dist/components/data-table-v9/filter-editors/text-filter-editor.svelte.d.ts +8 -0
- package/dist/components/data-table-v9/header/column-header-filter.svelte +66 -0
- package/dist/components/data-table-v9/header/column-header-filter.svelte.d.ts +30 -0
- package/dist/components/data-table-v9/table-view-state.svelte.d.ts +1 -0
- package/dist/components/data-table-v9/table-view-state.svelte.js +3 -0
- package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte +203 -459
- package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte.d.ts +1 -1
- package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte +82 -79
- package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte.d.ts +2 -0
- package/dist/components/dialog/alert-dialog.svelte +73 -71
- package/dist/components/dialog/dialog-root.svelte +45 -43
- package/dist/components/drawer/drawer-content.svelte +213 -211
- package/dist/components/popover/popover-content.svelte +97 -71
- package/dist/components/popover/popover-content.svelte.d.ts +2 -1
- package/dist/components/popover/popover-description.svelte +19 -15
- package/dist/components/popover/popover-labels.svelte.d.ts +18 -0
- package/dist/components/popover/popover-labels.svelte.js +35 -0
- package/dist/components/popover/popover-root.svelte +27 -24
- package/dist/components/popover/popover-title.svelte +19 -15
- package/dist/icons/PhFunnelFill.svelte +19 -0
- package/dist/icons/PhFunnelFill.svelte.d.ts +5 -0
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/utils/date-filter.d.ts +31 -0
- package/dist/utils/date-filter.js +58 -0
- package/dist/utils/date.d.ts +19 -1
- package/dist/utils/date.js +48 -1
- package/dist/utils/release-focus-on-exit.d.ts +22 -0
- package/dist/utils/release-focus-on-exit.js +26 -0
- package/package.json +1 -1
- package/skills/component-usage/references/overlays-and-floating-ui.md +278 -273
- package/skills/data-table/SKILL.md +314 -283
- package/skills/data-table/references/column-options.md +175 -150
- package/skills/data-table/references/table-state-and-reactivity.md +65 -60
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** The slice of a TanStack column the filter editors read and write. */
|
|
2
|
+
export type FilterableColumn = {
|
|
3
|
+
id: string;
|
|
4
|
+
getFilterValue(): unknown;
|
|
5
|
+
setFilterValue(value: unknown): void;
|
|
6
|
+
};
|
|
7
|
+
export type ColumnFilterEditingOptions = {
|
|
8
|
+
/** The table the edited columns belong to; read on every access so it may change. */
|
|
9
|
+
table: () => object;
|
|
10
|
+
/**
|
|
11
|
+
* Whether `columnId` still has an editor. An editor torn down by an outside change can
|
|
12
|
+
* still emit — Ark's NumberInput commits the typed number on the blur its unmount
|
|
13
|
+
* triggers — and that must not write back a filter the app just cleared.
|
|
14
|
+
*/
|
|
15
|
+
isActive?: (columnId: string) => boolean;
|
|
16
|
+
};
|
|
17
|
+
/** Drops every pending filter edit on `table` without committing, whichever host made it. */
|
|
18
|
+
export declare function cancelPendingFilterEdits(table: object): void;
|
|
19
|
+
/**
|
|
20
|
+
* One host's access to a table's pending filter edits (see PendingFilterEdits). The edits
|
|
21
|
+
* live with the table, not with the host or its editors, so an edit still commits after its
|
|
22
|
+
* editor unmounts with a closing popover.
|
|
23
|
+
*
|
|
24
|
+
* A control shows its pending edit if there is one, otherwise the column's live filter
|
|
25
|
+
* value — so a filter changed from outside (the app calling setFilterValue /
|
|
26
|
+
* setColumnFilters / resetColumnFilters) never leaves a stale value in the inputs, an edit
|
|
27
|
+
* pending during that change is dropped rather than re-applied, and committing one range
|
|
28
|
+
* bound never restores the other's old value.
|
|
29
|
+
*/
|
|
30
|
+
export declare class ColumnFilterEditing {
|
|
31
|
+
#private;
|
|
32
|
+
constructor(options: ColumnFilterEditingOptions);
|
|
33
|
+
textValue(column: FilterableColumn): string;
|
|
34
|
+
editText(column: FilterableColumn, value: string): void;
|
|
35
|
+
rangeBound(column: FilterableColumn, which: 'min' | 'max'): number | null;
|
|
36
|
+
editRange(column: FilterableColumn, which: 'min' | 'max', value: number | null): void;
|
|
37
|
+
/** Drops the pending edits for one column without committing them. */
|
|
38
|
+
cancel(columnId: string): void;
|
|
39
|
+
/** Drops every pending edit on the table without committing — including other hosts'. */
|
|
40
|
+
cancelAll(): void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { DebouncedFilterEdits } from '../../utils/debounced-filter-edit.svelte';
|
|
2
|
+
import { mergeRangeFilter, rangeBounds } from '../../utils/range-filter';
|
|
3
|
+
import { getFilterRevisions } from './filter-revisions.svelte';
|
|
4
|
+
/**
|
|
5
|
+
* The debounced edits pending on one table's filters. Shared by every component that edits
|
|
6
|
+
* them (the toolbar filter, each header filter), so the latest edit to a column wins no
|
|
7
|
+
* matter where it was made, and cancelling reaches edits made from any of them.
|
|
8
|
+
*/
|
|
9
|
+
class PendingFilterEdits {
|
|
10
|
+
text = new DebouncedFilterEdits();
|
|
11
|
+
range = new DebouncedFilterEdits();
|
|
12
|
+
}
|
|
13
|
+
const registry = new WeakMap();
|
|
14
|
+
function pendingEdits(table) {
|
|
15
|
+
let edits = registry.get(table);
|
|
16
|
+
if (!edits) {
|
|
17
|
+
edits = new PendingFilterEdits();
|
|
18
|
+
registry.set(table, edits);
|
|
19
|
+
}
|
|
20
|
+
return edits;
|
|
21
|
+
}
|
|
22
|
+
/** Drops every pending filter edit on `table` without committing, whichever host made it. */
|
|
23
|
+
export function cancelPendingFilterEdits(table) {
|
|
24
|
+
const edits = registry.get(table);
|
|
25
|
+
edits?.text.cancelAll();
|
|
26
|
+
edits?.range.cancelAll();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One host's access to a table's pending filter edits (see PendingFilterEdits). The edits
|
|
30
|
+
* live with the table, not with the host or its editors, so an edit still commits after its
|
|
31
|
+
* editor unmounts with a closing popover.
|
|
32
|
+
*
|
|
33
|
+
* A control shows its pending edit if there is one, otherwise the column's live filter
|
|
34
|
+
* value — so a filter changed from outside (the app calling setFilterValue /
|
|
35
|
+
* setColumnFilters / resetColumnFilters) never leaves a stale value in the inputs, an edit
|
|
36
|
+
* pending during that change is dropped rather than re-applied, and committing one range
|
|
37
|
+
* bound never restores the other's old value.
|
|
38
|
+
*/
|
|
39
|
+
export class ColumnFilterEditing {
|
|
40
|
+
#table;
|
|
41
|
+
#isActive;
|
|
42
|
+
constructor(options) {
|
|
43
|
+
this.#table = options.table;
|
|
44
|
+
this.#isActive = options.isActive ?? (() => true);
|
|
45
|
+
}
|
|
46
|
+
get #edits() {
|
|
47
|
+
return pendingEdits(this.#table());
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* What an edit is based on: the column's filter revision, which only ever goes up, so a
|
|
51
|
+
* filter cleared and set back to its old value still makes the edit stale. Tables not
|
|
52
|
+
* made by compote's createTable have no revisions; fall back to the filter value.
|
|
53
|
+
*/
|
|
54
|
+
#base(column) {
|
|
55
|
+
const revisions = getFilterRevisions(this.#table());
|
|
56
|
+
return revisions ? revisions.get(column.id) : column.getFilterValue();
|
|
57
|
+
}
|
|
58
|
+
textValue(column) {
|
|
59
|
+
const value = column.getFilterValue();
|
|
60
|
+
return (this.#edits.text.get(column.id, this.#base(column)) ??
|
|
61
|
+
(typeof value === 'string' ? value : ''));
|
|
62
|
+
}
|
|
63
|
+
editText(column, value) {
|
|
64
|
+
if (!this.#isActive(column.id))
|
|
65
|
+
return;
|
|
66
|
+
this.#edits.text.edit(column.id, this.#base(column), () => value, () => this.#base(column), (text) => column.setFilterValue(text || undefined));
|
|
67
|
+
}
|
|
68
|
+
rangeBound(column, which) {
|
|
69
|
+
const pending = this.#edits.range.get(column.id, this.#base(column));
|
|
70
|
+
if (pending && which in pending)
|
|
71
|
+
return pending[which] ?? null;
|
|
72
|
+
return rangeBounds(column.getFilterValue())[which === 'min' ? 0 : 1] ?? null;
|
|
73
|
+
}
|
|
74
|
+
editRange(column, which, value) {
|
|
75
|
+
if (!this.#isActive(column.id))
|
|
76
|
+
return;
|
|
77
|
+
this.#edits.range.edit(column.id, this.#base(column), (pending) => ({ ...pending, [which]: value }), () => this.#base(column), (pending) => column.setFilterValue(mergeRangeFilter(column.getFilterValue(), pending)));
|
|
78
|
+
}
|
|
79
|
+
/** Drops the pending edits for one column without committing them. */
|
|
80
|
+
cancel(columnId) {
|
|
81
|
+
this.#edits.text.cancel(columnId);
|
|
82
|
+
this.#edits.range.cancel(columnId);
|
|
83
|
+
}
|
|
84
|
+
/** Drops every pending edit on the table without committing — including other hosts'. */
|
|
85
|
+
cancelAll() {
|
|
86
|
+
cancelPendingFilterEdits(this.#table());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DataTableColumnType } from './types';
|
|
2
|
+
/** Which filter editor (and default filterFn) a column type uses. */
|
|
3
|
+
export type ColumnFilterKind = 'text' | 'range' | 'boolean' | 'select' | 'date';
|
|
4
|
+
/**
|
|
5
|
+
* The filter kind for a column type. The single mapping both the filter editors and the
|
|
6
|
+
* default filterFn follow, so a column never shows an editor its filterFn can't evaluate.
|
|
7
|
+
* `action` columns carry no value and get no filter. `time` stays a text filter until it has
|
|
8
|
+
* its own contract (a time-of-day range has to decide how to cross midnight).
|
|
9
|
+
*/
|
|
10
|
+
export declare function filterKindFor(type: DataTableColumnType | undefined): ColumnFilterKind | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The filter kind for a column type. The single mapping both the filter editors and the
|
|
3
|
+
* default filterFn follow, so a column never shows an editor its filterFn can't evaluate.
|
|
4
|
+
* `action` columns carry no value and get no filter. `time` stays a text filter until it has
|
|
5
|
+
* its own contract (a time-of-day range has to decide how to cross midnight).
|
|
6
|
+
*/
|
|
7
|
+
export function filterKindFor(type) {
|
|
8
|
+
switch (type) {
|
|
9
|
+
case 'number':
|
|
10
|
+
case 'currency':
|
|
11
|
+
case 'percent':
|
|
12
|
+
return 'range';
|
|
13
|
+
case 'boolean':
|
|
14
|
+
return 'boolean';
|
|
15
|
+
case 'select':
|
|
16
|
+
return 'select';
|
|
17
|
+
case 'date':
|
|
18
|
+
case 'date-time':
|
|
19
|
+
return 'date';
|
|
20
|
+
case 'action':
|
|
21
|
+
return null;
|
|
22
|
+
default:
|
|
23
|
+
return 'text';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -4,6 +4,10 @@ import { onDestroy } from 'svelte';
|
|
|
4
4
|
import { renderComponent, renderSnippet } from '@tanstack/svelte-table';
|
|
5
5
|
import { dataTableFeatures } from './features';
|
|
6
6
|
import { TYPE_NUMBER_FORMAT_DEFAULTS } from './data-table-utils';
|
|
7
|
+
import { filterKindFor } from './column-filter-kind';
|
|
8
|
+
import { dateCellTimeZone, formatDateCell } from './date-cell';
|
|
9
|
+
import { isEmptyDateRange, matchesDateRange, resolveDateRange } from '../../utils/date-filter';
|
|
10
|
+
import { cancelPendingFilterEdits } from './column-filter-editing.svelte';
|
|
7
11
|
import { changedFilterIds, FilterRevisions, registerFilterRevisions } from './filter-revisions.svelte';
|
|
8
12
|
import { formatSelectValue, matchesSelectFilter, selectCellValues } from '../../utils/select-filter';
|
|
9
13
|
const oneOfFilterFn = (row, columnId, filterValue) => {
|
|
@@ -90,6 +94,22 @@ export function createTable(options) {
|
|
|
90
94
|
onDestroy(() => subscription.unsubscribe());
|
|
91
95
|
}
|
|
92
96
|
registerFilterRevisions(table, filterRevisions);
|
|
97
|
+
// Debounced filter edits outlive the editors that made them, but not the table.
|
|
98
|
+
onDestroy(() => cancelPendingFilterEdits(table));
|
|
99
|
+
// A reset drops pending filter edits too — including one on a column with no committed
|
|
100
|
+
// filter, whose revision the reset leaves alone, so the edit would otherwise commit
|
|
101
|
+
// afterwards and bring a filter back. TanStack assigns these as own methods of the table
|
|
102
|
+
// (`table.reset` also writes state directly, bypassing onColumnFiltersChange).
|
|
103
|
+
const resetColumnFilters = table.resetColumnFilters;
|
|
104
|
+
table.resetColumnFilters = (defaultState) => {
|
|
105
|
+
cancelPendingFilterEdits(table);
|
|
106
|
+
resetColumnFilters(defaultState);
|
|
107
|
+
};
|
|
108
|
+
const resetTable = table.reset;
|
|
109
|
+
table.reset = () => {
|
|
110
|
+
cancelPendingFilterEdits(table);
|
|
111
|
+
resetTable();
|
|
112
|
+
};
|
|
93
113
|
return table;
|
|
94
114
|
}
|
|
95
115
|
function createColumnVisibility(columns) {
|
|
@@ -128,7 +148,7 @@ function createColumns(columns, localeCtx) {
|
|
|
128
148
|
};
|
|
129
149
|
}
|
|
130
150
|
const columnId = getColumnId(column);
|
|
131
|
-
const derivedFilterFn = column.filterFn ?? getFilterFnForType(column
|
|
151
|
+
const derivedFilterFn = column.filterFn ?? getFilterFnForType(column);
|
|
132
152
|
const columnDef = {
|
|
133
153
|
id: columnId,
|
|
134
154
|
header: column.header,
|
|
@@ -191,27 +211,26 @@ export function getColumnId(column) {
|
|
|
191
211
|
return column.accessorKey;
|
|
192
212
|
throw new Error('DataTableColumn with accessorFn requires an id.');
|
|
193
213
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
case 'number':
|
|
208
|
-
case 'currency':
|
|
209
|
-
case 'percent':
|
|
214
|
+
/**
|
|
215
|
+
* A calendar-day range filter for a date column, judged in the zone the column is shown in
|
|
216
|
+
* (read on every pass, so it follows a changed local zone like the cells do).
|
|
217
|
+
*/
|
|
218
|
+
function dateRangeFilterFn(formatOptions) {
|
|
219
|
+
const filterFn = (row, columnId, range) => matchesDateRange(row.getValue(columnId), range, dateCellTimeZone(formatOptions));
|
|
220
|
+
filterFn.resolveFilterValue = resolveDateRange;
|
|
221
|
+
filterFn.autoRemove = isEmptyDateRange;
|
|
222
|
+
return filterFn;
|
|
223
|
+
}
|
|
224
|
+
function getFilterFnForType(column) {
|
|
225
|
+
switch (filterKindFor(column.type)) {
|
|
226
|
+
case 'range':
|
|
210
227
|
return 'inNumberRange';
|
|
211
228
|
case 'boolean':
|
|
212
229
|
return 'equals';
|
|
213
230
|
case 'select':
|
|
214
231
|
return oneOfFilterFn;
|
|
232
|
+
case 'date':
|
|
233
|
+
return dateRangeFilterFn(column.formatOptions);
|
|
215
234
|
default:
|
|
216
235
|
return undefined;
|
|
217
236
|
}
|
|
@@ -231,14 +250,7 @@ function applyTypeFormat(column, value, localeCtx) {
|
|
|
231
250
|
}).format(numericValue);
|
|
232
251
|
}
|
|
233
252
|
if (column.type === 'date' || column.type === 'time' || column.type === 'date-time') {
|
|
234
|
-
|
|
235
|
-
if (isNaN(dateValue.getTime()))
|
|
236
|
-
return undefined;
|
|
237
|
-
const locale = column.formatLocale ?? localeCtx().locale;
|
|
238
|
-
return new Intl.DateTimeFormat(locale, {
|
|
239
|
-
...TYPE_DATE_FORMAT_DEFAULTS[column.type],
|
|
240
|
-
...column.formatOptions
|
|
241
|
-
}).format(dateValue);
|
|
253
|
+
return formatDateCell(value, column.type, column.formatLocale ?? localeCtx().locale, column.formatOptions);
|
|
242
254
|
}
|
|
243
255
|
if (column.type === 'boolean')
|
|
244
256
|
return value ? 'Yes' : 'No';
|