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
parent 2d854a1668
commit d41d8d6566
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
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.
* 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}`}

View file

@ -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();

View file

@ -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>