domma-cms 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +2 -3
  2. package/admin/css/admin.css +1 -1200
  3. package/admin/js/api.js +1 -242
  4. package/admin/js/app.js +5 -279
  5. package/admin/js/config/sidebar-config.js +1 -115
  6. package/admin/js/lib/card.js +1 -63
  7. package/admin/js/lib/image-editor.js +1 -869
  8. package/admin/js/lib/markdown-toolbar.js +46 -421
  9. package/admin/js/templates/layouts.html +44 -7
  10. package/admin/js/templates/page-editor.html +9 -0
  11. package/admin/js/templates/settings.html +18 -1
  12. package/admin/js/templates/users.html +29 -4
  13. package/admin/js/views/collection-editor.js +3 -487
  14. package/admin/js/views/collection-entries.js +1 -484
  15. package/admin/js/views/collections.js +1 -153
  16. package/admin/js/views/dashboard.js +1 -56
  17. package/admin/js/views/documentation.js +1 -12
  18. package/admin/js/views/index.js +1 -39
  19. package/admin/js/views/layouts.js +9 -42
  20. package/admin/js/views/login.js +7 -251
  21. package/admin/js/views/media.js +1 -240
  22. package/admin/js/views/navigation.js +14 -212
  23. package/admin/js/views/page-editor.js +53 -661
  24. package/admin/js/views/pages.js +5 -72
  25. package/admin/js/views/plugins.js +13 -90
  26. package/admin/js/views/settings.js +1 -199
  27. package/admin/js/views/tutorials.js +1 -12
  28. package/admin/js/views/user-editor.js +1 -88
  29. package/admin/js/views/users.js +7 -76
  30. package/bin/cli.js +18 -9
  31. package/config/auth.json +1 -17
  32. package/config/navigation.json +15 -0
  33. package/config/site.json +5 -4
  34. package/package.json +1 -1
  35. package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
  36. package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
  37. package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
  38. package/plugins/domma-effects/public/celebrations/index.js +1 -535
  39. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
  40. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
  41. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
  42. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
  43. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
  44. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
  45. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
  46. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
  47. package/plugins/example-analytics/stats.json +16 -12
  48. package/plugins/form-builder/admin/templates/form-editor.html +158 -130
  49. package/plugins/form-builder/admin/views/form-editor.js +3 -1
  50. package/plugins/form-builder/data/forms/contact-details.json +71 -35
  51. package/plugins/form-builder/data/forms/feedback.json +130 -0
  52. package/plugins/form-builder/data/submissions/feedback.json +1 -0
  53. package/plugins/form-builder/public/form-logic-engine.js +1 -568
  54. package/public/css/site.css +1 -302
  55. package/public/js/btt.js +1 -90
  56. package/public/js/cookie-consent.js +1 -61
  57. package/public/js/site.js +1 -204
  58. package/scripts/setup.js +12 -9
  59. package/server/middleware/auth.js +44 -21
  60. package/server/routes/api/auth.js +38 -8
  61. package/server/routes/api/collections.js +18 -5
  62. package/server/routes/api/layouts.js +18 -4
  63. package/server/routes/api/media.js +2 -3
  64. package/server/routes/api/navigation.js +2 -3
  65. package/server/routes/api/pages.js +3 -3
  66. package/server/routes/api/settings.js +2 -3
  67. package/server/routes/api/users.js +4 -6
  68. package/server/routes/public.js +3 -3
  69. package/server/server.js +8 -0
  70. package/server/services/markdown.js +102 -3
  71. package/server/services/userTypes.js +167 -0
  72. package/plugins/form-builder/email.js +0 -103
