mirror of
https://github.com/open-webui/open-webui
synced 2025-06-22 18:07:17 +00:00
feat: implement Step 3 - Enhanced Navigation Experience
🚀 **Enhanced Navigation Features:** ✅ Auto-navigate to first result when search finds matches ✅ Visual feedback during navigation (subtle pulse animation) ✅ Improved keyboard shortcuts (Cmd+↑/↓ as alternatives) ✅ Better edge case handling (no navigation when no results) ✅ Enhanced button states with proper disabled styling ✅ Temporary message highlighting (light blue background fade) 🎨 **UX Improvements:** ✅ Result counter now uses blue accent color when active ✅ Navigation buttons show visual feedback during use ✅ Enhanced tooltips with multiple shortcut options ✅ Contextual help text (shows shortcuts only when relevant) ✅ Smooth scroll with better positioning (block: center, inline: nearest) 🔧 **Technical Enhancements:** ✅ Cleaner navigation logic with dedicated navigateToResult function ✅ Proper bounds checking and early returns ✅ Visual feedback timing (300ms pulse, 1000ms message highlight) ✅ Enhanced scroll behavior matching modern UX patterns **Key Navigation Improvements:** - First result auto-selected when search finds matches - Message gets temporary blue highlight when navigated to - Search overlay pulses briefly during navigation - Multiple keyboard shortcuts for power users - Contextual help shows relevant shortcuts only **Navigation now feels smooth, responsive, and provides clear visual feedback to help users understand their current position in results.**
This commit is contained in:
parent
4f8126d429
commit
f50514046d
@ -18,6 +18,7 @@
|
|||||||
let searchQuery = '';
|
let searchQuery = '';
|
||||||
let matchingMessageIds: string[] = [];
|
let matchingMessageIds: string[] = [];
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
|
let isNavigating = false; // Visual feedback for navigation
|
||||||
|
|
||||||
// Computed values
|
// Computed values
|
||||||
$: totalResults = matchingMessageIds.length;
|
$: totalResults = matchingMessageIds.length;
|
||||||
@ -33,11 +34,21 @@
|
|||||||
closeSearch();
|
closeSearch();
|
||||||
} else if (e.key === 'Enter') {
|
} else if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (totalResults === 0) return; // No results to navigate
|
||||||
|
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
navigateToPrevious();
|
navigateToPrevious();
|
||||||
} else {
|
} else {
|
||||||
navigateToNext();
|
navigateToNext();
|
||||||
}
|
}
|
||||||
|
} else if (e.key === 'ArrowUp' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
// Cmd/Ctrl + Arrow Up for previous (alternative shortcut)
|
||||||
|
e.preventDefault();
|
||||||
|
if (totalResults > 0) navigateToPrevious();
|
||||||
|
} else if (e.key === 'ArrowDown' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
// Cmd/Ctrl + Arrow Down for next (alternative shortcut)
|
||||||
|
e.preventDefault();
|
||||||
|
if (totalResults > 0) navigateToNext();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -45,6 +56,7 @@
|
|||||||
searchQuery = '';
|
searchQuery = '';
|
||||||
matchingMessageIds = [];
|
matchingMessageIds = [];
|
||||||
currentIndex = 0;
|
currentIndex = 0;
|
||||||
|
isNavigating = false;
|
||||||
dispatch('close');
|
dispatch('close');
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -69,6 +81,11 @@
|
|||||||
|
|
||||||
matchingMessageIds = messageIds;
|
matchingMessageIds = messageIds;
|
||||||
currentIndex = messageIds.length > 0 ? 0 : 0;
|
currentIndex = messageIds.length > 0 ? 0 : 0;
|
||||||
|
|
||||||
|
// Auto-navigate to first result when search finds matches
|
||||||
|
if (messageIds.length > 0) {
|
||||||
|
scrollToCurrentResult();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInput = () => {
|
const handleInput = () => {
|
||||||
@ -76,17 +93,30 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const navigateToNext = () => {
|
const navigateToNext = () => {
|
||||||
if (totalResults > 0) {
|
if (totalResults === 0) return;
|
||||||
currentIndex = (currentIndex + 1) % totalResults;
|
|
||||||
scrollToCurrentResult();
|
const nextIndex = (currentIndex + 1) % totalResults;
|
||||||
}
|
navigateToResult(nextIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigateToPrevious = () => {
|
const navigateToPrevious = () => {
|
||||||
if (totalResults > 0) {
|
if (totalResults === 0) return;
|
||||||
currentIndex = currentIndex === 0 ? totalResults - 1 : currentIndex - 1;
|
|
||||||
scrollToCurrentResult();
|
const prevIndex = currentIndex === 0 ? totalResults - 1 : currentIndex - 1;
|
||||||
}
|
navigateToResult(prevIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToResult = (newIndex: number) => {
|
||||||
|
if (newIndex < 0 || newIndex >= matchingMessageIds.length) return;
|
||||||
|
|
||||||
|
currentIndex = newIndex;
|
||||||
|
scrollToCurrentResult();
|
||||||
|
|
||||||
|
// Visual feedback for navigation
|
||||||
|
isNavigating = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
isNavigating = false;
|
||||||
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollToCurrentResult = () => {
|
const scrollToCurrentResult = () => {
|
||||||
@ -94,8 +124,20 @@
|
|||||||
const messageId = matchingMessageIds[currentIndex];
|
const messageId = matchingMessageIds[currentIndex];
|
||||||
const messageElement = document.getElementById(`message-${messageId}`);
|
const messageElement = document.getElementById(`message-${messageId}`);
|
||||||
if (messageElement) {
|
if (messageElement) {
|
||||||
// Use same scroll pattern as Chat.svelte
|
// Enhanced scroll with better positioning
|
||||||
messageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
messageElement.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center',
|
||||||
|
inline: 'nearest'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add subtle visual feedback to the message
|
||||||
|
messageElement.style.transition = 'background-color 0.3s ease';
|
||||||
|
messageElement.style.backgroundColor = 'rgba(59, 130, 246, 0.1)'; // Light blue highlight
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
messageElement.style.backgroundColor = '';
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -120,6 +162,7 @@
|
|||||||
<div
|
<div
|
||||||
bind:this={searchContainer}
|
bind:this={searchContainer}
|
||||||
class="fixed top-4 right-4 z-50 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-3 min-w-80"
|
class="fixed top-4 right-4 z-50 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-3 min-w-80"
|
||||||
|
class:animate-pulse={isNavigating}
|
||||||
transition:fly={{ y: -20, duration: 200 }}
|
transition:fly={{ y: -20, duration: 200 }}
|
||||||
on:keydown={handleKeydown}
|
on:keydown={handleKeydown}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
@ -137,9 +180,9 @@
|
|||||||
class="flex-1 bg-transparent border-none outline-none text-sm text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400"
|
class="flex-1 bg-transparent border-none outline-none text-sm text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Results Counter -->
|
<!-- Results Counter with enhanced styling -->
|
||||||
{#if totalResults > 0}
|
{#if totalResults > 0}
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
<div class="text-xs font-medium text-blue-600 dark:text-blue-400 whitespace-nowrap">
|
||||||
{currentResult} of {totalResults}
|
{currentResult} of {totalResults}
|
||||||
</div>
|
</div>
|
||||||
{:else if searchQuery.trim()}
|
{:else if searchQuery.trim()}
|
||||||
@ -148,12 +191,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Navigation Buttons -->
|
<!-- Navigation Buttons with enhanced states -->
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors disabled:opacity-50"
|
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
|
class:bg-blue-50={isNavigating}
|
||||||
disabled={totalResults === 0}
|
disabled={totalResults === 0}
|
||||||
title="Previous (Shift+Enter)"
|
title="Previous (Shift+Enter or Cmd+↑)"
|
||||||
aria-label="Previous result"
|
aria-label="Previous result"
|
||||||
on:click={navigateToPrevious}
|
on:click={navigateToPrevious}
|
||||||
>
|
>
|
||||||
@ -161,9 +205,10 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors disabled:opacity-50"
|
class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
|
class:bg-blue-50={isNavigating}
|
||||||
disabled={totalResults === 0}
|
disabled={totalResults === 0}
|
||||||
title="Next (Enter)"
|
title="Next (Enter or Cmd+↓)"
|
||||||
aria-label="Next result"
|
aria-label="Next result"
|
||||||
on:click={navigateToNext}
|
on:click={navigateToNext}
|
||||||
>
|
>
|
||||||
@ -181,10 +226,16 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search Tips -->
|
<!-- Enhanced Search Tips -->
|
||||||
{#if searchQuery === ''}
|
{#if searchQuery === ''}
|
||||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||||
Press <kbd class="px-1 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-xs">Enter</kbd> to navigate results
|
<kbd class="px-1 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-xs">Enter</kbd> next •
|
||||||
|
<kbd class="px-1 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-xs">Shift+Enter</kbd> previous
|
||||||
|
</div>
|
||||||
|
{:else if totalResults > 1}
|
||||||
|
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
Navigate with <kbd class="px-1 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-xs">Enter</kbd> /
|
||||||
|
<kbd class="px-1 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-xs">Shift+Enter</kbd>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user