@gradio/imageeditor 0.19.0 → 0.20.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 (58) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/Example.svelte +9 -3
  3. package/Index.svelte +23 -24
  4. package/InteractiveImageEditor.svelte +99 -61
  5. package/dist/Example.svelte +9 -3
  6. package/dist/Example.svelte.d.ts +4 -18
  7. package/dist/Index.svelte +23 -24
  8. package/dist/Index.svelte.d.ts +2 -19
  9. package/dist/InteractiveImageEditor.svelte +99 -61
  10. package/dist/InteractiveImageEditor.svelte.d.ts +15 -32
  11. package/dist/shared/Anchor.svelte +13 -11
  12. package/dist/shared/Anchor.svelte.d.ts +4 -19
  13. package/dist/shared/Controls.svelte +63 -39
  14. package/dist/shared/Controls.svelte.d.ts +13 -28
  15. package/dist/shared/IconButton.svelte +38 -17
  16. package/dist/shared/IconButton.svelte.d.ts +5 -19
  17. package/dist/shared/ImageEditor.svelte +235 -160
  18. package/dist/shared/ImageEditor.svelte.d.ts +55 -2
  19. package/dist/shared/Layers.svelte +43 -29
  20. package/dist/shared/Layers.svelte.d.ts +10 -25
  21. package/dist/shared/Resize.svelte +22 -16
  22. package/dist/shared/Resize.svelte.d.ts +6 -22
  23. package/dist/shared/SecondaryToolbar.svelte +34 -18
  24. package/dist/shared/SecondaryToolbar.svelte.d.ts +10 -25
  25. package/dist/shared/Toolbar.svelte +60 -76
  26. package/dist/shared/Toolbar.svelte.d.ts +9 -24
  27. package/dist/shared/brush/BrushOptions.svelte +49 -29
  28. package/dist/shared/brush/BrushOptions.svelte.d.ts +6 -21
  29. package/dist/shared/brush/BrushSize.svelte +12 -9
  30. package/dist/shared/brush/BrushSize.svelte.d.ts +5 -20
  31. package/dist/shared/brush/ColorField.svelte +21 -16
  32. package/dist/shared/brush/ColorField.svelte.d.ts +6 -22
  33. package/dist/shared/brush/ColorPicker.svelte +22 -16
  34. package/dist/shared/brush/ColorPicker.svelte.d.ts +4 -18
  35. package/dist/shared/brush/ColorSwatch.svelte +41 -21
  36. package/dist/shared/brush/ColorSwatch.svelte.d.ts +10 -24
  37. package/dist/shared/brush/brush.js +7 -8
  38. package/dist/shared/core/editor.d.ts +5 -0
  39. package/dist/shared/core/editor.js +43 -2
  40. package/dist/shared/resize/resize.d.ts +2 -0
  41. package/dist/shared/resize/resize.js +27 -18
  42. package/package.json +8 -8
  43. package/shared/Anchor.svelte +13 -11
  44. package/shared/Controls.svelte +63 -39
  45. package/shared/IconButton.svelte +38 -17
  46. package/shared/ImageEditor.svelte +235 -160
  47. package/shared/Layers.svelte +43 -29
  48. package/shared/Resize.svelte +22 -16
  49. package/shared/SecondaryToolbar.svelte +34 -18
  50. package/shared/Toolbar.svelte +60 -76
  51. package/shared/brush/BrushOptions.svelte +49 -29
  52. package/shared/brush/BrushSize.svelte +12 -9
  53. package/shared/brush/ColorField.svelte +21 -16
  54. package/shared/brush/ColorPicker.svelte +22 -16
  55. package/shared/brush/ColorSwatch.svelte +41 -21
  56. package/shared/brush/brush.ts +8 -10
  57. package/shared/core/editor.ts +49 -2
  58. package/shared/resize/resize.ts +28 -20
@@ -6,7 +6,7 @@
6
6
  </script>
7
7
 
8
8
  <script lang="ts">
9
- import { onMount, createEventDispatcher, tick } from "svelte";
9
+ import { onMount, tick, type Snippet } from "svelte";
10
10
  import { get } from "svelte/store";
11
11
  import Toolbar, { type Tool as ToolbarTool } from "./Toolbar.svelte";
12
12
  import { CropTool } from "./crop/crop";
