runeforge 0.0.33 → 0.0.35

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.
@@ -64,21 +64,39 @@
64
64
  let currentPage = $state(pagination?.page ?? 1);
65
65
  let lastKnownPage = pagination?.page ?? 1;
66
66
 
67
+ const serverFilterSampleSize = 5;
68
+
69
+ // Server mode: there's no way to know every value a column can take without
70
+ // querying the whole (server-owned) dataset, so this is deliberately just a
71
+ // cosmetic hint — up to `serverFilterSampleSize` distinct values found on
72
+ // the current page, not an exhaustive list. Boolean is the one exception:
73
+ // its two states are always known, so both show up regardless of what the
74
+ // current page happens to contain.
67
75
  const distinctValues = $derived(
68
76
  pagination
69
- ? Object.fromEntries(
70
- columns
71
- .filter((c) => isFilterable(c) && c.type === 'boolean')
72
- .map((c) => [
73
- c.attribute,
74
- [
75
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
- { key: 'true', label: c.formatter?.(true as any, {} as T), row: {} as T },
77
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
- { key: 'false', label: c.formatter?.(false as any, {} as T), row: {} as T }
79
- ]
80
- ])
81
- )
77
+ ? {
78
+ ...Object.fromEntries(
79
+ Object.entries(
80
+ distinctEntries(
81
+ data,
82
+ columns.filter((c) => isFilterable(c) && c.type !== 'boolean')
83
+ )
84
+ ).map(([attribute, entries]) => [attribute, entries.slice(0, serverFilterSampleSize)])
85
+ ),
86
+ ...Object.fromEntries(
87
+ columns
88
+ .filter((c) => isFilterable(c) && c.type === 'boolean')
89
+ .map((c) => [
90
+ c.attribute,
91
+ [
92
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
+ { key: 'true', label: c.formatter?.(true as any, {} as T), row: {} as T },
94
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
+ { key: 'false', label: c.formatter?.(false as any, {} as T), row: {} as T }
96
+ ]
97
+ ])
98
+ )
99
+ }
82
100
  : distinctEntries(data, columns)
83
101
  );
84
102
 
@@ -1,95 +1,98 @@
1
1
  <script lang="ts">
2
- import Button from '../form/Button.svelte';
3
- import { getStrings } from '../../i18n/context.js';
2
+ import Button from '../form/Button.svelte';
3
+ import { getStrings } from '../../i18n/context.js';
4
4
 
5
- const strings = getStrings();
5
+ const strings = getStrings();
6
6
 
7
- let {
8
- page = $bindable(1),
9
- totalPages,
10
- pageStart,
11
- pageSize,
12
- total,
13
- }: {
14
- page?: number;
15
- totalPages: number;
16
- pageStart: number;
17
- pageSize: number;
18
- total: number;
19
- } = $props();
7
+ let {
8
+ page = $bindable(1),
9
+ totalPages,
10
+ pageStart,
11
+ pageSize,
12
+ total
13
+ }: {
14
+ page?: number;
15
+ totalPages: number;
16
+ pageStart: number;
17
+ pageSize: number;
18
+ total: number;
19
+ } = $props();
20
20
 
21
- function prev() {
22
- if (page > 1) page--;
23
- }
24
- function next() {
25
- if (page < totalPages) page++;
26
- }
21
+ function prev() {
22
+ if (page > 1) page--;
23
+ }
24
+ function next() {
25
+ if (page < totalPages) page++;
26
+ }
27
27
 
