@nextcloud/image-editor 1.0.0-beta.1

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/dist/index.mjs ADDED
@@ -0,0 +1,4683 @@
1
+ import './assets/index.css';
2
+ import { shallowRef, computed, defineComponent, openBlock, createElementBlock, normalizeClass, renderSlot, mergeProps, createElementVNode, toDisplayString, createCommentVNode, createTextVNode, watch, provide, inject, Fragment, renderList, createVNode, withCtx, createBlock, resolveDynamicComponent, unref, normalizeStyle, ref, useTemplateRef, onMounted, withDirectives, withKeys, withModifiers, vModelText, onBeforeUnmount } from "vue";
3
+ import Konva from "konva";
4
+ import NcButton from "@nextcloud/vue/components/NcButton";
5
+ import NcLoadingIcon from "@nextcloud/vue/components/NcLoadingIcon";
6
+ import { getGettextBuilder } from "@nextcloud/l10n/gettext";
7
+ import { emojiSearch } from "@nextcloud/vue/functions/emoji";
8
+ import NcEmojiPicker from "@nextcloud/vue/components/NcEmojiPicker";
9
+ import { showConfirmation } from "@nextcloud/dialogs";
10
+ import NcActionButton from "@nextcloud/vue/components/NcActionButton";
11
+ import NcActions from "@nextcloud/vue/components/NcActions";
12
+ function useHistory(capacity = 100) {
13
+ if (!Number.isInteger(capacity) || capacity < 1) {
14
+ throw new RangeError("History capacity must be a positive integer");
15
+ }
16
+ const entries = shallowRef([]);
17
+ const index = shallowRef(-1);
18
+ const canUndo = computed(() => index.value > 0);
19
+ const canRedo = computed(() => index.value < entries.value.length - 1);
20
+ const current = computed(() => entries.value[index.value]?.snapshot);
21
+ function push(snapshot, label) {
22
+ const kept = [...entries.value.slice(0, index.value + 1), { snapshot, label }];
23
+ entries.value = kept.slice(Math.max(0, kept.length - capacity));
24
+ index.value = entries.value.length - 1;
25
+ }
26
+ function jumpTo(target) {
27
+ if (target === index.value || target < 0 || target >= entries.value.length) {
28
+ return void 0;
29
+ }
30
+ index.value = target;
31
+ return current.value;
32
+ }
33
+ function undo() {
34
+ return canUndo.value ? jumpTo(index.value - 1) : void 0;
35
+ }
36
+ function redo() {
37
+ return canRedo.value ? jumpTo(index.value + 1) : void 0;
38
+ }
39
+ function clear() {
40
+ entries.value = [];
41
+ index.value = -1;
42
+ }
43
+ return {
44
+ canUndo,
45
+ canRedo,
46
+ current,
47
+ entries: computed(() => entries.value),
48
+ index: computed(() => index.value),
49
+ push,
50
+ undo,
51
+ redo,
52
+ jumpTo,
53
+ clear
54
+ };
55
+ }
56
+ function newId() {
57
+ if (typeof crypto.randomUUID === "function") {
58
+ return crypto.randomUUID();
59
+ }
60
+ return [...crypto.getRandomValues(new Uint8Array(16))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
61
+ }
62
+ function createInitialState() {
63
+ return {
64
+ rotation: 0,
65
+ fineRotation: 0,
66
+ zoom: 1,
67
+ flipX: false,
68
+ flipY: false,
69
+ crop: null,
70
+ adjustments: { brightness: 0, contrast: 0, saturation: 0 },
71
+ preset: "none",
72
+ annotations: []
73
+ };
74
+ }
75
+ function isPristine(state) {
76
+ const { adjustments } = state;
77
+ return state.rotation === 0 && state.fineRotation === 0 && state.zoom === 1 && !state.flipX && !state.flipY && state.crop === null && adjustments.brightness === 0 && adjustments.contrast === 0 && adjustments.saturation === 0 && state.preset === "none" && state.annotations.length === 0;
78
+ }
79
+ function orientedSize(natural, rotation) {
80
+ return rotation % 180 === 0 ? { width: natural.width, height: natural.height } : { width: natural.height, height: natural.width };
81
+ }
82
+ function clampRect(rect, bounds) {
83
+ const x = Math.min(Math.max(rect.x, 0), bounds.width - 1);
84
+ const y = Math.min(Math.max(rect.y, 0), bounds.height - 1);
85
+ return {
86
+ x,
87
+ y,
88
+ width: Math.max(1, Math.min(rect.width, bounds.width - x)),
89
+ height: Math.max(1, Math.min(rect.height, bounds.height - y))
90
+ };
91
+ }
92
+ function mapPointsCW(points, height) {
93
+ const mapped = [];
94
+ for (let i = 0; i < points.length; i += 2) {
95
+ mapped.push(height - (points[i + 1] ?? 0), points[i] ?? 0);
96
+ }
97
+ return mapped;
98
+ }
99
+ function mapRectCW(rect, height) {
100
+ return {
101
+ x: height - rect.y - rect.height,
102
+ y: rect.x,
103
+ width: rect.height,
104
+ height: rect.width
105
+ };
106
+ }
107
+ function mapAnnotationCW(annotation, height) {
108
+ switch (annotation.type) {
109
+ case "draw":
110
+ return { ...annotation, points: mapPointsCW(annotation.points, height) };
111
+ case "arrow":
112
+ return { ...annotation, points: mapPointsCW(annotation.points, height) };
113
+ case "rectangle":
114
+ case "ellipse":
115
+ return {
116
+ ...annotation,
117
+ rect: { ...annotation.rect, x: height - annotation.rect.y, y: annotation.rect.x },
118
+ rotation: (annotation.rotation + 90) % 360
119
+ };
120
+ case "redact":
121
+ return { ...annotation, rect: mapRectCW(annotation.rect, height) };
122
+ case "text":
123
+ case "sticker":
124
+ return {
125
+ ...annotation,
126
+ x: height - annotation.y,
127
+ y: annotation.x,
128
+ rotation: (annotation.rotation + 90) % 360
129
+ };
130
+ }
131
+ }
132
+ function rotateCW(state, oriented) {
133
+ return {
134
+ ...state,
135
+ rotation: (state.rotation + 90) % 360,
136
+ crop: state.crop && mapRectCW(state.crop, oriented.height),
137
+ annotations: state.annotations.map((annotation) => mapAnnotationCW(annotation, oriented.height))
138
+ };
139
+ }
140
+ function mapAnnotationFlipX(annotation, width) {
141
+ switch (annotation.type) {
142
+ case "draw":
143
+ case "arrow": {
144
+ const points = annotation.points.map((value, i) => i % 2 === 0 ? width - value : value);
145
+ return { ...annotation, points };
146
+ }
147
+ case "rectangle":
148
+ case "ellipse": {
149
+ const radians = annotation.rotation * Math.PI / 180;
150
+ return {
151
+ ...annotation,
152
+ rect: {
153
+ ...annotation.rect,
154
+ x: width - annotation.rect.x - annotation.rect.width * Math.cos(radians),
155
+ y: annotation.rect.y + annotation.rect.width * Math.sin(radians)
156
+ },
157
+ rotation: (360 - annotation.rotation) % 360
158
+ };
159
+ }
160
+ case "redact":
161
+ return {
162
+ ...annotation,
163
+ rect: { ...annotation.rect, x: width - annotation.rect.x - annotation.rect.width }
164
+ };
165
+ case "text":
166
+ case "sticker":
167
+ return {
168
+ ...annotation,
169
+ x: width - annotation.x,
170
+ rotation: (360 - annotation.rotation) % 360
171
+ };
172
+ }
173
+ }
174
+ function flipHorizontal(state, oriented) {
175
+ const sideways = state.rotation % 180 !== 0;
176
+ return {
177
+ ...state,
178
+ flipX: sideways ? state.flipX : !state.flipX,
179
+ flipY: sideways ? !state.flipY : state.flipY,
180
+ fineRotation: -state.fineRotation,
181
+ crop: state.crop && { ...state.crop, x: oriented.width - state.crop.x - state.crop.width },
182
+ annotations: state.annotations.map((annotation) => mapAnnotationFlipX(annotation, oriented.width))
183
+ };
184
+ }
185
+ function mapAnnotationFlipY(annotation, height) {
186
+ switch (annotation.type) {
187
+ case "draw":
188
+ case "arrow": {
189
+ const points = annotation.points.map((value, i) => i % 2 === 1 ? height - value : value);
190
+ return { ...annotation, points };
191
+ }
192
+ case "rectangle":
193
+ case "ellipse": {
194
+ const radians = annotation.rotation * Math.PI / 180;
195
+ return {
196
+ ...annotation,
197
+ rect: {
198
+ ...annotation.rect,
199
+ x: annotation.rect.x - annotation.rect.height * Math.sin(radians),
200
+ y: height - annotation.rect.y - annotation.rect.height * Math.cos(radians)
201
+ },
202
+ rotation: (360 - annotation.rotation) % 360
203
+ };
204
+ }
205
+ case "redact":
206
+ return {
207
+ ...annotation,
208
+ rect: { ...annotation.rect, y: height - annotation.rect.y - annotation.rect.height }
209
+ };
210
+ case "text":
211
+ case "sticker":
212
+ return {
213
+ ...annotation,
214
+ y: height - annotation.y,
215
+ rotation: (360 - annotation.rotation) % 360
216
+ };
217
+ }
218
+ }
219
+ function flipVertical(state, oriented) {
220
+ const sideways = state.rotation % 180 !== 0;
221
+ return {
222
+ ...state,
223
+ flipX: sideways ? !state.flipX : state.flipX,
224
+ flipY: sideways ? state.flipY : !state.flipY,
225
+ fineRotation: -state.fineRotation,
226
+ crop: state.crop && { ...state.crop, y: oriented.height - state.crop.y - state.crop.height },
227
+ annotations: state.annotations.map((annotation) => mapAnnotationFlipY(annotation, oriented.height))
228
+ };
229
+ }
230
+ function translateAnnotation(annotation, dx, dy) {
231
+ switch (annotation.type) {
232
+ case "draw":
233
+ case "arrow": {
234
+ const points = annotation.points.map((value, i) => value + (i % 2 === 0 ? dx : dy));
235
+ return { ...annotation, points };
236
+ }
237
+ case "rectangle":
238
+ case "ellipse":
239
+ case "redact":
240
+ return {
241
+ ...annotation,
242
+ rect: { ...annotation.rect, x: annotation.rect.x + dx, y: annotation.rect.y + dy }
243
+ };
244
+ case "text":
245
+ case "sticker":
246
+ return { ...annotation, x: annotation.x + dx, y: annotation.y + dy };
247
+ }
248
+ }
249
+ function duplicateAnnotation(annotation, offset = 16) {
250
+ return { ...translateAnnotation(annotation, offset, offset), id: newId() };
251
+ }
252
+ const _sfc_main$I = /* @__PURE__ */ defineComponent({
253
+ __name: "GlassSurface",
254
+ props: {
255
+ variant: {}
256
+ },
257
+ setup(__props) {
258
+ return (_ctx, _cache) => {
259
+ return openBlock(), createElementBlock("div", {
260
+ class: normalizeClass(["glass-surface", `glass-surface--${__props.variant ?? "card"}`])
261
+ }, [
262
+ renderSlot(_ctx.$slots, "default", {}, void 0, true)
263
+ ], 2);
264
+ };
265
+ }
266
+ });
267
+ const _export_sfc = (sfc, props) => {
268
+ const target = sfc.__vccOpts || sfc;
269
+ for (const [key, val] of props) {
270
+ target[key] = val;
271
+ }
272
+ return target;
273
+ };
274
+ const GlassSurface = /* @__PURE__ */ _export_sfc(_sfc_main$I, [["__scopeId", "data-v-b60e81d9"]]);
275
+ const _sfc_main$H = {
276
+ name: "ContrastCircleIcon",
277
+ emits: ["click"],
278
+ props: {
279
+ title: {
280
+ type: String
281
+ },
282
+ fillColor: {
283
+ type: String,
284
+ default: "currentColor"
285
+ },
286
+ size: {
287
+ type: Number,
288
+ default: 24
289
+ }
290
+ }
291
+ };
292
+ const _hoisted_1$E = ["aria-hidden", "aria-label"];
293
+ const _hoisted_2$z = ["fill", "width", "height"];
294
+ const _hoisted_3$w = { d: "M12,20C9.79,20 7.79,19.1 6.34,17.66L17.66,6.34C19.1,7.79 20,9.79 20,12A8,8 0 0,1 12,20M6,8H8V6H9.5V8H11.5V9.5H9.5V11.5H8V9.5H6M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,16H17V14.5H12V16Z" };
295
+ const _hoisted_4$v = { key: 0 };
296
+ function _sfc_render$r(_ctx, _cache, $props, $setup, $data, $options) {
297
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
298
+ "aria-hidden": $props.title ? null : "true",
299
+ "aria-label": $props.title,
300
+ class: "material-design-icon contrast-circle-icon",
301
+ role: "img",
302
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
303
+ }), [
304
+ (openBlock(), createElementBlock("svg", {
305
+ fill: $props.fillColor,
306
+ class: "material-design-icon__svg",
307
+ width: $props.size,
308
+ height: $props.size,
309
+ viewBox: "0 0 24 24"
310
+ }, [
311
+ createElementVNode("path", _hoisted_3$w, [
312
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$v, toDisplayString($props.title), 1)) : createCommentVNode("", true)
313
+ ])
314
+ ], 8, _hoisted_2$z))
315
+ ], 16, _hoisted_1$E);
316
+ }
317
+ const ContrastCircle = /* @__PURE__ */ _export_sfc(_sfc_main$H, [["render", _sfc_render$r]]);
318
+ const _sfc_main$G = {
319
+ name: "InvertColorsIcon",
320
+ emits: ["click"],
321
+ props: {
322
+ title: {
323
+ type: String
324
+ },
325
+ fillColor: {
326
+ type: String,
327
+ default: "currentColor"
328
+ },
329
+ size: {
330
+ type: Number,
331
+ default: 24
332
+ }
333
+ }
334
+ };
335
+ const _hoisted_1$D = ["aria-hidden", "aria-label"];
336
+ const _hoisted_2$y = ["fill", "width", "height"];
337
+ const _hoisted_3$v = { d: "M12,19.58V19.58C10.4,19.58 8.89,18.96 7.76,17.83C6.62,16.69 6,15.19 6,13.58C6,12 6.62,10.47 7.76,9.34L12,5.1M17.66,7.93L12,2.27V2.27L6.34,7.93C3.22,11.05 3.22,16.12 6.34,19.24C7.9,20.8 9.95,21.58 12,21.58C14.05,21.58 16.1,20.8 17.66,19.24C20.78,16.12 20.78,11.05 17.66,7.93Z" };
338
+ const _hoisted_4$u = { key: 0 };
339
+ function _sfc_render$q(_ctx, _cache, $props, $setup, $data, $options) {
340
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
341
+ "aria-hidden": $props.title ? null : "true",
342
+ "aria-label": $props.title,
343
+ class: "material-design-icon invert-colors-icon",
344
+ role: "img",
345
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
346
+ }), [
347
+ (openBlock(), createElementBlock("svg", {
348
+ fill: $props.fillColor,
349
+ class: "material-design-icon__svg",
350
+ width: $props.size,
351
+ height: $props.size,
352
+ viewBox: "0 0 24 24"
353
+ }, [
354
+ createElementVNode("path", _hoisted_3$v, [
355
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$u, toDisplayString($props.title), 1)) : createCommentVNode("", true)
356
+ ])
357
+ ], 8, _hoisted_2$y))
358
+ ], 16, _hoisted_1$D);
359
+ }
360
+ const InvertColors = /* @__PURE__ */ _export_sfc(_sfc_main$G, [["render", _sfc_render$q]]);
361
+ const _sfc_main$F = {
362
+ name: "WhiteBalanceSunnyIcon",
363
+ emits: ["click"],
364
+ props: {
365
+ title: {
366
+ type: String
367
+ },
368
+ fillColor: {
369
+ type: String,
370
+ default: "currentColor"
371
+ },
372
+ size: {
373
+ type: Number,
374
+ default: 24
375
+ }
376
+ }
377
+ };
378
+ const _hoisted_1$C = ["aria-hidden", "aria-label"];
379
+ const _hoisted_2$x = ["fill", "width", "height"];
380
+ const _hoisted_3$u = { d: "M3.55 19.09L4.96 20.5L6.76 18.71L5.34 17.29M12 6C8.69 6 6 8.69 6 12S8.69 18 12 18 18 15.31 18 12C18 8.68 15.31 6 12 6M20 13H23V11H20M17.24 18.71L19.04 20.5L20.45 19.09L18.66 17.29M20.45 5L19.04 3.6L17.24 5.39L18.66 6.81M13 1H11V4H13M6.76 5.39L4.96 3.6L3.55 5L5.34 6.81L6.76 5.39M1 13H4V11H1M13 20H11V23H13" };
381
+ const _hoisted_4$t = { key: 0 };
382
+ function _sfc_render$p(_ctx, _cache, $props, $setup, $data, $options) {
383
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
384
+ "aria-hidden": $props.title ? null : "true",
385
+ "aria-label": $props.title,
386
+ class: "material-design-icon white-balance-sunny-icon",
387
+ role: "img",
388
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
389
+ }), [
390
+ (openBlock(), createElementBlock("svg", {
391
+ fill: $props.fillColor,
392
+ class: "material-design-icon__svg",
393
+ width: $props.size,
394
+ height: $props.size,
395
+ viewBox: "0 0 24 24"
396
+ }, [
397
+ createElementVNode("path", _hoisted_3$u, [
398
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$t, toDisplayString($props.title), 1)) : createCommentVNode("", true)
399
+ ])
400
+ ], 8, _hoisted_2$x))
401
+ ], 16, _hoisted_1$C);
402
+ }
403
+ const WhiteBalanceSunny = /* @__PURE__ */ _export_sfc(_sfc_main$F, [["render", _sfc_render$p]]);
404
+ const _hoisted_1$B = { class: "editor-slider" };
405
+ const _hoisted_2$w = ["value", "data-test", "disabled", "aria-label", "min", "max", "step"];
406
+ const _sfc_main$E = /* @__PURE__ */ defineComponent({
407
+ __name: "EditorSlider",
408
+ props: {
409
+ value: {},
410
+ min: {},
411
+ max: {},
412
+ step: {},
413
+ label: {},
414
+ display: {},
415
+ dataTest: {},
416
+ disabled: { type: Boolean }
417
+ },
418
+ emits: ["input", "commit"],
419
+ setup(__props, { emit: __emit }) {
420
+ const props = __props;
421
+ const emit = __emit;
422
+ function onInput(event) {
423
+ emit("input", Number(event.target.value));
424
+ }
425
+ return (_ctx, _cache) => {
426
+ return openBlock(), createElementBlock("div", _hoisted_1$B, [
427
+ createElementVNode("input", {
428
+ value: props.value,
429
+ "data-test": __props.dataTest,
430
+ disabled: __props.disabled,
431
+ "aria-label": __props.label,
432
+ type: "range",
433
+ min: __props.min,
434
+ max: __props.max,
435
+ step: __props.step,
436
+ onInput,
437
+ onChange: _cache[0] || (_cache[0] = ($event) => emit("commit"))
438
+ }, null, 40, _hoisted_2$w),
439
+ createElementVNode("output", null, [
440
+ renderSlot(_ctx.$slots, "preview", {}, () => [
441
+ createTextVNode(toDisplayString(__props.display ?? props.value), 1)
442
+ ], true)
443
+ ])
444
+ ]);
445
+ };
446
+ }
447
+ });
448
+ const EditorSlider = /* @__PURE__ */ _export_sfc(_sfc_main$E, [["__scopeId", "data-v-6562ea60"]]);
449
+ const _hoisted_1$A = ["disabled", "data-test", "aria-pressed"];
450
+ const _sfc_main$D = /* @__PURE__ */ defineComponent({
451
+ __name: "IconTab",
452
+ props: {
453
+ label: {},
454
+ active: { type: Boolean },
455
+ disabled: { type: Boolean },
456
+ dataTest: {}
457
+ },
458
+ emits: ["click"],
459
+ setup(__props, { emit: __emit }) {
460
+ const emit = __emit;
461
+ return (_ctx, _cache) => {
462
+ return openBlock(), createElementBlock("button", {
463
+ type: "button",
464
+ class: normalizeClass(["icon-tab", { "icon-tab--active": __props.active }]),
465
+ disabled: __props.disabled,
466
+ "data-test": __props.dataTest,
467
+ "aria-pressed": __props.active,
468
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("click"))
469
+ }, [
470
+ renderSlot(_ctx.$slots, "default", {}, void 0, true),
471
+ createElementVNode("span", null, toDisplayString(__props.label), 1)
472
+ ], 10, _hoisted_1$A);
473
+ };
474
+ }
475
+ });
476
+ const IconTab = /* @__PURE__ */ _export_sfc(_sfc_main$D, [["__scopeId", "data-v-95889662"]]);
477
+ const gtBuilder = getGettextBuilder().detectLocale();
478
+ [].map((data) => gtBuilder.addTranslation(data.locale, data.json));
479
+ const gt = gtBuilder.build();
480
+ gt.ngettext.bind(gt);
481
+ const t = gt.gettext.bind(gt);
482
+ const VIEW_MARGIN = 16;
483
+ const MIN_ZOOM = 1;
484
+ const MAX_ZOOM = 4;
485
+ const ZOOM_SNAP = 1.05;
486
+ const PINCH_TOLERANCE = 0.01;
487
+ const WHEEL_LINE = 16;
488
+ const WHEEL_CLAMP = 300;
489
+ const WHEEL_SENSITIVITY = 15e-4;
490
+ function panBounds(visible, scale, container) {
491
+ return {
492
+ x: Math.max(0, (visible.width * scale - container.width) / 2 + VIEW_MARGIN),
493
+ y: Math.max(0, (visible.height * scale - container.height) / 2 + VIEW_MARGIN)
494
+ };
495
+ }
496
+ function clampPan(pan, bounds) {
497
+ return {
498
+ x: Math.min(bounds.x, Math.max(-bounds.x, pan.x)),
499
+ y: Math.min(bounds.y, Math.max(-bounds.y, pan.y))
500
+ };
501
+ }
502
+ function clampZoom(zoom) {
503
+ return zoom < ZOOM_SNAP ? MIN_ZOOM : Math.min(MAX_ZOOM, zoom);
504
+ }
505
+ function anchoredPan(pan, cursor, factor) {
506
+ return {
507
+ x: cursor.x - factor * (cursor.x - pan.x),
508
+ y: cursor.y - factor * (cursor.y - pan.y)
509
+ };
510
+ }
511
+ function wheelZoomFactor(deltaY, deltaMode, pageHeight) {
512
+ const scaled = deltaMode === 1 ? deltaY * WHEEL_LINE : deltaMode === 2 ? deltaY * pageHeight : deltaY;
513
+ const bounded = Math.min(WHEEL_CLAMP, Math.max(-WHEEL_CLAMP, scaled));
514
+ return Math.exp(-bounded * WHEEL_SENSITIVITY);
515
+ }
516
+ const MODE_DEFAULT_TOOL = {
517
+ select: "select",
518
+ crop: "crop",
519
+ finetune: "adjust",
520
+ filter: "adjust",
521
+ annotate: "draw",
522
+ sticker: "sticker",
523
+ redact: "redact"
524
+ };
525
+ const EDITOR_CONTEXT = /* @__PURE__ */ Symbol("nextcloud:image-editor");
526
+ function createEditorContext() {
527
+ const history = useHistory();
528
+ const state = shallowRef(createInitialState());
529
+ const viewZoom = shallowRef(MIN_ZOOM);
530
+ const viewPan = shallowRef({ x: 0, y: 0 });
531
+ const viewFit = shallowRef(null);
532
+ const boundsAt = (zoom) => {
533
+ const fit = viewFit.value;
534
+ return fit === null ? { x: 0, y: 0 } : panBounds(fit.visible, fit.scale * zoom, fit.container);
535
+ };
536
+ const activeMode = shallowRef("crop");
537
+ const activeTool = shallowRef(MODE_DEFAULT_TOOL.crop);
538
+ history.push(state.value, t("Original"));
539
+ const context = {
540
+ state,
541
+ activeMode,
542
+ activeTool,
543
+ setMode(mode) {
544
+ activeMode.value = mode;
545
+ activeTool.value = MODE_DEFAULT_TOOL[mode];
546
+ context.selectedId.value = null;
547
+ },
548
+ drawColor: shallowRef("#ff0000"),
549
+ strokeWidth: shallowRef(6),
550
+ fontSize: shallowRef(24),
551
+ sticker: shallowRef("😀"),
552
+ redactStyle: shallowRef("pixelate"),
553
+ cropAspect: shallowRef(null),
554
+ viewZoom,
555
+ viewPan,
556
+ viewFit,
557
+ panning: shallowRef(false),
558
+ setViewZoom(zoom, anchor) {
559
+ const previous = viewZoom.value;
560
+ const next = clampZoom(zoom);
561
+ viewZoom.value = next;
562
+ if (next === MIN_ZOOM) {
563
+ viewPan.value = { x: 0, y: 0 };
564
+ return;
565
+ }
566
+ const panned = anchor === void 0 ? viewPan.value : anchoredPan(viewPan.value, anchor, next / previous);
567
+ viewPan.value = clampPan(panned, boundsAt(next));
568
+ },
569
+ setViewPan(pan) {
570
+ viewPan.value = clampPan(pan, boundsAt(viewZoom.value));
571
+ },
572
+ interacting: shallowRef(false),
573
+ selectedId: shallowRef(null),
574
+ canUndo: history.canUndo,
575
+ canRedo: history.canRedo,
576
+ historyEntries: history.entries,
577
+ historyIndex: history.index,
578
+ commit(next, label) {
579
+ context.interacting.value = false;
580
+ state.value = next;
581
+ history.push(next, label);
582
+ },
583
+ preview(next) {
584
+ context.interacting.value = true;
585
+ state.value = next;
586
+ },
587
+ undo() {
588
+ const snapshot = history.undo();
589
+ if (snapshot !== void 0) {
590
+ state.value = snapshot;
591
+ }
592
+ },
593
+ redo() {
594
+ const snapshot = history.redo();
595
+ if (snapshot !== void 0) {
596
+ state.value = snapshot;
597
+ }
598
+ },
599
+ jumpTo(index) {
600
+ const snapshot = history.jumpTo(index);
601
+ if (snapshot !== void 0) {
602
+ state.value = snapshot;
603
+ }
604
+ },
605
+ reset(next) {
606
+ history.clear();
607
+ state.value = next ?? createInitialState();
608
+ activeMode.value = "crop";
609
+ activeTool.value = MODE_DEFAULT_TOOL.crop;
610
+ context.selectedId.value = null;
611
+ viewZoom.value = MIN_ZOOM;
612
+ viewPan.value = { x: 0, y: 0 };
613
+ context.panning.value = false;
614
+ history.push(state.value, next === void 0 ? t("Original") : t("Restored"));
615
+ }
616
+ };
617
+ watch(activeTool, (tool) => {
618
+ if (tool !== "select") {
619
+ context.selectedId.value = null;
620
+ }
621
+ });
622
+ provide(EDITOR_CONTEXT, context);
623
+ return context;
624
+ }
625
+ function useEditorContext() {
626
+ const context = inject(EDITOR_CONTEXT, null);
627
+ if (context === null) {
628
+ throw new Error("useEditorContext() called outside of an ImageEditor tree");
629
+ }
630
+ return context;
631
+ }
632
+ const _hoisted_1$z = { class: "adjust-panel" };
633
+ const _hoisted_2$v = { class: "adjust-panel__tabs" };
634
+ const _sfc_main$C = /* @__PURE__ */ defineComponent({
635
+ __name: "AdjustPanel",
636
+ props: {
637
+ loaded: { type: Boolean }
638
+ },
639
+ setup(__props) {
640
+ const context = useEditorContext();
641
+ const adjustments = [
642
+ { id: "brightness", label: t("Brightness"), icon: WhiteBalanceSunny },
643
+ { id: "contrast", label: t("Contrast"), icon: ContrastCircle },
644
+ { id: "saturation", label: t("Saturation"), icon: InvertColors }
645
+ ];
646
+ const activeAdjustment = shallowRef("brightness");
647
+ const display = computed(() => {
648
+ const value = context.state.value.adjustments[activeAdjustment.value];
649
+ return value > 0 ? `+${value}` : `${value}`;
650
+ });
651
+ function onAdjustInput(value) {
652
+ const state = context.state.value;
653
+ context.preview({
654
+ ...state,
655
+ adjustments: { ...state.adjustments, [activeAdjustment.value]: value }
656
+ });
657
+ }
658
+ function onSliderCommit() {
659
+ context.commit(context.state.value, adjustments.find((entry) => entry.id === activeAdjustment.value).label);
660
+ }
661
+ return (_ctx, _cache) => {
662
+ return openBlock(), createElementBlock("div", _hoisted_1$z, [
663
+ createElementVNode("div", _hoisted_2$v, [
664
+ (openBlock(), createElementBlock(Fragment, null, renderList(adjustments, (adjustment) => {
665
+ return createVNode(IconTab, {
666
+ key: adjustment.id,
667
+ label: adjustment.label,
668
+ active: activeAdjustment.value === adjustment.id,
669
+ disabled: !__props.loaded,
670
+ "data-test": `tab-${adjustment.id}`,
671
+ onClick: ($event) => activeAdjustment.value = adjustment.id
672
+ }, {
673
+ default: withCtx(() => [
674
+ (openBlock(), createBlock(resolveDynamicComponent(adjustment.icon), { size: 20 }))
675
+ ]),
676
+ _: 2
677
+ }, 1032, ["label", "active", "disabled", "data-test", "onClick"]);
678
+ }), 64))
679
+ ]),
680
+ createVNode(EditorSlider, {
681
+ value: unref(context).state.value.adjustments[activeAdjustment.value],
682
+ min: -100,
683
+ max: 100,
684
+ step: 1,
685
+ label: adjustments.find((entry) => entry.id === activeAdjustment.value).label,
686
+ display: display.value,
687
+ "data-test": `adjust-${activeAdjustment.value}`,
688
+ disabled: !__props.loaded,
689
+ onInput: onAdjustInput,
690
+ onCommit: onSliderCommit
691
+ }, null, 8, ["value", "label", "display", "data-test", "disabled"])
692
+ ]);
693
+ };
694
+ }
695
+ });
696
+ const AdjustPanel = /* @__PURE__ */ _export_sfc(_sfc_main$C, [["__scopeId", "data-v-d7f848cf"]]);
697
+ const _sfc_main$B = {
698
+ name: "ArrowTopRightIcon",
699
+ emits: ["click"],
700
+ props: {
701
+ title: {
702
+ type: String
703
+ },
704
+ fillColor: {
705
+ type: String,
706
+ default: "currentColor"
707
+ },
708
+ size: {
709
+ type: Number,
710
+ default: 24
711
+ }
712
+ }
713
+ };
714
+ const _hoisted_1$y = ["aria-hidden", "aria-label"];
715
+ const _hoisted_2$u = ["fill", "width", "height"];
716
+ const _hoisted_3$t = { d: "M5,17.59L15.59,7H9V5H19V15H17V8.41L6.41,19L5,17.59Z" };
717
+ const _hoisted_4$s = { key: 0 };
718
+ function _sfc_render$o(_ctx, _cache, $props, $setup, $data, $options) {
719
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
720
+ "aria-hidden": $props.title ? null : "true",
721
+ "aria-label": $props.title,
722
+ class: "material-design-icon arrow-top-right-icon",
723
+ role: "img",
724
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
725
+ }), [
726
+ (openBlock(), createElementBlock("svg", {
727
+ fill: $props.fillColor,
728
+ class: "material-design-icon__svg",
729
+ width: $props.size,
730
+ height: $props.size,
731
+ viewBox: "0 0 24 24"
732
+ }, [
733
+ createElementVNode("path", _hoisted_3$t, [
734
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$s, toDisplayString($props.title), 1)) : createCommentVNode("", true)
735
+ ])
736
+ ], 8, _hoisted_2$u))
737
+ ], 16, _hoisted_1$y);
738
+ }
739
+ const ArrowTopRight = /* @__PURE__ */ _export_sfc(_sfc_main$B, [["render", _sfc_render$o]]);
740
+ const _sfc_main$A = {
741
+ name: "EllipseOutlineIcon",
742
+ emits: ["click"],
743
+ props: {
744
+ title: {
745
+ type: String
746
+ },
747
+ fillColor: {
748
+ type: String,
749
+ default: "currentColor"
750
+ },
751
+ size: {
752
+ type: Number,
753
+ default: 24
754
+ }
755
+ }
756
+ };
757
+ const _hoisted_1$x = ["aria-hidden", "aria-label"];
758
+ const _hoisted_2$t = ["fill", "width", "height"];
759
+ const _hoisted_3$s = { d: "M12,6C16.41,6 20,8.69 20,12C20,15.31 16.41,18 12,18C7.59,18 4,15.31 4,12C4,8.69 7.59,6 12,6M12,4C6.5,4 2,7.58 2,12C2,16.42 6.5,20 12,20C17.5,20 22,16.42 22,12C22,7.58 17.5,4 12,4Z" };
760
+ const _hoisted_4$r = { key: 0 };
761
+ function _sfc_render$n(_ctx, _cache, $props, $setup, $data, $options) {
762
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
763
+ "aria-hidden": $props.title ? null : "true",
764
+ "aria-label": $props.title,
765
+ class: "material-design-icon ellipse-outline-icon",
766
+ role: "img",
767
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
768
+ }), [
769
+ (openBlock(), createElementBlock("svg", {
770
+ fill: $props.fillColor,
771
+ class: "material-design-icon__svg",
772
+ width: $props.size,
773
+ height: $props.size,
774
+ viewBox: "0 0 24 24"
775
+ }, [
776
+ createElementVNode("path", _hoisted_3$s, [
777
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$r, toDisplayString($props.title), 1)) : createCommentVNode("", true)
778
+ ])
779
+ ], 8, _hoisted_2$t))
780
+ ], 16, _hoisted_1$x);
781
+ }
782
+ const EllipseOutline = /* @__PURE__ */ _export_sfc(_sfc_main$A, [["render", _sfc_render$n]]);
783
+ const _sfc_main$z = {
784
+ name: "FormatTextIcon",
785
+ emits: ["click"],
786
+ props: {
787
+ title: {
788
+ type: String
789
+ },
790
+ fillColor: {
791
+ type: String,
792
+ default: "currentColor"
793
+ },
794
+ size: {
795
+ type: Number,
796
+ default: 24
797
+ }
798
+ }
799
+ };
800
+ const _hoisted_1$w = ["aria-hidden", "aria-label"];
801
+ const _hoisted_2$s = ["fill", "width", "height"];
802
+ const _hoisted_3$r = { d: "M18.5,4L19.66,8.35L18.7,8.61C18.25,7.74 17.79,6.87 17.26,6.43C16.73,6 16.11,6 15.5,6H13V16.5C13,17 13,17.5 13.33,17.75C13.67,18 14.33,18 15,18V19H9V18C9.67,18 10.33,18 10.67,17.75C11,17.5 11,17 11,16.5V6H8.5C7.89,6 7.27,6 6.74,6.43C6.21,6.87 5.75,7.74 5.3,8.61L4.34,8.35L5.5,4H18.5Z" };
803
+ const _hoisted_4$q = { key: 0 };
804
+ function _sfc_render$m(_ctx, _cache, $props, $setup, $data, $options) {
805
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
806
+ "aria-hidden": $props.title ? null : "true",
807
+ "aria-label": $props.title,
808
+ class: "material-design-icon format-text-icon",
809
+ role: "img",
810
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
811
+ }), [
812
+ (openBlock(), createElementBlock("svg", {
813
+ fill: $props.fillColor,
814
+ class: "material-design-icon__svg",
815
+ width: $props.size,
816
+ height: $props.size,
817
+ viewBox: "0 0 24 24"
818
+ }, [
819
+ createElementVNode("path", _hoisted_3$r, [
820
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$q, toDisplayString($props.title), 1)) : createCommentVNode("", true)
821
+ ])
822
+ ], 8, _hoisted_2$s))
823
+ ], 16, _hoisted_1$w);
824
+ }
825
+ const FormatText = /* @__PURE__ */ _export_sfc(_sfc_main$z, [["render", _sfc_render$m]]);
826
+ const _sfc_main$y = {
827
+ name: "PencilIcon",
828
+ emits: ["click"],
829
+ props: {
830
+ title: {
831
+ type: String
832
+ },
833
+ fillColor: {
834
+ type: String,
835
+ default: "currentColor"
836
+ },
837
+ size: {
838
+ type: Number,
839
+ default: 24
840
+ }
841
+ }
842
+ };
843
+ const _hoisted_1$v = ["aria-hidden", "aria-label"];
844
+ const _hoisted_2$r = ["fill", "width", "height"];
845
+ const _hoisted_3$q = { d: "M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z" };
846
+ const _hoisted_4$p = { key: 0 };
847
+ function _sfc_render$l(_ctx, _cache, $props, $setup, $data, $options) {
848
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
849
+ "aria-hidden": $props.title ? null : "true",
850
+ "aria-label": $props.title,
851
+ class: "material-design-icon pencil-icon",
852
+ role: "img",
853
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
854
+ }), [
855
+ (openBlock(), createElementBlock("svg", {
856
+ fill: $props.fillColor,
857
+ class: "material-design-icon__svg",
858
+ width: $props.size,
859
+ height: $props.size,
860
+ viewBox: "0 0 24 24"
861
+ }, [
862
+ createElementVNode("path", _hoisted_3$q, [
863
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$p, toDisplayString($props.title), 1)) : createCommentVNode("", true)
864
+ ])
865
+ ], 8, _hoisted_2$r))
866
+ ], 16, _hoisted_1$v);
867
+ }
868
+ const Pencil = /* @__PURE__ */ _export_sfc(_sfc_main$y, [["render", _sfc_render$l]]);
869
+ const _sfc_main$x = {
870
+ name: "RectangleOutlineIcon",
871
+ emits: ["click"],
872
+ props: {
873
+ title: {
874
+ type: String
875
+ },
876
+ fillColor: {
877
+ type: String,
878
+ default: "currentColor"
879
+ },
880
+ size: {
881
+ type: Number,
882
+ default: 24
883
+ }
884
+ }
885
+ };
886
+ const _hoisted_1$u = ["aria-hidden", "aria-label"];
887
+ const _hoisted_2$q = ["fill", "width", "height"];
888
+ const _hoisted_3$p = { d: "M4,6V19H20V6H4M18,17H6V8H18V17Z" };
889
+ const _hoisted_4$o = { key: 0 };
890
+ function _sfc_render$k(_ctx, _cache, $props, $setup, $data, $options) {
891
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
892
+ "aria-hidden": $props.title ? null : "true",
893
+ "aria-label": $props.title,
894
+ class: "material-design-icon rectangle-outline-icon",
895
+ role: "img",
896
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
897
+ }), [
898
+ (openBlock(), createElementBlock("svg", {
899
+ fill: $props.fillColor,
900
+ class: "material-design-icon__svg",
901
+ width: $props.size,
902
+ height: $props.size,
903
+ viewBox: "0 0 24 24"
904
+ }, [
905
+ createElementVNode("path", _hoisted_3$p, [
906
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$o, toDisplayString($props.title), 1)) : createCommentVNode("", true)
907
+ ])
908
+ ], 8, _hoisted_2$q))
909
+ ], 16, _hoisted_1$u);
910
+ }
911
+ const RectangleOutline = /* @__PURE__ */ _export_sfc(_sfc_main$x, [["render", _sfc_render$k]]);
912
+ function useAnnotationColor(context) {
913
+ function recolorable() {
914
+ const annotation = context.state.value.annotations.find((entry) => entry.id === context.selectedId.value);
915
+ return annotation !== void 0 && "color" in annotation && annotation.type !== "sticker" ? annotation : void 0;
916
+ }
917
+ function apply(color) {
918
+ const annotation = recolorable();
919
+ if (annotation === void 0 || annotation.color === color) {
920
+ return;
921
+ }
922
+ const state = context.state.value;
923
+ context.preview({
924
+ ...state,
925
+ annotations: state.annotations.map((entry) => entry.id === annotation.id ? { ...entry, color } : entry)
926
+ });
927
+ }
928
+ return {
929
+ preview(color) {
930
+ context.drawColor.value = color;
931
+ apply(color);
932
+ },
933
+ commit(color) {
934
+ context.drawColor.value = color;
935
+ apply(color);
936
+ if (recolorable() !== void 0) {
937
+ context.commit(context.state.value, t("Color"));
938
+ }
939
+ }
940
+ };
941
+ }
942
+ const _hoisted_1$t = { class: "annotate-panel" };
943
+ const _hoisted_2$p = { class: "annotate-panel__row" };
944
+ const _hoisted_3$o = { class: "annotate-panel__option" };
945
+ const _hoisted_4$n = ["value"];
946
+ const FONT_SAMPLE = "A";
947
+ const PREVIEW_CAP = 44;
948
+ const _sfc_main$w = /* @__PURE__ */ defineComponent({
949
+ __name: "AnnotatePanel",
950
+ props: {
951
+ loaded: { type: Boolean }
952
+ },
953
+ setup(__props) {
954
+ const context = useEditorContext();
955
+ const color = useAnnotationColor(context);
956
+ const labels = {
957
+ color: t("Color"),
958
+ strokeWidth: t("Stroke width"),
959
+ fontSize: t("Font size")
960
+ };
961
+ const subTools = [
962
+ { id: "draw", label: t("Draw"), icon: Pencil },
963
+ { id: "rectangle", label: t("Rectangle"), icon: RectangleOutline },
964
+ { id: "ellipse", label: t("Ellipse"), icon: EllipseOutline },
965
+ { id: "arrow", label: t("Arrow"), icon: ArrowTopRight },
966
+ { id: "text", label: t("Text"), icon: FormatText }
967
+ ];
968
+ const showStrokeOptions = computed(() => ["draw", "rectangle", "ellipse", "arrow"].includes(context.activeTool.value));
969
+ const viewScale = computed(() => (context.viewFit.value?.scale ?? 1) * context.viewZoom.value);
970
+ const strokePreview = computed(() => Math.min(PREVIEW_CAP, Math.max(2, context.strokeWidth.value * viewScale.value)));
971
+ const fontPreview = computed(() => Math.min(PREVIEW_CAP, Math.max(8, context.fontSize.value * viewScale.value)));
972
+ return (_ctx, _cache) => {
973
+ return openBlock(), createElementBlock("div", _hoisted_1$t, [
974
+ createElementVNode("div", _hoisted_2$p, [
975
+ (openBlock(), createElementBlock(Fragment, null, renderList(subTools, (tool) => {
976
+ return createVNode(unref(NcButton), {
977
+ key: tool.id,
978
+ "aria-label": tool.label,
979
+ title: tool.label,
980
+ disabled: !__props.loaded,
981
+ pressed: unref(context).activeTool.value === tool.id,
982
+ variant: "tertiary",
983
+ onClick: ($event) => unref(context).activeTool.value = tool.id
984
+ }, {
985
+ icon: withCtx(() => [
986
+ (openBlock(), createBlock(resolveDynamicComponent(tool.icon), { size: 20 }))
987
+ ]),
988
+ _: 2
989
+ }, 1032, ["aria-label", "title", "disabled", "pressed", "onClick"]);
990
+ }), 64)),
991
+ _cache[4] || (_cache[4] = createElementVNode("span", { class: "annotate-panel__divider" }, null, -1)),
992
+ createElementVNode("label", _hoisted_3$o, [
993
+ createTextVNode(toDisplayString(labels.color) + " ", 1),
994
+ createElementVNode("input", {
995
+ value: unref(context).drawColor.value,
996
+ type: "color",
997
+ "data-test": "color",
998
+ onInput: _cache[0] || (_cache[0] = ($event) => unref(color).preview($event.target.value)),
999
+ onChange: _cache[1] || (_cache[1] = ($event) => unref(color).commit($event.target.value))
1000
+ }, null, 40, _hoisted_4$n)
1001
+ ])
1002
+ ]),
1003
+ showStrokeOptions.value ? (openBlock(), createBlock(EditorSlider, {
1004
+ key: 0,
1005
+ value: unref(context).strokeWidth.value,
1006
+ min: 1,
1007
+ max: 32,
1008
+ step: 1,
1009
+ label: labels.strokeWidth,
1010
+ onInput: _cache[2] || (_cache[2] = ($event) => unref(context).strokeWidth.value = $event),
1011
+ onCommit: () => {
1012
+ }
1013
+ }, {
1014
+ preview: withCtx(() => [
1015
+ createElementVNode("span", {
1016
+ class: "annotate-panel__dot",
1017
+ "data-test": "stroke-preview",
1018
+ style: normalizeStyle({
1019
+ inlineSize: `${strokePreview.value}px`,
1020
+ blockSize: `${strokePreview.value}px`,
1021
+ backgroundColor: unref(context).drawColor.value
1022
+ })
1023
+ }, null, 4)
1024
+ ]),
1025
+ _: 1
1026
+ }, 8, ["value", "label"])) : unref(context).activeTool.value === "text" ? (openBlock(), createBlock(EditorSlider, {
1027
+ key: 1,
1028
+ value: unref(context).fontSize.value,
1029
+ min: 8,
1030
+ max: 128,
1031
+ step: 1,
1032
+ label: labels.fontSize,
1033
+ onInput: _cache[3] || (_cache[3] = ($event) => unref(context).fontSize.value = $event),
1034
+ onCommit: () => {
1035
+ }
1036
+ }, {
1037
+ preview: withCtx(() => [
1038
+ createElementVNode("span", {
1039
+ class: "annotate-panel__glyph",
1040
+ "data-test": "font-preview",
1041
+ style: normalizeStyle({
1042
+ fontSize: `${fontPreview.value}px`,
1043
+ color: unref(context).drawColor.value
1044
+ })
1045
+ }, toDisplayString(FONT_SAMPLE), 4)
1046
+ ]),
1047
+ _: 1
1048
+ }, 8, ["value", "label"])) : createCommentVNode("", true)
1049
+ ]);
1050
+ };
1051
+ }
1052
+ });
1053
+ const AnnotatePanel = /* @__PURE__ */ _export_sfc(_sfc_main$w, [["__scopeId", "data-v-1bf135e4"]]);
1054
+ const _sfc_main$v = {
1055
+ name: "FlipHorizontalIcon",
1056
+ emits: ["click"],
1057
+ props: {
1058
+ title: {
1059
+ type: String
1060
+ },
1061
+ fillColor: {
1062
+ type: String,
1063
+ default: "currentColor"
1064
+ },
1065
+ size: {
1066
+ type: Number,
1067
+ default: 24
1068
+ }
1069
+ }
1070
+ };
1071
+ const _hoisted_1$s = ["aria-hidden", "aria-label"];
1072
+ const _hoisted_2$o = ["fill", "width", "height"];
1073
+ const _hoisted_3$n = { d: "M15 21H17V19H15M19 9H21V7H19M3 5V19C3 20.1 3.9 21 5 21H9V19H5V5H9V3H5C3.9 3 3 3.9 3 5M19 3V5H21C21 3.9 20.1 3 19 3M11 23H13V1H11M19 17H21V15H19M15 5H17V3H15M19 13H21V11H19M19 21C20.1 21 21 20.1 21 19H19Z" };
1074
+ const _hoisted_4$m = { key: 0 };
1075
+ function _sfc_render$j(_ctx, _cache, $props, $setup, $data, $options) {
1076
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
1077
+ "aria-hidden": $props.title ? null : "true",
1078
+ "aria-label": $props.title,
1079
+ class: "material-design-icon flip-horizontal-icon",
1080
+ role: "img",
1081
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
1082
+ }), [
1083
+ (openBlock(), createElementBlock("svg", {
1084
+ fill: $props.fillColor,
1085
+ class: "material-design-icon__svg",
1086
+ width: $props.size,
1087
+ height: $props.size,
1088
+ viewBox: "0 0 24 24"
1089
+ }, [
1090
+ createElementVNode("path", _hoisted_3$n, [
1091
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$m, toDisplayString($props.title), 1)) : createCommentVNode("", true)
1092
+ ])
1093
+ ], 8, _hoisted_2$o))
1094
+ ], 16, _hoisted_1$s);
1095
+ }
1096
+ const FlipHorizontal = /* @__PURE__ */ _export_sfc(_sfc_main$v, [["render", _sfc_render$j]]);
1097
+ const _sfc_main$u = {
1098
+ name: "FlipVerticalIcon",
1099
+ emits: ["click"],
1100
+ props: {
1101
+ title: {
1102
+ type: String
1103
+ },
1104
+ fillColor: {
1105
+ type: String,
1106
+ default: "currentColor"
1107
+ },
1108
+ size: {
1109
+ type: Number,
1110
+ default: 24
1111
+ }
1112
+ }
1113
+ };
1114
+ const _hoisted_1$r = ["aria-hidden", "aria-label"];
1115
+ const _hoisted_2$n = ["fill", "width", "height"];
1116
+ const _hoisted_3$m = { d: "M3 15V17H5V15M15 19V21H17V19M19 3H5C3.9 3 3 3.9 3 5V9H5V5H19V9H21V5C21 3.9 20.1 3 19 3M21 19H19V21C20.1 21 21 20.1 21 19M1 11V13H23V11M7 19V21H9V19M19 15V17H21V15M11 19V21H13V19M3 19C3 20.1 3.9 21 5 21V19Z" };
1117
+ const _hoisted_4$l = { key: 0 };
1118
+ function _sfc_render$i(_ctx, _cache, $props, $setup, $data, $options) {
1119
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
1120
+ "aria-hidden": $props.title ? null : "true",
1121
+ "aria-label": $props.title,
1122
+ class: "material-design-icon flip-vertical-icon",
1123
+ role: "img",
1124
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
1125
+ }), [
1126
+ (openBlock(), createElementBlock("svg", {
1127
+ fill: $props.fillColor,
1128
+ class: "material-design-icon__svg",
1129
+ width: $props.size,
1130
+ height: $props.size,
1131
+ viewBox: "0 0 24 24"
1132
+ }, [
1133
+ createElementVNode("path", _hoisted_3$m, [
1134
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$l, toDisplayString($props.title), 1)) : createCommentVNode("", true)
1135
+ ])
1136
+ ], 8, _hoisted_2$n))
1137
+ ], 16, _hoisted_1$r);
1138
+ }
1139
+ const FlipVertical = /* @__PURE__ */ _export_sfc(_sfc_main$u, [["render", _sfc_render$i]]);
1140
+ const _sfc_main$t = {
1141
+ name: "RotateLeftIcon",
1142
+ emits: ["click"],
1143
+ props: {
1144
+ title: {
1145
+ type: String
1146
+ },
1147
+ fillColor: {
1148
+ type: String,
1149
+ default: "currentColor"
1150
+ },
1151
+ size: {
1152
+ type: Number,
1153
+ default: 24
1154
+ }
1155
+ }
1156
+ };
1157
+ const _hoisted_1$q = ["aria-hidden", "aria-label"];
1158
+ const _hoisted_2$m = ["fill", "width", "height"];
1159
+ const _hoisted_3$l = { d: "M13,4.07V1L8.45,5.55L13,10V6.09C15.84,6.57 18,9.03 18,12C18,14.97 15.84,17.43 13,17.91V19.93C16.95,19.44 20,16.08 20,12C20,7.92 16.95,4.56 13,4.07M7.1,18.32C8.26,19.22 9.61,19.76 11,19.93V17.9C10.13,17.75 9.29,17.41 8.54,16.87L7.1,18.32M6.09,13H4.07C4.24,14.39 4.79,15.73 5.69,16.89L7.1,15.47C6.58,14.72 6.23,13.88 6.09,13M7.11,8.53L5.7,7.11C4.8,8.27 4.24,9.61 4.07,11H6.09C6.23,10.13 6.58,9.28 7.11,8.53Z" };
1160
+ const _hoisted_4$k = { key: 0 };
1161
+ function _sfc_render$h(_ctx, _cache, $props, $setup, $data, $options) {
1162
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
1163
+ "aria-hidden": $props.title ? null : "true",
1164
+ "aria-label": $props.title,
1165
+ class: "material-design-icon rotate-left-icon",
1166
+ role: "img",
1167
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
1168
+ }), [
1169
+ (openBlock(), createElementBlock("svg", {
1170
+ fill: $props.fillColor,
1171
+ class: "material-design-icon__svg",
1172
+ width: $props.size,
1173
+ height: $props.size,
1174
+ viewBox: "0 0 24 24"
1175
+ }, [
1176
+ createElementVNode("path", _hoisted_3$l, [
1177
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$k, toDisplayString($props.title), 1)) : createCommentVNode("", true)
1178
+ ])
1179
+ ], 8, _hoisted_2$m))
1180
+ ], 16, _hoisted_1$q);
1181
+ }
1182
+ const RotateLeft = /* @__PURE__ */ _export_sfc(_sfc_main$t, [["render", _sfc_render$h]]);
1183
+ const _sfc_main$s = {
1184
+ name: "RotateRightIcon",
1185
+ emits: ["click"],
1186
+ props: {
1187
+ title: {
1188
+ type: String
1189
+ },
1190
+ fillColor: {
1191
+ type: String,
1192
+ default: "currentColor"
1193
+ },
1194
+ size: {
1195
+ type: Number,
1196
+ default: 24
1197
+ }
1198
+ }
1199
+ };
1200
+ const _hoisted_1$p = ["aria-hidden", "aria-label"];
1201
+ const _hoisted_2$l = ["fill", "width", "height"];
1202
+ const _hoisted_3$k = { d: "M16.89,15.5L18.31,16.89C19.21,15.73 19.76,14.39 19.93,13H17.91C17.77,13.87 17.43,14.72 16.89,15.5M13,17.9V19.92C14.39,19.75 15.74,19.21 16.9,18.31L15.46,16.87C14.71,17.41 13.87,17.76 13,17.9M19.93,11C19.76,9.61 19.21,8.27 18.31,7.11L16.89,8.53C17.43,9.28 17.77,10.13 17.91,11M15.55,5.55L11,1V4.07C7.06,4.56 4,7.92 4,12C4,16.08 7.05,19.44 11,19.93V17.91C8.16,17.43 6,14.97 6,12C6,9.03 8.16,6.57 11,6.09V10L15.55,5.55Z" };
1203
+ const _hoisted_4$j = { key: 0 };
1204
+ function _sfc_render$g(_ctx, _cache, $props, $setup, $data, $options) {
1205
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
1206
+ "aria-hidden": $props.title ? null : "true",
1207
+ "aria-label": $props.title,
1208
+ class: "material-design-icon rotate-right-icon",
1209
+ role: "img",
1210
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
1211
+ }), [
1212
+ (openBlock(), createElementBlock("svg", {
1213
+ fill: $props.fillColor,
1214
+ class: "material-design-icon__svg",
1215
+ width: $props.size,
1216
+ height: $props.size,
1217
+ viewBox: "0 0 24 24"
1218
+ }, [
1219
+ createElementVNode("path", _hoisted_3$k, [
1220
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$j, toDisplayString($props.title), 1)) : createCommentVNode("", true)
1221
+ ])
1222
+ ], 8, _hoisted_2$l))
1223
+ ], 16, _hoisted_1$p);
1224
+ }
1225
+ const RotateRight = /* @__PURE__ */ _export_sfc(_sfc_main$s, [["render", _sfc_render$g]]);
1226
+ const EDITOR_COMMANDS = /* @__PURE__ */ Symbol("nextcloud:image-editor:commands");
1227
+ function provideEditorCommands(commands) {
1228
+ provide(EDITOR_COMMANDS, commands);
1229
+ return commands;
1230
+ }
1231
+ function useEditorCommands() {
1232
+ const commands = inject(EDITOR_COMMANDS, null);
1233
+ if (commands === null) {
1234
+ throw new Error("useEditorCommands() called outside of an ImageEditor tree");
1235
+ }
1236
+ return commands;
1237
+ }
1238
+ const _hoisted_1$o = { class: "crop-panel" };
1239
+ const _hoisted_2$k = { class: "crop-panel__row" };
1240
+ const _hoisted_3$j = { class: "crop-panel__row" };
1241
+ const _sfc_main$r = /* @__PURE__ */ defineComponent({
1242
+ __name: "CropPanel",
1243
+ props: {
1244
+ loaded: { type: Boolean }
1245
+ },
1246
+ setup(__props) {
1247
+ const context = useEditorContext();
1248
+ const commands = useEditorCommands();
1249
+ const labels = {
1250
+ rotateLeft: t("Rotate left"),
1251
+ rotateRight: t("Rotate right"),
1252
+ flipHorizontal: t("Flip horizontal"),
1253
+ flipVertical: t("Flip vertical"),
1254
+ applyCrop: t("Apply crop"),
1255
+ resetCrop: t("Reset crop"),
1256
+ rotation: t("Rotation"),
1257
+ scale: t("Scale")
1258
+ };
1259
+ const aspectPresets = [
1260
+ { id: null, label: t("Free") },
1261
+ { id: "original", label: t("Original") },
1262
+ { id: 1, label: "1:1" },
1263
+ { id: 4 / 3, label: "4:3" },
1264
+ { id: 16 / 9, label: "16:9" }
1265
+ ];
1266
+ const cropControls = [
1267
+ { id: "rotation", label: labels.rotation },
1268
+ { id: "scale", label: labels.scale }
1269
+ ];
1270
+ const activeCropControl = shallowRef("rotation");
1271
+ const display = computed(() => activeCropControl.value === "rotation" ? `${context.state.value.fineRotation}°` : `×${context.state.value.zoom.toFixed(2)}`);
1272
+ function onTransformInput(value) {
1273
+ context.preview(activeCropControl.value === "rotation" ? { ...context.state.value, fineRotation: value } : { ...context.state.value, zoom: value });
1274
+ }
1275
+ function onSliderCommit() {
1276
+ context.commit(context.state.value, activeCropControl.value === "rotation" ? labels.rotation : labels.scale);
1277
+ }
1278
+ return (_ctx, _cache) => {
1279
+ return openBlock(), createElementBlock("div", _hoisted_1$o, [
1280
+ createElementVNode("div", _hoisted_2$k, [
1281
+ (openBlock(), createElementBlock(Fragment, null, renderList(aspectPresets, (preset) => {
1282
+ return createVNode(unref(NcButton), {
1283
+ key: String(preset.id),
1284
+ "data-test": `aspect-${preset.id === null ? "free" : preset.id === "original" ? "original" : preset.label}`,
1285
+ pressed: unref(context).cropAspect.value === preset.id,
1286
+ disabled: !__props.loaded,
1287
+ variant: "tertiary",
1288
+ onClick: ($event) => unref(context).cropAspect.value = preset.id
1289
+ }, {
1290
+ default: withCtx(() => [
1291
+ createTextVNode(toDisplayString(preset.label), 1)
1292
+ ]),
1293
+ _: 2
1294
+ }, 1032, ["data-test", "pressed", "disabled", "onClick"]);
1295
+ }), 64))
1296
+ ]),
1297
+ createElementVNode("div", _hoisted_3$j, [
1298
+ createVNode(unref(NcButton), {
1299
+ "aria-label": labels.rotateLeft,
1300
+ title: labels.rotateLeft,
1301
+ disabled: !__props.loaded,
1302
+ variant: "tertiary",
1303
+ onClick: _cache[0] || (_cache[0] = ($event) => unref(commands).rotateCCW())
1304
+ }, {
1305
+ icon: withCtx(() => [
1306
+ createVNode(RotateLeft, { size: 20 })
1307
+ ]),
1308
+ _: 1
1309
+ }, 8, ["aria-label", "title", "disabled"]),
1310
+ createVNode(unref(NcButton), {
1311
+ "aria-label": labels.rotateRight,
1312
+ title: labels.rotateRight,
1313
+ disabled: !__props.loaded,
1314
+ variant: "tertiary",
1315
+ onClick: _cache[1] || (_cache[1] = ($event) => unref(commands).rotateCW())
1316
+ }, {
1317
+ icon: withCtx(() => [
1318
+ createVNode(RotateRight, { size: 20 })
1319
+ ]),
1320
+ _: 1
1321
+ }, 8, ["aria-label", "title", "disabled"]),
1322
+ createVNode(unref(NcButton), {
1323
+ "aria-label": labels.flipHorizontal,
1324
+ title: labels.flipHorizontal,
1325
+ disabled: !__props.loaded,
1326
+ variant: "tertiary",
1327
+ onClick: _cache[2] || (_cache[2] = ($event) => unref(commands).flipHorizontal())
1328
+ }, {
1329
+ icon: withCtx(() => [
1330
+ createVNode(FlipHorizontal, { size: 20 })
1331
+ ]),
1332
+ _: 1
1333
+ }, 8, ["aria-label", "title", "disabled"]),
1334
+ createVNode(unref(NcButton), {
1335
+ "aria-label": labels.flipVertical,
1336
+ title: labels.flipVertical,
1337
+ disabled: !__props.loaded,
1338
+ variant: "tertiary",
1339
+ onClick: _cache[3] || (_cache[3] = ($event) => unref(commands).flipVertical())
1340
+ }, {
1341
+ icon: withCtx(() => [
1342
+ createVNode(FlipVertical, { size: 20 })
1343
+ ]),
1344
+ _: 1
1345
+ }, 8, ["aria-label", "title", "disabled"]),
1346
+ _cache[6] || (_cache[6] = createElementVNode("span", { class: "crop-panel__divider" }, null, -1)),
1347
+ (openBlock(), createElementBlock(Fragment, null, renderList(cropControls, (control) => {
1348
+ return createVNode(unref(NcButton), {
1349
+ key: control.id,
1350
+ "data-test": `tab-${control.id}`,
1351
+ pressed: activeCropControl.value === control.id,
1352
+ disabled: !__props.loaded,
1353
+ variant: "tertiary",
1354
+ onClick: ($event) => activeCropControl.value = control.id
1355
+ }, {
1356
+ default: withCtx(() => [
1357
+ createTextVNode(toDisplayString(control.label), 1)
1358
+ ]),
1359
+ _: 2
1360
+ }, 1032, ["data-test", "pressed", "disabled", "onClick"]);
1361
+ }), 64)),
1362
+ _cache[7] || (_cache[7] = createElementVNode("span", { class: "crop-panel__divider" }, null, -1)),
1363
+ createVNode(unref(NcButton), {
1364
+ "data-test": "reset-crop",
1365
+ variant: "tertiary",
1366
+ disabled: !__props.loaded || unref(context).state.value.crop === null,
1367
+ onClick: _cache[4] || (_cache[4] = ($event) => unref(commands).resetCrop())
1368
+ }, {
1369
+ default: withCtx(() => [
1370
+ createTextVNode(toDisplayString(labels.resetCrop), 1)
1371
+ ]),
1372
+ _: 1
1373
+ }, 8, ["disabled"]),
1374
+ createVNode(unref(NcButton), {
1375
+ "data-test": "apply-crop",
1376
+ variant: "secondary",
1377
+ disabled: !__props.loaded,
1378
+ onClick: _cache[5] || (_cache[5] = ($event) => unref(commands).applyCrop())
1379
+ }, {
1380
+ default: withCtx(() => [
1381
+ createTextVNode(toDisplayString(labels.applyCrop), 1)
1382
+ ]),
1383
+ _: 1
1384
+ }, 8, ["disabled"])
1385
+ ]),
1386
+ activeCropControl.value === "rotation" ? (openBlock(), createBlock(EditorSlider, {
1387
+ key: 0,
1388
+ value: unref(context).state.value.fineRotation,
1389
+ min: -45,
1390
+ max: 45,
1391
+ step: 1,
1392
+ label: labels.rotation,
1393
+ display: display.value,
1394
+ "data-test": "fine-rotation",
1395
+ disabled: !__props.loaded,
1396
+ onInput: onTransformInput,
1397
+ onCommit: onSliderCommit
1398
+ }, null, 8, ["value", "label", "display", "disabled"])) : (openBlock(), createBlock(EditorSlider, {
1399
+ key: 1,
1400
+ value: unref(context).state.value.zoom,
1401
+ min: 1,
1402
+ max: 3,
1403
+ step: 0.05,
1404
+ label: labels.scale,
1405
+ display: display.value,
1406
+ "data-test": "zoom",
1407
+ disabled: !__props.loaded,
1408
+ onInput: onTransformInput,
1409
+ onCommit: onSliderCommit
1410
+ }, null, 8, ["value", "label", "display", "disabled"]))
1411
+ ]);
1412
+ };
1413
+ }
1414
+ });
1415
+ const CropPanel = /* @__PURE__ */ _export_sfc(_sfc_main$r, [["__scopeId", "data-v-30fdb9f8"]]);
1416
+ const _hoisted_1$n = ["disabled", "data-test", "aria-pressed", "title"];
1417
+ const _hoisted_2$j = ["src", "alt"];
1418
+ const _sfc_main$q = /* @__PURE__ */ defineComponent({
1419
+ __name: "PresetChip",
1420
+ props: {
1421
+ url: {},
1422
+ label: {},
1423
+ active: { type: Boolean },
1424
+ disabled: { type: Boolean },
1425
+ dataTest: {}
1426
+ },
1427
+ emits: ["click"],
1428
+ setup(__props, { emit: __emit }) {
1429
+ const emit = __emit;
1430
+ return (_ctx, _cache) => {
1431
+ return openBlock(), createElementBlock("button", {
1432
+ type: "button",
1433
+ class: normalizeClass(["preset-chip", { "preset-chip--active": __props.active }]),
1434
+ disabled: __props.disabled,
1435
+ "data-test": __props.dataTest,
1436
+ "aria-pressed": __props.active,
1437
+ title: __props.label,
1438
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("click"))
1439
+ }, [
1440
+ createElementVNode("img", {
1441
+ src: __props.url,
1442
+ alt: __props.label
1443
+ }, null, 8, _hoisted_2$j),
1444
+ createElementVNode("span", null, toDisplayString(__props.label), 1)
1445
+ ], 10, _hoisted_1$n);
1446
+ };
1447
+ }
1448
+ });
1449
+ const PresetChip = /* @__PURE__ */ _export_sfc(_sfc_main$q, [["__scopeId", "data-v-b38cdab1"]]);
1450
+ function luma(r, g, b) {
1451
+ return 0.299 * r + 0.587 * g + 0.114 * b;
1452
+ }
1453
+ function saturate(imageData) {
1454
+ const { data } = imageData;
1455
+ const factor = this.saturation() + 1;
1456
+ for (let i = 0; i < data.length; i += 4) {
1457
+ const gray = luma(data[i], data[i + 1], data[i + 2]);
1458
+ data[i] = gray + (data[i] - gray) * factor;
1459
+ data[i + 1] = gray + (data[i + 1] - gray) * factor;
1460
+ data[i + 2] = gray + (data[i + 2] - gray) * factor;
1461
+ }
1462
+ }
1463
+ function warm(imageData) {
1464
+ const { data } = imageData;
1465
+ for (let i = 0; i < data.length; i += 4) {
1466
+ data[i] = Math.min(255, data[i] * 1.12);
1467
+ data[i + 2] = data[i + 2] * 0.9;
1468
+ }
1469
+ }
1470
+ function cool(imageData) {
1471
+ const { data } = imageData;
1472
+ for (let i = 0; i < data.length; i += 4) {
1473
+ data[i] = data[i] * 0.9;
1474
+ data[i + 2] = Math.min(255, data[i + 2] * 1.12);
1475
+ }
1476
+ }
1477
+ function fade(imageData) {
1478
+ const { data } = imageData;
1479
+ for (let i = 0; i < data.length; i += 4) {
1480
+ const luma2 = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
1481
+ data[i] = 28 + (0.7 * data[i] + 0.3 * luma2) * 0.86 * 1.04;
1482
+ data[i + 1] = 28 + (0.7 * data[i + 1] + 0.3 * luma2) * 0.86;
1483
+ data[i + 2] = 28 + (0.7 * data[i + 2] + 0.3 * luma2) * 0.86 * 0.96;
1484
+ }
1485
+ }
1486
+ function noir(imageData) {
1487
+ const { data } = imageData;
1488
+ for (let i = 0; i < data.length; i += 4) {
1489
+ const luma2 = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
1490
+ const value = Math.min(255, Math.max(0, (luma2 - 128) * 1.35 + 118));
1491
+ data[i] = value;
1492
+ data[i + 1] = value;
1493
+ data[i + 2] = value;
1494
+ }
1495
+ }
1496
+ function golden(imageData) {
1497
+ const { data } = imageData;
1498
+ for (let i = 0; i < data.length; i += 4) {
1499
+ data[i] = data[i] * 1.12 + 12;
1500
+ data[i + 1] = data[i + 1] * 1.04 + 5;
1501
+ data[i + 2] = data[i + 2] * 0.82;
1502
+ }
1503
+ }
1504
+ function coast(imageData) {
1505
+ const { data } = imageData;
1506
+ for (let i = 0; i < data.length; i += 4) {
1507
+ const luma2 = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
1508
+ const shadow = Math.max(0, 1 - luma2 / 140);
1509
+ data[i] = (data[i] - 128) * 1.12 + 132;
1510
+ data[i + 1] = (data[i + 1] - 128) * 1.12 + 132 + 10 * shadow;
1511
+ data[i + 2] = (data[i + 2] - 128) * 1.12 + 132 + 22 * shadow;
1512
+ }
1513
+ }
1514
+ function mist(imageData) {
1515
+ const { data } = imageData;
1516
+ for (let i = 0; i < data.length; i += 4) {
1517
+ const luma2 = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
1518
+ data[i] = 22 + (0.6 * data[i] + 0.4 * luma2) * 0.84;
1519
+ data[i + 1] = 22 + (0.6 * data[i + 1] + 0.4 * luma2) * 0.84;
1520
+ data[i + 2] = 26 + (0.6 * data[i + 2] + 0.4 * luma2) * 0.84;
1521
+ }
1522
+ }
1523
+ function berry(imageData) {
1524
+ const { data } = imageData;
1525
+ for (let i = 0; i < data.length; i += 4) {
1526
+ data[i] = (data[i] - 128) * 1.08 + 128 + 10;
1527
+ data[i + 1] = (data[i + 1] - 128) * 1.08 + 128 - 12;
1528
+ data[i + 2] = (data[i + 2] - 128) * 1.08 + 128 + 8;
1529
+ }
1530
+ }
1531
+ function cinema(imageData) {
1532
+ const { data } = imageData;
1533
+ for (let i = 0; i < data.length; i += 4) {
1534
+ const luma2 = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
1535
+ const balance = (luma2 - 128) / 128;
1536
+ data[i] = (data[i] - 128) * 1.06 + 128 + 16 * balance;
1537
+ data[i + 1] = (data[i + 1] - 128) * 1.06 + 128;
1538
+ data[i + 2] = (data[i + 2] - 128) * 1.06 + 128 - 20 * balance;
1539
+ }
1540
+ }
1541
+ function luna(imageData) {
1542
+ const { data } = imageData;
1543
+ for (let i = 0; i < data.length; i += 4) {
1544
+ const luma2 = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
1545
+ const value = 18 + luma2 * 0.9;
1546
+ data[i] = value;
1547
+ data[i + 1] = value;
1548
+ data[i + 2] = value + 4;
1549
+ }
1550
+ }
1551
+ function visibleRect(state, oriented) {
1552
+ return state.crop ?? { x: 0, y: 0, ...oriented };
1553
+ }
1554
+ function context2d(canvas) {
1555
+ const context = canvas.getContext("2d");
1556
+ if (context === null) {
1557
+ throw new Error("Canvas 2D context unavailable");
1558
+ }
1559
+ return context;
1560
+ }
1561
+ function supportsContextFilter(context) {
1562
+ if (context === null) {
1563
+ return false;
1564
+ }
1565
+ context.filter = "blur(1px)";
1566
+ return context.filter !== "none";
1567
+ }
1568
+ let contextFilterSupport = null;
1569
+ function contextFilterAvailable() {
1570
+ contextFilterSupport ??= supportsContextFilter(document.createElement("canvas").getContext("2d"));
1571
+ return contextFilterSupport;
1572
+ }
1573
+ function obfuscate(oriented, rect, style) {
1574
+ const strength = Math.max(4, Math.round(Math.min(oriented.width, oriented.height) / 40));
1575
+ const out = document.createElement("canvas");
1576
+ out.width = Math.max(1, Math.ceil(rect.width));
1577
+ out.height = Math.max(1, Math.ceil(rect.height));
1578
+ const context = context2d(out);
1579
+ if (style === "blur" && contextFilterAvailable()) {
1580
+ const pad = strength * 2;
1581
+ context.filter = `blur(${strength}px)`;
1582
+ context.drawImage(
1583
+ oriented,
1584
+ rect.x - pad,
1585
+ rect.y - pad,
1586
+ rect.width + pad * 2,
1587
+ rect.height + pad * 2,
1588
+ -pad,
1589
+ -pad,
1590
+ out.width + pad * 2,
1591
+ out.height + pad * 2
1592
+ );
1593
+ return out;
1594
+ }
1595
+ const small = document.createElement("canvas");
1596
+ small.width = Math.max(1, Math.ceil(rect.width / strength));
1597
+ small.height = Math.max(1, Math.ceil(rect.height / strength));
1598
+ context2d(small).drawImage(oriented, rect.x, rect.y, rect.width, rect.height, 0, 0, small.width, small.height);
1599
+ context.imageSmoothingEnabled = false;
1600
+ context.drawImage(small, 0, 0, out.width, out.height);
1601
+ return out;
1602
+ }
1603
+ function buildAnnotationNode(annotation, oriented) {
1604
+ const base = { id: annotation.id, name: "annotation" };
1605
+ switch (annotation.type) {
1606
+ case "draw":
1607
+ return new Konva.Line({
1608
+ ...base,
1609
+ points: annotation.points,
1610
+ stroke: annotation.color,
1611
+ strokeWidth: annotation.strokeWidth,
1612
+ lineCap: "round",
1613
+ lineJoin: "round"
1614
+ });
1615
+ case "arrow":
1616
+ return new Konva.Arrow({
1617
+ ...base,
1618
+ points: [...annotation.points],
1619
+ stroke: annotation.color,
1620
+ fill: annotation.color,
1621
+ strokeWidth: annotation.strokeWidth,
1622
+ pointerLength: annotation.strokeWidth * 4,
1623
+ pointerWidth: annotation.strokeWidth * 4
1624
+ });
1625
+ case "rectangle":
1626
+ return new Konva.Rect({
1627
+ ...base,
1628
+ ...annotation.rect,
1629
+ rotation: annotation.rotation,
1630
+ stroke: annotation.color,
1631
+ strokeWidth: annotation.strokeWidth
1632
+ });
1633
+ case "ellipse":
1634
+ return new Konva.Ellipse({
1635
+ ...base,
1636
+ x: annotation.rect.x,
1637
+ y: annotation.rect.y,
1638
+ offsetX: -annotation.rect.width / 2,
1639
+ offsetY: -annotation.rect.height / 2,
1640
+ radiusX: annotation.rect.width / 2,
1641
+ radiusY: annotation.rect.height / 2,
1642
+ rotation: annotation.rotation,
1643
+ stroke: annotation.color,
1644
+ strokeWidth: annotation.strokeWidth
1645
+ });
1646
+ case "text":
1647
+ case "sticker":
1648
+ return new Konva.Text({
1649
+ ...base,
1650
+ x: annotation.x,
1651
+ y: annotation.y,
1652
+ text: annotation.text,
1653
+ fill: annotation.color,
1654
+ fontSize: annotation.fontSize,
1655
+ rotation: annotation.rotation,
1656
+ // Kept in sync with the text overlay for WYSIWYG editing
1657
+ fontFamily: "Helvetica, Arial, sans-serif"
1658
+ });
1659
+ case "redact": {
1660
+ if (oriented === void 0) {
1661
+ throw new Error("Redaction requires the oriented image");
1662
+ }
1663
+ return new Konva.Image({
1664
+ ...base,
1665
+ image: obfuscate(oriented, annotation.rect, annotation.style),
1666
+ x: annotation.rect.x,
1667
+ y: annotation.rect.y
1668
+ });
1669
+ }
1670
+ }
1671
+ }
1672
+ function applyFilters(node, state, pixelRatio = 1) {
1673
+ const { brightness, contrast, saturation } = state.adjustments;
1674
+ const filters = [];
1675
+ if (brightness !== 0) {
1676
+ filters.push(Konva.Filters.Brighten);
1677
+ }
1678
+ if (contrast !== 0) {
1679
+ filters.push(Konva.Filters.Contrast);
1680
+ }
1681
+ if (saturation !== 0) {
1682
+ filters.push(saturate);
1683
+ }
1684
+ const presetFilters = {
1685
+ none: null,
1686
+ grayscale: Konva.Filters.Grayscale,
1687
+ noir,
1688
+ luna,
1689
+ sepia: Konva.Filters.Sepia,
1690
+ fade,
1691
+ warm,
1692
+ cool,
1693
+ golden,
1694
+ coast,
1695
+ mist,
1696
+ berry,
1697
+ cinema,
1698
+ invert: Konva.Filters.Invert,
1699
+ solarize: Konva.Filters.Solarize,
1700
+ posterize: Konva.Filters.Posterize,
1701
+ pop: Konva.Filters.Enhance
1702
+ }[state.preset];
1703
+ if (presetFilters !== null) {
1704
+ filters.push(presetFilters);
1705
+ }
1706
+ if (filters.length === 0) {
1707
+ node.filters([]);
1708
+ node.clearCache();
1709
+ return;
1710
+ }
1711
+ node.filters(filters);
1712
+ node.brightness(brightness / 100);
1713
+ node.contrast(contrast);
1714
+ node.saturation(saturation / 100);
1715
+ if (state.preset === "posterize") {
1716
+ node.levels(0.02);
1717
+ }
1718
+ if (state.preset === "pop") {
1719
+ node.enhance(0.25);
1720
+ }
1721
+ node.cache({ pixelRatio });
1722
+ }
1723
+ function thumbnailKey(state) {
1724
+ const { crop, adjustments } = state;
1725
+ return [
1726
+ crop?.x,
1727
+ crop?.y,
1728
+ crop?.width,
1729
+ crop?.height,
1730
+ adjustments.brightness,
1731
+ adjustments.contrast,
1732
+ adjustments.saturation
1733
+ ].join("|");
1734
+ }
1735
+ function presetThumbnail(oriented, state, preset, size = 96) {
1736
+ const visible = visibleRect(state, { width: oriented.width, height: oriented.height });
1737
+ const scale = Math.min(size / visible.width, size / visible.height);
1738
+ const thumb = document.createElement("canvas");
1739
+ thumb.width = Math.max(1, Math.round(visible.width * scale));
1740
+ thumb.height = Math.max(1, Math.round(visible.height * scale));
1741
+ context2d(thumb).drawImage(oriented, visible.x, visible.y, visible.width, visible.height, 0, 0, thumb.width, thumb.height);
1742
+ const stage = new Konva.Stage({
1743
+ container: document.createElement("div"),
1744
+ width: thumb.width,
1745
+ height: thumb.height
1746
+ });
1747
+ try {
1748
+ const node = new Konva.Image({ image: thumb, listening: false });
1749
+ applyFilters(node, { ...state, preset });
1750
+ const layer = new Konva.Layer();
1751
+ layer.add(node);
1752
+ stage.add(layer);
1753
+ return stage.toDataURL();
1754
+ } finally {
1755
+ stage.destroy();
1756
+ }
1757
+ }
1758
+ function createScene(stage) {
1759
+ const layer = new Konva.Layer();
1760
+ const viewGroup = new Konva.Group({ name: "view" });
1761
+ const contentGroup = new Konva.Group({ name: "content" });
1762
+ const imageNode = new Konva.Image({ image: void 0, listening: false });
1763
+ contentGroup.add(imageNode);
1764
+ viewGroup.add(contentGroup);
1765
+ layer.add(viewGroup);
1766
+ stage.add(layer);
1767
+ const built = /* @__PURE__ */ new Map();
1768
+ let filterKey = "";
1769
+ const update = (oriented, state, options) => {
1770
+ const orientedChanged = imageNode.image() !== oriented;
1771
+ if (orientedChanged) {
1772
+ imageNode.image(oriented);
1773
+ }
1774
+ const origin = options.showCropped ? visibleRect(state, { width: oriented.width, height: oriented.height }) : { x: 0, y: 0 };
1775
+ viewGroup.position({
1776
+ x: options.offset.x - origin.x * options.scale,
1777
+ y: options.offset.y - origin.y * options.scale
1778
+ });
1779
+ viewGroup.scale({ x: options.scale, y: options.scale });
1780
+ if (options.showCropped && state.crop !== null) {
1781
+ viewGroup.clip(state.crop);
1782
+ } else {
1783
+ viewGroup.clipWidth(void 0);
1784
+ viewGroup.clipHeight(void 0);
1785
+ }
1786
+ const pixelRatio = options.fastFilters ? Math.min(1, options.scale * (globalThis.devicePixelRatio || 1)) : 1;
1787
+ const { brightness, contrast, saturation } = state.adjustments;
1788
+ const nextFilterKey = `${brightness}|${contrast}|${saturation}|${state.preset}|${pixelRatio}`;
1789
+ if (orientedChanged || nextFilterKey !== filterKey) {
1790
+ applyFilters(imageNode, state, pixelRatio);
1791
+ filterKey = nextFilterKey;
1792
+ }
1793
+ const seen = /* @__PURE__ */ new Set();
1794
+ for (const annotation of state.annotations) {
1795
+ seen.add(annotation.id);
1796
+ const entry = built.get(annotation.id);
1797
+ if (entry !== void 0 && entry.annotation === annotation && !orientedChanged) {
1798
+ continue;
1799
+ }
1800
+ entry?.node.destroy();
1801
+ const node = buildAnnotationNode(annotation, oriented);
1802
+ contentGroup.add(node);
1803
+ built.set(annotation.id, { annotation, node });
1804
+ }
1805
+ for (const [id, entry] of built) {
1806
+ if (!seen.has(id)) {
1807
+ entry.node.destroy();
1808
+ built.delete(id);
1809
+ }
1810
+ }
1811
+ imageNode.zIndex(0);
1812
+ state.annotations.forEach((annotation, index) => built.get(annotation.id).node.zIndex(index + 1));
1813
+ };
1814
+ return {
1815
+ viewGroup,
1816
+ contentGroup,
1817
+ imageNode,
1818
+ update,
1819
+ destroy: () => {
1820
+ built.clear();
1821
+ layer.destroy();
1822
+ }
1823
+ };
1824
+ }
1825
+ function renderScene(stage, oriented, state, options) {
1826
+ const scene = createScene(stage);
1827
+ scene.update(oriented, state, options);
1828
+ return scene;
1829
+ }
1830
+ function renderToCanvas(oriented, state, maxSize) {
1831
+ const visible = visibleRect(state, { width: oriented.width, height: oriented.height });
1832
+ const pixelRatio = maxSize === void 0 ? 1 : Math.min(1, maxSize / Math.max(visible.width, visible.height));
1833
+ const stage = new Konva.Stage({
1834
+ // Detached container: the export stage is never displayed
1835
+ container: document.createElement("div"),
1836
+ width: visible.width,
1837
+ height: visible.height
1838
+ });
1839
+ try {
1840
+ renderScene(stage, oriented, state, {
1841
+ scale: 1,
1842
+ offset: { x: 0, y: 0 },
1843
+ showCropped: true
1844
+ });
1845
+ return stage.toCanvas({ pixelRatio });
1846
+ } finally {
1847
+ stage.destroy();
1848
+ }
1849
+ }
1850
+ function toImageCoords(pointer, state, oriented, options) {
1851
+ const visible = visibleRect(state, oriented);
1852
+ return {
1853
+ x: (pointer.x - options.offset.x) / options.scale + (options.showCropped ? visible.x : 0),
1854
+ y: (pointer.y - options.offset.y) / options.scale + (options.showCropped ? visible.y : 0)
1855
+ };
1856
+ }
1857
+ const _sfc_main$p = /* @__PURE__ */ defineComponent({
1858
+ __name: "FilterStrip",
1859
+ props: {
1860
+ loaded: { type: Boolean },
1861
+ oriented: {}
1862
+ },
1863
+ setup(__props) {
1864
+ const props = __props;
1865
+ const context = useEditorContext();
1866
+ const presets = [
1867
+ { id: "none", label: t("No filter") },
1868
+ { id: "pop", label: t("Pop") },
1869
+ { id: "golden", label: t("Golden") },
1870
+ { id: "coast", label: t("Coast") },
1871
+ { id: "cinema", label: t("Cinema") },
1872
+ { id: "berry", label: t("Berry") },
1873
+ { id: "mist", label: t("Mist") },
1874
+ { id: "warm", label: t("Warm") },
1875
+ { id: "cool", label: t("Cool") },
1876
+ { id: "fade", label: t("Fade") },
1877
+ { id: "grayscale", label: t("Grayscale") },
1878
+ { id: "noir", label: t("Noir") },
1879
+ { id: "luna", label: t("Luna") },
1880
+ { id: "sepia", label: t("Sepia") },
1881
+ { id: "invert", label: t("Invert") },
1882
+ { id: "solarize", label: t("Solarize") },
1883
+ { id: "posterize", label: t("Posterize") }
1884
+ ];
1885
+ const presetPreviews = shallowRef([]);
1886
+ watch(
1887
+ [() => props.oriented, () => thumbnailKey(context.state.value)],
1888
+ ([oriented]) => {
1889
+ presetPreviews.value = oriented ? presets.map((preset) => ({
1890
+ ...preset,
1891
+ url: presetThumbnail(oriented, context.state.value, preset.id)
1892
+ })) : [];
1893
+ },
1894
+ { immediate: true }
1895
+ );
1896
+ function setPreset(preset, label) {
1897
+ context.commit({ ...context.state.value, preset }, label);
1898
+ }
1899
+ return (_ctx, _cache) => {
1900
+ return openBlock(), createBlock(GlassSurface, {
1901
+ variant: "strip",
1902
+ class: "filter-strip"
1903
+ }, {
1904
+ default: withCtx(() => [
1905
+ (openBlock(true), createElementBlock(Fragment, null, renderList(presetPreviews.value, (preset) => {
1906
+ return openBlock(), createBlock(PresetChip, {
1907
+ key: preset.id,
1908
+ url: preset.url,
1909
+ label: preset.label,
1910
+ active: unref(context).state.value.preset === preset.id,
1911
+ disabled: !__props.loaded,
1912
+ "data-test": `preset-${preset.id}`,
1913
+ onClick: ($event) => setPreset(preset.id, preset.label)
1914
+ }, null, 8, ["url", "label", "active", "disabled", "data-test", "onClick"]);
1915
+ }), 128))
1916
+ ]),
1917
+ _: 1
1918
+ });
1919
+ };
1920
+ }
1921
+ });
1922
+ const FilterStrip = /* @__PURE__ */ _export_sfc(_sfc_main$p, [["__scopeId", "data-v-44007efa"]]);
1923
+ const _hoisted_1$m = { class: "redact-panel" };
1924
+ const _sfc_main$o = /* @__PURE__ */ defineComponent({
1925
+ __name: "RedactPanel",
1926
+ props: {
1927
+ loaded: { type: Boolean }
1928
+ },
1929
+ setup(__props) {
1930
+ const context = useEditorContext();
1931
+ const labels = {
1932
+ pixelate: t("Pixelate"),
1933
+ blur: t("Blur")
1934
+ };
1935
+ return (_ctx, _cache) => {
1936
+ return openBlock(), createElementBlock("div", _hoisted_1$m, [
1937
+ createVNode(unref(NcButton), {
1938
+ "data-test": "redact-pixelate",
1939
+ pressed: unref(context).redactStyle.value === "pixelate",
1940
+ disabled: !__props.loaded,
1941
+ variant: "tertiary",
1942
+ onClick: _cache[0] || (_cache[0] = ($event) => unref(context).redactStyle.value = "pixelate")
1943
+ }, {
1944
+ default: withCtx(() => [
1945
+ createTextVNode(toDisplayString(labels.pixelate), 1)
1946
+ ]),
1947
+ _: 1
1948
+ }, 8, ["pressed", "disabled"]),
1949
+ createVNode(unref(NcButton), {
1950
+ "data-test": "redact-blur",
1951
+ pressed: unref(context).redactStyle.value === "blur",
1952
+ disabled: !__props.loaded,
1953
+ variant: "tertiary",
1954
+ onClick: _cache[1] || (_cache[1] = ($event) => unref(context).redactStyle.value = "blur")
1955
+ }, {
1956
+ default: withCtx(() => [
1957
+ createTextVNode(toDisplayString(labels.blur), 1)
1958
+ ]),
1959
+ _: 1
1960
+ }, 8, ["pressed", "disabled"])
1961
+ ]);
1962
+ };
1963
+ }
1964
+ });
1965
+ const RedactPanel = /* @__PURE__ */ _export_sfc(_sfc_main$o, [["__scopeId", "data-v-ac701267"]]);
1966
+ const _hoisted_1$l = { class: "select-panel" };
1967
+ const _hoisted_2$i = {
1968
+ key: 0,
1969
+ class: "select-panel__option"
1970
+ };
1971
+ const _hoisted_3$i = ["value", "disabled"];
1972
+ const _hoisted_4$i = {
1973
+ key: 1,
1974
+ class: "select-panel__hint"
1975
+ };
1976
+ const _hoisted_5$1 = {
1977
+ key: 2,
1978
+ class: "select-panel__hint"
1979
+ };
1980
+ const _sfc_main$n = /* @__PURE__ */ defineComponent({
1981
+ __name: "SelectPanel",
1982
+ props: {
1983
+ loaded: { type: Boolean }
1984
+ },
1985
+ setup(__props) {
1986
+ const context = useEditorContext();
1987
+ const color = useAnnotationColor(context);
1988
+ const colorLabel = t("Color");
1989
+ const hint = t("Click an annotation to move, resize or recolor it");
1990
+ const transformHint = t("Drag to move, use the handles to resize");
1991
+ const selectedAnnotation = computed(() => context.state.value.annotations.find((annotation) => annotation.id === context.selectedId.value) ?? null);
1992
+ const recolorable = computed(() => selectedAnnotation.value !== null && selectedAnnotation.value.type !== "sticker" && selectedAnnotation.value.type !== "redact");
1993
+ return (_ctx, _cache) => {
1994
+ return openBlock(), createElementBlock("div", _hoisted_1$l, [
1995
+ recolorable.value ? (openBlock(), createElementBlock("label", _hoisted_2$i, [
1996
+ createTextVNode(toDisplayString(unref(colorLabel)) + " ", 1),
1997
+ createElementVNode("input", {
1998
+ value: unref(context).drawColor.value,
1999
+ type: "color",
2000
+ disabled: !__props.loaded,
2001
+ "data-test": "color",
2002
+ onInput: _cache[0] || (_cache[0] = ($event) => unref(color).preview($event.target.value)),
2003
+ onChange: _cache[1] || (_cache[1] = ($event) => unref(color).commit($event.target.value))
2004
+ }, null, 40, _hoisted_3$i)
2005
+ ])) : selectedAnnotation.value !== null ? (openBlock(), createElementBlock("span", _hoisted_4$i, toDisplayString(unref(transformHint)), 1)) : (openBlock(), createElementBlock("span", _hoisted_5$1, toDisplayString(unref(hint)), 1))
2006
+ ]);
2007
+ };
2008
+ }
2009
+ });
2010
+ const SelectPanel = /* @__PURE__ */ _export_sfc(_sfc_main$n, [["__scopeId", "data-v-2ca9996e"]]);
2011
+ const _hoisted_1$k = { class: "sticker-panel" };
2012
+ const _sfc_main$m = /* @__PURE__ */ defineComponent({
2013
+ __name: "StickerPanel",
2014
+ props: {
2015
+ loaded: { type: Boolean }
2016
+ },
2017
+ setup(__props) {
2018
+ const context = useEditorContext();
2019
+ const moreLabel = t("More emojis");
2020
+ const FALLBACK_STICKERS = ["😀", "😍", "🎉", "👍", "❤️", "⭐", "🔥", "💡", "✅", "❌", "❓", "⚠️"];
2021
+ function frequentStickers() {
2022
+ try {
2023
+ const frequent = emojiSearch("", 12).map((emoji) => emoji.native).filter((native) => typeof native === "string" && native !== "");
2024
+ return frequent.length >= 6 ? frequent : FALLBACK_STICKERS;
2025
+ } catch {
2026
+ return FALLBACK_STICKERS;
2027
+ }
2028
+ }
2029
+ const DEFAULT_STICKERS = frequentStickers();
2030
+ const stickers = computed(() => DEFAULT_STICKERS.includes(context.sticker.value) ? DEFAULT_STICKERS : [context.sticker.value, ...DEFAULT_STICKERS.slice(0, 11)]);
2031
+ return (_ctx, _cache) => {
2032
+ return openBlock(), createElementBlock("div", _hoisted_1$k, [
2033
+ (openBlock(true), createElementBlock(Fragment, null, renderList(stickers.value, (sticker) => {
2034
+ return openBlock(), createBlock(unref(NcButton), {
2035
+ key: sticker,
2036
+ "aria-label": sticker,
2037
+ pressed: unref(context).sticker.value === sticker,
2038
+ disabled: !__props.loaded,
2039
+ class: "sticker-panel__emoji",
2040
+ variant: "tertiary",
2041
+ onClick: ($event) => unref(context).sticker.value = sticker
2042
+ }, {
2043
+ default: withCtx(() => [
2044
+ createTextVNode(toDisplayString(sticker), 1)
2045
+ ]),
2046
+ _: 2
2047
+ }, 1032, ["aria-label", "pressed", "disabled", "onClick"]);
2048
+ }), 128)),
2049
+ createVNode(unref(NcEmojiPicker), {
2050
+ onSelect: _cache[0] || (_cache[0] = ($event) => unref(context).sticker.value = $event)
2051
+ }, {
2052
+ default: withCtx(() => [
2053
+ createVNode(unref(NcButton), {
2054
+ "data-test": "emoji-picker",
2055
+ variant: "tertiary"
2056
+ }, {
2057
+ default: withCtx(() => [
2058
+ createTextVNode(toDisplayString(unref(moreLabel)), 1)
2059
+ ]),
2060
+ _: 1
2061
+ })
2062
+ ]),
2063
+ _: 1
2064
+ })
2065
+ ]);
2066
+ };
2067
+ }
2068
+ });
2069
+ const StickerPanel = /* @__PURE__ */ _export_sfc(_sfc_main$m, [["__scopeId", "data-v-6307599b"]]);
2070
+ const _sfc_main$l = /* @__PURE__ */ defineComponent({
2071
+ __name: "EditorPanel",
2072
+ props: {
2073
+ loaded: { type: Boolean },
2074
+ oriented: {}
2075
+ },
2076
+ setup(__props) {
2077
+ const context = useEditorContext();
2078
+ return (_ctx, _cache) => {
2079
+ return unref(context).activeMode.value === "filter" ? (openBlock(), createBlock(FilterStrip, {
2080
+ key: 0,
2081
+ loaded: __props.loaded,
2082
+ oriented: __props.oriented
2083
+ }, null, 8, ["loaded", "oriented"])) : (openBlock(), createBlock(GlassSurface, {
2084
+ key: 1,
2085
+ variant: "card",
2086
+ class: "editor-card"
2087
+ }, {
2088
+ default: withCtx(() => [
2089
+ unref(context).activeMode.value === "crop" ? (openBlock(), createBlock(CropPanel, {
2090
+ key: 0,
2091
+ loaded: __props.loaded
2092
+ }, null, 8, ["loaded"])) : unref(context).activeMode.value === "finetune" ? (openBlock(), createBlock(AdjustPanel, {
2093
+ key: 1,
2094
+ loaded: __props.loaded
2095
+ }, null, 8, ["loaded"])) : unref(context).activeMode.value === "annotate" ? (openBlock(), createBlock(AnnotatePanel, {
2096
+ key: 2,
2097
+ loaded: __props.loaded
2098
+ }, null, 8, ["loaded"])) : unref(context).activeMode.value === "select" ? (openBlock(), createBlock(SelectPanel, {
2099
+ key: 3,
2100
+ loaded: __props.loaded
2101
+ }, null, 8, ["loaded"])) : unref(context).activeMode.value === "sticker" ? (openBlock(), createBlock(StickerPanel, {
2102
+ key: 4,
2103
+ loaded: __props.loaded
2104
+ }, null, 8, ["loaded"])) : unref(context).activeMode.value === "redact" ? (openBlock(), createBlock(RedactPanel, {
2105
+ key: 5,
2106
+ loaded: __props.loaded
2107
+ }, null, 8, ["loaded"])) : createCommentVNode("", true)
2108
+ ]),
2109
+ _: 1
2110
+ }));
2111
+ };
2112
+ }
2113
+ });
2114
+ const EditorPanel = /* @__PURE__ */ _export_sfc(_sfc_main$l, [["__scopeId", "data-v-3fb85d8f"]]);
2115
+ const _sfc_main$k = {
2116
+ name: "BlurIcon",
2117
+ emits: ["click"],
2118
+ props: {
2119
+ title: {
2120
+ type: String
2121
+ },
2122
+ fillColor: {
2123
+ type: String,
2124
+ default: "currentColor"
2125
+ },
2126
+ size: {
2127
+ type: Number,
2128
+ default: 24
2129
+ }
2130
+ }
2131
+ };
2132
+ const _hoisted_1$j = ["aria-hidden", "aria-label"];
2133
+ const _hoisted_2$h = ["fill", "width", "height"];
2134
+ const _hoisted_3$h = { d: "M14,8.5A1.5,1.5 0 0,0 12.5,10A1.5,1.5 0 0,0 14,11.5A1.5,1.5 0 0,0 15.5,10A1.5,1.5 0 0,0 14,8.5M14,12.5A1.5,1.5 0 0,0 12.5,14A1.5,1.5 0 0,0 14,15.5A1.5,1.5 0 0,0 15.5,14A1.5,1.5 0 0,0 14,12.5M10,17A1,1 0 0,0 9,18A1,1 0 0,0 10,19A1,1 0 0,0 11,18A1,1 0 0,0 10,17M10,8.5A1.5,1.5 0 0,0 8.5,10A1.5,1.5 0 0,0 10,11.5A1.5,1.5 0 0,0 11.5,10A1.5,1.5 0 0,0 10,8.5M14,20.5A0.5,0.5 0 0,0 13.5,21A0.5,0.5 0 0,0 14,21.5A0.5,0.5 0 0,0 14.5,21A0.5,0.5 0 0,0 14,20.5M14,17A1,1 0 0,0 13,18A1,1 0 0,0 14,19A1,1 0 0,0 15,18A1,1 0 0,0 14,17M21,13.5A0.5,0.5 0 0,0 20.5,14A0.5,0.5 0 0,0 21,14.5A0.5,0.5 0 0,0 21.5,14A0.5,0.5 0 0,0 21,13.5M18,5A1,1 0 0,0 17,6A1,1 0 0,0 18,7A1,1 0 0,0 19,6A1,1 0 0,0 18,5M18,9A1,1 0 0,0 17,10A1,1 0 0,0 18,11A1,1 0 0,0 19,10A1,1 0 0,0 18,9M18,17A1,1 0 0,0 17,18A1,1 0 0,0 18,19A1,1 0 0,0 19,18A1,1 0 0,0 18,17M18,13A1,1 0 0,0 17,14A1,1 0 0,0 18,15A1,1 0 0,0 19,14A1,1 0 0,0 18,13M10,12.5A1.5,1.5 0 0,0 8.5,14A1.5,1.5 0 0,0 10,15.5A1.5,1.5 0 0,0 11.5,14A1.5,1.5 0 0,0 10,12.5M10,7A1,1 0 0,0 11,6A1,1 0 0,0 10,5A1,1 0 0,0 9,6A1,1 0 0,0 10,7M10,3.5A0.5,0.5 0 0,0 10.5,3A0.5,0.5 0 0,0 10,2.5A0.5,0.5 0 0,0 9.5,3A0.5,0.5 0 0,0 10,3.5M10,20.5A0.5,0.5 0 0,0 9.5,21A0.5,0.5 0 0,0 10,21.5A0.5,0.5 0 0,0 10.5,21A0.5,0.5 0 0,0 10,20.5M3,13.5A0.5,0.5 0 0,0 2.5,14A0.5,0.5 0 0,0 3,14.5A0.5,0.5 0 0,0 3.5,14A0.5,0.5 0 0,0 3,13.5M14,3.5A0.5,0.5 0 0,0 14.5,3A0.5,0.5 0 0,0 14,2.5A0.5,0.5 0 0,0 13.5,3A0.5,0.5 0 0,0 14,3.5M14,7A1,1 0 0,0 15,6A1,1 0 0,0 14,5A1,1 0 0,0 13,6A1,1 0 0,0 14,7M21,10.5A0.5,0.5 0 0,0 21.5,10A0.5,0.5 0 0,0 21,9.5A0.5,0.5 0 0,0 20.5,10A0.5,0.5 0 0,0 21,10.5M6,5A1,1 0 0,0 5,6A1,1 0 0,0 6,7A1,1 0 0,0 7,6A1,1 0 0,0 6,5M3,9.5A0.5,0.5 0 0,0 2.5,10A0.5,0.5 0 0,0 3,10.5A0.5,0.5 0 0,0 3.5,10A0.5,0.5 0 0,0 3,9.5M6,9A1,1 0 0,0 5,10A1,1 0 0,0 6,11A1,1 0 0,0 7,10A1,1 0 0,0 6,9M6,17A1,1 0 0,0 5,18A1,1 0 0,0 6,19A1,1 0 0,0 7,18A1,1 0 0,0 6,17M6,13A1,1 0 0,0 5,14A1,1 0 0,0 6,15A1,1 0 0,0 7,14A1,1 0 0,0 6,13Z" };
2135
+ const _hoisted_4$h = { key: 0 };
2136
+ function _sfc_render$f(_ctx, _cache, $props, $setup, $data, $options) {
2137
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
2138
+ "aria-hidden": $props.title ? null : "true",
2139
+ "aria-label": $props.title,
2140
+ class: "material-design-icon blur-icon",
2141
+ role: "img",
2142
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2143
+ }), [
2144
+ (openBlock(), createElementBlock("svg", {
2145
+ fill: $props.fillColor,
2146
+ class: "material-design-icon__svg",
2147
+ width: $props.size,
2148
+ height: $props.size,
2149
+ viewBox: "0 0 24 24"
2150
+ }, [
2151
+ createElementVNode("path", _hoisted_3$h, [
2152
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$h, toDisplayString($props.title), 1)) : createCommentVNode("", true)
2153
+ ])
2154
+ ], 8, _hoisted_2$h))
2155
+ ], 16, _hoisted_1$j);
2156
+ }
2157
+ const Blur = /* @__PURE__ */ _export_sfc(_sfc_main$k, [["render", _sfc_render$f]]);
2158
+ const _sfc_main$j = {
2159
+ name: "CropIcon",
2160
+ emits: ["click"],
2161
+ props: {
2162
+ title: {
2163
+ type: String
2164
+ },
2165
+ fillColor: {
2166
+ type: String,
2167
+ default: "currentColor"
2168
+ },
2169
+ size: {
2170
+ type: Number,
2171
+ default: 24
2172
+ }
2173
+ }
2174
+ };
2175
+ const _hoisted_1$i = ["aria-hidden", "aria-label"];
2176
+ const _hoisted_2$g = ["fill", "width", "height"];
2177
+ const _hoisted_3$g = { d: "M7,17V1H5V5H1V7H5V17A2,2 0 0,0 7,19H17V23H19V19H23V17M17,15H19V7C19,5.89 18.1,5 17,5H9V7H17V15Z" };
2178
+ const _hoisted_4$g = { key: 0 };
2179
+ function _sfc_render$e(_ctx, _cache, $props, $setup, $data, $options) {
2180
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
2181
+ "aria-hidden": $props.title ? null : "true",
2182
+ "aria-label": $props.title,
2183
+ class: "material-design-icon crop-icon",
2184
+ role: "img",
2185
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2186
+ }), [
2187
+ (openBlock(), createElementBlock("svg", {
2188
+ fill: $props.fillColor,
2189
+ class: "material-design-icon__svg",
2190
+ width: $props.size,
2191
+ height: $props.size,
2192
+ viewBox: "0 0 24 24"
2193
+ }, [
2194
+ createElementVNode("path", _hoisted_3$g, [
2195
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$g, toDisplayString($props.title), 1)) : createCommentVNode("", true)
2196
+ ])
2197
+ ], 8, _hoisted_2$g))
2198
+ ], 16, _hoisted_1$i);
2199
+ }
2200
+ const Crop = /* @__PURE__ */ _export_sfc(_sfc_main$j, [["render", _sfc_render$e]]);
2201
+ const _sfc_main$i = {
2202
+ name: "CursorDefaultOutlineIcon",
2203
+ emits: ["click"],
2204
+ props: {
2205
+ title: {
2206
+ type: String
2207
+ },
2208
+ fillColor: {
2209
+ type: String,
2210
+ default: "currentColor"
2211
+ },
2212
+ size: {
2213
+ type: Number,
2214
+ default: 24
2215
+ }
2216
+ }
2217
+ };
2218
+ const _hoisted_1$h = ["aria-hidden", "aria-label"];
2219
+ const _hoisted_2$f = ["fill", "width", "height"];
2220
+ const _hoisted_3$f = { d: "M10.07,14.27C10.57,14.03 11.16,14.25 11.4,14.75L13.7,19.74L15.5,18.89L13.19,13.91C12.95,13.41 13.17,12.81 13.67,12.58L13.95,12.5L16.25,12.05L8,5.12V15.9L9.82,14.43L10.07,14.27M13.64,21.97C13.14,22.21 12.54,22 12.31,21.5L10.13,16.76L7.62,18.78C7.45,18.92 7.24,19 7,19A1,1 0 0,1 6,18V3A1,1 0 0,1 7,2C7.24,2 7.47,2.09 7.64,2.23L7.65,2.22L19.14,11.86C19.57,12.22 19.62,12.85 19.27,13.27C19.12,13.45 18.91,13.57 18.7,13.61L15.54,14.23L17.74,18.96C18,19.46 17.76,20.05 17.26,20.28L13.64,21.97Z" };
2221
+ const _hoisted_4$f = { key: 0 };
2222
+ function _sfc_render$d(_ctx, _cache, $props, $setup, $data, $options) {
2223
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
2224
+ "aria-hidden": $props.title ? null : "true",
2225
+ "aria-label": $props.title,
2226
+ class: "material-design-icon cursor-default-outline-icon",
2227
+ role: "img",
2228
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2229
+ }), [
2230
+ (openBlock(), createElementBlock("svg", {
2231
+ fill: $props.fillColor,
2232
+ class: "material-design-icon__svg",
2233
+ width: $props.size,
2234
+ height: $props.size,
2235
+ viewBox: "0 0 24 24"
2236
+ }, [
2237
+ createElementVNode("path", _hoisted_3$f, [
2238
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$f, toDisplayString($props.title), 1)) : createCommentVNode("", true)
2239
+ ])
2240
+ ], 8, _hoisted_2$f))
2241
+ ], 16, _hoisted_1$h);
2242
+ }
2243
+ const CursorDefaultOutline = /* @__PURE__ */ _export_sfc(_sfc_main$i, [["render", _sfc_render$d]]);
2244
+ const _sfc_main$h = {
2245
+ name: "PaletteOutlineIcon",
2246
+ emits: ["click"],
2247
+ props: {
2248
+ title: {
2249
+ type: String
2250
+ },
2251
+ fillColor: {
2252
+ type: String,
2253
+ default: "currentColor"
2254
+ },
2255
+ size: {
2256
+ type: Number,
2257
+ default: 24
2258
+ }
2259
+ }
2260
+ };
2261
+ const _hoisted_1$g = ["aria-hidden", "aria-label"];
2262
+ const _hoisted_2$e = ["fill", "width", "height"];
2263
+ const _hoisted_3$e = { d: "M12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2C17.5,2 22,6 22,11A6,6 0 0,1 16,17H14.2C13.9,17 13.7,17.2 13.7,17.5C13.7,17.6 13.8,17.7 13.8,17.8C14.2,18.3 14.4,18.9 14.4,19.5C14.5,20.9 13.4,22 12,22M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C12.3,20 12.5,19.8 12.5,19.5C12.5,19.3 12.4,19.2 12.4,19.1C12,18.6 11.8,18.1 11.8,17.5C11.8,16.1 12.9,15 14.3,15H16A4,4 0 0,0 20,11C20,7.1 16.4,4 12,4M6.5,10C7.3,10 8,10.7 8,11.5C8,12.3 7.3,13 6.5,13C5.7,13 5,12.3 5,11.5C5,10.7 5.7,10 6.5,10M9.5,6C10.3,6 11,6.7 11,7.5C11,8.3 10.3,9 9.5,9C8.7,9 8,8.3 8,7.5C8,6.7 8.7,6 9.5,6M14.5,6C15.3,6 16,6.7 16,7.5C16,8.3 15.3,9 14.5,9C13.7,9 13,8.3 13,7.5C13,6.7 13.7,6 14.5,6M17.5,10C18.3,10 19,10.7 19,11.5C19,12.3 18.3,13 17.5,13C16.7,13 16,12.3 16,11.5C16,10.7 16.7,10 17.5,10Z" };
2264
+ const _hoisted_4$e = { key: 0 };
2265
+ function _sfc_render$c(_ctx, _cache, $props, $setup, $data, $options) {
2266
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
2267
+ "aria-hidden": $props.title ? null : "true",
2268
+ "aria-label": $props.title,
2269
+ class: "material-design-icon palette-outline-icon",
2270
+ role: "img",
2271
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2272
+ }), [
2273
+ (openBlock(), createElementBlock("svg", {
2274
+ fill: $props.fillColor,
2275
+ class: "material-design-icon__svg",
2276
+ width: $props.size,
2277
+ height: $props.size,
2278
+ viewBox: "0 0 24 24"
2279
+ }, [
2280
+ createElementVNode("path", _hoisted_3$e, [
2281
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$e, toDisplayString($props.title), 1)) : createCommentVNode("", true)
2282
+ ])
2283
+ ], 8, _hoisted_2$e))
2284
+ ], 16, _hoisted_1$g);
2285
+ }
2286
+ const PaletteOutline = /* @__PURE__ */ _export_sfc(_sfc_main$h, [["render", _sfc_render$c]]);
2287
+ const _sfc_main$g = {
2288
+ name: "StickerEmojiIcon",
2289
+ emits: ["click"],
2290
+ props: {
2291
+ title: {
2292
+ type: String
2293
+ },
2294
+ fillColor: {
2295
+ type: String,
2296
+ default: "currentColor"
2297
+ },
2298
+ size: {
2299
+ type: Number,
2300
+ default: 24
2301
+ }
2302
+ }
2303
+ };
2304
+ const _hoisted_1$f = ["aria-hidden", "aria-label"];
2305
+ const _hoisted_2$d = ["fill", "width", "height"];
2306
+ const _hoisted_3$d = { d: "M5.5,2C3.56,2 2,3.56 2,5.5V18.5C2,20.44 3.56,22 5.5,22H16L22,16V5.5C22,3.56 20.44,2 18.5,2H5.5M5.75,4H18.25A1.75,1.75 0 0,1 20,5.75V15H18.5C16.56,15 15,16.56 15,18.5V20H5.75A1.75,1.75 0 0,1 4,18.25V5.75A1.75,1.75 0 0,1 5.75,4M14.44,6.77C14.28,6.77 14.12,6.79 13.97,6.83C13.03,7.09 12.5,8.05 12.74,9C12.79,9.15 12.86,9.3 12.95,9.44L16.18,8.56C16.18,8.39 16.16,8.22 16.12,8.05C15.91,7.3 15.22,6.77 14.44,6.77M8.17,8.5C8,8.5 7.85,8.5 7.7,8.55C6.77,8.81 6.22,9.77 6.47,10.7C6.5,10.86 6.59,11 6.68,11.16L9.91,10.28C9.91,10.11 9.89,9.94 9.85,9.78C9.64,9 8.95,8.5 8.17,8.5M16.72,11.26L7.59,13.77C8.91,15.3 11,15.94 12.95,15.41C14.9,14.87 16.36,13.25 16.72,11.26Z" };
2307
+ const _hoisted_4$d = { key: 0 };
2308
+ function _sfc_render$b(_ctx, _cache, $props, $setup, $data, $options) {
2309
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
2310
+ "aria-hidden": $props.title ? null : "true",
2311
+ "aria-label": $props.title,
2312
+ class: "material-design-icon sticker-emoji-icon",
2313
+ role: "img",
2314
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2315
+ }), [
2316
+ (openBlock(), createElementBlock("svg", {
2317
+ fill: $props.fillColor,
2318
+ class: "material-design-icon__svg",
2319
+ width: $props.size,
2320
+ height: $props.size,
2321
+ viewBox: "0 0 24 24"
2322
+ }, [
2323
+ createElementVNode("path", _hoisted_3$d, [
2324
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$d, toDisplayString($props.title), 1)) : createCommentVNode("", true)
2325
+ ])
2326
+ ], 8, _hoisted_2$d))
2327
+ ], 16, _hoisted_1$f);
2328
+ }
2329
+ const StickerEmoji = /* @__PURE__ */ _export_sfc(_sfc_main$g, [["render", _sfc_render$b]]);
2330
+ const _sfc_main$f = {
2331
+ name: "TuneIcon",
2332
+ emits: ["click"],
2333
+ props: {
2334
+ title: {
2335
+ type: String
2336
+ },
2337
+ fillColor: {
2338
+ type: String,
2339
+ default: "currentColor"
2340
+ },
2341
+ size: {
2342
+ type: Number,
2343
+ default: 24
2344
+ }
2345
+ }
2346
+ };
2347
+ const _hoisted_1$e = ["aria-hidden", "aria-label"];
2348
+ const _hoisted_2$c = ["fill", "width", "height"];
2349
+ const _hoisted_3$c = { d: "M3,17V19H9V17H3M3,5V7H13V5H3M13,21V19H21V17H13V15H11V21H13M7,9V11H3V13H7V15H9V9H7M21,13V11H11V13H21M15,9H17V7H21V5H17V3H15V9Z" };
2350
+ const _hoisted_4$c = { key: 0 };
2351
+ function _sfc_render$a(_ctx, _cache, $props, $setup, $data, $options) {
2352
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
2353
+ "aria-hidden": $props.title ? null : "true",
2354
+ "aria-label": $props.title,
2355
+ class: "material-design-icon tune-icon",
2356
+ role: "img",
2357
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2358
+ }), [
2359
+ (openBlock(), createElementBlock("svg", {
2360
+ fill: $props.fillColor,
2361
+ class: "material-design-icon__svg",
2362
+ width: $props.size,
2363
+ height: $props.size,
2364
+ viewBox: "0 0 24 24"
2365
+ }, [
2366
+ createElementVNode("path", _hoisted_3$c, [
2367
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$c, toDisplayString($props.title), 1)) : createCommentVNode("", true)
2368
+ ])
2369
+ ], 8, _hoisted_2$c))
2370
+ ], 16, _hoisted_1$e);
2371
+ }
2372
+ const Tune = /* @__PURE__ */ _export_sfc(_sfc_main$f, [["render", _sfc_render$a]]);
2373
+ const _hoisted_1$d = { class: "editor-sidebar" };
2374
+ const _sfc_main$e = /* @__PURE__ */ defineComponent({
2375
+ __name: "EditorSidebar",
2376
+ props: {
2377
+ loaded: { type: Boolean }
2378
+ },
2379
+ setup(__props) {
2380
+ const context = useEditorContext();
2381
+ const modes = [
2382
+ { id: "select", label: t("Select"), icon: CursorDefaultOutline },
2383
+ { id: "crop", label: t("Crop"), icon: Crop },
2384
+ { id: "finetune", label: t("Adjust"), icon: Tune },
2385
+ { id: "filter", label: t("Filter"), icon: PaletteOutline },
2386
+ { id: "annotate", label: t("Annotate"), icon: Pencil },
2387
+ { id: "sticker", label: t("Sticker"), icon: StickerEmoji },
2388
+ { id: "redact", label: t("Redact"), icon: Blur }
2389
+ ];
2390
+ return (_ctx, _cache) => {
2391
+ return openBlock(), createElementBlock("nav", _hoisted_1$d, [
2392
+ (openBlock(), createElementBlock(Fragment, null, renderList(modes, (mode) => {
2393
+ return createVNode(IconTab, {
2394
+ key: mode.id,
2395
+ label: mode.label,
2396
+ active: unref(context).activeMode.value === mode.id,
2397
+ disabled: !__props.loaded,
2398
+ onClick: ($event) => unref(context).setMode(mode.id)
2399
+ }, {
2400
+ default: withCtx(() => [
2401
+ (openBlock(), createBlock(resolveDynamicComponent(mode.icon), { size: 20 }))
2402
+ ]),
2403
+ _: 2
2404
+ }, 1032, ["label", "active", "disabled", "onClick"]);
2405
+ }), 64))
2406
+ ]);
2407
+ };
2408
+ }
2409
+ });
2410
+ const EditorSidebar = /* @__PURE__ */ _export_sfc(_sfc_main$e, [["__scopeId", "data-v-a4223674"]]);
2411
+ const _sfc_main$d = {
2412
+ name: "CheckIcon",
2413
+ emits: ["click"],
2414
+ props: {
2415
+ title: {
2416
+ type: String
2417
+ },
2418
+ fillColor: {
2419
+ type: String,
2420
+ default: "currentColor"
2421
+ },
2422
+ size: {
2423
+ type: Number,
2424
+ default: 24
2425
+ }
2426
+ }
2427
+ };
2428
+ const _hoisted_1$c = ["aria-hidden", "aria-label"];
2429
+ const _hoisted_2$b = ["fill", "width", "height"];
2430
+ const _hoisted_3$b = { d: "M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" };
2431
+ const _hoisted_4$b = { key: 0 };
2432
+ function _sfc_render$9(_ctx, _cache, $props, $setup, $data, $options) {
2433
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
2434
+ "aria-hidden": $props.title ? null : "true",
2435
+ "aria-label": $props.title,
2436
+ class: "material-design-icon check-icon",
2437
+ role: "img",
2438
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2439
+ }), [
2440
+ (openBlock(), createElementBlock("svg", {
2441
+ fill: $props.fillColor,
2442
+ class: "material-design-icon__svg",
2443
+ width: $props.size,
2444
+ height: $props.size,
2445
+ viewBox: "0 0 24 24"
2446
+ }, [
2447
+ createElementVNode("path", _hoisted_3$b, [
2448
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$b, toDisplayString($props.title), 1)) : createCommentVNode("", true)
2449
+ ])
2450
+ ], 8, _hoisted_2$b))
2451
+ ], 16, _hoisted_1$c);
2452
+ }
2453
+ const Check = /* @__PURE__ */ _export_sfc(_sfc_main$d, [["render", _sfc_render$9]]);
2454
+ const _sfc_main$c = {
2455
+ name: "CloseIcon",
2456
+ emits: ["click"],
2457
+ props: {
2458
+ title: {
2459
+ type: String
2460
+ },
2461
+ fillColor: {
2462
+ type: String,
2463
+ default: "currentColor"
2464
+ },
2465
+ size: {
2466
+ type: Number,
2467
+ default: 24
2468
+ }
2469
+ }
2470
+ };
2471
+ const _hoisted_1$b = ["aria-hidden", "aria-label"];
2472
+ const _hoisted_2$a = ["fill", "width", "height"];
2473
+ const _hoisted_3$a = { d: "M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" };
2474
+ const _hoisted_4$a = { key: 0 };
2475
+ function _sfc_render$8(_ctx, _cache, $props, $setup, $data, $options) {
2476
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
2477
+ "aria-hidden": $props.title ? null : "true",
2478
+ "aria-label": $props.title,
2479
+ class: "material-design-icon close-icon",
2480
+ role: "img",
2481
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2482
+ }), [
2483
+ (openBlock(), createElementBlock("svg", {
2484
+ fill: $props.fillColor,
2485
+ class: "material-design-icon__svg",
2486
+ width: $props.size,
2487
+ height: $props.size,
2488
+ viewBox: "0 0 24 24"
2489
+ }, [
2490
+ createElementVNode("path", _hoisted_3$a, [
2491
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$a, toDisplayString($props.title), 1)) : createCommentVNode("", true)
2492
+ ])
2493
+ ], 8, _hoisted_2$a))
2494
+ ], 16, _hoisted_1$b);
2495
+ }
2496
+ const Close = /* @__PURE__ */ _export_sfc(_sfc_main$c, [["render", _sfc_render$8]]);
2497
+ const _sfc_main$b = {
2498
+ name: "HistoryIcon",
2499
+ emits: ["click"],
2500
+ props: {
2501
+ title: {
2502
+ type: String
2503
+ },
2504
+ fillColor: {
2505
+ type: String,
2506
+ default: "currentColor"
2507
+ },
2508
+ size: {
2509
+ type: Number,
2510
+ default: 24
2511
+ }
2512
+ }
2513
+ };
2514
+ const _hoisted_1$a = ["aria-hidden", "aria-label"];
2515
+ const _hoisted_2$9 = ["fill", "width", "height"];
2516
+ const _hoisted_3$9 = { d: "M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3" };
2517
+ const _hoisted_4$9 = { key: 0 };
2518
+ function _sfc_render$7(_ctx, _cache, $props, $setup, $data, $options) {
2519
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
2520
+ "aria-hidden": $props.title ? null : "true",
2521
+ "aria-label": $props.title,
2522
+ class: "material-design-icon history-icon",
2523
+ role: "img",
2524
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2525
+ }), [
2526
+ (openBlock(), createElementBlock("svg", {
2527
+ fill: $props.fillColor,
2528
+ class: "material-design-icon__svg",
2529
+ width: $props.size,
2530
+ height: $props.size,
2531
+ viewBox: "0 0 24 24"
2532
+ }, [
2533
+ createElementVNode("path", _hoisted_3$9, [
2534
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$9, toDisplayString($props.title), 1)) : createCommentVNode("", true)
2535
+ ])
2536
+ ], 8, _hoisted_2$9))
2537
+ ], 16, _hoisted_1$a);
2538
+ }
2539
+ const HistoryIcon = /* @__PURE__ */ _export_sfc(_sfc_main$b, [["render", _sfc_render$7]]);
2540
+ const _sfc_main$a = {
2541
+ name: "MagnifyMinusOutlineIcon",
2542
+ emits: ["click"],
2543
+ props: {
2544
+ title: {
2545
+ type: String
2546
+ },
2547
+ fillColor: {
2548
+ type: String,
2549
+ default: "currentColor"
2550
+ },
2551
+ size: {
2552
+ type: Number,
2553
+ default: 24
2554
+ }
2555
+ }
2556
+ };
2557
+ const _hoisted_1$9 = ["aria-hidden", "aria-label"];
2558
+ const _hoisted_2$8 = ["fill", "width", "height"];
2559
+ const _hoisted_3$8 = { d: "M15.5,14H14.71L14.43,13.73C15.41,12.59 16,11.11 16,9.5A6.5,6.5 0 0,0 9.5,3A6.5,6.5 0 0,0 3,9.5A6.5,6.5 0 0,0 9.5,16C11.11,16 12.59,15.41 13.73,14.43L14,14.71V15.5L19,20.5L20.5,19L15.5,14M9.5,14C7,14 5,12 5,9.5C5,7 7,5 9.5,5C12,5 14,7 14,9.5C14,12 12,14 9.5,14M7,9H12V10H7V9Z" };
2560
+ const _hoisted_4$8 = { key: 0 };
2561
+ function _sfc_render$6(_ctx, _cache, $props, $setup, $data, $options) {
2562
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
2563
+ "aria-hidden": $props.title ? null : "true",
2564
+ "aria-label": $props.title,
2565
+ class: "material-design-icon magnify-minus-outline-icon",
2566
+ role: "img",
2567
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2568
+ }), [
2569
+ (openBlock(), createElementBlock("svg", {
2570
+ fill: $props.fillColor,
2571
+ class: "material-design-icon__svg",
2572
+ width: $props.size,
2573
+ height: $props.size,
2574
+ viewBox: "0 0 24 24"
2575
+ }, [
2576
+ createElementVNode("path", _hoisted_3$8, [
2577
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$8, toDisplayString($props.title), 1)) : createCommentVNode("", true)
2578
+ ])
2579
+ ], 8, _hoisted_2$8))
2580
+ ], 16, _hoisted_1$9);
2581
+ }
2582
+ const MagnifyMinusOutline = /* @__PURE__ */ _export_sfc(_sfc_main$a, [["render", _sfc_render$6]]);
2583
+ const _sfc_main$9 = {
2584
+ name: "MagnifyPlusOutlineIcon",
2585
+ emits: ["click"],
2586
+ props: {
2587
+ title: {
2588
+ type: String
2589
+ },
2590
+ fillColor: {
2591
+ type: String,
2592
+ default: "currentColor"
2593
+ },
2594
+ size: {
2595
+ type: Number,
2596
+ default: 24
2597
+ }
2598
+ }
2599
+ };
2600
+ const _hoisted_1$8 = ["aria-hidden", "aria-label"];
2601
+ const _hoisted_2$7 = ["fill", "width", "height"];
2602
+ const _hoisted_3$7 = { d: "M15.5,14L20.5,19L19,20.5L14,15.5V14.71L13.73,14.43C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.43,13.73L14.71,14H15.5M9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14M12,10H10V12H9V10H7V9H9V7H10V9H12V10Z" };
2603
+ const _hoisted_4$7 = { key: 0 };
2604
+ function _sfc_render$5(_ctx, _cache, $props, $setup, $data, $options) {
2605
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
2606
+ "aria-hidden": $props.title ? null : "true",
2607
+ "aria-label": $props.title,
2608
+ class: "material-design-icon magnify-plus-outline-icon",
2609
+ role: "img",
2610
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2611
+ }), [
2612
+ (openBlock(), createElementBlock("svg", {
2613
+ fill: $props.fillColor,
2614
+ class: "material-design-icon__svg",
2615
+ width: $props.size,
2616
+ height: $props.size,
2617
+ viewBox: "0 0 24 24"
2618
+ }, [
2619
+ createElementVNode("path", _hoisted_3$7, [
2620
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$7, toDisplayString($props.title), 1)) : createCommentVNode("", true)
2621
+ ])
2622
+ ], 8, _hoisted_2$7))
2623
+ ], 16, _hoisted_1$8);
2624
+ }
2625
+ const MagnifyPlusOutline = /* @__PURE__ */ _export_sfc(_sfc_main$9, [["render", _sfc_render$5]]);
2626
+ const _sfc_main$8 = {
2627
+ name: "RedoIcon",
2628
+ emits: ["click"],
2629
+ props: {
2630
+ title: {
2631
+ type: String
2632
+ },
2633
+ fillColor: {
2634
+ type: String,
2635
+ default: "currentColor"
2636
+ },
2637
+ size: {
2638
+ type: Number,
2639
+ default: 24
2640
+ }
2641
+ }
2642
+ };
2643
+ const _hoisted_1$7 = ["aria-hidden", "aria-label"];
2644
+ const _hoisted_2$6 = ["fill", "width", "height"];
2645
+ const _hoisted_3$6 = { d: "M18.4,10.6C16.55,9 14.15,8 11.5,8C6.85,8 2.92,11.03 1.54,15.22L3.9,16C4.95,12.81 7.95,10.5 11.5,10.5C13.45,10.5 15.23,11.22 16.62,12.38L13,16H22V7L18.4,10.6Z" };
2646
+ const _hoisted_4$6 = { key: 0 };
2647
+ function _sfc_render$4(_ctx, _cache, $props, $setup, $data, $options) {
2648
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
2649
+ "aria-hidden": $props.title ? null : "true",
2650
+ "aria-label": $props.title,
2651
+ class: "material-design-icon redo-icon",
2652
+ role: "img",
2653
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2654
+ }), [
2655
+ (openBlock(), createElementBlock("svg", {
2656
+ fill: $props.fillColor,
2657
+ class: "material-design-icon__svg",
2658
+ width: $props.size,
2659
+ height: $props.size,
2660
+ viewBox: "0 0 24 24"
2661
+ }, [
2662
+ createElementVNode("path", _hoisted_3$6, [
2663
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$6, toDisplayString($props.title), 1)) : createCommentVNode("", true)
2664
+ ])
2665
+ ], 8, _hoisted_2$6))
2666
+ ], 16, _hoisted_1$7);
2667
+ }
2668
+ const Redo = /* @__PURE__ */ _export_sfc(_sfc_main$8, [["render", _sfc_render$4]]);
2669
+ const _sfc_main$7 = {
2670
+ name: "RestoreIcon",
2671
+ emits: ["click"],
2672
+ props: {
2673
+ title: {
2674
+ type: String
2675
+ },
2676
+ fillColor: {
2677
+ type: String,
2678
+ default: "currentColor"
2679
+ },
2680
+ size: {
2681
+ type: Number,
2682
+ default: 24
2683
+ }
2684
+ }
2685
+ };
2686
+ const _hoisted_1$6 = ["aria-hidden", "aria-label"];
2687
+ const _hoisted_2$5 = ["fill", "width", "height"];
2688
+ const _hoisted_3$5 = { d: "M13,3A9,9 0 0,0 4,12H1L4.89,15.89L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3Z" };
2689
+ const _hoisted_4$5 = { key: 0 };
2690
+ function _sfc_render$3(_ctx, _cache, $props, $setup, $data, $options) {
2691
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
2692
+ "aria-hidden": $props.title ? null : "true",
2693
+ "aria-label": $props.title,
2694
+ class: "material-design-icon restore-icon",
2695
+ role: "img",
2696
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2697
+ }), [
2698
+ (openBlock(), createElementBlock("svg", {
2699
+ fill: $props.fillColor,
2700
+ class: "material-design-icon__svg",
2701
+ width: $props.size,
2702
+ height: $props.size,
2703
+ viewBox: "0 0 24 24"
2704
+ }, [
2705
+ createElementVNode("path", _hoisted_3$5, [
2706
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$5, toDisplayString($props.title), 1)) : createCommentVNode("", true)
2707
+ ])
2708
+ ], 8, _hoisted_2$5))
2709
+ ], 16, _hoisted_1$6);
2710
+ }
2711
+ const Restore = /* @__PURE__ */ _export_sfc(_sfc_main$7, [["render", _sfc_render$3]]);
2712
+ const _sfc_main$6 = {
2713
+ name: "UndoIcon",
2714
+ emits: ["click"],
2715
+ props: {
2716
+ title: {
2717
+ type: String
2718
+ },
2719
+ fillColor: {
2720
+ type: String,
2721
+ default: "currentColor"
2722
+ },
2723
+ size: {
2724
+ type: Number,
2725
+ default: 24
2726
+ }
2727
+ }
2728
+ };
2729
+ const _hoisted_1$5 = ["aria-hidden", "aria-label"];
2730
+ const _hoisted_2$4 = ["fill", "width", "height"];
2731
+ const _hoisted_3$4 = { d: "M12.5,8C9.85,8 7.45,9 5.6,10.6L2,7V16H11L7.38,12.38C8.77,11.22 10.54,10.5 12.5,10.5C16.04,10.5 19.05,12.81 20.1,16L22.47,15.22C21.08,11.03 17.15,8 12.5,8Z" };
2732
+ const _hoisted_4$4 = { key: 0 };
2733
+ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
2734
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
2735
+ "aria-hidden": $props.title ? null : "true",
2736
+ "aria-label": $props.title,
2737
+ class: "material-design-icon undo-icon",
2738
+ role: "img",
2739
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2740
+ }), [
2741
+ (openBlock(), createElementBlock("svg", {
2742
+ fill: $props.fillColor,
2743
+ class: "material-design-icon__svg",
2744
+ width: $props.size,
2745
+ height: $props.size,
2746
+ viewBox: "0 0 24 24"
2747
+ }, [
2748
+ createElementVNode("path", _hoisted_3$4, [
2749
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$4, toDisplayString($props.title), 1)) : createCommentVNode("", true)
2750
+ ])
2751
+ ], 8, _hoisted_2$4))
2752
+ ], 16, _hoisted_1$5);
2753
+ }
2754
+ const Undo = /* @__PURE__ */ _export_sfc(_sfc_main$6, [["render", _sfc_render$2]]);
2755
+ const _hoisted_1$4 = { class: "editor-topbar" };
2756
+ const _hoisted_2$3 = { class: "editor-topbar__history" };
2757
+ const _hoisted_3$3 = ["aria-label", "title", "disabled"];
2758
+ const _hoisted_4$3 = { class: "editor-topbar__actions" };
2759
+ const _sfc_main$5 = /* @__PURE__ */ defineComponent({
2760
+ __name: "EditorTopBar",
2761
+ props: {
2762
+ loaded: { type: Boolean }
2763
+ },
2764
+ emits: ["save", "cancel"],
2765
+ setup(__props, { emit: __emit }) {
2766
+ const emit = __emit;
2767
+ const context = useEditorContext();
2768
+ const commands = useEditorCommands();
2769
+ const labels = {
2770
+ undo: t("Undo"),
2771
+ redo: t("Redo"),
2772
+ revert: t("Revert all changes"),
2773
+ revertText: t("All edits will be discarded. This cannot be undone by closing the dialog."),
2774
+ zoomIn: t("Zoom in"),
2775
+ zoomOut: t("Zoom out"),
2776
+ resetZoom: t("Reset zoom"),
2777
+ save: t("Save"),
2778
+ cancel: t("Cancel"),
2779
+ history: t("Edit history"),
2780
+ step: t("Edit")
2781
+ };
2782
+ const historySteps = computed(() => context.historyEntries.value.map((entry, index) => ({
2783
+ index,
2784
+ label: entry.label ?? labels.step,
2785
+ active: index === context.historyIndex.value
2786
+ })).reverse());
2787
+ function stepZoom(direction) {
2788
+ const factor = direction === 1 ? 1.5 : 1 / 1.5;
2789
+ context.setViewZoom(context.viewZoom.value * factor);
2790
+ }
2791
+ function resetZoom() {
2792
+ context.setViewZoom(MIN_ZOOM);
2793
+ }
2794
+ async function onRevert() {
2795
+ if (await showConfirmation({
2796
+ name: labels.revert,
2797
+ text: labels.revertText,
2798
+ labelConfirm: labels.revert,
2799
+ labelReject: labels.cancel,
2800
+ severity: "warning"
2801
+ })) {
2802
+ commands.revert();
2803
+ }
2804
+ }
2805
+ return (_ctx, _cache) => {
2806
+ return openBlock(), createElementBlock("div", _hoisted_1$4, [
2807
+ _cache[9] || (_cache[9] = createElementVNode("span", { class: "editor-topbar__spacer" }, null, -1)),
2808
+ createElementVNode("div", _hoisted_2$3, [
2809
+ createVNode(unref(NcButton), {
2810
+ "data-test": "revert",
2811
+ "aria-label": labels.revert,
2812
+ title: labels.revert,
2813
+ disabled: !__props.loaded || !unref(context).canUndo.value,
2814
+ variant: "tertiary",
2815
+ onClick: onRevert
2816
+ }, {
2817
+ icon: withCtx(() => [
2818
+ createVNode(Restore, { size: 20 })
2819
+ ]),
2820
+ _: 1
2821
+ }, 8, ["aria-label", "title", "disabled"]),
2822
+ _cache[7] || (_cache[7] = createElementVNode("span", { class: "editor-topbar__separator" }, null, -1)),
2823
+ createVNode(unref(NcButton), {
2824
+ "aria-label": labels.undo,
2825
+ title: labels.undo,
2826
+ disabled: !__props.loaded || !unref(context).canUndo.value,
2827
+ variant: "tertiary",
2828
+ onClick: _cache[0] || (_cache[0] = ($event) => unref(context).undo())
2829
+ }, {
2830
+ icon: withCtx(() => [
2831
+ createVNode(Undo, { size: 20 })
2832
+ ]),
2833
+ _: 1
2834
+ }, 8, ["aria-label", "title", "disabled"]),
2835
+ createVNode(unref(NcButton), {
2836
+ "aria-label": labels.redo,
2837
+ title: labels.redo,
2838
+ disabled: !__props.loaded || !unref(context).canRedo.value,
2839
+ variant: "tertiary",
2840
+ onClick: _cache[1] || (_cache[1] = ($event) => unref(context).redo())
2841
+ }, {
2842
+ icon: withCtx(() => [
2843
+ createVNode(Redo, { size: 20 })
2844
+ ]),
2845
+ _: 1
2846
+ }, 8, ["aria-label", "title", "disabled"]),
2847
+ createVNode(unref(NcActions), {
2848
+ "aria-label": labels.history,
2849
+ title: labels.history,
2850
+ disabled: !__props.loaded,
2851
+ variant: "tertiary",
2852
+ "data-test": "history"
2853
+ }, {
2854
+ icon: withCtx(() => [
2855
+ createVNode(HistoryIcon, { size: 20 })
2856
+ ]),
2857
+ default: withCtx(() => [
2858
+ (openBlock(true), createElementBlock(Fragment, null, renderList(historySteps.value, (step) => {
2859
+ return openBlock(), createBlock(unref(NcActionButton), {
2860
+ key: step.index,
2861
+ "data-test": `history-step-${step.index}`,
2862
+ "aria-current": step.active,
2863
+ onClick: ($event) => unref(context).jumpTo(step.index)
2864
+ }, {
2865
+ icon: withCtx(() => [
2866
+ step.active ? (openBlock(), createBlock(Check, {
2867
+ key: 0,
2868
+ size: 20
2869
+ })) : createCommentVNode("", true)
2870
+ ]),
2871
+ default: withCtx(() => [
2872
+ createTextVNode(" " + toDisplayString(step.label), 1)
2873
+ ]),
2874
+ _: 2
2875
+ }, 1032, ["data-test", "aria-current", "onClick"]);
2876
+ }), 128))
2877
+ ]),
2878
+ _: 1
2879
+ }, 8, ["aria-label", "title", "disabled"]),
2880
+ _cache[8] || (_cache[8] = createElementVNode("span", { class: "editor-topbar__separator" }, null, -1)),
2881
+ createVNode(unref(NcButton), {
2882
+ "data-test": "zoom-out",
2883
+ "aria-label": labels.zoomOut,
2884
+ title: labels.zoomOut,
2885
+ disabled: !__props.loaded || unref(context).viewZoom.value <= unref(MIN_ZOOM),
2886
+ variant: "tertiary",
2887
+ onClick: _cache[2] || (_cache[2] = ($event) => stepZoom(-1))
2888
+ }, {
2889
+ icon: withCtx(() => [
2890
+ createVNode(MagnifyMinusOutline, { size: 20 })
2891
+ ]),
2892
+ _: 1
2893
+ }, 8, ["aria-label", "title", "disabled"]),
2894
+ createElementVNode("button", {
2895
+ type: "button",
2896
+ class: "editor-topbar__zoom",
2897
+ "data-test": "zoom-reset",
2898
+ "aria-label": labels.resetZoom,
2899
+ title: labels.resetZoom,
2900
+ disabled: !__props.loaded,
2901
+ onClick: resetZoom
2902
+ }, toDisplayString(Math.round(unref(context).viewZoom.value * 100)) + "% ", 9, _hoisted_3$3),
2903
+ createVNode(unref(NcButton), {
2904
+ "data-test": "zoom-in",
2905
+ "aria-label": labels.zoomIn,
2906
+ title: labels.zoomIn,
2907
+ disabled: !__props.loaded || unref(context).viewZoom.value >= unref(MAX_ZOOM),
2908
+ variant: "tertiary",
2909
+ onClick: _cache[3] || (_cache[3] = ($event) => stepZoom(1))
2910
+ }, {
2911
+ icon: withCtx(() => [
2912
+ createVNode(MagnifyPlusOutline, { size: 20 })
2913
+ ]),
2914
+ _: 1
2915
+ }, 8, ["aria-label", "title", "disabled"])
2916
+ ]),
2917
+ createElementVNode("div", _hoisted_4$3, [
2918
+ createVNode(unref(NcButton), {
2919
+ "data-test": "cancel",
2920
+ class: "editor-topbar__cancel-text",
2921
+ variant: "tertiary",
2922
+ onClick: _cache[4] || (_cache[4] = ($event) => emit("cancel"))
2923
+ }, {
2924
+ default: withCtx(() => [
2925
+ createTextVNode(toDisplayString(labels.cancel), 1)
2926
+ ]),
2927
+ _: 1
2928
+ }),
2929
+ createVNode(unref(NcButton), {
2930
+ "data-test": "cancel-icon",
2931
+ class: "editor-topbar__cancel-icon",
2932
+ "aria-label": labels.cancel,
2933
+ title: labels.cancel,
2934
+ variant: "tertiary",
2935
+ onClick: _cache[5] || (_cache[5] = ($event) => emit("cancel"))
2936
+ }, {
2937
+ icon: withCtx(() => [
2938
+ createVNode(Close, { size: 20 })
2939
+ ]),
2940
+ _: 1
2941
+ }, 8, ["aria-label", "title"]),
2942
+ createVNode(unref(NcButton), {
2943
+ "data-test": "save",
2944
+ variant: "primary",
2945
+ disabled: !__props.loaded,
2946
+ onClick: _cache[6] || (_cache[6] = ($event) => emit("save"))
2947
+ }, {
2948
+ default: withCtx(() => [
2949
+ createTextVNode(toDisplayString(labels.save), 1)
2950
+ ]),
2951
+ _: 1
2952
+ }, 8, ["disabled"])
2953
+ ])
2954
+ ]);
2955
+ };
2956
+ }
2957
+ });
2958
+ const EditorTopBar = /* @__PURE__ */ _export_sfc(_sfc_main$5, [["__scopeId", "data-v-9ba9bd23"]]);
2959
+ const _sfc_main$4 = {
2960
+ name: "ContentCopyIcon",
2961
+ emits: ["click"],
2962
+ props: {
2963
+ title: {
2964
+ type: String
2965
+ },
2966
+ fillColor: {
2967
+ type: String,
2968
+ default: "currentColor"
2969
+ },
2970
+ size: {
2971
+ type: Number,
2972
+ default: 24
2973
+ }
2974
+ }
2975
+ };
2976
+ const _hoisted_1$3 = ["aria-hidden", "aria-label"];
2977
+ const _hoisted_2$2 = ["fill", "width", "height"];
2978
+ const _hoisted_3$2 = { d: "M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z" };
2979
+ const _hoisted_4$2 = { key: 0 };
2980
+ function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
2981
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
2982
+ "aria-hidden": $props.title ? null : "true",
2983
+ "aria-label": $props.title,
2984
+ class: "material-design-icon content-copy-icon",
2985
+ role: "img",
2986
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
2987
+ }), [
2988
+ (openBlock(), createElementBlock("svg", {
2989
+ fill: $props.fillColor,
2990
+ class: "material-design-icon__svg",
2991
+ width: $props.size,
2992
+ height: $props.size,
2993
+ viewBox: "0 0 24 24"
2994
+ }, [
2995
+ createElementVNode("path", _hoisted_3$2, [
2996
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$2, toDisplayString($props.title), 1)) : createCommentVNode("", true)
2997
+ ])
2998
+ ], 8, _hoisted_2$2))
2999
+ ], 16, _hoisted_1$3);
3000
+ }
3001
+ const ContentCopy = /* @__PURE__ */ _export_sfc(_sfc_main$4, [["render", _sfc_render$1]]);
3002
+ const _sfc_main$3 = {
3003
+ name: "DeleteIcon",
3004
+ emits: ["click"],
3005
+ props: {
3006
+ title: {
3007
+ type: String
3008
+ },
3009
+ fillColor: {
3010
+ type: String,
3011
+ default: "currentColor"
3012
+ },
3013
+ size: {
3014
+ type: Number,
3015
+ default: 24
3016
+ }
3017
+ }
3018
+ };
3019
+ const _hoisted_1$2 = ["aria-hidden", "aria-label"];
3020
+ const _hoisted_2$1 = ["fill", "width", "height"];
3021
+ const _hoisted_3$1 = { d: "M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" };
3022
+ const _hoisted_4$1 = { key: 0 };
3023
+ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
3024
+ return openBlock(), createElementBlock("span", mergeProps(_ctx.$attrs, {
3025
+ "aria-hidden": $props.title ? null : "true",
3026
+ "aria-label": $props.title,
3027
+ class: "material-design-icon delete-icon",
3028
+ role: "img",
3029
+ onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("click", $event))
3030
+ }), [
3031
+ (openBlock(), createElementBlock("svg", {
3032
+ fill: $props.fillColor,
3033
+ class: "material-design-icon__svg",
3034
+ width: $props.size,
3035
+ height: $props.size,
3036
+ viewBox: "0 0 24 24"
3037
+ }, [
3038
+ createElementVNode("path", _hoisted_3$1, [
3039
+ $props.title ? (openBlock(), createElementBlock("title", _hoisted_4$1, toDisplayString($props.title), 1)) : createCommentVNode("", true)
3040
+ ])
3041
+ ], 8, _hoisted_2$1))
3042
+ ], 16, _hoisted_1$2);
3043
+ }
3044
+ const Delete = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["render", _sfc_render]]);
3045
+ const _sfc_main$2 = /* @__PURE__ */ defineComponent({
3046
+ __name: "SelectionToolbar",
3047
+ props: {
3048
+ box: {}
3049
+ },
3050
+ emits: ["duplicate", "delete"],
3051
+ setup(__props, { emit: __emit }) {
3052
+ const emit = __emit;
3053
+ const duplicateLabel = t("Duplicate");
3054
+ const deleteLabel = t("Delete");
3055
+ return (_ctx, _cache) => {
3056
+ return openBlock(), createBlock(GlassSurface, {
3057
+ variant: "pill",
3058
+ class: "selection-toolbar",
3059
+ "data-test": "selection-toolbar",
3060
+ style: normalizeStyle({
3061
+ insetInlineStart: `${__props.box.x + __props.box.width / 2}px`,
3062
+ // Above the selection as designed, clearing the rotate handle;
3063
+ // below only when the top edge would clip it
3064
+ insetBlockStart: __props.box.y - 76 >= 4 ? `${__props.box.y - 76}px` : `${__props.box.y + __props.box.height + 12}px`
3065
+ })
3066
+ }, {
3067
+ default: withCtx(() => [
3068
+ createVNode(unref(NcButton), {
3069
+ "data-test": "duplicate",
3070
+ "aria-label": unref(duplicateLabel),
3071
+ title: unref(duplicateLabel),
3072
+ variant: "tertiary",
3073
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("duplicate"))
3074
+ }, {
3075
+ icon: withCtx(() => [
3076
+ createVNode(ContentCopy, { size: 18 })
3077
+ ]),
3078
+ _: 1
3079
+ }, 8, ["aria-label", "title"]),
3080
+ createVNode(unref(NcButton), {
3081
+ "data-test": "delete",
3082
+ "aria-label": unref(deleteLabel),
3083
+ title: unref(deleteLabel),
3084
+ variant: "tertiary",
3085
+ onClick: _cache[1] || (_cache[1] = ($event) => emit("delete"))
3086
+ }, {
3087
+ icon: withCtx(() => [
3088
+ createVNode(Delete, { size: 18 })
3089
+ ]),
3090
+ _: 1
3091
+ }, 8, ["aria-label", "title"])
3092
+ ]),
3093
+ _: 1
3094
+ }, 8, ["style"]);
3095
+ };
3096
+ }
3097
+ });
3098
+ const SelectionToolbar = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-09c1d16e"]]);
3099
+ const _hoisted_1$1 = ["aria-label"];
3100
+ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
3101
+ __name: "TextOverlay",
3102
+ props: {
3103
+ x: {},
3104
+ y: {},
3105
+ fontSize: {},
3106
+ color: {},
3107
+ initial: {}
3108
+ },
3109
+ emits: ["confirm", "cancel"],
3110
+ setup(__props, { emit: __emit }) {
3111
+ const props = __props;
3112
+ const emit = __emit;
3113
+ const inputLabel = t("Annotation text");
3114
+ const value = ref(props.initial);
3115
+ const input = useTemplateRef("input");
3116
+ function autosize() {
3117
+ const element = input.value;
3118
+ if (element === null) {
3119
+ return;
3120
+ }
3121
+ element.style.width = "0";
3122
+ element.style.height = "0";
3123
+ element.style.width = `${Math.max(element.scrollWidth + 4, props.fontSize * 2)}px`;
3124
+ element.style.height = `${Math.max(element.scrollHeight, props.fontSize * 1.2)}px`;
3125
+ }
3126
+ onMounted(() => {
3127
+ autosize();
3128
+ input.value.focus();
3129
+ input.value.select();
3130
+ });
3131
+ function onEnter(event) {
3132
+ if (!event.shiftKey) {
3133
+ event.preventDefault();
3134
+ emit("confirm", value.value);
3135
+ }
3136
+ }
3137
+ return (_ctx, _cache) => {
3138
+ return withDirectives((openBlock(), createElementBlock("textarea", {
3139
+ ref_key: "input",
3140
+ ref: input,
3141
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => value.value = $event),
3142
+ class: "text-overlay",
3143
+ "aria-label": unref(inputLabel),
3144
+ "data-test": "text-overlay",
3145
+ style: normalizeStyle({
3146
+ insetInlineStart: `${__props.x}px`,
3147
+ insetBlockStart: `${__props.y}px`,
3148
+ fontSize: `${__props.fontSize}px`,
3149
+ color: __props.color
3150
+ }),
3151
+ rows: "1",
3152
+ onInput: autosize,
3153
+ onKeydown: [
3154
+ withKeys(onEnter, ["enter"]),
3155
+ _cache[1] || (_cache[1] = withKeys(withModifiers(($event) => emit("cancel"), ["stop"]), ["esc"]))
3156
+ ],
3157
+ onBlur: _cache[2] || (_cache[2] = ($event) => emit("confirm", value.value))
3158
+ }, null, 44, _hoisted_1$1)), [
3159
+ [vModelText, value.value]
3160
+ ]);
3161
+ };
3162
+ }
3163
+ });
3164
+ const TextOverlay = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-bbba034b"]]);
3165
+ function primaryColor() {
3166
+ const value = getComputedStyle(document.body).getPropertyValue("--color-primary-element").trim();
3167
+ return value !== "" ? value : "#0082c9";
3168
+ }
3169
+ function ambientColor(canvas) {
3170
+ const sample = document.createElement("canvas");
3171
+ sample.width = 8;
3172
+ sample.height = 8;
3173
+ const context = sample.getContext("2d");
3174
+ if (context === null) {
3175
+ return "88, 86, 112";
3176
+ }
3177
+ context.drawImage(canvas, 0, 0, 8, 8);
3178
+ let data;
3179
+ try {
3180
+ data = context.getImageData(0, 0, 8, 8).data;
3181
+ } catch {
3182
+ return "88, 86, 112";
3183
+ }
3184
+ let r = 0;
3185
+ let g = 0;
3186
+ let b = 0;
3187
+ let total = 0;
3188
+ for (let i = 0; i < data.length; i += 4) {
3189
+ const saturation = Math.max(data[i], data[i + 1], data[i + 2]) - Math.min(data[i], data[i + 1], data[i + 2]);
3190
+ const weight = saturation + 8;
3191
+ r += data[i] * weight;
3192
+ g += data[i + 1] * weight;
3193
+ b += data[i + 2] * weight;
3194
+ total += weight;
3195
+ }
3196
+ return `${Math.round(r / total)}, ${Math.round(g / total)}, ${Math.round(b / total)}`;
3197
+ }
3198
+ function ambientBackdrop(canvas) {
3199
+ const sample = document.createElement("canvas");
3200
+ const width = canvas instanceof HTMLImageElement ? canvas.naturalWidth : canvas.width;
3201
+ const height = canvas instanceof HTMLImageElement ? canvas.naturalHeight : canvas.height;
3202
+ if (width === 0 || height === 0) {
3203
+ return "";
3204
+ }
3205
+ sample.width = 24;
3206
+ sample.height = Math.max(1, Math.round(24 * height / width));
3207
+ const context = sample.getContext("2d");
3208
+ if (context === null) {
3209
+ return "";
3210
+ }
3211
+ context.drawImage(canvas, 0, 0, sample.width, sample.height);
3212
+ try {
3213
+ return sample.toDataURL();
3214
+ } catch {
3215
+ return "";
3216
+ }
3217
+ }
3218
+ function useAmbient(source) {
3219
+ const ambient = ref("88, 86, 112");
3220
+ const backdrop = ref("");
3221
+ watch(source, (image) => {
3222
+ if (image !== null) {
3223
+ ambient.value = ambientColor(image);
3224
+ backdrop.value = ambientBackdrop(image);
3225
+ }
3226
+ });
3227
+ return { ambient, backdrop };
3228
+ }
3229
+ function useAnnouncements(context) {
3230
+ const message = ref("");
3231
+ const modeLabels = {
3232
+ select: t("Select"),
3233
+ crop: t("Crop"),
3234
+ finetune: t("Adjust"),
3235
+ filter: t("Filter"),
3236
+ annotate: t("Annotate"),
3237
+ sticker: t("Sticker"),
3238
+ redact: t("Redact")
3239
+ };
3240
+ watch(context.activeMode, (mode) => {
3241
+ message.value = t("{mode} mode", { mode: modeLabels[mode] ?? mode });
3242
+ });
3243
+ let annotationCount = 0;
3244
+ watch(context.state, (state) => {
3245
+ if (state.annotations.length > annotationCount) {
3246
+ message.value = t("Annotation added");
3247
+ } else if (state.annotations.length < annotationCount) {
3248
+ message.value = t("Annotation removed");
3249
+ }
3250
+ annotationCount = state.annotations.length;
3251
+ });
3252
+ watch(() => context.state.value.preset, (preset) => {
3253
+ message.value = preset === "none" ? t("Filter removed") : t("Filter applied");
3254
+ });
3255
+ return message;
3256
+ }
3257
+ const TEXT_ENTRY = "input, textarea, select, [contenteditable]";
3258
+ const ACTIVATABLE = 'button, [role="button"], a[href], summary';
3259
+ function ownsTextEntry(target) {
3260
+ return target instanceof HTMLElement && target.closest(TEXT_ENTRY) !== null;
3261
+ }
3262
+ function ownsSpaceKey(target) {
3263
+ return target instanceof HTMLElement && target.closest(`${TEXT_ENTRY}, ${ACTIVATABLE}`) !== null;
3264
+ }
3265
+ const NUDGE_KEYS = {
3266
+ ArrowLeft: [-1, 0],
3267
+ ArrowRight: [1, 0],
3268
+ ArrowUp: [0, -1],
3269
+ ArrowDown: [0, 1]
3270
+ };
3271
+ function useEditorShortcuts(deps) {
3272
+ const { context } = deps;
3273
+ let nudging = false;
3274
+ function nudgeSelection(event) {
3275
+ const id = context.selectedId.value;
3276
+ const direction = NUDGE_KEYS[event.key];
3277
+ if (id === null || direction === void 0) {
3278
+ return;
3279
+ }
3280
+ event.preventDefault();
3281
+ const step = event.shiftKey ? 10 : 1;
3282
+ const state = context.state.value;
3283
+ nudging = true;
3284
+ context.preview({
3285
+ ...state,
3286
+ annotations: state.annotations.map((annotation) => annotation.id === id ? translateAnnotation(annotation, direction[0] * step, direction[1] * step) : annotation)
3287
+ });
3288
+ }
3289
+ function onKeydown(event) {
3290
+ if (deps.isTextEditing() || ownsTextEntry(event.target)) {
3291
+ return;
3292
+ }
3293
+ const meta = event.ctrlKey || event.metaKey;
3294
+ if (meta && !event.shiftKey && event.key.toLowerCase() === "z") {
3295
+ event.preventDefault();
3296
+ context.undo();
3297
+ return;
3298
+ }
3299
+ if (meta && event.shiftKey && event.key.toLowerCase() === "z" || meta && event.key.toLowerCase() === "y") {
3300
+ event.preventDefault();
3301
+ context.redo();
3302
+ return;
3303
+ }
3304
+ if (event.key === "+" || event.key === "=") {
3305
+ context.setViewZoom(context.viewZoom.value * 1.25);
3306
+ return;
3307
+ }
3308
+ if (event.key === "-") {
3309
+ context.setViewZoom(context.viewZoom.value / 1.25);
3310
+ return;
3311
+ }
3312
+ if (event.key in NUDGE_KEYS && context.selectedId.value !== null) {
3313
+ nudgeSelection(event);
3314
+ return;
3315
+ }
3316
+ if ((event.key === "Delete" || event.key === "Backspace") && context.selectedId.value !== null) {
3317
+ event.preventDefault();
3318
+ deps.onDelete();
3319
+ } else if (event.key === "Escape") {
3320
+ deps.onEscape();
3321
+ }
3322
+ }
3323
+ function onKeyup(event) {
3324
+ if (ownsTextEntry(event.target)) {
3325
+ return;
3326
+ }
3327
+ if (nudging && event.key in NUDGE_KEYS) {
3328
+ nudging = false;
3329
+ context.commit(context.state.value, t("Move or resize"));
3330
+ }
3331
+ }
3332
+ onMounted(() => {
3333
+ window.addEventListener("keydown", onKeydown);
3334
+ window.addEventListener("keyup", onKeyup);
3335
+ });
3336
+ onBeforeUnmount(() => {
3337
+ window.removeEventListener("keydown", onKeydown);
3338
+ window.removeEventListener("keyup", onKeyup);
3339
+ });
3340
+ }
3341
+ function needsCors(url) {
3342
+ let target;
3343
+ try {
3344
+ target = new URL(url, window.location.href);
3345
+ } catch {
3346
+ return false;
3347
+ }
3348
+ return (target.protocol === "http:" || target.protocol === "https:") && target.origin !== window.location.origin;
3349
+ }
3350
+ async function loadImage(source) {
3351
+ const url = typeof source === "string" ? source : URL.createObjectURL(source);
3352
+ try {
3353
+ return await new Promise((resolve, reject) => {
3354
+ const image = new Image();
3355
+ if (needsCors(url)) {
3356
+ image.crossOrigin = "anonymous";
3357
+ }
3358
+ image.onload = () => resolve(image);
3359
+ image.onerror = () => reject(new Error(t("Image could not be decoded")));
3360
+ image.src = url;
3361
+ });
3362
+ } finally {
3363
+ if (typeof source !== "string") {
3364
+ URL.revokeObjectURL(url);
3365
+ }
3366
+ }
3367
+ }
3368
+ async function canvasToBlob(canvas, type = "image/png", quality) {
3369
+ return new Promise((resolve, reject) => {
3370
+ canvas.toBlob((blob) => {
3371
+ if (blob === null) {
3372
+ reject(new Error(t("Canvas could not be encoded")));
3373
+ return;
3374
+ }
3375
+ resolve(blob);
3376
+ }, type, quality);
3377
+ });
3378
+ }
3379
+ function useExportImage(deps) {
3380
+ async function exportImage(options = {}) {
3381
+ const oriented = deps.oriented();
3382
+ if (oriented === null) {
3383
+ throw new Error("No image loaded");
3384
+ }
3385
+ const source = deps.source();
3386
+ if (source !== null && source.type !== "" && isPristine(deps.getState()) && options.maxSize === void 0 && (options.format === void 0 || options.format === source.type)) {
3387
+ return { blob: source, width: oriented.width, height: oriented.height, mimeType: source.type };
3388
+ }
3389
+ const canvas = renderToCanvas(oriented, deps.getState(), options.maxSize);
3390
+ const mimeType = options.format ?? "image/png";
3391
+ try {
3392
+ const blob = await canvasToBlob(canvas, mimeType, options.quality);
3393
+ return { blob, width: canvas.width, height: canvas.height, mimeType };
3394
+ } catch (error) {
3395
+ if (error instanceof DOMException && error.name === "SecurityError") {
3396
+ throw new Error(
3397
+ t("The image cannot be exported because it was loaded without cross-origin access"),
3398
+ { cause: error }
3399
+ );
3400
+ }
3401
+ throw error;
3402
+ }
3403
+ }
3404
+ async function save() {
3405
+ try {
3406
+ deps.onSaved(await exportImage(deps.saveOptions()));
3407
+ } catch (error) {
3408
+ deps.onError(error instanceof Error ? error : new Error(String(error)));
3409
+ }
3410
+ }
3411
+ return { exportImage, save };
3412
+ }
3413
+ function useTextEditing(deps) {
3414
+ const { context } = deps;
3415
+ const textEdit = ref(null);
3416
+ function startTextEdit(position, existing) {
3417
+ const options = deps.viewOptions();
3418
+ const oriented = deps.oriented();
3419
+ if (options === null || oriented === null) {
3420
+ return;
3421
+ }
3422
+ const origin = options.showCropped ? visibleRect(context.state.value, { width: oriented.width, height: oriented.height }) : { x: 0, y: 0 };
3423
+ textEdit.value = {
3424
+ sceneX: position.x,
3425
+ sceneY: position.y,
3426
+ screenX: options.offset.x + (position.x - origin.x) * options.scale,
3427
+ screenY: options.offset.y + (position.y - origin.y) * options.scale,
3428
+ screenFontSize: (existing?.fontSize ?? context.fontSize.value) * options.scale,
3429
+ color: existing?.color ?? context.drawColor.value,
3430
+ value: existing?.text ?? "",
3431
+ id: existing?.id ?? null
3432
+ };
3433
+ }
3434
+ function confirmTextEdit(text) {
3435
+ const edit = textEdit.value;
3436
+ textEdit.value = null;
3437
+ if (edit === null) {
3438
+ return;
3439
+ }
3440
+ const state = context.state.value;
3441
+ const trimmed = text.trim();
3442
+ if (edit.id !== null) {
3443
+ context.commit({
3444
+ ...state,
3445
+ annotations: trimmed === "" ? state.annotations.filter((annotation) => annotation.id !== edit.id) : state.annotations.map((annotation) => annotation.id === edit.id ? { ...annotation, text: trimmed } : annotation)
3446
+ }, trimmed === "" ? t("Delete") : t("Text"));
3447
+ } else if (trimmed !== "") {
3448
+ context.commit({
3449
+ ...state,
3450
+ annotations: [...state.annotations, {
3451
+ id: newId(),
3452
+ type: "text",
3453
+ x: edit.sceneX,
3454
+ y: edit.sceneY,
3455
+ text: trimmed,
3456
+ color: edit.color,
3457
+ fontSize: context.fontSize.value,
3458
+ rotation: 0
3459
+ }]
3460
+ }, t("Text"));
3461
+ }
3462
+ }
3463
+ return { textEdit, startTextEdit, confirmTextEdit };
3464
+ }
3465
+ function useWheelControls(element, context) {
3466
+ let dragFrom = null;
3467
+ let pinch = null;
3468
+ const pointers = /* @__PURE__ */ new Map();
3469
+ const spaceHeld = shallowRef(false);
3470
+ const toolFreeDrag = () => context.activeTool.value === "adjust";
3471
+ const zoomed = () => context.viewZoom.value > MIN_ZOOM;
3472
+ const panArmed = computed(() => context.viewZoom.value > MIN_ZOOM && (spaceHeld.value || context.activeTool.value === "adjust"));
3473
+ function anchorAt(position) {
3474
+ const rect = element.value?.getBoundingClientRect();
3475
+ if (rect === void 0) {
3476
+ return void 0;
3477
+ }
3478
+ return {
3479
+ x: position.x - rect.x - rect.width / 2,
3480
+ y: position.y - rect.y - rect.height / 2
3481
+ };
3482
+ }
3483
+ function currentPinch() {
3484
+ if (pointers.size !== 2) {
3485
+ return null;
3486
+ }
3487
+ const [a, b] = [...pointers.values()];
3488
+ return {
3489
+ distance: Math.hypot(a.x - b.x, a.y - b.y),
3490
+ centroid: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }
3491
+ };
3492
+ }
3493
+ function onWheel(event) {
3494
+ event.preventDefault();
3495
+ if (event.deltaY === 0) {
3496
+ return;
3497
+ }
3498
+ const height = element.value?.clientHeight ?? window.innerHeight;
3499
+ const factor = wheelZoomFactor(event.deltaY, event.deltaMode, height);
3500
+ context.setViewZoom(context.viewZoom.value * factor, anchorAt({ x: event.clientX, y: event.clientY }));
3501
+ }
3502
+ function onPointerDown(event) {
3503
+ if (event.pointerType === "touch") {
3504
+ pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
3505
+ if (pointers.size === 2) {
3506
+ pinch = currentPinch();
3507
+ context.panning.value = true;
3508
+ event.stopPropagation();
3509
+ }
3510
+ return;
3511
+ }
3512
+ const wantsPan = event.button === 1 || event.button === 0 && (spaceHeld.value || toolFreeDrag());
3513
+ if (!wantsPan || !zoomed()) {
3514
+ return;
3515
+ }
3516
+ event.preventDefault();
3517
+ event.stopPropagation();
3518
+ dragFrom = {
3519
+ pointer: { x: event.clientX, y: event.clientY },
3520
+ pan: context.viewPan.value
3521
+ };
3522
+ context.panning.value = true;
3523
+ element.value?.setPointerCapture(event.pointerId);
3524
+ }
3525
+ function onPointerMove(event) {
3526
+ if (event.pointerType === "touch" && pointers.has(event.pointerId)) {
3527
+ pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
3528
+ const next = currentPinch();
3529
+ if (next === null || pinch === null) {
3530
+ return;
3531
+ }
3532
+ event.stopPropagation();
3533
+ const ratio = next.distance / pinch.distance;
3534
+ if (Math.abs(ratio - 1) > PINCH_TOLERANCE) {
3535
+ context.setViewZoom(context.viewZoom.value * ratio, anchorAt(next.centroid));
3536
+ } else {
3537
+ context.setViewPan({
3538
+ x: context.viewPan.value.x + next.centroid.x - pinch.centroid.x,
3539
+ y: context.viewPan.value.y + next.centroid.y - pinch.centroid.y
3540
+ });
3541
+ }
3542
+ pinch = next;
3543
+ return;
3544
+ }
3545
+ if (dragFrom === null) {
3546
+ return;
3547
+ }
3548
+ event.stopPropagation();
3549
+ context.setViewPan({
3550
+ x: dragFrom.pan.x + (event.clientX - dragFrom.pointer.x),
3551
+ y: dragFrom.pan.y + (event.clientY - dragFrom.pointer.y)
3552
+ });
3553
+ }
3554
+ function onPointerUp(event) {
3555
+ if (event !== void 0) {
3556
+ pointers.delete(event.pointerId);
3557
+ }
3558
+ pinch = currentPinch();
3559
+ dragFrom = null;
3560
+ if (pointers.size < 2) {
3561
+ context.panning.value = false;
3562
+ }
3563
+ }
3564
+ function onKeydown(event) {
3565
+ if (event.code !== "Space" || ownsSpaceKey(event.target)) {
3566
+ return;
3567
+ }
3568
+ event.preventDefault();
3569
+ spaceHeld.value = true;
3570
+ }
3571
+ function onKeyup(event) {
3572
+ if (event.code === "Space") {
3573
+ spaceHeld.value = false;
3574
+ }
3575
+ }
3576
+ onMounted(() => {
3577
+ const target = element.value;
3578
+ target?.addEventListener("wheel", onWheel, { passive: false });
3579
+ target?.addEventListener("pointerdown", onPointerDown, { capture: true });
3580
+ target?.addEventListener("pointermove", onPointerMove, { capture: true });
3581
+ target?.addEventListener("pointerup", onPointerUp);
3582
+ target?.addEventListener("pointercancel", onPointerUp);
3583
+ window.addEventListener("keydown", onKeydown);
3584
+ window.addEventListener("keyup", onKeyup);
3585
+ });
3586
+ onBeforeUnmount(() => {
3587
+ const target = element.value;
3588
+ target?.removeEventListener("wheel", onWheel);
3589
+ target?.removeEventListener("pointerdown", onPointerDown, { capture: true });
3590
+ target?.removeEventListener("pointermove", onPointerMove, { capture: true });
3591
+ target?.removeEventListener("pointerup", onPointerUp);
3592
+ target?.removeEventListener("pointercancel", onPointerUp);
3593
+ window.removeEventListener("keydown", onKeydown);
3594
+ window.removeEventListener("keyup", onKeyup);
3595
+ });
3596
+ return { panArmed };
3597
+ }
3598
+ const DURATION = 0.3;
3599
+ function prefersReducedMotion() {
3600
+ return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
3601
+ }
3602
+ function pivotOnCenter(target) {
3603
+ target.node.offset(target.pivot);
3604
+ target.node.position(target.pivot);
3605
+ }
3606
+ function playTransition(kind, deps, context) {
3607
+ if (prefersReducedMotion()) {
3608
+ return;
3609
+ }
3610
+ const { scale } = deps;
3611
+ const easing = Konva.Easings.EaseInOut;
3612
+ const ratio = context.previousScale / scale;
3613
+ for (const target of deps.targets) {
3614
+ const { node } = target;
3615
+ node.setAttrs({ x: 0, y: 0, offsetX: 0, offsetY: 0, rotation: 0, scaleX: 1, scaleY: 1, opacity: 1 });
3616
+ switch (kind) {
3617
+ case "load":
3618
+ pivotOnCenter(target);
3619
+ node.opacity(0);
3620
+ node.scale({ x: 0.96, y: 0.96 });
3621
+ node.to({ opacity: 1, scaleX: 1, scaleY: 1, duration: DURATION, easing });
3622
+ break;
3623
+ case "rotate-cw":
3624
+ case "rotate-ccw":
3625
+ pivotOnCenter(target);
3626
+ node.rotation(kind === "rotate-cw" ? -90 : 90);
3627
+ node.scale({ x: ratio, y: ratio });
3628
+ node.to({ rotation: 0, scaleX: 1, scaleY: 1, duration: DURATION, easing });
3629
+ break;
3630
+ case "flip-h":
3631
+ pivotOnCenter(target);
3632
+ node.scaleX(-1);
3633
+ node.to({ scaleX: 1, duration: DURATION, easing });
3634
+ break;
3635
+ case "flip-v":
3636
+ pivotOnCenter(target);
3637
+ node.scaleY(-1);
3638
+ node.to({ scaleY: 1, duration: DURATION, easing });
3639
+ break;
3640
+ case "crop": {
3641
+ const { previousScale, previousOffset, previousOrigin } = context;
3642
+ node.scale({ x: ratio, y: ratio });
3643
+ node.position({
3644
+ x: (previousOffset.x - previousOrigin.x * previousScale - (deps.offset.x - deps.origin.x * scale)) * target.unit,
3645
+ y: (previousOffset.y - previousOrigin.y * previousScale - (deps.offset.y - deps.origin.y * scale)) * target.unit
3646
+ });
3647
+ node.to({ x: 0, y: 0, scaleX: 1, scaleY: 1, duration: DURATION, easing });
3648
+ break;
3649
+ }
3650
+ }
3651
+ }
3652
+ }
3653
+ function clampCropBox(bounds, oldBox, newBox, keepRatio) {
3654
+ const right = bounds.x + bounds.width;
3655
+ const bottom = bounds.y + bounds.height;
3656
+ let { x, y, width, height } = newBox;
3657
+ if (x < bounds.x) {
3658
+ width -= bounds.x - x;
3659
+ x = bounds.x;
3660
+ }
3661
+ if (y < bounds.y) {
3662
+ height -= bounds.y - y;
3663
+ y = bounds.y;
3664
+ }
3665
+ width = Math.min(width, right - x);
3666
+ height = Math.min(height, bottom - y);
3667
+ if (keepRatio && oldBox.height > 0) {
3668
+ const ratio = oldBox.width / oldBox.height;
3669
+ if (width / height > ratio) {
3670
+ width = height * ratio;
3671
+ } else {
3672
+ height = width / ratio;
3673
+ }
3674
+ if (Math.abs(newBox.x - oldBox.x) > 0.01) {
3675
+ x = Math.min(newBox.x + newBox.width, right) - width;
3676
+ }
3677
+ if (Math.abs(newBox.y - oldBox.y) > 0.01) {
3678
+ y = Math.min(newBox.y + newBox.height, bottom) - height;
3679
+ }
3680
+ }
3681
+ return {
3682
+ ...newBox,
3683
+ x,
3684
+ y,
3685
+ width: Math.max(8, width),
3686
+ height: Math.max(8, height)
3687
+ };
3688
+ }
3689
+ function attachCropOverlay(deps) {
3690
+ const view = { oriented: deps.oriented, scale: deps.scale, offset: deps.offset };
3691
+ const toStage = (rect) => ({
3692
+ x: view.offset.x + rect.x * view.scale,
3693
+ y: view.offset.y + rect.y * view.scale,
3694
+ width: rect.width * view.scale,
3695
+ height: rect.height * view.scale
3696
+ });
3697
+ const toScene = (rect) => ({
3698
+ x: (rect.x - view.offset.x) / view.scale,
3699
+ y: (rect.y - view.offset.y) / view.scale,
3700
+ width: rect.width / view.scale,
3701
+ height: rect.height / view.scale
3702
+ });
3703
+ let imageBounds = toStage({ x: 0, y: 0, ...view.oriented });
3704
+ const layer = new Konva.Layer({ name: "crop" });
3705
+ const makeShade = () => new Konva.Rect({
3706
+ fill: "rgba(0, 0, 0, 0.5)",
3707
+ listening: false
3708
+ });
3709
+ const shadeTop = makeShade();
3710
+ const shadeLeft = makeShade();
3711
+ const shadeRight = makeShade();
3712
+ const shadeBottom = makeShade();
3713
+ for (const shade of [shadeTop, shadeLeft, shadeRight, shadeBottom]) {
3714
+ layer.add(shade);
3715
+ }
3716
+ const cropNode = new Konva.Rect({
3717
+ // Named so it can be told apart from the four shade rects
3718
+ name: "crop-rect",
3719
+ ...toStage(clampRect(deps.initial ?? { x: 0, y: 0, ...view.oriented }, view.oriented)),
3720
+ stroke: "rgba(255, 255, 255, 0.9)",
3721
+ strokeWidth: 1,
3722
+ draggable: true,
3723
+ strokeScaleEnabled: false
3724
+ });
3725
+ layer.add(cropNode);
3726
+ const stageRect = () => ({
3727
+ x: cropNode.x(),
3728
+ y: cropNode.y(),
3729
+ width: cropNode.width() * cropNode.scaleX(),
3730
+ height: cropNode.height() * cropNode.scaleY()
3731
+ });
3732
+ const gridLines = Array.from({ length: 4 }, () => new Konva.Line({
3733
+ stroke: "rgba(255, 255, 255, 0.35)",
3734
+ strokeWidth: 1,
3735
+ listening: false
3736
+ }));
3737
+ for (const line of gridLines) {
3738
+ layer.add(line);
3739
+ }
3740
+ const clampToImage = (rect) => {
3741
+ const x = Math.min(Math.max(rect.x, imageBounds.x), imageBounds.x + imageBounds.width - rect.width);
3742
+ const y = Math.min(Math.max(rect.y, imageBounds.y), imageBounds.y + imageBounds.height - rect.height);
3743
+ return { ...rect, x, y };
3744
+ };
3745
+ const updateOverlay = () => {
3746
+ const rect = stageRect();
3747
+ const { x, y, width, height } = imageBounds;
3748
+ shadeTop.setAttrs({ x, y, width, height: rect.y - y });
3749
+ shadeLeft.setAttrs({ x, y: rect.y, width: rect.x - x, height: rect.height });
3750
+ shadeRight.setAttrs({ x: rect.x + rect.width, y: rect.y, width: x + width - rect.x - rect.width, height: rect.height });
3751
+ shadeBottom.setAttrs({ x, y: rect.y + rect.height, width, height: y + height - rect.y - rect.height });
3752
+ gridLines[0].points([rect.x + rect.width / 3, rect.y, rect.x + rect.width / 3, rect.y + rect.height]);
3753
+ gridLines[1].points([rect.x + rect.width * 2 / 3, rect.y, rect.x + rect.width * 2 / 3, rect.y + rect.height]);
3754
+ gridLines[2].points([rect.x, rect.y + rect.height / 3, rect.x + rect.width, rect.y + rect.height / 3]);
3755
+ gridLines[3].points([rect.x, rect.y + rect.height * 2 / 3, rect.x + rect.width, rect.y + rect.height * 2 / 3]);
3756
+ };
3757
+ let ratioLocked = false;
3758
+ const transformer = new Konva.Transformer({
3759
+ nodes: [cropNode],
3760
+ rotateEnabled: false,
3761
+ flipEnabled: false,
3762
+ keepRatio: false,
3763
+ // Pintura-style solid round corner dots
3764
+ enabledAnchors: ["top-left", "top-right", "bottom-left", "bottom-right"],
3765
+ anchorSize: 14,
3766
+ anchorCornerRadius: 7,
3767
+ anchorFill: "#111",
3768
+ anchorStroke: "#111",
3769
+ anchorStrokeWidth: 1,
3770
+ borderStroke: "rgba(255, 255, 255, 0.7)",
3771
+ boundBoxFunc: (oldBox, newBox) => ({ ...newBox, ...clampCropBox(imageBounds, oldBox, newBox, ratioLocked) })
3772
+ });
3773
+ layer.add(transformer);
3774
+ cropNode.dragBoundFunc((position) => clampToImage({
3775
+ ...position,
3776
+ width: cropNode.width() * cropNode.scaleX(),
3777
+ height: cropNode.height() * cropNode.scaleY()
3778
+ }));
3779
+ cropNode.on("dragmove transform", updateOverlay);
3780
+ const setAspect = (aspect) => {
3781
+ ratioLocked = aspect !== null;
3782
+ transformer.keepRatio(ratioLocked);
3783
+ if (aspect === null) {
3784
+ return;
3785
+ }
3786
+ const current = stageRect();
3787
+ const width = Math.min(
3788
+ Math.max(current.width, current.height * aspect),
3789
+ imageBounds.width,
3790
+ imageBounds.height * aspect
3791
+ );
3792
+ const height = width / aspect;
3793
+ const center = { x: current.x + current.width / 2, y: current.y + current.height / 2 };
3794
+ const position = clampToImage({
3795
+ x: center.x - width / 2,
3796
+ y: center.y - height / 2,
3797
+ width,
3798
+ height
3799
+ });
3800
+ cropNode.setAttrs({ ...position, width, height, scaleX: 1, scaleY: 1 });
3801
+ transformer.forceUpdate();
3802
+ updateOverlay();
3803
+ };
3804
+ updateOverlay();
3805
+ deps.stage.add(layer);
3806
+ return {
3807
+ layer,
3808
+ setAspect,
3809
+ update(next) {
3810
+ const scene = toScene(stageRect());
3811
+ view.oriented = next.oriented;
3812
+ view.scale = next.scale;
3813
+ view.offset = next.offset;
3814
+ imageBounds = toStage({ x: 0, y: 0, ...view.oriented });
3815
+ cropNode.setAttrs({ ...toStage(scene), scaleX: 1, scaleY: 1 });
3816
+ transformer.forceUpdate();
3817
+ updateOverlay();
3818
+ },
3819
+ getRect() {
3820
+ const scene = toScene(stageRect());
3821
+ return clampRect({
3822
+ x: Math.round(scene.x),
3823
+ y: Math.round(scene.y),
3824
+ width: Math.round(scene.width),
3825
+ height: Math.round(scene.height)
3826
+ }, view.oriented);
3827
+ },
3828
+ destroy() {
3829
+ transformer.destroy();
3830
+ layer.destroy();
3831
+ }
3832
+ };
3833
+ }
3834
+ function coverScale(box, degrees) {
3835
+ if (box.width <= 0 || box.height <= 0) {
3836
+ throw new RangeError("Dimensions must be positive");
3837
+ }
3838
+ const radians = degrees * Math.PI / 180;
3839
+ const cos = Math.abs(Math.cos(radians));
3840
+ const sin = Math.abs(Math.sin(radians));
3841
+ return Math.max(
3842
+ (box.width * cos + box.height * sin) / box.width,
3843
+ (box.width * sin + box.height * cos) / box.height
3844
+ );
3845
+ }
3846
+ function fitContain(content, container) {
3847
+ if (content.width <= 0 || content.height <= 0 || container.width <= 0 || container.height <= 0) {
3848
+ throw new RangeError("Dimensions must be positive");
3849
+ }
3850
+ const scale = Math.min(
3851
+ container.width / content.width,
3852
+ container.height / content.height,
3853
+ 1
3854
+ );
3855
+ const width = content.width * scale;
3856
+ const height = content.height * scale;
3857
+ return {
3858
+ scale,
3859
+ width,
3860
+ height,
3861
+ x: (container.width - width) / 2,
3862
+ y: (container.height - height) / 2
3863
+ };
3864
+ }
3865
+ function orientImage(image, state) {
3866
+ const natural = { width: image.naturalWidth, height: image.naturalHeight };
3867
+ const oriented = orientedSize(natural, state.rotation);
3868
+ const canvas = document.createElement("canvas");
3869
+ canvas.width = oriented.width;
3870
+ canvas.height = oriented.height;
3871
+ const context = canvas.getContext("2d");
3872
+ if (context === null) {
3873
+ throw new Error("Canvas 2D context unavailable");
3874
+ }
3875
+ const cover = coverScale(oriented, state.fineRotation) * Math.max(1, state.zoom);
3876
+ context.translate(oriented.width / 2, oriented.height / 2);
3877
+ context.rotate(state.fineRotation * Math.PI / 180);
3878
+ context.scale(cover, cover);
3879
+ context.rotate(state.rotation * Math.PI / 180);
3880
+ context.scale(state.flipX ? -1 : 1, state.flipY ? -1 : 1);
3881
+ context.drawImage(image, -natural.width / 2, -natural.height / 2);
3882
+ return canvas;
3883
+ }
3884
+ function transformPoints(points, node) {
3885
+ const radians = node.rotation() * Math.PI / 180;
3886
+ const cos = Math.cos(radians);
3887
+ const sin = Math.sin(radians);
3888
+ const result = [];
3889
+ for (let i = 0; i < points.length; i += 2) {
3890
+ const x = points[i] * node.scaleX();
3891
+ const y = points[i + 1] * node.scaleY();
3892
+ result.push(node.x() + x * cos - y * sin, node.y() + x * sin + y * cos);
3893
+ }
3894
+ return result;
3895
+ }
3896
+ function applyNodeTransform(annotation, node) {
3897
+ switch (annotation.type) {
3898
+ case "draw":
3899
+ case "arrow": {
3900
+ const points = transformPoints(annotation.points, node);
3901
+ const scale = (Math.abs(node.scaleX()) + Math.abs(node.scaleY())) / 2;
3902
+ const strokeWidth = Math.max(1, annotation.strokeWidth * scale);
3903
+ return { ...annotation, points, strokeWidth };
3904
+ }
3905
+ case "redact":
3906
+ return {
3907
+ ...annotation,
3908
+ rect: {
3909
+ x: node.x(),
3910
+ y: node.y(),
3911
+ width: Math.max(1, annotation.rect.width * node.scaleX()),
3912
+ height: Math.max(1, annotation.rect.height * node.scaleY())
3913
+ }
3914
+ };
3915
+ case "rectangle":
3916
+ case "ellipse":
3917
+ return {
3918
+ ...annotation,
3919
+ rect: {
3920
+ x: node.x(),
3921
+ y: node.y(),
3922
+ width: Math.max(1, annotation.rect.width * node.scaleX()),
3923
+ height: Math.max(1, annotation.rect.height * node.scaleY())
3924
+ },
3925
+ rotation: node.rotation()
3926
+ };
3927
+ case "text":
3928
+ case "sticker":
3929
+ return {
3930
+ ...annotation,
3931
+ x: node.x(),
3932
+ y: node.y(),
3933
+ fontSize: Math.max(4, annotation.fontSize * node.scaleY()),
3934
+ rotation: node.rotation()
3935
+ };
3936
+ }
3937
+ }
3938
+ function attachSelection(deps) {
3939
+ const accent = primaryColor();
3940
+ const transformer = new Konva.Transformer({
3941
+ rotateEnabled: true,
3942
+ flipEnabled: false,
3943
+ ignoreStroke: true,
3944
+ // A plain square with four corner handles
3945
+ enabledAnchors: ["top-left", "top-right", "bottom-left", "bottom-right"],
3946
+ anchorSize: 12,
3947
+ anchorCornerRadius: 6,
3948
+ anchorFill: "#fff",
3949
+ anchorStroke: accent,
3950
+ borderStroke: accent,
3951
+ rotateAnchorOffset: 24,
3952
+ rotationSnaps: [0, 45, 90, 135, 180, 225, 270, 315],
3953
+ rotationSnapTolerance: 6
3954
+ });
3955
+ const layer = new Konva.Layer({ name: "selection" });
3956
+ layer.add(transformer);
3957
+ deps.stage.add(layer);
3958
+ const findAnnotation = (id) => deps.getState().annotations.find((annotation) => annotation.id === id);
3959
+ const findNode = (id) => id === null ? null : deps.stage.find(".annotation").find((node) => node.id() === id) ?? null;
3960
+ const reportRect = () => {
3961
+ const node = findNode(deps.getSelectedId());
3962
+ deps.onSelectionRect(node ? node.getClientRect() : null);
3963
+ };
3964
+ const syncTransformer = () => {
3965
+ const id = deps.getSelectedId();
3966
+ const node = findNode(id);
3967
+ if (node === null || node === void 0) {
3968
+ transformer.nodes([]);
3969
+ deps.onSelectionRect(null);
3970
+ return;
3971
+ }
3972
+ const annotation = findAnnotation(id);
3973
+ transformer.rotateEnabled(annotation !== void 0 && annotation.type !== "redact");
3974
+ transformer.nodes([node]);
3975
+ deps.onSelectionRect(node.getClientRect());
3976
+ };
3977
+ const sync = () => {
3978
+ deps.stage.find(".annotation").forEach((node) => node.draggable(true));
3979
+ syncTransformer();
3980
+ };
3981
+ sync();
3982
+ const onWriteBack = (event) => {
3983
+ const node = event.target;
3984
+ const annotation = findAnnotation(node.id());
3985
+ if (annotation === void 0) {
3986
+ return;
3987
+ }
3988
+ const state = deps.getState();
3989
+ deps.commit({
3990
+ ...state,
3991
+ annotations: state.annotations.map((entry) => entry.id === annotation.id ? applyNodeTransform(annotation, node) : entry)
3992
+ }, t("Move or resize"));
3993
+ };
3994
+ const onClick = (event) => {
3995
+ if (event.target.hasName("annotation")) {
3996
+ deps.select(event.target.id());
3997
+ } else if (event.target === deps.stage || event.target instanceof Konva.Image) {
3998
+ deps.select(null);
3999
+ }
4000
+ syncTransformer();
4001
+ };
4002
+ const onDblClick = (event) => {
4003
+ const annotation = findAnnotation(event.target.id());
4004
+ if (annotation?.type === "text") {
4005
+ deps.editText(annotation);
4006
+ }
4007
+ };
4008
+ deps.stage.on("click.selection tap.selection", onClick);
4009
+ deps.stage.on("dblclick.selection dbltap.selection", onDblClick);
4010
+ deps.stage.on("dragend.selection transformend.selection", onWriteBack);
4011
+ deps.stage.on("dragmove.selection transform.selection", reportRect);
4012
+ return {
4013
+ sync,
4014
+ detach() {
4015
+ deps.stage.off(".selection");
4016
+ deps.stage.find(".annotation").forEach((node) => node.draggable(false));
4017
+ deps.onSelectionRect(null);
4018
+ transformer.destroy();
4019
+ layer.destroy();
4020
+ }
4021
+ };
4022
+ }
4023
+ const TOOL_LABELS = {
4024
+ draw: t("Draw"),
4025
+ rectangle: t("Rectangle"),
4026
+ ellipse: t("Ellipse"),
4027
+ arrow: t("Arrow"),
4028
+ sticker: t("Sticker"),
4029
+ redact: t("Redact")
4030
+ };
4031
+ function attachPointerTools(tool, deps) {
4032
+ if (!["draw", "rectangle", "ellipse", "arrow", "text", "sticker", "redact"].includes(tool)) {
4033
+ return () => {
4034
+ };
4035
+ }
4036
+ let active = null;
4037
+ let previewNode = null;
4038
+ let start = { x: 0, y: 0 };
4039
+ let pendingText = null;
4040
+ const scenePointer = () => {
4041
+ const pointer = deps.stage.getPointerPosition();
4042
+ return pointer === null ? null : deps.toScene(pointer);
4043
+ };
4044
+ const refreshPreview = () => {
4045
+ if (active === null) {
4046
+ previewNode?.destroy();
4047
+ previewNode = null;
4048
+ return;
4049
+ }
4050
+ if (previewNode !== null && (active.type === "draw" || active.type === "arrow")) {
4051
+ previewNode.points(active.points);
4052
+ return;
4053
+ }
4054
+ previewNode?.destroy();
4055
+ previewNode = null;
4056
+ const source = deps.oriented() ?? void 0;
4057
+ if (active.type === "redact" && source === void 0) {
4058
+ return;
4059
+ }
4060
+ previewNode = buildAnnotationNode(active, source);
4061
+ deps.contentGroup()?.add(previewNode);
4062
+ };
4063
+ const discard = () => {
4064
+ active = null;
4065
+ pendingText = null;
4066
+ previewNode?.destroy();
4067
+ previewNode = null;
4068
+ };
4069
+ const onPointerDown = () => {
4070
+ const point = scenePointer();
4071
+ if (point === null || deps.panning()) {
4072
+ return;
4073
+ }
4074
+ const options = deps.options();
4075
+ start = point;
4076
+ switch (tool) {
4077
+ case "draw":
4078
+ active = { id: newId(), type: "draw", points: [point.x, point.y], color: options.color, strokeWidth: options.strokeWidth };
4079
+ break;
4080
+ case "arrow":
4081
+ active = { id: newId(), type: "arrow", points: [point.x, point.y, point.x, point.y], color: options.color, strokeWidth: options.strokeWidth };
4082
+ break;
4083
+ case "rectangle":
4084
+ case "ellipse":
4085
+ active = { id: newId(), type: tool, rect: { x: point.x, y: point.y, width: 1, height: 1 }, rotation: 0, color: options.color, strokeWidth: options.strokeWidth };
4086
+ break;
4087
+ case "redact":
4088
+ active = { id: newId(), type: "redact", rect: { x: point.x, y: point.y, width: 1, height: 1 }, style: options.redactStyle };
4089
+ break;
4090
+ case "text":
4091
+ pendingText = point;
4092
+ return;
4093
+ case "sticker": {
4094
+ const state = deps.getState();
4095
+ const sticker = {
4096
+ id: newId(),
4097
+ type: "sticker",
4098
+ x: point.x,
4099
+ y: point.y,
4100
+ text: options.sticker,
4101
+ color: options.color,
4102
+ fontSize: options.fontSize * 2,
4103
+ rotation: 0
4104
+ };
4105
+ deps.commit({ ...state, annotations: [...state.annotations, sticker] }, TOOL_LABELS.sticker);
4106
+ return;
4107
+ }
4108
+ }
4109
+ refreshPreview();
4110
+ };
4111
+ const onPointerMove = () => {
4112
+ if (active === null) {
4113
+ return;
4114
+ }
4115
+ if (deps.panning()) {
4116
+ discard();
4117
+ return;
4118
+ }
4119
+ const point = scenePointer();
4120
+ if (point === null) {
4121
+ return;
4122
+ }
4123
+ switch (active.type) {
4124
+ case "draw":
4125
+ active.points.push(point.x, point.y);
4126
+ break;
4127
+ case "arrow":
4128
+ active.points = [start.x, start.y, point.x, point.y];
4129
+ break;
4130
+ case "rectangle":
4131
+ case "ellipse":
4132
+ case "redact":
4133
+ active = {
4134
+ ...active,
4135
+ rect: {
4136
+ x: Math.min(start.x, point.x),
4137
+ y: Math.min(start.y, point.y),
4138
+ width: Math.abs(point.x - start.x) || 1,
4139
+ height: Math.abs(point.y - start.y) || 1
4140
+ }
4141
+ };
4142
+ break;
4143
+ }
4144
+ refreshPreview();
4145
+ };
4146
+ const onPointerUp = () => {
4147
+ if (pendingText !== null) {
4148
+ deps.startTextEdit(pendingText);
4149
+ pendingText = null;
4150
+ return;
4151
+ }
4152
+ if (active === null) {
4153
+ return;
4154
+ }
4155
+ const state = deps.getState();
4156
+ deps.commit({ ...state, annotations: [...state.annotations, active] }, TOOL_LABELS[tool]);
4157
+ discard();
4158
+ };
4159
+ deps.stage.on("pointerdown.tool", onPointerDown);
4160
+ deps.stage.on("pointermove.tool", onPointerMove);
4161
+ deps.stage.on("pointerup.tool", onPointerUp);
4162
+ window.addEventListener("pointerup", onPointerUp);
4163
+ window.addEventListener("pointercancel", discard);
4164
+ return () => {
4165
+ deps.stage.off(".tool");
4166
+ window.removeEventListener("pointerup", onPointerUp);
4167
+ window.removeEventListener("pointercancel", discard);
4168
+ previewNode?.destroy();
4169
+ };
4170
+ }
4171
+ const _hoisted_1 = { class: "image-editor__shell" };
4172
+ const _hoisted_2 = { class: "image-editor__frame" };
4173
+ const _hoisted_3 = { class: "image-editor__viewport" };
4174
+ const _hoisted_4 = ["aria-label"];
4175
+ const _hoisted_5 = {
4176
+ key: 1,
4177
+ class: "image-editor__error",
4178
+ "data-test": "load-error"
4179
+ };
4180
+ const _hoisted_6 = {
4181
+ class: "hidden-visually",
4182
+ role: "status",
4183
+ "aria-live": "polite"
4184
+ };
4185
+ const _sfc_main = /* @__PURE__ */ defineComponent({
4186
+ __name: "ImageEditor",
4187
+ props: {
4188
+ src: {},
4189
+ label: {},
4190
+ exportOptions: {},
4191
+ initialState: {}
4192
+ },
4193
+ emits: ["save", "cancel", "error", "change"],
4194
+ setup(__props, { expose: __expose, emit: __emit }) {
4195
+ const props = __props;
4196
+ const emit = __emit;
4197
+ const labels = {
4198
+ canvas: t("Image editor"),
4199
+ failed: t("The image could not be loaded"),
4200
+ retry: t("Try again")
4201
+ };
4202
+ let loadAttempt = 0;
4203
+ const context = createEditorContext();
4204
+ const container = useTemplateRef("container");
4205
+ const loaded = ref(false);
4206
+ const errored = ref(false);
4207
+ const containerSize = shallowRef({ width: 0, height: 0 });
4208
+ const orientedCanvas = shallowRef(null);
4209
+ const sourceImage = shallowRef(null);
4210
+ const { ambient, backdrop } = useAmbient(sourceImage);
4211
+ const { panArmed } = useWheelControls(container, context);
4212
+ const announcement = useAnnouncements(context);
4213
+ const selectionBox = shallowRef(null);
4214
+ let stage = null;
4215
+ let scene = null;
4216
+ let cropOverlay = null;
4217
+ let selection = null;
4218
+ let detachTool = null;
4219
+ let attached = null;
4220
+ let resizeObserver = null;
4221
+ let pendingTransition = null;
4222
+ let lastView = null;
4223
+ const canvasCursor = computed(() => {
4224
+ if (context.panning.value) {
4225
+ return "grabbing";
4226
+ }
4227
+ if (panArmed.value) {
4228
+ return "grab";
4229
+ }
4230
+ const tool = context.activeTool.value;
4231
+ if (["draw", "rectangle", "ellipse", "arrow", "text", "sticker", "redact"].includes(tool)) {
4232
+ return "crosshair";
4233
+ }
4234
+ return "default";
4235
+ });
4236
+ const viewFit = computed(() => {
4237
+ const oriented = orientedCanvas.value;
4238
+ if (oriented === null || containerSize.value.width === 0 || containerSize.value.height === 0) {
4239
+ return null;
4240
+ }
4241
+ const showCropped = context.activeTool.value !== "crop";
4242
+ const visible = showCropped ? visibleRect(context.state.value, { width: oriented.width, height: oriented.height }) : { x: 0, y: 0, width: oriented.width, height: oriented.height };
4243
+ const container2 = containerSize.value;
4244
+ const fit = fitContain(
4245
+ { width: visible.width, height: visible.height },
4246
+ {
4247
+ width: Math.max(1, container2.width - VIEW_MARGIN * 2),
4248
+ height: Math.max(1, container2.height - VIEW_MARGIN * 2)
4249
+ }
4250
+ );
4251
+ return { scale: fit.scale, visible, container: container2, showCropped };
4252
+ });
4253
+ const viewOptions = computed(() => {
4254
+ const fit = viewFit.value;
4255
+ if (fit === null) {
4256
+ return null;
4257
+ }
4258
+ const scale = fit.scale * context.viewZoom.value;
4259
+ const pan = clampPan(context.viewPan.value, panBounds(fit.visible, scale, fit.container));
4260
+ return {
4261
+ scale,
4262
+ offset: {
4263
+ x: (fit.container.width - fit.visible.width * scale) / 2 + pan.x,
4264
+ y: (fit.container.height - fit.visible.height * scale) / 2 + pan.y
4265
+ },
4266
+ showCropped: fit.showCropped,
4267
+ fastFilters: context.interacting.value
4268
+ };
4269
+ });
4270
+ const { textEdit, startTextEdit, confirmTextEdit } = useTextEditing({
4271
+ context,
4272
+ viewOptions: () => viewOptions.value,
4273
+ oriented: () => orientedCanvas.value
4274
+ });
4275
+ const { exportImage, save: onSave } = useExportImage({
4276
+ oriented: () => orientedCanvas.value,
4277
+ getState: () => context.state.value,
4278
+ source: () => props.src instanceof Blob ? props.src : null,
4279
+ saveOptions: () => props.exportOptions ?? {},
4280
+ onSaved: (result) => emit("save", result),
4281
+ onError: (error) => emit("error", error)
4282
+ });
4283
+ function captureView() {
4284
+ return lastView ?? { previousScale: 1, previousOffset: { x: 0, y: 0 }, previousOrigin: { x: 0, y: 0 } };
4285
+ }
4286
+ function commitWithTransition(kind, next, label) {
4287
+ pendingTransition = { kind, context: captureView() };
4288
+ context.commit(next, label);
4289
+ }
4290
+ function renderView() {
4291
+ const oriented = orientedCanvas.value;
4292
+ const options = viewOptions.value;
4293
+ if (stage === null || oriented === null || options === null) {
4294
+ return;
4295
+ }
4296
+ stage.size(containerSize.value);
4297
+ scene ??= createScene(stage);
4298
+ scene.update(oriented, context.state.value, options);
4299
+ const renderedOrigin = options.showCropped ? visibleRect(context.state.value, { width: oriented.width, height: oriented.height }) : { x: 0, y: 0 };
4300
+ syncTools(oriented, options);
4301
+ if (pendingTransition !== null) {
4302
+ const visible = options.showCropped ? visibleRect(context.state.value, { width: oriented.width, height: oriented.height }) : { x: 0, y: 0, width: oriented.width, height: oriented.height };
4303
+ const center = {
4304
+ x: visible.x + visible.width / 2,
4305
+ y: visible.y + visible.height / 2
4306
+ };
4307
+ const targets = [{
4308
+ node: scene.contentGroup,
4309
+ pivot: center,
4310
+ unit: 1 / options.scale
4311
+ }];
4312
+ if (cropOverlay !== null) {
4313
+ targets.push({
4314
+ node: cropOverlay.layer,
4315
+ pivot: {
4316
+ x: options.offset.x + (center.x - renderedOrigin.x) * options.scale,
4317
+ y: options.offset.y + (center.y - renderedOrigin.y) * options.scale
4318
+ },
4319
+ unit: 1
4320
+ });
4321
+ }
4322
+ playTransition(pendingTransition.kind, {
4323
+ targets,
4324
+ scale: options.scale,
4325
+ offset: options.offset,
4326
+ origin: renderedOrigin
4327
+ }, pendingTransition.context);
4328
+ pendingTransition = null;
4329
+ }
4330
+ lastView = {
4331
+ previousScale: options.scale,
4332
+ previousOffset: options.offset,
4333
+ previousOrigin: { x: renderedOrigin.x, y: renderedOrigin.y }
4334
+ };
4335
+ }
4336
+ function syncTools(oriented, options) {
4337
+ if (stage === null) {
4338
+ return;
4339
+ }
4340
+ const tool = context.activeTool.value;
4341
+ const crop = context.state.value.crop;
4342
+ const fresh = attached !== null && attached.tool === tool && attached.oriented === oriented && attached.crop === crop;
4343
+ if (fresh) {
4344
+ selection?.sync();
4345
+ cropOverlay?.update({
4346
+ oriented: { width: oriented.width, height: oriented.height },
4347
+ scale: options.scale,
4348
+ offset: options.offset
4349
+ });
4350
+ return;
4351
+ }
4352
+ detachTool?.();
4353
+ detachTool = null;
4354
+ selection?.detach();
4355
+ selection = null;
4356
+ cropOverlay?.destroy();
4357
+ cropOverlay = null;
4358
+ attached = { tool, oriented, crop };
4359
+ if (tool === "select") {
4360
+ selection = attachSelection({
4361
+ stage,
4362
+ getState: () => context.state.value,
4363
+ commit: context.commit,
4364
+ select: (id) => {
4365
+ context.selectedId.value = id;
4366
+ },
4367
+ getSelectedId: () => context.selectedId.value,
4368
+ editText: (annotation) => startTextEdit({ x: annotation.x, y: annotation.y }, annotation),
4369
+ onSelectionRect: (rect) => {
4370
+ selectionBox.value = rect;
4371
+ }
4372
+ });
4373
+ } else if (tool === "crop") {
4374
+ cropOverlay = attachCropOverlay({
4375
+ stage,
4376
+ oriented: { width: oriented.width, height: oriented.height },
4377
+ scale: options.scale,
4378
+ offset: options.offset,
4379
+ initial: crop
4380
+ });
4381
+ applyCropAspect();
4382
+ } else if (tool !== "adjust") {
4383
+ detachTool = attachPointerTools(tool, {
4384
+ stage,
4385
+ contentGroup: () => scene?.contentGroup ?? null,
4386
+ oriented: () => orientedCanvas.value,
4387
+ getState: () => context.state.value,
4388
+ commit: context.commit,
4389
+ // Read the live view: the tool outlives the transform it was
4390
+ // attached under
4391
+ toScene: (pointer) => toImageCoords(
4392
+ pointer,
4393
+ context.state.value,
4394
+ { width: oriented.width, height: oriented.height },
4395
+ viewOptions.value ?? options
4396
+ ),
4397
+ panning: () => context.panning.value,
4398
+ options: () => ({
4399
+ color: context.drawColor.value,
4400
+ strokeWidth: context.strokeWidth.value,
4401
+ fontSize: context.fontSize.value,
4402
+ sticker: context.sticker.value,
4403
+ redactStyle: context.redactStyle.value
4404
+ }),
4405
+ startTextEdit: (position) => startTextEdit(position)
4406
+ });
4407
+ }
4408
+ }
4409
+ function applyCropAspect() {
4410
+ if (cropOverlay === null) {
4411
+ return;
4412
+ }
4413
+ const aspect = context.cropAspect.value;
4414
+ const oriented = orientedCanvas.value;
4415
+ if (aspect === "original" && oriented !== null) {
4416
+ cropOverlay.setAspect(oriented.width / oriented.height);
4417
+ } else {
4418
+ cropOverlay.setAspect(typeof aspect === "number" ? aspect : null);
4419
+ }
4420
+ }
4421
+ function refreshOrientedCanvas() {
4422
+ if (sourceImage.value === null) {
4423
+ return;
4424
+ }
4425
+ orientedCanvas.value = orientImage(sourceImage.value, context.state.value);
4426
+ }
4427
+ function seedState(seed, image) {
4428
+ if (seed.crop === null) {
4429
+ return seed;
4430
+ }
4431
+ const oriented = orientedSize({ width: image.naturalWidth, height: image.naturalHeight }, seed.rotation);
4432
+ return { ...seed, crop: clampRect(seed.crop, oriented) };
4433
+ }
4434
+ async function load() {
4435
+ const attempt = ++loadAttempt;
4436
+ loaded.value = false;
4437
+ errored.value = false;
4438
+ try {
4439
+ const image = await loadImage(props.src);
4440
+ if (attempt !== loadAttempt) {
4441
+ return;
4442
+ }
4443
+ sourceImage.value = image;
4444
+ context.reset(props.initialState === void 0 ? void 0 : seedState(props.initialState, image));
4445
+ const minDimension = Math.min(image.naturalWidth, image.naturalHeight);
4446
+ context.fontSize.value = Math.min(128, Math.max(12, Math.round(minDimension / 15)));
4447
+ pendingTransition = { kind: "load", context: captureView() };
4448
+ refreshOrientedCanvas();
4449
+ loaded.value = true;
4450
+ } catch (error) {
4451
+ if (attempt !== loadAttempt) {
4452
+ return;
4453
+ }
4454
+ errored.value = true;
4455
+ emit("error", error instanceof Error ? error : new Error(String(error)));
4456
+ }
4457
+ }
4458
+ function currentOriented() {
4459
+ const oriented = orientedCanvas.value;
4460
+ return { width: oriented.width, height: oriented.height };
4461
+ }
4462
+ function onRotateCW() {
4463
+ commitWithTransition("rotate-cw", rotateCW(context.state.value, currentOriented()), t("Rotate right"));
4464
+ }
4465
+ function onRotateCCW() {
4466
+ let state = context.state.value;
4467
+ let oriented = currentOriented();
4468
+ for (let i = 0; i < 3; i++) {
4469
+ state = rotateCW(state, oriented);
4470
+ oriented = { width: oriented.height, height: oriented.width };
4471
+ }
4472
+ commitWithTransition("rotate-ccw", state, t("Rotate left"));
4473
+ }
4474
+ function onFlipHorizontal() {
4475
+ commitWithTransition("flip-h", flipHorizontal(context.state.value, currentOriented()), t("Flip horizontal"));
4476
+ }
4477
+ function onFlipVertical() {
4478
+ commitWithTransition("flip-v", flipVertical(context.state.value, currentOriented()), t("Flip vertical"));
4479
+ }
4480
+ function onApplyCrop() {
4481
+ if (cropOverlay !== null) {
4482
+ const crop = cropOverlay.getRect();
4483
+ pendingTransition = { kind: "crop", context: captureView() };
4484
+ context.commit({ ...context.state.value, crop }, t("Crop"));
4485
+ context.setMode("annotate");
4486
+ }
4487
+ }
4488
+ function onDuplicateSelection() {
4489
+ const id = context.selectedId.value;
4490
+ const state = context.state.value;
4491
+ const annotation = state.annotations.find((entry) => entry.id === id);
4492
+ if (annotation === void 0) {
4493
+ return;
4494
+ }
4495
+ const copy = duplicateAnnotation(annotation);
4496
+ context.commit({ ...state, annotations: [...state.annotations, copy] }, t("Duplicate"));
4497
+ context.selectedId.value = copy.id;
4498
+ renderView();
4499
+ }
4500
+ function onRevert() {
4501
+ context.commit(createInitialState(), t("Revert all changes"));
4502
+ }
4503
+ function onResetCrop() {
4504
+ context.commit({ ...context.state.value, crop: null }, t("Reset crop"));
4505
+ }
4506
+ function onDeleteSelection() {
4507
+ const id = context.selectedId.value;
4508
+ if (id === null) {
4509
+ return;
4510
+ }
4511
+ const state = context.state.value;
4512
+ context.selectedId.value = null;
4513
+ context.commit({
4514
+ ...state,
4515
+ annotations: state.annotations.filter((annotation) => annotation.id !== id)
4516
+ }, t("Delete"));
4517
+ }
4518
+ useEditorShortcuts({
4519
+ context,
4520
+ isTextEditing: () => textEdit.value !== null,
4521
+ onDelete: onDeleteSelection,
4522
+ onEscape: () => {
4523
+ if (context.selectedId.value !== null) {
4524
+ context.selectedId.value = null;
4525
+ renderView();
4526
+ } else if (context.activeMode.value === "crop") {
4527
+ context.setMode("select");
4528
+ } else if (context.activeMode.value === "annotate" && context.activeTool.value !== "select") {
4529
+ context.activeTool.value = "select";
4530
+ }
4531
+ }
4532
+ });
4533
+ watch(viewFit, (fit) => {
4534
+ context.viewFit.value = fit;
4535
+ }, { immediate: true });
4536
+ provideEditorCommands({
4537
+ rotateCW: onRotateCW,
4538
+ rotateCCW: onRotateCCW,
4539
+ flipHorizontal: onFlipHorizontal,
4540
+ flipVertical: onFlipVertical,
4541
+ applyCrop: onApplyCrop,
4542
+ resetCrop: onResetCrop,
4543
+ revert: onRevert
4544
+ });
4545
+ watch(() => props.src, load);
4546
+ watch(
4547
+ [
4548
+ () => context.state.value.rotation,
4549
+ () => context.state.value.flipX,
4550
+ () => context.state.value.flipY,
4551
+ () => context.state.value.fineRotation,
4552
+ () => context.state.value.zoom
4553
+ ],
4554
+ refreshOrientedCanvas
4555
+ );
4556
+ watch(
4557
+ [context.state, context.activeTool, context.activeMode, context.viewZoom, context.viewPan, context.interacting, orientedCanvas, containerSize],
4558
+ renderView
4559
+ );
4560
+ watch(context.cropAspect, applyCropAspect);
4561
+ watch(context.selectedId, (id) => {
4562
+ const annotation = context.state.value.annotations.find((entry) => entry.id === id);
4563
+ if (annotation !== void 0 && "color" in annotation && annotation.type !== "sticker") {
4564
+ context.drawColor.value = annotation.color;
4565
+ }
4566
+ });
4567
+ watch([context.state, context.interacting], () => {
4568
+ if (!context.interacting.value) {
4569
+ emit("change", structuredClone(context.state.value));
4570
+ }
4571
+ });
4572
+ onMounted(() => {
4573
+ stage = new Konva.Stage({ container: container.value, width: 1, height: 1 });
4574
+ resizeObserver = new ResizeObserver(() => {
4575
+ const { clientWidth, clientHeight } = container.value;
4576
+ containerSize.value = { width: clientWidth, height: clientHeight };
4577
+ });
4578
+ resizeObserver.observe(container.value);
4579
+ load();
4580
+ });
4581
+ onBeforeUnmount(() => {
4582
+ resizeObserver?.disconnect();
4583
+ detachTool?.();
4584
+ selection?.detach();
4585
+ cropOverlay?.destroy();
4586
+ scene?.destroy();
4587
+ scene = null;
4588
+ stage?.destroy();
4589
+ stage = null;
4590
+ });
4591
+ __expose({
4592
+ exportImage,
4593
+ /**
4594
+ * Start over, optionally from a given state.
4595
+ *
4596
+ * @param state state to reset to, defaulting to a pristine one
4597
+ */
4598
+ reset: (state) => context.reset(state)
4599
+ });
4600
+ return (_ctx, _cache) => {
4601
+ return openBlock(), createElementBlock("div", {
4602
+ class: "image-editor",
4603
+ style: normalizeStyle({
4604
+ "--editor-ambient": unref(ambient),
4605
+ "--editor-backdrop": unref(backdrop) ? `url(${unref(backdrop)})` : "none"
4606
+ })
4607
+ }, [
4608
+ createElementVNode("div", _hoisted_1, [
4609
+ createElementVNode("div", _hoisted_2, [
4610
+ createElementVNode("div", _hoisted_3, [
4611
+ createElementVNode("div", {
4612
+ ref_key: "container",
4613
+ ref: container,
4614
+ class: "image-editor__canvas",
4615
+ style: normalizeStyle({ cursor: canvasCursor.value }),
4616
+ role: "img",
4617
+ "aria-label": __props.label ?? labels.canvas
4618
+ }, null, 12, _hoisted_4),
4619
+ !loaded.value && !errored.value ? (openBlock(), createBlock(unref(NcLoadingIcon), {
4620
+ key: 0,
4621
+ class: "image-editor__loading",
4622
+ size: 44
4623
+ })) : createCommentVNode("", true),
4624
+ errored.value ? (openBlock(), createElementBlock("div", _hoisted_5, [
4625
+ createElementVNode("p", null, toDisplayString(labels.failed), 1),
4626
+ createVNode(unref(NcButton), {
4627
+ variant: "secondary",
4628
+ "data-test": "retry",
4629
+ onClick: _cache[0] || (_cache[0] = ($event) => load())
4630
+ }, {
4631
+ default: withCtx(() => [
4632
+ createTextVNode(toDisplayString(labels.retry), 1)
4633
+ ]),
4634
+ _: 1
4635
+ })
4636
+ ])) : createCommentVNode("", true),
4637
+ unref(textEdit) !== null ? (openBlock(), createBlock(TextOverlay, {
4638
+ key: 2,
4639
+ x: unref(textEdit).screenX,
4640
+ y: unref(textEdit).screenY,
4641
+ "font-size": unref(textEdit).screenFontSize,
4642
+ color: unref(textEdit).color,
4643
+ initial: unref(textEdit).value,
4644
+ onConfirm: unref(confirmTextEdit),
4645
+ onCancel: _cache[1] || (_cache[1] = ($event) => textEdit.value = null)
4646
+ }, null, 8, ["x", "y", "font-size", "color", "initial", "onConfirm"])) : createCommentVNode("", true),
4647
+ selectionBox.value !== null ? (openBlock(), createBlock(SelectionToolbar, {
4648
+ key: 3,
4649
+ box: selectionBox.value,
4650
+ onDuplicate: onDuplicateSelection,
4651
+ onDelete: onDeleteSelection
4652
+ }, null, 8, ["box"])) : createCommentVNode("", true)
4653
+ ]),
4654
+ createVNode(EditorTopBar, {
4655
+ class: "image-editor__topbar",
4656
+ loaded: loaded.value,
4657
+ onSave: unref(onSave),
4658
+ onCancel: _cache[2] || (_cache[2] = ($event) => emit("cancel"))
4659
+ }, null, 8, ["loaded", "onSave"]),
4660
+ createVNode(EditorPanel, {
4661
+ class: normalizeClass(unref(context).activeMode.value === "filter" ? "image-editor__strip" : "image-editor__controls"),
4662
+ loaded: loaded.value,
4663
+ oriented: orientedCanvas.value
4664
+ }, null, 8, ["class", "loaded", "oriented"]),
4665
+ createVNode(EditorSidebar, {
4666
+ class: "image-editor__rail",
4667
+ loaded: loaded.value
4668
+ }, null, 8, ["loaded"]),
4669
+ createElementVNode("span", _hoisted_6, toDisplayString(unref(announcement)), 1)
4670
+ ])
4671
+ ])
4672
+ ], 4);
4673
+ };
4674
+ }
4675
+ });
4676
+ const ImageEditor = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-e3346ccd"]]);
4677
+ export {
4678
+ ImageEditor,
4679
+ createInitialState,
4680
+ isPristine,
4681
+ useHistory
4682
+ };
4683
+ //# sourceMappingURL=index.mjs.map