@gradio/image 0.26.0 → 0.26.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @gradio/image
2
2
 
3
+ ## 0.26.1
4
+
5
+ ### Fixes
6
+
7
+ - [#13165](https://github.com/gradio-app/gradio/pull/13165) [`1a0e277`](https://github.com/gradio-app/gradio/commit/1a0e2770067789ba6ec5646e473e1df183cd7183) - Use test utils. Thanks @freddyaboulton!
8
+
9
+ ### Dependency updates
10
+
11
+ - @gradio/utils@0.12.2
12
+ - @gradio/atoms@0.23.0
13
+ - @gradio/statustracker@0.13.1
14
+ - @gradio/upload@0.17.8
15
+
3
16
  ## 0.26.0
4
17
 
5
18
  ### Features
package/Image.test.ts ADDED
@@ -0,0 +1,700 @@
1
+ import { test, describe, afterEach, expect, vi } from "vitest";
2
+ import {
3
+ cleanup,
4
+ render,
5
+ fireEvent,
6
+ waitFor,
7
+ upload_file,
8
+ drop_file,
9
+ mock_client,
10
+ download_file,
11
+ TEST_JPG,
12
+ TEST_PNG
13
+ } from "@self/tootils/render";
14
+ import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
15
+ import { tick } from "svelte";
16
+
17
+ import Image from "./Index.svelte";
18
+ import { get_coordinates_of_clicked_image } from "./shared/utils";
19
+
20
+ const fake_value = {
21
+ path: "test.png",
22
+ url: "https://example.com/test.png",
23
+ orig_name: "test.png",
24
+ size: 1024,
25
+ mime_type: "image/png",
26
+ is_stream: false
27
+ };
28
+
29
+ const loading_status = {
30
+ status: "complete" as const,
31
+ eta: 0,
32
+ queue_position: 1,
33
+ queue_size: 1,
34
+ scroll_to_output: false,
35
+ visible: true,
36
+ fn_index: 0,
37
+ show_progress: "full" as const,
38
+ type: "input" as const,
39
+ stream_state: "closed" as const
40
+ };
41
+
42
+ const default_props = {
43
+ sources: ["upload", "webcam", "clipboard"] as (
44
+ | "upload"
45
+ | "webcam"
46
+ | "clipboard"
47
+ )[],
48
+ value: null as any,
49
+ label: "Image",
50
+ show_label: true,
51
+ interactive: true,
52
+ _selectable: false,
53
+ height: 300,
54
+ width: 300,
55
+ streaming: false,
56
+ stream_every: 1,
57
+ pending: false,
58
+ input_ready: true,
59
+ placeholder: "",
60
+ buttons: [] as (string | { value: string; id: number; icon: null })[],
61
+ webcam_options: { mirror: false, constraints: {} },
62
+ watermark: null,
63
+ loading_status
64
+ };
65
+
66
+ run_shared_prop_tests({
67
+ component: Image,
68
+ name: "Image",
69
+ base_props: {
70
+ ...default_props
71
+ },
72
+ has_label: false,
73
+ has_validation_error: false
74
+ });
75
+
76
+ describe("Image", () => {
77
+ afterEach(() => cleanup());
78
+
79
+ test("renders with null value showing upload area", async () => {
80
+ const { getByLabelText } = await render(Image, {
81
+ ...default_props,
82
+ value: null
83
+ });
84
+
85
+ expect(getByLabelText("image.drop_to_upload")).toBeVisible();
86
+ });
87
+
88
+ test("renders image when value is set", async () => {
89
+ const { container } = await render(Image, {
90
+ ...default_props,
91
+ value: fake_value
92
+ });
93
+
94
+ const img = container.querySelector("img");
95
+ expect(img).toBeTruthy();
96
+ expect(img?.getAttribute("src")).toBe("https://example.com/test.png");
97
+ });
98
+ });
99
+
100
+ describe("Props: sources", () => {
101
+ afterEach(() => cleanup());
102
+
103
+ test("multiple sources renders source selection buttons", async () => {
104
+ const { getByTestId } = await render(Image, {
105
+ ...default_props,
106
+ sources: ["upload", "webcam", "clipboard"]
107
+ });
108
+
109
+ const sourceSelect = getByTestId("source-select");
110
+ expect(sourceSelect).toBeTruthy();
111
+ });
112
+
113
+ test("single upload source does not render source selection", async () => {
114
+ const { queryByTestId, getByLabelText } = await render(Image, {
115
+ ...default_props,
116
+ sources: ["upload"]
117
+ });
118
+ expect(getByLabelText("image.drop_to_upload")).toBeVisible();
119
+ const sourceSelect = queryByTestId("source-select");
120
+ expect(sourceSelect).toBeNull();
121
+ });
122
+
123
+ test("single clipboard source does render source selection", async () => {
124
+ const { queryByTestId, getByLabelText } = await render(Image, {
125
+ ...default_props,
126
+ sources: ["clipboard"]
127
+ });
128
+ expect(getByLabelText("Paste from clipboard")).toBeTruthy();
129
+ const sourceSelect = queryByTestId("source-select");
130
+ expect(sourceSelect).not.toBeNull();
131
+ });
132
+
133
+ test("clipboard and upload sources render paste and upload buttons", async () => {
134
+ const { getByLabelText } = await render(Image, {
135
+ ...default_props,
136
+ sources: ["upload", "clipboard"]
137
+ });
138
+
139
+ expect(getByLabelText("Upload file")).toBeTruthy();
140
+ expect(getByLabelText("Paste from clipboard")).toBeTruthy();
141
+ });
142
+
143
+ test("upload and webcam sources render corresponding buttons", async () => {
144
+ const { getByLabelText } = await render(Image, {
145
+ ...default_props,
146
+ sources: ["upload", "webcam"]
147
+ });
148
+
149
+ expect(getByLabelText("Upload file")).toBeTruthy();
150
+ expect(getByLabelText("Capture from camera")).toBeTruthy();
151
+ });
152
+
153
+ test("clicking webcam source button hides the upload area", async () => {
154
+ const { getByLabelText } = await render(Image, {
155
+ ...default_props,
156
+ sources: ["upload", "webcam"]
157
+ });
158
+
159
+ expect(getByLabelText("image.drop_to_upload")).toBeVisible();
160
+
161
+ await fireEvent.click(getByLabelText("Capture from camera"));
162
+
163
+ // Re-query after click to avoid stale references from potential rerenders
164
+ expect(getByLabelText("image.drop_to_upload")).not.toBeVisible();
165
+ });
166
+
167
+ test("clicking upload source button shows the upload area again", async () => {
168
+ const { getByLabelText } = await render(Image, {
169
+ ...default_props,
170
+ sources: ["upload", "webcam"]
171
+ });
172
+
173
+ await fireEvent.click(getByLabelText("Capture from camera"));
174
+ expect(getByLabelText("image.drop_to_upload")).not.toBeVisible();
175
+
176
+ await fireEvent.click(getByLabelText("Upload file"));
177
+ expect(getByLabelText("image.drop_to_upload")).toBeVisible();
178
+ });
179
+ });
180
+
181
+ describe("Props: interactive", () => {
182
+ afterEach(() => cleanup());
183
+
184
+ test("interactive=true shows an upload area when value is null", async () => {
185
+ const { getByLabelText } = await render(Image, {
186
+ ...default_props,
187
+ interactive: true,
188
+ value: null
189
+ });
190
+
191
+ expect(getByLabelText("image.drop_to_upload")).toBeTruthy();
192
+ });
193
+
194
+ test("interactive=false renders the image without upload controls", async () => {
195
+ const { container, queryByLabelText } = await render(Image, {
196
+ ...default_props,
197
+ interactive: false,
198
+ value: fake_value,
199
+ buttons: ["fullscreen"]
200
+ });
201
+
202
+ const img = container.querySelector("img");
203
+ expect(img).toBeTruthy();
204
+ // No upload area or source selection in static mode
205
+ expect(queryByLabelText("image.drop_to_upload")).toBeNull();
206
+ expect(queryByLabelText("Upload file")).toBeNull();
207
+ });
208
+
209
+ test("interactive=false with null value does not show upload area", async () => {
210
+ const { queryByLabelText } = await render(Image, {
211
+ ...default_props,
212
+ interactive: false,
213
+ value: null
214
+ });
215
+
216
+ expect(queryByLabelText("image.drop_to_upload")).toBeNull();
217
+ });
218
+ });
219
+
220
+ describe("Events: change", () => {
221
+ afterEach(() => cleanup());
222
+
223
+ test("setting value triggers change event", async () => {
224
+ const { listen, set_data } = await render(Image, {
225
+ ...default_props,
226
+ value: null
227
+ });
228
+
229
+ const change = listen("change");
230
+
231
+ await set_data({ value: fake_value });
232
+
233
+ expect(change).toHaveBeenCalledTimes(1);
234
+ });
235
+
236
+ test("change event is not triggered on mount with a default value", async () => {
237
+ const { listen } = await render(Image, {
238
+ ...default_props,
239
+ value: fake_value
240
+ });
241
+
242
+ const change = listen("change", { retrospective: true });
243
+
244
+ expect(change).not.toHaveBeenCalled();
245
+ });
246
+
247
+ test("changing value multiple times triggers change each time", async () => {
248
+ const { listen, set_data } = await render(Image, {
249
+ ...default_props,
250
+ value: null
251
+ });
252
+
253
+ const change = listen("change");
254
+
255
+ const value_a = { ...fake_value, url: "https://example.com/a.png" };
256
+ const value_b = { ...fake_value, url: "https://example.com/b.png" };
257
+
258
+ await set_data({ value: value_a });
259
+ await set_data({ value: value_b });
260
+
261
+ expect(change).toHaveBeenCalledTimes(2);
262
+ });
263
+
264
+ test("setting value to null after a value triggers change", async () => {
265
+ const { listen, set_data } = await render(Image, {
266
+ ...default_props,
267
+ value: fake_value
268
+ });
269
+
270
+ const change = listen("change");
271
+
272
+ await set_data({ value: null });
273
+
274
+ expect(change).toHaveBeenCalledTimes(1);
275
+ });
276
+ });
277
+
278
+ describe("Props: buttons (static mode)", () => {
279
+ afterEach(() => cleanup());
280
+
281
+ test("buttons with download shows download link", async () => {
282
+ const { container } = await render(Image, {
283
+ ...default_props,
284
+ interactive: false,
285
+ value: {
286
+ ...TEST_JPG,
287
+ is_stream: false
288
+ },
289
+ buttons: ["download"]
290
+ });
291
+
292
+ const downloadLink = container.querySelector("a.download-link");
293
+ expect(downloadLink).toBeTruthy();
294
+
295
+ const { suggested_filename } = await download_file("a.download-link");
296
+ expect(suggested_filename).toBe("cheetah1.jpg");
297
+ });
298
+
299
+ test("buttons with fullscreen shows fullscreen button", async () => {
300
+ const { getByLabelText } = await render(Image, {
301
+ ...default_props,
302
+ interactive: false,
303
+ value: fake_value,
304
+ buttons: ["fullscreen"]
305
+ });
306
+
307
+ expect(getByLabelText("Fullscreen")).toBeTruthy();
308
+ });
309
+
310
+ test("empty buttons array shows no action buttons", async () => {
311
+ const { queryByLabelText } = await render(Image, {
312
+ ...default_props,
313
+ interactive: false,
314
+ value: fake_value,
315
+ buttons: []
316
+ });
317
+
318
+ expect(queryByLabelText("Fullscreen")).toBeNull();
319
+ expect(queryByLabelText("common.download")).toBeNull();
320
+ });
321
+
322
+ test("custom button renders and dispatches custom_button_click", async () => {
323
+ const { listen, getByLabelText } = await render(Image, {
324
+ ...default_props,
325
+ interactive: false,
326
+ value: fake_value,
327
+ buttons: [{ value: "Analyze", id: 7, icon: null }]
328
+ });
329
+
330
+ const custom = listen("custom_button_click");
331
+ const btn = getByLabelText("Analyze");
332
+
333
+ await fireEvent.click(btn);
334
+
335
+ expect(custom).toHaveBeenCalledTimes(1);
336
+ expect(custom).toHaveBeenCalledWith({ id: 7 });
337
+ });
338
+ });
339
+
340
+ describe("Props: buttons (interactive mode)", () => {
341
+ afterEach(() => cleanup());
342
+
343
+ test("clear button appears when image has a value", async () => {
344
+ const { getByLabelText } = await render(Image, {
345
+ ...default_props,
346
+ interactive: true,
347
+ value: fake_value
348
+ });
349
+
350
+ const clearBtn = getByLabelText("Remove Image");
351
+ expect(clearBtn).toBeTruthy();
352
+ });
353
+
354
+ test("clear button is not present when there is no image value", async () => {
355
+ const { queryByLabelText, getByLabelText } = await render(Image, {
356
+ ...default_props,
357
+ interactive: true,
358
+ value: null
359
+ });
360
+
361
+ // Smoke test: component rendered
362
+ expect(getByLabelText("image.drop_to_upload")).toBeTruthy();
363
+ expect(queryByLabelText("Remove Image")).toBeNull();
364
+ });
365
+
366
+ test("clicking clear button removes the image and dispatches clear and input", async () => {
367
+ const { getByLabelText, listen } = await render(Image, {
368
+ ...default_props,
369
+ interactive: true,
370
+ value: fake_value
371
+ });
372
+
373
+ const clear = listen("clear");
374
+ const input = listen("input");
375
+ const clearBtn = getByLabelText("Remove Image");
376
+
377
+ await fireEvent.click(clearBtn);
378
+
379
+ expect(clear).toHaveBeenCalledTimes(1);
380
+ expect(input).toHaveBeenCalledTimes(1);
381
+ });
382
+ });
383
+
384
+ describe("get_data", () => {
385
+ afterEach(() => cleanup());
386
+
387
+ test("get_data returns the current value", async () => {
388
+ const { get_data, set_data } = await render(Image, {
389
+ ...default_props,
390
+ value: null
391
+ });
392
+
393
+ const initial = await get_data();
394
+ expect(initial.value).toBeNull();
395
+
396
+ await set_data({ value: fake_value });
397
+
398
+ const updated = await get_data();
399
+ expect(updated.value).toEqual(fake_value);
400
+ });
401
+ });
402
+
403
+ describe("Selectable", () => {
404
+ afterEach(() => cleanup());
405
+
406
+ test("selectable mode shows crosshair cursor on the image", async () => {
407
+ const { container } = await render(Image, {
408
+ ...default_props,
409
+ interactive: false,
410
+ value: fake_value,
411
+ _selectable: true,
412
+ buttons: []
413
+ });
414
+
415
+ const frame = container.querySelector(".selectable");
416
+ expect(frame).toBeTruthy();
417
+ });
418
+
419
+ test("non-selectable mode does not show crosshair cursor", async () => {
420
+ const { container } = await render(Image, {
421
+ ...default_props,
422
+ interactive: false,
423
+ value: fake_value,
424
+ _selectable: false,
425
+ buttons: []
426
+ });
427
+
428
+ const frame = container.querySelector(".selectable");
429
+ expect(frame).toBeNull();
430
+ });
431
+ });
432
+
433
+ describe("get_coordinates_of_clicked_image", () => {
434
+ function make_mock_event(
435
+ clientX: number,
436
+ clientY: number,
437
+ imgProps: { naturalWidth: number; naturalHeight: number },
438
+ rect: { left: number; top: number; width: number; height: number }
439
+ ): MouseEvent {
440
+ const imgEl = document.createElement("img");
441
+ Object.defineProperty(imgEl, "naturalWidth", {
442
+ value: imgProps.naturalWidth
443
+ });
444
+ Object.defineProperty(imgEl, "naturalHeight", {
445
+ value: imgProps.naturalHeight
446
+ });
447
+ imgEl.getBoundingClientRect = () => ({
448
+ left: rect.left,
449
+ top: rect.top,
450
+ width: rect.width,
451
+ height: rect.height,
452
+ right: rect.left + rect.width,
453
+ bottom: rect.top + rect.height,
454
+ x: rect.left,
455
+ y: rect.top,
456
+ toJSON: () => {}
457
+ });
458
+
459
+ const container = document.createElement("div");
460
+ container.appendChild(imgEl);
461
+
462
+ return {
463
+ currentTarget: container,
464
+ clientX,
465
+ clientY
466
+ } as unknown as MouseEvent;
467
+ }
468
+
469
+ test("returns correct coordinates for a 1:1 scale image", () => {
470
+ const evt = make_mock_event(
471
+ 50,
472
+ 50,
473
+ {
474
+ naturalWidth: 100,
475
+ naturalHeight: 100
476
+ },
477
+ {
478
+ left: 0,
479
+ top: 0,
480
+ width: 100,
481
+ height: 100
482
+ }
483
+ );
484
+
485
+ const result = get_coordinates_of_clicked_image(evt);
486
+ expect(result).toEqual([50, 50]);
487
+ });
488
+
489
+ test("returns correct coordinates when image is scaled down", () => {
490
+ // 200x200 natural, displayed at 100x100, click at (25, 25) in viewport
491
+ const evt = make_mock_event(
492
+ 25,
493
+ 25,
494
+ {
495
+ naturalWidth: 200,
496
+ naturalHeight: 200
497
+ },
498
+ {
499
+ left: 0,
500
+ top: 0,
501
+ width: 100,
502
+ height: 100
503
+ }
504
+ );
505
+
506
+ const result = get_coordinates_of_clicked_image(evt);
507
+ expect(result).toEqual([50, 50]);
508
+ });
509
+
510
+ test("accounts for container offset", () => {
511
+ const evt = make_mock_event(
512
+ 60,
513
+ 70,
514
+ {
515
+ naturalWidth: 100,
516
+ naturalHeight: 100
517
+ },
518
+ {
519
+ left: 10,
520
+ top: 20,
521
+ width: 100,
522
+ height: 100
523
+ }
524
+ );
525
+
526
+ const result = get_coordinates_of_clicked_image(evt);
527
+ expect(result).toEqual([50, 50]);
528
+ });
529
+
530
+ test("returns null when click is outside image bounds", () => {
531
+ // Click at (-5, 50) relative to image → x = -5 which is < 0
532
+ const evt = make_mock_event(
533
+ -5,
534
+ 50,
535
+ {
536
+ naturalWidth: 100,
537
+ naturalHeight: 100
538
+ },
539
+ {
540
+ left: 0,
541
+ top: 0,
542
+ width: 100,
543
+ height: 100
544
+ }
545
+ );
546
+
547
+ const result = get_coordinates_of_clicked_image(evt);
548
+ expect(result).toBeNull();
549
+ });
550
+
551
+ test("returns null when click is beyond the right edge", () => {
552
+ const evt = make_mock_event(
553
+ 105,
554
+ 50,
555
+ {
556
+ naturalWidth: 100,
557
+ naturalHeight: 100
558
+ },
559
+ {
560
+ left: 0,
561
+ top: 0,
562
+ width: 100,
563
+ height: 100
564
+ }
565
+ );
566
+
567
+ const result = get_coordinates_of_clicked_image(evt);
568
+ expect(result).toBeNull();
569
+ });
570
+
571
+ test("handles landscape image with letterboxing (xScale > yScale)", () => {
572
+ // 400x200 natural image displayed in 200x200 container
573
+ // xScale = 400/200 = 2, yScale = 200/200 = 1
574
+ // xScale > yScale, so displayed_height = 200/2 = 100, y_offset = 50
575
+ // Click at (100, 100): x = (100-0)*2 = 200, y = (100-0-50)*2 = 100
576
+ const evt = make_mock_event(
577
+ 100,
578
+ 100,
579
+ {
580
+ naturalWidth: 400,
581
+ naturalHeight: 200
582
+ },
583
+ {
584
+ left: 0,
585
+ top: 0,
586
+ width: 200,
587
+ height: 200
588
+ }
589
+ );
590
+
591
+ const result = get_coordinates_of_clicked_image(evt);
592
+ expect(result).toEqual([200, 100]);
593
+ });
594
+
595
+ test("handles portrait image with pillarboxing (yScale > xScale)", () => {
596
+ // 200x400 natural image displayed in 200x200 container
597
+ // xScale = 200/200 = 1, yScale = 400/200 = 2
598
+ // yScale > xScale, so displayed_width = 200/2 = 100, x_offset = 50
599
+ // Click at (100, 100): x = (100-0-50)*2 = 100, y = (100-0)*2 = 200
600
+ const evt = make_mock_event(
601
+ 100,
602
+ 100,
603
+ {
604
+ naturalWidth: 200,
605
+ naturalHeight: 400
606
+ },
607
+ {
608
+ left: 0,
609
+ top: 0,
610
+ width: 200,
611
+ height: 200
612
+ }
613
+ );
614
+
615
+ const result = get_coordinates_of_clicked_image(evt);
616
+ expect(result).toEqual([100, 200]);
617
+ });
618
+
619
+ test("returns [NaN, NaN] when currentTarget is not an Element", () => {
620
+ const evt = {
621
+ currentTarget: {},
622
+ clientX: 50,
623
+ clientY: 50
624
+ } as unknown as MouseEvent;
625
+
626
+ const result = get_coordinates_of_clicked_image(evt);
627
+ expect(result).toEqual([NaN, NaN]);
628
+ });
629
+ });
630
+
631
+ const upload_props = {
632
+ ...default_props,
633
+ sources: ["upload"] as "upload"[],
634
+ interactive: true,
635
+ value: null,
636
+ root: "https://example.com",
637
+ client: mock_client()
638
+ };
639
+
640
+ describe("Events: upload via file input", () => {
641
+ afterEach(() => cleanup());
642
+
643
+ test("selecting a file triggers upload, change, and input events", async () => {
644
+ const { listen } = await render(Image, upload_props);
645
+
646
+ const upload = listen("upload");
647
+ const change = listen("change");
648
+ const input = listen("input");
649
+
650
+ await upload_file(TEST_JPG);
651
+
652
+ await waitFor(() => {
653
+ expect(upload).toHaveBeenCalledTimes(1);
654
+ });
655
+ expect(input).toHaveBeenCalledTimes(1);
656
+ expect(change).toHaveBeenCalledTimes(1);
657
+ });
658
+
659
+ test("drag and drop a file triggers upload, change, and input events", async () => {
660
+ const { listen } = await render(Image, upload_props);
661
+
662
+ const upload = listen("upload");
663
+ const change = listen("change");
664
+ const input = listen("input");
665
+
666
+ await drop_file(TEST_PNG, "[aria-label='image.drop_to_upload']");
667
+
668
+ await waitFor(() => {
669
+ expect(upload).toHaveBeenCalledTimes(1);
670
+ });
671
+ expect(input).toHaveBeenCalledTimes(1);
672
+ expect(change).toHaveBeenCalledTimes(1);
673
+ });
674
+
675
+ test("upload failure dispatches error event with the message", async () => {
676
+ const failing_upload = vi
677
+ .fn()
678
+ .mockRejectedValue(new Error("File too large"));
679
+ const { listen } = await render(Image, {
680
+ ...upload_props,
681
+ client: {
682
+ upload: failing_upload,
683
+ stream: async () => ({ onmessage: null, close: () => {} })
684
+ }
685
+ });
686
+
687
+ const error = listen("error");
688
+
689
+ await upload_file(TEST_JPG);
690
+
691
+ await waitFor(() => {
692
+ expect(failing_upload).toHaveBeenCalled();
693
+ });
694
+
695
+ await waitFor(() => {
696
+ expect(error).toHaveBeenCalledTimes(1);
697
+ });
698
+ expect(error).toHaveBeenCalledWith("File too large");
699
+ });
700
+ });
package/Index.svelte CHANGED
@@ -70,10 +70,16 @@
70
70
  }
