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.
Files changed (29) hide show
  1. package/dist/components/combobox/combobox.svelte +99 -28
  2. package/dist/components/combobox/types.d.ts +1 -0
  3. package/dist/components/data-table-v8/index.d.ts +4 -4
  4. package/dist/components/data-table-v8/index.js +4 -4
  5. package/dist/components/data-table-v8/toolbar/data-table-column-filter.svelte +375 -0
  6. package/dist/components/data-table-v8/{data-table-column-filter.svelte.d.ts → toolbar/data-table-column-filter.svelte.d.ts} +1 -1
  7. package/dist/components/data-table-v8/{data-table-column-visibility.svelte → toolbar/data-table-column-visibility.svelte} +4 -4
  8. package/dist/components/data-table-v8/{data-table-column-visibility.svelte.d.ts → toolbar/data-table-column-visibility.svelte.d.ts} +1 -1
  9. package/dist/components/data-table-v8/{data-table-search.svelte → toolbar/data-table-search.svelte} +3 -3
  10. package/dist/components/data-table-v8/{data-table-search.svelte.d.ts → toolbar/data-table-search.svelte.d.ts} +1 -1
  11. package/dist/components/data-table-v8/{data-table-virtual-rows.svelte → virtual/data-table-virtual-rows.svelte} +4 -4
  12. package/dist/components/data-table-v8/{data-table-virtual-rows.svelte.d.ts → virtual/data-table-virtual-rows.svelte.d.ts} +1 -1
  13. package/dist/components/data-table-v8/{data-table-virtualized.svelte → virtual/data-table-virtualized.svelte} +3 -3
  14. package/dist/components/data-table-v8/{data-table-virtualized.svelte.d.ts → virtual/data-table-virtualized.svelte.d.ts} +1 -1
  15. package/dist/components/data-table-v8/virtual/index.d.ts +1 -1
  16. package/dist/components/data-table-v8/virtual/index.js +1 -1
  17. package/dist/components/drawer/drawer-root.svelte +10 -3
  18. package/dist/components/drawer/drawer-root.svelte.d.ts +1 -1
  19. package/dist/components/number-input/number-input.svelte +8 -3
  20. package/dist/components/scroll-area/scroll-area-content.svelte +1 -1
  21. package/dist/components/scroll-area/scroll-area-scrollbar.svelte +1 -1
  22. package/dist/components/tabs/tabs.svelte +1 -0
  23. package/dist/components/toggle/toggle.svelte +5 -1
  24. package/dist/components/toggle-group/toggle-group-item.svelte +1 -4
  25. package/dist/components/toggle-group/toggle-group.svelte +5 -6
  26. package/package.json +1 -1
  27. package/dist/components/data-table-v8/data-table-column-filter.svelte +0 -248
  28. /package/dist/components/data-table-v8/{data-table-toolbar.svelte → toolbar/data-table-toolbar.svelte} +0 -0
  29. /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
- // Base collection only rebuilds when items prop changes
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="z-200 max-h-60 min-w-(--reference-width) overflow-auto rounded-md border bg-surface-document p-1 shadow-md data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95"
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 loading}
146
- <div class="flex items-center justify-center py-4">
147
- <span
148
- class="size-5 animate-spin rounded-full border-2 border-surface-3 border-t-ink-dim"
149
- ></span>
150
- </div>
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
- <Combobox.Empty class="py-2 text-center text-sm text-ink-dim">
153
- No results found
154
- </Combobox.Empty>
155
- {/if}
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
- {#each loading ? [] : collection.items as item (item.value)}
158
- <Combobox.Item
159
- {item}
160
- 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"
161
- >
162
- <Combobox.ItemText>{item.label}</Combobox.ItemText>
163
- <Combobox.ItemIndicator class="absolute right-2 items-center justify-center">
164
- <PhCheck class="size-3.5" />
165
- </Combobox.ItemIndicator>
166
- </Combobox.Item>
167
- {/each}
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>
@@ -9,5 +9,6 @@ export interface ComboboxProps<T extends ListItem> extends Omit<ComboboxRootBase
9
9
  multiple?: boolean;
10
10
  layout?: 'vertical' | 'horizontal';
11
11
  loading?: boolean;
12
+ virtualized?: boolean;
12
13
  class?: string;
13
14
  }
