@gradio/imageeditor 0.12.14 → 0.14.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 (131) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/Example.svelte +1 -1
  3. package/IMAGE_EDITOR_OVERVIEW.md +224 -0
  4. package/ImageEditor.stories.svelte +13 -5
  5. package/Index.svelte +46 -45
  6. package/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +65 -103
  7. package/README.md +2 -0
  8. package/dist/Example.svelte.d.ts +1 -1
  9. package/dist/Index.svelte +35 -33
  10. package/dist/Index.svelte.d.ts +25 -16
  11. package/dist/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +54 -88
  12. package/dist/{shared/InteractiveImageEditor.svelte.d.ts → InteractiveImageEditor.svelte.d.ts} +17 -15
  13. package/dist/shared/Anchor.svelte +311 -0
  14. package/dist/shared/Anchor.svelte.d.ts +18 -0
  15. package/dist/shared/Controls.svelte +169 -18
  16. package/dist/shared/Controls.svelte.d.ts +9 -2
  17. package/dist/shared/{tools/IconButton.svelte → IconButton.svelte} +19 -3
  18. package/dist/shared/{tools/IconButton.svelte.d.ts → IconButton.svelte.d.ts} +4 -1
  19. package/dist/shared/ImageEditor.svelte +572 -289
  20. package/dist/shared/ImageEditor.svelte.d.ts +54 -56
  21. package/dist/shared/Layers.svelte +260 -0
  22. package/dist/shared/Layers.svelte.d.ts +37 -0
  23. package/dist/shared/Resize.svelte +218 -0
  24. package/dist/shared/Resize.svelte.d.ts +29 -0
  25. package/dist/shared/SecondaryToolbar.svelte +55 -0
  26. package/dist/shared/SecondaryToolbar.svelte.d.ts +37 -0
  27. package/dist/shared/Toolbar.svelte +337 -0
  28. package/dist/shared/Toolbar.svelte.d.ts +42 -0
  29. package/dist/shared/{tools → brush}/BrushOptions.svelte +68 -64
  30. package/dist/shared/{tools → brush}/BrushOptions.svelte.d.ts +9 -9
  31. package/dist/shared/{tools → brush}/BrushSize.svelte +2 -27
  32. package/dist/shared/{tools → brush}/ColorField.svelte +2 -1
  33. package/dist/shared/{tools → brush}/ColorField.svelte.d.ts +1 -0
  34. package/dist/shared/{tools → brush}/ColorPicker.svelte +81 -9
  35. package/dist/shared/{tools → brush}/ColorPicker.svelte.d.ts +1 -0
  36. package/dist/shared/{tools → brush}/ColorSwatch.svelte +78 -38
  37. package/dist/shared/{tools → brush}/ColorSwatch.svelte.d.ts +6 -2
  38. package/dist/shared/brush/brush-cursor.d.ts +101 -0
  39. package/dist/shared/brush/brush-cursor.js +327 -0
  40. package/dist/shared/brush/brush-textures.d.ts +65 -0
  41. package/dist/shared/brush/brush-textures.js +385 -0
  42. package/dist/shared/brush/brush-utils.d.ts +32 -0
  43. package/dist/shared/brush/brush-utils.js +59 -0
  44. package/dist/shared/brush/brush.d.ts +152 -0
  45. package/dist/shared/brush/brush.js +397 -0
  46. package/dist/shared/brush/types.d.ts +37 -0
  47. package/dist/shared/brush/types.js +1 -0
  48. package/dist/shared/core/editor.d.ts +261 -0
  49. package/dist/shared/core/editor.js +1264 -0
  50. package/dist/shared/crop/crop.d.ts +201 -0
  51. package/dist/shared/crop/crop.js +727 -0
  52. package/dist/shared/image/image.d.ts +89 -0
  53. package/dist/shared/image/image.js +183 -0
  54. package/dist/shared/resize/resize.d.ts +270 -0
  55. package/dist/shared/resize/resize.js +1413 -0
  56. package/dist/shared/types.d.ts +16 -0
  57. package/dist/shared/utils/pixi.d.ts +16 -57
  58. package/dist/shared/utils/pixi.js +15 -113
  59. package/dist/shared/zoom/zoom.d.ts +136 -0
  60. package/dist/shared/zoom/zoom.js +548 -0
  61. package/package.json +8 -7
  62. package/shared/Anchor.svelte +384 -0
  63. package/shared/Controls.svelte +178 -37
  64. package/shared/{tools/IconButton.svelte → IconButton.svelte} +21 -4
  65. package/shared/ImageEditor.svelte +712 -350
  66. package/shared/Layers.svelte +282 -0
  67. package/shared/Resize.svelte +239 -0
  68. package/shared/SecondaryToolbar.svelte +67 -0
  69. package/shared/Toolbar.svelte +386 -0
  70. package/shared/brush/BRUSH_TOOL.md +134 -0
  71. package/shared/brush/BrushOptions.svelte +200 -0
  72. package/shared/{tools → brush}/BrushSize.svelte +2 -30
  73. package/shared/{tools → brush}/ColorField.svelte +3 -1
  74. package/shared/{tools → brush}/ColorPicker.svelte +86 -9
  75. package/shared/{tools → brush}/ColorSwatch.svelte +95 -42
  76. package/shared/brush/brush-cursor.ts +408 -0
  77. package/shared/brush/brush-textures.ts +485 -0
  78. package/shared/brush/brush-utils.ts +76 -0
  79. package/shared/brush/brush.ts +510 -0
  80. package/shared/brush/types.ts +41 -0
  81. package/shared/core/EDITOR.md +182 -0
  82. package/shared/core/editor.ts +1788 -0
  83. package/shared/crop/CROP.md +139 -0
  84. package/shared/crop/crop.ts +1027 -0
  85. package/shared/image/IMAGE.md +135 -0
  86. package/shared/image/image.ts +298 -0
  87. package/shared/resize/resize.ts +1894 -0
  88. package/shared/types.ts +19 -0
  89. package/shared/utils/pixi.ts +19 -246
  90. package/shared/zoom/zoom.ts +729 -0
  91. package/dist/shared/layers/Layers.svelte +0 -231
  92. package/dist/shared/layers/Layers.svelte.d.ts +0 -18
  93. package/dist/shared/layers/utils.d.ts +0 -73
  94. package/dist/shared/layers/utils.js +0 -156
  95. package/dist/shared/tools/Brush.svelte +0 -215
  96. package/dist/shared/tools/Brush.svelte.d.ts +0 -29
  97. package/dist/shared/tools/Crop.svelte +0 -155
  98. package/dist/shared/tools/Crop.svelte.d.ts +0 -16
  99. package/dist/shared/tools/Cropper.svelte +0 -271
  100. package/dist/shared/tools/Cropper.svelte.d.ts +0 -41
  101. package/dist/shared/tools/Handle.svelte +0 -260
  102. package/dist/shared/tools/Handle.svelte.d.ts +0 -29
  103. package/dist/shared/tools/Sources.svelte +0 -257
  104. package/dist/shared/tools/Sources.svelte.d.ts +0 -34
  105. package/dist/shared/tools/Tools.svelte +0 -102
  106. package/dist/shared/tools/Tools.svelte.d.ts +0 -48
  107. package/dist/shared/tools/brush.d.ts +0 -62
  108. package/dist/shared/tools/brush.js +0 -133
  109. package/dist/shared/tools/crop.d.ts +0 -15
  110. package/dist/shared/tools/crop.js +0 -154
  111. package/dist/shared/tools/index.d.ts +0 -5
  112. package/dist/shared/tools/index.js +0 -5
  113. package/dist/shared/tools/sources.d.ts +0 -46
  114. package/dist/shared/tools/sources.js +0 -121
  115. package/dist/shared/tools/types.d.ts +0 -1
  116. package/shared/layers/Layers.svelte +0 -250
  117. package/shared/layers/utils.ts +0 -294
  118. package/shared/tools/Brush.svelte +0 -280
  119. package/shared/tools/BrushOptions.svelte +0 -180
  120. package/shared/tools/Crop.svelte +0 -189
  121. package/shared/tools/Cropper.svelte +0 -346
  122. package/shared/tools/Handle.svelte +0 -289
  123. package/shared/tools/Sources.svelte +0 -283
  124. package/shared/tools/Tools.svelte +0 -162
  125. package/shared/tools/brush.ts +0 -247
  126. package/shared/tools/crop.ts +0 -213
  127. package/shared/tools/index.ts +0 -5
  128. package/shared/tools/sources.ts +0 -195
  129. package/shared/tools/types.ts +0 -1
  130. /package/dist/shared/{tools → brush}/BrushSize.svelte.d.ts +0 -0
  131. /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,72 +1,132 @@
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";
5
-
6
- /**
7
- * Can the current image be undone?
8
- */
9
- export let can_undo = false;
10
- /**
11
- * Can the current image be redone?
12
- */
13
- export let can_redo = false;
4
+ import { Check, Trash, ZoomIn, ZoomOut, Pan, Download } from "@gradio/icons";
14
5
 
