diff --git a/docs/ui-ux-optimization/progress-tracker.md b/docs/ui-ux-optimization/progress-tracker.md index eb2396a4..b6215d73 100644 --- a/docs/ui-ux-optimization/progress-tracker.md +++ b/docs/ui-ux-optimization/progress-tracker.md @@ -3,7 +3,7 @@ ## Project Overview **Goal**: Optimize Settings Modal UI/UX with left navigation sidebar **Started**: 2026-02-23 -**Current Phase**: P0 - Left Navigation Sidebar +**Current Phase**: P2 - Search Bar (Completed) --- @@ -74,25 +74,42 @@ None currently ## Phase 1: Section Collapse/Expand (P1) -### Status: Planned +### Status: Completed ✓ + +### Completion Notes +- All sections now have collapse/expand functionality +- Chevron icon rotates smoothly on toggle +- State persistence via localStorage working correctly +- CSS animations for smooth height transitions +- Settings order reorganized to match sidebar navigation ### Tasks -- [ ] Add collapse/expand toggle to section headers -- [ ] Add chevron icon with rotation animation -- [ ] Implement localStorage for state persistence -- [ ] Add CSS animations for smooth transitions +- [x] Add collapse/expand toggle to section headers +- [x] Add chevron icon with rotation animation +- [x] Implement localStorage for state persistence +- [x] Add CSS animations for smooth transitions +- [x] Reorder settings sections to match sidebar navigation --- ## Phase 2: Search Bar (P1) -### Status: Planned +### Status: Completed ✓ + +### Completion Notes +- Search input added to settings modal header with icon and clear button +- Real-time filtering with debounced input (150ms delay) +- Highlight matching terms with accent color background +- Handle empty search results with user-friendly message +- Keyboard shortcuts: Escape to clear search +- Sections with matches are automatically expanded +- All translation keys added and synchronized across languages ### Tasks -- [ ] Add search input to header area -- [ ] Implement real-time filtering -- [ ] Add highlight for matched terms -- [ ] Handle empty search results +- [x] Add search input to header area +- [x] Implement real-time filtering +- [x] Add highlight for matched terms +- [x] Handle empty search results --- @@ -121,7 +138,29 @@ None currently ## Change Log -### 2026-02-23 +### 2026-02-23 (P2) +- Completed Phase 2: Search Bar +- Added search input to settings modal header with search icon and clear button +- Implemented real-time filtering with 150ms debounce for performance +- Added visual highlighting for matched search terms using accent color +- Implemented empty search results state with user-friendly message +- Added keyboard shortcuts (Escape to clear search) +- Sections with matching content are automatically expanded during search +- Updated SettingsManager.js with search initialization and filtering logic +- Added comprehensive CSS styles for search input, highlights, and responsive design +- Added translation keys for search feature (placeholder, clear, no results) +- Synchronized translations across all language files + +### 2026-02-23 (P1) +- Completed Phase 1: Section Collapse/Expand +- Added collapse/expand functionality to all settings sections +- Implemented chevron icon with smooth rotation animation +- Added localStorage persistence for collapse state +- Reorganized settings sections to match sidebar navigation order +- Updated SettingsManager.js with section collapse initialization +- Added CSS styles for smooth transitions and animations + +### 2026-02-23 (P0) - Created project documentation - Started Phase 0 implementation - Analyzed existing code structure diff --git a/locales/de.json b/locales/de.json index 2a4c0200..7bf0be6e 100644 --- a/locales/de.json +++ b/locales/de.json @@ -275,6 +275,11 @@ "download": "[TODO: Translate] Download", "advanced": "[TODO: Translate] Advanced" }, + "search": { + "placeholder": "[TODO: Translate] Search settings...", + "clear": "[TODO: Translate] Clear search", + "noResults": "[TODO: Translate] No settings found matching \"{query}\"" + }, "storage": { "locationLabel": "Portabler Modus", "locationHelp": "Aktiviere, um settings.json im Repository zu belassen; deaktiviere, um es im Benutzerkonfigurationsordner zu speichern." diff --git a/locales/en.json b/locales/en.json index 9129c8b7..d50d0e6d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -275,6 +275,11 @@ "download": "Download", "advanced": "Advanced" }, + "search": { + "placeholder": "Search settings...", + "clear": "Clear search", + "noResults": "No settings found matching \"{query}\"" + }, "storage": { "locationLabel": "Portable mode", "locationHelp": "Enable to keep settings.json inside the repository; disable to store it in your user config directory." diff --git a/locales/es.json b/locales/es.json index 13662b7e..f2ef36c0 100644 --- a/locales/es.json +++ b/locales/es.json @@ -275,6 +275,11 @@ "download": "[TODO: Translate] Download", "advanced": "[TODO: Translate] Advanced" }, + "search": { + "placeholder": "[TODO: Translate] Search settings...", + "clear": "[TODO: Translate] Clear search", + "noResults": "[TODO: Translate] No settings found matching \"{query}\"" + }, "storage": { "locationLabel": "Modo portátil", "locationHelp": "Activa para mantener settings.json dentro del repositorio; desactívalo para guardarlo en tu directorio de configuración de usuario." diff --git a/locales/fr.json b/locales/fr.json index 89f1315c..4140c3ad 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -275,6 +275,11 @@ "download": "[TODO: Translate] Download", "advanced": "[TODO: Translate] Advanced" }, + "search": { + "placeholder": "[TODO: Translate] Search settings...", + "clear": "[TODO: Translate] Clear search", + "noResults": "[TODO: Translate] No settings found matching \"{query}\"" + }, "storage": { "locationLabel": "Mode portable", "locationHelp": "Activez pour garder settings.json dans le dépôt ; désactivez pour le placer dans votre dossier de configuration utilisateur." diff --git a/locales/he.json b/locales/he.json index 01ad3ed0..1f4c3af2 100644 --- a/locales/he.json +++ b/locales/he.json @@ -275,6 +275,11 @@ "download": "[TODO: Translate] Download", "advanced": "[TODO: Translate] Advanced" }, + "search": { + "placeholder": "[TODO: Translate] Search settings...", + "clear": "[TODO: Translate] Clear search", + "noResults": "[TODO: Translate] No settings found matching \"{query}\"" + }, "storage": { "locationLabel": "מצב נייד", "locationHelp": "הפעל כדי לשמור את settings.json בתוך המאגר; בטל כדי לשמור אותו בתיקיית ההגדרות של המשתמש." diff --git a/locales/ja.json b/locales/ja.json index dd7004d1..b152801f 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -275,6 +275,11 @@ "download": "[TODO: Translate] Download", "advanced": "[TODO: Translate] Advanced" }, + "search": { + "placeholder": "[TODO: Translate] Search settings...", + "clear": "[TODO: Translate] Clear search", + "noResults": "[TODO: Translate] No settings found matching \"{query}\"" + }, "storage": { "locationLabel": "ポータブルモード", "locationHelp": "有効にすると settings.json をリポジトリ内に保持し、無効にするとユーザー設定ディレクトリに格納します。" diff --git a/locales/ko.json b/locales/ko.json index d3ffcdde..43324c95 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -275,6 +275,11 @@ "download": "[TODO: Translate] Download", "advanced": "[TODO: Translate] Advanced" }, + "search": { + "placeholder": "[TODO: Translate] Search settings...", + "clear": "[TODO: Translate] Clear search", + "noResults": "[TODO: Translate] No settings found matching \"{query}\"" + }, "storage": { "locationLabel": "휴대용 모드", "locationHelp": "활성화하면 settings.json을 리포지토리에 유지하고, 비활성화하면 사용자 구성 디렉터리에 저장합니다." diff --git a/locales/ru.json b/locales/ru.json index 29788e47..d093a712 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -275,6 +275,11 @@ "download": "[TODO: Translate] Download", "advanced": "[TODO: Translate] Advanced" }, + "search": { + "placeholder": "[TODO: Translate] Search settings...", + "clear": "[TODO: Translate] Clear search", + "noResults": "[TODO: Translate] No settings found matching \"{query}\"" + }, "storage": { "locationLabel": "Портативный режим", "locationHelp": "Включите, чтобы хранить settings.json в репозитории; выключите, чтобы сохранить его в папке конфигурации пользователя." diff --git a/locales/zh-CN.json b/locales/zh-CN.json index e4346b84..0a18ce44 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -275,6 +275,11 @@ "download": "[TODO: Translate] Download", "advanced": "[TODO: Translate] Advanced" }, + "search": { + "placeholder": "[TODO: Translate] Search settings...", + "clear": "[TODO: Translate] Clear search", + "noResults": "[TODO: Translate] No settings found matching \"{query}\"" + }, "storage": { "locationLabel": "便携模式", "locationHelp": "开启可将 settings.json 保存在仓库中;关闭则保存在用户配置目录。" diff --git a/locales/zh-TW.json b/locales/zh-TW.json index ebb43375..e1af45d7 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -275,6 +275,11 @@ "download": "[TODO: Translate] Download", "advanced": "[TODO: Translate] Advanced" }, + "search": { + "placeholder": "[TODO: Translate] Search settings...", + "clear": "[TODO: Translate] Clear search", + "noResults": "[TODO: Translate] No settings found matching \"{query}\"" + }, "storage": { "locationLabel": "可攜式模式", "locationHelp": "啟用可將 settings.json 保存在儲存庫中;停用則保存在使用者設定目錄。" diff --git a/static/css/components/modal/settings-modal.css b/static/css/components/modal/settings-modal.css index 54bcfad2..efae43bb 100644 --- a/static/css/components/modal/settings-modal.css +++ b/static/css/components/modal/settings-modal.css @@ -120,9 +120,115 @@ .settings-header { display: flex; align-items: center; - justify-content: flex-start; gap: var(--space-1); margin-bottom: var(--space-2); + padding-right: 40px; /* Space for close button */ +} + +.settings-header .settings-search-wrapper { + margin-left: auto; +} + +/* Search Input Styles */ +.settings-search-wrapper { + position: relative; + display: flex; + align-items: center; + width: 240px; +} + +.settings-search-icon { + position: absolute; + left: 10px; + color: var(--text-color); + opacity: 0.5; + font-size: 0.9em; + pointer-events: none; +} + +.settings-search-input { + width: 100%; + padding: 6px 28px 6px 32px; + height: 32px; + border-radius: var(--border-radius-xs); + border: 1px solid var(--border-color); + background-color: var(--lora-surface); + color: var(--text-color); + font-size: 0.9em; + transition: all 0.2s ease; +} + +.settings-search-input:focus { + border-color: var(--lora-accent); + outline: none; + box-shadow: 0 0 0 2px rgba(var(--lora-accent-rgb, 79, 70, 229), 0.1); +} + +.settings-search-input::placeholder { + color: var(--text-color); + opacity: 0.5; +} + +.settings-search-clear { + position: absolute; + right: 6px; + width: 20px; + height: 20px; + border: none; + background: rgba(var(--border-color-rgb, 148, 163, 184), 0.3); + color: var(--text-color); + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.7em; + opacity: 0.6; + transition: all 0.2s ease; +} + +.settings-search-clear:hover { + opacity: 1; + background: rgba(var(--border-color-rgb, 148, 163, 184), 0.5); +} + +/* Search Highlight Styles */ +.settings-search-highlight { + background-color: rgba(var(--lora-accent-rgb, 79, 70, 229), 0.3); + color: var(--lora-accent); + padding: 0 2px; + border-radius: 2px; + font-weight: 500; +} + +/* Section visibility during search */ +.settings-section.search-match, +.setting-item.search-match { + display: block !important; +} + +.settings-section.search-hidden, +.setting-item.search-hidden { + display: none !important; +} + +/* Empty search results state */ +.settings-search-empty { + text-align: center; + padding: var(--space-4); + color: var(--text-color); + opacity: 0.7; +} + +.settings-search-empty i { + font-size: 2em; + margin-bottom: var(--space-2); + opacity: 0.5; +} + +.settings-search-empty p { + margin: 0; + font-size: 0.95em; } .settings-header h2 { @@ -461,13 +567,66 @@ padding-top: var(--space-2); } -.settings-section h3 { - font-size: 1.1em; +.settings-section-header { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + user-select: none; + padding: var(--space-1) 0; margin-bottom: var(--space-2); + transition: opacity 0.2s ease; +} + +.settings-section-header:hover { + opacity: 0.8; +} + +.settings-section-header h3 { + font-size: 1.1em; + margin: 0; color: var(--text-color); opacity: 0.9; } +.settings-section-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: none; + background: transparent; + color: var(--text-color); + cursor: pointer; + transition: transform 0.3s ease; + opacity: 0.6; +} + +.settings-section-toggle:hover { + opacity: 1; +} + +.settings-section-toggle .chevron { + transition: transform 0.3s ease; +} + +.settings-section.collapsed .settings-section-toggle .chevron { + transform: rotate(-90deg); +} + +.settings-section-content { + overflow: hidden; + transition: max-height 0.3s ease, opacity 0.3s ease; + max-height: 2000px; + opacity: 1; +} + +.settings-section.collapsed .settings-section-content { + max-height: 0; + opacity: 0; +} + .setting-item { display: flex; flex-direction: column; /* Changed to column for help text placement */ @@ -806,6 +965,17 @@ input:checked + .toggle-slider:before { flex-direction: column; } + .settings-header { + flex-direction: column; + align-items: flex-start; + gap: var(--space-2); + } + + .settings-header .settings-search-wrapper { + margin-left: 0; + width: 100%; + } + .settings-nav { width: 100%; max-height: 200px; diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index ad1e36b5..2ab3335d 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -365,6 +365,7 @@ export class SettingsManager { this.setupPriorityTagInputs(); this.initializeNavigation(); + this.initializeSearch(); this.initialized = true; } @@ -435,10 +436,266 @@ export class SettingsManager { } }); + // Initialize section collapse/expand + this.initializeSectionCollapse(); + // Initial update updateActiveNav(); } + initializeSectionCollapse() { + const sections = document.querySelectorAll('.settings-section, .setting-item[id^="section-"]'); + const STORAGE_KEY = 'settingsModal_collapsedSections'; + + // Load collapsed state from localStorage + let collapsedSections = {}; + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + collapsedSections = JSON.parse(stored); + } + } catch (e) { + console.warn('Failed to load collapsed sections state:', e); + } + + sections.forEach(section => { + const sectionId = section.getAttribute('data-section') || section.id; + const header = section.querySelector('.settings-section-header'); + const toggleBtn = section.querySelector('.settings-section-toggle'); + + if (!header || !toggleBtn) return; + + // Apply initial collapsed state + if (collapsedSections[sectionId]) { + section.classList.add('collapsed'); + } + + // Handle toggle click + const toggleSection = () => { + const isCollapsed = section.classList.toggle('collapsed'); + + // Save state to localStorage + collapsedSections[sectionId] = isCollapsed; + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(collapsedSections)); + } catch (e) { + console.warn('Failed to save collapsed sections state:', e); + } + }; + + // Click on header or toggle button + header.addEventListener('click', (e) => { + // Don't toggle if clicking on interactive elements within header + if (e.target.closest('a, button:not(.settings-section-toggle), input, select')) { + return; + } + toggleSection(); + }); + + toggleBtn.addEventListener('click', (e) => { + e.stopPropagation(); + toggleSection(); + }); + }); + } + + initializeSearch() { + const searchInput = document.getElementById('settingsSearchInput'); + const searchClear = document.getElementById('settingsSearchClear'); + + if (!searchInput) return; + + // Debounced search handler + let searchTimeout; + const debouncedSearch = (query) => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + this.performSearch(query); + }, 150); + }; + + // Handle input changes + searchInput.addEventListener('input', (e) => { + const query = e.target.value.trim(); + + // Show/hide clear button + if (searchClear) { + searchClear.style.display = query ? 'flex' : 'none'; + } + + debouncedSearch(query); + }); + + // Handle clear button click + if (searchClear) { + searchClear.addEventListener('click', () => { + searchInput.value = ''; + searchClear.style.display = 'none'; + searchInput.focus(); + this.performSearch(''); + }); + } + + // Handle Escape key to clear search + searchInput.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + if (searchInput.value) { + searchInput.value = ''; + if (searchClear) searchClear.style.display = 'none'; + this.performSearch(''); + } + } + }); + } + + performSearch(query) { + const sections = document.querySelectorAll('.settings-section, .setting-item[id^="section-"]'); + const settingsForm = document.querySelector('.settings-form'); + + // Remove existing empty state + const existingEmptyState = settingsForm?.querySelector('.settings-search-empty'); + if (existingEmptyState) { + existingEmptyState.remove(); + } + + if (!query) { + // Reset all sections to visible and remove highlights + sections.forEach(section => { + section.classList.remove('search-hidden', 'search-match'); + this.removeSearchHighlights(section); + }); + return; + } + + const lowerQuery = query.toLowerCase(); + let matchCount = 0; + + sections.forEach(section => { + const sectionText = this.getSectionSearchableText(section); + const hasMatch = sectionText.includes(lowerQuery); + + if (hasMatch) { + section.classList.remove('search-hidden'); + section.classList.add('search-match'); + this.highlightSearchMatches(section, lowerQuery); + matchCount++; + + // Expand section if it has matches + section.classList.remove('collapsed'); + } else { + section.classList.add('search-hidden'); + section.classList.remove('search-match'); + this.removeSearchHighlights(section); + } + }); + + // Show empty state if no matches found + if (matchCount === 0 && settingsForm) { + const emptyState = document.createElement('div'); + emptyState.className = 'settings-search-empty'; + emptyState.innerHTML = ` + +
${translate('settings.search.noResults', { query }, `No settings found matching "${query}"`)}
+ `; + settingsForm.appendChild(emptyState); + } + } + + getSectionSearchableText(section) { + // Get all text content from labels, help text, and headers + const labels = section.querySelectorAll('label'); + const helpTexts = section.querySelectorAll('.input-help'); + const headers = section.querySelectorAll('h3'); + + let text = ''; + + labels.forEach(el => text += ' ' + el.textContent); + helpTexts.forEach(el => text += ' ' + el.textContent); + headers.forEach(el => text += ' ' + el.textContent); + + return text.toLowerCase(); + } + + highlightSearchMatches(section, query) { + // Remove existing highlights first + this.removeSearchHighlights(section); + + if (!query) return; + + // Highlight in labels and help text + const textElements = section.querySelectorAll('label, .input-help, h3'); + + textElements.forEach(element => { + const walker = document.createTreeWalker( + element, + NodeFilter.SHOW_TEXT, + null, + false + ); + + const textNodes = []; + let node; + while (node = walker.nextNode()) { + if (node.textContent.toLowerCase().includes(query)) { + textNodes.push(node); + } + } + + textNodes.forEach(textNode => { + const parent = textNode.parentElement; + const text = textNode.textContent; + const lowerText = text.toLowerCase(); + + // Split text by query and wrap matches in highlight spans + const parts = []; + let lastIndex = 0; + let index; + + while ((index = lowerText.indexOf(query, lastIndex)) !== -1) { + // Add text before match + if (index > lastIndex) { + parts.push(document.createTextNode(text.substring(lastIndex, index))); + } + + // Add highlighted match + const highlight = document.createElement('span'); + highlight.className = 'settings-search-highlight'; + highlight.textContent = text.substring(index, index + query.length); + parts.push(highlight); + + lastIndex = index + query.length; + } + + // Add remaining text + if (lastIndex < text.length) { + parts.push(document.createTextNode(text.substring(lastIndex))); + } + + // Replace original text node with highlighted version + if (parts.length > 1) { + parts.forEach(part => parent.insertBefore(part, textNode)); + parent.removeChild(textNode); + } + }); + }); + } + + removeSearchHighlights(section) { + const highlights = section.querySelectorAll('.settings-search-highlight'); + + highlights.forEach(highlight => { + const parent = highlight.parentElement; + if (parent) { + // Replace highlight with its text content + parent.insertBefore(document.createTextNode(highlight.textContent), highlight); + parent.removeChild(highlight); + + // Normalize to merge adjacent text nodes + parent.normalize(); + } + }); + } + async openSettingsFileLocation() { try { const response = await fetch('/api/lm/settings/open-location', { diff --git a/templates/components/modals/settings_modal.html b/templates/components/modals/settings_modal.html index 78f4b726..b709a4f8 100644 --- a/templates/components/modals/settings_modal.html +++ b/templates/components/modals/settings_modal.html @@ -11,6 +11,22 @@ title="{{ t('settings.openSettingsFileLocation.tooltip') }}"> +