@streamscloud/kit 0.55.1 → 0.57.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 +7 -5
- package/dist/core/data-loaders/cursor-data-loader-with-search.svelte.js +31 -21
- package/dist/core/data-loaders/cursor-data-loader.svelte.d.ts +5 -3
- package/dist/core/data-loaders/cursor-data-loader.svelte.js +24 -14
- package/dist/core/data-loaders/data-loader.d.ts +2 -0
- package/dist/core/data-loaders/index.d.ts +1 -0
- package/dist/core/data-loaders/index.js +1 -0
- package/dist/core/data-loaders/keyed-cursor-data-loader.svelte.d.ts +28 -0
- package/dist/core/data-loaders/keyed-cursor-data-loader.svelte.js +66 -0
- package/dist/core/data-loaders/page-data-loader.svelte.d.ts +7 -5
- package/dist/core/data-loaders/page-data-loader.svelte.js +25 -17
- package/dist/ui/player/buttons/cmp.mobile-player-buttons.svelte +19 -6
- package/dist/ui/player/buttons/cmp.mobile-player-buttons.svelte.d.ts +4 -1
- package/dist/ui/player/buttons/cmp.player-buttons.svelte +64 -29
- package/dist/ui/player/buttons/cmp.player-buttons.svelte.d.ts +4 -2
- package/dist/ui/player/buttons/player-context-menu.svelte +1 -1
- package/dist/ui/player/buttons/types.d.ts +10 -2
- package/dist/ui/popover/cmp.popover-item.svelte +21 -6
- package/dist/ui/popover/cmp.popover-item.svelte.d.ts +4 -1
- package/package.json +1 -1
|
@@ -2,13 +2,14 @@ import { ContinuationToken, type CursorResult } from '..';
|
|
|
2
2
|
import type { IDataLoader } from './data-loader';
|
|
3
3
|
export declare class CursorDataLoaderWithSearch<T> implements IDataLoader<T> {
|
|
4
4
|
items: T[];
|
|
5
|
-
private
|
|
5
|
+
private _continuationToken;
|
|
6
6
|
private _searchString;
|
|
7
|
-
private
|
|
8
|
-
private
|
|
9
|
-
private
|
|
10
|
-
private
|
|
7
|
+
private _loadPage;
|
|
8
|
+
private _pending;
|
|
9
|
+
private _generation;
|
|
10
|
+
private _searchStringMinLength;
|
|
11
11
|
private _loading;
|
|
12
|
+
private _failed;
|
|
12
13
|
constructor(init: {
|
|
13
14
|
loadPage: (continuationToken: ContinuationToken, searchString: string) => Promise<CursorResult<T> | null>;
|
|
14
15
|
searchStringMinLength?: number;
|
|
@@ -16,6 +17,7 @@ export declare class CursorDataLoaderWithSearch<T> implements IDataLoader<T> {
|
|
|
16
17
|
get searchString(): string;
|
|
17
18
|
get loading(): boolean;
|
|
18
19
|
get initialLoading(): boolean;
|
|
20
|
+
get failed(): boolean;
|
|
19
21
|
loadMore: () => Promise<T[]>;
|
|
20
22
|
reset(): Promise<void>;
|
|
21
23
|
updateSearchString: (searchString: string | null) => void;
|
|
@@ -2,17 +2,18 @@ import { ContinuationToken } from '..';
|
|
|
2
2
|
import { Utils } from '../utils';
|
|
3
3
|
export class CursorDataLoaderWithSearch {
|
|
4
4
|
items = $state.raw([]);
|
|
5
|
-
|
|
5
|
+
_continuationToken = $state.raw(ContinuationToken.init());
|
|
6
6
|
_searchString = $state.raw('');
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
_loadPage;
|
|
8
|
+
_pending = null;
|
|
9
|
+
_generation = 0;
|
|
10
|
+
_searchStringMinLength = 1;
|
|
11
11
|
_loading = $state(false);
|
|
12
|
+
_failed = $state(false);
|
|
12
13
|
constructor(init) {
|
|
13
|
-
this.
|
|
14
|
+
this._loadPage = init.loadPage;
|
|
14
15
|
if (init.searchStringMinLength !== undefined) {
|
|
15
|
-
this.
|
|
16
|
+
this._searchStringMinLength = init.searchStringMinLength;
|
|
16
17
|
}
|
|
17
18
|
this.updateSearchString = Utils.debounce(this.updateSearchString, 400);
|
|
18
19
|
}
|
|
@@ -25,33 +26,36 @@ export class CursorDataLoaderWithSearch {
|
|
|
25
26
|
get initialLoading() {
|
|
26
27
|
return this._loading && this.items.length === 0;
|
|
27
28
|
}
|
|
29
|
+
get failed() {
|
|
30
|
+
return this._failed;
|
|
31
|
+
}
|
|
28
32
|
loadMore = async () => {
|
|
29
|
-
if (this.
|
|
30
|
-
return this.
|
|
33
|
+
if (this._pending) {
|
|
34
|
+
return this._pending;
|
|
31
35
|
}
|
|
32
|
-
if (!this.
|
|
36
|
+
if (!this._continuationToken.canLoadMore) {
|
|
33
37
|
return [];
|
|
34
38
|
}
|
|
35
39
|
this._loading = true;
|
|
36
40
|
const pending = this.runLoad();
|
|
37
|
-
this.
|
|
41
|
+
this._pending = pending;
|
|
38
42
|
try {
|
|
39
43
|
return await pending;
|
|
40
44
|
}
|
|
41
45
|
finally {
|
|
42
46
|
// a concurrent reset() may have installed a newer load — leave that one alone
|
|
43
|
-
if (this.
|
|
44
|
-
this.
|
|
47
|
+
if (this._pending === pending) {
|
|
48
|
+
this._pending = null;
|
|
45
49
|
this._loading = false;
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
};
|
|
49
53
|
async reset() {
|
|
50
|
-
this.
|
|
51
|
-
this.
|
|
54
|
+
this._generation++;
|
|
55
|
+
this._pending = null;
|
|
52
56
|
this._loading = false;
|
|
53
57
|
this.items = [];
|
|
54
|
-
this.
|
|
58
|
+
this._continuationToken = ContinuationToken.init();
|
|
55
59
|
await this.loadMore();
|
|
56
60
|
}
|
|
57
61
|
updateSearchString = (searchString) => {
|
|
@@ -67,22 +71,28 @@ export class CursorDataLoaderWithSearch {
|
|
|
67
71
|
}
|
|
68
72
|
};
|
|
69
73
|
runLoad = async () => {
|
|
70
|
-
const generation = this.
|
|
74
|
+
const generation = this._generation;
|
|
71
75
|
const search = this.isSearchStringEffective(this._searchString) ? this._searchString : '';
|
|
76
|
+
this._failed = false;
|
|
72
77
|
try {
|
|
73
|
-
const
|
|
78
|
+
const page = await this._loadPage(this._continuationToken, search);
|
|
74
79
|
// a reset() mid-load bumps the generation — drop the stale page instead of appending it to the cleared list
|
|
75
|
-
if (generation !== this.
|
|
80
|
+
if (generation !== this._generation) {
|
|
76
81
|
return [];
|
|
77
82
|
}
|
|
78
|
-
|
|
83
|
+
const result = page ?? { items: [], continuationToken: ContinuationToken.preventLoading() };
|
|
84
|
+
this._failed = page === null;
|
|
85
|
+
this._continuationToken = result.continuationToken;
|
|
79
86
|
this.items = [...this.items, ...result.items];
|
|
80
87
|
return result.items;
|
|
81
88
|
}
|
|
82
89
|
catch (error) {
|
|
83
90
|
console.error('CursorDataLoaderWithSearch: failed to load page', error);
|
|
91
|
+
if (generation === this._generation) {
|
|
92
|
+
this._failed = true;
|
|
93
|
+
}
|
|
84
94
|
return [];
|
|
85
95
|
}
|
|
86
96
|
};
|
|
87
|
-
isSearchStringEffective = (searchString) => searchString && searchString.length >= this.
|
|
97
|
+
isSearchStringEffective = (searchString) => searchString && searchString.length >= this._searchStringMinLength;
|
|
88
98
|
}
|
|
@@ -3,15 +3,17 @@ import type { IDataLoader } from './data-loader';
|
|
|
3
3
|
export declare class CursorDataLoader<T> implements IDataLoader<T> {
|
|
4
4
|
items: T[];
|
|
5
5
|
continuationToken: ContinuationToken;
|
|
6
|
-
private
|
|
7
|
-
private
|
|
8
|
-
private
|
|
6
|
+
private _loadPage;
|
|
7
|
+
private _pending;
|
|
8
|
+
private _generation;
|
|
9
9
|
private _loading;
|
|
10
|
+
private _failed;
|
|
10
11
|
constructor(init: {
|
|
11
12
|
loadPage: (continuationToken: ContinuationToken) => Promise<CursorResult<T> | null>;
|
|
12
13
|
});
|
|
13
14
|
get loading(): boolean;
|
|
14
15
|
get initialLoading(): boolean;
|
|
16
|
+
get failed(): boolean;
|
|
15
17
|
loadMore: () => Promise<T[]>;
|
|
16
18
|
reset(): Promise<void>;
|
|
17
19
|
private runLoad;
|
|
@@ -2,12 +2,13 @@ import { ContinuationToken } from '..';
|
|
|
2
2
|
export class CursorDataLoader {
|
|
3
3
|
items = $state.raw([]);
|
|
4
4
|
continuationToken = $state.raw(ContinuationToken.init());
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
_loadPage;
|
|
6
|
+
_pending = null;
|
|
7
|
+
_generation = 0;
|
|
8
8
|
_loading = $state(false);
|
|
9
|
+
_failed = $state(false);
|
|
9
10
|
constructor(init) {
|
|
10
|
-
this.
|
|
11
|
+
this._loadPage = init.loadPage;
|
|
11
12
|
}
|
|
12
13
|
get loading() {
|
|
13
14
|
return this._loading;
|
|
@@ -15,49 +16,58 @@ export class CursorDataLoader {
|
|
|
15
16
|
get initialLoading() {
|
|
16
17
|
return this._loading && this.items.length === 0;
|
|
17
18
|
}
|
|
19
|
+
get failed() {
|
|
20
|
+
return this._failed;
|
|
21
|
+
}
|
|
18
22
|
loadMore = async () => {
|
|
19
|
-
if (this.
|
|
20
|
-
return this.
|
|
23
|
+
if (this._pending) {
|
|
24
|
+
return this._pending;
|
|
21
25
|
}
|
|
22
26
|
if (!this.continuationToken.canLoadMore) {
|
|
23
27
|
return [];
|
|
24
28
|
}
|
|
25
29
|
this._loading = true;
|
|
26
30
|
const pending = this.runLoad();
|
|
27
|
-
this.
|
|
31
|
+
this._pending = pending;
|
|
28
32
|
try {
|
|
29
33
|
return await pending;
|
|
30
34
|
}
|
|
31
35
|
finally {
|
|
32
36
|
// a concurrent reset() may have installed a newer load — leave that one alone
|
|
33
|
-
if (this.
|
|
34
|
-
this.
|
|
37
|
+
if (this._pending === pending) {
|
|
38
|
+
this._pending = null;
|
|
35
39
|
this._loading = false;
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
};
|
|
39
43
|
async reset() {
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
44
|
+
this._generation++;
|
|
45
|
+
this._pending = null;
|
|
42
46
|
this._loading = false;
|
|
43
47
|
this.items = [];
|
|
44
48
|
this.continuationToken = ContinuationToken.init();
|
|
45
49
|
await this.loadMore();
|
|
46
50
|
}
|
|
47
51
|
runLoad = async () => {
|
|
48
|
-
const generation = this.
|
|
52
|
+
const generation = this._generation;
|
|
53
|
+
this._failed = false;
|
|
49
54
|
try {
|
|
50
|
-
const
|
|
55
|
+
const page = await this._loadPage(this.continuationToken);
|
|
51
56
|
// a reset() mid-load bumps the generation — drop the stale page instead of appending it to the cleared list
|
|
52
|
-
if (generation !== this.
|
|
57
|
+
if (generation !== this._generation) {
|
|
53
58
|
return [];
|
|
54
59
|
}
|
|
60
|
+
const result = page ?? { items: [], continuationToken: ContinuationToken.preventLoading() };
|
|
61
|
+
this._failed = page === null;
|
|
55
62
|
this.continuationToken = result.continuationToken;
|
|
56
63
|
this.items = [...this.items, ...result.items];
|
|
57
64
|
return result.items;
|
|
58
65
|
}
|
|
59
66
|
catch (error) {
|
|
60
67
|
console.error('CursorDataLoader: failed to load page', error);
|
|
68
|
+
if (generation === this._generation) {
|
|
69
|
+
this._failed = true;
|
|
70
|
+
}
|
|
61
71
|
return [];
|
|
62
72
|
}
|
|
63
73
|
};
|
|
@@ -3,5 +3,7 @@ export interface IDataLoader<T> {
|
|
|
3
3
|
loading: boolean;
|
|
4
4
|
/** Loading with nothing to show yet — first load, or a reload after the list was cleared by a reset. */
|
|
5
5
|
initialLoading: boolean;
|
|
6
|
+
/** The last attempt did not deliver a page, whether it returned nothing or threw; cleared when the next attempt starts. */
|
|
7
|
+
failed: boolean;
|
|
6
8
|
loadMore: () => Promise<T[]>;
|
|
7
9
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type { IDataLoader } from './data-loader';
|
|
2
2
|
export { CursorDataLoader } from './cursor-data-loader.svelte';
|
|
3
3
|
export { CursorDataLoaderWithSearch } from './cursor-data-loader-with-search.svelte';
|
|
4
|
+
export { KeyedCursorDataLoader } from './keyed-cursor-data-loader.svelte';
|
|
4
5
|
export { PageDataLoader } from './page-data-loader.svelte';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Data loaders
|
|
2
2
|
export { CursorDataLoader } from './cursor-data-loader.svelte';
|
|
3
3
|
export { CursorDataLoaderWithSearch } from './cursor-data-loader-with-search.svelte';
|
|
4
|
+
export { KeyedCursorDataLoader } from './keyed-cursor-data-loader.svelte';
|
|
4
5
|
export { PageDataLoader } from './page-data-loader.svelte';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ContinuationToken, type CursorResult } from '..';
|
|
2
|
+
export declare class KeyedCursorDataLoader<T> {
|
|
3
|
+
private _loaders;
|
|
4
|
+
private _loaded;
|
|
5
|
+
private _pending;
|
|
6
|
+
private _loadPage;
|
|
7
|
+
private _loadAlongside;
|
|
8
|
+
constructor(init: {
|
|
9
|
+
loadPage: (key: string, continuationToken: ContinuationToken) => Promise<CursorResult<T> | null>;
|
|
10
|
+
/** Started with the key's first page and awaited by `ensureLoaded`; its outcome never reaches `isLoaded`. */
|
|
11
|
+
loadAlongside?: (key: string) => Promise<void>;
|
|
12
|
+
});
|
|
13
|
+
items: (key?: string) => T[];
|
|
14
|
+
loading: (key?: string) => boolean;
|
|
15
|
+
initialLoading: (key?: string) => boolean;
|
|
16
|
+
canLoadMore: (key?: string) => boolean;
|
|
17
|
+
/** False until the key's first page loaded without failing, so a failed load can be retried; a later failed page never clears it. */
|
|
18
|
+
isLoaded: (key?: string) => boolean;
|
|
19
|
+
/** Loads the key's first page once; concurrent and repeat calls share that load's promise. */
|
|
20
|
+
ensureLoaded: (key?: string) => Promise<void>;
|
|
21
|
+
loadMore: (key?: string) => Promise<T[]>;
|
|
22
|
+
/** Drops the key's pages and loads them again from the first one. */
|
|
23
|
+
reload: (key?: string) => Promise<void>;
|
|
24
|
+
private startLoad;
|
|
25
|
+
private runLoad;
|
|
26
|
+
private runAlongside;
|
|
27
|
+
private loaderFor;
|
|
28
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ContinuationToken } from '..';
|
|
2
|
+
import { CursorDataLoader } from './cursor-data-loader.svelte';
|
|
3
|
+
export class KeyedCursorDataLoader {
|
|
4
|
+
_loaders = {};
|
|
5
|
+
_loaded = $state.raw({});
|
|
6
|
+
_pending = {};
|
|
7
|
+
_loadPage;
|
|
8
|
+
_loadAlongside;
|
|
9
|
+
constructor(init) {
|
|
10
|
+
this._loadPage = init.loadPage;
|
|
11
|
+
this._loadAlongside = init.loadAlongside;
|
|
12
|
+
}
|
|
13
|
+
items = (key = '') => this.loaderFor(key).items;
|
|
14
|
+
loading = (key = '') => this.loaderFor(key).loading;
|
|
15
|
+
initialLoading = (key = '') => this.loaderFor(key).initialLoading;
|
|
16
|
+
canLoadMore = (key = '') => this.loaderFor(key).continuationToken.canLoadMore;
|
|
17
|
+
/** False until the key's first page loaded without failing, so a failed load can be retried; a later failed page never clears it. */
|
|
18
|
+
isLoaded = (key = '') => this._loaded[key] === true;
|
|
19
|
+
/** Loads the key's first page once; concurrent and repeat calls share that load's promise. */
|
|
20
|
+
ensureLoaded = (key = '') => {
|
|
21
|
+
if (this._loaded[key]) {
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
}
|
|
24
|
+
return this._pending[key] ?? this.startLoad(key);
|
|
25
|
+
};
|
|
26
|
+
loadMore = (key = '') => this.loaderFor(key).loadMore();
|
|
27
|
+
/** Drops the key's pages and loads them again from the first one. */
|
|
28
|
+
reload = (key = '') => {
|
|
29
|
+
this._loaded = { ...this._loaded, [key]: false };
|
|
30
|
+
return this.startLoad(key);
|
|
31
|
+
};
|
|
32
|
+
startLoad = async (key) => {
|
|
33
|
+
const pending = this.runLoad(key);
|
|
34
|
+
this._pending[key] = pending;
|
|
35
|
+
try {
|
|
36
|
+
await pending;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
// a concurrent reload() may have installed a newer load — leave that one alone
|
|
40
|
+
if (this._pending[key] === pending) {
|
|
41
|
+
this._pending[key] = undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
runLoad = async (key) => {
|
|
46
|
+
const loader = this.loaderFor(key);
|
|
47
|
+
const alongside = this.runAlongside(key);
|
|
48
|
+
await loader.reset();
|
|
49
|
+
this._loaded = { ...this._loaded, [key]: !loader.failed };
|
|
50
|
+
await alongside;
|
|
51
|
+
};
|
|
52
|
+
runAlongside = async (key) => {
|
|
53
|
+
try {
|
|
54
|
+
await this._loadAlongside?.(key);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('KeyedCursorDataLoader: failed to load alongside the page', error);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
loaderFor = (key) => {
|
|
61
|
+
if (!this._loaders[key]) {
|
|
62
|
+
this._loaders[key] = new CursorDataLoader({ loadPage: (continuationToken) => this._loadPage(key, continuationToken) });
|
|
63
|
+
}
|
|
64
|
+
return this._loaders[key];
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -2,11 +2,12 @@ import type { IDataLoader } from './data-loader';
|
|
|
2
2
|
export declare class PageDataLoader<T> implements IDataLoader<T> {
|
|
3
3
|
private _items;
|
|
4
4
|
private _loading;
|
|
5
|
-
private
|
|
6
|
-
private
|
|
7
|
-
private
|
|
8
|
-
private
|
|
9
|
-
private
|
|
5
|
+
private _failed;
|
|
6
|
+
private _canLoadMore;
|
|
7
|
+
private _perPage;
|
|
8
|
+
private _loadPage;
|
|
9
|
+
private _pageNumber;
|
|
10
|
+
private _generation;
|
|
10
11
|
constructor(init: {
|
|
11
12
|
loadPage: (page: number, perPage: number) => Promise<T[]>;
|
|
12
13
|
perPage?: number;
|
|
@@ -14,6 +15,7 @@ export declare class PageDataLoader<T> implements IDataLoader<T> {
|
|
|
14
15
|
get items(): T[];
|
|
15
16
|
get loading(): boolean;
|
|
16
17
|
get initialLoading(): boolean;
|
|
18
|
+
get failed(): boolean;
|
|
17
19
|
loadMore: () => Promise<T[]>;
|
|
18
20
|
reset: () => Promise<void>;
|
|
19
21
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
export class PageDataLoader {
|
|
2
2
|
_items = $state.raw([]);
|
|
3
3
|
_loading = $state(false);
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
_failed = $state(false);
|
|
5
|
+
_canLoadMore = $state(true);
|
|
6
|
+
_perPage = 20;
|
|
7
|
+
_loadPage;
|
|
8
|
+
_pageNumber = 1;
|
|
9
|
+
_generation = 0;
|
|
9
10
|
constructor(init) {
|
|
10
|
-
this.
|
|
11
|
+
this._loadPage = init.loadPage;
|
|
11
12
|
if (init.perPage) {
|
|
12
|
-
this.
|
|
13
|
+
this._perPage = init.perPage;
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
16
|
get items() {
|
|
@@ -21,39 +22,46 @@ export class PageDataLoader {
|
|
|
21
22
|
get initialLoading() {
|
|
22
23
|
return this._loading && this._items.length === 0;
|
|
23
24
|
}
|
|
25
|
+
get failed() {
|
|
26
|
+
return this._failed;
|
|
27
|
+
}
|
|
24
28
|
loadMore = async () => {
|
|
25
|
-
if (this._loading || !this.
|
|
29
|
+
if (this._loading || !this._canLoadMore) {
|
|
26
30
|
return [];
|
|
27
31
|
}
|
|
28
|
-
const generation = this.
|
|
32
|
+
const generation = this._generation;
|
|
29
33
|
this._loading = true;
|
|
34
|
+
this._failed = false;
|
|
30
35
|
try {
|
|
31
|
-
const items = await this.
|
|
36
|
+
const items = await this._loadPage(this._pageNumber, this._perPage);
|
|
32
37
|
// a reset() mid-load bumps the generation — drop the stale page instead of appending it to the cleared list
|
|
33
|
-
if (generation !== this.
|
|
38
|
+
if (generation !== this._generation) {
|
|
34
39
|
return [];
|
|
35
40
|
}
|
|
36
|
-
this.
|
|
37
|
-
this.
|
|
41
|
+
this._pageNumber++;
|
|
42
|
+
this._canLoadMore = items.length === this._perPage;
|
|
38
43
|
this._items = [...this._items, ...items];
|
|
39
44
|
return items;
|
|
40
45
|
}
|
|
41
46
|
catch (error) {
|
|
42
47
|
console.error('PageDataLoader: failed to load page', error);
|
|
48
|
+
if (generation === this._generation) {
|
|
49
|
+
this._failed = true;
|
|
50
|
+
}
|
|
43
51
|
return [];
|
|
44
52
|
}
|
|
45
53
|
finally {
|
|
46
|
-
if (generation === this.
|
|
54
|
+
if (generation === this._generation) {
|
|
47
55
|
this._loading = false;
|
|
48
56
|
}
|
|
49
57
|
}
|
|
50
58
|
};
|
|
51
59
|
reset = async () => {
|
|
52
|
-
this.
|
|
60
|
+
this._generation++;
|
|
53
61
|
this._loading = false;
|
|
54
|
-
this.
|
|
62
|
+
this._pageNumber = 1;
|
|
55
63
|
this._items = [];
|
|
56
|
-
this.
|
|
64
|
+
this._canLoadMore = true;
|
|
57
65
|
await this.loadMore();
|
|
58
66
|
};
|
|
59
67
|
}
|
|
@@ -5,11 +5,19 @@ let { actions, contextActions = [], contextPosition = 'left-end' } = $props();
|
|
|
5
5
|
|
|
6
6
|
<div class="mobile-player-buttons">
|
|
7
7
|
{#each actions as action, index (index)}
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
<
|
|
8
|
+
{#if action.on}
|
|
9
|
+
<button type="button" class="mobile-player-buttons__action" disabled={action.disabled} aria-label={action.label || undefined} onclick={action.on.click}>
|
|
10
|
+
<span class="mobile-player-buttons__action-icon">
|
|
11
|
+
<IconSlot icon={action.icon} />
|
|
12
|
+
</span>
|
|
13
|
+
</button>
|
|
14
|
+
{:else}
|
|
15
|
+
<span class="mobile-player-buttons__action mobile-player-buttons__action--static" role="img" aria-label={action.label || undefined}>
|
|
16
|
+
<span class="mobile-player-buttons__action-icon">
|
|
17
|
+
<IconSlot icon={action.icon} />
|
|
18
|
+
</span>
|
|
11
19
|
</span>
|
|
12
|
-
|
|
20
|
+
{/if}
|
|
13
21
|
{/each}
|
|
14
22
|
{#if contextActions.length}
|
|
15
23
|
<div class="mobile-player-buttons__action mobile-player-buttons__action--menu">
|
|
@@ -21,7 +29,9 @@ let { actions, contextActions = [], contextPosition = 'left-end' } = $props();
|
|
|
21
29
|
<!--
|
|
22
30
|
@component
|
|
23
31
|
A vertically stacked column of player action buttons optimized for mobile touch targets.
|
|
24
|
-
`contextActions` appends an overflow-menu cell below the buttons.
|
|
32
|
+
`contextActions` appends an overflow-menu cell below the buttons. An action without `on` renders as a static
|
|
33
|
+
plaque — same cell, no pointer feedback, out of the tab order — while `disabled` keeps dimming an action that
|
|
34
|
+
exists but is currently unavailable.
|
|
25
35
|
|
|
26
36
|
### CSS Custom Properties
|
|
27
37
|
| Property | Description | Default |
|
|
@@ -37,7 +47,6 @@ A vertically stacked column of player action buttons optimized for mobile touch
|
|
|
37
47
|
--_player-button--menu--border-color: var(--sc-kit--player-button--menu--border-color, rgb(255 255 255 / 15%));
|
|
38
48
|
--_player-button--menu--item-color: var(--sc-kit--player-button--menu--item--color, var(--sc-kit--color--text--on-accent));
|
|
39
49
|
--_player-button--menu--item-background-hover: var(--sc-kit--player-button--menu--item--background--hover, rgb(255 255 255 / 12%));
|
|
40
|
-
cursor: pointer;
|
|
41
50
|
display: flex;
|
|
42
51
|
flex-direction: column;
|
|
43
52
|
justify-content: center;
|
|
@@ -48,6 +57,7 @@ A vertically stacked column of player action buttons optimized for mobile touch
|
|
|
48
57
|
display: flex;
|
|
49
58
|
justify-content: center;
|
|
50
59
|
align-items: center;
|
|
60
|
+
cursor: pointer;
|
|
51
61
|
padding: var(--_player-button--cell-padding);
|
|
52
62
|
--sc-kit--icon--color: var(--sc-kit--color--text--on-accent);
|
|
53
63
|
--sc-kit--icon--size: 2rem;
|
|
@@ -57,6 +67,9 @@ A vertically stacked column of player action buttons optimized for mobile touch
|
|
|
57
67
|
opacity: 0.5;
|
|
58
68
|
cursor: default;
|
|
59
69
|
}
|
|
70
|
+
.mobile-player-buttons__action--static {
|
|
71
|
+
cursor: default;
|
|
72
|
+
}
|
|
60
73
|
.mobile-player-buttons__action--menu {
|
|
61
74
|
padding: 0;
|
|
62
75
|
min-block-size: calc(var(--sc-kit--icon--size) + 2 * var(--_player-button--cell-padding));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PlayerButtonDef, PlayerContextActionDef } from './types';
|
|
2
2
|
import type { Placement } from '@floating-ui/dom';
|
|
3
3
|
type Props = {
|
|
4
|
+
/** Button definitions — one without `on` renders as a static plaque instead of a button. */
|
|
4
5
|
actions: PlayerButtonDef[];
|
|
5
6
|
/** Rows of an overflow menu rendered as the final cell — an ellipsis trigger opening a popover. Empty ⇒ no menu cell. @default [] */
|
|
6
7
|
contextActions?: PlayerContextActionDef[];
|
|
@@ -9,7 +10,9 @@ type Props = {
|
|
|
9
10
|
};
|
|
10
11
|
/**
|
|
11
12
|
* A vertically stacked column of player action buttons optimized for mobile touch targets.
|
|
12
|
-
* `contextActions` appends an overflow-menu cell below the buttons.
|
|
13
|
+
* `contextActions` appends an overflow-menu cell below the buttons. An action without `on` renders as a static
|
|
14
|
+
* plaque — same cell, no pointer feedback, out of the tab order — while `disabled` keeps dimming an action that
|
|
15
|
+
* exists but is currently unavailable.
|
|
13
16
|
*
|
|
14
17
|
* ### CSS Custom Properties
|
|
15
18
|
* | Property | Description | Default |
|
|
@@ -5,38 +5,63 @@ const hasMenu = $derived(contextActions.length > 0);
|
|
|
5
5
|
const cellCount = $derived(actions.length + (hasMenu ? 1 : 0));
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
|
-
{#
|
|
9
|
-
{#if
|
|
10
|
-
<div class="player-button player-button--menu" style:zoom={zoom}>
|
|
11
|
-
<PlayerContextMenu actions={contextActions} position={contextPosition} />
|
|
12
|
-
</div>
|
|
13
|
-
{:else}
|
|
14
|
-
{@const action = actions[0]}
|
|
8
|
+
{#snippet soloCell(action: PlayerButtonDef)}
|
|
9
|
+
{#if action.on}
|
|
15
10
|
<button
|
|
16
11
|
type="button"
|
|
17
12
|
class="player-button"
|
|
18
13
|
style:zoom={zoom}
|
|
19
14
|
class:player-button--scale-effect={scaleEffect}
|
|
20
15
|
disabled={action.disabled}
|
|
21
|
-
|
|
16
|
+
aria-label={action.label || undefined}
|
|
17
|
+
onclick={action.on.click}>
|
|
22
18
|
<span class="player-button__icon">
|
|
23
19
|
<IconSlot icon={action.icon} />
|
|
24
20
|
</span>
|
|
25
21
|
</button>
|
|
22
|
+
{:else}
|
|
23
|
+
<span class="player-button player-button--static" style:zoom={zoom} role="img" aria-label={action.label || undefined}>
|
|
24
|
+
<span class="player-button__icon">
|
|
25
|
+
<IconSlot icon={action.icon} />
|
|
26
|
+
</span>
|
|
27
|
+
</span>
|
|
28
|
+
{/if}
|
|
29
|
+
{/snippet}
|
|
30
|
+
|
|
31
|
+
{#snippet pillCell(action: PlayerButtonDef)}
|
|
32
|
+
{#if action.on}
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
class="player-buttons__action"
|
|
36
|
+
class:player-buttons__action--scale-effect={scaleEffect}
|
|
37
|
+
disabled={action.disabled}
|
|
38
|
+
aria-label={action.label || undefined}
|
|
39
|
+
onclick={action.on.click}>
|
|
40
|
+
<span class="player-buttons__action-icon">
|
|
41
|
+
<IconSlot icon={action.icon} />
|
|
42
|
+
</span>
|
|
43
|
+
</button>
|
|
44
|
+
{:else}
|
|
45
|
+
<span class="player-buttons__action player-buttons__action--static" role="img" aria-label={action.label || undefined}>
|
|
46
|
+
<span class="player-buttons__action-icon">
|
|
47
|
+
<IconSlot icon={action.icon} />
|
|
48
|
+
</span>
|
|
49
|
+
</span>
|
|
50
|
+
{/if}
|
|
51
|
+
{/snippet}
|
|
52
|
+
|
|
53
|
+
{#if cellCount === 1}
|
|
54
|
+
{#if hasMenu}
|
|
55
|
+
<div class="player-button player-button--menu" style:zoom={zoom}>
|
|
56
|
+
<PlayerContextMenu actions={contextActions} position={contextPosition} />
|
|
57
|
+
</div>
|
|
58
|
+
{:else}
|
|
59
|
+
{@render soloCell(actions[0])}
|
|
26
60
|
{/if}
|
|
27
61
|
{:else if cellCount > 1}
|
|
28
62
|
<div class="player-buttons" style:zoom={zoom}>
|
|
29
63
|
{#each actions as action, index (index)}
|
|
30
|
-
|
|
31
|
-
type="button"
|
|
32
|
-
class="player-buttons__action"
|
|
33
|
-
class:player-buttons__action--scale-effect={scaleEffect}
|
|
34
|
-
disabled={action.disabled}
|
|
35
|
-
onclick={action.callback}>
|
|
36
|
-
<span class="player-buttons__action-icon">
|
|
37
|
-
<IconSlot icon={action.icon} />
|
|
38
|
-
</span>
|
|
39
|
-
</button>
|
|
64
|
+
{@render pillCell(action)}
|
|
40
65
|
{/each}
|
|
41
66
|
{#if hasMenu}
|
|
42
67
|
<div class="player-buttons__action player-buttons__action--menu">
|
|
@@ -50,7 +75,9 @@ const cellCount = $derived(actions.length + (hasMenu ? 1 : 0));
|
|
|
50
75
|
@component
|
|
51
76
|
Desktop player action buttons — renders a single circular button or a vertically stacked pill for multiple actions.
|
|
52
77
|
`contextActions` adds an overflow-menu cell as the last item; it counts towards the single / pill split, so one
|
|
53
|
-
action plus a menu renders as a two-cell pill.
|
|
78
|
+
action plus a menu renders as a two-cell pill. An action without `on` renders as a static plaque — same cell
|
|
79
|
+
chrome, no hover, no scale, out of the tab order — while `disabled` keeps dimming an action that exists but is
|
|
80
|
+
currently unavailable.
|
|
54
81
|
|
|
55
82
|
### CSS Custom Properties
|
|
56
83
|
| Property | Description | Default |
|
|
@@ -70,6 +97,7 @@ action plus a menu renders as a two-cell pill.
|
|
|
70
97
|
--_player-button--menu--item-background-hover: var(--sc-kit--player-button--menu--item--background--hover, rgb(255 255 255 / 12%));
|
|
71
98
|
--_player-button--icon-scale: 1;
|
|
72
99
|
--_player-button--cell-padding: 0.625rem;
|
|
100
|
+
--_player-button--background--hover: var(--_player-button--color);
|
|
73
101
|
pointer-events: auto;
|
|
74
102
|
padding: var(--_player-button--cell-padding);
|
|
75
103
|
display: flex;
|
|
@@ -83,16 +111,21 @@ action plus a menu renders as a two-cell pill.
|
|
|
83
111
|
--sc-kit--icon--size: 1.75rem;
|
|
84
112
|
}
|
|
85
113
|
.player-button:hover:not(:disabled) {
|
|
86
|
-
background-color: var(--_player-button--
|
|
114
|
+
background-color: var(--_player-button--background--hover);
|
|
87
115
|
}
|
|
88
116
|
.player-button:disabled {
|
|
89
117
|
opacity: 0.5;
|
|
90
118
|
cursor: default;
|
|
91
119
|
}
|
|
120
|
+
.player-button--static {
|
|
121
|
+
--_player-button--background--hover: var(--_player-button--color--inactive);
|
|
122
|
+
cursor: default;
|
|
123
|
+
}
|
|
92
124
|
.player-button--scale-effect:hover:not(:disabled) {
|
|
93
125
|
--_player-button--icon-scale: 1.2;
|
|
94
126
|
}
|
|
95
127
|
.player-button--menu {
|
|
128
|
+
--_player-button--background--hover: var(--_player-button--color--inactive);
|
|
96
129
|
padding: 0;
|
|
97
130
|
min-block-size: calc(var(--sc-kit--icon--size) + 2 * var(--_player-button--cell-padding));
|
|
98
131
|
min-inline-size: calc(var(--sc-kit--icon--size) + 2 * var(--_player-button--cell-padding));
|
|
@@ -101,9 +134,6 @@ action plus a menu renders as a two-cell pill.
|
|
|
101
134
|
--sc-kit--popover-item--color: var(--_player-button--menu--item-color);
|
|
102
135
|
--sc-kit--popover-item--background--hover: var(--_player-button--menu--item-background-hover);
|
|
103
136
|
}
|
|
104
|
-
.player-button--menu:hover:not(:disabled) {
|
|
105
|
-
background-color: var(--_player-button--color--inactive);
|
|
106
|
-
}
|
|
107
137
|
.player-button__icon {
|
|
108
138
|
display: block;
|
|
109
139
|
transform: scale(var(--_player-button--icon-scale));
|
|
@@ -125,7 +155,6 @@ action plus a menu renders as a two-cell pill.
|
|
|
125
155
|
--_player-button--menu--border-color: var(--sc-kit--player-button--menu--border-color, rgb(255 255 255 / 15%));
|
|
126
156
|
--_player-button--menu--item-color: var(--sc-kit--player-button--menu--item--color, var(--sc-kit--color--text--on-accent));
|
|
127
157
|
--_player-button--menu--item-background-hover: var(--sc-kit--player-button--menu--item--background--hover, rgb(255 255 255 / 12%));
|
|
128
|
-
cursor: pointer;
|
|
129
158
|
display: flex;
|
|
130
159
|
flex-direction: column;
|
|
131
160
|
justify-content: center;
|
|
@@ -137,9 +166,11 @@ action plus a menu renders as a two-cell pill.
|
|
|
137
166
|
.player-buttons__action {
|
|
138
167
|
--_player-button--icon-scale: 1;
|
|
139
168
|
--_player-button--cell-padding: 0.5625rem;
|
|
169
|
+
--_player-button--background--hover: var(--_player-button--color);
|
|
140
170
|
display: flex;
|
|
141
171
|
justify-content: center;
|
|
142
172
|
align-items: center;
|
|
173
|
+
cursor: pointer;
|
|
143
174
|
padding: var(--_player-button--cell-padding);
|
|
144
175
|
border-radius: 1.25rem;
|
|
145
176
|
color: var(--sc-kit--color--text--on-accent);
|
|
@@ -148,17 +179,24 @@ action plus a menu renders as a two-cell pill.
|
|
|
148
179
|
transition: background-color 0.5s;
|
|
149
180
|
}
|
|
150
181
|
.player-buttons__action:hover:not(:disabled) {
|
|
151
|
-
background-color: var(--_player-button--
|
|
182
|
+
background-color: var(--_player-button--background--hover);
|
|
152
183
|
}
|
|
153
184
|
.player-buttons__action:disabled {
|
|
154
185
|
opacity: 0.5;
|
|
155
186
|
cursor: default;
|
|
156
187
|
}
|
|
188
|
+
.player-buttons__action--static {
|
|
189
|
+
--_player-button--background--hover: transparent;
|
|
190
|
+
cursor: default;
|
|
191
|
+
}
|
|
192
|
+
.player-buttons__action--scale-effect {
|
|
193
|
+
--_player-button--background--hover: transparent;
|
|
194
|
+
}
|
|
157
195
|
.player-buttons__action--scale-effect:hover:not(:disabled) {
|
|
158
196
|
--_player-button--icon-scale: 1.2;
|
|
159
|
-
background-color: transparent;
|
|
160
197
|
}
|
|
161
198
|
.player-buttons__action--menu {
|
|
199
|
+
--_player-button--background--hover: transparent;
|
|
162
200
|
padding: 0;
|
|
163
201
|
min-block-size: calc(var(--sc-kit--icon--size) + 2 * var(--_player-button--cell-padding));
|
|
164
202
|
min-inline-size: calc(var(--sc-kit--icon--size) + 2 * var(--_player-button--cell-padding));
|
|
@@ -167,9 +205,6 @@ action plus a menu renders as a two-cell pill.
|
|
|
167
205
|
--sc-kit--popover-item--color: var(--_player-button--menu--item-color);
|
|
168
206
|
--sc-kit--popover-item--background--hover: var(--_player-button--menu--item-background-hover);
|
|
169
207
|
}
|
|
170
|
-
.player-buttons__action--menu:hover:not(:disabled) {
|
|
171
|
-
background-color: transparent;
|
|
172
|
-
}
|
|
173
208
|
.player-buttons__action-icon {
|
|
174
209
|
display: block;
|
|
175
210
|
transform: scale(var(--_player-button--icon-scale));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PlayerButtonDef, PlayerContextActionDef } from './types';
|
|
2
2
|
import type { Placement } from '@floating-ui/dom';
|
|
3
3
|
type Props = {
|
|
4
|
-
/**
|
|
4
|
+
/** Button definitions — one without `on` renders as a static plaque instead of a button. */
|
|
5
5
|
actions: PlayerButtonDef[];
|
|
6
6
|
/** Rows of an overflow menu rendered as the final cell — an ellipsis trigger opening a popover. Empty ⇒ no menu cell. @default [] */
|
|
7
7
|
contextActions?: PlayerContextActionDef[];
|
|
@@ -15,7 +15,9 @@ type Props = {
|
|
|
15
15
|
/**
|
|
16
16
|
* Desktop player action buttons — renders a single circular button or a vertically stacked pill for multiple actions.
|
|
17
17
|
* `contextActions` adds an overflow-menu cell as the last item; it counts towards the single / pill split, so one
|
|
18
|
-
* action plus a menu renders as a two-cell pill.
|
|
18
|
+
* action plus a menu renders as a two-cell pill. An action without `on` renders as a static plaque — same cell
|
|
19
|
+
* chrome, no hover, no scale, out of the tab order — while `disabled` keeps dimming an action that exists but is
|
|
20
|
+
* currently unavailable.
|
|
19
21
|
*
|
|
20
22
|
* ### CSS Custom Properties
|
|
21
23
|
* | Property | Description | Default |
|
|
@@ -13,7 +13,7 @@ const localization = new PlayerButtonsLocalization();
|
|
|
13
13
|
{/snippet}
|
|
14
14
|
|
|
15
15
|
{#each actions as action (action.label)}
|
|
16
|
-
<PopoverItem disabled={action.disabled} on={{ click: action.
|
|
16
|
+
<PopoverItem disabled={action.disabled} on={{ click: action.on?.click }}>
|
|
17
17
|
<IconText icon={action.icon} size="sm">{action.label}</IconText>
|
|
18
18
|
</PopoverItem>
|
|
19
19
|
{/each}
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import type { IconProp } from '../../icon';
|
|
2
2
|
export type PlayerButtonDef = {
|
|
3
3
|
icon: IconProp;
|
|
4
|
-
|
|
4
|
+
/** Accessible name — rendered as `aria-label`, never as visible text. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Omit for a control with no action: it renders as a static plaque with the button's chrome and no pointer feedback, rather than a dimmed button. */
|
|
7
|
+
on?: {
|
|
8
|
+
click: () => void;
|
|
9
|
+
};
|
|
5
10
|
disabled?: boolean;
|
|
6
11
|
};
|
|
7
12
|
export type PlayerContextActionDef = {
|
|
8
13
|
icon: IconProp;
|
|
9
14
|
label: string;
|
|
10
|
-
|
|
15
|
+
/** Omit for a row with no action: it renders as static text that does not highlight and does not dismiss the menu. */
|
|
16
|
+
on?: {
|
|
17
|
+
click: () => void;
|
|
18
|
+
};
|
|
11
19
|
disabled?: boolean;
|
|
12
20
|
};
|
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
<script lang="ts">import { popoverIgnore } from './popover-ignore';
|
|
2
2
|
const { disabled = false, keepOpen = false, divider = false, inset = true, on, children } = $props();
|
|
3
|
+
const isStatic = $derived(!on?.click);
|
|
4
|
+
const interactive = $derived(!disabled && !isStatic);
|
|
3
5
|
const handleClick = () => {
|
|
4
|
-
if (
|
|
6
|
+
if (!interactive) {
|
|
5
7
|
return;
|
|
6
8
|
}
|
|
7
9
|
on?.click?.();
|
|
8
10
|
};
|
|
11
|
+
const handleKeydown = (event) => {
|
|
12
|
+
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
event.preventDefault();
|
|
16
|
+
event.currentTarget.click();
|
|
17
|
+
};
|
|
9
18
|
</script>
|
|
10
19
|
|
|
11
20
|
<div
|
|
12
21
|
class="popover-item"
|
|
13
22
|
class:popover-item--disabled={disabled}
|
|
23
|
+
class:popover-item--static={isStatic}
|
|
14
24
|
class:popover-item--divider={divider}
|
|
15
25
|
class:popover-item--inset={inset}
|
|
16
|
-
use:popoverIgnore={keepOpen}
|
|
26
|
+
use:popoverIgnore={keepOpen || isStatic}
|
|
17
27
|
role="menuitem"
|
|
18
|
-
tabindex={
|
|
28
|
+
tabindex={interactive ? 0 : -1}
|
|
19
29
|
onclick={handleClick}
|
|
20
|
-
onkeydown={
|
|
30
|
+
onkeydown={handleKeydown}>
|
|
21
31
|
<div class="popover-item__content">
|
|
22
32
|
{@render children()}
|
|
23
33
|
</div>
|
|
@@ -27,7 +37,9 @@ const handleClick = () => {
|
|
|
27
37
|
@component
|
|
28
38
|
PopoverItem — single row inside a Popover's content. Hover / disabled / opt-in divider
|
|
29
39
|
variants. Set `keepOpen` to prevent the parent popover from closing when clicked (useful
|
|
30
|
-
for toggle items inside menus).
|
|
40
|
+
for toggle items inside menus). A row given no `on` is static — full text color, no hover,
|
|
41
|
+
no focus, and a click neither fires nor dismisses the popover; `disabled` stays for a row
|
|
42
|
+
whose action exists but is currently unavailable.
|
|
31
43
|
|
|
32
44
|
### CSS Custom Properties
|
|
33
45
|
| Property | Description | Default |
|
|
@@ -70,7 +82,7 @@ for toggle items inside menus).
|
|
|
70
82
|
cursor: pointer;
|
|
71
83
|
transition: background-color var(--sc-kit--duration--base) var(--sc-kit--ease--default), color var(--sc-kit--duration--base) var(--sc-kit--ease--default);
|
|
72
84
|
}
|
|
73
|
-
.popover-item:hover:not(.popover-item--disabled) {
|
|
85
|
+
.popover-item:hover:not(.popover-item--disabled):not(.popover-item--static) {
|
|
74
86
|
background: var(--_di--background-hover);
|
|
75
87
|
}
|
|
76
88
|
.popover-item:focus-visible {
|
|
@@ -81,6 +93,9 @@ for toggle items inside menus).
|
|
|
81
93
|
color: var(--_di--color-disabled);
|
|
82
94
|
cursor: default;
|
|
83
95
|
}
|
|
96
|
+
.popover-item--static {
|
|
97
|
+
cursor: default;
|
|
98
|
+
}
|
|
84
99
|
.popover-item--divider {
|
|
85
100
|
box-shadow: 0 1px 0 0 var(--_di--divider-color);
|
|
86
101
|
margin-bottom: var(--_di--divider-spacing);
|
|
@@ -10,6 +10,7 @@ type Props = {
|
|
|
10
10
|
* @default true
|
|
11
11
|
*/
|
|
12
12
|
inset?: boolean;
|
|
13
|
+
/** Omit for a row with no action: it renders as static text that does not highlight, take focus, or dismiss the popover. */
|
|
13
14
|
on?: {
|
|
14
15
|
click?: () => void;
|
|
15
16
|
};
|
|
@@ -18,7 +19,9 @@ type Props = {
|
|
|
18
19
|
/**
|
|
19
20
|
* PopoverItem — single row inside a Popover's content. Hover / disabled / opt-in divider
|
|
20
21
|
* variants. Set `keepOpen` to prevent the parent popover from closing when clicked (useful
|
|
21
|
-
* for toggle items inside menus).
|
|
22
|
+
* for toggle items inside menus). A row given no `on` is static — full text color, no hover,
|
|
23
|
+
* no focus, and a click neither fires nor dismisses the popover; `disabled` stays for a row
|
|
24
|
+
* whose action exists but is currently unavailable.
|
|
22
25
|
*
|
|
23
26
|
* ### CSS Custom Properties
|
|
24
27
|
* | Property | Description | Default |
|