@gradio/imageeditor 0.12.14 → 0.13.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 (130) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/Example.svelte +1 -1
  3. package/IMAGE_EDITOR_OVERVIEW.md +224 -0
  4. package/ImageEditor.stories.svelte +4 -4
  5. package/Index.svelte +35 -46
  6. package/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +56 -103
  7. package/dist/Example.svelte.d.ts +1 -1
  8. package/dist/Index.svelte +29 -33
  9. package/dist/Index.svelte.d.ts +17 -16
  10. package/dist/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +47 -88
  11. package/dist/{shared/InteractiveImageEditor.svelte.d.ts → InteractiveImageEditor.svelte.d.ts} +13 -15
  12. package/dist/shared/Anchor.svelte +311 -0
  13. package/dist/shared/Anchor.svelte.d.ts +18 -0
  14. package/dist/shared/Controls.svelte +162 -23
  15. package/dist/shared/Controls.svelte.d.ts +7 -2
  16. package/dist/shared/{tools/IconButton.svelte → IconButton.svelte} +19 -3
  17. package/dist/shared/{tools/IconButton.svelte.d.ts → IconButton.svelte.d.ts} +4 -1
  18. package/dist/shared/ImageEditor.svelte +546 -289
  19. package/dist/shared/ImageEditor.svelte.d.ts +51 -56
  20. package/dist/shared/Layers.svelte +233 -0
  21. package/dist/shared/Layers.svelte.d.ts +35 -0
  22. package/dist/shared/Resize.svelte +218 -0
  23. package/dist/shared/Resize.svelte.d.ts +29 -0
  24. package/dist/shared/SecondaryToolbar.svelte +54 -0
  25. package/dist/shared/SecondaryToolbar.svelte.d.ts +35 -0
  26. package/dist/shared/Toolbar.svelte +337 -0
  27. package/dist/shared/Toolbar.svelte.d.ts +42 -0
  28. package/dist/shared/{tools → brush}/BrushOptions.svelte +68 -64
  29. package/dist/shared/{tools → brush}/BrushOptions.svelte.d.ts +9 -9
  30. package/dist/shared/{tools → brush}/BrushSize.svelte +2 -27
  31. package/dist/shared/{tools → brush}/ColorField.svelte +2 -1
  32. package/dist/shared/{tools → brush}/ColorField.svelte.d.ts +1 -0
  33. package/dist/shared/{tools → brush}/ColorPicker.svelte +81 -9
  34. package/dist/shared/{tools → brush}/ColorPicker.svelte.d.ts +1 -0
  35. package/dist/shared/{tools → brush}/ColorSwatch.svelte +78 -38
  36. package/dist/shared/{tools → brush}/ColorSwatch.svelte.d.ts +6 -2
  37. package/dist/shared/brush/brush-cursor.d.ts +101 -0
  38. package/dist/shared/brush/brush-cursor.js +327 -0
  39. package/dist/shared/brush/brush-textures.d.ts +71 -0
  40. package/dist/shared/brush/brush-textures.js +404 -0
  41. package/dist/shared/brush/brush-utils.d.ts +32 -0
  42. package/dist/shared/brush/brush-utils.js +59 -0
  43. package/dist/shared/brush/brush.d.ts +152 -0
  44. package/dist/shared/brush/brush.js +396 -0
  45. package/dist/shared/brush/types.d.ts +37 -0
  46. package/dist/shared/brush/types.js +1 -0
  47. package/dist/shared/core/editor.d.ts +252 -0
  48. package/dist/shared/core/editor.js +1221 -0
  49. package/dist/shared/crop/crop.d.ts +201 -0
  50. package/dist/shared/crop/crop.js +727 -0
  51. package/dist/shared/image/image.d.ts +89 -0
  52. package/dist/shared/image/image.js +185 -0
  53. package/dist/shared/resize/resize.d.ts +270 -0
  54. package/dist/shared/resize/resize.js +1413 -0
  55. package/dist/shared/types.d.ts +12 -0
  56. package/dist/shared/utils/pixi.d.ts +16 -57
  57. package/dist/shared/utils/pixi.js +15 -113
  58. package/dist/shared/zoom/zoom.d.ts +136 -0
  59. package/dist/shared/zoom/zoom.js +548 -0
  60. package/package.json +10 -9
  61. package/shared/Anchor.svelte +384 -0
  62. package/shared/Controls.svelte +194 -27
  63. package/shared/{tools/IconButton.svelte → IconButton.svelte} +21 -4
  64. package/shared/ImageEditor.svelte +682 -349
  65. package/shared/Layers.svelte +249 -0
  66. package/shared/Resize.svelte +239 -0
  67. package/shared/SecondaryToolbar.svelte +61 -0
  68. package/shared/Toolbar.svelte +386 -0
  69. package/shared/brush/BRUSH_TOOL.md +134 -0
  70. package/shared/brush/BrushOptions.svelte +200 -0
  71. package/shared/{tools → brush}/BrushSize.svelte +2 -30
  72. package/shared/{tools → brush}/ColorField.svelte +3 -1
  73. package/shared/{tools → brush}/ColorPicker.svelte +86 -9
  74. package/shared/{tools → brush}/ColorSwatch.svelte +95 -42
  75. package/shared/brush/brush-cursor.ts +408 -0
  76. package/shared/brush/brush-textures.ts +508 -0
  77. package/shared/brush/brush-utils.ts +76 -0
  78. package/shared/brush/brush.ts +506 -0
  79. package/shared/brush/types.ts +41 -0
  80. package/shared/core/EDITOR.md +182 -0
  81. package/shared/core/editor.ts +1730 -0
  82. package/shared/crop/CROP.md +139 -0
  83. package/shared/crop/crop.ts +1027 -0
  84. package/shared/image/IMAGE.md +135 -0
  85. package/shared/image/image.ts +300 -0
  86. package/shared/resize/resize.ts +1894 -0
  87. package/shared/types.ts +14 -0
  88. package/shared/utils/pixi.ts +19 -246
  89. package/shared/zoom/zoom.ts +729 -0
  90. package/dist/shared/layers/Layers.svelte +0 -231
  91. package/dist/shared/layers/Layers.svelte.d.ts +0 -18
  92. package/dist/shared/layers/utils.d.ts +0 -73
  93. package/dist/shared/layers/utils.js +0 -156
  94. package/dist/shared/tools/Brush.svelte +0 -215
  95. package/dist/shared/tools/Brush.svelte.d.ts +0 -29
  96. package/dist/shared/tools/Crop.svelte +0 -155
  97. package/dist/shared/tools/Crop.svelte.d.ts +0 -16
  98. package/dist/shared/tools/Cropper.svelte +0 -271
  99. package/dist/shared/tools/Cropper.svelte.d.ts +0 -41
  100. package/dist/shared/tools/Handle.svelte +0 -260
  101. package/dist/shared/tools/Handle.svelte.d.ts +0 -29
  102. package/dist/shared/tools/Sources.svelte +0 -257
  103. package/dist/shared/tools/Sources.svelte.d.ts +0 -34
  104. package/dist/shared/tools/Tools.svelte +0 -102
  105. package/dist/shared/tools/Tools.svelte.d.ts +0 -48
  106. package/dist/shared/tools/brush.d.ts +0 -62
  107. package/dist/shared/tools/brush.js +0 -133
  108. package/dist/shared/tools/crop.d.ts +0 -15
  109. package/dist/shared/tools/crop.js +0 -154
  110. package/dist/shared/tools/index.d.ts +0 -5
  111. package/dist/shared/tools/index.js +0 -5
  112. package/dist/shared/tools/sources.d.ts +0 -46
  113. package/dist/shared/tools/sources.js +0 -121
  114. package/dist/shared/tools/types.d.ts +0 -1
  115. package/shared/layers/Layers.svelte +0 -250
  116. package/shared/layers/utils.ts +0 -294
  117. package/shared/tools/Brush.svelte +0 -280
  118. package/shared/tools/BrushOptions.svelte +0 -180
  119. package/shared/tools/Crop.svelte +0 -189
  120. package/shared/tools/Cropper.svelte +0 -346
  121. package/shared/tools/Handle.svelte +0 -289
  122. package/shared/tools/Sources.svelte +0 -283
  123. package/shared/tools/Tools.svelte +0 -162
  124. package/shared/tools/brush.ts +0 -247
  125. package/shared/tools/crop.ts +0 -213
  126. package/shared/tools/index.ts +0 -5
  127. package/shared/tools/sources.ts +0 -195
  128. package/shared/tools/types.ts +0 -1
  129. /package/dist/shared/{tools → brush}/BrushSize.svelte.d.ts +0 -0
  130. /package/dist/shared/{tools/types.js → types.js} +0 -0
