@streamscloud/kit 0.63.0 → 0.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/utils/color-helper.d.ts +4 -0
- package/dist/core/utils/color-helper.js +10 -0
- package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte +17 -51
- package/dist/ui/cropper/image-editor-dialog/crop-and-resize.d.ts +4 -16
- package/dist/ui/cropper/image-editor-dialog/crop-and-resize.js +1 -14
- package/dist/ui/cropper/image-editor-dialog/index.d.ts +8 -7
- package/dist/ui/cropper/image-editor-dialog/index.js +8 -7
- package/dist/ui/cropper/image-editor-dialog/types.d.ts +1 -1
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-toolbar.svelte +149 -181
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-toolbar.svelte.d.ts +4 -13
- package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte +389 -147
- package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte.d.ts +13 -39
- package/dist/ui/cropper/img-cropper/frame-geometry.d.ts +82 -0
- package/dist/ui/cropper/img-cropper/frame-geometry.js +165 -0
- package/dist/ui/cropper/img-cropper/img-cropper-localization.d.ts +2 -3
- package/dist/ui/cropper/img-cropper/img-cropper-localization.js +12 -19
- package/dist/ui/cropper/img-cropper/img-cropper-toolbar-localization.d.ts +14 -5
- package/dist/ui/cropper/img-cropper/img-cropper-toolbar-localization.js +127 -33
- package/dist/ui/cropper/img-cropper/img-cropper-utils.d.ts +4 -51
- package/dist/ui/cropper/img-cropper/img-cropper-utils.js +35 -190
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.d.ts +99 -34
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.js +405 -104
- package/dist/ui/cropper/img-cropper/index.d.ts +1 -2
- package/dist/ui/cropper/img-cropper/index.js +0 -1
- package/package.json +1 -3
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-controls.svelte +0 -68
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-controls.svelte.d.ts +0 -21
- package/dist/ui/cropper/img-cropper/cropperjs-elements.d.ts +0 -35
- package/dist/ui/cropper/img-cropper/img-cropper-base-worker.svelte.d.ts +0 -40
- package/dist/ui/cropper/img-cropper/img-cropper-base-worker.svelte.js +0 -193
- package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.d.ts +0 -7
- package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.js +0 -51
- package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.d.ts +0 -7
- package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.js +0 -89
- package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.d.ts +0 -42
- package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.js +0 -1
|
@@ -10,4 +10,8 @@ export declare class ColorHelper {
|
|
|
10
10
|
* Check whether a color value represents "no color" (empty, transparent, zero alpha).
|
|
11
11
|
*/
|
|
12
12
|
static isTransparent(value: string | null | undefined): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Check whether a color value fully hides what is behind it (valid, 100% alpha).
|
|
15
|
+
*/
|
|
16
|
+
static isOpaque(value: string | null | undefined): boolean;
|
|
13
17
|
}
|
|
@@ -33,4 +33,14 @@ export class ColorHelper {
|
|
|
33
33
|
const parsed = colord(value);
|
|
34
34
|
return !parsed.isValid() || parsed.alpha() === 0;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Check whether a color value fully hides what is behind it (valid, 100% alpha).
|
|
38
|
+
*/
|
|
39
|
+
static isOpaque(value) {
|
|
40
|
+
if (!value) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const parsed = colord(value);
|
|
44
|
+
return parsed.isValid() && parsed.alpha() === 1;
|
|
45
|
+
}
|
|
36
46
|
}
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
<script lang="ts">import { FileWithBlobDataHelper } from '../../../core/files';
|
|
2
2
|
import { Dialog, DialogButton, DialogCancelButton } from '../../dialog';
|
|
3
3
|
import { Toastr } from '../../toast';
|
|
4
|
-
import { ImgCropper,
|
|
4
|
+
import { ImgCropper, 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
|
-
const cropper = untrack(() => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
showImageShadow: data.showImageShadow ?? true
|
|
15
|
-
});
|
|
16
|
-
});
|
|
9
|
+
const cropper = untrack(() => new ImgCropper({
|
|
10
|
+
mode: data.mode ?? 'cover',
|
|
11
|
+
aspectRatio: data.aspectRatio,
|
|
12
|
+
fillColor: data.fillColor
|
|
13
|
+
}));
|
|
17
14
|
const url = $derived(data.url);
|
|
18
15
|
const save = async () => {
|
|
19
16
|
try {
|
|
@@ -47,24 +44,18 @@ $effect(() => untrack(() => {
|
|
|
47
44
|
{loc.title}
|
|
48
45
|
{/snippet}
|
|
49
46
|
{#snippet bodySection()}
|
|
50
|
-
<div class="image-editor-
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
</div>
|
|
47
|
+
<div class="image-editor-dialog__workspace">
|
|
48
|
+
<ImgCropperView src={url} cropper={cropper}>
|
|
49
|
+
{#snippet toolbar()}
|
|
50
|
+
<ImgCropperToolbar cropper={cropper} />
|
|
51
|
+
{/snippet}
|
|
52
|
+
</ImgCropperView>
|
|
57
53
|
</div>
|
|
58
54
|
{/snippet}
|
|
59
55
|
{#snippet footer()}
|
|
60
56
|
<div class="image-editor-dialog__footer">
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
</div>
|
|
64
|
-
<div class="image-editor-dialog__buttons">
|
|
65
|
-
<DialogCancelButton on={{ click: cancel }}>{loc.cancel}</DialogCancelButton>
|
|
66
|
-
<DialogButton on={{ click: save }}>{loc.save}</DialogButton>
|
|
67
|
-
</div>
|
|
57
|
+
<DialogCancelButton on={{ click: cancel }}>{loc.cancel}</DialogCancelButton>
|
|
58
|
+
<DialogButton on={{ click: save }}>{loc.save}</DialogButton>
|
|
68
59
|
</div>
|
|
69
60
|
{/snippet}
|
|
70
61
|
</Dialog>
|
|
@@ -74,37 +65,12 @@ $effect(() => untrack(() => {
|
|
|
74
65
|
--sc-kit--dialog--height: calc(100vh - 2.5rem);
|
|
75
66
|
display: contents;
|
|
76
67
|
}
|
|
77
|
-
.image-editor-
|
|
68
|
+
.image-editor-dialog__workspace {
|
|
78
69
|
flex: 1;
|
|
79
|
-
|
|
80
|
-
background: #dadada;
|
|
81
|
-
overflow: hidden;
|
|
82
|
-
}
|
|
83
|
-
.image-editor-dialog__cropper-wrapper {
|
|
84
|
-
position: absolute;
|
|
85
|
-
inset: 3rem;
|
|
86
|
-
}
|
|
87
|
-
.image-editor-dialog__controls {
|
|
88
|
-
position: absolute;
|
|
89
|
-
bottom: 0.3125rem;
|
|
90
|
-
left: 50%;
|
|
91
|
-
transform: translateX(-50%);
|
|
92
|
-
z-index: 10;
|
|
70
|
+
min-block-size: 0;
|
|
93
71
|
}
|
|
94
72
|
.image-editor-dialog__footer {
|
|
95
|
-
container-type: inline-size;
|
|
96
|
-
display: flex;
|
|
97
|
-
flex: 1;
|
|
98
|
-
justify-content: space-between;
|
|
99
|
-
align-items: center;
|
|
100
|
-
}
|
|
101
|
-
.image-editor-dialog__toolbar {
|
|
102
|
-
flex: 1;
|
|
103
|
-
min-width: 0;
|
|
104
|
-
}
|
|
105
|
-
.image-editor-dialog__buttons {
|
|
106
|
-
white-space: nowrap;
|
|
107
73
|
display: flex;
|
|
74
|
+
justify-content: flex-end;
|
|
108
75
|
gap: 1.5rem;
|
|
109
|
-
margin-left: 1rem;
|
|
110
76
|
}</style>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { FileWithBlobUrl } from '../../../core/files';
|
|
2
2
|
import type { ImageEditorDynamicAspectRatio } from './types';
|
|
3
3
|
export type CropAndResizeOptions = {
|
|
4
|
-
/** Fixed aspect ratio, or dynamic selector, or omit
|
|
4
|
+
/** Fixed aspect ratio, or dynamic selector, or omit to follow the picture's own. */
|
|
5
5
|
aspectRatio?: number | ImageEditorDynamicAspectRatio;
|
|
6
|
-
/**
|
|
6
|
+
/** @default 'cover' */
|
|
7
7
|
mode?: 'contain' | 'cover';
|
|
8
|
-
/** Fill color in `contain` mode (transparent
|
|
8
|
+
/** Fill color in `contain` mode (transparent by default). */
|
|
9
9
|
fillColor?: string;
|
|
10
10
|
/**
|
|
11
11
|
* After cropping, downscale to fit these limits. `max1` / `max2` are not tied to width /
|
|
@@ -18,19 +18,7 @@ export type CropAndResizeOptions = {
|
|
|
18
18
|
} | false;
|
|
19
19
|
};
|
|
20
20
|
/**
|
|
21
|
-
* Opens the kit's image editor dialog, lets the user crop, and (optionally) downscales the
|
|
22
|
-
* result before returning. Composable opt-in helper — call it explicitly from a consumer's
|
|
23
|
-
* select handler when image cropping is needed before upload.
|
|
24
|
-
*
|
|
21
|
+
* Opens the kit's image editor dialog, lets the user crop, and (optionally) downscales the result before returning.
|
|
25
22
|
* Returns `null` when the user cancels the dialog.
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```ts
|
|
29
|
-
* const cropped = await cropAndResizeImage(file, {
|
|
30
|
-
* aspectRatio: 1,
|
|
31
|
-
* resize: { max1: 400, max2: 400 }
|
|
32
|
-
* });
|
|
33
|
-
* if (cropped) store.add(cropped);
|
|
34
|
-
* ```
|
|
35
23
|
*/
|
|
36
24
|
export declare const cropAndResizeImage: (file: File, options?: CropAndResizeOptions) => Promise<FileWithBlobUrl | null>;
|
|
@@ -2,20 +2,8 @@ import { FileWithBlobDataHelper, resizeBlob } from '../../../core/files';
|
|
|
2
2
|
import { openImageEditorDialog } from './index';
|
|
3
3
|
const DEFAULT_RESIZE_LIMITS = { max1: 1600, max2: 1000 };
|
|
4
4
|
/**
|
|
5
|
-
* Opens the kit's image editor dialog, lets the user crop, and (optionally) downscales the
|
|
6
|
-
* result before returning. Composable opt-in helper — call it explicitly from a consumer's
|
|
7
|
-
* select handler when image cropping is needed before upload.
|
|
8
|
-
*
|
|
5
|
+
* Opens the kit's image editor dialog, lets the user crop, and (optionally) downscales the result before returning.
|
|
9
6
|
* Returns `null` when the user cancels the dialog.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```ts
|
|
13
|
-
* const cropped = await cropAndResizeImage(file, {
|
|
14
|
-
* aspectRatio: 1,
|
|
15
|
-
* resize: { max1: 400, max2: 400 }
|
|
16
|
-
* });
|
|
17
|
-
* if (cropped) store.add(cropped);
|
|
18
|
-
* ```
|
|
19
7
|
*/
|
|
20
8
|
export const cropAndResizeImage = async (file, options = {}) => {
|
|
21
9
|
const sourceUrl = URL.createObjectURL(file);
|
|
@@ -35,7 +23,6 @@ export const cropAndResizeImage = async (file, options = {}) => {
|
|
|
35
23
|
}
|
|
36
24
|
const limits = options.resize ?? DEFAULT_RESIZE_LIMITS;
|
|
37
25
|
const resizedBlob = await resizeBlob(cropped, limits);
|
|
38
|
-
// Already-fitting blob? `resizeBlob` returns the same instance — short-circuit.
|
|
39
26
|
if (resizedBlob === cropped) {
|
|
40
27
|
return cropped;
|
|
41
28
|
}
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { type DialogResult } from '../../dialog';
|
|
2
2
|
import type { ImageEditorDialogOptions, ImageEditorDialogResult } from './types';
|
|
3
3
|
/**
|
|
4
|
-
* Opens a full-screen image editor dialog
|
|
4
|
+
* Opens a full-screen image editor dialog: drag the picture inside a fixed frame, scale it from the corners, zoom,
|
|
5
|
+
* rotate, align, and reset.
|
|
5
6
|
*
|
|
6
7
|
* ### Modes
|
|
7
|
-
* - **`cover`** (default) —
|
|
8
|
-
* - **`contain`** —
|
|
8
|
+
* - **`cover`** (default) — the picture always fills the frame; nothing outside the frame is exported.
|
|
9
|
+
* - **`contain`** — the picture is fitted inside the frame with an optional fill color for the remaining space.
|
|
9
10
|
*
|
|
10
11
|
* ### Aspect ratio
|
|
11
12
|
* - `number` — fixed ratio, no popover.
|
|
12
|
-
* - `ImageEditorDynamicAspectRatio` — popover with supported ratios. `allowFreeCrop: true`
|
|
13
|
-
*
|
|
14
|
-
* - Omitted —
|
|
13
|
+
* - `ImageEditorDynamicAspectRatio` — popover with supported ratios. `allowFreeCrop: true` adds the picture's own
|
|
14
|
+
* ratio as a choice; `initial` is optional when it's enabled.
|
|
15
|
+
* - Omitted — the frame follows the picture's own aspect ratio.
|
|
15
16
|
*
|
|
16
17
|
* ### Result
|
|
17
18
|
* - `croppedFile` — `FileWithBlobUrl` with `width`/`height` at natural resolution.
|
|
18
|
-
* - `selectedRatio` — active aspect ratio at save time (`null`
|
|
19
|
+
* - `selectedRatio` — the active aspect ratio at save time (`null` when following the picture's own).
|
|
19
20
|
*/
|
|
20
21
|
export declare const openImageEditorDialog: (options: ImageEditorDialogOptions) => Promise<DialogResult<ImageEditorDialogResult>>;
|
|
21
22
|
export type { CroppedFile, ImageEditorDialogOptions, ImageEditorDialogResult, ImageEditorDynamicAspectRatio } from './types';
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { Dialogs } from '../../dialog';
|
|
2
2
|
import { default as ImageEditorDialog } from './cmp.image-editor-dialog.svelte';
|
|
3
3
|
/**
|
|
4
|
-
* Opens a full-screen image editor dialog
|
|
4
|
+
* Opens a full-screen image editor dialog: drag the picture inside a fixed frame, scale it from the corners, zoom,
|
|
5
|
+
* rotate, align, and reset.
|
|
5
6
|
*
|
|
6
7
|
* ### Modes
|
|
7
|
-
* - **`cover`** (default) —
|
|
8
|
-
* - **`contain`** —
|
|
8
|
+
* - **`cover`** (default) — the picture always fills the frame; nothing outside the frame is exported.
|
|
9
|
+
* - **`contain`** — the picture is fitted inside the frame with an optional fill color for the remaining space.
|
|
9
10
|
*
|
|
10
11
|
* ### Aspect ratio
|
|
11
12
|
* - `number` — fixed ratio, no popover.
|
|
12
|
-
* - `ImageEditorDynamicAspectRatio` — popover with supported ratios. `allowFreeCrop: true`
|
|
13
|
-
*
|
|
14
|
-
* - Omitted —
|
|
13
|
+
* - `ImageEditorDynamicAspectRatio` — popover with supported ratios. `allowFreeCrop: true` adds the picture's own
|
|
14
|
+
* ratio as a choice; `initial` is optional when it's enabled.
|
|
15
|
+
* - Omitted — the frame follows the picture's own aspect ratio.
|
|
15
16
|
*
|
|
16
17
|
* ### Result
|
|
17
18
|
* - `croppedFile` — `FileWithBlobUrl` with `width`/`height` at natural resolution.
|
|
18
|
-
* - `selectedRatio` — active aspect ratio at save time (`null`
|
|
19
|
+
* - `selectedRatio` — the active aspect ratio at save time (`null` when following the picture's own).
|
|
19
20
|
*/
|
|
20
21
|
export const openImageEditorDialog = async (options) => {
|
|
21
22
|
return await Dialogs.open({
|
|
@@ -9,9 +9,9 @@ export type ImageEditorDynamicAspectRatio = {
|
|
|
9
9
|
allowFreeCrop: true;
|
|
10
10
|
};
|
|
11
11
|
export type ImageEditorDialogOptions = {
|
|
12
|
+
/** The picture to edit — same-origin, a `blob:` / `data:` URL, or a server that sends CORS headers; anything else fails to load. */
|
|
12
13
|
url: string;
|
|
13
14
|
fillColor?: string;
|
|
14
|
-
showImageShadow?: boolean;
|
|
15
15
|
mode?: 'contain' | 'cover';
|
|
16
16
|
aspectRatio?: number | ImageEditorDynamicAspectRatio;
|
|
17
17
|
};
|
|
@@ -1,86 +1,131 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
1
|
+
<script lang="ts">import { Button } from '../../button';
|
|
2
|
+
import { ColorPicker } from '../../color-picker';
|
|
3
|
+
import { Divider } from '../../divider';
|
|
2
4
|
import { Icon } from '../../icon';
|
|
3
|
-
import {
|
|
4
|
-
import { Popover } from '../../popover';
|
|
5
|
+
import { ToolbarIconButton } from '../../page-toolbar';
|
|
6
|
+
import { Popover, PopoverItem } from '../../popover';
|
|
7
|
+
import { Tooltip } from '../../tooltip';
|
|
5
8
|
import { ImgCropperToolbarLocalization } from './img-cropper-toolbar-localization';
|
|
6
9
|
import IconArrowMove from '@fluentui/svg-icons/icons/arrow_move_20_regular.svg?raw';
|
|
7
|
-
import
|
|
8
|
-
import
|
|
10
|
+
import IconArrowReset from '@fluentui/svg-icons/icons/arrow_reset_20_regular.svg?raw';
|
|
11
|
+
import IconRotateRight from '@fluentui/svg-icons/icons/arrow_rotate_clockwise_20_regular.svg?raw';
|
|
12
|
+
import IconRotateLeft from '@fluentui/svg-icons/icons/arrow_rotate_counterclockwise_20_regular.svg?raw';
|
|
13
|
+
import IconCheckmark from '@fluentui/svg-icons/icons/checkmark_20_regular.svg?raw';
|
|
9
14
|
import IconCrop from '@fluentui/svg-icons/icons/crop_20_regular.svg?raw';
|
|
10
|
-
import
|
|
15
|
+
import IconGridDots from '@fluentui/svg-icons/icons/grid_dots_20_regular.svg?raw';
|
|
11
16
|
import IconZoomIn from '@fluentui/svg-icons/icons/zoom_in_20_regular.svg?raw';
|
|
12
17
|
import IconZoomOut from '@fluentui/svg-icons/icons/zoom_out_20_regular.svg?raw';
|
|
13
18
|
const { cropper, children } = $props();
|
|
14
19
|
const loc = new ImgCropperToolbarLocalization();
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
});
|
|
20
|
+
const ZOOM_PRESETS = [1.5, 2, 3];
|
|
21
|
+
const ALIGN_FRACTIONS = [0, 0.5, 1];
|
|
22
|
+
const zoomPercent = $derived(Math.round(cropper.zoom * 100));
|
|
23
|
+
const ratioLabel = $derived(cropper.ratioLabel ?? loc.original);
|
|
20
24
|
</script>
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<button
|
|
31
|
-
type="button"
|
|
32
|
-
class="img-cropper-toolbar__button"
|
|
33
|
-
class:img-cropper-toolbar__button--active={cropper.dragMode === 'crop'}
|
|
34
|
-
onclick={() => cropper.enableCropMode()}>
|
|
35
|
-
<IconText icon={IconCrop}>{loc.crop}</IconText>
|
|
36
|
-
</button>
|
|
37
|
-
<button type="button" class="img-cropper-toolbar__button" onclick={() => cropper.zoomIn()}>
|
|
38
|
-
<IconText icon={IconZoomIn}>{loc.zoomIn}</IconText>
|
|
39
|
-
</button>
|
|
40
|
-
<button type="button" class="img-cropper-toolbar__button" onclick={() => cropper.zoomOut()}>
|
|
41
|
-
<IconText icon={IconZoomOut}>{loc.zoomOut}</IconText>
|
|
42
|
-
</button>
|
|
43
|
-
<button type="button" class="img-cropper-toolbar__button" onclick={() => cropper.rotate()}>
|
|
44
|
-
<IconText icon={IconArrowRotateClockwise}>{loc.rotate}</IconText>
|
|
45
|
-
</button>
|
|
46
|
-
<button type="button" class="img-cropper-toolbar__button img-cropper-toolbar__button--reset" onclick={() => cropper.reset()}>
|
|
47
|
-
<IconText icon={IconReset}>{loc.reset}</IconText>
|
|
48
|
-
</button>
|
|
26
|
+
{#snippet modeLabel(label: string, active: boolean)}
|
|
27
|
+
<span class="img-cropper-toolbar__mode">
|
|
28
|
+
{label}
|
|
29
|
+
{#if active}
|
|
30
|
+
<Icon src={IconCheckmark} />
|
|
31
|
+
{/if}
|
|
32
|
+
</span>
|
|
33
|
+
{/snippet}
|
|
49
34
|
|
|
35
|
+
<div
|
|
36
|
+
class="img-cropper-toolbar"
|
|
37
|
+
role="toolbar"
|
|
38
|
+
aria-label={loc.toolbar}
|
|
39
|
+
tabindex="-1"
|
|
40
|
+
onpointerdown={(event) => event.stopPropagation()}
|
|
41
|
+
onwheel={(event) => event.stopPropagation()}>
|
|
50
42
|
{#if cropper.ratioOptions}
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
{/each}
|
|
63
|
-
</div>
|
|
64
|
-
</Popover>
|
|
65
|
-
</div>
|
|
43
|
+
<Popover position="top-start" aria-label={loc.ratio} --sc-kit--popover--content--min-width="7.5rem">
|
|
44
|
+
{#snippet trigger()}
|
|
45
|
+
<span class="img-cropper-toolbar__ratio-trigger">
|
|
46
|
+
<Button type="presentational" size="sm" variant="ghost">{ratioLabel}</Button>
|
|
47
|
+
</span>
|
|
48
|
+
{/snippet}
|
|
49
|
+
{#each cropper.ratioOptions as option (String(option.value))}
|
|
50
|
+
<PopoverItem on={{ click: () => cropper.changeAspectRatio(option.value) }}>{option.value === null ? loc.original : option.label}</PopoverItem>
|
|
51
|
+
{/each}
|
|
52
|
+
</Popover>
|
|
53
|
+
<Divider orientation="vertical" />
|
|
66
54
|
{/if}
|
|
67
55
|
|
|
68
|
-
{
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
56
|
+
<Tooltip text={loc.zoomOut}>
|
|
57
|
+
<ToolbarIconButton
|
|
58
|
+
icon={IconZoomOut}
|
|
59
|
+
aria-label={loc.zoomOut}
|
|
60
|
+
disabled={!cropper.ready || cropper.zoom <= cropper.minZoom + 0.001}
|
|
61
|
+
on={{ click: cropper.zoomOut }} />
|
|
62
|
+
</Tooltip>
|
|
63
|
+
<Popover position="top" aria-label={loc.zoom}>
|
|
64
|
+
{#snippet trigger()}
|
|
65
|
+
<span class="img-cropper-toolbar__zoom-trigger">
|
|
66
|
+
<Button type="presentational" size="sm" variant="ghost">{zoomPercent}%</Button>
|
|
67
|
+
</span>
|
|
68
|
+
{/snippet}
|
|
69
|
+
<PopoverItem on={{ click: cropper.fill }}>{@render modeLabel(loc.fillFrame, cropper.restingMode === 'cover')}</PopoverItem>
|
|
70
|
+
<PopoverItem divider on={{ click: cropper.fit }}>{@render modeLabel(loc.fitFrame, cropper.restingMode === 'contain')}</PopoverItem>
|
|
71
|
+
{#each ZOOM_PRESETS as preset (preset)}
|
|
72
|
+
<PopoverItem on={{ click: () => cropper.setZoom(preset) }}>{Math.round(preset * 100)}%</PopoverItem>
|
|
73
|
+
{/each}
|
|
74
|
+
</Popover>
|
|
75
|
+
<Tooltip text={loc.zoomIn}>
|
|
76
|
+
<ToolbarIconButton icon={IconZoomIn} aria-label={loc.zoomIn} disabled={!cropper.ready || cropper.zoom >= cropper.maxZoom} on={{ click: cropper.zoomIn }} />
|
|
77
|
+
</Tooltip>
|
|
78
|
+
|
|
79
|
+
<Divider orientation="vertical" />
|
|
80
|
+
|
|
81
|
+
<Tooltip text={loc.freePosition}>
|
|
82
|
+
<ToolbarIconButton icon={IconArrowMove} aria-label={loc.freePosition} active={cropper.free} disabled={!cropper.ready} on={{ click: cropper.toggleFree }} />
|
|
83
|
+
</Tooltip>
|
|
84
|
+
<Tooltip text={loc.cropMode}>
|
|
85
|
+
<ToolbarIconButton
|
|
86
|
+
icon={IconCrop}
|
|
87
|
+
aria-label={loc.cropMode}
|
|
88
|
+
active={cropper.cropMode}
|
|
89
|
+
disabled={!cropper.ready}
|
|
90
|
+
on={{ click: () => cropper.setCropMode(!cropper.cropMode) }} />
|
|
91
|
+
</Tooltip>
|
|
92
|
+
{#if cropper.hasSelection}
|
|
93
|
+
<Tooltip text={loc.applyCrop}>
|
|
94
|
+
<ToolbarIconButton
|
|
95
|
+
icon={IconCheckmark}
|
|
96
|
+
aria-label={loc.applyCrop}
|
|
97
|
+
on={{ click: cropper.applyCrop }}
|
|
98
|
+
--sc-kit--toolbar-icon-button--color="var(--sc-kit--color--accent)" />
|
|
99
|
+
</Tooltip>
|
|
100
|
+
{/if}
|
|
101
|
+
<Popover position="top" aria-label={loc.align} --sc-kit--popover--content--min-width="0">
|
|
102
|
+
{#snippet trigger()}
|
|
103
|
+
<ToolbarIconButton icon={IconGridDots} presentational />
|
|
104
|
+
{/snippet}
|
|
105
|
+
<div class="img-cropper-toolbar__align" role="group" aria-label={loc.align}>
|
|
106
|
+
{#each ALIGN_FRACTIONS as fy (fy)}
|
|
107
|
+
{#each ALIGN_FRACTIONS as fx (fx)}
|
|
108
|
+
<button type="button" class="img-cropper-toolbar__align-dot" aria-label={loc.alignPosition(fx, fy)} onclick={() => cropper.align(fx, fy)}></button>
|
|
109
|
+
{/each}
|
|
110
|
+
{/each}
|
|
83
111
|
</div>
|
|
112
|
+
</Popover>
|
|
113
|
+
<Tooltip text={loc.rotateLeft}>
|
|
114
|
+
<ToolbarIconButton icon={IconRotateLeft} aria-label={loc.rotateLeft} disabled={!cropper.ready} on={{ click: () => cropper.rotate(-1) }} />
|
|
115
|
+
</Tooltip>
|
|
116
|
+
<Tooltip text={loc.rotateRight}>
|
|
117
|
+
<ToolbarIconButton icon={IconRotateRight} aria-label={loc.rotateRight} disabled={!cropper.ready} on={{ click: () => cropper.rotate(1) }} />
|
|
118
|
+
</Tooltip>
|
|
119
|
+
|
|
120
|
+
<Divider orientation="vertical" />
|
|
121
|
+
|
|
122
|
+
<Tooltip text={loc.reset}>
|
|
123
|
+
<ToolbarIconButton icon={IconArrowReset} aria-label={loc.reset} disabled={!cropper.ready} on={{ click: cropper.reset }} />
|
|
124
|
+
</Tooltip>
|
|
125
|
+
|
|
126
|
+
{#if cropper.showFillColor}
|
|
127
|
+
<Divider orientation="vertical" />
|
|
128
|
+
<ColorPicker value={cropper.fillColor} label={loc.fill} variant="field" size="sm" clearable on={{ change: (value) => (cropper.fillColor = value) }} />
|
|
84
129
|
{/if}
|
|
85
130
|
|
|
86
131
|
{#if children}
|
|
@@ -90,136 +135,59 @@ const selectedRatioLabel = $derived.by(() => {
|
|
|
90
135
|
|
|
91
136
|
<!--
|
|
92
137
|
@component
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
### Props
|
|
98
|
-
| Prop | Type | Default | Description |
|
|
99
|
-
|---|---|---|---|
|
|
100
|
-
| `cropper` | `ImgCropper` | — | Cropper controller instance |
|
|
101
|
-
| `children` | `Snippet` | — | Extra controls rendered at the end |
|
|
138
|
+
Floating toolbar for `ImgCropperView` — aspect ratio, zoom, free position, alignment, rotate, reset, and the frame's
|
|
139
|
+
fill color (in `contain` mode, whenever free position is on, or when the picture has its own transparency). Pass it
|
|
140
|
+
to `ImgCropperView`'s `toolbar` snippet prop.
|
|
102
141
|
|
|
103
142
|
### CSS Custom Properties
|
|
104
143
|
| Property | Description | Default |
|
|
105
144
|
|---|---|---|
|
|
106
|
-
| `--sc-kit--img-cropper-toolbar--
|
|
107
|
-
| `--sc-kit--img-cropper-toolbar--background` | Button background | `light-dark(white, dark-700)` |
|
|
108
|
-
| `--sc-kit--img-cropper-toolbar--border-color` | Button border color | `light-dark(neutral-200, dark-600)` |
|
|
109
|
-
| `--sc-kit--img-cropper-toolbar--active-color` | Active button accent color | `light-dark(primary-500, primary-400)` |
|
|
145
|
+
| `--sc-kit--img-cropper-toolbar--background` | Panel background | `var(--sc-kit--color--bg--panel)` |
|
|
110
146
|
-->
|
|
111
147
|
|
|
112
148
|
<style>.img-cropper-toolbar {
|
|
113
|
-
--_img-cropper-toolbar--color: var(--sc-kit--img-cropper-toolbar--color, var(--sc-kit--color--text--primary));
|
|
114
149
|
--_img-cropper-toolbar--background: var(--sc-kit--img-cropper-toolbar--background, var(--sc-kit--color--bg--panel));
|
|
115
|
-
--
|
|
116
|
-
--
|
|
117
|
-
container-type: inline-size;
|
|
150
|
+
--sc-kit--divider--gap: 0;
|
|
151
|
+
--sc-kit--color-picker--width: 8.75rem;
|
|
118
152
|
display: flex;
|
|
119
153
|
align-items: center;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
--sc-kit--
|
|
123
|
-
|
|
124
|
-
.img-cropper-toolbar__button {
|
|
125
|
-
font-size: 0.75rem;
|
|
126
|
-
border-radius: 0.25rem;
|
|
127
|
-
white-space: nowrap;
|
|
128
|
-
border: 1px solid var(--_img-cropper-toolbar--border-color);
|
|
129
|
-
color: var(--_img-cropper-toolbar--color);
|
|
154
|
+
gap: var(--sc-kit--space--1);
|
|
155
|
+
padding: var(--sc-kit--space--1);
|
|
156
|
+
border: 1px solid var(--sc-kit--color--border);
|
|
157
|
+
border-radius: var(--sc-kit--radius--lg);
|
|
130
158
|
background: var(--_img-cropper-toolbar--background);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
justify-content: center;
|
|
135
|
-
cursor: pointer;
|
|
136
|
-
transition: background-color linear 0.2s, color linear 0.2s, border-color linear 0.2s;
|
|
137
|
-
}
|
|
138
|
-
.img-cropper-toolbar__button:hover, .img-cropper-toolbar__button--active {
|
|
139
|
-
color: var(--_img-cropper-toolbar--active-color);
|
|
140
|
-
border-color: var(--_img-cropper-toolbar--active-color);
|
|
141
|
-
}
|
|
142
|
-
.img-cropper-toolbar__button--reset {
|
|
143
|
-
margin-left: 0.5rem;
|
|
144
|
-
}
|
|
145
|
-
.img-cropper-toolbar__button {
|
|
146
|
-
/* Set 'container-type: inline-size;' to reference container*/
|
|
147
|
-
}
|
|
148
|
-
@container (width < 53.125rem) {
|
|
149
|
-
.img-cropper-toolbar__button {
|
|
150
|
-
min-width: auto;
|
|
151
|
-
padding: 0.25rem;
|
|
152
|
-
--sc-kit--icon-text--text--display: none;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
.img-cropper-toolbar__label {
|
|
156
|
-
font-size: 0.625rem;
|
|
157
|
-
white-space: nowrap;
|
|
158
|
-
margin-right: 0.5rem;
|
|
159
|
-
}
|
|
160
|
-
.img-cropper-toolbar__ratio {
|
|
161
|
-
display: flex;
|
|
162
|
-
align-items: center;
|
|
163
|
-
margin-left: 1rem;
|
|
159
|
+
box-shadow: var(--sc-kit--shadow--lg);
|
|
160
|
+
cursor: default;
|
|
161
|
+
z-index: 1;
|
|
164
162
|
}
|
|
165
163
|
.img-cropper-toolbar__ratio-trigger {
|
|
166
|
-
|
|
167
|
-
border-radius: 0.125rem;
|
|
168
|
-
border: 1px solid var(--_img-cropper-toolbar--border-color);
|
|
169
|
-
cursor: pointer;
|
|
170
|
-
font-size: 0.75rem;
|
|
171
|
-
white-space: nowrap;
|
|
172
|
-
}
|
|
173
|
-
.img-cropper-toolbar__ratio-menu {
|
|
174
|
-
background: var(--_img-cropper-toolbar--background);
|
|
175
|
-
border: 1px solid var(--_img-cropper-toolbar--border-color);
|
|
176
|
-
border-radius: 0.25rem;
|
|
177
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
178
|
-
padding: 0.25rem 0;
|
|
179
|
-
}
|
|
180
|
-
.img-cropper-toolbar__ratio-item {
|
|
181
|
-
display: block;
|
|
182
|
-
width: 100%;
|
|
183
|
-
padding: 0.375rem 0.75rem;
|
|
184
|
-
border: none;
|
|
185
|
-
background: none;
|
|
186
|
-
text-align: left;
|
|
187
|
-
cursor: pointer;
|
|
188
|
-
font-size: 0.75rem;
|
|
189
|
-
color: var(--_img-cropper-toolbar--color);
|
|
164
|
+
--sc-kit--button--width: 5.25rem;
|
|
190
165
|
}
|
|
191
|
-
.img-cropper-
|
|
192
|
-
|
|
166
|
+
.img-cropper-toolbar__zoom-trigger {
|
|
167
|
+
--sc-kit--button--width: 4.25rem;
|
|
193
168
|
}
|
|
194
|
-
.img-cropper-
|
|
169
|
+
.img-cropper-toolbar__mode {
|
|
195
170
|
display: flex;
|
|
196
171
|
align-items: center;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
.img-cropper-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
background-size: 8px 8px;
|
|
210
|
-
background-position: 0 0, 0 4px, 4px -4px, -4px 0;
|
|
211
|
-
}
|
|
212
|
-
.img-cropper-toolbar__fill-reset {
|
|
213
|
-
display: flex;
|
|
214
|
-
align-items: center;
|
|
215
|
-
justify-content: center;
|
|
216
|
-
cursor: pointer;
|
|
217
|
-
color: var(--_img-cropper-toolbar--color);
|
|
218
|
-
background: none;
|
|
219
|
-
border: none;
|
|
172
|
+
justify-content: space-between;
|
|
173
|
+
gap: var(--sc-kit--space--2);
|
|
174
|
+
}
|
|
175
|
+
.img-cropper-toolbar__align {
|
|
176
|
+
display: grid;
|
|
177
|
+
grid-template-columns: repeat(3, 1.5rem);
|
|
178
|
+
gap: 0.25rem;
|
|
179
|
+
padding: var(--sc-kit--space--2);
|
|
180
|
+
}
|
|
181
|
+
.img-cropper-toolbar__align-dot {
|
|
182
|
+
inline-size: 1.5rem;
|
|
183
|
+
block-size: 1.5rem;
|
|
220
184
|
padding: 0;
|
|
221
|
-
--sc-kit--
|
|
185
|
+
border: 1px solid var(--sc-kit--color--border--strong);
|
|
186
|
+
border-radius: var(--sc-kit--radius--sm);
|
|
187
|
+
background: var(--sc-kit--color--bg--field);
|
|
188
|
+
cursor: pointer;
|
|
222
189
|
}
|
|
223
|
-
.img-cropper-
|
|
224
|
-
color: var(--
|
|
190
|
+
.img-cropper-toolbar__align-dot:hover {
|
|
191
|
+
border-color: var(--sc-kit--color--accent);
|
|
192
|
+
background: var(--sc-kit--color--accent--softer);
|
|
225
193
|
}</style>
|