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.
Files changed (58) 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 +10 -0
  4. package/dist/components/data-table-v9/column-filter-kind.js +25 -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 +230 -201
  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 +154 -151
  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 +33 -0
  17. package/dist/components/data-table-v9/filter-editors/column-filter-editor.svelte.d.ts +30 -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/table-view-state.svelte.d.ts +1 -0
  29. package/dist/components/data-table-v9/table-view-state.svelte.js +3 -0
  30. package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte +203 -459
  31. package/dist/components/data-table-v9/toolbar/data-table-column-filter.svelte.d.ts +1 -1
  32. package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte +82 -79
  33. package/dist/components/data-table-v9/virtual/data-table-virtualized.svelte.d.ts +2 -0
  34. package/dist/components/dialog/alert-dialog.svelte +73 -71
  35. package/dist/components/dialog/dialog-root.svelte +45 -43
  36. package/dist/components/drawer/drawer-content.svelte +213 -211
  37. package/dist/components/popover/popover-content.svelte +97 -71
  38. package/dist/components/popover/popover-content.svelte.d.ts +2 -1
  39. package/dist/components/popover/popover-description.svelte +19 -15
  40. package/dist/components/popover/popover-labels.svelte.d.ts +18 -0
  41. package/dist/components/popover/popover-labels.svelte.js +35 -0
  42. package/dist/components/popover/popover-root.svelte +27 -24
  43. package/dist/components/popover/popover-title.svelte +19 -15
  44. package/dist/icons/PhFunnelFill.svelte +19 -0
  45. package/dist/icons/PhFunnelFill.svelte.d.ts +5 -0
  46. package/dist/icons/index.d.ts +1 -0
  47. package/dist/icons/index.js +1 -0
  48. package/dist/utils/date-filter.d.ts +31 -0
  49. package/dist/utils/date-filter.js +58 -0
  50. package/dist/utils/date.d.ts +19 -1
  51. package/dist/utils/date.js +48 -1
  52. package/dist/utils/release-focus-on-exit.d.ts +22 -0
  53. package/dist/utils/release-focus-on-exit.js +26 -0
  54. package/package.json +1 -1
  55. package/skills/component-usage/references/overlays-and-floating-ui.md +278 -273
  56. package/skills/data-table/SKILL.md +314 -283
  57. package/skills/data-table/references/column-options.md +175 -150
  58. package/skills/data-table/references/table-state-and-reactivity.md +65 -60
@@ -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,10 @@
1
+ import type { 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;
@@ -0,0 +1,25 @@
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
+ }
@@ -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';