@@ -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 ColumnVisibility } from './data-table-column-visibility.svelte';
9
- export { default as ColumnFilter } from './data-table-column-filter.svelte';
10
- export { default as Search } from './data-table-search.svelte';
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 ColumnVisibility } from './data-table-column-visibility.svelte';
9
- export { default as ColumnFilter } from './data-table-column-filter.svelte';
10
- export { default as Search } from './data-table-search.svelte';
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 './data-table-utils';
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 '../popover';
4
- import * as ScrollArea from '../scroll-area';
5
- import Checkbox from '../checkbox/checkbox.svelte';
6
- import { getReactiveTableState, type DataTableInstance } from './data-table-utils';
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 './data-table-utils';
2
+ import { type DataTableInstance } from '../data-table-utils';
3
3
  declare function $$render<T extends RowData>(): {
4
4
  props: {
5
5
  table: DataTableInstance<T>;
@@ -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 '../../icons';
6
- import * as Field from '../field';
7
- import { getReactiveTableState, type DataTableInstance } from './data-table-utils';
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 './data-table-utils';
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 '../../icons';
7
- import type { DataTableInstance } from './data-table-utils';
8
- import FlexRender from './flex-render.svelte';
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 './data-table-utils';
23
+ } from '../data-table-utils';
24
24
 
25
25
  type Props = {
26
26
  rows: Row<T>[];
@@ -1,5 +1,5 @@
1
1
  import type { Row, RowData } from '@tanstack/table-core';
2
- import type { DataTableInstance } from './data-table-utils';
2
+ import type { DataTableInstance } from '../data-table-utils';
3
3
  declare function $$render<T extends RowData>(): {
4
4
  props: {
5
5
  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 './data-table-utils';
6
- import DataTableHead from './data-table-head.svelte';
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 './data-table-utils';
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 './data-table-utils';
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 '../data-table-virtualized.svelte';
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 '../data-table-virtualized.svelte';
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' | 'onOpenChange'> {
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 bind:open {...rest}>
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' | 'onOpenChange'> {
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 max-w-48 data-disabled:opacity-50 data-disabled:grayscale'
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?.toString()}
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-full 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"
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('py-3 ps-4 pe-6', className)}>
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-2 flex rounded-md bg-well opacity-0 transition-opacity duration-150',
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',
@@ -18,6 +18,7 @@
18
18
  bind:value
19
19
  {defaultValue}
20
20
  {orientation}
21
+ lazyMount
21
22
  class="flex data-[orientation='horizontal']:flex-col data-[orientation='vertical']:flex-row"
22
23
  >
23
24
  {@render children?.()}
@@ -13,6 +13,10 @@
13
13
  }: ToggleProps = $props();
14
14
  </script>
15
15
 
16
- <ArkToggle.Root {...rootProps} bind:pressed class={toggle({ size, icon, class: className as never })}>
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>
@@ -22,9 +22,6 @@
22
22
  );
23
23
  </script>
24
24
 
25
- <ToggleGroup.Item
26
- {...rest}
27
- class={itemClass}
28
- >
25
+ <ToggleGroup.Item {...rest} class={itemClass}>
29
26
  {@render children?.()}
30
27
  </ToggleGroup.Item>
@@ -36,7 +36,10 @@
36
36
 
37
37
  const rootClass = $derived(
38
38
  variant === 'ghost'
39
- ? cn('inline-flex w-fit gap-1 data-[orientation=vertical]:flex-col data-[orientation=vertical]:gap-1', className)
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,6 +1,6 @@
1
1
  {
2
2
  "name": "compote-ui",
3
- "version": "0.46.5",
3
+ "version": "0.47.2",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "dev": "vite dev --open",
@@ -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>