feat: enhance settings navigation with collapsible sub-items and local storage state

- Implemented a collapsible dropdown for navigation items in the settings view, allowing users to expand or collapse sub-items.
- Added local storage functionality to remember the open/closed state of the dropdown across sessions.
- Updated the styling and interaction logic for improved user experience and accessibility.

These changes improve the organization of navigation items and enhance user interaction within the settings view.
This commit is contained in:
webdevcody
2026-01-09 16:40:07 -05:00
parent 7ea64b32f3
commit 89a0877bcc
2 changed files with 43 additions and 10 deletions

View File

@@ -1,9 +1,13 @@
import { useState, useEffect } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { Project } from '@/lib/electron'; import type { Project } from '@/lib/electron';
import type { NavigationItem } from '../config/navigation'; import type { NavigationItem } from '../config/navigation';
import { GLOBAL_NAV_ITEMS, PROJECT_NAV_ITEMS } from '../config/navigation'; import { GLOBAL_NAV_ITEMS, PROJECT_NAV_ITEMS } from '../config/navigation';
import type { SettingsViewId } from '../hooks/use-settings-view'; import type { SettingsViewId } from '../hooks/use-settings-view';
const PROVIDERS_DROPDOWN_KEY = 'settings-providers-dropdown-open';
interface SettingsNavigationProps { interface SettingsNavigationProps {
navItems: NavigationItem[]; navItems: NavigationItem[];
activeSection: SettingsViewId; activeSection: SettingsViewId;
@@ -66,29 +70,52 @@ function NavItemWithSubItems({
activeSection: SettingsViewId; activeSection: SettingsViewId;
onNavigate: (sectionId: SettingsViewId) => void; onNavigate: (sectionId: SettingsViewId) => void;
}) { }) {
const [isOpen, setIsOpen] = useState(() => {
if (typeof window !== 'undefined') {
const stored = localStorage.getItem(PROVIDERS_DROPDOWN_KEY);
return stored === null ? true : stored === 'true';
}
return true;
});
useEffect(() => {
localStorage.setItem(PROVIDERS_DROPDOWN_KEY, String(isOpen));
}, [isOpen]);
const hasActiveSubItem = item.subItems?.some((subItem) => subItem.id === activeSection) ?? false; const hasActiveSubItem = item.subItems?.some((subItem) => subItem.id === activeSection) ?? false;
const isParentActive = item.id === activeSection; const isParentActive = item.id === activeSection;
const Icon = item.icon; const Icon = item.icon;
const ChevronIcon = isOpen ? ChevronDown : ChevronRight;
return ( return (
<div> <div>
{/* Parent item - non-clickable label */} {/* Parent item - clickable to toggle dropdown */}
<div <button
onClick={() => setIsOpen(!isOpen)}
className={cn( className={cn(
'w-full flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium text-muted-foreground', 'group w-full flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200 ease-out text-left',
isParentActive || (hasActiveSubItem && 'text-foreground') 'text-muted-foreground hover:text-foreground',
'hover:bg-accent/50',
'border border-transparent hover:border-border/40',
(isParentActive || hasActiveSubItem) && 'text-foreground'
)} )}
> >
<Icon <Icon
className={cn( className={cn(
'w-4 h-4 shrink-0 transition-all duration-200', 'w-4 h-4 shrink-0 transition-all duration-200',
isParentActive || hasActiveSubItem ? 'text-brand-500' : 'text-muted-foreground' isParentActive || hasActiveSubItem ? 'text-brand-500' : 'group-hover:text-brand-400'
)} )}
/> />
<span className="truncate">{item.label}</span> <span className="truncate flex-1">{item.label}</span>
</div> <ChevronIcon
{/* Sub-items - always displayed */} className={cn(
{item.subItems && ( 'w-4 h-4 shrink-0 transition-transform duration-200',
'text-muted-foreground/60 group-hover:text-muted-foreground'
)}
/>
</button>
{/* Sub-items - conditionally displayed */}
{item.subItems && isOpen && (
<div className="ml-4 mt-1 space-y-1"> <div className="ml-4 mt-1 space-y-1">
{item.subItems.map((subItem) => { {item.subItems.map((subItem) => {
const SubIcon = subItem.icon; const SubIcon = subItem.icon;

View File

@@ -876,7 +876,13 @@ export function PhaseModelSelector({
className="w-[320px] p-0" className="w-[320px] p-0"
align={align} align={align}
onWheel={(e) => e.stopPropagation()} onWheel={(e) => e.stopPropagation()}
onPointerDownOutside={(e) => e.preventDefault()} onPointerDownOutside={(e) => {
// Only prevent close if clicking inside a nested popover (thinking level panel)
const target = e.target as HTMLElement;
if (target.closest('[data-slot="popover-content"]')) {
e.preventDefault();
}
}}
> >
<Command> <Command>
<CommandInput placeholder="Search models..." /> <CommandInput placeholder="Search models..." />