@streamscloud/kit 0.9.13 → 0.9.15
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-localization.d.ts +12 -0
- package/dist/core/files/file-validation-localization.js +75 -0
- package/dist/core/files/file-validation-rule-sets.d.ts +8 -0
- package/dist/core/files/file-validation-rule-sets.js +24 -0
- package/dist/core/files/file-validation-rules.d.ts +20 -11
- package/dist/core/files/file-validation-rules.js +74 -45
- package/dist/core/files/file-validation-types.d.ts +9 -2
- package/dist/core/files/file-validator.d.ts +3 -3
- package/dist/core/files/file-validator.js +16 -2
- package/dist/core/files/index.d.ts +2 -1
- package/dist/core/files/index.js +1 -0
- 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/file-uploader/cmp.file-uploader.svelte +2 -7
- package/dist/ui/file-uploader/cmp.file-uploader.svelte.d.ts +8 -5
- package/dist/ui/open-file-button/cmp.open-file-button.svelte +2 -6
- package/dist/ui/open-file-button/cmp.open-file-button.svelte.d.ts +7 -4
- 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/tooltip/cmp.tooltip.svelte +7 -0
- package/dist/ui/video/cmp.video.svelte +17 -7
- package/package.json +9 -1
|
@@ -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
|
+
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
<script lang="ts">import { FileValidator } from '../../core/files';
|
|
1
|
+
<script lang="ts">import { deriveAccept, FileValidator } from '../../core/files';
|
|
2
2
|
import { Icon } from '../icon';
|
|
3
3
|
import IconArrowUpload from '@fluentui/svg-icons/icons/arrow_upload_24_regular.svg?raw';
|
|
4
4
|
const { multiple = false, validationRules, disabled = false, title = '', description = '', on } = $props();
|
|
5
|
-
|
|
6
|
-
// string. Single source of truth — consumer writes the MIME pattern once on the rule.
|
|
7
|
-
const accept = $derived(validationRules
|
|
8
|
-
?.map((r) => r.accept)
|
|
9
|
-
.filter((a) => !!a)
|
|
10
|
-
.join(',') ?? '');
|
|
5
|
+
const accept = $derived(deriveAccept(validationRules ?? []));
|
|
11
6
|
let inputRef = $state.raw(undefined);
|
|
12
7
|
let dragOver = $state(false);
|
|
13
8
|
// Snapshot of MIME types captured at dragenter — DataTransfer.files is empty during drag
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import type { FileValidationResult, FileValidationRule } from '../../core/files';
|
|
1
|
+
import type { FileValidationResult, FileValidationRule, FileValidationRuleSets } from '../../core/files';
|
|
2
2
|
type Props = {
|
|
3
3
|
/** Allow multi-file selection. @default false */
|
|
4
4
|
multiple?: boolean;
|
|
5
5
|
/**
|
|
6
|
-
* Validation rules applied to each picked / dropped file
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Validation rules applied to each picked / dropped file — a single set
|
|
7
|
+
* (`FileValidationRule[]`) or several sets (`FileValidationRule[][]`). With several sets,
|
|
8
|
+
* each file is validated by the first set whose `accept` matches it (a set with no `accept`
|
|
9
|
+
* rule matches anything; a file matching none is rejected). The native picker filter and
|
|
10
|
+
* drag-over visual hint are auto-derived from every set's `FileRules.Mime(...)` rule's
|
|
11
|
+
* `accept` — pass it once via the rule, the cmp wires the rest.
|
|
9
12
|
*/
|
|
10
|
-
validationRules?: FileValidationRule[];
|
|
13
|
+
validationRules?: FileValidationRule[] | FileValidationRuleSets;
|
|
11
14
|
disabled?: boolean;
|
|
12
15
|
/** Headline shown inside the drop zone. */
|
|
13
16
|
title?: string;
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
<script lang="ts">import { FileValidator, openFile } from '../../core/files';
|
|
1
|
+
<script lang="ts">import { deriveAccept, FileValidator, openFile } from '../../core/files';
|
|
2
2
|
import { Button } from '../button';
|
|
3
3
|
const { size = 'md', variant = 'secondary', disabled = false, multiple = false, capture, validationRules, icon, iconPosition = 'leading', children, 'aria-label': ariaLabel, on } = $props();
|
|
4
|
-
|
|
5
|
-
const accept = $derived(validationRules
|
|
6
|
-
?.map((r) => r.accept)
|
|
7
|
-
.filter((a) => !!a)
|
|
8
|
-
.join(',') ?? '');
|
|
4
|
+
const accept = $derived(deriveAccept(validationRules ?? []));
|
|
9
5
|
const handleClick = async () => {
|
|
10
6
|
const files = await openFile({ multiple, accept, capture });
|
|
11
7
|
if (files.length === 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FileValidationResult, FileValidationRule } from '../../core/files';
|
|
1
|
+
import type { FileValidationResult, FileValidationRule, FileValidationRuleSets } from '../../core/files';
|
|
2
2
|
import type { ButtonSize, ButtonVariant } from '../button';
|
|
3
3
|
import type { IconProp } from '../icon';
|
|
4
4
|
import type { Snippet } from 'svelte';
|
|
@@ -12,10 +12,13 @@ type Props = {
|
|
|
12
12
|
/** Optional file-picker capture hint (mobile camera / mic). */
|
|
13
13
|
capture?: 'user' | 'environment';
|
|
14
14
|
/**
|
|
15
|
-
* Rules applied to picked files
|
|
16
|
-
*
|
|
15
|
+
* Rules applied to picked files — a single set (`FileValidationRule[]`) or several sets
|
|
16
|
+
* (`FileValidationRule[][]`). With several sets, each file is validated by the first set
|
|
17
|
+
* whose `accept` matches it (a set with no `accept` rule matches anything; a file matching
|
|
18
|
+
* none is rejected). The native picker `accept` filter is auto-derived from every set's
|
|
19
|
+
* `FileRules.Mime(...)` rule — pass the MIME pattern once via the rule.
|
|
17
20
|
*/
|
|
18
|
-
validationRules?: FileValidationRule[];
|
|
21
|
+
validationRules?: FileValidationRule[] | FileValidationRuleSets;
|
|
19
22
|
/** Icon — forwarded to the underlying Button. */
|
|
20
23
|
icon?: IconProp;
|
|
21
24
|
/** Icon position. @default 'leading' */
|
|
@@ -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
|
}
|