@streamscloud/kit 0.85.1 → 0.87.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/README.md +2 -2
- package/dist/core/files/file-decorators.d.ts +3 -0
- package/dist/core/files/file-decorators.js +2 -0
- package/dist/core/files/file-validation-rules.d.ts +3 -8
- package/dist/core/files/file-with-blob-data-helper.d.ts +1 -1
- package/dist/core/files/file-with-blob-data-helper.js +1 -1
- package/dist/core/files/image-resizer.d.ts +17 -29
- package/dist/core/files/image-resizer.js +30 -57
- package/dist/core/files/index.d.ts +4 -4
- package/dist/core/files/index.js +2 -2
- package/dist/core/files/types.d.ts +14 -2
- package/dist/core/files/types.js +1 -10
- package/dist/core/files/upload-media-store.svelte.d.ts +11 -17
- package/dist/core/files/upload-media-store.svelte.js +32 -35
- package/dist/styles/_mixins.scss +71 -17
- package/dist/styles/reset.css +19 -12
- package/dist/ui/ai-panel/cmp.ai-panel.svelte +27 -0
- package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte +8 -12
- package/dist/ui/cropper/image-editor-dialog/crop-and-resize.d.ts +6 -13
- package/dist/ui/cropper/image-editor-dialog/crop-and-resize.js +6 -18
- package/dist/ui/cropper/image-editor-dialog/open-image-editor-dialog.d.ts +6 -1
- package/dist/ui/cropper/image-editor-dialog/open-image-editor-dialog.js +6 -1
- package/dist/ui/cropper/image-editor-dialog/types.d.ts +8 -5
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.d.ts +6 -2
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.js +18 -11
- package/dist/ui/date-picker/cmp.date-time-picker.svelte +67 -8
- package/dist/ui/date-picker/cmp.date-time-picker.svelte.d.ts +8 -5
- package/dist/ui/date-picker/cmp.time-picker.svelte +3 -2
- package/dist/ui/date-picker/cmp.time-picker.svelte.d.ts +2 -0
- package/dist/ui/dialog/cmp.dialog-container.svelte +19 -12
- package/dist/ui/dialog/cmp.dialog.svelte +19 -12
- package/dist/ui/dialog/layout/cmp.stepper-dialog.svelte +54 -0
- package/dist/ui/emoji-picker/cmp.emoji-panel.svelte +19 -12
- package/dist/ui/file-uploader/cmp.upload-progress-toaster.svelte +20 -11
- package/dist/ui/grouped-sidebar/cmp.grouped-sidebar.svelte +20 -11
- package/dist/ui/navigation/cmp.nav-menu.svelte +20 -11
- package/dist/ui/page-layout/cmp.page-layout.svelte +38 -24
- package/dist/ui/page-layout/cmp.page-panel-sidebars.svelte +40 -22
- package/dist/ui/page-layout/cmp.page-panel.svelte +20 -11
- package/dist/ui/popover/cmp.popover.svelte +19 -12
- package/dist/ui/select/cmp.input-suggest.svelte +1 -0
- package/dist/ui/select/cmp.singleselect-async.svelte +1 -0
- package/dist/ui/select/multiselect-base.svelte +1 -0
- package/dist/ui/select/select-core.svelte.js +41 -11
- package/dist/ui/select/select-listbox.svelte +42 -19
- package/dist/ui/select/select-listbox.svelte.d.ts +5 -3
- package/dist/ui/table/table-core.svelte +19 -12
- package/dist/ui/textarea/cmp.textarea.svelte +20 -11
- package/package.json +4 -7
package/README.md
CHANGED
|
@@ -15,8 +15,8 @@ without it agent sessions run with no hooks and no shared skills. Re-run after c
|
|
|
15
15
|
## Checks
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
npm run check # svelte-check + tsc for node files
|
|
19
|
-
npm run lint
|
|
18
|
+
npm run check # the gate: strict svelte-check + tsc for node files + eslint + prettier --check
|
|
19
|
+
npm run lint:fix # eslint --fix + prettier --write
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Release
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AspectRatioBound, FileValidationRule, VideoOrientationValue } from './file-validation-types';
|
|
2
|
+
import type { ResizeLimits } from './types';
|
|
2
3
|
/**
|
|
3
4
|
* Built-in rule factories. Each takes a constraint and an optional `message`; when omitted, the
|
|
4
5
|
* rule falls back to a localized default from `FileValidationLocalization` (kit ships no English
|
|
@@ -21,10 +22,7 @@ export declare const FileRules: {
|
|
|
21
22
|
AudioDuration: (maxSeconds: number, message?: string) => FileValidationRule;
|
|
22
23
|
AudioOnly: (message?: string) => FileValidationRule;
|
|
23
24
|
Extensions: (extensions: string[], message?: string) => FileValidationRule;
|
|
24
|
-
ImageDimensions: (limits:
|
|
25
|
-
max1: number;
|
|
26
|
-
max2: number;
|
|
27
|
-
}, message?: string) => FileValidationRule;
|
|
25
|
+
ImageDimensions: (limits: ResizeLimits, message?: string) => FileValidationRule;
|
|
28
26
|
ImageMinDimensions: (limits: {
|
|
29
27
|
min1: number;
|
|
30
28
|
min2: number;
|
|
@@ -55,9 +53,6 @@ export declare const FileRules: {
|
|
|
55
53
|
VideoOrientation: (orientation: VideoOrientationValue, message?: string, options?: {
|
|
56
54
|
tolerancePercent?: number;
|
|
57
55
|
}) => FileValidationRule;
|
|
58
|
-
VideoDimensions: (limits:
|
|
59
|
-
max1: number;
|
|
60
|
-
max2: number;
|
|
61
|
-
}, message?: string) => FileValidationRule;
|
|
56
|
+
VideoDimensions: (limits: ResizeLimits, message?: string) => FileValidationRule;
|
|
62
57
|
VideoDuration: (maxSeconds: number, message?: string) => FileValidationRule;
|
|
63
58
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Base64Helper } from './base64-helper';
|
|
2
|
+
import { toBlobWithName } from './file-decorators';
|
|
2
3
|
import { FileHelper } from './file-helper';
|
|
3
4
|
import { fetchFile } from './file-service';
|
|
4
|
-
import { toBlobWithName } from './types';
|
|
5
5
|
import { default as mime } from 'mime';
|
|
6
6
|
import { nanoid } from 'nanoid';
|
|
7
7
|
export class FileWithBlobDataHelper {
|
|
@@ -1,31 +1,19 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import type { ResizedBlob, ResizeLimits, SizedFile } from './types';
|
|
2
|
+
export declare const DEFAULT_RESIZE_LIMITS: Readonly<ResizeLimits>;
|
|
3
|
+
type PixelSize = {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
};
|
|
7
|
+
/** `size` rounded down to whole pixels, at least one per side. */
|
|
8
|
+
export declare const toPixelSize: (size: PixelSize) => PixelSize;
|
|
9
|
+
/** `size` downscaled, keeping its aspect ratio, until it fits `limits` (never enlarged), in whole pixels. */
|
|
10
|
+
export declare const fitSize: (size: PixelSize, limits: ResizeLimits) => PixelSize;
|
|
11
|
+
/** `resizeBlob` for a file: always a new `File` under the source's name, with its size. */
|
|
12
|
+
export declare const resizeImage: (file: File, limits: ResizeLimits) => Promise<SizedFile>;
|
|
6
13
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* and which one applies to the short side of the image.
|
|
11
|
-
*
|
|
12
|
-
* - If the image already fits within the constraints (both sides are within their respective limits),
|
|
13
|
-
* it is returned unchanged.
|
|
14
|
-
* - If both constraints are provided, the image is resized so that the long side matches its limit.
|
|
15
|
-
* However, if this causes the short side to exceed its limit, the short side constraint is applied instead.
|
|
16
|
-
* - If only one constraint is provided, it is applied to the longer side, and the other side is scaled proportionally.
|
|
17
|
-
* - If both constraints are zero or negative, the original image is returned without modifications.
|
|
18
|
-
*
|
|
19
|
-
* @param blob - The original image as a `Blob` object.
|
|
20
|
-
* @param limits - An object containing two numeric constraints, `max1` and `max2`.
|
|
21
|
-
* - **`max1`** - One of the two size constraints.
|
|
22
|
-
* - **`max2`** - The second size constraint.
|
|
23
|
-
* **Note:** `max1` and `max2` are not tied to specific dimensions (width or height).
|
|
24
|
-
* The function determines which one applies to the long side and which to the short side.
|
|
25
|
-
*
|
|
26
|
-
* @returns A promise that resolves to a new resized `Blob` object, or the original `Blob` if resizing is not needed.
|
|
14
|
+
* Downscales an image, keeping its aspect ratio, until both sides fit `limits` (see `ResizeLimits`).
|
|
15
|
+
* Only JPEG, PNG and WebP are re-encoded; an image that already fits, or of any other type (SVG, GIF, …),
|
|
16
|
+
* comes back as the very same `Blob`. Either way the result carries the image's pixel size.
|
|
27
17
|
*/
|
|
28
|
-
export declare const resizeBlob:
|
|
29
|
-
|
|
30
|
-
max2: number;
|
|
31
|
-
}) => Promise<Blob>;
|
|
18
|
+
export declare const resizeBlob: (blob: Blob, limits: ResizeLimits) => Promise<ResizedBlob>;
|
|
19
|
+
export {};
|
|
@@ -1,70 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
1
|
+
export const DEFAULT_RESIZE_LIMITS = Object.freeze({ max1: 1600, max2: 1000 });
|
|
2
|
+
const RESIZABLE_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
|
|
3
|
+
// A scaled side lands a hair under a whole number (999.9999999999999); a bare floor would drop a pixel.
|
|
4
|
+
const PIXEL_EPSILON = 1e-6;
|
|
5
|
+
/** `size` rounded down to whole pixels, at least one per side. */
|
|
6
|
+
export const toPixelSize = (size) => ({
|
|
7
|
+
width: Math.max(1, Math.floor(size.width + PIXEL_EPSILON)),
|
|
8
|
+
height: Math.max(1, Math.floor(size.height + PIXEL_EPSILON))
|
|
9
|
+
});
|
|
10
|
+
/** `size` downscaled, keeping its aspect ratio, until it fits `limits` (never enlarged), in whole pixels. */
|
|
11
|
+
export const fitSize = (size, limits) => {
|
|
12
|
+
const [longMax, shortMax] = limits.max1 >= limits.max2 ? [limits.max1, limits.max2] : [limits.max2, limits.max1];
|
|
13
|
+
const scale = Math.min(1, longMax / Math.max(size.width, size.height), shortMax / Math.min(size.width, size.height));
|
|
14
|
+
return toPixelSize({ width: size.width * scale, height: size.height * scale });
|
|
15
|
+
};
|
|
16
|
+
/** `resizeBlob` for a file: always a new `File` under the source's name, with its size. */
|
|
3
17
|
export const resizeImage = async (file, limits) => {
|
|
4
|
-
const blob = await resizeBlob(file, limits);
|
|
5
|
-
|
|
6
|
-
if (!(blob instanceof File)) {
|
|
7
|
-
result.name = file.name;
|
|
8
|
-
}
|
|
9
|
-
return result;
|
|
18
|
+
const { blob, width, height } = await resizeBlob(file, limits);
|
|
19
|
+
return Object.assign(new File([blob], file.name, { type: blob.type }), { width, height });
|
|
10
20
|
};
|
|
11
21
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* and which one applies to the short side of the image.
|
|
16
|
-
*
|
|
17
|
-
* - If the image already fits within the constraints (both sides are within their respective limits),
|
|
18
|
-
* it is returned unchanged.
|
|
19
|
-
* - If both constraints are provided, the image is resized so that the long side matches its limit.
|
|
20
|
-
* However, if this causes the short side to exceed its limit, the short side constraint is applied instead.
|
|
21
|
-
* - If only one constraint is provided, it is applied to the longer side, and the other side is scaled proportionally.
|
|
22
|
-
* - If both constraints are zero or negative, the original image is returned without modifications.
|
|
23
|
-
*
|
|
24
|
-
* @param blob - The original image as a `Blob` object.
|
|
25
|
-
* @param limits - An object containing two numeric constraints, `max1` and `max2`.
|
|
26
|
-
* - **`max1`** - One of the two size constraints.
|
|
27
|
-
* - **`max2`** - The second size constraint.
|
|
28
|
-
* **Note:** `max1` and `max2` are not tied to specific dimensions (width or height).
|
|
29
|
-
* The function determines which one applies to the long side and which to the short side.
|
|
30
|
-
*
|
|
31
|
-
* @returns A promise that resolves to a new resized `Blob` object, or the original `Blob` if resizing is not needed.
|
|
22
|
+
* Downscales an image, keeping its aspect ratio, until both sides fit `limits` (see `ResizeLimits`).
|
|
23
|
+
* Only JPEG, PNG and WebP are re-encoded; an image that already fits, or of any other type (SVG, GIF, …),
|
|
24
|
+
* comes back as the very same `Blob`. Either way the result carries the image's pixel size.
|
|
32
25
|
*/
|
|
33
26
|
export const resizeBlob = (blob, limits) => {
|
|
34
|
-
const { max1, max2 } = limits;
|
|
35
|
-
if (max1 <= 0 && max2 <= 0) {
|
|
36
|
-
return Promise.resolve(blob);
|
|
37
|
-
}
|
|
38
27
|
return new Promise((resolve, reject) => {
|
|
39
28
|
const image = new Image();
|
|
40
29
|
image.src = URL.createObjectURL(blob);
|
|
41
30
|
image.onload = () => {
|
|
42
31
|
const { width, height } = image;
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const originalShortSide = isHorizontal ? height : width;
|
|
46
|
-
const [longSideMax, shortSideMax] = max1 >= max2 ? [max1, max2] : [max2, max1];
|
|
47
|
-
if (longSideMax > 0 && originalLongSide <= longSideMax && shortSideMax > 0 && originalShortSide <= shortSideMax) {
|
|
48
|
-
URL.revokeObjectURL(image.src);
|
|
49
|
-
return resolve(blob);
|
|
50
|
-
}
|
|
51
|
-
let newLongSide = Math.min(originalLongSide, longSideMax);
|
|
52
|
-
let newShortSide = originalShortSide * (newLongSide / originalLongSide);
|
|
53
|
-
if (shortSideMax > 0 && newShortSide > shortSideMax) {
|
|
54
|
-
const reducer = newShortSide / shortSideMax;
|
|
55
|
-
newShortSide = shortSideMax;
|
|
56
|
-
newLongSide = newLongSide / reducer;
|
|
57
|
-
}
|
|
58
|
-
newLongSide = Math.floor(newLongSide);
|
|
59
|
-
newShortSide = Math.floor(newShortSide);
|
|
60
|
-
// Skip resize if dimensions unchanged (avoids lossy re-encoding)
|
|
61
|
-
if (newLongSide === originalLongSide && newShortSide === originalShortSide) {
|
|
32
|
+
const { width: newWidth, height: newHeight } = fitSize({ width, height }, limits);
|
|
33
|
+
if (!RESIZABLE_TYPES.has(blob.type) || (newWidth === width && newHeight === height)) {
|
|
62
34
|
URL.revokeObjectURL(image.src);
|
|
63
|
-
return resolve(blob);
|
|
35
|
+
return resolve({ blob, width, height });
|
|
64
36
|
}
|
|
65
|
-
const
|
|
66
|
-
const newHeight = isHorizontal ? newShortSide : newLongSide;
|
|
67
|
-
const canvas = getCanvas();
|
|
37
|
+
const canvas = document.createElement('canvas');
|
|
68
38
|
canvas.width = newWidth;
|
|
69
39
|
canvas.height = newHeight;
|
|
70
40
|
const ctx = canvas.getContext('2d');
|
|
@@ -75,9 +45,12 @@ export const resizeBlob = (blob, limits) => {
|
|
|
75
45
|
ctx.drawImage(image, 0, 0, newWidth, newHeight);
|
|
76
46
|
canvas.toBlob((resizedBlob) => {
|
|
77
47
|
URL.revokeObjectURL(image.src);
|
|
78
|
-
|
|
48
|
+
// Safari caps total canvas memory and holds a dropped canvas until GC; shrinking it to 1×1 releases it now.
|
|
49
|
+
canvas.width = 1;
|
|
50
|
+
canvas.height = 1;
|
|
51
|
+
ctx.clearRect(0, 0, 1, 1);
|
|
79
52
|
if (resizedBlob) {
|
|
80
|
-
resolve(resizedBlob);
|
|
53
|
+
resolve({ blob: resizedBlob, width: newWidth, height: newHeight });
|
|
81
54
|
}
|
|
82
55
|
else {
|
|
83
56
|
reject(new Error('Failed to create blob from canvas'));
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
export type { BlobWithName, FileWithBlobUrl, FileWithUploadUrl } from './types';
|
|
1
|
+
export type { BlobWithName, FileWithBlobUrl, FileWithUploadUrl, ResizedBlob, ResizeLimits, SizedFile } from './types';
|
|
2
2
|
export type { AspectRatioBound, FileValidationResult, FileValidationRule, FileValidationRuleSets, VideoOrientationValue } from './file-validation-types';
|
|
3
3
|
export type { BlobUpload, BlobUploadStrategy, PreviewableUploadingFile, UploadPreviewMode, UploadingFile, UploadingFileStatus } from './upload-types';
|
|
4
|
-
export type { AddOptions, UploadMediaStoreOptions } from './upload-media-store.svelte';
|
|
4
|
+
export type { AddOptions, UploadMediaStoreOptions, UploadOutcome } from './upload-media-store.svelte';
|
|
5
5
|
export type { FileTypeCheck } from './file-helper';
|
|
6
|
-
export { toBlobWithName, toFileWithUploadUrl } from './
|
|
6
|
+
export { toBlobWithName, toFileWithUploadUrl } from './file-decorators';
|
|
7
7
|
export { uploadBlob } from './blob-storage';
|
|
8
8
|
export { openFile } from './open-file';
|
|
9
9
|
export { FileHelper } from './file-helper';
|
|
10
10
|
export { downloadBlob, downloadFromUrl, downloadJson, fetchFile } from './file-service';
|
|
11
11
|
export { AcceptFileType, isPreviewableType, matchesAcceptedFileTypes } from './file-types';
|
|
12
12
|
export { deriveAccept } from './file-validation-rule-sets';
|
|
13
|
-
export { resizeBlob, resizeImage } from './image-resizer';
|
|
13
|
+
export { DEFAULT_RESIZE_LIMITS, fitSize, resizeBlob, resizeImage, toPixelSize } from './image-resizer';
|
|
14
14
|
export { FileWithBlobDataHelper } from './file-with-blob-data-helper';
|
|
15
15
|
export { FilesProvider } from './files-provider';
|
|
16
16
|
export { FileValidator } from './file-validator';
|
package/dist/core/files/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Utilities
|
|
2
|
-
export { toBlobWithName, toFileWithUploadUrl } from './
|
|
2
|
+
export { toBlobWithName, toFileWithUploadUrl } from './file-decorators';
|
|
3
3
|
export { uploadBlob } from './blob-storage';
|
|
4
4
|
export { openFile } from './open-file';
|
|
5
5
|
export { FileHelper } from './file-helper';
|
|
6
6
|
export { downloadBlob, downloadFromUrl, downloadJson, fetchFile } from './file-service';
|
|
7
7
|
export { AcceptFileType, isPreviewableType, matchesAcceptedFileTypes } from './file-types';
|
|
8
8
|
export { deriveAccept } from './file-validation-rule-sets';
|
|
9
|
-
export { resizeBlob, resizeImage } from './image-resizer';
|
|
9
|
+
export { DEFAULT_RESIZE_LIMITS, fitSize, resizeBlob, resizeImage, toPixelSize } from './image-resizer';
|
|
10
10
|
export { FileWithBlobDataHelper } from './file-with-blob-data-helper';
|
|
11
11
|
export { FilesProvider } from './files-provider';
|
|
12
12
|
export { FileValidator } from './file-validator';
|
|
@@ -2,11 +2,23 @@ export type FileWithBlobUrl = File & {
|
|
|
2
2
|
blobUrl: string;
|
|
3
3
|
id: string;
|
|
4
4
|
};
|
|
5
|
+
export type SizedFile = File & {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
};
|
|
9
|
+
/** Two positive pixel limits not tied to width / height — the image's longer side fits the larger one, its shorter side the smaller. */
|
|
10
|
+
export type ResizeLimits = {
|
|
11
|
+
max1: number;
|
|
12
|
+
max2: number;
|
|
13
|
+
};
|
|
14
|
+
export type ResizedBlob = {
|
|
15
|
+
blob: Blob;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
};
|
|
5
19
|
export type FileWithUploadUrl<T extends File = File> = T & {
|
|
6
20
|
uploadUrl: string;
|
|
7
21
|
};
|
|
8
|
-
export declare const toFileWithUploadUrl: <T extends File>(file: T, uploadUrl: string) => FileWithUploadUrl<T>;
|
|
9
22
|
export type BlobWithName = Blob & {
|
|
10
23
|
name: string;
|
|
11
24
|
};
|
|
12
|
-
export declare const toBlobWithName: (blob: Blob, name: string) => BlobWithName;
|
package/dist/core/files/types.js
CHANGED
|
@@ -1,10 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
const fileWithUploadUrl = file;
|
|
3
|
-
fileWithUploadUrl.uploadUrl = uploadUrl;
|
|
4
|
-
return fileWithUploadUrl;
|
|
5
|
-
};
|
|
6
|
-
export const toBlobWithName = (blob, name) => {
|
|
7
|
-
const blobWithName = blob;
|
|
8
|
-
blobWithName.name = name;
|
|
9
|
-
return blobWithName;
|
|
10
|
-
};
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResizeLimits } from './types';
|
|
1
2
|
import type { BlobUploadStrategy, PreviewableUploadingFile, UploadPreviewMode, UploadingFile } from './upload-types';
|
|
2
3
|
type SuccessfulUpload = Extract<UploadingFile, {
|
|
3
4
|
status: 'success';
|
|
@@ -16,15 +17,8 @@ export type UploadOutcome = {
|
|
|
16
17
|
succeeded: SuccessfulUpload[];
|
|
17
18
|
};
|
|
18
19
|
export type UploadMediaStoreOptions = {
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
* defaults. `max1` / `max2` are not tied to width / height — the longer side is matched to
|
|
22
|
-
* the larger limit. @default `{ max1: 1600, max2: 1000 }`
|
|
23
|
-
*/
|
|
24
|
-
resize?: {
|
|
25
|
-
max1: number;
|
|
26
|
-
max2: number;
|
|
27
|
-
} | false;
|
|
20
|
+
/** Downscale images to these limits before upload; `false` uploads them as they are. @default DEFAULT_RESIZE_LIMITS (1600 / 1000) */
|
|
21
|
+
resize?: ResizeLimits | false;
|
|
28
22
|
/** Parallel upload limit. @default 20 */
|
|
29
23
|
concurrency?: number;
|
|
30
24
|
on?: {
|
|
@@ -62,7 +56,7 @@ export declare class UploadMediaStore {
|
|
|
62
56
|
private _inFlight;
|
|
63
57
|
private _nextRunId;
|
|
64
58
|
private _strategy;
|
|
65
|
-
private
|
|
59
|
+
private _opts;
|
|
66
60
|
constructor(strategy: BlobUploadStrategy, opts?: UploadMediaStoreOptions);
|
|
67
61
|
/** Reactive read of the queue. */
|
|
68
62
|
get files(): UploadingFile[];
|
|
@@ -102,12 +96,12 @@ export declare class UploadMediaStore {
|
|
|
102
96
|
* with no extra wiring.
|
|
103
97
|
*/
|
|
104
98
|
upload(): Promise<UploadOutcome>;
|
|
105
|
-
private
|
|
106
|
-
private
|
|
107
|
-
private
|
|
108
|
-
private
|
|
109
|
-
private
|
|
110
|
-
private
|
|
111
|
-
private
|
|
99
|
+
private _outcome;
|
|
100
|
+
private _settled;
|
|
101
|
+
private _runQueued;
|
|
102
|
+
private _maybeResize;
|
|
103
|
+
private _fail;
|
|
104
|
+
private _failAll;
|
|
105
|
+
private _transition;
|
|
112
106
|
}
|
|
113
107
|
export {};
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { AppUploadActivity } from './app-upload-activity.svelte';
|
|
2
2
|
import { uploadBlob } from './blob-storage';
|
|
3
3
|
import { isPreviewableType } from './file-types';
|
|
4
|
-
import { resizeBlob } from './image-resizer';
|
|
4
|
+
import { DEFAULT_RESIZE_LIMITS, resizeBlob } from './image-resizer';
|
|
5
5
|
import { nanoid } from 'nanoid';
|
|
6
6
|
import { default as pLimit } from 'p-limit';
|
|
7
|
-
const DEFAULT_RESIZE_LIMITS = { max1: 1600, max2: 1000 };
|
|
8
7
|
const DEFAULT_CONCURRENCY = 20;
|
|
9
8
|
const toPreviewUrl = (file, mode) => mode === 'always' || (mode === 'mime' && isPreviewableType(file.type)) ? URL.createObjectURL(file) : null;
|
|
10
9
|
const revokePreviewUrl = (file) => {
|
|
@@ -38,10 +37,10 @@ export class UploadMediaStore {
|
|
|
38
37
|
_inFlight = new Map();
|
|
39
38
|
_nextRunId = 0;
|
|
40
39
|
_strategy;
|
|
41
|
-
|
|
40
|
+
_opts;
|
|
42
41
|
constructor(strategy, opts = {}) {
|
|
43
42
|
this._strategy = strategy;
|
|
44
|
-
this.
|
|
43
|
+
this._opts = opts;
|
|
45
44
|
}
|
|
46
45
|
/** Reactive read of the queue. */
|
|
47
46
|
get files() {
|
|
@@ -89,19 +88,19 @@ export class UploadMediaStore {
|
|
|
89
88
|
async upload() {
|
|
90
89
|
const queued = this._files.filter((f) => f.status === 'queued' || f.status === 'error');
|
|
91
90
|
if (queued.length === 0) {
|
|
92
|
-
await this.
|
|
93
|
-
return this.
|
|
91
|
+
await this._settled();
|
|
92
|
+
return this._outcome();
|
|
94
93
|
}
|
|
95
94
|
// Claim before the first await, or a second upload() during the strategy call re-claims the same files.
|
|
96
95
|
for (const entry of queued) {
|
|
97
|
-
this.
|
|
96
|
+
this._transition(entry.id, (f) => ({ id: f.id, file: f.file, progress: 0, previewUrl: f.previewUrl, status: 'uploading' }));
|
|
98
97
|
}
|
|
99
98
|
const fileIds = new Set(queued.map((f) => f.id));
|
|
100
99
|
const activityRunId = AppUploadActivity.beginRun(() => this._files.filter((f) => fileIds.has(f.id)));
|
|
101
100
|
const runId = this._nextRunId++;
|
|
102
101
|
const run = (async () => {
|
|
103
102
|
try {
|
|
104
|
-
await this.
|
|
103
|
+
await this._runQueued(queued);
|
|
105
104
|
}
|
|
106
105
|
finally {
|
|
107
106
|
AppUploadActivity.endRun(activityRunId);
|
|
@@ -109,43 +108,43 @@ export class UploadMediaStore {
|
|
|
109
108
|
}
|
|
110
109
|
})();
|
|
111
110
|
this._inFlight.set(runId, run);
|
|
112
|
-
await this.
|
|
113
|
-
return this.
|
|
111
|
+
await this._settled();
|
|
112
|
+
return this._outcome();
|
|
114
113
|
}
|
|
115
|
-
|
|
114
|
+
_outcome() {
|
|
116
115
|
return {
|
|
117
116
|
failed: this._files.filter((f) => f.status === 'error'),
|
|
118
117
|
succeeded: this._files.filter((f) => f.status === 'success')
|
|
119
118
|
};
|
|
120
119
|
}
|
|
121
|
-
async
|
|
120
|
+
async _settled() {
|
|
122
121
|
while (this._inFlight.size > 0) {
|
|
123
122
|
await Promise.all([...this._inFlight.values()]);
|
|
124
123
|
}
|
|
125
124
|
}
|
|
126
|
-
async
|
|
125
|
+
async _runQueued(queued) {
|
|
127
126
|
let presigned;
|
|
128
127
|
try {
|
|
129
128
|
presigned = await this._strategy(queued.length);
|
|
130
129
|
}
|
|
131
130
|
catch (error) {
|
|
132
|
-
this.
|
|
131
|
+
this._failAll(queued, error);
|
|
133
132
|
return;
|
|
134
133
|
}
|
|
135
134
|
if (presigned.length < queued.length) {
|
|
136
|
-
this.
|
|
135
|
+
this._failAll(queued, new Error(`BlobUploadStrategy returned ${presigned.length} slots for ${queued.length} files`));
|
|
137
136
|
return;
|
|
138
137
|
}
|
|
139
|
-
const limit = pLimit(this.
|
|
140
|
-
const resizeOpt = this.
|
|
138
|
+
const limit = pLimit(this._opts.concurrency ?? DEFAULT_CONCURRENCY);
|
|
139
|
+
const resizeOpt = this._opts.resize;
|
|
141
140
|
await Promise.all(queued.map((entry, i) => limit(async () => {
|
|
142
141
|
try {
|
|
143
|
-
const blob = resizeOpt === false ? entry.file : await this.
|
|
142
|
+
const blob = resizeOpt === false ? entry.file : await this._maybeResize(entry.file, resizeOpt ?? DEFAULT_RESIZE_LIMITS);
|
|
144
143
|
const slot = presigned[i];
|
|
145
144
|
await uploadBlob(slot.uploadUrl, blob, (loaded, total) => {
|
|
146
|
-
this.
|
|
145
|
+
this._transition(entry.id, (f) => (f.status === 'uploading' ? { ...f, progress: total > 0 ? loaded / total : 0 } : f));
|
|
147
146
|
});
|
|
148
|
-
const done = this.
|
|
147
|
+
const done = this._transition(entry.id, (f) => ({
|
|
149
148
|
id: f.id,
|
|
150
149
|
file: f.file,
|
|
151
150
|
progress: 1,
|
|
@@ -155,42 +154,40 @@ export class UploadMediaStore {
|
|
|
155
154
|
readUrl: slot.readUrl
|
|
156
155
|
}));
|
|
157
156
|
if (done?.status === 'success') {
|
|
158
|
-
this.
|
|
157
|
+
this._opts.on?.blobId?.(done, slot.id);
|
|
159
158
|
}
|
|
160
159
|
}
|
|
161
160
|
catch (error) {
|
|
162
|
-
this.
|
|
161
|
+
this._fail(entry.id, error);
|
|
163
162
|
}
|
|
164
163
|
})));
|
|
165
|
-
this.
|
|
164
|
+
this._opts.on?.done?.();
|
|
166
165
|
}
|
|
167
|
-
async
|
|
168
|
-
|
|
169
|
-
if (!file.type.startsWith('image/') || file.type === 'image/gif') {
|
|
166
|
+
async _maybeResize(file, limits) {
|
|
167
|
+
if (!file.type.startsWith('image/')) {
|
|
170
168
|
return file;
|
|
171
169
|
}
|
|
172
170
|
try {
|
|
173
|
-
return await resizeBlob(file, limits);
|
|
171
|
+
return (await resizeBlob(file, limits)).blob;
|
|
174
172
|
}
|
|
175
173
|
catch {
|
|
176
|
-
// Resize is best-effort: fall back to the original on failure (e.g. corrupt file).
|
|
177
174
|
return file;
|
|
178
175
|
}
|
|
179
176
|
}
|
|
180
|
-
|
|
177
|
+
_fail(id, error) {
|
|
181
178
|
const message = error instanceof Error ? error.message : 'Upload failed';
|
|
182
|
-
const failed = this.
|
|
179
|
+
const failed = this._transition(id, (f) => ({ id: f.id, file: f.file, progress: f.progress, previewUrl: f.previewUrl, status: 'error', error: message }));
|
|
183
180
|
if (failed?.status === 'error') {
|
|
184
|
-
this.
|
|
181
|
+
this._opts.on?.error?.(failed, error);
|
|
185
182
|
}
|
|
186
183
|
}
|
|
187
|
-
|
|
184
|
+
_failAll(files, error) {
|
|
188
185
|
for (const entry of files) {
|
|
189
|
-
this.
|
|
186
|
+
this._fail(entry.id, error);
|
|
190
187
|
}
|
|
191
|
-
this.
|
|
188
|
+
this._opts.on?.done?.();
|
|
192
189
|
}
|
|
193
|
-
|
|
190
|
+
_transition(id, next) {
|
|
194
191
|
let updated;
|
|
195
192
|
this._files = this._files.map((f) => {
|
|
196
193
|
if (f.id !== id) {
|
package/dist/styles/_mixins.scss
CHANGED
|
@@ -82,14 +82,24 @@
|
|
|
82
82
|
max-height: 100%;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
@mixin scrollbar(
|
|
85
|
+
@mixin scrollbar(
|
|
86
|
+
$thumb-color: var(--scrollbar--thumb-color, light-dark(#d1d5db, #4b5563)),
|
|
87
|
+
$track-color: var(--scrollbar--track-color, transparent),
|
|
88
|
+
$size: 6px,
|
|
89
|
+
$symmetric: null
|
|
90
|
+
) {
|
|
86
91
|
--_cross-browser-scrollbar--thumb-color: #{$thumb-color};
|
|
87
92
|
--_cross-browser-scrollbar--track-color: #{$track-color};
|
|
88
93
|
|
|
89
|
-
@include _cross-browser-scrollbar;
|
|
94
|
+
@include _cross-browser-scrollbar($size, $symmetric);
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
@mixin scrollbar-on-hover(
|
|
97
|
+
@mixin scrollbar-on-hover(
|
|
98
|
+
$thumb-color: var(--scrollbar--thumb-color, light-dark(#d1d5db, #4b5563)),
|
|
99
|
+
$track-color: var(--scrollbar--track-color, transparent),
|
|
100
|
+
$size: 6px,
|
|
101
|
+
$symmetric: null
|
|
102
|
+
) {
|
|
93
103
|
--_cross-browser-scrollbar--thumb-color: transparent;
|
|
94
104
|
--_cross-browser-scrollbar--track-color: transparent;
|
|
95
105
|
|
|
@@ -98,29 +108,73 @@
|
|
|
98
108
|
--_cross-browser-scrollbar--track-color: #{$track-color};
|
|
99
109
|
}
|
|
100
110
|
|
|
101
|
-
@include _cross-browser-scrollbar;
|
|
111
|
+
@include _cross-browser-scrollbar($size, $symmetric);
|
|
102
112
|
}
|
|
103
113
|
|
|
104
|
-
@mixin _cross-browser-scrollbar {
|
|
105
|
-
|
|
106
|
-
width: 6px;
|
|
107
|
-
height: 6px;
|
|
108
|
-
}
|
|
114
|
+
@mixin _cross-browser-scrollbar($size, $symmetric) {
|
|
115
|
+
--_cross-browser-scrollbar--gutter: 0px;
|
|
109
116
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
// a non-auto scrollbar-color / scrollbar-width (own or inherited) makes WebKit / Blink drop the 6px pseudo-element bar for the wider platform one
|
|
118
|
+
@include when-scrollbar-sizable {
|
|
119
|
+
scrollbar-color: auto;
|
|
120
|
+
scrollbar-width: auto;
|
|
121
|
+
|
|
122
|
+
&::-webkit-scrollbar {
|
|
123
|
+
width: $size;
|
|
124
|
+
height: $size;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
&::-webkit-scrollbar-track {
|
|
128
|
+
background: var(--_cross-browser-scrollbar--track-color);
|
|
129
|
+
border-radius: 100vw;
|
|
130
|
+
}
|
|
114
131
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
132
|
+
&::-webkit-scrollbar-thumb {
|
|
133
|
+
background: var(--_cross-browser-scrollbar--thumb-color);
|
|
134
|
+
border-radius: 100vw;
|
|
135
|
+
}
|
|
118
136
|
}
|
|
119
137
|
|
|
120
|
-
@
|
|
138
|
+
@include when-scrollbar-unsizable {
|
|
121
139
|
scrollbar-color: var(--_cross-browser-scrollbar--thumb-color) var(--_cross-browser-scrollbar--track-color);
|
|
122
140
|
scrollbar-width: thin;
|
|
123
141
|
}
|
|
142
|
+
|
|
143
|
+
// not scrollbar-gutter: both-edges here — Safari reserves both gutters on the end side
|
|
144
|
+
@if $symmetric {
|
|
145
|
+
@include when-scrollbar-sizable {
|
|
146
|
+
--_cross-browser-scrollbar--gutter: #{$size};
|
|
147
|
+
|
|
148
|
+
overflow-y: scroll;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@if $symmetric == all {
|
|
153
|
+
@include when-scrollbar-unsizable {
|
|
154
|
+
scrollbar-gutter: stable both-edges;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@mixin when-scrollbar-sizable {
|
|
160
|
+
@supports selector(::-webkit-scrollbar) {
|
|
161
|
+
@content;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// room to reserve for the platform scrollbar where its width can't be set (~11px thin bar + 1px); never subtract it — overlay scrollbars take 0
|
|
166
|
+
$unsizable-scrollbar-allowance: 12px;
|
|
167
|
+
|
|
168
|
+
// no ::-webkit-scrollbar (Firefox): the platform bar can't shrink into a panel padding, so a box sized for the 4–6px bar clips its content unless it adds the allowance here
|
|
169
|
+
@mixin when-scrollbar-unsizable {
|
|
170
|
+
@supports not selector(::-webkit-scrollbar) {
|
|
171
|
+
@content;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@mixin scrollbar-padding-inline($padding) {
|
|
176
|
+
padding-inline-start: max(#{$padding}, var(--_cross-browser-scrollbar--gutter, 0px));
|
|
177
|
+
padding-inline-end: max(0px, calc(#{$padding} - var(--_cross-browser-scrollbar--gutter, 0px)));
|
|
124
178
|
}
|
|
125
179
|
|
|
126
180
|
@mixin fade-in-animation($duration: 5s) {
|