runeforge 0.0.33 → 0.0.36

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.
@@ -33,6 +33,7 @@
33
33
  const datePopId = $props.id();
34
34
  const dateAnchorName = `--date-anchor-${datePopId}`;
35
35
  let datePopoverEl: HTMLElement | undefined = $state();
36
+ let calendarDateEl: (HTMLElement & { value: string }) | undefined = $state();
36
37
 
37
38
  const name = $derived(readonly ? undefined : field.attribute);
38
39
  const labelText = $derived(fieldLabel(field));
@@ -72,6 +73,22 @@
72
73
  );
73
74
  const isMultiValued = $derived(field.type === 'multiselect' || field.type === 'tree');
74
75
 
76
+ // cally's `change` event doesn't bubble, so the usual `onchange={...}` prop
77
+ // never fires — Svelte 5 delegates events like `change` to a listener on
78
+ // a shared ancestor, which only ever sees events that bubble up to it.
79
+ // Attaching directly on the element (same fix ColumnFilter's date-range
80
+ // picker already uses) sidesteps delegation entirely.
81
+ $effect(() => {
82
+ if (!calendarDateEl) return;
83
+ function handler() {
84
+ if (fieldDisabled) return;
85
+ record[field.attribute] = calendarDateEl!.value;
86
+ datePopoverEl?.hidePopover();
87
+ }
88
+ calendarDateEl.addEventListener('change', handler);
89
+ return () => calendarDateEl?.removeEventListener('change', handler);
90
+ });
91
+
75
92
  $effect(() => {
76
93
  if (!field.dependentOptions || isMultiValued) return;
77
94
  const current = record[field.attribute];
@@ -193,12 +210,8 @@
193
210
  >
194
211
  <calendar-date
195
212
  class="cally"
213
+ bind:this={calendarDateEl}
196
214
  value={String(record[field.attribute] ?? '')}
197
- onchange={(e: Event) => {
198
- if (fieldDisabled) return;
199
- record[field.attribute] = (e.currentTarget as HTMLElement & { value: string }).value;
200
- datePopoverEl?.hidePopover();
201
- }}
202
215
  >
203
216
  <svg
204
217
  aria-label={strings.previous}
@@ -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.36",
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",