sheer-ui 0.3.0 → 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
package/src/lib/assets/ui.css
CHANGED
|
@@ -569,6 +569,28 @@ body:has([data-text-selection-guard]) {
|
|
|
569
569
|
}
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
+
/* Without anchor positioning (iOS Safari ≤18, Firefox ESR 140; ~11% 2026-08) every surface
|
|
573
|
+
centers on screen instead of hanging off its trigger. The UA popover default already
|
|
574
|
+
centers (inset 0, auto margins, fit-content box), but preflight's `* { margin: 0 }` sends it
|
|
575
|
+
to the top-left and a consumer's `w-auto!` stretches it edge to edge, since an auto width
|
|
576
|
+
between two insets is solved, not shrunk; the fit-content caps restore the box. The JS
|
|
577
|
+
floating surfaces carry inline `inset: auto` and collision margins, hence the importants.
|
|
578
|
+
Delete once ESR 140 is out of support and iOS 18 is under 1%. */
|
|
579
|
+
@supports not (anchor-name: --a) {
|
|
580
|
+
[data-anchored] {
|
|
581
|
+
inset: 0;
|
|
582
|
+
margin: auto;
|
|
583
|
+
max-width: fit-content;
|
|
584
|
+
height: fit-content;
|
|
585
|
+
}
|
|
586
|
+
[data-floating-content] {
|
|
587
|
+
inset: 0 !important;
|
|
588
|
+
margin: auto !important;
|
|
589
|
+
max-width: fit-content;
|
|
590
|
+
height: fit-content;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
572
594
|
/* An exiting anchored surface (still shown while its [data-state=closed] fade runs, hidden by
|
|
573
595
|
the deferred hidePopover) must not catch the pointer. Shared here, not restated per surface. */
|
|
574
596
|
[data-anchored][data-state='closed'] {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
selectedValues.add(option.value);
|
|
75
75
|
}
|
|
76
76
|
const filterValues = Array.from(selectedValues);
|
|
77
|
-
column
|
|
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
|
|
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
|
|
32
|
+
{#if search}
|
|
32
33
|
<Input
|
|
33
34
|
{disabled}
|
|
34
35
|
placeholder={searchPlaceholder ?? 'Filter...'}
|
|
35
|
-
value={(
|
|
36
|
-
oninput={(e) =>
|
|
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.
|
|
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
|
|
23
|
+
readonly selectedRowIds = new SvelteSet<string>();
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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(
|
|
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.#
|
|
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.#
|
|
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.
|
|
198
|
-
|
|
199
|
-
?
|
|
200
|
-
|
|
201
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
222
|
+
set filterValue(value: unknown) {
|
|
243
223
|
const others = this.#table.columnFilters.filter((filter) => filter.id !== this.id);
|
|
244
|
-
this.#table.
|
|
224
|
+
this.#table.columnFilters = shouldAutoRemoveFilter(value) ? others : [...others, { id: this.id, value }];
|
|
245
225
|
}
|
|
246
226
|
|
|
247
227
|
get facetedUniqueValues(): ReadonlyMap<unknown, number> {
|