@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.
Files changed (36) hide show
  1. package/dist/core/utils/color-helper.d.ts +4 -0
  2. package/dist/core/utils/color-helper.js +10 -0
  3. package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte +17 -51
  4. package/dist/ui/cropper/image-editor-dialog/crop-and-resize.d.ts +4 -16
  5. package/dist/ui/cropper/image-editor-dialog/crop-and-resize.js +1 -14
  6. package/dist/ui/cropper/image-editor-dialog/index.d.ts +8 -7
  7. package/dist/ui/cropper/image-editor-dialog/index.js +8 -7
  8. package/dist/ui/cropper/image-editor-dialog/types.d.ts +1 -1
  9. package/dist/ui/cropper/img-cropper/cmp.img-cropper-toolbar.svelte +149 -181
  10. package/dist/ui/cropper/img-cropper/cmp.img-cropper-toolbar.svelte.d.ts +4 -13
  11. package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte +389 -147
  12. package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte.d.ts +13 -39
  13. package/dist/ui/cropper/img-cropper/frame-geometry.d.ts +82 -0
  14. package/dist/ui/cropper/img-cropper/frame-geometry.js +165 -0
  15. package/dist/ui/cropper/img-cropper/img-cropper-localization.d.ts +2 -3
  16. package/dist/ui/cropper/img-cropper/img-cropper-localization.js +12 -19
  17. package/dist/ui/cropper/img-cropper/img-cropper-toolbar-localization.d.ts +14 -5
  18. package/dist/ui/cropper/img-cropper/img-cropper-toolbar-localization.js +127 -33
  19. package/dist/ui/cropper/img-cropper/img-cropper-utils.d.ts +4 -51
  20. package/dist/ui/cropper/img-cropper/img-cropper-utils.js +35 -190
  21. package/dist/ui/cropper/img-cropper/img-cropper.svelte.d.ts +99 -34
  22. package/dist/ui/cropper/img-cropper/img-cropper.svelte.js +405 -104
  23. package/dist/ui/cropper/img-cropper/index.d.ts +1 -2
  24. package/dist/ui/cropper/img-cropper/index.js +0 -1
  25. package/package.json +1 -3
  26. package/dist/ui/cropper/img-cropper/cmp.img-cropper-controls.svelte +0 -68
  27. package/dist/ui/cropper/img-cropper/cmp.img-cropper-controls.svelte.d.ts +0 -21
  28. package/dist/ui/cropper/img-cropper/cropperjs-elements.d.ts +0 -35
  29. package/dist/ui/cropper/img-cropper/img-cropper-base-worker.svelte.d.ts +0 -40
  30. package/dist/ui/cropper/img-cropper/img-cropper-base-worker.svelte.js +0 -193
  31. package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.d.ts +0 -7
  32. package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.js +0 -51
  33. package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.d.ts +0 -7
  34. package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.js +0 -89
  35. package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.d.ts +0 -42
  36. package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.js +0 -1
@@ -5,23 +5,14 @@ type Props = {
5
5
  children?: Snippet;
6
6
  };
7
7
  /**
8
- * Toolbar with image manipulation buttons: Move, Crop, Zoom In/Out, Rotate, Reset.
9
- * Shows aspect ratio popover when `cropper.ratioOptions` is set, and fill color
10
- * picker when `cropper.showFillColor` is true (derived from `mode === 'contain'`).
11
- *
12
- * ### Props
13
- * | Prop | Type | Default | Description |
14
- * |---|---|---|---|
15
- * | `cropper` | `ImgCropper` | — | Cropper controller instance |
16
- * | `children` | `Snippet` | — | Extra controls rendered at the end |
8
+ * Floating toolbar for `ImgCropperView` — aspect ratio, zoom, free position, alignment, rotate, reset, and the frame's
9
+ * fill color (in `contain` mode, whenever free position is on, or when the picture has its own transparency). Pass it
10
+ * to `ImgCropperView`'s `toolbar` snippet prop.
17
11
  *
18
12
  * ### CSS Custom Properties
19
13
  * | Property | Description | Default |
20
14
  * |---|---|---|
21
- * | `--sc-kit--img-cropper-toolbar--color` | Button text/icon color | `light-dark(gray-800, white)` |
22
- * | `--sc-kit--img-cropper-toolbar--background` | Button background | `light-dark(white, dark-700)` |
23
- * | `--sc-kit--img-cropper-toolbar--border-color` | Button border color | `light-dark(neutral-200, dark-600)` |
24
- * | `--sc-kit--img-cropper-toolbar--active-color` | Active button accent color | `light-dark(primary-500, primary-400)` |
15
+ * | `--sc-kit--img-cropper-toolbar--background` | Panel background | `var(--sc-kit--color--bg--panel)` |
25
16
  */
