diff --git a/static/css/components/search-filter.css b/static/css/components/search-filter.css index 86a99cd7..7734b85c 100644 --- a/static/css/components/search-filter.css +++ b/static/css/components/search-filter.css @@ -361,6 +361,7 @@ padding: 16px; transition: transform 0.3s ease, opacity 0.3s ease; transform-origin: top right; + display: block; /* Ensure it's block by default */ } .search-options-panel.hidden { diff --git a/static/js/components/Header.js b/static/js/components/Header.js new file mode 100644 index 00000000..ee13b8e0 --- /dev/null +++ b/static/js/components/Header.js @@ -0,0 +1,155 @@ +/** + * Header.js - Manages the application header behavior across different pages + * Handles initialization of appropriate search and filter managers based on current page + */ +export class HeaderManager { + constructor() { + this.currentPage = this.detectCurrentPage(); + this.searchManager = null; + this.filterManager = null; + + // Initialize appropriate managers based on current page + this.initializeManagers(); + + // Set up common header functionality + this.initializeCommonElements(); + } + + detectCurrentPage() { + const path = window.location.pathname; + if (path.includes('/loras/recipes')) return 'recipes'; + if (path.includes('/checkpoints')) return 'checkpoints'; + if (path.includes('/loras')) return 'loras'; + return 'unknown'; + } + + initializeManagers() { + // Import and initialize appropriate search manager based on page + if (this.currentPage === 'loras') { + import('../managers/LoraSearchManager.js').then(module => { + const { LoraSearchManager } = module; + this.searchManager = new LoraSearchManager(); + window.searchManager = this.searchManager; + }); + + import('../managers/FilterManager.js').then(module => { + const { FilterManager } = module; + this.filterManager = new FilterManager(); + window.filterManager = this.filterManager; + }); + } else if (this.currentPage === 'recipes') { + import('../managers/RecipeSearchManager.js').then(module => { + const { RecipeSearchManager } = module; + this.searchManager = new RecipeSearchManager(); + window.searchManager = this.searchManager; + }); + + import('../managers/RecipeFilterManager.js').then(module => { + const { RecipeFilterManager } = module; + this.filterManager = new RecipeFilterManager(); + window.filterManager = this.filterManager; + }); + } else if (this.currentPage === 'checkpoints') { + import('../managers/CheckpointSearchManager.js').then(module => { + const { CheckpointSearchManager } = module; + this.searchManager = new CheckpointSearchManager(); + window.searchManager = this.searchManager; + }); + + // Note: Checkpoints page might get its own filter manager in the future + // For now, we can use a basic filter manager or none at all + } + } + + initializeCommonElements() { + // Handle theme toggle + const themeToggle = document.querySelector('.theme-toggle'); + if (themeToggle) { + themeToggle.addEventListener('click', () => { + if (typeof toggleTheme === 'function') { + toggleTheme(); + } + }); + } + + // Handle settings toggle + const settingsToggle = document.querySelector('.settings-toggle'); + if (settingsToggle) { + settingsToggle.addEventListener('click', () => { + if (window.settingsManager && typeof window.settingsManager.toggleSettings === 'function') { + window.settingsManager.toggleSettings(); + } + }); + } + + // Handle update toggle + const updateToggle = document.getElementById('updateToggleBtn'); + if (updateToggle) { + updateToggle.addEventListener('click', () => { + // Handle update check logic + }); + } + + // Handle support toggle + const supportToggle = document.getElementById('supportToggleBtn'); + if (supportToggle) { + supportToggle.addEventListener('click', () => { + // Handle support panel logic + }); + } + } + + // Helper method to update panel positions (can be called from window resize handlers) + updatePanelPositions() { + if (this.searchManager && typeof this.searchManager.updatePanelPositions === 'function') { + this.searchManager.updatePanelPositions(); + } else { + const searchOptionsPanel = document.getElementById('searchOptionsPanel'); + const filterPanel = document.getElementById('filterPanel'); + + if (!searchOptionsPanel && !filterPanel) return; + + // Get the header element + const header = document.querySelector('.app-header'); + if (!header) return; + + // Calculate the position based on the bottom of the header + const headerRect = header.getBoundingClientRect(); + const topPosition = headerRect.bottom + 5; // Add 5px padding + + // Set the positions + if (searchOptionsPanel) { + searchOptionsPanel.style.top = `${topPosition}px`; + } + + if (filterPanel) { + filterPanel.style.top = `${topPosition}px`; + } + + // Adjust panel horizontal position based on the search container + const searchContainer = document.querySelector('.header-search'); + if (searchContainer) { + const searchRect = searchContainer.getBoundingClientRect(); + + // Position the search options panel aligned with the search container + if (searchOptionsPanel) { + searchOptionsPanel.style.right = `${window.innerWidth - searchRect.right}px`; + } + + // Position the filter panel aligned with the filter button + if (filterPanel) { + const filterButton = document.getElementById('filterButton'); + if (filterButton) { + const filterRect = filterButton.getBoundingClientRect(); + filterPanel.style.right = `${window.innerWidth - filterRect.right}px`; + } + } + } + } + } + } + +// Initialize the header manager when the DOM is loaded +document.addEventListener('DOMContentLoaded', () => { +window.headerManager = new HeaderManager(); +}); \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index 78489f0c..8448a052 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -30,6 +30,7 @@ import { moveManager } from './managers/MoveManager.js'; import { FilterManager } from './managers/FilterManager.js'; import { createLoraCard, updateCardsForBulkMode } from './components/LoraCard.js'; import { bulkManager } from './managers/BulkManager.js'; +import { HeaderManager } from './components/Header.js'; // Add bulk mode to state state.bulkMode = false; @@ -76,7 +77,6 @@ document.addEventListener('DOMContentLoaded', async () => { modalManager.initialize(); // Initialize modalManager after DOM is loaded updateService.initialize(); // Initialize updateService after modalManager window.downloadManager = new DownloadManager(); // Move this after modalManager initialization - window.filterManager = new FilterManager(); // Initialize filter manager // Initialize state filters from filterManager if available if (window.filterManager && window.filterManager.filters) { @@ -90,7 +90,6 @@ document.addEventListener('DOMContentLoaded', async () => { initTheme(); initFolderTagsVisibility(); initBackToTop(); - window.searchManager = new SearchManager(); new LoraContextMenu(); // Initialize cards for current bulk mode state (should be false initially) diff --git a/static/js/managers/CheckpointSearchManager.js b/static/js/managers/CheckpointSearchManager.js new file mode 100644 index 00000000..7567bc7b --- /dev/null +++ b/static/js/managers/CheckpointSearchManager.js @@ -0,0 +1,150 @@ +/** + * CheckpointSearchManager - Specialized search manager for the Checkpoints page + * Extends the base SearchManager with checkpoint-specific functionality + */ +import { SearchManager } from './SearchManager.js'; +import { state } from '../state/index.js'; +import { showToast } from '../utils/uiHelpers.js'; + +export class CheckpointSearchManager extends SearchManager { + constructor(options = {}) { + super({ + page: 'checkpoints', + ...options + }); + + this.currentSearchTerm = ''; + + // Store this instance in the state + if (state) { + state.searchManager = this; + } + } + + async performSearch() { + const searchTerm = this.searchInput.value.trim().toLowerCase(); + + if (searchTerm === this.currentSearchTerm && !this.isSearching) { + return; // Avoid duplicate searches + } + + this.currentSearchTerm = searchTerm; + + const grid = document.getElementById('checkpointGrid'); + + if (!searchTerm) { + if (state) { + state.currentPage = 1; + } + this.resetAndReloadCheckpoints(); + return; + } + + try { + this.isSearching = true; + if (state && state.loadingManager) { + state.loadingManager.showSimpleLoading('Searching checkpoints...'); + } + + // Store current scroll position + const scrollPosition = window.pageYOffset || document.documentElement.scrollTop; + + if (state) { + state.currentPage = 1; + state.hasMore = true; + } + + const url = new URL('/api/checkpoints', window.location.origin); + url.searchParams.set('page', '1'); + url.searchParams.set('page_size', '20'); + url.searchParams.set('sort_by', state ? state.sortBy : 'name'); + url.searchParams.set('search', searchTerm); + url.searchParams.set('fuzzy', 'true'); + + // Add search options + const searchOptions = this.getActiveSearchOptions(); + url.searchParams.set('search_filename', searchOptions.filename.toString()); + url.searchParams.set('search_modelname', searchOptions.modelname.toString()); + + // Always send folder parameter if there is an active folder + if (state && state.activeFolder) { + url.searchParams.set('folder', state.activeFolder); + // Add recursive parameter when recursive search is enabled + const recursive = this.recursiveSearchToggle ? this.recursiveSearchToggle.checked : false; + url.searchParams.set('recursive', recursive.toString()); + } + + const response = await fetch(url); + + if (!response.ok) { + throw new Error('Search failed'); + } + + const data = await response.json(); + + if (searchTerm === this.currentSearchTerm && grid) { + grid.innerHTML = ''; + + if (data.items.length === 0) { + grid.innerHTML = '
No matching checkpoints found
'; + if (state) { + state.hasMore = false; + } + } else { + this.appendCheckpointCards(data.items); + if (state) { + state.hasMore = state.currentPage < data.total_pages; + state.currentPage++; + } + } + + // Restore scroll position after content is loaded + setTimeout(() => { + window.scrollTo({ + top: scrollPosition, + behavior: 'instant' // Use 'instant' to prevent animation + }); + }, 10); + } + } catch (error) { + console.error('Checkpoint search error:', error); + showToast('Checkpoint search failed', 'error'); + } finally { + this.isSearching = false; + if (state && state.loadingManager) { + state.loadingManager.hide(); + } + } + } + + resetAndReloadCheckpoints() { + // This function would be implemented in the checkpoints page + if (typeof window.loadCheckpoints === 'function') { + window.loadCheckpoints(); + } else { + // Fallback to reloading the page + window.location.reload(); + } + } + + appendCheckpointCards(checkpoints) { + // This function would be implemented in the checkpoints page + const grid = document.getElementById('checkpointGrid'); + if (!grid) return; + + if (typeof window.appendCheckpointCards === 'function') { + window.appendCheckpointCards(checkpoints); + } else { + // Fallback implementation + checkpoints.forEach(checkpoint => { + const card = document.createElement('div'); + card.className = 'checkpoint-card'; + card.innerHTML = ` +

${checkpoint.name}

+

${checkpoint.filename || 'No filename'}

+ `; + grid.appendChild(card); + }); + } + } +} \ No newline at end of file diff --git a/static/js/managers/LoraSearchManager.js b/static/js/managers/LoraSearchManager.js new file mode 100644 index 00000000..e9c22a65 --- /dev/null +++ b/static/js/managers/LoraSearchManager.js @@ -0,0 +1,135 @@ +/** + * LoraSearchManager - Specialized search manager for the LoRAs page + * Extends the base SearchManager with LoRA-specific functionality + */ +import { SearchManager } from './SearchManager.js'; +import { appendLoraCards } from '../api/loraApi.js'; +import { resetAndReload } from '../api/loraApi.js'; +import { state } from '../state/index.js'; +import { showToast } from '../utils/uiHelpers.js'; + +export class LoraSearchManager extends SearchManager { + constructor(options = {}) { + console.log("initializing lora search manager"); + super({ + page: 'loras', + ...options + }); + + this.currentSearchTerm = ''; + + // Store this instance in the state + if (state) { + state.searchManager = this; + } + } + + async performSearch() { + const searchTerm = this.searchInput.value.trim().toLowerCase(); + + // Log the search attempt for debugging + console.log('LoraSearchManager performSearch called with:', searchTerm); + + if (searchTerm === this.currentSearchTerm && !this.isSearching) { + return; // Avoid duplicate searches + } + + this.currentSearchTerm = searchTerm; + + const grid = document.getElementById('loraGrid'); + if (!grid) { + console.error('Error: Could not find loraGrid element'); + return; + } + + if (!searchTerm) { + if (state) { + state.currentPage = 1; + } + await resetAndReload(); + return; + } + + try { + this.isSearching = true; + if (state && state.loadingManager) { + state.loadingManager.showSimpleLoading('Searching...'); + } + + // Store current scroll position + const scrollPosition = window.pageYOffset || document.documentElement.scrollTop; + + if (state) { + state.currentPage = 1; + state.hasMore = true; + } + + const url = new URL('/api/loras', window.location.origin); + url.searchParams.set('page', '1'); + url.searchParams.set('page_size', '20'); + url.searchParams.set('sort_by', state ? state.sortBy : 'name'); + url.searchParams.set('search', searchTerm); + url.searchParams.set('fuzzy', 'true'); + + // Add search options + const searchOptions = this.getActiveSearchOptions(); + console.log('Active search options:', searchOptions); + + // Make sure we're sending boolean values as strings + url.searchParams.set('search_filename', searchOptions.filename ? 'true' : 'false'); + url.searchParams.set('search_modelname', searchOptions.modelname ? 'true' : 'false'); + url.searchParams.set('search_tags', searchOptions.tags ? 'true' : 'false'); + + // Always send folder parameter if there is an active folder + if (state && state.activeFolder) { + url.searchParams.set('folder', state.activeFolder); + // Add recursive parameter when recursive search is enabled + const recursive = this.recursiveSearchToggle ? this.recursiveSearchToggle.checked : false; + url.searchParams.set('recursive', recursive.toString()); + } + + console.log('Search URL:', url.toString()); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Search failed with status: ${response.status}`); + } + + const data = await response.json(); + console.log('Search results:', data); + + if (searchTerm === this.currentSearchTerm) { + grid.innerHTML = ''; + + if (data.items.length === 0) { + grid.innerHTML = '
No matching loras found
'; + if (state) { + state.hasMore = false; + } + } else { + appendLoraCards(data.items); + if (state) { + state.hasMore = state.currentPage < data.total_pages; + state.currentPage++; + } + } + + // Restore scroll position after content is loaded + setTimeout(() => { + window.scrollTo({ + top: scrollPosition, + behavior: 'instant' // Use 'instant' to prevent animation + }); + }, 10); + } + } catch (error) { + console.error('Search error:', error); + showToast('Search failed', 'error'); + } finally { + this.isSearching = false; + if (state && state.loadingManager) { + state.loadingManager.hide(); + } + } + } +} \ No newline at end of file diff --git a/static/js/managers/RecipeSearchManager.js b/static/js/managers/RecipeSearchManager.js new file mode 100644 index 00000000..bd72bf26 --- /dev/null +++ b/static/js/managers/RecipeSearchManager.js @@ -0,0 +1,145 @@ +/** + * RecipeSearchManager - Specialized search manager for the Recipes page + * Extends the base SearchManager with recipe-specific functionality + */ +import { SearchManager } from './SearchManager.js'; +import { state } from '../state/index.js'; +import { showToast } from '../utils/uiHelpers.js'; + +export class RecipeSearchManager extends SearchManager { + constructor(options = {}) { + super({ + page: 'recipes', + ...options + }); + + this.currentSearchTerm = ''; + + // Store this instance in the state + if (state) { + state.searchManager = this; + } + } + + async performSearch() { + const searchTerm = this.searchInput.value.trim().toLowerCase(); + + if (searchTerm === this.currentSearchTerm && !this.isSearching) { + return; // Avoid duplicate searches + } + + this.currentSearchTerm = searchTerm; + + const grid = document.getElementById('recipeGrid'); + + if (!searchTerm) { + if (state) { + state.currentPage = 1; + } + this.resetAndReloadRecipes(); + return; + } + + try { + this.isSearching = true; + if (state && state.loadingManager) { + state.loadingManager.showSimpleLoading('Searching recipes...'); + } + + // Store current scroll position + const scrollPosition = window.pageYOffset || document.documentElement.scrollTop; + + if (state) { + state.currentPage = 1; + state.hasMore = true; + } + + const url = new URL('/api/recipes', window.location.origin); + url.searchParams.set('page', '1'); + url.searchParams.set('page_size', '20'); + url.searchParams.set('sort_by', state ? state.sortBy : 'name'); + url.searchParams.set('search', searchTerm); + url.searchParams.set('fuzzy', 'true'); + + // Add search options + const searchOptions = this.getActiveSearchOptions(); + url.searchParams.set('search_name', searchOptions.modelname.toString()); + url.searchParams.set('search_tags', searchOptions.tags.toString()); + url.searchParams.set('search_loras', searchOptions.loras.toString()); + + const response = await fetch(url); + + if (!response.ok) { + throw new Error('Search failed'); + } + + const data = await response.json(); + + if (searchTerm === this.currentSearchTerm && grid) { + grid.innerHTML = ''; + + if (data.items.length === 0) { + grid.innerHTML = '
No matching recipes found
'; + if (state) { + state.hasMore = false; + } + } else { + this.appendRecipeCards(data.items); + if (state) { + state.hasMore = state.currentPage < data.total_pages; + state.currentPage++; + } + } + + // Restore scroll position after content is loaded + setTimeout(() => { + window.scrollTo({ + top: scrollPosition, + behavior: 'instant' // Use 'instant' to prevent animation + }); + }, 10); + } + } catch (error) { + console.error('Recipe search error:', error); + showToast('Recipe search failed', 'error'); + } finally { + this.isSearching = false; + if (state && state.loadingManager) { + state.loadingManager.hide(); + } + } + } + + resetAndReloadRecipes() { + // This function would be implemented in the recipes page + // Similar to resetAndReload for loras + if (typeof window.loadRecipes === 'function') { + window.loadRecipes(); + } else { + // Fallback to reloading the page + window.location.reload(); + } + } + + appendRecipeCards(recipes) { + // This function would be implemented in the recipes page + // Similar to appendLoraCards for loras + const grid = document.getElementById('recipeGrid'); + if (!grid) return; + + if (typeof window.appendRecipeCards === 'function') { + window.appendRecipeCards(recipes); + } else { + // Fallback implementation + recipes.forEach(recipe => { + const card = document.createElement('div'); + card.className = 'recipe-card'; + card.innerHTML = ` +

${recipe.name}

+

${recipe.description || 'No description'}

+ `; + grid.appendChild(card); + }); + } + } +} \ No newline at end of file diff --git a/static/js/managers/SearchManager.js b/static/js/managers/SearchManager.js index 51a00343..17259883 100644 --- a/static/js/managers/SearchManager.js +++ b/static/js/managers/SearchManager.js @@ -18,10 +18,20 @@ export class SearchManager { this.recursiveSearchToggle = document.getElementById('recursiveSearchToggle'); this.searchTimeout = null; - this.currentPage = document.body.dataset.page || 'loras'; + this.currentPage = options.page || document.body.dataset.page || 'loras'; + this.isSearching = false; + + // Create clear button for search input + this.createClearButton(); this.initEventListeners(); this.loadSearchPreferences(); + + // Initialize panel positions + this.updatePanelPositions(); + + // Add resize listener + window.addEventListener('resize', this.updatePanelPositions.bind(this)); } initEventListeners() { @@ -30,12 +40,14 @@ export class SearchManager { this.searchInput.addEventListener('input', () => { clearTimeout(this.searchTimeout); this.searchTimeout = setTimeout(() => this.performSearch(), this.options.searchDelay); + this.updateClearButtonVisibility(); }); // Clear search with Escape key this.searchInput.addEventListener('keydown', (e) => { if (e.key === 'Escape') { this.searchInput.value = ''; + this.updateClearButtonVisibility(); this.performSearch(); } }); @@ -44,14 +56,14 @@ export class SearchManager { // Search options toggle if (this.searchOptionsToggle) { this.searchOptionsToggle.addEventListener('click', () => { - this.searchOptionsPanel.classList.toggle('hidden'); + this.toggleSearchOptionsPanel(); }); } // Close search options if (this.closeSearchOptions) { this.closeSearchOptions.addEventListener('click', () => { - this.searchOptionsPanel.classList.add('hidden'); + this.closeSearchOptionsPanel(); }); } @@ -59,6 +71,16 @@ export class SearchManager { if (this.searchOptionTags) { this.searchOptionTags.forEach(tag => { tag.addEventListener('click', () => { + // Check if clicking would deselect the last active option + const activeOptions = document.querySelectorAll('.search-option-tag.active'); + if (activeOptions.length === 1 && activeOptions[0] === tag) { + // Don't allow deselecting the last option + if (typeof showToast === 'function') { + showToast('At least one search option must be selected', 'info'); + } + return; + } + tag.classList.toggle('active'); this.saveSearchPreferences(); this.performSearch(); @@ -73,6 +95,89 @@ export class SearchManager { this.performSearch(); }); } + + // Add global click handler to close panels when clicking outside + document.addEventListener('click', (e) => { + // Close search options panel when clicking outside + if (this.searchOptionsPanel && + !this.searchOptionsPanel.contains(e.target) && + e.target !== this.searchOptionsToggle && + !this.searchOptionsToggle.contains(e.target)) { + this.closeSearchOptionsPanel(); + } + + // Close filter panel when clicking outside (if filterManager exists) + const filterPanel = document.getElementById('filterPanel'); + const filterButton = document.getElementById('filterButton'); + if (filterPanel && + !filterPanel.contains(e.target) && + e.target !== filterButton && + !filterButton.contains(e.target) && + window.filterManager) { + window.filterManager.closeFilterPanel(); + } + }); + } + + createClearButton() { + // Create clear button if it doesn't exist + if (!this.searchInput) return; + + // Check if clear button already exists + let clearButton = this.searchInput.parentNode.querySelector('.search-clear'); + + if (!clearButton) { + // Create clear button + clearButton = document.createElement('button'); + clearButton.className = 'search-clear'; + clearButton.innerHTML = ''; + clearButton.title = 'Clear search'; + + // Add click handler + clearButton.addEventListener('click', () => { + this.searchInput.value = ''; + this.updateClearButtonVisibility(); + this.performSearch(); + }); + + // Insert after search input + this.searchInput.parentNode.appendChild(clearButton); + } + + this.clearButton = clearButton; + + // Set initial visibility + this.updateClearButtonVisibility(); + } + + updateClearButtonVisibility() { + if (this.clearButton) { + this.clearButton.classList.toggle('visible', this.searchInput.value.length > 0); + } + } + + toggleSearchOptionsPanel() { + if (this.searchOptionsPanel) { + const isHidden = this.searchOptionsPanel.classList.contains('hidden'); + if (isHidden) { + // Update position before showing + this.updatePanelPositions(); + this.searchOptionsPanel.classList.remove('hidden'); + this.searchOptionsToggle.classList.add('active'); + + // Ensure the panel is visible + this.searchOptionsPanel.style.display = 'block'; + } else { + this.closeSearchOptionsPanel(); + } + } + } + + closeSearchOptionsPanel() { + if (this.searchOptionsPanel) { + this.searchOptionsPanel.classList.add('hidden'); + this.searchOptionsToggle.classList.remove('active'); + } } loadSearchPreferences() { @@ -93,11 +198,45 @@ export class SearchManager { if (this.recursiveSearchToggle && preferences.recursive !== undefined) { this.recursiveSearchToggle.checked = preferences.recursive; } + + // Ensure at least one search option is selected + this.validateSearchOptions(); } catch (error) { console.error('Error loading search preferences:', error); + // Set default options if loading fails + this.setDefaultSearchOptions(); } } + validateSearchOptions() { + // Check if at least one search option is active + const hasActiveOption = Array.from(this.searchOptionTags).some(tag => + tag.classList.contains('active') + ); + + // If no search options are active, activate default options + if (!hasActiveOption) { + this.setDefaultSearchOptions(); + } + } + + setDefaultSearchOptions() { + // Default to filename search option if available + const filenameOption = Array.from(this.searchOptionTags).find(tag => + tag.dataset.option === 'filename' + ); + + if (filenameOption) { + filenameOption.classList.add('active'); + } else if (this.searchOptionTags.length > 0) { + // Otherwise, select the first option + this.searchOptionTags[0].classList.add('active'); + } + + // Save the default preferences + this.saveSearchPreferences(); + } + saveSearchPreferences() { try { const options = {}; @@ -117,15 +256,63 @@ export class SearchManager { } getActiveSearchOptions() { - const options = []; + const options = {}; this.searchOptionTags.forEach(tag => { - if (tag.classList.contains('active')) { - options.push(tag.dataset.option); - } + options[tag.dataset.option] = tag.classList.contains('active'); }); return options; } + updatePanelPositions() { + const searchOptionsPanel = document.getElementById('searchOptionsPanel'); + const filterPanel = document.getElementById('filterPanel'); + + if (!searchOptionsPanel && !filterPanel) return; + + // Get the header element + const header = document.querySelector('.app-header'); + if (!header) return; + + // Calculate the position based on the bottom of the header + const headerRect = header.getBoundingClientRect(); + const topPosition = headerRect.bottom + 5; // Add 5px padding + + // Set the positions + if (searchOptionsPanel) { + searchOptionsPanel.style.top = `${topPosition}px`; + + // Make sure the panel is visible when positioned + if (!searchOptionsPanel.classList.contains('hidden') && + window.getComputedStyle(searchOptionsPanel).display === 'none') { + searchOptionsPanel.style.display = 'block'; + } + } + + if (filterPanel) { + filterPanel.style.top = `${topPosition}px`; + } + + // Adjust panel horizontal position based on the search container + const searchContainer = document.querySelector('.header-search'); + if (searchContainer) { + const searchRect = searchContainer.getBoundingClientRect(); + + // Position the search options panel aligned with the search container + if (searchOptionsPanel) { + searchOptionsPanel.style.right = `${window.innerWidth - searchRect.right}px`; + } + + // Position the filter panel aligned with the filter button + if (filterPanel) { + const filterButton = document.getElementById('filterButton'); + if (filterButton) { + const filterRect = filterButton.getBoundingClientRect(); + filterPanel.style.right = `${window.innerWidth - filterRect.right}px`; + } + } + } + } + performSearch() { const query = this.searchInput.value.trim(); const options = this.getActiveSearchOptions(); diff --git a/static/js/recipes.js b/static/js/recipes.js index 23ec1f4b..748df4c5 100644 --- a/static/js/recipes.js +++ b/static/js/recipes.js @@ -6,6 +6,7 @@ import { ImportManager } from './managers/ImportManager.js'; import { RecipeCard } from './components/RecipeCard.js'; import { RecipeModal } from './components/RecipeModal.js'; import { SearchManager } from './managers/SearchManager.js'; +import { HeaderManager } from './components/Header.js'; class RecipeManager { constructor() { diff --git a/templates/components/header.html b/templates/components/header.html index 9ad6cfdb..6e169cf9 100644 --- a/templates/components/header.html +++ b/templates/components/header.html @@ -142,19 +142,38 @@ const recipesNavItem = document.getElementById('recipesNavItem'); const checkpointsNavItem = document.getElementById('checkpointsNavItem'); - const lorasIndicator = document.getElementById('lorasIndicator'); - const recipesIndicator = document.getElementById('recipesIndicator'); - const checkpointsIndicator = document.getElementById('checkpointsIndicator'); - if (currentPath === '/loras') { lorasNavItem.classList.add('active'); - lorasIndicator.style.display = 'block'; } else if (currentPath === '/loras/recipes') { recipesNavItem.classList.add('active'); - recipesIndicator.style.display = 'block'; } else if (currentPath === '/checkpoints') { checkpointsNavItem.classList.add('active'); - checkpointsIndicator.style.display = 'block'; + } + + // Initialize search options panel toggle + const searchOptionsToggle = document.getElementById('searchOptionsToggle'); + const searchOptionsPanel = document.getElementById('searchOptionsPanel'); + + if (searchOptionsToggle && searchOptionsPanel) { + searchOptionsToggle.addEventListener('click', function() { + const isHidden = searchOptionsPanel.classList.contains('hidden'); + if (isHidden) { + searchOptionsPanel.classList.remove('hidden'); + searchOptionsToggle.classList.add('active'); + } else { + searchOptionsPanel.classList.add('hidden'); + searchOptionsToggle.classList.remove('active'); + } + }); + + // Close search options panel when clicking the close button + const closeSearchOptions = document.getElementById('closeSearchOptions'); + if (closeSearchOptions) { + closeSearchOptions.addEventListener('click', function() { + searchOptionsPanel.classList.add('hidden'); + searchOptionsToggle.classList.remove('active'); + }); + } } }); \ No newline at end of file