compote-ui 0.46.5 → 0.47.2
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/combobox/combobox.svelte +99 -28
- package/dist/components/combobox/types.d.ts +1 -0
- package/dist/components/data-table-v8/index.d.ts +4 -4
- package/dist/components/data-table-v8/index.js +4 -4
- package/dist/components/data-table-v8/toolbar/data-table-column-filter.svelte +375 -0
- package/dist/components/data-table-v8/{data-table-column-filter.svelte.d.ts → toolbar/data-table-column-filter.svelte.d.ts} +1 -1
- package/dist/components/data-table-v8/{data-table-column-visibility.svelte → toolbar/data-table-column-visibility.svelte} +4 -4
- package/dist/components/data-table-v8/{data-table-column-visibility.svelte.d.ts → toolbar/data-table-column-visibility.svelte.d.ts} +1 -1
- package/dist/components/data-table-v8/{data-table-search.svelte → toolbar/data-table-search.svelte} +3 -3
- package/dist/components/data-table-v8/{data-table-search.svelte.d.ts → toolbar/data-table-search.svelte.d.ts} +1 -1
- package/dist/components/data-table-v8/{data-table-virtual-rows.svelte → virtual/data-table-virtual-rows.svelte} +4 -4
- package/dist/components/data-table-v8/{data-table-virtual-rows.svelte.d.ts → virtual/data-table-virtual-rows.svelte.d.ts} +1 -1
- package/dist/components/data-table-v8/{data-table-virtualized.svelte → virtual/data-table-virtualized.svelte} +3 -3
- package/dist/components/data-table-v8/{data-table-virtualized.svelte.d.ts → virtual/data-table-virtualized.svelte.d.ts} +1 -1
- package/dist/components/data-table-v8/virtual/index.d.ts +1 -1
- package/dist/components/data-table-v8/virtual/index.js +1 -1
- package/dist/components/drawer/drawer-root.svelte +10 -3
- package/dist/components/drawer/drawer-root.svelte.d.ts +1 -1
- package/dist/components/number-input/number-input.svelte +8 -3
- package/dist/components/scroll-area/scroll-area-content.svelte +1 -1
- package/dist/components/scroll-area/scroll-area-scrollbar.svelte +1 -1
- package/dist/components/tabs/tabs.svelte +1 -0
- package/dist/components/toggle/toggle.svelte +5 -1
- package/dist/components/toggle-group/toggle-group-item.svelte +1 -4
- package/dist/components/toggle-group/toggle-group.svelte +5 -6
- package/package.json +1 -1
- package/dist/components/data-table-v8/data-table-column-filter.svelte +0 -248
- /package/dist/components/data-table-v8/{data-table-toolbar.svelte → toolbar/data-table-toolbar.svelte} +0 -0
- /package/dist/components/data-table-v8/{data-table-toolbar.svelte.d.ts → toolbar/data-table-toolbar.svelte.d.ts} +0 -0
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { Field } from '@ark-ui/svelte/field';
|
|
4
4
|
import { useFilter } from '@ark-ui/svelte/locale';
|
|
5
5
|
import { Portal } from '@ark-ui/svelte/portal';
|
|
6
|
+
import { createVirtualizer } from '@tanstack/svelte-virtual';
|
|
7
|
+
import { untrack } from 'svelte';
|
|
6
8
|
import type { ComboboxProps } from './types';
|
|
7
9
|
import { createListCollection, type ListItem } from '../../utils/collections';
|
|
8
10
|
import { cn } from 'tailwind-variants';
|
|
@@ -18,26 +20,22 @@
|
|
|
18
20
|
readOnly,
|
|
19
21
|
multiple,
|
|
20
22
|
loading = false,
|
|
23
|
+
virtualized = false,
|
|
21
24
|
class: className,
|
|
22
25
|
onValueChange,
|
|
23
26
|
...restProps
|
|
24
27
|
}: ComboboxProps<T> = $props();
|
|
25
28
|
|
|
26
|
-
// Client-side filtering state
|
|
27
29
|
let filterText = $state('');
|
|
28
|
-
const filters = useFilter({ sensitivity: 'base' });
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
const filters = useFilter({ sensitivity: 'base' });
|
|
31
32
|
const baseCollection = $derived(createListCollection(items));
|
|
32
|
-
|
|
33
|
-
// Filtered view — lightweight .filter() on keystroke, full collection when empty
|
|
34
33
|
const collection = $derived(
|
|
35
34
|
filterText
|
|
36
35
|
? baseCollection.filter((itemString) => filters().contains(itemString, filterText))
|
|
37
36
|
: baseCollection
|
|
38
37
|
);
|
|
39
38
|
|
|
40
|
-
// Handle input change — only filter on actual user typing, not on selection/clear events
|
|
41
39
|
function handleInputChange(details: Combobox.InputValueChangeDetails) {
|
|
42
40
|
if (details.reason === 'input-change') {
|
|
43
41
|
filterText = details.inputValue;
|
|
@@ -78,6 +76,29 @@
|
|
|
78
76
|
}
|
|
79
77
|
onValueChange?.(details);
|
|
80
78
|
}
|
|
79
|
+
|
|
80
|
+
let contentRef = $state<HTMLDivElement | null>(null);
|
|
81
|
+
|
|
82
|
+
const virtualizer = createVirtualizer({
|
|
83
|
+
get count() {
|
|
84
|
+
return virtualized ? collection.size : 0;
|
|
85
|
+
},
|
|
86
|
+
getScrollElement: () => contentRef,
|
|
87
|
+
estimateSize: () => 36,
|
|
88
|
+
overscan: 10
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
$effect(() => {
|
|
92
|
+
const count = virtualized ? collection.size : 0;
|
|
93
|
+
const scrollElement = contentRef;
|
|
94
|
+
|
|
95
|
+
if (!scrollElement) return;
|
|
96
|
+
|
|
97
|
+
untrack(() => {
|
|
98
|
+
$virtualizer.setOptions({ ...$virtualizer.options, count });
|
|
99
|
+
$virtualizer.measure();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
81
102
|
</script>
|
|
82
103
|
|
|
83
104
|
<Combobox.Root
|
|
@@ -86,6 +107,9 @@
|
|
|
86
107
|
inputValue={controlledInputValue}
|
|
87
108
|
onValueChange={handleValueChange}
|
|
88
109
|
onInputValueChange={handleInputChange}
|
|
110
|
+
scrollToIndexFn={virtualized
|
|
111
|
+
? (d) => $virtualizer.scrollToIndex(d.index, { align: 'center' })
|
|
112
|
+
: undefined}
|
|
89
113
|
openOnClick
|
|
90
114
|
{multiple}
|
|
91
115
|
{readOnly}
|
|
@@ -140,31 +164,78 @@
|
|
|
140
164
|
<Portal>
|
|
141
165
|
<Combobox.Positioner class="data-[state=closed]:pointer-events-none">
|
|
142
166
|
<Combobox.Content
|
|
143
|
-
class=
|
|
167
|
+
class={cn(
|
|
168
|
+
'z-200 min-w-(--reference-width) rounded-md border bg-surface-document p-1 shadow-md',
|
|
169
|
+
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
170
|
+
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
171
|
+
virtualized ? 'overflow-hidden' : 'max-h-60 overflow-auto'
|
|
172
|
+
)}
|
|
144
173
|
>
|
|
145
|
-
{#if
|
|
146
|
-
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
174
|
+
{#if virtualized}
|
|
175
|
+
{#if loading}
|
|
176
|
+
<div class="flex items-center justify-center py-4">
|
|
177
|
+
<span
|
|
178
|
+
class="size-5 animate-spin rounded-full border-2 border-surface-3 border-t-ink-dim"
|
|
179
|
+
></span>
|
|
180
|
+
</div>
|
|
181
|
+
{:else if collection.size === 0}
|
|
182
|
+
<Combobox.Empty class="py-2 text-center text-sm text-ink-dim">
|
|
183
|
+
No results found
|
|
184
|
+
</Combobox.Empty>
|
|
185
|
+
{:else}
|
|
186
|
+
<div
|
|
187
|
+
bind:this={contentRef}
|
|
188
|
+
class="overflow-auto overscroll-contain"
|
|
189
|
+
style="height: min(15rem, {$virtualizer.getTotalSize()}px);"
|
|
190
|
+
>
|
|
191
|
+
<div
|
|
192
|
+
style="height: {$virtualizer.getTotalSize()}px; width: 100%; position: relative;"
|
|
193
|
+
>
|
|
194
|
+
{#each $virtualizer.getVirtualItems() as vItem (vItem.key)}
|
|
195
|
+
{@const item = collection.items[vItem.index]}
|
|
196
|
+
{#if item}
|
|
197
|
+
<Combobox.Item
|
|
198
|
+
{item}
|
|
199
|
+
aria-setsize={collection.size}
|
|
200
|
+
aria-posinset={vItem.index + 1}
|
|
201
|
+
style="position:absolute;top:0;left:0;width:100%;height:{vItem.size}px;transform:translateY({vItem.start}px)"
|
|
202
|
+
class="relative flex cursor-default items-center rounded-sm py-1.5 pr-8 pl-2 text-sm select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-surface-1 data-[state=checked]:bg-surface-1"
|
|
203
|
+
>
|
|
204
|
+
<Combobox.ItemText>{item.label}</Combobox.ItemText>
|
|
205
|
+
<Combobox.ItemIndicator class="absolute right-2 items-center justify-center">
|
|
206
|
+
<PhCheck class="size-3.5" />
|
|
207
|
+
</Combobox.ItemIndicator>
|
|
208
|
+
</Combobox.Item>
|
|
209
|
+
{/if}
|
|
210
|
+
{/each}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
{/if}
|
|
151
214
|
{:else}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
215
|
+
{#if loading}
|
|
216
|
+
<div class="flex items-center justify-center py-4">
|
|
217
|
+
<span
|
|
218
|
+
class="size-5 animate-spin rounded-full border-2 border-surface-3 border-t-ink-dim"
|
|
219
|
+
></span>
|
|
220
|
+
</div>
|
|
221
|
+
{:else}
|
|
222
|
+
<Combobox.Empty class="py-2 text-center text-sm text-ink-dim">
|
|
223
|
+
No results found
|
|
224
|
+
</Combobox.Empty>
|
|
225
|
+
{/if}
|
|
156
226
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
227
|
+
{#each loading ? [] : collection.items as item (item.value)}
|
|
228
|
+
<Combobox.Item
|
|
229
|
+
{item}
|
|
230
|
+
class="relative flex cursor-default items-center rounded-sm py-1.5 pr-8 pl-2 text-sm select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-surface-1 data-[state=checked]:bg-surface-1"
|
|
231
|
+
>
|
|
232
|
+
<Combobox.ItemText>{item.label}</Combobox.ItemText>
|
|
233
|
+
<Combobox.ItemIndicator class="absolute right-2 items-center justify-center">
|
|
234
|
+
<PhCheck class="size-3.5" />
|
|
235
|
+
</Combobox.ItemIndicator>
|
|
236
|
+
</Combobox.Item>
|
|
237
|
+
{/each}
|
|
238
|
+
{/if}
|
|
168
239
|
</Combobox.Content>
|
|
169
240
|
</Combobox.Positioner>
|
|
170
241
|
</Portal>
|
|
@@ -3,10 +3,10 @@ export { createTable } from './create-table.svelte';
|
|
|
3
3
|
export { renderComponent, renderSnippet } from './render-helpers';
|
|
4
4
|
export { default as FlexRender } from './flex-render.svelte';
|
|
5
5
|
export { default as Root } from './data-table.svelte';
|
|
6
|
-
export { default as Toolbar } from './data-table-toolbar.svelte';
|
|
7
6
|
export { default as Title } from './data-table-title.svelte';
|
|
8
|
-
export { default as
|
|
9
|
-
export { default as ColumnFilter } from './data-table-column-filter.svelte';
|
|
10
|
-
export { default as
|
|
7
|
+
export { default as Toolbar } from './toolbar/data-table-toolbar.svelte';
|
|
8
|
+
export { default as ColumnFilter } from './toolbar/data-table-column-filter.svelte';
|
|
9
|
+
export { default as ColumnVisibility } from './toolbar/data-table-column-visibility.svelte';
|
|
10
|
+
export { default as Search } from './toolbar/data-table-search.svelte';
|
|
11
11
|
export type { CreateDataTableOptions, DataTableInstance } from './create-table.svelte';
|
|
12
12
|
export type { DataTableAlign, DataTableAccessorFnColumn, DataTableAccessorKeyColumn, DataTableColumn, DataTableColumnBase, DataTableColumnOptions, DataTableColumnType, DataTableCellPropsResolver, DataTableCellRenderProps, DataTableGroupColumn, DataTableLeafColumnBase, DataTableLeafColumn } from './types';
|
|
@@ -3,8 +3,8 @@ export { createTable } from './create-table.svelte';
|
|
|
3
3
|
export { renderComponent, renderSnippet } from './render-helpers';
|
|
4
4
|
export { default as FlexRender } from './flex-render.svelte';
|
|
5
5
|
export { default as Root } from './data-table.svelte';
|
|
6
|
-
export { default as Toolbar } from './data-table-toolbar.svelte';
|
|
7
6
|
export { default as Title } from './data-table-title.svelte';
|
|
8
|
-
export { default as
|
|
9
|
-
export { default as ColumnFilter } from './data-table-column-filter.svelte';
|
|
10
|
-
export { default as
|
|
7
|
+
export { default as Toolbar } from './toolbar/data-table-toolbar.svelte';
|
|
8
|
+
export { default as ColumnFilter } from './toolbar/data-table-column-filter.svelte';
|
|
9
|
+
export { default as ColumnVisibility } from './toolbar/data-table-column-visibility.svelte';
|
|
10
|
+
export { default as Search } from './toolbar/data-table-search.svelte';
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends RowData">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
3
|
+
import type { Column, RowData } from '@tanstack/table-core';
|
|
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 { getReactiveTableState, type DataTableInstance } from '../data-table-utils';
|
|
9
|
+
import NumberInput from '../../number-input/number-input.svelte';
|
|
10
|
+
import * as Field from '../../field';
|
|
11
|
+
import { PhX, PhMagnifyingGlass } from '../../../icons';
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
table: DataTableInstance<T>;
|
|
15
|
+
triggerLabel?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let { table, triggerLabel = 'Filters' }: Props = $props();
|
|
19
|
+
|
|
20
|
+
let localText: Record<string, string> = $state({});
|
|
21
|
+
let localNumMin: Record<string, number> = $state({});
|
|
22
|
+
let localNumMax: Record<string, number> = $state({});
|
|
23
|
+
let localSelectSearch: Record<string, string> = $state({});
|
|
24
|
+
const timers: Record<string, ReturnType<typeof setTimeout>> = {};
|
|
25
|
+
|
|
26
|
+
const columnFilters = $derived(getReactiveTableState(table).columnFilters);
|
|
27
|
+
const activeCount = $derived(columnFilters.length);
|
|
28
|
+
|
|
29
|
+
let activeFilterIds: string[] = $derived(columnFilters.map((f) => f.id));
|
|
30
|
+
let showColumnPicker = $state(false);
|
|
31
|
+
let columnSearchText = $state('');
|
|
32
|
+
|
|
33
|
+
const activeColumns = $derived.by(() => {
|
|
34
|
+
getReactiveTableState(table);
|
|
35
|
+
return activeFilterIds
|
|
36
|
+
.map((id) => table.getColumn(id))
|
|
37
|
+
.filter((col): col is Column<T, unknown> => col != null);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const availableColumns = $derived.by(() => {
|
|
41
|
+
getReactiveTableState(table);
|
|
42
|
+
return table
|
|
43
|
+
.getAllLeafColumns()
|
|
44
|
+
.filter((col) => col.getCanFilter() && !activeFilterIds.includes(col.id))
|
|
45
|
+
.filter(
|
|
46
|
+
(col) =>
|
|
47
|
+
!columnSearchText ||
|
|
48
|
+
getColumnLabel(col).toLowerCase().includes(columnSearchText.toLowerCase())
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
onDestroy(() => {
|
|
53
|
+
Object.values(timers).forEach(clearTimeout);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function getColumnType(column: Column<T, unknown>): string | undefined {
|
|
57
|
+
return (column.columnDef.meta as Record<string, unknown> | undefined)?.type as
|
|
58
|
+
| string
|
|
59
|
+
| undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getColumnLabel(column: Column<T, unknown>): string {
|
|
63
|
+
return typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function addFilter(column: Column<T, unknown>) {
|
|
67
|
+
activeFilterIds = [...activeFilterIds, column.id];
|
|
68
|
+
showColumnPicker = false;
|
|
69
|
+
columnSearchText = '';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function removeFilter(column: Column<T, unknown>) {
|
|
73
|
+
activeFilterIds = activeFilterIds.filter((id) => id !== column.id);
|
|
74
|
+
column.setFilterValue(undefined);
|
|
75
|
+
delete localText[column.id];
|
|
76
|
+
delete localNumMin[column.id];
|
|
77
|
+
delete localNumMax[column.id];
|
|
78
|
+
delete localSelectSearch[column.id];
|
|
79
|
+
clearTimeout(timers[column.id]);
|
|
80
|
+
clearTimeout(timers[`${column.id}_min`]);
|
|
81
|
+
clearTimeout(timers[`${column.id}_max`]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function clearFilters() {
|
|
85
|
+
Object.values(timers).forEach(clearTimeout);
|
|
86
|
+
for (const key of Object.keys(timers)) delete timers[key];
|
|
87
|
+
localText = {};
|
|
88
|
+
localNumMin = {};
|
|
89
|
+
localNumMax = {};
|
|
90
|
+
localSelectSearch = {};
|
|
91
|
+
showColumnPicker = false;
|
|
92
|
+
columnSearchText = '';
|
|
93
|
+
table.resetColumnFilters();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function handleTextInput(column: Column<T, unknown>, value: string) {
|
|
97
|
+
localText[column.id] = value;
|
|
98
|
+
clearTimeout(timers[column.id]);
|
|
99
|
+
timers[column.id] = setTimeout(() => {
|
|
100
|
+
column.setFilterValue(value || undefined);
|
|
101
|
+
}, 300);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function handleNumericInput(
|
|
105
|
+
column: Column<T, unknown>,
|
|
106
|
+
which: 'min' | 'max',
|
|
107
|
+
value: number | null
|
|
108
|
+
) {
|
|
109
|
+
if (value === null) {
|
|
110
|
+
if (which === 'min') delete localNumMin[column.id];
|
|
111
|
+
else delete localNumMax[column.id];
|
|
112
|
+
} else {
|
|
113
|
+
if (which === 'min') localNumMin[column.id] = value;
|
|
114
|
+
else localNumMax[column.id] = value;
|
|
115
|
+
}
|
|
116
|
+
clearTimeout(timers[`${column.id}_${which}`]);
|
|
117
|
+
timers[`${column.id}_${which}`] = setTimeout(() => {
|
|
118
|
+
const min = localNumMin[column.id];
|
|
119
|
+
const max = localNumMax[column.id];
|
|
120
|
+
column.setFilterValue(min === undefined && max === undefined ? undefined : [min, max]);
|
|
121
|
+
}, 300);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getSelectValues(column: Column<T, unknown>): string[] {
|
|
125
|
+
return (column.getFilterValue() as string[] | undefined) ?? [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function handleSelectChange(column: Column<T, unknown>, value: string, checked: boolean) {
|
|
129
|
+
const current = getSelectValues(column);
|
|
130
|
+
const next = checked ? [...current, value] : current.filter((v) => v !== value);
|
|
131
|
+
column.setFilterValue(next.length ? next : undefined);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getFacetedValues(column: Column<T, unknown>): string[] {
|
|
135
|
+
return Array.from(column.getFacetedUniqueValues().keys()).map(String).sort();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getFacetedMinMax(column: Column<T, unknown>): [number | undefined, number | undefined] {
|
|
139
|
+
const vals = column.getFacetedMinMaxValues();
|
|
140
|
+
return vals ? [vals[0] as number, vals[1] as number] : [undefined, undefined];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getColumnFormatOptions(
|
|
144
|
+
column: Column<T, unknown>
|
|
145
|
+
): Intl.NumberFormatOptions | undefined {
|
|
146
|
+
return (column.columnDef.meta as Record<string, unknown> | undefined)?.formatOptions as
|
|
147
|
+
| Intl.NumberFormatOptions
|
|
148
|
+
| undefined;
|
|
149
|
+
}
|
|
150
|
+
</script>
|
|
151
|
+
|
|
152
|
+
<Popover.Root positioning={{ placement: 'bottom-end' }}>
|
|
153
|
+
<Popover.Trigger
|
|
154
|
+
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"
|
|
155
|
+
>
|
|
156
|
+
{triggerLabel}
|
|
157
|
+
{#if activeCount > 0}
|
|
158
|
+
({activeCount})
|
|
159
|
+
{/if}
|
|
160
|
+
</Popover.Trigger>
|
|
161
|
+
|
|
162
|
+
<Popover.Content class="w-72 p-0" showArrow={false}>
|
|
163
|
+
<div class="flex items-center justify-between px-3 py-2.5">
|
|
164
|
+
<span class="text-sm font-medium text-ink">Filters</span>
|
|
165
|
+
{#if activeCount > 0}
|
|
166
|
+
<button type="button" onclick={clearFilters} class="text-xs text-primary hover:underline">
|
|
167
|
+
Clear all
|
|
168
|
+
</button>
|
|
169
|
+
{/if}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{#if activeColumns.length > 0}
|
|
173
|
+
<div class="overflow-hidden border-t border-surface-3">
|
|
174
|
+
<ScrollArea.Root class="h-96">
|
|
175
|
+
<ScrollArea.Viewport>
|
|
176
|
+
<ScrollArea.Content>
|
|
177
|
+
{#each activeColumns as column (column.id)}
|
|
178
|
+
<div class="border border-surface-3 p-3">
|
|
179
|
+
<div class="mb-2 flex items-center justify-between">
|
|
180
|
+
<span class="text-sm font-medium text-ink">{getColumnLabel(column)}</span>
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
onclick={() => removeFilter(column)}
|
|
184
|
+
class="text-ink-dim transition-colors hover:text-ink"
|
|
185
|
+
>
|
|
186
|
+
<PhX class="size-3.5" />
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{#if getColumnType(column) === 'number' || getColumnType(column) === 'currency' || getColumnType(column) === 'percent'}
|
|
191
|
+
{@const [facetMin, facetMax] = getFacetedMinMax(column)}
|
|
192
|
+
{@const colFormatOptions = getColumnFormatOptions(column)}
|
|
193
|
+
<div class="flex flex-col gap-1.5">
|
|
194
|
+
<div class="min-w-0 flex-1">
|
|
195
|
+
<NumberInput
|
|
196
|
+
layout="horizontal"
|
|
197
|
+
label="From"
|
|
198
|
+
value={localNumMin[column.id] ?? facetMin ?? null}
|
|
199
|
+
min={facetMin}
|
|
200
|
+
max={facetMax}
|
|
201
|
+
formatOptions={colFormatOptions}
|
|
202
|
+
onValueChange={({ valueAsNumber }) =>
|
|
203
|
+
handleNumericInput(
|
|
204
|
+
column,
|
|
205
|
+
'min',
|
|
206
|
+
isNaN(valueAsNumber) ? null : valueAsNumber
|
|
207
|
+
)}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="min-w-0 flex-1">
|
|
211
|
+
<NumberInput
|
|
212
|
+
layout="horizontal"
|
|
213
|
+
label="To"
|
|
214
|
+
value={localNumMax[column.id] ?? facetMax ?? null}
|
|
215
|
+
min={facetMin}
|
|
216
|
+
max={facetMax}
|
|
217
|
+
formatOptions={colFormatOptions}
|
|
218
|
+
onValueChange={({ valueAsNumber }) =>
|
|
219
|
+
handleNumericInput(
|
|
220
|
+
column,
|
|
221
|
+
'max',
|
|
222
|
+
isNaN(valueAsNumber) ? null : valueAsNumber
|
|
223
|
+
)}
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
{:else if getColumnType(column) === 'boolean'}
|
|
228
|
+
{@const boolFilter = column.getFilterValue() as boolean | undefined}
|
|
229
|
+
<div class="flex overflow-hidden rounded border border-border text-xs">
|
|
230
|
+
<button
|
|
231
|
+
type="button"
|
|
232
|
+
onclick={() => column.setFilterValue(undefined)}
|
|
233
|
+
class={cn(
|
|
234
|
+
'flex-1 px-2 py-1',
|
|
235
|
+
boolFilter === undefined
|
|
236
|
+
? 'bg-surface-3 font-medium text-ink'
|
|
237
|
+
: 'text-ink-dim hover:bg-surface-2'
|
|
238
|
+
)}
|
|
239
|
+
>
|
|
240
|
+
All
|
|
241
|
+
</button>
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
onclick={() =>
|
|
245
|
+
column.setFilterValue(boolFilter === true ? undefined : true)}
|
|
246
|
+
class={cn(
|
|
247
|
+
'flex-1 border-x border-border px-2 py-1',
|
|
248
|
+
boolFilter === true
|
|
249
|
+
? 'bg-surface-3 font-medium text-ink'
|
|
250
|
+
: 'text-ink-dim hover:bg-surface-2'
|
|
251
|
+
)}
|
|
252
|
+
>
|
|
253
|
+
Yes
|
|
254
|
+
</button>
|
|
255
|
+
<button
|
|
256
|
+
type="button"
|
|
257
|
+
onclick={() =>
|
|
258
|
+
column.setFilterValue(boolFilter === false ? undefined : false)}
|
|
259
|
+
class={cn(
|
|
260
|
+
'flex-1 px-2 py-1',
|
|
261
|
+
boolFilter === false
|
|
262
|
+
? 'bg-surface-3 font-medium text-ink'
|
|
263
|
+
: 'text-ink-dim hover:bg-surface-2'
|
|
264
|
+
)}
|
|
265
|
+
>
|
|
266
|
+
No
|
|
267
|
+
</button>
|
|
268
|
+
</div>
|
|
269
|
+
{:else if getColumnType(column) === 'select'}
|
|
270
|
+
{@const allOptions = getFacetedValues(column)}
|
|
271
|
+
{@const search = localSelectSearch[column.id] ?? ''}
|
|
272
|
+
{@const options = search
|
|
273
|
+
? allOptions.filter((o) => o.toLowerCase().includes(search.toLowerCase()))
|
|
274
|
+
: allOptions}
|
|
275
|
+
{@const selected = getSelectValues(column)}
|
|
276
|
+
<div class="flex flex-col gap-1">
|
|
277
|
+
<Field.Root>
|
|
278
|
+
<Field.Input
|
|
279
|
+
placeholder="Search..."
|
|
280
|
+
value={search}
|
|
281
|
+
oninput={(e: Event) => {
|
|
282
|
+
localSelectSearch[column.id] = (
|
|
283
|
+
e.currentTarget as HTMLInputElement
|
|
284
|
+
).value;
|
|
285
|
+
}}
|
|
286
|
+
/>
|
|
287
|
+
</Field.Root>
|
|
288
|
+
<div class="max-h-44 overflow-hidden">
|
|
289
|
+
<ScrollArea.Root class="h-full">
|
|
290
|
+
<ScrollArea.Viewport>
|
|
291
|
+
<ScrollArea.Content>
|
|
292
|
+
<div class="flex flex-col gap-0.5">
|
|
293
|
+
{#each options as option (option)}
|
|
294
|
+
<Checkbox
|
|
295
|
+
size="sm"
|
|
296
|
+
label={option}
|
|
297
|
+
class="min-h-7 rounded-sm px-2 hover:bg-surface-2"
|
|
298
|
+
checked={selected.includes(option)}
|
|
299
|
+
onCheckedChange={({ checked }) =>
|
|
300
|
+
handleSelectChange(column, option, checked === true)}
|
|
301
|
+
/>
|
|
302
|
+
{/each}
|
|
303
|
+
</div>
|
|
304
|
+
</ScrollArea.Content>
|
|
305
|
+
</ScrollArea.Viewport>
|
|
306
|
+
<ScrollArea.Scrollbar orientation="vertical">
|
|
307
|
+
<ScrollArea.Thumb />
|
|
308
|
+
</ScrollArea.Scrollbar>
|
|
309
|
+
<ScrollArea.Corner />
|
|
310
|
+
</ScrollArea.Root>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
{:else}
|
|
314
|
+
<Field.Root>
|
|
315
|
+
<Field.Input
|
|
316
|
+
placeholder="Search..."
|
|
317
|
+
value={localText[column.id] ?? ''}
|
|
318
|
+
oninput={(e: Event) =>
|
|
319
|
+
handleTextInput(column, (e.currentTarget as HTMLInputElement).value)}
|
|
320
|
+
/>
|
|
321
|
+
</Field.Root>
|
|
322
|
+
{/if}
|
|
323
|
+
</div>
|
|
324
|
+
{/each}
|
|
325
|
+
</ScrollArea.Content>
|
|
326
|
+
</ScrollArea.Viewport>
|
|
327
|
+
<ScrollArea.Scrollbar orientation="vertical">
|
|
328
|
+
<ScrollArea.Thumb />
|
|
329
|
+
</ScrollArea.Scrollbar>
|
|
330
|
+
<ScrollArea.Corner />
|
|
331
|
+
</ScrollArea.Root>
|
|
332
|
+
</div>
|
|
333
|
+
{/if}
|
|
334
|
+
|
|
335
|
+
{#if showColumnPicker}
|
|
336
|
+
<div class="border-t border-surface-3 p-3">
|
|
337
|
+
<div class="relative mb-1">
|
|
338
|
+
<PhMagnifyingGlass
|
|
339
|
+
class="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-ink-dim"
|
|
340
|
+
/>
|
|
341
|
+
<input
|
|
342
|
+
type="text"
|
|
343
|
+
placeholder="Search columns..."
|
|
344
|
+
bind:value={columnSearchText}
|
|
345
|
+
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"
|
|
346
|
+
/>
|
|
347
|
+
</div>
|
|
348
|
+
<div class="max-h-48 overflow-y-auto">
|
|
349
|
+
{#each availableColumns as column (column.id)}
|
|
350
|
+
<button
|
|
351
|
+
type="button"
|
|
352
|
+
onclick={() => addFilter(column)}
|
|
353
|
+
class="w-full rounded px-2 py-1.5 text-left text-sm text-ink hover:bg-surface-2"
|
|
354
|
+
>
|
|
355
|
+
{getColumnLabel(column)}
|
|
356
|
+
</button>
|
|
357
|
+
{:else}
|
|
358
|
+
<p class="px-2 py-3 text-center text-sm text-ink-dim">No more columns</p>
|
|
359
|
+
{/each}
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
{:else}
|
|
363
|
+
<div class="border-t border-surface-3">
|
|
364
|
+
<button
|
|
365
|
+
type="button"
|
|
366
|
+
onclick={() => (showColumnPicker = true)}
|
|
367
|
+
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"
|
|
368
|
+
>
|
|
369
|
+
<span class="text-base leading-none">+</span>
|
|
370
|
+
Add Filter
|
|
371
|
+
</button>
|
|
372
|
+
</div>
|
|
373
|
+
{/if}
|
|
374
|
+
</Popover.Content>
|
|
375
|
+
</Popover.Root>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RowData } from '@tanstack/table-core';
|
|
2
|
-
import { type DataTableInstance } from '
|
|
2
|
+
import { type DataTableInstance } from '../data-table-utils';
|
|
3
3
|
declare function $$render<T extends RowData>(): {
|
|
4
4
|
props: {
|
|
5
5
|
table: DataTableInstance<T>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends RowData">
|
|
2
2
|
import type { RowData } from '@tanstack/table-core';
|
|
3
|
-
import * as Popover from '
|
|
4
|
-
import * as ScrollArea from '
|
|
5
|
-
import Checkbox from '
|
|
6
|
-
import { getReactiveTableState, type DataTableInstance } from '
|
|
3
|
+
import * as Popover from '../../popover';
|
|
4
|
+
import * as ScrollArea from '../../scroll-area';
|
|
5
|
+
import Checkbox from '../../checkbox/checkbox.svelte';
|
|
6
|
+
import { getReactiveTableState, type DataTableInstance } from '../data-table-utils';
|
|
7
7
|
|
|
8
8
|
type Props = {
|
|
9
9
|
table: DataTableInstance<T>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RowData } from '@tanstack/table-core';
|
|
2
|
-
import { type DataTableInstance } from '
|
|
2
|
+
import { type DataTableInstance } from '../data-table-utils';
|
|
3
3
|
declare function $$render<T extends RowData>(): {
|
|
4
4
|
props: {
|
|
5
5
|
table: DataTableInstance<T>;
|
package/dist/components/data-table-v8/{data-table-search.svelte → toolbar/data-table-search.svelte}
RENAMED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { onDestroy } from 'svelte';
|
|
3
3
|
import type { RowData } from '@tanstack/table-core';
|
|
4
4
|
import { cn, type ClassValue } from 'tailwind-variants';
|
|
5
|
-
import { PhMagnifyingGlass, PhX } from '
|
|
6
|
-
import * as Field from '
|
|
7
|
-
import { getReactiveTableState, type DataTableInstance } from '
|
|
5
|
+
import { PhMagnifyingGlass, PhX } from '../../../icons';
|
|
6
|
+
import * as Field from '../../field';
|
|
7
|
+
import { getReactiveTableState, type DataTableInstance } from '../data-table-utils';
|
|
8
8
|
|
|
9
9
|
type Props = {
|
|
10
10
|
table: DataTableInstance<T>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RowData } from '@tanstack/table-core';
|
|
2
2
|
import { type ClassValue } from 'tailwind-variants';
|
|
3
|
-
import { type DataTableInstance } from '
|
|
3
|
+
import { type DataTableInstance } from '../data-table-utils';
|
|
4
4
|
declare function $$render<T extends RowData>(): {
|
|
5
5
|
props: {
|
|
6
6
|
table: DataTableInstance<T>;
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import { createVirtualizer } from '@tanstack/svelte-virtual';
|
|
4
4
|
import { untrack } from 'svelte';
|
|
5
5
|
import { cn } from 'tailwind-variants';
|
|
6
|
-
import { PhArrowSquareOut, PhCheck, PhX } from '
|
|
7
|
-
import type { DataTableInstance } from '
|
|
8
|
-
import FlexRender from '
|
|
6
|
+
import { PhArrowSquareOut, PhCheck, PhX } from '../../../icons';
|
|
7
|
+
import type { DataTableInstance } from '../data-table-utils';
|
|
8
|
+
import FlexRender from '../flex-render.svelte';
|
|
9
9
|
import {
|
|
10
10
|
alignClass,
|
|
11
11
|
getBooleanCellValue,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
virtualColumnSizeStyle,
|
|
21
21
|
virtualGrowColumnSizeStyle,
|
|
22
22
|
virtualSelectionColumnSizeStyle
|
|
23
|
-
} from '
|
|
23
|
+
} from '../data-table-utils';
|
|
24
24
|
|
|
25
25
|
type Props = {
|
|
26
26
|
rows: Row<T>[];
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
import type { RowData } from '@tanstack/table-core';
|
|
3
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
4
|
import { cn, type ClassValue } from 'tailwind-variants';
|
|
5
|
-
import type { DataTableInstance } from '
|
|
6
|
-
import DataTableHead from '
|
|
5
|
+
import type { DataTableInstance } from '../data-table-utils';
|
|
6
|
+
import DataTableHead from '../data-table-head.svelte';
|
|
7
7
|
import DataTableVirtualRows from './data-table-virtual-rows.svelte';
|
|
8
8
|
import {
|
|
9
9
|
getAllRowsSelectionState,
|
|
10
10
|
getReactiveTableState,
|
|
11
11
|
getSelectedRowCount,
|
|
12
12
|
tableSizeStyle
|
|
13
|
-
} from '
|
|
13
|
+
} from '../data-table-utils';
|
|
14
14
|
|
|
15
15
|
type Props = Omit<HTMLAttributes<HTMLDivElement>, 'class'> & {
|
|
16
16
|
table: DataTableInstance<T>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RowData } from '@tanstack/table-core';
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
3
|
import { type ClassValue } from 'tailwind-variants';
|
|
4
|
-
import type { DataTableInstance } from '
|
|
4
|
+
import type { DataTableInstance } from '../data-table-utils';
|
|
5
5
|
declare function $$render<T extends RowData>(): {
|
|
6
6
|
props: Omit<HTMLAttributes<HTMLDivElement>, "class"> & {
|
|
7
7
|
table: DataTableInstance<T>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { default as Root } from '
|
|
1
|
+
export { default as Root } from './data-table-virtualized.svelte';
|
|
2
2
|
export { ColumnFilter, ColumnVisibility, FlexRender, Search, Title, Toolbar, createDataTableColumnHelper, createTable, renderComponent, renderSnippet } from '../index';
|
|
3
3
|
export type { CreateDataTableOptions, DataTableAccessorFnColumn, DataTableAccessorKeyColumn, DataTableAlign, DataTableCellPropsResolver, DataTableCellRenderProps, DataTableColumn, DataTableColumnBase, DataTableColumnOptions, DataTableColumnType, DataTableGroupColumn, DataTableInstance, DataTableLeafColumn, DataTableLeafColumnBase } from '../index';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { default as Root } from '
|
|
1
|
+
export { default as Root } from './data-table-virtualized.svelte';
|
|
2
2
|
export { ColumnFilter, ColumnVisibility, FlexRender, Search, Title, Toolbar, createDataTableColumnHelper, createTable, renderComponent, renderSnippet } from '../index';
|
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
import { Drawer } from '@ark-ui/svelte/drawer';
|
|
3
3
|
import type { DrawerRootProps } from '@ark-ui/svelte/drawer';
|
|
4
4
|
|
|
5
|
-
interface Props extends Omit<DrawerRootProps, 'open'
|
|
5
|
+
interface Props extends Omit<DrawerRootProps, 'open'> {
|
|
6
6
|
open?: boolean;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
let { children, open = $bindable(), ...rest }: Props = $props();
|
|
9
|
+
let { children, open = $bindable(), onOpenChange, ...rest }: Props = $props();
|
|
10
10
|
</script>
|
|
11
11
|
|
|
12
|
-
<Drawer.Root
|
|
12
|
+
<Drawer.Root
|
|
13
|
+
{open}
|
|
14
|
+
onOpenChange={(details) => {
|
|
15
|
+
open = details.open;
|
|
16
|
+
onOpenChange?.(details);
|
|
17
|
+
}}
|
|
18
|
+
{...rest}
|
|
19
|
+
>
|
|
13
20
|
{@render children?.()}
|
|
14
21
|
</Drawer.Root>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DrawerRootProps } from '@ark-ui/svelte/drawer';
|
|
2
|
-
interface Props extends Omit<DrawerRootProps, 'open'
|
|
2
|
+
interface Props extends Omit<DrawerRootProps, 'open'> {
|
|
3
3
|
open?: boolean;
|
|
4
4
|
}
|
|
5
5
|
declare const DrawerRoot: import("svelte").Component<Props, {}, "open">;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
const locale = useLocaleContext();
|
|
18
18
|
const rootClass = $derived(
|
|
19
19
|
layout === 'horizontal'
|
|
20
|
-
? 'flex items-center gap-1.5 w-full
|
|
20
|
+
? 'flex items-center gap-1.5 justify-between w-full w-full data-disabled:opacity-50 data-disabled:grayscale'
|
|
21
21
|
: 'flex flex-col gap-1.5 w-full max-w-48 data-disabled:opacity-50 data-disabled:grayscale'
|
|
22
22
|
);
|
|
23
23
|
</script>
|
|
@@ -28,7 +28,12 @@
|
|
|
28
28
|
{...restProps}
|
|
29
29
|
allowMouseWheel
|
|
30
30
|
locale={locale().locale}
|
|
31
|
-
value={value
|
|
31
|
+
value={value != null
|
|
32
|
+
? new Intl.NumberFormat(locale().locale, {
|
|
33
|
+
useGrouping: false,
|
|
34
|
+
maximumFractionDigits: 20
|
|
35
|
+
}).format(value)
|
|
36
|
+
: undefined}
|
|
32
37
|
readOnly={readonly}
|
|
33
38
|
onValueChange={(valueChangeDetails) => {
|
|
34
39
|
onValueChange?.(valueChangeDetails);
|
|
@@ -48,7 +53,7 @@
|
|
|
48
53
|
{/if}
|
|
49
54
|
<NumberInput.Control class="relative isolate">
|
|
50
55
|
<NumberInput.Input
|
|
51
|
-
class="h-9 w-
|
|
56
|
+
class="h-9 max-w-40 rounded-md border bg-surface-1 px-3 pr-8 text-right text-sm font-medium tabular-nums shadow-sm focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none data-invalid:border-danger data-invalid:focus-visible:ring-danger"
|
|
52
57
|
/>
|
|
53
58
|
<div
|
|
54
59
|
class="absolute top-px right-px bottom-px z-10 flex w-6 flex-col overflow-hidden rounded-r border-l"
|
|
@@ -10,6 +10,6 @@
|
|
|
10
10
|
let { class: className, children, ...rest }: Props = $props();
|
|
11
11
|
</script>
|
|
12
12
|
|
|
13
|
-
<ScrollArea.Content {...rest} class={cn('
|
|
13
|
+
<ScrollArea.Content {...rest} class={cn('p-3', className)}>
|
|
14
14
|
{@render children?.()}
|
|
15
15
|
</ScrollArea.Content>
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<ScrollArea.Scrollbar
|
|
14
14
|
{...rest}
|
|
15
15
|
class={cn(
|
|
16
|
-
'group pointer-events-none relative m-
|
|
16
|
+
'group pointer-events-none relative m-1 flex rounded-md bg-well opacity-0 transition-opacity duration-150',
|
|
17
17
|
'data-hover:pointer-events-auto data-hover:opacity-100',
|
|
18
18
|
'data-scrolling:pointer-events-auto data-scrolling:opacity-100 data-scrolling:duration-0',
|
|
19
19
|
'data-[orientation=vertical]:w-1 data-[orientation=vertical]:[&:not([data-overflow-y])]:hidden',
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
}: ToggleProps = $props();
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
|
-
<ArkToggle.Root
|
|
16
|
+
<ArkToggle.Root
|
|
17
|
+
{...rootProps}
|
|
18
|
+
bind:pressed
|
|
19
|
+
class={toggle({ size, icon, class: className as never })}
|
|
20
|
+
>
|
|
17
21
|
{@render children?.()}
|
|
18
22
|
</ArkToggle.Root>
|
|
@@ -36,7 +36,10 @@
|
|
|
36
36
|
|
|
37
37
|
const rootClass = $derived(
|
|
38
38
|
variant === 'ghost'
|
|
39
|
-
? cn(
|
|
39
|
+
? cn(
|
|
40
|
+
'inline-flex w-fit gap-1 data-[orientation=vertical]:flex-col data-[orientation=vertical]:gap-1',
|
|
41
|
+
className
|
|
42
|
+
)
|
|
40
43
|
: cn(
|
|
41
44
|
'inline-flex w-fit rounded border border-border data-[orientation=vertical]:flex-col [&>:not([hidden])~:not([hidden])]:border-border data-[orientation=horizontal]:[&>:not([hidden])~:not([hidden])]:border-s data-[orientation=horizontal]:[&>:not([hidden])~:not([hidden])]:border-e-0 data-[orientation=vertical]:[&>:not([hidden])~:not([hidden])]:border-t data-[orientation=vertical]:[&>:not([hidden])~:not([hidden])]:border-b-0',
|
|
42
45
|
className
|
|
@@ -44,10 +47,6 @@
|
|
|
44
47
|
);
|
|
45
48
|
</script>
|
|
46
49
|
|
|
47
|
-
<ToggleGroup.Root
|
|
48
|
-
{...rootProps}
|
|
49
|
-
bind:value
|
|
50
|
-
class={rootClass}
|
|
51
|
-
>
|
|
50
|
+
<ToggleGroup.Root {...rootProps} bind:value class={rootClass}>
|
|
52
51
|
{@render children?.()}
|
|
53
52
|
</ToggleGroup.Root>
|
package/package.json
CHANGED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
<script lang="ts" generics="T extends RowData">
|
|
2
|
-
import { onDestroy } from 'svelte';
|
|
3
|
-
import type { Column, RowData } from '@tanstack/table-core';
|
|
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 { getReactiveTableState, type DataTableInstance } from './data-table-utils';
|
|
9
|
-
import NumberInput from '../number-input/number-input.svelte';
|
|
10
|
-
import * as Field from '../field';
|
|
11
|
-
|
|
12
|
-
type Props = {
|
|
13
|
-
table: DataTableInstance<T>;
|
|
14
|
-
triggerLabel?: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
let { table, triggerLabel = 'Filters' }: Props = $props();
|
|
18
|
-
|
|
19
|
-
let localText: Record<string, string> = $state({});
|
|
20
|
-
let localNumMin: Record<string, number> = $state({});
|
|
21
|
-
let localNumMax: Record<string, number> = $state({});
|
|
22
|
-
const timers: Record<string, ReturnType<typeof setTimeout>> = {};
|
|
23
|
-
|
|
24
|
-
const columnFilters = $derived(getReactiveTableState(table).columnFilters);
|
|
25
|
-
const activeCount = $derived(columnFilters.length);
|
|
26
|
-
const filterableColumns = $derived.by(() => {
|
|
27
|
-
getReactiveTableState(table);
|
|
28
|
-
return table.getAllLeafColumns().filter((col) => col.getCanFilter());
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
onDestroy(() => {
|
|
32
|
-
Object.values(timers).forEach(clearTimeout);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
function getColumnType(column: Column<T, unknown>): string | undefined {
|
|
36
|
-
return (column.columnDef.meta as Record<string, unknown> | undefined)?.type as
|
|
37
|
-
| string
|
|
38
|
-
| undefined;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function getColumnLabel(column: Column<T, unknown>): string {
|
|
42
|
-
return typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function clearFilters() {
|
|
46
|
-
Object.values(timers).forEach(clearTimeout);
|
|
47
|
-
for (const key of Object.keys(timers)) delete timers[key];
|
|
48
|
-
localText = {};
|
|
49
|
-
localNumMin = {};
|
|
50
|
-
localNumMax = {};
|
|
51
|
-
table.resetColumnFilters();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function handleTextInput(column: Column<T, unknown>, value: string) {
|
|
55
|
-
localText[column.id] = value;
|
|
56
|
-
clearTimeout(timers[column.id]);
|
|
57
|
-
timers[column.id] = setTimeout(() => {
|
|
58
|
-
column.setFilterValue(value || undefined);
|
|
59
|
-
}, 300);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function handleNumericInput(
|
|
63
|
-
column: Column<T, unknown>,
|
|
64
|
-
which: 'min' | 'max',
|
|
65
|
-
value: number | null
|
|
66
|
-
) {
|
|
67
|
-
if (value === null) {
|
|
68
|
-
if (which === 'min') delete localNumMin[column.id];
|
|
69
|
-
else delete localNumMax[column.id];
|
|
70
|
-
} else {
|
|
71
|
-
if (which === 'min') localNumMin[column.id] = value;
|
|
72
|
-
else localNumMax[column.id] = value;
|
|
73
|
-
}
|
|
74
|
-
clearTimeout(timers[`${column.id}_${which}`]);
|
|
75
|
-
timers[`${column.id}_${which}`] = setTimeout(() => {
|
|
76
|
-
const min = localNumMin[column.id];
|
|
77
|
-
const max = localNumMax[column.id];
|
|
78
|
-
column.setFilterValue(min === undefined && max === undefined ? undefined : [min, max]);
|
|
79
|
-
}, 300);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function getSelectValues(column: Column<T, unknown>): string[] {
|
|
83
|
-
return (column.getFilterValue() as string[] | undefined) ?? [];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function handleSelectChange(column: Column<T, unknown>, value: string, checked: boolean) {
|
|
87
|
-
const current = getSelectValues(column);
|
|
88
|
-
const next = checked ? [...current, value] : current.filter((v) => v !== value);
|
|
89
|
-
column.setFilterValue(next.length ? next : undefined);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function getFacetedValues(column: Column<T, unknown>): string[] {
|
|
93
|
-
return Array.from(column.getFacetedUniqueValues().keys()).map(String).sort();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function getFacetedMinMax(column: Column<T, unknown>): [number | undefined, number | undefined] {
|
|
97
|
-
const vals = column.getFacetedMinMaxValues();
|
|
98
|
-
return vals ? [vals[0] as number, vals[1] as number] : [undefined, undefined];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function getColumnFormatOptions(
|
|
102
|
-
column: Column<T, unknown>
|
|
103
|
-
): Intl.NumberFormatOptions | undefined {
|
|
104
|
-
return (column.columnDef.meta as Record<string, unknown> | undefined)?.formatOptions as
|
|
105
|
-
| Intl.NumberFormatOptions
|
|
106
|
-
| undefined;
|
|
107
|
-
}
|
|
108
|
-
</script>
|
|
109
|
-
|
|
110
|
-
<Popover.Root positioning={{ placement: 'bottom-end' }}>
|
|
111
|
-
<Popover.Trigger
|
|
112
|
-
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"
|
|
113
|
-
>
|
|
114
|
-
{triggerLabel}
|
|
115
|
-
{#if activeCount > 0}
|
|
116
|
-
({activeCount})
|
|
117
|
-
{/if}
|
|
118
|
-
</Popover.Trigger>
|
|
119
|
-
|
|
120
|
-
<Popover.Content class="w-72 px-0" showArrow={false}>
|
|
121
|
-
<div class="mb-2 flex items-center justify-between px-4">
|
|
122
|
-
<span class="text-sm font-medium text-ink">Filters</span>
|
|
123
|
-
{#if activeCount > 0}
|
|
124
|
-
<button type="button" onclick={clearFilters} class="text-xs text-primary hover:underline">
|
|
125
|
-
Clear all
|
|
126
|
-
</button>
|
|
127
|
-
{/if}
|
|
128
|
-
</div>
|
|
129
|
-
<div class="mb-2 border-b border-surface-3"></div>
|
|
130
|
-
|
|
131
|
-
<ScrollArea.Root class="h-80">
|
|
132
|
-
<ScrollArea.Viewport>
|
|
133
|
-
<ScrollArea.Content>
|
|
134
|
-
{#each filterableColumns as column (column.id)}
|
|
135
|
-
<div class="mb-3">
|
|
136
|
-
<p class="mb-1 text-xs font-medium text-ink">{getColumnLabel(column)}</p>
|
|
137
|
-
|
|
138
|
-
{#if getColumnType(column) === 'number' || getColumnType(column) === 'currency' || getColumnType(column) === 'percent'}
|
|
139
|
-
{@const [facetMin, facetMax] = getFacetedMinMax(column)}
|
|
140
|
-
{@const colFormatOptions = getColumnFormatOptions(column)}
|
|
141
|
-
<div class="flex gap-1.5">
|
|
142
|
-
<div class="min-w-0 flex-1">
|
|
143
|
-
<p class="mb-1 text-xs text-ink-dim">From</p>
|
|
144
|
-
<NumberInput
|
|
145
|
-
value={localNumMin[column.id] ?? facetMin ?? null}
|
|
146
|
-
min={facetMin}
|
|
147
|
-
max={facetMax}
|
|
148
|
-
formatOptions={colFormatOptions}
|
|
149
|
-
onValueChange={({ valueAsNumber }) =>
|
|
150
|
-
handleNumericInput(
|
|
151
|
-
column,
|
|
152
|
-
'min',
|
|
153
|
-
isNaN(valueAsNumber) ? null : valueAsNumber
|
|
154
|
-
)}
|
|
155
|
-
/>
|
|
156
|
-
</div>
|
|
157
|
-
<div class="min-w-0 flex-1">
|
|
158
|
-
<p class="mb-1 text-xs text-ink-dim">To</p>
|
|
159
|
-
<NumberInput
|
|
160
|
-
value={localNumMax[column.id] ?? facetMax ?? null}
|
|
161
|
-
min={facetMin}
|
|
162
|
-
max={facetMax}
|
|
163
|
-
formatOptions={colFormatOptions}
|
|
164
|
-
onValueChange={({ valueAsNumber }) =>
|
|
165
|
-
handleNumericInput(
|
|
166
|
-
column,
|
|
167
|
-
'max',
|
|
168
|
-
isNaN(valueAsNumber) ? null : valueAsNumber
|
|
169
|
-
)}
|
|
170
|
-
/>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
{:else if getColumnType(column) === 'boolean'}
|
|
174
|
-
{@const boolFilter = column.getFilterValue() as boolean | undefined}
|
|
175
|
-
<div class="flex overflow-hidden rounded border border-border text-xs">
|
|
176
|
-
<button
|
|
177
|
-
type="button"
|
|
178
|
-
onclick={() => column.setFilterValue(undefined)}
|
|
179
|
-
class={cn(
|
|
180
|
-
'flex-1 px-2 py-1',
|
|
181
|
-
boolFilter === undefined
|
|
182
|
-
? 'bg-surface-3 font-medium text-ink'
|
|
183
|
-
: 'text-ink-dim hover:bg-surface-2'
|
|
184
|
-
)}
|
|
185
|
-
>
|
|
186
|
-
All
|
|
187
|
-
</button>
|
|
188
|
-
<button
|
|
189
|
-
type="button"
|
|
190
|
-
onclick={() => column.setFilterValue(boolFilter === true ? undefined : true)}
|
|
191
|
-
class={cn(
|
|
192
|
-
'flex-1 border-x border-border px-2 py-1',
|
|
193
|
-
boolFilter === true
|
|
194
|
-
? 'bg-surface-3 font-medium text-ink'
|
|
195
|
-
: 'text-ink-dim hover:bg-surface-2'
|
|
196
|
-
)}
|
|
197
|
-
>
|
|
198
|
-
Yes
|
|
199
|
-
</button>
|
|
200
|
-
<button
|
|
201
|
-
type="button"
|
|
202
|
-
onclick={() => column.setFilterValue(boolFilter === false ? undefined : false)}
|
|
203
|
-
class={cn(
|
|
204
|
-
'flex-1 px-2 py-1',
|
|
205
|
-
boolFilter === false
|
|
206
|
-
? 'bg-surface-3 font-medium text-ink'
|
|
207
|
-
: 'text-ink-dim hover:bg-surface-2'
|
|
208
|
-
)}
|
|
209
|
-
>
|
|
210
|
-
No
|
|
211
|
-
</button>
|
|
212
|
-
</div>
|
|
213
|
-
{:else if getColumnType(column) === 'select'}
|
|
214
|
-
{@const options = getFacetedValues(column)}
|
|
215
|
-
{@const selected = getSelectValues(column)}
|
|
216
|
-
<div class="flex flex-col gap-0.5">
|
|
217
|
-
{#each options as option (option)}
|
|
218
|
-
<Checkbox
|
|
219
|
-
size="sm"
|
|
220
|
-
label={option}
|
|
221
|
-
class="min-h-7 rounded-sm px-2 hover:bg-surface-2"
|
|
222
|
-
checked={selected.includes(option)}
|
|
223
|
-
onCheckedChange={({ checked }) =>
|
|
224
|
-
handleSelectChange(column, option, checked === true)}
|
|
225
|
-
/>
|
|
226
|
-
{/each}
|
|
227
|
-
</div>
|
|
228
|
-
{:else}
|
|
229
|
-
<Field.Root>
|
|
230
|
-
<Field.Input
|
|
231
|
-
placeholder="Search..."
|
|
232
|
-
value={localText[column.id] ?? ''}
|
|
233
|
-
oninput={(e: Event) =>
|
|
234
|
-
handleTextInput(column, (e.currentTarget as HTMLInputElement).value)}
|
|
235
|
-
/>
|
|
236
|
-
</Field.Root>
|
|
237
|
-
{/if}
|
|
238
|
-
</div>
|
|
239
|
-
{/each}
|
|
240
|
-
</ScrollArea.Content>
|
|
241
|
-
</ScrollArea.Viewport>
|
|
242
|
-
<ScrollArea.Scrollbar orientation="vertical">
|
|
243
|
-
<ScrollArea.Thumb />
|
|
244
|
-
</ScrollArea.Scrollbar>
|
|
245
|
-
<ScrollArea.Corner />
|
|
246
|
-
</ScrollArea.Root>
|
|
247
|
-
</Popover.Content>
|
|
248
|
-
</Popover.Root>
|
|
File without changes
|
|
File without changes
|