26
17
  declare const Cmp: import("svelte").Component<Props, {}, "">;
27
18
  type Cmp = ReturnType<typeof Cmp>;
@@ -1,183 +1,425 @@
1
1
  <script lang="ts">import { Spinner } from '../../spinner';
2
- import { Toastr } from '../../toast';
3
- import { default as ImgCropperControls } from './cmp.img-cropper-controls.svelte';
2
+ import { Text } from '../../typography';
4
3
  import { ImgCropperLocalization } from './img-cropper-localization';
5
- const { src, cropper, showControls = true } = $props();
4
+ import { ImgCropper } from './img-cropper.svelte';
5
+ import { untrack } from 'svelte';
6
+ const { src, cropper, toolbar } = $props();
7
+ const CORNERS = ['nw', 'ne', 'sw', 'se'];
6
8
  const loc = new ImgCropperLocalization();
7
- let cropperLoaded = $state(false);
8
- $effect(() => {
9
- const loadCropper = async () => {
10
- try {
11
- await import('cropperjs');
12
- cropperLoaded = true;
13
- }
14
- catch {
15
- Toastr.error(loc.initCropperError);
16
- }
17
- };
18
- void loadCropper();
19
- });
20
- const initCanvas = (canvasEl) => {
21
- const imageEl = canvasEl.querySelector('cropper-image');
22
- const selectionEl = canvasEl.querySelector('cropper-selection');
23
- const selectHandle = canvasEl.querySelector('cropper-handle[action="select"]');
24
- if (imageEl && selectionEl) {
25
- const initCropper = async () => {
26
- try {
27
- await cropper.init({
28
- src,
29
- cropperImage: imageEl,
30
- cropperSelection: selectionEl,
31
- cropperCanvas: canvasEl,
32
- selectHandle
33
- });
34
- }
35
- catch {
36
- Toastr.error(loc.imageLoadError);
37
- }
38
- };
39
- void initCropper();
9
+ const WHEEL_ZOOM_RATE = 0.0016;
10
+ const DRAW_START_PX = 4;
11
+ let stageEl = $state();
12
+ let drag = $state.raw(null);
13
+ let dragPointer = null;
14
+ let dragStart = { box: null, selection: null };
15
+ $effect(() => untrack(() => {
16
+ const mountedCropper = cropper;
17
+ if (stageEl) {
18
+ mountedCropper.setStage({ w: stageEl.clientWidth, h: stageEl.clientHeight });
40
19
  }
41
- return {
42
- destroy: () => {
43
- cropper.destroy();
44
- }
45
- };
20
+ void mountedCropper.init(src);
21
+ return () => mountedCropper.destroy();
22
+ }));
23
+ const beginDrag = (event, next) => {
24
+ stageEl?.setPointerCapture(event.pointerId);
25
+ dragPointer = event.pointerId;
26
+ dragStart = { box: cropper.box, selection: cropper.selection };
27
+ drag = next;
46
28
  };
47
- const observeResize = (el) => {
48
- const observer = new ResizeObserver(() => {
49
- if (cropper.ready) {
50
- cropper.refit();
29
+ const ignoreWhileDragging = (event) => {
30
+ if (drag) {
31
+ event.stopPropagation();
32
+ }
33
+ };
34
+ const observeStage = (node) => {
35
+ const observer = new ResizeObserver((entries) => {
36
+ const entry = entries.at(0);
37
+ if (entry) {
38
+ cropper.setStage({ w: entry.contentRect.width, h: entry.contentRect.height });
51
39
  }
52
40
  });
53
- observer.observe(el);
41
+ observer.observe(node);
54
42
  return { destroy: () => observer.disconnect() };
55
43
  };
44
+ const stagePoint = (event) => {
45
+ if (!stageEl) {
46
+ return null;
47
+ }
48
+ const stageRect = stageEl.getBoundingClientRect();
49
+ return { x: event.clientX - stageRect.left, y: event.clientY - stageRect.top };
50
+ };
51
+ const framePoint = (event) => {
52
+ const frame = cropper.frame;
53
+ const point = stagePoint(event);
54
+ return frame && point ? { x: point.x - frame.x, y: point.y - frame.y } : null;
55
+ };
56
+ const cornerPoint = (rect, corner) => ({
57
+ x: corner === 'ne' || corner === 'se' ? rect.x + rect.w : rect.x,
58
+ y: corner === 'sw' || corner === 'se' ? rect.y + rect.h : rect.y
59
+ });
60
+ const oppositeCorner = (corner) => (corner === 'nw' ? 'se' : corner === 'se' ? 'nw' : corner === 'ne' ? 'sw' : 'ne');
61
+ const startPan = (event) => {
62
+ if (event.button !== 0 || !cropper.ready) {
63
+ return;
64
+ }
65
+ event.stopPropagation();
66
+ beginDrag(event, { kind: 'pan', last: { x: event.clientX, y: event.clientY } });
67
+ };
68
+ const stagePointerDown = (event) => {
69
+ if (!cropper.cropMode || drag) {
70
+ return;
71
+ }
72
+ if (event.button !== 0) {
73
+ return;
74
+ }
75
+ const frame = cropper.frame;
76
+ const anchor = framePoint(event);
77
+ if (!frame || !anchor) {
78
+ return;
79
+ }
80
+ beginDrag(event, {
81
+ kind: 'selection',
82
+ anchor: { x: Math.min(Math.max(anchor.x, 0), frame.w), y: Math.min(Math.max(anchor.y, 0), frame.h) },
83
+ pressedAt: { x: event.clientX, y: event.clientY }
84
+ });
85
+ cropper.clearSelection();
86
+ };
87
+ const startScale = (event, corner) => {
88
+ const box = cropper.box;
89
+ const start = framePoint(event);
90
+ if (event.button !== 0 || !box || !start) {
91
+ return;
92
+ }
93
+ event.stopPropagation();
94
+ const draggedCorner = cornerPoint(box, corner);
95
+ const anchor = cornerPoint(box, oppositeCorner(corner));
96
+ beginDrag(event, { kind: 'scale', startBox: box, anchor, corner: draggedCorner, start });
97
+ };
98
+ const startSelectionHandle = (event, corner) => {
99
+ const selection = cropper.selection;
100
+ if (event.button !== 0 || !selection) {
101
+ return;
102
+ }
103
+ event.stopPropagation();
104
+ beginDrag(event, { kind: 'selection', anchor: cornerPoint(selection, oppositeCorner(corner)), pressedAt: null });
105
+ };
106
+ const startMoveSelection = (event) => {
107
+ if (event.button !== 0 || !cropper.selection) {
108
+ return;
109
+ }
110
+ event.stopPropagation();
111
+ beginDrag(event, { kind: 'move-selection', last: { x: event.clientX, y: event.clientY } });
112
+ };
113
+ const pointerMove = (event) => {
114
+ if (!drag || event.pointerId !== dragPointer) {
115
+ return;
116
+ }
117
+ if (drag.kind === 'pan') {
118
+ cropper.pan({ x: event.clientX - drag.last.x, y: event.clientY - drag.last.y });
119
+ drag = { kind: 'pan', last: { x: event.clientX, y: event.clientY } };
120
+ return;
121
+ }
122
+ if (drag.kind === 'selection') {
123
+ const { pressedAt } = drag;
124
+ if (pressedAt) {
125
+ if (Math.hypot(event.clientX - pressedAt.x, event.clientY - pressedAt.y) < DRAW_START_PX) {
126
+ return;
127
+ }
128
+ drag = { ...drag, pressedAt: null };
129
+ }
130
+ const point = framePoint(event);
131
+ if (!point) {
132
+ return;
133
+ }
134
+ cropper.updateSelection(drag.anchor, point);
135
+ return;
136
+ }
137
+ if (drag.kind === 'move-selection') {
138
+ cropper.moveSelection({ x: event.clientX - drag.last.x, y: event.clientY - drag.last.y });
139
+ drag = { kind: 'move-selection', last: { x: event.clientX, y: event.clientY } };
140
+ return;
141
+ }
142
+ const point = framePoint(event);
143
+ if (!point) {
144
+ return;
145
+ }
146
+ const diagonal = { x: drag.corner.x - drag.anchor.x, y: drag.corner.y - drag.anchor.y };
147
+ const lengthSquared = diagonal.x * diagonal.x + diagonal.y * diagonal.y || 1;
148
+ const traveled = { x: point.x - drag.start.x, y: point.y - drag.start.y };
149
+ const factor = Math.max(0.05, 1 + (traveled.x * diagonal.x + traveled.y * diagonal.y) / lengthSquared);
150
+ cropper.box = drag.startBox;
151
+ cropper.scaleAbout(factor, drag.anchor);
152
+ };
153
+ const endDrag = (event) => {
154
+ if (event.pointerId !== dragPointer) {
155
+ return;
156
+ }
157
+ if (drag?.kind === 'selection') {
158
+ cropper.finishSelection();
159
+ }
160
+ drag = null;
161
+ dragPointer = null;
162
+ };
163
+ const cancelDrag = (event) => {
164
+ if (event.pointerId !== dragPointer) {
165
+ return;
166
+ }
167
+ cropper.box = dragStart.box;
168
+ cropper.selection = dragStart.selection;
169
+ drag = null;
170
+ dragPointer = null;
171
+ };
172
+ const wheelZoom = (event) => {
173
+ if (!cropper.ready) {
174
+ return;
175
+ }
176
+ event.preventDefault();
177
+ if (drag) {
178
+ return;
179
+ }
180
+ const anchor = framePoint(event);
181
+ if (anchor) {
182
+ cropper.scaleAbout(Math.exp(-event.deltaY * WHEEL_ZOOM_RATE), anchor);
183
+ }
184
+ };
185
+ const placement = (rect) => `left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px`;
186
+ const pictureStyle = (box) => {
187
+ const placement = cropper.sourcePlacement({ x: 0, y: 0, w: box.w, h: box.h });
188
+ if (!placement) {
189
+ return '';
190
+ }
191
+ const { center, size } = placement;
192
+ const turned = cropper.rotation === 90 || cropper.rotation === 270;
193
+ return `left:${center.x}px;top:${center.y}px;width:${turned ? size.h : size.w}px;height:${turned ? size.w : size.h}px;rotate:${cropper.rotation}deg`;
194
+ };
56
195
  </script>
57
196
 
58
- <div class="img-cropper" use:observeResize style:--img-cropper--fill-color={cropper.fillColor || undefined}>
59
- {#if cropper.loading || !cropper.ready}
60
- <Spinner position="absolute-center" size="lg" />
61
- {/if}
62
-
63
- {#if cropperLoaded}
64
- <cropper-canvas
65
- class="img-cropper__canvas"
66
- class:img-cropper__canvas--visible={cropper.ready}
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}
70
- use:initCanvas
71
- background={(cropper.showFillColor && cropper.isTransparentFill) || undefined}>
72
- <cropper-image initial-center-size={cropper.mode} scalable translatable rotatable> </cropper-image>
73
- <cropper-shade hidden></cropper-shade>
74
- <cropper-handle action="select" plain></cropper-handle>
75
- <cropper-selection movable resizable aspect-ratio={cropper.aspectRatio ?? undefined}>
76
- <cropper-grid role="grid" bordered covered></cropper-grid>
77
- <cropper-crosshair centered></cropper-crosshair>
78
- <cropper-handle action="move" theme-color="rgba(255,255,255,0.35)"></cropper-handle>
79
- <cropper-handle action="n-resize"></cropper-handle>
80
- <cropper-handle action="e-resize"></cropper-handle>
81
- <cropper-handle action="s-resize"></cropper-handle>
82
- <cropper-handle action="w-resize"></cropper-handle>
83
- <cropper-handle action="ne-resize"></cropper-handle>
84
- <cropper-handle action="nw-resize"></cropper-handle>
85
- <cropper-handle action="se-resize"></cropper-handle>
86
- <cropper-handle action="sw-resize"></cropper-handle>
87
- </cropper-selection>
88
- </cropper-canvas>
89
-
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>
197
+ <div class="img-cropper">
198
+ <div
199
+ class="img-cropper__stage"
200
+ class:img-cropper__stage--dragging={drag !== null && !cropper.cropMode}
201
+ class:img-cropper__stage--crop-mode={cropper.cropMode}
202
+ role="presentation"
203
+ bind:this={stageEl}
204
+ use:observeStage
205
+ onpointerdowncapture={ignoreWhileDragging}
206
+ onpointerdown={stagePointerDown}
207
+ onpointermove={pointerMove}
208
+ onpointerup={endDrag}
209
+ onpointercancel={cancelDrag}
210
+ onwheel={wheelZoom}>
211
+ {#if cropper.failed}
212
+ <div class="img-cropper__state"><Text size="sm" tone="muted">{loc.imageLoadError}</Text></div>
213
+ {:else if !cropper.ready || !cropper.box || !cropper.frame}
214
+ <div class="img-cropper__state"><Spinner /></div>
215
+ {:else}
216
+ {@const box = cropper.box}
217
+ {@const frame = cropper.frame}
218
+ <div class="img-cropper__picture-box img-cropper__picture-box--outside" style={placement({ x: frame.x + box.x, y: frame.y + box.y, w: box.w, h: box.h })}>
219
+ <img class="img-cropper__picture" src={cropper.source} alt="" draggable="false" style={pictureStyle(box)} />
220
+ </div>
221
+ <div class="img-cropper__frame" style={placement(frame)} style:--_--img-cropper--fill-color={cropper.fillColor || undefined}>
222
+ <div class="img-cropper__picture-box" style={placement(box)}>
223
+ <img class="img-cropper__picture" src={cropper.source} alt="" draggable="false" style={pictureStyle(box)} />
224
+ </div>
225
+ </div>
226
+ <div class="img-cropper__outline" style={placement(frame)}></div>
227
+ {#if cropper.cropMode}
228
+ {#if cropper.selection}
229
+ {@const selection = cropper.selection}
230
+ <div
231
+ class="img-cropper__selection"
232
+ class:img-cropper__selection--dragging={drag !== null}
233
+ role="presentation"
234
+ style={placement({ x: frame.x + selection.x, y: frame.y + selection.y, w: selection.w, h: selection.h })}
235
+ onpointerdown={startMoveSelection}>
236
+ </div>
237
+ {#each CORNERS as corner (corner)}
238
+ {@const point = cornerPoint(selection, corner)}
239
+ <button
240
+ type="button"
241
+ class="img-cropper__handle img-cropper__handle--{corner}"
242
+ aria-label={loc.resizeSelection}
243
+ tabindex="-1"
244
+ style:left="{frame.x + point.x}px"
245
+ style:top="{frame.y + point.y}px"
246
+ onpointerdown={(event) => startSelectionHandle(event, corner)}>
247
+ </button>
248
+ {/each}
249
+ {/if}
250
+ {:else}
251
+ <div
252
+ class="img-cropper__hit"
253
+ class:img-cropper__hit--dragging={drag !== null}
254
+ role="presentation"
255
+ style={placement({ x: frame.x + box.x, y: frame.y + box.y, w: box.w, h: box.h })}
256
+ onpointerdown={startPan}>
257
+ <div class="img-cropper__picture-outline"></div>
258
+ {#each CORNERS as corner (corner)}
259
+ <button
260
+ type="button"
261
+ class="img-cropper__handle img-cropper__handle--{corner} img-cropper__handle--picture"
262
+ aria-label={loc.resizeHandle}
263
+ tabindex="-1"
264
+ style:left={corner === 'ne' || corner === 'se' ? '100%' : '0'}
265
+ style:top={corner === 'sw' || corner === 'se' ? '100%' : '0'}
266
+ onpointerdown={(event) => startScale(event, corner)}>
267
+ </button>
268
+ {/each}
269
+ </div>
270
+ {/if}
92
271
  {/if}
93
- {/if}
94
-
95
- {#if showControls}
96
- <div class="img-cropper__controls">
97
- <ImgCropperControls cropper={cropper} />
272
+ </div>
273
+ {#if toolbar}
274
+ <div class="img-cropper__toolbar" inert={drag !== null}>
275
+ {@render toolbar()}
98
276
  </div>
99
277
  {/if}
100
278
  </div>
101
279
 
102
280
  <!--
103
281
  @component
104
- Image cropper view based on CropperJS v2 web components.
105
-
106
- Renders a canvas with crop selection, rotation, zoom, and reset capabilities.
107
- The container fills 100% width and height of its parent; the image is centered inside.
108
- The component itself uses `overflow: visible` — the parent element should set
109
- `overflow: hidden` to clip the shadow overlay at its boundaries.
110
-
111
- #### Modes
112
-
113
- The behavior is determined by the `ImgCropper` instance passed via the `cropper` prop:
114
-
115
- - **`cover`** — the image always fills the canvas completely. The crop selection
116
- defines the visible area. After save, the result is the selected region at
117
- natural image resolution.
118
- - **`contain`** — the image is fitted inside the canvas with optional fill color
119
- for empty space. After crop, the canvas shrinks to match the cropped area's
120
- aspect ratio. After save, the result includes the fill color background.
121
-
122
- #### Aspect ratio
123
-
124
- Aspect ratio is configured on the `ImgCropper` instance via the `aspectRatio` option:
125
-
126
- - **Fixed number** (e.g. `16/9`) — locks the crop selection to this ratio.
127
- - **Dynamic object** (`ImgCropperDynamicAspectRatio`) — provides a list of `supported`
128
- ratios and an optional `initial` value. The toolbar renders a ratio popover 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).
132
-
133
- #### Shadow overlay
134
-
135
- When `showImageShadow` is enabled on the cropper instance, a dark semi-transparent
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.
282
+ Fixed-frame picture cropper — the frame stays put on the stage; drag pans the picture, the corner handles (shown while
283
+ the pointer is over the picture, always on touch) scale it about the opposite corner, and the wheel zooms about the
284
+ pointer. The part of the picture spilling past the frame stays visible but dimmed. Pass a `toolbar` snippet to mount
285
+ `ImgCropperToolbar` underneath the picture area. `src` and `cropper` are read once, on mount — to change either, remount
286
+ the view (e.g. `{#key}`) with a new `ImgCropper`.
139
287
 
140
288
  ### CSS Custom Properties
141
- None — canvas background, shadow, and sizing are controlled by the `ImgCropper` instance.
289
+ | Property | Description | Default |
290
+ |---|---|---|
291
+ | `--sc-kit--img-cropper--handle-color` | Corner handle fill | `#ffffff` |
292
+ | `--sc-kit--img-cropper--outline-color` | Frame outline color | `#4a4a4f` |
142
293
  -->
143
294
 
144
295
  <style>.img-cropper {
145
- --_fill-color: var(--img-cropper--fill-color);
296
+ --_img-cropper--handle-color: var(--sc-kit--img-cropper--handle-color, #ffffff);
297
+ --_img-cropper--outline-color: var(--sc-kit--img-cropper--outline-color, #4a4a4f);
298
+ --_img-cropper--background: var(--sc-kit--img-cropper--background, #1e1e20);
299
+ display: flex;
300
+ flex-direction: column;
301
+ inline-size: 100%;
302
+ block-size: 100%;
303
+ overflow: hidden;
304
+ background: var(--_img-cropper--background);
305
+ }
306
+ .img-cropper__stage {
146
307
  position: relative;
147
- width: 100%;
148
- height: 100%;
308
+ flex: 1;
309
+ min-block-size: 0;
310
+ touch-action: none;
311
+ user-select: none;
312
+ }
313
+ .img-cropper__stage--dragging {
314
+ cursor: grabbing;
315
+ }
316
+ .img-cropper__stage--crop-mode {
317
+ cursor: crosshair;
318
+ }
319
+ .img-cropper__toolbar {
149
320
  display: flex;
321
+ flex-shrink: 0;
150
322
  justify-content: center;
323
+ padding-inline: var(--sc-kit--space--4);
324
+ padding-block-end: var(--sc-kit--space--4);
325
+ }
326
+ .img-cropper__state {
327
+ position: absolute;
328
+ inset: 0;
329
+ display: flex;
151
330
  align-items: center;
152
- overflow: visible;
331
+ justify-content: center;
153
332
  }
154
- .img-cropper__canvas {
155
- width: 100%;
156
- height: 100%;
157
- min-width: 0;
158
- min-height: 0;
159
- background-color: var(--_fill-color);
160
- visibility: hidden;
333
+ .img-cropper__hit {
334
+ --_img-cropper--picture-controls: hidden;
335
+ position: absolute;
336
+ cursor: grab;
161
337
  }
162
- .img-cropper__canvas--visible {
163
- visibility: visible;
338
+ .img-cropper__hit::before {
339
+ content: "";
340
+ position: absolute;
341
+ inset: -0.5rem;
342
+ }
343
+ .img-cropper__hit:hover {
344
+ --_img-cropper--picture-controls: visible;
345
+ }
346
+ @media (any-pointer: coarse) {
347
+ .img-cropper__hit {
348
+ --_img-cropper--picture-controls: visible;
349
+ }
350
+ }
351
+ .img-cropper__hit--dragging {
352
+ --_img-cropper--picture-controls: visible;
353
+ cursor: grabbing;
354
+ }
355
+ .img-cropper__picture-box {
356
+ position: absolute;
357
+ pointer-events: none;
358
+ overflow: hidden;
359
+ }
360
+ .img-cropper__picture-box--outside {
361
+ opacity: 0.35;
362
+ }
363
+ .img-cropper__picture {
364
+ position: absolute;
365
+ max-inline-size: none;
366
+ translate: -50% -50%;
367
+ }
368
+ .img-cropper__frame {
369
+ --_img-cropper--fill-color: var(--_--img-cropper--fill-color, transparent);
370
+ --_check: rgb(255 255 255 / 10%);
371
+ position: absolute;
372
+ overflow: hidden;
373
+ background-image: linear-gradient(var(--_img-cropper--fill-color), var(--_img-cropper--fill-color)), linear-gradient(45deg, var(--_check) 25%, transparent 25%, transparent 75%, var(--_check) 75%), linear-gradient(45deg, var(--_check) 25%, transparent 25%, transparent 75%, var(--_check) 75%);
374
+ background-size: 100% 100%, 1rem 1rem, 1rem 1rem;
375
+ background-position: 0 0, 0 0, 0.5rem 0.5rem;
164
376
  }
165
- .img-cropper__canvas--shadow {
166
- overflow: visible;
377
+ .img-cropper__outline {
378
+ position: absolute;
379
+ border: 1px solid var(--_img-cropper--outline-color);
380
+ pointer-events: none;
167
381
  }
168
- .img-cropper__shadow {
382
+ .img-cropper__picture-outline {
169
383
  position: absolute;
170
- top: 50%;
171
- left: 50%;
172
- transform: translate(-50%, -50%);
173
- box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.8);
384
+ inset: 0;
385
+ border: 1px dashed rgba(255, 255, 255, 0.55);
174
386
  pointer-events: none;
175
- z-index: 2;
387
+ visibility: var(--_img-cropper--picture-controls);
388
+ }
389
+ .img-cropper__selection {
390
+ position: absolute;
391
+ border: 1px dashed rgba(255, 255, 255, 0.55);
392
+ box-shadow: 0 0 0 999rem rgba(0, 0, 0, 0.55);
393
+ cursor: grab;
394
+ }
395
+ .img-cropper__selection--dragging {
396
+ cursor: grabbing;
176
397
  }
177
- .img-cropper__controls {
398
+ .img-cropper__handle {
178
399
  position: absolute;
179
- bottom: 0;
180
- left: 50%;
181
- transform: translateX(-50%);
182
- z-index: 100;
400
+ inline-size: 0.75rem;
401
+ block-size: 0.75rem;
402
+ padding: 0;
403
+ border: 0;
404
+ background: transparent;
405
+ translate: -50% -50%;
406
+ cursor: nwse-resize;
407
+ }
408
+ .img-cropper__handle::before {
409
+ content: "";
410
+ position: absolute;
411
+ inset: 0.1875rem;
412
+ border-radius: 50%;
413
+ background: var(--_img-cropper--handle-color);
414
+ box-shadow: var(--sc-kit--shadow--sm);
415
+ }
416
+ .img-cropper__handle--ne, .img-cropper__handle--sw {
417
+ cursor: nesw-resize;
418
+ }
419
+ .img-cropper__handle--picture {
420
+ visibility: var(--_img-cropper--picture-controls);
421
+ }
422
+ .img-cropper__handle:focus-visible {
423
+ outline: 2px solid var(--sc-kit--color--border--focus);
424
+ outline-offset: 1px;
183
425
  }</style>
@@ -1,49 +1,23 @@
1
- import type { ImgCropper } from './img-cropper.svelte';
1
+ import { ImgCropper } from './img-cropper.svelte';
2
+ import { type Snippet } from 'svelte';
2
3
  type Props = {
4
+ /** The picture — same-origin, a `blob:` / `data:` URL, or a server that sends CORS headers; anything else fails to load. */
3
5
  src: string;
4
6
  cropper: ImgCropper;
5
- /** Show the built-in Apply/Cancel crop controls @default true */
6
- showControls?: boolean;
7
+ toolbar?: Snippet;
7
8
  };
8
9
  /**
9
- * Image cropper view based on CropperJS v2 web components.
10
- *
11
- * Renders a canvas with crop selection, rotation, zoom, and reset capabilities.
12
- * The container fills 100% width and height of its parent; the image is centered inside.
13
- * The component itself uses `overflow: visible` — the parent element should set
14
- * `overflow: hidden` to clip the shadow overlay at its boundaries.
15
- *
16
- * #### Modes
17
- *
18
- * The behavior is determined by the `ImgCropper` instance passed via the `cropper` prop:
19
- *
20
- * - **`cover`** — the image always fills the canvas completely. The crop selection
21
- * defines the visible area. After save, the result is the selected region at
22
- * natural image resolution.
23
- * - **`contain`** — the image is fitted inside the canvas with optional fill color
24
- * for empty space. After crop, the canvas shrinks to match the cropped area's
25
- * aspect ratio. After save, the result includes the fill color background.
26
- *
27
- * #### Aspect ratio
28
- *
29
- * Aspect ratio is configured on the `ImgCropper` instance via the `aspectRatio` option:
30
- *
31
- * - **Fixed number** (e.g. `16/9`) — locks the crop selection to this ratio.
32
- * - **Dynamic object** (`ImgCropperDynamicAspectRatio`) — provides a list of `supported`
33
- * ratios and an optional `initial` value. The toolbar renders a ratio popover when
34
- * multiple options are available. `allowFreeCrop: true` adds a free-ratio option
35
- * that lets the user draw an unconstrained selection.
36
- * - **Omitted** — defaults to free crop (no ratio constraint on the selection).
37
- *
38
- * #### Shadow overlay
39
- *
40
- * When `showImageShadow` is enabled on the cropper instance, a dark semi-transparent
41
- * `box-shadow` is applied directly to the `<cropper-canvas>` element, extending beyond
42
- * the canvas bounds so the surrounding area is dimmed. The parent element should set
43
- * `overflow: hidden` to clip the shadow at its boundaries.
10
+ * Fixed-frame picture cropper — the frame stays put on the stage; drag pans the picture, the corner handles (shown while
11
+ * the pointer is over the picture, always on touch) scale it about the opposite corner, and the wheel zooms about the
12
+ * pointer. The part of the picture spilling past the frame stays visible but dimmed. Pass a `toolbar` snippet to mount
13
+ * `ImgCropperToolbar` underneath the picture area. `src` and `cropper` are read once, on mount — to change either, remount
14
+ * the view (e.g. `{#key}`) with a new `ImgCropper`.
44
15
  *
45
16
  * ### CSS Custom Properties
46
- * None — canvas background, shadow, and sizing are controlled by the `ImgCropper` instance.
17
+ * | Property | Description | Default |
18
+ * |---|---|---|
19
+ * | `--sc-kit--img-cropper--handle-color` | Corner handle fill | `#ffffff` |
20
+ * | `--sc-kit--img-cropper--outline-color` | Frame outline color | `#4a4a4f` |
47
21
  */
48
22
  declare const Cmp: import("svelte").Component<Props, {}, "">;
49
23
  type Cmp = ReturnType<typeof Cmp>;