senangwebs-photobooth 1.0.1 → 2.0.2

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 (44) hide show
  1. package/README.md +219 -235
  2. package/dist/swp.css +884 -344
  3. package/dist/swp.js +1 -1
  4. package/examples/data-attribute.html +69 -0
  5. package/examples/index.html +56 -51
  6. package/examples/studio.html +83 -0
  7. package/package.json +12 -5
  8. package/src/css/swp.css +884 -344
  9. package/src/js/core/Canvas.js +398 -0
  10. package/src/js/core/EventEmitter.js +188 -0
  11. package/src/js/core/History.js +250 -0
  12. package/src/js/core/Keyboard.js +323 -0
  13. package/src/js/filters/FilterManager.js +248 -0
  14. package/src/js/index.js +48 -0
  15. package/src/js/io/Clipboard.js +52 -0
  16. package/src/js/io/FileManager.js +150 -0
  17. package/src/js/layers/BlendModes.js +342 -0
  18. package/src/js/layers/Layer.js +415 -0
  19. package/src/js/layers/LayerManager.js +459 -0
  20. package/src/js/selection/Selection.js +167 -0
  21. package/src/js/swp.js +297 -709
  22. package/src/js/tools/BaseTool.js +264 -0
  23. package/src/js/tools/BrushTool.js +314 -0
  24. package/src/js/tools/CropTool.js +400 -0
  25. package/src/js/tools/EraserTool.js +155 -0
  26. package/src/js/tools/EyedropperTool.js +184 -0
  27. package/src/js/tools/FillTool.js +109 -0
  28. package/src/js/tools/GradientTool.js +141 -0
  29. package/src/js/tools/HandTool.js +51 -0
  30. package/src/js/tools/MarqueeTool.js +103 -0
  31. package/src/js/tools/MoveTool.js +465 -0
  32. package/src/js/tools/ShapeTool.js +285 -0
  33. package/src/js/tools/TextTool.js +253 -0
  34. package/src/js/tools/ToolManager.js +277 -0
  35. package/src/js/tools/ZoomTool.js +68 -0
  36. package/src/js/ui/ColorManager.js +71 -0
  37. package/src/js/ui/UI.js +1211 -0
  38. package/swp_preview1.png +0 -0
  39. package/swp_preview2.png +0 -0
  40. package/webpack.config.js +4 -11
  41. package/dist/styles.js +0 -1
  42. package/examples/customization.html +0 -360
  43. package/spec.md +0 -239
  44. package/swp_preview.png +0 -0
