@gradio/image 0.26.3 → 0.28.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/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # @gradio/image
2
2
 
3
+ ## 0.28.0
4
+
5
+ ### Features
6
+
7
+ - [#13543](https://github.com/gradio-app/gradio/pull/13543) [`0533483`](https://github.com/gradio-app/gradio/commit/0533483bccdee38f334a598f18297e8c02966343) - Migrate Image components to Svelte 5. Thanks @dawoodkhan82!
8
+
9
+ ### Dependency updates
10
+
11
+ - @gradio/statustracker@0.15.1
12
+ - @gradio/icons@0.16.0
13
+ - @gradio/atoms@0.26.0
14
+ - @gradio/client@2.3.1
15
+ - @gradio/upload@0.18.1
16
+
17
+ ## 0.27.0
18
+
19
+ ### Features
20
+
21
+ - [#13526](https://github.com/gradio-app/gradio/pull/13526) [`53cb4ca`](https://github.com/gradio-app/gradio/commit/53cb4cae1ec3521e9170d12867253516413ba37a) - Run `pnpm lint` and `pnpm ts:check` on CI. Thanks @abidlabs!
22
+
23
+ ### Fixes
24
+
25
+ - [#13532](https://github.com/gradio-app/gradio/pull/13532) [`0a933b4`](https://github.com/gradio-app/gradio/commit/0a933b428f5b2158cb8c764f38abf0da2312d58a) - Fix `gr.SelectData` coordinates when the image in `gr.Image` does not fill its container. Thanks @hysts!
26
+ - [#13533](https://github.com/gradio-app/gradio/pull/13533) [`e5ec1ca`](https://github.com/gradio-app/gradio/commit/e5ec1ca66c45e7e3a057b75ac2b8b86660b2827b) - Fix fullscreen button not working in ImageSlider, interactive Image, native plots, and AnnotatedImage. Thanks @hysts!
27
+
28
+ ### Dependency updates
29
+
30
+ - @gradio/atoms@0.25.0
31
+ - @gradio/statustracker@0.15.0
32
+ - @gradio/utils@0.13.0
33
+ - @gradio/client@2.3.0
34
+ - @gradio/upload@0.18.0
35
+
36
+ ## 0.26.3
37
+
38
+ ### Dependency updates
39
+
40
+ - @gradio/client@2.2.2
41
+
42
+ ## 0.26.3
43
+
44
+ ### Dependency updates
45
+
46
+ - @gradio/client@2.2.1
47
+ - @gradio/upload@0.17.10
48
+
3
49
  ## 0.26.3
4
50
 
5
51
  ### Dependency updates
package/Example.svelte CHANGED
@@ -2,9 +2,15 @@
2
2
  import Image from "./shared/Image.svelte";
3
3
  import type { FileData } from "@gradio/client";
4
4
 
5
- export let value: null | FileData;
6
- export let type: "gallery" | "table";
7
- export let selected = false;
5
+ let {
6
+ value,
7
+ type,
8
+ selected = false
9
+ }: {
10
+ value: null | FileData;
11
+ type: "gallery" | "table";
12
+ selected?: boolean;
13
+ } = $props();
8
14
  </script>
9
15
 
10
16
  <div
@@ -1,7 +1,6 @@
1
1
  <script module>
2
2
  import { defineMeta } from "@storybook/addon-svelte-csf";
3
3
  import StaticImage from "./Index.svelte";
4
- import { userEvent, within, expect } from "storybook/test";
5
4
  import { allModes } from "../storybook/modes";
6
5
  import { wrapProps } from "../storybook/wrapProps";
7
6
 
@@ -37,13 +36,6 @@
37
36
  buttons: ["fullscreen", "download"],
38
37
  webcam_options: { mirror: true, constraints: null }
39
38
  }}
40
- play={async ({ canvasElement }) => {
41
- const canvas = within(canvasElement);
42
- const expand_btn = await canvas.findByRole("button", {
43
- name: "Fullscreen"
44
- });
45
- expect(expand_btn).toBeTruthy();
46
- }}
47
39
  >
48
40
  {#snippet template(args)}
49
41
  <div
@@ -146,41 +138,6 @@
146
138
  {/snippet}
147
139
  </Story>
148
140
 
149
- <Story
150
- name="interactive with upload, clipboard, and webcam"
151
- args={{
152
- sources: ["upload", "clipboard", "webcam"],
153
- value: {
154
- path: cheetah,
155
- url: cheetah,
156
- orig_name: "cheetah.jpg"
157
- },
158
- show_label: false,
159
- interactive: true,
160
- placeholder: md,
161
- buttons: [],
162
- webcam_options: { mirror: true, constraints: null }
163
- }}
164
- parameters={{ chromatic: { disableSnapshot: true } }}
165
- play={async ({ canvasElement }) => {
166
- const canvas = within(canvasElement);
167
- const webcamButton = await canvas.findByLabelText("Capture from camera");
168
- userEvent.click(webcamButton);
169
- userEvent.click(await canvas.findByTitle("grant webcam access"));
170
- userEvent.click(await canvas.findByLabelText("Upload file"));
171
- userEvent.click(await canvas.findByLabelText("Paste from clipboard"));
172
- }}
173
- >
174
- {#snippet template(args)}
175
- <div
176
- class="image-container"
177
- style="width: 300px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
178
- >
179
- <StaticImage {...wrapProps(args)} />
180
- </div>
181
- {/snippet}
182
- </Story>
183
-
184
141
  <Story
185
142
  name="interactive with webcam"
186
143
  args={{
@@ -217,28 +174,3 @@
217
174
  </div>
218
175
  {/snippet}
219
176
  </Story>
220
-
221
- <Story
222
- name="interactive webcam with streaming"
223
- args={{
224
- sources: ["webcam"],
225
- interactive: true,
226
- value: {
227
- path: cheetah,
228
- url: cheetah,
229
- orig_name: "cheetah.jpg"
230
- },
231
- streaming: true,
232
- buttons: ["download"],
233
- webcam_options: { mirror: true, constraints: null }
234
- }}
235
- >
236
- {#snippet template(args)}
237
- <div
238
- class="image-container"
239
- style="width: 300px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
240
- >
241
- <StaticImage {...wrapProps(args)} />
242
- </div>
243
- {/snippet}
244
- </Story>
package/Image.test.ts CHANGED
@@ -326,6 +326,25 @@ describe("Props: buttons (static mode)", () => {
326
326
  expect(getByLabelText("Fullscreen")).toBeTruthy();
327
327
  });
328
328
 
329
+ test("clicking the fullscreen button toggles fullscreen in interactive mode", async () => {
330
+ const { getByLabelText } = await render(Image, {
331
+ ...default_props,
332
+ interactive: true,
333
+ value: fake_value,
334
+ buttons: ["fullscreen"]
335
+ });
336
+
337
+ await fireEvent.click(getByLabelText("Fullscreen"));
338
+ await waitFor(() => {
339
+ expect(getByLabelText("Exit fullscreen mode")).toBeVisible();
340
+ });
341
+
342
+ await fireEvent.click(getByLabelText("Exit fullscreen mode"));
343
+ await waitFor(() => {
344
+ expect(getByLabelText("Fullscreen")).toBeVisible();
345
+ });
346
+ });
347
+
329
348
  test("empty buttons array shows no action buttons", async () => {
330
349
  const { queryByLabelText } = await render(Image, {
331
350
  ...default_props,
@@ -635,6 +654,52 @@ describe("get_coordinates_of_clicked_image", () => {
635
654
  expect(result).toEqual([100, 200]);
636
655
  });
637
656
 
657
+ test("handles image shown at natural size with empty space on both axes", () => {
658
+ // `object-fit: scale-down` never upscales: a 100x100 natural image in
659
+ // a 400x300 box is drawn at natural size, centered at offset (150, 100).
660
+ // Click at (150, 100) = top-left corner of the drawn image.
661
+ const evt = make_mock_event(
662
+ 150,
663
+ 100,
664
+ {
665
+ naturalWidth: 100,
666
+ naturalHeight: 100
667
+ },
668
+ {
669
+ left: 0,
670
+ top: 0,
671
+ width: 400,
672
+ height: 300
673
+ }
674
+ );
675
+
676
+ const result = get_coordinates_of_clicked_image(evt);
677
+ expect(result).toEqual([0, 0]);
678
+ });
679
+
680
+ test("returns null when click is in the empty space around a natural-size image", () => {
681
+ // 100x100 natural image in a 1920x1080 box: drawn at natural size,
682
+ // centered at offset (910, 490). A click at (500, 540) is inside the
683
+ // box but left of the drawn image, so no image pixel is under it.
684
+ const evt = make_mock_event(
685
+ 500,
686
+ 540,
687
+ {
688
+ naturalWidth: 100,
689
+ naturalHeight: 100
690
+ },
691
+ {
692
+ left: 0,
693
+ top: 0,
694
+ width: 1920,
695
+ height: 1080
696
+ }
697
+ );
698
+
699
+ const result = get_coordinates_of_clicked_image(evt);
700
+ expect(result).toBeNull();
701
+ });
702
+
638
703
  test("returns [NaN, NaN] when currentTarget is not an Element", () => {
639
704
  const evt = {
640
705
  currentTarget: {},
@@ -24,14 +24,3 @@
24
24
  <Image {...args} />
25
25
  {/snippet}
26
26
  </Story>
27
-
28
- <Story
29
- name="Null"
30
- args={{
31
- value: null
32
- }}
33
- >
34
- {#snippet template(args)}
35
- <Image {...args} />
36
- {/snippet}
37
- </Story>
package/Index.svelte CHANGED
@@ -19,7 +19,7 @@
19
19
  import type { ImageProps, ImageEvents } from "./shared/types";
20
20
 
21
21
  let stream_data = { value: null };
22
- let upload_promise = $state<Promise<any>>();
22
+ let upload_promise = $state<Promise<any> | null>(null);
23
23
  class ImageGradio extends Gradio<ImageEvents, ImageProps> {
24
24
  async get_data() {
25
25
  if (upload_promise) {
@@ -37,50 +37,34 @@
37
37
  }
38
38
 
39
39
  const props = $props();
40
- const gradio = new ImageGradio(props);
40
+ const gradio = new ImageGradio(props, { value: null });
41
41
 
42
+ let value = $state(gradio.props.value ?? null);
42
43
  let fullscreen = $state(false);
43
44
  let dragging = $state(false);
44
45
  let active_source = $derived.by(() =>
45
46
  gradio.props.sources ? gradio.props.sources[0] : null
46
47
  );
47
48
 
48
- let upload_component: ImageUploader;
49
- const handle_drag_event = (event: Event): void => {
50
- const drag_event = event as DragEvent;
51
- drag_event.preventDefault();
52
- drag_event.stopPropagation();
53
- if (drag_event.type === "dragenter" || drag_event.type === "dragover") {
54
- dragging = true;
55
- } else if (drag_event.type === "dragleave") {
56
- dragging = false;
57
- }
58
- };
59
-
60
- const handle_drop = (event: Event): void => {
61
- if (gradio.shared.interactive) {
62
- const drop_event = event as DragEvent;
63
- drop_event.preventDefault();
64
- drop_event.stopPropagation();
65
- dragging = false;
49
+ $effect(() => {
50
+ value = gradio.props.value ?? null;
51
+ });
66
52
 
67
- if (upload_component) {
68
- upload_component.loadFilesFromDrop(drop_event);
69
- }
70
- }
71
- };
53
+ $effect(() => {
54
+ gradio.props.value = value;
55
+ });
72
56
 
73
- let old_value = gradio.props.value;
57
+ let old_value = value;
74
58
  let mounted = false;
75
59
 
76
60
  $effect(() => {
77
61
  if (!mounted) {
78
- old_value = gradio.props.value;
62
+ old_value = value;
79
63
  mounted = true;
80
64
  return;
81
65
  }
82
- if (old_value !== gradio.props.value) {
83
- old_value = gradio.props.value;
66
+ if (old_value !== value) {
67
+ old_value = value;
84
68
  gradio.dispatch("change");
85
69
  }
86
70
  });
@@ -100,7 +84,7 @@
100
84
  width={gradio.props.width}
101
85
  allow_overflow={false}
102
86
  container={gradio.shared.container}
103
- scale{gradio.shared.scale}
87
+ scale={gradio.shared.scale}
104
88
  min_width={gradio.shared.min_width}
105
89
  bind:fullscreen
106
90
  >
@@ -112,14 +96,14 @@
112
96
  gradio.dispatch("clear_status", gradio.shared.loading_status)}
113
97
  />
114
98
  <StaticImage
115
- on:select={({ detail }) => gradio.dispatch("select", detail)}
116
- on:share={({ detail }) => gradio.dispatch("share", detail)}
117
- on:error={({ detail }) => gradio.dispatch("error", detail)}
118
- on:fullscreen={({ detail }) => {
99
+ onselect={(detail) => gradio.dispatch("select", detail)}
100
+ onshare={(detail) => gradio.dispatch("share", detail)}
101
+ onerror={(detail) => gradio.dispatch("error", detail)}
102
+ onfullscreen={(detail) => {
119
103
  fullscreen = detail;
120
104
  }}
121
105
  {fullscreen}
122
- value={gradio.props.value}
106
+ {value}
123
107
  label={gradio.shared.label}
124
108
  show_label={gradio.shared.show_label}
125
109
  selectable={gradio.props._selectable}
@@ -145,10 +129,6 @@
145
129
  scale={gradio.shared.scale}
146
130
  min_width={gradio.shared.min_width}
147
131
  bind:fullscreen
148
- on:dragenter={handle_drag_event}
149
- on:dragleave={handle_drag_event}
150
- on:dragover={handle_drag_event}
151
- on:drop={handle_drop}
152
132
  >
153
133
  {#if gradio.shared.loading_status.type === "output" || gradio.shared.loading_status.validation_error}
154
134
  <StatusTracker
@@ -161,9 +141,8 @@
161
141
  {/if}
162
142
  <ImageUploader
163
143
  bind:upload_promise
164
- bind:this={upload_component}
165
144
  bind:active_source
166
- bind:value={gradio.props.value}
145
+ bind:value
167
146
  bind:dragging
168
147
  selectable={gradio.props._selectable}
169
148
  root={gradio.shared.root}
@@ -174,31 +153,29 @@
174
153
  : gradio.props.buttons.some(
175
154
  (btn) => typeof btn === "string" && btn === "fullscreen"
176
155
  )}
177
- on:edit={() => gradio.dispatch("edit")}
178
- on:clear={() => {
156
+ onclear={() => {
179
157
  fullscreen = false;
180
158
  gradio.dispatch("clear");
181
159
  gradio.dispatch("input");
182
160
  }}
183
- on:stream={({ detail }) => {
161
+ onstream={(detail) => {
184
162
  stream_data = detail;
185
163
  gradio.dispatch("stream", detail);
186
164
  }}
187
- on:drag={({ detail }) => (dragging = detail)}
188
- on:upload={() => {
165
+ ondrag={(detail) => (dragging = detail)}
166
+ onupload={() => {
189
167
  gradio.dispatch("upload");
190
168
  gradio.dispatch("input");
191
169
  }}
192
- on:select={({ detail }) => gradio.dispatch("select", detail)}
193
- on:share={({ detail }) => gradio.dispatch("share", detail)}
170
+ onselect={(detail) => gradio.dispatch("select", detail)}
194
171
  onerror={(detail) => {
195
172
  gradio.shared.loading_status.status = "error";
196
173
  gradio.dispatch("error", detail);
197
174
  }}
198
- on:close_stream={() => {
175
+ onclose_stream={() => {
199
176
  gradio.dispatch("close_stream");
200
177
  }}
201
- on:fullscreen={({ detail }) => {
178
+ onfullscreen={(detail) => {
202
179
  fullscreen = detail;
203
180
  }}
204
181
  label={gradio.shared.label}
@@ -2,9 +2,15 @@
2
2
  import Image from "./shared/Image.svelte";
3
3
  import type { FileData } from "@gradio/client";
4
4
 
5
- export let value: null | FileData;
6
- export let type: "gallery" | "table";
7
- export let selected = false;
5
+ let {
6
+ value,
7
+ type,
8
+ selected = false
9
+ }: {
10
+ value: null | FileData;
11
+ type: "gallery" | "table";
12
+ selected?: boolean;
13
+ } = $props();
8
14
  </script>
9
15
 
10
16
  <div
@@ -1,23 +1,9 @@
1
1
  import type { FileData } from "@gradio/client";
2
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
- $$bindings?: Bindings;
5
- } & Exports;
6
- (internal: unknown, props: Props & {
7
- $$events?: Events;
8
- $$slots?: Slots;
9
- }): Exports & {
10
- $set?: any;
11
- $on?: any;
12
- };
13
- z_$$bindings?: Bindings;
14
- }
15
- declare const Example: $$__sveltets_2_IsomorphicComponent<{
2
+ type $$ComponentProps = {
16
3
  value: null | FileData;
17
4
  type: "gallery" | "table";
18
5
  selected?: boolean;
19
- }, {
20
- [evt: string]: CustomEvent<any>;
21
- }, {}, {}, string>;
22
- type Example = InstanceType<typeof Example>;
6
+ };
7
+ declare const Example: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type Example = ReturnType<typeof Example>;
23
9
  export default Example;
package/dist/Index.svelte CHANGED
@@ -19,7 +19,7 @@
19
19
  import type { ImageProps, ImageEvents } from "./shared/types";
20
20
 
21
21
  let stream_data = { value: null };
22
- let upload_promise = $state<Promise<any>>();
22
+ let upload_promise = $state<Promise<any> | null>(null);
23
23
  class ImageGradio extends Gradio<ImageEvents, ImageProps> {
24
24
  async get_data() {
25
25
  if (upload_promise) {
@@ -37,50 +37,34 @@
37
37
  }
38
38
 
39
39
  const props = $props();
40
- const gradio = new ImageGradio(props);
40
+ const gradio = new ImageGradio(props, { value: null });
41
41
 
42
+ let value = $state(gradio.props.value ?? null);
42
43
  let fullscreen = $state(false);
43
44
  let dragging = $state(false);
44
45
  let active_source = $derived.by(() =>
45
46
  gradio.props.sources ? gradio.props.sources[0] : null
46
47
  );
47
48
 
48
- let upload_component: ImageUploader;
49
- const handle_drag_event = (event: Event): void => {
50
- const drag_event = event as DragEvent;
51
- drag_event.preventDefault();
52
- drag_event.stopPropagation();
53
- if (drag_event.type === "dragenter" || drag_event.type === "dragover") {
54
- dragging = true;
55
- } else if (drag_event.type === "dragleave") {
56
- dragging = false;
57
- }
58
- };
59
-
60
- const handle_drop = (event: Event): void => {
61
- if (gradio.shared.interactive) {
62
- const drop_event = event as DragEvent;
63
- drop_event.preventDefault();
64
- drop_event.stopPropagation();
65
- dragging = false;
49
+ $effect(() => {
50
+ value = gradio.props.value ?? null;
51
+ });
66
52
 
67
- if (upload_component) {
68
- upload_component.loadFilesFromDrop(drop_event);
69
- }
70
- }
71
- };
53
+ $effect(() => {
54
+ gradio.props.value = value;
55
+ });
72
56
 
73
- let old_value = gradio.props.value;
57
+ let old_value = value;
74
58
  let mounted = false;
75
59
 
76
60
  $effect(() => {
77
61
  if (!mounted) {
78
- old_value = gradio.props.value;
62
+ old_value = value;
79
63
  mounted = true;
80
64
  return;
81
65
  }
82
- if (old_value !== gradio.props.value) {
83
- old_value = gradio.props.value;
66
+ if (old_value !== value) {
67
+ old_value = value;
84
68
  gradio.dispatch("change");
85
69
  }
86
70
  });
@@ -100,7 +84,7 @@
100
84
  width={gradio.props.width}
101
85
  allow_overflow={false}
102
86
  container={gradio.shared.container}
103
- scale{gradio.shared.scale}
87
+ scale={gradio.shared.scale}
104
88
  min_width={gradio.shared.min_width}
105
89
  bind:fullscreen
106
90
  >
@@ -112,14 +96,14 @@
112
96
  gradio.dispatch("clear_status", gradio.shared.loading_status)}
113
97
  />
114
98
  <StaticImage
115
- on:select={({ detail }) => gradio.dispatch("select", detail)}
116
- on:share={({ detail }) => gradio.dispatch("share", detail)}
117
- on:error={({ detail }) => gradio.dispatch("error", detail)}
118
- on:fullscreen={({ detail }) => {
99
+ onselect={(detail) => gradio.dispatch("select", detail)}
100
+ onshare={(detail) => gradio.dispatch("share", detail)}
101
+ onerror={(detail) => gradio.dispatch("error", detail)}
102
+ onfullscreen={(detail) => {
119
103
  fullscreen = detail;
120
104
  }}
121
105
  {fullscreen}
122
- value={gradio.props.value}
106
+ {value}
123
107
  label={gradio.shared.label}
124
108
  show_label={gradio.shared.show_label}
125
109
  selectable={gradio.props._selectable}
@@ -145,10 +129,6 @@
145
129
  scale={gradio.shared.scale}
146
130
  min_width={gradio.shared.min_width}
147
131
  bind:fullscreen
148
- on:dragenter={handle_drag_event}
149
- on:dragleave={handle_drag_event}
150
- on:dragover={handle_drag_event}
151
- on:drop={handle_drop}
152
132
  >
153
133
  {#if gradio.shared.loading_status.type === "output" || gradio.shared.loading_status.validation_error}
154
134
  <StatusTracker
@@ -161,9 +141,8 @@
161
141
  {/if}
162
142
  <ImageUploader
163
143
  bind:upload_promise
164
- bind:this={upload_component}
165
144
  bind:active_source
166
- bind:value={gradio.props.value}
145
+ bind:value
167
146
  bind:dragging
168
147
  selectable={gradio.props._selectable}
169
148
  root={gradio.shared.root}
@@ -174,31 +153,29 @@
174
153
  : gradio.props.buttons.some(
175
154
  (btn) => typeof btn === "string" && btn === "fullscreen"
176
155
  )}
177
- on:edit={() => gradio.dispatch("edit")}
178
- on:clear={() => {
156
+ onclear={() => {
179
157
  fullscreen = false;
180
158
  gradio.dispatch("clear");
181
159
  gradio.dispatch("input");
182
160
  }}
183
- on:stream={({ detail }) => {
161
+ onstream={(detail) => {
184
162
  stream_data = detail;
185
163
  gradio.dispatch("stream", detail);
186
164
  }}
187
- on:drag={({ detail }) => (dragging = detail)}
188
- on:upload={() => {
165
+ ondrag={(detail) => (dragging = detail)}
166
+ onupload={() => {
189
167
  gradio.dispatch("upload");
190
168
  gradio.dispatch("input");
191
169
  }}
192
- on:select={({ detail }) => gradio.dispatch("select", detail)}
193
- on:share={({ detail }) => gradio.dispatch("share", detail)}
170
+ onselect={(detail) => gradio.dispatch("select", detail)}
194
171
  onerror={(detail) => {
195
172
  gradio.shared.loading_status.status = "error";
196
173
  gradio.dispatch("error", detail);
197
174
  }}
198
- on:close_stream={() => {
175
+ onclose_stream={() => {
199
176
  gradio.dispatch("close_stream");
200
177
  }}
201
- on:fullscreen={({ detail }) => {
178
+ onfullscreen={(detail) => {
202
179
  fullscreen = detail;
203
180
  }}
204
181
  label={gradio.shared.label}
@@ -1,24 +1,44 @@
1
1
  <script lang="ts">
2
+ import type { HTMLImgAttributes } from "svelte/elements";
3
+
2
4
  let {
3
- src,
4
- restProps,
5
+ src = "",
6
+ restProps = {},
5
7
  data_testid,
6
- class_names
8
+ class_names = [],
9
+ onload,
10
+ ...imgProps
7
11
  }: {
8
- src: string;
9
- restProps: object;
10
- data_testid: string;
11
- class_names: string[];
12
+ src?: string;
13
+ restProps?: Record<string, any>;
14
+ data_testid?: string;
15
+ class_names?: string[];
16
+ onload?: HTMLImgAttributes["onload"];
17
+ [key: string]: any;
12
18
  } = $props();
19
+
20
+ const without_class = ({
21
+ class: _class,
22
+ ...props
23
+ }: Record<string, any>): Record<string, any> => props;
24
+
25
+ let rest_img_props = $derived(without_class(restProps));
26
+ let direct_img_props = $derived(without_class(imgProps));
27
+ let classes = $derived(
28
+ [class_names.join(" "), restProps.class, imgProps.class]
29
+ .filter(Boolean)
30
+ .join(" ")
31
+ );
13
32
  </script>
14
33
 
15
34
  <!-- svelte-ignore a11y-missing-attribute -->
16
35
  <img
17
36
  {src}
18
- class={(class_names || []).join(" ")}
37
+ class={classes}
19
38
  data-testid={data_testid}
20
- {...restProps}
21
- on:load
39
+ {...rest_img_props}
40
+ {...direct_img_props}
41
+ {onload}
22
42
  />
23
43
 
24
44
  <style>