mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-28 22:28:51 +01:00
feat: Enhance CategoryTabs and Marketplace components for better responsiveness and navigation
This commit is contained in:
parent
2d854a1668
commit
d41d8d6566
3 changed files with 38 additions and 26 deletions
|
|
@ -24,6 +24,7 @@ interface CategoryTabsProps {
|
|||
* Renders a tabbed navigation interface showing agent categories.
|
||||
* Includes loading states, empty state handling, and displays counts for each category.
|
||||
* Uses database-driven category labels with no hardcoded values.
|
||||
* Features multi-row wrapping for better responsive behavior.
|
||||
*/
|
||||
const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
||||
categories,
|
||||
|
|
@ -46,14 +47,13 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
|||
return category.label || category.value.charAt(0).toUpperCase() + category.value.slice(1);
|
||||
};
|
||||
|
||||
// Loading skeleton component
|
||||
const loadingSkeleton = (
|
||||
<div className="w-full pb-2">
|
||||
<div className="no-scrollbar flex gap-1.5 overflow-x-auto px-4">
|
||||
<div className="flex flex-wrap justify-center gap-1.5 px-4">
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-[36px] min-w-[80px] animate-pulse rounded-md bg-surface-tertiary"
|
||||
className="h-[36px] min-w-[80px] animate-pulse rounded-lg bg-surface-tertiary"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -67,15 +67,23 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
|||
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
newIndex = currentIndex > 0 ? currentIndex - 1 : categories.length - 1;
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
newIndex = currentIndex < categories.length - 1 ? currentIndex + 1 : 0;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
// Move up a row (approximate by moving back ~4-6 items)
|
||||
newIndex = Math.max(0, currentIndex - 5);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
// Move down a row (approximate by moving forward ~4-6 items)
|
||||
newIndex = Math.min(categories.length - 1, currentIndex + 5);
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
newIndex = 0;
|
||||
|
|
@ -94,7 +102,9 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
|||
// Focus the new tab
|
||||
setTimeout(() => {
|
||||
const newTab = document.getElementById(`category-tab-${newCategory.value}`);
|
||||
newTab?.focus();
|
||||
if (newTab) {
|
||||
newTab.focus();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
|
@ -108,16 +118,12 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
|||
|
||||
// Main tabs content
|
||||
const tabsContent = (
|
||||
<div className="relative w-full pb-2">
|
||||
<div className="w-full pb-2">
|
||||
<div
|
||||
className="no-scrollbar flex gap-1.5 overflow-x-auto overscroll-x-contain px-4 [-webkit-overflow-scrolling:touch] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
className="flex flex-wrap justify-center gap-1.5 px-4"
|
||||
role="tablist"
|
||||
aria-label={localize('com_agents_category_tabs_label')}
|
||||
aria-orientation="horizontal"
|
||||
style={{
|
||||
scrollSnapType: 'x mandatory',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
}}
|
||||
>
|
||||
{categories.map((category, index) => (
|
||||
<button
|
||||
|
|
@ -126,14 +132,11 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
|||
onClick={() => onChange(category.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e, category.value)}
|
||||
className={cn(
|
||||
'relative mt-1 cursor-pointer select-none whitespace-nowrap rounded-md px-3 py-2',
|
||||
'relative cursor-pointer select-none whitespace-nowrap rounded-lg px-3 py-2 transition-colors',
|
||||
activeTab === category.value
|
||||
? 'bg-surface-tertiary text-text-primary'
|
||||
? 'bg-surface-hover text-text-primary'
|
||||
: 'bg-surface-secondary text-text-secondary hover:bg-surface-hover hover:text-text-primary',
|
||||
)}
|
||||
style={{
|
||||
scrollSnapAlign: 'start',
|
||||
}}
|
||||
role="tab"
|
||||
aria-selected={activeTab === category.value}
|
||||
aria-controls={`tabpanel-${category.value}`}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
|
|||
const { navVisible, setNavVisible } = useOutletContext<ContextType>();
|
||||
const [hideSidePanel, setHideSidePanel] = useRecoilState(store.hideSidePanel);
|
||||
|
||||
// Get URL parameters (default to 'promoted' instead of 'all')
|
||||
const activeTab = category || 'promoted';
|
||||
// Get URL parameters (default to 'all' to ensure users see agents)
|
||||
const activeTab = category || 'all';
|
||||
const searchQuery = searchParams.get('q') || '';
|
||||
const selectedAgentId = searchParams.get('agent_id') || '';
|
||||
|
||||
|
|
@ -64,6 +64,15 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
|
|||
localStorage.setItem('fullPanelCollapse', 'false');
|
||||
}, [setHideSidePanel, hideSidePanel]);
|
||||
|
||||
// Redirect base /agents route to /agents/all for consistency
|
||||
useEffect(() => {
|
||||
if (!category && window.location.pathname === '/agents') {
|
||||
const currentSearchParams = searchParams.toString();
|
||||
const searchParamsStr = currentSearchParams ? `?${currentSearchParams}` : '';
|
||||
navigate(`/agents/all${searchParamsStr}`, { replace: true });
|
||||
}
|
||||
}, [category, navigate, searchParams]);
|
||||
|
||||
// Ensure endpoints config is loaded first (required for agent queries)
|
||||
useGetEndpointsQuery();
|
||||
|
||||
|
|
|
|||
|
|
@ -73,33 +73,33 @@ const SearchBar: React.FC<SearchBarProps> = ({ value, onSearch, className = '' }
|
|||
value={searchTerm}
|
||||
onChange={handleChange}
|
||||
placeholder={localize('com_agents_search_placeholder')}
|
||||
className="h-14 rounded-2xl border-2 border-border-medium bg-transparent pl-12 pr-12 text-lg text-text-primary shadow-lg placeholder:text-text-tertiary focus:border-border-heavy focus:ring-0"
|
||||
className="h-14 rounded-2xl border-border-medium bg-transparent pl-12 pr-12 text-lg text-text-primary shadow-md transition-[border-color,box-shadow] duration-200 placeholder:text-text-tertiary focus:border-border-heavy focus:shadow-lg focus:ring-0"
|
||||
aria-label={localize('com_agents_search_aria')}
|
||||
aria-describedby="search-instructions search-results-count"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
|
||||
{/* Search icon with proper accessibility */}
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-4" aria-hidden="true">
|
||||
<Search className="h-6 w-6 text-text-tertiary" />
|
||||
<Search className="size-5 text-text-secondary" />
|
||||
</div>
|
||||
|
||||
{/* Hidden instructions for screen readers */}
|
||||
<div id="search-instructions" className="sr-only">
|
||||
{localize('com_agents_search_instructions')}
|
||||
</div>
|
||||
|
||||
{/* Show clear button only when search has value - Google style */}
|
||||
{searchTerm && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className="group absolute right-3 top-1/2 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full bg-text-tertiary transition-colors duration-150 hover:bg-text-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
className="group absolute right-4 top-1/2 flex size-5 -translate-y-1/2 items-center justify-center rounded-full transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
aria-label={localize('com_agents_clear_search')}
|
||||
title={localize('com_agents_clear_search')}
|
||||
>
|
||||
<X className="h-3 w-3 text-white group-hover:text-white" strokeWidth={2.5} />
|
||||
<X
|
||||
className="size-5 text-text-secondary transition-colors duration-200 group-hover:text-text-primary"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue