diff --git a/src/public/js/tabs.js b/src/public/js/tabs.js index 66450091b..fb325ab87 100644 --- a/src/public/js/tabs.js +++ b/src/public/js/tabs.js @@ -14,10 +14,16 @@ PulseApp.ui.tabs = (() => { nestedTabContentContainer = document.querySelector('#log-content-area'); logSessionArea = document.getElementById('log-session-area'); + // Initial styling pass for all tabs to ensure consistent look from the start + tabs.forEach(tab => { + const isInitiallyActive = tab.classList.contains('active'); + styleMainTab(tab, isInitiallyActive); // Apply the unified styling + }); + tabs.forEach(tab => { tab.addEventListener('click', () => { const tabId = tab.getAttribute('data-tab'); - activateMainTab(tab, tabId); + activateMainTab(tab, tabId); // This will re-apply styles via its own calls to styleMainTab }); }); @@ -35,16 +41,12 @@ PulseApp.ui.tabs = (() => { } function activateMainTab(clickedTab, tabId) { - if (clickedTab.classList.contains('pointer-events-none')) return; // Don't activate disabled tabs + if (clickedTab.classList.contains('pointer-events-none')) return; - tabs.forEach(t => { - t.classList.remove('active', 'bg-white', 'dark:bg-gray-800', 'border-gray-300', 'dark:border-gray-700', 'text-gray-900', 'dark:text-white', '-mb-px'); - t.classList.add('bg-gray-100', 'dark:bg-gray-700/50', 'border-transparent', 'text-gray-600', 'dark:text-gray-400', 'hover:bg-gray-200', 'dark:hover:bg-gray-700'); - }); + tabs.forEach(t => styleMainTab(t, false)); // Use the helper tabContents.forEach(content => content.classList.add('hidden')); - clickedTab.classList.add('active', 'bg-white', 'dark:bg-gray-800', 'border-gray-300', 'dark:border-gray-700', 'text-gray-900', 'dark:text-white', '-mb-px'); - clickedTab.classList.remove('bg-gray-100', 'dark:bg-gray-700/50', 'border-transparent', 'text-gray-600', 'dark:text-gray-400', 'hover:bg-gray-200', 'dark:hover:bg-gray-700'); + styleMainTab(clickedTab, true); // Use the helper const activeContent = document.getElementById(tabId); if (activeContent) { @@ -69,12 +71,57 @@ PulseApp.ui.tabs = (() => { } } + const ACTIVE_MAIN_TAB_CLASSES = ['active', 'bg-white', 'dark:bg-gray-800', 'border-gray-300', 'dark:border-gray-700', 'text-blue-600', 'dark:text-blue-500']; + const INACTIVE_MAIN_TAB_CLASSES = ['text-gray-500', 'dark:text-gray-400', 'hover:text-gray-700', 'dark:hover:text-gray-300', 'hover:bg-gray-200', 'dark:hover:bg-gray-700', 'border-transparent']; + const _ACTIVE_STRUCTURAL_CLASSES = ['border', 'border-b-0', 'rounded-t', '-mb-px']; // For the "connected" look of the active tab + + function styleMainTab(tabElement, isActive) { + const isActuallyDisabled = DISABLED_TAB_CLASSES.some(cls => tabElement.classList.contains(cls)); + + // Always remove all potentially conflicting classes first for a clean slate + tabElement.classList.remove(...ACTIVE_MAIN_TAB_CLASSES); + tabElement.classList.remove(...INACTIVE_MAIN_TAB_CLASSES); + tabElement.classList.remove(..._ACTIVE_STRUCTURAL_CLASSES); + + if (isActuallyDisabled) { + // styleTabAvailability will add/remove DISABLED_TAB_CLASSES and DISABLED_TAB_EXTRA_BG. + // We ensure no 'cursor-pointer' from other states. + tabElement.classList.remove('cursor-pointer'); + } else { + // Not disabled: Apply distinct active or inactive styling. + if (isActive) { + tabElement.classList.add(...ACTIVE_MAIN_TAB_CLASSES); + tabElement.classList.add(..._ACTIVE_STRUCTURAL_CLASSES); + } else { // Is Inactive and Enabled + tabElement.classList.add(...INACTIVE_MAIN_TAB_CLASSES); + // Inactive tabs should not have the specific "connected" look of the active tab. + // _ACTIVE_STRUCTURAL_CLASSES were already removed. + // INACTIVE_MAIN_TAB_CLASSES includes 'border-transparent'. + } + // All enabled tabs (active or inactive) should be clickable. + tabElement.classList.add('cursor-pointer'); + // Clean up any lingering disabled styles if it's now enabled. + tabElement.classList.remove(...DISABLED_TAB_CLASSES); + tabElement.classList.remove(...DISABLED_TAB_EXTRA_BG); + } + } + + const ACTIVE_NESTED_TAB_CLASSES = ['active', 'text-blue-600', 'border-blue-600']; + const INACTIVE_NESTED_TAB_CLASSES = ['text-gray-500', 'hover:text-gray-700', 'dark:text-gray-400', 'dark:hover:text-gray-200', 'border-transparent', 'hover:border-gray-300', 'dark:hover:border-gray-600']; + + function styleNestedTab(tabElement, isActive) { + if (isActive) { + tabElement.classList.add(...ACTIVE_NESTED_TAB_CLASSES); + tabElement.classList.remove(...INACTIVE_NESTED_TAB_CLASSES); + } else { + tabElement.classList.remove(...ACTIVE_NESTED_TAB_CLASSES); + tabElement.classList.add(...INACTIVE_NESTED_TAB_CLASSES); + } + } + function activateNestedTab(targetId) { if (nestedTabsContainer) { - nestedTabsContainer.querySelectorAll('.nested-tab').forEach(nt => { - nt.classList.remove('active', 'text-blue-600', 'border-blue-600'); - nt.classList.add('text-gray-500', 'hover:text-gray-700', 'dark:text-gray-400', 'dark:hover:text-gray-200', 'border-transparent', 'hover:border-gray-300', 'dark:hover:border-gray-600'); - }); + nestedTabsContainer.querySelectorAll('.nested-tab').forEach(nt => styleNestedTab(nt, false)); } if (nestedTabContentContainer) { @@ -85,8 +132,7 @@ PulseApp.ui.tabs = (() => { const targetTab = nestedTabsContainer?.querySelector(`.nested-tab[data-nested-tab="${targetId}"]`); if (targetTab) { - targetTab.classList.add('active', 'text-blue-600', 'border-blue-600'); - targetTab.classList.remove('text-gray-500', 'hover:text-gray-700', 'dark:text-gray-400', 'dark:hover:text-gray-200', 'border-transparent', 'hover:border-gray-300', 'dark:hover:border-gray-600'); + styleNestedTab(targetTab, true); } const targetPanelContainer = nestedTabContentContainer?.querySelector(`#${targetId}`); @@ -96,6 +142,28 @@ PulseApp.ui.tabs = (() => { } } + const DISABLED_TAB_CLASSES = ['opacity-50', 'cursor-not-allowed', 'pointer-events-none']; + const ENABLED_TAB_CLASSES = ['hover:bg-gray-200', 'dark:hover:bg-gray-700', 'cursor-pointer']; + const DISABLED_TAB_EXTRA_BG = ['bg-gray-100', 'dark:bg-gray-700/50', 'border-transparent']; + const ACTIVE_TAB_CLASSES_TO_REMOVE_WHEN_DISABLED = ['active', 'bg-white', 'dark:bg-gray-800', 'border-gray-300', 'dark:border-gray-700']; + + function styleTabAvailability(tabElement, isAvailable) { + if (!isAvailable) { + tabElement.classList.add(...DISABLED_TAB_CLASSES); + tabElement.classList.remove(...ENABLED_TAB_CLASSES); + tabElement.setAttribute('title', 'Requires PBS integration to be configured and connected.'); + // Ensure inactive styling if disabled + tabElement.classList.remove(...ACTIVE_TAB_CLASSES_TO_REMOVE_WHEN_DISABLED); + tabElement.classList.add(...DISABLED_TAB_EXTRA_BG); + } else { + tabElement.classList.remove(...DISABLED_TAB_CLASSES); + tabElement.classList.add(...ENABLED_TAB_CLASSES); + tabElement.removeAttribute('title'); + // Remove potentially added disabled background if it was previously disabled + tabElement.classList.remove(...DISABLED_TAB_EXTRA_BG); + } + } + function updateTabAvailability() { const pbsTab = document.querySelector('.tab[data-tab="pbs"]'); const backupsTab = document.querySelector('.tab[data-tab="backups"]'); @@ -108,34 +176,39 @@ PulseApp.ui.tabs = (() => { const pbsDataArray = PulseApp.state.get('pbsDataArray') || []; const isPbsAvailable = pbsDataArray.length > 0 && pbsDataArray.some(pbs => pbs.status === 'ok'); - const disabledClasses = ['opacity-50', 'cursor-not-allowed', 'pointer-events-none']; - const enabledClasses = ['hover:bg-gray-200', 'dark:hover:bg-gray-700', 'cursor-pointer']; + styleTabAvailability(pbsTab, isPbsAvailable); + styleTabAvailability(backupsTab, isPbsAvailable); + + // Ensure correct active/inactive styling after availability change + const currentActiveTab = document.querySelector('.tab.active'); [pbsTab, backupsTab].forEach(tab => { - if (!isPbsAvailable) { - tab.classList.add(...disabledClasses); - tab.classList.remove(...enabledClasses); - tab.setAttribute('title', 'Requires PBS integration to be configured and connected.'); - tab.classList.remove('active', 'bg-white', 'dark:bg-gray-800', 'border-gray-300', 'dark:border-gray-700'); - tab.classList.add('bg-gray-100', 'dark:bg-gray-700/50', 'border-transparent'); + if (!tab) return; // Skip if tab element doesn't exist + if (isPbsAvailable) { + // Tab is now considered available + if (tab === currentActiveTab) { + // If it's the current active tab, ensure it's styled as active + styleMainTab(tab, true); + } else { + // If it's available but not active, ensure it's styled as inactive + styleMainTab(tab, false); + } } else { - tab.classList.remove(...disabledClasses); - tab.classList.add(...enabledClasses); - tab.removeAttribute('title'); + // Tab is unavailable. styleTabAvailability already handled its appearance. + // If it was the active tab, it should no longer appear active. + // styleTabAvailability removes ACTIVE_TAB_CLASSES_TO_REMOVE_WHEN_DISABLED. + // We might need to explicitly make another tab active if the current one became disabled. + // However, the problem description is about appearance when *available*. + // For now, let's assume disabling an active tab is handled or is a separate concern. + // The main goal here is to fix the appearance when isPbsAvailable is true. } }); } - function addLogTab(sessionId, sessionTitle, fullCriteriaDesc) { - if (!nestedTabsContainer || !nestedTabContentContainer || !logSessionArea) { - console.error("Log tab/content container or session area not found!"); - return null; // Indicate failure - } - const nestedTabId = `log-session-${sessionId}`; - const startTime = new Date(sessionId); - const shortTitle = startTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + // --- Log Tab Helper Functions --- + function createLogTabElement(nestedTabId, shortTitle, fullCriteriaDesc) { const newTab = document.createElement('div'); newTab.className = 'nested-tab px-3 py-1.5 cursor-pointer text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-b-2 border-transparent hover:border-gray-300 dark:hover:border-gray-600 flex items-center gap-1'; newTab.dataset.nestedTab = nestedTabId; @@ -143,107 +216,137 @@ PulseApp.ui.tabs = (() => { ${shortTitle} `; + return newTab; + } - const tabTitleSpan = newTab.querySelector('.log-tab-title'); - if (tabTitleSpan) { - tabTitleSpan.addEventListener('dblclick', () => { - tabTitleSpan.classList.add('hidden'); - const input = document.createElement('input'); - input.type = 'text'; - input.value = tabTitleSpan.textContent; - input.className = 'log-tab-rename-input flex-grow p-0 px-1 h-5 text-xs border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 outline-none'; - input.style.maxWidth = '150px'; + function setupLogTabRename(tabElement) { + const tabTitleSpan = tabElement.querySelector('.log-tab-title'); + if (!tabTitleSpan) return; - const finalizeRename = () => { - const newName = input.value.trim(); - if (newName) tabTitleSpan.textContent = newName; - input.remove(); - tabTitleSpan.classList.remove('hidden'); - }; - const cancelRename = () => { - input.remove(); - tabTitleSpan.classList.remove('hidden'); - }; + tabTitleSpan.addEventListener('dblclick', () => { + tabTitleSpan.classList.add('hidden'); + const input = document.createElement('input'); + input.type = 'text'; + input.value = tabTitleSpan.textContent; + input.className = 'log-tab-rename-input flex-grow p-0 px-1 h-5 text-xs border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 outline-none'; + input.style.maxWidth = '150px'; - input.addEventListener('blur', finalizeRename); - input.addEventListener('keydown', (e) => { - if (e.key === 'Enter') finalizeRename(); - else if (e.key === 'Escape') cancelRename(); - }); - const timerSpan = newTab.querySelector('.log-timer-display'); - if (timerSpan) newTab.insertBefore(input, timerSpan); - else newTab.appendChild(input); - input.focus(); - input.select(); + const finalizeRename = () => { + const newName = input.value.trim(); + if (newName) tabTitleSpan.textContent = newName; + input.remove(); + tabTitleSpan.classList.remove('hidden'); + }; + const cancelRename = () => { + input.remove(); + tabTitleSpan.classList.remove('hidden'); + }; + + input.addEventListener('blur', finalizeRename); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') finalizeRename(); + else if (e.key === 'Escape') cancelRename(); }); - } + // Insert input before the timer span if it exists, otherwise append + const timerSpan = tabElement.querySelector('.log-timer-display'); + if (timerSpan) tabElement.insertBefore(input, timerSpan); + else tabElement.appendChild(input); // Append if no timer yet + + input.focus(); + input.select(); + }); + } + + function createLogTabCloseButton(nestedTabId) { const tabCloseButton = document.createElement('button'); tabCloseButton.className = 'ml-auto pl-2 text-gray-400 hover:text-red-600 dark:hover:text-red-400 opacity-50 hover:opacity-100 transition-opacity'; tabCloseButton.innerHTML = ''; tabCloseButton.title = 'Close & Stop Log'; tabCloseButton.onclick = (event) => { event.stopPropagation(); - const tabElement = event.currentTarget.closest('.nested-tab'); - if (!tabElement) return; - const tabSessionId = tabElement.dataset.nestedTab; - const tabSessionIdStr = tabSessionId.replace('log-session-', ''); - - if (PulseApp.state.getActiveLogSession(tabSessionIdStr)) { - PulseApp.thresholds.logging.stopThresholdLogging(tabSessionIdStr, 'manual'); - } - - const contentToRemove = nestedTabContentContainer.querySelector(`#${tabSessionId}`); - tabElement.remove(); - if (contentToRemove) contentToRemove.remove(); - - if (PulseApp.thresholds.logging) PulseApp.thresholds.logging.updateClearAllButtonVisibility(); - - if (tabElement.classList.contains('active')) { - activateNestedTab('nested-tab-dashboard'); - } - - if (nestedTabsContainer && logSessionArea && nestedTabsContainer.querySelectorAll('.nested-tab[data-nested-tab^="log-session-"]').length === 0) { - logSessionArea.classList.add('hidden'); - } + // Extract sessionId from nestedTabId before calling remove + const sessionId = nestedTabId.replace('log-session-', ''); + removeLogTabAndContent(sessionId, true); // Pass true to indicate manual close }; - newTab.appendChild(tabCloseButton); + return tabCloseButton; + } + function createLogTabContentPanel(nestedTabId) { const newContent = document.createElement('div'); newContent.id = nestedTabId; - newContent.className = 'log-session-panel-container hidden'; + newContent.className = 'log-session-panel-container hidden'; // Start hidden + return newContent; + } - nestedTabsContainer.appendChild(newTab); - nestedTabContentContainer.appendChild(newContent); - logSessionArea.classList.remove('hidden'); + // --- Main Add Log Tab Function --- + function addLogTab(sessionId, sessionTitle, fullCriteriaDesc) { + if (!nestedTabsContainer || !nestedTabContentContainer || !logSessionArea) { + console.error("Log tab/content container or session area not found!"); + return null; + } + const nestedTabId = `log-session-${sessionId}`; + const startTime = new Date(sessionId); + const shortTitle = startTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + + // 1. Create elements using helpers + const newTabElement = createLogTabElement(nestedTabId, shortTitle, fullCriteriaDesc); + const closeButton = createLogTabCloseButton(nestedTabId); // Pass nestedTabId + const newContentPanel = createLogTabContentPanel(nestedTabId); + + // 2. Assemble tab element + newTabElement.appendChild(closeButton); // Append close button + + // 3. Setup interactions + setupLogTabRename(newTabElement); + + // 4. Append to DOM + nestedTabsContainer.appendChild(newTabElement); + nestedTabContentContainer.appendChild(newContentPanel); + logSessionArea.classList.remove('hidden'); // Ensure log area is visible + + // 5. Activate the new tab activateNestedTab(nestedTabId); - return newContent; // Return the content container div + return newContentPanel; // Return the content panel for adding log entries } - function removeLogTabAndContent(sessionId) { - const tabToRemove = nestedTabsContainer?.querySelector(`.nested-tab[data-nested-tab="log-session-${sessionId}"]`); - const contentToRemove = nestedTabContentContainer?.querySelector(`#log-session-${sessionId}`); + function removeLogTabAndContent(sessionId, isManualClose = false) { + const tabId = `log-session-${sessionId}`; + const tabToRemove = nestedTabsContainer?.querySelector(`.nested-tab[data-nested-tab="${tabId}"]`); + const contentToRemove = nestedTabContentContainer?.querySelector(`#${tabId}`); - if (tabToRemove) { - const wasActive = tabToRemove.classList.contains('active'); - tabToRemove.remove(); - if (contentToRemove) contentToRemove.remove(); + if (!tabToRemove) return; // Nothing to remove - if (PulseApp.thresholds.logging) PulseApp.thresholds.logging.updateClearAllButtonVisibility(); + const wasActive = tabToRemove.classList.contains('active'); - if (wasActive) { - activateNestedTab('nested-tab-dashboard'); - } + // Stop logging session if closed manually + if (isManualClose && PulseApp.state.getActiveLogSession(sessionId)) { + PulseApp.thresholds.logging.stopThresholdLogging(sessionId, 'manual'); + } + // Note: PulseApp.state.removeActiveLogSession(sessionId) should be called by stopThresholdLogging - if (nestedTabsContainer && logSessionArea && nestedTabsContainer.querySelectorAll('.nested-tab[data-nested-tab^="log-session-"]').length === 0) { - logSessionArea.classList.add('hidden'); - } - } + // Remove elements from DOM + tabToRemove.remove(); + if (contentToRemove) contentToRemove.remove(); + + // Update UI elements + if (PulseApp.thresholds.logging) { + PulseApp.thresholds.logging.updateClearAllButtonVisibility(); + } + + // Activate dashboard if the removed tab was active + if (wasActive) { + activateNestedTab('nested-tab-dashboard'); + } + + // Hide log area if no log tabs remain + if (nestedTabsContainer && logSessionArea && nestedTabsContainer.querySelectorAll('.nested-tab[data-nested-tab^="log-session-"]').length === 0) { + logSessionArea.classList.add('hidden'); + } } - return { init, activateNestedTab, @@ -251,4 +354,4 @@ PulseApp.ui.tabs = (() => { addLogTab, removeLogTabAndContent }; -})(); \ No newline at end of file +})();