compote-ui 0.55.4 → 0.56.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-helper.d.ts +12 -0
- package/dist/components/data-table-v9/column-helper.js +42 -0
- package/dist/components/data-table-v9/create-table.svelte.d.ts +42 -0
- package/dist/components/data-table-v9/create-table.svelte.js +248 -0
- package/dist/components/data-table-v9/data-table-cell-content.svelte +66 -0
- package/dist/components/data-table-v9/data-table-cell-content.svelte.d.ts +28 -0
- package/dist/components/data-table-v9/data-table-foot.svelte +111 -0
- package/dist/components/data-table-v9/data-table-foot.svelte.d.ts +32 -0
- package/dist/components/{data-table/data-table-head.svelte.md → data-table-v9/data-table-head.svelte} +47 -39
- package/dist/components/data-table-v9/data-table-head.svelte.d.ts +32 -0
- package/dist/components/data-table-v9/data-table-title.svelte.d.ts +10 -0
- package/dist/components/data-table-v9/data-table-utils.d.ts +53 -0
- package/dist/components/data-table-v9/data-table-utils.js +181 -0
- package/dist/components/data-table-v9/data-table.svelte +151 -0
- package/dist/components/data-table-v9/data-table.svelte.d.ts +41 -0
- package/dist/components/data-table-v9/features.d.ts +12 -0
- package/dist/components/data-table-v9/features.js +14 -0
- package/dist/components/data-table-v9/index.d.ts +11 -0
- package/dist/components/data-table-v9/index.js +9 -0
- package/dist/components/data-table-v9/table-view-state.svelte.d.ts +74 -0
- package/dist/components/data-table-v9/table-view-state.svelte.js +182 -0
- package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte +380 -0
- package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte.d.ts +29 -0
- package/dist/components/data-table-v9/toolbar/data-table-column-visibility.svelte +73 -0
- package/dist/components/data-table-v9/toolbar/data-table-column-visibility.svelte.d.ts +29 -0
- package/dist/components/data-table-v9/toolbar/data-table-search.svelte +58 -0
- package/dist/components/data-table-v9/toolbar/data-table-search.svelte.d.ts +32 -0
- package/dist/components/{data-table/data-table-toolbar.svelte.md → data-table-v9/toolbar/data-table-toolbar.svelte} +14 -15
- package/dist/components/data-table-v9/toolbar/data-table-toolbar.svelte.d.ts +12 -0
- package/dist/components/data-table-v9/types.d.ts +74 -0
- package/dist/components/data-table-v9/types.js +1 -0
- package/dist/components/data-table-v9/virtual/data-table-virtual-rows.svelte +131 -0
- package/dist/components/data-table-v9/virtual/data-table-virtual-rows.svelte.d.ts +40 -0
- package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte +79 -0
- package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte.d.ts +41 -0
- package/dist/components/data-table-v9/virtual/index.d.ts +3 -0
- package/dist/components/data-table-v9/virtual/index.js +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/package.json +12 -2
- package/dist/components/data-table/column-helper.ts.md +0 -96
- package/dist/components/data-table/create-table.ts.md +0 -386
- package/dist/components/data-table/data-table-column-filter.svelte.md +0 -249
- package/dist/components/data-table/data-table-column-visibility.svelte.md +0 -74
- package/dist/components/data-table/data-table-new.svelte.md +0 -245
- package/dist/components/data-table/data-table-utils.ts.md +0 -179
- package/dist/components/data-table/data-table-virtual-rows.svelte.md +0 -171
- package/dist/components/data-table/data-table-virtualized.svelte.md +0 -108
- package/dist/components/data-table/data-table.svelte.md +0 -214
- package/dist/components/data-table/index.ts.md +0 -22
- package/dist/components/data-table/types.ts.md +0 -101
- package/dist/components/data-table/virtual/index.ts.md +0 -26
- /package/dist/components/{data-table/data-table-title.svelte.md → data-table-v9/data-table-title.svelte} +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { tableFeatures, rowSortingFeature, rowSelectionFeature, columnFilteringFeature, columnFacetingFeature, globalFilteringFeature, columnVisibilityFeature, columnPinningFeature, columnSizingFeature, columnResizingFeature } from '@tanstack/svelte-table';
|
|
2
|
+
// Fixed feature registry for the data table. Declared once at module scope so the
|
|
3
|
+
// reference is stable across instances (v9 requires a stable `features` object).
|
|
4
|
+
export const dataTableFeatures = tableFeatures({
|
|
5
|
+
rowSortingFeature,
|
|
6
|
+
rowSelectionFeature,
|
|
7
|
+
columnFilteringFeature,
|
|
8
|
+
columnFacetingFeature,
|
|
9
|
+
globalFilteringFeature,
|
|
10
|
+
columnVisibilityFeature,
|
|
11
|
+
columnPinningFeature,
|
|
12
|
+
columnSizingFeature,
|
|
13
|
+
columnResizingFeature
|
|
14
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { createDataTableColumnHelper } from './column-helper';
|
|
2
|
+
export { createTable } from './create-table.svelte';
|
|
3
|
+
export { renderComponent, renderSnippet, FlexRender } from '@tanstack/svelte-table';
|
|
4
|
+
export { default as Root } from './data-table.svelte';
|
|
5
|
+
export { default as Title } from './data-table-title.svelte';
|
|
6
|
+
export { default as Toolbar } from './toolbar/data-table-toolbar.svelte';
|
|
7
|
+
export { default as ColumnFilter } from './toolbar/data-table-column-filter.svelte';
|
|
8
|
+
export { default as ColumnVisibility } from './toolbar/data-table-column-visibility.svelte';
|
|
9
|
+
export { default as Search } from './toolbar/data-table-search.svelte';
|
|
10
|
+
export type { CreateDataTableOptions, DataTableInstance } from './create-table.svelte';
|
|
11
|
+
export type { DataTableAlign, DataTableAccessorFnColumn, DataTableAccessorKeyColumn, DataTableColumn, DataTableColumnBase, DataTableColumnOptions, DataTableColumnType, DataTableCellPropsResolver, DataTableCellRenderProps, DataTableGroupColumn, DataTableLeafColumnBase, DataTableLeafColumn } from './types';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createDataTableColumnHelper } from './column-helper';
|
|
2
|
+
export { createTable } from './create-table.svelte';
|
|
3
|
+
export { renderComponent, renderSnippet, FlexRender } from '@tanstack/svelte-table';
|
|
4
|
+
export { default as Root } from './data-table.svelte';
|
|
5
|
+
export { default as Title } from './data-table-title.svelte';
|
|
6
|
+
export { default as Toolbar } from './toolbar/data-table-toolbar.svelte';
|
|
7
|
+
export { default as ColumnFilter } from './toolbar/data-table-column-filter.svelte';
|
|
8
|
+
export { default as ColumnVisibility } from './toolbar/data-table-column-visibility.svelte';
|
|
9
|
+
export { default as Search } from './toolbar/data-table-search.svelte';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Header, HeaderGroup, RowData } from '@tanstack/svelte-table';
|
|
2
|
+
import type { DataTableInstance } from './data-table-utils';
|
|
3
|
+
import type { DataTableFeatures } from './features';
|
|
4
|
+
export type DataTableViewState<T extends RowData> = ReturnType<typeof createTableViewState<T>>;
|
|
5
|
+
export type DataTableHeaderSection = 'left' | 'center' | 'right';
|
|
6
|
+
/**
|
|
7
|
+
* Shared derived state for the standard and virtualized table roots.
|
|
8
|
+
*
|
|
9
|
+
* Writable slice atoms are rune-backed, so reading them inside a $derived
|
|
10
|
+
* registers the dependency. Derived table APIs (getRowModel, header groups,
|
|
11
|
+
* getVisibleLeafColumns, …) are not reliably tracked by the beta adapter, so
|
|
12
|
+
* each derived below first reads the slices its result depends on.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createTableViewState<T extends RowData>(getTable: () => DataTableInstance<T>): {
|
|
15
|
+
readonly table: DataTableInstance<T>;
|
|
16
|
+
readonly columnPinning: import("@tanstack/svelte-table").ColumnPinningState;
|
|
17
|
+
readonly columnResizing: import("@tanstack/svelte-table").columnResizingState;
|
|
18
|
+
readonly columnSizing: import("@tanstack/svelte-table").ColumnSizingState;
|
|
19
|
+
readonly columnVisibility: import("@tanstack/svelte-table").ColumnVisibilityState;
|
|
20
|
+
readonly rowSelection: import("@tanstack/svelte-table").RowSelectionState;
|
|
21
|
+
readonly sorting: import("@tanstack/svelte-table").SortingState;
|
|
22
|
+
readonly rowModel: import("@tanstack/svelte-table").RowModel<{
|
|
23
|
+
rowSortingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
24
|
+
rowSelectionFeature: import("@tanstack/svelte-table").TableFeature;
|
|
25
|
+
columnFilteringFeature: import("@tanstack/svelte-table").TableFeature;
|
|
26
|
+
columnFacetingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
27
|
+
globalFilteringFeature: import("@tanstack/svelte-table").TableFeature;
|
|
28
|
+
columnVisibilityFeature: import("@tanstack/svelte-table").TableFeature;
|
|
29
|
+
columnPinningFeature: import("@tanstack/svelte-table").TableFeature;
|
|
30
|
+
columnSizingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
31
|
+
columnResizingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
32
|
+
}, T>;
|
|
33
|
+
readonly headerGroups: HeaderGroup<{
|
|
34
|
+
rowSortingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
35
|
+
rowSelectionFeature: import("@tanstack/svelte-table").TableFeature;
|
|
36
|
+
columnFilteringFeature: import("@tanstack/svelte-table").TableFeature;
|
|
37
|
+
columnFacetingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
38
|
+
globalFilteringFeature: import("@tanstack/svelte-table").TableFeature;
|
|
39
|
+
columnVisibilityFeature: import("@tanstack/svelte-table").TableFeature;
|
|
40
|
+
columnPinningFeature: import("@tanstack/svelte-table").TableFeature;
|
|
41
|
+
columnSizingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
42
|
+
columnResizingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
43
|
+
}, T>[];
|
|
44
|
+
getHeaderSection(header: Header<DataTableFeatures, T, unknown>): DataTableHeaderSection | undefined;
|
|
45
|
+
readonly visibleLeafColumns: import("@tanstack/svelte-table").Column<{
|
|
46
|
+
rowSortingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
47
|
+
rowSelectionFeature: import("@tanstack/svelte-table").TableFeature;
|
|
48
|
+
columnFilteringFeature: import("@tanstack/svelte-table").TableFeature;
|
|
49
|
+
columnFacetingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
50
|
+
globalFilteringFeature: import("@tanstack/svelte-table").TableFeature;
|
|
51
|
+
columnVisibilityFeature: import("@tanstack/svelte-table").TableFeature;
|
|
52
|
+
columnPinningFeature: import("@tanstack/svelte-table").TableFeature;
|
|
53
|
+
columnSizingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
54
|
+
columnResizingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
55
|
+
}, T, unknown>[];
|
|
56
|
+
readonly growColumn: import("@tanstack/svelte-table").Column<{
|
|
57
|
+
rowSortingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
58
|
+
rowSelectionFeature: import("@tanstack/svelte-table").TableFeature;
|
|
59
|
+
columnFilteringFeature: import("@tanstack/svelte-table").TableFeature;
|
|
60
|
+
columnFacetingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
61
|
+
globalFilteringFeature: import("@tanstack/svelte-table").TableFeature;
|
|
62
|
+
columnVisibilityFeature: import("@tanstack/svelte-table").TableFeature;
|
|
63
|
+
columnPinningFeature: import("@tanstack/svelte-table").TableFeature;
|
|
64
|
+
columnSizingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
65
|
+
columnResizingFeature: import("@tanstack/svelte-table").TableFeature;
|
|
66
|
+
}, T, unknown> | undefined;
|
|
67
|
+
readonly hasGrowColumn: boolean;
|
|
68
|
+
readonly isRowSelectionEnabled: boolean;
|
|
69
|
+
readonly isMultiRowSelectionEnabled: boolean;
|
|
70
|
+
readonly allRowsSelectionState: boolean | "indeterminate";
|
|
71
|
+
readonly selectedRowCount: number;
|
|
72
|
+
readonly isColumnResizing: boolean;
|
|
73
|
+
readonly hasFooter: boolean;
|
|
74
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { getColumnMeta } from './data-table-utils';
|
|
2
|
+
/**
|
|
3
|
+
* Shared derived state for the standard and virtualized table roots.
|
|
4
|
+
*
|
|
5
|
+
* Writable slice atoms are rune-backed, so reading them inside a $derived
|
|
6
|
+
* registers the dependency. Derived table APIs (getRowModel, header groups,
|
|
7
|
+
* getVisibleLeafColumns, …) are not reliably tracked by the beta adapter, so
|
|
8
|
+
* each derived below first reads the slices its result depends on.
|
|
9
|
+
*/
|
|
10
|
+
export function createTableViewState(getTable) {
|
|
11
|
+
const table = $derived.by(getTable);
|
|
12
|
+
const columnPinning = $derived.by(() => table.atoms.columnPinning.get());
|
|
13
|
+
const columnResizing = $derived.by(() => table.atoms.columnResizing.get());
|
|
14
|
+
const columnSizing = $derived.by(() => table.atoms.columnSizing.get());
|
|
15
|
+
const columnVisibility = $derived.by(() => table.atoms.columnVisibility.get());
|
|
16
|
+
const rowSelection = $derived.by(() => table.atoms.rowSelection.get());
|
|
17
|
+
const sorting = $derived.by(() => table.atoms.sorting.get());
|
|
18
|
+
const columnFilters = $derived.by(() => table.atoms.columnFilters.get());
|
|
19
|
+
const globalFilter = $derived.by(() => table.atoms.globalFilter.get());
|
|
20
|
+
const rowModel = $derived.by(() => {
|
|
21
|
+
void columnFilters;
|
|
22
|
+
void globalFilter;
|
|
23
|
+
void sorting;
|
|
24
|
+
return table.getRowModel();
|
|
25
|
+
});
|
|
26
|
+
const headerGroupsData = $derived.by(() => {
|
|
27
|
+
void columnPinning;
|
|
28
|
+
void columnSizing;
|
|
29
|
+
void columnVisibility;
|
|
30
|
+
return buildHeaderGroups(table);
|
|
31
|
+
});
|
|
32
|
+
const visibleLeafColumns = $derived.by(() => {
|
|
33
|
+
void columnSizing;
|
|
34
|
+
void columnVisibility;
|
|
35
|
+
return table.getVisibleLeafColumns();
|
|
36
|
+
});
|
|
37
|
+
const growColumn = $derived(visibleLeafColumns.find((col) => getColumnMeta(col.columnDef)?.grow));
|
|
38
|
+
const isRowSelectionEnabled = $derived(Boolean(table.options.enableRowSelection));
|
|
39
|
+
const isMultiRowSelectionEnabled = $derived(table.options.enableMultiRowSelection !== false);
|
|
40
|
+
const allRowsSelectionState = $derived.by(() => {
|
|
41
|
+
void rowSelection;
|
|
42
|
+
void rowModel;
|
|
43
|
+
return table.getIsAllRowsSelected()
|
|
44
|
+
? true
|
|
45
|
+
: table.getIsSomeRowsSelected()
|
|
46
|
+
? 'indeterminate'
|
|
47
|
+
: false;
|
|
48
|
+
});
|
|
49
|
+
const selectedRowCount = $derived.by(() => {
|
|
50
|
+
void rowSelection;
|
|
51
|
+
void rowModel;
|
|
52
|
+
return table.getSelectedRowModel().rows.length;
|
|
53
|
+
});
|
|
54
|
+
const isColumnResizing = $derived(columnResizing.isResizingColumn !== false);
|
|
55
|
+
const hasFooter = $derived(visibleLeafColumns.some((col) => {
|
|
56
|
+
const meta = getColumnMeta(col.columnDef);
|
|
57
|
+
return !!(meta?.sum || meta?.footer);
|
|
58
|
+
}));
|
|
59
|
+
return {
|
|
60
|
+
get table() {
|
|
61
|
+
return table;
|
|
62
|
+
},
|
|
63
|
+
get columnPinning() {
|
|
64
|
+
return columnPinning;
|
|
65
|
+
},
|
|
66
|
+
get columnResizing() {
|
|
67
|
+
return columnResizing;
|
|
68
|
+
},
|
|
69
|
+
get columnSizing() {
|
|
70
|
+
return columnSizing;
|
|
71
|
+
},
|
|
72
|
+
get columnVisibility() {
|
|
73
|
+
return columnVisibility;
|
|
74
|
+
},
|
|
75
|
+
get rowSelection() {
|
|
76
|
+
return rowSelection;
|
|
77
|
+
},
|
|
78
|
+
get sorting() {
|
|
79
|
+
return sorting;
|
|
80
|
+
},
|
|
81
|
+
get rowModel() {
|
|
82
|
+
return rowModel;
|
|
83
|
+
},
|
|
84
|
+
get headerGroups() {
|
|
85
|
+
return headerGroupsData.groups;
|
|
86
|
+
},
|
|
87
|
+
getHeaderSection(header) {
|
|
88
|
+
return headerGroupsData.sections.get(header);
|
|
89
|
+
},
|
|
90
|
+
get visibleLeafColumns() {
|
|
91
|
+
return visibleLeafColumns;
|
|
92
|
+
},
|
|
93
|
+
get growColumn() {
|
|
94
|
+
return growColumn;
|
|
95
|
+
},
|
|
96
|
+
get hasGrowColumn() {
|
|
97
|
+
return growColumn !== undefined;
|
|
98
|
+
},
|
|
99
|
+
get isRowSelectionEnabled() {
|
|
100
|
+
return isRowSelectionEnabled;
|
|
101
|
+
},
|
|
102
|
+
get isMultiRowSelectionEnabled() {
|
|
103
|
+
return isMultiRowSelectionEnabled;
|
|
104
|
+
},
|
|
105
|
+
get allRowsSelectionState() {
|
|
106
|
+
return allRowsSelectionState;
|
|
107
|
+
},
|
|
108
|
+
get selectedRowCount() {
|
|
109
|
+
return selectedRowCount;
|
|
110
|
+
},
|
|
111
|
+
get isColumnResizing() {
|
|
112
|
+
return isColumnResizing;
|
|
113
|
+
},
|
|
114
|
+
get hasFooter() {
|
|
115
|
+
return hasFooter;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Concatenate the left/center/right header-group sections into single rows.
|
|
121
|
+
*
|
|
122
|
+
* When a column group spans pinned and unpinned columns, TanStack splits it
|
|
123
|
+
* into separate header objects (one per section). The fragments are kept
|
|
124
|
+
* separate — the pinned fragment gets sticky positioning so the group label
|
|
125
|
+
* stays visible during horizontal scroll — but the label renders only once:
|
|
126
|
+
* on the pinned fragment, with the other fragments cloned as placeholders.
|
|
127
|
+
* The clone preserves the header's prototype so its methods (getContext,
|
|
128
|
+
* getSize, …) keep working.
|
|
129
|
+
*
|
|
130
|
+
* The returned WeakMap records which section each header came from; the head
|
|
131
|
+
* component needs it because a fragment's `column` spans all sections and
|
|
132
|
+
* can't identify the fragment's own section.
|
|
133
|
+
*/
|
|
134
|
+
function buildHeaderGroups(table) {
|
|
135
|
+
const sections = new WeakMap();
|
|
136
|
+
const leftHeaderGroups = table.getLeftHeaderGroups();
|
|
137
|
+
const centerHeaderGroups = table.getCenterHeaderGroups();
|
|
138
|
+
const rightHeaderGroups = table.getRightHeaderGroups();
|
|
139
|
+
const groups = centerHeaderGroups.map((headerGroup, index) => {
|
|
140
|
+
const parts = [
|
|
141
|
+
...(leftHeaderGroups[index]?.headers ?? []).map((header) => ({
|
|
142
|
+
header,
|
|
143
|
+
section: 'left'
|
|
144
|
+
})),
|
|
145
|
+
...headerGroup.headers.map((header) => ({ header, section: 'center' })),
|
|
146
|
+
...(rightHeaderGroups[index]?.headers ?? []).map((header) => ({
|
|
147
|
+
header,
|
|
148
|
+
section: 'right'
|
|
149
|
+
}))
|
|
150
|
+
];
|
|
151
|
+
const headers = [];
|
|
152
|
+
let i = 0;
|
|
153
|
+
while (i < parts.length) {
|
|
154
|
+
let j = i;
|
|
155
|
+
while (j + 1 < parts.length &&
|
|
156
|
+
parts[j + 1].header.column.id === parts[i].header.column.id) {
|
|
157
|
+
j++;
|
|
158
|
+
}
|
|
159
|
+
const run = parts.slice(i, j + 1);
|
|
160
|
+
// The label lives on the pinned fragment so it stays visible; left wins
|
|
161
|
+
// over right when a group somehow spans both pinned sections.
|
|
162
|
+
const labelIndex = Math.max(0, run.findIndex((part) => part.section !== 'center'));
|
|
163
|
+
run.forEach((part, k) => {
|
|
164
|
+
let header = part.header;
|
|
165
|
+
if (run.length > 1 && k !== labelIndex && !header.isPlaceholder) {
|
|
166
|
+
const clone = Object.assign(Object.create(Object.getPrototypeOf(header)), header);
|
|
167
|
+
clone.isPlaceholder = true;
|
|
168
|
+
header = clone;
|
|
169
|
+
}
|
|
170
|
+
sections.set(header, part.section);
|
|
171
|
+
headers.push(header);
|
|
172
|
+
});
|
|
173
|
+
i = j + 1;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
...headerGroup,
|
|
177
|
+
id: `${leftHeaderGroups[index]?.id ?? ''}|${headerGroup.id}|${rightHeaderGroups[index]?.id ?? ''}`,
|
|
178
|
+
headers
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
return { groups, sections };
|
|
182
|
+
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends RowData">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
3
|
+
import type { Column, RowData } from '@tanstack/svelte-table';
|
|
4
|
+
import * as Popover from '../../popover';
|
|
5
|
+
import * as ScrollArea from '../../scroll-area';
|
|
6
|
+
import Checkbox from '../../checkbox/checkbox.svelte';
|
|
7
|
+
import { cn } from 'tailwind-variants';
|
|
8
|
+
import type { DataTableInstance } from '../data-table-utils';
|
|
9
|
+
import type { DataTableFeatures } from '../features';
|
|
10
|
+
import NumberInput from '../../number-input/number-input.svelte';
|
|
11
|
+
import * as Field from '../../field';
|
|
12
|
+
import { PhX, PhMagnifyingGlass } from '../../../icons';
|
|
13
|
+
|
|
14
|
+
type Props = {
|
|
15
|
+
table: DataTableInstance<T>;
|
|
16
|
+
triggerLabel?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let { table, triggerLabel = 'Filters' }: Props = $props();
|
|
20
|
+
|
|
21
|
+
let localText: Record<string, string> = $state({});
|
|
22
|
+
let localNumMin: Record<string, number> = $state({});
|
|
23
|
+
let localNumMax: Record<string, number> = $state({});
|
|
24
|
+
let localSelectSearch: Record<string, string> = $state({});
|
|
25
|
+
const timers: Record<string, ReturnType<typeof setTimeout>> = {};
|
|
26
|
+
|
|
27
|
+
const columnFilters = $derived.by(() => table.atoms.columnFilters.get());
|
|
28
|
+
const columnVisibility = $derived.by(() => table.atoms.columnVisibility.get());
|
|
29
|
+
const activeCount = $derived(columnFilters.length);
|
|
30
|
+
|
|
31
|
+
let activeFilterIds: string[] = $derived(columnFilters.map((f) => f.id));
|
|
32
|
+
let showColumnPicker = $state(false);
|
|
33
|
+
let columnSearchText = $state('');
|
|
34
|
+
|
|
35
|
+
const activeColumns = $derived.by(() => {
|
|
36
|
+
return activeFilterIds
|
|
37
|
+
.map((id) => table.getColumn(id))
|
|
38
|
+
.filter((col): col is Column<DataTableFeatures, T, unknown> => col != null);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const availableColumns = $derived.by(() => {
|
|
42
|
+
void columnVisibility;
|
|
43
|
+
return table
|
|
44
|
+
.getAllLeafColumns()
|
|
45
|
+
.filter((col) => col.getCanFilter() && !activeFilterIds.includes(col.id))
|
|
46
|
+
.filter(
|
|
47
|
+
(col) =>
|
|
48
|
+
!columnSearchText ||
|
|
49
|
+
getColumnLabel(col).toLowerCase().includes(columnSearchText.toLowerCase())
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
onDestroy(() => {
|
|
54
|
+
Object.values(timers).forEach(clearTimeout);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function getColumnType(column: Column<DataTableFeatures, T, unknown>): string | undefined {
|
|
58
|
+
return (column.columnDef.meta as Record<string, unknown> | undefined)?.type as
|
|
59
|
+
| string
|
|
60
|
+
| undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getColumnLabel(column: Column<DataTableFeatures, T, unknown>): string {
|
|
64
|
+
return typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function addFilter(column: Column<DataTableFeatures, T, unknown>) {
|
|
68
|
+
activeFilterIds = [...activeFilterIds, column.id];
|
|
69
|
+
showColumnPicker = false;
|
|
70
|
+
columnSearchText = '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function removeFilter(column: Column<DataTableFeatures, T, unknown>) {
|
|
74
|
+
activeFilterIds = activeFilterIds.filter((id) => id !== column.id);
|
|
75
|
+
column.setFilterValue(undefined);
|
|
76
|
+
delete localText[column.id];
|
|
77
|
+
delete localNumMin[column.id];
|
|
78
|
+
delete localNumMax[column.id];
|
|
79
|
+
delete localSelectSearch[column.id];
|
|
80
|
+
clearTimeout(timers[column.id]);
|
|
81
|
+
clearTimeout(timers[`${column.id}_min`]);
|
|
82
|
+
clearTimeout(timers[`${column.id}_max`]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function clearFilters() {
|
|
86
|
+
Object.values(timers).forEach(clearTimeout);
|
|
87
|
+
for (const key of Object.keys(timers)) delete timers[key];
|
|
88
|
+
localText = {};
|
|
89
|
+
localNumMin = {};
|
|
90
|
+
localNumMax = {};
|
|
91
|
+
localSelectSearch = {};
|
|
92
|
+
showColumnPicker = false;
|
|
93
|
+
columnSearchText = '';
|
|
94
|
+
table.resetColumnFilters();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function handleTextInput(column: Column<DataTableFeatures, T, unknown>, value: string) {
|
|
98
|
+
localText[column.id] = value;
|
|
99
|
+
clearTimeout(timers[column.id]);
|
|
100
|
+
timers[column.id] = setTimeout(() => {
|
|
101
|
+
column.setFilterValue(value || undefined);
|
|
102
|
+
}, 300);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function handleNumericInput(
|
|
106
|
+
column: Column<DataTableFeatures, T, unknown>,
|
|
107
|
+
which: 'min' | 'max',
|
|
108
|
+
value: number | null
|
|
109
|
+
) {
|
|
110
|
+
if (value === null) {
|
|
111
|
+
if (which === 'min') delete localNumMin[column.id];
|
|
112
|
+
else delete localNumMax[column.id];
|
|
113
|
+
} else {
|
|
114
|
+
if (which === 'min') localNumMin[column.id] = value;
|
|
115
|
+
else localNumMax[column.id] = value;
|
|
116
|
+
}
|
|
117
|
+
clearTimeout(timers[`${column.id}_${which}`]);
|
|
118
|
+
timers[`${column.id}_${which}`] = setTimeout(() => {
|
|
119
|
+
const min = localNumMin[column.id];
|
|
120
|
+
const max = localNumMax[column.id];
|
|
121
|
+
column.setFilterValue(min === undefined && max === undefined ? undefined : [min, max]);
|
|
122
|
+
}, 300);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getSelectValues(column: Column<DataTableFeatures, T, unknown>): string[] {
|
|
126
|
+
return (column.getFilterValue() as string[] | undefined) ?? [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function handleSelectChange(
|
|
130
|
+
column: Column<DataTableFeatures, T, unknown>,
|
|
131
|
+
value: string,
|
|
132
|
+
checked: boolean
|
|
133
|
+
) {
|
|
134
|
+
const current = getSelectValues(column);
|
|
135
|
+
const next = checked ? [...current, value] : current.filter((v) => v !== value);
|
|
136
|
+
column.setFilterValue(next.length ? next : undefined);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getFacetedValues(column: Column<DataTableFeatures, T, unknown>): string[] {
|
|
140
|
+
return Array.from(column.getFacetedUniqueValues().keys()).map(String).sort();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getFacetedMinMax(
|
|
144
|
+
column: Column<DataTableFeatures, T, unknown>
|
|
145
|
+
): [number | undefined, number | undefined] {
|
|
146
|
+
const vals = column.getFacetedMinMaxValues();
|
|
147
|
+
return vals ? [vals[0] as number, vals[1] as number] : [undefined, undefined];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getColumnFormatOptions(
|
|
151
|
+
column: Column<DataTableFeatures, T, unknown>
|
|
152
|
+
): Intl.NumberFormatOptions | undefined {
|
|
153
|
+
return (column.columnDef.meta as Record<string, unknown> | undefined)?.formatOptions as
|
|
154
|
+
| Intl.NumberFormatOptions
|
|
155
|
+
| undefined;
|
|
156
|
+
}
|
|
157
|
+
</script>
|
|
158
|
+
|
|
159
|
+
<Popover.Root positioning={{ placement: 'bottom-end' }}>
|
|
160
|
+
<Popover.Trigger
|
|
161
|
+
class="flex h-9 cursor-pointer items-center rounded-md border border-surface-3 bg-surface-1 px-3 text-sm font-medium text-ink shadow-sm outline-none hover:bg-surface-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
162
|
+
>
|
|
163
|
+
{triggerLabel}
|
|
164
|
+
{#if activeCount > 0}
|
|
165
|
+
({activeCount})
|
|
166
|
+
{/if}
|
|
167
|
+
</Popover.Trigger>
|
|
168
|
+
|
|
169
|
+
<Popover.Content class="w-70 p-3 flex flex-col gap-3" showArrow={false}>
|
|
170
|
+
<div class="flex items-center justify-between py-2.5 mr-1">
|
|
171
|
+
<span class="text-sm font-medium text-ink">Filters</span>
|
|
172
|
+
{#if activeCount > 0}
|
|
173
|
+
<button type="button" onclick={clearFilters} class="text-xs text-primary hover:underline">
|
|
174
|
+
Clear all
|
|
175
|
+
</button>
|
|
176
|
+
{/if}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{#if activeColumns.length > 0}
|
|
180
|
+
<div class="overflow-hidden border-t border-surface-2">
|
|
181
|
+
<ScrollArea.Root class="h-96">
|
|
182
|
+
<ScrollArea.Viewport>
|
|
183
|
+
<ScrollArea.Content class="flex flex-col gap-3">
|
|
184
|
+
{#each activeColumns as column (column.id)}
|
|
185
|
+
<div class="border border-surface-3 p-3">
|
|
186
|
+
<div class="mb-2 flex items-center justify-between">
|
|
187
|
+
<span class="text-sm font-medium text-ink">{getColumnLabel(column)}</span>
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
onclick={() => removeFilter(column)}
|
|
191
|
+
class="text-ink-dim transition-colors hover:text-ink"
|
|
192
|
+
>
|
|
193
|
+
<PhX class="size-3.5" />
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{#if getColumnType(column) === 'number' || getColumnType(column) === 'currency' || getColumnType(column) === 'percent'}
|
|
198
|
+
{@const [facetMin, facetMax] = getFacetedMinMax(column)}
|
|
199
|
+
{@const colFormatOptions = getColumnFormatOptions(column)}
|
|
200
|
+
<div class="flex flex-col gap-1.5">
|
|
201
|
+
<div class="min-w-0 flex-1">
|
|
202
|
+
<NumberInput
|
|
203
|
+
layout="horizontal"
|
|
204
|
+
label="From"
|
|
205
|
+
value={localNumMin[column.id] ?? null}
|
|
206
|
+
min={facetMin}
|
|
207
|
+
max={facetMax}
|
|
208
|
+
formatOptions={colFormatOptions}
|
|
209
|
+
onValueChange={({ valueAsNumber }) =>
|
|
210
|
+
handleNumericInput(
|
|
211
|
+
column,
|
|
212
|
+
'min',
|
|
213
|
+
isNaN(valueAsNumber) ? null : valueAsNumber
|
|
214
|
+
)}
|
|
215
|
+
/>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="min-w-0 flex-1">
|
|
218
|
+
<NumberInput
|
|
219
|
+
layout="horizontal"
|
|
220
|
+
label="To"
|
|
221
|
+
value={localNumMax[column.id] ?? null}
|
|
222
|
+
min={facetMin}
|
|
223
|
+
max={facetMax}
|
|
224
|
+
formatOptions={colFormatOptions}
|
|
225
|
+
onValueChange={({ valueAsNumber }) =>
|
|
226
|
+
handleNumericInput(
|
|
227
|
+
column,
|
|
228
|
+
'max',
|
|
229
|
+
isNaN(valueAsNumber) ? null : valueAsNumber
|
|
230
|
+
)}
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
{:else if getColumnType(column) === 'boolean'}
|
|
235
|
+
{@const boolFilter = column.getFilterValue() as boolean | undefined}
|
|
236
|
+
<div class="flex overflow-hidden rounded border border-border text-xs">
|
|
237
|
+
<button
|
|
238
|
+
type="button"
|
|
239
|
+
onclick={() => column.setFilterValue(undefined)}
|
|
240
|
+
class={cn(
|
|
241
|
+
'flex-1 px-2 py-1',
|
|
242
|
+
boolFilter === undefined
|
|
243
|
+
? 'bg-surface-3 font-medium text-ink'
|
|
244
|
+
: 'text-ink-dim hover:bg-surface-2'
|
|
245
|
+
)}
|
|
246
|
+
>
|
|
247
|
+
All
|
|
248
|
+
</button>
|
|
249
|
+
<button
|
|
250
|
+
type="button"
|
|
251
|
+
onclick={() =>
|
|
252
|
+
column.setFilterValue(boolFilter === true ? undefined : true)}
|
|
253
|
+
class={cn(
|
|
254
|
+
'flex-1 border-x border-border px-2 py-1',
|
|
255
|
+
boolFilter === true
|
|
256
|
+
? 'bg-surface-3 font-medium text-ink'
|
|
257
|
+
: 'text-ink-dim hover:bg-surface-2'
|
|
258
|
+
)}
|
|
259
|
+
>
|
|
260
|
+
Yes
|
|
261
|
+
</button>
|
|
262
|
+
<button
|
|
263
|
+
type="button"
|
|
264
|
+
onclick={() =>
|
|
265
|
+
column.setFilterValue(boolFilter === false ? undefined : false)}
|
|
266
|
+
class={cn(
|
|
267
|
+
'flex-1 px-2 py-1',
|
|
268
|
+
boolFilter === false
|
|
269
|
+
? 'bg-surface-3 font-medium text-ink'
|
|
270
|
+
: 'text-ink-dim hover:bg-surface-2'
|
|
271
|
+
)}
|
|
272
|
+
>
|
|
273
|
+
No
|
|
274
|
+
</button>
|
|
275
|
+
</div>
|
|
276
|
+
{:else if getColumnType(column) === 'select'}
|
|
277
|
+
{@const allOptions = getFacetedValues(column)}
|
|
278
|
+
{@const search = localSelectSearch[column.id] ?? ''}
|
|
279
|
+
{@const options = search
|
|
280
|
+
? allOptions.filter((o) => o.toLowerCase().includes(search.toLowerCase()))
|
|
281
|
+
: allOptions}
|
|
282
|
+
{@const selected = getSelectValues(column)}
|
|
283
|
+
<div class="flex flex-col gap-1">
|
|
284
|
+
<Field.Root>
|
|
285
|
+
<Field.Input
|
|
286
|
+
placeholder="Search..."
|
|
287
|
+
value={search}
|
|
288
|
+
oninput={(e: Event) => {
|
|
289
|
+
localSelectSearch[column.id] = (
|
|
290
|
+
e.currentTarget as HTMLInputElement
|
|
291
|
+
).value;
|
|
292
|
+
}}
|
|
293
|
+
/>
|
|
294
|
+
</Field.Root>
|
|
295
|
+
<ScrollArea.Root>
|
|
296
|
+
<ScrollArea.Viewport class="max-h-40">
|
|
297
|
+
<ScrollArea.Content>
|
|
298
|
+
<div class="flex flex-col gap-0.5">
|
|
299
|
+
{#each options as option (option)}
|
|
300
|
+
<Checkbox
|
|
301
|
+
size="sm"
|
|
302
|
+
label={option}
|
|
303
|
+
class="min-h-7 rounded-sm px-2 hover:bg-surface-2"
|
|
304
|
+
checked={selected.includes(option)}
|
|
305
|
+
onCheckedChange={({ checked }) =>
|
|
306
|
+
handleSelectChange(column, option, checked === true)}
|
|
307
|
+
/>
|
|
308
|
+
{/each}
|
|
309
|
+
</div>
|
|
310
|
+
</ScrollArea.Content>
|
|
311
|
+
</ScrollArea.Viewport>
|
|
312
|
+
<ScrollArea.Scrollbar orientation="vertical">
|
|
313
|
+
<ScrollArea.Thumb />
|
|
314
|
+
</ScrollArea.Scrollbar>
|
|
315
|
+
<ScrollArea.Corner />
|
|
316
|
+
</ScrollArea.Root>
|
|
317
|
+
</div>
|
|
318
|
+
{:else}
|
|
319
|
+
<Field.Root>
|
|
320
|
+
<Field.Input
|
|
321
|
+
placeholder="Search..."
|
|
322
|
+
value={localText[column.id] ?? ''}
|
|
323
|
+
oninput={(e: Event) =>
|
|
324
|
+
handleTextInput(column, (e.currentTarget as HTMLInputElement).value)}
|
|
325
|
+
/>
|
|
326
|
+
</Field.Root>
|
|
327
|
+
{/if}
|
|
328
|
+
</div>
|
|
329
|
+
{/each}
|
|
330
|
+
</ScrollArea.Content>
|
|
331
|
+
</ScrollArea.Viewport>
|
|
332
|
+
<ScrollArea.Scrollbar orientation="vertical">
|
|
333
|
+
<ScrollArea.Thumb />
|
|
334
|
+
</ScrollArea.Scrollbar>
|
|
335
|
+
<ScrollArea.Corner />
|
|
336
|
+
</ScrollArea.Root>
|
|
337
|
+
</div>
|
|
338
|
+
{/if}
|
|
339
|
+
|
|
340
|
+
{#if showColumnPicker}
|
|
341
|
+
<div class="border-t border-surface-3 p-3">
|
|
342
|
+
<div class="relative mb-1">
|
|
343
|
+
<PhMagnifyingGlass
|
|
344
|
+
class="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-ink-dim"
|
|
345
|
+
/>
|
|
346
|
+
<input
|
|
347
|
+
type="text"
|
|
348
|
+
placeholder="Search columns..."
|
|
349
|
+
bind:value={columnSearchText}
|
|
350
|
+
class="h-8 w-full rounded border border-border bg-surface-1 pr-3 pl-7 text-sm text-ink outline-none placeholder:text-ink-dim focus:ring-1 focus:ring-ring"
|
|
351
|
+
/>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="max-h-48 overflow-y-auto">
|
|
354
|
+
{#each availableColumns as column (column.id)}
|
|
355
|
+
<button
|
|
356
|
+
type="button"
|
|
357
|
+
onclick={() => addFilter(column)}
|
|
358
|
+
class="w-full rounded px-2 py-1.5 text-left text-sm text-ink hover:bg-surface-2"
|
|
359
|
+
>
|
|
360
|
+
{getColumnLabel(column)}
|
|
361
|
+
</button>
|
|
362
|
+
{:else}
|
|
363
|
+
<p class="px-2 py-3 text-center text-sm text-ink-dim">No more columns</p>
|
|
364
|
+
{/each}
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
{:else}
|
|
368
|
+
<div class="border-t border-surface-3">
|
|
369
|
+
<button
|
|
370
|
+
type="button"
|
|
371
|
+
onclick={() => (showColumnPicker = true)}
|
|
372
|
+
class="flex w-full items-center gap-2 px-3 py-2.5 text-sm text-ink-dim hover:bg-surface-2 hover:text-ink"
|
|
373
|
+
>
|
|
374
|
+
<span class="text-base leading-none">+</span>
|
|
375
|
+
Add Filter
|
|
376
|
+
</button>
|
|
377
|
+
</div>
|
|
378
|
+
{/if}
|
|
379
|
+
</Popover.Content>
|
|
380
|
+
</Popover.Root>
|