import { useT } from '@app/i18n' import { type Icon, IconExternalLink as SquareArrowOutUpRight, IconX as X } from '@tabler/icons-react' import { Fragment, type ReactNode, useEffect, useRef, useState } from 'react' import { Link } from 'react-router-dom' export interface SettingsSection { id: string label: string /** Row glyph, mirroring the reference settings dialog. */ icon?: Icon /** Rows sharing a group sit under one small caption. */ group?: string /** Rows that leave the dialog for a full page instead of switching panes. */ href?: string } export interface SettingsModalShellProps { /** Nav heading, e.g. 用户设置 / 社区设置. Also the dialog's accessible name. */ title: string sections: SettingsSection[] active: string onSelect: (id: string) => void onClose: () => void children: ReactNode /** Replaces the section list while a section runs its own sub-navigation. */ navOverride?: ReactNode /** Swap key for the content pane (defaults to the active section id). */ contentToken?: string } /** * The large two-column settings overlay shared by every settings surface, so a * community's settings cannot drift away from the user's own in size or spacing. * * The backdrop deliberately does NOT close: these forms hold unsaved edits * across many sections, and click-away is far too lossy to be the dismiss path. */ export function SettingsModalShell({ title, sections, active, onSelect, onClose, children, navOverride, contentToken, }: SettingsModalShellProps) { const { t } = useT() useEffect(() => { const onKey = (event: KeyboardEvent) => { if (event.key === 'Escape') onClose() } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) }, [onClose]) const activeSection = sections.find((item) => item.id === active && !item.href) ?? sections.find((item) => !item.href) ?? sections[0] return (

{activeSection?.label ?? title}

{children}
) } /** * Cross-fades a swapped subtree: the outgoing tree is parked on top while the * replacement fades in, so the pane never flashes empty and never shifts. */ function FadeSwap({ token, className, children }: { token: string; className?: string; children: ReactNode }) { const [current, setCurrent] = useState<{ token: string; node: ReactNode }>({ token, node: children }) const [outgoing, setOutgoing] = useState<{ token: string; node: ReactNode } | null>(null) const frame = useRef(null) const shown = useRef({ token, node: children }) const exitTimer = useRef(undefined) useEffect(() => { const previous = shown.current shown.current = { token, node: children } setCurrent(shown.current) // Same view: only refresh the children (async data, edits) — no fade. if (token === previous.token) return setOutgoing(previous) frame.current?.scrollTo({ top: 0 }) window.clearTimeout(exitTimer.current) exitTimer.current = window.setTimeout(() => setOutgoing(null), 220) }, [token, children]) useEffect(() => () => window.clearTimeout(exitTimer.current), []) return (
{outgoing ? (
{outgoing.node}
) : null}
{current.node}
) }