compote-ui 0.79.2 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/components/data-table-v9/column-filter-editing.svelte.d.ts +41 -0
  2. package/dist/components/data-table-v9/column-filter-editing.svelte.js +88 -0
  3. package/dist/components/data-table-v9/column-filter-kind.d.ts +19 -0
  4. package/dist/components/data-table-v9/column-filter-kind.js +32 -0
  5. package/dist/components/data-table-v9/create-table.svelte.js +37 -25
  6. package/dist/components/data-table-v9/data-table-head.svelte +32 -3
  7. package/dist/components/data-table-v9/data-table-head.svelte.d.ts +2 -0
  8. package/dist/components/data-table-v9/data-table-utils.d.ts +7 -0
  9. package/dist/components/data-table-v9/data-table-utils.js +4 -0
  10. package/dist/components/data-table-v9/data-table.svelte +4 -1
  11. package/dist/components/data-table-v9/data-table.svelte.d.ts +2 -0
  12. package/dist/components/data-table-v9/date-cell.d.ts +13 -0
  13. package/dist/components/data-table-v9/date-cell.js +38 -0
  14. package/dist/components/data-table-v9/filter-editors/boolean-filter-editor.svelte +45 -0
  15. package/dist/components/data-table-v9/filter-editors/boolean-filter-editor.svelte.d.ts +7 -0
  16. package/dist/components/data-table-v9/filter-editors/column-filter-editor.svelte +39 -0
  17. package/dist/components/data-table-v9/filter-editors/column-filter-editor.svelte.d.ts +35 -0
  18. package/dist/components/data-table-v9/filter-editors/date-range-filter-editor.svelte +52 -0
  19. package/dist/components/data-table-v9/filter-editors/date-range-filter-editor.svelte.d.ts +28 -0
  20. package/dist/components/data-table-v9/filter-editors/range-filter-editor.svelte +51 -0
  21. package/dist/components/data-table-v9/filter-editors/range-filter-editor.svelte.d.ts +30 -0
  22. package/dist/components/data-table-v9/filter-editors/select-filter-editor.svelte +59 -0
  23. package/dist/components/data-table-v9/filter-editors/select-filter-editor.svelte.d.ts +28 -0
  24. package/dist/components/data-table-v9/filter-editors/text-filter-editor.svelte +20 -0
  25. package/dist/components/data-table-v9/filter-editors/text-filter-editor.svelte.d.ts +8 -0
  26. package/dist/components/data-table-v9/header/column-header-filter.svelte +66 -0
  27. package/dist/components/data-table-v9/header/column-header-filter.svelte.d.ts +30 -0
  28. package/dist/components/data-table-v9/index.d.ts +9 -1
  29. package/dist/components/data-table-v9/index.js +7 -0
  30. package/dist/components/data-table-v9/table-view-state.svelte.d.ts +1 -0
  31. package/dist/components/data-table-v9/table-view-state.svelte.js +3 -0
  32. package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte +13 -269
  33. package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte.d.ts +1 -1
  34. package/dist/components/data-table-v9/types.d.ts +3 -1
  35. package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte +4 -1
  36. package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte.d.ts +2 -0
  37. package/dist/components/data-table-v9/virtual/index.d.ts +2 -2
  38. package/dist/components/data-table-v9/virtual/index.js +1 -1
  39. package/dist/components/dialog/alert-dialog.svelte +2 -0
  40. package/dist/components/dialog/dialog-root.svelte +2 -0
  41. package/dist/components/drawer/drawer-content.svelte +2 -0
  42. package/dist/components/popover/popover-content.svelte +29 -3
  43. package/dist/components/popover/popover-content.svelte.d.ts +2 -1
  44. package/dist/components/popover/popover-description.svelte +4 -0
  45. package/dist/components/popover/popover-labels.svelte.d.ts +18 -0
  46. package/dist/components/popover/popover-labels.svelte.js +35 -0
  47. package/dist/components/popover/popover-root.svelte +3 -0
  48. package/dist/components/popover/popover-title.svelte +4 -0
  49. package/dist/icons/PhFunnelFill.svelte +19 -0
  50. package/dist/icons/PhFunnelFill.svelte.d.ts +5 -0
  51. package/dist/icons/index.d.ts +1 -0
  52. package/dist/icons/index.js +1 -0
  53. package/dist/utils/date-filter.d.ts +31 -0
  54. package/dist/utils/date-filter.js +58 -0
  55. package/dist/utils/date.d.ts +19 -1
  56. package/dist/utils/date.js +48 -1
  57. package/dist/utils/release-focus-on-exit.d.ts +22 -0
  58. package/dist/utils/release-focus-on-exit.js +26 -0
  59. package/package.json +1 -1
  60. package/skills/component-usage/references/overlays-and-floating-ui.md +7 -2
  61. package/skills/data-table/SKILL.md +80 -1
  62. package/skills/data-table/references/column-options.md +25 -0
  63. package/skills/data-table/references/table-state-and-reactivity.md +13 -8
