import type { ComponentType, ReactNode } from 'react'; import { useState } from 'react'; import { NavLink, Navigate, useParams } from 'react-router'; import type { LucideIcon } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { useTranslation, type AppTranslationKey } from '@/lib/i18n'; /** * Sections are declared as module-level consts holding JSX `content`, so they cannot call a hook — * they carry catalog KEYS and the renderers below resolve them. `titleKey` rather than `titleKeyName` * and deliberately not `title_key`: docs-check-settings.ts greps `key:\s*'…'` for route segments, and * a capital K keeps `titleKey` out of that match. */ export type SettingsSection = { key: string; icon: LucideIcon; titleKey: AppTranslationKey; descriptionKey: AppTranslationKey; content: ReactNode; }; export type SettingsSectionGroup = { labelKey: AppTranslationKey; icon: LucideIcon; sections: SettingsSection[]; }; type SettingsSidebarProps = { basePath: string; icon: LucideIcon; labelKey: AppTranslationKey; sections: SettingsSection[]; groups?: SettingsSectionGroup[]; hideHeader?: boolean; }; /** * A real link, not a button with an onClick. The section is in the URL, so cmd-click opens it in a tab, * the browser knows it has been visited, and `isActive` comes from the router instead of from a global * that the content pane had to be told about separately. */ const SectionLink = ({ section, to }: { section: SettingsSection; to: string }) => { const { t } = useTranslation(); return ( `flex items-start gap-2.5 py-2 px-3 rounded-lg text-left cursor-pointer transition-colors ${ isActive ? 'bg-duck-teal/10 text-duck-dark dark:text-foreground' : 'text-duck-dark/70 dark:text-foreground/70 hover:bg-duck-dark/5 dark:hover:bg-foreground/5 hover:text-duck-dark dark:hover:text-foreground' }` } > {({ isActive }) => ( <>
{t(section.titleKey)}
{t(section.descriptionKey)}
)}
); }; export const SettingsSidebar = ({ basePath, icon: Icon, labelKey, sections, groups, hideHeader, }: SettingsSidebarProps) => { const [search, setSearch] = useState(''); const { t } = useTranslation(); const query = search.toLowerCase(); // Matched against the RESOLVED words, so searching works in the language on screen. const matchesSearch = (s: SettingsSection) => t(s.titleKey).toLowerCase().includes(query) || t(s.descriptionKey).toLowerCase().includes(query); return (
{!hideHeader && (
{t(labelKey)}
)}
setSearch(ev.target.value)} className="h-8 text-xs" />
{groups ? groups.map((group) => { const filtered = group.sections.filter(matchesSearch); if (filtered.length === 0) return null; return (
{t(group.labelKey)}
{filtered.map((s) => ( ))}
); }) : sections .filter(matchesSearch) .map((s) => )}
); }; type SettingsContentProps = { sections: SettingsSection[]; }; export const SettingsContent = ({ sections }: SettingsContentProps) => { const { t } = useTranslation(); // The sidebar and this pane are two sibling panels that used to agree via a shared `useGlobal` key. // They agree via the URL now, so neither has to be told about the other — and neither can be the one // that seeds the slot, which is what made the old defaulting order matter (see §5.6). const { section: selectedKey } = useParams<{ section?: string }>(); const section = sections.find((s) => s.key === selectedKey); if (!section) { return (

{t('settings.selectSection')}

); } return (

{t(section.titleKey)}

{t(section.descriptionKey)}

{section.content}
); }; /** * The one guard each settings screen renders, per the route-pair convention: it canonicalises both the * bare page route and a section that does not exist, so `/settings/ai` and `/settings/ai/nonsense` both * land on the first section instead of on "Select a section". * * Place it after the screen's hooks — it returns early. */ type SettingsRouteProps = { basePath: string; sections: SettingsSection[]; children: ReactNode }; export const SettingsRoute = ({ basePath, sections, children }: SettingsRouteProps) => { const { section } = useParams<{ section?: string }>(); const first = sections[0]; if (first && !sections.some((s) => s.key === section)) return ; return <>{children}; }; type CreateSettingsPanelParams = { basePath: string; sidebarIcon: LucideIcon; sidebarLabelKey: AppTranslationKey; sections?: SettingsSection[]; groups?: SettingsSectionGroup[]; }; export const createSettingsPanelComponents = ({ basePath, sidebarIcon, sidebarLabelKey, sections = [], groups, }: CreateSettingsPanelParams) => { const allSections = groups ? groups.flatMap((g) => g.sections) : sections; const Sidebar: ComponentType = () => ( ); const Content: ComponentType = () => ; return { Sidebar, Content, allSections }; };