import { lazy, Suspense, useEffect, useRef, useState, type ComponentType, type KeyboardEvent, } from "react"; import { ArchiveIcon, FlaskConicalIcon, BotIcon, createLucideIcon, CalendarClockIcon, FileDiffIcon, FolderTreeIcon, GitBranchIcon, GlobeIcon, InfoIcon, MessageSquareTextIcon, MessagesSquareIcon, PanelsTopLeftIcon, KeyboardIcon, KeyRoundIcon, PaletteIcon, SearchIcon, ServerIcon, Settings2Icon, ShieldCheckIcon, SmartphoneIcon, WrenchIcon, XIcon, } from "lucide-react"; import { useLocation, useNavigate } from "@tanstack/react-router"; import { cn } from "../../lib/utils"; import { Button } from "../ui/button"; import { Kbd } from "../ui/kbd"; import { SidebarContent, SidebarFooter, SidebarGroup, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarInput, } from "../ui/sidebar"; import { scrollToSettingsTarget } from "./settingsLayout"; import { searchSettings, revealSettingsSearchTarget, SETTINGS_NAV_GROUPS, SETTINGS_SECTION_LABELS, type SettingsPath, type SettingsSearchItem, } from "./settingsSearch"; import { useAvailableSettingsSearchItems } from "./useAvailableSettingsSearchItems"; import { validateSettingsScopeSearch } from "./settingsScope"; import { useSettingsSectionNavigation } from "./useSettingsSectionNavigation"; import { workspaceAreaProps } from "../../lib/workspaceFocus"; 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/diagnostics": WrenchIcon, "/settings/appearance": PaletteIcon, "/settings/chat": MessageSquareTextIcon, "/settings/diffs": FileDiffIcon, "/settings/projects": PanelsTopLeftIcon, "/settings/keybindings": KeyboardIcon, "/settings/snap-shot": SnapShotIcon, "/settings/providers": BotIcon, "/settings/threads": MessagesSquareIcon, "/settings/browser": GlobeIcon, "/settings/devices": SmartphoneIcon, "/settings/scheduled-tasks": CalendarClockIcon, "/settings/source-control": GitBranchIcon, "/settings/worktrees": FolderTreeIcon, "/settings/connections": ServerIcon, "/settings/access": ShieldCheckIcon, "/settings/secrets": KeyRoundIcon, "/settings/maintenance": WrenchIcon, "/settings/labs": FlaskConicalIcon, "/settings/about": InfoIcon, "/settings/archived": ArchiveIcon, }; // Detail pages without a nav entry highlight the page that links to them. const SETTINGS_DETAIL_PAGE_PARENTS: Readonly> = { "/settings/diagnostics": "/settings/maintenance", "/settings/open-source-licenses": "/settings/about", }; // Attached only to the highlighted result, so it runs whenever the highlight lands on a row. function scrollResultIntoView(row: HTMLElement | null) { row?.scrollIntoView({ block: "nearest" }); } function SettingsSectionIcon({ to }: { to: SettingsPath }) { const Icon = SETTINGS_SECTION_ICONS[to]; return ; } export function SettingsSidebarNav({ pathname }: { pathname: string }) { const navArea = workspaceAreaProps("settings-nav", 0, "Settings sections"); const navigate = useNavigate(); const currentHash = useLocation({ select: (location) => location.hash }); const currentSearch = useLocation({ select: (location) => location.search }); const scopeSearch = validateSettingsScopeSearch(currentSearch); const activePath = SETTINGS_DETAIL_PAGE_PARENTS[pathname] ?? pathname; const navPaths = SETTINGS_NAV_GROUPS.flatMap((group) => group.paths); const sectionNavigation = useSettingsSectionNavigation( navPaths.find((to) => activePath === to || activePath.startsWith(`${to}/`)) ?? navPaths[0] ?? "", ); const searchInputRef = useRef(null); const [query, setQuery] = useState(""); // Below md the nav is an icon rail; searching expands it over the page. const [compactSearchOpen, setCompactSearchOpen] = useState(false); const [activeResultIndex, setActiveResultIndex] = useState(0); const searchableItems = useAvailableSettingsSearchItems(scopeSearch); const results = searchSettings(query, searchableItems); const isSearching = query.trim().length > 0; const hasResults = results.length > 0; const maxResultIndex = Math.max(results.length - 1, 0); // Persist the clamp so a shrink-then-recover of results keeps the clamped highlight. if (activeResultIndex > maxResultIndex) { setActiveResultIndex(maxResultIndex); } // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- window keydown listener for the find and slash search shortcuts useEffect(() => { const handleKeyDown = (event: globalThis.KeyboardEvent) => { if (event.defaultPrevented || event.isComposing) return; const isFindShortcut = event.key.toLowerCase() === "f" && (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey; const isSlashShortcut = event.key === "/" && !event.metaKey && !event.ctrlKey && !event.altKey; if (!isFindShortcut && !isSlashShortcut) return; const target = event.target; if ( target instanceof HTMLElement && ((isSlashShortcut && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) || target.closest( '[role="dialog"]:not([data-app-modal]), [aria-modal="true"]:not([data-app-modal]), [data-slot$="popup"]:not([data-app-modal]), [data-keybinding-capture]', ) !== null) ) { return; } event.preventDefault(); setCompactSearchOpen(true); searchInputRef.current?.focus(); searchInputRef.current?.select(); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, []); // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- focuses the search input DOM node after the compact search opens useEffect(() => { if (compactSearchOpen) searchInputRef.current?.focus(); }, [compactSearchOpen]); const handleSectionClick = (to: SettingsPath) => { void navigate({ to, hash: "", replace: true, hashScrollIntoView: false, }); }; const clearSearch = () => { setQuery(""); setActiveResultIndex(0); }; const handleSearchResultClick = (item: SettingsSearchItem) => { clearSearch(); setCompactSearchOpen(false); const targetId = item.targetId ?? item.id; revealSettingsSearchTarget(targetId); if (pathname === item.to && currentHash.replace(/^#/, "") === targetId) { scrollToSettingsTarget(targetId); return; } void navigate({ to: item.to, hash: targetId, replace: true, hashScrollIntoView: false, state: { settingsTargetHighlight: true }, }); }; const handleSearchKeyDown = (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((activeResultIndex + 1) % results.length); return; } if (event.key === "ArrowUp") { event.preventDefault(); setActiveResultIndex((activeResultIndex - 1 + results.length) % results.length); return; } if (event.key === "Enter") { event.preventDefault(); const result = results[activeResultIndex]; if (result) handleSearchResultClick(result); } }; return ( ); }