@@ -0,0 +1,41 @@
1
+ /** The slice of a TanStack column the filter editors read and write. */
2
+ export type FilterableColumn = {
3
+ id: string;
4
+ getFilterValue(): unknown;
5
+ setFilterValue(value: unknown): void;
6
+ };
7
+ export type ColumnFilterEditingOptions = {
8
+ /** The table the edited columns belong to; read on every access so it may change. */
9
+ table: () => object;
10
+ /**
11
+ * Whether `columnId` still has an editor. An editor torn down by an outside change can
12
+ * still emit — Ark's NumberInput commits the typed number on the blur its unmount
13
+ * triggers — and that must not write back a filter the app just cleared.
14
+ */
15
+ isActive?: (columnId: string) => boolean;
16
+ };
17
+ /** Drops every pending filter edit on `table` without committing, whichever host made it. */
18
+ export declare function cancelPendingFilterEdits(table: object): void;
19
+ /**
20
+ * One host's access to a table's pending filter edits (see PendingFilterEdits). The edits
21
+ * live with the table, not with the host or its editors, so an edit still commits after its
22
+ * editor unmounts with a closing popover.
23
+ *
24
+ * A control shows its pending edit if there is one, otherwise the column's live filter
25
+ * value — so a filter changed from outside (the app calling setFilterValue /
26
+ * setColumnFilters / resetColumnFilters) never leaves a stale value in the inputs, an edit
27
+ * pending during that change is dropped rather than re-applied, and committing one range
28
+ * bound never restores the other's old value.
29
+ */
30
+ export declare class ColumnFilterEditing {
31
+ #private;
32
+ constructor(options: ColumnFilterEditingOptions);
33
+ textValue(column: FilterableColumn): string;
34
+ editText(column: FilterableColumn, value: string): void;
35
+ rangeBound(column: FilterableColumn, which: 'min' | 'max'): number | null;
36
+ editRange(column: FilterableColumn, which: 'min' | 'max', value: number | null): void;
37
+ /** Drops the pending edits for one column without committing them. */
38
+ cancel(columnId: string): void;
39
+ /** Drops every pending edit on the table without committing — including other hosts'. */
40
+ cancelAll(): void;
41
+ }
@@ -0,0 +1,88 @@
1
+ import { DebouncedFilterEdits } from '../../utils/debounced-filter-edit.svelte';
2
+ import { mergeRangeFilter, rangeBounds } from '../../utils/range-filter';
3
+ import { getFilterRevisions } from './filter-revisions.svelte';
4
+ /**
5
+ * The debounced edits pending on one table's filters. Shared by every component that edits
6
+ * them (the toolbar filter, each header filter), so the latest edit to a column wins no
7
+ * matter where it was made, and cancelling reaches edits made from any of them.
8
+ */
9
+ class PendingFilterEdits {
10
+ text = new DebouncedFilterEdits();
11
+ range = new DebouncedFilterEdits();
12
+ }
13
+ const registry = new WeakMap();
14
+ function pendingEdits(table) {
15
+ let edits = registry.get(table);
16
+ if (!edits) {
17
+ edits = new PendingFilterEdits();
18
+ registry.set(table, edits);
19
+ }
20
+ return edits;
21
+ }
22
+ /** Drops every pending filter edit on `table` without committing, whichever host made it. */
23
+ export function cancelPendingFilterEdits(table) {
24
+ const edits = registry.get(table);
25
+ edits?.text.cancelAll();
26
+ edits?.range.cancelAll();
27
+ }
28
+ /**
29
+ * One host's access to a table's pending filter edits (see PendingFilterEdits). The edits
30
+ * live with the table, not with the host or its editors, so an edit still commits after its
31
+ * editor unmounts with a closing popover.
32
+ *
33
+ * A control shows its pending edit if there is one, otherwise the column's live filter
34
+ * value — so a filter changed from outside (the app calling setFilterValue /
35
+ * setColumnFilters / resetColumnFilters) never leaves a stale value in the inputs, an edit
36
+ * pending during that change is dropped rather than re-applied, and committing one range
37
+ * bound never restores the other's old value.
38
+ */
39
+ export class ColumnFilterEditing {
40
+ #table;
41
+ #isActive;
42
+ constructor(options) {
43
+ this.#table = options.table;
44
+ this.#isActive = options.isActive ?? (() => true);
45
+ }
46
+ get #edits() {
47
+ return pendingEdits(this.#table());
48
+ }
49
+ /**
50
+ * What an edit is based on: the column's filter revision, which only ever goes up, so a
51
+ * filter cleared and set back to its old value still makes the edit stale. Tables not
52
+ * made by compote's createTable have no revisions; fall back to the filter value.
53
+ */
54
+ #base(column) {
55
+ const revisions = getFilterRevisions(this.#table());
56
+ return revisions ? revisions.get(column.id) : column.getFilterValue();
57
+ }
58
+ textValue(column) {
59
+ const value = column.getFilterValue();
60
+ return (this.#edits.text.get(column.id, this.#base(column)) ??
61
+ (typeof value === 'string' ? value : ''));
62
+ }
63
+ editText(column, value) {
64
+ if (!this.#isActive(column.id))
65
+ return;
66
+ this.#edits.text.edit(column.id, this.#base(column), () => value, () => this.#base(column), (text) => column.setFilterValue(text || undefined));
67
+ }
68
+ rangeBound(column, which) {
69
+ const pending = this.#edits.range.get(column.id, this.#base(column));
70
+ if (pending && which in pending)
71
+ return pending[which] ?? null;
72
+ return rangeBounds(column.getFilterValue())[which === 'min' ? 0 : 1] ?? null;
73
+ }
74
+ editRange(column, which, value) {
75
+ if (!this.#isActive(column.id))
76
+ return;
77
+ this.#edits.range.edit(column.id, this.#base(column), (pending) => ({ ...pending, [which]: value }), () => this.#base(column), (pending) => column.setFilterValue(mergeRangeFilter(column.getFilterValue(), pending)));
78
+ }
79
+ /** Drops the pending edits for one column without committing them. */
80
+ cancel(columnId) {
81
+ this.#edits.text.cancel(columnId);
82
+ this.#edits.range.cancel(columnId);
83
+ }
84
+ /** Drops every pending edit on the table without committing — including other hosts'. */
85
+ cancelAll() {
86
+ cancelPendingFilterEdits(this.#table());
87
+ }
88
+ }
@@ -0,0 +1,19 @@
1
+ import type { DataTableColumnMeta, DataTableColumnType } from './types';
2
+ /** Which filter editor (and default filterFn) a column type uses. */
3
+ export type ColumnFilterKind = 'text' | 'range' | 'boolean' | 'select' | 'date';
4
+ /**
5
+ * The filter kind for a column type. The single mapping both the filter editors and the
6
+ * default filterFn follow, so a column never shows an editor its filterFn can't evaluate.
7
+ * `action` columns carry no value and get no filter. `time` stays a text filter until it has
8
+ * its own contract (a time-of-day range has to decide how to cross midnight).
9
+ */
10
+ export declare function filterKindFor(type: DataTableColumnType | undefined): ColumnFilterKind | null;
11
+ /**
12
+ * The filter kind of a table column (from its `type`): the editor `ColumnFilterEditor` shows
13
+ * for it and the filter value shape its default filterFn expects.
14
+ */
15
+ export declare function getColumnFilterKind(column: {
16
+ columnDef: {
17
+ meta?: DataTableColumnMeta;
18
+ };
19
+ }): ColumnFilterKind | null;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The filter kind for a column type. The single mapping both the filter editors and the
3
+ * default filterFn follow, so a column never shows an editor its filterFn can't evaluate.
4
+ * `action` columns carry no value and get no filter. `time` stays a text filter until it has
5
+ * its own contract (a time-of-day range has to decide how to cross midnight).
6
+ */
7
+ export function filterKindFor(type) {
8
+ switch (type) {
9
+ case 'number':
10
+ case 'currency':
11
+ case 'percent':
12
+ return 'range';
13
+ case 'boolean':
14
+ return 'boolean';
15
+ case 'select':
16
+ return 'select';
17
+ case 'date':
18
+ case 'date-time':
19
+ return 'date';
20
+ case 'action':
21
+ return null;
22
+ default:
23
+ return 'text';
24
+ }
25
+ }
26
+ /**
27
+ * The filter kind of a table column (from its `type`): the editor `ColumnFilterEditor` shows
28
+ * for it and the filter value shape its default filterFn expects.
29
+ */
30
+ export function getColumnFilterKind(column) {
31
+ return filterKindFor(column.columnDef.meta?.type);
32
+ }
@@ -4,6 +4,10 @@ import { onDestroy } from 'svelte';
4
4
  import { renderComponent, renderSnippet } from '@tanstack/svelte-table';
5
5
  import { dataTableFeatures } from './features';
6
6
  import { TYPE_NUMBER_FORMAT_DEFAULTS } from './data-table-utils';
7
+ import { filterKindFor } from './column-filter-kind';
8
+ import { dateCellTimeZone, formatDateCell } from './date-cell';
9
+ import { isEmptyDateRange, matchesDateRange, resolveDateRange } from '../../utils/date-filter';
10
+ import { cancelPendingFilterEdits } from './column-filter-editing.svelte';
7
11
  import { changedFilterIds, FilterRevisions, registerFilterRevisions } from './filter-revisions.svelte';
8
12
  import { formatSelectValue, matchesSelectFilter, selectCellValues } from '../../utils/select-filter';
9
13
  const oneOfFilterFn = (row, columnId, filterValue) => {
@@ -90,6 +94,22 @@ export function createTable(options) {
90
94
  onDestroy(() => subscription.unsubscribe());
91
95
  }
92
96
  registerFilterRevisions(table, filterRevisions);
97
+ // Debounced filter edits outlive the editors that made them, but not the table.
98
+ onDestroy(() => cancelPendingFilterEdits(table));
99
+ // A reset drops pending filter edits too — including one on a column with no committed
100
+ // filter, whose revision the reset leaves alone, so the edit would otherwise commit
101
+ // afterwards and bring a filter back. TanStack assigns these as own methods of the table
102
+ // (`table.reset` also writes state directly, bypassing onColumnFiltersChange).
103
+ const resetColumnFilters = table.resetColumnFilters;
104
+ table.resetColumnFilters = (defaultState) => {
105
+ cancelPendingFilterEdits(table);
106
+ resetColumnFilters(defaultState);
107
+ };
108
+ const resetTable = table.reset;
109
+ table.reset = () => {
110
+ cancelPendingFilterEdits(table);
111
+ resetTable();
112
+ };
93
113
  return table;
94
114
  }
95
115
  function createColumnVisibility(columns) {
@@ -128,7 +148,7 @@ function createColumns(columns, localeCtx) {
128
148
  };
129
149
  }
130
150
  const columnId = getColumnId(column);
131
- const derivedFilterFn = column.filterFn ?? getFilterFnForType(column.type);
151
+ const derivedFilterFn = column.filterFn ?? getFilterFnForType(column);
132
152
  const columnDef = {
133
153
  id: columnId,
134
154
  header: column.header,
@@ -191,27 +211,26 @@ export function getColumnId(column) {
191
211
  return column.accessorKey;
192
212
  throw new Error('DataTableColumn with accessorFn requires an id.');
193
213
  }
194
- const TYPE_DATE_FORMAT_DEFAULTS = {
195
- date: { day: '2-digit', month: '2-digit', year: 'numeric' },
196
- time: { hour: '2-digit', minute: '2-digit' },
197
- 'date-time': {
198
- day: '2-digit',
199
- month: '2-digit',
200
- year: 'numeric',
201
- hour: '2-digit',
202
- minute: '2-digit'
203
- }
204
- };
205
- function getFilterFnForType(type) {
206
- switch (type) {
207
- case 'number':
208
- case 'currency':
209
- case 'percent':
214
+ /**
215
+ * A calendar-day range filter for a date column, judged in the zone the column is shown in
216
+ * (read on every pass, so it follows a changed local zone like the cells do).
217
+ */
218
+ function dateRangeFilterFn(formatOptions) {
219
+ const filterFn = (row, columnId, range) => matchesDateRange(row.getValue(columnId), range, dateCellTimeZone(formatOptions));
220
+ filterFn.resolveFilterValue = resolveDateRange;
221
+ filterFn.autoRemove = isEmptyDateRange;
222
+ return filterFn;
223
+ }
224
+ function getFilterFnForType(column) {
225
+ switch (filterKindFor(column.type)) {
226
+ case 'range':
210
227
  return 'inNumberRange';
211
228
  case 'boolean':
212
229
  return 'equals';
213
230
  case 'select':
214
231
  return oneOfFilterFn;
232
+ case 'date':
233
+ return dateRangeFilterFn(column.formatOptions);
215
234
  default:
216
235
  return undefined;
217
236
  }
@@ -231,14 +250,7 @@ function applyTypeFormat(column, value, localeCtx) {
231
250
  }).format(numericValue);
232
251
  }
233
252
  if (column.type === 'date' || column.type === 'time' || column.type === 'date-time') {
234
- const dateValue = value instanceof Date ? value : new Date(value);
235
- if (isNaN(dateValue.getTime()))
236
- return undefined;
237
- const locale = column.formatLocale ?? localeCtx().locale;
238
- return new Intl.DateTimeFormat(locale, {
239
- ...TYPE_DATE_FORMAT_DEFAULTS[column.type],
240
- ...column.formatOptions
241
- }).format(dateValue);
253
+ return formatDateCell(value, column.type, column.formatLocale ?? localeCtx().locale, column.formatOptions);
242
254
  }
243
255
  if (column.type === 'boolean')
244
256
  return value ? 'Yes' : 'No';
@@ -6,6 +6,8 @@
6
6
  import type { DataTableInstance } from './data-table-utils';
7
7
  import type { DataTableViewState } from './table-view-state.svelte';
8
8
  import type { DataTableFeatures } from './features';
9
+ import type { DataTableAlign } from './types';
10
+ import ColumnHeaderFilter from './header/column-header-filter.svelte';
9
11
  import {
10
12
  alignClass,
11
13
  getColumnMeta,
@@ -29,9 +31,17 @@
29
31
  view: DataTableViewState<T>;
30
32
  isVirtual?: boolean;
31
33
  hasGrowColumn?: boolean;
34
+ /** Show a filter funnel in the header of every filterable column. */
35
+ headerFilters?: boolean;
32
36
  };
33
37
 
34
- let { table, view, isVirtual = false, hasGrowColumn = false }: Props = $props();
38
+ let {
39
+ table,
40
+ view,
41
+ isVirtual = false,
42
+ hasGrowColumn = false,
43
+ headerFilters = false
44
+ }: Props = $props();
35
45
 
36
46
  type Header = HeaderGroup<DataTableFeatures, T>['headers'][number];
37
47
 
@@ -46,6 +56,21 @@
46
56
  return undefined;
47
57
  }
48
58
 
59
+ function showHeaderFilter(header: Header) {
60
+ return headerFilters && header.subHeaders.length === 0 && header.column.getCanFilter();
61
+ }
62
+
63
+ /**
64
+ * Padding that keeps an active (always visible) filter funnel off the label, so it never
65
+ * hides the sort indicator. The funnel sits on the inner edge opposite the label.
66
+ */
67
+ function headerFilterPaddingClass(header: Header, align: DataTableAlign | undefined) {
68
+ if (!showHeaderFilter(header)) return undefined;
69
+ const filtered = view.columnFilters.some((filter) => filter.id === header.column.id);
70
+ if (!filtered) return undefined;
71
+ return align === 'right' ? 'pl-6' : 'pr-6';
72
+ }
73
+
49
74
  function headerSortDirection(header: Header) {
50
75
  void view.sorting;
51
76
  return header.column.getIsSorted();
@@ -141,10 +166,11 @@
141
166
  <th
142
167
  scope="col"
143
168
  class={cn(
144
- 'relative h-9 border-b border-surface-3 bg-surface-2 px-3 py-0 align-middle leading-5 font-medium',
169
+ 'group/th relative h-9 border-b border-surface-3 bg-surface-2 px-3 py-0 align-middle leading-5 font-medium',
145
170
  isVirtual && 'items-center',
146
171
  isVirtual && justifyClass(meta?.align),
147
- alignClass(meta?.align)
172
+ alignClass(meta?.align),
173
+ headerFilterPaddingClass(header, meta?.align)
148
174
  )}
149
175
  colspan={header.colSpan}
150
176
  aria-sort={header.column.getCanSort() ? getHeaderAriaSort(sortDirection) : undefined}
@@ -179,6 +205,9 @@
179
205
  <FlexRender {header} />
180
206
  {/if}
181
207
  {/if}
208
+ {#if !header.isPlaceholder && showHeaderFilter(header)}
209
+ <ColumnHeaderFilter {table} column={header.column} />
210
+ {/if}
182
211
  <!-- A group split by pinning renders as adjacent fragments; skip the
183
212
  divider between them so the group reads as one continuous header. -->
184
213
  {#if header.column.getCanResize() && !continuesInNextFragment}
@@ -7,6 +7,8 @@ declare function $$render<T extends RowData>(): {
7
7
  view: DataTableViewState<T>;
8
8
  isVirtual?: boolean;
9
9
  hasGrowColumn?: boolean;
10
+ /** Show a filter funnel in the header of every filterable column. */
11
+ headerFilters?: boolean;
10
12
  };
11
13
  exports: {};
12
14
  bindings: "";
@@ -81,6 +81,13 @@ export declare function getUrlCellValue(value: unknown): string | undefined;
81
81
  export declare function openUrlCell(value: string): void;
82
82
  export declare function getPhoneCellValue(value: unknown): string | undefined;
83
83
  export declare function openPhoneCell(value: string): void;
84
+ /** A column's display name: its string header, or its id when the header is rendered. */
85
+ export declare function getColumnLabel(column: {
86
+ id: string;
87
+ columnDef: {
88
+ header?: unknown;
89
+ };
90
+ }): string;
84
91
  export declare function getColumnMeta(columnDef: {
85
92
  meta?: DataTableColumnMeta;
86
93
  }): DataTableColumnMeta | undefined;
@@ -154,6 +154,10 @@ export function getPhoneCellValue(value) {
154
154
  export function openPhoneCell(value) {
155
155
  window.location.href = `tel:${value}`;
156
156
  }
157
+ /** A column's display name: its string header, or its id when the header is rendered. */
158
+ export function getColumnLabel(column) {
159
+ return typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id;
160
+ }
157
161
  // `columnDef.meta` is natively typed as DataTableColumnMeta via the `columnMeta`
158
162
  // type-only slot in features.ts; this accessor just narrows the columnDef shape.
159
163
  export function getColumnMeta(columnDef) {
@@ -22,6 +22,8 @@
22
22
  caption?: string;
23
23
  emptyMessage?: string;
24
24
  class?: ClassValue;
25
+ /** Show a filter funnel in the header of every filterable column. */
26
+ headerFilters?: boolean;
25
27
  onRowClick?: (details: { row: T; event: MouseEvent }) => void;
26
28
  onRowDoubleClick?: (details: { row: T; event: MouseEvent }) => void;
27
29
  };
@@ -31,6 +33,7 @@
31
33
  caption,
32
34
  emptyMessage = 'No rows found',
33
35
  class: className,
36
+ headerFilters = false,
34
37
  onRowClick,
35
38
  onRowDoubleClick,
36
39
  ...rest
@@ -77,7 +80,7 @@
77
80
  {#if caption}
78
81
  <caption class="sr-only">{caption}</caption>
79
82
  {/if}
80
- <DataTableHead {table} {view} hasGrowColumn={view.hasGrowColumn} />
83
+ <DataTableHead {table} {view} hasGrowColumn={view.hasGrowColumn} {headerFilters} />
81
84
  <tbody>
82
85
  {#each view.rowModel.rows as row (row.id)}
83
86
  {@const rowSelected = view.rowSelection[row.id] === true}
@@ -8,6 +8,8 @@ declare function $$render<T extends RowData>(): {
8
8
  caption?: string;
9
9
  emptyMessage?: string;
10
10
  class?: ClassValue;
11
+ /** Show a filter funnel in the header of every filterable column. */
12
+ headerFilters?: boolean;
11
13
  onRowClick?: (details: {
12
14
  row: T;
13
15
  event: MouseEvent;
@@ -0,0 +1,13 @@
1
+ export type DateCellType = 'date' | 'time' | 'date-time';
2
+ export declare const TYPE_DATE_FORMAT_DEFAULTS: Record<DateCellType, Intl.DateTimeFormatOptions>;
3
+ /**
4
+ * The zone a date column is shown — and therefore filtered — in: `formatOptions.timeZone`
5
+ * when the column sets one, otherwise the viewer's local zone.
6
+ */
7
+ export declare function dateCellTimeZone(formatOptions: Intl.DateTimeFormatOptions | undefined): string;
8
+ /**
9
+ * A date cell value formatted for display, or undefined when it can't be read. Reads the
10
+ * value with `readDateInZone`, exactly as the date range filter does, so a cell always shows
11
+ * the day it is filtered by.
12
+ */
13
+ export declare function formatDateCell(value: unknown, type: DateCellType, locale: string, formatOptions?: Intl.DateTimeFormatOptions): string | undefined;
@@ -0,0 +1,38 @@
1
+ import { CalendarDate, getLocalTimeZone } from '@internationalized/date';
2
+ import { readDateInZone } from '../../utils/date';
3
+ export const TYPE_DATE_FORMAT_DEFAULTS = {
4
+ date: { day: '2-digit', month: '2-digit', year: 'numeric' },
5
+ time: { hour: '2-digit', minute: '2-digit' },
6
+ 'date-time': {
7
+ day: '2-digit',
8
+ month: '2-digit',
9
+ year: 'numeric',
10
+ hour: '2-digit',
11
+ minute: '2-digit'
12
+ }
13
+ };
14
+ /**
15
+ * The zone a date column is shown — and therefore filtered — in: `formatOptions.timeZone`
16
+ * when the column sets one, otherwise the viewer's local zone.
17
+ */
18
+ export function dateCellTimeZone(formatOptions) {
19
+ return formatOptions?.timeZone ?? getLocalTimeZone();
20
+ }
21
+ /**
22
+ * A date cell value formatted for display, or undefined when it can't be read. Reads the
23
+ * value with `readDateInZone`, exactly as the date range filter does, so a cell always shows
24
+ * the day it is filtered by.
25
+ */
26
+ export function formatDateCell(value, type, locale, formatOptions) {
27
+ const timeZone = dateCellTimeZone(formatOptions);
28
+ const date = readDateInZone(value, timeZone);
29
+ if (!date)
30
+ return undefined;
31
+ const options = { ...TYPE_DATE_FORMAT_DEFAULTS[type], ...formatOptions };
32
+ // A calendar date has no instant: format its own year-month-day as UTC midnight in UTC,
33
+ // so no display zone can move it to another day.
34
+ if (date instanceof CalendarDate) {
35
+ return new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' }).format(date.toDate('UTC'));
36
+ }
37
+ return new Intl.DateTimeFormat(locale, { ...options, timeZone }).format(date.toDate());
38
+ }
@@ -0,0 +1,45 @@
1
+ <script lang="ts">
2
+ import { cn } from 'tailwind-variants';
3
+ import type { FilterableColumn } from '../column-filter-editing.svelte';
4
+
5
+ type Props = {
6
+ column: FilterableColumn;
7
+ };
8
+
9
+ let { column }: Props = $props();
10
+
11
+ const value = $derived(column.getFilterValue() as boolean | undefined);
12
+ </script>
13
+
14
+ <div class="flex overflow-hidden rounded border border-border text-xs">
15
+ <button
16
+ type="button"
17
+ onclick={() => column.setFilterValue(undefined)}
18
+ class={cn(
19
+ 'flex-1 px-2 py-1',
20
+ value === undefined ? 'bg-surface-3 font-medium text-ink' : 'text-ink-dim hover:bg-surface-2'
21
+ )}
22
+ >
23
+ All
24
+ </button>
25
+ <button
26
+ type="button"
27
+ onclick={() => column.setFilterValue(value === true ? undefined : true)}
28
+ class={cn(
29
+ 'flex-1 border-x border-border px-2 py-1',
30
+ value === true ? 'bg-surface-3 font-medium text-ink' : 'text-ink-dim hover:bg-surface-2'
31
+ )}
32
+ >
33
+ Yes
34
+ </button>
35
+ <button
36
+ type="button"
37
+ onclick={() => column.setFilterValue(value === false ? undefined : false)}
38
+ class={cn(
39
+ 'flex-1 px-2 py-1',
40
+ value === false ? 'bg-surface-3 font-medium text-ink' : 'text-ink-dim hover:bg-surface-2'
41
+ )}
42
+ >
43
+ No
44
+ </button>
45
+ </div>
@@ -0,0 +1,7 @@
1
+ import type { FilterableColumn } from '../column-filter-editing.svelte';
2
+ type Props = {
3
+ column: FilterableColumn;
4
+ };
5
+ declare const BooleanFilterEditor: import("svelte").Component<Props, {}, "">;
6
+ type BooleanFilterEditor = ReturnType<typeof BooleanFilterEditor>;
7
+ export default BooleanFilterEditor;
@@ -0,0 +1,39 @@
1
+ <script lang="ts" generics="T extends RowData">
2
+ import type { Column, RowData } from '@tanstack/svelte-table';
3
+ import type { DataTableFeatures } from '../features';
4
+ import { getColumnFilterKind } from '../column-filter-kind';
5
+ import { ColumnFilterEditing } from '../column-filter-editing.svelte';
6
+ import TextFilterEditor from './text-filter-editor.svelte';
7
+ import RangeFilterEditor from './range-filter-editor.svelte';
8
+ import BooleanFilterEditor from './boolean-filter-editor.svelte';
9
+ import SelectFilterEditor from './select-filter-editor.svelte';
10
+ import DateRangeFilterEditor from './date-range-filter-editor.svelte';
11
+
12
+ type Props = {
13
+ column: Column<DataTableFeatures, T, unknown>;
14
+ /**
15
+ * Pending (debounced) text and range edits. Hosts that can tear an editor down while
16
+ * it still holds an edit pass their own, with `isActive`; otherwise the editor uses
17
+ * one bound to the column's table.
18
+ */
19
+ editing?: ColumnFilterEditing;
20
+ };
21
+
22
+ let { column, editing: editingProp }: Props = $props();
23
+
24
+ const ownEditing = new ColumnFilterEditing({ table: () => column.table });
25
+ const editing = $derived(editingProp ?? ownEditing);
26
+ const kind = $derived(getColumnFilterKind(column));
27
+ </script>
28
+
29
+ {#if kind === 'range'}
30
+ <RangeFilterEditor {column} {editing} />
31
+ {:else if kind === 'boolean'}
32
+ <BooleanFilterEditor {column} />
33
+ {:else if kind === 'select'}
34
+ <SelectFilterEditor {column} />
35
+ {:else if kind === 'date'}
36
+ <DateRangeFilterEditor {column} />
37
+ {:else if kind === 'text'}
38
+ <TextFilterEditor {column} {editing} />
39
+ {/if}
@@ -0,0 +1,35 @@
1
+ import type { Column, RowData } from '@tanstack/svelte-table';
2
+ import type { DataTableFeatures } from '../features';
3
+ import { ColumnFilterEditing } from '../column-filter-editing.svelte';
4
+ declare function $$render<T extends RowData>(): {
5
+ props: {
6
+ column: Column<DataTableFeatures, T, unknown>;
7
+ /**
8
+ * Pending (debounced) text and range edits. Hosts that can tear an editor down while
9
+ * it still holds an edit pass their own, with `isActive`; otherwise the editor uses
10
+ * one bound to the column's table.
11
+ */
12
+ editing?: ColumnFilterEditing;
13
+ };
14
+ exports: {};
15
+ bindings: "";
16
+ slots: {};
17
+ events: {};
18
+ };
19
+ declare class __sveltets_Render<T extends RowData> {
20
+ props(): ReturnType<typeof $$render<T>>['props'];
21
+ events(): ReturnType<typeof $$render<T>>['events'];
22
+ slots(): ReturnType<typeof $$render<T>>['slots'];
23
+ bindings(): "";
24
+ exports(): {};
25
+ }
26
+ interface $$IsomorphicComponent {
27
+ new <T extends RowData>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
28
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
29
+ } & ReturnType<__sveltets_Render<T>['exports']>;
30
+ <T extends RowData>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
31
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
32
+ }
33
+ declare const ColumnFilterEditor: $$IsomorphicComponent;
34
+ type ColumnFilterEditor<T extends RowData> = InstanceType<typeof ColumnFilterEditor<T>>;
35
+ export default ColumnFilterEditor;