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%;
|
|
@@ -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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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.
|
|
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.
|
|
65
|
+
"vite": "^8.3.5",
|
|
66
66
|
"vitest": "^5.0.5"
|
|
67
67
|
},
|
|
68
68
|
"homepage": "https://www.sveltekit-ui.com",
|