15
6
  export let can_save = false;
16
7
  export let changeable = false;
8
+ export let current_zoom = 1;
9
+ export let tool: string;
10
+ export let min_zoom = true;
11
+ export let enable_download = false;
17
12
 
18
13
  const dispatch = createEventDispatcher<{
19
- /**
20
- * Remove the current image.
21
- */
22
14
  remove_image: void;
23
- /**
24
- * Undo the last action.
25
- */
26
15
  undo: void;
27
- /**
28
- * Redo the last action.
29
- */
30
16
  redo: void;
31
- /**
32
- * Save the current image.
33
- */
34
17
  save: void;
18
+ zoom_in: void;
19
+ zoom_out: void;
20
+ set_zoom: number | "fit";
21
+ pan: void;
22
+ download: void;
35
23
  }>();
24
+
25
+ let show_zoom_popup = false;
26
+
27
+ function handle_zoom_click(e: MouseEvent): void {
28
+ e.stopPropagation();
29
+ show_zoom_popup = !show_zoom_popup;
30
+ }
31
+
32
+ function handle_zoom_change(zoom: number | "fit"): void {
33
+ dispatch("set_zoom", zoom);
34
+ show_zoom_popup = false;
35
+ }
36
+
37
+ function handle_zoom_keydown(e: KeyboardEvent): void {
38
+ if (e.key === "Enter") {
39
+ handle_zoom_change(current_zoom);
40
+ }
41
+ }
42
+
43
+ $: formatted_zoom = Math.round(current_zoom * 100);
36
44
  </script>
