@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.
Files changed (57) hide show
  1. package/dist/core/toastr/index.d.ts +1 -1
  2. package/dist/core/toastr/toastr.scss +38 -0
  3. package/dist/core/toastr/toastr.svelte.d.ts +1 -1
  4. package/dist/core/toastr/toastr.svelte.js +13 -6
  5. package/dist/core/toastr/types.d.ts +2 -0
  6. package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte +9 -8
  7. package/dist/ui/cropper/image-editor-dialog/index.d.ts +4 -4
  8. package/dist/ui/cropper/image-editor-dialog/index.js +3 -3
  9. package/dist/ui/cropper/image-editor-dialog/types.d.ts +12 -12
  10. package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte +28 -38
  11. package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte.d.ts +8 -10
  12. package/dist/ui/cropper/img-cropper/img-cropper-base-worker.svelte.d.ts +40 -0
  13. package/dist/ui/cropper/img-cropper/img-cropper-base-worker.svelte.js +175 -0
  14. package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.d.ts +5 -38
  15. package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.js +29 -149
  16. package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.d.ts +5 -38
  17. package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.js +37 -135
  18. package/dist/ui/cropper/img-cropper/img-cropper-utils.d.ts +11 -1
  19. package/dist/ui/cropper/img-cropper/img-cropper-utils.js +30 -0
  20. package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.d.ts +17 -14
  21. package/dist/ui/cropper/img-cropper/img-cropper.svelte.d.ts +13 -31
  22. package/dist/ui/cropper/img-cropper/img-cropper.svelte.js +29 -28
  23. package/dist/ui/cropper/img-cropper/index.d.ts +1 -1
  24. package/dist/ui/dialog/cmp.dialog-container.svelte +12 -12
  25. package/dist/ui/dialog/dialog-data.d.ts +2 -0
  26. package/dist/ui/dialog/dialog-mount.d.ts +1 -1
  27. package/dist/ui/dialog/dialog-mount.js +2 -2
  28. package/dist/ui/dialog/dialogs.svelte.d.ts +3 -0
  29. package/dist/ui/dialog/dialogs.svelte.js +21 -2
  30. package/dist/ui/dialog/index.d.ts +1 -1
  31. package/dist/ui/dialog/index.js +1 -1
  32. package/dist/ui/dialog/types.svelte.d.ts +3 -14
  33. package/dist/ui/dialog/types.svelte.js +3 -18
  34. package/dist/ui/form-group/cmp.form-group-label.svelte +25 -0
  35. package/dist/ui/form-group/cmp.form-group-label.svelte.d.ts +8 -0
  36. package/dist/ui/form-group/cmp.form-group-note.svelte +16 -0
  37. package/dist/ui/form-group/cmp.form-group-note.svelte.d.ts +7 -0
  38. package/dist/ui/form-group/cmp.form-group.svelte +16 -0
  39. package/dist/ui/form-group/cmp.form-group.svelte.d.ts +8 -0
  40. package/dist/ui/form-group/index.d.ts +3 -0
  41. package/dist/ui/form-group/index.js +3 -0
  42. package/dist/ui/html-block/cmp.html-block.svelte +112 -0
  43. package/dist/ui/html-block/cmp.html-block.svelte.d.ts +7 -0
  44. package/dist/ui/html-block/index.d.ts +1 -0
  45. package/dist/ui/html-block/index.js +1 -0
  46. package/dist/ui/media-viewer-dialog/cmp.media-viewer-dialog.svelte +50 -0
  47. package/dist/ui/media-viewer-dialog/cmp.media-viewer-dialog.svelte.d.ts +9 -0
  48. package/dist/ui/media-viewer-dialog/index.d.ts +13 -0
  49. package/dist/ui/media-viewer-dialog/index.js +17 -0
  50. package/dist/ui/media-viewer-dialog/media-viewer-item.svelte +61 -0
  51. package/dist/ui/media-viewer-dialog/media-viewer-item.svelte.d.ts +7 -0
  52. package/dist/ui/media-viewer-dialog/types.d.ts +13 -0
  53. package/dist/ui/media-viewer-dialog/types.js +1 -0
  54. package/dist/ui/player/carousel/cmp.carousel.svelte +2 -2
  55. package/dist/ui/player/carousel/cmp.carousel.svelte.d.ts +1 -1
  56. package/dist/ui/video/cmp.video.svelte +16 -7
  57. package/package.json +14 -1