@@ -30,49 +30,92 @@
30
30
  import IconButton from "./IconButton.svelte";
31
31
 
32
32
  const { drag, open_file_upload } = create_drag();
33
- const dispatch = createEventDispatcher<{
34
- clear?: never;
35
- save: void;
36
- change: void;
37
- upload: void;
38
- input: void;
39
- download_error: string;
40
- }>();
41
33
 
42
34
  export const antialias = true;
43
35
 
44
- export let changeable = false;
45
- export let sources: Source[] = ["upload", "webcam", "clipboard"];
46
- export let transforms: Transform[] = ["crop", "resize"];
47
- export let canvas_size: [number, number];
48
- export let is_dragging = false;
49
- export let background_image = false;
50
- export let brush_options: Brush | false;
51
- export let eraser_options: Eraser | false;
52
- export let fixed_canvas = false;
53
- export let root: string;
54
- export let i18n: I18nFormatter;
55
- export let upload: Client["upload"];
56
- export let composite: FileData | null;
57
- export let layers: FileData[];
58
- export let background: FileData | null;
59
- export let border_region = 0;
60
- export let layer_options: LayerOptions;
61
- export let current_tool: ToolbarTool;
62
- export let webcam_options: WebcamOptions;
63
- export let show_download_button = false;
64
- export let theme_mode: "dark" | "light";
65
- export let full_history: CommandNode | null = null;
36
+ let {
37
+ changeable = false,
38
+ sources = ["upload", "webcam", "clipboard"],
39
+ transforms = ["crop", "resize"],
40
+ canvas_size,
41
+ is_dragging = $bindable(false),
42
+ background_image = $bindable(false),
43
+ brush_options,
44
+ eraser_options,
45
+ fixed_canvas = false,
46
+ root,
47
+ i18n,
48
+ upload,
49
+ composite,
50
+ layers,
51
+ background,
52
+ border_region = 0,
53
+ layer_options,
54
+ current_tool = $bindable(),
55
+ webcam_options,
56
+ show_download_button = false,
57
+ theme_mode,
58
+ full_history = $bindable(null),
59
+ has_drawn = $bindable(false),
60
+ can_undo = $bindable(false),
61
+ onclear,
62
+ onsave,
63
+ onchange,
64
+ onupload,
65
+ oninput,
66
+ ondownload_error,
67
+ children
68
+ }: {
69
+ changeable?: boolean;
70
+ sources?: Source[];
71
+ transforms?: Transform[];
72
+ canvas_size: [number, number];
73
+ is_dragging?: boolean;
74
+ background_image?: boolean;
75
+ brush_options: Brush | false;
76
+ eraser_options: Eraser | false;
77
+ fixed_canvas?: boolean;
78
+ root: string;
79
+ i18n: I18nFormatter;
80
+ upload: Client["upload"];
81
+ composite: FileData | null;
82
+ layers: FileData[];
83
+ background: FileData | null;
84
+ border_region?: number;
85
+ layer_options: LayerOptions;
86
+ current_tool: ToolbarTool;
87
+ webcam_options: WebcamOptions;
88
+ show_download_button?: boolean;
89
+ theme_mode: "dark" | "light";
90
+ full_history?: CommandNode | null;
91
+ has_drawn?: boolean;
92
+ can_undo?: boolean;
93
+ onclear?: () => void;
94
+ onsave?: () => void;
95
+ onchange?: () => void;
96
+ onupload?: () => void;
97
+ oninput?: () => void;
98
+ ondownload_error?: (error: string) => void;
99
+ children?: Snippet;
100
+ } = $props();
66
101
 
67
102
  let pixi_target: HTMLDivElement;
68
103
  let pixi_target_crop: HTMLDivElement;
69
104
 
70
- $: if (layer_options) {
71
- if (check_if_should_init()) {
72
- editor.set_layer_options(layer_options);
73
- refresh_tools();
74
- }
75
- }
105
+ let layer_options_key: string | undefined | null = null;
106
+ $effect(() => {
107
+ if (!check_if_should_init()) return;
108
+
109
+ const next_layer_options_key = JSON.stringify(layer_options);
110
+ if (next_layer_options_key === layer_options_key) return;
111
+
112
+ layer_options_key = next_layer_options_key;
113
+ editor.set_layer_options(layer_options);
114
+ });
115
+
116
+ $effect(() => {
117
+ refresh_tools();
118
+ });
76
119
 