28
- function buildPages(current: number, total: number): number[] {
29
- if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
28
+ function buildPages(current: number, total: number): number[] {
29
+ if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
30
30
 
31
- const visible = new Set<number>();
32
- visible.add(1);
33
- visible.add(total);
34
- for (let i = Math.max(1, current - 2); i <= Math.min(total, current + 2); i++) {
35
- visible.add(i);
36
- }
31
+ // Local, immediately-discarded dedup set for this one calculation, not
32
+ // reactive state — a plain Set is correct here.
33
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
34
+ const visible = new Set<number>();
35
+ visible.add(1);
36
+ visible.add(total);
37
+ for (let i = Math.max(1, current - 2); i <= Math.min(total, current + 2); i++) {
38
+ visible.add(i);
39
+ }
37
40
 
38
- const sorted = Array.from(visible).sort((a, b) => a - b);
39
- const result: number[] = [];
40
- for (let i = 0; i < sorted.length; i++) {
41
- if (i > 0 && sorted[i] - sorted[i - 1] > 1) result.push(0);
42
- result.push(sorted[i]);
43
- }
44
- return result;
45
- }
41
+ const sorted = Array.from(visible).sort((a, b) => a - b);
42
+ const result: number[] = [];
43
+ for (let i = 0; i < sorted.length; i++) {
44
+ if (i > 0 && sorted[i] - sorted[i - 1] > 1) result.push(0);
45
+ result.push(sorted[i]);
46
+ }
47
+ return result;
48
+ }
46
49
 
47
- const pages = $derived(buildPages(page, totalPages));
50
+ const pages = $derived(buildPages(page, totalPages));
48
51
 
49
- function handlePageInput(e: KeyboardEvent) {
50
- if (e.key !== 'Enter') return;
51
- const val = parseInt((e.currentTarget as HTMLInputElement).value);
52
- if (!isNaN(val) && val >= 1 && val <= totalPages) {
53
- page = val;
54
- } else {
55
- (e.currentTarget as HTMLInputElement).value = String(page);
56
- }
57
- }
52
+ function handlePageInput(e: KeyboardEvent) {
53
+ if (e.key !== 'Enter') return;
54
+ const val = parseInt((e.currentTarget as HTMLInputElement).value);
55
+ if (!isNaN(val) && val >= 1 && val <= totalPages) {
56
+ page = val;
57
+ } else {
58
+ (e.currentTarget as HTMLInputElement).value = String(page);
59
+ }
60
+ }
58
61
 
59
- function resetInput(e: FocusEvent) {
60
- (e.currentTarget as HTMLInputElement).value = String(page);
61
- }
62
+ function resetInput(e: FocusEvent) {
63
+ (e.currentTarget as HTMLInputElement).value = String(page);
64
+ }
62
65
  </script>
63
66
 
