@streamscloud/kit 0.58.0 → 0.60.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/dist/core/data-loaders/cursor-data-loader-with-search.svelte.d.ts +5 -0
- package/dist/core/data-loaders/cursor-data-loader-with-search.svelte.js +19 -6
- package/dist/core/data-loaders/cursor-data-loader.svelte.d.ts +5 -0
- package/dist/core/data-loaders/cursor-data-loader.svelte.js +19 -6
- package/dist/core/data-loaders/data-loader.d.ts +2 -0
- package/dist/core/data-loaders/keyed-cursor-data-loader.svelte.d.ts +4 -0
- package/dist/core/data-loaders/keyed-cursor-data-loader.svelte.js +8 -2
- package/dist/core/data-loaders/page-data-loader.svelte.d.ts +5 -0
- package/dist/core/data-loaders/page-data-loader.svelte.js +12 -1
- package/dist/ui/cropper/img-cropper/cropperjs-elements.d.ts +9 -7
- package/dist/ui/dynamic-component/cmp.dynamic-component.svelte +0 -14
- package/dist/ui/dynamic-component/cmp.dynamic-component.svelte.d.ts +0 -14
- package/dist/ui/grid-card/fields/cmp.grid-card-progress-field.svelte +1 -2
- package/dist/ui/grid-card/fields/cmp.grid-card-progress-field.svelte.d.ts +1 -4
- package/dist/ui/infinite-scroll/cmp.infinite-scroll.svelte +27 -4
- package/dist/ui/infinite-scroll/cmp.infinite-scroll.svelte.d.ts +15 -2
- package/dist/ui/infinite-scroll/index.d.ts +1 -0
- package/dist/ui/infinite-scroll/infinite-scroll-localization.d.ts +3 -0
- package/dist/ui/infinite-scroll/infinite-scroll-localization.js +12 -0
- package/dist/ui/infinite-scroll/types.d.ts +5 -0
- package/dist/ui/infinite-scroll/types.js +1 -0
- package/dist/ui/player/providers/chunks-player-buffer/player-chunks-manager.svelte.d.ts +0 -1
- package/dist/ui/player/providers/chunks-player-buffer/player-chunks-manager.svelte.js +1 -7
- package/dist/ui/player/providers/default-chunks-player-buffer.svelte.d.ts +0 -1
- package/dist/ui/player/providers/default-chunks-player-buffer.svelte.js +0 -3
- package/dist/ui/player/providers/types.d.ts +2 -1
- package/dist/ui/popover/cmp.hover-popover.svelte +0 -9
- package/dist/ui/popover/cmp.hover-popover.svelte.d.ts +0 -9
- package/dist/ui/popover/cmp.popover.svelte +1 -9
- package/dist/ui/popover/cmp.popover.svelte.d.ts +1 -9
- package/package.json +23 -23
|
@@ -5,6 +5,7 @@ export declare class CursorDataLoaderWithSearch<T> implements IDataLoader<T> {
|
|
|
5
5
|
private _continuationToken;
|
|
6
6
|
private _searchString;
|
|
7
7
|
private _loadPage;
|
|
8
|
+
private _resumeAfterFailure;
|
|
8
9
|
private _pending;
|
|
9
10
|
private _generation;
|
|
10
11
|
private _searchStringMinLength;
|
|
@@ -13,14 +14,18 @@ export declare class CursorDataLoaderWithSearch<T> implements IDataLoader<T> {
|
|
|
13
14
|
constructor(init: {
|
|
14
15
|
loadPage: (continuationToken: ContinuationToken, searchString: string) => Promise<CursorResult<T> | null>;
|
|
15
16
|
searchStringMinLength?: number;
|
|
17
|
+
/** Keep the cursor when a page fails, so the next `loadMore` retries it instead of ending the list for good. @default false */
|
|
18
|
+
resumeAfterFailure?: boolean;
|
|
16
19
|
});
|
|
17
20
|
get searchString(): string;
|
|
18
21
|
get loading(): boolean;
|
|
19
22
|
get initialLoading(): boolean;
|
|
20
23
|
get failed(): boolean;
|
|
24
|
+
get canLoadMore(): boolean;
|
|
21
25
|
loadMore: () => Promise<T[]>;
|
|
22
26
|
reset(): Promise<void>;
|
|
23
27
|
updateSearchString: (searchString: string | null) => void;
|
|
24
28
|
private runLoad;
|
|
29
|
+
private markFailed;
|
|
25
30
|
private isSearchStringEffective;
|
|
26
31
|
}
|
|
@@ -5,6 +5,7 @@ export class CursorDataLoaderWithSearch {
|
|
|
5
5
|
_continuationToken = $state.raw(ContinuationToken.init());
|
|
6
6
|
_searchString = $state.raw('');
|
|
7
7
|
_loadPage;
|
|
8
|
+
_resumeAfterFailure;
|
|
8
9
|
_pending = null;
|
|
9
10
|
_generation = 0;
|
|
10
11
|
_searchStringMinLength = 1;
|
|
@@ -12,6 +13,7 @@ export class CursorDataLoaderWithSearch {
|
|
|
12
13
|
_failed = $state(false);
|
|
13
14
|
constructor(init) {
|
|
14
15
|
this._loadPage = init.loadPage;
|
|
16
|
+
this._resumeAfterFailure = init.resumeAfterFailure ?? false;
|
|
15
17
|
if (init.searchStringMinLength !== undefined) {
|
|
16
18
|
this._searchStringMinLength = init.searchStringMinLength;
|
|
17
19
|
}
|
|
@@ -29,6 +31,9 @@ export class CursorDataLoaderWithSearch {
|
|
|
29
31
|
get failed() {
|
|
30
32
|
return this._failed;
|
|
31
33
|
}
|
|
34
|
+
get canLoadMore() {
|
|
35
|
+
return this._continuationToken.canLoadMore;
|
|
36
|
+
}
|
|
32
37
|
loadMore = async () => {
|
|
33
38
|
if (this._pending) {
|
|
34
39
|
return this._pending;
|
|
@@ -80,19 +85,27 @@ export class CursorDataLoaderWithSearch {
|
|
|
80
85
|
if (generation !== this._generation) {
|
|
81
86
|
return [];
|
|
82
87
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
if (page === null) {
|
|
89
|
+
this.markFailed();
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
this._continuationToken = page.continuationToken;
|
|
93
|
+
this.items = [...this.items, ...page.items];
|
|
94
|
+
return page.items;
|
|
88
95
|
}
|
|
89
96
|
catch (error) {
|
|
90
97
|
console.error('CursorDataLoaderWithSearch: failed to load page', error);
|
|
91
98
|
if (generation === this._generation) {
|
|
92
|
-
this.
|
|
99
|
+
this.markFailed();
|
|
93
100
|
}
|
|
94
101
|
return [];
|
|
95
102
|
}
|
|
96
103
|
};
|
|
104
|
+
markFailed = () => {
|
|
105
|
+
this._failed = true;
|
|
106
|
+
if (!this._resumeAfterFailure) {
|
|
107
|
+
this._continuationToken = ContinuationToken.preventLoading();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
97
110
|
isSearchStringEffective = (searchString) => searchString && searchString.length >= this._searchStringMinLength;
|
|
98
111
|
}
|
|
@@ -4,17 +4,22 @@ export declare class CursorDataLoader<T> implements IDataLoader<T> {
|
|
|
4
4
|
items: T[];
|
|
5
5
|
continuationToken: ContinuationToken;
|
|
6
6
|
private _loadPage;
|
|
7
|
+
private _resumeAfterFailure;
|
|
7
8
|
private _pending;
|
|
8
9
|
private _generation;
|
|
9
10
|
private _loading;
|
|
10
11
|
private _failed;
|
|
11
12
|
constructor(init: {
|
|
12
13
|
loadPage: (continuationToken: ContinuationToken) => Promise<CursorResult<T> | null>;
|
|
14
|
+
/** Keep the cursor when a page fails, so the next `loadMore` retries it instead of ending the list for good. @default false */
|
|
15
|
+
resumeAfterFailure?: boolean;
|
|
13
16
|
});
|
|
14
17
|
get loading(): boolean;
|
|
15
18
|
get initialLoading(): boolean;
|
|
16
19
|
get failed(): boolean;
|
|
20
|
+
get canLoadMore(): boolean;
|
|
17
21
|
loadMore: () => Promise<T[]>;
|
|
18
22
|
reset(): Promise<void>;
|
|
19
23
|
private runLoad;
|
|
24
|
+
private markFailed;
|
|
20
25
|
}
|
|
@@ -3,12 +3,14 @@ export class CursorDataLoader {
|
|
|
3
3
|
items = $state.raw([]);
|
|
4
4
|
continuationToken = $state.raw(ContinuationToken.init());
|
|
5
5
|
_loadPage;
|
|
6
|
+
_resumeAfterFailure;
|
|
6
7
|
_pending = null;
|
|
7
8
|
_generation = 0;
|
|
8
9
|
_loading = $state(false);
|
|
9
10
|
_failed = $state(false);
|
|
10
11
|
constructor(init) {
|
|
11
12
|
this._loadPage = init.loadPage;
|
|
13
|
+
this._resumeAfterFailure = init.resumeAfterFailure ?? false;
|
|
12
14
|
}
|
|
13
15
|
get loading() {
|
|
14
16
|
return this._loading;
|
|
@@ -19,6 +21,9 @@ export class CursorDataLoader {
|
|
|
19
21
|
get failed() {
|
|
20
22
|
return this._failed;
|
|
21
23
|
}
|
|
24
|
+
get canLoadMore() {
|
|
25
|
+
return this.continuationToken.canLoadMore;
|
|
26
|
+
}
|
|
22
27
|
loadMore = async () => {
|
|
23
28
|
if (this._pending) {
|
|
24
29
|
return this._pending;
|
|
@@ -57,18 +62,26 @@ export class CursorDataLoader {
|
|
|
57
62
|
if (generation !== this._generation) {
|
|
58
63
|
return [];
|
|
59
64
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
if (page === null) {
|
|
66
|
+
this.markFailed();
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
this.continuationToken = page.continuationToken;
|
|
70
|
+
this.items = [...this.items, ...page.items];
|
|
71
|
+
return page.items;
|
|
65
72
|
}
|
|
66
73
|
catch (error) {
|
|
67
74
|
console.error('CursorDataLoader: failed to load page', error);
|
|
68
75
|
if (generation === this._generation) {
|
|
69
|
-
this.
|
|
76
|
+
this.markFailed();
|
|
70
77
|
}
|
|
71
78
|
return [];
|
|
72
79
|
}
|
|
73
80
|
};
|
|
81
|
+
markFailed = () => {
|
|
82
|
+
this._failed = true;
|
|
83
|
+
if (!this._resumeAfterFailure) {
|
|
84
|
+
this.continuationToken = ContinuationToken.preventLoading();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
74
87
|
}
|
|
@@ -5,5 +5,7 @@ export interface IDataLoader<T> {
|
|
|
5
5
|
initialLoading: boolean;
|
|
6
6
|
/** The last attempt did not deliver a page, whether it returned nothing or threw; cleared when the next attempt starts. */
|
|
7
7
|
failed: boolean;
|
|
8
|
+
/** More pages are reachable. Goes false at the end of the list, and on a failure unless the loader was built with `resumeAfterFailure`. */
|
|
9
|
+
canLoadMore: boolean;
|
|
8
10
|
loadMore: () => Promise<T[]>;
|
|
9
11
|
}
|
|
@@ -5,16 +5,20 @@ export declare class KeyedCursorDataLoader<T> {
|
|
|
5
5
|
private _pending;
|
|
6
6
|
private _loadPage;
|
|
7
7
|
private _loadWithFirstPage;
|
|
8
|
+
private _resumeAfterFailure;
|
|
8
9
|
constructor(init: {
|
|
9
10
|
loadPage: (key: string, continuationToken: ContinuationToken) => Promise<CursorResult<T> | null>;
|
|
10
11
|
/** Started with the key's first page — never with `loadMore` — and awaited by `ensureLoaded`; its outcome never reaches `isLoaded`. */
|
|
11
12
|
loadWithFirstPage?: (key: string) => Promise<void>;
|
|
13
|
+
/** Keep a key's cursor when its page fails, so the next `loadMore` for it retries instead of ending that key's list for good. @default false */
|
|
14
|
+
resumeAfterFailure?: boolean;
|
|
12
15
|
});
|
|
13
16
|
items: (key?: string) => T[];
|
|
14
17
|
loading: (key?: string) => boolean;
|
|
15
18
|
initialLoading: (key?: string) => boolean;
|
|
16
19
|
/** The key's last attempt did not deliver a page, whether it returned nothing or threw; cleared when the next attempt for that key starts. */
|
|
17
20
|
failed: (key?: string) => boolean;
|
|
21
|
+
/** More pages are reachable for the key. Goes false on a failed page unless the loader was built with `resumeAfterFailure`. */
|
|
18
22
|
canLoadMore: (key?: string) => boolean;
|
|
19
23
|
/** False until the key's first page loaded without failing, so a failed load can be retried; a later failed page never clears it. */
|
|
20
24
|
isLoaded: (key?: string) => boolean;
|
|
@@ -6,16 +6,19 @@ export class KeyedCursorDataLoader {
|
|
|
6
6
|
_pending = {};
|
|
7
7
|
_loadPage;
|
|
8
8
|
_loadWithFirstPage;
|
|
9
|
+
_resumeAfterFailure;
|
|
9
10
|
constructor(init) {
|
|
10
11
|
this._loadPage = init.loadPage;
|
|
11
12
|
this._loadWithFirstPage = init.loadWithFirstPage;
|
|
13
|
+
this._resumeAfterFailure = init.resumeAfterFailure ?? false;
|
|
12
14
|
}
|
|
13
15
|
items = (key = '') => this.loaderFor(key).items;
|
|
14
16
|
loading = (key = '') => this.loaderFor(key).loading;
|
|
15
17
|
initialLoading = (key = '') => this.loaderFor(key).initialLoading;
|
|
16
18
|
/** The key's last attempt did not deliver a page, whether it returned nothing or threw; cleared when the next attempt for that key starts. */
|
|
17
19
|
failed = (key = '') => this.loaderFor(key).failed;
|
|
18
|
-
|
|
20
|
+
/** More pages are reachable for the key. Goes false on a failed page unless the loader was built with `resumeAfterFailure`. */
|
|
21
|
+
canLoadMore = (key = '') => this.loaderFor(key).canLoadMore;
|
|
19
22
|
/** False until the key's first page loaded without failing, so a failed load can be retried; a later failed page never clears it. */
|
|
20
23
|
isLoaded = (key = '') => this._loaded[key] === true;
|
|
21
24
|
/** Loads the key's first page once; concurrent and repeat calls share that load's promise. */
|
|
@@ -61,7 +64,10 @@ export class KeyedCursorDataLoader {
|
|
|
61
64
|
};
|
|
62
65
|
loaderFor = (key) => {
|
|
63
66
|
if (!this._loaders[key]) {
|
|
64
|
-
this._loaders[key] = new CursorDataLoader({
|
|
67
|
+
this._loaders[key] = new CursorDataLoader({
|
|
68
|
+
loadPage: (continuationToken) => this._loadPage(key, continuationToken),
|
|
69
|
+
resumeAfterFailure: this._resumeAfterFailure
|
|
70
|
+
});
|
|
65
71
|
}
|
|
66
72
|
return this._loaders[key];
|
|
67
73
|
};
|
|
@@ -6,16 +6,21 @@ export declare class PageDataLoader<T> implements IDataLoader<T> {
|
|
|
6
6
|
private _canLoadMore;
|
|
7
7
|
private _perPage;
|
|
8
8
|
private _loadPage;
|
|
9
|
+
private _resumeAfterFailure;
|
|
9
10
|
private _pageNumber;
|
|
10
11
|
private _generation;
|
|
11
12
|
constructor(init: {
|
|
12
13
|
loadPage: (page: number, perPage: number) => Promise<T[]>;
|
|
13
14
|
perPage?: number;
|
|
15
|
+
/** Keep the page number when a page fails, so the next `loadMore` retries it instead of ending the list for good. @default false */
|
|
16
|
+
resumeAfterFailure?: boolean;
|
|
14
17
|
});
|
|
15
18
|
get items(): T[];
|
|
16
19
|
get loading(): boolean;
|
|
17
20
|
get initialLoading(): boolean;
|
|
18
21
|
get failed(): boolean;
|
|
22
|
+
get canLoadMore(): boolean;
|
|
19
23
|
loadMore: () => Promise<T[]>;
|
|
20
24
|
reset: () => Promise<void>;
|
|
25
|
+
private markFailed;
|
|
21
26
|
}
|
|
@@ -5,10 +5,12 @@ export class PageDataLoader {
|
|
|
5
5
|
_canLoadMore = $state(true);
|
|
6
6
|
_perPage = 20;
|
|
7
7
|
_loadPage;
|
|
8
|
+
_resumeAfterFailure;
|
|
8
9
|
_pageNumber = 1;
|
|
9
10
|
_generation = 0;
|
|
10
11
|
constructor(init) {
|
|
11
12
|
this._loadPage = init.loadPage;
|
|
13
|
+
this._resumeAfterFailure = init.resumeAfterFailure ?? false;
|
|
12
14
|
if (init.perPage) {
|
|
13
15
|
this._perPage = init.perPage;
|
|
14
16
|
}
|
|
@@ -25,6 +27,9 @@ export class PageDataLoader {
|
|
|
25
27
|
get failed() {
|
|
26
28
|
return this._failed;
|
|
27
29
|
}
|
|
30
|
+
get canLoadMore() {
|
|
31
|
+
return this._canLoadMore;
|
|
32
|
+
}
|
|
28
33
|
loadMore = async () => {
|
|
29
34
|
if (this._loading || !this._canLoadMore) {
|
|
30
35
|
return [];
|
|
@@ -46,7 +51,7 @@ export class PageDataLoader {
|
|
|
46
51
|
catch (error) {
|
|
47
52
|
console.error('PageDataLoader: failed to load page', error);
|
|
48
53
|
if (generation === this._generation) {
|
|
49
|
-
this.
|
|
54
|
+
this.markFailed();
|
|
50
55
|
}
|
|
51
56
|
return [];
|
|
52
57
|
}
|
|
@@ -64,4 +69,10 @@ export class PageDataLoader {
|
|
|
64
69
|
this._canLoadMore = true;
|
|
65
70
|
await this.loadMore();
|
|
66
71
|
};
|
|
72
|
+
markFailed = () => {
|
|
73
|
+
this._failed = true;
|
|
74
|
+
if (!this._resumeAfterFailure) {
|
|
75
|
+
this._canLoadMore = false;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
67
78
|
}
|
|
@@ -9,15 +9,17 @@ type CamelToKebab<S extends string> = S extends `${infer F}${infer R}`
|
|
|
9
9
|
: S;
|
|
10
10
|
|
|
11
11
|
type CropperElementProps<T extends HTMLElement> = {
|
|
12
|
-
[
|
|
13
|
-
|
|
14
|
-
: K extends `$${string}`
|
|
12
|
+
[
|
|
13
|
+
K in keyof T as K extends keyof HTMLElement
|
|
15
14
|
? never
|
|
16
|
-
:
|
|
15
|
+
: K extends `$${string}`
|
|
17
16
|
? never
|
|
18
|
-
: K extends
|
|
19
|
-
?
|
|
20
|
-
:
|
|
17
|
+
: T[K] extends (...args: never[]) => unknown
|
|
18
|
+
? never
|
|
19
|
+
: K extends string
|
|
20
|
+
? CamelToKebab<K>
|
|
21
|
+
: never
|
|
22
|
+
]?: (T[K] extends boolean ? boolean : T[K] extends number ? number | string : string) | null;
|
|
21
23
|
} & import('svelte/elements').HTMLAttributes<HTMLElement>;
|
|
22
24
|
|
|
23
25
|
declare namespace svelteHTML {
|
|
@@ -12,19 +12,5 @@ Use when the component to render is decided at runtime (e.g. dispatched by id, f
|
|
|
12
12
|
renders many heterogeneous nodes). `DynamicComponentModel` keeps `props` reactive — calling
|
|
13
13
|
`model.updateProps(...)` updates the rendered component without re-mounting.
|
|
14
14
|
|
|
15
|
-
```svelte
|
|
16
|
-
<script lang="ts">
|
|
17
|
-
import { DynamicComponent, DynamicComponentModel } from '@streamscloud/kit/dynamic-component';
|
|
18
|
-
import { Button } from '@streamscloud/kit/button';
|
|
19
|
-
|
|
20
|
-
const model = new DynamicComponentModel({
|
|
21
|
-
component: Button,
|
|
22
|
-
props: { type: 'button', children: () => 'Click' }
|
|
23
|
-
});
|
|
24
|
-
</script>
|
|
25
|
-
|
|
26
|
-
<DynamicComponent {model} />
|
|
27
|
-
```
|
|
28
|
-
|
|
29
15
|
This component has no styles or visual surface of its own.
|
|
30
16
|
-->
|
|
@@ -10,20 +10,6 @@ type Props = {
|
|
|
10
10
|
* renders many heterogeneous nodes). `DynamicComponentModel` keeps `props` reactive — calling
|
|
11
11
|
* `model.updateProps(...)` updates the rendered component without re-mounting.
|
|
12
12
|
*
|
|
13
|
-
* ```svelte
|
|
14
|
-
* <script lang="ts">
|
|
15
|
-
* import { DynamicComponent, DynamicComponentModel } from '@streamscloud/kit/dynamic-component';
|
|
16
|
-
* import { Button } from '@streamscloud/kit/button';
|
|
17
|
-
*
|
|
18
|
-
* const model = new DynamicComponentModel({
|
|
19
|
-
* component: Button,
|
|
20
|
-
* props: { type: 'button', children: () => 'Click' }
|
|
21
|
-
* });
|
|
22
|
-
* </script>
|
|
23
|
-
*
|
|
24
|
-
* <DynamicComponent {model} />
|
|
25
|
-
* ```
|
|
26
|
-
*
|
|
27
13
|
* This component has no styles or visual surface of its own.
|
|
28
14
|
*/
|
|
29
15
|
declare const Cmp: import("svelte").Component<Props, {}, "">;
|
|
@@ -9,6 +9,5 @@ const { label, stage, stages } = $props();
|
|
|
9
9
|
|
|
10
10
|
<!--
|
|
11
11
|
@component
|
|
12
|
-
Grid-card field pairing a field label with a multi-stage `ProgressBar`. See `ProgressBar`
|
|
13
|
-
(`@streamscloud/kit/ui/progress-bar`) for the bar's own CSS Custom Properties.
|
|
12
|
+
Grid-card field pairing a field label with a multi-stage `ProgressBar`. See `ProgressBar` for the bar's own CSS Custom Properties.
|
|
14
13
|
-->
|
|
@@ -9,10 +9,7 @@ type Props = {
|
|
|
9
9
|
*/
|
|
10
10
|
stages: ProgressBarStage[];
|
|
11
11
|
};
|
|
12
|
-
/**
|
|
13
|
-
* Grid-card field pairing a field label with a multi-stage `ProgressBar`. See `ProgressBar`
|
|
14
|
-
* (`@streamscloud/kit/ui/progress-bar`) for the bar's own CSS Custom Properties.
|
|
15
|
-
*/
|
|
12
|
+
/** Grid-card field pairing a field label with a multi-stage `ProgressBar`. See `ProgressBar` for the bar's own CSS Custom Properties. */
|
|
16
13
|
declare const Cmp: import("svelte").Component<Props, {}, "">;
|
|
17
14
|
type Cmp = ReturnType<typeof Cmp>;
|
|
18
15
|
export default Cmp;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
2
|
-
|
|
1
|
+
<script lang="ts">import { Button } from '../button';
|
|
2
|
+
import { Spinner } from '../spinner';
|
|
3
|
+
import { InfiniteScrollLocalization } from './infinite-scroll-localization';
|
|
4
|
+
let { loadMore, container = null, rootMargin = '200px', children, loading, retry } = $props();
|
|
5
|
+
const localization = new InfiniteScrollLocalization();
|
|
6
|
+
const source = $derived(typeof loadMore === 'function' ? { loadMore, failed: false, canLoadMore: true } : loadMore);
|
|
3
7
|
let isLoading = $state(false);
|
|
4
8
|
let paginationTrigger = $state(null);
|
|
5
9
|
let itemsContainer = $state(null);
|
|
@@ -11,7 +15,8 @@ $effect(() => {
|
|
|
11
15
|
const items = itemsContainer;
|
|
12
16
|
const intersectionObserver = new IntersectionObserver((e) => {
|
|
13
17
|
const [entries] = e;
|
|
14
|
-
|
|
18
|
+
// an armed sentinel over a failing backend retries in a loop — the manual control is the only way on from here
|
|
19
|
+
if (entries.isIntersecting && !source.failed) {
|
|
15
20
|
void onLoadMore();
|
|
16
21
|
}
|
|
17
22
|
}, {
|
|
@@ -36,12 +41,15 @@ const onLoadMore = async () => {
|
|
|
36
41
|
}
|
|
37
42
|
isLoading = true;
|
|
38
43
|
try {
|
|
39
|
-
await loadMore();
|
|
44
|
+
await source.loadMore();
|
|
40
45
|
}
|
|
41
46
|
finally {
|
|
42
47
|
isLoading = false;
|
|
43
48
|
}
|
|
44
49
|
};
|
|
50
|
+
const onRetry = () => {
|
|
51
|
+
void onLoadMore();
|
|
52
|
+
};
|
|
45
53
|
</script>
|
|
46
54
|
|
|
47
55
|
<div class="infinite-scroll">
|
|
@@ -58,6 +66,14 @@ const onLoadMore = async () => {
|
|
|
58
66
|
<Spinner timeout={500} />
|
|
59
67
|
</div>
|
|
60
68
|
{/if}
|
|
69
|
+
{:else if source.failed && source.canLoadMore}
|
|
70
|
+
{#if retry}
|
|
71
|
+
{@render retry(onRetry)}
|
|
72
|
+
{:else}
|
|
73
|
+
<div class="infinite-scroll__retry">
|
|
74
|
+
<Button type="button" variant="secondary" size="sm" on={{ click: onRetry }}>{localization.loadMore}</Button>
|
|
75
|
+
</div>
|
|
76
|
+
{/if}
|
|
61
77
|
{/if}
|
|
62
78
|
</div>
|
|
63
79
|
|
|
@@ -67,6 +83,8 @@ Triggers an async `loadMore` callback when a sentinel element scrolls into view,
|
|
|
67
83
|
|
|
68
84
|
Pairs with `core/data-loaders` (`CursorDataLoader`, `PageDataLoader`, `CursorDataLoaderWithSearch`) — wire `loadMore={loader.loadMore}` and `{#each loader.items as item}` directly.
|
|
69
85
|
|
|
86
|
+
Passing the loader itself — `loadMore={loader}` instead of `loadMore={loader.loadMore}` — disarms the sentinel while the loader reports `failed`, so a failing backend is not asked again on its own and the already-loaded list is kept. The manual continue appears only when the loader can still continue (`resumeAfterFailure`); a loader that closed its list on the failure ends silently, as it does at the natural end. A successful retry re-arms the sentinel on its own.
|
|
87
|
+
|
|
70
88
|
### CSS Custom Properties
|
|
71
89
|
| Property | Description | Default |
|
|
72
90
|
|---|---|---|
|
|
@@ -100,4 +118,9 @@ Pairs with `core/data-loaders` (`CursorDataLoader`, `PageDataLoader`, `CursorDat
|
|
|
100
118
|
bottom: 0.3125em;
|
|
101
119
|
left: 50%;
|
|
102
120
|
transform: translateX(-50%);
|
|
121
|
+
}
|
|
122
|
+
.infinite-scroll__retry {
|
|
123
|
+
display: flex;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
padding: var(--sc-kit--space--3) 0;
|
|
103
126
|
}</style>
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import type { LoadMoreSource } from './types';
|
|
1
2
|
import type { Snippet } from 'svelte';
|
|
2
3
|
type Props = {
|
|
3
|
-
/**
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Where the next batch comes from. Either an async callback invoked when the sentinel becomes visible — it should resolve once the batch is appended
|
|
6
|
+
* (or no-op when there are no more items — `CursorDataLoader.loadMore` already returns `[]` in that case) — or a data loader, whose `failed` then also
|
|
7
|
+
* stops the sentinel from firing on its own and offers a manual continue instead.
|
|
8
|
+
*
|
|
9
|
+
* The object form must report `failed` and `canLoadMore` reactively: pass the loader itself, or an object literal rebuilt by the surrounding reactive
|
|
10
|
+
* scope (`{ loadMore: () => l.loadMore(key), failed: l.failed(key), canLoadMore: l.canLoadMore(key) }`). A plain object holding fixed values type-checks
|
|
11
|
+
* and leaves the sentinel armed through every failure.
|
|
12
|
+
*/
|
|
13
|
+
loadMore: LoadMoreSource;
|
|
5
14
|
/** Scroll ancestor used as the IntersectionObserver root. Required when the viewport differs from the scrolling container. */
|
|
6
15
|
container?: HTMLElement | null;
|
|
7
16
|
/** IntersectionObserver `rootMargin`. Default `'200px'` — fires the load a viewport-screen before reaching the bottom. */
|
|
@@ -9,12 +18,16 @@ type Props = {
|
|
|
9
18
|
children: Snippet;
|
|
10
19
|
/** Custom loading indicator snippet. Defaults to the kit `Spinner`. */
|
|
11
20
|
loading?: Snippet;
|
|
21
|
+
/** Manual continue shown while `failed`, taking the callback that retries the failed page. Defaults to a kit `Button`. */
|
|
22
|
+
retry?: Snippet<[() => void]>;
|
|
12
23
|
};
|
|
13
24
|
/**
|
|
14
25
|
* Triggers an async `loadMore` callback when a sentinel element scrolls into view, using IntersectionObserver. Re-observes the sentinel on container resize so layout shifts inside the parent re-evaluate visibility (a known IntersectionObserver gotcha). Internal `isLoading` guard prevents overlapping fires while a load is in flight.
|
|
15
26
|
*
|
|
16
27
|
* Pairs with `core/data-loaders` (`CursorDataLoader`, `PageDataLoader`, `CursorDataLoaderWithSearch`) — wire `loadMore={loader.loadMore}` and `{#each loader.items as item}` directly.
|
|
17
28
|
*
|
|
29
|
+
* Passing the loader itself — `loadMore={loader}` instead of `loadMore={loader.loadMore}` — disarms the sentinel while the loader reports `failed`, so a failing backend is not asked again on its own and the already-loaded list is kept. The manual continue appears only when the loader can still continue (`resumeAfterFailure`); a loader that closed its list on the failure ends silently, as it does at the natural end. A successful retry re-arms the sentinel on its own.
|
|
30
|
+
*
|
|
18
31
|
* ### CSS Custom Properties
|
|
19
32
|
* | Property | Description | Default |
|
|
20
33
|
* |---|---|---|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -18,6 +18,5 @@ export declare class PlayerChunksManager<TItem extends WithId, TChunk extends Wi
|
|
|
18
18
|
setActiveChunkIndex: (index: number, chunkItemIndex: number) => Promise<void>;
|
|
19
19
|
activateItemAtFlattenedIndex: (index: number) => Promise<void>;
|
|
20
20
|
warmUp: () => Promise<void>;
|
|
21
|
-
reset: () => Promise<void>;
|
|
22
21
|
private warmUpSequentially;
|
|
23
22
|
}
|
|
@@ -163,14 +163,7 @@ export class PlayerChunksManager {
|
|
|
163
163
|
this._warmUpDeferred = null;
|
|
164
164
|
}
|
|
165
165
|
};
|
|
166
|
-
reset = async () => {
|
|
167
|
-
this._activeChunkIndex = -1;
|
|
168
|
-
this._loadedChunks = [];
|
|
169
|
-
this._warmUpDeferred = null;
|
|
170
|
-
await this.warmUp();
|
|
171
|
-
};
|
|
172
166
|
warmUpSequentially = async () => {
|
|
173
|
-
const startChunkIndex = Math.max(0, this._activeChunkIndex);
|
|
174
167
|
// Calculate how many items we need ahead of current position
|
|
175
168
|
const getItemsAhead = () => {
|
|
176
169
|
const currentFlatIndex = this.flattenedActiveChunkItemIndex;
|
|
@@ -179,6 +172,7 @@ export class PlayerChunksManager {
|
|
|
179
172
|
};
|
|
180
173
|
while (getItemsAhead() < ITEMS_BUFFER_SIZE) {
|
|
181
174
|
// Find first non-fully-loaded chunk starting from active
|
|
175
|
+
const startChunkIndex = Math.max(0, this._activeChunkIndex);
|
|
182
176
|
let targetChunkIndex = -1;
|
|
183
177
|
for (let i = startChunkIndex; i < this._loadedChunks.length; i++) {
|
|
184
178
|
if (this._loadedChunks[i].canLoadMore) {
|
|
@@ -62,9 +62,6 @@ export class DefaultChunksPlayerBuffer {
|
|
|
62
62
|
}
|
|
63
63
|
this._playerChunksManager.activateItemAtFlattenedIndex(this.currentIndex - 1);
|
|
64
64
|
};
|
|
65
|
-
reset = () => {
|
|
66
|
-
this._playerChunksManager.reset();
|
|
67
|
-
};
|
|
68
65
|
ensureWarmedUp = async () => {
|
|
69
66
|
await this._playerChunksManager.warmUp();
|
|
70
67
|
};
|
|
@@ -34,7 +34,6 @@ export interface IPlayerBufferBase<T extends WithId> {
|
|
|
34
34
|
readonly animationDuration: number;
|
|
35
35
|
loadNext: () => void;
|
|
36
36
|
loadPrevious: () => void;
|
|
37
|
-
reset: () => void;
|
|
38
37
|
ensureWarmedUp: () => Promise<void>;
|
|
39
38
|
tryActivateItemById: (id: string) => boolean;
|
|
40
39
|
removeItemById: (id: string) => void;
|
|
@@ -44,6 +43,8 @@ type TExtended<T> = T & {
|
|
|
44
43
|
};
|
|
45
44
|
export interface IFeedPlayerBuffer<T extends WithId> extends IPlayerBufferBase<T> {
|
|
46
45
|
readonly kind: 'feed';
|
|
46
|
+
/** Drops the buffered feed and loads it again from the start. */
|
|
47
|
+
reset: () => void;
|
|
47
48
|
}
|
|
48
49
|
export interface IChunksPlayerBuffer<T extends WithId> extends IPlayerBufferBase<T> {
|
|
49
50
|
readonly kind: 'chunks';
|
|
@@ -48,15 +48,6 @@ to content stays "inside" the hover zone (no close fires). The `closeDelay` cove
|
|
|
48
48
|
small visual gap between trigger and content.
|
|
49
49
|
|
|
50
50
|
`bind:this` on HoverPopover forwards `open() / close() / toggle()` from the inner Popover.
|
|
51
|
-
|
|
52
|
-
```svelte
|
|
53
|
-
import { HoverPopover, PopoverItem } from '@streamscloud/kit/ui/popover';
|
|
54
|
-
|
|
55
|
-
<HoverPopover>
|
|
56
|
-
{#snippet trigger()}<Button type="presentational">Hover me</Button>{/snippet}
|
|
57
|
-
<PopoverItem>Action</PopoverItem>
|
|
58
|
-
</HoverPopover>
|
|
59
|
-
```
|
|
60
51
|
-->
|
|
61
52
|
|
|
62
53
|
<style>.hover-popover {
|
|
@@ -33,15 +33,6 @@ type Props = {
|
|
|
33
33
|
* small visual gap between trigger and content.
|
|
34
34
|
*
|
|
35
35
|
* `bind:this` on HoverPopover forwards `open() / close() / toggle()` from the inner Popover.
|
|
36
|
-
*
|
|
37
|
-
* ```svelte
|
|
38
|
-
* import { HoverPopover, PopoverItem } from '@streamscloud/kit/ui/popover';
|
|
39
|
-
*
|
|
40
|
-
* <HoverPopover>
|
|
41
|
-
* {#snippet trigger()}<Button type="presentational">Hover me</Button>{/snippet}
|
|
42
|
-
* <PopoverItem>Action</PopoverItem>
|
|
43
|
-
* </HoverPopover>
|
|
44
|
-
* ```
|
|
45
36
|
*/
|
|
46
37
|
declare const Cmp: import("svelte").Component<Props, {
|
|
47
38
|
/** Imperative API — bind:this on HoverPopover to access. */ open: () => void;
|
|
@@ -226,15 +226,7 @@ the same state can be reached by keyboard through another control (as in `UrlInp
|
|
|
226
226
|
protocol into the field does what the picker does). Never reach for it just to shorten a tab sequence.
|
|
227
227
|
|
|
228
228
|
Imperative control via `bind:this` — the component exports `open()`, `close()`, `toggle()`
|
|
229
|
-
methods
|
|
230
|
-
|
|
231
|
-
```svelte
|
|
232
|
-
import { Popover, type PopoverInstance } from '@streamscloud/kit/ui/popover';
|
|
233
|
-
let dd: PopoverInstance | undefined = $state.raw(undefined);
|
|
234
|
-
|
|
235
|
-
<Popover bind:this={dd}>...</Popover>
|
|
236
|
-
<button onclick={() => dd?.open()}>Open externally</button>
|
|
237
|
-
```
|
|
229
|
+
methods; type the ref with the exported `PopoverInstance` type.
|
|
238
230
|
|
|
239
231
|
With `panel` on, content sizes to `max-content` and, above the `min-width` floor, never exceeds the room Floating UI measures around the trigger: the `size()` middleware feeds the space left after
|
|
240
232
|
`flip` / `shift` into the max-width / max-height defaults. So the panel always has a definite width — which is what makes a consumer's own
|
|
@@ -51,15 +51,7 @@ type Props = {
|
|
|
51
51
|
* protocol into the field does what the picker does). Never reach for it just to shorten a tab sequence.
|
|
52
52
|
*
|
|
53
53
|
* Imperative control via `bind:this` — the component exports `open()`, `close()`, `toggle()`
|
|
54
|
-
* methods
|
|
55
|
-
*
|
|
56
|
-
* ```svelte
|
|
57
|
-
* import { Popover, type PopoverInstance } from '@streamscloud/kit/ui/popover';
|
|
58
|
-
* let dd: PopoverInstance | undefined = $state.raw(undefined);
|
|
59
|
-
*
|
|
60
|
-
* <Popover bind:this={dd}>...</Popover>
|
|
61
|
-
* <button onclick={() => dd?.open()}>Open externally</button>
|
|
62
|
-
* ```
|
|
54
|
+
* methods; type the ref with the exported `PopoverInstance` type.
|
|
63
55
|
*
|
|
64
56
|
* With `panel` on, content sizes to `max-content` and, above the `min-width` floor, never exceeds the room Floating UI measures around the trigger: the `size()` middleware feeds the space left after
|
|
65
57
|
* `flip` / `shift` into the max-width / max-height defaults. So the panel always has a definite width — which is what makes a consumer's own
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamscloud/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.0",
|
|
4
4
|
"author": "StreamsCloud",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -482,11 +482,11 @@
|
|
|
482
482
|
},
|
|
483
483
|
"peerDependencies": {
|
|
484
484
|
"@floating-ui/dom": "^1.7.6",
|
|
485
|
-
"@fluentui/svg-icons": "^1.1.
|
|
485
|
+
"@fluentui/svg-icons": "^1.1.341",
|
|
486
486
|
"@fontsource/inter": "^5.2.8",
|
|
487
487
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
|
488
488
|
"@fontsource/source-sans-pro": "^5.2.5",
|
|
489
|
-
"@urql/core": "^
|
|
489
|
+
"@urql/core": "^6.0.1",
|
|
490
490
|
"cleave-zen": "^0.0.17",
|
|
491
491
|
"colord": "^2.9.3",
|
|
492
492
|
"cropperjs": "^2.1.1",
|
|
@@ -498,7 +498,7 @@
|
|
|
498
498
|
"nanoid": "^5.1.11",
|
|
499
499
|
"p-limit": "^7.3.0",
|
|
500
500
|
"rfdc": "^1.4.1",
|
|
501
|
-
"svelte": "^5.
|
|
501
|
+
"svelte": "^5.57.0",
|
|
502
502
|
"svelte-awesome-color-picker": "^4.1.2",
|
|
503
503
|
"svelte-dnd-action": "^0.9.69",
|
|
504
504
|
"wheel-gestures": "^2.2.48",
|
|
@@ -507,50 +507,50 @@
|
|
|
507
507
|
"devDependencies": {
|
|
508
508
|
"@eslint/js": "^10.0.1",
|
|
509
509
|
"@floating-ui/dom": "^1.7.6",
|
|
510
|
-
"@fluentui/svg-icons": "^1.1.
|
|
510
|
+
"@fluentui/svg-icons": "^1.1.341",
|
|
511
511
|
"@fontsource/inter": "^5.2.8",
|
|
512
512
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
|
513
513
|
"@fontsource/source-sans-pro": "^5.2.5",
|
|
514
|
-
"@sveltejs/package": "^2.5.
|
|
515
|
-
"@sveltejs/vite-plugin-svelte": "^7.
|
|
514
|
+
"@sveltejs/package": "^2.5.8",
|
|
515
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
516
516
|
"@tsconfig/svelte": "^5.0.8",
|
|
517
|
-
"@types/node": "^
|
|
517
|
+
"@types/node": "^26.6.1",
|
|
518
518
|
"@urql/core": "^6.0.1",
|
|
519
|
-
"autoprefixer": "^10.
|
|
519
|
+
"autoprefixer": "^10.6.1",
|
|
520
520
|
"cleave-zen": "^0.0.17",
|
|
521
521
|
"colord": "^2.9.3",
|
|
522
522
|
"cropperjs": "^2.1.1",
|
|
523
523
|
"dequal": "^2.0.3",
|
|
524
524
|
"dompurify": "^3.4.5",
|
|
525
|
-
"eslint": "^10.
|
|
525
|
+
"eslint": "^10.10.0",
|
|
526
526
|
"eslint-config-prettier": "^10.1.8",
|
|
527
527
|
"eslint-formatter-codeframe": "^7.32.2",
|
|
528
528
|
"eslint-formatter-visualstudio": "^9.0.1",
|
|
529
|
-
"eslint-plugin-import-x": "^4.
|
|
530
|
-
"eslint-plugin-jsonc": "^3.
|
|
529
|
+
"eslint-plugin-import-x": "^4.17.1",
|
|
530
|
+
"eslint-plugin-jsonc": "^3.4.2",
|
|
531
531
|
"eslint-plugin-only-warn": "^1.2.1",
|
|
532
532
|
"eslint-plugin-promise": "^7.3.0",
|
|
533
|
-
"eslint-plugin-svelte": "^3.
|
|
533
|
+
"eslint-plugin-svelte": "^3.23.0",
|
|
534
534
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
535
535
|
"fuse.js": "^7.3.0",
|
|
536
|
-
"globals": "^17.
|
|
536
|
+
"globals": "^17.12.0",
|
|
537
537
|
"hugerte": "^1.0.10",
|
|
538
538
|
"mime": "^4.1.0",
|
|
539
539
|
"nanoid": "^5.1.11",
|
|
540
540
|
"p-limit": "^7.3.0",
|
|
541
|
-
"prettier": "^3.
|
|
542
|
-
"prettier-plugin-svelte": "^4.1.
|
|
543
|
-
"publint": "^0.3.
|
|
541
|
+
"prettier": "^3.9.7",
|
|
542
|
+
"prettier-plugin-svelte": "^4.1.1",
|
|
543
|
+
"publint": "^0.3.24",
|
|
544
544
|
"rfdc": "^1.4.1",
|
|
545
|
-
"sass": "^1.
|
|
546
|
-
"svelte": "^5.
|
|
545
|
+
"sass": "^1.104.1",
|
|
546
|
+
"svelte": "^5.57.0",
|
|
547
547
|
"svelte-awesome-color-picker": "^4.1.2",
|
|
548
|
-
"svelte-check": "^4.
|
|
548
|
+
"svelte-check": "^4.7.6",
|
|
549
549
|
"svelte-dnd-action": "^0.9.69",
|
|
550
|
-
"svelte-preprocess": "^6.0.
|
|
550
|
+
"svelte-preprocess": "^6.0.5",
|
|
551
551
|
"typescript": "^6.0.3",
|
|
552
|
-
"typescript-eslint": "^8.
|
|
553
|
-
"vite": "^8.0
|
|
552
|
+
"typescript-eslint": "^8.70.0",
|
|
553
|
+
"vite": "^8.3.0",
|
|
554
554
|
"vite-tsconfig-paths": "^6.1.1",
|
|
555
555
|
"wheel-gestures": "^2.2.48",
|
|
556
556
|
"yup": "^1.7.1"
|