71
71
  };
72
72
 
73
- let old_value = $state(gradio.props.value);
73
+ let old_value = gradio.props.value;
74
+ let mounted = false;
74
75
 
75
76
  $effect(() => {
76
- if (old_value != gradio.props.value) {
77
+ if (!mounted) {
78
+ old_value = gradio.props.value;
79
+ mounted = true;
80
+ return;
81
+ }
82
+ if (old_value !== gradio.props.value) {
77
83
  old_value = gradio.props.value;
78
84
  gradio.dispatch("change");
79
85
  }
package/dist/Index.svelte CHANGED
@@ -70,10 +70,16 @@
70
70
  }
71
71
  };
72
72
 
73
- let old_value = $state(gradio.props.value);
73
+ let old_value = gradio.props.value;
74
+ let mounted = false;
74
75
 
75
76
  $effect(() => {
76
- if (old_value != gradio.props.value) {
77
+ if (!mounted) {
78
+ old_value = gradio.props.value;
79
+ mounted = true;
80
+ return;
81
+ }
82
+ if (old_value !== gradio.props.value) {
77
83
  old_value = gradio.props.value;
78
84
  gradio.dispatch("change");
79
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/image",
3
- "version": "0.26.0",
3
+ "version": "0.26.1",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -10,15 +10,15 @@
10
10
  "cropperjs": "^2.0.1",
11
11
  "lazy-brush": "^2.0.2",
12
12
  "resize-observer-polyfill": "^1.5.1",
13
+ "@gradio/atoms": "^0.23.0",
13
14
  "@gradio/client": "^2.1.0",
14
- "@gradio/atoms": "^0.22.2",
15
- "@gradio/statustracker": "^0.13.0",
16
- "@gradio/upload": "^0.17.7",
17
15
  "@gradio/icons": "^0.15.1",
18
- "@gradio/utils": "^0.12.1"
16
+ "@gradio/statustracker": "^0.13.1",
17
+ "@gradio/utils": "^0.12.2",
18
+ "@gradio/upload": "^0.17.8"
19
19
  },
20
20
  "devDependencies": {
21
- "@gradio/preview": "^0.16.1"
21
+ "@gradio/preview": "^0.16.2"
22
22
  },
23
23
  "main_changeset": true,
24
24
  "main": "./Index.svelte",