@streamscloud/kit 0.9.12 → 0.9.14
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 +3 -1
- package/dist/core/data-loaders/cursor-data-loader-with-search.svelte.js +37 -16
- package/dist/core/data-loaders/cursor-data-loader.svelte.d.ts +3 -1
- package/dist/core/data-loaders/cursor-data-loader.svelte.js +34 -13
- package/dist/core/files/file-types.d.ts +4 -0
- package/dist/core/files/file-types.js +24 -9
- package/dist/core/files/file-validation-rules.js +2 -30
- package/dist/core/utils/number-helper.js +1 -1
- package/dist/ui/collection-list/cmp.collection-list.svelte +242 -0
- package/dist/ui/collection-list/cmp.collection-list.svelte.d.ts +64 -0
- package/dist/ui/collection-list/collection-list-localization.d.ts +4 -0
- package/dist/ui/collection-list/collection-list-localization.js +19 -0
- package/dist/ui/collection-list/index.d.ts +2 -0
- package/dist/ui/collection-list/index.js +2 -0
- package/dist/ui/collection-list/types.d.ts +28 -0
- package/dist/ui/collection-list/types.js +20 -0
- package/dist/ui/duration/cmp.duration.svelte +49 -0
- package/dist/ui/duration/cmp.duration.svelte.d.ts +27 -0
- package/dist/ui/duration/index.d.ts +1 -0
- package/dist/ui/duration/index.js +1 -0
- package/dist/ui/select/multiselect-base.svelte +1 -0
- package/dist/ui/select/select-core.svelte.d.ts +2 -0
- package/dist/ui/select/select-core.svelte.js +6 -2
- package/dist/ui/select/select-listbox.svelte +31 -16
- package/dist/ui/select/select-listbox.svelte.d.ts +1 -1
- package/dist/ui/stepper-dialog-layout/cmp.stepper-dialog-layout.svelte +1 -1
- package/dist/ui/tooltip/cmp.tooltip.svelte +7 -0
- package/dist/ui/video/cmp.video.svelte +17 -7
- package/package.json +13 -1
|
@@ -5,7 +5,8 @@ export declare class CursorDataLoaderWithSearch<T> implements IDataLoader<T> {
|
|
|
5
5
|
private continuationToken;
|
|
6
6
|
private _searchString;
|
|
7
7
|
private loadPage;
|
|
8
|
-
private
|
|
8
|
+
private pending;
|
|
9
|
+
private generation;
|
|
9
10
|
private searchStringMinLength;
|
|
10
11
|
constructor(init: {
|
|
11
12
|
loadPage: (continuationToken: ContinuationToken, searchString: string) => Promise<CursorResult<T> | null>;
|
|
@@ -15,5 +16,6 @@ export declare class CursorDataLoaderWithSearch<T> implements IDataLoader<T> {
|
|
|
15
16
|
loadMore: () => Promise<T[]>;
|
|
16
17
|
reset(): Promise<void>;
|
|
17
18
|
updateSearchString: (searchString: string | null) => void;
|
|
19
|
+
private runLoad;
|
|
18
20
|
private isSearchStringEffective;
|
|
19
21
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { ContinuationToken
|
|
1
|
+
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
7
|
loadPage;
|
|
8
|
-
|
|
8
|
+
pending = null;
|
|
9
|
+
generation = 0;
|
|
9
10
|
searchStringMinLength = 1;
|
|
10
11
|
constructor(init) {
|
|
11
12
|
this.loadPage = init.loadPage;
|
|
@@ -18,25 +19,27 @@ export class CursorDataLoaderWithSearch {
|
|
|
18
19
|
return this._searchString;
|
|
19
20
|
}
|
|
20
21
|
loadMore = async () => {
|
|
21
|
-
if (this.
|
|
22
|
-
return this.
|
|
22
|
+
if (this.pending) {
|
|
23
|
+
return this.pending;
|
|
23
24
|
}
|
|
24
25
|
if (!this.continuationToken.canLoadMore) {
|
|
25
26
|
return [];
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
const pending = this.runLoad();
|
|
29
|
+
this.pending = pending;
|
|
30
|
+
try {
|
|
31
|
+
return await pending;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
// a concurrent reset() may have installed a newer load — leave that one alone
|
|
35
|
+
if (this.pending === pending) {
|
|
36
|
+
this.pending = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
38
39
|
};
|
|
39
40
|
async reset() {
|
|
41
|
+
this.generation++;
|
|
42
|
+
this.pending = null;
|
|
40
43
|
this.items = [];
|
|
41
44
|
this.continuationToken = ContinuationToken.init();
|
|
42
45
|
await this.loadMore();
|
|
@@ -50,7 +53,25 @@ export class CursorDataLoaderWithSearch {
|
|
|
50
53
|
const isEffective = this.isSearchStringEffective(newValue);
|
|
51
54
|
this._searchString = newValue;
|
|
52
55
|
if (wasEffective || isEffective) {
|
|
53
|
-
this.reset();
|
|
56
|
+
void this.reset();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
runLoad = async () => {
|
|
60
|
+
const generation = this.generation;
|
|
61
|
+
const search = this.isSearchStringEffective(this._searchString) ? this._searchString : '';
|
|
62
|
+
try {
|
|
63
|
+
const result = (await this.loadPage(this.continuationToken, search)) ?? { items: [], continuationToken: ContinuationToken.preventLoading() };
|
|
64
|
+
// a reset() mid-load bumps the generation — drop the stale page instead of appending it to the cleared list
|
|
65
|
+
if (generation !== this.generation) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
this.continuationToken = result.continuationToken;
|
|
69
|
+
this.items = [...this.items, ...result.items];
|
|
70
|
+
return result.items;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
console.error('CursorDataLoaderWithSearch: failed to load page', error);
|
|
74
|
+
return [];
|
|
54
75
|
}
|
|
55
76
|
};
|
|
56
77
|
isSearchStringEffective = (searchString) => searchString && searchString.length >= this.searchStringMinLength;
|
|
@@ -4,10 +4,12 @@ export declare class CursorDataLoader<T> implements IDataLoader<T> {
|
|
|
4
4
|
items: T[];
|
|
5
5
|
continuationToken: ContinuationToken;
|
|
6
6
|
private loadPage;
|
|
7
|
-
private
|
|
7
|
+
private pending;
|
|
8
|
+
private generation;
|
|
8
9
|
constructor(init: {
|
|
9
10
|
loadPage: (continuationToken: ContinuationToken) => Promise<CursorResult<T> | null>;
|
|
10
11
|
});
|
|
11
12
|
loadMore: () => Promise<T[]>;
|
|
12
13
|
reset(): Promise<void>;
|
|
14
|
+
private runLoad;
|
|
13
15
|
}
|
|
@@ -1,33 +1,54 @@
|
|
|
1
|
-
import { ContinuationToken
|
|
1
|
+
import { ContinuationToken } from '..';
|
|
2
2
|
export class CursorDataLoader {
|
|
3
3
|
items = $state.raw([]);
|
|
4
4
|
continuationToken = $state.raw(ContinuationToken.init());
|
|
5
5
|
loadPage;
|
|
6
|
-
|
|
6
|
+
pending = null;
|
|
7
|
+
generation = 0;
|
|
7
8
|
constructor(init) {
|
|
8
9
|
this.loadPage = init.loadPage;
|
|
9
10
|
}
|
|
10
11
|
loadMore = async () => {
|
|
11
|
-
if (this.
|
|
12
|
-
return this.
|
|
12
|
+
if (this.pending) {
|
|
13
|
+
return this.pending;
|
|
13
14
|
}
|
|
14
15
|
if (!this.continuationToken.canLoadMore) {
|
|
15
16
|
return [];
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const pending = this.runLoad();
|
|
19
|
+
this.pending = pending;
|
|
20
|
+
try {
|
|
21
|
+
return await pending;
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
// a concurrent reset() may have installed a newer load — leave that one alone
|
|
25
|
+
if (this.pending === pending) {
|
|
26
|
+
this.pending = null;
|
|
27
|
+
}
|
|
21
28
|
}
|
|
22
|
-
this.continuationToken = result.continuationToken;
|
|
23
|
-
this.items = [...this.items, ...result.items];
|
|
24
|
-
this.deferred.resolve(result.items);
|
|
25
|
-
this.deferred = null;
|
|
26
|
-
return result.items;
|
|
27
29
|
};
|
|
28
30
|
async reset() {
|
|
31
|
+
this.generation++;
|
|
32
|
+
this.pending = null;
|
|
29
33
|
this.items = [];
|
|
30
34
|
this.continuationToken = ContinuationToken.init();
|
|
31
35
|
await this.loadMore();
|
|
32
36
|
}
|
|
37
|
+
runLoad = async () => {
|
|
38
|
+
const generation = this.generation;
|
|
39
|
+
try {
|
|
40
|
+
const result = (await this.loadPage(this.continuationToken)) ?? { items: [], continuationToken: ContinuationToken.preventLoading() };
|
|
41
|
+
// a reset() mid-load bumps the generation — drop the stale page instead of appending it to the cleared list
|
|
42
|
+
if (generation !== this.generation) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
this.continuationToken = result.continuationToken;
|
|
46
|
+
this.items = [...this.items, ...result.items];
|
|
47
|
+
return result.items;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error('CursorDataLoader: failed to load page', error);
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
};
|
|
33
54
|
}
|
|
@@ -13,4 +13,8 @@ export declare class AcceptFileType {
|
|
|
13
13
|
static readonly webAssetImageOrVideo: string;
|
|
14
14
|
static readonly webAssetIcon = "image/png";
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* `accept`-style matcher. Tokens may be MIMEs (`image/*`, `*\/*`, `application/pdf`) or
|
|
18
|
+
* extensions (`.pdf`), case-insensitive. Empty accept = match anything.
|
|
19
|
+
*/
|
|
16
20
|
export declare const matchesAcceptedFileTypes: (file: File, acceptedTypesAndExtensions: string) => boolean;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { FileHelper } from './file-helper';
|
|
2
1
|
export class AcceptFileType {
|
|
3
2
|
static any = '*/*';
|
|
4
3
|
static image = 'image/png,image/jpeg,image/webp';
|
|
@@ -15,14 +14,30 @@ export class AcceptFileType {
|
|
|
15
14
|
static webAssetImageOrVideo = [this.webAssetImage, this.webAssetVideo].join(',');
|
|
16
15
|
static webAssetIcon = 'image/png';
|
|
17
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* `accept`-style matcher. Tokens may be MIMEs (`image/*`, `*\/*`, `application/pdf`) or
|
|
19
|
+
* extensions (`.pdf`), case-insensitive. Empty accept = match anything.
|
|
20
|
+
*/
|
|
18
21
|
export const matchesAcceptedFileTypes = (file, acceptedTypesAndExtensions) => {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return (extensions.includes(fileExt) || fileTypes.includes(file.type)) && isValid;
|
|
22
|
+
const tokens = acceptedTypesAndExtensions
|
|
23
|
+
.split(',')
|
|
24
|
+
.map((s) => s.trim().toLowerCase())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
if (tokens.length === 0) {
|
|
27
|
+
return true;
|
|
26
28
|
}
|
|
27
|
-
|
|
29
|
+
const fileType = (file.type || '').toLowerCase();
|
|
30
|
+
const fileName = file.name.toLowerCase();
|
|
31
|
+
return tokens.some((token) => {
|
|
32
|
+
if (token.startsWith('.')) {
|
|
33
|
+
return fileName.endsWith(token);
|
|
34
|
+
}
|
|
35
|
+
if (token === '*/*') {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (token.endsWith('/*')) {
|
|
39
|
+
return fileType.startsWith(token.slice(0, -1));
|
|
40
|
+
}
|
|
41
|
+
return fileType === token;
|
|
42
|
+
});
|
|
28
43
|
};
|
|
@@ -1,32 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Permissive `accept`-style matcher. Tokens may be MIMEs (`image/*`, `application/pdf`) or
|
|
3
|
-
* extensions (`.pdf`). Empty accept = match anything. Unlike `matchesAcceptedFileTypes` from
|
|
4
|
-
* `./file-types`, this does NOT cross-check against an internal whitelist — only the
|
|
5
|
-
* consumer's accept string drives the decision.
|
|
6
|
-
*/
|
|
7
|
-
const matchesAccept = (file, accept) => {
|
|
8
|
-
if (!accept) {
|
|
9
|
-
return true;
|
|
10
|
-
}
|
|
11
|
-
const tokens = accept
|
|
12
|
-
.split(',')
|
|
13
|
-
.map((s) => s.trim().toLowerCase())
|
|
14
|
-
.filter(Boolean);
|
|
15
|
-
if (tokens.length === 0) {
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
const fileType = (file.type || '').toLowerCase();
|
|
19
|
-
const fileName = file.name.toLowerCase();
|
|
20
|
-
return tokens.some((t) => {
|
|
21
|
-
if (t.startsWith('.')) {
|
|
22
|
-
return fileName.endsWith(t);
|
|
23
|
-
}
|
|
24
|
-
if (t.endsWith('/*')) {
|
|
25
|
-
return fileType.startsWith(t.slice(0, -1));
|
|
26
|
-
}
|
|
27
|
-
return fileType === t;
|
|
28
|
-
});
|
|
29
|
-
};
|
|
1
|
+
import { matchesAcceptedFileTypes } from './file-types';
|
|
30
2
|
const readImageDimensions = (file) => new Promise((resolve, reject) => {
|
|
31
3
|
const url = URL.createObjectURL(file);
|
|
32
4
|
const img = new Image();
|
|
@@ -72,7 +44,7 @@ const readMediaDuration = (file, kind) => new Promise((resolve, reject) => {
|
|
|
72
44
|
export const FileRules = {
|
|
73
45
|
MaxSize: (bytes, message) => (file) => file.size > bytes ? message : null,
|
|
74
46
|
Mime: (accept, message) => {
|
|
75
|
-
const rule = (file) => (
|
|
47
|
+
const rule = (file) => (matchesAcceptedFileTypes(file, accept) ? null : message);
|
|
76
48
|
rule.accept = accept;
|
|
77
49
|
return rule;
|
|
78
50
|
},
|
|
@@ -9,7 +9,7 @@ export class NumberHelper {
|
|
|
9
9
|
const isNegative = n < 0;
|
|
10
10
|
let modulo = isNegative ? 0 - n : n;
|
|
11
11
|
const output = [];
|
|
12
|
-
for (; modulo >= 1000; modulo = Math.floor(
|
|
12
|
+
for (; modulo >= 1000; modulo = Math.floor(modulo / 1000)) {
|
|
13
13
|
output.unshift(String(modulo % 1000).padStart(3, '0'));
|
|
14
14
|
}
|
|
15
15
|
output.unshift(modulo);
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<script lang="ts" generics="T">import { Icon, IconSlot } from '../icon';
|
|
2
|
+
import { CollectionListLocalization } from './collection-list-localization';
|
|
3
|
+
import IconReorderDotsVertical from '@fluentui/svg-icons/icons/re_order_dots_vertical_20_regular.svg?raw';
|
|
4
|
+
import { flip } from 'svelte/animate';
|
|
5
|
+
import { dndzone } from 'svelte-dnd-action';
|
|
6
|
+
let { items, actions = [], orderable = true, showImage = true, actionsAlwaysVisible = false, view, on } = $props();
|
|
7
|
+
const localization = new CollectionListLocalization();
|
|
8
|
+
// unique per instance so two lists on one screen can't drag items between each other
|
|
9
|
+
const dndType = `collection-list-${crypto.randomUUID()}`;
|
|
10
|
+
const flipDurationMs = 200;
|
|
11
|
+
let dndItems = $state.raw([]);
|
|
12
|
+
$effect(() => {
|
|
13
|
+
dndItems = items ?? [];
|
|
14
|
+
});
|
|
15
|
+
const dragDisabled = $derived(!orderable || dndItems.length < 2);
|
|
16
|
+
const handleConsider = (e) => {
|
|
17
|
+
dndItems = e.detail.items;
|
|
18
|
+
};
|
|
19
|
+
const handleFinalize = (e) => {
|
|
20
|
+
const reordered = e.detail.items;
|
|
21
|
+
dndItems = reordered;
|
|
22
|
+
const newIndex = reordered.findIndex((i) => i.id === e.detail.info.id);
|
|
23
|
+
on?.reorder?.(reordered.map((i) => i.originalItem), newIndex);
|
|
24
|
+
};
|
|
25
|
+
// strip the library's default yellow outline on the cloned drag element
|
|
26
|
+
const cleanDraggedElement = (element) => {
|
|
27
|
+
if (element) {
|
|
28
|
+
element.style.outline = 'none';
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const visibleActions = (item, index) => actions.filter((a) => !a.hidden || !a.hidden(item.originalItem, index));
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
{#snippet builtinBody(item: CollectionItem<T>)}
|
|
35
|
+
{#if showImage}
|
|
36
|
+
<span class="collection-list__thumbnail">
|
|
37
|
+
{#if item.image}
|
|
38
|
+
<img class="collection-list__thumbnail-image" src={item.image} alt="" draggable="false" />
|
|
39
|
+
{/if}
|
|
40
|
+
</span>
|
|
41
|
+
{/if}
|
|
42
|
+
{#if item.icon}
|
|
43
|
+
<span class="collection-list__icon"><IconSlot icon={item.icon} /></span>
|
|
44
|
+
{/if}
|
|
45
|
+
{#if item.description}
|
|
46
|
+
<span class="collection-list__description" title={item.description}>{item.description}</span>
|
|
47
|
+
{/if}
|
|
48
|
+
{/snippet}
|
|
49
|
+
|
|
50
|
+
<div
|
|
51
|
+
class="collection-list"
|
|
52
|
+
use:dndzone={{
|
|
53
|
+
items: dndItems,
|
|
54
|
+
type: dndType,
|
|
55
|
+
flipDurationMs,
|
|
56
|
+
dropTargetStyle: {},
|
|
57
|
+
morphDisabled: true,
|
|
58
|
+
dragDisabled,
|
|
59
|
+
transformDraggedElement: cleanDraggedElement
|
|
60
|
+
}}
|
|
61
|
+
onconsider={handleConsider}
|
|
62
|
+
onfinalize={handleFinalize}>
|
|
63
|
+
{#each dndItems as item, index (item.id)}
|
|
64
|
+
{@const rowActions = visibleActions(item, index)}
|
|
65
|
+
<div class="collection-list__row" class:collection-list__row--actions-always={actionsAlwaysVisible} animate:flip={{ duration: flipDurationMs }}>
|
|
66
|
+
{#if !dragDisabled}
|
|
67
|
+
<span class="collection-list__handle" role="img" aria-label={localization.reorderHandle}>
|
|
68
|
+
<Icon src={IconReorderDotsVertical} />
|
|
69
|
+
</span>
|
|
70
|
+
{/if}
|
|
71
|
+
|
|
72
|
+
{#if view}
|
|
73
|
+
<div class="collection-list__body">{@render view({ item: item.originalItem, index })}</div>
|
|
74
|
+
{:else if item.imageClickCallback}
|
|
75
|
+
<button type="button" class="collection-list__body collection-list__body--clickable" onclick={() => item.imageClickCallback?.(item.originalItem)}>
|
|
76
|
+
{@render builtinBody(item)}
|
|
77
|
+
</button>
|
|
78
|
+
{:else}
|
|
79
|
+
<div class="collection-list__body">{@render builtinBody(item)}</div>
|
|
80
|
+
{/if}
|
|
81
|
+
|
|
82
|
+
{#if rowActions.length > 0}
|
|
83
|
+
<div class="collection-list__actions">
|
|
84
|
+
{#each rowActions as action (action)}
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
class="collection-list__action"
|
|
88
|
+
aria-label={action.label ?? localization.action}
|
|
89
|
+
onclick={() => action.callback(item.originalItem, index)}>
|
|
90
|
+
<IconSlot icon={action.icon} />
|
|
91
|
+
</button>
|
|
92
|
+
{/each}
|
|
93
|
+
</div>
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|
|
96
|
+
{/each}
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!--
|
|
100
|
+
@component
|
|
101
|
+
A reorderable vertical list of items: each row is an optional drag handle, an optional square
|
|
102
|
+
thumbnail + description (or a fully custom `view`), and hover-revealed icon actions. Generic over
|
|
103
|
+
the item payload `T`; the consumer maps its data into `CollectionItem<T>` and reads `on.reorder`.
|
|
104
|
+
|
|
105
|
+
### CSS Custom Properties
|
|
106
|
+
| Property | Description | Default |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `--sc-kit--collection-list--gap` | Gap between rows | `var(--sc-kit--space--2)` |
|
|
109
|
+
| `--sc-kit--collection-list--row--gap` | Gap between a row's handle / body / actions | `var(--sc-kit--space--3)` |
|
|
110
|
+
| `--sc-kit--collection-list--thumbnail--size` | Thumbnail inline size | `3.125rem` |
|
|
111
|
+
| `--sc-kit--collection-list--thumbnail--aspect-ratio` | Thumbnail aspect ratio | `1` |
|
|
112
|
+
| `--sc-kit--collection-list--thumbnail--radius` | Thumbnail corner radius | `var(--sc-kit--radius--sm)` |
|
|
113
|
+
| `--sc-kit--collection-list--thumbnail--background` | Thumbnail placeholder background | `var(--sc-kit--color--bg--element)` |
|
|
114
|
+
| `--sc-kit--collection-list--description--color` | Description / leading-icon color | `var(--sc-kit--color--text--primary)` |
|
|
115
|
+
| `--sc-kit--collection-list--handle--color` | Drag handle icon color | `var(--sc-kit--color--text--secondary)` |
|
|
116
|
+
| `--sc-kit--collection-list--action--color` | Action icon color | `var(--sc-kit--color--text--secondary)` |
|
|
117
|
+
| `--sc-kit--collection-list--action--background` | Action button background | `transparent` |
|
|
118
|
+
| `--sc-kit--collection-list--action--background--hover` | Action button hover background | `var(--sc-kit--color--bg--hover)` |
|
|
119
|
+
-->
|
|
120
|
+
|
|
121
|
+
<style>.collection-list {
|
|
122
|
+
--_cl--gap: var(--sc-kit--collection-list--gap, var(--sc-kit--space--2));
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
gap: var(--_cl--gap);
|
|
126
|
+
}
|
|
127
|
+
.collection-list__row {
|
|
128
|
+
--_cl--row-gap: var(--sc-kit--collection-list--row--gap, var(--sc-kit--space--3));
|
|
129
|
+
--_cl--thumbnail-size: var(--sc-kit--collection-list--thumbnail--size, 3.125rem);
|
|
130
|
+
--_cl--thumbnail-aspect-ratio: var(--sc-kit--collection-list--thumbnail--aspect-ratio, 1);
|
|
131
|
+
--_cl--thumbnail-radius: var(--sc-kit--collection-list--thumbnail--radius, var(--sc-kit--radius--sm));
|
|
132
|
+
--_cl--thumbnail-background: var(--sc-kit--collection-list--thumbnail--background, var(--sc-kit--color--bg--element));
|
|
133
|
+
--_cl--description-color: var(--sc-kit--collection-list--description--color, var(--sc-kit--color--text--primary));
|
|
134
|
+
--_cl--handle-color: var(--sc-kit--collection-list--handle--color, var(--sc-kit--color--text--secondary));
|
|
135
|
+
--_cl--action-color: var(--sc-kit--collection-list--action--color, var(--sc-kit--color--text--secondary));
|
|
136
|
+
--_cl--action-background: var(--sc-kit--collection-list--action--background, transparent);
|
|
137
|
+
--_cl--action-background-hover: var(--sc-kit--collection-list--action--background--hover, var(--sc-kit--color--bg--hover));
|
|
138
|
+
--_cl--actions-opacity: 0;
|
|
139
|
+
--_cl--actions-pointer-events: none;
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
gap: var(--_cl--row-gap);
|
|
143
|
+
min-width: 0;
|
|
144
|
+
}
|
|
145
|
+
.collection-list__row:hover, .collection-list__row:focus-within {
|
|
146
|
+
--_cl--actions-opacity: 1;
|
|
147
|
+
--_cl--actions-pointer-events: auto;
|
|
148
|
+
}
|
|
149
|
+
.collection-list__row--actions-always {
|
|
150
|
+
--_cl--actions-opacity: 1;
|
|
151
|
+
--_cl--actions-pointer-events: auto;
|
|
152
|
+
}
|
|
153
|
+
.collection-list__handle {
|
|
154
|
+
--sc-kit--icon--size: 1.25rem;
|
|
155
|
+
--sc-kit--icon--color: var(--_cl--handle-color);
|
|
156
|
+
flex: none;
|
|
157
|
+
display: inline-flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
cursor: grab;
|
|
160
|
+
}
|
|
161
|
+
.collection-list__handle:active {
|
|
162
|
+
cursor: grabbing;
|
|
163
|
+
}
|
|
164
|
+
.collection-list__body {
|
|
165
|
+
flex: 1;
|
|
166
|
+
min-width: 0;
|
|
167
|
+
display: flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
gap: var(--_cl--row-gap);
|
|
170
|
+
}
|
|
171
|
+
.collection-list__body--clickable {
|
|
172
|
+
appearance: none;
|
|
173
|
+
border: none;
|
|
174
|
+
background: none;
|
|
175
|
+
padding: 0;
|
|
176
|
+
font: inherit;
|
|
177
|
+
color: inherit;
|
|
178
|
+
text-align: start;
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
}
|
|
181
|
+
.collection-list__thumbnail {
|
|
182
|
+
flex: none;
|
|
183
|
+
position: relative;
|
|
184
|
+
inline-size: var(--_cl--thumbnail-size);
|
|
185
|
+
aspect-ratio: var(--_cl--thumbnail-aspect-ratio);
|
|
186
|
+
border-radius: var(--_cl--thumbnail-radius);
|
|
187
|
+
background: var(--_cl--thumbnail-background);
|
|
188
|
+
overflow: hidden;
|
|
189
|
+
}
|
|
190
|
+
.collection-list__thumbnail-image {
|
|
191
|
+
position: absolute;
|
|
192
|
+
inset: 0;
|
|
193
|
+
inline-size: 100%;
|
|
194
|
+
block-size: 100%;
|
|
195
|
+
object-fit: cover;
|
|
196
|
+
}
|
|
197
|
+
.collection-list__icon {
|
|
198
|
+
--sc-kit--icon--size: 1rem;
|
|
199
|
+
--sc-kit--icon--color: var(--_cl--description-color);
|
|
200
|
+
flex: none;
|
|
201
|
+
display: inline-flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
}
|
|
204
|
+
.collection-list__description {
|
|
205
|
+
flex: 1;
|
|
206
|
+
min-width: 0;
|
|
207
|
+
color: var(--_cl--description-color);
|
|
208
|
+
font-size: var(--sc-kit--font-size--md);
|
|
209
|
+
line-height: var(--sc-kit--line-height--md);
|
|
210
|
+
text-overflow: ellipsis;
|
|
211
|
+
max-width: 100%;
|
|
212
|
+
white-space: nowrap;
|
|
213
|
+
overflow: hidden;
|
|
214
|
+
}
|
|
215
|
+
.collection-list__actions {
|
|
216
|
+
flex: none;
|
|
217
|
+
display: flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
gap: var(--sc-kit--space--1);
|
|
220
|
+
margin-inline-start: auto;
|
|
221
|
+
opacity: var(--_cl--actions-opacity);
|
|
222
|
+
pointer-events: var(--_cl--actions-pointer-events);
|
|
223
|
+
transition: opacity var(--sc-kit--duration--base) var(--sc-kit--ease--default);
|
|
224
|
+
}
|
|
225
|
+
.collection-list__action {
|
|
226
|
+
--sc-kit--icon--size: 1.25rem;
|
|
227
|
+
--sc-kit--icon--color: var(--_cl--action-color);
|
|
228
|
+
flex: none;
|
|
229
|
+
display: inline-flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
justify-content: center;
|
|
232
|
+
inline-size: 1.75rem;
|
|
233
|
+
block-size: 1.75rem;
|
|
234
|
+
border: none;
|
|
235
|
+
border-radius: var(--sc-kit--radius--sm);
|
|
236
|
+
background: var(--_cl--action-background);
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
transition: background-color var(--sc-kit--duration--base) var(--sc-kit--ease--default);
|
|
239
|
+
}
|
|
240
|
+
.collection-list__action:hover {
|
|
241
|
+
background: var(--_cl--action-background-hover);
|
|
242
|
+
}</style>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CollectionAction, CollectionItem } from './types';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
declare function $$render<T>(): {
|
|
4
|
+
props: {
|
|
5
|
+
items: CollectionItem<T>[] | null;
|
|
6
|
+
/** Per-row icon buttons, revealed on row hover (or pinned via `actionsAlwaysVisible`). */
|
|
7
|
+
actions?: CollectionAction<T>[];
|
|
8
|
+
/** Enables drag reorder. Auto-disabled when fewer than 2 items. @default true */
|
|
9
|
+
orderable?: boolean;
|
|
10
|
+
/** Render the leading square thumbnail in the built-in row. Ignored when `view` is set. @default true */
|
|
11
|
+
showImage?: boolean;
|
|
12
|
+
/** Pin row actions visible instead of revealing them on hover/focus. @default false */
|
|
13
|
+
actionsAlwaysVisible?: boolean;
|
|
14
|
+
/** Replaces the built-in row body (thumbnail + description); the drag handle and actions still render around it. */
|
|
15
|
+
view?: Snippet<[{
|
|
16
|
+
item: T;
|
|
17
|
+
index: number;
|
|
18
|
+
}]>;
|
|
19
|
+
on?: {
|
|
20
|
+
reorder?: (items: T[], newIndex: number) => void;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
exports: {};
|
|
24
|
+
bindings: "";
|
|
25
|
+
slots: {};
|
|
26
|
+
events: {};
|
|
27
|
+
};
|
|
28
|
+
declare class __sveltets_Render<T> {
|
|
29
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
30
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
31
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
32
|
+
bindings(): "";
|
|
33
|
+
exports(): {};
|
|
34
|
+
}
|
|
35
|
+
interface $$IsomorphicComponent {
|
|
36
|
+
new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
37
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
38
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
39
|
+
<T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
40
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A reorderable vertical list of items: each row is an optional drag handle, an optional square
|
|
44
|
+
* thumbnail + description (or a fully custom `view`), and hover-revealed icon actions. Generic over
|
|
45
|
+
* the item payload `T`; the consumer maps its data into `CollectionItem<T>` and reads `on.reorder`.
|
|
46
|
+
*
|
|
47
|
+
* ### CSS Custom Properties
|
|
48
|
+
* | Property | Description | Default |
|
|
49
|
+
* |---|---|---|
|
|
50
|
+
* | `--sc-kit--collection-list--gap` | Gap between rows | `var(--sc-kit--space--2)` |
|
|
51
|
+
* | `--sc-kit--collection-list--row--gap` | Gap between a row's handle / body / actions | `var(--sc-kit--space--3)` |
|
|
52
|
+
* | `--sc-kit--collection-list--thumbnail--size` | Thumbnail inline size | `3.125rem` |
|
|
53
|
+
* | `--sc-kit--collection-list--thumbnail--aspect-ratio` | Thumbnail aspect ratio | `1` |
|
|
54
|
+
* | `--sc-kit--collection-list--thumbnail--radius` | Thumbnail corner radius | `var(--sc-kit--radius--sm)` |
|
|
55
|
+
* | `--sc-kit--collection-list--thumbnail--background` | Thumbnail placeholder background | `var(--sc-kit--color--bg--element)` |
|
|
56
|
+
* | `--sc-kit--collection-list--description--color` | Description / leading-icon color | `var(--sc-kit--color--text--primary)` |
|
|
57
|
+
* | `--sc-kit--collection-list--handle--color` | Drag handle icon color | `var(--sc-kit--color--text--secondary)` |
|
|
58
|
+
* | `--sc-kit--collection-list--action--color` | Action icon color | `var(--sc-kit--color--text--secondary)` |
|
|
59
|
+
* | `--sc-kit--collection-list--action--background` | Action button background | `transparent` |
|
|
60
|
+
* | `--sc-kit--collection-list--action--background--hover` | Action button hover background | `var(--sc-kit--color--bg--hover)` |
|
|
61
|
+
*/
|
|
62
|
+
declare const Cmp: $$IsomorphicComponent;
|
|
63
|
+
type Cmp<T> = InstanceType<typeof Cmp<T>>;
|
|
64
|
+
export default Cmp;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AppLocale } from '../../core/locale';
|
|
2
|
+
const loc = {
|
|
3
|
+
reorderHandle: {
|
|
4
|
+
en: 'Drag to reorder',
|
|
5
|
+
no: 'Dra for å endre rekkefølge'
|
|
6
|
+
},
|
|
7
|
+
action: {
|
|
8
|
+
en: 'Action',
|
|
9
|
+
no: 'Handling'
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
export class CollectionListLocalization {
|
|
13
|
+
get reorderHandle() {
|
|
14
|
+
return loc.reorderHandle[AppLocale.current];
|
|
15
|
+
}
|
|
16
|
+
get action() {
|
|
17
|
+
return loc.action[AppLocale.current];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IconProp } from '../icon';
|
|
2
|
+
/**
|
|
3
|
+
* A row in a {@link CollectionList}. Wraps the consumer's payload (`originalItem`) with the
|
|
4
|
+
* presentation fields the built-in row renders. Construct one per item; `id` auto-fills when omitted.
|
|
5
|
+
*/
|
|
6
|
+
export declare class CollectionItem<T> {
|
|
7
|
+
originalItem: T;
|
|
8
|
+
id: string;
|
|
9
|
+
image: string | null;
|
|
10
|
+
icon: IconProp | null;
|
|
11
|
+
description: string | null;
|
|
12
|
+
imageClickCallback: ((item: T) => void) | null;
|
|
13
|
+
constructor(init: {
|
|
14
|
+
originalItem: T;
|
|
15
|
+
id?: string;
|
|
16
|
+
image?: string | null;
|
|
17
|
+
icon?: IconProp | null;
|
|
18
|
+
description?: string | null;
|
|
19
|
+
imageClickCallback?: (item: T) => void;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export type CollectionAction<T> = {
|
|
23
|
+
icon: IconProp;
|
|
24
|
+
/** Accessible name for the icon-only button. Falls back to a generic localized label. */
|
|
25
|
+
label?: string;
|
|
26
|
+
callback: (item: T, index: number) => void;
|
|
27
|
+
hidden?: (item: T, index: number) => boolean;
|
|
28
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A row in a {@link CollectionList}. Wraps the consumer's payload (`originalItem`) with the
|
|
3
|
+
* presentation fields the built-in row renders. Construct one per item; `id` auto-fills when omitted.
|
|
4
|
+
*/
|
|
5
|
+
export class CollectionItem {
|
|
6
|
+
originalItem;
|
|
7
|
+
id;
|
|
8
|
+
image;
|
|
9
|
+
icon;
|
|
10
|
+
description;
|
|
11
|
+
imageClickCallback;
|
|
12
|
+
constructor(init) {
|
|
13
|
+
this.originalItem = init.originalItem;
|
|
14
|
+
this.id = init.id ?? crypto.randomUUID();
|
|
15
|
+
this.image = init.image ?? null;
|
|
16
|
+
this.icon = init.icon ?? null;
|
|
17
|
+
this.description = init.description ?? null;
|
|
18
|
+
this.imageClickCallback = init.imageClickCallback ?? null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">const { seconds, variant = 'raw', showZero = false } = $props();
|
|
2
|
+
const format = (secs) => {
|
|
3
|
+
const total = Math.max(0, Math.round(secs));
|
|
4
|
+
const hours = Math.floor(total / 3600);
|
|
5
|
+
const minutes = Math.floor((total % 3600) / 60);
|
|
6
|
+
const ss = (total % 60).toString().padStart(2, '0');
|
|
7
|
+
return hours > 0 ? `${hours}:${minutes.toString().padStart(2, '0')}:${ss}` : `${minutes}:${ss}`;
|
|
8
|
+
};
|
|
9
|
+
const value = $derived(seconds !== null && seconds > 0 ? format(seconds) : showZero ? format(0) : null);
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
{#if value !== null}
|
|
13
|
+
<span class="duration" class:duration--badge={variant === 'badge'}>{value}</span>
|
|
14
|
+
{/if}
|
|
15
|
+
|
|
16
|
+
<!--
|
|
17
|
+
@component
|
|
18
|
+
Renders a media/time duration given in seconds as a human-readable clock string — `m:ss` under an
|
|
19
|
+
hour, `h:mm:ss` at or above (seconds are rounded, so a 60-second carry rolls up). `null` or ≤ 0
|
|
20
|
+
renders nothing; pass `showZero` to force `0:00`. `variant='raw'` (default) is bare text that
|
|
21
|
+
inherits the parent's typography; `variant='badge'` is a dark overlay pill for the bottom corner of
|
|
22
|
+
a video thumbnail. Placement (e.g. absolute over media) is the consumer's job via the cascade.
|
|
23
|
+
|
|
24
|
+
### CSS Custom Properties
|
|
25
|
+
| Property | Description | Default |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `--sc-kit--duration--badge--background` | Badge background (badge variant) | `rgb(0 0 0 / 55%)` (dark translucent) |
|
|
28
|
+
| `--sc-kit--duration--badge--color` | Badge text color (badge variant) | `var(--sc-kit--color--text--on-accent)` |
|
|
29
|
+
| `--sc-kit--duration--badge--padding` | Badge padding (badge variant) | `var(--sc-kit--space--1) var(--sc-kit--space--2)` |
|
|
30
|
+
| `--sc-kit--duration--badge--border-radius` | Badge corner radius (badge variant) | `var(--sc-kit--radius--sm)` |
|
|
31
|
+
| `--sc-kit--duration--badge--font-size` | Badge font size (badge variant) | `var(--sc-kit--font-size--xs)` |
|
|
32
|
+
-->
|
|
33
|
+
<style>.duration {
|
|
34
|
+
font-variant-numeric: tabular-nums;
|
|
35
|
+
}
|
|
36
|
+
.duration--badge {
|
|
37
|
+
--_duration--badge-background: var(--sc-kit--duration--badge--background, rgb(0 0 0 / 55%));
|
|
38
|
+
--_duration--badge-color: var(--sc-kit--duration--badge--color, var(--sc-kit--color--text--on-accent));
|
|
39
|
+
--_duration--badge-padding: var(--sc-kit--duration--badge--padding, var(--sc-kit--space--1) var(--sc-kit--space--2));
|
|
40
|
+
--_duration--badge-border-radius: var(--sc-kit--duration--badge--border-radius, var(--sc-kit--radius--sm));
|
|
41
|
+
--_duration--badge-font-size: var(--sc-kit--duration--badge--font-size, var(--sc-kit--font-size--xs));
|
|
42
|
+
display: inline-block;
|
|
43
|
+
padding: var(--_duration--badge-padding);
|
|
44
|
+
background: var(--_duration--badge-background);
|
|
45
|
+
border-radius: var(--_duration--badge-border-radius);
|
|
46
|
+
color: var(--_duration--badge-color);
|
|
47
|
+
font-size: var(--_duration--badge-font-size);
|
|
48
|
+
line-height: var(--sc-kit--leading--normal);
|
|
49
|
+
}</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
/** Duration in seconds. Zero, negative, or `null` renders nothing unless `showZero`. */
|
|
3
|
+
seconds: number | null;
|
|
4
|
+
/** Visual style — `raw` is bare text; `badge` is a dark overlay pill for media thumbnails. @default 'raw' */
|
|
5
|
+
variant?: 'raw' | 'badge';
|
|
6
|
+
/** Render `0:00` for a null / zero / negative duration instead of nothing. @default false */
|
|
7
|
+
showZero?: boolean;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Renders a media/time duration given in seconds as a human-readable clock string — `m:ss` under an
|
|
11
|
+
* hour, `h:mm:ss` at or above (seconds are rounded, so a 60-second carry rolls up). `null` or ≤ 0
|
|
12
|
+
* renders nothing; pass `showZero` to force `0:00`. `variant='raw'` (default) is bare text that
|
|
13
|
+
* inherits the parent's typography; `variant='badge'` is a dark overlay pill for the bottom corner of
|
|
14
|
+
* a video thumbnail. Placement (e.g. absolute over media) is the consumer's job via the cascade.
|
|
15
|
+
*
|
|
16
|
+
* ### CSS Custom Properties
|
|
17
|
+
* | Property | Description | Default |
|
|
18
|
+
* |---|---|---|
|
|
19
|
+
* | `--sc-kit--duration--badge--background` | Badge background (badge variant) | `rgb(0 0 0 / 55%)` (dark translucent) |
|
|
20
|
+
* | `--sc-kit--duration--badge--color` | Badge text color (badge variant) | `var(--sc-kit--color--text--on-accent)` |
|
|
21
|
+
* | `--sc-kit--duration--badge--padding` | Badge padding (badge variant) | `var(--sc-kit--space--1) var(--sc-kit--space--2)` |
|
|
22
|
+
* | `--sc-kit--duration--badge--border-radius` | Badge corner radius (badge variant) | `var(--sc-kit--radius--sm)` |
|
|
23
|
+
* | `--sc-kit--duration--badge--font-size` | Badge font size (badge variant) | `var(--sc-kit--font-size--xs)` |
|
|
24
|
+
*/
|
|
25
|
+
declare const Cmp: import("svelte").Component<Props, {}, "">;
|
|
26
|
+
type Cmp = ReturnType<typeof Cmp>;
|
|
27
|
+
export default Cmp;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Duration } from './cmp.duration.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Duration } from './cmp.duration.svelte';
|
|
@@ -35,6 +35,7 @@ const core = createSelectCore({
|
|
|
35
35
|
getDebounceMs: () => debounceMs,
|
|
36
36
|
getCompare: () => compare,
|
|
37
37
|
getCanCreate: () => canCreate,
|
|
38
|
+
getHideCreateRow: () => showCreateSection,
|
|
38
39
|
getGroupHeader: () => groupHeader,
|
|
39
40
|
getSelectionMode: () => selectionMode,
|
|
40
41
|
isPicked: (option) => isPicked(option.value),
|
|
@@ -13,6 +13,8 @@ export type SelectCoreConfig<T> = {
|
|
|
13
13
|
getCompare: () => (a: T, b: T) => boolean;
|
|
14
14
|
/** Optional `canCreate` validator — presence enables creatable mode. */
|
|
15
15
|
getCanCreate: () => ((q: string) => boolean) | undefined;
|
|
16
|
+
/** When true, no `kind:'create'` row is emitted (the host renders its own create UI) — keyboard navigation must not reach a row the listbox doesn't show. @default false */
|
|
17
|
+
getHideCreateRow?: () => boolean;
|
|
16
18
|
/** Group-header behavior. `toggle-all` only makes sense for multi. */
|
|
17
19
|
getGroupHeader: () => 'static' | 'value' | 'toggle-all';
|
|
18
20
|
/**
|
|
@@ -83,7 +83,7 @@ export function createSelectCore(config) {
|
|
|
83
83
|
const groupMap = $derived(buildGroupMap(items));
|
|
84
84
|
const rows = $derived.by(() => {
|
|
85
85
|
const out = [];
|
|
86
|
-
if (showCreateRow) {
|
|
86
|
+
if (showCreateRow && !config.getHideCreateRow?.()) {
|
|
87
87
|
out.push({ kind: 'create', query });
|
|
88
88
|
}
|
|
89
89
|
const compare = config.getCompare();
|
|
@@ -187,9 +187,13 @@ export function createSelectCore(config) {
|
|
|
187
187
|
cancelInFlightLoad();
|
|
188
188
|
};
|
|
189
189
|
const resetFilterKeepFocus = () => {
|
|
190
|
+
// keep highlight on the picked row unless a filter was narrowing the list — else it flashes to the first row after a click
|
|
191
|
+
const wasFiltering = isFiltering || query !== '';
|
|
190
192
|
query = '';
|
|
191
193
|
isFiltering = false;
|
|
192
|
-
|
|
194
|
+
if (wasFiltering) {
|
|
195
|
+
highlight = navigableIndices[0] ?? 0;
|
|
196
|
+
}
|
|
193
197
|
config.getInputEl()?.focus();
|
|
194
198
|
void runLoad('');
|
|
195
199
|
};
|
|
@@ -8,9 +8,12 @@ import IconCheckboxUnchecked from '@fluentui/svg-icons/icons/checkbox_unchecked_
|
|
|
8
8
|
import IconCheckmark from '@fluentui/svg-icons/icons/checkmark_16_regular.svg?raw';
|
|
9
9
|
let { triggerEl, isOpen, rows, highlight, loading = false, listboxId, matchTriggerWidth = true, selectedDisplay = 'checkmark', optionSnippet, headerSnippet, bodySnippet, hideCreateRows = false, suppressEmpty = false, on } = $props();
|
|
10
10
|
const localization = new SelectListboxLocalization();
|
|
11
|
-
|
|
11
|
+
// rows keep their index into the full `rows` array — `highlight` and row ids are core-side indices,
|
|
12
|
+
// so filtering without preserving them shifts every row by one and Enter acts on the wrong row
|
|
13
|
+
const visibleRows = $derived(rows.map((row, index) => ({ row, index })).filter(({ row }) => !hideCreateRows || row.kind !== 'create'));
|
|
12
14
|
const hasRows = $derived(visibleRows.length > 0 || !!headerSnippet);
|
|
13
15
|
const showEmpty = $derived(!hasRows && !loading && !suppressEmpty);
|
|
16
|
+
const isHighlightMode = $derived(selectedDisplay === 'highlight');
|
|
14
17
|
let panelEl = $state.raw(undefined);
|
|
15
18
|
let triggerWidth = $state(undefined);
|
|
16
19
|
// Floating UI: position when open, autoUpdate on scroll/resize, cleanup on close.
|
|
@@ -129,14 +132,15 @@ $effect(() => {
|
|
|
129
132
|
{#if visibleRows.length === 0 && !headerSnippet}
|
|
130
133
|
<div class="select-listbox__empty">{localization.noMatches}</div>
|
|
131
134
|
{:else if bodySnippet}
|
|
132
|
-
{@render bodySnippet({ rows: visibleRows, highlight, triggerWidth, on: { pickRow: on.pickRow, hoverRow: on.hoverRow } })}
|
|
135
|
+
{@render bodySnippet({ rows: visibleRows.map(({ row }) => row), highlight, triggerWidth, on: { pickRow: on.pickRow, hoverRow: on.hoverRow } })}
|
|
133
136
|
{:else}
|
|
134
|
-
{#each visibleRows as row,
|
|
135
|
-
{@const rowId = `${listboxId}-row-${
|
|
137
|
+
{#each visibleRows as { row, index } (index)}
|
|
138
|
+
{@const rowId = `${listboxId}-row-${index}`}
|
|
136
139
|
{#if row.kind === 'create'}
|
|
137
140
|
<div
|
|
138
141
|
class="select-listbox__row select-listbox__row--create"
|
|
139
|
-
class:select-listbox__row--active={
|
|
142
|
+
class:select-listbox__row--active={index === highlight}
|
|
143
|
+
class:select-listbox__row--ring={isHighlightMode && index === highlight}
|
|
140
144
|
role="option"
|
|
141
145
|
aria-selected="false"
|
|
142
146
|
tabindex="-1"
|
|
@@ -144,7 +148,7 @@ $effect(() => {
|
|
|
144
148
|
data-row-id={rowId}
|
|
145
149
|
onclick={() => on.pickRow(row)}
|
|
146
150
|
onkeydown={() => undefined}
|
|
147
|
-
onmouseenter={() => on.hoverRow(
|
|
151
|
+
onmouseenter={() => on.hoverRow(index)}>
|
|
148
152
|
<span class="select-listbox__row-icon"><Icon src={IconAdd} /></span>
|
|
149
153
|
<span class="select-listbox__row-label">{localization.createLabel(row.query)}</span>
|
|
150
154
|
</div>
|
|
@@ -152,8 +156,10 @@ $effect(() => {
|
|
|
152
156
|
{#if row.selectable}
|
|
153
157
|
<div
|
|
154
158
|
class="select-listbox__row select-listbox__row--group-header select-listbox__row--selectable"
|
|
155
|
-
class:select-listbox__row--active={
|
|
159
|
+
class:select-listbox__row--active={index === highlight}
|
|
156
160
|
class:select-listbox__row--selected={row.selected}
|
|
161
|
+
class:select-listbox__row--ring={isHighlightMode && index === highlight}
|
|
162
|
+
class:select-listbox__row--fill={isHighlightMode && row.selected}
|
|
157
163
|
role="option"
|
|
158
164
|
aria-selected={row.state === 'checked'}
|
|
159
165
|
aria-checked={row.state === 'indeterminate' ? 'mixed' : row.state === 'checked'}
|
|
@@ -162,7 +168,7 @@ $effect(() => {
|
|
|
162
168
|
data-row-id={rowId}
|
|
163
169
|
onclick={() => on.pickRow(row)}
|
|
164
170
|
onkeydown={() => undefined}
|
|
165
|
-
onmouseenter={() => on.hoverRow(
|
|
171
|
+
onmouseenter={() => on.hoverRow(index)}>
|
|
166
172
|
{#if selectedDisplay === 'checkbox'}
|
|
167
173
|
{@render checkboxGlyph(row.state)}
|
|
168
174
|
{/if}
|
|
@@ -179,8 +185,10 @@ $effect(() => {
|
|
|
179
185
|
{:else}
|
|
180
186
|
<div
|
|
181
187
|
class="select-listbox__row select-listbox__row--option"
|
|
182
|
-
class:select-listbox__row--active={
|
|
188
|
+
class:select-listbox__row--active={index === highlight}
|
|
183
189
|
class:select-listbox__row--selected={row.selected}
|
|
190
|
+
class:select-listbox__row--ring={isHighlightMode && index === highlight}
|
|
191
|
+
class:select-listbox__row--fill={isHighlightMode && row.selected}
|
|
184
192
|
class:select-listbox__row--disabled={row.option.disabled}
|
|
185
193
|
class:select-listbox__row--indent={row.indent}
|
|
186
194
|
role="option"
|
|
@@ -191,7 +199,7 @@ $effect(() => {
|
|
|
191
199
|
data-row-id={rowId}
|
|
192
200
|
onclick={() => on.pickRow(row)}
|
|
193
201
|
onkeydown={() => undefined}
|
|
194
|
-
onmouseenter={() => on.hoverRow(
|
|
202
|
+
onmouseenter={() => on.hoverRow(index)}>
|
|
195
203
|
{#if selectedDisplay === 'checkbox'}
|
|
196
204
|
{@render checkboxGlyph(row.selected ? 'checked' : 'unchecked')}
|
|
197
205
|
{/if}
|
|
@@ -228,7 +236,7 @@ dismiss events upward. Floating UI handles placement with autoUpdate on scroll/r
|
|
|
228
236
|
| `--sc-kit--select--panel--padding` | Panel inner padding | `var(--sc-kit--space--1)` |
|
|
229
237
|
| `--sc-kit--select--option--padding-block` | Option vertical padding | `var(--sc-kit--space--2)` |
|
|
230
238
|
| `--sc-kit--select--option--padding-inline` | Option horizontal padding | `var(--sc-kit--space--3)` |
|
|
231
|
-
| `--sc-kit--select--option--background--active` | Active row background | `var(--sc-kit--color--
|
|
239
|
+
| `--sc-kit--select--option--background--active` | Active / focused row background | `var(--sc-kit--color--accent--softer)` |
|
|
232
240
|
| `--sc-kit--select--option--color` | Row text color | `var(--sc-kit--color--text--primary)` |
|
|
233
241
|
| `--sc-kit--select--option--color--selected` | Selected row text color | `var(--sc-kit--color--accent)` |
|
|
234
242
|
| `--sc-kit--select--group-header--color` | Group header text color | `var(--sc-kit--color--text--muted)` |
|
|
@@ -245,12 +253,15 @@ dismiss events upward. Floating UI handles placement with autoUpdate on scroll/r
|
|
|
245
253
|
--_lb--padding: var(--sc-kit--select--panel--padding, var(--sc-kit--space--1));
|
|
246
254
|
--_lb--row-padding-block: var(--sc-kit--select--option--padding-block, var(--sc-kit--space--2));
|
|
247
255
|
--_lb--row-padding-inline: var(--sc-kit--select--option--padding-inline, var(--sc-kit--space--3));
|
|
248
|
-
--_lb--row-background-active: var(--sc-kit--select--option--background--active, var(--sc-kit--color--
|
|
256
|
+
--_lb--row-background-active: var(--sc-kit--select--option--background--active, var(--sc-kit--color--accent--softer));
|
|
249
257
|
--_lb--row-color: var(--sc-kit--select--option--color, var(--sc-kit--color--text--primary));
|
|
250
258
|
--_lb--row-color-selected: var(--sc-kit--select--option--color--selected, var(--sc-kit--color--accent));
|
|
251
259
|
--_lb--group-header-color: var(--sc-kit--select--group-header--color, var(--sc-kit--color--text--muted));
|
|
252
260
|
--_lb--group-header-font-size: var(--sc-kit--select--group-header--font-size, var(--sc-kit--font-size--xs));
|
|
253
261
|
--_lb--empty-color: var(--sc-kit--select--empty--color, var(--sc-kit--color--text--muted));
|
|
262
|
+
display: flex;
|
|
263
|
+
flex-direction: column;
|
|
264
|
+
gap: 0.125rem;
|
|
254
265
|
position: absolute;
|
|
255
266
|
z-index: var(--sc-kit--z-index--popover);
|
|
256
267
|
background: var(--_lb--background);
|
|
@@ -298,6 +309,14 @@ dismiss events upward. Floating UI handles placement with autoUpdate on scroll/r
|
|
|
298
309
|
.select-listbox__row--selected {
|
|
299
310
|
color: var(--_lb--row-color-selected);
|
|
300
311
|
}
|
|
312
|
+
.select-listbox__row--ring {
|
|
313
|
+
background: transparent;
|
|
314
|
+
box-shadow: inset 0 0 0 2px var(--sc-kit--color--accent--soft);
|
|
315
|
+
}
|
|
316
|
+
.select-listbox__row--fill {
|
|
317
|
+
background: var(--sc-kit--color--accent--softer);
|
|
318
|
+
color: var(--_lb--row-color);
|
|
319
|
+
}
|
|
301
320
|
.select-listbox__row--disabled {
|
|
302
321
|
color: var(--sc-kit--color--text--muted);
|
|
303
322
|
cursor: default;
|
|
@@ -355,10 +374,6 @@ dismiss events upward. Floating UI handles placement with autoUpdate on scroll/r
|
|
|
355
374
|
.select-listbox__row-checkbox--checked, .select-listbox__row-checkbox--indeterminate {
|
|
356
375
|
--sc-kit--icon--color: var(--_lb--row-color-selected);
|
|
357
376
|
}
|
|
358
|
-
.select-listbox--display-highlight .select-listbox__row--selected {
|
|
359
|
-
background: var(--_lb--row-background-active);
|
|
360
|
-
color: var(--_lb--row-color);
|
|
361
|
-
}
|
|
362
377
|
.select-listbox__header {
|
|
363
378
|
margin-block-end: var(--sc-kit--space--1);
|
|
364
379
|
}
|
|
@@ -81,7 +81,7 @@ interface $$IsomorphicComponent {
|
|
|
81
81
|
* | `--sc-kit--select--panel--padding` | Panel inner padding | `var(--sc-kit--space--1)` |
|
|
82
82
|
* | `--sc-kit--select--option--padding-block` | Option vertical padding | `var(--sc-kit--space--2)` |
|
|
83
83
|
* | `--sc-kit--select--option--padding-inline` | Option horizontal padding | `var(--sc-kit--space--3)` |
|
|
84
|
-
* | `--sc-kit--select--option--background--active` | Active row background | `var(--sc-kit--color--
|
|
84
|
+
* | `--sc-kit--select--option--background--active` | Active / focused row background | `var(--sc-kit--color--accent--softer)` |
|
|
85
85
|
* | `--sc-kit--select--option--color` | Row text color | `var(--sc-kit--color--text--primary)` |
|
|
86
86
|
* | `--sc-kit--select--option--color--selected` | Selected row text color | `var(--sc-kit--color--accent)` |
|
|
87
87
|
* | `--sc-kit--select--group-header--color` | Group header text color | `var(--sc-kit--color--text--muted)` |
|
|
@@ -113,7 +113,7 @@ Completed sidebar steps are clickable to go back; `canAdvance(step)` gates Next
|
|
|
113
113
|
-->
|
|
114
114
|
|
|
115
115
|
<style>.stepper-dialog-layout {
|
|
116
|
-
--_sdl--sidebar-width: var(--sc-kit--stepper-dialog-layout--sidebar--width,
|
|
116
|
+
--_sdl--sidebar-width: var(--sc-kit--stepper-dialog-layout--sidebar--width, 20rem);
|
|
117
117
|
--_sdl--sidebar-background: var(--sc-kit--stepper-dialog-layout--sidebar--background, var(--sc-kit--color--bg--field-alt));
|
|
118
118
|
--sc-kit--dialog--height: var(--sc-kit--stepper-dialog-layout--height, auto);
|
|
119
119
|
--sc-kit--dialog--body--overflow-y: auto;
|
|
@@ -53,6 +53,13 @@ const hide = () => {
|
|
|
53
53
|
stopAutoUpdate = undefined;
|
|
54
54
|
visible = false;
|
|
55
55
|
};
|
|
56
|
+
// unmount while visible/pending must release the show timer and autoUpdate's scroll/resize observers
|
|
57
|
+
$effect(() => () => {
|
|
58
|
+
if (showTimer) {
|
|
59
|
+
clearTimeout(showTimer);
|
|
60
|
+
}
|
|
61
|
+
stopAutoUpdate?.();
|
|
62
|
+
});
|
|
56
63
|
</script>
|
|
57
64
|
|
|
58
65
|
<span
|
|
@@ -129,20 +129,30 @@ const play = async () => {
|
|
|
129
129
|
if (!video) {
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
|
+
let played = false;
|
|
132
133
|
try {
|
|
133
134
|
await video.play();
|
|
135
|
+
played = true;
|
|
134
136
|
}
|
|
135
|
-
catch {
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
finally {
|
|
139
|
-
if (video.paused) {
|
|
137
|
+
catch (error) {
|
|
138
|
+
// muted retry only on autoplay rejection — an AbortError from a user pause must stay paused
|
|
139
|
+
if (error instanceof DOMException && error.name === 'NotAllowedError' && video.paused) {
|
|
140
140
|
video.muted = true;
|
|
141
|
-
|
|
141
|
+
try {
|
|
142
|
+
await video.play();
|
|
143
|
+
played = true;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// still blocked without user interaction
|
|
147
|
+
}
|
|
142
148
|
}
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
143
151
|
everActivated = true;
|
|
144
152
|
}
|
|
145
|
-
|
|
153
|
+
if (played) {
|
|
154
|
+
on?.started?.({ id, src });
|
|
155
|
+
}
|
|
146
156
|
};
|
|
147
157
|
const pause = () => {
|
|
148
158
|
video?.pause();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamscloud/kit",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.14",
|
|
4
4
|
"author": "StreamsCloud",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -95,6 +95,10 @@
|
|
|
95
95
|
"types": "./dist/ui/ai-panel/index.d.ts",
|
|
96
96
|
"svelte": "./dist/ui/ai-panel/index.js"
|
|
97
97
|
},
|
|
98
|
+
"./ui/announcement-banner": {
|
|
99
|
+
"types": "./dist/ui/announcement-banner/index.d.ts",
|
|
100
|
+
"svelte": "./dist/ui/announcement-banner/index.js"
|
|
101
|
+
},
|
|
98
102
|
"./ui/avatar": {
|
|
99
103
|
"types": "./dist/ui/avatar/index.d.ts",
|
|
100
104
|
"svelte": "./dist/ui/avatar/index.js"
|
|
@@ -131,6 +135,10 @@
|
|
|
131
135
|
"types": "./dist/ui/chip-group/index.d.ts",
|
|
132
136
|
"svelte": "./dist/ui/chip-group/index.js"
|
|
133
137
|
},
|
|
138
|
+
"./ui/collection-list": {
|
|
139
|
+
"types": "./dist/ui/collection-list/index.d.ts",
|
|
140
|
+
"svelte": "./dist/ui/collection-list/index.js"
|
|
141
|
+
},
|
|
134
142
|
"./ui/color-picker": {
|
|
135
143
|
"types": "./dist/ui/color-picker/index.d.ts",
|
|
136
144
|
"svelte": "./dist/ui/color-picker/index.js"
|
|
@@ -163,6 +171,10 @@
|
|
|
163
171
|
"types": "./dist/ui/drawer/index.d.ts",
|
|
164
172
|
"svelte": "./dist/ui/drawer/index.js"
|
|
165
173
|
},
|
|
174
|
+
"./ui/duration": {
|
|
175
|
+
"types": "./dist/ui/duration/index.d.ts",
|
|
176
|
+
"svelte": "./dist/ui/duration/index.js"
|
|
177
|
+
},
|
|
166
178
|
"./ui/dynamic-component": {
|
|
167
179
|
"types": "./dist/ui/dynamic-component/index.d.ts",
|
|
168
180
|
"svelte": "./dist/ui/dynamic-component/index.js"
|