77
120
  function refresh_tools(): void {
78
121
  if (!editor || !ready) return;
@@ -80,13 +123,14 @@
80
123
  editor.set_subtool(current_subtool);
81
124
  }
82
125
 
83
- $: if (editor && ready && editor.layers) {
126
+ $effect(() => {
127
+ if (!(editor && ready && editor.layers)) return;
84
128
  const current_layers = get(editor.layers);
85
129
 
86
130
  if (current_layers.layers.length > 0 && !current_layers.active_layer) {
87
131
  refresh_tools_for_layer_changes(current_layers);
88
132
  }
89
- }
133
+ });
90
134
 
91
135
  /**
92
136
  * Refreshes tools when layer state changes
@@ -115,8 +159,6 @@
115
159
  return layer_options && editor && ready;
116
160
  }
117
161
 
118
- export let has_drawn = false;
119
-
120
162
  /**
121
163
  * Gets the image blobs from the editor
122
164
  * @returns {Promise<ImageBlobs>} Object containing background, layers, and composite image blobs
@@ -176,8 +218,8 @@
176
218
  crop.set_subtool("crop");
177
219
  background_image = true;
178
220
  zoom.set_zoom("fit");
179
- dispatch("upload");
180
- dispatch("input");
221
+ onupload?.();
222
+ oninput?.();
181
223
  } catch (error) {
182
224
  console.error("Error adding image from URL:", error);
183
225
  }
@@ -200,8 +242,8 @@
200
242
  try {
201
243
  await pending_bg;
202
244
  await editor.add_layers_from_url(source.map((item) => item.url));
203
- dispatch("change");
204
- dispatch("input");
245
+ onchange?.();
246
+ oninput?.();
205
247
  } catch (error) {
206
248
  console.error("Error adding layer from URL:", error);
207
249
  }
@@ -210,10 +252,10 @@
210
252
 
211
253
  let brush: BrushTool;
212
254
  let zoom: ZoomTool;
213
- let zoom_level = 1;
214
- let ready = false;
215
- let mounted = false;
216
- let min_zoom = true;
255
+ let zoom_level = $state(1);
256
+ let ready = $state(false);
257
+ let mounted = $state(false);
258
+ let min_zoom = $state(true);
217
259
 
218
260
  let last_dimensions = { width: 0, height: 0 };
219
261
 
@@ -221,9 +263,11 @@
221
263
  * Handles visibility changes and resets zoom if dimensions have changed
222
264
  */
223
265
  async function handle_visibility_change(): Promise<void> {
224
- if (!editor || !ready || !zoom) return;
266
+ if (!editor || !ready || !zoom || !pixi_target) return;
225
267
  await tick();
226
268
 
269
+ if (!pixi_target) return;
270
+
227
271
  const is_visible = pixi_target.offsetParent !== null;
228
272
 
229
273
  if (is_visible) {
@@ -316,8 +360,7 @@
316
360
 
317
361
  let crop: ImageEditor;
318
362
  let crop_zoom: ZoomTool;
319
- export let can_undo = false;
320
- let can_redo = false;
363
+ let can_redo = $state(false);
321
364
  async function init_image_editor(): Promise<void> {
322
365
  brush = new BrushTool();
323
366
  zoom = new ZoomTool();
@@ -382,8 +425,8 @@
382
425
  });
383
426
 
384
427
  editor.on("change", () => {
385
- dispatch("change");
386
- dispatch("input");
428
+ onchange?.();
429
+ oninput?.();
387
430
  full_history = editor.command_manager.history;
388
431
  });
389
432
 
@@ -403,23 +446,27 @@
403
446
  refresh_tools();
404
447
  }
405
448
 
406
- $: if (
407
- background == null &&
408
- layers.length == 0 &&
409
- composite == null &&
410
- editor &&
411
- ready
412
- ) {
413
- editor.reset_canvas();
414
- zoom.set_zoom("fit");
415
- handle_tool_change({ tool: "image" });
416
- background_image = false;
417
- has_drawn = false;
418
- }
449
+ $effect(() => {
450
+ if (
451
+ background == null &&
452
+ layers.length == 0 &&
453
+ composite == null &&
454
+ editor &&
455
+ ready
456
+ ) {
457
+ editor.reset_canvas();
458
+ zoom.set_zoom("fit");
459
+ handle_tool_change({ tool: "image" });
460
+ background_image = false;
461
+ has_drawn = false;
462
+ }
463
+ });
419
464
 