37
45
 
38
46
  <IconButtonWrapper>
39
- {#if changeable}
47
+ {#if enable_download}
40
48
  <IconButton
41
- disabled={!can_save}
42
- Icon={Check}
43
- label="Save changes"
49
+ Icon={Download}
50
+ label="Download"
44
51
  on:click={(event) => {
45
- dispatch("save");
52
+ dispatch("download");
46
53
  event.stopPropagation();
47
54
  }}
48
- background={"var(--color-green-500)"}
49
- color={"#fff"}
50
55
  />
51
56
  {/if}
57
+
58
+ <IconButton
59
+ Icon={Pan}
60
+ label="Pan"
61
+ on:click={(e) => {
62
+ e.stopPropagation();
63
+ dispatch("pan");
64
+ }}
65
+ highlight={tool === "pan"}
66
+ size="small"
67
+ padded={false}
68
+ transparent={true}
69
+ disabled={min_zoom}
70
+ />
71
+
52
72
  <IconButton
53
- disabled={!can_undo}
54
- Icon={Undo}
55
- label="Undo"
73
+ Icon={ZoomOut}
74
+ label="Zoom out"
56
75
  on:click={(event) => {
57
- dispatch("undo");
76
+ dispatch("zoom_out");
58
77
  event.stopPropagation();
59
78
  }}
60
79
  />
61
80
  <IconButton
62
- disabled={!can_redo}
63
- Icon={Redo}
64
- label="Redo"
81
+ Icon={ZoomIn}
82
+ label="Zoom in"
65
83
  on:click={(event) => {
66
- dispatch("redo");
84
+ dispatch("zoom_in");
67
85
  event.stopPropagation();
68
86
  }}
69
87
  />
88
+
89
+ <div class="zoom-number">
90
+ <span
91
+ role="button"
92
+ tabindex="0"
93
+ on:click={handle_zoom_click}
94
+ on:keydown={handle_zoom_keydown}>{formatted_zoom}%</span
95
+ >
96
+ {#if show_zoom_popup}
97
+ <div class="zoom-controls">
98
+ <ul>
99
+ <li>
100
+ <button on:click|stopPropagation={() => handle_zoom_change("fit")}>
101
+ Fit to screen
102
+ </button>
103
+ </li>
104
+ {#each [0.25, 0.5, 1, 2, 4] as zoom}
105
+ <li>
106
+ <button on:click|stopPropagation={() => handle_zoom_change(zoom)}>
107
+ {zoom * 100}%
108
+ </button>
109
+ </li>
110
+ {/each}
111
+ </ul>
112
+ </div>
113
+ {/if}
114
+ </div>
115
+ <div class="separator"></div>
116
+
117
+ {#if changeable}
118
+ <IconButton
119
+ disabled={!can_save}
120
+ Icon={Check}
121
+ label="Save changes"
122
+ on:click={(event) => {
123
+ dispatch("save");
124
+ event.stopPropagation();
125
+ }}
126
+ color="var(--color-accent)"
127
+ />
128
+ {/if}
129
+
70
130
  <IconButton
71
131
  Icon={Trash}
72
132
  label="Clear canvas"
@@ -76,3 +136,84 @@
76
136
  }}
77
137
  />
78
138
  </IconButtonWrapper>
139
+
140
+ <style>
141
+ .separator {
142
+ width: 1px;
143
+ height: 10px;
144
+ background-color: var(--border-color-primary);
145
+ }
146
+
147
+ .zoom-number {
148
+ position: relative;
149
+ width: 30px;
150
+ padding-left: 4px;
151
+ display: flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ }
155
+
156
+ .zoom-number span {
157
+ cursor: pointer;
158
+ font-size: var(--text-sm);
159
+ transition: color 0.15s ease;
160
+ }
161
+
162
+ .zoom-number span:hover {
163
+ color: var(--color-accent);
164
+ }
165
+
166
+ .zoom-controls {
167
+ position: absolute;
168
+ top: calc(100% + var(--spacing-xxs) + 3px);
169
+ left: -2px;
170
+ background: var(--block-background-fill);
171
+ border: 1px solid var(--color-gray-200);
172
+ border-radius: var(--radius-sm);
173
+ padding: var(--spacing-xxs);
174
+ box-shadow: var(--shadow-drop);
175
+ font-size: 12px;
176
+ z-index: var(--layer-2);
177
+ width: max-content;
178
+ color: var(--block-label-text-color);
179
+ border: 1px solid var(--block-border-color);
180
+ border-top-left-radius: 0;
181
+ border-top-right-radius: 0;
182
+ }
183
+
184
+ .zoom-controls ul {
185
+ list-style: none;
186
+ margin: 0;
187
+ padding: 0;
188
+ }
189
+
190
+ .zoom-controls li {
191
+ display: flex;
192
+ justify-content: space-between;
193
+ align-items: center;
194
+ transition: background-color 0.15s ease;
195
+ border-bottom: 1px solid var(--border-color-primary);
196
+ }
197
+
198
+ .zoom-controls li:last-child {
199
+ border-bottom: none;
200
+ }
201
+
202
+ .zoom-controls li:hover {
203
+ background-color: var(--background-fill-secondary);
204
+ }
205
+
206
+ .zoom-controls button {
207
+ width: 100%;
208
+ text-align: left;
209
+ padding: var(--spacing-sm) var(--spacing-md);
210
+ font-size: var(--text-sm);
211
+ line-height: var(--line-sm);
212
+ transition: all 0.15s ease;
213
+ border-radius: var(--radius-sm);
214
+ }
215
+
216
+ .zoom-controls button:hover {
217
+ color: var(--color-accent);
218
+ }
219
+ </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;