@streamscloud/kit 0.13.0 → 0.15.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/files/index.d.ts +1 -2
- package/dist/core/files/index.js +0 -1
- package/dist/core/files/upload-media-store.svelte.d.ts +16 -15
- package/dist/core/files/upload-media-store.svelte.js +43 -22
- package/dist/core/files/upload-types.d.ts +29 -44
- package/dist/ui/file-uploader/cmp.file-row.svelte +2 -2
- package/dist/ui/file-uploader/cmp.file-upload-progress.svelte +2 -2
- package/dist/ui/file-uploader/to-uploading-file.d.ts +3 -1
- package/dist/ui/file-uploader/to-uploading-file.js +19 -7
- package/dist/ui/image/cmp.image-stub.svelte +2 -4
- package/dist/ui/image/cmp.image-stub.svelte.d.ts +1 -1
- package/dist/ui/select/cmp.multiselect-tree.svelte +2 -3
- package/dist/ui/select/cmp.multiselect-tree.svelte.d.ts +2 -2
- package/dist/ui/select/multiselect-base.svelte +2 -2
- package/dist/ui/select/types.d.ts +4 -4
- package/package.json +1 -1
- package/dist/core/files/blob-storage-registry.svelte.d.ts +0 -28
- package/dist/core/files/blob-storage-registry.svelte.js +0 -37
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { BlobWithName, FileWithBlobUrl, FileWithUploadUrl } from './types';
|
|
2
2
|
export type { AspectRatioBound, FileValidationResult, FileValidationRule, FileValidationRuleSets, VideoOrientationValue } from './file-validation-types';
|
|
3
|
-
export type {
|
|
3
|
+
export type { BlobUpload, BlobUploadStrategy, UploadingFile, UploadingFileStatus } from './upload-types';
|
|
4
4
|
export type { UploadMediaStoreOptions } from './upload-media-store.svelte';
|
|
5
5
|
export { toBlobWithName, toFileWithUploadUrl } from './types';
|
|
6
6
|
export { uploadBlob } from './blob-storage';
|
|
@@ -14,5 +14,4 @@ export { FileWithBlobDataHelper } from './file-with-blob-data-helper';
|
|
|
14
14
|
export { FilesProvider } from './files-provider';
|
|
15
15
|
export { FileValidator } from './file-validator';
|
|
16
16
|
export { FileRules } from './file-validation-rules';
|
|
17
|
-
export { BlobStorage } from './blob-storage-registry.svelte';
|
|
18
17
|
export { UploadMediaStore } from './upload-media-store.svelte';
|
package/dist/core/files/index.js
CHANGED
|
@@ -11,5 +11,4 @@ export { FileWithBlobDataHelper } from './file-with-blob-data-helper';
|
|
|
11
11
|
export { FilesProvider } from './files-provider';
|
|
12
12
|
export { FileValidator } from './file-validator';
|
|
13
13
|
export { FileRules } from './file-validation-rules';
|
|
14
|
-
export { BlobStorage } from './blob-storage-registry.svelte';
|
|
15
14
|
export { UploadMediaStore } from './upload-media-store.svelte';
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { BlobUploadStrategy, UploadingFile } from './upload-types';
|
|
2
|
+
type SuccessfulUpload = Extract<UploadingFile, {
|
|
3
|
+
status: 'success';
|
|
4
|
+
}>;
|
|
5
|
+
type FailedUpload = Extract<UploadingFile, {
|
|
6
|
+
status: 'error';
|
|
7
|
+
}>;
|
|
2
8
|
export type UploadMediaStoreOptions = {
|
|
3
|
-
/** Discriminator forwarded to the registered `BlobUploadStrategy`. */
|
|
4
|
-
kind?: BlobKind;
|
|
5
|
-
/**
|
|
6
|
-
* Override the globally registered `BlobStorage` strategy for this store instance.
|
|
7
|
-
* Useful for tests / stories.
|
|
8
|
-
*/
|
|
9
|
-
strategy?: BlobUploadStrategy;
|
|
10
9
|
/**
|
|
11
10
|
* Auto-resize images before upload. Pass `false` to disable; pass an object to override the
|
|
12
11
|
* defaults. `max1` / `max2` are not tied to width / height — the longer side is matched to
|
|
@@ -20,9 +19,9 @@ export type UploadMediaStoreOptions = {
|
|
|
20
19
|
concurrency?: number;
|
|
21
20
|
on?: {
|
|
22
21
|
/** Fires per file once its presigned URL is allocated and the blob is uploaded. */
|
|
23
|
-
blobId?: (file:
|
|
22
|
+
blobId?: (file: SuccessfulUpload, blobId: string) => void;
|
|
24
23
|
/** Fires per file when the upload errors out. */
|
|
25
|
-
error?: (file:
|
|
24
|
+
error?: (file: FailedUpload, error: unknown) => void;
|
|
26
25
|
/** Fires once after all queued files have either succeeded or errored. */
|
|
27
26
|
done?: () => void;
|
|
28
27
|
};
|
|
@@ -40,8 +39,7 @@ export type UploadMediaStoreOptions = {
|
|
|
40
39
|
*
|
|
41
40
|
* @example
|
|
42
41
|
* ```ts
|
|
43
|
-
* const store = new UploadMediaStore({
|
|
44
|
-
* kind: 'avatar',
|
|
42
|
+
* const store = new UploadMediaStore(uploadStrategy, {
|
|
45
43
|
* on: { blobId: (f, id) => { avatarBlobId = id; } }
|
|
46
44
|
* });
|
|
47
45
|
* store.add(rawFile);
|
|
@@ -50,8 +48,9 @@ export type UploadMediaStoreOptions = {
|
|
|
50
48
|
*/
|
|
51
49
|
export declare class UploadMediaStore {
|
|
52
50
|
private _files;
|
|
51
|
+
private _strategy;
|
|
53
52
|
private opts;
|
|
54
|
-
constructor(opts?: UploadMediaStoreOptions);
|
|
53
|
+
constructor(strategy: BlobUploadStrategy, opts?: UploadMediaStoreOptions);
|
|
55
54
|
/** Reactive read of the queue. */
|
|
56
55
|
get files(): UploadingFile[];
|
|
57
56
|
/** Append a raw `File`, returns its `UploadingFile` wrapper (with a stable `id`). */
|
|
@@ -69,6 +68,8 @@ export declare class UploadMediaStore {
|
|
|
69
68
|
*/
|
|
70
69
|
upload(): Promise<void>;
|
|
71
70
|
private maybeResize;
|
|
72
|
-
private
|
|
73
|
-
private
|
|
71
|
+
private fail;
|
|
72
|
+
private failAll;
|
|
73
|
+
private transition;
|
|
74
74
|
}
|
|
75
|
+
export {};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { uploadBlob } from './blob-storage';
|
|
2
|
-
import { BlobStorage } from './blob-storage-registry.svelte';
|
|
3
2
|
import { resizeBlob } from './image-resizer';
|
|
4
3
|
import { nanoid } from 'nanoid';
|
|
5
4
|
import { default as pLimit } from 'p-limit';
|
|
@@ -18,8 +17,7 @@ const DEFAULT_CONCURRENCY = 20;
|
|
|
18
17
|
*
|
|
19
18
|
* @example
|
|
20
19
|
* ```ts
|
|
21
|
-
* const store = new UploadMediaStore({
|
|
22
|
-
* kind: 'avatar',
|
|
20
|
+
* const store = new UploadMediaStore(uploadStrategy, {
|
|
23
21
|
* on: { blobId: (f, id) => { avatarBlobId = id; } }
|
|
24
22
|
* });
|
|
25
23
|
* store.add(rawFile);
|
|
@@ -28,8 +26,10 @@ const DEFAULT_CONCURRENCY = 20;
|
|
|
28
26
|
*/
|
|
29
27
|
export class UploadMediaStore {
|
|
30
28
|
_files = $state.raw([]);
|
|
29
|
+
_strategy;
|
|
31
30
|
opts;
|
|
32
|
-
constructor(opts = {}) {
|
|
31
|
+
constructor(strategy, opts = {}) {
|
|
32
|
+
this._strategy = strategy;
|
|
33
33
|
this.opts = opts;
|
|
34
34
|
}
|
|
35
35
|
/** Reactive read of the queue. */
|
|
@@ -66,28 +66,35 @@ export class UploadMediaStore {
|
|
|
66
66
|
if (queued.length === 0) {
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
let presigned;
|
|
70
|
+
try {
|
|
71
|
+
presigned = await this._strategy(queued.length);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
this.failAll(queued, error);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
71
77
|
if (presigned.length < queued.length) {
|
|
72
|
-
|
|
78
|
+
this.failAll(queued, new Error(`BlobUploadStrategy returned ${presigned.length} slots for ${queued.length} files`));
|
|
79
|
+
return;
|
|
73
80
|
}
|
|
74
81
|
const limit = pLimit(this.opts.concurrency ?? DEFAULT_CONCURRENCY);
|
|
75
82
|
const resizeOpt = this.opts.resize;
|
|
76
83
|
await Promise.all(queued.map((entry, i) => limit(async () => {
|
|
77
|
-
this.
|
|
84
|
+
this.transition(entry.id, (f) => ({ id: f.id, file: f.file, progress: 0, status: 'uploading' }));
|
|
78
85
|
try {
|
|
79
86
|
const blob = resizeOpt === false ? entry.file : await this.maybeResize(entry.file, resizeOpt ?? DEFAULT_RESIZE_LIMITS);
|
|
80
87
|
const slot = presigned[i];
|
|
81
|
-
await uploadBlob(slot.
|
|
82
|
-
this.
|
|
88
|
+
await uploadBlob(slot.uploadUrl, blob, (loaded, total) => {
|
|
89
|
+
this.transition(entry.id, (f) => (f.status === 'uploading' ? { ...f, progress: total > 0 ? loaded / total : 0 } : f));
|
|
83
90
|
});
|
|
84
|
-
this.
|
|
85
|
-
|
|
91
|
+
const done = this.transition(entry.id, (f) => ({ id: f.id, file: f.file, progress: 1, status: 'success', blobId: slot.id, readUrl: slot.readUrl }));
|
|
92
|
+
if (done?.status === 'success') {
|
|
93
|
+
this.opts.on?.blobId?.(done, slot.id);
|
|
94
|
+
}
|
|
86
95
|
}
|
|
87
96
|
catch (error) {
|
|
88
|
-
|
|
89
|
-
this.patch(entry.id, { status: 'error', error: message });
|
|
90
|
-
this.opts.on?.error?.(this.byId(entry.id), error);
|
|
97
|
+
this.fail(entry.id, error);
|
|
91
98
|
}
|
|
92
99
|
})));
|
|
93
100
|
this.opts.on?.done?.();
|
|
@@ -105,14 +112,28 @@ export class UploadMediaStore {
|
|
|
105
112
|
return file;
|
|
106
113
|
}
|
|
107
114
|
}
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
fail(id, error) {
|
|
116
|
+
const message = error instanceof Error ? error.message : 'Upload failed';
|
|
117
|
+
const failed = this.transition(id, (f) => ({ id: f.id, file: f.file, progress: f.progress, status: 'error', error: message }));
|
|
118
|
+
if (failed?.status === 'error') {
|
|
119
|
+
this.opts.on?.error?.(failed, error);
|
|
120
|
+
}
|
|
110
121
|
}
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
throw new Error(`UploadMediaStore: file ${id} not found`);
|
|
122
|
+
failAll(files, error) {
|
|
123
|
+
for (const entry of files) {
|
|
124
|
+
this.fail(entry.id, error);
|
|
115
125
|
}
|
|
116
|
-
|
|
126
|
+
this.opts.on?.done?.();
|
|
127
|
+
}
|
|
128
|
+
transition(id, next) {
|
|
129
|
+
let updated;
|
|
130
|
+
this._files = this._files.map((f) => {
|
|
131
|
+
if (f.id !== id) {
|
|
132
|
+
return f;
|
|
133
|
+
}
|
|
134
|
+
updated = next(f);
|
|
135
|
+
return updated;
|
|
136
|
+
});
|
|
137
|
+
return updated;
|
|
117
138
|
}
|
|
118
139
|
}
|
|
@@ -1,57 +1,42 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Domain tags for blob uploads. Empty by default — the consumer augments via TypeScript
|
|
3
|
-
* declaration merging at app bootstrap to get type-safe `kind` strings everywhere.
|
|
4
|
-
*
|
|
5
|
-
* @example
|
|
6
|
-
* ```ts
|
|
7
|
-
* // app-side (e.g. src/app.d.ts)
|
|
8
|
-
* declare module '@streamscloud/kit/core/files' {
|
|
9
|
-
* interface BlobKinds {
|
|
10
|
-
* avatar: never;
|
|
11
|
-
* asset: never;
|
|
12
|
-
* organization: never;
|
|
13
|
-
* }
|
|
14
|
-
* }
|
|
15
|
-
*
|
|
16
|
-
* // Now type-checked at all use sites:
|
|
17
|
-
* new UploadMediaStore({ kind: 'avatar' }); // ✓
|
|
18
|
-
* new UploadMediaStore({ kind: 'typo' }); // ✗ TS error
|
|
19
|
-
* ```
|
|
20
|
-
*
|
|
21
|
-
* Without augmentation, `BlobKind` falls back to `string` so the API stays usable.
|
|
22
|
-
*/
|
|
23
|
-
export interface BlobKinds {
|
|
24
|
-
}
|
|
25
|
-
export type BlobKind = keyof BlobKinds extends never ? string : keyof BlobKinds;
|
|
26
1
|
/** A single presigned blob upload slot returned by a `BlobUploadStrategy`. */
|
|
27
2
|
export type BlobUpload = {
|
|
28
3
|
id: string;
|
|
29
|
-
|
|
4
|
+
/** Presigned URL the blob is PUT to. */
|
|
5
|
+
uploadUrl: string;
|
|
6
|
+
/** URL the uploaded blob can be read back from. */
|
|
7
|
+
readUrl: string;
|
|
30
8
|
};
|
|
31
9
|
/**
|
|
32
|
-
* Async producer of presigned blob upload slots
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
10
|
+
* Async producer of presigned blob upload slots, passed to the `UploadMediaStore` constructor.
|
|
11
|
+
* Called by `UploadMediaStore.upload()` with the queued file count; the strategy decides where
|
|
12
|
+
* and how to allocate the slots (e.g. a GraphQL mutation against the consumer's backend) and
|
|
13
|
+
* returns one `BlobUpload` per requested file.
|
|
36
14
|
*/
|
|
37
|
-
export type BlobUploadStrategy = (
|
|
38
|
-
count: number;
|
|
39
|
-
kind?: BlobKind;
|
|
40
|
-
}) => Promise<BlobUpload[]>;
|
|
15
|
+
export type BlobUploadStrategy = (count: number) => Promise<BlobUpload[]>;
|
|
41
16
|
/** Lifecycle status of a single file flowing through `UploadMediaStore`. */
|
|
42
17
|
export type UploadingFileStatus = 'queued' | 'uploading' | 'success' | 'error';
|
|
43
|
-
|
|
44
|
-
* Canonical shape passed to `FileRow` / `FileUploadProgress` and emitted by `UploadMediaStore`.
|
|
45
|
-
* Fields update reactively as the upload progresses.
|
|
46
|
-
*/
|
|
47
|
-
export type UploadingFile = {
|
|
18
|
+
type UploadingFileBase = {
|
|
48
19
|
id: string;
|
|
49
20
|
file: File;
|
|
50
21
|
/** 0..1 — fraction uploaded. 1 on success. */
|
|
51
22
|
progress: number;
|
|
52
|
-
status: UploadingFileStatus;
|
|
53
|
-
/** Set when `status === 'success'`. */
|
|
54
|
-
blobId?: string;
|
|
55
|
-
/** Set when `status === 'error'`. */
|
|
56
|
-
error?: string;
|
|
57
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Canonical shape passed to `FileRow` / `FileUploadProgress` and emitted by `UploadMediaStore`,
|
|
26
|
+
* discriminated by `status`. The `success` variant carries the resolved `blobId` / `readUrl`
|
|
27
|
+
* and the `error` variant carries the message, so consumers narrow on `status` instead of
|
|
28
|
+
* null-checking optional fields. Fields update reactively as the upload progresses.
|
|
29
|
+
*/
|
|
30
|
+
export type UploadingFile = (UploadingFileBase & {
|
|
31
|
+
status: 'queued';
|
|
32
|
+
}) | (UploadingFileBase & {
|
|
33
|
+
status: 'uploading';
|
|
34
|
+
}) | (UploadingFileBase & {
|
|
35
|
+
status: 'success';
|
|
36
|
+
blobId: string;
|
|
37
|
+
readUrl: string;
|
|
38
|
+
}) | (UploadingFileBase & {
|
|
39
|
+
status: 'error';
|
|
40
|
+
error: string;
|
|
41
|
+
});
|
|
42
|
+
export {};
|
|
@@ -20,8 +20,8 @@ const sizeText = $derived(StringHelper.toFileSizeString(file.file.size));
|
|
|
20
20
|
<div class="file-row__name">{file.file.name}</div>
|
|
21
21
|
{#if isUploading}
|
|
22
22
|
<div class="file-row__sub">{percent}% · {localization.uploadingHint}</div>
|
|
23
|
-
{:else if
|
|
24
|
-
<div class="file-row__sub file-row__sub--error">{file.error
|
|
23
|
+
{:else if file.status === 'error'}
|
|
24
|
+
<div class="file-row__sub file-row__sub--error">{file.error}</div>
|
|
25
25
|
{:else}
|
|
26
26
|
<div class="file-row__sub">{sizeText}</div>
|
|
27
27
|
{/if}
|
|
@@ -22,8 +22,8 @@ const isError = $derived(file.status === 'error');
|
|
|
22
22
|
<div class="file-upload-progress__bar" aria-hidden="true">
|
|
23
23
|
<span style:width="{percent}%"></span>
|
|
24
24
|
</div>
|
|
25
|
-
{#if
|
|
26
|
-
<div class="file-upload-progress__sub file-upload-progress__sub--error">{file.error
|
|
25
|
+
{#if file.status === 'error'}
|
|
26
|
+
<div class="file-upload-progress__sub file-upload-progress__sub--error">{file.error}</div>
|
|
27
27
|
{:else}
|
|
28
28
|
<div class="file-upload-progress__sub">{sizeText}</div>
|
|
29
29
|
{/if}
|
|
@@ -2,6 +2,8 @@ import type { UploadingFile, UploadingFileStatus } from '../../core/files';
|
|
|
2
2
|
/**
|
|
3
3
|
* Wrap a raw `File` into an `UploadingFile` for ad-hoc rendering with `FileRow` /
|
|
4
4
|
* `FileUploadProgress` (e.g. static read-only file lists, mocks in stories). Defaults to
|
|
5
|
-
* `status: 'success'` and `progress: 1` — i.e. "already complete, just show me".
|
|
5
|
+
* `status: 'success'` and `progress: 1` — i.e. "already complete, just show me". The display
|
|
6
|
+
* components read only `file` / `progress` / `status` / `error`, so the `success` variant's
|
|
7
|
+
* `blobId` / `readUrl` are empty placeholders here — this shim has no backing blob.
|
|
6
8
|
*/
|
|
7
9
|
export declare const toUploadingFile: (file: File, status?: UploadingFileStatus) => UploadingFile;
|
|
@@ -1,12 +1,24 @@
|
|
|
1
|
+
import { Utils } from '../../core/utils';
|
|
1
2
|
import { nanoid } from 'nanoid';
|
|
2
3
|
/**
|
|
3
4
|
* Wrap a raw `File` into an `UploadingFile` for ad-hoc rendering with `FileRow` /
|
|
4
5
|
* `FileUploadProgress` (e.g. static read-only file lists, mocks in stories). Defaults to
|
|
5
|
-
* `status: 'success'` and `progress: 1` — i.e. "already complete, just show me".
|
|
6
|
+
* `status: 'success'` and `progress: 1` — i.e. "already complete, just show me". The display
|
|
7
|
+
* components read only `file` / `progress` / `status` / `error`, so the `success` variant's
|
|
8
|
+
* `blobId` / `readUrl` are empty placeholders here — this shim has no backing blob.
|
|
6
9
|
*/
|
|
7
|
-
export const toUploadingFile = (file, status = 'success') =>
|
|
8
|
-
id: nanoid(),
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
export const toUploadingFile = (file, status = 'success') => {
|
|
11
|
+
const base = { id: nanoid(), file };
|
|
12
|
+
switch (status) {
|
|
13
|
+
case 'queued':
|
|
14
|
+
return { ...base, progress: 0, status };
|
|
15
|
+
case 'uploading':
|
|
16
|
+
return { ...base, progress: 0, status };
|
|
17
|
+
case 'success':
|
|
18
|
+
return { ...base, progress: 1, status, blobId: '', readUrl: '' };
|
|
19
|
+
case 'error':
|
|
20
|
+
return { ...base, progress: 0, status, error: '' };
|
|
21
|
+
default:
|
|
22
|
+
return Utils.assertUnreachable(status);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -13,11 +13,11 @@ A placeholder image stub showing a generic image icon, used as a fallback when n
|
|
|
13
13
|
### CSS Custom Properties
|
|
14
14
|
| Property | Description | Default |
|
|
15
15
|
|---|---|---|
|
|
16
|
-
| `--sc-kit--image-stub--color` | Icon
|
|
16
|
+
| `--sc-kit--image-stub--color` | Icon color | `var(--sc-kit--color--text--on-accent)` |
|
|
17
17
|
-->
|
|
18
18
|
|
|
19
19
|
<style>.image-stub {
|
|
20
|
-
--_image-stub--color: var(--sc-kit--image-stub--color, var(--sc-kit--color--
|
|
20
|
+
--_image-stub--color: var(--sc-kit--image-stub--color, var(--sc-kit--color--text--on-accent));
|
|
21
21
|
width: 100%;
|
|
22
22
|
height: 100%;
|
|
23
23
|
z-index: 2;
|
|
@@ -25,6 +25,4 @@ A placeholder image stub showing a generic image icon, used as a fallback when n
|
|
|
25
25
|
justify-content: center;
|
|
26
26
|
align-items: center;
|
|
27
27
|
color: var(--_image-stub--color);
|
|
28
|
-
border: 1px solid var(--_image-stub--color);
|
|
29
|
-
border-radius: 0.25rem;
|
|
30
28
|
}</style>
|
|
@@ -17,7 +17,7 @@ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> =
|
|
|
17
17
|
* ### CSS Custom Properties
|
|
18
18
|
* | Property | Description | Default |
|
|
19
19
|
* |---|---|---|
|
|
20
|
-
* | `--sc-kit--image-stub--color` | Icon
|
|
20
|
+
* | `--sc-kit--image-stub--color` | Icon color | `var(--sc-kit--color--text--on-accent)` |
|
|
21
21
|
*/
|
|
22
22
|
declare const Cmp: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
23
23
|
[evt: string]: CustomEvent<any>;
|
|
@@ -7,9 +7,8 @@ import { isSelectGroup } from './types';
|
|
|
7
7
|
const { options, value, compare = defaultCompare, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = false, groupHeader = 'toggle-all', selectionMode = 'children-only', selectedDisplay = 'checkbox', canCreate, icon, chevronIcon, optionSnippet, selectionSnippet, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
|
|
8
8
|
const fuse = $derived(buildFuseIndex(options));
|
|
9
9
|
const loadOptionsShim = (q) => Promise.resolve(runFuseSearch(fuse, q, options));
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
const parentSource = $derived(options.filter(isSelectGroup).reduce((acc, g) => (g.value !== undefined ? [...acc, { label: g.label, value: g.value }] : acc), []));
|
|
10
|
+
// Include flat roots, not only groups — else a childless root can't be picked as a create-parent.
|
|
11
|
+
const parentSource = $derived(options.flatMap((item) => (item.value !== undefined ? [{ label: item.label, value: item.value }] : [])));
|
|
13
12
|
const resolvedValue = $derived.by(() => {
|
|
14
13
|
const out = [];
|
|
15
14
|
for (const v of value) {
|
|
@@ -57,7 +57,7 @@ declare function $$render<T>(): {
|
|
|
57
57
|
selectedDisplay?: "checkmark" | "checkbox" | "highlight";
|
|
58
58
|
/** Equality comparator for `T`. Required when `T` is an object. @default (a, b) => a === b */
|
|
59
59
|
compare?: (a: T, b: T) => boolean;
|
|
60
|
-
/** Enables creatable mode. The predicate validates each query. When
|
|
60
|
+
/** Enables creatable mode. The predicate validates each query. When at least one top-level root has a `value`, the "Create …" UI expands into a section whose parent picker offers those roots (group or flat); otherwise a plain "Create …" row is shown. */
|
|
61
61
|
canCreate?: (query: string) => boolean;
|
|
62
62
|
/** Leading icon — string SVG source, `{ src, color?, size? }` object, or custom snippet. */
|
|
63
63
|
icon?: IconProp;
|
|
@@ -80,7 +80,7 @@ declare function $$render<T>(): {
|
|
|
80
80
|
on?: {
|
|
81
81
|
/** Fires when the user picks/unpicks (and on group toggle-all). Emits the full new selection (array of values). */
|
|
82
82
|
change?: (value: T[]) => void;
|
|
83
|
-
/** Fires when the user confirms the create-with-parent section. `parentValue` is the
|
|
83
|
+
/** Fires when the user confirms the create-with-parent section. `parentValue` is the chosen parent root's `value` from the parent dropdown — undefined when no parent was selected. Return a Promise to keep the spinner up. */
|
|
84
84
|
create?: (payload: {
|
|
85
85
|
query: string;
|
|
86
86
|
parentValue?: T;
|
|
@@ -103,8 +103,8 @@ const isExternalChips = $derived(chipMode === 'external');
|
|
|
103
103
|
const inlineVisible = $derived(isExternalChips || selectionSnippet ? [] : value.slice(0, maxVisible));
|
|
104
104
|
const inlineOverflow = $derived(isExternalChips || selectionSnippet ? 0 : Math.max(0, value.length - maxVisible));
|
|
105
105
|
// Create-with-parent section: shown when canCreate is gated true by core AND the proxy
|
|
106
|
-
// passed a non-empty `parentSource`. The source is the FULL unfiltered
|
|
107
|
-
// parent picker is stable regardless of which
|
|
106
|
+
// passed a non-empty `parentSource`. The source is the FULL unfiltered parent-candidate list
|
|
107
|
+
// so the parent picker is stable regardless of which roots currently survive the listbox filter.
|
|
108
108
|
const parentSourceList = $derived(parentSource ?? []);
|
|
109
109
|
const showCreateSection = $derived(core.canShowCreate && parentSourceList.length > 0);
|
|
110
110
|
let selectedParent = $state.raw(null);
|
|
@@ -156,7 +156,7 @@ export type MultiselectBaseProps<T> = {
|
|
|
156
156
|
* @default 'checkmark'
|
|
157
157
|
*/
|
|
158
158
|
selectedDisplay?: 'checkmark' | 'checkbox' | 'highlight';
|
|
159
|
-
/** Enables creatable mode. The predicate validates each query — only when it returns true does the create UI appear. When
|
|
159
|
+
/** Enables creatable mode. The predicate validates each query — only when it returns true does the create UI appear. When `parentSource` is non-empty, a create-with-parent section replaces the simple "Create …" row. */
|
|
160
160
|
canCreate?: (query: string) => boolean;
|
|
161
161
|
/** Where chips render. `'external'` puts them in a row above the trigger and unlocks `reorderable`. @default 'inline' */
|
|
162
162
|
chipMode?: 'inline' | 'external';
|
|
@@ -190,8 +190,8 @@ export type MultiselectBaseProps<T> = {
|
|
|
190
190
|
}]>;
|
|
191
191
|
/**
|
|
192
192
|
* Enables the create-with-parent section (shown above the listbox when `canCreate` gates true).
|
|
193
|
-
* Receives the FULL
|
|
194
|
-
*
|
|
193
|
+
* Receives the FULL parent-candidate list independent of current filter — so the parent picker offers
|
|
194
|
+
* every candidate regardless of what's currently visible in the listbox. When undefined or empty, a
|
|
195
195
|
* single "Create …" row is shown instead.
|
|
196
196
|
*/
|
|
197
197
|
parentSource?: SelectOption<T>[];
|
|
@@ -204,7 +204,7 @@ export type MultiselectBaseProps<T> = {
|
|
|
204
204
|
on?: {
|
|
205
205
|
/** Fires when the user picks/unpicks/reorders. Emits the full new selection. */
|
|
206
206
|
change?: (value: SelectOption<T>[]) => void;
|
|
207
|
-
/** Fires when the user activates "Create …". When `parentValue` is present, the user picked it via the create-with-parent section
|
|
207
|
+
/** Fires when the user activates "Create …". When `parentValue` is present, the user picked it via the create-with-parent section. Return a Promise to keep the spinner up until the consumer finishes. */
|
|
208
208
|
create?: (payload: {
|
|
209
209
|
query: string;
|
|
210
210
|
parentValue?: T;
|
package/package.json
CHANGED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { BlobUploadStrategy } from './upload-types';
|
|
2
|
-
/**
|
|
3
|
-
* Global registry for the blob upload strategy. Consumers register exactly one strategy at app
|
|
4
|
-
* bootstrap; `UploadMediaStore` reads from this registry on `.upload()`. Mirrors the Toastr /
|
|
5
|
-
* DialogHost pattern — one configured instance, accessible from anywhere.
|
|
6
|
-
*
|
|
7
|
-
* A per-instance override on `UploadMediaStore` is still available for tests and exotic
|
|
8
|
-
* contexts (e.g. mock backends in stories).
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```ts
|
|
12
|
-
* // app bootstrap:
|
|
13
|
-
* BlobStorage.register(async ({ count, kind }) => {
|
|
14
|
-
* const type = kindToGraphqlType(kind);
|
|
15
|
-
* const r = await graphql.mutation(CreateBlobUploads, { type, count });
|
|
16
|
-
* return r.data.createBlobUploads;
|
|
17
|
-
* });
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
declare class BlobStorageRegistry {
|
|
21
|
-
private _strategy;
|
|
22
|
-
get strategy(): BlobUploadStrategy;
|
|
23
|
-
get isRegistered(): boolean;
|
|
24
|
-
register(strategy: BlobUploadStrategy): void;
|
|
25
|
-
unregister(): void;
|
|
26
|
-
}
|
|
27
|
-
export declare const BlobStorage: BlobStorageRegistry;
|
|
28
|
-
export {};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Global registry for the blob upload strategy. Consumers register exactly one strategy at app
|
|
3
|
-
* bootstrap; `UploadMediaStore` reads from this registry on `.upload()`. Mirrors the Toastr /
|
|
4
|
-
* DialogHost pattern — one configured instance, accessible from anywhere.
|
|
5
|
-
*
|
|
6
|
-
* A per-instance override on `UploadMediaStore` is still available for tests and exotic
|
|
7
|
-
* contexts (e.g. mock backends in stories).
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```ts
|
|
11
|
-
* // app bootstrap:
|
|
12
|
-
* BlobStorage.register(async ({ count, kind }) => {
|
|
13
|
-
* const type = kindToGraphqlType(kind);
|
|
14
|
-
* const r = await graphql.mutation(CreateBlobUploads, { type, count });
|
|
15
|
-
* return r.data.createBlobUploads;
|
|
16
|
-
* });
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
class BlobStorageRegistry {
|
|
20
|
-
_strategy = $state.raw(null);
|
|
21
|
-
get strategy() {
|
|
22
|
-
if (!this._strategy) {
|
|
23
|
-
throw new Error('BlobStorage: no strategy registered. Call BlobStorage.register(...) at app bootstrap.');
|
|
24
|
-
}
|
|
25
|
-
return this._strategy;
|
|
26
|
-
}
|
|
27
|
-
get isRegistered() {
|
|
28
|
-
return this._strategy !== null;
|
|
29
|
-
}
|
|
30
|
-
register(strategy) {
|
|
31
|
-
this._strategy = strategy;
|
|
32
|
-
}
|
|
33
|
-
unregister() {
|
|
34
|
-
this._strategy = null;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
export const BlobStorage = new BlobStorageRegistry();
|