64
67
  {#if totalPages > 1}
65
- <div class="flex items-center justify-between">
66
- <span class="text-sm text-base-content/60">
67
- {strings.showing(pageStart + 1, Math.min(pageStart + pageSize, total), total)}
68
- </span>
68
+ <div class="flex items-center justify-between">
69
+ <span class="text-sm text-base-content/60">
70
+ {strings.showing(pageStart + 1, Math.min(pageStart + pageSize, total), total)}
71
+ </span>
69
72
 
70
- <div class="join">
71
- <Button class="join-item btn-sm" disabled={page === 1} onclick={prev}>«</Button>
73
+ <div class="join">
74
+ <Button class="join-item btn-sm" disabled={page === 1} onclick={prev}>«</Button>
72
75
 
73
- {#each pages as p, i (i)}
74
- {#if p === 0}
75
- <Button class="join-item btn-sm" disabled>…</Button>
76
- {:else if p === page}
77
- <input
78
- type="number"
79
- class="join-item btn btn-sm w-12 text-center appearance-none"
80
- style="background-color: var(--color-base-100);"
81
- min="1"
82
- max={totalPages}
83
- value={page}
84
- onkeydown={handlePageInput}
85
- onblur={resetInput}
86
- />
87
- {:else}
88
- <Button class="join-item btn-sm" onclick={() => (page = p)}>{p}</Button>
89
- {/if}
90
- {/each}
76
+ {#each pages as p, i (i)}
77
+ {#if p === 0}
78
+ <Button class="join-item btn-sm" disabled>…</Button>
79
+ {:else if p === page}
80
+ <input
81
+ type="number"
82
+ class="join-item btn btn-sm w-12 text-center appearance-none"
83
+ style="background-color: var(--color-base-100);"
84
+ min="1"
85
+ max={totalPages}
86
+ value={page}
87
+ onkeydown={handlePageInput}
88
+ onblur={resetInput}
89
+ />
90
+ {:else}
91
+ <Button class="join-item btn-sm" onclick={() => (page = p)}>{p}</Button>
92
+ {/if}
93
+ {/each}
91
94
 
92
- <Button class="join-item btn-sm" disabled={page === totalPages} onclick={next}>»</Button>
93
- </div>
94
- </div>
95
+ <Button class="join-item btn-sm" disabled={page === totalPages} onclick={next}>»</Button>
96
+ </div>
97
+ </div>
95
98
  {/if}
@@ -33,9 +33,13 @@ export class SortState {
33
33
  const col = columns.find((c) => c.attribute === attribute);
34
34
  const factor = direction === 'asc' ? 1 : -1;
35
35
  if (col?.formatter) {
36
- return [...rows].sort((a, b) => factor * cellRenderedText(a.row, col).localeCompare(cellRenderedText(b.row, col), undefined, { numeric: true }));
36
+ return [...rows].sort((a, b) => factor *
37
+ cellRenderedText(a.row, col).localeCompare(cellRenderedText(b.row, col), undefined, {
38
+ numeric: true
39
+ }));
37
40
  }
38
- return [...rows].sort((a, b) => factor * compare(a.row[attribute], b.row[attribute]));
41
+ return [...rows].sort((a, b) => factor *
42
+ compare(a.row[attribute], b.row[attribute]));
39
43
  }
40
44
  }
41
45
  export class FilterState {
@@ -58,9 +62,9 @@ export class FilterState {
58
62
  return this.dateRanges.get(attribute) ?? { from: '', to: '' };
59
63
  }
60
64
  hasActive(attribute) {
61
- return (this.text.get(attribute)?.length ?? 0) > 0
62
- || (this.values.get(attribute)?.size ?? 0) > 0
63
- || !!(this.dateRanges.get(attribute)?.from || this.dateRanges.get(attribute)?.to);
65
+ return ((this.text.get(attribute)?.length ?? 0) > 0 ||
66
+ (this.values.get(attribute)?.size ?? 0) > 0 ||
67
+ !!(this.dateRanges.get(attribute)?.from || this.dateRanges.get(attribute)?.to));
64
68
  }
65
69
  isChecked(attribute, value) {
66
70
  return this.values.get(attribute)?.has(value) ?? false;
@@ -101,11 +105,16 @@ export class FilterState {
101
105
  const range = this.dateRanges.get(col.attribute);
102
106
  if (range?.from || range?.to) {
103
107
  const raw = row[col.attribute];
108
+ // Local, immediately-discarded timestamps for a one-off comparison,
109
+ // not reactive state — a plain Date is correct here.
110
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
104
111
  const ts = raw ? new Date(raw).getTime() : NaN;
105
112
  if (isNaN(ts))
106
113
  return false;
114
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
107
115
  if (range.from && ts < new Date(range.from).getTime())
108
116
  return false;
117
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
109
118
  if (range.to && ts > new Date(range.to + 'T23:59:59.999').getTime())
110
119
  return false;
111
120
  }
@@ -126,6 +135,6 @@ export function snapshotFilter(filter) {
126
135
  return {
127
136
  text: Object.fromEntries(filter.text),
128
137
  values: Object.fromEntries([...filter.values].map(([k, set]) => [k, [...set]])),
129
- dateRanges: Object.fromEntries(filter.dateRanges),
138
+ dateRanges: Object.fromEntries(filter.dateRanges)
130
139
  };
131
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runeforge",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "description": "SvelteKit toolkit for building metadata-driven CRUD interfaces with tables, forms, and actions",
5
5
  "license": "MIT",
6
6
  "author": "Ezequiel Puerta",