compote-ui 0.79.2 → 0.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/data-table-v9/column-filter-editing.svelte.d.ts +41 -0
- package/dist/components/data-table-v9/column-filter-editing.svelte.js +88 -0
- package/dist/components/data-table-v9/column-filter-kind.d.ts +10 -0
- package/dist/components/data-table-v9/column-filter-kind.js +25 -0
- package/dist/components/data-table-v9/create-table.svelte.js +37 -25
- package/dist/components/data-table-v9/data-table-head.svelte +230 -201
- package/dist/components/data-table-v9/data-table-head.svelte.d.ts +2 -0
- package/dist/components/data-table-v9/data-table-utils.d.ts +7 -0
- package/dist/components/data-table-v9/data-table-utils.js +4 -0
- package/dist/components/data-table-v9/data-table.svelte +154 -151
- package/dist/components/data-table-v9/data-table.svelte.d.ts +2 -0
- package/dist/components/data-table-v9/date-cell.d.ts +13 -0
- package/dist/components/data-table-v9/date-cell.js +38 -0
- package/dist/components/data-table-v9/filter-editors/boolean-filter-editor.svelte +45 -0
- package/dist/components/data-table-v9/filter-editors/boolean-filter-editor.svelte.d.ts +7 -0
- package/dist/components/data-table-v9/filter-editors/column-filter-editor.svelte +33 -0
- package/dist/components/data-table-v9/filter-editors/column-filter-editor.svelte.d.ts +30 -0
- package/dist/components/data-table-v9/filter-editors/date-range-filter-editor.svelte +52 -0
- package/dist/components/data-table-v9/filter-editors/date-range-filter-editor.svelte.d.ts +28 -0
- package/dist/components/data-table-v9/filter-editors/range-filter-editor.svelte +51 -0
- package/dist/components/data-table-v9/filter-editors/range-filter-editor.svelte.d.ts +30 -0
- package/dist/components/data-table-v9/filter-editors/select-filter-editor.svelte +59 -0
- package/dist/components/data-table-v9/filter-editors/select-filter-editor.svelte.d.ts +28 -0
- package/dist/components/data-table-v9/filter-editors/text-filter-editor.svelte +20 -0
- package/dist/components/data-table-v9/filter-editors/text-filter-editor.svelte.d.ts +8 -0
- package/dist/components/data-table-v9/header/column-header-filter.svelte +66 -0
- package/dist/components/data-table-v9/header/column-header-filter.svelte.d.ts +30 -0
- package/dist/components/data-table-v9/table-view-state.svelte.d.ts +1 -0
- package/dist/components/data-table-v9/table-view-state.svelte.js +3 -0
- package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte +203 -459
- package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte.d.ts +1 -1
- package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte +82 -79
- package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte.d.ts +2 -0
- package/dist/components/dialog/alert-dialog.svelte +73 -71
- package/dist/components/dialog/dialog-root.svelte +45 -43
- package/dist/components/drawer/drawer-content.svelte +213 -211
- package/dist/components/popover/popover-content.svelte +97 -71
- package/dist/components/popover/popover-content.svelte.d.ts +2 -1
- package/dist/components/popover/popover-description.svelte +19 -15
- package/dist/components/popover/popover-labels.svelte.d.ts +18 -0
- package/dist/components/popover/popover-labels.svelte.js +35 -0
- package/dist/components/popover/popover-root.svelte +27 -24
- package/dist/components/popover/popover-title.svelte +19 -15
- package/dist/icons/PhFunnelFill.svelte +19 -0
- package/dist/icons/PhFunnelFill.svelte.d.ts +5 -0
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/utils/date-filter.d.ts +31 -0
- package/dist/utils/date-filter.js +58 -0
- package/dist/utils/date.d.ts +19 -1
- package/dist/utils/date.js +48 -1
- package/dist/utils/release-focus-on-exit.d.ts +22 -0
- package/dist/utils/release-focus-on-exit.js +26 -0
- package/package.json +1 -1
- package/skills/component-usage/references/overlays-and-floating-ui.md +278 -273
- package/skills/data-table/SKILL.md +314 -283
- package/skills/data-table/references/column-options.md +175 -150
- package/skills/data-table/references/table-state-and-reactivity.md +65 -60
|
@@ -1,459 +1,203 @@
|
|
|
1
|
-
<script lang="ts" generics="T extends RowData">
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
column: Column<DataTableFeatures, T, unknown>
|
|
205
|
-
): Intl.NumberFormatOptions | undefined {
|
|
206
|
-
return (column.columnDef.meta as Record<string, unknown> | undefined)?.formatOptions as
|
|
207
|
-
Intl.NumberFormatOptions | undefined;
|
|
208
|
-
}
|
|
209
|
-
</script>
|
|
210
|
-
|
|
211
|
-
<Popover.Root ids={{ trigger: triggerId }} positioning={{ placement: 'bottom-end' }}>
|
|
212
|
-
<Tooltip.Root ids={{ trigger: triggerId }} disabled={!!triggerLabel}>
|
|
213
|
-
<Tooltip.Trigger>
|
|
214
|
-
{#snippet asChild(tooltipProps)}
|
|
215
|
-
<Popover.Trigger
|
|
216
|
-
{...tooltipProps()}
|
|
217
|
-
aria-label={triggerLabel ? undefined : 'Filters'}
|
|
218
|
-
class={button({
|
|
219
|
-
variant: 'outline',
|
|
220
|
-
size: triggerLabel ? 'default' : 'icon',
|
|
221
|
-
class: 'relative'
|
|
222
|
-
})}
|
|
223
|
-
>
|
|
224
|
-
{#if triggerLabel}
|
|
225
|
-
{triggerLabel}
|
|
226
|
-
{:else}
|
|
227
|
-
<PhFunnel />
|
|
228
|
-
{/if}
|
|
229
|
-
{#if activeCount > 0}
|
|
230
|
-
<Badge
|
|
231
|
-
variant="solid"
|
|
232
|
-
color="primary"
|
|
233
|
-
class="absolute top-0 right-0 size-4 translate-x-1/3 -translate-y-1/3 justify-center px-1 text-xs"
|
|
234
|
-
>
|
|
235
|
-
{activeCount > 99 ? '99+' : activeCount}
|
|
236
|
-
</Badge>
|
|
237
|
-
{/if}
|
|
238
|
-
</Popover.Trigger>
|
|
239
|
-
{/snippet}
|
|
240
|
-
</Tooltip.Trigger>
|
|
241
|
-
<Tooltip.Content>Filters</Tooltip.Content>
|
|
242
|
-
</Tooltip.Root>
|
|
243
|
-
|
|
244
|
-
<Popover.Content class="flex w-70 flex-col gap-3 p-3" showArrow={false}>
|
|
245
|
-
<div class="mr-1 flex items-center justify-between py-2.5">
|
|
246
|
-
<span class="text-sm font-medium text-ink">Filters</span>
|
|
247
|
-
{#if activeCount > 0}
|
|
248
|
-
<button type="button" onclick={clearFilters} class="text-xs text-primary hover:underline">
|
|
249
|
-
Clear all
|
|
250
|
-
</button>
|
|
251
|
-
{/if}
|
|
252
|
-
</div>
|
|
253
|
-
|
|
254
|
-
{#if activeColumns.length > 0}
|
|
255
|
-
<div class="overflow-hidden border-t border-surface-2">
|
|
256
|
-
<ScrollArea.Root>
|
|
257
|
-
<ScrollArea.Viewport class="max-h-96">
|
|
258
|
-
<ScrollArea.Content class="flex flex-col gap-3">
|
|
259
|
-
{#each activeColumns as column (column.id)}
|
|
260
|
-
<div class="border border-surface-3 p-3">
|
|
261
|
-
<div class="mb-2 flex items-center justify-between">
|
|
262
|
-
<span class="text-sm font-medium text-ink">{getColumnLabel(column)}</span>
|
|
263
|
-
<button
|
|
264
|
-
type="button"
|
|
265
|
-
onclick={() => removeFilter(column)}
|
|
266
|
-
class="text-ink-dim transition-colors hover:text-ink"
|
|
267
|
-
>
|
|
268
|
-
<PhX class="size-3.5" />
|
|
269
|
-
</button>
|
|
270
|
-
</div>
|
|
271
|
-
|
|
272
|
-
{#if getColumnType(column) === 'number' || getColumnType(column) === 'currency' || getColumnType(column) === 'percent'}
|
|
273
|
-
{@const [facetMin, facetMax] = getFacetedMinMax(column)}
|
|
274
|
-
{@const colFormatOptions = getColumnFormatOptions(column)}
|
|
275
|
-
<div class="flex flex-col gap-1.5">
|
|
276
|
-
<div class="min-w-0 flex-1">
|
|
277
|
-
<NumberInput
|
|
278
|
-
layout="horizontal"
|
|
279
|
-
label="From"
|
|
280
|
-
bind:value={
|
|
281
|
-
() => getRangeBound(column, 'min'),
|
|
282
|
-
(bound) => handleNumericInput(column, 'min', bound ?? null)
|
|
283
|
-
}
|
|
284
|
-
min={facetMin}
|
|
285
|
-
max={facetMax}
|
|
286
|
-
formatOptions={colFormatOptions}
|
|
287
|
-
/>
|
|
288
|
-
</div>
|
|
289
|
-
<div class="min-w-0 flex-1">
|
|
290
|
-
<NumberInput
|
|
291
|
-
layout="horizontal"
|
|
292
|
-
label="To"
|
|
293
|
-
bind:value={
|
|
294
|
-
() => getRangeBound(column, 'max'),
|
|
295
|
-
(bound) => handleNumericInput(column, 'max', bound ?? null)
|
|
296
|
-
}
|
|
297
|
-
min={facetMin}
|
|
298
|
-
max={facetMax}
|
|
299
|
-
formatOptions={colFormatOptions}
|
|
300
|
-
/>
|
|
301
|
-
</div>
|
|
302
|
-
</div>
|
|
303
|
-
{:else if getColumnType(column) === 'boolean'}
|
|
304
|
-
{@const boolFilter = column.getFilterValue() as boolean | undefined}
|
|
305
|
-
<div class="flex overflow-hidden rounded border border-border text-xs">
|
|
306
|
-
<button
|
|
307
|
-
type="button"
|
|
308
|
-
onclick={() => column.setFilterValue(undefined)}
|
|
309
|
-
class={cn(
|
|
310
|
-
'flex-1 px-2 py-1',
|
|
311
|
-
boolFilter === undefined
|
|
312
|
-
? 'bg-surface-3 font-medium text-ink'
|
|
313
|
-
: 'text-ink-dim hover:bg-surface-2'
|
|
314
|
-
)}
|
|
315
|
-
>
|
|
316
|
-
All
|
|
317
|
-
</button>
|
|
318
|
-
<button
|
|
319
|
-
type="button"
|
|
320
|
-
onclick={() =>
|
|
321
|
-
column.setFilterValue(boolFilter === true ? undefined : true)}
|
|
322
|
-
class={cn(
|
|
323
|
-
'flex-1 border-x border-border px-2 py-1',
|
|
324
|
-
boolFilter === true
|
|
325
|
-
? 'bg-surface-3 font-medium text-ink'
|
|
326
|
-
: 'text-ink-dim hover:bg-surface-2'
|
|
327
|
-
)}
|
|
328
|
-
>
|
|
329
|
-
Yes
|
|
330
|
-
</button>
|
|
331
|
-
<button
|
|
332
|
-
type="button"
|
|
333
|
-
onclick={() =>
|
|
334
|
-
column.setFilterValue(boolFilter === false ? undefined : false)}
|
|
335
|
-
class={cn(
|
|
336
|
-
'flex-1 px-2 py-1',
|
|
337
|
-
boolFilter === false
|
|
338
|
-
? 'bg-surface-3 font-medium text-ink'
|
|
339
|
-
: 'text-ink-dim hover:bg-surface-2'
|
|
340
|
-
)}
|
|
341
|
-
>
|
|
342
|
-
No
|
|
343
|
-
</button>
|
|
344
|
-
</div>
|
|
345
|
-
{:else if getColumnType(column) === 'select'}
|
|
346
|
-
{@const allOptions = getSelectOptions(column)}
|
|
347
|
-
{@const search = localSelectSearch[column.id] ?? ''}
|
|
348
|
-
{@const options = search
|
|
349
|
-
? allOptions.filter((o) =>
|
|
350
|
-
o.label.toLowerCase().includes(search.toLowerCase())
|
|
351
|
-
)
|
|
352
|
-
: allOptions}
|
|
353
|
-
{@const selected = getSelectValues(column)}
|
|
354
|
-
<div class="flex flex-col gap-1">
|
|
355
|
-
<Field.Root hideMessageLine>
|
|
356
|
-
<Field.Input
|
|
357
|
-
placeholder="Search..."
|
|
358
|
-
value={search}
|
|
359
|
-
oninput={(e: Event) => {
|
|
360
|
-
localSelectSearch[column.id] = (
|
|
361
|
-
e.currentTarget as HTMLInputElement
|
|
362
|
-
).value;
|
|
363
|
-
}}
|
|
364
|
-
/>
|
|
365
|
-
</Field.Root>
|
|
366
|
-
<ScrollArea.Root>
|
|
367
|
-
<ScrollArea.Viewport class="max-h-40">
|
|
368
|
-
<ScrollArea.Content>
|
|
369
|
-
<div class="flex flex-col gap-0.5">
|
|
370
|
-
{#each options as option (option.value)}
|
|
371
|
-
<div
|
|
372
|
-
class="flex min-h-7 items-center gap-2 rounded-sm pr-2 hover:bg-surface-2"
|
|
373
|
-
>
|
|
374
|
-
<Checkbox
|
|
375
|
-
size="sm"
|
|
376
|
-
label={option.label}
|
|
377
|
-
class="min-h-7 flex-1 px-2"
|
|
378
|
-
checked={selected.includes(option.value)}
|
|
379
|
-
onCheckedChange={({ checked }) =>
|
|
380
|
-
handleSelectChange(column, option.value, checked === true)}
|
|
381
|
-
/>
|
|
382
|
-
<span class="text-xs text-ink-dim tabular-nums"
|
|
383
|
-
>{option.count}</span
|
|
384
|
-
>
|
|
385
|
-
</div>
|
|
386
|
-
{/each}
|
|
387
|
-
</div>
|
|
388
|
-
</ScrollArea.Content>
|
|
389
|
-
</ScrollArea.Viewport>
|
|
390
|
-
<ScrollArea.Scrollbar orientation="vertical">
|
|
391
|
-
<ScrollArea.Thumb />
|
|
392
|
-
</ScrollArea.Scrollbar>
|
|
393
|
-
<ScrollArea.Corner />
|
|
394
|
-
</ScrollArea.Root>
|
|
395
|
-
</div>
|
|
396
|
-
{:else}
|
|
397
|
-
<Field.Root hideMessageLine>
|
|
398
|
-
<Field.Input
|
|
399
|
-
placeholder="Search..."
|
|
400
|
-
bind:value={
|
|
401
|
-
() => getTextValue(column),
|
|
402
|
-
(text) => handleTextInput(column, String(text ?? ''))
|
|
403
|
-
}
|
|
404
|
-
/>
|
|
405
|
-
</Field.Root>
|
|
406
|
-
{/if}
|
|
407
|
-
</div>
|
|
408
|
-
{/each}
|
|
409
|
-
</ScrollArea.Content>
|
|
410
|
-
</ScrollArea.Viewport>
|
|
411
|
-
<ScrollArea.Scrollbar orientation="vertical">
|
|
412
|
-
<ScrollArea.Thumb />
|
|
413
|
-
</ScrollArea.Scrollbar>
|
|
414
|
-
<ScrollArea.Corner />
|
|
415
|
-
</ScrollArea.Root>
|
|
416
|
-
</div>
|
|
417
|
-
{/if}
|
|
418
|
-
|
|
419
|
-
{#if showColumnPicker}
|
|
420
|
-
<div class="border-t border-surface-3 p-3">
|
|
421
|
-
<div class="relative mb-1">
|
|
422
|
-
<PhMagnifyingGlass
|
|
423
|
-
class="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-ink-dim"
|
|
424
|
-
/>
|
|
425
|
-
<input
|
|
426
|
-
type="text"
|
|
427
|
-
placeholder="Search columns..."
|
|
428
|
-
bind:value={columnSearchText}
|
|
429
|
-
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"
|
|
430
|
-
/>
|
|
431
|
-
</div>
|
|
432
|
-
<div class="max-h-48 overflow-y-auto">
|
|
433
|
-
{#each availableColumns as column (column.id)}
|
|
434
|
-
<button
|
|
435
|
-
type="button"
|
|
436
|
-
onclick={() => addFilter(column)}
|
|
437
|
-
class="w-full rounded px-2 py-1.5 text-left text-sm text-ink hover:bg-surface-2"
|
|
438
|
-
>
|
|
439
|
-
{getColumnLabel(column)}
|
|
440
|
-
</button>
|
|
441
|
-
{:else}
|
|
442
|
-
<p class="px-2 py-3 text-center text-sm text-ink-dim">No more columns</p>
|
|
443
|
-
{/each}
|
|
444
|
-
</div>
|
|
445
|
-
</div>
|
|
446
|
-
{:else}
|
|
447
|
-
<div class="border-t border-surface-3">
|
|
448
|
-
<button
|
|
449
|
-
type="button"
|
|
450
|
-
onclick={() => (showColumnPicker = true)}
|
|
451
|
-
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"
|
|
452
|
-
>
|
|
453
|
-
<span class="text-base leading-none">+</span>
|
|
454
|
-
Add Filter
|
|
455
|
-
</button>
|
|
456
|
-
</div>
|
|
457
|
-
{/if}
|
|
458
|
-
</Popover.Content>
|
|
459
|
-
</Popover.Root>
|
|
1
|
+
<script lang="ts" generics="T extends RowData">
|
|
2
|
+
import type { Column, RowData } from '@tanstack/svelte-table';
|
|
3
|
+
import * as Popover from '../../popover';
|
|
4
|
+
import * as Tooltip from '../../tooltip';
|
|
5
|
+
import * as ScrollArea from '../../scroll-area';
|
|
6
|
+
import { getColumnLabel, type DataTableInstance } from '../data-table-utils';
|
|
7
|
+
import type { DataTableFeatures } from '../features';
|
|
8
|
+
import Badge from '../../badge/badge.svelte';
|
|
9
|
+
import { button } from '../../button/button.variants';
|
|
10
|
+
import { PhX, PhMagnifyingGlass, PhFunnel } from '../../../icons';
|
|
11
|
+
import { ColumnFilterEditing } from '../column-filter-editing.svelte';
|
|
12
|
+
import ColumnFilterEditor from '../filter-editors/column-filter-editor.svelte';
|
|
13
|
+
|
|
14
|
+
type Props = {
|
|
15
|
+
table: DataTableInstance<T>;
|
|
16
|
+
/** Text label for the trigger. When omitted, a funnel icon is shown instead. */
|
|
17
|
+
triggerLabel?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let { table, triggerLabel }: Props = $props();
|
|
21
|
+
|
|
22
|
+
// Popover and tooltip share one trigger element, so both must agree on its id.
|
|
23
|
+
const triggerId = $props.id();
|
|
24
|
+
|
|
25
|
+
// Pending edits belong to the table: shared with the other filter hosts, and they still
|
|
26
|
+
// commit after this popover closes.
|
|
27
|
+
const editing = new ColumnFilterEditing({
|
|
28
|
+
table: () => table,
|
|
29
|
+
isActive: (columnId) => activeFilterIds.includes(columnId)
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const columnFilters = $derived.by(() => table.atoms.columnFilters.get());
|
|
33
|
+
const columnVisibility = $derived.by(() => table.atoms.columnVisibility.get());
|
|
34
|
+
const activeCount = $derived(columnFilters.length);
|
|
35
|
+
|
|
36
|
+
// Filters the user has explicitly opened. Kept separate from the table's
|
|
37
|
+
// columnFilters so a filter card stays visible after its value is cleared
|
|
38
|
+
// (e.g. unchecking the last select option) until removed via the X button.
|
|
39
|
+
let manualFilterIds = $state<string[]>([]);
|
|
40
|
+
const activeFilterIds: string[] = $derived.by(() => {
|
|
41
|
+
const ids = columnFilters.map((f) => f.id);
|
|
42
|
+
for (const id of manualFilterIds) {
|
|
43
|
+
if (!ids.includes(id)) ids.push(id);
|
|
44
|
+
}
|
|
45
|
+
return ids;
|
|
46
|
+
});
|
|
47
|
+
let showColumnPicker = $state(false);
|
|
48
|
+
let columnSearchText = $state('');
|
|
49
|
+
|
|
50
|
+
const activeColumns = $derived.by(() => {
|
|
51
|
+
return activeFilterIds
|
|
52
|
+
.map((id) => table.getColumn(id))
|
|
53
|
+
.filter((col): col is Column<DataTableFeatures, T, unknown> => col != null);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const availableColumns = $derived.by(() => {
|
|
57
|
+
void columnVisibility;
|
|
58
|
+
return table
|
|
59
|
+
.getAllLeafColumns()
|
|
60
|
+
.filter((col) => col.getCanFilter() && !activeFilterIds.includes(col.id))
|
|
61
|
+
.filter(
|
|
62
|
+
(col) =>
|
|
63
|
+
!columnSearchText ||
|
|
64
|
+
getColumnLabel(col).toLowerCase().includes(columnSearchText.toLowerCase())
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function addFilter(column: Column<DataTableFeatures, T, unknown>) {
|
|
69
|
+
manualFilterIds = [...manualFilterIds, column.id];
|
|
70
|
+
showColumnPicker = false;
|
|
71
|
+
columnSearchText = '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function removeFilter(column: Column<DataTableFeatures, T, unknown>) {
|
|
75
|
+
manualFilterIds = manualFilterIds.filter((id) => id !== column.id);
|
|
76
|
+
editing.cancel(column.id);
|
|
77
|
+
column.setFilterValue(undefined);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function clearFilters() {
|
|
81
|
+
editing.cancelAll();
|
|
82
|
+
manualFilterIds = [];
|
|
83
|
+
showColumnPicker = false;
|
|
84
|
+
columnSearchText = '';
|
|
85
|
+
table.resetColumnFilters();
|
|
86
|
+
}
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<Popover.Root ids={{ trigger: triggerId }} positioning={{ placement: 'bottom-end' }}>
|
|
90
|
+
<Tooltip.Root ids={{ trigger: triggerId }} disabled={!!triggerLabel}>
|
|
91
|
+
<Tooltip.Trigger>
|
|
92
|
+
{#snippet asChild(tooltipProps)}
|
|
93
|
+
<Popover.Trigger
|
|
94
|
+
{...tooltipProps()}
|
|
95
|
+
aria-label={triggerLabel ? undefined : 'Filters'}
|
|
96
|
+
class={button({
|
|
97
|
+
variant: 'outline',
|
|
98
|
+
size: triggerLabel ? 'default' : 'icon',
|
|
99
|
+
class: 'relative'
|
|
100
|
+
})}
|
|
101
|
+
>
|
|
102
|
+
{#if triggerLabel}
|
|
103
|
+
{triggerLabel}
|
|
104
|
+
{:else}
|
|
105
|
+
<PhFunnel />
|
|
106
|
+
{/if}
|
|
107
|
+
{#if activeCount > 0}
|
|
108
|
+
<Badge
|
|
109
|
+
variant="solid"
|
|
110
|
+
color="primary"
|
|
111
|
+
class="absolute top-0 right-0 size-4 translate-x-1/3 -translate-y-1/3 justify-center px-1 text-xs"
|
|
112
|
+
>
|
|
113
|
+
{activeCount > 99 ? '99+' : activeCount}
|
|
114
|
+
</Badge>
|
|
115
|
+
{/if}
|
|
116
|
+
</Popover.Trigger>
|
|
117
|
+
{/snippet}
|
|
118
|
+
</Tooltip.Trigger>
|
|
119
|
+
<Tooltip.Content>Filters</Tooltip.Content>
|
|
120
|
+
</Tooltip.Root>
|
|
121
|
+
|
|
122
|
+
<Popover.Content class="flex w-70 flex-col gap-3 p-3" showArrow={false}>
|
|
123
|
+
<div class="mr-1 flex items-center justify-between py-2.5">
|
|
124
|
+
<Popover.Title class="text-sm font-medium text-ink">Filters</Popover.Title>
|
|
125
|
+
{#if activeCount > 0}
|
|
126
|
+
<button type="button" onclick={clearFilters} class="text-xs text-primary hover:underline">
|
|
127
|
+
Clear all
|
|
128
|
+
</button>
|
|
129
|
+
{/if}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{#if activeColumns.length > 0}
|
|
133
|
+
<div class="overflow-hidden border-t border-surface-2">
|
|
134
|
+
<ScrollArea.Root>
|
|
135
|
+
<ScrollArea.Viewport class="max-h-96">
|
|
136
|
+
<ScrollArea.Content class="flex flex-col gap-3">
|
|
137
|
+
{#each activeColumns as column (column.id)}
|
|
138
|
+
<div class="border border-surface-3 p-3">
|
|
139
|
+
<div class="mb-2 flex items-center justify-between">
|
|
140
|
+
<span class="text-sm font-medium text-ink">{getColumnLabel(column)}</span>
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onclick={() => removeFilter(column)}
|
|
144
|
+
class="text-ink-dim transition-colors hover:text-ink"
|
|
145
|
+
>
|
|
146
|
+
<PhX class="size-3.5" />
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<ColumnFilterEditor {column} {editing} />
|
|
151
|
+
</div>
|
|
152
|
+
{/each}
|
|
153
|
+
</ScrollArea.Content>
|
|
154
|
+
</ScrollArea.Viewport>
|
|
155
|
+
<ScrollArea.Scrollbar orientation="vertical">
|
|
156
|
+
<ScrollArea.Thumb />
|
|
157
|
+
</ScrollArea.Scrollbar>
|
|
158
|
+
<ScrollArea.Corner />
|
|
159
|
+
</ScrollArea.Root>
|
|
160
|
+
</div>
|
|
161
|
+
{/if}
|
|
162
|
+
|
|
163
|
+
{#if showColumnPicker}
|
|
164
|
+
<div class="border-t border-surface-3 p-3">
|
|
165
|
+
<div class="relative mb-1">
|
|
166
|
+
<PhMagnifyingGlass
|
|
167
|
+
class="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-ink-dim"
|
|
168
|
+
/>
|
|
169
|
+
<input
|
|
170
|
+
type="text"
|
|
171
|
+
placeholder="Search columns..."
|
|
172
|
+
bind:value={columnSearchText}
|
|
173
|
+
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"
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="max-h-48 overflow-y-auto">
|
|
177
|
+
{#each availableColumns as column (column.id)}
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
onclick={() => addFilter(column)}
|
|
181
|
+
class="w-full rounded px-2 py-1.5 text-left text-sm text-ink hover:bg-surface-2"
|
|
182
|
+
>
|
|
183
|
+
{getColumnLabel(column)}
|
|
184
|
+
</button>
|
|
185
|
+
{:else}
|
|
186
|
+
<p class="px-2 py-3 text-center text-sm text-ink-dim">No more columns</p>
|
|
187
|
+
{/each}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
{:else}
|
|
191
|
+
<div class="border-t border-surface-3">
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onclick={() => (showColumnPicker = true)}
|
|
195
|
+
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"
|
|
196
|
+
>
|
|
197
|
+
<span class="text-base leading-none">+</span>
|
|
198
|
+
Add Filter
|
|
199
|
+
</button>
|
|
200
|
+
</div>
|
|
201
|
+
{/if}
|
|
202
|
+
</Popover.Content>
|
|
203
|
+
</Popover.Root>
|