@viniaraujo68/plinth 0.3.1 → 0.6.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/README.md +197 -19
- package/dist/components/DateRangePicker.svelte +93 -57
- package/dist/components/DateRangePicker.svelte.d.ts +32 -15
- package/dist/components/ErrorDisplay.svelte +30 -16
- package/dist/components/ErrorDisplay.svelte.d.ts +4 -0
- package/dist/components/JsonView.svelte +43 -0
- package/dist/components/JsonView.svelte.d.ts +4 -0
- package/dist/components/JsonViewFrame.svelte +654 -0
- package/dist/components/JsonViewFrame.svelte.d.ts +31 -0
- package/dist/components/Paginator.svelte +34 -8
- package/dist/components/Paginator.svelte.d.ts +10 -0
- package/dist/components/RequestErrorDisplay.svelte +43 -0
- package/dist/components/RequestErrorDisplay.svelte.d.ts +11 -0
- package/dist/components/Tabs.svelte +204 -0
- package/dist/components/Tabs.svelte.d.ts +76 -0
- package/dist/components/VirtualJsonView.svelte +67 -0
- package/dist/components/VirtualJsonView.svelte.d.ts +4 -0
- package/dist/components/date-range.d.ts +82 -14
- package/dist/components/date-range.js +86 -5
- package/dist/components/index.d.ts +9 -2
- package/dist/components/index.js +15 -9
- package/dist/components/json-view.d.ts +70 -0
- package/dist/components/json-view.js +302 -0
- package/dist/components/request-error.d.ts +8 -0
- package/dist/components/request-error.js +55 -0
- package/dist/components/tabs.d.ts +5 -0
- package/dist/components/tabs.js +1 -0
- package/dist/http.d.ts +10 -5
- package/dist/http.js +34 -6
- package/dist/i18n/LocaleToggle.svelte +45 -0
- package/dist/i18n/LocaleToggle.svelte.d.ts +48 -0
- package/dist/i18n/i18n.svelte.d.ts +56 -0
- package/dist/i18n/i18n.svelte.js +94 -0
- package/dist/i18n/index.d.ts +3 -0
- package/dist/i18n/index.js +7 -0
- package/dist/i18n/translate.d.ts +30 -0
- package/dist/i18n/translate.js +38 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/redirect.d.ts +40 -0
- package/dist/redirect.js +59 -0
- package/dist/routing.svelte.d.ts +1 -0
- package/dist/routing.svelte.js +3 -0
- package/dist/shell/AppShell.svelte +265 -93
- package/dist/shell/AppShell.svelte.d.ts +3 -2
- package/dist/shell/NavigationProgress.svelte +85 -0
- package/dist/shell/NavigationProgress.svelte.d.ts +31 -0
- package/dist/shell/TopBar.svelte +191 -0
- package/dist/shell/TopBar.svelte.d.ts +41 -0
- package/dist/shell/UserAvatar.svelte +50 -0
- package/dist/shell/UserAvatar.svelte.d.ts +14 -0
- package/dist/shell/index.d.ts +2 -0
- package/dist/shell/index.js +7 -3
- package/dist/shell/initials.d.ts +6 -0
- package/dist/shell/initials.js +13 -0
- package/dist/table/DataTable.svelte +199 -9
- package/dist/table/DataTable.svelte.d.ts +28 -5
- package/dist/table/FilterBar.svelte +417 -0
- package/dist/table/FilterBar.svelte.d.ts +34 -0
- package/dist/table/FilterRuleEditor.svelte +290 -0
- package/dist/table/FilterRuleEditor.svelte.d.ts +33 -0
- package/dist/table/filter-labels.d.ts +23 -0
- package/dist/table/filter-labels.js +30 -0
- package/dist/table/filter.d.ts +27 -0
- package/dist/table/filter.js +279 -0
- package/dist/table/index.d.ts +4 -1
- package/dist/table/index.js +5 -2
- package/dist/table/types.d.ts +49 -0
- package/dist/toast/Toaster.svelte +7 -3
- package/dist/toast/index.d.ts +1 -1
- package/dist/toast/toast-manager.svelte.d.ts +29 -1
- package/dist/toast/toast-manager.svelte.js +61 -1
- package/dist/url-state/codec.d.ts +15 -0
- package/dist/url-state/codec.js +1 -0
- package/dist/url-state/index.d.ts +5 -0
- package/dist/url-state/index.js +4 -0
- package/dist/url-state/params-codec.d.ts +73 -0
- package/dist/url-state/params-codec.js +116 -0
- package/dist/url-state/search-params.d.ts +4 -0
- package/dist/url-state/search-params.js +28 -0
- package/dist/url-state/synced-state.svelte.d.ts +20 -0
- package/dist/url-state/synced-state.svelte.js +88 -0
- package/dist/url-state/url-store.svelte.d.ts +12 -0
- package/dist/url-state/url-store.svelte.js +56 -0
- package/dist/virtual/DataList.svelte +215 -0
- package/dist/virtual/DataList.svelte.d.ts +52 -0
- package/dist/virtual/VirtualList.svelte +150 -0
- package/dist/virtual/VirtualList.svelte.d.ts +42 -0
- package/dist/virtual/index.d.ts +7 -0
- package/dist/virtual/index.js +6 -0
- package/dist/virtual/keyed-cache.d.ts +7 -0
- package/dist/virtual/keyed-cache.js +20 -0
- package/dist/virtual/labels.d.ts +12 -0
- package/dist/virtual/labels.js +6 -0
- package/dist/virtual/paging.d.ts +5 -0
- package/dist/virtual/paging.js +11 -0
- package/dist/virtual/range.d.ts +9 -0
- package/dist/virtual/range.js +31 -0
- package/dist/virtual/size-tree.d.ts +10 -0
- package/dist/virtual/size-tree.js +56 -0
- package/dist/virtual/virtualizer.svelte.d.ts +38 -0
- package/dist/virtual/virtualizer.svelte.js +295 -0
- package/dist/virtual/windowed-data.svelte.d.ts +38 -0
- package/dist/virtual/windowed-data.svelte.js +139 -0
- package/dist/virtual/windowed-virtualizer.svelte.d.ts +9 -0
- package/dist/virtual/windowed-virtualizer.svelte.js +22 -0
- package/package.json +13 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export class SizeTree {
|
|
2
|
+
length;
|
|
3
|
+
#sizes;
|
|
4
|
+
#sums;
|
|
5
|
+
#topStep;
|
|
6
|
+
constructor(sizes) {
|
|
7
|
+
const length = sizes.length;
|
|
8
|
+
this.length = length;
|
|
9
|
+
this.#sizes = Float64Array.from(sizes);
|
|
10
|
+
this.#sums = new Float64Array(length + 1);
|
|
11
|
+
for (let node = 1; node <= length; node++) {
|
|
12
|
+
this.#sums[node] += this.#sizes[node - 1];
|
|
13
|
+
const parent = node + (node & -node);
|
|
14
|
+
if (parent <= length)
|
|
15
|
+
this.#sums[parent] += this.#sums[node];
|
|
16
|
+
}
|
|
17
|
+
this.#topStep = length === 0 ? 0 : 2 ** Math.floor(Math.log2(length));
|
|
18
|
+
}
|
|
19
|
+
get total() {
|
|
20
|
+
return this.startOf(this.length);
|
|
21
|
+
}
|
|
22
|
+
sizeAt(index) {
|
|
23
|
+
return index >= 0 && index < this.length ? this.#sizes[index] : 0;
|
|
24
|
+
}
|
|
25
|
+
set(index, size) {
|
|
26
|
+
if (index < 0 || index >= this.length)
|
|
27
|
+
return false;
|
|
28
|
+
const delta = size - this.#sizes[index];
|
|
29
|
+
if (delta === 0)
|
|
30
|
+
return false;
|
|
31
|
+
this.#sizes[index] = size;
|
|
32
|
+
for (let node = index + 1; node <= this.length; node += node & -node)
|
|
33
|
+
this.#sums[node] += delta;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
startOf(index) {
|
|
37
|
+
let sum = 0;
|
|
38
|
+
for (let node = Math.min(Math.max(0, index), this.length); node > 0; node -= node & -node)
|
|
39
|
+
sum += this.#sums[node];
|
|
40
|
+
return sum;
|
|
41
|
+
}
|
|
42
|
+
indexAt(offset) {
|
|
43
|
+
if (this.length === 0)
|
|
44
|
+
return -1;
|
|
45
|
+
let position = 0;
|
|
46
|
+
let remaining = offset;
|
|
47
|
+
for (let step = this.#topStep; step > 0; step = Math.floor(step / 2)) {
|
|
48
|
+
const next = position + step;
|
|
49
|
+
if (next <= this.length && this.#sums[next] <= remaining) {
|
|
50
|
+
position = next;
|
|
51
|
+
remaining -= this.#sums[next];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return Math.min(position, this.length - 1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Attachment } from "svelte/attachments";
|
|
2
|
+
import { type ScrollAlign } from "./range.js";
|
|
3
|
+
export type ItemKey = string | number;
|
|
4
|
+
export type ScrollAnchor = "start" | "end";
|
|
5
|
+
export interface VirtualItem {
|
|
6
|
+
index: number;
|
|
7
|
+
key: ItemKey;
|
|
8
|
+
start: number;
|
|
9
|
+
size: number;
|
|
10
|
+
}
|
|
11
|
+
export interface VirtualizerOptions {
|
|
12
|
+
count: () => number;
|
|
13
|
+
estimateSize?: number | ((index: number) => number);
|
|
14
|
+
overscan?: number | (() => number);
|
|
15
|
+
getItemKey?: (index: number) => ItemKey;
|
|
16
|
+
horizontal?: boolean;
|
|
17
|
+
anchorTo?: ScrollAnchor;
|
|
18
|
+
followOnAppend?: boolean;
|
|
19
|
+
initialOffset?: number | (() => number);
|
|
20
|
+
}
|
|
21
|
+
export declare class Virtualizer {
|
|
22
|
+
#private;
|
|
23
|
+
readonly horizontal: boolean;
|
|
24
|
+
readonly anchorTo: ScrollAnchor;
|
|
25
|
+
readonly items: VirtualItem[];
|
|
26
|
+
constructor(options: VirtualizerOptions);
|
|
27
|
+
get totalSize(): number;
|
|
28
|
+
get scrollOffset(): number;
|
|
29
|
+
get viewportSize(): number;
|
|
30
|
+
get isScrolling(): boolean;
|
|
31
|
+
viewport: Attachment<HTMLElement>;
|
|
32
|
+
measure: (index: number) => Attachment<HTMLElement>;
|
|
33
|
+
scrollToOffset(offset: number): void;
|
|
34
|
+
scrollToEnd(): void;
|
|
35
|
+
scrollToIndex(index: number, align?: ScrollAlign): void;
|
|
36
|
+
prepend(count: number): void;
|
|
37
|
+
clearMeasurements(): void;
|
|
38
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { untrack } from "svelte";
|
|
2
|
+
import { KeyedCache } from "./keyed-cache.js";
|
|
3
|
+
import { alignedOffset, clampOffset, visibleRange } from "./range.js";
|
|
4
|
+
import { SizeTree } from "./size-tree.js";
|
|
5
|
+
const DEFAULT_ESTIMATE = 48;
|
|
6
|
+
const DEFAULT_OVERSCAN = 6;
|
|
7
|
+
const SCROLL_IDLE_MS = 150;
|
|
8
|
+
const END_TOLERANCE = 4;
|
|
9
|
+
const SUBPIXEL = 0.5;
|
|
10
|
+
export class Virtualizer {
|
|
11
|
+
horizontal;
|
|
12
|
+
anchorTo;
|
|
13
|
+
#count;
|
|
14
|
+
#estimate;
|
|
15
|
+
#overscan;
|
|
16
|
+
#keyOf;
|
|
17
|
+
#followOnAppend;
|
|
18
|
+
#initialOffset;
|
|
19
|
+
#scrollOffset = $state(0);
|
|
20
|
+
#viewportSize = $state(0);
|
|
21
|
+
#isScrolling = $state(false);
|
|
22
|
+
#measured = $state.raw(new KeyedCache());
|
|
23
|
+
#measureAttachments = new KeyedCache();
|
|
24
|
+
#element;
|
|
25
|
+
#idleTimer;
|
|
26
|
+
#anchorIndex = 0;
|
|
27
|
+
#anchorDelta = 0;
|
|
28
|
+
#atEnd = false;
|
|
29
|
+
#previousCount = 0;
|
|
30
|
+
#skipNextFollow = false;
|
|
31
|
+
#ownScrollOffset = null;
|
|
32
|
+
#scrollRequest = 0;
|
|
33
|
+
#deferredTarget = null;
|
|
34
|
+
#initialApplied = false;
|
|
35
|
+
#layout = $derived.by(() => {
|
|
36
|
+
const count = Math.max(0, Math.floor(this.#count()));
|
|
37
|
+
const measured = this.#measured;
|
|
38
|
+
const sizes = new Float64Array(count);
|
|
39
|
+
for (let index = 0; index < count; index++)
|
|
40
|
+
sizes[index] = measured.get(this.#keyOf(index)) ?? this.#estimate(index);
|
|
41
|
+
return { tree: new SizeTree(sizes) };
|
|
42
|
+
});
|
|
43
|
+
items = $derived.by(() => {
|
|
44
|
+
const { tree } = this.#layout;
|
|
45
|
+
const range = visibleRange(tree, this.#scrollOffset, this.#viewportSize, this.#overscan());
|
|
46
|
+
if (!range)
|
|
47
|
+
return [];
|
|
48
|
+
const items = [];
|
|
49
|
+
let start = tree.startOf(range.from);
|
|
50
|
+
for (let index = range.from; index < range.to; index++) {
|
|
51
|
+
const size = tree.sizeAt(index);
|
|
52
|
+
items.push({ index, key: this.#keyOf(index), start, size });
|
|
53
|
+
start += size;
|
|
54
|
+
}
|
|
55
|
+
return items;
|
|
56
|
+
});
|
|
57
|
+
constructor(options) {
|
|
58
|
+
const { estimateSize = DEFAULT_ESTIMATE, overscan = DEFAULT_OVERSCAN } = options;
|
|
59
|
+
this.#count = options.count;
|
|
60
|
+
this.#estimate = typeof estimateSize === "function" ? estimateSize : () => estimateSize;
|
|
61
|
+
this.#overscan = typeof overscan === "function" ? overscan : () => overscan;
|
|
62
|
+
this.#keyOf = options.getItemKey ?? ((index) => index);
|
|
63
|
+
this.horizontal = options.horizontal ?? false;
|
|
64
|
+
this.anchorTo = options.anchorTo ?? "start";
|
|
65
|
+
this.#followOnAppend = options.followOnAppend ?? false;
|
|
66
|
+
this.#initialOffset = options.initialOffset;
|
|
67
|
+
$effect(() => {
|
|
68
|
+
const { tree } = this.#layout;
|
|
69
|
+
const viewportSize = this.#viewportSize;
|
|
70
|
+
untrack(() => this.#holdAnchor(tree, viewportSize));
|
|
71
|
+
});
|
|
72
|
+
$effect(() => {
|
|
73
|
+
const count = this.#count();
|
|
74
|
+
untrack(() => this.#countChanged(count));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
get totalSize() {
|
|
78
|
+
return this.#layout.tree.total;
|
|
79
|
+
}
|
|
80
|
+
get scrollOffset() {
|
|
81
|
+
return this.#scrollOffset;
|
|
82
|
+
}
|
|
83
|
+
get viewportSize() {
|
|
84
|
+
return this.#viewportSize;
|
|
85
|
+
}
|
|
86
|
+
get isScrolling() {
|
|
87
|
+
return this.#isScrolling;
|
|
88
|
+
}
|
|
89
|
+
viewport = (element) => untrack(() => {
|
|
90
|
+
this.#element = element;
|
|
91
|
+
this.#viewportSize = this.#clientSize(element);
|
|
92
|
+
this.#scrollOffset = this.#readScroll(element);
|
|
93
|
+
this.#captureAnchor();
|
|
94
|
+
const deferred = this.#deferredTarget;
|
|
95
|
+
this.#deferredTarget = null;
|
|
96
|
+
if (deferred) {
|
|
97
|
+
this.#scrollTo(deferred);
|
|
98
|
+
}
|
|
99
|
+
else if (!this.#initialApplied && this.#initialOffset !== undefined) {
|
|
100
|
+
const initial = this.#initialOffset;
|
|
101
|
+
this.#scrollTo(typeof initial === "function" ? initial : () => initial);
|
|
102
|
+
}
|
|
103
|
+
this.#initialApplied = true;
|
|
104
|
+
const onScroll = () => this.#userScrolled(element);
|
|
105
|
+
const observer = new ResizeObserver(() => {
|
|
106
|
+
this.#viewportSize = this.#clientSize(element);
|
|
107
|
+
});
|
|
108
|
+
element.addEventListener("scroll", onScroll, { passive: true });
|
|
109
|
+
observer.observe(element);
|
|
110
|
+
return () => {
|
|
111
|
+
element.removeEventListener("scroll", onScroll);
|
|
112
|
+
observer.disconnect();
|
|
113
|
+
clearTimeout(this.#idleTimer);
|
|
114
|
+
this.#isScrolling = false;
|
|
115
|
+
if (this.#element === element)
|
|
116
|
+
this.#element = undefined;
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
measure = (index) => this.#measureAttachments.getOrCreate(index, () => (element) => {
|
|
120
|
+
const observer = new ResizeObserver(([entry]) => this.#report(index, element, entry));
|
|
121
|
+
observer.observe(element);
|
|
122
|
+
return () => observer.disconnect();
|
|
123
|
+
});
|
|
124
|
+
scrollToOffset(offset) {
|
|
125
|
+
if (Number.isNaN(offset))
|
|
126
|
+
return;
|
|
127
|
+
this.#scrollTo(() => offset);
|
|
128
|
+
}
|
|
129
|
+
scrollToEnd() {
|
|
130
|
+
this.#scrollTo(() => Number.POSITIVE_INFINITY);
|
|
131
|
+
}
|
|
132
|
+
scrollToIndex(index, align = "start") {
|
|
133
|
+
if (Number.isNaN(index))
|
|
134
|
+
return;
|
|
135
|
+
untrack(() => {
|
|
136
|
+
const { tree } = this.#layout;
|
|
137
|
+
if (tree.length === 0)
|
|
138
|
+
return;
|
|
139
|
+
const target = Math.min(Math.max(0, Math.trunc(index)), tree.length - 1);
|
|
140
|
+
const resolved = align === "auto" ? this.#autoAlign(tree, target) : align;
|
|
141
|
+
if (!resolved)
|
|
142
|
+
return;
|
|
143
|
+
this.#scrollTo(() => {
|
|
144
|
+
const current = this.#layout.tree;
|
|
145
|
+
if (target >= current.length)
|
|
146
|
+
return null;
|
|
147
|
+
return alignedOffset(current.startOf(target), current.sizeAt(target), this.#viewportSize, this.#scrollOffset, resolved);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
prepend(count) {
|
|
152
|
+
if (!(count > 0))
|
|
153
|
+
return;
|
|
154
|
+
this.#anchorIndex += Math.floor(count);
|
|
155
|
+
this.#skipNextFollow = true;
|
|
156
|
+
}
|
|
157
|
+
clearMeasurements() {
|
|
158
|
+
untrack(() => {
|
|
159
|
+
this.#measured = new KeyedCache();
|
|
160
|
+
this.#measureAttachments.clear();
|
|
161
|
+
this.#captureAnchor();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
#autoAlign(tree, index) {
|
|
165
|
+
const start = tree.startOf(index);
|
|
166
|
+
const end = start + tree.sizeAt(index);
|
|
167
|
+
if (start < this.#scrollOffset)
|
|
168
|
+
return "start";
|
|
169
|
+
if (end > this.#scrollOffset + this.#viewportSize)
|
|
170
|
+
return "end";
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
#report(index, element, entry) {
|
|
174
|
+
untrack(() => {
|
|
175
|
+
const size = this.#sizeOf(element, entry);
|
|
176
|
+
if (!(size > 0))
|
|
177
|
+
return;
|
|
178
|
+
const { tree } = this.#layout;
|
|
179
|
+
if (index >= tree.length)
|
|
180
|
+
return;
|
|
181
|
+
this.#measured.set(this.#keyOf(index), size);
|
|
182
|
+
if (tree.set(index, size))
|
|
183
|
+
this.#layout = { tree };
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
#sizeOf(element, entry) {
|
|
187
|
+
const box = entry?.borderBoxSize[0];
|
|
188
|
+
if (box)
|
|
189
|
+
return this.horizontal ? box.inlineSize : box.blockSize;
|
|
190
|
+
const rect = element.getBoundingClientRect();
|
|
191
|
+
return this.horizontal ? rect.width : rect.height;
|
|
192
|
+
}
|
|
193
|
+
#userScrolled(element) {
|
|
194
|
+
const offset = this.#readScroll(element);
|
|
195
|
+
this.#scrollOffset = offset;
|
|
196
|
+
const own = this.#ownScrollOffset;
|
|
197
|
+
this.#ownScrollOffset = null;
|
|
198
|
+
if (own !== null && Math.abs(offset - own) < SUBPIXEL)
|
|
199
|
+
return;
|
|
200
|
+
this.#scrollRequest++;
|
|
201
|
+
this.#atEnd = this.#isAtEnd(offset);
|
|
202
|
+
this.#captureAnchor();
|
|
203
|
+
this.#isScrolling = true;
|
|
204
|
+
clearTimeout(this.#idleTimer);
|
|
205
|
+
this.#idleTimer = setTimeout(() => (this.#isScrolling = false), SCROLL_IDLE_MS);
|
|
206
|
+
}
|
|
207
|
+
#scrollTo(target) {
|
|
208
|
+
untrack(() => {
|
|
209
|
+
const element = this.#element;
|
|
210
|
+
if (!element) {
|
|
211
|
+
this.#deferredTarget = target;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const request = ++this.#scrollRequest;
|
|
215
|
+
this.#applyScroll(element, target);
|
|
216
|
+
requestAnimationFrame(() => {
|
|
217
|
+
const current = this.#element;
|
|
218
|
+
if (request === this.#scrollRequest && current)
|
|
219
|
+
this.#applyScroll(current, target);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
#applyScroll(element, target) {
|
|
224
|
+
untrack(() => {
|
|
225
|
+
const offset = target();
|
|
226
|
+
if (offset === null)
|
|
227
|
+
return;
|
|
228
|
+
this.#writeScroll(element, clampOffset(offset, this.totalSize, this.#viewportSize));
|
|
229
|
+
this.#atEnd = this.#isAtEnd(this.#scrollOffset);
|
|
230
|
+
this.#captureAnchor();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
#holdAnchor(tree, viewportSize) {
|
|
234
|
+
const element = this.#element;
|
|
235
|
+
if (!element || tree.length === 0)
|
|
236
|
+
return;
|
|
237
|
+
if (this.#anchorIndex >= tree.length) {
|
|
238
|
+
this.#anchorIndex = tree.length - 1;
|
|
239
|
+
this.#anchorDelta = 0;
|
|
240
|
+
}
|
|
241
|
+
const desired = Math.max(0, this.#anchoredOffset(tree, viewportSize));
|
|
242
|
+
if (Math.abs(desired - this.#readScroll(element)) > SUBPIXEL)
|
|
243
|
+
this.#writeScroll(element, desired);
|
|
244
|
+
}
|
|
245
|
+
#countChanged(count) {
|
|
246
|
+
const grew = count > this.#previousCount;
|
|
247
|
+
const skip = this.#skipNextFollow;
|
|
248
|
+
this.#previousCount = count;
|
|
249
|
+
this.#skipNextFollow = false;
|
|
250
|
+
if (count === 0) {
|
|
251
|
+
this.#anchorIndex = 0;
|
|
252
|
+
this.#anchorDelta = 0;
|
|
253
|
+
}
|
|
254
|
+
if (grew && !skip && this.#followOnAppend && this.#atEnd)
|
|
255
|
+
this.scrollToEnd();
|
|
256
|
+
}
|
|
257
|
+
#anchoredOffset(tree, viewportSize) {
|
|
258
|
+
if (this.anchorTo === "end")
|
|
259
|
+
return tree.startOf(this.#anchorIndex + 1) - viewportSize - this.#anchorDelta;
|
|
260
|
+
return tree.startOf(this.#anchorIndex) - this.#anchorDelta;
|
|
261
|
+
}
|
|
262
|
+
#captureAnchor() {
|
|
263
|
+
const { tree } = this.#layout;
|
|
264
|
+
if (tree.length === 0) {
|
|
265
|
+
this.#anchorIndex = 0;
|
|
266
|
+
this.#anchorDelta = 0;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (this.anchorTo === "end") {
|
|
270
|
+
const bottom = this.#scrollOffset + this.#viewportSize;
|
|
271
|
+
this.#anchorIndex = tree.indexAt(Math.max(0, bottom - 1));
|
|
272
|
+
this.#anchorDelta = tree.startOf(this.#anchorIndex + 1) - bottom;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
this.#anchorIndex = tree.indexAt(this.#scrollOffset);
|
|
276
|
+
this.#anchorDelta = tree.startOf(this.#anchorIndex) - this.#scrollOffset;
|
|
277
|
+
}
|
|
278
|
+
#isAtEnd(offset) {
|
|
279
|
+
return offset + this.#viewportSize >= this.totalSize - END_TOLERANCE;
|
|
280
|
+
}
|
|
281
|
+
#clientSize(element) {
|
|
282
|
+
return this.horizontal ? element.clientWidth : element.clientHeight;
|
|
283
|
+
}
|
|
284
|
+
#readScroll(element) {
|
|
285
|
+
return this.horizontal ? element.scrollLeft : element.scrollTop;
|
|
286
|
+
}
|
|
287
|
+
#writeScroll(element, offset) {
|
|
288
|
+
if (this.horizontal)
|
|
289
|
+
element.scrollLeft = offset;
|
|
290
|
+
else
|
|
291
|
+
element.scrollTop = offset;
|
|
292
|
+
this.#ownScrollOffset = this.#readScroll(element);
|
|
293
|
+
this.#scrollOffset = this.#ownScrollOffset;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface WindowedPage<Row> {
|
|
2
|
+
items: readonly Row[];
|
|
3
|
+
total: number;
|
|
4
|
+
}
|
|
5
|
+
export interface PageRequest {
|
|
6
|
+
page: number;
|
|
7
|
+
offset: number;
|
|
8
|
+
limit: number;
|
|
9
|
+
signal: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
export interface WindowedDataOptions<Row> {
|
|
12
|
+
pageSize: number;
|
|
13
|
+
fetchPage: (request: PageRequest) => Promise<WindowedPage<Row>>;
|
|
14
|
+
retain?: number;
|
|
15
|
+
signature?: () => string;
|
|
16
|
+
}
|
|
17
|
+
export type PageStatus = "loading" | "ready" | "error";
|
|
18
|
+
export interface PageFailure {
|
|
19
|
+
page: number;
|
|
20
|
+
error: unknown;
|
|
21
|
+
}
|
|
22
|
+
export declare class WindowedData<Row> {
|
|
23
|
+
#private;
|
|
24
|
+
readonly pageSize: number;
|
|
25
|
+
readonly retain: number;
|
|
26
|
+
readonly failures: PageFailure[];
|
|
27
|
+
readonly loading: boolean;
|
|
28
|
+
constructor(options: WindowedDataOptions<Row>);
|
|
29
|
+
get count(): number;
|
|
30
|
+
get resolved(): boolean;
|
|
31
|
+
get failedPages(): number[];
|
|
32
|
+
get error(): unknown;
|
|
33
|
+
get(index: number): Row | undefined;
|
|
34
|
+
statusOf(index: number): PageStatus | "idle";
|
|
35
|
+
ensure(start: number, end: number): void;
|
|
36
|
+
retry(): void;
|
|
37
|
+
dispose(): void;
|
|
38
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { untrack } from "svelte";
|
|
2
|
+
import { SvelteMap } from "svelte/reactivity";
|
|
3
|
+
import { pageSpan } from "./paging.js";
|
|
4
|
+
const DEFAULT_RETAIN = 2;
|
|
5
|
+
class PageSlot {
|
|
6
|
+
status = $state("loading");
|
|
7
|
+
rows = $state.raw([]);
|
|
8
|
+
error = $state.raw(undefined);
|
|
9
|
+
controller = null;
|
|
10
|
+
released = false;
|
|
11
|
+
}
|
|
12
|
+
export class WindowedData {
|
|
13
|
+
pageSize;
|
|
14
|
+
retain;
|
|
15
|
+
#fetchPage;
|
|
16
|
+
#signature;
|
|
17
|
+
#slots = new SvelteMap();
|
|
18
|
+
#currentSignature;
|
|
19
|
+
#disposed = false;
|
|
20
|
+
#count = $state(0);
|
|
21
|
+
#resolved = $state(false);
|
|
22
|
+
failures = $derived.by(() => [...this.#slots]
|
|
23
|
+
.filter(([, slot]) => slot.status === "error")
|
|
24
|
+
.map(([page, slot]) => ({ page, error: slot.error }))
|
|
25
|
+
.sort((left, right) => left.page - right.page));
|
|
26
|
+
loading = $derived.by(() => [...this.#slots.values()].some((slot) => slot.status === "loading"));
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.pageSize = Math.max(1, Math.floor(options.pageSize));
|
|
29
|
+
this.retain = Math.max(0, Math.floor(options.retain ?? DEFAULT_RETAIN));
|
|
30
|
+
this.#fetchPage = options.fetchPage;
|
|
31
|
+
this.#signature = options.signature;
|
|
32
|
+
}
|
|
33
|
+
get count() {
|
|
34
|
+
return this.#count;
|
|
35
|
+
}
|
|
36
|
+
get resolved() {
|
|
37
|
+
return this.#resolved;
|
|
38
|
+
}
|
|
39
|
+
get failedPages() {
|
|
40
|
+
return this.failures.map((failure) => failure.page);
|
|
41
|
+
}
|
|
42
|
+
get error() {
|
|
43
|
+
return this.failures[0]?.error;
|
|
44
|
+
}
|
|
45
|
+
get(index) {
|
|
46
|
+
if (!(index >= 0))
|
|
47
|
+
return undefined;
|
|
48
|
+
const slot = this.#slots.get(Math.floor(index / this.pageSize));
|
|
49
|
+
if (slot?.status !== "ready")
|
|
50
|
+
return undefined;
|
|
51
|
+
return slot.rows[index % this.pageSize];
|
|
52
|
+
}
|
|
53
|
+
statusOf(index) {
|
|
54
|
+
if (!(index >= 0))
|
|
55
|
+
return "idle";
|
|
56
|
+
return this.#slots.get(Math.floor(index / this.pageSize))?.status ?? "idle";
|
|
57
|
+
}
|
|
58
|
+
ensure(start, end) {
|
|
59
|
+
if (this.#disposed)
|
|
60
|
+
return;
|
|
61
|
+
const signature = this.#signature?.() ?? "";
|
|
62
|
+
untrack(() => {
|
|
63
|
+
if (signature !== this.#currentSignature)
|
|
64
|
+
this.#rekey(signature);
|
|
65
|
+
const span = pageSpan(start, end, this.pageSize, this.#resolved ? this.#count : null);
|
|
66
|
+
if (!span)
|
|
67
|
+
return;
|
|
68
|
+
for (let page = span.first; page <= span.last; page++) {
|
|
69
|
+
if (this.#slots.has(page))
|
|
70
|
+
continue;
|
|
71
|
+
const slot = new PageSlot();
|
|
72
|
+
this.#slots.set(page, slot);
|
|
73
|
+
this.#load(page, slot);
|
|
74
|
+
}
|
|
75
|
+
for (const [page, slot] of this.#slots) {
|
|
76
|
+
if (page >= span.first - this.retain && page <= span.last + this.retain)
|
|
77
|
+
continue;
|
|
78
|
+
this.#release(slot);
|
|
79
|
+
this.#slots.delete(page);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
retry() {
|
|
84
|
+
if (this.#disposed)
|
|
85
|
+
return;
|
|
86
|
+
untrack(() => {
|
|
87
|
+
for (const [page, slot] of this.#slots)
|
|
88
|
+
if (slot.status === "error")
|
|
89
|
+
this.#load(page, slot);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
dispose() {
|
|
93
|
+
this.#disposed = true;
|
|
94
|
+
untrack(() => this.#releaseAll());
|
|
95
|
+
}
|
|
96
|
+
#rekey(signature) {
|
|
97
|
+
this.#releaseAll();
|
|
98
|
+
this.#currentSignature = signature;
|
|
99
|
+
this.#count = 0;
|
|
100
|
+
this.#resolved = false;
|
|
101
|
+
}
|
|
102
|
+
#load(page, slot) {
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
slot.controller = controller;
|
|
105
|
+
slot.status = "loading";
|
|
106
|
+
slot.error = undefined;
|
|
107
|
+
const settled = (apply) => {
|
|
108
|
+
if (slot.released || controller.signal.aborted)
|
|
109
|
+
return;
|
|
110
|
+
slot.controller = null;
|
|
111
|
+
apply();
|
|
112
|
+
};
|
|
113
|
+
const request = (async () => this.#fetchPage({
|
|
114
|
+
page,
|
|
115
|
+
offset: page * this.pageSize,
|
|
116
|
+
limit: this.pageSize,
|
|
117
|
+
signal: controller.signal,
|
|
118
|
+
}))();
|
|
119
|
+
void request.then((result) => settled(() => {
|
|
120
|
+
slot.rows = result.items;
|
|
121
|
+
slot.status = "ready";
|
|
122
|
+
this.#count = Math.max(0, Math.floor(result.total));
|
|
123
|
+
this.#resolved = true;
|
|
124
|
+
}), (error) => settled(() => {
|
|
125
|
+
slot.error = error;
|
|
126
|
+
slot.status = "error";
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
#release(slot) {
|
|
130
|
+
slot.released = true;
|
|
131
|
+
slot.controller?.abort();
|
|
132
|
+
slot.controller = null;
|
|
133
|
+
}
|
|
134
|
+
#releaseAll() {
|
|
135
|
+
for (const slot of this.#slots.values())
|
|
136
|
+
this.#release(slot);
|
|
137
|
+
this.#slots.clear();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { WindowedData } from "./windowed-data.svelte.js";
|
|
2
|
+
import { Virtualizer, type VirtualizerOptions } from "./virtualizer.svelte.js";
|
|
3
|
+
export interface WindowedVirtualizerOptions<Row> extends Omit<VirtualizerOptions, "count"> {
|
|
4
|
+
data: WindowedData<Row>;
|
|
5
|
+
}
|
|
6
|
+
export declare class WindowedVirtualizer<Row> extends Virtualizer {
|
|
7
|
+
readonly data: WindowedData<Row>;
|
|
8
|
+
constructor({ data, ...options }: WindowedVirtualizerOptions<Row>);
|
|
9
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { untrack } from "svelte";
|
|
2
|
+
import { Virtualizer } from "./virtualizer.svelte.js";
|
|
3
|
+
export class WindowedVirtualizer extends Virtualizer {
|
|
4
|
+
data;
|
|
5
|
+
constructor({ data, ...options }) {
|
|
6
|
+
super({ ...options, count: () => data.count });
|
|
7
|
+
this.data = data;
|
|
8
|
+
$effect(() => {
|
|
9
|
+
const items = this.items;
|
|
10
|
+
const first = items.at(0)?.index ?? 0;
|
|
11
|
+
const last = items.at(-1)?.index ?? first;
|
|
12
|
+
data.ensure(first, last);
|
|
13
|
+
});
|
|
14
|
+
let hadRows = false;
|
|
15
|
+
$effect(() => {
|
|
16
|
+
const count = data.count;
|
|
17
|
+
if (count === 0 && hadRows)
|
|
18
|
+
untrack(() => this.clearMeasurements());
|
|
19
|
+
hadRows = count > 0;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viniaraujo68/plinth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A personal Svelte 5 design system: theme, application shell, interface primitives, routing metadata, a fetch wrapper and locale-aware formatters.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -96,6 +96,18 @@
|
|
|
96
96
|
"types": "./dist/formatters.d.ts",
|
|
97
97
|
"default": "./dist/formatters.js"
|
|
98
98
|
},
|
|
99
|
+
"./i18n": {
|
|
100
|
+
"types": "./dist/i18n/index.d.ts",
|
|
101
|
+
"svelte": "./dist/i18n/index.js"
|
|
102
|
+
},
|
|
103
|
+
"./virtual": {
|
|
104
|
+
"types": "./dist/virtual/index.d.ts",
|
|
105
|
+
"svelte": "./dist/virtual/index.js"
|
|
106
|
+
},
|
|
107
|
+
"./url-state": {
|
|
108
|
+
"types": "./dist/url-state/index.d.ts",
|
|
109
|
+
"svelte": "./dist/url-state/index.js"
|
|
110
|
+
},
|
|
99
111
|
"./package.json": "./package.json"
|
|
100
112
|
},
|
|
101
113
|
"peerDependencies": {
|