@@ -1,869 +1 @@
1
- /**
2
- * Image Editor Module
3
- * Opens an interactive editor modal (Cropper.js + DOM API) for a media file.
4
- * Returns a Promise<boolean> — true when the image was saved, false on cancel.
5
- */
6
- import {api} from '../api.js';
7
-
8
- // ---------------------------------------------------------------------------
9
- // Custom icon definitions
10
- // ---------------------------------------------------------------------------
11
-
12
- function registerIcons() {
13
- const icons = {
14
- 'rotate-cw': {
15
- viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', strokeWidth: 2,
16
- path: 'M21 2v6h-6M21 8A9 9 0 1 0 19.07 14.4'
17
- },
18
- 'rotate-ccw': {
19
- viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', strokeWidth: 2,
20
- path: 'M3 2v6h6M3 8a9 9 0 1 1 1.93 6.4'
21
- },
22
- 'flip-h': {
23
- viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', strokeWidth: 2,
24
- paths: ['M12 3v18', 'M4 6l4 6-4 6', 'M20 6l-4 6 4 6']
25
- },
26
- 'flip-v': {
27
- viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', strokeWidth: 2,
28
- paths: ['M3 12h18', 'M6 4l6 4 6-4', 'M6 20l6-4 6 4']
29
- },
30
- 'lock': {
31
- viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', strokeWidth: 2,
32
- paths: [
33
- 'M17 11H7a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2z',
34
- 'M12 8a4 4 0 0 0-4 4',
35
- ],
36
- path: 'M12 8V5a4 4 0 0 1 4 4v2'
37
- },
38
- 'unlock': {
39
- viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', strokeWidth: 2,
40
- paths: [
41
- 'M17 11H7a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2z',
42
- 'M8 11V7a4 4 0 0 1 8 0',
43
- ]
44
- },
45
- 'crop-icon': {
46
- viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', strokeWidth: 2,
47
- paths: ['M6 2v14a2 2 0 0 0 2 2h14', 'M18 22V8a2 2 0 0 0-2-2H2']
48
- },
49
- 'sliders': {
50
- viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', strokeWidth: 2,
51
- paths: ['M4 21v-7', 'M4 10V3', 'M12 21v-9', 'M12 8V3', 'M20 21v-5', 'M20 12V3',
52
- 'M1 14h6', 'M9 8h6', 'M17 16h6']
53
- },
54
- 'droplet': {
55
- viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', strokeWidth: 2,
56
- path: 'M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z'
57
- },
58
- 'image-plus': {
59
- viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', strokeWidth: 2,
60
- paths: [
61
- 'M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7',
62
- 'M16 5h6', 'M19 2v6', 'M3 15l5-5 4 4 3-3 4 4',
63
- ]
64
- },
65
- 'frame': {
66
- viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', strokeWidth: 2,
67
- paths: ['M7 3H3v4', 'M17 3h4v4', 'M7 21H3v-4', 'M17 21h4v-4']
68
- },
69
- 'file-type': {
70
- viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', strokeWidth: 2,
71
- paths: [
72
- 'M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z',
73
- 'M14 2v6h6', 'M10 13l-2 4h8l-2-4',
74
- ]
75
- },
76
- };
77
-
78
- for (const [name, def] of Object.entries(icons)) {
79
- try {
80
- I.register(name, def);
81
- } catch { /* already registered */
82
- }
83
- }
84
- }
85
-
86
- // ---------------------------------------------------------------------------
87
- // DOM helpers
88
- // ---------------------------------------------------------------------------
89
-
90
- function makeIcon(name) {
91
- const span = document.createElement('span');
92
- span.setAttribute('data-icon', name);
93
- return span;
94
- }
95
-
96
- function makeBtn(cls, iconName, title) {
97
- const btn = document.createElement('button');
98
- btn.className = cls;
99
- btn.type = 'button';
100
- btn.title = title;
101
- btn.appendChild(makeIcon(iconName));
102
- return btn;
103
- }
104
-
105
- function makeSep() {
106
- const sep = document.createElement('span');
107
- sep.className = 'image-editor-sep';
108
- return sep;
109
- }
110
-
111
- function clearChildren(el) {
112
- while (el.firstChild) el.removeChild(el.firstChild);
113
- }
114
-
115
- // ---------------------------------------------------------------------------
116
- // Main export
117
- // ---------------------------------------------------------------------------
118
-
119
- /**
120
- * Open the image editor for a media file.
121
- *
122
- * @param {{ name: string, url: string }} file
123
- * @returns {Promise<boolean>} Resolves true if the image was saved
124
- */
125
- export async function openImageEditor(file) {
126
- registerIcons();
127
-
128
- let imageInfo;
129
- try {
130
- imageInfo = await api.media.info(file.name);
131
- } catch (err) {
132
- E.toast(`Cannot open editor: ${err.message}`, {type: 'error'});
133
- return false;
134
- }
135
-
136
- return new Promise((resolve) => {
137
- // Geometric state
138
- let cropper = null;
139
- let cropActive = false;
140
- let totalRotation = 0;
141
- let flippedH = false;
142
- let flippedV = false;
143
- let aspectLocked = true;
144
- const aspectRatio = imageInfo.width / imageInfo.height;
145
- let cropperCanvas = null;
146
-
147
- // Effects state
148
- let activePreset = null;
149
- const adjustments = {brightness: 1, contrast: 1, saturation: 1, blur: 0, sharpen: 0, gamma: 1};
150
- const watermarkState = {image: null, position: 'bottom-right', opacity: 50, scale: 15};
151
- const borderState = {width: 0, colour: '#000000', mode: 'solid'};
152
- const formatState = {type: null, quality: 85};
153
-
154
- // ----------------------------------------------------------------
155
- // Build the wrapper
156
- // ----------------------------------------------------------------
157
- const wrapper = document.createElement('div');
158
- wrapper.className = 'image-editor';
159
-
160
- // --- Toolbar ---
161
- const toolbar = document.createElement('div');
162
- toolbar.className = 'image-editor-toolbar';
163
-
164
- const cropBtn = makeBtn('btn btn-sm btn-ghost editor-toolbar-btn', 'crop-icon', 'Toggle crop');
165
- const rotateCW = makeBtn('btn btn-sm btn-ghost editor-toolbar-btn', 'rotate-cw', 'Rotate clockwise');
166
- const rotateCCW = makeBtn('btn btn-sm btn-ghost editor-toolbar-btn', 'rotate-ccw', 'Rotate counter-clockwise');
167
- const flipHBtn = makeBtn('btn btn-sm btn-ghost editor-toolbar-btn', 'flip-h', 'Flip horizontal');
168
- const flipVBtn = makeBtn('btn btn-sm btn-ghost editor-toolbar-btn', 'flip-v', 'Flip vertical');
169
-
170
- toolbar.appendChild(cropBtn);
171
- toolbar.appendChild(makeSep());
172
- toolbar.appendChild(rotateCCW);
173
- toolbar.appendChild(rotateCW);
174
- toolbar.appendChild(makeSep());
175
- toolbar.appendChild(flipHBtn);
176
- toolbar.appendChild(flipVBtn);
177
-
178
- // --- Canvas ---
179
- const canvas = document.createElement('div');
180
- canvas.className = 'image-editor-canvas';
181
-
182
- const img = document.createElement('img');
183
- img.src = file.url + '?t=' + Date.now();
184
- img.alt = file.name;
185
- img.style.maxWidth = '100%';
186
- canvas.appendChild(img);
187
-
188
- // --- Resize bar ---
189
- const resizeBar = document.createElement('div');
190
- resizeBar.className = 'image-editor-resize';
191
-
192
- const wLabel = document.createElement('label');
193
- wLabel.textContent = 'W';
194
- const wInput = document.createElement('input');
195
- wInput.type = 'number';
196
- wInput.className = 'form-input';
197
- wInput.value = imageInfo.width;
198
- wInput.min = 1;
199
-
200
- const lockBtn = document.createElement('button');
201
- lockBtn.type = 'button';
202
- lockBtn.className = 'btn btn-sm btn-ghost editor-toolbar-btn active';
203
- lockBtn.title = 'Lock aspect ratio';
204
- lockBtn.appendChild(makeIcon('lock'));
205
-
206
- const hLabel = document.createElement('label');
207
- hLabel.textContent = 'H';
208
- const hInput = document.createElement('input');
209
- hInput.type = 'number';
210
- hInput.className = 'form-input';
211
- hInput.value = imageInfo.height;
212
- hInput.min = 1;
213
-
214
- const origLabel = document.createElement('span');
215
- origLabel.className = 'text-muted';
216
- origLabel.style.marginLeft = 'auto';
217
- origLabel.textContent = `Original: ${imageInfo.width}\u00d7${imageInfo.height}`;
218
-
219
- resizeBar.appendChild(wLabel);
220
- resizeBar.appendChild(wInput);
221
- resizeBar.appendChild(lockBtn);
222
- resizeBar.appendChild(hLabel);
223
- resizeBar.appendChild(hInput);
224
- resizeBar.appendChild(origLabel);
225
-
226
- // ----------------------------------------------------------------
227
- // Effects panel
228
- // ----------------------------------------------------------------
229
- const effectsPanel = document.createElement('div');
230
- effectsPanel.className = 'image-editor-effects';
231
-
232
- // Tab bar
233
- const tabBar = document.createElement('div');
234
- tabBar.className = 'image-editor-tab-bar';
235
-
236
- const tabDefs = [
237
- {id: 'filters', label: 'Filters', icon: 'droplet'},
238
- {id: 'adjust', label: 'Adjust', icon: 'sliders'},
239
- {id: 'watermark', label: 'Watermark', icon: 'image-plus'},
240
- {id: 'border', label: 'Border', icon: 'frame'},
241
- {id: 'format', label: 'Format', icon: 'file-type'},
242
- ];
243
-
244
- const tabPanels = {};
245
- const tabBtns = {};
246
-
247
- tabDefs.forEach(({id, label, icon}, i) => {
248
- const btn = document.createElement('button');
249
- btn.type = 'button';
250
- btn.className = 'image-editor-tab-btn' + (i === 0 ? ' active' : '');
251
- btn.dataset.tab = id;
252
- btn.appendChild(makeIcon(icon));
253
- const lbl = document.createElement('span');
254
- lbl.textContent = label;
255
- btn.appendChild(lbl);
256
- btn.addEventListener('click', () => switchTab(id));
257
- tabBar.appendChild(btn);
258
- tabBtns[id] = btn;
259
-
260
- const panel = document.createElement('div');
261
- panel.className = 'image-editor-tab-panel';
262
- panel.style.display = i === 0 ? 'block' : 'none';
263
- tabPanels[id] = panel;
264
- });
265
-
266
- function switchTab(id) {
267
- tabDefs.forEach(({id: tid}) => {
268
- tabPanels[tid].style.display = tid === id ? 'block' : 'none';
269
- tabBtns[tid].classList.toggle('active', tid === id);
270
- });
271
- }
272
-
273
- effectsPanel.appendChild(tabBar);
274
- tabDefs.forEach(({id}) => effectsPanel.appendChild(tabPanels[id]));
275
-
276
- // ----------------------------------------------------------------
277
- // Filters tab
278
- // ----------------------------------------------------------------
279
- const presetDefs = [
280
- {id: 'grayscale', label: 'Grayscale', cssPreview: true},
281
- {id: 'sepia', label: 'Sepia', cssPreview: true},
282
- {id: 'warm', label: 'Warm', cssPreview: false},
283
- {id: 'cool', label: 'Cool', cssPreview: false},
284
- {id: 'enhance', label: 'Enhance', cssPreview: false},
285
- {id: 'sharpen', label: 'Sharpen', cssPreview: false},
286
- {id: 'soften', label: 'Soften', cssPreview: false},
287
- {id: 'invert', label: 'Invert', cssPreview: true},
288
- ];
289
-
290
- const presetsGrid = document.createElement('div');
291
- presetsGrid.className = 'ie-presets-grid';
292
-
293
- const presetBtns = {};
294
- presetDefs.forEach(({id, label}) => {
295
- const btn = document.createElement('button');
296
- btn.type = 'button';
297
- btn.className = 'btn btn-sm btn-ghost ie-preset-btn';
298
- btn.textContent = label;
299
- btn.addEventListener('click', () => {
300
- activePreset = activePreset === id ? null : id;
301
- presetDefs.forEach(({id: pid}) => {
302
- presetBtns[pid].classList.toggle('active', pid === activePreset);
303
- });
304
- applyCssPreview();
305
- });
306
- presetBtns[id] = btn;
307
- presetsGrid.appendChild(btn);
308
- });
309
-
310
- const serverNote = document.createElement('p');
311
- serverNote.className = 'ie-server-note';
312
- serverNote.textContent = 'Warm, Cool, Enhance, Sharpen, and Soften have no live preview — applied on save only.';
313
-
314
- tabPanels['filters'].appendChild(presetsGrid);
315
- tabPanels['filters'].appendChild(serverNote);
316
-
317
- // ----------------------------------------------------------------
318
- // Slider helper
319
- // ----------------------------------------------------------------
320
- const sliderRefs = {};
321
-
322
- function makeSlider(label, min, max, step, defaultVal, onChange) {
323
- const row = document.createElement('div');
324
- row.className = 'ie-slider-row';
325
-
326
- const lbl = document.createElement('label');
327
- lbl.className = 'ie-slider-label';
328
- lbl.textContent = label;
329
-
330
- const slider = document.createElement('input');
331
- slider.type = 'range';
332
- slider.min = min;
333
- slider.max = max;
334
- slider.step = step;
335
- slider.value = defaultVal;
336
- slider.className = 'ie-slider';
337
-
338
- const valDisplay = document.createElement('span');
339
- valDisplay.className = 'ie-slider-val';
340
- valDisplay.textContent = defaultVal;
341
-
342
- slider.addEventListener('input', () => {
343
- const v = parseFloat(slider.value);
344
- valDisplay.textContent = step < 1 ? v.toFixed(1) : String(Math.round(v));
345
- onChange(v);
346
- });
347
-
348
- row.appendChild(lbl);
349
- row.appendChild(slider);
350
- row.appendChild(valDisplay);
351
-
352
- return {
353
- row, slider, valDisplay, defaultVal,
354
- reset() {
355
- slider.value = defaultVal;
356
- valDisplay.textContent = defaultVal;
357
- },
358
- };
359
- }
360
-
361
- // ----------------------------------------------------------------
362
- // Adjust tab
363
- // ----------------------------------------------------------------
364
- const sliderDefs = [
365
- {key: 'brightness', label: 'Brightness', min: 0.2, max: 3.0, step: 0.1, def: 1.0},
366
- {key: 'contrast', label: 'Contrast', min: 0.2, max: 3.0, step: 0.1, def: 1.0},
367
- {key: 'saturation', label: 'Saturation', min: 0.0, max: 3.0, step: 0.1, def: 1.0},
368
- {key: 'blur', label: 'Blur', min: 0, max: 10, step: 0.5, def: 0},
369
- {key: 'sharpen', label: 'Sharpen', min: 0, max: 10, step: 0.5, def: 0},
370
- {key: 'gamma', label: 'Gamma', min: 0.5, max: 3.0, step: 0.1, def: 1.0},
371
- ];
372
-
373
- sliderDefs.forEach(({key, label, min, max, step, def}) => {
374
- const ref = makeSlider(label, min, max, step, def, (val) => {
375
- adjustments[key] = val;
376
- applyCssPreview();
377
- });
378
- sliderRefs[key] = ref;
379
- tabPanels['adjust'].appendChild(ref.row);
380
- });
381
-
382
- const resetAdjBtn = document.createElement('button');
383
- resetAdjBtn.type = 'button';
384
- resetAdjBtn.className = 'btn btn-sm btn-ghost';
385
- resetAdjBtn.style.marginTop = '0.5rem';
386
- resetAdjBtn.textContent = 'Reset adjustments';
387
- resetAdjBtn.addEventListener('click', () => {
388
- sliderDefs.forEach(({key, def}) => {
389
- adjustments[key] = def;
390
- sliderRefs[key].reset();
391
- });
392
- applyCssPreview();
393
- });
394
- tabPanels['adjust'].appendChild(resetAdjBtn);
395
-
396
- // ----------------------------------------------------------------
397
- // Watermark tab
398
- // ----------------------------------------------------------------
399
- const wmChooseBtn = document.createElement('button');
400
- wmChooseBtn.type = 'button';
401
- wmChooseBtn.className = 'btn btn-sm btn-secondary';
402
- wmChooseBtn.textContent = 'Choose watermark image\u2026';
403
-
404
- const wmPreviewRow = document.createElement('div');
405
- wmPreviewRow.className = 'ie-wm-preview-row';
406
- wmPreviewRow.style.display = 'none';
407
-
408
- const wmPreviewImg = document.createElement('img');
409
- wmPreviewImg.className = 'ie-wm-preview-img';
410
-
411
- const wmPreviewName = document.createElement('span');
412
- wmPreviewName.className = 'ie-wm-preview-name';
413
-
414
- const wmRemoveBtn = document.createElement('button');
415
- wmRemoveBtn.type = 'button';
416
- wmRemoveBtn.className = 'btn btn-sm btn-ghost';
417
- wmRemoveBtn.textContent = 'Remove';
418
- wmRemoveBtn.addEventListener('click', () => {
419
- watermarkState.image = null;
420
- wmPreviewRow.style.display = 'none';
421
- wmChooseBtn.style.display = '';
422
- });
423
-
424
- wmPreviewRow.appendChild(wmPreviewImg);
425
- wmPreviewRow.appendChild(wmPreviewName);
426
- wmPreviewRow.appendChild(wmRemoveBtn);
427
-
428
- wmChooseBtn.addEventListener('click', async () => {
429
- const picked = await openMediaPicker();
430
- if (picked) {
431
- watermarkState.image = picked.name;
432
- wmPreviewImg.src = picked.url;
433
- wmPreviewName.textContent = picked.name;
434
- wmPreviewRow.style.display = 'flex';
435
- wmChooseBtn.style.display = 'none';
436
- }
437
- });
438
-
439
- const wmPosRow = document.createElement('div');
440
- wmPosRow.className = 'ie-slider-row';
441
- const wmPosLabel = document.createElement('label');
442
- wmPosLabel.className = 'ie-slider-label';
443
- wmPosLabel.textContent = 'Position';
444
- const wmPosSelect = document.createElement('select');
445
- wmPosSelect.className = 'form-select ie-select';
446
- [
447
- 'top-left', 'top-center', 'top-right',
448
- 'center-left', 'center', 'center-right',
449
- 'bottom-left', 'bottom-center', 'bottom-right',
450
- ].forEach(pos => {
451
- const opt = document.createElement('option');
452
- opt.value = pos;
453
- opt.textContent = pos.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
454
- if (pos === 'bottom-right') opt.selected = true;
455
- wmPosSelect.appendChild(opt);
456
- });
457
- wmPosSelect.addEventListener('change', () => {
458
- watermarkState.position = wmPosSelect.value;
459
- });
460
- wmPosRow.appendChild(wmPosLabel);
461
- wmPosRow.appendChild(wmPosSelect);
462
-
463
- const wmOpacityRef = makeSlider('Opacity %', 0, 100, 1, 50, (val) => {
464
- watermarkState.opacity = val;
465
- });
466
- const wmScaleRef = makeSlider('Scale %', 5, 50, 1, 15, (val) => {
467
- watermarkState.scale = val;
468
- });
469
-
470
- tabPanels['watermark'].appendChild(wmChooseBtn);
471
- tabPanels['watermark'].appendChild(wmPreviewRow);
472
- tabPanels['watermark'].appendChild(wmPosRow);
473
- tabPanels['watermark'].appendChild(wmOpacityRef.row);
474
- tabPanels['watermark'].appendChild(wmScaleRef.row);
475
-
476
- // ----------------------------------------------------------------
477
- // Border tab
478
- // ----------------------------------------------------------------
479
- const borderWidthRef = makeSlider('Width (px)', 0, 200, 1, 0, (val) => {
480
- borderState.width = val;
481
- });
482
-
483
- const borderColourRow = document.createElement('div');
484
- borderColourRow.className = 'ie-slider-row';
485
- const borderColourLabel = document.createElement('label');
486
- borderColourLabel.className = 'ie-slider-label';
487
- borderColourLabel.textContent = 'Colour';
488
- const borderColourInput = document.createElement('input');
489
- borderColourInput.type = 'color';
490
- borderColourInput.value = '#000000';
491
- borderColourInput.className = 'ie-colour-input';
492
- borderColourInput.addEventListener('input', () => {
493
- borderState.colour = borderColourInput.value;
494
- });
495
- borderColourRow.appendChild(borderColourLabel);
496
- borderColourRow.appendChild(borderColourInput);
497
-
498
- const borderModeRow = document.createElement('div');
499
- borderModeRow.className = 'ie-slider-row';
500
- const borderModeLabel = document.createElement('label');
501
- borderModeLabel.className = 'ie-slider-label';
502
- borderModeLabel.textContent = 'Mode';
503
- const borderModeSelect = document.createElement('select');
504
- borderModeSelect.className = 'form-select ie-select';
505
- [['solid', 'Solid'], ['mirror', 'Mirror'], ['repeat', 'Repeat']].forEach(([val, lbl]) => {
506
- const opt = document.createElement('option');
507
- opt.value = val;
508
- opt.textContent = lbl;
509
- borderModeSelect.appendChild(opt);
510
- });
511
- borderModeSelect.addEventListener('change', () => {
512
- borderState.mode = borderModeSelect.value;
513
- });
514
- borderModeRow.appendChild(borderModeLabel);
515
- borderModeRow.appendChild(borderModeSelect);
516
-
517
- tabPanels['border'].appendChild(borderWidthRef.row);
518
- tabPanels['border'].appendChild(borderColourRow);
519
- tabPanels['border'].appendChild(borderModeRow);
520
-
521
- // ----------------------------------------------------------------
522
- // Format tab
523
- // ----------------------------------------------------------------
524
- const fmtRow = document.createElement('div');
525
- fmtRow.className = 'ie-slider-row';
526
- const fmtLabel = document.createElement('label');
527
- fmtLabel.className = 'ie-slider-label';
528
- fmtLabel.textContent = 'Output format';
529
- const fmtSelect = document.createElement('select');
530
- fmtSelect.className = 'form-select ie-select';
531
- [['', 'Keep original'], ['jpeg', 'JPEG'], ['png', 'PNG'], ['webp', 'WebP']].forEach(([val, lbl]) => {
532
- const opt = document.createElement('option');
533
- opt.value = val;
534
- opt.textContent = lbl;
535
- fmtSelect.appendChild(opt);
536
- });
537
- fmtRow.appendChild(fmtLabel);
538
- fmtRow.appendChild(fmtSelect);
539
-
540
- const fmtQualityRef = makeSlider('Quality', 1, 100, 1, 85, (val) => {
541
- formatState.quality = val;
542
- });
543
- fmtQualityRef.row.style.display = 'none';
544
-
545
- fmtSelect.addEventListener('change', () => {
546
- formatState.type = fmtSelect.value || null;
547
- fmtQualityRef.row.style.display =
548
- (fmtSelect.value === 'jpeg' || fmtSelect.value === 'webp') ? 'flex' : 'none';
549
- });
550
-
551
- tabPanels['format'].appendChild(fmtRow);
552
- tabPanels['format'].appendChild(fmtQualityRef.row);
553
-
554
- // --- Footer ---
555
- const footer = document.createElement('div');
556
- footer.className = 'image-editor-footer';
557
-
558
- const cancelBtn = document.createElement('button');
559
- cancelBtn.type = 'button';
560
- cancelBtn.className = 'btn btn-ghost';
561
- cancelBtn.textContent = 'Cancel';
562
-
563
- const footerRight = document.createElement('div');
564
- footerRight.style.display = 'flex';
565
- footerRight.style.gap = '0.5rem';
566
-
567
- const copyBtn = document.createElement('button');
568
- copyBtn.type = 'button';
569
- copyBtn.className = 'btn btn-secondary';
570
- copyBtn.textContent = 'Save as copy';
571
-
572
- const overwriteBtn = document.createElement('button');
573
- overwriteBtn.type = 'button';
574
- overwriteBtn.className = 'btn btn-primary';
575
- overwriteBtn.textContent = 'Overwrite';
576
-
577
- footerRight.appendChild(copyBtn);
578
- footerRight.appendChild(overwriteBtn);
579
- footer.appendChild(cancelBtn);
580
- footer.appendChild(footerRight);
581
-
582
- wrapper.appendChild(toolbar);
583
- wrapper.appendChild(canvas);
584
- wrapper.appendChild(resizeBar);
585
- wrapper.appendChild(effectsPanel);
586
- wrapper.appendChild(footer);
587
-
588
- // ----------------------------------------------------------------
589
- // Modal
590
- // ----------------------------------------------------------------
591
- const modal = E.modal({title: `Edit: ${file.name}`, size: 'lg'});
592
- modal.element.appendChild(wrapper);
593
- modal.open();
594
-
595
- // Capture .cropper-canvas BEFORE Cropper fires 'ready' on the img
596
- img.addEventListener('ready', () => {
597
- cropperCanvas = canvas.querySelector('.cropper-canvas');
598
- applyCssPreview();
599
- }, {once: true});
600
-
601
- requestAnimationFrame(() => {
602
- cropper = new Cropper(img, {
603
- viewMode: 1,
604
- autoCrop: false,
605
- zoomable: false,
606
- movable: true,
607
- });
608
- Domma.icons.scan(wrapper);
609
- });
610
-
611
- // ----------------------------------------------------------------
612
- // Toolbar interactions
613
- // ----------------------------------------------------------------
614
- cropBtn.addEventListener('click', () => {
615
- cropActive = !cropActive;
616
- cropActive ? cropper.crop() : cropper.clear();
617
- cropBtn.classList.toggle('active', cropActive);
618
- });
619
-
620
- rotateCW.addEventListener('click', () => {
621
- cropper.rotate(90);
622
- totalRotation = (totalRotation + 90) % 360;
623
- });
624
-
625
- rotateCCW.addEventListener('click', () => {
626
- cropper.rotate(-90);
627
- totalRotation = (totalRotation - 90 + 360) % 360;
628
- });
629
-
630
- flipHBtn.addEventListener('click', () => {
631
- flippedH = !flippedH;
632
- cropper.scaleX(flippedH ? -1 : 1);
633
- flipHBtn.classList.toggle('active', flippedH);
634
- });
635
-
636
- flipVBtn.addEventListener('click', () => {
637
- flippedV = !flippedV;
638
- cropper.scaleY(flippedV ? -1 : 1);
639
- flipVBtn.classList.toggle('active', flippedV);
640
- });
641
-
642
- // ----------------------------------------------------------------
643
- // Aspect lock + resize inputs
644
- // ----------------------------------------------------------------
645
- lockBtn.addEventListener('click', () => {
646
- aspectLocked = !aspectLocked;
647
- lockBtn.classList.toggle('active', aspectLocked);
648
- clearChildren(lockBtn);
649
- lockBtn.appendChild(makeIcon(aspectLocked ? 'lock' : 'unlock'));
650
- Domma.icons.scan(lockBtn);
651
- });
652
-
653
- wInput.addEventListener('input', () => {
654
- if (!aspectLocked) return;
655
- const w = parseInt(wInput.value, 10);
656
- if (w > 0) hInput.value = Math.round(w / aspectRatio);
657
- });
658
-
659
- hInput.addEventListener('input', () => {
660
- if (!aspectLocked) return;
661
- const h = parseInt(hInput.value, 10);
662
- if (h > 0) wInput.value = Math.round(h * aspectRatio);
663
- });
664
-
665
- // ----------------------------------------------------------------
666
- // CSS filter preview
667
- // ----------------------------------------------------------------
668
- function buildCssFilter() {
669
- const parts = [];
670
- if (activePreset === 'grayscale') parts.push('grayscale(1)');
671
- if (activePreset === 'sepia') parts.push('sepia(1)');
672
- if (activePreset === 'invert') parts.push('invert(1)');
673
- if (adjustments.brightness !== 1) parts.push(`brightness(${adjustments.brightness})`);
674
- if (adjustments.contrast !== 1) parts.push(`contrast(${adjustments.contrast})`);
675
- if (adjustments.saturation !== 1) parts.push(`saturate(${adjustments.saturation})`);
676
- if (adjustments.blur > 0) parts.push(`blur(${adjustments.blur}px)`);
677
- return parts.join(' ');
678
- }
679
-
680
- function applyCssPreview() {
681
- if (!cropperCanvas) return;
682
- cropperCanvas.style.filter = buildCssFilter();
683
- }
684
-
685
- // ----------------------------------------------------------------
686
- // Media picker (watermark image selection)
687
- // ----------------------------------------------------------------
688
- async function openMediaPicker() {
689
- return new Promise(async (resolveMedia) => {
690
- let mediaList;
691
- try {
692
- mediaList = await api.media.list();
693
- } catch (err) {
694
- E.toast(`Failed to load media: ${err.message}`, {type: 'error'});
695
- resolveMedia(null);
696
- return;
697
- }
698
-
699
- const images = mediaList.filter(m => /\.(jpg|jpeg|png|webp|gif)$/i.test(m.name));
700
-
701
- const pickerContent = document.createElement('div');
702
- pickerContent.className = 'ie-media-picker';
703
-
704
- if (images.length === 0) {
705
- const empty = document.createElement('p');
706
- empty.className = 'text-muted';
707
- empty.style.padding = '1rem 0';
708
- empty.textContent = 'No images in media library.';
709
- pickerContent.appendChild(empty);
710
- } else {
711
- const grid = document.createElement('div');
712
- grid.className = 'ie-media-picker-grid';
713
-
714
- images.forEach(imgItem => {
715
- const thumb = document.createElement('div');
716
- thumb.className = 'ie-media-thumb';
717
-
718
- const thumbImg = document.createElement('img');
719
- thumbImg.src = imgItem.url;
720
- thumbImg.alt = imgItem.name;
721
-
722
- const nameSpan = document.createElement('span');
723
- nameSpan.className = 'ie-media-thumb-name';
724
- nameSpan.textContent = imgItem.name;
725
-
726
- thumb.appendChild(thumbImg);
727
- thumb.appendChild(nameSpan);
728
-
729
- thumb.addEventListener('click', () => {
730
- pickerModal.close();
731
- resolveMedia(imgItem);
732
- });
733
-
734
- grid.appendChild(thumb);
735
- });
736
-
737
- pickerContent.appendChild(grid);
738
- }
739
-
740
- const cancelPickBtn = document.createElement('button');
741
- cancelPickBtn.type = 'button';
742
- cancelPickBtn.className = 'btn btn-sm btn-ghost';
743
- cancelPickBtn.style.marginTop = '0.75rem';
744
- cancelPickBtn.textContent = 'Cancel';
745
- cancelPickBtn.addEventListener('click', () => {
746
- pickerModal.close();
747
- resolveMedia(null);
748
- });
749
- pickerContent.appendChild(cancelPickBtn);
750
-
751
- const pickerModal = E.modal({title: 'Choose watermark image', size: 'md'});
752
- pickerModal.element.appendChild(pickerContent);
753
- pickerModal.open();
754
- });
755
- }
756
-
757
- // ----------------------------------------------------------------
758
- // Save helpers
759
- // ----------------------------------------------------------------
760
- function buildOperations() {
761
- const ops = {};
762
-
763
- if (totalRotation) ops.rotate = totalRotation;
764
- if (flippedV) ops.flip = true;
765
- if (flippedH) ops.flop = true;
766
-
767
- if (cropActive) {
768
- const data = cropper.getData(true);
769
- if (data.width > 0 && data.height > 0) {
770
- ops.crop = {left: data.x, top: data.y, width: data.width, height: data.height};
771
- }
772
- }
773
-
774
- const targetW = parseInt(wInput.value, 10) || null;
775
- const targetH = parseInt(hInput.value, 10) || null;
776
- if (targetW !== imageInfo.width || targetH !== imageInfo.height) {
777
- ops.resize = {width: targetW, height: targetH};
778
- }
779
-
780
- if (activePreset) ops.preset = activePreset;
781
-
782
- const adjOut = {};
783
- if (adjustments.brightness !== 1) adjOut.brightness = adjustments.brightness;
784
- if (adjustments.contrast !== 1) adjOut.contrast = adjustments.contrast;
785
- if (adjustments.saturation !== 1) adjOut.saturation = adjustments.saturation;
786
- if (adjustments.blur > 0) adjOut.blur = adjustments.blur;
787
- if (adjustments.sharpen > 0) adjOut.sharpen = adjustments.sharpen;
788
- if (adjustments.gamma !== 1) adjOut.gamma = adjustments.gamma;
789
- if (Object.keys(adjOut).length > 0) ops.adjustments = adjOut;
790
-
791
- if (watermarkState.image) {
792
- ops.watermark = {
793
- image: watermarkState.image,
794
- position: watermarkState.position,
795
- opacity: watermarkState.opacity / 100,
796
- scale: watermarkState.scale / 100,
797
- };
798
- }
799
-
800
- if (borderState.width > 0) {
801
- ops.border = {
802
- width: borderState.width,
803
- colour: borderState.colour,
804
- mode: borderState.mode,
805
- };
806
- }
807
-
808
- if (formatState.type) {
809
- ops.format = {type: formatState.type, quality: formatState.quality};
810
- }
811
-
812
- return ops;
813
- }
814
-
815
- async function doSave(saveAs) {
816
- overwriteBtn.disabled = true;
817
- copyBtn.disabled = true;
818
- try {
819
- const operations = buildOperations();
820
- let outputName = saveAs;
821
-
822
- // Overwrite + format change: rename file and delete the original
823
- if (!saveAs && formatState.type) {
824
- const srcExt = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
825
- const newExt = '.' + formatState.type;
826
- if (srcExt !== newExt) {
827
- const base = file.name.slice(0, file.name.lastIndexOf('.'));
828
- outputName = base + newExt;
829
- operations._deleteOriginal = file.name;
830
- }
831
- }
832
-
833
- await api.media.transform(file.name, {operations, saveAs: outputName || undefined});
834
- E.toast('Image saved.', {type: 'success'});
835
- closeEditor(true);
836
- } catch (err) {
837
- E.toast(`Save failed: ${err.message}`, {type: 'error'});
838
- overwriteBtn.disabled = false;
839
- copyBtn.disabled = false;
840
- }
841
- }
842
-
843
- overwriteBtn.addEventListener('click', async () => {
844
- if (!await E.confirm(`Overwrite "${file.name}"? This cannot be undone.`)) return;
845
- await doSave(null);
846
- });
847
-
848
- copyBtn.addEventListener('click', async () => {
849
- const ext = formatState.type ? '.' + formatState.type
850
- : file.name.substring(file.name.lastIndexOf('.'));
851
- const base = file.name.slice(0, file.name.lastIndexOf('.'));
852
- await doSave(`${base}-edited${ext}`);
853
- });
854
-
855
- cancelBtn.addEventListener('click', () => closeEditor(false));
856
-
857
- // ----------------------------------------------------------------
858
- // Cleanup
859
- // ----------------------------------------------------------------
860
- function closeEditor(saved) {
861
- if (cropper) {
862
- cropper.destroy();
863
- cropper = null;
864
- }
865
- modal.close();
866
- resolve(saved);
867
- }
868
- });
869
- }
1
+ import{api as Ee}from"../api.js";function Ae(){const o={"rotate-cw":{viewBox:"0 0 24 24",stroke:"currentColor",fill:"none",strokeWidth:2,path:"M21 2v6h-6M21 8A9 9 0 1 0 19.07 14.4"},"rotate-ccw":{viewBox:"0 0 24 24",stroke:"currentColor",fill:"none",strokeWidth:2,path:"M3 2v6h6M3 8a9 9 0 1 1 1.93 6.4"},"flip-h":{viewBox:"0 0 24 24",stroke:"currentColor",fill:"none",strokeWidth:2,paths:["M12 3v18","M4 6l4 6-4 6","M20 6l-4 6 4 6"]},"flip-v":{viewBox:"0 0 24 24",stroke:"currentColor",fill:"none",strokeWidth:2,paths:["M3 12h18","M6 4l6 4 6-4","M6 20l6-4 6 4"]},lock:{viewBox:"0 0 24 24",stroke:"currentColor",fill:"none",strokeWidth:2,paths:["M17 11H7a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2z","M12 8a4 4 0 0 0-4 4"],path:"M12 8V5a4 4 0 0 1 4 4v2"},unlock:{viewBox:"0 0 24 24",stroke:"currentColor",fill:"none",strokeWidth:2,paths:["M17 11H7a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2z","M8 11V7a4 4 0 0 1 8 0"]},"crop-icon":{viewBox:"0 0 24 24",stroke:"currentColor",fill:"none",strokeWidth:2,paths:["M6 2v14a2 2 0 0 0 2 2h14","M18 22V8a2 2 0 0 0-2-2H2"]},sliders:{viewBox:"0 0 24 24",stroke:"currentColor",fill:"none",strokeWidth:2,paths:["M4 21v-7","M4 10V3","M12 21v-9","M12 8V3","M20 21v-5","M20 12V3","M1 14h6","M9 8h6","M17 16h6"]},droplet:{viewBox:"0 0 24 24",stroke:"currentColor",fill:"none",strokeWidth:2,path:"M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"},"image-plus":{viewBox:"0 0 24 24",stroke:"currentColor",fill:"none",strokeWidth:2,paths:["M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7","M16 5h6","M19 2v6","M3 15l5-5 4 4 3-3 4 4"]},frame:{viewBox:"0 0 24 24",stroke:"currentColor",fill:"none",strokeWidth:2,paths:["M7 3H3v4","M17 3h4v4","M7 21H3v-4","M17 21h4v-4"]},"file-type":{viewBox:"0 0 24 24",stroke:"currentColor",fill:"none",strokeWidth:2,paths:["M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z","M14 2v6h6","M10 13l-2 4h8l-2-4"]}};for(const[m,M]of Object.entries(o))try{I.register(m,M)}catch{}}function oe(o){const m=document.createElement("span");return m.setAttribute("data-icon",o),m}function K(o,m,M){const i=document.createElement("button");return i.className=o,i.type="button",i.title=M,i.appendChild(oe(m)),i}function je(){const o=document.createElement("span");return o.className="image-editor-sep",o}function Ve(o){for(;o.firstChild;)o.removeChild(o.firstChild)}export async function openImageEditor(o){Ae();let m;try{m=await Ee.media.info(o.name)}catch(M){return E.toast(`Cannot open editor: ${M.message}`,{type:"error"}),!1}return new Promise(M=>{let i=null,F=!1,j=0,D=!1,q=!1,O=!0;const ye=m.width/m.height;let se=null,y=null;const r={brightness:1,contrast:1,saturation:1,blur:0,sharpen:0,gamma:1},u={image:null,position:"bottom-right",opacity:50,scale:15},L={width:0,colour:"#000000",mode:"solid"},v={type:null,quality:85},k=document.createElement("div");k.className="image-editor";const C=document.createElement("div");C.className="image-editor-toolbar";const le=K("btn btn-sm btn-ghost editor-toolbar-btn","crop-icon","Toggle crop"),ke=K("btn btn-sm btn-ghost editor-toolbar-btn","rotate-cw","Rotate clockwise"),xe=K("btn btn-sm btn-ghost editor-toolbar-btn","rotate-ccw","Rotate counter-clockwise"),re=K("btn btn-sm btn-ghost editor-toolbar-btn","flip-h","Flip horizontal"),ie=K("btn btn-sm btn-ghost editor-toolbar-btn","flip-v","Flip vertical");C.appendChild(le),C.appendChild(je()),C.appendChild(xe),C.appendChild(ke),C.appendChild(je()),C.appendChild(re),C.appendChild(ie);const U=document.createElement("div");U.className="image-editor-canvas";const $=document.createElement("img");$.src=o.url+"?t="+Date.now(),$.alt=o.name,$.style.maxWidth="100%",U.appendChild($);const x=document.createElement("div");x.className="image-editor-resize";const Ne=document.createElement("label");Ne.textContent="W";const g=document.createElement("input");g.type="number",g.className="form-input",g.value=m.width,g.min=1;const h=document.createElement("button");h.type="button",h.className="btn btn-sm btn-ghost editor-toolbar-btn active",h.title="Lock aspect ratio",h.appendChild(oe("lock"));const Me=document.createElement("label");Me.textContent="H";const w=document.createElement("input");w.type="number",w.className="form-input",w.value=m.height,w.min=1;const X=document.createElement("span");X.className="text-muted",X.style.marginLeft="auto",X.textContent=`Original: ${m.width}\xD7${m.height}`,x.appendChild(Ne),x.appendChild(g),x.appendChild(h),x.appendChild(Me),x.appendChild(w),x.appendChild(X);const Y=document.createElement("div");Y.className="image-editor-effects";const ce=document.createElement("div");ce.className="image-editor-tab-bar";const de=[{id:"filters",label:"Filters",icon:"droplet"},{id:"adjust",label:"Adjust",icon:"sliders"},{id:"watermark",label:"Watermark",icon:"image-plus"},{id:"border",label:"Border",icon:"frame"},{id:"format",label:"Format",icon:"file-type"}],c={},Le={};de.forEach(({id:e,label:t,icon:a},s)=>{const n=document.createElement("button");n.type="button",n.className="image-editor-tab-btn"+(s===0?" active":""),n.dataset.tab=e,n.appendChild(oe(a));const p=document.createElement("span");p.textContent=t,n.appendChild(p),n.addEventListener("click",()=>Oe(e)),ce.appendChild(n),Le[e]=n;const l=document.createElement("div");l.className="image-editor-tab-panel",l.style.display=s===0?"block":"none",c[e]=l});function Oe(e){de.forEach(({id:t})=>{c[t].style.display=t===e?"block":"none",Le[t].classList.toggle("active",t===e)})}Y.appendChild(ce),de.forEach(({id:e})=>Y.appendChild(c[e]));const Be=[{id:"grayscale",label:"Grayscale",cssPreview:!0},{id:"sepia",label:"Sepia",cssPreview:!0},{id:"warm",label:"Warm",cssPreview:!1},{id:"cool",label:"Cool",cssPreview:!1},{id:"enhance",label:"Enhance",cssPreview:!1},{id:"sharpen",label:"Sharpen",cssPreview:!1},{id:"soften",label:"Soften",cssPreview:!1},{id:"invert",label:"Invert",cssPreview:!0}],me=document.createElement("div");me.className="ie-presets-grid";const Pe={};Be.forEach(({id:e,label:t})=>{const a=document.createElement("button");a.type="button",a.className="btn btn-sm btn-ghost ie-preset-btn",a.textContent=t,a.addEventListener("click",()=>{y=y===e?null:e,Be.forEach(({id:s})=>{Pe[s].classList.toggle("active",s===y)}),ae()}),Pe[e]=a,me.appendChild(a)});const pe=document.createElement("p");pe.className="ie-server-note",pe.textContent="Warm, Cool, Enhance, Sharpen, and Soften have no live preview \u2014 applied on save only.",c.filters.appendChild(me),c.filters.appendChild(pe);const Se={};function A(e,t,a,s,n,p){const l=document.createElement("div");l.className="ie-slider-row";const b=document.createElement("label");b.className="ie-slider-label",b.textContent=e;const d=document.createElement("input");d.type="range",d.min=t,d.max=a,d.step=s,d.value=n,d.className="ie-slider";const f=document.createElement("span");return f.className="ie-slider-val",f.textContent=n,d.addEventListener("input",()=>{const R=parseFloat(d.value);f.textContent=s<1?R.toFixed(1):String(Math.round(R)),p(R)}),l.appendChild(b),l.appendChild(d),l.appendChild(f),{row:l,slider:d,valDisplay:f,defaultVal:n,reset(){d.value=n,f.textContent=n}}}const We=[{key:"brightness",label:"Brightness",min:.2,max:3,step:.1,def:1},{key:"contrast",label:"Contrast",min:.2,max:3,step:.1,def:1},{key:"saturation",label:"Saturation",min:0,max:3,step:.1,def:1},{key:"blur",label:"Blur",min:0,max:10,step:.5,def:0},{key:"sharpen",label:"Sharpen",min:0,max:10,step:.5,def:0},{key:"gamma",label:"Gamma",min:.5,max:3,step:.1,def:1}];We.forEach(({key:e,label:t,min:a,max:s,step:n,def:p})=>{const l=A(t,a,s,n,p,b=>{r[e]=b,ae()});Se[e]=l,c.adjust.appendChild(l.row)});const z=document.createElement("button");z.type="button",z.className="btn btn-sm btn-ghost",z.style.marginTop="0.5rem",z.textContent="Reset adjustments",z.addEventListener("click",()=>{We.forEach(({key:e,def:t})=>{r[e]=t,Se[e].reset()}),ae()}),c.adjust.appendChild(z);const B=document.createElement("button");B.type="button",B.className="btn btn-sm btn-secondary",B.textContent="Choose watermark image\u2026";const N=document.createElement("div");N.className="ie-wm-preview-row",N.style.display="none";const ue=document.createElement("img");ue.className="ie-wm-preview-img";const he=document.createElement("span");he.className="ie-wm-preview-name";const V=document.createElement("button");V.type="button",V.className="btn btn-sm btn-ghost",V.textContent="Remove",V.addEventListener("click",()=>{u.image=null,N.style.display="none",B.style.display=""}),N.appendChild(ue),N.appendChild(he),N.appendChild(V),B.addEventListener("click",async()=>{const e=await De();e&&(u.image=e.name,ue.src=e.url,he.textContent=e.name,N.style.display="flex",B.style.display="none")});const _=document.createElement("div");_.className="ie-slider-row";const be=document.createElement("label");be.className="ie-slider-label",be.textContent="Position";const G=document.createElement("select");G.className="form-select ie-select",["top-left","top-center","top-right","center-left","center","center-right","bottom-left","bottom-center","bottom-right"].forEach(e=>{const t=document.createElement("option");t.value=e,t.textContent=e.replace(/-/g," ").replace(/\b\w/g,a=>a.toUpperCase()),e==="bottom-right"&&(t.selected=!0),G.appendChild(t)}),G.addEventListener("change",()=>{u.position=G.value}),_.appendChild(be),_.appendChild(G);const $e=A("Opacity %",0,100,1,50,e=>{u.opacity=e}),ze=A("Scale %",5,50,1,15,e=>{u.scale=e});c.watermark.appendChild(B),c.watermark.appendChild(N),c.watermark.appendChild(_),c.watermark.appendChild($e.row),c.watermark.appendChild(ze.row);const He=A("Width (px)",0,200,1,0,e=>{L.width=e}),Z=document.createElement("div");Z.className="ie-slider-row";const fe=document.createElement("label");fe.className="ie-slider-label",fe.textContent="Colour";const H=document.createElement("input");H.type="color",H.value="#000000",H.className="ie-colour-input",H.addEventListener("input",()=>{L.colour=H.value}),Z.appendChild(fe),Z.appendChild(H);const ee=document.createElement("div");ee.className="ie-slider-row";const ve=document.createElement("label");ve.className="ie-slider-label",ve.textContent="Mode";const T=document.createElement("select");T.className="form-select ie-select",[["solid","Solid"],["mirror","Mirror"],["repeat","Repeat"]].forEach(([e,t])=>{const a=document.createElement("option");a.value=e,a.textContent=t,T.appendChild(a)}),T.addEventListener("change",()=>{L.mode=T.value}),ee.appendChild(ve),ee.appendChild(T),c.border.appendChild(He.row),c.border.appendChild(Z),c.border.appendChild(ee);const te=document.createElement("div");te.className="ie-slider-row";const Ce=document.createElement("label");Ce.className="ie-slider-label",Ce.textContent="Output format";const P=document.createElement("select");P.className="form-select ie-select",[["","Keep original"],["jpeg","JPEG"],["png","PNG"],["webp","WebP"]].forEach(([e,t])=>{const a=document.createElement("option");a.value=e,a.textContent=t,P.appendChild(a)}),te.appendChild(Ce),te.appendChild(P);const ge=A("Quality",1,100,1,85,e=>{v.quality=e});ge.row.style.display="none",P.addEventListener("change",()=>{v.type=P.value||null,ge.row.style.display=P.value==="jpeg"||P.value==="webp"?"flex":"none"}),c.format.appendChild(te),c.format.appendChild(ge.row);const ne=document.createElement("div");ne.className="image-editor-footer";const Q=document.createElement("button");Q.type="button",Q.className="btn btn-ghost",Q.textContent="Cancel";const J=document.createElement("div");J.style.display="flex",J.style.gap="0.5rem";const S=document.createElement("button");S.type="button",S.className="btn btn-secondary",S.textContent="Save as copy";const W=document.createElement("button");W.type="button",W.className="btn btn-primary",W.textContent="Overwrite",J.appendChild(S),J.appendChild(W),ne.appendChild(Q),ne.appendChild(J),k.appendChild(C),k.appendChild(U),k.appendChild(x),k.appendChild(Y),k.appendChild(ne);const we=E.modal({title:`Edit: ${o.name}`,size:"lg"});we.element.appendChild(k),we.open(),$.addEventListener("ready",()=>{se=U.querySelector(".cropper-canvas"),ae()},{once:!0}),requestAnimationFrame(()=>{i=new Cropper($,{viewMode:1,autoCrop:!1,zoomable:!1,movable:!0}),Domma.icons.scan(k)}),le.addEventListener("click",()=>{F=!F,F?i.crop():i.clear(),le.classList.toggle("active",F)}),ke.addEventListener("click",()=>{i.rotate(90),j=(j+90)%360}),xe.addEventListener("click",()=>{i.rotate(-90),j=(j-90+360)%360}),re.addEventListener("click",()=>{D=!D,i.scaleX(D?-1:1),re.classList.toggle("active",D)}),ie.addEventListener("click",()=>{q=!q,i.scaleY(q?-1:1),ie.classList.toggle("active",q)}),h.addEventListener("click",()=>{O=!O,h.classList.toggle("active",O),Ve(h),h.appendChild(oe(O?"lock":"unlock")),Domma.icons.scan(h)}),g.addEventListener("input",()=>{if(!O)return;const e=parseInt(g.value,10);e>0&&(w.value=Math.round(e/ye))}),w.addEventListener("input",()=>{if(!O)return;const e=parseInt(w.value,10);e>0&&(g.value=Math.round(e*ye))});function Fe(){const e=[];return y==="grayscale"&&e.push("grayscale(1)"),y==="sepia"&&e.push("sepia(1)"),y==="invert"&&e.push("invert(1)"),r.brightness!==1&&e.push(`brightness(${r.brightness})`),r.contrast!==1&&e.push(`contrast(${r.contrast})`),r.saturation!==1&&e.push(`saturate(${r.saturation})`),r.blur>0&&e.push(`blur(${r.blur}px)`),e.join(" ")}function ae(){se&&(se.style.filter=Fe())}async function De(){return new Promise(async e=>{let t;try{t=await Ee.media.list()}catch(l){E.toast(`Failed to load media: ${l.message}`,{type:"error"}),e(null);return}const a=t.filter(l=>/\.(jpg|jpeg|png|webp|gif)$/i.test(l.name)),s=document.createElement("div");if(s.className="ie-media-picker",a.length===0){const l=document.createElement("p");l.className="text-muted",l.style.padding="1rem 0",l.textContent="No images in media library.",s.appendChild(l)}else{const l=document.createElement("div");l.className="ie-media-picker-grid",a.forEach(b=>{const d=document.createElement("div");d.className="ie-media-thumb";const f=document.createElement("img");f.src=b.url,f.alt=b.name;const R=document.createElement("span");R.className="ie-media-thumb-name",R.textContent=b.name,d.appendChild(f),d.appendChild(R),d.addEventListener("click",()=>{p.close(),e(b)}),l.appendChild(d)}),s.appendChild(l)}const n=document.createElement("button");n.type="button",n.className="btn btn-sm btn-ghost",n.style.marginTop="0.75rem",n.textContent="Cancel",n.addEventListener("click",()=>{p.close(),e(null)}),s.appendChild(n);const p=E.modal({title:"Choose watermark image",size:"md"});p.element.appendChild(s),p.open()})}function qe(){const e={};if(j&&(e.rotate=j),q&&(e.flip=!0),D&&(e.flop=!0),F){const n=i.getData(!0);n.width>0&&n.height>0&&(e.crop={left:n.x,top:n.y,width:n.width,height:n.height})}const t=parseInt(g.value,10)||null,a=parseInt(w.value,10)||null;(t!==m.width||a!==m.height)&&(e.resize={width:t,height:a}),y&&(e.preset=y);const s={};return r.brightness!==1&&(s.brightness=r.brightness),r.contrast!==1&&(s.contrast=r.contrast),r.saturation!==1&&(s.saturation=r.saturation),r.blur>0&&(s.blur=r.blur),r.sharpen>0&&(s.sharpen=r.sharpen),r.gamma!==1&&(s.gamma=r.gamma),Object.keys(s).length>0&&(e.adjustments=s),u.image&&(e.watermark={image:u.image,position:u.position,opacity:u.opacity/100,scale:u.scale/100}),L.width>0&&(e.border={width:L.width,colour:L.colour,mode:L.mode}),v.type&&(e.format={type:v.type,quality:v.quality}),e}async function Re(e){W.disabled=!0,S.disabled=!0;try{const t=qe();let a=e;if(!e&&v.type){const s=o.name.substring(o.name.lastIndexOf(".")).toLowerCase(),n="."+v.type;s!==n&&(a=o.name.slice(0,o.name.lastIndexOf("."))+n,t._deleteOriginal=o.name)}await Ee.media.transform(o.name,{operations:t,saveAs:a||void 0}),E.toast("Image saved.",{type:"success"}),Ie(!0)}catch(t){E.toast(`Save failed: ${t.message}`,{type:"error"}),W.disabled=!1,S.disabled=!1}}W.addEventListener("click",async()=>{await E.confirm(`Overwrite "${o.name}"? This cannot be undone.`)&&await Re(null)}),S.addEventListener("click",async()=>{const e=v.type?"."+v.type:o.name.substring(o.name.lastIndexOf(".")),t=o.name.slice(0,o.name.lastIndexOf("."));await Re(`${t}-edited${e}`)}),Q.addEventListener("click",()=>Ie(!1));function Ie(e){i&&(i.destroy(),i=null),we.close(),M(e)}})}