@@ -1,3 +1,3 @@
1
1
  import './toastr.scss';
2
2
  export { Toastr } from './toastr.svelte';
3
- export type { ToastrAction, ToastrOptions, ToastrPromiseMessages } from './types';
3
+ export type { ToastrAction, ToastrOptions, ToastrPromiseMessages, ToastrTheme } from './types';
@@ -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>): Promise<void>;
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, DialogSize } from '../../dialog';
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
- const showImageShadow = data.showImageShadow ?? true;
11
- if (data.mode === 'contain') {
12
- return new ImgCropper({ mode: 'contain', aspectRatio: data.aspectRatio, fillColor: data.fillColor, showImageShadow });
13
- }
14
- return new ImgCropper({ mode: 'cover', aspectRatio: data.aspectRatio, fillColor: data.fillColor, showImageShadow });
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: DialogSize.FullHD });
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: #DADADA;
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
- * - `{ initial, supported }` (cover) — dropdown with supported ratios.
13
- * - `{ initial?, supported, allowFreeCrop? }` (contain) dropdown with optional free crop.
14
- * - Omitted — free crop in contain, full canvas in cover.
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
- * - `{ initial, supported }` (cover) — dropdown with supported ratios.
13
- * - `{ initial?, supported, allowFreeCrop? }` (contain) dropdown with optional free crop.
14
- * - Omitted — free crop in contain, full canvas in cover.
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
- import type { ImgCropperContainAspectRatio, ImgCropperCoverAspectRatio } from '../img-cropper';
3
- export type ImageEditorDialogOptions = ImageEditorDialogCoverOptions | ImageEditorDialogContainOptions;
4
- type ImageEditorDialogOptionsBase = {
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
- type ImageEditorDialogCoverOptions = ImageEditorDialogOptionsBase & {
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 { destroy: () => cropper.destroy() };
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 crossorigin={cropper.corsMode === 'native' ? 'anonymous' : null} initial-center-size={cropper.mode} scalable translatable rotatable>
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" class:img-cropper__shadow--full={cropper.effectiveCanvasRatio === null}></div>
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` ratios and an `initial` value.
120
- The toolbar renders a ratio dropdown when multiple options are available.
121
- - In `contain` mode, `allowFreeCrop: true` adds a free-ratio option that lets
122
- the user draw an unconstrained selection. After crop, the canvas dynamically
123
- adopts the cropped area's proportions.
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
- overlay (`box-shadow: 0 0 0 9999px`) extends beyond the canvas bounds so the full
131
- image remains visible outside the crop area. In free-ratio mode (no fixed aspect
132
- ratio), the shadow covers the entire container instead.
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` ratios and an `initial` value.
32
- * The toolbar renders a ratio dropdown when multiple options are available.
33
- * - In `contain` mode, `allowFreeCrop: true` adds a free-ratio option that lets
34
- * the user draw an unconstrained selection. After crop, the canvas dynamically
35
- * adopts the cropped area's proportions.
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
- * overlay (`box-shadow: 0 0 0 9999px`) extends beyond the canvas bounds so the full
43
- * image remains visible outside the crop area. In free-ratio mode (no fixed aspect
44
- * ratio), the shadow covers the entire container instead.
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 type { DragMode, ImgCropperWorker, ImgCropperWorkerParams } from './img-cropper-worker.svelte';
2
- export declare class ImgCropperContainWorker implements ImgCropperWorker {
3
- cropBoxVisible: boolean;
4
- dragMode: DragMode;
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
- ready: () => Promise<void>;
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
  }