import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState, type ComponentType, type KeyboardEvent, } from "react"; import { ArchiveIcon, BlocksIcon, BotIcon, createLucideIcon, GitBranchIcon, PanelsTopLeftIcon, KeyboardIcon, Link2Icon, PaletteIcon, SearchIcon, Settings2Icon, XIcon, } from "lucide-react"; import { useLocation, useNavigate } from "@tanstack/react-router"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Kbd } from "../ui/kbd"; import { SidebarContent, SidebarFooter, SidebarGroup, SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar, } from "../ui/sidebar"; import { SidebarUtilityMenu } from "../sidebar/SidebarChrome"; import { scrollToSettingsTarget } from "./settingsLayout"; import { searchSettings, SETTINGS_SECTION_LABELS, type SettingsPath, type SettingsSearchItem, } from "./settingsSearch"; import { useAvailableSettingsSearchItems } from "./useAvailableSettingsSearchItems"; const SnapShotIcon = createLucideIcon("snap-shot", [ [ "path", { d: "M8 3H6a3 3 0 0 0-3 3v2M16 3h2a3 3 0 0 1 3 3v2M21 16v2a3 3 0 0 1-3 3h-2M8 21H6a3 3 0 0 1-3-3v-2", key: "capture-frame", }, ], ["rect", { width: "10", height: "8", x: "7", y: "8", rx: "2", key: "window" }], ["circle", { cx: "12", cy: "12", r: "1.5", key: "lens" }], ]); const T3ConnectSidebarSignIn = lazy(() => import("../clerk/T3ConnectSidebarSignIn").then((module) => ({ default: module.T3ConnectSidebarSignIn, })), ); const T3ConnectSidebarAvatar = lazy(() => import("../clerk/T3ConnectSidebarSignIn").then((module) => ({ default: module.T3ConnectSidebarAvatar, })), ); const SETTINGS_SECTION_ICONS: Readonly< Record> > = { "/settings/general": Settings2Icon, "/settings/appearance": PaletteIcon, "/settings/projects": PanelsTopLeftIcon, "/settings/keybindings": KeyboardIcon, "/settings/snap-shot": SnapShotIcon, "/settings/providers": BotIcon, "/settings/integrations": BlocksIcon, "/settings/source-control": GitBranchIcon, "/settings/connections": Link2Icon, "/settings/archived": ArchiveIcon, }; const SETTINGS_NAV_ITEMS: ReadonlyArray<{ label: string; to: SettingsPath; icon: ComponentType<{ className?: string }>; }> = (Object.keys(SETTINGS_SECTION_LABELS) as SettingsPath[]).map((to) => ({ to, label: SETTINGS_SECTION_LABELS[to], icon: SETTINGS_SECTION_ICONS[to], })); function SettingsSectionIcon({ to }: { to: SettingsPath }) { const Icon = SETTINGS_SECTION_ICONS[to]; return ; } export function SettingsSidebarNav({ pathname }: { pathname: string }) { const navigate = useNavigate(); const currentHash = useLocation({ select: (location) => location.hash }); const { isMobile, setOpenMobile, open, setOpen } = useSidebar(); const searchInputRef = useRef(null); const [query, setQuery] = useState(""); const [activeResultIndex, setActiveResultIndex] = useState(0); const searchableItems = useAvailableSettingsSearchItems(); const results = useMemo(() => searchSettings(query, searchableItems), [query, searchableItems]); const isSearching = query.trim().length > 0; const hasResults = results.length > 0; useEffect(() => { setActiveResultIndex((index) => Math.min(index, Math.max(results.length - 1, 0))); }, [results.length]); useEffect(() => { const result = results[activeResultIndex]; if (!result) return; document .getElementById(`settings-search-result-${result.id}`) ?.scrollIntoView({ block: "nearest" }); }, [activeResultIndex, results]); useEffect(() => { const handleKeyDown = (event: globalThis.KeyboardEvent) => { if (event.key !== "/" || event.metaKey || event.ctrlKey || event.altKey) return; const target = event.target; if ( target instanceof HTMLElement && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable || // Keep focus inside open dialogs and popups instead of escaping // their focus trap into the sidebar search. target.closest('[role="dialog"], [aria-modal="true"], [data-slot$="popup"]') !== null) ) { return; } event.preventDefault(); if (isMobile) { setOpenMobile(true); } else if (!open) { setOpen(true); } requestAnimationFrame(() => { searchInputRef.current?.focus(); searchInputRef.current?.select(); }); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [isMobile, open, setOpen, setOpenMobile]); const handleSectionClick = useCallback( (to: SettingsPath) => { if (isMobile) { setOpenMobile(false); } void navigate({ to, hash: "", replace: true, hashScrollIntoView: false, }); }, [isMobile, navigate, setOpenMobile], ); const clearSearch = useCallback(() => { setQuery(""); setActiveResultIndex(0); }, []); const handleSearchResultClick = useCallback( (item: SettingsSearchItem) => { clearSearch(); if (isMobile) { setOpenMobile(false); } const targetId = item.targetId ?? item.id; if ( item.to !== "/settings/projects" && pathname === item.to && currentHash.replace(/^#/, "") === targetId ) { scrollToSettingsTarget(targetId); return; } void navigate({ to: item.to, search: (previous) => item.to === "/settings/projects" ? { ...previous, project: undefined } : previous, hash: targetId, replace: true, hashScrollIntoView: false, state: { settingsTargetHighlight: true }, }); }, [clearSearch, currentHash, isMobile, navigate, pathname, setOpenMobile], ); const handleSearchKeyDown = useCallback( (event: KeyboardEvent) => { if (event.key === "Escape" && isSearching) { event.preventDefault(); event.stopPropagation(); clearSearch(); return; } if (results.length === 0) return; if (event.key === "ArrowDown") { event.preventDefault(); setActiveResultIndex((index) => (index + 1) % results.length); return; } if (event.key === "ArrowUp") { event.preventDefault(); setActiveResultIndex((index) => (index - 1 + results.length) % results.length); return; } if (event.key === "Enter") { event.preventDefault(); const result = results[activeResultIndex]; if (result) handleSearchResultClick(result); } }, [activeResultIndex, clearSearch, handleSearchResultClick, isSearching, results], ); return ( <>
{ setQuery(event.currentTarget.value); setActiveResultIndex(0); }} onKeyDown={handleSearchKeyDown} placeholder="Search" aria-label="Search settings" role="combobox" aria-autocomplete="list" aria-expanded={isSearching && hasResults} aria-controls={isSearching && hasResults ? "settings-search-results" : undefined} aria-activedescendant={ isSearching && results[activeResultIndex] ? `settings-search-result-${results[activeResultIndex].id}` : undefined } className="min-w-0 flex-1 [&_[data-slot=input]]:h-auto [&_[data-slot=input]]:p-0 [&_[data-slot=input]]:leading-normal [&_[data-slot=input]]:text-sm [&_[data-slot=input]]:font-medium [&_[data-slot=input]]:text-sidebar-foreground [&_[data-slot=input]]:placeholder:text-sidebar-muted-foreground" /> {isSearching ? ( ) : ( / )}
{isSearching && results.length === 0 ? (

No settings found

) : null} {isSearching ? ( {results.map((item, index) => ( setActiveResultIndex(index)} onClick={() => handleSearchResultClick(item)} > {item.title} {SETTINGS_SECTION_LABELS[item.to]} ))} ) : ( {SETTINGS_NAV_ITEMS.map((item) => { const Icon = item.icon; const isActive = pathname === item.to || pathname.startsWith(`${item.to}/`); return ( handleSectionClick(item.to)} > {item.label} ); })} )}
); }