Fix(tabs): Correct styling for consistent active/inactive states & initial load

This commit is contained in:
courtmanr@gmail.com
2025-05-08 11:12:25 +01:00
parent 69e4ee48fb
commit 4edf544681
+211 -108
View File
@@ -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 = (() => {
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block align-middle"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>
<span class="log-tab-title" title="${fullCriteriaDesc || 'Log started ' + shortTitle}">${shortTitle}</span>
`;
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 = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
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
};
})();
})();