sveltekit-ui 1.1.89 → 1.1.90

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.90 - 2026-10-21
4
+
5
+ ### Image editor
6
+
7
+ - Add clockwise and counterclockwise 90-degree rotation controls to Crop, with full-resolution export and the existing undo/redo history.
8
+ - Export owned image bytes locally without fetching data/object URLs; preserve the source and display export failures.
9
+ - Expose the async `rotate(degrees)` manager method, guard concurrent/disabled edits and show transformation failures without replacing the source.
10
+
3
11
  ## 1.1.89 - 2026-10-16
4
12
 
5
13
  ### Component fixes
@@ -7,6 +7,10 @@
7
7
  </script>
8
8
 
9
9
  <div class="container">
10
+ <div class="rotation_controls" aria-label="Image rotation">
11
+ <Button manager={manager?.rotate_left_button_manager} />
12
+ <Button manager={manager?.rotate_right_button_manager} />
13
+ </div>
10
14
  <div class="current_container">
11
15
  <p class="cur_spacer">Width: {manager?.current_image_width ?? "-"}px</p>
12
16
  <p class="cur_spacer">Height: {manager?.current_image_height ?? "-"}px</p>
@@ -48,6 +52,7 @@
48
52
  </div>
49
53
 
50
54
  <style>
55
+ .rotation_controls { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem; }
51
56
  .container {
52
57
  padding: 0 1rem;
53
58
  max-width: 100%;
@@ -56,6 +56,7 @@
56
56
  <p>Tab not found</p>
57
57
  {/if}
58
58
  </div>
59
+ {#if manager?.error_message}<p role="alert">{manager.error_message}</p>{/if}
59
60
  <Image {manager} />
60
61
  </div>
61
62
  {/snippet}
@@ -23,6 +23,7 @@ export function create_image_editor_manager(config) {
23
23
  let image_sources = $state([])
24
24
  let image_source_index = $state(null)
25
25
  let file_picker_file = $state(null)
26
+ let original_file_source = null
26
27
  let file_name = $state(null)
27
28
  let mime_type = $state(null)
28
29
  let mime_type_input = $state(null)
@@ -51,8 +52,10 @@ export function create_image_editor_manager(config) {
51
52
  let start_y = $state(null)
52
53
  let last_known_x = $state(null)
53
54
  let last_known_y = $state(null)
55
+ let is_rotating = $state(false)
56
+ let is_finishing = $state(false)
54
57
  let is_read_only = $derived(set_closurable(config?.is_read_only, false))
55
- let is_disabled = $derived(set_closurable(config?.is_disabled, false) || is_read_only)
58
+ let is_disabled = $derived(set_closurable(config?.is_disabled, false) || is_read_only || is_rotating || is_finishing)
56
59
  let is_interaction_disabled = $derived(is_disabled)
57
60
  let ai_error_message = $state(null)
58
61
  let error_message = $state(null)
@@ -89,6 +92,8 @@ export function create_image_editor_manager(config) {
89
92
  let filters_set_default_button_manager = $state(null)
90
93
  let filters_apply_filters_button_manager = $state(null)
91
94
 
95
+ let rotate_left_button_manager = $state(null)
96
+ let rotate_right_button_manager = $state(null)
92
97
  let crop_free_button_manager = $state(null)
93
98
  let crop_one_to_one_button_manager = $state(null)
94
99
  let crop_four_to_three_button_manager = $state(null)
@@ -390,6 +395,52 @@ export function create_image_editor_manager(config) {
390
395
  }
391
396
  }
392
397
 
398
+ // Rotation commits a new source just like crop/resize, so exports and the
399
+ // existing undo/redo stack always refer to the same edited pixels.
400
+ async function rotate(degrees = 90) {
401
+ if (is_disabled || is_destroyed || !selected_image_source || typeof document === "undefined" || typeof Image === "undefined") return false
402
+ if (!Number.isFinite(degrees) || degrees % 90 !== 0) {
403
+ error_message = "Use a rotation in 90-degree steps."
404
+ return false
405
+ }
406
+ const angle = ((degrees % 360) + 360) % 360
407
+ if (!angle) return false
408
+ const source = selected_image_source
409
+ is_rotating = true
410
+ error_message = null
411
+ try {
412
+ const image = new Image()
413
+ image.crossOrigin = "Anonymous"
414
+ await new Promise((resolve, reject) => {
415
+ image.onload = resolve
416
+ image.onerror = () => reject(new Error("The image could not be rotated. Try again."))
417
+ image.src = source
418
+ })
419
+ if (is_destroyed || source !== selected_image_source || is_read_only || set_closurable(config?.is_disabled, false)) return false
420
+ const width = image.naturalWidth, height = image.naturalHeight
421
+ if (!width || !height) throw new Error("The image could not be rotated. Try again.")
422
+ const canvas = document.createElement("canvas")
423
+ const swap = angle === 90 || angle === 270
424
+ canvas.width = swap ? height : width
425
+ canvas.height = swap ? width : height
426
+ const context = canvas.getContext("2d")
427
+ if (!context) throw new Error("The image could not be rotated. Try again.")
428
+ context.translate(canvas.width / 2, canvas.height / 2)
429
+ context.rotate(angle * Math.PI / 180)
430
+ context.drawImage(image, -width / 2, -height / 2)
431
+ const next = canvas.toDataURL(mime_type ?? "image/png", (file_quality_slider_manager?.val ?? 100) / 100)
432
+ image_sources = [next, ...image_sources.slice(image_source_index)]
433
+ image_source_index = 0
434
+ set_base_image(next)
435
+ return true
436
+ } catch (error) {
437
+ if (!is_destroyed && source === selected_image_source) error_message = error?.message ?? "The image could not be rotated. Try again."
438
+ return false
439
+ } finally {
440
+ if (!is_destroyed) is_rotating = false
441
+ }
442
+ }
443
+
393
444
  function undo() {
394
445
  if (is_disabled || is_destroyed) return
395
446
  if (Array.isArray(image_sources) && image_sources.length > 1 && image_source_index < image_sources.length - 1) {
@@ -411,6 +462,7 @@ export function create_image_editor_manager(config) {
411
462
  image_sources = []
412
463
  image_source_index = 0
413
464
  file_picker_file = null
465
+ original_file_source = null
414
466
  file_name_text_input_manager.set_val(null)
415
467
  file_size_data = null
416
468
  file_size_kb = null
@@ -418,17 +470,34 @@ export function create_image_editor_manager(config) {
418
470
  }
419
471
 
420
472
  async function finish() {
421
- if (is_disabled || is_destroyed) return
473
+ if (is_disabled || is_destroyed) return false
422
474
  const image_source = selected_image_source
423
- let file_to_return = null
424
- if (image_source) {
425
- file_to_return = await url_to_file(image_source, file_name_derived)
426
- }
427
- if (is_destroyed || image_source !== selected_image_source) return
428
- if (typeof config?.on_finish == "function") {
429
- config?.on_finish(file_to_return)
475
+ if (!image_source) return false
476
+ is_finishing = true
477
+ error_message = null
478
+ try {
479
+ let file_to_return
480
+ const data = /^data:(image\/(?:png|jpeg|webp));base64,([A-Za-z0-9+/]*={0,2})$/u.exec(image_source)
481
+ if (data) {
482
+ // Canvas exports already contain the bytes. Decoding locally avoids a
483
+ // network/CSP fetch for an in-memory image and keeps export offline.
484
+ const bytes = Uint8Array.from(atob(data[2]), character => character.charCodeAt(0))
485
+ file_to_return = new File([bytes], file_name_derived, { type: data[1] })
486
+ } else if (image_source === original_file_source && file_picker_file) {
487
+ file_to_return = new File([file_picker_file], file_name_derived, { type: file_picker_file.type })
488
+ } else {
489
+ file_to_return = await url_to_file(image_source, file_name_derived)
490
+ }
491
+ if (is_destroyed || image_source !== selected_image_source || is_read_only || set_closurable(config?.is_disabled, false)) return false
492
+ if (typeof config?.on_finish === "function" && await config.on_finish(file_to_return) === false) return false
493
+ popover_manager?.close()
494
+ return true
495
+ } catch (error) {
496
+ if (!is_destroyed && image_source === selected_image_source) error_message = error?.message ?? "The edited image could not be exported. Try again."
497
+ return false
498
+ } finally {
499
+ if (!is_destroyed) is_finishing = false
430
500
  }
431
- popover_manager?.close()
432
501
  }
433
502
 
434
503
  function drop_handler(e) {
@@ -1186,6 +1255,7 @@ export function create_image_editor_manager(config) {
1186
1255
  image_sources = []
1187
1256
  image_source_index = null
1188
1257
  file_picker_file = null
1258
+ original_file_source = null
1189
1259
  file_name = null
1190
1260
  file_name_text_input_manager.set_val(null)
1191
1261
  mime_type = null
@@ -1197,6 +1267,7 @@ export function create_image_editor_manager(config) {
1197
1267
  image_sources = [src_url]
1198
1268
  image_source_index = 0
1199
1269
  file_picker_file = file
1270
+ original_file_source = src_url
1200
1271
  let clean_file_name = String(file_input?.file_name ?? file?.name ?? "").replace(/\..+$/, "")
1201
1272
  file_name = clean_file_name
1202
1273
  file_name_text_input_manager.set_val(clean_file_name)
@@ -1282,6 +1353,18 @@ export function create_image_editor_manager(config) {
1282
1353
  selected_type: () => (screen_tab == "file" ? "selected" : null),
1283
1354
  on_click: () => set_screen_tab("file"),
1284
1355
  })
1356
+ rotate_left_button_manager = create_button_manager({
1357
+ text: "Rotate left 90°", type: "outlined", is_compressed: true,
1358
+ support_icon: "undo_arrow", html_type: "button",
1359
+ is_disabled: () => is_disabled || !selected_image_source,
1360
+ on_click: () => rotate(-90),
1361
+ })
1362
+ rotate_right_button_manager = create_button_manager({
1363
+ text: "Rotate right 90°", type: "outlined", is_compressed: true,
1364
+ support_icon: "redo_arrow", html_type: "button",
1365
+ is_disabled: () => is_disabled || !selected_image_source,
1366
+ on_click: () => rotate(90),
1367
+ })
1285
1368
  undo_button_manager = create_button_manager({
1286
1369
  type: "soft",
1287
1370
  is_no_wrap: true,
@@ -1893,6 +1976,9 @@ export function create_image_editor_manager(config) {
1893
1976
  get screen_tab_file_button_manager() {
1894
1977
  return screen_tab_file_button_manager
1895
1978
  },
1979
+ get rotate_left_button_manager() { return rotate_left_button_manager },
1980
+ get rotate_right_button_manager() { return rotate_right_button_manager },
1981
+ get is_rotating() { return is_rotating },
1896
1982
  get undo_button_manager() {
1897
1983
  return undo_button_manager
1898
1984
  },
@@ -2062,6 +2148,7 @@ export function create_image_editor_manager(config) {
2062
2148
  init_file,
2063
2149
  on_destroy,
2064
2150
  set_screen_tab,
2151
+ rotate,
2065
2152
  undo,
2066
2153
  redo,
2067
2154
  clear,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sveltekit-ui",
3
- "version": "1.1.89",
3
+ "version": "1.1.90",
4
4
  "description": "A SvelteKit UI component library for building modern web applications",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -62,7 +62,7 @@
62
62
  "svelte": "^5.57.2",
63
63
  "svelte-check": "^4.7.7",
64
64
  "typescript": "^6.0.3",
65
- "vite": "^8.3.4",
65
+ "vite": "^8.3.5",
66
66
  "vitest": "^5.0.5"
67
67
  },
68
68
  "homepage": "https://www.sveltekit-ui.com",