feat: Enhance CategoryTabs and Marketplace components for better responsiveness and navigation

This commit is contained in:
Marco Beretta 2025-08-04 18:50:54 +02:00 committed by Danny Avila
parent 55099d96ac
commit f53ba891c9
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
3 changed files with 38 additions and 26 deletions

View file

@ -24,6 +24,7 @@ interface CategoryTabsProps {
* Renders a tabbed navigation interface showing agent categories. * Renders a tabbed navigation interface showing agent categories.
* Includes loading states, empty state handling, and displays counts for each category. * Includes loading states, empty state handling, and displays counts for each category.
* Uses database-driven category labels with no hardcoded values. * Uses database-driven category labels with no hardcoded values.
* Features multi-row wrapping for better responsive behavior.
*/ */
const CategoryTabs: React.FC<CategoryTabsProps> = ({ const CategoryTabs: React.FC<CategoryTabsProps> = ({
categories, categories,
@ -46,14 +47,13 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
return category.label || category.value.charAt(0).toUpperCase() + category.value.slice(1); return category.label || category.value.charAt(0).toUpperCase() + category.value.slice(1);
}; };
// Loading skeleton component
const loadingSkeleton = ( const loadingSkeleton = (
<div className="w-full pb-2"> <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) => ( {[...Array(6)].map((_, i) => (
<div <div
key={i} 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> </div>
@ -67,15 +67,23 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
switch (e.key) { switch (e.key) {
case 'ArrowLeft': case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault(); e.preventDefault();
newIndex = currentIndex > 0 ? currentIndex - 1 : categories.length - 1; newIndex = currentIndex > 0 ? currentIndex - 1 : categories.length - 1;
break; break;
case 'ArrowRight': case 'ArrowRight':
case 'ArrowDown':
e.preventDefault(); e.preventDefault();
newIndex = currentIndex < categories.length - 1 ? currentIndex + 1 : 0; newIndex = currentIndex < categories.length - 1 ? currentIndex + 1 : 0;
break; 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': case 'Home':
e.preventDefault(); e.preventDefault();
newIndex = 0; newIndex = 0;
@ -94,7 +102,9 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
// Focus the new tab // Focus the new tab
setTimeout(() => { setTimeout(() => {
const newTab = document.getElementById(`category-tab-${newCategory.value}`); const newTab = document.getElementById(`category-tab-${newCategory.value}`);
newTab?.focus(); if (newTab) {
newTab.focus();
}
}, 0); }, 0);
} }
}; };
@ -108,16 +118,12 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
// Main tabs content // Main tabs content
const tabsContent = ( const tabsContent = (
<div className="relative w-full pb-2"> <div className="w-full pb-2">
<div <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" role="tablist"
aria-label={localize('com_agents_category_tabs_label')} aria-label={localize('com_agents_category_tabs_label')}
aria-orientation="horizontal" aria-orientation="horizontal"
style={{
scrollSnapType: 'x mandatory',
WebkitOverflowScrolling: 'touch',
}}
> >
{categories.map((category, index) => ( {categories.map((category, index) => (
<button <button
@ -126,14 +132,11 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
onClick={() => onChange(category.value)} onClick={() => onChange(category.value)}
onKeyDown={(e) => handleKeyDown(e, category.value)} onKeyDown={(e) => handleKeyDown(e, category.value)}
className={cn( 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 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', : 'bg-surface-secondary text-text-secondary hover:bg-surface-hover hover:text-text-primary',
)} )}
style={{
scrollSnapAlign: 'start',
}}
role="tab" role="tab"
aria-selected={activeTab === category.value} aria-selected={activeTab === category.value}
aria-controls={`tabpanel-${category.value}`} aria-controls={`tabpanel-${category.value}`}

View file

@ -43,8 +43,8 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
const { navVisible, setNavVisible } = useOutletContext<ContextType>(); const { navVisible, setNavVisible } = useOutletContext<ContextType>();
const [hideSidePanel, setHideSidePanel] = useRecoilState(store.hideSidePanel); const [hideSidePanel, setHideSidePanel] = useRecoilState(store.hideSidePanel);
// Get URL parameters (default to 'promoted' instead of 'all') // Get URL parameters (default to 'all' to ensure users see agents)
const activeTab = category || 'promoted'; const activeTab = category || 'all';
const searchQuery = searchParams.get('q') || ''; const searchQuery = searchParams.get('q') || '';
const selectedAgentId = searchParams.get('agent_id') || ''; const selectedAgentId = searchParams.get('agent_id') || '';
@ -64,6 +64,15 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
localStorage.setItem('fullPanelCollapse', 'false'); localStorage.setItem('fullPanelCollapse', 'false');
}, [setHideSidePanel, hideSidePanel]); }, [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) // Ensure endpoints config is loaded first (required for agent queries)
useGetEndpointsQuery(); useGetEndpointsQuery();

View file

@ -73,33 +73,33 @@ const SearchBar: React.FC<SearchBarProps> = ({ value, onSearch, className = '' }
value={searchTerm} value={searchTerm}
onChange={handleChange} onChange={handleChange}
placeholder={localize('com_agents_search_placeholder')} 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-label={localize('com_agents_search_aria')}
aria-describedby="search-instructions search-results-count" aria-describedby="search-instructions search-results-count"
autoComplete="off" autoComplete="off"
spellCheck="false" spellCheck="false"
/> />
{/* Search icon with proper accessibility */}
<div className="absolute inset-y-0 left-0 flex items-center pl-4" aria-hidden="true"> <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> </div>
{/* Hidden instructions for screen readers */} {/* Hidden instructions for screen readers */}
<div id="search-instructions" className="sr-only"> <div id="search-instructions" className="sr-only">
{localize('com_agents_search_instructions')} {localize('com_agents_search_instructions')}
</div> </div>
{/* Show clear button only when search has value - Google style */} {/* Show clear button only when search has value - Google style */}
{searchTerm && ( {searchTerm && (
<button <button
type="button" type="button"
onClick={handleClear} 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')} aria-label={localize('com_agents_clear_search')}
title={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> </button>
)} )}
</div> </div>