diff --git a/public/scripts/components/ClusterMembersComponent.js b/public/scripts/components/ClusterMembersComponent.js index f68e49b..2f3ccc6 100644 --- a/public/scripts/components/ClusterMembersComponent.js +++ b/public/scripts/components/ClusterMembersComponent.js @@ -518,14 +518,18 @@ class ClusterMembersComponent extends Component { } if (terminalBtn) { - this.addEventListener(terminalBtn, 'click', (e) => { + this.addEventListener(terminalBtn, 'click', async (e) => { e.stopPropagation(); e.preventDefault(); try { - if (window.TerminalPanel) { - this.ensureTerminalContainer(); - window.TerminalPanel.open(this.terminalPanelContainer, memberIp); - } + if (!window.TerminalPanel) return; + this.ensureTerminalContainer(); + const panel = window.TerminalPanel; + const wasMinimized = panel.isMinimized; + panel.open(this.terminalPanelContainer, memberIp); + if (wasMinimized && panel.restore) { + panel.restore(); + } } catch (err) { console.error('Failed to open member terminal:', err); } diff --git a/public/scripts/components/DrawerComponent.js b/public/scripts/components/DrawerComponent.js index 33a3e55..cb0d359 100644 --- a/public/scripts/components/DrawerComponent.js +++ b/public/scripts/components/DrawerComponent.js @@ -35,7 +35,12 @@ class DrawerComponent { header.innerHTML = `
Node Details
- + +
@@ -79,6 +102,7 @@
+
`; @@ -89,6 +113,7 @@ const sendBtn = this.panelEl.querySelector('.terminal-send-btn'); const closeBtn = this.panelEl.querySelector('.terminal-close-btn'); const clearBtn = this.panelEl.querySelector('.terminal-clear-btn'); + const minimizeBtn = this.panelEl.querySelector('.terminal-minimize-btn'); const sendHandler = () => { const value = (this.inputEl && this.inputEl.value) || ''; @@ -106,6 +131,7 @@ }); if (closeBtn) closeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.close(); }); if (clearBtn) clearBtn.addEventListener('click', (e) => { e.stopPropagation(); this._clear(); }); + if (minimizeBtn) minimizeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.minimize(); }); } _connect(nodeIp) { @@ -160,6 +186,74 @@ } } + minimize() { + try { + if (!this.panelEl || this.isMinimized) return; + this.panelEl.classList.remove('visible'); + this.panelEl.classList.add('minimized'); + this.isMinimized = true; + this.isOpen = true; + this._showDock(); + } catch (err) { + console.error('TerminalPanel.minimize error:', err); + } + } + + restore() { + try { + if (!this.panelEl || !this.isMinimized) return; + this.panelEl.classList.remove('minimized'); + requestAnimationFrame(() => { + this.panelEl.classList.add('visible'); + this.inputEl && this.inputEl.focus(); + }); + this.isMinimized = false; + this.isOpen = true; + this._hideDock(); + } catch (err) { + console.error('TerminalPanel.restore error:', err); + } + } + + _ensureDock() { + if (this.dockEl) return this.dockEl; + const dock = document.createElement('div'); + dock.className = 'terminal-dock'; + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'terminal-dock-btn'; + button.title = 'Show Terminal'; + button.setAttribute('aria-label', 'Show Terminal'); + button.innerHTML = ` + + Terminal + `; + button.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.restore(); + }); + dock.appendChild(button); + document.body.appendChild(dock); + this.dockEl = dock; + this.dockBtnEl = button; + return dock; + } + + _showDock() { + const dock = this._ensureDock(); + if (dock) dock.classList.add('visible'); + } + + _hideDock() { + if (this.dockEl) this.dockEl.classList.remove('visible'); + } + _send(text) { try { if (!this.socket || this.socket.readyState !== 1) { diff --git a/public/styles/main.css b/public/styles/main.css index f05d446..fbf1de7 100644 --- a/public/styles/main.css +++ b/public/styles/main.css @@ -3342,13 +3342,19 @@ select.param-input:focus { border: 1px solid var(--border-secondary); color: var(--text-secondary); border-radius: 8px; - padding: 0.35rem 0.6rem; + padding: 0.35rem; cursor: pointer; } .drawer-terminal-btn:hover { background: var(--bg-hover); color: var(--text-primary); } +.drawer-terminal-btn svg { + width: 18px; + height: 18px; + stroke: currentColor; + stroke-width: 2; +} .drawer-close { background: transparent; border: 1px solid var(--border-secondary); @@ -3381,7 +3387,6 @@ select.param-input:focus { display: flex; align-items: flex-end; justify-content: center; - padding: 1.5rem; z-index: 1001; pointer-events: none; } @@ -3404,6 +3409,10 @@ select.param-input:focus { flex-direction: column; } +.terminal-panel.minimized { + display: none; +} + .terminal-panel.visible { transform: translateY(0); opacity: 1; @@ -3437,6 +3446,14 @@ select.param-input:focus { background: var(--bg-hover); color: var(--text-primary); } + +.terminal-actions .terminal-minimize-btn { + width: 2rem; + padding: 0.25rem 0; + font-size: 1rem; + line-height: 1; +} + .terminal-body { flex: 1; overflow: auto; @@ -3465,6 +3482,18 @@ select.param-input:focus { border-radius: 6px; padding: 0.4rem 0.6rem; } +.terminal-input-row .terminal-clear-btn { + background: transparent; + border: 1px solid var(--border-secondary); + color: var(--text-secondary); + border-radius: 6px; + padding: 0.4rem 0.6rem; + cursor: pointer; +} +.terminal-input-row .terminal-clear-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} .terminal-send-btn { background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); color: white; @@ -3477,6 +3506,62 @@ select.param-input:focus { filter: brightness(1.05); } +.terminal-dock { + position: fixed; + bottom: 1.5rem; + left: 50%; + transform: translateX(-50%); + z-index: 1001; + display: none; + pointer-events: none; +} + +.terminal-dock.visible { + display: flex; +} + +.terminal-dock-btn { + display: inline-flex; + align-items: center; + gap: 0.6rem; + padding: 0.5rem 1rem; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(21, 31, 46, 0.85); + backdrop-filter: blur(14px); + color: var(--text-primary); + cursor: pointer; + box-shadow: 0 18px 40px rgba(0,0,0,0.35); + transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; + pointer-events: auto; +} + +.terminal-dock-btn:hover { + transform: translateY(-2px); + box-shadow: 0 22px 44px rgba(0,0,0,0.35); + background: rgba(35, 49, 68, 0.95); +} + +.terminal-dock-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; +} + +.terminal-dock-icon svg { + width: 18px; + height: 18px; + stroke: currentColor; + stroke-width: 2; +} + +.terminal-dock-label { + font-size: 0.9rem; + font-weight: 600; +} + /* Topology hover tooltip for labels */ .topology-tooltip { position: fixed;