@streamscloud/kit 0.1.10 → 0.1.12-1772032209109
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/toastr/index.d.ts +1 -1
- package/dist/core/toastr/toastr.scss +38 -0
- package/dist/core/toastr/toastr.svelte.d.ts +1 -1
- package/dist/core/toastr/toastr.svelte.js +13 -6
- package/dist/core/toastr/types.d.ts +2 -0
- package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte +9 -8
- package/dist/ui/cropper/image-editor-dialog/index.d.ts +4 -4
- package/dist/ui/cropper/image-editor-dialog/index.js +3 -3
- package/dist/ui/cropper/image-editor-dialog/types.d.ts +12 -12
- package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte +28 -38
- package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte.d.ts +8 -10
- package/dist/ui/cropper/img-cropper/img-cropper-base-worker.svelte.d.ts +40 -0
- package/dist/ui/cropper/img-cropper/img-cropper-base-worker.svelte.js +175 -0
- package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.d.ts +5 -38
- package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.js +29 -149
- package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.d.ts +5 -38
- package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.js +37 -135
- package/dist/ui/cropper/img-cropper/img-cropper-utils.d.ts +11 -1
- package/dist/ui/cropper/img-cropper/img-cropper-utils.js +30 -0
- package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.d.ts +17 -14
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.d.ts +13 -31
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.js +29 -28
- package/dist/ui/cropper/img-cropper/index.d.ts +1 -1
- package/dist/ui/dialog/cmp.dialog-container.svelte +12 -12
- package/dist/ui/dialog/dialog-data.d.ts +2 -0
- package/dist/ui/dialog/dialog-mount.d.ts +1 -1
- package/dist/ui/dialog/dialog-mount.js +2 -2
- package/dist/ui/dialog/dialogs.svelte.d.ts +3 -0
- package/dist/ui/dialog/dialogs.svelte.js +21 -2
- package/dist/ui/dialog/index.d.ts +1 -1
- package/dist/ui/dialog/index.js +1 -1
- package/dist/ui/dialog/types.svelte.d.ts +3 -14
- package/dist/ui/dialog/types.svelte.js +3 -18
- package/dist/ui/form-group/cmp.form-group-label.svelte +25 -0
- package/dist/ui/form-group/cmp.form-group-label.svelte.d.ts +8 -0
- package/dist/ui/form-group/cmp.form-group-note.svelte +16 -0
- package/dist/ui/form-group/cmp.form-group-note.svelte.d.ts +7 -0
- package/dist/ui/form-group/cmp.form-group.svelte +16 -0
- package/dist/ui/form-group/cmp.form-group.svelte.d.ts +8 -0
- package/dist/ui/form-group/index.d.ts +3 -0
- package/dist/ui/form-group/index.js +3 -0
- package/dist/ui/html-block/cmp.html-block.svelte +112 -0
- package/dist/ui/html-block/cmp.html-block.svelte.d.ts +7 -0
- package/dist/ui/html-block/index.d.ts +1 -0
- package/dist/ui/html-block/index.js +1 -0
- package/dist/ui/media-viewer-dialog/cmp.media-viewer-dialog.svelte +50 -0
- package/dist/ui/media-viewer-dialog/cmp.media-viewer-dialog.svelte.d.ts +9 -0
- package/dist/ui/media-viewer-dialog/index.d.ts +13 -0
- package/dist/ui/media-viewer-dialog/index.js +17 -0
- package/dist/ui/media-viewer-dialog/media-viewer-item.svelte +61 -0
- package/dist/ui/media-viewer-dialog/media-viewer-item.svelte.d.ts +7 -0
- package/dist/ui/media-viewer-dialog/types.d.ts +13 -0
- package/dist/ui/media-viewer-dialog/types.js +1 -0
- package/dist/ui/player/carousel/cmp.carousel.svelte +2 -2
- package/dist/ui/player/carousel/cmp.carousel.svelte.d.ts +1 -1
- package/dist/ui/video/cmp.video.svelte +16 -7
- package/package.json +14 -1
|
@@ -26,3 +26,41 @@
|
|
|
26
26
|
color: var(--info-bg);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
// Theme classes — override svelte-sonner color variables per-toast.
|
|
31
|
+
// Applied via the `class` option when an explicit theme is passed to Toastr methods.
|
|
32
|
+
.sc-toast--light {
|
|
33
|
+
--normal-bg: #fff;
|
|
34
|
+
--normal-border: hsl(0, 0%, 93%);
|
|
35
|
+
--normal-text: hsl(0, 0%, 9%);
|
|
36
|
+
--success-bg: hsl(143, 85%, 96%);
|
|
37
|
+
--success-border: hsl(145, 92%, 87%);
|
|
38
|
+
--success-text: hsl(140, 100%, 27%);
|
|
39
|
+
--info-bg: hsl(208, 100%, 97%);
|
|
40
|
+
--info-border: hsl(221, 91%, 93%);
|
|
41
|
+
--info-text: hsl(210, 92%, 45%);
|
|
42
|
+
--warning-bg: hsl(49, 100%, 97%);
|
|
43
|
+
--warning-border: hsl(49, 91%, 84%);
|
|
44
|
+
--warning-text: hsl(31, 92%, 45%);
|
|
45
|
+
--error-bg: hsl(359, 100%, 97%);
|
|
46
|
+
--error-border: hsl(359, 100%, 94%);
|
|
47
|
+
--error-text: hsl(360, 100%, 45%);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.sc-toast--dark {
|
|
51
|
+
--normal-bg: #000;
|
|
52
|
+
--normal-border: hsl(0, 0%, 20%);
|
|
53
|
+
--normal-text: hsl(0, 0%, 99%);
|
|
54
|
+
--success-bg: hsl(150, 100%, 6%);
|
|
55
|
+
--success-border: hsl(147, 100%, 12%);
|
|
56
|
+
--success-text: hsl(150, 86%, 65%);
|
|
57
|
+
--info-bg: hsl(215, 100%, 6%);
|
|
58
|
+
--info-border: hsl(223, 43%, 17%);
|
|
59
|
+
--info-text: hsl(216, 87%, 65%);
|
|
60
|
+
--warning-bg: hsl(64, 100%, 6%);
|
|
61
|
+
--warning-border: hsl(60, 100%, 9%);
|
|
62
|
+
--warning-text: hsl(46, 87%, 65%);
|
|
63
|
+
--error-bg: hsl(358, 76%, 10%);
|
|
64
|
+
--error-border: hsl(357, 89%, 16%);
|
|
65
|
+
--error-text: hsl(358, 100%, 81%);
|
|
66
|
+
}
|
|
@@ -4,5 +4,5 @@ export declare class Toastr {
|
|
|
4
4
|
static success(message: string, options?: ToastrOptions): Promise<void>;
|
|
5
5
|
static warning(message: string, options?: ToastrOptions): Promise<void>;
|
|
6
6
|
static info(message: string, options?: ToastrOptions): Promise<void>;
|
|
7
|
-
static promise<T>(promise: Promise<T> | (() => Promise<T>), messages: ToastrPromiseMessages<T
|
|
7
|
+
static promise<T>(promise: Promise<T> | (() => Promise<T>), messages: ToastrPromiseMessages<T>, options?: ToastrOptions): Promise<void>;
|
|
8
8
|
}
|
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
import { ToasterHost } from './toaster-host.svelte';
|
|
2
|
+
const toExternalToast = (options) => {
|
|
3
|
+
if (!options) {
|
|
4
|
+
return {};
|
|
5
|
+
}
|
|
6
|
+
const { theme, ...rest } = options;
|
|
7
|
+
return { ...rest, class: theme ? `sc-toast--${theme}` : undefined };
|
|
8
|
+
};
|
|
2
9
|
export class Toastr {
|
|
3
10
|
static async error(message, options) {
|
|
4
11
|
const toast = await ToasterHost.ensure();
|
|
5
|
-
toast.error(message, options);
|
|
12
|
+
toast.error(message, toExternalToast(options));
|
|
6
13
|
}
|
|
7
14
|
static async success(message, options) {
|
|
8
15
|
const toast = await ToasterHost.ensure();
|
|
9
|
-
toast.success(message, options);
|
|
16
|
+
toast.success(message, toExternalToast(options));
|
|
10
17
|
}
|
|
11
18
|
static async warning(message, options) {
|
|
12
19
|
const toast = await ToasterHost.ensure();
|
|
13
|
-
toast.warning(message, options);
|
|
20
|
+
toast.warning(message, toExternalToast(options));
|
|
14
21
|
}
|
|
15
22
|
static async info(message, options) {
|
|
16
23
|
const toast = await ToasterHost.ensure();
|
|
17
|
-
toast.info(message, options);
|
|
24
|
+
toast.info(message, toExternalToast(options));
|
|
18
25
|
}
|
|
19
|
-
static async promise(promise, messages) {
|
|
26
|
+
static async promise(promise, messages, options) {
|
|
20
27
|
const toast = await ToasterHost.ensure();
|
|
21
|
-
toast.promise(promise, messages);
|
|
28
|
+
toast.promise(promise, { ...messages, ...toExternalToast(options) });
|
|
22
29
|
}
|
|
23
30
|
}
|
|
@@ -2,10 +2,12 @@ export interface ToastrAction {
|
|
|
2
2
|
label: string;
|
|
3
3
|
onClick: () => void;
|
|
4
4
|
}
|
|
5
|
+
export type ToastrTheme = 'light' | 'dark';
|
|
5
6
|
export interface ToastrOptions {
|
|
6
7
|
action?: ToastrAction;
|
|
7
8
|
description?: string;
|
|
8
9
|
duration?: number;
|
|
10
|
+
theme?: ToastrTheme;
|
|
9
11
|
}
|
|
10
12
|
export interface ToastrPromiseMessages<T> {
|
|
11
13
|
loading: string;
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
<script lang="ts">import { FileWithBlobDataHelper } from '../../../core/files';
|
|
2
2
|
import { Toastr } from '../../../core/toastr';
|
|
3
|
-
import { Dialog, DialogButton, DialogCancelButton
|
|
3
|
+
import { Dialog, DialogButton, DialogCancelButton } from '../../dialog';
|
|
4
4
|
import { ImgCropper, ImgCropperControls, ImgCropperToolbar, ImgCropperView } from '../img-cropper';
|
|
5
5
|
import { ImageEditorDialogLocalization } from './image-editor-dialog-localization';
|
|
6
6
|
import { untrack } from 'svelte';
|
|
7
7
|
const { controller, data } = $props();
|
|
8
8
|
const loc = new ImageEditorDialogLocalization();
|
|
9
9
|
const cropper = untrack(() => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
return new ImgCropper({
|
|
11
|
+
mode: data.mode ?? 'cover',
|
|
12
|
+
aspectRatio: data.aspectRatio,
|
|
13
|
+
fillColor: data.fillColor,
|
|
14
|
+
showImageShadow: data.showImageShadow ?? true
|
|
15
|
+
});
|
|
15
16
|
});
|
|
16
17
|
const url = $derived(data.url);
|
|
17
18
|
const save = async () => {
|
|
@@ -36,7 +37,7 @@ const cancel = () => {
|
|
|
36
37
|
};
|
|
37
38
|
$effect(() => untrack(() => {
|
|
38
39
|
controller.updateSettings({ closeOnClickOutside: false, closeOnEsc: true });
|
|
39
|
-
controller.updateContainerSettings({ size:
|
|
40
|
+
controller.updateContainerSettings({ size: 'fullhd' });
|
|
40
41
|
}));
|
|
41
42
|
</script>
|
|
42
43
|
|
|
@@ -76,7 +77,7 @@ $effect(() => untrack(() => {
|
|
|
76
77
|
.image-editor-dialog__body {
|
|
77
78
|
flex: 1;
|
|
78
79
|
position: relative;
|
|
79
|
-
background: #
|
|
80
|
+
background: #dadada;
|
|
80
81
|
overflow: hidden;
|
|
81
82
|
}
|
|
82
83
|
.image-editor-dialog__cropper-wrapper {
|
|
@@ -9,13 +9,13 @@ import type { ImageEditorDialogOptions, ImageEditorDialogResult } from './types'
|
|
|
9
9
|
*
|
|
10
10
|
* ### Aspect ratio
|
|
11
11
|
* - `number` — fixed ratio, no dropdown.
|
|
12
|
-
* - `
|
|
13
|
-
* - `
|
|
14
|
-
* - Omitted — free crop
|
|
12
|
+
* - `ImageEditorDynamicAspectRatio` — dropdown with supported ratios. `allowFreeCrop: true`
|
|
13
|
+
* adds a free-ratio option; `initial` is optional when free crop is enabled.
|
|
14
|
+
* - Omitted — free crop (no ratio constraint on the selection).
|
|
15
15
|
*
|
|
16
16
|
* ### Result
|
|
17
17
|
* - `croppedFile` — `FileWithBlobUrl` with `width`/`height` at natural resolution.
|
|
18
18
|
* - `selectedRatio` — active aspect ratio at save time (`null` for free crop).
|
|
19
19
|
*/
|
|
20
20
|
export declare const openImageEditorDialog: (options: ImageEditorDialogOptions) => Promise<DialogResult<ImageEditorDialogResult>>;
|
|
21
|
-
export type { CroppedFile, ImageEditorDialogOptions, ImageEditorDialogResult } from './types';
|
|
21
|
+
export type { CroppedFile, ImageEditorDialogOptions, ImageEditorDialogResult, ImageEditorDynamicAspectRatio } from './types';
|
|
@@ -9,9 +9,9 @@ import { default as ImageEditorDialog } from './cmp.image-editor-dialog.svelte';
|
|
|
9
9
|
*
|
|
10
10
|
* ### Aspect ratio
|
|
11
11
|
* - `number` — fixed ratio, no dropdown.
|
|
12
|
-
* - `
|
|
13
|
-
* - `
|
|
14
|
-
* - Omitted — free crop
|
|
12
|
+
* - `ImageEditorDynamicAspectRatio` — dropdown with supported ratios. `allowFreeCrop: true`
|
|
13
|
+
* adds a free-ratio option; `initial` is optional when free crop is enabled.
|
|
14
|
+
* - Omitted — free crop (no ratio constraint on the selection).
|
|
15
15
|
*
|
|
16
16
|
* ### Result
|
|
17
17
|
* - `croppedFile` — `FileWithBlobUrl` with `width`/`height` at natural resolution.
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import type { FileWithBlobUrl } from '../../../core/files';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
export type ImageEditorDynamicAspectRatio = {
|
|
3
|
+
initial: number;
|
|
4
|
+
supported: number[];
|
|
5
|
+
allowFreeCrop?: false;
|
|
6
|
+
} | {
|
|
7
|
+
initial?: number;
|
|
8
|
+
supported: number[];
|
|
9
|
+
allowFreeCrop: true;
|
|
10
|
+
};
|
|
11
|
+
export type ImageEditorDialogOptions = {
|
|
5
12
|
url: string;
|
|
6
13
|
fillColor?: string;
|
|
7
14
|
showImageShadow?: boolean;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
mode?: 'cover';
|
|
11
|
-
aspectRatio?: number | ImgCropperCoverAspectRatio;
|
|
12
|
-
};
|
|
13
|
-
type ImageEditorDialogContainOptions = ImageEditorDialogOptionsBase & {
|
|
14
|
-
mode: 'contain';
|
|
15
|
-
aspectRatio?: number | ImgCropperContainAspectRatio;
|
|
15
|
+
mode?: 'contain' | 'cover';
|
|
16
|
+
aspectRatio?: number | ImageEditorDynamicAspectRatio;
|
|
16
17
|
};
|
|
17
18
|
export type CroppedFile = FileWithBlobUrl & {
|
|
18
19
|
width: number;
|
|
@@ -22,4 +23,3 @@ export type ImageEditorDialogResult = {
|
|
|
22
23
|
croppedFile: CroppedFile;
|
|
23
24
|
selectedRatio: number | null;
|
|
24
25
|
};
|
|
25
|
-
export {};
|
|
@@ -38,16 +38,24 @@ const initCanvas = (canvasEl) => {
|
|
|
38
38
|
};
|
|
39
39
|
void initCropper();
|
|
40
40
|
}
|
|
41
|
-
return {
|
|
41
|
+
return {
|
|
42
|
+
destroy: () => {
|
|
43
|
+
cropper.destroy();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
const observeResize = (el) => {
|
|
48
|
+
const observer = new ResizeObserver(() => {
|
|
49
|
+
if (cropper.ready) {
|
|
50
|
+
cropper.refit();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
observer.observe(el);
|
|
54
|
+
return { destroy: () => observer.disconnect() };
|
|
42
55
|
};
|
|
43
56
|
</script>
|
|
44
57
|
|
|
45
|
-
<div
|
|
46
|
-
class="img-cropper"
|
|
47
|
-
style:--img-cropper--fill-color={cropper.fillColor || undefined}
|
|
48
|
-
style:--img-cropper--ratio={cropper.effectiveCanvasRatio ?? undefined}
|
|
49
|
-
style:--img-cropper--natural-w={cropper.naturalWidth ? `${cropper.naturalWidth}px` : undefined}
|
|
50
|
-
style:--img-cropper--natural-h={cropper.naturalHeight ? `${cropper.naturalHeight}px` : undefined}>
|
|
58
|
+
<div class="img-cropper" use:observeResize style:--img-cropper--fill-color={cropper.fillColor || undefined}>
|
|
51
59
|
{#if cropper.loading || !cropper.ready}
|
|
52
60
|
<Loading positionAbsoluteCenter={true} />
|
|
53
61
|
{/if}
|
|
@@ -56,12 +64,12 @@ const initCanvas = (canvasEl) => {
|
|
|
56
64
|
<cropper-canvas
|
|
57
65
|
class="img-cropper__canvas"
|
|
58
66
|
class:img-cropper__canvas--visible={cropper.ready}
|
|
59
|
-
class:img-cropper__canvas--fitted={cropper.effectiveCanvasRatio !== null && cropper.naturalWidth > 0}
|
|
60
67
|
class:img-cropper__canvas--shadow={cropper.showImageShadow}
|
|
68
|
+
style:width={cropper.canvasGeometry.width ? `${cropper.canvasGeometry.width}px` : undefined}
|
|
69
|
+
style:height={cropper.canvasGeometry.height ? `${cropper.canvasGeometry.height}px` : undefined}
|
|
61
70
|
use:initCanvas
|
|
62
71
|
background={(cropper.showFillColor && cropper.isTransparentFill) || undefined}>
|
|
63
|
-
<cropper-image
|
|
64
|
-
</cropper-image>
|
|
72
|
+
<cropper-image initial-center-size={cropper.mode} scalable translatable rotatable> </cropper-image>
|
|
65
73
|
<cropper-shade hidden></cropper-shade>
|
|
66
74
|
<cropper-handle action="select" plain></cropper-handle>
|
|
67
75
|
<cropper-selection movable resizable aspect-ratio={cropper.aspectRatio ?? undefined}>
|
|
@@ -79,8 +87,8 @@ const initCanvas = (canvasEl) => {
|
|
|
79
87
|
</cropper-selection>
|
|
80
88
|
</cropper-canvas>
|
|
81
89
|
|
|
82
|
-
{#if cropper.showImageShadow && cropper.ready}
|
|
83
|
-
<div class="img-cropper__shadow"
|
|
90
|
+
{#if cropper.showImageShadow && cropper.ready && cropper.canvasGeometry.width > 0}
|
|
91
|
+
<div class="img-cropper__shadow" style:width="{cropper.canvasGeometry.width}px" style:height="{cropper.canvasGeometry.height}px"></div>
|
|
84
92
|
{/if}
|
|
85
93
|
{/if}
|
|
86
94
|
|
|
@@ -116,20 +124,18 @@ The behavior is determined by the `ImgCropper` instance passed via the `cropper`
|
|
|
116
124
|
Aspect ratio is configured on the `ImgCropper` instance via the `aspectRatio` option:
|
|
117
125
|
|
|
118
126
|
- **Fixed number** (e.g. `16/9`) — locks the crop selection to this ratio.
|
|
119
|
-
- **Dynamic object** — provides a list of `supported`
|
|
120
|
-
The toolbar renders a ratio dropdown when
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
- In `cover` mode, `initial` is required and free crop is not available.
|
|
125
|
-
- **Omitted** — defaults to free crop in `contain` mode, or fills the canvas in `cover` mode.
|
|
127
|
+
- **Dynamic object** (`ImgCropperDynamicAspectRatio`) — provides a list of `supported`
|
|
128
|
+
ratios and an optional `initial` value. The toolbar renders a ratio dropdown when
|
|
129
|
+
multiple options are available. `allowFreeCrop: true` adds a free-ratio option
|
|
130
|
+
that lets the user draw an unconstrained selection.
|
|
131
|
+
- **Omitted** — defaults to free crop (no ratio constraint on the selection).
|
|
126
132
|
|
|
127
133
|
#### Shadow overlay
|
|
128
134
|
|
|
129
135
|
When `showImageShadow` is enabled on the cropper instance, a dark semi-transparent
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
`box-shadow` is applied directly to the `<cropper-canvas>` element, extending beyond
|
|
137
|
+
the canvas bounds so the surrounding area is dimmed. The parent element should set
|
|
138
|
+
`overflow: hidden` to clip the shadow at its boundaries.
|
|
133
139
|
|
|
134
140
|
### Props
|
|
135
141
|
| Prop | Type | Default | Description |
|
|
@@ -144,10 +150,6 @@ None — canvas background, shadow, and sizing are controlled by the `ImgCropper
|
|
|
144
150
|
|
|
145
151
|
<style>.img-cropper {
|
|
146
152
|
--_fill-color: var(--img-cropper--fill-color);
|
|
147
|
-
--_ratio: var(--img-cropper--ratio);
|
|
148
|
-
--_natural-w: var(--img-cropper--natural-w, 100cqw);
|
|
149
|
-
--_natural-h: var(--img-cropper--natural-h, 100cqh);
|
|
150
|
-
container-type: size;
|
|
151
153
|
position: relative;
|
|
152
154
|
width: 100%;
|
|
153
155
|
height: 100%;
|
|
@@ -162,11 +164,6 @@ None — canvas background, shadow, and sizing are controlled by the `ImgCropper
|
|
|
162
164
|
background-color: var(--_fill-color);
|
|
163
165
|
visibility: hidden;
|
|
164
166
|
}
|
|
165
|
-
.img-cropper__canvas--fitted {
|
|
166
|
-
width: min(100cqw, 100cqh * var(--_ratio), max(var(--_natural-w), var(--_natural-h) * var(--_ratio)));
|
|
167
|
-
height: auto;
|
|
168
|
-
aspect-ratio: var(--_ratio);
|
|
169
|
-
}
|
|
170
167
|
.img-cropper__canvas--visible {
|
|
171
168
|
visibility: visible;
|
|
172
169
|
}
|
|
@@ -178,17 +175,10 @@ None — canvas background, shadow, and sizing are controlled by the `ImgCropper
|
|
|
178
175
|
top: 50%;
|
|
179
176
|
left: 50%;
|
|
180
177
|
transform: translate(-50%, -50%);
|
|
181
|
-
width: min(100cqw, 100cqh * var(--_ratio), max(var(--_natural-w), var(--_natural-h) * var(--_ratio)));
|
|
182
|
-
aspect-ratio: var(--_ratio);
|
|
183
178
|
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.8);
|
|
184
179
|
pointer-events: none;
|
|
185
180
|
z-index: 2;
|
|
186
181
|
}
|
|
187
|
-
.img-cropper__shadow--full {
|
|
188
|
-
width: 100cqw;
|
|
189
|
-
height: 100cqh;
|
|
190
|
-
aspect-ratio: auto;
|
|
191
|
-
}
|
|
192
182
|
.img-cropper__controls {
|
|
193
183
|
position: absolute;
|
|
194
184
|
bottom: 0;
|
|
@@ -28,20 +28,18 @@ type Props = {
|
|
|
28
28
|
* Aspect ratio is configured on the `ImgCropper` instance via the `aspectRatio` option:
|
|
29
29
|
*
|
|
30
30
|
* - **Fixed number** (e.g. `16/9`) — locks the crop selection to this ratio.
|
|
31
|
-
* - **Dynamic object** — provides a list of `supported`
|
|
32
|
-
* The toolbar renders a ratio dropdown when
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* - In `cover` mode, `initial` is required and free crop is not available.
|
|
37
|
-
* - **Omitted** — defaults to free crop in `contain` mode, or fills the canvas in `cover` mode.
|
|
31
|
+
* - **Dynamic object** (`ImgCropperDynamicAspectRatio`) — provides a list of `supported`
|
|
32
|
+
* ratios and an optional `initial` value. The toolbar renders a ratio dropdown when
|
|
33
|
+
* multiple options are available. `allowFreeCrop: true` adds a free-ratio option
|
|
34
|
+
* that lets the user draw an unconstrained selection.
|
|
35
|
+
* - **Omitted** — defaults to free crop (no ratio constraint on the selection).
|
|
38
36
|
*
|
|
39
37
|
* #### Shadow overlay
|
|
40
38
|
*
|
|
41
39
|
* When `showImageShadow` is enabled on the cropper instance, a dark semi-transparent
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
40
|
+
* `box-shadow` is applied directly to the `<cropper-canvas>` element, extending beyond
|
|
41
|
+
* the canvas bounds so the surrounding area is dimmed. The parent element should set
|
|
42
|
+
* `overflow: hidden` to clip the shadow at its boundaries.
|
|
45
43
|
*
|
|
46
44
|
* ### Props
|
|
47
45
|
* | Prop | Type | Default | Description |
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CanvasGeometry, DragMode, ImgCropperResult, ImgCropperWorker, ImgCropperWorkerParams } from './img-cropper-worker.svelte';
|
|
2
|
+
import type { CropperCanvas, CropperImage, CropperSelection } from 'cropperjs';
|
|
3
|
+
export declare abstract class ImgCropperBaseWorker implements ImgCropperWorker {
|
|
4
|
+
cropBoxVisible: boolean;
|
|
5
|
+
dragMode: DragMode;
|
|
6
|
+
canvasGeometry: CanvasGeometry;
|
|
7
|
+
destroy: () => void;
|
|
8
|
+
protected _image: CropperImage;
|
|
9
|
+
protected _selection: CropperSelection;
|
|
10
|
+
protected _canvas: CropperCanvas;
|
|
11
|
+
protected _selectHandle: HTMLElement | null;
|
|
12
|
+
protected _originalSrc: string;
|
|
13
|
+
protected _sourceMime: string | null;
|
|
14
|
+
protected _visualWidth: number;
|
|
15
|
+
protected _visualHeight: number;
|
|
16
|
+
protected abstract readonly _centerMode: 'contain' | 'cover';
|
|
17
|
+
constructor(params: ImgCropperWorkerParams);
|
|
18
|
+
ready: () => Promise<void>;
|
|
19
|
+
rotate: () => Promise<void>;
|
|
20
|
+
zoomIn: () => void;
|
|
21
|
+
zoomOut: () => void;
|
|
22
|
+
reset: () => Promise<void>;
|
|
23
|
+
crop: (options?: {
|
|
24
|
+
fillColor?: string;
|
|
25
|
+
}) => Promise<ImgCropperResult>;
|
|
26
|
+
save: (options?: {
|
|
27
|
+
fillColor?: string;
|
|
28
|
+
}) => Promise<ImgCropperResult>;
|
|
29
|
+
clearSelection: () => void;
|
|
30
|
+
refit: () => Promise<void>;
|
|
31
|
+
enableCropMode: () => void;
|
|
32
|
+
protected _wrapTransformOp: (fn: () => void) => void;
|
|
33
|
+
protected _applyMoveMode: () => void;
|
|
34
|
+
protected _resolveOutputFormat: (fillColor: string | undefined) => {
|
|
35
|
+
mime: string;
|
|
36
|
+
quality: number;
|
|
37
|
+
};
|
|
38
|
+
protected _fitImage: () => Promise<void>;
|
|
39
|
+
protected abstract _computeCanvasSize(): void;
|
|
40
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { ColorHelper } from '../../../core/utils';
|
|
2
|
+
import { computeImageScale, computeNaturalOutputSize, handleSelectionBoundary } from './img-cropper-utils';
|
|
3
|
+
import { tick } from 'svelte';
|
|
4
|
+
const OPAQUE_MIME_TYPES = new Set(['image/jpeg', 'image/bmp']);
|
|
5
|
+
export class ImgCropperBaseWorker {
|
|
6
|
+
cropBoxVisible = $state(false);
|
|
7
|
+
dragMode = $state('move');
|
|
8
|
+
canvasGeometry = $state.raw({ aspectRatio: null, width: 0, height: 0 });
|
|
9
|
+
destroy;
|
|
10
|
+
_image;
|
|
11
|
+
_selection;
|
|
12
|
+
_canvas;
|
|
13
|
+
_selectHandle;
|
|
14
|
+
_originalSrc;
|
|
15
|
+
_sourceMime;
|
|
16
|
+
_visualWidth = 0;
|
|
17
|
+
_visualHeight = 0;
|
|
18
|
+
constructor(params) {
|
|
19
|
+
this._originalSrc = params.originalSrc;
|
|
20
|
+
this._sourceMime = params.sourceMime;
|
|
21
|
+
this._image = params.image;
|
|
22
|
+
this._selection = params.selection;
|
|
23
|
+
this._canvas = params.canvas;
|
|
24
|
+
this._selectHandle = params.selectHandle;
|
|
25
|
+
this.canvasGeometry = { aspectRatio: params.aspectRatio, width: 0, height: 0 };
|
|
26
|
+
const onSelectionChange = (e) => {
|
|
27
|
+
const { width, height } = e.detail;
|
|
28
|
+
this.cropBoxVisible = width > 0 && height > 0;
|
|
29
|
+
};
|
|
30
|
+
const onBoundaryCheck = (e) => {
|
|
31
|
+
handleSelectionBoundary({ event: e, selection: this._selection, canvas: this._canvas });
|
|
32
|
+
};
|
|
33
|
+
this._selection.addEventListener('change', onSelectionChange);
|
|
34
|
+
this._selection.addEventListener('change', onBoundaryCheck);
|
|
35
|
+
this.destroy = () => {
|
|
36
|
+
this._selection.removeEventListener('change', onSelectionChange);
|
|
37
|
+
this._selection.removeEventListener('change', onBoundaryCheck);
|
|
38
|
+
};
|
|
39
|
+
this._applyMoveMode();
|
|
40
|
+
}
|
|
41
|
+
ready = async () => {
|
|
42
|
+
this._image.src = this._originalSrc;
|
|
43
|
+
await this._fitImage();
|
|
44
|
+
this._selection.$clear();
|
|
45
|
+
};
|
|
46
|
+
rotate = async () => {
|
|
47
|
+
[this._visualWidth, this._visualHeight] = [this._visualHeight, this._visualWidth];
|
|
48
|
+
this._computeCanvasSize();
|
|
49
|
+
await tick();
|
|
50
|
+
this._wrapTransformOp(() => {
|
|
51
|
+
this._image.$rotate('90deg');
|
|
52
|
+
this._image.$center(this._centerMode);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
zoomIn = () => {
|
|
56
|
+
this._image.$zoom(0.1);
|
|
57
|
+
};
|
|
58
|
+
zoomOut = () => {
|
|
59
|
+
this._image.$zoom(-0.1);
|
|
60
|
+
};
|
|
61
|
+
reset = async () => {
|
|
62
|
+
this._selection.$clear();
|
|
63
|
+
this._applyMoveMode();
|
|
64
|
+
this._image.src = this._originalSrc;
|
|
65
|
+
await this._fitImage();
|
|
66
|
+
};
|
|
67
|
+
crop = async (options) => {
|
|
68
|
+
const fillColor = options?.fillColor;
|
|
69
|
+
const imageScale = computeImageScale(this._image.$getTransform());
|
|
70
|
+
const outputSize = computeNaturalOutputSize({ displayWidth: this._selection.width, displayHeight: this._selection.height, imageScale });
|
|
71
|
+
const canvas = await this._selection.$toCanvas({
|
|
72
|
+
width: outputSize.width,
|
|
73
|
+
height: outputSize.height,
|
|
74
|
+
beforeDraw: (ctx, cvs) => {
|
|
75
|
+
if (fillColor && !ColorHelper.isTransparent(fillColor)) {
|
|
76
|
+
ctx.fillStyle = fillColor;
|
|
77
|
+
ctx.fillRect(0, 0, cvs.width, cvs.height);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const format = this._resolveOutputFormat(fillColor);
|
|
82
|
+
const dataUrl = canvas.toDataURL(format.mime, format.quality);
|
|
83
|
+
this._applyMoveMode();
|
|
84
|
+
this._image.src = dataUrl;
|
|
85
|
+
await this._fitImage();
|
|
86
|
+
this._selection.$clear();
|
|
87
|
+
return { dataUrl, width: canvas.width, height: canvas.height, mimeType: format.mime };
|
|
88
|
+
};
|
|
89
|
+
save = async (options) => {
|
|
90
|
+
const fillColor = options?.fillColor;
|
|
91
|
+
const beforeDraw = (ctx, cvs) => {
|
|
92
|
+
if (fillColor && !ColorHelper.isTransparent(fillColor)) {
|
|
93
|
+
ctx.fillStyle = fillColor;
|
|
94
|
+
ctx.fillRect(0, 0, cvs.width, cvs.height);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
// Compute output dimensions before micro-zoom so the result matches
|
|
98
|
+
// the source pixel density, not the display size.
|
|
99
|
+
const imageScale = computeImageScale(this._image.$getTransform());
|
|
100
|
+
const displayWidth = this.cropBoxVisible ? this._selection.width : this._canvas.offsetWidth;
|
|
101
|
+
const displayHeight = this.cropBoxVisible ? this._selection.height : this._canvas.offsetHeight;
|
|
102
|
+
const outputSize = computeNaturalOutputSize({ displayWidth, displayHeight, imageScale });
|
|
103
|
+
const toCanvasOptions = { width: outputSize.width, height: outputSize.height, beforeDraw };
|
|
104
|
+
// CropperJS $toCanvas exports only the visible portion of the canvas.
|
|
105
|
+
// Sub-pixel rounding can leave a 1px transparent edge when the image
|
|
106
|
+
// tightly fits the canvas. A micro-zoom nudges the image slightly past
|
|
107
|
+
// the boundary so the export captures the full area; the zoom is
|
|
108
|
+
// reversed immediately after.
|
|
109
|
+
const needsMicroZoom = !this.cropBoxVisible;
|
|
110
|
+
if (needsMicroZoom) {
|
|
111
|
+
// Forward zoom increases coverage — safe without suspending cover check
|
|
112
|
+
this._image.$zoom(0.003);
|
|
113
|
+
}
|
|
114
|
+
const canvas = this.cropBoxVisible ? await this._selection.$toCanvas(toCanvasOptions) : await this._canvas.$toCanvas(toCanvasOptions);
|
|
115
|
+
if (needsMicroZoom) {
|
|
116
|
+
// Reverse zoom shrinks image — must suspend cover check to avoid clamping
|
|
117
|
+
this._wrapTransformOp(() => this._image.$zoom(-0.003));
|
|
118
|
+
}
|
|
119
|
+
const format = this._resolveOutputFormat(fillColor);
|
|
120
|
+
const dataUrl = canvas.toDataURL(format.mime, format.quality);
|
|
121
|
+
return { dataUrl, width: canvas.width, height: canvas.height, mimeType: format.mime };
|
|
122
|
+
};
|
|
123
|
+
clearSelection = () => {
|
|
124
|
+
this._selection.$clear();
|
|
125
|
+
this._applyMoveMode();
|
|
126
|
+
};
|
|
127
|
+
refit = async () => {
|
|
128
|
+
this._selection.$clear();
|
|
129
|
+
this._applyMoveMode();
|
|
130
|
+
this._computeCanvasSize();
|
|
131
|
+
await tick();
|
|
132
|
+
this._wrapTransformOp(() => {
|
|
133
|
+
this._image.$resetTransform();
|
|
134
|
+
this._image.$center(this._centerMode);
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
enableCropMode = () => {
|
|
138
|
+
this._image.translatable = false;
|
|
139
|
+
if (this._selectHandle) {
|
|
140
|
+
this._selectHandle.setAttribute('action', 'select');
|
|
141
|
+
}
|
|
142
|
+
this.dragMode = 'crop';
|
|
143
|
+
};
|
|
144
|
+
_wrapTransformOp = (fn) => {
|
|
145
|
+
fn();
|
|
146
|
+
};
|
|
147
|
+
_applyMoveMode = () => {
|
|
148
|
+
this._image.translatable = true;
|
|
149
|
+
if (this._selectHandle) {
|
|
150
|
+
this._selectHandle.setAttribute('action', 'move');
|
|
151
|
+
}
|
|
152
|
+
this.dragMode = 'move';
|
|
153
|
+
this.cropBoxVisible = false;
|
|
154
|
+
};
|
|
155
|
+
_resolveOutputFormat = (fillColor) => {
|
|
156
|
+
if (this._sourceMime && OPAQUE_MIME_TYPES.has(this._sourceMime)) {
|
|
157
|
+
return { mime: 'image/jpeg', quality: 0.92 };
|
|
158
|
+
}
|
|
159
|
+
if (fillColor && !ColorHelper.isTransparent(fillColor)) {
|
|
160
|
+
return { mime: 'image/jpeg', quality: 0.92 };
|
|
161
|
+
}
|
|
162
|
+
return { mime: 'image/png', quality: 1 };
|
|
163
|
+
};
|
|
164
|
+
_fitImage = async () => {
|
|
165
|
+
const img = await this._image.$ready();
|
|
166
|
+
this._visualWidth = img.naturalWidth;
|
|
167
|
+
this._visualHeight = img.naturalHeight;
|
|
168
|
+
this._computeCanvasSize();
|
|
169
|
+
await tick();
|
|
170
|
+
this._wrapTransformOp(() => {
|
|
171
|
+
this._image.$resetTransform();
|
|
172
|
+
this._image.$center(this._centerMode);
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -1,40 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
naturalWidth: number;
|
|
6
|
-
naturalHeight: number;
|
|
7
|
-
canvasRatio: number | null;
|
|
8
|
-
destroy: () => void;
|
|
9
|
-
private _image;
|
|
10
|
-
private _selection;
|
|
11
|
-
private _canvas;
|
|
12
|
-
private _selectHandle;
|
|
13
|
-
private _originalSrc;
|
|
1
|
+
import { ImgCropperBaseWorker } from './img-cropper-base-worker.svelte';
|
|
2
|
+
import type { ImgCropperWorkerParams } from './img-cropper-worker.svelte';
|
|
3
|
+
export declare class ImgCropperContainWorker extends ImgCropperBaseWorker {
|
|
4
|
+
protected readonly _centerMode: "contain";
|
|
14
5
|
constructor(params: ImgCropperWorkerParams);
|
|
15
|
-
|
|
16
|
-
rotate: () => Promise<void>;
|
|
17
|
-
zoomIn: () => void;
|
|
18
|
-
zoomOut: () => void;
|
|
19
|
-
reset: () => Promise<void>;
|
|
20
|
-
crop: (options?: {
|
|
21
|
-
fillColor?: string;
|
|
22
|
-
freeRatio?: boolean;
|
|
23
|
-
}) => Promise<{
|
|
24
|
-
dataUrl: string;
|
|
25
|
-
width: number;
|
|
26
|
-
height: number;
|
|
27
|
-
}>;
|
|
28
|
-
save: (options?: {
|
|
29
|
-
fillColor?: string;
|
|
30
|
-
}) => Promise<{
|
|
31
|
-
dataUrl: string;
|
|
32
|
-
width: number;
|
|
33
|
-
height: number;
|
|
34
|
-
}>;
|
|
35
|
-
clearSelection: () => void;
|
|
36
|
-
enableCropMode: () => void;
|
|
37
|
-
private _applyMoveMode;
|
|
38
|
-
private _fitImage;
|
|
39
|
-
private _centerForCurrentRotation;
|
|
6
|
+
protected _computeCanvasSize: () => void;
|
|
40
7
|
}
|