From fc4c343418d13516aa49d9f8601dab8576fca169 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Wed, 30 Jul 2025 11:05:04 +0200 Subject: [PATCH] Improve minimized "Custom Output Area Active" styling Unified the appearance of the minimized "Custom Output Area Active" bar with the full menu styling: --- js/CustomShapeMenu.js | 75 +++++++++++++++++++++++++++---- js/css/custom_shape_menu.css | 44 ++++++++++++++++-- src/CustomShapeMenu.ts | 84 ++++++++++++++++++++++++++++++----- src/css/custom_shape_menu.css | 44 ++++++++++++++++-- 4 files changed, 218 insertions(+), 29 deletions(-) diff --git a/js/CustomShapeMenu.js b/js/CustomShapeMenu.js index 0492f8f..f7a27e5 100644 --- a/js/CustomShapeMenu.js +++ b/js/CustomShapeMenu.js @@ -3,6 +3,7 @@ import { addStylesheet, getUrl } from "./utils/ResourceManager.js"; const log = createModuleLogger('CustomShapeMenu'); export class CustomShapeMenu { constructor(canvas) { + this.isMinimized = false; this.canvas = canvas; this.element = null; this.worldX = 0; @@ -17,6 +18,7 @@ export class CustomShapeMenu { this._createUI(); if (this.element) { this.element.style.display = 'block'; + this._updateMinimizedState(); } // Position in top-left corner of viewport (closer to edge) const viewLeft = this.canvas.viewport.x; @@ -46,15 +48,50 @@ export class CustomShapeMenu { addStylesheet(getUrl('./css/custom_shape_menu.css')); this.element = document.createElement('div'); this.element.id = 'layerforge-custom-shape-menu'; + // --- MINIMIZED BAR --- + const minimizedBar = document.createElement('div'); + minimizedBar.className = 'custom-shape-minimized-bar'; + minimizedBar.textContent = "Custom Output Area Active"; + minimizedBar.style.display = 'none'; + minimizedBar.style.cursor = 'pointer'; + minimizedBar.onclick = () => { + this.isMinimized = false; + this._updateMinimizedState(); + }; + this.element.appendChild(minimizedBar); + // --- FULL MENU --- + const fullMenu = document.createElement('div'); + fullMenu.className = 'custom-shape-full-menu'; + // Minimize button (top right) + const minimizeBtn = document.createElement('button'); + minimizeBtn.innerHTML = "–"; + minimizeBtn.title = "Minimize menu"; + minimizeBtn.className = 'custom-shape-minimize-btn'; + minimizeBtn.style.position = 'absolute'; + minimizeBtn.style.top = '4px'; + minimizeBtn.style.right = '4px'; + minimizeBtn.style.width = '24px'; + minimizeBtn.style.height = '24px'; + minimizeBtn.style.border = 'none'; + minimizeBtn.style.background = 'transparent'; + minimizeBtn.style.color = '#888'; + minimizeBtn.style.fontSize = '20px'; + minimizeBtn.style.cursor = 'pointer'; + minimizeBtn.onclick = (e) => { + e.stopPropagation(); + this.isMinimized = true; + this._updateMinimizedState(); + }; + fullMenu.appendChild(minimizeBtn); // Create menu content const lines = [ - "🎯 Custom Output Area Active" + "Custom Output Area Active" ]; lines.forEach(line => { const lineElement = document.createElement('div'); lineElement.textContent = line; lineElement.className = 'menu-line'; - this.element.appendChild(lineElement); + fullMenu.appendChild(lineElement); }); // Create a container for the entire shape mask feature set const featureContainer = document.createElement('div'); @@ -209,7 +246,7 @@ export class CustomShapeMenu { featherSliderContainer.appendChild(featherSlider); featherSliderContainer.appendChild(featherValueDisplay); featureContainer.appendChild(featherSliderContainer); - this.element.appendChild(featureContainer); + fullMenu.appendChild(featureContainer); // Create output area extension container const extensionContainer = document.createElement('div'); extensionContainer.id = 'output-area-extension-container'; @@ -305,7 +342,8 @@ export class CustomShapeMenu { slidersContainer.appendChild(createExtensionSlider('Left extension:', 'left')); slidersContainer.appendChild(createExtensionSlider('Right extension:', 'right')); extensionContainer.appendChild(slidersContainer); - this.element.appendChild(extensionContainer); + fullMenu.appendChild(extensionContainer); + this.element.appendChild(fullMenu); // Add to DOM if (this.canvas.canvas.parentElement) { this.canvas.canvas.parentElement.appendChild(this.element); @@ -315,6 +353,7 @@ export class CustomShapeMenu { } this.uiInitialized = true; this._updateUI(); + this._updateMinimizedState(); // Add viewport change listener to update shape preview when zooming/panning this._addViewportChangeListener(); } @@ -348,8 +387,12 @@ export class CustomShapeMenu { _updateUI() { if (!this.element) return; + // Always update only the full menu part + const fullMenu = this.element.querySelector('.custom-shape-full-menu'); + if (!fullMenu) + return; const setChecked = (id, checked) => { - const input = this.element.querySelector(`#${id}`); + const input = fullMenu.querySelector(`#${id}`); if (input) input.checked = checked; }; @@ -357,23 +400,37 @@ export class CustomShapeMenu { setChecked('expansion-checkbox', this.canvas.shapeMaskExpansion); setChecked('feather-checkbox', this.canvas.shapeMaskFeather); setChecked('extension-checkbox', this.canvas.outputAreaExtensionEnabled); - const expansionCheckbox = this.element.querySelector('#expansion-checkbox')?.parentElement; + const expansionCheckbox = fullMenu.querySelector('#expansion-checkbox')?.parentElement; if (expansionCheckbox) { expansionCheckbox.style.display = this.canvas.autoApplyShapeMask ? 'flex' : 'none'; } - const featherCheckbox = this.element.querySelector('#feather-checkbox')?.parentElement; + const featherCheckbox = fullMenu.querySelector('#feather-checkbox')?.parentElement; if (featherCheckbox) { featherCheckbox.style.display = this.canvas.autoApplyShapeMask ? 'flex' : 'none'; } - const expansionSliderContainer = this.element.querySelector('#expansion-slider-container'); + const expansionSliderContainer = fullMenu.querySelector('#expansion-slider-container'); if (expansionSliderContainer) { expansionSliderContainer.style.display = (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskExpansion) ? 'block' : 'none'; } - const featherSliderContainer = this.element.querySelector('#feather-slider-container'); + const featherSliderContainer = fullMenu.querySelector('#feather-slider-container'); if (featherSliderContainer) { featherSliderContainer.style.display = (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskFeather) ? 'block' : 'none'; } } + _updateMinimizedState() { + if (!this.element) + return; + const minimizedBar = this.element.querySelector('.custom-shape-minimized-bar'); + const fullMenu = this.element.querySelector('.custom-shape-full-menu'); + if (this.isMinimized) { + minimizedBar.style.display = 'block'; + fullMenu.style.display = 'none'; + } + else { + minimizedBar.style.display = 'none'; + fullMenu.style.display = 'block'; + } + } _updateExtensionUI() { if (!this.element) return; diff --git a/js/css/custom_shape_menu.css b/js/css/custom_shape_menu.css index 7f7d9d4..3a1703e 100644 --- a/js/css/custom_shape_menu.css +++ b/js/css/custom_shape_menu.css @@ -29,16 +29,52 @@ gap: 8px; } +/* --- MINIMIZED BAR INTERACTIVE STYLE --- */ +.custom-shape-minimized-bar { + font-size: 13px; + font-weight: 600; + padding: 6px 12px; + border-radius: 6px; + background: #222; + color: #4a90e2; + box-shadow: 0 2px 8px rgba(0,0,0,0.10); + margin: 0 0 8px 0; + user-select: none; + cursor: pointer; + border: 1px solid #444; + transition: background 0.18s, color 0.18s, box-shadow 0.18s, border 0.18s; + outline: none; + text-shadow: none; + display: flex; + align-items: center; + gap: 8px; +} +.custom-shape-minimized-bar:hover, .custom-shape-minimized-bar:focus { + background: #2a2a2a; + color: #4a90e2; + border: 1.5px solid #4a90e2; + box-shadow: 0 4px 16px #4a90e244; +} + #layerforge-custom-shape-menu .feature-container { background-color: #3a3a3a; border-radius: 6px; - padding: 10px; + padding: 10px 12px; border: 1px solid #4a4a4a; + margin-bottom: 12px; + display: flex; + flex-direction: column; + gap: 10px; +} +#layerforge-custom-shape-menu .feature-container:last-child { + margin-bottom: 0; } #layerforge-custom-shape-menu .slider-container { - margin-top: 10px; + margin-top: 6px; + margin-bottom: 0; display: none; + gap: 6px; } #layerforge-custom-shape-menu .slider-label { @@ -95,8 +131,8 @@ #layerforge-custom-shape-menu .checkbox-container { display: flex; align-items: center; - gap: 10px; - padding: 6px; + gap: 8px; + padding: 5px 0; border-radius: 5px; cursor: pointer; transition: background-color 0.2s; diff --git a/src/CustomShapeMenu.ts b/src/CustomShapeMenu.ts index a3ae796..831d4ac 100644 --- a/src/CustomShapeMenu.ts +++ b/src/CustomShapeMenu.ts @@ -11,6 +11,7 @@ export class CustomShapeMenu { private worldY: number; private uiInitialized: boolean; private tooltip: HTMLDivElement | null; + private isMinimized: boolean = false; constructor(canvas: Canvas) { this.canvas = canvas; @@ -27,9 +28,10 @@ export class CustomShapeMenu { } this._createUI(); - + if (this.element) { this.element.style.display = 'block'; + this._updateMinimizedState(); } // Position in top-left corner of viewport (closer to edge) @@ -67,16 +69,54 @@ export class CustomShapeMenu { this.element = document.createElement('div'); this.element.id = 'layerforge-custom-shape-menu'; + // --- MINIMIZED BAR --- + const minimizedBar = document.createElement('div'); + minimizedBar.className = 'custom-shape-minimized-bar'; + minimizedBar.textContent = "Custom Output Area Active"; + minimizedBar.style.display = 'none'; + minimizedBar.style.cursor = 'pointer'; + minimizedBar.onclick = () => { + this.isMinimized = false; + this._updateMinimizedState(); + }; + this.element.appendChild(minimizedBar); + + // --- FULL MENU --- + const fullMenu = document.createElement('div'); + fullMenu.className = 'custom-shape-full-menu'; + + // Minimize button (top right) + const minimizeBtn = document.createElement('button'); + minimizeBtn.innerHTML = "–"; + minimizeBtn.title = "Minimize menu"; + minimizeBtn.className = 'custom-shape-minimize-btn'; + minimizeBtn.style.position = 'absolute'; + minimizeBtn.style.top = '4px'; + minimizeBtn.style.right = '4px'; + minimizeBtn.style.width = '24px'; + minimizeBtn.style.height = '24px'; + minimizeBtn.style.border = 'none'; + minimizeBtn.style.background = 'transparent'; + minimizeBtn.style.color = '#888'; + minimizeBtn.style.fontSize = '20px'; + minimizeBtn.style.cursor = 'pointer'; + minimizeBtn.onclick = (e) => { + e.stopPropagation(); + this.isMinimized = true; + this._updateMinimizedState(); + }; + fullMenu.appendChild(minimizeBtn); + // Create menu content const lines = [ - "🎯 Custom Output Area Active" + "Custom Output Area Active" ]; lines.forEach(line => { const lineElement = document.createElement('div'); lineElement.textContent = line; lineElement.className = 'menu-line'; - this.element!.appendChild(lineElement); + fullMenu.appendChild(lineElement); }); // Create a container for the entire shape mask feature set @@ -276,7 +316,7 @@ export class CustomShapeMenu { featherSliderContainer.appendChild(featherValueDisplay); featureContainer.appendChild(featherSliderContainer); - this.element.appendChild(featureContainer); + fullMenu.appendChild(featureContainer); // Create output area extension container const extensionContainer = document.createElement('div'); @@ -394,8 +434,10 @@ export class CustomShapeMenu { slidersContainer.appendChild(createExtensionSlider('Right extension:', 'right')); extensionContainer.appendChild(slidersContainer); - this.element.appendChild(extensionContainer); - + fullMenu.appendChild(extensionContainer); + + this.element.appendChild(fullMenu); + // Add to DOM if (this.canvas.canvas.parentElement) { this.canvas.canvas.parentElement.appendChild(this.element); @@ -405,6 +447,7 @@ export class CustomShapeMenu { this.uiInitialized = true; this._updateUI(); + this._updateMinimizedState(); // Add viewport change listener to update shape preview when zooming/panning this._addViewportChangeListener(); @@ -454,9 +497,13 @@ export class CustomShapeMenu { private _updateUI(): void { if (!this.element) return; - + + // Always update only the full menu part + const fullMenu = this.element.querySelector('.custom-shape-full-menu') as HTMLElement; + if (!fullMenu) return; + const setChecked = (id: string, checked: boolean) => { - const input = this.element!.querySelector(`#${id}`) as HTMLInputElement; + const input = fullMenu.querySelector(`#${id}`) as HTMLInputElement; if (input) input.checked = checked; }; @@ -465,27 +512,40 @@ export class CustomShapeMenu { setChecked('feather-checkbox', this.canvas.shapeMaskFeather); setChecked('extension-checkbox', this.canvas.outputAreaExtensionEnabled); - const expansionCheckbox = this.element.querySelector('#expansion-checkbox')?.parentElement as HTMLElement; + const expansionCheckbox = fullMenu.querySelector('#expansion-checkbox')?.parentElement as HTMLElement; if (expansionCheckbox) { expansionCheckbox.style.display = this.canvas.autoApplyShapeMask ? 'flex' : 'none'; } - const featherCheckbox = this.element.querySelector('#feather-checkbox')?.parentElement as HTMLElement; + const featherCheckbox = fullMenu.querySelector('#feather-checkbox')?.parentElement as HTMLElement; if (featherCheckbox) { featherCheckbox.style.display = this.canvas.autoApplyShapeMask ? 'flex' : 'none'; } - const expansionSliderContainer = this.element.querySelector('#expansion-slider-container') as HTMLElement; + const expansionSliderContainer = fullMenu.querySelector('#expansion-slider-container') as HTMLElement; if (expansionSliderContainer) { expansionSliderContainer.style.display = (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskExpansion) ? 'block' : 'none'; } - const featherSliderContainer = this.element.querySelector('#feather-slider-container') as HTMLElement; + const featherSliderContainer = fullMenu.querySelector('#feather-slider-container') as HTMLElement; if (featherSliderContainer) { featherSliderContainer.style.display = (this.canvas.autoApplyShapeMask && this.canvas.shapeMaskFeather) ? 'block' : 'none'; } } + private _updateMinimizedState(): void { + if (!this.element) return; + const minimizedBar = this.element.querySelector('.custom-shape-minimized-bar') as HTMLElement; + const fullMenu = this.element.querySelector('.custom-shape-full-menu') as HTMLElement; + if (this.isMinimized) { + minimizedBar.style.display = 'block'; + fullMenu.style.display = 'none'; + } else { + minimizedBar.style.display = 'none'; + fullMenu.style.display = 'block'; + } + } + private _updateExtensionUI(): void { if (!this.element) return; diff --git a/src/css/custom_shape_menu.css b/src/css/custom_shape_menu.css index 7f7d9d4..3a1703e 100644 --- a/src/css/custom_shape_menu.css +++ b/src/css/custom_shape_menu.css @@ -29,16 +29,52 @@ gap: 8px; } +/* --- MINIMIZED BAR INTERACTIVE STYLE --- */ +.custom-shape-minimized-bar { + font-size: 13px; + font-weight: 600; + padding: 6px 12px; + border-radius: 6px; + background: #222; + color: #4a90e2; + box-shadow: 0 2px 8px rgba(0,0,0,0.10); + margin: 0 0 8px 0; + user-select: none; + cursor: pointer; + border: 1px solid #444; + transition: background 0.18s, color 0.18s, box-shadow 0.18s, border 0.18s; + outline: none; + text-shadow: none; + display: flex; + align-items: center; + gap: 8px; +} +.custom-shape-minimized-bar:hover, .custom-shape-minimized-bar:focus { + background: #2a2a2a; + color: #4a90e2; + border: 1.5px solid #4a90e2; + box-shadow: 0 4px 16px #4a90e244; +} + #layerforge-custom-shape-menu .feature-container { background-color: #3a3a3a; border-radius: 6px; - padding: 10px; + padding: 10px 12px; border: 1px solid #4a4a4a; + margin-bottom: 12px; + display: flex; + flex-direction: column; + gap: 10px; +} +#layerforge-custom-shape-menu .feature-container:last-child { + margin-bottom: 0; } #layerforge-custom-shape-menu .slider-container { - margin-top: 10px; + margin-top: 6px; + margin-bottom: 0; display: none; + gap: 6px; } #layerforge-custom-shape-menu .slider-label { @@ -95,8 +131,8 @@ #layerforge-custom-shape-menu .checkbox-container { display: flex; align-items: center; - gap: 10px; - padding: 6px; + gap: 8px; + padding: 5px 0; border-radius: 5px; cursor: pointer; transition: background-color 0.2s;