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.
Files changed (53) hide show
  1. package/dist/components/data-table-v9/column-helper.d.ts +12 -0
  2. package/dist/components/data-table-v9/column-helper.js +42 -0
  3. package/dist/components/data-table-v9/create-table.svelte.d.ts +42 -0
  4. package/dist/components/data-table-v9/create-table.svelte.js +248 -0
  5. package/dist/components/data-table-v9/data-table-cell-content.svelte +66 -0
  6. package/dist/components/data-table-v9/data-table-cell-content.svelte.d.ts +28 -0
  7. package/dist/components/data-table-v9/data-table-foot.svelte +111 -0
  8. package/dist/components/data-table-v9/data-table-foot.svelte.d.ts +32 -0
  9. package/dist/components/{data-table/data-table-head.svelte.md → data-table-v9/data-table-head.svelte} +47 -39
  10. package/dist/components/data-table-v9/data-table-head.svelte.d.ts +32 -0
  11. package/dist/components/data-table-v9/data-table-title.svelte.d.ts +10 -0
  12. package/dist/components/data-table-v9/data-table-utils.d.ts +53 -0
  13. package/dist/components/data-table-v9/data-table-utils.js +181 -0
  14. package/dist/components/data-table-v9/data-table.svelte +151 -0
  15. package/dist/components/data-table-v9/data-table.svelte.d.ts +41 -0
  16. package/dist/components/data-table-v9/features.d.ts +12 -0
  17. package/dist/components/data-table-v9/features.js +14 -0
  18. package/dist/components/data-table-v9/index.d.ts +11 -0
  19. package/dist/components/data-table-v9/index.js +9 -0
  20. package/dist/components/data-table-v9/table-view-state.svelte.d.ts +74 -0
  21. package/dist/components/data-table-v9/table-view-state.svelte.js +182 -0
  22. package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte +380 -0
  23. package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte.d.ts +29 -0
  24. package/dist/components/data-table-v9/toolbar/data-table-column-visibility.svelte +73 -0
  25. package/dist/components/data-table-v9/toolbar/data-table-column-visibility.svelte.d.ts +29 -0
  26. package/dist/components/data-table-v9/toolbar/data-table-search.svelte +58 -0
  27. package/dist/components/data-table-v9/toolbar/data-table-search.svelte.d.ts +32 -0
  28. package/dist/components/{data-table/data-table-toolbar.svelte.md → data-table-v9/toolbar/data-table-toolbar.svelte} +14 -15
  29. package/dist/components/data-table-v9/toolbar/data-table-toolbar.svelte.d.ts +12 -0
  30. package/dist/components/data-table-v9/types.d.ts +74 -0
  31. package/dist/components/data-table-v9/types.js +1 -0
  32. package/dist/components/data-table-v9/virtual/data-table-virtual-rows.svelte +131 -0
  33. package/dist/components/data-table-v9/virtual/data-table-virtual-rows.svelte.d.ts +40 -0
  34. package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte +79 -0
  35. package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte.d.ts +41 -0
  36. package/dist/components/data-table-v9/virtual/index.d.ts +3 -0
  37. package/dist/components/data-table-v9/virtual/index.js +2 -0
  38. package/dist/index.d.ts +4 -0
  39. package/dist/index.js +4 -0
  40. package/package.json +12 -2
  41. package/dist/components/data-table/column-helper.ts.md +0 -96
  42. package/dist/components/data-table/create-table.ts.md +0 -386
  43. package/dist/components/data-table/data-table-column-filter.svelte.md +0 -249
  44. package/dist/components/data-table/data-table-column-visibility.svelte.md +0 -74
  45. package/dist/components/data-table/data-table-new.svelte.md +0 -245
  46. package/dist/components/data-table/data-table-utils.ts.md +0 -179
  47. package/dist/components/data-table/data-table-virtual-rows.svelte.md +0 -171
  48. package/dist/components/data-table/data-table-virtualized.svelte.md +0 -108
  49. package/dist/components/data-table/data-table.svelte.md +0 -214
  50. package/dist/components/data-table/index.ts.md +0 -22
  51. package/dist/components/data-table/types.ts.md +0 -101
  52. package/dist/components/data-table/virtual/index.ts.md +0 -26
  53. /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>