@gradio/imageslider 0.2.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.
@@ -0,0 +1,349 @@
1
+ export class ZoomableImage {
2
+ container;
3
+ image;
4
+ scale;
5
+ offsetX;
6
+ offsetY;
7
+ isDragging;
8
+ lastX;
9
+ lastY;
10
+ initial_left_padding;
11
+ initial_top_padding;
12
+ initial_width;
13
+ initial_height;
14
+ subscribers;
15
+ handleImageLoad;
16
+ real_image_size = { top: 0, left: 0, width: 0, height: 0 };
17
+ last_touch_distance;
18
+ constructor(container, image) {
19
+ this.container = container;
20
+ this.image = image;
21
+ this.scale = 1;
22
+ this.offsetX = 0;
23
+ this.offsetY = 0;
24
+ this.isDragging = false;
25
+ this.lastX = 0;
26
+ this.lastY = 0;
27
+ this.initial_left_padding = 0;
28
+ this.initial_top_padding = 0;
29
+ this.initial_width = 0;
30
+ this.initial_height = 0;
31
+ this.subscribers = [];
32
+ this.last_touch_distance = 0;
33
+ this.handleWheel = this.handleWheel.bind(this);
34
+ this.handleMouseDown = this.handleMouseDown.bind(this);
35
+ this.handleMouseMove = this.handleMouseMove.bind(this);
36
+ this.handleMouseUp = this.handleMouseUp.bind(this);
37
+ this.handleImageLoad = this.init.bind(this);
38
+ this.handleTouchStart = this.handleTouchStart.bind(this);
39
+ this.handleTouchMove = this.handleTouchMove.bind(this);
40
+ this.handleTouchEnd = this.handleTouchEnd.bind(this);
41
+ this.image.addEventListener("load", this.handleImageLoad);
42
+ this.container.addEventListener("wheel", this.handleWheel);
43
+ this.container.addEventListener("mousedown", this.handleMouseDown);
44
+ document.addEventListener("mousemove", this.handleMouseMove);
45
+ document.addEventListener("mouseup", this.handleMouseUp);
46
+ this.container.addEventListener("touchstart", this.handleTouchStart);
47
+ document.addEventListener("touchmove", this.handleTouchMove);
48
+ document.addEventListener("touchend", this.handleTouchEnd);
49
+ const observer = new ResizeObserver((entries) => {
50
+ for (const entry of entries) {
51
+ if (entry.target === this.container) {
52
+ this.handleResize();
53
+ this.get_image_size(this.image);
54
+ }
55
+ }
56
+ });
57
+ observer.observe(this.container);
58
+ }
59
+ handleResize() {
60
+ this.init();
61
+ }
62
+ init() {
63
+ const containerRect = this.container.getBoundingClientRect();
64
+ const imageRect = this.image.getBoundingClientRect();
65
+ this.initial_left_padding = imageRect.left - containerRect.left;
66
+ this.initial_top_padding = imageRect.top - containerRect.top;
67
+ this.initial_width = imageRect.width;
68
+ this.initial_height = imageRect.height;
69
+ this.reset_zoom();
70
+ this.updateTransform();
71
+ }
72
+ reset_zoom() {
73
+ this.scale = 1;
74
+ this.offsetX = 0;
75
+ this.offsetY = 0;
76
+ this.updateTransform();
77
+ }
78
+ handleMouseDown(e) {
79
+ const imageRect = this.image.getBoundingClientRect();
80
+ if (e.clientX >= imageRect.left &&
81
+ e.clientX <= imageRect.right &&
82
+ e.clientY >= imageRect.top &&
83
+ e.clientY <= imageRect.bottom) {
84
+ e.preventDefault();
85
+ if (this.scale === 1)
86
+ return;
87
+ this.isDragging = true;
88
+ this.lastX = e.clientX;
89
+ this.lastY = e.clientY;
90
+ this.image.style.cursor = "grabbing";
91
+ }
92
+ }
93
+ handleMouseMove(e) {
94
+ if (!this.isDragging)
95
+ return;
96
+ const deltaX = e.clientX - this.lastX;
97
+ const deltaY = e.clientY - this.lastY;
98
+ this.offsetX += deltaX;
99
+ this.offsetY += deltaY;
100
+ this.lastX = e.clientX;
101
+ this.lastY = e.clientY;
102
+ this.updateTransform();
103
+ this.updateTransform();
104
+ }
105
+ handleMouseUp() {
106
+ if (this.isDragging) {
107
+ this.constrain_to_bounds(true);
108
+ this.updateTransform();
109
+ this.isDragging = false;
110
+ this.image.style.cursor = this.scale > 1 ? "grab" : "zoom-in";
111
+ }
112
+ }
113
+ async handleWheel(e) {
114
+ e.preventDefault();
115
+ const containerRect = this.container.getBoundingClientRect();
116
+ const imageRect = this.image.getBoundingClientRect();
117
+ if (e.clientX < imageRect.left ||
118
+ e.clientX > imageRect.right ||
119
+ e.clientY < imageRect.top ||
120
+ e.clientY > imageRect.bottom) {
121
+ return;
122
+ }
123
+ const zoomFactor = 1.05;
124
+ const oldScale = this.scale;
125
+ const newScale = -Math.sign(e.deltaY) > 0
126
+ ? Math.min(15, oldScale * zoomFactor) // in
127
+ : Math.max(1, oldScale / zoomFactor); // out
128
+ if (newScale === oldScale)
129
+ return;
130
+ const cursorX = e.clientX - containerRect.left - this.initial_left_padding;
131
+ const cursorY = e.clientY - containerRect.top - this.initial_top_padding;
132
+ this.scale = newScale;
133
+ this.offsetX = this.compute_new_offset({
134
+ cursor_position: cursorX,
135
+ current_offset: this.offsetX,
136
+ new_scale: newScale,
137
+ old_scale: oldScale
138
+ });
139
+ this.offsetY = this.compute_new_offset({
140
+ cursor_position: cursorY,
141
+ current_offset: this.offsetY,
142
+ new_scale: newScale,
143
+ old_scale: oldScale
144
+ });
145
+ this.updateTransform(); // apply before constraints
146
+ this.constrain_to_bounds();
147
+ this.updateTransform(); // apply again after constraints
148
+ this.image.style.cursor = this.scale > 1 ? "grab" : "zoom-in";
149
+ }
150
+ // compute_offset_for_positions({ position: number, scale: number }) {
151
+ // return position - (scale / this.scale) * (position - this.offset);
152
+ // }
153
+ compute_new_position({ position, scale, anchor_position }) {
154
+ return position - (position - anchor_position) * (scale / this.scale);
155
+ }
156
+ compute_new_offset({ cursor_position, current_offset, new_scale, old_scale }) {
157
+ return (cursor_position -
158
+ (new_scale / old_scale) * (cursor_position - current_offset));
159
+ }
160
+ constrain_to_bounds(pan = false) {
161
+ if (this.scale === 1) {
162
+ this.offsetX = 0;
163
+ this.offsetY = 0;
164
+ return;
165
+ }
166
+ const onscreen = {
167
+ top: this.real_image_size.top * this.scale + this.offsetY,
168
+ left: this.real_image_size.left * this.scale + this.offsetX,
169
+ width: this.real_image_size.width * this.scale,
170
+ height: this.real_image_size.height * this.scale,
171
+ bottom: this.real_image_size.top * this.scale +
172
+ this.offsetY +
173
+ this.real_image_size.height * this.scale,
174
+ right: this.real_image_size.left * this.scale +
175
+ this.offsetX +
176
+ this.real_image_size.width * this.scale
177
+ };
178
+ const real_image_size_right = this.real_image_size.left + this.real_image_size.width;
179
+ const real_image_size_bottom = this.real_image_size.top + this.real_image_size.height;
180
+ if (pan) {
181
+ if (onscreen.top > this.real_image_size.top) {
182
+ this.offsetY = this.calculate_position(this.real_image_size.top, 0, "y");
183
+ }
184
+ else if (onscreen.bottom < real_image_size_bottom) {
185
+ this.offsetY = this.calculate_position(real_image_size_bottom, 1, "y");
186
+ }
187
+ if (onscreen.left > this.real_image_size.left) {
188
+ this.offsetX = this.calculate_position(this.real_image_size.left, 0, "x");
189
+ }
190
+ else if (onscreen.right < real_image_size_right) {
191
+ this.offsetX = this.calculate_position(real_image_size_right, 1, "x");
192
+ }
193
+ }
194
+ }
195
+ updateTransform() {
196
+ this.notify({ x: this.offsetX, y: this.offsetY, scale: this.scale });
197
+ }
198
+ destroy() {
199
+ this.container.removeEventListener("wheel", this.handleWheel);
200
+ this.container.removeEventListener("mousedown", this.handleMouseDown);
201
+ document.removeEventListener("mousemove", this.handleMouseMove);
202
+ document.removeEventListener("mouseup", this.handleMouseUp);
203
+ this.container.removeEventListener("touchstart", this.handleTouchStart);
204
+ document.removeEventListener("touchmove", this.handleTouchMove);
205
+ document.removeEventListener("touchend", this.handleTouchEnd);
206
+ this.image.removeEventListener("load", this.handleImageLoad);
207
+ }
208
+ subscribe(cb) {
209
+ this.subscribers.push(cb);
210
+ }
211
+ unsubscribe(cb) {
212
+ this.subscribers = this.subscribers.filter((subscriber) => subscriber !== cb);
213
+ }
214
+ notify({ x, y, scale }) {
215
+ this.subscribers.forEach((subscriber) => subscriber({ x, y, scale }));
216
+ }
217
+ handleTouchStart(e) {
218
+ e.preventDefault();
219
+ const imageRect = this.image.getBoundingClientRect();
220
+ const touch = e.touches[0];
221
+ if (touch.clientX >= imageRect.left &&
222
+ touch.clientX <= imageRect.right &&
223
+ touch.clientY >= imageRect.top &&
224
+ touch.clientY <= imageRect.bottom) {
225
+ if (e.touches.length === 1 && this.scale > 1) {
226
+ // one finger == prepare pan
227
+ this.isDragging = true;
228
+ this.lastX = touch.clientX;
229
+ this.lastY = touch.clientY;
230
+ }
231
+ else if (e.touches.length === 2) {
232
+ // two fingers == prepare pinch zoom
233
+ const touch1 = e.touches[0];
234
+ const touch2 = e.touches[1];
235
+ this.last_touch_distance = Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY);
236
+ }
237
+ }
238
+ }
239
+ get_image_size(img) {
240
+ if (!img)
241
+ return;
242
+ const container = img.parentElement?.getBoundingClientRect();
243
+ if (!container)
244
+ return;
245
+ const naturalAspect = img.naturalWidth / img.naturalHeight;
246
+ const containerAspect = container.width / container.height;
247
+ let displayedWidth, displayedHeight;
248
+ if (naturalAspect > containerAspect) {
249
+ displayedWidth = container.width;
250
+ displayedHeight = container.width / naturalAspect;
251
+ }
252
+ else {
253
+ displayedHeight = container.height;
254
+ displayedWidth = container.height * naturalAspect;
255
+ }
256
+ const offsetX = (container.width - displayedWidth) / 2;
257
+ const offsetY = (container.height - displayedHeight) / 2;
258
+ this.real_image_size = {
259
+ top: offsetY,
260
+ left: offsetX,
261
+ width: displayedWidth,
262
+ height: displayedHeight
263
+ };
264
+ }
265
+ handleTouchMove(e) {
266
+ if (e.touches.length === 1 && this.isDragging) {
267
+ // one finger == pan
268
+ e.preventDefault();
269
+ const touch = e.touches[0];
270
+ const deltaX = touch.clientX - this.lastX;
271
+ const deltaY = touch.clientY - this.lastY;
272
+ this.offsetX += deltaX;
273
+ this.offsetY += deltaY;
274
+ this.lastX = touch.clientX;
275
+ this.lastY = touch.clientY;
276
+ this.updateTransform();
277
+ }
278
+ else if (e.touches.length === 2) {
279
+ // two fingers == pinch zoom
280
+ e.preventDefault();
281
+ const touch1 = e.touches[0];
282
+ const touch2 = e.touches[1];
283
+ const current_distance = Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY);
284
+ if (this.last_touch_distance === 0) {
285
+ this.last_touch_distance = current_distance;
286
+ return;
287
+ }
288
+ const zoomFactor = current_distance / this.last_touch_distance;
289
+ const oldScale = this.scale;
290
+ const newScale = Math.min(15, Math.max(1, oldScale * zoomFactor));
291
+ if (newScale === oldScale) {
292
+ this.last_touch_distance = current_distance;
293
+ return;
294
+ }
295
+ // midpoint of touches relative to image
296
+ const containerRect = this.container.getBoundingClientRect();
297
+ const midX = (touch1.clientX + touch2.clientX) / 2 -
298
+ containerRect.left -
299
+ this.initial_left_padding;
300
+ const midY = (touch1.clientY + touch2.clientY) / 2 -
301
+ containerRect.top -
302
+ this.initial_top_padding;
303
+ this.scale = newScale;
304
+ this.offsetX = this.compute_new_offset({
305
+ cursor_position: midX,
306
+ current_offset: this.offsetX,
307
+ new_scale: newScale,
308
+ old_scale: oldScale
309
+ });
310
+ this.offsetY = this.compute_new_offset({
311
+ cursor_position: midY,
312
+ current_offset: this.offsetY,
313
+ new_scale: newScale,
314
+ old_scale: oldScale
315
+ });
316
+ this.updateTransform();
317
+ this.constrain_to_bounds();
318
+ this.updateTransform();
319
+ this.last_touch_distance = current_distance;
320
+ this.image.style.cursor = this.scale > 1 ? "grab" : "zoom-in";
321
+ }
322
+ }
323
+ handleTouchEnd(e) {
324
+ if (this.isDragging) {
325
+ this.constrain_to_bounds(true);
326
+ this.updateTransform();
327
+ this.isDragging = false;
328
+ }
329
+ if (e.touches.length === 0) {
330
+ this.last_touch_distance = 0;
331
+ }
332
+ }
333
+ calculate_position(screen_coord, image_anchor, axis) {
334
+ const containerRect = this.container.getBoundingClientRect();
335
+ // Calculate X offset if requested
336
+ if (axis === "x") {
337
+ const relative_screen_x = screen_coord;
338
+ const anchor_x = this.real_image_size.left + image_anchor * this.real_image_size.width;
339
+ return relative_screen_x - anchor_x * this.scale;
340
+ }
341
+ // Calculate Y offset if requested
342
+ if (axis === "y") {
343
+ const relative_screen_y = screen_coord;
344
+ const anchor_y = this.real_image_size.top + image_anchor * this.real_image_size.height;
345
+ return relative_screen_y - anchor_y * this.scale;
346
+ }
347
+ return 0;
348
+ }
349
+ }
package/img_01.png ADDED
Binary file
package/img_02.png ADDED
Binary file
package/img_03.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@gradio/imageslider",
3
+ "version": "0.2.0",
4
+ "description": "Gradio UI packages",
5
+ "type": "module",
6
+ "author": "",
7
+ "license": "ISC",
8
+ "private": false,
9
+ "dependencies": {
10
+ "@types/d3-drag": "^3.0.7",
11
+ "@types/d3-selection": "^3.0.11",
12
+ "d3-drag": "^3.0.0",
13
+ "d3-selection": "^3.0.0",
14
+ "@gradio/atoms": "^0.16.0",
15
+ "@gradio/client": "^1.14.2",
16
+ "@gradio/icons": "^0.12.0",
17
+ "@gradio/statustracker": "^0.10.10",
18
+ "@gradio/upload": "^0.16.3",
19
+ "@gradio/utils": "^0.10.2",
20
+ "@gradio/wasm": "^0.18.1"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "gradio": "./Index.svelte",
25
+ "svelte": "./dist/Index.svelte",
26
+ "types": "./dist/Index.svelte.d.ts"
27
+ },
28
+ "./example": {
29
+ "gradio": "./Example.svelte",
30
+ "svelte": "./dist/Example.svelte",
31
+ "types": "./dist/Example.svelte.d.ts"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "devDependencies": {
36
+ "@gradio/preview": "^0.13.0"
37
+ },
38
+ "main_changeset": true,
39
+ "peerDependencies": {
40
+ "svelte": "^4.0.0"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/gradio-app/gradio.git",
45
+ "directory": "js/imageslider"
46
+ }
47
+ }
@@ -0,0 +1,30 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+ import { IconButton } from "@gradio/atoms";
4
+ import { Clear } from "@gradio/icons";
5
+
6
+ const dispatch = createEventDispatcher();
7
+ </script>
8
+
9
+ <div>
10
+ <IconButton
11
+ Icon={Clear}
12
+ label="Remove Image"
13
+ on:click={(event) => {
14
+ dispatch("remove_image");
15
+ event.stopPropagation();
16
+ }}
17
+ />
18
+ </div>
19
+
20
+ <style>
21
+ div {
22
+ display: flex;
23
+ position: absolute;
24
+ top: var(--size-2);
25
+ right: var(--size-2);
26
+ justify-content: flex-end;
27
+ gap: var(--spacing-sm);
28
+ z-index: var(--layer-5);
29
+ }
30
+ </style>
@@ -0,0 +1,235 @@
1
+ <script lang="ts">
2
+ import Slider from "./Slider.svelte";
3
+ import { createEventDispatcher, tick } from "svelte";
4
+ import { BlockLabel, Empty, IconButton } from "@gradio/atoms";
5
+ import { Download } from "@gradio/icons";
6
+ import { Image } from "@gradio/icons";
7
+ import { type SelectData, type I18nFormatter } from "@gradio/utils";
8
+ import ClearImage from "./ClearImage.svelte";
9
+ import ImageEl from "./ImageEl.svelte";
10
+
11
+ import { Upload } from "@gradio/upload";
12
+ import { DownloadLink } from "@gradio/wasm/svelte";
13
+
14
+ import { type FileData, type Client } from "@gradio/client";
15
+
16
+ export let value: [FileData | null, FileData | null];
17
+
18
+ export let label: string | undefined = undefined;
19
+ export let show_label: boolean;
20
+ export let root: string;
21
+ export let position: number;
22
+ export let upload_count = 2;
23
+
24
+ export let show_download_button = true;
25
+ export let slider_color: string;
26
+ export let upload: Client["upload"];
27
+ export let stream_handler: Client["stream"];
28
+ export let max_file_size: number | null = null;
29
+ export let i18n: I18nFormatter;
30
+ export let max_height: number;
31
+
32
+ let value_: [FileData | null, FileData | null] = value || [null, null];
33
+
34
+ let img: HTMLImageElement;
35
+ let el_width: number;
36
+ let el_height: number;
37
+
38
+ async function handle_upload(
39
+ { detail }: CustomEvent<FileData[]>,
40
+ n: number
41
+ ): Promise<void> {
42
+ const new_value = [value[0], value[1]] as [
43
+ FileData | null,
44
+ FileData | null
45
+ ];
46
+ if (detail.length > 1) {
47
+ new_value[n] = detail[0];
48
+ } else {
49
+ new_value[n] = detail[n];
50
+ }
51
+ value = new_value;
52
+ await tick();
53
+
54
+ dispatch("upload", new_value);
55
+ }
56
+
57
+ let old_value = "";
58
+
59
+ $: if (JSON.stringify(value) !== old_value) {
60
+ old_value = JSON.stringify(value);
61
+ value_ = value;
62
+ }
63
+
64
+ const dispatch = createEventDispatcher<{
65
+ change: string | null;
66
+ stream: string | null;
67
+ edit: undefined;
68
+ clear: undefined;
69
+ drag: boolean;
70
+ upload: [FileData | null, FileData | null];
71
+ select: SelectData;
72
+ }>();
73
+
74
+ export let dragging = false;
75
+
76
+ $: dispatch("drag", dragging);
77
+ </script>
78
+
79
+ <BlockLabel {show_label} Icon={Image} label={label || i18n("image.image")} />
80
+
81
+ <div
82
+ data-testid="image"
83
+ class="image-container"
84
+ bind:clientWidth={el_width}
85
+ bind:clientHeight={el_height}
86
+ >
87
+ {#if value?.[0]?.url || value?.[1]?.url}
88
+ <ClearImage
89
+ on:remove_image={() => {
90
+ position = 0.5;
91
+ value = [null, null];
92
+ dispatch("clear");
93
+ }}
94
+ />
95
+ {/if}
96
+ {#if value?.[1]?.url}
97
+ <div class="icon-buttons">
98
+ {#if show_download_button}
99
+ <DownloadLink
100
+ href={value[1].url}
101
+ download={value[1].orig_name || "image"}
102
+ >
103
+ <IconButton Icon={Download} />
104
+ </DownloadLink>
105
+ {/if}
106
+ </div>
107
+ {/if}
108
+ <Slider
109
+ bind:position
110
+ disabled={upload_count == 2 || !value?.[0]}
111
+ {slider_color}
112
+ >
113
+ <div
114
+ class="upload-wrap"
115
+ style:display={upload_count === 2 ? "flex" : "block"}
116
+ class:side-by-side={upload_count === 2}
117
+ >
118
+ {#if !value_?.[0]}
119
+ <div class="wrap" class:half-wrap={upload_count === 1}>
120
+ <Upload
121
+ bind:dragging
122
+ filetype="image/*"
123
+ on:load={(e) => handle_upload(e, 0)}
124
+ disable_click={!!value?.[0]}
125
+ {root}
126
+ file_count="multiple"
127
+ {upload}
128
+ {stream_handler}
129
+ {max_file_size}
130
+ >
131
+ <slot />
132
+ </Upload>
133
+ </div>
134
+ {:else}
135
+ <ImageEl
136
+ variant="upload"
137
+ src={value_[0]?.url}
138
+ alt=""
139
+ bind:img_el={img}
140
+ {max_height}
141
+ />
142
+ {/if}
143
+
144
+ {#if !value_?.[1] && upload_count === 2}
145
+ <Upload
146
+ bind:dragging
147
+ filetype="image/*"
148
+ on:load={(e) => handle_upload(e, 1)}
149
+ disable_click={!!value?.[1]}
150
+ {root}
151
+ file_count="multiple"
152
+ {upload}
153
+ {stream_handler}
154
+ {max_file_size}
155
+ >
156
+ <slot />
157
+ </Upload>
158
+ {:else if !value_?.[1] && upload_count === 1}
159
+ <div
160
+ class="empty-wrap fixed"
161
+ style:width="{el_width * (1 - position)}px"
162
+ style:transform="translateX({el_width * position}px)"
163
+ class:white-icon={!value?.[0]?.url}
164
+ >
165
+ <Empty unpadded_box={true} size="large"><Image /></Empty>
166
+ </div>
167
+ {:else if value_?.[1]}
168
+ <ImageEl
169
+ variant="upload"
170
+ src={value_[1].url}
171
+ alt=""
172
+ fixed={upload_count === 1}
173
+ transform="translate(0px, 0px) scale(1)"
174
+ {max_height}
175
+ />
176
+ {/if}
177
+ </div>
178
+ </Slider>
179
+ </div>
180
+
181
+ <style>
182
+ .upload-wrap {
183
+ display: flex;
184
+ justify-content: center;
185
+ align-items: center;
186
+ height: 100%;
187
+ width: 100%;
188
+ }
189
+
190
+ .wrap {
191
+ width: 100%;
192
+ }
193
+
194
+ .half-wrap {
195
+ width: 50%;
196
+ }
197
+ .image-container,
198
+ .empty-wrap {
199
+ width: var(--size-full);
200
+ height: var(--size-full);
201
+ }
202
+
203
+ .fixed {
204
+ --anim-block-background-fill: 255, 255, 255;
205
+ position: absolute;
206
+ top: 0;
207
+ left: 0;
208
+ background-color: rgba(var(--anim-block-background-fill), 0.8);
209
+ z-index: 0;
210
+ }
211
+
212
+ @media (prefers-color-scheme: dark) {
213
+ .fixed {
214
+ --anim-block-background-fill: 31, 41, 55;
215
+ }
216
+ }
217
+
218
+ .side-by-side :global(img) {
219
+ /* width: 100%; */
220
+ width: 50%;
221
+ object-fit: contain;
222
+ }
223
+
224
+ .empty-wrap {
225
+ pointer-events: none;
226
+ }
227
+
228
+ .icon-buttons {
229
+ display: flex;
230
+ position: absolute;
231
+ right: 8px;
232
+ z-index: var(--layer-top);
233
+ top: 8px;
234
+ }
235
+ </style>