420
- $: current_tool === "image" &&
421
- current_subtool === "crop" &&
422
- crop_zoom.set_zoom("fit");
465
+ $effect(() => {
466
+ if (current_tool === "image" && current_subtool === "crop") {
467
+ crop_zoom.set_zoom("fit");
468
+ }
469
+ });
423
470
 
424
471
  /**
425
472
  * Handles file uploads
@@ -439,10 +486,15 @@
439
486
  background_image = true;
440
487
  zoom.set_zoom("fit");
441
488
  handle_tool_change({ tool: "draw" });
442
- dispatch("upload");
489
+ onupload?.();
443
490
  }
444
491
 
445
- $: background_image = can_undo && editor.command_manager.contains("AddImage");
492
+ $effect(() => {
493
+ if (editor) {
494
+ background_image =
495
+ can_undo && editor.command_manager.contains("AddImage");
496
+ }
497
+ });
446
498
 
447
499
  /**
448
500
  * Handles tool change events
@@ -500,13 +552,13 @@
500
552
  }
501
553
  }
502
554
 
503
- let eraser_size_visible = false;
504
- let selected_color: ColorInput | string;
505
- let selected_size: number;
506
- let selected_opacity = 1;
507
- let selected_eraser_size: number;
555
+ let eraser_size_visible = $state(false);
556
+ let selected_color = $state<ColorInput | string>();
557
+ let selected_size = $state<number>();
558
+ let selected_opacity = $state(1);
559
+ let selected_eraser_size = $state<number>();
508
560
 
509
- $: {
561
+ $effect(() => {
510
562
  if (brush_options) {
511
563
  update_brush_options();
512
564
  }
@@ -514,7 +566,7 @@
514
566
  if (eraser_options) {
515
567
  update_eraser_options();
516
568
  }
517
- }
569
+ });
518
570
 
519
571
  function update_brush_options(): void {
520
572
  if (!brush_options) return;
@@ -551,51 +603,76 @@
551
603
  options.default_size === "auto" ? 25 : options.default_size;
552
604
  }
553
605
 
554
- let brush_size_visible = false;
555
-
556
- let brush_color_visible = false;
606
+ let brush_size_visible = $state(false);
607
+
608
+ let brush_color_visible = $state(false);
609
+
610
+ $effect(() => {
611
+ if (!brush) return;
612
+ brush.set_brush_color(
613
+ (() => {
614
+ let color_value: ColorInput = "black";
615
+ if (selected_color === "auto") {
616
+ if (!brush_options) return "black";
617
+ const default_color =
618
+ brush_options.colors.find((color) =>
619
+ Array.isArray(color)
620
+ ? color[0] === brush_options.default_color
621
+ : color === brush_options.default_color
622
+ ) || brush_options.colors[0];
623
+
624
+ color_value = Array.isArray(default_color)
625
+ ? default_color[0]
626
+ : default_color;
627
+ } else {
628
+ color_value = selected_color ?? "black";
629
+ }
630
+ return color_value;
631
+ })()
632
+ );
633
+ });
557
634
 
558
- $: brush?.set_brush_color(
559
- (() => {
560
- let color_value;
561
- if (selected_color === "auto") {
562
- if (!brush_options) return "black";
563
- const options = brush_options;
564
- const default_color =
565
- options.colors.find((color) =>
566
- Array.isArray(color)
567
- ? color[0] === options.default_color
568
- : color === options.default_color
569
- ) || options.colors[0];
635
+ $effect(() => {
636
+ brush?.set_brush_size(
637
+ typeof selected_size === "number" ? selected_size : 25
638
+ );
639
+ });
570
640
 
571
- color_value = Array.isArray(default_color)
572
- ? default_color[0]
573
- : default_color;
574
- } else {
575
- color_value = selected_color;
576
- }
577
- return color_value;
578
- })()
579
- );
641
+ $effect(() => {
642
+ brush?.set_eraser_size(
643
+ typeof selected_eraser_size === "number" ? selected_eraser_size : 25
644
+ );
645
+ });
580
646
 
581
- $: brush?.set_brush_size(
582
- typeof selected_size === "number" ? selected_size : 25
583
- );
647
+ let disable_click = $state(true);
648
+ $effect(() => {
649
+ disable_click =
650
+ current_tool !== "image" ||
651
+ (current_tool === "image" && background_image) ||
652
+ (current_tool === "image" && current_subtool === "webcam") ||
653
+ !sources.includes("upload");
654
+ });
584
655
 
585
- $: brush?.set_eraser_size(
586
- typeof selected_eraser_size === "number" ? selected_eraser_size : 25
587
- );
656
+ let current_subtool = $state<Subtool | null>(null);
657
+ let preview = $state(false);
658
+ $effect(() => {
659
+ brush?.preview_brush(preview);
660
+ });
661
+ $effect(() => {
662
+ brush?.set_brush_opacity(selected_opacity);
663
+ });
588
664
 
589
- $: disable_click =
590
- current_tool !== "image" ||
591
- (current_tool === "image" && background_image) ||
592
- (current_tool === "image" && current_subtool === "webcam") ||
593
- !sources.includes("upload");
665
+ function remove_image(): void {
666
+ onclear?.();
667
+ editor.reset_canvas();
668
+ handle_tool_change({ tool: "image" });
669
+ background_image = false;
670
+ has_drawn = false;
671
+ }
594
672
 
595
- let current_subtool: Subtool | null = null;
596
- let preview = false;
597
- $: brush?.preview_brush(preview);
598
- $: brush?.set_brush_opacity(selected_opacity);
673
+ export function handle_remove(): void {
674
+ remove_image();
675
+ }
599
676
 
600
677
  function handle_zoom_change(zoom_level: number | "fit"): void {
601
678
  zoom.set_zoom(zoom_level);
@@ -622,19 +699,23 @@
622
699
  }
623
700
  }
624
701
 
625
- function handle_capture(e: CustomEvent): void {
626
- if (e.detail !== null) {
627
- handle_files(e.detail as Blob);
702
+ function handle_capture(value: FileData | Blob | null): void {
703
+ if (value !== null) {
704
+ handle_files(value as Blob);
628
705
  }
629
706
  handle_subtool_change({ tool: current_tool, subtool: null });
630
707
  }
631
708
 
632
709
  function handle_save(): void {
633
- dispatch("save");
710
+ onsave?.();
634
711
  }
635
712
 
636
- $: add_image_from_url(composite || background);
637
- $: add_layers_from_url(layers);
713
+ $effect(() => {
714
+ add_image_from_url(composite || background);
715
+ });
716
+ $effect(() => {
717
+ add_layers_from_url(layers);
718
+ });
638
719
 
639
720
  async function handle_crop_confirm(): Promise<void> {
640
721
  const { image } = await crop.get_crop_bounds();
@@ -645,8 +726,8 @@
645
726
  resize: false
646
727
  });
647
728
  handle_subtool_change({ tool: "image", subtool: null });
648
- dispatch("change");
649
- dispatch("input");
729
+ onchange?.();
730
+ oninput?.();
650
731
  }
651
732
 
652
733
  async function handle_download(): Promise<void> {
@@ -654,7 +735,7 @@
654
735
 
655
736
  const blob = blobs.composite;
656
737
  if (!blob) {
657
- dispatch("download_error", "Unable to generate image to download.");
738
+ ondownload_error?.("Unable to generate image to download.");
658
739
  return;
659
740
  }
660
741
  const url = URL.createObjectURL(blob);
@@ -675,12 +756,13 @@
675
756
 
676
757
  let old_background = background;
677
758
 
678
- $: if (old_background !== background) {
759
+ $effect(() => {
760
+ if (old_background === background) return;
679
761
  handle_tool_change({ tool: "draw" });
680
762
  handle_subtool_change({ tool: "draw", subtool: null });
681
763
  current_tool = "draw";
682
764
  old_background = background;
683
- }
765
+ });
684
766
  </script>
685
767
 
686
768
  <div
@@ -691,7 +773,8 @@
691
773
  on_drag_change: (dragging) => (is_dragging = dragging),
692
774
  on_files: handle_files,
693
775
  accepted_types: "image/*",
694
- disable_click: disable_click
776
+ disable_click: disable_click,
777
+ ignore_click_selector: ".toolbar-wrap"
695
778
  }}
696
779
  aria-label={"Click to upload or drop files"}
697
780
  aria-dropeffect="copy"
@@ -700,30 +783,24 @@
700
783
  {#if current_subtool !== "crop"}
701
784
  <Controls
702
785
  {changeable}
703
- on:set_zoom={(e) => handle_zoom_change(e.detail)}
704
- on:zoom_in={() => zoom_in_out("in")}
705
- on:zoom_out={() => zoom_in_out("out")}
786
+ onset_zoom={(zoom) => handle_zoom_change(zoom)}
787
+ onzoom_in={() => zoom_in_out("in")}
788
+ onzoom_out={() => zoom_in_out("out")}
706
789
  {min_zoom}
707
790
  current_zoom={zoom_level}
708
- on:remove_image={() => {
709
- dispatch("clear");
710
- editor.reset_canvas();
711
- handle_tool_change({ tool: "image" });
712
- background_image = false;
713
- has_drawn = false;
714
- }}
791
+ onremove_image={remove_image}
715
792
  tool={current_tool}
716
793
  can_save={true}
717
- on:save={handle_save}
718
- on:pan={(e) => {
794
+ onsave={handle_save}
795
+ onpan={() => {
719
796
  handle_tool_change({ tool: "pan" });
720
797
  }}
721
798
  enable_download={show_download_button}
722
- on:download={() => handle_download()}
799
+ ondownload={() => handle_download()}
723
800
  {can_undo}
724
801
  {can_redo}
725
- on:undo={handle_undo}
726
- on:redo={handle_redo}
802
+ onundo={handle_undo}
803
+ onredo={handle_redo}
727
804
  />
728
805
  {/if}
729
806
 
@@ -732,8 +809,8 @@
732
809
  {sources}
733
810
  {transforms}
734
811
  background={background_image}
735
- on:tool_change={(e) => handle_tool_change(e.detail)}
736
- on:subtool_change={(e) => handle_subtool_change(e.detail)}
812
+ ontool_change={(detail) => handle_tool_change(detail)}
813
+ onsubtool_change={(detail) => handle_subtool_change(detail)}
737
814
  show_brush_size={brush_size_visible}
738
815
  show_brush_color={brush_color_visible}
739
816
  show_eraser_size={eraser_size_visible}
@@ -755,9 +832,7 @@
755
832
  <Webcam
756
833
  {upload}
757
834
  {root}
758
- on:capture={handle_capture}
759
- on:error
760
- on:drag
835
+ oncapture={handle_capture}
761
836
  streaming={false}
762
837
  mode="image"
763
838
  include_audio={false}
@@ -773,23 +848,23 @@
773
848
  <SecondaryToolbar
774
849
  enable_additional_layers={layer_options.allow_additional_layers}
775
850
  layers={editor.layers}
776
- on:new_layer={() => {
851
+ onnew_layer={() => {
777
852
  editor.add_layer();
778
853
  }}
779
- on:change_layer={(e) => {
780
- editor.set_layer(e.detail);
854
+ onchange_layer={(id) => {
855
+ editor.set_layer(id);
781
856
  if (current_tool === "draw") {
782
857
  handle_tool_change({ tool: "draw" });
783
858
  }
784
859
  }}
785
- on:move_layer={(e) => {
786
- editor.move_layer(e.detail.id, e.detail.direction);
860
+ onmove_layer={({ id, direction }) => {
861
+ editor.move_layer(id, direction);
787
862
  }}
788
- on:delete_layer={(e) => {
789
- editor.delete_layer(e.detail);
863
+ ondelete_layer={(id) => {
864
+ editor.delete_layer(id);
790
865
  }}
791
- on:toggle_layer_visibility={(e) => {
792
- editor.toggle_layer_visibility(e.detail);
866
+ ontoggle_layer_visibility={(id) => {
867
+ editor.toggle_layer_visibility(id);
793
868
  }}
794
869
  />
795
870
  {/if}
@@ -820,7 +895,7 @@
820
895
  />
821
896
  </div>
822
897
  {/if}
823
- <slot></slot>
898
+ {#if children}{@render children()}{/if}
824
899
  </div>
825
900
 
826
901
  <style>