@@ -0,0 +1,1211 @@
1
+ /**
2
+ * SenangWebs Photobooth - UI Manager
3
+ * TOAST UI-Inspired Simple Layout
4
+ * @version 3.0.0
5
+ */
6
+
7
+ import { Events } from '../core/EventEmitter.js';
8
+ import '@bookklik/senangstart-icons';
9
+
10
+ export class UI {
11
+ constructor(app) {
12
+ this.app = app;
13
+ this.container = null;
14
+ this.currentMenu = null;
15
+ this.isFullscreen = false;
16
+ }
17
+
18
+ init(container) {
19
+ this.container = container;
20
+ this.container.classList.add('swp-app');
21
+ this.createLayout();
22
+ this.bindEvents();
23
+ }
24
+
25
+ createLayout() {
26
+ this.container.innerHTML = `
27
+ <!-- Header Bar -->
28
+ <div class="swp-header">
29
+ <div class="swp-header-left">
30
+ <button class="swp-header-btn" data-action="load" title="Load Image">
31
+ <ss-icon icon="folder-open" thickness="2"></ss-icon>
32
+ <span>Load</span>
33
+ </button>
34
+ <div class="swp-download-dropdown">
35
+ <button class="swp-header-btn" data-action="toggle-download" title="Download">
36
+ <ss-icon icon="save" thickness="2"></ss-icon>
37
+ <span>Download</span>
38
+ <ss-icon icon="chevron-down" thickness="2" class="swp-dropdown-arrow"></ss-icon>
39
+ </button>
40
+ <div class="swp-dropdown-menu" hidden>
41
+ <button class="swp-dropdown-item" data-format="png">PNG</button>
42
+ <button class="swp-dropdown-item" data-format="jpeg">JPEG</button>
43
+ <button class="swp-dropdown-item" data-format="webp">WebP</button>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ <div class="swp-header-center">
48
+ <button class="swp-icon-btn" data-action="undo" title="Undo (Ctrl+Z)">
49
+ <ss-icon icon="arrow-rotate-ccw" thickness="2"></ss-icon>
50
+ </button>
51
+ <button class="swp-icon-btn" data-action="redo" title="Redo (Ctrl+Shift+Z)">
52
+ <ss-icon icon="arrow-rotate-cw" thickness="2"></ss-icon>
53
+ </button>
54
+ <div class="swp-divider"></div>
55
+ <button class="swp-icon-btn" data-action="history" title="History">
56
+ <ss-icon icon="clock" thickness="2"></ss-icon>
57
+ </button>
58
+ <button class="swp-icon-btn" data-action="layers" title="Layers">
59
+ <ss-icon icon="layer-stacks" thickness="2"></ss-icon>
60
+ </button>
61
+ <div class="swp-divider"></div>
62
+ <button class="swp-icon-btn" data-action="reset" title="Reset">
63
+ <ss-icon icon="time-reset" thickness="2"></ss-icon>
64
+ </button>
65
+ </div>
66
+ <div class="swp-header-right">
67
+ <button class="swp-icon-btn" data-action="center" title="Center Canvas">
68
+ <ss-icon icon="container" thickness="2"></ss-icon>
69
+ </button>
70
+ <button class="swp-icon-btn" data-action="fullscreen" title="Fullscreen">
71
+ <ss-icon icon="focus" thickness="2"></ss-icon>
72
+ </button>
73
+ </div>
74
+ </div>
75
+
76
+ <!-- Main Workspace (full width canvas) -->
77
+ <div class="swp-workspace"></div>
78
+
79
+ <!-- Side Panel (History / Layers) -->
80
+ <div class="swp-side-panel" hidden>
81
+ <div class="swp-side-panel-header">
82
+ <span class="swp-side-panel-title">Panel</span>
83
+ <button class="swp-icon-btn swp-side-panel-close" data-action="close-panel">
84
+ <ss-icon icon="cross" thickness="2"></ss-icon>
85
+ </button>
86
+ </div>
87
+ <div class="swp-side-panel-content"></div>
88
+ </div>
89
+ <!-- Sub-menu Panel (contextual options) -->
90
+ <div class="swp-submenu" hidden></div>
91
+
92
+ <!-- Bottom Menu Bar -->
93
+ <div class="swp-menu-bar">
94
+ <button class="swp-menu-item" data-menu="crop">
95
+ <ss-icon icon="crop" thickness="2"></ss-icon>
96
+ <span>Crop</span>
97
+ </button>
98
+ <button class="swp-menu-item" data-menu="rotate">
99
+ <ss-icon icon="arrow-path" thickness="2"></ss-icon>
100
+ <span>Rotate</span>
101
+ </button>
102
+ <button class="swp-menu-item" data-menu="flip">
103
+ <ss-icon icon="arrow-left-arrow-right" thickness="2"></ss-icon>
104
+ <span>Flip</span>
105
+ </button>
106
+ <button class="swp-menu-item" data-menu="resize">
107
+ <ss-icon icon="sliders-vertical" thickness="2"></ss-icon>
108
+ <span>Resize</span>
109
+ </button>
110
+ <button class="swp-menu-item" data-menu="draw">
111
+ <ss-icon icon="pencil" thickness="2"></ss-icon>
112
+ <span>Draw</span>
113
+ </button>
114
+ <button class="swp-menu-item" data-menu="shape">
115
+ <ss-icon icon="shapes" thickness="2"></ss-icon>
116
+ <span>Shape</span>
117
+ </button>
118
+ <button class="swp-menu-item" data-menu="text">
119
+ <ss-icon icon="text" thickness="2"></ss-icon>
120
+ <span>Text</span>
121
+ </button>
122
+ <button class="swp-menu-item" data-menu="filter">
123
+ <ss-icon icon="magic-wand" thickness="2"></ss-icon>
124
+ <span>Filter</span>
125
+ </button>
126
+ </div>
127
+ `;
128
+
129
+ this.bindHeaderActions();
130
+ this.bindMenuActions();
131
+ }
132
+
133
+ bindHeaderActions() {
134
+ const header = this.container.querySelector('.swp-header');
135
+ const sidePanel = this.container.querySelector('.swp-side-panel');
136
+
137
+ // Close panel button
138
+ sidePanel?.querySelector('[data-action="close-panel"]')?.addEventListener('click', () => {
139
+ this.closeSidePanel();
140
+ });
141
+
142
+ // Download dropdown handling
143
+ const downloadDropdown = this.container.querySelector('.swp-download-dropdown');
144
+ const dropdownMenu = downloadDropdown?.querySelector('.swp-dropdown-menu');
145
+
146
+ // Format selection
147
+ dropdownMenu?.querySelectorAll('[data-format]').forEach(item => {
148
+ item.addEventListener('click', (e) => {
149
+ e.stopPropagation();
150
+ const format = item.dataset.format;
151
+ this.app.file.export(format);
152
+ dropdownMenu.hidden = true;
153
+ });
154
+ });
155
+
156
+ // Close dropdown when clicking outside
157
+ document.addEventListener('click', (e) => {
158
+ if (dropdownMenu && !downloadDropdown.contains(e.target)) {
159
+ dropdownMenu.hidden = true;
160
+ }
161
+ });
162
+
163
+ header.addEventListener('click', (e) => {
164
+ const btn = e.target.closest('[data-action]');
165
+ if (!btn) return;
166
+
167
+ const action = btn.dataset.action;
168
+ switch (action) {
169
+ case 'load':
170
+ this.openFileDialog();
171
+ break;
172
+ case 'toggle-download':
173
+ if (dropdownMenu) {
174
+ dropdownMenu.hidden = !dropdownMenu.hidden;
175
+ }
176
+ break;
177
+ case 'undo':
178
+ this.app.history.undo();
179
+ break;
180
+ case 'redo':
181
+ this.app.history.redo();
182
+ break;
183
+ case 'history':
184
+ this.toggleSidePanel('history');
185
+ break;
186
+ case 'layers':
187
+ this.toggleSidePanel('layers');
188
+ break;
189
+ case 'reset':
190
+ this.resetCanvas();
191
+ break;
192
+ case 'center':
193
+ this.app.canvas.fitToScreen();
194
+ break;
195
+ case 'fullscreen':
196
+ this.toggleFullscreen();
197
+ break;
198
+ }
199
+ });
200
+ }
201
+
202
+ bindMenuActions() {
203
+ const menuBar = this.container.querySelector('.swp-menu-bar');
204
+
205
+ menuBar.addEventListener('click', (e) => {
206
+ const menuItem = e.target.closest('.swp-menu-item');
207
+ if (!menuItem) return;
208
+
209
+ const menu = menuItem.dataset.menu;
210
+ this.selectMenu(menu);
211
+ });
212
+ }
213
+
214
+ selectMenu(menu) {
215
+ // Toggle off if same menu clicked
216
+ if (this.currentMenu === menu) {
217
+ this.closeSubmenu();
218
+ return;
219
+ }
220
+
221
+ this.currentMenu = menu;
222
+
223
+ // Update active state
224
+ this.container.querySelectorAll('.swp-menu-item').forEach(item => {
225
+ item.classList.toggle('active', item.dataset.menu === menu);
226
+ });
227
+
228
+ // Activate corresponding tool
229
+ this.activateToolForMenu(menu);
230
+
231
+ // Show submenu
232
+ this.showSubmenu(menu);
233
+ }
234
+
235
+ activateToolForMenu(menu) {
236
+ const toolMap = {
237
+ 'crop': 'crop',
238
+ 'rotate': 'move',
239
+ 'flip': 'move',
240
+ 'resize': 'move',
241
+ 'draw': 'brush',
242
+ 'shape': 'shape',
243
+ 'text': 'text',
244
+ 'filter': 'move'
245
+ };
246
+
247
+ const toolName = toolMap[menu];
248
+ if (toolName) {
249
+ this.app.tools.setTool(toolName);
250
+ }
251
+ }
252
+
253
+ showSubmenu(menu) {
254
+ const submenu = this.container.querySelector('.swp-submenu');
255
+ submenu.hidden = false;
256
+
257
+ switch (menu) {
258
+ case 'crop':
259
+ this.renderCropSubmenu(submenu);
260
+ break;
261
+ case 'rotate':
262
+ this.renderRotateSubmenu(submenu);
263
+ break;
264
+ case 'flip':
265
+ this.renderFlipSubmenu(submenu);
266
+ break;
267
+ case 'resize':
268
+ this.renderResizeSubmenu(submenu);
269
+ break;
270
+ case 'draw':
271
+ this.renderDrawSubmenu(submenu);
272
+ break;
273
+ case 'shape':
274
+ this.renderShapeSubmenu(submenu);
275
+ break;
276
+ case 'text':
277
+ this.renderTextSubmenu(submenu);
278
+ break;
279
+ case 'filter':
280
+ this.renderFilterSubmenu(submenu);
281
+ break;
282
+ }
283
+ }
284
+
285
+ closeSubmenu() {
286
+ this.currentMenu = null;
287
+ this.container.querySelectorAll('.swp-menu-item').forEach(item => {
288
+ item.classList.remove('active');
289
+ });
290
+ const submenu = this.container.querySelector('.swp-submenu');
291
+ submenu.hidden = true;
292
+ submenu.innerHTML = '';
293
+
294
+ // Reset tool to move
295
+ this.app.tools.setTool('move');
296
+ }
297
+
298
+ renderCropSubmenu(submenu) {
299
+ submenu.innerHTML = `
300
+ <div class="swp-submenu-content">
301
+ <div class="swp-submenu-title">Crop</div>
302
+ <div class="swp-submenu-group">
303
+ <label class="swp-submenu-label">Preset</label>
304
+ <div class="swp-btn-group">
305
+ <button class="swp-submenu-btn active" data-ratio="free">Free</button>
306
+ <button class="swp-submenu-btn" data-ratio="1:1">1:1</button>
307
+ <button class="swp-submenu-btn" data-ratio="4:3">4:3</button>
308
+ <button class="swp-submenu-btn" data-ratio="16:9">16:9</button>
309
+ </div>
310
+ </div>
311
+ <div class="swp-submenu-actions">
312
+ <button class="swp-btn" data-action="cancel">Cancel</button>
313
+ <button class="swp-btn swp-btn-primary" data-action="apply">Apply</button>
314
+ </div>
315
+ </div>
316
+ `;
317
+
318
+ this.bindCropSubmenuEvents(submenu);
319
+ }
320
+
321
+ bindCropSubmenuEvents(submenu) {
322
+ submenu.querySelectorAll('[data-ratio]').forEach(btn => {
323
+ btn.addEventListener('click', () => {
324
+ submenu.querySelectorAll('[data-ratio]').forEach(b => b.classList.remove('active'));
325
+ btn.classList.add('active');
326
+
327
+ const ratio = btn.dataset.ratio;
328
+ const cropTool = this.app.tools.getTool('crop');
329
+ if (cropTool) {
330
+ if (ratio === 'free') {
331
+ cropTool.setAspectRatio(null);
332
+ } else {
333
+ const [w, h] = ratio.split(':').map(Number);
334
+ cropTool.setAspectRatio(w / h);
335
+ }
336
+ }
337
+ });
338
+ });
339
+
340
+ submenu.querySelector('[data-action="cancel"]')?.addEventListener('click', () => {
341
+ this.closeSubmenu();
342
+ });
343
+
344
+ submenu.querySelector('[data-action="apply"]')?.addEventListener('click', () => {
345
+ const cropTool = this.app.tools.getTool('crop');
346
+ if (cropTool?.applyCrop) {
347
+ cropTool.applyCrop();
348
+ }
349
+ this.closeSubmenu();
350
+ });
351
+ }
352
+
353
+ renderRotateSubmenu(submenu) {
354
+ submenu.innerHTML = `
355
+ <div class="swp-submenu-content">
356
+ <div class="swp-submenu-title">Rotate</div>
357
+ <div class="swp-submenu-group">
358
+ <div class="swp-btn-group" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px;">
359
+ <button class="swp-submenu-btn" data-rotate="-90">
360
+ <ss-icon icon="rotate-minus" thickness="2"></ss-icon>
361
+ <span>-90°</span>
362
+ </button>
363
+ <button class="swp-submenu-btn" data-rotate="90">
364
+ <ss-icon icon="rotate-add" thickness="2"></ss-icon>
365
+ <span>+90°</span>
366
+ </button>
367
+ </div>
368
+ </div>
369
+ <div class="swp-submenu-group">
370
+ <label class="swp-submenu-label">Custom Angle</label>
371
+ <div class="swp-range-wrap">
372
+ <input type="range" class="swp-slider" id="rotateAngle" min="-180" max="180" value="0">
373
+ <span class="swp-range-value">0°</span>
374
+ </div>
375
+ </div>
376
+ <div class="swp-submenu-actions">
377
+ <button class="swp-btn" data-action="cancel">Cancel</button>
378
+ <button class="swp-btn swp-btn-primary" data-action="apply">Apply</button>
379
+ </div>
380
+ </div>
381
+ `;
382
+
383
+ this.bindRotateSubmenuEvents(submenu);
384
+ }
385
+
386
+ bindRotateSubmenuEvents(submenu) {
387
+ let currentAngle = 0;
388
+
389
+ submenu.querySelectorAll('[data-rotate]').forEach(btn => {
390
+ btn.addEventListener('click', () => {
391
+ const angle = parseInt(btn.dataset.rotate);
392
+ this.rotateCanvas(angle);
393
+ });
394
+ });
395
+
396
+ const slider = submenu.querySelector('#rotateAngle');
397
+ const valueDisplay = submenu.querySelector('.swp-range-value');
398
+
399
+ slider?.addEventListener('input', (e) => {
400
+ currentAngle = parseInt(e.target.value);
401
+ valueDisplay.textContent = `${currentAngle}°`;
402
+ });
403
+
404
+ submenu.querySelector('[data-action="cancel"]')?.addEventListener('click', () => {
405
+ this.closeSubmenu();
406
+ });
407
+
408
+ submenu.querySelector('[data-action="apply"]')?.addEventListener('click', () => {
409
+ if (currentAngle !== 0) {
410
+ this.rotateCanvas(currentAngle);
411
+ }
412
+ this.closeSubmenu();
413
+ });
414
+ }
415
+
416
+ renderFlipSubmenu(submenu) {
417
+ submenu.innerHTML = `
418
+ <div class="swp-submenu-content">
419
+ <div class="swp-submenu-title">Flip</div>
420
+ <div class="swp-submenu-group">
421
+ <div class="swp-btn-group swp-btn-group-lg">
422
+ <button class="swp-submenu-btn" data-flip="horizontal">
423
+ <ss-icon icon="flip-horizontal" thickness="2"></ss-icon>
424
+ <span>Horizontal</span>
425
+ </button>
426
+ <button class="swp-submenu-btn" data-flip="vertical">
427
+ <ss-icon icon="flip-vertical" thickness="2"></ss-icon>
428
+ <span>Vertical</span>
429
+ </button>
430
+ </div>
431
+ </div>
432
+ </div>
433
+ `;
434
+
435
+ submenu.querySelectorAll('[data-flip]').forEach(btn => {
436
+ btn.addEventListener('click', () => {
437
+ const direction = btn.dataset.flip;
438
+ this.flipCanvas(direction);
439
+ });
440
+ });
441
+ }
442
+
443
+ renderResizeSubmenu(submenu) {
444
+ const currentWidth = this.app.canvas.width;
445
+ const currentHeight = this.app.canvas.height;
446
+
447
+ submenu.innerHTML = `
448
+ <div class="swp-submenu-content">
449
+ <div class="swp-submenu-title">Resize Canvas</div>
450
+ <div class="swp-submenu-group">
451
+ <label class="swp-submenu-label">Dimensions</label>
452
+ <div class="swp-resize-inputs">
453
+ <div class="swp-input-group">
454
+ <label>Width</label>
455
+ <input type="number" class="swp-input" id="resizeWidth" value="${currentWidth}" min="1" max="10000">
456
+ </div>
457
+ <div class="swp-input-group">
458
+ <label>Height</label>
459
+ <input type="number" class="swp-input" id="resizeHeight" value="${currentHeight}" min="1" max="10000">
460
+ </div>
461
+ </div>
462
+ </div>
463
+ <div class="swp-submenu-group">
464
+ <label class="swp-checkbox-label">
465
+ <input type="checkbox" id="lockAspectRatio" checked>
466
+ <span>Lock aspect ratio</span>
467
+ </label>
468
+ </div>
469
+ <div class="swp-submenu-group">
470
+ <label class="swp-submenu-label">Presets</label>
471
+ <div class="swp-btn-group swp-btn-group-wrap">
472
+ <button class="swp-submenu-btn swp-preset-btn" data-width="1920" data-height="1080">1920×1080</button>
473
+ <button class="swp-submenu-btn swp-preset-btn" data-width="1280" data-height="720">1280×720</button>
474
+ <button class="swp-submenu-btn swp-preset-btn" data-width="800" data-height="600">800×600</button>
475
+ <button class="swp-submenu-btn swp-preset-btn" data-width="500" data-height="500">500×500</button>
476
+ </div>
477
+ </div>
478
+ <div class="swp-submenu-actions">
479
+ <button class="swp-btn" data-action="cancel">Cancel</button>
480
+ <button class="swp-btn swp-btn-primary" data-action="apply">Apply</button>
481
+ </div>
482
+ </div>
483
+ `;
484
+
485
+ this.bindResizeSubmenuEvents(submenu, currentWidth, currentHeight);
486
+ }
487
+
488
+ bindResizeSubmenuEvents(submenu, originalWidth, originalHeight) {
489
+ const widthInput = submenu.querySelector('#resizeWidth');
490
+ const heightInput = submenu.querySelector('#resizeHeight');
491
+ const lockCheckbox = submenu.querySelector('#lockAspectRatio');
492
+ const aspectRatio = originalWidth / originalHeight;
493
+
494
+ widthInput?.addEventListener('input', () => {
495
+ if (lockCheckbox?.checked) {
496
+ const newWidth = parseInt(widthInput.value) || 1;
497
+ heightInput.value = Math.round(newWidth / aspectRatio);
498
+ }
499
+ });
500
+
501
+ heightInput?.addEventListener('input', () => {
502
+ if (lockCheckbox?.checked) {
503
+ const newHeight = parseInt(heightInput.value) || 1;
504
+ widthInput.value = Math.round(newHeight * aspectRatio);
505
+ }
506
+ });
507
+
508
+ submenu.querySelectorAll('.swp-preset-btn').forEach(btn => {
509
+ btn.addEventListener('click', () => {
510
+ widthInput.value = btn.dataset.width;
511
+ heightInput.value = btn.dataset.height;
512
+ lockCheckbox.checked = false; // Uncheck to allow preset change
513
+ });
514
+ });
515
+
516
+ submenu.querySelector('[data-action="cancel"]')?.addEventListener('click', () => {
517
+ this.closeSubmenu();
518
+ });
519
+
520
+ submenu.querySelector('[data-action="apply"]')?.addEventListener('click', () => {
521
+ const newWidth = parseInt(widthInput.value) || originalWidth;
522
+ const newHeight = parseInt(heightInput.value) || originalHeight;
523
+
524
+ if (newWidth !== originalWidth || newHeight !== originalHeight) {
525
+ this.resizeCanvas(newWidth, newHeight);
526
+ }
527
+ this.closeSubmenu();
528
+ });
529
+ }
530
+
531
+ resizeCanvas(width, height) {
532
+ this.app.history.pushState(`Resize to ${width}×${height}`);
533
+ this.app.canvas.resize(width, height);
534
+ this.app.canvas.fitToScreen();
535
+ this.app.canvas.render();
536
+ }
537
+
538
+ renderDrawSubmenu(submenu) {
539
+ const currentColor = this.app.colors.foreground;
540
+ const brushTool = this.app.tools.getTool('brush');
541
+ const currentSize = brushTool?.size || 10;
542
+
543
+ submenu.innerHTML = `
544
+ <div class="swp-submenu-content">
545
+ <div class="swp-submenu-title">Draw</div>
546
+ <div class="swp-submenu-group">
547
+ <label class="swp-submenu-label">Brush Size</label>
548
+ <div class="swp-range-wrap">
549
+ <input type="range" class="swp-slider" id="brushSize" min="1" max="100" value="${currentSize}">
550
+ <span class="swp-range-value">${currentSize}px</span>
551
+ </div>
552
+ </div>
553
+ <div class="swp-submenu-group">
554
+ <label class="swp-submenu-label">Color</label>
555
+ <input type="color" class="swp-color-input" id="brushColor" value="${currentColor}">
556
+ </div>
557
+ </div>
558
+ `;
559
+
560
+ this.bindDrawSubmenuEvents(submenu);
561
+ }
562
+
563
+ bindDrawSubmenuEvents(submenu) {
564
+ const sizeSlider = submenu.querySelector('#brushSize');
565
+ const sizeValue = submenu.querySelector('.swp-range-value');
566
+ const colorInput = submenu.querySelector('#brushColor');
567
+
568
+ sizeSlider?.addEventListener('input', (e) => {
569
+ const size = parseInt(e.target.value);
570
+ sizeValue.textContent = `${size}px`;
571
+ const brushTool = this.app.tools.getTool('brush');
572
+ if (brushTool) {
573
+ brushTool.size = size;
574
+ }
575
+ });
576
+
577
+ colorInput?.addEventListener('input', (e) => {
578
+ this.app.colors.setForeground(e.target.value);
579
+ });
580
+ }
581
+
582
+ renderShapeSubmenu(submenu) {
583
+ const shapeTool = this.app.tools.getTool('shape');
584
+ const currentShape = shapeTool?.options?.shape || 'rectangle';
585
+ const currentFillColor = shapeTool?.options?.fillColor || this.app.colors.foreground;
586
+ const currentStrokeWidth = shapeTool?.options?.strokeWidth || 2;
587
+
588
+ submenu.innerHTML = `
589
+ <div class="swp-submenu-content">
590
+ <div class="swp-submenu-title">Shape</div>
591
+ <div class="swp-submenu-group">
592
+ <label class="swp-submenu-label">Shape Type</label>
593
+ <div class="swp-btn-group">
594
+ <button class="swp-submenu-btn ${currentShape === 'rectangle' ? 'active' : ''}" data-shape="rectangle">
595
+ <ss-icon icon="square" thickness="2"></ss-icon>
596
+ </button>
597
+ <button class="swp-submenu-btn ${currentShape === 'ellipse' ? 'active' : ''}" data-shape="ellipse">
598
+ <ss-icon icon="circle" thickness="2"></ss-icon>
599
+ </button>
600
+ <button class="swp-submenu-btn ${currentShape === 'line' ? 'active' : ''}" data-shape="line">
601
+ <ss-icon icon="minus" thickness="2"></ss-icon>
602
+ </button>
603
+ </div>
604
+ </div>
605
+ <div class="swp-submenu-group">
606
+ <label class="swp-submenu-label">Fill Color</label>
607
+ <input type="color" class="swp-color-input" id="shapeFill" value="${currentFillColor}">
608
+ </div>
609
+ <div class="swp-submenu-group">
610
+ <label class="swp-submenu-label">Stroke Width</label>
611
+ <div class="swp-range-wrap">
612
+ <input type="range" class="swp-slider" id="shapeStroke" min="0" max="20" value="${currentStrokeWidth}">
613
+ <span class="swp-range-value">2px</span>
614
+ </div>
615
+ </div>
616
+ </div>
617
+ `;
618
+
619
+ this.bindShapeSubmenuEvents(submenu);
620
+ }
621
+
622
+ bindShapeSubmenuEvents(submenu) {
623
+ submenu.querySelectorAll('[data-shape]').forEach(btn => {
624
+ btn.addEventListener('click', () => {
625
+ submenu.querySelectorAll('[data-shape]').forEach(b => b.classList.remove('active'));
626
+ btn.classList.add('active');
627
+
628
+ const shapeTool = this.app.tools.getTool('shape');
629
+ if (shapeTool) {
630
+ shapeTool.options.shape = btn.dataset.shape;
631
+ }
632
+ });
633
+ });
634
+
635
+ const fillInput = submenu.querySelector('#shapeFill');
636
+ fillInput?.addEventListener('input', (e) => {
637
+ const shapeTool = this.app.tools.getTool('shape');
638
+ if (shapeTool) {
639
+ shapeTool.options.fillColor = e.target.value;
640
+ shapeTool.options.filled = true;
641
+ }
642
+ this.app.colors.setForeground(e.target.value);
643
+ });
644
+
645
+ const strokeSlider = submenu.querySelector('#shapeStroke');
646
+ const strokeValue = submenu.querySelectorAll('.swp-range-value')[0];
647
+ strokeSlider?.addEventListener('input', (e) => {
648
+ const width = parseInt(e.target.value);
649
+ if (strokeValue) strokeValue.textContent = `${width}px`;
650
+ const shapeTool = this.app.tools.getTool('shape');
651
+ if (shapeTool) {
652
+ shapeTool.options.strokeWidth = width;
653
+ if (width > 0) {
654
+ shapeTool.options.stroked = true;
655
+ shapeTool.options.strokeColor = this.app.colors.foreground;
656
+ }
657
+ }
658
+ });
659
+ }
660
+
661
+ renderTextSubmenu(submenu) {
662
+ const currentColor = this.app.colors.foreground;
663
+
664
+ submenu.innerHTML = `
665
+ <div class="swp-submenu-content">
666
+ <div class="swp-submenu-title">Text</div>
667
+ <div class="swp-submenu-group">
668
+ <label class="swp-submenu-label">Font Size</label>
669
+ <div class="swp-range-wrap">
670
+ <input type="range" class="swp-slider" id="textSize" min="12" max="120" value="32">
671
+ <span class="swp-range-value">32px</span>
672
+ </div>
673
+ </div>
674
+ <div class="swp-submenu-group">
675
+ <label class="swp-submenu-label">Font</label>
676
+ <select class="swp-select" id="textFont">
677
+ <option value="Arial">Arial</option>
678
+ <option value="Helvetica">Helvetica</option>
679
+ <option value="Georgia">Georgia</option>
680
+ <option value="Times New Roman">Times New Roman</option>
681
+ <option value="Courier New">Courier New</option>
682
+ <option value="Impact">Impact</option>
683
+ </select>
684
+ </div>
685
+ <div class="swp-submenu-group">
686
+ <label class="swp-submenu-label">Color</label>
687
+ <input type="color" class="swp-color-input" id="textColor" value="${currentColor}">
688
+ </div>
689
+ <div class="swp-submenu-group">
690
+ <label class="swp-submenu-label">Style</label>
691
+ <div class="swp-btn-group">
692
+ <button class="swp-submenu-btn" data-style="bold">
693
+ <strong>B</strong>
694
+ </button>
695
+ <button class="swp-submenu-btn" data-style="italic">
696
+ <em>I</em>
697
+ </button>
698
+ </div>
699
+ </div>
700
+ </div>
701
+ `;
702
+
703
+ this.bindTextSubmenuEvents(submenu);
704
+ }
705
+
706
+ bindTextSubmenuEvents(submenu) {
707
+ const sizeSlider = submenu.querySelector('#textSize');
708
+ const sizeValue = submenu.querySelector('.swp-range-value');
709
+
710
+ sizeSlider?.addEventListener('input', (e) => {
711
+ const size = parseInt(e.target.value);
712
+ sizeValue.textContent = `${size}px`;
713
+ const textTool = this.app.tools.getTool('text');
714
+ if (textTool) {
715
+ textTool.fontSize = size;
716
+ }
717
+ });
718
+
719
+ const fontSelect = submenu.querySelector('#textFont');
720
+ fontSelect?.addEventListener('change', (e) => {
721
+ const textTool = this.app.tools.getTool('text');
722
+ if (textTool) {
723
+ textTool.fontFamily = e.target.value;
724
+ }
725
+ });
726
+
727
+ const colorInput = submenu.querySelector('#textColor');
728
+ colorInput?.addEventListener('input', (e) => {
729
+ this.app.colors.setForeground(e.target.value);
730
+ });
731
+
732
+ submenu.querySelectorAll('[data-style]').forEach(btn => {
733
+ btn.addEventListener('click', () => {
734
+ btn.classList.toggle('active');
735
+ const textTool = this.app.tools.getTool('text');
736
+ if (textTool) {
737
+ if (btn.dataset.style === 'bold') {
738
+ textTool.bold = btn.classList.contains('active');
739
+ } else if (btn.dataset.style === 'italic') {
740
+ textTool.italic = btn.classList.contains('active');
741
+ }
742
+ }
743
+ });
744
+ });
745
+ }
746
+
747
+ renderFilterSubmenu(submenu) {
748
+ submenu.innerHTML = `
749
+ <div class="swp-submenu-content swp-submenu-wide">
750
+ <div class="swp-submenu-title">Filter</div>
751
+ <div class="swp-submenu-group">
752
+ <div class="swp-filter-grid">
753
+ <button class="swp-filter-btn" data-filter="none">
754
+ <div class="swp-filter-preview"></div>
755
+ <span>None</span>
756
+ </button>
757
+ <button class="swp-filter-btn" data-filter="grayscale">
758
+ <div class="swp-filter-preview swp-filter-grayscale"></div>
759
+ <span>Grayscale</span>
760
+ </button>
761
+ <button class="swp-filter-btn" data-filter="sepia">
762
+ <div class="swp-filter-preview swp-filter-sepia"></div>
763
+ <span>Sepia</span>
764
+ </button>
765
+ <button class="swp-filter-btn" data-filter="invert">
766
+ <div class="swp-filter-preview swp-filter-invert"></div>
767
+ <span>Invert</span>
768
+ </button>
769
+ <button class="swp-filter-btn" data-filter="blur">
770
+ <div class="swp-filter-preview swp-filter-blur"></div>
771
+ <span>Blur</span>
772
+ </button>
773
+ <button class="swp-filter-btn" data-filter="brightness">
774
+ <div class="swp-filter-preview swp-filter-brightness"></div>
775
+ <span>Brighten</span>
776
+ </button>
777
+ <button class="swp-filter-btn" data-filter="contrast">
778
+ <div class="swp-filter-preview swp-filter-contrast"></div>
779
+ <span>Contrast</span>
780
+ </button>
781
+ <button class="swp-filter-btn" data-filter="sharpen">
782
+ <div class="swp-filter-preview swp-filter-sharpen"></div>
783
+ <span>Sharpen</span>
784
+ </button>
785
+ </div>
786
+ </div>
787
+ <div class="swp-submenu-group swp-filter-intensity" hidden>
788
+ <label class="swp-submenu-label">Intensity</label>
789
+ <div class="swp-range-wrap">
790
+ <input type="range" class="swp-slider" id="filterIntensity" min="0" max="100" value="50">
791
+ <span class="swp-range-value">50%</span>
792
+ </div>
793
+ </div>
794
+ <div class="swp-submenu-actions">
795
+ <button class="swp-btn" data-action="cancel">Cancel</button>
796
+ <button class="swp-btn swp-btn-primary" data-action="apply">Apply</button>
797
+ </div>
798
+ </div>
799
+ `;
800
+
801
+ this.bindFilterSubmenuEvents(submenu);
802
+ }
803
+
804
+ bindFilterSubmenuEvents(submenu) {
805
+ let selectedFilter = 'none';
806
+ let intensity = 50;
807
+
808
+ const intensityGroup = submenu.querySelector('.swp-filter-intensity');
809
+ const intensitySlider = submenu.querySelector('#filterIntensity');
810
+ const intensityValue = submenu.querySelector('.swp-filter-intensity .swp-range-value');
811
+
812
+ submenu.querySelectorAll('.swp-filter-btn').forEach(btn => {
813
+ btn.addEventListener('click', () => {
814
+ submenu.querySelectorAll('.swp-filter-btn').forEach(b => b.classList.remove('active'));
815
+ btn.classList.add('active');
816
+ selectedFilter = btn.dataset.filter;
817
+
818
+ // Show/hide intensity slider
819
+ if (selectedFilter !== 'none') {
820
+ intensityGroup.hidden = false;
821
+ this.previewFilter(selectedFilter, intensity);
822
+ } else {
823
+ intensityGroup.hidden = true;
824
+ this.app.filters.cancelPreview();
825
+ }
826
+ });
827
+ });
828
+
829
+ intensitySlider?.addEventListener('input', (e) => {
830
+ intensity = parseInt(e.target.value);
831
+ intensityValue.textContent = `${intensity}%`;
832
+ if (selectedFilter !== 'none') {
833
+ this.previewFilter(selectedFilter, intensity);
834
+ }
835
+ });
836
+
837
+ submenu.querySelector('[data-action="cancel"]')?.addEventListener('click', () => {
838
+ this.app.filters.cancelPreview();
839
+ this.closeSubmenu();
840
+ });
841
+
842
+ submenu.querySelector('[data-action="apply"]')?.addEventListener('click', () => {
843
+ if (selectedFilter !== 'none') {
844
+ this.applyFilter(selectedFilter, intensity);
845
+ }
846
+ this.closeSubmenu();
847
+ });
848
+ }
849
+
850
+ // Canvas manipulation methods
851
+ rotateCanvas(angle) {
852
+ const layer = this.app.layers.getActiveLayer();
853
+ if (!layer) return;
854
+
855
+ // Save history
856
+ this.app.history.pushState(`Rotate ${angle}°`);
857
+
858
+ // Perform rotation
859
+ const tempCanvas = document.createElement('canvas');
860
+ const tempCtx = tempCanvas.getContext('2d');
861
+
862
+ const rad = (angle * Math.PI) / 180;
863
+ const sin = Math.abs(Math.sin(rad));
864
+ const cos = Math.abs(Math.cos(rad));
865
+
866
+ const w = layer.width;
867
+ const h = layer.height;
868
+ const newW = Math.round(w * cos + h * sin);
869
+ const newH = Math.round(w * sin + h * cos);
870
+
871
+ tempCanvas.width = newW;
872
+ tempCanvas.height = newH;
873
+
874
+ tempCtx.translate(newW / 2, newH / 2);
875
+ tempCtx.rotate(rad);
876
+ tempCtx.drawImage(layer.canvas, -w / 2, -h / 2);
877
+
878
+ // Update canvas size if needed (for 90° rotations)
879
+ if (angle === 90 || angle === -90) {
880
+ this.app.canvas.resize(newH, newW);
881
+ }
882
+
883
+ layer.clear();
884
+ layer.ctx.drawImage(tempCanvas, 0, 0);
885
+ this.app.canvas.render();
886
+ }
887
+
888
+ flipCanvas(direction) {
889
+ const layer = this.app.layers.getActiveLayer();
890
+ if (!layer) return;
891
+
892
+ this.app.history.pushState(`Flip ${direction}`);
893
+
894
+ const tempCanvas = document.createElement('canvas');
895
+ const tempCtx = tempCanvas.getContext('2d');
896
+ tempCanvas.width = layer.width;
897
+ tempCanvas.height = layer.height;
898
+
899
+ if (direction === 'horizontal') {
900
+ tempCtx.translate(layer.width, 0);
901
+ tempCtx.scale(-1, 1);
902
+ } else {
903
+ tempCtx.translate(0, layer.height);
904
+ tempCtx.scale(1, -1);
905
+ }
906
+
907
+ tempCtx.drawImage(layer.canvas, 0, 0);
908
+ layer.clear();
909
+ layer.ctx.drawImage(tempCanvas, 0, 0);
910
+ this.app.canvas.render();
911
+ }
912
+
913
+ previewFilter(filterName, intensity) {
914
+ // Convert 0-100 intensity to appropriate filter values
915
+ let options = {};
916
+ switch (filterName) {
917
+ case 'brightness':
918
+ // Convert 0-100 to -128 to 128 range (50 = 0 change)
919
+ options.value = Math.round((intensity - 50) * 2.56);
920
+ break;
921
+ case 'contrast':
922
+ // Convert 0-100 to -128 to 128 range (50 = 0 change)
923
+ options.value = Math.round((intensity - 50) * 2.56);
924
+ break;
925
+ case 'saturation':
926
+ // Convert 0-100 to -100 to 100 range (50 = 0 change)
927
+ options.value = (intensity - 50) * 2;
928
+ break;
929
+ case 'blur':
930
+ // Convert 0-100 to 1-10 radius
931
+ options.radius = Math.max(1, Math.round(intensity / 10));
932
+ break;
933
+ case 'sharpen':
934
+ // Convert 0-100 to 0-2 amount
935
+ options.amount = intensity / 50;
936
+ break;
937
+ case 'hueRotate':
938
+ // Convert 0-100 to 0-360 degrees
939
+ options.angle = intensity * 3.6;
940
+ break;
941
+ default:
942
+ options.value = intensity;
943
+ }
944
+ this.app.filters.previewFilter(filterName, options);
945
+ }
946
+
947
+ applyFilter(filterName, intensity) {
948
+ // Convert 0-100 intensity to appropriate filter values
949
+ let options = {};
950
+ switch (filterName) {
951
+ case 'brightness':
952
+ options.value = Math.round((intensity - 50) * 2.56);
953
+ break;
954
+ case 'contrast':
955
+ options.value = Math.round((intensity - 50) * 2.56);
956
+ break;
957
+ case 'saturation':
958
+ options.value = (intensity - 50) * 2;
959
+ break;
960
+ case 'blur':
961
+ options.radius = Math.max(1, Math.round(intensity / 10));
962
+ break;
963
+ case 'sharpen':
964
+ options.amount = intensity / 50;
965
+ break;
966
+ case 'hueRotate':
967
+ options.angle = intensity * 3.6;
968
+ break;
969
+ default:
970
+ options.value = intensity;
971
+ }
972
+ this.app.filters.applyFilter(filterName, options);
973
+ }
974
+
975
+ resetCanvas() {
976
+ if (confirm('Reset all changes?')) {
977
+ this.app.file.newDocument({
978
+ width: this.app.options.width,
979
+ height: this.app.options.height
980
+ });
981
+ this.closeSubmenu();
982
+ }
983
+ }
984
+
985
+ openFileDialog() {
986
+ const input = document.createElement('input');
987
+ input.type = 'file';
988
+ input.accept = 'image/*';
989
+ input.onchange = (e) => {
990
+ const file = e.target.files[0];
991
+ if (file) {
992
+ const reader = new FileReader();
993
+ reader.onload = (e) => {
994
+ this.app.loadImage(e.target.result);
995
+ };
996
+ reader.readAsDataURL(file);
997
+ }
998
+ };
999
+ input.click();
1000
+ }
1001
+
1002
+ toggleFullscreen() {
1003
+ if (!document.fullscreenElement) {
1004
+ this.container.requestFullscreen();
1005
+ this.isFullscreen = true;
1006
+ } else {
1007
+ document.exitFullscreen();
1008
+ this.isFullscreen = false;
1009
+ }
1010
+ }
1011
+
1012
+ // Side Panel Methods
1013
+ toggleSidePanel(panelType) {
1014
+ const sidePanel = this.container.querySelector('.swp-side-panel');
1015
+ if (!sidePanel) return;
1016
+
1017
+ // If already showing this panel, close it
1018
+ if (!sidePanel.hidden && this.currentPanel === panelType) {
1019
+ this.closeSidePanel();
1020
+ return;
1021
+ }
1022
+
1023
+ // Show panel
1024
+ sidePanel.hidden = false;
1025
+ this.currentPanel = panelType;
1026
+
1027
+ // Update title
1028
+ const title = sidePanel.querySelector('.swp-side-panel-title');
1029
+ if (title) {
1030
+ title.textContent = panelType.charAt(0).toUpperCase() + panelType.slice(1);
1031
+ }
1032
+
1033
+ // Render content
1034
+ const content = sidePanel.querySelector('.swp-side-panel-content');
1035
+ if (panelType === 'history') {
1036
+ this.renderHistorySidePanel(content);
1037
+ } else if (panelType === 'layers') {
1038
+ this.renderLayersSidePanel(content);
1039
+ }
1040
+
1041
+ // Update button active states
1042
+ this.container.querySelectorAll('[data-action="history"], [data-action="layers"]').forEach(btn => {
1043
+ btn.classList.toggle('active', btn.dataset.action === panelType);
1044
+ });
1045
+ }
1046
+
1047
+ closeSidePanel() {
1048
+ const sidePanel = this.container.querySelector('.swp-side-panel');
1049
+ if (sidePanel) {
1050
+ sidePanel.hidden = true;
1051
+ }
1052
+ this.currentPanel = null;
1053
+
1054
+ // Remove active states
1055
+ this.container.querySelectorAll('[data-action="history"], [data-action="layers"]').forEach(btn => {
1056
+ btn.classList.remove('active');
1057
+ });
1058
+ }
1059
+
1060
+ renderHistorySidePanel(content) {
1061
+ const states = this.app.history.getStates();
1062
+
1063
+ content.innerHTML = `
1064
+ <div class="swp-panel-list">
1065
+ ${states.length === 0 ? '<div class="swp-panel-empty">No history yet</div>' : ''}
1066
+ ${states.map(state => `
1067
+ <div class="swp-panel-item ${state.isCurrent ? 'active' : ''}" data-index="${state.index}">
1068
+ <ss-icon icon="clock" thickness="2"></ss-icon>
1069
+ <span>${state.name}</span>
1070
+ </div>
1071
+ `).join('')}
1072
+ </div>
1073
+ `;
1074
+
1075
+ content.querySelectorAll('.swp-panel-item').forEach(item => {
1076
+ item.addEventListener('click', () => {
1077
+ this.app.history.goToState(parseInt(item.dataset.index));
1078
+ this.renderHistorySidePanel(content);
1079
+ });
1080
+ });
1081
+ }
1082
+
1083
+ renderLayersSidePanel(content) {
1084
+ const layers = this.app.layers.getLayers().slice().reverse();
1085
+ const active = this.app.layers.getActiveLayer();
1086
+
1087
+ content.innerHTML = `
1088
+ <div class="swp-panel-list">
1089
+ ${layers.length === 0 ? '<div class="swp-panel-empty">No layers</div>' : ''}
1090
+ ${layers.map(layer => `
1091
+ <div class="swp-panel-item ${layer.id === active?.id ? 'active' : ''}" data-id="${layer.id}">
1092
+ <button class="swp-layer-vis-btn ${layer.visible ? 'visible' : ''}" data-action="toggle-visibility">
1093
+ <ss-icon icon="${layer.visible ? 'eye' : 'eye-slash'}" thickness="2"></ss-icon>
1094
+ </button>
1095
+ <span class="swp-layer-name">${layer.name}</span>
1096
+ <span class="swp-layer-opacity">${layer.opacity}%</span>
1097
+ </div>
1098
+ `).join('')}
1099
+ </div>
1100
+ <div class="swp-panel-actions">
1101
+ <button class="swp-btn swp-btn-sm" data-action="add-layer">
1102
+ <ss-icon icon="plus" thickness="2"></ss-icon> Add
1103
+ </button>
1104
+ <button class="swp-btn swp-btn-sm swp-btn-danger" data-action="delete-layer">
1105
+ <ss-icon icon="trash" thickness="2"></ss-icon> Delete
1106
+ </button>
1107
+ </div>
1108
+ `;
1109
+
1110
+ // Layer click handlers
1111
+ content.querySelectorAll('.swp-panel-item').forEach(item => {
1112
+ item.addEventListener('click', (e) => {
1113
+ if (!e.target.closest('[data-action]')) {
1114
+ this.app.layers.setActiveLayer(item.dataset.id);
1115
+ this.renderLayersSidePanel(content);
1116
+ }
1117
+ });
1118
+
1119
+ item.querySelector('[data-action="toggle-visibility"]')?.addEventListener('click', (e) => {
1120
+ e.stopPropagation();
1121
+ const layer = this.app.layers.getLayer(item.dataset.id);
1122
+ if (layer) {
1123
+ this.app.layers.setLayerVisibility(item.dataset.id, !layer.visible);
1124
+ this.renderLayersSidePanel(content);
1125
+ }
1126
+ });
1127
+ });
1128
+
1129
+ // Action buttons
1130
+ content.querySelector('[data-action="add-layer"]')?.addEventListener('click', () => {
1131
+ this.app.layers.addLayer();
1132
+ this.renderLayersSidePanel(content);
1133
+ });
1134
+
1135
+ content.querySelector('[data-action="delete-layer"]')?.addEventListener('click', () => {
1136
+ const activeLayer = this.app.layers.getActiveLayer();
1137
+ if (activeLayer) {
1138
+ this.app.layers.removeLayer(activeLayer.id);
1139
+ this.renderLayersSidePanel(content);
1140
+ }
1141
+ });
1142
+ }
1143
+
1144
+ bindEvents() {
1145
+ // History events - update buttons and panel
1146
+ this.app.events.on(Events.HISTORY_PUSH, () => {
1147
+ this.updateHistoryButtons();
1148
+ this.updateHistoryPanel();
1149
+ });
1150
+ this.app.events.on(Events.HISTORY_UNDO, () => {
1151
+ this.updateHistoryButtons();
1152
+ this.updateHistoryPanel();
1153
+ });
1154
+ this.app.events.on(Events.HISTORY_REDO, () => {
1155
+ this.updateHistoryButtons();
1156
+ this.updateHistoryPanel();
1157
+ });
1158
+
1159
+ // Layer events - update layers panel
1160
+ this.app.events.on(Events.LAYER_ADD, () => this.updateLayersPanel());
1161
+ this.app.events.on(Events.LAYER_REMOVE, () => this.updateLayersPanel());
1162
+ this.app.events.on(Events.LAYER_SELECT, () => this.updateLayersPanel());
1163
+ this.app.events.on(Events.LAYER_VISIBILITY, () => this.updateLayersPanel());
1164
+ this.app.events.on(Events.LAYER_UPDATE, () => this.updateLayersPanel());
1165
+ this.app.events.on(Events.LAYER_RENAME, () => this.updateLayersPanel());
1166
+ this.app.events.on(Events.LAYER_REORDER, () => this.updateLayersPanel());
1167
+ this.app.events.on(Events.LAYER_OPACITY, () => this.updateLayersPanel());
1168
+ }
1169
+
1170
+ updateHistoryButtons() {
1171
+ const undoBtn = this.container.querySelector('[data-action="undo"]');
1172
+ const redoBtn = this.container.querySelector('[data-action="redo"]');
1173
+
1174
+ if (undoBtn) {
1175
+ undoBtn.disabled = !this.app.history.canUndo();
1176
+ }
1177
+ if (redoBtn) {
1178
+ redoBtn.disabled = !this.app.history.canRedo();
1179
+ }
1180
+ }
1181
+
1182
+ // Update layers panel if it's currently visible
1183
+ updateLayersPanel() {
1184
+ if (this.currentPanel === 'layers') {
1185
+ const content = this.container.querySelector('.swp-side-panel-content');
1186
+ if (content) {
1187
+ this.renderLayersSidePanel(content);
1188
+ }
1189
+ }
1190
+ }
1191
+
1192
+ // Update history panel if it's currently visible
1193
+ updateHistoryPanel() {
1194
+ if (this.currentPanel === 'history') {
1195
+ const content = this.container.querySelector('.swp-side-panel-content');
1196
+ if (content) {
1197
+ this.renderHistorySidePanel(content);
1198
+ }
1199
+ }
1200
+ }
1201
+
1202
+ updateToolbox() {
1203
+ // No-op in simplified UI
1204
+ }
1205
+
1206
+ getWorkspace() {
1207
+ return this.container.querySelector('.swp-workspace');
1208
+ }
1209
+ }
1210
+
1211
+ export default UI;