| ... | @@ -8,6 +8,7 @@ import { | ... | @@ -8,6 +8,7 @@ import { |
| 8 | useState, | 8 | useState, |
| 9 | type ComponentType, | 9 | type ComponentType, |
| 10 | type KeyboardEvent, | 10 | type KeyboardEvent, |
| | 11 | type ReactNode, |
| 11 | } from "react"; | 12 | } from "react"; |
| 12 | import { | 13 | import { |
| 13 | ArchiveIcon, | 14 | ArchiveIcon, |
| ... | @@ -24,6 +25,7 @@ import { | ... | @@ -24,6 +25,7 @@ import { |
| 24 | import { useLocation, useNavigate } from "@tanstack/react-router"; | 25 | import { useLocation, useNavigate } from "@tanstack/react-router"; |
| 25 | | 26 | |
| 26 | import { Button } from "../ui/button"; | 27 | import { Button } from "../ui/button"; |
| | 28 | import { Collapsible, CollapsiblePanel } from "../ui/collapsible"; |
| 27 | import { Input } from "../ui/input"; | 29 | import { Input } from "../ui/input"; |
| 28 | import { Kbd } from "../ui/kbd"; | 30 | import { Kbd } from "../ui/kbd"; |
| 29 | import { | 31 | import { |
| ... | @@ -114,6 +116,22 @@ function SettingsSectionIcon({ to }: { to: SettingsPath }) { | ... | @@ -114,6 +116,22 @@ function SettingsSectionIcon({ to }: { to: SettingsPath }) { |
| 114 | return <Icon className="mt-0.5 size-3.5 shrink-0 text-sidebar-muted-foreground/60" />; | 116 | return <Icon className="mt-0.5 size-3.5 shrink-0 text-sidebar-muted-foreground/60" />; |
| 115 | } | 117 | } |
| 116 | | 118 | |
| | 119 | function SettingsSubmenuCollapse({ |
| | 120 | open, |
| | 121 | children, |
| | 122 | }: { |
| | 123 | readonly open: boolean; |
| | 124 | readonly children: ReactNode; |
| | 125 | }) { |
| | 126 | return ( |
| | 127 | <Collapsible open={open}> |
| | 128 | <CollapsiblePanel className="duration-150 ease-out motion-reduce:transition-none"> |
| | 129 | {children} |
| | 130 | </CollapsiblePanel> |
| | 131 | </Collapsible> |
| | 132 | ); |
| | 133 | } |
| | 134 | |
| 117 | export function SettingsSidebarNav({ pathname }: { pathname: string }) { | 135 | export function SettingsSidebarNav({ pathname }: { pathname: string }) { |
| 118 | const navigate = useNavigate(); | 136 | const navigate = useNavigate(); |
| 119 | const currentHash = useLocation({ select: (location) => location.hash }); | 137 | const currentHash = useLocation({ select: (location) => location.hash }); |
| ... | @@ -125,6 +143,9 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { | ... | @@ -125,6 +143,9 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { |
| 125 | const results = useMemo(() => searchSettings(query, searchableItems), [query, searchableItems]); | 143 | const results = useMemo(() => searchSettings(query, searchableItems), [query, searchableItems]); |
| 126 | const isSearching = query.trim().length > 0; | 144 | const isSearching = query.trim().length > 0; |
| 127 | const hasResults = results.length > 0; | 145 | const hasResults = results.length > 0; |
| | 146 | const activeSettingsPath = SETTINGS_NAV_ITEMS.find( |
| | 147 | (item) => pathname === item.to || pathname.startsWith(`${item.to}/`), |
| | 148 | )?.to; |
| 128 | | 149 | |
| 129 | useEffect(() => { | 150 | useEffect(() => { |
| 130 | setActiveResultIndex((index) => Math.min(index, Math.max(results.length - 1, 0))); | 151 | setActiveResultIndex((index) => Math.min(index, Math.max(results.length - 1, 0))); |
| ... | @@ -347,8 +368,8 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { | ... | @@ -347,8 +368,8 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { |
| 347 | <SidebarMenu className="ps-px"> | 368 | <SidebarMenu className="ps-px"> |
| 348 | {SETTINGS_NAV_ITEMS.map((item) => { | 369 | {SETTINGS_NAV_ITEMS.map((item) => { |
| 349 | const Icon = item.icon; | 370 | const Icon = item.icon; |
| 350 | const isActive = pathname === item.to || pathname.startsWith(`${item.to}/`); | | |
| 351 | const pageSections = SETTINGS_PAGE_SECTIONS[item.to]; | 371 | const pageSections = SETTINGS_PAGE_SECTIONS[item.to]; |
| | 372 | const isActive = activeSettingsPath === item.to; |
| 352 | return ( | 373 | return ( |
| 353 | <SidebarMenuItem key={item.to}> | 374 | <SidebarMenuItem key={item.to}> |
| 354 | <SidebarMenuButton | 375 | <SidebarMenuButton |
| ... | @@ -358,21 +379,23 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { | ... | @@ -358,21 +379,23 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { |
| 358 | <Icon /> | 379 | <Icon /> |
| 359 | <span className="truncate">{item.label}</span> | 380 | <span className="truncate">{item.label}</span> |
| 360 | </SidebarMenuButton> | 381 | </SidebarMenuButton> |
| 361 | {isActive && pageSections ? ( | 382 | {pageSections ? ( |
| 362 | <SidebarMenuSub className="border-l-0"> | 383 | <SettingsSubmenuCollapse open={isActive}> |
| 363 | {pageSections.map((section) => ( | 384 | <SidebarMenuSub className="border-l-0"> |
| 364 | <SidebarMenuSubItem key={section.targetId}> | 385 | {pageSections.map((section) => ( |
| 365 | <SidebarMenuSubButton | 386 | <SidebarMenuSubItem key={section.targetId}> |
| 366 | render={<button type="button" />} | 387 | <SidebarMenuSubButton |
| 367 | size="sm" | 388 | render={<button type="button" />} |
| 368 | className="w-full text-sidebar-muted-foreground/65" | 389 | size="sm" |
| 369 | onClick={() => handlePageSectionClick(item.to, section.targetId)} | 390 | className="w-full text-sidebar-muted-foreground/65" |
| 370 | > | 391 | onClick={() => handlePageSectionClick(item.to, section.targetId)} |
| 371 | <span className="ms-0.5">{section.label}</span> | 392 | > |
| 372 | </SidebarMenuSubButton> | 393 | <span className="ms-0.5">{section.label}</span> |
| 373 | </SidebarMenuSubItem> | 394 | </SidebarMenuSubButton> |
| 374 | ))} | 395 | </SidebarMenuSubItem> |
| 375 | </SidebarMenuSub> | 396 | ))} |
| | 397 | </SidebarMenuSub> |
| | 398 | </SettingsSubmenuCollapse> |
| 376 | ) : null} | 399 | ) : null} |
| 377 | </SidebarMenuItem> | 400 | </SidebarMenuItem> |
| 378 | ); | 401 | ); |