sheer-ui 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sheer-ui",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Svelte 5 UI component library with Tailwind CSS",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -74,7 +74,7 @@
74
74
  selectedValues.add(option.value);
75
75
  }
76
76
  const filterValues = Array.from(selectedValues);
77
- column?.setFilterValue(filterValues.length ? filterValues : undefined);
77
+ column.filterValue = filterValues.length ? filterValues : undefined;
78
78
  }}>
79
79
  <div
80
80
  class={[
@@ -100,7 +100,7 @@
100
100
  {#if selectedValues.size > 0}
101
101
  <Command.Separator />
102
102
  <Command.Group>
103
- <Command.Item onSelect={() => column?.setFilterValue(undefined)} class="justify-center text-center">Clear filters</Command.Item>
103
+ <Command.Item onSelect={() => (column.filterValue = undefined)} class="justify-center text-center">Clear filters</Command.Item>
104
104
  </Command.Group>
105
105
  {/if}
106
106
  </Command.List>
@@ -25,16 +25,16 @@
25
25
  } = $props();
26
26
 
27
27
  const isFiltered = $derived(table.columnFilters.length > 0);
28
+ const search = $derived(searchColumn === undefined ? undefined : table.column(searchColumn));
28
29
  </script>
29
30
 
30
31
  <div class="flex items-center justify-between">
31
- {#if searchColumn}
32
+ {#if search}
32
33
  <Input
33
34
  {disabled}
34
35
  placeholder={searchPlaceholder ?? 'Filter...'}
35
- value={(table.column(searchColumn)?.filterValue as string) ?? ''}
36
- oninput={(e) => table.column(searchColumn)?.setFilterValue(e.currentTarget.value)}
37
- onchange={(e) => table.column(searchColumn)?.setFilterValue(e.currentTarget.value)}
36
+ value={(search.filterValue as string) ?? ''}
37
+ oninput={(e) => (search.filterValue = e.currentTarget.value)}
38
38
  class="h-8 w-37.5 lg:w-62.5" />
39
39
  {/if}
40
40
  <div class="flex items-center space-x-2">
@@ -46,7 +46,7 @@
46
46
  {/each}
47
47
 
48
48
  {#if isFiltered}
49
- <Button variant="ghost" {disabled} onclick={() => table.resetColumnFilters()} class="h-8! px-2! lg:px-3!">
49
+ <Button variant="ghost" {disabled} onclick={() => (table.columnFilters = [])} class="h-8! px-2! lg:px-3!">
50
50
  Reset
51
51
  <XIcon />
52
52
  </Button>
@@ -20,13 +20,16 @@ export class DataTable<TData> {
20
20
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
21
  readonly #defs: () => ColumnDef<TData, any>[];
22
22
  readonly #paginated: boolean;
23
- readonly #selection = new SvelteSet<string>();
23
+ readonly selectedRowIds = new SvelteSet<string>();
24
24
 
25
- sorting = $state<ColumnSort[]>([]);
26
- columnFilters = $state<ColumnFilter[]>([]);
25
+ // Raw so a change is always a reassignment: the page record below keys on their identity.
26
+ sorting = $state.raw<ColumnSort[]>([]);
27
+ columnFilters = $state.raw<ColumnFilter[]>([]);
27
28
  columnVisibility = $state<Record<string, boolean>>({});
28
- #pageIndex = $state(0);
29
29
  #pageSize = $state(10);
30
+ // A page index holds for the sorting and filters it was set under: a sort or filter change reads
31
+ // as page 0 without a write, while a data refetch underneath keeps the page.
32
+ #page = $state.raw({ index: 0, sorting: this.sorting, columnFilters: this.columnFilters });
30
33
 
31
34
  constructor(options: DataTableOptions<TData>) {
32
35
  this.#data = options.data;
@@ -40,7 +43,7 @@ export class DataTable<TData> {
40
43
  readonly #columns: Column<TData, any>[] = $derived.by(() => this.#defs().map((def) => new Column(this, def)));
41
44
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
45
  readonly #columnsById = $derived(new Map<string, Column<TData, any>>(this.#columns.map((column) => [column.id, column])));
43
- readonly #coreRows: readonly Row<TData>[] = $derived.by(() => this.#data().map((original, index) => new Row(this, this.#selection, original, index)));
46
+ readonly #coreRows: readonly Row<TData>[] = $derived.by(() => this.#data().map((original, index) => new Row(this, this.selectedRowIds, original, index)));
44
47
 
45
48
  readonly #activeFilters = $derived(
46
49
  this.columnFilters.flatMap((filter) => {
@@ -65,7 +68,9 @@ export class DataTable<TData> {
65
68
 
66
69
  readonly pageCount = $derived.by(() => (this.#paginated ? Math.max(1, Math.ceil(this.#sorted.length / this.#pageSize)) : 1));
67
70
  // Clamped on read, never written back: data shrinking under a stale index lands on the last page.
68
- readonly #safeIndex = $derived(Math.min(this.#pageIndex, this.pageCount - 1));
71
+ readonly #safeIndex = $derived(
72
+ this.#page.sorting === this.sorting && this.#page.columnFilters === this.columnFilters ? Math.min(this.#page.index, this.pageCount - 1) : 0,
73
+ );
69
74
 
70
75
  readonly rows: readonly Row<TData>[] = $derived.by(() =>
71
76
  this.#paginated ? this.#sorted.slice(this.#safeIndex * this.#pageSize, (this.#safeIndex + 1) * this.#pageSize) : this.#sorted,
@@ -79,7 +84,7 @@ export class DataTable<TData> {
79
84
  }
80
85
 
81
86
  setPageIndex(index: number) {
82
- this.#pageIndex = Math.min(Math.max(index, 0), this.pageCount - 1);
87
+ this.#page = { index: Math.min(Math.max(index, 0), this.pageCount - 1), sorting: this.sorting, columnFilters: this.columnFilters };
83
88
  }
84
89
  nextPage() {
85
90
  this.setPageIndex(this.#safeIndex + 1);
@@ -90,23 +95,10 @@ export class DataTable<TData> {
90
95
  setPageSize(size: number) {
91
96
  const next = Math.max(1, size);
92
97
  // Keep the current top row visible, matching upstream's paging math.
93
- this.#pageIndex = Math.floor((this.#safeIndex * this.#pageSize) / next);
98
+ this.#page = { index: Math.floor((this.#safeIndex * this.#pageSize) / next), sorting: this.sorting, columnFilters: this.columnFilters };
94
99
  this.#pageSize = next;
95
100
  }
96
101
 
97
- // User-initiated sort and filter changes restart paging; a data refetch underneath does not.
98
- setSorting(sorting: ColumnSort[]) {
99
- this.sorting = sorting;
100
- this.#pageIndex = 0;
101
- }
102
- setColumnFilters(filters: ColumnFilter[]) {
103
- this.columnFilters = filters;
104
- this.#pageIndex = 0;
105
- }
106
- resetColumnFilters() {
107
- this.setColumnFilters([]);
108
- }
109
-
110
102
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
103
  get allColumns(): readonly Column<TData, any>[] {
112
104
  return this.#columns;
@@ -122,15 +114,6 @@ export class DataTable<TData> {
122
114
  { id: '0', headers: this.visibleColumns.map((column) => new Header(this, column)) },
123
115
  ]);
124
116
 
125
- get selectedRowIds(): ReadonlySet<string> {
126
- return this.#selection;
127
- }
128
- setSelectedRowIds(ids: Iterable<string>) {
129
- const next = new Set(ids);
130
- for (const id of this.#selection) if (!next.has(id)) this.#selection.delete(id);
131
- for (const id of next) this.#selection.add(id);
132
- }
133
-
134
117
  readonly selectedRows: readonly Row<TData>[] = $derived(this.#coreRows.filter((row) => row.isSelected));
135
118
  readonly filteredSelectedRows: readonly Row<TData>[] = $derived(this.filteredRows.filter((row) => row.isSelected));
136
119
 
@@ -194,26 +177,23 @@ export class Column<TData, TValue = unknown> {
194
177
  const existingIndex = old.findIndex((sort) => sort.id === this.id);
195
178
 
196
179
  if (old.length > 0 && this.#canMultiSort && multi) {
197
- this.#table.setSorting(
198
- existing
199
- ? !hasManualValue && nextOrder === false
200
- ? old.filter((sort) => sort.id !== this.id)
201
- : old.map((sort) => (sort.id === this.id ? { ...sort, desc: nextDesc } : sort))
202
- : [...old, { id: this.id, desc: nextDesc }],
203
- );
180
+ this.#table.sorting = existing
181
+ ? !hasManualValue && nextOrder === false
182
+ ? old.filter((sort) => sort.id !== this.id)
183
+ : old.map((sort) => (sort.id === this.id ? { ...sort, desc: nextDesc } : sort))
184
+ : [...old, { id: this.id, desc: nextDesc }];
204
185
  return;
205
186
  }
206
187
 
207
188
  // Single mode: only the most recently applied sort toggles through the cycle; anything else replaces.
208
189
  if (existing !== undefined && existingIndex === old.length - 1) {
209
- this.#table.setSorting(
190
+ this.#table.sorting =
210
191
  !hasManualValue && nextOrder === false
211
192
  ? old.filter((sort) => sort.id !== this.id)
212
- : old.map((sort) => (sort.id === this.id ? { ...sort, desc: nextDesc } : sort)),
213
- );
193
+ : old.map((sort) => (sort.id === this.id ? { ...sort, desc: nextDesc } : sort));
214
194
  return;
215
195
  }
216
- this.#table.setSorting([{ id: this.id, desc: nextDesc }]);
196
+ this.#table.sorting = [{ id: this.id, desc: nextDesc }];
217
197
  }
218
198
 
219
199
  get #canMultiSort(): boolean {
@@ -239,9 +219,9 @@ export class Column<TData, TValue = unknown> {
239
219
  get filterValue(): unknown {
240
220
  return this.#table.columnFilters.find((filter) => filter.id === this.id)?.value;
241
221
  }
242
- setFilterValue(value: unknown) {
222
+ set filterValue(value: unknown) {
243
223
  const others = this.#table.columnFilters.filter((filter) => filter.id !== this.id);
244
- this.#table.setColumnFilters(shouldAutoRemoveFilter(value) ? others : [...others, { id: this.id, value }]);
224
+ this.#table.columnFilters = shouldAutoRemoveFilter(value) ? others : [...others, { id: this.id, value }];
245
225
  }
246
226
 
247
227
  get facetedUniqueValues(): ReadonlyMap<unknown, number> {