@@ -0,0 +1,384 @@
1
+ <script lang="ts">
2
+ import { spring } from "svelte/motion";
3
+ import { Image } from "@gradio/icons";
4
+ import { onMount, createEventDispatcher } from "svelte";
5
+
6
+ const positions = [
7
+ "top-left",
8
+ "top",
9
+ "top-right",
10
+ "left",
11
+ "center",
12
+ "right",
13
+ "bottom-left",
14
+ "bottom",
15
+ "bottom-right"
16
+ ] as const;
17
+ const dispatch = createEventDispatcher<{
18
+ position: (typeof positions)[number];
19
+ }>();
20
+
21
+ const spring_opt = {
22
+ stiffness: 0.1,
23
+ precision: 0.5
24
+ };
25
+ const pos = spring([5, 5], spring_opt);
26
+ const init = [(120 - 10) / 3, (120 - 10) / 3];
27
+ const dimensions = spring(init, spring_opt);
28
+ type Arrow = {
29
+ x: number;
30
+ y: number;
31
+ x_dir: number;
32
+ y_dir: number;
33
+ rotation: number;
34
+ type: number;
35
+ };
36
+ const arrow_spring = spring<Arrow[]>([], spring_opt);
37
+
38
+ let last_i = 0;
39
+ let expanded = true;
40
+ const box_size = (120 - 5 * 2) / 3;
41
+
42
+ async function handle_box_hover(i: number): Promise<void> {
43
+ expanded = false;
44
+ last_i = i;
45
+ const y = Math.floor(i / 3);
46
+ const x = i % 3;
47
+ pos.set([x * box_size + 5 * x + 5, y * box_size + 5 * y + 5]);
48
+
49
+ dimensions.set(init);
50
+ }
51
+
52
+ let last_pos = 0;
53
+ async function handle_box_click(i: number, stagger = false): Promise<void> {
54
+ if (expanded && stagger) return;
55
+
56
+ const y = Math.floor(i / 3);
57
+ const x = i % 3;
58
+
59
+ const [initial_arrows, eventual_arrows] = get_valid_offsets({
60
+ x: x,
61
+ y: y
62
+ });
63
+
64
+ if (stagger && last_i !== i) {
65
+ await Promise.all([
66
+ pos.set([x * box_size + 5 * x + 5, y * box_size + 5 * y + 5]),
67
+ arrow_spring.set(initial_arrows, { hard: true }),
68
+ dimensions.set(init)
69
+ ]);
70
+ } else {
71
+ await arrow_spring.set(initial_arrows, { hard: true });
72
+ }
73
+ last_pos = i;
74
+ expanded = true;
75
+
76
+ pos.set([0, 0]);
77
+ dimensions.set([120 + 10, 120 + 10]);
78
+ arrow_spring.set(eventual_arrows);
79
+
80
+ dispatch("position", positions[i]);
81
+ }
82
+
83
+ function get_valid_offsets(anchorPoint: {
84
+ x: number;
85
+ y: number;
86
+ }): [Arrow[], Arrow[]] {
87
+ const destination_points: { x: number; y: number }[] = [
88
+ { x: 0, y: 0 },
89
+ { x: 1, y: 0 },
90
+ { x: 2, y: 0 },
91
+ { x: 0, y: 1 },
92
+ { x: 1, y: 1 },
93
+ { x: 2, y: 1 },
94
+ { x: 0, y: 2 },
95
+ { x: 1, y: 2 },
96
+ { x: 2, y: 2 }
97
+ ];
98
+
99
+ const offsets = destination_points
100
+ .map((dest) => ({
101
+ x: dest.x - anchorPoint.x,
102
+ y: dest.y - anchorPoint.y,
103
+ dest
104
+ }))
105
+ .filter((offset) => !(offset.x === 0 && offset.y === 0));
106
+
107
+ const normalized_offsets = offsets
108
+ .map((offset) => {
109
+ // cardinal
110
+ if (offset.x === 0 || offset.y === 0) {
111
+ const magnitude = Math.max(Math.abs(offset.x), Math.abs(offset.y));
112
+ return {
113
+ magnitude,
114
+ x: offset.x / magnitude,
115
+ y: offset.y / magnitude,
116
+ dest: offset.dest,
117
+ original: offset,
118
+ type: "cardinal" as const
119
+ };
120
+ }
121
+
122
+ // ordinal
123
+ if (Math.abs(offset.x) === Math.abs(offset.y)) {
124
+ const magnitude = Math.abs(offset.x);
125
+ return {
126
+ magnitude,
127
+ x: offset.x / magnitude,
128
+ y: offset.y / magnitude,
129
+ dest: offset.dest,
130
+ original: offset,
131
+ type: "ordinal" as const
132
+ };
133
+ }
134
+
135
+ // skip the rest
136
+ return null;
137
+ })
138
+ .filter((offset) => offset !== null);
139
+
140
+ const directions = new Map<
141
+ string,
142
+ {
143
+ x: number;
144
+ y: number;
145
+ dest: { x: number; y: number };
146
+ original: { x: number; y: number };
147
+ magnitude: number;
148
+ type: "cardinal" | "ordinal";
149
+ }[]
150
+ >();
151
+
152
+ normalized_offsets.forEach((offset) => {
153
+ if (directions.has(`${offset.x}-${offset.y}`)) {
154
+ directions.get(`${offset.x}-${offset.y}`)?.push(offset);
155
+ } else {
156
+ directions.set(`${offset.x}-${offset.y}`, [offset]);
157
+ }
158
+ });
159
+
160
+ const unique_directions = Array.from(directions.values()).map(
161
+ (direction) => {
162
+ return direction.sort((a, b) => b.magnitude - a.magnitude)[0];
163
+ }
164
+ );
165
+
166
+ const eventual_arrows = unique_directions.map((arrow) => ({
167
+ x:
168
+ arrow.dest.x * box_size +
169
+ 14 * arrow.dest.x +
170
+ 14 +
171
+ 10 +
172
+ (arrow.type === "ordinal" ? 0 : 12 * arrow.x),
173
+ y:
174
+ arrow.dest.y * box_size +
175
+ 14 * arrow.dest.y +
176
+ 14 +
177
+ 10 +
178
+ (arrow.type === "ordinal" ? 0 : 12 * arrow.y),
179
+ x_dir: arrow.x,
180
+ y_dir: arrow.y,
181
+ rotation:
182
+ Math.atan2(arrow.original.y, arrow.original.x) * (180 / Math.PI) + 180,
183
+ type: arrow.type === "ordinal" ? 1 : 2
184
+ }));
185
+
186
+ const initial_arrows = eventual_arrows.map((arrow) => ({
187
+ ...arrow,
188
+ x:
189
+ anchorPoint.x * box_size +
190
+ 14 * anchorPoint.x + // gaps
191
+ 14 + // padding
192
+ 10 + // arrow head
193
+ (arrow.x_dir * box_size) / 2 +
194
+ arrow.x_dir * (arrow.type === 1 ? 0 : 10), // offset based on direction
195
+
196
+ y:
197
+ anchorPoint.y * box_size +
198
+ 14 * anchorPoint.y +
199
+ 14 +
200
+ 10 +
201
+ (arrow.y_dir * box_size) / 2 +
202
+ arrow.y_dir * (arrow.type === 1 ? 0 : 10)
203
+ }));
204
+
205
+ return [initial_arrows, eventual_arrows];
206
+ }
207
+
208
+ onMount(() => {
209
+ handle_box_click(0);
210
+ });
211
+ </script>
212
+
213
+ <div class="wrap" class:expanded>
214
+ <div
215
+ class="box-wrap"
216
+ role="grid"
217
+ tabindex="0"
218
+ on:mouseleave={() => handle_box_click(last_pos, true)}
219
+ >
220
+ {#each { length: 9 } as _, i}
221
+ <button
222
+ class="box"
223
+ class:active-box={last_pos === i}
224
+ on:mouseenter={() => handle_box_hover(i)}
225
+ on:click={() => handle_box_click(i)}
226
+ >
227
+ <span class:active={last_pos === i} class="icon-wrap">
228
+ <Image />
229
+ </span>
230
+ </button>
231
+ {/each}
232
+ </div>
233
+ <div
234
+ class="expanding-box"
235
+ style:width="{$dimensions[0]}px"
236
+ style:height="{$dimensions[1]}px"
237
+ style:transform="translate({$pos[0]}px, {$pos[1]}px)"
238
+ ></div>
239
+ <svg viewBox="0 0 150 150">
240
+ {#each $arrow_spring as arrow}
241
+ <line
242
+ x1={(last_pos % 3) * box_size +
243
+ 14 * (last_pos % 3) +
244
+ 14 +
245
+ 10 +
246
+ (arrow.x_dir * box_size) / 2 +
247
+ arrow.x_dir * (arrow.type === 1 ? 0 : 10)}
248
+ x2={arrow.x}
249
+ y1={Math.floor(last_pos / 3) * box_size +
250
+ 14 * Math.floor(last_pos / 3) +
251
+ 14 +
252
+ 10 +
253
+ (arrow.y_dir * box_size) / 2 +
254
+ arrow.y_dir * (arrow.type === 1 ? 0 : 10)}
255
+ y2={arrow.y}
256
+ />
257
+ {/each}
258
+ {#each $arrow_spring as arrow}
259
+ <g style:transform="translate(-75px, -75px)">
260
+ <g style:transform="translate({arrow.x}px, {arrow.y}px)">
261
+ <path
262
+ d="M 70,75 80,69 80,81 Z"
263
+ style:transform="rotate({arrow.rotation}deg)"
264
+ style:transform-origin="75 75"
265
+ />
266
+ </g>
267
+ </g>
268
+ {/each}
269
+ </svg>
270
+ </div>
271
+
272
+ <style>
273
+ .wrap {
274
+ position: relative;
275
+ margin: 10px 0;
276
+ height: 130px;
277
+ width: 130px;
278
+ padding: 5px;
279
+ }
280
+
281
+ .box-wrap {
282
+ width: 100%;
283
+ height: 100%;
284
+ display: grid;
285
+ grid-template-columns: repeat(3, 1fr);
286
+ grid-template-rows: repeat(3, 1fr);
287
+ gap: 5px;
288
+
289
+ background-color: var(--block-background-fill);
290
+ }
291
+
292
+ .box {
293
+ cursor: pointer;
294
+ border: none;
295
+ border: #ccc dashed 1px;
296
+ border-radius: 4px;
297
+ transition: 0.1s;
298
+ display: flex;
299
+ justify-content: center;
300
+ align-items: center;
301
+ }
302
+
303
+ .box::after {
304
+ background: none !important;
305
+ }
306
+
307
+ .expanding-box {
308
+ --mod: 3;
309
+ --border: 2;
310
+ border: orange solid 2px;
311
+ border-radius: 4px;
312
+ position: absolute;
313
+ top: 0px;
314
+ left: 0px;
315
+ width: 100%;
316
+ height: 100%;
317
+ pointer-events: none;
318
+
319
+ width: calc(100% / var(--mod));
320
+ height: calc(100% / var(--mod));
321
+ transform-origin: 50% 0;
322
+ box-sizing: border-box;
323
+ }
324
+
325
+ svg {
326
+ position: absolute;
327
+ top: 0px;
328
+ left: 0px;
329
+ width: 100%;
330
+ height: 100%;
331
+
332
+ pointer-events: none;
333
+ }
334
+
335
+ .icon-wrap {
336
+ opacity: 0.2;
337
+ height: 50%;
338
+ width: 50%;
339
+
340
+ display: flex;
341
+ justify-content: center;
342
+ align-items: center;
343
+ border-color: #ccc;
344
+ transition: 0.1s;
345
+ padding-top: 1px;
346
+ }
347
+
348
+ .wrap.expanded .icon-wrap {
349
+ opacity: 0;
350
+ }
351
+
352
+ .wrap.expanded .box {
353
+ border-color: #fff;
354
+ }
355
+
356
+ .icon-wrap.active {
357
+ opacity: 1 !important;
358
+ }
359
+
360
+ svg {
361
+ opacity: 0;
362
+ padding: 5px;
363
+ transition: 0.1s;
364
+ }
365
+ .expanded svg {
366
+ opacity: 1;
367
+ }
368
+
369
+ svg line {
370
+ stroke: #999;
371
+ stroke-width: 3px;
372
+ stroke-linecap: round;
373
+ }
374
+
375
+ svg path {
376
+ stroke-width: 3;
377
+ stroke: #999;
378
+ stroke-linejoin: round;
379
+ stroke-linecap: round;
380
+ fill: #999;
381
+ /* transition: 0.5s; */
382
+ transform-origin: 75px 75px;
383
+ }
384
+ </style>
@@ -1,19 +1,28 @@
1
1
  <script lang="ts">
2
2
  import { createEventDispatcher } from "svelte";
3
3
  import { IconButton, IconButtonWrapper } from "@gradio/atoms";
4
- import { Undo, Redo, Check, Trash } from "@gradio/icons";
4
+ import { Check, Trash, ZoomIn, ZoomOut, Pan } from "@gradio/icons";
5
5
 
6
6
  /**
7
- * Can the current image be undone?
7
+ * Can the current image be saved?
8
8
  */
9
- export let can_undo = false;
9
+ export let can_save = false;
10
10
  /**
11
- * Can the current image be redone?
11
+ * Is the current image changeable?
12
12
  */
13
- export let can_redo = false;
14
-
15
- export let can_save = false;
16
13
  export let changeable = false;
14
+ /**
15
+ * The current zoom level.
16
+ */
17
+ export let current_zoom = 1;
18
+ /**
19
+ * The current tool.
20
+ */
21
+ export let tool: string;
22
+ /**
23
+ * Is the current zoom level minimum?
24
+ */
25
+ export let min_zoom = true;
17
26
 
18
27
  const dispatch = createEventDispatcher<{
19
28
  /**
@@ -32,10 +41,105 @@
32
41
  * Save the current image.
33
42
  */
34
43
  save: void;
44
+ /**
45
+ * Zoom in.
46
+ */
47
+ zoom_in: void;
48
+ /**
49
+ * Zoom out.
50
+ */
51
+ zoom_out: void;
52
+ /**
53
+ * Set the zoom level.
54
+ */
55
+ set_zoom: number | "fit";
56
+ /**
57
+ * Pan the image.
58
+ */
59
+ pan: void;
35
60
  }>();
61
+
62
+ let show_zoom_popup = false;
63
+
64
+ function handle_zoom_click(e: MouseEvent): void {
65
+ e.stopPropagation();
66
+ show_zoom_popup = !show_zoom_popup;
67
+ }
68
+
69
+ function handle_zoom_change(zoom: number | "fit"): void {
70
+ dispatch("set_zoom", zoom);
71
+ show_zoom_popup = false;
72
+ }
73
+
74
+ function handle_zoom_keydown(e: KeyboardEvent): void {
75
+ if (e.key === "Enter") {
76
+ handle_zoom_change(current_zoom);
77
+ }
78
+ }
79
+
80
+ $: formatted_zoom = Math.round(current_zoom * 100);
36
81
  </script>
37
82
 
38
83
  <IconButtonWrapper>
84
+ <IconButton
85
+ Icon={Pan}
86
+ label="Pan"
87
+ on:click={(e) => {
88
+ e.stopPropagation();
89
+ dispatch("pan");
90
+ }}
91
+ highlight={tool === "pan"}
92
+ size="small"
93
+ padded={false}
94
+ transparent={true}
95
+ disabled={min_zoom}
96
+ />
97
+
98
+ <IconButton
99
+ Icon={ZoomOut}
100
+ label="Zoom out"
101
+ on:click={(event) => {
102
+ dispatch("zoom_out");
103
+ event.stopPropagation();
104
+ }}
105
+ />
106
+ <IconButton
107
+ Icon={ZoomIn}
108
+ label="Zoom in"
109
+ on:click={(event) => {
110
+ dispatch("zoom_in");
111
+ event.stopPropagation();
112
+ }}
113
+ />
114
+
115
+ <div class="zoom-number">
116
+ <span
117
+ role="button"
118
+ tabindex="0"
119
+ on:click={handle_zoom_click}
120
+ on:keydown={handle_zoom_keydown}>{formatted_zoom}%</span
121
+ >
122
+ {#if show_zoom_popup}
123
+ <div class="zoom-controls">
124
+ <ul>
125
+ <li>
126
+ <button on:click|stopPropagation={() => handle_zoom_change("fit")}>
127
+ Fit to screen
128
+ </button>
129
+ </li>
130
+ {#each [0.25, 0.5, 1, 2, 4] as zoom}
131
+ <li>
132
+ <button on:click|stopPropagation={() => handle_zoom_change(zoom)}>
133
+ {zoom * 100}%
134
+ </button>
135
+ </li>
136
+ {/each}
137
+ </ul>
138
+ </div>
139
+ {/if}
140
+ </div>
141
+ <div class="separator"></div>
142
+
39
143
  {#if changeable}
40
144
  <IconButton
41
145
  disabled={!can_save}
@@ -45,28 +149,10 @@
45
149
  dispatch("save");
46
150
  event.stopPropagation();
47
151
  }}
48
- background={"var(--color-green-500)"}
49
- color={"#fff"}
152
+ color="var(--color-accent)"
50
153
  />
51
154
  {/if}
52
- <IconButton
53
- disabled={!can_undo}
54
- Icon={Undo}
55
- label="Undo"
56
- on:click={(event) => {
57
- dispatch("undo");
58
- event.stopPropagation();
59
- }}
60
- />
61
- <IconButton
62
- disabled={!can_redo}
63
- Icon={Redo}
64
- label="Redo"
65
- on:click={(event) => {
66
- dispatch("redo");
67
- event.stopPropagation();
68
- }}
69
- />
155
+
70
156
  <IconButton
71
157
  Icon={Trash}
72
158
  label="Clear canvas"
@@ -76,3 +162,84 @@
76
162
  }}
77
163
  />
78
164
  </IconButtonWrapper>
165
+
166
+ <style>
167
+ .separator {
168
+ width: 1px;
169
+ height: 10px;
170
+ background-color: var(--border-color-primary);
171
+ }
172
+
173
+ .zoom-number {
174
+ position: relative;
175
+ width: 30px;
176
+ padding-left: 4px;
177
+ display: flex;
178
+ align-items: center;
179
+ justify-content: center;
180
+ }
181
+
182
+ .zoom-number span {
183
+ cursor: pointer;
184
+ font-size: var(--text-sm);
185
+ transition: color 0.15s ease;
186
+ }
187
+
188
+ .zoom-number span:hover {
189
+ color: var(--color-accent);
190
+ }
191
+
192
+ .zoom-controls {
193
+ position: absolute;
194
+ top: calc(100% + var(--spacing-xxs) + 3px);
195
+ left: -2px;
196
+ background: var(--block-background-fill);
197
+ border: 1px solid var(--color-gray-200);
198
+ border-radius: var(--radius-sm);
199
+ padding: var(--spacing-xxs);
200
+ box-shadow: var(--shadow-drop);
201
+ font-size: 12px;
202
+ z-index: var(--layer-2);
203
+ width: max-content;
204
+ color: var(--block-label-text-color);
205
+ border: 1px solid var(--block-border-color);
206
+ border-top-left-radius: 0;
207
+ border-top-right-radius: 0;
208
+ }
209
+
210
+ .zoom-controls ul {
211
+ list-style: none;
212
+ margin: 0;
213
+ padding: 0;
214
+ }
215
+
216
+ .zoom-controls li {
217
+ display: flex;
218
+ justify-content: space-between;
219
+ align-items: center;
220
+ transition: background-color 0.15s ease;
221
+ border-bottom: 1px solid var(--border-color-primary);
222
+ }
223
+
224
+ .zoom-controls li:last-child {
225
+ border-bottom: none;
226
+ }
227
+
228
+ .zoom-controls li:hover {
229
+ background-color: var(--background-fill-secondary);
230
+ }
231
+
232
+ .zoom-controls button {
233
+ width: 100%;
234
+ text-align: left;
235
+ padding: var(--spacing-sm) var(--spacing-md);
236
+ font-size: var(--text-sm);
237
+ line-height: var(--line-sm);
238
+ transition: all 0.15s ease;
239
+ border-radius: var(--radius-sm);
240
+ }
241
+
242
+ .zoom-controls button:hover {
243
+ color: var(--color-accent);
244
+ }
245
+ </style>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { type ComponentType } from "svelte";
3
+ import type { ColorInput } from "tinycolor2";
3
4
  export let Icon: ComponentType;
4
5
  export let label = "";
5
6
  export let show_label = false;
@@ -9,11 +10,13 @@
9
10
  export let highlight = false;
10
11
  export let disabled = false;
11
12
  export let hasPopup = false;
12
- export let color = "var(--block-label-text-color)";
13
+ export let color: string | ColorInput = "var(--block-label-text-color)";
13
14
  export let transparent = false;
14
15
  export let background = "var(--background-fill-primary)";
15
16
  export let offset = 0;
16
- $: _color = highlight ? "var(--color-accent)" : color;
17
+ export let label_position: "left" | "right" = "left";
18
+ export let roundedness: "quite" | "very" = "quite";
19
+ $: _color = highlight ? "var(--color-accent)" : color.toString();
17
20
  </script>
18
21
 
19
22
  <button
@@ -29,8 +32,11 @@
29
32
  style:color={!disabled && _color ? _color : "var(--block-label-text-color)"}
30
33
  style:--bg-color={!disabled ? background : "auto"}
31
34
  style:margin-left={offset + "px"}
35
+ class="{roundedness}-round"
32
36
  >
33
- {#if show_label}<span>{label}</span>{/if}
37
+ {#if show_label && label_position === "left"}
38
+ <span style="margin-left: 4px;">{label}</span>
39
+ {/if}
34
40
  <div
35
41
  class:small={size === "small"}
36
42
  class:large={size === "large"}
@@ -38,6 +44,9 @@
38
44
  >
39
45
  <Icon />
40
46
  </div>
47
+ {#if show_label && label_position === "right"}
48
+ <span style="margin-right: 4px;">{label}</span>
49
+ {/if}
41
50
  </button>
42
51
 
43
52
  <style>
@@ -48,11 +57,19 @@
48
57
  gap: 1px;
49
58
  z-index: var(--layer-2);
50
59
  /* background: var(--background-fill-primary); */
51
- border-radius: var(--radius-sm);
60
+ /* border-radius: var(--radius-sm); */
52
61
  color: var(--block-label-text-color);
53
62
  border: 1px solid transparent;
54
63
  }
55
64
 
65
+ .quite-round {
66
+ border-radius: var(--radius-sm);
67
+ }
68
+
69
+ .very-round {
70
+ border-radius: var(--radius-md);
71
+ }
72
+
56
73
  button[disabled] {
57
74
  opacity: 0.5;
58
75
  box-shadow: none;